多路io复用技术

多路io复用技术

1.应用场景

当一台服务处理大量连接请求的时候有两种高效的的方法:

  • 多线程:对于每个单独的连接都使用单独的线程处理
  • 多路io复用:监听多个连接,当有事件发生时立即处理

由于多线程带来的系统资源开销、上下文切换等问题现如今的常用技术是io多路复用

多路io复用在一个线程里处理,提高了该线程的利用率不会空闲等待

2.select

2.1概述

将监听的socket集合通过select传入内核,内核对select传入的socket集合进行遍历标记可读或者可写,然后将事件集合拷贝回用户态,用户态在进行遍历

2.2代码

sockfd = socket(AF_INET,SOCK_STREAM,0);
memset(&addr,0,sizeof(int));
addr.sin_family = AF_INET;
addr.sin_port = htons(2000);
addr.sin_addr.s_addr = INADDR_ANY;
bind(sockfd,(struct sockaddr*)&client,&addrlen);
listen(sockfd,5);



for(int i = 0;i < 5;++i){
    memset(&client,0,sizeof(client));
    addrlen = sizeof(client);
    ads[i] = accept(sockfd,(struct sockaddr*)&client,&addrlen);
    if(fds[i] > max)
        max = fds[i];
}



whlie(1){
    FD_ZERO(&rset);
    for(int i = 0;i < 5;++i){
        FD_SET(fds[i],&rset);
    }
    
    puts("round again");
    select(max+1,&rset,NULL,NULL,NULL);
    
    for(int i = 0;i < 5;++i){
        if(FD_ISSET(fds[i],&rset)){
            memset(buffer,0,MAXBUF);
            puts(buffer);
        }
    }
}

2.3仍存在的问题

  • 由于传入的端口集使用位图的数据结构故有大小限制

  • 数据由用户态拷贝到内核态的开销

  • rset需要每次重新初始化的开销

  • 事件处理逻辑O(n)的时间复杂度

3.poll

3.1概述

相对于select采用了结构体数组,没有了使用位图时监听端口的大小限制,其余跟select并无太大出入

3.2代码

struct pollfd{
    int fd;
    short events;
    short revents;
}
for(int i = 0;i < 5;++i){
    memeset(&client,0,sizoof(client));
    addrlen = sizeof(client);
    pollfds[i].fd = accept(sockfd,(struct sockaddr*) &client,&addrlen);
    pollfds[i].events = POLLIN;
}
sleep(1);
while(1){
    puts("round again");
    poll(pollfds,5,50000);
    
    for(int i = 0;i < 5;++i){
        if(pollfds[i].revents & POLLIN){
            pollfds[i].revents = 0;
            memset(buffer,0,MAXBUF);
            read(pollfds[i].fd,buffer,MAXBUF);
            puts(buffer);
        }
    }
}

3.3仍存在的问题

  • 数据从用户态拷贝到内核态的开销
  • O(n)时间复杂度的事件处理逻辑

4.epoll

4.1概述

系统的epoll内核采用红黑树,当我们注册epoll事件的时候就会进行红黑树的插入操作O(logn),而不用拷贝整个socket集合,节省了大量的拷贝和内存分配

在内核中会使用链表存储事件,epollwait() 返回时只会返回就绪的文件描述符的数量,这样用户就不用进行O(n)的遍历

epoll并不存在共享内存的概念,epoll会进行内核态数据拷贝到用户态这是普遍存在的错误观念

epollwait()的第二参数就传入在用户态开辟空间的epoll_event数组,如果是共享内存则应该需要设计为传入一个epoll_event内存指针即可

4.2代码

struct epoll_event events[5];
int epfd = epoll_create(10);
...
...
for(int i = 0;i < 5;++i){
    static struct epoll_event ev;
    memset(&client,0,sizeof(client));
    int addrlen = sizeof(client);
    ev.data.fd = accept(sockfd,(struct sockaddr*)&client,&addrlen);
    ev.events = EPOLLIN;
    epoll_ctl(epfd,EPOLL_CTL_ADD,ev.data.fd,&ev);
}
while(1){
    puts("round again");
    nfds = epoll_wait(epfd,events,5,10000);
    
    for(int i = 0;i < nfds;++i){
        memset(buffer,0,MAXBUF);
        puts(buffer);
    }
}

4.3 LT模式和ET模式

  • LT(边缘模式)当epoll内核监听的socket集合中有可读事件发生时,epollwait() 系统调用只会返回一次,即使用户没有调用read系统调用读取数据也只会返回一次,通常会搭配非阻塞socket使用,循环不断读取事件知道返回EAGAIN和EWOULDBLOCK状态码
  • ET(水平模式)当epoll内核监听的socket集合中有可读事件发生的时候,服务端会不断地从epollwait苏醒知道数据read()完毕才可以

边缘触发常常比水平触发效率更高因为,边缘触发减少了epoll_wait()系统调用的次数(系统调用需要开销)

参考资料