Linux字符设备驱动框架总结

字符设备驱动简介

Linux的三大设备驱动分别是字符设备、块设备、网络设备。其中,字符设备是按照字节流进行读写操作的设备,读写数据是分先后顺序的。
Linux应用程序对驱动程序的调用流程如下:
Linux应用程序对驱动程序的调用流程
在Linux系统中,应用程序运行在应用空间,而驱动属于内核的一部分,运行于内核空间。如果用户空间想对内核空间进行操作,需要使用“系统调用”(实际上是利用中断实现,可参考Linux系统调用详解(实现机制分析)–linux内核剖析(六))的方式来实现从用户态“陷入”内核态,才能实现对底层驱动的操作。C库提供了open/close/write/read等函数。在Linux系统中,系统调用作为C库的一部分,调用open函数时的流程如下:
open函数调用过程
每一个系统调用,在驱动中都有与之对应的驱动函数。在Linux内核文件 include/linux/fs.h 中定义了 内核驱动操作函数集合的结构体 file_operations,如下:

struct file_operations {
    struct module *owner;   //拥有该结构体的模块的指针,一般设置为THIS_MODULE
    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 *);
    unsigned int (*poll) (struct file *, struct poll_table_struct *); //轮询函数,用于查询设备是否可以进行非阻塞的读写(POLL机制)
    long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long); //提供对于设备的控制功能,与应用程序ioctl函数对应,32位系统(读写设备参数、读设备状态、控制设备)
    long (*compat_ioctl) (struct file *, unsigned int, unsigned long); //同unlocked_ioctl,64位系统
    /*用户将设备的内存映射到用户空间中,方便用户直接操作,一般帧缓冲设备会使用此函数,如LCD驱动的显存*/
    int (*mmap) (struct file *, struct vm_area_struct *);
    int (*mremap)(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 *); //关闭设备文件,对应应用程序的close函数
    int (*fsync) (struct file *, loff_t, loff_t, int datasync); 
    int (*aio_fsync) (struct kiocb *, int datasync); //同fasync函数,但是是异步处理待处理数据
    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 **, 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
};

字符设备驱动框架代码

下面先以虚拟字符设备hello_drv来展示字符设备的驱动框架,然后再进行分析。

 /* *************************** hello_drv.c *************************** */
 /*
 虚拟字符设备:简单实现对设备文件的读写操作
*/
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
//主设备号
static int major = 0;  /* 主设备号, 0 表示由系统分配 */
static char kernel_buf[1024]; /* 内核数据缓存区 */
static struct class *hello_class;

#define MN(a,b)   ((a) <  (b) ? (a) : (b))

/*
* @description : 从设备读取数据
* @param       :
*        file  : 打开的设备文件(文件描述符)
*        buf   : 返回给用户空间的数据缓冲区
*        size  :要读取的数据长度
*        offset:相对于文件首地址的偏移
* @return      : 读取的字节数,如果为负值,表示读取失败
*/
static ssize_t hello_drv_read(struct file* file, char __user *buf, size_t size, loff_t *offset) {
	int err;
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	err = copy_to_user(buf, kernel_buf, MIN(1024, size));
	return MIN(1024, size);
}

/*
* @description : 向设备写数据
* @param       :
*        file  : 打开的设备文件(文件描述符)
*        buf   : 要写给设备写入的数据
*        size  :要写入的数据长度
*        offset:相对于文件首地址的偏移
* @return      : 写入的字节数,如果为负值,表示读取失败
*/
static ssize_t hello_drv_write(struct file* file, const char __user *buf, size_t size, loff_t *offset) {
	int err;
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	err = copy_from_user(kernel_buf, buf, MIN(1024, size));
	return MIN(1024, size);
}

/* 打开设备 */
static int hello_drv_open(struct inode* node, struct file* file) {
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	return 0;
}

/* 关闭设备 */
static int hello_drv_close(struct inode* node, struct file* file) {
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	return 0;
}

/* 设备操作函数集合结构体 */
static struct file_operations hello_drv = {
	.owner = THIS_MODULE,
	.open = hello_drv_open,
	.read = hello_drv_read,
	.write = hello_drv_write,
	.release = hello_drv_close,
};

