![](https://img.haomeiwen.com/i12979420/cb955c0b82ab7a7c.png)
关于fd你可以简单理解为指向socket资源的一个指针,因为内核只返回给我们socket的fd,所以我们对socket的操作都是通过这个fd。
![](https://img.haomeiwen.com/i12979420/1ebf4efa8a4dcbf4.png)
![](https://img.haomeiwen.com/i12979420/d2776198f3cd71c3.png)
![](https://img.haomeiwen.com/i12979420/f28853b7634cb900.png)
![](https://img.haomeiwen.com/i12979420/051323850a577df1.png)
用户空间为什么要拷贝fd到内核空间呢?
因为用户态不能执行内核操作。
网络数据包传输过来,解析端口号可以找到对应的进程,但是是怎么找到对应的socket的呢?
根据来源ip和端口号,目标ip 和端口号,就可以定位到。
![](https://img.haomeiwen.com/i12979420/e82c95645a155214.png)
![](https://img.haomeiwen.com/i12979420/df28bb409b83d89a.png)
![](https://img.haomeiwen.com/i12979420/5d5d43e1593c9007.png)
![](https://img.haomeiwen.com/i12979420/da5954c60baec60d.png)
![](https://img.haomeiwen.com/i12979420/78b1077e08f648e0.png)
![](https://img.haomeiwen.com/i12979420/41877fe0bce7a5f6.png)
![](https://img.haomeiwen.com/i12979420/b8f0a4a83ec793b1.png)
![](https://img.haomeiwen.com/i12979420/5fd4519c1aa6e54c.png)
![](https://img.haomeiwen.com/i12979420/586811c166f81b2e.png)
![](https://img.haomeiwen.com/i12979420/b00a6e03cd256b01.png)
![](https://img.haomeiwen.com/i12979420/9b305fe61d3f8554.png)
![](https://img.haomeiwen.com/i12979420/a67a126d6ec4b77f.png)
![](https://img.haomeiwen.com/i12979420/edb50643a75c5efe.png)
![](https://img.haomeiwen.com/i12979420/d7df015bbda7d47c.png)
![](https://img.haomeiwen.com/i12979420/c064645bcbfd6213.png)
![](https://img.haomeiwen.com/i12979420/856d24289369aedf.png)
![](https://img.haomeiwen.com/i12979420/632e46e99bd25c7c.png)
![](https://img.haomeiwen.com/i12979420/8bb6869d519d6e11.png)
![](https://img.haomeiwen.com/i12979420/0a4dfafa99c66f6e.png)
讨论了 select/poll 几个缺点,针对这几个缺点,就需要解决以下几件事:
- 如何突破文件描述符数量的限制
- 如何避免用户态和内核态对文件描述符集合的拷贝
- socket 就绪后,如何避免线性遍历文件描述符集合
如何突破文件描述符数量的限制?
其实 poll 已经解决了,poll 使用的是链表的方式管理 socket 描述符,但问题是不够高效,如果有百万级别的连接需要管理,如何快速的插入和删除就变得很重要,于是 epoll 采用了红黑树的方式进行管理,这样能保证在添加 socket 和删除 socket 时,有 O(log(n)) 的复杂度。
如何避免用户态和内核态对文件描述符集合的拷贝?
其实对于 select 来说,由于这个集合是保存在用户态的,所以当调用 select 时需要屡次的把这个描述符集合拷贝到内核空间。所以如果要解决这个问题,可以直接把这个集合放在内核空间进行管理。没错,epoll 就是这样做的,epoll 在内核空间创建了一颗红黑树,应用程序直接把需要监控的 socket 对象添加到这棵树上,直接从用户态到内核态了,而且后续也不需要再次拷贝了。
socket就绪后,如何避免内核线性遍历文件描述符集合?
这个问题就会比较复杂,要完整理解就得涉及到内核收包到应用层的整个过程。
这里先简单讲一下,与 select 不同,epoll 使用了一个双向链表来保存就绪的 socket,这样当活跃连接数不多的情况下,应用程序只需要遍历这个就绪链表就行了,而 select 没有这样一个用来存储就绪 socket 的东西,导致每次需要线性遍历所有socket,以确定是哪个或者哪几个 socket 就绪了。这里需要注意的是,这个就绪链表保存活跃链接,数量是较少的,也需要从内核空间拷贝到用户空间。
从上面 3 点可以看到 epoll 的几个特点:
- 程序在内核空间开辟一块缓存,用来管理 epoll 红黑树,高效添加和删除
- 红黑树位于内核空间,用来直接管理 socket,减少和用户态的交互
- 使用双向链表缓存就绪的 socket,数量较少
- 只需要拷贝这个双向链表到用户空间,再遍历就行,注意这里也需要拷贝,没有共享内存
当一个包从网卡进来之后,是如何走到应用程序的呢?中间经过了哪些步骤呢?
- 进程用户态创建Socket.
- 调用read后,如果当前Socket数据接受队列上没有数据时则会将当前进程阻塞掉,修改当前进程状态,并让出CPU使用权,一直等待到有数据包的到来,这里当前进程已经让出CPU使用权,CPU已经不在对当前进程进行调度.
- 数据包到达网卡后,通过DMA技术将本次网络包复制到RingBuffer中.
- 网卡通过给CPU特定针脚发送电压变化来通知CPU有网络包到来,也就是网络包到来硬中断.
- CPU简单处理后发送软中断,此时内核线程ksoftirqd来进行处理.
- 内核线程根据特定软中断信号执行网络包接受处理函数,其会将网络包从RingBuffer中取出来放入到指定Socket的数据就绪队列中.
- 唤醒用户进程,CPU重新调度,调度到之后,进入内核态读取Socket数据就绪队列中的数据包到用户态.
简单总结一下收包以及触发的过程:
- 包从网卡进来
- 一路经过各个子系统到达内核协议栈(传输层)
- 内核根据包的 {src_ip:src_port, dst_ip:dst_port} 找到 socket 对象(内核维护了一份四元组和 socket 对象的一一映射表)
- 数据包被放到 socket 对象的接收缓冲区
- 内核唤醒 socket 对象上的等待队列中的进程,通知 socket 事件
- 进程唤醒,处理 socket 事件(read/write)
参考
I/O 多路复用解析
https://www.bilibili.com/video/BV1r54y1f7bU
网友评论