zoukankan      html  css  js  c++  java
  • 深入理解Linux网络技术内幕——中断与网络驱动程序

    接收到帧时通知驱动程序

        在网络环境中。设备(网卡)接收到一个数据帧时,须要通知驱动程序进行处理。

    有一下几种通知机制:

    轮询:
        内核不断检查设备是否有话要说。(比較耗资源,但在一些情况下却是最佳方法)
    中断:
        特定事件发生时,设备驱动程序代表内核指示设备产生硬件中断,内核中断其他活动满足设备的须要。

    多数网络驱动程序使用中断。

    中断期间处理多帧:
        中断被通知。且驱动程序运行。

    然后保持帧的接收(加载),直到输入队列达到指定的数目、或者一直做下去知道队列清空、或者经过指定时间。

    定时器驱动的中断事件
        驱动程序指定设备定期产生中断事件(驱动程序主动。而不是设备主动,与前面的中断不同)。然后处理函数处理上次驱动以来到达的帧。

    这样的机制会导致帧处理的延时。比方指定时间为100ms。而帧可能在第0ms、第50ms、也可能在第100ms刚好到达,平均延时为50ms。

    组合机制
        低流量负载下使用中断
        高流量负载下使用定时器驱动的中断

    中断的优缺点:
        中断在低流量负载下是非常好的选择。但在高流量负载情况下,因为没接收到一个帧就进行一次中断,非常easy让CPU在处理中断上浪费时间,甚至崩溃。
        负责接收帧的代码,分为两部分(实际上为中断的上半部函数、下半部函数)。上半部函数将帧复制到输出队列,并运行其它一些不可抢占的工作。下半部函数的内容则是内核处理输入队列中的帧(将帧传给详细的协议处理)。因为上半部函数能够抢占下半部函数的运行,在高流量负载下,就有可能上半部函数一直运行,而下半部函数被搁置。而导致输入队列溢出,系统崩溃。



    中断处理函数

    为什么有下半部函数

        简单的说,下半部函数之所以存在是由于中断是不可抢占的。而我们假设花太多时间去处理一个中断,则可能导致其它中断迟迟不能运行。为此。我们将中断处理程序分为上半部函数和下半部函数。上半部函数主要运行中断处理程序中不可抢占的内容(如把帧从设备复制到输入队列)。下半部函数运行可被抢占的内容(如帧的详细给各自协议的处理)。
        上半部函数独占CPU资源运行,下半部函数运行时能够被其它中断抢占CPU资源。有了下半部函数后,中断处理程序的模型例如以下:
        1)设备发送信号给CPU,通知有中断事件
        2)CPU关中断,运行上半部函数
        3)上半部函数运行
        4)上半部函数运行完成。CPU开中断。并运行下半部函数
        上半部函数处理的主要内容包含:
        a)把内核稍后要处理的中断事件的全部信息保存到RAM
        b)设置标识,一边内核之后知道须要处理该中断。及怎样处理
        c)开中断,

    下半部函数解决方式

        内核提供多种下半部函数的解决方式,主要有旧式下半部、微任务、软IRQ三种。

    不同的解决方式的区别主要在于执行环境及并发与上锁。

        1)旧式下半部: 不论什么时刻,仅仅有一个旧式下半部函数能够运行(无论多少个CPU)
        2)微任务:        不论什么时刻。每一个CPU,仅仅有一个微任务实例能够运行.(多数情况下的选择)
        3)软IRQ:        不论什么时刻,一个CPU的每一个软IRQ仅仅有一个实例能够执行。(收发帧等须要及时响应的的网络任务的选择
    /***********************Linux-2.6.32************************************/
    //include/linux/hardirq.h
    in_irq()       //CPU正服务于硬件中断时,返回True
    in_softirq()   //CPU正服务于软件中断时,返回True
    in_interrupt() //CPU正在服务于一个硬件中断或软件中断。或抢占功能关闭时,返回True  
     
    //arch/x86/include/asm/hardirq.h
    local_softirq_pending()  //本地CPU至少有一个IRQ出于未决状态时,返回True
     
    //include/linux/interrupt.h
    __raise_softirq_irqoff()  //设置与软IRQ相关联的标识,将IRQ标记为未决
    raise_softirq_irqoff()    //__raise_softirq_irqoff包裹函数,当in_interrupt为False时,唤醒ksoftirqd
    raise_softirq()           //包裹raise_softirq_irqoff,调用raise_softirq_irqoff前先关中断
     
    //kernel/softirq.c
    __local_bh_enable()   //开启本地CPU的下半部 
    local_bh_enable()     //假设有不论什么软IRQ未决,且in_interrupt返回False,则invoke_softirq
    local_bh_disable()    //关闭CPU下半部
     
    //include/linux/irqflags.h
    local_irq_enable()    //开启本地CPU中断功能
    local_irq_disable()   //关闭本地CPU中断功能
    local_irq_save()      //先把本地CPU中断状态保存,再予以关闭
    local_irq_restore()   //恢复本地CPU之前的中断状态,恢复local_irq_save保存的中断信息
     
    //include/linux/spinlock.h
    spin_lock_bh()      //取得回旋锁。关闭下半部及抢占功能
    spin_unlock_bh()    //释放回旋锁,重新启动下半部抢占功能



    抢占功能

        Linux2.5之后的内核实现了全然抢占(preemptitle)的功能。(即内核本身也能够被抢占)。可是有些时候。内核运行的任务不希望被抢占,(比方正在服务于硬件)这时就须要关闭抢占功能。以下是几个与抢占功能的管理相关的函数。

    //inculde/linux/preempt.h
    preempt_disable()          //为当前任务关闭抢占功能。可反复调用,递增引用计数器
    preempt_enable()           //抢占功能再度开启,(须要先检查引用计数器是否为0)
    preempt_enable_no_resch()  //递减引用计数器,仅仅有引用计数器为0时,抢占功能才干再度开启
    preempt_check_resched()    //由preempt_enable调用,检查引用计数器是否为0.
     
    // arch/x86/include/asm/thread_info.h
    struct thread_info {
        ……
        int         preempt_count;  /* 0 => preemptable,
                               <0 => BUG */ //抢占计数器,指定进程能否被抢占
        ……
    };


    下半部函数

       下半部函数的基础构架有下面几个部分:
    1)分类:把下半部函数分成适当类型
    2)关联:注冊(登记)下半部函数类型及其处理函数间的关联关系
    3)调度:为下半部函数进行调度,以准备运行
    4)通知:通知内核BH的存在

    旧式下半部函数(linux-2.2曾经)

        旧式下半部函数模型(如linux-2.2版本号)把下半部分为非常多种类型。例如以下:
    enum {
        TIMER_BH = 0,
        CONSOLE_BH,
        TQUEUE_BH,
        DIGI_BH,
        SERIAL_BH,
        RISCOM8_BH,
        SPECIALIX_BH,
        AURORA_BH,
        ESP_BH,
        NET_BH,      //网络下半部
        SCSI_BH,
        IMMEDIATE_BH,
        KEYBOARD_BH,
        CYCLADES_BH,
        CM206_BH,
        JS_BH,
        MACSERIAL_BH,
        ISICOM_BH
    };


    各个类型及其处理函数用init_bh()关联。如网络下半部在net_dev_init中关联
    _ _initfunc(int net_dev_init(void))
    {
        ... ... ...
        init_bh(NET_BH, net_bh);
        ... ... ...
    }


    中断处理函数要触发下半部函数时,就使用mark_bh在全局位图bh_active设置标志位
    extern inline void mark_bh(int nr)
    {
        set_bit(nr, &bh_active);
    };


    如网络设备接收到一个帧时,就调用netif_rx通知内核,将帧复制到输入队列backlog,然后标记NET_BH下半部标识:
    skb_queue_tail(&backlog, skb);
    mark_bh(NET_BH);
    return

    引入软IRQ

        linux-2.4版本号以后的linux内核引入了软IRQ。

    (软IRQ能够视为IRQ的多线程版本号)

        新式软IRQ有下面几种类型(linux-2.4仅仅有六种。后面又发展了):
    //include/linux/interrupt.h
    enum
    {
        HI_SOFTIRQ=0,     //高优先级微任务
        TIMER_SOFTIRQ,
        NET_TX_SOFTIRQ,  //网络软IRQ
        NET_RX_SOFTIRQ,  //网络软IRQ
        BLOCK_SOFTIRQ,
        BLOCK_IOPOLL_SOFTIRQ,
        TASKLET_SOFTIRQ,  //低优先级微任务软IRQ
        SCHED_SOFTIRQ,
        HRTIMER_SOFTIRQ,
        RCU_SOFTIRQ,    /* Preferable RCU should always be the last softirq */
     
        NR_SOFTIRQS
    };      
        一种软IRQ在一个CPU上仅仅能由一个实例在执行。
        为此,每种软IRQ类型维护一个softnet_data类型的数组,数组的大小为CPU的数目。而每一个CPU相应该类型的软IRQ维护一个 softnet_data的数据结构。
    /*
     * Incoming packets are placed on per-cpu queues so that
     * no locking is needed.
     */
    struct softnet_data
    {
        struct Qdisc        *output_queue;      //qdisc是queueing discipline的简写。也就是排队规则,即qos.这里也就是输出帧的控制。
        struct sk_buff_head input_pkt_queue;    //当输入帧被驱动取得之前,就保存在这个队列里,(不适用与napi驱动,napi有自己的私有队列)
        struct list_head    poll_list;          //表示有输入帧待处理的设备链表。 
        struct sk_buff      *completion_queue;  //表示已经成功被传递出的帧的链表。
     
        struct napi_struct  backlog;            //用来兼容非napi的驱动。                                                                    
    };

    初始化在net_dev_init中
    static int __init net_dev_init(void)
    {
        ......
        for_each_possible_cpu(i) {
            struct softnet_data *queue;
     
            queue = &per_cpu(softnet_data, i);
            skb_queue_head_init(&queue->input_pkt_queue);
            queue->completion_queue = NULL;
            INIT_LIST_HEAD(&queue->poll_list);
     
            queue->backlog.poll = process_backlog;
            queue->backlog.weight = weight_p;
            queue->backlog.gro_list = NULL;
            queue->backlog.gro_count = 0;
        }
        ......
    }


    软IRQ的注冊于调度机制

        软IRQ的注冊与调度机制与旧式模型类似,仅仅是函数不一样。


        相应init_bh(),软IRQ使用spen_softirq()对软IRQ类型与其关联函数的关系进行注冊。

    // kernel/softirq.c
    void open_softirq(int nr, void (*action)(struct softirq_action *))                            
    {
        softirq_vec[nr].action = action;
    }

    软IRQ通过下列函数在本地CPU上进行调度,准备运行:
    __raise_softirq_irqoff()  //设置与软IRQ相关联的标识,将IRQ标记为未决
    raise_softirq_irqoff()    //__raise_softirq_irqoff包裹函数,当in_interrupt为False时,唤醒ksoftirqd
    raise_softirq()           //包裹raise_softirq_irqoff,调用raise_softirq_irqoff前先关中断
    软IRQ详细的运行參考其它博文
    do_IRQ
    schecule
    do_softirq
    參考其它博文


    微任务

        微任务是建立在软IRQ的基础之上的。相应软IRQ的HI_SOFTIRQ(高优先级微任务)和TASKLET_SOFTIRQ(普通优先级微任务)。
        每一个CPU有两份tasklet_struct表。一份相应HI_SOFTIRQ,一份相应TASKLET_SOFTIRQ。
    /*
     * Tasklets
     */
    struct tasklet_head
    {
        struct tasklet_struct *head;                                                                                                        
        struct tasklet_struct **tail;
    };
     
    static DEFINE_PER_CPU(struct tasklet_head, tasklet_vec);
    static DEFINE_PER_CPU(struct tasklet_head, tasklet_hi_vec);


    微任务有一些特征(与旧式下半部函数的差别)
    1)微任务不限数目,可是base_bh的每一位标识仅仅限于一种类型的下半部函数
    2)微任务提供两种等级的优先级
    3)不同微任务能够再不同CPU上同事执行
    4)微任务相对于旧式下半部来说是动态的,不须要静态地在XXX_BH或XXX_SOFTIRQ枚举列表中静态声明
    struct tasklet_struct
    {
        struct tasklet_struct *next;  //把关联到同一个CPU的结构链接起来                                                                                                   
        unsigned long state;          //位图标识,其可能的取值由TASKLET_STATE_XXX枚举
        atomic_t count;               //计数器,0表示微任务被关闭,不可运行。非0表示微任务已经开启
        void (*func)(unsigned long);  //要运行的函数
        unsigned long data;           //上面函数的參数
    }; 
     
    enum
    {
        TASKLET_STATE_SCHED,    /* Tasklet is scheduled for execution */
        TASKLET_STATE_RUN   /* Tasklet is running (SMP only) */
    };




  • 相关阅读:
    设计模式学习笔记——Bridge 桥接模式
    设计模式学习笔记——Adapter 适配器模式
    protoc protobuff安装
    docker-compose启动consul
    docker etcd 环境搭建
    nifi的去重方案设计(二)-外部存储mysql全局去重
    实现一套ES全文检索语法-到Lucene语法的转换工具,以实现在es外部兼容处理文本分词
    nifi的去重方案设计(一)-单队列内去重.md
    k8s 证书过期处理
    部分项目从kafka迁移至pulsar,近期使用中碰到了一些问题,勉强把大的坑踩完了,topic永驻,性能相关
  • 原文地址:https://www.cnblogs.com/lcchuguo/p/5202190.html
Copyright © 2011-2022 走看看