这篇文章是 《读薄「Linux 内核设计与实现」》系列文章 的第 IV 篇,本文主要讲了以下问题:中断和中断处理程序的概念与实现原理、Linux 中的下半部以及内核同步方法。
中断是一种特殊的电信号,由硬件发向处理器,处理器接收到中断时,会马上箱操作系统反映,由操作系统进行处理。中断随时可以产生,因此,内核随时可能因为新到来的中断而被打断。
不同的设备对应的中断不同,每个中断通过一个唯一的数字标识,这些中断值通常被称为中断请求(IRQ)线。
中断处理程序又成为中断处理例程(ISR),是内核在响应中断时执行的一个函数
一个中断处理程序对应一个中断,一个设备可能发出多种中断
对于外部设备,中断处理程序是设备驱动程序的一部分
在 Linux 中,中断处理程序和 C 函数区别不大,但有自己的规范,主要是运行时需要在中断上下文中
驱动程序可以通过 request_irq()
函数注册一个中断处理程序(linux/interrupt.h)
int request_irq(unsigned int irq,
irqhandler_t handler,
unsigined long falgs,
const char *name,
void *dev)
irq:
表示要分配的中断号
handler:
一个指针,指向处理这个中断的实际中断处理函数
typedef irqhandler_t(*irq_handler_t)(int, void*);
卸载驱动程序时,需要注销响应中断处理程序,并释放中断线。
void free_irq(unsigned int irq, void *dev);
如果指定的中断线不是共享的,那么该函数删除处理程序的同时将禁用这条中断线;中断线是共享的,则仅仅删除 dev 对应的处理程序,而这条中断线本身只有在删除了最后一个处理程序时才会被禁用
local_irq_disable();
local_irq_enable();
又想中断处理程序运行的快,又想中断处理程序完成的工作多,这两个目的显然有所抵触,所以把中断处理分为两个部分:
中断处理程序是上半部,接收到一个中断,它就立刻开始执行,但只做有严格时限的工作,例如一些只有在中断被禁止的状态下才能完成的工作
能够被允许稍后完成的工作会推迟2到下半部去,此后,在合适的时机,下半部会被开中断执行
Q1:为什么要分上半部和下半部?
Q2:上半部和下半部如何分开?
下半部的任务就是执行与终端处理密切相关但中断处理程序本身不执行的工作。我们期望中断处理程序将尽量多的工作放到下半部执行,以快速从中断返回。
此处的软中断和系统调用使用的 int 80H 不同,是操作系统支持的一种,在编译期间静态分配
linux/interrupt.h
中: struct softirq_action{
void (*action)(struct sfotirq_action*); //待执行的函数
void *data; //传递的参数
}
kernel/softirq.c
static struct softirq_action softirq_vec[NR_SOFTIRQS];
void softirq_handler(struct softirq_action*); //传递整个结构体
一个注册的软中断必须在被标记后才会执行下列地方,待处理的软中断会被检查和执行:
不管执行的时机,软中断都要在 do_softirq
中执行
分配索引: 通过 linux/interrupt.h
中的一个枚举类型中声明一个新的软中断
注册处理程序:在运行时通过调用 open_softirq()
注册软中断处理程序,有两个参数,软中断和处理函数
触发软中断: raise_softirq()
函数可以将一个软中断设置为挂起状态,让它在下次调用 do_softirq()
函数时投入运行
基于软中断的实现,但它的接口更简单,锁保护要求更低
struct tasklet_struct{
struct tasklet_struct *next; //链表
unsigned long state; //tasklet 状态
atomic_t count; //引用计数器
void (*funx)(unsigned long); //taklet 处理函数
unsigned long data; //给处理器函数传递的参数
}
被触发的软中断存放在2个数据结构: tasklet_vec
, task_hi_vec
,这两个数据结构都是由 task_struct
构成的链表,由 tasklet_schedule()
和 task_hi_schedule()
进行调度,调度步骤如下:
(1)检查 tasklet 状态,如果为 TASK_STATE_SCHED
则返回
(2)调用 _tasklet_schedule()
(3)保存中断状态,禁止本地中断
(4)把需要调度的 tasklet 加到 tasklet_vec
或 tasklet_hi_vec
链表头
(5)唤起 TASKLET_SOFTIRQ
或 HI_SOFTIRQ
软中断,下一次调用 do_softirq()
时会执行该 tasklet
(6)恢复中断
tasklet_action()
或 task_hi_action()
[ tasklet 处理的核心 ]: TASKLET_STATE_RUN
判断这个 tasklet 是否在其他处理器上运行,如果是,跳到笑一个 tasklet TASKLET_STATE_RUN
TASKLET_STATE_RUN
DECLARE_TASKLET(name,func,data);
DECLARE_TASKLET_DIASBLED(name,func,data);
tasklet_init(t, takslet_handler, dev);
void tasklet_handler(unsigned long data)
注意:不能再 tasklet 中使用信号量或者其他阻塞式函数
tasklet_schedule(&my_tasklet);
tasklet_enable(&my_tasklet);
tasklet_disable(&my_tasklet);
ksoftirqd 是内核线程,每个处理器都有一个,用于在空闲处理器处理软中断
for(;;){
if(!softirq_pending(cpu))
schedule();
set_current_state(TASK_RUNNING);
while(softirq_pending(cpu)){
do_softirq();
if(need_resched())
schedule();
}
set_current_sdtate(TASK_INTERRUPTIBLE);
}
只要有待处理的软中断,该线程就会处理
工作队列机制将下半部功能交给内核县城去执行,有线程上下文,可以睡眠
schedule()
,休眠 TASK_RUNNING
run_workqueue()
执行被推后的工作 该函数循环遍历链表上每个待处理的工作:
静态:
DECLARE_WORK(name, void(*func)(void*), void *data);
动态:
INIT_WORK(struct work_struct *work, void(*func)(void*), void *data);
void work_handler(void *data)
schedule_work(&work);
schedule_delayed_work(&work, delay);
void flush_scheduled_work(void);
机制 | 上下文 | 顺序执行保障 |
---|---|---|
软中断 | 中断 | 没有 |
tasklet | 中断 | 同类型不能同时执行 |
工作队列 | 进程 | 没有(和进程上下文一样被调度) |
Q:我们要选择哪种机制?
如果有休眠的要求,选择工作队列;否则,最好使用 tasklet;要是必须专注性能的提高,选择软中断
如果进程上下文和一个下半部共享数据,在访问这些数据之前,你需要禁止下半部的处理并得到锁的使用权
如果中断上下文和一个下半部共享数据,在访问数据之前,需要禁止中断并得到锁的使用权
临界区就是访问和操作共享资源的代码段,必须保证原子地执行才能保证安全
保证在临界区执行的县城只有一个
中断
软中断和 tasklet
内核抢占
睡眠及用户空间的同步
对称多处理
要有一个或多个执行线程和一个或多个资源
每一个线程都在等待其中一个资源
所有的资源都被占用
所有县城都在互相等待,但他们永远不会释放已经占有的资源
atomic_dec_and_test(atmoic_t, *v)
set_bit(0, &word)
自旋锁只能被一个可执行进程持有
若争用一个被占用的锁则进程忙等(旋转)
自旋锁不能长期被占用,否则效率低
本文的版权归作者罗远航 所有,采用 Attribution-NonCommercial 3.0 License 。任何人可以进行转载、分享,但不可在未经允许的情况下用于商业用途;转载请注明出处。感谢配合!