本节讨论如何使用任务队列来延迟处理某些任务并将这些任务的执行委托给另一个内核线程。
内核编程中的一项常见操作是对某个任务进行调度,使它以后由另一线程执行。以下示例给出了可能需要以后由另一线程执行某个任务的一些原因:
当前的代码路径对时间有关键要求。要执行的其他任务对时间没有关键要求。
其他任务可能需要获取另一个线程当前持有的锁。
在当前上下文中无法进行阻塞。但其他任务可能需要阻塞,例如,它需要等待内存。
某种情况正在阻止代码路径完成,但是当前的代码路径不能休眠或失败。需要将当前任务排入队列,以便在该情况消失后执行。
需要以并行方式启动多个任务。
对于上面的每种情况,任务都在不同的上下文中执行。不同的上下文通常是持有一组不同锁的不同内核线程,并可能具有不同的优先级。任务队列提供一个通用内核 API 来调度异步任务。
任务队列是一个任务列表,一个或多个线程为该列表提供服务。如果任务队列只有一个服务线程,则所有任务肯定会按照它们在列表中添加的先后顺序执行。如果任务队列有多个服务线程,则任务的执行顺序是未知的。
如果任务队列有多个服务线程,请确保某个任务的执行不依赖于其他任何任务的执行。任务之间的相关性会导致产生死锁。
以下 DDI 接口管理任务队列。这些接口在 sys/sunddi.h 头文件中定义。有关这些接口的更多信息,请参见 taskq(9F) 手册页。
ddi_taskq_t |
不透明句柄 |
TASKQ_DEFAULTPRI |
系统缺省优先级 |
DDI_SLEEP |
可以阻塞以获得内存 |
DDI_NOSLEEP |
不能阻塞以获得内存 |
ddi_taskq_create() |
创建任务队列 |
ddi_taskq_destroy() |
销毁任务队列 |
ddi_taskq_dispatch() |
在任务队列中添加任务 |
ddi_taskq_wait() |
等待暂挂的任务完成 |
ddi_taskq_suspend() |
暂挂任务队列 |
ddi_taskq_suspended() |
检查任务队列是否已暂挂 |
ddi_taskq_resume() |
恢复暂挂的任务队列 |
在驱动程序中的典型应用是在调用 attach(9E) 时创建任务队列。大多数 taskq_dispatch() 调用都来自中断上下文。
要了解 Solaris 驱动程序中使用的任务队列,请访问 http://hub.opensolaris.org/bin/view/Main/。 在右上角单击 "Source Browser"(源代码浏览器)。在搜索区域的 "Symbol"(符号)字段中,输入 ddi_taskq_create。在 "File Path"(文件路径)字段中输入 amr。在“项目”列表中选择 onnv。单击 "Search"(搜索)按钮。在搜索结果中,应可看到 Dell PERC 3DC/4SC/4DC/4Di RAID 设备的 SCSI HBA 驱动程序 (amr.c)。
单击文件名 amr.c。将在 amr_attach() 入口点中调用 ddi_taskq_create() 函数。ddi_taskq_destroy() 函数将在 amr_detach() 入口点中调用,也会在 amr_attach() 入口点的错误处理部分中调用。ddi_taskq_dispatch () 函数在 amr_done() 函数中调用,而后者在 amr_intr() 函数中调用。amr_intr () 函数是一个中断处理函数,它是 amr_attach() 入口点中的 ddi_add_intr(9F) 函数的参数。
本节介绍两种可用来监视任务队列所使用的系统资源的方法。任务队列会导出任务队列线程使用系统时间的相关统计信息。任务队列还会使用 DTrace SDT 探测器来确定任务队列何时开始执行某个任务,以及何时完成执行。
每个任务队列都有一组关联的 kstat 计数器。检查以下 kstat(1M) 命令的输出:
$ kstat -c taskq module: unix instance: 0 name: ata_nexus_enum_tq class: taskq crtime 53.877907833 executed 0 maxtasks 0 nactive 1 nalloc 0 priority 60 snaptime 258059.249256749 tasks 0 threads 1 totaltime 0 module: unix instance: 0 name: callout_taskq class: taskq crtime 0 executed 13956358 maxtasks 4 nactive 4 nalloc 0 priority 99 snaptime 258059.24981709 tasks 13956358 threads 2 totaltime 120247890619 |
以上所示的 kstat 输出包含以下信息:
任务队列的名称及其实例编号
已调度任务的数目 (tasks) 以及已执行任务的数目 (executed)
处理任务队列的内核线程数 ( threads) 及其优先级 (priority)
处理所有任务花费的总时间(以纳秒为单位)(totaltime)
以下示例说明如何使用 kstat 命令来观察计数器(已调度任务的数目)是如何随时间而递增的:
$ kstat -p unix:0:callout_taskq:tasks 1 5 unix:0:callout_taskq:tasks 13994642 unix:0:callout_taskq:tasks 13994711 unix:0:callout_taskq:tasks 13994784 unix:0:callout_taskq:tasks 13994855 unix:0:callout_taskq:tasks 13994926 |
任务队列提供了若干个有用的 SDT 探测器。本节介绍的所有探测器都具有以下两个参数:
ddi_taskq_create() 返回的任务队列指针
指向 taskq_ent_t 结构的指针。在 D 脚本中使用该指针可以提取函数和参数。
可以使用这些探测器来收集有关各个任务队列以及通过这些队列执行的各个任务的精确计时信息。例如,以下脚本每隔 10 秒列显通过任务队列调度的函数:
# !/usr/sbin/dtrace -qs sdt:genunix::taskq-enqueue { this->tq = (taskq_t *)arg0; this->tqe = (taskq_ent_t *) arg1; @[this->tq->tq_name, this->tq->tq_instance, this->tqe->tqent_func] = count(); } tick-10s { printa ("%s(%d): %a called %@d times\n", @); trunc(@); } |
在特定的计算机上,以上 D 脚本生成以下输出:
callout_taskq(1): genunix`callout_execute called 51 times callout_taskq(0): genunix`callout_execute called 701 times kmem_taskq(0): genunix`kmem_update_timeout called 1 times kmem_taskq(0): genunix`kmem_hash_rescale called 4 times callout_taskq(1): genunix`callout_execute called 40 times USB_hid_81_pipehndl_tq_1(14): usba`hcdi_cb_thread called 256 times callout_taskq(0): genunix`callout_execute called 702 times kmem_taskq(0): genunix`kmem_update_timeout called 1 times kmem_taskq(0): genunix`kmem_hash_rescale called 4 times callout_taskq(1): genunix`callout_execute called 28 times USB_hid_81_pipehndl_tq_1(14): usba`hcdi_cb_thread called 228 times callout_taskq(0): genunix`callout_execute called 706 times callout_taskq(1): genunix`callout_execute called 24 times USB_hid_81_pipehndl_tq_1(14): usba`hcdi_cb_thread called 141 times callout_taskq(0): genunix`callout_execute called 708 times |