引言:FAT文件系统概述

FAT(File Allocation Table,文件分配表)文件系统是操作系统领域中最经典且广泛使用的文件系统之一。它最初由微软在1977年开发,用于其早期的磁盘操作系统(DOS),随后演变为FAT12、FAT16、FAT32以及exFAT等多个版本。在操作系统实验中,深入理解FAT文件系统不仅能帮助我们掌握文件系统的基本原理,还能提升我们对磁盘管理、数据结构和系统编程的理解。

FAT文件系统的核心优势在于其简单性和跨平台兼容性。它被广泛应用于U盘、SD卡、嵌入式设备以及Windows系统的启动分区中。然而,随着现代文件系统(如NTFS、ext4)的发展,FAT的局限性也逐渐显现,例如不支持大文件、缺乏高级权限管理等。在本指南中,我们将从原理、实现和问题排查三个维度,全面剖析FAT文件系统。

文章将分为三个主要部分:首先,详细解析FAT文件系统的底层原理;其次,通过实际代码示例展示如何在OS实验中实现FAT文件系统的基本操作;最后,提供常见问题排查指南,帮助读者在实验中快速定位和解决问题。无论你是操作系统课程的学生,还是对文件系统感兴趣的开发者,这篇文章都将提供详尽的指导。接下来,让我们从FAT文件系统的基本结构开始。

FAT文件系统的基本原理

FAT文件系统的整体结构

FAT文件系统将磁盘划分为几个关键区域,这些区域共同构成了文件系统的骨架。理解这些区域的布局和功能是掌握FAT原理的第一步。一个标准的FAT文件系统磁盘通常包含以下部分:

  1. 引导扇区(Boot Sector):也称为保留区(Reserved Region)的起始部分,包含文件系统的元数据,如每扇区字节数、每簇扇区数、FAT表数量等。它是系统启动时读取的第一个扇区,如果损坏,整个磁盘可能无法识别。

  2. FAT表(File Allocation Table):这是FAT文件系统的核心,用于记录簇的分配状态。每个簇在FAT表中都有一个条目(entry),表示该簇是空闲的、已分配的、坏簇还是文件的最后一个簇。FAT12使用12位条目,FAT16使用16位,FAT32使用32位。

  3. 根目录区(Root Directory):对于FAT12和FAT16,根目录是一个固定大小的区域,存储根目录下的文件和子目录条目。FAT32则将根目录视为一个普通目录,可以动态增长。

  4. 数据区(Data Region):实际存储文件内容的地方,被划分为簇。文件的内容通过簇链存储,FAT表充当链表指针的角色。

这些区域的布局可以通过一个简单的示意图来理解(假设一个简化的磁盘):

[引导扇区] -> [FAT1] -> [FAT2] -> [根目录] -> [数据区]

在实际磁盘中,FAT表通常有两个副本(FAT1和FAT2),用于冗余备份。如果FAT1损坏,系统可以使用FAT2恢复。

FAT表的工作机制

FAT表是FAT文件系统的灵魂。它本质上是一个数组,每个索引对应一个簇号(从2开始,因为0和1有特殊含义)。簇号0通常表示空闲簇,簇号1表示保留或坏簇。文件的存储通过簇链实现:文件的第一个簇号存储在目录条目中,后续簇通过FAT表链接。

例如,假设一个文件占用簇2、3和5。FAT表的内容可能如下:

  • 簇2的条目指向簇3(值为3)
  • 簇3的条目指向簇5(值为5)
  • 簇5的条目表示文件结束(通常用0xFFF或类似值表示)

这种链表结构使得文件可以非连续存储,但也会导致碎片化问题。在OS实验中,模拟FAT表的更新是实现文件系统的关键步骤。

目录条目结构

