【Linux】文件操作/文件描述符/重定向原理/缓冲区

news2025/7/16 15:21:01

目录

一.文件的概念

1.什么是内存级文件?

2.什么是磁盘级文件?

3.文件IO的过程

4.linux下, 一切皆文件

二.文件操作(C语言接口)

1.语言级文件接口与系统级文件接口

1).什么是语言级别的文件接口?

2).为什么要有语言级别文件接口, 直接用系统接口不好吗?

3).系统级别文件接口缺点很多, 但我们为什么还要学习呢?

2.什么叫做当前路径

3.C语言文件接口的使用

1).fopen -- 打开文件

2).fwrite -- 输出到文件

3).fread -- 从文件输入

4).fclose -- 关闭文件

三.文件操作(系统接口)

1.C语言接口对应系统接口

2.标记位的传入方式(open系统接口)

3.系统文件接口的使用

1).write

2).read 

3).一个简易版的cat命令

四.文件描述符

1.用OS管理进程的方式 VS 进程管理文件的方式

2.文件如何被组织? 文件描述符又是什么? 与组织这些文件有关系吗?

3.对比C语言中的FILE*与fd

4.用代码验证以上结论

验证一: 每个进程默认打开三个文件

验证二: 文件描述符的分配规则

验证三: stdout对应1号文件描述符

五.重定向原理

1.重定向的基本原理

2.重定向的模拟实现(dup2系统调用)

六.linux一切皆文件

七.用户级缓冲区

1.对于缓冲区的认识

2.缓冲区的刷新策略

3.如何证明语言级缓冲区的存在

1).用户级缓冲区被封装在哪里

2).代码验证用户级缓冲区的存在

4.自己设计一个用户级缓冲区


一.文件的概念

文件 = 内容 + 属性

想要对文件进行操作, 要么对内容, 要么对属性

1.什么是内存级文件?

文件是存放在磁盘上的, 只有操作系统有权限来操作文件, 那么如果做为普通用户的我们, 如何去操作文件呢? 所以, 如果想要让普通用户访问文件, 操作系统必须提供相应的接口, 普通用户通过编写程序的方式, 让程序执行起来加载到内存成为进程, 通过进程去调用文件操作接口, 再去通过系统文件接口操作文件, 我们就通过进程间接的操作了文件, 所以, 程序被加载到内存成为进程, 通过进程打开的文件也从硬盘加载到了内存, 这种加载到内存的文件就被称为内存级文件

2.什么是磁盘级文件?

所有的文件一般情况下都是存储在磁盘上的, 文件被操作则就会被加载到内存, 相反, 那些没有被使用或打开的文件就静静的呆在磁盘上, 这种文件称为磁盘级文件

3.文件IO的过程

IO的含义: input/output, 输入/输出, 这里所讲的输入与输出, 是站在内存角度来看待的

例如: scanf/fread/fgets是一个输入的过程, 这是要把某文件的数据, 输入到内存中

        printf/fwrite/fputs是一个输出的过程, 这是要把内存中的数据, 输出到某文件中

一次文件IO的过程:

普通文件 -- fread -- 内存 -- fwrite -- 另一个普通文件

4.linux下, 一切皆文件

站在系统的角度, 只要是能进行输入输出的设备就被称之为文件

linux下, 一切皆文件: 普通的磁盘上的文件是文件, 显示器, 键盘, 网卡, 声卡, 磁盘几乎所有的外设, 他们都至少可以拥有输入输出一种功能, 都可以称之为文件

以上只是一些基础概念, 在本篇博客的中后期, 会深度理解什么是: linux下, 一切皆文件

二.文件操作(C语言接口)

1.语言级文件接口与系统级文件接口

1).什么是语言级别的文件接口?

每一个操作系统有一套独属于自己的文件操作接口(系统调用), 不同的语言基于这套系统文件接口进行了一定程度封装, 来做为每种语言自己的文件操作接口

2).为什么要有语言级别文件接口, 直接用系统接口不好吗?

总体来说, 有两大原因

1.系统接口比较复杂, 使用成本高, 每种语言都进行一定程度封装, 来简化文件操作接口, 从而降低使用成本

2.系统接口不具备移植性, 不具备跨平台性, 每套操作系统都有独属于自己的系统文件操作接口, 如果使用系统接口的话, linux下的程序就不能在windows环境下运行了, 其他OS也同理, 那么为了解决这个问题, 语言级别的封装就显得尤为重要, 每种语言会把所有OS的系统文件接口全部封装一遍, 再通过条件编译的方式来控制在特定的OS下使用封装好的属于这个OS的语言级别文件接口, 这样看来, 有了语言级别的文件操作接口, 我就不必使用系统接口, 直接使用语言接口, 支持了跨平台性, 一份代码可以在多个OS多个平台下运行

3).系统级别文件接口缺点很多, 但我们为什么还要学习呢?

从使用角度来看, 在实际开发中为了保证可移植性, 跨平台性, 我们一定会使用语言给我们封装好的文件接口, 但是在学习阶段, 仍然需要去深度理解和挖掘系统接口的设计方式, 理解了底层的系统文件接口, 再去学习语言级别文件接口就会清楚很多了, 不管是哪种语言便都很容易入手

