Linux-基础IO

🌎Linux基础IO


文章目录:

Linux基础IO

    C语言中IO交互
      常用C接口
        fopen
        fputs
        fwrite
        fgets

      当前路径
      三个文件流

    系统文件IO
      open函数
      参数含义
      close函数

      write函数
        参数含义

    文件描述符fd
      认识文件描述符

      重定向
        输出重定向
        输入重定向
        追加重定向
        重定向接口

    缓冲区
      简单认识缓冲区
      技术角度认识缓冲区
      FILE结构体
      编码模拟

    总结


前言:

  在刚开始学习Linux的时候,我们记住了Linux下一切皆文件,我们通过这篇文章来深入了解Linux下文件的构成及应用。

在这里插入图片描述


🚀C语言中IO交互

✈️ 常用C接口
🚩 fopen

fopen:打开一个文件。

代码示例:

#include<stdio.h>

int main()
{
    FILE* fp = fopen("./log.txt", "w");//打开一个文件,如果没有则创建一个文件
    if(fp == NULL)
    {
        perror("fopen");
        return 1;
    }

	//文件操作介于打开和关闭之间

    fclose(fp);//关闭文件
    return 0;
}

在这里插入图片描述

注意

  当以 ‘w’ 方式打开文件时:该文件会被清空。

  当以 ‘a’ 方式打开文件时:正常打开该文件,如果有写入操作则是追加写入。

  当以 ‘r’ 方式打开文件时:仅读取文件。


🚩 fputs

fputs:向文件流中写入一个字符串

代码示例:

#include<stdio.h>

int main()
{
    FILE* fp = fopen("./log.txt", "w");
    if(fp == NULL)
    {
        perror("fopen");
        return 1;
    }

    const char* str = "this is file operate\n";
    fputs(str, fp);

    fclose(fp);
    return 0;
}

在这里插入图片描述


🚩 fwrite

fwrite:向二进制文件写入数据。

代码示例:

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

#define FILENAME "log.txt"

int main()
{
    FILE* fp = fopen("./log.txt", "w");
    if(fp == NULL)
    {
        perror("fopen");
        return 1;
    }

    const char* msg = "this is file operate\n";
    int cnt = 5;
    while(cnt)
    {
        fwrite(msg, strlen(msg), 1, fp);
        printf("write %d block\n", n);
        cnt--;

    }

    fclose(fp);
    return 0;
}

在这里插入图片描述

第一个参数:

写入数据的对象。

第二个参数:

基本单位的大小。

第三个参数:

表示写入多少个基本单位。

第四个参数:

表示文件流

返回值:

表示写入的基本单位的个数,也就是第三个参数

在这里插入图片描述


🚩 fgets

fgets:读取一个字符串。

代码示例:

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

int main()
{
    FILE* fp = fopen("./log.txt", "r");//r方式打开
    if(fp == NULL)
    {
        perror("fopen");
        return 1;
    }

    char buffer[64];
    while(1)
    {
        char* r = fgets(buffer, sizeof(buffer), fp);
        if(!r) break;

        printf("%s\n", buffer);
    }

    fclose(fp);
    return 0;
}

在这里插入图片描述

  这里我只列举了部分常用C语言IO接口,如果有遗忘,请自行复习。


✈️当前路径

  当我们在程序中创建一个文件时,例如使用 fopen函数以 ‘w’ 方式打开文件,文件不存在时则创建文件,但是为什么文件创建位置是在当前路径下呢?

在这里插入图片描述
  其实是通过该进程的一项属性数据来判断所处路径的,我们可以查询该进程pid,在proc目录下进行查看该进程:

在这里插入图片描述

  cwd表示该进程当前所处工作目录,exe表示可执行程序所处路径。

注意: 当前路径不是指可执行程序所处路径,而是指该程序运行为进程时所处路径。


✈️三个文件流

  刚开始接触Linux的时候,我们都知道有句话叫做:Linux下一切皆文件,那么键盘、显示器、网卡、声卡等等这些对于Linux来说都是文件!

  我们使用Linux都知道,想要对一个文件进行操作,我们必须要打开一个文件,这是必须的。但是为什么 显示器文件键盘文件 这些文件我们并不需要直接打开就可以直接使用呢?

