Linux 基础IO——文件(中)

Posted 风起、风落

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux 基础IO——文件(中)相关的知识,希望对你有一定的参考价值。

文章目录

1. 文件描述符为什么从3开始使用?

修改test.c文件内容

#include<sys/types.h>    
#include<sys/stat.h>    
#include<fcntl.h>    
#include<stdio.h>    
#include<unistd.h>    
#include<errno.h>    
#include<string.h>    
#define LOG "log.txt"        
int main()    
    
  umask (0);//将权限掩码设置成0        
    
  int fd=open(LOG, O_RDONLY  );//打开一个文件,若文件不存在则重新创建一个        
  if(fd==-1)//说明打开失败        
      
    printf("fd:%d,errno:%d,errstring:%s\\n",fd,errno,strerror(errno));//打印出错误信息        
      
  else    
  printf("fd :%d\\n",  fd);                                                                                                                                                                  
 char buffer[1024];    
 ssize_t n= read(fd,buffer,sizeof(buffer)-1);//使用系统接口来进行IO的时候,一定要注意\\0的问题    
 if(n>0)//成功了,实际读到了多少字节    
     
  buffer[n]='\\0';    
  printf("%s\\n",buffer);    
     
  close(fd); //关闭文件        
  return 0;    
 

运行可执行程序,发现文件描述符返回的是3

但为啥是3,不是0 ,1,2
任何一个进程,在启动的时候,默认会打开当前进程的三个文件:
标准输入、标准输出、标准错误 ——本质都是文件
C语言:标准输入(stdin) 标准输出(stdout) 、标准错误(stderr) ——文件在系统层的表现
C++: 标准输入(cin) 标准输出(cout) 、标准错误(cerr) ——文件在系统层的表现,它是一个类


因为Linux下一切皆文件,所以向显示器打印,本质就是向文件中写入
标准输入—设备文件—>键盘文件
标准输出—设备文件—> 显示器文件
标准错误—设备文件—> 显示器文件


创建test.cc文件(cc后缀即cpp代码)

#include<iostream>      
#include<cstdio>//写C++时,使用C++风格的C语言代码      
int main()      
      
  //C语言      
  printf("hello printf->stdout\\n");//向stdout进行输出      
  fprintf(stdout,"hello printf->stdout\\n ");//将数据向stdout进行输出      
  fprintf(stderr,"helllo printf->stderr\\n");//将数据向标准错误打印      
      
      
  //C++      
  std::cout<<"hello cout->cout"<<std::endl;//表示标准输出      
  std::cerr<<"hello cerr->cerr"<<std::endl;//向标准错误中打印数据      
      
return 0;      
 

输出重定向是将标准输出重定向,此时log.txt文件中只会存在标准输出的内容
所以标准输出和标准错误都会向显示器打印,但是其实是不一样的

0默认对应标准输入,1默认对应标准输出、2默认对应标准错误


修改myfile.c文件内容

#include<sys/types.h>    
#include<sys/stat.h>    
#include<fcntl.h>    
#include<stdio.h>    
#include<unistd.h>    
#include<errno.h>    
#include<string.h>    
#define LOG "log.txt"        
int main()    
    
int fd1=open(LOG,O_WRONLY |O_CREAT | O_TRUNC,0666);    
int fd2=open(LOG,O_WRONLY |O_CREAT | O_TRUNC,0666);    
int fd3=open(LOG,O_WRONLY |O_CREAT | O_TRUNC,0666);    
int fd4=open(LOG,O_WRONLY |O_CREAT | O_TRUNC,0666);    
int fd5=open(LOG,O_WRONLY |O_CREAT | O_TRUNC,0666);    
int fd6=open(LOG,O_WRONLY |O_CREAT | O_TRUNC,0666);    
printf("%d\\n",fd1);    
printf("%d\\n",fd2);    
printf("%d\\n",fd3);    
printf("%d\\n",fd4);    
printf("%d\\n",fd5);    
printf("%d\\n",fd6);                                                                                                                                                                         
  return 0;    

运行可执行程序,发现 打印结果为 3 4 5 6 7 8
因为 标准输入、标准输出、标准错误分别占用了0 、1、2,所以只能从3开始

文件描述符(open对应的返回值)本质就是数组的下标

2. 文件描述符本质理解


启动代码时就会变成一个进程,该进程在内核中就必须有自己的数据结构 struct task_struct,
称之为当前进程所对应的进程描述符
打开文件时,操作系统会把文件加载到内存里,以供CPU通过进程的方式来访问对应的文件

任何一个进程,在启动的时候,默认会打开进程的三个文件,系统中一定会存在大量被打开的文件,这些文件一定会被操作系统管理起来,通过先描述,在组织,创建 struct file 结构体,该结构体一定包含文件属性等,每一次创建并打开文件时,都是在内核中创建一个struct file的结构体

目前认为只要找到file,就可以找到所有文件内容
为了维护一个进程和多个文件的映射关系,在内核中定义了数据结构struct files_struct,该结构体内部有一个数组struct file* fd [ ] ,是一个内容为struct file*的数组
当进程初始化时,会创建struct files_struct 结构体,通过结构体找到数组,只要有数组一定有下标

3. 如何理解Linux下的一切皆文件?


内存把数据写到显示器上,属于写入的过程,读取是从键盘中读取的,键盘输入后,操作系统把输入的数据回显到显示器上了,所以显示器只能负责打印

不同的硬件所对应的方法是完全不一样的,打开键盘时,操作系统内部会创建struct file对象

将键盘的read方法和 write方法 保存到函数指针中

每一个设备也只需要把方法的地址放入函数指针中
在当前进程看来,所有的东西都是文件对象,要有数据放到缓冲区里,底层读写时只需要调用对应的方法,来完成对应的读写,不关心底层的差异化
操作系统也有自己的wirte和read,本质上是拷贝,将应用层的数据拷贝到缓冲区里,在调用底层不同设备的方法,所以看起来就是Linux下一切皆文件

4. FILE是什么,谁提供?和内核的struct有关系么?

操作系统层面,必须要访问fd,才能找到文件
任何语言层访问外设或者文件必须经历操作系统
FILE *fopen(const char *path, const char *mode);

FILE是一个结构体,FILE由C语言提供的


C语言动态库


C语言头文件

证明struct FILE结构体中存在文件描述符fd

#include<sys/types.h>      
#include<sys/stat.h>      
#include<fcntl.h>      
#include<stdio.h>      
#include<unistd.h>      
#include<errno.h>      
#include<string.h>      
#define LOG "log.txt"          
int main()      
    
printf("%d\\n",stdin->_fileno);//fileno代表文件描述符    
printf("%d\\n",stdout->_fileno);    
printf("%d\\n",stderr->_fileno);    
FILE*fp=fopen(LOG,"w");    
printf("%d\\n",fp->_fileno);                                                                                                                                                                 
                                                                                                                                                      
  return 0;                                                                                                                                           
   

说明结构体struct FILE内部存在文件描述符
同时因为0 1 2 被占用了,所以我们自己写的文件描述符返回3

5. 重定向的本质

关闭文件描述符0后,发现从0开始可以被输出了


关闭文件描述符0和2后,发现0和2都可以被使用了


进程中,文件描述符的分配规则:在文件描述符表中,最小的,没有被使用的数组元素分配给新文件

输出重定向

若不关闭文件描述符1,当前printf打印的结果显示到显示器上面


关闭文件描述符1,再打开新的文件log.txt


此时运行可执行程序没有显示出you can see me,打开新文件发现本来应该打印到显示器的内容,打印到log.txt中了

本来应该打印到显示器上的内容,打印到文件里 ,这种现象叫做重定向


在文件描述符表中,最小的,没有被使用的数组元素分配给新文件,所以把文件描述符1分配给了log.txt

1号下标里面的地址填成了log.txt文件的地址,上层printf打印它知道吗?
不知道,它也不关心,它只认文件描述符1

重定向的原理:在上层无法感知的情况下,在OS内部 ,更改进程内部对应的文件描述符表中,特定下标的指向

输入重定向

先在log.txt文件中输入内容 123 456
修改myfile.txt文件内容

关闭文件描述符0,所以scanf读取时会读取log.txt文件中的内容

读取的内容与log.txt文件内容相同


