struct file在C语言中究竟如何使用?

99ANYc3cd6
预计阅读时长 33 分钟
位置: 首页 C语言 正文

struct file 并不是一个标准的 C 语言库结构体,它不是一个像 struct tm (来自 <time.h>) 或 struct stat (来自 <sys/stat.h>) 那样你可以直接在自己的用户空间 C 程序中使用的结构体。

c语言struct file
(图片来源网络,侵删)

struct fileLinux 内核 中的一个核心数据结构,它代表了内核中的一个打开的文件(或更准确地说是“打开的文件描述符”的内核表示),如果你想在用户空间编程,你几乎不会直接操作 struct file,理解它对于进行 Linux 内核编程(尤其是驱动程序开发)至关重要。

下面我将从以下几个方面来解释 struct file

  1. 它是什么?—— 文件描述符的内核视角
  2. 它的核心成员有哪些?—— 解析结构体
  3. 它在内核中是如何使用的?—— 生命周期
  4. 为什么它重要?—— 核心作用
  5. 用户空间 vs. 内核空间
  6. 一个简单的内核模块示例

它是什么?—— 文件描述符的内核视角

在用户空间,当你调用 open() 函数时,它会返回一个小的非负整数,这就是文件描述符,你可以把这个文件描述符想象成一个“句柄”或“凭证”,它代表了你的进程与一个打开文件之间的连接。

在内核层面,这个简单的整数背后关联着一系列复杂的数据结构来管理文件 I/O,其中最重要的就是 struct file

c语言struct file
(图片来源网络,侵删)

可以把它想象成一个“打开文件的动态信息记录卡”,这张卡片记录了:

  • 当前读写位置 (f_pos)
  • 访问模式 (只读、只写、读写)
  • 指向文件 inode 的指针 (f_inode),inode 包含了文件的静态元数据(大小、所有者、权限等)。
  • 指向文件操作表的指针 (f_op),这是最重要的部分之一,它定义了可以对这个文件执行的所有操作(读、写、 seek、 ioctl 等)。
  • 文件私有数据 (private_data),驱动程序可以用它来存储自己的状态信息。

关键关系:

进程 -> 文件描述符 (整数) -> struct file (内核对象) -> struct inode (文件静态信息) -> 文件系统 -> 块设备 (如硬盘)

一个进程可以有多个打开的文件,因此一个进程可以有多个文件描述符,多个进程也可以打开同一个文件,这意味着内核中会存在多个 struct file 对象,但它们可能共享同一个 struct inode 对象。

c语言struct file
(图片来源网络,侵删)

它的核心成员有哪些?—— 解析结构体

struct file 定义在 Linux 内核的 <linux/fs.h> 头文件中,它的成员非常多,因为内核版本不同也会有变化,以下是其中一些最核心和最常用的成员:

struct file {
    /*
     * ... (其他成员)
     */
    unsigned int f_flags;          // 文件打开时设置的标志,如 O_RDONLY, O_RDWR, O_NONBLOCK 等
    fmode_t f_mode;                // 读/写模式,如 FMODE_READ, FMODE_WRITE
    loff_t f_pos;                  // 当前读写位置,loff_t 是 64 位的,支持大文件
    struct file_operations *f_op;  // 指向文件操作结构体的指针,这是驱动的核心!
    void *private_data;            // 驱动程序可以使用的私有数据指针
    struct inode *f_inode;         // 指向该文件对应的 inode 对象
    struct dentry *f_path.dentry;  // 指向目录项对象,包含路径信息
    /*
     * ... (其他成员,如 f_count 引用计数等)
     */
};

成员详解:

  • f_flags: 保存了 open() 系统调用时传入的标志,如 O_APPEND (追加写)、O_NONBLOCK (非阻塞) 等,驱动程序可以检查这些标志来改变行为。
  • f_mode: 标志文件是以读模式还是写模式打开的。f_mode & FMODE_WRITE 为真表示可写。
  • f_pos: 当前文件偏移量,当你调用 read()write() 时,数据就从/向这个位置开始操作,操作完成后,内核会自动更新这个值,对于普通文件,它指向文件中的字节位置,对于设备,其含义由驱动程序自己定义。
  • f_op: 这是连接驱动程序和 VFS (虚拟文件系统) 的桥梁,它是一个指向 struct file_operations 结构体的指针。struct file_operations 包含了一系列函数指针,如 read, write, llseek, open, release, ioctl 等,当用户空间调用 read(fd, ...) 时,VFS 会找到这个 struct file 对象,然后通过它的 f_op 指针找到对应的 read 函数并执行它。编写驱动程序,主要就是填充这个结构体。
  • private_data: 这是一个非常实用的通用指针,驱动程序的 open 函数通常会在这里分配内存或存储设备号、设备结构体指针等,然后在 read, write, release 等函数中通过这个指针来访问和修改自己的状态。
  • f_inode: 指向文件的 inode 对象。inode 存储了文件的静态属性,如文件大小、所有者、组、权限、时间戳等。struct file 是动态的(读写位置会变),而 struct inode 是相对静态的。

