需要云服务器等云产品来学习Linux的同学可以移步/–>腾讯云<–/官网,轻量型云服务器低至112元/年,优惠多多。(联系我有折扣哦)
#include
#include
#include
int main()
{
printf("hello printf\n");
fprintf(stdout, "hello fprintf\n");
fputs("hello fputs\n", stdout);
const char* msg = "hello write\n";
write(1, msg, strlen(msg));
fork();
return 0;
}
这个运行结果看起来是没有任何问题的。那么现在我们把运行结果进行一次重定向
明显看到打印出来的数据变多了,这是什么原因呢?
这里我们把fork给去掉试试?
所以可以推测出来一定是fork
的问题。
仔细观察可以发现,出现重复打印的是C语言的接口,所以应该是C语言的问题
这里面实际上是一个C语言缓冲区的问题,接下来我们来了解一下这个C语言缓冲区的相关内容
1. 缓冲区是什么?
缓冲区本质上就是一段内存 在内存中提前预留一段空间,用来存储即将输入或者输出的内容,这一段预留的空间就是缓冲区。
2. 为什么要有缓冲区?
我们知道内存相对于外设来说是一个高速设备,如果每次IO都要从外设读取或者直接写到外设,这种等待的时间太长了(相对来说),所以就干脆预留一段空间,将所有需要写入到外设的数据拷贝到内存的某个空间(缓冲区),然后由操作系统决定什么时候真正写入到外设。这种方式能够很大的提高效率。这个拷贝数据的过程我们并不需要手动实现,而是通过fwrite来实现,那么实际上这个fwrite的本质就是一个拷贝功能的函数,fread同理
**通过这种策略,数据可以直接拷贝到缓冲区,高速设备不用在等待低速设备,提高计算机的效率。 **
上文中我们说到,缓冲区的数据什么时候真正写到磁盘中是由操作系统决定的,那么操作系统将缓冲区的内容写到磁盘中遵循什么样的策略呢?
缓冲区的刷新策略:
1. 立即刷新(无缓冲)缓冲区中一出现数据就刷新到外设中——这种很少出现
2. 行刷新(行缓冲)——数据按行刷新,每拷贝一行数据就刷新一次,显示器采用的就是这种刷新策略,因为显示器是给人看了,而按行刷新符合人的阅读习惯,同时刷新效率也不会太低
3. 缓冲区满刷新(全缓冲)——待数据把缓冲区填满后再刷新,这种刷新方式效率最高,一般应用于磁盘文件
两种特殊情况:
- 用户使用 fflush 等函数强制进行缓冲区刷新;
- 一般进程推出后缓冲区会被刷新
把思绪回到文章开头的那个现象中,有了现在的知识储备,我们再来思考一下这个现象:加上fork之后,向显示器上打印的数据有四条,向文件中写入的数据有七条,那么肯定是有三条数据在缓冲区中没有被刷新出来。注意:这三条数据是C语言提供的接口产生的,系统调用的接口并没有出现这种现象,所以这个缓冲区肯定是在语言层的。
实际上,C语言封装了一个FILE结构体,在FILE结构体中,除了我们之前说的fd
之外,还维护了一个缓冲区
我们来找一找FILE结构体的源码
在/usr/include/stdio.h
中
在/usr/include/libio.h
中
所以现在我们再来解释一下这个奇怪的现象:
- 一般C库函数写入文件的时候是全缓冲的,但是写入到显示器是行缓冲
printf fwrite
库函数是自带缓冲区的,当发生了重定向之后,写入显示器的行缓冲就会变成全缓冲- 所以我们放入缓冲区的数据不会被立刻刷新,直到fork之后,这个缓冲区也被拷贝了两份
- 进程退出之后,所有的缓冲区数据会被刷新,写入到文件中,包括子进程的缓冲区,所以就有了两份数据
- 由于这个缓冲区是语言层的,
write
系统调用并没有这个缓冲区,所以write的数据不会被拷贝两份
简单总结来说:重定向导致刷新策略发生了改变(由行缓冲变成了全缓冲)。同时发生了写时拷贝,缓冲区的数据变成了两份一样的,父子进程各自刷新,出现重复写入同一份数据。
注意,这里实现的只是demo级的FILE,当然会有很多bug,理解其中意思就行。
/* myStdio.h */
#pragma once
#include
#include
#include
#include
#include
#include
#include
#define SIZE 1024 // 缓冲区容量
// 缓冲区刷新策略
#define SYNC_NOW 1
#define SYNC_LINE 2
#define SYNC_FULL 4
typedef struct _FILE //定义的FILE结构体
{
int fileno; //文件描述符
int flag; //缓冲区刷新方式
char buffer[SIZE]; //缓冲区
int cap; //缓冲区容量
int size; //缓冲区写入的数据大小
}FILE_;
//这里我们自己实现的函数和结构体在后面加了下划线
FILE_ *fopen_(const char* path_name, const char* mode);
void fclose_(FILE_* fp);
void fflush_(FILE_* fp);
void fwrite_(const void *ptr, int num, FILE_* fp);
/* myStdio.c */
#include "myStdio.h"
FILE_ *fopen_(const char* path_name, const char* mode)
{
int flags = 0; // 打开方式
int defaultMode = 0666; // 默认的创建文件权限
//设置打开的方式
if(strcmp(mode, "r") == 0)
{
flags |= O_RDONLY;
}
else if(strcmp(mode, "w") == 0)
{
flags |= (O_WRONLY | O_CREAT | O_TRUNC);
}
else if(strcmp(mode, "a") == 0)
{
flags |= (O_WRONLY | O_CREAT | O_APPEND);
}
else
{
//do nothing
}
//按照不同方式打开文件并记录文件的fd
int fd = 0;
if(flags & O_RDONLY) // 只读
fd = open(path_name, flags);
else //可能要创建
fd = open(path_name, flags, defaultMode);
if(fd < 0)
{
//文件打开失败
char* str = strerror(errno);
write(2, str, strlen(str));
return NULL;
}
//文件打开成功,构造FILE*对象并填充内容
FILE_* fp = (FILE_*)malloc(sizeof(FILE_));
if(fp == NULL)
{
char* str = strerror(errno);
write(2, str, strlen(str));
return NULL;
}
fp->fileno = fd;
fp->cap = SIZE;
fp->size = 0;
memset(fp->buffer, 0, SIZE);
fp->flag = SYNC_LINE;
return fp;
}
void fflush_(FILE_* fp)
{
//如果缓冲区有内容的话执行写入
if(fp->size > 0)
{
write(fp->fileno, fp->buffer, fp->size);
fp->size = 0;
}
}
void fclose_(FILE_* fp)
{
//刷新缓冲区
fflush_(fp);
//关闭文件
close(fp->fileno);
//释放FILE结构体
free(fp);
fp = NULL;
}
void fwrite_(const void *ptr, int num, FILE_* fp)
{
//写到缓冲区(本质上是拷贝,吧数据从ptr中拷贝到fp->buffer)
memcpy(fp->buffer + fp->size, ptr, num);
fp->size += num;
//执行刷新策略
if(fp->flag & SYNC_NOW) //无缓冲
{
if(fp->size != 0)
{
write(fp->fileno, fp->buffer, fp->size);
fp->size = 0;//清空缓冲区
}
}
else if(fp->flag & SYNC_LINE) // 行缓冲
{
if(fp->buffer[fp->size - 1] == '\n') //这里不考虑 abcd\nefg
{
write(fp->fileno, fp->buffer, fp->size);
fp->size = 0;//清空缓冲区
}
}
else if(fp->flag & SYNC_FULL) // 全缓冲
{
if(fp->size == fp->cap)
{
write(fp->fileno, fp->buffer, fp->size);
fp->size = 0;//清空缓冲区
}
}
else
{
//do nothing
}
}
/* mian.c */
#include "myStdio.h"
int main()
{
FILE_* fp = fopen_("./log.txt", "w");
if(fp == NULL)
{
return 1;
}
const char* msg = "hello world\n";
fwrite_(msg, strlen(msg), fp);
fclose_(fp);
return 0;
}
实际上我们上文中一直在讲的是都是语言层面的缓冲区,但是**操作系统也是有缓冲区的** 。
我们在把一串信息写入到外设(磁盘)中的时候,经历了以下过程
- 通过fputs、fprintf、fwrite等函数把massage写入到**FILE结构体中的缓冲区 **;
- 通过系统调用write将FILE结构体中的缓冲区内容写入到OS内核缓冲区中;
- 由OS决定什么时候将OS内核的数据真正写入到磁盘中;
那么,出现一个问题,如果在第二步结束之后,操作系统宕机了,怎么办?
可能会导致数据丢失。
那么如果在一个对数据丢失0容忍的系统内出现这个问题咋办?
有一个系统调用可以解决这个问题:fsync
这个系统调用的功能就是:强制将内核缓冲区中的数据立刻同步到外设中,而不再采用操作系统的刷新策略
所以,我们上文中实现的fflush_函数事实上得加上这句话
void fflush_(FILE_* fp)
{
//如果缓冲区有内容的话执行写入
if(fp->size > 0)
{
write(fp->fileno, fp->buffer, fp->size);
fp->size = 0;
fsync(fp->fileno);
}
}
本节完…