简单字符设备文件的实现(包含文件的读写和逻辑梳理)


前言

这里以一个简单的字符设备文件的源码实现为例子,做出仔细分析。

本文采取总分总的方式,先给大家看看整体逻辑图,然后介绍源码,最后对照图梳理一下整体的逻辑。

在这里插入图片描述


1. 头文件

以下列出的都是Linux 内核中的标准头文件,包含了各种在内核开发中常用的功能和函数。其中包括对模块初始化文件操作内存管理以及字符设备等关键功能的支持。

每行注释函数,分别归属于前面对应的内核库。先不介绍每个函数有什么用,后续使用再详细分析和介绍。

#include <linux/module.h>		// module_init  module_exit
#include <linux/init.h>			// __init  __exit
#include <linux/fs.h>			// file_operations
#include <linux/uaccess.h>		// copy_to-user		copy_from_user
#include <linux/cdev.h>			// cdev_init	cdev_add	cdev_del
#include <linux/slab.h> 		// kmalloc	kfree

2. 全局变量定义

#define MYNMAJOR 240			// 主设备号
#define MYNAME "my_chrdev"  	// 设备名称
char kbuf[100];	// 定义一个缓存数组,用于内核接收数据
static struct cdev *char_dev;  // 定义一个字符设备结构体
static int major;	// 用于保存主设备号
  • kbuf:这个数组在内核中被分配,并且在内核模块的整个生命周期中都可以访问。
  • struct cdev类型:一种表示字符设备的结构体,可以通过它来管理字符设备的操作。

3. 设备操作的实体函数

这些函数具有标准的划分,分为openreleasereadwrite方法。
用户根据具体需求实现,实际上实现功能就是字符驱动程序对应的.open.release.read.write方法的功能。在结构体中建立设备文件的四大方法与设备驱动的四大方法的连接。
(坚持看下去吧,我也坚持写下来了呢)。

3.1 open方法

该函数是字符设备驱动中的"打开"函数,用于处理用户空间程序打开设备文件时的操作。方法名通常采用driver_name_open的格式。

  • struct inode *inode:代表设备文件的索引节点;
  • struct file *file:代表设备文件的文件描述符。

通常在该函数内实现:设备初始化分配资源设置状态等准备工作。

static int my_chrdev_open(struct inode *inode, struct file *file)
{
	// code part for opening the device
	printk(KERN_INFO "test_module_open\n");
	return 0;
}

返回值为0,表示设备成功打开;返回非零,则表示设备打开错误,该错误码会传递给用户空间程序。


3.2 release方法

open函数对应,release用于处理用户空间程序关闭设备文件时的操作,通常在设备文件关闭的时候执行。

在该函数内通常需要实现:清理资源关闭设备保存状态等功能。

static int my_chrdev_release(struct inode *inode, struct file *file)
{
	// code part for release the device
	printk(KERN_INFO "test_chrdev_release\n");
	return 0;
}

返回值为0,表示设备成功释放;返回非零,则表示设备释放错误,该错误码会传递给用户空间程序。


3.3 write方法

这个函数的作用是将用户空间的数据写入设备的数据缓冲区,并可能触发与硬件相关的操作。

  • struct file *file:这是表示打开的设备文件的文件描述符的结构体指针。
  • const char __user *buf:这是用户提供的缓冲区,其中的数据将被写入设备。
  • size_t size:这是用户要写入的字节数。
  • loff_t *opps:这是一个表示当前写入位置的指针,用于跟踪在文件中的写入位置。
static ssize_t my_chrdev_write(struct file *file, const char __user *buf, size_t size, loff_t *opps)
{
	int ret = -1;
	printk(KERN_INFO "my_chrdev_write\n");	
	
	// memset函数将缓冲区`kbuf`的内容设置为0,以便在写入新数据之前将之前的数据清楚
	memset(kbuf, 0, sizeof(kbuf));
	
	// 将用户提供的数据从用户空间的`buf`缓冲区复制到刚才定义的内核空间中的`kbuf`缓冲区中
	ret = copy_from_user(kbuf, buf, size);
	if (ret){
		printk(KERN_ERR "copy_from_user fail...\n");
		return -EINVAL;
	}
	
	printk(KERN_INFO "copy_from_user success...\n");
	// real sense: we will write some code for operation the hardware, based on the above data
	// coding...
	return size;
}

