在linux设备驱动第一篇:设备驱动程序简介中简单介绍了字符驱动,本篇简单介绍如何写一个简单的字符设备驱动。本篇借鉴LDD中的源码,实现一个与硬件设备无关的字符设备驱动,仅仅操作从内核中分配的一些内存。
下面就开始学习如何写一个简单的字符设备驱动。首先我们来分解一下字符设备驱动都有那些结构或者方法组成,也就是说实现一个可以使用的字符设备驱动我们必须做些什么工作。
1、主设备号和次设备号
对于字符设备的访问是通过文件系统中的设备名称进行的。他们通常位于/dev目录下。如下:
- 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
1.1、设备编号的表达
在内核中,dev_t用来保存设备编号,包括主设备号和次设备号。在2.6的内核版本种,dev_t是一个32位的数,其中12位用来表示主设备号,其余20位用来标识次设备号。
通过dev_t获取主设备号和次设备号使用下面的宏:
MAJOR(dev_t dev);
MINOR(dev_t dev);
相反,通过主设备号和次设备号转换为dev_t类型使用:
MKDEV(int major, int minor);
1.2、分配和释放设备编号
在构建一个字符设备之前,驱动程序首先要获得一个或者多个设备编号,这类似一个营业执照,有了营业执照才在内核中正常工作营业。完成此工作的函数是:
- int register_chrdev_region(dev_t first, unsigned int count, const char *name);
- int alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned int count, const char *name);
在不使用时需要释放这些设备编号,已提供其他设备程序使用:
- void unregister_chrdev_region(dev_t dev, unsigned int count);
分配到设备编号之后,我们只是拿到了营业执照,虽说现在已经准备的差不多了,但是我们只是从内核中申请到了设备号,应用程序还是不能对此设备作任何事情,我们需要一个简单的函数来把设备编号和此设备能实现的功能连接起来,这样我们的模块才能提供具体的功能.这个操作很简单,稍后就会提到,在此之前先介绍几个重要的数据结构。
2、重要的数据结构
注册设备编号仅仅是完成一个字符设备驱动的第一步。下面介绍大部分驱动都会包含的三个重要的内核的数据结构。
2.1、文件操作file_operations
file_operations是第一个重要的结构,定义在 <linux/fs.h>, 是一个函数指针的集合,设备所能提供的功能大部分都由此结构提供。这些操作也是设备相关的系统调用的具体实现。此结构的具体实现如下所示:
- 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);
- };
2.2、文件结构struct file
struct file, 定义于 <linux/fs.h>, 是设备驱动中第二个最重要的数据结构。文件结构代表一个打开的文件. (它不特定给设备驱动; 系统中每个打开的文件有一个关联的 struct file 在内核空间). 它由内核在 open 时创建, 并传递给在文件上操作的任何函数, 直到最后的关闭. 在文件的所有实例都关闭后, 内核释放这个数据结构。file结构的详细可参考fs.h,这里列出来几个重要的成员。
- struct file_operations *f_op:就是上面刚刚介绍的文件操作的集合结构。
- mode_t f_mode:文件模式确定文件是可读的或者是可写的(或者都是), 通过位 FMODE_READ 和 FMODE_WRITE. 你可能想在你的 open 或者 ioctl 函数中检查这个成员的读写许可, 但是你不需要检查读写许可, 因为内核在调用你的方法之前检查. 当文件还没有为那种存取而打开时读或写的企图被拒绝, 驱动甚至不知道这个情况
- loff_t f_pos:当前读写位置. loff_t 在所有平台都是 64 位。驱动可以读这个值, 如果它需要知道文件中的当前位置, 但是正常地不应该改变它。
- unsigned int f_flags:这些是文件标志, 例如 O_RDONLY, O_NONBLOCK, 和 O_SYNC. 驱动应当检查 O_NONBLOCK 标志来看是否是请求非阻塞操作。
- void *private_data:open 系统调用设置这个指针为 NULL, 在为驱动调用 open 方法之前. 你可自由使用这个成员或者忽略它; 你可以使用这个成员来指向分配的数据, 但是接着你必须记住在内核销毁文件结构之前, 在 release 方法中释放那个内存. private_data 是一个有用的资源, 在系统调用间保留状态信息, 我们大部分例子模块都使用它
2.3、inode 结构
inode 结构由内核在内部用来表示文件. 因此, 它和代表打开文件描述符的文件结构是不同的. 可能有代表单个文件的多个打开描述符的许多文件结构, 但是它们都指向一个单个 inode 结构。
inode 结构包含大量关于文件的信息。但对于驱动程序编写来说一般不用关心,暂且不说。
3、字符设备的注册
内核在内部使用类型 struct cdev 的结构来代表字符设备. 在内核调用你的设备操作前, 你编写分配并注册一个或几个这些结构。
更多的情况是把cdv结构嵌入到你自己封装的设备结构中,这时需要使用下面的方法来分配和初始化:
有 2 种方法来分配和初始化一个这些结构. 如果你想在运行时获得一个独立的 cdev 结构, 你可以为此使用这样的代码:
- struct cdev *my_cdev = cdev_alloc();
- my_cdev->ops = &my_fops;
- 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);
- 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
5、总结
本篇主要介绍了简单字符设备的编写与实现以及其中的关键点。下一篇会主要讲解下驱动的一些常用的调试技巧。
第一时间获得博客更新提醒,以及更多技术信息分享,欢迎关注个人微信公众平台:程序员互动联盟(coder_online),扫一扫下方二维码或搜索微信号coder_online即可关注,我们可以在线交流。
相关推荐
《Linux设备驱动开发详解:基于最新的Linux4.0内核配套光盘》一书由宋保华编写,针对Linux4.0内核进行了深入浅出的解析与实践指导。本书结合了理论与实践,旨在帮助读者掌握Linux设备驱动程序的设计与开发技能。 ##...
《Linux设备驱动程序》第三版(通常简称ldd3)是Linux系统开发领域的经典之作,由Robert Love编写,深入浅出地介绍了Linux内核设备驱动的编写与理解。该书的源码包“ldd3_examples”包含了书中所涉及的所有示例代码...
第3篇 Linux设备驱动实例 第13章 Linux块设备驱动 272 第14章 Linux终端设备驱动 304 第15章 Linux的I2C核心、总线与设备驱动 333 第16章 Linux网络设备驱动 363 第17章 Linux音频设备驱动 388 第...
《LINUX设备驱动程序》(Linux.Device.Driver) 第三版是深入探讨Linux内核设备驱动程序开发的重要参考资料,尤其对于那些希望理解Linux系统如何与硬件交互的开发者来说,这本书是不可或缺的。书中详细阐述了如何编写...
总的来说,《ldd3_pdf Linux设备驱动第三版》是一本全面、深入的Linux设备驱动教程,无论你是初学者还是有经验的开发者,都能从中受益匪浅,提升你在Linux系统下的硬件驱动开发技能。通过学习这本书,你可以更好地...
《Linux设备驱动程序》第三版2.6是Linux内核开发者和系统工程师的重要参考资料,它深入探讨了如何为Linux操作系统编写和理解设备驱动程序。这本书由Jonathan Corbet、Greg Kroah-Hartman和Alessandro Rubini合著,是...
《Linux设备驱动程序 第三版》是一本专为Linux初学者和进阶者设计的教程,旨在帮助读者深入理解Linux操作系统中的设备驱动程序开发。这本书是Linux学习路径中不可或缺的一环,作者建议在阅读完《Unix环境高级编程》...
三、嵌入式Linux设备驱动程序的开发流程 嵌入式Linux设备驱动程序的开发流程主要包括设备驱动程序的设计、编写、测试和调试等步骤。 1. 设备驱动程序的设计:在设计设备驱动程序时,需要考虑设备的特性、内核的...
《Linux设备驱动程序》(中文版第三版)是一本深入探讨Linux系统下设备驱动程序开发的专业书籍。本书针对想要理解Linux内核工作原理以及如何编写高效、稳定的设备驱动的开发者,提供了详尽的指导和实践案例。以下是该...
我们来写一个最简单的字符设备驱动程序。虽然它什么也不做,但是通过它可以了解Linux的设备驱动程序的工作原理。把下面的C代码输入机器,你就会获得一个真正的设备驱动程序。 #define __NO_VERSION__ #include ...
《Linux设备驱动程序》(中文第二版)是一本深度探讨Linux系统下设备驱动程序开发的专业书籍,对于想要深入了解Linux内核以及如何与硬件交互的开发者来说,是不可或缺的参考资料。这本书详细介绍了Linux环境下设备...
本书的第三版提供了全面且深入的讲解,帮助读者理解如何为Linux内核编写设备驱动,以支持各种硬件设备。以下是该书涉及的一些核心知识点: 1. **Linux内核架构**:介绍Linux内核的基本结构,包括模块化设计,进程...
在学习过程中,配合《Linux设备驱动程序》第三版(英文版)的阅读,开发者可以逐步掌握Linux系统下编写设备驱动的技能,为在嵌入式ARM平台上构建高效稳定的系统打下坚实基础。这本书不仅适合初学者入门,也对经验...
总的来说,《Linux设备驱动程序》第三版的源代码为学习者提供了一个丰富的实践平台,通过实际操作,不仅可以深化对Linux内核机制的理解,还能提升动手解决问题的能力。无论是初学者还是有经验的开发者,都能从中...
《LINUX设备驱动程序》第三版是一本深入探讨Linux内核设备驱动开发的经典著作,它为读者揭示了如何编写高效、可靠的Linux系统设备驱动。源码是书中理论知识的实践体现,通过分析和研究这些源码,我们可以深入了解...
《Linux设备驱动程序(LDD)第三版》是Linux内核开发者和系统工程师的必备参考书。这本书详尽地介绍了如何为Linux操作系统编写设备驱动程序,是深入理解Linux内核与硬件交互机制的重要教程。以下是对该书核心知识点的...