Linux学习基础IO——系统调用 | 文件描述符fd | 重定向

Posted 一只大喵咪1201

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux学习基础IO——系统调用 | 文件描述符fd | 重定向相关的知识,希望对你有一定的参考价值。

🐱作者:一只大喵咪1201
🐱专栏:《Linux学习》
🔥格言:你只管努力,剩下的交给时间!

基础IO


在正式开始介绍之前,本喵先重新谈论一下文件:

  1. 空文件也是要在内存中占据空间的,因为它还有属性数据。
  2. 文件 = 属性 + 内容
  3. 文件操作 = 对内容 + 对属性 或者对内容和属性的操作。
  4. 标定一个文件的时候,必须使用:路径 + 文件名,因为文件具有唯一性。
  5. 如果没有指名文件路径,默认是对当前路径的文件进行访问。
  6. 一个文件,在没有被打开的时候,是不能进行访问的。
  7. 二进制可执行文件,在没有运行的时候,所谓的文件操作都是没有执行的。
  8. 磁盘上的文件分为被打开的文件和没有被打开的文件。

结论:文件操作的本质,是进程和被打开文件之间的关系。

上面对文件的认识,是我们所有使用Linux用户的共识。

🍎文件操作

🍉使用C接口进行文件操作



在将我们的程序编译完成以后,再运行,发现生成了一个新的文件,并且文件中的内容和我们代码中写的一样。

  • 这个过程中,使用的是C语言的接口进行文件操作。
  • 以写的方式打开文件名问log.txt的文件,没有的这个文件的话就会创建。
  • 使用C接口向该文件中写入内容。

本喵曾在文章C语言文件操作一文中详细介绍过C语言的文件操作接口,有兴趣的小伙伴可以去看看。

不同的编程语言都有文件操作的接口,包括C++,Java,Python,php等等语言,并且它们的操作接口函数都不一样,但是它们所在的系统都是Linux系统。

无论上层语言如何变化,但是进行文件操作的时候,各种语言最终都会调用Linux的文件操作的系统调用接口。

🍉文件操作的系统调用

open函数:


可以看到,函数声明有两个,一个是两个参数的,一个是三个参数的,它们必然不是函数重载,因为Linux是用纯C实现的。

参数解释:

  • const char* pathname:这是文件路径,也就是我们要打开的文件所在的路径,其中包括文件名,如果没有路径只有文件名的话,默认在当前路径打开。
  • int flags:打开方式选项标志位。在使用C语言进行文件操作的时候,打开方式有“w”,“r”,“a”等方式,系统调用open也有,只是将这些标志放在了一个32位的变量中。
    不同打开方式,其对应的比特位就会被置1。然后将这个设置好的flags变量传给open系统调用,就会按照相应的方式打开文件。
  • mode_t mode:它是权限值,如果这个文件不存在,那么以写的方式打开的时候就会创建这个文件,在创建文件的时候需要给这个文件设定权限(使用八进制数)。如果这个文件存在的话,那么就不用传第三个参数了,因为文件的权限已经确定了。
  • 返回值:是一个int类型的参数,具体的在后面本喵会介绍,但是如果打开失败就会返回-1。


如果有多个选项需要按位或在一起,共同组成int flags变量传给open系统调用。

常用选项功能
O_RDONLY只读
O_WRONLY只写
O_RDWR读写
O_CREAT以写方式打开时,如果文件不存在则创建文件
O_TRUNC以写方式打开时,清空文件中原内容再写
O_APPEND追加方式打开文件

此时我们没有给open传第三个参数。

执行我们写的代码后,log.txt文件是创建了,但是它是红色的,说明它有错误。可以看到它前面的权限是乱的,因为我们没有指定创建文件时的权限。


将第三个参数加上,因为创建的是普通文件,所以就给它的默认权限是0666。


可以看到,此时创建的文件就正常了,但是权限并不是我们设定的0666,而是0664,这是因为有默认权限掩码(umask)的影响。


我们也可以使用系统调用在程序中改变umask的值。


将默认权限掩码改成0以后,再创建的文件的权限就是我们设置的0666了。本喵曾在文章【Linux学习】权限详细讲解过权限相关的内容,有兴趣的小伙伴可以去看看。

close函数:


将打开文件时返回的int类型的fd值传给close系统调用后,这个被打开的文件就被关闭了。关闭成功返回0,如果关闭失败就返回-1。

