内核是如何和用户进程协作的.
本文参考张彦飞老师的 《深入理解Linux网络:修炼底层内功,掌握高性能原理》。
读完本文将会收获一下内容:(安安静静的读完、思考完)
-
一些基本概念. -
TCP网络包到达之后处理流程. -
Select工作原理. -
Epoll工作原理. -
同步阻塞IO模型.
一、在理解之前,有几个概念需要理解一下.
1.1 同步、异步的概念.
同步异步体现在在进程/线程程实现某个动作时未得到结果前是否可以执行其他动作.
-
同步:执行某个动作结果未返回时,当前进程/线程等待结果返回. -
异步:执行某个动作结果未返回时,当前进程/线程不必等待结果返回,可以去做别的事情. 当第一个动作完成后,会通过回调来通知该动作执行者动作执行完毕.
1.2 阻塞、非阻塞的概念.
阻塞非阻塞体现在进程/协程在实现某个动作时未得到结果前是否挂起.
-
阻塞:执行某个动作未返回时,当前进程/线程挂起. -
非阻塞:执行某个动作未返回时,不会阻塞当前进程/线程.
1.3 内核与用户进程协作的几种方式.
-
select:将要查看的socket文件描述符透传够内核,这个时候会发生一次描述符拷贝,内核拿到具体的文件描述符后遍历当前集合是否有事件发生,如果某个文件描述符有事件发生,则会将该文件描述符的二进制位设置为1,用户进程通过遍历所有文件描述符所表示的二进制位是否为1,如果为1则表示有数据到达,这个时候进行系统调用去取数据即可. -
poll:本质上和select一样,只是解决了select能监控的文件描述符数量问题。本质上也需要发生内核拷贝. -
epoll:本质上省略去了文件描述符内核拷贝,以及没有文件描述符数量的限制.
二、select基本工作原理.
-
调用 socket
函数创建socket文件描述符.bind
绑定当前机器信息(比如随机分配端口),listen
初始化半连接队列和全连接队列. -
调用 accept
函数等待TCP链接,每一个链接进来都将当前连接的文件描述符放入到bitmap
中(fd_set
). -
调用 FD_ZERO
:将给定的文件描述符集合清空,每次进行读取数据前状态必须清空. -
调用 FD_SET
:将要监听的文件描述符设置为1
,表示要监听这几个文件描述符指向的链接. -
调用 select
函数,将要监听的文件描述符集合传入给select
函数. -
select
会将给定的文件描述符中就绪的描述符设置为1
,未就绪的设置为0
. -
用户进程根据返回状态进行读取数据即可.
总结:
三、Epoll工作原理.
3.1 EPOLL_CREATE
EventPolls 数据结构
epoll_create
初始化一个epoll
内核结构,返回该结构的文件描述符. 下面介绍一下每个字段的含义:
-
wq
: 等待队列链表。软中断就绪的时候会通过该链表找到阻塞在Epoll
上等待该socket数据的用户进程. -
rdlist:就绪文件描述列表。当有socket上有数据到来时,软中断时内核会把就绪的socket的文件描述符放入到 epoll
就绪文件描述列表中. -
rbr: EpollItem
存储结构红黑树。支持海量连接的高效查找、插入、删除、遍历等。通过这棵树来管理用户进程下添加进来的所有socket连接.
3.2 EPOLL_CTL_ADD
每次要添加一个新的连接时,都会初始化一个EpollItem结构体,用来结构化struct. 从下面我们可以看到每一个eventItem
都有一个指向eventPoll结构的指针。
struct epitem{
struct rb_node rbn; // 红黑树节点.
struct epoll_filefd ffd; // 当前节点表示的socket文件描述符.
struct eventpoll *ep; // 所归属的eventpoll对象.
struct list_head pwqlist; // 数据接受队列.包含回调指针.
}
在初始化时,同时会对该item
注册一个回调函数(当有数据到来时要执行的回调函数):ep_pool_callback
,这里并没有将当前进程添加到当前socket的回调函数中,而只是设置了一个回调函数,并且进程描描述符设置的为NULL,这是因为等待进程被放在eventpoll
实例上了。
整个添加连接的过程可以说是构造一个EpollItem的过程,大概如下:
-
为 EpItem
初始化内存. -
设置当前Item所关联的socket文件描述符. -
定义并初始化等待队列. -
注册数据到来时的回调函数: ep_poll_callback
,当前socket三次握手之后,处于等待数据接受状态,当前进程处于阻塞状态,所以需要设置回调函数. -
将EpItem插入到eventPoll对象的红黑树中.
插入后具体的结构如下:
我们知道,当我们通过EPOLL_CTL_ADD
将某一个Socket
添加到指定的Epoll
中时,会形成上面的结构,其中epitem
和EventPoll
互相关联,同时Socket
的等待队列中的回调函数为ep_poll_callback
,当某一个TCP包到来时,具体发生什么,我们下面接着来看. 而ep_poll_callback
函数主要做了两件事情:
-
首先会将当前 Socket
文件描述符存入到当前epoll
的就绪文件描述符队列中. -
其次会唤醒当前 epoll
上的等待进程,等待进程被唤醒后,将会接着执行read
. -
用户进程读取到数据后,进程将会从内核态转换用户态,此时我们就可以对TCP包进行编解码操作.
3.3 EPOLL_WAIT工作原理.
下面我们从EPOLL_WAIT
开始分析,EPOLL收到数据包后是怎么唤醒用户进程来内核读取指定Socket
数据就绪队列中的数据的.
-
第一步:数据包到网卡之后,首先会将当前TCP包通过DMA技术存入到
RingBuffer
中,之后会通过硬中断通知CPU当前网卡有数据到来,其次硬中断中触发网络包到达软中断,此时内核线程ksoftirqd
收到软中断后,将会将数据包从RingBuffer
中取出放到当前Socket
的数据就绪队列中. -
第二步:唤醒等待在
Socket
上的用户进程. 我们知道在将某个Socket
封装成epitem
添加到EventPoll
中时,已经将Socket
的等待队列中的等待项中的回调函数设置成了epoll
的回调函数ep_poll_callback
,那么当当前包添加到数据就绪队列中之后,则会执行进程等待队列中的回调函数,而epoll
则会执行ep_poll_callback
回调函数. -
第三步:执行
epoll
回调函数ep_poll_callback
.而ep_poll_callback
主要做了以下内容: 将当前 Socket
文件描述符存入到当前epoll
的就绪文件描述符队列中.如果 EventPoll
等待队列上有进程在等待,那么将会通过在回调函数中调用default_wake_function
来唤醒用户进程,而用户进程的描述符是封装在epoll
的等待项中的,这里只需要通过进程描述符修改进程的状态为可运行状态,同时将进程放入到可执行队列中,然后就等待CPU调度执行.第四步:用户进程感知到
EVENT_WAIT
结束后,继续执行read
. 这里我有一个猜想是:epoll_wait
返回就绪的文件描述符个数,而用户态也是可以获取到就绪队列的文件句柄,或者,用户态根据返回的个数直接去就绪的文件描述符队列中取出N个数据项,那么这N个数据项就是本次已经就绪的Socket
事件,此时,用户态也能正常读取到数据.
总的流程类似下面这种图.
四、同步阻塞IO.
其实,epoll
在没有数据到来时,也是会对进程阻塞的,那么为什么epoll
会比select
等性能好呢?其实这个是因为select
系统调用时涉及到用户态到内核态的内存拷贝,其会将用户态的文件描述符集合拷贝到内核态,同理,系统调用返回时,也会发生拷贝,其会将就绪的文件描述符状态拷贝到用户态.而epoll
则不是,epoll
在有事件发生时,则会返回就绪的文件描述符,其中不在涉及内核态到用户态的拷贝过程,所以在海量链接下epoll
性能是非常突出的.
我们既然已经了解了多路复用机制,那么我们也来简单了解下基本的同步阻塞IO模型,也就是目前客户端基本上使用的模型. 整体流程图如下:
进本流程描述:
-
进程用户态创建 Socket
. -
调用 read
后,如果当前Socket
数据接受队列上没有数据时则会将当前进程阻塞掉,修改当前进程状态,并让出CPU使用权,一直等待到有数据包的到来,这里当前进程已经让出CPU使用权,CPU已经不在对当前进程进行调度. -
数据包到达网卡后,通过DMA技术将本次网络包复制到 RingBuffer
中. -
网卡通过给CPU特定针脚发送电压变化来通知CPU有网络包到来,也就是网络包到来硬中断. -
CPU简单处理后发送软中断,此时内核线程 ksoftirqd
来进行处理. -
内核线程根据特定软中断信号执行网络包接受处理函数,其会将网络包从 RingBuffer
中取出来放入到指定Socket
的数据就绪队列中. -
唤醒用户进程,CPU重新调度,调度到之后,进入内核态读取 Socket
数据就绪队列中的数据包到用户态.
五、总结:
-
多路复用:多个 Socket
复用一个进程,一个进程内监听多个Socket
. -
Select特点: -
IO多路复用. -
文件描述符限制、用户态-内核态内存拷贝 性能不是很好. -
Epoll特点: -
使用红黑树存储文件描述符,发生用户态到内核态的拷贝只有在调用 EPOLL_CTL_ADD
指令时才会发生,不会向select
和poll
一样,每次都会发生拷贝. -
通过异步IO事件确定就绪的文件描述符而不是轮询. -
使用队列存储就绪文件描述符,且返回就绪文件描述符的个数 N
,用户进程可直接从就绪队列中获取N
个就绪的文件描述符进行数据读取. 避免轮询查找就绪的文件描述符.
原文始发于微信公众号(社恐的小马同学):深入理解Epoll工作原理.
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/269719.html