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

验证了缓冲区的存在。


相关推荐
运维行者_1 分钟前
Azure数据库监控:如何在2025年选择合适的工具
运维·服务器·网络·数据库·flask·自动化·azure
dbkx_292 分钟前
个人自用debian启动
linux·运维·debian
qq19257230271 小时前
网络协议传输层
网络·网络协议
芯盾时代1 小时前
安全大模型智驱网络和数据安全效能跃迁
网络·人工智能·安全·网络安全
dualven_in_csdn1 小时前
搞了两天的win7批处理脚本问题
java·linux·前端
晨曦backend3 小时前
Vim 匹配跳转与搜索命令完整学习笔记
linux·编辑器·vim
小镇学者3 小时前
【PHP】导入excel 报错Trying to access array offset on value of type int
android·php·excel
爬呀爬的水滴4 小时前
解决Ubuntu24.04版本,右键没有共享选项的问题
linux·服务器·ubuntu·samba·共享文件夹
IT coke4 小时前
centos7部署AWStats日志分析系统
linux·运维·centos
雾岛心情5 小时前
【黑客与安全】Linux的常用命令之系统架构信息获取系列命令
linux·运维·服务器