Linux 字符设备驱动
文章目录
1、概念与框架
1.1、字符设备
Linux内核中有那么多驱动程序,应用层怎么才能精确的调用到底层的驱动程序?
- 1.在Linux文件系统中,每个文件都用一个struct inode结构体来描述,这个结构体里面记录了这个文件的所有信息,例如:文件类型,访问权限等。
- 2.在Linux操作系统中,每个驱动程序在应用层的/dev目录下都会有一个设备文件和它对应,并且该文件会有对应的主设备号和次设备号。
- 3.在Linux操作系统中,每个驱动程序都要分配一个主设备号,字符设备的设备号保存在struct cdev结构体中。
设备通过以流的方式向用户程序传递数据。字符设备驱动通过/dev目录下的特殊文件公开设备属性和功能。字符设备在内核中表示为 struct cdev的实例,定义在include/linux/cdev.h中:
struct cdev {
struct kobject kobj;
struct module *owner;
const struct file_operations *ops;//接口函数集合
struct list_head list;//内核链表
dev_t dev; //设备号
unsigned int count;//次设备号个数
};
- 4.在Linux操作系统中,每打开一次文件,Linux操作系统在VFS层都会分配一个struct file结构体来描述打开的这个文件。该结构体用于维护文件打开权限、文件指针偏移值、私有内存地址等信息。
这里顺便提一下linux中的其他两种设备,
- 块设备:和字符设备类似,块设备也是通过/dev目录下的文件系统节点来访问。块设备(例如磁盘)上能够容纳filesystem。在大多数的Unix系统中,进行I/O操作时块设备每次只能传输一个或多个完整的块,而每块包含512字节(或2的更高次幂字节的数据)。Linux可以让app像字符设备一样地读写块设备,允许一次传递任意多字节的数据
- 网络设备:网络设备却围绕数据包的传送和接收而设计。网络驱动程序不需要知道各个连接的相关信息,它只要处理数据包即可。
由于不是面向流的设备,因此将网络接口映射到filesystem中的节点(比如/dev/tty1)比较困难。Unix访问网络接口的方法仍然是给它们分配一个唯一的名字(比如eth0),但这个名字在filesystem中不存在对应的节点。
内核和网络设备驱动程序间的通信,完全不同于内核和字符以及块驱动程序之间的通信,内核调用一套和数据包相关的函数socket,也叫套接字。
1.2、主设备与次设备
设备注册时,必须注册主设备和次设备号,主设备标这个设备,次设备用作本地设备列表检索。
设备号的分配与释放,略。
1.3、设备文件操作
在Linux内核模块中,设备文件操作是通过实现一组特定的文件操作接口(file operations)来实现的。这些接口定义在 struct file_operations 结构中,并由内核调用来响应用户空间对设备文件的操作。
struct file_operations {
struct module *owner;//拥有该结构的模块的指针,一般为THIS_MODULES
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 (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);//初始化一个异步的读取操作
ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);//初始化一个异步的写入操作
int (*readdir) (struct file *, void *, filldir_t);//仅用于读取目录,对于设备文件,该字段为NULL
unsigned int (*poll) (struct file *, struct poll_table_struct *); //轮询函数,判断目前是否可以进行非阻塞的读写或写入
int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long); //执行设备I/O控制命令
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long); //不使用BLK文件系统,将使用此种函数指针代替ioctl
long (*compat_ioctl) (struct file *, unsigned int, unsigned long); //在64位系统上,32位的ioctl调用将使用此函数指针代替
int (*mmap) (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 *, struct dentry *, int datasync); //刷新待处理的数据
int (*aio_fsync) (struct kiocb *, int datasync); //异步刷新待处理的数据
int (*fasync) (int, struct file *, int); //通知设备FASYNC标志发生变化
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 **);
};
1.4、内核中的文件表示
- struct inode是文件系统的数据结构,它只与操作系统相关,用于保存文件或目录,是其他文件的信息。
struct inode {
umode_t i_mode; // 文件类型和访问权限
kuid_t i_uid; // 文件拥有者的用户 ID
kgid_t i_gid; // 文件拥有者的组 ID
struct inode_operations *i_op; // inode 操作函数集合
const struct file_operations *i_fop; // 文件操作函数集合
struct super_block *i_sb; // inode 所属的超级块
struct address_space *i_mapping; // 文件的地址空间
unsigned long i_ino; // inode 号
atomic_t i_count; // inode 引用计数
dev_t i_rdev; // 如果是字符或块设备文件,表示设备号
loff_t i_size; // 文件大小
struct timespec i_atime; // 最后访问时间
struct timespec i_mtime; // 最后修改时间
struct timespec i_ctime; // inode 改变时间
// ... 其他成员 ...
};
- struct file结构是更高级的文件描述,它代表内核打开的文件,依赖与底层的 struct inode。
struct file {
// 文件状态和控制信息
loff_t f_pos; // 当前文件位置
struct file_operations *f_op; // 文件操作函数集合
// 文件描述符管理
const struct file_operations *f_orig_fops; // 指向原始文件操作函数集合
struct path f_path; // 文件的路径信息
// 文件访问模式和标志
unsigned int f_flags; // 文件访问模式标志
// 其他成员
spinlock_t f_lock; // 文件锁,用于同步
atomic_t f_count; // 引用计数,用于追踪打开文件的引用次数
unsigned int f_mode; // 文件模式
unsigned int f_version; // 文件版本号
void *private_data; // 文件私有数据指针
// ... 其他成员 ...
};
1.6、分配与注册字符设备
套框架,略
1.7、附录
2、写文件操作
2.1、kmd和umd数据交换
略
2.2、open方法
open 方法是字符设备驱动中的一个文件操作函数,用于处理用户空间程序通过系统调用 open 打开设备文件时的操作。在字符设备驱动中,open 方法通常会执行一些初始化或设置操作,并可能会在需要的情况下进行资源分配。
以下是一个关于 open 方法的基本讲解:
int mychardev_open(struct inode* inodep, struct file* filep) {
// 在设备被打开时执行的操作
// inodep: inode 结构指针,包含有关文件的元数据信息
// filep: file 结构指针,用于表示已打开文件的实例
// 1. 执行初始化或资源分配操作
// 2. 检查设备是否已经打开(可选)
// 3. 更新设备状态信息(可选)
printk(KERN_INFO "mychardev: Device has been opened.\n");
// 返回 0 表示成功打开设备,其他值表示错误
return 0;
}
2.3、release方法
release 方法是字符设备驱动中的一个文件操作函数,用于处理用户空间程序通过系统调用 close 关闭设备文件时的操作。在字符设备驱动中,release 方法通常用于执行资源释放、清理和关闭设备等操作。
以下是一个关于 release 方法的基本讲解:
int mychardev_release(struct inode* inodep, struct file* filep) {
// 在设备被关闭时执行的操作
// inodep: inode 结构指针,包含有关文件的元数据信息
// filep: file 结构指针,用于表示已打开文件的实例
// 1. 执行资源释放或清理操作
// 2. 更新设备状态信息(可选)
printk(KERN_INFO "mychardev: Device has been closed.\n");
// 返回 0 表示成功关闭设备,其他值表示错误
return 0;
}
2.4、write方法
write 方法是字符设备驱动中的一个文件操作函数,用于处理用户空间程序通过系统调用 write 向设备文件写入数据时的操作。在字符设备驱动中,write 方法通常用于接收用户空间传递的数据,并进行相应的处理,可能包括将数据写入设备硬件、缓存或内核数据结构中。
以下是一个关于 write 方法的基本讲解:
ssize_t mychardev_write(struct file* filep, const char __user *buffer, size_t len, loff_t* offset) {
// 在设备文件写入数据时执行的操作
// filep: file 结构指针,用于表示已打开文件的实例
// buffer: 用户空间传递的数据缓冲区
// len: 要写入的数据长度
// offset: 写入位置的偏移量
// 1. 从用户空间复制数据到内核空间
// 2. 执行相应的设备操作,将数据写入设备硬件或缓存
// 3. 更新设备状态信息(可选)
printk(KERN_INFO "mychardev: Writing to device.\n");
// 返回写入的字节数,负值表示错误
return len;
}
2.5、read方法
read 方法是字符设备驱动中的一个文件操作函数,用于处理用户空间程序通过系统调用 read 从设备文件读取数据时的操作。在字符设备驱动中,read 方法通常用于从设备硬件、缓存或内核数据结构中读取数据,并将数据传递到用户空间。
以下是一个关于 read 方法的基本讲解:
ssize_t mychardev_read(struct file* filep, char __user *buffer, size_t len, loff_t* offset) {
// 在设备文件读取数据时执行的操作
// filep: file 结构指针,用于表示已打开文件的实例
// buffer: 用户空间传递的数据缓冲区
// len: 要读取的数据长度
// offset: 读取位置的偏移量
// 1. 从设备硬件、缓存或内核数据结构中读取数据
// 2. 将数据传递到用户空间
// 3. 更新设备状态信息(可选)
// 示例:向用户空间传递一个简单的字符串
const char *data = "Hello, this is from mychardev!\n";
size_t dataSize = strlen(data);
// 从设备硬件或缓存中读取数据
// 这里的例子只是向用户空间传递一个固定的字符串
// 在实际的设备驱动中,可能需要从设备硬件读取真实的数据
// 并将数据存储在 kernelBuffer 中
char *kernelBuffer = kmalloc(dataSize, GFP_KERNEL);
if (!kernelBuffer) {
printk(KERN_ALERT "mychardev: Failed to allocate kernel buffer.\n");
return -ENOMEM;
}
strcpy(kernelBuffer, data);
// 将数据传递到用户空间
if (copy_to_user(buffer, kernelBuffer, dataSize)) {
kfree(kernelBuffer);
printk(KERN_ALERT "mychardev: Failed to copy data to user space.\n");
return -EFAULT; // 复制失败
}
// 释放内核缓冲区
kfree(kernelBuffer);
// 返回成功读取的字节数
return dataSize;
}
2.6、llseek方法
llseek 方法是字符设备驱动中的一个文件操作函数,用于处理用户空间程序通过系统调用 lseek 对设备文件的文件位置进行定位时的操作。lseek 主要用于改变文件的读写位置,从而实现随机访问文件的功能。
以下是一个关于 llseek 方法的基本讲解:
loff_t mychardev_llseek(struct file* filep, loff_t offset, int whence) {
// 在设备文件进行定位时执行的操作
// filep: file 结构指针,用于表示已打开文件的实例
// offset: 定位的偏移量
// whence: 定位方式
// 1. 根据定位方式和偏移量计算新的文件位置
// 2. 更新文件位置信息
// 3. 返回新的文件位置
// 示例:只支持文件位置的前进定位
loff_t newPosition;
// 根据定位方式和偏移量计算新的文件位置
switch (whence) {
case SEEK_SET:
newPosition = offset;
break;
case SEEK_CUR:
newPosition = filep->f_pos + offset;
break;
case SEEK_END:
// 示例中不支持从文件末尾定位
printk(KERN_ALERT "mychardev: SEEK_END not supported.\n");
return -EINVAL;
default:
// 未知的定位方式
printk(KERN_ALERT "mychardev: Unknown seek mode.\n");
return -EINVAL;
}
// 更新文件位置信息
filep->f_pos = newPosition;
// 返回新的文件位置
return newPosition;
}
2.7、poll方法
poll 方法是字符设备驱动中的一个文件操作函数,用于处理用户空间程序通过系统调用 poll 或 select 进行轮询操作时的操作。poll 主要用于检查设备上是否发生某些事件,如可读、可写等,以便进行相应的处理。
以下是一个关于 poll 方法的基本讲解:
#include <linux/fs.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/cdev.h>
#include <linux/poll.h>
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
#define DEVICE_NAME "mychardev"
#define CLASS_NAME "mycharclass"
static int majorNumber;
static struct class* charClass = NULL;
static struct device* charDevice = NULL;
struct my_data {
// 自定义的文件相关数据结构
int fd_type; // 用于标识文件类型等信息
// ... 可以添加其他信息
};
static unsigned int mychardev_poll(struct file* filep, struct poll_table_struct* wait) {
unsigned int mask = 0;
// 从 file 结构中获取自定义数据结构
struct my_data *mydata = filep->private_data;
// 示例:假设设备总是可写的
if (mydata->fd_type == 1) {
mask |= POLLOUT | POLLWRNORM;
}
// 将等待队列添加到 poll_table 中
poll_wait(filep, wait, wait_queue_head_t);
return mask;
}
static struct file_operations fops = {
// Other file operations (open, read, write, release, etc.) can be added here
.poll = mychardev_poll,
};
static int __init mychardev_init(void) {
// ... (省略初始化代码)
// 注册字符设备驱动
majorNumber = register_chrdev(0, DEVICE_NAME, &fops);
// ... (省略后续注册和创建设备的代码)
return 0;
}
static void __exit mychardev_exit(void) {
// ... (省略清理和注销的代码)
}
module_init(mychardev_init);
module_exit(mychardev_exit);
在poll方法中,struct file* filep 参数表示打开文件的实例,它是内核中的数据结构,用于表示打开的文件。这个参数是内核在调用poll方法时传递给该方法的。
2.8、ioctl方法
ioctl 方法是字符设备驱动中的一个文件操作函数,用于处理用户空间程序通过系统调用 ioctl 发起设备控制命令时的操作。ioctl 允许用户空间程序向设备发出各种命令,以实现设备的特定控制、配置和查询等功能。
以下是一个关于 ioctl 方法的基本讲解:
long mychardev_ioctl(struct file *filep, unsigned int cmd, unsigned long arg) {
// 在进行 ioctl 操作时执行的操作
// filep: file 结构指针,用于表示已打开文件的实例
// cmd: ioctl 命令码,用于标识用户空间程序发起的具体命令
// arg: 用户空间传递的参数
// 1. 根据 ioctl 命令码执行相应的操作
// 2. 返回执行结果
// 示例:假设支持一个自定义的 IOCTL 命令
switch (cmd) {
case MY_IOCTL_COMMAND:
// 执行自定义的 IOCTL 命令
// arg 可以包含用户空间传递的参数
printk(KERN_INFO "mychardev: Received custom IOCTL command.\n");
// 执行相应的操作
break;
// 可以添加其他命令的处理逻辑
default:
// 未知的 IOCTL 命令
printk(KERN_ALERT "mychardev: Unknown IOCTL command.\n");
return -EINVAL;
}
// 返回执行结果
return 0;
}