Linux设备驱动程序学习(3)-字符设备驱动程序

开始学习《Linux设备驱动程序(第三版)》第三章,本章主要是学习字符设备的基本操作,以scull为研究对象,即“simple character utility for loading localities”(区域装载的简单字符工具),scull是一个操作内存区域的字符设备驱动程序,这片内存区域就相当于一个设备。scull可以为真实的设备驱动程序提供一个样板。

 

一、主设备号和次设备号

主设备号标识设备对应的驱动程序。次设备号由内核使用,用于正确确定设备文件对应的设备。内核允许多个驱动程序共享一个主设备号。

1、 设备编号的内部表达

内核中,用dev_t类型<linux/types.h>来保存设备编号,dev_t是个32位的数,12位用来表示主设备号,20位表示次设备号。

实际使用中,应该使用<linux/kdev_t.h>中的宏来变换格式:

获得dev_t的主设备号或

次设备号

MAJOR(dev_t dev)

MINOR(dev_t dev)

将主设备号和次设备号转化成dev_t类型

MKDEV(int major,int minor)

 

2、分配和释放设备编号

在建立一个字符设备之前,驱动程序首先要做的就是获得一个或者多个设备编号,完成该工作的函数声明在<lnux/fs.h>中。

静态分配设备编号:

1 int register_chrdev_region(dev_t from, unsigned count, const char *name)

适用于已知设备号的情况

成功执行返回0

1 dev_t from        要分配的设备编号范围的起始值
2 unsigned count      所请求的连续设备编号的个数
3 const char *name   与该编号范围关联的设备名称

 

动态分配设备编号

1 int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)

成功执行返回0

1 dev_t *dev            用于保存已分配范围的第一个设备编号
2 unsigned baseminor 第一个次设备号,通常是0
3 unsigned count 所请求的连续设备编号的个数
4 const char *name 与该编号范围关联的设备名称

 

释放设备编号

不管是采用什么方法分配设备号,释放设备号需使用下面函数:

1 void unregister_chrdev_region(dev_t from, unsigned count)
1 dev_t from              设备注册的第一个设备号
2 unsigned count      已注册的连续设备编号的个数

 

分配设备编号的最佳方式是:默认采用动态分配,同时保留在加载甚至是编译时指定设备号的余地。

 

下面是scull.c中用来获取设备号的代码:

 1 if (scull_major) {
2 dev = MKDEV(scull_major, scull_minor);
3 result = register_chrdev_region(dev, scull_nr_devs, "scull");
4 } else {
5 result = alloc_chrdev_region(&dev, scull_minor, scull_nr_devs,
6 "scull");
7 scull_major = MAJOR(dev);
8 }
9 if (result < 0) {
10 printk(KERN_WARNING "scull: can't get major %d\n", scull_major);
11 return result;
12 }

这部分中,函数中参数name是和该编号范围关联的设备名称,获取设备编号后,它将出现在/proc/devices和sysfs中。

 

二、一些重要的数据结构

大部分的驱动程序都会涉及到三个内核数据结构,分别是file_operations、files和inode。它们定义在<lnux/fs.h>中。

1、file结构

系统中,每一个打开的文件在内核空间都有一个对应的file结构。由内核在open时创建并传递给在该文件上进行操作的所有函数,直到最后的close函数。在文件的所有实例都被关闭后,内核会释放这个结构。指向struct file的指针通常被称为file或者filp(文件指针),书中一致取filp。File是结构本身,filp则是指向该结构的指针。

struct file比较重要的结构成员如下:

1 struct file {
2 struct dentry *f_dentry; // 文件对应的目录项(dentry)结构
3 struct file_operations *f_op; // 与文件相关的操作
4 unsigned int f_flags; // 文件标志
5 mode_t f_mode; // 文件模式,可读或可写
6 loff_t f_pos; // 当前读写/位置
7 void *private_data; // 跨系统调用时保存信息
8 };

 

1、inode结构

内核用inode表示磁盘上的文件。区别file结构:

file表示打开的文件描述符,对于单个文件,可能会有许多个表示打开的文件描述符的file结构,但是他们都指向单个inode结构。

inode结构中包含了大量的有关文件信息:

1 struct inode {
2 dev_t i_rdev; // 包含了真正的设备编号
3 struct cdev *i_cdev; // 当inode指向一个字符设备文件时,该字段包含了指向struct cdev结构的指针
4 };

 

内核开发者增加了两个新的宏,可用来从一个inode中获得主设备号和次设备号:

获得主设备号

unsigned imajor(struct inode *inode)

获得次设备号

unsigned iminor(struct inode *inode)

如果我们想从inode结构中获得主次设备号,我们应该使用上述宏,而不是直接操作i_rdev。

 

1、 文件操作

struct file_operations结构用来建立设备驱动程序和设备编号之间的连接。  结构中包含了一组函数指针,每个打开的文件(在内部用一个file结构表示)和一组函数关联。这些操作主要是用来实现系统调用,我们可以认为文件是一个“对象”,而操作它的函数是“方法”,即对象声明的动作将作用于其本身

file_operations结构或者指向这类结构的指针称为fops。该结构中的每一个字段都必须指向驱动程序中实现特定操作的函数,对于支持的操作,对应的字段可置为NULL值。

 1 struct file_operations {
2 struct module *owner;
3 loff_t (*llseek) (struct file *, loff_t, int);
4 ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
5 ssize_t (*aio_read) (struct kiocb *, char __user *, size_t, loff_t);
6 ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
7 ssize_t (*aio_write) (struct kiocb *, const char __user *, size_t, loff_t);
8 int (*readdir) (struct file *, void *, filldir_t);
9 unsigned int (*poll) (struct file *, struct poll_table_struct *);
10 int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
11 int (*mmap) (struct file *, struct vm_area_struct *);
12 int (*open) (struct inode *, struct file *);
13 int (*flush) (struct file *);
14 int (*release) (struct inode *, struct file *);
15 int (*fsync) (struct file *, struct dentry *, int datasync);
16 int (*aio_fsync) (struct kiocb *, int datasync);
17 int (*fasync) (int, struct file *, int);
18 int (*lock) (struct file *, int, struct file_lock *);
19 ssize_t (*readv) (struct file *, const struct iovec *, unsigned long, loff_t *);
20 ssize_t (*writev) (struct file *, const struct iovec *, unsigned long, loff_t *);
21 ssize_t (*sendfile) (struct file *, loff_t *, size_t, read_actor_t, void *);
22 ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
23 unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
24 int (*check_flags)(int);
25 int (*dir_notify)(struct file *filp, unsigned long arg);
26 int (*flock) (struct file *, int, struct file_lock *);
27 };

 

 scull设备的file_operations结构初始化如下:

1 struct file_operations scull_fops = {
2 .owner = THIS_MODULE,
3 .read = scull_read,
4 .write = scull_write,
5 .open = scull_open, /* 函数名即函数入口地址 */
6 .release = scull_release,
7 };/* 注意这里标记化结构体初始化的语法 */

 标记化结构初始化语法允许结构成员进行重新排列。

 

三、字符设备的注册

内核内部使用struct cdev结构来表示字符设备。在内核调用设备的操作之前,必须分配并注册一个或者多个上述结构。代码应包含<linux/cdev.h>,其中定义了这个结构以及与其相关的一些辅助函数。

注册一个独立的cdev设备的过程如下:

1、为struct cdev分配空间(如果已经将struct cdev嵌入到自己设备的特定结构中,并分配的内存空间,则该步骤可省)

1 struct cdev *my_cdev = cdev_alloc();

 2、初始化struct cdev

1 void cdev_init(struct cdev *cdev, struct file_operations *fops)

 3、初始化cdev.owner

1 cdev.owner = THIS_MODULE;

 4、在cdev结构都设置好之后,最后的步骤是告诉内核该结构的信息(在驱动程序还没有完全准备好处理设备上的操作时,就不能调用下面函数)。

1 int cdev_add(struct cdev *p, dev_t dev, unsigned count)

 附:从系统中移除一个字符设备

1 void cdev_del(struct cdev *p)

 

scull完成设备注册的代码如下(之前已经为struct scull_dev 分配了空间):

 1 /*
2 * Set up the char_dev structure for this device.
3 */
4 /* 设备注册函数 */
5 static void scull_setup_cdev(struct scull_dev *dev, int index)
6 {
7 /* 由主、次设备号得到完整具体的设备号 */
8 /* 主设备号:scull_major */
9 /* 次设备号:scull_minor + index */
10 int err, devno = MKDEV(scull_major, scull_minor + index);
11
12 /* 初始化cdev结构,且指定其ops函数指针 */
13 cdev_init(&dev->cdev, &scull_fops);
14
15 /* 指定cdev结构所有者 */
16 dev->cdev.owner = THIS_MODULE;
17
18 /* 这一步可以省略,因为调用cdev_init时已实现 */
19 //dev->cdev.ops = &scull_fops;
20
21 /* 向内核注册设备,立即生效 */
22 err = cdev_add (&dev->cdev, /* 设备对应的cdev结构 */
23 devno, /* 设备对应的第一个设备号 */
24 1); /* 和该设备关联的连续设备编号的数目,常取1 */
25
26 /* Fail gracefully if need be */
27 if (err) /* 向内核注册设备失败 */
28 printk(KERN_NOTICE "Error %d adding scull%d", err, index);
29 }

 

早期的注册方法

新的代码不应该再使用这些老的接口,因为这种机制会在将来的内核中消失,这些函数声明在<lnux/fs.h>中。

注册一个字符设备驱动程序的经典方式:

1 int register_chrdev(unsigned int major, const char *name, struct file_operations *fops)

如果使用register_chrdev注册设备,则将自己的设备从系统中移除的正确方法是:

1 int unregister_chrdev(unsigned int major, const char *name)

 

四、scull模型的内存使用

 

scull使用的内存区域这里也称为设备,其长度是可变的。写的越多,它就变得越长。用更短的文件以覆盖方式写设备时则会变短。

下面是描述scull设备的结构体:

1 /*
2 * Representation of scull quantum sets.
3 */
4 /* 量子集链表,每一个链表项内嵌一个量子集 */
5 struct scull_qset {
6 void **data; /* 指明量子集(指针数组)起始位置 */
7 struct scull_qset *next; /* 指向下一个量子集链表项 */
8 };

 

 1 /* 定义scull_dev结构体用来描述scull设备 */
2 struct scull_dev {
3 struct scull_qset *data; /* 指向第一个scull_qset结构体 */
4 int quantum; /* 量子大小,量子也是指针,指向的内存区域大小即为quantum */
5 int qset;/* 量子集大小(指针数组元素个数),量子集即指针数组,其元素即量子 */
6 unsigned long size; /* 数据总量 */
7 unsigned int access_key;
8 struct semaphore sem;
9 struct cdev cdev; /* 字符设备结构 */
10 };

 

scull设备量子集、量子的理解

量子集实际上是一个指针数组,其成员即量子,量子是一个指针,指向某一个内存块,该内存块的大小为quantum字节,量子集中有多少个量子,即该指针数组元素的个数,用qset衡量。在scull设备中,可以存在多个这样的量子集(指针数组),每个量子集内嵌在量子集链表struct scull_qset中,并且所有的量子集大小都相等(即每个量子集中量子个数都一样),每个量子的大小也相等(量子指针所指向的内存块大小相等)。

 

scull驱动程序引入了Linux内核用于内存管理的两个核心函数。这两个函数定义在<linux/slab.h>中:

1 void *kmalloc(size_t size, int flags);
2 void kfree(const void *ptr);

 

 

scull驱动代码中直接操作量子集、量子的函数:

scull_trim()

