【Linux】目录、路径与软硬链接:Linux文件组织的奥秘

文章目录

    • 目录、路径与软硬链接:Linux文件组织的奥秘
    • 一、目录与文件名
      • [1.1 核心问题](#1.1 核心问题)
      • [1.2 目录也是文件](#1.2 目录也是文件)
      • [1.3 目录的数据块内容](#1.3 目录的数据块内容)
      • [1.4 通过代码验证](#1.4 通过代码验证)
      • [1.5 访问文件的真实过程](#1.5 访问文件的真实过程)
    • 二、路径解析
      • [2.1 问题的提出](#2.1 问题的提出)
      • [2.2 根目录的特殊性](#2.2 根目录的特殊性)
      • [2.3 路径解析过程](#2.3 路径解析过程)
        • [2.3.1 解析绝对路径](#2.3.1 解析绝对路径)
        • [2.3.2 解析相对路径](#2.3.2 解析相对路径)
      • [2.4 路径从哪里来](#2.4 路径从哪里来)
        • [2.4.1 进程的CWD](#2.4.1 进程的CWD)
        • [2.4.2 系统的缺省目录](#2.4.2 系统的缺省目录)
        • [2.4.3 用户的家目录](#2.4.3 用户的家目录)
    • 三、路径缓存
      • [3.1 性能问题](#3.1 性能问题)
      • [3.2 dentry结构体](#3.2 dentry结构体)
        • [3.2.1 dentry定义](#3.2.1 dentry定义)
        • [3.2.2 dentry构成树形结构](#3.2.2 dentry构成树形结构)
      • [3.3 路径缓存工作原理](#3.3 路径缓存工作原理)
        • [3.3.1 第一次访问](#3.3.1 第一次访问)
        • [3.3.2 第二次访问](#3.3.2 第二次访问)
        • [3.3.3 Hash加速](#3.3.3 Hash加速)
      • [3.4 LRU淘汰策略](#3.4 LRU淘汰策略)
    • 四、挂载分区
      • [4.1 问题的提出](#4.1 问题的提出)
      • [4.2 挂载实验](#4.2 挂载实验)
        • [4.2.1 创建磁盘镜像](#4.2.1 创建磁盘镜像)
        • [4.2.2 格式化为ext4文件系统](#4.2.2 格式化为ext4文件系统)
        • [4.2.3 查看当前挂载情况](#4.2.3 查看当前挂载情况)
        • [4.2.4 创建挂载点](#4.2.4 创建挂载点)
        • [4.2.5 挂载](#4.2.5 挂载)
        • [4.2.6 使用挂载的分区](#4.2.6 使用挂载的分区)
        • [4.2.7 卸载](#4.2.7 卸载)
      • [4.3 循环设备(loop device)](#4.3 循环设备(loop device))
      • [4.4 挂载的本质](#4.4 挂载的本质)
      • [4.5 Linux中的挂载表](#4.5 Linux中的挂载表)
    • 五、软硬链接
      • [5.1 硬链接](#5.1 硬链接)
        • [5.1.1 创建硬链接](#5.1.1 创建硬链接)
        • [5.1.2 硬链接的本质](#5.1.2 硬链接的本质)
        • [5.1.3 硬链接的操作](#5.1.3 硬链接的操作)
        • [5.1.4 硬链接的限制](#5.1.4 硬链接的限制)
      • [5.2 软链接(符号链接)](#5.2 软链接(符号链接))
        • [5.2.1 创建软链接](#5.2.1 创建软链接)
        • [5.2.2 软链接的本质](#5.2.2 软链接的本质)
        • [5.2.3 软链接的操作](#5.2.3 软链接的操作)
        • [5.2.4 软链接可以跨分区](#5.2.4 软链接可以跨分区)
        • [5.2.5 软链接可以链接目录](#5.2.5 软链接可以链接目录)
      • [5.3 软硬链接对比](#5.3 软硬链接对比)
      • [5.4 软硬链接的用途](#5.4 软硬链接的用途)
        • [5.4.1 硬链接的用途](#5.4.1 硬链接的用途)
        • [5.4.2 软链接的用途](#5.4.2 软链接的用途)
    • 六、文件的三个时间
      • [6.1 Access Time (atime)](#6.1 Access Time (atime))
      • [6.2 Modify Time (mtime)](#6.2 Modify Time (mtime))
      • [6.3 Change Time (ctime)](#6.3 Change Time (ctime))
    • 七、总结

目录、路径与软硬链接:Linux文件组织的奥秘

💬 欢迎讨论:这是Linux系统编程系列的第十篇文章。在上一篇中,我们深入理解了Ext2文件系统的内部结构,知道了文件的属性存储在inode中,内容存储在数据块中。但我们访问文件时使用的是文件名,而不是inode号。目录是什么?路径解析是如何工作的?软硬链接有什么区别?本篇将揭示Linux文件组织的奥秘。

👍 点赞、收藏与分享:这篇文章包含了目录原理、路径解析过程和软硬链接的完整实现,内容深入,如果对你有帮助,请点赞、收藏并分享!

🚀 循序渐进:建议先学习第九篇的Ext2文件系统知识,这样理解本篇会更轻松。


一、目录与文件名

1.1 核心问题

在上一篇文章中,我们知道:

  • 文件的属性存储在inode
  • 文件的内容存储在数据块
  • 每个文件有一个唯一的inode号

但问题来了:

bash 复制代码
ls -l test.c
-rw-r--r-- 1 root root 654 Sep 13 14:56 test.c

我们访问文件时使用的是文件名test.c,而不是inode号。

那么:

  1. 文件名存储在哪里?
  2. 文件名和inode号是如何关联的?
  3. 目录在文件系统中扮演什么角色?

1.2 目录也是文件

核心认知:在Linux中,目录也是文件!

bash 复制代码
普通文件:
┌──────────┐    ┌─────────────┐
│  inode   │───→│  数据块     │
│  (属性)  │    │  (文件内容) │
└──────────┘    └─────────────┘

目录文件:
┌──────────┐    ┌──────────────────┐
│  inode   │───→│  数据块          │
│  (属性)  │    │  (文件名-inode   │
│          │    │   映射表)        │
└──────────┘    └──────────────────┘

目录的特点:

  1. 目录也有inode
  2. 目录的数据块存储的是:文件名 ↔ inode号 的映射关系
  3. 目录的权限控制谁可以访问这个目录
  4. 目录不存储文件的实际内容

1.3 目录的数据块内容

让我们看看目录的数据块存储了什么:

c 复制代码
// 目录项结构体(简化)
struct ext2_dir_entry_2 {
    __le32  inode;          // inode号
    __le16  rec_len;        // 记录长度
    __u8    name_len;       // 文件名长度
    __u8    file_type;      // 文件类型
    char    name[255];      // 文件名(变长)
};

一个典型的目录数据块:

bash 复制代码
┌──────────┬──────────┬─────────────────┐
│  inode   │ rec_len  │  name           │
├──────────┼──────────┼─────────────────┤
│ 2        │ 12       │ .               │  ← 当前目录
│ 1048577  │ 12       │ ..              │  ← 父目录
│ 263464   │ 16       │ test.c          │
│ 263465   │ 16       │ main.c          │
│ 263466   │ 16       │ abc.txt         │
│ 263467   │ 20       │ subdir          │
└──────────┴──────────┴─────────────────┘

特殊目录项:

  • .(点):指向当前目录自己的inode
  • ..(点点):指向父目录的inode

1.4 通过代码验证

让我们编写程序来验证目录的本质:

c 复制代码
// readdir_test.c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <dirent.h>
#include <sys/types.h>
#include <unistd.h>

int main(int argc, char *argv[]) {
    if (argc != 2) {
        fprintf(stderr, "Usage: %s <directory>\n", argv[0]);
        exit(EXIT_FAILURE);
    }
    
    // 打开目录
    DIR *dir = opendir(argv[1]);
    if (!dir) {
        perror("opendir");
        exit(EXIT_FAILURE);
    }
    
    printf("%-30s %-10s\n", "Filename", "Inode");
    printf("─────────────────────────────────────────\n");
    
    struct dirent *entry;
    
    // 读取目录项
    while ((entry = readdir(dir)) != NULL) {
        printf("%-30s %-10lu\n", entry->d_name, (unsigned long)entry->d_ino);
    }
    
    closedir(dir);
    return 0;
}

编译运行:

bash 复制代码
gcc readdir_test.c -o readdir_test
./readdir_test /home/user/test
Filename                       Inode     
─────────────────────────────────────────
.                              1596260   
..                             1310721   
test.c                         1596261   
main.c                         1596262   
abc.txt                        1596263   
subdir                         1596264   

对比ls -i:

bash 复制代码
ls -i /home/user/test
1596260 .       1596261 test.c   1596263 abc.txt
1310721 ..      1596262 main.c   1596264 subdir

完全一致!验证成功!✓

1.5 访问文件的真实过程

现在我们可以理解访问文件的完整流程了:

bash 复制代码
用户执行:cat test.c

步骤1:获取当前工作目录
    进程的CWD:/home/user/test
    
步骤2:解析路径(假设是相对路径)
    完整路径:/home/user/test/test.c
    
步骤3:打开当前目录
    打开 /home/user/test 的inode(1596260)
    
步骤4:读取目录内容
    从目录的数据块中查找 "test.c"
    找到映射:test.c → inode 1596261
    
步骤5:访问目标文件
    打开inode 1596261
    从inode中获取数据块位置
    读取数据块
    显示内容

用伪代码表示:

c 复制代码
// 简化的文件访问过程
int access_file(const char *filename) {
    // 1. 获取当前目录的inode
    int cwd_inode = current->cwd_inode;
    
    // 2. 读取当前目录的数据块
    char *dir_data = read_inode_data(cwd_inode);
    
    // 3. 在目录中查找文件名
    struct dir_entry *entry = find_entry(dir_data, filename);
    if (!entry) return -ENOENT;  // 文件不存在
    
    // 4. 获取文件的inode号
    int file_inode = entry->inode;
    
    // 5. 读取文件的inode
    struct inode *inode = read_inode(file_inode);
    
    // 6. 根据inode中的i_block[]读取数据
    char *file_data = read_file_data(inode);
    
    return 0;
}

二、路径解析

2.1 问题的提出

我们说"访问文件需要先打开当前目录",但是:

当前目录不也是文件吗?

访问当前目录不也需要知道它的inode号吗?要知道它的inode号,不也得打开它的父目录吗?

这不就陷入无限递归了吗?

bash 复制代码
访问 test.c
    ↓
需要打开 /home/user/test 目录
    ↓
需要知道 test 目录的inode
    ↓
需要打开 /home/user 目录
    ↓
需要知道 user 目录的inode
    ↓
需要打开 /home 目录
    ↓
需要知道 home 目录的inode
    ↓
需要打开 / 目录
    ↓
需要知道 / 目录的inode
    ↓
???

2.2 根目录的特殊性

答案:根目录是出口!

根目录的特殊之处:

  1. inode号固定:通常是2(inode 1保留,inode 2是根目录)
  2. 系统启动时就知道:不需要查找
  3. 没有父目录/.. 指向自己
  4. 所有路径的起点
bash 复制代码
ls -id /
2 /

stat / | grep Inode
Device: 802h/2050d	Inode: 2  Links: 19

验证根目录的 . 和 ... 都指向自己:

bash 复制代码
ls -id / /. /..
2 /
2 /.
2 /..

2.3 路径解析过程

路径解析:从根目录开始,依次打开每个目录,直到找到目标文件

2.3.1 解析绝对路径

解析路径:/home/user/test/test.c

bash 复制代码
步骤1:从根目录开始
    已知:根目录inode = 2
    
步骤2:在根目录中查找 "home"
    打开inode 2的数据块
    查找目录项 "home"
    找到:home → inode 786433
    
步骤3:在home目录中查找 "user"
    打开inode 786433的数据块
    查找目录项 "user"
    找到:user → inode 1310721
    
步骤4:在user目录中查找 "test"
    打开inode 1310721的数据块
    查找目录项 "test"
    找到:test → inode 1596260
    
步骤5:在test目录中查找 "test.c"
    打开inode 1596260的数据块
    查找目录项 "test.c"
    找到:test.c → inode 1596261
    
步骤6:访问目标文件
    打开inode 1596261
    读取数据块
    成功!

图解路径解析:

bash 复制代码
       根目录(inode 2)
           ↓
    ┌──────┴──────┐
    │ 数据块       │
    │ home → 786433│
    │ etc  → 262145│
    │ usr  → ...   │
    └──────┬──────┘
           ↓ 786433
      home目录
           ↓
    ┌──────┴──────┐
    │ 数据块       │
    │ user →1310721│
    │ root → ...   │
    └──────┬──────┘
           ↓ 1310721
      user目录
           ↓
    ┌──────┴──────┐
    │ 数据块       │
    │ test →1596260│
    │ doc  → ...   │
    └──────┬──────┘
           ↓ 1596260
      test目录
           ↓
    ┌──────┴──────┐
    │ 数据块       │
    │test.c→1596261│
    │main.c→ ...   │
    └──────┬──────┘
           ↓ 1596261
      test.c文件
    ┌─────────────┐
    │  inode      │
    │  i_block[0] │
    │  i_block[1] │
    └──────┬──────┘
           ↓
    ┌─────────────┐
    │  数据块     │
    │  (文件内容) │
    └─────────────┘
2.3.2 解析相对路径

如果执行:

bash 复制代码
cd /home/user/test
cat test.c

相对路径的解析:

bash 复制代码
步骤1:获取当前工作目录
    进程的CWD inode = 1596260(/home/user/test)
    
步骤2:在当前目录中查找 "test.c"
    打开inode 1596260的数据块
    查找目录项 "test.c"
    找到:test.c → inode 1596261
    
步骤3:访问文件
    打开inode 1596261
    读取数据

优势:相对路径少了很多步骤!

2.4 路径从哪里来

问题:最开始的路径是从哪里来的?

2.4.1 进程的CWD

每个进程都有一个当前工作目录(Current Working Directory)

c 复制代码
// 进程描述符中的字段
struct task_struct {
    // ...
    struct fs_struct *fs;  // 文件系统信息
    // ...
};

struct fs_struct {
    int users;
    rwlock_t lock;
    int umask;
    struct dentry *root;    // 根目录
    struct dentry *pwd;     // 当前工作目录(CWD)
    // ...
};
bash 复制代码
# 查看进程的CWD
ls -l /proc/self/cwd
lrwxrwxrwx 1 user user 0 Oct 28 20:00 /proc/self/cwd -> /home/user/test

# 改变CWD
cd /tmp
ls -l /proc/self/cwd
lrwxrwxrwx 1 user user 0 Oct 28 20:00 /proc/self/cwd -> /tmp
2.4.2 系统的缺省目录

Linux为什么要有那么多缺省目录?

bash 复制代码
ls -l /
drwxr-xr-x   2 root root  4096 bin     # 可执行文件
drwxr-xr-x   3 root root  4096 boot    # 启动文件
drwxr-xr-x  17 root root  3880 dev     # 设备文件
drwxr-xr-x  98 root root  4096 etc     # 配置文件
drwxr-xr-x   6 root root  4096 home    # 用户家目录
drwxr-xr-x  14 root root  4096 lib     # 库文件
drwxr-xr-x   2 root root  4096 sbin    # 系统可执行文件
drwxr-xr-x  12 root root  4096 usr     # 用户程序
drwxr-xr-x  12 root root  4096 var     # 可变数据

原因:构建完整的路径体系!

  1. 系统启动时创建根目录 /
  2. 在根目录下创建标准子目录
  3. 在子目录下继续创建文件和目录
  4. 形成完整的树形结构

这样所有文件都自然有了路径!

2.4.3 用户的家目录
bash 复制代码
echo $HOME
/home/user

cd
pwd
/home/user

当你创建文件时:

bash 复制代码
touch myfile.txt

实际上是在当前目录创建,天然就有了路径:

bash 复制代码
/home/user/myfile.txt

结论:系统 + 用户共同构建Linux路径结构!


三、路径缓存

3.1 性能问题

思考:每次访问文件都要从根目录开始解析路径?

bash 复制代码
访问 /home/user/test/test.c
    → 打开 /
    → 打开 /home
    → 打开 /home/user
    → 打开 /home/user/test
    → 打开 /home/user/test/test.c
    
需要5次磁盘I/O!太慢了!

解决方案:路径缓存!

3.2 dentry结构体

Linux内核使用dentry(directory entry,目录项)结构体在内存中缓存路径。

3.2.1 dentry定义

字段随内核版本演进,这里只列出理解路径缓存所需的关键成员示例。

c 复制代码
struct dentry {
    atomic_t d_count;                  // 引用计数
    unsigned int d_flags;              // 标志
    spinlock_t d_lock;                 // 锁
    
    struct inode *d_inode;             // 指向inode(关键!)
    struct dentry *d_parent;           // 指向父目录(关键!)
    struct qstr d_name;                // 文件名(关键!)
    
    struct list_head d_lru;            // LRU链表
    struct list_head d_child;          // 子目录链表
    struct list_head d_subdirs;        // 子目录链表头
    struct list_head d_alias;          // inode别名链表
    
    struct hlist_node d_hash;          // hash链表
    
    struct dentry_operations *d_op;    // 操作方法
    struct super_block *d_sb;          // 超级块
    
    unsigned long d_time;              // 重新验证时间
    void *d_fsdata;                    // 文件系统私有数据
    
    unsigned char d_iname[DNAME_INLINE_LEN_MIN];  // 短文件名
};

关键字段:

  • d_inode:指向对应的inode
  • d_parent:指向父目录的dentry
  • d_name:文件名
  • d_child:兄弟节点(同一目录下的其他文件)
  • d_subdirs:子节点(当前目录下的文件)
3.2.2 dentry构成树形结构
bash 复制代码
            根目录dentry("/")
          d_inode = inode 2
                 ↓
        ┌────────┴────────┐
        │                 │
    home dentry       etc dentry
    d_parent = /      d_parent = /
    d_inode = 786433  d_inode = 262145
        ↓
    user dentry
    d_parent = /home
    d_inode = 1310721
        ↓
    test dentry
    d_parent = /home/user
    d_inode = 1596260
        ↓
    ┌────┴────┐
    │         │
test.c dentry  main.c dentry
d_parent=test  d_parent=test
d_inode=1596261 d_inode=1596262

每个dentry对应路径中的一个分量!

3.3 路径缓存工作原理

3.3.1 第一次访问
bash 复制代码
第一次访问 /home/user/test/test.c:

1. 在dentry cache中查找 → 未命中
2. 从磁盘解析路径:
   / → home → user → test → test.c
3. 为每个路径分量创建dentry
4. 将dentry添加到cache
5. 返回结果
3.3.2 第二次访问
bash 复制代码
第二次访问 /home/user/test/test.c:

1. 在dentry cache中查找 → 命中!
2. 直接从dentry中获取inode
3. 减少路径解析的 I/O / 减少读目录块的次数
4. 快速返回
3.3.3 Hash加速

dentry还通过Hash表加速查找:

bash 复制代码
Hash表:
┌────┬─────────────────┐
│ 0  │ → dentry_a      │
├────┼─────────────────┤
│ 1  │ → dentry_b → dentry_c │
├────┼─────────────────┤
│... │                 │
├────┼─────────────────┤
│1023│ → dentry_z      │
└────┴─────────────────┘

查找 "/home/user/test/test.c":
1. 计算路径的hash值
2. 在hash表中查找
3. O(1)时间复杂度!

3.4 LRU淘汰策略

内存有限,不可能缓存所有路径,因此需要淘汰策略:

bash 复制代码
LRU链表(Least Recently Used):
┌─────┐    ┌─────┐    ┌─────┐    ┌─────┐
│最近 │ ←→ │     │ ←→ │     │ ←→ │最久 │
│使用 │    │     │    │     │    │未用 │
└─────┘    └─────┘    └─────┘    └─────┘
  ↑                                  ↓
  新访问的dentry移到头部            从尾部淘汰

当内存不足时,从LRU链表尾部淘汰dentry。


四、挂载分区

4.1 问题的提出

我们知道:

  • inode号不能跨分区
  • 一个磁盘可以有多个分区
  • Linux如何访问多个分区?

4.2 挂载实验

让我们通过实验来理解挂载:

4.2.1 创建磁盘镜像
bash 复制代码
# 创建一个5MB的文件作为磁盘镜像
dd if=/dev/zero of=./disk.img bs=1M count=5
5+0 records in
5+0 records out
5242880 bytes (5.2 MB, 5.0 MiB) copied

# 查看文件
ls -lh disk.img
-rw-r--r-- 1 user user 5.0M Oct 28 20:00 disk.img
4.2.2 格式化为ext4文件系统
bash 复制代码
mkfs.ext4 disk.img
mke2fs 1.45.5 (07-Jan-2020)
Discarding device blocks: done
Creating filesystem with 5120 1k blocks and 1280 inodes
Filesystem UUID: 12345678-1234-1234-1234-123456789abc
Superblock backups stored on blocks: 
        3072

Allocating group tables: done
Writing inode tables: done
Creating journal (1024 blocks): done
Writing superblocks and filesystem accounting information: done
4.2.3 查看当前挂载情况
bash 复制代码
df -h
Filesystem      Size  Used Avail Use% Mounted on
/dev/sda1        50G   20G   28G  42% /
tmpfs           986M     0  986M   0% /dev/shm
/dev/sda2        30G   10G   18G  36% /home
4.2.4 创建挂载点
bash 复制代码
sudo mkdir /mnt/mydisk
4.2.5 挂载
bash 复制代码
sudo mount -t ext4 ./disk.img /mnt/mydisk

df -h
Filesystem      Size  Used Avail Use% Mounted on
/dev/sda1        50G   20G   28G  42% /
tmpfs           986M     0  986M   0% /dev/shm
/dev/sda2        30G   10G   18G  36% /home
/dev/loop0      4.9M   24K  4.5M   1% /mnt/mydisk  ← 新挂载!
4.2.6 使用挂载的分区
bash 复制代码
cd /mnt/mydisk
sudo touch testfile
ls -li
total 12
11 drwx------ 2 root root 12288 Oct 28 20:00 lost+found
12 -rw-r--r-- 1 root root     0 Oct 28 20:01 testfile

# 注意:inode从11开始!(不同分区的inode独立编号)
4.2.7 卸载
bash 复制代码
cd ~
sudo umount /mnt/mydisk

df -h
Filesystem      Size  Used Avail Use% Mounted on
/dev

继续!

bash 复制代码
/dev/sda1        50G   20G   28G  42% /
tmpfs           986M     0  986M   0% /dev/shm
/dev/sda2        30G   10G   18G  36% /home
# /dev/loop0 消失了!

4.3 循环设备(loop device)

什么是 /dev/loop0?

bash 复制代码
ls -l /dev/loop*
brw-rw---- 1 root disk 7, 0 Oct 28 20:00 /dev/loop0
brw-rw---- 1 root disk 7, 1 Oct 28 20:00 /dev/loop1
brw-rw---- 1 root disk 7, 2 Oct 28 20:00 /dev/loop2
brw-rw---- 1 root disk 7, 3 Oct 28 20:00 /dev/loop3
...

循环设备(loopback device):

  • 伪设备(pseudo-device)
  • 允许将普通文件 当作块设备使用
  • 常用于挂载ISO镜像、磁盘镜像等
bash 复制代码
普通文件 disk.img
    ↓
通过loop设备
    ↓
变成块设备 /dev/loop0
    ↓
可以挂载为文件系统

4.4 挂载的本质

挂载(mount):将分区与目录关联

bash 复制代码
挂载前:
/mnt/mydisk 是一个普通的空目录

挂载后:
/mnt/mydisk 成为访问 disk.img 分区的入口

┌─────────────────────────────────┐
│  根分区(/dev/sda1)              │
│  ┌───────────────────┐          │
│  │  /mnt/            │          │
│  │    mydisk/ ←─────┼──────┐   │
│  └───────────────────┘      │   │
└─────────────────────────────┼───┘
                              │
                    挂载点(mount point)
                              ↓
                ┌─────────────────────┐
                │ disk.img 分区        │
                │ (独立的文件系统)    │
                │ inode 11, 12, 13...  │
                └─────────────────────┘

关键:通过路径前缀判断访问哪个分区!

bash 复制代码
访问 /home/user/test.c
    → 路径前缀是 /
    → 访问根分区 /dev/sda1

访问 /mnt/mydisk/testfile
    → 路径前缀是 /mnt/mydisk
    → 访问挂载的分区 /dev/loop0

4.5 Linux中的挂载表

bash 复制代码
mount
/dev/sda1 on / type ext4 (rw,relatime)
/dev/sda2 on /home type ext4 (rw,relatime)
/dev/loop0 on /mnt/mydisk type ext4 (rw,relatime)

内核维护一个挂载表:

c 复制代码
struct mount {
    struct dentry *mnt_mountpoint;   // 挂载点目录
    struct dentry *mnt_root;         // 挂载文件系统的根目录
    struct super_block *mnt_sb;      // 超级块
    // ...
};

路径解析时会检查挂载点:

c 复制代码
struct dentry *path_walk(const char *path) {
    struct dentry *dentry = root_dentry;  // 从根开始
    
    for each component in path:
        // 检查当前dentry是否是挂载点
        if (is_mountpoint(dentry)):
            // 切换到挂载的文件系统
            dentry = get_mounted_root(dentry);
        
        // 在当前目录中查找下一级
        dentry = lookup_component(dentry, component);
    
    return dentry;
}

五、软硬链接

5.1 硬链接

硬链接:多个文件名指向同一个inode

5.1.1 创建硬链接
bash 复制代码
touch abc
ln abc def      # 创建硬链接

ls -li abc def
263466 -rw-r--r-- 2 root root 0 Oct 28 20:00 abc
263466 -rw-r--r-- 2 root root 0 Oct 28 20:00 def

观察:

  1. inode号相同:263466
  2. 硬链接数:2(第三列)
5.1.2 硬链接的本质
bash 复制代码
┌────────────────────────────────┐
│  目录的数据块                   │
│  ┌──────┬─────────┐            │
│  │inode │ 文件名  │            │
│  ├──────┼─────────┤            │
│  │263466│ abc     │──┐         │
│  │263466│ def     │──┼───┐     │
│  └──────┴─────────┘  │   │     │
└──────────────────────┼───┼─────┘
                       ↓   ↓
                  ┌─────────────┐
                  │ inode 263466 │
                  │ i_links = 2  │ ← 硬链接计数
                  │ i_block[0]   │
                  └──────┬───────┘
                         ↓
                  ┌─────────────┐
                  │  数据块     │
                  │  (文件内容) │
                  └─────────────┘

关键点:

  • abcdef 是两个不同的文件名
  • 但指向同一个inode
  • 共享相同的文件内容
  • inode中的 i_links_count 记录链接数
5.1.3 硬链接的操作

写入文件:

bash 复制代码
echo "Hello" > abc
cat def
Hello         # def 也能看到内容!

echo "World" > def
cat abc
World         # abc 也更新了!

删除文件:

bash 复制代码
rm abc
ls -li def
263466 -rw-r--r-- 1 root root 6 Oct 28 20:00 def
                 ↑
            硬链接数变为1

cat def
World         # 内容还在!

删除过程:

bash 复制代码
rm abc 做了什么?

1. 在目录中删除 (263466, "abc") 这个映射
2. 将 inode 263466 的硬链接计数减1
3. 如果计数 > 0,什么都不做
4. 如果计数 == 0,释放inode和数据块

再删除 def:

bash 复制代码
rm def
ls -li def
ls: cannot access 'def': No such file or directory

# 此时 inode 263466 的硬链接计数变为0
# inode和数据块被释放
5.1.4 硬链接的限制

1. 不能跨分区

bash 复制代码
ln /home/user/abc /mnt/mydisk/def
ln: failed to create hard link: Invalid cross-device link

原因:inode号在不同分区中独立编号

2. 不能链接目录

bash 复制代码
mkdir mydir
ln mydir mydir_link
ln: mydir: hard link not allowed for directory

原因:防止出现循环(... 是特殊的硬链接,是由系统设置的,用户都不能为目录设置硬链接)

5.2 软链接(符号链接)

软链接:通过名字引用另一个文件

5.2.1 创建软链接
bash 复制代码
touch abc
ln -s abc abc.link    # -s 表示软链接

ls -li
263466 -rw-r--r-- 1 root root  0 Oct 28 20:00 abc
263467 lrwxrwxrwx 1 root root  3 Oct 28 20:01 abc.link -> abc
       ↑                       ↑
    注意l(link)           箭头指向目标

观察:

  1. inode号不同:263466 vs 263467
  2. 软链接的权限是 lrwxrwxrwx(l表示链接)
  3. 大小是3("abc"的长度)
  4. 显示箭头 -> abc
5.2.2 软链接的本质
bash 复制代码
┌────────────────────────────────┐
│  目录的数据块                   │
│  ┌──────┬──────────┐           │
│  │inode │ 文件名   │           │
│  ├──────┼──────────┤           │
│  │263466│ abc      │───┐       │
│  │263467│ abc.link │─┐ │       │
│  └──────┴──────────┘ │ │       │
└──────────────────────┼─┼───────┘
                       │ │
        ┌──────────────┘ │
        ↓                ↓
┌─────────────┐   ┌─────────────┐
│inode 263467  │   │inode 263466 │
│i_mode=link   │   │i_mode=file  │
│i_block[0]    │   │i_block[0]   │
└──────┬───────┘   └──────┬───────┘
       ↓                  ↓
┌─────────────┐   ┌─────────────┐
│  数据块     │   │  数据块     │
│  "abc"      │   │  "Hello"    │
│  (目标路径) │   │  (文件内容) │
└─────────────┘   └─────────────┘

软链接是一个独立的文件!

  • 有自己的inode
  • 数据块存储的是目标文件的路径
5.2.3 软链接的操作

读取文件:

bash 复制代码
echo "Hello" > abc
cat abc.link
Hello

# 内核的处理:
# 1. 打开 abc.link
# 2. 发现是软链接
# 3. 读取链接内容:"abc"
# 4. 重新打开 "abc"
# 5. 读取 abc 的内容

删除目标文件:

bash 复制代码
rm abc
ls -l abc.link
lrwxrwxrwx 1 root root 3 Oct 28 20:01 abc.link -> abc
                                                    ↑
                                               目标不存在!

cat abc.link
cat: abc.link: No such file or directory //软链接本身还在,但解析时找不到它指向的目标,因此打开失败(ENOENT)

软链接变成了"悬空链接"(dangling link)!

5.2.4 软链接可以跨分区
bash 复制代码
ln -s /home/user/abc /mnt/mydisk/abc.link
ls -l /mnt/mydisk/abc.link
lrwxrwxrwx 1 root root 14 Oct 28 20:01 abc.link -> /home/user/abc

# 可以成功!因为软链接只存储路径字符串
5.2.5 软链接可以链接目录
bash 复制代码
mkdir mydir
ln -s mydir mydir.link
ls -ld mydir.link
lrwxrwxrwx 1 root root 5 Oct 28 20:01 mydir.link -> mydir

cd mydir.link
pwd
/home/user/mydir.link   # 仍然显示链接名
pwd -P                # -P 显示物理路径
/home/user/mydir

5.3 软硬链接对比

特性 硬链接 软链接
inode 共享同一个inode 独立的inode
文件类型 普通文件 链接文件(l)
存储内容 指向inode 存储目标路径字符串
跨分区 ❌ 不能 ✅ 可以
链接目录 ❌ 不能 ✅ 可以
目标删除 文件仍存在 变成悬空链接
硬链接计数 增加计数 不增加计数
性能 直接访问 多一次路径解析
磁盘占用 硬链接不会复制文件数据块;它只是在目录中新增一个目录项,因此会消耗少量目录空间 软链接有独立 inode;目标路径字符串通常存放在数据区,但当路径较短时可能被直接存入 inode(fast symlink),不额外分配数据块。

5.4 软硬链接的用途

5.4.1 硬链接的用途

1. ... 就是硬链接

bash 复制代码
ls -ldi . .. mydir
1596260 drwxr-xr-x 3 user user 4096 Oct 28 20:00 .
1310721 drwxr-xr-x 5 user user 4096 Oct 28 20:00 ..
1596261 drwxr-xr-x 2 user user 4096 Oct 28 20:00 mydir

ls -ldi mydir mydir/.
1596261 drwxr-xr-x 2 user user 4096 Oct 28 20:00 mydir
1596261 drwxr-xr-x 2 user user 4096 Oct 28 20:00 mydir/.
        ↑ inode号相同!

2. 文件备份

bash 复制代码
cp important.txt important.txt.bak  # 占用双倍空间
ln important.txt important.txt.bak  # 不占用额外空间
5.4.2 软链接的用途

1. 快捷方式

bash 复制代码
# 在桌面创建程序的快捷方式
ln -s /usr/bin/firefox ~/Desktop/firefox

2. 版本管理

bash 复制代码
ls -l /usr/bin/python*
lrwxrwxrwx python -> python3.8
-rwxr-xr-x python2.7
-rwxr-xr-x python3.8
-rwxr-xr-x python3.9

# 切换Python版本只需改变链接
sudo ln -sf /usr/bin/python3.9 /usr/bin/python

3. 目录整合

bash 复制代码
# 将多个磁盘的数据整合到一个目录下
ln -s /data/disk1/videos ~/Videos/disk1
ln -s /data/disk2/videos ~/Videos/disk2

4. 跨分区引用

bash 复制代码
ln -s /mnt/storage/large_file.dat ~/Documents/file

六、文件的三个时间

Linux为每个文件记录三个时间戳:

bash 复制代码
stat test.c
  File: test.c
  Access: 2024-10-28 10:00:00.123456789 +0800
  Modify: 2024-10-28 09:30:00.123456789 +0800
  Change: 2024-10-28 09:30:00.123456789 +0800

6.1 Access Time (atime)

最后访问时间

bash 复制代码
cat test.c      # 读取文件
stat test.c | grep Access
Access: 2024-10-28 10:05:00.123456789 +0800  # 更新了

用途:

  • 了解文件是否被使用
  • 清理长期未访问的文件

注意:

  • 频繁更新atime影响性能
  • 现代系统默认使用 relatime 选项(相对时间更新)

6.2 Modify Time (mtime)

内容最后修改时间

bash 复制代码
echo "new content" > test.c  # 修改文件内容
stat test.c | grep Modify
Modify: 2024-10-28 10:10:00.123456789 +0800  # 更新了

用途:

  • make 命令判断是否需要重新编译
  • 备份程序判断哪些文件需要备份

6.3 Change Time (ctime)

属性最后改变时间

bash 复制代码
chmod 755 test.c  # 修改权限
stat test.c | grep Change
Change: 2024-10-28 10:15:00.123456789 +0800  # 更新了

哪些操作会更新ctime:

  • 修改权限(chmod)
  • 修改所有者(chown)
  • 修改文件内容(也会更新mtime)
  • 创建硬链接

注意:ctime不能手动设置!


七、总结

通过本篇文章,我们完整理解了Linux文件组织的核心机制:

核心知识点:

  1. 目录的本质

    • 目录也是文件
    • 数据块存储文件名→inode映射
    • ... 是特殊的硬链接
  2. 路径解析

    • 从根目录开始递归解析
    • 根目录inode固定(通常是2)
    • 依次打开每个目录,查找下一级
  3. 路径缓存

    • dentry结构体
    • 树形结构 + Hash表 + LRU
    • 大幅提升性能
  4. 挂载分区

    • 将分区与目录关联
    • 通过路径前缀判断访问哪个分区
    • 循环设备允许挂载文件
  5. 硬链接

    • 多个文件名共享一个inode
    • 不能跨分区、不能链接目录
    • 用于备份、防止误删
  6. 软链接

    • 独立文件,存储目标路径
    • 可以跨分区、可以链接目录
    • 用于快捷方式、版本管理

完整的文件访问流程图:

bash 复制代码
用户:cat /home/user/test.c

1. 路径解析
   ├─ 从根目录(inode 2)开始
   ├─ 打开/,查找"home" → inode 786433
   ├─ 打开/home,查找"user" → inode 1310721
   ├─ 打开/home/user,查找"test.c" → inode 1596261
   └─ (优先查dentry缓存)

2. 打开文件
   ├─ 读取inode 1596261
   ├─ 检查权限
   ├─ 分配文件描述符
   └─ 返回fd=3

3. 读取数据
   ├─ 从inode的i_block[]获取块号
   ├─ 读取数据块
   ├─ 复制到用户空间
   └─ 更新atime

4. 显示内容
   └─ 输出到终端

💡 思考题

  1. 为什么删除文件时,硬链接数为0才真正删除?
  2. 如果创建了大量软链接指向同一个文件,性能会受影响吗?
  3. 为什么不能为目录创建硬链接?(循环问题)
  4. 挂载点目录下原有的文件去哪了?(被隐藏了,卸载后恢复)

至此,我们完整理解了从磁盘硬件到文件系统再到文件组织的全部核心原理!

相关推荐
wdfk_prog2 小时前
[Linux]学习笔记系列 -- [fs]file_table
linux·笔记·学习
张太行_2 小时前
Linux shell中设置串口参数
linux·运维·chrome
乾元2 小时前
LLM 自动生成安全基线与等保合规初稿——把“网络工程事实”转译为“可审计的制度语言”
运维·网络·人工智能·python·安全·架构
大连好光景2 小时前
WSL下创建的Ubuntu系统与Windows实现显卡直通
linux·运维·ubuntu
huangjiazhi_2 小时前
Ubuntu 添加服务自启动
linux·运维·ubuntu
wqfhenanxc2 小时前
vscode/cursor 远程Linux基础命令
linux·ide·vscode
吴爃2 小时前
N8N调用系统接口进行AI分析
运维·人工智能·ai
Zeku2 小时前
20251130 - 详细解析Framebuffer应用编程中涉及到的API函数
linux·驱动开发·嵌入式软件·linux应用开发
保卫大狮兄2 小时前
TPM 到底用在设备管理的哪个阶段?
大数据·运维