【Linux】基础IO--重定向理解Linux下一切皆文件缓冲区

news2024/11/28 8:50:41

文章目录

  • 一、重定向
    • 1.什么是重定向
    • 2.dup2 系统调用
    • 3.理解输入重定向、输出重定向和追加重定向
    • 4.简易shell完整实现
  • 二、理解linux下一切皆文件
  • 三、缓冲区
    • 1.为什么要有缓冲区
    • 2.缓冲区的刷新策略
    • 3.缓冲区的位置
    • 4.实现一个简易的C语言缓冲区
    • 5.内核缓冲区

一、重定向

1.什么是重定向

在上面的代码中,当我们把0号文件描述符关闭之后,会出现下面的状况:

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <fcntl.h>

int main()
{
    //关闭标准输出
    umask(0);
    int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
    if(fd < 0)
    {
        perror("open failed");
        return 1;
    }

    printf("open fd:%d\n",fd);
    fprintf(stdout,"open fd:%d\n",fd);
	fflush(stdout);
    
    close(fd);

    return 0;
}

在这里插入图片描述

我们可以看到,本来应该打印到显示器上的数据却最终输出到了 log.txt 文件中,原因如下:

我们在调用 open 打开 log.txt 之前关闭了标准输出,那么其对应的1号 fd 就闲置了出来,而 fd 的分配规则是从小到大依次寻找未被使用的最小值,所以 log.txt 对应的 fd 就为1;

同时,我们调用的 printf、fprintf 是C语言封装的输出函数,其底层调用系统调用,效果是向标准输出文件中写入数据,而标准输出 fd 默认为 1,但是 printf 与 fprintf 并不知道 1 号 fd 指向了另一个文件 log.txt,所以原本应该写入到显示器上的数据会写入到 log.txt 中。
在这里插入图片描述

注:由于向文件中写数据的缓冲区刷新策略不同,所以这里我们需要在关闭文件之前进行缓冲区刷新,否则 log.txt 中没有数据,具体细节在后文。

像这样,本来应该往一个文件中写入数据,但是却写入到另一个文件中去了,这种特性就叫做重定向;而重定向的本质是上层使用的 fd 不变,在内核中更改 fd 指向的 file 对象,即更改文件描述符表数组中 fd 下标中的内容,让其变为另一个 file 对象的地址。(同一个 fd 指向不同的 file 对象)

2.dup2 系统调用

我们可以使用上面 close(1) 的方式实现重定向,但是我们发现先关闭、再打开这种方式非常麻烦,并且如果 0 和 1 号 fd 都被关闭时,我们还需要先创建一个无用的临时文件占用掉 0 号 fd 之后才能使新文件的 fd 为 1。为了解决这种尴尬的情况,操作系统提供了一个系统调用接口 dup2 来让我们直接进行重定向。

函数功能

dup2() makes newfd be the copy of oldfd, closing newfd first if necessary – dup2 函数会让 newfd 成为 oldfd 的一份拷贝,并且在必要时关闭 newfd。

在这里插入图片描述

函数参数

int dup2(int oldfd, int newfd);
# 头文件:<unistd.h>
# oldfd:旧的文件描述符
# newfd:新的文件描述符
# int:函数返回值,成功返回 newfd,失败返回-1

函数使用

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main()
{
    int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
    if(fd < 0)
    {
        perror("open failed");
        return 1;
    }

    int n = dup2(fd,1);
    if(n < 0)
    {
        perror("dup2");
        return 2;
    }

    printf("open fd:%d\n",fd);
    fprintf(stdout,"open fd:%d\n",fd);

    fflush(stdout);

    close(fd);

    return 0;
}

在这里插入图片描述

printf和fprintf是C库当中的IO函数,一般往 stdout 中输出,但是stdout底层访问文件的时候,找的还是fd:1, 但此时,fd:1下标所表示内容,已经变成了log.txt的地址,不再是显示器文件的地址,所以,输出的任何消息都会往文件中写入,进而完成输出重定向。

注意:dup2 系统调用让 newfd 成为 oldfd 的一份拷贝,其本质是将 oldfd 下标里面存放的 file 对象的地址拷贝到 newfd 下标的空间中,即拷贝的是 fd 对应空间中的数据,而并不是两个 fd 数字之间进行拷贝,这样也没有意义;并且拷贝完成后只会留下 oldfd。

3.理解输入重定向、输出重定向和追加重定向

Linux 中主要有三种主要的重定向 – 输入重定向、输出重定向和追加重定向;在 Linux 命令行中它们分别使用 <、>、>> 表示,演示如下:

在这里插入图片描述

输出重定向