2.什么叫做当前路径

FILE* fp = fopen("log.txt", "w");

如果以这种方式打开一个文件, 在打开文件时, 只写出文件名, 并不带路径, 那么系统会默认为当前路径去操作这个文件

也就是说, 如果这个文件不存在, 该文件会被创建, 被创建到哪里呢? 既然系统默认为当前路径去操作这个文件, 那么当然是创建到当前路径

我们看到的现象是: 我们的程序写在哪个路径下, 文件就被创建在哪个路径下, 但其实这并不正确, 这并不是相对路径! 

如果我将生成的可执行程序mybin移动到上一路径, 并且在上一路径的下一路径去执行这个mybin, log.txt会被创建到哪里呢?

结论是: 如果我将mybin剪切到上一路径, 我在上一路径的下一路径去执行这个mybin, 文件会被创建到我所执行程序的那个路径下

什么是当前路径: 当一个进程运行起来的时候, 每个进程都会记录自己当前所处的工作路径!

那么我们如何去查看这个工作路径呢? 在/proc目录, 会存有每一个当前正在运行的进程, 去/proc查看对应进程即可

我们现在给上面的代码添加一个死循环, 来观察运行起来的进程的当前路径

依旧采用将生成的可执行程序拷贝到上一路径, 然后在当前路径去运行的方式, 来观察它的"相对路径"

在proc目录下观察到的该进程中的cwd即是该进程的工作路径也就是该进程的相对路径!

我们可以再去上一路径执行一次, 再来观察一次mybin进程的相对路径

可以得出结论了: 正在运行的进程的相对路径取决于我们在哪里去运行这个可执行程序, 与可执行文件在哪, 程序在哪本身并没有关系

3.C语言文件接口的使用

1).fopen -- 打开文件

返回值FILE*, 返回一个结构体指针

第一次参数path, 文件名及文件所在路径, 如果只写文件名, 默认为进程的当前路径

第二个参数mode, 打开文件的方式

几种打开文件的方式:

注意: w系列的打开文件的方式, 如果文件不存在则会新建文件, 如果文件存在则会先清空文件, 再向文件中写入内容 

输入输出函数

2).fwrite -- 输出到文件

值得一提的是: 在调用fwrite函数, 传第二个参数时, 意思是要写入多少个字符

'\0'是字符串结束标志, 是C语言规定的, 而OS是不需要遵守的, 所以fwrite最终还是要去调用系统接口的, 在传入第二个参数时strlen(str)不能+1, 不能把'\0'也带上! 在向文件写入时, 只可写入有效字符

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

int main()
{
  FILE* fp = fopen("log.txt", "w");
  if(fp == NULL)
  {
    perror("fopen:");
    return 1;
  }

  const char* str1 = "hello fwrite\n";
  const char* str2 = "hello fputs\n";
  const char* str3 = "hello fprintf\n";

  fwrite(str1, strlen(str1), 1, fp); 
  fputs(str2, fp);
  fprintf(fp, "%s", str3);

  fclose(fp); 
  return 0;
}

所以, 如果我们以w方式打开一个文件, 并且什么都不写入, 那么这个文件内的内容就全部被清空了

同样, 如果我们输出重定向一个文件, 但并不向文件内写入任何内容, 文件也是被清空了

3).fread -- 从文件输入

#include<stdio.h>

int main()
{
  FILE* fp = fopen("log.txt", "r"); 
  if(fp == NULL)
  {
    perror("fopen: ");
    return 2;
  }
  //这里可以不需要对buffer全部初始化为0
  //因为读取到文件内容后,会自动在字符串结尾添加'\0'
  char buffer1[64];
  char buffer2[64];
  char buffer3[64];
  fread(buffer1, sizeof(buffer1), 1, fp);
  fseek(fp, 0, SEEK_SET);//将指针归位到初始位置
  fgets(buffer2, sizeof(buffer2), fp);
  fscanf(fp, "%[^\n]s", buffer3);//fscanf读取到'\n'结束

  printf("buffer1: %s", buffer1);
  printf("buffer2: %s", buffer2);
  printf("buffer3: %s\n", buffer3);

  fclose(fp);

  return 0;
}

从文件中读字符串时, 不需要关心'\0', 因为'\0'是字符串的结束标志这是C语言的规定, 与文件无关, 所以文件读取到C语言进程中时会自动在字符串末尾添加'\0'

4).fclose -- 关闭文件

在一个程序中, 只要打开了一个文件, 必须对应的要在对这个文件操作结束时, 关闭这个文件

为什么一定要关闭文件

1.为了避免内存泄漏, 如果一个进程打开了文件而操作结束后却不关闭这个文件, 在进程还未结束之前, 占用了资源却不再去使用, 这就属于内存泄漏

2.fclose底层封装了close, 在C语言级别的fclose在封装时, 也将C语言库级别的缓冲区封装进去了, 所以在调用fclose不仅仅是关闭文件, 也刷新了C语言库级别的缓冲区, 将数据刷新到内核级缓冲区, 再由调用的close从内核级缓冲区刷新到内存

