代码:
#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