和前面文章的第一部分一样,这些文字是为了帮别人或者自己理清思路的。而不是所谓的源代码分析。想分析源代码的,还是直接debug源代码最好,看不论什么文档以及书都是下策。

因此这类帮人理清思路的文章尽可能的记成流水的方式,尽可能的简单明了。

Linux 2.6+内核的wakeup callback机制

Linux内核通过睡眠队列来组织全部等待某个事件的task,而wakeup机制则能够异步唤醒整个睡眠队列上的task,每个睡眠队列上的节点都拥有一个callback,wakeup逻辑在唤醒睡眠队列时,会遍历该队列链表上的每个节点,调用每个节点的callback,假设遍历过程中遇到某个节点是排他节点,则终止遍历。不再继续遍历后面的节点。整体上的逻辑能够用以下的伪代码表示:

睡眠等待

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

我们仅仅须要狠狠地关注这个callback机制,它能做的事真的不止select/poll/epoll,Linux的AIO也是它来做的,注冊了callback。你差点儿能够让一个堵塞路径在被唤醒的时候做不论什么事情。

一般而言,一个callback里面都是以下的逻辑:

common_callback_func(...)
{
do_something_private;
wakeup_common;
}

当中。do_something_private是wait_entry自己的自己定义逻辑,而wakeup_common则是公共逻辑。旨在将该wait_entry的task增加到CPU的就绪task队列,然后让CPU去调度它。

如今留个思考,假设实现select/poll,应该在wait_entry的callback上做什么文章呢?
       .....

select/poll的逻辑

要知道,在大多数情况下。要高效处理网络数据,一个task通常会批量处理多个socket,哪个来了数据就去读那个,这就意味着要公平对待全部这些socket,你不可能堵塞在不论什么socket的“数据读”上,也就是说你不能在堵塞模式下针对不论什么socket调用recv/recvfrom,这就是多路复用socket的实质性需求。

假设有N个socket被同一个task处理。怎么完毕多路复用逻辑呢?非常显然。我们要等待“数据可读”这个事件,而不是去等待“实际的数据”!。我们要堵塞在事件上,该事件就是“N个socket中有一个或多个socket上有数据可读”,也就是说,仅仅要这个堵塞解除,就意味着一定有数据可读,意味着接下来调用recv/recvform一定不会堵塞!还有一方面。这个task要同一时候排入全部这些socket的sleep_list上,期待随意一个socket仅仅要有数据可读。都能够唤醒该task。
       那么,select/poll这类多路复用模型的设计就显而易见了。

select/poll的设计非常easy。为每个socket引入一个poll例程,该历程对于“数据可读”的推断例如以下:

poll()
{
...
if (接收队列不为空) {
ev |= POLL_IN;
}
...
}

当task调用select/poll的时候。假设没有数据可读。task会堵塞。此时它已经排入了全部N个socket的sleep_list,仅仅要有一个socket来了数据,这个task就会被唤醒,接下来的事情就是

for_each_N_socket as sk; do
event.evt = sk.poll(...);
event.sk = sk;
put_event_to_user;
done;

可见。仅仅要有一个socket有数据可读,整个N个socket就会被遍历一遍调用一遍poll函数。看看有没有数据可读,其实,当堵塞在select/poll的task被唤醒的时候,它根本不知道详细socket有数据可读。它仅仅知道这些socket中至少有一个socket有数据可读。因此它须要遍历一遍。以示求证。遍历完毕后。用户态task能够依据返回的结果集来对有事件发生的socket进行读操作。
       可见。select/poll非常原始,假设有100000个socket(夸张吗?),有一个socket可读,那么系统不得不遍历一遍...因此select仅仅限制了最多能够复用1024个socket,而且在Linux上这是宏控制的。select/poll仅仅是朴素地实现了socket的多路复用,根本不适合大容量网络server的处理场景。其瓶颈在于,不能随着socket的增多而战时扩展性。

epoll对wait_entry callback的利用

既然一个wait_entry的callback能够做随意事,那么是否能让其做的比select/poll场景下的wakeup_common很多其它呢?
       为此,epoll准备了一个链表。叫做ready_list,全部处于ready_list中的socket,都是有事件的,对于数据读而言。都是确实有数据可读的。

epoll的wait_entry的callback要做的就是,将自己自行增加到这个ready_list中去。等待epoll_wait返回的时候。仅仅须要遍历ready_list就可以。epoll_wait睡眠在一个单独的队列(single_epoll_waitlist)上,而不是socket的睡眠队列上。
       和select/poll不同的是,使用epoll的task不须要同一时候排入全部多路复用socket的睡眠队列,这些socket都拥有自己的队列,task仅仅须要睡眠在自己的单独队列中等待事件就可以,每个socket的wait_entry的callback逻辑为:

