人間とウェブの未来

「ウェブの歴史は人類の歴史の繰り返し」という観点から色々勉強しています。

カーネルスレッドのループと停止をカーネルモジュールで実装

以前Linuxのカーネルスレッドがループして暴走したときに、カーネルスレッドの扱いを調べていた時期があって、それの簡単な動きを再現するべくカーネルモジュールを作りました。まずは、自力で試したい人は以下を見ずに試すと良いでしょう。

まずはカーネルスレッドをループさせる

カーネルスレッドは、所謂ps上で[kthread_dayo]みたいな感じで見えるプロセスの事です。厳密にはユーザランドのプロセスも定期的にカーネル側の処理が走るときは[]付けになったりするのですが、簡単には上記のような状態とします。これをユーザランドのプログラミングで作ることは難しいのですが、カーネルモジュールを使えばすごく簡単につくれます。

まずは以下のようにカーネルスレッドをループさせるコードを書きます。

#include <linux/module.h>
#include <linux/sched.h>
#include <linux/kthread.h>

MODULE_AUTHOR("matsumotory");
MODULE_DESCRIPTION("run kthread");
MODULE_LICENSE("GPL");

struct task_struct *k;

static int kthread_cb(void *arg)
{
  printk("[%s] running as kthread\n", k->comm);
  while(!kthread_should_stop()) {
    schedule();
  }

  return 0;
}

static int __init run_kthread_init(void)
{
  k = kthread_create(kthread_cb, NULL, "matsumotory");
  printk(KERN_INFO "[%s] wake up as kthread\n", k->comm);
  wake_up_process(k);

  return 0;
}

static void __exit run_kthread_exit(void)
{
  printk(KERN_INFO "[%s] stop kthread\n", k->comm);
  kthread_stop(k);
}

module_init(run_kthread_init);
module_exit(run_kthread_exit);
API Training Shop Blog About

そして、これを動作しているkernelとそれに該当するkernelのコードを持ってきた上で以下のようなMakefileを作ります。

obj-m := run_kthread.o

# ここにカーネルソースがある
ROOTDIR  := /home/matsumotory/linux-4.2.3/
PWD   := $(shell pwd)

default:
  $(MAKE) -C $(ROOTDIR) M=$(PWD) modules

clean:
  rm -f *.o *.ko

これで、以下のようにビルドしてからできたkernelモジュールを読み込みましょう。

make
insmod run_kthread.ko
lsmod | grep run_kthread

すると、以下のようにループするkernelスレッドが動きますわーい。

$ ps auwx | grep "\[matsumotory\]"
root     21747  102  0.0      0     0 ?        R    13:33   0:13 [matsumotory]

また、dmesgには以下のようなログが吐かれます。

[ 1139.247589] [matsumotory] wake up as kthread
[ 1139.247603] [matsumotory] running as kthread

カーネルスレッドを殺す

このループしたカーネルスレッドをユーザランド側から殺そうとしても死にません。なんせカーネルスレッドだし実装的にもシグナルとかガン無視していますし、あれこれ殺せない!!となるかもしれません。

でも安心して下さい。次はこのカーネルスレッドをカーネル側から殺すカーネルモジュールを作ってみましょう。まずは以下のようにカーネルモジュールのコードを書きます。

#include <linux/module.h>
#include <linux/debugfs.h>
#include <linux/sched.h>
#include <linux/pid.h>
#include <linux/kthread.h>
#include <asm-generic/uaccess.h>

MODULE_AUTHOR("matsumotory");
MODULE_DESCRIPTION("kill kthread");
MODULE_LICENSE("GPL");

static int file_value;
static struct dentry *dentry_file;
struct task_struct *k;

