Linux:6_基础IO

基础IO

一.理解"文件"

  • 文件分类
    1.内存级(被打开)文件
    2.磁盘级文件

1. 狭义理解

  • 文件在磁盘里
  • 磁盘是永久性存储介质,因此文件在磁盘上的存储是永久性的
  • 磁盘是外设 (即是输出设备也是输入设备)
  • 磁盘上的文件本质是对文件的所有操作,都是对外设的输入和输出简称IO

2. 广义理解

  • Linux 下一切皆文件(键盘、显示器、网卡、磁盘…… 这些都是抽象化的过程)(后面会讲如何去理解)

3. 文件操作的归类认知

  • 对于 0KB 的空文件是占用磁盘空间的
  • 文件是文件属性(元数据)和文件内容的集合(文件 = 属性(元数据)+ 内容)
  • 所有的文件操作本质是文件内容操作和文件属性操作

4. 系统角度

  • 对文件的操作本质是进程对文件的操作

    访问文件,需要先打开文件!
    谁打开文件?进程打开的文件!
    对文件的操作,本质是进程对文件的操作!

  • 磁盘的管理者是操作系统

  • 文件的读写本质不是通过 C 语言 / C++ 的库函数来操作的(这些库函数只是为用户提供方便),而是通过文件相关的系统调用接口来实现的(fopen,fclose…等库封装了底层os的文件系统调用!)

二.回顾C文件接口

(见C语言进阶文件博客)

C语言基础:文件操作与数据持久化,-CSDN博客

1. hello.c打开文件

image-20250904165150227

打开的myfile文件在哪个路径下?

  • 在程序的当前路径下,那系统怎么知道程序的当前路径在哪里呢?

可以使用 ls /proc/[进程id] -l 命令查看当前正在运行进程的信息:

image-20250904165225209

其中:

  • cwd:指向当前进程运行目录的一个符号链接。
  • exe:指向启动当前进程的可执行文件(完整路径)的符号链接。

打开文件,本质是进程打开,所以,进程知道自己在哪里,即便文件不带路径,进程也知道。由此OS就能知道要创建的文件放在哪里。

2. hello.c写文件

image-20250904165344098

3. hello.c读文件

image-20250904165520705

稍作修改,实现简单cat命令:

image-20250904165646087

4. 输出信息到显示器,你有哪些方法

image-20250904165718999

5. stdin & stdout & stderr

  • C默认会打开三个输入输出流,分别是stdin, stdout, stderr
  • 仔细观察发现,这三个流的类型都是FILE, fopen返回值类型,文件指针*

image-20250905011019324

6. 打开文件的方式

image-20250905011126848

如上,是我们之前学的文件相关操作。还有fseek ftell rewind 的函数,在C部分已经有所涉猎,请同学们自行复习。

补充:往文件里写字符串不要加0

三.系统文件I/O

打开文件的方式不仅仅是fopen,ifstream等流式,语言层的方案,其实系统才是打开文件最底层的方案。不过,在学习系统文件IO之前,先要了解下如何给函数传递标志位,该方法在系统文件IO接口中会使用到:

1. 一种传递标志位的方法(本质就是位图)

#include "stdio.h"#define ONE_FLAG (1<<0) //00000000 0000 00000000 00000000 00000001
#define TWO_FLAG (1<<1) //00000000 0000 00000000 00000000 00000010
#define THREE_FLAG (1<<2) //00000000 0000 00000000 00000000 00000100
#define FOUR_FLAG (1<<3) //00000000 0000 00000000 00000000 00001000void Print(int flags)
{if(flags & ONE_FLAG){printf("One\n");}if(flags & TWO_FLAG){printf("Two\n");}if(flags & THREE_FLAG){printf("Three\n");}if(flags & FOUR_FLAG){printf("Four\n");}
}int main()
{Print(ONE_FLAG);printf("\n");Print(ONE_FLAG | TWO_FLAG);printf("\n");Print(ONE_FLAG | TWO_FLAG | THREE_FLAG);printf("\n");Print(ONE_FLAG | TWO_FLAG | THREE_FLAG | FOUR_FLAG);printf("\n");Print(ONE_FLAG | FOUR_FLAG);printf("\n");return 0;
}
//结果
OneOne
TwoOne
Two
ThreeOne
Two
Three
FourOne
Four

操作文件,除了上小节的C接口(当然,C++也有接口,其他语言也有),我们还可以采用系统接口来进行文件访问, 先来直接以系统代码的形式,实现和上面一模一样的代码:

2. hello.c 写文件

image-20250905011606426

2. hello.c读文件

image-20250905011726790

4. 接口介绍

open

image-20250905012210052

mode_t通常被定义为一个无符号整数,在大多数Linux平台上typedef unsigned int mode_t;

open 函数具体使用哪个,和具体应用场景相关,如目标文件不存在,需要open创建,则第三个参数表示创建文件的默认权限,否则,使用两个参数的open。

write read close lseek ,类比C文件相关接口。

(0).补充
①.ssize_t

ssize_t 是一个有符号整数类型,

typedef long ssize_t; 		//大多数操作系统中

%zd 是专门用来打印 ssize_t 的格式化符号。

