Linux 系统对中断处理的演进

Linux 系统对中断处理的演进

Linux 中断系统的变化并不大,比较重要的就是引入了 threaded irq: 使用内核线程来处理中断。

Linux 系统中有硬件中断,也有软件中断。对硬件中断的处理有 2 个原则:不能嵌套,越快越好。

Linux 对中断的扩展:硬中断、软件中断

Linux 系统把中断的意义扩展了,对于按键中断等硬件产生的中断,称之为 硬件中断(hard irq) 。每个硬件中断都有对应的处理函数,比如按键中断、网卡中断的处理函数肯定不一样。

为了方便理解,可以先认为对硬件中断的处理是用数组来实现的,数组里存放的是函数指针:
在这里插入图片描述
上面的图是简化之后的,在Linux 中这个数组复杂多了。

当发生A中断事,对应的 irq_function_A 函数被调用。硬件导致该函数被调用。

相对的,还可以人为地制造中断:软件中断 soft irq,如下图所示
在这里插入图片描述
同理,上图也是简化之后的。

随之的问题来了:

  1. 软件中断何时产生?
    由软件决定,对于 X 号软件中断,只需要把它的 flag 设置为1就表示发生了该中断。
  2. 软件中断何时处理?
    软件中断,并不需要太着急,有空的时候再处理即可。
    问题是什么时候有空?不能让它一直等吧?
    Linux 系统中,各种硬件中断频繁发生,至少定时器中断每10ms 发生一次;
    也可以取巧;
    处理完硬件中断后,再去处理软件中断中断?
  3. 有哪些软件中断?
    查内核源码 include/linux/interrupt.h
enum
{
	HI_SOFTIRQ=0,
	TIMER_SOFTIRQ,
	NET_TX_SOFTIRQ,
	NET_RX_SOFTIRQ,
	BLOCK_SOFTIRQ,
	IRQ_POLL_SOFTIRQ,
	TASKLET_SOFTIRQ,
	SCHED_SOFTIRQ,
	HRTIMER_SOFTIRQ, /* Unused, but kept as tools rely on the
			    numbering. Sigh! */
	RCU_SOFTIRQ,    /* Preferable RCU should always be the last softirq */
 
	NR_SOFTIRQS
};

怎么触发软件中断?最核心的函数是 raise_softirq ,简单地理解就是设置 softirq_veq[nr] 的标志位:

extern void raise_softirq(unsigned int nr);

怎么设置软件中断的处理函数

extern void open_softirq(int nr, void (*action)(struct softirq_action *));

中断处理原则1:不能嵌套

官方资料中也有提到 https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=e58aa3d2d0cc
中断处理函数需要调用C函数,这就需要用到栈。

  • 中断A正在处理的过程中,假设又发生了中断B,那么在栈里要保存A的现场,然后处理B
  • 在处理B的过程中又发生了中断C,那么栈里要保存B的现场,然后处理C。

如果中断嵌套突然暴发,那么栈将越来越大,栈终将耗尽。所以,为了防止这种情况发生,也是为了简单化中断的处理;

在 Linux 系统上中断无法嵌套:即当前中断 A 没处理完之前,不会响应另一个中断 B(即使它的优先级更高)。

中断处理原则2:越快越好

妈妈在家中照顾小孩时,门铃响起,她开门取快递:这就是中断的处理。她取个快递敢花上半天吗?不怕小孩出意外吗?

同理,在Linux 系统中,中断的处理也是越快越好。

在单芯片系统中,假如中断处理的很慢,那么应用程序在这段时间就无法执行,系统也会显得很迟钝。

SMP 系统中,假设中断处理很慢,那么正在处理这个中断的CPU上的其他线程也无法执行。

在中断的处理过程中,该CPU是不能进行进程调度的,所以中断的处理要越快越好,尽早让其他中断能被处理 — 进程调度靠定时器中断来实现。

其实在Linux 系统中使用中断是挺简单的,为某个中断 irq 注册中断处理函数 handler,可以使用 request_irq 函数:

