Linux I/O 复用

2022/09/16

Linux I/O 操作是Linux中非常重要的一点,现代计算机计算离不开各种各样的IO,比如设备IO,网络IO,文件IO,所以在进行Linux开发的时候需要理解各种IO模型,并选择最适合自己应用的模型。而目前大多数的高性能服务器需要处理大量的并发,它们一般采用IO复用模型,所以在本文中会重点介绍这种模型。

1. Linux I/O 模型

Linux读取输入的消息一般可以分为下面两步

  1. 等待数据就位
  2. 将数据从内核态拷贝到用户态

下面五种 I / O 模型都是根据在这两个阶段采用不同的措施来进行分类的

1.1 杜塞模型

Blocking I/O model

杜塞模型是最常用的一种模型,当应用发起请求,此时程序就会发送杜塞直到数据到达并拷贝到用户态才返回。

2.2 多路复用

I/O multiplexing model.

多路复用同堵塞模型一样,在请求的时候仍然会堵塞,不过它堵塞的不是单个文件描述符而是多个文件描述符所组成的集合,多路复用需要进行两次 system call , 第一次是告诉内核需要监听那些文件描述符,然后应用可以进行读取,如果数据就绪了就可以将其拷贝到用户态进行读取了,如果没有就绪则继续堵塞。在Linux中多路复用函数大致可以分为三类,select, poll, epoll, 在下面会对它们进行具体的介绍。

2.3 非杜塞模型

Nonblocking I/O model

非杜塞模型就是程序每次请求如果数据已经准备好了则将数据从内核态拷贝到用户态并返回,否则返回错误。

2.4 信号驱动模型

Signal-Driven I/O model

使用非堵塞模型过一段时间就需要检查数据是否就绪,需要耗费CPU性能,信号驱动模型则采用了一种更加优雅的方式来通知程序数据已经就位,那就是给程序发送就绪信号,当程序收到这个序号之后就可以将数据从内核缓存去中拷贝到用户缓冲区并返回了。

2.5 异步IO

Asynchronous I/O model.

异步模型与上面的信号驱动模型非常相似,唯一的不同点在于异步模型是自动将数据从内核态拷贝到用户态,然后再发送信号通知应用,而信号驱动模型则是当数据就绪之后就通知程序,然后程序发起接收数据才把数据从内核态拷贝到用户态。

2. 多路复用

Linux 中多路复用的模型有 select, poll, epoll 四种。

2.1 select

int select(int maxfdp1, fd_set *readfds, fd_set *writefds, 
           fd_set *exceptfds, struct timeval *timeout);

参数

返回值

返回值表示已经准备好了的文件描述符个数,如果返回值为零表示当所设置的时间超时了还没有任何文描述符返回,如果为 -1 则表示发生了错误

备注

select 监听的结果通过 bitmap 存储在readfds, writefds, exceptfds 中,如果对其中那个参数不感兴趣可以将其设置为 NULL

select 所能监听的文件描述符是有限的,最大限制保存在 FD_SETSIZE 宏定义中,默认的值为 1024,也就是说select 默认只能一次监听 1024 个客户端。

2.2 poll

int poll (struct pollfd *fds, unsigned int nfds, int timeout);

poll 中使用 pollfd 这个结构体数组来代替 select 所用的 bitmap, 这样既可以提高效率还没有了最高数量限制,但是和select 一样在返回之后任需要遍历结构体数组,结构体数组的定义如下

struct pollfd {
      int fd;
      short events; 
      short revents;
};

2.2 epoll

先复习下 epoll 的用法。如下的代码中,先用e poll_create 创建一个 epoll 对象 epfd,再通过 epoll_ctl将需要监视的 socket 添加到 epfd 中,最后调用 epoll_wait 等待数据。

int s = socket(AF_INET, SOCK_STREAM, 0);
bind(s, ...);
listen(s, ...)

int epfd = epoll_create(...);
epoll_ctl(epfd, ...); //将所有需要监听的socket添加到epfd中

while(1) {
    int n = epoll_wait(...);
    for(接收到数据的socket){
        //处理
    }
}

epoll 的性能是要优于 select 的,要搞清楚为什么 epoll 这么快,首先需要搞清楚为什么 select 会比较慢。select 需要将所有文件描述符一起放入内核中,当其中有文件描述符就绪之后 select 把文件描述符一起拷贝到用户态,然后用户应用再遍历文件描述符找到那个就绪的描述符,并读取相应数据。第一 select 在这里进行了两次拷贝,这种拷贝一般是不必须要的,epoll 则采用了红黑树来管理文件描述符,这种数据结构可以高效的插入与删除元素,无需要频繁的两边拷贝,全部放在内核中,如果需要删除或添加,速度也非常快。然后就是 select 需要遍历才能获取那个是已经就绪的文件描述符,epoll 则采用了一个就绪链表的结构表存储已经继续的文件描述符,当读取的时候自己从就绪链表上返回就可以了,不需要再进行遍历。