9.1 Decisions and Traffic Direction
描述了数据包的传输方向和协议栈的关系。分为三类:
- 传入的包:
从L1一直到上层的应用层,一级一级传递。
- 发出的包:
与上述的相反,中最上层的应用层,一级一级向下传递,直到L1。
- 转发的包:
L1层接收到了数据包,需要在IP层(L3)判断是否为转发包,如果是,则不再向上层传递,经过处理后转而发送至L1。
A black cat stalking a spider
9.2 Notifying Drivers When Frames Are Received
9.2.2 中断方式
如果有人为邮筒做了个装置,这个装置可以在邮筒中有信件的时候通知邮递员,那么就成了类似中断了。这样在多数情况下可以较好的满足性能上的需求,但是当网卡附负载太重的时候,会给CPU带来较多的负载:CPU在处理中断上会花费过多的时间。还拿上面的例子,在邮件太多的时候,如果采用中断方式,那么邮递员就有的忙了。
采用中断处理时,对于输入的frame的处理可以分成两个部分:
- 驱动:
驱动将数据拷贝到内核能够访问的输入列队里。这部分工作在中断上下文中,
- 内核:
处理列队中的数据,并将其向协议栈的上一层传递。
9.2.3 单个中断中处理多帧数据
在内核接收到中断以后,可以暂时关闭其他的中断源,转而一直输入列队中读取数据。这样就可以在单个中断中处理多帧数据了。但是很少有驱动这样去做。
NAPI,是这中技术的一个改进。NAPI中,当内核受到了网卡的中断信号以后,暂时关闭这个网卡上的中断,并在一定时间内将对该网卡的处理由中断改为轮询。这样可以在负载较高的负载环境中包书较好的效率。
9.2.4 Timer-Driven Interrupts
见过路边的邮筒吧?上面写了每天的几点会打开(有邮递员来将信取走)。或者说,每隔多长时间邮递员过来查一次。
9.2.5 Combinations
先用中断方式,等中断被触发后,改用timer-driven。
9.3 中断处理函数
9.3.1 Bottom Half Handlers
中断的handler 通常用数字来标识。
中断函数在运行起见,内核代码运行在中断上下文中,在该上下文中,由于CPU在处理中断,所有的中断源被暂时关闭。也就是说,如果CPU在进行中断处理的时候,它不能接收其他的中断请求,同时他也不能去执行其他的进程:CPU已经完全被中断处理函数占用,而且不能被抢占。总之:中断处理函数不可抢占,不可重入。
这种设计可以避免竞争冒险,但是这种不可抢占的方式,无疑降低内核的整体性能。
因此,内核的中断处理应该越短越好。结合实际,硬件上的存储空间有限,且一旦丢失则无法挽回。而用户空间中的进程,则通常可以适当的延迟一下,秋后问斩。因此,现代的中断处理被分成了两个部分:top half & bottom half。其中的top half必须在独占CPU的时候去做,在释放CPU之前完成,主要负责处理硬件相关,例如拷贝数据至列队。而bottom half,则可以在释放CPU之后,在后面CPU相对“清闲”的时候去完成。从这个角度来讲,可以将bottom half看作是一个异步的调用请求:调用bottom half中的某个函数去完成预定的操作。
使用这个方法,我们可以重新定义一下中断处理的模型:
- 1. 设备向CPU发出信号,作为中断通知;
- 2. 关闭中断,执行top half,主要包括:
- 在内存中记录所有内核处理该中断所需的信息。
- 通过某种方式作出标记,以确保内核会在稍后用前面保存的信息来处理这个中断。
- 重新打开前面关闭的中断。
- 3. CPU空闲的时候,检测前面设置的标记,处理数据(bottom half),并将前面设置的标记清零。
9.3.2 Bottom Halves Solutions
Bottom half有多种构建方式,其区别主要在于运行的上下文环境以及并发处理和锁处理。
9.3.4 内核的抢占
内核抢占相关函数:
- preemptdisable
Disables preemption for the current task. Can be called repeatedly, incrementing a reference counter.
- preempt_enable
Enable 抢占
- preempt_enable_no_resched
The reverse of preempt_disable, allowing preemption to be enabled again.preempt_enable_no_resched simply decrements a reference counter, whichallows preemption to be re-enabled when it reaches zero. preempt_enable,in addition, checks whether the counter is zero and forces a call toschedule( ) to allow any higher-priority task to run.
- preempt_check_resched
This function is called by preempt_enable and differentiates it frompreempt_enable_no_resched.
9.3.5 Bottom-Half Handlers
9.3.6 SoftIRQ Initialization
SoftIRQ 的出初始化由函数softirq_init完成,该函数在系统初始化函数start_kernel中被调用,代码如下:
void __init softirq_init(void){ int cpu; for_each_possible_cpu(cpu) { int i; per_cpu(tasklet_vec, cpu).tail = &per_cpu(tasklet_vec, cpu).head; per_cpu(tasklet_hi_vec, cpu).tail = &per_cpu(tasklet_hi_vec, cpu).head; for (i = 0; i < NR_SOFTIRQS; i++) INIT_LIST_HEAD(&per_cpu(softirq_work_list[i], cpu)); } register_hotcpu_notifier(&remote_softirq_cpu_notifier); open_softirq(TASKLET_SOFTIRQ, tasklet_action); open_softirq(HI_SOFTIRQ, tasklet_hi_action);} |
网络代码相关的 NET_RX_SOFTIRQ 和 NET_TX_SOFTIRQ的初始化则在net_dev_init中。
static int __init net_dev_init(void){ ... open_softirq(NET_TX_SOFTIRQ, net_tx_action); open_softirq(NET_RX_SOFTIRQ, net_rx_action); ...} |
9.3.7 Pending SoftIRQ的处理
函数do_softirq用于处理Pending的Softirq。该函数首先检查CPU是否正在相应中断(in_irq),如果是则不作处理,直接返回。如果CPU没有相应中断,则将pending irq保存起来(loacal_softirq_save),随后调用__do_softirq来处理pending的IRQ。这一过程如下:
asmlinkage void do_softirq(void){ __u32 pending; unsigned long flags; if (in_interrupt()) return; local_irq_save(flags); pending = local_softirq_pending(); if (pending) __do_softirq(); local_irq_restore(flags);} |
9.3.8 Per-Architecture Processing of softirq
9.3.9 内核线程: ksoftirqd
内核在后台有名为ksoftirqd的线程,专门用于检查没有执行的softirqhandlers,并尽可能在CPU没有服务中断的时候去执行他们。每一个CPU都有这样的一个线程,其命名分别为:ksoftirqd_CPU0, ksoftirqd_CPU1等等。
函数ksoftirqd定义如下:
static int ksoftirqd(void * __bind_cpu){ set_current_state(TASK_INTERRUPTIBLE); while (!kthread_should_stop()) { preempt_disable(); if (!local_softirq_pending()) { preempt_enable_no_resched(); schedule(); preempt_disable(); } __set_current_state(TASK_RUNNING); while (local_softirq_pending()) { /* Preempt disable stops cpu going offline. If already offline, we'll be on wrong CPU: don't process */ if (cpu_is_offline((long)__bind_cpu)) goto wait_to_die; do_softirq(); preempt_enable_no_resched(); cond_resched(); preempt_disable(); rcu_sched_qs((long)__bind_cpu); } preempt_enable(); set_current_state(TASK_INTERRUPTIBLE); } __set_current_state(TASK_RUNNING); return 0;wait_to_die: preempt_enable(); /* Wait for kthread_stop */ set_current_state(TASK_INTERRUPTIBLE); while (!kthread_should_stop()) { schedule(); set_current_state(TASK_INTERRUPTIBLE); } __set_current_state(TASK_RUNNING); return 0;} |
进程的优先级从-20到+19,其中-20为最高,+19为最低,线程ksoftirqd的优先级被设置成了最低(-19),以避免频繁的NET_RX_SOFTIRQ会给CPU带来太大的负担。
该线程一旦启动,就会不停的去调用do_softirq来处理Pending的handler。
9.3.10 How the Networking Code Uses softirqs
9.4 softnet_data数据结构
对输入的数据来讲,每一个CPU都有自己的列队。该列队用softnet_data来表示,代码如下:
/* * Incoming packets are placed on per-cpu queues so that * no locking is needed. */struct softnet_data{ struct Qdisc *output_queue; struct sk_buff_head input_pkt_queue; struct list_head poll_list; struct sk_buff *completion_queue; struct napi_struct backlog;}; |
9.4.1 成员介绍
- output_queue
- completion_queue
已经完成接收工作的sk_buff,这些数据可以被释放。
- poll_list
等待处理的设备的双向链表。
9.4.2 softnet_data 的初始化
该数据结构的初始化在net_dev_init中,如下:
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;} | |