基础IO(中)——Linux

本文深入探讨Linux中的缓冲区机制,包括缓冲区的理论知识、用户层缓冲区的设计、minishell的重定向支持以及缓冲区与进程、文件描述符关闭的相关问题。通过实例分析了缓冲区在不同场景下的刷新策略,特别是如何处理标准输出和错误输出的重定向。
摘要由CSDN通过智能技术生成

1. 缓冲区

1.1 理论知识

  1. 什么是缓冲区?
    就是一段内存空间
    这个空间是由 用户提供的

2. 为什么要有缓冲区?
写透模式,WT——成本高,慢
写回模式,WB——成本低,快
缓冲区存在的意义:加速,提高整机效率;主要是为了提高用户的响应速度

  1. 缓冲区在哪里?
    缓冲区刷新策略:
  • 立即刷新
  • 行刷新(行缓冲\n \r\n)
  • 满刷新(全缓冲)
     
    特殊情况:
  • 用户强制刷新(fflush)
  • 进程退出
  • 缓冲策略 = 一般 + 特殊
  1. 关于缓冲区的认识:
    一般而言,行缓冲的设备文件——显示器
    全缓冲的设备文件——磁盘文件
    注意:所有的设备,永远都倾向于全缓冲——缓冲区满了才刷新——意味着需要更少次的IO操作——更少次的外设访问——提高效率
    (和外部设备IO的时候,数据量的大小不是主要矛盾,是你和外部设备预备IO的过程是最耗费时间的)
    其他刷新策略是结合具体情况做的妥协!
    显示器:直接给用户看的,一方面要照顾效率,一方面要照顾用户体验
    (极端情况:你是可以自定义规则的)
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<fcntl.h>

int mian()
{
//c
printf("hello printf\n");
fprintf(stdout, "hello fprintf\n");
const char *s = "hello fputs\n";
fputs(s, stdout);

//os
const char *ss = "hello write\n";
write(1,ss, strlen(ss));
//注意:我们是在最后调用的fork,上面的函数是已经被执行完了
fork();//创建子进程
 return 0}

同一个程序,向显示器打印输出4条文本
./myfile
hello printf
hello fprintf
hello fputs
hello write

向普通文件(磁盘上)(重定向之后),打印的时候变成了7行:
只有 write打印一次,也就是C语言IO接口都打印两次,os系统接口只打印一次和向显示器打印一样
./myfile > log.txt
cat log.txt
hello write
hello printf
hello printf
hello fprintf
hello fprintf
hello fputs
hello fputs

原因:
这种现象与fork()有关
fork上面的函数是已经被执行完了,但并不代表数据已经刷新了
缓冲区的数据也是进程的数据
写时拷贝,会将文件拷贝两次

上面的测试,并不影响系统接口,如果有所谓的缓冲区,那我们之前所谈的缓冲区应该是由谁维护呢?
之前所谈的缓冲区,绝对不是由os提供的,因为如果是OS提供的,那么我们上面的代码,现象应该是一样的。所以是由C标准库提供的。

  1. 如果向显示器打印,刷新策略是行刷新,那么最后执行fork的时候,一定是函数执行完了并且数据已经被刷新了,此时fork毫无意义!
  2. 如果对应的程序进行了重定向——变成了要向磁盘文件打印——隐形的刷新策略就变成了全缓冲——此时\n就没有意义。所以fork的时候——一定是函数执行完了,但是数据还没有刷新。
    那么在当前进程对应的C标准库中的缓冲区中,这部分数据是不是父进程的数据?
    是的!!!
    fork之后,return 0 需要父子各自退出
    刷新是不是写的过程?是!
    所以就会出现写时拷贝,使得父子各一份,所以就会出现打印两次
     
    C标准库给我们提供的是用户级缓冲区
    在fork之前强制刷新
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<fcntl.h>

int mian()
{
//c
printf("hello printf\n");
fprintf(stdout, "hello fprintf\n");
const char *s = "hello fputs\n";
fputs(s, stdout);

//os
const char *ss = "hello write\n";
write(1,ss, strlen(ss));

       fflush(stdout);//只传stdout就知道缓冲区在哪里

//注意:我们是在最后调用的fork,上面的函数是已经被执行完了
fork();//创建子进程
 return 0}

C语言,打开文件
FILE *fopen(const char *path,const char *mode);
struct FILE:结构体——1.内部封装了fd 2. 还包含了该文件fd对应的语言层的缓冲区结构

C语言中打开的FILE,文件流cout cin是类
里面必须包含1. fd 2. buffer
cout << —— operator <<重载
struct FILE中有自己的内核缓冲区

 
总结:

1.2 我们自己设计一下用户层缓冲区

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

./myfile
one: hello world
./myfile > log.txt
cat log.txt
one: hello world
one: hello world

 

