带你搞透IO多路复用原理(select、poll和epoll)

IO多路复用是什么?

        IO多路复用是指使用单个线程同时处理多个IO请求。在IO多路复用模型中一个线程可以监视多个文件描述符(fd),一旦某个fd就绪(读/写就绪)或者超时,就能够通知应用程序进行相应的读写操作。IO多路复用使线程不会阻塞在某一个特定的IO请求上,而是不断地去对内核通知的请求进行处理,其具体实现方式有select、poll和epoll三种。

select

        废话不多说先上源码(部分)

/* 创建socket监听 */
sockfd = socket(AF_INET, SOCK_STREAM, 0);
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(2000);
addr.sin_addr.s_addr = INADDR_ANY;
bind(sockfd, (struct sockaddr*)&addr, sizeof(addr));
listen(sockfd, 5);

/* 接受5个客户端连接 */
int fds[5]; // fd 数组
for (i=0; i<5; i++)
{
    memset(&client, 0, sizeof(client));
    addrlen = sizeof(client);
    fds[i] = accept(sockfd, (struct sockaddr*)&client, &addrlen);
    if (fds[i] > max)
        max = fds[i]; // 记录最大fd数值
}
/* ============================================================= */
while (1) 
{
    FD_ZERO(&rset); // 重置&rset
    for (i=0; i<5; i++) 
    {
    	FD_SET(fds[i], &rset);    
    }
    
    puts("round again");
    select(max+1, &rset, NULL, NULL, NULL); // 主角select
    
    for (i=0; i<5; i++)
    {
        if (FD_ISSET(fds[i], &rset))
        {
            memset(buffer, 0, MAXBUF);
            read(fds[i], buffer, MAXBUF);
            puts(buffer); // 业务处理逻辑
        }
    }
}

 这里我们分两部分来看,首先是横线上半部分,主要就是做了两件事:第一是创建了socket客户端,第二是创建了5个fd放入了数组内,并记录了最大值。

下半部分我们主要看select 系统调用,主要关注后面四个参数,分别是读文件描述集合,写文件描述集合和异常文件描述集合,我们一般关注读事件,所以这里后面三个传Null。这里的&rset其实是一个bitmap,用来表征哪一个文件描述符是被启用或者被监听的。这里的select会将这个bitmap从用户空间拷贝到内核空间,然后由内核来监视,是否有文件描述符就绪,这里是会阻塞的。一旦有就绪,进行后续的处理,遍历fd数组,看是否有fd被置位(标识为有数据来了),然后对被置位的fd进行相应的操作。

总结select过程:

  1. 每个连接都会返回一个fd,我们需要一个数组fds 记录所有连接的fd。
  2. 我们需要一个叫&rset 的数据结构,它是一个默认 1024 bit大小的一个位图bitmap结构,每个bit用来标识一个fd。
  3. 用户进程通过 select 系统调用把 &rset 结构的数据拷贝到内核,由内核来监视并判断哪些连接有数据到来,如果有连接准备好数据,select 系统调用就返回。
  4. select 返回后,用户进程只知道某个或某几个连接有数据,但并不知道是哪个连接。所以需要遍历 fds  中的每个 fd, 当该 fd被置位时,代表 该 fd表示的连接有数据需要被读取。然后我们读取该 fd的数据并进行业务操作。
  5. 重新置位 &rset  ,然后跳转到步骤 3 循环执行。

优缺点:

  1. 将监视fd是否就绪这一工作由用户态放到了内核态,效率更高
  2. 可监控的文件描述符数量最大为 1024 个,就代表最大能支持的并发为1024,这个是操作系统内核决定的。
  3. 用户进程的文件描述符集合&rset  每次都需要从用户进程拷贝到内核,有一定的性能开销。
  4. select 函数返回,我们只知道有文件描述符满足要求,但不知道是哪个,所以需要遍历所有文件描述符,复杂度为O(n)。

poll 

        poll是另一种I/O多路复用的实现方式,它与 select 的流程基本相似,时间复杂度是O(n),底层用链表存储fd,主要解决了 select 1024个文件描述符的限制问题,不过仍然存在文件描述符状态在用户态和内核态的频繁拷贝,和遍历所有文件描述符的问题,这导致了在面对高并发的实现需求时,它的性能不会很高。

epoll

         epoll 是 select 和 poll的增强版本,它更加灵活,没有描述符数量的限制,并且省去了大量文件描述符频繁在用户态和内核态之间拷贝的资源消耗,且时间复杂度为O(1),看代码:

struct epoll_event events[5];
int epfd = epoll_create(10);
// ...
for (i=0; i<5; i++)
{
    static struct epoll_event ev;
    memset(&client, 0, sizeof(client));
    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 (i=0; i<nfds; i++)
    {
        memset(buffer, 0, MAXBUF);
        read(events[i].data.fd, buffer, MAXBUF);
        puts(buffer);
    }
}

epoll_create() 方法生成一个 epoll 专用的文件描述符epfd,epfd在用户态和内核态之间共享,避免了poll和select用户态和内核态文件描述符状态的拷贝。

epoll_ctl() 方法是 epoll 的事件注册函数,告诉内核要监视的文件描述符和事件类型。

epoll_wait() 方法等待事件的产生,类似 select 调用 

epoll 底层使用了 RB-Tree 红黑树和 list 链表实现。内核创建了红黑树用于存储 epoll_ctl 传来的 socket,另外创建了一个 list 链表,用于存储准备就绪的事件 

epoll的执行流程:
1.当有数据的时候,会把相应的文件描述符“置位”,这时候会把有数据的文件描述符放到链表首部。
2.epoll会返回有数据的文件描述符的个数

3.根据返回的个数读取前N个文件描述符即可

4.读取、处理
 

epoll 相比于 select 和 poll,它更高效的本质在于:

  1. 减少了用户态和内核态文件描述符状态的拷贝,epoll 只需要一个专用的文件句柄即可;
  2. 减少了文件描述符的遍历,select 和 poll 每次都要遍历所有的文件描述符,用来判断哪个连接准备就绪;epoll 返回的是准备就绪的文件描述符,效率大大提高;
  3. 没有并发数量的限制,性能不会随文件描述符数量的增加而下降

 

 

  • 2
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

李孛欢

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值