Pylone Blog

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の参照数カウントについても省略しています。