自学内容网 自学内容网

【Linux】重定向&&缓冲区

一、文件内核级缓冲区

在一个struct file内部还要有一个数据结构-----文件的内核级缓冲区。

1.1 write写入操作

当我们去对一个文件写入的时候,那么是如何进行写入的呢?

如:write(3,"hello",..)

先找到文件的内核缓冲区,然后把"hello"拷贝到文件的内核缓冲区,然后再由os自主决定什么时候刷新到外设。

write本质是一个拷贝函数,从用户拷贝到内核,文件这些数据结构都是OS给我们提供的。

每个文件都有属于自己的文件操作表,都有属于自己的内核级缓冲区。

我们平时写文件的时候,比如word文档的时候,已经键盘里进行输入了,为什么最后还要进行保存,保存是在干什么?写只是把数据写到文件的内核级缓冲区,保存是把内容从缓冲区刷新到外设,这个过程叫做写入。

1.2 read读取操作

上层调用read(3,buffer,...),在读的时候本质是在做什么呢?

找到文件描述符表,找到文件,然后他会检测当前数据是在文件内核级缓冲区内,还是在磁盘上,如果在读的时候,数据没在缓冲区里面,就会触发我们read方法,把数据从磁盘读到缓冲区里,然后read开始进行把缓冲区里的数据拷贝到buffer中。

回顾scanf,调用scanf时,scanf对应的外设中根本没有数据,此时调用scanf就会阻塞。

1.3 文件的内核缓冲区作用

因为内存的的操作非常块,外设的操作非常慢,

如果每一次写入一部分数据,都要进行一次IO访问外设,如果写一百次就要一百次IO,这样耗费的时间就特别长,而我们数据从内存拷贝到内存这个速度是特别快的,我们把一百次的数据积累到一块,统一坐刷新,这样就可以节省99次IO的时间,

所以缓冲区的存在,是为了提高效率。

二、重定向

我们在打开文件的时候,把0关掉会怎么样? 

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
 
int main()
{
    close(0);
    int fd1 = open("log1.txt",O_WRONLY | O_CREAT | O_APPEND,0666);
    int fd2 = open("log2.txt",O_WRONLY | O_CREAT | O_APPEND,0666);
    int fd3 = open("log3.txt",O_WRONLY | O_CREAT | O_APPEND,0666);
    int fd4 = open("log4.txt",O_WRONLY | O_CREAT | O_APPEND,0666);
 
    printf("fd1: %d\n",fd1);
    printf("fd2: %d\n",fd2);
    printf("fd3: %d\n",fd3);
    printf("fd4: %d\n",fd4);
 
    close(fd1);
    close(fd3);
    close(fd3);
    close(fd4);
    return 0;
}
[zxw@hcss-ecs-cc58 lesson18]$ ./test 
fd1: 0
fd2: 3
fd3: 4
fd4: 5

 此时我们发现,0号位置被fd1文件描述符占了。

再把2关了。

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
 
int main()
{
    close(2);
    int fd1 = open("log1.txt",O_WRONLY | O_CREAT | O_APPEND,0666);
    int fd2 = open("log2.txt",O_WRONLY | O_CREAT | O_APPEND,0666);
    int fd3 = open("log3.txt",O_WRONLY | O_CREAT | O_APPEND,0666);
    int fd4 = open("log4.txt",O_WRONLY | O_CREAT | O_APPEND,0666);
 
    printf("fd1: %d\n",fd1);
    printf("fd2: %d\n",fd2);
    printf("fd3: %d\n",fd3);
    printf("fd4: %d\n",fd4);
 
    close(fd1);
    close(fd3);
    close(fd3);
    close(fd4);
    return 0;
}
[zxw@hcss-ecs-cc58 lesson18]$ ./test 
fd1: 2
fd2: 3
fd3: 4
fd4: 5

 此时我们发现,2号位置被fd1文件描述符占了。

所以,fd的分配规则是,系统在创建文件描述符时会寻找当前未使用的最小下标

