基础IO
一.理解"文件"
- 文件分类
1.内存级(被打开)文件
2.磁盘级文件
1. 狭义理解
- 文件在磁盘里
- 磁盘是永久性存储介质,因此文件在磁盘上的存储是永久性的
- 磁盘是外设 (即是输出设备也是输入设备)
- 磁盘上的文件本质是对文件的所有操作,都是对外设的输入和输出简称IO
2. 广义理解
- Linux 下一切皆文件(键盘、显示器、网卡、磁盘…… 这些都是抽象化的过程)(后面会讲如何去理解)
3. 文件操作的归类认知
- 对于 0KB 的空文件是占用磁盘空间的
- 文件是文件属性(元数据)和文件内容的集合(文件 = 属性(元数据)+ 内容)
- 所有的文件操作本质是文件内容操作和文件属性操作
4. 系统角度
-
对文件的操作本质是进程对文件的操作
访问文件,需要先打开文件!
谁打开文件?进程打开的文件!
对文件的操作,本质是进程对文件的操作! -
磁盘的管理者是操作系统
-
文件的读写本质不是通过 C 语言 / C++ 的库函数来操作的(这些库函数只是为用户提供方便),而是通过文件相关的系统调用接口来实现的(fopen,fclose…等库封装了底层os的文件系统调用!)
二.回顾C文件接口
(见C语言进阶文件博客)
C语言基础:文件操作与数据持久化,-CSDN博客
1. hello.c打开文件
打开的myfile文件在哪个路径下?
- 在程序的当前路径下,那系统怎么知道程序的当前路径在哪里呢?
可以使用 ls /proc/[进程id] -l
命令查看当前正在运行进程的信息:
其中:
- cwd:指向当前进程运行目录的一个符号链接。
- exe:指向启动当前进程的可执行文件(完整路径)的符号链接。
打开文件,本质是进程打开,所以,进程知道自己在哪里,即便文件不带路径,进程也知道。由此OS就能知道要创建的文件放在哪里。
2. hello.c写文件
3. hello.c读文件
稍作修改,实现简单cat命令:
4. 输出信息到显示器,你有哪些方法
5. stdin & stdout & stderr
- C默认会打开三个输入输出流,分别是stdin, stdout, stderr
- 仔细观察发现,这三个流的类型都是FILE, fopen返回值类型,文件指针*
6. 打开文件的方式
如上,是我们之前学的文件相关操作。还有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 写文件
2. hello.c读文件
4. 接口介绍
open
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
都属于系统提供的接口,称之为系统调用接口 - 回忆一下我们讲操作系统概念时,画的一张图
系统调用接口和库函数的关系,一目了然。
所以,可以认为, f#
系列的函数,都是对系统调用的封装,方便二次开发。
6. 文件描述符fd
-
通过对open函数的学习,我们知道了文件描述符就是一个小整数
(1).0 & 1 & 2
-
Linux进程默认情况下会有3个缺省打开的文件描述符,分别是标准输入0, 标准输出1, 标准错误2.
-
0,1,2对应的物理设备一般是:键盘,显示器,显示器
所以输入输出还可以采用如下方式:
而现在知道,文件描述符就是从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
(2).文件描述符的分配规则
直接看代码:
输出发现是fd: 3
关闭0或者2,再看
发现是结果是: fd: 0
或者fd 2
,可见,文件描述符的分配规则:在files_struct数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符。
(3).重定向
- 重定向=打开文件的方式+dup2
那如果关闭1呢?看代码:
此时,我们发现,本来应该输出到显示器上的内容,输出到了文件myfile
当中,其中,fd=1。这种现象叫做输出重定向。常见的重定向有: >
, >>
, <
那重定向的本质是什么呢?重定向的本质是更改文件描述符表的指针指向,数组下标不变
重定向完整写法是:
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
-
可同时重定向多个
./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) 一起重定向到同一个文件。
-
为什么存在一个标准错误呢?
答:可以通过重定向能力,把常规消息和错误消息(printf/perror和cout/cerr)进行分离!
(4).使用 dup2 系统调用
- 注:进程替换不影响重定向的结果
函数原型如下:进行重定向的系统调用
示例代码
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
下,以下展示了该结构部分我们关系的内容:
值得关注的是struct file
中的f_op
指针指向了一个file_operations
结构体,这个结构体中的成员除了struct module* owner 其余都是函数指针。该结构和struct file
都在fs.h下。
file_operation
就是把系统调用和驱动程序关联起来的关键数据结构,这个结构的每一个成员都对应着一个系统调用。读取file_operation
中相应的函数指针,接着把控制权转交给函数,从而完成了Linux设备驱动程序的工作。
介绍完相关代码,一张图总结:
上图中的外设,每个设备都可以有自己的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
除了上述列举的默认刷新方式,下列特殊情况也会引发缓冲区的刷新:
- 缓冲区满时;
- 执行flush语句;
- 强制从文件内核缓冲区刷新到外设方法:
- fsync(同步系统调用)
fsync的作用是把内核缓冲区的数据强制刷新到外设上.(称为持久化或落盘)
基本用法
#include <unistd.h>int fsync(int fd);
- 参数:
- fd:文件描述符。
- 返回值
- 成功返回 0;
- 失败返回 -1,并设置 errno。
- sync(命令)
sync命令的作用是强制把内核页缓存中所有挂起的数据写入磁盘。
用法
sync
直接运行,不带参数; 效果是:把所有挂起的文件系统缓冲区写盘。
刷新示例如下:
我们本来想使用重定向思维,让本应该打印在显示器上的内容写到“log.txt”文件中,但我们发现,程序运行结束后,文件中并没有被写入内容:
这是由于我们将1号描述符重定向到磁盘文件后,缓冲区的刷新方式成为了全缓冲。而我们写入的内容并没有填满整个缓冲区,导致并不会将缓冲区的内容刷新到磁盘文件中。怎么办呢?可以使用fflush强制刷新下缓冲区。注:重定向会改变刷新方式!!!
还有一种解决方法,刚好可以验证一下stderr是不带缓冲区的,代码如下:
这种方式便可以将2号文件描述符重定向至文件,由于stderr没有缓冲区,“hello world”不用fflash就可以写入文件:
4. FILE
- 因为IO相关函数与系统调用接口对应,并且库函数封装系统调用,所以本质上,访问文件都是通过fd访问的。
- 所以C库当中的FILE结构体内部,必定封装了fd。
来段代码在研究一下:
运行出结果:
但如果对进程实现输出重定向呢?./hello > file
,我们发现结果变成了:
**我们发现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
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;
}