< Linux > 基础IO(文件描述符)

目录

1、准备工作(文件描述符)

2、C语言文件IO

        什么是当前路径?

        C语言文件接口汇总

        默认打开的三个流

3、系统文件IO

        3.1、open

                open的第一个参数(pathname)

                open的第二个参数(flags)

                open的第三个参数(mode)

        3.2、close

        3.3、write

        3.4、read

        3.5、对比C语言文件操作与系统的文件操作

        3.6、open的返回值

4、文件描述符fd

5、文件描述符的分配规则

6、重定向

        重定向的原理

        dup2

7、FILE

        FILE当中的文件描述符

        FILE当中的缓冲区


1、准备工作(文件描述符)

1、文件 = 文件内容 + 文件属性

  • 文件内容就是真正写入的内容,文件属性就是文件名、文件大小、文件的权限、拥有者所属组……,文件属性也是数据,即便你创建一个空文件,也要占据磁盘空间

2、文件操作 = 文件内容的操作 + 文件属性的操作

  • 我们有可能在操作文件的过程中,既改变内容,又改变属性

3、所谓的“打开”文件,究竟在干什么?

  • 将文件的属性内容或内容加载到内存中,根据冯诺依曼体系结构决定的,cpu只能从内存中对数据做读写

4、是不是所有的文件,都会处于被打开的状态?没有被打开的文件,在哪里?

  • 不是的,没有被打开的文件只在磁盘上静静的存储着!

5、打开的文件(内存文件)和 磁盘文件

  • 下面所讲到的都是打开的文件

6、通常我们代开文件,访问文件,关闭文件,是谁在进行相关操作?

  • 进程!C语言中我们访问文件需要用到fopen、fclose、fread……,当代码写完变成程序后,并且运行此文件程序,此时才会执行对应的代码,然后才是真正的对文件进行相关的操作,所以真正对文件操作的其实是进程。

2、C语言文件IO

什么是当前路径?

根据我们前面的学习,当fopen以写入的方式打开一个文件时,若文件不存在,则会自动在当前路径创建该文件,那么这里的当前路径是什么呢?以下面的代码为测试用例:

#include<stdio.h>
#include<unistd.h>
int main()
{
	FILE* fp = fopen("log.txt", "w");//写入
	if (fp == NULL)
	{
		perror("fopen");
		return 1;
	}
	printf("mypid: %d", getpid());
	while (1)
	{
		sleep(1);
	}
	const char* msg = "hello world";
	int cnt = 1;
	while (cnt < 20)
	{
		fprintf(fp, "%s: %d\n", msg, cnt++);
	}
	fclose(fp);
	return 0;
}

此段代码我fopen写入的方式打开了log.txt文件,那么我在date17目录下运行可执行程序myfile,那么该可执行程序创建的log.txt文件会出现在date17目录下,此外上述代码还获取到了当前进程的pid,并且让此进程一直循环下去,方便后续操作。

根据我们获取到的当前进程的pid,再根据我们先前学到的知识,根据该pid在根目录下的proc目录下查看此进程的信息如下:

下面来解释下cwd和exe:

  • cwd表示当前进程所处的工作路径。
  • exe表示进程对应的可执行程序的磁盘文件。 

上面的运行结果也正如我们所料:log.txt创建在了与当前可执行程序路径所在的位置,也是当前进程所处的工作路径,那是否就意味着这里说的“当前路径”是指“当前可执行程序所处的路径”呢?还是说“当前路径”是指“当前进程所处的路径”呢?

  • 这里我们把刚才生成的log.txt文件删除掉,对代码进行如下的修改,利用上次学到的chdir函数来更改此进程的工作路径:chdir("/home/xzy")

make clean;make后再次运行myfile程序,结果如下:

此时现象已经很明显了,我运行了myfile可执行程序,但是并没有在当前可执行程序myfile所处在的date17目录下看到我想要的log.txt文件,相反我却在/home/xzy路径下看到了log.txt文件,这就足以证明我利用chdir更改进程所处的路径后,生产的文件也随之更改,这足矣证明此当前路径即为当前进程所处的路径,为了更具有说服力,我们依旧是利用proc查看当前进程9752的相关信息:

综上,当前路径就是当前进程所处的路径!!!


C语言文件接口汇总

C语言文件操作函数如下:

文件操作函数功能
fopen打开文件
fclose关闭文件
fputc写入一个字符
fgetc

读取一个字符

fputs写入一个字符串
fgets读取一个字符串
fprintf格式化写入数据
fscanf格式化读取数据
fwrite向二进制文件写入数据
fread从二进制文件读取数据
fseek设置文件指针的位置
ftell计算当前文件指针相对于起始位置的偏移量
rewind设置文件指针到文件的起始位置
ferror判断文件操作过程中是否发生错误
feof判断文件指针是否读取到文件末尾

C语言的文件操作我之前已经详细讲解过,如果想了解上述诸多文件操作函数的使用方法,还请跳转到博主的如下两篇博文:

下面对fopen再强调下:

//打开文件
FILE * fopen ( const char * filename, const char * mode );

fopen参数含义:

  • 参数1:文件名
  • 参数2:文件打开方式

打开方式如下:

文件使用方式含义如果指定文件不存在
"r"(只读)为了输入数据,打开一个已经存在的文本文件出错
"w"(只写)为了输出数据,打开一个文本文件建立一个新的文件
"a"(追加)向文本文件尾添加数据建立一个新的文件
"rb"(只读)为了输入数据,打开一个二进制文件出错
"wb"(只写)为了输出数据,打开一个二进制文件建立一个新的文件
"ab"(追加)向一个二进制文件尾添加数据出错
"r+"(读写)为了读和写,打开一个文本文件出错
"w+"(读写)为了读和写,建立一个新的文件建立一个新的文件
"a+"(读写)打开一个文件,在文件尾进行读写建立一个新的文件
"rb+"(读写)为了读和写打开一个二进制文件出错
"wb+"(读写)为了读和写,新建一个新的二进制文件建立一个新的文件
"ab+"(读写)打开一个二进制文件,在文件尾进行读和写建立一个新的文件

接下来取其中部分进行演示:

1、"a"追加:

