Linux 驱动开发笔记(三)
本节介绍简单字符设备驱动开发,包括编写驱动、编译,创建设备,读写设备。
一、开发环境
系统版本:Ubuntu 22.04 LTS
内核版本:5.15.0-40-generic
【注】:系统之直接装载硬件上的,没有用虚拟机
二、简介
- 如何标识驱动。驱动设备在Linux操作系统中通过设备号标识设备。每个设备都有主设备号和次设备号,其中主设备号一般对应于驱动,即同一个驱动对应的多个设备的主设备号相同。
- 驱动与文件系统的衔接。驱动在代码中需要实现文件系统的接口供系统调用。应用程序调用操作系统函数,操作系统的函数通过文件系统的接口调用驱动的方法。
- 设备信息的存储。驱动在Linux中通过哈希表存储。哈希表中存储了设备结构体,其中包含了设备号,设备的操作方法,设备名等信息。该结构体在下一节介绍。
二、驱动编写步骤及重要结构和函数
由于需要通过文件系统读写设备,因此需要同时符合操作系统对驱动的规范和文件系统的规范。
1). 首先介绍几个基础知识和重要结构体。
- 设备号。设备号分为主设备号和次设备号。使用32位存储,高12为位主设备号,低20位为次设备号。类型是dev_t
//定义一个设备号
struct dev_t char_dev_t;
- 设备结构体。
设备结构体标识了设备的基本信息
struct cdev {
struct kobject kobj;//暂时未涉及到,应该是内核管理的数据
struct module *owner;//字符设备驱动程序所在的内核模块对象的指针
const struct file_operations *ops;//文件系统操作接口,结构体中包含了对文件的各种操作函数
struct list_head list;//链接结构,维护逻辑存储结构,暂时未涉及到
dev_t dev;//设备号
unsigned int count;//对应主设备号下的设备个数,即同类型设备个数
};
- 文件系统操作接口
文件操作系统结构体中包含文件的基本操作,读写,打开关闭。实际编码中需要创建该结构体,并将实现的函数指针对应赋值,随后存入cdev中注册入系统。
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, bool spin);
int (*iterate) (struct file *, struct dir_context *);
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 *);
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 (*setfl)(struct file *, unsigned long);
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
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);
} __randomize_layout;
虽然选项很多,但是本次只实现了打开、关闭、读、 写四个函数。驱动装载后可以对设备进行读写。
//一般取值为 THIS_MODULE
struct module *owner;
//打开文件
int (*open) (struct inode *, struct file *);
//关闭文件
int (*release) (struct inode *, struct file *);
//读取文件
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
//写入数据到文件
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
- inode
这个inode就是linux文件系统中的inode。存储了文件的基本信息,例如读写权限、所属组、创建时间、最近修改时间等。在驱动开发中需要注意的属性只有两个。
dev_t i_rdev;//文件对应的设备号
struct cdev *i_cdev;//对应设备结构体
- file
该结构体中的数据也非常多,下面仅列出驱动开发中可能涉及的部分。
struct file {
//操作接口
const struct file_operations *f_op;
//私有数据,操作系统不做管理,由驱动开发者存储必要数据
//本次测试中在打开文件时将其指向对应文件的缓冲区
//在文件读写时方便操作
void *private_data;
};
2). 驱动初始化与文件读写
驱动的初始化分几个基本步骤
-
申请设备号
驱动在初始化时可以自定义设备号,也可以申请空闲的设备号。即静态申请和动态申请。其中静态申请肯能会由于已经存在相同的设备号而申请失败,动态申请处了所有设备号全部被占用,否则一般不会申请失败。因此本次采用动态申请。设备号申请同时申请主设备号和次设备号。
//静态申请设备号 //参数: // from:要申请的设备号 // count:要申请的次设备号的个数(申请出的次设备号连续,起始值是第一个参数中的次设备号) // name:驱动名(参考其他书写的是设备名称,但是这个名称和实际的设备名称好像并没有什么关系) // 在/proc/devices目录中可查 //返回值:返回0表示申请成功,否则返回错误码 int register_chrdev_region(dev_t from, unsigned count, const char *name); //动态申请设备号 //参数: // dev:[output]要存放设备号的结构体地址 // baseminor:起始设备号 // count:要申请的次设备号的个数(申请出的次设备号连续) // name:驱动名(参考其他书写的是设备名称,但是这个名称和实际的设备名称好像并没有什么关系) // 在/proc/devices目录中可查 //返回值:返回0表示申请成功,否则返回错误码 int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char* name); //释放设备号 //参数: // from:要释放的设备号 // count:子设备号个数 void unregister_chrdev_region(dev_t from, unsigned count);
-
申请cdev内存空间
申请cdev的内存空间,同样是两种方式。可以直接定义变量或者动态内存申请,不管是动态内存申请的指针还是静态变量一般都定义全局变量方便操作。定义静态变量按照C语言中定义变量的方式定义即可,动态申请有专门的函数进行cdev结构体的内存申请。本次测试采用定义静态全局变量的方式。下面时动态内存申请和释放的函数。
//调用函数直接返回一个可用cdev首地址 struct cdev *cdev_alloc(void); //释放由cdev_alloc申请的内存空间 void cdev_del(struct cdev *p);
-
绑定cdev与操作函数接口
使用以下函数将cdev与定义的操作接口关联
//关联cdev与操作接口 //参数: // cdev:设备信息结构体 // fops:操作接口 void cdev_init(struct cdev *cdev, const struct file_operations *fops);
-
将cdev添加到系统中
//将设备添加到系统中 //参数: // p:要添加到系统中的cdev // dev:设备号 // count:添加到系统中的设备个数 int cdev_add(struct cdev *p, dev_t dev,unsigned count); //将设备从系统中删除 //参数: // p:设备cdev void cdev_del(struct cdev *p);
-
创建设备
创建设备可以在驱动创建时同时使用代码创建,也可以等驱动装载入内核后使用mknod创建设备。
#使用mknod 创建设备
#设备类型使用c表示字符设备,使用b表示块设备
sudo mknod [设备路径] [设备类型(c/b)] [主设备号] [次设备号]
#例如:创建字符设备/dev/rw_dev0 设备号为[504:0]
sudo mknod /dev/rw_dev0 c 504 0
下面是使用代码创建设备需要的函数。
//注册设备class
//该函数为宏函数,下面列出的不是函数声明,仅是以函数声明的形式列出输入和输出的类型
//参数:
// owner:一般使用THIS_MOUDLE
// name:模块名字符串
//返回值:
// 设备class
struct class * class_create(struct module *owner,const char* name);
//注销设备class
//参数:
// cls:设备class
void class_destroy(struct class *cls);
//创建设备文件
//参数:
// class:设备class
// parent:设备的父节点,若没有则置空(NULL)
// devt:设备号
// drvdata:传递参数使用(暂时不会用)
// fmt:设备名,该参数和后续的参数可以像print一样使用,即可以使用占位符进行格式化输出
struct device *device_create(struct class *class, struct device *parent,dev_t devt, void *drvdata, const char *fmt, ...);
//删除设备
//参数:
// class:设备class
// devt:设备号
void device_destroy(struct class *class, dev_t devt)
【注】:cdev在《[野火]i.MX Linux 开发实战指南》书中基本等同于设备,上述也采用了相同的说法。最开始学的时候以为第四步将设备添加到系统中就能在系统中看到设备文件了。但是其实不然,创建设备是第5步,经过测试发现cdev与设备文件的关系更像萝卜坑和萝卜的关系。即前四步都是在挖萝卜坑,包括申请萝卜坑的编号,同类萝卜的操作等。前四步挖好萝卜坑之后 后面才能放萝卜,即设备文件。如果cdev只设置了三个,最终创建设备文件时最多也只能有三个,与申请的次设备号对应。创建文件的部分若使用mknod创建设备文件,即使使用未申请的设备号也不会产生错误,但是对创建出的设备进行操作时会发现不可读写。如果使用同一个设备号创建多个设备文件,则多个设备文件应该会指向同一个缓冲区。虽然这部分代码是在open函数中自己写的,但是也只能使用设备号区分不同的设备。
3). 驱动中数据的cpoy
驱动的数据属于内核数据,不同于用户数据,也不能使用标准的C语言的拷贝函数进行拷贝。下面是驱动中使用的内存内存拷贝函数。
//__user 标识是用户的缓冲区
//n 拷贝的数据长度
//从用户内存拷贝到内核内存
static inline long copy_from_user(void *to, const void __user * from, unsigned long n);
//从内核内存拷贝到用户内存
static inline long copy_to_user(void __user *to, const void *from, unsigned long n);
三、程序源码
/**
* 内存模拟虚拟文件系统,每个设备是一个文件可读写,创建3个设备
*/
//内核模块基本头文件
#include <linux/module.h>
//文件系统头文件
#include <linux/fs.h>
//cdev头文件
#include <linux/cdev.h>
//设备个数
#define DEV_COUNT 3
//起始子设备号
#define MINOR_START 0
//缓冲区大小
#define BUF_SIZE 256
static int rw_open(struct inode * in, struct file * fp);
static int rw_release(struct inode * in, struct file *fp);
static ssize_t rw_read(struct file *fp, char __user * user_buf, size_t count, loff_t *ppos);
static ssize_t rw_write (struct file *fp, const char __user *user_buf, size_t count, loff_t *ppos);
//定义文件系统操作函数
static struct file_operations opt={
.owner=THIS_MODULE,
.open=rw_open,
.release=rw_release,
.read=rw_read,
.write=rw_write
};
//全局设备号
static dev_t char_dev_t;
//定义三个设备
static struct cdev char_cdev[DEV_COUNT];
//缓冲区
static unsigned char char_buf[DEV_COUNT][BUF_SIZE];
//设备类
struct class * rw_class;
int rw_init(void)
{
int ret,i;
printk("rw init\n");
//动态申请主设备号,并申请该主设备号下的指定个数、指定起始值的子设备号
ret = alloc_chrdev_region(&char_dev_t,MINOR_START,DEV_COUNT,"dev_rw");
if(ret<0){
printk("fail to alloc devno\n");
goto alloc_error;
}
printk("rw dev_t[%d:%d]\n",MAJOR(char_dev_t),MINOR(char_dev_t));
//绑定操作函数
for(i=0;i<DEV_COUNT;i++)
cdev_init(&char_cdev[i],&opt);
//添加设备到系统
ret = cdev_add(char_cdev,char_dev_t,DEV_COUNT);
if(ret<0){
printk("fail to add cdev\n");
goto add_error;
}
/**************如果使用mknod创建设备该段可以不加 START*****************/
//创建设备class
rw_class = class_create(THIS_MODULE,"rw_class");
//创建设备
for(i=0;i<DEV_COUNT;i++)
device_create(rw_class,NULL,char_dev_t+i,NULL,"rw_dev%d",i);
/**************如果使用mknod创建设备该段可以不加 END*****************/
return 0;
add_error:
//添加失败,注销设备号
unregister_chrdev_region(char_dev_t, DEV_COUNT);
alloc_error:
return ret;
}
void rw_exit(void)
{
int i;
printk("rw exit\n");
/**************如果使用mknod创建设备该段可以不加 START*****************/
//删除设备
for(i=0;i<DEV_COUNT;i++)
device_destroy(rw_class,char_dev_t+i);
//删除设备class
class_destroy(rw_class);
/**************如果使用mknod创建设备该段可以不加 END*****************/
//删除设备cedv
for(i=0;i<DEV_COUNT;i++)
cdev_del(&char_cdev[i]);
//释放设备号
unregister_chrdev_region(char_dev_t,DEV_COUNT);
}
static int rw_open(struct inode * in, struct file * fp)
{
printk("open rw device:%d\n",in->i_rdev);
fp->private_data = char_buf[MINOR(in->i_rdev)];
return 0;
}
static int rw_release(struct inode * in, struct file *fp)
{
printk("close rw device:%d\n",in->i_rdev);
return 0;
}
static ssize_t rw_read(struct file *fp, char __user * user_buf, size_t count, loff_t *ppos)
{
unsigned long p = *ppos;
int ret;
int tmp = count;
if(p > BUF_SIZE)
return 0;
if(tmp > BUF_SIZE - p)
tmp = BUF_SIZE - p;
ret = copy_to_user(user_buf,fp->private_data+p,count);
*ppos += tmp;
return tmp;
}
static ssize_t rw_write (struct file *fp, const char __user *user_buf, size_t count, loff_t *ppos)
{
unsigned long p = *ppos;
int ret;
int tmp = count;
if(p > BUF_SIZE)
return 0;
if(tmp > BUF_SIZE - p)
tmp = BUF_SIZE - p;
ret = copy_from_user(fp->private_data+p,user_buf,count);
*ppos += tmp;
return tmp;
}
module_init(rw_init);
module_exit(rw_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("cxcc");
MODULE_DESCRIPTION("read and write test module");
【注】:开源协议部分相对上一节进行了修改,由GPL2改为了GPL。原因是在加入设备class创建和创建设备后编译时出现GPL不兼容的问题修改后问题解决,对开源协议不太了解,抽空学习。
四、程序测试
暂时没空写驱动测试的部分了,请谅解。上述代码已测试。
可以使用echo命令写入文件,使用cat命令读取文件。也可以使用C语言程序打开文件进行测试。