I/O 多路复用底层原理

前文:5种经典IO模型

  在非阻塞IO模型中,通过多线程遍历的方式来解决阻塞,但在每一个线程里都通过while循环中做系统调用是非常浪费资源的,因此可以统一将文件描述传给内核,由内核统一便利,返回结果给我们就行。其中select就是这样的方式,随着对select的优化又出现其他几种IO模型

多路复用IO模型有select,poll,epoll,kqueue(unix)

从前往后是在不断进步的

select:

  在使用select模型中,其中一个线程通过不断接受连接,将所有的文件描述符统一放入一个fd_list,另外一个用户线程在具体使用时就是通过select函数将fd_list传给内核,通过内核返回的fd_list再去遍历查找已经就绪的文件描述符,通过这种方式可以减少大量无意义的系统调用开销。

  其中使用select方式伪代码如下:

while(1) {
  nready = select(list);
  // 用户层依然要遍历,只不过少了很多无效的系统调用
  for(fd <-- fdlist) {
    if(fd != -1) {
      // 只读已就绪的文件描述符
      read(fd, buf);
      // 总共只有 nready 个已就绪描述符,不用过多遍历
      if(--nready == 0) break;
    }
  }
}

  但select主要缺陷是,对单个进程打开的文件描述是有一定限制的,它由FD_SETSIZE设置,默认值是1024,虽然可以通过编译内核改变,但相对麻烦,另外在检查数组中是否有文件描述需要读写时,采用的是线性扫描的方法,即不管这些socket是不是属于自己的,都轮询一遍(线性轮询),所以效率比较低。

select具体操作:

  • 1.select创建3个文件描述符集,并将这些文件描述符拷贝到内核中,这里限制了文件句柄的最大的数量为1024(注意是全部传入---第一次拷贝);
  • 2.内核针对读缓冲区和写缓冲区来判断是否可读可写,这个动作和select无关;
  • 3.内核在检测到文件句柄可读/可写时就产生中断通知监控者select,select被内核触发之后,就返回可读可写的文件句柄的总数;
  • 4.select会将之前传递给内核的文件句柄再次从内核传到用户态(第2次拷贝),select返回给用户态的只是可读可写的文件句柄总数,再使用FD_ISSET宏函数来检测哪些文件I/O可读可写(遍历);
  • 5.select对于事件的监控是建立在内核的修改之上的,也就是说经过一次监控之后,内核会修改位,因此再次监控时需要再次从用户态向内核态进行拷贝(第N次拷贝)

poll:

  poll本质和select没有区别,但其采用链表存储,解决了select最大连接数存在限制的问题,但其也是采用遍历的方式来判断是否有设备就绪,所以效率比较低,另外一个问题是大量的fd数组在用户空间和内核空间之间来回复制传递,也浪费了不少性能。

 

epoll&kqueue:

  在理解epoll之前可以考虑select和poll模型有哪些缺点?

  1.文件描述符集合的大量拷贝,不断从用户进程拷贝到内核,再从内核拷贝到用户进程,在高并发时这个问题更加明显?  

    针对这个问题能否通过在内核中维护一个公有的fd_list,之后就只有新增修改删除操作,不必copy

  2.select在内核中也同样是通过遍历的方式寻找文件描述符的就绪状态,依然是个通过过程?

    能否将这个同步过程转为异步通知

  3.select会统一返回所有的就绪fd,用户进程还需要遍历整个文件描述符集合?

    能否返回给用户进程自己的就绪文件描述符

   epoll就是为了解决这些问题而进行优化的

  epoll和kqueue是更先进的IO复用模型,其也没有最大连接数的限制(1G内存,可以打开约10万左右的连接),并且仅仅使用一个文件描述符,就可以管理多个文件描述符,并且将用户关系的文件描述符的事件存放到内核的一个事件表中(底层采用的是map的方式红黑树),这样在用户空间和内核空间的copy只需一次。另外这种模型里面,采用了类似事件驱动的回调机制或者叫通知机制,在注册fd时加入特定的状态,一旦fd就绪就会主动通知内核。这样以来就避免了前面说的无脑遍历socket的方法,这种模式下仅仅是活跃的socket连接才会主动通知内核,所以直接将时间复杂度降为O(1)。

  一个fd被添加到epoll中之后(EPOLL_ADD),内核会为它生成一个对应的epitem结构对象.epitem被添加到eventpoll的红黑树中.红黑树的作用是使用者调用EPOLL_MOD的时候可以快速找到fd对应的epitem。

epoll具体操作:

  • 1.首先执行epoll_create在内核专属于epoll的高速cache区,并在该缓冲区建立红黑树和就绪链表,用户态传入的文件句柄将被放到红黑树中(第一次拷贝)。
  • 2.内核针对读缓冲区和写缓冲区来判断是否可读可写,这个动作与epoll无关;
  • 3.epoll_ctl执行add动作时除了将文件句柄放到红黑树上之外,还向内核注册了该文件句柄的回调函数,内核在检测到某句柄可读可写时则调用该回调函数,回调函数将文件句柄放到就绪链表。
  • 4.epoll_wait只监控就绪链表就可以,如果就绪链表有文件句柄,则表示该文件句柄可读可写,并返回到对应的用户态(少量的拷贝);
  • 5.由于内核不修改文件句柄的位,因此只需要在第一次传入就可以重复监控,直到使用epoll_ctl删除,否则不需要重新传入,因此无多次拷贝。
  • 6.epoll是继承了select/poll的I/O复用的思想,并在二者的基础上从监控IO流、查找I/O事件等角度来提高效率,具体地说就是内核句柄列表、红黑树、就绪list链表来实现的。

 

posted @ 2021-12-30 16:04  LeeJuly  阅读(165)  评论(0编辑  收藏  举报