IO多路复用的理解/演变过程( 二 )


文章插图
你是不是觉得这有些多路复用的意思?
但这和我们用多线程去将阻塞 IO 改造成看起来是非阻塞 IO 一样,这种遍历方式也只是我们用户自己想出的小把戏,每次遍历遇到 read 返回 -1 时仍然是一次浪费资源的系统调用 。
【IO多路复用的理解/演变过程】所以,还是得恳请操作系统老大 , 提供给我们一个有这样效果的函数,我们将一批文件描述符通过一次系统调用传给内核 , 由内核层去遍历(而不是在用户态调用 , 再陷入到内核态中去遍历),才能真正解决这个问题 。
selectselect 是操作系统提供的系统调用函数,通过它,我们可以把一个文件描述符的数组发给操作系统,让操作系统去遍历 , 确定哪个文件描述符可以读写, 然后告诉我们去处理:

IO多路复用的理解/演变过程

文章插图
select系统调用的函数定义如下 。
  1. int select(
  2. int nfds,
  3. fd_set *readfds,
  4. fd_set *writefds,
  5. fd_set *exceptfds,
  6. struct timeval *timeout);
  7. // nfds:监控的文件描述符集里最大文件描述符加1
  8. // readfds:监控有读数据到达文件描述符集合 , 传入传出参数
  9. // writefds:监控写数据到达文件描述符集合 , 传入传出参数
  10. // exceptfds:监控异常发生达文件描述符集合, 传入传出参数
  11. // timeout:定时阻塞监控时间,3种情况
  12. //1.NULL,永远等下去
  13. //2.设置timeval , 等待固定时间
  14. //3.设置timeval里时间均为0,检查描述字后立即返回,轮询
服务端代码 , 这样来写 。
首先一个线程不断接受客户端连接,并把 socket 文件描述符放到一个 list 里 。
  1. while(1) {
  2. connfd = accept(listenfd);
  3. fcntl(connfd, F_SETFL, O_NONBLOCK);
  4. fdlist.add(connfd);
  5. }
然后 , 另一个线程不再自己遍历,而是调用 select,将这批文件描述符 list 交给操作系统去遍历 。
  1. while(1) {
  2. // 把一堆文件描述符 list 传给 select 函数
  3. // 有已就绪的文件描述符就返回,nready 表示有多少个就绪的
  4. nready = select(list);
  5. ...
  6. }
不过 , 当 select 函数返回后,用户依然需要遍历刚刚提交给操作系统的 list 。
只不过,操作系统会将准备就绪的文件描述符做上标识 , 用户层将不会再有无意义的系统调用开销 。
  1. while(1) {
  2. nready = select(list);
  3. // 用户层依然要遍历,只不过少了很多无效的系统调用
  4. for(fd <-- fdlist) {
  5. if(fd != -1) {
  6. // 只读已就绪的文件描述符
  7. read(fd, buf);
  8. // 总共只有 nready 个已就绪描述符,不用过多遍历
  9. if(--nready == 0) break;
  10. }
  11. }
  12. }
可以看出几个细节:
  1. select 调用需要传入 fd 数组 , 需要拷贝一份到内核,高并发场景下这样的拷贝消耗的资源是惊人的 。(可优化为不复制)
  2. select 在内核层仍然是通过遍历的方式检查文件描述符的就绪状态 , 是个同步过程,只不过无系统调用切换上下文的开销 。(内核层可优化为异步事件通知)
  3. select 仅仅返回可读文件描述符的个数,具体哪个可读还是要用户自己遍历 。(可优化为只返回给用户就绪的文件描述符 , 无需用户做无效的遍历)
整个 select 的流程图如下 。
IO多路复用的理解/演变过程

文章插图
可以看到,这种方式,既做到了一个线程处理多个客户端连接(文件描述符),又减少了系统调用的开销(多个文件描述符只有一次 select 的系统调用 + n 次就绪状态的文件描述符的 read 系统调用) 。
epollepoll 是最终的大 boss,它解决了 select 和 poll 的一些问题 。
还记得上面说的 select 的三个细节么?epoll 主要就是针对这三点进行了改进 。
  1. 内核中保存一份文件描述符集合,无需用户每次都重新传入 , 只需告诉内核修改的部分即可 。
  2. 内核不再通过轮询的方式找到就绪的文件描述符,而是通过异步 IO 事件唤醒 。
  3. 内核仅会将有 IO 事件的文件描述符返回给用户,用户也无需遍历整个文件描述符集合 。
使用起来,其内部原理就像如下一般丝滑 。
IO多路复用的理解/演变过程

推荐阅读