【linux仓库】万物至简的设计典范:如何用‘文件’这一个概念操纵整个Linux世界?

发布于:2025-09-03 ⋅ 阅读:(24) ⋅ 点赞:(0)

🌟 各位看官好,我是egoist2023

🌍 Linux == Linux is not Unix !

🚀 今天来学习一切皆文件的相关知识。

👍 如果觉得这篇文章有帮助,欢迎您一键三连,分享给更多人哦!

目录

书接上文

前言 

一切皆文件

刨根问底

总结


书接上文

本文深入剖析了Linux文件描述符(FD)的核心机制。我们从open系统调用的返回值切入,揭示了其“数组下标”的本质特性。通过追溯进程task_struct中的files_struct结构,我们阐明了0、1、2号FD被固定分配为标准输入、输出、错误的规则,并验证了C库FILE结构体对FD的封装关系。

在此基础上,我们系统讲解了FD的分配规则重定向的底层实现(基于dup2系统调用),以及父子进程间FD的继承机制

然而,这一切精巧的设计,都服务于一个更为宏大和深刻的Linux设计哲学——“一切皆文件”

FD,正是这一哲学得以实现的基石与枢纽。它不仅仅是一个用于访问普通文件的句柄,更是一个统一的抽象接口。正是凭借FD这个“万能手柄”,Linux才能将形态各异的外部设备——无论是磁盘、键盘、显示器,还是网络套接字、管道——都抽象为一种可以统一进行读写(read/write)操作的对象。

接下来,我们将超越普通文件的范畴,深入虚拟文件系统(VFS) 层,探索Linux是如何用“文件”这同一个概念,来统一抽象万物的。我们将看到,正是FD机制的存在,才使得“一切皆文件”从一句口号,变成了一个强大而优雅的现实。

前言 

OS要不要管理硬件呢?它是软硬件资源的管理者,要管理,那么该如何管理呢?

先描述,再组织!!!

struct device
{
    int type;
    int status;
    ...
    struct list_head node;
}

一切皆文件

首先,在windows中是⽂件的东西,它们在linux中也是⽂件;其次⼀些在windows中不是⽂件的东

西,⽐如进程、磁盘、显⽰器、键盘这样硬件设备也被抽象成了⽂件,你可以使⽤访问⽂件的⽅法访问它们获得信息;甚⾄管道,也是⽂件;将来我们要学习⽹络编程中的socket(套接字)这样的东西,使⽤的接⼝跟⽂件接⼝也是⼀致的。

这样做最明显的好处是,开发者仅需要使⽤⼀套 API 和开发⼯具,即可调取 Linux 系统中绝⼤部分的资源。举个简单的例⼦,Linux 中⼏乎所有读(读⽂件,读系统状态,读PIPE)的操作都可以⽤

read 函数来进行;几乎所有更改(更改⽂件,更改系统参数,写 PIPE)的操作都可以⽤ write 函

数来进⾏。

之前我们讲过,当打开⼀个⽂件时,操作系统为了管理所打开的⽂件,都会为这个⽂件创建⼀个file结构体,该结构体定义在 /usr/src/kernels/3.10.0-1160.71.1.el7.x86_64/include/linux/fs.h 下,以下展示了该结构部分我们关系的内容:

struct file {
    ...
    struct inode *f_inode; /* cached value */
    const struct file_operations *f_op;
    ...
    atomic_long_t f_count;  // 表⽰打开⽂件的引⽤计数,如果有多个⽂件指针指向
                               它,就会增加f_count的值。

    unsigned int f_flags;   // 表⽰打开⽂件的权限

    fmode_t f_mode;         // 设置对⽂件的访问模式,例如:只读,只写等。所有
                            的标志在头⽂件<fcntl.h> 中定义

    loff_t f_pos;           // 表⽰当前读写⽂件的位置
    ...

} __attribute__((aligned(4))); /* lest something weird decides that 2 is OK */

值得关注的是 struct file 中的 f_op 指针指向了⼀个 file_operations 结构体,这个结构体中的成员除了struct module* owner 其余都是函数指针。该结构和 struct file 都在fs.h下。