文件在打开的前提一定是基于进程的,而进程在运行的过程中会打开默认的三个流,即标准输入流,标准输出流、标准错误流。而对应C语言中就是 stdin、stdout、stderr

  标准输入流对应的设备是键盘、标准输出与标准错误流对应的设备是显示器。

在这里插入图片描述
  当我们使用C语言运行一个程序的时候,操作系统会默认将这三个流给打开,于是,我们使用printf、scanf、gets、puts等接口时可以直接使用。

  也就是说我们的输入输出是因为stdin和stdout流是默认打开的状态,我们可以根据stdin、stdout来直接对屏幕进行输出:

#include<stdio.h>

int main()
{
    fprintf(stdout, "you can see me\n");//对标准输出流进行写入
    fprintf(stdout, "yes I'can\n");//对标准输出流进行写入
    return 0;
}

在这里插入图片描述
  对标准输出流进行写入,其实就是将数据打印到显示器上!

注意:并不是只有C语言有此特性,其他语言例如C++的cout、cin也具有标准流。这种特性并不是有语言层面提供的,而是由操作系统提供的。


🚀系统文件IO

  除了使用C语言或者其他语言的IO交互,我们也可以采用调用系统接口来进行文件访问,而系统调用时更接近于底层的,其他语言都是对系统的系统调用进行封装的。

✈️open函数

open函数是fopen函数的底层,其为Linux的系统调用,函数原型为:

int open(const char *pathname, int flags, mode_t mode);
参数含义
  • pathname:表示 需要传入的文件路径,当只有文件名的时候,表示子在当前目录打开或创建该文件。

  • flags:表示打开文件的方式。通常打开文件的常用方式分为以下几种:

flags选项含义
O_RDONLY以只读的方式打开文件
O_WRONLY以只写的方式打开文件
O_APPEND以追加的方式打开文件
O_CREAT文件不存在时,则创建文件
O_RDWR以读写的方式打开文件
O_TRUNC清空文件
  • mode:表示创建文件的默认方式。不需要创建文件时,这个参数不必传参。

  为了能理解第二个参数flags ,我们通过以下代码来观察:

#include<stdio.h>

#define O_LISTEN 1// 0001
#define O_TALK 2 // 0010
#define O_READ 4  // 0100
#define O_WRITE 8 // 1000

void Listen()
{
    printf("linten English dialog\n");
}

void Talk()
{
    printf("talk about English\n");
}

void Read()
{
    printf("read English newspaper\n");
}

void Write()
{
    printf("write English article\n");
}

void operate(int flags)
{
	//根据二进制位来判断调用函数接口类型
    if(flags & O_LISTEN)
        Listen();
    if(flags & O_TALK)
        Talk();
    if(flags & O_READ)
        Read();
    if(flags & O_WRITE)
        Write();
}

int main()
{
    operate(O_LISTEN);
    printf("\n");
    operate(O_TALK | O_READ);//按位或运算调用
    printf("\n");
    operate(O_LISTEN | O_TALK | O_READ | O_WRITE);
    return 0;
}

在这里插入图片描述

  我们可以 使用或运算 来做出 不同的行为,同样,open接口的flags参数也是如此使用方式,例如,我们以 使用open模拟fopen函数的 ‘w’ 行为

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

int main()
{
    int fd = open("log.txt", O_WRONLY|O_CREAT, 0666);//文件默认权限设置为666
    if(fd == -1)
    {
        perror("open");
    }
    return 0;
}

在这里插入图片描述

  我们确实模仿出了fopen函数的功能,仔细看文件权限,与我们想要的并不同,最后三项应该是 rw- 才对,这是因为存在叫做 权限掩码(umask) 的东西,其通常默认为0002,与mode的关系是 umask & mode,所以我们在设置权限之前,需要把umask设置为0:

在这里插入图片描述

