Linuxのドライバを読みつつPlantUMLの紹介をする


本記事ではLinuxのドライバソースを読みながら、PlantUMLを軽く紹介します。

PlantUMLとは

公式サイト: https://plantuml.com/

PlantUMLの解説記事はたくさんあるので詳細は割愛しますが、名前の通りUMLを書くためのツールです。特徴はなんといっても、テキストで記述できる点です。

UMLといえば設計のために使用することが多いのですが、当然動作解析にも使用できます。例えばドライバのソースを読んでいると、ファイル間の関数呼び出し関係が複雑になり、理解が追いつかないことがありませんか (筆者の脳はスズメの涙ほどのキャパシティしかないため、タグジャンプを2段くらい重ねると呼び出し元が何だったかすぐに忘れます)。誰がどういう順序で関数を呼んでいるのかを明確にできればよいのですが、そんなときシーケンス図が役に立ちます。

PlantUMLでシーケンス図を書くとこんなかんじ。

@startuml
Alice -> Bob: SYN
Alice <-- Bob: ACK
@enduml

上記をテキストエディタで作成したら、適当な名前(例: hoge.puml)で保存して、下記コマンドで画像生成します。

$ plantuml hoge.puml

実行すると、コマンド実行したディレクトリにhoge.pngが生成されます。

構文は、
オブジェクト名 矢印 ('->'や'-->'など) オブジェクト名 : メッセージ
で、オブジェクト名にはソースファイル名、メッセージには呼び出す関数を入れてしまえば簡単にファイル間の関数呼び出し関係を表現できそうです。なお、より詳細な構文は公式サイトを参照してください。

Linuxドライバソースを読む

例として、Linux v5.4のUSBカメラのドライバを軽く見てみます。drivers/media/usb/uvcの下にあります。

$ ls drivers/media/uvc/
Kconfig  Makefile  uvc_ctrl.c  uvc_debugfs.c  uvc_driver.c
uvc_entity.c  uvc_isight.c  uvc_metadata.c  uvc_queue.c
uvc_status.c  uvc_v4l2.c  uvc_video.c  uvcvideo.h

なんでこんなにファイルがあるんですかね。

LinuxのUSBの最も低いレベルのドライバ(ホストコントローラやHUBの制御)はdrivers/usb/coreの下にあります。一方、drivers/media/uvc/にあるUVCドライバは、接続されたUSBデバイスのクラスがUVCだった場合に動作する部分です。ここにあるファイルがどういう関係なのかよくわからないので、これらのファイルの関係をある程度理解することを目標に読み進めていきます。が、あくまで今回の目的はPlantUMLを紹介することですので、あまり詳しくは読み込みません。

まずはとっかかりです。UVCドライバはカメラドライバなので、Linuxのビデオキャプチャフレームワーク(V4L2)を使って、ユーザランドからの/dev/videoXXXに対するioctl()に応じてカメラの操作をする、という仕組みになっているはずです。この想定で読み始めます。

まずはgrepすると、どうやらuvc_driver.c、uvc_metadata.c、uvc_v4l2.cの3つがioctl絡みの処理をしているようです。例として、データフォーマットの設定をするioctl VIDIOC_S_FMTの処理を追ってみます。

それっぽい名前をgrepすると、uvc_metadata.cとuvc_ioctl.cの中に見つかります。

$ grep -iH -n vidioc_s_fmt drivers/media/usb/uvc/*
drivers/media/usb/uvc/uvc_metadata.c:133:   .vidioc_s_fmt_meta_cap      = uvc_meta_v4l2_set_format,
drivers/media/usb/uvc/uvc_v4l2.c:472: * - VIDIOC_S_FMT
drivers/media/usb/uvc/uvc_v4l2.c:1474:  .vidioc_s_fmt_vid_cap = uvc_ioctl_s_fmt_vid_cap,
drivers/media/usb/uvc/uvc_v4l2.c:1475:  .vidioc_s_fmt_vid_out = uvc_ioctl_s_fmt_vid_out,

どちらのファイルも、v4l2_ioctl_ops構造体の関数ポインタに自身の関数をセットしています。

ioctl VIDIOC_S_FMTは、v4l2_format構造体のポインタを引数にとるAPIで、ioctl()を呼び出した時に最初に入るのはdrivers/media/v4l2-core/v4l2-ioctl.cのv4l_s_fmt()です。ここを見るとわかるのですが、このAPIはv4l2_format構造体のtypeで設定する種類を指定して呼び出します。v4l2_s_fmt()では、typeに応じてv4l2_ioctl_ops構造体にセットされている関数ポインタを呼び出します。

v4l2-ioctl.c
static int v4l_s_fmt(const struct v4l2_ioctl_ops *ops,
                struct file *file, void *fh, void *arg)
{
...
    switch (p->type) {
    case V4L2_BUF_TYPE_VIDEO_CAPTURE:
        if (unlikely(!ops->vidioc_s_fmt_vid_cap))
            break;
        CLEAR_AFTER_FIELD(p, fmt.pix);
        ret = ops->vidioc_s_fmt_vid_cap(file, fh, arg);
        /* just in case the driver zeroed it again */
        p->fmt.pix.priv = V4L2_PIX_FMT_PRIV_MAGIC;
        if (vfd->vfl_type == VFL_TYPE_TOUCH)
            v4l_pix_format_touch(&p->fmt.pix);
        return ret;
