文件系统1(FAT32)

1 前言

最近偶然看到一个fat32的代码,也不多,就一个c文件,正好也想看看文件系统。就从这个代码开始学了。代码如下:

https://github.com/strawberryhacker/fat32

2 最简单的文件系统

用GPT做了个最简单的文件系统。 

// myfs.c:生成一个只读的简单文件系统镜像
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <stdint.h>

#define BLOCK_SIZE 512
#define IMAGE_SIZE (BLOCK_SIZE * 64)  // 64 blocks = 32 KB image

int main() {
    FILE *fp = fopen("myfs.img", "wb");
    if (!fp) {
        perror("fopen");
        return 1;
    }

    // 创建空镜像
    uint8_t zero[BLOCK_SIZE] = {0};
    for (int i = 0; i < IMAGE_SIZE / BLOCK_SIZE; i++) {
        fwrite(zero, 1, BLOCK_SIZE, fp);
    }

    // 写入一个文件:hello.txt
    const char *filename = "hello.txt";
    const char *filecontent = "Hello from my tiny FS!\n";

    fseek(fp, BLOCK_SIZE * 1, SEEK_SET);  // 块1存文件内容
    fwrite(filecontent, 1, strlen(filecontent), fp);

    fseek(fp, BLOCK_SIZE * 0, SEEK_SET);  // 块0做目录项
    fwrite(filename, 1, strlen(filename), fp);
    fputc(0x00, fp); // null terminator
    uint32_t file_block = 1;  // 文件内容块号
    uint32_t file_size = strlen(filecontent);
    fwrite(&file_block, sizeof(file_block), 1, fp);
    fwrite(&file_size, sizeof(file_size), 1, fp);

    fclose(fp);
    printf("Image myfs.img created.\n");
    return 0;
}

代码其实很简单,可以看出,就是分配了一个32KB的块,然后其中每512字节做为一个块。

在这些块中,第一个块就是保存索引,没有什么特别的数据结构,就是顺序保存。首先是文件名,然后是uint32的索引位置,最后是uint32的文件长度。如果再有新数据,就再增加这三个参数。

后面的块就是数据内容。

真的很简单,对吗?但是这样也有限制,如果文件名是8字节,再加上两个uint32,也就是说索引最多也就保存512/16=32个文件。

从上面的代码可以看出,文件系统其实就是在二进制的存储空间中,用人的思维结构化保存数据的方法。(我说的)

这里有一个小疑问,为什么文件做成顺序排布,非要分块,就像上面的512字节。就像下图,一个文件夹固定就是512字节(现代的ext4中这个块的大小是4KB)。如果直接顺序存储不是更省空间吗?如果一个系统中大量的小文件,甚至可以导致真实存储空间降低到10%。。。

 查了一下大概的原因是几个吧,第一个是存储的特性,读取是按照扇区来读取,也就是一次就读取4KB,在内存中操作后,然后再回写。也就是说就算是小文件10byte,也要读取4KB。在速度方面弄成更小并没有时间上的优势。第二个是除非整个文件系统是只读的,否则对文件的修改和删除,要修改整条文件链,这个就有点得不偿失了(加个DMA辅助兴许还行)。。最后就是现在也做过权衡,也许小于4KB的文件并不是很多,综合下来还是能接受,没有太大的浪费。

最后就是生成img在linux中是无法挂载的,会报错。

tom@raspberrypi:~/fat $ sudo mount -o loop,ro -t vfat myfs.img /mnt/myfs
mount: /mnt/myfs: wrong fs type, bad option, bad superblock on /dev/loop0, missing codepage or helper program, or other error.
       dmesg(1) may have more information after failed mount system call.
mount: (hint) your fstab has been modified, but systemd still uses
       the old version; use 'systemctl daemon-reload' to reload.

这是因为这个img并不是标准的fat格式,真要挂载就会报错了。。。

==========================20260323补==========================

其实不用写代码,直接用命令生成即可。参考https://blog.csdn.net/fanged/article/details/159195420

挂载的时候命令。这里一定要手动设置offset,否则要报错。

sudo mkdir -p /mnt/loop

sudo mount -o loop,ro,offset=$((2048*512)) test.dd /mnt/loop

ls /mnt/loop/

cat /mnt/loop/LICENCE_FILE.BROADCOM

sudo umount /mnt/loop

另外也可以参考git的那篇,构建是

dd if=/dev/zero of=disk.img bs=1M count=64
parted disk.img --script mklabel msdos
parted disk.img --script mkpart primary fat32 1MiB 100%

加载是:

# 1. 将镜像映射为回环设备,-P 会自动扫描分区表
sudo losetup -fP disk.img

# 2. 查看生成的设备名(通常是 /dev/loop0p1)
ls /dev/loop0*

# 3. 格式化该分区(强制 FAT32)
sudo mkfs.vfat -F 32 /dev/loop0p1

# 4. 现在可以挂载了
sudo mount /dev/loop0p1 /mnt/loop

# 5. 用完记得卸载并释放设备
sudo umount /mnt/loop
sudo losetup -d /dev/loop0

完了使用mdir查看是否构建成功。