#include<stdio.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);//文件默认权限设置为666
    if(fd == -1)
    {
        perror("open");
    }
    return 0;
}

在这里插入图片描述

✈️close函数
int close(int fd);

  close函数属于Linux下的系统调用,其功能是 关闭一个文件描述符,参数是 有待关闭的文件描述符。

在这里插入图片描述

✈️write函数

函数定义

ssize_t write(int fd, const void* buf, size_t count);
🚩 参数含义
  • fd:需要传入的文件描述符。
  • buf:需要写入的字符串的起始位置。
  • count:需要写入字符串的长度。

  其中第三个参数需要注意,传入的字符串长度是不算 \0 的,因为这是系统调用接口,并非C语言。

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

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

    const char* str = "hello sys call\n";
    write(fd, str, strlen(str));//长度不算 \0

    close(fd);
    return 0;
}

在这里插入图片描述

  但是如果我们写入的字符串改变了并且没有 \n:

const char* str = "aaaa";
write(fd, str, strlen(str));

在这里插入图片描述
  如果这样,那么下次进行写入就是以 覆盖的方式进行写入。所以我们在打开文件的时候需要将open函数的选项增加一个 O_TRUNC 选项:

int fd = open("log.txt", O_WRONLY|O_CREAT|O_TRUNC, 0666);

在这里插入图片描述

  如果需要实现什么功能,就需要提供对应的选项。


🚀文件描述符fd

  文件描述符在上文中不止出现了一次,包括 open 函数的返回值,close 函数的参数等等,从其出现的频率来看,似乎是很重要的一个东西。

✈️认识文件描述符

  既然open 函数返回值是文件描述符,那么我们可以创建多个open函数,使用多个返回值接收并且打印来观察现象:

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

int main()
{
    int fd1 = open("log1.txt", O_WRONLY|O_CREAT|O_TRUNC, 0666);
    int fd2 = open("log2.txt", O_WRONLY|O_CREAT|O_TRUNC, 0666);
    int fd3 = open("log3.txt", O_WRONLY|O_CREAT|O_TRUNC, 0666);
    int fd4 = open("log4.txt", O_WRONLY|O_CREAT|O_TRUNC, 0666);
    int fd5 = open("log5.txt", O_WRONLY|O_CREAT|O_TRUNC, 0666);

    printf("fd1:%d\n", fd1);
    printf("fd2:%d\n", fd2);
    printf("fd3:%d\n", fd3);
    printf("fd4:%d\n", fd4);
    printf("fd5:%d\n", fd5);
    return 0;
}

在这里插入图片描述

  我们观察到的现象是,文件描述符是从3开始的,那么012去哪里了?并且为什么它们是连续的??

  其实0、1、2文件描述符已经被使用了!其分别是:标准输入、标准输出、标准错误!而它们是连续的,其实也就是 数组下标

  而我们在上文中也提到过三个标准流,即:

在这里插入图片描述

  他们的类型都是 FILE* 类型,其实 FILE 是C标准库自己封装的一个结构体。而这三个流分别是文件描述符的前三个,那么 FILE 结构体内必定 封装特定的fd!

  我们经常说,Linux下一切皆文件,那么一个空文件,它的大小真的是0吗,其实在很久以前我们也探讨过,只要文件被创建,那么就不可能为0。

  文件 = 内容 + 属性

  那么每个文件必然具有一些相同的初始属性,比如文件标志位,文件权限位,文件对下一个文件的指针,缓冲区等等。这些属性很杂乱,所以操作系统需要对其进行管理,那么还是那六个字:先描述,再组织

  将这些属性组织到结构体当中,便更有利于操作系统的管理:

在这里插入图片描述

  在task_struct 中存在一个 files 指针,该指针指向一个 files_struct 的结构体,在该结构体当中存在一个 fd_array 的指针数组,而 数组的下标就对应我们所谓的文件描述符

在这里插入图片描述

  因为0、1、2这三个文件描述符时默认打开的,但是这里我把它关闭(仅关闭0位置),再使用 open 创建一个文件,会发生什么?

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

