攻破:重定向 && 缓冲区

前言:

​ 从上一章开始,我们进入了文件IO的学习,认识了文件描述符是个什么,以及在操作系统内部是如何管理那么多文件的,最终我们还解释了为什么说“linux一切皆文件”这个概念。下面我们继续往下学习,认识输入输出重定向以及了解缓冲区的概念。

认识读文件read

​ 首先我们需要知道:文件 = 属性 + 内容。
​ 因此,对文件操作本质就是对文件的属性或文件的内容进行操作。
​ 对于文件的内容操作,不论我们是使用系统调用接口还是说语言层面的函数,我们都可以实现对文件的内容的操作,但是对于属性我们暂时无法添加相对应的属性,但我们可以通过一个函数来实现文件属性的查找:

int stat(const char* path, struct stat* buf);
  • struct stat这个结构体包含了文件的各种信息,比如文件大小。而buf就是我们的输出型参数。

  • 而对于read的操作,我们只需要先将open里的宏替换成O_RDONLY,然后再实现read的代码:

    ssize read
    (
    	int fd, 		// 从文件描述符 fd 指定的文件中读取内容
    	void* buf, 		// 将读取到的内容放到 buf 中
    	size_t count	// 指明希望从文件中读取的字节数
    );	
    

所以我们便可以先利用write在log.txt里写入“hello linux file" 再通过stat获取文件的大小,最后通过read读取数据存放至buffer数组中,便可以实现文件的读取,代码如下:

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

int main()
{
    struct stat st;
    int n = stat("log.txt", &st);
    
    printf("file size: %lu\n", st.st_size);

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

    char* file_buffer = (char*)malloc(st.st_size);
    if(file_buffer == NULL)
    {
        perror("malloc");
        return 2;
    }
    
    n = read(fd, file_buffer, st.st_size);
    if(n < 0)
    {
        perror("read");
        return 3;
    }

    printf("file_buffer: %s\n", file_buffer);

    return 0;
}

image-20240824123612178

认识重定向&&缓冲区

重定向现象及分析:

  • 先看代码:
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>

int main()
{
    close(1); // 先关闭默认的stdout,也就是关闭了显示屏
    int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
    if(fd < 0)
    {
        perror("open");
        return 1;
    }
    printf("fd: %d\n", fd);
    fprintf(stdout, "%s\n", "this is fprintf");
    
    const char* msg = "This is write\n";
    int n = write(fd, msg, strlen(msg));
    if(n < 0)
    {
        perror("write");
        return 2;
    }
    return 0;
}

image-20240824125401360

image-20240824131025786

问题出现了!我明明在代码中printf了我的文件描述符fd,为什么没有显示呢?
为什么我明明直接向stdout用fprintf打印了一句话,最后也没有在显示屏显示呢?
还有,为什么在问log.txt的文件中,新创建的文件描述符变成了1?1不是显示器文件吗???

  • 分析现象:

image-20240824130922446

image-20240824131741123

image-20240824132227300

因此,printf和fprintf原本是向1号对应的显示屏打印的,但是由于先关闭了一号,导致发生了文件的重定向,log.txt的文件描述符就变为了1号。因此最终其实是向log.txt中打印数据!

我们也能得知文件描述符的分配规则
——先查自己的文件描述符表(就是上图的struct file* arrat[ ]数组),分配最小的且没有退出的文件描述符fd。

dup2的介绍:

​ 现在我差不多知道什么叫重定向了,不就是原本是往显示器文件打印数据,变成了往普通文件打印数据吗。但是实现这个却要先close掉显示器文件,可不可以不关掉直接发生重定向呢?对此,我们就要引入一个函数:

int dup2(int oldfd, int newfd);

​ 对于这个函数,它的本质其实是 文件描述符下标所对应的内容的拷贝,将老的文件描述符往新的文件描述符进行拷贝。
​ 例如我想实现上述那样,将原本的3号文件描述符拷贝至1号文件描述符(当然这里一定是对内容的拷贝而不是下标!!!)我们就可以这样写:dup2(fd, 1);(意思是将fd的内容拷贝至1号文件描述符当中)

  • 代码演示:
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>

int main()
{
    int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
    if(fd < 0)
    {
        perror("open");
        return 1;
    }
    
    dup2(fd, 1);
    
    // C
    printf("fd: %d\n", fd);
    fprintf(stdout, "%s\n", "this is fprintf");
    
    // system call
    const char* msg = "This is write\n";
    int n = write(fd, msg, strlen(msg));
    if(n < 0)
    {
        perror("write");
        return 2;
    }
    
    return 0;
}