本来要从键盘中读取,结果现在要在文件中读取,这叫做输入重定向

追加重定向

关闭文件描述符1后,导致printf不会打印在显示器上,而是追加到log.txt文件中

运行可执行程序,无显示,都追加到log.txt文件中


重定向函数 ——dup2

输入 man dup2 查看

刚刚重定向时,需要先关闭文件描述符1,再打开文件
现在可以直接将文件打开,使用dup2重定向
输出重定向对应的文件描述符是1
打开myfile文件,假设其文件描述符是fd
newfd为oldfd的一份拷贝,最后只剩下oldfd
dup2(fd,1)


将3号描述符里面的内容拷贝到1里面,用3号内容覆盖1号内容,此时1号描述符就不再指向标准输出了,转而指向myfile文件,写入1的内容,就会写入文件中


把本来应该显示到标准输出的内容,显示到log.txt文件中


此时printf打印内容显示到log.txt文件中


6. 如何理解缓冲区?

修改myfile.c文件的内容

#include<sys/types.h>    
#include<sys/stat.h>    
#include<fcntl.h>    
#include<stdio.h>    
#include<unistd.h>    
#include<errno.h>    
#include<string.h>    
#define LOG "log.txt"            
int main()    
    
  //C库    
fprintf(stdout,"hello world\\n");    
//系统调用    
 const char*msg="hello write\\n";    
 write(1,msg,strlen(msg));    
 fork();
  return 0;    

运行可执行程序只有两行信息,但是重定向到log.txt文件后,打印出三行信息,说明重复打印了


若将fork函数注释掉后,发现 两者显示结果相同\\


struct FILE除了会封装fd之外,还会预留一部分输出缓冲区
当把字符串想写入stdout中时 ,struct FILE除了fd,还有一部分缓冲区
当我们想写的时候,并不是把数据拷贝到操作系统内部,而是把数据放到缓冲区当中
此时这个fprintf函数会直接返回
C库会结合一定的刷新策略,将缓冲区中的数据写入操作系统(write(FILE->fd,xxxx))


刷新策略:
1.无缓冲 (不提供缓冲)
2.行缓冲
如果碰到\\n,就会把\\n在内之前的内容刷新出来
3. 全缓冲
只有把缓冲区写满的时候,才会刷新缓冲区
显示器采用的刷新策略:行缓冲
普通文件采用的刷新策略:全缓冲


为什么要有缓冲区?
节省调用者的时间
系统调用也会花费时间
可能写了10次,如果每次调用fprintf传给操作系统 都要花费时间
但若都写入缓冲区中,统一传给操作系统 效率就变高了


write接口不论有没有重定向,都会正常打印,因为调用write是系统调用 没有缓冲区,直接调用就写给操作系统了
而使用fprintf ,数据会先写入缓冲区
当要打印到显示器中时 刷新策略:行缓冲
因为打印的内容都存在\\n,在调用fork时,打印的内容已经在缓冲区中被刷新走了,刷新之后在fork就没有任何意义了
所以fork就什么也没干

当打印到普通文件时 刷新策略:全缓冲
使用 hello world 没办法把缓冲区写满,就无法刷新,父子两个进程都要刷新
刷新就要对缓冲区做清空,即对数据做修改,此时谁先刷新就先发生写时拷贝,所以最终就会打印两次相同数据

Linux基础IO-IO接口,文件描述符,重定向

【Linux】基础IO

一、C语言中文件IO操作

1.C语言中的开关读写文件

在学习Linux中的IO操作之前,我们先来简单的回顾一下,C语言中我们常用的一些IO操作的接口。

1.1.fopen()

FILE* fopen(const char* path, const char* mode);
  • 函数参数
    • path:要打开的文件
    • mode:打开文件的方式
      • r:可读方式
      • r+:可读可写方式
      • w:可写方式,如果文件不存在,就创建一个文件。如果文件已经存在,就截断一个文件(清空文件内容)
      • w+:可读可写方式,如果文件不存在,就创建一个文件。如果文件已经存在,就截断一个文件(清空文件内容)
      • a:追加写,但是不可以读取内容。如果文件不存在,就创建一个文件。如果文件已经存在,就在文件的末尾开始追加写
      • a+:追加写,可以读取内容。如果文件不存在,就创建一个文件。如果文件已经存在,就在文件的末尾开始追加写
  • 函数返回值
    • 成功:返回一个文件流指针FILE
    • 失败:返回NULL

