前言
本篇文章来讲解在Linux中tasklet,工作队列,内核线程的使用。
一、tasklet
tasklet在内核里面其实就是下面这样的一个结构体:
struct tasklet_struct { struct tasklet_struct *next; unsigned long state; atomic_t count; void (*func)(unsigned long); unsigned long data; };
下面是对 struct tasklet_struct 结构体成员的解释:
struct tasklet_struct *next: 链表中的下一个任务队列项。任务队列项通过 next 成员在链表中连接在一起,可以形成一个队列。
unsigned long state: 任务队列项的状态标志。用于跟踪任务队列项当前的状态,例如等待执行、已禁用等。
atomic_t count: 计数器,用于跟踪任务队列项的引用计数。当某个任务队列项被多个地方引用时,可以通过增加计数器来避免释放或重用。
void (*func)(unsigned long): 函数指针,指向任务队列项需要执行的函数。当任务队列项被调度执行时,内核将调用该函数来执行任务。
unsigned long data: 存储额外的数据,供任务队列项函数使用。可以将数据与任务队列项关联起来,以在执行函数时传递给函数。
相关的三个重要函数:
tasklet_init(struct tasklet_struct *tasklet, void (*func)(unsigned long), unsigned long data)
tasklet_init() 函数用于初始化一个任务队列项。它接受三个参数:
tasklet:指向要初始化的 struct tasklet_struct 对象的指针。
func:指向任务队列项的处理函数的指针。该函数将在任务队列项被调度执行时被调用。
data:传递给任务队列项处理函数的数据。
通过调用 tasklet_init(),可以为任务队列项设置适当的处理函数和数据。初始化的任务队列项是处于禁用状态的,需要通过调用 tasklet_schedule() 来启用它。
tasklet_kill(struct tasklet_struct *tasklet)
tasklet_kill() 函数用于停止一个正在运行或已经被调度的任务队列项。它接受一个参数:
tasklet:指向要停止的任务队列项的指针。
调用 tasklet_kill() 将会禁用任务队列项,并确保它不会被再次调度执行。这在你不再需要该任务队列项时非常有用,可以确保它不会再干扰系统的正常操作。
tasklet_schedule(struct tasklet_struct *tasklet)
tasklet_schedule() 函数用于将一个任务队列项添加到任务队列中以待执行。它接受一个参数:
tasklet:指向要调度的任务队列项的指针。
调用 tasklet_schedule() 将会启用被调度的任务队列项,并安排它在适当的时机执行。在软中断上下文中调用该函数会立即执行任务队列项的处理函数,而在其他上下文中,例如进程上下文,稍后会调度执行。
任务队列项的处理函数将在任务队列中的适当时机被调用,以更好地与操作系统的内核执行过程进行协调。
使用tasklet步骤:
1.先定义tasklet,需要使用时调用tasklet_schedule,驱动卸载前调用tasklet_kill。
2.tasklet_schedule只是把tasklet放入内核队列,它的func函数会在软件中断的执行过程中被调用。
注意:
tasklet_schedule只会把tasklet放入队列一次,调用完成后需要再次放入队列中。
tasklet内部机制:
tasklet属于TASKLET_SOFTIRQ软件中断。
入口函数为tasklet_action。
void __init softirq_init(void) { int cpu; for_each_possible_cpu(cpu) { per_cpu(tasklet_vec, cpu).tail = &per_cpu(tasklet_vec, cpu).head; per_cpu(tasklet_hi_vec, cpu).tail = &per_cpu(tasklet_hi_vec, cpu).head; } open_softirq(TASKLET_SOFTIRQ, tasklet_action); open_softirq(HI_SOFTIRQ, tasklet_hi_action); }
当驱动程序调用tasklet_schedule时,会设置tasklet的state为TASKLET_STATE_SCHED,并把它放入某个链表:
void __tasklet_schedule(struct tasklet_struct *t) { unsigned long flags; local_irq_save(flags); t->next = NULL; *__this_cpu_read(tasklet_vec.tail) = t; __this_cpu_write(tasklet_vec.tail, &(t->next)); raise_softirq_irqoff(TASKLET_SOFTIRQ); local_irq_restore(flags); }
当发生硬件中断时,内核处理完硬件中断后,会处理软件中断。对于TASKLET_SOFTIRQ软件中断,会调用tasklet_action函数。
注意:
1.tasklet_schedule调度tasklet时,其中的函数并不会立刻执行,而只是把tasklet放入队列。
2.调用一次tasklet_schedule,只会导致tasklnet的函数被执行一次。
3.如果tasklet的函数尚未执行,多次调用tasklet_schedule也是无效的,只会放入队列一次。
tasklet_action函数:
这个函数会将队列中的tasklet逐个取出来执行:
static __latent_entropy void tasklet_action(struct softirq_action *a) { struct tasklet_struct *list; local_irq_disable(); list = __this_cpu_read(tasklet_vec.head); __this_cpu_write(tasklet_vec.head, NULL); __this_cpu_write(tasklet_vec.tail, this_cpu_ptr(&tasklet_vec.head)); local_irq_enable(); while (list) { struct tasklet_struct *t = list; list = list->next; if (tasklet_trylock(t)) { if (!atomic_read(&t->count)) { if (!test_and_clear_bit(TASKLET_STATE_SCHED, &t->state)) BUG(); t->func(t->data); tasklet_unlock(t); continue; } tasklet_unlock(t); } local_irq_disable(); t->next = NULL; *__this_cpu_read(tasklet_vec.tail) = t; __this_cpu_write(tasklet_vec.tail, &(t->next)); __raise_softirq_irqoff(TASKLET_SOFTIRQ); local_irq_enable(); } }
二、工作队列
tasklet使用起来非常方便简单,但是tasklet依然是属于中断,在中断执行时,应用程序还是无法得到执行,这样就可能导致系统的卡顿。所以就有了工作队列。
工作队列在内核中的定义:
struct work_struct { atomic_long_t data; struct list_head entry; work_func_t func; #ifdef CONFIG_LOCKDEP struct lockdep_map lockdep_map; #endif };
1.atomic_long_t data: 这是一个原子长整型变量,通常用于存储工作项相关的数据。原子型意味着对该变量的读取和写入是原子操作,可以在多线程环境下进行安全的并发访问。
2.struct list_head entry: 这是一个链表节点,用于将工作项(work_struct)添加到一个链表中。链表节点通常包含指向前一个节点和后一个节点的指针,以便在链表中进行插入、删除和遍历操作。
3.work_func_t func: 这是一个函数指针类型(work_func_t),用于指向执行工作项的函数。通过该函数指针,可以在工作队列中执行相应的操作或任务。
4.#ifdef CONFIG_LOCKDEP 和 struct lockdep_map lockdep_map: 这部分代码是一个预处理指令,用于条件编译。在特定的配置条件下(CONFIG_LOCKDEP宏定义为真),会包含struct lockdep_map类型的lockdep_map成员变量。lockdep_map用于在工作项中添加用于锁依赖分析的调试信息。
使用工作队列时,步骤如下:
① 构造一个work_struct结构体,里面有函数;
② 把这个work_struct结构体放入工作队列,内核线程就会运行work中的函数。
相关函数:
1.INIT_WORK:
INIT_WORK用于初始化一个工作项(struct work_struct)。其原型如下:
void INIT_WORK(struct work_struct *work, void (*func)(struct work_struct *work));
该函数用于初始化一个工作项,并指定要执行的回调函数(func)。回调函数将在调用schedule_work函数时被调度执行。初始化后的工作项可以被添加到工作队列中,以便在合适的时机执行相关操作。
2.schedule_work:
schedule_work用于将一个工作项添加到工作队列,以便在合适的时机执行工作项的回调函数。其原型如下:
int schedule_work(struct work_struct *work);
该函数将指定的工作项添加到工作队列,工作队列会按照一定的调度机制在合适的时机执行工作项。工作项的回调函数将在调度时被执行。这样可以将一些延后执行的任务或者需要在工作队列上下文中执行的任务安排在合适的时机执行。
注意:schedule_work函数是异步的,意味着它只是将工作项添加到队列,然后立即返回,并不会等待工作项的执行完成。
三、内核线程
有了工作队列后就可以把耗时的任务放入工作队列中进行处理了,但是当有多个work时前面没有执行完的work就会影响到后面work的执行,这样的效率还是不高,那么这个时候就需要使用中断的线程化处理了。
使用这个机制需要使用到下面这个函数:
request_threaded_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);
该函数用于请求指定中断号(irq)的中断,并将中断处理函数(handler)注册到该中断上。当中断事件发生时,处理函数会被调用。
这个函数具有以下参数:
irq:要请求的中断号。
handler:中断处理函数,即中断发生时要调用的函数。它通常是一个标准的中断处理函数,接收中断相关的参数。
thread_fn:中断线程函数,当中断处理函数返回IRQ_WAKE_THREAD时会调用。它通常用于执行耗时操作,以便避免中断处理函数的执行时间过长。
irqflags:中断标志,用于指定中断的行为,例如共享中断、触发类型等。
devname:设备名称,用于标识请求中断的设备。
dev_id:设备ID,用于传递设备相关的数据给中断处理函数。
request_threaded_irq的工作流程如下:
1.检查中断号的有效性和是否已被占用。如果中断号无效或已被占用,函数会返回错误。
2.创建一个中断描述符(struct irq_desc)并初始化相应的字段。
3.将中断处理函数和中断线程函数注册到中断描述符的对应字段上。
4.配置中断描述符的中断行为,如中断类型、中断共享等。
5.将中断描述符添加到全局中断控制器中断描述符数组中,以便系统可以管理该中断。
6.启用中断,允许中断事件触发中断处理函数的调用。
在请求中断成功后,中断事件发生时,中断控制器会检测到对应的中断号,然后调用已注册的中断处理函数或中断线程函数(如果应用)。中断处理函数是在中断上下文中执行的,因此需要遵循中断上下文的要求,尽量做到快速和非阻塞。如果中断处理函数在处理中需要较长时间,可以调用wake_up_process函数唤醒中断线程函数,以便执行更复杂的工作。
总结而言,request_threaded_irq函数用于请求指定中断,并将中断处理函数注册到该中断上。它提供了一种在Linux内核中管理中断的机制,以响应硬件事件或触发的事件,处理与中断相关的操作
总结
本篇文章就讲解到这里,需要大家好好理解并进行实践。