並列で処理した時のみカーネルパニックが発生するようなカーネルモジュールを作ってそれを修正してみる


🎄本記事は IPFactory Advent Calendar 2019 - Qiita 17日目の記事です🎄

はじめに

こんにちは。miyase256です。
今回は、並列で処理した時のみカーネルパニックが発生するようなカーネルモジュールを作り、そのバグを修正するまでの流れをまとめてみたいと思います。
「開発中のカーネルモジュールで並列処理関連のバグを踏んでしまい、そのバグの原因調査/修正のために、踏んだバグを再現する最小構成カーネルモジュールを作る必要ができた」というのが本記事の想定状況です。
そもそも並列処理関連のバグというだけで十分面白いデバッグが大変なのですが、更にその並列処理が行われるのがユーザ空間ではなくカーネル空間で、しかもクラッシュする地点がLinuxカーネルの中、といった、個人的にはめちゃくちゃ面白い手強いバグを、今回は生み出し退治していきたいと思います。

環境

Linux ubuntu19 5.0.0-37-generic #40-Ubuntu SMP Thu Nov 14 00:14:01 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux

カーネルモジュール開発の基礎をおさらい

まずはカーネルモジュール開発の基礎をさらっとおさらいします。
まずはMakefileを用意します。

.PHONY: all clean

obj-m += reproduce_mod.o

KVERSION := $(shell uname -r)
KDIR := /lib/modules/$(KVERSION)
KBUILD := $(KDIR)/build
PWD := $(shell pwd)

all:
    @make -C $(KBUILD) M=$(PWD) modules

clean:
    rm -rf  *.o *.ko *.mod.c *.symvers *.order .tmp_versions
    rm -rf .reproduce_mod.*

私はよくこのようなMakefileを使います。
バグを再現するカーネルモジュールなので、名前はreproduce_mod.koにします。
とりあえずhello, worldさせてみましょう。以下のコードをビルドしてください。

#include <linux/module.h>

MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("Taisei Miyagawa <[email protected]>");
MODULE_DESCRIPTION("My kernel module");

static int my_init(void) {
    printk(KERN_INFO "Hello, world!\n");
    return 0;
}

static void my_exit(void) {
    printk(KERN_INFO "Bye, world!\n");
}

module_init(my_init);
module_exit(my_exit);

MODULE_LICENSEにはライセンスを、MODULE_AUTHORには開発者情報を、MODULE_DESCRIPTIONには簡潔なプログラムの説明を記述します。
module_initの引数に渡された関数がカーネルモジュールロード時に、module_exitの引数に渡された関数がカーネルモジュールアンロード時に実行されます。

$ make
make[1]: Entering directory '/usr/src/linux-headers-5.0.0-37-generic'
  Building modules, stage 2.
  MODPOST 1 modules
make[1]: Leaving directory '/usr/src/linux-headers-5.0.0-37-generic'

ビルドが成功したらinsmodコマンドでロードしてみましょう。

$ sudo insmod ./reproduce_mod.ko

エラーが出ずに正常終了したら、dmesgコマンドでカーネルが出力したメッセージを見てみましょう。

$ dmesg
[ 4350.065334] Hello, world!

カーネルモジュールにおけるprintk関数は、このように、dmesgで表示されるカーネルメッセージに出力されます。この出力から、insmod時に正しくmy_init関数が呼ばれていることが確認できました。
では、rmmodコマンドでカーネルモジュールをアンロードしてみましょう。

$ sudo rmmod reproduce_mod
$ dmesg
[ 4350.065334] Hello, world!
[ 4355.913743] Bye, world!

dmesgで見てみると、正しくmy_exit関数が呼ばれていることが確認できました。
これで、カーネルモジュール開発のおさらいは終わりになります。

バグを含むカーネルモジュールを書く

それではさっそく、並列で処理した時のみ発生するバグを含んだカーネルモジュールを開発していきます。

どのように並列処理させるか

カーネル空間で並列処理を行うためには、大きく分けて「並列処理を用いて何かを行うカーネルモジュールを実装する」かもしくは「カーネルモジュールの機能をユーザ空間から並列処理で呼び出す」かのどちらかを選択する必要があります。どちらでもよいのですが、今回は前者の方法で取り組むことにします。

どのようなバグを再現するか

