<>(IO复用等内容)2019-07-10

Linux 简要基础知识

  • 用户空间/内核空间
    操作系统的核心是内核,独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的所有权限。为了保证用户进程不能直接操作内核(kernel),保证内核的安全,操作系统将虚拟空间划分为两部分,一部分为内核空间,一部分为用户空间,内核功能模块运行在内核空间,而应用程序运行在用户空间.
  • 缓存I/O
    缓存I/O又称为标准I/O,大多数文件系统的默认I/O操作都是缓存I/O.在Linux的缓存I/O机制中,操作系统会将I/O的数据缓存在文件系统的页缓存中,即数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间.

1.I/O复用

I/O复用使得程序能同时监听多个文件描述符,可以有效提高程序的性能,但本身是阻塞的,所以当多个文件描述符同时就绪时,如果不采取额外措施,将只能按顺序依次处理其中的每一个文件描述符,如果要实现高并发,则只能通过多线程或者多进程来实现.

  • 先构造一张有关文件描述符的列表,将要监听的文件描述符添加到该表中.
  • 然后调用一个函数,监听该表中的文件描述符,直到这些描述符表中的一个进行I/O操作时,该函数才返回;其中该函数是阻塞函数,而且对文件描述符的检测操作是由内核完成的.

1.select(轮询):

相关函数:

#include 
int select (int nfds, fd_set* readfds, fd_set* writedfds, fd_set* exceptfds, struct timeval* timeout);
//如:
fd_set reads;
FD_ZERO(&reads);
FD_SET(lfd, &reads);
while(1)
{
      ret = select(maxfd+1, &reads, NULL, NULL, NULL);
}

---参数:
nfds:要检测的文件描述中最大的fd+1;
readfds,writedfds,exceptfds:分别指向读集合,写集合和异常事件的描述符集合;
timeout:用来设置select的超时时间,当设置为NULL时表示永久阻塞,或者进行设置如:

struct timeval a;
 a.tv_sec = 10;
 a.tv_usec = 0;

文件描述符操作函数:

- 全部清空
○ void FD_ZERO(fd_set *set);
- 从集合中删除某一项
○ void FD_CLR(int fd, fd_set *set);
- 将某个文件描述符添加到集合
○ void FD_SET(int fd, fd_set *set);
- 判断某个文件描述符是否在集合中
○ int FD_ISSET(int fd, fd_set *set);

select的优缺点:

  • 优点:跨平台
  • 缺点:
    1.每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大;
    2.同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大.
    3.底层通过数组的数据结构实现,且无法突破1024个文件描述符的限制.

2.poll(轮询):

相关函数:

#include 
int poll(struct pollfd* fds, nfds_t nfds, int timeout);//成功时返回就绪文件描述符的总数
//如:
for(int i=0; i<1024; ++i)
    {
        allfd[i].fd = -1;
    }
    allfd[0].fd = lfd;
    allfd[0].events = POLLIN;
while(1)
{
      int ret = poll(allfd, max_index+1, -1); 
}

---参数:
fds:是一种poll类型结构体数组, 指定文件描述符上发生的可读可写与异常事件;

struct pollfd {
int fd;/* 文件描述符 */
short events; /* 等待的事件 */
short revents; /* 实际发生的事件 */ - 内核给的反馈
};

nfds:数组的最大长度, 数组中最后一个使用的元素下标+1,每次系统内核都会轮询检测更新;
timeout:用来设置select的超时时间,当设置为-1时表示永久阻塞,0时会立即返回,>0时为等待的时长.
poll相比于select的优势在于poll可以突破文件描述符最大值1024的限制,且poll底层的实现采用的是内部链表数据结构.

3.epoll:

epoll是linux内核特有的复用函数,他会把用户关心的文件描述符的事件放在内核里的一个事件表上,无需像select和poll那样每次调用都要重复传入文件描述符集和事件集.但epoll需要一个额外的文件描述符,来唯一标示内核中的这个事件表,底层使用红黑树来完成.
相关函数:

