通过上述“先描述再组织”的方式,可以将内存中所有打开的文件管理起来。可如何将进程和进程打开的文件关联起来呢?
文件描述符
内核中,每个进程都有一个内核数据结构叫做struct files_struct
,用来描述进程的相关文件信息。
我们只关心该结构体最后一个字段,文件结构体指针数组struct file* fd_array[]
,用于存放进程所打开的文件结构体的地址。
将数组fd_array
下标作为文件描述符,来为进程的每个文件编号,文件描述符的本质就是内核文件数组的下标。
用户层只通过文件描述符访问文件,不必涉及底层细节。
int fd = open ("./log.txt", O_WRONLY | O_CREAT, 0664);
用数组作为结构体最后一个字段,也就是变长数组。
文件描述符的性质
文件描述符可以唯一的映射到对应的文件,那么调用系统接口也可以直接对0
, 1
,2
号文件进行读写。
write(1, "hello world\n", 12); // 向1号文件写入
write(2, "hello world\n", 12); // 向2号文件写入
char buff[64] = {0};
read(0, buff, sizeof(buff) - 1); // 向0号文件读取
文件描述符的分配规则:分配文件描述符数组中最小且没有使用的位置,作为新文件的fd。
文件描述符和文件指针
文件描述符是系统对上提供的,用来唯一标识一个文件的。FILE*
文件指针是C语言语言层提供给C语言程序,用来唯一标识一个文件的。
FILE
文件结构体,内部是C语言关于文件属性的集合,必定要封装系统提供的文件描述符。
printf("%d\n", stdin->_fileno); // stdin/stdout/stderr中封装的有fd
printf("%d\n", stdout->_fileno);
printf("%d\n", stderr->_fileno);
重定向
#include <unistd.h>
int dup2(int oldfd, int newfd);
把 oldfd 指向的文件地址拷贝给 newfd 位置,自此就可以通过 newfd 访问老文件了。
oldfd 和 newfd 看作两个变量,dup2 作用就是将 oldfd 赋值给 newfd。
在系统内部,修改进程对应文件描述符表的特定下标位置的内容,致使该 fd 指向其他文件。这个过程上层是无法感知的。
// 输入重定向
int fd = open("log.txt", O_RDONLY);
dup2(fd, 0);
// 输出重定向
int fd = open("log.txt", O_WRONLY);
dup2(fd, 1);
重定向和父子进程
- 进程程序替换后dup2的作用并不会消失。修改代码数据和修改进程文件描述符表,二者并不冲突。
- 进程的
files_struct
也是内核数据结构,也会被子进程继承下来。
虚拟文件系统
进程默认打开标准输入输出错误文件,分别对应着键盘和显示器设备。如何做到“一切皆文件”的呢?
不同的硬件设备,如键盘显示器等,它们的驱动程序提供给操作系统的读写接口肯定是不一样的。操作系统为避免差异统一管理,在驱动层上添加了一层虚拟文件系统vfs
,也就是一堆文件结构体的集合。
文件结构体中只声明读写函数的形式,由底层设备驱动提供具体的读写函数实现。
当有数据来时,系统先通过系统的读写接口将数据拷贝到对应文件的缓冲区中,再通过设备驱动提供的读写接口将数据真正送到硬件上。
系统不需要关心底层设备细节,只需调用对应文件结构体读写方法即可。这样就能以文件视角统一的看待所有设备。这就是系统层面的多态。
文件结构体像是只声明读写函数的父类对象,设备驱动像是拥有读写函数具体实现的子类对象。
这就是Linux系统一切皆文件的实现方法。
1.2 文件操作接口
open/close
#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);
int creat(const char\* pathname, mode_t mode);
#include <unistd.h>
int close(int fd);
pathname | flags | mode | 返回值 |
---|---|---|---|
文件的路径 | 打开模式 | 文件权限 | 成功返回文件描述符,失败返回–1 |
flags
是打开模式标志位,本质是32位整型位图,可以将多个宏参数按位或传入。
如:O_WRONLY
代表只写,O_CREAT
代表创建,O_RDONLY
代表只读,O_TRUNC
表示清空原有内容。
这些宏都是互不相同且只有一个比特位为1的整数。可以将多个宏或起来传入,简单且高效。
mode
是八进制整数,用来设置该文件的权限信息。需要提前设置程序文件权限掩码umask
。
如:0664
,从前向后每一位分别代表用户组员其他人的读写执行权限。
- 和C文件接口
fopen
不同,open
以写方式打开,默认不会清空文件内容。写入会覆盖原有内容。
创建文件时必须要指定文件权限。已知文件存在可以不管权限。
umask(00);
int fd = open("log.txt", O_CREAT | O_WRONLY, 0666);
int fd = open("log.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);
int fd = open("log.txt", O_CREAT | O_WRONLY | O_APPEND, 0666);
printf("fd: %d, errno: %d, errdisc: %s\n", fd, errno, strerror(errno));
close(fd);
write/read
#include <unistd.h>
ssize_t write(int fd, const void\* buf, size_t count);
ssize_t read (int fd, void\* buf, size_t count);
fd | buf | count | 返回值 | |
---|---|---|---|---|
write | 文件描述符 | 写入字符串 | 写入的字符个数 | 真正写入的字符个数 |
read | 文件描述符 | 读取数组 | 读取的字符个数 | 真正读取的字符个数 |
//write
ssize_t s = write(fd, "hello log.txt\n", strlen(msg));
assert(s);
//read
char buff[1024] = {0};
ssize_t s = read(fd, buff, sizeof(buff) - 1);
assert(s);
1.3 缓冲区
先用示例代码,引出缓冲区的存在。
close(1);
int fd = open("./log. txt", O_WRONLY, O_CREAT, 0664);
printf("hello printf\n");
//close(fd);
// 如果不close,log.txt会存在内容。 如果close,log.txt不会存在内容
用户缓冲区和内核缓冲区
语言会提供语言层面的缓冲区,属于用户缓冲区。内核层也存在缓冲区,叫做内核缓冲区。数据会先后经过语言和内核缓冲区,最后到达硬件设备。
C语言缓冲区实际上就是
FILE
结构体中指向的一段用户空间。
调用语言级的文件读写函数如,本质是:
- 先将数据写入到用户缓冲区;
- 再通过系统调用,刷新用户缓冲区的内容到内核缓冲区;
- 最后,内核刷新数据到磁盘或其他外设中。
也就是说,通过语言库函数读写文件,都必将数据输出到用户缓冲区,再交给系统输出到内核,并不会直接到外设上。
用户缓冲区的刷新策略有三种:
- 不缓冲,立即刷新;
- 行缓冲,一行结束就刷新缓冲区;一般显示器是行刷新。
- 全缓冲,缓冲区满了才刷新。一般向磁盘文件写入是全缓冲,最后进程退出时还会统一刷新一次。
此时再回看本节开头的例子代码。
close(1);
int fd = open("./log. txt", O_WRONLY, O_CREAT, 0664);
printf("hello printf\n");
//close(fd);
// 如果不close,log.txt会存在内容。 如果close,log.txt不会存在内容
重定向1号文件为普通文件,输出一行后数据仍在用户缓冲区中。此时直接关闭系统文件流,会导致用户缓冲区的内容最终无法刷新到文件中。
避免这种情况可以手动 fflush 刷新用户缓冲区,或者关闭C文件流而不是系统文件流。
write(3, "hello write\n", 12);
fprintf(file_ptr, "hello printf\n");
close(1); // 关闭文件
write
直接输出到内核缓冲区,不会被 close 关闭文件而“拦截”;printf
是语言库函数,要经用户缓冲区再到内核缓冲区,会被 close 关闭文件“拦截”住。
缓冲区和子进程
write(fd, "hello write\n", 12);
fprintf(fp, "hello printf\n");
fork(); // 创建子进程
执行结果如图所示:
hello write
hello printf # 父进程
hello printf # 子进程
子进程创建时,printf 等库函数输出的数据仍在C缓冲区中。C缓冲区是存在于C语言文件结构体FILE
中的内存空间。
故C缓冲区属于程序代码和数据,父子进程是共享的。任意一方刷走C缓冲区的数据就会触发写时拷贝,剩下一方必定也会刷新走属于自己的那一份数据。
也就是说,只要子进程创建之后,数据仍然存在于用户缓冲区中,那么父子进程必然会都刷新一遍。
当然,可以在提前刷新缓冲区,fork 之时就不会存在数据写实拷贝了。
2. 磁盘文件
文件没被打开时是存储在磁盘上的,接下来就是探究磁盘如何合理的存储文件。
2.1 硬盘的物理结构
文件就是文件内容和文件属性的集合。文件如果没有被打开,则是存储在磁盘上的。
磁盘是计算机中的唯一的机械设备。虽然机械硬盘几乎退出桌面市场,但它具有容量大,价格便宜,寿命长的优点,所以企业中一般都使用机械硬盘存储数据。
磁盘整体由盘片、主轴、磁头、机械臂以及其他部分组成,其中主轴带动多个盘片高速转动,由机械臂带动磁头在盘片上寻找指定位置进行读写。
- 主轴上套有一摞盘片,每一个盘片有两个面,每个面都有一个磁头。
- 盘片表面多个同心圆划分出“磁道”,半径所在直线划出“扇面”,扇面和磁道划分出“扇区”。每个扇区大小一般为512字节。
- 磁头首先要移动到指定的磁道,盘片不停旋转,等待指定的扇区到来,即可进行读写。
这就是CHS定位法。
抽象逻辑结构
CHS是磁盘硬件的定位方式,如果直接将该方法写入内核。就会使内核代码冗余度高、耦合度高、难以维护。
操作系统会对磁盘这样的块设备,抽象出统一的逻辑结构。
由于磁头很小,可以看成一个点,磁盘虽然是圆形,对于磁头来说,磁道是线性的一条直线。故可以将磁盘抽象成线性结构,如下图所示:
磁盘可以抽象成元素为磁道的数组,磁道抽象成元素为扇区的数组,通过下标可以定位任意一个扇区。系统IO的基本单位为4KB,我们将8个扇区看作一个块,通过地址也能定位任意一个块,这就是逻辑块地址LBA。
LBA和CHS地址通过简单数学运算就可以转化,这样系统和磁盘的交互方式就打通了。
2.2 磁盘区域的管理
系统是如何按LBA逻辑结构去管理磁盘的呢?以ext2文件系统为例。
- 将一整块硬盘,划分成多个磁盘分区。
- 每个磁盘分区都有一个存储磁盘系统加载信息的启动块
Boot Block
和多个分组Block Group
。
磁盘文件的存储
每一个块组Block Group
内部,又被划分出了不同区域,这里才是存储文件的开始:
内容 | 解释 |
---|---|
Super Block | 存储整个分区文件系统的属性信息。 备份于多个分组中,防止故障 |
Group Descriptor Table | 存储本组的使用详细统计信息,如组内 inode 和 data block 的使用情况 |
Inode Table | 实际存储文件属性的区域 |
Data Blocks | 实际存储文件内容的区域 |
Block Bitmap | 便于在Data Block 查找空block 的辅助位图 |
Inode BitMap | 便于在Inode Table 查找空inode 的辅助位图 |
Linux 存储文件是将文件的内容和属性分离存储的。
-
Inode Table
:是实际存储文件属性的区域。- 这块区域也被划分成很多
inode
节点,用来存储单个文件的属性,大小一般为128Bytes。 - 一个文件一个
inode
,所以inode
编号可以唯一标识文件。
- 这块区域也被划分成很多
-
Data Blocks
:是实际存储文件内容的区域。- 这块区域又划分成很多
block
块,用来存储单个文件的内容,大小一般为4KB。 inode
结构体中有block
的索引数组。
- 这块区域又划分成很多
Block BitMap
,Inode BitMap
:遍历Inode Table
找空余inode
效率太低。因此使用两个位图来帮助我们查找空余位置。- 位图的下标对应每个
inode/block
的位置下标。 - 位图的元素值为1表示该
inode/block
被占用、为0表示inode/block
未使用。
理解inode
//inode结构体,存储文件的属性信息
struct inode {
// 文件的所有属性
int inode_number;
int blocks[NUM]; // 存储该文件所占有的所有block的下标
};
NUM个block大小终究是有上限的,如何做到一个文件的大小无上限呢?
blocks数组存储的是文件所占用的block的索引下标。其中分为直接索引和二级索引甚至多级索引。
- 直接索引:直接就是存储文件内容的block的下标。
- 二级索引:对应的block中不存储文件内容,存储文件后续使用的block的下标,依此找到后续block。
文件系统会根据文件的大小,来规定一级索引和多级索引的数量。
理解文件增删查改
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上运维知识点,真正体系化!
加入社区》https://bbs.csdn.net/forums/4304bb5a486d4c3ab8389e65ecb71ac0
是有上限的,如何做到一个文件的大小无上限呢?
blocks数组存储的是文件所占用的block的索引下标。其中分为直接索引和二级索引甚至多级索引。
- 直接索引:直接就是存储文件内容的block的下标。
- 二级索引:对应的block中不存储文件内容,存储文件后续使用的block的下标,依此找到后续block。
文件系统会根据文件的大小,来规定一级索引和多级索引的数量。
理解文件增删查改
[外链图片转存中…(img-EXjrKEDQ-1725712086667)]
[外链图片转存中…(img-75voE9YR-1725712086668)]
[外链图片转存中…(img-x4HY1TeV-1725712086668)]
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上运维知识点,真正体系化!
加入社区》https://bbs.csdn.net/forums/4304bb5a486d4c3ab8389e65ecb71ac0