NetBSD Advent Calendar 2023 21日目の記事です。今日はNetBSDにおける、シグナル送信周りの機能を見てゆこうと思います。
NetBSD(に限らず、UNIX系OSも)では、「シグナル」と呼ばれる特定の値をプロセス間で送受信する機能が提供されていいます。ユーザ空間から見ると、signal(3)というライブラリ関数でシグナル送信機能が利用できます。シグナルを利用する分にはライブラリ関数を利用するだけで充分ですが、カーネル空間側ではどのような処理が行われているのでしょうか?今日の記事ではこのあたりを追いかけてみようと思います。
ユーザ空間側でのシグナル送信処理
killコマンド
まずはユーザ空間側でのシグナル送信処理を見てみます。プロセスへのシグナル送信はkill(1)コマンドで行うため、まずはkillコマンドのソースコードを見てみます。
kill
コマンドのソースコードは/usr/src/bin/kill/kill.cにあります。main()
関数の中で引数等の処理を行っていますが、最終的にkill(2)でシグナルを送出しています。
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)システムコールは/usr/src/sys/kern/sys_sig.cで定義されています。sys_kill()
ではユーザ空間から渡された値(プロセスIDとシグナル番号)を ksiging_t
に設定して kill1()
関数を読んでいます。
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()
を呼び出していますが、その際にプロセスグループへのシグナル送信か否かで指定する引数の内容を変えています。
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()
も同一ソースファイル内にあります。ここまででパラメータチェックなどの下準備が完了しているので、あとはシグナル送信対象のプロセスに(プロセステーブルを辿って)シグナルをセットするだけ(のはず…)です。
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で以下のように定義されています。
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()
を適用しています。
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()
を適用しています。
(思っていたよりも結構ややこしい処理が行われていますね…)
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
フラグを立てます。
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()
の中でシグナルのハンドリングが行われるようです。
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が有効であるか等)といった様々な要素が関連するため、複雑な処理になるのは仕方のないことかもしれません。