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

目录
阻塞IO
非阻塞 IO
select
epoll
总结一下 。
阻塞IO服务端为了处理客户端的连接和请求的数据,写了如下代码 。

  1. listenfd = socket();// 打开一个网络通信端口
  2. bind(listenfd);// 绑定
  3. listen(listenfd);// 监听
  4. while(1) {
  5. connfd = accept(listenfd);// 阻塞建立连接
  6. int n = read(connfd, buf);// 阻塞读数据
  7. doSomeThing(buf);// 利用读到的数据做些什么
  8. close(connfd);// 关闭连接,循环等待下一个连接
  9. }
这段代码会执行得磕磕绊绊,就像这样 。
IO多路复用的理解/演变过程

文章插图
可以看到,服务端的线程阻塞在了两个地方,一个是 accept 函数,一个是 read 函数 。
如果再把 read 函数的细节展开 , 我们会发现其阻塞在了两个阶段 。
IO多路复用的理解/演变过程

文章插图
这就是传统的阻塞 IO 。
整体流程如下图 。
IO多路复用的理解/演变过程

文章插图
所以,如果这个连接的客户端一直不发数据,那么服务端线程将会一直阻塞在 read 函数上不返回,也无法接受其他客户端连接 。
这肯定是不行的 。
非阻塞 IO为了解决上面的问题,其关键在于改造这个 read 函数 。
有一种聪明的办法是,每次都创建一个新的进程或线程,去调用 read 函数,并做业务处理 。
  1. while(1) {
  2. connfd = accept(listenfd);// 阻塞建立连接
  3. pthread_create(doWork);// 创建一个新的线程
  4. }
  5. void doWork() {
  6. int n = read(connfd, buf);// 阻塞读数据
  7. doSomeThing(buf);// 利用读到的数据做些什么
  8. close(connfd);// 关闭连接,循环等待下一个连接
  9. }
这样,当给一个客户端建立好连接后,就可以立刻等待新的客户端连接 , 而不用阻塞在原客户端的 read 请求上 。
IO多路复用的理解/演变过程

文章插图
不过 , 这不叫非阻塞 IO,只不过用了多线程的手段使得主线程没有卡在 read 函数上不往下走罢了 。操作系统为我们提供的 read 函数仍然是阻塞的 。
所以真正的非阻塞 IO,不能是通过我们用户层的小把戏 , 而是要恳请操作系统为我们提供一个非阻塞的 read 函数 。
这个 read 函数的效果是 , 如果没有数据到达时(到达网卡并拷贝到了内核缓冲区),立刻返回一个错误值(-1),而不是阻塞地等待 。
操作系统提供了这样的功能,只需要在调用 read 前,将文件描述符设置为非阻塞即可 。
  1. fcntl(connfd, F_SETFL, O_NONBLOCK);
  2. int n = read(connfd, buffer) != SUCCESS);
这样 , 就需要用户线程循环调用 read , 直到返回值不为 -1,再开始处理业务 。
IO多路复用的理解/演变过程

文章插图
这里我们注意到一个细节 。
非阻塞的 read,指的是在数据到达前,即数据还未到达网卡,或者到达网卡但还没有拷贝到内核缓冲区之前,这个阶段是非阻塞的 。     
当数据已到达内核缓冲区,此时调用 read 函数仍然是阻塞的,需要等待数据从内核缓冲区拷贝到用户缓冲区,才能返回 。
整体流程如下图
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多路复用的理解/演变过程

推荐阅读