三.文件操作(系统接口)

1.C语言接口对应系统接口

C语言文件接口 vs 系统文件接口 (一一对应关系)

        fopen                  open

        fwrite                   write

        fread                   read

        fclose                  close

所有的语言级别的接口, 底层都一定封装了系统接口, 为了降低使用成本与支持平台之间的兼容性

2.标记位的传入方式(open系统接口)

先了解一下open系统接口

注: C语言也可以支持类似C++中的函数重载, 功能大体类似但并不是函数重载

第一个参数: 与fopen的第一个参数相同, 路径 + 文件名, 如果不带路径, 默认就是当前路径(关于什么是当前路径在上面已经给出解释与验证)

第二个参数: 一个可以标记多个标记位的有符号整数

常用的标记: O_CREAT(若文件不存在则创建), O_RDONLY(只读), O_WRONLY(只写), O_RDWR(读写), O_TRUNC(清空), O_APPEND(追加)

例如在语言层面上的不同的打开方式, fopen("log.txt", "w"); 实际在封装时, w就代表了O_CREAT|O_WRONLY|O_TRUNC

这些标记全部为大写, 且全部都是宏定义

重点解析: 如何使用一个int类型的数来标记多个标记位

如何标记多个状态?

一个int类型整数占4Bytes, 一共是32bit, 每一个bit可以标记一种状态(0或1), 那么32个bit即最多可标记32种状态

如何传入多个状态?

每种状态分别占用不同的位, 然后将这些状态用位运算或("|")到一起, 就可以将多种状态传入到flags中了

如果验证某种状态是否存在?

用flags与("&")上那个状态对应的数值, 若结果不变, 因为只有当1&1=1, 所以说明该状态是存在的

用一段简易代码来说明以上原理

#include<stdio.h>

#define A 0x1
#define B 0x2
#define C 0x4

void func(int flags)
{
  printf("flags: %x\n", flags);
  if((flags & A) == A)
  {
    printf("A is ok\n");
  }
  if((flags & B) == B)
  {
    printf("B is ok\n");
  }
  if((flags & C) == C)
  {
    printf("C is ok\n");
  }
}

int main()
{
  func(A | B | C);
  func(A | B);
  func(B | C);
  return 0;
}

第三个参数: 传入权限, 只有当新建文件时, 才需要传入这个权限, 例如: 0666, 0644

当文件创建完成时, 这个mode并不是文件最终的权限, mode & (~umask) 才是新创建出的文件的最终权限, 所以如果我们想完全控制新建文件的权限, 需要在程序内部使用umask()来设置权限掩码, 创建出来后的权限仍是 mode & (~umask) 只不过这时这个umask是我们自己设置的

用程序验证以上结论:

#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
int main()
{
  umask(0000);
  //使用系统接口操作文件
  int fd = open("log.txt", O_CREAT|O_WRONLY|O_TRUNC, 0666); 
  if(fd < 0)//创建或打开文件失败
  {
    perror("open: ");
    return 2;
  }
  //...
  close(fd);
  return 0;
}

4.返回值: 返回一个整数fd, 若打开或创建文件失败则返回-1

对于返回值是什么, fd是文件描述符, 也是本篇最重要的内容, 在本篇文章后面会重点详谈 

3.系统文件接口的使用

1).write

#include<stdio.h>
#include<string.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
int main()
{
  umask(0000);
  //使用系统接口操作文件
  int fd = open("log.txt", O_CREAT|O_WRONLY|O_TRUNC, 0666); 
  if(fd < 0)//创建或打开文件失败
  {
    perror("open: ");
    return 2;
  }
  const char* str1 = "hello write\n";
  
  write(fd, str1, strlen(str1));

  close(fd);
  return 0;
}

2).read 

#include<stdio.h>
#include<string.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 2;
  }
  char buffer[64];
  
  read(fd, buffer, sizeof(buffer));

  printf("%s", buffer);

  close(fd);
  return 0;
}

3).一个简易版的cat命令

#include<stdio.h>

int main(int argc, char* argv[])
{
  if(argc != 2)
  {
    printf("输入参数有误, 必须输入一个参数");
    return 3;
  }
  FILE* fp = fopen(argv[1], "r");
  if(fp == NULL)
  {
    perror("fopen: ");
    return 2;
  }
  char buffer[64];
  fread(buffer, sizeof(buffer), 1, fp);
  printf("%s", buffer);
  fclose(fp);
  return 0;
}

四.文件描述符

1.用OS管理进程的方式 VS 进程管理文件的方式

一台启动着的电脑内会有很多进程, 操作系统为能够正确且有序的调度和管理每一个进程, 就必须对进程进行管理

那么操作系统如何管理这么多进程呢? 先描述, 再组织. 先将进程抽象成一个个的task_struct(PCB), 然后再将这些结构体以链表的形式组织起来

一个正在执行的进程, 可以打开很多文件, 那么进程想要对文件进行读写就必须管理每一个文件