struct file_operations {
    struct module *owner;
    //指向拥有该模块的指针;
    loff_t (*llseek) (struct file *, loff_t, int);

    //llseek ⽅法⽤作改变⽂件中的当前读/写位置, 并且新位置作为(正的)返回值.
    ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);

    //⽤来从设备中获取数据. 在这个位置的⼀个空指针导致 read 系统调⽤以 -
EINVAL("Invalid argument") 失败. ⼀个⾮负返回值代表了成功读取的字节数( 返回值是⼀个
"signed size" 类型, 常常是⽬标平台本地的整数类型).
    ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);

    //发送数据给设备. 如果 NULL, -EINVAL 返回给调⽤ write 系统调⽤的程序. 如果⾮负, 返
回值代表成功写的字节数.
    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);
    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 *);

    //mmap ⽤来请求将设备内存映射到进程的地址空间. 
    如果这个⽅法是 NULL, mmap 系统调⽤返回 -ENODEV.
    int (*open) (struct inode *, struct file *);

    //打开⼀个⽂件
    int (*flush) (struct file *, fl_owner_t id);

    //flush 操作在进程关闭它的设备⽂件描述符的拷⻉时调用;
    int (*release) (struct inode *, struct file *);

    //在⽂件结构被释放时引⽤这个操作. 如同 open, release 可以为 NULL.
    int (*fsync) (struct file *, struct dentry *, int datasync);

    //用户调⽤来刷新任何挂着的数据.
    int (*aio_fsync) (struct kiocb *, int datasync);
    int (*fasync) (int, struct file *, int);
    int (*lock) (struct file *, int, struct file_lock *);

    //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 **);
};

file_operation 就是把系统调⽤和驱动程序关联起来的关键数据结构,这个结构的每⼀个成员都

对应着⼀个系统调用。读取 file_operation 中相应的函数指针,接着把控制权转交给函数,从⽽

完成了Linux设备驱动程序的⼯作。

上图中的外设,每个设备都可以有⾃⼰的read、write,但⼀定是对应着不同的操作⽅法!!但通过struct file 下 file_operation 中的各种函数回调,让我们开发者只⽤file便可调取 Linux 系统中绝⼤部分的资源!!这便是“linux下⼀切皆⽂件”的核心理解。

刨根问底

当我们打开⽂件时,操作系统在内存中要创建相应的数据结构来描述⽬标⽂件。⽽进程执⾏open系统调⽤,所以必须让进程和⽂件关联起来。每个进程都有⼀个指针*files, 指向⼀张表files_struct,该表最重要的部分就是包含⼀个指针数组,每个元素都是⼀个指向打开⽂件的指针!

Linux中,打开文件,要为我们创建struct fle,三个核心:
1.文件属性
2.文件内核缓冲区
3.底层设备文件的操作表(方法集)

根据上面所讲:

struct file 中还有一个 f_op 指针,它指向⼀个 file_operations 结构体。这个file_operations即是一个方法集,如read、write等等。

而每个设备都提供了自身需求的对应方法集的实现。

这样便做到了在上层部分函数接口都是统一的,而在下层部分每个函数接口的实现方法都是不同的。

而上层不就是C++中实现多态的基类吗?下层不就是C++中实现多态的派生类。

输出结论:一切皆文件!是站在进程的视角,在struct file结构体之上,看待文件的视角,strcutfile的方法集是一样的,但访问方式是不同的,因为不同的设备有不同的实现方法。

总结

本文深入解析了Linux"一切皆文件"的设计哲学,重点剖析了文件描述符(FD)的核心机制。从进程task_struct中的files_struct结构出发,阐述了FD作为"数组下标"的本质特性及其分配规则。通过分析struct file和file_operations结构体,揭示了Linux如何通过统一接口(如read/write)抽象各类设备资源,实现不同设备的差异化操作。关键点在于:上层提供统一接口,下层由各设备实现具体操作,类似C++的多态机制,使进程能以统一视角访问异构资源。这种设计极大简化了开发,使开发者仅需一套API即可操作绝大多数系统资源。

 


网站公告

今日签到

点亮在社区的每一天
去签到