int main()
{
    close(0);
    int fd1 = open("log1.txt", O_WRONLY|O_CREAT|O_TRUNC, 0666);
    int fd2 = open("log2.txt", O_WRONLY|O_CREAT|O_TRUNC, 0666);
    int fd3 = open("log3.txt", O_WRONLY|O_CREAT|O_TRUNC, 0666);
    int fd4 = open("log4.txt", O_WRONLY|O_CREAT|O_TRUNC, 0666);
    int fd5 = open("log5.txt", O_WRONLY|O_CREAT|O_TRUNC, 0666);

    printf("fd1:%d\n", fd1);
    printf("fd2:%d\n", fd2);
    printf("fd3:%d\n", fd3);
    printf("fd4:%d\n", fd4);
    printf("fd5:%d\n", fd5);
    return 0;
}

在这里插入图片描述
  0位置的fd被我们关闭了,但当我们在创建文件的时候,0号位置被新创建的文件占用了。如果我再关闭2号文件描述符呢?

close(0);
close(2);

在这里插入图片描述

  看来我们 关闭 一个默认打开的文件描述符,那么新建文件就会:按照顺序占用 被关闭(未被使用) 的文件描述符。


✈️重定向

  了解了什么是文件描述符之后,我们就可以根据文件描述符的规则来实现不同的重定向功能。

  我们在最开始学习Linux指令的时候使用过重定向功能,而重定向无外乎 输入重定向输出重定向

在这里插入图片描述

  重定向的原理是,将原本需要输入或者输出的对象文件变为指定的对象文件

  比如,我们知道Linux下一些皆文件,那么键盘、显示器都是文件,而我们平常的打印,其实就是对 “显示器文件” 上进行写入,而重定向就是将原本向 “显示器文件” 写入更改为向其他文件写入。

🚩 输出重定向

  而更改重定向文件其实是就是更改文件描述符指向的文件:

在这里插入图片描述

我们使用C语言来模拟一下情况:

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

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

    printf("Hello Linux xxxxxxxxxxxx\n");
    printf("Hello Linux xxxxxxxxxxxx\n");
    printf("Hello Linux xxxxxxxxxxxx\n");
    printf("Hello Linux xxxxxxxxxxxx\n");

    fflush(stdout);

    close(fd);
    return 0;
}

在这里插入图片描述


🚩 输入重定向

  同样,输入重定向也是先关闭默认打开的0号文件描述符,使得新创建的分配到0号文件描述符,这样进行输入的时候就重定向到该文件内:

在这里插入图片描述

C语言模拟:

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

int main()
{
    close(0);
    umask(0);
    int fd = open("log1.txt", O_RDONLY|O_CREAT, 0666);//这里为只读
    if(fd < 0)
    {
        perror("open");
        return 1;
    }

    char buff[64];
    while(scanf("%s", buff) == EOF)
    {
		printf("%s\n", buff);
	}

    close(fd);
    return 0;
}

在这里插入图片描述


🚩 追加重定向

  追加重定向,与输出重定向不同的是,输出重定向每次向文件内输入时都会清空文件内容再做输入,而追加重定向是追加写入文件内,不修改原来文件的文本。

  其实实现起来也很简单,将open 函数的flags参数添加上 O_APPEND 即可:

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

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

    printf("Hello Linux xxxxxxxxxxxx\n");
    printf("Hello Linux xxxxxxxxxxxx\n");
    printf("Hello Linux xxxxxxxxxxxx\n");
    printf("Hello Linux xxxxxxxxxxxx\n");

    fflush(stdout);

    close(fd);
    return 0;
}

在这里插入图片描述


🚩 重定向接口

  我们整个重定向需要搞那么麻烦吗?万一在代码段当中添加了其他需求到最后自己是否还能认得这段代码?为了方便,Linux给我们提供了一个接口,dup2

在这里插入图片描述

直接一段代码来看用法:

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