epoll_wakecallback(...)
{
add_this_socket_to_ready_list;
wakeup_single_epoll_waitlist;
}

为此。epoll须要一个额外的调用,那就是epoll_ctrl ADD。将一个socket增加到epoll table中,它主要提供一个wakeup callback,将这个socket指定给一个epoll entry,同一时候会初始化该wait_entry的callback为epoll_wakecallback。整个epoll_wait以及协议栈的wakeup逻辑例如以下所看到的:
协议栈唤醒socket的睡眠队列
1.数据包排入了socket的接收队列;。
2.唤醒socket的睡眠队列,即调用各个wait_entry的callback;
3.callback将自己这个socket增加ready_list;
4.唤醒epoll_wait睡眠在的单独队列。

自此。epoll_wait继续前行。遍历调用ready_list里面每个socket的poll历程,搜集事件。这个过程是例行的,由于这是不可缺少的,ready_list里面每个socket都有数据可读,做不了无用功,这是和select/poll的本质差别(select/poll中,即便没有数据可读。也要全部遍历一遍)。
       总结一下,epoll逻辑要做以下的例程:

epoll add逻辑

define wait_entry
wait_entry.socket = this_socket;
wait_entry.callback = epoll_wakecallback;
add_entry_to_list(wait_entry, this_socket.sleep_list);

epoll wait逻辑

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;

epoll唤醒的逻辑

add_this_socket_to_ready_list;
wakeup_single_wait_list;

综合以上。能够给出以下的关于epoll的流程图。能够对照本文第一部分的流程图做比較

能够看出。epoll和select/poll的本质差别就是,在发生事件的时候,每个epoll item(也就是socket)都拥有自己单独的一个wakeup callback,而对于select/poll而言。仅仅有一个!这就意味着epoll中,一个socket发生事件,能够调用其独立的callback来处理它自身。从宏观上看,epoll的高效在于分离出了两类睡眠等待。一个是epoll本身的睡眠等待。它等待的是“随意一个socket发生事件”,即epoll_wait调用返回的条件,它并不适合直接睡眠在socket的睡眠队列上,假设真要这样,究竟睡谁呢?毕竟那么多socket...因此它仅仅睡自己。一个socket的睡眠队列一定要仅仅和它自己相关。因此还有一类睡眠等待是每个socket自身的,它睡眠在自己的队列上就可以。

epoll的ET和LT

是时候提到ET和LT了,最大的争议在于哪个性能高。而不是究竟怎么用。各种文档上都说ET高效,但其实,根本不是这样,对于实际而言,LT高效的同一时候。更安全。

两者究竟什么差别呢?

概念上的差别

ET:仅仅有状态发生变化的时候,才会通知。比方数据缓冲去从无到有的时候(不可读-可读),假设缓冲区里面有数据,便不会一直通知。
LT:仅仅要缓冲区里面有数据。就会一直通知。
查了非常多资料,得到的答案无非就是相似上述的。然而假设看Linux的实现,反而让人对ET更加迷惑。什么叫状态发生变化呢?比方数据接收缓冲区里面一次性来了10个数据包,对照上述流程图。非常显然会调用10次的wakeup操作,是不是意味着这个socket要被增加ready_list 10次呢?肯定不是这种,第二个数据包到来调用wakeup callback时,发现该socket已经在ready_list了。肯定不会再加了,此时epoll_wait返回,用户读取了1个数据包之后。假设程序有bug。便不再读取了。此时缓冲区里面还有9个数据包。问题来了。此时假设协议栈再排入一个包,究竟是通知还是不通知呢??依照概念理解,不会通知了,由于这不是“状态的变化”,可是其实在Linux上你试一下的话,发现是会通知的,由于仅仅要有包排入socket队列。就会触发wakeup callback,就会将socket放入ready_list中,对于ET而言,在epoll_wait返回前,socket就已经从ready_list中摘除了。因此,假设在ET模式下,你发现程序堵塞在epoll_wait了,并不能下结论说一定是数据包没有收完一个原因导致的。也可能是数据包确实没有收完,但假设此时来一个新的数据包。epoll_wait还是会返回的。尽管这并没有带来缓冲去状态的边沿变化。

