Pylone Blog - タグ:tips

再帰実行しないmakefile

この記事ではmakeの再帰呼びだしに頼らないmakefileを書くためのtipsを紹介します。 社内勉強会向けに作成した資料をベースとして、公開用に再構成しました。

ある程度の規模のプロジェクトのビルドを効率化したり、 LinuxカーネルやAndroid、OpenJDKなどで使われている(kbuild)ビルド機構を理解するのに役立つでしょう。 記述はMakeの拡張機能が使えることを前提としていますが、BSD系のmakeでも類似の技法は機能するはずです。

Recursive Make Considered Harmful

過去にはよく使われたmakefileの書きかたとして、makefileのルールから別のmakeを (recursiveに) 呼びだす、というものがありました。 そのようなmakeの使い方は

  • なぜかビルドにとても時間がかかる
  • ときどきファイルが正しく再生成されない

といった問題を起こすのでやめるべきといわれて10年以上たちますが、いまだにそのような構成のプロジェクトはよく見られます。 makefileを適当に動くものを雛形にして作るなら、できるだけ内容を理解して適切なコピー元を選びましょう。

サンプルプロジェクト

以降では例としてソースファイルの構成が

- (toplevel)
  - src/
      main.c
  - parser/
      parser.grammar

ビルド手順は

  1. parser.grammarからパーサジェネレータ(lemon)でparser.cとparser.hを生成
  2. parser.cをコンパイルしてparser.oを生成
  3. parser.hとmain.cからmain.oを生成
  4. main.oとparser.oをリンクして実行ファイルappを生成

という場合を想定したmakefile群の書き方を考えます。

分割しない場合

今回の例のサイズであれば、単一のmakefileに全ての依存関係を書く:

all: app

src/main.o: src/main.c parser/parser.h

parser/parser.o: parser/parser.c
parser/parser.c parser/parser.h: parser/parser.grammar
        lemon $<

app: src/main.o parser/parser.o
        $(CC) $(LDFLAGS) -o $@ $+

ことも十分可能です。

しかし、現実的なサイズのプロジェクトを単一のmakefileでビルドするのは難しいでしょう。

再帰バージョン

旧い方法に従うと、

  1. サブディレクトリのためのmakefile群 (parser/Makefile, src/Makefile) を用意する
  2. トップレベルのMakefileのルールとして、サブディレクトリでmakeを実行する

ということになります。

トップレベルのMakefile

parser/、src/それぞれのディレクトリでmakeコマンドを起動することになります。

#Makefile
SUBDIRS := parser src
all: $(SUBDIRS) app

$(SUBDIRS):
        $(MAKE) -C $@

app: src/main.o parser/parser.o
        $(CC) $(LDFLAGS) -o $@ $+

.PHONY: all $(SUBDIRS)
サブディレクトリのmakefile

関連するファイルの生成規則だけを含むように分割すると

#parser/Makefile
parser.o: parser.c
parser.h parser.c: parser.grammar
        lemon $<
#src/Makefile
main.o: main.c ../parser/parser.h

となるでしょう。

再帰的makeの問題点

とりあえず使える程度の動作はしますが、

  • 必要がなくても下位makeプロセスが起動されるので遅い
  • makeに平行処理を許した (たとえば make -j3) 場合はビルドに失敗することがある

といった問題を解決するのは困難です。

非再帰バージョン (include directive)

GNU Makeの、makefile内に別のファイルを読み込む拡張機能(include directive)を使えばmakefileの一部を別のファイルへと分割可能です。

この例の場合、src/、parser/以下の依存関係を

#src/build.mk
src/main.o: src/main.o parser/parser.h

および

#parser/build.mk
parser/parser.o: parser/parser.c
parser/parser.h parser/parser.c: parser/parser.grammar
        lemon $<

として抜きだして、トップレベルのMakefileからそれぞれを読みこむ

#Makefile
all: main

main: src/main.o parser/parser.o
        $(CC) $(LDFLAGS) -o $@ $+

include src/build.mk
include parser/build.mk

ようにできます。

非再帰バージョン (下位へのパラメータ渡し)

前の例ではincludeされるファイル内で明示的にディレクトリ名 (src/、parser/) を都度書いていますが、もしこれを修正することになるとかなりの手間がかかります。 makeの変数を使ってincludeする側から渡すようにして、ディレクトリ名に依存してしまう要素を減らしましょう。