输入重定向我们上面已经实现了,就是通过 dup2(fd, 1) 系统调用将目标文件 fd 中的内容拷贝到 1 号 fd 中,从而将本该写入到显示器中的数据写入到目标文件中。

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

#define FILE_NAME "log.txt"

int main()
{
    int fd = open(FILE_NAME, O_WRONLY | O_CREAT | O_TRUNC, 0666);
    if (fd < 0)
    {
        perror("open failed");
        return 1;
    }

    int n = dup2(fd, 1);
    if (n < 0)
    {
        perror("dup2");
        return 2;
    }

    char buffer[64];
    int cnt = 5;
    while (cnt)
    {
        sprintf(buffer, "%s:%d\n", "hello world", cnt--);
        write(1, buffer, strlen(buffer));
    }
    
     // fflush(stdout);

    close(fd);

    return 0;
}

在这里插入图片描述

追加重定向

理解了输出重定向之后,追加重定向就变得非常简单了,只需要在打开文件时去掉 O_TRUNC 选项,加上 O_APPEND 选项即可。

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#define FILE_NAME "log.txt"

int main()
{
    int fd = open(FILE_NAME, O_WRONLY | O_CREAT | O_APPEND, 0666);
    if (fd < 0)
    {
        perror("open failed");
        return 1;
    }

    int n = dup2(fd, 1);
    if (n < 0)
    {
        perror("dup2");
        return 2;
    }

    printf("fd:%d\n", fd);
    fprintf(stdout, "fd:%d\n", fd);
    char* msg = "hello redirect\n";
    write(1, msg, strlen(msg));

    fflush(stdout);
    close(fd);

    return 0;
}

在这里插入图片描述

输入重定向

输入重定向就是通过 dup2(fd, 0) 系统调用将目标文件 fd 中的内容拷贝到 0 号 fd 中,从而将本该从标准输入 (键盘) 读入的数据转换为从目标文件中读入。

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

#define FILE_NAME "log.txt"

int main()
{
    int fd = open(FILE_NAME, O_RDONLY, 0666);
    if (fd < 0)
    {
        perror("open failed");
        return 1;
    }

    int n = dup2(fd, 0);
    if (n < 0)
    {
        perror("dup2");
        return 2;
    }

    char buffer[64];
    while(fgets(buffer,sizeof (buffer) - 1,stdin)!=NULL)
    {
        buffer[strlen(buffer)] = '\0';
        printf("%s",buffer);
    }
    close(fd);

    return 0;
}

在这里插入图片描述

4.简易shell完整实现

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <string.h>
#include <assert.h>
#include <errno.h>
#include <ctype.h> //isspace
#include <stdlib.h>

#define NUM 1024
#define OPT_NUM 64

char commandline[NUM]; // 指令数组
char *myargv[OPT_NUM]; // 指针数组,用来保存切割后的字符
int lastsig = 0;       // 退出信号
int lastcode = 0;      // 退出码

// 定义重定向相关的宏
#define NONE_REDIR 0
#define INPUT_REDIR 1
#define OUTPUT_REDIR 2
#define APPEND_REDIR 3

#define trimspace(start)        \
    do                          \
    {                           \
        while (isspace(*start)) \
            ++start;            \
    } while (0)

int redirtype = NONE_REDIR; // 重定向的类型默认为无重定向
char *redirfile = NULL;     // 重定向的文件的起始位置

// "ls -a -l -i > myfile.txt" -> "ls -a -l -i" "myfile.txt" ->
void commandcheck(char *command)
{
    assert(command);
    char *start = command;
    char *end = start + strlen(command);

    while (start < end)
    {
        if (*start == '>')
        {
            *start = '\0';
            start++;
            if (*start == '>')
            {
                // "ls -a >> file.log"
                redirtype = APPEND_REDIR;
                start++;
            }
            else
            {
                // "ls -a >    file.log"
                redirtype = OUTPUT_REDIR;
            }

            trimspace(start);
            redirfile = start;
            break;
        }
        else if (*start == '<')
        {
            //"cat <      file.txt"
            *start = '\0';
            start++;
            trimspace(start);
            redirtype = INPUT_REDIR;
            redirfile = start;
            break;
        }
        else
        {
            start++;
        }
    }
}

