
与上一篇文章的第一部分一样,这些文本是为了帮助他人或阐明自己的想法,而不是所谓的源代码分析. 如果要分析源代码,最好直接调试源代码. 下一个最好的东西是任何文件或书本. 因此,这类有助于人们理清思路的文章应尽可能流畅,简洁明了.
Linux内核使用睡眠队列来组织所有等待事件的任务,并且唤醒机制可以异步唤醒整个睡眠队列中的任务. 睡眠队列中的每个节点都有一个回调,并且唤醒逻辑会唤醒睡眠队列. ,它将遍历队列链接列表上的每个节点,调用每个节点的回调,如果遍历过程中遇到的节点是一个排他节点,则遍历将终止,后续节点将不再继续遍历. 总体逻辑可以由以下伪代码表示:
define sleep_list;
define wait_entry;
wait_entry.task= current_task;
wait_entry.callback = func1;
if (something_not_ready); then
# 进入阻塞路径
add_entry_to_list(wait_entry, sleep_list);
go on:
schedule();
if (something_not_ready); then
goto go_on;
endif
del_entry_from_list(wait_entry, sleep_list);
endif
... something_ready;
for_each(sleep_list) as wait_entry; do
wait_entry.callback(...);
if(wait_entry.exclusion); then
break;
endif
done
我们只需要密切注意此回调机制即可. 它可以做的不仅仅是选择/轮询/ epoll. Linux AIO也可以做到这一点. 注册回调,您几乎可以创建一个阻塞路径. 一般来说,回调包含以下逻辑:
common_callback_func(...)
{
do_something_private;
wakeup_common;
}
其中,do_something_private是wait_entry自己的自定义逻辑,而wakeup_common是公共逻辑,其目的是将wait_entry任务添加到CPU的就绪任务队列中,然后让CPU对其进行调度.
现在考虑一下. 如果实现select / poll,应该在wait_entry的回调上做什么?
.....
您知道,在大多数情况下,您需要有效地处理网络数据. 一项任务通常批量处理多个套接字,并读取其中的任何一个. 这意味着您必须公平对待所有这些套接字. 您可能不会在任何套接字的“数据读取”中被阻止,这意味着您不能以阻止模式在任何套接字上调用recv / recvfrom,这是复用套接字的基本要求.
假设N个套接字由同一任务处理,如何完成多路复用逻辑?显然,我们必须等待“数据可读”事件,而不是“实际数据”! !!我们要阻塞该事件,该事件是“ N个套接字中的一个或多个套接字具有数据可读性”,也就是说,只要解除此阻塞,就意味着必须有数据可读性,这意味着下一个Calling recv / recvform一定不能阻止!另一方面,该任务应同时列在所有这些套接字的sleep_list上. 只要可以读取数据,任何任务都可以唤醒任务.
然后,像select / poll这样的多路复用模型的设计就显而易见了.

select / poll的设计非常简单. 为每个套接字引入了轮询例程. 在此过程中对“可读数据”的判断如下:
poll()
{
...
if (接收队列不为空) {
ev |= POLL_IN;
}
...
}
当任务调用选择/轮询时,如果没有可用数据,任务将被阻止. 此时,它已被放置在所有N个套接字的sleep_list中. 只要一个套接字中有数据,任务就会被唤醒,下一件事是
for_each_N_socket as sk; do
event.evt = sk.poll(...);
event.sk = sk;
put_event_to_user;
done;
可以看出,只要一个套接字具有要读取的数据,就会遍历整个N个套接字,并且将再次调用poll函数以查看数据是否可读. 实际上,当唤醒在select / poll中阻塞的任务时,当时,它还不知道特定的套接字有要读取的数据. 它只知道这些套接字中的至少一个具有要读取的数据. 因此,它需要遍历以显示验证. 遍历完成后,用户状态任务可以使用返回的结果. 设置为读取带有事件的套接字.
可以看出select / poll是非常原始的. 如果有100,000个套接字(夸大了?),并且有可读的套接字,则系统必须遍历它. 因此epoll_wait 实现睡眠,选择仅限制了最多1024个可重用的套接字. 在Linux上,这是宏控制的. select / poll仅实现套接字多路复用,不适用于处理大容量网络服务器的方案. 瓶颈在于,随着套接字的增加,它无法在战时扩展.
既然wait_entry的回调可以做任何事情,那么在select / poll方案中,它能做的比唤醒up更多吗?
为此,epoll准备了一个名为ready_list的链表. ready_list中的所有套接字都有事件. 对于数据读取,确实存在数据可读性. epoll的wait_entry回调所有需要做的就是将自身添加到这个ready_list中. 等待epoll_wait返回时,只需要遍历ready_list. epoll_wait睡在单独的队列(single_epoll_waitlist)上,而不是套接字的睡眠队列上.
与select / poll不同,使用epoll的任务无需同时放入所有多路复用套接字的睡眠队列中. 这些套接字具有自己的队列,任务只需要在各自的队列中休眠即可等待事件. 也就是说,每个套接字的wait_entry的回调逻辑为:
epoll_wakecallback(...)
{
add_this_socket_to_ready_list;
wakeup_single_epoll_waitlist;
}
为此,epoll需要一个额外的调用(即epoll_ctrl ADD)来向epoll表添加套接字,它主要提供唤醒回调,该调用指定此套接字. 给一个epoll项,并将wait_entry的回调初始化为epoll_wakecallback. 协议栈的整个epoll_wait和唤醒逻辑如下:
协议栈唤醒套接字的睡眠队列

