猿创征文| Linux——基础I/O3| 缓冲区|自己设计缓冲区实现文件操作|minishell重定向

简介: 笔记

缓冲区


缓冲区是一段内存空间,缓冲区可以提高整机效率,缓冲区刷新策略:


1.立即刷新


2.行刷新(行缓冲\n),会把\n之前的刷新出去


3.满刷新(缓冲区满了全刷新出去)


特殊情况: 1.用户强制刷新,如fflush


                  2.进程退出


谁提供缓冲区,谁维护。


采用行缓冲的设备文件——显示器 全缓冲设备文件——磁盘文件


所有的设备永远都倾向于全缓冲。缓冲区满了才刷新,需要更少次的I/O操作,也就是更少此外设的访问。


其他刷新策略是结合具体情况做妥协,既要考虑效率,也要考虑用户体验


正常情况下打印4条消息

1.png2.png

清空log.txt,然后重定向

3.png



注释掉fork

4.png

清空之后重定向,log.txt,打印了四条语句,说明这种情况一定跟fork有关

5.png



上面打印了7次我们发现,hellowrite只打印了一次,调用的是write。也就是打俩次的是C语言接口,打一次的是操作系统的接口


同样一个文件向显示器打印4行,向文件打印7行,C接口打印俩次,系统接口打印一次。


fork之前的函数已经执行完了,但不代表已经刷新了。


打印俩次,是因为缓冲区由C语言的标准库维护。如果是操作系统提供的,上面的现象应该一样。

6.png

将数据写到C标准库缓冲区,之后调用write接口,把数据刷新到操作系统里,像fputs这种函数,只不过是把数据写到了C标准库的缓冲区当中。


进程也可直接调用write接口

7.png



如果向显示器打印,刷新策略是行刷新,那么最后执行fork的时候,一定是函数执行完了,数据已经被刷新了。此时fork无意义


如果重定向,往磁盘文件打印,刷新策略就变成了全缓冲,\n此时也没意义,fork执行完后return 0;fork的时候函数是执行完了,但是数据还没刷新。数据在当前进程对应的C标准库的缓冲区中,这部分数据是父进程的数据,return 0;会强制刷新缓冲区,刷新是一个写的过程,在return 0的时刻会发生写时拷贝,刷新是写的过程,父子进程要保证独立性,发生了写实拷贝。

8.png9.png

此时又变成了四条。


fflush是C语言提供的接口,这是因为在fork之前强制刷新了缓冲区,刷新的时候给fflush传入stdout就能刷新,stdout跟缓冲区好像没关系,fflush中传的参数是FILE*类型,因为在C语言中,打开一个文件返回值是FILE*,FILE结构体除了封装了fd之外,还封装了缓冲区结构。

10.png

C语言中打开的FILE一般叫文件流。


自己设计缓冲区实现文件操作


#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <assert.h>
#include <stdlib.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)
{
    assert(pathname);
    assert(mode);
    MyFILE *fp = NULL;
    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));
            memset(fp, 0, sizeof(MyFILE));
            fp->fd = fd;
        }
    }
    else if(strcmp(mode, "w+") == 0)
    {
    }
    else if(strcmp(mode, "a") == 0)
    {
    }
    else if(strcmp(mode, "a+") == 0)
    {
    }
    else{
        //什么都不做
    }
    return fp;
}
//是不是应该是C标准库中的实现!
void fputs_(const char *message, MyFILE *fp)
{
    assert(message);
    assert(fp);
    strcpy(fp->buffer+fp->end, message); //abcde\0
    fp->end += strlen(message);
    //for debug
    printf("%s\n", fp->buffer);
    //暂时没有刷新, 刷新策略是谁来执行的呢?用户通过执行C标准库中的代码逻辑,来完成刷新动作
    //这里效率提高,体现在哪里呢??因为C提供了缓冲区,那么我们就通过策略,减少了IO的执行次数(不是数据量)
    if(fp->fd == 0)
    {
        //标准输入
    }
    else if(fp->fd == 1)
    {
        //标准输出
        if(fp->buffer[fp->end-1] =='\n' )
        {
            //fprintf(stderr, "fflush: %s", fp->buffer); //2
            write(fp->fd, fp->buffer, fp->end);
            fp->end = 0;
        }
    }
    else if(fp->fd == 2)
    {
        //标准错误
    }
    else
    {
        //其他文件
    }
}
void fflush_(MyFILE *fp)
{
    assert(fp);
    if(fp->end != 0)
    {
        //暂且认为刷新了--其实是把数据写到了内核
        write(fp->fd, fp->buffer, fp->end);
        syncfs(fp->fd); //将数据写入到磁盘
        fp->end = 0;
    }
}
void fclose_(MyFILE *fp)
{
    assert(fp);
    fflush_(fp);
    close(fp->fd);
    free(fp);
}
int main()
{
    //close(1);
    MyFILE *fp = fopen_("./log.txt", "w");
    if(fp == NULL)
    {
        printf("open file error");
        return 1;
    }
    fputs_("one: hello world", fp);
    fork();
    fclose_(fp);
}