image-20240824170845832

这个时候我们会发现通过dup2实现的重定向,原本是3号文件描述符并不会发生改变,原因如下:

image-20240825124405263

缓冲区的引入:

​ 其实对于上述的代码,我们并没有写完全。按道理来说,在使用系统调用open打开一个文件,最后是需要close掉的,可是我们并没有close。接下来我们使用先关close(1)而实现重定向的代码来分析缓冲区:

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

int main()
{
    close(1); // 先关闭默认的stdout,也就是关闭了显示屏
    int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
    if(fd < 0)
    {
        perror("open");
        return 1;
    }
    printf("fd: %d\n", fd);
    fprintf(stdout, "%s\n", "this is fprintf");
    
    const char* msg = "This is write\n";
    int n = write(fd, msg, strlen(msg));
    if(n < 0)
    {
        perror("write");
        return 2;
    }
    
    close(fd); // 不同的在这里,我最后把fd关了
    return 0;
}

image-20240825125508753

​ 最后结果运行结果也是没有在显示屏上显示,原理我们知道了发生了重定向。但我们发现log.txt文件里的数据缺少了很多,没有printf / fprintf打印的数据,只有系统调用的数据,这是什么原因呢?
image-20240825133133893

image-20240825141538273

但如果我们在close(1)之前刷新缓冲区,就可以显示数据了:
使用函数fflush(stdout);

image-20240825141948835

至此我们就发现数据写入进去了。

缓冲区的理解:

​ 从上述的理解来看,缓冲区分为 “用户级缓冲区” “内核级缓冲区”。
​ 存在两大好处:
​ 1、解耦(不必在意底层是如何实现)
​ 2、提高效率

  • 为什么提高效率?

    ——目的是提高用户的效率。
    首先调用系统调用,是有成本的!
    少调用成本就低了,效率就高了。
    先暂存,最后再刷新(提高了刷新IO效率)

  • 是什么?

    是一段内存空间(每个文件都有自己的缓冲区)

  • 为什么?

    给上层提供高效的IO体验,间接提高整体效率。

  • 怎么办?

    1. 立即刷新(无缓存,直接释放缓冲区) —> fflush(stdout), int sync(int fd);
    2. 行刷新 —> 显示器(照顾用户的习惯)
    3. 全缓冲 —> 缓冲区写满,才刷新(针对的是普通文件)
    4. 进程退出,系统会自动刷新,强制刷新

来看看代码:

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

int main()
{
    printf("hello printf\n");
    fprintf(stdout, "hello fprintf\n");
    
    const char* msg = "hello write\n";
    write(1, msg, strlen(msg));

    fork();

    return 0;
}

image-20240825154730193

​ 代码这一看是没啥问题的,因为加了\n就会自动刷新,从语言层缓冲区刷新至内核级缓冲区此时我们使用重定向—— ./myfile > log.txt
image-20240825155608467

​ 这时我们就会发现一个神奇的现象,关于语言层的输出最终会被打印两次。造成这种现象的就是fork( )导致的。

​ 当运行程序不发生重定向时,stdout是面向的显示器的,也就是1号文件描述符对应的是显示器,针对于显示器,语言级的缓冲区是按照 “行刷新” 的方式进行刷新缓冲区,所以我们就会看到程序通过’\n’一行一行的输出至屏幕上。但是由于执行了 ./myfile > log.txt,导致stdout面向的是普通文件,针对于普通文件,语言级的缓冲区是按照 “全缓冲” 的方式进行刷新的,只有当语言级缓冲区满了或者进程退出时,才会刷新缓冲区。

​ 问题就出现在进程上,当面向文件时执行的全刷新,当fork( )创建子进程后,会导致在同一个代码结束时,造成两次刷新语言级缓冲区,从而导致打印了两次。
​ 下面我们通过进程等待,来看看缓冲区被释放的过程:

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

int main()
{
    printf("hello printf/ ");
    fprintf(stdout, "hello fprintf/ ");
    
    const char* msg = "hello write/ ";
    write(1, msg, strlen(msg));
    

    pid_t id = fork();
    if(id == 0)
    {
        // child
        sleep(3);
        printf("<- child process quit ->/ ");
        return 0;
    }
 
    // father
    int status = 0;
    pid_t rid = waitpid(id, &status, 0);
    
    sleep(3);
    printf("<- father process quit ->/ ");
    return 0;
}

image-20240825162811240
这个代码下来可以验证下,因为我有用sleep函数进行时间控制

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

无双@

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

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

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

打赏作者

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

抵扣说明:

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

余额充值