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() |
核心逻辑:链接操作的知识体系
下面这张图帮你理清这几个函数之间的关系:
实际项目中的选择
说实话,我很少在嵌入式项目里用硬链接。为什么?因为嵌入式文件系统通常是 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 文件系统的「名字」和「实体」之间的关系。很多高级操作——比如文件锁、临时文件创建、日志轮转——都离不开这些基础函数。