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

🌟 各位看官好,我是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要不要管理硬件呢?它是软硬件资源的管理者,要管理,那么该如何管理呢?

先描述,再组织!!!

bash 复制代码
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 下,以下展示了该结构部分我们关系的内容:

bash 复制代码
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下。

bash 复制代码
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即可操作绝大多数系统资源。

相关推荐
虚伪的空想家19 分钟前
arm架构服务器使用kvm创建虚机报错,romfile “efi-virtio.rom“ is empty
linux·运维·服务器·javascript·arm开发·云原生·kvm
火车头-11031 分钟前
【docker 部署nacos1.4.7】
运维·docker·容器
深藏bIue32 分钟前
linux服务器mysql目录下的binlog文件删除
linux·服务器·mysql
铁手飞鹰44 分钟前
二叉树(C语言,手撕)
c语言·数据结构·算法·二叉树·深度优先·广度优先
虾..1 小时前
Linux 进程状态
linux·运维·服务器
测试者家园1 小时前
DevOps 到底改变了测试什么?
运维·自动化测试·软件测试·devops·持续测试·智能化测试·软件测试和开发
扛枪的书生2 小时前
Linux 通用软件包 AppImage 打包详解
linux
只想安静的写会代码2 小时前
网卡信息查询、配置、常见故障排查
linux·服务器·windows
jiayong232 小时前
多子系统架构下的Nginx部署策略与最佳实践
运维·nginx·系统架构
皮糖小王子2 小时前
Docker打开本地镜像
运维·docker·容器