Linux字符设备驱动开发详细教程(简单字符设备驱动框架)

文章目录

字符设备驱动开发

一、字符设备驱动简介

在理解字符设备驱动之前,首先要明白什么是字符设备。Linux/Unix 系统将硬件设备分为两大类:

  1. 字符设备

    • 特点 :以字节流 的形式进行数据传输。数据像水流一样,没有固定的块大小,只能顺序访问
    • 举例 :键盘、鼠标、串口、控制台 (/dev/console)、声卡等。我们最常见的例子就是终端,你输入的字符会一个接一个地传递给系统。
  2. 块设备

    • 特点 :以数据块 为单位进行数据传输(如 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)
  • 这是字符设备驱动的灵魂 ,是一个函数指针的集合

  • 驱动开发者通过填充这个结构体,告诉内核:"当用户对我的设备文件进行某种操作时,请调用我写的这个函数。"

  • 关键成员包括:

    c 复制代码
    struct 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位的无符号整数。

设备号由两部分组成:

  1. 主设备号
  2. 次设备号

它们共同构成了一个完整的设备号。

  • 主设备号 :占用高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 字符设备驱动的编写一般流程

  1. 确定主次设备号:静态指定或动态向内核申请。
  2. 实现 file_operations 方法 :编写 open, read, write 等函数的具体代码。在这些函数中,你可能会:
    • 从硬件寄存器读取数据(read)。
    • 向硬件寄存器写入数据(write)。
    • 配置硬件(ioctl)。
    • 使用 copy_to_user()copy_from_user() 在内核空间和用户空间之间安全地传递数据。
  3. 初始化并注册 cdev
    • 使用 cdev_init()cdevfile_operations 绑定。
    • 使用 cdev_add() 将设备注册到内核,使其生效。
  4. 创建设备文件 :在 /dev 目录下使用 mknod 命令或通过驱动代码自动创建(使用 device_create)一个设备文件,并将其与你的设备号关联。
  5. 模块的加载和卸载
    • 在模块加载函数 (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 (设备标识)
应用场景 键盘、鼠标、串口、打印机、各种传感器等非存储类设备
相关推荐
2301_810746312 分钟前
CKA冲刺40天笔记 - day20-day21 SSL/TLS详解
运维·笔记·网络协议·kubernetes·ssl
❀͜͡傀儡师7 分钟前
docker 部署 komari-monitor监控
运维·docker·容器·komari
物联网软硬件开发-轨物科技33 分钟前
【轨物方案】软硬件一体赋能,开启矿山机械远程智慧运维新篇章
运维
月熊35 分钟前
在root无法通过登录界面进去时,通过原本的普通用户qiujian如何把它修改为自己指定的用户名
linux·运维·服务器
大江东去浪淘尽千古风流人物1 小时前
【DSP】向量化操作的误差来源分析及其经典解决方案
linux·运维·人工智能·算法·vr·dsp开发·mr
打码人的日常分享2 小时前
智慧城市一网统管建设方案,新型城市整体建设方案(PPT)
大数据·运维·服务器·人工智能·信息可视化·智慧城市
赖small强2 小时前
【Linux驱动开发】NOR Flash 技术原理与 Linux 系统应用全解析
linux·驱动开发·nor flash·芯片内执行
风掣长空3 小时前
Google Test (gtest) 新手完全指南:从入门到精通
运维·服务器·网络
luback3 小时前
前端对Docker简单了解
运维·docker·容器
0思必得03 小时前
[Web自动化] 开发者工具应用(Application)面板
运维·前端·python·自动化·web自动化·开发者工具