Linux的IO(初阶)
创始人
2024-03-08 21:13:57
0

Linux的IO(初阶)

文章目录

  • Linux的IO(初阶)
    • 1.C语言文件IO
      • 1.1 C语言文件的(复习)
      • 1.2 相对路径与绝对路径(复习)
      • 1.3 C语言中文件操作函数(复习)
      • 1.4 C语言文件写入方式(复习)
      • 1.5 三个默认打开的流
      • 1.6 FILE类型的理解
    • 2.Linux系统文件IO
      • 2.1 打开文件的系统接口函数:open
      • 2.2 关闭文件的系统接口函数:close
      • 2.3 写入文件信息的系统接口函数:write
      • 2.4 读取文件信息的系统接口函数:read
    • 3.文件描述符fd
      • 3.1 为什么Linux下一切皆文件
      • 3.2 系统管理文件的原理
      • 3.3 文件操作符fd的分配原则
    • 4.对重定向的理解
      • 4.1 输入重定向的原理(<)
      • 4.2 输出重定向的原理(>)
      • 4.3 对dup2的理解
      • 4.4 对dup2的使用

1.C语言文件IO

1.1 C语言文件的(复习)

文件分为文本文件和二进制文件:

  • 文本文件:在内存中什么样,保存在硬盘时需要转化,需要转化成字符,这个操作需要程序员做
  • 二进制文件:在内存中什么样,在硬盘就什么样

1.2 相对路径与绝对路径(复习)

路径可以分为:绝对路径、相对路径

  1. 绝对路径:指目录下的绝对位置,通常从盘符开始的路径。如:C:\data\images\1.png
  2. 相对路径:从当前文件开始出发找目标文件的过程。如:\images\1.png
  3. Windows目录分隔符:\
  4. Linux目录分隔符:/
  5. 注意:C语言下\是转义字符,一般写出\\,比如:C:\\data\\images\\1.png

1.3 C语言中文件操作函数(复习)

请添加图片描述


1.4 C语言文件写入方式(复习)

请添加图片描述


1.5 三个默认打开的流

都说Linux下一切皆文件,也就是说Linux下的任何东西都可以看作是文件,那么显示器和键盘当然也可以看作是文件。我们能看到显示器上的数据,是因为我们向“显示器文件”写入了数据,电脑能获取到我们敲击键盘时对应的字符,是因为电脑从“键盘文件”读取了数据

