Linux基础IO——系统文件IO&&fd&&重定向

Posted 沐曦希

tags:

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

大家好我是沐曦希💕

文章目录

一、前言

1.重新谈论文件

空文件,也要在磁盘占据空间,因为文件也有属性,属性也属于数据,需要空间进行存储。所以文件包括内容和属性

所以对文件操作就是对内容或者对属性进行操作,或者对内容和属性进行操作。

文件具有唯一性,所以在标定一个文件时候,必须使用文件路径加上文件名。如果没有指明对应的文件路径,默认是该文件在当前路径,从而进行文件访问。
当我们把fopen,fclose,fread,fwrite等接口写完之后,代码编译之后,形成二进制可执行程序之后,但是没有运行,文件对应的操作就没有被执行,所以对文件的操作本质是进程对文件的操作!

一个文件如果没有被打开,不可以直接进行文件访问,一个文件要被访问,就必须先被打卡开(由用户进程+OS打开)

注意:不是所有磁盘的文件都可以被打开

文件分为被打开的文件和没有被打开的文件,而文件由进程打开,所以文件操作的本质:进程和被打开文件的关系

2.重新谈论文件操作

不同语言有不同文件操作接口,但底层都一样。
因为文件存储在磁盘中,而磁盘属于硬件,而OS管理软硬件,所以所有人想要访问磁盘不能绕过OS,必须使用OS提供的文件级别的系统调用接口来访问磁盘,但是操作系统始终只有一个。
所以上层语言无论如何变化:库函数底层必须调用系统调用接口,库函数可以千变万化,但是底层不变。

二、回归C文件接口

1.打开和关闭

对于C语言文件操作,首先要打开文件

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


mode参数是以mode形式打开文件,mode取值:


打开失败将会返回NULL ,而打开成功则返回文件的指针(FILE*)。最后我们则需要关闭(fclose)文件。

int fclose ( FILE * stream );

stream – 这是指向 FILE 对象的指针,该 FILE 对象指定了要被关闭的流。


关闭成功,返回0,否则返回EOF

2.读写文件

通过fgets和fputs以字符串形式进行读写,也可以通过fprint和fscanf进行格式化读写。

int fputs ( const char * str, FILE * stream );
char * fgets ( char * str, int num, FILE * stream );
int fprintf ( FILE * stream, const char * format, ... );
int fscanf ( FILE * stream, const char * format, ... );

当前路径:一个进程运行起来的时候,每个进程都会去记录自己当前所处的工作路径。

所有当前路径也就是当前进程的工作路径,可以被修改,所以每个进程都有自己的当前路径

文件操作中r和w分别代表读和写,r+(读写)代表不存在则出错,w+(读写)代表不存在则创建,a(append)进行追加,追加也是写入,a+()也是读写,写是追加。r+b是以二进制形式进行读,r+w是以二进制写入。

  • fprintf+"w"

  • fgets+"r"
    fgets会给字符串结尾添加\\0


buff[strlen(buff) - 1] = 0

以上代码目的是在读取的时候按行打印,把\\n多读了,处理一下\\n;

  • fprintf+"a"

3.扩展

以w方式打开文件,C会自动清空文件内部的数据

  • 比特位传递选项

在C语言中,传标记位一个整数标记位一般传一个整数,而一个整数有32个比特位,所以我们可以通过比特位来传递选项。每个宏对应的数值,只有一个比特位是1,批次位置不会重复。下面使用比特位来传递选项,一个比特位一个选项,比特位位置不能重复:


通过|传递参数,这样就能传递多个标志位了。

  • stdin & stdout & stderr

C默认会打开三个输入输出流,分别是stdin, stdout, stderr
仔细观察发现,这三个流的类型都是FILE*, fopen返回值类型,文件指针

三、系统文件

操作文件,除了上述C接口(当然,C++也有接口,其他语言也有),我们还可以采用系统接口来进行文件访问

1.open和close

  • open
    fopen的底层就是调用open系统接口


mode参数含义是:文件权限,普通文件默认创建的是0664:一个文件形成的时候有默认文件的野码umask,普通文件创建的时候默认的起始权限是0666,在形成文件的时候0666&~umask。umask的默认值是0002。
不传mode的后果是生成的文件是乱码

pathname是文件路径

函数调用成功返回文件的描述符,调用失败返回-1

#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); //文件不存在
//pathname:打开文件名
//flags:标志位。O_RDONLY:只读 O_WRONLY:只写 O_RDWR:读写

O_CREAT:文件不存在,则只能由我们去创建它,也要传递mode,来指明该新文件的权限。

注意:O_CREAT是一个建议选项,文件存在还是不存在都可以使用

  • close

fclose底层是调用close系统接口

注意:使用open并不会帮我们创建文件,而C语言的文件操作函数fopen会自动创建文件是因为它封装了会帮我们自动创建,但是对于系统接口我们需要加上O_CREAT(文件不存在自动创建).最终成功帮我们自动创建成功!但是权限是乱的,但是文件默认以什么权限创建?我们默认情况下目录以777,普通文件以666开始,这些都是通过open的第三个参数mode选项设置权限的,设定创建默认文件的权限。


