这里以一个简单的字符设备文件的源码实现为例子,做出仔细分析。
以下列出的都是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
#define MYNMAJOR 240 // 主设备号
#define MYNAME "my_chrdev" // 设备名称
char kbuf[100]; // 定义一个缓存数组,用于内核接收数据
static struct cdev *char_dev; // 定义一个字符设备结构体
static int major; // 用于保存主设备号
kbuf
:这个数组在内核中被分配,并且在内核模块的整个生命周期中都可以访问。struct cdev
类型:一种表示字符设备的结构体,可以通过它来管理字符设备的操作。这些函数具有标准的划分,分为open
、release
、read
、write
方法。
用户根据具体需求实现,实际上实现功能就是字符驱动程序对应的.open
、.release
、.read
、.write
方法的功能。在结构体中建立设备文件的四大方法与设备驱动的四大方法的连接。
(坚持看下去吧,我也坚持写下来了呢)。
该函数是字符设备驱动中的"打开"函数,用于处理用户空间程序打开设备文件时的操作。方法名通常采用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,表示设备成功打开;返回非零,则表示设备打开错误,该错误码会传递给用户空间程序。
与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,表示设备成功释放;返回非零,则表示设备释放错误,该错误码会传递给用户空间程序。
这个函数的作用是将用户空间的数据写入设备的数据缓冲区,并可能触发与硬件相关的操作。
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;
}
在实际应用中,你可能需要根据写入的数据来执行一些特定的操作,比如基于数据来控制硬件设备的行为。
这个函数的作用是从设备的数据缓冲区 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
该代码定义了一个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,
};
字符设备创建及准备工作,其中包括:
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;
}
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");
}
module_init(chrdev_init);
: 这一行代码用于指定模块的初始化函数。在模块加载时,内核会调用 chrdev_init 函数,这是模块初始化的入口点。
module_exit(chrdev_exit);
: 这一行代码用于指定模块的卸载函数。在模块卸载时,内核会调用 chrdev_exit 函数,这是模块卸载的入口点。
module_init(chrdev_init);
module_exit(chrdev_exit);
MODULE_LICENSE("GPL");