因此。对于缓冲区状态的变化。不能简单理解为有和无这么简单,而是数据包的到来和不到来。
       ET和LT是中断的概念,假设你把数据包的到来。即插入到socket接收队列这件事理解成一个中断事件,所谓的边沿触发不就是这个概念吗?

实现上的差别

在代码实现的逻辑上,ET和LT实现的差别在于LT一旦有事件则会一直加进ready_list。直到下一次的poll将其移出,然后在探測到感兴趣事件后再将其加进ready_list。由poll例程来推断是否有事件,而不是全然依赖wakeup callback,这是真正意义的poll。即不断轮询!也就是说。LT模式是全然轮询的,每次都会去poll一次。直到poll不到感兴趣的事件,才会休息。此时就仅仅有数据包的到来能够又一次依赖wakeup callback将其增加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而言。主要就是链表操作和wakeup callback操作,对于ET而言,是wakeup callback将socket增加到ready_list,而对于LT而言。则除了wakeup callback能够将socket增加到ready_list之外,epoll_wait也能够将其为了下一次的poll增加到ready_list,wakeup callback中反而有更少工作量。但这并非性能差异的根本。性能差异的根本在于链表的遍历。假设有海量的socket採用LT模式,由于每次发生事件后都会再次将其增加ready_list。那么即便是该socket已经没有事件了。还是会用一次poll来确认。这额外的一次对于无事件socket没有意义的遍历在ET上是没有的。可是注意。遍历链表的性能消耗仅仅有在链表超长时才会体现,你认为千儿八百的socket就会体现LT的劣势吗?诚然。ET确实会降低数据可读的通知次数,但这其实并没有带来压倒性的优势。
       LT确实比ET更easy使用,也不easy死锁,还是建议用LT来正常编程。而不是用ET来偶尔炫技。

编程上的差别

epoll的ET在堵塞模式下,无法识别到队列空事件,从而仅仅是堵塞在单独一个socket的Recv而不是全部被监控socket的epoll_wait调用上。尽管不会影响代码的执行,仅仅要该socket有数据到来便好,可是会影响编程逻辑。这意味着解除了多路复用的武装,造成大量socket的饥饿。即便有数据了,也没法读。

当然,对于LT而言。也有相似的问题,可是LT会激进地反馈数据可读,因此事件不会轻易由于你的编程错误而被丢弃。
       对于LT而言,由于它会不断反馈,仅仅要有数据,你想什么时候读就能够什么时候读。它永远有“下一次poll”的机会主动探知是否有数据能够继续读。即便使用堵塞模式,仅仅要不要跨越堵塞边界造成其它socket饥饿。读多少数据均能够,可是对于ET而言,它在通知你的应用程序数据可读后。尽管新的数据到来还是会通知,可是你并不能控制新的数据一定会来以及什么时候来。所以你必须读全然部的数据才干离开,读全然部的时候意味着你必须能够探知数据为空,因此也就是说,你必须採用非堵塞模式,直到返回EAGIN错误。

给出几个ET模式下的tips

1.队列缓冲区的大小包含skb结构体本身的长度,230左右
2.ET模式下。wakeup callback中将socket增加ready_list的次数 >= 收到数据包的个数,因此
多个数据报足够快到达可能仅仅会触发一次epoll wakeup callback的成功回调,此时仅仅会将socket增加进ready_list一次
        =>造成队列满
                =>兴许的大报文加不进去
        =>瓶塞效应
        =>能够填补缓冲区剩余hole的小报文能够触发ET模式的epoll_wait返回。假设最小长度就是1,那么能够发送0长度的包引诱epoll_wait返回
            =>可是由于skb结构体的大小是固有大小,以上的引诱不能保证会成功。

3.epoll惊群,能够參考ngx的经验
4.epoll也可借鉴NAPI关中断的方案,直到Recv例程返回EAGIN或者错误发生,epoll的wakeup callback不再被调用。这意味着仅仅要缓冲区不为空。就算来了新的数据包也不会通知了。
a.仅仅要socket的epoll wakeup callback被调用,禁掉兴许的通知;
b.Recv例程在返回EAGIN或者错误的时候,開始兴许的通知。

