Linux 内核是独立的软件,他没有使用任何 C 语言库,他自己实现了很多工具和辅助工具。
本系列文章将盘点一些内核提供的辅助工具函数。在编写驱动程序时,我们可以利用内核提供的工具函数,方便实现目标功能。
前期文章:
Linux驱动程序可用的内核辅助工具(一)
本文继续梳理内核提供的辅助函数。
Linux内核提供了一种延迟机制,支持函数延迟调用和执行。
软件中断 Softirq
这种延迟机制仅用于快速处理,因为它在在中断上下文中运行。很少(几乎从不)直接使用Softirq,只 有网络和块设备子系统使用Softirq。
Tasklet是 Softirq的实例,几乎每种需要使用Softirq的情况,有Tasklet就足够了。
在大多数情况下,Softirq 在硬件中断服务程序中被调度,这些中断发生很快,快过对它们的服务速度,内核会对它们排队以便稍后处理。
Ksoftirqd负责后期执行(进程上下文)。Ksoftirqd 是单 CPU 内核线程,用于处理未服务的软件中断。其具体实现在文件 kernel/softirq.c
中。
Tasklet
Tasklet 构建在 Softirq 的下半部机制。它们在内核中表示为 struct tasklet_struct
的实例:
struct tasklet_struct
{
struct tasklet_struct *next;
unsigned long state;
atomic_t count;
void (*func)(unsigned long);
unsigned long data;
};
Tasklet 本质上是不可重入的。
Tasklet声明
动态声明:
void tasklet_init(struct tasklet_struct *t, void (*func)(unsigned long), unsigned long data);
静态声明:
DECLARE_TASKLET( tasklet_example, tasklet_function, tasklet_data );
DECLARE_TASKLET_DISABLED(name, func, data);
前一个函数创建的 Tasklet 已经启用,将 count 设置为 0,准备好被调度。
后一个静态声明函数创建的 Tasklet 被禁用,其中 count 设置为 1。
被禁用的 Tasklet 可以通过函数 tasklet_enable ()
启用。
void tasklet_enable(struct tasklet_struct *);
如果要禁用 Tasklet 则调用如下函数:
void tasklet_disable(struct tasklet_struct *);
void tasklet_disable_nosync(struct tasklet_struct *);
tasklet_disable()
需要等 Tasklet 终止完成后才会返回。
tasklet_disable_nosync()
会立即返回,不用等禁止动作完成。
Tasklet调度
创建完成之后,需要调用如下函数,使其参与调度:
void tasklet_schedule(struct tasklet_struct *t);
void tasklet_hi_schedule(struct tasklet_struct *t);
内核把普通优先级和高优先级的Tasklet维护在两个不同的链表中。
tasklet_schedule
将 Tasklet
添加到普通优先级链表中,用 TASKLET_SOFTIRQ
标志调度相关的 Softirq。
tasklet_hi_schedule
将 Tasklet
添加到高优先级链表中,并用 HI_SOFTIRQ
标志调度相关的 Softirq。
调用函数 tasklet_kill 可以停止 Tasklet,这个函数的主要作用是防止 Tasklet再次运行,或者该Tasklet当前计划运行时,会等待其执行完成后再杀掉它。
void tasklet_kill(struct tasklet_struct *t);
tasklet的特点
Tasklet相关的一些特点 :
tasklet_schedule
将不会执行任何操作,该Tasklet最终也仅执行一次。这部分内容在上一篇文章简单介绍过。在这里详细讲解工作队列相关的内容。
工作队列是从Linux内核 2.6 版本开始增加的,这是一种最常用、最简单的延迟机制。工作队列只能运行在抢占上下文中。
工作队列是建立在内核线程之上的,即工作队列运行在内核线程上。内核有两种方法处理工作队列:
内核全局工作队列
这种队列整个系统共享,不应该长时间独自占用。另外,队列上挂起的任务在每个CPU上是串行执行的,任务不应该长时间睡眠。
如果占用队列的任务长时间睡眠,该队列上的其他任务都无法运行,任务可能需要较长时间才能得到CPU。
共享工作队列中的工作由每个CPU上内核创建的events/n线程执行。
定义并初始化一个 work 操作如下:
struct work_struct wrk;
INIT_WORK(_work, _func);
使用共享队列,不需要创建工作队列,可以使用下边几个函数,将工作添加到共享工作队列中参与调度:
(1)绑定到当前CPU
int schedule_work(struct work_struct *work);
(2)绑定到当前CPU,并延迟执行
static inline bool schedule_delayed_work(struct delayed_work *dwork,unsigned long delay);
(3)指定调度的CPU
int schedule_work_on(int cpu, struct work_struct *work);
(4)指定调度的CPU,并延迟执行
int scheduled_delayed_work_on(int cpu, struct delayed_work *dwork, unsigned long delay);
提交到共享队列的工作,可以通过 cancel_delayed_work()
取消。
刷新共享队列:
void flush_scheduled_work(void);
专用工作队列
工作队列的结构体为 struct workqueue_struct
,其中排列的工作结构体为 struct work_struct
。
工作能够被内核线程调度需要执行以下几个步骤:
struct workqueue_struct
struct work_strtuct
work_struct
中内核提供的工作队列操作函数在文件 include/linux/workqueue.h
中。
声明工作和工作队列:
struct workqueue_struct *myqueue;
struct work_struct thework;
定义工作处理函数
void dowork(void *data)
{
/*代码*/
};
初始化工作队列,把工作嵌入到工作队列中:
myqueue = create_singlethread_workqueue("mywork" );
INIT_WORK( &thework, dowork, );
创建工作队列的函数 create_singlethread_workqueue()
,会在每个可用的处理器上创建单独的内核线程。
使工作参与调度,即将任务提交到工作队列中
queue_work(myqueue, &thework);
至少延迟指定时间后,才能得到内核线程调度:
queue_dalayed_work(myqueue, &thework, );
如果工作已经在队列中,则调度函数返回 false;否则返回true。
delay 表示入队之前等待的 jiffy 数,可以使用辅助函数 msecs_to_jiffies
把标准 ms 延迟转换为 jiffy 。
取消工作队列中的某个任务,异步执行:
int cancel_delayed_work(struct work_struct *work);
必须检查函数的返回值是否是true。确保工作自身没有再次入队,之后必须显式刷新工作队列。
还有一种同步取消某个任务,分别用于非延迟 和 延迟的工作。
int cancel_work_sync(struct work_struct *work);
int cancel_delayed_work_sync(struct delayed_work *dwork);
中断是设备中止内核的一种方法,告诉内核发生了重要的事情。这些在Linux系统上被称作IRQ。中断的主要优点是避免对设备的轮询,由设备上报自身状态的改变,而不是由内核去轮询设备状态。
为获取中断通知,需要向 IRQ 注册一个称作中断处理程序的函数,在每次中断发生的时候,将调用这个函数。
注册中断处理程序
注册中断处理程序,通过回调函数实现。当中断触发时运行。注册函数在
文件中声明:
int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *name, void *dev)
注册成功,函数返回 0。其各个入口参数解释如下:
flags:这是一些位掩码,定义在
中。常用的掩码值如下:
IRQF_TIMER:通知内核这个处理程序是由系统定时器中断触发的。
IRQF_SHARED:用于两个或多个设备共享的中断线。共享这个中断线的所有设备都必须设置该标志。如果被忽略,将只能为该中断线注册一个处理程序。
IRQ_ONESHOT:主要在线程中断中使用,它要求内核在硬中断处理程序没有完成之前,不要重新启用该中断。在线程处理程序运行之前,中断会一直保持禁用状态。
name:内核用来标识 /proc/interrupts
和 /proc/irq
中的驱动程序。
dev:作为参数传递给中断处理程序,用来标识这个设备,对每个中断处理程序是唯一的。对于非共享中断,它可以是NULL,但共享中断不能为NULL。使用 dev 常见的方法是提供设备结构,即一个指向设备数据结构的指针。
handler:这是中断发生时的回调函数,也就是中断处理函数。其函数原型为:
typedef irqreturn_t (*irq_handler_t)(int, void *);
第一个参数为 IRQ 数值,与 request_irq 中的作用相同。第二个参数和reqeust_irq中的dev作用相同。这两个参数由内核传递给中断处理程序。
该函数的返回值有两个:
Linux系统中,编写中断处理程序时,不必担心重入问题,为了避免中断嵌套,所有处理器上的中断处理程序服务的 中断线均被内核禁用。
释放注册的中断处理程序:
void free_irq(unsigned int irq, void *dev)
如果指定的 IRQ 不是共享的,那么 free_irq 不会删除中断处理程序,而仅仅是禁用中断线。如果 IRQ 是共享的,则只有删除通过dev(应该与request_irq中使用的相同)确定的中断处理程序。
如果中断线上所有中断处理程序被删除,中断线才会被禁用。
直到指定 IRQ 中断处理函数没有处理完,则 free_irq 会阻塞;中断处理完成后, free_irq 会立即执行。
注意,应该必须避免在中断上下文中使用 request_irq 和 free_irq。
中断服务程序和锁
中断服务程序运行在原子上下文中,只能使用自旋锁控制并发。
每当有全局变量可供用户代码(用户任务,即系统调用)和中断代码访问时,此共享数据应受用户代码中 spin_lock_irqsave()
的保护。
只使用自旋锁也会出问题。中断处理程序的优先级总是高于用户任务,即使该任务持有旋锁也会被中断。仅仅禁用 IRQ 是不够的。在另一个CPU上可能会发生中断,如果访问相同的数据,则会发生异常。
而使用 spin_lock_irqsave()
将禁用本地CPU上的所有中断,防止系统调用被任何类型的中断所中断。
两个不同的中断处理程序间共享数据时,在这些处理程序中还应该使用 spin_lock_irqsave()
来保护共享数据,以防止其他 IRQ 触发和无用的自旋。
中断下半部(Bottom halve)是一种把中断处理程序分成两部分的机制:上半部、下半部。
无论中断处理程序是否持有自旋锁,在运行该中断处理程序的CPU上都会禁止抢占。中断处理程序耗费的时间越多,给予其他任务的 CPU 时间就越少,这可能会增大其他中断的延迟,从而增加整个系统的延迟。
要保持系统的响应及时,在于尽快确认引发中断的设备。
在 Linux 系统上(实际上在所有操作系统上,硬件设计决定),任何中断处理程序运行时,都会在所有处理器上禁用其当前中断线,有时可能需要在实际运行处理程序的 CPU 上禁止所有中断,但不应该错过中断。
为了满足这一需要,引入了半部的概念。
也就是把中断处理过程分成两个部分:
(1)第一部分称作上半部或者硬IRQ,它使用 request_irq() 注册处理函数,最终将根据需要屏蔽/隐藏中断。执行快速操作,调度第二部分和下一部分,然后确认中断线。禁用的所有中断都必须在退出下半部之前重新启用。
(2)第二部分称作下半部,会处理一些比较耗时的任务,在它执行期间,中断再次启用。这样就不会错过中断。
中断下半部的设计使用了工作延迟机制,前边已经介绍过了。根据选择不同,下半部可以运行在中断上下文,也可以运行在进程上下文。
Softirq 和 Tasklet在(软件)中断上下文中执行,禁止被抢占。工作队列 和 线程IRQ 在进程(或者只是任务)上下文中执行,可以被抢占。
Tasklet 作为下半部的延迟机制大多用在 DMA、网络和块设备驱动程序中。示例代码:
struct my_data
{
int my_int_var;
struct tasklet_struct the_tasklet;
int dma_request;
};
static void my_tasklet_work(unsigned long data)
{
/* 代码 */
}
struct my_data *md = init_my_data;
/* 在probe或init函数中的某个位置*/
[...]
tasklet_init(&md->the_tasklet, my_tasklet_work, (unsigned long)md);
static irqreturn_t my_irq_handler(int irq, void *dev_id)
{
struct my_data *md = dev_id;
/* 安排Tasklet */
tasklet_schedule(&md.dma_tasklet);
return IRQ_HANDLED;
}
在上面的例子中,Tasklet 将执行函数 my_tasklet_work()
工作队列作为中断下半部,示例代码:
static DECLARE_WAIT_QUEUE_HEAD(my_wq); /* 声明并初始化等待队列 */
static struct work_struct my_work;
/* probe函数的某些地方 */
/*
*工作队列的初始化。work_handler是将要返回的调用
*/
INIT_WORK(my_work, work_handler);
static irqreturn_t my_interrupt_handler(int irq, void *dev_id)
{
uint32_t val;
struct my_data = dev_id;
val = readl(my_data->reg_base + REG_OFFSET);
if (val == 0xFFCD45EE))
{
my_data->done = true;
wake_up_interruptible(&my_wq);
}
else
{
schedule_work(&my_work);
}
return IRQ_HANDLED;
}
上面的示例使用等待队列或工作队列来唤醒正在等待而可能睡眠的进程,或者根据寄存器的值来调度工作。由于没有共享数据或资源,因此不需要禁用其他的 IRQ。
线程化中断(threaded IRQ)的主要目标是将中断禁用的时间减少到最低限度。
使用线程化中断,注册中断处理程序的方式将得到简化。甚至不必自己调度下半部,内核会完成这部分工作。下半部在专用内核线程中执行。
使用 request_threaded_irq()
来代替 request_irq()
注册中断处理函数:
int request_threaded_irq(unsigned int irq, irq_handler_t handler,\
irq_handler_t thread_fn, unsigned long irqflags, \
const char *devname, void *dev_id)
这个函数的参数列表中有两个函数:
handler 函数:这与使用 request_irq()
注册时使用的函数一样。它表示上半部函数,在原子上下文中(或硬中断)中运行。如果它能更快地处理中断,不用下半部,它应该返回 IRQ_HANDLED
。
如果中断处理需要使用下半部。它应该返回 IRQ_WAKE_THREAD
,从而导致调度 thread_fn
函数,此时必须提供 thread_fn
函数。
thread_fn 函数:这代表下半部,由上半部调度。当硬中断处理程序(handler
函数)返回 IRQ_WAKE_THREAD
时,将调度与该下半部相关联的内核线程,在内核线程运行时调用 thread_fn
函数。thread_fn
函数完成时必须返回 IRQ_HANDLED
。
在任何能够使用工作队列调度下半部的地方,都可以使用线程化中断。
真正的线程化中断必须定义 handler
和 thread_fn
。如果 handler
为 NULL
,而 thread_fn
不为 NULL
,则内核将安装默认的硬中断处理程序,它将简单地返回 IRQ_WAKE_THREAD
来调度下半部。
记住,handler
总是在中断上下文中调用。
OK,终于介绍完了。后面精彩继续。
感谢阅读,加油~
觉得文章不错,点击“分享”、“赞”、“在看” 呗!