write函数:


参数解释:

  • int fd:打开文件时返回的int类型整数(文件描述符)。
  • const void* buf:要写入的数组地址。对于系统调用来说,它并不在意写入的数据是什么类型的,它接收到的数据都是二进制的数字,然后按照字节为单位写入。
  • size_t count:要写入的字节个数。
  • 返回类型size_t:写入多个自己就返回多少。

清空方式写入:



可以看到,成功将字符串写入到了log.txt文件中。

追加方式写入:


使用追加方式打开文件,再多次使用系统调用write写入。


可以看到在原有的基础上,多了5行。

read函数:


参数解释:

  • int fd:打开文件时返回的文件描述符。
  • void* buf:从文件中读取的数据放在这个数组中,同样系统不管文件中的数据类型是什么,都是按字节放入这个数组中。
  • size_t count:要读取的字节个数。
  • ssize_t:读取了多少个返回多少。


此时log.txt中有五行内容,如上图绿色框中所示。

使用只读方式打开,并且将读取的内容放在ch数组中。


编译执行之后,成功打印出了log.txt中的内容。

  • 接收文件中数据的数组我们设定的大小是1024个字节,显然文件中的数据时没有这么大的。
  • read系统调用的第三个参数传的也是1024,但是肯定没有读取到1024个字节。
  • 每一个文件中都有一个文件结束符标志,在C语言中我们见过,也就是EOF,read函数会自动判断文件是否结束,所以即使设置读取的字节是1024个,但是能够符合我们对读取要求,将文件中的内容都读取出来。

以上便是我们常用的文件操作的系统调用。可以看到,对文件进行什么样的操作,取决于以什么样的方式打开文件,再用相应的操作函数去操作文件。

继续和C语言对应:


C语言的文件操作函数,封装了对应的系统调用接口函数。所以说,无论什么语言,文件操作相关的函数都是对系统调用的封装。

🍎文件描述符fd

在使用系统调用open时,返回的那个整数就是文件描述符。


将文件名使用宏的方式打开多个文件。

现在我们见到了文件描述符,发现它就是几个数字。

  • 前面本喵已经讲过,文件操作的本质就是进程被打开文件的关系。
  • 系统中会存在大量被打开的文件,而操作系统同样会管理这些被打开的文件。
  • 管理的方式和管理进程类似,也是采用先描述,再组织的方式。

当一个文件被打开后,操作系统会创建一个对应的结构体对象,类型是struct file。

struct file

	//文件大小
	//文件类型
	......
	//文件的各种属性

  • 每打开一个文件,操作系统就会创建这样的一个结构体对象将被打开的文件描述出来。
  • 将多个这样的结构体对象采用一定的方式组织起来,比如链表的方式,以方便操作系统管理这些被打开的文件。

在描述进程的结构体task_struct中,有一个指针,struct files_struct* files,这个指针指向一个结构体对象,该对象类型如下:

struct files_struct

	//......
	struct file* array[];

  • struct files_struct结构体中存在一个指针数组array,该数组中的指针指向的是一个个struct file类型的结构体对象。
  • 换言之,该数组中放的是被打开文件结构体对象的地址。
  • 每一个被指向的struct file结构体对象都描述着一个被打开的文件。

在前面我们看到,打印出来的fd值是连续的小整数,这些小整数就是struct files_struct 结构体中指针数组struct file* array[]的下标。

文件描述符的本质,就是数组的下标。

下面本喵通过一张示意图来展示一下:

  • 当一个程序被加载到内存中,操作系统会创建一个结构体struct task_struct对象,在该结构体中有一个指针struct files_struct* files,指向一个struct files_struct结构体对象。
  • 这个结构体也被叫做进程描述符表,该结构体中有一个数组struct file* array[],数组中存放的是被打开文件的结构体对象的地址。如上图中,下标为3,也就是fd的是3的时候,访问到的是struct file* array[3]。
  • 通过数组中访问到的地址,可以找到对应打开文件的结构体对象,如上图中的struct file log.txt。

只有被打开的文件才会在内存中创建struct file结构体对象,没有被打开的文件就静静的躺在磁盘上。

不是该进程打开的文件,该进程执行的文件描述符表中也没有这个文件的地址。

🍉文件描述符fd=0/1/2

