小结:
下半部 | 注册时间 | 上下文 | 顺序执行保障 | 场景 | 是否响应中断 |
---|---|---|---|---|---|
软中断 | 编译时静态注册 | 中断 | 没有,即使相同类型也可以同时在其他处理器上执行,锁保护或单处理器 | 给系统中对时间要求严格以及最重要的下半部使用 | 允许响应中断,但自己不能休眠 |
tasklet | 动态注册,通过软中断实现 | 中断 | 相同类型的不能同时执行,锁保护要求低 | 不需休眠的场景 | 不能睡眠,调度时会禁止本地中断 |
工作队列 | 可动态注册,通过内核线程实现 | 进程 | 没有(和进程上下文一样被调度) | 需要睡眠的场景 | 允许重新调度甚至是睡眠,不能访问用户空间,因为没有相关内存映射 |
软中断:do_softirq(),内核定时器和tasklet都是建立在软中断之上
tasklet:tasklet调度时会禁止本地中断
workqueue_struct:每个CPU一个,系统有默认的event/n
ksoftirq:一组辅助处理软中断(和tasklet)的内核线程,ksoftirqd/n
选择标准:需要睡眠,选工作队列;不需要睡眠,选软中断或tasklet。
void local_bh_disable()
禁止下半部中断使得硬件得以发出通知给处理器,处理器收到中断后,会马上向操作系统反馈此信号的到来,然后由操作系统处理。硬件设备生成中断的时候并不考虑与处理器的时钟同步(中断随时可以产生)。
特定的中断总是与特定的设备相关联。每个IRQ线都会被关联一个数值量。
异常:异常与中断不同,产生时必须考虑处理器时钟同步。异常也称为同步中断。
内核执行中断处理函数来相应中断,一个设备的中断处理程序是设备驱动程序的一部分。
中断处理程序与内核函数的区别:中断处理程序是被内核调用来响应中断的,他们运行在中断上下文(不可阻塞)
中断处理切为2个部分:
中断处理程序是上半部:接到中断立即执行,只做严格实现的工作,此时中断被禁止。
下半部:被允许稍后完成的工作,此时为开中断。
设备如果使用中断,则需要注册一个中断处理程序。
驱动程序通过request_irq()
注册一个中断处理程序,并激活给定的中断线:
int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,
const char* name, void *dev)
irq:要分配的中断号(预定或动态分配)
handler:指向实际中断处理程序
typedef irqreturn_t (*irq_handler_t)(int, void*);这是handler的原型,接受两个参数
中断处理程序标志
第三个参数flags是标识:
第四个参数name,为中断相关的设备的ASCII文本表示。
第五个参数dev用于共享中断线,dev提供唯一的标志信息,以便从共享中断线的诸多中断处理程序中删除指定的哪一个。实践中往往传递驱动程序的设备结构
注意:request_irq()函数可能睡眠, 原因是由于request_irq()或需要在/proc/irq文件中创建一个与中断对应的项,会调用proc_mkdir() ⇒ proc_create() ⇒ kmalloc(),而kmalloc是睡眠的。
3. 释放中断处理程序
卸载驱动时,要注销相应中断处理程序,释放中断线,调用void free_irq(unsigned int irq, void *dev)
中断上下文具有严格的时间限制,应迅速、简洁,因为中断处理程序打断了其他代码(有可能是另一个中断处理程序)。
中断处理程序栈:没有自己的栈,它共享所中断进程的内核栈。内核栈大小是2页。
设备产生中断 ⇒ 总线把电信号发给中断控制器 ⇒ 若为激活的,中断控制器将中断发往处理器 ⇒ 处理器关闭中断 ⇒ 跳到中断处理程序入口 ⇒ 返回内核运行中断的代码。
中断开始于预定义入口,类似系统调用,对于每条中断线,处理器会跳到对应的一个唯一的位置。
procfs是一个虚拟文件系统,只存在于内核内存,在procfs中读写文件都需要调用内核函数,模拟从真实文件中读或写。/proc/interrupts文件,存放系统与中断相关的统计信息。
linux内核提供一组接口用于操作机器上的中断状态,这些接口为我们提供了能够禁止当前处理器的中断系统。
一般来说控制中断系统的原因最终还是提供同步,以确保某个中断处理程序不会抢占当前代码,还可以禁止内核抢占。但无法防止共享数据的并发访问。
总之,锁,用于防止来自其他处理器的并发访问;禁止中断,防止来自其他中断处理程序的并发访问。
以前的代码:使用全局cli():禁止系统中所有处理器上的中断
现在的代码:结合使用本地中断控制和自旋锁。
可以从中断或进程上下文中调用,且不会睡眠。
PCI设备必须支持中断线共享,因此,它们根本不应该使用这些接口。
irqs_diable()
用于判断本地处理器上中断是否被禁止。in_interrupt()
:判断内核是否在中断处理程序或下半部处理程序中in_irq()
:判断是否在中断处理程序通常情况,要检查是否在进程上下文,因为睡眠只能在进程上下文。
为了减少中断处理程序所需完成的工作量,因为它运行时,当前中断线在所有处理器上都会被屏蔽。
下半部关键在于,他们运行时允许响应所有的中断。
softirq_action
结构表示struct softirq_action{
void (*action)(struct softirq_action *);
}
static struct softirq_action softirq_vec[NR_SOFTIRQS];要么
//每个被注册的软中断占据数组一项,因此最多32个软中断
(1). 软中断处理程序
实现上面的action函数
(2). 执行软中断
通常,中断处理程序会在返回前标记它的软中断,以触发软中断。
以下会检查和执行软中断:
do_softirq() 执行软中断,会遍历每一个,并调用其处理程序。
2. 使用软中断
软中断保留给系统中对时间要求最严格以及最重要的下半部使用,目前只有网络和SCSI直接使用软中断。内核定时器和tasklet都是建立在软中断之上。
(1). 分配索引
通过在
(2). 注册处理程序
运行时通过调用open_softirq()注册软中断处理程序
open_softirq(NET_TX_SOFTIRQ, net_tx_action);
//参数分别为软中断处理号和处理函数
(3). 触发你的软中断
raise_softirq()
将软中断设置为挂起,让它下次调用do_softirq()函数时投入运行。且该函数在触发一个软中断之前要先禁止中断,触发后再恢复。如果已经禁止,则使用raise_softirq_irqoff().
//中断已经被禁止
raise_softirq_irqoff(NET_TX_SOFTIRQ);
在中断处理函数中触发软中断是最常见的形式。内核执行完中断处理函数后,马上执行do_softirq(),于是软中断开始执行剩余工作。
struct tasklet_struct
{
struct tasklet_struct *next;
unsigned long state; //tasklet状态,仅能0、TASKLET_STATE_SCHED、TASKLET_STATE_RUN
atomic_t count; //引用计数,不为0,则tasklet被禁止;为0才可执行
void (*func)(unsigned long); // tasklet处理函数
unsigned long data; //给tasklet处理函数的参数
};
(2). 调度tasklet
已调度的tasklet(即被触发的)存放在两个每处理器链表:tasklet_vec(普通tasklet)和tasklet_hi_vec(高优先级的tasklet)中。
tasklet由tasklet_schedule()和tasklet_hi_schedule()函数进行调度。tasklet调度时会禁止本地中断。
其处理程序tasklet_action()和tasklet_hi_action()为tasklet处理的核心。
总之,所有tasklet都通过重复运用HI_SOFTIRQ和TASKLET_SOFTIRQ这两个软中断实现。但tasklet保证同一时间里只有一个给定类型的tasklet会被执行(其他不同类型也可以同时执行)。
使用tasklet
(1). 声明你自己的tasklet
静态创建:
DECLARE_TASKLET(name, func, data) // func 函数,data 其参数
DECLARE_TASKLET_DISABLED(name, func, data)
当该tasklet被调度以后,给定func函数会被执行。disable会将tasklet引用计数设为1,使处于禁止状态。
动态创建:
tasklet_init(t, tasklet_handler, dev); // 通过将一个间接引用赋给动态创建的tasklet_struct
(2). 编写你自己的tasklet处理程序
void tasklet_handler(unsigned long data)
由于tasklet运行时允许响应中断,因此也必须做好预防工作。适当加锁:
(3). 调度你的tasklet
tasklet_schedule(&my_tasklet);
调度my_tasklet
一般一个tasklet总在调度它的处理器上执行,以更好利用处理器高速缓存。
tasklet_disable()
:禁止某个tasklet
tasklet_enable()
:激活某个tasklet
tasklet_kill()
:从队列中去掉一个tasklet
(4). ksoftirq
一组辅助处理软中断(和tasklet)的内核线程,当内核中出现大量软中断时,会辅助。
问题:当软中断触发频率很高,尤其是有自行重复触发时
内核解决:不会立即处理重新触发的软中断。且当大量软中断出现时,唤醒一组内核线程处理,这些线程运行在最低优先级(nice值为19),以减少跟其他重要任务抢夺资源。且重新触发的软中断也能得到执行。
所有线程名为ksoftirqd/n
,n是处理器编号。只要do_softirq()函数发现已经执行过的内核线程重新触发了自己,ksoftirq就会被唤醒。
工作队列(等价于内核线程)是另外一种将工作推后执行的形式,将工作推后,交由内核线程去执行 - 这个下半部分总是会在进程上下文中执行,因此,工作队列允许重新调度甚至是睡眠。虽然在进程上下文,但不能访问用户空间,因为内核线程在用户空间没有相关的内存映射。
需要睡眠,选工作队列;不需要睡眠,选软中断或tasklet。
events/n
,n为处理器编号。struct workqueue_struct {
。。。
};
由cpu_workqueue_struct组成的数组,数组每一项对应一个处理器。每个工作者线程对应一个cpu_workqueue_struct结构体。
2. 表示工作的数据结构
struct work_struct {
atomic_long_t data; //参数
struct list_head entry;
work_func_t func; //待执行函数
};
工作者线程执行worker_thread(),这个函数执行死循环,当有操作被插入队列时会被唤醒。每个处理器的每种类型的队列都有这样的work_struct的链表,唤醒后会执行链表上的所有工作,完成后睡眠。
工作队列实现机制的总结
最高一层是工作者线程。系统允许多种工作者线程,每种类型,**CPU上都有一个该类的工作者线程。**每个工作者线程都要cpu_workqueue_struct结构体表示。
(4个CPU,有四个event类型的线程,4个自定义的falcon类型的线程,则会有一个对应event类型和一个对应falcon类型的workqueue_struct)
work_struct:推后执行的工作
使用工作队列
(1). 创建推后的工作
静态:DECLARE_WORK(name, void (*func)(void *), void *data);
动态:INIT_WORK(struct work_struct *work, void (*func)(void *), void *data);
(2). 工作队列处理函数
void work_handler(void *data);
(3). 对工作进行调度
将工作提交给缺省的events工作线程:schedule_work(&work);
会被马上执行
schedule_delayed_work(&work,delay);
会延迟delay个节拍后执行
(4). 刷新操作
目的:模块卸载前,或防止竞争,可能需要确保不再有待处理的工作
刷新:void flush_scheduled_work(void);
只是等待执行结束,不会刷新,等待时会休眠
取消延迟工作:int cancel_delayed_work(work);
(5). 创建新的工作队列
struct workqueue_struct *create_workqueue(const char* name);
会创建所有CPU上的工作者线程。
其调度函数:
int queue_work(struct workqueue_struct *wq, work);
int queue_delayed_work( wq, work, delay);
刷新函数:
flush_workqueue(struct workqueue_struct *wq);
三种选择:软中断、tasklet和工作队列。
tasklet基于软中断,工作队列则基于内核线程。
下半部 | 上下文 | 顺序执行保障 |
---|---|---|
软中断 | 中断 | 没有 |
tasklet | 中断 | 同类型不能同时执行,即使是不同处理器 |
工作队列 | 进程 | 没有(和进程上下文一样被调度) |
一般考虑两点:
(1). 有休眠的需要么?是,则选工作队列,否,选tasklet。
(2). 是否专注性能的提高?是则选软中断。
重要性:在使用下半部机制时,即使是在单处理器上,避免共享数据被同时访问也是至关重要的。
记住,一个下半部实体可能在任何时候执行。
tasklet:只需要考虑intra-tasklet的同步问题。
(1). 如果进程上下文和一个下半部共享数据,在访问数据之前,需要禁止下半部的处理并得到锁的使用权。为了本地和SMP的保护并防止死锁的出现。
(2). 如果中断上下文和一个下半部共享数据,。。。。。。。。,需要禁止中断并得到锁的使用权。。。。。。。。。。。。
工作队列:任何在工作队列中被共享的数据需要锁机制。
为保证共享数据的安全,一般是先得到一个锁后在禁止下半部的处理(驱动),如果是内核代码,一般仅禁止下半部就好。
void local_bh_disable()
:禁止本地处理器的软中断和tasklet的处理
void local_bh_enable()
:激活本地处理器的软中断和tasklet的处理
这两个函数是通过preempt_count(每个进程维护的一个计数器,之前是用于判断内核是否可被抢占),当计数器变为0时,下半部才能被处理。
不能禁止工作队列的执行,因为他是在进程上下文,不会涉及异步执行的问题。由于软中断和tasklet是异步发生的(即在中断处理返回的时候),所以内核代码必须禁止他们。???