diff --git a/doc/spec_Foundation.md b/doc/spec_Foundation.md index f7e4672..8b7245c 100644 --- a/doc/spec_Foundation.md +++ b/doc/spec_Foundation.md @@ -341,6 +341,8 @@ S モードの R30 は、常にスタブのアドレスでなければなりま - 2 = CPU 統計。16 bytes のヘッダ(`elapsed_cycles`、`kernel_cycles`、`idle_cycles`、`clock_hz`)に、プロセスごとの 8 bytes(`pid`、`user_cycles`)が続きます。すべて u32、カウンタは 2^32 で wrap します。レコード順は sel 1 と同じで、終了記録も含み、再利用時は新 PID のカウンタを 0 にします。PeC timer ch1 を IRQ なしの 48 MHz free-running clock としてカーネルが予約します。スケジューラの起動時から計測し、HALT 中の経過も idle に含みます。使用率はスナップショット間の符号なし差分で計算し、間隔は約89秒未満とします。 - `kernel_cycles` は C の trap ハンドラ入口から scheduler 終了までの概算です。アセンブリの context save/restore と C 計測関数の境界外の命令は、前後の user/idle に含まれます。厳密な OS オーバーヘッドとは一致しません。`user_cycles` はプロセスの全スレッドの合計、syscall 処理時間は kernel 側です。ch1 による計測自体の実行コストもあります。 - 3 = `fnd_meminfo`(16 bytes)。順に `ram_bytes`、`vram_bytes`、`pcmram_bytes`、予約(0)。ハードウェアの実装容量で、BMC のメモリ容量レジスタ([BMC §1.5](spec_BMC_DMAC.md))の値です。`ram_bytes` は sel 0 と同じ(容量不明なら 16 MiB)。`vram_bytes` と `pcmram_bytes` は BMC の値そのままで、0 は容量不明を表します + - 4 = 呼び出したプロセス自身の `pid` と `parent`(8 bytes、u32 が 2 つ)。POSIX の `getpid`/`getppid` 用 + - 5 = PROCESS ハンドルの pid。ハンドル番号を `buf` に渡し(メモリは使わない。`cap` は無視)、結果が pid です。ハンドルが PROCESS でなければ `FND_E_BAD_HANDLE`。`proc_fork` や `proc_spawn` が返したハンドルから子の pid を知るのに使う - その他は `FND_E_BAD_ARG` ### 9.1 ケーパビリティ @@ -396,13 +398,15 @@ S モードの R30 は、常にスタブのアドレスでなければなりま | Off | 名前 | 意味 | |---:|---|---| | 0 | magic | `0x464E4453`(`FNDS`) | -| 4 | version | 1 | +| 4 | version | 2(1 は env_count、env_off を欠く。読み手は 0 として扱う) | | 8 | size | ブロック全体の byte 数 | | 12 | argc | 引数の数 | | 16 | argv_off | argc 個の u32 オフセットの配列(ブロック先頭から) | | 20 | cwd_off | カレントディレクトリ(絶対パス)の位置 | | 24 | role_count | 役割表の件数 | | 28 | roles_off | `(role, handle)` の組の配列 | +| 32 | env_count | 環境変数の数(version 2) | +| 36 | env_off | env_count 個の u32 オフセットの配列。指す先は `NAME=value`(version 2) | - 文字列は NUL で終わります。 - role の値:1 `STDIN`、2 `STDOUT`、3 `STDERR`、4 `FS`(fs への CHANNEL)、5 `CONSOLE`。 @@ -423,7 +427,7 @@ CPU が Extension V を報告するとき(`CPU_FEATURES` bit 9)、カーネ - **`blk_io`**(§10.4):DMA は変換されないので、転送はカーネルのバッファを経由し、完了後に複写します。 - **フォールトと COW**:`proc_fork`(§9.6)で共有した書き込み可能ページは、PTE の W を落として SW bit 9(COW)を立てます(SW bit 8 は所有フレーム)。書き込み保護フォールト `0x8006000A` が COW ページを指す場合、共有中なら新しいフレームへ 4 KiB をコピーし、最後の所有者ならコピーせず W を復元します。TLB を消去して同じ EPC の命令を再実行します。真の読み取り専用ページや未マップページへのアクセス、COW 解決のメモリ不足を含む、ほかの U モードフォールトはプロセスを終了します。終了コードは CAUSE です。需要ページングはないので、ページは `proc_spawn` と `mem_grow` が先にすべてマップします。 - **`sys_info`**:sel 0 の `free_bytes` は未使用の物理フレームの合計、sel 1 のプロセスごとのメモリはマップされた所有フレーム(ページ表を除く)の合計です。共有フレームは各プロセスの値に含まれるため、プロセスの値の合計は物理使用量と一致しません。 -- **限界**:プロセスは 7 個、スレッドは 8 個までのままです(§9)。 +- **限界**:プロセスは 10 個、スレッドは 16 個までです。pipe サービスが 1 つ使います。 ### 9.4 終了と後始末 @@ -602,6 +606,8 @@ stage1 は、`kernel_load` = `0x00010000`、`kernel_size` ≤ `0xE0000`(§5.1 カーネルは同梱モジュールを表の順に spawn します。各モジュールには、argv = [name]、cwd = `/`、役割表なしの起動ブロックを与えます。表の最後は init でなければなりません。すべて spawn し終えたら、staging 領域を解放します。 +標準の構成は console、disk、fs、pipe([API §9.5](spec_Foundation_API.md))、init の順です。 + ## 12. C からの利用 - ABI ヘッダ `foundation/include/foundation/{abi.h,errors.h,ipc.h}` に、本書の番号と構造体を定義します。 diff --git a/doc/spec_Foundation_API.md b/doc/spec_Foundation_API.md index 519800a..184d0bc 100644 --- a/doc/spec_Foundation_API.md +++ b/doc/spec_Foundation_API.md @@ -10,7 +10,7 @@ Revision 0.1、2026-10-24。Status: **ドラフト(主要な設計判断は確 |---|---| | プロトコル | [Foundation §8](spec_Foundation.md) の IPC(`fnd_msg`、`ipc_call`、ハンドル転送)の上に載る、サービスとクライアントの約束 | | サービス | fs(`svc_lookup("fs")`)、console(`svc_lookup("console")`)、デバイスサービス | -| ライブラリ | libfnd(`foundation/lib/fnd`、C11、WAL `fnd` 1.2 または静的リンク) | +| ライブラリ | libfnd(`foundation/lib/fnd`、C11、WAL `fnd` 1.3 または静的リンク) | | ヘッダ | `foundation/include/foundation/{fnd.h,api.h}`。`api.h` は本書の番号と構造体、`fnd.h` は libfnd の関数 | 本書が定めないもの: @@ -45,6 +45,7 @@ Revision 0.1、2026-10-24。Status: **ドラフト(主要な設計判断は確 | `0x0100–0x01FF` | fs セッション操作(§6) | | `0x0200–0x02FF` | ディレクトリ操作(§7) | | `0x0300–0x03FF` | デバイスサービスの操作(§9) | +| `0x0400–0x04FF` | pipe サービスの操作(§9.5) | | `0x1000–0x10FF` | コンソール操作(§8) | | `0x1100–0x11FF` | VDPビットマップ/CLUT操作(§9.1) | | `0x1200–0x12FF` | SGC 状態・出力GP・リセット操作(§9.3) | @@ -80,7 +81,7 @@ Revision 0.1、2026-10-24。Status: **ドラフト(主要な設計判断は確 | オブジェクト | 作る操作 | 使える操作 | |---|---|---| | fs セッション | `svc_lookup("fs")`、`FS_SESSION_NEW` | §6 | -| ストリーム | `FS_OPEN`、`DEV_OPEN`(ファイル、コンソール、デバイス) | §5。コンソールなら §8 も | +| ストリーム | `FS_OPEN`、`DEV_OPEN`(ファイル、コンソール、デバイス)、`PIPE_OPEN`(pipe の両端) | §5。コンソールなら §8 も | | ディレクトリ | `FS_DIR_OPEN` | §7 | - 開いたファイル 1 つが 1 つの CHANNEL です。libfnd のファイルハンドルは、カーネルのハンドル番号そのものです(実装案 §4.4 の「別の名前空間」という案は採らない)。 @@ -454,13 +455,28 @@ C API は `` の `fnd_sgc_get_status`、`fnd_sgc_set_output` - `ST_STAT` は DEVICE、上表の mode、UID 0、size 0 を返します。乱数デバイスの flags は READ_ONLY、UART は 0 です。`ST_SYNC` は成功する no-op、seek/truncate/その他の操作は UNSUPPORTED。権限のない read/write は ACCESS です。ユーザーバッファは IPC の検証後に `mm_copy_*` でコピーします。 - C アプリは既存の `fnd_file_open`、`fnd_read`、`fnd_write`、`fnd_close` を使えます。たとえば `fnd_file_open("/dev/prng", FND_OPEN_READ, FND_MODE_DEFAULT, &h)` で開いて任意長を読み出せます。 +### 9.5 pipe サービス `pipe` + +名前なしの pipe(POSIX の `pipe(2)`)のためのサービスです。`svc_lookup("pipe")` で得た CHANNEL は `PIPE_OPEN` だけを受け付けます。pipe の両端は、サービスの PORT から作った CHANNEL 1 本ずつで、badge で端を区別します。端のハンドルは `dup`・`XFER_HANDLE`・`proc_fork` で増やせます。端の最後のハンドルが閉じる(`CLIENT_GONE`)と、その端が閉じたことになります。 + +| op | 名前 | 要求 | 応答 | +|---|---|---|---| +| `0x0400` | `PIPE_OPEN` | `a0` = 0: 新しい pipe を作る。1: `a1` = pipe の id の書き端を付ける | `a0` = pipe の id、`a3` = 端のストリーム(`XFER_HANDLE`)。作る呼び出しは読み端を返す | + +- 書き端は pipe ごとに 1 回だけ付けられます(付け済みなら `FND_E_NOT_FOUND`)。読み端と書き端の両方が閉じると pipe は消えます。 +- 端のストリームは §5 のうち `ST_READ`(読み端)、`ST_WRITE`(書き端)、`ST_STAT`(`type` = 5 PIPE)、`ST_SYNC`(何もしない)を受け付けます。`ST_SEEK` と `ST_TRUNCATE` は `FND_E_UNSUPPORTED`、向きが違う端への読み書きは `FND_E_ACCESS` です。 +- `ST_READ` はデータが届くまで待ち、届いた分(最大 `a0`)を返します。書き端がすべて閉じていてデータがなければ、長さ 0(EOF)を返します。 +- `ST_WRITE`(4096 bytes まで)は、全体が入る空きができるまで待ち、まとめて書きます(原子的)。読み端がすべて閉じていれば `FND_E_PEER_GONE` です。待っている要求は端が閉じたときに答えます。 +- 容量は 1 つの pipe につき 4096 bytes、同時に待てる要求は 32 件、pipe は 32 本です。超えると `FND_E_LIMIT` です。 +- サービスはどのバックエンドでも起動します。ただし `fork` で端を共有できるのは VM だけです(`proc_fork` が VM 専用のため)。 + ## 10. 構造体 ### 10.1 `fnd_stat`(64 bytes) | Off | 名前 | 意味 | |---:|---|---| -| 0 | `type` | 1 FILE、2 DIR、3 DEVICE、4 CONSOLE | +| 0 | `type` | 1 FILE、2 DIR、3 DEVICE、4 CONSOLE、5 PIPE | | 4 | `mode` | FS2 §8 の mode | | 8 | `uid` | 所有者 | | 12 | `flags` | bit 0 `READ_ONLY`(mount か媒体が書き込み禁止)、bit 1 `SEEKABLE`。その他は 0 | @@ -602,7 +618,8 @@ typedef struct { const char *const *argv; /* argv[0] を含む */ const char *cwd; /* NULL なら現在の cwd */ fnd_handle std_in, std_out, std_err; /* 0 なら自分のものを渡す */ - uint32_t flags; /* 予約。0 */ + uint32_t flags; /* FND_EXEC_KEEP_ARGV0(1): argv[0] を置き換えずそのまま渡す。他は予約。0 */ + const char *const *envp; /* NULL 終端の "NAME=value"。NULL なら空の環境 */ } fnd_exec_t; fnd_status fnd_proc_exec(const char *path, const fnd_exec_t *ex, fnd_handle *proc); @@ -613,9 +630,11 @@ fnd_status fnd_proc_kill(fnd_handle proc); `fnd_proc_fork()` は WAL `fnd` 1.2 の追加 API です。カーネルの `proc_fork` を呼び、親に子の PROCESS ハンドル、子に 0、失敗時に負のエラーを返します。VM・単一スレッドなどの制約は [カーネル仕様 §9.6](spec_Foundation.md) を参照してください。ハンドルが指すファイル位置・fs セッション・cwd などのサービス側状態は親子で共有します。 +環境変数は `WAL fnd 1.3` で加わりました。`fnd_envc()` と `fnd_env(i)` が、起動ブロック([Foundation §9.3](spec_Foundation.md)、version 2)の環境を返します。子へは `fnd_exec_t.envp` で渡し、`fork` した子は環境をメモリごと引き継ぎます。環境と引数は合わせて起動ブロックの 4096 bytes に収まらなければならず、環境は 128 件までです。 + `fnd_proc_exec` は次の手順で起動します。 -1. `path` を `EXEC` で開き(X 権限の検査)、全体を `fnd_mem_alloc` の領域に読み込んで閉じる。大きさの上限は `FND_EXEC_IMAGE_MAX`(1 MiB)で、超えれば `FND_E_LIMIT`。 +1. `path` を `EXEC` で開き(X 権限の検査)、全体を `fnd_mem_alloc` の領域に読み込んで閉じる。大きさの上限は `FND_EXEC_IMAGE_MAX`(1.5 MiB。カーネルの `FND_SPAWN_IMAGE_MAX` も同じ)で、超えれば `FND_E_LIMIT`。 2. `FS_SESSION_NEW` で子のための fs セッションを作る。 3. 起動ブロックを作る。cwd は `ex->cwd`、なければ `fnd_cwd_get` の値。役割表は 1 `STDIN`、2 `STDOUT`、3 `STDERR`、4 `FS`、5 `CONSOLE` で、`handles` にこの順に並べる(CONSOLE の役割がなければ 5 は省く)。 4. イメージが依存を持つ WAX v3 なら、依存する WAL を集める([Foundation §9.5](spec_Foundation.md))。依存表の各 `(library_id, abi_major)` について `/lib/..wal` を `EXEC` で開いて読み込み、その WAL の依存表も同じようにたどる。同じ ID と major は 1 回だけ読む。ファイルがなければ `FND_E_DEPENDENCY`、合計が WAX を含めて 8 個を超えれば `FND_E_LIMIT` で、起動しない。WAL の中身の検証と minor の確認はカーネルが行う。 @@ -695,7 +714,7 @@ init は、各サービスの起動後にシェルなどのアプリを起動す | UID の認証 | v1 は全セッション UID 0。カーネル ABI に資格情報を加えてから、`FS_SESSION_NEW` で UID を下げる操作などを定める | | `DEV_OPEN` の呼び出し元の確認 | v1 では確認できない(§9)。カーネル ABI でサービスにケーパビリティ付きの CHANNEL を渡す方法を決めてから閉じる | | 時刻の取得 | FS2 §7 の「一貫した時計のスナップショット」が未定義。libfnd の `fnd_time_now` はそれを待つ | -| 大きいイメージの起動 | `FND_EXEC_IMAGE_MAX` は 1 MiB。親のスロットより大きい子は、fs がカーネルへ直接渡す方式(将来)まで起動できない | +| 大きいイメージの起動 | `FND_EXEC_IMAGE_MAX` は 1.5 MiB。親のスロットより大きい子は、fs がカーネルへ直接渡す方式(将来)まで起動できない | | WAL の一覧 | **決定(2026-10-22)**:`/lib/..wal` に置き、`fnd_proc_exec` が依存をたどって読み、`proc_spawn` の `libs` で渡す(§11.6、[Foundation §9.2・§9.5](spec_Foundation.md)) | | sol のライブラリ | `sol/foundation/libfnd.sol` と `tools/solc/build_foundation.py` でC操作を利用可能。`examples/foundation-sol` は実FS2で5001 bytesのwrite/read/close、65件の列挙、rewind、ENDと資源解放、子の起動/待機を検証する | | 64-bit 整数 | `tools/llvm-src32` での `int64_t` の対応を実装時に確認する。使えなければ、libfnd の 64-bit 引数を上位・下位の組に置き換える | diff --git a/doc/spec_pkcl.md b/doc/spec_pkcl.md index b40fa8f..323d50a 100644 --- a/doc/spec_pkcl.md +++ b/doc/spec_pkcl.md @@ -58,7 +58,7 @@ python tools/llvm-src32/driver.py app.c foundation/lib/pkcl/*.c \ ### 3.3 起動 -`--entry pkcl_start` を指定すると、`crt.c` の `pkcl_start` が `fnd_init`、`pkcl_init`(fd 0〜2 の準備)、argv の組み立てを行い、`int main(int argc, char **argv)` を呼んで、その戻り値で `fnd_exit` します。`environ` は現状空です(§7 G2)。`pkcl_init` は POSIX 呼び出しの先頭でも遅延して呼ばれるので、`main(void)` と `fnd_init` の既存形式のアプリでも pkcl の関数を使えます。 +`--entry pkcl_start` を指定すると、`crt.c` の `pkcl_start` が `fnd_init`、`pkcl_init`(fd 0〜2 の準備)、argv の組み立てを行い、`int main(int argc, char **argv)` を呼んで、その戻り値で `fnd_exit` します。`environ` は起動ブロック version 2 の環境から作ります(なければ空)。`pkcl_init` は POSIX 呼び出しの先頭でも遅延して呼ばれるので、`main(void)` と `fnd_init` の既存形式のアプリでも pkcl の関数を使えます。 ## 4. 型と ABI の方針 @@ -114,7 +114,8 @@ Foundation の `fnd_status`(負値)を `pkcl_errno` で errno に直しま | `isatty` | `ST_STAT` の type = `CONSOLE` | 実装済み | | `fstat` | `ST_STAT` | 実装済み | | `pread`、`pwrite`、`readv`、`writev` | `lseek` + read/write で代用できるが原子的ではない | 予定 | -| `pipe`、`pipe2`、`mkfifo` | ユーザー空間の `pipe` サービス(IPC +ハンドル転送。カーネル変更なし)。§7 G3 | 予定 | +| `pipe`、`pipe2` | ユーザー空間の `pipe` サービス(API §9.5)。`read` は 1 回に 4096 bytes まで返す。端は `S_IFIFO` | 実装済み | +| `mkfifo` | 名前付き pipe は持たない | `ENOSYS` | | `select`、`poll` | コンソールは `CON_READ_KEY(NOWAIT)`、ファイルは常に ready、pipe はサービスの op | 予定 | | `mmap`、`munmap` | 無い。`MAP_PRIVATE` ファイル読み込みだけは read で模倣できるが、BusyBox は不要 | 不可 | | `fcntl(F_SETLK)`、`flock` | ロックなし | 不可(`ENOSYS`) | @@ -151,22 +152,23 @@ Foundation の `fnd_status`(負値)を `pkcl_errno` で errno に直しま ### 6.4 プロセス -Foundation のプロセスは `fnd_proc_exec`(= `proc_spawn`)で作る新しいイメージで、`fork` に相当する操作はありません(メモリのコピーも COW の仕組みもプロセスの複製にはない)。 +プロセスは `fnd_proc_exec`(= `proc_spawn`)で作る新しいイメージのほかに、VM バックエンドでは `proc_fork`(Foundation §9.6)で複製できます。`fork` は COW で、ハンドルは複製、ファイル位置は親子で共有です。生存スレッドが 1 本で、THREAD ハンドルと MMIO 付与がないことが条件です。slot バックエンドでは `fork` は `ENOSYS` です。 | 関数 | 方式 | 状態 | |---|---|---| | `_exit`、`exit` | `fnd_exit`(`exit` と `atexit` は libc) | `_exit` 実装済み | | `posix_spawn`、`posix_spawnp` | `fnd_proc_exec`。file actions(`dup2`/`close`/`open`)は子の `std_in/out/err` に渡すハンドルへ翻訳(0〜2 だけ)。`posix_spawnp` は `PATH` を `fnd_path_stat` で探す | 予定(M2) | -| `waitpid`、`wait` | pkcl の子プロセス表(pid → PROCESS ハンドル)と `fnd_proc_wait`。`WNOHANG` は `wait(h, 0, …)`(`WOULD_BLOCK`→0) | 予定(M2) | +| `waitpid`、`wait` | pkcl の子プロセス表(32 件、pid → PROCESS ハンドル)と `wait`。`WNOHANG` は `wait(h, 0, …)`(`WOULD_BLOCK`→0)。終了コードは `WEXITSTATUS`、カーネルが殺したときは `SIGKILL`、フォールトは `SIGSEGV` | 実装済み | | `execve`、`execv`、`execvp` | **代理 exec**:子を spawn して待ち、その終了コードで `_exit`。fd の継承は 0〜2 のみ。自分のハンドル(fd 3 以上)は渡らない | 予定(M2) | | `system`、`popen` | `posix_spawn("/bin/sh")` + `waitpid`(`popen` は pipe 後) | 予定 | -| `fork`、`vfork` | **不可**(`ENOSYS`)。BusyBox は NOMMU 構成と `posix_spawn` への置き換えで回避する(§8.3) | 不可 | -| `getpid`、`getppid` | カーネルに「自分の pid」を返す手段がない。§7 G1 | 要変更 | -| `kill`、`signal`、`sigaction`、`raise` | 非同期シグナルはない。§6.5 | 予定(M4) | +| `fork`、`vfork` | `proc_fork`(VM のみ)。`vfork` は `fork` と同じ(親の停止はしない)。子は pkcl の子プロセス表を空にして始まる | 実装済み(VM) | +| `getpid`、`getppid` | `sys_info` sel 4(自分の pid と parent)。PROCESS ハンドルの pid は sel 5。`fork` の戻りの pid は sel 5 で得る | 実装済み | +| `kill`、`signal`、`sigaction`、`raise` | `kill(子の pid, SIGKILL/SIGTERM)` は `proc_kill`、それ以外のシグナルの配送はない。§6.5 | `kill` は実装済み、他は予定(M4) | | `sleep`、`nanosleep`、`usleep` | `sleep`(tick は 10 ms 単位に切り上げ) | 予定(M4) | | `getuid` 系 | 常に 0 | 実装済み | | `setuid` 系、`chroot` | | 不可(`EPERM`/`ENOSYS`) | -| `getenv`、`setenv` | libc。環境の継承は §7 G2 | 要変更 | +| `getenv`、`setenv` | libc。環境は起動ブロック version 2 から `environ` に作り、`execve` は `envp` を渡す。`fork` の子はメモリごと引き継ぐ | 実装済み | +| `pipe`、`pipe2` | pipe サービス(API §9.5)。`fork`(VM)で端を共有できる | 実装済み | ### 6.5 シグナル @@ -199,17 +201,17 @@ Foundation のプロセスは `fnd_proc_exec`(= `proc_spawn`)で作る新し ## 7. Foundation 側のギャップ(pkcl が待つもの) -pkcl の方針は「少しの変更なら入れる」です。次は、それぞれ独立した小さな変更の候補です。**この PR では実装しません**(Issue にします)。 +pkcl の方針は「少しの変更なら入れる」です。次は、それぞれ独立した小さな変更の候補です。G1・G2・G3・G7 は実装済みで、残りは Issue にします。 | ID | 欠けているもの | 提案する最小の変更 | 影響する POSIX | |---|---|---|---| -| G1 | 自分の pid を知る手段 | `sys_info` に sel を 1 つ足す(自プロセスの pid と parent) | `getpid`、`getppid`、`kill(getpid())` | -| G2 | 環境変数の受け渡し | **カーネルは起動ブロックを解釈しない**(Foundation §9.3)ので、libfnd の規約として起動ブロック version 2 に `env_off`/`env_count` を足す。`fnd_exec_t` に `envp` を足す | `environ`、`getenv`、`execve` の環境 | -| G3 | パイプ | ユーザー空間の `pipe` サービス(`svc_register`、`FS_OPEN` 風の op で 2 本のハンドルを `XFER_HANDLE` で返す)。カーネル変更なし | `pipe`、`|`、`popen`、`mkfifo` | +| G1(済) | 自分の pid を知る手段 | `sys_info` に sel を 1 つ足す(自プロセスの pid と parent) | `getpid`、`getppid`、`kill(getpid())` | +| G2(済) | 環境変数の受け渡し | **カーネルは起動ブロックを解釈しない**(Foundation §9.3)ので、libfnd の規約として起動ブロック version 2 に `env_off`/`env_count` を足す。`fnd_exec_t` に `envp` を足す | `environ`、`getenv`、`execve` の環境 | +| G3(済) | パイプ | ユーザー空間の `pipe` サービス(`svc_register`、`FS_OPEN` 風の op で 2 本のハンドルを `XFER_HANDLE` で返す)。カーネル変更なし | `pipe`、`|`、`popen`、`mkfifo` | | G4 | inode 番号 | `fnd_stat` の予約欄(offset 60)に FS2 の inode を入れる(FS2 の inode 番号。実装時に FS2 仕様で確認する) | `st_ino`、`cp`/`mv` の同一ファイル判定 | | G5 | 時刻の更新 | `FS_UTIME` op(path、mtime、atime)。FS2 は mtime を持つ | `touch`、`cp -p`、`utimes` | | G6 | シンボリックリンク/ハードリンク | FS2 の拡張が必要(影響大)。当面入れない | `ln`、`readlink` | -| G7 | `setjmp`/`longjmp` | `tools/llvm-src32` の README に言及がなく、対応は未確認。ash が必須。未対応なら runtime のアセンブリ関数で実装する | ash、`libc` 全般 | +| G7(済) | `setjmp`/`longjmp` | バックエンドが呼び出しをインライン展開する(`tools/llvm-src32/backend.py`)。`sigsetjmp` は信号マスクを持たないので `setjmp` と同じ | ash、`libc` 全般 | | G8 | 64 bit のファイルオフセット | `off_t` を 64 bit にする(i64 は対応済み)。API の `fnd_seek` を libfnd に足す(現在は pkcl が `ST_SEEK` を直接呼ぶ) | 2 GiB 超のファイル | | G9 | `ST_READ` のノンブロッキング | コンソールはあるが、pipe と通常のストリームにない | `O_NONBLOCK`、`poll` | @@ -228,16 +230,16 @@ BusyBox は GPL-2.0 なので、**リポジトリには含めません**。Newli | ファイル I/O、`stat`、`mkdir` 等 | 実装済み(§6.1〜6.3) | | `DIR`(`ls`、`cp -r`、`rm -r`) | 実装済み(`posix/dirent.c`) | | libc:`stdio`(`printf` 系、`fopen`、バッファ)、`strtol`、`qsort`、`getopt`、`regcomp`(`grep`、`sed`)、`locale` の最小 | 実装済み(Newlib の posix プロファイル、§9) | -| `setjmp`/`longjmp`(ash が必須) | 未対応。`posix/setjmp.c` は `setjmp` が 0 を返し `longjmp` は終了する仮実装(G7) | -| `posix_spawn`、`waitpid`(外部コマンド、`xargs`、`find -exec`) | `execve` 系は「子を起動して終了を待ち、終了コードで自分も終わる」代理実装。`vfork`/`waitpid` は失敗(M2 の残り) | -| `pipe`(シェルのパイプ、`popen`) | `ENOSYS`(G3、M4) | +| `setjmp`/`longjmp`(ash が必須) | 実装済み。バックエンドがインライン展開する(G7) | +| `posix_spawn`、`waitpid`(外部コマンド、`xargs`、`find -exec`) | `waitpid`・`fork` は実装済み(VM)。`execve` 系は「子を起動して終了を待ち、終了コードで自分も終わる」代理実装。`posix_spawn` は予定 | +| `popen` | `pipe` + `posix_spawn`(予定) | | `termios`、`ioctl`、`signal`(対話シェル、`vi`、`top`) | termios/`TIOCGWINSZ` は console の LINE/RAW に対応。`sigaction` は登録を受け付けるだけで配送されない。`poll` は要求をそのまま準備済みとして返す(G9) | | `getpwnam` 等 | BusyBox の `USE_BB_PWD_GRP` で自前実装を使い、`/etc/passwd` を読む | | 浮動小数点 | `llvm-src32` が SoftFloat で対応済み。BusyBox は `awk`、`dc` 以外で不要 | ### 8.3 `fork` を使わない構成 -BusyBox の NOMMU 構成(`CONFIG_NOMMU`)と NOFORK/NOEXEC applet(`FEATURE_PREFER_APPLETS`、`FEATURE_SH_NOFORK`、`FEATURE_SH_STANDALONE`)を使い、`fork` を呼ぶ経路を無くします。外部コマンドは `posix_spawn("/bin/busybox", argv…)` の再実行(`BB_FORCE_APPLETS`風)で起動します。**パイプラインは複数プロセスを並行して動かし、pipe サービス越しにつなぐ**ことで実現します。ash の NOMMU サポートは「`vfork` + exec」を前提としているため、`vfork` が使えない場合に ash を使えるかは M5 の調査項目です(hush は NOMMU でもより素直)。 +`proc_fork` が使える VM では BusyBox を MMU 構成(`fork` を使う)で組めます。`proc_exec` に相当する操作がないので、`execve` は代理 exec(子を作って待つ)のままで、`FEATURE_SH_STANDALONE` と NOEXEC/NOFORK applet が `execve` の回数を減らします。slot バックエンド向けには、BusyBox の NOMMU 構成と NOFORK/NOEXEC applet(`FEATURE_PREFER_APPLETS`、`FEATURE_SH_NOFORK`、`FEATURE_SH_STANDALONE`)を使い、`fork` を呼ぶ経路を無くします。外部コマンドは `posix_spawn("/bin/busybox", argv…)` の再実行(`BB_FORCE_APPLETS`風)で起動します。**パイプラインは複数プロセスを並行して動かし、pipe サービス越しにつなぐ**ことで実現します。ash の NOMMU サポートは「`vfork` + exec」を前提としているため、`vfork` が使えない場合に ash を使えるかは M5 の調査項目です(hush は NOMMU でもより素直)。 ### 8.4 ビルド @@ -252,9 +254,11 @@ python3 tools/pkcl/busybox.py --busybox-source /path/to/busybox-1.36.1 \ 2. 各翻訳単位を `driver.py -c` で LLVM IR にする(`-Os`、`gnu11`)。`-D__clang_major__=0` で Clang 向けのインラインアセンブリの分岐を避け、`-DBB_GLOBAL_CONST=` で `ptr_to_globals` を rodata に置かせない(U モードで書込みフォルトになる)。バリアの空 asm は IR から消す。 3. 全部を一度にリンクして `busybox.wax`(WAX v3、`fnd` WAL を import)を作る。 -`/bin/busybox.wax` を置いて `busybox echo hi` のように呼びます(シンボリックリンクがないので applet 名のファイルは作らない。必要なら小さな WAX か shell 側のディスパッチ)。WAX は 1 MiB の `FND_EXEC_IMAGE_MAX` に収まる必要があり、シェルのヒープ(スロット 4 は 2 MiB)に丸ごと読み込まれるので、**サイズが applet の数を決める**。現在の構成は 1.04 MB です。サイズの内訳と抑えている手段は §9 を参照。 +`/bin/busybox.wax` を置いて `busybox echo hi` のように呼びます(シンボリックリンクがないので applet 名のファイルは作らない。必要なら小さな WAX か shell 側のディスパッチ)。WAX は 1.5 MiB の `FND_EXEC_IMAGE_MAX`(カーネルの `FND_SPAWN_IMAGE_MAX` も同じ。staging は 3 MiB)に収まる必要があり、シェルのヒープに丸ごと読み込まれるので、**サイズが applet の数を決める**。NOMMU 構成は 1.04 MB、ash を足した MMU 構成は 1.39 MB です。サイズの内訳と抑えている手段は §9 を参照。 -**設定が選ぶもの**:`CONFIG_NOMMU=y`、`CONFIG_LFS=n`(`off_t` は 32 bit)、`CONFIG_STATIC=y`、シェル(ash/hush)なし。applet は `cat echo true false pwd basename dirname head wc mkdir rmdir rm cp mv ls sleep test printf`。applet を足すにはサイズを確認すること(`tools/pkcl/busybox.py` のあとで `busybox.wax` が 1 MiB 未満であること。テストも確認する)。 +**設定が選ぶもの**:`CONFIG_NOMMU=y`、`CONFIG_LFS=n`(`off_t` は 32 bit)、`CONFIG_STATIC=y`、シェル(ash/hush)なし。applet は `cat echo true false pwd basename dirname head wc mkdir rmdir rm cp mv ls sleep test printf`。applet を足すにはサイズを確認すること(`busybox.wax` が 1.5 MiB 未満であること。テストも確認する)。 + +**MMU 構成と ash**:`--config tools/pkcl/busybox.config --config tools/pkcl/busybox_ash.config` のように重ねると、`CONFIG_NOMMU=n` で ash が入ります(VM バックエンドが必要)。ash は `fork` でサブシェルとパイプラインを作り、applet は `CONFIG_BUSYBOX_EXEC_PATH` の `/bin/busybox` を `execve` し直して動かします(`/bin/busybox.wax` と同じ内容を拡張子なしでも置く)。このとき applet 名を argv[0] に渡す必要があるので、pkcl の `execve` は `FND_EXEC_KEEP_ARGV0` を使います。スクリプト(`busybox sh /a.sh`)、`|`、`()`、リダイレクト、`export`、対話の `busybox sh` を確認済みです。ash 固有の補い:`fcntl(F_DUPFD_CLOEXEC)`(スクリプトの読み込み)、`times`、`WCOREDUMP`。 ## 9. libc の方針 @@ -284,9 +288,9 @@ pkcl のヘッダは libc なしの `pkcl-nolibc`(`errno`、`fcntl`、`unistd` | M0 | 設計、ヘッダ、fd I/O、パス操作、errno、起動コード、ゲストテスト | 完了 | | M1 | ディレクトリ(`DIR`)、`O_DIRECTORY`、`statvfs`、`chown`、`dup3`、`pread`/`pwrite` | `DIR`・`chown` 縮退は完了。残りは予定 | | M2 | libc 統合(Newlib の posix プロファイル)、代理 `execve` | 完了。`posix_spawn`・`waitpid` は予定 | -| M3 | 環境変数(G2)、`getpid`(G1)、時刻、`sleep` | 時刻・`sleep` は完了。G1・G2 は予定(`getpid` は 1 を返す仮実装) | -| M4 | pipe サービス(G3)、`poll`、シグナル、termios | termios は完了。他は予定 | -| M5 | BusyBox のビルド統合(`--busybox-source`)、最初の applet、ash/hush の調査、`setjmp`(G7) | ビルド統合と最初の applet は完了。ash/hush と `setjmp` は予定 | +| M3 | 環境変数(G2)、`getpid`(G1)、時刻、`sleep` | 完了 | +| M4 | pipe サービス(G3)、`poll`、シグナル、termios | pipe・termios は完了。`poll` の実装、シグナルの配送は予定 | +| M5 | BusyBox のビルド統合(`--busybox-source`)、最初の applet、ash/hush の調査、`setjmp`(G7) | ビルド統合と最初の applet は完了。`setjmp` は完了。ash/hush は予定 | | M6 | 64 bit オフセット(G8)、`utime`(G5)、inode(G4) | 予定 | 各 Issue は GitHub Issues にあります。GitHub のマイルストーンは分野の分類(Foundation OS)で、上の M は Issue タイトルの接頭辞に書きます。 diff --git a/foundation/README.md b/foundation/README.md index a6efca0..9e946fd 100644 --- a/foundation/README.md +++ b/foundation/README.md @@ -90,7 +90,7 @@ IRQ の focused test は `python -m unittest discover -s foundation/tests/irq -v | `kernel/blk.c` | `blk_io`(§10.4)。IDC と DMAC ch0、IDC 割り込み(IRQ2)で要求を 1 つずつ FIFO で処理 | | `kernel/fb.c`, `include/foundation/fb.h` | カーネル `/dev/fb0` ドライバとC helper(API §9.1)。ビットマップとCLUT | | `kernel/fndboot.c` | FNDBOOT のモジュール表の検査と、同梱モジュールの起動(§11) | -| `include/foundation/api.h`, `fnd.h`, `lib/fnd/` | libfnd([API 仕様 §11](../doc/spec_Foundation_API.md))のうち、今あるもの。WAL「fnd」1.2(`/lib/fnd.1.wal`)としてビルドし、export は `lib/fnd/fnd.exports`。1.1はディレクトリ列挙・作成/削除・file delete/sync、1.2は `fnd_proc_fork` を追加 | +| `include/foundation/api.h`, `fnd.h`, `lib/fnd/` | libfnd([API 仕様 §11](../doc/spec_Foundation_API.md))のうち、今あるもの。WAL「fnd」1.3(`/lib/fnd.1.wal`)としてビルドし、export は `lib/fnd/fnd.exports`。1.1はディレクトリ列挙・作成/削除・file delete/sync、1.2は `fnd_proc_fork` を追加、1.3は環境変数(`fnd_envc`、`fnd_env`、`fnd_exec_t.envp`)を追加 | | `services/console/` | console サービス(API 仕様 §8、PCG と PeC キーボード) | | `services/disk/` | disk サービス(`blk_io` を fs に IPC で渡す。プロトコルは `disk.h`) | | `fs/` | fs サービス(FS2 と DSFS、VFS、API 仕様 §5〜§7 のプロトコル)。次の節 | @@ -334,8 +334,8 @@ FND_MM=nommu python -m unittest discover -s foundation/tests/shell # nommu( - `src32_testbench` には UART がありません。そのため、そこで `debug_write` に 1 byte 以上を渡したり `kernel_panic` が UART へ出力したりすると、カーネル内でバスエラーになります(`kernel_panic` は PCG へ先に描くので画面には残り、再入は黙って停止します)。エミュレーター本体では UART に出力されます。 - RAM 容量の取得方法(§4.1)はまだ決めていません。nommu のページプールは [0x400000, RAM 末尾)、最大 256 MiB です。 - 仮想記憶では、権限はページ単位です。Foundation のイメージは `--page-align` でコード(RX)とデータ(RW)を別ページに置くので W^X です(`.rodata` は RW データの中で、R 専用ではありません)。MMIO の付与はページ単位で、窓の検査は byte 単位に加えてページ範囲が届く先も検査しますが、同じページにある別のレジスタには触れます([仕様 §5.5](../doc/spec_Foundation.md))。 -- プロセスは(仮想記憶でも nommu でも)7 個、スレッドは 8 個までです。スロットにもアリーナにも縛られないので、`FND_MAX_PROCS` を増やせます。 -- スレッドは全体で `FND_MAX_THREADS`(8)個です。IRQ の割り当ては PORT の最後のクローズまたは所有プロセスの終了で解放されます。 +- プロセスは(仮想記憶でも nommu でも)10 個、スレッドは 16 個までです。スロットにもアリーナにも縛られないので、`FND_MAX_PROCS` を増やせます。 +- スレッドは全体で `FND_MAX_THREADS`(16)個です。IRQ の割り当ては PORT の最後のクローズまたは所有プロセスの終了で解放されます。 - `--protected` の C プログラムでは、アセンブラが `.ORG` で分けたブロックのうち、同じ権限で並ぶものの隙間(整列の埋め草)をゼロで埋めて 1 つにします(PROTECTED WAX は 7 ブロックまで)。ゼロ初期化のグローバルもイメージに含まれるので、シェルのイメージは約 230 KiB です。 - WAX の読み込み中は割り込み禁止です。`tests/wax` の起動 11 回(うち 5 回は失敗)は、-O0 の Scalar で約 16M サイクル(1 回あたり約 30 tick 分)かかります。-O2 では約 10M サイクルです。 - timer の ACK から IRQC の W1C までの間に次の tick が来ると、IRQC の edge が失われます。カーネルは W1C の後に timer の pending をもう一度確認して、取りこぼしを防いでいます。 diff --git a/foundation/boot/build.py b/foundation/boot/build.py index f083b08..30356d5 100644 --- a/foundation/boot/build.py +++ b/foundation/boot/build.py @@ -11,10 +11,11 @@ console the console service (services/console) disk the disk service, blk_io for fs (services/disk) fs the fs service (fs/) + pipe the pipe service (services/pipe) shell /bin/shell.wax (doc/spec_Foundation_shell.md) /bin/* the standard commands (bin/, §10.3 of the shell spec) -libfnd is the WAL "fnd" 1.2 (/lib/fnd.1.wal, exports in lib/fnd/fnd.exports), +libfnd is the WAL "fnd" 1.3 (/lib/fnd.1.wal, exports in lib/fnd/fnd.exports), and the shell and the commands are WAX v3 that import it; fnd_proc_exec reads it from /lib when it starts them (§9.5). init and the services are FNDBOOT modules (§11) with libfnd linked in, since the kernel starts them @@ -187,7 +188,7 @@ def main(argv=None): # Every image below is an independent driver run, except that /bin programs import the fnd WAL. fnd_job = pool.submit(driver, [*LIBFND, LIBFND_PROC, LIBFND_RENAME], root / "lib/fnd.1.wal", args, "--wal", "--page-align", "--image-id", "fnd", - "--abi-version", "1.2", "--exports", FOUNDATION / "lib/fnd/fnd.exports") + "--abi-version", "1.3", "--exports", FOUNDATION / "lib/fnd/fnd.exports") # /lib/lzma.1.wal: the LZMA SDK adapter (lib/lzma, WAL "lzma" 1.0). Self-contained: it needs # no libfnd and no heap, so it adds nothing to the fs heap; programs that import it pass an arena. lzma_spec = importlib.util.spec_from_file_location("lzma_build", LZMA_DIR / "build.py") @@ -230,6 +231,8 @@ def main(argv=None): image=pool.submit(static, [*FS, FOUNDATION / "lib/fnd/name.c"], out / "fs.wax", args, f"-DFS2_FULL_MOUNT_CHECK={FS2_FULL_MOUNT_CHECK}", "-I", str(FOUNDATION / "fs"), stack=FS_STACK)), + dict(name="pipe", priority=FND_PRIO_SERVICE, caps=FND_CAP_SVC, + image=pool.submit(static, [services / "pipe/main.c"], out / "pipe.wax", args)), dict(name="init", priority=FND_PRIO_SERVICE, caps=0, image=pool.submit(static, [services / "init/main.c", LIBFND_PROC], out / "init.wax", args)), ] diff --git a/foundation/include/foundation/api.h b/foundation/include/foundation/api.h index c227828..3558c14 100644 --- a/foundation/include/foundation/api.h +++ b/foundation/include/foundation/api.h @@ -59,6 +59,9 @@ #define FND_DIR_NEXT 0x0200u #define FND_DIR_REWIND 0x0201u +/* §9a: the pipe service. */ +#define FND_PIPE_OPEN 0x0400u + /* §9: device services. */ #define FND_DEV_OPEN 0x0300u @@ -68,6 +71,7 @@ #define FND_TYPE_DIR 2u #define FND_TYPE_DEVICE 3u #define FND_TYPE_CONSOLE 4u +#define FND_TYPE_PIPE 5u #define FND_STAT_READ_ONLY 1u #define FND_STAT_SEEKABLE 2u @@ -102,7 +106,7 @@ #define FND_CON_DISPLAY_8X16 3u /* 80x30, 8x16 */ /* §11.6 */ -#define FND_EXEC_IMAGE_MAX 0x100000u +#define FND_EXEC_IMAGE_MAX 0x180000u /* §11.1: the end of a listing or of the input. */ #define FND_END 1 diff --git a/foundation/include/foundation/fnd.h b/foundation/include/foundation/fnd.h index 9a94960..719666f 100644 --- a/foundation/include/foundation/fnd.h +++ b/foundation/include/foundation/fnd.h @@ -37,18 +37,22 @@ typedef struct { } fnd_dirent_t; /* §11.6 */ +#define FND_EXEC_KEEP_ARGV0 1u /* argv[0] is passed as given, not replaced by the path */ typedef struct { uint32_t argc; const char *const *argv; /* argv[0] included */ const char *cwd; /* NULL: the current cwd */ fnd_handle std_in, std_out, std_err; /* 0: pass our own */ - uint32_t flags; /* reserved, 0 */ + uint32_t flags; /* FND_EXEC_* */ + const char *const *envp; /* NULL-terminated "NAME=value" strings; NULL: an empty environment */ } fnd_exec_t; /* §11.2 */ fnd_status fnd_init(void); uint32_t fnd_argc(void); const char *fnd_argv(uint32_t i); +uint32_t fnd_envc(void); +const char *fnd_env(uint32_t i); /* "NAME=value" */ fnd_handle fnd_std_in(void); fnd_handle fnd_std_out(void); fnd_handle fnd_std_err(void); diff --git a/foundation/include/foundation/ipc.h b/foundation/include/foundation/ipc.h index 5ac6ac8..ed32f70 100644 --- a/foundation/include/foundation/ipc.h +++ b/foundation/include/foundation/ipc.h @@ -67,16 +67,17 @@ typedef struct fnd_spawn_libs { #define FND_SPAWN_MAX_HANDLES 16u #define FND_STARTUP_MAX 4096u /* Largest image proc_spawn copies to its staging area. */ -#define FND_SPAWN_IMAGE_MAX 0x100000u +#define FND_SPAWN_IMAGE_MAX 0x180000u /* §9.3 startup block header. */ #define FND_STARTUP_MAGIC 0x464E4453u /* FNDS */ -#define FND_STARTUP_VERSION 1u +#define FND_STARTUP_VERSION 2u /* 1: without env_count and env_off (a reader treats them as 0) */ typedef struct fnd_startup { uint32_t magic, version, size; uint32_t argc, argv_off; uint32_t cwd_off; uint32_t role_count, roles_off; /* (role, handle) pairs */ + uint32_t env_count, env_off; /* version 2: "NAME=value" strings, like argv */ } fnd_startup; /* §6: handle values are 1..63. */ diff --git a/foundation/include/foundation/kernel.h b/foundation/include/foundation/kernel.h index 56f6655..f7ca3c1 100644 --- a/foundation/include/foundation/kernel.h +++ b/foundation/include/foundation/kernel.h @@ -9,8 +9,8 @@ /* Supervisor-side interface of the kernel, for the image that links * it (boot code, demo, tests). User programs use . */ -#define FND_MAX_PROCS 7u /* process records; no longer tied to memory slots */ -#define FND_MAX_THREADS 8u /* user threads; the idle thread is extra */ +#define FND_MAX_PROCS 10u /* process records; no longer tied to memory slots */ +#define FND_MAX_THREADS 16u /* user threads; the idle thread is extra */ #define FND_REGIONS 8u /* Extension P regions: the nommu backend's limit */ /* The regions a process record holds: an image's (at most 8) and the MMIO * grants (at most FND_MAX_GRANTS). The nommu backend keeps all in the 8 CPU diff --git a/foundation/include/pkcl-nolibc/fcntl.h b/foundation/include/pkcl-nolibc/fcntl.h index aea3b0b..b2457a1 100644 --- a/foundation/include/pkcl-nolibc/fcntl.h +++ b/foundation/include/pkcl-nolibc/fcntl.h @@ -16,6 +16,7 @@ #define O_DIRECTORY 0x200000 #define F_DUPFD 0 +#define F_DUPFD_CLOEXEC 14 #define F_GETFD 1 #define F_SETFD 2 #define F_GETFL 3 diff --git a/foundation/include/pkcl-nolibc/sys/stat.h b/foundation/include/pkcl-nolibc/sys/stat.h index d77d29d..89e787c 100644 --- a/foundation/include/pkcl-nolibc/sys/stat.h +++ b/foundation/include/pkcl-nolibc/sys/stat.h @@ -6,11 +6,12 @@ #define S_IFDIR 0040000 #define S_IFCHR 0020000 #define S_IFREG 0100000 +#define S_IFIFO 0010000 #define S_ISDIR(m) (((m) & S_IFMT) == S_IFDIR) #define S_ISCHR(m) (((m) & S_IFMT) == S_IFCHR) #define S_ISREG(m) (((m) & S_IFMT) == S_IFREG) #define S_ISBLK(m) 0 -#define S_ISFIFO(m) 0 +#define S_ISFIFO(m) (((m) & S_IFMT) == S_IFIFO) #define S_ISLNK(m) 0 #define S_ISSOCK(m) 0 diff --git a/foundation/include/pkcl-posix/setjmp.h b/foundation/include/pkcl-posix/setjmp.h new file mode 100644 index 0000000..e50ee34 --- /dev/null +++ b/foundation/include/pkcl-posix/setjmp.h @@ -0,0 +1,10 @@ +#ifndef PKCL_SETJMP_H +#define PKCL_SETJMP_H +#include_next +/* シグナルマスクは保存しない(pkcl はシグナルを配送しない)ので、sig 付きは付かないものと同じ。 */ +#ifndef sigsetjmp +typedef jmp_buf sigjmp_buf; +#define sigsetjmp(env, savemask) setjmp(env) +#define siglongjmp(env, val) longjmp(env, val) +#endif +#endif diff --git a/foundation/include/pkcl-posix/sys/wait.h b/foundation/include/pkcl-posix/sys/wait.h new file mode 100644 index 0000000..436fc8e --- /dev/null +++ b/foundation/include/pkcl-posix/sys/wait.h @@ -0,0 +1,8 @@ +#ifndef PKCL_SYS_WAIT_H +#define PKCL_SYS_WAIT_H +#include_next +/* Foundation has no core dumps. */ +#ifndef WCOREDUMP +#define WCOREDUMP(status) 0 +#endif +#endif diff --git a/foundation/include/pkcl/pkcl.h b/foundation/include/pkcl/pkcl.h index 2494e7d..97ede33 100644 --- a/foundation/include/pkcl/pkcl.h +++ b/foundation/include/pkcl/pkcl.h @@ -19,6 +19,11 @@ int pkcl_fail(fnd_status status); /* fd が開いていれば Foundation のハンドル、そうでなければ 0(errno = EBADF)。 */ fnd_handle pkcl_handle(int fd); +/* 開いたハンドルを最小の空き記述子に据える。失敗なら -1(EMFILE)でハンドルはそのまま。 + * status は F_GETFL の値で、pipe の端なら PKCL_FD_PIPE を足す。 */ +#define PKCL_FD_PIPE 0x40000000u +int pkcl_fd_install(fnd_handle h, uint32_t status); + /* 最初の POSIX 呼び出しで自動的に呼ばれる。fnd_init と fd 0〜2 の準備をする。 */ void pkcl_init(void); diff --git a/foundation/kernel/fndboot.c b/foundation/kernel/fndboot.c index 3ca0d9f..a1d430c 100644 --- a/foundation/kernel/fndboot.c +++ b/foundation/kernel/fndboot.c @@ -52,7 +52,8 @@ int32_t fnd_boot_check(const void *fndboot) { int32_t fnd_boot_spawn(const void *fndboot, uint32_t index, const struct fnd_image *libs, uint32_t lib_count) { const uint8_t *boot = fndboot; const uint8_t *entry = entry_at(boot, index); + const char *name = (const char *)entry + 32; const struct fnd_grant *grants = word(entry + 20) ? (const void *)(boot + word(entry + 16)) : 0; - return fnd_proc_spawn_module((const char *)entry + 32, word(entry + 8), word(entry + 12), boot + word(entry), + return fnd_proc_spawn_module(name, word(entry + 8), word(entry + 12), boot + word(entry), word(entry + 4), libs, lib_count, grants, word(entry + 20)); } diff --git a/foundation/kernel/internal.h b/foundation/kernel/internal.h index 4f8e242..20e1f30 100644 --- a/foundation/kernel/internal.h +++ b/foundation/kernel/internal.h @@ -60,6 +60,7 @@ int32_t proc_spawn(struct fnd_proc *caller, uint32_t spawn); int32_t proc_startup(const struct fnd_proc *proc, uint32_t buf, uint32_t cap); int32_t proc_mem_grow(struct fnd_proc *proc, uint32_t delta); void proc_release(struct fnd_proc *proc); /* a PROCESS handle closed */ +int32_t ipc_process_handle_pid(struct fnd_proc *proc, uint32_t h); int32_t proc_sys_info(const struct fnd_proc *caller, uint32_t sel, uint32_t buf, uint32_t cap); /* ipc.c: handles (§6), IPC (§8) and services (§8.6). */ diff --git a/foundation/kernel/ipc.c b/foundation/kernel/ipc.c index ecb4e8d..c30475d 100644 --- a/foundation/kernel/ipc.c +++ b/foundation/kernel/ipc.c @@ -681,6 +681,12 @@ static struct fnd_proc *process_handle(struct fnd_proc *proc, uint32_t h) { return e && e->kind == FND_OBJ_PROCESS ? e->object : 0; } +/* sys_info sel 5: the pid behind a PROCESS handle, or a negative error. */ +int32_t ipc_process_handle_pid(struct fnd_proc *proc, uint32_t h) { + struct fnd_proc *target = process_handle(proc, h); + return target ? (int32_t)target->pid : FND_E_BAD_HANDLE; +} + int32_t sys_wait(struct fnd_thread *thread, uint32_t h, uint32_t timeout, uint32_t code_ptr) { struct fnd_proc *target = process_handle(thread->proc, h); struct fnd_handle_entry *e = entry(thread->proc, h); diff --git a/foundation/kernel/proc.c b/foundation/kernel/proc.c index 38bf432..fac5a4b 100644 --- a/foundation/kernel/proc.c +++ b/foundation/kernel/proc.c @@ -704,11 +704,13 @@ int32_t proc_mem_grow(struct fnd_proc *proc, uint32_t delta) { } /* sys_info (§9): sel 0 is fnd_sysinfo, sel 1 the process list, sel 2 CPU - * accounting, sel 3 fnd_meminfo. Copies up to cap bytes and returns the + * accounting, sel 3 fnd_meminfo, sel 4 the caller's pid and parent, sel 5 the + * pid of a PROCESS handle (passed in buf). Copies up to cap bytes and returns the * whole answer's size. */ #define SYSINFO_BYTES 32u #define PROCINFO_BYTES 48u #define MEMINFO_BYTES 16u +#define SELFINFO_BYTES 8u static uint32_t proc_mem_bytes(const struct fnd_proc *proc) { if (mm_vm) @@ -728,6 +730,8 @@ static void put_word(const struct fnd_proc *proc, uint32_t buf, uint32_t cap, ui } int32_t proc_sys_info(const struct fnd_proc *caller, uint32_t sel, uint32_t buf, uint32_t cap) { + if (sel == 5) + return ipc_process_handle_pid((struct fnd_proc *)caller, buf); uint32_t live = 0, records = 0; for (uint32_t i = 0; i < FND_MAX_PROCS; ++i) { if (procs[i].state == FND_PROC_LIVE) { @@ -745,6 +749,8 @@ int32_t proc_sys_info(const struct fnd_proc *caller, uint32_t sel, uint32_t buf, size = 16u + records * 8u; else if (sel == 3) size = MEMINFO_BYTES; + else if (sel == 4) + size = SELFINFO_BYTES; else return FND_E_BAD_ARG; if (cap > size) @@ -768,6 +774,11 @@ int32_t proc_sys_info(const struct fnd_proc *caller, uint32_t sel, uint32_t buf, } return (int32_t)size; } + if (sel == 4) { + put_word(caller, buf, cap, 0, caller->pid); + put_word(caller, buf, cap, 4u, caller->parent); + return (int32_t)size; + } if (sel == 3) { uint32_t info[4] = {mm_ram_bytes(), bmc_vram_bytes(), bmc_pcmram_bytes(), 0}; for (uint32_t w = 0; w < 4u; ++w) diff --git a/foundation/lib/fnd/fnd.exports b/foundation/lib/fnd/fnd.exports index bfd0ad4..81eee3f 100644 --- a/foundation/lib/fnd/fnd.exports +++ b/foundation/lib/fnd/fnd.exports @@ -1,4 +1,4 @@ -# libfnd as the WAL "fnd" 1.2 (doc/spec_Foundation_API.md §11, +# libfnd as the WAL "fnd" 1.3 (doc/spec_Foundation_API.md §11, # doc/spec_WAX_v3.md §4.2). One export per line: name and call ABI 1 # signature, as tools/llvm-src32 --exports reads them. Keep in step with # ; changing a signature needs a new major version. @@ -20,6 +20,8 @@ fnd_dir_next fn(u32,ptr)->i32 fnd_dir_open fn(ptr,ptr)->i32 fnd_dir_remove fn(ptr)->i32 fnd_dir_rewind fn(u32)->i32 +fnd_env fn(u32)->ptr +fnd_envc fn()->u32 fnd_exit fn(i32)->void fnd_file_delete fn(ptr)->i32 fnd_file_open fn(ptr,u32,u32,ptr)->i32 diff --git a/foundation/lib/fnd/proc.c b/foundation/lib/fnd/proc.c index bb4d8c4..99e2aba 100644 --- a/foundation/lib/fnd/proc.c +++ b/foundation/lib/fnd/proc.c @@ -17,13 +17,17 @@ static int32_t build_startup(uint32_t *block, const fnd_exec_t *ex, const char * uint32_t role_count) { uint32_t argc = ex->argc ? ex->argc : 1u; uint32_t off = sizeof(fnd_startup); + uint32_t envc = 0; + while (ex->envp && ex->envp[envc]) + ++envc; uint32_t argv_off = off, roles_off = argv_off + 4u * argc; - off = roles_off + 8u * role_count; - if (argc > FND_SPAWN_MAX_HANDLES * 64u || off > FND_STARTUP_MAX) + uint32_t env_off = roles_off + 8u * role_count; + off = env_off + 4u * envc; + if (argc > FND_SPAWN_MAX_HANDLES * 64u || envc > 128u || off > FND_STARTUP_MAX) return FND_E_LIMIT; char *bytes = (char *)block; for (uint32_t i = 0; i < argc; ++i) { - const char *arg = i == 0 ? argv0 : ex->argv[i]; + const char *arg = i == 0 && !((ex->flags & FND_EXEC_KEEP_ARGV0) && ex->argv && ex->argv[0]) ? argv0 : ex->argv[i]; uint32_t n = str_len(arg) + 1u; if (n > FND_STARTUP_MAX - off) return FND_E_LIMIT; @@ -32,12 +36,21 @@ static int32_t build_startup(uint32_t *block, const fnd_exec_t *ex, const char * bytes[off + k] = arg[k]; off += n; } + for (uint32_t i = 0; i < envc; ++i) { + uint32_t len = str_len(ex->envp[i]) + 1u; + if (len > FND_STARTUP_MAX - off) + return FND_E_LIMIT; + block[env_off / 4u + i] = off; + for (uint32_t k = 0; k < len; ++k) + bytes[off + k] = ex->envp[i][k]; + off += len; + } uint32_t cwd_off = off, n = str_len(cwd) + 1u; if (n > FND_STARTUP_MAX - off) return FND_E_LIMIT; for (uint32_t k = 0; k < n; ++k) bytes[off + k] = cwd[k]; - off = (off + n + 3u) & ~3u; + off = (off + n + 7u) & ~7u; if (off > FND_STARTUP_MAX) return FND_E_LIMIT; for (uint32_t i = 0; i < role_count; ++i) { @@ -45,7 +58,7 @@ static int32_t build_startup(uint32_t *block, const fnd_exec_t *ex, const char * block[roles_off / 4u + 2u * i + 1u] = i + 1u; /* child handle 1 + index */ } *(fnd_startup *)block = - (fnd_startup){FND_STARTUP_MAGIC, FND_STARTUP_VERSION, off, argc, argv_off, cwd_off, role_count, roles_off}; + (fnd_startup){FND_STARTUP_MAGIC, FND_STARTUP_VERSION, off, argc, argv_off, cwd_off, role_count, roles_off, envc, env_off}; return (int32_t)off; } diff --git a/foundation/lib/fnd/start.c b/foundation/lib/fnd/start.c index 20ed172..6cd257a 100644 --- a/foundation/lib/fnd/start.c +++ b/foundation/lib/fnd/start.c @@ -5,10 +5,13 @@ #include #define ARGS_MAX 64u +#define ENV_MAX 128u static uint32_t *block; /* the startup block, on the heap */ static uint32_t argc; static const char *argv[ARGS_MAX]; +static const char *env[ENV_MAX]; +static uint32_t envc; static fnd_handle roles[FND_ROLE_CONSOLE + 1u]; /* CHANNEL は1..63。キャッシュは next の初回に確保し、close で解放する。 @@ -40,16 +43,27 @@ static const char *block_string(uint32_t off, uint32_t size) { * roles (§11.2). */ static const char *read_startup(void) { int32_t size = fnd_sys_proc_startup(0, 0); - if (size < (int32_t)sizeof(fnd_startup) || size > (int32_t)FND_STARTUP_MAX || + if (size < 32 || size > (int32_t)FND_STARTUP_MAX || !(block = fnd_mem_alloc((uint32_t)size)) || fnd_sys_proc_startup(block, (uint32_t)size) != size) return 0; const fnd_startup *h = (const fnd_startup *)block; uint32_t n = (uint32_t)size; - if (h->magic != FND_STARTUP_MAGIC || h->version != FND_STARTUP_VERSION || h->size != n || + uint32_t header = h->version == 1u ? 32u : sizeof(fnd_startup); /* version 1 stops at roles_off */ + if (size < (int32_t)header || h->magic != FND_STARTUP_MAGIC || (h->version != 1u && h->version != 2u) || + h->size != n || h->argc > ARGS_MAX || (h->argv_off & 3u) || h->argv_off > n || h->argc * 4u > n - h->argv_off || (h->roles_off & 3u) || h->roles_off > n || h->role_count * 8u > n - h->roles_off) return 0; + uint32_t env_count = h->version == 1u ? 0 : h->env_count, env_off = h->version == 1u ? 0 : h->env_off; + if (env_count > ENV_MAX || (env_off & 3u) || env_off > n || env_count * 4u > n - env_off) + return 0; const char *cwd = block_string(h->cwd_off, n); + for (uint32_t i = 0; i < env_count; ++i) { + env[i] = block_string(block[env_off / 4u + i], n); + if (!env[i]) + return 0; + } + envc = env_count; for (uint32_t i = 0; i < h->argc; ++i) { argv[i] = block_string(block[h->argv_off / 4u + i], n); if (!argv[i]) @@ -72,6 +86,8 @@ fnd_status fnd_init(void) { return FND_OK; } +uint32_t fnd_envc(void) { return envc; } +const char *fnd_env(uint32_t i) { return i < envc ? env[i] : 0; } uint32_t fnd_argc(void) { return argc; } const char *fnd_argv(uint32_t i) { return i < argc ? argv[i] : 0; } fnd_handle fnd_std_in(void) { return roles[FND_ROLE_STDIN]; } diff --git a/foundation/lib/pkcl/crt.c b/foundation/lib/pkcl/crt.c index 3c4ca0d..506d615 100644 --- a/foundation/lib/pkcl/crt.c +++ b/foundation/lib/pkcl/crt.c @@ -10,7 +10,7 @@ extern int main(int argc, char **argv); extern void exit(int code) __attribute__((noreturn)); #endif -/* 環境変数。起動ブロックにはまだ環境がない(doc/spec_pkcl.md §7 G2)ので空から始める。 */ +/* 環境変数。起動ブロック(version 2)の env から作る。なければ空。 */ static char *empty_environ[1]; char **environ = empty_environ; @@ -24,6 +24,16 @@ void pkcl_start(void) { for (uint32_t i = 0; i < argc; ++i) argv[i] = (char *)fnd_argv(i); /* 起動ブロックは書き換え可能なヒープ上にある */ argv[argc] = 0; + uint32_t envc = fnd_envc(); + if (envc) { + char **env = fnd_mem_alloc((envc + 1u) * sizeof(char *)); + if (!env) + fnd_exit(FND_E_NO_MEMORY); + for (uint32_t i = 0; i < envc; ++i) + env[i] = (char *)fnd_env(i); + env[envc] = 0; + environ = env; + } #ifdef PKCL_LIBC exit(main((int)argc, argv)); /* atexit と stdio の flush を libc に任せる */ #else diff --git a/foundation/lib/pkcl/errno.c b/foundation/lib/pkcl/errno.c index 89157a9..b1e30b8 100644 --- a/foundation/lib/pkcl/errno.c +++ b/foundation/lib/pkcl/errno.c @@ -55,12 +55,14 @@ int pkcl_errno(fnd_status status) { return ENOTEMPTY; case FND_E_NAME_TOO_LONG: return ENAMETOOLONG; + case FND_E_PEER_GONE: + return EPIPE; case FND_E_NO_SPACE: return ENOSPC; case FND_E_CROSS_DEVICE: return EXDEV; default: - return status >= 0 ? 0 : EIO; /* PEER_GONE, IO, NEEDS_FSCK と未知のコード */ + return status >= 0 ? 0 : EIO; /* IO, NEEDS_FSCK と未知のコード */ } } diff --git a/foundation/lib/pkcl/fd.c b/foundation/lib/pkcl/fd.c index 36a9fbe..c502299 100644 --- a/foundation/lib/pkcl/fd.c +++ b/foundation/lib/pkcl/fd.c @@ -64,6 +64,15 @@ static int fd_alloc(int from) { return -1; } +int pkcl_fd_install(fnd_handle h, uint32_t status) { + pkcl_init(); + int fd = fd_alloc(0); + if (fd < 0) + return -1; + fds[fd] = (struct fd_entry){h, status, 0}; + return fd; +} + uint32_t pkcl_mode_to_fnd(uint32_t m) { /* FS2: bit 0〜2 が other の X/W/R、bit 3〜5 が owner の X/W/R。 */ return ((m >> 6) & 7u) << 3 | (m & 7u); @@ -71,7 +80,8 @@ uint32_t pkcl_mode_to_fnd(uint32_t m) { uint32_t pkcl_mode_from_fnd(uint32_t type, uint32_t f) { uint32_t owner = (f >> 3) & 7u, other = f & 7u; - uint32_t kind = type == FND_TYPE_DIR ? S_IFDIR : type == FND_TYPE_FILE ? S_IFREG : S_IFCHR; + uint32_t kind = type == FND_TYPE_DIR ? S_IFDIR : type == FND_TYPE_FILE ? S_IFREG + : type == FND_TYPE_PIPE ? S_IFIFO : S_IFCHR; /* group は持たないので other と同じに見せる。 */ return kind | owner << 6 | other << 3 | other; } @@ -140,6 +150,9 @@ ssize_t read(int fd, void *buf, size_t len) { } if (len > 0x7FFFFFFFu) len = 0x7FFFFFFFu; + /* fnd_read は満杯の塊のあと次の塊を待つ。pipe は届いた分だけ返す(POSIX の短い read)。 */ + if ((fds[fd].status & PKCL_FD_PIPE) && len > FND_IPC_MAX_DATA) + len = FND_IPC_MAX_DATA; uint32_t got = 0; if (pkcl_fail(fnd_read(h, buf, (uint32_t)len, &got))) return -1; @@ -230,14 +243,15 @@ int fcntl(int fd, int cmd, ...) { int arg = va_arg(ap, int); va_end(ap); switch (cmd) { - case F_DUPFD: { + case F_DUPFD: + case F_DUPFD_CLOEXEC: { int nfd = fd_alloc(arg < 0 ? 0 : arg); if (nfd < 0) return -1; int32_t copy = fnd_sys_handle_dup(h); if (copy <= 0) return pkcl_fail(copy); - fds[nfd] = (struct fd_entry){(fnd_handle)copy, fds[fd].status, 0}; + fds[nfd] = (struct fd_entry){(fnd_handle)copy, fds[fd].status, cmd == F_DUPFD_CLOEXEC ? FD_CLOEXEC : 0}; return nfd; } case F_GETFD: @@ -246,7 +260,7 @@ int fcntl(int fd, int cmd, ...) { fds[fd].cloexec = (uint32_t)arg & FD_CLOEXEC; return 0; case F_GETFL: - return (int)fds[fd].status; + return (int)(fds[fd].status & ~PKCL_FD_PIPE); case F_SETFL: /* アクセスモードは変えられない。O_APPEND は開く時のストリームに固定なので、 * 後から変えても効かない(記録も更新しない)。O_NONBLOCK は記録だけする。 */ diff --git a/foundation/lib/pkcl/newlib/syscalls.c b/foundation/lib/pkcl/newlib/syscalls.c index e6df834..b5eb9fe 100644 --- a/foundation/lib/pkcl/newlib/syscalls.c +++ b/foundation/lib/pkcl/newlib/syscalls.c @@ -25,32 +25,21 @@ int _unlink(const char *path) { return unlink(path); } int _mkdir(const char *path, mode_t mode) { return mkdir(path, mode); } int _fcntl(int fd, int cmd, int arg) { return fcntl(fd, cmd, arg); } -/* FS2 にリンクはなく、pkcl はシグナルとプロセスの複製を持たない(§6.4、§6.5)。 */ -int _link(const char *old_path, const char *new_path) { - (void)old_path, (void)new_path; - errno = ENOSYS; - return -1; -} -int _kill(int pid, int sig) { - (void)pid, (void)sig; - errno = ENOSYS; - return -1; -} -int _fork(void) { - errno = ENOSYS; - return -1; -} -int _wait(int *status) { - (void)status; - errno = ECHILD; - return -1; -} -int _execve(const char *path, char *const argv[], char *const envp[]) { - (void)path, (void)argv, (void)envp; - errno = ENOSYS; - return -1; -} -int _getpid(void) { return 1; } /* §7 G1: 自分の pid を知る手段がまだない */ +/* pkcl-nolibc のヘッダには載っていない POSIX 関数(posix/ にある)。 */ +extern int link(const char *, const char *); +extern int kill(int, int); +extern int fork(void); +extern int wait(int *); +extern int execve(const char *, char *const[], char *const[]); +extern int getpid(void); + +/* FS2 にリンクはない。プロセスは pkcl の posix/process.c(fork は VM のカーネルが必要)。 */ +int _link(const char *old_path, const char *new_path) { return link(old_path, new_path); } +int _kill(int pid, int sig) { return kill(pid, sig); } +int _fork(void) { return fork(); } +int _wait(int *status) { return wait(status); } +int _execve(const char *path, char *const argv[], char *const envp[]) { return execve(path, argv, envp); } +int _getpid(void) { return getpid(); } /* カーネルの tick(10 ms)と RTC から。 */ int _gettimeofday(struct timeval *tv, void *tz) { diff --git a/foundation/lib/pkcl/posix/proc.c b/foundation/lib/pkcl/posix/proc.c index e8fa216..28e0f96 100644 --- a/foundation/lib/pkcl/posix/proc.c +++ b/foundation/lib/pkcl/posix/proc.c @@ -21,7 +21,6 @@ extern char **environ; * exec 族は「子を起動して終了を待ち、その終了コードで自分も終わる」で置き換える。 * 呼び出し元から見ると戻らない点が exec と同じで、違うのは pid とメモリが残ることだけ。 */ int execve(const char *path, char *const argv[], char *const envp[]) { - (void)envp; /* §7 G2: 環境ブロックはまだ渡せない */ pkcl_init(); uint32_t argc = 0; while (argv && argv[argc]) @@ -29,6 +28,8 @@ int execve(const char *path, char *const argv[], char *const envp[]) { fnd_exec_t ex = {0}; ex.argc = argc; ex.argv = (const char *const *)argv; + ex.envp = (const char *const *)envp; + ex.flags = FND_EXEC_KEEP_ARGV0; /* BusyBox picks the applet from argv[0] */ ex.std_in = pkcl_handle(0); ex.std_out = pkcl_handle(1); ex.std_err = pkcl_handle(2); @@ -67,24 +68,7 @@ int execvp(const char *file, char *const argv[]) { return -1; } -pid_t vfork(void) { - errno = ENOSYS; - return -1; -} -pid_t wait(int *status) { - (void)status; - errno = ECHILD; - return -1; -} -pid_t waitpid(pid_t pid, int *status, int options) { - (void)pid, (void)status, (void)options; - errno = ECHILD; - return -1; -} - -pid_t getpid(void) { return 1; } -pid_t getppid(void) { return 0; } -pid_t setsid(void) { return 1; } +pid_t setsid(void) { return getpid(); } int setuid(uid_t uid) { return uid == 0 ? 0 : (errno = EPERM, -1); } int setgid(gid_t gid) { return gid == 0 ? 0 : (errno = EPERM, -1); } int seteuid(uid_t uid) { return setuid(uid); } diff --git a/foundation/lib/pkcl/posix/process.c b/foundation/lib/pkcl/posix/process.c new file mode 100644 index 0000000..6d257d9 --- /dev/null +++ b/foundation/lib/pkcl/posix/process.c @@ -0,0 +1,156 @@ +/* fork・wait・kill・pid(doc/spec_pkcl.md §6.4、§7 G1)。 + * Foundation の proc_fork(Foundation §9.6)の上に作る。VM のカーネルだけが fork でき、slot では + * FND_E_UNSUPPORTED なので ENOSYS になる。子は PROCESS ハンドルで待つ。ハンドルの番号は親子で同じだが、 + * 子に渡されるのは親が持つ他のハンドルで、親の子のハンドルは渡されない。 */ +#define _GNU_SOURCE 1 +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#define CHILD_MAX 32 + +static struct { + pid_t pid; + fnd_handle handle; +} children[CHILD_MAX]; +static pid_t own_pid, own_parent; + +static void load_self(void) { + uint32_t info[2]; + if (own_pid) + return; + if (fnd_sys_info(4, info, sizeof info) == (int32_t)sizeof info) { + own_pid = (pid_t)info[0]; + own_parent = (pid_t)info[1]; + } else { + own_pid = 1; + } +} + +pid_t getpid(void) { + load_self(); + return own_pid; +} +pid_t getppid(void) { + load_self(); + return own_parent; +} + +pid_t fork(void) { + pkcl_init(); + int slot = -1; + for (int i = 0; i < CHILD_MAX; ++i) { + if (!children[i].pid) { + slot = i; + break; + } + } + if (slot < 0) { + errno = EAGAIN; + return -1; + } + int32_t result = fnd_proc_fork(); + if (result < 0) { + errno = pkcl_errno(result); + return -1; + } + if (result == 0) { /* 子: 親の子は自分の子ではない */ + memset(children, 0, sizeof children); + own_pid = 0; + return 0; + } + int32_t pid = fnd_sys_info(5, result, 0); + if (pid <= 0) { + fnd_proc_kill((fnd_handle)result); + fnd_sys_handle_close((fnd_handle)result); + errno = pkcl_errno(pid); + return -1; + } + children[slot].pid = (pid_t)pid; + children[slot].handle = (fnd_handle)result; + return (pid_t)pid; +} + +/* vfork は fork と同じ。親は止まらないが、BusyBox が子で行うのは exec か _exit だけ。 */ +pid_t vfork(void) { return fork(); } + +/* exit_code を wait の status に直す。終了コードは 0〜255、強制終了は SIGKILL、それ以外(フォールト)は SIGSEGV。 */ +static int wait_status(int32_t code) { + if (code >= 0 && code < 256) + return code << 8; + return code == FND_E_KILLED ? SIGKILL : SIGSEGV; +} + +pid_t waitpid(pid_t pid, int *status, int options) { + pkcl_init(); + for (;;) { + int candidates = 0, only = -1; + for (int i = 0; i < CHILD_MAX; ++i) { + if (children[i].pid && (pid <= 0 || children[i].pid == pid)) { + only = candidates++ ? -1 : i; + } + } + for (int i = 0; i < CHILD_MAX; ++i) { + if (!children[i].pid || (pid > 0 && children[i].pid != pid)) + continue; + int32_t code = 0; + /* 待つ相手が 1 つなら、カーネルの中で待つ(polling より速い) */ + int32_t r = fnd_sys_wait(children[i].handle, i == only && !(options & WNOHANG) ? FND_FOREVER : 0, &code); + if (r == FND_E_WOULD_BLOCK) + continue; + fnd_sys_handle_close(children[i].handle); + pid_t done = children[i].pid; + children[i].pid = 0; + if (r < 0) { + errno = pkcl_errno(r); + return -1; + } + if (status) + *status = wait_status(code); + return done; + } + if (!candidates) { + errno = ECHILD; + return -1; + } + if (options & WNOHANG) + return 0; + fnd_sys_sleep(1); /* 複数の子のどれかを待つカーネルの手段がないので、短い間隔で見る */ + } +} + +pid_t wait(int *status) { return waitpid(-1, status, 0); } + +int kill(pid_t pid, int sig) { + pkcl_init(); + if (sig < 0 || sig >= NSIG) { + errno = EINVAL; + return -1; + } + if (pid == getpid()) + return sig == 0 ? 0 : (errno = ENOSYS, -1); /* 自分宛のシグナルの配送はない(§6.5) */ + for (int i = 0; i < CHILD_MAX; ++i) { + if (children[i].pid == pid) { + if (sig != 0) + fnd_proc_kill(children[i].handle); + return 0; + } + } + errno = ESRCH; + return -1; +} + +/* CPU 時間は取れないので 0。戻り値は起動からの tick(10 ms 単位、sysconf(_SC_CLK_TCK) = tick_hz)。 */ +clock_t times(struct tms *buf) { + if (buf) { + buf->tms_utime = buf->tms_stime = buf->tms_cutime = buf->tms_cstime = 0; + } + return (clock_t)(uint32_t)fnd_sys_time_get(0); +} diff --git a/foundation/lib/pkcl/posix/setjmp.c b/foundation/lib/pkcl/posix/setjmp.c deleted file mode 100644 index bd4c52e..0000000 --- a/foundation/lib/pkcl/posix/setjmp.c +++ /dev/null @@ -1,14 +0,0 @@ -/* setjmp / longjmp はコンパイラ側の対応がまだない(doc/spec_pkcl.md §7 G7)。 - * setjmp は常に 0 を返し、longjmp は戻り先がないので終了する。 */ -#include -#include - -int setjmp(jmp_buf env) { - (void)env; - return 0; -} -void longjmp(jmp_buf env, int value) { - (void)env, (void)value; - fnd_con_write("pkcl: longjmp is not supported\n"); - fnd_exit(127); -} diff --git a/foundation/lib/pkcl/posix/term.c b/foundation/lib/pkcl/posix/term.c index 456f00d..1e1111d 100644 --- a/foundation/lib/pkcl/posix/term.c +++ b/foundation/lib/pkcl/posix/term.c @@ -1,13 +1,16 @@ /* 端末・ioctl・poll・pipe(doc/spec_pkcl.md §6.6)。端末は console の LINE / RAW モードに対応づける。 */ #define _GNU_SOURCE 1 #include +#include #include #include #include #include #include +#include #include +#include #include static int is_console(int fd) { @@ -114,8 +117,52 @@ int poll(struct pollfd *fds, nfds_t count, int timeout) { return ready; } -int pipe(int fds[2]) { - (void)fds; - errno = ENOSYS; /* §7 G3: pipe サービスがまだない */ - return -1; +/* 2 つの端は pipe サービスの CHANNEL 1 本ずつ。読み端を開いて id を受け取り、その id で書き端を付ける。 */ +static int pipe_open(uint32_t mode, uint32_t id, uint32_t *out_id) { + int32_t svc = fnd_sys_svc_lookup("pipe"); + if (svc <= 0) { + errno = ENOSYS; /* VM でない構成には pipe サービスがない */ + return -1; + } + uint32_t args[4] = {mode, id, 0, 0}; + fnd_msg rep; + fnd_status status = fnd_call((fnd_handle)svc, FND_PIPE_OPEN, args, 0, 0, 0, 0, &rep); + fnd_close((fnd_handle)svc); + if (pkcl_fail(status)) + return -1; + if (!(rep.flags & FND_MSG_XFER_HANDLE) || !rep.a3) { + errno = EIO; + return -1; + } + if (out_id) + *out_id = rep.a0; + return (int)rep.a3; } + +int pipe2(int fds[2], int flags) { + pkcl_init(); + uint32_t id = 0; + int32_t r = pipe_open(0, 0, &id); + if (r < 0) + return -1; + int32_t w = pipe_open(1, id, 0); + if (w < 0) { + fnd_close((fnd_handle)r); + return -1; + } + uint32_t nb = (uint32_t)flags & O_NONBLOCK; + int rfd = pkcl_fd_install((fnd_handle)r, O_RDONLY | PKCL_FD_PIPE | nb); + int wfd = rfd < 0 ? -1 : pkcl_fd_install((fnd_handle)w, O_WRONLY | PKCL_FD_PIPE | nb); + if (wfd < 0) { + fnd_close((fnd_handle)r); + fnd_close((fnd_handle)w); + if (rfd >= 0) + close(rfd); + return -1; + } + fds[0] = rfd; + fds[1] = wfd; + return 0; +} + +int pipe(int fds[2]) { return pipe2(fds, 0); } diff --git a/foundation/services/console/main.c b/foundation/services/console/main.c index f98a189..91d5c0b 100644 --- a/foundation/services/console/main.c +++ b/foundation/services/console/main.c @@ -53,10 +53,10 @@ static int ready_eof; static uint8_t data[FND_IPC_MAX_DATA]; static uint8_t out[FND_IPC_MAX_DATA]; -static void reply(uint32_t token, int32_t result, uint32_t a0, uint32_t a1, uint32_t a2, const void *buf, - uint32_t len) { +static int32_t reply(uint32_t token, int32_t result, uint32_t a0, uint32_t a1, uint32_t a2, const void *buf, + uint32_t len) { fnd_msg msg = {(uint32_t)result, 0, a0, a1, a2, 0, (uint32_t)(uintptr_t)buf, len}; - fnd_sys_ipc_reply(token, &msg); /* a client that is gone needs no answer */ + return fnd_sys_ipc_reply(token, &msg); /* a client that is gone needs no answer */ } static struct stream *find_stream(uint32_t badge) { @@ -152,6 +152,13 @@ static void line_key(const fnd_key_t *k) { } } +/* A key that was handed to a reader that no longer exists goes back to the front. */ +static void unget_key(const fnd_key_t *k) { + key_head = (key_head + KEYS - 1u) % KEYS; + keys[key_head] = *k; + ++key_count; +} + /* Gives queued keys to the waiting requests, oldest first. */ static void dispatch(void) { for (;;) { @@ -164,12 +171,14 @@ static void dispatch(void) { --key_count; struct pending *p = &pending[0]; if (p->op == FND_CON_READ_KEY) { - reply(p->token, FND_OK, k.code, k.ch, k.mods, 0, 0); + if (reply(p->token, FND_OK, k.code, k.ch, k.mods, 0, 0) != FND_OK) + unget_key(&k); /* the reader was killed before CLIENT_GONE reached us */ drop_pending(0); } else if (mode == FND_CON_RAW) { char buf[4]; if (k.code == FND_KEY_CHAR && !(k.mods & FND_MOD_CTRL)) { - reply(p->token, FND_OK, 0, 0, 0, buf, encode(k.ch, buf)); + if (reply(p->token, FND_OK, 0, 0, 0, buf, encode(k.ch, buf)) != FND_OK) + unget_key(&k); drop_pending(0); } } else { diff --git a/foundation/services/pipe/main.c b/foundation/services/pipe/main.c new file mode 100644 index 0000000..f5fb307 --- /dev/null +++ b/foundation/services/pipe/main.c @@ -0,0 +1,274 @@ +/* The pipe service (doc/spec_Foundation_API.md §9a): anonymous pipes behind the stream protocol. + * + * It registers as "pipe". A channel from svc_lookup only takes PIPE_OPEN, which makes a pipe (a0 = 0: + * replies the read end and a0 = the pipe id) or attaches the write end of an existing pipe (a0 = 1, + * a1 = id). An end is a channel of the service's port whose badge names it, so the ends survive + * fork (the handles are shared) and dup, and CLIENT_GONE (the last handle of an end closed) + * is what makes EOF and EPIPE. ST_READ and ST_WRITE wait in one queue and are answered when + * data or room arrives. A write is atomic: it waits until the whole request fits. */ +#include +#include + +#define PIPES 16u +#define PIPE_SIZE FND_IPC_MAX_DATA /* a write of the largest request can always fit in the end */ +#define PENDING 16u +#define LOOKUP_BADGE 0x80000000u + +struct pipe { + uint32_t id; /* 0: free */ + uint32_t read_badge, write_badge; /* 0: not open (never attached, or closed) */ + uint32_t head, count; + uint8_t buf[PIPE_SIZE]; +}; + +struct pending { + uint32_t token, badge; /* badge 0: free */ + uint32_t op; + uint32_t max; /* ST_READ: bytes asked for; ST_WRITE: bytes in wbuf */ + uint8_t wbuf[FND_IPC_MAX_DATA]; +}; + +static struct pipe pipes[PIPES]; +static struct pending pending[PENDING]; +static uint32_t next_id, next_badge; +static uint8_t data[FND_IPC_MAX_DATA]; +static uint8_t out[FND_IPC_MAX_DATA]; + +static void reply(uint32_t token, int32_t result, uint32_t a0, const void *buf, uint32_t len) { + fnd_msg msg = {(uint32_t)result, 0, a0, 0, 0, 0, (uint32_t)(uintptr_t)buf, len}; + fnd_sys_ipc_reply(token, &msg); /* a client that is gone needs no answer */ +} + +static struct pipe *by_badge(uint32_t badge, int *is_write) { + for (uint32_t i = 0; badge && i < PIPES; ++i) { + if (pipes[i].id && pipes[i].read_badge == badge) { + *is_write = 0; + return &pipes[i]; + } + if (pipes[i].id && pipes[i].write_badge == badge) { + *is_write = 1; + return &pipes[i]; + } + } + return 0; +} + +static uint32_t new_badge(void) { + /* §4.4: a badge is not reused right after it is freed. */ + next_badge = next_badge >= 0x7FFFFFFFu ? 1u : next_badge + 1u; + return next_badge; +} + +/* Answers every waiting request that the pipe's state now settles. */ +static void settle(struct pipe *p) { + for (uint32_t i = 0; i < PENDING; ++i) { + struct pending *q = &pending[i]; + if (!q->badge) + continue; + if (q->op == FND_ST_READ && q->badge == p->read_badge) { + if (p->count) { + uint32_t n = p->count < q->max ? p->count : q->max; + for (uint32_t k = 0; k < n; ++k) + out[k] = p->buf[(p->head + k) % PIPE_SIZE]; + p->head = (p->head + n) % PIPE_SIZE; + p->count -= n; + reply(q->token, FND_OK, 0, out, n); + q->badge = 0; + } else if (!p->write_badge) { /* EOF */ + reply(q->token, FND_OK, 0, 0, 0); + q->badge = 0; + } + } else if (q->op == FND_ST_WRITE && q->badge == p->write_badge) { + if (!p->read_badge) { + reply(q->token, FND_E_PEER_GONE, 0, 0, 0); + q->badge = 0; + } else if (p->count + q->max <= PIPE_SIZE) { + for (uint32_t k = 0; k < q->max; ++k) + p->buf[(p->head + p->count + k) % PIPE_SIZE] = q->wbuf[k]; + p->count += q->max; + reply(q->token, FND_OK, q->max, 0, 0); + q->badge = 0; + } + } + } +} + +static void settle_all(void) { + /* A read that was answered may make room for a write, and the reverse, so go around until quiet. */ + for (uint32_t round = 0; round < 2u; ++round) { + for (uint32_t i = 0; i < PIPES; ++i) { + if (pipes[i].id) + settle(&pipes[i]); + } + } +} + +static struct pending *queue(const fnd_recv *r, uint32_t op, uint32_t max) { + for (uint32_t i = 0; i < PENDING; ++i) { + if (!pending[i].badge) { + pending[i].token = r->token; + pending[i].badge = r->badge; + pending[i].op = op; + pending[i].max = max; + return &pending[i]; + } + } + return 0; +} + +/* PIPE_OPEN on a lookup channel; replies itself on success. */ +static int32_t pipe_open(fnd_handle port, const fnd_recv *r) { + uint32_t mode = r->msg.a0, id = r->msg.a1; + struct pipe *p = 0; + if (mode == 0) { + for (uint32_t i = 0; i < PIPES; ++i) { + if (!pipes[i].id) { + p = &pipes[i]; + break; + } + } + if (!p) + return FND_E_LIMIT; + next_id = next_id >= 0x7FFFFFFFu ? 1u : next_id + 1u; + *p = (struct pipe){0}; + p->id = next_id; + p->read_badge = new_badge(); + int32_t ch = fnd_sys_chan_create(port, p->read_badge); + if (ch < 0) { + p->id = 0; + return ch; + } + fnd_msg msg = {FND_OK, FND_MSG_XFER_HANDLE, p->id, 0, 0, (uint32_t)ch, 0, 0}; + if (fnd_sys_ipc_reply(r->token, &msg) != FND_OK) { + fnd_sys_handle_close((uint32_t)ch); + p->id = 0; + } + return FND_OK; + } + if (mode != 1) + return FND_E_BAD_ARG; + for (uint32_t i = 0; i < PIPES; ++i) { + if (pipes[i].id == id && id) { + p = &pipes[i]; + break; + } + } + if (!p || p->write_badge) + return FND_E_NOT_FOUND; + uint32_t badge = new_badge(); + int32_t ch = fnd_sys_chan_create(port, badge); + if (ch < 0) + return ch; + fnd_msg msg = {FND_OK, FND_MSG_XFER_HANDLE, p->id, 0, 0, (uint32_t)ch, 0, 0}; + if (fnd_sys_ipc_reply(r->token, &msg) != FND_OK) { + fnd_sys_handle_close((uint32_t)ch); + return FND_OK; + } + p->write_badge = badge; + return FND_OK; +} + +/* Returns 1 if the request was answered (or will be later). */ +static int handle(fnd_handle port, const fnd_recv *r, int32_t *result) { + const fnd_msg *m = &r->msg; + if (r->badge & LOOKUP_BADGE) { + if (m->op != FND_PIPE_OPEN) { + *result = FND_E_UNSUPPORTED; + return 0; + } + *result = pipe_open(port, r); + return *result == FND_OK; + } + int is_write; + struct pipe *p = by_badge(r->badge, &is_write); + if (!p) { + *result = FND_E_BAD_HANDLE; + return 0; + } + *result = FND_OK; + switch (m->op) { + case FND_ST_READ: + if (is_write) + *result = FND_E_ACCESS; + else if (m->a0 == 0 || m->a0 > FND_IPC_MAX_DATA) + *result = FND_E_BAD_ARG; + else if (!queue(r, FND_ST_READ, m->a0 < r->reply_cap ? m->a0 : r->reply_cap)) + *result = FND_E_LIMIT; + else { + settle_all(); + return 1; + } + return 0; + case FND_ST_WRITE: + if (!is_write) + *result = FND_E_ACCESS; + else if (m->len > PIPE_SIZE) + *result = FND_E_BAD_ARG; + else if (m->len == 0) + reply(r->token, FND_OK, 0, 0, 0), *result = FND_OK; + else { + struct pending *q = queue(r, FND_ST_WRITE, m->len); + if (!q) { + *result = FND_E_LIMIT; + } else { + for (uint32_t k = 0; k < m->len; ++k) + q->wbuf[k] = data[k]; + settle_all(); + return 1; + } + } + return m->len == 0 && *result == FND_OK; + case FND_ST_STAT: { + fnd_stat_t st = {FND_TYPE_PIPE}; + st.mode = is_write ? 0x12u : 0x24u; /* owner W / owner R (spec_FS2 §8) */ + for (uint32_t i = 0; i < sizeof st; ++i) + out[i] = ((const uint8_t *)&st)[i]; + reply(r->token, FND_OK, 0, out, sizeof st); + return 1; + } + case FND_ST_SYNC: + return 0; + default: + *result = FND_E_UNSUPPORTED; /* ST_SEEK, ST_TRUNCATE */ + return 0; + } +} + +/* CLIENT_GONE (§4.3): the last handle of an end closed. */ +static void client_gone(uint32_t badge) { + int is_write; + struct pipe *p = by_badge(badge, &is_write); + if (!p) + return; + if (is_write) + p->write_badge = 0; + else + p->read_badge = 0; + for (uint32_t i = 0; i < PENDING; ++i) { /* requests of the end that is gone */ + if (pending[i].badge == badge) + pending[i].badge = 0; + } + settle(p); + if (!p->read_badge && !p->write_badge) + p->id = 0; +} + +int main(void) { + fnd_init(); + int32_t port = fnd_sys_port_create(); + if (port < 0 || fnd_sys_svc_register("pipe", port) != FND_OK) + fnd_exit(FND_E_BAD_HANDLE); + for (;;) { + fnd_recv r = {{0}, (uint32_t)(uintptr_t)data, sizeof data, 0, 0, 0, 0, FND_FOREVER, 0}; + int32_t status = fnd_sys_ipc_recv((uint32_t)port, &r); + if (status == FND_OK && r.token == 0) { + if (r.msg.op == FND_NOTIFY_CLIENT_GONE) + client_gone(r.badge); + } else if (status == FND_OK) { + int32_t result; + r.msg.buf = 0; /* the data is in data[] */ + if (!handle((fnd_handle)port, &r, &result)) + reply(r.token, result, 0, 0, 0); + } + } +} diff --git a/foundation/tests/disk/test_disk_boot.py b/foundation/tests/disk/test_disk_boot.py index 11ee74c..c1cc6a0 100644 --- a/foundation/tests/disk/test_disk_boot.py +++ b/foundation/tests/disk/test_disk_boot.py @@ -107,11 +107,11 @@ def test_image_layout(self): self.assertEqual((header['magic'], header['version'], header['size']), (fndboot.MAGIC, 1, len(boot))) self.assertEqual((header['kernel_load'], header['kernel_entry']), (0x10000, 0x10000)) self.assertEqual(boot[header['kernel_off']:][:header['kernel_size']], kernel) - self.assertEqual([m['name'] for m in modules], ['console', 'disk', 'fs', 'init']) # init last (§11) + self.assertEqual([m['name'] for m in modules], ['console', 'disk', 'fs', 'pipe', 'init']) # init last (§11) for module in modules: image = (self.build / f"{module['name']}.wax").read_bytes() self.assertEqual(boot[module['offset']:][:module['size']], image) - self.assertEqual([m['caps'] for m in modules], [8, 12, 8, 0]) # SVC for the services, DISK for disk + self.assertEqual([m['caps'] for m in modules], [8, 12, 8, 8, 0]) # SVC for the services, DISK for disk self.assertEqual(len(modules[0]['grants']), 4) # the console's MMIO windows (VDP, text, wide font, input) for path in ('/bin/shell.wax', '/bin/echo.wax', '/lib/fnd.1.wal', '/lib/lzma.1.wal', '/lib/rle.1.wal', '/lib/code.1.wal'): inode = volume.resolve(path) diff --git a/foundation/tests/phase1/user.c b/foundation/tests/phase1/user.c index e649869..cb56962 100644 --- a/foundation/tests/phase1/user.c +++ b/foundation/tests/phase1/user.c @@ -42,7 +42,7 @@ void task_probe(volatile uint32_t *box) { box[PROBE_CPU_SIZE] = (uint32_t)fnd_sys_info(2u, 0, 0); box[PROBE_CPU_BAD_POINTER] = (uint32_t)fnd_sys_info(2u, (void *)0x100u, 16u); box[PROBE_CPU_ALIGNMENT] = (uint32_t)fnd_sys_info(2u, (void *)((uintptr_t)cpu + 1u), 16u); - box[PROBE_CPU_BAD_SELECTOR] = (uint32_t)fnd_sys_info(4u, cpu, sizeof cpu); + box[PROBE_CPU_BAD_SELECTOR] = (uint32_t)fnd_sys_info(6u, cpu, sizeof cpu); int32_t needed = fnd_sys_info(2u, cpu, 16u); box[PROBE_CPU_PREFIX] = needed == 48 && cpu[4] == 0x12345678u && cpu[0] > 0 && cpu[1] > 0; box[PROBE_CPU_CLOCK] = cpu[3]; diff --git a/foundation/tests/pkcl/guest_posix.c b/foundation/tests/pkcl/guest_posix.c new file mode 100644 index 0000000..67c0dd4 --- /dev/null +++ b/foundation/tests/pkcl/guest_posix.c @@ -0,0 +1,88 @@ +/* pkcl の POSIX 層(fork・wait・pipe・setjmp)を実際の Foundation で確かめるゲスト。Newlib の posix 構成で組む。 + * 失敗したら失敗した行番号で終了する(0 が成功)。 */ +#include +#include +#include +#include +#include +#include +#include + +#define CHECK(x) \ + do { \ + if (!(x)) \ + _exit(__LINE__); \ + } while (0) + +static int shared = 1; + +int main(void) { + /* 自分の pid と、子から見た親。 */ + pid_t me = getpid(); + CHECK(me > 0); + + /* fork: COW でメモリは分かれ、終了コードは waitpid で受け取れる。 */ + pid_t child = fork(); + CHECK(child >= 0); + if (child == 0) { + shared = 99; + CHECK(getppid() == me && getpid() != me); + _exit(7); + } + int status = 0; + CHECK(waitpid(child, &status, 0) == child); + CHECK(WIFEXITED(status) && WEXITSTATUS(status) == 7); + CHECK(shared == 1); + CHECK(waitpid(child, &status, WNOHANG) == -1 && errno == ECHILD); + + /* pipe: 4 KiB を超える書き込みも、読み手が減らすまで待って全部届き、閉じれば EOF。 */ + int p[2]; + CHECK(pipe(p) == 0); + struct stat st; + CHECK(fstat(p[0], &st) == 0 && S_ISFIFO(st.st_mode)); + child = fork(); + CHECK(child >= 0); + if (child == 0) { + static char block[6000]; + close(p[0]); + memset(block, 'x', sizeof block); + CHECK(write(p[1], "hello ", 6) == 6); + CHECK(write(p[1], block, sizeof block) == (ssize_t)sizeof block); + close(p[1]); + _exit(0); + } + close(p[1]); + char buf[8192]; + size_t total = 0; + ssize_t n; + int first = 1; + while ((n = read(p[0], buf, sizeof buf)) > 0) { + if (first) + CHECK(memcmp(buf, "hello ", 6) == 0); + first = 0; + total += (size_t)n; + } + CHECK(n == 0 && total == 6006); + CHECK(waitpid(child, &status, 0) == child && WEXITSTATUS(status) == 0); + close(p[0]); + + /* 読み手がいない pipe への書き込みは EPIPE。 */ + int q[2]; + CHECK(pipe(q) == 0); + close(q[0]); + CHECK(write(q[1], "a", 1) == -1 && errno == EPIPE); + close(q[1]); + + /* setjmp / longjmp。 */ + jmp_buf jb; + volatile int hops = 0; + int v = setjmp(jb); + if (v == 0) { + hops = 1; + longjmp(jb, 5); + } + CHECK(v == 5 && hops == 1); + + puts("pkcl posix passed"); + return 0; +} diff --git a/foundation/tests/pkcl/test_busybox.py b/foundation/tests/pkcl/test_busybox.py index e99a31b..c19824a 100644 --- a/foundation/tests/pkcl/test_busybox.py +++ b/foundation/tests/pkcl/test_busybox.py @@ -38,8 +38,8 @@ def setUpClass(cls): if result.returncode: raise RuntimeError(result.stdout + result.stderr) wax = (out / 'busybox.wax').read_bytes() - # 実行形式は 1 MiB(FND_EXEC_IMAGE_MAX)まで。収まらないと proc_exec が FND_E_LIMIT になる。 - assert len(wax) < 0x100000, f'busybox.wax is {len(wax)} bytes' + # 実行形式は 1.5 MiB(FND_EXEC_IMAGE_MAX)まで。収まらないと proc_exec が FND_E_LIMIT になる。 + assert len(wax) < 0x180000, f'busybox.wax is {len(wax)} bytes' volume = test_shell.fs2.Volume(bytearray(cls.disk)) volume.put('/bin/busybox.wax', wax, mode=0x3D) cls.disk = bytes(volume.img) @@ -85,5 +85,66 @@ def test_help_lists_applets(self): self.assertIn('basename, cat, cp', screen) +ASH_SCRIPT = """echo one | cat +export Z=zed +busybox sh -c 'echo Z=$Z' +for i in 1 2 3; do echo n$i; done | wc -l +echo hi > /tmp/o.txt; cat < /tmp/o.txt +(echo sub; exit 3); echo rc=$? +false || echo f-ok +echo done-ash +""" + + +@unittest.skipUnless(BUSYBOX and NEWLIB, 'SRC32_BUSYBOX_SOURCE と SRC32_NEWLIB_SOURCE が必要') +@unittest.skipIf(os.environ.get('FND_MM') in ('nommu', 'slot'), 'ash は fork を使うので VM バックエンドでだけ動く') +class BusyboxAshTests(unittest.TestCase): + """MMU 構成の BusyBox(ash)。fork、pipe、環境変数、applet の自己 exec を通す。""" + + @classmethod + def setUpClass(cls): + test_shell.STOP_TICKS = 3000 + test_shell.ShellTests.setUpClass.__func__(cls) + out = cls.work / 'busybox-ash' + command = [sys.executable, str(ROOT / 'tools/pkcl/busybox.py'), '--busybox-source', BUSYBOX, + '--newlib-source', NEWLIB, '--fnd-wal', str(cls.work / 'root/lib/fnd.1.wal'), '--out', str(out), + '--config', str(ROOT / 'tools/pkcl/busybox.config'), + '--config', str(ROOT / 'tools/pkcl/busybox_ash.config')] + if os.environ.get('CLANG'): + command += ['--clang', os.environ['CLANG']] + result = subprocess.run(command, capture_output=True, text=True, timeout=3000) + if result.returncode: + raise RuntimeError(result.stdout + result.stderr) + wax = (out / 'busybox.wax').read_bytes() + assert len(wax) < 0x180000, f'busybox.wax is {len(wax)} bytes' + volume = test_shell.fs2.Volume(bytearray(cls.disk)) + volume.put('/bin/busybox.wax', wax, mode=0x3D) + volume.put('/bin/busybox', wax, mode=0x3D) # BUSYBOX_EXEC_PATH: ash runs applets by exec'ing this + volume.put('/a.sh', ASH_SCRIPT.encode(), mode=0x3D) + cls.disk = bytes(volume.img) + + @classmethod + def tearDownClass(cls): + test_shell.ShellTests.tearDownClass.__func__(cls) + + def test_script_and_pipelines(self): + run = self.work / 'run' + run.mkdir(exist_ok=True) + (run / 'disk.img').write_bytes(self.disk) + (run / 'config.yaml').write_text( + 'memory:\n ram_mib: 16\n vram_mib: 4\n' + 'disk:\n - slot: 0\n type: hdd\n image: disk.img\n read_only: false\n', encoding='utf-8') + command = [str(self.harness), str(self.image), '42', '--frames', '3600', '--dump-pcg', + '--cpu', os.environ.get('FOUNDATION_TEST_CPU', 'cached'), + '--type', '30=busybox sh /a.sh\\n'] + result = subprocess.run(command, cwd=run, capture_output=True, text=True, + encoding='utf-8', errors='replace', timeout=1800) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + lines = result.stdout.splitlines() + screen = lines[lines.index('PCG SCREEN BEGIN') + 1:lines.index('PCG SCREEN END')] + for expected in ('one', 'Z=zed', '3', 'hi', 'sub', 'rc=3', 'f-ok', 'done-ash'): + self.assertIn(expected, screen) + + if __name__ == '__main__': unittest.main() diff --git a/foundation/tests/pkcl/test_pkcl.py b/foundation/tests/pkcl/test_pkcl.py index efd9b18..524e7c9 100644 --- a/foundation/tests/pkcl/test_pkcl.py +++ b/foundation/tests/pkcl/test_pkcl.py @@ -64,5 +64,56 @@ def test_posix_io(self): self.assertIn('status=0', result.stdout) +NEWLIB = os.environ.get('SRC32_NEWLIB_SOURCE') + + +@unittest.skipUnless(NEWLIB, 'SRC32_NEWLIB_SOURCE が必要') +class PkclPosixTests(unittest.TestCase): + """fork・waitpid・pipe・setjmp。VM の構成(既定)で走る。""" + + @classmethod + def setUpClass(cls): + test_shell.ShellTests.setUpClass.__func__(cls) + guest = cls.work / 'posixcheck.wax' + sources = [HERE / 'guest_posix.c', *sorted(PKCL.glob('*.c')), *sorted((PKCL / 'posix').glob('*.c')), + PKCL / 'newlib/syscalls.c'] + command = [sys.executable, str(ROOT / 'tools/llvm-src32/driver.py'), *map(str, sources), + '-I', str(ROOT / 'foundation/include/pkcl-posix'), '-I', str(ROOT / 'foundation/include'), + '-I', str(ROOT / 'sdk/cpt32/include'), '-DPKCL_LIBC', + '--newlib-source', NEWLIB, '--newlib-profile', 'posix', '--newlib-cache', str(cls.work / 'newlib'), + '--entry', 'pkcl_start', '-O1', '--wax3', '--page-align', '--image-id', 'test.posix', + '--import-wal', str(cls.work / 'root/lib/fnd.1.wal'), + '--origin', '0x800000', '--stack-top', '0xC00000', '--user-stack-size', '0x10000', + '-o', str(guest)] + if os.environ.get('CLANG'): + command += ['--clang', os.environ['CLANG']] + result = subprocess.run(command, capture_output=True, text=True, timeout=1800) + if result.returncode: + raise RuntimeError(result.stdout + result.stderr) + volume = test_shell.fs2.Volume(bytearray(cls.disk)) + volume.put('/bin/posixcheck.wax', guest.read_bytes(), mode=0x3D) + cls.disk = bytes(volume.img) + + @classmethod + def tearDownClass(cls): + test_shell.ShellTests.tearDownClass.__func__(cls) + + def test_posix_process_and_pipe(self): + run = self.work / 'run' + run.mkdir() + (run / 'disk.img').write_bytes(self.disk) + (run / 'config.yaml').write_text( + 'memory:\n ram_mib: 16\n vram_mib: 4\n' + 'disk:\n - slot: 0\n type: hdd\n image: disk.img\n read_only: false\n', encoding='utf-8') + command = [str(self.harness), str(self.image), '42', '--frames', '1500', '--dump-pcg', + '--cpu', os.environ.get('FOUNDATION_TEST_CPU', 'cached'), + '--type', '30=posixcheck\\n', '--type', '330=echo status=$?\\n'] + result = subprocess.run(command, cwd=run, capture_output=True, text=True, + encoding='utf-8', errors='replace', timeout=900) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertIn('pkcl posix passed', result.stdout) + self.assertIn('status=0', result.stdout) + + if __name__ == '__main__': unittest.main() diff --git a/foundation/tests/shell/test_shell.py b/foundation/tests/shell/test_shell.py index 7304952..701f0dc 100644 --- a/foundation/tests/shell/test_shell.py +++ b/foundation/tests/shell/test_shell.py @@ -497,16 +497,20 @@ def test_exit_restarts_shell(self): # MEM depends on the build. After spawn, fnd__exec frees its images # before the shell calls wait. A time-slice expiry in that cleanup # lets ps snapshot the ready shell (RUN) before it blocks in wait. + # The pipe service is pid 4, which shifts the later pids by one. + pipe = [r' 4 0 BLOCK 1 +[1-9][0-9]*K pipe'] + shift = len(pipe) patterns = [r' 1 0 BLOCK 1 +[1-9][0-9]*K console', r' 2 0 BLOCK 1 +[1-9][0-9]*K disk', r' 3 0 BLOCK 1 +[1-9][0-9]*K fs', - r' 4 0 BLOCK 1 +[1-9][0-9]*K init', - r' 6 4 (RUN |BLOCK) 2 +[1-9][0-9]*K shell', - r' 7 6 RUN 2 +[1-9][0-9]*K ps'] - self.assertEqual(len(rows), 14, rows) - for row, pattern in zip(rows[7:13], patterns): + *pipe, + r' %d 0 BLOCK 1 +[1-9][0-9]*K init' % (4 + shift), + r' %d %d (RUN |BLOCK) 2 +[1-9][0-9]*K shell' % (6 + shift, 4 + shift), + r' %d %d RUN 2 +[1-9][0-9]*K ps' % (7 + shift, 6 + shift)] + self.assertEqual(len(rows), 14 + shift, rows) + for row, pattern in zip(rows[7:], patterns): self.assertRegex(row, '^' + pattern + '$') - self.assertEqual(rows[13], '/>') + self.assertEqual(rows[-1], '/>') def test_top_snapshot_and_return_to_shell(self): rows = self.screen('top -n 1\\n', 'echo done\\n', at_frames=[30, 300], cpu='pipeline') @@ -515,11 +519,12 @@ def test_top_snapshot_and_return_to_shell(self): self.assertEqual(rows[5:7], [self.mem_line(4096), ' CPU% PID PARENT STATE PRIO MEM NAME']) # /bin/top runs beside the services, init and the shell, busiest first. - names = sorted(row.split()[-1] for row in rows[7:13]) - self.assertEqual(names, ['console', 'disk', 'fs', 'init', 'shell', 'top']) - for row in rows[7:13]: + shift = 1 # the pipe service + names = sorted(row.split()[-1] for row in rows[7:13 + shift]) + self.assertEqual(names, sorted(['console', 'disk', 'fs', 'init', 'pipe', 'shell', 'top'])) + for row in rows[7:13 + shift]: self.assertRegex(row, r'^ +\d+\.\d% +\d+ +\d+ (RUN |BLOCK) +[12] +[1-9][0-9]*K \w+$') - self.assertEqual(rows[13:], ['/> echo done', 'done', '/>']) + self.assertEqual(rows[13 + shift:], ['/> echo done', 'done', '/>']) # Check measured proportions, rather than pinning costs to one build. values = [float(v.rstrip('%')) for v in rows[4].split() if v.endswith('%')] self.assertTrue(all(0 <= v <= 100 for v in values)) @@ -545,7 +550,7 @@ def test_ctrl_c_kills_running_command(self): # and ^C goes to STDERR. top ignores nothing here: the shell never # passes Ctrl+C to the child. rows = self.screen('top\\n', '\\C', 'echo $?\\n', at_frames=[30, 290, 320]) - self.assertIn('^C', rows) + self.assertTrue(any('^C' in row for row in rows), rows) # the row depends on where top was drawing self.assertEqual(rows[-3:], ['/> echo $?', '-20', '/>']) def test_ctrl_c_interrupts_a_blocked_read(self): diff --git a/memory/2026-10-25-issue-168-pkcl-fork-pipe-env.md b/memory/2026-10-25-issue-168-pkcl-fork-pipe-env.md new file mode 100644 index 0000000..312d8f8 --- /dev/null +++ b/memory/2026-10-25-issue-168-pkcl-fork-pipe-env.md @@ -0,0 +1,41 @@ +# pkcl: fork・waitpid・pipe・環境変数・setjmp(Issue #168、#169 の一部) + +`proc_fork`(VM のみ)と COW がマージされたあとに、BusyBox の MMU 構成(ash/hush)に要る POSIX 面を足した作業の記録。 + +## 足したもの(契約) + +- `sys_info` sel 4 = 呼んだプロセスの pid と parent(8 bytes)。sel 5 = PROCESS ハンドルの pid(ハンドル番号を `buf` に渡す)。`fork` の戻りは PROCESS ハンドルなので、pid は sel 5 で引く。 +- pkcl `posix/process.c`: `fork`/`vfork`/`waitpid`/`wait`/`kill`/`getpid`/`getppid`。子プロセス表は 32 件で、`fork` の子では空にして始める(子は孫しか待てない)。`waitpid(WNOHANG)` は `wait(h, 0)` の `FND_E_WOULD_BLOCK` を 0 に直す。 +- `setjmp`/`longjmp` は `tools/llvm-src32/backend.py` がインライン展開する(このバックエンドは inline asm も weak も alias もないので、C やアセンブリ関数では書けない)。`sigsetjmp` は `pkcl-posix/setjmp.h` で `setjmp` に倒す。 +- pipe サービス(`foundation/services/pipe`、名前 `pipe`、API §9.5)。端は badge 付き CHANNEL で、`fork`/`dup` で共有できる。EOF と EPIPE は `CLIENT_GONE`(端の最後のハンドルが閉じた)で決まる。どのバックエンドでも起動する(`fork` が VM 専用なので、端の共有は VM だけ)。 +- 環境変数: 起動ブロック version 2(`env_count`、`env_off`)、`fnd_envc()`/`fnd_env(i)`(WAL fnd 1.3)、`fnd_exec_t.envp`。`pkcl_start` が `environ` を作り、`execve` が `envp` を渡す。 +- プロセス上限を 7/8 から 10/16(`FND_MAX_PROCS`/`FND_MAX_THREADS`)に増やした。pipe サービスが 1 つ使い、`fork` の子に余地が要るため。 12 にすると `tests/wax` の PIE 配置の期待(42/43)が崩れるので、10 までにしてある。 + +## 落とし穴 + +- `ST_READ` を 4096 bytes より大きく要求すると libfnd の `fnd_read` は満杯の塊の次を待つ。pipe の `read` は pkcl が 4096 bytes に切る(`PKCL_FD_PIPE`)。切らないとシェルのパイプが固まる。 +- `FND_E_PEER_GONE` は `EPIPE` に対応づけた(それまでは `EIO`)。SIGPIPE は配送されない。 +- 起動ブロックは 4096 bytes まで。環境と引数の合計がこれを超えると `execve` は `E2BIG` 相当(`FND_E_LIMIT`)で失敗する。 +- `execve` は代理 exec(子を作って待ち、終了コードで自分も終わる)のまま。fork 後の exec は動くが、イメージを毎回読み直す。 +- `-k posix` のように名前で絞ると `test_posix_io`(nolibc 側)も一緒に走る。nolibc の `sys/stat.h` に `S_IFIFO` がないと両方が壊れる。 + +- console は、読み手が `kill` された直後(`CLIENT_GONE` が届く前)に渡したキーを失っていた。`ipc_reply` が失敗したらキーをキューの先頭へ戻す(`unget_key`)。`test_ctrl_c_interrupts_a_blocked_read` が起動ブロックの大きさ(8 の倍数かどうか)で落ちる形で見つかった。 +- 起動ブロックの大きさは 8 byte 境界に切り上げる(`lib/fnd/proc.c`)。上の失敗の引き金だったタイミング差を減らすためで、原因そのものは console 側。 +- 新しいサービスを足すと `ps`/`top`、FNDBOOT のモジュール表を数えるテスト(`test_shell.py`、`test_disk_boot.py`)の期待値が変わる。pipe は全バックエンドで起動するので行数は同じ。 + +## 検証 + +- `SRC32_NEWLIB_SOURCE=... python3 -m unittest discover -s foundation/tests/pkcl -k posix`(`guest_posix.c`: fork の COW、waitpid、execve と環境、6 KB の pipe、EOF、EPIPE、setjmp)。 +- `python3 -m unittest discover -s foundation/tests/shell`(VM と `FND_MM=nommu` の両方)、`foundation/tests/wax`、`tools/llvm-src32` の `test_toolchain -k foundation`。 + +## 残り(#169 ほか) + +`poll`/`O_NONBLOCK`(pipe と通常ストリーム)、シグナルの配送(SIGINT、SIGCHLD)、`posix_spawn`、`proc_exec`(in-place exec)、BusyBox の MMU 構成(ash/hush)での不足シンボルの洗い出しと 1 MiB 制限。 + +## BusyBox の MMU 構成と ash(同じ作業の続き) + +- `busybox.py --config busybox.config --config busybox_ash.config`(複数指定は後勝ち)。ash 入りは 1.39 MB なので、`FND_EXEC_IMAGE_MAX` と `FND_SPAWN_IMAGE_MAX` を 1.5 MiB にした(kernel の staging は 3 MiB で、WAL と合わせて収まる)。 +- ash が要求した不足は `times`、`WCOREDUMP`、`fcntl(F_DUPFD_CLOEXEC)`(スクリプトを開くと `sh: 3: m` のような壊れたメッセージで落ちる)だけだった。 +- applet の自己 exec は `BUSYBOX_EXEC_PATH`。既定の `/proc/self/exe` は Foundation にないので `/bin/busybox` にし、WAX を拡張子なしでも置く。`fnd_proc_exec` は argv[0] をパスで上書きするため、applet 名が消えて `a.sh: applet not found` になる。`FND_EXEC_KEEP_ARGV0` で防ぐ。 +- 設定ファイルを更新しても `--stage link` は `.config` を作り直さない。出力ディレクトリを変えるか消す。 +- 確認済み: `busybox sh /a.sh`(`|`、`()`、`export`、`<` `>`、`||`)、対話の `busybox sh`。未確認: ジョブ制御、`read`、`trap`(シグナルがない)、`wait`、`kill`。 diff --git a/tools/llvm-src32/backend.py b/tools/llvm-src32/backend.py index 9177593..2eeef6f 100644 --- a/tools/llvm-src32/backend.py +++ b/tools/llvm-src32/backend.py @@ -28,6 +28,7 @@ "cpt32_run_user", "cpt32_return_to_kernel", "cpt32_jump", "cpt32_ctx_resume", "cpt32_cpu_features") # Intrinsics that execute supervisor-only instructions (HALT, MFSR, MTSR, IRET). +_SETJMP_NAMES = ("setjmp", "_setjmp", "__builtin_setjmp", "longjmp", "_longjmp", "siglongjmp") _SUPERVISOR_INTRINSICS = ("cpt32_halt", "cpt32_mfsr", "cpt32_mtsr", "cpt32_run_user", "cpt32_return_to_kernel", "cpt32_ctx_resume") _KERNEL_CONTEXT = "__llvm_src32_kernel_context" @@ -1002,6 +1003,9 @@ def call(self, inst, args): if name in _INTRINSICS: self.privilege_intrinsic(inst, name, values) return + if name in _SETJMP_NAMES and name not in self.owner.definitions: + self.jump_buffer_call(inst, name, values) + return if name.startswith("llvm."): self.intrinsic(inst, name, values) return @@ -1087,6 +1091,40 @@ def constant_operand(self, value, what, valid): self.fail(f"{what} {number:#x} is out of range") return number + def jump_buffer_call(self, inst, name, values): + """setjmp and longjmp, expanded at the call site (doc/spec_pkcl.md §7 G7). + + Every value lives in the frame, addressed from FP, so a jump buffer only needs SP, FP + and the resume address: words 0, 1 and 2 of the buffer. setjmp stores the result slot + after the label, which longjmp reaches with R1 = the value. + """ + c = self.c + if name in _SETJMP_NAMES[:3]: + if len(values) != 1 or str(inst.type) != "i32": + self.fail(f"{name} must take a jump buffer and return int") + resume = c.fresh("setjmp_resume") + self.load(values[0], 3) + c.mem("ST", 28, 3, 0) + c.mem("ST", 29, 3, 4) + c.imm(2, ("label", resume)) + c.mem("ST", 2, 3, 8) + c.imm(1, 0) + c.label(resume) + self.store(inst) + return + if len(values) != 2: + self.fail(f"{name} must take a jump buffer and a value") + taken = c.fresh("longjmp_value") + self.load(values[1], 1) + self.load(values[0], 3) + c.emit(f"BNE R1, R0, {taken}") + c.imm(1, 1) + c.label(taken) + c.mem("LD", 2, 3, 8) + c.mem("LD", 29, 3, 4) + c.mem("LD", 28, 3, 0) + c.emit("JR R2") + def privilege_intrinsic(self, inst, name, values): """SRC32 HALT and Extension P intrinsics (sdk/cpt32/include/cpt32/cpu.h).""" c = self.c diff --git a/tools/pkcl/busybox.py b/tools/pkcl/busybox.py index e530fd5..8056f27 100644 --- a/tools/pkcl/busybox.py +++ b/tools/pkcl/busybox.py @@ -48,11 +48,12 @@ def configure(args, build: Path): make = ["make", "-C", str(args.busybox_source), f"O={build}"] run([*make, "allnoconfig"], log=build / "allnoconfig.log") wanted = {} - for line in args.config.read_text(encoding="utf-8").splitlines(): - line = line.split("#", 1)[0].strip() if not line.startswith("CONFIG_") else line.strip() - if "=" in line: - key, value = line.split("=", 1) - wanted[key] = value + for config in args.config: # later files override earlier ones + for line in config.read_text(encoding="utf-8").splitlines(): + line = line.split("#", 1)[0].strip() if not line.startswith("CONFIG_") else line.strip() + if "=" in line: + key, value = line.split("=", 1) + wanted[key] = value lines, seen = [], set() for line in (build / ".config").read_text(encoding="utf-8").splitlines(): match = re.match(r"# (CONFIG_\w+) is not set", line) or re.match(r"(CONFIG_\w+)=", line) @@ -193,7 +194,9 @@ def main(argv=None): parser.add_argument("--newlib-source", type=Path, required=True) parser.add_argument("--fnd-wal", type=Path, required=True, help="/lib/fnd.1.wal built by foundation/boot/build.py") parser.add_argument("--out", type=Path, default=Path("build/busybox")) - parser.add_argument("--config", type=Path, default=HERE / "busybox.config") + parser.add_argument("--config", type=Path, action="append", + help="option files applied in order on top of allnoconfig (default: busybox.config); " + "add busybox_ash.config for the MMU configuration with ash") parser.add_argument("--clang") parser.add_argument("--cache", type=Path, help="Newlib IR cache (default: tools/llvm-src32's)") parser.add_argument("-O", dest="optimization", default="-Os", choices=["-O0", "-O1", "-O2", "-Os", "-Oz"]) @@ -201,6 +204,7 @@ def main(argv=None): parser.add_argument("--only", help="regular expression: compile only the matching objects (debugging)") parser.add_argument("--stage", choices=["configure", "compile", "link"], default="link") args = parser.parse_args(argv) + args.config = args.config or [HERE / "busybox.config"] args.busybox_source = args.busybox_source.resolve() args.out = args.out.resolve() build = args.out / "bb" diff --git a/tools/pkcl/busybox_ash.config b/tools/pkcl/busybox_ash.config new file mode 100644 index 0000000..0adb531 --- /dev/null +++ b/tools/pkcl/busybox_ash.config @@ -0,0 +1,14 @@ +# BusyBox の MMU 構成(fork を使う)と ash。busybox.config の後に重ねる(doc/spec_pkcl.md §8.3)。 +# Foundation の VM バックエンドで動く。slot/nommu のバックエンドには fork がない。 +CONFIG_NOMMU=n +CONFIG_SH_IS_ASH=y +CONFIG_SH_IS_NONE=n +CONFIG_SHELL_ASH=y +CONFIG_ASH=y +CONFIG_ASH_OPTIMIZE_FOR_SIZE=y +CONFIG_FEATURE_SH_STANDALONE=y +CONFIG_FEATURE_PREFER_APPLETS=y +# applet を exec し直すときの実行形式。WAX を拡張子なしでも置く(/bin/busybox)。 +CONFIG_BUSYBOX_EXEC_PATH="/bin/busybox" +CONFIG_WC=y +CONFIG_ENV=y