shimada-kの日記

ソフトウェア・エンジニアのブログです

clistを使ってカーネル側とユーザ側で通信させてみました

以前の記事で作成したカーネル側の循環リストライブラリ(clist)を使って、実際にカーネル空間とユーザ空間でデータ通信をしてみました。

LinuxカーネルではftraceやSystemTapなどのトレーサが使えます。両者とも非常にすばらしいツールなのですが、リングバッファを使用しているので

  1. ある程度長い期間で一連のデータを発生順序を保証してトレースするには不向き
  2. テキストI/Fなので、二次的な可視化にはパーサが必要

という課題もあるように思います。

ということで、発生順序を保証しつつ長い期間でトレースするためにclistを使い、Excelやその他の可視化ツールで扱いやすいように、バイナリ形式でトレースしたデータを通信する仕組みを考えました。

clistはカーネルツリーに仕込んであって、ビルド済みという前提です(仕込み方はコチラ)。通信にはキャラクタデバイスとシグナルを使うことにしました。

f:id:shimada-k:20111227154822p:image:w360

カーネル側では

  • カーネルイベントが発生する箇所で循環リストへオブジェクトがpushされる
  • ドライバコード内で定期的に循環リストをポーリングして、データが溜まっていたら一時メモリにpullする
  • ユーザアプリにシグナルを投げる

ユーザ側では

  • ドライバ側にioctl(2)して自分のPID、シグナル番号、その他必要な設定情報を登録する
  • カーネルからシグナルが届くとシグナルハンドラ内でドライバからデータをread(2)し、ファイルにwrite(2)する

カーネル側もユーザ側もコード内で冒頭の「イベント固有の設定」の箇所を変更すればそのまま適用可能なコードになっています。

まずはカーネル側のコードです。キャラクタデバイスを作成するのと、システムコールの実装があります。全部だと長いので本質的な部分だけ載せておきます。

環境:Linux debian 3.0.0-amd64 [debian wheezy]

clist_benchmark.c

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/timer.h>
#include <linux/proc_fs.h>	/* alloc_chrdev_region */
#include <linux/sched.h>

#include <asm/uaccess.h>	/* copy_from_user, copy_to_user */
#include <linux/cdev.h>	/* cdev_init */
#include <linux/ioctl.h>	/* _IO* */
#include <linux/cpumask.h>	/* cpumask_weight() */

#include <linux/clist.h>	/* 自作循環リストライブラリ */

#define MODNAME "clist_benchmark"
#define MINOR_COUNT 1

enum signal_status{
	SIG_READY,
	SIGRESET_REQUEST,
	SIGRESET_ACCEPTED,
	MAX_STATUS
};

/* ユーザからデバイス初期化時に送られるデータ構造 */
struct ioc_submit_spec{
	int pid;
	int signo, flush_period;
	int nr_node, node_nr_composed;
	int dummy;
};

struct signal_spec{	/* ユーザ空間とシグナルで通信するための管理用構造体 */
	enum signal_status sr_status;
	int signo, flush_period;
	struct siginfo info;
	struct task_struct *t;
	struct timer_list flush_timer;
};

static char *log_prefix = "module[clist_benchmark]";

static dev_t dev_id;  /* デバイス番号 */
static struct cdev c_dev; /* キャラクタデバイス用構造体 */

static struct clist_controller *clist_ctl;
static struct signal_spec sigspec;

/* プロトタイプ宣言 */
extern int send_sig_info(int sig, struct siginfo *info, struct task_struct *p);
static int clbench_open(struct inode *inode, struct file *filp);
static int clbench_release(struct inode* inode, struct file* filp);
static ssize_t clbench_read(struct file* filp, char* buf, size_t count, loff_t* offset);
static long clbench_ioctl(struct file *flip, unsigned int cmd, unsigned long arg);

/*
	システムコールを担当する関数たち
*/
static struct file_operations clbench_fops = {
	.owner   = THIS_MODULE,
	.open    = clbench_open,
	.release = clbench_release,
	.read    = clbench_read,
	.write   = NULL,
	.unlocked_ioctl   = clbench_ioctl,	/* kernel 2.6.36以降はunlocked_ioctl */
};


/**********************************************************
*
*	イベント固有の設定
*	扱うイベントに合わせて変更が必要な箇所
*
**********************************************************/

/*
	注意!	・ユーザ空間とやりとりするオブジェクトはパッディングが発生しない構造にすること
		・この構造体のメンバのsizeof()の合計がsizeof(struct object)と一致するようにすること

		取得したいイベントにあわせてこの構造体とclbench_add_object()を作成する
*/
struct object{
	unsigned long i_ino;
	long long ppos;
	long sec, usec;
};