作用:以某种方式打开一个文件,并返回一个指向该文件的文件流指针。

1.2.fclose()

int fclose(FILE* fp);

作用:关闭传入的文件流指针指向的文件。

1.3.fwrite()

size_t fwrite(void* ptr, size_t size, size_t nmemb, FILE* stream);
  • 函数参数
    • ptr:写入文件的内容
    • size:往文件中写入的块的大小,单位为字节
    • nmemb:预期写入的块数
    • stream:预期写入文件的文件指针
  • 函数返回值
    • 成功:写入文件中的块数
  • 常见用法
    • 定义块的大小为1个字节,nmemb为向写入的字节数量,返回值为成功写入的字节数
#include <cstdio>
#include <cstring>
#include <cstdlib>

int main()
{
    FILE* fp = fopen("myfile.txt", "w");
    if (!fp) {
        perror("fopen");
        exit(-1);
    }

    const char* msg = "hello Linux file\\n";
    fwrite(msg, sizeof(char), strlen(msg), fp);

    fclose(fp);
    return 0;
}

运行结果:

注意:fopen()中的path不是执行程序的所处的路径,而是进程运行时做出的路径。

举例:

1.4.fread()

size_t fread(void* ptr, size_t size, size_t nmemb, 	FILE* stream);
  • 函数参数
    • ptr:将从文件读取的内容保存在ptr所指向的空间中
    • size:定义读文件时块的大小,单位为字节
    • nmemb:期望从文件中读的块数
    • stream:预期读取文件的文件指针
  • 函数返回值
    • 成功从文件中读取的块的个数
  • 常见用法
    • 定义块的大小为1个字节,nmemb为向写入的字节数量,返回值为成功读取的字节数
#include <cstdio>
#include <cstring>
#include <cstdlib>

int main()
{
    FILE* fp = fopen("myfile.txt", "r");
    if (!fp) {
        perror("fopen");
        exit(-1);
    }

    char buff[64];
    fread(buff, sizeof(char), sizeof(buff) / sizeof(char), fp);
    printf("%s", buff);

    fclose(fp);
    return 0;
}

运行结果:

2.stdin&&stdout&&stderr

默认情况下,C语言会自动打开两个输入输出流,分别是stdinstdoutstderr

这三个流的类型都是FILE,也就是文件指针类型。

既然是文件指针,所以这三个指针分别指向键盘,显示器,显示器。后面的系统IO会再详细的讲解这三个输入输出流。

3.三个标准流和IO接口

可以利用上面这三个标准流和C语言的IO接口,将字符串直接打印到显示器上。

#include <cstdio>
#include <cstring>
#include <cstdlib>

int main()
{
    FILE* fp = fopen("myfile.txt", "w+");
    if (!fp) {
        perror("fopen");
        exit(-1);
    }

    char buff[64];
    fread(buff, sizeof(char), 12, stdin);// 从键盘中输入到buff中

    fwrite(buff, sizeof(char), strlen(buff), stdout); // 从buff中写入到显示器上

    fclose(fp);
    return 0;
}

运行结果:

二、系统文件IO

其实除了C语言之外,很多语言都是自己的IO接口函数,但是下面我们要谈论的就是系统给我们提供的IO接口,也就是系统级别的IO接口。

1.系统级别的开关读写文件

1.1.open()

// 在打开的文件已经存在的时候
int open(const char* pathname, int flags);
// 在打开的文件不存在的时候
int open(const char* pathname, int flags, mode_t mode);
  • 函数参数
    • pathname:需要打开的文件
    • flags:打开文件的方式
      • 必选项
        • O_RDONLY:只读方式
        • O_WRONLY:只写方式
        • O_RDWR:读写方式
      • 可选项
        • O_TRUNC:截断文件(清空文件内容)
        • O_CREAT:文件不存在则创建文件
        • O_APPEND:追加方式
        • O_EXXL | O_CREAT:如果文件存在,则打开文件失败
      • 原理
        • 可以使用按位或的方式进行组合:如打开并创建只写文件O_WRONLY | O_CREAT
        • 本质是利用了位图的方式来表示每一种的方式
    • mode:当打开一个新开的文件的时候,需要给一个文件设置权限,需要设置一个8进制的数字。这个和umask也会有关系
  • 函数返回值
    • 成功:返回一个文件描述符(后面介绍)
    • 失败:返回-1

