基础IO(Linux)

本文深入探讨了Linux中的文件操作,从C语言的fopen开始,介绍了Linux的OPEN系统调用,文件描述符fd的概念及分配原理。讲解了文件加载过程,包括如何通过文件描述符与进程关联,并分析了文件描述符重定向的实现。此外,还讨论了缓冲区的作用以及dup2系统调用在文件重定向中的应用。通过对这些概念的解析,加深了对Linux文件系统和I/O操作的理解。
摘要由CSDN通过智能技术生成

1. 回顾c语言文件

学习之前先来简单回顾一下c语言的IO知识,首先fopen打开文件,第一个参数是文件名,如果没有这个文件会对应的在源文件目录下创建一个。fopen第二个参数是权限,即以什么方式来对这个文件进行操作。然后调用对应的函数。操作完之后在使用fclose清理。

  1 #include<stdio.h>
  2 #include<string.h>
  3 int main()
  4 {
  5   FILE* fp=fopen("log.txt","w");
  6 
  7   if(fp==NULL)
  8   {
  9     perror("fopen err\n");
 10     return 1;
 11   }
 12   const char* msg="hello world!!";
 13   int n=5;                                                                                                                                                          
 14   while(n--){
 15   fwrite(msg,strlen(msg),1,fp);
 16   }
 17   fclose(fp);
 18   return 0;
 19 }

把对应的字符串写入log.txt,写入5遍
在这里插入图片描述
那么今天在LINUX中我们需要从进程,系统的角度,深刻的理解IO过程。

2. Linux中的文件

假如一个创建一个空文件,那么他在硬盘上是否占空间呢?
肯定是占的。因为一个文件由内容+属性组成的。属性也是数据
我们之前的fwrite就是修改文件内容。

一切语言,默认打开3个文件。
stdin:标准输入,键盘
stdout:标准输出,显示器
stderr:标准错误,显示器
为什么要默认打开三个文件呢?
当计算机发明出来的时候,人们就要与计算机交互,语言是中间载体。是语言帮我们通过系统打开这三个文件。

简单验证一下,我们将fp文件换成stdout显示器文件。他就将这些字符串打印到了显示器中

fwrite(msg,strlen(msg),1,stdout);     

在这里插入图片描述
所以普通文件和显示器文件在语言层面是没有什么区别的。

3. OPEN系统调用

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

  • 第一个参数是文件名字,可以带路径,不带路径默认在本源文件。

  • 第二个参数类型是int,传入的是宏常量,其实各个参数都是一个比特位为1,经过按位或之后通过辨认这个整数中比特位为1的位置,来确定以什么方式打开。

  • 在这里插入图片描述

  • 第三个参数就是创建文件之后的权限,为八进制,受到掩码的限制。

这就是open这个系统调用的大概描述,fopen是封装起来的库函数,为了更好地进行二次开发。

4. 文件描述符fd

 1 #include<stdio.h>
  2 #include<sys/stat.h>
  3 #include<sys/types.h>
  4 #include<fcntl.h>
  5 #include<unistd.h>
  6 #define SIZE 128
  7 int main()
  8 {
  9   int fd=open("log.txt",O_RDONLY);
 10   if(fd<0)
 11   {
 12     perror("open err!\n");
 13     return 0;
 14   }
 15   char buf[SIZE];
 16   read(fd,buf,SIZE);
 17   printf("%d\n %s",fd,buf); //这是前面测试read,这里我在外面吧log.txt清空了,这里验证fd                                                                                                                                      
 18   int fd1=open("log1.txt",O_RDONLY|O_CREAT);
 19   int fd2=open("log2.txt",O_RDONLY|O_CREAT);
 20   int fd3=open("log3.txt",O_RDONLY|O_CREAT);
 21   int fd4=open("log4.txt",O_RDONLY);
 22   int fd5=open("log5.txt",O_RDONLY);
 23   printf("%d\n",fd1);
 24   printf("%d\n",fd2);
 25   printf("%d\n",fd3);
 26   printf("%d\n",fd4);
 27   printf("%d\n",fd5);
 28   close(fd);
 29 }

在这里插入图片描述
发现fd依次从3开始增长,其实0,1,2就是默认打开的3个文件,stdin,stdout,stderr。后面没有创建成功,所以输出fd为-1.
这也正体现了fopen里面也对O_CREATE进行了封装。
这个fd是一个整数,我们在程序中打开了多个文件,运行起来也就是进程打开了多个文件,那么就要对它进行描述和组织。

