1、文件打开和读写的基本过程

磁盘上保存文件:文件=文件内容 + 文件属性

进程运行时,PCB内部维护文件描述符表(fd数组),每一个进程都有一个操作系统级别的文件缓冲区,每打开一个文件,就会创建一个struct file ,对应一份独立的内核缓冲区

  • read系统调用:
ssize_t read(int fd, void *buf, size_t count);

buf是用户缓冲区,属于用户空间,用户代码可以直接访问,根据fd找到struct file以及它的内核缓冲区,如果内核缓冲区没有数据:进程阻塞,操作系统驱动把磁盘数据加载到内核缓冲区;加载完成唤醒进程,read 只做拷贝:把数据从内核缓冲区拷贝到用户缓冲区 buf所以read函数本质是拷贝函数

普通磁盘文件磁盘加载速度快,阻塞感知不到

  • write系统调用:
ssize_t write(int fd, const void *buf, size_t count);

buf是用户缓冲区,write把buf中写好的数据拷贝给内核缓冲区,write本质也是拷贝函数

  • 文件修改逻辑:

磁盘不能原地修改文件,修改文件的流程是:磁盘 → 加载到内核缓冲区 → 修改内核缓冲区数据 → 操作系统重新刷新回磁盘,在文件IO中,大部分情况下,要先加载,再操作,再刷新。read/write系统调用,只负责用户空间和内核缓冲区中数据的拷贝,磁盘和内核缓冲区之间的数据搬运是由操作系统驱动自动完成的

  • 为什么要有语言级缓冲区:

当使用malloc或new开辟空间时,需要系统调用,2倍扩容机制的目的是减少系统调用的次数,所以,调用系统调用是有成本的

使用read或write时,需要系统调用,但如果用户输入多次小批量数据,那么系统调用次数就会变多,如果把这些数据先缓存在用户层,攒一批,就可以减少系统调用次数

比如fputs:把参数s指向的“hello world”字符串拷贝到FILE对象内部的输出缓冲区,此时可以还输入多个字符串,这些字符串都会存到输出缓冲区中,当满足刷新条件时,调用系统调用,把数据拷贝到内核缓冲区,最后刷新把字符串输出到屏幕

输入缓冲区和输出缓冲区这两块就是C语言中的语言级缓冲区,有了语言级缓冲区可以提高C语言的IO函数的运行效率

2、引入缓冲区

以fopen为例:

FILE *fopen(const char *path, const char *mode);

FILE的本质是结构体,FILE *fopen() 打开文件后,会在fopen内部创建结构体FILE对象

struct FILE {
	int fd;               // 文件描述符,对应内核打开的文件
	char inbuffer[];      // 输入缓冲区
	char outbuffer[];     // 输出缓冲区
	// ...其他成员
};

stdin/stdout/stderr也是FILE*,分别对应 fd=0、1、2

fclose(fp)会释放这个FILE对象,关闭 fd,同时刷新缓冲区

  • 缓冲区的三种类型:

全缓冲:缓冲区满了才刷新,普通文件一般是全缓冲

行缓冲:stdout终端,遇到\n才刷新,目标文件是显示器

无缓冲:直接刷新,没有语言级缓冲

  • 三种现象:
  1. 重定向
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>

int main()
{
    close(1);
    int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
    printf("fd is : %d\n", fd);  // stdout --> 1
    
    return 0;
}

在这里插入图片描述

此时实现输出重定向,如果加上close:

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>

int main()
{
    close(1);
    int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
    printf("fd is : %d\n", fd);  // stdout --> 1
    close(fd);
    return 0;
}

在这里插入图片描述

此时读取log.txt中的内容无输出

重定向后目标文件是log.txt,不是显示器,从行缓冲转换成全缓冲,缓冲区满了才能刷新,执行完printf后,数据还在用户缓冲区,然后执行close(fd),进程退出时刷新缓冲区,但fd已关闭,无数据,缓冲区中的字符串直接丢弃

从这里可以看出,刷新的本质是从语言缓冲区通过write(fd)系统调用把数据拷贝到文件的内核缓冲区里

  1. exit和_exit

exit:

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>

int main()
{
    printf("hello world");
    sleep(3);
    exit(0);

    return 0;
}

在这里插入图片描述

_exit:

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>

int main()
{
    printf("hello world");
    sleep(3);
    _exit(0);

    return 0;
}

在这里插入图片描述

缓冲区一定不在操作系统内,exit()会冲刷缓冲区,如果缓冲区在操作系统内,那么_exit也一定会冲刷缓冲区

在这里插入图片描述

  1. 向显示器打印字符串
#include <stdio.h>
#include <unistd.h>
#include <string.h>

int main()
{
    // C库函数
    // 向显示器打印字符串,有几种做法?
    printf("hello printf\n");
    fprintf(stdout, "hello fprintf\n");
    const char *s = "hello fputs\n";
    fputs(s, stdout);

    // 系统调用
    const char *ss = "hello write\n";
    write(1, ss, strlen(ss));

    fork();
    return 0;
}

在这里插入图片描述
重定向到log.txt文件后打印log.txt中的内容:

在这里插入图片描述

除了write,其余的都输出了两遍

重定向后,输出本应向显示器上输出,为行刷新,重定向后变成向普通文件中写,变成全缓冲,缓冲区写满才会刷新,所以父进程打印的字符串会留在缓冲区,创建子进程后就return返回,父子都会调用fclose,父子进程要各自刷新一次,所以输出两次

内核的系统调用write已经把数据写给操作系统了,已经把数据从用户内存拷贝到内核缓冲区,交给操作系统管理,不再属于这个进程的用户内存,数据不再缓冲区里缓存,所以输出一次

内核文件缓冲区细节:

  1. 只要把数据从用户(语言)缓冲区拷贝到了内核文件缓冲区,就相当于交给了硬件
  2. 客观上,就是写给了 file 对应的文件内核缓冲区啊 → OS → 自主刷新 → OS→ 磁盘,OS有自己的刷新策略,什么时候刷新,有OS自己定

如果想让内核缓冲区立即刷新,那么需要用到fsync函数

#include <unistd.h>
int fsync(int fd);

3、标准错误stderr

程序变成进程是为了完成任务,即对数据做加工处理,那么就会有数据源和处理结果,为了方便debug,把标准输入标记为0,把标准输出标记为1

test.c:

#include <stdio.h>
#include <unistd.h>
#include <string.h>

int main()
{
    // 标准输出: 1
    printf("这是一个正常消息\n");
    fprintf(stdout,"这也是一个正常的日志消息\n");
    const char *s1 = "这是一个正常消息, write\n";
    write(1, s1, strlen(s1));

    // 标准错误: 2
    fprintf(stderr, "这是一个错误消息\n");
    const char *s2 = "这是一个错误消息, write\n";
    write(2, s2, strlen(s2));
    perror("perror, hello");

    return 0;
}

进行重定向:
在这里插入图片描述

这样可以是标准错误和标准输出分离

程序在运行时会产生大量的消息,此时可以把调试信息全部写到标准错误中,正常的输出写到标准输出中此时1、2分离可以方便调试

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