Linux字符设备驱动框架总结
字符设备驱动简介
Linux的三大设备驱动分别是字符设备、块设备、网络设备。其中,字符设备是按照字节流进行读写操作的设备,读写数据是分先后顺序的。
Linux应用程序对驱动程序的调用流程如下:
在Linux系统中,应用程序运行在应用空间,而驱动属于内核的一部分,运行于内核空间。如果用户空间想对内核空间进行操作,需要使用“系统调用”(实际上是利用中断实现,可参考Linux系统调用详解(实现机制分析)–linux内核剖析(六))的方式来实现从用户态“陷入”内核态,才能实现对底层驱动的操作。C库提供了open/close/write/read等函数。在Linux系统中,系统调用作为C库的一部分,调用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 字符设备驱动开发 (二)—— 自动创建设备节点