写在前面
开发中经常会遇到这样的需求:同一份 SDK、离线包或本地文档,希望同时出现在多个项目目录里;切换 Git worktree 时,又不想复制一份几 GB 的资料。Windows 用户可能会想到快捷方式、目录联接(junction),Linux 用户则更熟悉硬链接、符号链接和 bind mount。
这些机制看起来都像“给同一份内容增加入口”,底层含义却完全不同。选错以后,轻则搜索工具看不到文件,重则清理一个工作区时穿过链接删掉真实数据。
本文从路径解析讲起,横向比较 Linux 与 Windows 的几类链接,最后给出 Git worktree 共享本地资料的安全方案。
一、先分清名字、对象和路径
假设程序打开下面的路径:
| |
操作系统不是把整个字符串当成一个不可分割的地址,而是从起点开始,逐级解析 srv、project、config 和 app.json。理解各种链接,关键是看其中某一级保存的究竟是什么:
| 机制 | 链接中保存或建立的关系 | 解析结果 |
|---|---|---|
| 硬链接 | 新名称直接引用同一个文件对象 | 两个名称地位基本平等 |
| 符号链接 | 保存另一个路径字符串 | 访问时继续解析目标路径 |
| Windows junction | 目录上的 NTFS 重解析点,保存目标目录信息 | I/O 管理器按重解析数据转向目标目录 |
| Linux bind mount | 把已有文件或目录子树再次挂到目录层次中的另一个位置 | 从另一个挂载点进入同一子树 |
| Windows 快捷方式 | .lnk 文件中保存供 Shell 使用的目标和启动信息 | 只有理解 Shell Link 的应用才会主动解析 |
1.1 硬链接指向文件对象
在 Linux 常见文件系统中,目录项把文件名映射到 inode;在 NTFS 中,可以近似理解为多个名称关联同一个文件记录。硬链接增加的是同一文件对象的名称,而不是创建一个“目标路径”。
因此,删除其中一个名称通常不会删除文件数据。只有链接计数归零,并且不再有进程打开该文件时,系统才有条件回收相应存储。硬链接不能跨文件系统或卷,因为另一套文件系统无法直接引用本文件系统内部的对象标识;普通接口也不允许给目录创建硬链接,以免目录树出现难以处理的环。
1.2 符号链接指向另一个名称
符号链接(symbolic link,symlink)自身是特殊文件,内容本质上是一个路径字符串。这个路径可以是绝对路径,也可以是相对路径;相对目标以符号链接所在目录为基准解析,而不是以调用者当前工作目录为基准。
目标可以暂时不存在,所以符号链接可能成为悬空链接(dangling symlink)。它还能跨文件系统并指向目录,这是它比硬链接灵活的原因,也是部署、打包和清理工具必须决定“是否跟随链接”的原因。
1.3 Junction 和 bind mount 都不是“目录硬链接”
Windows junction 与 Linux bind mount 都能让一个目录树从另一个位置出现,但它们不是同一种机制:
- junction 是 NTFS 目录上的重解析点,目标必须是同一台计算机上的本地目录,可以跨本地卷,但不能直接指向远程共享;
- bind mount 是 Linux VFS 的挂载操作,把已有文件或目录子树附加到当前挂载命名空间中的另一个位置,并不创建符号链接文件。
把二者类比有助于选型,但不能把命令、权限和生命周期直接互换。
二、Linux:硬链接、符号链接与 bind mount
下面的示例在一个临时实验目录中执行。先创建原始文件:
| |
2.1 硬链接:同一个文件的另一个名字
ln 默认创建硬链接:
| |
修改任意一个名称,另一个名称读取到的也是同一份内容:
| |
注意,这里的“不需要管理员权限”不等于不检查权限。创建硬链接仍要求调用者对新名称所在目录拥有相应权限,还会受到文件系统、挂载选项和 Linux 保护策略的约束。
适合硬链接的场景包括同一文件系统内给不可变文件增加名称,或者某些基于链接计数管理文件生命周期的工具。它不适合表示目录别名,也不能表达“始终跟随某个路径的最新替换文件”:如果程序用新文件替换目标路径,旧硬链接仍指向替换前的文件对象。
2.2 符号链接:把另一个路径写进文件
用 ln -s 创建符号链接:
| |
readlink shared 显示链接中原样保存的目标,readlink -f shared 则尝试规范化并解析为最终路径。由于这里保存的是相对目标 real,整个 link-lab 目录一起移动后,二者的相对关系仍可能保持有效。
删除符号链接自身应针对链接名称操作:
| |
此操作不会删除 real。但不能据此推断所有递归工具都安全:find、cp、tar、rsync、备份程序和语言运行库各有是否跟随符号链接的选项与默认值。以 GNU find 为例,-P、-H、-L 就代表不同的跟随策略。对陌生清理命令,应先查看文档并在测试目录验证。
2.3 bind mount:在另一个挂载点暴露目录树
如果希望目标在应用看来就是另一个挂载点,或者需要配合挂载命名空间、容器和只读挂载控制,可以使用 bind mount:
| |
bind mount 通常需要 CAP_SYS_ADMIN,普通桌面环境下往往表现为需要 sudo;容器用户命名空间等环境可能另有授权方式。它的生命周期属于挂载体系:临时执行的命令重启后不会自动恢复,需要通过 /etc/fstab、systemd mount unit 或容器运行参数进行持久化。
mount --bind 默认继承底层挂载的属性。若要提供只读入口,应使用系统支持的只读 bind mount 方式并验证实际挂载选项;经典的 bind 后 remount 是多步操作,并非原子的安全边界。对强隔离需求,不能只依赖一条临时挂载命令。
三、Windows:硬链接、符号链接、Junction 与快捷方式
Windows 的 NTFS 支持硬链接、符号链接和 junction。符号链接与 junction 都通过重解析点(reparse point)实现,但使用不同的重解析标签和规则。FAT32 等文件系统不具备完整的 NTFS 链接能力,因此创建前还要确认链接所在卷的文件系统。
3.1 Junction:本地目录的重解析入口
在命令提示符中,mklink /J 创建目录联接:
| |
PowerShell 可以完成同样的操作:
| |
Junction 只面向目录,通常不需要提升为管理员。它可以从 C: 指向本机 D: 上的目录,却不能直接指向 UNC 路径或映射的远程卷。mklink /J 也能记录一个尚不存在的目标路径,此时入口在目标出现前不可正常使用。相较于符号链接,它还缺少可随目录整体搬迁的相对目标语义;共享目录改名或迁移后,应重新创建 junction,而不是假定它会自动修复。
3.2 Windows 符号链接:更接近 Linux symlink
mklink 不加 /D 时创建文件符号链接,加 /D 时创建目录符号链接:
| |
Windows 符号链接支持文件和目录,也支持相对目标。传统环境中,创建符号链接通常要求提升权限;现代 Windows 开启开发者模式后,调用支持 SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE 的工具可以请求非提升创建。最终能否成功仍取决于 Windows 版本、开发者模式、调用工具和安全策略,脚本应检查退出码,而不要仅凭“这是 Windows 11”作判断。
PowerShell 示例:
| |
3.3 Windows 硬链接:同卷文件的另一个名称
Windows 的硬链接只能用于文件,且所有名称必须位于同一个卷:
| |
不要把硬链接当成“实时复制”。它没有两个独立副本,修改任何一个名称都会修改同一文件;删除一个名称也不会撤销经由其他名称进行的修改。
3.4 .lnk 快捷方式:Shell 数据,不是文件系统链接
Windows 快捷方式是扩展名为 .lnk 的二进制 Shell Link 文件,可以保存目标、工作目录、启动参数、图标和热键。资源管理器双击时会解析它,但普通的 open、cd 或路径拼接不会把 folder.lnk\data.txt 自动改写到目标目录。
PowerShell 可以通过 Shell COM 创建快捷方式:
| |
快捷方式适合面向人的启动入口,不适合给编译器、Git 或服务进程提供路径别名。
3.5 创建之后不要靠图标猜
资源管理器图标只能提供提示。应通过元数据确认链接类型和目标:
| |
也可以查询重解析点:
| |
删除目录 junction 或目录符号链接时,先确认路径本身确实是预期的重解析点,再删除链接入口:
| |
文件符号链接使用 del 删除。不要为了“保险”随手给未知路径加递归或强制删除参数;不同工具、运行库和版本对重解析点遍历的处理并不完全一致。删除前先检查 LinkType、目标路径和目标中的重要文件是否已有备份。
3.6 Git Bash 中的 ln -s 要单独验证
Git Bash 运行在 Git for Windows 携带的 MSYS2 兼容层上,不能把 Linux 中的 ln -s 行为原样套过来。在未启用 Windows 原生链接模式的环境中,ln -s 可能退化为复制目标;针对 Git for Windows 2.55.0.windows.3 的本地实验中,文件和目录都表现为普通副本,而不是重解析点。
需要请求 Windows 原生符号链接时,可以先设置 MSYS 运行时选项:
| |
nativestrict 的含义是原生符号链接创建失败时让命令失败,而不是静默换成另一种表示。它仍受开发者模式、进程权限和目标文件系统能力约束。创建后应回到 PowerShell 用 Get-Item 或在命令提示符中用 fsutil reparsepoint query 验证,不能只看 ln 是否返回成功。
Git 配置 core.symlinks 主要控制 Git 检出符号链接时的行为,不会自动把当前 Git Bash 会话中的 ln -s 变成 Windows 原生符号链接。这是两层不同的配置。
四、跨平台对照:它们究竟像不像
| 特性 | Linux 硬链接 | Windows 硬链接 | Linux symlink | Windows symlink | Windows junction | Linux bind mount | Windows .lnk |
|---|---|---|---|---|---|---|---|
| 对象类型 | 文件系统名称 | 文件系统名称 | 特殊文件 | NTFS 重解析点 | NTFS 目录重解析点 | 挂载关系 | 普通 Shell Link 文件 |
| 可指向文件 | 是 | 是 | 是 | 是 | 否 | 是 | 是 |
| 可指向目录 | 普通用户接口不允许 | 否 | 是 | 是 | 是 | 是 | 是 |
| 可跨文件系统或卷 | 否 | 否 | 是 | 是 | 可跨本地卷 | 是 | 是 |
| 可直接指向远程位置 | 不适用 | 否 | 取决于目标路径和挂载 | 可以表达路径,访问仍受系统策略约束 | 否 | 可绑定已经挂载的远程树 | 可以 |
| 目标可不存在 | 否 | 否 | 是 | 是 | 可以,但入口会悬空 | 否(源与挂载点都需存在,可先预建空挂载点) | 可以失效 |
| 相对目标 | 不适用 | 不适用 | 是 | 是 | 不作为可移植相对链接使用 | 不适用 | Shell 自行解析 |
| 常见特殊权限 | 目录写权限等 | 目录与文件权限 | 目录写权限等 | 提升权限或开发者模式等条件 | 通常无需提升 | 通常需要挂载能力 | 通常无需提升 |
| 是否适合程序路径 | 是 | 是 | 是,但工具可选择不跟随 | 是,但工具可识别重解析点 | 是,但工具可识别重解析点 | 是 | 通常不适合 |
这张表描述的是常见本地文件系统与标准工具行为。网络文件系统、容器、安全软件和企业策略都可能增加限制,自动化脚本仍应以实际命令结果为准。
五、如何选择
可以按下面的顺序判断:
- 只是给人提供启动入口:Windows 使用快捷方式;Linux 桌面环境通常使用
.desktop文件。它们不是程序路径重定向机制。 - 同一文件系统内让一个文件拥有多个平等名称:使用硬链接。先确认你真正需要共享同一个文件对象,而不是复制或版本管理。
- 需要跨文件系统、相对路径或文件与目录通用链接:使用符号链接。把工具的跟随策略纳入测试。
- Windows 上只需要给本地目录增加稳定入口,并希望免提升创建:junction 往往最省事;远程共享和相对可移植目标不适用。
- Linux 上需要挂载命名空间、容器视图或挂载属性控制:考虑 bind mount;它比 symlink 更接近系统部署机制,也需要更严格的权限和生命周期管理。
如果目的只是节省空间,还应先考虑内容寻址缓存、包管理器缓存、Git LFS、制品仓库或写时复制文件系统。链接解决的是命名和可见性,不自动解决版本、完整性、并发写入与供应链可信度。
六、实战:让多个 Git worktree 使用同一份本地资料
6.1 为什么新 worktree 看不到本地资料
git worktree 为同一仓库创建多个工作树。它们共享对象数据库和大部分引用,但每个工作树有独立的检出文件、HEAD 和索引。只有 Git 跟踪的文件会按目标提交检出;主工作树里未跟踪或被忽略的 SDK、文档和本地配置不会自动复制到新工作树。
如果这些资料体积较大且读多写少,可以在所有工作树之外建立一个中立共享区:
| |
6.2 Windows 使用 junction
分别在工作树中创建入口:
| |
把入口名称加入仓库的 .gitignore,或者只在当前克隆的 .git/info/exclude 中忽略:
| |
.git/info/exclude 不会提交到仓库,适合纯本机约定;.gitignore 适合团队都采用相同入口名称的情况。
6.3 Linux 使用 symlink 或 bind mount
普通开发资料通常先选择 symlink:
| |
示例是否能保持相对关系取决于你的实际目录层次,创建后必须验证:
| |
若应用拒绝符号链接,或者需要通过挂载命名空间控制视图,再考虑 bind mount:
| |
6.4 哪些内容可以共享
适合共享的通常是只读或低频修改内容:
- 本地设计资料和大体积参考手册;
- 经过校验、版本固定的厂商 SDK 安装包;
- 可丢弃、能够重新生成的下载缓存。
以下目录不应在并行 worktree 间共享:
bin/、obj/、构建输出和测试结果;- IDE 状态目录;
- 运行时数据库、日志和可能并发写入的工作目录;
- 不同分支可能要求不同版本的依赖展开目录。
链接不会提供并发控制。两个分支同时写入共享目标时,修改会立即互相影响,可能造成难以复现的构建结果。
6.5 不要假定所有工具都会扫描进去
能够执行 type local-docs\guide.txt 或 cat local-docs/guide.txt,只证明按这个明确路径访问可以到达目标,不证明所有工具都会递归遍历:
git grep面向 Git 已跟踪内容,不会因为目录入口存在就自动索引外部未跟踪资料;- IDE、全文搜索、备份和杀毒工具可能跳过链接、限制在工作区边界内,或要求单独开启跟随链接;
- AI 编程工具还可能受工作区沙箱和可写根目录限制,即使操作系统可以解析路径,也不代表工具被授权读取目标。
因此,应在仓库说明文件中记录真实共享目录、用途和只读约定,并让初始化脚本创建入口。验证时直接读取一个哨兵文件,再单独测试 IDE 搜索、构建和 AI 工具需要的能力。
七、真正危险的地方:遍历、删除与信任边界
7.1 工具决定是否跟随链接
操作系统提供路径解析能力,应用程序仍可以查看链接元数据并选择不跟随。文件树遍历工具往往区分下面几种策略:
- 永不跟随遇到的链接;
- 只跟随命令行直接指定的链接;
- 跟随遍历途中遇到的全部链接。
最后一种策略可能走出预期目录、重复扫描同一棵树,甚至遇到环。备份、归档、同步和删除命令都必须查明策略,不能从 cat link/file 成功推导其递归行为。
7.2 删除入口不等于递归清空入口
安全目标通常是“删除链接本身,不碰目标”。Linux symlink 使用 rm link-name;Windows 目录 junction 或目录 symlink 在确认类型后使用 rmdir link-name。不要把未知路径交给未经验证的递归删除工具,也不要在链接路径后随意添加可能改变解析行为的分隔符。
自动化脚本至少应执行三步:
- 读取并确认链接类型;
- 解析目标并确认它处于允许范围;
- 使用删除链接入口的 API,而不是先枚举子项再逐个删除。
重要数据仍然需要独立备份。链接不是备份,也不是回收站。
7.3 链接不能绕过权限
经过链接访问目标时,系统仍会检查路径和目标对象的权限。反过来,如果高权限程序信任低权限用户能够替换的链接,攻击者可能在“检查路径”和“真正打开文件”之间改变目标,形成链接攻击或 TOCTOU(time-of-check to time-of-use)问题。
高权限服务不应把“先取规范化路径,再按字符串判断前缀”当作完整防线。更稳妥的方式是限制可写目录、使用能够约束跟随行为的句柄式 API,并在同一权限边界内完成打开和验证。
7.4 可移植性需要显式设计
Git 可以在类 Unix 系统中记录符号链接,但检出行为会受到文件系统能力和 core.symlinks 等配置影响。Windows junction 本身不是一种可以按同样语义提交并在 Linux 自动还原的 Git 跨平台对象。
因此,本地开发入口更适合由初始化脚本按平台创建:
| |
脚本应幂等:入口正确时不重复创建,入口存在但目标错误时明确失败,不擅自覆盖真实目录。
八、建立一个最小验证闭环
无论选择哪种机制,完成后都验证四件事:
- 类型正确:它究竟是硬链接、symlink、junction、挂载点还是
.lnk; - 目标正确:相对路径的基准和最终解析位置符合预期;
- 工具可用:构建、搜索、备份和 AI 工具是否按需要遍历;
- 删除安全:在只有测试数据的目录里确认删除入口不会删除目标。
Windows 可以这样检查:
| |
Linux 可以这样检查:
| |
如果脚本依赖某个链接,验证失败时应立即退出并给出修复提示,而不是继续执行后续构建或清理步骤。
九、小结
各种“链接”解决的不是同一个问题:
- 硬链接让同一个文件对象拥有多个名称;
- 符号链接保存另一个路径,灵活但存在悬空、遍历策略和安全边界;
- Windows junction 是面向本地目录的 NTFS 重解析入口;
- Linux bind mount 是挂载体系的一部分,适合命名空间和部署视图;
- Windows
.lnk是 Shell 快捷方式,不是通用文件系统路径。
在 Git worktree 场景中,junction 和 symlink 很适合把只读本地资料挂入多个工作树,但共享可写输出、假定工具一定递归、或者用未经验证的递归命令清理链接,都可能把便利变成事故。先确定机制,再验证具体工具的行为,最后把创建、检查和删除过程写进脚本,才是可重复的工程方案。
参考资料
- Microsoft Learn:Hard Links and Junctions
- Microsoft Learn:Reparse Points
- Microsoft Learn:Reparse Points and File Operations
- Microsoft Learn:mklink
- Microsoft Learn:CreateSymbolicLink
- Microsoft Learn:Shell Links
- Git for Windows Release Notes
- Linux man-pages:symlink(7)
- Linux man-pages:link(2)
- Linux man-pages:mount(2)
- Linux man-pages:openat2(2)
- util-linux:mount(8)
- Git 官方文档:git-worktree