作用:打开一个文件

1.2.close()

int close(int fd);

  • 函数参数
    • fd:文件描述符

作用:关闭一个文件

1.3.write()

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

  • 函数参数
    • fd:文件描述符
    • buf:将buf中的内容写到文件中
    • count:期望写入的字节数
  • 返回值
    • 返回的字节数

代码示例:

#include <cstdio>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <cstring>
#include <cstdlib>

int main()
{
    // 创建一个权限为666的权限
    umask(0);
    int fd = open("file.txt", O_WRONLY | O_CREAT, 0666);
    if (fd < 0) {
        perror("open");
        exit(1);
    }
    // 将msg写入file.txt中
    const char* msg = "I am studing Linux IO\\n";
    write(fd, msg, strlen(msg));
    close(fd);
    return 0;
}

运行结果:

1.4.read()

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

  • 函数参数
    • fd:文件描述符
    • buf:将文件中的内容读到buf中
    • count:期望写入的字节数
  • 返回值
    • 返回的字节数

代码示例:

#include <cstdio>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <cstring>
#include <cstdlib>

int main()
{
    int fd = open("file.txt", O_RDONLY);
    if (fd < 0) {
        perror("open");
        exit(1);
    }
    char buff[64];
    read(fd, buff, sizeof(buff));
    printf("%s", buff);
    close(fd);
    return 0;
}

运行结果:

2.系统文件IO VS C文件IO

上面的fopenfclosefreadfwrite都是C标准库中的函数,我们统称为库函数。

openclosereadwrite都是系统提供的接口,我们称之为系统调用接口。

如下图:

右下图可知,系统调用接口在lib库函数之下,所以库函数中其实求出对系统调用接口的二次封装。

因为库函数是系统函数的一层封装,因此库函数对文件操作的时候,必然会使用系统调用接口。每打开一个文件所获得的文件指针FILE都有一个文件描述符fd与之对应。

为什么不适用系统调用接口,而是只使用库函数的IO调用接口?

1.虽然库函数有函数调用的开销,但是系统调用比库函数调用还要慢,因为它需要把上下文环境切换成为内核模式。

2.系统调用与操作系统是相关的,所以系统调用接口没有跨平台的可移植性。

3.一般读写文件都是要操作大量的数据,而库函数调用要大大减少系统调用的次数。这是因为缓冲区的技术,内核缓冲区是全缓冲,只有当缓冲区写完之后或者结束之后,才会将缓冲区中的内容写入文件中。

3.文件描述符fd

​ 在上面open的接口中,我们提到了fd,这个也是open接口的返回值。而writeread接口也是通过fd这个参数使得文件可以读写,可以说fd是整个系统IO的灵魂,所以接下来,我们需要好好地理解一下fd

3.1.什么是文件描述符

在Linux下一切皆文件,而大量的文件需要被高效的组织和管理,因此就诞生了文件描述符fd(file descriptor)。

文件描述符是内核为高效的管理已经被打开的文件所创建的索引,它是一个非负整数,用于指代被打开的文件,所有执行I/O操作的系统调用都是通过文件描述符完成的。

进程和文件之间的对应关系是如何建立的?

由图可知:文件描述符就是从0开始的正整数。但我们打开一个文件的时候,操作系统都需要创建一个数据结构来描述这个文件。所以struct file结构体就应运而生了,它就是表示打开的一个文件对象。

当进程执行open函数的时候,必须要让进程和文件关联起来。所以在每一个进程的PCB中都是一个struct files_struct* files指针,它指向一张表files_struct,这个表中有一个指针数组fd_array[],其中指针数组的每一个元素都是一个指向struct file结构的struct file*指针,而这个文件指针就指向打开的文件。

注意:向文件写入数据后,数据其实先写入对应文件的缓冲区当中,只有当将缓冲区中的内容刷新到磁盘当中时才算真正地写入到文件当中。

