【Hello Linux】基础IO(二)

简介: 【Hello Linux】基础IO(二)

文件描述符fd

是什么?

文件描述符是进程中用来标识文件的一个无符号整数

为什么要有文件描述符?

文件是由进程运行时打开的 一个进程可以打开大量的文件 与此同时系统中还存在着大量的进程 所以说系统中可能存在着大量被打开的文件

所以说操作系统必须要对系统中存在着的大量的文件进行管理

根据先描述再组织的原则 操作系统为每个文件创建struct file结构体 用来描述这个文件的各项数据 在描述完毕之后使用双链表的数据结构将这些结构体组织起来 之后操作系统对于文件的管理也就变成了对这张双链表的增删查改

但是我们前面也说过 一个进程可能打开大量的文件 当然一个文件也可能被大量的进程打开 那么我们应该如何区别每个进程打开了什么文件呢?

我们都知道进程 = 程序 + PCB + mm_struct + 页表

而实际上PCB中还有一个指针 它指向名为files_struct的结构体

在files_struct中有一个叫做fd_array的指针数组 该指针数组的下标就是我们所说的文件描述符

我们我们运行进程 打开log.txt文件的时候 我们首先会将文件从磁盘加载到内存当中 并且形成对应的struct file结构体并且连入双链表中

然后在file_array中找到一个未被使用的空间 让这个空间指向我们刚刚的struct file结构体

最后将这个空间的下标返回给进程

所以说只要我们有一个文件的fd就能够对这个文件进行各种操作

为什么我们打开一个文件之后 默认最小的fd是3呢?

因为当我们c语言程序成为进程的时候 它就会占用0 1 2 三个文件描述符

其中0代表的是默认输入流 它对应的文件是键盘

1代表的是默认输出流 它对应的文件是显示器

2代表的是默认错误流 它对应的文件是显示器

扩展: 简单介绍下磁盘文件和内存文件

在磁盘中的文件就叫做磁盘文件 被加载到内存中的文件就叫做内存文件

磁盘中的文件天然有两部分组成 文件属性 + 文件数据

文件属性代表着文件的创建时间 文件大小 修改时间等信息

文件数据就是我们写入的各种数据

从这里我们可以知道 就算我们只创建了一个空文件 它也是占用空间的

当我们的磁盘文件加载到内存中时一般都是先加载文件属性 当我们需要进行读取数据 写入数据等操作时我们才开始加载文件的数据

文件描述符的分配规则

首先我们连续打开五个文件 看看分配的文件描述符是什么

测试代码如下

1 #include <stdio.h>
  2 #include <sys/types.h>
  3 #include <sys/stat.h>
  4 #include <fcntl.h>
  5 
  6 
  7 int main()
  8 {
  9   int fd1 = open("log1.txt" , O_RDWR | O_CREAT , 0666);
 10   int fd2 = open("log2.txt" , O_RDWR | O_CREAT , 0666);
 11   int fd3 = open("log3.txt" , O_RDWR | O_CREAT , 0666);
 12   int fd4 = open("log4.txt" , O_RDWR | O_CREAT , 0666);
 13   int fd5 = open("log5.txt" , O_RDWR | O_CREAT , 0666);
 14   int fd6 = open("log6.txt" , O_RDWR | O_CREAT , 0666);
 15   printf("%d\n",fd1);
 16   printf("%d\n",fd2);
 17   printf("%d\n",fd3);
 18   printf("%d\n",fd4);
 19   printf("%d\n",fd5);
 20   printf("%d\n",fd6);                                                                                                                
 21   return 0;
 22 }

结果如下

我们发现文件描述符是从3开始连续分配的

前面我们也讲过了 因为 0 1 2已经被系统的三个流占用了

所以说自然从3开始分配

那么如果我们关闭中间文件描述符为2的文件呢?

我们发现此时文件描述符从2开始连续递增了

那么如果我们只关闭文件描述符为1的文件呢?

我们发现此时文件描述符从0开始跳过了12 之后连续递增了

注意 我们这里不能关闭fd为1的文件 因为那个是标准输入流 关闭了显示器上就不能打印信息了

那么我们很简单就能总结出文件描述符的分配规则了

它是从file_array中找到未使用空间的最小下标开始分配

重定向

重定向是指将一个程序输出的内容从一个位置(例如终端)传递到另一个位置(例如文件或另一个程序的输入)的过程

重定向的原理

在理解了上面的文件描述符和分配规则之后我们对于重定向原理的理解应该是十分简单的一件事

首先我们给出下面的这样一段代码

