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。
好吧,这次主要还是简单看看,理解一些重要概念就行了。。。

&spm=1001.2101.3001.5002&articleId=146541974&d=1&t=3&u=02b10804237043e582cdba83603c55df)
238

被折叠的 条评论
为什么被折叠?