int main()
{
    while (1)
    {
        redirtype = NONE_REDIR;
        redirfile = NULL;
        errno = 0;

        // 输出提示符
        printf("用户名@主机名 当前路径#");
        fflush(stdout);

        // 获取用户输入, 输入的时候,输入\n
        char *s = fgets(commandline, sizeof(commandline) - 1, stdin);
        assert(s != NULL);
        (void)s;

        // 清除最后一个\n , abcd\n
        commandline[strlen(s) - 1] = 0;

        // "ls -a -l -i" -> "ls" "-a" "-l" "-i" -> 1->n
        // "ls -a -l -i > myfile.txt" -> "ls -a -l -i" "myfile.txt" ->
        // "ls -a -l -i >> myfile.txt" -> "ls -a -l -i" "myfile.txt" ->
        // "cat < myfile.txt" -> "cat" "myfile.txt" ->
        commandcheck(commandline);
        myargv[0] = strtok(commandline, " ");

        int i = 1;
        if (myargv[0] != NULL && strcmp(myargv[0], "ls") == 0)
        {
            myargv[i++] = (char *)"--color=auto";
        }

        // 如果没有子串了,strtok->NULL, myargv[end] = NULL
        while (myargv[i++] = strtok(NULL, " "))
            ;

        // 如果是cd命令,不需要创建子进程,让shell自己执行对应的命令,本质就是执行系统接口
        // 像这种不需要让我们的子进程来执行,而是让shell自己执行的命令 --- 内建/内置命令
        if (myargv[0] != NULL && strcmp(myargv[0], "cd") == 0)
        {
            if (myargv[1] != NULL)
            {
                chdir(myargv[1]);
            }
            continue;
        }

        if (myargv[0] != NULL && myargv[1] != NULL && strcmp(myargv[0], "echo") == 0)
        {
            if (strcmp(myargv[1], "$?") == 0)
            {
                printf("%d,%d\n", lastcode, lastsig);
            }
            else
            {
                printf("%s\n", myargv[1]);
            }
            continue;
        }

        // 执行命令
        pid_t id = fork();
        assert(id != -1);

        if (id == 0)
        {
            // 因为命令是子进程执行的,真正重定向的工作一定要是子进程来完成
            // 如何重定向,是父进程要给子进程提供信息的
            // 这里重定向会影响父进程吗?不会,进程具有独立性
            switch (redirtype)
            {
            case NONE_REDIR:
                // 什么都不做
                break;
            case INPUT_REDIR:
            {
                int fd = open(redirfile, O_RDONLY);
                if (fd < 0)
                {
                    perror("open failed");
                    exit(errno);
                }
                // 重定向的文件已经成功打开了
                dup2(fd, 0);
            }
            break;
            case OUTPUT_REDIR:
            case APPEND_REDIR:
            {
                umask(0);
                int flags = O_WRONLY | O_CREAT;
                if (redirtype == APPEND_REDIR)
                    flags |= O_APPEND;
                else
                    flags |= O_TRUNC;
                int fd = open(redirfile, flags, 0666);
                if (fd < 0)
                {
                    perror("open");
                    exit(errno);
                }
                dup2(fd, 1);
            }
            break;
            default:
                printf("bug?\n");
                break;
            }

            execvp(myargv[0], myargv); // 执行程序替换的时候,会不会影响曾经进程打开的重定向的文件?不会
            exit(1);
        }

        int status = 0;
        pid_t ret = waitpid(id, &status, 0);
        assert(ret > 0);
        (void)ret;
        
        lastcode = ((status >> 8) & 0xFF);
        lastsig = (status & 0x7F);
    }
}

二、理解linux下一切皆文件

操作系统是一款管理软件,它通过向下管理好各种软硬件资源 (手段),来向上提供良好 (安全、稳定、高效) 的运行环境 (目的);也就是说,键盘、显示器、磁盘、网卡等硬件也是由操作系统来管理的。而操作系统管理软硬件的方法是 先描述、再组织,即先将这些设备的各种属性抽象出来组成一个结构体,然后为每一个设备都创建一个结构体对象,再用某种数据结构将这些对象组织起来;这也就是我们上面学习到的 文件内核数据结构 file;

同时,每种硬件的访问方法都是不一样的,比如,向磁盘中读写数据与向网卡中读写数据是有明显差异的,所以操作系统需要为每一种硬件都单独提供对应的 Read、Write 方法,这些方法位于驱动层。

但是,内核数据结构是位于操作系统层的,它如何与对应的读写方法联系起来呢?-- 通过函数指针,即在 struct file 结构体中创建函数指针变量,用于指向具体的 Write 和 Read 方法函数,这样每一个硬件都可以通过自己 file 对象中的 writep 和 readp 函数指针变量来找到位于驱动层的 Write 和 Read 方法,如下:

struct file
{
    //文件的各种属性
    int types;  //文件的类型
    int status;  //文件的状态
    int (*writep)(...);  //函数指针,指向读函数
    int (*readp)(...);	//函数指针,指向写函数
    struct file* next;  //下一个file对象的地址
    //...
}

在这里插入图片描述

