文章目录
一、引言
二、引入文件描述符fd
2、1 观察fd的值
2、2 fd保存的位置
三、详解文件描述符fd
3、1 为什么要有文件描述符呢
3、2 到底什么是文件操作符呢
四、文件描述符的使用
4、1 验证文件描述符
4、1、1 验证stdin、stdout、stdout
4、1、2 验证fd值的大小顺序
4、2 输入输出重定向
4、2、1 dup2的使用
五、Linux下一切皆文件
🙋♂️ 作者:@Ggggggtm 🙋♂️
👀 专栏:Linux从入门到精通 👀
💥 标题:文件描述符💥
❣️ 寄语:与其忙着诉苦,不如低头赶路,奋路前行,终将遇到一番好风景 ❣️
一、引言
在Linux操作系统中,文件描述符是一种用于访问文件或输入/输出资源的抽象概念,它是为了更有效地管理和操作文件、设备、套接字等资源而引入的。文件描述符的作用和重要性在操作系统和编程中具有深远意义。
通过 文件操作(C语言vs系统调用)上篇文章 文件操作(C语言vs系统调用) 对系统调用 open的讲解后,我们知道文件描述符是一个整数。当时它代表的含义是什么呢?又有什么意义呢?本篇文章会对文件描述符进行详细解释。
二、引入文件描述符fd
2、1 观察fd的值
我们学习系统调用后,知道open调用完之后,会返回一个整型的值,该值就是文件描述符。那我们不妨打印出来观察一下fd值的大小和规律。代码如下:
#include<stdio.h> #include<sys/types.h> #include<sys/stat.h> #include<fcntl.h> #include<unistd.h> int main() { int fd1 = open("log1.txt", O_WRONLY|O_CREAT|O_APPEND, 0666); //rw-rw-rw- printf("open success, fd: %d\n", fd1); int fd2 = open("log2.txt", O_WRONLY|O_CREAT|O_APPEND, 0666); //rw-rw-rw- printf("open success, fd: %d\n", fd2); int fd3 = open("log3.txt", O_WRONLY|O_CREAT|O_APPEND, 0666); //rw-rw-rw- printf("open success, fd: %d\n", fd3); int fd4 = open("log4.txt", O_WRONLY|O_CREAT|O_APPEND, 0666); //rw-rw-rw- printf("open success, fd: %d\n", fd4); close(fd1); close(fd2); close(fd3); close(fd4); return 0; }
上述代码就是打开了四个文件,然后打印他们的文件描述符fd的值。我们看输出结果:
我们惊奇的发现,文件描述符的值好像是有规律的!从上述打印的结果观察出,fd的值是从3开始,依次往上加一递增的。那么问题来了:为什么从3开始呢?有0、1、2吗?
我们在学习C语言时,可能会听说过:当我们运行程序时,系统会默认帮我们打开标准输入、标准输出、标准错误。那标准输入、标准输出、标准错误到底是什么呢?
首先我们要知道Linux下,一切皆文件。难道显示器和键盘也是文件吗?答案是是的!!(后面我们也会对Linux下一切皆文件进行讲解)没错,当程序运行起来时,系统会默认帮我们打开三个标准文件的!而这三个文件分别对应的fd值就是0、1、2。
2、2 fd保存的位置
我们知道当程序运行起来时,系统会默认帮我们打开三个标准文件后,那么这三个文件描述符fd的只会被保存起来的。如果不保存起来,怎么向标准输出,也就是显示器打印值呢?或者又怎么从标准输入,也就是键盘读取值呢?所以他们的fd值一定会被保存起来的。那么问题来了,这个值保存在了哪里了呢?
上述调用fopen时,返回值为FILE* ,FILE是什么类型呢?FILE就是一个结构体!fd其实就是保存在了一个结构体,该结构体就是一个文件结构体FILE(struct file)。过海结构体中包含的文件的大部分信息。后面我们也会给出具体的详细解释。
三、详解文件描述符fd
3、1 为什么要有文件描述符呢
到这里会一直有疑问:到底什么是文件描述符呢(就是一个整数吗)?为什么要有文件描述符呢?接下来会详细解释。
首先,一个进程可能会打开很对文件。上面的代码中就打开了四个文件,如果想的话甚至可能会更多。内存中不仅仅只有一个进程吧!那么就很有可能在系统中打开了很多文件。那么操作系统要不要对这些文件进行管理呢?必须要进行管理!!!怎么管理呢?先描述,后组织!!!
怎么描述呢?我们都知道,Linux操作系统使用C语言写的。那么问题就转换成了C语言用什么来描述一个对象呢?不就是结构体吗!关键问题来了:怎么进行组织呢?这就与文件描述符有关系了。接着往下看。
当一个程序运行起来加载到内存后,创建对应的数据结构,就会变成一个进程。其中关键是进程控制块PCB。在Linux内核中,PCB为task_struct。其中,task_struct中就包含了一个文件结构体指针files_struct* fs。改指针就是指向的文件结构体。重点是该文件结构体中有一个指针数组file* fd_arry[](文件映射表),该指针数组指向的就是我们所打开的文件。我们所新打开的文件fd的值是文件描述表中最小的为空的位置。具体可结合下图理解:
我们在对上述的组织过程进行从头到尾的详细阐述一下:首先进程调用fwrite -> 找到所传入的FILE* -> 找到FILE中的fd -> 调用内部封装的write(系统调用) -> 找到进程的task_struct -> 找files_struct* fs -> 找到 files_struct -> 扎到files_struct中的file* fd_array[] -> fd_array[fd] -> 该位置的值是一个指针,该指针指向的就是file -> 找到该文件后,再进行操作。
相信到这里,你就会对为什么要引入文件操作符fd就会清楚了。一个很重要的原因就是便于管理文件和操作。
3、2 到底什么是文件操作符呢
刚开始我们只知道文件操作符是一个整数,其具体到表的含义并不知道。通过对上述的了解后,我们也就知道了文件描述符是一个下标,是用来标识和操作文件或者输入输出设备的整数。每个打开的文件(包括标准输入、标准输出和标准错误输出)都会被分配一个唯一的文件描述符。文件描述符的使用主要有以下几个方面:
- 文件操作:通过文件描述符,我们可以对文件进行读取、写入、定位等操作。例如,可以使用文件描述符来打开、关闭、读取、写入文件。
- 输入输出重定向:文件描述符可以用于在程序运行时动态地将输入输出重定向到其他文件。例如,可以将程序的输出重定向到文件中,或者将文件作为程序的输入。
- 管道通信:文件描述符可以用于实现进程间的通信,其中最常见的方式是使用管道。通过创建管道,并使用文件描述符将数据从一个进程传递给另一个进程。
接下来下面我们会对上述的三个使用进行讲解的。
四、文件描述符的使用
4、1 验证文件描述符
4、1、1 验证stdin、stdout、stdout
首先我们要验证三个标准:stdin、stdout、stdout中是否包含文件描述符fd。首先stdin、stdout、stdout是数据类型什么类型的呢?结合我们所学的文件操作函数,数据类型不就是FILE*吗。验证代码如下:
#include<stdio.h> #include<sys/types.h> #include<sys/stat.h> #include<fcntl.h> #include<unistd.h> int main() { //_fileno就是文件描述符fd printf("%d\n%d\n%d\n",stdin->_fileno,stdout->_fileno,stderr->_fileno); int fd1 = open("log1.txt", O_WRONLY|O_CREAT|O_APPEND, 0666); //rw-rw-rw- printf("open success, fd: %d\n", fd1); close(fd1); return 0; }
输出结果如下:
4、1、2 验证fd值的大小顺序
我们上述提到:新打开的文件fd的值是文件描述表中最小的为空的位置。怎么证明呢?不要忘记了可以通过close结合fd,进行关闭系统默认打开的标准文件。验证代码如下:
#include<stdio.h> #include<sys/types.h> #include<sys/stat.h> #include<fcntl.h> #include<unistd.h> int main() { close(0) int fd1 = open("log1.txt", O_WRONLY|O_CREAT|O_APPEND, 0666); //rw-rw-rw- printf("open success, fd: %d\n", fd1); close(fd1); return 0; }
上述代码就是把fd值为0(标准输入)文件关闭,我们再新创建文件,在观察它的fd值。结果如下:
确实,值为0了。我们在通过下述代码再次验证:
#include<stdio.h> #include<sys/types.h> #include<sys/stat.h> #include<fcntl.h> #include<unistd.h> int main() { close(0); int fd1 = open("log1.txt", O_WRONLY|O_CREAT|O_APPEND, 0666); //rw-rw-rw- printf("open success, fd: %d\n", fd1); int fd2 = open("log2.txt", O_WRONLY|O_CREAT|O_APPEND, 0666); //rw-rw-rw- printf("open success, fd: %d\n", fd2); close(fd1); close(fd2); return 0; }
输出结果如下:
确实,我们只关闭了fd值为0(标准输入)文件关闭,同时打开两个文件。其中一个fd的值为0,另一个为3。我们所述的是正确的。
4、2 输入输出重定向
我们平常的输入是从标准输入(键盘)读取,输出往标准输出(显示器)打印!输入和输出重定向就是不再从标准输入读取,向标准输出打印了,就是从指定的文件读取,或只输入到指定文件。什么原理呢?
首先,我们知道默认是输出到标准输出(显示器)上。我们能不能关闭标准输出,然后打开一个我们想输出到指定的文件,这是新打开的文件fd的值不就是我们刚刚关闭的标准输出文件的fd的值嘛!!! 根据我们上述详解文件描述知道,操作系统內部只认识文件标识符fd,并不认识所谓的stdin、stdout。此时新打开的文件不就称为了标准输出吗!!!我们可结合下图理解:
通过上述讲解,思路是有了。我们再看代码实现:
#include<stdio.h> #include<sys/types.h> #include<sys/stat.h> #include<fcntl.h> #include<unistd.h> #include<string.h> int main() { close(1); int fd = open("log.txt", O_WRONLY | O_APPEND | O_CREAT); if(fd<0) { perror("open"); return 1; } printf("fd: %d\n", fd); // stdout->FILE{fileno=1}->log.txt printf("fd: %d\n", fd); printf("fd: %d\n", fd); printf("fd: %d\n", fd); printf("fd: %d\n", fd); printf("fd: %d\n", fd); fprintf(stdout, "hello fprintf\n"); const char *s = "hello fwrite\n"; fwrite(s, strlen(s), 1, stdout); printf("fd: %d\n", fd); fflush(stdout); close(fd); return 0; }
我们再来看输出结果:
确实不在输出到屏幕上,而是我们制定的文件。这不就是输出重定向吗。
4、2、1 dup2的使用
dup2函数的作用有两个方面:一是复制文件描述符,二是重新分配文件描述符。
- 复制文件描述符:
dup2
可以用来复制一个已打开的文件描述符,创建一个新的文件描述符,使其指向相同的文件、管道或套接字。这种方式可以实现重定向输入、输出或错误流的功能。- 重新分配文件描述符: 使用
dup2
可以将一个文件描述符重新指定为指定的文件、管道或套接字。如果新的文件描述符已经被打开,则系统会自动关闭它。这里我们主要讲解复制文件描述符。
在每次输入重定向前,都需要调用close关闭标准输出,未免会有点麻烦。有没有其他的方法呢?答案是有的。我们可以看一下dup系列的系统调用接口。如下图:
这里我们就讲解dup2的使用。 具体如下图:
一共是有两个参数:oldfd、newfd, newfd是拷贝oldfd得到的。
我们想要的效果是:把新打开的文件fd值为3的地址拷贝到fd值为1的地址处。那么结合我们上面说到newfd是拷贝oldfd得到的。那么dup2的使用方法不就是dup2(3,1)。我们再看输出重定向的代码:
#include<sys/types> #include<sys/stat.h> #include <unistd.h> #include <fcntl.h> #include <stdio.h> #include <string.h> int main() { int fd = open("file.txt", O_WRONLY | O_CREAT, 0644); if (fd < 0) { perror("open"); return -1; } // 复制文件描述符 int newfd = dup2(fd, stdout); // 将标准输出重定向到文件 dup2(3,1); if (newfd < 0) { perror("dup2"); return -1; } printf("Hello, World!\n"); // 输出将被重定向到文件 close(fd); return 0; }
运行结果如下:
输入重定向的原理与输出重定向的原理相同,本篇文章就不再做过多解释。
五、Linux下一切皆文件
在Linux中,"一切皆文件"(Everything is a file)是一个重要的概念,用于描述Linux操作系统中所有资源和设备都以文件的形式进行访问和处理。
这个概念可以理解为,无论是硬盘上的文件、网卡、设备、进程等,都被抽象为文件的形式存在。在Linux系统中,通过文件系统(File System)来管理和访问这些资源。
具体来说,"一切皆文件"可以被解释为:
文件:在Linux中,普通的文件就是我们常见的文本文件、二进制文件等。它们被组织成一个层次结构的目录树,通过路径来定位和访问。
目录:目录也是一种文件,它包含了其他文件和目录的信息。通过目录,可以组织和管理文件的层次结构。
设备文件:Linux将硬件设备(如磁盘、网络接口等)和虚拟设备(例如打印机,输入设备)都看作是文件来处理。通过设备文件,可以读取和写入设备的数据。
- 进程:在Linux系统中,每个正在运行的进程都有与之关联的文件。通过读取和写入相应的文件,可以与进程进行通信和交互。
通过将所有资源都抽象为文件,Linux提供了一套统一的接口,可以使用相同的命令和工具来访问和管理不同的资源。这种统一性使得Linux操作系统更加灵活和强大,同时也方便了开发者和系统管理员进行各种操作和配置。