other的权限是只读,因为有掩码umask,我们可以通过在进程中设置umask来改变文件的最终权限




但是此时父进程shell的umask结果还是0002,我们改变的是子进程的文件权限,因为进程具有独立性,并不会影响父进程的umask。

2.write和read

  • write


fd:所写的文件的文件描述符

buf:缓冲区数据,参数是void*,之前所说,文件读取分为文本类和二进制类,这是对于语言提供给我们的文件读取的分类。但是在操作系统看来,都是void*,也就是二进制!

count:缓冲区所写的字节个数

返回值:返回写入的字节数,在这里我们并不太需要关注返回值

#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#define FILE_NAME "log.txt"
int main()

    umask(0);
    int fd = open(FILE_NAME,O_WRONLY | O_CREAT,0666);
    if(fd<0)
    
        perror("open");
        return 1;
    
    int cnt = 5;
    char outBuffer[64];
    while(cnt)
    
        sprintf(outBuffer,"%s:%d\\n","helloworld",cnt--);
        
        write(fd,outBuffer,strlen(outBuffer));
    
    close(fd);



如果string+1了,则会出现乱码的问题:

  • 清空问题
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#define FILE_NAME "log.txt" 
int main()

    umask(0);
    int fd = open(FILE_NAME,O_WRONLY | O_CREAT | O_TRUNC,0666);
    if(fd<0)
    
        perror("open");
        return 1;
    
    close(fd);



可以发现之前的hello world并没有被清空,而C语言文件操作函数fwrite每次写入都会清空文件中所有内容,这是因为C语言对write进行了封装,而在Linux下实现自动清空内容,需要我们自己添加选项内容O_TRUNC:


所以C语言简单的一个"w",底层就需要封装write和O_WRONLY(写入) | O_CREAT(不存在则创建 | O_TRUNC(清空),以及传入属性

  • 追加O_APPEND


    "a"在系统层面上是封装了write,O_WRONLY | O_CREAT | O_APPEND(追加)
  • read

    从一个文件描述符中读取文件
//头文件
#include <unistd.h>
//返回值ssize_t系统定制类型
ssize_t read(int fd, void *buf, size_t count);

读文件需要用到选项O_RDONLY

成功返回读取到多少个字节,0代表读到文件结尾。

读文件的前提是文件已经是存在的了。

#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<assert.h>
#define FILE_NAME "log.txt"
int main()

    int fd = open(FILE_NAME, O_RDONLY);
    assert(fd > 0);
    char buffer[1024];
    ssize_t num = read(fd, buffer, sizeof(buffer) - 1);
    if(num > 0)
        buffer[num] = 0; // 字符串结束标识符'\\0'
    printf("%s",buffer);
    close(fd);
    return 0;

3.总结

系统调用接口:open/close/write/read/lseek必须用文件描述符
而C语言库函数(libc)接口:fopen/fclose/fwrite/fread/fseek是封装了系统调用接口

四、理解文件

文件操作的本质:进程和被打开文件的关系

进程可以打开多个文件,那么系统中一定存在大量的被打开的文件,而被打开的文件,要被OS管理起来。而管理的本质是先描述,再组织,所以操作系统为了管理对应的打开文件,必定要为文件创建对应的内核数据结构进行标识文件:struct file;(与C语言的FILE没有关系) 其中包含了文件的大部分属性

进程和被打开的文件如何关联,也就是说进程和被打开文件的关系是如何维护的?通过文件打开(open)的返回值和文件描述符进行联系。

#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<assert.h>
#define FILE_NAME(number) "log.txt"#number
int main()

    int fd1 = open(FILE_NAME(1), O_WRONLY | O_CREAT | O_APPEND, 0666);
    int fd2 = open(FILE_NAME(2), O_WRONLY | O_CREAT | O_APPEND, 0666);
    int fd3 = open(FILE_NAME(3), O_WRONLY | O_CREAT | O_APPEND, 0666);
    int fd4 = open(FILE_NAME(4), O_WRONLY | O_CREAT | O_APPEND, 0666);
    printf("fd:%d", fd1);
    printf("fd:%d", fd2);    
    printf("fd:%d", fd3);    
    printf("fd:%d", fd4);
    close(fd1);
    close(fd2);
    close(fd3);
    close(fd4);
    return 0;


1.为什么从3开始,0,1,2呢?
2.3,4,5,6是连续的小整数,就像数组下标一样

五、文件描述符

在C语言阶段我们知道C程序会默认打开三个标准输入输出流:stdin(键盘),stdout(显示器),stderr(显示器)

FILE实际上是一个结构体!访问文件时,底层open必须采用系统调用,而系统调用接口访问文件必须用文件描述符,而在C语言用的并不是文件描述符,而是FILE,所以这个FILE结构体必定有一个文件描述符的字段。所以C语言不仅在接口上有封装,连数据类型都有封装。



这就很好的解答了为什么open的返回值是从3开始的问题!因为0,1,2默认被占用,我们的C语言封装了接口,同时也封装了操作系统内的文件描述符。

1.理解

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

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

这也就是为什么文件操作系统读到的数是整数,而且是连续的,因为文件操作系统内标记进程和文件之间的关系就是文件描述符表,用数组标定文件内容!通过文件描述符来访问文件!

2.分配规则

文件描述都是从最小未被使用的下标开始分配的,那么分别关掉0,1,2会发生什么?

  • 关掉0


    当我们把0关掉时候,该描述符没有被占用,当我们调用系统接口来创建一个新文件时候,该文件占用了0下标的文件描述符。

如果在创建一个文件对象,会在自己的文件描述符表从小到大按照顺序寻找最小的且没有被占用的fd.

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

  • 关掉1

当我们关闭1时,此时1不在指向标准输出(显示器),不在向显示器打印,当我们打开文件的时候,系统会存在文件对象,然后在把文件的地址在files_struct找一个最小的没有被使用的文件描述符,此时是1,此时就把文件的地址填入1的下标里,在把1号文件描述符返回给上层,此时fd就拿到返回值1。


这里没有显示结果,根据前面推测,那么打印的内容应该打印在log.txt上。

此时打印的结果并没有在新打开的文件里,这是因为缓冲区的问题,没有被显示出来
刷新缓冲区看看:

#include<stdio.h>    
#include<sys/types.h>    
#include<sys/stat.h>    
#include<fcntl.h>    
#include<unistd.h>    
int main()    
    
    close(1);    
    umask(0);    
    int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
    if(fd < 0)    
        
        perror("open");    
        return 1;    
     
    printf("open fd:%d\\n", fd); // printf->stdout    
    fprintf(stdout, "open fd:%d\\n", fd);
    fflush(stdout);
    close(fd);
    return 0;


我们发现,本来应该输出到显示器上的内容,输出到了文件 log.txt当中,其中,fd=1。这种特性叫做输出重定向。

六、重定向

常见的重定向有:>(输出重定向), >>(追加), <(输入重定向)

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

1.接口

  • dup2


dup2的作用是两个文件描述符之间进行拷贝,是把fd里面的内容拷贝

dup2一旦重定向之后,最终剩下的都是oldfd

代码实现:

#include<stdio.h>    
#include<unistd.h>    
#include<sys/types.h>    
#include<sys/stat.h>    
#include<fcntl.h>    
#include<string.h>    
#define FILE_NAME "log.txt"    
int main()    
    
    int fd = open(FILE_NAME, O_WRONLY | O_APPEND | O_CREAT, 0666);
    if(fd < 0)    
        
        perror("open");    
        return 1;    
        
    dup2(fd,1); // 输出重定向    
    const char* msg = "hello world";    
    write(1, msg, strlen(msg));
    printf("\\n");
    return 0;


把本该显示在显示器的内容打印在文件上

  • 输入重定向


当然重定向到这还未结束

七、Linux下一切皆文件

在冯诺依曼体系中,我们知道硬件有键盘、显示器、磁盘、网卡等外设,在IO过程中,外设任何的数据处理都需要把数据读到内存,处理完毕之后将内存中的数据刷新到外设当中。因为软硬件资源多,所以操作系统需要对其先描述,再组织。所以这些外设都有对应的结构体,存储着对应着属性信息:

例如(只是一个例子,并非真实):

struct keyboard;
struct tty;
struct disk;
struct netcard;

同时,对应着自己的IO函数,例如(只是一个例子,并非真实):

int keyboardRead()
int keyboardWrite()NULL
int ttyRead()
int ttyRead()
...

每种硬件访问方法是不一样的

具体硬件的读写方法都在应用匹配的驱动程序里。Linux一切皆文件是这样体现的:任何一个被打开的文件结构体对象struct file //各种文件的属性 对象

struct file

	//各种文件属性
	int type;
	int status;
	int (*readp)();
	int (*write)();
	...
;

不同的文件对应的读写方法不一样,struct file对象里面可以有很多的(*readp)()、(*writep)()函数指针,通过函数指针指向具体的读写方法。

站在struct file上层看来,所有的设备和文件,统一都是struct file->,就可以调用到具体设备的方法。即Linux下一切皆文件

上层调用不同的文件,底层调用不同的方法。站在上层,只需要调用对应的统一的文件,使用struct file,访问不同的文件,这是C语言实现多态的特征。这里struct file称为在操作系统层面上虚拟出来的文件对象vfs(虚拟文件系统)

PCB指向被打开的文件:

我们所谓的关闭文件只是在表明用户给OS说已经不需要使用了,由OS决定,OS把引用计数减到0时,才被OS真正删除掉。

四、写在最后

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-基础IO

Linux之文件基础IO详解

Linux基础IO篇

《Linux从0到99》九 基础IO

《Linux从0到99》九 基础IO

《Linux从0到99》九 基础IO