文章目录
背景
在执行go程序的时候,其中有一步是把/tmp目录下的一个文件移动到用户目录下,使用go的os.Rename函数来实现。经测试在mac上是可以正常跑的,但是在linux机器上却报错了。报错如下:
go run ./script/download-go go1.22.3
download from https://go.dev/dl/go1.22.3.linux-amd64.tar.gz
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 75 100 75 0 0 53 0 0:00:01 0:00:01 --:--:-- 53
100 65.7M 100 65.7M 0 0 160k 0 0:07:00 0:07:00 --:--:-- 419k
rename /tmp/go2503368948/go go-release/go1.22.3: invalid cross-device link
exit status 1
错误信息 invalid cross-device link 表明 /tmp 目录和 go-release 目录是在不同的文件系统中。也就是说不能把一个文件从一个文件系统移动(或重命名)到另一个文件系统。
os.Rename算是比较常用的文件操作函数,博主一直把os.Rename当作mv在使用,也一直没有遇到过这个问题,还是挺奇怪的,值得探索一下出错的原因。
运行环境
操作系统: linux_x86_64
CPU架构: amd_64
Golang: 1.22.2
文件系统对比
linux下的文件系统
df
Filesystem Size Used Avail Use% Mounted on
dev 7.6G 0 7.6G 0% /dev
run 7.7G 3.2M 7.7G 1% /run
/dev/nvme0n1p2 916G 299G 571G 35% /
tmpfs 7.7G 50M 7.6G 1% /dev/shm
tmpfs 7.7G 24K 7.7G 1% /tmp
mac下的文件系统
df
Filesystem 512-blocks Used Available Capacity iused ifree %iused Mounted on
/dev/disk3s3s1 965595304 28934208 217744920 12% 387452 1088724600 0% /
devfs 400 400 0 100% 692 0 100% /dev
/dev/disk3s6 965595304 40 217744920 1% 0 1088724600 0% /System/Volumes/VM
/dev/disk3s4 965595304 22879080 217744920 10% 1372 1088724600 0% /System/Volumes/Preboot
可以看到mac系统上,/根目录都对应同一个挂载点和同一个文件系统。而在我的linux开发机上/tmp是另一个挂载点和文件系统,根据报错信息来看,也是符合预期的。
linux下的mv指令
测试从/tmp目录移动文件到用户目录下,结果是可行的,并没有报错。
golang的os.Rename源码
直接去源码里面找代码看下,源码目录为GOROOT目录下面的src目录。因为没有配置vim环境,所以只能通过rg的方式来找了。
os.Rename
rg "Rename"
// file_unix.go
return syscall.Rename(oldname, newname)
syscall.Rename
// cd syscall
// rg "Raname"
// syscall_linux.go文件
unc Rename(oldpath string, newpath string) (err error) {
return Renameat(_AT_FDCWD, oldpath, _AT_FDCWD, newpath)
syscall.Renameat
// zsyscall_linux_amd64.go 文件
func Renameat(olddirfd int, oldpath string, newdirfd int, newpath string) (err error) {
var _p0 *byte
_p0, err = BytePtrFromString(oldpath)
if err != nil {
return
}
var _p1 *byte
_p1, err = BytePtrFromString(newpath)
if err != nil {
return
}
// 调用的是SYS_RENAMEAT这个系统调用
_, _, e1 := Syscall6(SYS_RENAMEAT, uintptr(olddirfd), uintptr(unsafe.Pointer(_p0)), uintptr(newdirfd), uintptr(unsafe.Pointer(_p1)), 0, 0)
if e1 != 0 {
err = errnoErr(e1)
}
return
}
// zsysnum_linux_amd64.go
// 对应的系统调用编号是264
SYS_RENAMEAT = 264
SYS_RENAMEAT是什么
对于linux操作系统,每个系统调用在系统调用表中都有一个唯一的编号。这个编号就是系统调用的标识,当用户的程序想要进行系统调用时,会使用这个编号对系统调用进行引用。用户的程序只能通过这个编号与系统调用交互,及进行相关的读、写、打开文件或者申请内存等操作。
而这里的SYS_RENAMEAT对应的就是系统调用的编号,可以搜索: **Linux System Call Table **来查看不同CPU架构对应的系统调用编号。
参考:https://www.chromium.org/chromium-os/developer-library/reference/linux-constants/syscalls/
x86_64
查看系统调用函数文档
什么是man page
简单来说就是linux系统的API文档介绍,主要介绍系统提供的命令含义及用法。但是系统文档相对来说还是比较长的,因此著名的开源项目TLDR(https://github.com/tldr-pages/tldr) 也是由此而来,旨在简化man page带来的长文本负担。
TL;DR 代表“太长;没有读”。它起源于互联网俚语,用于表示长文本(或其中的一部分)因太长而被跳过。
man page的用法
用法介绍网上一大堆,这里只聚焦我们的问题,怎么查看系统调用函数的介绍。首先man page对这些系统函数是做了分区的,如下:
user commands
就是常用的命令行函数都在这里,例如ls。左上角的LS(1)就代表在分区1
// man 1 ls
LS(1) User Commands LS(1)
NAME
ls - list directory contents
SYNOPSIS
ls [OPTION]... [FILE]...
DESCRIPTION
List information about the FILEs (the current directory by default). Sort entries alphabetically if none of -cftuvSUX nor --sort is specified.
Mandatory arguments to long options are mandatory for short options too.
-a, --all
do not ignore entries starting with .
-A, --almost-all
do not list implied . and ..
system calls
系统调用函数,这里拿renameat举例子,左上角的rename(2)代表的就是系统调用函数。
man 2 renamert
rename(2) System Calls Manual rename(2)
NAME
rename, renameat, renameat2 - change the name or location of a file
LIBRARY
Standard C library (libc, -lc)
SYNOPSIS
#include <stdio.h>
int rename(const char *oldpath, const char *newpath);
#include <fcntl.h> /* Definition of AT_* constants */
#include <stdio.h>
int renameat(int olddirfd, const char *oldpath,
int newdirfd, const char *newpath);
int renameat2(int olddirfd, const char *oldpath,
int newdirfd, const char *newpath, unsigned int flags);
Feature Test Macro Requirements for glibc (see feature_test_macros(7)):
renameat():
Since glibc 2.10:
_POSIX_C_SOURCE >= 200809L
Before glibc 2.10:
_ATFILE_SOURCE
renameat2():
_GNU_SOURCE
renameat不支持跨挂载点调用
EXDEV oldpath and newpath are not on the same mounted filesystem. (Linux permits a filesystem to be mounted at multiple points, but rename() does not
work across different mount points, even if the same filesystem is mounted on both.)
这里已经明确说了,不允许跨挂载点的调用,哪怕是同一个文件系统也不行。
strace确定程序调用了renameat
// 只显示 renameat 的调用情况
strace -f -e trace=renameat go run ./script/download-go go1.22.3
// 结果
[pid 92270] renameat(AT_FDCWD, "/tmp/go734096947/go", AT_FDCWD,
"go-release/go1.22.3") = -1 EXDEV (Invalid cross-device link)
结合上面的系统调用函数分析,已经明确了根因。
怎么避免错误
参考:
golang社区关于os.Rename的讨论
开源社区的方案
csdn上的避免方案
目前os包没有直接提供类似于mv的函数,常规解决方案就是先copy,再rename,这样就能避免跨挂载点工作导致的错误。
总结
本来只是个小问题,知道报错的含义之后,很容易就会想到避免错误的方案。但是寻根问底也是工程师的天性,知其然更要知其所以然。
哀吾生之须臾,羡知识之无穷。
end