《Essential Linux Device Drivers》第3章(上)

  1. 云栖社区>
  2. 博客>
  3. 正文

《Essential Linux Device Drivers》第3章(上)

技术小胖子 2017-11-07 00:16:00 浏览945
展开阅读全文


3 内核组件

本章将对一些驱动开发相关的内核组件进行讲解。我们首先以内核线程开始,它类似于用户空间的进程,通常用于并发处理。
另外,内核还提供了一些接口,使用它们可以简化代码、消除冗余、增强代码可读性并有利于代码的长期维护。本章会学习链表、哈希链表、工作队列、通知链(notifier chain)、完成以及错误处理辅助接口等。这些辅助接口经过了优化,而且清除了bug,因此你的驱动可以继承这些优点。
内核线程
内核线程是一种在内核空间实现后台任务的方式。该任务可以是繁忙地处理异步事务,也可以睡眠等待某事件的发生。内核线程与用户进程相似,唯一的不同是内核线程位于内核空间可以访问内核函数和数据结构。和用户进程相似,由于可抢占调度的存在,内核现在看起来也在独占CPU。很多设备驱动都使用了内核线程以完成辅助任务。例如,USB设备驱动核心的khubd内核线程的作用就是监控USB集线器,并在USB被热插拔的时候配置USB设备。
创建内核线程
让我们用一个例子老学习内核线程的知识。当我们在开发这个例子线程的时候,你也会学习到进程状态、等待队列的概念,并接触到用户模式辅助函数。当你熟悉内核线程以后,你可以使用它作为在内核中进行各种各样实验的媒介。
假定我们的线程要完成这样的工作:一旦它检测到某一关键的内核数据结构的健康状态极度恶化(譬如,网络接受缓冲区的空闲内存低于警戒水位),就激活一个用户模式程序给你发送一封email或发出一个呼机警告。
该任务比较适合用内核线程来实现,原因如下:
1)它是一个等待异步事件的后台任务;
2)由于实际的事件侦测由内核的其他部分完成,本任务也需要访问内核数据结构;
3)它必须激活一个用户模式的辅助程序,这比较耗费时间。
内建的内核线程
使用ps命令可以查看系统中正在运行的内核线程(也称为内核进程)。内核线程的名字被一个方括号括起来了:
bash> ps -ef
UID        PID  PPID  C STIME TTY         TIME CMD
root         1     0  0 22:36 ?       00:00:00 init [3]
root         2     0  0 22:36 ?       00:00:00 [kthreadd]
root         3     2  0 22:36 ?       00:00:00 [ksoftirqd/0]
root         4     2  0 22:36 ?       00:00:00 [events/0]
root        38     2  0 22:36 ?       00:00:00 [pdflush]
root        39     2  0 22:36 ?       00:00:00 [pdflush]
root        29     2  0 22:36 ?       00:00:00 [khubd]
root       695     2  0 22:36 ?       00:00:00 [kjournald]
...
root      3914     2  0 22:37 ?       00:00:00 [nfsd]
root      3915     2  0 22:37 ?       00:00:00 [nfsd]
...
root      4015  3364  0 22:55 tty3    00:00:00 -bash
root      4066  4015  0 22:59 tty3    00:00:00 ps -ef
[ksoftirqd/0]内核线程是实现软中断的助手。软中断是由中断发起的可以被延后执行的底半部。在第4章《打下基础》将对底半部和软中断进行详细的分析,这里的基本理论是让中断处理程序中的代码越少越好。中断处理时间越小,系统屏蔽中断的时间会越短,这会降低时延。Ksoftirqd的工作是确保高负荷情况下,软中断既不会饥饿,又不至于压垮系统。在对称多处理器(SMP)及其上,多个线程实例可以并行地运行在不同的处理器上,为了提高吞吐率,系统为每个CPU都创建了一个ksoftirqd线程(ksoftirqd/n,其中n代表了CPU序号)。
events/n(其中n代表了CPU序号)实现了工作队列,它是另一种在内核中延后执行的手段。内核中期待延迟执行工作的程序可以创建自己的工作队列,或者使用缺省的events/n工作者线程。第4章也对工作队列进行了深入分析。
pdflush内核线程的任务是对页高速缓冲中的脏页进行写回(flush out)。页高速缓冲会对磁盘数据进行缓存,为了提供性能,实际的磁盘写操作会一直延迟到pdflush后台程序将脏数据写回磁盘才进行。当系统中可用的空闲内存低于门限,或者页变成脏页后一段时间。在2.4内核中,这2个任务分配被bdflushkupdated2个单独的线程完成。你可能会注意到ps的输出中有2pdflush的实例,如果内核感觉到现存的实例已经在满负荷运转,它会创建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线程有一个父亲。如果某个父进程在没有等待其所有子进程都退出的时候就死掉了,它的所有子进程都会成为僵死进程(zombie process),仍然消耗资源。将父亲重新定义为kthreadd可以避免这种情况,并且确保线程退出的时候能进行恰当的清理工作[1]
 [1]2.6.21及更早的内核中,daemonize()会通过调用reparent_to_init()将本线程的父亲置为init任务。
