编写设备驱动程序

第 5 章 管理事件和排队任务

驱动程序使用事件来响应状态更改。本章提供以下有关事件的信息:

驱动程序使用任务队列来管理任务之间的资源相关性。本章提供有关任务队列的以下信息:

管理事件

系统经常需要对用户操作或系统请求之类的条件更改做出响应。例如,设备可能会在某个组件开始过热时发出警告,或者可能在将 DVD 插入驱动器后启动影片播放机。设备驱动程序可以使用称为事件的特殊消息来通知系统发生了状态更改。

事件介绍

事件是指设备驱动程序向相关实体发送的消息,用以指示发生了状态更改。在 Solaris OS 中,事件以用户定义的名称-值对结构的形式实现,这些结构使用 nvlist* 函数进行管理。(请参见 nvlist_alloc(9F) 手册页。)事件由供应商、类以及子类组成。例如,可以定义一个类用于监视环境条件。环境类可以具有子类,用来指示温度、风扇状态以及电源方面的变化。

发生状态更改时,设备将通知驱动程序。驱动程序随后将使用 ddi_log_sysevent(9F) 函数在称为 sysevent 的队列中记录此事件。sysevent 队列会将事件传递到用户级,以便通过 syseventd 守护进程或 syseventconfd 守护进程进行处理。这些守护进程会将通知发送到订阅了指定事件通知的所有应用程序。

用户级应用程序的设计者可以使用以下两种方法处理事件:

下图对此流程进行了说明。

图 5–1 事件检测

图中显示如何将事件记录到 sysevent 队列中,以通知用户级应用程序。

使用 ddi_log_sysevent() 记录事件

设备驱动程序使用 ddi_log_sysevent(9F) 接口生成和记录系统事件。

ddi_log_sysevent() 语法

ddi_log_sysevent() 使用以下语法:

int ddi_log_sysevent(dev_info_t *dip, char *vendor, char *class, 
    char *subclass, nvlist_t *attr-list, sysevent_id_t *eidp, int sleep-flag);

其中:

dip

指向相应驱动程序处理的 dev_info 节点的指针。

vendor

指向定义驱动程序供应商的字符串的指针。第三方驱动程序应使用其公司的股票代号或类似的持久标识符。Sun 提供的驱动程序会使用 DDI_VENDOR_SUNW

class

指向定义事件类的字符串的指针。class 是特定于驱动程序的值。表示影响设备的一组环境条件的字符串可能即是一个类的示例。事件使用方必须能够理解该值。

subclass

表示 class 参数子集的特定于驱动程序的字符串。例如,在表示环境条件的类中,事件子类可能是指设备的温度。事件使用方必须能够理解该值。

attr-list

指向列出与事件关联的名称-值特性的 nvlist_t 结构的指针。名称-值特性是驱动程序定义的,可以是指设备的特定特性或条件。

例如,可同时读取 CD-ROM 和 DVD 的设备。此设备可能具有一个名称为 disc_type 并且值等于 cd_romdvd 的特性。

classsubclass 一样,事件使用方必须能够解释名称-值对。

有关名称-值对以及 nvlist_t 结构的更多信息,请参见定义事件特性以及 nvlist_alloc(9F) 手册页。

如果事件没有任何特性,则此参数应设置为 NULL

eidp

sysevent_id_t 结构的地址。sysevent_id_t 结构用于提供事件的唯一标识。ddi_log_sysevent (9F) 将向此结构返回系统提供的事件序列号和时间戳。有关 sysevent_id_t 结构的更多信息,请参见 ddi_log_sysevent(9F) 手册页。

sleep-flag

指示调用者如何处理不可用资源可能性的标志。如果 sleep-flag 设置为 DDI_SLEEP,则驱动程序会阻塞,直到资源可用为止。如果设置为 DDI_NOSLEEP,则分配不会休眠且不能保证成功。如果返回了 DDI_ENOMEM,则驱动程序以后需要重试该操作。

