文章目录
-
- [一 基础知识](#一 基础知识)
- [二 linux 下的字符设备](#二 linux 下的字符设备)
-
- [字符设备在 `/dev` 目录下](#字符设备在
/dev
目录下) - [用 `ls -l` 命令查看字符设备文件类型](#用
ls -l
命令查看字符设备文件类型) - 主设备号和次设备号
- [字符设备在 `/dev` 目录下](#字符设备在
- [三 字符驱动模块的编写](#三 字符驱动模块的编写)
-
- [1. 头文件引入](#1. 头文件引入)
- [2. 定义错误码枚举](#2. 定义错误码枚举)
- [3. 设备操作函数定义](#3. 设备操作函数定义)
- [4. 关键结构体与变量定义](#4. 关键结构体与变量定义)
- [5. 驱动入口函数(`chrdev_init`)](#5. 驱动入口函数(
chrdev_init
)) - [6. 驱动出口函数(`chardev_exit`)](#6. 驱动出口函数(
chardev_exit
)) - [7. 模块注册相关宏](#7. 模块注册相关宏)
- [四 关键接口解释](#四 关键接口解释)
-
- [1. `dev_t` 类型变量(设备号)](#1.
dev_t
类型变量(设备号)) - [2. `MAJOR` 和 `MINOR` 宏](#2.
MAJOR
和MINOR
宏) - [3. `alloc_chrdev_region` 函数(设备号分配)](#3.
alloc_chrdev_region
函数(设备号分配)) - [4. `cdev_init` 函数(初始化字符设备结构体)](#4.
cdev_init
函数(初始化字符设备结构体)) - [5. `struct file_operations` 结构体(设备操作方法集合)](#5.
struct file_operations
结构体(设备操作方法集合)) - [6. `cdev_add` 函数(将字符设备添加到内核)](#6.
cdev_add
函数(将字符设备添加到内核)) - [7. `device_create` 函数(创建设备文件节点)](#7.
device_create
函数(创建设备文件节点)) - [8. `class_create` 函数(创建设备类)](#8.
class_create
函数(创建设备类)) - [9. `device_destroy` 函数(删除设备文件节点)](#9.
device_destroy
函数(删除设备文件节点)) - [10. `class_destroy` 函数(销毁设备类)](#10.
class_destroy
函数(销毁设备类)) - [11. `cdev_del` 函数(从内核中删除字符设备结构体)](#11.
cdev_del
函数(从内核中删除字符设备结构体)) - [12. `unregister_chrdev_region` 函数(释放设备号资源)](#12.
unregister_chrdev_region
函数(释放设备号资源))
- [1. `dev_t` 类型变量(设备号)](#1.
一 基础知识
字符设备是Linux操作系统中设备类型的一种
概念
字符设备是指以字节流的方式顺序访问的设备,其数据传输通常是无结构的、按顺序依次进行的,就像对一个普通文件按字节读写一样,一次读写一个或多个字节的数据。常见的字符设备包括键盘、鼠标、串口、终端设备等。
特点
- 顺序访问
以线性的顺序进行数据的读写操作,例如从键盘读取输入,是按照用户按键的先后顺序逐个字符接收的,不能像访问磁盘文件那样随意定位到某个特定位置进行读写(磁盘文件属于块设备,可以随机读写)。 - 无缓冲或较小缓冲
一般来说,字符设备往往没有大容量的内部缓冲区或者仅有较小的缓冲区。比如串口设备,数据一旦从外部传输过来就尽快被接收处理,不会像磁盘那样先缓存大量数据再批量处理,它更强调实时性地对逐个字符进行响应。 - 设备节点表示
在Linux系统的/dev
目录下会有对应的设备文件节点来代表字符设备,通过对这些设备文件进行标准的文件操作(如open
、read
、write
、close
等)系统调用,就能实现与实际硬件设备之间的数据交互。例如/dev/ttyS0
可能代表系统中的一个串口设备,应用程序可以打开这个文件节点,向其写入数据发送到串口连接的外部设备,或者从它读取外部设备发来的数据。
常见应用场景
- 终端交互
在Linux系统中,用户通过终端(如/dev/tty1
等虚拟终端或者通过串口连接的物理终端设备)登录并输入命令,输入的字符按顺序逐个被终端对应的字符设备驱动接收处理,然后传递给shell等上层应用程序进行解析和执行相应的操作,输出结果也同样按顺序逐个字符返回显示在终端上。 - 串口通信
许多嵌入式系统、工业控制场景会使用串口进行设备间通信。比如将开发板通过串口与电脑相连,电脑端的程序可以打开对应的串口字符设备(如/dev/ttyUSB0
之类的设备节点,取决于具体的串口设备映射情况),向开发板发送控制指令(字符形式的数据,像AT
指令等),也能接收开发板返回的状态信息等字符数据,实现数据交互。
二 linux 下的字符设备
以下是对这段话中涉及的几个关键要点更详细的阐述:
字符设备在 /dev
目录下
在 Linux 操作系统中,/dev
目录是存放设备文件的特殊目录,它就像是一个接口,让应用程序能够与各类硬件设备进行交互。字符设备以文件的形式存在于这个目录之中,例如 /dev/tty1
(代表第一个虚拟终端设备)、/dev/ttyS0
(常见的串口设备)等。虽然它们在形式上看起来类似普通文件,但本质有着很大区别,普通文件主要存储在磁盘等存储介质上,数据的读写遵循文件系统的管理规则,而字符设备文件对应的是硬件设备,读写操作最终会转化为对实际硬件的控制或数据传输行为。
用 ls -l
命令查看字符设备文件类型
当在终端中执行 ls -l
命令查看 /dev
目录下的文件时,可以通过输出结果中第一个字符来判断文件类型。对于字符设备文件,其第一个字符显示为 c
。例如:
在上述输出中,最左边的 c
就表明 /dev/tty1
是一个字符设备文件,后面的权限信息(rw-rw-rw-
等)、所属用户、所属组以及文件的主设备号和次设备号(这里是 4, 1
)等信息也依次列出。
主设备号和次设备号
-
主设备号(Major Device Number)
主设备号主要用于标识设备的类型,它将一类具有相似功能或访问方式的设备归为一组,并关联到对应的设备驱动程序。在内核中,不同的主设备号对应着不同的驱动模块,例如主设备号为
4
的可能都和终端设备相关,由终端设备驱动来统一管理操作。当应用程序发起对某个字符设备的操作请求时,内核根据设备文件的主设备号就能快速找到对应的驱动程序,从而把请求转交给相应的驱动去处理。 -
次设备号(Minor Device Number)
次设备号用于在同一类型(相同主设备号)的设备中区分不同的个体设备。以串口设备为例,如果系统中有多个串口,它们的主设备号可能是相同的(都对应串口设备驱动模块),但每个串口会有不同的次设备号来进行区分,像
/dev/ttyS0
可能次设备号为0
,/dev/ttyS1
可能次设备号为1
等等,这样内核就能准确知道具体是要操作哪一个实际的硬件设备个体了。
三 字符驱动模块的编写
代码如下
c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/kdev_t.h>
#include <linux/cdev.h>
#include <linux/kernel.h>
enum EXITCODE
{
DEVNUM_FAILED = -1,
ADD_FAILED = -2
};
static int chrdev_open(struct inode* inode, struct file* file)
{
printk("chardev is open\n");
//打印调用堆栈
dump_stack();
return 0;
}
static ssize_t chrdev_read(struct file* file, \
char __user *buf, size_t size, loff_t *off)
{
printk("chardev is read\n");
return 0;
}
static ssize_t chrdev_write(struct file* file, \
const char __user *buf, size_t size, loff_t* off)
{
printk("chardev is write");
return 0;
}
static int chrdev_release(struct inode* inode, struct file* file)
{
return 0;
}
//定义dev_t类型表示设备号
static dev_t dev_num;
//定义cdev变量表示要注册的字符设备
static struct cdev cdev_m;
//定义方法集合
static struct file_operations cdev_op = {
//将owner指向本模块,可以避免
//模块正在被使用的时候卸载
.owner = THIS_MODULE,
.open = chrdev_open,
.read = chrdev_read,
.write = chrdev_write,
.release = chrdev_release
};
// struct class 类型变量 表示创建的类
static struct class* class_test;
// 驱动入口函数
static int __init chrdev_init(void)
{
//返回值便于判断
int ret = 0;
// major 主设备号, minor 次设备号
unsigned int major = 0;
unsigned int minor = 0;
//自动获取设备号接口alloc_chrdev_region
ret = alloc_chrdev_region(&dev_num, 0, 1, "chardev_name");
if (ret < 0)
{
printk("get dev_num failed\n");
return DEVNUM_FAILED;
}
printk("get dev_num sucess \n");
major = MAJOR(dev_num);
minor = MINOR(dev_num);
printk("major is %d", major);
printk("minor is %d", minor);
// 初始化cdev_m结构体,绑定方法集合
cdev_init(&cdev_m, &cdev_op);
printk("cdev_init sucess \n");
//将owner指向本模块避免模块在使用时被卸载
cdev_m.owner = THIS_MODULE;
//绑定设备号到cdev_m结构体
ret = cdev_add(&cdev_m, dev_num, 1);
if (ret < 0)
{
printk("cdev_add failed\n");
return ADD_FAILED;
}
printk("cdev_add sucessed \n");
//初始化类
class_test = class_create(THIS_MODULE, "class_test");
//device_creat 进行设备创建
device_create(class_test, NULL, dev_num, NULL, "device_test");
printk("moudle init sucess\n");
return 0;
}
// 驱动出口函数
static void __exit chardev_exit(void)
{
device_destroy(class_test, dev_num);
class_destroy(class_test);
cdev_del(&cdev_m);
unregister_chrdev_region(dev_num, 1);
printk("moudle eixt\n");
}
//注册入口函数
module_init(chrdev_init);
//注册出口函数
module_exit(chardev_exit);
//开源协议版本
MODULE_LICENSE("GPL v2");
以下基于你提供的代码来说明Linux字符设备驱动模块的编写流程:
1. 头文件引入
c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/kdev_t.h>
#include <linux/cdev.h>
#include <linux/kernel.h>
首先,引入必要的头文件,这些头文件各自有着重要作用:
<linux/init.h>
和<linux/module.h>
提供了模块初始化和退出相关的宏定义(如__init
、__exit
)以及模块相关的基本结构和函数声明,用于定义驱动模块如何被加载进内核以及如何被卸载。<linux/fs.h>
包含了文件系统相关操作的结构体和函数声明,对于字符设备来说,这里面定义了如struct file_operations
等重要结构体,用于描述设备操作函数的集合。<linux/kdev_t.h>
定义了设备号(dev_t
类型)相关的类型和操作宏,帮助处理设备号的分配、解析等操作,因为每个字符设备在内核中都需要通过唯一的设备号来标识。<linux/cdev.h>
提供了字符设备结构体(struct cdev
)以及相关操作函数的声明,用于在内核中构建和管理字符设备对象。<linux/kernel.h>
包含了一些内核通用的函数和宏定义,像printk
函数用于在内核空间打印信息,方便调试和记录状态。
2. 定义错误码枚举
c
enum EXITCODE
{
DEVNUM_FAILED = -1,
ADD_FAILED = -2
};
通过定义枚举类型 EXITCODE
,设定了在驱动模块初始化过程中可能出现的不同错误情况对应的错误码,这样在后续代码中如果遇到设备号分配失败或者字符设备添加失败等问题时,可以返回相应明确的错误码,便于上层(比如内核加载模块时)知晓具体出错环节。
3. 设备操作函数定义
chrdev_open
函数:
c
static int chrdev_open(struct inode* inode, struct file* file)
{
printk("chardev is open\n");
//打印调用堆栈
dump_stack();
return 0;
}
当用户空间的程序通过 open
系统调用打开对应的字符设备文件时,内核会调用此函数。这里的函数只是简单地打印一条表示设备已打开的消息,并调用 dump_stack
(通常用于调试目的,可查看当前的调用堆栈情况),最后返回 0
表示打开操作成功。实际应用中,在这个函数里可以进行设备相关的初始化工作,比如初始化硬件设备的寄存器、配置设备工作模式等。
chrdev_read
函数:
c
static ssize_t chrdev_read(struct file* file, \
char __user *buf, size_t size, loff_t *off)
{
printk("chardev is read\n");
return 0;
}
此函数在用户空间发起 read
系统调用尝试从字符设备读取数据时被内核调用。当前代码只是简单打印了正在进行读操作的提示信息,并未真正实现数据读取功能(正常情况下应该从设备的存储区域或者数据来源处获取数据,并将其复制到用户空间提供的缓冲区 buf
中),返回 0
表示读取了 0 个字节的数据。
chrdev_write
函数:
c
static ssize_t chrdev_write(struct file* file, \
const char __user *buf, size_t size, loff_t* off)
{
printk("chardev is write");
return 0;
}
当用户空间通过 write
系统调用向字符设备写入数据时,内核会调用该函数。这里仅打印了表示正在写操作的信息,实际并没有把用户空间传来的数据真正写入到设备对应的存储或控制区域,返回 0
表示写入了 0 个字节的数据。
chrdev_release
函数:
c
static int chrdev_release(struct inode* inode, struct file* file)
{
return 0;
}
在用户空间程序使用 close
系统调用关闭字符设备文件时,内核会调用这个函数来执行设备释放相关操作,当前它直接返回 0
,表示关闭操作成功。实际应用中,可能需要在这个函数里清理设备使用过程中占用的资源,比如释放硬件设备占用的内存、复位相关寄存器等。
4. 关键结构体与变量定义
- 设备号变量定义:
c
static dev_t dev_num;
定义了 dev_t
类型的变量 dev_num
,用于存储字符设备的设备号,设备号由主设备号和次设备号组成,后续通过相关函数分配得到具体的值,这个设备号用于在内核中唯一标识该字符设备。
- 字符设备结构体定义:
c
static struct cdev cdev_m;
声明了 struct cdev
类型的结构体变量 cdev_m
,它代表要注册的字符设备,后续需要对其进行初始化、绑定操作函数以及关联设备号等一系列操作,使其能被内核正确识别和管理。
- 文件操作函数集合结构体定义:
c
static struct file_operations cdev_op = {
//将owner指向本模块,可以避免
//模块正在被使用的时候卸载
.owner = THIS_MODULE,
.open = chrdev_open,
.read = chrdev_read,
.write = chrdev_write,
.release = chrdev_release
};
定义 struct file_operations
结构体 cdev_op
,将前面定义的各个设备操作函数(open
、read
、write
、release
)整合在一起,形成一个统一的操作函数集合,并且通过 .owner
字段将其所属模块指定为当前模块(THIS_MODULE
),告知内核该操作集合属于哪个模块,这样内核就能在合适的时候判断模块是否能被卸载(比如当有程序正在使用该模块对应的设备时,就不能卸载模块)。
- 设备类变量定义:
c
static struct class* class_test;
定义 struct class
类型的变量 class_test
,用于创建一个设备类。设备类是内核中对设备进行分类管理的一种机制,通过创建设备类,可以方便地在 /dev
目录下创建对应的设备文件节点,便于用户空间程序通过这些设备文件与字符设备进行交互。
5. 驱动入口函数(chrdev_init
)
- 设备号分配:
c
int ret = 0;
unsigned int major = 0;
unsigned int minor = 0;
ret = alloc_chrdev_region(&dev_num, 0, 1, "chardev_name");
if (ret < 0)
{
printk("get dev_num failed\n");
return DEVNUM_FAILED;
}
printk("get dev_num sucess \n");
major = MAJOR(dev_num);
minor = MINOR(dev_num);
printk("major is %d", major);
printk("minor is %d", minor);
在驱动入口函数中,首先调用 alloc_chrdev_region
函数来动态分配设备号。这个函数会在内核中自动寻找一个未使用的主设备号,并根据传入的参数分配相应的次设备号(这里从 0
开始,分配 1
个设备号),同时关联一个设备名称("chardev_name"
)。如果分配失败(ret < 0
),就通过 printk
函数打印错误消息,并返回对应的错误码(DEVNUM_FAILED
);若分配成功,则打印出成功获取设备号的消息,并通过 MAJOR
和 MINOR
宏分别获取并打印出分配得到的主设备号和次设备号。
- 字符设备初始化与绑定操作函数:
c
cdev_init(&cdev_m, &cdev_op);
printk("cdev_init sucess \n");
cdev_m.owner = THIS_MODULE;
使用 cdev_init
函数初始化之前定义的字符设备结构体 cdev_m
,将其与包含设备操作函数的 cdev_op
结构体进行绑定,使字符设备知道在不同操作请求(如打开、读写、关闭)时应该调用哪些对应的函数。然后将 cdev_m
的 .owner
属性设置为当前模块(THIS_MODULE
),保证模块归属的一致性,便于内核进行模块管理。
- 字符设备添加到内核:
c
ret = cdev_add(&cdev_m, dev_num, 1);
if (ret < 0)
{
printk("cdev_add failed\n");
return ADD_FAILED;
}
printk("cdev_add sucessed \n");
调用 cdev_add
函数将初始化好的字符设备 cdev_m
以及对应的设备号(dev_num
)添加到内核中,这样内核就能识别并管理这个字符设备了。如果添加失败(ret < 0
),会打印错误消息并返回相应错误码(ADD_FAILED
);添加成功则打印出成功添加的提示信息。
- 设备类创建与设备文件节点创建:
c
class_test = class_create(THIS_MODULE, "class_test");
device_create(class_test, NULL, dev_num, NULL, "device_test");
printk("moudle init sucess\n");
首先调用 class_create
函数创建一个名为 "class_test"
的设备类,该设备类属于当前模块(THIS_MODULE
)。接着使用 device_create
函数基于创建的设备类 class_test
和已分配的设备号 dev_num
创建一个名为 "device_test"
的设备文件节点,这个设备文件节点会出现在 /dev
目录下,使得用户空间的程序可以像操作普通文件一样通过这个设备文件与字符设备进行交互,最后打印出模块初始化成功的消息。
6. 驱动出口函数(chardev_exit
)
c
void __exit chardev_exit(void)
{
device_destroy(class_test, dev_num);
class_destroy(class_test);
cdev_del(&cdev_m);
unregister_chrdev_region(dev_num, 1);
printk("moudle eixt\n");
}
此函数在驱动模块被卸载时执行,用于清理在驱动初始化过程中创建和分配的各类资源。具体操作如下:
- 首先调用
device_destroy
函数删除/dev
目录下对应的设备文件节点(即之前通过device_create
创建的"device_test"
设备文件),避免设备文件残留造成混淆。 - 接着调用
class_destroy
函数销毁之前创建的设备类(class_test
),释放设备类相关的资源。 - 然后使用
cdev_del
函数从内核中删除字符设备结构体(cdev_m
),解除内核对该字符设备的管理。 - 最后调用
unregister_chrdev_region
函数释放之前分配的设备号(dev_num
),将设备号资源归还给内核,完成整个资源清理过程后,通过printk
函数打印出模块退出的消息。
7. 模块注册相关宏
c
module_init(chrdev_init);
module_exit(chardev_exit);
MODULE_LICENSE("GPL v2");
module_init
和module_exit
这两个宏分别用于注册驱动的入口函数(chrdev_init
)和出口函数(chardev_exit
),告知内核在加载和卸载模块时应该调用哪些函数来进行相应的初始化和清理操作。MODULE_LICENSE("GPL v2")
声明了该模块遵循的开源协议版本,表明采用 GNU 通用公共许可证第二版(GPL v2)协议,符合 Linux 内核模块开发的规范要求,使得内核知道该模块在版权和使用方面的相关限制及规定。
四 关键接口解释
以下是对这些Linux字符设备驱动开发中关键接口的详细解释:
1. dev_t
类型变量(设备号)
-
定义与用途 :
dev_t
是Linux内核中用于表示设备号的数据类型,本质上是一个整数类型(其具体的位宽可能因内核配置而异,通常为32位或64位)。设备号由主设备号(Major Device Number)和次设备号(Minor Device Number)两部分组成,主设备号用于标识设备的类型,它决定了设备将由哪个驱动程序来处理,相同类型的设备一般具有相同的主设备号;次设备号则用于在同一类型的多个设备中进行区分,比如系统中有多个串口设备,它们的主设备号相同,通过不同的次设备号来区分各个具体的串口。在代码中,通常会定义一个dev_t
类型的变量来存储通过相关函数分配得到的设备号,后续可基于这个变量进行设备注册、操作等一系列流程。 -
示例代码体现:
c
static dev_t dev_num;
在上述代码中,定义了 dev_num
变量用于保存设备号,后续会通过如 alloc_chrdev_region
函数为其赋值,并且可以利用 MAJOR
和 MINOR
宏从中提取主设备号和次设备号信息。
2. MAJOR
和 MINOR
宏
-
定义与用途 :
MAJOR
和MINOR
是Linux内核提供的宏,用于从dev_t
类型的设备号中提取出主设备号和次设备号。它们基于内核中设备号的编码规则,通过位运算等方式来实现提取操作。开发人员在代码中使用这两个宏,能够方便地获取设备号中的关键组成部分,进而用于日志记录、判断设备类型、区分具体设备个体等操作,例如在打印设备号分配情况或者在内核驱动中根据不同设备个体执行差异化操作时就会用到它们。 -
示例代码体现:
c
unsigned int major = 0;
unsigned int minor = 0;
major = MAJOR(dev_num);
minor = MINOR(dev_num);
printk("major is %d", major);
printk("minor is %d", minor);
在上述代码片段中,先声明了变量 major
和 minor
分别用于存储提取出的主设备号和次设备号,然后通过 MAJOR
和 MINOR
宏从已有的 dev_num
设备号变量中提取相应值,并使用 printk
函数进行打印展示,方便查看设备号的具体分配情况。
3. alloc_chrdev_region
函数(设备号分配)
-
定义与用途 :
alloc_chrdev_region
函数用于动态分配字符设备的设备号。它会在内核的设备号资源池中自动查找一个尚未被使用的主设备号,并根据传入的参数来分配对应的次设备号。其主要目的是为新创建的字符设备获取一个唯一的设备号标识,使得内核能够对该设备进行管理和区分于其他设备。 -
函数原型及参数说明:
c
int alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned int count, const char *name);
dev
:指向dev_t
类型变量的指针,函数成功执行后,会将分配得到的设备号存储到这个指针所指向的变量中,以便后续在设备注册等操作中使用。firstminor
:指定次设备号的起始值,通常传入0
,表示从次设备号为 0 开始分配,根据实际需求也可以指定其他合适的起始值。count
:表示要分配的设备数量,一般情况下,对于单个字符设备驱动开发,传入1
即可,表示只需要分配一个设备号;如果要同时为多个相关的字符设备分配连续的设备号,则传入相应的设备数量值。name
:一个字符串参数,用于给分配的设备命名,这个名称会在内核的一些设备管理相关的地方体现,比如在/proc/devices
文件中可以看到设备对应的名称(格式为 主设备号 设备名),方便开发人员查看设备信息以及调试等操作。
-
返回值说明 :
如果设备号分配成功,函数返回
0
;若在分配过程中出现错误,例如内核中已经没有可用的主设备号资源了,函数会返回一个负数,在代码中通常需要根据返回值进行相应的错误处理,比如打印错误提示信息、返回错误码给调用者或者进行资源清理等操作,以保证驱动程序的稳定性和正确性。 -
示例代码体现:
c
int ret = 0;
ret = alloc_chrdev_region(&dev_num, 0, 1, "chardev_name");
if (ret < 0)
{
printk("get dev_num failed\n");
return DEVNUM_FAILED;
}
上述代码中,调用 alloc_chrdev_region
函数尝试为字符设备分配设备号,将分配结果存储在 dev_num
变量中,若分配失败(ret < 0
),则通过 printk
函数打印错误消息,并返回预先定义的错误码(DEVNUM_FAILED
),表示设备号分配这一环节出现了问题。
4. cdev_init
函数(初始化字符设备结构体)
-
定义与用途 :
cdev_init
函数用于初始化struct cdev
类型的字符设备结构体,它的核心作用是将一个自定义的字符设备与一组设备操作函数(定义在struct file_operations
结构体中)进行绑定,使得内核知道针对这个字符设备的各种操作(如打开、读写、关闭等)应该调用哪些具体的函数来处理,从而建立起字符设备与实际操作逻辑之间的联系。 -
函数原型及参数说明:
c
void cdev_init(struct cdev *cdev, const struct file_operations *fops);
cdev
:指向要初始化的struct cdev
结构体变量的指针,这个结构体代表了即将要注册到内核的字符设备,通过该函数的初始化操作,会对其内部的成员进行设置,使其能够与相关操作函数正确关联起来。fops
:指向struct file_operations
结构体的指针,该结构体包含了一系列设备操作函数的指针,如open
、read
、write
、release
等,这些函数定义了针对字符设备的各种操作逻辑,cdev_init
函数会将cdev
结构体与这个操作函数集合进行绑定,以便内核后续依据相应操作请求调用对应的函数。
- 示例代码体现:
c
static struct cdev cdev_m;
static struct file_operations cdev_op = {
.owner = THIS_MODULE,
.open = chrdev_open,
.read = chrdev_read,
.write = chrdev_write,
.release = chrdev_release
};
cdev_init(&cdev_m, &cdev_op);
在上述代码中,首先定义了 struct cdev
类型的变量 cdev_m
和包含设备操作函数的 struct file_operations
结构体 cdev_op
,然后通过 cdev_init
函数将 cdev_m
与 cdev_op
进行绑定,这样就为字符设备 cdev_m
设定好了后续在内核中响应各种操作请求时所需调用的具体函数。
5. struct file_operations
结构体(设备操作方法集合)
-
定义与用途 :
struct file_operations
是Linux内核中极为重要的一个结构体,它用于将字符设备(或其他可被文件系统抽象表示的设备,如块设备等)相关的各种操作函数进行统一封装和管理。结构体中定义了众多函数指针成员,每个成员对应一种设备操作,开发人员通过自定义这些函数指针所指向的具体函数,来实现字符设备特定的操作逻辑,进而将这个结构体与字符设备结构体(struct cdev
)相关联,使得内核能够依据用户空间发起的系统调用(如open
、read
、write
、close
等)准确地调用相应的设备操作函数,最终实现用户空间与设备之间的交互。 -
结构体成员示例及说明(部分常用成员):
c
struct file_operations {
struct module *owner;
ssize_t (*read)(struct file *, char __user *, size_t, loff_t *);
ssize_t (*write)(struct file *, const char __user *, size_t, loff_t *);
int (*open)(struct inode *, struct file *);
int (*release)(struct inode *, struct file *);
// 还有很多其他成员,用于不同的设备操作,如 lseek、ioctl 等
};
owner
:通常设置为THIS_MODULE
,用于告知内核这个操作函数集合所属的模块,这样内核可以基于此进行模块管理,例如判断模块是否能被卸载(如果有设备正在使用该模块对应的操作函数,那么该模块就不能被卸载)。read
:指向设备读取函数的指针,其函数原型为ssize_t (*read)(struct file *, char __user *, size_t, loff_t *)
,当用户空间发起read
系统调用时,内核会调用这个指针所指向的函数来处理从设备读取数据并返回给用户空间的操作。write
:指向设备写入函数的指针,函数原型为ssize_t (*write)(struct file *, const char __user *, size_t, loff_t *)
,用于处理用户空间通过write
系统调用向设备写入数据的操作,即将用户空间的数据写入到设备对应的存储或控制区域。open
:指向设备打开函数的指针,函数原型为int (*open)(struct inode *, struct file *)
,当用户空间通过open
系统调用打开设备文件时,内核调用此函数来进行设备的初始化等相关操作,比如初始化设备相关的寄存器、设置设备状态等。release
:指向设备关闭函数的指针,函数原型为int (*release)(struct inode *, struct file *)
,在用户空间使用close
系统调用关闭设备文件时,内核调用此函数来执行设备释放相关的操作,例如清理设备使用过程中占用的资源、复位相关寄存器等。
- 示例代码体现:
c
static struct file_operations cdev_op = {
.owner = THIS_MODULE,
.open = chrdev_open,
.read = chrdev_read,
.write = chrdev_write,
.release = chrdev_release
};
这里定义了 struct file_operations
结构体 cdev_op
,并将其成员函数指针分别指向自定义的 chrdev_open
、chrdev_read
、chrdev_write
、chrdev_release
等设备操作函数,以此来定义字符设备的具体操作逻辑,后续再通过 cdev_init
等函数将其与字符设备结构体绑定,使设备能按设定的逻辑响应操作请求。
6. cdev_add
函数(将字符设备添加到内核)
-
定义与用途 :
cdev_add
函数的作用是将已经初始化好(通过cdev_init
函数初始化并绑定了操作函数集合)的字符设备结构体(struct cdev
)以及对应的设备号添加到内核中,使得内核能够识别并管理这个字符设备,这样当用户空间发起对该设备相关的系统调用时,内核就能根据设备号准确地找到对应的字符设备结构体,并调用其绑定的操作函数来进行处理,从而实现设备与用户空间的交互。 -
函数原型及参数说明:
c
int cdev_add(struct cdev *p, dev_t dev, unsigned int count);
p
:指向要添加到内核的struct cdev
结构体变量的指针,也就是之前已经完成初始化且绑定了操作函数集合的字符设备结构体,该结构体包含了设备相关的操作逻辑以及与内核交互的必要信息。dev
:表示该字符设备对应的设备号(dev_t
类型),这个设备号是通过alloc_chrdev_region
等函数分配得到的,内核依靠这个设备号来唯一标识该字符设备,以便区分于其他众多设备。count
:指定要添加的设备数量,通常对于单个字符设备驱动开发,传入1
即可,表示添加一个字符设备到内核中;如果是批量添加多个相关的字符设备,则传入相应的设备数量值。
-
返回值说明 :
如果字符设备添加成功,函数返回
0
;若在添加过程中出现问题,比如设备号冲突、内核资源不足等情况,函数会返回一个负数,在代码中一般需要根据返回值进行相应的错误处理,例如返回错误码、进行资源清理操作(如释放已分配的设备号、删除相关的设备结构体等),以避免出现内核不稳定或者资源泄漏等问题。 -
示例代码体现:
c
int ret = 0;
ret = cdev_add(&cdev_m, dev_num, 1);
if (ret < 0)
{
printk("cdev_add failed\n");
return ADD_FAILED;
}
上述代码调用 cdev_add
函数将字符设备结构体 cdev_m
以及对应的设备号 dev_num
添加到内核中,若添加失败(ret < 0
),则通过 printk
函数打印错误消息,并返回预先定义的错误码(ADD_FAILED
),表示字符设备添加到内核这一环节出现了错误。
7. device_create
函数(创建设备文件节点)
-
定义与用途 :
device_create
函数用于在/dev
目录下创建一个设备文件节点,它基于给定的设备类(struct class
)和设备号来创建对应的设备文件,使得用户空间的程序能够像操作普通文件一样通过这个设备文件与字符设备进行交互,方便了应用程序与设备驱动之间的通信,是实现设备驱动在用户空间可见和可访问的重要步骤。 -
函数原型及参数说明:
c
struct device *device_create(struct class *class, struct device *parent,
dev_t dev, void *drvdata, const char *fmt,...);
class
:指向struct class
类型的指针,代表设备所属的类,设备类是内核中对设备进行分类管理的一种机制,通常先通过class_create
函数创建好设备类,再将其作为参数传入此处用于创建设备文件节点,使得创建的设备文件节点能够关联到对应的设备类下进行统一管理。parent
:指向struct device
类型的指针,一般用于指定设备的父设备,如果没有父设备,可以传入NULL
,表示该设备是独立的。dev
:设备号(dev_t
类型),即之前分配并与字符设备结构体绑定的设备号,通过这个设备号来确定要创建设备文件节点对应的具体设备,确保设备文件与实际的字符设备相对应。drvdata
:一个指针,用于传递一些驱动相关的数据,一般情况下如果没有特殊的数据需要传递,可以传入NULL
。fmt
:一个格式化字符串,用于指定设备文件的名称,后续还可以根据需要传入可变参数来按照格式化规则生成完整的设备文件名,例如传入"my_device"
,则会在/dev
目录下创建名为my_device
的设备文件(实际名称可能还会根据内核的一些默认规则添加前缀等)。
- 返回值说明 :
如果设备文件节点创建成功,函数返回一个指向struct device
结构体的指针,代表创建好的设备;若创建过程中出现错误,比如设备类不存在、设备号不合法等情况,函数返回NULL
,在代码中通常需要根据返回值进行相应的错误处理,例如打印错误提示信息、进行资源清理等操作,确保系统的稳定性。
8. class_create
函数(创建设备类)
-
定义与用途 :
class_create
函数用于在内核中创建一个设备类,设备类是一种对设备进行分类管理的机制,它可以方便地组织和管理多个相关的设备,同时也为后续在/dev
目录下创建设备文件节点提供了基础,通过将设备关联到相应的设备类,能够使设备的管理更加有序、规范,并且便于用户空间程序统一地查找和访问相关设备。 -
函数原型及参数说明:
c
struct class *class_create(struct module *owner, const char *name);
owner
:指向struct module
类型的指针,通常设置为THIS_MODULE
,用于表明这个设备类所属的模块,这样内核可以基于此进行模块与设备类相关的管理,例如判断在模块卸载时是否需要对相关设备类进行清理等操作。name
:一个字符串,用于指定设备类的名称,这个名称会在内核中用于标识该设备类,方便开发人员在管理和查看设备相关信息时进行区分和识别,例如在/sys/class
目录下可以看到以该名称命名的目录,对应创建的设备类。
- 返回值说明 :
如果设备类创建成功,函数返回一个指向struct class
结构体的指针,代表创建好的设备类;若创建过程中出现错误,比如内存不足、名称冲突等情况,函数返回一个错误指针(通常是ERR_PTR
类型的值),在代码中需要根据返回值进行相应的错误处理,比如判断返回值是否为错误指针,如果是则打印错误提示信息、进行相关
以下是继续对剩余接口的解释:
9. device_destroy
函数(删除设备文件节点)
-
定义与用途 :
device_destroy
函数用于删除之前通过device_create
函数在/dev
目录下创建的设备文件节点。当设备驱动模块被卸载或者设备不再需要对外提供用户空间访问接口时,需要调用此函数来清理相应的设备文件,避免文件残留导致系统中出现无效的设备文件引用,保证设备管理的一致性和系统的整洁性。 -
函数原型及参数说明:
c
void device_destroy(struct class *class, dev_t dev);
class
:指向struct class
类型的指针,即之前创建设备文件节点时所关联的设备类,用于确定要删除的设备文件节点所属的设备类范围,确保准确删除对应的设备文件,避免误删其他无关设备的文件节点。
*dev
:设备号(dev_t
类型),与要删除的设备文件节点对应的设备号一致,通过设备类和设备号的双重定位,精准地找到并删除对应的设备文件节点。
10. class_destroy
函数(销毁设备类)
-
定义与用途 :
class_destroy
函数用于销毁之前通过class_create
函数创建的设备类。当设备驱动模块被卸载,且相关的设备文件节点都已经通过device_destroy
函数删除后,需要调用此函数来释放设备类所占用的内核资源,完成整个设备管理相关资源的清理工作,避免内存泄漏等问题,使内核资源得到合理回收和再利用。 -
函数原型及参数说明:
c
void class_destroy(struct class *class);
这里只需要传入要销毁的设备类的指针(即之前 class_create
函数返回的指向 struct class
结构体的指针),函数会根据传入的指针找到对应的设备类,并释放其在内核中占用的各种资源,如相关的数据结构、内存空间等。
11. cdev_del
函数(从内核中删除字符设备结构体)
-
定义与用途 :
cdev_del
函数用于将已经添加到内核中的字符设备结构体(通过cdev_add
函数添加的struct cdev
结构体)从内核中删除。通常在设备驱动模块卸载时调用,它解除了内核与该字符设备之间的关联,释放了字符设备结构体及其相关资源在内核中占用的空间,是清理设备驱动相关资源的重要一步,确保内核资源的正确回收和后续系统的稳定运行。 -
函数原型及参数说明:
c
void cdev_del(struct cdev *p);
只需传入指向要删除的 struct cdev
结构体的指针(也就是之前通过 cdev_init
等操作初始化并添加到内核的字符设备结构体指针),函数就会执行相应的删除操作,将该字符设备从内核的设备管理体系中移除,并释放相关资源。
12. unregister_chrdev_region
函数(释放设备号资源)
-
定义与用途 :
unregister_chrdev_region
函数用于释放之前通过alloc_chrdev_region
等函数分配的设备号资源。当设备驱动模块不再使用相应的设备号(比如驱动被卸载的情况),需要调用此函数将设备号归还给内核的设备号资源池,以便其他设备驱动可以使用这些空闲的设备号,实现设备号资源的合理分配和循环利用,避免设备号资源的浪费和潜在的冲突问题。 -
函数原型及参数说明:
c
void unregister_chrdev_region(dev_t dev, unsigned int count);
dev
:设备号(dev_t
类型),传入之前分配并使用的设备号,用于明确要释放的设备号资源具体是哪个(或哪些,如果分配了多个连续设备号的话)。count
:指定要释放的设备号数量,一般与之前分配设备号时传入alloc_chrdev_region
函数的count
参数值保持一致,通常在单个字符设备驱动卸载时传入1
,表示释放一个设备号资源。