zoukankan      html  css  js  c++  java
  • Linux内核中网络数据包的接收-第二部分 select/poll/epoll

    和前面文章的第一部分一样。这些文字是为了帮别人或者自己理清思路的,而不是所谓的源代码分析,想分析源代码的,还是直接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或者错误的时候,開始兴许的通知。

  • 相关阅读:
    oracle plsql 统计
    oracle plsql 自定义异常
    oracle plsql 异常
    oracle 游标
    oracle 存储函数,更新库存
    oracle TRUNC()函数
    plsql 的三种循环
    plsql if
    plsql 记录型变量
    CAS示例环境部署及配置
  • 原文地址:https://www.cnblogs.com/yjbjingcha/p/7097179.html
Copyright © 2011-2022 走看看