5. 文件加载过程

当我们从硬盘打开一个或多个文件,先要把它加载到内存中,用结构体描述,在用链表连接起来。那么怎么与进程关联起来呢,实际上就是tasksruct里存储这指向file_struct的结构体,这个file_struct内有一个结构体指针数组,结构体指针数组内又存储着指向文件的指针,数组的下标对应的指针指向了那个文件。

在这里插入图片描述

至此进程就与文件关联起来,而且open成功后就会返回fd,fd就代表了数组下标,也就意味着我们可以通过它找到对应的文件,进行下一步操作。

对于不同文件,比如硬盘上的文件,网卡,显示器,他的读写方法一定是不一样的,而在描述文件的时候,结构体内也加入了函数指针,指向该文件对应的函数。
在这里插入图片描述
虽然在底层读写方法的原理是不一样的,但是在我们使用者看来,你们都是文件,虽然最低层实现的方法不一样,但是用函数指针调用却毫无差别,用同一种方法访问的确是不同的代码,就像是面向对象中多态的意思。其实面向对象也就是通过这么一个个工程不断总结出来的。

5.1 总结

在来梳理一下整个过程,open打开文件,加载到内存中,操作系统管理起来。进程调用的open那么这个文件怎么和进程关联起来呢,进程task_struct存储着指向file_struct的指针,file_struct里存储着结构体指针数组,这个数组里面存储着指针,指针指向对应的文件结构体,怎么区分这些指针呢,用数组的下标。也就是说文件结构体(加载到内存的文件)通过数组下标就和进程关联起来。
open成功后,返回数组下标fd,这时你对文件进行操作,怎么找到呢,直接传fd,在调用操作对应的方法。

6. 文件描述符的分配,重定向原理

  1 #include<stdio.h>
  2 #include<sys/stat.h>
  3 #include<sys/types.h>
  4 #include<fcntl.h>
  5 #include<unistd.h>
  6 #define SIZE 128
  7 int main()
  8 {
  9   close(0);
 10   int fd=open("log.txt",O_RDONLY|O_CREAT);
 11   if(fd<0)
 12   {
 13     perror("open err!\n");
 14     return 0;
 15   }
 16   printf("%d\n",fd);
 17   close(fd);                                                        
 18 
 19 }

默认的文件描述符从最小的没有被分配的给分配,默认0,1,2被占用。
假如关掉1,然后重新open一个文件。

  1 #include<stdio.h>
  2 #include<sys/stat.h>
  3 #include<sys/types.h>
  4 #include<fcntl.h>
  5 #include<unistd.h>
  6 #define SIZE 128
  7 int main()
  8 {
  9   close(1);
 10   int fd=open("log.txt",O_WRONLY|O_CREAT,0644);                                                                                                                     
 11   if(fd<0)
 12   {
 13     perror("open err!\n");
 14     return 0;
 15   }
 16   printf("hello world ---%d\n",fd);
 17   fflush(stdout);//stdout此时是log.txt文件,是全缓冲,不刷新的话就打印不到文件中,后面详细讲
 18   close(fd);
 19 
 20 }

发现原本要打印到显示器上的内容被写入到了log.txt中
在这里插入图片描述
其实原理很简单就是下图这样:
在这里插入图片描述
那么echo xxx >file的本质就是先创建子进程,子进程执行,将字符串重定向到file文件里,本质上就是先关闭1,然后open打开文件,1号下标的指针指向file,调用write,字符串写入到file中,子进程执行完,被回收。
那么追加>>就是open打开文件时,参数设置成O_APPEND。

7. 系统调用的fd与库函数

   1 #include<stdio.h>
    2 #include<string.h>
    3 #include<unistd.h>
    4 #include <sys/types.h>
    5 #include <sys/stat.h>
    6 #include <fcntl.h>
    7 
    8 int main()
    9 {
   10   //close(1);
   11   //int fd=open("log.txt",O_CREAT|O_APPEND|O_WRONLY);
   12 
   13   const char* str="hello write\n";
   14   const char* str1="hello world printf\n";
   15   const char* str2="hello world fprintf\n";
   16   write(1,str,strlen(str));                                                                                                                                       
   17   printf(str1);
   18   fprintf(stdout,str2);
        fflush(stdout);//假如close掉1,log.txt是文件,文件是全缓冲,不刷新就看不到了。
   19   return 0;
   20 }

