NetBSDでのsignal(3)で送出されたシグナルがカーネル空間でどう扱われるか追いかけてみる

[NetBSD Advent Calendar 2023](https://qiita.com/advent-calendar/2023/netbsd) 21日目の記事です。今日はNetBSDにおける、シグナル送信周りの機能を見てゆこうと思います。

NetBSD(に限らず、UNIX系OSも)では、「シグナル」と呼ばれる特定の値をプロセス間で送受信する機能が提供されていいます。ユーザ空間から見ると、[signal(3)](https://man.netbsd.org/signal.3)というライブラリ関数でシグナル送信機能が利用できます。シグナルを利用する分にはライブラリ関数を利用するだけで充分ですが、カーネル空間側ではどのような処理が行われているのでしょうか？今日の記事ではこのあたりを追いかけてみようと思います。

## ユーザ空間側でのシグナル送信処理

### killコマンド

まずはユーザ空間側でのシグナル送信処理を見てみます。プロセスへのシグナル送信は[kill(1)](https://man.netbsd.org/kill.1)コマンドで行うため、まずはkillコマンドのソースコードを見てみます。

`kill` コマンドのソースコードは[/usr/src/bin/kill/kill.c](https://github.com/NetBSD/src/blob/trunk/bin/kill/kill.c)にあります。`main()` 関数の中で引数等の処理を行っていますが、最終的に[kill(2)](https://man.netbsd.org/kill.2)でシグナルを送出しています。

```c
 72 int
 73 main(int argc, char *argv[])
 74 {
 75     int errors;
 76     int numsig;
 77     pid_t pid;
...
194         if (kill(pid, numsig) == -1) {
195             warn("%s", *argv);
196             errors = 1;
197         }
```

### kill(2)システムコール

[kill(2)](https://man.netbsd.org/kill.2)システムコールは[/usr/src/sys/kern/sys_sig.c](https://github.com/NetBSD/src/blob/trunk/sys/kern/kern_sig.c)で定義されています。`sys_kill()` ではユーザ空間から渡された値(プロセスIDとシグナル番号)を `ksiging_t` に設定して `kill1()` 関数を読んでいます。

```c
300 int
301 sys_kill(struct lwp *l, const struct sys_kill_args *uap, register_t *retval)
302 {
303     /* {
304         syscallarg(pid_t)   pid;
305         syscallarg(int) signum;
306     } */
307     ksiginfo_t  ksi;
308
309     KSI_INIT(&ksi);
310
311     ksi.ksi_signo = SCARG(uap, signum);
312     ksi.ksi_code = SI_USER;
313     ksi.ksi_pid = l->l_proc->p_pid;
314     ksi.ksi_uid = kauth_cred_geteuid(l->l_cred);
315
316     return kill1(l, SCARG(uap, pid), &ksi, retval);
317 }
```

同じソースファイル内で定義されています。`kill1()` 関数を見てみます。ここではパラメータチェック後に `killpg1()` を呼び出していますが、その際にプロセスグループへのシグナル送信か否かで指定する引数の内容を変えています。

```c
223 int
224 kill1(struct lwp *l, pid_t pid, ksiginfo_t *ksi, register_t *retval)
225 {
226     int error;
227     struct proc *p;
...
269     switch (pid) {
270     case -1:        /* broadcast signal */
271         return killpg1(l, ksi, 0, 1);
272     case 0:         /* signal own process group */
273         return killpg1(l, ksi, 0, 0);
274     default:        /* negative explicit process group */
275         return killpg1(l, ksi, -pid, 0);
276     }
277     /* NOTREACHED */
278 }
```

`killpg1()` も同一ソースファイル内にあります。ここまででパラメータチェックなどの下準備が完了しているので、あとはシグナル送信対象のプロセスに(プロセステーブルを辿って)シグナルをセットするだけ(のはず…)です。　

```c
 800 /*
 801  * killpg1: common code for kill process group/broadcast kill.
 802  */
 803 int
 804 killpg1(struct lwp *l, ksiginfo_t *ksi, int pgid, int all)
 805 {
 806     struct proc *p, *cp;
 807     kauth_cred_t    pc;
 808     struct pgrp *pgrp;
 809     int     nfound;
 810     int     signo = ksi->ksi_signo;
 811
 812     cp = l->l_proc;
 813     pc = l->l_cred;
 814     nfound = 0;
 815
 816     mutex_enter(proc_lock);
...

```

プロセステーブルを操作するため、`mutex_enter()` でクリティカルセクションに入っています。ちなみに、`proc_lock` は[/usr/src/sys/kern/kern_proc.c](https://github.com/NetBSD/src/blob/trunk/sys/kern/kern_proc.c)で以下のように定義されています。

```c
 120 kmutex_t *      proc_lock   __cacheline_aligned;
```

プロセステーブル( `allproc` )でプロセスを一つずつ辿り、 `mutex_enter(p->p_lock)` 自身のプロセス以外のもの、かつ、`p->p_flag` に `PK_SYSTEM` (システムプロセス( `kthread` )でないプロセスを抽出しています。その後、 `kauth_authorize_process()` で `KAUTH_PROCESS_SIGNAL` が許可されているプロセスに `kpsignal2()` を適用しています。

```c
 817     if (all) {
 818         /*
 819          * Broadcast.
 820          */
 821         PROCLIST_FOREACH(p, &allproc) {
 822             if (p->p_pid <= 1 || p == cp ||
 823                 (p->p_flag & PK_SYSTEM) != 0)
 824                 continue;
 825             mutex_enter(p->p_lock);
 826             if (kauth_authorize_process(pc,
 827                 KAUTH_PROCESS_SIGNAL, p, KAUTH_ARG(signo), NULL,
 828                 NULL) == 0) {
 829                 nfound++;
 830                 if (signo)
 831                     kpsignal2(p, ksi);
 832             }
 833             mutex_exit(p->p_lock);
 834         }
 835     } else {
...
 856     }
 857 out:
 858     mutex_exit(proc_lock);
 859     return nfound ? 0 : ESRCH;
 860 }
```

これまた同じソースファイル内にある `kpsignal2()` を見てみます。シグナルを送出するプロセスの状態(生成途中のプロセスor停止しているプロセスかどうか等)によってシグナルを廃棄する等の処理を経たのち、`sigput()` で対象プロセスにシグナル( `ksiginfo_t` )を設定しています。

対象プロセスのLWP( `p->lwps` )に `sigpost()` を適用しています。
(思っていたよりも結構ややこしい処理が行われていますね…)

```c
1294 int
1295 kpsignal2(struct proc *p, ksiginfo_t *ksi)
1296 {
1297     int prop, signo = ksi->ksi_signo;
1298     struct lwp *l = NULL;
1299     ksiginfo_t *kp;
1300     lwpid_t lid;
1301     sig_t action;
1302     bool toall;
1303     int error = 0;
...
1485     /*
1486      * Make signal pending.
1487      */
1488     KASSERT((p->p_slflag & PSL_TRACED) == 0);
1489     if ((error = sigput(&p->p_sigpend, p, kp)) != 0)
1490         goto out;
```

`sigput()` (これも同じソースファイルで定義されています)では、`struct proc->p_sigpend` (引数の `signed_t *sp` として渡されてくる)で該当するシグナルの格納場所( `kp->ksi_signo` )に `KSI_COPY()` マクロで `ksiginfo_t *ksi` をセットしたのち、 `kp->ksi_flags` にシグナルがキューイングされていることを示す `KSI_QUEUED` フラグを立てます。

```c
 618 static int
 619 sigput(sigpend_t *sp, struct proc *p, ksiginfo_t *ksi)
 620 {
 621     ksiginfo_t *kp;
...
 636     size_t count = 0;
 637     TAILQ_FOREACH(kp, &sp->sp_info, ksi_list) {
 638         count++;
 639         if (ksi->ksi_signo >= SIGRTMIN && ksi->ksi_signo <= SIGRTMAX)
 640             continue;
 641         if (kp->ksi_signo == ksi->ksi_signo) {
 642             KSI_COPY(ksi, kp);
 643             kp->ksi_flags |= KSI_QUEUED;
 644             return 0;
 645         }
 646     }
...
 658     return 0;
 659 }
```

`sigpost()` の時点ではプロセス構造体にシグナルがキューイングされるだけで、その後にある `sigpost()` の中でシグナルのハンドリングが行われるようです。


```c
1294 int
1295 kpsignal2(struct proc *p, ksiginfo_t *ksi)
1296 {
...
1489     if ((error = sigput(&p->p_sigpend, p, kp)) != 0)
...
1499     /*
1500      * Try to find an LWP that can take the signal.
1501      */
1502     LIST_FOREACH(l, &p->p_lwps, l_sibling) {
1503         if (sigpost(l, action, prop, kp->ksi_signo) && !toall)
1504             break;
1505     }
...
1518 }
```

## まとめ

NetBSDでのシグナル処理を見てみました。思いのほかカーネル空間では複雑な手順を踏んで処理されていました。プロセスの状態やLWP(Light Weight Process)、カーネルのトレース(デバッグ中であるとかDTraceが有効であるか等)といった様々な要素が関連するため、複雑な処理になるのは仕方のないことかもしれません。