如图,站在操作系统内核数据结构上层来看,所有的软硬件设备和文件统一都是 file 对象,即 Linux 下一切皆文件。

注:对于键盘来说,我们只能从其中读入数据,而并不能向其写入数据;同样的,对于显示器来说,我们只能向其写入数据,而并不能从它读入数据;所以,键盘的 Write 方法和显示器的显示器的 Read 方法我们都设为 NULL。
其实 Linux 一切皆文件的特性就是面向对象语言多态的特性,file 结构体相当于基类,驱动层的各种方法和结构就相当于子类。(Linux 在编写时C++等面向对象的语言还并没有出现,所以这里是用C语言模拟实现C++面向对象)

同时,struct file 是操作系统当中虚拟出来的一层文件对象,在 Linux 中,我们一般将这一层称为 虚拟文件系统 vfs,通过它,我们就可以摒弃掉底层设备的差别,统一使用文件接口的方式来进行文件操作。

三、缓冲区

1.为什么要有缓冲区

缓冲区本质上就是一段内存,那么缓冲区是由谁申请的?缓冲区属于谁?以及为什么要有缓冲区呢?

我们以一个快递的例子进行说明:

你有一个很好的朋友他在北京,而你在重庆,你俩经常给对方送东西,那么这里有2种选择,第一你骑着车或者坐车去北京送给他,二是通过顺丰发送快递到你朋友那里。我相信大多数人更愿意选择第二种方案。

顺丰快递不是你送到快递点就立马把你的包裹寄出去,而是达到一定的数量之后一起送出去

在现实生活中,快递行业的的意义是什么呢,节省了发送者的时间。

在上面的案例中,重庆就相当于内存,北京就相当于磁盘,发送的包裹相当于数据,但由于磁盘属于外设,进程直接向磁盘文件中写数据的效率非常低,所以有了缓冲区,进程可以将自己的数据拷贝到缓冲区中,再由缓冲区将数据写入到磁盘文件中去;不过,和现实生活中一样,顺丰快递不会为了你一个人的一件快递就运输一趟,而是快递积累到一定数量时统一运输,缓冲区也不会一有数据就立马刷新,而是会采取一定的刷新策略。

所以,在现实生活中,快递行业的意义是节省发送者的时间;而在计算机中,缓冲区的意义是节省进程进行数据 IO 的时间

与其理解 fwrite 是将数据写入到文件的函数,不如理解 fwrite 是进行数据拷贝的函数,因为 fwrite 函数只是将数据从进程拷贝到缓冲区中,并没有真正将数据写入到磁盘文件中。

2.缓冲区的刷新策略

于缓冲区中的一块数据,一次写入到外设的效率是要高于少量多批次写入到外设的,因为缓冲区等待磁盘就绪的时间要远多于写入数据的时间。(比如缓冲区写入一次数据一共要花费 1s,那么可能其中 990ms 都在等待外设就绪,只有 10ms 左右的时间在进行数据写入)

所以,为了提高效率,缓冲区一定会结合具体的设备定制自己的刷新策略,Linux 中缓冲区一共存在三种刷新策略和两种特殊情况;
三种刷新策略:

1.立即刷新 (无缓冲):缓冲区中一出现数据就立马刷新,这种很少出现;
2.行刷新 (行缓冲):每拷贝一行数据就刷新一次,显示器采用的就是这种刷新策略,因为显示器是给人看了,而按行刷新符合人的阅读习惯,同时刷新效率也不会太低;
3.缓冲区满 (全缓冲):待数据把缓冲区填满后再刷新,这种刷新方式效率最高,一般应用于磁盘文件。

两种特殊情况:

1.用户使用 fflush 等函数强制进行缓冲区刷新;

2.进程退出时一般都要进行缓冲区刷新;

3.缓冲区的位置

要知道缓冲区的位置,我们先来观察下面几个现象:

#include <stdio.h>
#include <unistd.h>
#include <string.h>
int main()
{
    printf("hello printf\n");
    fprintf(stdout, "hello fprintf\n");
    const char* fputsString = "hello fputs\n";
    fputs(fputsString, stdout);

    const char* msg = "hello write\n";
    write(1, msg, strlen(msg));
    
    return 0;
}

现象1:

在这里插入图片描述

现象2:

在这里插入图片描述

#include <stdio.h>
#include <unistd.h>
#include <string.h>
int main()
{
    printf("hello printf\n");
    fprintf(stdout, "hello fprintf\n");
    const char* fputsString = "hello fputs\n";
    fputs(fputsString, stdout);

    const char* msg = "hello write\n";
    write(1, msg, strlen(msg));

    fork();

    return 0;
}

现象3:

在这里插入图片描述

现象4:

在这里插入图片描述

我们发现 printf 和 fwrite (库函数)都输出了2次,而 write 只输出了一次(系统调用)。为什么呢?肯定和

fork有关!并且这个现象一定和缓冲区有关,并且缓冲区一定不在内存中,如果缓冲区在内存中,那么write也一定会打印2次。

一般C库函数写入文件时是全缓冲的,而写入显示器是行缓冲。

printf fwrite 库函数会自带缓冲区(进度条例子就可以说明),当发生重定向到普通文件时,数据的缓冲方式由行缓冲变成了全缓冲。而我们放在缓冲区中的数据,就不会被立即刷新,甚至fork之后但是进程退出之后,会统一刷新,写入文件当中。但是fork的时候,父子数据会发生写时拷贝,所以当你父进程准备刷新的时候,子进程也就有了同样的一份数据,随即产生两份数据。write 没有变化,说明没有所谓的缓冲

我们之前谈论的所有缓冲区都不在操作系统内核中,而是位于用户级语言层面;实际上,对于C语言来说,缓冲区位于 FILE 结构体中,Linux 下,我们可以在 /usr/include/libio.h 中找到缓冲区的相关信息:

在这里插入图片描述

在这里插入图片描述

综上:printf、fwrite、fputs 等库函数会自带缓冲区,而 write 系统调用没有带缓冲区;同时,我们这里所说的缓冲区,都是用户级缓冲区。那这个缓冲区谁提供呢? printf、fwrite、fputs 是库函数, write 是系统调用,库函数在系统调用的 “上层”, 是对系统调用的 “封装”,但是 write 没有缓冲区,而 printf、fwrite、fputs 有,足以说明该缓冲区是二次加上的,又因为是C库函数,所以是由C标准库提供的。

代码结束之前,进行创建子进程

1.如果我们没有进行>,看到了4条消息stdout 默认使用的是行刷新,在进程fork之前,三条C函数已经将数据进行打印输出到显示器上(外设)你的FILE内部,进程内部不存在对应的数据啦

2.如果我们进行了>, 写入文件不再是显示器,而是普通文件,采用的刷新策略是全缓冲,之前的3条c显示函数,虽然带了\n,但是不足以stdout缓冲区写满!数据并没有被刷新!!!执行fork的时候,stdout属于父进程,创建子进程时, 紧接着就是进程退出!谁先退出,一定要进行缓冲区刷新(就是修改)写时拷贝!!数据最终会显示两份

3.write为什么没有呢?上面的过程都和wirte无关,wirte没有FILE,而用的是fd,就没有C提供的缓冲区

现在我们就可以解释上面的四种现象了:

现象1:printf、fprintf、fputs 三种C语言接口函数都是向标准输出即显示器中打印数据,而显示器采用的是行缓冲区,同时,我们每条打印语句后面都带有换行符,所以 printf、fprintf、fputs 语句执行后立即刷新缓冲区;而 write 是系统调用,不存在缓冲区,所以也是语句执行后立即刷新;所以输出结果是四条语句顺序打印。

现象2:我们通过输入重定向指令 > 将本该写入到显示器文件中的数据写入到了磁盘文件中,由于磁盘文件采用全缓冲刷新策略,所以 printf、fprintf、fputs 三条语句执行完毕后数据并不会刷新,因为缓冲区并没有被写满,而是等到进程退出这种特殊情况才会将三条语句刷新到磁盘文件中,但此时,write 语句也已经执行完毕,而 write 系统调用没有缓冲区,执行立即写入;所以输出结果是 write 在最前面。

现象3:显示器采用行缓冲,所以在 fork 之前 printf、fprintf、fputs 三条语句的数据均已刷新到显示器上了,而对于进程数据来说,如果数据位于缓冲区内,那么该数据属于进程,此时 fork 子进程也会指向该数据;但如果该数据已经写入到磁盘文件了,那么数据就不属于进程了,此时 fork 子进程也不在指向该数据了;所以,这里 fork 子进程不会做任何事情,结果和现象1一样。

现象4:我们使用重定向指令将本该写入显示器文件的数据写入到磁盘文件中,而磁盘文件采用全缓冲,所以 fork 子进程时 printf、fprintf、fputs 的数据还存在于缓冲区中 (缓冲区没满,同时父进程还没有退出,所以缓冲区没有刷新),也就是说,此时数据还属于父进程,那么 fork 之后子进程也会指向该数据;而 fork 之后紧接着就是进程退出,父子进程某一方先退出时会刷新缓冲区,由于刷新缓冲区会清空缓冲区中的数据,为了保持进程独立性,先退出的一方会发生 写时拷贝,然后向磁盘文件中写入 printf、fprintf、fputs 三条数据;然后,后退出的一方也会进行缓冲区的刷新;所以,最终 printf、fprintf、fputs 的数据会写入两份 (父子进程各写入一份),且 write 由于属于系统调用没有缓冲区,所以只写入一份数据且最先写入。