次に考えることは、どのようなバグを再現するかです。並列で処理したときのみ発生するバグといっても、発生させる方法はたくさんあります。
ただ単にデッドロックやレースコンディションを起こすだけのシンプルすぎるカーネルモジュールでは、楽しくありませんよね。ということで、今回は、Linuxカーネルのワークキューという機能を(間違った使い方で)使って、レースコンディションを発生させてみようと思いますウキウキ。

ワークキューとは

Linux2.4における「タスクキュー」の代替としてLinux2.6で導入されたシステムで、ワークキューにenqueueされたカーネル関数はワーカスレッドと呼ばれる特別なカーネルスレッドで順次実行されるようになっています。
この機能を使う目的はいくつかあるのですが、最も代表的なのは、Linuxカーネル/カーネルモジュールの割り込み処理におけるbottom half(後半部)の実装でしょう。
割り込みには、「ハンドラ内で時間のかかる処理をどう実行するか」という重要な問題があります。
この問題に対応しなければ、割り込みが長時間ブロックされたままになってしまい、パフォーマンスが著しく劣化してしまいます。
Linuxカーネルでは、「割り込みハンドラを二つに分離する」ことでこの問題を解決しました。
この半分にしたうちの前半部分が「top half(前半部)」で、もう一方の後半部分が「bottom half(後半部)」です。
top halfはデバイスデータの固有のバッファへの保存、bottom halfのスケジュールなどのみを行い、一瞬で終わるようになっています。bottom halfはそれ以外の必要な処理を全て担当し、後のもっと安全な時間に実行されるようになっています。
このbottom halfのスケジュールに、ワークキューが用いられています。
同じ目的で「遅延処理」という機能が用いられることもありますが、ワークキューとは別物です。大きな違いは、「遅延処理は割り込みコンテキストで動作し、ワークキューの関数はプロセスコンテキストで動作する」ということでしょう。プロセス切替のできない割り込みコンテキストと違い、プロセスコンテキストでは実行が中断する可能性のある関数を実行することができます。したがって、ワークキューで実行される関数はアトミックである必要がありません。
詳しくは解説しませんが、ワークキューはこの目的のほかにも、「waitが必要になった関数をワークキューに突っ込み次の処理へ進む」「処理の直列化」などの目的で使うこともできます。

ソースコード

では、バグのあるコードを見てみましょう。

#include <linux/module.h>
#include <linux/kthread.h>
#include <asm/delay.h>

#define WQ_NAME "my_wq"
#define TIMEOUT_SEC 5
#define THREADS_NR 5

MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("Taisei Miyagawa <[email protected]>");
MODULE_DESCRIPTION("kernel module to reproduce bug");

struct thread_data {
    unsigned int id;
    struct task_struct *tsk;
};

struct thread_data tdata_[THREADS_NR];
struct delayed_work dw;
struct workqueue_struct *my_wq = NULL;

void task(struct work_struct *work) {
    printk(KERN_INFO "This is task.\n");
}

int thread_func(void *data) {
    struct thread_data *tdata = (struct thread_data *)data;
    printk(KERN_INFO "I'm thread_func_%u\n", tdata->id);
    INIT_DELAYED_WORK(&dw, task);
    my_wq = alloc_workqueue(WQ_NAME, WQ_MEM_RECLAIM, 0);
    queue_delayed_work(
            my_wq, &dw,
            msecs_to_jiffies(TIMEOUT_SEC * 1000));
    while(!kthread_should_stop()) {
        schedule();
    }
    return 0;
}

static void run_kthreads(unsigned int n_threads) {
    unsigned int i = 0;
    for(i = 0; i < n_threads; i++) {
        tdata_[i].id = i;
        tdata_[i].tsk = kthread_run(thread_func, &tdata_[i], "thread_func_%u", i);
    }
}

static void stop_kthreads(unsigned int n_threads) {
    unsigned int i = 0;
    for (i = 0; i < n_threads; i++) {
        kthread_stop(tdata_[i].tsk);
        printk(KERN_INFO "bye thread_func_%u\n", tdata_[i].id);
        tdata_[i].tsk = NULL;
    }
}

static int __init my_init(void) {
    printk(KERN_INFO "Hello, world\n");
    run_kthreads(THREADS_NR);
    return 0;
}

static void __exit my_exit(void) {
    stop_kthreads(THREADS_NR);
    printk(KERN_INFO "Bye, world\n");
}

module_init(my_init);
module_exit(my_exit);

実行

それでは、このコードをmakeしてinsmodし、実行してみましょう。

