之前在讲IO操作的时候,其中系统级IO中的open,write,read,close都用到了文件描述符(file descriptor),其中open的返回值为文件描述符,write、read和close都是在传参的时候需要传文件的文件描述符。
那么,文件描述符到底是个什么样的概念呢?
简单地来说,文件描述符就是一个小整数,它是非负数,最小值为0,操作系统内核利用文件描述符来访问文件。而实际上,它是一个索引,指向内核为每一个进程所维护的该进程打开文件的记录表。当我们打开一个文件时,内核要在内存中创建数据结构来描述目标文件,于是便有了我们的file结构体,它表示一个已经打开的文件对象。而当进程执行open系统调用接口的时候,我们需要让进程和文件关联起来,每个进程都有一个文件指针*files,它指向一张文件结构表。这张表里最重要的东西就是一个指针数组,里面存放的都是指向各种文件的指针。而文件描述符,就是这个指针数组的下标。
从图中可以看到,系统在打开一个文件的时候,会先默认打开三个文件标准输入,标准输出,标准错误,它们三个分别占据了这个文件描述符数组的前三个,也就是下标0,1,2。这样我们新打开一个文件,这个文件的文件描述符只有被存放到3中了,那么一定是每次打开第一个文件,它的文件描述符都是3吗?我们通过一段代码来看一下。
#include
#include
#include
#include
#include
#include
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
#include
#include
#include
#include
#include
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
#include
#include
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,根目录的上层目录就是他自己
存储一个文件的过程
从磁盘上读取一个文件的过程
往文件里写文件的过程
程序调用write函数请求写文件
write函数根据给的文件描述符参数,拿到对应文件的文件表项,在文件表项中拿到目录项模块,找到对应文件的inode
在inode中,根据文件表项中的偏移量计算出要往文件中写的页
通过inode找到文件对应的address_space
在address_space中访问该文件的页缓存树,查找对应的页缓存节点:
a). 如果页缓存命中,那么直接把文件内容修改更新在页缓存的页中,写文件就结束了;这时候文件修改位于页缓存,并没有写回到磁盘文件中去;
b). 如果页缓存缺失,那么发生缺页异常,将会创建一个缓存页,通过inode找到文件该页的磁盘地址,读取相应的页填充该缓存页;然后重新查找缓存页
一个页缓存中的页如果被修改,那么会被标记成脏页。脏页需要写回到磁盘中的文件块。有两种方式可以把脏页写回磁盘:
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下常用的快捷方式,或者是复制了一份文件从而变成一份新的文件。当我们删除新的文件的时候,原文件不会随之删除,反之如果删除原文件,那么软链接就变成了死链接。
动态库和静态库
下面我们通过一段程序来生成静态库和动态库
add.h
add.c
sub.h
sub.c
main.c
生成静态库
ar是gnu归档工具,rc表示替换或创建
t:列出静态库中文件
v:详细信息
如上图,我们可以看到程序运行处了正确的结果,此时我们删除静态库,程序依然可以运行成功。
生成动态库
使用动态库
编译选项
gcc main.c -o main -L . -lmymath
运行动态库