在上面打开多个文件的时候,我们将打开文件的fd值打印出来,发现它是从3开始的。


那么fd = 0/1/2是什么呢?

C默认会打开三个输入输出流,分别是stdin,stdout,stderr。


可以看到,这三个流是FILE*类型的指针,暂时不用管FILE是什么,只需要知道它是一个结构体。


使用C语言的文件操作结构打开一个文件,再使用系统调用去向文件中写内容。

成功的写入了。

  • 系统调用write第一个参数需要传文件描述符fd。
  • 上面代码中,传入的是FILE->_fileno,并且成功运行。
  • 说明FILE中的_fileno就是文件描述符fd。

我们此时已经确定的知道了,FILE结构体中是有文件描述符的。


在之前的代码中,加上打印三个流的文件描述符的语句,如上图中红色框所示。


文件描述符0 1 2出现了。

  • fd = 0:标准输入流(stdin)
  • fd = 1:标准输出流(stdout)
  • fd = 2:标准错误(stderr)

此时我们便清楚了为什么我们打开的文件,文件描述符是从3开始的,因为012被默认打开的三个流占据了。


每个进程的文件描述符表如上图所示。

🍉文件描述符fd的分配规则

为什么我们打开的文件,fd是从3开始的?不是从5或者6开始的呢?


我们将fd=0的标准输入流关闭掉,再打开文件,并且打印fd值。


我们发现此时的fd成了0,而不是3了。


同样的,将fd=2的流关闭,在打开文件。


此时fd的值又成了2了。

根据这个现象,可以得出结论:文件描述符fd的分别规则是:从小到大,按顺序查找,将没有被占用的数组下标作为被打开文件的文件描述符fd值。

🍎重定向

前面我们只关闭过0和2,没有关闭过1,现在我们关闭一下1来看看。


将标准输出关闭,然后打开文件,并且打印出打开文件的文件描述符fd。


此时运行该程序后,什么都没有显示。

  • 因为将标准输出关闭了,所以无法显示。

根据前面分析的文件描述符分配规则,可以推断出,将标准输出关闭以后,再打开一个文件,此时这个文件的文件描述符fd等于1。

  • 在将fd=1关闭后,再打开一个文件,从小到大按顺序查找,发现数组下标为1的位置没有被占用,所以新打开文件的fd就等于1。
  • printf函数原本是要输出到标准输出的,也就是fd为1的数组中指向的struct file对象的地址。
  • 此时下标为1的数组中不再是标准输出了,而变成了我们新打开文件的地址。
  • 但是printf已经写死了,它仍然会写入到fd为1的文件中,所以原本打印在显示器上的内容此时会写入到新打开的文件中。


查看新打开的文件内容,发现是原本应该打印在屏幕上的内容。


同样的,将1关闭以后,以追加的方式打开一个文件,并且写入多行内容。


同样没有打印到屏幕上,而是打印到了新打开的文件中。

这种将本应该输出到标准输出改为输出到其他文件中的行为称为重定向。

重定向的本质上:上层语言使用的fd不变,在内核中改变fd对应的struct file*地址。

🍉重定向系统调用

上面重定向的实现总感觉怪怪的,还需要关闭,然后再打开新文件,而且也不是很方便,所以操作系统提供了一个系统调用,可以直接实现重定向。


我们常用的函数是dup2。

参数解释:

  • 第一个参数是我们新打开文件的fd。
  • 第二个参数是标准输出到fd,也就是1。


原本应该输出到显示器上的内容输出到了文件log.txt中。

我们上面一直演示的都是本应该输出到显示器重定向输出到了文件中,这种从显示器到文件的重定向叫做输出重定向。

在shell中有命令可以直接实现输出重定向:


ll命令原本是将文件包括属性显示到屏幕上的,使用大于号>输出重定向到了log.txt文件中,如上图绿色框中所示。

  • 使用只读方式打开文件log.txt该文件原本就存在。

    文件中内容是如上图中红色框。
  • 将原本struct file* array[]数组中下标0的内容改成下标为fd的内容,也就是dup2(fd,0)的作用。
  • 使用标准输入函数fgets,从标准输入流也就是键盘中读取字符串。
  • 屏幕上打印读取到的内容。


运行时直接输出log.txt中的内容,没有从键盘获取数据。也就是说,fgets函数是从文件中获取到内容,而不是标准输入。