②.off_t
typedef long off_t;        // 在 32 位系统上
typedef long long off_t;   // 在部分 64 位系统上
// 打印常用
off_t size;
printf("%lld\n", (long long)size);
(1).read

read的作用是读文件

#include <unistd.h>ssize_t read(int fd, void *buf, size_t count);
  • 参数
    • fd:文件描述符。
    • buf:读到的数据存放到这个缓冲区。
    • count:希望读取的字节数。
  • 返回值
    • 成功:实际读取的字节数(遇到文件结尾时返回0)。
    • 失败:-1,并设置 errno。
(2).write

write的作用是写文件

#include <unistd.h>ssize_t write(int fd, const void *buf, size_t count);
  • 参数
    • fd:文件描述符。
    • buf:要写的数据。
    • count:写多少字节。
  • 返回值
    • 成功:实际写入的字节数。
    • 失败: -1,并设置 errno。
(3).close

close的作用是关闭一个已经打开的文件描述符;自动刷新缓冲区

函数原型

#include <unistd.h>int close(int fd);
  • 参数 fd:文件描述符。
  • 返回值
    • 成功 :0
    • 失败: -1,并设置 errno。
(4).lseek

lseek的作用是改变文件读写光标位置

#include <unistd.h>
#include <sys/types.h>off_t lseek(int fd, off_t offset, int whence);
  • 参数
    • fd:文件描述符。
    • offset:偏移量,可以是正数、负数。
    • whence:参照点:
      • SEEK_SET:从文件开头偏移 。
      • SEEK_CUR:从当前位置偏移 。
      • SEEK_END:从文件末尾偏移 。
  • 返回值
    • 成功:新的光标位置(以字节为单位)
    • 失败:返回 -1。

5. open函数返回值

在认识返回值之前,先来认识一下两个概念: 系统调用库函数

  • 上面的fopen fclose fread fwrite 都是C标准库当中的函数,我们称之为库函数(libc)。
  • open close read write lseek 都属于系统提供的接口,称之为系统调用接口
  • 回忆一下我们讲操作系统概念时,画的一张图

image-20250905013143095

系统调用接口和库函数的关系,一目了然。

所以,可以认为, f# 系列的函数,都是对系统调用的封装,方便二次开发。

6. 文件描述符fd

  • 通过对open函数的学习,我们知道了文件描述符就是一个小整数

    image-20250908093347249

(1).0 & 1 & 2
  • Linux进程默认情况下会有3个缺省打开的文件描述符,分别是标准输入0, 标准输出1, 标准错误2.

  • 0,1,2对应的物理设备一般是:键盘,显示器,显示器

    所以输入输出还可以采用如下方式:

image-20250905013333162

image-20250908104803520

而现在知道,文件描述符就是从0开始的小整数。当我们打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件。于是就有了file结构体。表示一个已经打开的文件对象。而进程执行open系统调用,所以必须让进程和文件关联起来。每个进程都有一个指针*files, 指向一张表files_struct,该表最重要的部分就是包含一个指针数组,每个元素都是一个指向打开文件的指针!所以,本质上,文件
描述符就是该数组的下标。所以,只要拿着文件描述符,就可以找到对应的文件。

对于以上原理结论我们可通过内核源码验证:

首先要找到task_struct结构体在内核中为位置,地址为:/usr/src/kernels/3.10.0-1160.71.1.el7.x86_64/include/linux/sched.h(3.10.0-1160.71.1.el7.x86_64是内核版本,可使用uname -a自行查看服务器配置, 因为这个文件夹只有一个,所以也不用刻意去分辨,内核版本其实也随意)

  • 要查看内容可直接用vscode在windows下打开内核源代码

  • 相关结构体所在位置

    • struct task_struct/usr/src/kernels/3.10.0-1160.71.1.el7.x86_64/include/linux/sched.h

    • struct files_struct/usr/src/kernels/3.10.0-1160.71.1.el7.x86_64/include/linux/fdtable.h

    • struct file/usr/src/kernels/3.10.0-1160.71.1.el7.x86_64/include/linux/fs.h

image-20250908105602560

(2).文件描述符的分配规则

直接看代码:

image-20250908105641029

输出发现是fd: 3

关闭0或者2,再看

image-20250908105712451

发现是结果是: fd: 0 或者fd 2 ,可见,文件描述符的分配规则:在files_struct数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符。

(3).重定向
  • 重定向=打开文件的方式+dup2

那如果关闭1呢?看代码:

image-20250908105956412

此时,我们发现,本来应该输出到显示器上的内容,输出到了文件myfile 当中,其中,fd=1。这种现象叫做输出重定向。常见的重定向有: > , >> , <

那重定向的本质是什么呢?重定向的本质是更改文件描述符表的指针指向,数组下标不变

image-20250908110221589

重定向完整写法是:

command fd>filename/&fd fd>filename/fd ... #(可同时重定向多个)
即:command 文件描述符>文件名/&文件描述符 文件描述符>文件名/文件描述符 ... #(可同时重定向多个)

例:

