Linux 文件系统:文件描述符、管理文件

2024-03-19 08:44

本文主要是介绍Linux 文件系统:文件描述符、管理文件,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

目录

一、三个标注输入输出流

二、文件描述符fd

1、通过C语言管理文件—理解文件描述符fd

2、文件描述符实现原理

3、文件描述符0、1、2

4、总结

三、如何管理文件

1、打开文件的过程

2、内核空间的结构

struct task_struct(PCB)

struct files_struct和struct file *fd_array[]

struct file

3、深入到内核空间的操作 

fopen()的操作流程:

fwrite()操作流程


一、三个标注输入输出流

在Linux系统中,存在一个核心概念:“一切皆文件”。这意味着系统中的几乎所有东西,包括硬件设备如键盘和显示器,都被抽象为文件。这种设计使得与这些设备的交互变得统一和简化,因为你可以使用标准的文件操作API来与它们交互。

尽管如此,我们通常不会直接“打开”键盘或显示器文件来进行读写操作。相反,C和C++程序通常通过标准输入输出流来与这些设备交互。这是因为C/C++的标准库(如stdio.h)为我们提供了一组抽象的输入输出功能,这些功能背后默认与键盘和显示器等设备的文件描述符相连接。

当你启动一个C或C++程序时,运行时环境(如操作系统的shell)会自动为程序打开三个基本的文件流:

  • 标准输入(stdin):通常与键盘关联,用于读取输入。在程序中,它被表示为FILE* stdin;
  • 标准输出(stdout):通常与显示器关联,用于输出信息。在程序中,它被表示为FILE* stdout;
  • 标准错误(stderr):也通常与显示器关联,但用于输出错误信息。在程序中,它被表示为FILE* stderr;

这三个流在程序开始执行时就已经打开并可用,因此可以直接使用如printfscanffgets等函数进行输出和输入操作,而无需手动打开任何文件。这些函数内部会处理与标准输入输出流相关的所有细节,使得与用户的交互变得简单直接。

简而言之,虽然Linux下一切皆文件,包括键盘和显示器,但是在C/C++程序中,我们通过标准的输入输出流(stdin、stdout、stderr)来与这些设备进行交互,而无需直接打开或操作它们的文件描述符。这些流在程序启动时由运行时环境自动为我们打开和配置。

二、文件描述符fd

1、通过C语言管理文件—理解文件描述符fd

在C语言的世界里,FILE 是一个关键的概念,用于表示文件流。它实际上是一个结构体,由C标准库提供,内含多种成员,旨在抽象和管理文件操作的复杂性。这个结构体封装了文件的各种信息,如缓冲状态、当前读写位置等,让开发者能够通过一系列标准库函数,如 fopenfwritefread 等,来方便地进行文件操作。

        然而,当我们深入到操作系统的层面,尤其是在UNIX或类UNIX系统中,会发现操作系统本身并不直接认识 FILE 结构体。对于操作系统而言,文件是通过文件描述符(File Descriptor, 简称fd)来识别和管理的。文件描述符是一个非常底层的概念,它是一个整数值,代表了进程中打开文件的唯一标识。

        因此,虽然在使用C标准库进行文件操作时我们操作的是 FILE 类型的指针,底层实现这些功能的时候,C库函数实际上会转换成操作系统理解的文件描述符。这意味着,尽管我们在编程时与 FILE 打交道,C标准库在背后会处理与文件描述符相关的所有细节,确保我们的文件操作最终能够被操作系统正确执行。这一层抽象既隐藏了底层的复杂性,也提供了更丰富、更易用的接口给程序员。

2、文件描述符实现原理

通过对open函数的学习,我们知道了文件描述符就是一个小整数,其内部实现是通过位图的原理,具体实现方式如下:
#include <stdio.h>// 定义标志位,用int中的不重复的一个bit,就可以标识一种状态
#define ONE   0x1    //0000 0001
#define TWO   0x2    //0000 0010
#define THREE 0x4    //0000 0100// 显示函数,根据标志位输出不同的消息
void show(int flags) {if (flags & ONE)  printf("你好,第一种状态\n");if (flags & TWO)printf("你好,第二种状态\n");if (flags & THREE)printf("你好,第三种状态\n");
}int main() {// 分别显示不同的状态show(ONE);show(TWO);show(ONE | TWO); // 使用按位或运算符组合标志位show(ONE | TWO | THREE);//0000 0001 | 0000 0010=0000 0011show(ONE | THREE);return 0;
}[hbr@VM-16-9-centos exercise_func]$ ./mytest 
你好,第一种状态
-----------------------------------------
你好,第二种状态
-----------------------------------------
你好,第一种状态
你好,第二种状态
-----------------------------------------
你好,第一种状态
你好,第二种状态
你好,第三种状态
-----------------------------------------
你好,第一种状态
你好,第三种状态

