Linux | 文件描述符理解 | 系统级别的文件操作讲解 | 一切皆文件的由来 | 重定向dup2函数讲解

对文件的基本理解

1.在Linux中,文件由文件属性和文件内容组成,并且一个文件所占用的内存不可能为0,因为即使一个文件的内容为空,它依然具有属性(何时创建,最后修改时间…),这些属性需要占用内存

2.打开文件的本质是将文件从磁盘加载到内存中,当一个文件被打开之前,该文件存储在磁盘上,属于磁盘文件,当打开文件时,文件被加载到内存中,此时的文件属于内存文件

3.我们研究文件操作时,是研究谁和文件之间的关系,谁在操作文件?在C语言中,我们使用fopen,fclose,fwrite,fread等函数对文件进行操作,这些函数在代码中被使用,代码最后会被编译成为可执行程序,可执行程序运行后成为进程,当可执行程序成为进程时,代码中对文件的操作才会被执行,所以我们研究文件操作,实际上是研究进程与文件之间的关系,只有搞懂了进程与文件间的关系才能对文件操作有深刻的理解。

何为当前路径?

使用C语言以w的方式打开一个文件时,如果文件不存在,默认会在当前路径下创建该文件并打开。

fopen("log.txt", "w");

在这里插入图片描述
(先用ll指令展示当前目录下的文件,当前目录下没有log.txt,file作为一个可执行程序,将以w的方式打开log.txt文件,运行file程序,再用ll指令查看当前目录下的文件,发现log.txt被创建)

所以当前路径指的是当前所处的目录吗?又或者说当前路径是指与可执行程序所在的目录相同的目录吗(因为log.txt与可执行程序file创建在了同一个目录下)?

修改file.c的代码,使该程序打印进程的pid,接着打开log.txt文件,最后死循环的休眠(暂停程序),然后再复制一个渠道,通过进程的pid查看进程的相关消息

int main()
{
	printf("%d\n", getpid());
	fopen("log.txt", "w");
	while (1)
	{
		sleep(1);
	}
	return 0;
}

在这里插入图片描述
proc存储了进程的信息,在复制的渠道中展示proc目录下进程pid为9010的目录,可以查看该进程所对应的信息。
在这里插入图片描述
可以看到有两行信息,一个是exe路径信息,表示可执行所在的路径,一个是cwd(current work dirtory)当前工作路径,我们经常说的当前路径,实际上指的是当前工作路径。若打开文件,但文件不存在,文件的默认创建路径为当前工作路径

使用chdir可以改变当前进程的工作路径,修改file.c文件,添加改变该进程工作路径的代码,如果在修改后的工作路径下能查找到创建的log.txt文件,则验证了文件的默认创建路径为当前工作路径的说法。

// 修改后的file.c文件
int main()
{
	printf("%d\n", getpid());
	chdir("/home/cw");
	fopen("log.txt", "w");
	while (1)
	{
		sleep(1);
	}
	return 0;
}

运行修改后的文件
在这里插入图片描述
当前工作路径被修改为/home/cw,查看该路径下的文件
在这里插入图片描述
发现log.txt文件被创建在该目录下在这里插入图片描述
退出死循环的进程,使用ll打印当前目录下的文件,log.txt没有被创建,说明log.txt只被创建在/home/cw路径下。

经过以上代码验证,可以得到结论:当前路径指的是当进程的工作路径,与可执行程序所在的路径无关

文件与操作系统的关系

当我们向文件写入数据时,最终是向磁盘写入数据,磁盘属于硬件的一种,硬件由操作系统管理,所以因为操作系统是磁盘的管理者,想要访问磁盘就必须经过操作系统(我们不能绕过管理者去访问被其管理的对象),因此,所有对文件的操作都必须贯穿操作系统,操作系统作为软件与硬件之间的软件层,暴露出接口供上层使用,向上接收上层的操作,向下使用并管理着硬件。

语言对系统接口的封装

如何理解printf?printf是一个C语言函数,作用是写入数据到显示器上,显示器作为一个硬件,由操作系统管理,而printf想要访问显示器,就必须经过操作系统,所以可以推测,printf只能使用操作系统提供的接口,访问底层的显示器。

综上所述,printf作为C语言的流输出函数,实际上是对系统接口的封装。对于不同操作系统,printf的封装都不相同,因为不同操作系统提供的接口肯定不同。那么每种语言都有流输入与流输出,它们相关的IO函数也是对系统文件接口的封装吗?答案是是的,那么为什么要对系统的文件接口进行封装?

