linuxカーネル内部インターフェースの変更例

はじめに

Linuxカーネル(以下カーネルと表記)の外部ユーザ空間とのインターフェースはシステムコールが増えることはあっても既存のものが変更されることはほとんどなく、極力互換性が保たれるようになっています。しかしカーネル内部のインターフェースはめまぐるしく変わります1。本記事ではその一例として、カーネル内で一定時間後に所定の処理を呼び出すタイマーという機能のインターフェースが変更された話、およびその影響について紹介いたします。

何もしてないのにビルドできなくなった

筆者が昔々、およそ8年前書いた以下のカーネルモジュールのコードを本日カーネルv4.18のモジュールとしてビルドすると★★★と書いた行でエラーが出ました。

#include <linux/module.h>
#include <linux/timer.h>

MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("Satoru Takeuchi <satoru.takeuchi@gmail.com>");
MODULE_DESCRIPTION("timer kernel module: oneshot timer");

struct timer_list mytimer;

#define MYTIMER_TIMEOUT_SECS    10

static void mytimer_fn(unsigned long arg)
{ 
        printk(KERN_ALERT "10 secs passed.\n");
}

static int mymodule_init(void)
{
        init_timer(&mytimer);                                     ★★★
        mytimer.expires = jiffies + MYTIMER_TIMEOUT_SECS*HZ;
        mytimer.data = 0;
        mytimer.function = mytimer_fn;
        add_timer(&mytimer);

        return 0;
}

static void mymodule_exit(void)
{ 
        del_timer(&mytimer);
}

module_init(mymodule_init);
module_exit(mymodule_exit);

エラー内容は「init_timer()という関数は存在しない」というものでした。この関数はタイマーを初期化するためのものです。上記コードを書いた時点では古いカーネルについてビルドは成功していたことから、この古いカーネルからv4.18までのある時点で当該関数が削除されたということが言えます。では、どういう理由でどのような流れでこうなったかをこれから紐解いていきましょう。

init_timer()が消えた理由

結論からいいますと、カーネル内のinit_time()を使っていた箇所はすべて新しいtimer_setup()という関数を使うように置き換えられています。その上でカーネル内部でユーザがいなくなったinit_timer()は削除されました。

事の発端はinit_timer()のインターフェイスセキュリティホールを生みやすいなどの色々問題があったことからtimer_setup()を追加するパッチが提案されて、それが公式カーネルにマージされたことです。

init_timer()とtimer_setup()の違いはざっくりいうと次の通りです。

  • タイマーが時間切れになったときに呼び出されるコールバック関数を、タイマーの初期化関数(timer_setup())を呼ぶときに引数として設定するようになった
  • コールバック関数の引数がタイマーを表すstruct timer_list型のデータのdataフィールドではなく当該データそのものへのポインタになった

コードを使って違いを説明します。タイマーを登録してから10秒後にmytimer_fn()を呼び出したいときにはinit_timer()は次のように使います。

...
struct timer_list mytimer;
...
{
        ...
        init_timer(&mytimer);
        mytimer.expires = jiffies + 10*HZ; // 10秒後にタイマーを起動させる。jiffiesは現在時刻を指す
        mytimer.data = 0;                  // 下記関数が呼び出されたときの引数として渡される値
        mytimer.function = mytimer_fn;     // コールバック関数
        add_timer(&mytimer);
        ...
}
...
static void mytimer_fn(unsigned long arg)
{ 
        ...
}

これに対してtimer_setup()は次のように使います。

...
struct timer_list mytimer;
...
{
        ...
        timer_setup(&mytimer, mytimer_fn, 0);  // 第三引数にはフラグを設定。ここでは説明を省略
        mytimer->expires = jiffies + 10*HZ;
        add_timer(&my_timer);
        ...
}
...
static void mytimer_fn(struct timer_list *t)   // タイマーが切れたときにはtはmy_timerを指す
{ 
        ...
}

このようなときにlinuxは過去のソフトウェアに多かった「今後はinit_timer()ではなくtimer_setup()を使ってね、すでにinit_timer()を使っているコードはそのまま動くよ」というやりかたではなく、「既存のinit_timer()呼び出しを全てtimer_setup()に全部置き換えた上でinit_time()を丸ごと削除するぜ」という大規模なリファクタリングをすることがよくあります。今回のタイマーの初期化についてはリファクタリングをする方向に舵が切られたというわけです。

移行は一度にやると間違いが発生したときのトラブルシューティングが大変なので、注意深く段階的に行われました。移行後は前述のようにinit_timer()は新たなユーザーが現れないように削除されました

細かい置換の過程に興味のあるかたはinclude/linux/timer.hkernel/time/timer.cをgit blameで調べるなどして深掘りしてみてください。みなさんが今後ご自身のコードをリファクタリングするときの参考になるかと思います。

移行による影響

移行による影響度はみなさんに関係のあるカーネル関係のコードが公式のカーネルソースにマージされているかどうかによって大きく異なります。マージされている場合は変更を施した人か、あるいはサブシステムメンテナなどの、そのコードに責任のある人が関連コードを変更します。それに対してマージされていない独自パッチや独自モジュールなどは自分で全部修正しなければいけません。後者の典型例は本記事の最初に述べた筆者独自のモジュールです。

カーネル関連コードを書く場合は公式カーネルにマージするほうが好ましい(ことが多い)理由の一つがこのようなインターフェースの変更などに追従するコストが減ることです。筆者のコードの場合は節題にした「何もしてないのにビルドできなくなった」ではなく、「何もしなかったからビルドできなくなった」というわけです。

おわりに

本記事ではlinuxカーネルの外部インターフェースは保たれるが内部インタフェースは保たれないということ、および、インターフェース変更による影響を最小化するにはコードを公式カーネルにマージしておく必要があることを述べました。今後みなさまがカーネルおよびカーネルモジュールの開発をするときの助けになれば幸いです。


  1. 詳細については公式ドキュメントをご覧ください