対応している機能
paludarium が目指すのは、実際のプログラムを本物の Linux と同じ結果で動かすことです。 ここに挙げたものは、差分テストで本物の x86-64 Linux と比べて確かめています。
命令
- 整数命令とフラグ:算術・論理・シフト・ビット操作、
lock付きの原子的な命令、REPの文字列命令(途中で止まったときの再開を含む)、CMPXCHG8B/CMPXCHG16B、 BMI 系(ADCX/ADOX、ANDN、BZHI、MULX、PEXT、RORX、SHLX/SHRX)、MOVBE、ENTER/LEAVEなど。 - ヒントの命令:
PAUSEと、プリフェッチのPREFETCHNTA/PREFETCHT0/T1/T2。 状態を何も変えず、フォールトも起こしません。 - SSE 系:実際のプログラム(probe、aube、pitchfork)が使う SSE 系の 145 の命令 (フェンスの
MFENCE/LFENCE/SFENCEを含む)と、AES・PCLMUL・SHA などの暗号の命令。浮動小数点はソフトウェアで計算します。 - 命令のデコードには iced-x86 を使っています。
CPUIDはホストの値を見せません。実装していない機能は広告しません。- AVX、AVX-512、x87、MMX は対象外です。
ゲストが実行できない命令に当たったときは、本物の Linux と同じようにゲストに SIGILL を送ります。ページフォールトは SIGSEGV、整数の 0 除算は SIGFPE になります。
システムコール
実装したシステムコールだけを、明示した表で扱います。それ以外はすべて ENOSYS を返し、 ホストに渡すことはありません。次の表には実装したシステムコールをすべて載せていて、 lint:all がカーネルと照らし合わせます。
| 分類 | システムコール |
|---|---|
| プロセスとスレッド | clone、clone3、fork、vfork、execve、wait4、waitid、pidfd_open、pidfd_send_signal、exit、exit_group、getpid、getppid、gettid、set_tid_address、futex、sched_getaffinity、sched_yield、setpgid/getpgid/getpgrp、setsid/getsid、getpriority/setpriority、prlimit64/getrlimit/setrlimit、prctl |
| メモリ | mmap(anonymous と、プライベートまたは共有のファイル)、munmap、mremap、mprotect、madvise、msync、brk、arch_prctl |
| システムの情報 | uname、sysinfo、getuid、geteuid、getgid、getegid、getgroups、getrusage、times |
| シグナル | rt_sigaction、rt_sigprocmask、rt_sigreturn、sigaltstack、kill、tkill、tgkill、setitimer、rt_sigtimedwait、rt_sigqueueinfo、rt_tgsigqueueinfo、rt_sigpending、rt_sigsuspend、pause、restart_syscall |
| 時刻 | clock_gettime、clock_getres、nanosleep、clock_nanosleep、timer_create/timer_settime/timer_gettime/timer_getoverrun/timer_delete |
| ファイル | open/openat/creat、read、write、readv/writev、pread64/pwrite64、preadv/pwritev、copy_file_range、sendfile、close、close_range、lseek、stat/fstat/lstat/newfstatat、statx、getdents64、mkdir/mkdirat、rmdir、unlink/unlinkat、rename/renameat、link/linkat、symlink/symlinkat、readlink/readlinkat、truncate/ftruncate、fallocate、memfd_create、flock、fcntl、dup/dup2/dup3、pipe/pipe2、chdir/fchdir、getcwd、umask、chmod/fchmod/fchmodat、chown/fchown/lchown/fchownat、utimensat、fsync/fdatasync、sync、fadvise64、access/faccessat/faccessat2、statfs/fstatfs |
| 入出力 | poll、ppoll、select、pselect6、ioctl(TCGETS、TIOCGWINSZ、FIOCLEX、FIONCLEX、FIONBIO、FIOASYNC、FIOQSIZE、FIGETBSZ、SIOCATMARK、FIONREAD/SIOCINQ、SIOCOUTQ) |
| イベント | eventfd2、timerfd_create/timerfd_settime/timerfd_gettime、epoll_create/epoll_create1、epoll_ctl、epoll_wait、epoll_pwait |
| ソケット | socket(ネットワークがあれば AF_INET と AF_INET6、なければ EAFNOSUPPORT)、connect、getsockname、getpeername、bind、listen、accept、accept4(ゲスト自身のループバックの上で。listen と accept は TCP)、socketpair(AF_UNIX、SOCK_STREAM、SOCK_SEQPACKET、SOCK_DGRAM)、sendto、recvfrom、sendmsg と recvmsg(SCM_RIGHTS でのディスクリプタ、SCM_CREDENTIALS での資格情報、SCM_PIDFD での送り手の pidfd の受け渡し)、setsockopt と getsockopt(SOL_SOCKET)、shutdown |
| 乱数 | getrandom |
pread64・pwrite64・preadv・pwritevはファイルの位置を動かさず、パイプとソケットではESPIPEを返します。O_APPENDのファイルでは、Linux と同じく位置を指定した書き込みも 末尾に足します。copy_file_rangeは、同じマウントにある 2 つの通常のファイルの間でコピーし(マウントを またぐとEXDEV)、入力の末尾で止まります。確かめる順番は Linux と同じです(EISDIR、EINVAL、EBADF、EOVERFLOW、同じファイルの中で範囲が重なるとEINVAL)。sendfileは、通常のファイルを書き込める任意のファイルに送り、パイプには 1 回でその容量 までを送ります。どちらも、渡されたオフセットかファイルの位置を進めます。sendfileは、 失敗したときもオフセットを書き戻します。statxは、statにある項目(STATX_BASIC_STATS)と、デバイス番号としてマウントのst_devを返します。flags と mask は Linux と同じように確かめます(EINVAL)。AT_EMPTY_PATHはディスクリプタを、AT_FDCWDなら作業ディレクトリを指し、Linux 6.11 からと同じく、それとともに渡した空ポインタのパスも同じ扱いです。fallocateは、Linux のvfs_fallocateと同じ順に引数を確かめます(EINVAL、EOPNOTSUPP、EBADF、通常のファイルでなければESPIPE・EISDIR・ENODEV、 そしてEFBIG)。そのあとは tmpfs と同じふるまいです。通常の確保は範囲の ページを確保し、FALLOC_FL_KEEP_SIZEがなければファイルを大きくします。FALLOC_FL_PUNCH_HOLEは範囲が覆うページを解放し、残りを 0 にします (O_APPENDのファイルでも)。ほかのモードはEOPNOTSUPPです。RLIMIT_FSIZEを超えて大きくするとSIGXFSZを起こしてEFBIGで失敗し、 メモリ上のファイルシステムの 4 GiB を超えるfallocateは、何も確保せずにENOSPCで失敗します(書き込みにはこの上限はありません。 制限 を見てください)。メモリ上のファイルシステムのファイルは、tmpfs と同じく中身を 4 KiB のページで 持ちます。書き込みは触れたページを確保し、それ以外は 0 として読める穴で、 メモリを使いません。そのため
ftruncateで大きくしたファイルは、書くまで何も 使いません。st_blocksは確保したページを数え、ファイルを小さくすると末尾より 後ろのページを解放します。SEEK_DATAとSEEK_HOLEのlseekは、書いたページと 穴を見つけます。fallocateで確保したページは書くまで穴で、ファイルの末尾も 穴です。末尾以降や負のオフセットはENXIOです。マウントしたホストのディレクトリのファイルでは、Linux と macOS のホストなら
SEEK_DATA、SEEK_HOLE、st_blocksをホストから得ます(st_blocksは Unix の ホストすべてで)。ホストのファイルシステムが穴の場所を答えられないときと、ほかの ホストでは、Linux のgeneric_file_llseekが穴のないファイルシステムで扱うのと 同じく、ファイル全体をデータとします。memfd_createは、リンクのないメモリ上のファイル(モード 0777、MFD_NOEXEC_SEALなら 0666)を作り、flags と名前を Linux と同じく確かめます (EINVAL、EFAULT)。MFD_HUGETLBは、hugetlbfs のないカーネルと同じくENOSYSです。fcntlのF_ADD_SEALS・F_GET_SEALSで扱うシールは、MFD_ALLOW_SEALINGがなければF_SEAL_SEALから始まり(MFD_NOEXEC_SEALならF_SEAL_EXEC)、tmpfs と同じくファイルを制限します。F_SEAL_WRITEとF_SEAL_FUTURE_WRITEは書き込み・穴あけ・書き込める共有マッピングを、F_SEAL_SHRINKとF_SEAL_GROWは大きさを(書き込みはファイルの中で終わる最後の ページで止まります)、F_SEAL_EXECは実行ビットを、どれもEPERMで拒みます。 memfd どうしは、Linux の内部の tmpfs マウントと同じく専用のファイルシステムを 共有するので、memfd とマウント上のファイルの間のcopy_file_rangeはEXDEVです。 ほかのファイルにシールはありません(EINVAL)。本物の tmpfs のファイルならF_SEAL_SEALを返すところですが、メモリ上の/tmpのファイルも同じです。 Linux と同じく、書き込める共有マッピングが memfd を見せている間のF_SEAL_WRITEはEBUSYになり、書き込みを封じた memfd の共有マッピングは書き込めるようにできません (EACCES)。実行できる memfd へのF_SEAL_EXECも書き込みを封じるので、そのときは Linux 7.1 以降と同じくEBUSYになります(それより前のカーネルはシールを加えます)。close_rangeはfirstからlastまでの開いているディスクリプタを閉じ、CLOSE_RANGE_CLOEXECなら閉じずに close-on-exec を付けます。CLOSE_RANGE_UNSHAREは先に、CLONE_FILESのスレッドと共有している表をそのスレッド専用の写しにします。 共有していたスレッドのディスクリプタはそのまま残ります。未知のフラグやlastより 大きいfirstはEINVALです。futexはWAIT・WAKE・WAIT_BITSET・WAKE_BITSETを扱います。futex・epoll・eventfdは paludarium の中で実装していて、ホストの同じ機能には 頼りません。そのため、macOS・Windows・WebAssembly でも同じように動きます。ネットワークがなければ、
socket()はAF_INETとAF_INET6にEAFNOSUPPORTを返します。pollはepoll_waitと同じように、どれかの記述子が準備できるか、ミリ秒の タイムアウトが過ぎるか(負の値なら過ぎません)、シグナルが届くまで(EINTR)待ちます。 ホストの標準入力は、Linux の端末やパイプと同じく、読むバイトがあるときか、入力が 終わったとき(WebAssembly ではGuest.stdinを閉じたとき、Unix のホストでは記述子が 終わったとき)だけ読めるとします。終わったときは切断(POLLHUP)も示し、readは 0 を返します。poll・ppoll・select・pselect6と、標準入力を監視できるepollは、どれもそう見ます。それを見分けられないホスト(Windows)では、いつも読めると します。出力はいつも書けます。パイプの読み出し側は、読まれていないデータがあるときだけ読めるとし、書き手が いなくなると切断だけを示します。Linux のpipe_pollと同じです。ppoll・select・pselect6も同じように待ちます。Linux と同じく、残りの時間を タイムアウトの引数に書き戻し(0 のタイムアウトには書き戻しません)、ppollとpselect6は一時的なシグナルのマスクを受け取り、ハンドラはそのマスクで動きます。selectは、集合の中の閉じた記述子にEBADFを返し、見る記述子は 1024 個 (FD_SETSIZE)までです。Linux はnfdsを記述子の表の大きさで切ります。パイプは Linux と同じく 64 KiB を持ちます。
fcntlのF_SETPIPE_SZとF_GETPIPE_SZは、pipe_set_sizeと同じように大きさを変えて返します。大きさは 1 ページ以上の 2 のべき乗で、読まれていないデータより小さくするとEBUSY、 2³¹ を超えるとEINVALです。SOCK_DGRAMの組は、SOCK_SEQPACKETと同じくメッセージの区切りを保ちますが、 ファイルの終わりがありません。相手が閉じると、読み込みは待ち続け、次の送信はECONNREFUSED、その後はENOTCONNになります。EPIPEでSIGPIPEを送るのは、 Linux と同じくSOCK_STREAMだけです。recvmsgとrecvfromは、Linux の AF_UNIX のソケットと同じくフラグを扱います。MSG_PEEKはデータを残し、受け渡すディスクリプタの写しを渡します。MSG_WAITALLは、 ストリームの終わりやディスクリプタが来るまで、またはデータを受け取ったあとにシグナルが 来るまで、ストリームのバッファを満たします。ただしMSG_PEEKと一緒なら、見つけた分を 返し、続きを待ちません。MSG_TRUNCはメッセージの全体の長さを 返します。sendmsgとsendtoはMSG_NOSIGNALとMSG_DONTWAITを扱います。 データグラムとパケットのソケットへのMSG_OOBはEOPNOTSUPPです。ほかのフラグは、 Linux と同じく何もしません。ストリームへの書き込みは、Linux の
unix_stream_sendmsgと同じく、いくつかの部分に 分けて送ります。1 つの部分は最大 36544 バイト(1 ページの先頭部分と 32 KiB のページ)、 または送信バッファの半分から 64 バイトを引いた大きさです。送信バッファはSO_SNDBUFが 2 倍にして設定し、読み出す値なので、SO_SNDBUFを 10000 にすると 1 つの部分は 9936 バイトです。 ディスクリプタは最初の部分と一緒に渡るので、それを受け取る読み込みはそこで止まります。 部分ごとにO_ASYNCの持ち主へシグナルを送ります。データグラムとパケットのメッセージは 分けません。ストリームは、Linux 7.0 の AF_UNIX と同じく帯域外のデータを持ちます。
MSG_OOBを 付けた送信では、ほかのデータのあとに、最後の 1 バイトが帯域外のバイトになります (空の送信はEOPNOTSUPP)。まだ読まれていない前のバイトは、ふつうのデータになります。 読み込みは印の手前で止まり、印はSIOCATMARKでわかります。poll、selectの例外の 集合、epollにはPOLLPRIが出ます。MSG_OOBを付けたrecvがそのバイトを取り (ないときはEINVAL)、MSG_PEEKなら残します。何も読まないうちに印に着いた読み込みは、 そのバイトを捨てて先を読みます。SO_OOBINLINEなら、そのバイトをふつうのデータとして 読みます(そのときMSG_OOBを付けたrecvはEINVAL)。1 バイトの帯域外の送信が 受け渡すディスクリプタは、渡されずに閉じられます。帯域外のバイトは、受け取る側の 持ち主にSIGURGを送ります(下のシグナル駆動の入出力を見てください)。Linux と同じくシグナル駆動の入出力を扱います。
fcntlのF_SETOWNで、どのファイルにも 持ち主を決められます。- プロセス、または負の値でプロセスグループ
F_SETOWN_EXでは、1 つのスレッドも
持ち主がいなければ
ESRCHです。F_GETOWNとF_GETOWN_EXで読み出し、F_SETSIGで 送るシグナルを選びます。O_ASYNCを持てるのはパイプとソケットだけで、ほかのファイルではF_SETFLが落とし、FIOASYNCで付けようとするとENOTTYです。O_ASYNCを付けたパイプやソケットは、持ち主にカーネルからSIGIOを送ります。F_SETSIGがあれば代わりにそのシグナルを送り、si_code(POLL_IN、POLL_OUT、POLL_ERR、POLL_PRI、POLL_HUP)、si_band、si_fd(O_ASYNCを 付けたディスクリプタ)を付けます。- パイプ:書き込みは、終わるときに読む側へ送ります(いっぱいで書けなかったときも 送ります)。待つときは、待つたびに、その前にも送ります。読み込みは、データがあってもなくても、 終わるときに書く側へ送ります。どちらかの端を閉じると、もう一方へ送ります。
- ソケット:送信すると相手へ送ります。ストリームとパケットのソケットでは、書き込みの
shutdownでも、閉じたときも送り、閉じたときは切断(ハングアップ)になります。データグラムの ソケットでは、どちらでも送りません。バッファが いっぱいで送れなかった側には、読み込みでバッファの 4 分の 3 が空いたときに 1 回送ります。 データグラムとパケットのソケットでは、いっぱいのときにpoll、select、epoll_ctlで 書き込めるかを尋ねた側にも同じように送ります。epoll_waitで尋ねた側にも送りますが、 Linux と同じく、尋ねるのは準備ができた一覧にあるものだけです。一覧に載るのは、追加した ときに準備ができていたもの、起こされたもの、レベルトリガーで報告したあとのものです (エッジトリガーで報告したものは載り直しません)。どちらも、O_ASYNCが付いている間に 起きたものだけが対象です。 帯域外のバイトはPOLL_PRIのシグナルも送ります。 - TCP と UDP のソケット(ネットワーク):Linux の TCP と同じように 送ります。接続が開くと
POLL_OUT、失敗するとPOLL_ERR、データが届くとPOLL_INです。相手の終わりが届くとPOLL_IN(こちらも閉じていればPOLL_HUP)を 送り、続けてPOLL_INを送ります。送信やpollがバッファをいっぱいと見たあとは、 空きができたときにPOLL_OUTを 1 回送ります。DNS の代わりの答えは、用意できた ときにPOLL_INを送り、ゲストのソケットからのデータグラムも、届いたときにPOLL_INを送ります。接続した UDP のソケットへのループバックの ICMP の答えはPOLL_ERRを送ります。 - 待っている読み手:ストリームで読み込みが待っているときは、その待ちを終わらせるデータ、 終わり、切断のシグナルを送りません。Linux もそのときは送りません。分けて送る送信では、 送らないのは最初の分だけです。ただし相手が
shutdownしたときや閉じたときは、 Linux が送らないのは読み手がまだ動いていないときだけです(読み手を起こしてから 確かめるため)。そのため Linux では送ることもあります。データグラムとパケットの ソケットの読み手には送ります。
setsockoptとgetsockoptは、Linux のsk_setsockoptとsk_getsockoptと同じく、 既定の値やチェックも含めてSOL_SOCKETのオプションを扱います。4 バイトに満たないintの値はEINVAL、読み出す値は渡された長さで切り、ほかのレベルはEOPNOTSUPP、 知らないオプションはENOPROTOOPTです。SO_TYPE、SO_DOMAIN、SO_PROTOCOL、SO_ERROR、SO_ACCEPTCONN、SO_PEERCREDとSO_PEERPIDFD(その組を作ったプロセス)は 読むだけです。SO_SNDBUFとSO_RCVBUFは 2 倍にして、Linux の下限と上限に収め、SO_LINGERはstruct lingerを受け取ります。SO_PASSCREDを設定すると、Linux の AF_UNIX と同じく、メッセージが資格情報を運びます。 どちらかの端に設定があるときに送ったメッセージは、送り手のプロセス ID、ユーザー、 グループを運びます。SCM_CREDENTIALSを渡したときは、その値を運びます。設定のある 受け手は、それをSCM_CREDENTIALSのメッセージで、ディスクリプタより先に受け取ります。 資格情報なしで送ったメッセージは、プロセス 0、ユーザーとグループ 65534 として読めます。 そのような受け手のストリームの読み込みは、readとMSG_WAITALLも含めて、資格情報の 違う書き込みをまとめて読みません。SCM_CREDENTIALSはstruct ucredをちょうど 1 つ 持つ必要があり(違えばEINVAL)、どちらの ID も -1 にはできません(EINVAL)。 そのあとでプロセスを探し、なければESRCHです。SO_PASSPIDFDを設定すると、SO_PASSCREDと同じようにメッセージが資格情報を運び、 設定のある受け手は、送り手のプロセスの pidfd(close-on-exec)をSCM_PIDFDの メッセージで受け取ります。Linux 7.0 と同じく、ディスクリプタのあとに来ます。pidfd を 作るのは、そのメッセージ全体が制御のバッファに入るときだけです。入らないときは メッセージを失い(MSG_CTRUNC)、ディスクリプタも作りません。どちらの端も資格情報を 渡していないときに送ったメッセージは、何も運びません。SO_PEERPIDFDは、ペアを 作ったプロセスの新しい pidfd を読み出します。渡した長さに番号の一部しか入らなくても、 pidfd は作ります。読み出すだけのオプションで、setsockoptはENOPROTOOPTです。SO_RCVTIMEOとSO_SNDTIMEO(と_NEWの形)は、Linux と同じく、そのソケットでのrecv、read、send、writeが待つ時間を限ります。限りを過ぎるとEAGAIN、または そこまでに動かした分を返し、MSG_WAITALLもそこで止まります。0 のtimevalは いつまでも待ち、負の値は待ちません。1 秒を超えるマイクロ秒はEDOMです。値は 1000 Hz の時計の刻みで持つので、読み出すとミリ秒に切り上がっています。そのような待ちを シグナルが終わらせるとEINTRになり、Linux のsock_intr_errnoと同じくSA_RESTARTでもやり直しません。SO_RCVLOWATを設定すると、Linux のsock_rcvlowatと同じく、ブロックするストリームのrecvやreadは、その量のバイトが揃うか、バッファが一杯になるまで待ちます。ストリームの 終わり、時間切れ、ディスクリプタの到着では、それより少なくても返します。MSG_PEEK、 ブロックしない読み込み、poll、データグラムとパケットのソケットは待たせません。0 は 1、 負の値はINT_MAXとして扱います。SO_PEEK_OFFは、Linux のsk_peek_offと同じく、MSG_PEEKが読み始める位置を決めます。 覗くたびに読んだ分だけ進み、読み込むたびに取った分だけ戻ります(0 より下には戻りません)。 データグラムとパケットのソケットでは、メッセージ単位で先へ進み、一度覗いた空のメッセージも 飛ばします。MSG_TRUNCは、そこから先のメッセージの長さを返します。どんな値も受け付け、 負の値(既定は -1)では位置を使いません。SO_TIMESTAMPとSO_TIMESTAMPNS、それに_NEWの形は、Linux の AF_UNIX と同じく、 メッセージにタイムスタンプを付けます。設定したデータグラムやパケットのソケットは、 メッセージごとに実時間を制御メッセージで受け取ります。資格情報やディスクリプタより先に並び、SO_TIMESTAMPではtimeval、SO_TIMESTAMPNSではtimespecです。ほかの制御メッセージと 同じく、制御バッファに収まる分に切ります。時刻は、受け手がそのときタイムスタンプを 取っていれば送った時刻、そうでなければ最初に読むか覗いた時刻です。ストリームには付きません。getsockoptは、それぞれ自分の形で設定したときだけ 1 を返します。どれかを切っても、次に どれかを設定するまで_NEWの選択は残ります。ファイルの
MAP_SHAREDのマッピングは、Linux と同じくファイルのページキャッシュを 見せます。どのプロセスのものでも、そのファイルの共有マッピングはみな同じページを 持ちます。そこへの書き込みはreadにも、そのファイルのプライベートなマッピングにも 見え、write、ftruncate、穴あけはマッピングに見える中身を変えます。ファイルの 終わりより先への書き込みは、ファイルに入りません。このマッピングが書き込めるのも、mprotectで書き込めるようにできるのも、Linux と同じく、ファイルを読み書きの両方で 開いたときだけです(ほかはEACCES)。ファイルの
MAP_PRIVATEのマッピングは、まだ書いていないページでは同じページ キャッシュを見せます。そのため、そこではファイルの後の変更も見えます。ページに初めて 書き込むと、Linux と同じく、そのページをそのプロセスだけのために写し取ります。forkした子とは、どちらもまだ書いていないページを共有します。ファイルを縮めて、写しが ファイルの終わりより完全に先になると、その写しは捨てられます。そのため、ファイルが また伸びればファイルを読み直します。Linux も同じく写しを捨てます。ファイルのマッピングで、ファイルの終わりより完全に先にあるページに触れると、 プライベートでも共有でも、Linux と同じく
SIGBUS(BUS_ADRERRとそのアドレス)に なります。そのようなメモリを渡したシステムコールはEFAULTで失敗します。終わりは ファイルに合わせて動きます。ファイルが伸びれば先のページも読めるようになり、縮めば 前に読んだページでもSIGBUSになります。ファイルが一部だけを覆うページは、 ファイルの終わりより先が 0 として読めます。madvise:MADV_DONTNEEDを受けたプライベートなページは空になります。無名の ページはそのあと 0 として読め、ファイルのマッピングのページは、Linux がファイルから 読み込み直すのと同じく、そのときのファイルの中身を読みます。mremapで動かしたあとも、forkした子でも同じです。プログラム自身の セグメントは Linux と同じ形で置きます。ファイルのデータを持つページはファイルの マッピングで、実行ファイルを読み直します。mprotectで書き込めるようにして 書き換えた読み込み専用のセグメント(コード)も同じです。残り(.bss)は無名の メモリで、0 として読めます。共有メモリは自分の中身を保ちます。MADV_FREEでは 中身を残します。メモリが足りているときの Linux と同じです。 アクセスの仕方のヒントは受け付け、ほかの指定にはEINVALを返します。mremapは、後ろが空いていればその場で広げ、空いていなければMREMAP_MAYMOVEで 中身ごと動かします。MREMAP_FIXEDとMREMAP_DONTUNMAPも使えます。共有のメモリは、 動かしてもforkした相手と共有したままです。ファイルのマッピングは、ファイルの 続きの上へ広げられ、そのページは初めて触れたときに読み込みます。また、複数の マッピングにまたがる範囲をMREMAP_FIXEDで動かすと、EFAULTになります(最近の Linux は動かせます)。msyncは、引数と範囲を確かめ、MS_SYNCかMS_ASYNCなら共有のファイルの マッピングへの書き込みをファイルに書き戻します。sched_getaffinityでは、ゲストのどのスレッドもホストのすべての CPU で動けるとします。 そのため、ゲストにはホストと同じ数の CPU が見えます(WebAssembly では 1 つ)。unameは x86_64 の Linux で、ホスト名はpaludariumと答えます。CPUIDと同じく、 ホストのことは見せません。ゲストは root(ユーザーとグループが 0)として動きます。 ファイルはどれも、chownで別の持ち主やグループにするまでは root のもので、 メモリ上のファイルシステムはその持ち主を覚えています。Linux と同じく、chownは ディレクトリ以外のファイルから set-user-ID のビットを外し、グループが実行できる ときは set-group-ID のビットも外します。持ち主とグループが変わらなくても外します。 set-group-ID のディレクトリの中に作ったものはディレクトリのグループを継ぎ、 新しいディレクトリはそのビットも継ぎます。fchmodとfchownは、pipeやpipe2で作ったパイプとソケットペアについても、fstatが返すモードと持ち主を 変えます。パイプでは両端がそれらを共有し、ソケットペアでは端ごとに持ちます。 ホストが用意する標準入力、標準出力、標準エラー出力ではEPERMを返します。chmodとutimensatは、メモリ上のファイルシステムでも、マウントしたホストの ディレクトリでも使えます。Windows のホストでは、chmodが変えるのは読み取り専用の 属性だけです。statが返すのは更新時刻だけで、アクセスと変更の時刻は 0 です。マウントしたディレクトリのファイルでは、渡した両方の時刻がホストに残ります。 umask は、Linux でCLONE_FSが共有するのと同じく、スレッドの間で共有します。fsyncとfdatasyncはファイルとディレクトリでは成功し、パイプ・ソケット・端末ではEINVALを返します。fadvise64はヒントとして受け付けます。statfsは、メモリ上のファイルシステムを 4 GiB の tmpfs として返します。使用量と inode の数は、実際に持っているものです。マウントしたホストのディレクトリでは、 ホストのファイルシステムの値を返します。/proc/self/mountinfo、/proc/self/mounts、/proc/mountsはマウントの一覧です(ルートはtmpfs、--mountごとにhostfs。 ホストのパスは見せません)。マウントごとにmountinfoの行と同じst_devを持つので、dfはどのマウントも表示します。/procにあるのは、ほかには自分のプロセスのものだけです。/proc/self(/proc/<pid>としても)は、readlinkで PID になり、その中のものを相対的に 開けるディレクトリです。ほかに/proc/self/statusと/proc/self/fdがあります。/proc/self/statusは、Linux と同じ項目を同じ順番で持ちます (umask、ID、スレッドの数、シグナルのマスク、ケーパビリティなど)。ただし、何も 測っていないメモリと CPU の項目はありません。/proc/self/fd(モード 0500)は、.、..と、開いているディスクリプタごとの リンクを並べます。名前は番号で、Linux と同じく先頭に 0 のない形で読みます。各リンクのlstatは、ユーザーのビットでディスクリプタのアクセスを示し、大きさは 64 です。statはリンクをたどってファイルを示します。readlinkは Linux と同じ名前を返します。.、..とシンボリックリンクを解決したファイルのパス(このプロセスのものは/proc/<pid>/…)で、どこからも指されなくなれば空白と(deleted)が付きます。ほかにpipe:[<inode>]、socket:[<inode>]、anon_inode:[eventfd]・[eventpoll]・[timerfd]・[pidfd](Linux 6.9 以降は pidfs の名前です)、/memfd:<名前> (deleted)です。 そのためにパイプとソケットは自分の inode 番号を持ちます。リンクを開くと、ファイルを パスで開き直し、memfd なら同じファイルとシールへの新しいハンドルを返します。パイプと 標準の入出力は、持っているアクセスなら同じハンドルを返します。ソケットは Linux と同じくENXIOです。消されたファイル、パイプの反対の端、種類のないファイルも、Linux なら 開き直せますが、ここではENXIOです。/dev(モード 0755)は、メモリのデバイスと、Linux のシステムがそこに置くリンクを 持ち、それだけを並べます。ゲストが/devの下に作ったファイルは、開いたりstatしたりできますが、一覧には出ません。各エントリは 1 つの inode 番号を持ち、statとgetdentsは同じ番号を返します。/devは読み込み用にしか開けず(EISDIR)、 デバイスはO_DIRECTORYでは開けません(ENOTDIR)。null、zero、full、random、urandomは キャラクタデバイス(モード 0666)で、st_rdevに Linux と同じ番号(1:3、1:5、1:7、 1:8、1:9)を持ちます。nullは何も読めず、zeroとfullは 0 を、randomとurandomはホストの乱数を読みます。fullは書き込みをENOSPCで拒み、ほかは 受け取ります。zeroのマッピングは無名のメモリで、MAP_SHAREDなら共有です。fd、stdin、stdout、stderrは/proc/self/fdとその最初の 3 つへのリンク (0777)で、openやstat、それを通るパス(/dev/fd/3)はリンクをたどります。 リンクにO_NOFOLLOWを付けるとELOOPです。O_PATHも付けると Linux はリンク そのものを開きますが、ここでは対応しておらず、やはりELOOPです。ホストの マウントのファイルはデバイス番号を持ちません。CPU 時間は、ホストが測れるところ(Linux・macOS・Windows)では、ゲストのスレッドの 最初のシステムコールからの、ホストのスレッドの CPU 時間(ユーザーとシステムの合計) です。WebAssembly などそれ以外では、代わりに経過時間を数えます。
CLOCK_THREAD_CPUTIME_IDはそのスレッドの時間、CLOCK_PROCESS_CPUTIME_IDは プロセスのスレッドの合計(それぞれ最後のシステムコールの時点)を返します。子は 0 から 始まり、待たれたときに親の「子の時間」に足されます。getrusage、times、wait4とwaitidの rusage は、すべてをユーザーの時間として返します。システムコールのための エミュレータの処理も、ホストのユーザー空間で動くためです。メモリの使用量は測りません。clock_gettimeは、CLOCK_MONOTONIC_RAW、粗い時計、CLOCK_BOOTTIME(ホストは サスペンドしないものとします)、アラームの時計、CLOCK_TAI(TAI のずれは 0)も 読めます。clock_getresは 1 ns、粗い時計では 4 ms を返します。accessは root として、読み書きはいつも許し、実行は実行のビットがあるか ディレクトリのときに許します。getgroupsはグループを返しません。sysinfoは、空きが 4 GiB でスワップのない固定のメモリを返し、ホストのメモリは 見せません。起動からの時間は単調時計の値です。timerfd は
CLOCK_REALTIMEかCLOCK_MONOTONICで動き(CLOCK_BOOTTIMEはCLOCK_MONOTONICとして扱います)、絶対時刻と周期の指定ができます。読むと前回からの 満了の回数を返し、満了するとpollとepollから読めるように見えます。TFD_TIMER_CANCEL_ON_SETは受け付けますが、ゲストは時計を設定できないので取り消しは 起きません。アラームの時計はEPERMを返します。POSIX タイマーは
CLOCK_REALTIMEかCLOCK_MONOTONICで動き、プロセスへの シグナル(SIGEV_SIGNAL。SI_TIMERの siginfo とオーバーランつき)か、プロセスの 中の 1 つのスレッドへのシグナル(SIGEV_THREAD_ID)で知らせるか、何も知らせません (SIGEV_NONE)。SIGEV_THREADは、Linux と同じくSIGEV_SIGNALとして扱います (スレッドは C ライブラリがSIGEV_THREAD_IDを使って作ります)。スレッドへの タイマーのシグナルは、そのスレッドがまだ受け取っていない間だけオーバーランに足され、 スレッドが終わっていれば失われます。CPU 時間の時計のタイマーはEINVALを返します。タイマーはforkでは受け継がれず、execveで消えます。rt_sigtimedwaitは、プロセス全体に 届いてブロックされているシグナルも受け取ります。clone3は、struct clone_argsを Linux と同じように確かめ(後ろに足された部分が 0 でない長い構造体にはE2BIG)、それが表すcloneとして動きます。CLONE_CLEAR_SIGHANDは、ハンドラを設定したシグナルを既定に戻します。set_tidとCLONE_INTO_CGROUPはEINVALを返します。cloneそのものは、Linux と同じく flags の 下位 32 ビットだけを読むので、CLONE_CLEAR_SIGHANDはclone3でしか使えません。prctlはPR_SET_NAME/PR_GET_NAME(スレッドごとの 15 バイトまでの名前。execveでプログラムのファイル名になり、新しいスレッドとプロセスに受け継がれます)、PR_SET_PDEATHSIG(親が終わったときにシグナルを送ります)、PR_SET_DUMPABLE、PR_SET_CHILD_SUBREAPER(forkでは受け継がれず、execveでは残ります)、PR_SET_NO_NEW_PRIVSと、それぞれのPR_GET_*に対応します。ほかの指定にはEINVALを返します。プロセスが終わると、その子は、
PR_SET_CHILD_SUBREAPERを設定していてまだ終わって いない一番近い祖先に、いなければ PID 名前空間の init にあたる最初のプロセスに 引き取られます。Linux のforget_original_parentと同じです。その後getppidは 新しい親を返し、新しい親はその子を待てて、子の終了シグナルはSIGCHLDになります。 すでに終わっていた子は、すぐに新しい親に知らせます(その親がSIGCHLDを 無視していれば、すぐに回収します)。最初のプロセスが終わると、PID 名前空間の init が終わったとき(Linux の
zap_pid_ns_processes)と同じように、ほかのすべてのプロセスがカーネルからSIGKILLを受け取り、実行もそこで終わります。終わったプロセスのせいでプロセスグループが孤児になり(同じセッションの別のグループに 親を持つメンバーがいなくなり)、そのグループに止まっているメンバーがいれば、Linux の
kill_orphaned_pgrpと同じように、すべてのメンバーにカーネルから(SI_KERNELで)SIGHUP、続けてSIGCONTを送ります。最初のプロセスの親はエミュレータの外にあり、 最初のプロセスが動いている間は、そのような親として数えます。最初のプロセス自身も、 ほかの親と同じように、自分の子のグループをつなぐ親として数えます。Linux が数えない のはシステム全体の init の子だけで、最初のプロセスは、コンテナと同じく PID 名前空間の init として振る舞うからです。killは、Linux のkill_something_infoと同じように、PID かそのスレッドの ID でそのプロセスに、0 で呼び出し元のプロセスグループに、-pgidでほかのグループに、 -1 で最初のプロセスと呼び出し元以外のすべてのプロセスに届きます。終わったプロセスも、 回収されるまでは数えます。送る相手のプロセスがなければ、無効なシグナルでもESRCHを返し、相手がいれば無効なシグナルはEINVALを返します。killやpidfd_send_signalで送ったシグナルのsi_pidは送り主の PID に、PR_SET_PDEATHSIGのシグナルのsi_pidは終わった親の PID になります。rt_sigqueueinfo(sigqueueの中身)は、呼び出し元の siginfo をそのままプロセスに 送り、rt_tgsigqueueinfoは 1 つのスレッドに送ります。そのためシグナルは値を運び、 リアルタイムシグナルは送った数だけ届きます。Linux と同じく、カーネルやkill、tkillが付けるsi_codeを付けられるのは、自分自身に送るスレッドだけです(相手を 探す前にEPERM)。Linux が形を知らないsi_codeでは、siginfo の残りを 0 にして おく必要があります(E2BIG)。資源の上限は Linux の既定値(
RLIMIT_NOFILEは 1024/4096、RLIMIT_STACKは 8 MiB/無制限など)から始まり、forkで受け継がれ、読み書きできます。ハードのRLIMIT_NOFILEは 1048576(Linux のfs.nr_open)まで、埋め込む側がそれより低い 天井を決めたときはその値まで上げられます。Linux と同じく 守らせるのは次の 3 つです。RLIMIT_NOFILE:ソフトの上限を超える新しいディスクリプタはEMFILE(openは 何も作りません)、上限を超える番号へのdup2/dup3はEBADF、上限を超える 番号からのF_DUPFDはEINVALになります。RLIMIT_FSIZE:通常のファイルへの書き込み(O_APPENDなら末尾から書くwrite、writev、pwrite、pwritevと、copy_file_range、sendfile)はソフトの上限で 止まります。上限かその先から始まる 書き込みと、ファイルを上限より大きくするtruncate/ftruncateとfallocateは、SIGXFSZを 起こしてEFBIGで失敗します。RLIMIT_CORE:コアダンプするシグナル(SIGSEGV、SIGABRTなど)で終わる プロセスは、ソフトの上限が 1 ページ以上で、PR_SET_DUMPABLEでダンプを止めて いなければ、Linux のdo_coredumpが既定のcore_patternでするのと同じように、 作業ディレクトリにcoreという名前の ELF のコアを書きます(モードは 0600 から umask を除いたもので、前のコアは置き換えます)。コアには、各スレッドのレジスタ (NT_PRSTATUS)、プロセス(NT_PRPSINFO)、致命的なシグナル(NT_SIGINFO)、 補助ベクトル、メモリが入ります。そのときwaitはCLD_DUMPEDとコアのフラグを 返します。上限より大きいコアはそこで切りますが、Linux が書き始めたコアを ダンプしたと報告するのと同じように、ダンプしたと返します。
プロセスグループとセッションも同じように持ち、
wait4とwaitidでプロセス グループを選んで待てます。最初のプロセスのセッションはエミュレータの外で始まった ものなので、getsidは、Linux が PID 名前空間の外の ID を示すのと同じく 0 を 返します。setsidは、PID がグループを指すプロセスではEPERMで失敗し、それ 以外のプロセスを新しいセッションとグループの先頭にします。setpgidは Linux と 同じくセッションを確かめ、セッションの先頭、別のセッションの子、別のセッションの グループにはEPERMを、execveを済ませた子にはEACCESを返します。グループは、 まだ回収されていない終わった子も含め、メンバーが残っている間は存在します。nice 値は Linux と同じくスレッドごとに持ちます。新しいスレッドやプロセスは作った スレッドの値を受け継ぎ、
execveは呼んだスレッドの値を保ちます。getpriorityは 見つけたスレッドのうち最も高い優先度を20 - niceで返し、setpriorityはそれらを すべて -20〜19 に収めた値にします。ゲストは root なので下げることもできます。PRIO_PROCESSは 1 つのスレッド(0 は呼んだスレッド)、PRIO_PGRPはプロセス グループのすべてのスレッド、PRIO_USERはユーザー 0 のすべてのスレッドです。まだ 回収されていない終わったプロセスも見つかります。スレッドがなければESRCH、 知らないwhichはEINVALです。値は報告するだけで、スケジューリングは変えません。waitidは、Linux と同じ確かめ方と書き出し方をします。報告する子がないときは、 エラーのときも含めて siginfo の項目を 0 にします。終わった子は、WEXITEDかWCONTINUEDがないと待てません(ECHILD)。停止と再開は、WUNTRACED(WSTOPPED)かWCONTINUEDを付けた待ちにそれぞれ一度だけ報告し(WNOWAITなら 残します)、親がSA_NOCLDSTOPを付けていなければSIGCHLDで知らせます。SIGCHLDの siginfo には、子の PID、CLD_*のコード(CLD_EXITED、CLD_KILLED、CLD_STOPPED、CLD_CONTINUED)、状態、si_utimeとしての CPU 時間が入ります。si_uidは 0 で、SIGCHLDが保留中に届いた次のSIGCHLDは、Linux と同じく捨てます。SIGCHLDを無視するかSA_NOCLDWAITを設定したプロセスの子は、終了シグナルがSIGCHLDなら、Linux のdo_notify_parentと同じく終わったその場で回収します。 待ちはそうした子が終わるまで待ってからECHILDで失敗し、その CPU 時間は親の 子の分に足しません。SIGCHLDを無視しているときは、ブロック中でもSIGCHLDを 保留しません。それより前にできたゾンビは、待ちのために残ります。pidfd_openの pidfd は、そのプロセスが終わると読めるようになり(回収されると 切断も示します。自動で回収された子では両方を同時に示します)、epollで待てます。ノンブロッキングの pidfd でのwaitid(P_PIDFD)はEAGAINを返します。pidfd_send_signalは siginfo と 範囲の指定を受け付けず、PIDFD_THREADを付けてもプロセス全体を指します。不正なポインタを渡されたときは
EFAULTを返します。
ネットワーク
組み込む側が行き先を許可しない限り、ゲストはネットワークを使えません (Config::with_network、コマンドの --allow-net、JavaScript の network の指定。 ブラウザでは 中継 を通します)。許可の一覧には、ホスト名 (github.com)、そのすべてのサブドメイン(*.github.com)、IPv4 アドレス、IPv6 アドレス([::1])を書きます。どれもポートを付けるか(github.com:443)、付けずに すべてのポートを許します。
- TCP のクライアントのソケット:
socket(AF_INETまたはAF_INET6, SOCK_STREAM)(SOCK_NONBLOCK、SOCK_CLOEXECも付けられます)は、ホストを通して接続するか、 ループバックの上のゲスト自身のサーバーに接続します。connect:ブロッキングなら接続を待ちます。ノンブロッキングならEINPROGRESSを返し、そのあとソケットが書き込めるようになり、SO_ERRORが結果を 1 回だけ返します。2 回目の呼び出しはEALREADY、接続したあとは 0、その次からはEISCONNです。失敗した接続はその誤りを返し、SO_ERRORが取ったあとはECONNABORTEDです。IPv6 のソケットは、IPv4 のアドレスに::ffff:a.b.c.dとして 届きます。- 方針が許さない行き先は
ENETUNREACHです。ただし 53 番ポートは DNS の代わりに 届き(下を参照)、ゲストのソケットが待ち受けていないループバックのアドレスは 拒まれます(ECONNREFUSED。ノンブロッキングのソケットならEINPROGRESSのあとSO_ERRORで)。 - データ:
read、write、send/recv、sendto/recvfrom、sendmsg/recvmsgを、MSG_PEEK、MSG_WAITALL、MSG_DONTWAIT、MSG_NOSIGNALとともに使えます。書き込みはすべて送るまで続きます。相手が リセットした接続は、その誤りを 1 回返し、そのあとは入力の終わりです。接続したことの ないソケットや、shutdown(SHUT_WR)のあとの送信はEPIPEで、SIGPIPEも 送ります。接続したことのないソケットの受信はENOTCONNです。 - 準備状態:
poll、select、epollは Linux のtcp_pollと同じものを 見ます。接続する前は切断と書き込み可能、接続している間は何もなし、データか入力の 終わりがあればPOLLIN(終わりならPOLLRDHUPも)、送信のバッファに空きが あればPOLLOUT、両方向を閉じたらPOLLHUP、誤りが残っていればPOLLERRです。 shutdown、getsockname(接続したあとはホストのアドレスとポート)、getpeername、bind、FIONREAD/SIOCINQ、SIOCOUTQ。- オプション:
SO_TYPE、SO_DOMAIN、SO_PROTOCOL、SO_ERROR、SO_ACCEPTCONN、SO_KEEPALIVE、SO_REUSEADDR、SO_REUSEPORT、SO_SNDBUF、SO_RCVBUF、SO_RCVTIMEO、SO_SNDTIMEO、SO_LINGER、SO_PRIORITY、SO_BROADCAST、SO_OOBINLINE、TCP_NODELAY(ホストにも伝えます)、TCP_KEEPIDLE、TCP_KEEPINTVL、TCP_KEEPCNT、TCP_QUICKACK、TCP_USER_TIMEOUT、IP_TOS、IPV6_V6ONLY。既定値と範囲の確かめ方は Linux と 同じです。
- 名前解決:組み込みの DNS の代わりが、ゲストのリゾルバが 53 番ポートへ送る UDP の 問い合わせに、宛先のアドレスによらず答えます。宛先は
/etc/resolv.confのサーバー、 またはその既定です。許可した名前のAとAAAAの問い合わせには、ホストが引いた アドレスを(60 秒の有効期間で)返し、ほかの名前にはNXDOMAINを返します。許可した 名前について返したアドレスへの接続は、最後に返してから 10 分の間、その名前と同じ ように許可します。データグラムの答えは 512 バイトまでです。すべてのアドレスが 入らない答えには切り詰めの印(TC)を付け、TCP で問い合わせ直せるようにします。 53 番ポートへの TCP の接続も、宛先のアドレスによらず DNS の代わりに届きます。 2 バイトの長さを前に付けた問い合わせを受け取り、1 つずつ、すべてのアドレスを入れて 答えます。 - 名前で届くのはグローバルなアドレスだけ:ループバック、プライベート(RFC 1918 と ユニークローカル)、リンクローカル、共有(RFC 6598)、マルチキャスト、予約済みの アドレス(NAT64 や 6to4 のアドレスに含まれる IPv4 のアドレスとしても)は、DNS の代わりが答えから除き、名前の規則でも届きません。DNS がそこを指す 名前(DNS リバインディング)で、ホスト自身やその内側のネットワークに届かないように するためです。アドレスが 1 つも残らない名前は
NXDOMAINです。localhostと そのサブドメインは、ループバックにも届きます。ほかのそういうアドレスに届くには、 アドレスそのものを許可します(127.0.0.1:8080)。
ループバックのサーバー
TCP のソケットは、ゲスト自身のループバックの上で Linux と同じく bind、listen、 accept できます。UDP のソケットも、そこで bind してデータグラムをやり取りできます。ゲストのソケットが、待ち受けているループバックのアドレスに接続すると、 方針によらずカーネルの中で届き、ゲストの外には出ません。ホストもほかのマシンも、 ゲストの待ち受けには届きません。待ち受けていなければ、許可したアドレスはこれまで どおりホストに届き、ほかは拒まれます。何も許可しないネットワーク(JavaScript の allow: []、Rust の NetworkPolicy::new())にもループバックはあります。コマンドで ネットワークを作るには --allow-net が要ります。
bindはループバックのアドレス(127.0.0.0/8、::1、IPv6 のソケットでは IPv4 を IPv4-mapped にしたもの)か未指定のアドレスに、ポートを付けて、ポート 0 なら 32768〜60999 の一時的なポートを取ります。確かめ方とその順序は Linux と同じです。 ほかのアドレスはEADDRNOTAVAIL、bind済みならEINVAL、両方が覆うアドレスで ほかのソケット(待ち受け、それが受け付けた接続、クライアント)がポートを持って いればEADDRINUSEです。ただし、両方がSO_REUSEADDRを付けていて相手が待ち 受けていないとき、両方がSO_REUSEPORTを付けているときは共有できます。::の IPv6 のソケットは、IPV6_V6ONLYでなければ IPv4 のポートも持ちます。listenはbacklog+ 1 本までの接続を待たせます(多くて 4096、net.core.somaxconn)。bindしていないソケットは、すべてのアドレスで一時的な ポートを取ります。待ち受けているソケットは、接続が待っている間は読めます (poll、select、epoll、O_ASYNCのPOLL_IN)。SO_ACCEPTCONNを示し、FIONREADはEINVAL、connectはEISCONNで、shutdown(SHUT_RD)かAF_UNSPECのconnectで待ち受けをやめます。connectは、待ち受けの列が接続を受け取った時点で、acceptの前に戻ります。 ノンブロッキングなら Linux と同じくEINPROGRESSです。列がいっぱいなら、acceptが空きを作るまで待ちます。acceptとaccept4(SOCK_NONBLOCK、SOCK_CLOEXEC)は、最初に 待っている接続と相手のアドレスを返します。待ち受けているソケットがノンブロッキング でなければ、SO_RCVTIMEOまで待ちます(EAGAIN)。受け付けたソケットは、待ち 受けているソケットのオプションを引き継ぎます。- リセットは Linux と同じです。読んでいない入力のあるソケットや
SO_LINGERの 時間が 0 のソケットを閉じたとき、AF_UNSPECで切断したとき、まだ渡していない 接続のある待ち受けを閉じたときは、相手をリセットします(相手は持っている分を 読んだあとECONNRESET)。閉じた相手に送ったバイトにはリセットが返り、次の送信はEPIPEです。 - UDP の
bindは TCP と同じですが、ポートは UDP 自身のもので、両方がSO_REUSEADDRを付けていればポートを共有できます。ループバックのアドレスへの データグラムは、そこにbindしたゲストのソケットに、Linux のcompute_scoreと 同じ選び方で届きます。接続したソケットは相手からだけ受け取り、そのアドレスにbindしたもの、接続したもの、IPv4 のものが先で、同じなら最後にbindしたものです。bindせずに送るソケットは、すべてのアドレスで一時的なポートを取ります。接続する ソケットは、ループバックの送り元のアドレスでポートを取り、相手からだけ受け取ります。AF_UNSPECで切断すると、ゲストが自分でbindしたポートとアドレスだけが残ります。 データグラムは IPv4 で 65507 バイト、IPv6 で 65527 バイトまでで(超えるとEMSGSIZE)、空でもよく、読み手のバッファに切り詰められます(MSG_TRUNC)。FIONREADは最初のものの長さです。受け取るソケットがないときは、そこに接続した ソケットにループバックの ICMP の答えが届き、次の送信か受信が 1 回だけECONNREFUSEDになります(SO_ERRORとPOLLERRにも出ます)。53 番ポートへの データグラムで、受け取るゲストのソケットがないものは、これまでどおり DNS の代わりに 届きます。 - UDP の
shutdownは、接続していてもいなくてもソケットに印を付けます(接続して いなければ、Linux のinet_shutdownと同じく、それでもENOTCONNを返します)。 読む側を閉じると、待つはずの受信はすぐに 0 で終わります。ただしデータグラムは それまでどおり届き、先に読めます。ノンブロッキングの受信はEAGAINです。送る側を 閉じると、送信はEPIPEで、SIGPIPEは来ません。pollはそのあとPOLLINとPOLLRDHUPを、両方を閉じるとPOLLHUPも示します。
スレッドとプロセス
cloneで作ったゲストのスレッドは、ネイティブではホストのスレッドに、WebAssembly では Worker に割り当てます。fork/vfork相当のcloneとexecveで、仮想ファイルシステムの中の別の プログラムを起動し、wait4で終了を受け取れます。Rust のstd::process::Commandも 動きます。posix_spawnを使わずに fork するとき(たとえばpre_execを使うとき)も 同じです。CLONE_THREADなしでCLONE_VM | CLONE_SIGHANDを付けたcloneで作ったプロセスは、 シグナルの処理の設定を親と共有します。片方のsigactionが、もう片方にも見えます。execveすると、Linux と同じく、そのプロセスは自分だけの設定に戻ります。
シグナル
- 登録、マスク、配送(Linux の x86-64 のシグナルフレームと
rt_sigreturn)、sigaltstack、リアルタイムシグナルのキュー、SIGSTOPによる停止と再開を扱います。 停止で中断されたブロッキングの呼び出しは、ハンドラが動かないときの Linux と同じく、 再開の後にやり直します。対象はread、write、readv、writev、pread64、pwrite64、preadv、pwritev、wait4、waitid、pause、rt_sigsuspend、flock、sendto、recvfrom、sendmsg、recvmsg、ppoll、select、pselect6、時間制限なしのpoll、時間制限なしのfutexの待ちです。nanosleep、相対時間のclock_nanosleep、時間制限つきのpoll、時間制限つきのfutexの待ちは、restart_syscallで元の締め切りのまま待ちを続け(futexは先に値をもう一度確かめ、変わっていればEAGAINを返します)、絶対時刻のclock_nanosleepはもう一度実行します。ハンドラが 動いたときはやり直さず、EINTRを返します。SA_RESTARTを付けたハンドラが動いたときは、Linux と同じく、ハンドラから戻った後に、停止の後にやり直す呼び出し(readから時間制限なしのfutexの待ちまで)をやり直します。 ただしpause、rt_sigsuspend、poll、ppoll、select、pselect6と、SO_RCVTIMEOやSO_SNDTIMEOで待ちに時間制限のあるソケットの呼び出しは、EINTRを返します。epoll_waitとrt_sigtimedwaitは、Linux と同じくEINTRを返します。rt_sigsuspendは、シグナルのマスクを渡されたマスクに置き換えてハンドラが動くまで待ちます。 ハンドラはそのマスクで動き、終わると元のマスクに戻ります。 - シグナルを無視にする
sigaction(SIG_IGNか、既定で無視されるSIGCHLD、SIGCONT、SIGURG、SIGWINCHへのSIG_DFL)は、Linux のdo_sigactionと同じく、プロセスと すべてのスレッドで保留中のそのシグナルを捨てます。その後に送られたブロック中の シグナルは、保留のまま残ります。
ファイルシステム
- 既定では、実行ごとに、ほかと切り離されたメモリ上のファイルシステムを作ります。
/tmpもあります。 - 作成、読み書き、ハードリンク、シンボリックリンク、
flock、rename、ディレクトリの 一覧、stat系に対応しています。開いているファイルはunlinkの後も使えます。 - 起動の前にファイルを置けます。
- 指定したときだけ、ホストのディレクトリをマウントできます(コマンドの
--mount、 Rust のConfig::mounts)。操作はすべてそのディレクトリの中に閉じ込められ、..やシンボリックリンクで外には出られません。
確かめたプログラム
- C と Rust の hello world と、命令ごとの小さなテストのプログラム。
- probe:スレッドとファイルシステムの機能を 8 項目で確かめる Rust のプログラム (tokio のタイマー、UNIX のソケットの組、rayon の並列処理、Mutex と Condvar、 ファイル操作、ハードリンク、シンボリックリンク、
flock)。 - aube:Rust で書かれた Node.js のパッケージマネージャ(v2.8.3)。
--version、install、install --frozen-lockfile、listの 4 つの操作が、本物の Linux と 同じ出力と終了コードになります。 - ripgrep:Rust で書かれたテキストの検索のツール(15.2.0)。検索を複数のスレッドで 動かします。正規表現での置き換え、並列の検索、ファイルをメモリへマップした検索、2 つの エラーを含む 9 つの検索が、本物の Linux と同じ出力と終了コードになります。
--versionは比べていません。CPUIDが見せる SIMD の機能を表示するためで、 paludarium はそれをわざと絞っています。
これらは、Linux・macOS・Windows と、Node.js・Chromium・Firefox・Safari のすべてで、 本物の x86-64 Linux と同じ結果になることを確かめています。