第3章内核组件本章将对一些驱动开发相关的内核组件进行讲解。我们首先以内核线程开始,它类似于用户空间的进程,通常用于并发处理。另外,内核还提供了一些接口,使用它们可以简化代码、消除冗余、增强代码可读性并有利于代码的长期维护。本章会学习链表、哈希链表、工作队列、通知链(notifierchain)、完成以及错误处理辅助接口等。这些辅助接口经过了优化,而且清除了bug,因此你的驱动可以继承这些优点。内核线程内核线程是一种在内核空间实现后台任务的方式。该任务可以是繁忙地处理异步事务,也可以睡眠等待某事件的发生。内核线程与用户进程相似,唯一的不同是内核线程位于内核空间可以访问内核函数和数据结构。和用户进程相似,由于可抢占调度的存在,内核现在看起来也在独占CPU。很多设备驱动都使用了内核线程以完成辅助任务。例如,USB设备驱动核心的khubd内核线程的作用就是监控USB集线器,并在USB被热插拔的时候配置USB设备。创建内核线程让我们用一个例子老学习内核线程的知识。当我们在开发这个例子线程的时候,你也会学习到进程状态、等待队列的概念,并接触到用户模式辅助函数。当你熟悉内核线程以后,你可以使用它作为在内核中进行各种各样实验的媒介。假定我们的线程要完成这样的工作:一旦它检测到某一关键的内核数据结构的健康状态极度恶化(譬如,网络接受缓冲区的空闲内存低于警戒水位),就激活一个用户模式程序给你发送一封email或发出一个呼机警告。该任务比较适合用内核线程来实现,原因如下:(1)它是一个等待异步事件的后台任务;(2)由于实际的事件侦测由内核的其他部分完成,本任务也需要访问内核数据结构;(3)它必须激活一个用户模式的辅助程序,这比较耗费时间。内建的内核线程使用ps命令可以查看系统中正在运行的内核线程(也称为内核进程)。内核线程的名字被一个方括号括起来了:bashps-efUIDPIDPPIDCSTIMETTYTIMECMDroot10022:36?00:00:00init[3]root20022:36?00:00:00[kthreadd]root32022:36?00:00:00[ksoftirqd/0]root42022:36?00:00:00[events/0]root382022:36?00:00:00[pdflush]root392022:36?00:00:00[pdflush]root292022:36?00:00:00[khubd]root6952022:36?00:00:00[kjournald]...root39142022:37?00:00:00[nfsd]root39152022:37?00:00:00[nfsd]...root40153364022:55tty300:00:00-bashroot40664015022:59tty300:00:00ps-ef[ksoftirqd/0]内核线程是实现软中断的助手。软中断是由中断发起的可以被延后执行的底半部。在第4章《打下基础》将对底半部和软中断进行详细的分析,这里的基本理论是让中断处理程序中的代码越少越好。中断处理时间越小,系统屏蔽中断的时间会越短,这会降低时延。Ksoftirqd的工作是确保高负荷情况下,软中断既不会饥饿,又不至于压垮系统。在对称多处理器(SMP)及其上,多个线程实例可以并行地运行在不同的处理器上,为了提高吞吐率,系统为每个CPU都创建了一个ksoftirqd线程(ksoftirqd/n,其中n代表了CPU序号)。events/n(其中n代表了CPU序号)实现了工作队列,它是另一种在内核中延后执行的手段。内核中期待延迟执行工作的程序可以创建自己的工作队列,或者使用缺省的events/n工作者线程。第4章也对工作队列进行了深入分析。pdflush内核线程的任务是对页高速缓冲中的脏页进行写回(flushout)。页高速缓冲会对磁盘数据进行缓存,为了提供性能,实际的磁盘写操作会一直延迟到pdflush后台程序将脏数据写回磁盘才进行。当系统中可用的空闲内存低于门限,或者页变成脏页后一段时间。在2.4内核中,这2个任务分配被bdflush和kupdated这2个单独的线程完成。你可能会注意到ps的输出中有2个pdflush的实例,如果内核感觉到现存的实例已经在满负荷运转,它会创建1个新的实例以服务磁盘队列。当你的系统有多个磁盘,而且要频繁访问它们的时候,这种方式会提高吞吐率。在以前的章节中我们已经看到,kjournald是通用内核日志线程,它被EXT3等文件系统使用。Linux网络文件系统(NFS)通过一套名为nfsd的内核线程实现。在被内核中负责监控我们感兴趣的数据结构的任务唤醒之前,我们的例子线程一直会放弃CPU。在被唤醒后,它激活一个用户模式辅助程序并将恰当的身份代码传递给它。使用kernel_thread()可以创建内核线程:ret=kernel_thread(mykthread,NULL,CLONE_FS|CLONE_FILES|CLONE_SIGHAND|SIGCHLD);标记参数定义了父子之间要共享的资源。CLONE_FILES意味着打开的文件要被贡献,CLONE_SIGHAND意味着信号处理被共享。清单3.1显示了例子的实现。由于内核线程通常是设备驱动的助手,它们往往在驱动初始化的时候被创建。但是,本例的内核线程可以在任意合适的位置被创建,例如init/main.c。这个线程开始的时候调用daemonize(),它会执行初始的家务工作,之后将本线程的父亲线程改为kthreadd。每个Linux线程有一个父亲。如果某个父进程在没有等待其所有子进程都退出的时候就死掉了,它的所有子进程都会成为僵死进程(zombieprocess),仍然消耗资源。将父亲重新定义为kthreadd可以避免这种情况,并且确保线程退出的时候能进行恰当的清理工作[1]。[1]在2.6.21及更早的内核中,daemonize()会通过调用reparent_to_init()将本线程的父亲置为init任务。由于daemonize()在默认情况下会阻止所有的信号,因此,你的线程如果想处理某个信号,应该调用allow_signal()来使能它。在内核中没有信号处理函数,因此我们使用signal_pending()来检查信号的存在并采取适当的行动。出于调试目的,清单3.1中的代码使能了SIGKILL的传递,在收到该信号后,本线程会寿终正寝。面对更高层次的kthreadAPI(其目的在于超越kernel_thread()),kernel_thread()的地位下降了。以后我们会分析kthreads。清单3.1实现一个内核线程staticDECLARE_WAIT_QUEUE_HEAD(myevent_waitqueue);rwlock_tmyevent_lock;externunsignedintmyevent_id;/*Holdstheidentityofthetroubleddatastructure.Populatedlateron*/staticintmykthread(void*unused){unsignedintevent_id=0;DECLARE_WAITQUEUE(wait,current);/*Becomeakernelthreadwithoutattacheduserresources*/daemonize(mykthread);/*RequestdeliveryofSIGKILL*/allow_signal(SIGKILL);/*Thethreadsleepsonthiswaitqueueuntilit'swokenupbypartsofthekernelinchargeofsensingthehealthofdatastructuresofinterest*/add_wait_queue(&myevent_waitqueue,&wait);for(;;){/*Relinquishtheprocessoruntiltheeventoccurs*/set_current_state(TASK_INTERRUPTIBLE);schedule();/*Allowotherpartsofthekerneltorun*//*DieifIreceiveSIGKILL*/if(signal_pending(current))break;/*Controlgetsherewhenthethreadiswokenup*/read_lock(&myevent_lock);/*Criticalsectionstarts*/if(myevent_id){/*Guardagainstspuriouswakeups*/event_id=myevent_id;read_unlock(&myevent_lock);/*Criticalsectionends*//*Invoketheregisteredusermodehelperandpasstheidentitycodeinitsenvironment*/run_umode_handler(event_id);/*Expandedlateron*/}else{read_unlock(&myevent_lock);}}set_current_state(TASK_RUNNING);remove_wait_queue(&myevent_waitqueue,&wait);return0;}将其编译入内核并运行,在ps命令的输出中,你将看到这个线程mykthread:bashps-efUIDPIDPPIDCSTIMETTYTIMECMDroot10021:56?00:00:00init[3]root21022:36?00:00:00[ksoftirqd/0]...root1111021:56?00:00:00[mykthread]...在我们深入探究线程的实现之前,先写一段代码,让它监控我们感兴趣的数据结构的“健康状态”,一旦发生问题就唤醒mykthread:/*Executedbypartsofthekernelthatownthedatastructureswhosehealthyouwanttomonitor*//*...*/if(my_key_datastructurelookstroubled){write_lock(&myevent_lock);/*Serialize*//*Fillintheidentityofthedatastructure*/myevent_id=datastructure_id;write_unlock(&myevent_lock);/*Wakeupmykthread*/wake_up_interruptible(&myevent_waitqueue);}/*...*/清单3.1运行在进程上下文,而上面的代码即可以运行于进程上下文,又可以运行于中断上下文。进程和中断上下文通过内核数据结构通信。在我们的例子中用于通信的是myevent_id和myevent_waitqueue。myevent_id包含了有问题的数据结构的身份信息,对它的访问通过加锁进行了串行处理。要注意的是只有在编译时配置了CONFIG_PREEMPT的情况下,内核线程才是可抢占的。如果CONFIG_PREEMPT被关闭,如果你运行在没有抢占补丁的2.4内核上,如果你的线程不进入睡眠状态,它将使系统冻结。如果你注释掉清单3.1中的schedule(),并且在内核配置时关闭了CONFIG_PREEMPT选项,你的系统将被锁住。在第19章《用户空间的设备驱动》讨论调度策略时,你将学会怎样从内核线程获得软实时响应。进程状态和等待队列下面的语句是清单3.1中将mykthread置于睡眠状态并等待时间的代码片段:add_wait_queue(&myevent_waitqueue,&wait);for(;;){/*...*/set_current_state(T