/*
	balance_tasks()@sched.cで呼び出される関数 ロードバランスが行われている箇所で呼び出される
	@p ロードバランスされたtask_structのアドレス
	@src_cpu 最も忙しいCPU番号
	@this_cpu ロードバランス先のCPU番号

	※カーネルイベントが補足される箇所にこの関数を挿入する
*/
void clbench_add_object(unsigned long i_ino, long long ppos)
{
	struct object obj;
	struct timeval t;

	if(sigspec.sr_status != SIG_READY){	/* シグナルを送信できる状態かどうか */
		return;
	}

	do_gettimeofday(&t);

	obj.i_ino = i_ino;
	obj.ppos = ppos;
	obj.sec = (long)t.tv_sec;
	obj.usec = (long)t.tv_usec;

	/* この関数はフック先でしか実行されていないので、エラー処理は行っていない */

	clist_push_one((void *)&obj, clist_ctl);
}
EXPORT_SYMBOL(clbench_add_object);

/**********************************************************
	イベント固有の設定ここまで
**********************************************************/
(以下省略)

次にユーザ側のコードです。キャラクタデバイスへのioctl(2)とシグナルを受ける設定、シグナルハンドラ内でキャラクタデバイスにread(2)/write(2)しています。こちらは比較的短いので、すべてのコードを載せておきます。

clbench_listener.c

#include <stdio.h>
#include <stdlib.h>		/* calloc(3) */
#include <unistd.h>		/* open(2), sleep(3) */
#include <sys/types.h>
#include <signal.h>		/* getpid(2) */

#include <fcntl.h>
#include <sys/ioctl.h>



/**********************************************************
*
*	イベント固有の設定
*	扱うイベントに合わせて変更が必要な箇所
*
**********************************************************/

#define DEVICE_FILE			"/dev/clbench"
#define FLUSH_PERIOD			1500	/* デバイス側でCLISTに対してポーリングする周期(ミリ秒で指定) */
#define CLIST_NR_NODE		10	/* CLISTでのノード数 */
#define CLIST_NODE_NR_COMPOSED	100	/* CLISTで1ノードに含まれるオブジェクト数 */
#define READ_NR_OBJECT		250	/* デバイスファイルに読みにいく際の最大オブジェクト数 */

/*
	注意!	・ユーザ空間とやりとりする構造体はパッディングが発生しない構造にすること
		・この構造体のメンバのsizeof()の合計がsizeof(struct object)と一致するようにすること
		・この構造体の定義はドライバ側のものと同一であること
*/
struct object{
	unsigned long i_ino;
	long long ppos;
	long sec, usec;
};

/**********************************************************
	イベント固有の設定ここまで
**********************************************************/

#define IO_MAGIC				'k'
#define IOC_USEREND_NOTIFY			_IO(IO_MAGIC, 0)	/* ユーザアプリ終了時 */
#define IOC_SIGRESET_REQUEST		_IO(IO_MAGIC, 1)	/* send_sig_argをリセット要求 */
#define IOC_SUBMIT_SPEC			_IOW(IO_MAGIC, 2, struct signal_spec *)	/* ユーザからのパラメータ設定 */

struct ioc_submit_spec{
	int pid;
	int signo, flush_period;
	int nr_node, node_nr_composed;
	int dummy;	/* padding防止のための変数 */
};

int dev, out;	/* ファイルディスクリプタ */
int count;
void *buffer;

/*
	カーネルからのシグナルのハンドラ関数
	@sig シグナルハンドラの仕様
*/
void clbench_handler(int sig)
{
	ssize_t size;

	/* カーネルのメモリを読む */
	size = read(dev, buffer, sizeof(struct object) * READ_NR_OBJECT);
	/* ファイルに書き出す */
	write(out, buffer, (size_t)size);

	printf("read(オブジェクト数): %d\n", (int)(size / sizeof(struct object)));

	count += (int)(size / sizeof(struct object));

	lseek(dev, 0, SEEK_SET);
}

