基础IO(上)

简介: 本文主要讲述了文件描述符、重定向以及缓冲区的概念和运用。

前言

文件 = 内容 + 属性

  1. 所有对文件的操作就是对内容操作和对属性操作。
  2. 内容是数据,属性也是数据。存储文件,必须既存储内容又存储数据。创建文件默认就是在磁盘中的。
  3. 我们要访问一个文件的时候,都是要先把这个文件打开的。访问文件的本质就是进程在打开文件,打开之后就要把磁盘中的文件加载到内存中去。
  4. 一个进程可以打开多个文件,多个进程也可以打开多个文件。换言之,加载到内存中被开的文件,可能会存在多个。

操作系统在运行中,可能会打开很多个文件,那么操作系统要不要管理这些被打开的文件呢,如果需要的话如何管理呢?六字真言~

先描述,再组织!

一个文件要被打开,一定要先内核中形成被打开的文件对象。

  1. 文件按照是否被打开分为:被打开的文件和没有被打开的文件。被打开的文件在内存中,没有被打开的文件在磁盘中。
  2. 研究被打开的文件本质就是:进程和被打开文件的关系。

1. 回顾C文件接口

老规矩,首先打开man手册学习下fopen接口

image-20240520115440635

  • 文件以"w"的方式打开,会先清空文件内容,如果文件不存在则会自动创建!
  • 文件以"a"的方式打开,也是写入,但不会清空文件,而是从文件结尾追加新内容!

那么如何向文件写入内容呢?可以使用fputs或者fwrite,依旧查看一下man手册:

fputs

image-20240523120704077

fwrite:

image-20240523120748496

这里演示一下以"w"的方式打开,并向文件内写入一些内容

#include <stdio.h>
#include <string.h>
int main()
{
   
   
    FILE *fp = fopen("log.txt", "w");
    if (NULL == fp)
    {
   
   
        perror("fopen");
        return 1;
    }

    const char *msg = "hello linux!\n";
    int count = 10;
    while (count--)
    {
   
   
        fputs(msg, fp);
    }
    fclose(fp);
    return 0;
}

image-20240523121325425

成功写入~!

2. 系统文件I/O

用man手册查看一下open

image-20240521163005375

  • pathname:要打开或创建的目标文件
  • flags:打开文件时,可以传入多个参数选项,用下面的一个或者多个常量进行“或”运算,构成flags。
  • 参数:
    • O_RDONLY:只读打开
    • O_WRONLY:只写打开
      • O_RDWR:读,写打开这三个常量,必须指定一个且只能指定一个
    • O_CREAT:若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限
    • O_APPEND:追加写
    • O_TRUNC:清空文件
  • 返回值:
    • 成功:新打开的文件描述符
    • 失败:-1
#include <stdio.h>
#include <string.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, 0666);
    if (fd < 0)
    {
   
   
        perror("open");
        return 1;
    }

    const char *msg = "hello file system-call\n";
    // 操作这个文件
    write(fd, msg, strlen(msg));
    // 关闭这个文件
    close(fd);
    return 0;
}

当我们想向一个文件中写入字符串的时候,不需要strlen()+1,因为\0是C语言的规定,而不是文件的规定!

参数O_WRONLY默认写入不是完全覆盖式写入,而是从文件开始依次覆盖写入,剩余的保留。如果需要修改写的方式,则需要上段代码中的第十行代码。

  • 按照写方式打开,如果文件不存在就创建它,如果有内容会先清空文件的内容!

    int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
    与之对应的C语言接口,fopen以"w"方式打开:
    FILE *fp = fopen("log.txt", "w");

  • 按照写方式打开,如果文件不存在就创建它,如果有内容则从文件结尾处开始写入,追加但不清空文件!

    int fd = open("log.txt", O_WRONLY | O_CREAT | O_APPEND, 0666);
    与之对应的C语言接口,fopen以"a"方式打开:
    FILE *fp = fopen("log.txt", "a");

所以说,我们学习的C语言打开文件的接口,在底层一定封装了系统调用!

3. 文件描述符fd

通过对open函数的学习,我们知道了文件描述符就是一个小整数。其实文件描述符fd本质也就是数组的下标!

我们发现我们所创建的文件描述符是从3开始的,那么0、1、2去哪里了呢?

  • Linux进程默认情况下会有3个缺省打开的文件描述符,分别是标准输入0, 标准输出1, 标准错误2。
  • 0、1、2对应的物理设备一般是:键盘,显示器,显示器。

image-20240523205321668

文件描述符就是从0开始的小整数。当我们打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件。于是就有了file结构体。表示一个已经打开的文件对象。而进程执行open系统调用,所以必须让进程和文件关联起来。每个进程都有一个指针*files,指向一张表files_struct,该表最重要的部分就是包含一个指针数组,每个元素都是一个指向打开文件的指针!所以,本质上,文件描述符就是该数组的下标。所以,只要拿着文件描述符,就可以找到对应的文件。

