1、第十一讲 Linux驱动程序设计,授课班级:07计算机专业 授课教师:王震,主要内容,设备驱动的基本原理 设备驱动的编写方法 2.6内核设备模型框架 设备驱动中的中断处理方法,ChavezWangG,2,嵌入式系统研究室,设备驱动的基本原理-1,嵌入式系统研究室,3,ChavezWangG,USER APP,VFS,DEVICE op func DEV_REGISTER,HARDWARE,用户空间,内核空间,设备驱动,设备文件与设备文件系统,Linux中,字符设备和块设备都是通过文件节点进行访问。 每个设备对应一个文件名,操作时对应各自的驱动程序。,设备文件与设备文件系统,Linux系统靠主次
2、设备号来联系驱动程序和设备文件节点,依靠主设备号标志不同的驱动程序,注册Linux设备号的方法,为避免不同的驱动程序具有相同的设备号,需要提供一种分配设备号的机制 每个驱动程序分配一个主设备号:不可行,Linux最多支持255个主设备 根据/proc/devices中的对应关系,用脚本动态的创建设备文件:太麻烦,程序员不愿意 设备文件系统自动管理,注册Linux设备号的方法,系统启动时,会把设备驱动程序挂载在/dev/目录下,Linux设备文件的创建和删除、目录层次都都由各个设备驱动程序管理 /dev/下面每个文件都动态对应了一个系统上存在的设备驱动程序。 新添加(或者删除)一个设备,比如u盘
3、,系统就会自动在/dev目录中创建(或者删除)对应的设备节点。,注册Linux设备号的方法,在设备文件系统中,由于分的比较细致,一些驱动程序的对应目标跟以前比一样,如: Linux2.4之前:/dev/fb0 /dev/ttyS0 Linux2.6之后:/dev/fb/0 /dev/tts/0 可以用符号链接进行更改,以便与之前的相匹配: ln-s /dev/fb/0 /dev/fb0 ls-s /dev/tts/0 /dev/ttyS0,设备驱动的基本原理-2,设备分类 字符设备:存取时没有缓存、只能顺序读/写的设备。可通过设备文件节点被访问 与普通文件的区别:普通文件的访问可以前后移动访问
4、指针,而大多数字符设备不支持该操作。 典型的字符设备 鼠标 键盘 串口,嵌入式系统研究室,4,ChavezWangG,设备驱动的基本原理-3,设备分类 块设备:一般块设备都有缓存支持,并且支持随机存取 创建的块设备 硬盘 软盘 ramdisk,嵌入式系统研究室,5,ChavezWangG,设备驱动的基本原理-4,设备分类 网络设备:从BSD UNIX网络组件移植而来。网络设备没有对应地映射到文件系统的设备节点。在Linux中,网络设备的访问采用Socket机制实现,嵌入式系统研究室,6,ChavezWangG,设备驱动的基本原理-5,设备号:Linux采用主设备号和次设备号来标志一个具体设备。
5、 主设备号用来标志设备类型 次设备号用来区分不同的具体设备 系统创建一个设备驱动程序时,设备驱动需要使用一个主设备号向内核注册此驱动。创建一个设备节点的方法: mknod 设备名 设备类型 主设备号 次设备号 例:mknod ttyS0 c 64 4,嵌入式系统研究室,7,ChavezWangG,设备驱动的基本原理-6-内核模块,内核模块的概念:内核模块是一些可以让操作系统内核在需要时载入和执行的代码,不需要时可以从操作系统中卸载 内核模块是Linux内核运行时动态扩展的一种技术,可以在Linux内核运行期间向内核动态添加代码,扩展内核的功能,嵌入式系统研究室,8,ChavezWangG,设备
6、驱动的基本原理-7-内核模块,内核模块与应用程序加载的不同: 内核模块的加载只是向内核预先注册自己以便服务于将来的某个请求,只是加载了某项功能,而不需要马上执行 应用程序加载后就开始执行 内核模块不能使用外部函数库,只能使用内核导出的函数 应用程序可以使用外部函数库 内核模块只能运行在内核空间,并且不生成新的进程 应用程序运行在用户空间,一般一个应用程序生成一个新的进程,嵌入式系统研究室,9,ChavezWangG,设备驱动的基本原理-8-内核模块,内核模块的框架 #include #include #include MODULE_LICENSE(“Dual BSD/GPL“); static
7、 int hello_init(void) printk(KERN_ALERT “Hello, worldn“); return 0; static void hello_exit(void)printk(KERN_ALERT “Goodbye, worldn“); module_init(hello_init); module_exit(hello_exit);,嵌入式系统研究室,10,ChavezWangG,设备驱动的基本原理-9-内核模块,2.6系列内核模块的编译与加载 内核源码树的每个子目录都有一个kconfig文件,为makefile提供配置数据库,分别描述了所属目录源文件相关的内核
8、配置菜单项 内核配置时,从kconfig中读出配置菜单,配置后生成.config文件 编译内核时,makefile读入对应的.config文件生成内核映像 加入新驱动到内核源码树时,需要修改相应目录的kconfig,将新驱动加入内核的配置菜单,同时需要修改makefile文件,嵌入式系统研究室,11,ChavezWangG,设备驱动的基本原理-10-内核模块,嵌入式系统研究室,12,ChavezWangG,.config,Makefile,Kconfig,Kconfig,Kconfig,makefile,makefile,arch/arm/makefile,Scrip/makefile,men
9、uconfig,输出,Kconfig配置文件 Konfig文件树是内核的配置数据库 每个Kconfig文件描述一系列内核配置菜单项,每个菜单项提供一个关键字标识 语法: Config 例:config HELLO_MODULEbool “hello test module”,嵌入式系统研究室,13,ChavezWangG,设备驱动的基本原理-11-内核模块,设备驱动的基本原理-12-内核模块,Kconfig配置文件 类型定义有:bool 、tristate、string、hex、integer等依赖性定义:如果菜单项的出现依赖于另一个定义时,就用关键字depends on或者 requires
10、标识config HELLO_MODULEbool “hello test model”depends on ARCH_PXA 帮助定义:关键字help 或者 help-,嵌入式系统研究室,14,ChavezWangG,设备驱动的基本原理-13-内核模块,内核的makefile:内核源码根目录下的顶层makefile可以分为五部分 该makefile本身,负责linux内核的二进制映像文件vmlinux和内核模块的编译 内核配置文件.config,记录内核的当前配置并在编译时提供给makefile成为其一部分 特定体系结构相关目录下的makefile,提供与体系结构相关的信息 Scripts下
11、的makefile.*,包含一些内核模块编译共用的定义和规则 内核源码树各子目录中的与模块编译相关的kbuild makefile。编译时,集成上层makefile传下来的宏定义和其他编译规则将源代码编入内核或编译成内核模块,嵌入式系统研究室,15,ChavezWangG,Kbuild makefile内容:最简单的只有一行。以hello.c为例: obj-y+=hello.o :编译进内核的二进制映像中 obj-m+=hello.o:编译成内核的可加载模块 obj-$(CONFIG_HELLO_MODULE)+=hello.o 根据变量的不同,既可以编译进内核,也可以编译为模块,嵌入式系统研
12、究室,16,ChavezWangG,设备驱动的基本原理-13-内核模块,Kbuild makefile内容:若具有多个文件,如mymodule由file1.c file2.c构成,则 Makefile定义为 obj-m:=mymodule.o Mymodule-objs:=file1.o file2.o 在内核源码树之外的模块,则用下面的方式编译:make-C M=$PWD modules,嵌入式系统研究室,17,ChavezWangG,设备驱动的基本原理-13-内核模块,驱动程序的编译,设备驱动程序的结构-1,设备驱动程序从总体上看可以分为两部分 驱动与内核的接口层:实现在内核的注册加载和卸
13、载清除工作。若采用了中断处理,还要包括中断处理函数的注册与注销。 硬件设备接口层:主要描述驱动程序与设备的交互。包括: 硬件探测 硬件初始化 设备读写访问 设备控制操作,嵌入式系统研究室,18,ChavezWangG,虚拟文件系统与硬件驱动的接口1,Linux系统中用户对设备的操作采用文件接口实现。 虚拟文件系统将这种对文件的访问操作转化为对设备的具体操作。 虚拟文件系统为设备驱动提供了一个标准化的文件操作实现接口。 该接口定义在: Include/linux/fs.h中的 file_operations定义,嵌入式系统研究室,19,ChavezWangG,/* NOTE:* read, wr
14、ite, poll, fsync, readv, writev can be called* without the big kernel lock held in all filesystems.*/ struct file_operations struct module *owner;loff_t (*llseek) (struct file *, loff_t, int);ssize_t (*read) (struct file *, char _user *, size_t, loff_t *);ssize_t (*aio_read) (struct kiocb *, char _u
15、ser *, size_t, loff_t);ssize_t (*write) (struct file *, const char _user *, size_t, loff_t *);ssize_t (*aio_write) (struct kiocb *, const char _user *, size_t, loff_t);int (*readdir) (struct file *, void *, filldir_t);unsigned int (*poll) (struct file *, struct poll_table_struct *);int (*ioctl) (str
16、uct inode *, struct file *, unsigned int, unsigned long);,嵌入式系统研究室,20,ChavezWangG,int (*mmap) (struct file *, struct vm_area_struct *);int (*open) (struct inode *, struct file *);int (*flush) (struct file *);int (*release) (struct inode *, struct file *);int (*fsync) (struct file *, struct dentry *,
17、 int datasync);int (*aio_fsync) (struct kiocb *, int datasync);int (*fasync) (int, struct file *, int);int (*lock) (struct file *, int, struct file_lock *);ssize_t (*readv) (struct file *, const struct iovec *, unsigned long, loff_t *);ssize_t (*writev) (struct file *, const struct iovec *, unsigned
18、 long, loff_t *);ssize_t (*sendfile) (struct file *, loff_t *, size_t, read_actor_t, void *);ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);int (*check_flags)
19、(int);int (*dir_notify)(struct file *filp, unsigned long arg);int (*flock) (struct file *, int, struct file_lock *); ;,嵌入式系统研究室,21,ChavezWangG,注意: 结构体中的函数,并不是每个都需要实现的。 不需要实现的函数,可以直接初始化为空。 在嵌入式系统中,一般仅仅实现其中的几个接口函数:read, write, ioctl, open, release等,就可以完成应用系统需要的功能,嵌入式系统研究室,22,ChavezWangG,虚拟文件系统与硬件驱动的接口
20、2,Open方法:方法open用于打开设备,open操作是对设备的第一个操作,如果open方法为空,则设备始终打开成功 递增使用计数 检查特定设备错误 如果设备是首次打开,则对其进行初始化 识别次设备号,嵌入式系统研究室,23,ChavezWangG,虚拟文件系统与硬件驱动的接口3,虚拟文件系统与硬件驱动的接口4,release方法:方法release在关闭文件时调用,其工作与open方法相反 释放由open分配的filp-private_data中的左右内容 使用计数减一 在最后一次关闭操作时关闭设备,嵌入式系统研究室,24,ChavezWangG,虚拟文件系统与硬件驱动的接口5,read方
21、法:方法read从设备中读取数据,函数返回非负值表示成功读出的字节数 返回值等于传递给read系统调用的count参数,表明请求的数据传输成功 返回值大于0,但小于传递给read系统调用的count参数,表明部分数据传输成功,根据设备的不同,导致这个问题的原因也不同,一般采取再次读取的方法 返回值=0,表示到达文件的末尾 返回值为负数,表示出现错误,并且指明何种错误 在阻塞型io中,read调用会出现阻塞。,嵌入式系统研究室,25,ChavezWangG,虚拟文件系统与硬件驱动的接口6,write方法:方法write向设备中写入数据,函数返回非负值表示成功写入的字节数 返回值等于传递给writ
22、e系统调用的count参数,表明请求的数据传输成功 返回值大于0,但小于传递给write系统调用的count参数,表明部分数据传输成功,根据设备的不同,导致这个问题的原因也不同,一般采取再次写入的方法 返回值=0,表示没有写入任何数据。此时一般会重复调用write 返回值为负数,表示出现错误,并且指明是何种错误。 在阻塞性io中,write调用会出现阻塞。,嵌入式系统研究室,26,ChavezWangG,虚拟文件系统与硬件驱动的接口7,ioctl方法:方法ioctl为设备调用ioctl提供了一种执行设备特定命令的方法,主要是读写之外的其他控制。如配置设备、进入或者退出某种操作模式等。 实验板上
23、的SPI设备通道的选择操作,即通过这种方式,嵌入式系统研究室,27,ChavezWangG,虚拟文件系统与硬件驱动的接口8,*owner指向拥有该结构的模块,内核使用指针维护模块的使用计数 llseek用来修改文件当前位置,并将新位置作为结果返回。loff_t是在linux中定义的长偏移量。出错时返回负值。,嵌入式系统研究室,28,ChavezWangG,方法poll是系统调用select和poll的后端实现,用这两个系统调用来查询设备是否可以读写或者是否处于某种状态。如果poll为空,则驱动设备会被认为既可读,又可写,返回值是一个状态掩码。 方法mmap将设备内存映射到进程地址空间。,嵌入式
24、系统研究室,29,ChavezWangG,虚拟文件系统与硬件驱动的接口9,简单的字符驱动-1,大多数字符设备比较简单,通常直接使用file_operations接口。 本课程将以一个虚拟的字符设备test_char为例,说明字符驱动的结构,嵌入式系统研究室,30,ChavezWangG,简单的字符驱动-2,虚拟文件系统与设备驱动程序的接口 本设备之设计到读写和设备的打开关闭操作。因此可以定义以下file_opertions结构体变量。 struct file_operations testchar_fops.read=test_read,.write=test_write,.open=test
25、_open,.release=test_release ; 注意:定义结构体变量之前需要先实现用到的函数,嵌入式系统研究室,31,ChavezWangG,简单的字符驱动-3,static char test_val=a;/用全局变量保存虚拟设备读写的数据 static ssize_t test_read(struct file *filp, char _user *buf, size_t count, loff_t *l) copy_to_user(buf, ,嵌入式系统研究室,32,ChavezWangG,简单的字符驱动-4,驱动模块的加载和卸载操作#define TEST_MAJOR 25
26、1#define TEST_NAME “TEST_CHAR”int _init init_routine(void) register_chrdev(TEST_MAJOR, TEST_NAME, ,嵌入式系统研究室,33,ChavezWangG,Linux 2.6内核设备模型-1,Linux2.2版之前没有统一的驱动形式,2.4通过使用一组通用接口将PCI、PCMIA等整合到一个单一的设备结构中,2.6内核则更进一步,试图在整个系统的范围内对硬件设备进行抽象,建立一个统一的全新设备模型 新模型包括四个重要数据结构: struct kobject struct kset struct ktype
27、 struct subsystem,嵌入式系统研究室,34,ChavezWangG,Linux 2.6内核设备模型-2,struct kobject结构:最底层的基础数据结构 struct kobjectchar *k_name;/动态分配空间的设备名称char nameKOBJ_NAME_LEN;/长度受限的设备名称atomic_t refcount;/引用计数struct list_head entry;/挂接到所有集合中去的入口struct kobject *parent;/ 商机对象的指针struct kset *kset;/所属对象集合的指针struct kobj_type ktyp
28、e;/ 所属对象类型指针struct dentry *dentry;/在sysfs系统中的文件节点路径指针 ,嵌入式系统研究室,35,ChavezWangG,Linux 2.6内核设备模型-3,struct kset结构:用于描述同一类kobject集合 struct ksetstruct subsystem *subsys;/所在的subsystem的指针struct kobj_type *ktype;/集合中的对象类型struct list_head list;/集合中的对象链表 struct kobject kobj;/集合自身相关信息的内核对象struct kset_hotplug_o
29、ps *hotplug_ops;/该集合的热插拔函数 ,嵌入式系统研究室,36,ChavezWangG,Linux 2.6内核设备模型-4,struct kobj_ktype结构: struct kobj_typevoid (*release)(struct kobject *);/本类对象的释放函数struct sysfs_ops *sysfs_ops;/本类对象在sysfs中的操作函数struct attribute * *default_attrs;/本类对象在sysfs中的属性 ,嵌入式系统研究室,37,ChavezWangG,Linux 2.6内核设备模型-5,struct subs
30、ystem内核对象子系统:是最上层的数据结构,用于描述同属一类设备的子系统 struct subsystemstruct kset kset;/该子系统的内核对象集合struct rw- semaphore rwsem;/互斥访问信号量 ,嵌入式系统研究室,38,ChavezWangG,Linux 2.6内核设备模型-6,系统初始化时,drivers/base/init.c中的driver-init()函数将几个顶层子系统的subsystem数据结构注册到内核 初始化子系统数据结构 在sysfs文件系统下建立各个子系统的目录树 五个子系统: 设备子系统devices_subbus 总线子系统b
31、us_subbus 设备基类子系统class_subbus 固件子系统(fireware_subbus) 虚拟系统总线子系统(system_subbus),嵌入式系统研究室,39,ChavezWangG,Linux 2.6内核设备模型-7,五个子系统构成了管理框架 初始化时,初始化程序调用内核对象注册函数将设备数据结构中的成员kobject结构注册到相应的子系统中 最终用户可以通过sysfs文件系统看到所有设备的信息,嵌入式系统研究室,40,ChavezWangG,Linux内核的中断处理-1,中断机制是实现外部I/O异步操作的重要方法 Linux内核对外部设备的中断处理在设备驱动程序里实现,
32、嵌入式系统研究室,41,ChavezWangG,Linux内核的中断处理-2,中断处理程序的注册 注册函数 Int request_irq(unsigned int irq, irqreturn_t(*handler)(int, void*,struct pt_regs*),unsigned long irq_flags, const char *devname, void * dev_id); 该函数成功则返回0; irq为要注册的中断号 Handler指向中断处理程序的函数指针 irq-flags为中断标志,含义如下: SA_INTERRUPT:不受中断屏蔽的影响,任何情况下都可以响应。一
33、般只有时钟中断具有此标志。 SA_SAMPLE_RAMDOM:产生中断的间隔时间放入系统随机数的内核熵池 SA_SHIRQ:表明为共享中断,嵌入式系统研究室,42,ChavezWangG,Linux内核的中断处理-3,中断处理程序的注册 dev_name:与中断相关的设备名字。 dev_id:主要用于共享中断中,释放共享中断时,根据dev_id的标志信息来删除指定的那个中断处理程序处理的中断。 若dev_id设为null,则为独占式的中断,嵌入式系统研究室,43,ChavezWangG,Linux内核的中断处理-4,中断处理程序的释放 Void free_irq(unsigned int ir
34、q, void *dev_id) 若释放的中断是共享的,则dev_id对应的中断处理程序与中断的绑定关系被删除 若删除了最后一个中断处理程序的绑定关系,则此中断被禁用 若要释放的中断是非共享的,则删除中断处理程序的同时也将禁用中断,嵌入式系统研究室,44,ChavezWangG,Linux内核的中断处理-5,中断处理程序的编写 中断处理程序与其他内核函数的不同之处:中断处理程序发送或者接受数据都必须在内核空间,不能在内核与用户空间之间发送或者接受数据 中断不能做任何可能发生休眠的操作,也不能调用schedule函数进行进程调度。 static irqreturn_t irq_handler(i
35、nt irq, void *dev_id,struct pt_regs *regs) 当中断程序被调用并正确处理时,返回IRQ_HANDLED,否则返回IRQ_NONE,嵌入式系统研究室,45,ChavezWangG,Linux2.6内核的工作推后执行机制-1,问题引入: 通常中断处理程序需要尽量做到短小和快速处理,以免中断阻塞时间过长,影响实时性和中断响应速度 但是,有些设备产生中断后需要处理较多的与中断相关的工作,所以速度会比较慢 处理方法 将中断处理分为两个部分: 中断处理程序部分:只做必要的有严格时限的工作,如中断应答和复位硬件等,一般需要屏蔽中断的情况下处理 其他需要时间的不是很紧急
36、的工作推后处理。,嵌入式系统研究室,46,ChavezWangG,内核工作推后执行的机制主要有: 软中断 tasklet 工作队列,嵌入式系统研究室,47,ChavezWangG,Linux2.6内核的工作推后执行机制-2,软中断:通常用于执行频率很高的强实时性的场合。作为一种底层机制,很少由内核程序员直接使用。 enum HI_SOFTIRQ=0,/优先级高的tasklet软中断TIMER_SOFTIRQ,/定时器软中断NET_TX_SOFTIRQ,/网络数据包发送软中断NET_RX_SOFTIRQ,/网络数据包接收软中断SCSI_SOFTIRQ,/SCSI软中断的软中断TASKLET_SO
37、FTIRQ./tastlet软中断的软中断 ,嵌入式系统研究室,48,ChavezWangG,Linux2.6内核的工作推后执行机制-3,tasklet:小任务,指一小段可以执行的嗲吗,通常以函数的形式出现,对于io驱动程序,是实现工作推后执行的首选方法。 tasklet基于软中断机制实现,实际上只是一种软中断的应用和包装 struct tasklet_structstruct tasklet_struct *next;/指向链表中的下一个结构体unsigned long state;/tasklet的状态,初始化时为0,被调用时为1atomic_t count;/引用计数器,若不为0,则不能
38、执行;若为0则可以执行void (*func)(unsigned long);/tasklet处理函数unsigned long data;/给tasklet处理函数的参数 ,嵌入式系统研究室,49,ChavezWangG,Linux2.6内核的工作推后执行机制-4,tasklet的使用 tasklet声明: DECLARE_TASKLET(name,func,data)/初始值为0,激活状态 DECLARE_TASKLET_DISABLED(name,func,data)/初始值为1,停用状态 name:tasklet_struct变量的名字 func:是tasklet的执行函数 data:
39、是tasklet执行函数的参数,嵌入式系统研究室,50,ChavezWangG,Linux2.6内核的工作推后执行机制-5,tasklet的使用 tasklet执行函数定义 Void func(unsigned long data) 注意: 必须与struct tasklet_struct结构中的func定义一致 Func()要在tasklet声明之前定义 不能使用信号量或者其他可能引起阻塞的函数,因为tasklet不能休眠,嵌入式系统研究室,51,ChavezWangG,Linux2.6内核的工作推后执行机制-6,tasklet的使用 tasklet的调度 tasklet_schedule(
40、&name):将一个tasklet状态置为准备执行状态,系统由合适的机会时,就会尽快执行 Tasklet_disable():将tasklet的count计数增1,从而停用该tasklet Tasklet_enable():激活tasklet。 Tasklet_kill():从等待执行的tasklet队列中删除一个tasklet,嵌入式系统研究室,52,ChavezWangG,Linux2.6内核的工作推后执行机制-6,工作队列 每个工作队列有一个专门的线程,称之为工作队列线程 工作队列中推后执行的工作由此线程完成,所有的工作在线程的上下文中被执行。因此,允许重新调度甚至睡眠,嵌入式系统研究室
41、,53,ChavezWangG,Linux2.6内核的工作推后执行机制-7,工作队列:struct work_struct数据结构 Struct work_structunsigned long pending;/工作是否等待处理struct list_head entry;/工作队列中的工作链表void(* func)(void *);/处理函数void *data;/处理函数的参数void *wq_data;/内部使用struct timer_list timer;/定时器 ,嵌入式系统研究室,54,ChavezWangG,Linux2.6内核的工作推后执行机制-8,工作队列的使用 创建工
42、作队列: Struct workquene_struct *create_workquene(const char *name) /创建一个新工作队列,返回新工作队列的指针 例如:struct worksquene_struct *my_workqe;my_workqe=create_workquene(“my_workqe”); 创建推后执行的工作 编译时创建工作队列: #define DECLARE_WORK(name,func,data) struct work_struct n=_WORK_INITALIZER(name,func,data) 运行时动态创建: Struct work_
43、struct my_work; INIT_WORK(my_work,my_work_func,data);,嵌入式系统研究室,55,ChavezWangG,Linux2.6内核的工作推后执行机制-9,工作队列的使用 定义工作的任务处理函数 Void func(void *data) 将工作加入工作队列中 int quene_work(struct workquene_struct *squene,struct work_struct *work); /即时加入工作队列 int quene_delayed_work(struct workquene_struct *quene, struct w
44、ork_struct *work, unsigned long delay); /延迟一段时间加入工作队列 Int cancel_delayed_work(struct work_struct *work) /取消延期加入工作队列的工作;如果已经加入则不受影响,嵌入式系统研究室,56,ChavezWangG,Linux2.6内核的工作推后执行机制-10,工作队列的使用 刷新工作队列 Void fastcall flush_workquene(struct workquene_struct *wq); /在卸载前,保证操作都已经执行完毕。如果没有执行完毕,会进入休眠状态 销毁工作队列 Void destroy_workquene(struct workquene_struct *wq); /销毁使用完毕的工作队列 注意:不是所有的驱动都需要自己的工作队列。,嵌入式系统研究室,57,ChavezWangG,Linux2.6内核的工作推后执行机制-6,