static ssize_t kill_kthread_by_pid(struct file *file, const char __user *ubuf, size_t len, loff_t *ppos)
{
  pid_t pid;
  char kbuf[64];
  struct pid *pid_s;

  if (len > sizeof(kbuf) -1) {
    printk(KERN_INFO "kill_kthread_by_pid: invalid pid\n");
    return -EINVAL;
  }

  if (copy_from_user(kbuf, ubuf, len)) {
    printk(KERN_INFO "kill_kthread_by_pid: copy from user land to kernel failed\n");
    return -EFAULT;
  }

  kbuf[len] = '\0';

  if (!(pid = simple_strtoul(kbuf, NULL, 10))) {
    printk(KERN_INFO "kill_kthread_by_pid: convert pid failed, invalid pid\n");
    return -EINVAL;
  }

  if (!(pid_s = find_get_pid(pid))) {
    printk(KERN_INFO "kill_kthread_by_pid: not found pid struct\n");
    return -ESRCH;
  }

  if (!(k = get_pid_task(pid_s, PIDTYPE_PID))) {
    printk(KERN_INFO "kill_kthread_by_pid: not found task struct\n");
    put_pid(pid_s);
    return -ESRCH;
  }

  printk(KERN_INFO "[%s=%d] kill kthread by kthread_stop\n", k->comm, pid);
  kthread_stop(k);

  return len;
}

static struct file_operations fops = {
  .write = kill_kthread_by_pid,
};

static int __init kill_kthread_module_init(void)
{
  dentry_file = debugfs_create_file("kill_kthread", 0644, NULL, &file_value, &fops);

  if (!dentry_file) {
    printk(KERN_ERR "kill_kthread_module_init failed");
    return -ENODEV;
  }
    
  return 0;
}

static void __exit kill_kthread_module_exit(void)
{
  debugfs_remove(dentry_file);
}

module_init(kill_kthread_module_init);
module_exit(kill_kthread_module_exit);

そして、以下の様なMakefileを作ります。

obj-m := kill_kthread.o

ROOTDIR  := /home/matsumotory/linux-4.2.3/
PWD   := $(shell pwd)

default:
  $(MAKE) -C $(ROOTDIR) M=$(PWD) modules

clean:
  rm -f *.o *.ko

そしてカーネルモジュールをビルドしてロードしましょう。

make
insmoda kill_kthread.ko
lsmod | grep kill_kthread

そして、このカーネルモジュールの使い方は、カーネルスレッドのPIDを指定してそれをdebugfs上のファイルに渡す事で内部的にカーネルを停止させるフラグをたてるような実装にしています。

なので、debugfsをマウントしていない方は以下のようにマウントして、

mount -t debugfs nodev /sys/kernel/debug/

上記でループさせているカーネルスレッドのPIDを取得してから以下のようにファイルに書き込みましょう。

echo $pid  > /sys/kernel/debug/kill_kthread

するとわーい、ループしていたカーネルスレッドが消えてなくなったと思います。やった、カーネルスレッドをコントロールした気分になったぞ!!

コードとしてはようするに、ループさせるコードの中で以下のようにカーネルスレッドがループする条件を以下のように実装しています。

  while(!kthread_should_stop()) {
    schedule();
  }

そして、停止させるカーネルモジュールのコードは渡されたPIDから対応するカーネルスレッドを特定し、

kthread_stop(k);

によってkthread_should_stopでチェックされるフラグをカーネル側から立てます。これによって、ループの条件が偽となりループが止まるわけです。実際にカーネルのコードを読んでもらうと、kthread_should_stopkthread_stopがPIDに紐づく構造体から辿れるフラグを操作している事がわかるでしょう。

ちなみに、どちらのカーネルモジュールも以下のコマンドでリムーブすることももちろん可能です。

rmmod run_kthread
lsmod | grep run_kthread
rmmod kill_kthread
lsmod | grep kill_kthread

dmesgでカーネルスレッドがストップされた旨が出力されます。

[ 1148.646109] [matsumotory] stop kthread

ps上もいませんね。

$ ps auwx | grep "\[matsumotory\]"

まとめ

ということで、カーネルスレッドってなんかあったなぁという所から、それを実際にカーネルモジュールで操作してみると意外と簡単にできることがわかりますね。

こうやって最初は簡単な実装からはじめて、徐々に深い所へとコード量を増やしながら理解を深めていくと、カーネルと親しみを持ちつつ勉強できるかもしれません。