接下来我们把1号的位置关了。代码不变,我们仔细观察下面创建文件的字节数。

[zxw@hcss-ecs-cc58 lesson18]$ ./test 
[zxw@hcss-ecs-cc58 lesson18]$ ll
total 20
-rw-rw-r-- 1 zxw zxw    0 Jan 16 19:26 log1.txt
-rw-rw-r-- 1 zxw zxw    0 Jan 16 19:26 log2.txt
-rw-rw-r-- 1 zxw zxw    0 Jan 16 19:26 log3.txt
-rw-rw-r-- 1 zxw zxw    0 Jan 16 19:26 log4.txt
-rw-rw-r-- 1 zxw zxw   59 Jan 16 19:11 Makefile
-rwxrwxr-x 1 zxw zxw 8456 Jan 16 19:26 test
-rw-rw-r-- 1 zxw zxw  600 Jan 16 19:26 test.c

 我们发现输出的内容没有在文件log1.txt内输出。

把1关了相当于把显示器输出关了,printf不知道哦我们关了一号文件,printf就无法把内容输入到显示器。

给代码加fflush(stdout),就可以,为什么,我们再用户级缓冲区说。

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
 
int main()
{
    close(2);
    int fd1 = open("log1.txt",O_WRONLY | O_CREAT | O_APPEND,0666);
    int fd2 = open("log2.txt",O_WRONLY | O_CREAT | O_APPEND,0666);
    int fd3 = open("log3.txt",O_WRONLY | O_CREAT | O_APPEND,0666);
    int fd4 = open("log4.txt",O_WRONLY | O_CREAT | O_APPEND,0666);
 
    printf("fd1: %d\n",fd1);
    printf("fd2: %d\n",fd2);
    printf("fd3: %d\n",fd3);
    printf("fd4: %d\n",fd4);

    fflush(stdout);

    close(fd1);
    close(fd3);
    close(fd3);
    close(fd4);
    return 0;
}
[zxw@hcss-ecs-cc58 lesson18]$ cat log1.txt 
fd1: 1
fd2: 3
fd3: 4
fd4: 5

 printf本来应该向显示器文件写入,结果却写入到文件中什么意思?

上层stdout封装的1不变,把1号下标内容指向显示器改成指向文件,这个动作我们就叫做重定向。

1号下标内容原本是显示器,我们把显示器的内容换成了文件里,所以写到了文件中。

重定向的原理:

简单说将 fd_array 数组当中的元素struct file* 指针的指向关系进行修改,改变成为其它的struct file结构体的地址—每个文件描述符都是一个内核中文件描述信息数组的下标,对应有一个文件的描述信息用于操作文件,而重定向就是在不改变所操作的文件描述符的情况下,通过改变描述符对应的文件描述信息进而实现改变所操作的文件。

重定向可以分为输出重定向、追加重定向和输入重定向三种类型。输出重定向是将本应该打印到显示器的内容输出到了指定的文件中,例如 ls > list.txt;追加重定向是将本应该打印到显示器的内容追加式地输出到了指定的文件中,例如 ls >> list.txt;输入重定向是将本应该从键盘中读取的内容改为从指定的文件中读取,例如 cat < input.txt

 2.1 dup2

操作系统给我们提供一个系统函数dup2 

       #include <unistd.h>
       int dup2(int oldfd, int newfd);

       makes newfd be the copy of oldfd, closing newfd first if necessary

把oldfd拷贝到newfd地位置,可以这样理解,newfd是最死的最快的,oldfd是最老的活的最久的。

dup2(fd1,2) 

把要重定向2的地址覆盖,原本2指向的文件会自动关闭,要拷贝的那个fd1默认是没有关的,但是如果不关会被两个指针指向,但是一个文件可以被多个指针共同指向,struct file里面有f_count,叫做引用计数,有一个指针指向,引用计数就为1,两个指向,引用计数就为2,当引用计数为0时,才会进行关闭。

2.2 输出重定向 

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
 