3、文件描述符0、1、2

在Linux进程中,文件描述符0、1、和2分别用于标准输入(stdin)、标准输出(stdout)、和标准错误(stderr)的缺省打开。这三个文件描述符为进程通信与数据输入输出提供了基础。

  • 当你从键盘输入时,默认情况下,这些输入数据通过文件描述符0(标准输入)进入程序;
  • 当程序需要输出信息到屏幕时,它会使用文件描述符1(标准输出)和文件描述符2(标准错误),其中标准输出用于常规信息,标准错误专用于输出错误信息。

从文件描述符3开始,是进程打开或创建的其他文件和资源的描述符。

  • 当一个进程通过如open系统调用打开或创建新文件时,分配给该文件的文件描述符将是当前未被使用的最小的正整数文件描述符。
  • 这意味着,如果进程没有打开除标准输入、输出和错误外的其他文件,那么新打开的文件将使用文件描述符3。

通过下面程序就明白了:

[hbr@VM-16-9-centos exercise_func]$ cat testC.c 
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>int main() 
{umask(0);int fd1 = open("log1.txt", O_WRONLY|O_CREAT|O_APPEND,0666); printf("open success, fd1: %d\n", fd1);int fd2 = open("log2.txt", O_WRONLY|O_CREAT|O_APPEND,0666); printf("open success, fd2: %d\n", fd2);int fd3 = open("log3.txt", O_WRONLY|O_CREAT|O_APPEND,0666); printf("open success, fd3: %d\n", fd3);int fd4 = open("log4.txt", O_WRONLY|O_CREAT|O_APPEND,0666); printf("open success, fd4: %d\n", fd4);close(fd1);close(fd2);close(fd3);close(fd4);return 0;
}
[hbr@VM-16-9-centos exercise_func]$ ll
total 40
-rw-rw-r-- 1 hbr hbr   72 Mar 18 21:19 makefile
-rwxrwxr-x 1 hbr hbr 8664 Mar 18 23:20 mytest
-rw-rw-r-- 1 hbr hbr  954 Mar 18 23:29 testC.c
[hbr@VM-16-9-centos exercise_func]$ make
gcc -std=c99 -o mytest testC.c
[hbr@VM-16-9-centos exercise_func]$ ./mytest 
open success, fd1: 3
open success, fd2: 4
open success, fd3: 5
open success, fd4: 6
[hbr@VM-16-9-centos exercise_func]$ ll
total 40
-rw-rw-rw- 1 hbr hbr    0 Mar 18 23:30 log1.txt
-rw-rw-rw- 1 hbr hbr    0 Mar 18 23:30 log2.txt
-rw-rw-rw- 1 hbr hbr    0 Mar 18 23:30 log3.txt
-rw-rw-rw- 1 hbr hbr    0 Mar 18 23:30 log4.txt
-rw-rw-r-- 1 hbr hbr   72 Mar 18 21:19 makefile
-rwxrwxr-x 1 hbr hbr 8512 Mar 18 23:30 mytest
-rw-rw-r-- 1 hbr hbr  954 Mar 18 23:29 testC.c

这个编号系统继续下去,对于每一个新打开的文件或资源(如网络套接字),内核都会分配一个唯一的、递增的文件描述符。这使得进程可以同时管理多个输入输出流,包括文件读写、网络通信等。

现在知道,文件描述符就是从0开始的小整数。
  • 当我们打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件。于是就有了file结构体。表示一个已经打开的文件对象。
  • 而进程执行open系统调用,所以必须让进程和文件关联起来。
  • 每个进程都有一个指针*files, 指向一张表files_struct,该表最重要的部分就是包涵一个指针数组,每个元素都是一个指向打开文件的指针!
  • 所以,本质上,文件描述符就是该数组的下标。所以,只要拿着文件描述符,就可以找到对应的文件。

4、总结