$ make
$ sudo insmod ./reproduce_mod.ko

dmesgコマンドを実行してみると、次のような出力が見えます。

$ dmesg
[ 6565.428305] Hello, world
[ 6565.428436] I'm thread_func_0
[ 6565.432537] I'm thread_func_2
[ 6565.432691] I'm thread_func_1
[ 6565.433649] I'm thread_func_3
[ 6565.433662] I'm thread_func_4

ここから、並列処理でタスクが実行されていることがわかります。
そして、5秒経ったくらいでカーネルパニックによりOSがクラッシュしてしまうのではないでしょうか。

原因調査

それでは、このコードのどこが悪かったのか、調べていきます。
kexec/kdumpを用いて取得したKernel Crash Dumpを解析したり、クラッシュ時のdmesgを見たりすると、Linuxカーネルの include/linux/list.h 内部で落ちていることがわかります。
具体的には、run_timer_softirq() の中で呼ばれる __hlist_del() の中の next->pprev = pprev; の部分でカーネルパニックが発生しているようです。
そして、クラッシュ時のメモリ/レジスタの状態とLinuxカーネルのアセンブリコードを見比べてみると、next0xdead000000000200 になっているのが原因だとわかります。

ここで、「なぜ __hlist_del() が呼ばれたのか」を考えてみましょう。
これは、run_timer_softirq() の中で __hlist_del() が呼ばれていることから簡単にわかります。run_timer_softirq() というのは、タイマ割り込みが発生したときに実行される関数です。__hlist_del() というのは、双方向リストからノードを削除する関数です。したがって、ここから「タイマによる起爆でノードがリストから削除され、その削除時にクラッシュした」ということがわかります。

この時点で「 queue_delayed_work() が悪そう」だと気付いた方、鋭いですね。
はい、queue_delayed_work() という関数は、先程紹介した「ワークキュー」というLinuxカーネルの機能を使うためのカーネル関数で、ワークキューへのタスクのenqueueを行います。そして、この関数はただenqueueするだけではなく、保留中の関数を実行するまでの最小遅延を指定することができます。それが第3引数の msecs_to_jiffies(TIMEOUT_SEC * 1000) の部分です。この TIMEOUT_SEC はコードでは 5 として定義されており、5秒間は少なくとも遅延してdequeueされることがわかります。ここに、タイマが使われています。そして、実際に、このカーネルモジュールをinsmodしてから約5秒後にクラッシュします。明らかにこの部分が原因に見えますね。

ここまで聞いて、「 queue_delayed_work() の周りを排他制御すればいいんじゃね」と思った方、鋭いですが、惜しいですね。
実は、排他制御を追加するだけでは、このバグを取り除くことはできません。
ということで、なぜ queue_delayed_work() でクラッシュするのか、Linuxカーネルのコードを読んで調査していきましょう。

queue_delayed_work() が呼ばれると、内部で add_timer() が呼ばれます。
またこの更に内部では enqueue_timer() という関数が呼ばれており、その中では hlist_add_head() が呼ばれています。
ここで、 timer_base という構造体の struct hlist_head vectors[WHEEL_SIZE] という双方向リストに、timer_list 構造体の struct hlist_node entry というノードを追加しています。

タイマが期限を迎えると、 run_timer_softirq() が呼び出されます。
この関数は内部で expire_timers() を呼んでおり、更にこの中の detach_timer() という関数が __hlist_del() を呼んでいます。
ここで、先程のノード追加処理と真逆のノード削除処理が行われます。

では、リストへの追加と削除の流れを把握したところで、実際にカーネルモジュール内で queue_delayed_work() に渡している第1引数/第2引数も考慮しながら、メモリの状態をトレースしてみましょう。正確にトレースすることができたら、いくつかの重要な点に気付くかと思います。
detach_timer() の最後に、リストから削除したノードの next メンバに LIST_POISON2 を代入していることや、queue_delayed_work() に第2引数として渡している &dw のアドレスが毎回同じであり、それが影響して、双方向リストに同じノードがいくつも(並列数だけ)追加されてしまっているということなどです。

