网络模型

2023-09-18 ⏳4.7分钟(1.9千字)

IO模型

阻塞 I/O 模型

阻塞 I/O 是最常见的 I/O 模型,在默认情况下,当我们通过 read 或者 write 等系统调用读写文件或者网络时,应用程序会被阻塞。

当我们执行 read 系统调用时,应用程序会从用户态陷入内核态,内核会检查文件描述符是否可读;当文件描述符中存在数据时,操作系统内核会将准备好的数据拷贝给应用程序并交回控制权。

非阻塞 I/O 模型

当进程把一个文件描述符设置成非阻塞时,执行 read 和 write 等 I/O 操作会立刻返回。

第一次从文件描述符中读取数据会触发系统调用并返回 EAGAIN 错误,EAGAIN 意味着该文件描述符还在等待缓冲区中的数据;随后,应用程序会不断轮询调用 read 直到它的返回值大于 0,这时应用程序就可以对读取操作系统缓冲区中的数据并进行操作。进程使用非阻塞的 I/O 操作时,可以在等待过程中执行其他任务,提高 CPU 的利用率。

I/O 多路复用模型

可以通过一次系统调用,检查多个文件描述符的状态。这是 I/O 多路复用的主要优点,相比于非阻塞 I/O,在文件描述符较多的场景下,避免了频繁的用户态和内核态的切换,减少了系统调用的开销。

进程可以通过 select、poll、epoll 发起 I/O 多路复用的系统调用,这些系统调用都是同步阻塞的:如果传入的多个文件描述符中,有描述符就绪,则返回就绪的描述符;否则如果所有文件描述符都未就绪,就阻塞调用进程,直到某个描述符就绪,或者阻塞时长超过设置的 timeout 后,再返回。I/O 多路复用内部使用非阻塞 I/O 检查每个描述符的就绪状态。

select

int select(int nfds,
            fd_set *restrict readfds,
            fd_set *restrict writefds,
            fd_set *restrict errorfds,
            struct timeval *restrict timeout);

readfds、writefds、errorfds 是三个文件描述符集合。select 会遍历每个集合的前 nfds 个描述符,分别找到可以读取、可以写入、发生错误的描述符,统称为“就绪”的描述符。然后用找到的子集替换参数中的对应集合,返回所有就绪描述符的总数。

timeout 参数表示调用 select 时的阻塞时长。如果所有文件描述符都未就绪,就阻塞调用进程,直到某个描述符就绪,或者阻塞超过设置的 timeout 后,返回。如果 timeout 参数设为 NULL,会无限阻塞直到某个描述符就绪;如果 timeout 参数设为 0,会立即返回,不阻塞。

缺点:

poll

poll 和 select 几乎没有区别。poll 在用户态通过数组方式传递文件描述符,在内核会转为链表方式存储,没有最大数量的限制

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

其中 fds 是一个 pollfd 结构体类型的数组,调用 poll() 时必须通过 nfds 指出数组 fds 的大小,即文件描述符的数量。

epoll

epoll 是对 select 和 poll 的改进,避免了“性能开销大”和“文件描述符数量少”两个缺点。

简而言之,epoll 有以下几个特点:

epoll_create

int epoll_create(int size);

epoll_create 会创建一个 epoll 实例,同时返回一个引用该实例的文件描述符。 epoll 实例内部存储:

epoll_ctl

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

epoll_ctl 会监听文件描述符 fd 上发生的 event 事件。参数说明:

epoll_ctl 会将文件描述符 fd 添加到 epoll 实例的监听列表里,同时为 fd 设置一个回调函数,并监听事件 event。当 fd 上发生相应事件时,会调用回调函数,将 fd 添加到 epoll 实例的就绪队列上。

epoll_wait

int epoll_wait(int epfd, struct epoll_event *events,
               int maxevents, int timeout);

这是 epoll 模型的主要函数,功能相当于 select。

参数说明:

返回值表示 events 中存储的就绪描述符个数,最大不超过 maxevents。

epoll 的优点

一开始说,epoll 是对 select 和 poll 的改进,避免了“性能开销大”和“文件描述符数量少”两个缺点。

对于“文件描述符数量少”,select 使用整型数组存储文件描述符集合,而 epoll 使用红黑树存储,数量较大。

对于“性能开销大”,epoll_ctl 中为每个文件描述符指定了回调函数,并在就绪时将其加入到就绪列表,因此 epoll 不需要像 select 那样遍历检测每个文件描述符,只需要判断就绪列表是否为空即可。这样,在没有描述符就绪时,epoll 能更早地让出系统资源。

相当于时间复杂度从 O(n) 降为 O(1)

此外,每次调用 select 时都需要向内核拷贝所有要监听的描述符集合,而 epoll 对于每个描述符,只需要在 epoll_ctl 传递一次,之后 epoll_wait 不需要再次传递。这也大大提高了效率。

水平触发、边缘触发

select 只支持水平触发,epoll 支持水平触发和边缘触发。

水平触发(LT,Level Trigger):当文件描述符就绪时,会触发通知,如果用户程序没有一次性把数据读/写完,下次还会发出可读/可写信号进行通知。

边缘触发(ET,Edge Trigger):仅当描述符从未就绪变为就绪时,通知一次,之后不会再通知。

区别:边缘触发效率更高,减少了事件被重复触发的次数,函数不会返回大量用户程序可能不需要的文件描述符。

水平触发、边缘触发的名称来源:数字电路当中的电位水平,高低电平切换瞬间的触发动作叫边缘触发,而处于高电平的触发动作叫做水平触发。

三者对比

select、poll 都是在用户态维护文件描述符集合,因此每次需要将完整集合传给内核;epoll 由操作系统在内核中维护文件描述符集合,因此只需要在创建的时候传入文件描述符。

此外 select 只支持水平触发,epoll 支持边缘触发。

当连接数较多并且有很多的不活跃连接时,epoll 的效率比其它两者高很多。当连接数较少并且都十分活跃的情况下,由于 epoll 需要很多回调,因此性能可能低于其它两者。

参考

https://imageslr.com/2020/02/27/select-poll-epoll.html