【奔跑吧!Linux 内核(第二版)】第6章:简单的字符设备驱动(一)
笨叔 陈悦. 奔跑吧 Linux 内核(第2版) [M]. 北京: 人民邮电出版社, 2020.
文章目录
- 从一个简单的字符设备开始
- 一个简单的字符设备
- 字符设备驱动详解
- 字符设备驱动的抽象(cdev 数据结构)
- 设备节点
- file_operations 数据结构
- 演示
- 源码
- *驱动程序*
- *Makefile*
- *用户空间程序*
为了写好一个设备驱动程序,你需要具备如下知识技能:
- 了解 Linux 内核字符设备驱动的架构。其中包括了解 Linux 字符设备驱动是如何组织的,应用程序是如何与驱动交互的。
- 了解 Linux 内核字符设备驱动相关的 API。其中涉及字符设备的相关基础知识,如字符设备的描述、设备号的管理、file_operations 的实现、ioctl 交互的设计和 Linux 设备模型的管理等。
- 了解 Linux 内核内存管理的 API。设备驱动不可避免地需要和内存打交道,如设备里的数据需要和用户程序交互,设备需要做 DMA 操作等。常见的应用场景有很多,如设备的内存需要映射到用户空间,然后和用户空间中的程序做交互,这就会用到 mmap 这个 API。
- 了解 Linux 内核中管理中断的 API。因为几乎所有的设备都支持中断模式,所以中断程序是设备驱动中不可或缺的部分。我们需要了解和熟悉 Linux 内核提供的中断管理相关的接口函数,例如,如何注册中断、如何编写中断处理程序等。
- 了解 Linux 内核中同步和锁等相关的 API。因为 Linux 是多进程、多用户的操作系统,而且支持内核抢占,所以进程间同步变得很复杂,即使是编写简单的字符设备驱动,也需要考虑同步和竞争的问题。
- 了解所要编写驱动的芯片原理。
从一个简单的字符设备开始
字符设备驱动开发流程:
- (.init)分配设备号(手动或自动,
int register_chrdev_region(dev_t from, unsigned count, const char *name);
或者int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name);
); - (.init)分配 cdev 结构体(静态或动态,
static struct cdev mydemo_cdev;
或者struct mydemo_cdev = cdev_alloc();
); - (.init)初始化 cdev 结构体,建立设备与驱动操作方法集 file_operations 之间的连接关系(
void cdev_init(mydemo_cdev, const struct file_operations *fops);
); - (.init)注册 cdev 到系统中(
int cdev_add(mydemo_cdev , dev_t dev, unsigned count);
); - 实现 file_operations 中的操作函数;
- 创建设备文件(mknod或udev机制创建);
- 编写用户空间程序,以使用设备。
一个简单的字符设备
模块初始化部分:
static int __init simple_char_init(void)
{int ret;ret = alloc_chrdev_region(&dev, 0, count, DEMO_NAME);if(ret){printk("failed to allocate char device region");return ret;}demo_cdev = cdev_alloc();if(!demo_cdev){printk("cdev_alloc failed\n");goto unregister_chrdev;}cdev_init(demo_cdev, &demodrv_fops);ret = cdev_add(demo_cdev, dev, count);if(ret){printk("cdev_add failed\n");goto cdev_fail;}printk("succeeded register char device: %s\n", DEMO_NAME);printk("Major number=%d, minor number=%d\n", MAJOR(dev), MINOR(dev));return 0;
cdev_fail:cdev_del(demo_cdev);unregister_chrdev:unregister_chrdev_region(dev, count);return ret;
}static void __exit simple_char_exit(void)
{printk("removing device\n");if(demo_cdev)cdev_del(demo_cdev);unregister_chrdev_region(dev, count);
}
file_operations
核心函数:
static int demodrv_open(struct inode *inode, struct file *file)
{int major = MAJOR(inode->i_rdev);int minor = MINOR(inode->i_rdev);printk("%s: major=%d, minor=%d\n", __func__, major, minor);return 0;
}static int demodrv_release(struct inode *inode, struct file *file)
{return 0;
}static ssize_t demodrv_read(struct file *file, char __user *buf, size_t lbuf, loff_t *ppos)
{printk("%s enter\n", __func__);return 0;
}static ssize_t demodrv_write(struct file *file, const char __user *buf, size_t count, loff_t *f_pos)
{printk("%s enter\n", __func__);return 0;
}static const struct file_operations demodrv_fops = {.owner = THIS_MODULE,.open = demodrv_open,.release = demodrv_release,.read = demodrv_read,.write = demodrv_write
};
字符设备驱动详解
字符设备驱动的抽象(cdev 数据结构)
cdev结构体定义在Linux内核头文件<linux/cdev.h>中,其完整定义如下:
struct cdev {struct kobject kobj; // 内嵌的内核对象struct module *owner; // 指向拥有该设备的模块指针const struct file_operations *ops; // 文件操作结构体指针struct list_head list; // 用于将设备加入内核链表中dev_t dev; // 设备号(主设备号+次设备号)unsigned int count; // 设备数量
};
- kobj:表示一个内核对象,用于实现内核对象的通用功能,如引用计数、sysfs接口支持等。它是cdev在内核中注册为对象的基础。
- owner:指向拥有该字符设备的内核模块的指针,通常设置为THIS_MODULE。这个成员可以防止设备的方法正在被使用时,设备所在模块被意外卸载。
- ops:指向file_operations结构体的指针,该结构体定义了字符设备的各种操作函数,如open、read、write等。这是字符设备驱动与内核交互的核心接口。
- list:用于将cdev结构体链接到一个内核链表中,便于内核管理多个字符设备或设备实例。
- dev:设备号,为32位整数,其中高12位表示主设备号,低20位表示次设备号。主设备号标识设备类型,次设备号区分同类型的不同设备。
- count:表示与该设备关联的设备号范围,通常为1。当驱动程序管理多个次设备时,count表示次设备号的数量。
申请到设备号后,一般可以通过如下宏获取主设备号和此设备号。
#define MAJOR(dev) ((dev) >> 20) // 提取主设备号(高12位)
#define MINOR(dev) ((dev) & 0xFFFFF) // 提取次设备号(低20位)
#define MKDEV(ma, mi) ((ma) << 20 | (mi)) // 生成设备号
设备节点
Linux 系统中有一条原则 —— “万物皆文件”。设备节点也算特殊的文件,成为设备文件,是连接内核空间驱动和用户空间应用程序的桥梁。如果应用程序想使用驱动提供的服务或者操作设备,那么需要通过访问设备文件来完成。设备文件使得用户程序操作硬件设备就像操作普通文件一样方便。
设备节点的生成方式有两种:
mknod
命令:mknod filename type major minor
,type: c/d- udev 机制。udev 是一个在用户空间中使用的工具,它能够根据系统中硬件设备的状态动态地更新设备节点,包括设备节点的创建、删除等。这种机制必须联合 sysfs 和 tmpfs 来实现,sysfs 为 udev 提供设备入口和 uevent 通道,tmpfs 为 udev 设备文件提供存放空间。
file_operations 数据结构
struct file_operations {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 (*read_iter)(struct kiocb *, struct iov_iter *);ssize_t (*write_iter)(struct kiocb *, struct iov_iter *);int (*iterate)(struct file *, struct dir_context *);int (*iterate_shared)(struct file *, struct dir_context *);__poll_t (*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 *);unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);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 (*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);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 **, void **);int (*fallocate)(struct file *, int, loff_t, loff_t);void (*show_fdinfo)(struct seq_file *, struct file *);unsigned long (*copy_file_range)(struct file *, loff_t, struct file *, loff_t, size_t, unsigned int);int (*remap_file_range)(struct file *, loff_t, struct file *, loff_t, loff_t, unsigned int);int (*fadvise)(struct file *, loff_t, loff_t, int);
};
关键成员说明:
- owner指向拥有该结构的模块指针(通常为 THIS_MODULE),用于模块引用计数管理。
- read/write同步读写接口,需使用 copy_to_user/copy_from_user安全访问用户空间缓冲区。
- read_iter/write_iter异步读写接口(替代旧版 aio_read/aio_write),支持零拷贝操作。
- unlocked_ioctl设备控制命令入口,无需大内核锁,参数直接传递用户空间数据。
- mmap将设备内存映射到用户空间,常用于帧缓冲区等场景。
- poll实现非阻塞 I/O,返回设备可读写状态(如 POLLIN)。
- open/release设备打开/关闭时的资源初始化和清理入口。
演示
1.编译设备驱动程序并加载驱动模块。
通过 dmesg
命令可以获取到系统为驱动程序所分配的设备号,亦可以通过查看 /proc/devices
虚拟文件系统中的 devices 节点信息获取设备号(主设备号:240),如下所示:
2.创建设备节点文件——搭建用户空间和内核空间的桥梁。
3.在用户空间设计测试程序。
通过操作第2步创建好的/dev/demo_drv
文件,实现对设备驱动程序的控制。
源码
驱动程序
simple_char.c
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/init.h>
#include <linux/cdev.h>#define DEMO_NAME "my_demo_dev"
static dev_t dev;
static struct cdev *demo_cdev;
static signed count = 1;static int demodrv_open(struct inode *inode, struct file *file)
{int major = MAJOR(inode->i_rdev);int minor = MINOR(inode->i_rdev);printk("%s: major=%d, minor=%d\n", __func__, major, minor);return 0;
}static int demodrv_release(struct inode *inode, struct file *file)
{return 0;
}static ssize_t demodrv_read(struct file *file, char __user *buf, size_t lbuf, loff_t *ppos)
{printk("%s enter\n", __func__);return 0;
}static ssize_t demodrv_write(struct file *file, const char __user *buf, size_t count, loff_t *f_pos)
{printk("%s enter\n", __func__);return 0;
}static const struct file_operations demodrv_fops = {.owner = THIS_MODULE,.open = demodrv_open,.release = demodrv_release,.read = demodrv_read,.write = demodrv_write
};static int __init simple_char_init(void)
{int ret;ret = alloc_chrdev_region(&dev, 0, count, DEMO_NAME);if(ret){printk("failed to allocate char device region");return ret;}demo_cdev = cdev_alloc();if(!demo_cdev){printk("cdev_alloc failed\n");goto unregister_chrdev;}cdev_init(demo_cdev, &demodrv_fops);ret = cdev_add(demo_cdev, dev, count);if(ret){printk("cdev_add failed\n");goto cdev_fail;}printk("succeeded register char device: %s\n", DEMO_NAME);printk("Major number=%d, minor number=%d\n", MAJOR(dev), MINOR(dev));return 0;
cdev_fail:cdev_del(demo_cdev);unregister_chrdev:unregister_chrdev_region(dev, count);return ret;
}static void __exit simple_char_exit(void)
{printk("removing device\n");if(demo_cdev)cdev_del(demo_cdev);unregister_chrdev_region(dev, count);
}module_init(simple_char_init);
module_exit(simple_char_exit);MODULE_AUTHOR("JJM");
MODULE_LICENSE("GPL v2");
MODULE_DESCRIPTION("simple character device");
Makefile
BASEINCLUDE ?= /lib/modules/`uname -r`/buildmydemo-objs := simple_char.oobj-m := mydemo.oall:$(MAKE) -C $(BASEINCLUDE) M=$(PWD) modulesclean:$(MAKE) -C $(BASEINCLUDE) M=$(PWD) cleanrm -f *.ko
用户空间程序
test.c
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>#define DEMO_DEV_NAME "/dev/demo_drv"int main()
{char buffer[64];int fd;fd = open(DEMO_DEV_NAME, O_RDONLY);if(fd < 0){printf("open device %s failed\n", DEMO_DEV_NAME);return -1;}read(fd, buffer, 64);close(fd);return 0;
}