Linux内核中网络数据包的接收-第二部分 select/poll/epoll的更多相关文章

  1. Linux内核中网络数据包的接收-第一部分 概念和框架

    与网络数据包的发送不同,网络收包是异步的的.由于你不确定谁会在什么时候突然发一个网络包给你.因此这个网络收包逻辑事实上包括两件事:1.数据包到来后的通知2.收到通知并从数据包中获取数据这两件事发生在协 ...

  2. Linux网络 - 数据包的接收过程【转】

    转自:https://segmentfault.com/a/1190000008836467 本文将介绍在Linux系统中,数据包是如何一步一步从网卡传到进程手中的. 如果英文没有问题,强烈建议阅读后 ...

  3. [转]Linux网络 - 数据包的接收过程

    转, 原文: https://segmentfault.com/a/1190000008836467 ------------------------------------------------- ...

  4. linux内核中网络文件系统的注册初始化

    针对内核3.9 系统开启时,会使用init/main.c,然后再里面调用kernel_init(),在里面会再调用do_basic_setup(),调用do_initcalls(),调用do_one_ ...

  5. Linux网络 - 数据包的接收过程(转)

    https://segmentfault.com/a/1190000008836467

  6. 游戏中网络数据包和HTTP数据的思考

    快下班了,对于这个沙盒类文字游戏,其实考虑的东西还是很多的,服务器的架构,NPC, NPC API的运算,等等等 现在在思考大数据传输,比如背包数据或者拍卖行的商品展示数据在传输的时候的性能问题 目前 ...

  7. linux 内核网络数据包接收流程

    转:https://segmentfault.com/a/1190000008836467 本文将介绍在Linux系统中,数据包是如何一步一步从网卡传到进程手中的. 如果英文没有问题,强烈建议阅读后面 ...

  8. [转]Linux网络 - 数据包的发送过程

    转, 原文:https://segmentfault.com/a/1190000008926093 -------------------------------------------------- ...

  9. Linux 中的网络数据包捕获

    Linux 中的网络数据包捕获 Ashish Chaurasia, 工程师 简介: 本教程介绍了捕获和操纵数据包的不同机制.安全应用程序,如 VPN.防火墙和嗅探器,以及网络应用程序,如路由程序,都依 ...

随机推荐

  1. Sequence(优先队列)

    http://poj.org/problem?id=2442 题意:给你n*m的矩阵,然后每行取一个元素,组成一个包含n个元素的序列,一共有n^m种序列, 让你求出序列和最小的前n个序列的序列和. # ...

  2. (三)Appium-desktop 打包

    appium-desktop经过二次开发后,需要打包为应用提供给其它同学使用.我们知道appium-desktop是使用electron来构建跨平台桌面应用程序.electron有electron-p ...

  3. Oracle 12.2.0.1 RAC for rhel 7.X 数据库安装(节点1执行root.sh失败)

    说明: 最开始是用的rehat7.2安装12.2.0.1,后面安装GI节点一执行root.sh脚本失败,排查原因,最开始以为是操作系统的问题,换成rehat7.6,同样的出现问题,经过一番折腾,后面通 ...

  4. 06-联系人管理(xib应用)

    ViewController.h文件中: @interface ViewController : UIViewController - (IBAction)add:(UIBarButtonItem * ...

  5. Android 使用SQLite存储以及读取Drawable对象

    在进行Android开发过程中,我们经常会接触到Drawable对象,那么,若要使用数据库来进行存储及读取,该如何实现? 一.存储 //第一步,将Drawable对象转化为Bitmap对象 Bitma ...

  6. Centos 自动删除日志文件的Shell代码

    #!/bin/bash # #判断文件夹内文件的大小,如果大于一定的数值,那么删除 # echo '判断文件夹内文件的大小,如果大于一定的数值,并且文件名称包含数字(年月日)的删除,那么删除' pat ...

  7. 【Linux】Ubuntu输入法不能开机自启的解决方法

    操作系统:Ubuntu Kylin 16.10 自从操作系统安装了搜狗输入法以后,每次重启电脑都需要手动启动Fcitx,才能启动搜狗输入法.下面给大家介绍输入法开机自启的解决方法: 操作系统的用户家目 ...

  8. SQLite 在 Android 的应用

    Android提供了创建和使用SQLite数据库的API(Application Programming Interface,应用程序编程接口). 在Android系统中,主要由类SQLiteData ...

  9. 【sqli-labs】 less23 Error based - strip comments (GET型基于错误的去除注释的注入)

    . 加单引号报错 加# http://localhost/sqli-labs-master/Less-23/?id=1'%23 错误没有改变,推测过滤了# 查看源码发现# -- 都被替换掉了 那么可用 ...

  10. swift里 as、as!、as?区别 T.Type与动态类型

    as 1.编译器进行类型转换合法性检查:静态 let cell = collectionView.dequeueReusableCell(withReuseIdentifier: shoppingLi ...