由于daemonize()在默认情况下会阻止所有的信号,因此,你的线程如果想处理某个信号,应该调用allow_signal()来使能它。在内核中没有信号处理函数,因此我们使用signal_pending()来检查信号的存在并采取适当的行动。出于调试目的,清单3.1中的代码使能了SIGKILL的传递,在收到该信号后,本线程会寿终正寝。
    面对更高层次的kthread API(其目的在于超越kernel_thread()),kernel_thread()的地位下降了。以后我们会分析kthreads
清单3.1 实现一个内核线程
static DECLARE_WAIT_QUEUE_HEAD(myevent_waitqueue);
rwlock_t myevent_lock;
extern unsigned int myevent_id;  /* Holds the identity of the
                                    troubled data structure.
                                    Populated later on */
static int mykthread(void *unused)
{
  unsigned int event_id = 0;
  DECLARE_WAITQUEUE(wait, current);
  /* Become a kernel thread without attached user resources */
  daemonize("mykthread");
 
  /* Request delivery of SIGKILL */
  allow_signal(SIGKILL);
 
  /* The thread sleeps on this wait queue until it's
     woken up by parts of the kernel in charge of sensing
     the health of data structures of interest */
  add_wait_queue(&myevent_waitqueue, &wait);
 
  for (;;) {
    /* Relinquish the processor until the event occurs */
    set_current_state(TASK_INTERRUPTIBLE);
    schedule();  /* Allow other parts of the kernel to run */
    /* Die if I receive SIGKILL */
    if (signal_pending(current)) break;
    /* Control gets here when the thread is woken up */
    read_lock(&myevent_lock);      /* Critical section starts */
    if (myevent_id) { /* Guard against spurious wakeups */
      event_id = myevent_id;
      read_unlock(&myevent_lock); /* Critical section ends */
      /* Invoke the registered user mode helper and
         pass the identity code in its environment */
      run_umode_handler(event_id); /* Expanded later on */
    } else {
      read_unlock(&myevent_lock);
    }
  }
 
  set_current_state(TASK_RUNNING);
  remove_wait_queue(&myevent_waitqueue, &wait);
  return 0;
}
将其编译入内核并运行,在ps命令的输出中,你将看到这个线程mykthread
bash> ps -ef
    UID        PID  PPID C STIME TTY       TIME CMD
    root         1     0 0 21:56 ?     00:00:00 init [3]
    root         2     1 0 22:36 ?     00:00:00 [ksoftirqd/0]
    ...
    root         111   1 0 21:56 ?     00:00:00 [mykthread]
    ...
在我们深入探究线程的实现之前,先写一段代码,让它监控我们感兴趣的数据结构的“健康状态”,一旦发生问题就唤醒mykthread
/* Executed by parts of the kernel that own the
   data structures whose health you want to monitor */
