【Linux】文件描述符


一、复习C语言的文件操作

在将深入理解文件之前我们先来简单复习一下C语言的文件操作,之后再来学习文件的系统调用接口,通过对比来引出它们之间的关系。

1.1 fopen

在这里插入图片描述

1.2 以w写入的方式打开文件

int fprintf(FILE* stream, const char* format, ...); //fprintf 函数可以将格式化的数据写到指定的流中

在这里插入图片描述

注:以 w 的方式打开文件,文件不存在会自动创建该文件;如果文件存在,先清空文件的内容再进行写入。

在这里插入图片描述

int snprintf(char *str, size_t size, const char *format, ...); // 将写入的数据放在缓冲区中,然后从缓冲区中读取数据

在这里插入图片描述

1.3 以a追加的方式打开文件

在这里插入图片描述

1.4 以 r 的方式打开文件

在这里插入图片描述

关于C语言部分的文件操作就简单讲到这里,下面我们将重点来讲述系统调用接口,后续将它们进行对比来阐述它们之间的关系。

二、系统文件I/O

2.1 open

在这里插入图片描述

我们可以看到上图中有两个open函数,下面我们需要讲解的是红色部分的open函数,因为第三个参数可以为创建的文件设置文件权限。

系统调用 open 的参数和返回值:

  • 第一个参数:文件路径+文件名。只提供文件名,默认在当前路径进行文件操作。
  • 第二个参数:打开文件的方式。该参数是通过宏来表示不同的打开方式,如:O_RDONLY 只读方式打开文件,O_WRONLY 只写方式打开文件等。通过按位或可以实现不同的文件打开方式,原因是这些宏都是通过比特位的不同来标记不同的选项,也就是说一个比特位就是一个选项。需要注意的是,比特位的位置不能重复。
  • 第三个参数:设置文件的起始权限权限。我们可以为打开的文件设置不同的权限,使用 C 语言文件操作函数创建出来的文件默认权限是 664,文件的权限 = 起始权限 & (~umask),普通文件的起始权限是 666,目录文件的起始权限是 777。
  • 返回值:成功打开文件时返回一个大于 0 的文件描述符,打开失败则返回 -1 并且设置错误码 errno。关于文件描述符后续我们将重点进行讲解。

我们先来通过一个例子讲讲第二个参数的原理:

在这里插入图片描述

我们下面将使用的选项如下:

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


在这里插入图片描述
上述例子我们可以发现,open函数以写的方式打开文件不与C语言以写的方式打开文件一样:当不存在指定文件时,open函数会返回-1并且它的退出码为2表示找不到该文件;而fopen函数会创建一个新的文件进行写入操作,所以对于open函数来说,我们首先需要创建一个文件再进行写入操作:

在这里插入图片描述

从上图我们发现此时我们log.txt与myfile可执行程序颜色一致,但是我们不应该看到的就是一个普通文件吗?为什么这里却与我们想的不同呢?

其实此时的log.txt文件权限是一个乱码的形式,只不过这里我的机器上表现的不是很明显,大家下来自己可以试一试,有可能你的文件会变成红色并且它的权限出现了rwx以外的字母,为何会出现乱码?因为此时我们创建一个文件并未对它进行权限设置,所以导致了这个问题,我们可以通过第三个参数来设置权限解决这个问题。

在这里插入图片描述

通过上图我们可以发现每次我们设置的是起始权限,由于掩码的存在我们需要经过重新计算才能得到最终文件的权限,但这种方式是不是看起来不是特别的直观啊,所以我们可以自己设置一下掩码umask。

在这里插入图片描述

我们将掩码设置为0,所以此时open函数的第三个参数设置的起始权限就是我们的最终权限,我们就可以直观的知道指定文件的权限了。

2.2 write

write函数是将数据先写入缓冲区中,然后从缓冲区读取数据写入文件中。

在这里插入图片描述

在这里插入图片描述

下面我们将代码修改了一下,继续来执行一下:

在这里插入图片描述

我们看到再次将数据写入该文件时,此时是在开头写入我们的内容,并没有在写入aaaaaa之前进行清空,所以要想每次写入文件时清空原始的文件内容再进行写入,我们可以使用O_TRUNC宏参数将文件内容清空:

在这里插入图片描述

追加写入文件

我们只需要添加O_APPEND选项就可以实现追加写入文件了:

在这里插入图片描述

2.3 read

read函数与write函数的使用方式一致:

在这里插入图片描述

在这里插入图片描述

三、预备知识

在切入文件描述符这个话题之前我们先引入关于文件的一些预备知识:

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

四、文件描述符

上述过程中open函数的返回值就是文件描述符,那么它到底是什么呢?返回多少?