int main()
{
    umask(0);
    int fd = open("log.txt", O_WRONLY|O_CREAT|O_TRUNC, 0666);
    dup2(fd, 1);//将fd重定向到1

    printf("Can you see me??\n");
    printf("Can you see me??\n");
    printf("Can you see me??\n");
    printf("Can you see me??\n");

    close(fd);
    return 0;
}

在这里插入图片描述

  追加与输入重定向皆可使用dup接口进行重定向,这样简化了代码量,使代码更具可读性。


🚀缓冲区

✈️简单认识缓冲区

  我们可能经常听到 “缓冲区” 这个词,它到底是什么或许你还没有深究过,缓冲区本质上就是一块内存区域,那么为什么要有缓冲区呢?我们来看下面的例子:

  他穿越了,穿越到了千禧年的大学生身上,有一个高中同学叫做阿飞,阿熊在安徽上大学,阿飞在广东上大学。过两天就是阿飞的生日,阿熊买了一个最新显卡准备坐火车送给阿飞,于是阿熊买了火车票,一路颠簸的去掉了广东,然后把礼物送给阿飞,吃顿饭就走了。
在这里插入图片描述
  阿熊回到了2024年,正巧阿熊现在的高中同学阿乐也在广东准备过生日,阿熊在安徽,于是阿熊精心挑选了一个键盘,准备送给阿乐。现在是2024年,阿熊拿上键盘直接下楼到邮政快递公司把快递寄了过去。
  但是快递公司并不是拿到你的快递就开始出发配送,而是要等到一定的量到了再配送。等到一车货够了,那么就会出发送快递。过了两天阿乐收到了你的消息,于是也下楼到邮政取了快递。

  上述情况,我们仅仅是为了送一个生日礼物,但是这样的开销是不是就太大了,不仅要买来回车票,到地方可能还要住旅馆,而且花费的时间也很多。这就好比操作系统把每一次的输入都立即送到显示器上一样,电信号看似很快,但是千千万万个信息呢?

  而有了快递公司就方便了许多,只需要下楼寄个快递,等到一定数目的快递集齐了快递就可以发过去了,而对方收到快递也仅仅只需要下楼到快递公司取个快递。

  不论是C语言,还是操作系统,它们同样如此,既然一次一次来回写入开销很大,倒不如开辟一块内存区域,当内容空间的内容满了,再做刷新。

  所以,总的来说,缓冲区其实就是 以空间换时间的一种方式


✈️技术角度认识缓冲区

  我们以前所接触的缓冲区几乎都是语言层面的缓冲区,而缓冲区也分为系统层和语言层缓冲区。

在这里插入图片描述

  C语言中的printf/fgets等函数底层其实就是调用系统调用来实现输出的。但是系统调用本身就是需要成本的,所以我们用户层面就要尽量较少的访问系统调用。

  这就好比,阿熊月末没钱了,通常一顿饭要10块钱,撑到下个月大概还有不到10顿,那么阿熊是向朋友一次借10块分10次借还是一次借100就借一次呢?显然阿熊会选择后者。

  C语言也是这么想的,所以C原也提供了缓冲区,我们通常写入数据其实 写入的是C语言的缓冲区,再由C语言调用系统调用把数据刷新到内核当中。从而间接减少系统调用的次数。

缓冲类型分为:

  • 全缓冲:全部刷新,普通文件缓冲区写满才刷新。
  • 行刷新:\n之前的内容进行刷新。
  • 无刷新:无刷新。

✈️FILE结构体

  既然存在缓冲区这个东西,那么它存储在哪呢?实际上 缓冲区是由FILE结构体来维护的

  在上文我们说stdin、stdout、stderr这三个流的类型皆是 FILE* 类型,而每个文件都有自己的FILE结构体,所以 每个文件都有自己的缓冲区

  不仅如此,C语言的很多接口的参数也都是FILE* 类型:

在这里插入图片描述

   拿fwrite来举例,仅仅是把 *ptr 的 (size * nmemb) 字节大小的内容拷贝到 FILE 缓冲区内,需要的时候内部再决定如何刷新。

  所以这些接口大部分时间都是向FILE内的缓冲区进行拷贝,所以在 用户层面上这些接口的效率也比较高

