期末复习笔记¶
Gemini Deep Research¶
Prompt:
我正在学习“操作系统”课程,本课程基本基于Unix v6系统(实际是v6++,tongji使用cpp重写的版本)。请你根据...章节的部分内容,为我生成自学友好的文档来详细讲解,帮助我进行期末复习。注意减少繁琐的前置知识讲解(已在前序章节复习过),重点放在本部分PPT内容详细讲解即可。
我正在学习“操作系统”课程,本课程基本基于Unix v6系统(实际是v6++,tongji使用cpp重写的版本)。请你根据进程管理章节习题课的内容,为我生成详细的期末复习。需要包括流程图的详细讲解说明(主),以及例题的简要分析(次)。
由于章节前后关联很大,如果不说“减少繁琐的前置知识讲解”的话,Gemini会用大量篇幅阐述前序章节知识,导致当前PPT文件的重点不够突出。
期末考试范围¶
时钟中断与系统调用,以及所有S16之后的内容。
U4 进程管理¶
4.1 UNIX的进程调度状态¶
2个调度方式 6个调度状态 5个内核函数
- 睡眠优先级:
p_pri数值越小,优先级越高,进程重要性高,但也意味着睡眠可中断(反直觉) - 抢占调度发生时机:中断/系统调用处理即将返回用户态时,通过RunRun触发
- 抢占只能发生在进程从核心态返回用户态的时刻,但是核心态不可抢占
Process类¶
proc 结构体是内核管理进程的最小集合。因为即使进程不在内存中,内核调度器(Scheduler)仍需知道该进程的存在,以便在条件满足时将其换回,所以proc 表(process 数组)必须常驻内存。
Process类的设计体现了Unix“简单即美”的哲学。我们可以将PCB中的成员变量按功能划分为四大类:标识信息、图像位置信息、调度控制信息、信号与终端信息。
进程标识信息¶
这一组变量用于唯一地标记系统中的每一个进程,是进程存在的“身份证”。
p_uid(short): 用户ID。标识进程所属的用户,用于权限控制。p_pid(int): 进程标识符。内核分配给进程的唯一数字编号。p_ppid(int): 父进程标识符。记录创建当前进程的父进程PID。
p_ppid的存在至关重要。如果父进程在子进程结束前意外终止,子进程将成为“孤儿进程”,通常会被PID为1的init进程收养。这种层级关系保证了系统中没有游离的、不可控的执行实体。
进程图像的位置信息¶
Unix进程的执行需要内存支持。PCB必须记录进程的代码段(Text)、数据段(Data)和栈(Stack)在物理内存或磁盘交换区中的位置。
p_addr(unsigned long): 指向该进程Per-Process Data Area (PPDA)区(包含核心栈和User结构)在物理内存中的起始地址。- 当进程被调度上台时,内存管理单元(MMU)的寄存器会被设置为该值,从而建立进程的虚拟地址空间。
p_size(unsigned int): 进程图像(除代码段以外部分)的长度,以字节或块为单位。- 用于内存分配和交换(Swapping)时计算需要传输的数据量。
*p_textp(Text): 指向代码段描述符的指针。- Unix支持正文共享(Shared Text),即多个运行相同程序(如vi编辑器)的进程可以共享同一块只读的物理内存代码区。
p_textp使得PCB不需要重复记录代码段信息,大大节省了内存。
- Unix支持正文共享(Shared Text),即多个运行相同程序(如vi编辑器)的进程可以共享同一块只读的物理内存代码区。
调度相关信息¶
-
p_stat: 枚举类型(ProcessState),表示进程当前的调度状态,是调度器Swtch函数决策的直接依据。SRUN:就绪/运行(Unix不区分)。SSLEEP:高优先级睡眠,进程正在等待一个“不可中断”的事件(如磁盘 I/O 完成)。此时进程优先级通常 < 0。SWAIT:低优先级睡眠,进程正在等待一个“可中断”的事件(如用户输入、父进程等待子进程结束)。此时进程优先级通常 ≥ 0。SIDL:创建中,尚未初始化完成,不可调度。SZOMB:僵尸态,进程已终止但其父进程尚未调用wait收集其退出状态。SSTOP:停止态,进程被信号(如SIGSTOP)暂停执行。
-
p_flag(int): 位掩码,用于记录一些非互斥的状态,特别是与交换(Swapping)相关的状态。SLOAD:进程是否驻留在内存中。SSYS:系统进程标志,不可换出。SLOCK:进程被锁定在内存中,禁止交换。这通常用于正在进行 DMA(直接内存访问)操作的进程,防止物理内存地址发生变化导致硬件传输错误。SSWAP:进程正在交换中,防止重复调度。
-
p_pri(int): 进程优先数。数值越小表示优先级越高。p_pri = min { 255, ( p_cpu / 16 + PUSER + p_nice ) }
p_cpu(int): CPU使用率的度量值。计算动态优先级的核心参数,用于实现多级反馈队列算法,防止CPU密集型进程长期霸占处理器。- 每秒60次的时钟中断中,现运行进程
p_cpu+1,一秒计时到的时钟中断中,所有进程的p_cpu减去调度魔数。
- 每秒60次的时钟中断中,现运行进程
-
p_nice(int): 用户可通过nice命令调整的修正值。允许用户主动降低自己进程的优先级(即增加p_nice值),体现了Unix系统的协作精神。 -
p_time(int): 驻留时间。记录进程在内存中或在交换区中停留了多久。Swapper进程(0号进程)利用这个值来决定“换出谁”(在内存呆最久的)或“换入谁”(在磁盘等最久的),以防止抖动(Thrashing)。 p_wchan(unsigned long): 等待通道(Wait Channel)。当进程处于睡眠状态(SSLEEP或SWAIT)时,该变量记录了进程正在等待的事件(通常是某个内核对象的地址,如缓冲区头部的地址)。它是实现同步机制的核心。
信号与控制台终端¶
p_sig(int): 信号位图。每一位代表一种信号(如SIGINT, SIGKILL)。当进程被调度运行前,内核会检查此字段,若非零则处理信号。*p_ttyp(TTy): 指向控制该进程的终端结构。用于处理终端信号(如Ctrl+C产生的SIGINT)。
4.2 UNIX的进程切换调度¶
-
WakeUpAll丢失唤醒问题:某一时刻中断处理调用WakeUpAll把目标进程的p_wchan=0、p_stat=SRUN;但随后睡眠的进程返回到 Sleep 的代码继续执行,把p_stat又写回SSLEEP。结果进程的状态:p_wchan=0(没有记录等待事件),但p_stat=SSLEEP(标记为睡眠),永远无法被WakeUp唤醒。 -
SetPri优先级重算- 整数秒,重算所有用户态就绪进程、现运行进程的优先数;
- 现运行进程优先级下降,设置
RunRun标志,表示需要重新调度- 抢占:如果
RunRun > 0,当前进程不会立即返回用户态执行下一条指令,而是自觉调用Swtch,让出 CPU
- 抢占:如果
- 现运行进程优先级下降,设置
- 系统调用末尾,重算现运行进程的优先数;
- 刷掉核心态下的优先级,恢复计算获得的用户态优先级
- 整数秒,重算所有用户态就绪进程、现运行进程的优先数;
RunRun标志- 设置时机:
- 当一个高优先级的进程(如刚刚 I/O 完成苏醒的进程,或者刚创建的子进程)进入
SRUN状态,且其优先级优于当前运行进程时。 - 当前进程的时间片用完,或者优先级因计算而降低时
- 当一个高优先级的进程(如刚刚 I/O 完成苏醒的进程,或者刚创建的子进程)进入
- 检查时机
- 在系统调用返回用户态前
- 在中断处理程序返回用户态前
- 设置时机:
Swtch上下文切换- 保存现场
- 现运行进程改为
0#- 为0#建立核心态页表
- 用0#的
u_rsav[2]数组恢复esp、ebp寄存器
- 调用
Select选择新进程- 选择条件:从上次查找到的进程位置开始线性循环扫描process数组,找图像在内存的优先级最高的就绪进程
- 即:
Process::SRUN == process[i].p_stat && (process[i].p_flag & Process::SLOAD) != 0的所有进程中,p_pri的值最小(或p_time最大,即驻留时间最长,防止饥饿)
- 即:
- 如果没有找到合适进程,则CPU进入空闲等待
- 选择条件:从上次查找到的进程位置开始线性循环扫描process数组,找图像在内存的优先级最高的就绪进程
- 现运行进程改为新进程
- 恢复现场
- 映射 U 区:将新进程的
p_addr加载到内存管理单元(MMU)或内核映射寄存器,使得内核的固定 U 区地址映射到新进程的 U 区 - 设置新进程用户页表
- 用新进程的
u_rsav[2]数组恢复 esp、ebp 寄存器
- 映射 U 区:将新进程的
- 执行
ret弹出新进程的返回地址,跳转到新进程的核心态执行流 - 注意:
- 非抢占式调度:Swtch的第一棒和最后一棒一定是不同的进程
- 抢占式调度:如果现运行进程优先级下降但依然是最高的,则可能选中自己,第一棒和最后一棒是同一个进程
4.3 UNIX的进程创建与终止¶
- 创建子进程的关键:为子进程创建一个能正确上台的图像
- 并发控制
fork()之后,父进程和子进程谁先执行?- 进程切换和创建的核心?
user结构¶
- 当进程不运行时(被换出),内核不需要访问其打开的文件或核心栈。因此,u 区被设计为进程“图像”的一部分,可一同被换出。
| 特性 | proc 结构 (Process Table Entry) | user 结构 (U-Area) |
|---|---|---|
| 驻留性 | 永久常驻物理内存 (Resident) | 可被交换到磁盘 (Swappable) |
| 访问频率 | 高频(调度器每时每刻都在扫描) | 仅当进程为“当前运行进程”时被访问 |
| 包含内容 | 调度状态, 优先级, PID, 图像指针 | 核心栈, 打开文件表, 系统调用参数, 寄存器现场 |
| 生命周期 | 从 fork 创建到父进程 wait 回收 | 从 fork 创建到 exit 变为僵尸前 |
| 关键字段 | p_stat, p_pri, p_addr | u_rsav, u_ar0, u_error |
进程创建:fork与NewProc¶
NewProc流程¶
- 资源预分配与槽位搜索
- 搜索空闲槽位:遍历
process数组,找到p_stat == NULL的第一个槽位 - 唯一标识符生成:分配新的
p_pid
- 搜索空闲槽位:遍历
- 元数据复制 (
proc结构)p_textp,p_flag,p_nice等字段直接复制p_ppid设置为父进程的p_pidp_stat设置为SRUN,p_pri初始化为0(最高优先级)
- 资源引用计数的维护
- 代码段(Text Segment):如果父进程有代码段(程序是“纯代码”,指令只读、共享),增加其引用计数
x_count和x_ccount - 打开文件表:增加每个打开文件的引用计数
f_count - 当前工作目录:增加目录的引用计数
i_count
- 代码段(Text Segment):如果父进程有代码段(程序是“纯代码”,指令只读、共享),增加其引用计数
- 图像复制(
fork开销最大部分)- 内存申请(
p_size) - 内容复制:数据段、用户栈、U区
- 页表建立:
- 代码段相同(映射到相同的物理页框 0x201)
- 数据段、堆栈段:父进程 0x203,子进程新建
- 内存申请(
NewProc 的栈帧魔术与返回机制:子进程是如何返回的?¶
u_rsav(Register Save):保存进程上下文切换时的关键寄存器:ESP (栈指针) 和 EBP (基址指针)-
两次返回的实现逻辑
-
父进程的返回路径:
- 完成所有复制工作。
NewProc函数正常执行到底。- return 0。
- 在
ProcessManager::Fork中,判断if (NewProc() == 0),进入 else 分支。 - 将子进程的 PID 写入用户态寄存器 EAX(通过
u.u_ar0[EAX])。 - 系统调用结束,返回用户态,返回值 = Child PID。
- 子进程的返回路径:
- 子进程被创建后,处于
SRUN状态,暂停在内存中。 - 调度:稍后,调度器 (Swtch) 选中子进程。
- 恢复现场:Swtch 使用子进程
u.u_rsav恢复栈指针。 - 苏醒:子进程在
NewProc内部“醒来”(就像刚执行完 SaveU 一样)。 - 逻辑分叉:内核代码必须通过某种方式(如检查
p_pid或由调度器强制设置返回值)让NewProc在子进程上下文中返回。 - 在
ProcessManager::Fork中,判断if (NewProc())为真。 - 清除返回值:代码显式将
u.u_ar0[EAX]设置为 0。 - 时间清零:清除
u_cstime等计时器。 - 系统调用结束,返回用户态,返回值 = 0。
| 阶段 | 父进程行为 | 子进程行为 | 备注 |
|---|---|---|---|
| 调用 Fork | 发起 int 0x80 中断 | (不存在) | |
| 进入 NewProc | 申请资源,SaveU 保存现场 | (不存在) | 关键点:保存点在父进程 |
| 内存复制 | 将自身镜像拷贝给子进程 | 诞生:拥有父进程的栈副本 | 子进程拥有了“在 NewProc 中”的记忆 |
| NewProc 返回 | 返回 0 | (挂起在 SRUN 状态) | 父进程继续执行 |
| 调度切换 | (可能被挂起或继续) | 被 Swtch 选中上台 | |
| 现场恢复 | - | 从 u_rsav 恢复,仿佛在 NewProc 中 | “时间旅行”发生 |
| Fork 层处理 | 收到 0,写入 Child PID | 收到 1,写入 0 | 通过返回值区分身份 |
Fork 后的竞态条件(?)¶
一般而言,不存在子进程抢在父进程之前返回用户态的情况。但是,如果系统负载太重,有可能出现0#进程换入子进程前,父进程一直没有机会运行的偶发情况,此情形下子进程有可能先于父进程返回用户态。
进程终止与同步:Exit 与 Wait¶
略。
4.4 UNIX的进程图像交换¶
进程图像¶
- 进程控制块(PCB)
- 代码段(Text Segment)
- 数据段、堆栈段(Data and Stack Segment)
-
核心态数据区(Per-Process Data Area, PPDA/u-area)
-
BufferManager::Swap函数:
bool BufferManager::Swap(
int blkno, // 交换区中的磁盘块号
unsigned long addr, // 内存起始物理地址
int count, // 传输字节数
enum Buf::BufFlag flag // 传输方向:Buf::B_READ 或 Buf::B_WRITE
)
- 异步 I/O
- 启动 I/O
- 进程入睡
- 中断唤醒
- 恢复执行
0 号进程:系统的调度枢纽与 Swapper¶
- 执行
ProcessManager::Sched()死循环- 信号量
RunOut:表示交换出去了,等待换入(目前磁盘没有处于就绪状态进程)RunIn:表示想换入,但进不来。
- 信号量
4.5 进程通信¶
- 临界资源:只能被一个进程在某一时刻使用的资源,包括硬件资源和软件资源
- 临界区:进程中访问临界资源的代码段
- 相关临界区:不同进程中访问同一临界资源的临界区
互斥机制的演进与信号量设计¶
- 互斥准则:空闲让进、忙则等待、有限等待、让权等待
- 早期方案
- 开关中断:
- 优点:简单高效
- 缺点:只能用于单处理器系统,用户态无法使用(用户可能关中断后不打开,导致系统死机)
- 锁变量:
- 优点:可用于多处理器系统,用户态可用
- 缺点:忙等待,浪费CPU时间片
- 开关中断:
- 信号量(Semaphore)
value:表示可用资源数P:Proberen(申请/等待)V:Verhogen(释放/增加)
- 结构定义:
// Unix v6++ 信号量结构示意
struct semaphore {
int value; // 信号量的值
struct sem_queue *head; // 等待该信号量的进程队列头指针
};
void P(semaphore &s) {
s.value = s.value - 1; // 先减1
if (s.value < 0) {
// 资源不足(减1前为0或更小)
// 将当前进程状态置为睡眠(阻塞)
// 将当前进程插入s.head指向的等待队列
sleep();
}
}
void V(semaphore &s) {
s.value = s.value + 1; // 先加1
if (s.value <= 0) {
// 说明加1之前s.value是负数,即有进程在等待
// 从s.head队列中唤醒一个进程
wakeup();
}
}
- 模式实现:
- 互斥模式:保护临界区,防止冲突。其特点是P/V操作在同一个进程中成对出现,夹住临界区。
- 同步模式:进程间协作,防止竞态。其特点是P操作和V操作出现在不同进程中,形成“先行者-后继者”关系。
// 互斥模式
Process A:
P(mutex); // 关锁
// 临界区代码 (如 count++)
V(mutex); // 开锁
// 同步模式
Process Predecessor (先行者):
// 执行任务...
V(proceed); // 发送消息:我做完了,你可以继续了
Process Successor (后继者):
P(proceed); // 等待消息:如果先行者没发V,我就阻塞在这里
// 执行后续任务...
U5 设备管理¶
5.1 I/O硬件系统¶
- DMA(Direct Memory Access,直接存储器存取)
- 允许高速外设直接与主存交换数据
- 设备控制器(Device Controller),或I/O接口:
- 连接CPU和外设的硬件模块
- 主要功能
- 接受和识别命令
- 包含寄存器,用于存放命令、状态和数据
- 数据交换和缓冲
- 状态监控和报告
- 差错控制
- 接受和识别命令
- I/O通道(I/O Channel)
- 进一步简化,专用于I/O操作的处理器(协处理器)
5.2 I/O软件系统¶
-
分层架构
- 用户层
- 设备无关层
- 设备命名与映射、缓冲管理、设备调度、错误报告
- 驱动层
- 将上层抽象请求转换为具体设备命令
- 中断层
- 硬件层
-
核心控制表结构
- 系统设备表(SDT, System Device Table)
- 设备控制表(DCT, Device Control Table)
- 控制器控制表(COCT, Controller Control Table)
- 通道控制表(CHCT, Channel Control Table)
- 逻辑设备到物理设备的映射(LUT)
- 逻辑名 -> LUT -> 物理设备号 -> SDT -> DCT -> COCT -> CHCT
- 只有当设备、控制器、通道三者都成功分配后,I/O操作才能真正启动
5.3 磁盘存储器管理¶
- 磁盘
- 磁道(Track):盘面上的同心圆。
- 柱面(Cylinder):所有盘面上半径相同的磁道集合。
- 扇区(Sector):磁道被划分成的若干个弧形区域,是磁盘读写的最小单位。
- 缓冲管理
- \(T\):数据读入时间;\(C\):CPU处理时间;\(M\):缓冲区数据复制到用户空间时间
- 单缓冲:\(Max(C, T) + M\)
- 双缓冲:\(Max(C, T)\)
- 循环缓冲区:\(Max(C, T/n)\),n为缓冲区块数
- 磁盘调度算法
- 先来先服务:按照请求到达顺序服务。
- 最短寻道时间优先:优先选择距离当前磁头位置最近的请求。
- 扫描算法(SCAN, 电梯算法):磁头在一个方向上移动,服务所有请求,直到到达最远端,然后反向移动。
- 无饥饿,性能较好,但是响应时间不均匀,远端请求等待时间长。
- 循环扫描算法(C-SCAN):磁头在一个方向上移动,服务所有请求,到达最远端后迅速返回起点,继续向一个方向移动。
- 响应时间均匀,但寻道时间略长。
5.4 UNIX字符块设备管理¶
缓存管理¶
-
缓冲控制块(Buffer Control Block):描述缓冲区状态、归属设备及队列位置的所有关键信息,对应
class Buf结构b_flags:状态标志位B_READB_WRITEB_DONE:I/O操作完成,表示缓冲区内的数据是有效的(对于读操作)或已成功写入磁盘(对于写操作)B_BUSY:互斥锁,不应该出现在自由链表中B_WANTED:争用标记,表示有进程在等待该缓冲区变为可用- 当前进程
brelse后,若发现B_WANTED被置位,则必须发出wakeup唤醒等待进程
- 当前进程
B_ASYNC:异步I/O标志B_DELWRI:延迟写标志,表示逻辑上是“空闲”的(可以放在自由链表上),但用之前必须先写回磁盘
b_dev:所属设备号- 高8位的主设备号:标识驱动程序
- 低8位的次设备号:标识具体物理单元,如磁盘分区
b_blkno:逻辑块号b_addr:缓冲区在内存中的首地址b_wcount:读写计数器,传输字节数b_resid:剩余字节数b_error:错误码b_forw、b_back:设备哈希队列指针- 哈希函数:
hash(dev, blkno) = (dev + blkno) mod N (dev, blkno)快速查找缓存
- 哈希函数:
av_forw、av_back:自由链表指针- 所有当前未被标记为
B_BUSY的缓冲区都链接在全局双向循环的自由链表上 - 使用LRU管理空闲缓冲区
- 所有当前未被标记为
-
getblk:使用权分配- !!五种场景
- 缓存命中且空闲
- 缓存未命中,从自由链表分配
- 缓存未命中,遭遇脏数据: 写回磁盘后分配
- 缓存未命中,自由链表为空(资源耗尽):
sleep等待 - 缓存命中但被锁定(资源争用):
B_WANTED+sleep等待
- !!五种场景
-
bread:读操作- 调用
getblk获取缓冲区 - 检查
B_DONE标志- 已完成:直接返回
- 未完成:发起读请求,
sleep等待I/O完成
- 调用
- 写操作
- 同步写:
bwrite- 元数据更新,需要完整性
- 异步写:
bawrite- 脏块清空,或者预读逻辑
- 延迟写:
bdwrite:设置B_DELWRI->设置B_DONE->brelse放回自由链表- 直到重用,或强制刷新时才写回磁盘
- 同步写:
brelse:释放缓冲区- 检查
B_WANTED,若置位则wakeup - 清理所有标志位(
B_BUSY等) - 正常数据:插入自由链表尾部(LRU)
- 异常数据:插入自由链表头部(优先重用)
- 检查
块设备读写过程¶
略。
U6 文件系统¶
6.1 文件系统概述¶
基本概念¶
- 文件系统接口
- 命令接口
- 系统调用接口
-
文件类型
- 按用途:
- 系统文件
- 库文件
- 用户文件
- 按存取方式:
- 只读
- 读写
- 可执行
- 按组织形式
- 普通文件
- 目录文件
- 特殊文件(设备文件、管道文件、链接文件等)
- 按用途:
-
物理结构:混合索引结构(三级索引结构)
- 空闲盘块的管理:成组链接法
- 目录结构:带勾连的树形结构
- 逻辑结构:管理文件和存储空间
- 用户接口层
- 系统实现层
- 存储空间分配
- 文件目录管理
- 文件存取控制(权限)
- 文件读写
- 层级结构:进程 -> 打开文件表 -> inode
- 用户进程中的文件描述符表
- 打开文件时,分配文件描述符,指向系统全局文件表项
- 系统打开文件表
- 记录当前被打开的文件的状态信息(
File结构)
- 记录当前被打开的文件的状态信息(
- 内存
inode表- 记录文件的元数据(权限、大小、位置等)和磁盘块地址索引
- 系统将活动文件的索引节点(FCB)从磁盘读取到内存,形成内存inode表
- 磁盘
inode区和数据区- 磁盘上存储文件元数据和实际数据的区域
- 用户进程中的文件描述符表
用户界面¶
- 文件打开:
- 顺序存取:默认一次读写的起始位置是上一次读写结束位置的下一个字节
fd = open ( name, mode)name:文件名mode:文件主:文件主同组用户:其他用户- 读(r)
- 写(w)
- 执行(x)
n = read ( fd, buf, nbytes)n:实际读到的字节数fd:文件描述符buf:进程地址空间中存放读回数据的首址nbytes:要读的字节数
n = write ( fd, buf, nbytes)n:实际写入的字节数
- 随机存取:
seek ( fd, offset, ptrname)ptrname:位置指针的参照位置0:+offset1:当前位置+offset,可正负2:文件末尾-offset
- 顺序存取:默认一次读写的起始位置是上一次读写结束位置的下一个字节
- 文件的勾连与取消
6.2 文件的逻辑结构与物理结构¶
基本概念¶
- 目录管理的本质是解决“按名存取”的问题。将文件名与文件元数据分离,可以提高检索效率。
- 目录项(Directory Entry)
- 文件名:28bytes
- 索引节点号:4bytes
- 逻辑结构:从用户角度(应用程序层)观察到的文件的组织形式,是程序可直接处理的数据及其结构
- 物理结构:文件在存储介质上实际存储的形式,是文件在磁盘上存储的字节流
- 连续结构
- 链接结构
- 隐式:指针信息存储在每个数据块中(通常在块末尾或开头存放指向下一块的指针),目录项只需记录链表的起始块(还有可选的末尾块以便快速追加)
- 不支持高效的随机访问;可靠性较差,指针损坏会导致文件不可读
- 显式(文件分配表FAT):将所有指针信息存储在内存的一张文件分配表中,目录项只需记录文件的起始块索引,即FAT表的入口
- 支持随机访问,但是FAT占用大量内存
- 隐式:指针信息存储在每个数据块中(通常在块末尾或开头存放指向下一块的指针),目录项只需记录链表的起始块(还有可选的末尾块以便快速追加)
- 索引结构:为每个文件建立一个独立的索引块,其中存储该文件各逻辑块对应的物理块号。目录项保存指向该文件索引块的指针。
- 单级索引
- 多级索引
- 混合索引(Unix)
磁盘布局结构¶
自底向上(从0号物理块开始):
- 引导块(Boot Block):存放引导程序,通常是0号块
- 超级块(Super Block):记录了文件系统的全局属性,如总盘块数、空闲盘块数、空闲inode数等关键管理信息,通常是1号块(或200-201号块)
- 频繁读写。内存中保留副本
- 索引节点区(Inode Area):存放文件的索引节点(Inode),每个文件必须对应一个Inode,紧接超级块之后(如202-1023号块)
- 每个 Block (512B) 可容纳 8 个 DiskInode (64B),系统的最大文件数由此区的大小决定
- 频繁读写。通过 iget/iput 机制在内存和磁盘间交换
- 文件数据区(File Data Area):存放文件的实际数据内容,以及目录文件的目录项
索引节点inode¶
// 外存文件控制块DiskInode(外存索引节点,64个字节)
class DiskInode
{
public:
unsigned int d_mode; /* 状态的标志位 */
int d_nlink; /* 该文件在目录树中不同路径名的数量 */
short d_uid; /* 文件所有者的用户标识数 */
short d_gid; /* 文件所有者的组标识数 */
int d_size; /* 文件大小,字节为单位 */
int d_addr[10]; /* 文件逻辑块号和物理块号转换的混合索引表 */
int d_atime; /* 最近访问时间 */
int d_mtime; /* 最后修改时间 */
};
三级索引结构¶
- 每个盘块:512字节
- 每个索引块:128个块号指针(4字节/指针)
-
lbn = Offset / 512:逻辑块号 -
小文件(直接索引,6个):
- 块数:6块(0-5号),由6个直接指针覆盖
- 最大容量:6x512B=3KB
index0 = lbn;- 所有数据块地址都存在inode中,访问时无需额外索引块,速度类似连续分配的直接计算
- 大文件(一次间接索引,2个):
- 块数:256块,范围:6~261(6+2x128-1)
- 最大容量:3+256x512B=131KB
index0 = (lbn - 6) / 128 + 6;:找到间接索引块号index1 = (lbn - 6) % 128;:找到块内偏移
- 巨型文件(二次间接索引,2个):
- 块数:2x128x128=32768块,范围:262~33029(6+256+32768-1)
- 最大容量:33029x512B≈16MB
index0 = (lbn – 6 - 128 * 2 ) / (128 * 128) + 6index2 = ( (lbn - (128 * 2 + 6) ) / 128) % 128index1 = (lbn - (128 * 2 + 6) ) % 128-> 最终数据块号
文件打开结构与三级表机制(Process Table, File Table, Inode Table)¶
核心数据结构¶
- 内存索引节点(In-Core Inode)
i_count: 记录当前有多少个系统文件表项(File Object)指向该 Inode。仅当i_count == 0时,该内存 Inode 才会被释放或重用
- 文件物理删除的唯一条件是
i_nlink == 0 && i_count == 0
class Inode : public DiskInode {
public:
//... DiskInode 成员的副本...
// 以下为内存扩充字段
unsigned int i_flag; // 动态状态标志,用于预读算法
unsigned int i_mode; // 权限与类型,如 ILOCK (锁定), IUPD (数据已修改), IACC (被访问)
int i_count; // 引用计数(核心)
short i_dev; // 设备号
int i_number; // Inode 编号
int i_lastr; // 上次读取的逻辑块号(用于预读算法)
//...
};
- 系统打开文件表(System Open File Table)
f_offset: 记录当前文件读写指针的位置(相对于文件开头的偏移量,以字节为单位)。Unix 将此属性放在 File 对象而非 Inode 对象中,意味着读写位置是与“打开实例”绑定的,而非与“文件本身”绑定。
class File {
public:
unsigned int f_flag; // 打开模式 (Read, Write, Pipe...)
int f_count; // 引用计数 (有多少个进程 fd 指向此对象)
Inode* f_inode; // 指向内存 Inode 的指针
int f_offset; // 【关键】文件读写指针 (R/W Pointer)
};
- 进程打开文件表(User Open File Table)
- 位于
User结构中,每个进程维护自己的文件描述符表
- 位于
文件关闭¶
- 用户级释放: 将当前进程
u_ofiles[fd]置空。 - File 对象释放: 找到对应的 File 对象,执行
f_count--。- 分支判断: 如果
f_count > 0(说明还有其他进程或 fd 在使用,如 fork 后的子进程),则操作到此为止,返回。 - 如果
f_count == 0,说明该打开实例已失效,释放 File 对象,进入下一步。
- 分支判断: 如果
- Inode 对象释放: 找到 File 指向的 Inode,执行
i_count--。- 分支判断: 如果
i_count > 0(说明还有其他独立打开该文件的实例),则操作到此为止。 - 如果
i_count == 0,说明文件已彻底不再被访问。- 回写检查: 检查
Inode.i_flag是否包含IUPD(内容曾被修改)。若有,调用IUpdate将 Inode 元数据写回磁盘。 - 锁释放: 清除内存 Inode,允许该表项被其他文件重用。
- 回写检查: 检查
- 分支判断: 如果
文件读写¶
略。
6.3 文件存储空间管理¶
SuperBlock¶
class SuperBlock {
public:
int s_isize; // Inode 区占用的盘块数,用于界定数据区的起始位置
int s_fsize; // 盘块总数,用于边界检查
// --- 空闲数据块管理核心 ---
int s_nfree; // 直接管理的空闲盘块数量(栈顶指针)
int s_free; // 直接管理的空闲盘块索引表(栈本身)
// --- 空闲 Inode 管理核心 ---
int s_ninode; // 直接管理的空闲 Inode 数量
int s_inode; // 直接管理的空闲 Inode 索引表
// --- 同步与锁机制 ---
int s_flock; // 空闲盘块锁(Free Block Lock)
int s_ilock; // 空闲 Inode 锁(Inode Lock)
int s_fmod; // 修改标志(SuperBlock Modified flag),即脏位
int s_ronly; // 只读文件系统标志
int s_time; // 最近一次更新时间
int padding; // 填充位,确保 SuperBlock 大小对齐 1024 字节
};
s_isize:Inode区域的大小(盘块数)。s_fsize:文件系统总大小。s_ninode:超级块中空闲Inode栈的栈顶指针(当前空闲Inode数量)。s_inode:直接管理的空闲Inode编号数组(作为栈使用)。- 分配:
s_ninode>0:s_inode[--s_ninode]返回一个空闲Inode号s_ninode==0: 从Inode区扫描,补充空闲Inode栈
- 回收:
s_ninode<100:s_inode[s_ninode++] = ino将Inode号压入栈s_ninode==100: 忽略该Inode号
- 分配:
s_nfree:超级块中空闲盘块栈的栈顶指针。s_free:直接管理的空闲盘块号数组(作为栈使用)。s_ilock / s_flock:用于互斥访问的锁标志。s_fmod:内存副本修改标志,指示是否需要回写磁盘。
成组链接法¶
- 分配
s_nfree递减;s_nfree>0: 返回s_free[s_nfree]s_nfree==0: 组切换,刚才分配出去的s_free块,是指向下一组的“链头”- 必须先将该块(原s_free)中的索引数据(下一组的nfree和free表)读入到超级块的s_nfree和s_free数组中
- 然后再返回
s_free[s_nfree]
- 回收
s_nfree<100: 将盘块号压入s_free[s_nfree++]= 被回收盘块号s_nfree==100: 组切换- 将当前
s_nfree和s_free数组写入被回收盘块中,作为新组的“链头” - 重置
s_nfree = 1,s_free[0] = 被回收盘块号
- 将当前
6.4 文件系统的目录管理¶
FileManager::NameI¶
- 功能:将用户传入的路径字符串转换为目标文件的内存 Inode 指针
Mode:OPEN/CREATE/DELETE- 目录检索流程
- 路径判定
/:绝对路径,调用iget(ROOTINO)获取根目录(Inode 1)的内存副本,i_count增 1- 非
/:相对路径,使用进程的当前工作目录u_cdir作为起点,i_count增 1 Inode设置为/
- 路径解析
- 权限检查,是否有执行搜索权限
- 根据
i_addr索引表,逐块读取目录数据盘块,线性扫描目录项 - 若找到匹配的文件名:
- 若路径未结束,且当前 Inode 是目录,继续下一段路径解析
- 若路径结束,返回该 Inode 指针
- 终结处理
- 根据
ModeOPEN: 返回找到的 Inode 指针CREATE/DELETE: 返回父目录 Inode 指针
- 根据
- 路径判定
- 读写流程
- 异步更新机制
- 目录修改:
NameI修改的是Buffer Cache中的目录数据块,标记为BDEWRI,延迟写 - Inode修改:
write改变文件大小,标记Inode为IUPD,延迟写 - 一致性风险
- 文件已分配但无目录项:如果 Inode 分配并写入了磁盘,但包含文件名的目录块还在内存中未写入,断电后该文件将成为“孤儿”,占用空间却无法访问
- 目录项存在但 Inode 无效:如果目录项写入了磁盘,但新分配的 Inode 还在内存中,断电后目录项将指向一个垃圾 Inode 或空闲 Inode,导致严重的数据错误
- 目录修改: