【Linux】Linux的文件操作

news2024/11/30 6:35:35

文件操作对于不少编程初学者来说都是一件头疼的事情,不管你学习的是什么编程语言,C/C++/Java/Go/Python ,因为我们学习到的文件相关的操作对你来说都是一个黑盒,我们只是知道我们应该怎么使用相应的文件函数进行操作,而对于内部的实现方式的一无所知,这导致很多初学者对于文件操作都很恐惧。

其实这不是你的问题,因为文件操作涉及了许多操作系统相关的知识,我们只有站在操作系统的角度才能对文件操作有比较深刻的理解,所以本篇文章我们来学习一下Linux下的基础IO操作,来加深我们对于语言级别文件操作以及系统的理解。

Linux的文件操作

  • 一、关于文件的预备知识
  • 二、Linux下文件读写的系统调用
    • 1、open函数
    • 2、close函数
    • 3、write函数
    • 4、read函数
  • 三、文件描述符
    • 1、理解文件描述符
    • 2、文件描述符的分配规则
  • 四、理解Linux下一切皆文件
  • 五、重定向
    • 重定向的原理
    • 系统调用dup2
  • 六、缓冲区的理解


一、关于文件的预备知识

  1. 文件 = 内容 + 属性,针对文件的操作都是对文件内容和属性的操作。
  2. 当我们没有打开文件时,文件位于磁盘中,当文件被打开时,文件就要操作系统加载到内存中,这是冯诺依曼体系结构对我们文件操作的限制!
  3. 文件是被进程打开的!
  4. 实际进程在运行的过程中肯定会打开多个文件,对于多个文件我们操作系统要将它们组织起来以便于管理和维护。所以当文件被打开时,操作系统要为被打开的文件创建相应的内核数据结构
  5. 为了管理和组织文件,Linux操作系统定义了一个struct file的结构体,里面记录了文件的各种属性以及文件内容的位置,每个struct file结构体里面又都有一个struct file*的指针,通过这个指针将我们的所有文件对象组织成了链表,于是我们对文件的操作就变成了对链表的增删查改。

在这里插入图片描述

二、Linux下文件读写的系统调用

Linux中关于文件读写的系统调用主要有以下几个接口,下面我们将一 一介绍:
open close write read

1、open函数

我们打开man手册在2号目录里面可以看到open的函数原型以及介绍:

在这里插入图片描述
我们可以看到open函数有三个头文件,我们在使用时一个也不能落下!
我们还看到open函数有两个,这里我们先介绍第一个open函数。

  • 第一个参数是路径名称,这个路径可以是相对路径,也可以是绝对路径

  • 第二个参数是一个位图结构的参数,我们可以使用|来进行连接,从而一次传递多个参数。
    其参数主要如下:

    • O_RDONLY 只读打开                    (英文read only的缩写)
    • O_WRONLY 只写打开                    (英文write only的缩写)
    • O_CREAT 文件不存在就创建该文件              (英文creat)
    • O_TRUNC 文件每次打开都会进行清空             (英文truncate)
    • O_APPEND 文件写入时以追加的方式进行写入       (英文append)
  • 返回值:如果打开成功会返回一个大于或等于0整数,这个整数叫做文件表示符,用来标定一个文件,如果打开失败会返回-1表示打开失败。

我们先用第一个接口打开一个文件

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<sys /stat.h>
#include<fcnt1.h>

#define LOG  "mylog.txt"

int main()
{
	//由于是位图结构,我们传参时要用 | 将参数连接起来
	int fd = open(LOG, O_WRONLY | O_CREAT | O_TRUNC); //以写的方式打开文件 ,如果文件不存在就创建。
	if(fd == -1)
	{
		perror( "open fail");
		exit(-1);
	}
	return 0;
}

第一次打开:
在这里插入图片描述

这里新创建的文件打开的权限有一点奇怪,我们删除再重新运行程序:

在这里插入图片描述
新创建的文件打开的权限还是有一点奇怪,而且和上一次的创建的文件权限竟然不同,我们再次删除再重新运行程序:

在这里插入图片描述

这一次就更加奇怪了,这时为什么呢?

因为我们使用的第一个open函数是没有指明创建后的文件权限是什么的,所以创建出的文件权限是一个随机值,这时我们就要考虑第二个open函数了!

  • 第三个参数 :mode_t是一种无符号整形,我们的第三个参数可以以8进制的方式传入我们创建的文件的权限。

修改我们上面的代码:

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<sys /stat.h>
#include<fcnt1.h>

#define LOG  "mylog.txt"

int main()
{
	//由于是位图结构,我们传参时要用 | 将参数连接起来
	int fd = open(LOG, O_WRONLY | O_CREAT | O_TRUNC, 0666); //创建的文件权限是 -rw-rw-rw-。
	if(fd == -1)
	{
		perror( "open fail");
		exit(-1);
	}
	return 0;
}

运行结果:

在这里插入图片描述
最终的结果并不是:-rw-rw-rw- 这是为什么呢?

这是因为我们Linux的普通用户的权限掩码影响了我们文件创建的权限,我们可以使用系统调用umask()来进行设置我们进程的权限掩码。

在这里插入图片描述

运行下面的代码,我们得到了我们想要的结果:

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<sys /stat.h>
#include<fcnt1.h>

#define LOG  "mylog.txt"

int main()
{
	umask(0)	//将权限掩码设置为0
	//由于是位图结构,我们传参时要用 | 将参数连接起来
	int fd = open(LOG, O_WRONLY | O_CREAT| O_TRUNC, 0666); //创建的文件权限是 -rw-rw-rw-。
	if(fd == -1)
	{
		perror( "open fail");
		exit(-1);
	}
	return 0;
}

在这里插入图片描述

2、close函数

close是关闭文件,我们打开的文件最后都要进行关闭,不然会占用系统资源,因此我们上面的代码其实是不规范的。

在这里插入图片描述

  • 参数: 要关闭的文件标识符fd。
  • 返回值:如果执行成功返回0,执行失败返回 -1,并设置错误码。

下面就是使用Linux系统调用比较标准的打开和关闭操作。

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys /stat.h>
#include<fcnt1.h>

#define LOG  "mylog.txt"

int main()
{
	umask(0)	//将权限掩码设置为0
	//打开文件
	int fd = open(LOG, O_WRONLY | O_CREAT | O_TRUNC, 0666); //创建的文件权限是 -rw-rw-rw-。
	if(fd == -1)
	{
		perror( "open fail");
		exit(-1);
	}
	
	//关闭文件
	close(fd);
	return 0;
}

3、write函数

write函数是写入函数,通过此函数我们能向文件中写入数据。
在这里插入图片描述

  • 第一个参数是文件标识符,也就是要写入的文件。
  • 第二个参数是一个指针,指向的是要写入的数据
  • 第三个参数是一个变量,表示最多写入多少个字节的数据
  • 返回值:实际写入的字节个数。

注意:需要注意的是我们使用系统调用时,不要将C语言的\0输入到文件中。

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys /stat.h>
#include<fcnt1.h>

#define LOG  "mylog.txt"

int main()
{
	umask(0)	//将权限掩码设置为0
	//打开文件
	int fd = open(LOG, O_WRONLY | O_CREAT| O_TRUNC, 0666); //创建的文件权限是 -rw-rw-rw-。
	if(fd == -1)
	{
		perror( "open fail");
		exit(-1);
	}
	
	const char* str1 = "hello world!\n" ;
	for(int i = 0; i < 5; ++i)
	{
		ssize_t n = write(fd, str1, strlen(str1));//strlen()后面不要 +1
		if(n < 0)
		{
			perror( "write fail: ");
			exit( -1);
		}
	}

	//关闭文件
	close(fd);
	return 0;
}

运行结果

在这里插入图片描述
在这里插入图片描述

4、read函数

read函数是读取函数,可以从文件中将数据读进变量中。

在这里插入图片描述

  • 第一个参数是:要读取的文件的文件标识符。
  • 第二个参数是:指针,这个指针指向了一块可以存储数据的空间。
  • 第三个参数是: 要读取的字节个数。
  • 返回值:实际读取的字节个数。

注意:read函数会读取换行符(\n)并将其作为普通字符处理,只有在读取到指定的字节数或者遇到文件的结束才会停止读取。

#include<stdio.h>    
#include<stdlib.h>    
#include<string.h>    
#include<unistd.h>    
#include<sys/types.h>    
#include<sys/stat.h>    
#include<fcntl.h>    
    
    
#define LOG "mylog.txt"    