1. 数据包排队到套接字的接收队列中;
2. 唤醒套接字的睡眠队列,即调用每个wait_entry的回调;
3.callback将此套接字添加到ready_list;
4. 唤醒epoll_wait睡眠的单独队列.
从那时起,epoll_wait继续遍历ready_list中每个套接字的轮询历史记录并收集事件. 因为它是必不可少的,所以此过程是常规的. ready_list中的每个套接字都有要读取的数据,不能做无用的工作. 这是与select / poll的本质区别. ,而且还可以遍历所有对象. )
总而言之,epoll逻辑执行以下例程:
define wait_entry
wait_entry.socket = this_socket;
wait_entry.callback = epoll_wakecallback;
add_entry_to_list(wait_entry, this_socket.sleep_list);
define single_wait_list
define single_wait_entry
single_wait_entry.callback = wakeup_common;
single_wait_entry.task = current_task;
if (ready_list_is_empty); then
# 进入阻塞路径
add_entry_to_list(single_wait_entry, single_wait_list);
go on:
schedule();
if (sready_list_is_empty); then
goto go_on;
endif
del_entry_from_list(single_wait_entry, single_wait_list);
endif
for_each_ready_list as sk; do
event.evt = sk.poll(...);
event.sk = sk;
put_event_to_user;
done;
add_this_socket_to_ready_list;
wakeup_single_wait_list;
总而言之,您可以给出以下有关epoll的流程图,可以将其与本文第一部分中的流程图进行比较.
可以看出epoll和select / poll之间的本质区别在于,当事件发生时,每个epoll项(即套接字)都有自己的唤醒回调. 对于选择/民意测验,只有一个!这意味着在epoll中,会发生一个套接字事件,它可以调用其独立的回调来处理自身. 从宏观角度来看,epoll的效率在于两种睡眠等待时间的分离. 一种是epoll的睡眠等待. 它等待“任何套接字事件”,这是epoll_wait调用返回的条件. 它不适合直接睡眠在这种情况下,在套接字的睡眠队列中,谁将要睡眠?毕竟有这么多插座...所以它只能自己睡觉. 套接字的睡眠队列只能与自身相关,因此每种套接字本身都有另一种类型的睡眠等待,并且它可以在自己的队列中进行睡眠.
是时候提到ET和LT. 最大的争议是哪种性能很高,而不是如何使用它. 各种文件都说ET是有效的,但实际上并非如此. 出于实际目的,LT是高效且更安全的. 两者有什么区别?