小总结:

  • 所以本质上文件描述符就是struct file_struct结构中fd_array数组的下标。而只要拿到了这个文件描述符,就可以找到对应的文件

什么是进程创建会默认打开文件的0,1,2?

在Linux中,进程是通过进程描述符fd来访问文件的,文件描述符实际上是一个整数。在程序刚启动的时候,默认有三个文件描述符,分别是:0(代表标准输入stdin),1(代表标准输出stdout),2(代表标准错误stderr)。对应的物理设备就是:键盘,显示器,显示器

这三个文件设备都有自己对应的struct file系统会默认的生成这三个结构体,并使用双链表将他们连接起来,并且将struct file的地址放入到struct file* fd_array[]数组的对应在0, 1, 2位置上。这个默认生成结构体并将地址放在fd_array数组的过程就叫做默认打开了标准输入流,标准输出流和标准错误流。

补充:磁盘文件和内存文件的区别?

上面说的都是在操作进程打开的文件,正是因为操作系统中有大量的进程打开了大量的文件,所以需要使用struct filestruct files_struct这样的结构体去管理这些文件。而这些文件都是在内存中加载的文件,所以我们称之为**「内存文件」**。

如果一个文件储存在磁盘当中,我们就称之为「磁盘文件」。这两种文件的关系就是当一个磁盘文件被加载到内存当中的话,就成为了内存文件。

磁盘文件由两部分构成:「文件内容」和「文件属性」。文件内容就是文件中的数据内容,而文件属性(元信息)就是一个文件的基本信息。这就像是去超市买一盒牛奶,其中的牛奶就是文件内容,而牛奶的包装盒上的牛奶成分分析表就是文件属性。(在后面的文件系统中,还会详细地介绍磁盘文件)

文件加载到内存时,一般先加载文件的属性信息,然后将文件内容放入缓冲区中,延后式的慢慢加载内存。

3.2.如何创建文件描述符

进程通过文件描述符最常见的方式就是通过系统调用接口open或者是从父进程继承过来的。

虽然文件描述符对于每一个进程的PID都是唯一的,但是每一个进程都是一个进程描述表struct files_struct,用于管理进程描述符,当使用fork创建子进程的时候,子进程会获得父进程进程描述表的一个副本,所以子进程可以拿到父进程的进程描述符,因此就可以打开父进程所有的文件。

3.3. 文件描述符的分配规则

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

如果再打开一个新的文件的话,就分配一个最小的没有使用的文件描述符fd。因为默认打开了0, 1, 2,所以新的文件描述符就应该从3开始的。

**思考:因为文件描述符也是可以使用close关闭掉的,所以如果我们先将stdin对应的0关闭掉的话,然后在此时打开一个新的文件的话,则这个文件对应struct file的文件描述符就应该是0,此时这个文件就变成了标准输入。**如果我们向标准输入中输入一些内容的话,其实就输入到了这个文件当中。这个原理就和重定向的原理很像,只不过重定向要比这个原来还要复杂一点,但是这个可以帮助我们学习重定向。

#include <cstdio>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <cstring>
#include <cstdlib>

int main()
{
    close(0); // 关闭stdin
    int fd = open("file.txt", O_RDONLY);
    if (fd < 0) {
        perror("open");
        exit(1);
    }
    printf("%d\\n", fd);
    close(fd);
    return 0;
}

运行结果:

3.3. 文件描述符与打开文件之间的关系

每一个文件描述符都对应着一个打开的文件,同时,不同的文件描述符也可以指向同一个文件。

同一个文件可以被同一个进程打开多次,也可以被不同的进程打开。

系统为每一个级进程都创建了一个文件描述表。内核中维护了三种文件描述表。

1.进程级别的文件描述表

进程级别的文件描述符表的每一条目都记录了单个文件描述符的相关信息。