minishell重定向


当输入"ls -a -l > log.txt"


我们转换为"ls -a -l \0 log.txt"


定义四个宏代表当前重定向的状态

11.png

进行程序替换的时候不会影响一个文件曾经打开的文件描述符,程序替换只会影响页表,只会影响代码和数据,不会对进程曾经打开过的文件有任何影响


#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <assert.h>
#define NUM 1024
#define SIZE 32
#define SEP " "
//保存完整的命令行字符串
char cmd_line[NUM];
//保存打散之后的命令行字符串
char *g_argv[SIZE];
// 写一个环境变量的buffer,用来测试
char g_myval[64];
#define INPUT_REDIR 1
#define OUTPUT_REDIR 2
#define APPEND_REDIR 3
#define NONE_REDIR 0
int redir_status = NONE_REDIR;
char *CheckRedir(char *start)
{
    assert(start);
    char *end = start + strlen(start) - 1; //ls -a -l\0
    while(end >= start)
    {
        if(*end == '>')
        {
            if(*(end-1) == '>')
            {
                redir_status = APPEND_REDIR;
                *(end-1) = '\0';
                end++;
                break;
            }
            redir_status = OUTPUT_REDIR;
            *end = '\0';
            end++;
            break;
            //ls -a -l>myfile.txt
            //ls -a -l>>myfile.txt
        }
        else if(*end == '<')
        {
            //cat < myfile.txt,输入
            redir_status = INPUT_REDIR;
            *end = '\0';
            end++;
            break;
        }
        else{
            end--;
        }
    }
    if(end >= start)
    {
        return end; //要打开的文件
    }
    else{
        return NULL;
    }
}
// shell 运行原理 : 通过让子进程执行命令,父进程等待&&解析命令
int main()
{
    extern char**environ;
    //0. 命令行解释器,一定是一个常驻内存的进程,不退出
    while(1)
    {
        //1. 打印出提示信息 [whb@localhost myshell]# 
        printf("[root@我的主机 myshell]# ");
        fflush(stdout);
        memset(cmd_line, '\0', sizeof cmd_line);
        //2. 获取用户的键盘输入[输入的是各种指令和选项: "ls -a -l -i"]
        // "ls -a -l>log.txt"
        // "ls -a -l>>log.txt"
        // "ls -a -l<log.txt"
        if(fgets(cmd_line, sizeof cmd_line, stdin) == NULL)
        {
            continue;
        }
        cmd_line[strlen(cmd_line)-1] = '\0';
        // 2.1: 分析是否有重定向, "ls -a -l>log.txt" -> "ls -a -l\0log.txt"
        //"ls -a -l -i\n\0"
        char *sep = CheckRedir(cmd_line);
        //printf("echo: %s\n", cmd_line);
        //3. 命令行字符串解析:"ls -a -l -i" -> "ls" "-a" "-i"
        // export myval=105
        g_argv[0] = strtok(cmd_line, SEP); //第一次调用,要传入原始字符串
        int index = 1;
        if(strcmp(g_argv[0], "ls") == 0)
        {
            g_argv[index++] = "--color=auto";
        }
        if(strcmp(g_argv[0], "ll") == 0)
        {
            g_argv[0] = "ls";
            g_argv[index++] = "-l";
            g_argv[index++] = "--color=auto";
        }
        //?
        while(g_argv[index++] = strtok(NULL, SEP)); //第二次,如果还要解析原始字符串,传入NULL
        if(strcmp(g_argv[0], "export") == 0 && g_argv[1] != NULL)
        {
            strcpy(g_myval, g_argv[1]);
            int ret = putenv(g_myval);
            if(ret == 0) printf("%s export success\n", g_argv[1]);
            //for(int i = 0; environ[i]; i++)
            //    printf("%d: %s\n", i, environ[i]);
            continue;
        }
        //for debug
        //for(index = 0; g_argv[index]; index++)
        //    printf("g_argv[%d]: %s\n", index, g_argv[index]);
        //4.内置命令, 让父进程(shell)自己执行的命令,我们叫做内置命令,内建命令
        //内建命令本质其实就是shell中的一个函数调用
        if(strcmp(g_argv[0], "cd") == 0) //not child execute, father execute
        {
            if(g_argv[1] != NULL) chdir(g_argv[1]); //cd path, cd ..
            continue;
        }
        //5. fork()
        pid_t id = fork();
        if(id == 0) //child
        {
            if(sep != NULL)
            {
                int fd = -1;
                //说明命令曾经有重定向
                switch(redir_status)
                {
                    case INPUT_REDIR:
                        fd = open(sep, O_RDONLY);
                        dup2(fd, 0);
                        break;
                    case OUTPUT_REDIR:
                        fd = open(sep, O_WRONLY | O_TRUNC | O_CREAT, 0666);
                        dup2(fd, 1);
                        break;
                    case APPEND_REDIR:
                        //TODO
                        fd = open(sep, O_WRONLY | O_APPEND | O_CREAT, 0666);
                        dup2(fd, 1);
                        break;
                    default:
                        printf("bug?\n");
                        break;
                }
            }
           // printf("下面功能让子进程进行的\n");
           // printf("child, MYVAL: %s\n", getenv("MYVAL"));
           // printf("child, PATH: %s\n", getenv("PATH"));
            //cd cmd , current child path
            //execvpe(g_argv[0], g_argv, environ); // ls -a -l -i
            //不是说好的程序替换会替换代码和数据吗??
            //环境变量相关的数据,会被替换吗??没有!
            execvp(g_argv[0], g_argv); // ls -a -l -i
            exit(1);
        }
        //father
        int status = 0;
        pid_t ret = waitpid(id, &status, 0);
        if(ret > 0) printf("exit code: %d\n", WEXITSTATUS(status));
    }
}