那么我们就有以下疑问了:

  • 为什么我们向“显示器文件”写入数据以及从“键盘文件”读取数据前,不需要进行打开“显示器文件”和“键盘文件”的相应操作?

  • 打开文件一定是进程运行的时候打开的,而任何进程在运行的时候都会默认打开三个输入输出流,即``标准输入流、标准输出流以及标准错误流,对应到C语言当中就是stdin、stdout以及stderr`

  • 标准输入流对应的设备就是键盘,标准输出流和标准错误流对应的设备都是显示器

  • 查看man手册我们就可以发现,stdin、stdout以及stderr这三个家伙实际上都是FILE*类型的,FILE本质就是一个结构体

extern FILE *stdin;
extern FILE *stdout;
extern FILE *stderr;
  • 当我们的C程序被运行起来时,操作系统就会默认使用C语言的相关接口将这三个输入输出流打开,之后我们才能调用类似于scanf和printf之类的函数向键盘和显示器进行相应的输入输出操作
    请添加图片描述

  • 也就是说,stdin、stdout以及stderr与我们打开某一文件时获取到的文件指针是同一个概念,试想我们使用fputs函数时,将其第二个参数设置为stdout,此时fputs函数会不会之间将数据显示到显示器上呢?

#include 
int main()
{fputs("hello stdin\n", stdout);fputs("hello stdout\n", stdout);fputs("hello stderr\n", stdout);return 0;
}

请添加图片描述

  • 答案是肯定的,此时我们相当于使用fputs函数向“显示器文件”写入数据,也就是显示到显示器上
  • 注意: 不止是C语言当中有标准输入流、标准输出流和标准错误流,C++当中也有对应的cin、cout和cerr,其他所有语言当中都有类似的概念。实际上这种特性并不是某种语言所特有的,而是由操作系统所支持的

1.6 FILE类型的理解

请添加图片描述

一些问题的理解:

  1. 外设不止一种,OS该怎么去管理这么多的文件?
    • 先描述再组织。用struct file描述后操作系统再进行组织管理即可
  2. 每个外设的读写方式都不同,用同一种类型(file)该怎样去让他们调用自己的读写呢?
    • 提供不同的方法即可。C里面则是以函数指针的形式体现【这种结构体中写方法的形式】,如下图:
      请添加图片描述
  3. 语言上的接口不过是对系统接口的封装,为什么语言不直接调用系统接口?
    • 语言为了保证自身生态的完整性,肯定不会去直接调用系统接口,而且直接调用的话不具备可移植性。 比如我在windows系统下调用windows的接口,在linux下肯定就行不通了。但是如果我对其进行封装,比如fopen这个函数,在linux下就调用linux系统给的接口,windows下就调用windows系统的接口
    • 同一个头文件不同平台下的实现可能不同

2.Linux系统文件IO

2.1 打开文件的系统接口函数:open

系统接口中使用open函数打开文件,open函数的函数原型如下:

请添加图片描述

int open(const char *pathname, int flags, mode_t mode);

下面我们对open函数的每个参数和返回值进行解析:

  1. open的第一个参数:pathname

    • open函数的第一个参数是pathname,表示要打开或创建的目标文件
    • 若pathname以路径的方式给出,则当需要创建该文件时,就在pathname路径下进行创建
    • 若pathname以文件名的方式给出,则当需要创建该文件时,默认在当前路径下进行创建

  2. open的第二个参数:flags

    • open函数的第二个参数是flags,表示打开文件的方式

    • 其中常用选项有如下几个:

请添加图片描述

  • 打开文件时,可以传入多个参数选项,当有多个选项传入时,将这些选项用“或(|)”运算符隔开

  • 例如,若想以只写的方式打开文件,但当目标文件不存在时自动创建文件,则第二个参数设置如下:

    O_WRONLY | O_CREAT
    
  • flags参数是整型,有32比特位,若将一个比特位作为一个标志位,则理论上flags可以传递32种不同的标志位

  • 实际上传入flags的每一个选项在系统当中都是以宏的方式进行定义的:

请添加图片描述

  • 例如,O_RDONLYO_WRONLYO_RDWRO_CREAT在系统当中的宏定义如下:

    #define O_RDONLY         00
    #define O_WRONLY         01
    #define O_RDWR           02
    #define O_CREAT        0100
    
  • 这些宏定义选项的共同点就是,它们的二进制序列当中有且只有一个比特位是1(O_RDONLY选项的二进制序列为全0,表示O_RDONLY选项为默认选项),且为1的比特位是各不相同的,这样一来,在open函数内部就可以通过使用“与”运算来判断是否设置了某一选项


  1. open的第三个参数:mode

    • open函数的第三个参数是mode,表示创建文件的默认权限

    • 例如,将mode设置为0666,则文件创建出来的权限为:-rw-rw-rw-

    • 但实际上创建出来文件的权限值还会受到umask(文件默认掩码)的影响,实际创建出来文件的权限为:mode&(~umask)。umask的默认值一般为0002,当我们设置mode值为0666时实际创建出来文件的权限为0664,即:-rw-rw-r–

    • 若想创建出来文件的权限值不受umask的影响,则需要在创建文件前使用umask函数将文件默认掩码设置为0

      umask(0); //将文件默认掩码设置为0
      
    • 当不需要创建文件时,open的第三个参数可以不必设置


  2. open的返回值:

请添加图片描述

  • open函数的返回值是新打开文件的文件描述符

  • 我们可以尝试一次打开多个文件,然后分别打印它们的文件描述符:

    #include 
    #include 
    #include 
    #include 
    int main()
    {umask(0);int fd1 = open("log1.txt", O_RDONLY | O_CREAT, 0666);int fd2 = open("log2.txt", O_RDONLY | O_CREAT, 0666);int fd3 = open("log3.txt", O_RDONLY | O_CREAT, 0666);int fd4 = open("log4.txt", O_RDONLY | O_CREAT, 0666);int fd5 = open("log5.txt", O_RDONLY | O_CREAT, 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开始连续且递增的

请添加图片描述

  • 我们再尝试打开一个根本不存在的文件,也就是open函数打开文件失败:

    #include                                                                                        
    #include 
    #include 
    #include 
    int main()
    {int fd = open("test.txt", O_RDONLY);printf("%d\n", fd);return 0;
    }
    

    运行程序后可以看到,打开文件失败时获取到的文件描述符是-1

请添加图片描述

  • 实际上这里所谓的文件描述符本质上是一个指针数组的下标,指针数组当中的每一个指针都指向一个被打开文件的文件信息,通过对应文件的文件描述符就可以找到对应的文件信息

  • 当使用open函数打开文件成功时数组当中的指针个数增加,然后将该指针在数组当中的下标进行返回,而当文件打开失败时直接返回-1,因此,成功打开多个文件时所获得的文件描述符就是连续且递增的

  • 而Linux进程默认情况下会有3个缺省打开的文件描述符,分别就是标准输入0、标准输出1、标准错误2,这就是为什么成功打开文件时所得到的文件描述符是从3开始进程分配的


2.2 关闭文件的系统接口函数:close

系统接口中使用close函数关闭文件,close函数的函数原型如下:

请添加图片描述

int close(int fd);

使用close函数时传入需要关闭文件的文件描述符即可,若关闭文件成功则返回0,若关闭文件失败则返回-1


2.3 写入文件信息的系统接口函数:write

系统接口中使用write函数向文件写入信息,write函数的函数原型如下:

请添加图片描述

ssize_t write(int fd, const void *buf, size_t count);

我们可以使用write函数,将buf位置开始向后count字节的数据写入文件描述符为fd的文件当中:

  • 如果数据写入成功,实际写入数据的字节个数被返回
  • 如果数据写入失败,-1被返回

举例代码:

//运行程序后,在当前路径下就会生成对应文件,文件当中就是我们写入的内容
#include 
#include 
#include 
#include 
#include 
#include 
int main()
{int fd = open("log.txt", O_WRONLY | O_CREAT, 0666);if (fd < 0){perror("open");return 1;}const char* msg = "hello syscall\n";for (int i = 0; i < 5; i++){write(fd, msg, strlen(msg));}close(fd);return 0;
}

请添加图片描述


2.4 读取文件信息的系统接口函数:read

系统接口中使用read函数从文件读取信息,read函数的函数原型如下:

请添加图片描述

ssize_t read(int fd, void *buf, size_t count);

我们可以使用read函数,从文件描述符为fd的文件读取count字节的数据到buf位置当中:

  • 如果数据读取成功,实际读取数据的字节个数被返回
  • 如果数据读取失败,-1被返回

举例代码:

#include 
#include 
#include 
#include 
#include 
#include 
int main()
{int fd = open("log.txt", O_RDONLY);if (fd < 0){perror("open");return 1;}char ch;while (1){ssize_t s = read(fd, &ch, 1);if (s <= 0){break;}write(1, &ch, 1); //向文件描述符为1的文件写入数据,即向显示器写入数据}close(fd);return 0;
}

运行程序后,就会将我们刚才写入文件的内容读取出来,并打印在显示器上,如下图:

请添加图片描述


3.文件描述符fd

3.1 为什么Linux下一切皆文件

每一个硬件都会对应一个struct_file,都会有对应不同访问的方法。 OS看到的时虚拟层,都是一个个的文件(struct_file),如下图:

请添加图片描述


3.2 系统管理文件的原理

我们如果想了解文件描述符的含义,首先我们得知道系统时如何管理文件的!

让我们来思考下以下问题:

  1. 系统为什么要管理文件?

    • 文件是由进程打开的,一个进程可以打开多个文件。理论上来说,多个进程可以打开一个文件,但是这样会导致文件内容混乱,一般不这样做。所以我们可以理解进程和文件的关系为1对多的关系
    • 系统中会有很多进程,也可能会打开很多的文件,为了使文件不会再系统中出现混乱,所以需要系统管理文件

  2. 系统如何管理文件?

    • 先描述和组织
    • 描述文件:Linux操作系统是由C语言编写的,所以操作系统描述文件是通过结构体来描述文件的。这个结构体名字为struct_file
    • 组织文件:系统用一种数据结构,将描述的文件链接起来。用链表,将每一个struct_file连接起来
    • 这样的管理方式就像系统对进程的管理方式一样

  3. 文件与进程的关系?

    • 我们知道,当一个程序运行起来时,操作系统会将该程序的代码和数据加载到内存,然后为其创建对应的task_struct、mm_struct、页表等相关的数据结构,并通过页表建立虚拟内存和物理内存之间的映射关系,如下图:

请添加图片描述

  • 而task_struct当中有一个指针,该指针指向一个名为files_struct的结构体,在该结构体当中就有一个名为fd_array的指针数组,该数组的下标就是我们所谓的文件描述符,即:文件描述符fd实际就是files_struct里数组的下标

  • 当进程打开log.txt文件时,我们需要先将该文件从磁盘当中加载到内存,形成对应的struct file,将该struct file连入文件双链表,并将该结构体的首地址填入到fd_array数组当中下标为3的位置,使得fd_array数组中下标为3的指针指向该struct file,最后返回该文件的文件描述符给调用进程即可,如下图:
    请添加图片描述

  • 因此,我们只要有某一文件的文件描述符,就可以找到与该文件相关的文件信息,进而对文件进行一系列输入输出操作


  1. 各个文件结构体的定义方式是什么样的?

    1. task_struct里的files_struct* 指针结构如下:
      请添加图片描述

    2. files_struct里维护的对应关系的数组:

请添加图片描述

  1. 数组类型struct file的定义:

请添加图片描述

  1. file里描述的操作:

请添加图片描述


  1. 什么叫做进程创建的时候会默认打开0、1、2?

    • 0就是标准输入流,对应键盘;1就是标准输出流,对应显示器;2就是标准错误流,也是对应显示器
    • 而键盘和显示器都属于硬件,属于硬件就意味着操作系统能够识别到,当某一进程创建时,操作系统就会根据键盘、显示器、显示器形成各自的struct file,将这3个struct file连入文件双链表当中,并将这3个struct file的地址分别填入fd_array数组下标为0、1、2的位置,至此就默认打开了标准输入流、标准输出流和标准错误流

  2. 磁盘文件与内存文件如何区分?

    • 当文件存储在磁盘当中时,我们将其称之为磁盘文件,而当磁盘文件被加载到内存当中后,我们将加载到内存当中的文件称之为内存文件。磁盘文件和内存文件之间的关系就像程序和进程的关系一样,当程序运行起来后便成了进程,而当磁盘文件加载到内存后便成了内存文件
    • 磁盘文件由两部分构成,分别是文件内容和文件属性。文件内容就是文件当中存储的数据,文件属性就是文件的一些基本信息,例如文件名、文件大小以及文件创建时间等信息都是文件属性,文件属性又被称为元信息
    • 文件加载到内存时,一般先加载文件的属性信息,当需要对文件内容进行读取、输入或输出等操作时,再延后式的加载文件数据

3.3 文件操作符fd的分配原则

结论:文件描述符是从最小但是没有被使用的fd_array数组下标开始进行分配的

验证如下:

比如:尝试连续打开五个文件,看看这五个打开后获取到的文件描述符

#include 
#include 
#include 
#include 
int main()
{umask(0);int fd1 = open("log1.txt", O_RDONLY | O_CREAT, 0666);int fd2 = open("log2.txt", O_RDONLY | O_CREAT, 0666);int fd3 = open("log3.txt", O_RDONLY | O_CREAT, 0666);int fd4 = open("log4.txt", O_RDONLY | O_CREAT, 0666);int fd5 = open("log5.txt", O_RDONLY | O_CREAT, 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开始连续递增的,这很好理解,因为文件描述符本质就是数组的下标,而当进程创建时就默认打开了标准输入流、标准输出流和标准错误流,也就是说数组当中下标为0、1、2的位置已经被占用了,所以只能从3开始进行分配

若我们在打开这五个文件前,先关闭文件描述符为0的文件,此后文件描述符的分配又会是怎样的呢?

//同上面代码,不过我们关闭文件描述符为0的文件
close(0);

请添加图片描述

可以看到,第一个打开的文件获取到的文件描述符变成了0,而之后打开文件获取到的文件描述符还是从3开始依次递增的

我们再试试在打开这五个文件前,将文件描述符为0和2的文件都关闭(不要将文件描述符为1的文件关闭,因为这意味着关闭了显示器文件,此时运行程序将不会有任何输出)

close(0);
close(2);

请添加图片描述

最终我们得出结论:文件描述符是从最小但是没有被使用的fd_array数组下标开始进行分配的


4.对重定向的理解

重定向分为:

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

重定向的是文件描述符为1的标准输出流


4.1 输入重定向的原理(<)

输入重定向就是,将我们本应该从一个文件读取数据,现在重定向为从另一个文件读取数据

验证过程:

例如,如果我们想让本应该从“键盘文件”读取数据的scanf函数,改为从log.txt文件当中读取数据,那么我们可以在打开log.txt文件之前将文件描述符为0的文件关闭,也就是将“键盘文件”关闭,这样一来,当我们后续打开log.txt文件时所分配到的文件描述符就是0

请添加图片描述

#include 
#include 
#include 
#include 
#include 
int main()
{close(0);int fd = open("log.txt", O_RDONLY | O_CREAT, 0666);if (fd < 0){perror("open");return 1;}char str[40];while (scanf("%s", str) != EOF){printf("%s\n", str);}close(fd);return 0;
}

请添加图片描述

一点现象:运行结果后,我们发现scanf函数将log.txt文件当中的数据都读取出来了

原因:

  • scanf函数是默认从stdin读取数据的,而stdin指向的FILE结构体中存储的文件描述符是0,因此scanf实际上就是向文件描述符为0的文件读取数据
  • 实际上我们使用重定向时,重定向的是文件描述符为1的标准输出流,而并不会对文件描述符为2的标准错误流进行重定向

4.2 输出重定向的原理(>)

  • 在明确了文件描述符的概念及其分配规则后,现在我们已经具备理解重定向原理的基础了
  • 结论:重定向的本质就是修改文件描述符下标对应的struct file*的内容

验证过程:

输出重定向就是,将我们本应该输出到一个文件的数据重定向输出到另一个文件中

例如,如果我们想让本应该输出到“显示器文件”的数据输出到log.txt文件当中,那么我们可以在打开log.txt文件之前将文件描述符为1的文件关闭,也就是将“显示器文件”关闭,这样一来,当我们后续打开log.txt文件时所分配到的文件描述符就是1,如下图:

请添加图片描述

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

请添加图片描述

一点现象:运行结果后,我们发现显示器上并没有输出数据(printf的),对应数据输出到了log.txt文件当中

原因:

  • printf函数是默认向stdout输出数据的,而stdout指向的是一个struct FILE类型的结构体,该结构体当中有一个存储文件描述符的变量,而stdout指向的FILE结构体中存储的文件描述符就是1,因此printf实际上就是向文件描述符为1的文件输出数据
  • C语言的数据并不是立马写到了内存操作系统里面,而是写到了C语言的缓冲区当中,所以使用printf打印完后需要使用fflush将C语言缓冲区当中的数据刷新到文件中
    请添加图片描述

4.3 对dup2的理解

之前的重定向方法 ,需要先关闭再打开fd,比较麻烦,通常采用dup2实现数据的覆盖达到重定向的目的

要完成重定向我们只需进行fd_array数组当中元素的拷贝即可。例如,我们若是将fd_array[3]当中的内容拷贝到fd_array[1]当中,因为C语言当中的stdout就是向文件描述符为1文件输出数据,那么此时我们就将输出重定向到了文件log.txt

请添加图片描述


4.4 对dup2的使用

函数原型:

请添加图片描述

int dup2(int oldfd, int newfd);

函数功能: dup2会将fd_array[oldfd]的内容拷贝到fd_array[newfd]当中,如果有必要的话我们需要先使用关闭文件描述符为newfd的文件

函数返回值: dup2如果调用成功,返回newfd,否则返回-1

使用dup2时,我们需要注意以下两点:

  • 如果oldfd不是有效的文件描述符,则dup2调用失败,并且此时文件描述符为newfd的文件没有被关闭
  • 如果oldfd是一个有效的文件描述符,但是newfd和oldfd具有相同的值,则dup2不做任何操作,并返回newfd

使用案例:

例如,我们将打开文件log.txt时获取到的文件描述符和1传入dup2函数,那么dup2将会把fd_arrya[fd]的内容拷贝到fd_array[1]中,在代码中我们向stdout输出数据,而stdout是向文件描述符为1的文件输出数据,因此,本应该输出到显示器的数据就会重定向输出到log.txt文件当中

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

请添加图片描述

相关内容

热门资讯

AWSECS:访问外部网络时出... 如果您在AWS ECS中部署了应用程序,并且该应用程序需要访问外部网络,但是无法正常访问,可能是因为...
AWSElasticBeans... 在Dockerfile中手动配置nginx反向代理。例如,在Dockerfile中添加以下代码:FR...
银河麒麟V10SP1高级服务器... 银河麒麟高级服务器操作系统简介: 银河麒麟高级服务器操作系统V10是针对企业级关键业务...
北信源内网安全管理卸载 北信源内网安全管理是一款网络安全管理软件,主要用于保护内网安全。在日常使用过程中,卸载该软件是一种常...
AWR报告解读 WORKLOAD REPOSITORY PDB report (PDB snapshots) AW...
AWS管理控制台菜单和权限 要在AWS管理控制台中创建菜单和权限,您可以使用AWS Identity and Access Ma...
​ToDesk 远程工具安装及... 目录 前言 ToDesk 优势 ToDesk 下载安装 ToDesk 功能展示 文件传输 设备链接 ...
群晖外网访问终极解决方法:IP... 写在前面的话 受够了群晖的quickconnet的小水管了,急需一个新的解决方法&#x...
不能访问光猫的的管理页面 光猫是现代家庭宽带网络的重要组成部分,它可以提供高速稳定的网络连接。但是,有时候我们会遇到不能访问光...
Azure构建流程(Power... 这可能是由于配置错误导致的问题。请检查构建流程任务中的“发布构建制品”步骤,确保正确配置了“Arti...