1. 文件描述符
为了更好的介绍 Linux 的 IO 多路复用,我们需要对其文件描述符有一定的了解。
通过前面的介绍与学习,我们知道了,Linux 的每一个进程都对应的一个 PCB,而每个 PCB 中都维护了一个文件描述符表,这个表单维护了一组文件描述符,每个文件描述符都对应着一个文件指针,文件指针指向 OS 底层的打开文件表,打开文件表中维护着一个指向 i-node 表的指针。进程的文件描述符通过这两个表单来维护当前进程所有打开的文件。
通过上面的分析,我们可以知道,文件描述符表是进程私有的,打开文件表与i-node表是线程共享的。
另外,对于网络 IO 而言,每一个 socket 也可以抽象为一个文件描述符。
2. IO 多路复用的概念
首先说明一点, IO 多路复用中的 IO 指的是网络 IO。
在 Linux 的缓存IO机制(标准IO)中,操作系统会将IO的数据缓存在文件系统的页缓存中,即数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。
这样的话,数据在传输过程中就需要在应用程序地址空间和内核进行多次数据拷贝操作,带来很大的 CPU 以及内存开销。
IO 多路复用,多路是指网络连接,复用指的是同一个线程。一个线程可以监视多个文件描述符,一旦某个文件描述符就绪,就能够通知应用程序进行相应的读写操作。
3. IO 多路复用的三种实现形式
3.1 select
数据结构
// 数据结构 (bitmap)
typedef struct {
unsigned long fds_bits[__FDSET_LONGS];
} fd_set;
过程简述
-
每个进程都有自己的 fd_set 来维护自己的 fd,在 select 中,采用的固定大小的位图来维护;
-
当调用 select 命令时,会将 fd_set 从用户态复制到内核态,之后,内核会用轮询的方式确定每个 fd 的可用状态;
-
确定后,内核会把可以操作的 fd 的掩码以及数量返回给用户,用户再进行操作。
分析
- 单个进程所打开的 fd 是有限制的,通过
FD_SETSIZE设置,默认1024 。 - 每次调用 select,都需要把 fd 集合从用户态拷贝到内核态。
- 以轮训的方式确认 fd 的可用性,开销大。
3.2 poll
数据结构
struct pollfd {
int fd; // 需要监视的文件描述符
short events; // 需要内核监视的事件
short revents; // 实际发生的事件
};
过程简述
与 select 几乎没有区别,只是基于链表来存储 fd,没有了数量限制。
3.3 epoll
数据结构
struct eventpoll {
/*红黑树的根节点,这颗树中存储着所有添加到epoll中的需要监控的事件*/
struct rb_root rbr;
/*双链表中则存放着将要通过epoll_wait返回给用户的满足条件的事件*/
struct list_head rdlist;
};
struct epitem{
struct rb_node rbn;//红黑树节点
struct list_head rdllink;//双向链表节点
struct epoll_filefd ffd; //事件句柄信息
struct eventpoll *ep; //指向其所属的eventpoll对象
struct epoll_event event; //期待发生的事件类型
}
过程简述
- 每个事件都对应一个 epitem 结构体,会以节点的形式挂在红黑树上。
- 当事件可以被触发时,事件会被钩子函数挂到就绪队列上。
- 每次 epoll 命令只需要检查就绪队列就可以了。
LT 与 ET
epoll 有 LT 水平触发与 ET 垂直触发两种模式。
- LT 模式下,只要这个 fd 还有数据可读,每次 epoll_wait 都会返回它的事件,提醒用户程序去操作;
- ET 模式下,它只会提示一次,默认用户会处理。
4. 零拷贝
4.1 传统IO(resd/write)
在传统的IO场景下,如果我们想要把磁盘上的文件通过网络发送出去,需要先将磁盘文件拷贝到内核的缓存区,然后CPU拷贝到用户态缓存区,之后CPU拷贝到socker缓存区,最后再拷贝到网卡,进行发送。
在这期间,发生了 4 次用户态与内核态的上下文切换(每次CPU拷贝都是一次系统调用,一次系统调用有两次切换)。
另外,还发生了 4 次数据拷贝,其中两次是 DMA 的拷贝,另外两次是通过 CPU 拷贝的。
4.2 mmap/write
mmap 利用了虚拟内存的特性,用户空间与内核缓存公用一块内存,减少了一次CPU的拷贝。
但是在mmap的过程中,用户与内核态的切换次数仍然为4次,并没有改变。
4.3 sendfile
用户态向内核太发送sendfile()命令,用户态来完成三次复制过程,这样就减少了一次write()的两次上下文切换。
4.3 带有 scatter/gather 的 sendfile方式
Linux 2.4 提供了带有 scatter/gather 的 sendfile 操作,这个操作可以把最后一次 CPU COPY 去除。其原理就是在内核空间 Read BUffer 和 Socket Buffer 不做数据复制,而是将 Read Buffer 的内存地址、偏移量记录到相应的 Socket Buffer 中,这样就不需要复制。其本质和虚拟内存的解决方法思路一致,就是内存地址的记录。
4.4 splice
sendfile 只适用于将数据从文件拷贝到 socket 套接字上,同时需要硬件的支持,这也限定了它的使用范围。Linux 在 2.6.17 版本引入 splice 系统调用,不仅不需要硬件支持,还实现了两个文件描述符之间的数据零拷贝,虽然实现不尽相同,但是整理流程与上者基本一致,都是两次上下文切换,0次CPD拷贝,2次DMA拷贝。
我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿。