cdev结构体及其相关函数
1、在 linux 2.6内核中,使用 cdev结构体描述字符设备,cdev 的定义在 <linux/cdev.h> 中可找到,其定义如下:
[cpp]
1. /*include/linux/cdev.h*/
[cpp]
1. struct cdev {
2. struct kobject kobj; //内嵌的kobject对象
3. struct module *owner; //所属模块
4. const struct file_operations *ops;
5. //文件操作结构,在写驱动时,其结构体内的大部分函数要被实现
6. struct list_head list;
7. dev_t dev; //设备号,int 类型,高12位为主设备号,低20位为次设备号
8. unsigned int count;
9. };
可以使用如下宏调用来获得主、次设备号:
MAJOR(dev_t dev)
MINOR(dev_t dev)
MKDEV(int major,int minor) //通过主次设备号来生成dev_t
以上宏调用在内核源码中如此定义:
[cpp]
1. #define MINORBITS 20
2. #define MINORMASK ((1U << MINORBITS) - 1)
3. //(1<<20 -1) 此操作后,MINORMASK宏的低20位为1,高12位为0
4.
5. #define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))
6. #define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))
7. #define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))
2、下面一组函数用来对cdev结构体进行操作:
[cpp]
1. void cdev_init(struct cdev *, const struct file_operations *);//初始化,建立cdev和file_operation 之间的连接
2.
3. struct cdev *cdev_alloc(void); //动态申请一个cdev内存
4.
5. void cdev_put(struct cdev *p); //释放
6.
7. int cdev_add(struct cdev *, dev_t, unsigned); //注册设备,通常发生在驱动模块的加载函数中
8.
9. void cdev_del(struct cdev *);//注销设备,通常发生在驱动模块的卸载函数中
3、在调用cdev_add()函数向系统注册字符设备之前,应首先调用register_chrdev_region()或alloc_chrdev_region()来向系统申请设备号:
[cpp]
1. int register_chrdev_region(dev_t from,unsigned count,const char *name)
[cpp]
1. int alloc_chrdev_region(dev_t *dev,unsigned baseminor,unsigned count,const char *name)
函数代替,他们之间的区别在于:register_chrdev_region()函数用于已知起始设备的设备号时,而alloc_chrdev_region()设备号未知,向系统动态申请未被占用的设备号的情况,函数调用成功后,会把得到的设备号放入第一个参数dev中,优点在于不会造成设备号重复的冲突。
在调用cdev_del函数从系统注销字符设备之后,应调用unregister_chrdev_region():
[cpp]
1. void unregister_chrdev_region(dev_t from,unsigned count)
函数释放原先申请的设备号。
他们之间的顺序关系如下:
register_chrdev_region()-->cdev_add() //此过程在加载模块中
cdev_del()-->unregister_chrdev_region() //此过程在卸载模块中
有两个方法可以分配并初始化 cedv 结构。如果希望在运行时动态的获得一个独立的 cdev 结构,可以如下这么做:
[cpp]
1. struct cdev *my_cdev = cdev_alloc();
2. my_cdev->ops = &my_fops;
cdev_alloc(void) 函数的代码为(对 cdev 结构体操作的系列函数可在 fs/char_dev.c 中找到):
[cpp]
1. struct cdev *cdev_alloc(void)
2. {
3. struct cdev *p = kzalloc(sizeof(struct cdev), GFP_KERNEL);
4. if (p) {
5. INIT_LIST_HEAD(&p->list);
6. kobject_init(&p->kobj, &ktype_cdev_dynamic);
7. }
8. return p;
9. }
cdev_alloc() 的源代码可能由于内核版本号的不同而有差别(上面的代码为 2.6.30)
有时可能希望就把 cdev 结构内嵌在自己的特定设备结构里,那么在分配好 cdev 结构后,就用 cdev_init() 函数对其初始化:
[cpp]
1. void cdev_init (struct cdev *cdev, struct file_operations *fops)
cdev_init() 函数代码为:
[cpp]
1. void cdev_init(struct cdev *cdev, const struct file_operations *fops)
2. {
3. memset(cdev, 0, sizeof *cdev);
4. INIT_LIST_HEAD(&cdev->list);
5. kobject_init(&cdev->kobj, &ktype_cdev_default);
6. cdev->ops = fops; //将传入的文件操作结构体指针赋值给cdev的ops
7. }
另外,像 cdev 中的 owner 要设置为 THIS_MOULE 。
一旦 cdev 结构体设置完毕,最后一步就是要把这事告诉给内核,使用下面的函数:
[cpp]
1. int cdev_add(struct cdev *p, dev_t dev, unsigned count)
cdev_add() 对应的代码为:
[cpp]
1. /**
2. * cdev_add() - add a char device to the system
3. * @p: the cdev structure for the device
4. * @dev: the first device number for which this device is responsible
5. * @count: the number of consecutive minor numbers corresponding to this
6. * device
7. *
8. * cdev_add() adds the device represented by @p to the system, making it
9. * live immediately. A negative error code is returned on failure.
10. */
11. int cdev_add(struct cdev *p, dev_t dev, unsigned count)
12. {
13. p->dev = dev;
14. p->count = count;
15. return kobj_map(cdev_map, dev, count, NULL, exact_match, exact_lock, p);
16. }
参数 p 是 cdev 结构体的指针;
参数 dev 是设备响应的第一个设备号;
参数 count 和设备相关联的设备号的数目。
一般的,count 的值为 1,但是有些情形也可能是大于 1 的数。比如 SCSI 磁带机,它通过给每个物理设备安排多个此设备号来允许用户在应用程序里选择操作模式(比如密度)。
cdev_add 如果失败了,那么返回一个负值,表明驱动无法加载到系统中。然而它一般情况下都会成功,一旦 cdev_add 返回,设备也就 “活” 了起来,于是所对应的操作方法(file_operations 结构里所定义的各种函数)也就能为内核所调用。
从系统中移除一个字符设备,可以调用:
[cpp]
1. void cdev_del(struct cdev *p)
老版本的字符设备注册与注销
在许多驱动程序代码里,会看到许多字符设备驱动并没有用 cdev 这个接口。这是一种老式的方法,但新写的代码应该使用 cdev 接口。
用于注册字符设备驱动程序的老式函数 register_chrdev() 函数定义如下:
[cpp]
1. int register_chardev (unsigned int major, const char *name, struct file_operations *fops)
利用该函数注册时,应先定义好主设备号、设备驱动程序的名称、file_operations 结构体的变量。
应用程序中利用设备文件搜索设备驱动程序的时候使用主设备号 (major) 。
在内核中表示 proc 文件系统或错误代码时,使用设备驱动程序名称。
另外,利用 unregister_chrdev() 函数注销字符设备驱动程序时,可以作为区分标志。注册函数中关键的地方是定义 file_operations 结构体变量的地址。
所谓注册字符设备驱动程序,应理解为在内核中注册与主设备号相关的 file_operations 结构体。
register_chrdev() 函数注册完设备驱动程序,把定义主设备号的 major 设置为 0,返回注册的主设备号(动态分配),把已知的主设备号设为 major 值时,返回 0 (人工指定)。注册失败时,返回负值
从内核中注销字符设备驱动程序的 unregister_chrdev() 函数形式如下:
[cpp]
1. int unregister_chrdev (unsigned int major, const char *name)
该函数中使用主设备号(major) 和设备驱动程序名称 (name) 与 register_chrdev 函数中使用的值相同,因为内核会把这些参数作为注销字符设备驱动程序的基准对比两个设定内容。从内核成功注销了字符设备驱动程序时,返回 0 ,失败则返回负值。
file_operations结构体分析
Linxu驱动中的设备文件注册的操作方法结构体,也是向用户层提供操作接口的方法体,我的版本为3.1.10。原型在内核源码 /include/linux/fs.h中定义:
1. struct file_operations {
2. struct module *owner;
3. //第一个 file_operations 成员根本不是一个操作; 它是一个指向拥有这个结构的模块的指针. 这个成员用来在它的操作还在被使用时阻止模块被卸载. 几乎所有时间中, 它被简单初始化为 THIS_MODULE, 一个在 <linux/module.h> 中定义的宏.
4. loff_t (*llseek) (struct file *, loff_t, int);
5. //llseek 方法用作改变文件中的当前读/写位置, 并且新位置作为(正的)返回值. loff_t 参数是一个"long offset", 并且就算在 32位平台上也至少 64 位宽. 错误由一个负返回值指示. 如果这个函数指针是 NULL, seek 调用会以潜在地无法预知的方式修改 file 结构中的位置计数器( 在"file 结构" 一节中描述).
6. ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
7. //用来从设备中获取数据. 在这个位置的一个空指针导致 read 系统调用以 -EINVAL("Invalid argument") 失败. 一个非负返回值代表了成功读取的字节数( 返回值是一个 "signed size" 类型, 常常是目标平台本地的整数类型).
8. ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
9. //发送数据给设备. 如果 NULL, -EINVAL 返回给调用 write 系统调用的程序. 如果非负, 返回值代表成功写的字节数.
10. ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
11. //初始化一个异步读 -- 可能在函数返回前不结束的读操作. 如果这个方法是 NULL, 所有的操作会由 read 代替进行(同步地).
12. ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
13. //初始化设备上的一个异步写.
14. int (*readdir) (struct file *, voidvoid *, filldir_t);
15. //对于设备文件这个成员应当为 NULL; 它用来读取目录, 并且仅对文件系统有用.
16. unsigned int (*poll) (struct file *, struct poll_table_struct *);
17. //poll 方法是 3 个系统调用的后端: poll, epoll, 和 select, 都用作查询对一个或多个文件描述符的读或写是否会阻塞. poll 方法应当返回一个位掩码指示是否非阻塞的读或写是可能的, 并且, 可能地, 提供给内核信息用来使调用进程睡眠直到 I/O 变为可能. 如果一个驱动的 poll 方法为 NULL, 设备假定为不阻塞地可读可写.
18. long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
19. long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
20. // 2.6.35 之后的版本只有这两个ioctl,少了inode参数,功能不变 表示系统调用提供了发出设备特定命令的方法
21. int (*mmap) (struct file *, struct vm_area_struct *);
22. //mmap 用来请求将设备内存映射到进程的地址空间. 如果这个方法是 NULL, mmap 系统调用返回 -ENODEV.
23. int (*open) (struct inode *, struct file *);
24. //尽管这常常是对设备文件进行的第一个操作, 不要求驱动声明一个对应的方法. 如果这个项是 NULL, 设备打开一直成功, 但是你的驱动不会得到通知.
25. int (*flush) (struct file *, fl_owner_t id);
26. //flush 操作在进程关闭它的设备文件描述符的拷贝时调用; 它应当执行(并且等待)设备的任何未完成的操作. 这个必须不要和用户查询请求的 fsync 操作混淆了. 当前, flush 在很少驱动中使用; SCSI 磁带驱动使用它, 例如, 为确保所有写的数据在设备关闭前写到磁带上. 如果 flush 为 NULL, 内核简单地忽略用户应用程序的请求.
27. int (*release) (struct inode *, struct file *);
28. //在文件结构被释放时引用这个操作. 如同 open, release 可以为 NULL.
29. int (*fsync) (struct file *, loff_t, loff_t, int datasync);
30. //这个方法是 fsync 系统调用的后端, 用户调用来刷新任何挂着的数据. 如果这个指针是 NULL, 系统调用返回 -EINVAL.
31. int (*aio_fsync) (struct kiocb *, int datasync);
32. //这是 fsync 方法的异步版本.
33. int (*fasync) (int, struct file *, int);
34. //这个操作用来通知设备它的 FASYNC 标志的改变. 异步通知是一个高级的主题, 在第 6 章中描述. 这个成员可以是NULL 如果驱动不支持异步通知.
35. int (*lock) (struct file *, int, struct file_lock *);
36. //lock 方法用来实现文件加锁; 加锁对常规文件是必不可少的特性, 但是设备驱动几乎从不实现它.
37. ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
38. //sendpage 是 sendfile 的另一半; 它由内核调用来发送数据, 一次一页, 到对应的文件. 设备驱动实际上不实现 sendpage.
39. unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
40. //这个方法的目的是在进程的地址空间找一个合适的位置来映射在底层设备上的内存段中. 这个任务通常由内存管理代码进行; 这个方法存在为了使驱动能强制特殊设备可能有的任何的对齐请求. 大部分驱动可以置这个方法为 NULL.
41. int (*check_flags)(int);
42. //这个方法允许模块检查传递给 fnctl(F_SETFL...) 调用的标志.
43. int (*flock) (struct file *, int, struct file_lock *);
44. //文件加锁
45. ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
46. //由VFS调用,将管道数据粘接到文件。
47. ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
48. //由VFS调用,将文件数据粘接到管道
49. //用于从管道传输文件数据,目前只用于splice2系统调用;
50. int (*setlease)(struct file *, long, struct file_lock **);
51. // 设置一个打开标志到open的file
52. long (*fallocate)(struct file *file, int mode, loff_t offset,
53. loff_t len);
54. // 通过设置mode标志FA_ALLOCATE或FA_DEALLOCATE,表示preallocation and deallocation of preallocated blocks respectively,由sys_fallocate调用。
55. };
Linux字符设备驱动的组成
在Linux中,字符设备驱动由以下几个部分组成:
1、字符设备驱动模块加载于卸载函数
在字符设备驱动模块加载函数中应该实现设备号的申请和cdev的注册,而在卸载函数中应实现设备号的释放和cdev的注销
1 /* 设备结构体 2 struct xxx_dev_t { 3 struct cdev cdev; 4 ... 5 } xxx_dev; 6 /* 设备驱动模块加载函数 7 static int _ _init xxx_init(void) 8 { 9 ... 10 cdev_init(&xxx_dev.cdev,&xxx_fops);/*初始化cdev fops用来建立与cdev的连接 11 xxx_dev.cdev.owner = THIS_ _MODULE; 12 /* 获取字符设备号*/ 13 if (xxx_major) { 14 register_chrdev_region(xxx_dev_no, 1, DEV_NAME); 15 } else { 16 alloc_chrdev_region(&xxx_dev_no, 0, 1, DEV_NAME); 17 } 18 19 ret = cdev_add(&xxx_dev.cdev, xxx_dev_no, 1); /* 注册设备*/ 20 ... 21 } 22 /*设备驱动模块卸载函数*/ 23 static void _ _exit xxx_exit(void) 24 { 25 unregister_chrdev_region(xxx_dev_no, 1); /*释放占用的设备号*/ 26 cdev_del(&xxx_dev.cdev); /* 注销设备*/ 27 ... 28 }
|
2、字符设备驱动的file_operation体中的成员函数
file_operation结构体中成员函数是字符设备驱动与内核的接口,是用户空间对Linux进行系统调用最终的落实者。大多数字符设备驱动会实现read()、write()、ioctl()函数。
1 /* 读设备*/ 2 ssize_t xxx�read(struct file *filp, char _user *buf, size_t count, 3 loff_t*f_pos) 4 { 5 ... 6 copy_to_user(buf, ..., ...); 7 ... 8 } 9 /* 写设备*/ 10 ssize_t xxx_write(struct file *filp, const char _user *buf, size_t count, 11 loff_t *f_pos) 12 { 13 ... 14 copy_from_user(..., buf, ...); //内核空间到用户空间 15 ... 16 } 17 /* ioctl函数 */ 18 int xxx_ioctl(struct inode *inode, struct file *filp, unsigned int cmd, 19 unsigned long arg) 20 { 21 ... 22 switch (cmd) { 23 case XXX_CMD1: 24 ... 25 break; 26 case XXX_CMD2: 27 ... 28 break; 29 default: 30 /*不能支持的命令*/ 31 return - ENOTTY; 32 } 33 return 0; 34 } |
设备驱动的读写函数中,filp是文件结构体指针,buf是用户空间内存的地址(该地址在内核空间不能直接读写),count是要写的字节数,f_ops是读/写的位置相对于文件开头的偏移。
由于内核空间和用户空间不能直接互访,故借用函数copy_from_user()完成用户空间到内核空间的拷贝,以及copy_to_user完成内核空间到用户空间的拷贝。
两个函数的原型为:
unsigned long copy_from_user(void *to, const void _user *from, unsigned long count); unsigned long copy_to_user(void _user *to, const void *from, unsigned long count; |
若要复制的内存是简单类型,如:char,int,long等,则可以使用简单的put_user()和get_user()。
int val; /*内核空间整形变量 ... get_user(val, (int *) arg); /* 用户到内核,arg是用户空间的地址 ... put_user(val, (int *) arg); /*内核到用户,arg是用户空间的地址 |
在字符设备驱动中,要定义一个file_operations的实例,并将具体设备驱动的函数赋值给file_operations的成员。
1 struct file_operations xxx_fops = { 2 .owner = THIS_MODULE, 3 .read = xxx_read, 4 .write = xxx_write, 5 .ioctl = xxx_ioctl, 6 ... 7 }; |
在字符设备驱动模块加载与卸载函数模板中的cdev_init(&xxx_dev.cdev,&xxx_fops);语句可建立与cdev的连接。
字符设备驱动的结构如下:
设备驱动程序编写流程:
设备驱动程序可以使用模块的方式加载的方式加载到内核中去,驱动开发时时没有main()函数,模块在调用insmod命令时被加载,此时的入口点式init_module()函数,通常在该函数中完成设备的注册。同样,模块在调用rmmod命令时被卸载,此时的入口点是cleanup_module()函数,在该函数中完成设备的卸载。在设备完成注册加载之后,用户的应用程序就可以对该设备进行一定的操作,如open()、read()、write()等,而驱动程序就是用于实现这些操作,在用户应用程序调用相应入口函数时执行相关的操作。
设备功能是由file_operation()函数定义的。
proc文件系统
/proc文件系统是一个伪文件系统,它是一种内核和内核模块用来向进程发送信息的机制。这个伪文件系统让用户可以和内核内部数据结构进行交互,获取有关系统和进程的有用信息,在运行时通过改变内核参数来改变设置。与其他文件系统不同,/proc存在于内存之中而不是在硬盘之中。/proc文件系统体现了内核及进程运行的内容,在加载模块成功后,读者可以通过查看/proc/device文件获得相关设备的主设备号
globalmem字符设备驱动
globalmem流程图:
globalmem设备是一个虚拟设备,同时意味着“全局内存”,在globalmem字符设备驱动中会分配一片大小GLOBALMEM_SIZE(4KB)的内存空间,并在驱动中提供针对这片内存的读写、控制和定位函数,以供用户空间的进程能通过Linux系统调用访问这片内存。
globalmem可被两个或两个以上的进程同时访问,其中全局内存可作为用户空间进程进行通信的一种蹩脚手段。
源码:
globalmen模块
[cpp]
1. #include<linux/module.h>
2. #include<linux/types.h>
3. #include<linux/fs.h>
4. #include<linux/errno.h>
5. #include<linux/mm.h>
6. #include<linux/sched.h>
7. #include<linux/init.h>
8. #include<linux/cdev.h>
9. #include<linux/slab.h>
10. #include<linux/kdev_t.h>
11. #include<linux/device.h>
12. #include <asm/io.h>
13. #include<asm/system.h>
14. #include<asm/uaccess.h>
15.
16. #define DEVICE_NAME "globalmem"
17. #defineGLOBALMEM_SIZE 0x1000 /*全局内存最大4K字节*/
18. #define MEM_CLEAR0x1 /*清0全局内存*/
19. #define GLOBALMEM_MAJOR241 /*预设的globalmem的主设备号*/
20.
21. static intglobalmem_major = GLOBALMEM_MAJOR;
22. /*globalmem设备结构体*/
23. struct globalmem_dev
24. {
25. struct cdev cdev; /*cdev结构体*/
26. unsigned char mem[GLOBALMEM_SIZE];/*全局内存*/
27. };
28.
29. struct globalmem_dev*globalmem_devp; /*设备结构体指针*/
30.
31. static struct class*globalmem0_class;
32. static struct class*globalmem1_class;
33.
34. /*文件打开函数*/
35. int globalmem_open(structinode *inode, struct file *filp)
36. {
37. /*将设备结构体指针赋值给文件私有数据指针*/
38. struct globalmem_dev *dev;
39.
40. dev = container_of(inode->i_cdev,structglobalmem_dev,cdev);
41. filp->private_data = dev;
42. return 0;
43. }
44. /*文件释放函数*/
45. intglobalmem_release(struct inode *inode, struct file *filp)
46. {
47. return 0;
48. }
49.
50. /* ioctl设备控制函数 */
51. static intglobalmem_ioctl(struct inode *inodep, struct file *filp, unsigned
52. int cmd, unsigned long arg)
53. {
54. struct globalmem_dev *dev =filp->private_data;/*获得设备结构体指针*/
55.
56. switch (cmd)
57. {
58. case MEM_CLEAR:
59. memset(dev->mem, 0,GLOBALMEM_SIZE);
60. printk(KERN_INFO "globalmem is setto zero\n");
61. break;
62.
63. default:
64. return - EINVAL;
65. }
66. return 0;
67. }
68.
69. /*读函数*/
70. static ssize_tglobalmem_read(struct file *filp, char __user *buf, size_t size,
71. loff_t *ppos)
72. {
73. unsigned long p = *ppos;
74. unsigned int count = size;
75. int ret = 0;
76. struct globalmem_dev *dev =filp->private_data; /*获得设备结构体指针*/
77.
78. /*分析和获取有效的写长度*/
79. if (p >= GLOBALMEM_SIZE)
80. return count ? - ENXIO: 0;
81. if (count > GLOBALMEM_SIZE - p)
82. count = GLOBALMEM_SIZE - p;
83.
84. /*内核空间->用户空间*/
85. if (copy_to_user(buf, (void*)(dev->mem +p), count))
86. {
87. ret = - EFAULT;
88. }
89. else
90. {
91. *ppos += count;
92. ret = count;
93.
94. printk(KERN_INFO "read %d bytes(s)from %d\n", count, p);
95. }
96.
97. return ret;
98. }
99.
100. /*写函数*/
101. static ssize_tglobalmem_write(struct file *filp, const char __user *buf,
102. size_t size, loff_t *ppos)
103. {
104. unsigned long p = *ppos;
105. unsigned int count = size;
106. int ret = 0;
107. struct globalmem_dev *dev =filp->private_data; /*获得设备结构体指针*/
108.
109. /*分析和获取有效的写长度*/
110. if (p >= GLOBALMEM_SIZE)
111. return count ? - ENXIO: 0;
112. if (count > GLOBALMEM_SIZE - p)
113. count = GLOBALMEM_SIZE - p;
114.
115. /*用户空间->内核空间*/
116. if (copy_from_user(dev->mem + p, buf,count))
117. ret = - EFAULT;
118. else
119. {
120. *ppos += count;
121. ret = count;
122.
123. printk(KERN_INFO "written %d bytes(s)from %d\n", count, p);
124. }
125.
126. return ret;
127. }
128.
129. /* seek文件定位函数 */
130. static loff_tglobalmem_llseek(struct file *filp, loff_t offset, int orig)
131. {
132. loff_t ret = 0;
133. switch (orig)
134. {
135. case 0: /*相对文件开始位置偏移*/
136. if (offset < 0)
137. {
138. ret = - EINVAL;
139. break;
140. }
141. if ((unsigned int)offset >GLOBALMEM_SIZE)
142. {
143. ret = - EINVAL;
144. break;
145. }
146. filp->f_pos = (unsigned int)offset;
147. ret = filp->f_pos;
148. break;
149. case 1: /*相对文件当前位置偏移*/
150. if ((filp->f_pos + offset) >GLOBALMEM_SIZE)
151. {
152. ret = - EINVAL;
153. break;
154. }
155. if ((filp->f_pos + offset) < 0)
156. {
157. ret = - EINVAL;
158. break;
159. }
160. filp->f_pos += offset;
161. ret = filp->f_pos;
162. break;
163. default:
164. ret = - EINVAL;
165. break;
166. }
167. return ret;
168. }
169.
170. /*文件操作结构体*/
171. static const structfile_operations globalmem_fops =
172. {
173. .owner = THIS_MODULE,
174. .llseek = globalmem_llseek,
175. .read = globalmem_read,
176. .write = globalmem_write,
177. .ioctl = globalmem_ioctl,
178. .open = globalmem_open,
179. .release = globalmem_release,
180. };
181.
182. /*初始化并注册cdev*/
183. static voidglobalmem_setup_cdev(struct globalmem_dev *dev, int index)
184. {
185. int err, devno = MKDEV(globalmem_major,index);
186.
187. cdev_init(&dev->cdev,&globalmem_fops);
188. dev->cdev.owner = THIS_MODULE;
189. dev->cdev.ops = &globalmem_fops;
190. err = cdev_add(&dev->cdev, devno, 1);
191. if (err)
192. printk(KERN_NOTICE "Error %d addingLED%d", err, index);
193. }
194.
195. /*设备驱动模块加载函数*/
196. int globalmem_init(void)
197. {
198. int result;
199. dev_t devno = MKDEV(globalmem_major, 0);
200.
201. /* 申请设备号*/
202. if (globalmem_major)
203. result = register_chrdev_region(devno, 2,DEVICE_NAME);
204. else /* 动态申请设备号 */
205. {
206. result = alloc_chrdev_region(&devno, 0,2, DEVICE_NAME);
207. globalmem_major = MAJOR(devno);
208. }
209. if (result < 0)
210. return result;
211.
212. /* 动态申请2个设备结构体的内存*/
213. globalmem_devp = kmalloc(2*sizeof(structglobalmem_dev), GFP_KERNEL);
214. if (!globalmem_devp) /*申请失败*/
215. {
216. result = - ENOMEM;
217. goto fail_malloc;
218. }
219. memset(globalmem_devp, 0, 2*sizeof(structglobalmem_dev));
220.
221. globalmem_setup_cdev(&globalmem_devp[0],0);
222. globalmem_setup_cdev(&globalmem_devp[1],1);
223.
224. //注册一个类,使mdev可以在/dev/下面建立设备节点
225. globalmem0_class =class_create(THIS_MODULE, "globalmem0");
226. if( IS_ERR(globalmem0_class) )
227. {
228. printk(KERN_NOTICE, "creatglobalmem0_class failed!");
229. return -1;
230. }
231.
232. //创建一个设备节点,节点名字为globalmem0
233. device_create(globalmem0_class, NULL,MKDEV(globalmem_major, 0), NULL, "globalmem0");
234. printk(KERN_NOTICE, "globalmem0initialized!");
235.
236. globalmem1_class =class_create(THIS_MODULE, "globalmem1");
237. if( IS_ERR(globalmem1_class) )
238. {
239. printk(KERN_NOTICE, "creatglobalmem1_class failed!");
240. return -1;
241. }
242.
243. //创建一个设备节点,节点名字为globalmem1
244. device_create(globalmem1_class, NULL,MKDEV(globalmem_major, 1), NULL, "globalmem1");
245. printk(KERN_NOTICE, "globalmem1initialized!");
246.
247. return 0;
248.
249. fail_malloc: unregister_chrdev_region(devno,2);
250. return result;
251. }
252.
253. /*模块卸载函数*/
254. void globalmem_exit(void)
255. {
256. cdev_del(&(globalmem_devp[0].cdev));
257. cdev_del(&(globalmem_devp[1].cdev)); /*注销cdev*/
258. kfree(globalmem_devp); /*释放设备结构体内存*/
259. unregister_chrdev_region(MKDEV(globalmem_major, 0), 2); /*释放设备号*/
260. class_destroy(globalmem0_class);
261. class_destroy(globalmem1_class);
262. }
263.
264. MODULE_AUTHOR("SongBaohua");
265. MODULE_LICENSE("DualBSD/GPL");
266.
267. module_param(globalmem_major,int, S_IRUGO);
268.
269. module_init(globalmem_init);
270. module_exit(globalmem_exit);
测试程序:
[cpp]
1. #include <stdio.h>
2. #include <stdlib.h>
3. #include <time.h>
4. #include <unistd.h>
5. #include<linux/fcntl.h>
6.
7. int main()
8. {
9. int fd;
10. char buf1[]="testglobalmen data!";//写入memdev设备的内容
11. char buf_read[4096]; //memdev设备的内容读入到该buf中
12.
13. if((fd=open("/dev/globalmem0",O_RDWR))==-1)//打开memdev设备
14. {
15. printf("open globalmem0 WRONG!\n");
16. return 0;
17. }
18.
19. int count = 0;
20. while(count<=100)
21. {
22. printf("openglobalmem0 SUCCESS!\n");
23. printf("buf is %s\n",buf1);
24. write(fd,buf1,sizeof(buf1));//把buf中的内容写入memdev设备
25. lseek(fd,0,SEEK_SET); //把文件指针重新定位到文件开始的位置
26. read(fd,buf_read,sizeof(buf1));//把memdev设备中的内容读入到buf_read中
27. printf("buf_read is %s\n",buf_read);
28. count++;
29. }
30.
31. close(fd);
32. return 0;
33. }
34.
分析:
一.MKDEV
[cpp]
1. /linux/kdev_t.h
2. #define _LINUX_KDEV_T_H
3. 3#ifdef __KERNEL__
4. 4#define MINORBITS 20
5. 5#define MINORMASK ((1U << MINORBITS) - 1)
6. 6
7. 7#define MAJOR(dev) ((unsigned int) ((dev) >>MINORBITS))
8. 8#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))
9. 9#define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))
当编译内核的时候,__KERNEL__在命令行被定义
When you compile your kernel, __KERNEL__ isdefined on the command line.
User-space programs need access tothe kernel headers, but some of the info in kernel headers is intended only forthe kernel. Wrapping some statements in an #ifdef __KERNEL__/#endif blockensures that user-space programs don't see those statements.
主设备号高12位,次设备号低20位
二.kmalloc和vmalloc的区别
1.kmalloc
1>kmalloc内存分配和malloc相似,除非被阻塞否则他执行的速度非常快,而且不对获得空间清零.在用kmalloc申请函数后,要对起清零.用memset()函数对申请的内存进行清零。
2>kamlloc函数原型:
#include<linux/slab.h>
void *kmalloc(size_t size, intflags);
(1)第一个参数是要分配的块的大小
(2)第二个参数是分配标志(flags),他提供了多种kmalloc的行为。
(3)第三个最常用的GFP_KERNEL;
A.表示内存分配(最终总是调用get_free_pages来实现实际的分配;这就是GFP前缀的由来)是代表运行在内核空间的进程执行的。使用GFP_KERNEL容许kmalloc在分配空闲内存时候如果内存不足容许把当前进程睡眠以等待。因此这时分配函数必须是可重入的。如果在进程上下文之外如:中断处理程序、tasklet以及内核定时器中这种情况下current进程不该睡眠,驱动程序该使用GFP_ATOMIC.
B.GFP_ATOMIC
用来从中断处理和进程上下文之外的其他代码中分配内存. 从不睡眠.
C.GFP_KERNEL
内核内存的正常分配. 可能睡眠.
D.GFP_USER
用来为用户空间页来分配内存; 它可能睡眠.
E.GFP_HIGHUSER
如同 GFP_USER, 但是从高端内存分配, 如果有. 高端内存在下一个子节描述.
F.GFP_NOFS,GFP_NOIO
这个标志功能如同 GFP_KERNEL, 但是它们增加限制到内核能做的来满足请求. 一个 GFP_NOFS 分配不允许进行任何文件系统调用, 而 GFP_NOIO 根本不允许任何 I/O 初始化. 它们主要地用在文件系统和虚拟内存代码, 那里允许一个分配睡眠, 但是递归的文件系统调用会是一个坏注意.
上面列出的这些分配标志可以是下列标志的相或来作为参数, 这些标志改变这些分配如何进行:
__GFP_DMA
这个标志要求分配在能够 DMA 的内存区. 确切的含义是平台依赖的并且在下面章节来解释.
__GFP_HIGHMEM
这个标志指示分配的内存可以位于高端内存.
__GFP_COLD
正常地, 内存分配器尽力返回"缓冲热"的页 -- 可能在处理器缓冲中找到的页. 相反, 这个标志请求一个"冷"页, 它在一段时间没被使用. 它对分配页作 DMA 读是有用的, 此时在处理器缓冲中出现是无用的. 一个完整的对如何分配 DMA 缓存的讨论看"直接内存存取"一节在第 1 章.
__GFP_NOWARN
这个很少用到的标志阻止内核来发出警告(使用 printk), 当一个分配无法满足.
__GFP_HIGH
这个标志标识了一个高优先级请求, 它被允许来消耗甚至被内核保留给紧急状况的最后的内存页.
Ø __GFP_REPEAT
__GFP_NOFAIL
__GFP_NORETRY
这些标志修改分配器如何动作, 当它有困难满足一个分配. __GFP_REPEAT 意思是" 更尽力些尝试" 通过重复尝试 -- 但是分配可能仍然失败. __GFP_NOFAIL 标志告诉分配器不要失败; 它尽最大努力来满足要求. 使用__GFP_NOFAIL 是强烈不推荐的; 可能从不会有有效的理由在一个设备驱动中使用它. 最后, __GFP_NORETRY告知分配器立即放弃如果得不到请求的内存.
Ø 内存区段
__GFP_DMA和__GFP_HIGHMEM的使用与平台相关,Linux把内存分成3个区段:可用于DMA的内存、常规内存、以及高端内存。X86平台上ISA设备DMA区段是内存的前16MB,而PCI设备无此限制。
内存区后面的机制在 mm/page_alloc.c 中实现, 而内存区的初始化在平台特定的文件中, 常常在 arch 目录树的 mm/init.c。
3>kamlloc的使用方法:
Linux 处理内存分配通过创建一套固定大小的内存对象池. 分配请求被这样来处理, 进入一个持有足够大的对象的池子并且将整个内存块递交给请求者. 驱动开发者应当记住的一件事情是, 内核只能分配某些预定义的, 固定大小的字节数组.
如果你请求一个任意数量内存, 你可能得到稍微多于你请求的, 至多是 2 倍数量. 同样, 程序员应当记住kmalloc 能够处理的最小分配是 32 或者 64 字节, 依赖系统的体系所使用的页大小. kmalloc 能够分配的内存块的大小有一个上限. 这个限制随着体系和内核配置选项而变化. 如果你的代码是要完全可移植, 它不能指望可以分配任何大于 128 KB. 如果你需要多于几个 KB, 但是, 有个比 kmalloc 更好的方法来获得内存。在设备驱动程序或者内核模块中动态开辟内存,不是用malloc,而是kmalloc ,vmalloc,或者用get_free_pages直接申请页。释放内存用的是kfree,vfree,或free_pages. kmalloc函数返回的是虚拟地址(线性地址). kmalloc特殊之处在于它分配的内存是物理上连续的,这对于要进行DMA的设备十分重要. 而用vmalloc分配的内存只是线性地址连续,物理地址不一定连续,不能直接用于DMA.
注意kmalloc最大只能开辟128k-16,16个字节是被页描述符结构占用了。kmalloc用法参见khg.
内存映射的I/O口,寄存器或者是硬件设备的RAM(如显存)一般占用F0000000以上的地址空间。在驱动程序中不能直接访问,要通过kernel函数vremap获得重新映射以后的地址。
另外,很多硬件需要一块比较大的连续内存用作DMA传送。这块内存需要一直驻留在内存,不能被交换到文件中去。但是kmalloc最多只能开辟大小为32XPAGE_SIZE的内存,一般的PAGE_SIZE=4kB,也就是128kB的大小的内存。
3.kmalloc和vmalloc的区别
• vmalloc()与 kmalloc()都可用于分配内存
• kmalloc()分配的内存处于3GB~high_memory之 间,这段内核空间与物理内存的映射一一对应
•vmalloc()分配的内存在 VMALLOC_START~4GB之间,这段非连续内 存区映射到物理内存也可能是非连续的
• 在内核空间中调用kmalloc()分配连续物理空间,而调用vmalloc()分配非物理连续空间。
• 把kmalloc()所分配内核空间中的地址称为内核逻辑地址
• 把vmalloc()分配的内核空间中的地址称 为内核虚拟地址
• vmalloc()在分配过程中须更新内核页表
总结:
1.kmalloc和vmalloc分配的是内核的内存,malloc分配的是用户的内存
2.kmalloc保证分配的内存在物理上是连续的,kmalloc()分配的内存在0xBFFFFFFF-0xFFFFFFFF以上的内存中,driver一般是用它来完成对DS的分配,更适合于类似设备驱动的程序来使用;
3.vmalloc保证的是在虚拟地址空间上的连续,vmalloc()则是位于物理地址非连续,虚地址连续区,起始位置由VMALLOL_START来决定,一般作为交换区、模块的分配。
3.kmalloc能分配的大小有限,vmalloc和malloc能分配的大小相对较大(因为vmalloc还可以处理交换空间)。
4.内存只有在要被DMA访问的时候才需要物理上连续,vmalloc比kmalloc要慢
5.vmalloc使用的正确场合是分配一大块,连续的,只在软件中存在的,用于缓冲的内存区域。不能在微处理器之外使用。
6.vmalloc 中调用了 kmalloc (GFP—KERNEL),因此也不能应用于原子上下文。
7.kmalloc和 kfree管理内核段内分配的内存,这是真实地址已知的实际物理内存块。
8.vmalloc对应于vfree,分配连续的虚拟内存,但是物理上不一定连续。
9.kmalloc分配内存是基于slab,因此slab的一些特性包括着色,对齐等都具备,性能较好。物理地址和逻辑地址都是连续的
三.module_param
1.为什么引入
在用户态下编程可以通过main()来传递命令行参数,而编写一个内核模块则可通过module_param()来传递命令行参数.
2.module_param宏是Linux 2.6内核中新增的,该宏被定义在include/linux/moduleparam.h文件中,具体定义如下:
[cpp] view plain copy print?
1. /* Helper functions: type is byte, short, ushort, int, uint, long,
2. ulong,charp, bool or invbool, or XXX if you define param_get_XXX,
3. param_set_XXX and param_check_XXX.
4. */
5. #define module_param_named(name, value, type,perm)
6. param_check_##type(name,&(value));
7. module_param_call(name, param_set_##type, param_get_##type, &value,perm);
8. __MODULE_PARM_TYPE(name, #type)
9. #define module_param(name, type,perm)
10. module_param_named(name, name, type, perm)
由此可知module_param的实现是通过module_param_named(name, name, type, perm)的。
3.module_param使用了3个参数:变量名,它的类型,以及一个权限掩码用来做一个辅助的sysfs入口。
这个宏定义应当放在任何函数之外,典型地是出现在源文件的前面。
eg:static char *whom="world"
static int tige=1;
module_param(tiger,int,S_IRUGO);
module_param(whom,charp,S_IRUGO);
4.模块参数支持许多类型:
bool
invbool
一个布尔型(true 或者 false)值(相关的变量应当是 int 类型). invbool 类型颠倒了值, 所以真值变成 false, 反之亦然.
charp:一个字符指针值. 内存为用户提供的字串分配, 指针因此设置.
int
long
short
uint
ulong
ushort
基本的变长整型值.以 u 开头的是无符号值.
5.数组参数,用逗号间隔的列表提供的值, 模块加载者也支持。
声明一个数组参数,使用:
module_param_array(name,type,num,perm);
这里 name是你的数组的名子(也是参数名),
type是数组元素的类型,
num是一个整型变量,
perm是通常的权限值.
如果数组参数在加载时设置,num 被设置成提供的数的个数. 模块加载者拒绝比数组能放下的多的值.
Tiger-John说明:
perm参数的作用是什么?
最后的module_param 字段是一个权限值,表示此参数在sysfs文件系统中所对应的文件节点的属性。你应当使用 <linux/stat.h>中定义的值. 这个值控制谁可以存取这些模块参数在 sysfs 中的表示.当perm为0时,表示此参数不存在 sysfs文件系统下对应的文件节点。 否则,模块被加载后,在/sys/module/ 目录下将出现以此模块名命名的目录, 带有给定的权限.。
权限在include/linux/stat.h中有定义
比如:
#defineS_IRWXU 00700
#defineS_IRUSR 00400
#defineS_IWUSR 00200
#defineS_IXUSR 00100
#defineS_IRWXG 00070
#defineS_IRGRP 00040
#defineS_IWGRP 00020
#defineS_IXGRP 00010
#defineS_IRWXO 00007
#defineS_IROTH 00004
#defineS_IWOTH 00002
#defineS_IXOTH 00001
使用S_IRUGO 参数可以被所有人读取, 但是不能改变; S_IRUGO|S_IWUSR 允许 root 来改变参数. 注意, 如果一个参数被 sysfs修改, 你的模块看到的参数值也改变了, 但是你的模块没有任何其他的通知. 你应当不要使模块参数可写, 除非你准备好检测这个改变并且因而作出反应.
四.file文件结构
struct file, 定义于<linux/fs.h>, 是设备驱动中第二个最重要的数据结构. 注意 file 与用户空间程序的 FILE 指针没有任何关系. 一个 FILE定义在 C 库中, 从不出现在内核代码中. 一个 struct file, 另一方面, 是一个内核结构, 从不出现在用户程序中.
文件结构代表一个打开的文件.(它不特定给设备驱动; 系统中每个打开的文件有一个关联的 struct file 在内核空间). 它由内核在 open 时创建,并传递给在文件上操作的任何函数, 直到最后的关闭. 在文件的所有实例都关闭后, 内核释放这个数据结构.
在内核源码中, struct file的指针常常称为 file 或者 filp("file pointer"). 我们将一直称这个指针为 filp 以避免和结构自身混淆.因此, file 指的是结构, 而 filp 是结构指针.
struct file 的最重要成员在这展示.如同在前一节, 第一次阅读可以跳过这个列表. 但是, 在本章后面, 当我们面对一些真实 C 代码时, 我们将更详细讨论这些成员.
mode_tf_mode;
文件模式确定文件是可读的或者是可写的(或者都是),通过位 FMODE_READ 和 FMODE_WRITE. 你可能想在你的 open 或者 ioctl 函数中检查这个成员的读写许可,但是你不需要检查读写许可, 因为内核在调用你的方法之前检查. 当文件还没有为那种存取而打开时读或写的企图被拒绝, 驱动甚至不知道这个情况.
loff_tf_pos;
当前读写位置.loff_t 在所有平台都是 64 位( 在 gcc 术语里是 long long ). 驱动可以读这个值, 如果它需要知道文件中的当前位置,但是正常地不应该改变它; 读和写应当使用它们作为最后参数而收到的指针来更新一个位置, 代替直接作用于 filp->f_pos. 这个规则的一个例外是在llseek 方法中, 它的目的就是改变文件位置.
unsignedint f_flags;
这些是文件标志,例如 O_RDONLY, O_NONBLOCK, 和 O_SYNC. 驱动应当检查 O_NONBLOCK 标志来看是否是请求非阻塞操作(我们在第一章的"阻塞和非阻塞操作"一节中讨论非阻塞 I/O ); 其他标志很少使用. 特别地, 应当检查读/写许可, 使用 f_mode而不是 f_flags. 所有的标志在头文件 <linux/fcntl.h> 中定义.
structfile_operations *f_op;
和文件关联的操作.内核安排指针作为它的 open 实现的一部分, 接着读取它当它需要分派任何的操作时. filp->f_op 中的值从不由内核保存为后面的引用;这意味着你可改变你的文件关联的文件操作, 在你返回调用者之后新方法会起作用. 例如, 关联到主编号 1 (/dev/null, /dev/zero, 等等)的open 代码根据打开的次编号来替代 filp->f_op 中的操作. 这个做法允许实现几种行为, 在同一个主编号下而不必在每个系统调用中引入开销.替换文件操作的能力是面向对象编程的"方法重载"的内核对等体.
void*private_data;
open系统调用设置这个指针为 NULL, 在为驱动调用 open 方法之前. 你可*使用这个成员或者忽略它; 你可以使用这个成员来指向分配的数据,但是接着你必须记住在内核销毁文件结构之前, 在 release 方法中释放那个内存. private_data 是一个有用的资源, 在系统调用间保留状态信息,我们大部分例子模块都使用它.
structdentry *f_dentry;
关联到文件的目录入口(dentry )结构. 设备驱动编写者正常地不需要关心 dentry 结构, 除了作为 filp->f_dentry->d_inode 存取inode 结构.
真实结构有多几个成员,但是它们对设备驱动没有用处. 我们可以安全地忽略这些成员, 因为驱动从不创建文件结构; 它们真实存取别处创建的结构.
五.inode结构
inode 结构由内核在内部用来表示文件. 因此, 它和代表打开文件描述符的文件结构是不同的.可能有代表单个文件的多个打开描述符的许多文件结构, 但是它们都指向一个单个 inode 结构.
inode 结构包含大量关于文件的信息. 作为一个通用的规则, 这个结构只有 2 个成员对于编写驱动代码有用:
dev_ti_rdev;
对于代表设备文件的节点,这个成员包含实际的设备编号.
structcdev *i_cdev;
structcdev 是内核的内部结构, 代表字符设备; 这个成员包含一个指针, 指向这个结构, 当节点指的是一个字符设备文件时.
i_rdev 类型在 2.5 开发系列中改变了, 破坏了大量的驱动. 作为一个鼓励更可移植编程的方法, 内核开发者已经增加了 2 个宏,可用来从一个 inode 中获取主次编号:
unsigned int iminor(struct inode *inode);
unsigned int imajor(struct inode *inode);
为了不要被下一次改动抓住, 应当使用这些宏代替直接操作 i_rdev.
六.container_of()
container_of()的作用是通过结构体成员的指针找到对应
结构体的指针,这个技巧在Linux内核编程中十分常用。在container_of
(inode->i_cdev,structglobalmem_dev,cdev)语句中,传给container_of()的第1个参数是结
构体成员的指针,第2个参数为整个结构体的类型,第3 个参数为传入的第1 个参数即
结构体成员的类型,container_of()返回值为整个结构体的指针。
七.ioctl函数
首先说明在2.6.36以后ioctl函数已经不再存在了,而是用unlocked_ioctl和compat_ioctl两个函数实现以前版本的ioctl函数。同时在参数方面也发生了一定程度的改变,去除了原来ioctl中的struct inode参数,同时改变了返回值。
但是驱动设计过程中存在的问题变化并不是很大,同样在应用程序设计中我们还是采用ioctl实现访问,而并不是unlocked_ioctl函数,因此我们还可以称之为ioctl函数的实现。
ioctl函数的实现主要是用来实现具体的硬件控制,采用相应的命令控制硬件的具体操作,这样就能使得硬件的操作不再是单调的读写操作。使得硬件的使用更加的方便。
ioctl函数实现主要包括两个部分,首先是命令的定义,然后才是ioctl函数的实现,命令的定义是采用一定的规则。
ioctl的命令主要用于应用程序通过该命令操作具体的硬件设备,实现具体的操作,在驱动中主要是对命令进行解析,通过switch-case语句实现不同命令的控制,进而实现不同的硬件操作。
ioctl函数的命令定义方法:
int (*unlocked_ioctl)(struct file*filp,unsigned int cmd,unsigned long arg)
虽然其中没有指针的参数,但是通常采用arg传递指针参数。cmd是一个命令。每一个命令由一个整形数据构成(32bits),将一个命令分成四部分,每一部分实现具体的配置,设备类型(幻数)8bits,方向2bits,序号8bits,数据大小13/14bits。命令的实现实质上就是通过简单的移位操作,将各个部分组合起来而已。
一个命令的分布的大概情况如下:
|---方向位(31-30)|----数据长度(29-16)----------------|---------设备类型(15-8)------|----------序号(7-0)----------|
|----------------------------------------------------------------------------------------------------------------------------------------|
其中方向位主要是表示对设备的操作,比如读设备,写设备等操作以及读写设备等都具有一定的方向,2个bits只有4种方向。
数据长度表示每一次操作(读、写)数据的大小,一般而已每一个命令对应的数据大小都是一个固定的值,不会经常改变,14bits说明可以选择的数据长度最大为16k。
设备类型类似于主设备号(由于8bits,刚好组成一个字节,因此经常采用字符作为幻数,表示某一类设备的命令),用来区别不同的命令类型,也就是特定的设备类型对应特定的设备。序号主要是这一类命令中的具体某一个,类似于次设备号(256个命令),也就是一个设备支持的命令多达256个。
同时在内核中也存在具体的宏用来定义命令以及解析命令。
但是大部分的宏都只是定义具体的方向,其他的都需要设计者定义。
主要的宏如下:
[cpp]
1. #include<linux/ioctl.h>
2. _IO(type,nr) 表示定义一个没有方向的命令,
3. _IOR(type,nr,size) 表示定义一个类型为type,序号为nr,数据大小为size的读命令
4. _IOW(type,nr,size) 表示定义一个类型为type,序号为nr,数据大小为size的写命令
5. _IOWR(type,nr,size) 表示定义一个类型为type,序号为nr,数据大小为size的写读命令
通常的type可采用某一个字母或者数字作为设备命令类型。
是实际运用中通常采用如下的方法定义一个具体的命令:
[cpp]
1. //头文件
2. #include<linux/ioctl.h>
3. /*定义一系列的命令*/
4. /*幻数,主要用于表示类型*/
5. #define MAGIC_NUM 'k'
6. /*打印命令*/
7. #define MEMDEV_PRINTF _IO(MAGIC_NUM,1)
8. /*从设备读一个int数据*/
9. #define MEMDEV_READ _IOR(MAGIC_NUM,2,int)
10. /*往设备写一个int数据*/
11. #define MEMDEV_WRITE _IOW(MAGIC_NUM,3,int)
12. /*最大的序列号*/
13. #define MEM_MAX_CMD 3
还有对命令进行解析的宏,用来确定具体命令的四个部分(方向,大小,类型,序号)具体如下所示:
[cpp]
1. /*确定命令的方向*/
2. _IOC_DIR(nr)
3. /*确定命令的类型*/
4. _IOC_TYPE(nr)
5. /*确定命令的序号*/
6. _IOC_NR(nr)
7. /*确定命令的大小*/
8. _IOC_SIZE(nr)
上面的几个宏可以用来命令,实现命令正确性的检查。
ioctl的实现过程主要包括如下的过程:
1、命令的检测
2、指针参数的检测
3、命令的控制switch-case语句
1、命令的检测主要包括类型的检查,数据大小,序号的检测,通过结合上面的命令解析宏可以快速的确定。
[cpp]
1. /*检查类型,幻数是否正确*/
2. if(_IOC_TYPE(cmd)!=MAGIC_NUM)
3. return -EINVAL;
4. /*检测命令序号是否大于允许的最大序号*/
5. if(_IOC_NR(cmd)> MEM_MAX_CMD)
6. return -EINVAL;
2、主要是指针参数的检测。指针参数主要是因为内核空间和用户空间的差异性导致的,因此需要来自用户空间指针的有效性。使用copy_from_user,copy_to_user,get_user,put_user之类的函数时,由于函数会实现指针参量的检测,因此可以省略,但是采用__get_user(),__put_user()之类的函数时一定要进行检测。具体的检测方法如下所示:
[cpp]
1. if(_IOC_DIR(cmd) & _IOC_READ)
2. err = !access_ok(VERIFY_WRITE,(void *)args,_IOC_SIZE(cmd));
3. else if(_IOC_DIR(cmd) & _IOC_WRITE)
4. err = !access_ok(VERIFY_READ,(void *)args,_IOC_SIZE(cmd));
5. if(err)/*返回错误*/
6. return -EFAULT;
当方向是读时,说明是从设备读数据到用户空间,因此要检测用户空间的指针是否可写,采用VERIFY_WRITE,而当方向是写时,说明是往设备中写数据,因此需要检测用户空间中的指针的可读性VERIFY_READ。检查通常采用access_ok()实现检测,第一个参数为读写,第二个为检测的指针,第三个为数据的大小。
3、命名的控制:
命令的控制主要是采用switch和case相结合实现的,这于window编程中的检测各种消息的实现方式是相同的。
[cpp]
1. /*根据命令执行相应的操作*/
2. switch(cmd)
3. {
4. case MEMDEV_PRINTF:
5. printk("<--------CMD MEMDEV_PRINTF Done------------>\n\n");
6. ...
7. break;
8. case MEMDEV_READ:
9. ioarg = &mem_devp->data;
10. ...
11. ret = __put_user(ioarg,(int *)args);
12. ioarg = 0;
13. ...
14. break;
15. case MEMDEV_WRITE:
16. ...
17. ret = __get_user(ioarg,(int *)args);
18. printk("<--------CMD MEMDEV_WRITE Done ioarg = %d--------->\n\n",ioarg);
19. ioarg = 0;
20. ...
21. break;
22. default:
23. ret = -EINVAL;
24. printk("<-------INVAL CMD--------->\n\n");
25. break;
26. }
这只是基本的框架结构,实际中根据具体的情况进行修改。这样就实现了基本的命令控制。
文件操作支持的集合如下:
[cpp]
1. /*添加该模块的基本文件操作支持*/
2. static const struct file_operations mem_fops =
3. {
4. /*结尾不是分号,注意其中的差别*/
5. .owner = THIS_MODULE,
6. .llseek = mem_llseek,
7. .read = mem_read,
8. .write = mem_write,
9. .open = mem_open,
10. .release = mem_release,
11. /*添加新的操作支持*/
12. .unlocked_ioctl = mem_ioctl,
13. };
需要注意不是ioctl,而是unlocked_ioctl。