跳转至

期末复习笔记

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不需要重复记录代码段信息,大大节省了内存。
调度相关信息
  • 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减去调度魔数。
  • 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=0p_stat=SRUN;但随后睡眠的进程返回到 Sleep 的代码继续执行,把 p_stat 又写回 SSLEEP。结果进程的状态:p_wchan=0(没有记录等待事件),但 p_stat=SSLEEP(标记为睡眠),永远无法被WakeUp唤醒。

  • SetPri优先级重算

    • 整数秒,重算所有用户态就绪进程、现运行进程的优先数;
      • 现运行进程优先级下降,设置RunRun标志,表示需要重新调度
        • 抢占:如果 RunRun > 0,当前进程不会立即返回用户态执行下一条指令,而是自觉调用 Swtch,让出 CPU
    • 系统调用末尾,重算现运行进程的优先数;
      • 刷掉核心态下的优先级,恢复计算获得的用户态优先级
  • RunRun标志
    • 设置时机:
      • 当一个高优先级的进程(如刚刚 I/O 完成苏醒的进程,或者刚创建的子进程)进入 SRUN 状态,且其优先级优于当前运行进程时。
      • 当前进程的时间片用完,或者优先级因计算而降低时
    • 检查时机
      • 在系统调用返回用户态前
      • 在中断处理程序返回用户态前
  • 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进入空闲等待
    • 现运行进程改为新进程
    • 恢复现场
      • 映射 U 区:将新进程的 p_addr 加载到内存管理单元(MMU)或内核映射寄存器,使得内核的固定 U 区地址映射到新进程的 U 区
      • 设置新进程用户页表
      • 用新进程的 u_rsav[2] 数组恢复 esp、ebp 寄存器
    • 执行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

进程创建:forkNewProc

NewProc流程
  1. 资源预分配与槽位搜索
    1. 搜索空闲槽位:遍历process数组,找到p_stat == NULL的第一个槽位
    2. 唯一标识符生成:分配新的p_pid
  2. 元数据复制 (proc 结构)
    1. p_textp, p_flag, p_nice等字段直接复制
    2. p_ppid设置为父进程的p_pid
    3. p_stat设置为SRUN, p_pri初始化为0(最高优先级)
  3. 资源引用计数的维护
    1. 代码段(Text Segment):如果父进程有代码段(程序是“纯代码”,指令只读、共享),增加其引用计数x_countx_ccount
    2. 打开文件表:增加每个打开文件的引用计数f_count
    3. 当前工作目录:增加目录的引用计数i_count
  4. 图像复制(fork开销最大部分)
    1. 内存申请(p_size)
    2. 内容复制:数据段、用户栈、U区
    3. 页表建立:
      1. 代码段相同(映射到相同的物理页框 0x201)
      2. 数据段、堆栈段:父进程 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#进程换入子进程前,父进程一直没有机会运行的偶发情况,此情形下子进程有可能先于父进程返回用户态。

进程终止与同步:ExitWait

略。

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_READ
      • B_WRITE
      • B_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_forwb_back:设备哈希队列指针
      • 哈希函数:hash(dev, blkno) = (dev + blkno) mod N
      • (dev, blkno)快速查找缓存
    • av_forwav_back:自由链表指针
      • 所有当前未被标记为B_BUSY的缓冲区都链接在全局双向循环的自由链表上
      • 使用LRU管理空闲缓冲区
  • getblk:使用权分配

    • !!五种场景
      1. 缓存命中且空闲
      2. 缓存未命中,从自由链表分配
      3. 缓存未命中,遭遇脏数据: 写回磁盘后分配
      4. 缓存未命中,自由链表为空(资源耗尽): sleep等待
      5. 缓存命中但被锁定(资源争用): 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:+offset
          • 1:当前位置+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) + 6
    • index2 = ( (lbn - (128 * 2 + 6) ) / 128) % 128
    • index1 = (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结构中,每个进程维护自己的文件描述符表
文件关闭
  1. 用户级释放: 将当前进程 u_ofiles[fd] 置空。
  2. File 对象释放: 找到对应的 File 对象,执行 f_count--
    • 分支判断: 如果 f_count > 0(说明还有其他进程或 fd 在使用,如 fork 后的子进程),则操作到此为止,返回。
    • 如果 f_count == 0,说明该打开实例已失效,释放 File 对象,进入下一步。
  3. 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_nfrees_free数组写入被回收盘块中,作为新组的“链头”
      • 重置s_nfree = 1s_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 指针
    • 终结处理
      • 根据Mode
        • OPEN: 返回找到的 Inode 指针
        • CREATE/DELETE: 返回父目录 Inode 指针
  • 读写流程
  • 异步更新机制
    • 目录修改:NameI修改的是Buffer Cache中的目录数据块,标记为BDEWRI,延迟写
    • Inode修改:write改变文件大小,标记Inode为IUPD,延迟写
    • 一致性风险
      • 文件已分配但无目录项:如果 Inode 分配并写入了磁盘,但包含文件名的目录块还在内存中未写入,断电后该文件将成为“孤儿”,占用空间却无法访问
      • 目录项存在但 Inode 无效:如果目录项写入了磁盘,但新分配的 Inode 还在内存中,断电后目录项将指向一个垃圾 Inode 或空闲 Inode,导致严重的数据错误