posix_spawn/vfork/clone(CLONE_VFORK)
はメモリを共有するので速い. 速いが、親子でメモリを共有するので危険である.
それでも速いのでプロセス起動で気をつけて使っている言語があり、どういう実装をしているか調べた.
前提
- fork-exec間はasync-singal-safeな関数しか使ってはいけない http://mkosaki.blog46.fc2.com/blog-entry-886.html
- mallocを使っても問題ないかは実装依存 http://mimosa-pudica.net/linux-closefrom.html
jemallocはおそらくだめそんなことはなかった- いまどきどれもpthread_atforkでロックの開放処理をしているのかも
Rust
- どうも元ネタを追ってみるとFreeBSDでmalloc(jemalloc)が固まる問題らしい https://github.com/rayon-rs/rayon/issues/540
- これ自体は別問題で, 速度がほしいからという理由のようだ
posix_spawnを使う条件
- ENOENTを直接返すこと
- 単にposix_spawn側の実装バグの問題
- Linux/MacOS/FreeBSDであること
- ENOENTを直接返すことができるOSのため
- Linuxの場合, Glibcのバージョンが2.24以上であること
- ENOENTを返すposix_spawnはこのバージョンから
- getcwdでディレクトリを返すこと
- getgidが成功する
- getuidが成功する
- PATHが変更されていないこと
- race conditionがある
どう使っているか
- posix_spawnに持たせる属性を決めている
posix_spawnattr_setsigmask
シグナルをマスクしている- forkするスレッドはfork-exec間でシグナルを受け付けたくない
posix_spawnattr_setsigdefault
SIGPIPEをSIG_DFLにしている- シグナルハンドラでグローバル変数を参照するかもしれないので避けたい
- 別スレッドがシグナルをうけたときにメモリを書き換えるかも、という危険があるので
- (追記): そういえばRustはランタイム初期化時にSIGPIPEをSIG_IGNにセットしているので、その設定によって起動するプロセスが影響を受けることを避けたいためにSIG_DFLにしなおしているというほうがメインな気がする
- (追記): glibc2.29+だとchdirも指定できるようになっている
まとめ
Dellの中の人でFreeBSDのコアコミッタの人なので、けっこう信頼できるんじゃないだろうか. ただいくつか気になる点はあり、
すべてのシグナルに対してSIG_DFLを定義しなおさなくてもいいのか- してたわ
- uid/gidが特権ユーザでないことはチェックしなくてよいのか
- これはposix_spawn側で対策されていそうな雰囲気がある
- PATHの変更をみていること
- PATHは状態変わることで振るまいは変わるが具体的にどういった問題があるんだろう
- race conditionがある
(追記) あとでいろいろ調べ直したけどめちゃくちゃよく考えられてる実装だった。
Go
clone(CLONE_VM | CLONE_VFORK)を使う条件
どう使っているか
- clone(2)に
SIGCHLD|CLONE_VFORK|CLONE_VM
を指定して使っている - cloneでシステムコール呼び出しをしている場合, 親子はスタックを共有しないらしい
- 引数にmmap()で確保したメモリを渡すことで制御できるようだ
fork-exec間に子プロセスは何をしているか
- keep capabilitiesをprctlでセットしている(実行ユーザとかが変更されたかチェック)
- ほかにもいろいろやっているがforkと共通なので割愛
- 主にcapabilitiesとかなので権限まわりをセキュアにしたいのだろう
まとめ
自前でがんばっているためコードはかなり複雑になる. 現時点では制限がかなり厳しい(Linuxかつamd64のみ)ので、逆に意図しない問題は起きづらそう. 当然ゴルーチンが全部停止するためスループットが悪化するという懸念はされている.
Ruby
vforkを使う条件
- vforkを実装していること
- uid/gidが特権ユーザでないこと
- setuidとかされたくない
fork-exec間に子プロセスは何をしているか
- シグナルに対してSIG_DFLを再定義
- invalidなsignumは無視
SIG_IGN
が定義されてたらSIG_IGN
に- その場合でもSIGPIPEはSIG_DFLにする
- sigprocmaskでシグナルをmaskする
- pthread_sigmaskでないのはasync signal safeなものを使いたいため
pthread_setcancelstate
でPTHREAD_CANCEL_DISABLE
して cancel が無効であることを宣言するpthread_cleanup_push
で登録した cancellation cleanup handler も呼び出されるのは困る- シグナルハンドラと同様の理由
まとめ
だいぶ安全よりに倒しているのではないか.
Nim
posix_spawnを使う条件
cloneを使う条件
- Linuxだったら基本使う
- fork-exec間はasync-signal-safeな関数しか使わないようになっている
どう使っているか
- 基本Rustと一緒だけどシグナルまわりが怪しい
- 具体的にいうとSIGPIPEをSIG_DFLにセットしていないとかが違う
- 追記:これはRustがランタイム起動時にSIGPIPEをSIG_IGNにセットする特殊事情なので特におかしいことではない
- clone()を使っているときに親がchdirしてからclone()を呼び、また親のディレクトリを戻している?
- chdirしてからcloneが返ってくるまで親のカレントディレクトリが変わるというわりとめちゃくちゃな実装
まとめ
基本的に優良な実装にみえる.
ただしいろいろ不明すぎて安全に使えるのか不安.
シグナルまわりとか.
clone()を使っていてGoっぽいけどカレントディレクトリ指定とかやたら怪しい実装になっている