LDD学习-第十章-顶半部与底半部 设备的中断会打断内核中进程的正常调度和运行,系统对高吞吐率的追求势必要求中断服务程序尽可能地短小精悍。为了在中断执行时间尽可能短和中断处理需要完成大量工作之间找到一个平衡点,Linux将中断处理程序分解为两个半部:顶半部(top half)和底半部(bottom half)。
顶半部是实际响应中断的例程,也就是request_irq注册的中断例程,一般只做严格时间限制的工作;底半部是一个被顶半部调度,并在稍后更安全的时间内执行的例程。所谓的稍后更安全的时间,是并没有规定的。只要是把中断任务稍微推后,并在CPU不太繁忙的时候工作就可以,一般情况下底半部是在顶半部返回后就马上执行。但是,他们之间的重要区别是,顶半部禁止本CPU上的相同中断,底半部允许所有中断再次发生。
下半部的任务就是执行与中断处理密切相关但中断处理程序本生身不执行的任务。最好情况当然是中断处理程序(顶半部)把所有的工作都交給下半部执行,而自己啥都不做。因为我们总是希望中断处理程序尽可能快的返回。但是,中断处理程序注定要完成一部分工作。遗憾的是,并没有谁严格规定说什么任务应该在哪个部分中完成,换句话说,这个决定完全由驱动工程师来做。记住,中断处理程序会异步执行,并且在最好的情况下它也会锁定当前的中断线,因此将中断处理程序缩短的越小就越好。当然,没有规则并不是没有经验和教训:
1.如果一个任务对时间非常敏感,感觉告诉我还是将其放在中断处理程序中执行是个好的选择。2.如果一个任务和硬件相关,还是将其放在中断处理程序中执行吧。3.如果一个任务要保证不被其他中断(特别是相同的中断)打断,那就将其放在中断处理程序中吧。4.其他所有任务,除非你有更好的理由,否则全部丢到下半部执行。
总之,一句话:中断处理程序要执行的越快越好。
底半部的实现
底半部的实现主要有几种方式:tasklet,工作队列和软中断。
1. tasklet实现。tasklet是底半部处理的优选机制,因为这种机制非常快,但是所有taklet代码必须是原子的,而且内核保证所有tasklet会运行在第一次调度他们的函数所运行的CPU上。
tasklet可以被系统多次调度运行,但是tasklet的调度不会积累,实际只运行一次,也就是说内核会保证每次只有一个tasklet实例在本地CPU运行,在运行tasklet的过程中如果再被调度,则tasklet会重新运行(tasklet运行前的几次调度,只会引起一次实际的运行;tasklet运行中的调度,会使tasklet重新调度运行)。在SMP系统上,不同的tasklet可以并行运行在不同的CPU上,在这种情况下要考虑tasklet之间的竞争关系。并且,tasklet运行过程中允许中断的再次发生(底半部的特征),因此需要保证tasklet和顶半部的竞争关系。
tasklet不能休眠,也就是说不能在tasklet中使用信号量或者其他什么阻塞式的函数。由于tasklet运行时允许响应中断,所以必须做好预防工作,如果新加入的tasklet和中断处理程序之间共享了某些数据额的话。两个相同的tasklet绝不能同时执行,如果新加入的tasklet和其他的tasklet或者软中断共享了数据,就必须要进行适当地锁保护。
tasklet特性:
1.0 tasklet可以被禁止或者启用,但是只有禁用和启用的次数相同,tasklet才会运行
2.0 tasklet可以注册自己本身
3.0 tasklet可以调度在通常的优先级和较高的优先级上,且较高优先级总会先执行
4.0 如果系统负荷不重,tasklet会马上执行,但始终不会晚于下一个定时器滴答
5.0 一个tasklet和其他tasklet可以并发,但是自己永远是串行执行的。即,同一个tasklet不会在多个CPU上同时运行,且总是运行在第一次调度自己的程序运行的CPU上
struct tasklet_struct数据结构
struct tasklet_struct{
struct tasklet_struct *next;
unsigned long state;
atomic_t count;
void (*func)(unsigned long);
unsigned long data;
};
其中, func:tasklet的处理程序
data:func的唯一参数(unsinged long)
state:0, TASKLET_STATE_SCHED(已调度,准备运行),TASKLET_STATE_RUN(tasklet正在运行)。
count:tasklet引用计数,0时tasklet被激活,并在挂起状态时能够运行;非0值,tasklet被禁止运行
静态创建tasklet方法:
DECLARE_TASKLET(name,func,data);
DECLARE_TASKLET_DISABLED(name,func,data); //count==1,被禁止运行
动态创建tasklet方法:
tasklet_init(t,tasklet_handler,dev);
其中,name: 该tasklet起的名字
func:tasklet_struct中的func,与改tasklet绑定的处理函数
ata:传给func的参数unsigned long型
在顶半部调度tasklet
tasklet_schedule(&my_tasklet);
tasklet其他函数接口
void tasklet_diasble(struct tasklet_struct *t);
忙等待对应的tasklet运行完成。调用此函数后,保证该tasklet不会在运行中
void tasklet_disable_nosync(struct tasklet_struct *t);
禁用指定的tasklet,但是不会等待tasklet运行完成。会产生安全问题
void tasklet_enable(struct tasklet_struct *t);
启用一个被禁止的tasklet,如果被调度,则很快被运行。
void tasklet_hi_schedule(struct tasklet_struct *t);
较高优先级的tasklet调用,一般用在延迟时间较短的tasklet上
void tasklet_kill(struct tasklet_struct *t);
保证指定的tasklet不会再度被调度运行。设备关闭或移除是调用此函数。
如果指定的tasklet正在调度执行,则等待完成
2. 工作队列实现。
工作队列会在将来的某个时间、在某个特殊的工作者进程上下文中调用一个函数。因此,可以在必要时休眠,并且允许重新调度,但是我们不能从工作队列向用户空间复制数据。因为,工作队列函数运行在工作者进程上下文(内核线程)中,没有相应的用户态内存映射,例如DMA和内存映射。
工作队列子系统是一个用于创建内核线程的接口,通过它创建的进程负责执行由内核其他部分排到队列里的任务。它创建的这些内核线程被称作工作者线程(worker threads).工作队列可以让你的驱动程序创建一个专门的工作者线程来处理需要推后的工作。不过,工作队列子系统提供了一个缺省的工作者线程来处理这些工作。因此,工作队列最基本的表现形式就转变成一个把需要推后执行的任务交给特定的通用线程这样一种接口。缺省的工作线程叫做event/n.每个处理器对应一个线程,这里的n代表了处理器编号。除非一个驱动程序或者子系统必须建立一个属于自己的内核线程,否则最好还是使用缺省线程。
共享工作者队列(线程)
如果我们不实现自己的工作队列(workqueque_struct),则可以使用内核默认提供给每个CPU的工作者队列(线程)。使用这个工作队列时,我们应该记住正在和其他人共享该队列。这意味着我们不应该长期独占该队列,即不能长时间休眠;而且,我们的任务可能需要更长的时间才能获得处理器时间以运行。
work_struct结构
struct work_struct{
unsigned long pending;
struct list_head entry; //连接所有工作的链表
void (*func)(void *); //处理函数
void *data; //传递给处理函数的参数
void *wq_data;
struct timer_list timer; //y延迟工作队列所用到的定时器
}
静态创建工作队列:
DECLARE_WORK(my_work, void (* func)(void *), void *data);
动态创建工作队列:
struct work_struct my_work;
INIT_WORK(struct work_struct *my_work, void (*func)(void *), void *data);
//第一次创建工作时调用
PREPARE_WORK(struct work_struct *, void (*func)(void *), void *data);
//用于修改已经提交到工作队列中的struct work_struct 结构
在顶半部调度工作队列,即使用内核默认的共享的工作者队列(线程)。
void schedule_work(struct work_struct *t);
//工作队列所在的工作者队列被唤醒时,在工作队列中等待执行的函数将被执行
void schedule_delay_work(struct work_struct *work, unsigned long delay);
//延迟delay时间,执行工作队列
取消挂起的工作队列入口项
int cancel_delayed_work(struct work_struct *work);
返回非0值,该入口项在开始执行前被取消;0,该工作已经在其他处理器上运行。
当插入工作队列中执行的工作依赖于工作队列中已经存在的函数的执行,
则可以利用刷新机制确保工作队列中的工作已经执行完毕。
void fulsh_workqueque(struct workqueue_struct *queue);
在该函数返回后,内核确保任何在该函数调用前执行的函数均执行完成,并不再运行。
释放资源
void destory_workqueue(struct workqueue_struct *queue);
非共享工作队列。
以上,是使用内核默认提供的工作者线程队列(大多数时候建议使用),但是也可以自己创建工作队列(线程)。
struct workqueue_struct *create_workqueue(const char *name);
//name是新创建的工作队列线程的名字,内核负责该工作队列的初始化。
struct workqueue_struct *creat_singlethread_workqueque(const char *name);
前一个函数会在所有CPU中为该队列创建专用的线程;后一个函数会在调用的CPU上创建工作队列的线程。随后调用下面2个函数可以将work_struct结构的工作提交到指定的工作队列(线程)
将工作提交到已经存在的工作队列,即不使用内核默认提供的工作者线程队列
int queue_work(struct workqueue_struct *queue, struct work_struct *work);
int queque_delayed_work(struct workqueue_struct *queue,
struct work_struct *work, unsigned long delay);
成功返回 1, 非0说明work已经存在于队列中,不能再次添加。
取消提交的工作work_struct可以调用上面描述的cancel_delayed_work函数,但是刷新工作队列需要调用下面的函数。由于无法得知是否有其他人使用该队列,因此不能确切的知道该函数需要多长时间才能返回。
void flush_scheduled_work(void);