12.png

相关文章
|
28天前
|
存储 缓存 固态存储
|
2月前
|
人工智能 监控 Shell
常用的 55 个 Linux Shell 脚本(包括基础案例、文件操作、实用工具、图形化、sed、gawk)
这篇文章提供了55个常用的Linux Shell脚本实例,涵盖基础案例、文件操作、实用工具、图形化界面及sed、gawk的使用。
293 2
|
3月前
|
存储 Unix Linux
Linux I/O 重定向与管道
【8月更文挑战第17天】重定向在Linux中改变命令I/O流向,默认有&quot;&gt;&quot;覆盖输出至文件及&quot;&gt;&gt;&quot;追加输出至文件末尾,便于保存结果;使用&quot;&lt;&quot;从文件读取输入而非键盘,高效处理数据。文件描述符如0(stdin)、1(stdout)、2(stderr)标识I/O资源,支持读写操作。管道以&quot;|&quot;连接命令,使前一命令输出成为后一命令输入,如排序用户或找出CPU占用最高的进程,构建复杂数据处理流程。
47 9
|
3月前
|
监控 Linux
在Linux中,如何监控磁盘I/O性能?
在Linux中,如何监控磁盘I/O性能?
|
3月前
|
Linux
Linux的I/O操作
Linux的I/O操作
|
3月前
|
存储 Unix Linux
Linux I/O 重定向与管道
【8月更文挑战第14天】输出重定向可将命令结果存入文件,如`&gt;`覆盖写入或`&gt;&gt;`追加写入。输入重定向从文件读取数据,如`&lt;`代替键盘输入。这些操作利用文件描述符(如0:stdin, 1:stdout, 2:stderr)管理I/O。管道`|`连接命令,使前一命令输出作为后一命令输入,便于数据处理,如排序用户`sort -t: -k3 -n /etc/passwd | head -3`或查找CPU占用高的进程`ps aux --sort=-%cpu | head -6`。
39 4
|
3月前
|
Unix Linux Shell
Linux I/O 重定向简介
Linux I/O 重定向简介
36 2
|
3月前
|
存储 Linux 数据处理
在Linux中,管道(pipe)和重定向(redirection)的是什么?
在Linux中,管道(pipe)和重定向(redirection)的是什么?
|
3月前
|
小程序 Linux 开发者
Linux之缓冲区与C库IO函数简单模拟
通过上述编程实例,可以对Linux系统中缓冲区和C库IO函数如何提高文件读写效率有了一个基本的了解。开发者需要根据应用程序的具体需求来选择合适的IO策略。
30 0
|
16天前
|
Linux
Linux系统之expr命令的基本使用
【10月更文挑战第18天】Linux系统之expr命令的基本使用
51 4