本章介绍 Solaris 多线程内核的锁定原语和线程同步机制。设计设备驱动程序时应当充分利用多线程的特性。本章介绍有关以下主题的信息:
在传统 UNIX 系统中,每一部分内核代码都采用两种方法终止:通过显式调用 sleep(1) 放弃对处理器的控制权或通过硬件中断。Solaris OS 的运行方式与此不同。系统可随时抢占内核线程以运行其他线程。由于所有内核线程共享内核地址空间,并且通常需要读取和修改相同的数据,因此内核提供了大量的锁定原语以防止线程损坏共享数据。这些机制包括互斥锁(又称为 mutex)、读取器/写入器锁以及信号量。
数据的存储类用于指示驱动程序是否需要采取显式步骤控制对数据的访问。共有三个数据存储类:
自动(栈)数据。每个线程有一个专用栈,因此驱动程序永远无需锁定自动变量。
全局静态数据。全局静态数据可由驱动程序中任意数量的线程共享。驱动程序有时可能需要锁定此类型的数据。
内核堆数据。驱动程序中任意数量的线程均可共享内核堆数据,如 kmem_alloc(9F) 分配的数据。驱动程序需要随时保护共享数据。
互斥锁 (mutex) 通常与一组数据关联,并控制对这些数据的访问。互斥锁提供了一种一次仅允许一个线程访问这些数据的方法。互斥锁函数包括:
释放任何关联的存储空间。
获取互斥锁。
释放互斥锁。
初始化互斥锁。
测试以确定当前线程是否持有互斥锁。仅限于在 ASSERT(9F) 中使用。
获取互斥锁(如果可用),但不阻塞。
设备驱动程序通常会为每个驱动程序数据结构分配一个互斥锁。互斥锁通常是该结构中类型为 kmutex_t 的字段。调用 mutex_init(9F) 以初始化要使用的互斥锁。通常在执行 attach(9E) 时为每个设备互斥锁进行此调用,在执行 _init(9E) 时为全局驱动程序互斥锁进行此调用。
例如,
struct xxstate *xsp; /* ... */ mutex_init(&xsp->mu, NULL, MUTEX_DRIVER, NULL); /* ... */
有关互斥锁初始化的较完整示例,请参见第 6 章。
驱动程序在卸载之前必须使用 mutex_destroy(9F) 销毁互斥锁。通常在执行 detach(9E) 时为每个设备互斥锁进行销毁操作,在执行 _fini(9E) 时为全局驱动程序互斥锁进行销毁操作。
驱动程序如果需要读写共享数据结构,必须执行以下操作:
获取互斥锁
访问数据
释放互斥锁
互斥锁的作用域(即互斥锁保护的数据)完全由程序员决定。仅当访问数据结构的每个代码路径都保护数据结构并且同时持有互斥锁时,互斥锁才会保护该数据结构。
读取器/写入器锁可控制对数据集的访问。读取器/写入器锁之所以这样命名,是由于许多线程可同时持有读锁,但仅有一个线程可持有写锁。
大多数设备驱动程序不使用读取器/写入器锁。这些锁的速度比互斥锁要慢。这些锁仅当保护通常进行读取但不经常写入的数据时才会提高性能。在此情况下,对互斥锁的争用可能会成为一个瓶颈,因此使用读取器/写入器锁效率可能更高。下表概述了读取器/写入器函数。有关详细信息,请参见 rwlock(9F) 手册页。读取器/写入器锁函数包括:
销毁读取器/写入器锁
将读取器/写入器锁的持有者从写入器降级为读取器
获取读取器/写入器锁
释放读取器/写入器锁
初始化读取器/写入器锁
确定是否持有用于读/写操作的读取器/写入器锁
尝试在无需等待的情况下获取读取器/写入器锁
尝试将读取器/写入器锁的持有者从读取器升级为写入器
计数信号量可用作管理设备驱动程序中线程的替代原语。有关更多信息,请参见 semaphore(9F) 手册页。信号函数包括:
销毁信号。
初始化信号。
减小信号,可能会阻塞。
减小信号,但不会阻塞(如果信号处于待处理状态)。请参见线程无法接收信号。
尝试减小信号,但不阻塞。
增加信号,可能会解除阻塞等待者。
除了保护共享数据外,驱动程序通常需要在多个线程之间同步执行。
条件变量是线程同步的标准形式。这些变量专门用于互斥锁。关联互斥锁可以确保条件的检查是原子操作,并且线程可以基于关联的条件变量阻塞,同时不会忽略对条件的更改或条件已更改的信号。
condvar(9F) 函数包括:
向基于条件变量等待的所有线程发出信号。
销毁条件变量。
初始化条件变量。
向基于条件变量等待的一个线程发出信号。
等待条件、超时或信号。请参见线程无法接收信号。
等待条件或超时。
等待条件。
等待条件或在收到信号时返回零。请参见线程无法接收信号。
针对每个条件声明 kcondvar_t 类型的条件变量。通常,条件变量是驱动程序定义的数据结构中的一个变量。使用 cv_init(9F) 可初始化每个条件变量。与互斥锁类似,条件变量通常在执行 attach(9E) 时初始化。以下是一个初始化条件变量的典型示例:
cv_init(&xsp->cv, NULL, CV_DRIVER, NULL);
有关条件变量初始化的较完整示例,请参见第 6 章。
要使用条件变量,请在等待条件的代码路径中执行以下步骤:
获取用于保护条件的互斥锁。
测试条件。
如果测试结果表明不允许线程继续执行,请使用 cv_wait(9F) 根据条件阻塞当前线程。cv_wait(9F) 函数将在阻塞线程之前释放互斥锁,并在返回之前重新获取互斥锁。从 cv_wait(9F) 返回时,重复该测试。
测试表明允许线程继续执行后,请将条件设置为其新值。例如,将设备标志设置为繁忙。
释放互斥锁。
请在代码路径中执行以下步骤以发出条件信号:
获取用于保护条件的互斥锁。
设置条件。
使用 cv_broadcast(9F) 向阻塞的线程发出信号。
释放互斥锁。
以下示例使用繁忙标志以及互斥锁和条件变量来强制 read(9E) 例程进行等待,直到设备不再繁忙时为止,然后开始传送。
static int xxread(dev_t dev, struct uio *uiop, cred_t *credp) { struct xxstate *xsp; /* ... */ mutex_enter(&xsp->mu); while (xsp->busy) cv_wait(&xsp->cv, &xsp->mu); xsp->busy = 1; mutex_exit(&xsp->mu); /* perform the data access */ } static uint_t xxintr(caddr_t arg) { struct xxstate *xsp = (struct xxstate *)arg; mutex_enter(&xsp->mu); xsp->busy = 0; cv_broadcast(&xsp->cv); mutex_exit(&xsp->mu); }
如果使用 cv_wait(9F) 根据某个条件将线程阻塞,但该条件不发生,则该线程将永远等待。要避免这种情况,请使用 cv_timedwait(9F),它取决于执行唤醒的其他线程。cv_timedwait() 采取绝对等待时间作为参数。如果时间已到但未发生事件,则 cv_timedwait() 将返回 -1。如果满足条件,则 cv_timedwait() 将返回一个正值。
cv_timedwait(9F) 要求自上次重新引导系统以来的绝对等待时间(以时钟周期表示)。通过使用 ddi_get_lbolt(9F) 检索当前值可确定该等待时间。驱动程序通常具有的是最大等待秒数或微秒数,因此需要使用 drv_usectohz(9F) 将该值转换为时钟周期,然后与 ddi_get_lbolt(9F) 的值相加。
以下示例说明如何使用 cv_timedwait(9F) 最多等待五秒钟便访问设备,然后向调用方返回 EIO。
clock_t cur_ticks, to; mutex_enter(&xsp->mu); while (xsp->busy) { cur_ticks = ddi_get_lbolt(); to = cur_ticks + drv_usectohz(5000000); /* 5 seconds from now */ if (cv_timedwait(&xsp->cv, &xsp->mu, to) == -1) { /* * The timeout time 'to' was reached without the * condition being signaled. */ /* tidy up and exit */ mutex_exit(&xsp->mu); return (EIO); } } xsp->busy = 1; mutex_exit(&xsp->mu);
虽然设备驱动程序写入器通常首选使用 cv_timedwait(9F) 而不是 cv_wait(9F),但是有时选用 cv_wait(9F) 会更好。例如,如果驱动程序基于以下条件等待,则使用 cv_wait(9F) 更合适:
内部驱动程序状态发生变化,在此情况下状态变化可能要求执行一些命令或设置要经过的时间
驱动程序的某些部分必须单线程执行
已在管理可能超时的情况,如 "A" 取决于 "B",同时 "B" 使用 cv_timedwait(9F)
驱动程序可能正在等待不会产生或长时间不会发生的条件。在此类情况下,用户可发送信号中止该线程。根据驱动程序设计,信号可能无法将驱动程序唤醒。
cv_wait_sig(9F) 允许使用信号解除阻塞线程。借助此功能,用户可以通过使用kill(1) 向线程发送信号或键入中断字符,从而免于可能的长时间等待。如果 cv_wait_sig(9F) 由于收到信号而返回,则会返回零;如果条件发生,则返回非零值。但是,对于可能未收到信号的情况,请参见线程无法接收信号。
以下示例说明如何使用 cv_wait_sig(9F) 以允许使用信号解除阻塞线程。
mutex_enter(&xsp->mu); while (xsp->busy) { if (cv_wait_sig(&xsp->cv, &xsp->mu) == 0) { /* Signaled while waiting for the condition */ /* tidy up and exit */ mutex_exit(&xsp->mu); return (EINTR); } } xsp->busy = 1; mutex_exit(&xsp->mu);
cv_timedwait_sig(9F) 与 cv_timedwait(9F) 和 cv_wait_sig(9F) 相似,不同之处在于,达到超时后,如果没有发出条件信号,则 cv_timedwait_sig() 将返回 -1,如果向线程发送了信号(例如 kill(2)),则将返回 0。
对于 cv_timedwait(9F) 和 cv_timedwait_sig(9F),系统将使用上次重新引导系统以来的绝对时钟周期度量时间。
在设计大多数设备驱动程序时,都应该确保锁定方案简单易懂。使用额外的锁允许更多并发,但会增加开销。使用的锁越少,占用的时间越短,但允许的并发会更少。通常,对每个数据结构使用一个互斥锁,对驱动程序必须等待的每个事件或条件使用一个条件变量,对驱动程序的每个主要全局数据集使用一个互斥锁。请避免长时间持有互斥锁。选择锁定方案时,请遵循以下指导原则:
优先使用程序入口点的多线程语义。
确保所有程序入口点可重入。可以通过将静态变量更改为自动变量来减少共享数据量。
如果驱动程序获取了多个互斥锁,请在所有代码路径中按相同顺序获取和释放这些互斥锁。
在相同的功能空间中持有锁和释放锁。
调用可阻塞的 DDI 接口时请避免持有驱动程序互斥锁,例如使用 KM_SLEEP 调用 kmem_alloc(9F)。
要查看锁的用法,请使用 lockstat(1M)。lockstat(1M) 可监视所有内核锁定事件、收集有关事件的频率和计时数据,并显示这些数据。
有关多线程操作的更多详细信息,请参见《多线程编程指南》。
同一线程不可重复获取互斥锁。如果已持有互斥锁,则再次尝试声明此互斥锁会导致产生以下故障消息:
panic: recursive mutex_enter. mutex %x caller %x
释放当前线程未持有的互斥锁会产生以下故障消息:
panic: mutex_adaptive_exit: mutex not held by thread
以下故障消息仅在单处理器上出现:
panic: lock_set: lock held and only one CPU
lock_set 故障消息指明线程持有自旋互斥锁 (spin mutex),并且该锁将会永久旋转,因为没有其他 CPU 可以释放此互斥锁。如果驱动程序忘记释放某个代码路径上的互斥锁,或在持有互斥锁时阻塞,则会发生此情况。
具有高级中断的设备调用的例程阻塞(如 cv_wait(9F))时通常会导致出现 lock_set 故障消息。另一个常见原因是高级处理程序通过调用 mutex_enter(9F) 获取自适应互斥锁。
线程收到信号时,可以唤醒 sema_p_sig()、cv_wait_sig() 和 cv_timedwait_sig() 函数。由于某些线程无法接收信号,因此可能会出现问题。例如,如果由于应用程序调用 close(2) 而导致调用 close(9E),则可以收到信号。但是,如果是从 exit(2) 处理(关闭所有打开的文件描述符)中调用 close(9E),则线程无法收到信号。如果线程无法收到信号,则 sema_p_sig() 的行为与 sema_p() 相同,cv_wait_sig() 的行为与 cv_wait() 相同,cv_timedwait_sig() 的行为与 cv_timedwait() 相同。
对于可能永远不会发生的事件,请注意避免永久休眠。永远不会发生的事件会创建不可中止 (defunct) 的线程并使设备不可用,除非重新引导系统。失效进程无法接收信号。
要检测当前线程是否可接收信号,请使用 ddi_can_receive_sig(9F) 函数。如果 ddi_can_receive_sig() 函数返回 B_TRUE,则以上函数可在收到信号时唤醒。如果 ddi_can_receive_sig() 函数返回 B_FALSE,则以上函数无法在收到信号时唤醒。如果 ddi_can_receive_sig() 函数返回 B_FALSE,则驱动程序会使用替代方法(如 timeout(9F) 函数)重新唤醒。
出现此问题的一个重要情况是使用串行端口。如果远程系统声明了流量控制,并且 close(9E) 函数在尝试清空输出数据时阻塞,则端口会堵塞,直到解决流量控制情况或重新引导系统为止。此类驱动程序应检测到此情况并设置计时器,以便在流量控制情况持续过长时间时中止清空操作。
此问题还会影响 qwait_sig(9F) 函数。此函数将在《STREAMS Programming Guide》中的第 7 章 “STREAMS Framework – Kernel Level”中介绍。