int main()    
{    
  int fd = open(LOG, O_RDONLY);    
  if(fd == -1)    
  {    
    perror("open fail");                                                                                                                                       
  }    
   
  //定义缓冲区 
  char str2[64];    
  
  for(int i = 0; i < 5; ++i)    
  {    
    ssize_t  n = read(fd, str2, sizeof(str2) - 1);    
    if(n < 0)    
    {    
      perror("write fail: ");    
      exit(-1);    
    }
    //注意在读取到的字符串后面加'\0'才是C风格的字符串    
    str2[n] = '\0';    
    printf("%s",str2);    
   }    
	close(fd);
}

在这里插入图片描述

三、文件描述符

我们知道:C语言的文件操作的函数是C库函数,库函数与系统调用之间的关系是:库函数封装了系统调用

在这里插入图片描述

在这里插入图片描述

我们还知道: 一个进程默认会打开三个流,标准输入流,标准输出流,标准错误流。

这三个流在C/C++中默认是:

  • C中: stdin stdout stderr
  • C++中 cin cout cerr

1、理解文件描述符

我们知道C语言打开一个文件之后,会返回一个FILE*的指针,我们的C的三个标准流也是FILE*的指针,并且它们分别指向了 键盘文件显示器文件显示器文件。当我们尝试去向这标准输出流或者是标准错误流中去输入数据,其实就是向显示器文件进行数据写入,当我们尝试向标准输入流中读取数据,其实就是对键盘文件进行数据读取。

既然是文件,那么在Linux下进行文件操作必须要有文件描述符,那么我们C程序(以及其他编程语言写的程序)默认打开的三个流,也要有文件描述符,只有有了文件描述符我们系统才能找到对应的文件。

实际上C程序(以及其他编程语言写的程序)形成的进程默认打开的三个流在Linux中对应的文件描述符都是:

文件描述符
标准输入流0
标准输出流1
标准错误流2

观察它们的文件描述符的数字规律,我们可以思考一下我们的进程在打开默认的三个流之后再打开一个文件,那么这个文件的文件描述符是多少?

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>


#define LOG "mylog.txt"

int main()
{
    int fd = open(LOG, O_WRONLY | O_CREAT | O_TRUNC, 0666);
    if(fd == -1)
    {    
      perror("open fail");    
      exit(-1);
    }    
    //打印出文件标识符
    printf("%d\n",fd);   
    close(fd);         
    return 0;
}

在这里插入图片描述

没有错!是3!你可能会觉得这里和数组的下标好像啊!其实文件描述符就是数组的下标

我们以前说过操作系统要为我们打开的文件创建一个struct file对象用来组织管理被打开的文件,我们又知道文件是被进程打开的,那么进程一定要知道操作系统为文件创建的struct file在哪里,所以在内核数据结构中,还要有一张表,这张表里面记录了每一个被打开的文件的struct file的位置。

于是操作系统又会为我们创建一个struct file_struct结构,这个结构里面有一个指针struct file* fd_array,指向了一个指针数组,指针数组里面存储的就是struct file对象的地址,而我们得到的文件描述符其实就是这个指针数组的下标。

我们进程的PCB(在Linux下是task_struct)里面存放一个struct file_struct*指针,通过这个指针能够找到struct file_struct结构,通过这个结构我们能找到文件描述符的指针数组,通过指针数组,我们能够找到struct file对象,通过这个对象我们又能够文件的内容数据。

在这里插入图片描述

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

有了文件描述符的理解以后我们就要讨论下一个问题了,文件描述符是怎么分配的呢?

有了上面的经验我们可能会猜测文件描述符的分配规则是:files_struct数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符。

那么实际上是不是这样呢?我们可以进行实验来确定我们的结果,假设我们在打开新的文件时,先关闭标准输入流,那么0号位置的文件标识符就被空出来了,我们再进行打开文件,看新打开的文件标识符是不是0。

实验代码:

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>


#define LOG "mylog.txt"

int main()
{
	close(0);
    int fd = open(LOG, O_WRONLY | O_CREAT | O_TRUNC, 0666);
    if(fd == -1)
    {    
      perror("open fail");    
      exit(-1);
    }    
    //打印出文件标识符
    printf("%d\n",fd);   
    close(fd);         
    return 0;
}

