基础IO(文件描述符 | 重定向 | 文件系统 | inode | 软硬链接)


复习C文件IO

文件的读写

#include<stdio.h>
#include<string.h>
int main()
{
	// 写
	FILE *wfp = fopen("myfile.txt", "w");
	if (!wfp){
		printf("fopen error!\n");
	}
	const char *str = "hello world!\n";
	fwrite(str, strlen(str), 1, wfp);

	fseek(wfp, 0, SEEK_END);
	int size = ftell(wfp);
	rewind(wfp);
	fclose(wfp);

	// 读
	FILE *rfp = fopen("myfile.txt", "r");
	if (!rfp){
		printf("fopen error!\n");
	}
	char buf[1024];
	while (1){
		size_t s = fread(buf, 1, size, rfp);
		if (s > 0){
			buf[s] = 0;
			printf("%s", buf);
		}
		if (feof(rfp)){
			break;
		}
	}

	fclose(rfp);
	return 0;
}

输出信息到显示器,有哪些方法?

#include <stdio.h>
#include <string.h>
int main()
{
	const char *msg = "hello fwrite\n";
	fwrite(msg, strlen(msg), 1, stdout);
	printf("hello printf\n");
	fprintf(stdout, "hello fprintf\n");
	return 0;
}

C默认会打开三个输入输出流:

stdin - 标准输入 - 键盘;
stdout - 标准输出 - 显示器;
stderr - 标准错误 - 显示器。

在这里插入图片描述

仔细观察发现,这三个流的类型都是FILE*, 文件指针。

以上,是我们之前学的C的文件操作。

本文将站在系统的角度,深刻理解文件操作。

一、系统文件IO

除了可以使用语言提供的接口,还可以采用系统接口来进行文件操作。

接口介绍

基本的四个接口:open read write close
重点介绍open,其余三个可类比C语言fwrite()等库函数。

#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: 打开文件时,可以传入多个参数选项,用下面的一个或者多个常量进行“或”运算,构成flags。
参数说明
O_RDONLY只读打开
O_WRONLY只写打开
O_RDWR读写打开
O_CREAT若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限
O_APPEND追加写

【前三个常量,必须指定一个且只能指定一个 】

  • mode:用来表明创建的文件的权限,八进制形式。当flags中含有O_CREAT这个参数的时候,必须有权限。 从低位往上,每三个比特位为一组,分别表示其他人、组、用户。 如:输入0664,表示的是0000 000 110 110 100,等价于-rw-rw-r–。
  • 返回值
    成功:新打开的文件描述符
    失败:-1

fopen fclose fread fwrite 都是C标准库当中的函数,我们称之为库函数。
而open close read write 都属于系统提供的接口,称为系统调用接口。

回顾之前,系统调用接口(下) 与库函数(上)是上下层的关系,所以要真正研究一个文件是怎么被写入被读取的等等操作,研究系统调用更加直白。
基于系统调用这四个接口,会延伸出不同各种语言IO相关的系统调用接口。
所有的IO操作本质是往硬件上写,因此必须通过穿透操作系统的帮助。

在这里插入图片描述

二、文件描述符fd

在open的介绍中,我们知道fd是其调用成功的返回值,也就是一个int型整数。
而C语言会默认打开三个标准流,分别对应的文件描述符为:
0:标准输入
1:标准输出
2:标准错误

文件描述符的本质就是数组下标(从0开始的整数)。

当我们打开的文件数目多了,就要被操作系统管理起来!
而管理的本质是:先描述,再组织。

为了描述,操作系统在内存中要创建相应的数据结构来描述目标文件。

// 描述文件的结构体
struct file{
	// 文件的属性信息
	// 文件的缓冲与存储位置
}

再组织:把这些结构体用相应的数据结构一个一个连起来。

所以我们要明确一个概念:
打开一个文件的本质就是,从磁盘加载到内存,操作系统为其创建struct file结构体,然后把它们用数据结构组织起来。(与进程的管理相似)

我们知道一个进程可以访问多个文件,所以进程与文件是一对多的关系。为了知道一个进程打开了哪些文件,所以我们必须把进程的PCB和组织文件的结构体struct file关联起来。

在Linux内核中,用的是一个数组struct file* fd_array[],进行关联。

在这里插入图片描述
通过上图,解释:一个进程怎么通过文件描述符找到文件?

  • 每个进程都有一个指针files, 指向一张表files_struct,该表最重要的部分就是包涵一个指针数组struct file* fd_array[],每个元素都是一个指向打开文件的指针。所以,本质上,文件描述符就是该数组的下标。所以,只要拿到文件描述符,就可以找到对应的文件了。

描述文件的结构体中会有一些函数指针,(面向对象的思想)基于基本的接口去实现输入输出到不同地方的方法。
在这里插入图片描述
Linux一切皆文件,体现在struct file这样一个虚拟层上面,上层调用大家用的都是同一个接口(函数指针),不同的语言,指向不同的方法(多态)。

解释语句write(4, “hello”, 5);的过程 :操作系统找到当前进程的PCB,就找到了结构体内部的file指针,系统在数组里索引4(传入的fd),找到对应的文件,用文件对应的write方法把数据写到相应的地方。

文件描述符的分配规则

默认0、1、2分别代表标准输入、标准输出、标准错误。

其实,所有语言都会提供标准输入、输出、错误这样默认的打开窗口。因为计算机要和人交互,语言本质上是人和计算机中间交互的载体,提供交互的功能。一个语言为什么会默认打开三个流?底层(os默认)打开的,必须打开。

  • 运行下面代码,发现结果为3
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
	int fd = open("myfile", O_RDONLY);
	if(fd < 0){
	perror("open");
	return 1;
	}
	printf("fd: %d\n", fd);
	close(fd);
	return 0;
}

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

重定向

把本来应该显示在显示器里的内容,显示在了文件里,这个动作叫做输出重定向。

关掉1,指向普通文件,命令行并不知道。它仍然认为1是标准输出,但其实printf输出到了文件中!
在这里插入图片描述

重定向的本质就是在操作系统底层对文件描述符的开启和关闭。

stdout和1相比,1更靠近底层,stdout是C语言的概念。

  • 我们来看一段代码:
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>

int main()
{
	close(1);

	int fd = open("log.txt", O_CREAT | O_APPEND | O_WRONLY, 0644);

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

	const char* str1 = "write\n";
	const char* str2 = "printf\n";
	const char* str3 = "fprintf\n";

	write(1, str1, strlen(str1));
	printf("%s", str2);
	fprintf(stdout, "%s", str3);

	fork();

	fflush(stdout);

	close(fd);
	return 0;
	return 0;
}

结果为:在这里插入图片描述

  • write只打印了一次,而printf和fprintf却打印两次。

fprintf 和 printf是库函数,write是系统调用。
缓冲区(一块内存)
显示器:行缓冲(碰到\n,就刷新)
文件:全缓冲(只有写满时,才刷出)

解释:输出重定向,将本该打印到显示器的数据打印到了文件中。缓冲方式由行缓冲变为全缓冲,所以只有写满时,才刷出。fork之后,父子进程数据独立,各自私有一份,于是它们各持一份缓冲区中的printf和fprintf,在fflush之后,两个进程刷新出数据,于是就被打印了两次。

write是系统调用,不受重定向影响,因此也不会受缓冲方式影响。
说明我们以前说的缓冲区是用户级缓冲区,是由C标准库提供的。

fflush :是把用户级缓冲区往操作系统里刷新,数据缓冲是语言提供的缓冲。

重定向是会影响缓冲方式。

使用 dup2 系统调用

#include <unistd.h>
	int dup2(int oldfd, int newfd);
  • 作用:使newfd 是 oldfd的一份拷贝。
  • 拷贝的含义:把newfd里面指针所指的内容拷给oldfd。(在数组里面进行内容的拷贝,文件描述符的值并不变!)
  • 例如:dup2(fd, 1);往1中重定向,所以这个文件就可以用两个文件描述符找到它。
    为了防止文件描述符泄漏,要即时关闭oldfd:close(fd);

重定向前,若newfd已经有打开的文件,则会关闭
重定向后,oldfd和newfd都会操作oldfd所操作的文件

追加重定向的文件打开方式:O_APPEND;
普通重定向的文件打开方式:O_TRUNC。

三、理解文件系统

inode

为了更好的理解inode,我们先来了解一下磁盘文件系统图,磁盘是典型的块设备,硬盘分区被划分为一个个的block。一个block的大小是由格式化的时候确定的,并且不可以更改。
在这里插入图片描述

定位数据时依次通过盘面,柱面,扇区来缩小范围。
因此我们可以将磁盘的分区抽象成一个数组,空间区域太大,这便是划分的意义。
在这里插入图片描述
文件系统一样,也需要划分。
文件 = 属性 + 内容

// 文件系统:描述整个空间的使用情况。
filesystem{
	// 基本情况:
	// 空间一共多大?分区有多少已经使用/没有被使用
	// inode、block有多少
	// group
	// 方法
}

在这里插入图片描述

  • Block Group:ext2文件系统会根据分区的大小划分为数个Block Group。而每个Block Group都有着相
    同的结构组成。政府管理各区的例子
  • Super Block(超级块):存放文件系统本身的结构信息。记录的信息主要有:block 和 inode的总量,未使用的block和inode的数量,一个block和inode的大小,最近一次挂载的时间,最近一次写入数据的时间,最近一次检验磁盘的时间等其他文件系统的相关信息。Super Block的信息被破坏,可以说整个文件系统结构就被破坏了。
  • Group Descriptor Table(块组描述符GDT):描述块组属性信息,具体一个分组里的详细情况,是对两个位图的高度概括。
  • Block Bitmap(块位图):Block Bitmap中记录着Data Block中哪个数据块已经被占用,哪个数据块没有被占用。
  • inode Bitmap(inode位图):每个bit表示一个inode是否空闲可用。
  • inode table:存放文件属性。如文件大小、所有者、最近修改时间等。
  • data blocks(数据区):存放文件内容。