/* ... */
 
if (my_key_datastructure looks troubled) {
  write_lock(&myevent_lock);  /* Serialize */
  /* Fill in the identity of the data structure */
  myevent_id = datastructure_id;
 
  write_unlock(&myevent_lock);
 
  /* Wake up mykthread */
  wake_up_interruptible(&myevent_waitqueue);
}
 
/* ... */
清单3.1运行在进程上下文,而上面的代码即可以运行于进程上下文,又可以运行于中断上下文。进程和中断上下文通过内核数据结构通信。在我们的例子中用于通信的是myevent_idmyevent_waitqueuemyevent_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(TASK_INTERRUPTIBLE);
  schedule();    /* Relinquish the processor */
  /* Point A */
 
  /* ... */
}
set_current_state(TASK_RUNNING);
remove_wait_queue(&myevent_waitqueue, &wait);
 
上面代码片段的操作依靠了2个概念:等待队列和进程状态。
等待队列用于存放需要等待事件和系统资源的线程。在被负责侦测事件的中断服务程序或另一个线程唤醒之前,位于等待队列的线程会处于睡眠。入列和出列的操作分别通过调用add_wait_queue()remove_wait_queue()完成,而唤醒队列中的任务则通过wake_up_interruptible()完成。
一个内核线程(或一个常规的进程)可以处于如下状态中的一种:运行(running)、可被打断的睡眠(interruptible)、不可被打断的睡眠(uninterruptible)、僵死(zombie)、停止(stopped)、追踪(traced)和dead。这些状态的定义位于include/linux/sched.h
1)处于运行状态(TASK_RUNNING)的进程位于调度器的运行队列(run queue)中,等待调度器将CPU时间分给它执行;
2)处于可被打断的睡眠状态(TASK_INTERRUPTIBLE)的进程正在等待一个事件的发生,不在调度器的运行队列之中。当它等待的事件发生后,或者它被信号打断,它将重新进入运行队列;
3)处于不可被打断的睡眠状态(TASK_INTERRUPTIBLE)的进程与处于可被打断的睡眠状态的进程的行为相似,唯一的区别是信号的发生不会导致该进程被重新放入运行队列;
4)处于停止状态(TASK_STOPPED)的进程由于收到了某些信号已经停止执行;
5)如果1个应用程序(如strace)正在使用内核的ptrace支持以拦截一个进程,该进程将处于追踪状态(TASK_TRACED);
6)处于僵死状态(EXIT_ZOMBIE)的进程已经被终止,但是其父进程并未等待它完成。一个退出后的进程要么处于EXIT_ZOMBIE状态,要么处于死亡状态(EXIT_DEAD)。
你可以使用set_current_state()来设置内核线程的运转状态。
现在回到前面的代码片段。mykthread在等待队列myevent_waitqueue上睡眠,并将它的状态修改为TASK_INTERRUPTIBLE,表明了它不想进入调度器运行队列的愿望。对schedule()的调用将导致调度器从运行队列中选择一个新的任务投入运行。当负责健康状况检测的代码通过wake_up_interruptible(&myevent_waitqueue)唤醒mykthread后,该进程将重新回到调度器运行队列。而与此同时,进程的状态也改变为TASK_RUNNING,因此,以便唤醒的动作发生在设置。另外,如果SIGKILL被传递给了该线程,它也会返回运行队列。之后,一旦调度器从运行队列中选择了mykthread线程,它将从Point A开始恢复执行。
用户模式辅助函数
清单3.1中的mykthread会通过调用run_umode_handler()向用户空间通告被侦测到的事件:
/* Called from Listing 3.1 */
static void
run_umode_handler(int event_id)
{
  int i = 0;
  char *argv[2], *envp[4], *buffer = NULL;
  int value;
 
  argv[i++] = myevent_handler; /* Defined in
                                  kernel/sysctl.c */
 
  /* Fill in the id corresponding to the data structure
     in trouble */
  if (!(buffer = kmalloc(32, GFP_KERNEL))) return;
  sprintf(buffer, "TROUBLED_DS=%d", event_id);
 
  /* If no user mode handlers are found, return */
  if (!argv[0]) return; argv[i] = 0;
 
  /* Prepare the environment for /path/to/helper */
  i = 0;
  envp[i++] = "HOME=/";
  envp[i++] = "PATH=/sbin:/usr/sbin:/bin:/usr/bin";
  envp[i++] = buffer; envp[i]   = 0;
 
  /* Execute the user mode program, /path/to/helper */
  value = call_usermodehelper(argv[0], argv, envp, 0);
 
  /* Check return values */
  kfree(buffer);
}
内核支持这种机制:向用户模式的程序发出请求,让其执行某些程序。run_umode_handler()通过调用call_usermodehelper()使用了这种机制。你必须通过/proc/sys/目录中的一个结点来注册run_umode_handler()要激活的用户模式程序。为了完成此项工作,必须确保CONFIG_SYSCTL/proc/sys/目录中的文件全部都被看作sysctl接口)配置选项在内核配置时已经使能,并且在kernel/sysctl.ckern_table数组中添加一个入口:                
{
  .ctl_name     = KERN_MYEVENT_HANDLER, /* Define in
                                           include/linux/sysctl.h */
  .procname     = "myevent_handler",
  .data         = &myevent_handler,
  .maxlen       = 256,
  .mode         = 0644,
  .proc_handler = &proc_dostring,
  .strategy     = &sysctl_string,
},
上述代码会导致proc文件系统中产生新的/proc/sys/kernel/myevent_handler结点。为了注册用户模式辅助程序,运行如下命令:
bash> echo /path/to/helper > /proc/sys/kernel/myevent_handler
mykthread调用run_umode_handler()时,/path/to/helper程序将开始执行。
mykthread通过TROUBLED_DS环境变量将有问题的内核数据结构的身份信息传递用户模式辅助程序。该辅助程序可以是一段简单的脚本,它发送给你一封包含了从环境变量搜集到的信息的邮件警报:
bash> cat /path/to/helper
#!/bin/bash
echo Kernel datastructure $TROUBLED_DS is in trouble | mail -s Alert root
call_usermodehelper()的调用必须发生在进程上下文,而且以root权限运行。它借用下文很快就要讨论的工作队列(work queue)得以实现。
辅助接口
内核中存在一些有用的辅助接口,这些接口可以有效地减轻驱动开发人员的负担。其中的一个例子就是双向链表库的实现。许多设备驱动需要维护和操作链表数据结构,内核的链表接口函数消除了管理链表指针的需要,也使得开发人员无需调试与链表维护相关的繁琐问题。本节我们将学会怎样使用链表(list)、哈希链表(hlist)、工作队列(work queue)、完成函数(completion function)、通知块(notifier block)和kthreads
我们可以等效的方式去完成辅助接口提供的功能。譬如,你可以不使用链表库,而是使用自己实现的链表操作函数,你也可以不使用工作队列而使用内核线程来进行延后的工作。但是,使用标准的内核辅助接口的好处是,它可以简化你的代码、消除冗余、增强代码的可读性,并对长期维护有利。
由于内核非常庞大,你总是能找到没有利用这些辅助机制优点的代码,因此,更新这些代码也许是一种好的开始给内核开发贡献代码的方式。
链表
为了组成双向链表,可以使用include/linux/list.h文件中提供的函数。首先,你需要在你的数据结构中嵌套一个list_head结构体:
#include <linux/list.h>
 