这种从标准输入到文件的重定向叫做输入重定向。

shell中同样有输入重定向的命令:是小于号<,具体本喵就不显示了。


使用追加方式打开文件,并且采用输出重定向方式输出内容。

在原本文件内容都基础上追加内容。

这种以追加方式打开文件,并且采用输出重定向的方式称为追加重定向。

shell中同样有追加重定向:

使用双大于号>>,实现了追加重定向,在原本log.txt内容都基础上追加内容。

🍉进程独立性

子进程重定向了以后,会影响父进程吗?根据进程独立性我们可以知道,肯定是不会影响到。


在子进程中进行输出重定向,父进程同样在标准输出打印。


子进程中的重定向,并没有影响父进程标准输出的打印。

  • 有两个进程,一个父进程,一个子进程,操作系统维护着两个task_struct结构体,如上图红色框所示。
  • 每个进程的PCB中都有一个struct files_struct*的指针files。它们各自指向的struct files_struct结构体中都有一个文件描述符表。
  • 两个文件描述符表中的内容在子进程刚创建时是一样的,所以它们都指向相同的被打开的文件。
  • 当子进程将自己文件描述符表中下标为1的文件关闭以后,并不影响父进程文件描述符表中下标为1的数组中的内容。

每个进程都会维护自己的文件描述符表,所以多个进程就会存在多个文件描述符表,但是这些表中的指针指向的被打开文件只有一套。

某个进程进行文件的打开与关闭操作时,只需要修改自己的文件描述符表就可以,不会对其他进程造成任何影响。

🍎Linux下一切皆文件

Linux下一切皆文件,这句话相信每一个学习Linux的人都听过,那么如何理解呢?

同样以文件操作的角度来看待硬件,如下图所示:

  • 每一个硬件,操作系统都会维护一个struct file类型的结构体,硬件的各种信息都在这个结构体中,并且还有对应读写函数指针(对硬件的操作主要就是读写)。
  • 每个硬件的具体读写函数的实现方式都在驱动层中,使用到相应的硬件时,操作系统会通过维护的结构体中的函数指针调用相应的读写函数。
  1. 站在操作系统的角度来看下层,无论驱动层和硬件层中有什么,在它看来都是struct file结构体,都是通过维护这个结构体来控制各种硬件。
  2. 站在操作系统的角度来看上层,无论用户层以及系统调用有什么,在它看来都是一个个进程,都是一个个的task_struct结构体,都是通过维护这个结构体来调度各个进程的。

真正的文件在操作系统中的体现也是结构体,操作系统维护的同样是被打开文件的结构体而不是文件本身。

一切皆文件是指:在操作系统中一切都是结构体。

🍎总结

这篇文章主要讲解的是基础IO的应用,包括文件操作的系统调用,文件描述符fd的本质,重定向,以及如何理解Linux下一切皆文件。

Linux入门基础IO

基础IO

✔回顾C文件的接口

在学习C语言时我们了解了一些C语言的对于文件操作的接口。
其中有fopen、fclose、fputc、fgetc、fputs、fgets、fprintf、fscanf、fread、fwrite等

用一段代码简单回顾一下:

   #include<stdio.h>
   #include<string.h>
   int main()
   
     	FILE* fp=fopen("myfile","a+");                                                               
     	if(!fp)
     	
      		printf("fopen error\\n");
     	
    	int count=5;
    	const char *m="hello linux\\n";
    	while(count--)
    	//对文件进行写入
    	fwrite(m,strlen(m),1,fp);
    
    //关闭文件
    fclose(fp);
    return 0;
  

我们先要了解,把内容写入文件中,先要有这个文件,然后就是要打开这个文件。
在上面的代码中

FILE* fp=fopen("myfile","a+");

第一个参数:文件的路径/文件名(不带路径会在当前路径下创建这个文件)。
当前路径:当前进程运行的路径。
第二个参数:就是以怎样的方式来。

在学习Linux时,我们经常听说“一切皆文件“。

那么显示器、键盘是文件吗?
在C语言时,我们经常用printf()函数来把内容显示到显示器上。
而现在,我们不用printf()函数来打印内容。

     #include<stdio.h>
     #include<string.h>
     int main()
     
    	char *m="hello linux\\n";  
        fwrite(m,strlen(m),1,stdout);                                                                                             
       	return 0;
    


这样我们可以了解,显示器也可以看作文件,也可以用fwrite()函数来写入。

重点来了:任何进程在运行时,都会默认打开三个输入输出流。
分别是:
标准输入(键盘)stdin
标准输出(显示器)stdin
标准错误(显示器)stderr
这三个流的类型都是FILE*,文件指针。


✔系统文件I/O

文件操作除了上述的C接口以外,还有我们的系统接口来进行对文件的操作。

我们用C接口操作文件在Linux上跑,其实是C在调用Linux的系统接口来完成的。
所以说,C库文件的接口是对系统调用接口的一次封装。

第一个接口:open接口,与C的区别是前面没有f。

   #include <sys/types.h>
   #include <sys/stat.h>
   #include <fcntl.h>

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

参数分别是:路径或文件名、选项、权限。

选项:

  • O_RDONLY: 只读打开
  • O_WRONLY: 只写打开
  • O_RDWR : 读,写打开
    这三个常量,必须指定一个且只能指定一个
  • O_CREAT : 若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限
  • O_APPEND: 追加写

其中注意的是这些接口的返回类型是int。
文件打开成功后,会返回一个较小的非负整数,表示该文件的文件描述符。
失败返回-1。

用一段代码来感受一下吧。

     #include<stdio.h>
     #include<sys/stat.h>
     #include<sys/types.h>
     #include<fcntl.h>
     #include<unistd.h>
     #include<string.h>
     int main()
     
       umask(0);
      int fd=open("myfile",O_WRONLY|O_CREAT,0666);
      if(fd<0)
      
        perror("open");
        return 1;
      
      char *buf="hello linux\\n";                                                                
      write(fd,buf,strlen(buf));
    
      close(fd);
      return 0;
    


其中一: open中的0666,表示创建文件的时候文件权限的666。当然这要设置一下默认掩码。
其中二:O_WRONLY|O_CREAT 表示如果有该文件就对该文件以只写的方式打开,如果没有就创建这个文件,权限为666,以只写的方式打开。

为什么要用O_WRONLY|O_CREAT来表示呢?
不难看出,这些用大写字母来表示的选项是用宏。这些宏都是对应一个bit位,像位图一样。
我们在传O_WRONLY|O_CREAT的时候,
会用if(O_WRONLY&F)来判断这个选项等等。

而在write中第一个参数是文件的描述符。而文件描述符又是什么呢?

✔文件描述符

我们先用一段代码来感受文件描述符:

   #include<sys/types.h>
   #include<sys/stat.h>
   #include<fcntl.h>
   #include<stdio.h> 
   int main()
   
     umask(0);
    int fd1=open("myfile",O_WRONLY|O_CREAT,0666); 
    int fd2=open("myfile",O_WRONLY|O_CREAT,0666);
    int fd3=open("myfile",O_WRONLY|O_CREAT,0666);  
    int fd4=open("myfile",O_WRONLY|O_CREAT,0666);
  
    printf("fd1:%d\\n",fd1);
    printf("fd2:%d\\n",fd2);
    printf("fd3:%d\\n",fd3);
    printf("fd4:%d\\n",fd4);                                                                                        
    return 0;
  


open执行成功返回一个较小的非负整形,也就是文件描述符。
通过上面的代码执行效果来看,有点像一个数组的下标。
其实这就是一个数组的下标,其中数组的0、1、2下标分被键盘(标准输入)、显示器(标准输出)、显示器(标准错误)给占了。
所以分配下来的是3、4、5、6。

而为什么是数组呢?
那么我们先要了解内存文件和磁盘文件了。

上面的代码创建了myfile文件,其文件的属性(文件大小、文件名、最近一次修改文件的时间等)会以struct file结构体在内存中保存起来。
而该文件的内容是在磁盘上的,也就是磁盘文件。
而这些struct file会被操作系统用双链表的形式来组织起来,和PCB类似。

而一个进程创建会创建PCB,其中PCB中有一个指针指向一个叫files_struct的结构体,其结构体中有一部分是以指针数组的形式存在的,其中存放的内容就是struct file结构体的地址。

进程通过文件描述符找到这个存放文件地址的地放,进而来对文件进行操作。

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

文件描述符的分配规则