我们来看看C语言库是如何定义的:

/usr/include/libio.h
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
};

  由此可以清晰的观察到C语言级别的缓冲区是如何定义的,这里需要注意的另一个点是 文件描述符被 _fileno 封装

  下面我写一段代码来证明缓冲区的存在:

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

int main()
{
    //Use system call
    const char* s1 = "hello write\n";
    write(1, s1, strlen(s1));

    //Use C inteface
    const char* s2 = "hello fprintf\n";
    fprintf(stdout, "%s", s2);

    const char* s3 = "hello  fwrite\n";
    fwrite(s3, strlen(s3), 1, stdout);

    fork();//注意这里进行fork
    return 0;
}

在这里插入图片描述
  这个现象就很有趣了,第一次运行没什么问题,三个数据全部打印出来,但是当我们第二次运行并且重定向到空文件当中时却出了问题,你可以先思考为什么。

  其实这是因为,第一次运行程序其实是向显示器打印,这个行为默认的刷新行为是 行刷新。而第二次重定向到了文件中,这个时候刷新方式就变为了 全缓冲
  而全缓冲正常情况下是进程退出时才进行刷新策略的。而在程序的最后我们进行了fork创建了子进程。
  而这个时候,缓冲区接收的数据没有满,所以这个时候不论哪个进程先退出,都会将数据写入到C语言中的缓冲区当中,最终造成了打印出来的数据有两项是重复的。
  而write为什么只打印一次?这是因为write函数是系统调用,并 不参与 语言层的缓冲区,所以只打印一次。

在这里插入图片描述

  当某一个进程退出时,那么一定要将自己缓冲区中的数据刷新到内核当中,而 刷新的本质就是写入!而一旦写入就会 立马发生 写时拷贝,子进程就有自己的缓冲区,将数据写入到缓冲区中,子进程退出后就会造成二次刷新。

  而这个现象也恰恰说明了语言层是存在缓冲区的。


✈️编码模拟

  为了更加深刻理解缓冲区这个概念,我们不妨编写一段代码来加深印象:

bash准备:

[xzy@iZ0jle4p97d8x4byf3u32mZ buffer]$ ll
total 0
-rw-rw-r-- 1 xzy xzy 0 May 13 22:54 filetest.c
-rw-rw-r-- 1 xzy xzy 0 May 13 22:54 makefile
-rw-rw-r-- 1 xzy xzy 0 May 13 22:54 mystdio.c
-rw-rw-r-- 1 xzy xzy 0 May 13 22:54 mystdio.h

mystdio.h:

#pragma once 

#include<stdio.h>

#define SIZE 4096
#define NONE_FLUSH (1<<1)//无刷新
#define LINE_FLUSH (1<<2)//行刷新
#define FULL_FLUSH (1<<3)//全缓冲

typedef struct _myFILE
{
    char outbuffer[SIZE];//输出缓冲区
    int pos;//位置
    int cap;//容量
    int fileno;//文件描述符
    int flush_mode;//刷新方式
}myFILE;

myFILE *my_fopen(const char* pathname, const char* mode);

void my_fclose(myFILE* fp);

int my_fwrite(myFILE* fp, const char* s, int size);

mystdio.c:

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

const char* toString(int flag)
{
    if(flag & NONE_FLUSH) return "None";//无缓冲
    else if(flag & LINE_FLUSH) return "Line";//flag为行缓冲
    else if(flag & FULL_FLUSH) return "FULL";//全缓冲
    return "Unknow";
}

void DebugPrint(myFILE* fp)//debug代码是否有误
{
    printf("outbuffer: %s\n", fp->outbuffer);
    printf("fd: %d\n", fp->fileno);
    printf("pos: %d\n", fp->pos);
    printf("cap: %d\n", fp->cap);
    printf("flush_mode: %s", toString(fp->flush_mode));
}