在Linux系统中,文件标识符(File Identifier)通常指的是文件描述符(File Descriptor)。文件描述符是一个非负整数,用于唯一标识一个已打开的文件或I/O流。

  1. 标识打开文件:文件描述符是操作系统用来标识已打开文件的机制。每个打开的文件都会有一个对应的文件描述符。

  2. 整数值:文件描述符是一个非负整数,通常从0开始递增。标准输入、标准输出和标准错误的文件描述符分别是0、1和2,从文件描述符3开始,是进程打开或创建的其他文件和资源的描述符。

  3. 系统调用参数:文件描述符通常作为参数传递给系统调用,比如readwriteclose等,以指定要操作的文件。

  4. 资源管理:操作系统会维护一个文件描述符表,用于跟踪每个进程打开的文件及其对应的文件描述符。这个表中记录了文件描述符和文件之间的映射关系。

  5. 文件操作:通过文件描述符,程序可以进行文件的读取、写入和其他操作。文件描述符是进行文件 I/O 操作的关键。

  6. 标准文件描述符:在Linux系统中,通常有三个标准文件描述符:0(标准输入)、1(标准输出)和2(标准错误),它们分别对应键盘输入、终端输出和错误信息输出。

三、如何管理文件

一个进程不仅可以打开多个文件,而且这种情况非常普遍,形成了一对多(1:n)的关系。所以,在操作系统的内部,为了有效地管理众多被打开的文件,确实需要对这些文件进行适当的组织和管理。

操作系统会为每一个被打开的文件创建一个结构体(通常称为 struct file 或类似的名称),该结构体包含了关于这个文件的几乎所有信息,如文件的属性、位置指针、读写权限等。如果存在大量的被打开的文件,操作系统会通过数据结构,如双链表,来组织这些 struct file 的实例,以便高效管理和访问。

 文件描述符fd在内核中的表现,可以被理解为指向这些 struct file 实例的指针数组的下标。

1、打开文件的过程

  • 当一个进程请求打开一个文件时,操作系统会首先在内核中创建一个对应的 struct file 对象。
  • 接着,它会在文件描述符表(这是一个每个进程都有的、指向 struct file 实例的指针数组)中寻找一个未被使用的条目,并将新创建的 struct file 对象的引用存放在那里。最后,操作系统将这个条目的索引(即文件描述符)返回给进程。
  • 因此,当进程需要进行文件操作时,它会提供文件描述符作为参数。操作系统通过这个文件描述符,查找进程的文件描述符表,从而找到对应的 struct file 对象,进而进行具体的文件操作。这种机制既简化了文件操作的接口,也保证了操作的安全性和效率。

2、内核空间的结构

struct task_struct(PCB)

  • 每个运行中的进程在内核中都有一个struct task_struct实例,其中包含了进程的所有信息,包括它的文件描述符表,通过files指针指向struct files_struct

struct files_structstruct file *fd_array[]

  • struct files_struct维护了一个文件描述符表fd_array[],这个表是一个指针数组,每个指针指向一个struct file实例。文件描述符(fd)实质上是这个数组的索引。

struct files_struct是Linux内核中的一个结构体,它的主要作用是管理和跟踪一个进程打开的所有文件描述符。在Linux系统中,当进程打开一个文件时,内核会为该文件分配一个文件描述符(fd),这是一个非负整数,用作索引来访问进程打开的文件。文件描述符为进程与打开的文件之间的交互提供了一个简单的抽象。

  • 具体来说,struct files_struct包含了以下关键信息:
  • count: 一个引用计数,表示有多少个地方引用到这个files_struct。这是用于内存管理和确保结构体在不再被需要时可以被正确地释放。
  • fdt: 指向一个fdtable结构的指针,该结构实际上保存了文件描述符的数组(fd_array)以及与之相关的一些其他数据,如文件描述符的最大数量等。
  • fd_array: 通常作为fdtable的一部分,这是一个指针数组,每个指针指向一个struct file结构体实例。每个struct file实例代表一个打开的文件,包含了文件的当前状态、位置偏移量、以及文件操作的方法等信息。
  • 文件描述符的分配和回收机制files_struct还管理着文件描述符的分配和回收,确保每次打开文件时都能分配一个唯一的最小可用文件描述符,并在文件关闭时回收该描述符。
  • 通过管理每个进程的文件描述符表,struct files_struct为进程提供了对打开文件的有效访问和控制,使得进程可以执行读写、查询状态以及执行其他文件操作。

struct file

  • struct file代表一个打开的文件的所有信息,包括文件的状态、当前偏移量、与文件相关的操作函数等。它是实际执行读写操作的基础。

3、深入到内核空间的操作 