ET: 仅在状态更改时通知,例如,当数据缓冲区从头开始读取(不可读)时,如果缓冲区中有数据,则不会一直得到通知;
LT: 只要缓冲区中有数据,它将始终被通知.
在检查了很多信息之后,答案仅是上面的答案,但是如果您看一下Linux的实现,它将使人们对ET更加困惑. 状态变化是什么?例如,一次在数据接收缓冲区中有10个数据包. 与上述流程图相比,很明显,唤醒操作将被调用10次. 这是否意味着该套接字将被添加到ready_list 10次?绝对不是这样. 当为第二个数据包调用唤醒回调时,发现套接字已经在ready_list中,并且不会再次添加. 此时,epoll_wait返回. 用户读取1个数据包后,假定程序有一个错误,不再读取,此时缓冲区中仍然有9个数据包,问题就来了,如果此时协议栈将另一个数据包排队,则会通知还是不通知? ?根据此概念,不会通知它,因为这不是“状态更改”,但是实际上,如果在Linux上尝试,它将被通知,因为只要套接字队列中有数据包,唤醒回调将被触发. 套接字将被放置在ready_list中. 对于ET,在epoll_wait返回之前,套接字已从ready_list中删除. 因此,如果在ET模式下,您发现该程序在epoll_wait中被阻止,则不能得出结论,它一定是由于未完全接收到数据包的原因引起的,或者可能是由于未完全接收到数据包的原因所致,但是,如果此时有新数据包出现,则epoll_wait仍将返回,尽管这不会使缓冲区的边缘改变到该状态.
因此,不能简单地将缓冲区状态的变化理解为存在或不存在,而是数据包的到达和不存在.
ET和LT是中断的概念. 如果您了解数据包的到来,也就是将其作为中断事件插入到套接字接收队列中,那不是所谓的边缘触发概念吗?
在代码实现的逻辑上,ET和LT实现之间的区别在于,一旦LT发生事件,它将始终添加到ready_list中,直到下一次轮询将其删除为止,然后在检测到以下事件后将其添加利益. ready_list. 轮询例程用于确定是否存在事件,而不是完全依赖于唤醒回调. 这是民意测验的真正含义,即不断进行民意测验!换句话说,LT模式被完全轮询. 它将每次轮询一次,直到轮询对事件不感兴趣时才会停止. 此时,只有数据包的到来可以依靠唤醒回调将其添加到ready_list中. . 在实现中,可以从以下代码中看到两者之间的区别.
epoll_wait
for_each_ready_list_item as entry; do
remove_from_ready_list(entry);
event = entry.poll(...);
if (event) then
put_user;
if (LT) then
# 以下一次poll的结论为结果
add_entry_to_ready_list(entry);
endif
endif
done
性能上的差异主要体现在数据结构的组织和算法上. 对于epoll,主要是链表操作和唤醒回调操作. 对于ET,是唤醒回调,将套接字添加到ready_list. 对于LT,除了唤醒回调可以将套接字添加到ready_list之外,epoll_wait还可以将其添加到ready_list进行下一次轮询. 唤醒回调的工作量较小,但这不是性能差异的根源. 基本要点是遍历链接列表. 如果在LT模式下有大量套接字,那么由于每个事件发生后它们都会再次添加到ready_list中,因此即使套接字没有事件,也仍将通过轮询进行确认. 这是额外的时间. 对于ET上的无事件套接字,没有有意义的遍历. 但是请注意,遍历链表的性能消耗只会在链表过长时反映出来. 您是否认为成千上万的插座将反映LT的劣势?的确,当读取数据时,ET确实会减少通知的数量,但这实际上并没有带来压倒性的优势.
LT确实比ET更容易使用,并且不容易死锁. 建议使用LT进行普通编程,而不要使用ET偶尔炫耀自己的技能.
Epoll的ET在阻塞模式下无法识别队列为空的事件,因此它只会在单个套接字的Recv上阻塞,而不是所有受监视套接字的epoll_wait调用,尽管它不会影响代码的运行,只要因为套接字具有数据到达的功能,但是它会影响编程逻辑,这意味着多路复用被解除,导致大量套接字处于饥饿状态. 即使有数据,也无法读取. 当然,对于LT来说,也存在类似的问题,但是LT会积极反馈数据以使其可读,因此由于编程错误,事件不会轻易被丢弃.
对于LT,由于它将不断反馈,因此只要有数据,您就可以随时读取它. 即使您使用它,也将始终有机会进行“下次轮询”,以主动检测是否有继续读取的数据. 阻塞模式,只要您不越过阻塞边界而导致其他套接字不足,就可以读取所需的任意数量的数据,但是对于ET,它会在通知应用程序该数据可读之后,但是新数据仍然会收到通知epoll_wait 实现睡眠,您无法控制新数据的发送和发送时间,因此您必须在离开之前读取所有数据. 一直读取意味着您必须能够检测到数据为空,因此,必须使用非阻塞模式,直到返回EAGIN错误.

1. 队列缓冲区的大小包括skb结构本身的长度,大约为230.
2. 在ET模式下,套接字在唤醒回调中添加到ready_list的次数> =接收到的数据包的数量,因此
多个数据报的到达速度足够快可能只会触发一次epoll唤醒回调的成功回调,并且套接字只会被添加到ready_list一次
=>完整队列
=>无法添加以下大消息
=>软木效果
=>可以填充缓冲区剩余漏洞的小消息可以触发ET模式下的epoll_wait返回. 如果最小长度为1,则可以发送长度为0的数据包,以促使epoll_wait返回
=>但是,由于skb结构的大小是固有的,因此上述诱惑不能保证成功.
3.epoll冲击组,可以参考ngx的经验
4.epoll还可以从NAPI中断解决方案中学习. 在Recv例程返回EAGIN或发生错误之前,将不再调用epoll的唤醒回调. 这意味着只要缓冲区不为空,就会接收到新数据. 该包裹将不会收到通知.
a. 只要调用了套接字的epoll唤醒回调,就禁用后续通知;
b.Recv例程在返回EAGIN或错误时启动后续通知.
本文来自电脑杂谈,转载请注明本文网址:
http://www.pc-fly.com/a/jisuanjixue/article-153389-1.html
表情在哪里
我会对美国说关你鸟事