进程和文件系统
问题导入
是否有想过我们的电脑中的文件在被打开的时候,在电脑中都进行了什么操作,才能让我们成功地对它进行增删改查,让我们带着这个问题,一点点来理解电脑,或者准确点说是操作系统是如何操作的。
以下我们以c语言角度来慢慢理解大致的文件系统操作流程,因为c语言偏底层一点,其他语言都是类似,可以类比学习。
一,语言层面的文件操作
文件接口函数
预备知识
- C默认会打开三个输入输出流,分别是
stdin, stdout, stderr
- 仔细观察发现,这三个流的类型都是
FILE*
, fopen返回值类型,文件指针(实际上就是C语言库里面的结构体指针)
语言接口
- fopen(),fprintf(),fclose()
这里简单回顾一下以前学过的c语言接口
👉 详细文件知识戳这
系统接口
可以通过查询 2号手册1,来认识以下系统调用
- open
int open(【文件名】, 【状态参数】, 【文件权限】);
返回值:文件描述符 (关于文件描述符fd后面会详述)
这里主要解释一下【状态参数】,类似于fopen
中的模式,只不过这里的参数是多个,如图
mode
常用状态参数
- O_WRONLY 写入
- O_CREAT 如果文件不存在则创建文件
- O_TRUNC 清空文件内容
- O_APPEND 追加内容
- O_RDONLY 读取
“w” 相当于 1+2+3 ,“r”相当于 5,“a”相当于 1+2+4
- write
- read
注意:
- 字符串要去除’\0’,因为是系统调用,识别不出’\0’的意思,会引起乱码,所以我们使用的时候可以
strlen(buf)-1
- 系统接口read的特性就是,一口气全读取完内容,不会按行读取之类的。
语言接口和系统接口的关系
只要我们稍加思考就可知道,语言接口都是对系统接口进行封装过的,所以语言接口更加的功能多样,用法更简单,不仅C语言如此,其他函数也都是如此的,因为操作系统只有一个,要想对文件进行操作管理,那么就必须调用操作系统提供的系统调用才能实现,所以各大语言无论怎么封装,只要他们的底层的操作系统一样,那么实现都是一样的。
上面我们简单的介绍了系统调用和语言接口的关系,那么操作系统又是如何管理电脑里的文件的呢?
二,操作系统中的文件操作
1. 标准输入,标准输出,标准错误
前文有简单提及到,在linux系统中,一切皆文件,任何一个进程在启动的时候,都会默认打开当前进程的三个文件。
- 标准输入
- 标准输出
- 标准错误
C语言默认会打开stdin,stdout,stderr三个文件
C++默认会打开cin,cout,cerr三个标准流
当我们调用语言提供的文件接口时,会发现类似fopen,fprintf
…的返回值是一个FLIE*
类型的文件。很显然在C语言里,这不是内置类型,是一个结构体指针,既然是结构体指针,那必定有结构体FILE,里面肯定封装了不少调用底层的接口。为什么这么说,因为文件的操作都是由操作系统来管理的,操作系统作为连接我们用户和电脑硬件的媒介,协助我们更好地操作电脑。
2. 文件描述符
操作系统是根据文件描述符fd来对文件进行管理的
以C语言为例
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
void test2()
{
printf("stdin: fd = %d\n", stdin->_fileno);
printf("stdout: fd = %d\n", stdout->_fileno);
printf("stderr: fd = %d\n", stderr->_fileno);
}
int main()
{
test2();
return 0;
}
stdin 对应文件的文件描述符fd就是 0
stdout 对应文件的文件描述符fd就是 1
stderr 对应文件的文件描述符fd就是 2
代码里,FILE*中的成员fileno就是对fd的封装,FILE中还有更多的函数指针,就是用来封装系统调用的。
3. 文件描述符的工作原理
前面有提到,默认开启的三个文件,如果我们将其关闭,此时的文件描述符表会如何给文件分配fd呢?
void test3()
{
close(0);
close(2);
int fd1 = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
int fd2 = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
printf("fd1 = %d\n", fd1);
printf("fd2 = %d\n", fd2);
}
可以看到,我们关闭了stdin和stderr,他们的fd就被新的文件对象2引用了
答案是:从小分配。就是较小的,没有被分配到的fd会分配给新的打开文件。
我们可以多打开一些文件,然后对他们的fd进行打印,看看是不是递增的,如果是递增的排列下来,那么这样的结构可能是什么呢?
void test3()
{
int fd1 = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
int fd2 = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
int fd3 = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
int fd4 = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
int fd5 = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
printf("%d\n", fd1);
printf("%d\n", fd2);
printf("%d\n", fd3);
printf("%d\n", fd4);
printf("%d\n", fd5);
}
清楚看到,结合前面的三个已经被打开的文件,文件描述符fd是从0开始依次递增的,而且发现同一个文件可以重复打开并分别对应不同的文件描述符。结合我们所学的知识,一个进程是在内存里是由pcb3来存储相关数据信息的,多次调用系统接口open那么一定会产生多组数据,所以一定有一个类似数组一样的数据结构来存储信息。
我们继续学习得知,其实fd的本质就是返回的数组下标。
在Linux系统中,每个文件都被视为一个打开的文件,并且每个打开的文件都有一个文件描述符,用于唯一标识该文件。当你打开一个文件时,内核会为该文件分配一个文件描述符,并将该文件描述符添加到当前进程的文件描述符表中。
4. 重定向的本质
前文我们简单讲述了 fd 的工作流程和作用,现在我们用重定向的例子来加深对它作用的理解。
用法
简单理解:将输出/输入/错误流的内容流向到另一个文件内容里
void test3()
{
printf("fd1 = 0\n", fd1);
printf("fd2 = 2\n", fd2);
printf("fd3 = 3\n", fd3);
printf("fd4 = 4\n", fd4);
printf("fd5 = 5\n", fd5);
}
代码如图,本来要打印到屏幕上的数字内容被转移到了 log.txt 文件里面了。
本质
- 输出重定向
void test4()
{
close(1);
int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
printf("输出一行话\n");
}
观察下面一段代码和结果,你是否由所疑问,为什么打印的内容没有显示出来,而在 log.txt 文件却有显示内容。
别急,我们来一一解答。
以输出重定向为例,结合上面所学的知识,我们看到在函数里调用了系统调用close(1)
,将 fd 为 1 的文件对象关闭了,而 fd = 1 的文件对象正好是C语言的输出流stdout,根据文件描述符的工作特性,较小又未使用的fd将会分配给新打开的文件对象,正好 log.txt 的被打开,fd 分配为 1。
所以综上可以推出, 程序将输出的内容输入到fd=1的文件对象里。
- 追加重定向
void test4()
{
close(1);
int fd = open("log.txt", O_WRONLY | O_CREAT | O_APPEND, 0666);
printf("输出一行话\n");
}
我们发现,其实追加重定向和输出重定向差不多,只是把open()函数
中的O_TRUNC
改成O_APPEND
即可,原理都一样。
- 输入重定向
由此我们不难推断另外两个重定向——输入重定向,追加重定向,的本质。
下面看代码验证
void test4()
{
close(0); // 关闭stdin文件
int fd = open("content.txt", O_RDONLY); // 只读状态
int n, m;
scanf("%d%d", &n, &m);
printf("n=%d m=%d\n", n, m);
}
可以发现,程序并没有让我们输入值,而是去content.txt文件
里找值。
由此又可加以论证,程序从fd=0的文件里读取输入内容。
以上三个程序实际上就是重定向的大致原理,重定向的本质就是:在上层无法感知的情况下,在OS内部,更改进程对应的文件描述符表中,特定下标的指向! ! !
- 例子
将常规消息打印到 log.normal,异常消息打印到 log.error
实现代码:
void test5()
{
close(1); // 关闭stdout
close(2); // 关闭stderr
int fd1 = open("log.normal", O_WRONLY | O_CREAT | O_TRUNC, 0666);
int fd2 = open("log.error", O_WRONLY | O_CREAT | O_TRUNC, 0666);
fprintf(stdout, "正常信息\n");
fprintf(stdout, "正常信息\n");
fprintf(stdout, "正常信息\n");
fprintf(stdout, "正常信息\n");
fprintf(stderr, "错误信息\n");
fprintf(stderr, "错误信息\n");
fprintf(stderr, "错误信息\n");
fprintf(stderr, "错误信息\n");
fprintf(stderr, "错误信息\n");
}
dup2()
重定向带来了很多可能,但是是不是每次重定向都得写这么多代码来模拟实现呢?
其实不然,操作系统早就给我们定义好了系统调用接口,那就是dup2()
用法有失偏颇,我们可以这样理解 dup2 (【新的fd1】,【旧的fd2】)
这个系统调用的作用类似 fd2 = fd1,用fd1覆盖fd2,已达到目的效果。