FILE *stdout = fopen("显示器",“w");-> open->1
stdout -> FILE* -> FILE->fileno(1)

  • FILE *stdout = fopen("显示器", "w");

    • 用户程序请求打开"显示器"(在实际应用中,stdout通常预定义且指向标准输出)进行写操作。fopen函数内部调用open系统调用,并将返回的文件描述符(fd)封装在一个FILE结构体中。这个结构体提供了一个高级接口来进行后续的文件I/O操作。

fopen()的操作流程:

fopen->open->检查并分配fd->struct file->在struct file *fd_array[fd]指向struct file->fopen从open获得fd->创建FILE*

  1. 接收参数fopen()函数被调用时,接收两个参数:文件路径(path)和模式(mode)。模式指定了文件的访问类型(如读取、写入、追加等)。

  2. 高级抽象:通过FILE *指针,fopen()为程序员提供了一个高级的文件操作抽象。FILE *封装了文件描述符(fd)以及其他进行文件I/O所需的信息,比如缓冲区的地址、缓冲区大小、文件的当前位置指针等。

  3. 系统调用open()获取fdfopen()内部会调用系统调用open(),向操作系统请求打开指定路径的文件。

    1. 内核处理:操作系统内核接收到open系统调用后,会执行以下操作:

      • 内核会检查进程的权限以确定是否允许打开指定文件。
      • 如果权限检查通过,内核会为该文件分配一个文件描述符。
  4. 创建struct file结构体:一旦open系统调用被执行,内核首先会进行权限检查、解析路径等操作。如果所有检查都通过,内核会创建一个struct file实体来代表这个新打开的文件。

  5. 进程打开的文件会被加入到文件描述符表:open函数返回文件描述符fd之后,进程打开的文件会被加入到文件描述符表struct file *fd_array[]中。这意味着文件描述符表会在文件被成功打开后更新,以便进程可以通过文件描述符来访问该文件。

    1. 文件描述符表:在task_struct中,有一个指向files_struct的指针,files_struct维护着进程打开的所有文件的信息,包括文件描述符表fd array[]。由此获取到文件描述符(fd),并将其作为索引,访问到对应的文件信息。

  6. 创建FILE结构体实例

    1. 一旦struct file实体创建完成,并且open系统调用成功,内核会将相应的文件描述符(fd)返回给用户空间的调用者。这个文件描述符实际上是struct file实体在进程文件描述符表中的索引。

    2. 一旦fopenopen系统调用获取到文件描述符(fd),它会在用户空间分配并初始化一个FILE结构体实体,这个实体封装了文件描述符和其他高级I/O操作所需的信息,如缓冲区等。
    3. fopen之后返回的是一个指向FILE结构体的指针,供用户程序进行后续的文件操作。
  • 当一个进程调用 open() 等系统调用来打开文件时,实际上并不需要直接访问该进程的 task_struct 结构体。文件的打开操作是基于进程的文件描述符表进行的,这个表存储了进程打开的文件以及它们对应的文件描述符。
  • 文件描述符表是由进程的 files_struct 结构体间接引用的,而 files_struct 结构体是存储在进程的 task_struct 结构体中的。因此,虽然文件的打开操作涉及到文件描述符表,但并不需要直接访问进程的 task_struct 结构体,因为文件描述符表是通过 task_struct 间接引用的。这种间接引用的设计使得操作系统能够有效地管理进程的文件描述符和文件操作,同时保持进程信息的封装和隔离。

fwrite()操作流程

fwrite()-> FILE* -> fd -> write -> write(fd,...) -> 自己执行操作系统内部的write方法 ->能找到进程的task struct->*fs -> files struct->fd arrayl]->fd array[fd]->struct file->内存文件被找到了! ->进行写入操作。

  1. 首先,fwrite()函数接收一个FILE*类型的指针,这是C语言标准库提供的文件操作的高级抽象。通过FILE*指针获得背后关联着一个文件描述符(fd),它是一个低级的概念,直接与操作系统的文件系统接口相连。
  2. fwrite()被调用时,它最终会通过一系列转换和底层调用,调用到操作系统的write()函数,具体形式为write(fd, ...)。这个write()函数是系统调用,直接与内核交互,执行具体的写入操作。
  3. 访问task_struct:每个进程在操作系统内部都有一个代表它的task_struct结构体。当进程执行write()调用时,系统已经通过当前的执行上下文(比如CPU的当前执行线程关联的进程)直接访问到了该进程的task_struct。这不是通过文件描述符完成的,而是基于当前正在执行的进程上下文。
  4. task_struct是Linux内核中的进程控制块(PCB),包含了进程的所有信息。在task_struct中,有一个指向files_struct的指针,这个files_struct维护着进程打开的所有文件的信息,包括一个文件描述符表fd array[]
  5. fd array[]中,通过文件描述符fd作为索引,可以找到对应的struct file实例。struct file包含了文件的详细信息,包括文件的当前状态、位置偏移量以及如何操作这个文件的方法等。
  6. 找到了struct file之后,操作系统就能够确定具体要操作的内存中的文件。随后,它执行写入操作,将数据从用户空间传输到内核空间,最终写入到文件中。

