Python psutilとsysctl(3)について

はじめに

Let's Encryptのベータテストに応募したところ、テストユーザーになることができました。 そこで、証明書の取得方法を調べてみると、Pythonで書かれたクライアントを使って取得することが分かりました。 Pythonで書かれているのだから、クロスプラットフォームなのに違いないと最初は思ったのですが、psutilというモジュールを使っており、これが各プラットフォームに独自のCで書かれたコードをたくさん持っていました。 psutil 3.3.0をNetBSDでだいたい動くようにする中で分かった、NetBSD独自のシステム情報の取得の仕組みを紹介します。

psutilは、ごく最近OpenBSDのサポートが追加されました。また、FreeBSDのサポートは以前からされています。 NetBSDサポートの追加は、この2つのOS用のコードをうまく再利用しつつ進めました。

psutilは、Python System and process Utilitiesの略で、プロセスやシステムの情報をアーキテクチャーによらず統一した方法で呼び出せる仕組みです。
https://pythonhosted.org/psutil/
にドキュメントがあり、実行例も丁寧に記述されています。なので、動作確認も比較的容易です。

NetBSDで動くようにするためのパッチは、
http://cvsweb.netbsd.org/bsdweb.cgi/pkgsrc/sysutils/py-psutil/patches/
にあります。 こういう類いのプログラムを書く経験がほとんどないので、より良い実装方法がありましたら、教えていただければ助かります。

システムの情報を入手する仕組み

システムの情報をユーザーランドのプログラムが入手するには、カーネルに問い合わせをして教えてもらわなくてはなりません。今回psutilでNetBSDをサポートするに当たって調べたところ、以下の2つの仕組みが用意されているようです。

  • sysctlライブラリー関数
  • kvmライブラリーに定義されている関数類

今回は、sysctlライブラリー関数の方を使っているのですが、私の理解できた範囲で、両方について説明してみます。

sysctlライブラリー関数

sysctlについては、sysctl(3)とsysctl(7)のマニュアルページを読みました。 sysctl関数は、MIB (Management Information Base)と言うintの配列を使って欲しい情報を指定して、情報を入手する仕組みです。情報を設定することもできますが、今回はそのような使い方はしませんでした。

$ man 3 sysctl
と実行してみると、マニュアルページを読むことができます。 実際にMIBに指定するパラメーターはsysctl(7)マニュアルページに記載されていますので、
$ man 7 sysctl
を参照すると、いくぶんかは説明されています。 MIBは、intの配列を定義して例えば以下のように手作業で作って使うこともできますが、sysctl(8)コマンドで使用するような文字列を使って、MIBを生成することもできます。
mib[0] = CTL_KERN;
mib[1] = KERN_FILE2;
mib[2] = KERN_FILE_BYFILE;
mib[3] = 0;
mib[4] = sizeof(struct kinfo_file);
mib[5] = 0;
sysctlnametomib("net.inet6.tcp6.pcblist", mib, &namelen)
ほぼ、sysctl(8)コマンドで指定する文字列でMIBを作れるようです。

kvmライブラリーに定義されている関数

これらは、kvm(3)マニュアルページに解説されています。/dev/mem等を通じて情報にアクセスします。これも情報を設定することもできます。 今回は、kvm(3)マニュアルページに解説されている関数は、結果的には使用しませんでした。なので、例はありません。 実行中のカーネルだけでなく、core等から情報を取得することもできるようなので、役立つ場面もあるかもしれません。

sysctl(3)での実例

psutilにNetBSDサポートを追加するに当たって、sysctl(3)を使いましたが、sysctl(7)の解説だけでは、実例がなく良く分からないこともありました。NetBSDのsrcツリーの中の事例も参照しつつ進めました。

以下には、sysctl(3)を使って、各種の情報を取得していく方法について、記していきたいと思います。 なんとなく、NetBSDに特有なものを記載します。pkgsrc/sysutils/py-psutilだと、psutil/arch/bsd/以下の内容です。

プロセスをPIDで指定して、その実行ファイル名を絶対パスで取得する

NetBSD currentのみですが、以下のようにして取得できます。

mib[0] = CTL_KERN;
mib[1] = KERN_PROC_ARGS;
mib[2] = pid;
mib[3] = KERN_PROC_PATHNAME;
mib[3]は、以下のように変えれば、他の情報を得られます。
KERN_PROC_ARGV 引数の文字列
KERN_PROC_ENV 環境変数の文字列
KERN_PROC_NARGV 引数の数
KERN_PROC_NENV 環境変数の数
と、ここまで書いて気付きましたが、psutil_get_cmd_args()は書き方が最適ではないですね。KERN_PROC_NARGVを使えば良いでしょうね。

psutilで使っている箇所は以下です。