FAT文件系统的目录条目(Directory Entry)是一个32字节的结构,用于描述文件或子目录的基本信息。标准FAT12/16目录条目结构如下(以字节偏移表示):

  • 0-7: 文件名(8字节)和扩展名(3字节),不足部分用空格填充。
  • 8-10: 属性字节(如只读、隐藏、系统、卷标、目录、存档)。
  • 11: 保留。
  • 12-13: 创建时间的毫秒部分。
  • 14-15: 创建时间(时、分、秒)。
  • 16-17: 创建日期。
  • 18-19: 访问日期。
  • 20-21: 高16位簇号(仅FAT32使用)。
  • 22-23: 修改时间。
  • 24-25: 修改日期。
  • 26-27: 低16位簇号(文件起始簇)。
  • 28-31: 文件大小(字节)。

对于长文件名(LFN),FAT使用额外的目录条目来存储Unicode名称,这些条目有特殊的属性值(0x0F)。

在OS实验中,解析和创建这些条目是实现文件操作的基础。例如,读取一个文件时,需要先找到其目录条目,获取起始簇号,然后通过FAT表遍历簇链读取数据。

FAT文件系统的版本差异

FAT文件系统有多个版本,主要区别在于支持的磁盘大小和文件大小:

  • FAT12:适用于小容量磁盘(如软盘),簇大小通常为512字节到8KB,最大分区约32MB,文件大小限制为4GB(但实际更小)。
  • FAT16:支持更大分区(最大2GB),簇大小可达64KB,文件大小限制为4GB。
  • FAT32:支持分区高达2TB,文件大小最大4GB,簇大小通常为4KB到32KB。
  • exFAT:扩展FAT,支持更大文件和分区,常用于闪存设备。

在实验中,选择合适的版本取决于模拟的磁盘大小。例如,对于一个1.44MB的软盘模拟,使用FAT12;对于U盘模拟,使用FAT32。

理解这些原理后,我们可以进入实现部分,通过代码展示如何在OS实验中构建FAT文件系统。

OS实验中FAT文件系统的实现过程

在操作系统实验中,实现FAT文件系统通常涉及模拟磁盘I/O、管理FAT表和目录结构。以下是一个简化的实现示例,使用C语言编写,适用于Linux环境。我们假设有一个模拟的磁盘文件(disk.img),并通过文件操作来模拟扇区读写。

步骤1:模拟磁盘和引导扇区

首先,我们需要创建一个模拟磁盘,并写入引导扇区。引导扇区包含BPB(BIOS Parameter Block),这是FAT文件系统的核心元数据。

以下代码创建一个1.44MB的FAT12磁盘镜像,并初始化引导扇区:

#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>

#define DISK_SIZE (1474560) // 1.44MB = 1440 * 1024 bytes
#define SECTOR_SIZE 512
#define FAT12_CLUSTER_SIZE 512 // 每簇512字节,对于软盘

// BPB结构体(简化版)
typedef struct {
    uint8_t jmpBoot[3];     // 跳转指令
    uint8_t OEMName[8];     // OEM名称
    uint16_t bytesPerSector; // 每扇区字节数
    uint8_t sectorsPerCluster; // 每簇扇区数
    uint16_t reservedSectors; // 保留扇区数
    uint8_t numFATs;        // FAT表数量
    uint16_t rootEntries;   // 根目录条目数(FAT12/16)
    uint16_t totalSectors16; // 总扇区数(16位)
    uint8_t media;          // 媒体描述符
    uint16_t sectorsPerFAT; // 每FAT扇区数
    uint16_t sectorsPerTrack; // 每磁道扇区数
    uint16_t numHeads;      // 磁头数
    uint32_t hiddenSectors; // 隐藏扇区数
    uint32_t totalSectors32; // 总扇区数(32位)
    // FAT12/16的BPB到此结束,FAT32有扩展
} __attribute__((packed)) BPB;