makeの変数RELに対象ディレクトリ名が格納されるようにすれば、(ディレクトリ名を$(REL)に置きかえることで) src/build.mkを

$(REL)main.o: $(REL)main.c parser/parser.h

parser/build.mk を

$(REL)parser.o: $(REL)parser.c
$(REL)parser.c $(REL)parser.h: $(REL)parser.grammar
        lemon $<

のように書きなおすことができます。

この場合、トップレベルのmakefile側でincludeする前にRELを設定しておく

all: app

REL := src/
include $(REL)build.mk

REL := parser/
include $(REL)build.mk

app: src/main.o parser/parser.o
        $(CC) $(LDFLAGS) -o $@ $+

ことになります。 なお、includeされるファイル毎に値を変えたいRELはsimply-expanded な (:=を用いた) 変数にしないと、最後の定義によって全ての$(REL)の値が上書きされてしまうことに注意してください。

非再帰バージョン (上位へのパラメータ渡し)

関連するオブジェクトファイルが多くなる場合、すべてを手作業で列挙するのは危険なので、includeされる側でファイルのリストを構築した方がよいでしょう。

トップレベルのmakefileを(simply-expandedな)変数OBJSを利用するように:

all: app
OBJS :=

REL := src/
include $(REL)build.mk

REL := parser/
include $(REL)build.mk

app: $(OBJS)
        $(CC) $(LDFLAGS) -o $@ $+

書いておくと、OBJSはmakefile全体で共有されるため、 src/build.mk、parser/build.mk内でOBJSへ加えた変更がトップレベルの "app" のルール処理に反映されます。

src/build.mk、parser/build.mk 内では、

# src/build.mk
OBJS += $(REL)main.o
# parser/build.mk
OBJS += $(REL)parser.o

のようにして、必要なオブジェクトファイルを順に追加していけばよいでしょう。

ただし、includeされる側で行なう操作はリストへの追加だけにするべきです。リスト自体を上書きしてしまう (「+=」ではなく「:=」を使う)

OBJS := $(REL)foo.o

と、グローバルなOBJSの内容は破壊されます。 下位のmakefileの作成者が信用できないなら、OBJSへの操作を (includeされる側には委ねないで) 上位側に移した方がよいかもしれません。

非再帰バージョン (自動include)

下位ディレクトリが多数存在し、それぞれに対する個々のincludeをトップレベルに書きたくない場合、GNU Make自身に等価な記述を生成させることができます。

下位のmakefileの名前をbuild.mkとしたとき、build.mkが存在する下位ディレクトリのリストは

$(dir $(wildcard */build.mk))

で得られます。これと

  • 引数を与えると
          REL := [引数のディレクトリ名]
          include $(REL)build.mk
    を生成するmakeのユーザー定義関数
  • 組み込み関数foreachによるリスト要素への関数適用

を組み合わせれば、下位ディレクトリのbuild.mkを自動的にincludeするmakefileを

all: app

SUBDIRS := $(dir $(wildcard */build.mk))
OBJS :=

define include_fragment
 REL:= $(1)
 include $(1)build.mk
endef

$(foreach i, $(SUBDIRS), \
        $(eval $(call include_fragment, $(i))))

app: $(OBJS)
        $(CC) $(LDFLAGS) -o $@ $+

のように書くことができます。

こうしておけば、下位ディレクトリを追加した際にはbuild.mkを置くだけでトップレベルのmakefileから自動的に読みこまれます。 あとからmakefileを解読するのは難しくなってしまいますが、目的によっては便利に使えるのではないでしょうか。

ターゲットごとに変数値を設定

includeを使ってmakefileを合成する場合、makeの変数は全体で共有されます。 しかし、一部のターゲットについてだけCFLAGS、LDFLAGSといった変数の値を変更したい場合もあるでしょう。

幸いGNU Makeには

ターゲット : 変数定義

とすることで、ターゲットごとの変数値を設定する機能 (target-specific variable value) が存在します。

たとえば main.o の生成規則を

$(REL)main.o: $(REL)main.c parser/parser.h

として定義していたとき、

$(REL)main.o: CFLAGS += -I parser/
$(REL)main.o: $(REL)main.c parser/parser.h

