操作系统——简单字符设备文件的原理以及实现

简单字符设备文件的原理以及实现

    • 1. 头文件
    • 2. 全局变量定义
    • 3. 设备操作的实体函数
      • 3.1 open方法
      • 3.2 release方法
      • 3.3 write方法
      • 3.4 read 方法
    • 4 设备文件结构体
    • 5. 设备的初始化与销毁
      • 5.1 chrdev_init
      • 5.2 chrdev_exit
    • 6. 模块初始化与卸载

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

1. 头文件

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

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

#include 		// module_init  module_exit
#include 			// __init  __exit
#include 			// file_operations
#include 		// copy_to-user		copy_from_user
#include 			// cdev_init	cdev_add	cdev_del
#include  		// 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");	

你可能感兴趣的:(linux)