您当前的位置:首页 > 计算机 > 系统应用 > Linux

Linux驱动程序——简单字符设备驱动

时间:07-30来源:作者:点击数:
城东书院 www.cdsy.xyz

背景

目的

  1. 编写简单的字符设备驱动,实现基本的open、read和write方法
  2. 编写相应的用户空间测试程序,调用read函数,并能看到对应的驱动程序执行日志

实现

代码:

#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_opensimpledev_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;

其中

  • kobj:用于Linux设备驱动模型
  • owner:字符设备驱动程序所在的内核模块对象指针
  • ops:字符设备驱动程序中最关键的一个操作函数,在和应用程序的交互过程中起到桥梁枢纽作用
  • list:用来将字符设备串成一个链表
  • dev:字符设备的设备号,由主设备号和次设备号组成
  • count:同属一个主设备号的此设备号的个数
    主设备号和此设备号可以通过下面的宏来获取:
#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是一个工作在用户空间的工具,它能够根据系统中硬件设备的状态动态地更新设备节点,包括设备节点的创建、删除等。这个机制必须联合sysfstmpfs来实现,sysfsudev提供设备入口和uevent通道,tmpfsudev设备文件提供存放空间。

字符设备的操作方法集合

在上面的简单字符设备驱动中,定义了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;
  • llseek:用来修改文件的当前读写位置,并返回新位置
  • read:用来从设备驱动中读取数据到用户空间,函数返回成功读取的字节数,失败返回负数
  • write:用来把用户空间的数据写入设备中,函数返回成功写入的字节数
  • poll: 用来查询设备是否可以立即读写,该方法主要用于阻塞型I/O操作
  • unlocked_ioctl和compat_ioctl:用来提供与设备相关的控制命令的实现
  • mmap:将设备内存映射到进程的虚拟地址空间中
  • open:用来打开设备
  • release:用来关闭设备
  • fsync:实现一种称为异步通知的方法

misc设备

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
城东书院 www.cdsy.xyz
方便获取更多学习、工作、生活信息请关注本站微信公众号城东书院 微信服务号城东书院 微信订阅号
推荐内容
相关内容
栏目更新
栏目热门
本栏推荐