4.实现一个简易的C语言缓冲区

在理解了缓冲区的各种原理之后,我们可以手动的实现一个简易的C语言缓冲区,即实现一个简易的 FILE 结构体以及 fopen、fwrite、fclose、fflush 等C语言文件操作的相关函数,来使得我们对缓冲区的理解能够更加深刻。

mystdio.h

#pragma once 

#include <stdio.h>
#include <string.h>
#include <assert.h>
#include <errno.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>

#define SIZE 1024
#define SYNC_NOW 1
#define SYNC_LINE 2
#define SYNC_FULL 4

typedef struct EILE_
{
    int _fileno;//文件描述符
    int _flags;//刷新方式
    char buffer[SIZE];
    int _capacity;
    int _size;
}_FILE;

_FILE* fopen_(const char* path_name,const char* mode);
void fwrite_(const void* ptr,size_t size,_FILE* fp);
void fclose_(_FILE* fp);
void fflush_(_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
    {
        // 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, strlen(err));
        return NULL; // 为什么C语言文件打开失败返回NULL的原因
    }
    _FILE *fp = (_FILE *)malloc(sizeof(_FILE));
    assert(fp);

    fp->_fileno = fd;
    fp->_size = 0;
    fp->_capacity = SIZE;
    fp->_flags = SYNC_LINE; // 默认为行刷新
    memset(fp->buffer, 0, SIZE);

    return fp; // 为什么打开一个文件,就会返回一个FILE *指针
}

void fwrite_(const void *ptr, size_t size, _FILE *fp)
{
    // 1. 写入到缓冲区中
    memcpy(fp->buffer + fp->_size, ptr, size); // 这里我们不考虑缓冲区溢出的问题
    fp->_size += size;

    // 2. 判断是否刷新
    // 1.立即刷新,直接写入
    if (fp->_flags & SYNC_NOW)
    {
        write(fp->_fileno, fp->buffer, fp->_size);
        fp->_size = 0;
    }
    // 2.行刷新,判断最后的字符为'\0'
    else if (fp->_flags & SYNC_LINE)
    {
        if (fp->buffer[fp->_size - 1] == '\0')
        {
            write(fp->_fileno, fp->buffer, fp->_size);
            fp->_size = 0;
        }
    }
    // 3.全刷新,判断缓冲区是否写满
    else if (fp->_flags & SYNC_FULL)
    {
        if (fp->_size == fp->_capacity)
        {
            write(fp->_fileno, fp->buffer, fp->_size);
            fp->_size = 0;
        }
    }
    else
    {
    }
}
void fclose_(_FILE *fp)
{
    fflush_(fp);
    close(fp->_fileno);
}
void fflush_(_FILE *fp)
{
    if (fp->_size > 0)
        write(fp->_fileno, fp->buffer, fp->_size);
    fsync(fp->_fileno); // 将数据,强制要求OS进行外设刷新!
    fp->_size = 0;
}

test.c

#include "mystdio.h"

#define FILE_NAME "log.txt"

int main()
{
    _FILE *fp = fopen_(FILE_NAME, "w");
    if (fp == NULL)
    {
        perror("open failed");
        return 1;
    }

    int cnt = 5;
    char buffer[64];
    while (cnt)
    {
        sprintf(buffer, "%s:%d\n", "hello world", cnt--);
        fwrite_(buffer, strlen(buffer), fp);

    }

    fclose_(fp);
    return 0;
}

5.内核缓冲区

我们之前理解的通过C语言文件接口向磁盘文件写入数据的过程是这样的:进程先通过 fprintf、fwrite、fputs 等函数将数据拷贝到缓冲区中,然后再由缓冲区以某种刷新方式刷新 (写入) 到磁盘文件中;

但实际上缓冲区并不是直接将数据写入到磁盘文件中的,而是先将数据拷贝到 内核缓冲区 – 位于内核数据结构 file 结构体中的一块内存空间 中,最后再由操作系统自主决定以什么样的刷新策略将数据写入到外设中,而这个写入的过程和用户毫无关系。

也就是说,我们向外设中写入数据其实一共分为三个步骤 – 先通过 fwrite 等语言层面的文件操作接口将进程数据拷贝到语言层面的缓冲区中,然后再根据缓冲区的刷新策略 (无、行、全) 通过 write 系统调用将数据拷贝到 file 结构体中的内核缓冲区中,最后再由操作系统自主将数据真正的写入到外设中。(所以 fwrite 和 write 其实叫做拷贝函数更合适)

注:这里操作系统的刷新策略比我们应用层 FILE 中的缓冲区的刷新策略要复杂的多,因为操作系统要根据不同的整体内存使用情况来选择不同的刷新策略,而不仅仅是死板的分为分行缓冲、全缓冲、无缓冲这么简单。

这里还存在一种特殊情况,既然进程数据被拷贝到内核缓冲区中,再由操作系统自主刷新,那么如果操作系统崩溃了就势必会出现数据丢失;这样对于像银行这种对数据丢失0容忍的机构来说就存在一定的风险,所以操作系统提供了一个系统调用函数 fsync,其作用就是将内核缓冲区中的数据立刻直接同步到外设中,而不再采用操作系统的刷新策略。
在这里插入图片描述

我们可以使用 fsync 接口将我们实现的简易C语言缓冲区设置为在内核缓冲区中采用无缓冲:

fwrite 等C语言库函数具有缓冲区,该缓冲区位于 FILE 结构体中,我们通过这些接口向外设写入数据时需要先将数据拷贝到缓冲区中,然后再由缓冲区根据特定的刷新策略将数据写入到外设中;而 write 等系统调用没有缓冲区,进程数据将直接写入到外设中

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1286619.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

VMware虚拟机系统CentOS镜像的下载

文章目录 阿里云下载官网下载参考文档 一些小版本可能过时或者其他原因已经不能存在了&#xff0c;只有大版本号最新的&#xff0c;或者其他最新版本 阿里云下载 1-百度搜索&#xff1a;阿里云 2-找到开发者社区 3-找到下载&#xff0c;选择镜像 4-选择系统 5-点击镜像地…

【eNSP实践】eNSP实战篇(2)之简单实现交换机与主机的配置(图文详解)

目录 写在前面涉及知识1、交换机实验1.1 实验条件1.2 实验步骤A、打开eNSP软件&#xff0c;创建拓扑B、搭建主机与交换机连线C、配置交换机和主机D、验证不同网段设备可通性 1.3 通过交换机查看MAC地址 写在最后 写在前面 其实前面文章我有介绍关于路由器的使用&#xff0c;但…

OCP Java17 SE Developers 复习题08

答案 答案 答案 A. This code is correct. Line 8 creates a lambda expression that checks whether the age is less than 5, making option A correct. Since there is only one parameter and it does not specify a type, the parentheses around the parameter are …

TrustZone​之在安全状态之间切换

如果处理器处于NS.EL1,而软件想要转移到S.EL1,应该如何实现呢? 要改变安全状态,无论是向上还是向下,执行都必须经过EL3,如下图所示: 前面的图表显示了在不同安全状态之间移动涉及的步骤的示例序列。逐步进行解释: 进入较高的异常级别需要一个异常。通常,此异常…

网络程序设计

互相连接&#xff0c;发送信息 tcp和udp协议 tcp会有准备&#xff0c;udp不会准备。 8080端口&#xff1a;tomcat端口&#xff0c;java和web相连接 80端口&#xff1a;http 21端口&#xff1a;ftp 套接字 socket&#xff1a;提供给程序可以对外进行连接的接口 ip地址 特…

利用github copilot完成代码,利用正则化完成字符串中信息查找

利用正则化完成字符串中的字符拆解。 下面的代码是实现在“计算机组成原理-计科2101-123456-小明同学.docx”中提取出班级&#xff08;grade&#xff09;&#xff0c;学号&#xff08;id&#xff09;&#xff0c;姓名&#xff08;name&#xff09;。以下的代码都是github copi…

java中Random随机数使用和生成随机数的多个示例

在 Java 中&#xff0c;我们可以使用 java.util.Random 类生成伪随机数。伪随机数的特性是&#xff0c;虽然它们看起来是随机的&#xff0c;但实际上它们是由一个固定的算法生成的。只要我们提供相同的种子&#xff0c;这个算法就会生成相同的数字序列。 首先&#xff0c;我们…

Java链接数据库

本文介绍的是Java链接数据库中的JDBC操作&#xff0c;JDBC虽然现在用的不多&#xff0c;但面试的时候会问道。需要有相应的了解。下面以链接MySQL为例子。 JDBC 什么jdbc Java DataBase Connectivity是一种用于执行SQL语句的Java API&#xff0c;它由一组用Java语言编写的类和…

pod容器内无法访问集群外部主机ipv6地址