myFILE* my_fopen(const char* pathname, const char* mode)//模拟fopen函数
{
    int flag = 0;
    if(strcmp(mode, "r") == 0)//r方式打开
    {
        flag |= O_RDONLY;
    }
    else if(strcmp(mode, "w") == 0)//w 方式打开
    {
        flag |= (O_CREAT | O_WRONLY | O_TRUNC);
    }
    else if(strcmp(mode, "a") == 0)//a 方式打开
    {
        flag |= (O_CREAT | O_WRONLY | O_APPEND);
    }
    else 
    {
        return NULL;
    }

    int fd = 0;
    if(flag & O_WRONLY)//是否为写的方式打开
    {
        umask(0);
        fd = open(pathname, flag, 0666);//写的方式打开很可能会创建文件
    }
    else 
    {
        fd = open(pathname, flag);//只读方式打开
    }
    
    if(fd < 0) return NULL;

	//将FILE对象初始化
    myFILE* fp = (myFILE*)malloc(sizeof(myFILE));
    fp -> fileno = fd;
    fp -> cap = SIZE;
    fp -> pos = 0;
    fp -> flush_mode = LINE_FLUSH;//默认为行缓冲

    return fp;
}

void my_fflush(myFILE* fp)//刷新
{
    if(fp->pos == 0) return;
    write(fp->fileno, fp->outbuffer, fp->pos);
    fp->pos = 0;
}

void my_fclose(myFILE* fp)//自定义关闭文件
{
    my_fflush(fp);//退出前要刷新
    close(fp -> fileno);
    free(fp);
}

int my_fwrite(myFILE* fp, const char* s, int size)//自定义fwrite
{
    memcpy(fp->outbuffer + fp->pos, s, size);
    fp->pos += size;
    if((fp->flush_mode & LINE_FLUSH) && fp->outbuffer[fp->pos - 1] == '\n') //行刷新
    {
        my_fflush(fp);   
    }
    else if((fp->flush_mode & FULL_FLUSH) && fp->pos == fp->cap)//全缓冲
    {
        my_fflush(fp);
    }
    return size;
}

filetest.c:

#include"mystdio.h"
#include<string.h>

const char* filename = "./log.txt";//文件名称

int main()
{
    myFILE* fp = my_fopen(filename, "w");//以写的方式打开文件
    if(fp == NULL) return 1;

    int cnt = 5;//进行数量测试
    char buffer[64];//缓冲区
    
    while(cnt)
    {
        snprintf(buffer, sizeof(buffer), "youcanseeme, bro:%d \n", cnt--);
        my_fwrite(fp, buffer, strlen(buffer));
        sleep(2);
    }

    my_fclose(fp);
    return 0;
}

在这里插入图片描述

  运行成功之后,我们就可以看到现象,在log文件中打印了我们测试的内容。


📒✏️总结

  •  C语言的一些IO接口需要熟悉,例如fwrite,fputs等等。
  •  当前当前路径是根据进程的cwd来决定的,C语言默认打开三个流:stdin、stdout、stderr。他们三个 分别占用0、1、2三个文件描述符
  •  系统层面的IO交互接口有 write、open、close、read等需要理解。
  • 文件=内容+属性;一个文件是否为空都会存在属性,而操作系统为了维护文件的属性,先描述再组织,将文件的属性组织为一个结构体file,而 每个file以双链表的形式相连
  •  因为Linux下一切皆文件,所以文件也需要被组织起来,于是file结构体的指针file*被组织起来封装在一个叫做files_struct 指针数组内,而数组下标就是 文件描述符
  •  重定向是 根据更改文件描述符的指向文件 做到的,可以使用dup2接口做调整。
  •  缓冲区本质上是一块内存区域,而缓冲区分为系统层缓冲区和语言层缓冲区,在C语言中缓冲区被封装在FILE结构体内,每一个文件都有自己的缓冲区
  •  缓冲区满了会刷新到内核中,而 刷新的本质就是写入

在这里插入图片描述
  希望这篇文章能够帮到你【玫瑰】~~

  • 70
    点赞
  • 68
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 78
    评论