1.3 minishell支持重定向

myshell.c

#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");
return 0;
}

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

close(fd);

return 0;
}

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

1.4 补充两个小问题

1.4.1 close关闭fd之后文件内部没有数据

写一个之前的代码
Makefile

myfile:myfile.c
gcc -o $@ $^
.PHONY:clean
clean:
   rm -f myfile

myfile.c

#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");
return 0;
}

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

close(fd);

return 0;
}

在这里插入图片描述

 

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

printf("hello world\n");//stdout->1

//const char *msg = "hello wrold\n";
//write(fd, msg, strlen(msg));

close(fd);

return 0;
}

在这里插入图片描述

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

printf("hello world\n");//stdout->1

//const char *msg = "hello wrold\n";
//write(fd, msg, strlen(msg));

close(fd);

return 0;
}

在这里插入图片描述
 

close(1);

在这里插入图片描述

什么都不显示
是因为\n之前的字符串在缓冲区里

想要看见打印内容需要:

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

printf("hello world\n");//stdout->1
fflush(stdout);

//const char *msg = "hello wrold\n";
//write(fd, msg, strlen(msg));

close(fd);

return 0;
}

在这里插入图片描述

为什么fflush?
printf——1——stdout,数据会暂存在stdout的缓冲区中
没有fflush先close(fd)
fd——1:数据在缓冲区中,但是对应的fd先关了,数据便无法刷新了!
所以想要看见,就需要先刷新

 

1.4.2 1,2 stdout、 stderr

mv myfile.c myfile.cc
myfile:myfile.cc
gcc -o $@ $^
.PHONY:clean
clean:
   rm -f myfile
#include<iostream>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main()
{
//stdot -> 1
printf("hello printf 1\n");
fprintf(stdout, "hello fprintf 1\n");
//stderr ->2
perror("hello perror 2");//stderr

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

const char *s2 = "hello write 2\n";
write(2, s2, strlen(s2));

//cout ->1
std::cout << "hello cout 1" << std::endl;
//cerr ->2
std::cerr << "hello cerr 2" << std::endl;

}

./myfile
hello printf 1
hello fprintf 1
hello perrpr 2: Success
hello write 1
hello write 2
hello cout 1
hello cerr 2

重定向是往1号文件描述符里写
>>也是如此
./myfile > log.txt
hello perror 2: Success
hello write 2
hello cerr 2

cat log.txt
hello write 1
hello printd 1
hello fprintf 1
hello cout 1

可看出,1和2对应的都是显示器文件
但是它们两个是不同的,就像同一个显示器文件被打开了两次
一般而言,如果程序运行可能有问题的话,建议使用stderr或者cerr来打印
如果是常规的文本内容,我们建议进行cout,stdout打印

常规与报错分开打印:
./myfile > ok.txt 2>err.txt
cat ok.txt
hello write 1
hello printd 1
hello fprintf 1
hello cout 1
cat err.txt
hello perror 2: Success
hello write 2
hello cerr 2

全部打印到一个文件里:
./myfile > log.txt 2>&1
cat log.txt
hello perrpr 2: Success
hello write 1
hello write 2
hello printf 1
hello fprintf 1
hello cout 1
hello cerr 2
2>&1就是将1的内容拷贝给2一份

拷贝:
cat < log.txt
hello perrpr 2: Success
hello write 1
hello write 2
hello printf 1
hello fprintf 1
hello cout 1
hello cerr 2
cat < log.txt >back.txt
cat back.txt
hello write 1
hello write 2
hello printf 1
hello fprintf 1
hello cout 1
hello cerr 2

 
perror、errno

#include <errno.h>

int main()
{
//stdot -> 1
printf("hello printf 1\n");
fprintf(stdout, "hello fprintf 1\n");
//stderr ->2

errno = 2
perror("hello perror 2");//stderr

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

const char *s2 = "hello write 2\n";
write(2, s2, strlen(s2));

//cout ->1
std::cout << "hello cout 1" << std::endl;
//cerr ->2
std::cerr << "hello cerr 2" << std::endl;

}

报错就会变:
./myfile
hello printf 1
hello fprintf 1
hello perrpr 2: No such file or directory
hello write 1
hello write 2
hello cout 1
hello cerr 2

1,2,3对应不同的错误——错误码
perror会自动匹配错误码

 
自己设置一个perror:

#include <errno.h>

void myperror(const char *msg)
{
fprintf(stderr, "%s: %s\n", msg, strerrer(errno));
}

int main()
{
int fd = open("log.txt", O_RDONLY);
if (fd < 0)
{
myperror("open");
return 1;
}
return 0;
}

记得先删掉log.txt文件因为这里只读不写
./myfile
open: No such file or directory

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Hey pear!

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值