其中,一个文件中:

inode{	        //一个
//属性集合
}
data blocks{	//可能有多个
//内容集合
}

inode:存放文件的属性【文件 : inode = 1:1】
data blocks:存放文件的数据【文件 : inode : data blocks = 1:1:n】

一个文件的inode必须与它的data blocks关联起来,也就是说inode中必须要包含与data blocks对应的映射关系。
这样,在找文件时,先找到它的inode,然后找映射关系,就自然找到它的数据了。

  • 那么,如何找到inode?
    inode id:分区内唯一的编号,用来标识一个inode。
  • inode id如何分配?
    inode bitmap:inode位图。inode table中的inode空间有没有被分配出去,由inode位图决定。
    blocks bitmap:相同的,blocks的分配情况由blocks位图决定。
    位图可以简单理解成一串比特位,0代表该位置没有被分配出去,1代表已占用。

因此,在硬盘中创建一个文件的过程如下:
首先,找到对应分区以及块组(由目录决定),然后为它分配inode,在inode位图里找到一个没有被使用过的0比特位,置为1,把文件所对应的属性信息填到对应的inode中;
再去block位图中分配一个位置,把内容填到对应的blocks中,然后把数据块编号写到inode映射关系里。这样就将文件的属性和内容关联起来了。

描述查找一个文件:
首先找到文件所在的分区及块组,然后拿着inode id 在inode table中找到对应的inode信息,获取属性,根据inode中与blocks的映射关系,找到数据块,将对应数据获取出来。

描述删除一个文件
先找到。然后在位图中,将该inode对应的有效编号由1置为0,数据块由1置0。
(这也就间接说明,为什么我们从U盘中拷贝一个大文件需要几分钟,而删掉它只需几秒。在删除时,清几个比特位即可,inode和blocks都不会被清空,所以删除是可以恢复滴【前提是必须知道对应文件的inode,且要在它被覆盖之前】找到并将对应的inode和blocks置1即可恢复。)

因此,文件系统做区域划分,分区管理有很大意义。每一个分区又分为许多块组,我们只需研究管理一个块组,便可管理整个文件系统。

所以,站在系统的角度:
文件 = inode(属性集合) +数据块(内容集合,内含文件名和inode id)

软硬链接

所以,真正找到磁盘上文件的并不是文件名,而是inode。 在linux中可以允许多个文件名对应于同一个inode。 在这里插入图片描述
执行ln log.txt h_link
在这里插入图片描述我们发现,二者共用一个inode id,且链接数(这里是硬链接)为2。

执行ln -s log.txt link
在这里插入图片描述
link具有独立的inode,是一个独立的文件。

硬链接

和指向的文件共享同一个inode,不是一个独立的文件。

软链接

具有独立的inode,是一个独立的文件(内容存放的是它所指向文件的路径),当可执行程序所在路径较深时,可在项目顶层建立链接,直接运行链接就可以。

现象理解

我们先创建一个文件mkdir dir ,默认的链接数为2:
在这里插入图片描述

  • 这里的链接数(硬链接),代表有多少个文件用名字和dir的inode建立了关系。

这是因为:dir里存有.(指向当前目录)和. .(指向上级目录)
所以除了dir本身和它的inode建立了映射关系之外,还有目录中的.也和它的inode建立了关系。

可以发现.的inode id与dir的inode id 一样。
在这里插入图片描述

在dir目录里面创建temp目录mkdir temp发现dir的硬链接数变为了3
在这里插入图片描述
这是因为:dir本身和它的inode建立关系 + 目录中.与它的inode建立关系 + temp目录中的…与dir的inode建立关系。

总结:

  • Linux系统允许,多个文件名指向同一个inode号码。这意味着,可以用不同的文件名访问同样的内容,这时对文件内容进行修改,会影响到所有文件名,但是,删除一个文件名,不影响另一个文件名的访问。这种情况就被称为硬链接

除了硬链接以外,还有一种特殊情况。

  • 文件A和文件B的inode id虽然不一样,但是文件A的内容是文件B的路径。读取文件A时,系统会自动将访问者导向文件B。因此,无论打开哪一个文件,最终读取的都是文件B。这时,文件A就称为文件B的软链接。这意味着,文件A依赖于文件B而存在,如果删除了文件B,打开文件A就会报错:“No such file or directory”。

软链接与硬链接最大的不同:文件A指向文件B的文件名,而不是文件B的inode id,因此,文件B的inode链接数不会发生变化。


  • 5
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值