Linux内核网络收包角度——浅入中断(1)

语言: CN / TW / HK

以下文章来源于技术简说   ,作者董旭

文章主要参考:

[1]公众号 人人都是极客http://mp.weixin.qq.com/s/e0Zj8MFMF921O_UAAj2phw

[2] 知乎:http://zhuanlan.zhihu.com/p/157741043?utm_source=wechat_session&utm_medium=social&utm_oi=932961182772514816&utm_campaign=shareopn

一、先抛开网络收包引起的一系列相关知识,看ARM下中断相关的概念

几个概念:

外设

中断源,当设备需要请求某种服务时,会发起一个硬件中断信号,将信号传递到中断控制器输入引脚上。 

中断控制器:

中断控制器负责收集所有中断源发起的中断,转换成CPU可识别的vector,相当于一个代理,如ARM公司提供的GIC中断控制器,有四个版本 GIC v1~v4。 外部设备产生的中断事件不会直接通过NTR总线进入CPU,而是先发给中断控制器,中断控制器再转发给CPU。中断控制器可以:管理、控制可屏蔽中断、对可屏蔽中断进行优先权判定。假设没有中断控制器,CPU为每一个外设都准备一个引脚,完全是不现实的,一方面引脚不够用,还会增大CPU体积,最重要的是需要维护一个中断等待队列,优先级的判断由CPU来做,极大地降低了CPU的效率,这也是中断控制器存在的意义所在。

异常向量表:

异常指CPU的某些异常状态或者一些系统事件(可能来自外部,也可能来自内部),这些状态或者事件可以导致cpu执行一些预先设定的动作, CPU每执行完一条指令都会检查有无异常发生。 ARM中异常向量如下表所示:

__vectors_start是异常向量的基地址,如下所示:

 .section .vectors, "ax", %progbits
.L__vectors_start:
W(b) vector_rst
W(b) vector_und
W(ldr) pc, .L__vectors_start + 0x1000
W(b) vector_pabt
W(b) vector_dabt
W(b) vector_addrexcptn
W(b) vector_irq
W(b) vector_fi

HW interrupt ID:

对于中断控制器而言,收集了多个外设的中断设备的请求,并向上传递,因此中断控制器需要对外设中断进行编码,中断控制器用HW interrupt ID来标识外设的中断。但是在 中断控制器级联的情况下,仅仅用HW interrupt ID已经不能唯一标识一个外设中断 ,如下所示:

此时还需要知道该HW interrupt ID所属的中断控制器,因此 引入了irq domain的概念,一个中断控制器就是一个irq domain 。索引每个中断控制器对应自己的中断号,硬件中断号在不同控制器上是可以重复编码的。

IRQ number:

CPU需要为每一个外设中断编号,我们称之IRQ Number。这个IRQ number是一个虚拟的interrupt ID,和硬件无关,仅仅是被CPU用来标识一个外设中断。(中断控制器级联情况下,硬件中断号无法做到ID的唯一性)

关于IRQ_Number与HWIRQ,可通过cat /proc/interrupt查看:

中断上半部、下半部:

设备的 中断会打断内核中进程的正常调度和运行 ,在中断到来时,要完成的工作往往比较复杂,可能要大量的耗时处理,为了 在中断执行事件尽可能短和中断处理需完成大量工作之间找一个平衡点 ,Linux将中断处理程序分解为两个半部:top half 、bottom half,也就是中断上半部和中断下半部,上半部往往完成尽可能少的比较紧急的工作,它往往只是简单地读取寄存器中的中断状态并清除中断标志后就进行"登记中断"的工作,后续工作交给下半部完成。

软中断:

中断的下半部实现方式有3种,分别是软中断、tasklet、工作队列,软中断是下半部的实现方式之一,就是在硬件中断(也叫中断顶半部分)执行完毕后,通过wakeup_softirqd()的方式唤醒一个softirq队列,然后中断程序返回,softirq队列也在适当的时候开始执行。