在实际应用中,你可能需要根据写入的数据来执行一些特定的操作,比如基于数据来控制硬件设备的行为。


3.4 read 方法

这个函数的作用是从设备的数据缓冲区 kbuf 中读取数据到用户提供的缓冲区中,并更新当前读取位置,以便下一次读取。

  • struct file *file:表示打开的设备文件的文件描述符的结构体指针;
  • char __user *buf:这是用户空间提供的缓冲区,数据将被复制到这个缓冲区;
  • size_t size:这是用户请求读取的字节数;
  • loff_t *ppos:这表示一个当前读取所在位置的指针,用于追踪在文件中的读取位置。
static ssize_t my_chrdev_read(struct file *file, char __user *buf, size_t size, loff_t *ppos)
{
	int ret = -1;
	printk(KERN_INFO "my_chrdev_read\n");	
	
	// 计算当前读取位置到数据末尾的剩余字节数
    int remaining_bytes = strlen(kbuf) - *ppos;

	// 如果剩余字节数小于等于0,表示没有剩余数据可读,函数返回0.
    if (remaining_bytes <= 0)
    	printk(KERN_ERR "no remaining_bytes need to be read\n");
            return 0;
	// 预防用户读取数据字节数,超出缓冲区剩余可读数据字节数
	// 因为用户可以每次设置读取多少个字节数
    if (size > remaining_bytes)
            size = remaining_bytes;
	// 将数据从内核空间的kbyf复制到用户提供的buf缓冲区
    if(copy_to_user(buf, kbuf + *ppos, size) != 0)
    	printk(KERN_ERR "copy_to_user fail\n");
            return -EFAULT;
	
	// 更新读取指针,使其指向下一个要读取的位置(感觉指针存储于内核当中,因为是指针,所以会同步更新)
    *ppos += size;

    printk(KERN_ERR "copy_to_user success...\n");
    return size;
}

copy_to_user(buf, kbuf + *ppos, size)

  • kbuf + *ppos: 表示将读取指针偏移到,剩余剩余未读取字节的位置首部
  • size: 表示从 kbuf + *ppos位置开始,需要复制size大小的字节给buf

4 设备文件结构体

该代码定义了一个file_operation内置类型的结构体,my_modules_fops,用于将字符设备的操作函数与字符设备驱动程序关联起来。

  • .owner = THIS_MODULE:该字段用于指定模块拥有者,通常使用THIS_MODULE,表示当前模块是这个文件操作结构的;
  • .open = my_chrdev_open:将字符设备的打开操作open与源码中自定义的my_chrdev_open方法建立关联,当用户空间使用open系统调用时,内核会调用my_chrdev_open函数;
  • .release = my_chrdev_release:字符设备释放函数,将字符设备的释放操作release与源码中自定义的my_chrdev_release方法建立关联,当用户空间使用release系统调用时,内核会调用my_chrdev_release函数;
  • .read = my_chrdev_read:这一行将字符设备的读取操作与一个自定义的函数 my_chrdev_read 关联起来。当用户空间程序使用 read 系统调用从这个字符设备中读取数据时,内核会调用 my_chrdev_read 函数来执行读取操作。
  • .write = my_chrdev_write: 这一行将字符设备的写入操作与一个自定义的函数 my_chrdev_write 关联起来。当用户空间程序使用 write 系统调用向这个字符设备中写入数据时,内核会调用 my_chrdev_write 函数来执行写入操作。
static const struct file_operations my_modules_fops = {
	.owner		=	THIS_MODULE,		// means it is a driver
	.open		=	my_chrdev_open,		// build the link between real function(my_chrdev_open) with device operation api(.open)
	.release	=	my_chrdev_release,	// the same 
	.read		=	my_chrdev_read,
	.write		= 	my_chrdev_write,	
};

5. 设备的初始化与销毁

5.1 chrdev_init

