はじめてQiitaで記事を書きます。
見にくい部分があると思いますが、よろしくお願いいたします。
背景
いきなりですが、私は今大学でLinuxのシステムパフォーマンスに関する研究(Disk I/O周辺)を行っています。研究の都合上、カーネル自体を直接書き換えるのは困難な場面が多く、カーネルモジュールを書くことによりいろいろ実験や実装を行っているのが現状です。
Linuxカーネル(以下Linux)にはトレースツールと呼ばれる、内部の状態を解析できるような便利ツールがデフォルトで提供されています。
特に私のようなシステムパフォーマンスを題材にしている人間は、トレースポイントとよばれるLinux内にあらかじめ定義されているポイントを利用してデバッグやパフォーマンスのモニタリングを行います。
私の研究ではこのトレースポイントを利用したカーネルモジュールを書いているのですが、今回はそれにまつわるtipsとなっています。
環境
今回の記事で対象としているLinuxは次のバージョンとなっています
6.10
6.11
問題
次のソースコードはblock_rq_insertポイントに対しフックを仕掛けて、リクエスト情報を取得するプログラムとなっています。
#include <linux/module.h>
#include <linux/init.h>
#include <linux/blkdev.h>
#include <trace/events/block.h>
static void probe_block_rq_insert(void *ignore, struct request *rq)
{
const char *rwbs = "";
sector_t sector = blk_rq_pos(rq);
unsigned int nr_sectors = blk_rq_sectors(rq);
if (rq_data_dir(rq) == WRITE)
rwbs = "WRITE";
else if (rq_data_dir(rq) == READ)
rwbs = "READ";
else
rwbs = "OTHER";
printk(KERN_INFO "block_rq_insert: sector=%llu, nr_sectors=%u, rwbs=%s\n",
(unsigned long long)sector, nr_sectors, rwbs);
}
// モジュールの初期化
static int __init block_rq_insert_hook_init(void)
{
int ret;
ret = register_trace_block_rq_insert(probe_block_rq_insert, NULL);
if (ret)
{
pr_err("Failed to register probe for bolock_rq_insert\n");
return ret;
}
pr_info("blk_insert_test module loaded\n");
return 0;
}
// モジュール終了
static void __exit block_rq_insert_hook_exit(void)
{
unregister_trace_block_rq_insert(probe_block_rq_insert, NULL);
pr_info("blk_insert_test module unloaded\n");
}
module_init(block_rq_insert_hook_init);
module_exit(block_rq_insert_hook_exit);
MODULE_LICENSE("GPL");
こちらをmakeし、insmodでモジュールを入れてあげることによりリクエスト情報がdmesgよりわかるようになります。
ここで、block_rq_insertポイントについて少し解説を入れると、このポイントはI/Oがキューへ挿入される際に発行されるイベントとなっています。
私の研究では、デバイスドライバに送られる際の動作を知りたいという事情があるため、このポイントでは不適切です。
そのため、上記のポイントではなくblock_rq_issueと呼ばれるイベントにてフックを仕掛けようと考え、次のように実装を行いました。
#include <linux/module.h>
#include <linux/init.h>
#include <linux/blkdev.h>
#include <trace/events/block.h>
// プローブ関数
static void probe_block_rq_issue(void *ignore, struct request *rq)
{
const char *rwbs = "";
sector_t sector = blk_rq_pos(rq);
unsigned int nr_sectors = blk_rq_sectors(rq);
if (rq_data_dir(rq) == WRITE)
rwbs = "WRITE";
else if (rq_data_dir(rq) == READ)
rwbs = "READ";
else
rwbs = "OTHER";
printk(KERN_INFO "block_rq_issue: sector=%llu, nr_sectors=%u, rwbs=%s\n",
(unsigned long long)sector, nr_sectors, rwbs);
}
// モジュール初期化
static int __init block_rq_issue_hook_init(void)
{
int ret;
// トレースポイントをプローブ関数に登録
ret = register_trace_block_rq_issue(probe_block_rq_issue, NULL);
if (ret)
{
pr_err("Failed to register probe for block_rq_issue: %d\n", ret);
return ret;
}
pr_info("block_rq_issue_hook module loaded\n");
return 0;
}
// モジュール終了
static void __exit block_rq_issue_hook_exit(void)
{
// トレースポイントからプローブを解除
unregister_trace_block_rq_issue(probe_block_rq_issue, NULL);
pr_info("block_rq_issue_hook module unloaded\n");
}
module_init(block_rq_issue_hook_init);
module_exit(block_rq_issue_hook_exit);
MODULE_LICENSE("GPL");
実装内容はほとんど変わらないですね。
フックを仕掛けている部分であるregister周りのところだけが違うのみとなっています。
ではこちらのコードをビルドしてみるとどうなるのでしょうか?
usrname@ubuntuserver:~/kernel_module/blk_rq_issue$ make
make -C /lib/modules/6.10.1/build M=/home/usrname/kernel_module/blk_rq_issue modules
make[1]: Entering directory '/usr/src/linux-6.10.1'
MODPOST /home/usrname/kernel_module/blk_rq_issue/Module.symvers
ERROR: modpost: "__tracepoint_block_rq_issue" [/home/usrname/kernel_module/blk_rq_issue/blk_rq_issue_module.ko] undefined!
make[3]: *** [scripts/Makefile.modpost:145: /home/usrname/kernel_module/blk_rq_issue/Module.symvers] Error 1
make[2]: *** [/usr/src/linux-6.10.1/Makefile:1886: modpost] Error 2
make[1]: *** [Makefile:240: __sub-make] Error 2
make[1]: Leaving directory '/usr/src/linux-6.10.1'
make: *** [Makefile:4: all] Error 2
このようにビルド時にエラーを吐かれてしまいました。
重要なのはここの部分で
ERROR: modpost: "__tracepoint_block_rq_issue" [/home/usrname/kernel_module/blk_rq_issue/blk_rq_issue_module.ko] undefined!
なぜかblock_rq_issueポイントのシンボルが使えないと怒られています。
当然ですが、block_rq_issueポイント自体は使えていることを確認しておりますし、なぜかフックを仕掛けられない状況にずっと頭を悩ませていました。
解決策
さて、こちらの問題ですが解決策はいたって簡単であり、カーネルソースコードにexportするポイントを追加してビルドを行うことで解決できます。
EXPORT_TRACEPOINT_SYMBOL_GPL(block_bio_remap);
EXPORT_TRACEPOINT_SYMBOL_GPL(block_rq_remap);
EXPORT_TRACEPOINT_SYMBOL_GPL(block_bio_complete);
EXPORT_TRACEPOINT_SYMBOL_GPL(block_split);
EXPORT_TRACEPOINT_SYMBOL_GPL(block_unplug);
EXPORT_TRACEPOINT_SYMBOL_GPL(block_rq_insert);
こちらがblock関係で使えるトレースポイントのEXPORTになっています。
先ほど挙げた問題の通り、block_rq_insertに関してはデフォルトで入っていますがblock_rq_issueについては入っていないことが確認できますね。
この部分に、追加したいトレースポイントを入れたうえでカーネルをビルドすることによりフックできなかったポイントに対しフックを仕掛けられるようになります。
まとめ
今回は研究対象の都合上block関係のみを話題にしましたが、ほかの部分でも同様の問題が起きるだろうと想像できます。
このような問題で詰まった場合は対象のトレースポイントがEXPORTの対象に入っているかどうかをソースコード上で探すとよいといえますね。