一、背景 同事反馈他这边有一环境出现pod容器内无法请求集群外部主机ipv6地址&#xff0c;但是在pod所在集群所主机上是可以请求到外部主机ipv6地址。 二、问题处理过程 首先主机和主机之间ipv6地址能通讯&#xff0c;说明主机之间网络是没啥问题&#xff0c;哪问题就出在容器…

Python语言基础学习大纲(由某大模型生成)

自从上次经丙察察游了一次滇藏线&#xff0c;已有3个没写一篇了。今天利用由某大模型生成的上面这张思维导图&#xff0c;配合这个大模型生成的6000多字拼凑出一篇博文聊以交差。 Python语言概述 一、语言特点 1.语法简单明了 Python的语法简洁易懂&#xff0c;使得编写代码…

邮件群发工具的功能:实用性与高效率功能推荐

市场营销对于每个企业来讲都至关重要&#xff0c;他能为企业带来商机的增长&#xff0c;获得持续的收益。邮件营销作为一种传统但是较少为众多行业使用的营销手段&#xff0c;同样也存在着无限的潜力。 它可以实现&#xff1a; 精准点对点个性化营销。数据报表追踪营销效果。…

如果不小心修改了按钮的名字并且忘记了原名字

出现上述情况&#xff0c;可以右边点击转到代码&#xff0c;注释掉问题行&#xff0c;此页的设计界面就恢复了。

Taro 学习教程 - - - - - 开发环境的安装 helloworld

一、Taro脚手架安装 npm install tarojs/cli -g // or yarn add tarojs/cli -g // or cnpm install tarojs/cli -g1.1 如何判断taro安装成功 taro -v正常安装成功之后显示如图&#xff1a; 1.2 环境变量配置(自行判断是否需要手动配置) 如果遇到如下问题&#xff0c;则是需要…

顶级资源!五个免费图标素材网站

图片太花哨了&#xff0c;纯文本太单调了&#xff1f;别忘了设计师的魔法武器——图标&#xff01;图标材料是UI设计师不可缺少的一部分。优秀的图标设计不仅可以提高界面美感&#xff0c;还可以提高用户的互动体验&#xff0c;帮助用户更好地了解应用程序的功能和信息。在本文…

2024年CSC国际区域问题研究及外语高层次人才培养项目介绍

国家留学基金委&#xff08;CSC&#xff09;公布了2024年国际区域问题研究及外语高层次人才培养项目&#xff0c;申报时间均为3月中下旬。为帮助关注者了解项目申报情况&#xff0c;知识人网小编特整理本文。 近日&#xff0c;国家留学基金委&#xff08;CSC&#xff09;公布了…

P4715 【深基16.例1】淘汰赛-仅思路

首先从题干要求入手&#xff0c;我们可以了解到题目要求是二进一&#xff0c;不难想到这是二叉树的题 再来&#xff0c;从题干可以知道&#xff0c;我们所采用的结构体除了需要有树的两个左右节点指针外&#xff0c;还需要两个变量用来储存“能力值”和“编号” 在这道题中&am…

回归预测 | MATLAB实现CNN-BiLSTM(卷积双向长短期记忆神经网络

效果一览 基本介绍 提出一种同时考虑时间与空间因素的卷积&#xff0d;双向长短期记忆&#xff08; CNN-BiLSTM&#xff09;模型&#xff0c;将具有空间局部特征提取能力的卷积神经网络&#xff08;CNN&#xff09;和具有能同时考虑前后方向长时间信息的双向长短期记忆&#xf…

BL121EN:IEC 61850到OPC UA的即插即用无缝转换解决方案

添加图片注释&#xff0c;不超过 140 字&#xff08;可选&#xff09; 在当今快速发展的工业自动化领域&#xff0c;实现不同通信协议之间的无缝连接是提高系统集成度、数据共享和设备互操作性的关键。钡铼技术&#xff08;Bay-Tech&#xff09;BL121EN硬网关应运而生&#xf…

YOLOv4 学习笔记

文章目录 前言一、YOLOv4贡献和改进二、YOLOv4核心概念三、YOLOv4网络架构四、YOLOv4数据增强五、YOLOv4的损失函数总结 前言 在近年来的目标检测领域&#xff0c;YOLOv4的出现标志着一个重要的技术突破。YOLOv4不仅继承了YOLO系列快速、高效的特点&#xff0c;还引入了一系列…

HarmonyOS开发基础(一)

HarmonyOS开发基础&#xff08;一&#xff09; // &#xff1a;装饰器&#xff1a;用来装饰类结构、方法、变量 Entry // Entry&#xff1a;标记当前组件为入口组件 Component // Component&#xff1a;标记为自定义组件 // struct&#xff1a;自定义组件&#xff0c;可复用的…