#include<stdio.h>
#include<unistd.h>
int main()
{
	FILE* fp = fopen("log.txt", "a");//写入
	if (fp == NULL)
	{
		perror("fopen");
		return 1;
	}
	const char* msg = "hello world";
	int cnt = 1;
	while (cnt <= 5)
	{
		fprintf(fp, "%s: %d\n", msg, cnt++);
	}
	fclose(fp);
	return 0;
}

2、"w"只写:

  • 如果一个文件本就存在,那么以w的方式写入会先清空你文件的内容,如果不存在那么就创建个新的,并且从文件的最开始写入。

  • 我们在上述已经创建好log.txt文件的基础上执行此程序:

  • 此时当我们以w方式打开文件,准备写入的时候,其实文件已经先被清空了。 

3、"r"只读:

  • 以如下的代码示例:
#include<stdio.h>
#include<unistd.h>
int main()
{
	FILE* fp = fopen("log.txt", "r");
	if (fp == NULL)
	{
		perror("fopen");
		return 1;
	}
	char buffer[64];
	while (fgets(buffer, sizeof(buffer), fp) != NULL)
	{
		printf("echo: %s", buffer);
	}
	fclose(fp);
	return 0;
}
  • 我们预先给log.txt文件输入以下内容:

  • 运行myfile程序:

4、根据上述对文件的读取,我们现在写个小功能(利用文件操作模拟实现cat命令):

  • 代码如下:
#include<stdio.h>
#include<unistd.h>
//myfilel filename
int main(int argc, char* argv[])
{
    if (argc != 2)
    {
        printf("Usage: %s filename\n", argv[0]);
        return 1;
    }
    FILE* fp = fopen(argv[1], "r");
    if (fp == NULL)
    {
        perror("fopen");
        return 1;
    }
    char buffer[64];
    while (fgets(buffer, sizeof(buffer), fp) != NULL)
    {
        printf("%s", buffer);
    }
    fclose(fp);
    return 0;
}
  • 现在运行可执行程序myfile + 要读取的文件名即可完成cat的功能:

问1:当我们想文件写入的时候,最终是不是向磁盘写入?

  • 是的,磁盘是硬件,只有OS操作系统有资格向硬件写入,我们不能绕开操作系统对磁盘资源的访问,换言之,所有的上层访问文件的操作,都必须贯穿操作系统,而操作系统不相信任何人,所以只能使用操作系统提供的相关系统调用来让OS被上层使用。

问2:如何理解printf?我们怎么从来没有见过?

  • 首先,printf的结果是显示到显示器上的,显示器是硬件,其管理者只能是操作系统,必须通过对应的调用接口来访问显示器,所以printf内部一定封装了系统接口
  • 我们从来没有见过这些系统调用接口是因为所有的语言都对系统接口做了封装,所以看不到底层的系统调用接口的差别

问3:为什么要封装?

  1. 原生系统接口,使用成本太高了!
  2. 使用原生系统接口,一段代码只能在一个平台上跑,不同的平台暴露出的文件接口是不一样的有时候,最终会导致语言不具备跨平台性!

问4:封装是如何解决跨平台问题的呢?

  • 穷举所有的底层接口 + 条件编译!

默认打开的三个流

  • 都说 Linux 下一切皆文件,也就是说Linux下的任何东西都可以看作是文件,那么显示器和键盘当然也可以看作是文件。我们能看到显示器上的数据,是因为我们向“显示器文件”写入了数据,电脑能获取到我们敲击键盘时对应的字符,是因为电脑从“键盘文件”读取了数据。

为什么我们向“显示器文件“写入数据以及从“键盘文件”读取数据前,不需要进行打开“显示器文件”和键盘文件“的相应操作?

  • 需要注意的是,打开文件一定是进程运行的时候打开的,而任何进程在运行的时候都会默认打开三个输入输出流,即标准输入流、标准输出流、标准错误流,对应到C语言中就是stdin,stdout,stderr。其中,标准输入流对应的设备就是键盘,标准输出流和标准错误流对应的设备都是显示器。

查看man手册我们就可以发现,stdin、stdout、stderr实际上都是FILE*类型的。

当我们的C程序被运行起来时,操作系统就会默认使用C语言的相关接口将这三个输入输出流打开,之后我们才能调用类似于scanf和printf之类的函数向键盘和显示器进行相应的输入输出操作。

也就是说,stdin、stdout、stderr与我们打开某一文件时获取到的文件指针是同一个概念,试想我们使用fputs函数时,将其第二个参数设置为stdout,此时fputs函数会被和将数据显示到显示器上呢?

#include<stdio.h>
int main()
{
    fputs("hello stdin\n", stdout);
    fputs("hello stdout\n", stdout);
    fputs("hello stderr\n", stdout);
    return 0;
}

答案是肯定的,此时我们相当于使用fputs函数向“显示器文件”写入数据,也就是显示到显示器上。

注意:不止是C语言中有标准输入流、标准输出流和标准错误流,C++当中也有对应的cin,cout和cerr,其它所有语言都有类似的概念。实际上这种特性并不是某种语言所特有的,而是由操作系统所支持的。


3、系统文件IO

操作文件除了C语言接口、C++接口或是其他语言的接口外,操作系统也有一套系统接口来进行文件的访问。相比于C库函数或其他语言的库函数而言,系统调用接口更贴近底层,实际上这些语言的库函数都是对系统接口进行了封装。

我们在Linux平台下运行C代码时,C库函数就是对Linux系统调用接口进行的封装,在Windows平台下运行C代码时,C库函数就是对Windows系统调用接口进行的封装,这样做使得语言有了跨平台性,也方便进行二次开发。


3.1、open

下面基于系统接口来实现对文件的操作,C语言中我们要打开文件用的是fopen,但是系统接口中我们使用open函数打开文件。其函数原型如下:

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);

open的第一个参数(pathname)

第一个参数pathname代表着要打开或创建的目标文件名

  • 若pathname以路径的方式给出,则当需要创建该文件时,就在pathname路径下进行创建。
  • 若pathname以文件名的方式给出,则当需要创建该文件时,默认在当前路径下进行创建。(注意当前路径的含义)

open的第二个参数(flags)