即使设置为 DDI_SLEEP,此界面也可能返回错误(如系统繁忙),syseventd 守护进程不响应或不尝试在中断上下文中记录事件。

记录事件的样例代码

设备驱动程序可执行以下任务来记录事件:

以下示例说明如何使用 ddi_log_sysevent()


示例 5–1 调用 ddi_log_sysevent()

char *vendor_name = "DDI_VENDOR_JGJG"
char *my_class = "JGJG_event";
char *my_subclass = "JGJG_alert";
nvlist_t *nvl;
/* ... */
nvlist_alloc(&nvl, nvflag, kmflag);
/* ... */
(void) nvlist_add_byte_array(nvl, propname, (uchar_t *)propval, proplen + 1); 
/* ... */
if (ddi_log_sysevent(dip, vendor_name, my_class, 
    my_subclass, nvl, NULL, DDI_SLEEP)!= DDI_SUCCESS)
    cmn_err(CE_WARN, "error logging system event"); 
nvlist_free(nvl);

定义事件特性

事件特性定义为名称-值对列表。Solaris DDI 提供了用于在名称-值对中存储信息的例程和结构。名称-值对保留在 nvlist_t 结构中,此结构对于驱动程序是不透明的。名称-值对的值可以是布尔值、int、字节、字符串、nvlist 或这些数据类型的数组。int 可以定义为 16 位、32 位或 64 位,可以带符号,也可不带符号。

下面是创建名称-值对列表的步骤。

  1. 使用 nvlist_alloc(9F) 创建 nvlist_t 结构。

    nvlist_alloc() 接口会采用以下三个参数:

    • nvlp-指向 nvlist_t 结构指针的指针

    • nvflag-指示名称-值对的名称唯一性的标志。如果此标志设置为 NV_UNIQUE_NAME_TYPE,则会从列表中删除与新对的名称和类型相匹配的任何现有对。如果标志设置为 NV_UNIQUE_NAME,则会删除任何同名的现有对,而不考虑对的类型。只要对的类型不同,通过指定 NV_UNIQUE_NAME_TYPE,列表即可包含两个或多个同名的对,但如果指定 NV_UNIQUE_NAME,则列表中只能有一个对名称实例。如果未设置标志,则不会执行任何唯一性检查,将由列表的使用方负责处理同名的对。

    • kmflag-指示内核内存分配策略的标志。如果此参数设置为 KM_SLEEP,则驱动程序会阻塞,直到请求的内存可进行分配为止。KM_SLEEP 分配可能会休眠,但是保证会成功。KM_NOSLEEP 分配保证不会休眠,但是可能会在当前无可用内存时返回 NULL

  2. 使用名称-值对填充 nvlist。例如,要添加字符串,请使用 nvlist_add_string(9F)。要添加 32 位整数数组,请使用 nvlist_add_int32_array(9F)nvlist_add_boolean(9F) 手册页包含用于添加对的可用接口的完整列表。

要取消分配列表,请使用 nvlist_free(9F)

以下代码样例说明如何创建名称-值对列表。


示例 5–2 创建和填充名称-值对列表

nvlist_t*
create_nvlist()
    {
    int err;
    char *str = "child";
    int32_t ints[] = {0, 1, 2};
    nvlist_t *nvl;

    err = nvlist_alloc(&nvl, NV_UNIQUE_NAME, 0);    /* allocate list */
    if (err)
        return (NULL);
    if ((nvlist_add_string(nvl, "name", str) != 0) ||
        (nvlist_add_int32_array(nvl, "prop", ints, 3) != 0)) {
        nvlist_free(nvl);
        return (NULL);
    }
    return (nvl);
}

驱动程序可通过相应类型的查找函数(如 nvlist_lookup_int32_array(9F))来检索 nvlist 中的元素,此类查找函数将要搜索的名称-值对的名称作为参数。


注 –

仅当在调用 nvlist_alloc(9F) 时指定了 NV_UNIQUE_NAMENV_UNIQUE_NAME_TYPE 的情况下,这些接口才会正常工作。否则,将返回 ENOTSUP,因为此列表不能包含多个同名的对。


