前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >系统文件IO/文件描述符/重定向/FILE/缓冲区的理解

系统文件IO/文件描述符/重定向/FILE/缓冲区的理解

作者头像
二肥是只大懒蓝猫
发布2023-03-30 14:35:25
7870
发布2023-03-30 14:35:25
举报
文章被收录于专栏:热爱C嘎嘎

本文目标: 认识文件相关系统调用接口 认识文件描述符,理解重定向 对比fd和FILE,理解系统调用和库函数的关系

来来来,学起来!动起来!热爱计算机的我们必然可以克服种种困难去达成我们的目标!

谈文件:

对于文件,有以下共识:

①空文件,也是要在磁盘中占据空间 ②文件 = 内容 + 属性 ③文件操作 =  对文件内容的操作 + 对文件属性的操作 ④标定一个问题,必须是文件路径+文件名【唯一性】 ⑤如果没有指明路径,那么默认是在当前路径(进程当前路径)进行文件操作 ⑥当我们在C语言中,把fopen、fclose、fread、fwrite等接口写完后,然后通过代码编译、形成二进制可执行程序之后,但是没有运行程序,此时的文件并没有被操作起来,这意味着,对文件的操作,其本质是进程对文件的操作!因为程序运行起来,创建了进程,文件才被操作。 ⑦文件要被访问,那就必须被打开。是被用户进程和OS一起打开的,用户进程负责对接口的调用,OS则是负责对文件的打开。并且,虽然文件要被打开才能被访问,但是磁盘上的文件并不是全都被打开的,因此文件我们可以狭隘地分被打开的和未被打开的两种文件。 结合以上,我们得出结论:文件操作的本质就是进程与被打开的文件的关系!我们研究文件操作,就是在研究两者的关系!

对于文件操作系统,C语言有,C++有,Java等等的计算机语言都有,虽然它们的接口不一样,但是,它们的底层,都是调用了操作系统提供的文件操作的接口。因为对于文件来说,文件是放在磁盘的,而磁盘是硬件,只有操作系统有资格去访问硬件,因此要对文件进行操作,就必须通过OS,OS提供系统级别的系统调用接口,而操作系统只有一个,因此,底层就是相同的啦!

也就是说:不管上层语言如何变化,库函数底层必须调用系统调用接口。

系统文件IO:

操作文件,除了调用计算机语言:C,C++等待的库函数接口以外,那就是可以调用系统接口。

接口介绍:

open:

代码语言:javascript
复制
#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);

pathname: 要打开或创建的目标文件,就跟C语言的一样,选择路径,默认当前路径
flags: 打开文件时,可以传入多个参数选项,用下面的一个或者多个常量进行“或”运算,构成flags。
mode:权限,就是创建的文件的权限是啥,得告诉接口函数

参数:
O_RDONLY: 只读打开
O_WRONLY: 只写打开
O_RDWR : 读,写打开
这三个常量,必须指定一个且只能指定一个
O_CREAT : 若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限
O_APPEND: 追加写
O_TRUNC:清空文件内容

返回值:
成功:新打开的文件描述符
失败:-1

对于flags的参数选项,是OS通过比特位来传递选项的,看下面代码:

代码语言:javascript
复制
//每一个宏,对应的数值,只有一个比特位是1,彼此的位置不重叠
#define ONE (1<<0)  //0x1   0001
#define TWO (1<<1)   //0x2  0010
#define THREE (1<<2)  //0x4  0100
#define FOUR (1<<3)  //0x8   1000

void show(int flags)
{
	if (flags & ONE)printf("one\n");
	if (flags & TWO)printf("two\n");
	if (flags & THREE)printf("three\n");
	if (flags & FOUR)printf("four\n");
}

int main()
{
	show(ONE);
	show(TWO);
	show(ONE | TWO);
	show(ONE | TWO | THREE);
	show(ONE | TWO | THREE | FOUR);
	return 0;
}

因为每个比特位都对应这一个选项,而且是不能重叠的,因此,选项对应的比特位都是单独一个1.不能出现3(0011)这样的值。这里也就解释了关于flags参数的使用方法了。

下面是open接口、write接口和read接口的使用:

三个参数的:

以写的方式:

O_WRONLY:只写打开,但是在没有文件存在的时候,会打开失败,可以或上O_CREAT,默认权限为666。对应关闭文件的是close。

代码语言:javascript
复制
int main()
{

	int fd = open(FILE_NAME, O_WRONLY | O_CREAT, 0666);
	if (fd < 0)
	{
		perror("fd faile");
		return 1;
	}
	close(fd);
	return 0;
}

两个参数的:

代码语言:javascript
复制
int fd = open(FILE_NAME, O_WRONLY);

write:

代码语言:javascript
复制
#include<unistd.h>
size_t write(int fd, const void* buf, size_t count);

fd:想要往哪个文件写
buf:缓冲区对应的数据
count:缓冲区的字节个数

返回值:写了的字节个数

 write接口比较简单粗暴,buf的类型是const void*,因为对于文件,它的文本类跟二进制类都是语言本身提供的分类。但是对于操作系统来说,它们都是二进制的。

代码如下:

代码语言:javascript
复制
int main()
{

	int fd = open(FILE_NAME, O_WRONLY | O_CREAT, 0666);
	if (fd < 0)
	{
		perror("fd faile");
		return 1;
	}

	char outbuff[64];
	int cnt = 5;
	while (cnt)
	{
		sprintf(outbuff, "%s:%d\n", "hello Linux", cnt--);
		write(fd, outbuff, strlen(outbuff));
	}
	close(fd);
	return 0;
}

这里的strlen(outbuff)不能减一,不要想着里面的'\0'。对于C语言有规定'\0'作为字符串的结尾,但是这关我文件什么事?我文件的内容结尾又不需要'\0'作结尾。所以不要加1,如果加1了,我们的文件内容就不是我们预期的那样子了。

 当我们将代码修改一下:hello Linux改成aaaa,然后执行程序。

代码语言:javascript
复制
sprintf(outbuff, "%s:%d\n", "aaaa", cnt--);

 这样会导致结果出现异常,其原因是我们没有清空原来的文件内容,所以需要加上O_TRUNC.

代码语言:javascript
复制
 int fd = open(FILE_NAME,O_WRONLY | O_CREAT | O_TRUNC,0666);   

所以,当我们在写C语言的文件操作的代码的时候,我们写入"w",其实就是在调用了open,然后操作系统自动给我们传入了FILE_NAME, O_WRONLY | O_CREAT | O_TRUNC, 0666等等的参数!这就是C语言文件接口和系统调用接口的关系。

对于追加,C语言中的"a",也是这里的O_APPEND

代码语言:javascript
复制
 int fd = open(FILE_NAME,O_WRONLY | O_CREAT | O_APPEND,0666);  

open以读的方式:

需用用到read接口:

代码语言:javascript
复制
#include<unistd.h>
ssize_t read(int fd, void* buf, size_t count);
ssize_t:系统定制的类型:有符号整数,可以大于0,等于0,小于0.
fd:期望读的文件
buf:读去的缓冲区,它的void*也是一样,表示读过去的文件类型不管是什么类型,来到这里都是二进制
count:读的字节个数

返回值:成功,就返回读到的字节个数
代码语言:javascript
复制
 40     int fd = open(FILE_NAME,O_RDONLY);                                                                                                   
 41     if(fd<0)                                                                                                                             
 42     {                                                                                                                                    
 43         perror("fd faile");                                                                                                              
 44         return 1;                                                                                                                        
 45     }                                                                                                                                    
 46                                                                                                                                          
 47     char buffer[1024];                                                                                                                   
 48     ssize_t num = read(fd,buffer,sizeof(buffer)-1);//因为结尾是'\0',不读'\0'                                                            
 49     if(num > 0)                                                                                                                          
 50     {                                                                                                                                    
 51         buffer[num] = 0;// 当文件中的数据读取到了buffer中,然而buffer和打印的函数是C语言接口,需要加上0;num > 0表示读取成功,是读取的字节个数   
 52     }                                                                                                                   
 53     printf("%s",buffer);  

如上:系统调用的文件操作接口:open、close、write、read.当然,还有lseek。

对应的C语言的接口:

系统调用

C语言库函数接口

open

foen

close

fclose

write

fwrite

read

fread

lseek

flseek

文件描述符fd:

文件操作的本质,是进程和被打开文件的关系,而进程是可以打开多个文件,那么系统中就一定会存在大量被打开的文件,这些文件都需要OS通过内核数据结构struct_file{}来进行标识文件,来管理文件。那么进程和这些被打开的文件之间的关系是通过文件描述符来维护的。

来看看文件描述符fd:

通过open接口,其返回值就是文件描述符fd。创建五个文件,分别返回其描述符,得到的结果是3,4,5,6,7.

我们看到,它是从3开始的,那么0,1,2呢?

我们使用C语言写文件的时候,FILE其实是一个结构体,因为库函数中的fopen调用的系统接口open,返回的是fd,那么FILE结构体里面必有一个字段,那就是文件描述符!因此,我们可以使用FILE结构体的字段,将标准输入输出流的文件描述符打印出来,就可以知道了:0,1,2对应的物理设备一般是:键盘(stdin),显示器(stdout),显示器(stderr),也就是0表示标准输入, 1表示标准输出, 2表示标准错误。所以,从3开始的原因就是0,1,2被占用了。

那么为什么是从0开始,0,1,2,3,4...这样的顺序呢?

 如上图,由于当一个文件被加载到内存时,会有许多个被打开的文件存在,这是负责打开这个文件的进程一看,那么多文件,选谁好?此时就会创建一个结构体,里面有一个指针数组,用来存放这些文件,而这个数组,也称为文件描述符表。此时,当我们需要打开一个文件的时候,会通过这个数组来访问它,并且返回这个数组的下标,这个下标就是文件描述符!所以,文件描述符的本质,就是一个数组的下标。

文件描述符的分配规则

如果将文件描述符为0,1,2的文件关掉,然后新建一个文件,并打印它的文件描述符,那么此时它的fd又是如何的呢?请看下面代码:

首先,没有关闭0,1,2任一文件时,fd为3

代码语言:javascript
复制
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
int main()
{

	int fd = open("test.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
	if (fd < 0)
	{
		perror("open");
		return 1;
	}
	printf("fd %d\n", fd);

	close(fd);
	return 0;
}

 关闭0:即在新创建文件或打开文件前,先关闭文件描述符为0的文件:

代码语言:javascript
复制
    close(0);
    int fd = open("test.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
    //......

此时的结果:

 关闭2:此时的结果是fd为2。如果把0和2同时关掉,那么fd为0。

因此:文件描述符的分配规则:在files_struct数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符

重定向:

代码语言:javascript
复制
    close(1);
    int fd = open("test.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
    //......

关闭1:什么都没显示,即不会显示预期中的1。那是因为我们关掉的1,是标准输出的文件描述符,是固定的,然后创建出来的文件会被存进下标为1的数组空间中!那么此时,fd为1,是这个文件的文件描述符了,但是标准输出的文件描述符依然是1,只不过在下标为1的这个空间里,变成了新建的那个文件。并且,我们打开文件一看,本来应该打印出来的那个fd为1的信息,此时是被写到了文件里面去了!这种特性,就叫做重定向!

当子进程重定向后,是不会影响到父进程的,因为进程具有独立性

重定向的本质就是长层的fd不变,在内核中修改fd对于的struct file*的地址。

 dup2系统调用:

重定向的功能可以用dup2接口来实现。

代码语言:javascript
复制
#include<unistd.h>

int dup2(int oldfd, int newfd);
函数功能为将newfd描述符重定向到oldfd描述符,相当于重定向完毕后都是操作oldfd所操作的文件
但是在过程中如果newfd本身已经有对应打开的文件信息,则会先关闭文件后再重定向(否则会资源泄露)


返回值:成功,就会返回新的文件描述符,也就是fd。失败,返回-1

看下面代码:

代码语言:javascript
复制
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
int main()
{
	int fd = open("test.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
	if (fd < 0)
	{
		perror("open");
		return 1;
	}

	dup2(fd, 1);//将fd中的文件拷贝到文件描述符为1数组空间中

	printf("fd %d\n", fd);
	close(fd);
	return 0;
}

结果如下:

对于重定向,可以操作的由追加重定向,输入重定向,输出重定向。

追加重定向:

重定向的同时,追加文本内容:

代码语言:javascript
复制
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
int main()
{

	//close(0);
	int fd = open("test.txt", O_WRONLY | O_CREAT | O_APPEND, 0666);
	if (fd < 0)
	{
		perror("open");
		return 1;
	}
	dup2(fd, 1);
	printf("fd %d\n", fd);

	const char* msg = "hello Linux";
	write(1, msg, strlen(msg));//写入标准输出
	close(fd);
	return 0;
}

 输入重定向:

本来是从stdin(0)上输入的内容,变为从指定文件上输入。

代码如下:

代码语言:javascript
复制
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
int main()
{

	//close(0);
	int fd = open("test.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
	if (fd < 0)
	{
		perror("open");
		return 1;
	}

	dup2(fd, 0);

	char line[64];
	while (1)
	{
		printf(">");
		if (fgets(line, sizeof(line), stdin) == NULL)break;
		printf("%s", line);
	}

	return 0;
}

若没有重定向,没有使用dup2(fd,0);的指令,那么就是从键盘(标准输入)输入,并打印

 但在重定向后,便从指定文件中获取信息并打印。

 常见的重定向有:> >> <:

这些重定向指令在命令行上使用。>为输出重定向,<为输入重定向,>>为追加重定向。

下面,我将模拟实现简易版的shell,并且是添加了重定向功能的!代码如下:

代码语言:javascript
复制
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <ctype.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/wait.h>
#include <assert.h>
#include <errno.h>

#define NUM 1024
#define OPT_NUM 64

#define NONE_REDIR   0  //无
#define INPUT_REDIR  1  //输出
#define OUTPUT_REDIR 2  //输出
#define APPEND_REDIR  3 //出错

#define trimSpace(start) do{\
            while(isspace(*start)) ++start;\
        }while(0)

char lineCommand[NUM];
char* myargv[OPT_NUM]; //指针数组
int  lastCode = 0;
int  lastSig = 0;

int redirType = NONE_REDIR;
char* redirFile = NULL;

// "ls -a -l -i > myfile.txt" -> "ls -a -l -i" "myfile.txt" ->
void commandCheck(char* commands)
{
    assert(commands);
    char* start = commands;
    char* end = commands + strlen(commands);

    while (start < end)
    {
        if (*start == '>')
        {
            *start = '\0';
            start++;
            if (*start == '>')
            {
                // "ls -a >> file.log"
                redirType = APPEND_REDIR;
                start++;
            }
            else
            {
                // "ls -a >    file.log"
                redirType = OUTPUT_REDIR;
            }
            trimSpace(start);
            redirFile = start;
            break;
        }
        else if (*start == '<')
        {
            //"cat <      file.txt"
            *start = '\0';
            start++;
            trimSpace(start);
            // 填写重定向信息
            redirType = INPUT_REDIR;
            redirFile = start;
            break;
        }
        else
        {
            start++;
        }
    }
}


int main()
{
    while (1)
    {
        redirType = NONE_REDIR;
        redirFile = NULL;
        errno = 0;

        // 输出提示符
        printf("用户名@主机名 当前路径# ");
        fflush(stdout);

        // 获取用户输入, 输入的时候,输入\n
        char* s = fgets(lineCommand, sizeof(lineCommand) - 1, stdin);
        assert(s != NULL);
        (void)s;
        // 清除最后一个\n , abcd\n
        lineCommand[strlen(lineCommand) - 1] = 0; // ?

        // "ls -a -l -i >> myfile.txt" -> "ls -a -l -i" "myfile.txt" ->

        commandCheck(lineCommand);
        // 字符串切割
        myargv[0] = strtok(lineCommand, " ");
        int i = 1;
        if (myargv[0] != NULL && strcmp(myargv[0], "ls") == 0)
        {
            myargv[i++] = (char*)"--color=auto";
        }

        // 如果没有子串了,strtok->NULL, myargv[end] = NULL
        while (myargv[i++] = strtok(NULL, " "));

        // 如果是cd命令,不需要创建子进程,让shell自己执行对应的命令,本质就是执行系统接口
        // 像这种不需要让我们的子进程来执行,而是让shell自己执行的命令 --- 内建/内置命令
        if (myargv[0] != NULL && strcmp(myargv[0], "cd") == 0)
        {
            if (myargv[1] != NULL) chdir(myargv[1]);
            continue;
        }
        if (myargv[0] != NULL && myargv[1] != NULL && strcmp(myargv[0], "echo") == 0)
        {
            if (strcmp(myargv[1], "$?") == 0)
            {
                printf("%d, %d\n", lastCode, lastSig);
            }
            else
            {
                printf("%s\n", myargv[1]);
            }
            continue;
        }
        // 测试是否成功, 条件编译
#ifdef DEBUG
        for (int i = 0; myargv[i]; i++)
        {
            printf("myargv[%d]: %s\n", i, myargv[i]);
        }
#endif
        // 内建命令 --> echo

        // 执行命令
        pid_t id = fork();
        assert(id != -1);

        if (id == 0)
        {
            // 因为命令是子进程执行的,真正重定向的工作一定要是子进程来完成
            // 如何重定向,是父进程要给子进程提供信息的
            // 这里重定向不会影响父进程,进程具有独立性
            switch (redirType)
            {
            case NONE_REDIR:
                // 什么都不做
                break;
            case INPUT_REDIR:
            {
                int fd = open(redirFile, O_RDONLY);
                if (fd < 0) {
                    perror("open");
                    exit(errno);
                }
                // 重定向的文件已经成功打开了
                dup2(fd, 0);
            }
            break;
            case OUTPUT_REDIR:
            case APPEND_REDIR:
            {
                umask(0);
                int flags = O_WRONLY | O_CREAT;
                if (redirType == APPEND_REDIR) flags |= O_APPEND;
                else flags |= O_TRUNC;
                int fd = open(redirFile, flags, 0666);
                if (fd < 0)
                {
                    perror("open");
                    exit(errno);
                }
                dup2(fd, 1);
            }
            break;
            default:
                printf("bug?\n");
                break;
            }

            execvp(myargv[0], myargv); // 执行程序替换的时候,不会影响曾经进程打开的重定向的文件
            exit(1);
        }
        int status = 0;
        pid_t ret = waitpid(id, &status, 0);
        assert(ret > 0);
        (void)ret;
        lastCode = ((status >> 8) & 0xFF);
        lastSig = (status & 0x7F);
    }
}

于是,我们可以像是正常地在命令行上写入指令。

理解Linux下一切皆文件   

在冯诺依曼体系中,硬件都属于外设,对于外设的数据处理,都是先把数据读到内存,当处理完后,再将数据刷新到外设中,这就称作IO。

而对于硬件来说,是操作系统来管理它们,操作系统为了方便管理,就会对不同的外设硬件创建出对应的结构体,每一个结构体里面包含了相应的属性信息,都有属于自己的读写方法,这就意味着每种硬件的方法方法是一定不一样的。可是怎么来表示这些硬件的方法呢?

Linux做了一件事,就是提取硬件的属性,创建一个结构体struct file(){}来访问硬件,并且这个结构体里面拥有读写的方法的函数指针,最后将每种硬件对应的结构体连起来,组织起来,当需要调用硬件的方法的时候,只需要去结构体里面找,然后调用方法即可。于是,站在struct file上层去看,所有的文件和设备,统一都是struct file,即内核数据结构,这部分也称为虚拟文件(vfs)。 这就是所谓的Linux一切皆文件!

FILE/缓冲区问题

上面我们提到,因为IO相关函数与系统调用接口对应,并且库函数封装系统调用,所以本质上,访问文件都是通过fd访问的。所以C库当中的FILE结构体内部,必定封装了fd。

先看下面代码及其现象:

代码语言:javascript
复制
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#include<string.h>

int main()
{
	//C接口                                                                                                                                    
	printf("hello printf\n");
	fprintf(stdout, "hello fprintf\n");
	fputs("hello fputs\n", stdout);

	// system call                                                                                                                             
	const char* msg = "hello write\n";
	write(1, msg, strlen(msg));
    
    //fork();
	return 0;
}

 运行了代码之和,结果和预期中的一样,四个打印都出来了。

接着,我们重定向输入到文件中,那么,文件里面的内容,也跟预期中的一样,是这四个打印的内容。

 然而,当我们把代码中被注释掉的fork()放出来,按照同样的测试,不重定向的话,打印出来的结果,也是这四个,但是一旦重定向,就会有以下现象:

 凡是C语言接口的,都被打印了两次!看着以上的操作,可以判断是fork()函数带来的原因。

第一:如果printf调用成功,即信息输入到了显示器上,就说明是写到外设上了,就不属于这个父进程了,但如果数据没有被写到显示器上,此时这个数据依旧属于这个父进程。

第二:如果printf调用成功,数据不一定会到了显示器上,也就是在冯诺依曼体系中,没有从内存到外设这一步,因此依旧属于当前进程,然后当调用fork(),最后进程结束,需要刷新缓冲区,将数据从内存刷新到外设。

理解缓冲区问题:

缓冲区的本质就是一段内存,那么就会有以下问题:

这段内存是谁申请的? 这段内存是属于谁的? 这段内存存在的意义?

缓冲区就是相当于现实生活中的快递公司,有了快递公司,当我们想把某样东西送给远方的亲戚朋友时,就不需要我们自己还得坐飞机坐火车那样花时间送过去了,只需要把东西交给快递公司,快递公司就会帮我们送到。所以说快递存在的意义就是节省我们的时间。

同理,缓冲区就是如此,进程在与外设进行数据处理的时候,会通过缓冲区来进行数据的交流,即将数据从内存拷贝到缓冲区中,从而达到节省进程进行数据IO的时间!这就是缓冲区存在的意义!

 数据从内存拷贝到缓冲区时,通过fwrite函数来进行拷贝,因此我们与其将fwrite函数理解为是写入到文件的函数,倒不如说它是拷贝函数,将数据从进程进行拷贝到缓冲区或外设。

缓冲区刷新策略:

缓冲区会结合具体的设备,定制相应的刷新策略:

a.立即刷新,这种是无缓冲,拿到数据后直接刷新                 :比如说fflush();刷新缓冲区,直接刷新。 b.行刷新,也就是行缓冲,显示器就是这种。                 :因为显示器是给人看的,正常来说,我们都是从左向右一行一行地看。而刷新效率是一下子全刷新的话,效率是最高的。因此在保证刷新效率的同时,也要保证体验效果,就有了行缓冲。 c.缓冲区满,也就是全缓冲,对应的是磁盘文件等等。                 :全缓冲的效率是最高的,因为等缓冲区满了之和,一下子全刷新,IO的时间只需一次。 另外两种刷新情况: 1.进程退出后,缓冲区一般都要刷新 2.用户强制刷新

缓冲区在哪?指的是什么缓冲区?

看回上面的代码,我们发现程序执行后,打印出来是4次,但是如果输入到文件中,发现是7次,而且凡是C语言的接口所打印的,都多了1次。那么这里说明了,这种现象一定是跟缓冲区有关!并且,缓冲区一定不在内核当中!如果在的话,那么write也会打印两次。

所以,我们讨论的缓冲区,都是用户级别语言层面给我们提供的缓冲区。我们在读写文件的时候,都会用stdout、stdin、stderr或者是别的文件,它们对应的类型是FILE*,而FILE是一个结构体,里面包含了fd,还有缓冲区

当我们使用C语言,写出fflush,fwrit的接口时候,是要传文件指针的,而文件指针就是FILE*,包含了缓冲区!FILE里面封装了fd,所以C语言会在合适的时候,将我们的数据刷新到外设。

解释上面程序结果现象:

fork()函数是在代码结束之前的,也就是在代码结束之前,子进程被创建。

①如果我们没有进行重定向,会看到四条打印结果。因为stdout是行刷新,在进程fork()之前,三条C语言函数已经将数据打印到标准输出(显示器外设),此时的FILE内部,或者说是进程内部,已经没有了这些数据了

②如果我们进行了重定向,是写入文件,而文件是属于磁盘文件,一般C库函数写入文件时,是全缓冲的,因此采用的刷新策略是全缓冲,之前的三条C语言函数,虽然带了'\n',但是不足以让stdout缓冲区写满,数据并没有被刷新!

③在执行fork()的时候,stdout属于父进程,创建子进程时,紧接着是进程退出,谁先退出,谁就要刷新缓冲区,将数据拿走,放到外设中,所以刷新的本质就是修改,修改的时候,就会发生写实拷贝!此时,父子进程都有一份一样的数据,不管谁先退出,那就先刷新呗,你退出,刷新完,到我退出刷新。最后就导致有两份的数据被刷新到外设了。

④write是系统调用,上面的过程与write无关,write没有FILE,用的是fd,因此也就没有C语言提供的缓冲区,再怎么写时拷贝,跟它无关。

缓冲区与OS的关系:

当数据需要刷新到外设的时候,进程会创建一个struct file的结构体,还有一个内核缓冲区。

我们上面所说的缓冲区刷新策略,是在用户层面,C语言的FILE自己的缓冲策略,而操作系统可不会理这些策略,它有自己的判断,即OS可以自主决定,是否将内核缓冲区中的数据刷新到外设上。比如内核缓冲区空间不够了,那么OS就会强制刷新等等。用户也能强制让内核缓冲区里面的数据之间刷新到外设中,使用fsync(int fd)函数。

 综上: printf fwrite 库函数会自带缓冲区,而 write 系统调用没有带缓冲区。另外,我们这里所说的缓冲区,都是用户级缓冲区。其实为了提升整机性能,OS也会提供相关内核级缓冲区。那这个缓冲区谁提供呢? printf fwrite 是库函数, write 是系统调用,库函数在系统调用的“上层”, 是对系统调用的“封装”,但是 write 没有缓冲区,而 printf,fwrite 有,足以说明,该缓冲区是二次加上的,又因为是C,所以由C标准库提供。

本篇完,但文件还没完,下一篇:文件系统!冲!

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2023-02-06,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 谈文件:
  • 系统文件IO:
    • 接口介绍:
    • 文件描述符fd:
      • 文件描述符的分配规则
        • 重定向:
          •  dup2系统调用:
          •  常见的重定向有:> >> <:
      • 理解Linux下一切皆文件   
      • FILE/缓冲区问题
        • 理解缓冲区问题:
        • 缓冲区与OS的关系:
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档