无论读写,都要先把数据加载到文件缓冲区中去!

我们在应用层进行的数据读写,本质是什么?

本质就是将内核缓冲区中的数据,来回进行拷贝!

文件描述符的分配规则

#include <stdio.h>
#include <string.h>
#include <unistd.h>
int main()
{
   
   
    char buf[1024];
    ssize_t s = read(0, buf, 1024);
    if (s > 0)
    {
   
   
        buf[s] = 0;// 也就是buf这个字符串以'\0'结尾
        write(1, buf, strlen(buf));
    }
    return 0;
}

效果如下:

image-20240523223202939

可以看到我们不用scanfprintf也能实现输入和打印,同时也证明了一点

进程默认已经打开了0、1、2,我们可以直接使用0、1、2进行数据的访问!

当我们一上来就关闭默认打开的0或2号文件:

#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
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;
}

image-20240524111604866image-20240524111622955

我们再次创建文件时会发现,我们所创建的新文件fd被分配为了0或者2,于是得到一个结论:

文件描述符的分配规则是,寻找最小的,没有被使用的数据位置,分配给指定的打开文件!

但是如果我们关闭1号文件,屏幕上就什么都不会显示了,因为1号文件是默认输出文件。

4. 重定向

针对上段代码,做出点稍稍的修改~

int main()
{
   
   
    close(1); // 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);
    printf("stdout->fd:%d\n", stdout->_fileno);
    fflush(stdout);
    close(fd);
    return 0;
}

当然我们执行完程序后,屏幕依旧什么都不显示,但是我们惊奇的发现,在log.txt文件内出现了这样的情况:

image-20240524114519427

printf默认是打印到显示器中去的,但是为什么会打印到我们的文件中去呢?

image-20240524115707669

printf默认写入stdout中,也就是1号文件,凡是往1号文件写入的内容,都不再写入stdout中了,而是写入了新建的log.txt文件中去。这种现象叫做输出重定向。

重定向的本质,其实就是修改特定文件fd下标的内容。也就是上层fd不变,底层fd指向的内容在改变。

如果我们每次重定向都需要关闭文件,那这样太麻烦,那么有没有一个接口可以帮助我们进行重定向呢?

dup2系统调用

image-20240524164444259

以输出重定向为例:

dup2() makes newfd be the copy of oldfd, closing newfd first if necessary.

newfd成为oldfd的拷贝,oldfd被保留下来了,如果传参,应该这样传:dup2(fd,1)