struct files_struct {
atomic_t count; /* 共享该表的进程数 */
rwlock_t file_lock; /* 保护以下的所有域,以免在tsk->alloc_lock中的嵌套*/
int max_fds; /*当前文件对象的最大数*/
int max_fdset; /*当前文件描述符的最大数*/
    int next_fd;*已分配的文件描述符加1*/
struct file ** fd; /* 指向文件对象指针数组的指针 */
fd_set *close_on_exec; /*指向执行exec( )时需要关闭的文件描述符*/
fd_set *open_fds; /*指向打开文件描述符的指针*/
fd_set close_on_exec_init;/* 执行exec( )时需要关闭的文件描述符的初 值集合*/
        fd_set open_fds_init; /*文件描述符的初值集合*/
struct file * fd_array[32];/* 文件对象指针的初始化数组*/
};

2.系统级别的文件描述表

内核对所有打开的文件有一个系统级别的文件描述符表。有时也称为打开文件描述符表,并将表格中各条目称为打开文件句柄。一个打开文件句柄存储了这个打开文件的全部相关信息。

struct file
{
struct list_head f_list; /*所有打开的文件形成一个链表*/
struct dentry *f_dentry; /*指向相关目录项的指针*/
struct vfsmount *f_vfsmnt; /*指向VFS安装点的指针*/
struct file_operations *f_op; /*指向文件操作表的指针*/
mode_t f_mode; /*文件的打开模式*/
loff_t f_pos; /*文件的当前位置*/
unsigned short f_flags; /*打开文件时所指定的标志*/
unsigned short f_count; /*使用该结构的进程数*/
unsigned long f_reada, f_ramax, f_raend, f_ralen, f_rawin;
/*预读标志、要预读的最多页面数、上次预读后的文件指针、预读的字节数以及
预读的页面数*/
int f_owner; /* 通过信号进行异步I/O数据的传送*/
unsigned int f_uid, f_gid; /*用户的UID和GID*/
int f_error; /*网络写操作的错误码*/

unsigned long f_version; /*版本号*/
void *private_data; /* tty驱动程序所需 */

};

3.文件系统的inode

作用:保护了文件系统的相关信息。

不同级别的文件表述表的关系:

3.4.文件描述符fd与文件指针FILE的区别

在linux系统中打开文件就会获得文件描述符,它是一个数组的下标。每个进程控制块(PCB)中保存着一份文件描述符表,文件描述符就是文件描述符表的索引,每个表项都有一个指向打开文件的文件指针,这个文件指针指向进程用户区中的一个被称为FILE的数据结构。FILE结构包含一个缓冲区和一个文件描述符。而文件描述符是文件描述符表的一个索引。

4.重定向

理解了文件描述符后,就可以讲一讲重定向的原理了。

4.1.重定向原理

**简单来说:重定向的原理就是修改了文件描述符和打开文件的对应关系。**接下来的三个例子会进一步的帮助你理解这句话。

4.1.1.输入重定向原理

原本文件描述符指向标准输入流文件,而现在我们先将标准输入流文件关闭,然后再打开一个文件,这时文件描述符0就分给了新打开的文件了。

举例:

scanf默认是从标准输入中获取内容,如果打开的文件的文件符为0的话,那么就从打开的文件中获取内容。

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

int main()
{
    close(0);
    int fd = open("file.txt", O_RDONLY | O_CREAT, 0644);

    if (fd < 0) {
        perror("open");
        exit(1);
    }

    char str[1024];
    while (scanf("%s", str) != EOF) 
        printf("%s", str);

    close(fd);
    return 0;
}

运行结果:

4.1.2.输出重定向

原本文件描述符指向标准输出流文件,而现在我们先将标准输出流文件关闭,然后再打开一个文件,这时文件描述符1就分给了新打开的文件了。


[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bIkuVqc4-1635688996871)(D:\\github\\gitee\\linux-study\\【Linux】基础IO.assets\\1635427605224.png)]

举例:

printf标准输出默认是往显示器上打印内容,如果打开的文件的文件符为1的话,那么就往打开的文件中打印内容。

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

int main()
{
    close(1);
    int fd = open("file.txt", O_WRONLY | O_CREAT, 0644);

    if (fd < 0) {
        perror("open");
        exit(1);
    }
    
    linux运维基础2

Linux运维之路 基础篇:Linux基础命令

linux学习13 Linux运维常用文件管理命令及系统变量基础

linux运维基础文件管理

运维基础--Linux中bash的基础特性

Linux运维应该怎么去学习?