16、链接与符号链接:link()、unlink()、symlink()、readlink() 函数的使用

文件链接这个话题,说实话,很多初学者容易绕晕。硬链接、软链接、符号链接……名字就够乱的。但只要你搞懂了底层原理,其实就那么回事。

我个人习惯把链接理解为「文件的别名」。你想想看,一个文件在磁盘上存着,怎么让多个路径都能找到它?这就是链接要解决的问题。

硬链接:多个名字,同一个 inode

硬链接的本质,是给同一个 inode 多起一个名字。inode 是什么?它是文件系统里真正存储文件元数据的结构体。每个文件都有一个唯一的 inode 编号,而文件名只是 inode 的一个「入口」。

link() 函数就能创建一个硬链接:

#include <unistd.h>

int link(const char *oldpath, const char *newpath);

调用成功后,oldpath 和 newpath 指向同一个 inode。你修改其中一个文件的内容,另一个也会同步变化——因为它们本来就是同一个文件。

关键点:硬链接不能跨文件系统,也不能链接目录(POSIX 禁止,Linux 更严格)。原因很简单:inode 编号只在同一个文件系统内唯一。

unlink():删除的是「名字」,不是文件

很多人以为 unlink() 是删除文件,其实它只是删除一个「目录项」。真正的文件数据什么时候被回收?当 inode 的链接计数降到 0,并且没有进程打开这个文件时,内核才会释放磁盘空间。

#include <unistd.h>

int unlink(const char *pathname);

我在项目中遇到过一个问题:某个守护进程写日志文件,运维人员直接 rm 删除了日志文件,但磁盘空间没释放。为什么?因为进程还持有文件描述符,inode 的引用计数没归零。后来我改用 unlink() 配合 open() 的 O_CREAT 标志,才彻底解决日志轮转的问题。

避坑指南:我曾经在嵌入式设备上调试一个 bug,发现 unlink() 返回成功,但文件还在。查了半天,原来是另一个进程用 open() 打开了这个文件,没有关闭。记住:unlink() 只是删除目录项,文件数据要等所有引用都释放才真正消失。

符号链接:一个指向路径的「快捷方式」

符号链接(也叫软链接)和硬链接完全不同。它不共享 inode,而是存储了一个目标路径的字符串。你可以把它理解成 Windows 里的快捷方式。

#include <unistd.h>

int symlink(const char *target, const char *linkpath);

符号链接可以跨文件系统,也可以链接目录。但它有一个致命弱点:如果目标文件被删除了,符号链接就成了「悬空链接」——访问它会报 ENOENT 错误。

我的习惯:在嵌入式系统中,我经常用符号链接来管理配置文件版本。比如 /etc/config 指向 /etc/config_v2.1,升级时只需要修改符号链接,不用动其他程序。

readlink():读取符号链接的内容

既然符号链接存的是路径字符串,那怎么读取这个字符串?用 readlink()

#include <unistd.h>

ssize_t readlink(const char *pathname, char *buf, size_t bufsiz);

注意:readlink() 不会在 buf 末尾加 '\0' 终止符。你需要自己处理。我一般这样写:

char buf[256];
ssize_t len = readlink("/path/to/symlink", buf, sizeof(buf) - 1);
if (len != -1) {
    buf[len] = '\0';
    printf("指向: %s\n", buf);
}

为什么要留一个字节?因为 readlink() 返回的是实际写入的字节数,如果路径正好 255 字节,不加 '\0' 就溢出了。

硬链接 vs 符号链接:一张表说清楚

特性 硬链接 符号链接
底层原理 共享同一个 inode 存储目标路径字符串
跨文件系统 不支持 支持
链接目录 不支持 支持
目标被删除后 文件数据仍在(只要还有硬链接) 变成悬空链接,访问报错
创建函数 link() symlink()
读取内容 不需要(直接访问 inode) readlink()

核心逻辑:链接操作的知识体系

下面这张图帮你理清这几个函数之间的关系:

链接操作核心逻辑 文件系统(磁盘) inode 表 + 数据块 硬链接 符号链接 inode 共享 存储路径 link() / unlink() symlink() / readlink() 硬链接操作 inode,符号链接操作路径字符串 unlink() 减少 inode 引用计数,文件数据延迟释放 硬链接 符号链接 系统调用

实际项目中的选择

说实话,我很少在嵌入式项目里用硬链接。为什么?因为嵌入式文件系统通常是 FAT 或 YAFFS,它们要么不支持硬链接,要么实现有坑。符号链接反而更实用。

举个例子:我做过一个 OTA 升级模块,系统中有两个分区:/app_v1/app_v2。根目录下放一个符号链接 /app,指向当前运行的版本。升级时,先下载到备用分区,然后原子性地修改符号链接。这样即使升级过程中断电,也不会影响系统启动。

// 原子性切换符号链接
symlink("/app_v2", "/app_new");
rename("/app_new", "/app");  // rename 是原子的

嗯,这里要注意:rename() 会覆盖目标路径,如果 /app 已经存在,它会被替换掉。这个技巧在嵌入式系统里非常实用。

总结一下:

  • 硬链接:共享 inode,不能跨文件系统,不能链接目录
  • 符号链接:存路径字符串,可以跨文件系统,可以链接目录
  • unlink():删除目录项,不是删除文件数据
  • readlink():读取符号链接的目标路径,注意手动加 '\0'

最后说一句:搞懂链接操作,你才算真正理解了 Linux 文件系统的「名字」和「实体」之间的关系。很多高级操作——比如文件锁、临时文件创建、日志轮转——都离不开这些基础函数。

公众号:蓝海资料掘金营,微信 deep3321