中断描述符表:

在Linux Kernel中,对于每一个外设的IRQ都有统一的描述方式,在Linux Kernel中用struct irq_desc来表示,称为中断描述符,保存了中断相关的信息,如IRQ_Number、硬件中断号、 中断服务例程 等:

struct irq_desc {
......
struct irq_data irq_data; // 保存软件中断号、irq domain等信息
......

//该中断的通用处理函数
irq_flow_handler_t handle_irq;
......
struct irqaction *action; /* IRQ action list */
......

#ifdef CONFIG_SPARSE_IRQ
struct rcu_head rcu;
struct kobject kobj;
#endif
int parent_irq;
struct module *owner;
const char *name;
} ____cacheline_internodealigned_in_smp;

而中断描述符表有两种组织方式,第一种是数组的方式:

struct irq_desc irq_desc[NR_IRQS] __cacheline_aligned_in_smp = {
[0 ... NR_IRQS-1] = {
.handle_irq = handle_bad_irq,
.depth = 1,
.lock = __RAW_SPIN_LOCK_UNLOCKED(irq_desc->lock),
}
}

NR_IRQS决定了该硬件平台的IRQ最大数目,但用数组的方式,一旦系统定义了很大的NR_IRQS,实际如果用了其中了少部分,那会造成很大的内存浪费。第二种是radix tree的方式,使用HW interruput作为索引,每一个中断描述符动态分配。在内核种查看CONFIG_SPACE_IRQ选项是否开启,若开启说明采用的是radix tree的方式:

中断服务例程:

上面的中断描述表中也介绍到,中断描述表struct irq_desc中存放着中断相关信息,一个中断所需要的资源都集中在这个结构体中描述,其中最重要的之一就是中断服务例程,struct irq_desc中断描述符的struct irq_action组成一个链表,每个irq_action都有回调函数:handler执行中断处理,如下图所示,组织成链表是因为一个中断线上可能会挂载好几个设备,这几个设备会共用这个中断,具体是哪个设备需要判断。

注册中断:

从上面的中断服务例程中可以到,中断的相应处理是通过中断描述符为桥梁进行调用一系列中断处理函数的,Linux内核API:

//irq是中断编号、myhandler是中断处理函数、flages是中断类型,name是设备名称、dev指设备
request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *name, void *dev

该函数实现动态地申请注册一个中断,根据传入的irq号获得irq_desc文件描述符,然后动态地创建一个irq_action,根据传入的handler回调函数初始化irq_action,最后把irq_action链入链表,完成中断的动态申请及注册。

基础案例如下:

# include <linux/kernel.h>
# include <linux/init.h>
# include <linux/module.h>
# include <linux/interrupt.h>

static int irq; //irq号
static char * devname; //设备名称

//这两个是用来让我们在命令行传入参数
module_param(irq,int,0644);
module_param(devname,charp,0644); //这里charp相当于char*,是字符指针

struct myirq
{

int devid; //这个主要用在共享irq中
};

struct myirq mydev={1119};

//中断处理函数
static irqreturn_t myirq_handler(int irq,void * dev)
{
struct myirq mydev;
static int count=1;
mydev = *(struct myirq*)dev;
printk("key: %d..\n",count);
printk("devid:%d ISR is working..\n",mydev.devid);
printk("ISR is leaving......\n");
count++;
return IRQ_HANDLED;
}


//内核模块初始化函数
static int __init myirq_init(void) //最重要的工作是注册中断线,并将自己写的中断服务例程注册进去,用request_irq完成
{
printk("Module is working...\n");
if(request_irq(irq,myirq_handler,IRQF_SHARED,devname,&mydev)!=0)
{
printk("%s request IRQ:%d failed..\n",devname,irq);
return -1;
}
printk("%s request IRQ:%d success...\n",devname,irq);
return 0;
}

//内核模块退出函数
static void __exit myirq_exit(void)
{
printk("Module is leaving...\n");
free_irq(irq,&mydev); //注销函数
printk("Free the irq:%d..\n",irq);
}

