パケット受信処理の待ち合わせ部分

ネットワークスタックの、割り込みコンテキストからプロセスをたたき起こすまでの処理はしっかり読んだつもりなのだが、プロセスコンテキストで寝ている側を深く把握していなかったのでちょっと眺めてみる。

システムコールからプロセスがスリープするまで(UDP

アプリケーションからのシステムコールが最終的にudp_recvmsgを呼ぶところまでの説明は割愛。
udp_recvmsgは__skb_recv_datagramを呼んで、sk->sk_receive_queueに積まれたskbを取り出そうとする。
(skはカーネル内のソケット構造体)
この時、パケットがなければwait_for_packetがコールされるが、これが待ち合わせ処理のwait側。
wait_for_packetでは、prepare_to_wait_exclusiveset_current_stateを呼びcurrent->stateに新しい値をセットしている。
(currentは現在このCPUで実行中のプロセスを指す)
prepare_to_wait_exclusiveで渡されているのはTASK_INTERRUPTIBLEなので、シグナルで割り込み可能なスリープ状態に状態を変更している。
(プロセスの状態遷移に関してはこのへんの記事に書いてある)
wait_for_packetは、既にパケットが届いているんじゃないか、とかいろいろチェック処理をしてから最終的にschedule_timeoutを呼ぶ。
ここでのtimeout値はおそらくソケットに設定されている通信のタイムアウト時間に基づく。
ここではタイマーの設定などをおこなっているが最終的にはscheduleを呼ぶ。
scheduleはすぐに__scheduleを呼ぶのでこっちを読んでいく。
if (unlikely(signal_pending_state(prev->state, prev)))という条件式がある。
signal_pending_stateは要するにcurrent->state & TASK_INTERRUPTIBLEをチェックしているので、ここでは真になる。
これが真だとdeactivate_taskからdequeue_task、更にdequeue_task_fairが呼ばれてランキューからcurrentが取り除かれる。
これによって、このプロセスはパケットの着信・タイムアウト・シグナルなどの理由で起こされるまでスケジュールされないようになった。
プロセスを起こす側との待ち合わせはsk->sk_wq->waitに定義されているwait_queue_head_tを介してwaitqueueのAPIで行なっている。
sk->sk_wq->waitは生のまま使わずに、sk_sleep(sk)というマクロを使う(RCUによる参照のため)。
(waitqueueの説明はこのへんの記事に書いてある。古いけど。)

システムコールからプロセスがスリープするまで(TCP

tcp_recvmsgを見てみる。
UDPより色々な処理が行われているが、sk->sk_receive_queueが空だと最終的にsk_wait_dataが呼ばれる。
ここで呼ばれるprepare_to_waitは、UDPで呼ばれていたprepare_to_wait_exclusiveとほぼ同様の処理を行なっている。
その後、sk_wait_eventマクロの中で、schedule_timeoutが呼ばれる。
その後の処理はUDPと同様と考えて構わないと思われる。

起こす側(UDP

NICから上がってきたパケットはIP層の処理を経てudp_rcv__udp4_lib_rcvudp_queue_rcv_skb__udp_queue_rcv_skbsock_queue_rcv_skbの順に呼ばれていき、最終的にsk->sk_data_readyを呼んでいる。
これは、sock_def_readableを指しており、sk->sk_wq->waitに対してwake_up_interruptible_sync_pollを用いて起床リクエストをかけている。
wake_up_interruptible_sync_poll→__wake_up_sync_key__wake_up_commonの順で見ていくと、wait_queue_tに設定されたコールバック関数が呼ばれている
このコールバック関数はwait_for_packetで設定していたもので、receiver_wake_functionを指している。
autoremove_wake_functiondefault_wake_functionの順で呼ばれ、try_to_wake_upに行き着く。
try_to_wake_upはプロセスを再び実行可能状態にしてランキューに登録する。
また、負荷状況によっては、他のCPUへプロセスマイグレーションを行う。

起こす側(TCP

パケットを受け取ってsk->sk_data_readyでプロセスを起こすのは同様。
但し、コールバック関数はsk_wait_dataでDEFINE_WAITを呼ぶことによりautoremove_wake_functionに設定されている
autoremove_wake_functionはUDPでのコールバック関数であるreceiver_wake_functionからも呼ばれていたので、コールバック以降の処理はほぼ同様。

感想

プロセスを起こす関数としてtry_to_wake_upが明確に存在して呼ばれているのは簡単にわかるのに、スリープ処理周りでdeactivate_taskとかしているようには見えなかったのが謎かったので読んでみた。
scheduleから呼ばれてた…。しらんがな…。