int main()
{
    int fd = open("log.txt",O_WRONLY | O_CREAT | O_TRUNC,0666);
 
    dup2(fd ,1);
    printf("hello word\n");
    fprintf(stdout,"hello bit\n");
    fputs("111111\n",stdout);
    char *message = "aaaaaa\n";
    fwrite(message,1,strlen(message),stdout);
    write(1,"cccccc\n",7);
 
 
    return 0;
}
[zxw@hcss-ecs-cc58 lesson18]$ cat log.txt 
cccccc
hello word
hello bit
111111
aaaaaa

第一,这里为什么没有加fflush却可以打印到文件中,这是因为,没有对文件进行关闭,当进程结束后,会自动刷新缓冲区里的内容,后面会讲。

第二,我们cccccc是最后写入的却第一个打印上去,因为write是无缓冲的系统调用,会直接讲数据写入到文件中,前面几个打印都是会会先打印用户级缓冲区,后面会具体讲用户级缓冲区,也就是write不需要经过用户级缓冲区,直接将用户指定的数据从用户空间的缓冲区发送到内核缓冲区。

 2.3 追加重定向

只是把上面的代码O_TRUNC改成了O_APPEND

[zxw@hcss-ecs-cc58 lesson18]$ cat log.txt 
cccccc
hello word
hello bit
111111
aaaaaa
cccccc
hello word
hello bit
111111
aaaaaa

 2.4 输入重定向

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

int main()
{
    int fd = open("log.txt",O_RDWR);
    dup2(fd,0);
    char buffer[2048];
    size_t s = read(0,buffer,sizeof(buffer));
    if(s > 0)
    {
        buffer[s] = 0;
        printf("%s",buffer);                                                                            
    }

    return 0;
}
[zxw@hcss-ecs-cc58 lesson18]$ ./test 
cccccc
hello word
hello bit
111111
aaaaaa
cccccc
hello word
hello bit
111111
aaaaaa

三、 用户级缓冲区

解决上面遗留问题

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
 
int main()
{
    close(1);
    int fd1 = open("log1.txt",O_WRONLY | O_CREAT | O_TRUNC,0666);
 
    printf("fd1: %d\n",fd1);
    write(fd2,"fd1\n",4);
    close(fd1);
    return 0;
}
[zxw@hcss-ecs-cc58 lesson19]$ cat log1.txt 
fd2
[zxw@hcss-ecs-cc58 lesson19]$ ll
total 24
-rw-rw-r-- 1 zxw zxw    4 Jan 17 19:25 log1.txt
-rw-rw-r-- 1 zxw zxw   58 Jan 17 19:19 Makefile
-rwxrwxr-x 1 zxw zxw 8512 Jan 17 19:25 test
-rw-rw-r-- 1 zxw zxw  307 Jan 17 19:25 test.c

为什么只有write打印在log1.txt中。printf的内容为什么没有打印到log1.txt?我们在上面代码的close上添加fflush(stdout)

[zxw@hcss-ecs-cc58 lesson19]$ cat log1.txt 
fd2
fd1: 1

此时我们发现log1.txt中就显示出所打印的内容。为什么?为什么printf需要fflush而write就不需要,printf底层难道不是write???

看下面

在我们调用一下一些C语言接口的时候,并不是把数据直接拷贝到文件内核级缓冲区,把数据拷贝到内核,他的成本会很高,因为会调用系统调用。为了提高效率,先把数据先放到用户级缓冲区里,等他收集足够多的,字符串信息后,然后统一调用我们系统函数接口,从用户级缓冲区拷贝到内核级缓冲区,这样一次调用就能完成大量的数据拷贝工作。

这个缓冲区在哪???

以C语言为例(每个编程语言都一样),我们知道C语言中文件操作通常都是通过 FILE 结构体来进行的,因此,FILE 结构体一定包含了有关文件的信息,包括文件描述符、缓冲区等。

我们可以打开/usr/include/libio.h来看看源码 

上面的代码就是与缓冲区相关的内容。

 好~~~~~~接下来我们解决上面之前遗留的问题

