1. Linux IO简介
1.1 IO概述
IO是Input、Output的简称,也就是输入和输出。在计算机世界中,除了计算和存储,剩下的几乎都是IO,它包含硬件设备层面的数据交换,也包含软件层面的数据传输。
1.2 Linux IO方式
Linux IO方式表示Linux系统处理IO事件的方式,IO事件包括可读事件、可写事件、错误事件等。Linux IO方式主要包括:
- 阻塞IO:在IO事件发生之前,会一直阻塞。
- 非阻塞IO:如果没有IO事件,直接返回错误。
- IO多路复用:同时监听多个描述符的IO事件,取出状态ready的描述符列表。
- 异步IO:为IO事件绑定处理程序,当事件发生时触发处理程序。
- 信号驱动IO:利用Linux的信号机制,当IO事件发生时,触发信号处理程序。
- 等。
2. IO复用
2.1 概念
IO复用(I/O Multiplexing)通俗的来说:是同时处理多个描述符IO事件的一种技术手段。这些文件描述符包括:socket套接字、普通文件、设备文件等。
举个简单例子:tcp server同时处理两个文件描述符,一个是标准输入,一个是tcp连接。当server接收标准输入时,可能会因调用fgets()
而阻塞,从而无法及时处理另一个tcp连接的可读事件,比如有tcp client发送了数据。
如果tcp server想要同时处理多个描述符的事件,可能的做法是开启多个线程或进程,各自等待描述符的可读可写事件,但这样一来,就需要引入线程间同步和通信问题,大大增加编程的复杂性。
但IO复用技术的出现,可以很好的解决上述问题,它直接管理多个描述符,选出IO事件ready的描述符列表。这种操作方式允许应用程序以较低的成本、较高的效率,同时管理多个描述符的IO事件。
在Linux中,以下接口可以实现IO多路复用:
- select
- poll
- epoll
2.2 select
select函数的函数签名如下:
1 | int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout); |
它监听一组描述符,并在指定时间内选出可读、可写或者异常的描述符列表。参数介绍如下;
- timeout:select函数执行的时间,在这个时间范围内选出ready的描述符列表。
- readfds、writefds、exceptfds:关心的描述符集合的指针,包括可读、可写、异常的描述符集合。可以通过
FD_ISSET
宏来判断描述符是否ready。 - nfds:监听的描述符集合的最大值+1,类似于描述符范围的上限。
大致处理过程:
- 将用户空间的fdsets拷贝到内核空间,并分配结果buffer。
- 遍历fdsets所有fd,找到对应的文件对象file指针,通过poll()检查描述符的事件是否满足条件。
- 检查超时、遍历完后,将满足条件的fd放入结果buffer中。
- 拷贝结果到用户空间,并释放之前申请的内存。
2.3 poll
poll和select的原理没啥本质不同,只是实现的方式更加优雅,封装了很多细节。其函数签名如下:
1 | #include <poll.h> |
将监听的描述符以数组的方式传入,并为每个描述符绑定了监听事件。
2.4 epoll
select和poll在描述符集合量级较小的情况下,性能表现还可以,但如果监听的描述符集合太大,就需要考虑epoll了。epoll的用法如下:
- 创建epoll的句柄,size是监听描述符的个数。
1 | int epoll_create(int size); |
- 对监听的描述符操作
1 | int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); |
参数解释如下:
- epfd:
epoll_create
返回的句柄 - op: 标识操作类型
- EPOLL_CTL_ADD: 添加
- EPOLL_CTL_DEL: 删除
- EPOLL_CTL_MD: 修改
- fd: 文件描述符
- event: 需要监听的事件。
- 监听事件的发生
1 | int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout); |
- epfd:等待epfd上的io事件,最多返回maxevents个事件
- events:用来从内核得到事件的集合
- maxevents:events数量,该maxevents值不能大于创建epoll_create()时的size
- timeout:超时时间(毫秒,0会立即返回)
epoll有两种工作模式
- LT(水平触发,默认):当时间发生时,如果用户不处理的话,内核会一直通知此事件。
- ET(垂直触发):只有当事件状态发生改变时,内核才会通知。(如果描述符缓冲区还有未处理的数据,内核下次不会再通知了,因为状态没有发生改变)
epoll的工作处理流程,大致如下:
- 当新增描述符监听时,也就是调用EPOLL_CTL_ADD,内核会将初始化一个
epitem
(保存fd、监听事件等等),插入到红黑树中,并将回调函数ep_poll_callback
通过vfs_poll
注册到描述符的等待队列中。 - 当描述符ready后,会调用回调函数
ep_poll_callback
,它会将描述符对应的epitem
拷贝到rdllist
中。 - 调用
epoll_wait
其实是检查rdllist
的过程。
2.5 select vs epoll
从工作原理中,也可以看出select与epoll的诸多不同,优缺点对比如下:
- 支持监听的描述符数量
- select支持的描述符数量受限:因为需要遍历描述符列表、多次用户空间到内核空间的内存拷贝。
- epoll可以支撑监听海量的描述符。
- 性能
- select随着描述符数量的增多,性能下降严重,时间复杂度 > O(n)。
- epoll通过回调函数的方式,在描述符增长的情况下,依旧有出色的性能。
- 使用场景
- select使用方式较为简单,适用于描述符量级小的情况。
- epoll适用于同时监听大量的描述符。
3. 总结
IO模式多种多样,在软件开发过程中,不能唯一而论,要根据实际的应用场景,选择合适的IO技术,才能取得事半功倍的效果。