深入理解Linux网络技术内幕——中断与网络驱动程序

接收到帧时通知驱动程序

在网络环境中,设备(网卡)接收到一个数据帧时,需要通知驱动程序进行处理。有一下几种通知机制:

轮询:

内核不断检查设备是否有话要说。(比较耗资源,但在一些情况下却是最佳方法)

中断:

特定事件发生时,设备驱动程序代表内核指示设备产生硬件中断,内核中断其它活动满足设备的需要。多数网络驱动程序使用中断。

中断期间处理多帧:

中断被通知,且驱动程序执行。然后保持帧的接收(载入),直到输入队列达到指定的数目、或者一直做下去知道队列清空、或者经过指定时间。

定时器驱动的中断事件

驱动程序指定设备定期产生中断事件(驱动程序主动,而不是设备主动,与前面的中断不同)。然后处理函数处理上次驱动以来到达的帧。这种机制会导致帧处理的延时,比如指定时间为100ms,而帧可能在第0ms、第50ms、也可能在第100ms刚好到达,平均延时为50ms。

组合机制

低流量负载下使用中断

高流量负载下使用定时器驱动的中断

中断的优缺点:

中断在低流量负载下是很好的选择,但在高流量负载情况下,由于没接收到一个帧就进行一次中断,很容易让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) */
};

时间: 2024-08-26 06:01:18

深入理解Linux网络技术内幕——中断与网络驱动程序的相关文章

深入理解Linux网络技术内幕——IPv4 报文的传输发送

报文传输,指的是报文离开本机,发往其他系统的过程. 传输可以由L4层协议发起,也可以由报文转发发起. 在深入理解Linux网络技术内幕--IPv4 报文的接收(转发与本地传递)一文中,我们可以看到,报文转发最后会调用dst_output与邻居子系统进行交互,然后传给设备驱动程序. 这里,我们从L4层协议发起的传输,最后也会经历这一过程(调用dst_output).本文讨论的是L4层协议发起的传输,在IPv4协议处理(IP层)中的一些环节. 大蓝图 我们先看下传输环节的大蓝图,以便对传输这一过程有

深入理解Linux网络技术内幕——网络设备初始化

概述 内核的初始化过程过程中,与网络相关的工作如下所示: 内核引导时执行start_kernel,start_kernel结束之前会调用rest_init,rest_init初始化内核线程init(在Linux3-12中为kernel_init). asmlinkage void __init start_kernel(void) { ... parse_early_param();//间接调用parse_args parse_args(...); //处理内核引导程序(boot loader)

《深入理解Linux网络技术内幕》阅读笔记 --- 路由

一.Linux内核中路由相关的主要数据结构 struct fib_result:对路由表查找后返回该结构,它的内容并不是简单的包含下一跳信息,而且包含其他特性,例如策略路由所需的更多参数. struct fib_rule:表示由策略路由在路由流量时选择路由表的规则 struct fib_node:一条路由表项.例如,该数据结构用于存储由route add或ip route add命令添加一条路由时生成的信息. struct fn_zone:一个zone表示子网掩码长度相同的一组路由 struct

深入理解Linux网络技术内幕——帧的接收与传输

帧的接收 NAPI与netif_rx(非NAPI) Linux内核获取网络帧到达通知的方式有两中:中断和轮询.(中断值设备向内核发出中断,轮询指linux内核主动轮询设备) 在早起的linux内核中,网络帧主要以中断的方式通知linux内核帧的到达.这是非NAPI方式. 现在的操作系统中,linux内核使用NAPI方式, 获取帧到达的消息.NAPI混合使用了中断和轮询. netif_rx(非NAPI): 每一个帧接收完毕时,设备向内核发送一个中断.(在低流量负载的情况下,这种方式对比轮询优势明显

深入理解Linux网络技术内幕——设备的注册于初始化(一)

副标题:设备注册相关的基本结构的原理框架 设备注册与删除时间 设备在下列两种情况下进行注册: 1)加载NIC驱动时 2)插入热插拔设备时 这里NIC与热插拔设备有些不同.a.对于非热插拔NIC来说,NIC的注册是伴随着其驱动的发生的,而NIC可以内建到内核,也可以作为模块载入,如果内建入内核,则NIC设备和初始化均发生在引导时,如果NIC作为模块加载,则NIC的注册和驱动初始化均发生在模块加载时.b. 对于热插拔NIC设备来说,其驱动已经加载,因此设备的注册发生在插入设备,内核通知关联驱动时.

深入理解Linux网络技术内幕——协议处理函数

网络帧在进入网络层时,需要区分不同的网络协议进行处理,这就需要涉及协议处理函数. 首先我们从驱动接收到一个数据帧,分析数据帧在协议栈中自下而上的传输流程. 设备驱动程序在接收到一个数据帧时,会将其保存在一个sk_buff缓冲区数据结构,并对其进行初始化. struct sk_buff { ...... __be16 protocol:16; ...... } 在这个缓冲区结构体中,有一个protocol字段,用于标识网络层的协议. 我们知道网络帧在设备驱动程序中处理后,设备驱动程序会调用neti

深入理解Linux网络技术内幕——路由子系统的概念与高级路由

本文讨论IPv4的路由子系统.(IPv6对路由的处理不同). 基本概念 路由子系统工作在三层,用来转发入口流量. 路由子系统主要设计 路由器.路由.路由表等概念. 路由器: 配备多个网络接口卡(NIC),并且能利用自身网络信息进行入口流量转发的设备. 路由: 流量转发,决定目的地的过程 路由表:转发信息库,该库中储存路由需要本地接收还是转发的信息, 以及转发流量时所需要的信息.(即,信息库用来判断,要不要转发,如果要转发,向哪里转发). 我们了解,路由器有多个网卡,但是多个NIC的设备不一定就是

深入理解Linux网络技术内幕——用户空间与内核空间交互

概述: 内核空间与用户空间经常需要进行交互.举个例子:当用户空间使用一些配置命令如ifconfig或route时,内核处理程序就要响应这些处理请求. 用户空间与内核有多种交互方式,最常用的有以下四种:通过/proc虚拟文件系统,通过/sys虚拟文件系统,通过ioctl系统调用,通过Netlink socket. 其中编写程序时最常使用ioctl,这四种方式中有两种是通过虚拟文件系统. procfs 与 sysctl procfs挂载/proc  sysctl挂载在/proc/sys(与后面介绍的

深入理解Linux网络技术内幕——IPv4 报文的接收(转发与本地传递)

我们知道,报文经过网卡驱动处理后,调用net_receive_skb传递给具体的协议处理函数,对于IPv4报文来说,其协议处理函数就是ip_rcv了,ip_rcv在进行一些健康检查等操作后,会调用ip_rcv_finish来处理报文.这也是IPv4协议对报文接收处理的开始. 我们先看下ip_rcv_finish源代码: ip_rcv_finish: //ip数据报文的主要处理程序(ip_rcv仅仅只是对ip数据报做一些健康性检查) //ip_rcv_finish 其实是进行路由表查询,,决定报文