那么进程如何管理这么多文件呢? 先描述, 再组织. 先将文件抽象成一个个的file结构体, 然后再将这些结构体组织起来, file结构体内部几乎存储了对应的文件的全部内容!

2.文件如何被组织? 文件描述符又是什么? 与组织这些文件有关系吗?

对于系统接口, 每打开一个文件, 系统会返回一个int类型的值, 我们通常是这样写的int fd = open(...);

这个fd就是文件描述符, 每个进程为了管理好自己打开的全部文件, 就需要将这些file结构体组织起来

在进程中, 组织好这些file结构体的方式如下:

array指针数组的下标就是文件描述符!

array这个指针数组中的每一个元素存储的是指向文件(file结构体)的指针, 有了文件描述符就可以找到指针, 有了指针就可以找到指定文件

文件描述符的分配规则: 从数组下标值的最低处开始分配

每个进程默认打开三个文件: 标准输入, 标准输出, 标准错误, 这三个文件分别对应键盘, 显示器, 显示器

fd: 0 --- 标准输入 --- 键盘

fd: 1 --- 标准输出 --- 显示器

fd: 2 --- 标准错误 --- 显示器

在linux下, 一切皆文件, 只要是能读或写的设备都可以称之为文件, 所以像键盘, 显示器在linux的设计理念中, 也是文件

3.对比C语言中的FILE*与fd

在C语言中 标准输入 --- stdin, 标准输出 --- stdout, 标准错误 --- stderr

所以以fopen打开文件时, 例如: FILE* fp = fopen(...)

stdin, stdout, stderr也都是FILE*类型的指针

这个FILE*是一个结构体指针, FILE这个结构体内部也一定封装了fd, 因为操作系统必须通过fd去找到对应文件, 无论何种语言的封装, 最终一定要回归底层, 一定会调用系统调用, 想要调用系统调用就一定遵守操作系统的规则

也就是说在正常情况下, stdin内封装0描述符, stdout内封装1描述符, stderr内封装2描述符

4.用代码验证以上结论

验证一: 每个进程默认打开三个文件

首先观察到的现象是, 系统为我们新建的文件log.txt分配的文件描述符是3, 那么0, 1, 2去哪里了?

我们从0中读取后, 向1中打印

分别使用系统接口, 从0读入数据到buffer, 再将buffer的数据打印到1中, 对应的是从键盘输入数据后又打印到显示器上 

验证二: 文件描述符的分配规则

先关闭0号文件描述符, 在打开一个新建文件, 观察这个打开的新文件的文件描述符

关闭了0, 文件描述符从最小开始分配, 所以log.txt就分配到了0号文件描述符

验证三: stdout对应1号文件描述符

分别使用C语言接口向stdout打印, 系统接口向1号文件描述符打印

但由于标准输出和标准错误对应的都是显示器, 所以要将这些打印结果输出重定向到一个指定文件, 若文件中存在两次打印结果, 则验证成功

五.重定向原理

1.重定向的基本原理

重定向的本质: 那标准输出举例, 原本应该输入到标准输出(显示器)的内容, 输出到了指定文件中

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

int main()
{
  //先关闭标准输出
  close(1);//stdout --- 1
  int fd = open("log.txt", O_CREAT | O_TRUNC | O_WRONLY, 0666);
  if(fd < 0)
  {
    perror("open: ");
    return 2;
  }
  printf("log.txt分配到的fd: %d\n", fd);
  //向stdout中输出一些内容, 实际上printf内部默认指定向stdout输出
  fprintf(stdout, "hello OutputRedirection\n");

  fflush(stdout);//fflush是语言级别的接口, 在使用系统接口关闭文件描述符前, 需要刷新语言级别的缓冲区到指定文件
  close(fd);
  return 0;
}

一张图来说明以上代码都做了哪些事情, 简单来说这就是输出重定向

2.重定向的模拟实现(dup2系统调用)

但这是在明确知道了文件描述符的分配规则后, 才能够以这种方式来进行重定向操作, 在linux中的输入/输出/追加重定向并不是以这种形式, 而是使用dup2系统调用来实现的

dup2函数的使用: 传入两个int类型参数, 分别是两个fd

dup2会将以oldfd为下标的元素中的内容拷贝给以newfd为下标的元素中的内容

例如: int fd = open(...);

         dup2(fd, 1);

解释: 将fd下标中的内容拷贝到1下标中的内容中, 也就是将标准输出重定向到log.txt

 代码验证:

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

int main()
{
  int fd = open("log.txt", O_CREAT | O_TRUNC | O_WRONLY, 0666);
  if(fd < 0)
  {
    perror("open: ");
    return 2;
  }
  
  dup2(fd, 1);

  printf("log.txt分配到的fd: %d\n", fd);
  //向stdout中输出一些内容, 实际上printf内部默认指定向stdout输出
  fprintf(stdout, "hello OutputRedirection\n");

  fflush(stdout);//fflush是语言级别的接口, 在使用系统接口关闭文件描述符前, 需要刷新语言级别的缓冲区到指定文件
  close(fd);
  return 0;
}

