procfsの実装からNetBSDとLinuxでのスタンスの違い(?)を読み解いてみる


NetBSD Advent Calendar 2021 4日目の記事です。
今日は /proc における、NetBSDとLinuxでのスタンスの違い(?)をソースコードから読み解いて行こうと思います。

NetBSDでの/proc

Linux Advent Calendar 20213日目の記事で逆ポーランド計算機をカーネル内で実装し、 /proc/krpn でユーザランドからアクセスするという記事を書きました。

ふと、「NetBSDでも同様に /proc を動的に作成・削除できそうな気がする…」と調査してみたのですが、その過程で /proc に対するスタンスの違い(?)が見て取れたので紹介してみようと思います。

ソースコードから/procの実装を見てゆく

/procの大まかな定義方法を調べる

ソースコードを読み解くにあたり、取っ掛かりを見つけたいところです。 /proc の下を見てみると、 /proc/version/proc/cpuinfo といったファイルが見えます。このあたりをキーワードにして探すのが良さそうです。

# ls -l /proc/ | tail
-r--r--r--  1 root     wheel      0 Dec  6 23:40 cpuinfo
lr-xr-xr-x  1 root     wheel      4 Dec  6 23:40 curproc@ -> 5886
-r--r--r--  1 root     wheel      0 Dec  6 23:40 devices
-r--r--r--  1 root     wheel      0 Dec  6 23:40 loadavg
-r--r--r--  1 root     wheel      0 Dec  6 23:40 meminfo
-r--r--r--  1 root     wheel      0 Dec  6 23:40 mounts
lr-xr-xr-x  1 root     wheel      4 Dec  6 23:40 self@ -> curproc
-r--r--r--  1 root     wheel      0 Dec  6 23:40 stat
-r--r--r--  1 root     wheel      0 Dec  6 23:40 uptime
-r--r--r--  1 root     wheel      0 Dec  6 23:40 version

とりあえず"version"というキーワードで検索してみると、 sys/miscfs/procfs というディレクトリがあり、その中の procfs_vnops.cPFSversion という定義があります。

# cd /usr/src/sys
# find ./miscfs/procfs/ | xargs grep version | grep procfs
...
./miscfs/procfs/procfs_vfsops.c:      case PFSversion:        /* /proc/version = -r--r--r-- */
./miscfs/procfs/procfs_vnops.c:       { DT_REG, N("version"),     PFSversion,        procfs_validfile_linux },
./miscfs/procfs/procfs_vnops.c:       case PFSversion:
./miscfs/procfs/procfs_vnops.c:       case PFSversion:

procfs_vnops.c を見ると、N("version")N("cpuinfo") といった、 /proc の下にあるファイルと同じ名前の定義がありました。

sys/miscfs/procfs/procfs_vnops.c:
 189 /*
 190  * List of files in the root directory. Note: the validate function will
 191  * be called with p == NULL for these ones.
 192  */
 193 static const struct proc_target proc_root_targets[] = {
 194 #define N(s) sizeof(s)-1, s
 195     /*    name          type        validp */
 196     { DT_REG, N("meminfo"),     PFSmeminfo,        procfs_validfile_linux },
 197     { DT_REG, N("cpuinfo"),     PFScpuinfo,        procfs_validfile_linux },
 198     { DT_REG, N("uptime"),      PFSuptime,         procfs_validfile_linux },
 199     { DT_REG, N("mounts"),      PFSmounts,         procfs_validfile_linux },
 200     { DT_REG, N("devices"),     PFSdevices,        procfs_validfile_linux },
 201     { DT_REG, N("stat"),        PFScpustat,        procfs_validfile_linux },
 202     { DT_REG, N("loadavg"),     PFSloadavg,        procfs_validfile_linux },
 203     { DT_REG, N("version"),     PFSversion,        procfs_validfile_linux },
 204 #undef N
 205 };

struct proc_target の定義を見ると、 *pt_name/proc のノード名、 *pt_valid/proc のハンドラとなる関数を指定すれば良さそうです。

