1. IO模型
Socket的输入操作有两步。
- wait for data - 等待网络传输数据到达,到达后复制到内核缓冲区;
- copy data from kernel to user - 把数据从内核缓冲区复制到应用进程缓冲区。
涉及到两个对象:process调用这个IO的进程/线程,kernel系统内核。
1.1 同步阻塞IO
用户线程发出请求后就一直被阻塞,直到数据到达并从内核缓冲区复制到进程缓冲区才返回。
1.2 同步非阻塞IO
用户线程发出请求后,内核立即返回错误码,但用户线程需要不断发出IO请求询问内核数据到达没,到达了才进行第二阶段。这个过程叫做轮询。
1.3 IO多路复用/异步阻塞IO
又称为事件驱动IO,单个进程有处理多个IO的能力,避免一个socket一个线程的开销和切换。用户注册多个socket,reactor一对多监听,不断调用select读取激活的socket,再一对多分发给对应处理器处理。
使用了 Reactor 反应堆模型。
- 将用户线程轮询IO操作状态的工作交给事件处理器,用户线程可以继续执行做其他的工作,Reactor线程负责调用内核的select函数检查socket状态。
- 当有socket被激活时,则通知相应的用户线程,执行handle_event进行数据读取、处理的工作。
- 由于select函数是阻塞的,因此多路IO复用模型也被称为异步阻塞IO模型。
1.4 信号驱动IO/异步非阻塞IO
sigaction系统调用,内核立即返回,应用进程可以去干其他事,当数据到达时,内核发送SIGIO信号给应用进程通知应用进程可以进行IO的数据复制。
相比于轮询方式,CPU利用率更高。
1.5 异步IO
aio_read系统调用立即返回,应用进程可以去干其他事,内核在完成所有操作后发送信号,通知应用进程IO已经完成。
1.6 比较
- 同步IO:将数据从内核缓冲区复制到应用进程缓冲区时,应用进程会阻塞。
- 异步IO:第二阶段不会阻塞。
- 阻塞IO:第一阶段就阻塞线程,直到获得数据。
- 非阻塞IO:线程发出请求立即返回,但需要轮询或者再次进行系统调用进行第二阶段。
2. wakeup callback机制
Linux 内核的事件唤醒回调机制是IO多路复用的本质。
概括来说,Linux通过睡眠队列管理所有等待 Socket 事件的线程,通过 wakeup 机制异步唤醒整个睡眠队列上等待事件的线程,通知线程事件发生。
- 睡眠等待
- select、poll、epoll 陷入内核,判断监控的 socket 是否有关心的事件发生,如果没,则为当前 process 构建一个 wait_entry 节点,插入到监控 socket 的 sleep_list;
- 进入循环的 schedule 直到关心的事件发生;
- 事件发生后,将当前线程的 wait_entry 节点从 socket 的 sleep_list 中删除。
- 异步唤醒
- socket 顺序遍历其睡眠队列,依次调用每个节点的 callback 函数;
- 直到完成遍历或者遇到某个排他节点。
3. IO复用机制
fd 文件描述符,用于表述指向文件的引用的抽象化概念。
3.1 Select
底层通过一个long类型的数组 fd_set,存放文件句柄。
每次调用select时,把 fd_set 集合从用户态复制到内核态,在内核轮询遍历集合,且对集合有 1024 的大小限制。
内部的轮询,是通过为每个 socket 添加 poll 逻辑,用来收集该 socket 发生的事件。轮询就是遍历 socket 调用 poll 逻辑,直到有事件发生。
所以存在三个问题:
- fd 集合限制为 1024,底层数组;
- fd 集合每次都要从用户态拷贝到内核态;
- 每次都在遍历集合收集可读数据。
3.2 poll
本质上和select一样,解决了 fd 集合大小限制问题。底层通过链表形式 pollfd 实现,所以没有最大连接数的限制。
其他两个缺点并没有改进,不适用大并发场景。
3.3 epoll
只适用于Linux。
- 针对集合拷贝。
- 使用事件回调通知,通过 epoll_ctl() 注册fd进行增删改,调用 epoll_wait() 等待事件产生。
- 内核 2.6.8 之前底层使用哈希表存储,之后使用红黑树。
- epoll_wait() 通过将内核空间和用户空间(都是虚拟地址)映射到同一块物理内存地址,用来减少用户态和内核态间的数据交换。
- 针对集合遍历。
- 引入中间层,为每个 socket 提供单独的回调函数,当其就绪时将自身加入准备队列 ready_list 中;
- 等待线程的回调函数遍历 ready_list 上所有的 socket,调用 poll 逻辑收集事件,唤醒线程。
3.3.1 工作模式
- Level Triggered 水平触发。默认模式,只要fd还有事件,每次 epoll_wait() 都会再次通知进程。
- Edge Triggered 边沿触发。通知之后进程必须立即处理事件,下次 epoll_wait() 不会收到该fd的通知。
3.4 比较
方式 | select | poll | epoll |
---|---|---|---|
操作方式 | 遍历 | 遍历 | 回调 |
底层实现 | 数组 | 链表 | 哈希表/红黑树 |
最大连接数 | 1024 or 2048 | 无上限 | 无上限 |
IO效率 | 轮询O(n) | 轮询O(n) | 事件回调,将就绪的fd放进就绪队列,每次只用判断队列是否为空O(1) |
fd拷贝 | 每次调用都把fd集合从用户态拷贝到内核态 | 每次调用都把fd集合从用户态拷贝到内核态 | 调用epoll_ctl()时fd拷贝进内核并保存 |
3.5 适用场景
- select的时间精度是微秒,适用实时性要求高的场景。
- poll没有最大描述符限制,若实时性要求不高且平台支持,用poll。
- epoll适用Linux平台,且有大量描述符需要同时轮询。
推荐阅读 大话select,poll,epoll