NetBSD Advent Calendar 2023 22日目の記事です。今日はカーネルモジュール経由でNetBSDのプロセス情報を取得してみます。
NetBSDのカーネルモジュールサンプル
NetBSDのカーネルモジュールサンプルを試してみる(NetBSD Advent Calendar 2018 3日目)の記事でNetBSDのカーネルモジュールを動かしてみていました。
これらはごく簡単なサンプルであり、あくまでもカーネルモジュールの動作概要や利用できる機能を概観する類のものでしたが、今回はこれらのサンプルを拡張し、少し実用的な形にしてみます。
NeBSDのプロセス構造体(struct proc)
NetBSDではstruct procという構造体で各プロセスを管理しています。以下に今回の記事で使用する構造体メンバを抜粋します。
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においては、スレッドというプロセスの上で動作する軽量プロセス(というかスレッド)がサポートされています。NetBSDでは、struct proc
内の構造体メンバ、 LIST_HEAD(, lwp) p_lwps
でスレッドが管理されています。また、int p_nlwps
に現在管理している lwp
(Light Weight Process)の数が記録されています。
NetBSDのtop(1)コマンドでも -t
オプションでスレッド数が確認できますが、一つのプロセスのスレッドが各行に表示され、パッと見ではひとつのプロセス上で動いているスレッド数が確認しづらい感じです。例えば以下はプロセスID 1145
のプロセス上で5個のスレッドが動作している例です。
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できるようにする(NetBSD Advent Calendar 2018 11日目)の記事ではioctl(2)でユーザ空間からカーネル空間にデータを受け渡しする例を示していました(※)。
※ただし、この記事では一部私の認識違いによる間違った解説が含まれています…。ioctl(2)
で文字列データを渡せるかのような記述になっていますが、データとして渡せるのは long
型の4byteだけです。
とりあえず、今回作成するカーネルモジュールもioctl(2)
でプロセスIDを渡し、read(2)
で結果(の文字列)を受け取るという方法でやりとりしてみます。
さっそくカーネルモジュールを作成してみる
さっそくカーネルモジュールを作成してみます。サンプルコードは以下になります。
ioctl(2)
でプロセスIDを渡したのち、read(2)
が呼ばれた際に一致するプロセスIDがあれば該当プロセスの情報を収集し、文字列データとしてユーザ空間に返しています。PK_SYSTEM
フラグが立っているプロセスはカーネルスレッドであるため、その場合は早々にスキップします。
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
の中から必要な構造体メンバの値を抽出するだけです。
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)でユーザ空間に送り出して処理は完了です。
145 if ((e = uiomove(result, strlen(result), uio)))
146 return e;
147
148 return 0;
149 }
このカーネルモジュールにはデバイスファイル経由でアクセスるため、以下のコマンドで /dev/proc_sample
を作成しておきます。
# mknod /dev/proc_sample c 210 0
# file /dev/proc_sample
/dev/proc_sample: character special (210/0)
ユーザ空間側のプログラム
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行です。
36 ioctl(fd, KAUTH_SAMPLE_IOCTL_PID, &value);
37 read(fd, buf, BUFSIZ);
実際にコンパイルして実行してみます。指定したプロセスIDに対するスレッド数(LWP)が取得できました。
# 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"と入力するごとにスレッドを作成するプログラムです。
# 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
と表示されており、正しくスレッド数が取得できているようです。
# ps | grep pthread_sample
232 pts/1 S+ 0:00.02 ./pthread_sample
#
# ./sample 232
PID COMMAND LWP
232 pthread_sample 3
確認のため、もう一つスレッドを作成して確認してみます。
Ctrl-D to quit. > create
thread 2 created.
スレッドが新たに1個作成され、LWPの表示は 4
になっています。
# ./sample 232
PID COMMAND LWP
232 pthread_sample 4
この状態でスレッドを上限(このサンプルプログラムでは作成できるスレッド上限は10個)まで作成し、プロセス終了時の pthread_join()
でスレッド数が減ってゆくのを観察してみます。
スレッドのサンプルプログでは Ctrl-D
の入力で pthread_join()
が走ります。
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()
が行われている間、別の端末からプロセス数を取得してみます。
# 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)
で受け取る形態は、他のカーネル機能の実験にも応用できそうです。