PyObject *
psutil_proc_exe(PyObject *self, PyObject *args) {
    pid_t pid;
    char pathname[MAXPATHLEN];
    int error;
    int mib[4];
    int ret;
    size_t size;

    if (! PyArg_ParseTuple(args, "l", &pid))
        return NULL;

    mib[0] = CTL_KERN;
    mib[1] = KERN_PROC_ARGS;
    mib[2] = pid;
    mib[3] = KERN_PROC_PATHNAME;

    size = sizeof(pathname);
    error = sysctl(mib, 4, NULL, &size, NULL, 0);
    if (error == -1) {
        PyErr_SetFromErrno(PyExc_OSError);
        return NULL;
    }

    error = sysctl(mib, 4, pathname, &size, NULL, 0);
    if (error == -1) {
        PyErr_SetFromErrno(PyExc_OSError);
        return NULL;
    }
    if (size == 0 || strlen(pathname) == 0) {
        ret = psutil_pid_exists(pid);
        if (ret == -1)
            return NULL;
        else if (ret == 0)
            return NoSuchProcess();
        else
            strcpy(pathname, "");
    }
    return Py_BuildValue("s", pathname);
}

プロセスの持つスレッド数を取得する

各プロセスの情報は、kinfo_proc2構造体に格納できます。 /usr/include/sys/sysctl.h に定義がされています。

kinfo_proc2構造体にプロセスIDを指定して情報を格納するには、以下のようなMIBを用意します。pidにプロセスIDを指定します。

mib[0] = CTL_KERN;
mib[1] = KERN_PROC2;
mib[2] = KERN_PROC_PID; // PIDを指定して条件を絞ることを示しています。
mib[3] = pid;
mib[4] = size;
mib[5] = 1;
mib[2]は、以下のように変えれば、絞る条件を変更できます。
KERN_PROC_ALL  絞らない
KERN_PROC_GID  グループID
KERN_PROC_PGRP  プロセスグループ
KERN_PROC_RGID  リアルグループID
KERN_PROC_RUID  リアルユーザーID
KERN_PROC_SESSION セッションID
KERN_PROC_TTY  TTYデバイス
KERN_PROC_UID  ユーザーID
プロセスの持つスレッド数は、p_nlwpsに格納されています。

psutilでの使用例は以下のようです。

int
psutil_kinfo_proc(pid_t pid, kinfo_proc *proc) {
    // Fills a kinfo_proc struct based on process pid.
    int ret;
    int mib[6];
    size_t size = sizeof(kinfo_proc);

    mib[0] = CTL_KERN;
    mib[1] = KERN_PROC2;
    mib[2] = KERN_PROC_PID;
    mib[3] = pid;
    mib[4] = size;
    mib[5] = 1;

    ret = sysctl((int*)mib, 6, proc, &size, NULL, 0);
    if (ret == -1) {
        PyErr_SetFromErrno(PyExc_OSError);
        return -1;
    }
    // sysctl stores 0 in the size if we can't find the process information.
    if (size == 0) {
        NoSuchProcess();
        return -1;
    }
    return 0;
}

PyObject *
psutil_proc_num_threads(PyObject *self, PyObject *args) {
    // Return number of threads used by process as a Python integer.
    long pid;
    kinfo_proc kp;
    if (! PyArg_ParseTuple(args, "l", &pid))
        return NULL;
    if (psutil_kinfo_proc(pid, &kp) == -1)
        return NULL;
    return Py_BuildValue("l", (long)kp.p_nlwps);
}

プロセスの持つファイルディスクリプター数を取得する

開かれているファイルに関する情報は、kinfo_file構造体に格納して参照します、 例えばあるプロセスの開いているファイルディスクリプターの数を取得するには、以下のようなMIBを用意します。

mib[0] = CTL_KERN;
mib[1] = KERN_FILE2;
mib[2] = KERN_FILE_BYPID; /* 
mib[3] = (int) pid;
mib[4] = sizeof(struct kinfo_file);
mib[5] = 0;
この結果として得られたkinfo_file構造体の配列の長さが、あるファイルの開いているファイルディスクリプターの数になります。

プロセスのソケットの情報を取得する

Let's Encryptは、psutilのnet_connectionsというメソッドを使っています。これは、netstat(1)やsockstat(1)のような機能で、システムの全てのソケット接続の状況をプロセスごとに取得するものです。 NetBSDのsysctlでは、直接このような機能はないようです。 そこで、sockstat(1)のソースコードを参考にして、kinfo_proc2とkinfo_fileの2種類の構造体を組み合わせることにしました。kinfo_porc2のki_sockaddrとlkinfo_fileのki_fdataが同じ内容を指しているので、その関係を使って結び付けました。

実例は、 http://cvsweb.netbsd.org/bsdweb.cgi/pkgsrc/sysutils/py-psutil/patches/patch-psutil_arch_bsd_netbsd__socks.c?rev=1.1&content-type=text/x-cvsweb-markup を参照してください。

おわりに

psutilのLinux用のコードは、多くがprocfsからの情報を使っています。BSDとの思想の違いは、こういう所にもあるのかもしれません。

0 件のコメント:

コメントを投稿

注: コメントを投稿できるのは、このブログのメンバーだけです。

Windows 11 Pro 24H2からSambaのguest ok = yesな共有フォルダーへアクセスする

Microsoft Windows 11 Proを動かしているマシンで、sambaでguest ok = yesにしている共有フォルダーにアクセスしていた。 Windows 11を24H2にアップデートしたところ、その共有フォルダーを開こうとすると、ログインを求められ、...