1 #include <stdio.h>
  2 #include <unistd.h>
  3 #include <string.h>
  4 #include <sys/types.h>
  5 #include <sys/stat.h>
  6 #include <fcntl.h>
  7 
  8 
  9 int main()
 10 {
 11   close(1);
 12   int fd = open("log.txt" , O_CREAT | O_RDWR , 0666);
 13 
 14   int count = 5 ;
 15   while(count--)
 16   {
 17     fputs("hello world!\n" , stdout);
 18   }
 19   fflush(stdout);                                                                                                                               
 20   close(fd);
 21   return 0;
 22 }

我们首先将标准输出流关闭 之后打开一个log.txt的文件

紧接着向标准输出流中写入五句hello world

最后刷新缓冲区

我们编译运行程序

我们发现屏幕中什么都没有打印 反而是log.txt文件中有了五句hello world

这是为什么呢?

因为我们关闭了标准输入流 (1) 所以自然无法向屏幕中打印数据了

而根据文件描述符的分配规则 文件描述符会优先分配未被占用的下标最小的位置

而这个时候1就是最小的位置 所以说我们新打开文件的描述符fd就会等于1

这时候我们向标准输出流写入的数据都会写入到文件当中

这也就完成了我们的重定向

这也就是我们输出重定向的原理

输入重定向的原理和输出重定向类似 这里就不再赘述

stderr和stdout的区别

我们首先看下面的这段代码

1 #include <stdio.h>
  2 #include <unistd.h>
  3 #include <string.h>
  4 #include <sys/types.h>
  5 #include <sys/stat.h>
  6 #include <fcntl.h>
  7 
  8 int main()
  9 {
 10   fputs("hello stderr\n", stderr);
 11   fputs("hello stdout\n", stdout);                                    
 12   return 0;
 13 }

我们这里分别使用stderr和stdout打印了数据

因为两个文件都是对应的显示器 所以当然会往显示器上打印这两句话

我们发现确实符合我们的预期

接下来看这一段指令

我们发现只有标准输入流被重定向了 标准错误流没有没重定向 还是打印到了屏幕中

当然这个也很好理解 因为我们重定向的时候是对于文件描述符为1的文件重定向 而sterr的文件描述符为2

这就是它们两者的区别

dup2

我们能不能在不关闭流的情况下进行重定向呢?

首先来看下面这个图

c语言在底层设计的之后就指定1为标准输出流

stdout就是向文件描述符为1的文件中输出数据

所以说我们只需要改变1指向的位置 让1指向需要重定向的文件就可以实现重定向了

在linux中提供了一个系统函数dup2来完成我们上面说的事情

它的函数原型如下

int dup2(int oldfd, int newfd);

dup2会将fd_array[oldfd]的内容拷贝到fd_array[newfd]当中

如果拷贝成功则会返回newfd 如果拷贝失败则会返回-1

我们下面直接使用代码来演示下dup2的作用

1 #include <stdio.h>
  2 #include <unistd.h>
  3 #include <string.h>
  4 #include <sys/types.h>
  5 #include <sys/stat.h>
  6 #include <fcntl.h>
  7 
  8 int main()
  9 {
 10   int fd = open("log.txt" , O_RDWR);
 11   if (fd < 0)
 12   {
 13     perror("open");
 14   }
 15 
 16   dup2(fd,1);
 17   fputs("hello ! im dup2!\n",stdout);                                                                                                
 18   return 0;
 19 }

解释下上面的代码

首先先打开一个叫做log.txt的文件 接受它的文件描述符fd

接着我们将fd内的内容拷贝到stdout中

最后我们向stdout输出语句

我们查看编译运行后的结果

我们发现即使没有关闭流我们也成功的进行了重定向

FILE

FILE是C语言中处理文件输入输出的结构体类型,包含了与文件相关的信息和状态,例如文件指针、文件打开方式、缓冲区等。

我们在cplusplus官网中可以看到这几张图

我们可以看到 c语言默认打开的三个流 而它们的数据类型全部是FILE*

而我们的底层全部是用文件描述符fd找到 sturuct file去对文件进行操作的

那么我们很轻松的就可以推断出FILE这个结构体中肯定包含了文件描述符fd

推断出了这一点之后我们再来理解下 c语言的文件操作函数是怎么执行的

下面拿fopen来举例

当我们调用fopen函数的时候 在底层fopen函数会调用系统结构open函数打开或创建对应的文件 并且获取到对应的文件描述符fd 将文件描述符fd填写到FILE结构体中的_fileno变量中 在上层返回给用户FILE*的指针