负责释放整个数据区(类似清零),并且在文件以只写方式打开时由scull_open调用,以及在模块退出函数scull_cleanup_module()中被调用:

 1 /*
2 * Empty out the scull device; must be called with the device
3 * semaphore held.
4 */
5 /* 设备文件清除函数 */
6 /* 释放整个数据区:量子集链表项->量子集->量子。简单遍历链表并且释放它发现的任何量子集和量子 */
7 /* 在scull_open在文件为写而打开时调用 */
8 /* 调用该函数时必须要有信号量-后面再理解 */
9 int scull_trim(struct scull_dev *dev)
10 {
11 struct scull_qset *next, *dptr;
12 int qset = dev->qset;/* dev非空,量子集大小,即量子集中量子个数,指针数组中元素个数 */
13 int i;
14
15 /* 遍历设备所有量子集链表项,循环次数为设备的量子集个数次 */
16 for (dptr = dev->data;/* 第一个量子集链表项 */
17 dptr;/* dptr是否为NULL */
18 dptr = next)/* 下一个量子集链表项 */
19 {
20 if (dptr->data) {/* 量子集(指针数组)中是否有数据 */
21 for (i = 0; i < qset; i++)/* 遍历释放当前量子集中的每个量子,量子集大小为qset */
22 kfree(dptr->data[i]);/* 释放一个量子(其指向的内存块),量子(其指向的内存块)大小为quantum字节 */
23 kfree(dptr->data);/* 释放一个量子集(指针数组),存储qset个量子(指针)时占据的内存 */
24 dptr->data = NULL;
25 }
26 next = dptr->next;/* 获取下一个量子集链表项 */
27 kfree(dptr);/* 释放当前量子集链表项 */
28 }
29 /* 清理struct scull_dev *dev中变量的值 */
30 dev->size = 0;
31 dev->quantum = scull_quantum;
32 dev->qset = scull_qset;
33 dev->data = NULL;
34 return 0;
35 }

 

 

scull_follow():

以下是scull模块中的一个沿链表前行得到正确scull_set指针的函数,将在read和write方法中被调用:

 1 /*
2 * Follow the list
3 */
4 /* 返回dev设备的第n个量子集链表项指针,量子集不够n个就申请新的 */
5 struct scull_qset *scull_follow(struct scull_dev *dev, int n)
6 {
7 struct scull_qset *qs = dev->data;/* 当前设备的第一个量子集 */
8
9 /* Allocate first qset explicitly if need be */
10 /* 如果当前设备还没有量子集,则显示地分配第一个量子集 */
11 if (! qs) {
12 /* kmalloc动态分配连续的物理地址、虚拟地址连续的内存空间,用于小内存分配 */
13 qs = dev->data = kmalloc(sizeof(struct scull_qset),/* 要分配的块大小 */
14 GFP_KERNEL);/* 内存管理器的行为标志 */
15 if (qs == NULL)
16 return NULL;/* 分配失败 */
17 memset(qs, 0, sizeof(struct scull_qset));/* 清空所分配的内存块 */
18 }
19
20 /* Then follow the list */
21 /* 遍历当前设备的量子集链表n步,确保有n个量子集,量子集不够就申请新的 */
22 while (n--) {
23 if (!qs->next) {/* 量子集不够n个,申请新的 */
24 qs->next = kmalloc(sizeof(struct scull_qset), GFP_KERNEL);
25 if (qs->next == NULL)/* 分配失败 */
26 return NULL; /* Never mind */
27 memset(qs->next, 0, sizeof(struct scull_qset));
28 }
29 qs = qs->next;
30 continue;/* 结束本次循环 */
31 }
32 return qs;/* 返回dev设备的第n个量子集入口指针 */
33 }

 

 

五、openrelease

open方法提供给驱动程序以初始化的能力,从而为以后的操作完成初始化做准备。

在设备驱动程序中,open应完成如下工作:

1、检查设备特定的错误(诸如设备未就绪或类似的硬件问题)

2、如果设备是首次打开,则对其进行初始化

3、如有必要,更新f_op指针

4、分配并填写置于filp->private_data里的数据结构

在实际应用中,cdev结构一般嵌套在特定的设备结构中。如scull设备中,cdev结构体嵌套在scull_cdev结构中,我们通常不需要cdev结构本身,而是希望得到包含cdev结构的scull_cdev结构,在这种情况下,需要使用内核中的一个宏,它定义在<linux/kernel.h>中:

 1 /**
2 * container_of - cast a member of a structure out to the containing structure
3 *
4 * @ptr: the pointer to the member.
5 * @type: the type of the container struct this is embedded in.
6 * @member: the name of the member within the struct.
7 *
8 */
9 #define container_of(ptr, type, member) ({ \
10 const typeof( ((type *)0)->member ) *__mptr = (ptr); \
11 (type *)( (char *)__mptr - offsetof(type,member) );})

 其作用为:通过指针ptr,获得包含ptr所指向数据(是member结构体)的type结构体的指针。即是用指针得到另外一个指针。

在scull中应用该宏的代码如下:

1 /* 识别需要被打开的设备(得到设备对应的设备结构体) */
2 /* 宏container_of利用父结构体struct scull_dev的成员struct cdev cdev得到指向该父结构体的指针 */
3 dev = container_of(inode->i_cdev,/* 指向该成员的指针,struct inode中定义,struct cdev *i_cdev */
4 struct scull_dev,/* 父结构体类型 */
5 cdev);/* 该成员的名称,其包含在父结构体struct scull_dev中,struct cdev cdev */

 

 

release方法和open作用相反,其应完成的工作如下:

1、释放由open分配的、保存在filp->private_data中的所有内容

2、在最后一次关闭操作时关闭设备

但是,并不是每个close系统调用都会引起对release方法的调用,只有那些真正释放设备数据结构的close系统调用才会调用这个方法。内核对每个file 结构维护其被使用多少次的计数器,无论是fork还是dup,都不会创建新的file 数据结构(仅由open 创建),他们只是增加已有结构中的使用计数。只有在file 结构的计数归为0时,close 系统调用才会执行release 方法,这只在删除这个结构时参才会发生。release方法与close 系统调用间的关系保证了对于每次open驱动程序只会看到对应的一次release 调用。

因为scull被定义为一个全局持久的内存区,所以它的release什么都不要去做。

注意:flush方法在应用程序每次调用close 时都会被调用,但是很少有驱动程序去实现flush,因为在close时并没有什么事情需要去做,除非release被调用。

 

六、readwrite

read和write的作用主要是实现用户空间和内核空间之间整段数据的拷贝。这种能力由下面的内核函数提供,它们在<asm/uaccess.h>中定义,它们用于拷贝任意一段字节序列:

 

1 unsigned long copy_to_user(void __user *to, const void *from, 
2                 unsigned long n)
3 unsigned long copy_from_user(void *to, const void __user *from,
4                  unsigned long n)

 

这两个函数在调用时会检查用户空间的指针是否有效。如果不需要检查用户空间的指针,则可以调用下面两个函数:

1 unsigned long __copy_from_user(void *to, const void __user *from, unsigned long n)
2 unsigned long __copy_to_user(void __user *to, const void *from, unsigned long n)

 

由内核源码可知,copy_to_user和copy_from_user分别是对__copy_from_user和__copy_to_user的进一步封装调用。

 

七、开发板上实验

实验平台:mini2440(256M NAND)       

内核版本:友善的内核(Linux 2.6.32.2)及文件系统

模块程序:http://files.cnblogs.com/ycz9999/scull.zip

模块测试程序:http://files.cnblogs.com/ycz9999/scull_test.zip

1、量子集、量子大小使用默认值

scull_quantum = 4000

scull_qset = 1000

插入驱动模块,建立设备节点

[root@FriendlyARM 3]# ls
scull.ko scull_test
[root@FriendlyARM 3]# insmod scull.ko
[root@FriendlyARM 3]# lsmod
scull 3157 0 - Live 0xbf000000
[root@FriendlyARM 3]# cat /proc/devices
Character devices:
1 mem
4 /dev/vc/0
4 tty
5 /dev/tty
5 /dev/console
5 /dev/ptmx
7 vcs
10 misc
13 input
14 sound
21 sg
29 fb
81 video4linux
89 i2c
90 mtd
116 alsa
128 ptm
136 pts
180 usb
188 ttyUSB
189 usb_device
204 s3c2410_serial
253 scull
254 rtc