LIST_POISON2 の定義を追ってみましょう。
include/linux/poison.h で、#define LIST_POISON2 ((void *) 0x200 + POISON_POINTER_DELTA) と定義されています。この 0x200 、ピンと来ましたか? クラッシュの原因となった 0xdead000000000200 の末尾の 200 である可能性が極めて高く見えます。また、もしそうであれば、0xdead000000000000 の部分は POISON_POINTER_DELTA ではないかと推測できます。POISON_POINTER_DELTA の定義を探してみると、# define POISON_POINTER_DELTA _AC(CONFIG_ILLEGAL_POINTER_VALUE, UL) というのが見つかります。しかし、この CONFIG_ILLEGAL_POINTER_VALUE という定数、定義を探しても、0 としか見つかりません。
ここで、Linuxカーネルを読む上で非常に重要なtipsの一つを紹介します。このような、先頭が CONFIG_ から始まる定数をLinuxカーネル内で見かけた場合、configファイルの中を見てみましょう。
実は、このファイルはLinuxカーネルのリポジトリには入っていません。カーネルをビルドする時は、現在のカーネルに適応されているconfigをコピーして持ってきて使います(ただ持ってきて終わりではないので注意してください)。
ということで、現在のバージョンのconfigを見てみましょう。

$ cat /boot/config-$(uname -r) | grep "CONFIG_ILLEGAL_POINTER_VALUE"
CONFIG_ILLEGAL_POINTER_VALUE=0xdead000000000000

見つかりました。値も推測通りの 0xdead000000000000 です。
これで、クラッシュの原因となった next を作った命令が detach_timer() 内の entry->next = LIST_POISON2; であると、ほぼ確信することができました。

また、先程述べたように、 今回のコードでは、同じノードが複数回enqueueされてしまいます。つまり、entry->nextLIST_POISON2 になったノードを再びdequeueしようとするということです。このような操作をしてしまうと、__hlist_del() 内の if (next) というチェックをすり抜け、next->pprev = pprev; でアクセス違反が発生してしまう、ということになります。

バグ修正

毎回enqueueするタスクを変える

したがって、今回は、queue_delayed_work() の第2引数を毎回変えればクラッシュせずに済むようになります。その修正を加えたコードは以下になります。

#include <linux/module.h>
#include <linux/kthread.h>
#include <asm/delay.h>

#define WQ_NAME "my_wq"
#define TIMEOUT_SEC 5
#define THREADS_NR 5

MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("Taisei Miyagawa <[email protected]>");
MODULE_DESCRIPTION("kernel module to reproduce bug");

struct thread_data {
    unsigned int id;
    struct task_struct *tsk;
    struct delayed_work dw;
};

struct thread_data tdata_[THREADS_NR];
struct workqueue_struct *my_wq = NULL;

void task(struct work_struct *work) {
    printk(KERN_INFO "This is task.\n");
}

int thread_func(void *data) {
    struct thread_data *tdata = (struct thread_data *)data;
    printk(KERN_INFO "I'm thread_func_%u\n", tdata->id);
    INIT_DELAYED_WORK(&(tdata->dw), task);
    queue_delayed_work(
            my_wq, &(tdata->dw),
            msecs_to_jiffies(TIMEOUT_SEC * 1000));
    while(!kthread_should_stop()) {
        schedule();
    }
    return 0;
}

static void run_kthreads(unsigned int n_threads) {
    unsigned int i = 0;
    for(i = 0; i < n_threads; i++) {
        tdata_[i].id = i;
        tdata_[i].tsk = kthread_run(thread_func, &tdata_[i], "thread_func_%u", i);
    }
}

static void stop_kthreads(unsigned int n_threads) {
    unsigned int i = 0;
    for (i = 0; i < n_threads; i++) {
        kthread_stop(tdata_[i].tsk);
        printk(KERN_INFO "bye thread_func_%u\n", tdata_[i].id);
        tdata_[i].tsk = NULL;
    }
}

static int __init my_init(void) {
    printk(KERN_INFO "Hello, world\n");
        my_wq = alloc_workqueue(WQ_NAME, WQ_MEM_RECLAIM, 0);
    run_kthreads(THREADS_NR);
    return 0;
}

static void __exit my_exit(void) {
    stop_kthreads(THREADS_NR);
    printk(KERN_INFO "Bye, world\n");
}

module_init(my_init);
module_exit(my_exit);

ロードし、5秒後にdmesgを見てみましょう。