可以将名称-值列表中的各对放在连续内存中。此方法有助于将列表传递给已订阅了通知的实体。第一步是使用 nvlist_size(9F) 获取列表所需的内存块的大小。第二步是使用 nvlist_pack(9F) 将列表压缩到缓冲区中。收到缓冲区内容的使用方可使用 nvlist_unpack(9F) 解压缩缓冲区。

用户级开发者和内核级开发者均可使用用于处理名称-值对的函数。可以在《man pages section 3: Library Interfaces and Headers》《man pages section 9: DDI and DKI Kernel Functions》中找到这些函数的相同手册页。有关针对名称-值对执行操作的函数的列表,请参见下表。

表 5–1 使用名称-值对的函数

手册页 

用途/函数 

nvlist_add_boolean(9F)

向列表中添加名称-值对。函数包括: 

nvlist_add_boolean()nvlist_add_boolean_value ()nvlist_add_byte()nvlist_add_int8() nvlist_add_uint8()nvlist_add_int16()nvlist_add_uint16 ()nvlist_add_int32()nvlist_add_uint32() nvlist_add_int64()nvlist_add_uint64()nvlist_add_string ()nvlist_add_nvlist()nvlist_add_nvpair() nvlist_add_boolean_array()nvlist_add_int8_array、nvlist_add_uint8_array ()nvlist_add_nvlist_array()nvlist_add_byte_array ()nvlist_add_int16_array()nvlist_add_uint16_array ()nvlist_add_int32_array()nvlist_add_uint32_array ()nvlist_add_int64_array()nvlist_add_uint64_array ()nvlist_add_string_array()

nvlist_alloc(9F)

处理名称-值列表缓冲区。函数包括: 

nvlist_alloc()nvlist_free() nvlist_size()nvlist_pack()nvlist_unpack ()nvlist_dup()nvlist_merge()

nvlist_lookup_boolean(9F)

搜索名称-值对。函数包括: 

nvlist_lookup_boolean()nvlist_lookup_boolean_value ()nvlist_lookup_byte()nvlist_lookup_int8 ()nvlist_lookup_int16()nvlist_lookup_int32 ()nvlist_lookup_int64()nvlist_lookup_uint8 ()nvlist_lookup_uint16()nvlist_lookup_uint32 ()nvlist_lookup_uint64()nvlist_lookup_string ()nvlist_lookup_nvlist()nvlist_lookup_boolean_array、nvlist_lookup_byte_array()nvlist_lookup_int8_array() nvlist_lookup_int16_array()nvlist_lookup_int32_array() nvlist_lookup_int64_array()nvlist_lookup_uint8_array() nvlist_lookup_uint16_array()nvlist_lookup_uint32_array() nvlist_lookup_uint64_array()nvlist_lookup_string_array() nvlist_lookup_nvlist_array()nvlist_lookup_pairs()

nvlist_next_nvpair(9F)

获取名称-值对数据。函数包括: 

nvlist_next_nvpair()nvpair_name() nvpair_type()

nvlist_remove(9F)

删除名称-值对。函数包括: 

nv_remove()nv_remove_all()

排队任务

本节讨论如何使用任务队列来延迟处理某些任务并将这些任务的执行委托给另一个内核线程。

任务队列简介

内核编程中的一项常见操作是对某个任务进行调度,使它以后由另一线程执行。以下示例给出了可能需要以后由另一线程执行某个任务的一些原因:

对于上面的每种情况,任务都在不同的上下文中执行。不同的上下文通常是持有一组不同锁的不同内核线程,并可能具有不同的优先级。任务队列提供一个通用内核 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 输出包含以下信息:

以下示例说明如何使用 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

任务队列 DTrace SDT 探测器

任务队列提供了若干个有用的 SDT 探测器。本节介绍的所有探测器都具有以下两个参数:

可以使用这些探测器来收集有关各个任务队列以及通过这些队列执行的各个任务的精确计时信息。例如,以下脚本每隔 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