1.为什么只有write打印在log1.txt中。printf的内容为什么没有打印到log1.txt

因为关闭了stdout,文件fd1替换了stdout的位置,即使有\n但是不在stdout而是在普通文件。所以用户级缓冲区的内容没有刷新到文件内核级缓冲区,导致printf的内容没有打印到log1.txt中。

2.在上面代码的close上添加fflush(stdout)。此时我们发现log1.txt中就显示出所打印的内容。为什么?为什么printf需要fflush而write就不需要,printf底层难道不是write???

因为fflush把用户级缓冲区的内容刷新到了文件级缓冲区,write是系统函数,直接把内容写到了文件级缓冲区,而printf是c语言库中的函数,内容写在用户级缓冲区中。我们从FILE的源码可知printf底层封装了write。printf表面是输出函数其实也是拷贝函数。 

在用户级缓冲区就可以人为的进行控制缓冲区的刷新方案:

  • 显示器文件:行刷新(\n)
  • 普通文件:缓冲区写满再刷新,
  • 不经过缓冲区,不用C语言接口,直接调用系统函数接口

从用户级缓冲区到内核级缓冲区,我们就认为发生了拷贝,用户把数据交给内核,就跟用户无关了

 补充下一个知识。

当一个进程退出的时候,会自动刷新缓冲区内容,包括stdin stdout stderr。exit也是一样退出前会刷新缓冲区。

什么意思???

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
 
int main()
{
    close(1);
    int fd1 = open("log1.txt",O_WRONLY | O_CREAT | O_TRUNC,0666);
 
    printf("fd1:\n");
    write(fd2,"fd1\n",4);
    fclose(stdout);
    return 0;
}
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
 //去除fclose
int main()
{
    close(1);
    int fd1 = open("log1.txt",O_WRONLY | O_CREAT | O_TRUNC,0666);
    printf("fd1:\n");
    write(fd2,"fd1\n",4);
    return 0;
}
[zxw@hcss-ecs-cc58 lesson19]$ cat log1.txt 
fd2
fd1: 

 两者都能把printf中的内容刷新出来。

fclose返回值是FILE,C语言层函数。当关闭stdout的时候会先把用户缓冲区的内容先刷新出来。

当去除fclose时,这个程序进行结束后,会自动刷新缓冲区内容。

我们在看一个小例子

#include <stdio.h>
#include <string.h>
#include <unistd.h>
 
int main()
{
    //C库
    printf("hello printf\n");
    fprintf(stdout,"hello fprintf\n");
    const char *message = "hello fwrite\n";
    fwrite(message,1,strlen(message),stdout);
 
    //系统调用
    const char *w = "hello write\n";
    write(1,w,strlen(w));
 
    fork();
    return 0;
}
[zxw@hcss-ecs-cc58 lesson19]$ ./test 
hello world 
hello fprintf
hello fwrite
hello write
[zxw@hcss-ecs-cc58 lesson19]$ ./test > test.txt 
[zxw@hcss-ecs-cc58 lesson19]$ cat test.txt 
hello write
hello world 
hello fprintf
hello fwrite
hello world 
hello fprintf
hello fwrite

 我们发现我们正常执行的时候我们会在显示器中正常打印,但是我们把输出的内容重定向到test.txt中,write的内容正常输出,其余C语言库中的函数全部输出两次。这是为什么呢?

根据我们上面所学的原理:

当我们打印到显示器中,显示器是按行刷新,碰到了\n自然会刷新到显示器中。当fork()执行完以后,父进程结束,子进程结束,用户级缓冲区没有东西可以刷新。自然而打印hello world hello fprintf hello fwrite hello write。

当我们输出重定向到test.txt中,我们刷新的方式变了,我们执行完子进程后用户级缓冲区中还有数据,当子进程执行完以后把C语言库函数调用的数据放在用户缓冲区数据刷新出来,同样又因为写时拷贝,父进程又刷了一遍。


原文地址:https://blog.csdn.net/m0_73911405/article/details/145189030

免责声明:本站文章内容转载自网络资源,如侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!