### 回答1: Linux IO 模型是指 Linux 操作系统中的 IO 处理机制。它的目的是解决多个程序同时使用 IO 设备时的资源竞争问题,以及提供一种高效的 IO 处理方式。 Linux IO 模型主要分为三种:阻塞 IO、非阻塞 IOIO 多路复用。 阻塞 IO 指的是当程序进行 IO 操作时,会被挂起直到 IO 操作完成,这种方式简单易用,但是对于高并发环境不太适用。 非阻塞 IO 指的是程序进行 IO 操作时,如果无法立即完成,会立即返回一个错误码,程序可以通过循环不断地进行 IO 操作来实现轮询的效果。非阻塞 IO 可以提高程序的响应速度,但是会增加程序的复杂度。 IO 多路复用指的是程序可以同时监听多个 IO 设备,一旦有 IO 事件发生,就会立即执行相应的操作。IO 多路复用可以提高程序的效率,但是需要程序员手动编写代码来实现。 Linux IO 模型还有其他的实现方式,比如信号驱动 IO 和异步 IO 等。但是这些方式的使用比较复杂,一般不常用。 ### 回答2: Linux中的IO模型是指操作系统在处理输入输出的过程中所遵循的一种方式。它主要包括阻塞IO、非阻塞IO、多路复用IO和异步IO四种模型。 阻塞IO是最简单的IO模型,当一个IO操作发生时,应用程序会被阻塞,直到IO操作完成才能继续执行。这种模型的特点是简单直接,但是当有多个IO操作时会造成线程的阻塞,影响系统的性能。 非阻塞IO是在阻塞IO基础上发展而来的,应用程序在发起一个IO操作后可以继续执行其他任务,不必等待IO操作的完成。但是需要通过轮询来不断地检查IO操作是否完成,效率相对较低。 多路复用IO使用select、poll、epoll等系统调用来监听多个IO事件,当某个IO事件就绪时,应用程序才会进行读写操作,避免了前两种模型的效率问题。多路复用IO模型适用于连接数较多时的场景,如服务器的网络通信。 异步IO是最高效的IO模型,应用程序发起一个IO操作后,立即可以执行其他任务,不需要等待IO操作的完成。当IO操作完成后,操作系统会通知应用程序进行后续处理。异步IO模型常用于高吞吐量、低延迟的应用,如高性能服务器和数据库等。 总之,Linux IO模型提供了多种不同的方式来处理输入输出,每种模型都有其适用的场景和特点。选择合适的IO模型可以提高系统的性能和效率。 ### 回答3: Linux IO模型是指操作系统中用于处理输入输出操作的一种方法或机制。在Linux中,常见的IO模型有阻塞IO、非阻塞IOIO多路复用和异步IO。 阻塞IO是最基本的IO模型,当应用程序发起一个IO请求时,它将一直阻塞等待直到IO操作完成,期间无法做其他任务。虽然简单易用,但是对资源的利用不高。 非阻塞IO在发起一个IO请求后,不会阻塞等待IO操作完成,而是立即返回并继续做其他任务。应用程序需要不断地轮询IO操作状态,直到操作完成。由于需要不断轮询,对CPU的占用较高,但可以提高资源的利用率。 IO多路复用是通过一个线程同时监听多个IO事件,从而实现并发处理多个IO操作。在IO多路复用模型中,应用程序不需要进行轮询,而是通过调用select、poll或epoll等系统调用监听多个文件描述符的IO事件。这样可以在单个线程中处理多个IO操作,提高并发性能。 异步IO模型在发起一个IO请求后,应用程序不需要等待IO操作完成,而是继续做其他任务。当IO操作完成后,操作系统会通知应用程序。异步IO模型需要操作系统的支持,效率较高,但实现较为复杂。 通过选择合适的IO模型,可以根据不同的应用场景来提高IO操作的效率和性能。例如,对于需要同时处理大量连接的服务器应用,IO多路复用是一种常见的选择;而对于需要处理大量IO操作的高性能服务器,则可以考虑使用异步IO模型。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 78
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

阿熊不会编程

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

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

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

打赏作者

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

抵扣说明:

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

余额充值