static inline int __must_check
request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,
	    const char *name, void *dev)
{
	return request_threaded_irq(irq, handler, NULL, flags, name, dev);
}
 
 
extern int __must_check
request_threaded_irq(unsigned int irq, irq_handler_t handler,
		     irq_handler_t thread_fn,
		     unsigned long flags, const char *name, void *dev);

handler 函数中,代码尽可能高效

但是,处理某个中断要做的事情就是很多,没办法加快。比如对于按键中断,我们需要等待即使毫秒消除机械抖动。难道要在 handler 中等待吗?对于计算机来说,这可是一个段很长的时间。怎么办?

答案是:拆分

拆分为:上半部、下半部

当一个中断要耗费很多时间来处理,它的坏处是:在这段时间内,其他中断无法被处理。换句话说,在这段时间内,系统是关中断的。

如果某个中断就是要做那么多事,我们能不能把它拆分成两部分:紧急的、不紧急的?

在 handler 函数里只做紧急的事,然后就重新开中断,让系统得以正常运行;那些不紧急的事,以后再处理,处理时是开中断的。

在这里插入图片描述
中断下半部的实现有很多种方法,讲两种主要的:task(小任务)work queue(工作队列)

下半部要做的事情耗时不是太长:tasklet

假设我们把中断分为上半部、下半部。发生中断时,上半部下半部的代码何时、如何被调用?

当下半部比较耗时但是能忍受,并且它的处理比较简单时,可以用 tasklet 来处理下半部。 tasklet 是使用软件中断来实现

enum
{
	HI_SOFTIRQ=0,
	TIMER_SOFTIRQ,
	NET_TX_SOFTIRQ,
	NET_RX_SOFTIRQ,
	BLOCK_SOFTIRQ,
	IRQ_POLL_SOFTIRQ,
	TASKLET_SOFTIRQ,            // tasklet软件中断,用来处理中断下半部
	SCHED_SOFTIRQ,
	HRTIMER_SOFTIRQ, /* Unused, but kept as tools rely on the
			    numbering. Sigh! */
	RCU_SOFTIRQ,    /* Preferable RCU should always be the last softirq */
 
	NR_SOFTIRQS
};

使用流程图简化一下:
在这里插入图片描述
假设硬件中断A的上半部函数为 irq_top_half_A,下半部为 irq_bottom_half_A

使用情景化的分析,才能理解上述代码的;

  1. 硬件中断 A 处理过程中,没有其他中断发生:
    一开始, preempt_count = 0;
    上述流程图①~⑨依次执行,上半部、下半部的代码各执行一次。
  2. 硬件中断 A 处理过程中,又再次发生了中断 A:
    一开始,preempt_count = 0;
    执行到第6时,一开中断后,中断A又再次使得CPU 跳到中断向量表。

注意:这时 preempt_count 等于1,并且中断下半部的代码并未执行。CPU 又从①开始再次执行中断 A 的上半部代码:

在第①步 preempt_count 等于 2;
在第③步 preempt_count 等于 1;
在第④步发现 preempt_count 等于 1,所以直接结束当前第 2 次中断的处理;

注意:重点来了,第2次中断发生后,打断了第一次中断A的第⑦步处理。当第2次中断B处理完毕, CPU 会继续去执行第⑦步。

在第⑦步里,它会去执行中断 A 的下半部,也会去执行中断 B 的下半部。

所以,多个中断的下半部,是汇集在一起处理的。

总结:

  1. 中断的处理可以分为上半部,下半部
  2. 中断上半部,用来处理紧急的事,它是在关中断的状态下执行的
  3. 中断下半部,用来处理耗时的、不那么紧急的事,它是在开中断的状态下执行的
  4. 中断下半部执行时,有可能会被多次打断,有可能会再次发生同一个中断
  5. 中断上半部执行完后,触发中断下半部的处理
  6. 中断上半部、下半部的执行过程中,不能休眠:中断休眠的话,以后谁来调度进程啊?

