文章目录
- 参考资料
select | poll | epoll | |
存储方式 | 数组 | 链表 | 红黑树 |
操作方式 | 轮询机制 | 轮询机制 | 通知机制(回调) |
最大连接数 | linux下为1024 | 无上限 | 上限很大 |
fd拷贝 | 每次调用都需要全部fd数据从用户态到内核态拷贝 | 每次调用都需要全部fd数据从用户态到内核态拷贝 | 仅操作时拷贝进内核一次,后续 |
IO效率 | 每次调用均线性遍历,时间复杂度为O(n) | 每次调用均现行遍历,时间复杂度为O(n) | 每次调用 |
1.Socket
基础概念
(0)文件描述(file description表示一个打开文件的上下文信息,如文件的大小、内容、编码等),如同一个抽屉,该文件描述由内核管理,而文件描述和文件描述符不同——回顾OS笔记第23点可知open()
系统调用后,内核分配给用户空间一个文件描述符fd——即抽屉的把手(,翻译为句柄,fd其实就是一个整数)。
一个抽屉可以有多个把手(即文件描述可对应多个fd);只有多个fd都关了,内核才知道该文件描述没人用了(可回顾硬链接),所以就回收。
(1)socket是一种特殊接口(也是一种文件描述符fd),如插座端口上的孔,端口不能被其他进程占用。Socket即为实现操作某个IP地址上的某个端口达到点到点通信的目的,需要绑定到某个具体的进程中和端口中。
(2)客户端和服务器之间的通信都需要唯一的socket,每个socket都由 {协议、本地地址、本地端口} 表示,一个完整的套接字则由{协议、本地地址、本地端口、远程端口}表示。
(3)socket也有类似打开文件的函数调用,该函数返回一个整型的socket描述符。
分类
(1)流式socket——TCP通信;
(2)数据报socket——UDP通信;
(3)原始socket。
2.接收data流程
1)网卡接收数据
最终就是网卡读取数据后,存入内存中,该过程用到DMA、IO通路:
(1)网卡收到网线传来的数据;
(2)硬件电路传输;
(3)将数据写入到内存中的某个地址上。
2)如何知接收到data
当网卡将data写入内存后,网卡向CPU发出一个中断信号,CPU从而知道有新数据到来,从而CPU执行中断处理程序。
3)进程阻塞和CPU关系
阻塞是进程调度的关键一环,指进程在等待某件事(如接收到网络数据)前的等待状态,方法有recv、select、epoll等。
服务器端的简化流程:
可以回顾OS的进程状态转换图:
等待态即阻塞态,从简化流程中看出,一开始创建socket语句,即创建一个由文件系统管理的socket对象(该对象包含发送缓冲区、接收缓冲区与等待队列等,其中等待队列指向所有需要等待该socket事件的进程)上面的recv
是一个阻塞方法,当运行到recv时会一直等待,直到接收到数据为止;
当socket接收到数据后,OS将socket等待队列上的进程重新放回到工作队列,即该进程由【阻塞态】变为【就绪态】,之后变为【运行态】。
socket的接收缓冲区已经有了data,recv返回接收到的数据。
中断程序作用:
(1)先将网络数据写入到对应socket的接收缓冲区里面;
(2)之后唤醒在等待队列中的进程,重新将该进程放入到工作队列中。
而唤醒进程后,OS如何知道网络数据对应哪个socket?
答:网络数据报的首部字段包含IP和端口,而socket和端口号是一一对应关系。
4.同时监视多个socket
回顾多路复用的“复用”和分用:
而最后一个概念就提到I/O多路复用。现在服务器端要管理多个客户端连接,而recv只能监视一个socket
,为了解决这个问题先后出现了select
和epoll
。
1.select
1)设计思想
一个fds数组存放所有需要监视的socket,然后调用select
——如果fds中所有socket都没有数据,select会阻塞,直到有一个socket接收到数组,select返回,唤醒进程。
用户可以遍历fds数组,通过FD_ISSET判断具体哪个socket收到数据,然后做出处理。
若进程A同时监视三个socket(sock1、2、3),调用select后A进程加入到这3个socket的等待队列,而如果此时sock2收到了数据后,利用中断处理程序,进程被唤醒从而离开等待队列,重新调入工作队列中(如下图),程序只需遍历一遍socket列表,就可以得到就绪的socket。
上面的例子是只有一个socket接收缓冲区有数据,如果有多个socket接收缓冲区有数据,则select
直接返回,不会阻塞,select的返回值也就可能大于0。
2)select的缺点
(1)每次调用 select 都需要将进程加入到所有监视 socket 的等待队列,每次唤醒都需要从每个队列中移除。这里涉及了两次遍历;
而且每次都要将整个 fds 列表传递给内核,有一定的开销。正是因为遍历操作开销大,出于效率的考量,才会规定 select 的最大监视数量,默认只能监视 1024 个 socket。
(2)进程被唤醒后,程序并不知道哪些 socket 收到数据,还需要遍历一次。
2.epoll特点
为了减少遍历次数。
1)功能分离
多数情况下,需要监视的socket相对固定,因此epoll将【维护等待队列】和【阻塞进程】分离——先用epoll_ctl
维护等待队列,再调用epoll_wait
阻塞进程。
2)就绪列表
为了避免像select一样程序不知道哪些socket收到数据而需要逐个遍历,epoll就在内核维护一个就绪列表(引用收到数据的socket)。只要获取该就绪列表rdlist
的内容,就能知道哪些socket收到数据。
5.epoll详解
1)创建epoll对象
当某个进程调用epoll_create
后,内核会创建一个eventpoll对象(和socket一样会等待队列)。内核要维护就绪列表等数据,就绪列表可以作为eventpoll的成员。
2)维护监视列表(epoll_ctl)
创建了eventpoll对象后,利用epoll_ctl
添加或删除所要监听的socket。
如下图通过epoll_ctl添加sock1、2、3的监听,内核会将eventpoll添加到这三个socket的等待队列中。
![在这里插入图片描述](https://img-blog.csdnimg.cn/20210125221031832.png#pic_center #pic_center =400x)
添加所要监听的socket
3)接收数据
当socket收到数据后,中断程序会操作eventpoll对象,而不是直接操作进程.
当socket接收到数据后,中断程序会给eventpoll对象的就绪列表添加socket的引用,如下图红线。
eventpoll对象相当于socket和进程之间的中介,socket的数据接收后不直接影响进程,而是通过改变eventpoll的就绪列表来改变进程状态。
4)阻塞和唤醒进程
当程序执行到epoll_wait
时,如果rdlist已经引用了socket,那么epoll_wait
直接返回,如果rdlist为空,阻塞进程。
假如计算机中正在运行进程 A 和进程 B,在某时刻进程 A 运行到了 epoll_wait 语句。如下图所示,内核会将进程 A 放入 eventpoll 的等待队列中,阻塞进程。
当 socket 接收到数据,中断程序一方面修改 rdlist,另一方面唤醒 eventpoll 等待队列中的进程,进程 A 再次进入运行状态(如下图)。也因为 rdlist 的存在,进程 A 可以知道哪些 socket 发生了变化。
6.eventpoll的DS
eventpoll包含了lock、mtx、wq(等待队列)与rdlist等成员,其中rdlist就绪列表和 rbr兴趣列表最重要。
从《Linux/Unix系统编程手册》中提到,epoll实例实现了2个目的:
(1)记录了在进程中声明过的刚兴趣的文件描述符列表——(兴趣列表),其实就是上面介绍的eventpoll添加所要监听的socket。
(2)维护了处于I/O就绪态的文件描述符列表——(就绪列表),其实就是上面介绍rdlist列表。
1)就绪列表的结构
eventpoll的就绪列表用双向链表实现,为啥呢——就绪列表引用着就绪的socket,需要能够快速插入数据,即利用epoll_ctl
实现监听socket,也要能够快速删除。
2)兴趣列表的索引结构
epoll
使用【红黑树】作为索引结构,追踪当前监听的所有文件描述符——即兴趣列表用了红黑树。
epoll将“维护监视队列”和“进程阻塞”分离,也意味着需要有个数据结构来保存监视的 socket,至少要方便地添加和移除,还要便于搜索,以避免重复添加。红黑树是一种自平衡二叉查找树,搜索、插入和删除时间复杂度都是O(log(N))。
PS:
因为操作系统要兼顾多种功能,以及由更多需要保存的数据,rdlist 并非直接引用 socket,而是通过 epitem 间接引用,红黑树的节点也是 epitem 对象。同样,文件系统也并非直接引用着 socket。
参考资料
(1)(2)https://www.ivdone.top/article/1659.html (3)select poll epoll解读