$ make
$ sudo insmod ./reproduce_mod.ko
$ dmesg
[ 4804.041540] Hello, world
[ 4804.043208] I'm thread_func_0
[ 4804.043489] I'm thread_func_1
[ 4804.044637] I'm thread_func_2
[ 4804.044868] I'm thread_func_3
[ 4804.050931] I'm thread_func_4
[ 4809.125920] This is task.
[ 4809.125926] This is task.
[ 4809.125934] This is task.
[ 4809.129920] This is task.
[ 4809.129923] This is task.

5秒経ってもクラッシュせずに正常終了することが確認できます。

しかし、(確かにこの修正が正しい場合も十分にあるのですが、)仮にこのカーネルモジュールの機能が「そもそも並列で実行されるべきではない」ものであり、それをユーザ空間から並列で呼び出してしまったと仮定すると、この修正は正しいものとは言えません。なぜなら、並列処理が成功してしまっているからです。

ひとつにする

そういった場合には、「順次実行されるようにする」か、もしくは「ひとつにする」というのが、王道の修正パターンになります。
それでは、今回は後者の「ひとつにする」という方向の修正も行ってみましょう。

#include <linux/module.h>
#include <linux/kthread.h>
#include <asm/delay.h>

#define WQ_NAME "my_wq"
#define TIMEOUT_SEC 5
#define THREADS_NR 5

MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("Taisei Miyagawa <[email protected]>");
MODULE_DESCRIPTION("kernel module to reproduce bug");

struct thread_data {
    unsigned int id;
    struct task_struct *tsk;
};

struct thread_data tdata_[THREADS_NR];
struct delayed_work dw;
struct workqueue_struct *my_wq = NULL;
bool enqueued = false;
struct mutex lock;

void task(struct work_struct *work) {
    printk(KERN_INFO "This is task.\n");
}

void dequeue_if_enqueued(void) {
    mutex_lock(&lock);
    if(!enqueued) {
        mutex_unlock(&lock);
        return;
    }
    cancel_delayed_work_sync(&dw);
    enqueued = false;
    mutex_unlock(&lock);
}

void enqueue_if_not_enqueued(void) {
    mutex_lock(&lock);
    if (enqueued) {
        mutex_unlock(&lock);
        return;
    }
    INIT_DELAYED_WORK(&dw, task);
    queue_delayed_work(
            my_wq, &dw,
            msecs_to_jiffies(TIMEOUT_SEC * 1000));
    enqueued = true;
    mutex_unlock(&lock);
}

int thread_func(void *data) {
    struct thread_data *tdata = (struct thread_data *)data;
    printk(KERN_INFO "I'm thread_func_%u\n", tdata->id);
    dequeue_if_enqueued();
    enqueue_if_not_enqueued();
    while(!kthread_should_stop()) {
        schedule();
    }
    return 0;
}

static void run_kthreads(unsigned int n_threads) {
    unsigned int i = 0;
    for(i = 0; i < n_threads; i++) {
        tdata_[i].id = i;
        tdata_[i].tsk = kthread_run(thread_func, &tdata_[i], "thread_func_%u", i);
    }
}

static void stop_kthreads(unsigned int n_threads) {
    unsigned int i = 0;
    for (i = 0; i < n_threads; i++) {
        kthread_stop(tdata_[i].tsk);
        printk(KERN_INFO "bye thread_func_%u\n", tdata_[i].id);
        tdata_[i].tsk = NULL;
    }
    dequeue_if_enqueued();
}

static int __init my_init(void) {
    printk(KERN_INFO "Hello, world\n");
    my_wq = alloc_workqueue(WQ_NAME, WQ_MEM_RECLAIM, 0);
    mutex_init(&lock);
    run_kthreads(THREADS_NR);
    return 0;
}

static void __exit my_exit(void) {
    stop_kthreads(THREADS_NR);
    printk(KERN_INFO "Bye, world\n");
}

module_init(my_init);
module_exit(my_exit);

ロードしてdmesgを見てみます。

$ make
$ sudo insmod ./reproduce_mod.ko
$ dmesg
[ 7270.406379] Hello, world
[ 7270.407155] I'm thread_func_0
[ 7270.407762] I'm thread_func_1
[ 7270.407886] I'm thread_func_2
[ 7270.408012] I'm thread_func_3
[ 7270.418876] I'm thread_func_4
[ 7275.672417] This is task.

enqueued という、enqueueされているかどうかを表すbool変数を別途用意し、dequeue_if_enqueued()enqueue_if_not_enqueued() という二つの関数を用いて、「既にenqueueされている状態でenqueueしようとした場合はdequeueしてからenqueueする」という処理をするように変更しました。
これで、いくら並列化されようと、たった一つのタスクのみを実行するようになりました。
enqueue/dequeue処理中に enqueued 変数の状態(true/false)が "あるべき状態" であることを保証するため、mutexロックによる排他制御を追加しました。