它在内核中是如何使用的?—— 生命周期

struct file 对象的生命周期是动态的,与文件描述符的打开和关闭紧密相关:

  1. 创建: 当一个进程成功调用 open() 系统时,内核会为这次打开创建一个新的 struct file 对象。
  2. 使用: 这个新创建的 struct file 对象会被关联到进程的文件描述符表中,之后,所有针对该文件描述符的 I/O 操作(read, write, lseek 等)都会通过这个 struct file 对象进行。
  3. 销毁: 当进程调用 close() 关闭文件描述符时,内核会减少 struct file 对象的引用计数,当引用计数降为 0 时,内核会释放这个 struct file 对象占用的内存,通常在销毁前,会调用 f_op->release() 函数,让驱动程序进行清理工作(如释放 private_data 中的内存)。

为什么它重要?—— 核心作用

struct file 是 Linux 内核设计哲学的体现,它实现了设备驱动与 VFS 的解耦

  • 统一接口: VFS (Virtual File System) 提供了一套统一的文件操作接口给用户空间,无论是操作一个普通文本文件、一个块设备(如 /dev/sda)还是一个网络套接字,用户程序调用的 read(), write() 函数都是一样的。
  • 动态绑定: struct file 中的 f_op 指针是“动态”的,VFS 在处理 I/O 请求时,并不关心具体是哪种设备,它只需要通过 struct file 找到对应的 file_operations 结构体,然后调用里面的函数即可。
  • 驱动程序实现: 作为驱动程序开发者,你的工作就是:
    1. 定义一个 struct file_operations 结构体,并实现其中的 read, write 等函数。
    2. 当设备被打开时(open 系统调用),内核会创建一个 struct file 对象,并将你定义的 file_operations 结构体的地址赋值给 f_op 字段。
    3. 之后,所有针对该设备的 I/O 请求,都会被 VFS 转发到你实现的函数中。

这种设计使得 Linux 的文件系统非常灵活和可扩展。


用户空间 vs. 内核空间

特性 用户空间 内核空间
代码位置 你的 C 程序(如 main() 函数) Linux 内核模块(如 init_module() 函数)
访问对象 使用文件描述符 (整数) 直接操作 struct file, struct inode 等内核对象
库支持 可以使用标准 C 库 (stdio.h, fcntl.h 等) 使用内核头文件 (<linux/fs.h>, <linux/module.h> 等)
权限 受限,不能直接访问硬件 最高权限,可以访问所有内存和硬件
函数调用 调用 open(), read(), write()库函数,这些函数最终会触发系统调用进入内核 直接调用内核提供的函数,如 copy_from_user(), copy_to_user()
struct file 不可见,你只能拿到一个整数文件描述符 核心数据结构,直接操作

一个简单的内核模块示例