工作队列

在中断下半部的执行过程中,虽然是开中断的,期间可以处理各类中断。但是毕竟整个中断的处理还没走完,这期间 APP 是无法执行的。

假设下半部要执行1、2分钟,在这1、2分钟里APP都是无法响应的。

这谁受得了?所以,如果中断要做的事情实在太耗时,那就不能用软件中断来做,而应该用内核线程来做:在中断上半部唤醒内核线程。内核线程和APP 都一样竞争执行, APP 有机会执行,系统不会卡顿。

这个内核线程是系统帮我们创建的,一般是 kworker 线程,内核中有很多这样的线程:
在这里插入图片描述
kworker 线程要去“工作队列” (work queue)上取出一个一个“工作”(work),来执行它里面的函数。

那我们怎么使用 work、 work queue 呢?

  1. 创建 work:
    得先写出一个函数,然后用这个函数填充一个 work 结构体。比如:
static DECLARE_WORK(aer_recover_work, aer_recover_work_func);
 
 
 
 
static void aer_recover_work_func(struct work_struct *work)
{
	struct aer_recover_entry entry;
	struct pci_dev *pdev;
 
	while (kfifo_get(&aer_recover_ring, &entry)) {
		pdev = pci_get_domain_bus_and_slot(entry.domain, entry.bus,
						   entry.devfn);
		if (!pdev) {
			pr_err("AER recover: Can not find pci_dev for %04x:%02x:%02x:%x\n",
			       entry.domain, entry.bus,
			       PCI_SLOT(entry.devfn), PCI_FUNC(entry.devfn));
			continue;
		}
		cper_print_aer(pdev, entry.severity, entry.regs);
		if (entry.severity == AER_NONFATAL)
			pcie_do_recovery(pdev, pci_channel_io_normal,
					 PCIE_PORT_SERVICE_AER);
		else if (entry.severity == AER_FATAL)
			pcie_do_recovery(pdev, pci_channel_io_frozen,
					 PCIE_PORT_SERVICE_AER);
		pci_dev_put(pdev);
	}
}
  1. 要执行这个函数时,把 work 提交给 work queue 就可以了

schedule_work(&aer_recover_work);

上述函数会把 work 提供给系统默认的 work queue: system_wq,它是一个队列。

  1. 谁来执行 work 中的函数?
    不用我们管, schedule_work 函数不仅仅是把 work 放入队列,还会把kworker 线程唤醒。此线程抢到时间运行时,它就会从队列中取出 work,执行里面的函数。

  2. 谁把 work 提交给 work queue?
    在中断场景中,可以在中断上半部调用 schedule_work 函数。

总结

  1. 很耗时的中断处理,应该放到线程里去
  2. 可以使用 work,work queue
  3. 在中断上半部调用 schedule_work 函数,触发 work 的处理
  4. 既然是在线程中运行,那对应的函数可以休眠。

新技术:threaded irq

使用线程来处理中断,并不是什么新鲜事。使用 work 就可以实现,但是需要定义 work、调用 schedule_work,好麻烦啊。

内核是为懒人服务的,再做出一个函数:

extern int __must_check
request_threaded_irq(unsigned int irq, irq_handler_t handler,
		     irq_handler_t thread_fn,
		     unsigned long flags, const char *name, void *dev);

在这里插入图片描述
你可以只提供 thread_fn,系统会为这个函数创建一个内核线程。发生中断时,内核线程就会执行这个函数。

以前用 work 来线程化地处理中断,一个 worker 线程只能由一个 CPU 执行,多个中断的 work 都由同一个 worker 线程来处理,在单 CPU 系统中也只能忍着了。但是在 SMP 系统中,明明有那么多 CPU 空着,你偏偏让多个中断挤在这个CPU 上?

新技术 threaded irq,为每一个中断都创建一个内核线程;多个中断的内核线程可以分配到多个 CPU 上执行,这提高了效率。

  • 9
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值