目录
1.用户级别的缓冲区
int main()
{
// C语言
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;
}
1.我们将该代码直接编译,可执行文件打印的输出都没什么问题。不过当我们将其内容重定向到新文件时,我们会发现,C接口的都打印了两次,而操作系统接口没有。
2.不过,当我们注释掉fork的文件,我们又会发现,之前重定向打印两次的数据,又变回了一次,所以我们能清楚,出现的问题在fork函数上。
3.打印出现问题,其实我们还可以理解以下一些事情。父进程是打印出去了,但是能出现fork还有数据,说明父进程的东西没有干净,被子进程拷贝了,以至于重定向时出现了问题。
那么我们能指向真正的问题:缓冲区的相关原理
缓冲区介绍
1.缓冲区本质就是一块内存,用来存储缓冲数据
2.内存与磁盘之间的读写时,内存不会无时无刻对磁盘进行操作的,因为那样效率太低了
3.缓冲区的意义就在于为了节省内存数据IO的时间
4.用户没有将内存的数据拷贝到缓冲区中,不代表不拷贝。我们调用的C语言IO流语法,其实本质都是拷贝内容到缓冲区中。
5.缓冲区的执行其实就是给数据刷新到磁盘。
6.刷新的策略有很多种:立即刷新、行刷新、写满刷新
7.对比起来,写满在输出的效率是最高的。对于磁盘文件而言,写满刷新策略最优;对于显示器,出于习惯,我们对行的阅读比较看重,因此显示器一般采用行刷新;立即刷新基本是手动调用的,用户自己决定。
8.其实有两种特殊情况:用户强制刷新和进程退出都会强制刷新缓冲区
缓冲区设计原理相关知识
1.对于上面的代码问题,其实我们能知道缓冲区不会在内核中;因为write的代码没有在文件里被打印两次
2.而C语言打印两次,其实也就意味着这个缓冲区是用户级语言给我们提供的缓冲区
3.对于文件操作,stdin,stdout,stderr都需要FILE*这个结构体,其实我们说的缓冲区就在这个结构体中
解决遗留fork的问题
1.针对为进行重定向的程序,进程打印四条语句到显示器上。其原因是因为使用C的stdout是,系统默认其刷新策略为行刷新,在fork之前,C语言的语句都已经刷新到显示器上了,FILE内部不存在数据。
2.针对重定向过的程序,该进程写入文件不再是显示器,而是文件,文件的缓冲策略是全缓冲,之前C的三条语句即使存在\n,也不能将缓冲区刷新掉,数据没有被刷新。fork时,子进程复制父进程的缓冲区内容,那么进程退出将缓冲区的数据刷新。父进程有一个系统调用接口,父与子进程的缓冲区都有C的三条语句。
实现缓冲区
main
#include "mystdio.h" #include <stdio.h> // int main() // { // FILE_ *fp = fopen_("./log.txt", "w"); // if (fp == NULL) // { // return 1; // } // const char *msg = "hello world\n"; // fwrite_((void*)msg, strlen(msg), fp); // fclose_(fp); // return 0; // } // 行刷新 int main() { FILE_ *fp = fopen_("./log.txt", "w"); if (fp == NULL) { return 1; } const char *msg = "hello world "; int num = 10; while (1) { num--; fwrite_((void *)msg, strlen(msg), fp); sleep(1); if (num == 0) break; if (num == 5) fflush_(fp); //手动刷新 printf("num:%d\n", num); } fclose_(fp); return 0; } // 结束刷新
mystdio.h
#pragma once #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <string.h> #include <errno.h> #include <stdlib.h> #include <assert.h> #define SIZE 1024 #define SYNC_NOW 1 #define SYNC_LINE 2 #define SYNC_FULL 4 typedef struct _FILE { int flags; // 刷新方式 int fileno; int cap; // 容量 int size; // 使用量 char buffer[SIZE]; } FILE_; FILE_ *fopen_(const char *path_name, const char *mode); void fwrite_(const void *ptr, int num, FILE_ *fp); void fflush_(FILE_ *fp); void fclose_(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_TRUNC | O_CREAT); } else if (strcmp(mode, "a") == 0) { flags |= (O_WRONLY | O_APPEND | O_CREAT); } else { // TODO; } int fd = 0; if (flags & O_RDONLY) fd = open(path_name, flags); else fd = open(path_name, flags, defaultMode); if (fd < 0) { const char *err = strerror(errno); write(2, err, strerror(err)); return NULL; } FILE_ *fp = (FILE_ *)malloc(sizeof(FILE_)); assert(fp); fp->flags = SYNC_LINE; // 默认行刷新 fp->fileno = fd; fp->cap = SIZE; fp->size = 0; memset(fp->buffer, 0, SIZE); fp->buffer[0] = 0; return fp; } void fwrite_(const void *ptr, int num, FILE_ *fp) { // 1.写入缓冲区 memcpy(fp->buffer + fp->size, ptr, num); fp->size += num; // 2.是否刷新 if (fp->flags & SYNC_NOW) { write(fp->fileno, fp->buffer, fp->size); fp->size = 0; // 清空缓冲区,惰性释放 } else if (fp->flags & SYNC_FULL) { if (fp->size == fp->cap) { write(fp->fileno, fp->buffer, fp->size); fp->size = 0; } } else if (fp->flags & SYNC_LINE) { if (fp->buffer[fp->size - 1] == '\n') { write(fp->fileno, fp->buffer, fp->size); fp->size = 0; } } else { } } void fflush_(FILE_ *fp) { if (fp->size > 0) write(fp->fileno, fp->buffer, fp->size); fp->size = 0; } void fclose_(FILE_ *fp) { fflush_(fp); printf("\n"); close(fp->fileno); }
2.内核缓冲区
我们知道用户级缓冲区的原理和实现后,还需要知道内核的缓冲区的相关内容。
1.在fflush之后,我们不能简单的以为数据缓冲区直接被拷贝进了磁盘
2.在内核中,文件结构体和硬件之间还有一个内核缓冲区,整个缓冲区是由操作系统决定什么时候刷新,而这些刷新的策略要比用户级缓冲区更加复杂
3.用户跟内核缓冲区毫无关系
4.如果只是操作系统自己决定所谓的内核缓冲区,其实依然不安全,因为当操作系统的内核缓冲区未清空过程,出现了异常,使得操作系统整体崩溃无法运作,就会出现数据丢失,因为数据还没有及时的放入硬件中。
5.那么人为的操作其实也还是有必要的所以操作系统提供一个接口,让用户使用,该接口强制操作系统同步数据到硬件中
6.其实我们清楚,所谓用户的数据写入本质都是拷贝,数据拷贝到缓冲区,缓冲区拷贝到内核缓冲区,内核拷贝到硬件