这个示例创建一个虚拟设备 /dev/mychardev,当向它写入数据时,它会将数据回显(类似 echo),并打印内核日志。

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/slab.h> // for kmalloc/kfree
#include <linux/uaccess.h> // for copy_from_user, copy_to_user
#define DEVICE_NAME "mychardev"
#define CLASS_NAME  "mychardev_class"
// 定义我们的文件操作结构体
static struct file_operations fops = {
    .owner = THIS_MODULE,
    // .read 和 .write 是必须实现的
    .read =  my_device_read,
    .write = my_device_write,
    // 其他操作可以暂时为 NULL
    .open =  my_device_open,
    .release = my_device_close,
};
// 设备号 (主设备号和次设备号)
static dev_t dev_num;
static struct class *my_class;
static struct cdev my_cdev;
// private_data 可以指向任何东西,这里我们用一个简单的字符串来存储
static char *kernel_buffer;
static int buffer_size = 1024;
// 打开设备时的回调函数
static int my_device_open(struct inode *inode, struct file *file) {
    printk(KERN_INFO "mychardev: Device opened.\n");
    // 将我们的内核缓冲区指针存入 private_data
    file->private_data = kernel_buffer;
    return 0;
}
// 关闭设备时的回调函数
static int my_device_close(struct inode *inode, struct file *file) {
    printk(KERN_INFO "mychardev: Device closed.\n");
    return 0;
}
// 从设备读取数据的回调函数
static ssize_t my_device_read(struct file *file, char __user *user_buffer, size_t len, loff_t *offset) {
    printk(KERN_INFO "mychardev: Read operation called.\n");
    // 示例:这里我们只是简单返回,不实现具体读取逻辑
    return 0;
}
// 向设备写入数据的回调函数
static ssize_t my_device_write(struct file *file, const char __user *user_buffer, size_t len, loff_t *offset) {
    char *data = (char *)file->private_data; // 从 private_data 获取我们的缓冲区
    int bytes_to_copy = len;
    if (bytes_to_copy > buffer_size)
        bytes_to_copy = buffer_size;
    // 将用户空间的数据拷贝到内核空间
    if (copy_from_user(data, user_buffer, bytes_to_copy) != 0) {
        printk(KERN_ERR "mychardev: Failed to copy data from user.\n");
        return -EFAULT;
    }
    printk(KERN_INFO "mychardev: Received %zu bytes from user: %s\n", len, data);
    return bytes_to_copy; // 返回实际写入的字节数
}
// 模块初始化函数
static int __init mychardev_init(void) {
    // 1. 分配设备号
    if (alloc_chrdev_region(&dev_num, 0, 1, DEVICE_NAME) < 0) {
        printk(KERN_ERR "mychardev: Failed to allocate device number.\n");
        return -1;
    }
    printk(KERN_INFO "mychardev: Device number allocated: %d, %d\n", MAJOR(dev_num), MINOR(dev_num));
    // 2. 创建设备类
    my_class = class_create(THIS_MODULE, CLASS_NAME);
    if (IS_ERR(my_class)) {
        unregister_chrdev_region(dev_num, 1);
        printk(KERN_ERR "mychardev: Failed to create device class.\n");
        return PTR_ERR(my_class);
    }
    // 3. 创建字符设备文件
    if (IS_ERR(device_create(my_class, NULL, dev_num, NULL, DEVICE_NAME))) {
        class_destroy(my_class);
        unregister_chrdev_region(dev_num, 1);
        printk(KERN_ERR "mychardev: Failed to create device file.\n");
        return PTR_ERR(device_create(my_class, NULL, dev_num, NULL, DEVICE_NAME));
    }
    // 4. 初始化 cdev 结构体并添加到内核
    cdev_init(&my_cdev, &fops);
    my_cdev.owner = THIS_MODULE;
    if (cdev_add(&my_cdev, dev_num, 1) < 0) {
        device_destroy(my_class, dev_num);
        class_destroy(my_class);
        unregister_chrdev_region(dev_num, 1);
        printk(KERN_ERR "mychardev: Failed to add cdev.\n");
        return -1;
    }
    // 5. 分配 private_data 使用的内存
    kernel_buffer = kmalloc(buffer_size, GFP_KERNEL);
    if (!kernel_buffer) {
        // ... 清理代码 ...
        printk(KERN_ERR "mychardev: Failed to allocate kernel buffer.\n");
        return -1;
    }
    memset(kernel_buffer, 0, buffer_size);
    printk(KERN_INFO "mychardev: Module loaded successfully.\n");
    return 0;
}
// 模块退出函数
static void __exit mychardev_exit(void) {
    // 1. 释放 cdev
    cdev_del(&my_cdev);
    // 2. 销毁设备文件和类
    device_destroy(my_class, dev_num);
    class_destroy(my_class);
    // 3. 释放设备号
    unregister_chrdev_region(dev_num, 1);
    // 4. 释放 private_data 的内存
    kfree(kernel_buffer);
    printk(KERN_INFO "mychardev: Module unloaded.\n");
}
module_init(mychardev_init);
module_exit(mychardev_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple character device driver example.");

在这个例子中,你可以看到:

  • fops 结构体将我们自定义的 my_device_readmy_device_write 函数与 VFS 连接起来。
  • my_device_open 中,我们将一个内核缓冲区的地址赋值给了 file->private_data
  • my_device_write 中,我们通过 file->private_data 获取到了这个缓冲区,并使用 copy_from_user 将用户空间的数据安全地拷贝进来,这就是 struct file 在驱动程序中的典型用法。

struct file 是 Linux 内核管理打开文件的核心数据结构,它是一个动态的、与特定文件描述符相关的对象,封装了文件的当前状态和操作方法,对于内核开发者,尤其是驱动开发者来说,理解并熟练使用 struct file 及其相关的 file_operations 是一项必备技能,它使得复杂的硬件操作能够通过统一的文件 I/O 接口呈现给用户空间,极大地简化了应用程序的开发。

-- 展开阅读全文 --
头像
dede小游戏php网站源码如何获取?
« 上一篇 01-24
织梦栏目页文章路径不对
下一篇 » 01-24

相关文章

取消
微信二维码
支付宝二维码

目录[+]