操作系统(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 集合,大大提高了检测的效率。

image.png

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

在一段指定时间内,监听用户感兴趣的文件描述符上的可读、可写和异常事件。

#include <sys/select.h>
int select( int nfds, fd_set* readfds,fd_set* writefds,fd_set*exceptfds,
struct timeval*timeout);
  • ndfs:被监听的文件描述符的总数

  • readfds、writefds、exceptfds:分别指向可读、可写和异常等事件对应的文件描述符集合。select调用返回时,内核将修改它们来通知应用程序哪些文件描述符已经就绪。

  • timeout:设置select函数的超时事件

5 poll

和select类似,在指定时间内轮询一定数量的文件描述符,以测试是否有就绪的文件描述符

#include <poll.h>
int poll(struct pollfd* fds, nfds_t nfds, int timeout);

struct pollfd{
int fd; //文件描述符
short events; //注册的事件
short revents; //实际发生的事件,由内核填充
}
  • fds:pollfd结构类型的数组,指定所有我们感兴趣的文件描述符上发生的可读、可写和异常等事件

  • nfds:被监听事件集合fds的大小

  • timeout:指定poll的超时值,单位是毫秒。当timeout为-1时,poll调用将永远阻塞,直到某个事件发生;当timeout为0时,poll调用将立即返回

image.png

image.png

6 epoll

把用户关心的文件描述符上的事件放在内核里的一个事件表中,而无须像select和poll那样每次调用都要重复传入文件描述符或事件集。
epoll使用一个额外的文件描述符,来唯一标识内核中的这个事件表

6.1.1 epoll_create

#include <sys/epoll.h>
int epoll_create(int size)
  • 返回文件描述符,作为其它epoll系统调用的第一个参数,以指定要访问的内核事件表

6.1.2 epoll_ctl

#include  <sys/epoll.h>
int epoll_ctl(int epfd,int op, int fd, struct epoll_event *event)

struct epoll_event{
__uint32_t events; //epoll事件
epoll_data_t data; //用户数据
};

typedef union epoll_data{
void* ptr;
int fd; //事件所从属的目标文件描述符
uint32_t u32;
uint64_t u64;
} epoll_data_t;
  • fd:要操作的文件描述符

  • op:操作类型,EPOLL_CTL_ADDEPOLL_CTL_MODEPOLL_CTL_DEL

  • 成功时返回0,失败则返回-1并设置errno

6.1.3 epoll_wait

在一段超时时间内等待一组文件描述符上的事件

#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event* events,int maxevents, int timeout );
  • 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的区别

image.png

8 Reactor和Proactor

9 事件处理模式

服务器程序需要处理三类事件:I/O事件、信号及定时事件。同步I/O模型通常用于实现Reactor模式异步I/O模型用于实现Proactor模式使用同步I/O方式模拟出Proactor模式

在IO模型中,“同步”和“异步”区分的是内核向应用程序通知的是何种IO事件(是就绪事件还是完成事件),以及该由谁来完成IO读写(是应用程序还是内核)。

  • 同步IO:应用程序完成读写

  • 异步IO:内核完成读写

9.1 模拟Proactor模式

主线程执行数据读写操作,读写完成之后,主线程向工作线程通知这一“完成事件”。那么从工作线程的角度来看,它们就直接获得了数据读写的结果,接下来要做的只是对读写的结果进行逻辑处理。(主线程代替内核完成数据读写)

image.png

10 并发模式

  • 半同步/半异步模式(half-sync/half-async)

  • 领导者/追随者模式(Leader/Followers)

在并发模式中,“同步”指的是程序完全按照代码序列的顺序执行;“异步”指的是程序的执行需要由系统事件来驱动。常见的系统事件包括中断、信号等。

10.1 半同步/半反应堆模式

半同步/半反应堆模式(half-sync/half-reactive)
Reactor模式:工作线程自己从socket上读取客户请求和往socket写入服务器应答模拟Proactor模式:主线程完成数据的读写。这种情况下,主线程一般会将应用程序数据、任务类型等信息封装为一个任务对象,然后将其(或者指向该任务对象的一个指针)插入请求队列。工作线程从请求队列中取得任务对象之后,即可直接处理之,而无须执行读写操作了。

image.png

10.2 半同步/半异步模式

image.png

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)