第五章字符设备驱动讲解
第五章字符设备驱动讲解
对于整个的Linux设备驱动学习过程中,字符设备驱动是尤为基础也是重要的设备驱动,正是由于字符型的设备驱动简单,适合学习者首先学习的一种设备驱动,同时后续的各种设备驱动往往都是由字符型设备驱动改进或变换过去的,因此学习字符型设备驱动能够为后续学习其他驱动做好准备。
5.1字符设备驱动框架
字符型设备驱动主要包含如下的组成部分:
模块的入口函数(加载函数)、退出函数(卸载函数)和 file_operations 结构体中成员函数的实现。
1.模块的入口函数(加载函数)
字符设备驱动中的入口函数主要完成如下任务:申请设备号、字符设备初始化和将字符设备驱动添加到内核中。如下为一个字符型设备驱动入口函数的事例:
static const struct file_operations test_led_fops = { .owner = THIS_MODULE, .write = test_led_write, .open = test_led_open, .release = test _led_release, .read = test_led_read, }; //入口函数 static int __init test_led_init(void) { int rt=0; //1、申请设备号 led_num=MKDEV(239,0); rt = register_chrdev_region(led_num,1,"test_leds"); if(rt < 0) { printk("register_chrdev_region fail\n"); return rt; } ///2、字符设备初始化 cdev_init(&test_led_cdev,&test_led_fops); //3、字符设备添加到内核 rt = cdev_add(&test_led_cdev,led_num,1); if(rt < 0) { printk("cdev_add fail\n"); goto fail_cdev_add; } printk("test led init\n"); return 0; fail_cdev_add: unregister_chrdev_region(led_num,1); return rt; }
上述的代码中是使用的静态申请设备的方法进行驱动注册的:
led_num=MKDEV(239,0);//静态申请设备号 rt = register_chrdev_region(led_num,1,"test_leds");
一般情况下为了避免设备主次设备号冲突的情况发生,需要使用动态申请的方法来申请设备号,由内核动态给我们分配合适的设备号,因此需要将对应的代码修改为如下来动态获取:
//动态申请设备号 rt=alloc_chrdev_region(&led_num,0,1,"gec6818_leds");
2.模块的退出函数(卸载函数)
模块的退出函数即是驱动卸载函数主要完成的任务是释放设备号和注销字符设备的注册。如下为一个卸载函数的事例:
//出口函数 static void __exit test_led_exit(void) { cdev_del(&test_led_cdev);//注销字符设备的注册 unregister_chrdev_region(led_num,1);//注销字符设备的设备号 printk("test led exit\n"); }
3.file_operations结构体以及成员函数
file_operations结构体是一个设备驱动中非常重要的成员,该成员提供给用户空间操作设备驱动的方法,如read、write和ioctl等。其中file_operations的成员函数本质上是一系列函数指针,这些函数指针在初始化函数中一般是将我们自己定义的函数的地址赋值给它们。
在驱动中对file_operations的初始化,首先是定义file_operations及初始化赋值,接着通过如下函数对字符设备进行初始化:
void cdev_init(struct cdev *, struct file_operations *);
如下是具体对file_operations进行初始化的实例:
static const struct file_operations test_led_fops = { .owner = THIS_MODULE, .write = test_led_write, .open = test_led_open, .release = test _led_release, .read = test_led_read, }; static int test_led_open (struct inode * inode, struct file *file) { printk("test_led_open \n"); ............ return 0; } static int test_led_release (struct inode * inode, struct file *file) { printk("test_led_release \n"); ............ return 0; } static ssize_t test_led_write (struct file * file, const char __user * buf, size_t len, loff_t * off) { printk("test_led_write \n"); ............. return len; } static ssize_t gec6818_led_read (struct file *file, char __user *buf, size_t len, loff_t * offs) { printk("test_led_read\n"); ............. return len; }
5.2字符设备驱动的知识补充
为了更加方便读者对字符设备程序的理解,接下来将对本节中常用的一些结构体和函数进行补充说明。
1、设备类型
在linux中主要有如下设备类型:
c:字符设备 b:块设备 d:目录 l:符号链接
其中在/dev目录主要的是字符设备和块设备,字符设备以c作为开头,块设备以b作为开头,如下为其中的设备存在例子:
crw-rw---- 1 root root 29, 0 Jack 1 1970 fb0 brw-rw---- 1 root root 179, 1 Jack 1 1970 mmcblk0p1
2、设备号
在linux设备驱动中是以设备号来区分各个不同的设备的,设备号又像我们的身份证ID那样的形式存在来进行识别,有所不同的是设备号是由主设备号和次设备号组成。同时设备号使用一个无符号的整型数表示,主设备号占12位,次设备号20位。
如下为字符设备fb0的详细信息,其中29为主设备号,0为次设备号。
[root@GEC6818 /dev]#ls -l fb0 crw-rw---- 1 root root 29, 0 Jack 1 1970 fb0
3、file_operations是结构体定义补充
struct file_operations { struct module *owner;//占有该结构的模块的指针,一般为 THIS_MODULES loff_t(*llseek)(struct file *, loff_t, int);//设置修改文件当前的读写位置 //从设备中读取数据 ssize_t(*read)(struct file *, char _ _user *, size_t, loff_t*); //向设备发送数据 ssize_t(*write)(struct file *, const char _ _user *, size_t, loff_t*); //异步的读取操作初始化 ssize_t(*aio_read)(struct kiocb *, char _ _user *, size_t, loff_t); //异步的写入操作初始化 ssize_t(*aio_write)(struct kiocb *, const char _ _user *, size_t, loff_t); //仅用于读取目录,若使用在设备文件,该字段为 NULL int(*readdir)(struct file *, void *, filldir_t); //轮询函数,判断目前是否可以进行非阻塞的读取或写入 unsigned int(*poll)(struct file *, struct poll_table_struct*); //设备的I/O 控制命令 int(*ioctl)(struct inode *, struct file *, unsigned int, unsigned long); long(*unlocked_ioctl)(struct file *, unsigned int, unsigned long); //在 64位系统上,32 位的ioctl 调用 long(*compat_ioctl)(struct file *, unsigned int, unsigned long); //请求将设备内存映射到进程地址空间 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 *, int datasync); //异步的fsync操作 int(*aio_fsync)(struct kiocb *, int datasync); //通知设备 FASYNC 标志发生变化 int(*fasync)(int, struct file *, int); int(*lock)(struct file *, int, struct file_lock*); 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)(int); int(*dir_notify)(struct file *filp, unsigned long arg); int(*flock)(struct file *, int, struct file_lock*); ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t,unsigned int); ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int); int (*setlease)(struct file *, long, struct file_lock **); };
4、字符设备初始化函数
参数说明: cdev,要初始化的字符设备结构体 ops,为该字符设备添加文件操作集 返回值:无 函数功能:对字符设备进行初始化,这个函数需要与cdev_add()配套使用,将字符设备加 入到内核中。 void cdev_init(struct cdev *cdev, const struct file_operations *fops)
5、字符设备加载到内核函数
参数说明: p,初始化好的字符设备 dev,设备号 count,次设备的数量 返回值:成功返回0; 失败,返回负数的错误码。 函数功能:将字符设备添加进内核中。 int cdev_add(struct cdev *p, dev_t dev, unsigned count)
6、将字符设备从内核删除函数
参数说明: p,初始化好的字符设备 返回值:无 函数功能:将字符设备从内核种删除注销。 void cdev_del(struct cdev *p)
5.3字符设备驱动示例
如下将给出一个字符设备驱动的例子以及对该驱动进行测试的应用程序代码,主要实现的功能为运行用户程序来读取驱动的字符串数据的功能。
如下为test_led_drv.c驱动代码:
#include <linux/init.h> #include <linux/kernel.h> #include <linux/module.h> #include <linux/fs.h> #include <linux/cdev.h> #include <linux/uaccess.h> static struct cdev test_led_cdev; //字符设备结构体 static dev_t led_num=0; //设备号 static int test_led_open (struct inode * inode, struct file *file) { printk("test_led_open \n"); return 0; } static int test_led_release (struct inode * inode, struct file *file) { printk("test_led_release \n"); return 0; } static ssize_t test_led_write (struct file * file, const char __user * buf, size_t len, loff_t * off) { int rt; char kbuf[64]={0}; //判断当前len是否合法 if(len > sizeof kbuf) return -EINVAL; //返回参数无效错误码 //从用户空间拷贝数据 rt = copy_from_user(kbuf,buf,len); //获取成功复制的字节数 len = len - rt; printk("test_led_write,kbuf[%s],len[%d]\n",kbuf,len); return len; } static ssize_t test_led_read (struct file *file, char __user *buf, size_t len, loff_t * offs) { int rt; char kbuf[6]={'1','2','3','4','\n','\0'}; //判断当前len是否合法 if(len > sizeof kbuf) return -EINVAL; //返回参数无效错误码 //从内核空间拷贝到用户空间 rt = copy_to_user(buf,kbuf,len); //获取成功复制的字节数 len = len - rt; printk("test_led_read,__user buf[%s],len[%d]\n",buf,len); return len; } static const struct file_operations gec6818_led_fops = { .owner = THIS_MODULE, .write = test_led_write, .open = test_led_open, .release = test_led_release, .read = test_led_read, }; //入口函数 static int __init test_led_init(void) { int rt=0; //动态申请设备号 rt=alloc_chrdev_region(&led_num,0,1,"gec6818_leds"); if(rt < 0) { printk("alloc_chrdev_region fail\n"); return rt; } printk("led_major = %d\n",MAJOR(led_num)); printk("led_minor = %d\n",MINOR(led_num)); //字符设备初始化 cdev_init(&test_led_cdev,&test_led_fops); //字符设备添加到内核 rt = cdev_add(&test_led_cdev,led_num,1); if(rt < 0) { printk("cdev_add fail\n"); goto fail_cdev_add; } printk("testled init\n"); return 0; fail_cdev_add: unregister_chrdev_region(led_num,1); return rt; } //出口函数 static void __exit test_led_exit(void) { cdev_del(&test_led_cdev); unregister_chrdev_region(led_num,1); printk("testled exit\n"); } module_init(test_led_init); module_exit(test_led_exit) //模块描述 MODULE_AUTHOR("whl_work@163.com"); //作者信息 MODULE_DESCRIPTION("test led driver"); //模块功能说明 MODULE_LICENSE("GPL"); //许可证:驱动遵循GPL协议
如下为led_test.c测试代码:
#include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <string.h> int main(int argc, char **argv) { int fd=-1; int len; char buf[64]="hello teacher.wen"; //打开test_leds设备 fd = open("/dev/test_leds",O_RDWR); if(fd < 0) { perror("open /dev/test_leds:"); return fd; } sleep(2); write(fd,buf,strlen(buf)); sleep(2); len = read(fd,buf,6); if(len > 0) printf("read buf:%s\n len=%d\n",buf,len); sleep(2); close(fd); return 0; }
由于用户空间并不能直接读写,因此在需要使用特定的函数来进行用户空间和内核空间的数据交换:
//从用户空间拷贝数据给内核空间 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);
上面的驱动程序测试结果如下:
[root@jack /]#./led_test [ 378.003000] test_led_open [ 380.004000] test_led_write,kbuf[hello teacher.wen],len[17] [ 382.006000] test_led_read,__user buf[1234],len[5]read buf:1234 len=6 [ 384.008000] test_led_release
5.3 本章总结
本章主要讲解了字符设备驱动的框架结构,以及编写字符设备驱动的必备知识,如设备号和file_operations等,同时在最后给出了一个读写字符设备驱动数据的测试用例,让读者对字符设备驱动在实践中有更如的理解,为后续章节的学习打下重要的基础。