六.linux一切皆文件

在linux中, 一切皆文件

在宏观上, 所有能够支持读或写的设备, 全部都是文件

在微观上, 每一个能够支持读或写的设备, 底层都是不同的硬件, 其对应不同的操作方法

故在linux中, 把每一个支持读或写的设备, 抽象成一个文件, 即一个struct file结构体, 结构体内部封装有其自己的读或写的方法

注: linux内核使用C语言编写, 在C语言中struct结构体可以有变量也可以有"方法", 对比C++的类, C语言通过函数指针的形式支持了让结构体内拥有函数

在上层看来, 文件与多种硬件之间没有任何差别, 因为在OS看来, 它们都是一个个的struct file, 所以这些硬件读写的底层实现肯定是不一样的, 但是由于操作系统对硬件进行了封装(一切皆文件), 进程就可以通过操作系统来以操作文件的方式去操作硬件, 所以在上层看来, 这些硬件的调用方式没有区别, 但底层实现是截然不同的

七.用户级缓冲区

1.对于缓冲区的认识

缓冲区一般情况下分为两种:

用户级缓冲区 vs 内核级缓冲区

用户级缓冲区由语言提供 vs 内核级缓冲区由系统提供

缓冲区存在的意义: 为了减少IO次数, 更少次的外设访问, 提高机器效率

具体的提高效率的方式体现在了减少IO次数, 例如: 如果要写入100条数据, 如果没有缓冲区, 一共有100次IO, 如果有缓冲区, 且此时采用全缓冲的形式, 也就是将100条数据全部写入到缓冲区内, 缓冲区写满或程序退出时, 统一刷新缓冲区, 这样就只有1次IO, 提高效率具体不是体现在写入数据量的多少, 而是IO次数的多少, IO的时间消耗大, 主要是准备IO的时间相比之下比较费时

总结: 缓冲区分为两种, 缓冲区的存在为了减少IO提高效率, 且和外部设备访问时消耗的时间数据量不是主要矛盾, 而是准备IO的过程比较耗时

本篇重点讨论用户级缓冲区

2.缓冲区的刷新策略

缓冲区的刷新策略分为: 一般情况与特殊情况

一般情况下, 缓冲区有三大刷新策略

1).满刷新 --- 缓冲区被写满时才会刷新

2).行刷新 --- 遇到'\n'就刷新

3).立即刷新 --- 字面意思, 输入一个刷新一个, 立刻刷新

特殊情况下, 缓冲区多种刷新策略

1).程序退出时自动刷新

2).用户强制刷新

3).缓冲区的刷新策略是可以由我们自己, 根据不同的需求来实现自己的缓冲区刷新策略

一般的, 所有的设备都更倾向于满刷新, 因为这种刷新策略是相较于其他刷新策略而言IO次数最少的

但是, 由于需求不同, 并不是所有的设备都要用满刷新这种刷新方式

例如: 向显示器输出, 采用行刷新的刷新策略; 向文件中输出, 采用满刷新的刷新策略

3.如何证明语言级缓冲区的存在

1).用户级缓冲区被封装在哪里

用户级缓冲区是由语言提供的, 那么它就是一定存在于某语言库中

用C语言举例, 如果用户想要强制刷新缓冲区到指定文件, 就需要调用fflush, 这是语言级别的接口, 需要#include<stdio.h>

然而这个C语言接口fflush的参数只有一个FILE*类型的指针, 通过阅读本篇博客, 我们已经知道了FILE内部封装了fd, 是C语言对于系统级别接口的封装, 既然FILE是一个结构体, 那么结构体内部就一定封装了其他东西, 所以用户级缓冲区又是由语言提供, 且每个文件都会有自己的缓冲区, 那么FILE结构体内部不仅封装了文件描述符fd, 同时也封装了用户级缓冲区

2).代码验证用户级缓冲区的存在

这一块的理解需要有对fork创建子进程有很深刻的认识作为铺垫

fork详解, 传送入口: 

http://t.csdn.cn/oHVvi

http://t.csdn.cn/1Kmsk