Block devices:
259 blkext
7 loop
8 sd
31 mtdblock
65 sd
66 sd
67 sd
68 sd
69 sd
70 sd
71 sd
128 sd
129 sd
130 sd
131 sd
132 sd
133 sd
134 sd
135 sd
179 mmc
[root@FriendlyARM 3]# ls /sys/module/
aircable hid_apple omninet tcp_cubic
ark3116 io_edgeport opticon tda8290
belkin_sa io_ti option tda9887
ch341 ipaq oti6858 tea5761
cp210x ipw pl2303 tea5767
cyberjack ir_usb printk ti_usb_3410_5052
cypress_m8 iuu_phoenix qcserial tuner_simple
digi_acceleport kernel safe_serial tuner_xc2028
dm9000 keyboard scsi_mod usb_storage
empeg keyspan scull usbcore
ftdi_sio keyspan_pda sg usbhid
funsoft kl5kusb105 sierra usbserial
garmin_gps kobil_sct snd uvcvideo
gspca_gl860 lockd snd_pcm v4l1_compat
gspca_m5602 mct_u232 snd_pcm_oss visor
gspca_main mos7720 snd_timer vt
gspca_mr97310a mos7840 soundcore whiteheat
gspca_ov519 mousedev spcp8x5 xc5000
gspca_stv06xx mt20xx spurious yaffs
gspca_zc3xx navman sunrpc
hid nfs symbolserial
[root@FriendlyARM 3]# mknod -m 666 /dev/scull0 c 253 0
[root@FriendlyARM 3]# mknod -m 666 /dev/scull1 c 253 1
[root@FriendlyARM 3]# mknod -m 666 /dev/scull2 c 253 2
[root@FriendlyARM 3]# mknod -m 666 /dev/scull3 c 253 3
[root@FriendlyARM 3]# ls /dev/scull*
/dev/scull0 /dev/scull1 /dev/scull2 /dev/scull3
[root@FriendlyARM 3]#

在创建设备节点时,驱动程序是动态分配的设备号,所以需要从/proc/devices中获得设备号。在申请设备号时,已经指定了起始的次设备号和注册的设备号的个数,因此在指定次设备号时,不要超出了次设备号的范围。以scull为例,驱动程序动态申请了scull_nr_devs 个(4个)设备号,且起始次设备号为0,如果要创建4个设备号连续的设备节点,则最大次设备号不能超过3。否则,执行应用程序时,代码运行失败!

 

2>启动测试程序

[root@FriendlyARM 3]# ./scull_test
write ok! code=20
read ok! code=20
[0]=0 [1]=1 [2]=2 [3]=3 [4]=4
[5]=5 [6]=6 [7]=7 [8]=8 [9]=9
[10]=10 [11]=11 [12]=12 [13]=13 [14]=14
[15]=15 [16]=16 [17]=17 [18]=18 [19]=19

[root@FriendlyARM 3]#

 

2、设置量子大小为6

scull_quantum=6

scull_qset = 1000

1>插入驱动模块

[root@FriendlyARM 3]# insmod scull.ko scull_quantum=6
[root@FriendlyARM 3]# lsmod
scull 3157 0 - Live 0xbf006000
[root@FriendlyARM 3]#

 

2>启动测试程序

[root@FriendlyARM 3]# ./scull_test
write error! code=6
write error! code=6
write error! code=6
write ok! code=2
read error! code=6
read error! code=6
read error! code=6
read ok! code=2
[0]=0 [1]=1 [2]=2 [3]=3 [4]=4
[5]=5 [6]=6 [7]=7 [8]=8 [9]=9
[10]=10 [11]=11 [12]=12 [13]=13 [14]=14
[15]=15 [16]=16 [17]=17 [18]=18 [19]=19

[root@FriendlyARM 3]#

 

3、设置量子大小为6,量子集大小为2

scull_quantum=6

scull_qset = 2

1>插入驱动模块

[root@FriendlyARM 3]# insmod scull.ko scull_quantum=6 scull_qset=2
[root@FriendlyARM 3]# lsmod
scull 3157 0 - Live 0xbf00c000
[root@FriendlyARM 3]#

 

2>启动测试程序

[root@FriendlyARM 3]# ./scull_test
write error! code=6
write error! code=6
write error! code=6
write ok! code=2
read error! code=6
read error! code=6
read error! code=6
read ok! code=2
[0]=0 [1]=1 [2]=2 [3]=3 [4]=4
[5]=5 [6]=6 [7]=7 [8]=8 [9]=9
[10]=10 [11]=11 [12]=12 [13]=13 [14]=14
[15]=15 [16]=16 [17]=17 [18]=18 [19]=19

[root@FriendlyARM 3]#

本实验测试了模块的读写能力,还测试了量子读写是否有效。但是,由于自己在内核知识上的欠缺,关于应用程序和对应驱动程序间是如何进行参数传递的,还是有很多疑问。

 

参考:

《Linux设备驱动程序(第三版)》

Tekkaman Ninja: http://blog.chinaunix.net/uid/20543672.html

posted @ 2012-03-30 14:01 ycz0926 阅读(...) 评论(...) 编辑 收藏