#include 
int epoll_create(int size);//创建epoll句柄,相当于创建一个根
//或者int epoll_create1(EPOLL_CLOEXEC);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);//将accept后的文件描述符栓到树上
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);//在超时时间内等待一组文件描述符上的事件,成功时返回就绪的文件描述符个数,
//而poll与select会返回整个文件描述符数组,用户代码需要遍历数组以找到哪些文件描述符上有活动的IO事件

---参数:
epoll_ctl函数:
epfd:是要操作的epoll_create产生的句柄;
op:为要指定的操作:

  • EPOLL_CTL_ADD 注册新的fd到epfd中
  • EPOLL_CTL_MOD 修改已注册的fd的监听事件
  • EPOLL_CTL_DEL 从epfd中删除一个fd
    fd:为要监听的文件描述符;
    events:表示要监听的事件,如EPOLLIN(可读事件),EPOLLET(使用ET工作模式).
    epoll_wait函数(作用是当检测到事件时,将所有就绪的事件从内核事件表中复制到它的第二个参数events所指的数组中,只用于就绪事件,通知内核fd文件I/O检测):
    epfd:是要操作的epoll_create产生的句柄;
    events: 表示从内核得到的就绪事件集合;
    maxevents: 告诉内核events的大小,最多监听多少个事件;
    timeout:设置超时事件.

4.epoll的工作模式

1.水平触发模式(默认的模式)

  • 只要fd对应的缓冲区有数据,epoll_wait就会返回;
  • 返回次数与发送数据的次数没有关系.
  • 相当于一个效率较高的poll.

2.边沿触发模式

  • fd默认的阻塞属性;
  • 客户端给server发数据,发一次数据server的epoll_wait就返回一次,不在乎数据是否读完,在ET模式下,读事件触发,一次性把数据要是读不完,之后,就再不会触发ET时间了,这样会使套接字一直阻塞下去,读不到新数据;要是设置为非阻塞了,那我们在处理读时间处就应该设置循环,意思就是事件尽管触发一次,但读的次数由我来决定,我要读到套接字返回EAGAIN为止。这就是使用epoll的情况时,套接字设置为非阻塞的原因!
    epoll的使用ET的实例"
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

int main(int argc, const char* argv[])
{
    if(argc < 2)
    {
        printf("eg: ./a.out port\n");
        exit(1);
    }
    struct sockaddr_in serv_addr;
    socklen_t serv_len = sizeof(serv_addr);
    int port = atoi(argv[1]);

    // 创建套接字
    int lfd = socket(AF_INET, SOCK_STREAM, 0);
    // 初始化服务器 sockaddr_in 
    memset(&serv_addr, 0, serv_len);
    serv_addr.sin_family = AF_INET;                   // 地址族 
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);    // 监听本机所有的IP
    serv_addr.sin_port = htons(port);            // 设置端口 
    // 绑定IP和端口
    bind(lfd, (struct sockaddr*)&serv_addr, serv_len);

    // 设置同时监听的最大个数
    listen(lfd, 36);
    printf("Start accept ......\n");

    struct sockaddr_in client_addr;
    socklen_t cli_len = sizeof(client_addr);

    // 创建epoll树根节点
    int epfd = epoll_create(2000);
    // 初始化epoll树
    struct epoll_event ev;

    // 设置边沿触发
    ev.events = EPOLLIN;
    ev.data.fd = lfd;
    epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);

    struct epoll_event all[2000];
    while(1)
    {
        // 使用epoll通知内核fd 文件IO检测
        int ret = epoll_wait(epfd, all, sizeof(all)/sizeof(all[0]), -1);
        printf("================== epoll_wait =============\n");

        // 遍历all数组中的前ret个元素
        for(int i=0; i 0 )
                {
                    // 数据打印到终端
                    write(STDOUT_FILENO, buf, len);
                    // 发送给客户端
                    send(fd, buf, len, 0);
                }
                if(len == 0)
                {
                    printf("客户端断开了连接\n");
                    ret = epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
                    if(ret == -1)
                    {
                        perror("epoll_ctl - del error");
                        exit(1);
                    }
                    close(fd);
                }
                else if(len == -1)
                {
                    if(errno == EAGAIN)
                    {
                        printf("缓冲区数据已经读完\n");
                    }
                    else
                    {
                        printf("recv error----\n");
                        exit(1);
                    }
                }
            }
        }
    }

    close(lfd);
    return 0;
}