在计算机中,文件描述符(File Descriptor)是一个非负整数,用来唯一标识一个打开的文件。文件描述符是Unix和类Unix系统中文件I/O操作的基础。当打开一个文件时,操作系统会为该文件分配一个文件描述符,以便在后续的操作中对该文件进行读写等操作。文件描述符通常是一个小的非负整数,操作系统通过文件描述符来维护已经打开的文件的状态信息。

我们通过下面一个例子来实验一下:

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

#define LOG "log.txt"

int main()
{
    int fd1 = open(LOG, O_CREAT | O_WRONLY | O_TRUNC, 0666);
    int fd2 = open(LOG, O_CREAT | O_WRONLY | O_TRUNC, 0666);
    int fd3 = open(LOG, O_CREAT | O_WRONLY | O_TRUNC, 0666);
    int fd4 = open(LOG, O_CREAT | O_WRONLY | O_TRUNC, 0666);
    int fd5 = open(LOG, O_CREAT | O_WRONLY | O_TRUNC, 0666);
    int fd6 = open(LOG, O_CREAT | O_WRONLY | 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 开始的呢?0、1、2 哪去了?在Unix和类Unix系统中,0、1、2这三个文件描述符有特殊的含义。它们分别代表标准输入(stdin)、标准输出(stdout)和标准错误输出(stderr)。通常情况下,打开的文件会从3开始分配文件描述符。

在这里插入图片描述

我们可以看到这三个标准I/O流的类型都为FILE*,那么FILE*到底是什么?我们先保留疑问后续再来进行探究,现阶段我们只需要知道在进程启动时会默认打开这三个标准I/O文件就可以了。

4.1 进程与文件之间的关系

通过之间的预备知识我们知道文件操作本质上都是进程与文件之间的关系,那么一个进程是不是可以打开多个文件?就像上述我们看到源代码中调用了5次open函数来打开文件,所以进程 : 被打开的文件 = 1 :n。 操作系统是不是可以创建多个进程来打开文件?那么每个进程需要打开对应的文件,进程如何管理它们对应的文件?先描述再组织。 我们可以通过一种数据结构将被打开文件的地址用一个数组保存起来,同时这个结构体中还包含其他信息,例如:该进程管理文件的个数… 如何组织?我们只需要将该结构体的地址保存在task_struct中,该进程通过找到该结构体就能找到对应需要被打开的文件了。

在这里插入图片描述
通过上图我们可以知道我们每打开一个文件,就将文件对应的地址保存在fd_array数组中,而数组下标就对应着文件。

结论:文件描述符本质上就是文件描述符表的下标-->数组的下标。进程与被打开文件的关系:进程通过文件描述符表指向对应的被打开的文件。

4.2 Linux下一切皆文件的理解

在这里插入图片描述

“一切皆文件”是Linux系统设计的核心原则之一,它是将所有的设备、文件、目录、进程等都看作是文件。这意味着在Linux系统中,所有的操作都是通过对文件的读、写、创建、删除等操作来实现的,无论是硬盘、U盘、鼠标、键盘、网络设备、进程等,都被看作是文件。

这种设计有很多好处,例如:

  • 统一的操作方式:无论是对文件、目录、设备还是进程进行操作,都使用相同的命令和方式,使得用户和管理员不需要学习多种不同的操作方法,降低了学习和使用的难度。
  • 灵活性:由于一切皆文件,用户可以像操作普通文件一样对设备进行操作,例如通过读写设备文件来控制硬件设备,或者通过读写网络设备文件来进行网络通信等。
  • 安全性:由于所有的设备、文件、进程都被看作是文件,Linux系统可以通过文件权限来进行约束某些行为,保证了安全性。

4.3 系统调用接口与C库函数的关系

通过上文一系列的铺垫,我们知道 fopen fclose fread fwrite 都是C标准库当中的函数,我们称之为库函数(libc),而open close read write 都属于系统提供的接口,称之为系统调用接口。

还记得我们在操作系统中讲述的一张计算机软硬件体系结构图嘛:

在这里插入图片描述

相信看完这张图你会对它们之间的调用关系非常清晰,实际上大部分库函数都会调用系统调用。

在这里插入图片描述

从实现者的角度来看,系统调用与库函数之间有根本的区别,但从用户的角度来看,其区别不重要,因为它们都可以实现对应的功能;但是最终我们要明白大部分库函数只是基于系统调用对它进行了二次开发将其封装起来了,所以系统调用通常是不能被替换的,而库函数可以!!

我们可以通过下图来看看它们之间的关系:

在这里插入图片描述

4.4 FILE是什么

下面我们来探究一下之前所说的FILE到底是什么?

FILE*是什么,它是一个指针,FILE是什么?它是结构体。谁提供的?C语言提供的。它与上面我们讲的struct file有关系吗? 上下层的关系。 通过上一个话题我们就知道FILE是在用户层,而fd在内核层,那么此时C库函数一定调用了系统调用接口,而上述使用的write、read系统调用接口使用了参数fd,所以FILE中就一定包含了fd这一属性!!

下面我们来验证一下三个标准IO文件的文件描述符:

在这里插入图片描述

4.5 文件描述符的分配规则

下面我们通过几个实验来看看文件描述符的分配规则:

正常情况下:
在这里插入图片描述
关闭1号文件:

在这里插入图片描述

关闭0号文件:
在这里插入图片描述

关闭0号文件和2号文件:

在这里插入图片描述

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

五、重定向

上述我们针对文件描述符所做的实验其实就可以认为是重定向操作,我们通过一张图看看重定向的原理:

在这里插入图片描述

关闭1号文件让其指向myfile新文件,进程根本不关心1号文件它的内容是否改变。

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

上述实验中如果我们要进行重定向需要先要关闭某个文件,再由文件描述符分配规则重新指向,这样方式有点不方便,所以系统为了支持我们更好地进行重定向,给我们提供了一个系统调用接口dup2函数。

在这里插入图片描述

上述dup2函数的使用解释非常的重要,你可能认为oldfd就是原先的文件,newfd就是重定向之后的文件,但实则不然我们不能根据字面意思去理解,应该根据接口说明去使用,oldfd就是需要新的文件,newfd是原先的文件。下面我们来看看它的使用:

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

为了更好的向大家阐述重定向与dup2函数,我们来做一个实验:

在这里插入图片描述

在这里插入图片描述

上述过程中我们使用dup2函数使1号文件重定位到fd号文件(bite.txt),当我们close(fd)时此时输出内容到fd文件中,而当我们close(1)时,此时什么都没有,这是为何??

在close(fd)之前,printf就已经将内容输出到bite.txt中了,1号标准输出文件也指向的是bite.txt文件,所以最终在1号标准输出文件中可以看到bite.txt中的内容;而close(1),虽然printf在关闭1号文件之前就将输出内容输出到bite.txt中了,但此时1号下标已经不再指向bite.txt了,此时1号下标就找不到bite.txt了,所以最终就看不到bite.txt中的内容了。

那么有读者可能有疑问了,我的1号下标和fd下标都是指向bite.txt文件,那么我关闭其中一个另一个不是还指向着bite.txt吗?

在这里插入图片描述

其实理解这个问题的关键还是dup2函数,dup2函数调用成功之后返回的是newfd的文件描述符即1号下标,所以接下来实际操作文件bite.txt都是利用1号下标进行引用的,这就是为什么关闭掉1号文件下标的指向时查找不到bite.txt的原因。

关闭文件的本质是什么?

关闭的本质是关闭一个文件描述符,使其不再指向对应的文件,而非删除这个文件,该文件还是存在于磁盘中。在执行close(fd)时,内核会将文件描述符从该进程的文件描述符表中删除,并释放该文件描述符所占的资源,包括文件表项和v-node结点,该文件描述符不再有效,任何试图使用该文件描述符进行读写的操作都将失败。

4.1 输出重定向

上述我使用dup2函数实现的就是输出重定向,对于讲解下面的各种重定向我将不再采用这种形式去实现,既然懂得原理了我们就直接通过指令来进行讲述。

我们来看一个例子,顺便好好讲讲stdout与stderr的关系:

在这里插入图片描述
从上图中我们发现stdout与stderr都是向显示器中打印内容,那么它们之间有什么区别??

在这里插入图片描述

那么这个stderr文件到底有什么用呢?

其实它代表的是标准错误文件,我们通常将错误信息放在这个文件中,而正常信息就直接放在标准输入文件当中,下面我们来实现一下这个功能:

在这里插入图片描述
另外我们还可以通过指令来实现:

在这里插入图片描述

在这里插入图片描述

4.2 追加重定向

输出重定向讲完了,那么追加重定向就非常的简单了,下面我们简单的来看看就好:

在这里插入图片描述

使用>>指令:

在这里插入图片描述

4.3 输入重定向

在这里插入图片描述

关于重定向我个人认为如果确实是有点摸不着头脑,我们可以简单记一个小tips:>、>>以及< 前面的文件描述符表下标得到后面跟的文件的地址,此时文件描述符就指向后面跟的文件了!!是对后面跟的文件进行操作!!


本篇文章的内容就讲到这里了,如果文章有任何问题或者错处欢迎大家评论区相互交流orz~~🙈🙈

  • 14
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

malloc不出对象

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

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

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

打赏作者

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

抵扣说明:

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

余额充值