MODULE_LICENSE("GPL");
module_init(myirq_init);
module_exit(myirq_exit)

测试:

tips: 测试后尽快卸载掉内核模块,否则日志累加很快占用大量磁盘空间

二、在介绍Linux内核网卡收包时的中断流程前,再把网卡注册硬中断的过程分析一下

1、通过ethtool工具查看当前网卡的网卡驱动信息:

2、分析e1000网卡驱动注册中断过程:

其模块初始化函数:

static int __init e1000_init_module(void)
{
int ret;
pr_info("%s - version %s\n", e1000_driver_string, e1000_driver_version);

pr_info("%s\n", e1000_copyright);

ret = pci_register_driver(&e1000_driver);
...
...
return ret;
}

e1000_driver这个结构体是一个关键,它的赋值如下:

static struct pci_driver e1000_driver = {
.name = e1000_driver_name,
.id_table = e1000_pci_tbl,
.probe = e1000_probe,
.remove = e1000_remove,
.driver = {
.pm = &e1000_pm_ops,
},
.shutdown = e1000_shutdown,
.err_handler = &e1000_err_handler
}

其中很主要的一个方法就是.probe方法,也就是e1000_probe():

static int e1000_probe(struct pci_dev *pdev, const struct pci_device_id *ent)
{
...
...
netdev->netdev_ops = &e1000_netdev_ops;
e1000_set_ethtool_ops(netdev);
...
...
}

这是e1000主要的初始化函数, 其注册了netdev的netdev_ops,用的是e1000_netdev_ops这个结构体:

static const struct net_device_ops e1000_netdev_ops = {
.ndo_open = e1000_open,
.ndo_stop = e1000_close,
.ndo_start_xmit = e1000_xmit_frame,
.ndo_set_rx_mode = e1000_set_rx_mode,
.ndo_set_mac_address = e1000_set_mac,
.ndo_tx_timeout = e1000_tx_timeout,
...
...
}

e1000_open函数就是中断注册开始的地 方!  网卡设备的启动与关闭网卡设备启动时首先调用函数e1000_open()

int e1000_open(struct net_device *netdev)
{
struct e1000_adapter *adapter = netdev_priv(netdev);
struct e1000_hw *hw = &adapter->hw;
...
...
err = e1000_request_irq(adapter);
...
}

e1000在这里注册了中断,中断处理函数handler是e1000_intr()

