miniio: POSIX移植
API検討のためにPOSIXに移植する。ちょっと難しいのは、ポータブルで非同期な名前解決手法が存在しないことで、このためだけにワーカースレッドを作成する必要がある。
非同期な名前解決はOS固有の手法が提供されていることが多い。
-
getaddrinfo_a
-- glibcのみ -
GetAddrInfoEx
-- Windows8以降で非同期版(や、DnsQuery https://github.com/libuv/libuv/issues/4388 )が提供されている
今回はこれらは考慮しない。というかPOSIXのsocketなんだからpthreadくらい有るだろって事で。。今回はBSD"風"ソケットAPIに対するポータビリティは考慮しないことにする。
POSIX仕様を集める
- Realtime(タイマや非同期I/O): https://pubs.opengroup.org/onlinepubs/9699919799/functions/V2_chap02.html#tag_15_08
- Thread: https://pubs.opengroup.org/onlinepubs/9699919799/functions/V2_chap02.html#tag_15_09
- Socket: https://pubs.opengroup.org/onlinepubs/9699919799/functions/V2_chap02.html#tag_15_10
一旦miniioのコア仕様からプロセスは外そうと思っているので、いわゆるPOSIX準拠組込みOSにも対応できる。
poll
の対応状況
常識的なシステムでは poll
が共通のwaitとして select
の代わりに利用できる。組込みOSの対応状況は:
- NuttX: 推奨 https://nuttx.apache.org/docs/latest/reference/user/10_filesystem.html#poll-h
- FreeRTOS: selectしか無く、そもそもPOSIX名も提供していない https://www.freertos.org/FreeRTOS-Plus/FreeRTOS_Plus_TCP/API/select.html
- Zephyr: 提供している https://docs.zephyrproject.org/latest/doxygen/html/group__bsd__sockets.html#gae2d9b8390c125624595e2b400a08ea29
- Eclipse ThreadX(旧Azure RTOS):
nx_bsd_poll
として実は存在するが、write方向のnon-blockingに対応していない https://github.com/eclipse-threadx/rtos-docs/blob/main/rtos-docs/netx-duo/appendix-d.md
... 打率低いな。。
Chimeとタイマの実装
Chimeはminiioのwait API miniio_ioctx_process
を他のスレッドから起床させるためのAPIで、 miniio_chime_trigger
は唯一のスレッドセーフAPIとなる。(他のAPIはI/Oスレッドから呼出す必要がある)
poll
の実I/Oに拠らない起床方法には2つあり、1つはシグナルでsyscallを中断させることで、もう1つは単に socketpair
や pipe
で作成したfdに別のスレッドから書き込む(self-pipe)方法がある。
今回は1つのpipeをコンテキストの初期化時に確保しておいて、timerやchimeでは常にそれを使う方向で処理することにした。BSD系のkqueueやLinuxのtimerfd/eventfdではもっと効率的にこれらを実現できる。
- poll: https://pubs.opengroup.org/onlinepubs/9699919799/functions/poll.html#tag_16_363
- pipe: https://pubs.opengroup.org/onlinepubs/9699919799/functions/pipe.html#tag_16_362
複数のスレッドから同時にpipeに書き込むとデータが混ざる可能性がある。このため、self-pipeでpipeに書き込むデータは1バイトでなければならない。timerとchime(とin-flightな名前解決)の最大数は、この1バイトで識別できる数(255個)に制約されることになる。
誤解を招きそうなのでreword。。POSIXでは、pipeに纏まったサイズを書いた場合、アトミックである(メッセージの割り込みが起こらない)ことを求めている。
Write requests of {PIPE_BUF} bytes or less shall not be interleaved with data from other processes doing writes on the same pipe. Writes of greater than {PIPE_BUF} bytes may have data interleaved, on arbitrary boundaries, with writes by other processes, whether or not the O_NONBLOCK flag of the file status flags is set.
I/O is intended to be atomic to ordinary files and pipes and FIFOs. Atomic means that all the bytes from a single operation that started out together end up together, without interleaving from other I/O operations.
ただし、いっぺんに読めることは保証していない:
The standard developers considered adding atomicity requirements to a pipe or FIFO, but recognized that due to the nature of pipes and FIFOs there could be no guarantee of atomicity of reads of {PIPE_BUF} or any other size that would be an aid to applications portability.
ただし、このアトミック性は書き込みが成功したときの要件であって、writeは書き込みが "部分的に" 成功しても良いことになっている。このため、無条件にアトミック性を要求できるメッセージの最大長は1バイトになる。BSDのkeventやLinuxのeventfd、WindowsのIOCP等はポインタ長のメッセージをやりとりできるため、このような制約はself-pipingにユニークとなる。
readとwriteへの対応
... 良いアイデアが無い。一旦ブロッキングI/Oに統一して、その場でreadなりwriteなりする実装にしてしまうか。。ただ、Cygwinのようにブロックすべきでないシチュエーションで read
がブロックする https://qiita.com/okuoku/items/7a3a4944745b0424d415#o_nonblock-なpipe2がブロックする バグとか割と普通にあるのでちょっと怖いものはある。
ちなみに write
ではダメで、 send
で MSG_NOSIGNAL
する必要がある。そうしないとTCP接続の対向が不在だった場合に SIGPIPE
が来てしまう。