Linux字符设备驱动程序框架概述
一、前言
最近跟着正点原子的视频进行学习了字符设备驱动,写下本篇文章以备忘。若有不妥之处望指点。学完字符设备驱动的感想是驱动程序的编写不像应用程序编写那样讲求各种技巧,体现各种高效率,驱动程序开发是按照一定的框架来,至少字符设备驱动是这样,本文重点概述的就是字符设备驱动开发的框架,也可以说是流程的。在结尾附上代码。下面正式进入正题。
二、总体流程概述
上节已经说明字符设备驱动是按照固定框架来,一下将分条进行概述:
- file_operation结构体的初始化,内容包括:open、write、read、close等基本函数的编写;
- 字符设备的初始化,内容包括:a.初始化设备结构体(包括:设备号、主设备号、次设备号、cdev结构体、class结构体等,成员按照实际需求来),b.分配字符设备号(动态分配和静态分配),c.注册字符设备号(cdev),d.自动创建设备节点(类class和设备device) ;
- 字符设备的注销 :注销的内容包括:注销设备、注销设备类、注销设备号;
- 驱动函数注册 :内容包括:module_init和module_exit,当然也有一些说明,比如:MODULE_AUTHOR,MODULE_VERSION,MODULE_DESCRIPTION,MODULE_LICENSE等
三、分条介绍
1、file_operation结构体的初始化模块
file_operation结构体的成员函数是用于操作这个字符设备。比较数值的write,read。open,close等函数都是该结构体的成员。而对这个结构体的初始化实际上就是编写以上一些功能函数,并将其添加经file_operation结构体。结构体的内容如下,file_operation结构体内容较多,在这里本文只对write、read、open、release和ioctl函数进行讲解。
struct file_operations {
/*拥有该结构的模块计数,一般为THIS_MODULE*/
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 (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
int (*readdir) (struct file *, void *, filldir_t);
/*轮询函数,判断目前是否可以进行非阻塞的读取或写入*/
unsigned int (*poll) (struct file *, struct poll_table_struct *);
/*执行设备的I/O命令*/
int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
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 *);
/*打开设备文件*/
int (*open) (struct inode *, struct file *);
int (*flush) (struct file *, fl_owner_t id);
/*关闭设备文件*/
int (*release) (struct inode *, struct file *);
int (*fsync) (struct file *, struct dentry *, int datasync);
int (*aio_fsync) (struct kiocb *, int datasync);
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 **);
};
1.1 open()与release()函数
先看看open函数的原型;
int (*open) (struct inode *, struct file *);
应用层的open函数与之对应,当应用层使用open()打开一个文件时,在驱动层该open()函数会被执行,。使用范例如下,当open被执行时可以在/var/log/kern.log中查看printk输出的信息。
static int chrdevbase_open(struct inode *inode, struct file *filp)
{
printk("chrdevbase open!\r\n");
return 0;
}
release()函数原型如下:
int (*release) (struct inode *, struct file *);
应用层的close()函数与之对应,通常在应用层执行close关闭文件时该函数被执行。使用示例如下,同样在关闭文件时可以在/var/log/kern.log中查看printk输出的信息。
static int chrdevbase_release(struct inode *inode, struct file *filp)
{
printk("chrdevbase release!\r\n");
return 0;
}
1.2 read()与write()函数
在这里的read()和write()函数与应用层的read()和write()不同,应用层的读写文件是调用的是驱动层的读写函数,那么在这里就存在一个缓冲区的问题,即将内核空间缓冲区的数据拷贝到用户空间的缓冲区中,或者将用户空间缓冲区的数据拷贝到内核空间的缓冲区中。这里就不是简单的直接复制,需要通过copy_to_user()和copy_from_user()函数来完成这个搬运工作,这也是read(0和write(0函数的核心。
在这里主要介绍cpoy_to_user和copy_from_user两个函数。他们的原型如下,两个函数的返回值为0则表示成功。
static inline unsigned long __must_check copy_to_user(void __user *to, const void *from, unsigned long n)
static inline unsigned long __must_check copy_from_user(void *to, const void __user *from, unsigned long n)
在这两个函数的使用过程中很多新手(没错就是我)会有个误区,从字面意思来看read函数调用copy_from_user来获取数据,write函数调用copy_to_user来写数据。其实并不是这样,造成这样的错误的原因是选择参考系错误,正确的是应该是以内核态来看,比如copy_to_user指的是将数据从内核空间写到用户空间,而copy_from_user指的是从用户空间写到内核空间。那么现在就清楚了,read函数调用copy_to_user将应用层的user需要的数据copy给它,write函数调用copy_form_user将应用层的user需要写的数据copy到内核。
在看实例之前先了解下read和write函数的原型。
ssize_t (*write) (struct file * filp, const char __user * buffer, size_t count, loff_t * ppos);
ssize_t (*read) (struct file * filp, char __user * buffer, size_t size , loff_t * p);
应用实例:
static ssize_t chrdevbase_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
int retvalue = 0;
/* 向用户空间发送数据 */
memcpy(readbuf, kerneldata, sizeof(kerneldata));
retvalue = copy_to_user(buf, readbuf, cnt);
if(retvalue == 0){
printk("kernel senddata ok!\r\n");
}else{
printk("kernel senddata failed!\r\n");
}
//printk("chrdevbase read!\r\n");
return 0;
}
static ssize_t chrdevbase_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
int retvalue = 0;
/* 接收用户空间传递给内核的数据并且打印出来 */
retvalue = copy_from_user(writebuf, buf, cnt);
if(retvalue == 0){
printk("kernel recevdata:%s\r\n", writebuf);
}else{
printk("kernel recevdata failed!\r\n");
}
//printk("chrdevbase write!\r\n");
return 0;
}
1.3 file_operation结构体初始化
以上就是一些最基本的功能函数的介绍,在上述的函数写完之后,将其初始化进file_operation结构体。如下:
static struct file_operations newchrled_fops = {
.owner = THIS_MODULE,
.open = chrdevbase_open,
.read = chrdevbase_read,
.write = chrdevbase_write,
.release = chrdevbase_release,
};
2、字符设备初始化模块
在第二部分中将该部分分成了几个步骤,都是在__init 初始化函数中进行,当在中端执行insmod时,__init函数被执行。在这里将对初始化的步骤详细描述。
2.1 初始化设备
初始化设备的实质就是定义一个结构体,结构体的包含了后续将要用到的各种变量。比如设备号、主设备号、次设备号、用于注册字符设备的cdev结构体以及用于创建设备节点的class结构和device结构等。字符设备结构体看需求对成员进行添加或者删除,也可以不定义结构体,单独定义这些变量,这里定义成结构体是方便于管理和理解,免得显得很混乱。自定义的结构体如下,在这里只是举例,实际成员按照需求来增删。
struct newchrled_dev{
dev_t devid; /* 设备号 */
struct cdev cdev; /* cdev */
struct class *class; /* 类 */
struct device *device; /* 设备 */
int major; /* 主设备号 */
int minor; /* 次设备号 */
};
2.2 分配字符设备号
分配字符设备号分为两种,即静态分配和动态分配。
静态分配使用register_chrdev_region(),原型如下:
int register_chrdev_region(dev_t from, unsigned count, const char *name)
参数:dev_t from:需要注册的设备号,该设备号是人为指定的。unsigned count:需要注册的设备数。const char *name 设备名称。
注意:这里的设备号需要通过MKDEV宏对设备结构体中的主设备号和次设备号生成:from=MKDEV(mydev.MAJOR,mydev.MINOR)。如果制定的设备号已经使用,则函数返回负值。
动态分配设备号使用alloc_chrdev_region(),汉书原型如下:
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,const char *name)
参数:dev_t *dev是一个输出型的参数,由系统自动分配的设备号,可以通过宏MAJOR(dev)和MINOR(dev)来查看系统分配的主设备号和次设备号。unsigned baseminor 次设备号的起始0~255。unsigned count,const需要申请设备号的个数。const char *name设备的名称,会在/proc/devices下显示,同时也可以显示设备号。
以上便是字符设备号的两种分配方法,另外在早期的内和版本中提高的是chrdev_region函数,他将动态分配和静态分配放在一起,而签署两种方法是这个函数的一个分化和改进。
2.4 注册字符设备
注册字符设备到系统主要通过两个函数实现,分别是cdev_init函数和cdev_add函数。当然有cdev_add添加函数就有cadev_del删除函数。在说明两个函数之前先指明下cdev结构体变量。
struct cdev {
struct kobject kobj; // 每个 cdev 都是一个 kobject
struct module *owner; // 指向实现驱动的模块
const struct file_operations *ops; // 操纵这个字符设备文件的方法
struct list_head list; // 与 cdev 对应的字符设备文件的 inode->i_devices 的链表头
dev_t dev; // 起始设备编号
unsigned int count; // 设备范围号大小
};
cdev_init函数的功能是将cdev和file_operations关联起来。函数原型如下:
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
{
memset(cdev, 0, sizeof *cdev);
INIT_LIST_HEAD(&cdev->list);
kobject_init(&cdev->kobj, &ktype_cdev_default);
cdev->ops = fops;
}
有函数的定义可知,传入的cdev是一个为初始化的cdev结构体指针和已经初始化的file_operation结构体。
cdev_add函数的功能是将cdev结构体和设备号关联起来。函数原型如下:
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
{
int error;
p->dev = dev;
p->count = count;
error = kobj_map(cdev_map, dev, count, NULL,
exact_match, exact_lock, p);
if (error)
return error;
kobject_get(p->kobj.parent);
return 0;
}
入口参数是cdev结构体,之前分配的设备号以及注册数量。
2.5 自动创建设备节点
自动创建设备节点的反义词就是手动创建设备节点,也就是刚开始的时候我们经常用mknod命令去在/dev下手动添加设备节点。用了自从创建设备节点之后就不用那么麻烦了。
自动创建设备节点分为两步骤,第一步是设备类创建(class结构体),第二步
是设备节点创建(device结构体)。以上两个步骤通过class_create函数和device_create函数实现。
class_creat函数原型如下;
struct class *class_create(struct module *owner, const char *name)
其中owner所有者一般用THIS_MODULE,第二个参数name指的是设备名称。需要注意的是该函数用IS_ERR(class)来验错,如果为正值则表示创建失败。
device_create函数原型如下:
struct device *device_create(struct class *class, struct device *parent, dev_t devt, void *drvdata, const char *fmt, ...)
struct class *class表示制定所要创建的设备所从属的类,struct device *parent表示这个设备的父设备,如果没有就指定为NULL,目前来说都是制定NULL,dev_t devt表示设备号,void *drvdata表示当设备添加时回调显示的数据,onst char *fmt表示设备名称,同样用IS_ERR(device)来验错。
以上便完成了字符设备初始化流程的概述,当然在实际编程中还有一些需要注意的细节问题,这些会在后续的demo代码中指出来。
3、字符设备注销模块
自动设备欸的注销主要是使用__exit声明的函数对前面注册的设备进行注销。比如分配的设备号,注册的字符设备,设备节点,设备类等。注销他们也有特定的函数,具体如下。
功能 | 函数 |
---|---|
注销字符设备 | void cdev_del(struct cdev *p) |
注销设备号 | unregister_chrdev_region(dev_t from, unsigned count) |
注销设备节点 | void device_destroy(struct class *dev, dev_t devt); |
注销设备类 | void class_destroy(struct class *cls); |
4、驱动函数注册模块
驱动函数的注册模块实际上就是将上述的__init和__exit函数注册进内核,使用module_init和module_exit分别注册。使用方法如下:
module_init(chrdevbase_init);
module_exit(chrdevbase_exit);
除此之外,还可以根据需求添加一些说明信息
MODULE_LICENSE("GPL");
MODULE_AUTHOR("xxxxxxx");
MODULE_DESCRIPTION("chrdevbase driver");
以上便通过分模块的方式对驱动开发的流程进行了一个概述,在这里并由对一些函数的具体实现进行叙述,对于初学者来说,先要有对整个驱动开发有一个具体的印象菜是最重要的,而不是紧扣细节(其实是作者才疏学浅哈哈)。
四、驱动代码与测试代码
1 驱动代码(chrdev.c)
#include<linux/module.h>
#include<linux/kernel.h>
#include<linux/fs.h>
#include<linux/init.h>
//#include<linux/delay.h>
#include<asm/uaccess.h>
#include<linux/device.h>
#include<linux/cdev.h>
#include <linux/errno.h>
#include <linux/ide.h>
#define DEVICE_NAME "Board_Drv"
/* dev_Board设备结构体 */
struct Board{
dev_t dev; /* 设备号 */
struct cdev dev_c; /* cdev */
struct class *cdev_class; /* 类 */
struct device *device; /* 设备 */
int major; /*主设备号 */
int minor; /*次设备号 */
} ;
struct Board dev_Board;
static int dev_Board_open(struct inode *inode,struct file *file)
{
printk("dev_Board_open success\n");//该信息会在/var/log/kern.log中显示
return 0;
}
static ssize_t dev_Board_read(struct file *file,char __user *buff,size_t size,loff_t *ppos)
{
int ret;
char k_buf[50]="hello boy!";
if(copy_to_user(buff,k_buf,size)){ //将k_buf的数据通过buf送到应用层
ret=-EFAULT;
} else{
ret=size;
}
return ret; //返回读到的字节数
}
static ssize_t dev_Board_write(struct file *file,const char __user *buff,size_t size,loff_t *pp0s)
{
char k_buf[50];
int ret;
if(copy_from_user(k_buf,buff,size)){ //将应用层的数据拷贝到内核空间的k_buf中,相当于内核空间读应用层传过来的数据
ret=-EFAULT;
}else{
ret=size;
printk("kern:%s\n",k_buf);
}
return ret;
}
static const struct file_operations Board_fops={
.owner=THIS_MODULE,
.open=dev_Board_open,
.read=dev_Board_read,
.write=dev_Board_write,
};
static int __init Board_init(void)
{
int ret,err;
//分配设备号
if(dev_Board.major){
if(dev_Board.minor){
dev_Board.dev=MKDEV(dev_Board.major,dev_Board.minor);
}
else{
dev_Board.dev=MKDEV(dev_Board.major,0);
}
register_chrdev_region(dev_Board.dev,1,DEVICE_NAME); //静态分配设备号
}
else{
ret=alloc_chrdev_region(&dev_Board.dev,0,1,DEVICE_NAME); //动态分配设备号
if(ret<0)
{
printk("Board_Drv failure\n");
unregister_chrdev_region(dev_Board.dev,1);
return ret;
}
dev_Board.major=MAJOR(dev_Board.dev); //获取自动分配的主设备号
dev_Board.minor=MINOR(dev_Board.dev); //获取自动分配的次设备号
}
printk("dev_Board.major=%d,dev_Board.minor=%d\r\n",dev_Board.major,dev_Board.minor);
//注册字符设备(初始化cdev结构体)
dev_Board.dev_c.owner=THIS_MODULE;
cdev_init(&dev_Board.dev_c,&Board_fops);
if((err=cdev_add(&dev_Board.dev_c,dev_Board.dev,1))<0)
{
printk(KERN_NOTICE"error %d adding %s dev\n",err,DEVICE_NAME);
unregister_chrdev_region(dev_Board.dev,1);
return err;
}
//创建设备节点
dev_Board.cdev_class=class_create(THIS_MODULE,DEVICE_NAME); //创建类
if(IS_ERR(dev_Board.cdev_class))
{
printk("ERR:cannot creat a cdev_class\n");
unregister_chrdev_region(dev_Board.dev,1);
return -1;
}
dev_Board.device=device_create(dev_Board.cdev_class,NULL,dev_Board.dev,0,DEVICE_NAME); //创建设备
if(IS_ERR(dev_Board.device))
{
ret=PTR_ERR(dev_Board.device);
printk("fail to device_create\n");
unregister_chrdev_region(dev_Board.dev,1);
return -1;
}
return ret;
}
//字符设备注销模块
static void __exit Board_exit(void)
{
cdev_del(&dev_Board.dev_c);
device_destroy(dev_Board.cdev_class,dev_Board.dev);
class_destroy(dev_Board.cdev_class);
unregister_chrdev_region(dev_Board.dev,1);
printk("Boar_Drv exit\n");
}
module_init(Board_init);
module_exit(Board_exit);
MODULE_AUTHOR("Yang");
MODULE_VERSION("20.10.1");
MODULE_DESCRIPTION("Board driver");
MODULE_LICENSE("GPL");
2 测试代码(chrapp.c)
#include<stdio.h>
#include<unistd.h>
#include<fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
int main()
{
int fd;
fd=open("/dev/Board_Drv",O_RDWR);
if(fd<0)
{
printf("open dev error\r\n");
return 0;
}
int ret;
char buf[50]="Hey girl";
ret=write(fd,buf,sizeof(buf));
if(ret<0)
{
printf("open dev error\r\n");
return 0;
}
char read_buf[20]={'\0'};
ret=read(fd,read_buf,20);
if(ret<0)
{
printf("open dev error\r\n");
return 0;
}
printf("read_buf is %s\n",read_buf);
return 0;
}
3.Makefile
obj-m := chrdrv.o
KERNEL_DIR ?=/lib/modules/$(shell uname -r)/build
PWD :=$(shell pwd)
modules:
$(MAKE) -C $(KERNEL_DIR) M=$(PWD) modules
.PHONY:modules clean
clean:
$(MAKE) -C $(KERNEL_DIR) M=$(PWD) clean
4 运行代码
1.首先对驱动文件chrdev.cj通过makefile进行编译生成chrdev.ko文件。
2.然后对测试程序chrapp.c进行编译(sudo gcc chrapp.c -o chraoo)。
3.加载驱动sudo insmod chrdev.ko,然后执行sudo cat /proc/devices就可以找到设备,我们这里是Board_Drv和相关的设备号,再执行sudo ls /dev,就可以找到Board_Drv设备节点,再次执行 sudo cat /var/log/ken.log就可以看到我们打印的新注册设备的主设备号和次设备号。
4,执行测试代码 sudo ./chrapp,可以在终端上看到来自内核的信息“‘hello boy!’,通过tail /var/log/kern.log可以看到用户写给内核的信息“‘hey girl!’。
5,卸载驱动,执行sudo rmmod chrdev.ko,再次查看/dev和/proc/devives,此时Board_Drv已经没有啦,然后查看/var/log/kern.log就可以看到我们打印的卸载信息。
五、总结
以上便完成了对字符设备驱动流程的一个概述,本文的重点是流程,所以并没有对很多结构体、函数等细节进行过多的描述,如果对你有一点点的帮助,将会是我的荣幸。文中若有不当之处,欢迎指正!
同时本文编写过程中,有参考部分优秀博文,在这里推荐给大家。
Linux 字符设备驱动开发基础(三)—— read()、write() 相关函数解析
Linux 字符设备驱动(三)—字符设备驱动开发完整流程