1. 引入
#include <stdio.h>
#include <string.h>
int main()
{
const char *msg0="hello printf\n";
const char *msg1="hello fwrite\n";
const char *msg2="hello write\n";
printf("%s", msg0);
fwrite(msg1, strlen(msg0), 1, stdout);
write(1, msg2, strlen(msg2));
fork();
return 0;
}
输出结果
打印到了屏幕上:
hello printf
hello fwrite
hello write
注意:
- 当我们fork()的时候,上面的函数已经被执行完了。但并不代表着数据被刷新出来了。
将文件输出重定向到 log.txt文件
bash: ./myfile.c>log,txt
bash: cat log.txt
输出结果
hello write
hello printf
hello fwrite
hello printf
hello fwrite
同样的一个程序,向显示器打印时输出3行文本,向普通文件(磁盘)上打印时,变成了5行,其中C语言IO接口打印两次,系统IO接口打印一次。
分析
一定与fork()有关。
缓冲区一定不是操作系统提供维护的,如果是,上面的hello write也应该被刷新两次。
2. 缓冲区
什么是缓冲区?就是一段内存空间。
为什么要有缓冲区?避免频繁调用系统IO操作(更少次的外设的访问,和外设预备IO的过程是最耗费时间的),提高整机效率,提高用户的相应速度。
2.1 缓冲区的刷新策略
1、立即刷新
2、行刷新(行缓冲) 即\n
3、满刷新(全缓冲)
4、特殊情况:用户强制刷新(fflush)。进程退出。
一般而言,行缓冲的设备文件——显示器。全缓冲的设备文件——磁盘文件。
调用close(fd)就会直接关闭文件,而不会先刷新出缓冲区的数据。
2.2 原因
缓冲区绝对不是操作系统维护的,如果是,write的部分也该写入两次。
程序进行了重定向,要向磁盘文件打印。刷新策略由行刷新更新为满刷新,\n失去作用。
我们是在最后调用的fork(),此时函数绝对已经执行完成了,只是结果没有刷新到文件上。刷新是写入吗?是的。
在fork()之后,子进程中也保有父进程执行的数据,因为在进程结束时需要将缓冲区内容刷新到相应文件,此时发生了写时拷贝。父子进程都执行刷新操作,所以写入了两次。
而向显示器输出时,为行刷新,轮到fork()的时候,缓冲区内没有数据内容,也就不需要写入。所以C语言接口仅写入一次。
write本身就是系统调用接口,没有(用户级)缓冲区(但是有内核级缓冲区,进入该缓冲区,数据已经成为了内核数据,与进程无关),直接进行了刷新。
2.3 缓冲区的维护
上述操作不变,在代码行中,在fork()之前添加fflush(stdout);
打印结果又和输出内容到显示屏一样了。可这里的stdout是FILE*类型,虽然封装了fd,但是他怎么可以刷新缓冲区?
结论:我们上述所提及到的缓冲区是C标准库维护的!
struct FILE结构体不仅封装了文件描述符,同样包含了fd对应的语言层的缓冲区结构。
2.4 总结
- 一般C库函数写入文件时是全缓冲的,而写入显示器是行缓冲。
- printf fwrite 库函数会自带缓冲区,当发生重定向到普通文件时,数据的缓冲方式由行缓冲变成了全缓冲。
- 而我们放在缓冲区中的数据,没有被立即刷新。
- 进程退出之后,缓冲区会统一刷新,写入文件。
- 刷新也是写入的过程,所以fork后,子进程写入发生写时拷贝,子进程有了同样的 一份数据,随即产生两份数据。
- write 没有变化,说明没有所谓的缓冲。
3. 用户级缓冲区的实现
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <assert.h>
#define NUM 1024
struct Myfile
{
int fd;
char buffer[1024];
int end;//当前缓冲区的结尾
};
typedef struct Myfile Myfile;
Myfile* fopen_(const char* pathname, const char* mode)
{
//保证 pathname 和mode 都不为空
assert(pathname);
assert(mode);
Myfile* fp = NULL;//初始化fp
if (strcmp(mode, "r") == 0)
{
}
else if (strcmp(mode, "r+") == 0)
{
}
else if (strcmp(mode, "w") == 0)
{
//先使用系统调用接口打开文件
int fd = open(pathname, O_WRONLY | O_TRUNC | O_CREAT,0666);
if (fd > 0)
{
//申请空间
fp = (Myfile*)malloc(sizeof(Myfile));
//初始化空间为0
memset(fp, 0, sizeof(Myfile));
fp->fd = fd;
}
//小于0 打开失败不进行任何操作 最后返回空指针
}
else if (strcmp(mode, "w+") == 0)
{
}
else if (strcmp(mode, "a") == 0)
{
}
else if (strcmp(mode, "a+") == 0)
{
}
else
{
//未知指令 什么都不做
}
return fp;
}
void fputs_(const char* message, Myfile* fp)
{
assert(message);
assert(fp);
//从buffer的结束位置拷贝message,buffer内容逐次增长
strcpy(fp->buffer + fp->end, message);
fp->end += strlen(message);
//打印给自己看的
//不用写 显示每次输出的值是否正确 且连续
printf("%s\n", fp->buffer);
//判断输入有没有/n
//这里部分暂时无法实现 只能体现刷新策略
//刷新策略是用户通过执行C标准库中的代码逻辑,来完成刷新动作的
if (fp->fd == 0)
{
//向 标准输入 的buffer输入
//这里不写打开显示器的相关代码
}
else if (fp->fd == 1)
{
//向 标准输出 的buffrt输入
if (fp->buffer[fp->end - 1] == '\n')// 123412/n/0
{
//有换行符号
write(fp->fd, fp->buffer, fp->end);
fp->end = 0;
}
}
else if (fp->fd == 2)
{
//向 标准错误 的buffer输入
}
else
{
//其他文件
}
}
void fflush_(Myfile* fp)
{
assert(fp);
if (fp->end != 0)
{
//有数据才要刷新
write(fp->fd, fp->buffer, fp->end);
syncfs(fp->fd);//将数据写入磁盘
fp->end = 0;//刷新好了 下次从头写入buffer缓冲区
}
}
void fclose_(Myfile* fp)
{
assert(fp);
//文件关闭前 先刷新缓冲区
fflush_(fp);
close(fp->fd);
free(fp);
}
int main()
{
Myfile* fp = fopen_("./log.txt" ,"w");
if (fp == NULL)
{
printf("open file error");
return 1;
}
fputs_("1:hello world ", fp);
fputs_("2:hello world ", fp);
fputs_("3:hello world ", fp);
//这里会向屏幕打印 因为fputs_加了printf
//只是为了给自己显示的看看
//1:hello world
//1:hello world 2:hello world
//1:hello world 2:hello world 3:hello world
#if 0
bash:cat log.txt
//写入了 1 : hello world 2 : hello world 3 : hello world
#endif
return 0;
}