Linux 字符设备驱动

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;
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

起风就扬帆

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值