这篇关于Linux 文件系统:文件描述符、管理文件的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/825425

相关文章

linux-基础知识3

打包和压缩 zip 安装zip软件包 yum -y install zip unzip 压缩打包命令: zip -q -r -d -u 压缩包文件名 目录和文件名列表 -q:不显示命令执行过程-r:递归处理,打包各级子目录和文件-u:把文件增加/替换到压缩包中-d:从压缩包中删除指定的文件 解压:unzip 压缩包名 打包文件 把压缩包从服务器下载到本地 把压缩包上传到服务器(zip

Linux 网络编程 --- 应用层

一、自定义协议和序列化反序列化 代码: 序列化反序列化实现网络版本计算器 二、HTTP协议 1、谈两个简单的预备知识 https://www.baidu.com/ --- 域名 --- 域名解析 --- IP地址 http的端口号为80端口,https的端口号为443 url为统一资源定位符。CSDNhttps://mp.csdn.net/mp_blog/creation/editor

【Python编程】Linux创建虚拟环境并配置与notebook相连接

1.创建 使用 venv 创建虚拟环境。例如,在当前目录下创建一个名为 myenv 的虚拟环境: python3 -m venv myenv 2.激活 激活虚拟环境使其成为当前终端会话的活动环境。运行: source myenv/bin/activate 3.与notebook连接 在虚拟环境中,使用 pip 安装 Jupyter 和 ipykernel: pip instal

Linux_kernel驱动开发11

一、改回nfs方式挂载根文件系统         在产品将要上线之前,需要制作不同类型格式的根文件系统         在产品研发阶段,我们还是需要使用nfs的方式挂载根文件系统         优点:可以直接在上位机中修改文件系统内容,延长EMMC的寿命         【1】重启上位机nfs服务         sudo service nfs-kernel-server resta

【Linux 从基础到进阶】Ansible自动化运维工具使用

Ansible自动化运维工具使用 Ansible 是一款开源的自动化运维工具,采用无代理架构(agentless),基于 SSH 连接进行管理,具有简单易用、灵活强大、可扩展性高等特点。它广泛用于服务器管理、应用部署、配置管理等任务。本文将介绍 Ansible 的安装、基本使用方法及一些实际运维场景中的应用,旨在帮助运维人员快速上手并熟练运用 Ansible。 1. Ansible的核心概念

Linux服务器Java启动脚本

Linux服务器Java启动脚本 1、初版2、优化版本3、常用脚本仓库 本文章介绍了如何在Linux服务器上执行Java并启动jar包, 通常我们会使用nohup直接启动,但是还是需要手动停止然后再次启动, 那如何更优雅的在服务器上启动jar包呢,让我们一起探讨一下吧。 1、初版 第一个版本是常用的做法,直接使用nohup后台启动jar包, 并将日志输出到当前文件夹n

[Linux]:进程(下)

✨✨ 欢迎大家来到贝蒂大讲堂✨✨ 🎈🎈养成好习惯,先赞后看哦~🎈🎈 所属专栏:Linux学习 贝蒂的主页:Betty’s blog 1. 进程终止 1.1 进程退出的场景 进程退出只有以下三种情况: 代码运行完毕,结果正确。代码运行完毕,结果不正确。代码异常终止(进程崩溃)。 1.2 进程退出码 在编程中,我们通常认为main函数是代码的入口,但实际上它只是用户级

【Linux】应用层http协议

一、HTTP协议 1.1 简要介绍一下HTTP        我们在网络的应用层中可以自己定义协议,但是,已经有大佬定义了一些现成的,非常好用的应用层协议,供我们直接使用,HTTP(超文本传输协议)就是其中之一。        在互联网世界中,HTTP(超文本传输协议)是一个至关重要的协议,他定义了客户端(如浏览器)与服务器之间如何进行通信,以交换或者传输超文本(比如HTML文档)。

如何编写Linux PCIe设备驱动器 之二

如何编写Linux PCIe设备驱动器 之二 功能(capability)集功能(capability)APIs通过pci_bus_read_config完成功能存取功能APIs参数pos常量值PCI功能结构 PCI功能IDMSI功能电源功率管理功能 功能(capability)集 功能(capability)APIs int pcie_capability_read_wo