void create_disk_image(const char* filename) {
    int fd = open(filename, O_CREAT | O_WRONLY, 0644);
    if (fd < 0) {
        perror("Failed to create disk image");
        return;
    }

    // 初始化全零磁盘
    uint8_t zero[SECTOR_SIZE] = {0};
    for (int i = 0; i < DISK_SIZE / SECTOR_SIZE; i++) {
        write(fd, zero, SECTOR_SIZE);
    }
    lseek(fd, 0, SEEK_SET);

    // 填充BPB
    BPB bpb = {0};
    bpb.jmpBoot[0] = 0xEB; bpb.jmpBoot[1] = 0x3C; bpb.jmpBoot[2] = 0x90; // 简单跳转
    memcpy(bpb.OEMName, "MYFAT12 ", 8);
    bpb.bytesPerSector = SECTOR_SIZE;
    bpb.sectorsPerCluster = 1; // 1扇区/簇
    bpb.reservedSectors = 1;   // 1个保留扇区(引导扇区)
    bpb.numFATs = 2;           // 两个FAT表
    bpb.rootEntries = 224;     // 根目录条目数(FAT12标准)
    bpb.totalSectors16 = 2880; // 总扇区数(1.44MB)
    bpb.media = 0xF0;          // 软盘媒体
    bpb.sectorsPerFAT = 9;     // 每FAT 9扇区
    bpb.sectorsPerTrack = 18;  // 每磁道18扇区
    bpb.numHeads = 2;          // 2磁头
    bpb.hiddenSectors = 0;
    bpb.totalSectors32 = 0;

    write(fd, &bpb, sizeof(BPB));
    close(fd);
    printf("Disk image '%s' created successfully.\n", filename);
}

int main() {
    create_disk_image("disk.img");
    return 0;
}

解释

  • 这个程序创建一个名为disk.img的文件,大小为1.44MB。
  • BPB结构体定义了FAT12的基本参数:每扇区512字节、每簇1扇区、两个FAT表、根目录224条目等。
  • __attribute__((packed))确保结构体无填充,直接对应磁盘布局。
  • 运行后,磁盘镜像就准备好了,但FAT表和数据区还是空的。

在实验中,你可以使用hexdump工具查看磁盘内容:hexdump -C disk.img | head -20,会看到BPB的二进制表示。

步骤2:初始化FAT表

FAT表需要在磁盘上占据连续空间。对于FAT12,每个簇用12位(1.5字节)表示,因此一个扇区(512字节)可以存储约341个簇条目。9个扇区的FAT可以支持约3072个簇(但实际用于2848个数据簇)。

以下代码初始化FAT12表,标记簇0和1为保留,簇2-2848为空闲:

#include <stdio.h>
#include <stdint.h>
#include <fcntl.h>
#include <unistd.h>

#define FAT12_SECTORS 9
#define FAT_ENTRIES_PER_SECTOR (SECTOR_SIZE * 2 / 3) // 512 * 2 / 3 ≈ 341

void init_fat(const char* disk_file) {
    int fd = open(disk_file, O_RDWR);
    if (fd < 0) {
        perror("Failed to open disk");
        return;
    }

    // FAT表从扇区1开始(引导扇区是扇区0)
    lseek(fd, SECTOR_SIZE, SEEK_SET);

    // FAT12条目:每个簇2个字节,但12位,所以需要处理半字节
    // 简化:我们用16位数组模拟,然后转换为12位
    uint16_t fat_entries[3072] = {0}; // 假设最多3072簇
    fat_entries[0] = 0xFF0; // 簇0:保留
    fat_entries[1] = 0xFF0; // 簇1:保留
    for (int i = 2; i < 2849; i++) {
        fat_entries[i] = 0x000; // 空闲
    }
    fat_entries[2848] = 0xFFF; // 最后一个簇标记结束(示例)

    // 写入FAT1
    uint8_t fat_sector[SECTOR_SIZE];
    int entry_index = 0;
    for (int sec = 0; sec < FAT12_SECTORS; sec++) {
        memset(fat_sector, 0, SECTOR_SIZE);
        for (int byte = 0; byte < SECTOR_SIZE; byte += 3) {
            if (entry_index >= 2849) break;
            // 将两个12位条目打包成3字节
            uint16_t e1 = fat_entries[entry_index++];
            uint16_t e2 = (entry_index < 2849) ? fat_entries[entry_index++] : 0;
            fat_sector[byte] = e1 & 0xFF;
            fat_sector[byte + 1] = ((e1 >> 8) & 0x0F) | ((e2 & 0x0F) << 4);
            fat_sector[byte + 2] = (e2 >> 4) & 0xFF;
        }
        write(fd, fat_sector, SECTOR_SIZE);
    }

    // 写入FAT2(副本)
    lseek(fd, SECTOR_SIZE * (1 + FAT12_SECTORS), SEEK_SET);
    for (int sec = 0; sec < FAT12_SECTORS; sec++) {
        lseek(fd, SECTOR_SIZE * (1 + FAT12_SECTORS + sec), SEEK_SET);
        write(fd, fat_sector, SECTOR_SIZE); // 简化,直接复制FAT1
    }

    close(fd);
    printf("FAT tables initialized.\n");
}