のように定義を追加することで、このルールを実行する時だけ CFLAGS に「-I parser/」が追加されます。

この機能はincludeディレクティブによって展開された時点の変数の値を保存しておくのにも使えるので、makefile自体のデバッグにも役立ちます。たとえば

$(REL)parser.o: LOCAL_REL := $(REL)
$(REL)parser.o: $(REL)parser.c
        @echo "LOCAL_REL=" $(LOCAL_REL)
        $(CC) $(CFLAGS) -c -o $@ $<

とすれば、上書きされてしまう前にRELを保存しておいて確認することもできるでしょう。

まとめ

新しく作るmakefileでは「$(MAKE) -C」を止めて「include」を使いましょう。

makefileを書くとき、makeの出力に大量の「Entering directory」「Leaving directory」(残念な環境では「入りますディレクトリ」) といったメッセージが含まれるようなプロジェクトを参考にするのは避けるべきです。

jffs2イメージをloopback mount

block2mtdドライバを使えば、jffs2イメージファイルをloopbackでmountできます。

erasesize 128K (131072) のrootfs.jffs2を/mntにmountする場合

mount:

# modprobe jffs2
# modprobe mtdblock
# losetup /dev/loop0 rootfs.jffs2
# modprobe block2mtd block2mtd=/dev/loop0,131072
# mount -t jffs2 -o ro /dev/mtdblock0 /mnt

umount:

# umount /mnt
# rmmod block2mtd
# losetup -d /dev/loop0

seq_fileの使い方

Linuxカーネルが持つ、疑似ファイルの実装を補助する機構を紹介します。

カーネルからユーザ空間へデータを渡す手段としては、 /proc や /sys 以下に作成した疑似ファイルを使うことが多いでしょう。

渡したい値が単純な型の場合はカーネル組込みのヘルパ関数を使えば十分(sysfs 経由でモジュールパラメータにアクセスsysfs 経由でモジュールパラメータにアクセス (2))ですが、 より複雑なデータを渡すならば:

  • 疑似ファイルの内容となるテキスト量が大きい場合、カーネル空間に全体を保持たくない。
  • データ構造を操作に時間がかかる場合、ユーザ空間から要求された部分だけを処理することが望ましい。
  • 同時に複数の読み手が存在したり、読み出し途中でデータが変更される場合の排他が必要。

などの点を考慮するべきです。

本記事では、シーケンシャルな疑似ファイルを実装するために用意されている補助関数群について解説します。 カーネル内でも広く使われている機構なので、知っておくとコードを読む際にも役立つでしょう。

seq_file

VFSの層ではファイル中の位置はバイト単位で扱われます。固定長のバッファで扱える程度のデータ量ならよいのですが、 疑似ファイル内容を一括生成・保持できない場合、自力で出力済のバイト数管理やバイト単位でのシークを実装するのは手間がかかります。

fs/seq_file.cには出力したいデータが

  • 通し番号が付けられる(配列や木などの)要素の集合として表現できる
  • 要素の番号がわかれば、その要素の文字列表現が得られる

という条件を満たすとき、各要素へのアクセスをイテレータ操作として抽象化する仕組みが用意されています。

これを使うと、所定のアクセサを作成するだけで、VFSに登録する struct file_operations のメンバのほとんどをカーネルから提供される汎用関数でまかなうことができます。

使用例

サンプルとして、全ttyについてdebugfs上の疑似ファイル経由として termios状態を一覧するカーネルモジュールのソースtermios_dumper.cを用意しました。

個別のttyの状態取得ならtcgetattr()でも十分なのですが、システム上で 現在activeな全てのttyをリストするために、カーネル内部のデータ構造を直接読んでいます。

疑似ファイルの登録

seq_fileを使用する場合でも、ファイルシステムへの疑似ファイルの登録は通常のまま

  1. struct file_operationsを作成
  2. ファイルシステム毎の登録関数の呼び出し

という手順で行います。

ただし、VFSに渡すstruct file_operationsのうち、読みこみ可能な疑似ファイルを実装するために必要な

  • open
  • read
  • llseek
  • release

のうちread, llseek, releaseとしては、seq_fileで実装されているseq_read, seq_lseek, seq_releaseをそのまま使用できます。