miscfs/procfs/procfs_vnops.c:
 150 static const struct proc_target {
 151     u_char  pt_type;
 152     u_char  pt_namlen;
 153     const char  *pt_name;
 154     pfstype pt_pfstype;
 155     int (*pt_valid)(struct lwp *, struct mount *);
 156 } proc_targets[] = {
...

/procのノード名は決め打ちにされている?

あらためて /proc/version に相当する定義を見てみます。マクロ N("version")sizeof(s)-1s の2つの定義に展開されるため、 struct proc_target と照らし合わせると N("version")/proc のノード名になり、 PFSversionstruct proc_target.pt_pfstype に渡されるようです。

sys/miscfs/procfs/procfs_vnops.c:
 193 static const struct proc_target proc_root_targets[] = {
 194 #define N(s) sizeof(s)-1, s
 195     /*    name          type        validp */
...
 203     { DT_REG, N("version"),     PFSversion,        procfs_validfile_linux },
 204 #undef N
 205 };

PFSversion/proc のノード毎に処理を分岐させるために利用される気がします。実際にソースコードを見てみると以下のenumで定数化されています。

miscfs/procfs/procfs.h:
 78 /*
 79  * The different types of node in a procfs filesystem
 80  */
 81 typedef enum {
...
114     PFSversion, /* kernel version (if -o linux) */
...
119 } pfstype;

これで /proc を動的に生やすための大まかな流れが把握できたような…と思ったのですが、 PFSversion に相当する値はenumで定義されているため、 typedef enum { ... } pfstype の範囲を超える値を指定してもダメなような…。加えて、範囲内の値でも別の /proc ノードと重複してしまうのでうまく動作しないはずです…。

/procノードを増やすにはカーネルの再ビルドが必要

もう少し調べてみます。 PFSversion を参照している個所を見ると、 procfs_rw() 関数の中で /proc のノードによって処理を分岐しています。 miscfs/procfs/procfs_subr.c はファイル名の通り、 procfs 自身の処理ルーチンとなっています。

この実装内容から考えるに、Linuxでの実装とは異なり、カーネルモジュールのような形で /proc ノードを生やしたりすることはできなさそうです。

miscfs/procfs/procfs_subr.c:
140 int
141 procfs_rw(void *v)
142 {
...
191     switch (pfs->pfs_type) {
...
277     case PFSversion:
278         error = procfs_doversion(curl, p, pfs, uio);
279         break;
...
291     default:
292         error = EOPNOTSUPP;
293         break;
294     }

この/procのスタンスの違いはなぜ?

こうやって調べてみると、NetBSDとLinuxでは /proc の利用シーンに対するスタンスの違いが見えてみます。

カーネルモジュールのサンプルは /usr/src/sys/modules/examples に置かれているのですが、探しても /proc を操作するサンプルが見当たらなかったのは、そもそもそういった使い方を想定していないからなのかもしれません…。

mount_procfs(8)を見ると、 /proc で提供されるファイル( /proc ノード)の一覧が記載されています。

FILES

     /proc/#
     /proc/#/cmdline
     /proc/#/cwd
     /proc/#/exe
     /proc/#/file
     /proc/#/fpregs
     /proc/#/map
     /proc/#/maps
     /proc/#/mem
     /proc/#/note
     /proc/#/notepg
     /proc/#/regs
     /proc/#/root
     /proc/#/status
     /proc/curproc
     /proc/self

考えてみれば、 /proc は「プロセスに関する情報を提供するファイルシステム」であるため、プロセスに関連しないものや動的に生えてくる任意のプロセス情報といったものは提供しない(させない)ようにしている、とも言えます。

じつは /proc/echo みたいなサンプルを用意しようと考えていたのですが、こうやって一通り調べた後で考え直してみると、 /proc の用途にマッチしていませんね…。

まとめ

NetBSDでの /proc の実装を調べてみました。併せてLinuxと照らし合わせてみると、 /proc の実装スタンスの違いも見えてきました。
プロセスの情報を返す、という /proc の元々の用途を考えると、NetBSDの実装は意図も踏まえた内容にも思えてきます。

参考URL