// 在main中调用:init_fat("disk.img");

解释

  • FAT表位于引导扇区后,FAT1从扇区1开始,FAT2从扇区1+9=10开始。
  • FAT12使用12位条目,因此需要将两个16位值打包成3字节:例如,簇2和簇3的条目(0x000和0x000)打包为字节[0x00, 0x00, 0x00]。
  • 代码中,我们用16位数组模拟,然后手动打包。实际实现中,可以使用位操作函数优化。
  • 这个初始化只标记了簇0-1为保留,其余为空闲。在文件创建时,会更新FAT表。

运行此代码后,FAT表被写入磁盘。你可以用od -t x1 disk.img | head -50查看扇区1的内容,验证FAT的二进制布局。

步骤3:初始化根目录区

根目录区紧随FAT2之后。对于FAT12,根目录从扇区19开始(1个引导 + 9个FAT1 + 9个FAT2 = 19),占用14个扇区(224条目 * 32字节 / 512 = 14扇区)。

以下代码初始化根目录,创建一个示例文件条目:

#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>

#define ROOT_DIR_SECTORS 14
#define ROOT_ENTRIES 224

typedef struct {
    uint8_t name[8];        // 文件名
    uint8_t ext[3];         // 扩展名
    uint8_t attr;           // 属性
    uint8_t reserved[10];   // 保留
    uint16_t cluster;       // 起始簇
    uint32_t size;          // 文件大小
} __attribute__((packed)) DirEntry;

void init_root_dir(const char* disk_file) {
    int fd = open(disk_file, O_RDWR);
    if (fd < 0) {
        perror("Failed to open disk");
        return;
    }

    // 根目录从扇区19开始
    lseek(fd, SECTOR_SIZE * 19, SEEK_SET);

    DirEntry entry = {0};
    // 创建一个示例文件 "TEST.TXT"
    memcpy(entry.name, "TEST    ", 8); // 8字节,不足补空格
    memcpy(entry.ext, "TXT", 3);
    entry.attr = 0x20; // 存档文件
    entry.cluster = 2;  // 起始簇2
    entry.size = 512;   // 文件大小512字节(1簇)

    // 写入第一个条目
    write(fd, &entry, sizeof(DirEntry));

    // 剩余条目清零
    DirEntry empty = {0};
    for (int i = 1; i < ROOT_ENTRIES; i++) {
        write(fd, &empty, sizeof(DirEntry));
    }

    close(fd);
    printf("Root directory initialized with example file.\n");
}

// 在main中调用:init_root_dir("disk.img");

解释

  • DirEntry结构体对应目录条目,注意文件名和扩展名用空格填充到固定长度。
  • 这个示例创建了一个名为”TEST.TXT”的文件,起始簇2,大小512字节。
  • 属性0x20表示这是一个普通文件(存档位)。
  • 根目录区初始化后,磁盘就具备了基本的FAT12结构。你可以用hexdump查看扇区19的内容,验证条目。

步骤4:文件读写操作