第二个参数flags表示打开文件要传递的选项(打开文件的方式),其常用选项有如下几个:

参数选项含义
O_RDONLY以只读的方式打开文件
O_WRONLY以只写的方式打开文件
O_RDWR以读写的方式打开文件
O_CREAT若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限
O_APPEND以追加的方式打开文件
O_TRUNC把原有文件清空

补充1:

  • 实际上传入flags的每一个选项在系统当中都是以宏的方式进行定义的

  • 例如,O_RDONLY、O_WRONLY、O_RDWR、O_CREAT在系统中的宏定义如下:
#define O_RDONLY        00
#define O_WRONLY        01
#define O_RDWR          02
#define O_CREAT       0100
  • 这些宏定义选项的共同点就是,它们的二进制序列当中有且只有一个比特位是1(O_RDONLY选项的二进制序列为全0,表示O_RDONLY选项为默认选项),且为1的比特位是各不相同的,这样一来,在open函数内部就可以通过使用“与”运算来判断是否设置了某一选项。
int open(arg1, arg2, arg3){
	if (arg2&O_RDONLY){
		//设置了O_RDONLY选项
	}
	if (arg2&O_WRONLY){
		//设置了O_WRONLY选项
	}
	if (arg2&O_RDWR){
		//设置了O_RDWR选项
	}
	if (arg2&O_CREAT){
		//设置了O_CREAT选项
	}
	//...
}

补充2:

  • 打开文件时,可以传入多个参数选项,当有多个参数选项传入时,将这些选项用或 “ | ” 运算符隔开。

例如,若想以只写的方式打开文件,但当目标文件不存在时自动创建文件,则第二个参数设置如下:

O_WRONLY | O_CREAT

补充3:

  • 且系统接口open的第二个参数flags是整型,有32比特位,若将一个比特位作为一个标志位,则理论上flags可以传递32种不同的标志位,也就是说系统传递标记位,是用位图结构来进行传递的!
  • 综上:每一个宏标记,一般只需要有一个比特位是1,并且和其它宏对应的值不能重叠。

示例:(如下我自己设计的标记位,在内部做不同的事情)

#include<stdio.h>
#define PRINT_A 0x1 //0000 0001
#define PRINT_B 0x2 //0000 0010
#define PRINT_C 0x4 //0000 0100
#define PRINT_D 0x8 //0000 1000
#define PRINT_DFL 0x0
void Show(int flags)
{
    if (flags & PRINT_A)  printf("hello A\n");
    if (flags & PRINT_B)  printf("hello B\n");
    if (flags & PRINT_C)  printf("hello C\n");
    if (flags & PRINT_D)  printf("hello D\n");
    if (flags == PRINT_DFL) printf("hello Default\n");
}
int main()
{
    printf("PRINT_DFL:\n");
    Show(PRINT_DFL);
    printf("PRINT_A\n");
    Show(PRINT_A);
    printf("PRINT_B\n");
    Show(PRINT_B);
    printf("PRINT_A 和 PRINT_B\n");
    Show(PRINT_A | PRINT_B);
    printf("PRINT_C 和 PRINT_D\n");
    Show(PRINT_C | PRINT_D);
    printf("PRINT all:\n");
    Show(PRINT_A | PRINT_B | PRINT_C | PRINT_D);
    return 0;
}

下面来正式使用系统的open打开文件,并使用flags选项来做演示:

#include<stdio.h>
#include<sys/stat.h>
#include<fcntl.h>
int main()
{
	int fd = open("log.txt", O_WRONLY | O_CREAT);
	if (fd < 0)
	{
		//打开失败
		perror("open");//输出对应的错误码
		return 1;
	}
	printf("fd: %d\n", fd);
}

我们运行此程序,并用ll指令查看相关信息:

注意看这里输出的fd(open函数的返回值)是3,具体为何是3后续再谈。我们确实把一个不存在的文件(log.txt)创建好了,可是此文件的权限都是乱码,原因在于你新建一个文件,此文件要受linux权限约束的,你新建文件的权限必须得告诉我,所以当你打开一个曾经并不存在的文件,我们不能用两个参数的open,相反要用三个参数的open函数,看下文讲解: 


open的第三个参数(mode)

第三个参数mode表示创建文件的默认权限

  • 例如就上述的例子,我们把log.txt文件的mode默认权限设置为0666:

运行此程序,查看真实的权限值:

  • 怎么实际的权限值为0664呢?实际上创建出来的权限值还会收到umask(文件默认权限掩码)的影响,实际创建出来文件的权限值为:mode & (~umask)。umask的默认权限值一般为0002,当我们设置mode值为0666时实际创建出来的文件权限为0664。

若想创建出文件的权限值不受umask的影响,则需要在创建文件前使用umask函数将文件掩码设置为0。


3.2、close

C语言关闭文件使用的是fclose,系统接口使用close函数关闭文件,close函数的原型如下:

#include <unistd.h>
int close(int fd);

使用close函数时传入需要关闭文件的文件描述符即可,若关闭文件成功则返回0,关闭文件失败则返回-1。


3.3、write

在C语言中我们对文件的写入使用的是fprintf、fputs、fwrite……。在系统接口中,我们使用的是write函数向文件写入信息,write函数的原型如下:

#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);

解释其参数意义:

  • fd:特定的文件描述符
  • buf:写入缓冲区对应的字符串起始地址
  • count:写入缓冲区的大小

示例:

#include<stdio.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
int main()
{
	umask(0);//将文件权限掩码设置为0
	int fd = open("log.txt", O_WRONLY | O_CREAT, 0666);
	if (fd < 0)
	{
		//打开失败
		perror("open");//输出对应的错误码
		return 1;
	}
	printf("fd: %d\n", fd);

	int cnt = 0;
	const char* str = "hello world\n";
	while (cnt < 5)
	{
		write(fd, str, strlen(str));
		cnt++;
	}

	close(fd);
	return 0;
}

运行此程序,并用cat指令输出写入到log.txt文件的内容:


3.4、read

C语言的读取操作是fread,系统接口使用的是read函数从文件读取信息,read函数的原型如下:

#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);

