Linux从0到1——基础IO(上)【文件描述符/重定向/缓冲区】

Linux从0到1------基础IO(上)

  • [1. 预备知识](#1. 预备知识)
  • [2. 复习一下常见的C语言文件接口](#2. 复习一下常见的C语言文件接口)
  • [3. 系统调用接口](#3. 系统调用接口)
    • [3.1 函数传参小技巧------标志位](#3.1 函数传参小技巧——标志位)
    • [3.2 使用系统调用接口](#3.2 使用系统调用接口)
      • [3.2.1 open](#3.2.1 open)
      • [3.2.2 write](#3.2.2 write)
      • [3.2.3 read](#3.2.3 read)
  • [4. 文件描述符fd](#4. 文件描述符fd)
    • [4.1 fd的本质](#4.1 fd的本质)
    • [4.2 理解struct file结构体](#4.2 理解struct file结构体)
    • [4.3 fd的分配规则](#4.3 fd的分配规则)
  • [5. 重定向](#5. 重定向)
    • [5.1 引入](#5.1 引入)
    • [5.2 一般的重定向写法------配合函数dup2](#5.2 一般的重定向写法——配合函数dup2)
    • [5.3 stderr的意义](#5.3 stderr的意义)
  • [6. 缓冲区](#6. 缓冲区)
    • [6.1 预备知识](#6.1 预备知识)
    • [6.2 看一个样例](#6.2 看一个样例)
    • [6.3 用户缓冲区VS内核缓冲区](#6.3 用户缓冲区VS内核缓冲区)
    • [6.4 验证缓冲区的存在](#6.4 验证缓冲区的存在)

1. 预备知识


1. 文件 = 内容 + 属性:

  • 所有对文件的操作都可以分为两种:a. 对内容操作 b. 对属性操作;
  • 内容是数据,属性其实也是数据。存储文件,必须即存储内容又存储数据。这里指的文件默认就是在磁盘中的文件;
  • 进程要访问一个文件时,都是要先把这个文件打开的:
    • 打开前:这个文件就是普通的磁盘文件;
    • 打开后:就是将磁盘文件加载到内存。

2. 一个进程可以打开多个文件吗?多个进程可以打开多个文件吗?

  • 一个进程可以打开多个文件,多个进程可以打开多个文件。所以加载到内存中,被打开的文件可能会存在多个。
  • 既然操作系统在运行时,可能会打开多个文件,那么操作系统一定要对这些文件进行管理------先描述,再组织。
  • 我们大胆猜测一下,一个文件要被打开,一定要先在内核中形成被打开的文件对象(结构体),这些对象又可以通过一定的方式链接起来(链表)。

3. 文件按照是否被打开,分为:被打开的文件、没有被打开的文件

  • 被打开的文件,存在于内存中;
  • 没有被打开的文件,存在于磁盘中。

4. 本次研究文件操作的本质是:研究进程和被打开文件之间的关系。


2. 复习一下常见的C语言文件接口


1. fopen:


2. fputs:

3. 代码实践:

c 复制代码
#include<stdio.h>

int main()
{
    // "w": 按照写方式打开,如果文件不存在就创建它,并且每次打开都会清空文件内容
    // "r": 按照只读的方式打开,文件不存在直接报错
    // "a": 按照追加方式打开,如果文件不存在就创建它,每次打开不会清空文件内容,会在文件结尾处写入
    FILE *fp = fopen("log.txt", "w");
    if (NULL == fp)
    {
        perror("fopen");
        return 1;
    }

    const char *msg = "hello Linux file\n";
    fputs(msg, fp);     // 像文件中写入字符串

    fclose(fp);

    return 0;
}

3. 系统调用接口

  • 进程打开文件的说法是不准确的,准确的说法应该是,进程通过操作系统打开文件。所以上层的fopenfread等函数在底层一定封装了系统调用接口。

3.1 函数传参小技巧------标志位


1. 先写一段代码:

c 复制代码
#include<stdio.h>

#define Print1 1         // 0001
#define Print2 (1<<1)   // 0010
#define Print3 (1<<2)   // 0100
#define Print4 (1<<3)   // 1000

void Print(int flags)
{
    if (flags&Print1) printf("hello 1 ");
    if (flags&Print2) printf("hello 2 ");
    if (flags&Print3) printf("hello 3 ");
    if (flags&Print4) printf("hello 4 ");
    printf("\n");
}

int main()
{
    Print(Print1);
    Print(Print1|Print2);
    Print(Print1|Print2|Print3);
    Print(Print3|Print4);
    Print(Print4);

    return 0;
}

2. 编译并运行:

3. 解释:

  • 对于Print函数来说,它只有一个参数flags,这个参数是标记位;
  • flags一共有32个比特位,这里我们只使用四个比特位,也就是只有四个选项;
  • 定义了四个宏Print*,也是四个选项,他们对应的二进制信息已在代码中写出,通过|的方式,将这些选项组合起来,传给Print函数;
  • Print函数内部,通过if (flags&Print*)的方式,可以判断对应的选项是否传入(对应比特位是否是1),如果传入了,就执行该条if后的代码;
  • 上面说的选项,也叫标志位。flags就是各种标记位的组合。

3.2 使用系统调用接口


3.2.1 open


1. 查看man手册:

  • flags参数就是各种标志位的组合;
  • pathname就是文件路径;
  • 返回值是int类型的数据,是打开文件的文件描述符fd,关于这个文件描述符,我们后面再详细讲,这里只需要知道,文件描述符的使用方式和C语言接口中的FILE*文件指针一样即可;
  • open失败时,会返回-1,同时错误码被设置;
  • 可以看到open接口有两个,第二个还有一个参数mode,需要我们以8进制形式传入权限。

2. flags对应选项:

  • O_WRONLY:以只写方式打开;
  • O_CREAT:以只写方式打开时,如果文件不存在,就创建它;
  • O_TRUNC:以只写方式打开时,清空文件内容;
  • O_APPEND:以只写方式打开时,不清空文件,在文件末尾追加内容。

3. 代码实例:

c 复制代码
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>

int main()
{
    // 打开已经存在的文件,不需要带权限
    // 打开不存在的文件,需要带权限;如果打开不存在的文件,不带权限,那么这个新文件的权限是乱码
    // 权限以8进制方案传入
    int fd = open("log.txt", O_WRONLY | O_CREAT, 0666);
    if (fd < 0)
    {
        perror("open");
        return 1;
    }

    // close也是一个系统调用,用于关闭文件,头文件是unistd.h
    // 参数是fd
    close(fd);  

    return 0;
}
  • 编译并运行:
  • 问题:为什么我们设置的权限是666,可是创建的log.txt权限却是664?

4. 关于权限:

  • 如果我们打开一个不存在的文件,还不传权限,那么它的文件描述符是乱码。
c 复制代码
#include<stdio.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);	// 不带权限
    if (fd < 0)
    {
        perror("open");
        return 1;
    }

    close(fd);  

    return 0;
}
  • 关于3中的问题,答案是有系统默认的权限掩码存在,默认是0002,将对应的权限过滤掉了。我们可以通过umask函数来重新设置权限掩码:
c 复制代码
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>

int main()
{
    umask(0);   // 重新设置权限掩码
    int fd = open("log.txt", O_WRONLY | O_CREAT, 0666);
    if (fd < 0)
    {
        perror("open");
        return 1;
    }

    // close也是一个系统调用,用于关闭文件,头文件是unistd.h
    // 参数是fd
    close(fd);  

    return 0;
}
  • 注意:不建议使用上面这种方式重新设置权限掩码,尽量和系统默认权限掩码保持一致。

5. close接口:

  • 关闭哪个文件,就传对应文件的文件描述符即可。

3.2.2 write


1. 查看man手册:

2. 代码实例:

c 复制代码
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<string.h>

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

    const char *msg = "hello file system call\n";

    // 操作文件
    write(fd, msg, strlen(msg)); // 要不要传 strlen(msg) + 1,将'\0'也传进去?
    // 第三个参数不要传 strlen(msg) + 1,'\0'只是C语言层面的概念,不是文件层面的概念
    // '\0'传进文件,会出现乱码

    close(fd);  

    return 0;
}
  • O_WRONLY | O_CREAT的方式打开文件,write默认是覆盖式写入。比如文件中原本有内容aaaa,如果再向文件中写入bb,文件内容就会变为bbaa

3. fopen以w方式打开文件的底层:

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

    close(fd);  

    return 0;
}

4. fopen以a方式打开文件的底层:

c 复制代码
int main()
{
    int fd = open("log.txt", O_WRONLY | O_CREAT | O_APPEND, 0666);
    if (fd < 0)
    {
        perror("open");
        return 1;
    }

    close(fd);  

    return 0;
}

3.2.3 read


1. 查看man手册:

  • fd:要读取文件的文件描述符;
  • buf:用户自定义的一块空间(缓冲区);
  • count:缓冲区总大小;

2. 代码实例:

c 复制代码
int main()
{
    int fd = open("log.txt", O_RDONLY);
    if (fd < 0)
    {
        perror("open");
        return 1;
    }

    char buffer[1024];
    read(fd, buffer, 1024);
    printf("%s\n", buffer);
    close(fd);

    return 0;
}

4. 文件描述符fd


4.1 fd的本质


1. 文件描述符fd的本质,就是数组下标:

  • 操作系统会为打开的文件创建一个结构体struct file来描述它,然后通过链表的方式将多个打开的文件组织起来;
  • 进程PCB中会有一个struct files_struct *files指针,指向该进程管理打开文件的结构体struct files_struct。其中有一个成员为struct file *fd_array[]指针数组,每一个位置对应一个打开文件的结构体对象struct file
  • 文件描述符fd,其实就是struct file *fd_array[]数组的下标,所以只要拿到对应的文件描述符(数组下标),就可以找到对应的文件;
  • C/C++程序在运行时,会默认打开三个文件,标准输入流stdin,标准输出流stdout,和标准错误流stderr。这三个文件分别对应的硬件设备为,键盘、显示器、显示器。文件描述符数组的0, 1, 2位置,分别对应这几个文件。

2. FILE*到底是什么?

  • stdinstdoutstderr的都是FILE*类型的指针。
  • FILE其实就是C语言中提供的一个结构体类型,我们可以大胆猜测一下,FILE的内部必定封装了文件描述符。

关于FILE现在没有办法讲太多。

3. 代码验证:

c 复制代码
int main()
{
    int fda = open("loga.txt", O_WRONLY | O_CREAT | O_APPEND, 0666);
    int fdb = open("logb.txt", O_WRONLY | O_CREAT | O_APPEND, 0666);
    int fdc = open("logc.txt", O_WRONLY | O_CREAT | O_APPEND, 0666);
    int fdd = open("logd.txt", O_WRONLY | O_CREAT | O_APPEND, 0666);

    printf("stdin->fd: %d\n", stdin->_fileno);	// 这个_fileno成员就是文件描述符
    printf("stdout->fd: %d\n", stdout->_fileno);
    printf("stderr->fd:%d \n", stderr->_fileno);
    printf("fda: %d\n", fda);
    printf("fdb: %d\n", fdb);
    printf("fdc: %d\n", fdc);
    printf("fdd: %d\n", fdd);

    close(fda);
    close(fdb);
    close(fdc);
    close(fdd);

    return 0;
}
  • 可以发现,文件描述符的分配是有一定规律的。

4. 如何理解一切皆文件?

  • 底层的很多硬件,大多都有两个基本的功能,输入和输出。但是它们的输入和输出方法是不同的;
  • 但是在上层,我们想通过一切皆文件的方式去管理底层不同的硬件,是如何做到的?
  • 比如此时打开一个磁盘文件,OS在上层就会为磁盘文件创建一个struct file对象。里面有两个很重要的内容就是读方法和写方法的指针,指向磁盘文件具体的读写方法;
  • 从此往后,我们再调用磁盘的读写方法时,不用关心底层是如何实现的,直接调用struct file对象中的方法即可,一切皆文件;
  • 这种封装的方式,可以让我们自然联想到C++中的继承和多态。

4.2 理解struct file结构体


struct file结构体中,必定要存储两个信息:a. 文件的属性 b. 文件的内容。

对文件的操作无非就分为两种,一种是读,一种是写。但是无论读写,都需要先将磁盘中的文件数据加载到文件缓冲区中。

我们在应用层进行的数据读写,本质是将内核缓冲区中的数据进行来回拷贝。


4.3 fd的分配规则


1. 进程默认已经打开了fd为0,1,2的三个文件,我们可以通过0,1,2直接访问:

  • 0,2可以直接使用,从侧面验证了上述结论。
c 复制代码
int main()
{
    char buffer[1024];	// 用户自己定义的缓冲区
    ssize_t s = read(0, buffer, 1024); // 从键盘读取
    if (s > 0)
    {
        write(1, buffer, strlen(buffer));	// 向显示器写入
    }
    return 0;
}

2. 文件描述符的分配规则是:从上往下扫描struct file *fd_array[]数组,寻找最小的,没有被使用的位置对应的下标,分配给打开的文件。

  • 关闭0或2,再打开文件,查看分配给新打开文件的fd(先不要关闭1):
c 复制代码
int main()
{
    close(0);
    //close(2);
    int fd = open("log.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);
    if (fd < 0)
    {
        perror("open");
        return 1;
    }
    
    printf("fd: %d\n", fd);
    
    close(fd);

    return 0;
}
  • 发现显示器输出结果是: fd: 0 或者 fd: 2
  • 可见,文件描述符的分配规则:在struct file *fd_array[]数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符。

如果关闭1号文件(显示器),则无法在显示器中看到输出结果。


5. 重定向


5.1 引入


1. 先看代码,观察现象:

c 复制代码
int main()
{
    close(1);	// 先关1
    int fd = open("log.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);
    if (fd < 0)
    {
        perror("open");
        return 1;
    }
   
    printf("fd: %d\n", fd);
    printf("stdout->fd: %d\n", stdout->_fileno);

    // C语言提供的缓冲区问题,先待定!
    fflush(stdout); // 在close前,刷新缓冲区

    close(fd);

    return 0;
}
  • printf明明是向显示器打印的,怎么打印到了文件log.txt里?
  • 这种现象叫输出重定向>,常见的重定向有:
    • 输出重定向:>
    • 追加重定向: >>
    • 输入重定向:<

2. 解释:

  • 首先,根据fd的分配规则,关闭1后,再打开新文件log.txt,新文件的fd就是1。文件描述符数组的1号位置,不再指向显示器,而是文件log.txt
  • printf只认文件描述符1,默认向struct file *fd_array[]数组下标为1的位置所指向的文件打印,所以本该打印到显示器上的内容,打印到了文件log.txt中;
  • 一定要在close前刷新缓冲区,因为printf会先将数据放到C语言提供的缓冲区中,刷新缓冲区,才能让缓冲区中的数据换入到文件中;

这里只是粗力度的解释一下为什么要刷新缓冲区,关于缓冲区更多的细节,我们在后面讲解。

3. 输入重定向(一般不这样写):

c 复制代码
int main()
{
    close(0);   
    int fd = open("log.txt", O_RDONLY); // fd == 0
    if (fd < 0)
    {
        perror("open");
        return 1;
    }

    char buffer[1024];	// 用户自己定义的缓冲区,先将数据读到buffer
    fread(buffer, 1, sizeof(buffer), stdin);    // stdin->fd: 0
    printf("%s\n", buffer);
    close(fd);
    
    return 0;
}

4. 追加重定向:

  • 只需要将1中代码中,openO_TRUNC选项改为O_APPEND即可。
  • 不过一般也不这样写。
c 复制代码
int main()
{
    close(1);	// 先关1
    int fd = open("log.txt", O_CREAT | O_WRONLY | O_APPEND, 0666);
    if (fd < 0)
    {
        perror("open");
        return 1;
    }
   
    printf("fd: %d\n", fd);
    printf("stdout->fd: %d\n", stdout->_fileno);

    // C语言提供的缓冲区问题,先待定!
    fflush(stdout); // 在close前,刷新缓冲区

    close(fd);

    return 0;
}

5. 重定向的本质:

  • 上层fd不变,底层fd所指向的内容在改变。

5.2 一般的重定向写法------配合函数dup2


1. dup2函数:

  • oldfd下标指向的内容拷贝给newfd下标指向的内容。

2. 输出重定向:

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

    dup2(fd, 1);

	// 如下内容将打印到文件 log.txt 中
    printf("hello printf\n");
    fprintf(stdout, "hello fprintf\n");
    close(fd);

    return 0;
}

3. 追加重定向:

c 复制代码
int main()
{
    int fd = open("log.txt", O_WRONLY | O_CREAT | O_APPEND, 0666);
    if (fd < 0)
    {
        perror("open");
        return 1;
    }

    dup2(fd, 1);

	// 如下内容将追加到文件 log.txt 中
    printf("hello printf\n");
    fprintf(stdout, "hello fprintf\n");
    close(fd);

    return 0;
}

4. 输入重定向:

c 复制代码
int main()
{
    int fd = open("log.txt", O_RDONLY);
    if (fd < 0)
    {
        perror("open");
        return 1;
    }
    dup2(fd, 0);

    char buffer[1024];
    fread(buffer, 1, 1024, stdin);

    printf("%s\n", buffer);
    close(fd);

    return 0;
}

5. 引用计数f_count:

  • 通过重定向的学习,我们知道了,一个打开的文件,可以被多个struct file*指针指向。如图,log.txt就被两个struct file*指针指向。
  • 我们在调用close接口关闭一个文件时,如果这个文件还被其他的指针指向,该怎么办,这样不是会互相影响吗?
  • 所以struct file结构体内还设计了一个f_count字段,它是引用计数,记录有多少个指针指向自己。如果f_count不为0,则执行一次closef_count就减一。直到f_count等于0时,才释放log.txt的资源。

5.3 stderr的意义


1. 看代码,观察现象:

c 复制代码
int main()
{
    fprintf(stdout, "hello stdout\n");
    fprintf(stderr, "hello stderr\n");
    return 0;
}
  • 这个问题现在很好解释,因为重定向只是将1位置的指针改成指向log.txt了,但是stderr对应的2位置的指针,还是指向显示器文件,所以第二条fprintf语句还是向显示器打印了。
  • 想让这两条fprintf语句都往log.txt打印,需要将2位置也重定向了。在命令行中可以直接执行指令./myfile > log.txt 2>&1,其中2>&1表示让1位置的指针覆盖2位置的指针,这样1位置和2位置都指向log.txt了。
  • 所以./myfile > log.txt的完整写法应该是./myfile 1>log.txt

2. stderr的实际运用:

  • 比如在一个日志系统中,我们希望将正常信息和错误信息进行分流,将他们放在不同的文件中。这时候就可以使用输出重定向,将1位置和2位置重定向到不同的文件,将错误信息单独储存起来。

6. 缓冲区


6.1 预备知识


1. 我们理解的缓冲区:

  • 缓冲区其实就是一部分内存,重要的是搞清楚这一部分内存由谁提供。
    • 用户缓冲区:用户自己提供,用户在程序中自己定义的缓冲区,如char buffer[1024]
    • C语言缓冲区:由C语言提供的,定义在C库中;
    • 内核缓冲区:由操作系统提供的,内核级别的缓冲区。

2. 缓冲区存在的意义:

  • 任何缓冲区存在的目的只有一个,就是提高效率。
  • 举一个生活中的例子:
    • 假如你住在云南,你要给你远在北京的朋友送一个键盘。在快递还没有出现的时候,你需要先自己坐火车跑到北京,把键盘送到朋友手中,然后自己再跑回来,一来一回花了一个月时间,效率低下。
    • 后来快递出现了,你可以先把键盘给楼下的菜鸟驿站,然后由菜鸟驿站完成送键盘的任务,等键盘到了北京,你的朋友再去自己楼下的菜鸟驿站把键盘拿到手。
    • 整个过程中,键盘从云南到送到北京这个时间成本是不可避免的,这个时间成本由菜鸟驿站承担了。但是你就轻松了很多,在你把键盘送到菜鸟驿站的那一刻,你就可以认为你把键盘送出去了,然后你就可以干自己的事情了。
    • 所以菜鸟驿站的存在,节省了使用者的时间。
    • 菜鸟驿站就像缓冲区 ,进程先将数据放入缓冲区,然后由缓冲区执行向特定位置传输数据的操作,解放进程。所以,缓冲区的主要作用是------提高使用者的效率

3. 缓冲区的刷新策略:

  • 菜鸟驿站在送快递时,肯定也有自己的配送方式。

    • 比如某一个客户要求紧急配送,驿站就派专机专门送这个快递;
    • 不紧急的快递,派专机送成本太高了,会积累到一定的量后,统一配送。
  • 缓冲区的刷新也有自己的策略:

    • 无缓冲(立即刷新);
    • 行缓冲(行刷新);
    • 全缓冲(缓冲区满了,再刷新)。
  • 上面说的都是缓冲区刷新的一般策略,除此之外还有一些特殊情况:

    • 因为某些场景需要,需要强制刷新缓冲区;
    • 进程退出时,一定要进行缓冲区刷新。

4. 磁盘和显示器的刷新策略:

  • 一般对于显示器文件,会进行行刷新;
  • 对于磁盘文件,采取全缓冲策略。

6.2 看一个样例


1. 观察现象:

c 复制代码
int main()
{
    fprintf(stdout, "C: hello fprintf\n");
    printf("C: hello printf\n");
    fputs("C: hello fputs\n", stdout);

    const char *str = "system call: hello write\n";
    write(1, str, strlen(str));

    fork();

    return 0;
}
  • 向显示器打印:
  • 重定向,向log.txt文件中打印:

2. 理解样例:

  • 当我们直接向显示器打印时,显示器的刷新方式是行刷新,并且我们写的所有打印语句后都有\n\n是一种行刷新策略)。在fork函数执行之前,缓冲区中的数据已经全部刷新,缓冲区为空。(包括系统调用接口write,系统内核级别的缓冲区也为空,这个后面说)
  • 重定向到log.txt文件的本质,是向磁盘文件写入,系统对数据的刷新方式就变成了全缓冲。
  • 全缓冲,意味着实际写入的简单数据,不足以把缓冲区写满,无法达到刷新条件。fork函数执行的时候,数据依旧在缓冲区中。
  • 由于write对应的打印内容正常打印了,所以我们可以得出一个结论:我们目前所谈的"缓冲区"和操作系统没有关系,只和C语言本身有关。
  • C/C++提供的缓冲区,里面保存的一定是用户的数据,属于当前进程在运行时自己的数据。当进程将数据交给操作系统后,这个数据就不属于当前进程了,而是属于操作系统。
  • 当进程退出时,一般要强制刷新缓冲区。缓冲区的刷新,本质上也是一种对当前进程一个变量的清空或"写入"操作。
  • fork后,任意一个进程退出的时候,会强制刷新缓冲区(修改变量),此时就会发生写时拷贝,所以我们看到缓冲区中打印的内容多出一份。
  • write是系统调用,没有使用C语言的缓冲区,它会将数据直接写入操作系统。所以这部分数据不属于进程,也就不会发生写时拷贝。

6.3 用户缓冲区VS内核缓冲区


1. 用户缓冲区:

  • 我们日常接触的最多的是C/C++提供的语言级别的缓冲区;
  • 我们通常说的刷新,指的是将C语言缓冲区中的数据刷新到操作系统中。

2. 内核缓冲区:

  • C语言缓冲区刷新到操作系统中后,实际上是传给了内核缓冲区,内核缓冲区最后还要将数据刷新到磁盘文件上;
  • 不同的操作系统,内核缓冲区的刷新策略不同。

3. 解释printf("hello printf\n");这段代码从执行,到在显示器打印的全过程:

  • 首先,hello printf\n这段数据会先通过printf函数写入到C语言缓冲区中,然后printf函数返回,至此它的任务就完成了;
  • 接着,C语言缓冲区中的数据会根据一定的刷新策略,通过write接口,写入操作系统;
  • 写入操作系统,实际上是先写入了stdout对应的内核缓冲区,然后操作系统再根据自己的刷新策略,将内核缓冲区中的数据刷新到磁盘中。

6.4 验证缓冲区的存在


任何情况下,我们调用C语言文件接口的时候,都要有一个FILE*指针。我们都知道FILE结构体中封装了文件描述符fd,其实FILE结构体中也封装了缓冲区。

在在/usr/include/stdio.h中,有:

c 复制代码
typedef struct _IO_FILE FILE;

/usr/include/libio.h中,有:

c 复制代码
struct _IO_FILE {
 int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags
 
 //缓冲区相关
 /* The following pointers correspond to the C++ streambuf protocol. */
 /* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
 char* _IO_read_ptr; /* Current read pointer */
 char* _IO_read_end; /* End of get area. */
 char* _IO_read_base; /* Start of putback+get area. */
 char* _IO_write_base; /* Start of put area. */
 char* _IO_write_ptr; /* Current put pointer. */
 char* _IO_write_end; /* End of put area. */
 char* _IO_buf_base; /* Start of reserve area. */
 char* _IO_buf_end; /* End of reserve area. */
 /* The following fields are used to support backing up and undo. */
 char *_IO_save_base; /* Pointer to start of non-current get area. */
 char *_IO_backup_base; /* Pointer to first valid character of backup area */
 char *_IO_save_end; /* Pointer to end of non-current get area. */
 
 struct _IO_marker *_markers;
 
 struct _IO_FILE *_chain;
 
 int _fileno; //封装的文件描述符
#if 0
 int _blksize;
#else
 int _flags2;
#endif
 _IO_off_t _old_offset; /* This used to be _offset but it's too small. */
 
#define __HAVE_COLUMN /* temporary */
 /* 1+column number of pbase(); 0 is unknown. */
 unsigned short _cur_column;
 signed char _vtable_offset;
 char _shortbuf[1];
 
 /* char* _save_gptr; char* _save_egptr; */
 
 _IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};

验证了缓冲区的存在。


相关推荐
Hello.Reader3 分钟前
使用 Nmap 进行 SSL/TLS 加密套件枚举
网络·网络协议·ssl
geekrabbit24 分钟前
Ubuntu 22.04上安装Python 3.10.x
linux·python·ubuntu
miao_zz1 小时前
基于react native的锚点
android·react native·react.js
安卓美女1 小时前
Android自定义View性能优化
android·性能优化
ID_14 HuFei1 小时前
sed编辑器与awk的用法
linux·编辑器
驯龙高手_追风2 小时前
Ubuntu下安装最新版本Apache2文件服务器
linux·服务器·ubuntu
炸裂狸花猫2 小时前
Linux技术04-IPVS
linux·运维·服务器
快快小毛毛2 小时前
CC攻击防御策略要怎么调整?使用游戏盾有效解决
运维·服务器·网络·tcp/ip·游戏·udp
lys_132 小时前
wireshark打开时空白|没有接口,卸载重装可以解决
网络·测试工具·wireshark
J老熊2 小时前
Linux下抓包分析Java应用程序HTTP接口调用:基于tcpdump与Wireshark的综合示例
java·linux·运维·web安全·http·面试