在这里插入图片描述

结果说明我们猜想的结论是正确的!

四、理解Linux下一切皆文件

学习过Linux的人大多数都听过一句话:“Liunx下一切皆文件”,这一点对于文件操作非常重要,下面我们来谈一谈对于这句话的理解。

我们知道像:键盘,显示器,网卡… 这些都是硬件,根本不是文件,为什么我们能把它们当成文件去看待呢?

首先,我们在操作硬件时都是通过驱动程序进行操作硬件的,而对于不同的硬件它们的驱动程序肯定是不同的,例如键盘只能被读取而不能被写入,显示器只能被写入,而不能被读取,网卡既可以被读取又可以被写入 …

尽管它们的驱动是不同点,但是我们可以设计一个类,对于它们的进行封装,封装完以后,上面的调用者就看不到底层的内容了,上面的调用者只需要对于这个类进行操作就可以达到同样的效果,这样对于调用者来说,它的操作都是在操作这个类,它接触不到底层的驱动以及硬件,当然对于它来说它看到的是什么那就是什么了。

于是Linuxstruct file结构体封装这些驱动的函数指针,这样进程在使用键盘,显示器时就像是在使用文件一样,这样一来不管是普通文件还是硬件,对于进程来说都是文件。

而我们用户使用操作系统又是通过进程的方式来使用操作系统的,所以我们用户的视角和进程的视角是一样的,在我们用户来看想要让磁盘帮我们保存一些数据,我们只需要保存一下文件就行了,我们没有必要把磁盘拿出来,然后通过一些物理手段向磁盘中刻入数据,进程也是和我们用户一样,它也认为我只要保存一下文件就能完成任务了,所以对于我们用户和进程来说一切都是文件,我们也只能接触到文件,所以Linux下一切皆文件。

在这里插入图片描述

五、重定向

在谈论重定向之前我们先来谈论一下C语言中的FILE

我们使用C语言进行打开文件时,系统都会给我们一个FILE指针那这个FILE指针是什么呢?是谁给我们提供的呢?

答案是:是C语言给我们提供的,这个FILE其实就是一个C库给我们封装的一个结构体,而且这个结构体内部一定要有文件描述符fd,因为IO相关函数与系统调用接口对应,并且库函数封装系统调用,所以本质上,访问文件都是通过fd访问的。所以C库当中的FILE结构体内部,必定封装了fd

在C库的内部源代码中有这样一些源代码:

//将 _IO_FILE 重命名为FILE
typedef struct _IO_FILE FILE; //在/usr/include/stdio.h

struct _IO_FILE {
int _flags; 
// ......

struct _IO_FILE *_chain;
int _fileno; //封装的文件描述符

//......
};

通过这段源代码,我们知道FILE内部有一个叫 _fileno的文件描述符,那么我们就可以将stdin stdout stderr的文件描述符打印出来,看看与我们上面的结论是不是一样的。

打印三个标准流的文件描述符

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>


#define LOG "mylog.txt"

int main()
{
  //打印出文件标识符
  printf("%d\n", stdin->_fileno);    
  printf("%d\n", stdout->_fileno);    
  printf("%d\n", stderr->_fileno);    
  close(fd);         
  return 0;
}

在这里插入图片描述

结果和我们以前给的结论是一样的。

重定向的原理

输入重定向
看下面一段代码,我们就可以尝试如果我们关闭1号文件描述符,然后我们再打开一个文件,之后我们向stdout里面输入一些数据,看一看会发生什么?还是打印到显示器上面吗?

#include<stdio.h>    
#include<stdlib.h>    
#include<string.h>    
#include<unistd.h>    
#include<sys/types.h>    
#include<sys/stat.h>    
#include<fcntl.h>    
    
    
#define LOG "mylog.txt"    
//#define N 64    
int main()    
{    
  //关闭标准输出流
  close(1);    
  int fd = open(LOG, O_WRONLY | O_CREAT | O_TRUNC);    
  if(fd < 0)    
  {    
    perror("open fail:");    
    exit(-1);                                                                                                                                                  
  }    
  printf("you can see me?\n");    
  printf("you can see me?\n");    
  printf("you can see me?\n");    
  printf("you can see me?\n");    
    
  return 0;    
}

在这里插入图片描述

