Linux基础内容(15)—— 缓冲区

 Linux基础内容(14)—— 基础IO与文件描述https://blog.csdn.net/m0_63488627/article/details/129480022?spm=1001.2014.3001.5501

目录

1.用户级别的缓冲区

 缓冲区介绍

缓冲区设计原理相关知识

实现缓冲区

2.内核缓冲区


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;
}

Linux基础内容(15)—— 缓冲区_第1张图片

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*这个结构体,其实我们说的缓冲区就在这个结构体中

Linux基础内容(15)—— 缓冲区_第2张图片

解决遗留fork的问题

1.针对为进行重定向的程序,进程打印四条语句到显示器上。其原因是因为使用C的stdout是,系统默认其刷新策略为行刷新,在fork之前,C语言的语句都已经刷新到显示器上了,FILE内部不存在数据。

2.针对重定向过的程序,该进程写入文件不再是显示器,而是文件,文件的缓冲策略是全缓冲,之前C的三条语句即使存在\n,也不能将缓冲区刷新掉,数据没有被刷新。fork时,子进程复制父进程的缓冲区内容,那么进程退出将缓冲区的数据刷新。父进程有一个系统调用接口,父与子进程的缓冲区都有C的三条语句。

实现缓冲区

main

#include "mystdio.h"
#include 

// 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 
#include 
#include 
#include 
#include 
#include 
#include 

#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.用户跟内核缓冲区毫无关系

Linux基础内容(15)—— 缓冲区_第3张图片

4.如果只是操作系统自己决定所谓的内核缓冲区,其实依然不安全,因为当操作系统的内核缓冲区未清空过程,出现了异常,使得操作系统整体崩溃无法运作,就会出现数据丢失,因为数据还没有及时的放入硬件中。

5.那么人为的操作其实也还是有必要的所以操作系统提供一个接口,让用户使用,该接口强制操作系统同步数据到硬件中

Linux基础内容(15)—— 缓冲区_第4张图片

6.其实我们清楚,所谓用户的数据写入本质都是拷贝,数据拷贝到缓冲区,缓冲区拷贝到内核缓冲区,内核拷贝到硬件

你可能感兴趣的:(linux,服务器,c语言,centos)