浅谈文件描述符及文件系统

之前在讲IO操作的时候,其中系统级IO中的open,write,read,close都用到了文件描述符(file descriptor),其中open的返回值为文件描述符,write、read和close都是在传参的时候需要传文件的文件描述符。

那么,文件描述符到底是个什么样的概念呢?
简单地来说,文件描述符就是一个小整数,它是非负数,最小值为0,操作系统内核利用文件描述符来访问文件。而实际上,它是一个索引,指向内核为每一个进程所维护的该进程打开文件的记录表。当我们打开一个文件时,内核要在内存中创建数据结构来描述目标文件,于是便有了我们的file结构体,它表示一个已经打开的文件对象。而当进程执行open系统调用接口的时候,我们需要让进程和文件关联起来,每个进程都有一个文件指针*files,它指向一张文件结构表。这张表里最重要的东西就是一个指针数组,里面存放的都是指向各种文件的指针。而文件描述符,就是这个指针数组的下标。
这里写图片描述

从图中可以看到,系统在打开一个文件的时候,会先默认打开三个文件标准输入,标准输出,标准错误,它们三个分别占据了这个文件描述符数组的前三个,也就是下标0,1,2。这样我们新打开一个文件,这个文件的文件描述符只有被存放到3中了,那么一定是每次打开第一个文件,它的文件描述符都是3吗?我们通过一段代码来看一下。

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

int main()
{
	//close(0);
	close(2);
	int fd=open("close1",O_RDONLY);
	if(fd<0)
	{
		perror("open!\n");
		return 1;
	}
	printf("%d\n",fd);

	close(fd);
	return 0;
}

这里我们在程序一开始就关闭了文件描述符为0或2的文件(注意不能关1,当然其实也可以,只不过这样我们的结果就出不到终端窗口了,因为1是标准输出),看一下我们的结果
这里写图片描述

这里写图片描述
我们可以看到,我们关了0,那么打开的文件的文件描述符即为0,;我们关了2,那么打开的文件的文件描述符即为2。由此可见,文件描述符的分配规则是,找到当前未被使用的最小的一个下标,作为新打开文件的文件描述符。

关于输出重定向的问题
之前我们说不能关闭1,因为会看不到结果,那么我们就要关闭1,会有什么结果呢?看代码

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

int main()
{
	//close(0);
	//close(2);
	umask(0);
	close(1);
	int fd=open("close1",O_WRONLY|O_CREAT,0666);
	if(fd<0)
	{
		perror("open!\n");
		return 1;
	}
	printf("%d\n",fd);
	fflush(stdout);

	close(fd);
	return 0;
}

这里写图片描述
我们可以看到,本来是要输出到显示器上的内容,输出到了我们的文件close1当中,并且,该文件的fd为1,也就是我们关闭的标准输出的原始文件描述符。这叫做输出重定向,下面我们来看一下它的本质。
这里写图片描述

printf函数的输出结果一般是往标准输出输出的。但stdout在底层寻找文件的时候,还是找的fd=1的文件。这里原来fd=1的文件是stdout,但此时我们改成了close1,所以输出的任何信息都会往该文件中写入,实现了输出重定向。

在这里顺便提下能够输出到显示器的函数printf,fwrite,write进行重定向时的区别
printf和fwrite是C库函数,它们两个自带用户级别的缓冲区。当写入普通文件时,缓冲方式为全缓冲;当写入显示器时,缓冲方式为行缓冲。当进行了输出重定向后,在缓冲区中的数据不会被立即刷新,当进程退出的时候,会统一刷新。而write属于系统调用接口,它没有缓冲区。下面给一份代码来感受一下它们的区别:

#include <stdio.h>
#include <unistd.h>
#include <string.h>

int main()
{
	char* msg1="pringf!\n";
	char* msg2="fwrite!\n";
	char* msg3="write!\n";

	printf("%s",msg1);
	fwrite(msg2,1,strlen(msg2),stdout);
	write(1,msg3,strlen(msg3));

	fork();
	return 0;
}

输入结果如下
这里写图片描述

当我们把输出结果重定向到一个file文件中之后,再来看看结果
这里写图片描述

正如之前所说,在发生输出重定向之时,printf以及fwrite中的缓冲区并没有立即刷新,即便是调用了fork。fork之后,子进程会写时拷贝一份父进程的数据,所以当父进程准备刷新的时候,子进程也就有了同样的一份数据,便有了如上的两份数据。write并没有缓冲区,它是直接输出的,而且是第一个输出,因为别的都还在缓冲。

关于文件系统

Linux中有一个很重要的理念,即“一切皆文件”,任何目录,进程,命令,设备等,归根结底在Linux看来都是文件。它们被分成若干个基本存储单元,存放在磁盘的不同物理地址上,并具有特定的权限。

那么既然任何东西都可以被看成是文件,操作系统就需要来管理文件,而文件系统,就是操作系统来管理文件的方式。简单的说,操作系统通过文件系统来管理分布在磁盘上的各个文件。

通过stat指令可以查看文件的状态信息
这里写图片描述
上图中的Links表示该目录下有的链接数,如果该文件是目录,Links表示该目录还有多少目录(包括隐藏目录.和…);如果该文件是普通文件,Links表示指向该文件的硬链接数加上它本身(即如果有一个硬链接指向该文件,那么该文件的Links就是2)。该数也可以通过stat结构体的st_nlink字段获得

这里我们需要解释几个概念
磁盘 :存放文件的设备,如下图的/dev/sda
这里写图片描述
分区:磁盘上划分出来的空间,如下图的/dev/sda1
这里写图片描述

Block :块,是系统文件读写和存放数据的最小单位。每个Block里只能存放一个文件的数据。如果文件大于Block大小,则该文件会占用多个Block;如果文件小于Block大小,他也会完全占用该Block,剩余的空间也不会再被使用(磁盘空间会浪费)。因此,在对Block大小进行设置的时候要考虑当前系统存放的数据的特点,如果有很多小于Block大小的文件,格式化的时候却把Block设置成较大,这样会造成很多的磁盘空间浪费;反之,如果系统里以大文件居多,这时却把Block设置成较小,固然不会浪费磁盘空间,但也导致Block较多,影响读写性能。

Sector :扇区,是磁盘控制器每次对磁盘进行读写的最小单位。扇区是最小的物理存储的单位,一般为512字节,由磁盘生产商确定,用户改不了。磁盘读到的Sectoer数据会先放在磁盘的缓存里,直到整个Block的所有Sector都缓存到了才会传输给内存,交由文件系统处理。

超级块(super Block):也是一个block,用来记录文件系统的整体信息,包括 inode/block总量,使用量,剩余量,以及文件系统的格式与相关信息等

indoe :这是一个非常重要的概念,是Linux非常厉害的一个设计。它将文件的属性,权限等和文件的数据分开存放。
这里写图片描述

下面我们来看一下操作系统是如何将文件的属性和数据分开来存放的。
我们先创建一个新的文件,如下图所示(ls -i可以显示文件对应的i节点号)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uUdU4oIU-1608188736410)(https://img-blog.csdn.net/20171217123112831?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvbHZ5aWJpbjg5MA==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast)]

目录:也是文件的一种,自然也有自己的inode和block,目录的inode主要记录目录的属性,权限等,目录的block主要记录目录下的文件名和对应的inode
这里写图片描述

目录有以下属性:

1.创建的时候会分配一个inode给目录,如果目录是空的,则不占用block,如果目录下文件过多,可能会占用多个block

2.访问文件时,先访问文件所在目录的inode,验证是否有权限,如果有则访问对应父目录的block,获得该文件对应inode,验证是否有该文件的权限,若有则访问该文件的block

3.父目录的inode从父目录的父目录获得!这样层层递推,Linux对所有文件的访问都是从最上层的根目录开始的

4.根目录的inode是固定的,一般是2号inode,根目录的上层目录就是他自己

存储一个文件的过程

  1. 内核先找到一个空闲的i节点,这里是137,把文件属性信息记录到其中。
  2. 内核在数据区找到几个空闲的磁盘块(假设需要三个磁盘块分别是300,500,800),将内核缓冲区的第一块数据复制到第一块磁盘块(300),往后以此类推,直到存完数据
  3. 文件内容按顺序300,500,800存放。内核在inode上的磁盘分布区记录了上述记录。
  4. 新的文件名为pigff,内核将入口(137,pigff)添加到目录文件。文件名和inode之间的对应关系将文件名和文件的内容及其属性连接起来(如上面目录的图)。

从磁盘上读取一个文件的过程

  1. 程序调用read函数请求读文件
  2. read函数根据给的文件描述符参数,拿到对应文件的文件表项,在文件表项中拿到目录项模块,找到对应文件的inode
  3. 在inode中,根据文件表项中的偏移量计算出要读取的页
  4. 通过inode找到文件对应的address_space(该结构体是用来管理文件(struct inode)映射到内存的页面(struct page)的
  5. 在address_space中访问该文件的页缓存树,查找对应的页缓存节点:如果页缓存命中,那么直接返回文件内容;如果页缓存缺失,那么发生缺页异常,将会创建一个缓存页,通过inode找到文件该页的磁盘地址,读取相应的页填充该缓存页;然后重新查找缓存页
  6. 文件内容读取成功

往文件里写文件的过程

  1. 程序调用write函数请求写文件

  2. write函数根据给的文件描述符参数,拿到对应文件的文件表项,在文件表项中拿到目录项模块,找到对应文件的inode

  3. 在inode中,根据文件表项中的偏移量计算出要往文件中写的页

  4. 通过inode找到文件对应的address_space

  5. 在address_space中访问该文件的页缓存树,查找对应的页缓存节点:
    a). 如果页缓存命中,那么直接把文件内容修改更新在页缓存的页中,写文件就结束了;这时候文件修改位于页缓存,并没有写回到磁盘文件中去
    b). 如果页缓存缺失,那么发生缺页异常,将会创建一个缓存页,通过inode找到文件该页的磁盘地址,读取相应的页填充该缓存页;然后重新查找缓存页

  6. 一个页缓存中的页如果被修改,那么会被标记成脏页。脏页需要写回到磁盘中的文件块。有两种方式可以把脏页写回磁盘:
    a). 手动调用sync()或者fsync()系统调用把脏页写回;
    b). pdflush进程会定时把脏页写回到磁盘;
    注意: 脏页不能被置换出内存,如果脏页正在被写回,那么会被设置写回标记,这时候该页就被上锁,其他写请求被阻塞直到锁释放。

实际上读写文件发生了两次拷贝,从磁盘拷贝到页缓存,再从页缓存拷贝到用户空间。这期间都发生了系统调用,要从用户态切换到内核态。

理解硬链接
事实上,真正找到磁盘上的文件的并不是文件名,而是inode。我们通过硬链接可以让多个文件对应于同一个inode。目录没有硬链接。
这里写图片描述
这里写图片描述
可以看到第一列,两个文件对应的inode都是137,它们被称为指向文件的硬链接。内核记录了这个链接数,inode137对应的硬链接数为2。每次新创建一个文件的时候在删除文件的时候,我们干了两件事:1.将目录中对应的记录删除。2.将文件的硬链接数-1,如果为0,则将对应的磁盘释放。

在用mv命令为一个文件更名的时候,该文件的实际内容并未移动,只需要构造一个指向现有i节点的新目录项,并解除与旧目录项的链接。

以便于简单理解,我们可以把硬链接理解为C++当中的引用。即硬链接的两个文件,实际上都是一个文件,tmp可以当作是pigff的别名。当我们修改其中一个文件的内容时,另一个文件也会随之修改。上图可以看到两者出了名字其余信息全部相同。
这里写图片描述
如上图,我们把ls的输出结果重定向到了tmp文件中,tmp文件的大小从之间的0变到了82,而同时pigff文件的大小也变为了82。
总而言之,硬链接就是两个指向相同inode文件的文件,他们除了名字不同其他都相同。删除原文件,不会对硬链接文件有影响

理解软链接
硬链接是通过inode引用另外一个文件,软链接是通过名字引用另外一个文件。
这里写图片描述
如图我们在ln命令加一个-s选项,便是软链接,他有一个指向关系,从显示结果的第一列我们可以看到 lyb文件的类型是一个链接文件“l”。

简单地理解,我们可以把软链接理解为Windows下常用的快捷方式,或者是复制了一份文件从而变成一份新的文件。当我们删除新的文件的时候,原文件不会随之删除,反之如果删除原文件,那么软链接就变成了死链接。

动态库和静态库

  • 静态库(.a):程序在编译链接的时候把库的代码链接到可执行文件中。程序运行的时候将不再需要静态库。我们用的最多的函数printf便是在链接的时候链接到可执行文件当中的。
  • 动态库(.so):程序在运行的时候才去链接动态库的代码,多个程序共享使用库的代码。
  • 一个与动态库链接的可执行文件仅仅包含它用到的函数入口地址的一个表,而不是外部函数所在目标文件的整个机器码。
  • 在可执行文件开始运行之前,外部函数的机器码由操作系统从磁盘上的该动态库复制到内存中,这个过程称为动态链接。
  • 动态库可以在多个程序间共享,所以动态链接使得可执行文件更小,节省了磁盘空间。操作系统采用虚拟内存机制允许物理内存中的一份动态库被要用到该库的所有进程公用,节省了内存和磁盘空间。

下面我们通过一段程序来生成静态库和动态库
add.h
这里写图片描述
add.c
这里写图片描述
sub.h
这里写图片描述
sub.c
这里写图片描述
main.c
这里写图片描述
这里写图片描述

生成静态库
这里写图片描述
ar是gnu归档工具,rc表示替换或创建
t:列出静态库中文件
v:详细信息

这里写图片描述
-L:指定库路径
-l: 指定库名

如上图,我们可以看到程序运行处了正确的结果,此时我们删除静态库,程序依然可以运行成功。
这里写图片描述

生成动态库

  • shared:表示生成共享库格式
  • fPIC:产生位置无关码
  • 库名规则:libxxx.so

这里写图片描述

使用动态库
编译选项

  • L:链接库所在路径
  • l:链接动态库,只要库名即可(去掉lib以及版本号)
gcc main.c -o main -L . -lmymath

运行动态库

  • 拷贝.so文件到系统共享库路径下,一般是/usr/lib

  • 更改LD_LIBRARY_PATH
    这里写图片描述

  • ldconfig配置/etc/ld.so.conf.d/ , ldconfig更新
    在/etc/ld.so.conf.d目录中创建一个my.conf,里面只有一句话,就是刚才创建的动态库的路径/usr/lib/libmymath.so,保存退出后执行ldconfig。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值