文章目录
- 字符设备驱动开发
-
- 一、字符设备驱动简介
- 二、什么是字符设备驱动?
- 三、字符设备驱动的核心组成部分
-
-
- [3.1 设备号 (`dev_t`)](#3.1 设备号 (
dev_t)) - [3.2 文件操作集 (`struct file_operations`)](#3.2 文件操作集 (
struct file_operations)) - [3.3 字符设备结构 (`struct cdev`)](#3.3 字符设备结构 (
struct cdev))
- [3.1 设备号 (`dev_t`)](#3.1 设备号 (
-
- 四、字符设备驱动的编写重要关键部分
-
- 4.1驱动模块的加载和卸载
- 4.2字符设备注册与注销
- [4.3 实现设备的具体操作函数](#4.3 实现设备的具体操作函数)
- [4.4 Linux设备号](#4.4 Linux设备号)
- [4.5 设备号的分配与注册](#4.5 设备号的分配与注册)
-
- [1. 静态分配](#1. 静态分配)
- [2. 动态分配(推荐)](#2. 动态分配(推荐))
- [4.6 字符设备驱动的编写一般流程](#4.6 字符设备驱动的编写一般流程)
- 五、一个简单的"虚拟"字符设备驱动示例
-
- [5.1 驱动函数:](#5.1 驱动函数:)
- [5.2 对应的用户层应用程序:](#5.2 对应的用户层应用程序:)
-
- [5.2.1 文件操作基本函数](#5.2.1 文件操作基本函数)
- 六、总结
字符设备驱动开发
一、字符设备驱动简介
在理解字符设备驱动之前,首先要明白什么是字符设备。Linux/Unix 系统将硬件设备分为两大类:
-
字符设备
- 特点 :以字节流 的形式进行数据传输。数据像水流一样,没有固定的块大小,只能顺序访问。
- 举例 :键盘、鼠标、串口、控制台 (
/dev/console)、声卡等。我们最常见的例子就是终端,你输入的字符会一个接一个地传递给系统。
-
块设备
- 特点 :以数据块 为单位进行数据传输(如 512字节、1KB、4KB)。支持随机访问,可以任意跳到某个数据块进行读写。通常有缓存机制。
- 举例:硬盘、U盘、SD卡等存储设备。
简单比喻:
- 字符设备 像一个水龙头,水(数据)是连续不断地流出来的。
- 块设备 像一个邮局信箱,数据被分装在一个个固定大小的"信件"(数据块)里,你可以随意取任意一封信。
二、什么是字符设备驱动?
字符设备驱动 就是一段运行在内核空间 的程序代码,它充当了用户空间应用程序 与底层硬件字符设备之间的桥梁。
它负责:
- 将用户的请求 (如
read,write)翻译成操控硬件的具体指令。 - 将硬件的行为和状态 (如数据到达、操作完成)反馈给用户程序。
用户程序通过操作系统的"一切皆文件 "哲学来访问设备,即通过打开 /dev 目录下的某个设备文件(如 /dev/ttyS0 代表串口1),然后使用标准的文件操作函数(如 open, read, write, close)与之交互。而驱动的工作,就是实现这些函数背后的具体逻辑。
应用程序运行在用户空间 ,而 Linux 驱动属于内核的一部分,因此驱动运行于内核空间。当我们在用户空间想要实现对内核的操作,比如使用 open 函数打开/dev/led 这个驱动,因为用户空间不能直接对内核进行操作,因此必须使用一个叫做"系统调用"的方法来实现从用户空间"陷入"到内核空间,这样才能实现对底层驱动的操作。open、close、write 和 read 等这些函数是由 C 库提供的,在 Linux 系统中,系统调用作为 C 库的一部分。当我们调用 open 函数的时候流程如图1所示:

图一

三、字符设备驱动的核心组成部分
编写一个字符设备驱动,主要涉及以下几个核心概念和结构:
3.1 设备号 (dev_t)
- 这是内核中用来唯一标识一个设备的号码。
- 由 主设备号 和 次设备号 组成。
- 主设备号:标识设备对应的驱动。例如,所有由同一个驱动管理的串口共享一个主设备号。
- 次设备号:由驱动使用,用来区分同一个驱动管理的不同设备或实例。例如,一个驱动管理两个串口,它们主设备号相同,但次设备号不同。
- 驱动需要向内核申请(或静态指定)一个设备号。
3.2 文件操作集 (struct file_operations)
-
这是字符设备驱动的灵魂 ,是一个函数指针的集合。
-
驱动开发者通过填充这个结构体,告诉内核:"当用户对我的设备文件进行某种操作时,请调用我写的这个函数。"
-
关键成员包括:
cstruct file_operations { struct module *owner; loff_t (*llseek) (struct file *, loff_t, int); ssize_t (*read) (struct file *, char __user *, size_t, loff_t *); ssize_t (*write) (struct file *, const char __user *, size_t,loff_t *); ssize_t (*read_iter) (struct kiocb *, struct iov_iter *); ssize_t (*write_iter) (struct kiocb *, struct iov_iter *); int (*iterate) (struct file *, struct dir_context *); unsigned int (*poll) (struct file *, struct poll_table_struct *); long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long); long (*compat_ioctl) (struct file *, unsigned int, unsigned long); int (*mmap) (struct file *, struct vm_area_struct *); int (*mremap)(struct file *, struct vm_area_struct *); int (*open) (struct inode *, struct file *); int (*flush) (struct file *, fl_owner_t id); int (*release) (struct inode *, struct file *); int (*fsync) (struct file *, loff_t, loff_t, int datasync); int (*aio_fsync) (struct kiocb *, int datasync); int (*fasync) (int, struct file *, int); int (*lock) (struct file *, int, struct file_lock *); ssize_t (*sendpage) (struct file *, struct page *, int, size_t,loff_t *, int); unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long); int (*check_flags)(int); int (*flock) (struct file *, int, struct file_lock *); ssize_t (*splice_write)(struct pipe_inode_info *, struct file *,loff_t *, size_t, unsigned int); ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int); int (*setlease)(struct file *, long, struct file_lock **, void **); long (*fallocate)(struct file *file, int mode, loff_t offset,loff_t len); void (*show_fdinfo)(struct seq_file *m, struct file *f); #ifndef CONFIG_MMU unsigned (*mmap_capabilities)(struct file *); #endif };owner拥有该结构体的模块的指针,一般设置为 THIS_MODULE。llseek函数用于修改文件当前的读写位置。read:实现用户空间read()调用的逻辑。write:实现用户空间write()调用的逻辑。poll是个轮询函数,用于查询设备是否可以进行非阻塞的读写。unlocked_ioctl函数提供对于设备的控制功能,与应用程序中的ioctl函数对应。compat_ioctl函数与unlocked_ioctl函数功能一样,区别在于在 64 位系统上,32 位的应用程序调用将会使用此函数。在 32 位的系统上运行 32 位的应用程序调用的是unlocked_ioctl。mmap函数用于将设备的内存映射到进程空间中(也就是用户空间),一般帧缓冲设备会使用此函数,比如 LCD 驱动的显存,将帧缓冲(LCD 显存)映射到用户空间中以后应用程序就可以直接操作显存了,这样就不用在用户空间和内核空间之间来回复制。open:当设备文件被打开时调用,常用于初始化。release:当设备文件被关闭时调用,常用于清理资源。ioctl:用于实现设备特定的命令和控制(如设置串口波特率)。fasync函数用于刷新待处理的数据,用于将缓冲区中的数据刷新到磁盘中。aio_fsync函数与fasync函数的功能类似,只是aio_fsync是异步刷新待处理的数据。
3.3 字符设备结构 (struct cdev)
- 内核用这个结构体来在内部表示一个字符设备。
- 它通常与
dev_t(设备号)和file_operations(操作方法)关联在一起。 - 驱动需要分配、初始化并把这个
cdev结构体添加到内核中,这样内核才知道这个设备的存在。
四、字符设备驱动的编写重要关键部分
4.1驱动模块的加载和卸载
Linux 驱动有两种运行方式,第一种就是将驱动编译进 Linux 内核中 ,这样当 Linux 内核启动的时候就会自动运行驱动程序。第二种就是将驱动编译成模块(Linux 下模块扩展名为.ko) ,在Linux 内核启动以后使用"insmod"命令加载驱动模块。在调试驱动的时候一般都选择将其编译为模块,这样我们修改驱动以后只需要编译一下驱动代码即可,不需要编译整个 Linux 代码。
c
module_init(xxx_init); //注册模块加载函数
module_exit(xxx_exit); //注册模块卸载函数
module_init 函数用来向 Linux 内核注册一个模块加载函数,参数 xxx_init 就是需要注册的具体函数,当使用"insmod"命令加载驱动的时候,xxx_init 这个函数就会被调用。module_exit()函数用来向 Linux 内核注册一个模块卸载函数,参数 xxx_exit 就是需要注册的具体函数,当使用"rmmod"命令卸载具体驱动的时候 xxx_exit 函数就会被调用。
4.2字符设备注册与注销
字符设备的注册和注销函数原型如下所示:
c
static inline int register_chrdev(unsigned int major, const char *name,
const struct file_operations *fops)
static inline void unregister_chrdev(unsigned int major, const char *name)
register_chrdev 函数用于注册字符设备,此函数一共有三个参数,这三个参数的含义如下:
major:主设备号,Linux 下每个设备都有一个设备号,设备号分为主设备号和次设备号两部分,关于设备号后面会详细讲解。
name:设备名字,指向一串字符串。
fops:结构体 file_operations 类型指针,指向设备的操作函数集合变量。
unregister_chrdev 函数用户注销字符设备,此函数有两个参数,这两个参数含义如下:
major:要注销的设备对应的主设备号。
name:要注销的设备对应的设备名。
一般字符设备的注册在驱动模块的入口函数 xxx_init 中进行,字符设备的注销在驱动模块的出口函数 xxx_exit 中进行。
c
static struct file_operations test_fops;
/* 驱动入口函数 */
static int __init xxx_init(void) {
/* 入口函数具体内容 */
int retvalue = 0;
/* 注册字符设备驱动 */
retvalue = register_chrdev(200, "chrtest", &test_fops);
if(retvalue < 0){
/* 字符设备注册失败,自行处理 */
}
return 0;
}
/* 驱动出口函数 */
static void __exit xxx_exit(void)
{
/* 注销字符设备驱动 */
unregister_chrdev(200, "chrtest");
}
/* 将上面两个函数指定为驱动的入口和出口函数 */
module_init(xxx_init);
module_exit(xxx_exit);
4.3 实现设备的具体操作函数
前面我们定义了file_operations结构体类型的变量test_fops,但是还没对其进行初始化,也就是初始化其中的open、release、read 和 write 等具体的设备操作函数。接下来对这些函数进行初始化。在初始化 test_fops 之前我们要分析一下需求,也就是要对 chrtest这个设备进行哪些操作,只有确定了需求以后才知道我们应该实现哪些操作函数。假设对 chrtest这个设备有如下两个要求:
1 、能够对 chrtest 进行打开和关闭操作
设备打开和关闭是最基本的要求,几乎所有的设备都得提供打开和关闭的功能。因此我们需要实现 file_operations 中的 open 和 release 这两个函数。
2 、对 chrtest 进行读写操作
假设 chrtest 这个设备控制着一段缓冲区(内存),应用程序需要通过 read 和 write 这两个函数对 chrtest 的缓冲区进行读写操作。所以需要实现 file_operations 中的 read 和 write 这两个函数。
c
/*
* @description : 打开设备
* @param - inode : 传递给驱动的inode
* @param - filp : 设备文件,file结构体有个叫做private_data的成员变量
* 一般在open的时候将private_data指向设备结构体。
* @return : 0 成功;其他 失败
*/
static int chrdevbase_open(struct inode *inode, struct file *filp)
{
//printk("chrdevbase open!\r\n");
return 0;
}
/*
* @description : 从设备读取数据
* @param - filp : 要打开的设备文件(文件描述符)
* @param - buf : 返回给用户空间的数据缓冲区
* @param - cnt : 要读取的数据长度
* @param - offt : 相对于文件首地址的偏移
* @return : 读取的字节数,如果为负值,表示读取失败
*/
static ssize_t chrdevbase_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
int retvalue = 0;
/* 向用户空间发送数据 */
memcpy(readbuf, kerneldata, sizeof(kerneldata));
retvalue = copy_to_user(buf, readbuf, cnt);
if(retvalue == 0){
printk("kernel senddata ok!\r\n");
}else{
printk("kernel senddata failed!\r\n");
}
//printk("chrdevbase read!\r\n");
return 0;
}
/*
* @description : 向设备写数据
* @param - filp : 设备文件,表示打开的文件描述符
* @param - buf : 要写给设备写入的数据
* @param - cnt : 要写入的数据长度
* @param - offt : 相对于文件首地址的偏移
* @return : 写入的字节数,如果为负值,表示写入失败
*/
static ssize_t chrdevbase_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
int retvalue = 0;
/* 接收用户空间传递给内核的数据并且打印出来 */
retvalue = copy_from_user(writebuf, buf, cnt);
if(retvalue == 0){
printk("kernel recevdata:%s\r\n", writebuf);
}else{
printk("kernel recevdata failed!\r\n");
}
//printk("chrdevbase write!\r\n");
return 0;
}
/*
* @description : 关闭/释放设备
* @param - filp : 要关闭的设备文件(文件描述符)
* @return : 0 成功;其他 失败
*/
static int chrdevbase_release(struct inode *inode, struct file *filp)
{
//printk("chrdevbase release!\r\n");
return 0;
}
/*
* 设备操作函数结构体
*/
static struct file_operations chrdevbase_fops = {
.owner = THIS_MODULE,
.open = chrdevbase_open,
.read = chrdevbase_read,
.write = chrdevbase_write,
.release = chrdevbase_release,
};
4.4 Linux设备号
设备号 是 Linux 内核用来唯一标识一个设备的编号。它类似于我们身份证号,内核通过这个号码来找到对应的设备驱动。
它是一个 dev_t 类型的变量(在 <linux/types.h> 中定义),本质上是一个32位的无符号整数。
设备号由两部分组成:
- 主设备号
- 次设备号
它们共同构成了一个完整的设备号。
- 主设备号 :占用高12位。它用来标识设备对应的驱动程序。内核通过主设备号将设备文件与驱动程序关联起来。例如,所有由同一个字符设备驱动管理的设备,它们的主设备号是相同的。
- 次设备号 :占用低20位。它由驱动程序自己使用 ,用来区分由同一个驱动管理的不同设备实例或不同功能。
一个形象的比喻:
- 主设备号 就像是一个公司的总机号码,你拨打这个号码可以找到这家公司。
- 次设备号 就像是公司的分机号码,帮你转接到公司内部的某个具体部门或员工。
Linux 提供了一个名为 dev_t 的数据类型表示设备号,dev_t 定义在文件 include/linux/types.h 里面,定义如下:
c
typedef __u32 __kernel_dev_t;
......
typedef __kernel_dev_t dev_t;
可以看出 dev_t 是__u32 类型的,而__u32 定义在文件 include/uapi/asm-generic/int-ll64.h 里面,定义如下:
c
typedef unsigned int __u32;
综上所述,dev_t 其实就是 unsigned int 类型,是一个 32 位的数据类型。这 32 位的数据构成了主设备号和次设备号两部分,其中高 12 位为主设备号,低 20 位为次设备号。因此 Linux系统中主设备号范围为 0~4095,所以大家在选择主设备号的时候一定不要超过这个范围。在文件 include/linux/kdev_t.h 中提供了几个关于设备号的操作函数(本质是宏),如下所示:
c
#define MINORBITS 20
#define MINORMASK ((1U << MINORBITS) - 1) 8
#define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))
#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))
#define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))
宏 MINORBITS 表示次设备号位数,一共是 20 位。
宏 MINORMASK 表示次设备号掩码。
宏 MAJOR 用于从 dev_t 中获取主设备号,将 dev_t 右移 20 位即可。
宏 MINOR 用于从 dev_t 中获取次设备号,取 dev_t 的低 20 位的值即可。
宏 MKDEV 用于将给定的主设备号和次设备号的值组合成 dev_t 类型的设备号。
4.5 设备号的分配与注册
驱动在使用设备号前,必须向内核"申请"或"注册"。主要有两种方式:
1. 静态分配
开发者自己指定一个明确的主设备号。不推荐,因为容易与系统中已有的设备号冲突。
c
#define MY_MAJOR 250 // 假设我们选250做主设备号
dev_t devno = MKDEV(MY_MAJOR, 0); // 次设备号从0开始
register_chrdev_region(devno, count, "my_device");
2. 动态分配(推荐)
让内核自动分配一个可用的主设备号。这是现代驱动开发的首选方式,可以避免冲突。
c
dev_t devno;
int major;
// 请求内核分配 count 个设备号(从次设备号0开始)
alloc_chrdev_region(&devno, 0, count, "my_device");
// 获取动态分配的主设备号
major = MAJOR(devno);
无论哪种方式,在驱动卸载时,都必须释放设备号:
c
unregister_chrdev_region(devno, count);
4.6 字符设备驱动的编写一般流程
- 确定主次设备号:静态指定或动态向内核申请。
- 实现
file_operations方法 :编写open,read,write等函数的具体代码。在这些函数中,你可能会:- 从硬件寄存器读取数据(
read)。 - 向硬件寄存器写入数据(
write)。 - 配置硬件(
ioctl)。 - 使用
copy_to_user()和copy_from_user()在内核空间和用户空间之间安全地传递数据。
- 从硬件寄存器读取数据(
- 初始化并注册
cdev:- 使用
cdev_init()将cdev与file_operations绑定。 - 使用
cdev_add()将设备注册到内核,使其生效。
- 使用
- 创建设备文件 :在
/dev目录下使用mknod命令或通过驱动代码自动创建(使用device_create)一个设备文件,并将其与你的设备号关联。 - 模块的加载和卸载 :
- 在模块加载函数 (
module_init) 中完成上述的申请设备号、注册cdev等操作。 - 在模块卸载函数 (
module_exit) 中完成相反的清理工作:注销cdev、释放设备号等。
- 在模块加载函数 (
五、一个简单的"虚拟"字符设备驱动示例
chrdevbase 不是实际存在的一个设备,是引入的一个虚拟设备。chrdevbase 设备有两个缓冲区,一个为读缓冲区,一个为写缓冲区,这两个缓冲区的大小都为 100 字节。在应用程序中可以向 chrdevbase 设备的写缓冲区中写入数据,从读缓冲区中读取数据。
应用程序调用 open 函数打开 chrdevbase 这个设备,打开以后可以使用 write 函数向chrdevbase 的写缓冲区 writebuf 中写入数据(不超过 100 个字节),也可以使用 read 函数读取读缓冲区 readbuf 中的数据操作,操作完成以后应用程序使用 close 函数关闭 chrdevbase 设备。
5.1 驱动函数:
c
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#define CHRDEVBASE_MAJOR 200 /* 主设备号 */
#define CHRDEVBASE_NAME "chrdevbase" /* 设备名 */
static char readbuf[100]; /* 读缓冲区 */
static char writebuf[100]; /* 写缓冲区 */
static char kerneldata[] = {"kernel data!"};
/*
* @description : 打开设备
* @param - inode : 传递给驱动的inode
* @param - filp : 设备文件,file结构体有个叫做private_data的成员变量
* 一般在open的时候将private_data指向设备结构体。
* @return : 0 成功;其他 失败
*/
static int chrdevbase_open(struct inode *inode, struct file *filp)
{
//printk("chrdevbase open!\r\n");
return 0;
}
/*
* @description : 从设备读取数据
* @param - filp : 要打开的设备文件(文件描述符)
* @param - buf : 返回给用户空间的数据缓冲区
* @param - cnt : 要读取的数据长度
* @param - offt : 相对于文件首地址的偏移
* @return : 读取的字节数,如果为负值,表示读取失败
*/
static ssize_t chrdevbase_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
int retvalue = 0;
/* 向用户空间发送数据 */
memcpy(readbuf, kerneldata, sizeof(kerneldata));
retvalue = copy_to_user(buf, readbuf, cnt);
if(retvalue == 0){
printk("kernel senddata ok!\r\n");
}else{
printk("kernel senddata failed!\r\n");
}
//printk("chrdevbase read!\r\n");
return 0;
}
/*
* @description : 向设备写数据
* @param - filp : 设备文件,表示打开的文件描述符
* @param - buf : 要写给设备写入的数据
* @param - cnt : 要写入的数据长度
* @param - offt : 相对于文件首地址的偏移
* @return : 写入的字节数,如果为负值,表示写入失败
*/
static ssize_t chrdevbase_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
int retvalue = 0;
/* 接收用户空间传递给内核的数据并且打印出来 */
retvalue = copy_from_user(writebuf, buf, cnt);
if(retvalue == 0){
printk("kernel recevdata:%s\r\n", writebuf);
}else{
printk("kernel recevdata failed!\r\n");
}
//printk("chrdevbase write!\r\n");
return 0;
}
/*
* @description : 关闭/释放设备
* @param - filp : 要关闭的设备文件(文件描述符)
* @return : 0 成功;其他 失败
*/
static int chrdevbase_release(struct inode *inode, struct file *filp)
{
//printk("chrdevbase release!\r\n");
return 0;
}
/*
* 设备操作函数结构体
*/
static struct file_operations chrdevbase_fops = {
.owner = THIS_MODULE,
.open = chrdevbase_open,
.read = chrdevbase_read,
.write = chrdevbase_write,
.release = chrdevbase_release,
};
/*
* @description : 驱动入口函数
* @param : 无
* @return : 0 成功;其他 失败
*/
static int __init chrdevbase_init(void)
{
int retvalue = 0;
/* 注册字符设备驱动 */
retvalue = register_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME, &chrdevbase_fops);
if(retvalue < 0){
printk("chrdevbase driver register failed\r\n");
}
printk("chrdevbase init!\r\n");
return 0;
}
/*
* @description : 驱动出口函数
* @param : 无
* @return : 无
*/
static void __exit chrdevbase_exit(void)
{
/* 注销字符设备驱动 */
unregister_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME);
printk("chrdevbase exit!\r\n");
}
/*
* 将上面两个函数指定为驱动的入口和出口函数
*/
module_init(chrdevbase_init);
module_exit(chrdevbase_exit);
/*
* LICENSE和作者信息
*/
MODULE_LICENSE("GPL");
MODULE_AUTHOR("wzy");
5.2 对应的用户层应用程序:
c
#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"
static char usrdata[] = {"usr data!"};//数组 usrdata 是测试 APP 要向 chrdevbase 设备写入的数据。
/*
* @description : main主程序
* @param - argc : argv数组元素个数
* @param - argv : 具体参数
* @return : 0 成功;其他 失败
*/
int main(int argc, char *argv[])
{
int fd, retvalue;
char *filename;
char readbuf[100], writebuf[100];
if(argc != 3){
printf("Error Usage!\r\n");
return -1;
}
/*判断运行测试 APP 的时候输入的参数是不是为 3 个,main 函数的 argc 参数表示参数数量,argv[]保存着具体的参数,如果参数不为 3 个的话就表示测试 APP 用法错误。比如,现在要从 chrdevbase 设备中读取数据,需要输入如下命令:
./chrdevbaseApp /dev/chrdevbase 1
上述命令一共有三个参数"./chrdevbaseApp"、"/dev/chrdevbase"和"1",这三个参数分别对应 argv[0]、argv[1]和 argv[2]。第一个参数表示运行chrdevbaseAPP 这个软件,第二个参数表示测试APP要打开/dev/chrdevbase这个设备。第三个参数就是要执行的操作,1表示从chrdevbase中读取数据,2 表示向 chrdevbase 写数据。
*/
filename = argv[1];
//获取要打开的设备文件名字,argv[1]保存着设备名字。
/* 打开驱动文件 */
fd = open(filename, O_RDWR);
//调用 C 库中的 open 函数打开设备文件:/dev/chrdevbase。
if(fd < 0){
printf("Can't open file %s\r\n", filename);
return -1;
}
if(atoi(argv[2]) == 1){
/* 从驱动文件读取数据 判断 argv[2]参数的值是 1 还是 2,因为输入命令的时候其参数都是字符串格式的,因此需要借助 atoi 函数将字符串格式的数字转换为真实的数字。*/
retvalue = read(fd, readbuf, 50);
//当 argv[2]为 1 的时候表示要从 chrdevbase 设备中读取数据,一共读取 50 字节的数据,读取到的数据保存在 readbuf 中,读取成功以后就在终端上打印出读取到的数据。
if(retvalue < 0){
printf("read file %s failed!\r\n", filename);
}else{
/* 读取成功,打印出读取成功的数据 */
printf("read data:%s\r\n",readbuf);
}
}
if(atoi(argv[2]) == 2){
/* 向设备驱动写数据 */
memcpy(writebuf, usrdata, sizeof(usrdata));
retvalue = write(fd, writebuf, 50);
//当 argv[2]为 2 的时候表示要向 chrdevbase 设备写数据。
if(retvalue < 0){
printf("write file %s failed!\r\n", filename);
}
}
/* 关闭设备 */
retvalue = close(fd);//对 chrdevbase 设备操作完成以后就关闭设备。
if(retvalue < 0){
printf("Can't close file %s\r\n", filename);
return -1;
}
return 0;
}
5.2.1 文件操作基本函数
1、open函数
open 函数原型如下:
c
int open(const char *pathname, int flags)
open 函数参数含义如下:
pathname:要打开的设备或者文件名。
flags:文件打开模式,以下三种模式必选其一:
O_RDONLY 只读模式
O_WRONLY 只写模式
O_RDWR 读写模式
因为我们要对 chrdevbase 这个设备进行读写操作,所以选择 O_RDWR。除了上述三种模式以外还有其他的可选模式,通过逻辑或来选择多种模式:
O_APPEND 每次写操作都写入文件的末尾
O_CREAT 如果指定文件不存在,则创建这个文件
O_EXCL 如果要创建的文件已存在,则返回 -1,并且修改 errno 的值
O_TRUNC 如果文件存在,并且以只写/读写方式打开,则清空文件全部内容
O_NOCTTY 如果路径名指向终端设备,不要把这个设备用作控制终端。
O_NONBLOCK 如果路径名指向 FIFO/块文件/字符文件,则把文件的打开和后继I/O 设置为非阻塞
DSYNC 等待物理 I/O 结束后再 write。在不影响读取新写入的数据的前提下,不等待文件属性更新。
O_RSYNC read 等待所有写入同一区域的写操作完成后再进行。
O_SYNC 等待物理 I/O 结束后再 write,包括更新文件属性的 I/O。
返回值:如果文件打开成功的话返回文件的文件描述符。
2、read函数
read 函数原型如下:
c
ssize_t read(int fd, void *buf, size_t count)
read 函数参数含义如下:
fd:要读取的文件描述符,读取文件之前要先用 open 函数打开文件,open 函数打开文件成
功以后会得到文件描述符。
buf:数据读取到此 buf 中。
count:要读取的数据长度,也就是字节数。
返回值:读取成功的话返回读取到的字节数;如果返回 0 表示读取到了文件末尾;如果返回负值,表示读取失败。
3、write函数
write 函数原型如下:
c
ssize_t write(int fd, const void *buf, size_t count);
write 函数参数含义如下:
fd:要进行写操作的文件描述符,写文件之前要先用 open 函数打开文件,open 函数打开文
件成功以后会得到文件描述符。
buf:要写入的数据。
count:要写入的数据长度,也就是字节数。
返回值:写入成功的话返回写入的字节数;如果返回 0 表示没有写入任何数据;如果返回负值,表示写入失败。
4、close函数
close 函数原型如下:
c
int close(int fd);
close 函数参数含义如下:
fd:要关闭的文件描述符。
返回值:0 表示关闭成功,负值表示关闭失败。
六、总结
| 特性 | 描述 |
|---|---|
| 本质 | 连接用户程序与字符设备的内核代码桥梁 |
| 数据单位 | 字节流 |
| 访问方式 | 顺序访问 |
| 用户接口 | 通过 /dev/ 下的设备文件,使用标准文件IO函数 (open, read, write, close, ioctl) |
| 核心结构 | file_operations (定义操作行为), cdev (内核内部表示), dev_t (设备标识) |
| 应用场景 | 键盘、鼠标、串口、打印机、各种传感器等非存储类设备 |