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使用nload监控网络流量的方法

《Linux使用nload监控网络流量的方法》Linux中的nload命令是一个用于实时监控网络流量的工具,它提供了传入和传出流量的可视化表示,帮助用户一目了然地了解网络活动,本文给大家介绍了Linu... 目录简介安装示例用法基础用法指定网络接口限制显示特定流量类型指定刷新率设置流量速率的显示单位监控多个

ElasticSearch+Kibana通过Docker部署到Linux服务器中操作方法

《ElasticSearch+Kibana通过Docker部署到Linux服务器中操作方法》本文介绍了Elasticsearch的基本概念,包括文档和字段、索引和映射,还详细描述了如何通过Docker... 目录1、ElasticSearch概念2、ElasticSearch、Kibana和IK分词器部署

Linux流媒体服务器部署流程

《Linux流媒体服务器部署流程》文章详细介绍了流媒体服务器的部署步骤,包括更新系统、安装依赖组件、编译安装Nginx和RTMP模块、配置Nginx和FFmpeg,以及测试流媒体服务器的搭建... 目录流媒体服务器部署部署安装1.更新系统2.安装依赖组件3.解压4.编译安装(添加RTMP和openssl模块

linux下多个硬盘划分到同一挂载点问题

《linux下多个硬盘划分到同一挂载点问题》在Linux系统中,将多个硬盘划分到同一挂载点需要通过逻辑卷管理(LVM)来实现,首先,需要将物理存储设备(如硬盘分区)创建为物理卷,然后,将这些物理卷组成... 目录linux下多个硬盘划分到同一挂载点需要明确的几个概念硬盘插上默认的是非lvm总结Linux下多

linux进程D状态的解决思路分享

《linux进程D状态的解决思路分享》在Linux系统中,进程在内核模式下等待I/O完成时会进入不间断睡眠状态(D状态),这种状态下,进程无法通过普通方式被杀死,本文通过实验模拟了这种状态,并分析了如... 目录1. 问题描述2. 问题分析3. 实验模拟3.1 使用losetup创建一个卷作为pv的磁盘3.

Linux环境变量&&进程地址空间详解

《Linux环境变量&&进程地址空间详解》本文介绍了Linux环境变量、命令行参数、进程地址空间以及Linux内核进程调度队列的相关知识,环境变量是系统运行环境的参数,命令行参数用于传递给程序的参数,... 目录一、初步认识环境变量1.1常见的环境变量1.2环境变量的基本概念二、命令行参数2.1通过命令编程

Linux之进程状态&&进程优先级详解

《Linux之进程状态&&进程优先级详解》文章介绍了操作系统中进程的状态,包括运行状态、阻塞状态和挂起状态,并详细解释了Linux下进程的具体状态及其管理,此外,文章还讨论了进程的优先级、查看和修改进... 目录一、操作系统的进程状态1.1运行状态1.2阻塞状态1.3挂起二、linux下具体的状态三、进程的

Linux编译器--gcc/g++使用方式

《Linux编译器--gcc/g++使用方式》文章主要介绍了C/C++程序的编译过程,包括预编译、编译、汇编和链接四个阶段,并详细解释了每个阶段的作用和具体操作,同时,还介绍了调试和发布版本的概念... 目录一、预编译指令1.1预处理功能1.2指令1.3问题扩展二、编译(生成汇编)三、汇编(生成二进制机器语

Rsnapshot怎么用? 基于Rsync的强大Linux备份工具使用指南

《Rsnapshot怎么用?基于Rsync的强大Linux备份工具使用指南》Rsnapshot不仅可以备份本地文件,还能通过SSH备份远程文件,接下来详细介绍如何安装、配置和使用Rsnaps... Rsnapshot 是一款开源的文件系统快照工具。它结合了 Rsync 和 SSH 的能力,可以帮助你在 li

Linux部署jar包过程

《Linux部署jar包过程》文章介绍了在Linux系统上部署Java(jar)包时需要注意的几个关键点,包括统一JDK版本、添加打包插件、修改数据库密码以及正确执行jar包的方法... 目录linux部署jar包1.统一jdk版本2.打包插件依赖3.修改密码4.执行jar包总结Linux部署jar包部署