1.系统接口较为复杂,学习成本高,使用难度大
2.使语言具有跨平台性,对于不同的操作系统,语言级别的IO函数都可以正常使用,不会出现可以在linux下能跑的代码,在windows下不能跑

其中第二点原因为主要原因,每种语言采用:穷举所有操作系统+条件编译的方法,对所有操作系统的文件接口进行了封装。在不同操作系统下,一个语言封装好的IO函数将被条件编译为不同的代码。因为这层封装,使用者可以以一种统一的视角看待所有操作系统的文件接口,使用者只需要关心封装好的函数该怎么使用,不用去关心底层的系统接口要怎样使用。

但是,学习了系统级别的文件接口,可以使我们对语言级别的文件接口有更深的理解。

系统的文件接口

打开与关闭

在这里插入图片描述
打开文件函数open:pathname是要打开的文件,与C语言一样,如果不带路径,默认在当前工作路径下创建文件,flags表示打开文件的方式,mode为文件的权限状态(如果创建文件需要设置其权限状态),该函数返回一个文件描述符fd,fd是一个文件的唯一标识符。

其中的重点是flags,flags作为一个标记位,以位图结构表示打开文件的方式(位图:假设有一个32位bit的数,一般情况下31个比特位为0,1个比特位为1,当1出现在不同的比特位,就表示不同的标记,通常用宏来表示一个标记,所以一个32位bit的数可以表示32个不同的标记,这就是位图的思想)。

// test.c文件
#include <stdio.h>

#define PRINT_A 0x1  // 0000 0001
#define PRINT_B 0x2  // 0000 0010
#define PRINT_C 0x4  // 0000 0100
#define PRINT_D 0x8  // 0000 1000
#define PRINT_DF 0x0 // 0000 0000

void print(int flags)
{
	if(flags & PRINT_A) printf("hello a\n");
	if(flags & PRINT_B) printf("hello b\n");
	if(flags & PRINT_C) printf("hello c\n");
	if(flags & PRINT_D) printf("hello d\n");
	if(flags == PRINT_DF) printf("hello df\n");
}

int main()
{
	print(PRINT_A);
	print(PRINT_A | PRINT_B);
	print(PRINT_A | PRINT_B | PRINT_C);
	return 0;
}

以上代码是一个简单的位图运用,不同的宏表示不同的标记,不同的标记表示不同的功能,如果想要调用两个以上的功能,需要传两个以上的宏,宏之间用按位或运算符连接。

在print函数中,将传入的标记位flags与不同的宏进行按位与操作,如果得到的结果为1,说明需要调用该宏对应的方法,以上代码将根据宏的不同打印出不同的语句。
在这里插入图片描述
(代码执行结果)

文件的系统接口函数open需要用到的宏

O_RDONLY:以只读方式打开文件
O_WRONLY:以只写方式打开文件
O_RDWR:以可读可写的方式打开文件
O_APPEND:以追加方式打开文件
O_CREAT:打开文件时,若文件不存在,则创建文件
O_TRUNC:如果文件存在,并且打开方式包含可写,文件的内容将被清空

// file.c文件
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main()
{
	int fd = open("log.txt", O_WRONLY | O_TRUNC | O_CREAT, 0666);  
	// 与C语言的fopen("log.txt", "w")等价
	fclose(fd);
	return 0;
}

(以只写的方式打开文件,若文件不存在则创建,若文件存在则清空文件内容,最后将文件的权限状态设置为666,由于默认的权限掩码为002,最终文件的权限状态是664)。
在这里插入图片描述
在这里插入图片描述

关闭文件函数close的介绍:使用close函数需要传入文件的fd,如果函数返回0,文件关闭成功,返回-1表示关闭时发生了错误。每个文件都有一个计数器,用来记录当前有几个进程打开了文件,如果计数大于1,关闭文件只会导致计数–,不会真的关闭文件,如果计数为1,此时的关闭文件才会去释放文件所占用的资源

在这里插入图片描述
可以发现这三个宏组成的打开方式与C语言的w打开方式产生的效果是一样的,同时也能看出C语言对系统接口进行了封装,使得函数的使用成本降低。

读写函数

在这里插入图片描述
系统接口的写函数write:将buf的数据写入count个字节大小数据到fd文件中。

//file.c文件
int main()
{
	int fd = open("log.txt", O_WRONLY | O_TRUNC | O_CREAT, 0666);  
	char buffer[128] = {"hello file\n"};
	int i = 0;
	// 向log.txt文件中写入5次buffer
	for (i = 0; i < 5; i++)
	{
		// 注意这里的buffer不用加1,加1表示将'\0'也写入文件,但是'\0'只是C语言的字符串结束标识符
		// linux不认识'\0',如果将其写入会导致乱码
		write(fd, buffer, strlen(buffer));
	}
	close(fd);
	return 0;
}

在这里插入图片描述
(代码运行结果,buffer的内容被写到log.txt中五次)
在这里插入图片描述
linux的读函数read:从文件fd中读取count个字节大小数据到buf中。该函数会返回实际上读取到的字节数,如果发生错误,函数返回-1。

int main()
{
	// 以只读的方式打开文件
	int fd = open("log.txt", O_RDONLY);  
	char buffer[128] = { 0 };
	
	ssize_t size = read(fd, buffer, sizeof(buffer) - 1); 
	if (size > 0)
	{
		// 将读取到的字符串最后放上'\0'表示字符串的结束
		buffer[size] = '\0';
		printf("%s\n", buffer);
	}
	close(fd);
	return 0;
}

在这里插入图片描述

文件描述符fd的理解

通过阅读open函数的返回值介绍,我们得知如果使用open函数打开一个文件成功,函数返回值大于等于0,文件打开失败函数返回值为-1。
在这里插入图片描述
连续创建多个文件,观察它们的文件描述符是怎样的

// file.c文件
int main()
{
	int fda = open("loga.txt", O_WRONLY | O_CREAT);  
	int fdb = open("loga.txt", O_WRONLY | O_CREAT);  
	int fdc = open("loga.txt", O_WRONLY | O_CREAT);  
	int fdd = open("loga.txt", O_WRONLY | O_CREAT);  
	int fde = open("loga.txt", O_WRONLY | O_CREAT);  
	int fdf = open("loga.txt", O_WRONLY | O_CREAT);  
	
	
	printf("fda:%d\n", fda);
	printf("fdb:%d\n", fdb);
	printf("fdc:%d\n", fdc);
	printf("fdd:%d\n", fdd);
	printf("fde:%d\n", fde);
	printf("fdf:%d\n", fdf);
	return 0;
}

在这里插入图片描述
可以看到从创建的第一个文件开始,文件描述符从3递增到8,刚才说过,一个文件如果创建成功,open函数会返回大于等于0的fd值,为什么上面的进程创建的第一个文件的fd却不是0,而是3?0,1,2这些fd被谁使用了?

其中的原因为:一个进程运行起来后,默认会打开三个文件流,标准输入流,标准输出流,标准错误流,它们分别对应着0,1,2三个fd文件描述符。
在这里插入图片描述
C语言分别用stdin,stdout,stderr来表示这三个文件流,它们的类型为FILE结构体指针。由于linux系统用fd区分不同的文件,linux不识别什么FILE,stdin,它只关心文件的描述符fd,所以作为上层封装,C语言的FILE中肯定有一个fd,以供操作系统识别文件。有了以上的理论支持,我们可以写一段代码验证,stdin,stdout,stderr对应的fd为0,1,2

// testfd.c文件
#include <stdio.h>

int main()
{
	// 在FILE中fileno对应着文件描述符fd
	printf("stdin:%d\n", stdin->_fileno);
	printf("stdout:%d\n", stdout->_fileno);
	printf("stderr:%d\n", stderr->_fileno);
	return 0;
}

在这里插入图片描述
综上所述,fd从3开始使用的原因是:一个进程运行起来后,默认打开了三个文件流,分别占用了0,1,2三个文件描述符

如何理解Linux中,一切皆文件?

linux对文件的管理

一个系统中,可能同时打开许多文件,与进程一样,操作系统需要对这些文件进行管理,要进行管理,首先要描述管理对象的属性,再组织这些属性,linux下,描述文件属性的结构体叫做struct file,每个结构体描述一个文件的信息,操作系统用合适的数据结构连接这些结构体,这里以链表为例,用链表连接这些描述文件信息的结构体
在这里插入图片描述
所以操作系统用对一张链表的管理替代了对文件的管理。

进程对文件的管理

对于进程来说,一个进程可能打开多个个文件,那么进程也需要对其打开的文件进行管理,进程在系统中对应的结构为task_struct,该结构中有一个指针files,指向一个类型为struct files_struct的结构体,该结构体维护了一个进程打开的所有文件的信息(可结合下图理解),struct files_struct结构体中有一个数组fd_array,其存储元素的类型是指向struct file的指针。之前说过struct file是操作系统用来描述文件的结构体,所以fd_array中的元素指向的是操作系统用来描述文件的结构体struct file。因为文件描述符fd是从0开始增加的,所以fd就可以作为fd_array数组中的下标,该数组的0,1,2下标指向了操作系统中的三个标准文件流,其他下标则指向其他文件,若有的下标没有文件可以指向,则指向空。所以进程打开文件,实际上是将该文件在系统中的结构struct file的地址放到进程的fd_array数组中,并将指向该文件结构的元素下标fd返回给用户
在这里插入图片描述

文件描述符的分配规则

(补充文件描述符的分配规则:打开一个文件时,操作系统会遍历fd_array数组,找到一个最小的且没有被使用的下标,将其分配给新的文件,由于0,1,2下标被三个标准文件流占用,所以一般情况下fd都是从3开始使用的)

但是如果先关闭了fd为0的文件,再打开一个其他文件,根据分配规则,该文件的fd将是0。

// testfd.c 文件
int main()
{
	close(0);
	int fd = open("test.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
	printf("fd : %d\n", fd);
	
	return 0;
}

(先关闭fd为0的文件,再打开一个新文件,最后打印该文件的下标)
在这里插入图片描述
新文件的fd为0,结果与分配规则的说法相同

一切皆文件

在这里插入图片描述
C语言如何实现面向对象?在C++问世前,使用C语言编写操作系统的过程中就有了面向对象的思想,但是C语言的结构体中只能定义变量,不能定义函数,要在结构体中实现一个函数,只能使用函数指针。在初始化结构体变量时,将函数指针指向对应的函数即可,只是函数被定义在了类外。

对于所有的IO设备,肯定包含了两个操作:输入和输出。操作系统不能直接操作硬件,只能通过驱动与硬件交互,驱动是由硬件厂商提供的配置程序,有了驱动操作系统就能和硬件实现数据的通信。而驱动中肯定包含了一个IO设备的读方法和写方法,所以操作系统就可以调用IO设备驱动中的读方法,读取硬件信息,调用驱动中的写方法,向硬件写入信息。

Linux将所有的硬件看成文件,在操作系统与底层硬件之间加入软件层struct_file,使操作系统可以以统一的视角(文件)看待所有的硬件以及软件。比如,操作系统看待磁盘的方式就是:在内存中创建struct_file结构体,该结构体包含了磁盘的属性以及方法,其中的方法包括写方法和读方法两种,分别对应着两个函数指针,将结构体中写方法指向磁盘驱动中对应的写方法,再将结构体中的读方法指向磁盘驱动中对应的读方法。当进程需要读取磁盘时,操作系统就会找到该进程中的磁盘文件,调用该文件的读方法,由于该指针指向磁盘驱动中的读方法,最终会通过驱动的读方法读取磁盘。

可以结合上图理解,所有的IO设备都有自己的读写方法,系统创建的文件中肯定有两个函数指针,指向了该文件所对应了读写方法。操作系统将软硬件都看成文件,这样的文件系统叫做虚拟文件系统

综上所述,当进程需要调用一个IO设备的读(写)方法时,操作系统会在该进程中找到对应的文件,接着执行文件中读(写)指针所指向的函数。如果一个IO设备没有读或写方法时,对应文件中的指针将指向空,以表示该设备没有读或写操作。

Linux下,一切皆文件的本质是:操作系统以一种统一的视角看待软硬件,要实现这样的设计,最常用的做法是在操作系统与软硬件之间加入一层软件层,实现操作系统与底层软硬件之间的解耦。这样的操作与虚拟地址空间的实现有着异曲同工之妙,虚拟地址空间的设计使得进程以统一的视角看待内存,虚拟文件系统的设计使得操作系统将软硬件都描述为一种结构体,每个结构体对象的读写方法各不相同,但操作系统只需要通过函数指针调用其指向的方法即可。

重定向的原理

对于文件,语言只识别fd文件描述符,如C语言的FILE结构体,该结构体中有一个fd,C语言就是通过该fd识别文件流。如调用类似scanf函数,C语言就会去封装了0号fd的FILE文件中读取数据。调用printf函数,C语言就会去封装了1号fd的FILE文件中读取数据

那么重定向也就很好理解了,重定向即重新确定数据的流动方向。原本要输出到一个fd文件,现在却输出到了另一个fd文件,这叫输出重定向。原本要从一个fd文件中读取数据,现在却从另一个fd文件读取数据,这叫输入重定向。什么时候会发生重定向?假设语言要向fd为1的文件中输出信息,虽然fd为1的文件是显示器,但我们可以将其他文件的fd修改为1,此时数据就向其他文件输出,而不向显示器输出了

// testfd.c 文件
int main()
{
	close(1); // 先关闭fd为1的文件
	int fd = open("test.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666); // 此时打开的文件的fd为1
	printf("fd : %d\n", fd); 
	fflush(stdout);
	return 0;
}

(先关闭fd为1的文件,再打开一个txt文件,根据fd的分配规则该文件的fd为1,此时调用printf函数,语言就会通过操作系统向标准输出流输出信息,但语言只识别fd,于是fd为1的文件被操作系统识别为了标准输出流,但此时fd为1的文件是一个普通txt文件,向该文件输出信息不会使信息显示在屏幕上,而是写入到文件中(由于普通文件采用全刷新策略,需要使用fflush刷新缓冲区才能将数据写入到文件中)。)
在这里插入图片描述
但重定向不需要用户手动操作,以上代码只是演示重定向的原理,系统提供了dup2接口执行重定向
在这里插入图片描述
在这里插入图片描述
比如dup2(3, 1)这行代码,是将进程中的fd_array数组(数组存储了系统级别的文件结构体struct file的地址)中下标为3的元素拷贝到下标为1的元素位置上,也就是说下标为1的元素被下标为3的元素覆盖了,不再执行标准输出流文件,其指向的文件与下标为3的元素所指向的文件相同。由于fd为1的下标元素指向了fd为3的下标元素所指向的文件,所以原来向fd为1的文件输出的数据现在输出到了fd为3的文件中。
在这里插入图片描述

重定向函数dup2的使用

// dup2.c 文件
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main()
{
	int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
	dup2(fd, 1); // 将标准输出重定向到fd文件中
	printf("hello fd:%d", fd);
	// 由于缓冲区的存在,需要刷新才能使信息显示到log.txt文件中
	fflush(stdout);
	close(fd);
	return 0;
}

以上代码将fd为3的文件地址拷贝到fd为1的文件地址,printf会向fd为1的文件中写入数据,由于fd为1的文件被重定向成了一个普通文件,所以printf的输出信息将写入到fd为3的log.txt文件中,在运行程序的前后查看log.txt文件中的内容,发现数据确实被写到了log.txt文件中。
在这里插入图片描述

int main()
{
	int fd = open("log.txt", O_RDONLY, 0666);
	dup2(fd, 0); // 将标准输入重定向到fd文件中
	
	char buffer[1024];
	while (fgets(buffer, sizeof(buffer), stdin))
	{
	  printf("%s", buffer);
	}
	
	close(fd);
	return 0;
}

代码解读:打开一个log.txt文件并获取它的fd值,将fd_array的fd的文件地址拷贝到fd为0的元素位置,使标准输入被重定向到fd文件中,原来需要从键盘(标准输入)中读取数据,现在从fd文件(log.txt文件)中读取。向log.txt文件中写入一些数据,在运行dup2程序前查看txt文件内容,接着运行dup2,程序成功读取并打印了log.txt文件中的内容。
在这里插入图片描述
如果将重定向函数注释,那么程序将从键盘中读取数据并打印
在这里插入图片描述
最后再谈一下重定向:由于操作系统只识别文件的fd,而上层的C语言将文件封装成了FILE,其中包括了文件的fd,那么只需要修改FILE文件中的fd,并使之与其他FILE文件的fd不相同,就能瞒过操作系统。比如操作系统关闭fd为1的文件(将标准输出流文件关闭),再打开一个普通文件,它的fd为1,调用printf时,操作系统只会向fd为1的文件输出信息,却不知道此时的fd为1的文件不再是标准输出流文件,而是其他文件了。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值