答案是并没有打印到显示器中,而是打印到了文件中,相信有了前面的基础你已经明白了,我们将stdout关闭后,新打开的文件占据了1号文件描述符,而我们的printf函数只认识1号文件描述符,所以向1号文件描述符指向的文件输入内容,就导致数据输入到了文件里面!

在这里插入图片描述

追加重定向
追加重定向的原理很简单,我们只需要将文件的打开方式加上O_APPEND去掉O_TRUNC

例如对于刚才的文件进行重定向:

#include<stdio.h>    
#include<stdlib.h>    
#include<string.h>    
#include<unistd.h>    
#include<sys/types.h>    
#include<sys/stat.h>    
#include<fcntl.h>    
    
    
#define LOG "mylog.txt"    
//#define N 64    
int main()    
{    
    
  close(1);    
  int fd = open(LOG, O_WRONLY | O_CREAT | O_APPEND);    
  if(fd < 0)    
  {    
    perror("open fail:");    
    exit(-1);    
  }    
  printf("this is append\n");    
  printf("this is append\n");    
  printf("this is append\n");                                                                                                                                    
    
  return 0;    

在这里插入图片描述

输入重定向
同理,我们把0号文件标识符给关闭,然后打开我们的新文件进行scanf,那么我们应该会从新打开的文件中读取数据,我们看一看结果:

#include<stdio.h>    
#include<stdlib.h>    
#include<string.h>    
#include<unistd.h>    
#include<sys/types.h>    
#include<sys/stat.h>    
#include<fcntl.h>    
    
    
#define LOG "mylog.txt"    
//#define N 64    
int main()    
{    
  close(0);    
  int fd = open(LOG, O_RDONLY);    
  if (fd < 0)    
  {    
    perror("open fail:");    
    exit(-1);    
  }    
  int a;    
  char c;    
  scanf("%d %c",&a, &c);                                                                                                                                       
  printf("%d %c\n", a, c);    
  return 0;    
}

在这里插入图片描述

结果是符合我们的预期的!

重定向的原理:在上层无法感知的情况下,在操作系统内部,更改进程对应的文件描述符表中,特定下标的指向!!!


根据这些原理我们来实现一个需求:将标准输出流与标准错误流的信息进行分流。

分析:我们知道标准输入流与标准输出流其实打开的是都是显示器文件,如果我们直接用标准输出标准错误流一起使用,就会导致错误信息与正确信息混合在一起,导致我们难以找到错误所在。

我们可以使用重定向进行分流,我们先关闭1号文件描述符,然后新打开一个文件normal.txt,然后关闭2号文件描述符,再打开一个新的文件error.txt这样我们再使用标准输入或标准错误流时,信息会被写入两个不同的文件中,我们关心错误信息就可以打开error.txt进行查看,关心正确信息,就可以打开normal.txt进行查看。

原本不分流时:

#include<stdio.h>    
    
int main()    
{    
  fprintf(stdout, "stdout->normal\n");                                                                                                                         
  fprintf(stdout, "stdout->normal\n");    
  fprintf(stdout, "stdout->normal\n");    
  fprintf(stdout, "stdout->normal\n");    
    
    
  fprintf(stderr, "stderr->error\n");    
  fprintf(stderr, "stderr->error\n");    
  fprintf(stderr, "stderr->error\n");    
    
  return 0;    
}    
                            

错误信息与正确信息混在一起!!

在这里插入图片描述

进行分流:

#include<stdio.h>    
#include<sys/types.h>    
#include<sys/stat.h>    
#include<fcntl.h>    
#include<unistd.h>    
    
int main()    
{    
  umask(0);    
  close(1);    
  open("normal.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);    
  close(2);    
  open("error.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);                                                                                                       
  fprintf(stdout, "stdout->normal\n");    
  fprintf(stdout, "stdout->normal\n");    
  fprintf(stdout, "stdout->normal\n");    
  fprintf(stdout, "stdout->normal\n");    
    
    
  fprintf(stderr, "stderr->error\n");    
  fprintf(stderr, "stderr->error\n");    
  fprintf(stderr, "stderr->error\n");    
  close(1);
  close(2); 
  return 0;    
}    

分流完成!
在这里插入图片描述


系统调用dup2

其实呢,对于上面的操作我们手动关闭其实是有一些不方便的,对于上面的操作Linux给我们提供了一个系统调用dup2,它的作用就是用第一个标识符里面的地址覆盖第二个的标识符中的地址。从而达到重定向的目的。

在这里插入图片描述

  • 第一个参数:要保留的参数。
  • 第二个参数: 要被覆盖的参数。
  • 返回值: 成功就返回第二个文件表示符,失败就返回 -1,并设置错误码。

对于上面的分流代码我们就可以:

#include<stdio.h>      
#include<sys/types.h>                                                         
#include<sys/stat.h>      
#include<fcntl.h>      
#include<unistd.h>      
      
int main()                                          
{      
  umask(0);      
  int fd1 = open("normal.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);      
  int fd2 = open("error.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666); 
       
  int n1 = dup2(fd1, 1);    
  int n2 = dup2(fd2, 2);
  
  //将dup2的返回值打印进文件中    
  printf("%d %d\n", n1, n2);
                                                                                                                                     
  fprintf(stdout, "stdout->normal\n");                                                                                                     
  fprintf(stdout, "stdout->normal\n");                                                                                                     
  fprintf(stdout, "stdout->normal\n");                                                                                                     
  fprintf(stdout, "stdout->normal\n");                                                                                                     
                                                                                                                                           
                                                                                                                                           
  fprintf(stderr, "stderr->error\n");                                                                                                      
  fprintf(stderr, "stderr->error\n");                                                                                                      
  fprintf(stderr, "stderr->error\n");  
                                                                                                      
  close(fd1);
  close(fd2);                                                                                                                                         
  return 0;                                                                                                                                
}                                             

在这里插入图片描述

六、缓冲区的理解

我们以前学习C语言的文件操作时,我们都知道FILE 里面应该是有缓冲区的,现在我们学习操作系统时我们又知道操作系统内核里面也是有缓冲区的,那这两个缓冲区是一样的吗?

对于这个问题我们现在不好回答,我们只能先给出结论:是不一样的,FILE是C库提供给我们的一个结构体,里面的缓冲区对应的是用户态的缓冲区,linux内核中的缓冲区,对应的是内核态的缓冲区。

我们先看下面的代码,根据现象我们来分析问题,最后再来理解一下缓冲区。

#include<stdio.h>      
#include<unistd.h>      
#include<string.h>      

      
int main()      
{      
  printf("printf : hello world!\n");      
      
  const char* str = "write: hello world!\n";      
  write(1, str, strlen(str));        
  
  //创建子进程    
  fork();                                                                                                                                                      
  return 0;      
}                   

结果:

在这里插入图片描述

我们发现当我们直接运行和重定向后的结果是不同的,而且printf()会比write多一次打印,这时为什么呢?

其实呢这与C库的缓冲区有关系!缓冲区在哪里?在你进行fopen打开文件的时候,你会得到FILE结构体,缓冲区就在这个FILE结构体中!!

在这里插入图片描述

C库会结合一定的刷新策略,将我们缓冲区中的数据写入给操作系统(通过write (FILE->fd,xXXX) ) ;

  1. 无缓冲
  2. 行缓冲 (显示器采用的刷新策略: 行缓冲)
  3. 全缓冲 (普通文件采用的刷新策略:全缓冲)

通过这张图片我们就能很好的知道,为什么会出现上面的情况了。

在运行时,printf函数使用的是显示器文件,所以代码运行后立即就被C库的刷新到了操作系统内核里面的缓冲区了,write函数本身就是向操作系统内核里面写入数据,因此也将数据写入到操作系统内核里面的缓冲区了,fork之后FILE里面的缓冲区的数据内容要被清空,但是FILE的缓冲区里面本身就没有数据,无法输出数据了,进程也结束了。

但是,当变成重定向时,由于printf函数使用的文件变成了普通文件了,数据的刷新方式变成了全缓冲,所以printf代码运行之后数据被暂存到了FILE的缓冲区里面了,而write函数写的数据向系统内核里面直接写入了数据,程序运行完毕,FILE内部的缓冲区要被清洗(此时缓冲区里面有数据),但是进程从一个变成了两个,要清洗两次缓冲区,于是log.txt里面就有了两次printf打印的内容。

为什么C库的FILE里面要有缓冲区呢?

答案是:节省调用者的时间! 如果我们想直接把数据写到操作系统内核中就需要调用系统调用,而系统调用的使用代价是要比普通函数大的多的,因此为了尽量少的使用系统调用,尽量一次IO能够读取和写入更多的数据,所以 FILE内部才有了缓冲区。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/512239.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Linux线程同步(2)——初识互斥锁

互斥锁&#xff08;mutex&#xff09;又叫互斥量&#xff0c;从本质上说是一把锁&#xff0c;在访问共享资源之前对互斥锁进行上锁&#xff0c;在访问完成后释放互斥锁&#xff08;解锁&#xff09;&#xff1b;对互斥锁进行上锁之后&#xff0c;任何其它试图再次对互斥锁进行加…

C语言:青蛙跳台与汉诺塔问题

青蛙跳台 原理&#xff1a;一只青蛙跳n个台阶&#xff0c;青蛙可以一次性跳1个台阶&#xff0c;也可以跳2个台阶&#xff0c;问&#xff0c;有多少种跳法&#xff0c;可以跳过n个台阶。 分析&#xff1a;青蛙跳台本质上是递归问题&#xff0c;那它为什么是递归问题呢&#xff…

cleanmymac在哪下载?中文官网安装教程

CleanMyMac是一个系统清理工具&#xff0c;删除系统缓存文件 , 多余的应用程序语言包 , PowerPc软件运行库等。 是个给你的硬盘瘦身的好工具。 系统&#xff1a;macOS 10.14&#xff08;在10.15以及Big Sur中的安装激活教程相同&#xff09;登录CleanMyMac X下载页面&#xff0…

第十四章_缓存双写一致性之更新策略探讨

缓存双写一致性的理解 如果redis中有数据 需要和数据库中的值相同 如果redis中无数据 数据库中的值要是最新值&#xff0c;且准备回写redis 缓存按照操作来分&#xff0c;细分2种 只读缓存 读写缓存 同步直写策略 写数据库后也同步写redis缓存&#xff0c;缓存和数据库…

01:mysql基本操作---DDL

目录 前言: 1:SQL分类 2:类型 3:sql表的创建----简单版本 前言: 1:SQL语句可以单行或多行书写&#xff0c;以分号结尾。 2:SQL语句可以使用空格/缩进来增强语句的可读性。 3:MySQL数据库的SQL语句不区分大小写&#xff0c;关键字建议使用大写 4:注释: 单行注释:-- 注释…

RabbitMQ详解(三):消息模式(fanout、direct、topic、work)

消费模式 参考官网&#xff1a;https://www.rabbitmq.com/getstarted.html 简单模式 Simple, 参考RabbitMQ详解&#xff08;二&#xff09;&#xff1a;消息模式 Simple(简单)模式 简单模式是最简单的消息模式&#xff0c;它包含一个生产者、一个消费者和一个队列。生产者向队…

量化散户交易数据:追涨爆亏99%,杀跌少赚28倍?| 追涨杀跌一时爽,散户钱包火葬场?【邢不行】

你第一次炒股的经历是不是这样的&#xff1a; 你有一个朋友&#xff0c;他说在XX股票上大赚了一笔&#xff0c;你听后是既羡慕又不服。 于是你下载了炒股软件&#xff0c;看了眼这只股票&#xff0c;有点心动。但由于没有交易经验&#xff0c;股价又确实涨了不少&#xff0c;…

Python基础入门编程代码练习(四)

一、遍历列表 通过 input输入3个人信息&#xff0c;每个人有姓名和年龄&#xff0c;将信息存入字典中&#xff0c;并将将字典存入列表。 遍历列表&#xff0c;打印每个人的信息&#xff0c;打印格式如下&#xff1a; 张三 20李四 22王五 23 1. 输入三个人的信息 (输入 inpu…

qiankun 微前端 demo(Vue2)

前言 这是我最近刚开始学微前端&#xff08;qiankun框架&#xff09;做的一个小demo&#xff0c;做的时候还是遇到很多问题的&#xff0c;在网上也是看了很多别人的Blog&#xff0c;最后也是磨出来了&#x1f602;&#x1f602;&#x1f602;&#xff1b;这篇文章总统分为分为…

国产麒麟操作系统 myCat1.6读写分离

我的环境是麒麟操作系统&#xff0c;我只配置读写分离 一、使用说明&#xff0c;java环境&#xff0c;解压就能用 下载地址https://raw.githubusercontent.com/MyCATApache/Mycat-download/master/1.6-RELEASE/Mycat-server-1.6-RELEASE-20161028204710-linux.tar.gz mycat 是j…

OJ刷题之旅

题目 现在给你两种颜色的箩筐&#xff0c;需要的时候&#xff0c;就把一个个大小差一圈的筐叠上去&#xff0c;使得从上往下看时&#xff0c;边筐花色交错。这个工作现在要让计算机来完成&#xff0c;得看你的了。 输入 输入是一个个的三元组&#xff0c;分别是&#xff0c;外…

SpringCloud使用SkyWalking实现分布式链路追踪1

文章目录 一、MicrometerTracingBrave(Sleuth)链路追踪1、MicrometerTracingBrave和Zipkin的概论2、Docker搭建Zipkin服务3、MicrometerTracingBrave和Zipkin实现链路追踪 二、SkyWaking服务的安装与使用1、SkyWalking的概论2、Java探针的环境搭建3、Java探针实现日志监控4、Sk…

Netty——介绍和maxContentLength配置

官网 介绍 Netty框架的设计思路是基于NIO的事件驱动编程模型&#xff0c;核心组件包括&#xff1a; Channel&#xff1a;通道&#xff0c;负责网络数据的读写操作&#xff1b; EventLoop&#xff1a;事件循环&#xff0c;处理I/O事件和用户自定义事件&#xff1b; ChannelFut…

【子集树】输出一个序列的子序列

【子集树】输出一个序列的子序列 给一个序列 1 2 3 输出序列的子集 1 2 3 12 13 23 123 如何实现&#xff1f; 由上可以看出 类似于全排列 如何用全排列 实现子集输出&#xff1f; 也就是子集树&#xff1f; #include<iostream>using namespace std;const int N 1e5…

【C++技能树】令常规运算符用在类上 --类的六个成员函数II

Halo&#xff0c;这里是Ppeua。平时主要更新C语言&#xff0c;C&#xff0c;数据结构算法…感兴趣就关注我吧&#xff01;你定不会失望。 本篇导航 0.运算符重载1.赋值运算符 重载2.比较运算符 重载3.比较运算符 ! 重载4.比较运算符 < 重载5.比较运算符 < 重载6. 比较…

图片处理软件:分享6款非常实用的图片处理工具

目录 一、移动端 1、snapseed 2、一键抠图 3、pixlr 二、电脑端 1、图片编辑助手 2.GIMP 3、photopea 今天给大家分享6款非常实用的图片处理工具&#xff0c;其中包含移动端和电脑端&#xff0c;每一款都非常实用&#xff0c;希望对大家能有所帮助&#xff01; 一、移…

《编程思维与实践》1059.计算a的n次方的大整数

《编程思维与实践》1059.计算a的n次方的大整数 题目 思路 高精度的问题统一的解决思路是用一个数组去存大整数的每一位数,运算转化为对数组的操作. 可以从个位开始存(逆序),也可以从最高位开始存(顺序),以处理方便为主要考虑因素. 同时可以将大整数定义为一个结构体,包含位数,…

软件架构:理解分析三层结构观点

三层结构的简单描述及优点   三层体系结构&#xff0c;即用户层、应用层和数据库服务器。用户层主要指用户界面&#xff0c;它要求尽可能的简单&#xff0c;使最终用户不需要进行任何培训就能方便地访问信息&#xff1b;第二层就是应用服务器&#xff0c;也就是常说的中间件&…

webpack: 4 loader汇总(style-loader等)

所有的loader必须匹配规则&#xff0c;否则不生效 配置文件中&#xff0c;module中rules的use执行顺序是从后往前执行 url-loader 用于将文件转换为base64 URI的webpack加载程序。 options limit limit指定文件大小&#xff0c;小于limit的图片不会生成图片以base64格式被引入…

客观地说,应该看一看 Web3.0 了

武术圈有名言&#xff1a;“八极加劈挂&#xff0c;神鬼都害怕”。要是 Web3.0AGI 的话&#xff0c;世界将会变成什么样子&#xff1f; 数科星球原创作者丨苑晶编辑丨大兔 Web3.0 的价值开始受到重视&#xff0c;在最近&#xff0c;来自香港的好消息再次带火了这个领域的热度。…