はじめに
昨年(2018年)末、Macのcpコマンドのオプションが私の職場でも話題となった。
最近のmacOSでは一瞬でファイルがコピーできるという話
それは、APFS (Apple File System)において、cpコマンドで-c
オプションを指定すると、
clonefileシステムコールを使ってくれるので、数GB級のファイルも一瞬でコピーを行うことができるというものだ。
そして、私はclonefileコールを使ってファイルコピーをするGolangのツールを書きたくなった。
特に経緯とか良い人はソースを直接どうぞ → https://github.com/n-someya/goclonefile
Golangにおけるシステムコール発行パッケージ
Golangにおける、システムコール発行のパッケージは歴史的経緯から2つ存在する。
syscallパッケージは、Golangの標準パッケージでシステムコールを発行するための機能を提供している。
しかし、OSごとに異なる固有仕様のサポートなど一連の問題1 により、現在はsyscallパッケージではなく、
golang.org/x/sysパッケージに、機能追加がされている。
clonefileシステムコールはmacOSの固有機能のため、golang.org/x/sys/unixパッケージをみた方が良い。(macOSはUnixベース)
Unixの場合、システムコールにはそれぞれ番号が割り当たっている。
どのシステムコールにどの番号が振られているかを定数定義しているファイルが、
zsysnum_darwin_amd64.go
である。
const (
//中略
SYS_CLONEFILEAT = 462
//中略
)
その中にclonefileのシステムコール番号も記載されている。
これは期待できそうだ。
上記の定数を利用して、実際にシステムコールを発行しているのが、
zsyscall_darwin_amd64.1_11.go
このファイルだ。
しかし、どこを見てもSYS_CLONEFILEAT
を使っている箇所がない。。。
実装されていないようだ。。。
Golangによるclonefileシステムコールの発行処理実装
しょうがないので、自分で実装するしかない。
Appleによるとclonefileシステムコール(正確にはclonefileatシステムコール)は、5つの引数を受け付ける。
引数 | パラメータ |
---|---|
1 | 第2引数が相対パスの場合のカレントディレクトリのディスクリプタ |
2 | コピー元ファイルのパス |
3 | 第4引数が相対パスの場合のカレントディレクトリのディスクリプタ |
4 | コピー先ファイルのパス |
5 | フラグ(現状は、NOFOLLOW: シンボリックリンクのリンク元までは追わないのみ?) |
一方、unixパッケージのシステムコール発行関数は、引数3つのSyscall()、引数6つのSyscall6()
、引数9つのSyscall9()の3つあるので、第6引数を0として、Syscall6を呼び出すのが良さそうだ。
最終的には以下のようなコードとなった。
var (
// Flags
CLONE_NOFOLLOW CLONEFILE_FLAG = 0x0001 /* Don't follow symbolic links */
CLONE_NOOWNERCOPY CLONEFILE_FLAG = 0x0002 /* Don't copy ownership information from source */
AT_FDCWD = -2
)
// Clonefile clonefile is fast copy method for darwin(MacOS)
func Clonefile(src, dst string) (err error) {
if runtime.GOOS != OS_DARWIN {
return fmt.Errorf("Clonefile is implemented for macOS")
}
var _p0, _p1 *byte
_p0, err = unix.BytePtrFromString(src)
if err != nil {
return
}
_p1, err = unix.BytePtrFromString(dst)
if err != nil {
return
}
_, _, e1 := unix.Syscall6(unix.SYS_CLONEFILEAT, uintptr(AT_FDCWD), uintptr(unsafe.Pointer(_p0)), uintptr(AT_FDCWD), uintptr(unsafe.Pointer(_p1)), uintptr(CLONE_NOFOLLOW), 0)
if e1 != 0 {
err = errnoErr(e1)
}
return
}
色々な補足
【補足1】
go1.12からはmacOSに関しては、システムコール発行の実装が変更となっているようだ。
システムコール関数は、syscall_syscall6などの小文字+アンダーバーの関数となっており、、システムコール番号もlibcから取得する方法となっている。
https://github.com/golang/sys/blob/9fbf701fc3/unix/zsyscall_darwin_arm64.go
(なんでだろう)
【補足2】
直接システムコールを呼ぶのではなく、
https://github.com/go-darwin/apfs/blob/master/clone.go
こちらの方のように、C言語の関数を直接呼ぶ方が作法としては良いのかもしれない。
-
Go Binary Hacks - syscallとgolang.org/x/sys/ #golang, やgoogleのドキュメントに詳しい話が掲載されている ↩