struct list_head {
  struct list_head *next, *prev;
};
 
struct mydatastructure {
  struct list_head mylist; /* Embed */
  /* ... */                /* Actual Fields */
};
mylist是用于链接mydatastructure多个实例的链表。如果你在mydatastructure数据结构内嵌入了多个链表头,mydatastructure就可以被链接到多个链表中,每个list_head用于其中的一个链表。你可以使用链表库函数来在链表中增加和删除元素。
在进入细节的讨论之前,我们先总结以下链表库说提供的链表操作接口,如表3.1所示。
3.1 链表操作函数
函数
作用
INIT_LIST_HEAD()
初始化表头
list_add()
在表头后增加一个元素
list_add_tail()
在链表尾部增加一个元素
list_del()
从链表中删除一个元素
list_replace()
用另一个元素替代链表中的某一元素
list_entry()
遍历链表中的所有结点
list_for_each_entry()/
list_for_each_entry_safe()
被简化的链表递归接口
list_empty()
检查链表是否为空
list_splice()
2个链表联合起来
为了论证链表的用法,我们来实现一个实例。这个实例也可以为理解下一节要讨论的工作队列的概念打下基础。假设你的内核驱动程序需要从某个入口点开始执行一个艰巨的任务,譬如让当前线程进入睡眠等待状态。在该任务完成之前,你的驱动不想被阻塞,因为这会降低依赖于它的应用程序的响应速度。因此,当驱动需要执行这种工作的时候,它将相应的函数加入一个工作函数的链表,并延后执行它。而实际的工作则在一个内核线程中执行,该线程会遍历链表,并在后台执行这些工作函数。驱动将工作函数放入链表的尾部,而内核则从链表的头部取元素,因此,则包装了这些放入队列的工作会以先进先出的原则执行。当然,驱动的剩余部分需要被修改以适应这种延后执行的策略。在理解这个例子之前,你首先要意识到在清单3.5中,我们将使用工作队列(work queue)接口来完成相同的工作,而且用工作队列的方式会显得更加简单。
首先看看本例中使用的关键的数据结构:
static struct _mydrv_wq {
  struct list_head mydrv_worklist; /* Work List */
  spinlock_t lock;                 /* Protect the list */
  wait_queue_head_t todo;          /* Synchronize submitter
                                      and worker */
} mydrv_wq;
 
 
struct _mydrv_work {
  struct list_head mydrv_workitem; /* The work chain */
  void (*worker_func)(void *);     /* Work to perform */
  void *worker_data;               /* Argument to worker_func */
  /* ... */                        /* Other fields */
} mydrv_work;
mydrv_wq是一个针对所有工作发布者的全局变量。其成员包括一个指向工作链表头部的指针、一个用于在发起工作的驱动函数和执行该工作的线程之间进行通信的等待队列。链表辅助函数不会对链表成员的访问进行保护,因此,你需要使用并发机制以串行化同时发生的指针引用。mydrv_wq的另一个成员——自旋锁用于此目的。清单3.2中的驱动初始化函数mydrv_init()会初始化自旋锁、链表头、等待队列,并启动工作者线程。
清单3.2 初始化数据结构
static int __init
mydrv_init(void)
{
  /* Initialize the lock to protect against
     concurrent list access */
  spin_lock_init(&mydrv_wq.lock);
 
  /* Initialize the wait queue for communication
     between the submitter and the worker */
  init_waitqueue_head(&mydrv_wq.todo);
 
  /* Initialize the list head */
  INIT_LIST_HEAD(&mydrv_wq.mydrv_worklist);
 
  /* Start the worker thread. See Listing 3.4 */
  kernel_thread(mydrv_worker, NULL,
                  CLONE_FS | CLONE_FILES | CLONE_SIGHAND | SIGCHLD);
  return 0;
}
在查看工作者线程(执行被提交的工作)之前,我们先看看提交者本身。清单3.3给出一个函数,内核的其他部分可以利用它来提交工作。该函数调用list_add_tail()来将一个工作函数添加到链表的尾部,图3.1显示了工作队列的物理结构。



 本文转自 21cnbao 51CTO博客,原文链接:http://blog.51cto.com/21cnbao/119946,如需转载请自行联系原作者



网友评论

登录后评论
0/500
评论
技术小胖子
+ 关注