如果我们close(0)

     #include<sys/stat.h>
     #include<sys/types.h>
     #include<fcntl.h>
     #include<stdio.h>  
     #include<unistd.h>
     int main()
     
      close(0);
      umask(0);
      int fd1=open("myfile",O_WRONLY|O_CREAT,0666);
    
      printf("fd1:%d\\n",fd1);
      return 0;
    


文件描述符的分配规则:在files_struct数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符。

重定向

我们如果关闭close(1)

	 //输出重定向
     #include<sys/stat.h>
     #include<sys/types.h>
     #include<fcntl.h>
     #include<stdio.h>
     #include<unistd.h>
     #include<string.h>
     
     int main()
     
      close(1);
      umask(0);
   	  int fd1=open("myfile",O_WRONLY|O_CREAT,0666);    
      char *duf="hello linux\\n";
	  
	  //printf("%s",duf);
	  //fflush(stdout);//更新流的用户空间缓冲数据
      write(1,duf,strlen(duf));                                                                 
      return 0;
     

✔FILE

在学习C语言中的对文件操作的函数中有FILE*类型的。

FILE *fopen(const char *path, const char *mode);

那么FILE*是什么呢?

FILE是一个结构体,FILE*是一个结构体的指针。
我们都知道,C库中的IO相关的函数其实是对系统调用的封装。
在系统调用的IO型接口中,open函数的类型是int型。

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

open函数返回的是一个文件描述符。可以通过文件描述符来找到对应的文件。

而FILE结构体中就有一个int型的变量来表示这个文件描述符,这就是为什么C中的IO也可以找到对应的文件,这就是一种封装。

我们来看一下FILE结构体中的代码。

在/sur/include/stdio.h 可以找到
FILE中的int _fileno就是对文件描述符的封装。

struct _IO_FILE 
 int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags
 //缓冲区相关
 /* The following pointers correspond to the C++ streambuf protocol. */
 /* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
 char* _IO_read_ptr; /* Current read pointer */
 char* _IO_read_end; /* End of get area. */
 char* _IO_read_base; /* Start of putback+get area. */
 char* _IO_write_base; /* Start of put area. */
 char* _IO_write_ptr; /* Current put pointer. */
 char* _IO_write_end; /* End of put area. */
 char* _IO_buf_base; /* Start of reserve area. */
 char* _IO_buf_end; /* End of reserve area. */
 /* The following fields are used to support backing up and undo. */
 char *_IO_save_base; /* Pointer to start of non-current get area. */
 char *_IO_backup_base; /* Pointer to first valid character of backup area */
 char *_IO_save_end; /* Pointer to end of non-current get area. */
 struct _IO_marker *_markers;
 struct _IO_FILE *_chain;

 int _fileno; //封装的文件描述符

//……

我们来看下面一段代码:

#include<stdio.h>
#include<sys/stat.h>
#include<sys/tyoes.h>
#include<fcntl.h>
#include<string.h>

int main()

	close(1);
	int fd=open("myfile",O_WRONLY|O_CREAT,0666);
	
	const char* arr="hello linux\\n";
	
	fwrite("arr",strlen(arr),1,stdout);
	
	return 0;


字符串并没有打印到显示器上,而是被写入到了myfile文件中。

为什么呢?

在这之前先要了解,C中的stdin、stdout、stdree这三个流都是FILE*型的,并且这三这FILE中的文件描述符被固定为0、1、2。这就是为什么在C中用stdin、stdout、stderr就能找到键盘、显示器、显示器。

而在上面代码中关闭了1,myfile的文件描述符是1,所以在fwrite函数中用stdout还是写入到了myfile文件中。这就是为什么显示器上没有打印,而写入到了myfile文件的原因。而通过这段代码,我们现在应该要了解FILE*是什么了。

最后,fopen究竟做了什么?
1、给调用的用户申请struct FILE结构体变量,并返回地址(FILE*)
2、在底层通过open打开文件,并返回fd,把fd填充进FILE变量的fileno中。

缓冲区

有两段代码:

#include<stdio.h>
#include<unistd.h>
void A()

	printf("hello linux\\n");
	sleep(3);


void B()

	printf("hello linux");
	sleep(3);


int main()

	A();
	B();
	return 0;

其中A是先显示hello Linux,再等待3秒。
B是先等待3秒,再显示hello Linux。

把内容回显给显示器时,内容先被写入缓冲区,采用的是行缓存,当遇到\\n时就会刷新缓冲区,把内容写人显示器中,当缓冲区内容被写满时也会刷新缓冲区。