.openに登録する関数では、

  • open()されたファイルに対して、seq_open()を呼んでハンドラを登録。
  • 必要ならリソースの確保/ロック

を行ないます。

サンプルでは登録先のファイルシステムをdebugfsとしたので、以下のようにしています。

static const struct file_operations fops = {
    .open    = termios_dumper_open,
    .read    = seq_read,
    .llseek  = seq_lseek,
    .release = seq_release,
};

...

static int __init termios_dumper_init(void)
{
...
    if(IS_ERR(fs_root = debugfs_create_dir(KBUILD_BASENAME, NULL)))
        return PTR_ERR(fs_root);

    if(IS_ERR(fs_file = debugfs_create_file("state", 0444, fs_root, NULL,
                                            &fops)))
        return PTR_ERR(fs_file);

この例では、特にリソースの初期化が必要ないため、.openに登録したtest_seqfile_openの実装はseq_open()の呼び出しだけです。

static int test_seqfile_open(struct inode *inode, struct file *file)
{
    return seq_open(file, &seqfile_ops);
}

seq_operations

読み出し用のイテレータに必要な操作:

  • 初期化
  • 終了処理
  • 「次」の要素に移動
  • 値(文字列表現)の取得

は、

struct seq_operations {
        void * (*start) (struct seq_file *m, loff_t *pos);
        void (*stop) (struct seq_file *m, void *v);
        void * (*next) (struct seq_file *m, void *v, loff_t *pos);
        int (*show) (struct seq_file *m, void *v);
};

の各メンバとして登録されます。

start

seq_file用のファイルがopen()された時に呼ばれます。

  • 使用するリソースの確保
  • イテレータの初期化

を行ない、成功した場合には初期化済みのイテレータ、失敗した場合はNULLを返すことになるでしょう。

引数*posが0より大きい場合には、その位置を指すようにしたイテレータが返却されるべきです。

※ イテレータの更新処理は、(pos)ではなく、(*pos)を使います。

イテレータを進める処理は、next()からの処理でも必要となるため、共通化してもよいでしょう。サンプルでは update_iterator()としています。

next

seq_fileが、現在のイテレータからのデータを消費し終えた時、 現在のイテレータと次に指すべき位置を引数として呼ばれます。

読み手のread()に対応していると考えてよいでしょう。 ただし、カーネルによる先読み処理の対象となるため、read()の度にかならず呼ばれるわけではありあせん。 また、読み手が小量のデータしが要求していなくても、さらに先まで要求されることがあります。

指定位置を指すイテレータを用意して、それへのポインタを返します。 この場合返却したポインタは後述するshow()への引数として使われます。

処理に失敗した場合はNULLを返却します。このとき、続いてstop()が呼ばれますが、stop()の引数にはNULLしか渡らないため、リソースの開放にイテレータが必要なら、next()内で済ませておく必要があります。

stop

読み手に十分なテキストが渡ったか、next()からデータがもうないことを通知した(NULLを返した)後に呼びだされます。

リソースの開放を行なうべきですが、引数として渡されるポインタはNULLの場合があることに注意が必要です。

show

引数として渡されたイテレータから、読み手に渡す(疑似ファイルの内容となる)文字列を構築します。

構築にはseq_printf(), seq_puts()などのヘルパ関数を使用します。これらのヘルパは繰替えし呼ぶこともでき(追記されます)、最終的な長さは自動的に管理されるので、成功時には0を返せば適切に処理されます。

ただし、一度に構築できる文字列の全長はPAGE_SIZE程度なので、長くなりすぎる場合はイテレータの構造を変える必要があるでしょう。

サンプルコードでは、現在のイテレータが指しているtermiosの情報を、seq_printf()経由でgrepしやすい書式に変換しています。

サンプルtermios_dumper.cについての補足

サンプルとして使用したコードtermios_dumper.cでは、カーネルから公開されていはいttyドライバのリストの先頭要素のアドレスを知るために、struct tty_driverのmagic要素を用いて検索していますが、これは低い確率ですが(メモリ中の値が偶然TTY_MAGICと一致したり、get_current_tty()が呼べない場合に)失敗する可能性のある処理です。 確実を期すなら、カーネルの公開シンボルにtty coreのtty_driversを追加するか、専用のアクセサを追加するべきでしょう。

また、ttyサブシステム内の構造体を辿る処理はttyのmutexを取得した上で行うべきなのですが、

  • tty_mutexを占有すると副作用が大きい(ロック中に新規にttyを開こうとした他のプロセス全てが停止するため)
  • 改造して失敗したときにシステムをロックしてしまう
  • 今回のサンプルでは読み出ししかおこなわないため、整合性が崩れても大きな問題にならない

ため、デモ目的では不要と判断してコメントアウトしてあります。 このため、疑似ファイルからの読出し途中でttyが増減すると、表示の一貫性が崩れることがあるでしょう。 同様の理由で、kobjectの参照数カウントについても省略しています。

sysfs 経由でモジュールパラメータにアクセス (2)

以前の記事で sysfs からモジュールパラメータにアクセスする簡単な方法を紹介しましたが、今回はパラメータの型を独自に定義する例として Base36 のパラメータを持つ簡単なモジュールを紹介します。

コード

base36param.c:

#include <linux/init.h>
#include <linux/module.h>
#include <linux/moduleparam.h>
#include <linux/kernel.h>

static int stored_value = 0;

static int param_set_base36(const char *val, struct kernel_param *kp)
{
    if (('0' <= val[0]) && (val[0] <= '9')) {
            stored_value = val[0] -'0';
            return 0;
    }
    if (('a' <= val[0]) && (val[0] <= 'z')) {
            stored_value = val[0] -'a' +10;
            return 0;
    }
    if (('A' <= val[0]) && (val[0] <= 'Z')) {
            stored_value = val[0] -'A' +10;
            return 0;
    }
    return -EINVAL;
}

static int param_get_base36(char *buffer, struct kernel_param *kp)
{
    if (stored_value < 0 || 36 < stored_value)
        return -EINVAL;

    if (stored_value < 10)
        buffer[0] = '0' + stored_value;
    else
        buffer[0] = 'A' + stored_value - 10;

    buffer[1] = '\0';

    return 2;
}

/* dummy checker */
#define param_check_base36(name, p) __param_check(name, p, void);

static int __init base36param_init(void)
{
    printk(KERN_INFO "%s called\n", __func__);
    return 0; /* succeeded */
}

static void __exit base36param_cleanup(void)
{
    printk(KERN_INFO "%s called\n", __func__);
    return;
}

module_init(base36param_init);
module_exit(base36param_cleanup);

module_param(stored_value, base36, 0644);
MODULE_PARM_DESC(stored_value, "can write [0-9A-Za-z], read as [0-9A-Z].");

MODULE_LICENSE("GPL");
MODULE_AUTHOR("MINAMI Hirokazu");
MODULE_DESCRIPTION("sample module which has a custom typed parameter");

Makefile:

KERNEL_SRC = /lib/modules/$(shell uname -r)/build
#CROSS_COMPILE =
#ARCH =
BUILD_DIR := $(shell pwd)
VERBOSE = 0

obj-m := base36param.o

all:
	make -C $(KERNEL_SRC) SUBDIRS=$(BUILD_DIR) KBUILD_VERBOSE=$(VERBOSE) modules

clean:
	rm -rf *.o *.ko *.mod.c .*.cmd .tmp_versions *~ Module.symvers

ビルド

環境に応じて Makefile の KERNEL_SRC, CROSS_COMPILE, ARCH を修正し、make を実行します。

使い方

値 (Base36) をパラメータに書き込む:

# echo 'h' > /sys/module/base36param/parameters/stored_value

パラメータを読み込む:

# cat /sys/module/base36param/parameters/stored_value
H

解説

モジュールパラメータに独自の型を持たせるためには、 module_param を使います。

module_param(stored_value, base36, 0644);

module_param は以下のようなマクロです。

/* Helper functions: type is byte, short, ushort, int, uint, long,
   ulong, charp, bool or invbool, or XXX if you define param_get_XXX,
   param_set_XXX and param_check_XXX. */
#define module_param_named(name, value, type, perm)			   \
	param_check_##type(name, &(value));				   \
	module_param_call(name, param_set_##type, param_get_##type, &value, perm); \
	__MODULE_PARM_TYPE(name, #type)

#define module_param(name, type, perm)				\
	module_param_named(name, name, type, perm)

param_set_##type と param_get_##type が肝です。 sysfs 経由でパラメータが読み書きされると、ここで登録される param_set_##type と param_get_##type が呼ばれます。 (module_param_call の説明は省きます)

base36param.c の場合、プリプロセッサによってマクロ展開された param_set_base36 と param_get_base36 が登録されることになります。

マクロのコメントにある通り、 独自の型ではなく int などの場合は あらかじめ用意されているヘルパー関数群が使われるので 自前で用意する必要はありません。

先日公開した 仮想バッテリドライバ はこれの応用です。 ファイルとして読み書きできるので ユーザ空間のプログラムとヘッダを共有する必要がない点は ioctl(2) より使い勝手がよいかもしれません。

関連記事

usbmon

Linux kernel 組込みの USB サブシステムをモニタする仕組み(usbmon)について紹介します。 debugfs の活用例としても参考になるのではないでしょうか。

前提

Linux kernel 2.6.21以降を

  • CONFIG_USBMON
  • CONFIG_DEBUGFS

を有効にしてコンパイルした環境が必要です。

準備

  1. debugfsがまだマウントされていなければマウントします。 一般的に /sys/kernel/debug/ にマウントされるため、
    # mount -t debugfs none /sys/kernel/debug
    
    すればよいでしょう。
  2. usbmon をモジュールとして作成していた場合、カーネルに組み込みます。
    # modprobe usbmon
    

以上の手順を実行すると、debugfs の下(/sys/kernel/debug/usbmon/) に、 「番号 + "s"/"t"/"u"」 という仮想ファイル群が生成されているはずです。

$ ls /sys/kernel/debug/usbmon/
0s  0u  1t  2s  2u  3t  4s  4u  5t  6s  6u
0t  1s  1u  2t  3s  3u  4t  5s  5u  6t

実際に何個のファイルが見えるかは、システム構成によります。

キャプチャ

「USBバス番号 + 'u'」という名前のファイルに、 USBの通信内容が配送されます。

とりあえずバス番号に 0 を指定して("0u"を使って)、全通信を監視してみるとよいでしょう。

# cat  /sys/kernel/debug/usbmon/0u
dfaac8c0 2557329542 S Bo:5:004:2 -115 31 = 55534243 214b0100 00000000 00000600 00000000 00000000 00000000 000000
dfaac8c0 2557329639 C Bo:5:004:2 0 31 >
dfaac8c0 2557329649 S Bi:5:004:2 -115 13 <
dfaac8c0 2557331148 C Bi:5:004:2 0 13 = 55534253 214b0100 00000000 01
dfaac8c0 2557331171 S Bo:5:004:2 -115 31 = 55534243 224b0100 12000000 80000603 00000012 00000000 00000000 000000
dfaac8c0 2557331388 C Bo:5:004:2 0 31 >

行の内容は以下のようになっています。

  • URB Tag
  • timestamp
  • Event Type (S - submission, C - callback, E - error)
  • 通信種類([CZIB][io]): バス番号 : Device address : Endpoint number
  • URB Status word
  • データ長さ
  • データ内容

特定のデバイスだけを監視したい場合、lsusb の結果を参考にするとよいでしょう。たとえば

$ lsusb |grep PL2303
Bus 005 Device 005: ID 067b:2303 Prolific Technology, Inc. PL2303 Serial Port

のようになっていた場合、 PL2303(USB-シリアルコンバータ)が5番に接続されているので、5uを使えばよいことがわかります。

その他の情報

疑似ファイルには他に 't'/'s'でおわるものがあります。

't'でおわるもの

'*t' は互換性のために残されている古い書式の疑似ファイルなので、 通常使用する必要はないでしょう。

's'でおわるもの

'*s' からは usbmon 自体の動作状況を読むことができます。たとえば

# cat /sys/kernel/debug/usbmon/5s
nreaders 1 events 350 text_lost 0

なら、

  • 現在 1つのプロセスが バス#5 を監視して中
  • いままでに 350 個のURBが読み出された
  • 読まれる前にバッファから溢れてしまった URB はない

ことがわかります。

's' で終わるファイル群の処理では排他制御が考慮されないため、 このインターフェース経由に高負荷をかける場合は何をしているのかよく理解した上で行なってください。