【Linux】基础IO(二)

发布于:2025-05-13 ⋅ 阅读:(12) ⋅ 点赞:(0)

📝前言:

上篇文章我们对Linux的基础IO有了一定的了解,这篇文章我们来讲讲IO更底层的东西

  1. 重定向及其原理
  2. 感受file_operation
  3. 文件缓冲区

🎬个人简介:努力学习ing
📋个人专栏:Linux
🎀CSDN主页 愚润求学
🌄其他专栏:C++学习笔记C语言入门基础python入门基础C++刷题专栏


一,重定向及其原理

系统中的重定向

简单回顾一下。

输出重定向

  1. 覆盖输出(>)
echo "Hello, World!" > test.txt

这会把命令的输出(原来echo命令的输出是到显示器上的)写入文件,如果文件原本就存在,内容会被覆盖。

  1. 追加输出(>>)
echo "Append this line" >> test.txt

此操作会把命令的输出添加到文件末尾,不会覆盖原有的内容。

输入重定向

  1. 标准输入重定向(<)
wc -l < test.txt

该命令会从文件中读取输入,而不是从键盘获取。(将命令的输入源从默认的键盘,转变为文件或者其他命令的输出)

重定向的原理

以输出重定向>为例:

可见,重定向操作的本质就是,把原本要输出到a文件流的数据,输出到了b文件流。

这是什么做到的呢?
知识储备:

  1. 文件描述符的分配原则(找最小没被分配的)
  2. C语言的输入输出库函数底层是对系统调用的封装,传的是固定的文件描述符编号
    • printf为例,假如printf封装的是write,向标准输出流输出,则fd == 1(这个在C语言库里实现是固定死的)

ls > myfile.txt为例,重定向的流程就是:

  • 先用myfile.txtfd覆盖文件描述表里下标为1的位置
    • 先创建myfile.txt,则fd == 3,然后再直接覆盖
    • 或者:先close(1),然后open,此时给myfile.txt分配的fd就是1

下面以直接覆盖为例:

  • 此时,13都指向myfile.txt
  • 然后,ls本来是往stdout打印(也就是往fd == 1),这个是固定死的。ls的打印只认fd == 1
  • 但是,此时fd == 1指向myfile.txt,于是就向myfile.txt里面打印了

在这里插入图片描述
总结,重定向操作就是:

  • 让文件描述表的下标“重定向”(指向不同文件)
  • 由于上层的接口不知道下层指向改变了,于是原来输出到原来下标对应的文件的内容,就输出到了新执行的文件里面