缓冲区有:

  1. 无缓冲
  2. 行缓冲:遇到\\n就会把缓冲区\\n之前的内容刷新出来,否则等缓冲区写满。效率和和用性做的平衡。
  3. 全缓冲:等缓冲区写满

(常见对显示器内容刷新,用的是行缓存,这样才能更快的看到我们的内容)

看代码

#include<stdio.h>
#include<unistd.h>
#include<string.h>

int main()

  printf("hello printf\\n");
  fprintf(stdout,"hello fprintf\\n"); 
  const char*mag2="hello write\\n";
  write(1,mag2,strlen(mag2));
  fork();
  return 0;

运行结果是:

hello printf
hello fprintf
hello write

但是我们对该进程进行输入重定向到文件中:./a.out > myfile

hello write
hello printf
hello fprintf
hello printf
hello fprintf


因为

当我们重定向后,文件描述符1已经不表示显示器了,而是我们的文件。这时候,缓冲区采用的是全缓存。
系统调用的IO接口是无缓冲,可以直接写入。
当缓冲区中存放了“hello printf\\n”和“hello fprintf\\n”时,创建了子进程,在return 0;之前进行了写时拷贝,所以最后打印了两次字符串。

这里面的缓冲区是C提供的,也是由FILE结构体进行维护。
缓冲区是在内存中的,在用户层。
缓冲区的数据刷新不是直接刷新到文件中,而是要经过内核区再写入到文件,这里有OS自己的刷新机制,这里不谈(我还没学到,哈哈)。

fclose和close

fclose:在关闭1之前,刷新了C中的缓冲区。内容可以被写入到文件内。

#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<string.h>
#include<unistd.h>

int main()

  close(1);
  int fd=open("myfile",O_WRONLY|O_CREAT,0666);
  char *arr="hello linux\\n";
  fprintf(stdout,arr);
  fclose(stdout);
  return 0;


close:由于采用了全缓,当close(1)时,系统调用的看不见C的缓冲区,没有刷新缓冲区就关掉了,故没有写入到文件中。

#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<string.h>
#include<unistd.h>

int main()

  close(1);
  int fd=open("myfile",O_WRONLY|O_CREAT,0666);
  char *arr="hello linux\\n";
  fprintf(stdout,arr);
  //fflush(stdout);//可以在调用close之前,先刷新缓冲区。
  fclose(stdout);
  return 0;


调用fclose是先调用ffiush,再调用close。

✔dup2系统调用

在上面的重定向中,我们要先close(1),再打开文件,这样好繁琐。我们有一个更简单的方法。

int dup2(int oldfd, int newfd);
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<string.h>
#include<unistd.h>

int main()

  int fd=open("myfile",O_WRONLY|O_CREAT,0666);  
  char *arr="hello linux\\n"; 
  close(1);
  dup2(fd,1);  
  printf("%s",arr);
  return 0;

✔理解文件系统

文件系统是Linux的一个重要部分,在Linux中玩了有一段时间,我一直有一个困惑,文件是怎么创建出来的?通过学习,慢慢的我自己有了一点了解。

文件=文件的属性+文件的内容,我们在查看文件大小时,显示的是文件内容的大小,其属性信息并没有算在里面,这说明了,文件的属性和文件的内容是分离存储的,在磁盘上。文件属性叫做元信息。

我们先简单了解磁盘:
磁盘有扇区、磁道、柱面、磁头……
文件的写入到磁盘中,会对磁盘寻址,其中会对柱头、磁道、扇面来寻找要写入的内容的地方。

假设磁盘的大小为500GB,对这么大的空间进行管理,系统采用了分区(就像中国也有省,市,县一样)

inode

inode是任何一个文件属性的集合,Linux中几乎每一个文件都有一个inode编号。

文件的元信息就是保存在inode中的,inode是一个结构体。