hp@DESKTOP-430500P:~/test/fat$ mdir -i disk.img@@1M
 Volume in drive : has no label
 Volume Serial Number is 3387-8588
Directory for ::/

TEST     TXT         0 2026-03-23   8:01  test.txt
        1 file                    0 bytes
                         65 026 560 bytes free
 

3 mini fat32

现在回到之前的开源代码。其实可以看到,这并不算是完整的文件系统,是在已有的img上,可以进行读写操作。

先看看FAT32格式说明

## **1. 基本结构**
FAT32 的磁盘布局分为几个固定区域:
```
| 引导扇区 | FAT1 | FAT2(备份) | 根目录区 | 数据区(文件/目录) |
```
### **(1) 引导扇区(Boot Sector)**
- **位置**:第一个扇区(通常512字节)。
- **关键信息**:
  - 每扇区字节数(通常512)。
  - 每簇扇区数(簇大小,如4KB = 8扇区)。
  - FAT表数量(通常2份,FAT1和FAT2互为备份)。
  - 根目录起始簇号(FAT32的根目录可在数据区任意位置,不像FAT16固定)。

### **(2) FAT表(File Allocation Table)**
- **作用**:记录每个簇的分配状态(空闲/占用/坏簇)和文件占用的簇链。
- **条目大小**:32位(4字节),故称FAT32。
  - 例如:文件A占用簇2→3→5,则FAT表中:
    ```
    FAT[2] = 3, FAT[3] = 5, FAT[5] = 0xFFFFFFFF(文件结束标志)
    ```
- **备份**:FAT2是FAT1的完整拷贝,用于恢复。

### **(3) 根目录和数据区**
- **根目录**:普通子目录一样存储在数据区,无固定位置。
- **文件/目录条目**:每个条目32字节,包含:
  - 文件名(8.3格式或长文件名扩展)。
  - 文件属性(存档/只读/隐藏等)。
  - 起始簇号、文件大小、最后修改时间。

---

## **2. 关键特性**
### **(1) 簇(Cluster)大小**
- 簇是文件分配的最小单位,由多个扇区组成(如4KB=8扇区)。
- **簇大小影响**:
  - **大簇**:适合存大文件(减少FAT表开销),但浪费空间存小文件。
  - **小簇**:节省空间,但FAT表更大,大文件性能下降。
- **默认簇大小**根据分区大小决定:
  | 分区大小       | 默认簇大小 |
  |----------------|------------|
  | < 8GB          | 4KB        |
  | 8GB–16GB       | 8KB        |
  | 16GB–32GB      | 16KB       |
  | > 32GB(不推荐)| 32KB       |

### **(2) 文件名存储**
- **短文件名(8.3格式)**:如`README.TXT`(8字符名+3字符扩展名)。
- **长文件名(VFAT扩展)**:
  - 使用多个目录条目存储UTF-16编码的长名(如`中文文档.docx`)。
  - 每个长名条目占32字节,按倒序排列在短名前。

### **(3) 文件大小与分区限制**
- **最大单文件**:4GB−1字节(因32位文件长度字段)。
- **最大分区**:理论2TB,但Windows限制为32GB(因簇效率问题)。

---

从介绍可以看出,之前的512字节在这里就叫做扇区Sector。这里还有个关键概念就是簇Cluster,一个簇一般是4KB,也就是8个扇区。这个扇区数是可以调整的。

在一个img中,第一个sector就是引导扇区,记录了整个img的信息。后面比较重要的就是FAT,这个有两个,互为备份。记录了具体的文件存储信息。然后就是root文件夹,最后面就是文件的具体存储。

下面还是欣赏一下代码吧。

首先是封装了读写两个函数

static bool disk_read(uint8_t* buf, uint32_t sect)
{
  if (fseek(g_file, sect * 512, SEEK_SET))
    return false;

  return 1 == fread(buf, 512, 1, g_file);
}

static bool disk_write(const uint8_t* buf, uint32_t sect)
{
  if (fseek(g_file, sect * 512, SEEK_SET))
    return false;

  return 1 == fwrite(buf, 512, 1, g_file);
}

这里可以看到,此时就是按照之前所说,实现了块读写。一下512字节。直接封装在读写函数之中。

初始化

  if (!disk_init(argv[1]))
    return 0;

  // You can scan the drive for FAT32 partitions before mounting to avoid 
  // allocating excess fat structures.
  fat_probe(&g_ops, 0);

  // Mount the partition under /mnt
  fat_mount(&g_ops, 0, &g_fat, "mnt");

其中disk_init是打开image文件。

fat_probe是检查文件格式。传入的参数partition 0,应该是FAT32支持4个分区,此时还能传1到3。probe时验证的就是引导扇区。定义如下:

typedef struct __attribute__((packed))
{
  uint8_t jump[3];
  char name[8];
  uint16_t bytes_per_sect;
  uint8_t sect_per_clust;
  uint16_t res_sect_cnt;
  uint8_t fat_cnt;
  uint16_t root_ent_cnt;
  uint16_t sect_cnt_16;
  uint8_t media;
  uint16_t sect_per_fat_16;
  uint16_t sect_per_track;
  uint16_t head_cnt;
  uint32_t hidden_sect_cnt;
  uint32_t sect_cnt_32;
  uint32_t sect_per_fat_32;
  uint16_t ext_flags;
  uint8_t minor;
  uint8_t major;
  uint32_t root_cluster;
  uint16_t info_sect;
  uint16_t copy_bpb_sector;
  uint8_t reserved_0[12];
  uint8_t drive_num;
  uint8_t reserved_1;
  uint8_t boot_sig;
  uint32_t volume_id;
  char volume_label[11];
  char fs_type[8];
  uint8_t reserved_2[420];
  uint8_t sign[2];
} Bpb;

这里__attribute__((packed))是让编译器不进行内存排布优化,不填充字节。整个结果刚好是512字节,也就是一个块。

校验的细节如下,就不多讨论了。

static bool check_fat(uint8_t* buf)
{
  Bpb* bpb = (Bpb*)buf;
  
  if (bpb->jump[0] != 0xeb && bpb->jump[0] != 0xe9)
    return false;

  // Check if we need to be this strict.
  if (bpb->fat_cnt != 2)
    return false;
  
  if (bpb->root_ent_cnt || bpb->sect_cnt_16 || bpb->sect_per_fat_16)
    return false;
  
  if (bpb->info_sect != 1)
    return false;

  if (memcmp(bpb->fs_type, "FAT32   ", 8))
    return false;
    
  if (bpb->bytes_per_sect != 512)
    return false;
  
  // Only two FAT tables should exist
  if (!(bpb->ext_flags & EXT_FLAG_MIRROR) && (bpb->ext_flags & EXT_FLAG_ACT) > 1)
    return false;
  
  // FAT type is determined from the count of clusters
  uint32_t sect_cnt = bpb->sect_cnt_32 - (bpb->res_sect_cnt + bpb->fat_cnt * bpb->sect_per_fat_32);
  return sect_cnt/ bpb->sect_per_clust >= 65525;
}

mount的操作也是读取了引导扇区,将它挂载到分区表。这里的mount并不是mount到了linux系统,而是它自己的链表。

代码就不贴了。。。

最后看看几个关键的函数open read write close

open

int fat_file_open(File* file, const char* path, uint8_t flags)
{
  Dir dir;
  dir.fat = 0;

  int err = follow_path(&dir, &path, NULL);
  if (err && err != FAT_ERR_EOF)
    return err;
  
  if (err == FAT_ERR_EOF) // File does not exist
  {
    if (0 == (flags & FAT_CREATE))
      return FAT_ERR_DENIED;
    
    int len = last_subpath_len(path);
    if (len == 0)
      return FAT_ERR_PATH;

    // Create a new file
    uint32_t clust;
    err = create_chain(dir.fat, &clust);
    if (err)
      return err;
    
    err = dir_add(&dir, path, len, FAT_ATTR_ARCHIVE, clust);
    if (err)
      return err;
  }

  Sfn* sfn = dir_ptr(&dir);

  file->fat = dir.fat;
  file->dir_sect = dir.sect;
  file->dir_idx = dir.idx;
  file->sclust = sfn_cluster(sfn);
  file->clust = file->sclust;
  file->sect = 0xffffffff;
  file->offset = 0;
  file->attr = sfn->attr;
  file->size = sfn->size;
  file->flags = flags;

  if (file->size && flags & FAT_TRUNC)
  {
    file->size = 0;
    file->flags |= FAT_MODIFIED;
  }

  return fat_file_seek(file, 0, (flags & FAT_APPEND) ? FAT_SEEK_END : FAT_SEEK_START);
}

可以看出,open操作就是查找或者创建文件,这个查找信息就是在之前的FAT区域。查到了之后填充FILE结构体并返回。(实际的操作还是麻烦很多,这里就不多说了)

read

int fat_file_read(File* file, void* buf, int len, int* bytes)
{
  *bytes = 0;
  uint8_t* dst = buf;

  if (!file->fat)
    return FAT_ERR_PARAM;

  if (0 == (file->flags & FAT_READ))
    return FAT_ERR_DENIED;
  
  file->flags |= FAT_ACCESSED;

  while (len && file->offset < file->size)
  {
    int idx = file->offset % 512;
    int cnt = LIMIT(len, LIMIT(512 - idx, file->size - file->offset));
    memcpy(dst, file->buf + idx, cnt);

    *bytes += cnt;
    dst += cnt;
    len -= cnt;

    int err = fat_file_seek(file, cnt, FAT_SEEK_CURR);
    if (err)
      return err;
  }

  return FAT_ERR_NONE;
}

相关的位置,在open的时候就已经找到,这里还是按照扇区来读取。

write基本就是read的反操作。

close

//------------------------------------------------------------------------------
// Closes a file. Updates the directory entry if modified. Writes back the write 
// buffer if dirty.

int fat_file_close(File* file)
{
  if (!file->fat)
    return FAT_ERR_PARAM;
  return fat_file_sync(file);
}

close的主要操作就是把buf中的数据写回img。

好吧,这次主要还是简单看看,理解一些重要概念就行了。。。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值