int main()
{
   
   
    int fd = open("log.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);
    if (fd < 0)
    {
   
   
        perror("open");
        return 1;
    }
    dup2(fd, 1);
    printf("hello printf\n");
    fprintf(stdout, "hello fprintf\n");
    close(fd);
    return 0;
}

image-20240524170810256

在命令行中常见的重定向有:>,>>,<

  • >:输出重定向
  • >>:追加重定向
  • <:输入重定向

image-20240524172122758

5. 标准错误流

int main()
{
   
   
    fprintf(stdout, "hello stdout!\n");
    fprintf(stderr, "hello stderr!\n");
    return 0;
}

形成一个可执行程序文件,把标准输出和标准错误流打印在屏幕上。

当我们用可执行程序文件进行输出重定向时,标准错误流依旧打印在屏幕上:

image-20240524212232185

这并不难理解,因为我们只是做了输出重定向,也就是把1号文件替换成了log.txt,此时stderr没有改变,依旧会将内容输出在屏幕上,如果想要把stderr也就是2号文件重定向,需要如下操作:

./myfile > log.txt 2>&1(取1号文件的地址,使2号文件重定向到该地址中)

上面重定向的写法其实是简写,完整的写法应该是这样的:

./myfile 1> log.txt

为什么要有标准错误流呢

当我们在日常开发中,有些消息是常规信息,有些消息是错误信息。在语言上我们向标准错误打印就是向2号文件打印,所有我们用标准错误打印的信息都是错误信息,将常规信息和错误信息分别打印到不同的文件中,方便我们日后统一排查,如有错误直接查看错误信息所在的文件即可。(在C语言中perror就是向标准错误打印~!)

6. 缓冲区

6.1 前言

我们所理解的缓冲区:就是一部分内存,但是这一部分内存由谁提供呢?

  • 缓冲区的主要作用是提高效率——提高使用者的效率。
  • 因为有缓冲区的存在,我们可以积累一部分数据再统一发送,从而提高了发送的效率。

缓冲区因为能够暂存数据,必定要有一定的刷新方式:

  1. 无缓冲(立即刷新)
  2. 行缓冲(行刷新)
  3. 全缓冲(缓冲区满了再刷新)

这些是缓冲区刷新的一般方式,但是还有特殊情况:

  1. 强制刷新
  2. 进行进程退出的时候,一般要进行缓冲区的刷新

一般对于显示器文件,行刷新(行缓冲)
对于磁盘上的文件,全缓冲(缓冲写满再刷新)

6.2 一个样例

#include <stdio.h>
#include <string.h>
#include <unistd.h>
int main()
{
   
   
    fprintf(stdout, "hello fprintf!\n");
    printf("hello printf!\n");
    fputs("hello fputs!\n", stdout);

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

    fork();
    return 0;
}

可以看到,直接运行程序和将可执行程序重定向到log.txt文件中的运行结果是不一样的!

image-20240604203500804

理解样例:

  1. 当我们直接向显示器打印的时候,显示器文件的刷新方式是行刷新!而且这段代码中输出的所有字符串都有\n,在fork之前数据已经全部被刷新了,包括systemcall。
  2. 重定向到log.txt,本质是向磁盘文件中写入(不是显示器了),我们系统对于数据的刷新方式已经由行缓冲刷新变成了全缓冲刷新了!
  3. 全缓冲意味着缓冲区变大,实际我们写入的简单数据已经不足以把缓冲区写满了,fork在执行的时候,数据依旧在缓冲区中!
  4. 我们目前所谈的“缓冲区”和操作系统是没有关系的~只能和C语言本身有关!

我们日常所用的最多的其实是C/C++提供的语言级别的缓冲区!也就是用户缓冲区。

  1. C/C++提供的缓冲区,里面保存的一定是用户的数据,属于当前进程在运行时自己的数据。但是如果我们把数据交给了OS,那么这个数据就属于OS,而不属于我自己了。
  2. 当进程退出的时候,一般要进行缓冲区的刷新,即便你的数据没有满足缓冲区的刷新条件!

刷新缓冲区属不属于清空或者写入操作呢?算!

fork执行完之后退出,任意一个进程(父进程或者子进程)在退出的时候刷新缓冲区,并发生写时拷贝。

我们发现write系统调用并没有打印打两份,说明:

write并没有使用C语言的缓冲区,而是直接写入到操作系统中去,已经不属于进程了,所以不发生写时拷贝!

那么什么是刷新呢?

刷新就是把C/C++的缓冲区里的内容写入OS中去,这个工作就叫做刷新

C/C++语言缓冲区存在的意义是什么呢?

是为了提高printffprintf等这些函数的调用效率!一个函数的调用时间越短意味着后面的代码执行越快,也就意味着程序运行的整体效率都提高了。

(顺便提一下,printf这些函数在格式化输出的时候,就是在拷贝到缓冲区的时候做的~)

任何情况下,我们输入输出的时候,都要有一个FILE类型,FILE是一个结构体,在这个结构体里包含了fd,那么在这个结构体里再包含一段缓冲区也未尝不可。

6.3 FILE

我们来看下FILE结构体:

首先使用命令:

grep -n 'typedef struct _IO_FILE FILE' /usr/include/stdio.h

image-20240604210029406

FILE结构体在第48行,我们直接将文件定位到48行

vim /usr/include/stdio.h +48

image-20240604210356150

对_IO_FILE结构体的重命名为FILE,也就是我们平常所看到的FILE类型。

再来找一下具体的定义:

grep -n 'struct _IO_FILE {' /usr/include/libio.h

image-20240604210932254

FILE结构体具体定义在第246行,我们直接将文件定位到246行

vim /usr/include/libio.h +246

image-20240604211142169

目录
相关文章
|
2月前
|
缓存 Linux API
文件IO和标准IO的区别
文件IO和标准IO的区别
35 2
|
7月前
|
缓存
标准IO和直接IO
标准IO和直接IO
55 0
|
12月前
|
存储 Linux 块存储
基础IO+文件(二)
基础IO+文件
51 0
|
12月前
|
编译器 Linux vr&ar
基础IO+文件(三)
基础IO+文件
49 0
|
12月前
|
缓存 Linux C语言
基础IO+文件(一)
基础IO+文件
63 0
|
Linux
day26-系统IO(2022.2.23)
day26-系统IO(2022.2.23)
104 1
|
12月前
|
存储 Linux 文件存储
基础IO详解(一)
基础IO详解
86 0
|
12月前
|
存储 Linux C语言
基础IO详解(二)
基础IO详解
69 0
|
存储 资源调度 Kubernetes
Garden.io:它是什么以及为什么要使用它?
Garden.io:它是什么以及为什么要使用它?
121 0
|
存储 设计模式 缓存

热门文章

最新文章