/* 驱动入口函数 */
static void __init hello_init(void) {
	int err;

	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	major = register_chrdev(0, "hello", &hello_drv);   
	hello_class = class_create(THIS_MODULE, "hello_class");
	err = PTR_ERR(hello_class);
	if (IS_ERR(hello_class)) {
		printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
		unregister_chrdev(major, "hello");
		return -1;
	}
	device_create(hello_class, NULL, MKDEV(major, 0), NULL, "hello");	/* 设备节点/dev/hello创建 */
	return 0;
}

/* 驱动出口函数 */
static void __exit hello_exit(void)	{
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	device_destroy(hello_class, MKDEV(major, 0);
	class_destroy(hello_class);
	unregister_chrdev(major, "hello");
}
module_init(hello_init);	/*指定设备驱动入口函数*/
module_exit(hello_exit);	/*指定设备驱动出口函数*/
MODULE_LICENSE("GPL");

从上面的代码中,我们可以总结出一套字符设备驱动编写的大致过程:

1. 指定模块的入口函数和出口函数
2. 在入口函数注册字符设备并创建设备节点,在出口函数注销字符设备并销毁设备节点
2. 在入口函数注册字符设备并创建设备节点,在出口函数注销字符设备并销毁设备节点
3. 注册字符设备时,指定设备号并绑定设备操作file_operations结构体
4. 完成对file_operations结构体中相应操作函数的编写

驱动的Makefile如下:(注意代码中注释)

## 1.KERDIR 对应的是内核源码路径,需先配置,编译。必须配置好下列环境:
##   ARCH=arm
##   CROSS_COMPILE=arm-linux-gnu- (指定自己的编译器)
##   PATH=xxx/xxx/bin (编译器的路径)

KERDIR = /home/xxx/xxx #内核源码路径
all:
	make -C $(KERDIR) M=`pwd` modules
clean:
	make -C $(KERDIR) M=`pwd` clean

obj-m	+= hello_drv.o

编译成功后,会生成hello_drv.ko文件,为linux内核模块。
拷贝该模块到设备的linux文件系统中,执行如下命令即可实现模块的加载与卸载:

## 加载
$ insmod hello_drv.ko #不能解决模块依赖关系
$ modprobe hello_drv.ko #分析模块依赖关系并依次加载,默认从/lib/modules/<kernel-version>/下查找模块
## 卸载
$ rmmod hello_drv.ko #只卸载该模块 
$ modprobe -r hello_drv.ko #卸载该模块及其依赖模块

字符设备驱动的编写要点

在Linux内核中,使用结构体cdev来描述字符设备,其包含的两个要素是 file_operations 和 设备号,其定义在 include/linux/cdev.h中,如下:

struct cdev { 
	struct kobject kobj; //内嵌的内核对象. 
	struct module *owner; //该字符设备所在的内核模块的对象指针,一般置为THIS_MODULE,表示模块 
	const struct file_operations *ops; //该结构描述了字符设备所能实现的方法,是极为关键的一个结构体. 
	struct list_head list; //用来将已经向内核注册的所有字符设备形成链表. 
	dev_t dev; //字符设备的设备号,由主设备号和次设备号构成. 
	unsigned int count; //隶属于同一主设备号的次设备号的个数. 
};

1. 设备号:用于确定设备的唯一性,由主设备号和次设备号组成
2. file_operations:定义设备驱动提供给VFS的接口方式,如常见的open()/read()/write()等。

字符设备、字符设备驱动与用户空间访问该设备的程序三者的关系如下:
字符设备、字符设备驱动与用户空间访问该设备的程序三者的关系
从上面的关系图可以看出,字符设备驱动需要完成4个要点:
1. 设备号的申请与释放
2. 字符设备的注册与注销
3. 设备操作的实现(file_operations)
4. 设备文件的创建

下面将按照这三个要点展开分析:

1. 设备号的申请与释放

一个字符设备或块设备都有一个主设备号和一个次设备号。主设备号用来标志与设备文件相连的驱动程序,用来反映设备类型。次设备号被驱动程序用来辨别操作的是哪个设备,用来区分同类型的设备。 在32位机中,高12位表示主设备号,低20位表示次设备号。内核提供了宏实现主设备,次设备号与设备号之间的转换:

//dev   -- 设备号
//major -- 主设备号
//minor -- 次设备号
int major = MAJOR(dev_t dev);
int minor = MINOR(dev_t dev);
dev_t dev = MKDEV(int major, int minor);

可使用如下命令查看已使用的设备号和设备名:

$cat /proc/devices

设备号的申请

设备号的申请有2种方式,有静态申请和动态申请。

静态申请

对于已知某主设备号为被使用的情况下,可使用该种方式申请设备号,用到的API为:

int register_chrdev_region(dev_t from, unsigned count, const char *name);

此API会直接申请指定的设备号from,如果该设备号已被使用,则申请失败,返回错误的负值。

动态申请

为确保申请到合适的设备号,一般采用动态申请的方式申请设备号,采用的API为:

int alloc_chrdev_region(dev_t* dev, unsigned baseminor, unsigned count, const char *name);

此API将从主设备号0开始查找未被使用的设备号,并将其结果返回到dev。
设备号的申请接口其实都调用了__register_chrdev_region函数,从这个函数可以看出register_chrdev_region是直接将major注册进去,而alloc_chrdev_region从major=0开始,逐个查找设备号,直到找到闲置的设备号,才将其注册进去。

设备号的释放

设备号的释放采用的API如下:

void unregister_chrdev_region(dev_t from, unsigned count);

2. 字符设备的注册与注销

在申请设备号成功之后,就可以开始对字符设备进行注册,注册需要完成的关键步骤就是建立cdev和file_operations之间的连接。下面主要介绍字符设备注册与注销相关的API。
1. 字符设备初始化接口

void cdev_init(struct cdev *cdev, const struct file_operations *fops);

该函数主要对struct cdev结构体做初始化,最重要的是建立cdev和file_operations之间的连接。
2. 字符设备内存申请接口

struct cdev* cdev_alloc(void);

该函数动态申请了一个cdev内存,并做了cdev_init()中所作的初始化工作,但并未指定file_operations ops成员,后续需显示的完成cdev和file_operations之间的连接,即:.ops = xxx_ops
3. 字符设备添加接口(注册)

int cdev_add(struct cdev *p, dev_t dev, unsigned count);

该函数向内核注册一个字符设备,即正式通知内核p代表的字符设备已经可以使用。
4. 字符设备删除接口(注销)

void cdev_del(struct cdev *p);

该函数向内核注销一个字符设备,即正式通知内核p代表的字符设备已经不可以使用。

上述两个步骤一起才能实现字符设备的注册与注销,内核还提供了相关的API来一次完成上述的注册注销。
(1) 字符设备直接注册方式:
static inline int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops);
该函数可实现字符设备cdev与设备号和file_operations的连接。当传入的major为0时,函数内部会自动查找合适的主设备号。返回值为主设备号或者失败负值。
字符设备直接注销方式:
static inline void unregister_chrdev(unsigned int major, const char *name);
该函数可实现字符设备cdev的删除以及设备号的释放。

3. 设备操作的实现(file_operations)

设备操作的实现主要是通过自定义file_operations结构体,并实现该结构体中对应的操作函数成员,如 open,write,read,release,ioctl等。这部分的实现是驱动操作的核心,也需要根据设备的特性进行编写。

4. 设备文件的创建

在使用命令加载驱动模块之后,如果内核的配置不支持udev或者mdev机制,则需要手动创建设备文件节点 /dev/xxx,否则可利用udev(mdev)机制实现自动创建。

手动创建

使用命令mknod可手动创建设备文件,格式如下:

mknod filename type major minor

如设备名为xxx,设备类型为字符设备,主设备号为200,次设备号为0,则为 mknod /dev/xxx c 200 0

自动创建

首先得确保支持udev(mdev)机制,由busybox配置。在驱动代码里调用class_create为该设备创建一个class,再为每个设备调用device_create创建对应的设备。
相关部分可参考:Linux 字符设备驱动开发 (二)—— 自动创建设备节点

总结

字符设备驱动模型总结

参考资料:
Linux 字符设备驱动结构(一)—— cdev 结构体、设备号相关知识解析

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值