static int e1000_request_irq(struct e1000_adapter *adapter)
{
struct net_device *netdev = adapter->netdev;
irq_handler_t handler = e1000_intr;
int irq_flags = IRQF_SHARED;
int err;

err = request_irq(adapter->pdev->irq, handler, irq_flags, netdev->name,
...
...
}

至此一个网卡的重点处理函数注册完毕。

三、网卡收包时硬中断处理过程

1、 数据包达到网卡,存入网卡硬件的缓冲区,然后将数据包DMA到内存中的缓冲区,这个过程不需要CPU参与,只需要DMA这个硬件设备,配合网卡这个设备即可,这个过程的前提是网卡驱动需要在内存中申请一个struct sk_buffer的缓冲区,然后把这个sk_buffer的地址告知网卡,这样DMA时才能把数据拷贝到对应的位置上。

2、 在1、中整个过程由硬件完成,完成后需要网卡来通知内核,让内核处理数据包,网卡向 CPU 发起中断信号,CPU 打断当前的程序:

一、 中介绍了异常向量表, 异常向量表 vectors 中设置了各种异常的入口,网络收包行为引起的是异步中断,CPU跳转到异步中断异常向量处,执行对应的异常处理执行:

 .section .vectors, "ax", %progbits
.L__vectors_start:
W(b) vector_rst
W(b) vector_und
W(ldr) pc, .L__vectors_start + 0x1000
W(b) vector_pabt
W(b) vector_dabt
W(b) vector_addrexcptn
W(b) vector_irq ----->发生中断时执行指令
W(b) vector_fi

使用宏vector_stub表示这个vector_irq:

 vector_stub irq, IRQ_MODE, 4

.long __irq_usr @ 0 (USR_26 / USR_32)
.long __irq_invalid @ 1 (FIQ_26 / FIQ_32)
.long __irq_invalid @ 2 (IRQ_26 / IRQ_32)
.long __irq_svc @ 3 (SVC_26 / SVC_32)
.long __irq_invalid @ 4
.long __irq_invalid @ 5
.long __irq_invalid @ 6
.long __irq_invalid @ 7
.long __irq_invalid @ 8
.long __irq_invalid @ 9
.long __irq_invalid @ a
.long __irq_invalid @ b
.long __irq_invalid @ c
.long __irq_invalid @ d
.long __irq_invalid @ e
.long __irq_invalid @ f

3、 上面2、中提到的 irq_user与irq_svc具体实现大致相同,主要是根据被中断时CPU所处的状态进行选择,以irq_user为例:

__irq_usr:
usr_entry
kuser_cmpxchg_check
irq_handler
get_thread_info tsk
mov why, #0
b ret_to_user_from_irq
UNWIND(.fnend )
ENDPROC(__irq_usr)

函数主要执行:保存现场、调用irq_handler、恢复现场,核心是保存现场后,跳入中断处理irq_handler,在ARM 32上irq_handler如下:

 .macro irq_handler
#ifdef CONFIG_MULTI_IRQ_HANDLER
ldr r1, =handle_arch_irq
mov r0, sp
badr lr, 9997f
ldr pc, [r1]
#else
arch_irq_handler_default
#endif
9997:
.endm

CONFIG_MULTI_IRQ_HANDLER宏表示"允许每台机器在运行时指定它自己的IRQ处理程序",当前Linux内核默认是不开启的.所以走else的arch_irq_handler_default逻辑arch_irq_handler_default 宏 调用了asm_do_IRQ.

在ARM 64下:handle_arch_irq如下,

 .macro irq_handler
ldr_l x1, handle_arch_irq
mov x0, sp
irq_stack_entry
blr x1
irq_stack_exit
.end

采用ARM 64进行分析,irq_handler将调用gic_handle_irq:

static asmlinkage void __exception_irq_entry gic_handle_irq(struct pt_regs *regs)
{
u32 irqnr;

do {
irqnr = gic_read_iar();

if (likely(irqnr > 15 && irqnr < 1020) || irqnr >= 8192) {
int err;

if (static_key_true(&supports_deactivate))
gic_write_eoir(irqnr);
else
isb();

err = handle_domain_irq(gic_data.domain, irqnr, regs);
if (err) {
WARN_ONCE(true, "Unexpected interrupt received!\n");
if (static_key_true(&supports_deactivate)) {
if (irqnr < 8192)
gic_write_dir(irqnr);
} else {
gic_write_eoir(irqnr);
}
}
continue;
}
if (irqnr < 16) {
gic_write_eoir(irqnr);
if (static_key_true(&supports_deactivate))
gic_write_dir(irqnr);
#ifdef CONFIG_SMP
/*
* Unlike GICv2, we don't need an smp_rmb() here.
* The control dependency from gic_read_iar to
* the ISB in gic_write_eoir is enough to ensure
* that any shared data read by handle_IPI will
* be read after the ACK.
*/

handle_IPI(irqnr, regs);
#else
WARN_ONCE(true, "Unexpected SGI received!\n");
#endif
continue;
}
} while (irqnr != ICC_IAR1_EL1_SPURIOUS);

该函数首先读取处理器接口中的中断确认寄存器 得到硬件中断号 ,当硬件中断号大于15且小于1020或者硬件中断号大于或等于8192时,中断来自外设, 调用函数handle_domain_irq():

int __handle_domain_irq(struct irq_domain *domain, unsigned int hwirq,
bool lookup, struct pt_regs *regs)
{
struct pt_regs *old_regs = set_irq_regs(regs);
unsigned int irq = hwirq;
int ret = 0;

irq_enter();//进入中断上下文

#ifdef CONFIG_IRQ_DOMAIN
if (lookup)
irq = irq_find_mapping(domain, hwirq);//根据硬件中断号查找Linux中断号
#endif

/*
* Some hardware gives randomly wrong interrupts. Rather
* than crashing, do something sensible.
*/

if (unlikely(!irq || irq >= nr_irqs)) {
ack_bad_irq(irq);
ret = -EINVAL;
} else {
generic_handle_irq(irq);
}

irq_exit();//退出中断上下文
set_irq_regs(old_regs);
return ret;
}

该函数:

  1. 调用irq_enter()进入中断上下文

  2. 调用函数irq_find_mapping根据硬件中断号查找Linux中断号(IRQ_number)

  3. 调用generic_handle_irq()函数处理中断,该函数是底层架构无关层的入口,也就是中断通用层,在该层 通过Linux中断号找到了对应的中断描述符,并通过generic_handle_irq_desc()函数调用中断描述符对应的handle_irq()函数

  4. irq_exit()函数退出中断上下文

4、 详细看generic_handle_irq:

int generic_handle_irq(unsigned int irq)
{
struct irq_desc *desc = irq_to_desc(irq);//根据IRQ寻找到对应的中断描述符

if (!desc)
return -EINVAL;
generic_handle_irq_desc(desc);//处理中断
return 0;
}

generic_handle_irq_desc():

static inline void generic_handle_irq_desc(struct irq_desc *desc)
{
desc->handle_irq(desc);
}

关于desc->handle_irq来历,由gic_irq_domain_map根据hwirq觉得,硬件中断号小于32的指向handle_percpu_devid_irq,其他情况指向handle_fasteoi_irq,网卡的硬件中断号是大于32的,所以指向 handle_fasteoi_irq函数:

handle_fasteoi_irq->handle_irq_event->handle_irq_event_percpu:

irqreturn_t __handle_irq_event_percpu(struct irq_desc *desc, unsigned int *flags)
{
irqreturn_t retval = IRQ_NONE;
unsigned int irq = desc->irq_data.irq;
struct irqaction *action;

record_irq_time(desc);

for_each_action_of_desc(desc, action) {
irqreturn_t res;

trace_irq_handler_entry(irq, action);
res = action->handler(irq, action->dev_id);
trace_irq_handler_exit(irq, action, res);
......
}

return retval;
}

在for_each_action_of_desc中:遍历中断描述符中的action链表,依次执行每个action元素中的primary handler回调函数action->handler。

二、 中分析了网卡e1000网卡驱动注册的中断处理函数是:e1000_intr

5、 在e1000_intr中看一下关键的部分:

static irqreturn_t e1000_intr(int __always_unused irq, void *data)
{
......
if (napi_schedule_prep(&adapter->napi)) {
adapter->total_tx_bytes = 0;
adapter->total_tx_packets = 0;
adapter->total_rx_bytes = 0;
adapter->total_rx_packets = 0;
__napi_schedule(&adapter->napi);
}

return IRQ_HANDLED;
}

__napi_schedule(&adapter->napi)函数激活NAPI,关于NAPI在以后的文字中进行总结,这里先大概了解:【 随着网络带宽的发展,网速越来越快,之前的中断收包模式已经无法适应目前千兆,万兆的带宽,每次收包都发生硬中断通知CPU, CPU一直陷入硬中断而没有时间来处理别的事情了。为了解决这个问题,内核在2.6中引入了NAPI机制。 NAPI就是混合中断和轮询的方式来收包,当有中断来了,驱动关闭中断,通知内核收包,内核软中断轮询当前网卡,在规定时间尽可能多的收包。时间用尽或者没有数据可收,内核再次开启中断,准备下一次收包。

void __napi_schedule(struct napi_struct *n)
{
unsigned long flags;

local_irq_save(flags);//禁用中断
____napi_schedule(this_cpu_ptr(&softnet_data), n);
local_irq_restore(flags);//恢复中断
}

具体处理过程调用 ____napi_schedule(this_cpu_ptr(&softnet_data), n);

static inline void ____napi_schedule(struct softnet_data *sd,
struct napi_struct *napi)
{
list_add_tail(&napi->poll_list, &sd->poll_list);
__raise_softirq_irqoff(NET_RX_SOFTIRQ);
}

NAPI的激活如上函数:

1、内核网络系统在初始化时每个CPU都会有一个结构体,它会把队列对应的信息插入到结构体的链表里,换句话说,每个网卡队列在接收数据的时候,需要把自己的队列信息告诉对应的CPU,将这两个信息绑定起来,保证某个CPU处理某个队列。

2、要与触发硬中断一样,需要触发软中断,其实就是修改 pending 的某个标志位,然后内核中有一个线程不断轮询这组标志位,看哪个是 1 了,就去软中断向量表里,寻找这个标志位对应的处理程序,然后执行它。

可以看到上面网卡硬中断的处理函数做的事情非常简单:将网卡设备 dev 放入 poll_lis轮询列表 里,然后立刻发起了一次软中断

6、 接下来就是执行irq_exit函数,这里是软中断和硬中断衔接的一个地方:

void irq_exit(void)
{
#ifndef __ARCH_IRQ_EXIT_IRQS_DISABLED
local_irq_disable();
#else
lockdep_assert_irqs_disabled();
#endif
account_irq_exit_time(current);
preempt_count_sub(HARDIRQ_OFFSET);
if (!in_interrupt() && local_softirq_pending())
invoke_softirq();

tick_irq_exit();
rcu_irq_exit();
trace_hardirq_exit(); /* must be last! */
}

在irq_exit()的第一步就是一个local_irq_disable(),也就是说禁止了中断,不再响应中断。因为下面要处理所有标记为要处理的软中断,关中断是因为后面要清除这些软中断,将CPU软中断的位图中置位的位清零,这需要关中断,防止其它进程对位图的修改造成干扰。

然后preempt_count_sub(HARDIRQ_OFFSET),硬中断的计数减1,表示当前的硬中断到这里就结束了。

假设当前中断结束后没有其它中断了,也就是不在中断上下文了,且当前CPU有等待处理的软中断,即local_softirq_pending()也为真。那么执行invoke_softirq() ,逻辑就是:首先如果ksoftirqd正在被执行,那么不处理被pending的软中断,交给ksoftirqd线程来处理,这里直接退出。如果ksoftirqd没有正在运行,那么判断force_irqthreads,也就是判断是否配置了CONFIG_IRQ_FORCED_THREADING,是否要求强制将软中断处理都交给ksoftirqd线程。因为这里明显要在中断处理退出的最后阶段处理软中断,但是也可以让ksoftirqd来后续处理。如果设置了force_irqthreads,则不再执行__do_softirq(),转而执行wakeup_softirqd()来唤醒ksoftirqd线程,将其加入可运行队列,然后退出。如果没有设置force_irqthreads,那么就执行__do_softirq()


static inline void invoke_softirq(void)
{
if (ksoftirqd_running())
return;

if (!force_irqthreads) {
#ifdef CONFIG_HAVE_IRQ_EXIT_ON_IRQ_STACK
/*
* We can safely execute softirq on the current stack if
* it is the irq stack, because it should be near empty
* at this stage.
*/

__do_softirq();
#else
/*
* Otherwise, irq_exit() is called on the task stack that can
* be potentially deep already. So call softirq in its own stack
* to prevent from any overrun.
*/

do_softirq_own_stack();
#endif
} else {
wakeup_softirqd();
}
}

这里就完成网卡硬中断与软中断的衔接, 交给ksoftirqd线程或其他进行软中断处理 ,关于软中断以及NAPI相关的知识,在下几次文章中分析。