解释其参数意义:

  • fd:特定的文件描述符
  • buf:写入缓冲区对应的字符串起始地址
  • count:写入缓冲区的大小

我们可以使用read函数,从文件描述符为fd的文件读取count字节的数据到buf位置当中。

  • 如果数据读取成功,实际读取数据的字节个数被返回
  • 如果数据读取失败,-1被返回

示例:

#include<stdio.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#include<string.h>
int main()
{
	umask(0);//将文件权限掩码设置为0

	int fd = open("log.txt", O_RDONLY);
	if (fd < 0)
	{
		//打开失败
		perror("open");//输出对应的错误码
		return 1;
	}
	printf("fd: %d\n", fd);

	char buffer[128];
	ssize_t s = read(fd, buffer, sizeof(buffer) - 1);
	if (s > 0)
	{
		//读取成功
		buffer[s] = '\0';
		printf("%s", buffer);
	}

	close(fd);
	return 0;
}


3.5、对比C语言文件操作与系统的文件操作

1、C中以"w"方式打开文件,是会清空原文件的

  • C语言的文件操作中,以w的方式打开文件时,是会清空原文件的,可我们使用系统接口对文件操作,按照如下的测试用例是不足以实现向C语言那样清空原文件再写入的功能:

  • 为了实现向C语言那样以w的方式写入并且先清空源文件的内容再写入新内容的操作,我们需要给open的第二个参数多家一个选项(O_TRUNC),此选项的作用就是清空原文件的内容。

  • 对比C语言完成上述功能和系统操作完成上述功能:
C语言操作:fopen("log.txt", "w");
系统操作:int id = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
  • 综上:实际上C语言以"w"的写入方式底层的open调所传入的选项就是O_WRONLY | O_CREAT | O_TRUNC。C语言只需要用到"w"即可了,但是系统方面就要传这么多选项,而且属性也要设置。

 2、C中以"a"方式打开文件,是会追加的

  • C中以"a"的方式打开文件,是以追加的方式向文本文件尾部添加数据,为了让我们的系统接口也能完成此追加操作,我们需要将open的第二个参数中的选项O_TRUNC换成O_APPEND:

  • 对比C语言完成上述功能和系统操作完成上述功能:
C语言实现:fopen("log.txt", "a");
系统操作:int fd = open("log.txt", O_WRONLY | O_CREAT | O_APPEND, 0666);
  • 综上:C语言中只用了一个"a"实现的追加功能,在系统底层,需要用到O_WRONLY | O_CREAT | O_APPEND这一组选项来完成对应的操作。

总结:实际上我们系统层面上的接口是我们C语言操作文件的底层实现。


3.6、open的返回值

open的返回值类型是int,如果打开文件成功,则返回新打开的文件描述符,若打开失败,则返回-1。

我们可以尝试一次打开多个文件,然后分别打印它们的文件描述符:

