目录
一、三个标注输入输出流
二、文件描述符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;
。
这三个流在程序开始执行时就已经打开并可用,因此可以直接使用如printf
、scanf
、fgets
等函数进行输出和输入操作,而无需手动打开任何文件。这些函数内部会处理与标准输入输出流相关的所有细节,使得与用户的交互变得简单直接。
简而言之,虽然Linux下一切皆文件,包括键盘和显示器,但是在C/C++程序中,我们通过标准的输入输出流(stdin、stdout、stderr)来与这些设备进行交互,而无需直接打开或操作它们的文件描述符。这些流在程序启动时由运行时环境自动为我们打开和配置。
二、文件描述符fd
1、通过C语言管理文件—理解文件描述符fd
在C语言的世界里,FILE
是一个关键的概念,用于表示文件流。它实际上是一个结构体,由C标准库提供,内含多种成员,旨在抽象和管理文件操作的复杂性。这个结构体封装了文件的各种信息,如缓冲状态、当前读写位置等,让开发者能够通过一系列标准库函数,如 fopen
、fwrite
、fread
等,来方便地进行文件操作。
然而,当我们深入到操作系统的层面,尤其是在UNIX或类UNIX系统中,会发现操作系统本身并不直接认识 FILE
结构体。对于操作系统而言,文件是通过文件描述符(File Descriptor, 简称fd)来识别和管理的。文件描述符是一个非常底层的概念,它是一个整数值,代表了进程中打开文件的唯一标识。
因此,虽然在使用C标准库进行文件操作时我们操作的是 FILE
类型的指针,底层实现这些功能的时候,C库函数实际上会转换成操作系统理解的文件描述符。这意味着,尽管我们在编程时与 FILE
打交道,C标准库在背后会处理与文件描述符相关的所有细节,确保我们的文件操作最终能够被操作系统正确执行。这一层抽象既隐藏了底层的复杂性,也提供了更丰富、更易用的接口给程序员。
2、文件描述符实现原理
#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 0011
show(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
这个编号系统继续下去,对于每一个新打开的文件或资源(如网络套接字),内核都会分配一个唯一的、递增的文件描述符。这使得进程可以同时管理多个输入输出流,包括文件读写、网络通信等。
- 当我们打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件。于是就有了file结构体。表示一个已经打开的文件对象。
- 而进程执行open系统调用,所以必须让进程和文件关联起来。
- 每个进程都有一个指针*files, 指向一张表files_struct,该表最重要的部分就是包涵一个指针数组,每个元素都是一个指向打开文件的指针!
- 所以,本质上,文件描述符就是该数组的下标。所以,只要拿着文件描述符,就可以找到对应的文件。
4、总结
在Linux系统中,文件标识符(File Identifier)通常指的是文件描述符(File Descriptor)。文件描述符是一个非负整数,用于唯一标识一个已打开的文件或I/O流。
-
标识打开文件:文件描述符是操作系统用来标识已打开文件的机制。每个打开的文件都会有一个对应的文件描述符。
-
整数值:文件描述符是一个非负整数,通常从0开始递增。标准输入、标准输出和标准错误的文件描述符分别是0、1和2,从文件描述符3开始,是进程打开或创建的其他文件和资源的描述符。
-
系统调用参数:文件描述符通常作为参数传递给系统调用,比如
read
、write
、close
等,以指定要操作的文件。 -
资源管理:操作系统会维护一个文件描述符表,用于跟踪每个进程打开的文件及其对应的文件描述符。这个表中记录了文件描述符和文件之间的映射关系。
-
文件操作:通过文件描述符,程序可以进行文件的读取、写入和其他操作。文件描述符是进行文件 I/O 操作的关键。
-
标准文件描述符:在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_struct
和struct 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*
-
接收参数:
fopen()
函数被调用时,接收两个参数:文件路径(path
)和模式(mode
)。模式指定了文件的访问类型(如读取、写入、追加等)。 -
高级抽象:通过
FILE *
指针,fopen()
为程序员提供了一个高级的文件操作抽象。FILE *
封装了文件描述符(fd)以及其他进行文件I/O所需的信息,比如缓冲区的地址、缓冲区大小、文件的当前位置指针等。 -
系统调用
open()获取fd
:fopen()
内部会调用系统调用open()
,向操作系统请求打开指定路径的文件。-
内核处理:操作系统内核接收到
open
系统调用后,会执行以下操作:- 内核会检查进程的权限以确定是否允许打开指定文件。
- 如果权限检查通过,内核会为该文件分配一个文件描述符。
-
-
创建struct file结构体:一旦
open
系统调用被执行,内核首先会进行权限检查、解析路径等操作。如果所有检查都通过,内核会创建一个struct file
实体来代表这个新打开的文件。 -
进程打开的文件会被加入到文件描述符表:在
open
函数返回文件描述符fd之后,进程打开的文件会被加入到文件描述符表struct file *fd_array[]
中。这意味着文件描述符表会在文件被成功打开后更新,以便进程可以通过文件描述符来访问该文件。-
文件描述符表:在
task_struct
中,有一个指向files_struct
的指针,files_struct
维护着进程打开的所有文件的信息,包括文件描述符表fd array[]
。由此获取到文件描述符(fd),并将其作为索引,访问到对应的文件信息。
-
-
创建
FILE
结构体实例:-
一旦
struct file
实体创建完成,并且open
系统调用成功,内核会将相应的文件描述符(fd)返回给用户空间的调用者。这个文件描述符实际上是struct file
实体在进程文件描述符表中的索引。 - 一旦
fopen
从open
系统调用获取到文件描述符(fd),它会在用户空间分配并初始化一个FILE
结构体实体,这个实体封装了文件描述符和其他高级I/O操作所需的信息,如缓冲区等。 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->内存文件被找到了! ->进行写入操作。
- 首先,
fwrite()
函数接收一个FILE*
类型的指针,这是C语言标准库提供的文件操作的高级抽象。通过FILE*
指针获得背后关联着一个文件描述符(fd
),它是一个低级的概念,直接与操作系统的文件系统接口相连。 - 当
fwrite()
被调用时,它最终会通过一系列转换和底层调用,调用到操作系统的write()
函数,具体形式为write(fd, ...)
。这个write()
函数是系统调用,直接与内核交互,执行具体的写入操作。 - 访问
task_struct
:每个进程在操作系统内部都有一个代表它的task_struct
结构体。当进程执行write()
调用时,系统已经通过当前的执行上下文(比如CPU的当前执行线程关联的进程)直接访问到了该进程的task_struct
。这不是通过文件描述符完成的,而是基于当前正在执行的进程上下文。 task_struct
是Linux内核中的进程控制块(PCB),包含了进程的所有信息。在task_struct
中,有一个指向files_struct
的指针,这个files_struct
维护着进程打开的所有文件的信息,包括一个文件描述符表fd array[]
。- 在
fd array[]
中,通过文件描述符fd
作为索引,可以找到对应的struct file
实例。struct file
包含了文件的详细信息,包括文件的当前状态、位置偏移量以及如何操作这个文件的方法等。 - 找到了
struct file
之后,操作系统就能够确定具体要操作的内存中的文件。随后,它执行写入操作,将数据从用户空间传输到内核空间,最终写入到文件中。