代码:
- #include <linux/module.h>
- #include <linux/fs.h>
- #include <linux/uaccess.h>
- #include <linux/init.h>
- #include <linux/cdev.h>
-
- #define DEMO_NAME "simple_chardev"
- static dev_t dev;
- static struct cdev *simple_cdev;
- static signed count = 1;
-
- static int simpledev_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 simpledev_release(struct inode *inode, struct file *file)
- {
- return 0;
- }
-
- static ssize_t simpledev_read(struct file *file, char __user* buf, size_t lbuf, loff_t *ppos)
- {
- printk("%s enter\n", __func__);
- return 0;
- }
-
- static ssize_t simpledev_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 simpledev_fops = {
- .owner = THIS_MODULE,
- .open = simpledev_open,
- .release = simpledev_release,
- .read = simpledev_read,
- .write = simpledev_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;
- }
- // 使用cdev_alloc方法产生cdev结构体
- // 或者手动赋值
- simple_cdev = cdev_alloc();
- if (!simple_cdev) {
- printk("cdev_alloc failed");
- goto unregister_chrdev;
- }
-
- // 初始化cdev数据结构,并且建立该设备与驱动操作方法集合file_operations之间的连接关系。
- cdev_init(simple_cdev, &simpledev_fops);
- // 把一个字符设备添加到系统中,通常在驱动程序的probe函数里会调用该接口来注册一个字符设备
- // 参数1 表示一个设备的cdev数据结构
- // 参数2 表示设备的设备号
- // 参数3 表示这个主设备号里可以有多少个次设备号。
- // 通常一个主设备号可以有多个次设备号不同的设备,如系统中同时存在多个串口,名字都是tty开头,他们的主设备号都是4
- ret = cdev_add(simple_cdev, dev, count);
- if (ret) {
- printk("cdev_add failed");
- 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,通常在驱动程序的卸载函数里面会调用
- cdev_del(simple_cdev);
- unregister_chrdev:
- unregister_chrdev_region(dev, count);
-
- return ret;
- }
-
- static void __exit simple_char_exit(void)
- {
- printk("removing device\n");
-
- if (simple_cdev) {
- cdev_del(simple_cdev);
- }
-
- unregister_chrdev_region(dev, count);
- }
-
- module_init(simple_char_init);
- module_exit(simple_char_exit);
-
- MODULE_AUTHOR("marvin");
- MODULE_LICENSE("GPL");
- MODULE_DESCRIPTION("simple character device");
-
makefile:
- ifneq ($(KERNELRELEASE),)
- MODULE_NAME = simplechardev
- $(MODULE_NAME)-objs := simple_chardev.o
- obj-m := $(MODULE_NAME).o
- else
- KERNEL_DIR = /lib/modules/`uname -r`/build
- MODULEDIR := $(shell pwd)
-
- .PHONY: modules
- default: modules
-
- modules:
- make -C $(KERNEL_DIR) M=$(MODULEDIR) modules
-
- clean distclean:
- make -C /lib/modules/$(shell uname -r)/build/ M=$(PWD) clean
- rm -f *.o *.mod.c .*.*.cmd *.ko
- rm -rf .tmp_versions
- endif
-
编译完之后,直接用insmod加载
- $ sudo insmod simplechardev.ko
-
对应的内核日志输出
- [ 3617.974349] succeeded register char device: simple_chardev
- [ 3617.974352] Major number = 238, minor number = 0
-
可以看到,内核模块在初始化时输出了系统分配的主设备号238,以及次设备号0。查看/proc/devices这个proc虚拟文件系统中的devices节点信息,看到了生成了名称为simple_chardev的设备,主设备号为238
- $ cat /proc/devices
- 238 simple_chardev
-
生成的设备需要在/dev目录下面生成对应的节点,目前需要手动生成:
- $ sudo mknod /dev/simple_chardev c 238 0
- $ ls -l /dev/simple_chardev
- crw-r--r-- 1 root root 238, 0 6月 19 23:41 /dev/simple_chardev
-
上面已经完成了和内核相关的事情,接下来需要编写一个用户测试程序操作该字符设备。
- #include <stdio.h>
- #include <fcntl.h>
- #include <unistd.h>
-
- #define DEMO_DEV_NAME "/dev/simple_chardev"
-
- 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, sizeof(buffer));
- close(fd);
-
- return 0;
- }
-
编译运行:
- $ ./test
-
内核会打印read日志:
- [ 5321.423689] simpledev_open: major = 238, minor = 0
- [ 5321.423694] simpledev_read enter
-
可以看到,日志里面有simpledev_open和simpledev_read函数的输出日志,说明测试程序成功操作了测试驱动。
字符设备驱动管理的核心对象是以字符为数据流的设备,在Linux内核中,使用struct cdev数据结构来对其进行抽象和描述。
- struct cdev {
- struct kobject kobj;
- struct module *owner;
- const struct file_operations *ops;
- struct list_head list;
- dev_t dev;
- unsigned int count;
- } __randomize_layout;
-
其中
- #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))
-
也就是高12比特为主设备号,低20比特为次设备号。
字符设备驱动的初始化函数(probe函数)的一个重要工作就是为设备分配设备号。设备号是系统中的资源,内核必须避免发生两个设备驱动程序使用同一个设备号的情况。Linux内核提供两个接口来完成设备号的申请:
- extern int alloc_chrdev_region(dev_t *, unsigned, unsigned, const char *);
- extern int register_chrdev_region(dev_t, unsigned, const char *);
-
register_chardev_region函数需要指定主设备号,可以连续分配多个。也就是说,在使用该函数之前,驱动编写者必须保证要分配的主设备号在系统中没有被人使用。内核文档documentation/devices.txt文件描述了系统中已经分配出去的主设备号,因此使用该接口函数的程序员应该事先约定该文档,避免使用已经被系统占用的主设备号。
alloc_chardev_region会自动分配一个主设备号,可以避免和系统占用的主设备号重复,推荐使用该接口来分配主设备号。
在驱动程序的卸载函数中需要把主设备号释放给系统:
- extern void unregister_chrdev_region(dev_t, unsigned);
-
在Linux系统中,万物皆文件。设备节点也算一个特殊的文件,称为设备文件,是连接内核空间驱动程序和用户空间应用程序的桥梁。如果应用程序想要使用驱动程序提供的服务或者操作设备,那么需要通过访问该设备文件来完成。设备文件使得用户程序操作硬件设备就像操作普通文件一样方便。
主设备号代表一类设备,次设备号代表同一类设备的不同个体,每个次设备号都有一个不同的设备节点。
按照Linux的习惯,系统中所有的设备节点都存放在/dev目录中。dev目录是一个动态生成的、使用devtmpfs虚拟文件系统挂载的、基于RAM的虚拟文件系统。
- $ ls -l /dev
- total 0
- crw-r--r-- 1 root root 10, 235 Jun 20 21:34 autofs
- drwxr-xr-x 2 root root 340 Jun 20 21:34 block
- drwxr-xr-x 2 root root 80 Jun 20 21:34 bsg
- crw------- 1 root root 10, 234 Jun 20 21:34 btrfs-control
- drwxr-xr-x 3 root root 60 Jun 20 21:34 bus
- drwxr-xr-x 2 root root 3000 Jun 20 21:34 char
- crw------- 1 root root 5, 1 Jun 20 21:34 console
- lrwxrwxrwx 1 root root 11 Jun 20 21:34 core -> /proc/kcore
- crw------- 1 root root 10, 125 Jun 20 21:34 cpu_dma_latency
- crw------- 1 root root 10, 203 Jun 20 21:34 cuse
- drwxr-xr-x 8 root root 160 Jun 20 21:34 disk
- drwxr-xr-x 3 root root 100 Jun 20 21:34 dri
-
第一列中c表示字符设备,d表示块设备。后面还会显示设备的主设备号和次设备号。
设备节点的生成有两种方式:一种是使用mknod命令手动生成,一种是使用udev机制动态生成。
手工生成设备节点可以使用mknod命令,格式如下:
- $ sudo mknod filename type major minor
-
udev是一个工作在用户空间的工具,它能够根据系统中硬件设备的状态动态地更新设备节点,包括设备节点的创建、删除等。这个机制必须联合sysfs和tmpfs来实现,sysfs为udev提供设备入口和uevent通道,tmpfs为udev设备文件提供存放空间。
在上面的简单字符设备驱动中,定义了open、release、read、write等方法,这些函数统称为file_operations方法。
通过cdev_init()函数,我们使得file_operations方法集和设备建立的一个连接关系,这样我们可以在用户空间通过open等函数操作设备节点。
以open操作为例:
open函数的第一个参数是设备文件名,第二个参数用来指定文件打开的属性。open函数执行成功会放回一个文件描述符,否则返回-1。
应用程序的open函数执行时,会通过系统调用进入内核空间,在内核空间的虚拟文件系统层(VFS)经过转换,最后会调用设备驱动的file_operations方法集中的open方法。因此,驱动开发这必须要了解file_operations结构体的组成,该结构体定义在include/linux/fs.h文件中。字符设备驱动程序的核心开发工作是实现file_operations方法集中的各种方法。虽然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 (*iopoll)(struct kiocb *kiocb, struct io_comp_batch *,
- unsigned int flags);
- 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 mmap_supported_flags;
- 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 *);
- 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);
- void (*splice_eof)(struct file *file);
- int (*setlease)(struct file *, int, struct file_lock **, void **);
- long (*fallocate)(struct file *file, int mode, loff_t offset,
- loff_t len);
- void (*show_fdinfo)(struct seq_file *m, struct file *f);
- #ifndef CONFIG_MMU
- unsigned (*mmap_capabilities)(struct file *);
- #endif
- ssize_t (*copy_file_range)(struct file *, loff_t, struct file *,
- loff_t, size_t, unsigned int);
- loff_t (*remap_file_range)(struct file *file_in, loff_t pos_in,
- struct file *file_out, loff_t pos_out,
- loff_t len, unsigned int remap_flags);
- int (*fadvise)(struct file *, loff_t, loff_t, int);
- int (*uring_cmd)(struct io_uring_cmd *ioucmd, unsigned int issue_flags);
- int (*uring_cmd_iopoll)(struct io_uring_cmd *, struct io_comp_batch *,
- unsigned int poll_flags);
- } __randomize_layout;
-
misc device称为杂项设备,Linux内核把一些不符合预先确定的字符设备划分为杂项设备,这类设备的主设备号是10。内核使用struct miscdevice数据结构来描述这类设备:
- struct miscdevice {
- int minor;
- const char *name;
- const struct file_operations *fops;
- struct list_head list;
- struct device *parent;
- struct device *this_device;
- const struct attribute_group **groups;
- const char *nodename;
- umode_t mode;
- };
-
内核提供了注册杂项塞红包的两个接口函数,驱动程序采用misc_register函数来注册,他会自动创建设备节点,不需要使用mknod命令手动创建设备节点,因此使用misc机制来创建字符设备驱动是比较方便的:
- extern int misc_register(struct miscdevice *misc);
- extern void misc_deregister(struct miscdevice *misc);
-
改造前面写的字符驱动使用misc机制:simple_miscchardev.c
- #include <linux/module.h>
- #include <linux/fs.h>
- #include <linux/init.h>
- #include <linux/miscdevice.h>
-
- #define DEMO_NAME "simple_miscchardev"
- static struct device *simple_miscchardev;
-
- static int simpledev_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 simpledev_release(struct inode *inode, struct file *file)
- {
- return 0;
- }
-
- static ssize_t simpledev_read(struct file *file, char __user* buf, size_t lbuf, loff_t *ppos)
- {
- printk("%s enter\n", __func__);
- return 0;
- }
-
- static ssize_t simpledev_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 simpledev_fops = {
- .owner = THIS_MODULE,
- .open = simpledev_open,
- .release = simpledev_release,
- .read = simpledev_read,
- .write = simpledev_write
- };
-
- static struct miscdevice simple_misc_device = {
- .minor = MISC_DYNAMIC_MINOR,
- .name = DEMO_NAME,
- .fops = &simpledev_fops,
- };
-
- static int __init simple_char_init(void)
- {
- int ret;
-
- ret = misc_register(&simple_misc_device);
- if (ret) {
- printk("failed to allocate misc char device region");
- return ret;
- }
-
- simple_miscchardev = simple_misc_device.this_device;
- printk("Major number = %d, minor number = %d\n", MAJOR(simple_miscchardev->devt), MINOR(simple_miscchardev->devt));
- return 0;
- }
-
- static void __exit simple_char_exit(void)
- {
- printk("removing device\n");
-
- misc_deregister(&simple_misc_device);
- }
-
- module_init(simple_char_init);
- module_exit(simple_char_exit);
-
- MODULE_AUTHOR("marvin");
- MODULE_LICENSE("GPL");
- MODULE_DESCRIPTION("simple misc character device");
-
加载模块:
- $ sudo insmod simplemiscchardev.ko
-
-
内核日志输出:
- [ 7276.953967] simplemiscchardev: loading out-of-tree module taints kernel.
- [ 7276.953975] simplemiscchardev: module verification failed: signature and/or required key missing - tainting kernel
- [ 7276.954934] Major number = 10, minor number = 122
-
查看dev目录,设备节点已经创建好,其中主设备号是10,次设备号是动态分配的122。
- $ ls -lg /dev | grep simple
- crw------- 1 root 10, 122 Jun 20 23:35 simple_miscchardev
-
运行测试程序:
- $ sudo ./test
-
-
内核会打印read日志:
- [ 7588.956688] simpledev_open: major = 10, minor = 122
- [ 7588.956693] simpledev_read enter
-