《驱动学习 - 字符设备驱动》
1.1字符设备驱动基础
字符设备驱动:设备对数据的处理是按照字节流的形式进行的。
在linux中,“一切皆文件”(除了网络设备),这表示设备最终都会体现为一个文件。设备文件通常位于/dev目录下、
内核通常用主设备号区别一类设备,次设备号用于区分同一类设备的不同个人或不同分区。
手动创建设备文件
mknod /dev/vser0 c 256 0
mknod是make node的缩写。用于创建一个节点(设备文件也叫设备节点)、在linux系统中,一个节点代表一个文件。
1.2 字符设备驱动框架
example 1.1
1 #include <linux/init.h> 2 #include <linux/kernel.h> 3 #include <linux/module.h> 4 5 #include <linux/fs.h> 6 7 8 #define VSER_MAJOR 256 9 #define VSER_MINOR 0 10 #define VSER_DEV_CNT 1 11 #define VSER_DEV_NAME "vser" 12 13 static int __init vser_init(void) 14 { 15 int ret; 16 dev_t dev; 17 18 /* MKDEV宏将主设备号和次设备号合并成一个设备号 */ 19 dev = MKDEV(VSER_MAJOR, VSER_MINOR); 20 21 /* 将构造的设备号注册到内核中(静态) */ 22 ret = register_chrdev_region(dev, VSER_DEV_CNT, VSER_DEV_NAME); 23 if(ret) 24 goto reg_err; 25 return 0; 26 27 reg_err: 28 return ret; 29 } 30 31 static void __exit vser_exit(void) 32 { 33 dev_t dev; 34 35 dev = MKDEV(VSER_MAJOR, VSER_MINOR); 36 37 /* 从内核中注销设备号 */ 38 unregister_chrdev_region(dev, VSER_DEV_CNT); 39 } 40 41 module_init(vser_init); 42 module_exit(vser_exit); 43 44 MODULE_LICENSE("GPL");
注意:使用goto函数进行集中错误处理在驱动中非常常见,虽然这和一般的C语言编程规则相悖(因为C语言面向程序的语言,要求模块单入口,单出口,但是goto破坏了这种结构)。
register_chrdev_region注册设备号的方式称为静态注册设备号,这样如果两个驱动都是用同样的设备号,那么后加载的驱动将会失败,因为设备号冲突。可以使用动态分配设备号的方式避免这个问题。alloc_chrdev_region函数。
MKDEV该宏的作用是将主设备号左移20位和次设备号相或。
MAJOR和MINOR分别是从设备号中取出主设备号和次设备号。
#define MINORBITS 20 #define MINORMASK ((1U << MINORBITS) - 1) #define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS)) #define MINOR(dev) ((unsigned int) ((dev) & MINORMASK)) #define MKDEV(ma, mi) ((ma) << MINORBITS | (mi))
example 1.2
#include <linux/init.h> #include <linux/kernel.h> #include <linux/module.h> #include <linux/fs.h> #include <linux/cdev.h> #define VSER_MAJOR 256 #define VSER_MINOR 0 #define VSER_DEV_CNT 1 #define VSER_DEV_NAME "vser"
/* 代表一个具体得字符设备 */ static struct cdev vsdev;
/* 操作该设备得一些方法 */ static struct file_operations vser_ops = { .owner = THIS_MODULE, }; static int __init vser_init(void) { int ret; dev_t dev; /* MKDEV宏将主设备号和次设备号合并成一个设备号 */ dev = MKDEV(VSER_MAJOR, VSER_MINOR); /* 将构造的设备号注册到内核中(静态) */ ret = register_chrdev_region(dev, VSER_DEV_CNT, VSER_DEV_NAME); if(ret) goto reg_err; cdev_init(&vsdev, &vser_ops); vsdev.owner = THIS_MODULE;
/* 初始化vsdev中得部分成员,将vsdev中得ops指针指向vser_ops */ ret = cdev_add(&vsdev, dev, VSER_DEV_CNT); if(ret) goto add_err; return 0; add_err: unregister_chrdev_region(dev, VSER_DEV_CNT); reg_err: return ret; } static void __exit vser_exit(void) { dev_t dev; dev = MKDEV(VSER_MAJOR, VSER_MINOR); /* 删除cdev对象 */ cdev_del(&vsdev); /* 从内核中注销设备号 */ unregister_chrdev_region(dev, VSER_DEV_CNT); } module_init(vser_init); module_exit(vser_exit); MODULE_LICENSE("GPL");
cdev_init函数初始化了vsdev中得部分成员。另外一个最重要得操作就是将vsdev中得ops指针指向了vser_ops,这样通过设备号找到vsdev对象后,就能找到相关得操作方法集合,并调用其中的方法。
owner是一个指向struct module类型变量的指针,THIS_MODULE是包含驱动的模块中的struct module类型对象的地址,类似于C++中的this指针。这样就能够通过vsdev或vser_fops找到对应的模块。
cdev_add函数将cdev对象初始化后添加到内核中的cdev_map散列表中。
在例1.2中的cdev对象是静态定义的,我们也可以进行动态分配,对应的函数是cdev_alloc。