int main(int argc, char *argv[])
{
	int nr_wcurr, signo, nr_picked, grain;
	struct sigaction act;
	struct ioc_submit_spec submit_spec;
	ssize_t size;

	dev = open(DEVICE_FILE, O_RDONLY);
	out = open("./output.clbench", O_CREAT|O_WRONLY|O_TRUNC);

	buffer = (struct object *)calloc(READ_NR_OBJECT, sizeof(struct object));

	/* デバイスの準備 */
	submit_spec.pid = (int)getpid();
	submit_spec.signo = SIGUSR1;
	submit_spec.flush_period = FLUSH_PERIOD;
	submit_spec.nr_node = CLIST_NR_NODE;
	submit_spec.node_nr_composed = CLIST_NODE_NR_COMPOSED;

	ioctl(dev, IOC_SUBMIT_SPEC, &submit_spec);

	/* シグナルハンドリングの準備 */
	act.sa_handler = clbench_handler;
	act.sa_flags = 0;

	sigemptyset(&act.sa_mask);
	sigaction(SIGUSR1, &act, NULL);

	/* SIGTERMをブロックするための設定 */
	sigaddset(&act.sa_mask, SIGTERM);
	sigprocmask(SIG_BLOCK, &act.sa_mask, NULL);

	/* シグナルが届くまでmainスレッドは無限ループ */
	while(1){
		if(sigwait(&act.sa_mask, &signo) == 0){	/* シグナルが受信できたら */
			if(signo == SIGTERM){
				puts("main:SIGTERM recept");
				break;
			}
		}
	}

	/***						***
	***	シグナルを受信するとここに到達する	***
	***						***/

	/* カーネル側に終了通知を送る */
	ioctl(dev, IOC_USEREND_NOTIFY, &nr_wcurr);

	printf("wcurr_len:%d\n", nr_wcurr);

	/* clist_pull_end()でpull残しがないように大きい方でメモリを確保 */
	if(nr_wcurr >= READ_NR_OBJECT){
		/* 一端freeして、再度calloc */
		free(buffer);
		buffer = calloc(nr_wcurr, sizeof(struct object));

		grain = nr_wcurr;
	}
	else{
		/* bufferをそのまま使うのでcalloc無し */
		grain = READ_NR_OBJECT;
	}

	/* grainだけひたすら読んでread(2)が0を返したらbreakする */
	while(1){
		/* カーネルのメモリを読む */
		size = read(dev, buffer, sizeof(struct object) * grain);

		if(size == 0){	/* ここを通るということはclist_benchmark側がSIGRESET_ACCEPTEDになったということ */
			break;
		}

		/* ファイルに書き出す */
		write(out, buffer, (size_t)size);

		printf("read(オブジェクト数): %d\n", (int)(size / sizeof(struct object)));

		count += (int)(size / sizeof(struct object));

		lseek(dev, 0, SEEK_SET);
	}

	putchar('\n');

	/* ベンチマーク結果を出力 */
	puts("------------ベンチマーク結果---------------");
	printf("入出力オブジェクト総数:%d\n", count);
	printf("読み込み粒度(オブジェクト数):%d\n", READ_NR_OBJECT);
	printf("clistのノード数:%d\n", 10);
	printf("clistのノードに含まれるオブジェクト数:%d\n", 100);

	/* リソース解放 */
	free(buffer);

	close(out);
	close(dev);
}

clist_benchmark.cのclbench_add_object()を実装してイベントを取得可能な箇所へフックをかけるのですが、今回はページキャッシュのミスを計測することにしました。フックはdo_generic_file_read()にかけました。ページキャッシュミスについては以前の記事にあります。

mm/filemap.c

■追加ここから■
#ifdef CONFIG_CLBENCH
extern void clbench_add_object(unsigned long i_ino, long long ppos);
#endif
■追加ここまで■
static void do_generic_file_read(struct file *filp, loff_t *ppos,
		read_descriptor_t *desc, read_actor_t actor)
{
	struct address_space *mapping = filp->f_mapping;
	struct inode *inode = mapping->host;
	struct file_ra_state *ra = &filp->f_ra;
	pgoff_t index;
	pgoff_t last_index;
	pgoff_t prev_index;
	unsigned long offset;      /* offset into pagecache page */
	unsigned int prev_offset;
	int error;

	index = *ppos >> PAGE_CACHE_SHIFT;
	prev_index = ra->prev_pos >> PAGE_CACHE_SHIFT;
	prev_offset = ra->prev_pos & (PAGE_CACHE_SIZE-1);
	last_index = (*ppos + desc->count + PAGE_CACHE_SIZE-1) >> PAGE_CACHE_SHIFT;
	offset = *ppos & ~PAGE_CACHE_MASK;

	for (;;) {
		struct page *page;
		pgoff_t end_index;
		loff_t isize;
		unsigned long nr, ret;

		cond_resched();
find_page:
		page = find_get_page(mapping, index);
		if (!page) {
			page_cache_sync_readahead(mapping,
					ra, filp,
					index, last_index - index);
			page = find_get_page(mapping, index);
			if (unlikely(page == NULL)){
■追加ここから■
#ifdef CONFIG_CLBENCH
				clbench_add_object(inode->i_ino, *ppos);
#endif
■追加ここまで■
				goto no_cached_page;
			}
		}

ここまでやってカーネルをビルド&起動します。起動したらmknode(1)でデバイスファイルを作成しclbench_listenerを起動するとイベントの計測が開始されます。

イベント計測を終了したい時にclbench_listenerにSIGTERMシグナルを送ります。そうするとclist内に残っているオブジェクトを読みきって、終了します。左がclbench_listenerの画面で右がカーネルログです。

f:id:shimada-k:20111228163919p:image

clbench_listenerが正常に終了するとオブジェクト列が書き出されたファイルが作成されます。今回の場合output.clbenchという名前です。

イベント計測中に動かすプログラムについてやoutput.clbenchの解析は別エントリで書こうと思います。一応今回はなんとか動くものを作りました、ということで。