FILE缓冲区

我们首先来看下面的一段代码

1 #include <stdio.h>
  2 #include <unistd.h>
  3 #include <string.h>
  4 
  5 
  6 int main()
  7 {
  8   // c
  9   printf("im printf\n");
 10   fputs("im fputs\n" , stdout);
 11 
 12   // linux
 13   const char* str = "im write\n";
 14   write(1 , str , strlen(str));
 15   fork();
 16   return 0;                                                                                           
 17 }

解释下上面的这段代码 我们分别调用了两个c语言的函数还有一个系统接口函数打印语句

在最后我们使用fork函数创建了一个子进程

接下来我们编译运行这段代码

正常打印了三条语句 没有问题

可是当我们重定向的时候便会发生一个奇怪的现象

我们使用c语言打印的两条语句竟然打印了两次 而系统结构打印的语句只打印了一次

解释这个奇怪的现象

我们知道这肯定跟fork函数创建的子进程有关 子进程和父进程会共享数据和代码 所以说会打印两份

可是为什么只有c语言的函数打印了两份

又为什么只有重定向的时候打印两次呢

为什么只有重定向的时候打印两次呢

要解释上面的两个问题我们首先要了解缓冲区以及缓冲区的刷新策略

缓冲区的刷新策略分为三种

  • 无缓冲 (立即刷新)
  • 行缓冲 (往显示器中打印数据)
  • 全缓冲 (对磁盘文件中写入数据)

所以说我们往显示器中打印的时候因为带上了换行符 所以会立即刷新数据

刷新数据之后缓冲区就没有数据了 所以说子进程不会打印任何数据

而我们重定向实际上就是往磁盘文件中写入数据 此时采取的刷新策略就是全缓冲 即缓冲区满了才刷新

所以说在创建子进程之后缓冲区中仍有数据 父子进程都会打印一份 这才造成了打印两次的现象

那么到这里为止我们解决了为什么只有重定向的时候打印两次的问题

为什么只有c语言的函数打印了两份

还记不记得我们上面FILE的定义

FILE是C++中处理文件输入输出的一个结构体类型,包含了与文件相关的信息和状态,例如文件指针、文件打开方式、缓冲区等。

也就是说FILE这个结构体中包含缓冲区 这也是c语言层面的缓冲区

所以说只有c语言的函数才能享有这个缓冲区

那么操作系统也有缓冲区嘛?

答案是肯定的 操作系统也有自己的缓冲区

当我们刷新用户区的数据的时候实际上并不是将用户缓冲区的数据直接刷新到磁盘文件中而是刷新到操作系统的缓冲区中 之后经由操作系统的缓冲区将数据刷新到磁盘文件中

相关文章
|
17天前
|
缓存 安全 Linux
Linux 五种IO模型
Linux 五种IO模型
|
24天前
|
小程序 Linux 开发者
Linux之缓冲区与C库IO函数简单模拟
通过上述编程实例,可以对Linux系统中缓冲区和C库IO函数如何提高文件读写效率有了一个基本的了解。开发者需要根据应用程序的具体需求来选择合适的IO策略。
22 0
|
26天前
|
存储 IDE Linux
Linux源码阅读笔记14-IO体系结构与访问设备
Linux源码阅读笔记14-IO体系结构与访问设备
|
2月前
|
Linux 数据处理 C语言
【Linux】基础IO----系统文件IO & 文件描述符fd & 重定向(下)
【Linux】基础IO----系统文件IO & 文件描述符fd & 重定向(下)
48 0
|
2月前
|
Linux 编译器 C语言
【Linux】基础IO----理解缓冲区
【Linux】基础IO----理解缓冲区
36 0
【Linux】基础IO----理解缓冲区
|
2月前
|
缓存 网络协议 算法
【Linux系统编程】深入剖析:四大IO模型机制与应用(阻塞、非阻塞、多路复用、信号驱动IO 全解读)
在Linux环境下,主要存在四种IO模型,它们分别是阻塞IO(Blocking IO)、非阻塞IO(Non-blocking IO)、IO多路复用(I/O Multiplexing)和异步IO(Asynchronous IO)。下面我将逐一介绍这些模型的定义:
118 1
|
2月前
|
Linux C语言 C++
【Linux】基础IO----系统文件IO & 文件描述符fd & 重定向(上)
【Linux】基础IO----系统文件IO & 文件描述符fd & 重定向(上)
35 0
|
3月前
|
Linux 网络安全 开发工具
【linux】基础IO |文件操作符
【linux】基础IO |文件操作符
30 0