💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
## 7.6. 工作队列 工作队列是, 表面上看, 类似于 taskets; 它们允许内核代码来请求在将来某个时间调用一个函数. 但是, 有几个显著的不同在这 2 个之间, 包括: - tasklet 在软件中断上下文中运行的结果是所有的 tasklet 代码必须是原子的. 相反, 工作队列函数在一个特殊内核进程上下文运行; 结果, 它们有更多的灵活性. 特别地, 工作队列函数能够睡眠. - tasklet 常常在它们最初被提交的处理器上运行. 工作队列以相同地方式工作, 缺省地. - 内核代码可以请求工作队列函数被延后一个明确的时间间隔. 两者之间关键的不同是 tasklet 执行的很快, 短时期, 并且在原子态, 而工作队列函数可能有高周期但是不需要是原子的. 每个机制有它适合的情形. 工作队列有一个 struct workqueue_struct 类型, 在 <linux/workqueue.h> 中定义. 一个工作队列必须明确的在使用前创建, 使用一个下列的 2 个函数: ~~~ struct workqueue_struct *create_workqueue(const char *name); struct workqueue_struct *create_singlethread_workqueue(const char *name); ~~~ 每个工作队列有一个或多个专用的进程("内核线程"), 它运行提交给这个队列的函数. 如果你使用 create_workqueue, 你得到一个工作队列它有一个专用的线程在系统的每个处理器上. 在很多情况下, 所有这些线程是简单的过度行为; 如果一个单个工作者线程就足够, 使用 create_singlethread_workqueue 来代替创建工作队列 提交一个任务给一个工作队列, 你需要填充一个 work_struct 结构. 这可以在编译时完成, 如下: ~~~ DECLARE_WORK(name, void (*function)(void *), void *data); ~~~ 这里 name 是声明的结构名称, function 是从工作队列被调用的函数, 以及 data 是一个传递给这个函数的值. 如果你需要建立 work_struct 结构在运行时, 使用下面 2 个宏定义: ~~~ INIT_WORK(struct work_struct *work, void (*function)(void *), void *data); PREPARE_WORK(struct work_struct *work, void (*function)(void *), void *data); ~~~ INIT_WORK 做更加全面的初始化结构的工作; 你应当在第一次建立结构时使用它. PREPARE_WORK 做几乎同样的工作, 但是它不初始化用来连接 work_struct 结构到工作队列的指针. 如果有任何的可能性这个结构当前被提交给一个工作队列, 并且你需要改变这个队列, 使用 PREPARE_WORK 而不是 INIT_WORK. 有 2 个函数来提交工作给一个工作队列: ~~~ int queue_work(struct workqueue_struct *queue, struct work_struct *work); int queue_delayed_work(struct workqueue_struct *queue, struct work_struct *work, unsigned long delay); ~~~ 每个都添加工作到给定的队列. 如果使用 queue_delay_work, 但是, 实际的工作没有进行直到至少 delay jiffies 已过去. 从这些函数的返回值是 0 如果工作被成功加入到队列; 一个非零结果意味着这个 work_struct 结构已经在队列中等待, 并且第 2 次没有加入. 在将来的某个时间, 这个工作函数将被使用给定的 data 值来调用. 这个函数将在工作者线程的上下文运行, 因此它可以睡眠如果需要 -- 尽管你应当知道这个睡眠可能怎样影响提交给同一个工作队列的其他任务. 这个函数不能做的是, 但是, 是存取用户空间. 因为它在一个内核线程中运行, 完全没有用户空间来存取. 如果你需要取消一个挂起的工作队列入口, 你可以调用: ~~~ int cancel_delayed_work(struct work_struct *work); ~~~ 返回值是非零如果这个入口在它开始执行前被取消. 内核保证给定入口的执行不会在调用 cancel_delay_work 后被初始化. 如果 cancel_delay_work 返回 0, 但是, 这个入口可能已经运行在一个不同的处理器, 并且可能仍然在调用 cancel_delayed_work 后在运行. 要绝对确保工作函数没有在 cancel_delayed_work 返回 0 后在任何地方运行, 你必须跟随这个调用来调用: ~~~ void flush_workqueue(struct workqueue_struct *queue); ~~~ 在 flush_workqueue 返回后, 没有在这个调用前提交的函数在系统中任何地方运行. 当你用完一个工作队列, 你可以去掉它, 使用: ~~~ void destroy_workqueue(struct workqueue_struct *queue); ~~~ ### 7.6.1. 共享队列 一个设备驱动, 在许多情况下, 不需要它自己的工作队列. 如果你只偶尔提交任务给队列, 简单地使用内核提供的共享的, 缺省的队列可能更有效. 如果你使用这个队列, 但是, 你必须明白你将和别的在共享它. 从另一个方面说, 这意味着你不应当长时间独占队列(无长睡眠), 并且可能要更长时间它们轮到处理器. jiq ("just in queue") 模块输出 2 个文件来演示共享队列的使用. 它们使用一个单个 work_struct structure, 这个结构这样建立: ~~~ static struct work_struct jiq_work; /* this line is in jiq_init() */ INIT_WORK(&jiq_work, jiq_print_wq, &jiq_data); ~~~ 当一个进程读 /proc/jiqwq, 这个模块不带延迟地初始化一系列通过共享的工作队列的路线. ~~~ int schedule_work(struct work_struct *work); ~~~ 注意, 当使用共享队列时使用了一个不同的函数; 它只要求 work_struct 结构作为一个参数. 在 jiq 中的实际代码看来如此: ~~~ prepare_to_wait(&jiq_wait, &wait, TASK_INTERRUPTIBLE); schedule_work(&jiq_work); schedule(); finish_wait(&jiq_wait, &wait); ~~~ 这个实际的工作函数打印出一行就象 jit 模块所作的, 接着, 如果需要, 重新提交这个 work_structcture 到工作队列中. 在这是 jiq_print_wq 全部: ~~~ static void jiq_print_wq(void *ptr) { struct clientdata *data = (struct clientdata *) ptr; if (! jiq_print (ptr)) return; if (data->delay) schedule_delayed_work(&jiq_work, data->delay); else schedule_work(&jiq_work); } ~~~ 如果用户在读被延后的设备 (/proc/jiqwqdelay), 这个工作函数重新提交它自己在延后的模式, 使用 schedule_delayed_work: ~~~ int schedule_delayed_work(struct work_struct *work, unsigned long delay); ~~~ 如果你看从这 2 个设备的输出, 它看来如: ~~~ % cat /proc/jiqwq time delta preempt pid cpu command 1113043 0 0 7 1 events/1 1113043 0 0 7 1 events/1 1113043 0 0 7 1 events/1 1113043 0 0 7 1 events/1 1113043 0 0 7 1 events/1 % cat /proc/jiqwqdelay time delta preempt pid cpu command 1122066 1 0 6 0 events/0 1122067 1 0 6 0 events/0 1122068 1 0 6 0 events/0 1122069 1 0 6 0 events/0 1122070 1 0 6 0 events/0 ~~~ 当 /proc/jiqwq 被读, 在每行的打印之间没有明显的延迟. 相反, 当 /proc/jiqwqdealy 被读时, 在每行之间有恰好一个 jiffy 的延时. 在每一种情况, 我们看到同样的进程名子被打印; 它是实现共享队列的内核线程的名子. CPU 号被打印在斜线后面; 我们从不知道当读 /proc 文件时哪个 CPU 会在运行, 但是这个工作函数之后将一直运行在同一个处理器. 如果你需要取消一个已提交给工作队列的工作入口, 你可以使用 cancel_delayed_work, 如上面所述. 刷新共享队列需要一个不同的函数, 但是: ~~~ void flush_scheduled_work(void); ~~~ 因为你不知道别人谁可能使用这个队列, 你从不真正知道 flush_schduled_work 返回可能需要多长时间.