现在,我们实现文件的读取和写入。读取文件需要:1) 查找目录条目;2) 获取起始簇;3) 遍历FAT表读取簇链;4) 从数据区读取数据。

数据区从扇区33开始(19 + 14 = 33),每个簇对应1扇区。

以下代码实现读取”TEST.TXT”文件:

#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>

#define DATA_START_SECTOR 33

// 读取FAT条目(简化版,假设已加载FAT到内存)
uint16_t read_fat_entry(int fd, uint16_t cluster) {
    // FAT1从扇区1开始,每个簇12位
    uint32_t fat_offset = (cluster * 3) / 2;
    uint32_t fat_sector = 1 + (fat_offset / SECTOR_SIZE);
    uint32_t fat_entry_offset = fat_offset % SECTOR_SIZE;

    lseek(fd, SECTOR_SIZE * fat_sector + fat_entry_offset, SEEK_SET);
    uint8_t bytes[2];
    read(fd, bytes, 2);

    uint16_t entry;
    if (cluster & 1) { // 奇数簇
        entry = (bytes[1] << 4) | (bytes[0] >> 4);
    } else { // 偶数簇
        entry = (bytes[1] << 8) | bytes[0];
    }
    return entry;
}

void read_file(const char* disk_file, const char* filename) {
    int fd = open(disk_file, O_RDONLY);
    if (fd < 0) {
        perror("Failed to open disk");
        return;
    }

    // 步骤1: 查找根目录条目
    lseek(fd, SECTOR_SIZE * 19, SEEK_SET);
    DirEntry entry;
    uint16_t start_cluster = 0;
    uint32_t file_size = 0;

    for (int i = 0; i < ROOT_ENTRIES; i++) {
        read(fd, &entry, sizeof(DirEntry));
        if (entry.name[0] == 0) break; // 空条目结束
        if (entry.name[0] == 0xE5) continue; // 已删除

        char name[9], ext[4];
        memcpy(name, entry.name, 8); name[8] = '\0';
        memcpy(ext, entry.ext, 3); ext[3] = '\0';

        // 简单匹配(实际应去除空格)
        if (strncmp(name, "TEST    ", 8) == 0 && strncmp(ext, "TXT", 3) == 0) {
            start_cluster = entry.cluster;
            file_size = entry.size;
            break;
        }
    }

    if (start_cluster == 0) {
        printf("File '%s' not found.\n", filename);
        close(fd);
        return;
    }

    // 步骤2: 读取簇链
    printf("Reading file '%s' (size: %u bytes, start cluster: %u):\n", filename, file_size, start_cluster);
    uint16_t current_cluster = start_cluster;
    uint32_t bytes_read = 0;
    uint8_t buffer[SECTOR_SIZE];

    while (current_cluster < 0xFF0 && bytes_read < file_size) { // 0xFF0+ 表示结束或坏簇
        uint32_t sector = DATA_START_SECTOR + (current_cluster - 2) * 1; // 簇从2开始,数据区从33
        lseek(fd, SECTOR_SIZE * sector, SEEK_SET);
        read(fd, buffer, SECTOR_SIZE);

        // 打印缓冲区内容(假设是文本,实际可写入文件)
        uint32_t to_read = (file_size - bytes_read < SECTOR_SIZE) ? (file_size - bytes_read) : SECTOR_SIZE;
        printf("Cluster %u: ", current_cluster);
        for (int i = 0; i < to_read; i++) {
            if (buffer[i] >= 32 && buffer[i] <= 126) {
                printf("%c", buffer[i]);
            } else {
                printf(".");
            }
        }
        printf("\n");

        bytes_read += to_read;

        // 获取下一个簇
        current_cluster = read_fat_entry(fd, current_cluster);
    }

    printf("Total bytes read: %u\n", bytes_read);
    close(fd);
}

// 在main中调用:read_file("disk.img", "TEST.TXT");