(補足: 既にリストから取り除いたタイマに対して del_timer() を実行しても問題ありません)

デッドロック

次に、task() にロックを必要とする処理を追加したと仮定します。今回は簡単のため、task() 全体をロックすることにします。
すると途端に、このコードは "まずいコード" になってしまいます。
どこが "まずい" のかと言うと、cancel_delayed_work_sync()task() がデッドロックしてしまう可能性ができてしまうのです。
詳しく解説します。
dequeue_if_enqueued() が呼ばれ、またその内部で cancel_delayed_work_sync() が呼ばれた直後にタイマが期限を迎え、task() がロックを獲得しようとした時、既に dequeue_if_enqueued() がロックを持ってしまっているため task() はロックを獲得できずwaitします。しかし、cancel_delayed_work_sync() は、既にタスク(今回は task() のことを指す)が実行されている場合はそのタスクの終了を待つという仕様になっています。これにより、task()cancel_delayed_work_sync() もお互いの終了を待ってしまい、デッドロックが発生することになります。
この問題を解決するためには、cancel_delayed_work_sync() を実行する間の一瞬だけロックを外し、その一瞬をレースコンディションから守るようにフローを制御する必要があります。この修正を加えたコードが以下になります。

#include <linux/module.h>
#include <linux/kthread.h>
#include <asm/delay.h>

#define WQ_NAME "my_wq"
#define TIMEOUT_SEC 5
#define THREADS_NR 5

MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("Taisei Miyagawa <[email protected]>");
MODULE_DESCRIPTION("kernel module to reproduce bug");

struct thread_data {
    unsigned int id;
    struct task_struct *tsk;
};

struct thread_data tdata_[THREADS_NR];
struct delayed_work dw;
struct workqueue_struct *my_wq = NULL;
bool enqueued = false;
bool dequeuing = false;
struct mutex lock;

void task(struct work_struct *work) {
    mutex_lock(&lock);
    printk(KERN_INFO "This is task.\n");
    mutex_unlock(&lock);
}

void dequeue_if_enqueued(void) {
    mutex_lock(&lock);
    if(!enqueued || dequeuing) {
        mutex_unlock(&lock);
        return;
    }
    dequeuing = true;
    mutex_unlock(&lock);
    cancel_delayed_work_sync(&dw);
    mutex_lock(&lock);
    enqueued = false;
    dequeuing = false;
    mutex_unlock(&lock);
}

void enqueue_if_not_enqueued(void) {
    mutex_lock(&lock);
    if (enqueued) {
        mutex_unlock(&lock);
        return;
    }
    INIT_DELAYED_WORK(&dw, task);
    queue_delayed_work(
            my_wq, &dw,
            msecs_to_jiffies(TIMEOUT_SEC * 1000));
    enqueued = true;
    mutex_unlock(&lock);
}

int thread_func(void *data) {
    struct thread_data *tdata = (struct thread_data *)data;
    printk(KERN_INFO "I'm thread_func_%u\n", tdata->id);
    dequeue_if_enqueued();
    enqueue_if_not_enqueued();
    while(!kthread_should_stop()) {
        schedule();
    }
    return 0;
}

static void run_kthreads(unsigned int n_threads) {
    unsigned int i = 0;
    for(i = 0; i < n_threads; i++) {
        tdata_[i].id = i;
        tdata_[i].tsk = kthread_run(thread_func, &tdata_[i], "thread_func_%u", i);
    }
}

static void stop_kthreads(unsigned int n_threads) {
    unsigned int i = 0;
    for (i = 0; i < n_threads; i++) {
        kthread_stop(tdata_[i].tsk);
        printk(KERN_INFO "bye thread_func_%u\n", tdata_[i].id);
        tdata_[i].tsk = NULL;
    }
    dequeue_if_enqueued();
}

static int __init my_init(void) {
    printk(KERN_INFO "Hello, world\n");
    my_wq = alloc_workqueue(WQ_NAME, WQ_MEM_RECLAIM, 0);
    mutex_init(&lock);
    run_kthreads(THREADS_NR);
    return 0;
}

static void __exit my_exit(void) {
    stop_kthreads(THREADS_NR);
    printk(KERN_INFO "Bye, world\n");
}