#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#include<string.h>
int main()
{
  int fd = open("log.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);
  if(fd < 0)
  {
    //打开or创建文件失败
    perror("open: "); 
    return 2;
  }
  //输出重定向
  dup2(fd, 1);
  
  //向显示器写入, 但由于已经做了输出重定向
  //所以向显示器写入的内容都写入到了文件中
  //既然最终写入到了文件中, 那么就要遵守文件的缓冲区刷新规则:满刷新
  
  //以下每个写入的字符串都带上'\n'这里可以演示不做输出重定向,直接向显示器写入,会发生不同的现象
  //系统接口
  const char* str1 = "hello write\n";
  write(1, str1, strlen(str1));

  //语言接口
  const char* str2 = "hello fwrite\n";
  const char* str3 = "hello fprintf\n";
  const char* str4 = "hello fputs\n";
  fwrite(str2, strlen(str2), 1, stdout);
  fprintf(stdout, "%s", str3);
  fputs(str4, stdout);
  
  //以上逻辑全部执行完毕之后,创建子进程,调用fork函数
  fork();

  return 0;
}

先回顾一下, 显示器的刷新策略 -- 行刷新, 文件的刷新策略 -- 满刷新

这里虽然是向显示器输出, 但是我们是在输出之前做了输出重定向的, 所以我们所看到的向显示器输出, 就变为向文件输出

缓冲区的刷新策略也自然就是满刷新

以上这段代码, 在fork函数调用之前, 是没有刷新缓冲区的, 也就是说此时缓冲区中的内容还存在

缓冲区是属于父进程的, 自然也就是属于父进程中的数据, 那在创建子进程时, 子进程会拷贝父进程的数据, 会连同打开的文件的缓冲区以写时拷贝的方式拷贝下来

当父子进程结束时, 一定会有一个进程先结束, 假设在这里是子进程先结束, 进程结束就会刷新子进程的缓冲区

重点: 缓冲区是子进程中的数据, 将缓冲区的内容刷新出去, 是一种修改操作(也可以理解为是一种写的操作), 此时父进程中的缓冲区发生拷贝, 复制了一份映射到了新的物理内存中, 当父进程再刷新时, 又将父进程中缓冲区的内容刷新出去了, 这就解释了为什么语言级别的接口会被打印两次

那么, 为什么系统接口write只打印了一次呢?

因为语言级别接口是要经过一层用户级缓冲区的, 也就是先向用户级缓冲区内输出, 而系统级别的接口直接向内核级缓冲区写入

并且语言级别的缓冲区底层也是要调用系统接口的, 例如fflush底层调用write将数据刷新出去

系统接口不会向用户级缓冲区进行写入, 所以也就解释了为什么只有write只会打印一次

所以, 经过以上的验证, 还可以说明一个问题: 尽量在向一个文件写入完毕之后手动刷新缓冲区, 避免出现奇怪现象 

4.自己设计一个用户级缓冲区

目的: 模拟实现文件操作接口,理解用户级缓冲区的存在,以及理解用户级缓冲区如何控制刷新策略

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

//模拟实现FILE,内部封装文件描述符与缓冲区
typedef struct MyFile
{
  int fd;//文件描述符
  char buffer[64];//缓冲区
  int end;//记录缓冲区中存储字符个数
}MyFile;

MyFile* MyFopen(const char* pathname, const char* mode)
{
  MyFile* fp = NULL;
  if(strcmp(mode, "r") == 0)
  {

  }
  else if(strcmp(mode, "r+") == 0)
  {

  }
  else if(strcmp(mode, "w") == 0)
  {
    int fd = open(pathname, O_CREAT | O_TRUNC | O_WRONLY, 0666);
    if(fd >= 0)
    {
      fp = (MyFile*)malloc(sizeof(MyFile));
      memset(fp, 0, sizeof(MyFile));
      fp->fd = fd;
    }
  }
  else if(strcmp(mode, "w+") == 0)
  {

  }
  else if(strcmp(mode, "a") == 0)
  {

  }
  else if(strcmp(mode, "a+") == 0)
  {

  }
  else
  {
    //do nothing
  }
  return fp;
}
int MyFflush(MyFile* fp);
int MyFputs(const char* str, MyFile* fp)
{
  //本质上是先写入到用户级缓冲区
  strcpy(fp->buffer+fp->end, str);
  fp->end += strlen(str);

  //还未刷新时
  //for debug
  //这里由于一会原本1号文件描述符已经关闭,后打开的是文件,所以为了验证打印到显示器上,就直接打印到标准错误上
  fprintf(stderr, "缓冲区中的内容: %s", fp->buffer);
  sleep(1);

  //根据不同的打开文件,采用对应的刷新策略
  if(fp->fd == 0)
  {
    //键盘的刷新策略
  }
  else if(fp->fd == 1)
  {
    //显示器的刷新策略
    //显示器 -- 行刷新 -- 即遇到'\n'就刷新
    if(fp->buffer[fp->end - 1] == '\n')
    {
      MyFflush(fp);
      fp->end = 0;
    }
  }
  else if(fp->fd == 2)
  {
    //显示器的刷新策略
  }
  else if(fp->fd == 3)
  {
    //一般情况下,新打开的文件的刷新策略
  }
  //else if(){...}
  else
  {

  }
  return 1;
}

int MyFflush(MyFile* fp)
{
  //刷新缓冲区
  if(fp->end != 0)
  {
    write(fp->fd, fp->buffer, strlen(fp->buffer));
    fp->end = 0;
    fsync(fp->fd);//将数据从内核级缓冲区刷新到磁盘(硬件)
  }
  return fp->end;
}

int MyClose(MyFile* fp)
{
  //刷新缓冲区
  MyFflush(fp);
  //关闭文件流
  close(fp->fd);
  return 0;
}

int main()
{
  //由于上面只实现了fd为1的缓冲区刷新策略,这里先关闭1,将新打开的文件分配到1号文件描述符
  close(1);
  MyFile* fp = MyFopen("log.txt", "w");
  if(fp == NULL)
  {
    perror("MyFopen: ");
    return 2;
  }
  //fp->fd == 1,采用显示器的行刷新策略
  MyFputs("hello world 1", fp);
  MyFputs("hello world 2\n", fp);
  MyFputs("hello world 3", fp);
  MyFputs("hello world 4\n", fp);
  
  MyClose(fp);
  return 0;
}

结论: 用C语言举例, 文件的用户级缓冲区是被封装在struct FILE中的, 文件用户级缓冲区的刷新策略是在fwrite/fputs/fprintf中实现的

补充一: fsync系统调用 -- 真正将数据写入到磁盘硬件上

补充二: 显示器采用的刷新策略是行刷新, 那么为什么我们在打字的时候, 打出去的每一个字我们都可以从显示器看到呢

当我们在输入是, 本质是进程从键盘文件读数据, 而我们从显示器上面看到了, 并且是立即刷新的刷新策略, 这是通过显示器回显的方式, 提供用户的使用体验, 那么也就是说同样的都是显示器文件, 却同时拥有两种不同的刷新策略, 当进程向显示器输出时一般采用行刷新, 当进程读取用户输入的数据而又通过显示器文件回显到显示器上, 采用立即刷新策略, 也就是说, 显示器或者是同一个文件可以同时具有不同的刷新策略

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

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

相关文章

老系统如何重构之最全总结

目录 1. 重构的概念 1.1 重构的定义 1.2 重构的分类 2 为什么重构 3 如何重构 3.1 说服业务方 3.2 确定重构的目标 3.3 老系统的熟悉与梳理 3.4 数据库的重构 3.5 前后端的系统重构 3.6 数据迁移与检查 3.7 系统检查联调测试 3.8 系统切换 1. 重构的概念 1.1 重构…

DlhSoft Gantt Chart Hyper Library for HTML5 Standard Edition

DlhSoft Gantt Chart Hyper Library 甘特图超级库包括一组交互式计划组件&#xff0c;可用于使用纯 JavaScript、TypeScript 或 Angular、React 或 Vue 等框架构建的启用时间线的应用程序 基于 JavaScript 的甘特图 可定制的网格列、汇总的工作分解结构、带有可拖动条和依赖线…

静态HTML网页设计作品 DIV布局家乡介绍网页模板代码---(太原 10页带本地存储登录注册 js表单校验)

⛵ 源码获取 文末联系 ✈ Web前端开发技术 描述 网页设计题材&#xff0c;DIVCSS 布局制作,HTMLCSS网页设计期末课程大作业 | 家乡旅游景点 | 家乡民生变化 | 介绍自己的家乡 | 我的家乡 | 家乡主题 | HTML期末大学生网页设计作业 HTML&#xff1a;结构 CSS&#xff1a;样式 在…

基于ssm jsp超市在线销售平台的设计与实现

近年来&#xff0c;网络信息技术的迅猛发展&#xff0c;互联网逐渐渗透到人们日常生活中的方 方面面&#xff0c;而给我们的生活带来巨大变化的电子商务正在以前所未有的速度蓬勃发 展&#xff0c;电子商务也成为网络研究与应用的热点之一。网上商店是电子商务的重要方 面&…

【干货】教你在十分钟内编译一个Linux内核,并在虚拟机里运行!

前言 这篇文章将会简单的介绍如何在Linux系统上面&#xff0c;编译一个5.19的内核&#xff0c;然后在QEMU虚拟机中运行。 下载Linux内核源码 首先&#xff0c;我们需要下载Linux的代码&#xff1a; https://mirrors.edge.kernel.org/pub/linux/kernel/v5.x/linux-5.19.10.t…

使用vue互联QQ音乐完成网站音乐播放器

&#x1f3b6; 文章简介&#xff1a;使用vue互联QQ音乐完成网站音乐播放器 &#x1f4a1; 创作目的&#xff1a;记录使用APlayer播放器MetingJs实现 在线播放qq音乐、网易云音…等平台的音乐 ☀️ 今日天气&#xff1a;2022-11-19 小雨多云 天空灰蒙蒙的 &#x1f972; &#x…

还在付费使用 XShell?我选择这款超牛逼的 SSH 客户端,完全免费

分享过FinallShell这款SSH客户端&#xff0c;也是xiaoz目前常用的SSH客户端工具&#xff0c;FinalShell使用起来方便顺手&#xff0c;但令我不爽的是tab数量变多的时候FinalShell越来越卡&#xff0c;而且内存占用也比较高。 最近发现一款使用使用C语言开发的跨平台SSH客户端W…

【无人机】基于Matlab的四旋翼无人机控制仿真

✅作者简介&#xff1a;热爱科研的Matlab仿真开发者&#xff0c;修心和技术同步精进&#xff0c;matlab项目合作可私信。 &#x1f34e;个人主页&#xff1a;Matlab科研工作室 &#x1f34a;个人信条&#xff1a;格物致知。 更多Matlab仿真内容点击&#x1f447; 智能优化算法 …

uni-app —— 下拉刷新 上拉加载

文章目录 前言一、下拉刷新 1.开启下拉刷新2.监听下拉刷新3.关闭下拉刷新二、上拉加载总结一、下拉刷新 1. 开启下拉刷新 在uni-app中有两种方式开启下拉刷新 需要在 ​​pages.json ​​​ 里&#xff0c;找到的当前页面的pages节点&#xff0c;并在​​style​​​ 选项中开…

这次把怎么做好一个PPT讲清-总体篇

文章目录一、背景二、图表化、图示化三、关键词设计四、版式层级五、逻辑关系图**1&#xff09;常用逻辑****2&#xff09;如何让逻辑关系图好看**六、对齐、分组和对比**对齐****分组****分组就是将同类得信息放在一起&#xff0c;靠的更近一点**那么&#xff0c;实现分组原则…

基于S32K144实现TPS929120的基本控制功能

文章目录前言1.TPS92910简介2.硬件调试平台2.1 灯板原理图2.2 参考电流2.3 器件地址3.TPS929120通信协议3.1 物理层3.2 数据链路层3.3 传输层2.3.1 读写时序2.3.2 帧格式说明2.3.3 寄存器lock与unlock2.3.4 输出通道控制4.使用S32K144驱动TPS929104.1 实现命令帧格式4.1.1 写寄…

【云原生】玩转Kubernetes实战(一):Pod、ConfigMap的使用

本文主要是利用Kubernetes 集群搭建出一个 WordPress 网站&#xff0c;用了三个镜像&#xff1a;WordPress、MariaDB、Nginx。 下面是其简单的架构图&#xff0c;用于直观的展示这个系统的内部逻辑关系&#xff1a; 简单来说&#xff0c;就是要通过本地地址http://127.0.0.1…

Spring AOP[详解]

一.需求引入 在开发过程中,总会有一些功能与业务逻辑代码耦合度不强(例如保存日志,提交事务,权限验证,异常处理),我们可以将这些代码提取到一个工具类中,需要使用时在调用工具类来实现. ​ 但是这样也会有弊端,那就是我们的代码已经开发完毕,后期如果需要增加公共功能就需要更…

Pinpoint--基础--03--安装部署

Pinpoint–基础–03–安装部署 前提 使用hd用户登陆 完成基础环境搭建https://blog.csdn.net/zhou920786312/article/details/118212302代码位置 https://gitee.com/DanShenGuiZu/learnDemo/tree/master/pinpoint-learn/demo11、安装环境准备 1.1、jdk1.8 基础环境搭建 包含…

一文搞懂MySQL表字段类型长度的含义

不知道大家第一眼看标题的时候有没有理解&#xff0c;什么是“字段类型长度”&#xff0c;这里我来解释下&#xff0c;就比如我们在MySQL建表的时候&#xff0c;比如下面这个建表语句&#xff1a; CREATE TABLE user (id int(10) DEFAULT NULL,name varchar(50) DEFAULT NULL,…

linux系统离线安装docker(分步法一键法)

1 前言 在有的项目场景中&#xff0c;服务器是不允许连接外网的。此时若想在服务器上安装部署docker容器&#xff0c;就不能采用在线方式了&#xff0c;不过可以采取离线方式进行安装。下面我们就一起看看离线安装的两种办法。 一种是分步安装法&#xff0c;一种是一键安装法…

Python冷知识:如何找出新版本增加或删除了哪些标准库?

“内置电池”是 Python 最为显著的特性之一&#xff0c;它提供了 200 多个开箱即用的标准库。但是&#xff0c;历经了 30 多年的发展&#xff0c;很多标准库已经成为了不得不舍弃的历史包袱&#xff0c;因为它们正在“漏电”&#xff01; 好消息是&#xff0c;Python 正在进行…

Pinpoint--基础--02--架构设计

Pinpoint–基础–02–架构设计 1、整体架构 1.1、Pinpoint Collector 数据收集模块&#xff0c;接收Agent发送过来的监控数据&#xff0c;并存储到HBase部署在 Web 容器上 1.2、Pinpoint Web 监控展示模块&#xff0c;展示系统调用关系、调用详情、应用状态等&#xff0c;并…

CleanMyMac磁盘空间内存瘦身清理软件使用教程

许多用着Mac系统电脑的朋友们总是卸载不干净电脑垃圾软件&#xff0c;想要把垃圾软件卸载干净&#xff0c;可以尝试使用苹果电脑清理软件CleanMyMac。 经典的电脑深度清理软件——CleanMyMac。由于苹果电脑硬盘售价高昂&#xff0c;且不可以自行安装内存&#xff0c;很多苹果用…

代码随想录day60|结束亦是开始|84.柱状图中最大的矩形|总结

代码随想录day60 来了老弟 84.柱状图中最大的矩形 思路 本题和42. 接雨水是遥相呼应的两道题目&#xff0c;建议都要仔细做一做&#xff0c;原理上有很多相同的地方&#xff0c;但细节上又有差异&#xff0c;更可以加深对单调栈的理解&#xff01;42. 接雨水 其实这两道题目先…