运行打印在了显示器中
在这里插入图片描述
当close掉1,fd就会分配到1,原本打印到显示器的内容就被写到了log.txt中

   1 #include<stdio.h>
    2 #include<string.h>
    3 #include<unistd.h>
    4 #include <sys/types.h>
    5 #include <sys/stat.h>
    6 #include <fcntl.h>
    7 
    8 int main()
    9 {
   10   close(1);
   11   int fd=open("log.txt",O_CREAT|O_APPEND|O_WRONLY);
   12 
   13   const char* str="hello write\n";
   14   const char* str1="hello world printf\n";
   15   const char* str2="hello world fprintf\n";
   16   write(1,str,strlen(str));                                                                                                                                       
   17   printf(str1);
   18   fprintf(stdout,str2);
        fflush(stdout);//stdout是文件,全缓冲需要刷新
   19   return 0;
   20 }

查看文件也没问题

在这里插入图片描述
与系统调用write不同的是,这两个库函数都会用到stdout这个指针,指向显示器文件。这个指针是file*的结构体,也就是说实际上在这个结构体内一定封装一个fd文件描述符。1被关闭,文件描述符指向了那个log.txt文件。

8. 缓冲区

当我们不close1的时候会打印三个字符串到显示器中。
当我们close1的时候强制刷新打印三个字符串到文件当中。

    1 #include<stdio.h>
    2 #include<string.h>
    3 #include<unistd.h>
    4 #include <sys/types.h>
    5 #include <sys/stat.h>
    6 #include <fcntl.h>
    7 
    8 int main()
    9 {
   10   
   11   int fd=open("log.txt",O_CREAT|O_APPEND|O_WRONLY,0644);
   12 
   13   const char* str="hello write\n";
   14   const char* str1="hello world printf\n";
   15   const char* str2="hello world fprintf\n";
   16   write(1,str,strlen(str));
   17   printf(str1);
   18   fprintf(stdout,str2);
   19   fork();
   20   fflush(stdout);
   21   close(fd);                                                                                                                                                      
   22   return 0;
   23 }

那么当我们fork的时候,会创建子进程。
当我们不close1的时候会打印三个字符串到显示器中。(行缓冲,子进程拷贝的缓冲区没有东西,也就没有刷新打印出来)
在这里插入图片描述
当我们close1的时候,fork之后强制刷新,文件之中会有5个字符串,其中两个库函数的信息被重复打印。(全缓冲,先放到缓冲区里,子进程拷贝缓冲区,进程结束或遇到fflush,父子进程同时刷新)
在这里插入图片描述
也就是说一旦重定向,会影响缓冲方式。
而对于显示器和硬件中的文件写入时缓冲的方式也不同
库函数对应的缓冲方式:
在这里插入图片描述

打印到文件时,库函数信息为什么会出现两次,因为fork之前两个打印信息被全缓冲到缓冲区,fork之后子进程复制父进程,缓冲区数据也被复制。之后两个进程执行fflush或进程结束,就一共打印了4次。

write从头到尾只打印一次,也就是说他没有所谓用户级缓冲区,内核级缓冲区不在讨论范围。那么就可以得出一个结论c标准库提供了一个用户级缓冲区。
在这里插入图片描述
缓冲区由struct file来维护

9.dup2系统调用实现重定向

之前的重定向是关闭1,然后打开文件,文件就分配了1这个文件描述符。
那么假如文件早已打开怎么实现呢?

操作系统提供了一个系统调用,dup2
在这里插入图片描述

在这里插入图片描述
这句话的意思就是new是old的一份拷贝,即把3是下标对应一个指针,把这个指针的地址拷贝给1下标的指针,1指针这时就指向了这个文件。
所以传参的时候oldfd3,newfd就是1。可以这么理解

你想要我打印到显示器的东西打印到文件,就得让显示器的fd对应指针,指向你文件的fd对应指针指向的内容,怎么指向,把我指针内容(指针存储地址)拷贝给你。dup2(oldfd,newfd)把3拷给1。

即现在有两个指针,指向这个文件

在这里插入图片描述
那么我们就要关闭fd,因为文件描述符有限,而且不关闭就造成了文件描述符泄漏。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

楠c

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

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

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

打赏作者

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

抵扣说明:

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

余额充值