Linux设备驱动程序学习(3)-字符设备驱动程序
来源:互联网 发布:淘宝网站是怎么赚钱的 编辑:程序博客网 时间:2024/05/17 05:08
开始学习《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 第一个次设备号,通常是03 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 }
五、open和release
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被调用。
六、read和write
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]# lsscull.ko scull_test[root@FriendlyARM 3]# insmod scull.ko[root@FriendlyARM 3]# lsmodscull 3157 0 - Live 0xbf000000[root@FriendlyARM 3]# cat /proc/devicesCharacter 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 mtd116 alsa128 ptm136 pts180 usb188 ttyUSB189 usb_device204 s3c2410_serial253 scull254 rtcBlock devices:259 blkext 7 loop 8 sd 31 mtdblock 65 sd 66 sd 67 sd 68 sd 69 sd 70 sd 71 sd128 sd129 sd130 sd131 sd132 sd133 sd134 sd135 sd179 mmc[root@FriendlyARM 3]# ls /sys/module/aircable hid_apple omninet tcp_cubicark3116 io_edgeport opticon tda8290belkin_sa io_ti option tda9887ch341 ipaq oti6858 tea5761cp210x ipw pl2303 tea5767cyberjack ir_usb printk ti_usb_3410_5052cypress_m8 iuu_phoenix qcserial tuner_simpledigi_acceleport kernel safe_serial tuner_xc2028dm9000 keyboard scsi_mod usb_storageempeg keyspan scull usbcoreftdi_sio keyspan_pda sg usbhidfunsoft kl5kusb105 sierra usbserialgarmin_gps kobil_sct snd uvcvideogspca_gl860 lockd snd_pcm v4l1_compatgspca_m5602 mct_u232 snd_pcm_oss visorgspca_main mos7720 snd_timer vtgspca_mr97310a mos7840 soundcore whiteheatgspca_ov519 mousedev spcp8x5 xc5000gspca_stv06xx mt20xx spurious yaffsgspca_zc3xx navman sunrpchid 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_testwrite ok! code=20read 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]# lsmodscull 3157 0 - Live 0xbf006000[root@FriendlyARM 3]#
2>启动测试程序
[root@FriendlyARM 3]# ./scull_testwrite error! code=6write error! code=6write error! code=6write ok! code=2read error! code=6read error! code=6read error! code=6read 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]# lsmodscull 3157 0 - Live 0xbf00c000[root@FriendlyARM 3]#
2>启动测试程序
[root@FriendlyARM 3]# ./scull_testwrite error! code=6write error! code=6write error! code=6write ok! code=2read error! code=6read error! code=6read error! code=6read 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
- Linux设备驱动程序学习(3)-字符设备驱动程序
- Linux设备驱动程序学习(1)-字符设备驱动程序
- Linux设备驱动程序学习(1)-字符设备驱动程序
- Linux设备驱动程序学习(1)-字符设备驱动程序
- Linux设备驱动程序学习(1)-字符设备驱动程序
- Linux设备驱动程序学习(1)-字符设备驱动程序
- Linux设备驱动程序学习(1) -字符设备驱动程序
- Linux设备驱动程序学习笔记03:字符设备驱动程序I
- Linux设备驱动程序学习笔记04:字符设备驱动程序II
- Linux设备驱动程序学习笔记05:字符设备驱动程序III
- Linux设备驱动程序学习笔记06:字符设备驱动程序IV
- Linux设备驱动程序学习笔记07:字符设备驱动程序V
- linux字符设备驱动程序
- Linux 字符设备驱动程序
- linux字符设备驱动程序
- Linux驱动程序-----字符设备
- Linux字符设备驱动程序
- Linux字符设备驱动程序
- 在linux环境下实现定时计划任务
- arcgis 批量裁剪工具
- VS2008快捷键大全
- JQuery音视频播放插件Jplayer
- Linux程序设计入门
- Linux设备驱动程序学习(3)-字符设备驱动程序
- ireport_几种不同的数据源_实现报表制作
- hdu-求数列的和
- pthread_join/pthread_exit用法实例
- hdu 1241 最基本的DFS题目
- IOS---------NSDateFormatter的格式字符串
- 浅谈导航数据中POI搜索技术原理
- 二叉树中的递归理解
- hdu-水仙花数