...

typeにV4L2_BUF_TYPE_VIDEO_CAPTUREを指定した場合、uvc_v4l2.cで.vidioc_s_fmt_vid_capにセットした関数uvc_ioctl_s_fmt_vid_cap()の呼び出し経路に入るようです。

で、これをPlantUMLで表現すると

@startuml
participant "User Space" as user
participant "v4l2-ioctl.c" as v4l2
participant "uvc_v4l2.c" as uvc

user -> v4l2: ioctl(fd, V4L2_S_FMT, *v4l2_format)
alt v4l2_format->type == V4L2_BUF_TYPE_VIDEO_CAPTURE
    v4l2 -> uvc: uvc_ioctl_s_fmt_vid_cap()
end
@enduml

こんな感じでどうでしょう。先頭で"participant 〜 as ..."としているのは、オブジェクト名にハイフン'-'が入る場合、文法の都合上ダブルクオートで括る必要があるため、いちいち書くのが面倒なので別名をつけています。

この先の関数呼び出しをもう少し深く追っていきます。

uvc_v4l2.c
static int uvc_ioctl_s_fmt_vid_cap(struct file *file, void *fh,
                   struct v4l2_format *fmt)
{
    struct uvc_fh *handle = fh;
    struct uvc_streaming *stream = handle->stream;
    int ret;

    ret = uvc_acquire_privileges(handle);
    if (ret < 0)
        return ret;

    return uvc_v4l2_set_format(stream, fmt);
}

ここから呼び出しているuvc_acquire_privileges()、uvc_v4l2_set_format()はどちらもuvc_v4l2.cの関数ですが、uvc_v4l2_set_format()はその先でuvc_v4l2_try_format()を呼び、drivers/media/usb/uvc/uvc_video.cのuvc_probe_video()を呼びます。さらにuvc_set_video_ctrl()、__uvc_query_ctrl()と呼び出し、ここでようやくUSBのコアな関数usb_control_msg()を呼び出します。

より詳細に書く場合は、先ほどのシーケンスに全ての関数呼び出しを書いていけばよいです。関数呼び出しを全て順番に記述していってみます。

@startuml
participant "User Space" as user
participant "v4l2-ioctl.c" as v4l2
participant "uvc_v4l2.c" as uvc
participant "uvc_video.c" as uvc_video
participant "usb/core/message.c " as usb_core

user -> v4l2: ioctl(fd, V4L2_S_FMT, *v4l2_format)
alt v4l2_format->type == V4L2_BUF_TYPE_VIDEO_CAPTURE
    v4l2 -> uvc: uvc_ioctl_s_fmt_vid_cap()
    uvc -> uvc: uvc_v4l2_try_format()
    uvc -> uvc_video:  uvc_probe_video()
    uvc_video -> uvc_video: uvc_set_video_ctrl()
    uvc_video -> uvc_video: __uvc_query_ctrl()
    uvc_video -> usb_core: usb_control_msg()
end
@enduml

ここまで読むと、ユーザランドからioctl()呼び出された後、USBデバイスにコマンドが投げられるまでの大まかな構造が見えてきます。また、uvc_v4l2.cがv4l2のioctl()処理を、uvc_video.cがUSB通信に係るメッセージ処理を行う役割なんだろうと予想できます。

まとめ

本記事では、Cで書かれたLinuxのソースコードを例に、PlantUMLの紹介をしました。

個人的には、コールスタックをメモ書きするような要領で直感的に書けるので好きです。Javaが必要だったり、毎回ビルドする必要があるなど(これはAtomやVSCodeのプラグインで解決できるので他の記事を調べてみてください)がネックでしょうか。ですが、テキストで記述できる点はものすごく大きいメリットです。

設計でPlantUMLを使う例はいくつものサイトで紹介されているのですが、こうして解析に使う例はあまり見当たらなかったので書いてみました。腰を据えてソースを読みたいときに、ぜひ活用してみてください。

なお、AtomやVSCodeのプラグインを利用すれば、いちいちビルドコマンドをたたかなくとも自動でプレビュー生成できるようになります。そんな紹介記事もたくさんありますので、検索してみてください。