module_init(my_init);
module_exit(my_exit);

dequeuing という、dequeue処理が走っている最中であるかどうかを示すbool変数を追加し、複数の cancel_delayed_work_sync() が同時に実行されないように制御するようにしました。

enum化

enqueued / dequeuing という二つのbool変数では、以下の4パターンの可能性があります。

  • enqueued: false, dequeuing: false
  • enqueued: true, dequeuing: true
  • enqueued: true, dequeuing: false
  • enqueued: false, dequeuing: true

しかし、状態遷移図を書いてみると、このうち以下の3パターンしか使われていないことがわかります。

  • enqueued: false, dequeuing: false
  • enqueued: true, dequeuing: false
  • enqueued: true, dequeuing: true

ということで、二つのbool変数から、三つの状態を持つ一つのenumに変更しました。

#include <linux/module.h>
#include <linux/kthread.h>
#include <asm/delay.h>

#define WQ_NAME "my_wq"
#define TIMEOUT_SEC 5
#define THREADS_NR 5

MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("Taisei Miyagawa <[email protected]>");
MODULE_DESCRIPTION("kernel module to reproduce bug");

struct thread_data {
    unsigned int id;
    struct task_struct *tsk;
};

struct thread_data tdata_[THREADS_NR];
struct delayed_work dw;
struct workqueue_struct *my_wq = NULL;
enum {
    NOT_ENQUEUED = 0,
    ENQUEUED_AND_NOT_DEQUEUING,
    ENQUEUED_AND_DEQUEUING,
};
u8 state = NOT_ENQUEUED;
struct mutex lock;

void task(struct work_struct *work) {
    mutex_lock(&lock);
    printk(KERN_INFO "This is task.\n");
    mutex_unlock(&lock);
}

void dequeue_if_enqueued(void) {
    mutex_lock(&lock);
    if (state != ENQUEUED_AND_NOT_DEQUEUING) {
        mutex_unlock(&lock);
        return;
    }
    state = ENQUEUED_AND_DEQUEUING;
    mutex_unlock(&lock);
    cancel_delayed_work_sync(&dw);
    mutex_lock(&lock);
    state = NOT_ENQUEUED;
    mutex_unlock(&lock);
}

void enqueue_if_not_enqueued(void) {
    mutex_lock(&lock);
    if (state != NOT_ENQUEUED) {
        mutex_unlock(&lock);
        return;
    }
    INIT_DELAYED_WORK(&dw, task);
    queue_delayed_work(
            my_wq, &dw,
            msecs_to_jiffies(TIMEOUT_SEC * 1000));
    state = ENQUEUED_AND_NOT_DEQUEUING;
    mutex_unlock(&lock);
}

int thread_func(void *data) {
    struct thread_data *tdata = (struct thread_data *)data;
    printk(KERN_INFO "I'm thread_func_%u\n", tdata->id);
    dequeue_if_enqueued();
    enqueue_if_not_enqueued();
    while(!kthread_should_stop()) {
        schedule();
    }
    return 0;
}

static void run_kthreads(unsigned int n_threads) {
    unsigned int i = 0;
    for(i = 0; i < n_threads; i++) {
        tdata_[i].id = i;
        tdata_[i].tsk = kthread_run(thread_func, &tdata_[i], "thread_func_%u", i);
    }
}

static void stop_kthreads(unsigned int n_threads) {
    unsigned int i = 0;
    for (i = 0; i < n_threads; i++) {
        kthread_stop(tdata_[i].tsk);
        printk(KERN_INFO "bye thread_func_%u\n", tdata_[i].id);
        tdata_[i].tsk = NULL;
    }
    dequeue_if_enqueued();
}

static int __init my_init(void) {
    printk(KERN_INFO "Hello, world\n");
    my_wq = alloc_workqueue(WQ_NAME, WQ_MEM_RECLAIM, 0);
    mutex_init(&lock);
    run_kthreads(THREADS_NR);
    return 0;
}

static void __exit my_exit(void) {
    stop_kthreads(THREADS_NR);
    printk(KERN_INFO "Bye, world\n");
}

module_init(my_init);
module_exit(my_exit);

思い付く範囲ではこの程度でしょうか。
これ以上は何のアイデアも思い浮かばないため、ここでこのコードの改良はひとまず終わりにします。
ここまで読んでいただき、ありがとうございました。