操作系统(5):网络系统
1 select/poll/epoll
select/poll/epoll 内核提供给用户态的多路复用系统调用,进程可以通过一个系统调用函数从内核中获取多个事件。
1.1 select
select 实现多路复用的方式是,将已连接的 Socket 都放到一个文件描述符集合,然后调用 select 函数将文件描述符集合拷贝到内核里,让内核来检查是否有网络事件产生,检查的方式很粗暴,就是通过遍历文件描述符集合的方式,当检查到有事件产生后,将此 Socket 标记为可读或可写, 接着再把整个文件描述符集合拷贝回用户态里,然后用户态还需要再通过遍历的方法找到可读或可写的 Socket,然后再对其处理。
所以,对于 select 这种方式,需要进行 2 次「遍历」文件描述符集合,一次是在内核态里,一个次是在用户态里 ,而且还会发生 2 次「拷贝」文件描述符集合,先从用户空间传入内核空间,由内核修改后,再传出到用户空间中。
select 使用固定长度的 BitsMap,表示文件描述符集合,而且所支持的文件描述符的个数是有限制的,在 Linux 系统中,由内核中的 FD_SETSIZE 限制, 默认最大值为 1024
,只能监听 0~1023 的文件描述符。
1.2 poll
poll 不再用 BitsMap 来存储所关注的文件描述符,取而代之用动态数组,以链表形式来组织,突破了 select 的文件描述符个数限制,当然还会受到系统文件描述符限制。
但是 poll 和 select 并没有太大的本质区别,都是使用「线性结构」存储进程关注的 Socket 集合,因此都需要遍历文件描述符集合来找到可读或可写的 Socket,时间复杂度为 O(n),而且也需要在用户态与内核态之间拷贝文件描述符集合,这种方式随着并发数上来,性能的损耗会呈指数级增长。
1.3 epoll
epoll 通过两个方面,很好解决了 select/poll 的问题。
第一点,epoll 在内核里使用红黑树来跟踪进程所有待检测的文件描述字,把需要监控的 socket 通过 epoll_ctl()
函数加入内核中的红黑树里,红黑树是个高效的数据结构,增删改一般时间复杂度是 O(logn)
。而 select/poll 内核里没有类似 epoll 红黑树这种保存所有待检测的 socket 的数据结构,所以 select/poll 每次操作时都传入整个 socket 集合给内核,而 epoll 因为在内核维护了红黑树,可以保存所有待检测的 socket ,所以只需要传入一个待检测的 socket,减少了内核和用户空间大量的数据拷贝和内存分配。
第二点, epoll 使用事件驱动的机制,内核里维护了一个链表来记录就绪事件,当某个 socket 有事件发生时,通过回调函数内核会将其加入到这个就绪事件列表中,当用户调用 epoll_wait()
函数时,只会返回有事件发生的文件描述符的个数,不需要像 select/poll 那样轮询扫描整个 socket 集合,大大提高了检测的效率。
2 I/O模型
socket在创建的时候默认是阻塞的。阻塞和非阻塞的概念能应用于所有文件描述符,而不仅仅是socket。我们称阻塞的文件描述符为阻塞IO,称非阻塞的文件描述符为非阻塞IO。
-
针对阻塞I/O执行的系统调用可能因为无法立即完成而被操作系统挂起,直到等待的事件发生为止。可能被阻塞的系统调用包括accept、send、recv和connect。
-
针对非阻塞IO执行的系统调用则总是立即返回,而不管事件是否已经发生。如果事件没有立即发生,这些系统调用就返回-1,和出错的情况一样。此时我们必须根据errno来区分这两种情况。对accept、sent和recv而言,事件未发生时erno通常被设置成EAGAIN(意为“再来一次”)或者EWOULDBLOCK(意为“期望阻塞”);对connect而言,ermo则被设置成EINPROGRESS(意为“在处理中”)。
I/O复用(I/O multiplexing):指的是通过一个支持同时感知多个描述符的函数系统调用,阻塞在这个系统调用上,等待某一个或者几个描述符准备就绪,就返回可读条件。I/O复用使得程序能同时监听多个文件描述符。
I/O复用虽然能同时监听多个文件描述符,但它本身是阻塞的。
3 文件描述符就绪条件
3.1 可读
-
socket内核接收缓存区中的字节数大于或等于其低水位标记SORCVLOWAT。此时我们可以无阻塞地读该socket,并且读操作返回的字节数大于0。
-
socket通信的对方关闭连接。此时对该socket的读操作将返回0。
-
监听socket上有新的连接请求。
-
socket上有未处理的错误。此时我们可以使用getsockopt来读取和清除该错误。
3.2 可写
-
socket内核发送缓存区中的可用字节数大于或等于其低水位标记SOSNDLOWAT。此时我们可以无阻塞地写该socket,并且写操作返回的字节数大于0。
-
socket的写操作被关闭。对写操作被关闭的socket执行写操作将触发一个SIGPIPE信号。
-
socket使用非阻塞connect连接成功或者失败(超时)之后。
-
socket上有未处理的错误。此时我们可以使用getsockopt来读取和清除该错误
4 select
在一段指定时间内,监听用户感兴趣的文件描述符上的可读、可写和异常事件。
|
-
ndfs:被监听的文件描述符的总数
-
readfds、writefds、exceptfds:分别指向可读、可写和异常等事件对应的文件描述符集合。select调用返回时,内核将修改它们来通知应用程序哪些文件描述符已经就绪。
-
timeout:设置select函数的超时事件
5 poll
和select类似,在指定时间内轮询一定数量的文件描述符,以测试是否有就绪的文件描述符
|
-
fds:pollfd结构类型的数组,指定所有我们感兴趣的文件描述符上发生的可读、可写和异常等事件
-
nfds:被监听事件集合fds的大小
-
timeout:指定poll的超时值,单位是毫秒。当timeout为-1时,poll调用将永远阻塞,直到某个事件发生;当timeout为0时,poll调用将立即返回
6 epoll
把用户关心的文件描述符上的事件放在内核里的一个事件表中,而无须像select和poll那样每次调用都要重复传入文件描述符或事件集。
epoll使用一个额外的文件描述符,来唯一标识内核中的这个事件表
6.1.1 epoll_create
|
-
返回文件描述符,作为其它epoll系统调用的第一个参数,以指定要访问的内核事件表
6.1.2 epoll_ctl
|
-
fd:要操作的文件描述符
-
op:操作类型,
EPOLL_CTL_ADD
、EPOLL_CTL_MOD
、EPOLL_CTL_DEL
-
成功时返回0,失败则返回-1并设置errno
6.1.3 epoll_wait
在一段超时时间内等待一组文件描述符上的事件
|
-
maxevents参数指定最多监听多少个事件,它必须大于0。
-
成功时返回就绪的文件描述符的个数,失败时返回-1并设置errno
6.1.4 LT和ET模式
epoll对文件描述符的操作有两种模式:LT(LevelTrigger,电平触发)模式和ET(Edge Trigger,边沿触发)模式。LT模式是默认的工作模式,这种模式下epoll相当于一个效率较高的poll。当往epoll内核事件表中注册一个文件描述符上的EPOLLET事件时,epoll将以ET模式来操作该文件描述符。ET模式是epoll的高效工作模式。
LT
对于采用LT工作模式的文件描述符,当epoll_wait检测到其上有事件发生并将此事件通知应用程序后,应用程序可以不立即处理该事件。这样,当应用程序下一次调用epoll_wait时,epoll_wait还会再次向应用程序通告此事件,直到该事件被处理。
ET
而对于采用ET工作模式的文件描述符,当epoll_wait检测到其上有事件发生并将此事件通知应用程序后,应用程序必须立即处理该事件,因为后续的epoll_wait调用将不再向应用程序通知这一事件。边缘触发模式一般和非阻塞 I/O 搭配使用,以避免没有数据可读写时,进程会阻塞在读写函数那里
可见,ET模式在很大程度上降低了同一个epoll事件被重复触发的次数,因此效率要比LT模式高。
6.1.5 EPOLLONESHOT
对于注册了EPOLLONESHOT事件的文件描述符,操作系统最多触发其上注册的一个可读、可写或者异常事件,且只触发一次,除非我们使用epollctl函数重置该文件描述符上注册的EPOLLONESHOT事件。这样,当一个线程在处理某个socket时,其他线程是不可能有机会操作该socket的。但反过来思考,注册了EPOLLONESHOT事件的socket一旦被某个线程处理完毕,该线程就应该立即重置这个socket上的EPOLLONESHO事件,以确保这个socket下一次可读时,其EPOLLIN事件能被触发,进而让其他工作线程有机会继续处理这个socket。
7 select、poll和epoll的区别
8 Reactor和Proactor
9 事件处理模式
服务器程序需要处理三类事件:I/O事件、信号及定时事件。同步I/O模型通常用于实现Reactor模式异步I/O模型用于实现Proactor模式使用同步I/O方式模拟出Proactor模式
在IO模型中,“同步”和“异步”区分的是内核向应用程序通知的是何种IO事件(是就绪事件还是完成事件),以及该由谁来完成IO读写(是应用程序还是内核)。
-
同步IO:应用程序完成读写
-
异步IO:内核完成读写
9.1 模拟Proactor模式
主线程执行数据读写操作,读写完成之后,主线程向工作线程通知这一“完成事件”。那么从工作线程的角度来看,它们就直接获得了数据读写的结果,接下来要做的只是对读写的结果进行逻辑处理。(主线程代替内核完成数据读写)
10 并发模式
-
半同步/半异步模式(half-sync/half-async)
-
领导者/追随者模式(Leader/Followers)
在并发模式中,“同步”指的是程序完全按照代码序列的顺序执行;“异步”指的是程序的执行需要由系统事件来驱动。常见的系统事件包括中断、信号等。
10.1 半同步/半反应堆模式
半同步/半反应堆模式(half-sync/half-reactive)
Reactor模式:工作线程自己从socket上读取客户请求和往socket写入服务器应答模拟Proactor模式:主线程完成数据的读写。这种情况下,主线程一般会将应用程序数据、任务类型等信息封装为一个任务对象,然后将其(或者指向该任务对象的一个指针)插入请求队列。工作线程从请求队列中取得任务对象之后,即可直接处理之,而无须执行读写操作了。
10.2 半同步/半异步模式
11 一致性哈希
应用场景是针对于有状态服务新增或下线节点
9.4 什么是一致性哈希? | 小林coding (xiaolincoding.com)
12 参考
彻底弄懂IO复用:深入了解select,poll,epoll - 知乎 (zhihu.com)
9.2 I/O 多路复用:select/poll/epoll | 小林coding (xiaolincoding.com)
9.3 高性能网络模式:Reactor 和 Proactor | 小林coding (xiaolincoding.com)