上图为磁盘文件的系统图(内核内存映射肯定有所不同),磁盘是一个典型的块设备,磁盘的分区被划分为一个个block。一个block的大小是由格式化的时候确定的,并且不可以更改。例如mke2fs的-b选项可以设定block大小为1024、2048或4096字节。而上图中启动块(Boot Block)的大小是确定的。

  • Block Group:ext2文件系统会根据分区的大小划分为数个Block Group。而每个Block Group都有着相同的结构组成。如政府管理各区的例子
  • 超级块(Super Block):存放文件系统本身的结构信息。记录的信息主要有:bolck 和 inode的总量,未使用的block和inode的数量,一个block和inode的大小,最近一次挂载的时间,最近一次写入数据的时间,最近一次检验磁盘的时间等其他文件系统的相关信息。Super Block的信息被破坏,可以说整个文件系统结构就被破坏了
  • GDT,Group Descriptor Table:块组描述符,描述块组属性信息,有兴趣的可以在了解一下
  • 块位图(Block Bitmap):Block Bitmap中记录着Data Block中哪个数据块已经被占用,哪个数据块没有被占用
  • inode位图(inode Bitmap):每个bit表示一个inode是否空闲可用。
  • inode Table:存放文件属性 如 文件大小,所有者,最近修改时间等
  • 数据区(Data blocks):存放文件内容

数据区中是一个一个的块,每个块的大小是4KB,用来存放数据。(存在多级索引,我还没学,就不讲了)

inode结构体中有一个数组(int block[12])记录块(Data blockse)的位置。
一个普通文件的创建。

先要去inode位图中找到未被使用的inode,并申请下来把文件的属性记录其中,如果要对该文件写入内容,则系统会根据内容的大小去块位图中申请所需要的空闲块,并写入内容。内核在inode上的磁盘分布区记录了上述块列表。之后,内核会把该文件的inode编号和文件名添加到所在目录文件中。该文件的inode编号和该文件的文件名对应起来。

目录的创建

目录也是文件,也有自己的inode编号。目录在创建的过程中和上面普通文件的创建有点类似,不同的是,目录文件的内容是存放目录下的文件名和inode指针,使这些文件名和inode指针一一对应起来。

ls 命令:

ls -l 命令

这也可以看出,目录和文件之前的联系。

文件的删除

文件的删除并没有那么复杂,只要把对应inode的位图中的数据改掉(把1置成0),对应块位图数据也修改掉(把1置成0)。这也就是为什么删除的文件可以恢复过来的原因,只要把位图再置回来。

创建一个新文件主要有一下4个操作:

  1. 存储属性
    内核先找到一个空闲的i节点(这里是263466)。内核把文件信息记录到其中。
  2. 存储数据
    该文件需要存储在三个磁盘块,内核找到了三个空闲块:300,500,800。将内核缓冲区的第一块数据复制到300,下一块复制到500,以此类推。
  3. 记录分配情况
    文件内容按顺序300,500,800存放。内核在inode上的磁盘分布区记录了上述块列表。
  4. 添加文件名到目录新的文件名abc。linux如何在当前的目录中记录这个文件?内核将入口(263466 abc)添加到目录文件。文件名和inode之间的对应关系将文件名和文件的内容及属性连接起来。

硬链接

ln 文件名 要创建的文件名


这两个文件的inode号相同,说明myfile-s不是一个独立的文件,只是在目录的数据中添加了一个新文件名,该文件名对应的ionde和myfile相同。

硬连接数

硬连接数的数量是,有多少个文件对应的inode编号相同。
myfile文件和myfile-s文件的inode编号相同,所以硬连接数位2。

想要释放这个文件对应磁盘空间释放,要把硬连接数变成0。删除一个相同inode编号的文件,该硬连接数-1。

也就是说:

  • abc和def的链接状态完全相同,他们被称为指向文件的硬链接。内核记录了这个连接数,inode
    263466 的硬连接数为2。

  • 我们在删除文件时干了两件事情:1.在目录中将对应的记录删除,2.将硬连接数-1,如果为0,则将对应的磁盘释放。

软链接

ln -s 文件名 要创建的文件名


硬链接是通过inode引用另外一个文件,软链接是通过名字引用另外一个文件。

文件的三个时间

用stat 文件名可以查看

  • Access 最后访问时间
    Modify 文件内容最后修改时间
    Change 属性最后修改时间

以上是关于Linux学习基础IO——系统调用 | 文件描述符fd | 重定向的主要内容,如果未能解决你的问题,请参考以下文章

Linux入门基础IO

Linux下的基础IO

《Linux从0到99》九 基础IO

《Linux从0到99》九 基础IO

《Linux从0到99》九 基础IO

Linux篇第九篇——基础IO(系统文件IO+文件描述符+重定向+文件系统+软硬链接)