epoll使用LT模式仿muduo的实现的epoll(更规范,考虑的东西更多):

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#include 
#include 
#include 
#include 

#include 
#include 
#include 
using namespace std;
typedef std::vector EventList;

int main(void)
{
    signal(SIGPIPE, SIG_IGN); //忽略SIGPIPE信号
    signal(SIGCHLD, SIG_IGN);//忽略SIGCHLD信号,僵尸进程直接交给init进程处理

     //空闲fd
   // int idlefd = open("/dev/null",O_RDONLY | O_CLOEXEC);

    //创建套接字
    int lfd = socket(AF_INET,SOCK_STREAM | SOCK_NONBLOCK ,0);
    assert( lfd >= 0);
    cout << lfd << endl;
    struct sockaddr_in serv_addr;
    memset(&serv_addr,0,sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(8888);
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);

    //bind绑定
    int ret2 = bind(lfd,(struct sockaddr*)&serv_addr,sizeof(serv_addr));
    assert(ret2 != -1);

    listen(lfd,36);
    cout << "start accept......" << endl;
    
    vector clients;
    int epfd = epoll_create(5);

    struct epoll_event ev;
    ev.data.fd = lfd;
    ev.events = EPOLLIN;
    
    epoll_ctl(epfd,EPOLL_CTL_ADD,lfd,&ev);

    eventlist events(16);
    struct sockaddr_in client_addr;
    socklen_t cli_len;
    int connfd;
    int ret;
    while(1)
    {
        ret = epoll_wait(epfd,&*events.begin(),static_cast(events.size()),-1);
        assert(ret != -1);
//  cout << ret << endl;
        if(ret == 0)        //什么事没有
            continue;
        if((size_t)ret == events.size())     //扩展
            events.resize(events.size()*2);

        for(int i = 0;i

三种I/O复用方式的比较:


select:本质上是通过设置或者检查存放fd标志位的数据结构来进行下一步处理。这样所带来的缺点是:
(1) 单个进程可监视的fd数量被限制.
(2) 需要维护一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大.
(3) 对socket进行扫描时是线性扫描.
poll:本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态,如果设备就绪则在设备等待队列中加入一项并继续遍历,如果遍历完所有fd后没有发现就绪设备,则挂起当前进程,直到设备就绪或者主动超时,被唤醒后它又要再次遍历fd.这个过程经历了多次无谓的遍历.
它没有最大连接数的限制,原因是它是基于链表来存储的,但是同样有一个缺点:
大量的fd的数组被整体复制于用户态和内核地址空间之间,而不管这样的复制是不是有意义.
poll还有一个特点是“水平触发”,如果报告了fd后,没有被处理,那么下次poll时会再次报告该fd.
epoll:在前面说到的复制问题上,epoll使用mmap,共享内存减少复制开销。
还有一个特点是,epoll使用“事件”的就绪通知方式,通过epoll_ctl注册fd,一旦该fd就绪,内核就会采用类似callback的回调机制来激活该fd,epoll_wait便可以收到通知.

select:内核需要将消息传递到用户空间,都需要内核拷贝动作.
poll:同select.
epoll:epoll通过内核和用户空间共享一块内存来实现的.

你可能感兴趣的:(<>(IO复用等内容)2019-07-10)