//stream.cc
int main()
{//向标准输出打印,stdout,cout->1std::cout<<"hello cout"<<std::endl;    printf("hello printf\n");//向标准错误进行打印,stderr, cerr->2, 显示器std::cerr << "hello cerr" <<std::endl;fprintf(stderr, "hello stderr\n");return 0;
}
czj@iv-ye46gvrx8gcva4hc07x0:linux$ ./stream 1>log.normal 2>log.error
#运行stream, 屏幕无打印, 标准输出打印到了log.normal, 标准错误打印到了log.error.

注:

  1. 不写文件描述符,则默认</>/>>左边文件描述符是1

  2. 可同时重定向多个

    ./stream 1>log.txt 2>log.txt
    

    可以将1和2重定向到同一文件中,但是文件只会有标准错误打印的内容,因为2重定向的时候清空了文件;

    czj@iv-ye46gvrx8gcva4hc07x0:lesson20$ cat log.txt 
    hello cerr
    hello stderr
    

解决办法:

#法一:后面的改成追加重定向
./stream 1>log.txt 2>>log.txt
#法二:fd>&fd(我们一般多用法二)
./stream 1>log.txt 2>&1
#法三:&>filename
./stream &>log.txt #完全等价法二
  • &后面跟文件描述符,区分文件夹名;

    n>&m = dup2(m, n)

  • &> 是 Bash 的一种简写语法,表示把 标准输出 (fd=1) 和 标准错误 (fd=2) 一起重定向到同一个文件。

  1. 为什么存在一个标准错误呢?

    答:可以通过重定向能力,把常规消息和错误消息(printf/perror和cout/cerr)进行分离!

(4).使用 dup2 系统调用
  • 注:进程替换不影响重定向的结果

函数原型如下:进行重定向的系统调用

image-20250908111341680

示例代码

image-20250908111723471

printf是C库当中的IO函数,一般往 stdout 中输出,但是stdout底层访问文件的时候,找的还是fd:1,但此时,fd:1下标所表示内容,已经变成了./log的地址,不再是显示器文件的地址,所以,输出的任何消息都会往文件中写入,进而完成输出重定向。追加和输入重定向同理。

