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 (设备标识)
应用场景 键盘、鼠标、串口、打印机、各种传感器等非存储类设备
相关推荐
LCG元2 小时前
Linux 下的端口转发:ssh、socat、iptables 三种方案对比
linux
LCG元2 小时前
深入理解 Linux 网络命名空间:自己动手实现"虚拟网络"
linux
powerfulhell3 小时前
11.11作业
linux·运维·centos
板鸭〈小号〉3 小时前
进程间关系(linux)
linux·运维·服务器
liu****3 小时前
18.HTTP协议(一)
linux·网络·网络协议·http·udp·1024程序员节
脏脏a4 小时前
【Linux】冯诺依曼体系结构与操作系统概述
linux·服务器
恪愚4 小时前
webRTC:流程和socket搭建信令服务器
运维·服务器·webrtc
adnyting4 小时前
【Linux日新月异(三)】CentOS 7软件管理深度指南:从YUM到源码编译
linux·运维·centos
知识分享小能手5 小时前
openEuler入门学习教程,从入门到精通,云计算与 Linux 操作系统概述(1)
linux·云计算·openeuler