#include<stdio.h>  
#include<unistd.h>  
#include<sys/stat.h>
#include<fcntl.h> 
int main()
{
    umask(0);//将文件权限掩码设置为0
    int fda = open("loga.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
    int fdb = open("logb.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
    int fdc = open("logc.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
    int fdd = open("logd.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
    int fde = open("loge.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
    printf("fda: %d\n", fda);
    printf("fdb: %d\n", fdb);
    printf("fdc: %d\n", fdc);
    printf("fdd: %d\n", fdd);
    printf("fde: %d\n", fde);
    return 0;
}​

运行程序后可以看到,打开文件的文件描述符是从3开始连续且递增的:

问1:为什么返回的文件描述符是从3开始,0,1,2去哪了呢?

  • 其实这里的0、1、2被默认打开了,0对应的就是标准输入(键盘),1对应标准输出(显示器),2对应标准错误(显示器),我们用如下代码分别证明:

 验证读取(0):

  • 可以发现,我们用0号文件描述符是可以完成读取的,不一定非得要stdin或scanf函数等。

验证写入(1):

验证标准错误(2):

而C语言我们学过三个文件流指针:

上述一开始的fd返回值是用的系统接口open函数的,而这里stdin,stdout,stderr对应的是C语言的,而C库函数的内部调用的是系统接口,对应关系如下:

上述系统接口的文件描述符和C语言中的文件指针到底是何关系,我们得先搞清楚FILE*的含义:

  • FILE*是文件指针,FILE是C库提供的结构体,内部封装了多个成员,对文件操作而言,系统接口只认文件描述符fd,FILE内部必定封装了fd,我们用如下代码证明(_fileno就是文件指针内部结构体封装的文件描述符):

综上,再回到一开始的问题:为什么返回的文件描述符是从3开始,0,1,2去哪了呢?

  • 答案:因为Linux进程默认打开3个文件描述符,分别是标准输入0,标准输出1,标准错误2,既然0,1,2被占了,所以这就是为什么成功打开文件时所得到的文件描述符是从3开始进行分配的。

问2:为什么返回值文件描述符fd会是0,1,2,3,4,5……,其它的不可以吗?

  • 实际上这里所谓的文件描述符本质上是一个指针数组的下标,且这个下标对应的是内核的下标。我们上述调用open,write,read返回的文件描述符都是系统接口,都是操作系统提供的返回值,具体怎么个数组下标还请看下文的文件描述符fd:

4、文件描述符fd

  • 文件是由进程运行时打开的,一个进程可以打开多个文件,所以在内核中,进程 : 打开的文件 = 1 : n,而系统中又存在大量进程,也就是说,在系统中任何时刻都可能存在大量已经打开的文件。
  • OS操作系统需要对这些被打开的文件进行管理,管理方式就是先描述,再组织。所以一个文件被打开,在内核中,要创建被打开的文件的内核数据结构(先描述),即struct file结构体,其内部包含了我想看到的文件的大部分内容 + 属性,然后将这些结构体以双链表的形式链接起来,随后对被打开文件的管理,就转换成为了对链表的增删改查!

问3:进程和文件之间的对应关系是如何建立的?

  • 我们知道,当一个程序运行起来时,操作系统会将该程序的代码和数据加载到内存,然后为其创建对应的task_struct,mm_struct,页表等相关的数据结构,并通过页表建立虚拟内存和物理内存之间的关系: 

  • 而task_struct中还有一个指针(struct files_struct *files),该指针指向一个名为files_struct的结构体,在该结构体当中有一个名为fd_array的指针数组,里面存放的是struct file*的指针,该指针指向的就是被打开的文件结构,如果没有指向,那就指向NULL。该数组的下标就是我们所谓的文件描述符。此时就把进程和文件的映射关系建立好了!

  • 我们上述所画的这一坨都是在内核中实现的,因此,我们只要有某一文件的文件描述符,就可以找到与该文件相关的文件信息,从而对文件进行一系列输入输出操作。

再来解决上述一开始问的问题:为什么返回值文件描述符fd会是0,1,2,3,4,5……,其它的不可以吗?

  • 答案:本质是因为文件描述符是一个指针数组的下标,系统当中使用指针数组的方式建立进程和文件之间的对应关系,将文件描述符返回给上层用户,上层用户就可以在调用后续接口继续传入文件描述符,来索引对应的指针数组来找到对应的文件。

问4:如何理解Linux下一切皆文件?

  • 如果我们要用C语言来实现面向对象(类),只能使用struct结构体,我们采用函数指针的形式来在struct中实现函数方法:
struct file
{
    //对象的属性
    //函数指针
    void (*readp)(struct file *filep, int fd ...);
    void (*writep)(struct file *filep, int fd ...);
    ...
}
void read(struct file *filep, int fd ...)
{
    //逻辑代码
}
void write(struct file *filep, int fd ...)
{
    //逻辑代码
}
  • 在计算机里有各种硬件(键盘、显示器、磁盘、网卡……),这些设备统一称为外设(IO设备),以磁盘为例,它们都一定有对应自己的一套读写方法,不同的设备对应的读写方法一定是不一样的,如果现在要打开磁盘,那么OS就在内核给你创建一套struct file,用readp指针指向read方法,writep指针指向write方法,打开显示器等其它外设也是类似的,这一操作就是OS内的文件系统做的软件封装,再往上就是一个进程里的指针指向一结构体,该结构体内部有一个指针数组,下标就是文件描述符,其内部存放struct file*的指针,从而指向各个设备的读或写的方法。

  • 上述整个过程就是“Linux下一切皆文件”,也就是说未来你想打开一个文件,把读写方法和属性记下来,在内核里给你这个硬件创建对应的struct file,初始化时把对应的函数指针指向你具体的设备,但在内核中存在的永远都是struct file,用链表结构关联起来,所以一个进程都以统一的视角看待文件,所以我们访问不同的file指向的谁完全取决于其底层的读写方法。有点多态的感觉了。我们把上述的设计出的struct file来表示一个一个文件的叫做VFS虚拟文件系统。

问5:0,1,2对应stdin,stdout,stderr,对应的设备分别是键盘,显示器,显示器。可这些都是硬件啊,也用你上面的struct file来标识对应的文件吗?

  • 其实此问题的答案在问4(Linux下一切皆文件)已经讲解过了,当你打开一个文件,把读写方法和属性记下来,在内核里给你这个硬件创建对应的struct file,初始化时把对应的函数指针指向你具体的设备,但在内核中存在的永远都是struct file,用链表结构关联起来,所以一个进程都以统一的视角看待文件,所以我们访问不同的file指向的谁完全取决于其底层的读写方法。当然需要struct file来标识对应的文件。

5、文件描述符的分配规则

再次连续打开5个文件,看看这5个文件打开后获取到的文件描述符:

#include<stdio.h>  
#include<unistd.h>  
#include<sys/stat.h>
#include<fcntl.h> 
int main()
{
    umask(0);//将文件权限掩码设置为0
    int fda = open("loga.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
    int fdb = open("logb.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
    int fdc = open("logc.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
    int fdd = open("logd.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
    int fde = open("loge.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
    printf("fda: %d\n", fda);
    printf("fdb: %d\n", fdb);
    printf("fdc: %d\n", fdc);
    printf("fdd: %d\n", fdd);
    printf("fde: %d\n", fde);
    close(fda);
    close(fdb);
    close(fdc);
    close(fdd);
    close(fde);
    return 0;
}​

可以看到这五个文件获取到的文件描述符都是从3开始连续递增的,这很好理解,因为文件描述符本质就是数组的下标,而当进程创建时就默认打开了标准输入流、标准输出流和标准错误流,也就是说数组当中下标为0、1、2的位置已经被占用了,所以只能从3开始进行分配。

若我们在打开这5个文件之前,先关闭文件描述符为0的文件,此时文件描述符的分配又会是怎样的呢?

close(0);

可以看到,第一个打开的文件获取到的文件描述符变成了0,而之后打开文件获取到的文件描述符依旧是从3开始依次递增的:

如果我先关闭文件描述符为2的文件呢,此时文件描述符的分配又会是怎样的呢?

close(2);

可以看到,第一个打开的文件获取到的文件描述符变成了2,而之后打开文件获取到的文件描述符依旧是从3开始依次递增的:

如果我先同时关闭文件描述符为0和2的文件呢,此时文件描述符的分配又会是怎样的呢?

close(0);
close(2);

可以看到前两个打开的文件获取到的文件描述符是0和2,之后打开文件获取到的文件描述符才是从3开始依次递增的。

如果我先关闭文件描述符为1的文件呢,此时文件描述符的分配又会是怎样的呢?

close(1);

结果竟然是空的:

在给出原因前,我们先总结文件描述符的分配规则:

  • 从头遍历数组fd_array[ ],找到一个最小的,没有被使用的下标,分配给新的文件。

而上述输出结果为空的原因就是:printf是往stdout输出的,stdout对应的就是1,根据fd的分配规则,fd就是1,虽然已经不再指向对应的显示器了,但是已经指向了loga.txt的底层struct file对象!正常情况下结果应该出现在loga.txt文件里,可是我们却并不能看到:

理论上来说输出的值是在loga.txt文件里的,这里我们在close关闭文件之前指向下面的语句即可:

fflush(stdout);

至于这里为什么必须要加fflush,这就和我们之前提到过的缓冲区有关了,并且我printf应该是往显示器上输出内容的,却直接输出到了文件里,这就是重定向。具体详情下文解释。


6、重定向

重定向的原理

1、输出重定向>原理:

  • 输出重定向就是,将我们本应该输出到一个文件的数据重定向输出到另一个文件中。

如上图所示:

  • 当我close(1)后,指针数组下标1的位置就不再指向标准输出流文件了,此时使用open函数打开一个log.txt文件, 根据文件描述符的分配规则:从头遍历数组fd_array[ ],找到一个最小的,没有被使用的下标,分配给新的文件。找到的1就分配给了log.txt文件,于是乎log.txt的地址就填到1里头了,并把1返回给用户。
  • 但是我标准库里头有个stdout,就是FILE,FILE内部又封装了fd,这个fd天然就是1,上述使用的fprintf就是向stdout打印,我上面所作的内容对应stdout来说是不知道的,它只知道要向1里写入,但其实已经被狸猫换太子了,自然数据就写入上文已经调整后的log.txt新文件了。

综上,我们要进行重定向,上层只认0,1,2,3,4,5这样的fd,我们可以在OS内部,通过一定的方式调整数组的特定下标的内容(指向),我们就可以完成重定向操作了,

#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
int main()
{
	close(1);
	int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
	if (fd < 0)
	{
		perror("open");
		return 0;
	}
	//本来应该向显示器打印,最终却变成了向指定文件打印
	fprintf(stdout, "打开文件成功, fd: %d\n", fd);
	fflush(stdout);
	close(fd);
	return 0;
}

注意:C语言的数据并不是立马写到了内存操作系统里头,而是写到了C语言的缓冲区当中,所以当使用printf打印完后需要使用fflush将C语言缓冲区当中的数据刷新到文件中

2、追加重定向>>原理:

  • 追加重定向和输出重定向的唯一区别就是,输出重定向是覆盖式输出数据,而追加重定向是追加式输出数据。

例如:我们想让本应该输出到“显示器文件”的数据追加式输出到log.txt文件当中,那么我们应该先将文件描述符为1的文件关闭,然后再以追加式写入的方式打开log.txt文件,这样一来,我们就将数据追加重定向到了文件log.txt当中。

#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
int main()
{
	close(1);
	int fd = open("log.txt", O_WRONLY | O_CREAT | O_APPEND, 0666);
	if (fd < 0)
	{
		perror("open");
		return 0;
	}
	//本来应该向显示器打印,最终却变成了向指定文件打印
	fprintf(stdout, "打开文件成功, fd: %d\n", fd);
	fflush(stdout);
	close(fd);
	return 0;
}

3、输入重定向<原理: 

  • 输入重定向就是,将我们本应该从一个文件读取数据,现在重定向为从另一个文件读取数据。

例如,如果我们想让本应该从“键盘文件”读取数据的scanf函数,改从log.txt文件中读取数据,那么我们可以打开log.txt文件之前将文件描述符为0的文件关闭,也就是将“键盘文件”关闭,这样一来,当我们后续打开log.txt文件时所分配到的文件描述符就是0.

#include<stdio.h>  
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
int main()
{
    close(0);
	int fd = open("log.txt", O_RDONLY);
	if (fd < 0)
	{
		perror("open");
		return 0;
	}
	char line[64];
	while (fgets(line, sizeof line, stdin) != NULL)
	{
		printf("%s\n", line);
	}
	fflush(stdout);
	close(fd);
	return 0;
}

问:标准输出流1和标准错误流2对应的都是显示器,它们有什么区别?

来看如下的代码,代码中分别向标准输出流和标准错误流输出了几行字符串:

#include<iostream>
#include<cstdio>
using namespace std;
int main()
{
	//stdout
	printf("hello printf 1\n");
	fprintf(stdout, "hello fprintf 1\n");
	fputs("hello fputs 1\n", stdout);
	//stderr
	fprintf(stderr, "hello fprintf 2\n");
	fputs("hello puts 2\n", stderr);
	perror("hello perror 2"); //stderr                                                                                                                                               
	//cout 
	cout << "hello cout 1" << endl;
	//cerr
	cerr << "hello cerr 2" << endl;
	return 0;
}

直接运行程序,结果很显然就是在显示器上输出8行字符串:

这样看起来标准输出流和标准错误流并没有区别,都是向显示器输出数据。但我们若是将程序运行结果重定向输出到文件log.txt当中,我们会发现log.txt文件当中只有向标准输出流输出的四行字符串,而向标准错误流输出的两行数据并没有重定向到文件当中,而是仍然输出到了显示器上。 

实际上我们使用重定向时,默认重定向的是文件描述符是1的标准输出流,而并不会对文件描述符是2的标准错误流进行重定向。 如果我现在也想重定向文件描述符为2的标准错误流呢?看如下的指令:

[xzy@ecs-333953 date20]$ ./myfile > stdout.txt 2>stderr.txt

上述指令做了两次重定向,第一次把标准输出重定向到了文件描述符为1的显示器,第二次是把标准错误重定向到了文件描述符为2的显示器,上述把标准输出和标准错误区分开的意义是可以区分日常程序中哪些是输出,哪些是错误,我们需要对错误信息做区分。

前面已经提到,重定向只会默认把标准输出的进行处理,并不会重定向标准错误,如果我想让标准输出和标准错误一同混合起来到一个文件显示,看如下指令:

[xzy@ecs-333953 date20]$ ./myfile > all.txt 2>&1

此时会发现此指令让标准输出和标准错误输出到了同一个文件,画图解释:

  • 上述指令让本来应该指向1的内容全部写到all.txt文件,随后把1里的内容拷贝到2里,再把2的内容写到all.txt文件。

补充:perror 

  • perror内部套用了errno(全局变量),目的是记录最近一次C库函数调用失败的原因。下面使用库函数的perror和自己模拟实现的my_perror分别测试一次:

库函数的perror:

自己实现的my_perror:


dup2

在Linux操作系统中提供了系统接口dup2,我们可以使用该函数完成重定向。dup2的函数原型如下:

函数原型:int dup2(int oldfd, int newfd);
功能:dup2() makes newfd be the copy of oldfd, closing newfd first if necessary

函数功能:dup2会将fd_array[oldfd]的内容拷贝到fd_array[newold]当中,如果有必要的话我们需要先使用关闭文件描述符为newfd的文件。

函数返回值:dup2如果调用成功,返回newfd,否则返回-1。

注意:

  1. 如果oldfd不是有效的文件描述符,则dup2调用失败,并且此时文件描述符为newfd的文件没有被关闭。
  2. 如果oldfd是一个有效的文件描述符,但是newfd和oldfd具有相同的值,则dup2不做任何操作,并返回newfd。

补充:

  • 上述的拷贝是把旧的文件描述符下标oldfd的内容拷贝到新的文件描述符下标newfd,并非拷贝数字,最终只剩oldfd下标对应的内容。

示例1:输出重定向>

  • 抛去上文的close(1),这里我们新打开的文件log.txt分配的文件描述符理应为3,如下图所示:

我本来是向显示器打印的,现在想输出重定向到log.txt上, 也就是把log.txt的文件描述符3的内容拷贝到stdout文件描述符1里头去。

代码如下:

#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
int main()
{
	int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
	if (fd < 0)
	{
		perror("open");
		return 0;
	}
	int ret = dup2(fd, 1);
	if (ret > 0)  close(fd);//关闭旧的
	printf("ret: %d\n", ret);//1
	//本来应该向显示器打印,最终却变成了向指定文件打印
	fprintf(stdout, "打开文件成功, fd: %d\n", fd);
	fflush(stdout);
	close(fd);
	return 0;
}

示例2:追加重定向>>

  • 追加重定向仅仅是在打开文件的方式发生了改变,由原先的O_TRUNC变成了O_APPEND,其它的和输出重定向完全一样:
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
int main()
{
	int fd = open("log.txt", O_WRONLY | O_CREAT | O_APPEND, 0666);
	if (fd < 0)
	{
		perror("open");
		return 0;
	}
	dup2(fd, 1);
	//本来应该向显示器打印,最终却变成了向指定文件打印
	fprintf(stdout, "打开文件成功, fd: %d\n", fd);
	fflush(stdout);
	close(fd);
	return 0;
}

示例3:输入重定向<

  • 输入重定向就是把从键盘读取数据改为重定向从文件读取数据,如下代码示例:
#include<stdio.h>  
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
int main()
{
	int fd = open("log.txt", O_RDONLY);
	if (fd < 0)
	{
		perror("open");
		return 0;
	}
	dup2(fd, 0);
	char line[64];
	while (fgets(line, sizeof line, stdin) != NULL)
	{
		printf("%s\n", line);
	}
	fflush(stdout);
	close(fd);
	return 0;
}


7、FILE

FILE当中的文件描述符

因为库函数是对系统调用接口的封装,本质上访问文件都是通过文件描述符fd进行访问的,所以C库当中的FILE结构体内部必定封装了文件描述符fd。

  • 首先,我们在/usr/include/stdio.h头文件中可以看到下面这句代码,也就是说FILE实际上就是struct _IO_FILE结构体的一个别名。
typedef struct _IO_FILE FILE;

而我们在/usr/include/libio.h头文件中可以找到struct _IO_FILE结构体的定义,在该结构体的众多成员当中,我们可以看到一个名为_fileno的成员,这个成员实际上就是封装的文件描述符。

struct _IO_FILE {
	int _flags;       /* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags

	//缓冲区相关
	/* The following pointers correspond to the C++ streambuf protocol. */
	/* Note:  Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
	char* _IO_read_ptr;   /* Current read pointer */
	char* _IO_read_end;   /* End of get area. */
	char* _IO_read_base;  /* Start of putback+get area. */
	char* _IO_write_base; /* Start of put area. */
	char* _IO_write_ptr;  /* Current put pointer. */
	char* _IO_write_end;  /* End of put area. */
	char* _IO_buf_base;   /* Start of reserve area. */
	char* _IO_buf_end;    /* End of reserve area. */
	/* The following fields are used to support backing up and undo. */
	char *_IO_save_base; /* Pointer to start of non-current get area. */
	char *_IO_backup_base;  /* Pointer to first valid character of backup area */
	char *_IO_save_end; /* Pointer to end of non-current get area. */

	struct _IO_marker *_markers;

	struct _IO_FILE *_chain;

	int _fileno; //封装的文件描述符
#if 0
	int _blksize;
#else
	int _flags2;
#endif
	_IO_off_t _old_offset; /* This used to be _offset but it's too small.  */

#define __HAVE_COLUMN /* temporary */
	/* 1+column number of pbase(); 0 is unknown. */
	unsigned short _cur_column;
	signed char _vtable_offset;
	char _shortbuf[1];

	/*  char* _save_gptr;  char* _save_egptr; */

	_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};

现在我们再来理解一下C语言当中的fopen函数究竟在做什么?

  • fopen函数在上层为用户申请FILE结构体变量,并返回该结构体的地址(FILE*),在底层通过系统接口open打开对应的文件,得到文件描述符fd,并把fd填充到FILE结构体当中的_fileno变量中,至此便完成了文件的打开操作。
  • 而C语言当中的其他文件操作函数,比如fread、fwrite、fputs、fgets等,都是先根据我们传入的文件指针找到对应的FILE结构体,然后在FILE结构体当中找到文件描述符,最后通过文件描述符对文件进行的一系列操作。

FILE当中的缓冲区

1、什么是缓冲区?

  • 缓冲区的本质:就是一段内存

2、为什么要有缓冲区?

假如你身处南京,想把一本书赠送给你远在北京的朋友,此时有两种方法:

  1. 自己送过去(做公交车,乘飞机,打车……)。这种方法是可行的,不过最大的问题在于其会耽误你自己的时间。
  2. 通过顺丰快递把物品寄过去,此法的最大便利之处就在于解放你的时间。

法二的好处1、解放你自己的时间:

  • 上述顺丰所处的作用就是缓冲区,要寄过去的书就是数据,你就是一个进程,你(进程)把书(数据)交给顺丰(缓冲区)实际上是解放你(进程)的时间。你(进程)就可以继续向后执行后续的代码。

法二的好处2、集中处理数据:

  • 当你后续零零散散还需要寄好几本书的时候,顺丰不会说我每收到一本书就寄过去,顺丰一定得是积累到一定的量(假设500本)才开始整体运输,那么此时我运输500本书的时间就等同于运输1本的时间。

下面总结缓冲区的作用:

  1. 解放使用缓冲区的进程时间。
  2. 缓冲区的存在可以集中处理数据刷新,减少IO的次数,从而达到提高整机的效率的目的。

3、缓冲区在哪里?

用户层和操作系统内部都有缓冲区,但具体是哪一种呢,我们使用代码验证:

上述代码运行后,hello printf和hello write均能同时刷新出来,现在修改代码,删去源代码中的换行符'\n'。

此时会发现hello write立马刷新出来了,而hello printf则等了5s才刷新出来,这里去掉\n的hello printf之所以没有立马刷新出来,是因为有缓冲区的存在,导致数据没有被刷新出来,当我们假设fflush时就可以将hello printf立马刷新出来了,但是我write可是立即刷新的,并且printf函数内部封装了write,那么这个缓冲区在哪里呢?

  • 首先,write无论有无\n,都能立即刷新,所以这个缓冲区一定不在write内部,而write是系统调用的,因此我们曾经谈论的缓冲区,不是内核级别的,那么这个缓冲区只能是C语言提供的,这是语言级别的缓冲区。

再看如下的代码:

如上我又添加了几个测试函数,上述fprintf、fputs、printf内部都封装了wirte,加上\n,都能同时刷新出数据,现在,删去上述的\n再来观察看:

  • 此时依旧是系统接口的write函数先刷新出来的,等待进程退出的时候,fprintf、fputs、printf才刷新出来,并且这三个函数内部均封装了write函数,也就是说只要调用了write函数,数据就一定能立马刷新出来,没有立马刷新是因为没有立即调用write,而是在缓冲区(语言级别的)等待。
  • 上述fprintf、fputs、printf三个函数是C语言提供的,都有一个stdout参数,stdout是FILE*类型的,而FILE*是一个结构体,该结构体内部封装了很多成员属性,其中一个是fd文件描述符,还有一个是该FILE对应的语言级别的缓冲区!
//缓冲区相关
	/* The following pointers correspond to the C++ streambuf protocol. */
	/* Note:  Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
char* _IO_read_ptr;   /* Current read pointer */
char* _IO_read_end;   /* End of get area. */
char* _IO_read_base;  /* Start of putback+get area. */
char* _IO_write_base; /* Start of put area. */
char* _IO_write_ptr;  /* Current put pointer. */
char* _IO_write_end;  /* End of put area. */
char* _IO_buf_base;   /* Start of reserve area. */
char* _IO_buf_end;    /* End of reserve area. */
/* The following fields are used to support backing up and undo. */
char* _IO_save_base; /* Pointer to start of non-current get area. */
char* _IO_backup_base;  /* Pointer to first valid character of backup area */
char* _IO_save_end; /* Pointer to end of non-current get area. */
int _fileno; //封装的文件描述符

看如下的简图:

  • 当我们使用printf、fprintf、fputs函数把数据写入时,并不是直接调用write,而是先把数据写到cache缓冲区里头,当数据量积累到一定程度,它会定期的通过fd文件描述符去调用write函数,从而刷新到内存中。此过程就相当于上文顺丰寄快递的例子。

问1:如果在刷新之前关闭了fd,会有什么问题?

  • 看如下代码:

  • 在sleep期间,数据没有被刷新出来,只能在stdout对应的缓冲区里头,当进程退出时,我想刷新了,可是你把文件描述符fd给关掉了,所以后续调用write函数就失败了,也就刷新不出来了。 

问2:如何理解语言级别缓冲区?

  • 既然缓冲区在FILE内部,在C语言中,我们每打开一个文件,都要有一个FILE*会返回,这就意味着每一个文件都有一个fd和属于它自己的语言级别缓冲区!

问3:如何理解定期的通过fd调用write函数从而刷新到内存,即刷新策略的问题?

刷新策略有三个常规情况,两个特殊情况。

常规:

  1. 无缓冲(立即刷新)
  2. 行缓冲(逐行刷新) ——  常见的对显示器进行刷新数据
  3. 全缓冲(缓冲区满,刷新)——  常见的对磁盘文件写入数据

特殊:

  1. 进程退出,强制刷新
  2. 用户强制刷新

看如下的代码:

#include<stdio.h>
#include<string.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
int main()
{
	const char* str1 = "hello printf\n";
	const char* str2 = "hello fprintf\n";
	const char* str3 = "hello fputs\n";
	const char* str4 = "hello write\n";
	//C库函数
	printf(str1);
	fprintf(stdout, str2);
	fputs(str3, stdout);
	//系统接口
	write(1, str4, strlen(str4));
	//是调用完了上面的代码,才执行的fork
	fork();
	return 0;
}

运行该程序,我们可以看到printf、fputs和write函数都成功将对应内容输出到了显示器上:

但是,当我们将程序的结果重定向到log.txt文件当中后,我们发现文件当中的内容与我们直接打印输出到显示器的内容是不一样的:

为什么C库函数打印的内容重定向到文件后就变成了两份,而系统接口打印的内容还是原来的一份呢?

  • 首先,当我们直接执行可执行程序,将数据打印到显示器时所采用的就是行缓冲,因为代码当中每句话后面都有\n,所以当我们执行完对应代码后就立即将数据刷新到了显示器上。
  • 而后当我们将运行的结果重定向到log.txt文件时,数据的刷新策略就变为了全缓冲,此时我们使用printf、fprintf、fputs函数打印的数据都打印到了C语言自带的缓冲区中,之后当我们使用fork函数创建子进程时,由于进程间具有独立性,fork之后,父子进程就要退出了,此时父进程或是子进程对要刷新缓冲区内容时,本质就是对父子进程共享的数据进行了修改,此时就需要对数据进行写时拷贝,至此缓冲区当中的数据就变成了两份,一份父进程的,一份子进程的,所以重定向到log.txt文件当中printf、fprintf、fputs函数打印的数据就有两份,但由于write函数是系统接口,我们可以将write函数看作是没有缓冲区的,因此write函数打印的数据就只打印了一份。
  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

三分苦

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值