字符设备创建及准备工作,其中包括:

  • 设备号分配
  • 字符设备内存分配
  • 字符设备的初始化
  • 将字符设备添加到内核的字符设备列表
static int __init chrdev_init(void)
{
	int ret;
	int devNo;  // 保存设备号(主设备号)
	// 1. 像系统申请设备号
	// devNo设备号地址,次设备号从0开始,1是要分配的设备号数量,CharDriver为设备名称
	ret = alloc_chrdev_region(&devNo, 0, 1, "CharDriver");
	if (ret < 0){
		printk(KERN_ERR "Failed to allocate device number\n");
		return ret;
	}

	// 2. 如果你想修改设备号,可以用如下两行代码
	// 否则可以不需要,因为devNo已经是一个MKDEV设备号
	major = MAJOR(devNo);
	devNo = MKDEV(major, 0);  //
	
	// 3. 分配字符设备文件内存
	// kzalloc 用于分配内核内存,并将分配的内存区域初始化为零
	char_dev = kzalloc(sizeof(struct cdev), GFP_KERNEL);
	if (!char_dev){
		// 设备内存分配错误,则释放设备号资源
		unregister_chrdev_region(devNo, 1);
		return 0;
	}

	// 4. 初始化字符设备结构体
	// 在char_dev内存中初始化字符设备结构体,并将设备文件操作my_modules_fops与之关联
	// 目的是为了告诉内核,如何处理字符设备的操作
	cdev_init(char_dev, &my_modules_fops);

	// 5. 将此设备添加到内核的字符设备列表中
	// 并告诉内核具体的主次设备号为devNo,设备数量为1
	ret = cdev_add(char_dev, devNo, 1);
	if (ret < 0){
		unregister_chrdev_region(devNo, 1);
		printk(KERN_ERR "Failed to add character device\n");
		return ret;
	}

	printk(KERN_INFO "Character driver loaded\n");
	return 0;
}

5.2 chrdev_exit

  • cdev_del(char_dev);: 这一行代码从内核中删除之前添加的字符设备。cdev_del 函数用于删除字符设备的注册,确保不再接受对该设备的操作请求。

  • kfree(char_dev);: 这一行代码释放之前分配的字符设备结构体 char_dev 所占用的内存。这是为了防止内存泄漏,确保在卸载模块时释放了所有分配的内存。

  • unregister_chrdev_region(MKDEV(major, 0), 1);: 这一行代码注销之前分配的设备号。它使用 unregister_chrdev_region 函数来释放之前分配的设备号资源,参数 MKDEV(major, 0) 用于构建正确的设备号,1 表示释放一个设备号。

static void __exit chrdev_exit(void)
{
	// del character divice
	cdev_del(char_dev);

	kfree(char_dev);

	// release device number
	unregister_chrdev_region(MKDEV(major, 0), 1);

	printk(KERN_INFO "Character driver unloaded\n");
}

6. 模块初始化与卸载

module_init(chrdev_init);: 这一行代码用于指定模块的初始化函数。在模块加载时,内核会调用 chrdev_init 函数,这是模块初始化的入口点。

module_exit(chrdev_exit);: 这一行代码用于指定模块的卸载函数。在模块卸载时,内核会调用 chrdev_exit 函数,这是模块卸载的入口点。

module_init(chrdev_init);
module_exit(chrdev_exit);

MODULE_LICENSE("GPL");	

7. 字符设备读写——逻辑梳理

我觉得可以从两个角度两讲:计算机的角度和用户的角度。

- 计算机视角设备可以理解为计算机的一些资源(打印机、显示器、GPU显卡等),而每一个设备都应当对应着一个设备驱动程序,这在操作系统中通过两个链表进行保存的。
你每添加一个设备,就需要安装一个对应的驱动程序;同理,你每安装一个驱动程序,都需要添加一个设备。

即:操作系统是通过驱动程序来发出操控设备的指令

- 用户视角:设备文件是与设备驱动程序相关联的文件,通过与设备驱动程序进行交互,以便用户和应用程序可以访问硬件设备,而无需了解内核驱动程序的详细实现。