解释

  • 查找条目:遍历根目录区,匹配文件名和扩展名。实际实验中,需要处理长文件名和大小写不敏感。
  • 读取FAT:FAT12的条目计算涉及位操作。奇数簇时,条目跨越字节边界。
  • 簇链遍历:从起始簇开始,读取数据区,直到遇到结束标记(0xFFF)或文件大小满足。
  • 这个示例假设文件内容已存在(在初始化时未写入数据,实际需先写入)。要写入文件,需要:分配空闲簇(扫描FAT),更新FAT表,更新目录条目,写入数据区。

对于写入操作,以下是简要代码框架(不完整,需结合FAT更新):

void write_file(const char* disk_file, const char* filename, const char* content, uint32_t size) {
    int fd = open(disk_file, O_RDWR);
    // 1. 分配簇:扫描FAT找到空闲簇
    // 2. 更新FAT:写入簇链
    // 3. 更新根目录:添加新条目
    // 4. 写入数据区:lseek到DATA_START_SECTOR + (簇-2)*1
    // 示例:简化,假设单簇文件
    uint16_t free_cluster = 2; // 假设簇2空闲
    // 更新FAT:簇2 -> 0xFFF
    // 更新目录:添加条目
    // 写入数据:lseek(fd, SECTOR_SIZE * (DATA_START_SECTOR + (free_cluster-2)), SEEK_SET); write(fd, content, size);
    close(fd);
}

在实验中,完整实现需要处理错误(如磁盘满、簇分配失败),并确保FAT和目录的一致性。建议使用调试工具如GDB逐步跟踪。

步骤5:子目录支持

FAT支持子目录,通过创建目录条目(属性0x10)实现。子目录的数据区存储其条目,类似于根目录。实现时,需要递归解析路径(如”/subdir/file.txt”)。

例如,创建子目录:

  • 在根目录添加条目,属性0x10,起始簇=3。
  • 在簇3的数据区初始化空目录条目(包括”.“和”..“)。

这扩展了基本实现,适用于更复杂的实验。

通过以上步骤,你可以在OS实验中构建一个基本的FAT文件系统。实际项目中,可以参考开源实现如DOSFS库,或使用FUSE(Filesystem in Userspace)挂载模拟磁盘进行测试。

常见问题排查指南

在OS实验中,实现FAT文件系统时常见问题包括磁盘无法识别、文件读取错误、FAT表不一致等。以下指南提供排查步骤和解决方案,按问题类型分类。

问题1:磁盘无法挂载或引导失败

症状:使用mount命令或虚拟机加载磁盘时,提示”Invalid filesystem”或引导错误。

排查步骤

  1. 检查引导扇区:使用hexdump -C disk.img | head -10查看扇区0。确认BPB字段正确,如bytesPerSector=512sectorsPerCluster=1。常见错误:OEM名称未填充空格,或跳转指令无效。
  2. 验证FAT表位置:FAT1应从扇区1开始。使用dd if=disk.img of=fat1.bin bs=512 skip=1 count=9提取FAT1,检查内容是否全零(初始化后)。
  3. 工具辅助:使用fsck.fat -v disk.img(Linux)或Windows的chkdsk检查文件系统。输出会报告BPB错误或FAT损坏。
  4. 解决方案:如果BPB错误,重新创建磁盘镜像。确保在写入时使用lseek精确定位扇区。示例修复代码:
    
    // 重新写入BPB
    lseek(fd, 0, SEEK_SET);
    write(fd, &bpb, sizeof(BPB));
    

预防:在初始化代码中添加断言,如assert(bpb.bytesPerSector == 512);

问题2:文件读取失败或数据错误

症状:读取文件时,内容为空、乱码,或程序崩溃(如段错误)。

