在linux设备驱动第一篇:设备驱动程序简介中简单介绍了字符驱动,本篇简单介绍如何写一个简单的字符设备驱动。本篇借鉴LDD中的源码,实现一个与硬件设备无关的字符设备驱动,仅仅操作从内核中分配的一些内存。
下面就开始学习如何写一个简单的字符设备驱动。首先我们来分解一下字符设备驱动都有那些结构或者方法组成,也就是说实现一个可以使用的字符设备驱动我们必须做些什么工作。
xxx@ubuntu:~$ ls -l /dev/ total 0 brw-rw---- 1 root disk 7, 0 3月 25 10:34 loop0 brw-rw---- 1 root disk 7, 1 3月 25 10:34 loop1 brw-rw---- 1 root disk 7, 2 3月 25 10:34 loop2 crw-rw-rw- 1 root tty 5, 0 3月 25 12:48 tty crw--w---- 1 root tty 4, 0 3月 25 10:34 tty0 crw-rw---- 1 root tty 4, 1 3月 25 10:34 tty1 crw--w---- 1 root tty 4, 10 3月 25 10:34 tty10其中b代表块设备,c代表字符设备。对于普通文件来说,ls -l会列出文件的长度,而对于设备文件来说,上面的7,5,4等代表的是对应设备的主设备号,而后面的0,1,2,10等则是对应设备的次设备号。那么主设备号和次设备号分别代表什么意义呢?一般情况下,可以这样理解,主设备号标识设备对应的驱动程序,也就是说1个主设备号对应一个驱动程序。当然,现在也有多个驱动程序共享主设备号的情况。而次设备号有内核使用,用于确定/dev下的设备文件对应的具体设备。举一个例子,虚拟控制台和串口终端有驱动程序4管理,而不同的终端分别有不同的次设备号。
int register_chrdev_region(dev_t first, unsigned int count, const char *name);first是要分配的设备编号范围的起始值。count是连续设备的编号的个数。name是和该设备编号范围关联的设备名称,他将出现在/proc/devices和sysfs中。此函数成功返回0,失败返回负的错误码。此函数是在已知主设备号的情况下使用,在未知主设备号的情况下,我们使用下面的函数:
int alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned int count, const char *name);dev用于输出申请到的设备编号,firstminor要使用的第一个此设备编号。
void unregister_chrdev_region(dev_t dev, unsigned int count);此函数多在模块的清除函数中调用。
struct file_operations { //它是一个指向拥有这个结构的模块的指针. 这个成员用来在它的操作还在被使用时阻止模块被卸载. 几乎所有时间中, 它被简单初始化为 THIS_MODULE struct module *owner; 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 *, const struct iovec *, unsigned long, loff_t); ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t); ssize_t (*read_iter) (struct kiocb *, struct iov_iter *); ssize_t (*write_iter) (struct kiocb *, struct iov_iter *); int (*iterate) (struct file *, struct dir_context *); unsigned int (*poll) (struct file *, struct poll_table_struct *); long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long); 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 *, fl_owner_t id); int (*release) (struct inode *, struct file *); int (*fsync) (struct file *, loff_t, loff_t, int datasync); int (*aio_fsync) (struct kiocb *, int datasync); 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 (*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 **); long (*fallocate)(struct file *file, int mode, loff_t offset, loff_t len); int (*show_fdinfo)(struct seq_file *m, struct file *f); };需要说明的是这里面的函数在驱动中不用全部实现,不支持的操作留置为NULL。
inode 结构由内核在内部用来表示文件. 因此, 它和代表打开文件描述符的文件结构是不同的. 可能有代表单个文件的多个打开描述符的许多文件结构, 但是它们都指向一个单个 inode 结构。
inode 结构包含大量关于文件的信息。但对于驱动程序编写来说一般不用关心,暂且不说。
有 2 种方法来分配和初始化一个这些结构. 如果你想在运行时获得一个独立的 cdev 结构, 你可以为此使用这样的代码:
更多的情况是把cdv结构嵌入到你自己封装的设备结构中,这时需要使用下面的方法来分配和初始化:struct cdev *my_cdev = cdev_alloc(); my_cdev->ops = &my_fops;
后面的例子程序就是这么做的。一旦 cdev 结构建立, 最后的步骤是把它告诉内核:void cdev_init(struct cdev *cdev, struct file_operations *fops);
int cdev_add(struct cdev *dev, dev_t num, unsigned int count)
这里, dev 是 cdev 结构, num 是这个设备响应的第一个设备号, count 是应当关联到设备的设备号的数目. 常常 count 是 1。
从系统去除一个字符设备, 调用:
void cdev_del(struct cdev *dev);4、一个简单的字符设备
上面大致介绍了实现一个字符设备所要做的工作,下面就来一个真实的例子来总结上面介绍的内容。源码中的关键地方已经作了注释。#include <linux/module.h> #include <linux/types.h> #include <linux/fs.h> #include <linux/errno.h> #include <linux/mm.h> #include <linux/sched.h> #include <linux/init.h> #include <linux/cdev.h> #include <asm/io.h> #include <asm/uaccess.h> #include <linux/timer.h> #include <asm/atomic.h> #include <linux/slab.h> #include <linux/device.h> #define CDEVDEMO_MAJOR 255 /*预设cdevdemo的主设备号*/ static int cdevdemo_major = CDEVDEMO_MAJOR; /*设备结构体,此结构体可以封装设备相关的一些信息等 信号量等也可以封装在此结构中,后续的设备模块一般都 应该封装一个这样的结构体,但此结构体中必须包含某些 成员,对于字符设备来说,我们必须包含struct cdev cdev*/ struct cdevdemo_dev { struct cdev cdev; }; struct cdevdemo_dev *cdevdemo_devp; /*设备结构体指针*/ /*文件打开函数,上层对此设备调用open时会执行*/ int cdevdemo_open(struct inode *inode, struct file *filp) { printk(KERN_NOTICE "======== cdevdemo_open "); return 0; } /*文件释放,上层对此设备调用close时会执行*/ int cdevdemo_release(struct inode *inode, struct file *filp) { printk(KERN_NOTICE "======== cdevdemo_release "); return 0; } /*文件的读操作,上层对此设备调用read时会执行*/ static ssize_t cdevdemo_read(struct file *filp, char __user *buf, size_t count, loff_t *ppos) { printk(KERN_NOTICE "======== cdevdemo_read "); } /* 文件操作结构体,文中已经讲过这个结构*/ static const struct file_operations cdevdemo_fops = { .owner = THIS_MODULE, .open = cdevdemo_open, .release = cdevdemo_release, .read = cdevdemo_read, }; /*初始化并注册cdev*/ static void cdevdemo_setup_cdev(struct cdevdemo_dev *dev, int index) { printk(KERN_NOTICE "======== cdevdemo_setup_cdev 1"); int err, devno = MKDEV(cdevdemo_major, index); printk(KERN_NOTICE "======== cdevdemo_setup_cdev 2"); /*初始化一个字符设备,设备所支持的操作在cdevdemo_fops中*/ cdev_init(&dev->cdev, &cdevdemo_fops); printk(KERN_NOTICE "======== cdevdemo_setup_cdev 3"); dev->cdev.owner = THIS_MODULE; dev->cdev.ops = &cdevdemo_fops; printk(KERN_NOTICE "======== cdevdemo_setup_cdev 4"); err = cdev_add(&dev->cdev, devno, 1); printk(KERN_NOTICE "======== cdevdemo_setup_cdev 5"); if(err) { printk(KERN_NOTICE "Error %d add cdevdemo %d", err, index); } } int cdevdemo_init(void) { printk(KERN_NOTICE "======== cdevdemo_init "); int ret; dev_t devno = MKDEV(cdevdemo_major, 0); struct class *cdevdemo_class; /*申请设备号,如果申请失败采用动态申请方式*/ if(cdevdemo_major) { printk(KERN_NOTICE "======== cdevdemo_init 1"); ret = register_chrdev_region(devno, 1, "cdevdemo"); }else { printk(KERN_NOTICE "======== cdevdemo_init 2"); ret = alloc_chrdev_region(&devno,0,1,"cdevdemo"); cdevdemo_major = MAJOR(devno); } if(ret < 0) { printk(KERN_NOTICE "======== cdevdemo_init 3"); return ret; } /*动态申请设备结构体内存*/ cdevdemo_devp = kmalloc(sizeof(struct cdevdemo_dev), GFP_KERNEL); if(!cdevdemo_devp) /*申请失败*/ { ret = -ENOMEM; printk(KERN_NOTICE "Error add cdevdemo"); goto fail_malloc; } memset(cdevdemo_devp,0,sizeof(struct cdevdemo_dev)); printk(KERN_NOTICE "======== cdevdemo_init 3"); cdevdemo_setup_cdev(cdevdemo_devp, 0); /*下面两行是创建了一个总线类型,会在/sys/class下生成cdevdemo目录 这里的还有一个主要作用是执行device_create后会在/dev/下自动生成 cdevdemo设备节点。而如果不调用此函数,如果想通过设备节点访问设备 需要手动mknod来创建设备节点后再访问。*/ cdevdemo_class = class_create(THIS_MODULE, "cdevdemo"); device_create(cdevdemo_class, NULL, MKDEV(cdevdemo_major, 0), NULL, "cdevdemo"); printk(KERN_NOTICE "======== cdevdemo_init 4"); return 0; fail_malloc: unregister_chrdev_region(devno,1); } void cdevdemo_exit(void) /*模块卸载*/ { printk(KERN_NOTICE "End cdevdemo"); cdev_del(&cdevdemo_devp->cdev); /*注销cdev*/ kfree(cdevdemo_devp); /*释放设备结构体内存*/ unregister_chrdev_region(MKDEV(cdevdemo_major,0),1); //释放设备号 } MODULE_LICENSE("Dual BSD/GPL"); module_param(cdevdemo_major, int, S_IRUGO); module_init(cdevdemo_init); module_exit(cdevdemo_exit);Makefile文件如下:ifneq ($(KERNELRELEASE),) obj-m := cdevdemo.o else KERNELDIR ?= /lib/modules/$(shell uname -r)/build PWD := $(shell pwd) default: $(MAKE) -C $(KERNELDIR) M=$(PWD) modules endif clean: rm -rf *.o *~ core .depend .*.cmd *.ko *.mod.c .tmp_versions modules.order Module.symvers温馨提示:测试环境为Linux ubuntu 3.16.0-33-generic。5、总结
本篇主要介绍了简单字符设备的编写与实现以及其中的关键点。下一篇会主要讲解下驱动的一些常用的调试技巧。
第一时间获得博客更新提醒,以及更多技术信息分享,欢迎关注个人微信公众平台:程序员互动联盟(coder_online),扫一扫下方二维码或搜索微信号coder_online即可关注,我们可以在线交流。
原文地址:http://blog.csdn.net/haomcu/article/details/44620725