カーネルモジュール経由でNetBSDのプロセス情報を取得してみる

[NetBSD Advent Calendar 2023](https://qiita.com/advent-calendar/2023/netbsd) 22日目の記事です。今日はカーネルモジュール経由でNetBSDのプロセス情報を取得してみます。

## NetBSDのカーネルモジュールサンプル

[NetBSDのカーネルモジュールサンプルを試してみる](https://qiita.com/furandon_pig/items/647c21cbe21eeb3aafc2)(NetBSD Advent Calendar 2018 3日目)の記事でNetBSDのカーネルモジュールを動かしてみていました。

 * [/usr/src/sys/module/examples](https://github.com/NetBSD/src/tree/trunk/sys/modules/examples)

これらはごく簡単なサンプルであり、あくまでもカーネルモジュールの動作概要や利用できる機能を概観する類のものでしたが、今回はこれらのサンプルを拡張し、少し実用的な形にしてみます。

## NeBSDのプロセス構造体(struct proc)

NetBSDでは[struct proc](https://github.com/NetBSD/src/blob/b29a6b6f4f2d6952c48d54dc4be4678f71b07e19/sys/sys/proc.h#L233)という構造体で各プロセスを管理しています。以下に今回の記事で使用する構造体メンバを抜粋します。

```c
230 struct proc {
...
263     pid_t       p_pid;      /* :: Process identifier. */
...
268     LIST_HEAD(, lwp) p_lwps;    /* p: List of LWPs. */
...
274     int         p_nlwps;    /* p: Number of LWPs */
...
335     char        p_comm[MAXCOMLEN+1];
```

PC上で動作する一般的なOSにおいては、[スレッド](https://ja.wikipedia.org/wiki/%E3%82%B9%E3%83%AC%E3%83%83%E3%83%89_(%E3%82%B3%E3%83%B3%E3%83%94%E3%83%A5%E3%83%BC%E3%82%BF))というプロセスの上で動作する軽量プロセス(というかスレッド)がサポートされています。NetBSDでは、`struct proc` 内の構造体メンバ、 `LIST_HEAD(, lwp) p_lwps` でスレッドが管理されています。また、`int p_nlwps` に現在管理している `lwp` (Light Weight Process)の数が記録されています。

NetBSDの[top(1)](https://man.netbsd.org/top.1)コマンドでも `-t` オプションでスレッド数が確認できますが、一つのプロセスのスレッドが各行に表示され、パッと見ではひとつのプロセス上で動いているスレッド数が確認しづらい感じです。例えば以下はプロセスID `1145` のプロセス上で5個のスレッドが動作している例です。

```sh
load averages:  0.01,  0.01,  0.00;               up 0+06:40:21        20:01:43
5 threads: 5 sleeping
CPU states:  0.0% user,  0.0% nice,  0.0% system,  0.0% interrupt,  100% idle
Memory: 72M Act, 4K Wired, 10M Exec, 45M File, 371M Free
Swap: 512M Total, 512M Free

  PID   LID USERNAME PRI STATE      TIME   WCPU    CPU NAME      COMMAND
 1145     5 root      85 nanoslp    0:00  0.00%  0.00% -         pthread_sample
 1145     4 root      85 nanoslp    0:00  0.00%  0.00% -         pthread_sample
 1145     3 root      85 nanoslp    0:00  0.00%  0.00% -         pthread_sample
 1145     2 root      85 nanoslp    0:00  0.00%  0.00% -         pthread_sample
 1145     1 root      85 ttyraw     0:00  0.00%  0.00% -         pthread_sample
```

そこで今回ので記事では、指定してプロセスIDに対して、スレッドの個数を数値として返すことができるカーネルモジュールを作成してみます。

## 指定したプロセスのスレッド数を取得するカーネルモジュール

### カーネルモジュールとのやりとり方法を考える

[NetBSDのカーネルモジュールサンプルでioctlできるようにする](https://qiita.com/furandon_pig/items/4caad87eb33ff3c979c3)(NetBSD Advent Calendar 2018 11日目)の記事では[ioctl(2)](https://man.netbsd.org/ioctl.2)でユーザ空間からカーネル空間にデータを受け渡しする例を示していました(※)。
※ただし、この記事では一部私の認識違いによる間違った解説が含まれています…。`ioctl(2)`で文字列データを渡せるかのような記述になっていますが、データとして渡せるのは `long` 型の4byteだけです。

とりあえず、今回作成するカーネルモジュールも`ioctl(2)`でプロセスIDを渡し、`read(2)`で結果(の文字列)を受け取るという方法でやりとりしてみます。

### さっそくカーネルモジュールを作成してみる

さっそくカーネルモジュールを作成してみます。サンプルコードは以下になります。

https://gist.github.com/furandon-pig/b0facb1439f6ff4950a8c2bea7db3cbb

`ioctl(2)` でプロセスIDを渡したのち、`read(2)` が呼ばれた際に一致するプロセスIDがあれば該当プロセスの情報を収集し、文字列データとしてユーザ空間に返しています。`PK_SYSTEM` フラグが立っているプロセスはカーネルスレッドであるため、その場合は早々にスキップします。

```c
115 int
116 proc_sample_read(dev_t self __unused, struct uio *uio, int flags __unused)
117 {
...
122     struct proc *p;
123     PROCLIST_FOREACH(p, &allproc) {
124         if (p->p_pid <= 1 || (p->p_flag & PK_SYSTEM) != 0) {
125             continue;
126         }
127         if (p->p_pid == target_pid) {
```

対象となるプロセスが見つかったら、あとは`struct proc`の中から必要な構造体メンバの値を抽出するだけです。

```c
115 int
116 proc_sample_read(dev_t self __unused, struct uio *uio, int flags __unused)
117 {
...
119     char result[1024] = { '\0' };
...
127         if (p->p_pid == target_pid) {
128             char buf[128];
129
130             r = strcat(r, "PID\tCOMMAND\tPATH\tLWP\n");
131
132             snprintf(buf, 128, "%ld", (long int)p->p_pid);
133             r = strcat(r, buf);
134             r = strcat(r, "\t");
135             r = strcat(r, p->p_comm);
136             r = strcat(r, "\t");
137             snprintf(buf, 128, "%ld\t", (long int)p->p_nlwps);
138             r = strcat(r, buf);
139             r = strcat(r, "\0");
140
141             break;
142         }
```

最後に構築した文字列を[uiomove(9)](https://man.netbsd.org/uiomove.9)でユーザ空間に送り出して処理は完了です。

```c
145     if ((e = uiomove(result, strlen(result), uio)))
146         return e;
147
148     return 0;
149 }
```

このカーネルモジュールにはデバイスファイル経由でアクセスるため、以下のコマンドで `/dev/proc_sample` を作成しておきます。

```sh
# mknod /dev/proc_sample c 210 0
# file /dev/proc_sample
/dev/proc_sample: character special (210/0)
```

## ユーザ空間側のプログラム

https://gist.github.com/furandon-pig/5525f8c76790ea6ba19718bdec7b3e6d

```c
 17 int main(int argc, char *argv[])
 18 {
 19     char buf[BUFSIZ];
 20     int fd;
 21     long value = -1;
 22
 23     if (argc < 2) {
 24         fprintf(stderr, "Error: too few argument.\n");
 25         exit(1);
 26     }
 27     value = atol(argv[1]);
 28
 29     fd = open("/dev/kauth_sample", O_RDONLY);
 30     if (fd == -1) {
 31         fprintf(stderr, "open() failed.\n");
 32         exit(-1);
 33     }
 34
 35     memset(buf, '\0', sizeof(buf));
 36     ioctl(fd, KAUTH_SAMPLE_IOCTL_PID, &value);
 37     read(fd, buf, BUFSIZ);
 38     printf("%s\n", buf);
 39
 40     close(fd);
 41
 42     return 0;
 43 }
```

ごく簡単なプログラムですが、主要な部分は以下の2行です。

```c
 36     ioctl(fd, KAUTH_SAMPLE_IOCTL_PID, &value);
 37     read(fd, buf, BUFSIZ);
```

実際にコンパイルして実行してみます。指定したプロセスIDに対するスレッド数(LWP)が取得できました。

```sh
# gcc -Wall -g -o proc_sample_userland proc_sample_userland.c
#
# man 9 ioctl &
[3] 937
# ./proc_sample_userland 937
PID     COMMAND LWP
937     man     1
```

## スレッド数を増減させて確認してみる

上記の実行例ではスレッド数が `1` (単一のスレッドのみで動作している)ので、あまり面白味(？)がありません。実際にスレッドを作成するサンプルプログラムを利用してスレッド数(LWP)の増減を確認してみます。

以下は単純に"create"と入力するごとにスレッドを作成するプログラムです。

https://gist.github.com/furandon-pig/dd1cde9665569a4a63d8b95fe8c81bcd

```sh
# gcc -Wall -g -o pthread_sample pthread_sample.c
# ./pthread_sample
Ctrl-D to quit. > create
thread 0 created.
Ctrl-D to quit. > I'm thread 1.
Ctrl-D to quit. > create
thread 1 created.
Ctrl-D to quit. > I'm thread 2.

```

上記の段階でスレッドは3個作成されています(メインスレッド1個に"create"の入力で作成したスレッド2個の合計3個)。この状態で作成したユーザランドからカーネルモジュール経由でスレッド数を取得する機能を試してみると、スレッド(LWP)は `3` と表示されており、正しくスレッド数が取得できているようです。

```sh
# ps | grep pthread_sample
 232 pts/1 S+   0:00.02 ./pthread_sample
#
# ./sample 232
PID     COMMAND LWP
232     pthread_sample  3
```

確認のため、もう一つスレッドを作成して確認してみます。

```sh
Ctrl-D to quit. > create
thread 2 created.
```

スレッドが新たに1個作成され、LWPの表示は `4` になっています。

```sh
# ./sample 232
PID     COMMAND LWP
232     pthread_sample  4
```

この状態でスレッドを上限(このサンプルプログラムでは作成できるスレッド上限は10個)まで作成し、プロセス終了時の `pthread_join()` でスレッド数が減ってゆくのを観察してみます。

スレッドのサンプルプログでは `Ctrl-D` の入力で `pthread_join()` が走ります。

```sh
Ctrl-D to quit. > ^D
thread 0 joined.
thread 1 joined.
thread 2 joined.
thread 3 joined.
thread 4 joined.
thread 5 joined.
thread 6 joined.
thread 7 joined.
thread 8 joined.
thread 9 joined.
```

`pthread_join()` が行われている間、別の端末からプロセス数を取得してみます。

```sh
# while true; do ./sample 15413 | grep pthread; sleep 1; done
15413   pthread_sample  11
15413   pthread_sample  11
15413   pthread_sample  11
15413   pthread_sample  10
15413   pthread_sample  8
15413   pthread_sample  6
15413   pthread_sample  5
15413   pthread_sample  5
15413   pthread_sample  3
```

内部的にも終了したスレッド数が反映されていることが確認できます。

## まとめ

カーネルモジュール経由でNetBSDカーネル内のプロセス情報( `struct proc` )を参照してみました。ユーザ空間から `ioctl(2)` で必要なデータを渡して結果を `read(2)` で受け取る形態は、他のカーネル機能の実験にも応用できそうです。