示例:
在这里插入图片描述
我们写用重定向操作的时候,还可以指定要重定向的文件描述表的下标。如:

  • ls 1> myfile.txt
    • 让文件描述表1指向myfile.txtls1输出,此时1指向myfile.txt,于是就往myfile.txt输出了。
  • ls 1>myfile.txt 2>&1(实现重定向两个)
    • 1重定向到myfile.txt2再重定向到1(也相当于指向myfile.txt

dup2系统调用

dup2,用于复制文件描述符:

  • 原型:int dup2(int oldfd, int newfd);
  • newfd原来的要关闭
  • 然后把下标为oldfd的文件指针复制到文件描述符表下标为newfd的位置(直接覆盖)

使用示例:

 60 int main()
 61 {
 62     int fd = open("log.txt", O_WRONLY | O_CREAT, 0666);
 63     dup2(fd, 1);
 64     printf("hello world!\n");                                                                                                                                                                                
 65     return 0;
 66 } 

运行结果:
在这里插入图片描述

myshell中添加重定向操作

知识储备:

  • 进程程序替换,不会影响被打开的文件(即:不会影响文件描述表)

重要代码:

 35 // 全局变量与重定向有关
 36 #define NoneRedir 0 
 37 #define InputRedir 1
 38 #define OutputRedir 2
 39 #define AppRedir 3
 40 int redir = NoneRedir;
 41 char *filename = nullptr;

229 char* TrimSpace(char* pos)
230 {
231     while(isspace(*pos))
232         pos++;                                                                                                                                                                                               
233     return pos;
234 }

236 void ParseRedir(char command_buffer[], int len)
237 {
238     int end = len - 1;
239     while(end >= 0)
240     {
241         if(command_buffer[end] == '<')
242         {
243             redir = InputRedir;
244             command_buffer[end] = 0;
245             filename = TrimSpace(&command_buffer[end] + 1);
246             break;
247         }
248         else if(command_buffer[end] == '>')
249         {
250             if(command_buffer[end-1] == '>')
251             {
252                 redir = AppRedir;
253                 command_buffer[end] = 0;
254                 command_buffer[end-1] = 0;
255                 filename = TrimSpace(&command_buffer[end] + 1);
256                 break;
257             }
258             else
259             {
260                 redir = OutputRedir;
261                 command_buffer[end] = 0;
262                 filename = TrimSpace(&command_buffer[end] + 1);
263                 break;
264             }
265         }
266         else
267         {
268             end--;
269         }
270     }
271 }

273 void DoRedir()
274 {
275     if(redir == InputRedir)
276     {
277         if(filename)
278         {
279             int fd = open(filename, O_RDONLY);
280             if(fd < 0) exit(2);
281             dup2(fd, 0);
282         }
283         else exit(1);
284     }
285     else if(redir == OutputRedir)
286     {
287         if(filename)
288         {
289             int fd = open(filename, O_CREAT | O_WRONLY | O_TRUNC, 0666);
290             if(fd < 0) exit(4);
291             dup2(fd, 1);
292         }
293         else exit(3);
294     }
295     else if(redir == AppRedir)
296     {
297         if(filename)
298         {
299             int fd = open(filename, O_CREAT | O_WRONLY | O_APPEND, 0666);
300             if(fd < 0) exit(6);
301             dup2(fd, 1);
302         }
303         else exit(5);
304     }
305     else
306     {}
307 }

注意:ParseRedir是在分析指令的时候调用,DoRedir()则是在执行指令之前,DoRedir以后就会影响指令的IO结果

运行结果:

在这里插入图片描述

二,感受 file_operation

理解一下:一切皆文件

  • 在Linux下,一切皆文件。是Linux的重要设计理念。
  • 不同硬件设备的访问是不一样的,但是Linux都把他们抽象成了文件(如进程、磁盘、显示器、键盘…)
  • 使得用户可以使用统一的文件操作接口(如open、read、write、close等)来访问这些资源

如何实现的呢?
在这里插入图片描述

  • 在Linux下,大部分设备都被抽象成了struct file(对于不同类型的设备,struct file的属性和操作函数可能会有所不同)
  • 但,struct file 中都有一个 f_op 指针,指向了⼀个 file_operations 结构体
  • struct file_operations 是一个函数指针集合,定义了对设备的各种操作方法
  • 尽管不同设备的访问方式不同,但是在 file_operations这一层,都会有相同的,如open、read、write、close等方法
  • 当访问一个设备的时候(如对磁盘进行read),进程会找到磁盘的struct file,通过f_op 指针找到file_operations,然后调用里面的read,这个read的是磁盘提供的read
  • 对于file_operations的上层(进程)而言,对设备read,都是去file_operations里面找read,操作一样(所以,一切皆文件)
  • 对应进程的再上层(用户),也是一切皆文件
  • 本质:找到read以后,调用不同设备所提供的不同方法

总结:在file_operations层统一接口

三,文件缓冲区

意义

为什么要有缓冲区?

  • 缓冲区用来存放文件的内容。
  • 因为系统调用是有成本的,现将内容存放到缓冲区,而不是直接刷新
  • 可以减少系统调用的次数,提高使用者的效率。

缓冲区刷新

前置知识储备:

  • 对应文件而言,语言层有一个FILE结构体,在内核层也有一个struct file结构体
  • 语言层文件有缓冲区(FILE维护),内核层文件也有缓冲区(struct file维护)
  • 不同文件通常有各自独立的缓冲区
  • 对于C语言的文件缓冲区,刷新缓冲区,就是把数据拷贝给内核文件缓冲区(一旦拷贝过去,我们就认为已经刷新好了,内核层的刷新策略复杂,由OS自主决定,我们不研究)
  • C语言文件缓冲区的刷新条件
    • 立即刷新:约等于没有缓冲区(如:stderr)
    • 全刷新:当缓冲区满的时候才刷新(普通文件通常是)
    • 行刷新:满了一行的长度 / 遇到换行符\n(显示器通常是)
  • 当然fflush可以强制刷新,进程退出也会自动刷新缓冲区(这些说的都是从语言层刷新到内核层,刷到内核层我们就认为刷好了,能看见了)

数据在缓冲区的流动

系统调用 write

对于系统调用而言,write的数据,直接到内核文件缓冲区,然后被刷新。

库函数

对于printf而言

  • printf并不是直接写到内核文件缓冲区额,而是先把数据输出到了语言层的缓冲区
  • 当遇到刷新缓冲区时,要通过fd(找到对应的文件),调用系统调用比如(write),把语言层缓冲区的内容拷贝给内核文件缓冲区,才完成真正的刷新

我们可以看下面几个代码:

示例一

代码1:

 68 int main()
 69 {
 70     close(1);
 71     int fd = open("log.txt", O_WRONLY | O_CREAT, 0666);                                                                                                                                                      
 72     printf("hello world!\n");
 73     return 0;
 74 }

在这里插入图片描述
成功写入

代码2:

 68 int main()
 69 {
 70     close(1);
 71     int fd = open("log.txt", O_WRONLY | O_CREAT, 0666);
 72     printf("hello world!\n");
 73     close(1);                                                                                                                                                                                                
 74     return 0;
 75 }

在这里插入图片描述
写入失败
为什么?

  • 首先,重定向会改变刷新条件
    • printf("hello world!\n"); 如果往显示器上输出,显示器是行刷新,但是重定向以后 1 指向普通文件了,是全刷新
  • 现象解释
    • printf往文件log.txt里面输出了"hello world!\n",但是没有刷新
    • 在进程结束前close(1)close会直接触发fflush,但文件已关闭,文件描述符1已失效,找不到对应的文件和内核文件缓冲区,刷新失败!

示例二

代码1:

 78 #include <stdio.h>
 79 #include <string.h>
 80 int main()
 81 {
 82     const char *msg0="hello printf\n";
 83     const char *msg1="hello fwrite\n";
 84     const char *msg2="hello write\n";
 85     printf("%s", msg0);
 86     fwrite(msg1, strlen(msg0), 1, stdout);
 87     write(1, msg2, strlen(msg2));
 88     // fork();                                                                                                                                                                                               
 89     return 0;
 90 }

输出结果1(无重定向):
在这里插入图片描述
输出结果2(有重定向):
在这里插入图片描述

代码2(结尾加个fork):

 78 #include <stdio.h>
 79 #include <string.h>
 80 int main()
 81 {
 82     const char *msg0="hello printf\n";
 83     const char *msg1="hello fwrite\n";
 84     const char *msg2="hello write\n";
 85     printf("%s", msg0);
 86     fwrite(msg1, strlen(msg0), 1, stdout);
 87     write(1, msg2, strlen(msg2));
 88     fork();                                                                                                                                                                                                  
 89     return 0;
 90 }

输出结果3(无重定向):
在这里插入图片描述
输出结果4(有重定向):
在这里插入图片描述

为什么会这样呢?

  • 先解释 结果2 和 结果 4
    • fork会创建子进程,文件描述符对应的用户层缓冲区会被复制,内核层文件缓冲区会被共享
    • 重定向时,往log.txt文件里面输出
      • 对于write系统调用,直接输出到内核文件缓冲区
      • 但是对应库函数fwriteprintf,输出的内容还在用户层缓冲区,未被刷新。
      • 没有fork的时候,在进程结束的时候,被刷新到内核文件缓冲区(只有一份)
      • fork的时候
        • 父子各自刷一份语言层缓冲区fwrieprintf写的内容
        • 但系统调用的是只有父进程的那一份。因为,代码已经被执行过了,并且数据是直接写到内核文件缓冲区的
  • 对于结果1 和 结果3:
    • 因为没有重定向,显示器是行刷新,遇到\n就刷新
    • 所以在代码执行到fork的时候,语言层的缓冲区的内容已经被刷出来了
    • 并且fork在最后,前面的代码只由父进程执行一次

🌈我的分享也就到此结束啦🌈
要是我的分享也能对你的学习起到帮助,那简直是太酷啦!
若有不足,还请大家多多指正,我们一起学习交流!
📢公主,王子:点赞👍→收藏⭐→关注🔍
感谢大家的观看和支持!祝大家都能得偿所愿,天天开心!!!


网站公告

今日签到

点亮在社区的每一天
去签到