(5).在minishell中添加重定向功能
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <ctype.h>
#include <unistd.h>
#include <cstring>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unordered_map>#define COMMAND_SIZE 1024
#define FORMAT "[%s@%s %s]# "// 下面是shell定义的全局变量
// 1.命令行参数表
#define MAXARGC 128
char* g_argv[MAXARGC];
int g_argc = 0;// 2.环境变量表
#define MAX_ENVS 100
char* g_env[MAX_ENVS];
int g_envs = 0;// 3.别名映射表
std::unordered_map<std::string,std::string> g_alias; //提一下,不写了.// 4.关于重定向,我们关心的内容
#define NONE_REDIR 0
#define INPUT_REDIR 1
#define OUTPUT_REDIR 2
#define APPEND_REDIR 3int redir = NONE_REDIR;
std::string filename;// for test
char cwd[1024];
char cwdenv[1024];// last exit code
int lastcode=0;const char* GetUserName()
{const char* name = getenv("USER");return name==NULL?"None":name;
}const char* GetHostName()
{//const char* hostname = getenv("HOSTNAME");static char hostname[64];int i = gethostname(hostname,64);return i==-1?"None":hostname;
}const char* GetPwd()
{//const char* pwd = getenv("PWD");const char* pwd = getcwd(cwd,sizeof(cwd));if(pwd != NULL){// 法1:遍历g_env更新环境变量if (getenv("PWD") == NULL){snprintf(cwdenv, sizeof(cwdenv), "PWD=%s", cwd);putenv(cwdenv);g_env[g_envs++] = cwdenv;g_env[g_envs] = NULL;}else{for (int i = 0; i < g_envs; i++){if (strncmp(g_env[i], "PWD=", 4) == 0){snprintf(g_env[i], strlen(g_env[i]) + 1, "PWD=%s", cwd);putenv(g_env[i]);break;}}}//法2:用cwdenv临时变量// snprintf(cwdenv,sizeof(cwdenv),"PWD=%s",cwd);// putenv(cwdenv);}return pwd==NULL?"None":pwd;
}const char* GetHome()
{const char* home = getenv("HOME");return home == NULL ? "" : home;
}void InitEnv()
{extern char **environ;memset(g_env,0,sizeof(g_env));g_envs = 0;//本来要从配置文件来//1.获取环境变量for(int i = 0;environ[i];i++){// 1.1申请空间g_env[i] = (char*)malloc(strlen(environ[i])+1);strcpy(g_env[i],environ[i]);g_envs++;}g_env[g_envs++] = (char*)"HAHA=for_test"; //测试标识g_env[g_envs] = NULL;//2.导成环境变量for(int i = 0; g_env[i];i++){putenv(g_env[i]);}environ = g_env;
}//command
bool Cd()
{if (g_argc == 1){std::string home = GetHome();if (home.empty())return true;chdir(home.c_str());}else{std::string where = g_argv[1];// cd - / cd ~if (where == "-"){// Todu}else if (where == "~"){// Todu}else{chdir(where.c_str());}}return true;
}void Echo()
{if (g_argc == 2){// echo "hello world"// echo $?// echo $PATHstd::string option = g_argv[1];if (option == "$?"){std::cout << lastcode << std::endl;lastcode = 0;}else if (option[0] == '$'){std::string env_name = option.substr(1);const char *env_value = getenv(env_name.c_str());if (env_value){std::cout << env_value << std::endl;}}else{std::cout << option << std::endl;}}
}//切割得到当前目录名
std::string DirName(const char* pwd)
{
#define SLASH "/" //注意:局部定义define外部也可以用std::string dir = pwd;if(dir==SLASH)return SLASH;auto pos = dir.rfind(SLASH);if(pos == std::string::npos)return "BUG?";return dir.substr(pos+1);
}void MakeCommandLine(char cmd_prompt[],int size)
{snprintf(cmd_prompt,size,FORMAT,GetUserName(),GetHostName(),DirName(GetPwd()).c_str());//snprintf(cmd_prompt,size,FORMAT,GetUserName(),GetHostName(),GetPwd());
}void PrintCommandPrompt()
{char prompt[COMMAND_SIZE];MakeCommandLine(prompt,sizeof(prompt));printf("%s",prompt);fflush(stdout);
}bool GetCommandLine(char *out,int size)
{char *c = fgets(out,size,stdin);         if(c==NULL)return false;out[strlen(out)-1]=0;//清理\nif(strlen(out)==0)return false;return true;
}bool CommandParse(char* commandline)
{
#define SEP " "//命令行分析"ls -a -l" -> "ls" "-a" "-l"g_argc = 0;g_argv[g_argc++] = strtok(commandline,SEP);while(g_argv[g_argc++] = strtok(nullptr,SEP));g_argc--;return g_argc > 0 ? true : false;
}void PrintArgv()
{for(int i = 0; g_argv[i];i++){printf("g_argv[%d]->%s\n",i,g_argv[i]);}printf("argc:%d\n",g_argc);
}//检查是否是内键命令(有些内键命令磁盘上也有一份,是为了处理shell本身,shell脚本能用)
bool CheckAndExecBuiltin()
{//内键命令重定向,采用打开一个临时文件,用0/1/2覆盖临时文件,再dup2,完事再从临时文件换回来std::string cmd = g_argv[0];if(cmd=="cd"){Cd();return true;}else if(cmd=="echo"){Echo();return true;}else if(cmd=="export"){//...}else if(cmd=="alias"){//...}return false;
}int Execute()
{pid_t id = fork();if(id == 0){//子进程检查重定向情况int fd = -1;if(redir == INPUT_REDIR){fd = open(filename.c_str(), O_RDONLY);if(fd<0) exit(1);dup2(fd,0);close(fd);}else if(redir == OUTPUT_REDIR){umask(0);fd = open(filename.c_str(), O_CREAT | O_WRONLY | O_TRUNC,0666);if(fd<0) exit(2);dup2(fd,1);close(fd);}else if(redir == APPEND_REDIR){umask(0);fd = open(filename.c_str(), O_CREAT | O_WRONLY | O_APPEND,0666);if(fd<0) exit(2);dup2(fd,1);close(fd);}else{}//child//进程替换不影响重定向的结果execvp(g_argv[0],g_argv);exit(1);}int status = 0;//fatherpid_t rid = waitpid(id,&status,0);if(rid>0){lastcode = WEXITSTATUS(status);}(void)rid; //用一下rid省的编译器告警return 0;
}void clearup()
{for(int i = 0; i < g_envs; i++){free(g_env[i]);}
}void TrimSpace(char cmd[],int& end)
{while(isspace(cmd[end])){end++;}
}void RedirChech(char cmd[])
{//清空上条命令残留数据redir = NONE_REDIR;filename.clear();int start = 0;int end = strlen(cmd)-1;while(end > start){if(cmd[end] == '<'){cmd[end++] = 0;TrimSpace(cmd,end);redir = INPUT_REDIR;filename = &cmd[end];//或filename = cmd+endbreak;}else if(cmd[end] == '>'){if(cmd[end-1] == '>'){// >>cmd[end-1] = 0;cmd[end++] = 0;TrimSpace(cmd,end);redir = APPEND_REDIR;filename = &cmd[end];//或filename = cmd+endbreak;}else{// >cmd[end++] = 0;TrimSpace(cmd,end);redir = OUTPUT_REDIR;filename = &cmd[end];//或filename = cmd+endbreak;}}else{end--;}}
}int main()
{// shell启动的时侯,从系统中获取环境变量// 真实的shell从配置文件中读,但是配置文件是shell脚本,目前看不懂// 我们的环境变量信息直接从父shell统一来InitEnv();while(true){//1.输出命令行提示符PrintCommandPrompt();//2.获取用户输入的命令char commandline[COMMAND_SIZE];if(!GetCommandLine(commandline,sizeof(commandline)))continue;//3.重定向分析"ls -a -l > file.txt" -> "ls -a -l" "file.txt" -> 判断重定向方式RedirChech(commandline);// printf("redir:%d,filename:%s\n",redir,filename.c_str());//4.命令行分析"ls -a -l" -> "ls" "-a" "-l"if(!CommandParse(commandline))continue; //PrintArgv();//可补:检查别名,替换命令//5.检查并处理内键命令if(CheckAndExecBuiltin())continue;//6.执行命令Execute();} //释放堆空间clearup();return 0;
}

四.理解“一切皆文件”

首先,在windows中是文件的东西,它们在linux中也是文件;其次一些在windows中不是文件的东西,比如进程、磁盘、显示器、键盘这样硬件设备也被抽象成了文件,你可以使用访问文件的方法访问它们获得信息;甚至管道,也是文件;将来我们要学习网络编程中的socket(套接字)这样的东西,使用的接口跟文件接口也是一致的。

这样做最明显的好处是,开发者仅需要使用一套 API 和开发工具,即可调取 Linux 系统中绝大部分的资源。举个简单的例子,Linux 中几乎所有读(读文件,读系统状态,读PIPE)的操作都可以用read 函数来进行;几乎所有更改(更改文件,更改系统参数,写 PIPE)的操作都可以用 write 函数来进行。

之前我们讲过,当打开一个文件时,操作系统为了管理所打开的文件,都会为这个文件创建一个file结构体,该结构体定义在/usr/src/kernels/3.10.0-1160.71.1.el7.x86_64/include/linux/fs.h 下,以下展示了该结构部分我们关系的内容:

image-20250908143413114

值得关注的是struct file 中的f_op 指针指向了一个file_operations 结构体,这个结构体中的成员除了struct module* owner 其余都是函数指针。该结构和struct file 都在fs.h下。

image-20250908143605967

file_operation 就是把系统调用和驱动程序关联起来的关键数据结构,这个结构的每一个成员都对应着一个系统调用。读取file_operation 中相应的函数指针,接着把控制权转交给函数,从而完成了Linux设备驱动程序的工作。

介绍完相关代码,一张图总结:

image-20250908143806110

上图中的外设,每个设备都可以有自己的read、write,但一定是对应着不同的操作方法!!但通过struct file 下file_operation 中的各种函数回调,让我们开发者只用file便可调取 Linux 系统中绝大部分的资源!!这便是“linux下一切皆文件”的核心理解。

五.缓冲区

1. 什么是缓冲区(内存的一段空间)

缓冲区是内存空间的一部分。也就是说,在内存空间中预留了一定的存储空间,这些存储空间用来缓冲输入或输出的数据,这部分预留的空间就叫做缓冲区。缓冲区根据其对应的是输入设备还是输出设备,分为输入缓冲区和输出缓冲区。

2. 为什么要引入缓冲区机制(提高效率:提高使用者的效率)

读写文件时,如果不会开辟对文件操作的缓冲区,直接通过系统调用对磁盘进行操作(读、写等),那么每次对文件进行一次读写操作时,都需要使用读写系统调用来处理此操作,即需要执行一次系统调用,执行一次系统调用将涉及到CPU状态的切换,即从用户空间切换到内核空间,实现进程上下文的切换,这将损耗一定的CPU时间,频繁的磁盘访问对程序的执行效率造成很大的影响。系统调用是有成本的!

为了减少使用系统调用的次数,提高效率,我们就可以采用缓冲机制。比如我们从磁盘里取信息,可以在磁盘文件进行操作时,可以一次从文件中读出大量的数据到缓冲区中,以后对这部分的访问就不需要再使用系统调用了,等缓冲区的数据取完后再去磁盘中读取,这样就可以减少磁盘的读写次数,再加上计算机对缓冲区的操作大 快于对磁盘的操作,故应用缓冲区可大 提高计算机的运行速度。

又比如,我们使用打印机打印文档,由于打印机的打印速度相对较慢,我们先把文档输出到打印机相应的缓冲区,打印机再自行逐步打印,这时我们的CPU可以处理别的事情。可以看出,缓冲区就是一块内存区,它用在输入输出设备和CPU之间,用来缓存数据。它使得低速的输入输出设备和高速的CPU能够协调工作,避免低速的输入输出设备占用CPU,解放出CPU,使其能够高效率工作。

3. 缓冲类型

分类:
1.用户级缓冲区(库缓冲区)
2.内核级缓冲区

C标准库中有文件缓冲区(在FILE中),struct file中也有文件内核缓冲区;
当用户满足以下三个
1.强制刷新;
2.刷新条件满足;
3.进程退出;
任意一条时,将C标准库文件缓冲区数据,根据文件描述符刷新(采取fd+系统调用,比如write)到操作系统文件内核缓冲区里,即拷贝交给操作系统.

注:

  • 1.操作系统会参考下面这几种刷新方式,怎么刷新到外设我们不关心。我们认为只要把数据交给OS,就相当于交给了硬件!
  • 2.数据交给系统,交给硬件–本质全是拷贝!
    计算机数据流动的本质:一切皆拷贝!!

标准I/O提供了3种类型的缓冲区。

  • 全缓冲区:这种缓冲方式要求填满整个缓冲区后才进行I/O系统调用操作。对于磁盘文件的操作通常使用全缓冲的方式访问。
  • 行缓冲区:在行缓冲情况下,当在输入和输出中遇到换行符时,标准I/O库函数将会执行系统调用操作。当所操作的流涉及一个终端时(例如标准输入和标准输出),使用行缓冲方式。因为标准I/O库每行的缓冲区长度是固定的,所以只要填满了缓冲区,即使还没有遇到换行符,也会执行I/O系统调用操作,默认行缓冲区的大小为1024。大多显示器用,
  • 无缓冲区:无缓冲区是指标准I/O库不对字符进行缓存,直接调用系统调用。标准出错流stderr通常是不带缓冲区的,这使得出错信息能够尽快地显示出来。立即刷新–无缓冲–写透模式WT

除了上述列举的默认刷新方式,下列特殊情况也会引发缓冲区的刷新:

  1. 缓冲区满时;
  2. 执行flush语句;

image-20250909081741707

  • 强制从文件内核缓冲区刷新到外设方法:
  1. fsync(同步系统调用)

fsync的作用是把内核缓冲区的数据强制刷新到外设上.(称为持久化或落盘)

基本用法

#include <unistd.h>int fsync(int fd);
  • 参数
    • fd:文件描述符。
  • 返回值
    • 成功返回 0;
    • 失败返回 -1,并设置 errno。

  1. sync(命令)

sync命令的作用是强制把内核页缓存中所有挂起的数据写入磁盘。

用法

sync

直接运行,不带参数; 效果是:把所有挂起的文件系统缓冲区写盘。


刷新示例如下:

image-20250909082350752

我们本来想使用重定向思维,让本应该打印在显示器上的内容写到“log.txt”文件中,但我们发现,程序运行结束后,文件中并没有被写入内容:

image-20250909082456589

这是由于我们将1号描述符重定向到磁盘文件后,缓冲区的刷新方式成为了全缓冲。而我们写入的内容并没有填满整个缓冲区,导致并不会将缓冲区的内容刷新到磁盘文件中。怎么办呢?可以使用fflush强制刷新下缓冲区。注:重定向会改变刷新方式!!!

image-20250909082524620

还有一种解决方法,刚好可以验证一下stderr是不带缓冲区的,代码如下:

image-20250909082746732

这种方式便可以将2号文件描述符重定向至文件,由于stderr没有缓冲区,“hello world”不用fflash就可以写入文件:

image-20250909082843390

4. FILE

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

来段代码在研究一下:

image-20250909083437514

运行出结果:

image-20250909083451567

但如果对进程实现输出重定向呢?./hello > file,我们发现结果变成了:

image-20250909083527619

**我们发现printf 和fwrite(库函数)都输出了2次,而 write 只输出了一次(系统调用)。**为什么呢?肯定和fork有关!

  • 一般C库函数写入文件时是全缓冲的,而写入显示器是行缓冲。
  • printf fwrite 库函数+会自带缓冲区(进度条例子就可以说明),当发生重定向到普通文件时,数据的缓冲方式由行缓冲变成了全缓冲。
  • 而我们放在缓冲区中的数据,就不会被立即刷新,甚至fork之后
  • 但是进程退出之后,会统一刷新,写入文件当中。
  • 但是fork的时候,父子数据会发生写时拷贝,所以当你父进程准备刷新的时候,子进程也就有了同样的一份数据,随即产生两份数据。
  • write 没有变化,说明没有所谓的缓冲。

综上: printf fwrite 库函数会自带缓冲区,而write系统调用没有带缓冲区。另外,我们这里所说的缓冲区,都是用户级缓冲区。其实为了提升整机性能,OS也会提供相关内核级缓冲区,不过不再我们讨论范围之内。

那这个缓冲区谁提供呢? printf fwrite 是库函数, write 是系统调用,库函数在系统调用的“上层”, 是对系统调用的“封装”,但是write 没有缓冲区,而 printf fwrite 有,足以说明,该缓冲区是二次加上的,又因为是C,所以由C标准库提供。

如果有兴趣,可以看看FILE结构体:(glibc 的 FILE 结构体实现)用户层缓冲区
typedef struct _IO_FILE FILE;/usr/include/stdio.h

image-20250909084419772

5. 简单设计一下libc库

mystdio.h

#pragma once#include <stdio.h>#define MAX 1024
#define NONE_FLUSH (1<<0)
#define LINE_FLUSH (1<<1)
#define FULL_FLUSH (1<<2)typedef struct IO_FILE
{int fileno;int flag;char outbuffer[MAX];int bufferlen;int flush_method;
}MyFile;MyFile* MyFopen(const char *path, const char *mode);void MyFclose(MyFile *);int MyFwrite(MyFile *, void *str, int len);void MyFFlush(MyFile *);

mystdio.c

#include "mystdio.h"
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>//static修饰的是函数,让他只在本文件中使用
static MyFile* BuyFile(int fd, int flag)
{MyFile* f = (MyFile*)malloc(sizeof(MyFile));if(f==NULL)return NULL;f->bufferlen = 0;f->fileno = fd;f->flag = flag;f->flush_method = LINE_FLUSH;memset(f->outbuffer,0,sizeof(f->outbuffer));return f;
}MyFile* MyFopen(const char *path, const char *mode)
{int flag = -1;int fd = 0;if(strcmp(mode,"w") == 0){flag = O_CREAT | O_WRONLY | O_TRUNC;fd = open(path, flag, 0666);}if(strcmp(mode,"a") == 0){flag = O_CREAT | O_WRONLY | O_APPEND;fd = open(path, flag, 0666);}if(strcmp(mode,"r") == 0){flag = O_RDONLY;fd = open(path, flag);}else{//TODO        }if(fd < 0)return NULL;return BuyFile(fd,flag);
}void MyFclose(MyFile *file)
{if(file->fileno < 0)return;MyFFlush(file);close(file->fileno);free(file);
}int MyFwrite(MyFile *file, void *str, int len)
{//1. 拷贝memcpy(file->outbuffer+file->bufferlen,str,len);file->bufferlen+=len;//2. 尝试判断是否满足刷新条件if((file->flush_method & LINE_FLUSH)&&(file->outbuffer[file->bufferlen-1]=='\n'))MyFFlush(file);return 0;
}void MyFFlush(MyFile *file)
{if(file->bufferlen <= 0)return;// 把数据从用户拷贝到内核文件缓冲区中int n = write(file->fileno, file->outbuffer, file->bufferlen);(void)n;//内核文件缓冲区强制刷新到外设fsync(file->fileno);file->bufferlen = 0;
}

usercode.c

#include "mystdio.h"
#include <string.h>
#include <unistd.h>int main()
{MyFile* filep = MyFopen("./log.txt","a");if(!filep){printf("fopen error\n");return 1;}int cnt = 10;while(cnt--){char* msg = (char*)"hello myfile!";MyFwrite(filep,msg,strlen(msg));MyFFlush(filep);printf("buffer:%s\n",filep->outbuffer);sleep(1);}MyFclose(filep);return 0;
}

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

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

相关文章

Coze源码分析-资源库-删除插件-前端源码-核心逻辑

删除插件逻辑 1. 删除操作入口组件 删除插件操作主要通过 usePluginConfig hook 中的 renderActions 方法实现&#xff0c;该方法返回 TableAction 组件来处理表格行的操作。 文件位置&#xff1a;frontend/packages/studio/workspace/entry-base/src/pages/library/hooks/u…

第一代:嵌入式本地状态(Flink 1.x)

最初的架构将状态以 JVM Heap 对象的形式存储在 TaskManager 的内存中。对于小规模数据集&#xff0c;这种方式效果良好&#xff0c;但随着状态大小的增长超出内存&#xff0c;将所有状态保存在内存中变得成本高昂且不稳定。 为了解决状态规模增长的问题&#xff0c;引入了一种…

跨境金融数据对接实践:印度NSE/BSE股票行情API集成指南

跨境金融数据对接实践&#xff1a;印度NSE/BSE股票行情API集成指南 关键词&#xff1a;印度股票数据对接 NSE实时行情 BSE证券接口 金融API开发 Python请求示例一、印度股市数据源技术解析&#xff08;核心价值&#xff09; 印度两大交易所数据获取难点&#xff1a; 时区差异&a…

AFSim2.9.0学习笔记 —— 1、AFSim及完整工具介绍(文末附:完整afsim2.9.0源码、编译好的完整工具包、中文教材等)

&#x1f514; AFSim2.9.0 相关技术、疑难杂症文章合集&#xff08;掌握后可自封大侠 ⓿_⓿&#xff09;&#xff08;记得收藏&#xff0c;持续更新中…&#xff09; AFSim介绍 AFSim&#xff08;Advanced Framework for Simulation Integration & Modeling【高级仿真集成与…

ArcGIS学习-18 实战-降雨量空间分布插值分析

设置环境加载要素投影查看要素&#xff0c;发现均不是投影数据&#xff0c;但都是地理坐标都是WGS1984使用工具进行批量投影然后新建空地图&#xff0c;重新加载确认图层的投影与栅格数据一致插值样条法得到反距离权重法插值得到克里金法插值得到

HarmonyOS应用开发:深入理解声明式UI与弹窗交互的最佳实践

HarmonyOS应用开发&#xff1a;深入理解声明式UI与弹窗交互的最佳实践 引言 随着HarmonyOS 4.0的发布及后续版本的演进&#xff0c;华为的分布式操作系统已经进入了全新的发展阶段。基于API 12及以上的开发环境为开发者提供了更强大、更高效的开发工具和框架。在HarmonyOS应用…

探索Java并发编程--从基础到高级实践技巧

Thread&#xff08;线程&#xff09;线程 程序执行的最小单位&#xff08;一个进程至少有一个线程&#xff09;。线程内有自己的执行栈、程序计数器&#xff08;PC&#xff09;&#xff0c;但与同进程内其他线程共享堆内存与进程资源 在java中&#xff0c;线程由java.lang.Thr…

Go语言实战案例-开发一个Markdown转HTML工具

这个小工具可以把 .md 文件转换为 .html 文件&#xff0c;非常适合写笔记、博客或者快速预览 Markdown 内容。&#x1f4cc; 案例目标• 读取一个 Markdown 文件• 使用开源库将 Markdown 转换为 HTML• 将 HTML 输出到新文件中&#x1f4e6; 所需库我们用 goldmark 这个 Markd…

基于51单片机的太阳能锂电池充电路灯

基于51单片机的太阳能锂电池充电路灯系统设计 1 系统功能介绍 本设计以 STC89C52单片机 为核心&#xff0c;构建了一个能够利用太阳能为锂电池充电并智能控制LED路灯的系统。系统结合了 光照检测电路、LED灯电路、按键检测电路、太阳能充电电路 等模块&#xff0c;实现了节能、…

PAT 1178 File Path

这一题的大意是给出了一个windows的文件夹目录&#xff0c;让我们按照所属的目录关系&#xff0c;来找相应的目录是否存在&#xff0c;如果存在&#xff0c;就输出找到该文件的路径&#xff0c;如果不存在输出error 我的思路是用合适的树形结构保存下来目录的所属关系&#xff…

云原生部署_k8s入门

K8S官网文档&#xff1a;&#xfeff;https://kubernetes.io/zh/docs/home/Kubernetes是什么Kubernetes 是用于自动部署、扩缩和管理容器化应用程序的开源系统。 Kubernetes 源自 &#xff0c;Google 15 年生产环境的运维经验同时凝聚了社区的最佳创意和实践。简称K8s.Kubernet…

实战项目-----Python+OpenCV 实现对视频的椒盐噪声注入与实时平滑还原”

实战项目实现以下功能&#xff1a;功能 1&#xff1a;为视频每一帧添加椒盐噪声作用&#xff1a;模拟真实环境中图像传输或采集时可能出现的噪声。实现方式&#xff1a;读取视频的每一帧。随机选择 10000 个像素点&#xff0c;将其设置为黑色&#xff08;0&#xff09;或白色&a…

Day42 PHP(mysql注入、跨库读取)

一、sql注入基本原理&#xff1a;没有对用户输入的数据进行限制&#xff0c;导致数据库语句可以做什么&#xff0c;用户就可以做什么。取决于不同数据库的不同查询语言&#xff0c;所以为什么有mysql注入/orcale注入等等。步骤&#xff1a;【access】表名&#xff08;字典爆破来…

机器人控制器开发(部署——软件打包备份更新)

文章总览 为什么做备份更新 为机器人控制器设计一套打包备份更新机制&#xff0c;为控制器的批量生产和产品与项目落地做准备。 当某个模块出现bug需要升级时&#xff0c;用户可以快速获取正确的bak包并导入到控制器中重启生效。 如果没有做好软件的备份更新机制&#xff0c…

LaTeX TeX Live 安装与 CTAN 国内镜像配置(Windows / macOS / Linux 全流程)

这是一份面向国内环境的 LaTeX 从零到可编译 指南&#xff1a;覆盖 TeX Live / MacTeX 安装、PATH 配置、CTAN 国内镜像&#xff08;清华/北外/上交/中科大等&#xff09;一键切换与回滚、常见坑位&#xff08;权限、镜像路径、版本切换&#xff09;、以及 XeLaTeX/latexmk 的实…

WhoisXML API再次荣登2025年美国Inc. 5000快速成长企业榜单

WhoisXML API非常自豪地宣布&#xff0c;我们再次荣登美国权威榜单——2025年Inc.5000全美成长最快的私营企业之一。今年&#xff0c;公司在地区排名中位列第119名&#xff0c;在全美总体排名中位列第4,271名。Inc. 5000榜单要求参评企业必须保持独立运营&#xff0c;并在2021至…

Elasticsearch面试精讲 Day 9:复合查询与过滤器优化

【Elasticsearch面试精讲 Day 9】复合查询与过滤器优化 在Elasticsearch的搜索体系中&#xff0c;复合查询&#xff08;Compound Queries&#xff09;与过滤器&#xff08;Filters&#xff09;优化是构建高效、精准搜索逻辑的核心能力。作为“Elasticsearch面试精讲”系列的第…

Android使用ReactiveNetwork监听网络连通性

引入库 implementation com.github.pwittchen:reactivenetwork-rx2:3.0.8监听网络连接变更ReactiveNetwork.observeNetworkConnectivity(context).subscribeOn(Schedulers.io())// ... // anything else what you can do with RxJava.observeOn(Schedulers.computation()).subs…

基于阿里云部署 RustDesk 自托管服务器

基于阿里云部署 RustDesk 自托管服务器一、背景与需求场景二、什么是 RustDesk&#xff1f;为什么选择自托管&#xff1f;2.1 RustDesk 是什么&#xff1f;2.2 为什么选择自托管&#xff1f;三、环境准备与架构说明四、操作步骤4.1 在阿里云上安装 RustDesk 服务端4.1.1 下载并…

细说分布式ID

针对高并发写&#xff0c;分布式ID是其业务基础&#xff0c;本文从一个面试题细细展开。面试官&#xff1a;1.对于Mysql的InnoDB引擎下&#xff0c;自增ID和UUID作为主键各自有什么优劣&#xff0c;对于一张表的主键你建议使用哪种ID&#xff1f;2.除了UUID是否还了解其他类型的…