文件系统
💡 核心结论
inode存储文件元数据,数据块存储文件内容
目录是特殊的文件,存储文件名到inode的映射
硬链接共享inode,软链接是独立文件存储路径
日志文件系统通过预写日志保证一致性
缓存和预读是文件系统性能的关键
1. 文件系统结构
1.1 磁盘布局
典型文件系统布局(如ext2/3/4):
┌──────────────┬──────────┬──────────┬──────────┬──────────┐
│ Boot Block │ Super │ inode │ Data │ Data │
│ (1 block) │ Block │ Bitmap │ Bitmap │ Blocks │
└──────────────┴──────────┴──────────┴──────────┴──────────┘
Boot Block: 启动信息
Super Block: 文件系统元信息
inode Bitmap: inode使用情况
Data Bitmap: 数据块使用情况
Data Blocks: 实际数据
1.2 超级块(Super Block)
struct ext2_super_block {
__u32 s_inodes_count; // inode总数
__u32 s_blocks_count; // 块总数
__u32 s_free_blocks_count; // 空闲块数
__u32 s_free_inodes_count; // 空闲inode数
__u32 s_first_data_block; // 第一个数据块
__u32 s_log_block_size; // 块大小(1024 << s_log_block_size)
__u32 s_blocks_per_group; // 每组块数
__u32 s_inodes_per_group; // 每组inode数
__u32 s_mtime; // 挂载时间
__u32 s_wtime; // 写入时间
__u16 s_magic; // 魔数(0xEF53)
__u16 s_state; // 文件系统状态
// ... 更多字段
};
2. inode结构
2.1 inode定义
inode(索引节点):存储文件元数据
struct ext2_inode {
__u16 i_mode; // 文件类型和权限
__u16 i_uid; // 所有者用户ID
__u32 i_size; // 文件大小(字节)
__u32 i_atime; // 访问时间
__u32 i_ctime; // 创建时间
__u32 i_mtime; // 修改时间
__u32 i_dtime; // 删除时间
__u16 i_gid; // 组ID
__u16 i_links_count; // 硬链接计数
__u32 i_blocks; // 块数
__u32 i_flags; // 标志
// 数据块指针(关键!)
__u32 i_block[15]; // 块指针数组
// [0-11]: 直接指针
// [12]: 一级间接指针
// [13]: 二级间接指针
// [14]: 三级间接指针
};
2.2 数据块索引
i_block[15]数组:
[0-11]: 直接指针 → 数据块
12个块 × 4KB = 48KB
[12]: 一级间接指针 → 指针块 → 数据块
1024个指针 × 4KB = 4MB
[13]: 二级间接指针 → 指针块 → 指针块 → 数据块
1024 × 1024 × 4KB = 4GB
[14]: 三级间接指针
1024 × 1024 × 1024 × 4KB = 4TB
最大文件大小 ≈ 4TB
查找数据块示例:
// 读取文件偏移offset处的数据
int get_block_num(struct inode *inode, int offset) {
int block_size = 4096;
int block_index = offset / block_size;
int ptrs_per_block = block_size / 4; // 每块1024个指针
if (block_index < 12) {
// 直接块
return inode->i_block[block_index];
}
block_index -= 12;
if (block_index < ptrs_per_block) {
// 一级间接块
int *indirect = read_block(inode->i_block[12]);
return indirect[block_index];
}
block_index -= ptrs_per_block;
if (block_index < ptrs_per_block * ptrs_per_block) {
// 二级间接块
int level1_index = block_index / ptrs_per_block;
int level2_index = block_index % ptrs_per_block;
int *indirect1 = read_block(inode->i_block[13]);
int *indirect2 = read_block(indirect1[level1_index]);
return indirect2[level2_index];
}
// 三级间接块...
}
3. 目录结构
3.1 目录项
目录也是文件,内容是目录项列表
struct ext2_dir_entry {
__u32 inode; // inode号
__u16 rec_len; // 记录长度
__u8 name_len; // 文件名长度
__u8 file_type; // 文件类型
char name[255]; // 文件名
};
目录示例:
目录inode: 10
数据块内容:
[inode=12, name="file1.txt"]
[inode=13, name="file2.txt"]
[inode=14, name="subdir"]
3.2 路径解析
int path_lookup(const char *path) {
int current_inode = root_inode; // 从根目录开始
char *token = strtok(path, "/");
while (token != NULL) {
// 1. 读取当前目录的数据块
struct ext2_inode *dir = read_inode(current_inode);
char *dir_data = read_block(dir->i_block[0]);
// 2. 查找目录项
struct ext2_dir_entry *entry = find_entry(dir_data, token);
if (!entry) {
return -ENOENT; // 文件不存在
}
// 3. 进入下一级
current_inode = entry->inode;
token = strtok(NULL, "/");
}
return current_inode;
}
路径查找示例:
查找路径: /home/user/file.txt
1. 从根inode (2) 开始
2. 读取根目录,查找"home" → inode 100
3. 读取inode 100,查找"user" → inode 200
4. 读取inode 200,查找"file.txt" → inode 300
5. 返回inode 300
4. 硬链接 vs 软链接
4.1 硬链接
原理:多个目录项指向同一个inode
# 创建硬链接
$ ln file.txt hardlink.txt
# inode相同
$ ls -i
12345 file.txt
12345 hardlink.txt
int create_hard_link(const char *oldpath, const char *newpath) {
// 1. 获取原文件inode
int inode_num = path_lookup(oldpath);
// 2. 在新目录中创建目录项
int dir_inode = path_lookup(dirname(newpath));
add_dir_entry(dir_inode, basename(newpath), inode_num);
// 3. 增加硬链接计数
struct inode *inode = read_inode(inode_num);
inode->i_links_count++;
write_inode(inode);
return 0;
}
特点:
共享inode和数据
删除一个链接,文件仍存在
不能跨文件系统
不能链接目录
4.2 软链接(符号链接)
原理:独立的文件,内容是目标路径
# 创建软链接
$ ln -s file.txt symlink.txt
# inode不同
$ ls -i
12345 file.txt
67890 symlink.txt
int create_symlink(const char *target, const char *linkpath) {
// 1. 创建新inode
int inode_num = alloc_inode();
struct inode *inode = read_inode(inode_num);
// 2. 设置类型为符号链接
inode->i_mode = S_IFLNK | 0777;
// 3. 存储目标路径
strcpy(inode->i_block, target); // 路径存在i_block中
inode->i_size = strlen(target);
// 4. 在目录中添加目录项
int dir_inode = path_lookup(dirname(linkpath));
add_dir_entry(dir_inode, basename(linkpath), inode_num);
return 0;
}
特点:
独立文件
可以跨文件系统
可以链接目录
目标删除后变成悬空链接
4.3 对比
特性 |
硬链接 |
软链接 |
|---|---|---|
inode |
相同 |
不同 |
跨文件系统 |
否 |
是 |
链接目录 |
否 |
是 |
原文件删除 |
仍可访问 |
悬空 |
磁盘空间 |
共享 |
独立(路径) |
5. 文件操作
5.1 文件创建
int create_file(const char *path, mode_t mode) {
// 1. 分配inode
int inode_num = alloc_inode();
struct inode *inode = get_inode(inode_num);
// 2. 初始化inode
inode->i_mode = mode;
inode->i_uid = current_uid();
inode->i_gid = current_gid();
inode->i_size = 0;
inode->i_atime = inode->i_mtime = inode->i_ctime = time(NULL);
inode->i_links_count = 1;
inode->i_blocks = 0;
// 3. 在父目录添加目录项
int dir_inode = path_lookup(dirname(path));
add_dir_entry(dir_inode, basename(path), inode_num);
return inode_num;
}
5.2 文件读取
ssize_t file_read(int fd, void *buf, size_t count, off_t offset) {
struct file *file = get_file(fd);
struct inode *inode = file->f_inode;
// 1. 计算读取范围
if (offset >= inode->i_size) {
return 0; // EOF
}
size_t to_read = min(count, inode->i_size - offset);
size_t bytes_read = 0;
// 2. 逐块读取
while (bytes_read < to_read) {
int block_num = get_block_num(inode, offset + bytes_read);
char *block_data = read_block(block_num);
int block_offset = (offset + bytes_read) % BLOCK_SIZE;
int copy_len = min(BLOCK_SIZE - block_offset, to_read - bytes_read);
memcpy(buf + bytes_read, block_data + block_offset, copy_len);
bytes_read += copy_len;
}
// 3. 更新访问时间
inode->i_atime = time(NULL);
return bytes_read;
}
5.3 文件写入
ssize_t file_write(int fd, const void *buf, size_t count, off_t offset) {
struct file *file = get_file(fd);
struct inode *inode = file->f_inode;
size_t bytes_written = 0;
while (bytes_written < count) {
// 1. 获取或分配数据块
int block_num = get_block_num(inode, offset + bytes_written);
if (block_num == 0) {
block_num = alloc_block();
set_block_num(inode, offset + bytes_written, block_num);
}
// 2. 写入数据
char *block_data = read_block(block_num);
int block_offset = (offset + bytes_written) % BLOCK_SIZE;
int copy_len = min(BLOCK_SIZE - block_offset, count - bytes_written);
memcpy(block_data + block_offset, buf + bytes_written, copy_len);
write_block(block_num, block_data);
bytes_written += copy_len;
}
// 3. 更新inode
if (offset + bytes_written > inode->i_size) {
inode->i_size = offset + bytes_written;
}
inode->i_mtime = inode->i_ctime = time(NULL);
return bytes_written;
}
5.4 文件删除
int delete_file(const char *path) {
// 1. 获取inode
int inode_num = path_lookup(path);
struct inode *inode = read_inode(inode_num);
// 2. 从父目录删除目录项
int dir_inode = path_lookup(dirname(path));
remove_dir_entry(dir_inode, basename(path));
// 3. 减少硬链接计数
inode->i_links_count--;
// 4. 如果没有链接了,释放资源
if (inode->i_links_count == 0) {
// 释放数据块
for (int i = 0; i < inode->i_blocks / 8; i++) {
int block_num = get_block_num(inode, i * BLOCK_SIZE);
free_block(block_num);
}
// 释放inode
free_inode(inode_num);
}
return 0;
}
6. 缓存机制
6.1 页缓存(Page Cache)
原理:将磁盘数据缓存在内存中
struct page_cache {
struct rb_root pages; // 红黑树索引
spinlock_t lock;
};
struct cached_page {
unsigned long index; // 页索引
struct page *page; // 物理页
int dirty; // 是否修改
struct rb_node rb_node;
};
void *read_with_cache(struct inode *inode, off_t offset) {
unsigned long page_index = offset / PAGE_SIZE;
// 1. 查找缓存
struct cached_page *cp = find_cached_page(inode, page_index);
if (cp) {
// 缓存命中
return cp->page->data + (offset % PAGE_SIZE);
}
// 2. 缓存未命中,从磁盘读取
struct page *page = alloc_page();
read_from_disk(inode, page_index, page->data);
// 3. 加入缓存
add_to_cache(inode, page_index, page);
return page->data + (offset % PAGE_SIZE);
}
6.2 预读(Read-ahead)
void readahead(struct file *file, off_t offset, size_t count) {
// 预读后续N个页面
int ra_pages = 16; // 预读16个页面
for (int i = 1; i <= ra_pages; i++) {
off_t ra_offset = offset + i * PAGE_SIZE;
if (ra_offset >= file->f_inode->i_size) {
break;
}
// 异步读取到缓存
async_read_to_cache(file->f_inode, ra_offset);
}
}
6.3 脏页回写
void flush_dirty_pages() {
struct list_head *dirty_list = get_dirty_pages();
list_for_each_entry(page, dirty_list, list) {
// 写回磁盘
write_to_disk(page->inode, page->index, page->data);
// 标记为干净
page->dirty = 0;
}
}
// 定期刷新(每30秒)
void pdflush_thread() {
while (1) {
sleep(30);
flush_dirty_pages();
}
}
7. 日志文件系统
7.1 一致性问题
问题:系统崩溃可能导致文件系统不一致
场景:创建文件
1. 在目录中添加目录项
2. 分配inode
3. 写入数据
如果在步骤2后崩溃:
- 目录项存在,但inode未分配
- 文件系统不一致!
7.2 日志机制
原理:先写日志,再写数据
struct journal_entry {
int transaction_id;
int type; // BEGIN, COMMIT, ABORT
// 操作内容
};
void journaled_create(const char *path) {
int tid = begin_transaction();
// 1. 写日志
write_journal(tid, BEGIN);
write_journal(tid, "allocate inode");
write_journal(tid, "add dir entry");
write_journal(tid, "write data");
write_journal(tid, COMMIT);
// 2. 执行实际操作
int inode = alloc_inode();
add_dir_entry(path, inode);
write_data(inode, data);
// 3. 标记日志完成
write_journal(tid, DONE);
}
恢复过程:
void recover() {
for (transaction in journal) {
if (transaction.state == COMMIT &&
transaction.state != DONE) {
// 重做未完成的事务
redo_transaction(transaction);
} else if (transaction.state == BEGIN &&
transaction.state != COMMIT) {
// 回滚未提交的事务
undo_transaction(transaction);
}
}
}
8. 磁盘调度
8.1 FCFS(先来先服务)
void fcfs_schedule(request_queue *queue) {
while (!queue_empty(queue)) {
request *req = dequeue(queue);
seek_to(req->sector);
read_or_write(req);
}
}
8.2 SSTF(最短寻道时间优先)
void sstf_schedule(request_queue *queue, int current_pos) {
while (!queue_empty(queue)) {
// 选择距离当前位置最近的请求
request *closest = find_closest(queue, current_pos);
seek_to(closest->sector);
read_or_write(closest);
current_pos = closest->sector;
}
}
8.3 SCAN(电梯算法)
void scan_schedule(request_queue *queue) {
int direction = UP; // 向上扫描
int current = 0;
while (!queue_empty(queue)) {
if (direction == UP) {
request *req = get_next_higher(queue, current);
if (req) {
seek_to(req->sector);
current = req->sector;
} else {
direction = DOWN; // 到达顶端,反向
}
} else {
request *req = get_next_lower(queue, current);
if (req) {
seek_to(req->sector);
current = req->sector;
} else {
direction = UP; // 到达底端,反向
}
}
}
}
9. 常见问题
Q1: 为什么需要inode?
A:
分离文件名和元数据
支持硬链接
快速查找文件属性
灵活的权限管理
Q2: 目录可以有硬链接吗?
A:
不可以(会导致循环)
只有
.和..是特殊的硬链接可以创建目录的软链接
Q3: 如何提高文件系统性能?
A:
页缓存
预读
延迟写入
inode缓存
目录缓存
Q4: ext4比ext3好在哪里?
A:
更大文件和文件系统支持
Extent代替间接块(连续分配)
延迟分配
日志校验和
在线碎片整理
参考资源
《Operating Systems: Three Easy Pieces》- File Systems章节
Linux源码:
fs/ext4/,fs/inode.cext4文档:Documentation/filesystems/ext4.txt
《Unix文件系统剖析》