接收到帧时通知驱动程序
在网络环境中,设备(网卡)接收到一个数据帧时,需要通知驱动程序进行处理。有一下几种通知机制:
轮询:
内核不断检查设备是否有话要说。(比较耗资源,但在一些情况下却是最佳方法)
中断:
特定事件发生时,设备驱动程序代表内核指示设备产生硬件中断,内核中断其它活动满足设备的需要。多数网络驱动程序使用中断。
中断期间处理多帧:
中断被通知,且驱动程序执行。然后保持帧的接收(载入),直到输入队列达到指定的数目、或者一直做下去知道队列清空、或者经过指定时间。
定时器驱动的中断事件
驱动程序指定设备定期产生中断事件(驱动程序主动,而不是设备主动,与前面的中断不同)。然后处理函数处理上次驱动以来到达的帧。这种机制会导致帧处理的延时,比如指定时间为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) */ };