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


也就是说这不是真正意义上的非阻塞IO 。
IO 多路复用
为每个客户端创建一个线程,服务器端的线程资源很容易被耗光 。

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

文章插图
当然还有个聪明的办法,我们可以每 accept 一个客户端连接后,将这个文件描述符(connfd)放到一个数组里 。
fdlist.add(connfd);然后弄一个新的线程去不断遍历这个数组,调用每一个元素的非阻塞 read 方法 。
  1. while(1) {
  2. for(fd <-- fdlist) {
  3. if(read(fd) != -1) {
  4. doSomeThing();
  5. }
  6. }
  7. }
这样,我们就成功用一个线程处理了多个客户端连接 。
IO多路复用的理解/演变过程

文章插图
你是不是觉得这有些多路复用的意思?
但这和我们用多线程去将阻塞 IO 改造成看起来是非阻塞 IO 一样,这种遍历方式也只是我们用户自己想出的小把戏,每次遍历遇到 read 返回 -1 时仍然是一次浪费资源的系统调用 。
所以,还是得恳请操作系统老大,提供给我们一个有这样效果的函数,我们将一批文件描述符通过一次系统调用传给内核,由内核层去遍历(而不是在用户态调用,再陷入到内核态中去遍历),才能真正解决这个问题 。
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. }
可以看出几个细节:

经验总结扩展阅读