排查步骤

  1. 检查目录条目:打印根目录内容。使用od -t x1 disk.img -j 19*512 -N 14*512查看扇区19-32。确认条目中的簇号和大小匹配实际数据。
  2. 验证FAT链:手动计算FAT条目。例如,对于簇2,读取扇区1的偏移(2*32=3)字节,检查是否为0x000(空闲)或0xFFF(结束)。如果链断裂,文件将提前结束。
  3. 数据区对齐:确认数据区从扇区33开始。读取时,簇2对应扇区33(33 + (2-2)*1 = 33)。常见错误:簇计算偏移1(忘记簇从2开始)。
  4. 缓冲区溢出:确保读取时不超过文件大小。使用valgrind运行程序检查内存错误。
  5. 解决方案:添加日志输出。例如,在读取FAT时打印:
    
    printf("FAT entry for cluster %u: 0x%03X\n", cluster, entry);
    
    如果FAT损坏,使用FAT2副本恢复:复制FAT2到FAT1。

预防:实现FAT一致性检查函数,扫描所有簇,确保无循环或孤立链。

问题3:文件创建/删除后磁盘不一致

症状:创建文件后,FAT未更新,导致后续操作失败;或删除文件后,簇未释放。

排查步骤

  1. 检查FAT更新:创建文件后,提取FAT1,验证新簇是否标记为已分配(非零)。删除时,簇应清零。
  2. 目录条目同步:删除文件时,条目首字节应变为0xE5。创建时,检查是否覆盖空条目。
  3. 碎片化测试:创建多个文件,观察FAT链是否非连续。使用fsck检查是否有未释放簇。
  4. 解决方案:实现原子更新:先写FAT,再写目录,最后数据。使用事务日志(实验中可选)记录操作。
    
    // 删除文件示例
    void delete_file(int fd, DirEntry* entry) {
       // 1. 清空FAT链
       uint16_t cluster = entry->cluster;
       while (cluster < 0xFF0) {
           uint16_t next = read_fat_entry(fd, cluster);
           write_fat_entry(fd, cluster, 0x000); // 清零
           cluster = next;
       }
       // 2. 标记目录条目删除
       entry->name[0] = 0xE5;
       lseek(fd, -sizeof(DirEntry), SEEK_CUR);
       write(fd, entry, sizeof(DirEntry));
    }
    

预防:定期运行fsck模拟检查。实验中,保持FAT1和FAT2同步。

问题4:性能或容量问题

症状:大文件读写慢,或提示”磁盘满”但实际有空间。

排查步骤

  1. 簇大小不匹配:确认sectorsPerCluster与文件大小匹配。FAT12簇小,适合小文件;大文件用FAT32。
  2. FAT表大小:计算所需FAT扇区:sectorsPerFAT = (totalClusters * 1.5 + 511) / 512。如果太小,簇号溢出。
  3. 根目录限制:FAT12根目录固定224条目,满后无法创建新文件。
  4. 解决方案:对于大文件,切换到FAT32(修改BPB,根目录动态)。优化FAT扫描:使用位图加速空闲簇查找。

预防:实验前计算磁盘布局:总扇区 = 保留 + FAT*2 + 根目录 + 数据。使用脚本验证。

通用调试技巧

  • 日志记录:在关键函数中添加printf,输出扇区号、簇号、偏移。
  • 十六进制编辑器:使用xxd或HxD(Windows)手动查看/编辑磁盘镜像。
  • 模拟器测试:使用QEMU或Bochs运行引导测试,或FUSE挂载验证读写。
  • 参考实现:阅读Linux内核的FAT驱动(fs/fat/),或Windows的FAT源码。
  • 常见陷阱:字节序(小端)、对齐(使用packed)、文件名大小写(FAT不敏感,但需统一)。

通过这些步骤,大多数问题都能快速定位。实验中,保持磁盘备份(cp disk.img disk.bak)以防万一。

结论

FAT文件系统作为OS实验的经典主题,不仅揭示了文件存储的本质,还锻炼了系统编程技能。从原理到实现,再到问题排查,我们覆盖了全流程。通过本文的代码示例和指南,你应该能独立构建和调试FAT文件系统。建议从简单FAT12开始,逐步扩展到FAT32和子目录支持。如果遇到具体问题,欢迎参考相关书籍如《Operating System Concepts》或在线资源。继续探索,文件系统的奥秘无穷!