在Unix/Linux中,设备文件通常存在于/dev目录下,并根据设备类型和编号命名。例如,/dev/sda 可能表示系统中的第一个硬盘,/dev/ttyUSB0 可能表示USB串口设备。

重点来了

我们通过执行df -h查看系统中磁盘的使用情况
在这里插入图片描述
喔吼,可以看到两个sda都是位于dev目录下。注意,我们查找的是磁盘的使用情况,而终端通过告诉我们/dev/sda1的使用情况,来进一步告诉我们磁盘使用情况。

这不就对上了吗“Linux中,一切皆文件”。

原来所谓的皆文件,指的是这些设备资源在Linux中对应着设备文件的概念,这里直接让我悟了。(没看懂的建议反复观看这段内容)

我们操作磁盘、打印机、或者其它硬件设备,操作系统都为用户提供了一个对应的设备文件(设备节点),从而方便用户间接的操作这些硬件设备。

7.2 三者之间的关系

这里面的逻辑可以展示为如下结构图

在这里插入图片描述

设备文件与内核驱动程序进行绑定,这意味着它们通过内核驱动程序来实际操作硬件设备。用户和应用程序可以打开、读取、写入和关闭设备文件,而这些操作会触发相应的内核驱动程序操作。

7.3 设备文件的好处

总结:设备文件用提供一种标准化的方式来访问硬件设备并屏蔽底层硬件的复杂性

7.4 字符文件的读写(逻辑的梳理)

在这里插入图片描述
(1)设备驱动的注册

设备驱动三要素:

  • 设备号devNo:主设备号和次设备号
  • 文件操作结构体file_operation:四个系统调用对应的实体函数实现
  • 字符设备结构体cdev:内存的申请,结构体的初始化,在内核中的注册

(2)装载驱动

insmod charDriver.ko	# 安装编译好的字符驱动模块

module_init(chrdev_init)————>chrdev_init

(3)写操作

echo "hello Drivers!" > /dev/charDriver
  • .open ———> my_chrdev_read
  • .write ———> my_chrdev_write
  • .release ———>my_chrdev_release

(4)读操作

cat /dev/charDriver
  • .open ———> my_chrdev_read
  • .read ———> my_chrdev_read
  • .release ———>my_chrdev_release

(5)卸载驱动

rmmod charDriver		# 卸载指定驱动模块

module_exit(chrdev_exit)————>chrdev_exit


8. 总结

感觉以前操作系统白学了,看来理解一个东西还是需要一定的时间和一定的实践。

东西比较基础,写的比较多且随意,希望有任何问题,可以留言讨论。==!

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
以下是Python中易错的知识点梳理: 1. 缩进问题:Python中缩进是非常重要的,缩进不正确会导致程序出现语法错误或逻辑错误。 2. 变量命名:变量名不能以数字开头,不能包含空格和特殊字符,避免使用Python的关键字作为变量名。 3. 引用传递:Python中的列表、字典、集合等可变对象是引用传递,容易出现修改原对象的问题。 4. 字符串和列表的区别:字符串是不可变对象,而列表是可变对象,对字符串的修改会生成新的字符串,而对列表的修改会修改原列表。 5. 列表切片问题:在进行列表切片时,如果不指定切片的起始和终止位置,会默认从头开始或到末尾结束,容易出现索引越界的问题。 6. 匿名函数问题:Python中的lambda函数是匿名函数,不能直接调用,需要通过赋值给变量或作为参数传递给其他函数使用。 7. 类属性和实例属性:Python中的类属性是所有实例共享的,实例属性是每个实例独有的,容易出现使用混淆的问题。 8. 函数参数传递问题:Python中的函数参数传递有两种方式,分别是位置参数和关键字参数,容易出现顺序混乱或重复定义的问题。 9. 元组和列表的区别:元组和列表都是有序集合,但元组是不可变对象,不能修改,而列表是可变对象,可以修改。 10. 文件读写问题:在进行文件读写时,需要注意文件的打开和关闭操作,否则会出现文件无法访问或数据不完整的问题。 希望这些易错知识点的梳理能够帮助你避免在Python编程中常见的问题,提高编程效率和代码质量。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值