目录
0 文件系统
硬:磁盘上的一个文件由i节点(元数据+数据块索引表)和数据块组成。
软: 一个被打开的文件在系统内核中通过文件表项和v节点加以标识
0.1 文件系统的逻辑结构
一个磁盘驱动器被划分成一到多个分区,每个分区上都建有独立的文件系统,每个文件系统包括:
引导块:计算机加电启动时,ROM BIOS从这里读取可执行代码和程序,以完成操作系统自 举。
超级块:记录文件系统的整体信息,如文件系统的格式和大小,i节点和数据块的总量、使用 量和剩余量等。
柱面组若干:每个柱面组包括:
超级块副本:同上
柱面组信息:柱面组的整体描述
i节点映射表:是i节点号与i节点磁盘位置的对应表(类似字典偏旁部首表,便于快速找到)
块位图:位图中每个二进制位对应一个数据块,1和0代表块的占用/空闲状态
i节点表:包含若干i节点,记录文件的元数据(即文件属性)和数据块索引表
数据块集:包含若干数据块,存储文件的内容数据
0.2 i节点(index node, inode)
i节点,即索引节点(index node, inode),包括元数据(文件属性,类似ls -l命令查看)和数据块索引表,详情如下:
文件类型和权限
文件的硬连接数
文件的用户和组
文件的字节大小
文件的最后访问时间、最后修改时间和最后状态改变时间
文件数据块索引表(溢出的话👇)
0.3 数据块(data block)
每个块的大小为512/1024/4096字节,包括直接快、间接块:
直接块:存储文件的实质内容数据
文件块:存储普通文件的内容数据
目录块:存储目录文件的内容数据
间接块:存储下级文件数据块索引表(我来接👆)
0.4 硬链接(hard link)
每个文件对应一个i节点号,可对应多个文件名。对应几个,就有几个硬链接。
每个文件对应一个i节点号,i节点号与文件名的对应关系信息存储在文件所在目录中。(目录也是文件,存的是目录下每个文件的文件名名与其i节点号的对应关系,一i 一文件 多文件号)
硬链接只能引用同一文件系统中的文件。它引用的是文件在文件系统中的物理索引(也称为inode)。当您移动或删除原始文件时,硬链接不会被破坏,因为它所引用的是文件的物理数据而不是文件在文件结构中的位置。硬链接的文件不需要用户有访问原始文件的权限,也不会显示原始文件的位置,这样有助于文件的安全。如果您删除的文件有相应的硬链接,那么这个文件依然会保留,直到所有对它的引用都被删除。——百度百科,硬链接
软链接类似于win的快捷方式。
0.5 文件访问流程
针对给定的文件名,从其所在目录中可以得到与之对应的i节点号,再通过i节点映射表查到该i节点在磁盘上的具体位置,读取i节点信息并从中找到数据块索引,进而找到相应的数据块,最终获得文件的完整内容,如下图。
1 文件的类型
通过ls -l命令可以查看文件类型,如普通文件 -,目录文件 d等等。
1.1 普通文件 -
在Unix/Linux系统中通常见到的文件,如C/C++语言编写的源代码文件,编译器、汇编器和链接器产生的汇编文件、目标文件和可执行文件,各种系统配置文件,Shell脚本文件,音视频等数字内容的多媒体文件,乃至包括数据库在内的各种应用程序所维护、管理和使用的数据文件等,都是普通文件。
组成文件的线性数组里字节的数目,即文件长度或文件大小,其最大值受限于Linux内核中用于管理文件的C语言代码数据类型的大小,某些文件系统还可能强加自己的限制,将其限定在更小的值。
操作系统内核并没有对并发文件访问强加任何限制,不同的进程能够同时读写一个文件,并发访问的结果取决于独立操作的顺序,且通常是不可预测的。
一个文件包括两部分数据数据,元数据(文件的类型、权限、大小、用户、组、各种时间戳等)存储在i节点中(数据块索引表也在i节点中),内容数据存储在数据块中。
1.2 目录文件 d
系统通过一个i节点号唯一标识一个文件的存在(可以有多个文件名),但人们更愿意使用有意义的文件名来访问文件。目录就是用来建立文件名和i节点之间的映射的。
目录的本质就是一个普通文件,与其他普通文件的唯一区别是它仅仅存储文件名(不是文件)和i节点号的的映射,每一个这样的映射,用目录中的一个条目表示,称为硬链接。
既然目录也是文件,那么它同样也有自己的i节点,每个目录的文件名(目录名)和它的i节点号之间的映射,记录在父目录中,以此类推,形成一棵目录树。
根目录的i节点在i节点表中的存储位置是固定的,因此虽然没有父目录,根目录也能被正确检索。
打开路径的本质就是找一串i节点号:
当系统内核打开类似"/home/t/uc/04.txt"这样的路径时,它会从根目录开始遍历路径中的每一个目录项来查找下一项的i节点号。根目录的i节点可以直接拿到,这样就可以在根目录中找到home目录的i节点号,然后在home目录中找到t目录的i节点号,再在t目录中找到uc目录的i节点号,最终在uc目录中找到04.txt文件的i节点号并打开该文件。
如果路径字符串的第一个字符不是'/',则表示相对路径,路径解析从当前工作目录开始。
每个目录中都有两个特殊的条目:.和..分别映射该目录本身和其父目录的i节点好,根目录没有父目录,故其.和..均映射到根目录本身。
1.3 符号链接(软链接)文件 l
通过 ln -s xxx.c yyy.c可以创建一个符号链接。
符号链接文件看上去像普通文件,每个符号链接文件都有自己的i节点和包含被链接文件完整路径名的数据块。
相比于硬链接,符号链接的解析需要更大的系统开销,因为有效地解析符号链接至少需要解析2个文件——符号链接文件本身和它所链接的文件。
文件系统会为硬链接维护链接计数,但不会为符号链接维护任何东西,因此相较于硬链接,符号链接缺乏透明性。
特殊文件(1.4 ~ 1.7):
特殊文件是以文件形式表示的内核对象,具体包括:
1.4 本地套接字 s
套接字是网络编程的基础,本地套接字是其面向本机通信的一个变种,它需要依赖文件系统中的一种特殊文件——本地套接字文件。
1.5 字符设备 c
设备驱动将字节按顺序写入队列,用户程序从队列中按其被写入的顺序将字节依次读出,如键盘。
1.6 块设备 b
设备驱动将字节数组映射到可寻址的设备上,用户程序可以任意顺序访问数组中的任意字节,如硬盘。
1.7 有名管道 p
有名管道是一种以文件描述符为信道的进程间通信(IPC)机制,即使是不相关的进程也能通过有名管道文件交换数据。
2 文件的打开与关闭
2.1 open()
共3个版本,这里提供形参最全(3个)个版本:
#include<fcntl.h>
int open(char const* pathname, int flags, mode_t mode);
功能:打开已有的文件或创建新文件
pathname 文件路径
flags 状态标志,表示当前用户执行当前文件时的操作权限。可取以下值:
O_RDONLY 只读
O_WRONLY 只写
O_RDWR 读写
O_APPEND 追加
O_CREAT 不存在即创建,已存在即打开(没有E)
O_EXCL 不存在即创建,已存在即报错
O_TRUNC 不存在即创建,已存在即打开
mode 权限模式。仅在创建新文件时有效,可用形如0664的八进制数表示,
高位到低位依次表示拥有者用户、同组用户和其他用户的读、写和执行权限。
mode权限高于flags。
权限掩码:默认为0002,创建文件时指定权限0666,实际0666&~0002 -->0664。
返回值:当前未被使用的,最小文件描述符(非负整数)。失败返-1。
//open.c 文件的打开和关闭
#include<stdio.h>
#include<fcntl.h>// open()
#include<unistd.h>// close()
int main(void){
//打开文件 返回值文件描述符,表示所打开的文件
int fd = open("./open.txt",O_RDWR | O_CREAT | O_TRUNC,0777);
if(fd == -1){ //试试少一个=,open(非空地址)为真,硬执行perror(),输出SUCCESS 0.0
perror("open");
return -1;
}
printf("fd = %d\n",fd);
//关闭文件
close(fd);
return 0;
}
2.2 close()
#include<unistd.h>
int close(int fd);
功能:关闭处于打开状态的文件描述符
fd 处于打开状态的文件描述符
返回值:成0败-1
2.3 fopen() vs open()
文件 文件描述符fd
stdin 0
stdout 1
stderr 2
FILE*类型 int型
用于标准库函数 用于系统调用函数
fopen(), fseek(), fgets() open(), read(), lseek()
3 文件的内核结构
一个处于打开状态的文件,系统会为其在内核中维护一套专门的数据结构,保存该文件的信息,知道它被关闭。
v节点与v节点表:
文件的元数据和在磁盘上的存储位置都保存在其i节点中,而i节点保存在分区柱面组的i节点表中。在打开文件时将其i节点信息存入内存,并辅以其它必要信息形成一个专门的数据结构,势必会提高对该文件的访问效率,这个存在于进程的内核空间,包含文件i节点信息的数据结构称为v节点。多个v节点结构以链表的形式构成v节点表。
文件表项与文件表
由文件状态标志(来自open函数的flags参数)、文件读写位置(最后一次读写的最后一个字节的下一个位置)和v节点指针等信息组成的内核数据结构被称为文件表项。多个文件表项以链表的形式构成文件表。通过文件表项一方面可以实时记录每次读写操作的准确位置,另一方面可以通过v节点指针访问包括该文件各种元数据和磁盘位置在内的i节点信息。
多次打开同一文件,无论在同一进程还是不同进程,系统内核只产生1个v节点。
每次打开同一文件,都会产生1个新的文件表项,各自维护各自的文件状态标志和当前文件偏移,共享同一个v节点。(开启n次某文件,不关闭,存在n个文件表项,1个v节点。n个close()操作,导致n个文件表项都释放,v节点才会释放。)
打开一个文件意味着内存资源(文件表项、v节点等)的分配,而关闭一个文件其实就是为了释放这些资源。但如果关闭的文件(文件表项跟着释放)在其他文件中处于打开状态,那么其v节点并不会释放,知道系统中所有曾打开过该文件的进程都显示或隐式地将其关闭,其v节点才会被释放。
一个处于打开状态的文件可以被删除,但它所占用的磁盘空间,直到它的v节点彻底消失后才会被标记为自由。
4 文件描述符
文件描述符是数组下标,关联文件表项和v节点。
由文件的内核结构可知,一个被打开的文件在系统内核中通过文件表项和v节点加以标识。
有关该文件的所有后续操作,如读取、写入、随机访问,乃至关闭等,都无一例外地要依赖于文件表项和v节点。因此有必要将文件表项和v节点体现在 完成这些后续操作的函数的参数中。
但这又势必会将位于内核空间中的内存地址暴露给运行于用户空间中的程序代码。一旦某个用户进程出现操作失误,极有可能造成系统内核失稳,进而影响其它正常运行的用户进程,对操作系统的安全运行造成极大威胁。
为了解决内核对象在可访问性与安全性之间的矛盾,Unix系统通过所谓的文件描述符,将位于内核空间中的文件表项间接地提供给运行于用户空间中的程序代码。
为了便于管理在系统中运行的各个进程,内核会维护一张存有各进程信息的列表,称为进程表。系统中的每个进程在进程表中都占有一个表项,即进程表项。每个进程表项都包含了针对特定进程的描述信息,如进程ID、用户ID、组ID(上图未标出)等,也包含了一个被称为文件描述符表(上图标出)的数据结构。
作为文件描述符表项在文件描述符表中的下标(绕,记住是下标即可),合法的文件描述符一定是个非负整数。
每次产生新的文件描述符表项,系统总是从0开始在文件描述符表中寻找最小的未使用项。
每关闭一个文件描述符,无论被其索引的文件表项和v节点是否被删除,与之对应的文件描述符表项一定会被标记为未使用,并在后续操作中被新的文件描述符所占用。
系统内核缺省为每个进程打开3个文件描述符,他们在unistd.h头文件中被定义为3个宏(建议使用宏名称,而不是数字):
#define STDIN_FILENO 0 //标准输入
#define STDOUT_FILENO 1 //标准输出
#define STDERR_FILENO 2 //标准错误
printf()、perror()一定输出到1、2。
1、2对应的文件默认是stdout、stderr,但可以改变——输出重定向:
//redir.c 输出重定向
#include<stdio.h>
#include<unistd.h>
#include<fcntl.h>
int main(void){
// 文件描述符1不再对应显示器,让1对应我指定的文件
// printf --> 1 --> 文件
//文件描述符1空闲
close(1);
//打开新文件,占文件描述符1
int fd = open("./out.txt",O_WRONLY | O_CREAT | O_TRUNC,0664);
printf("fd = %d\n",fd);
return 0;
}
//编译执行,发现输出不在stdout,而是重定向到了out.txt中
上述代码是下列命令的底层逻辑:
./open > abc.txt
5 文件的读写
5.0 注意
写是无限,字符指针:char* buf;
读是有限,字符数组:char buf[32] = {}; (注意减1,如下代码)
5.1 write()
#include <unistd.h>
ssize_t write(int fd, void const* buf, size_t count);
功能:向指定文件写入数据
fd 文件描述符
buf 内存缓冲区,即要写入的数据
count 期望写入的字节数
返回值:成功返回实际写入的字节数,失败返回-1
//write.c 向文件中写入数据
#include<stdio.h>
#include<unistd.h>// close() write()
#include<fcntl.h>// open()
#include<string.h>
int main(void){
//打开文件
int fd = open("./shared.txt",O_WRONLY | O_CREAT | O_TRUNC,0664);//3个形参
if(fd == -1){
perror("open");
return -1;
}
//向文件中写入数据
char* buf = "hello world!";
ssize_t size = write(fd,buf,strlen(buf));
if(size == -1){
perror("write");
return -1;
}
printf("实际写入%ld个字节\n",size);
//关闭文件
close(fd);
return 0;
}
5.2 read()
#include <unistd.h>
ssize_t read(int fd, void* buf, size_t count);
功能:从指定文件中读取数据,
fd 文件描述符
buf 内存缓冲区,存取读到的数据
count 期望读取的字节数:sizeof(buf)-1
返回值:成功返回实际读取的字节数,失败返回-1
//read.c 读取文件
#include<stdio.h>
#include<fcntl.h> // open()
#include<unistd.h>// read() close()
int main(void){
//打开文件 shared.txt
int fd = open("./shared.txt",O_RDONLY);//2个形参。man手册可查看,该函数有3种版本。
if(fd == -1){
perror("open");
return -1;
}
//读取文件
char buf[32] = {};
ssize_t size = read(fd,buf,sizeof(buf)-1); //-1保证buf是字符串,好打印。允许读31个。
if(size == -1){
perror("read");
return -1;
}
printf("实际读取%ld个字节\n",size); //实际读12个,hello world!。
printf("%s\n",buf);
//关闭文件
close(fd);
return 0;
}