编写字符设备驱动程序
一、前言
Linux设备驱动模型是一种抽象,为内核建立起统一的设备模型。其目的是:提供一个对系统结构的一般性抽象描述。Linux设备模型跟踪所有系统所知道的设备,以便让设备驱动模型的核心程序协调驱动与新设备之间的关系。
在Linux当中,一切皆文件,设备也是如此,Linux操作系统把设备纳入文件系统的范畴来管理。
-
第一、每个设备都对应一个文件名,在内核中也就对应一个索引节点。
-
第二、对文件操作的系统调用大都适用于设备文件。
-
第三、从应用程序的角度看,设备文件逻辑上的空间是一个线性空间(起始地址为0,每读取一个字节加1)。从这个逻辑空间到具体设备物理空间(如磁盘的磁道、扇区)的映射则是由内核提供,并被划分为文件操作和设备驱动两个层次
设备分为三类:
1、字符设备:是指只能一个字节一个字节读写的设备,不能随机读取设备内存中的某一数据,读取数据需要按照先后数据。字符设备是面向流的设备,常见的字符设备有鼠标、键盘、串口、控制台和LED设备等。
2、块设备:是指可以从设备的任意位置读取一定长度数据的设备。块设备包括硬盘、磁盘、U盘和SD卡等。
每一个字符设备或块设备都在/dev目录下对应一个设备文件。Linux用户程序通过设备文件(或称设备节点)来使用驱动程序操作字符设备和块设备。
3.网络设备。网络设备使用套接字socket访问,虽然也使用read,write系统调用,但这些调用只作用于软件对象
二、编写一个简单的字符设备
2.1 准备工作
在编写字符设备之前,先了解一下几个重要的函数及概念
2.1.1 cdev数据结构
struct cdev {
struct kobject kobj;//用于设备驱动的模型
struct module *owner;//字符设备驱动所在内核模块对象指针
const struct file_operations *ops;//字符设备驱动的操作方法集
struct list_head list;//将字符设备串成一个链表
dev_t dev;//字符设备号,高12为主设备号,低20为次设备号
unsigned int count;//主设备号下的次设备号的个数
} __randomize_layout;
其中对于主设备号以及次设备号的获取,是通过MAJOR宏及MINOR宏来实现
#define MINORBITS 20 //表示次设备号的位数
#define MINORMASK ((1U << MINORBITS) - 1) //后20位为1
#define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))//设备号右移20位,即保留高12,主设备号
#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))//设备号与MINORMASK进行与操作,保留低20位,即次设备号
2.1.2 cdev_alloc()
cdev_alloc() 函数是用来产生一个cdev类型的数据结构,在这里,实现产生cdev类型的数据结构也可以通过定义一个全局静态变量来实现
static struct cdev mydemo_cdev;
struct mydemo_cdev = cdev_alloc();
struct cdev *cdev_alloc(void)
{
struct cdev *p = kzalloc(sizeof(struct cdev), GFP_KERNEL);//使用 kzalloc 分配 sizeof(struct cdev) 大小的内存
if (p) {
INIT_LIST_HEAD(&p->list);//通过 INIT_LIST_HEAD 初始化结构体中的 list 成员
kobject_init(&p->kobj, &ktype_cdev_dynamic);//初始化cdev类型的p的kobj对象,并关联ktype_cdev_dynamic类型
}
return p;
}
2.1.3 cdev_init()
cdev_init() 函数对cdev数据结构进行初始化,并建设备与驱动的操作方法集之间的连接关系
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
{
memset(cdev, 0, sizeof *cdev);//清0
INIT_LIST_HEAD(&cdev->list);//通过 INIT_LIST_HEAD 初始化 cdev 结构体中的 list 成员
kobject_init(&cdev->kobj, &ktype_cdev_default);//初始化cdev类型的kobj对象,并关联ktype_cdev_default类型
cdev->ops = fops;//cdev->ops 指向定义的fops,完成设备与驱动的操作方法集的关联
}
2.1.4 cdev_add()
cdev_add() 函数把一个字符设备添加到系统当中,实现注册字符设备驱动
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
{
int error;
// 将传入的设备号和设备数量分别保存到 cdev 结构体的 dev 和 count 成员中
p->dev = dev;
p->count = count;
// 使用 kobj_map 将字符设备映射到内核设备映射表中
error = kobj_map(cdev_map, dev, count, NULL,
exact_match, exact_lock, p);
if (error)
return error;
kobject_get(p->kobj.parent);//增加字符设备结构体kobj对象的父亲对象的引用计数
return 0;
}
2.1.5 cdev_del()
cdev_del() 函数把一个字符设备从系统当中删除,实现注销字符设备驱动
void cdev_del(struct cdev *p)
{
cdev_unmap(p->dev, p->count); // 使用 cdev_unmap 函数取消字符设备的设备映射
kobject_put(&p->kobj);//减少字符设备结构体kobj对象的引用计数
}
2.1.6 alloc_chrdev_region()
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,
const char *name)
{
struct char_device_struct *cd;
cd = __register_chrdev_region(0, baseminor, count, name);//通过调用__register_chrdev_region来获取相对应的字符设备范围
if (IS_ERR(cd))
return PTR_ERR(cd);
*dev = MKDEV(cd->major, cd->baseminor);//返回设备号
return 0;
}
char_device_struct结构体是一个哈希表的表项,通过主设备号来找到对应的字符设备结构体。
static struct char_device_struct {
struct char_device_struct *next;//指向哈希表的下一个元素
unsigned int major;//主设备号
unsigned int baseminor;//次设备号
int minorct;//次设备号的个数
char name[64];//字符设备的名称
struct cdev *cdev; //指向字符设备结构体的指针
} *chrdevs[CHRDEV_MAJOR_HASH_SIZE];
2.1.7 __register_chrdev_region()
static struct char_device_struct *
__register_chrdev_region(unsigned int major, unsigned int baseminor,
int minorct, const char *name)
{
struct char_device_struct *cd, *curr, *prev = NULL;
int ret;
int i;
//如果主设备号不合法
if (major >= CHRDEV_MAJOR_MAX) {
pr_err("CHRDEV \"%s\" major requested (%u) is greater than the maximum (%u)\n",
name, major, CHRDEV_MAJOR_MAX-1);
return ERR_PTR(-EINVAL);
}
//如果次设备号的个数不合法
if (minorct > MINORMASK + 1 - baseminor) {
pr_err("CHRDEV \"%s\" minor range requested (%u-%u) is out of range of maximum range (%u-%u) for a single major\n",
name, baseminor, baseminor + minorct - 1, 0, MINORMASK);
return ERR_PTR(-EINVAL);
}
//申请一段空间大小为sizeof(struct char_device_struct)的内存
cd = kzalloc(sizeof(struct char_device_struct), GFP_KERNEL);
if (cd == NULL)
return ERR_PTR(-ENOMEM);
//加锁
mutex_lock(&chrdevs_lock);
//如果主设备号为0
if (major == 0) {
ret = find_dynamic_major();//调用find_dynamic_major() 动态获取一个主设备号
if (ret < 0) {
pr_err("CHRDEV \"%s\" dynamic allocation region is full\n",
name);
goto out;//如果获取不到设备号,则说明该区域范围内设备号满了,解锁,释放cd空间,返回错误信息
}
major = ret;//主设备号为自动分配的设备号
}
ret = -EBUSY;
//i 为该主设备号对应在哈希表中的索引
i = major_to_index(major);
//遍历哈希表,来检查一下要注册的设备号是否和存在的设备号冲突
for (curr = chrdevs[i]; curr; prev = curr, curr = curr->next) {
if (curr->major < major)
continue;
if (curr->major > major)
break;
if (curr->baseminor + curr->minorct <= baseminor)
continue;
if (curr->baseminor >= baseminor + minorct)
break;
goto out;//冲突的话直接解锁,释放cd空间,返回错误信息
}
//不冲突的话,对cd进行初始化
cd->major = major;
cd->baseminor = baseminor;
cd->minorct = minorct;
strlcpy(cd->name, name, sizeof(cd->name));
if (!prev) {
//插入cd到表头
cd->next = curr;
chrdevs[i] = cd;
} else {
//插入cd
cd->next = prev->next;
prev->next = cd;
}
//解锁,并返回cd
mutex_unlock(&chrdevs_lock);
return cd;
out:
mutex_unlock(&chrdevs_lock);
kfree(cd);
return ERR_PTR(ret);
}
2.1.8 unregister_chrdev_region()
此函数是用来释放申请的设备号的
void unregister_chrdev_region(dev_t from, unsigned count)//参数分别为要释放的设备号的起始,以及释放设备号的个数
{
dev_t to = from + count;//释放设备号的终止
dev_t n, next;
//遍历要释放的设备号
for (n = from; n < to; n = next) {
next = MKDEV(MAJOR(n)+1, 0);//MKDEV是将主设备号及次设备号组合一起,因此next是指向下一个设备号
if (next > to)//如果下一个要释放的设备号大于要终止的设备号,则让next=to来终止循环
next = to;
kfree(__unregister_chrdev_region(MAJOR(n), MINOR(n), next - n));//释放__unregister_chrdev_region函数返回的设备号的内存
}
}
2.1.9 __unregister_chrdev_region()
static struct char_device_struct *
__unregister_chrdev_region(unsigned major, unsigned baseminor, int minorct)
{
struct char_device_struct *cd = NULL, **cp;
int i = major_to_index(major);//i 为该主设备号对应在哈希表中的索引
mutex_lock(&chrdevs_lock);//设置锁
// 遍历字符设备哈希表,查找匹配的字符设备结构体
for (cp = &chrdevs[i]; *cp; cp = &(*cp)->next)
if ((*cp)->major == major &&
(*cp)->baseminor == baseminor &&
(*cp)->minorct == minorct)
break;
// 找到匹配的设备结构体,将其从哈希表中移除
if (*cp) {
cd = *cp;
*cp = cd->next;
}
//释放锁
mutex_unlock(&chrdevs_lock);
// 返回找到的字符设备结构体指针
return cd;
}
2.2 代码实现
2.2.1 simple_device.c
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/init.h>
#include <linux/cdev.h>
#define DEMO_NAME "my_demo_dev"
static dev_t dev;
static struct cdev *demo_cdev;
static signed count = 1;
static int demodrv_open(struct inode *inode, struct file *file)
{
int major = MAJOR(inode->i_rdev);//主设备号
int minor = MINOR(inode->i_rdev);//次设备号
printk("%s: pid=%d, major=%d, minor=%d\n", __func__, current->pid, major, minor);//打印函数,设备号及进程号等相关信息
return 0;
}
static int demodrv_release(struct inode *inode, struct file *file)
{
return 0;
}
static ssize_t
demodrv_read(struct file *file, char __user *buf, size_t lbuf, loff_t *ppos)
{
printk("%s enter\n", __func__);
return 0;
}
static ssize_t
demodrv_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 demodrv_fops = {
.owner = THIS_MODULE,
.open = demodrv_open,
.release = demodrv_release,
.read = demodrv_read,
.write = demodrv_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;
}
//申请成功
demo_cdev = cdev_alloc();//分配一个cdev结构
if (!demo_cdev) {
printk("cdev_alloc failed\n");
goto unregister_chrdev;//如果分配失败,则注销字符设备
}
cdev_init(demo_cdev, &demodrv_fops);//初始化cdev结构体并建立设备和驱动操作方法集的链接
ret = cdev_add(demo_cdev, dev, count);//将该字符设备加入到系统中去
if (ret) {
printk("cdev_add failed\n");
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_del(demo_cdev);
unregister_chrdev:
unregister_chrdev_region(dev, count);
return ret;
}
//模块的注销函数
static void __exit simple_char_exit(void)
{
printk("removing device\n");
if (demo_cdev)//如果该字符设备还存在,则从系统中移除
cdev_del(demo_cdev);
unregister_chrdev_region(dev, count);//注销字符设备
}
module_init(simple_char_init);
module_exit(simple_char_exit);
MODULE_AUTHOR("kylin");
MODULE_LICENSE("GPL v2");
MODULE_DESCRIPTION("simpe character device");
2.2.2 编译运行
执行以下代码
make
sudo insmod simple_device.ko
sudo dmesg
可以看到插入模块后,系统为该模块申请了主设备号240,次设备号为0,也可以通过cat /proc/devices的信息,如下,可看到生成了名为my_demo_dev的设备,设备号为240
接下来,在/dev/目录下生成对应的节点,这块通过mknod来实现,在该目录下执行sudo mknod /dev/demo_drv c 240 0
cd 进入/dev目录下,使用ls -l命令可查看到刚生成的对应节点
2.2.3 编写用户态代码test.c
在用户态编写代码,来打开刚刚创建的节点demo_drv,并且调用了read函数,然后关闭此设备
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#define DEMO_DEV_NAME "/dev/demo_drv"
int main()
{
char buffer[64];
int fd;
fd = open(DEMO_DEV_NAME, O_RDONLY);
if (fd < 0) {
printf("open device %s failded\n", DEMO_DEV_NAME);
return -1;
}
read(fd, buffer, 64);
close(fd);
return 0;
}
三、使用misc机制来创建设备驱动
misc device 称为杂项设备,杂项设备的主设备号是10,杂项设备的数据结构如下所示:
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()函数来注册,使用misc_deregister()来注销设备,在注册杂项设备之后不需手动创建节点
3.1 准备工作
3.1.1 misc_register()
int misc_register(struct miscdevice *misc)
{
dev_t dev;
int err = 0;
// #define MISC_DYNAMIC_MINOR 255
bool is_dynamic = (misc->minor == MISC_DYNAMIC_MINOR);//判断是否是内核动态分配次设备号
INIT_LIST_HEAD(&misc->list);//调用INIT_LIST_HEAD 将misc->list初始化
mutex_lock(&misc_mtx);//设置锁
//如果是内核动态分配的话
if (is_dynamic) {
//i指向一个未被使用的次设备号的位图中的索引
int i = find_first_zero_bit(misc_minors, DYNAMIC_MINORS);
//如果i的值大于等于动态分配的范围
if (i >= DYNAMIC_MINORS) {
err = -EBUSY;
goto out;//则解锁并进行错误处理
}
misc->minor = DYNAMIC_MINORS - i - 1;//misc->minor = 动态分配的范围 - 分配的设备的索引 -1
set_bit(i, misc_minors);//给misc_minors的第i位设置为1
} else {
//如果不是内核动态分配的话
struct miscdevice *c;
//遍历misc_list链表的每一项,看分配的次设备号是否被使用
list_for_each_entry(c, &misc_list, list) {
if (c->minor == misc->minor) {
err = -EBUSY;
goto out;//如果被使用的话,则解锁并返回报错
}
}
}
dev = MKDEV(MISC_MAJOR, misc->minor);//调用MKDEV来把主设备号以及次设备号链接起来,其中 MISC_MAJOR = 10
misc->this_device =
device_create_with_groups(misc_class, misc->parent, dev,
misc, misc->groups, "%s", misc->name);//创建设备文件并并将设备添加到设备类中
if (IS_ERR(misc->this_device)) {
//如果是内核动态分配次设备号
if (is_dynamic) {
//i 是动态分配的范围- 杂项设备的次设备号 - 1,即在位图中的索引
int i = DYNAMIC_MINORS - misc->minor - 1;
//i如果不合法的话
if (i < DYNAMIC_MINORS && i >= 0)
clear_bit(i, misc_minors);//清除位图
misc->minor = MISC_DYNAMIC_MINOR;//杂项设备号 设置为默认的 255
}
err = PTR_ERR(misc->this_device);
goto out;//解锁,并返回报错信息
}
/*
* Add it to the front, so that later devices can "override"
* earlier defaults
*/
list_add(&misc->list, &misc_list);//把misc->list项加入到misc_list链表头中
out:
mutex_unlock(&misc_mtx);//解锁
return err;
}
3.1.2 misc_deregister()
void misc_deregister(struct miscdevice *misc)
{
int i = DYNAMIC_MINORS - misc->minor - 1;//i 是动态分配的范围- 杂项设备的次设备号 - 1,即在位图中的索引
if (WARN_ON(list_empty(&misc->list)))
return;
mutex_lock(&misc_mtx);//设置锁
list_del(&misc->list);//从misc_list中删除misc->list
device_destroy(misc_class, MKDEV(MISC_MAJOR, misc->minor));//销毁设备号对应的信息
if (i < DYNAMIC_MINORS && i >= 0)
clear_bit(i, misc_minors);//清理位图
mutex_unlock(&misc_mtx);//解锁
}
3.2 代码实现
3.2.1 mydemodrv_misc.c
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/init.h>
#include <linux/miscdevice.h>
#define DEMO_NAME "my_demo_dev"
static struct device *mydemodrv_device;
static int demodrv_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 demodrv_release(struct inode *inode, struct file *file)
{
return 0;
}
static ssize_t
demodrv_read(struct file *file, char __user *buf, size_t lbuf, loff_t *ppos)
{
printk("%s enter\n", __func__);
return 0;
}
static ssize_t
demodrv_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 demodrv_fops = {
.owner = THIS_MODULE,
.open = demodrv_open,
.release = demodrv_release,
.read = demodrv_read,
.write = demodrv_write
};
// 定义一个杂项设备结构体实例
static struct miscdevice mydemodrv_misc_device = {
.minor = MISC_DYNAMIC_MINOR,
.name = DEMO_NAME,
.fops = &demodrv_fops,
};
//模块的注册函数
static int __init simple_char_init(void)
{
int ret;
ret = misc_register(&mydemodrv_misc_device);//注册杂项设备
if (ret) {
printk("failed register misc device\n");
return ret;
}
//注册成功之后
mydemodrv_device = mydemodrv_misc_device.this_device;
printk("succeeded register char device: %s\n", DEMO_NAME);
return 0;
}
//模块的销毁函数
static void __exit simple_char_exit(void)
{
printk("removing device\n");
misc_deregister(&mydemodrv_misc_device);//注销杂项设备
}
module_init(simple_char_init);
module_exit(simple_char_exit);
MODULE_AUTHOR("Benshushu");
MODULE_LICENSE("GPL v2");
MODULE_DESCRIPTION("simpe character device");
3.2.2 编译运行
编译并插入模块之后,查看/dev目录下的设备节点,结果如下图所示,发现节点已创建好了。主设备号为10,次设备号为56
make
sudo insmod mydemodrv_misc.ko
ls -l /dev
3.2.3 编写用户态代码test.c
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#define DEMO_DEV_NAME "/dev/my_demo_dev"
int main()
{
char buffer[64];
int fd;
fd = open(DEMO_DEV_NAME, O_RDONLY);
if (fd < 0) {
printf("open device %s failded\n", DEMO_DEV_NAME);``
return -1;
}
read(fd, buffer, 64);
close(fd);
return 0;
}
编译并运行用户态的测试文件,在进行打印内核消息,结果如图所示,可以看到杂项设备的信息
四、简单的虚拟设备
在实际的项目中,一些字符设备内部会有缓冲区(buffer),也叫FIFO缓冲区,在本实验中,我们通过write()方法把用户数据写入虚拟设备的FIFO缓冲区中,还可以通过read()方法把FIFO缓冲区的数据读出来
4.1 准备工作
4.1.1 copy_to_user()
static __always_inline unsigned long __must_check
copy_to_user(void __user *to, const void *from, unsigned long n)
//to指向用户空间的目标地址,from指向内核的源地址,n表示复制的字节数
{
if (likely(check_copy_size(from, n, true)))//如果check_copy_size结果为真,则使用_copy_to_user函数来实现复制
n = _copy_to_user(to, from, n);
return n;
}
4.1.2 _copy_to_user
static inline __must_check unsigned long
_copy_to_user(void __user *to, const void *from, unsigned long n)
//to指向用户空间的目标地址,from指向内核的源地址,n表示复制的字节数
{
might_fault();
//如果检查给定的地址合法
if (access_ok(to, n)) {
kasan_check_read(from, n);// Kernel Address SANitizer(KASAN)中的一个函数,用于检查对内核空间的读操作是否合法。
n = raw_copy_to_user(to, from, n);//raw_copy_to_user用于将数据从内核空间复制到用户空间
}
return n;//函数返回未能成功复制的字节数
}
4.1.3 copy_from_user()
static __always_inline unsigned long __must_check
copy_from_user(void *to, const void __user *from, unsigned long n)
//to 内核空间的目标地址,from 用户空间的源地址 n 要复制的字节数
{
if (likely(check_copy_size(to, n, false)))//如果check_copy_size结果为真,则使用_copy_from_user函数来实现复制
n = _copy_from_user(to, from, n);
return n;
}
4.1.4 _copy_from_user()
static inline __must_check unsigned long
_copy_from_user(void *to, const void __user *from, unsigned long n)
//to 内核空间的目标地址,from 用户空间的源地址 n 要复制的字节数
{
unsigned long res = n;
might_fault();
//如果检查给定的地址合法
if (likely(access_ok(from, n))) {
kasan_check_write(to, n);// Kernel Address SANitizer(KASAN)中的一个函数,用于检查对内核空间的写操作是否合法
res = raw_copy_from_user(to, from, n);//raw_copy_from_user用于将数据从用户空间复制到内核空间,res 的值将被更新为未能成功复制的字节数
}
if (unlikely(res))//如果复制不成功
memset(to + (n - res), 0, res);//使用memset将将目标地址 to 中的剩余字节清零
return res;//函数返回未能成功复制的字节数
}
4.2 代码实现
4.2.1 mydemo_misc_fifo.c
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/init.h>
#include <linux/miscdevice.h>
#include <linux/slab.h>
#define DEMO_NAME "my_demo_dev"
static struct device *mydemodrv_device;
//虚拟FIFO设备的缓冲区
static char *device_buffer;
#define MAX_DEVICE_BUFFER_SIZE 64
static int demodrv_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 demodrv_release(struct inode *inode, struct file *file)
{
return 0;
}
static ssize_t
demodrv_read(struct file *file, char __user *buf, size_t lbuf, loff_t *ppos)
//file表示打开的设备文件,buf表示用户空间的内存起始地址,lbuf表示用户需要读取的字节数,ppos指向文件的位置
{
int actual_readed;//真实读取的字节数
int max_free;//最大空闲的字节数
int need_read;//需要读的字节数
int ret;
printk("%s enter\n", __func__);//输出函数名
max_free = MAX_DEVICE_BUFFER_SIZE - *ppos;//max_free是缓冲区现在空闲的大小
need_read = max_free > lbuf ? lbuf : max_free; //如果空闲的大小比要读取的字节数大,则need_read=要读取的字节数,否则,need_read=空闲大小,这一步是防止数据溢出
if (need_read == 0)//如果need_read==0,即没有空闲的空间
dev_warn(mydemodrv_device, "no space for read");
ret = copy_to_user(buf, device_buffer + *ppos, need_read);//将设备的FIFO缓冲区的内容读取need_read个字节复制到用户空间的缓冲区buf中,其返回值为未能复制的字节数
if (ret == need_read)//没有复制任何数据
return -EFAULT;
actual_readed = need_read - ret;//真实读取的字节数为need_read - 未能复制的个数
*ppos += actual_readed;//更新ppos指针
printk("%s, actual_readed=%d, pos=%lld\n",__func__, actual_readed, *ppos);
return actual_readed;//返回真实读取的个数等
}
static ssize_t
demodrv_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos)
//file表示打开的设备文件,buf表示用户空间的内存起始地址,count表示用户需要读取的字节数,ppos指向文件的位置
{
int actual_write;//真实写入的数据
int free;//空闲的个数
int need_write;//需要写的个数
int ret;
printk("%s enter\n", __func__);
free = MAX_DEVICE_BUFFER_SIZE - *ppos; //free是缓冲区现在空闲的大小
need_write = free > count ? count : free;//如果需要写的字节大于空闲字节,则need_write = 空闲字节,否则need_write= 需要写的字节数
if (need_write == 0)//need_write =0,则没有空闲大小写
dev_warn(mydemodrv_device, "no space for write");
// copy_from_user用于将用户空间中的数据复制到内核空间,该函数返回未能复制的字节数
ret = copy_from_user(device_buffer + *ppos, buf, need_write);//将用户空间缓冲区的need_write字节的数据,复制到设备的FIFO缓冲区
if (ret == need_write)//如果没有复制成功
return -EFAULT;
actual_write = need_write - ret;//真实写入的数据 = need_write - 未能复制的字节数
*ppos += actual_write;//ppos指针的值 +=actual_write
printk("%s: actual_write =%d, ppos=%lld\n", __func__, actual_write, *ppos);
return actual_write;//返回真实写入的字节数
}
//定义了驱动的操作集
static const struct file_operations demodrv_fops = {
.owner = THIS_MODULE,
.open = demodrv_open,
.release = demodrv_release,
.read = demodrv_read,
.write = demodrv_write
};
static struct miscdevice mydemodrv_misc_device = {
.minor = MISC_DYNAMIC_MINOR,
.name = DEMO_NAME,
.fops = &demodrv_fops,
};
//模块的注册函数
static int __init simple_char_init(void)
{
int ret;
device_buffer = kmalloc(MAX_DEVICE_BUFFER_SIZE, GFP_KERNEL);//为缓冲区申请空间
if (!device_buffer)
return -ENOMEM;
ret = misc_register(&mydemodrv_misc_device);//注册杂项设备
if (ret) {
printk("failed register misc device\n");
kfree(device_buffer);//释放申请的缓冲区空间
return ret;
}
//注册成功之后
mydemodrv_device = mydemodrv_misc_device.this_device;
printk("succeeded register char device: %s\n", DEMO_NAME);
return 0;
}
//模块的销毁函数
static void __exit simple_char_exit(void)
{
printk("removing device\n");
kfree(device_buffer);//释放申请的缓冲区空间
misc_deregister(&mydemodrv_misc_device);//注销杂项设备
}
module_init(simple_char_init);
module_exit(simple_char_exit);
MODULE_AUTHOR("Benshushu");
MODULE_LICENSE("GPL v2");
MODULE_DESCRIPTION("simpe character device");
4.2.2 编译运行
执行以下命令
make
sudo insmod mydemo_misc_fifo.ko
sudo dmesg
运行结果为
4.2.3 编写用户态代码test.c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
//设备文件的路径
#define DEMO_DEV_NAME "/dev/my_demo_dev"
int main()
{
char buffer[64];//用于向设备写入数据的缓冲区
int fd;//文件描述符,标识要打开的文件
int ret;
size_t len;
char message[] = "Testing the virtual FIFO device";
char *read_buffer;//从设备读取的数据的缓冲区
len = sizeof(message);
fd = open(DEMO_DEV_NAME, O_RDWR);
if (fd < 0) {
printf("open device %s failded\n", DEMO_DEV_NAME);
return -1;
}
/*1. 向设备写入数据*/
ret = write(fd, message, len);
if (ret != len) {
printf("canot write on device %d, ret=%d", fd, ret);
return -1;
}
read_buffer = malloc(2*len);//申请并舒适化读取数据的缓冲区
memset(read_buffer, 0, 2*len);
/*关闭文件并重新打开文件*/
close(fd);
fd = open(DEMO_DEV_NAME, O_RDWR);
if (fd < 0) {
printf("open device %s failded\n", DEMO_DEV_NAME);
return -1;
}
ret = read(fd, read_buffer, 2*len);//从缓冲区读取数据
printf("read %d bytes\n", ret);//打印读取的字节数及内容
printf("read buffer=%s\n", read_buffer);
close(fd);
return 0;
}
运行结果如下图,读取的字节是32*2=64字节,从用户空间读入数据放到设备的缓冲区中
五、KFIFO环形缓冲区
在之前创建一个文件系统章节中接触到了KFIFO环形缓冲区,环形缓冲区通常有一个读指针和一个写指针,读指针指向环形缓冲区中可读的数据,写指针指向环形缓冲区可写的数据,通过移动读指针和写指针来实现缓冲区的数据读取和写入。
Linux内核实现的KFIFO环形缓冲区,在读线程和写线程并发条件下,不需要使用额外的锁来保证环形缓冲区的数据安全。
5.1 准备工作
5.1.1 初始化环形缓冲区
通过 DEFINE_KFIFO(fifo,type,size)
宏来初始化 KFIFO 环形缓冲区,其中fifo表示环形缓冲区的名称,type表示环形缓冲区的类型,size表示环形缓冲区的大小,size必须是2的整数次幂。
5.1.2 kfifo_from_user(fifo,from,len,copied)
kfifo_from_user()
宏用来将用户空间的数据写入KFIFO环形缓冲区中,其中fifo表示环形缓冲区的名称,from表示用户空间缓冲区的起始地址,len表示要复制多少元素,copied保存了成功复制元素的数量,用作返回值
5.1.3 kfifo_to_user(fifo ,to,len,copied)
kfifo_to_user()
宏用来读出KFIFO环形缓冲区中的数据并复制到用户空间中,其中fifo表示环形缓冲区的名称,to表示要将数据复制到用户空间目标地址,len表示要复制多少元素,copied保存了成功复制元素的数量,用作返回值
5.2 代码实现
5.2.1 kfifo.c
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/init.h>
#include <linux/miscdevice.h>
#include <linux/slab.h>
#include <linux/kfifo.h>
#define DEMO_NAME "my_demo_dev"
//定义了环形缓冲区
DEFINE_KFIFO(mydemo_fifo,char,64);
static struct device *mydemodrv_device;
//虚拟FIFO设备的缓冲区
static char *device_buffer;
#define MAX_DEVICE_BUFFER_SIZE 64
static int demodrv_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 demodrv_release(struct inode *inode, struct file *file)
{
return 0;
}
static ssize_t
demodrv_read(struct file *file, char __user *buf, size_t lbuf, loff_t *ppos)
//file表示打开的设备文件,buf表示用户空间的内存起始地址,lbuf表示用户需要读取的字节数,ppos指向文件的位置
{
int actual_readed;//真实读取的字节数
int ret;
printk("%s enter\n", __func__);//输出函数名
//读取数据这部分通过kfifo_to_user 函数将从KFIFO 中读取出的数据复制到用户空问中去,此时 actual_write 中保存的是实际读出的数据长度
ret = kfifo_to_user(&mydemo_fifo,buf,lbuf,&actual_readed);
if (ret)//没有复制任何数据
return -EIO;
printk("%s, actual_readed=%d, pos=%lld\n",__func__, actual_readed, *ppos);
return actual_readed;//返回真实读取的个数等
}
static ssize_t
demodrv_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos)
//file表示打开的设备文件,buf表示用户空间的内存起始地址,count表示用户需要读取的字节数,ppos指向文件的位置
{
unsigned int actual_write;//真实写入的数据
int ret;
printk("%s enter\n", __func__);
//kfifo _from user 函数将用户数据写入到 KFIFO 中,此时 actual_write 中保存的是实际写入成功的字节数
ret = kfifo_from_user(&mydemo_fifo,buf,count,&actual_write);
if (ret)//没有复制任何数据
return -EIO;
printk("%s: actual_write =%d, ppos=%lld\n", __func__, actual_write, *ppos);
return actual_write;//返回真实写入的字节数
}
//定义了驱动的操作集
static const struct file_operations demodrv_fops = {
.owner = THIS_MODULE,
.open = demodrv_open,
.release = demodrv_release,
.read = demodrv_read,
.write = demodrv_write
};
static struct miscdevice mydemodrv_misc_device = {
.minor = MISC_DYNAMIC_MINOR,
.name = DEMO_NAME,
.fops = &demodrv_fops,
};
//模块的注册函数
static int __init simple_char_init(void)
{
int ret;
ret = misc_register(&mydemodrv_misc_device);//注册杂项设备
if (ret) {
printk("failed register misc device\n");
return ret;
}
//注册成功之后
mydemodrv_device = mydemodrv_misc_device.this_device;
printk("succeeded register char device: %s\n", DEMO_NAME);
return 0;
}
//模块的销毁函数
static void __exit simple_char_exit(void)
{
printk("removing device\n");
misc_deregister(&mydemodrv_misc_device);//注销杂项设备
}
module_init(simple_char_init);
module_exit(simple_char_exit);
MODULE_AUTHOR("Benshushu");
MODULE_LICENSE("GPL v2");
MODULE_DESCRIPTION("simpe character device");
5.2.2 编译运行
执行以下命令
make
sudo insmod mydemo_misc_kfifo.ko
sudo dmesg
5.2.3 用户态代码test.c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#define DEMO_DEV_NAME "/dev/my_demo_dev"
int main()
{
char buffer[64];
int fd;
int ret;
size_t len;
char message[] = "Testing the virtual FIFO device";
char *read_buffer;
len = sizeof(message);
fd = open(DEMO_DEV_NAME, O_RDWR);
if (fd < 0) {
printf("open device %s failded\n", DEMO_DEV_NAME);
return -1;
}
/*1. write the message to device*/
ret = write(fd, message, len);
if (ret != len) {
printf("canot write on device %d, ret=%d", fd, ret);
return -1;
}
read_buffer = malloc(2*len);
memset(read_buffer, 0, 2*len);
ret = read(fd, read_buffer, 2*len);
printf("read %d bytes\n", ret);
printf("read buffer=%s\n", read_buffer);
close(fd);
return 0;
}
运行结果
六、提问并回答
6.1 字符设备驱动程序的运行逻辑是什么?
下图为添加字符设备的大概流程,将字符设备添加到系统中之后,使用mknod来注册一个设备节点。在用户态,通过打开设备节点来操纵设备驱动。这里的打开设备节点,把该设备节点当成一个文件来看待的。其实主要是通过编写file_operations的这些钩子函数,实现不同的功能。用户态通过open,read,write等函数调用的其实就是设备驱动的file_operations当中的钩子函数。
6.2 file_operations()这个结构体起什么作用?
在Linux中,一切皆是“文件”,字符设备也是这样,file_operations结构体中的成员函数是字符设备程序设计的主要内容,Linux使用file_operations结构访问驱动程序的函数,这个结构的每一个成员的名字都对应着一个调用。用户进程利用在对设备文件进行如read/write操作的时候,系统调用通过设备文件的主设备号找到相应的设备驱动程序,然后读取这个数据结构相应的函数指针,接着把控制权交给该函数,这是Linux的设备驱动程序工作的基本原理。
6.3 设备号在驱动程序中起什么作用?为什么要有主设备号和次设备号?
设备号是系统中珍贵的资源,在驱动程序中起到标识和区分不同设备的作用,主设备号和次设备号的组合形成了一个唯一的设备号,通过这个设备号,内核可以确定要访问的是哪个具体的设备。因此内核要确保避免两个设备驱动的主设备号一样。
主设备号代表一类设备,而次设备代表同一类设备中的不同个体,每个次设备都有一个不同的设备节点。设备节点的内容在/dev目录下可查看。设备号一般是32位,其中高12为主设备号,低20位为次设备号。 当用户程序打开某设备文件时,内核根据设备文件中包含的设备号找到相应的设备驱动程序,并执行相应的文件操作。
6.4 驱动程序的注册和注销函数是做什么?为什么要进注册和注销?
注册函数是在驱动程序加载时执行的函数。这个函数负责完成模块的初始化工作,包括设备的注册、设备号的申请、初始化设备、 将设备加入到系统中等工作,完成这些步骤后,内核就能正确地识别和使用这个设备
注销函数是在驱动程序卸载时执行的函数。这个函数负责完成模块的清理工作,包括设备的注销,释放设备结构等工作,完成这些步骤后,模块就可以被安全地从内核中卸载。
注册和注销就相当于进程的新建和终止状态,是一个生命周期的两个阶段。分别负责把设备驱动加载到系统中和从系统移除设备的工作。
6.5 Makefile文件编写和之前编写模块的Makefile有什么区别?
在这部分Makefile文件的编写,使我了解了Makefile文件的编写规则:
BASEINCLUDE ?= /lib/modules/`uname -r`/build
mydemo_misc_fifo-objs := fifo.o
obj-m := mydemo_misc_fifo.o
all :
$(MAKE) -C $(BASEINCLUDE) M=$(PWD) modules;
clean:
$(MAKE) -C $(BASEINCLUDE) M=$(PWD) clean;
rm -f *.ko;
在这里添加了一行mydemo_misc_fifo-objs := fifo.o
,经过查找资料,这行代码是将要编译成内核模块的目标文件列表。在这里,fifo.c
是源代码文件, fifo.o
是编译后生成的目标文件。这个列表是为了告诉内核模块在构建的时候将这些目标文件链接到最终的内核模块中。即obj-m
定义了将要生成的内核模块的名称,最终用于构建内核模块时生成的 .ko
文件。
6.6 设备驱动如何与inode,file等联系起来?
Linux 系统中有一条原则——“万物皆文件”。设各节点也算特殊的文件,称为设备文件,是连接内核空问驱动和用户空间应用程序的桥梁。如果应用程序想使用驱动提供的服务或操作设备,那么需要通过访问设备文件来完成。设备文件使得用户操作设备就像操作普通文件一样方便。
设备驱动和inode,file结构之间的关系如图所示: