まえがき

この動画を見てたら自分もcat作りたくなったのでやってみました.

普通に作っても二番煎じなので,別の方法でやります.

この記事はLinuxその2 Advent Calendar 2020の15日目の記事です.

環境

vagrant@ubuntu-focal:~$ uname -a
Linux ubuntu-focal 5.10.0-rc3+ #2 SMP Tue Dec 15 01:20:55 JST 2020 x86_64 x86_64 x86_64 GNU/Linux

commit hashは652db3de96a630e8051ffa921286000bb9ee2727です.

ここのカーネルをいい感じにビルドしてます.

一般的な環境では動作しません.

cat

catとは,concatの略称であり,本来はファイル同士を連結させる為のコマンドであるという話もよく聞きます.

別にどう使おうが自分の勝手なので使用方法には言及しませんが,「引数に複数のファイルが与えられたらつなげて表示する」という機能は実装しておきたいです.

仕様

今回実装するcat

という挙動のプログラムとします.

一般的にcatは引数を与えなかった時,標準入力から読みますが,今回は事情によりその機能は実装しません.

readfile(2)

さて,はじめに紹介した動画では

の3つのシステムコールを主に用いてcatを実装しています.

ファイルを開き,そのファイルから読み出すというopenreadの組み合わせは非常によく出てくるパターンです.

何度も使うパターンはまとめておきたいと思いませんか.

カーネルの開発者も同じ考えで,ファイルを開き,バッファに読み出し,ファイルを閉じる一連の流れを一つのシステムコールで実行できるような実装をしました.

それがこのブランチです.

sysfsprocfsにある比較的サイズの小さなファイルを読むのに適しています.

システムコールの発行には大きなオーバーヘッドが発生するため,その回数を減らせるのはアドバンテージとなります.

実装

readfileのmanを読むと使い方がわかります.

ssize_t readfile(int dirfd, const char *pathname, void *buf, size_t count, int flags);

引数はこんな感じです.

多くがopenatと共通しています.dirfdは今回はAT_FDCWDで良いでしょう.

flagsに設定できるフラグは限られていて,O_NOFOLLOWO_NOATIMEが使用可能です.

ちょっと面倒ですがとりあえずO_NOFOLLOWにしておきましょう.

Code

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <err.h>
#define __NR_readfile   441

int main(int argc, char **argv) {
    if (argc < 2)
        return printf("Usage: %s [file]\n", *argv), 1;

    for (int i = 1; argv[i] != NULL; i++) {
        char buf[4096] = {0};
        int nread = 0;
        if ((nread = syscall(__NR_readfile, AT_FDCWD, argv[i], buf, 4096, O_NOFOLLOW)) == -1)
            err(1, "Error on readfile to %s", argv[i]);
        write(STDOUT_FILENO, buf, nread);
    }
}

Exec

実行してみます.

$ cat cat.c
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <err.h>
#define __NR_readfile   441

int main(int argc, char **argv) {
    if (argc < 2)
        return printf("Usage: %s [file]\n", *argv), 1;

    for (int i = 1; argv[i] != NULL; i++) {
        char buf[4096] = {0};
        int nread = 0;
        if ((nread = syscall(__NR_readfile, AT_FDCWD, argv[i], buf, 4096, O_NOFOLLOW)) == -1)
            err(1, "Error on readfile to %s", argv[i]);
        write(STDOUT_FILENO, buf, nread);
    }
}
$ gcc -o cat cat.c
$ ./cat cat.c
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <err.h>
#define __NR_readfile   441

int main(int argc, char **argv) {
    if (argc < 2)
        return printf("Usage: %s [file]\n", *argv), 1;

    for (int i = 1; argv[i] != NULL; i++) {
        char buf[4096] = {0};
        int nread = 0;
        if ((nread = syscall(__NR_readfile, AT_FDCWD, argv[i], buf, 4096, O_NOFOLLOW)) == -1)
            err(1, "Error on readfile to %s", argv[i]);
        write(STDOUT_FILENO, buf, nread);
    }
}

複数ファイルも可能です

$ cat hoge fuga piyo
hoge
fuga
piyo
$ ./cat hoge fuga piyo
hoge
fuga
piyo

エラーの処理も問題無さそうです

$ ./cat
Usage: ./cat [file]
$ ./cat abcdef
cat: Error on readfile to abcdef: No such file or directory

問題

作っておいてなんですが,今回作成したプログラムには問題があります.

今回は適当にバッファを4096で確保していますが,この場合4096バイト以上のファイルは途中までしか表示できません.

開いたファイルはシステムコール内で閉じられているので,seekができないのです.

小さなファイルを読み出すのに作られたシステムコールであるため,バッファサイズ分だけ読み出せればそれで良いのです.

read readfile

readfileを使ったcatの実装は大して難しくも無いので,readfile自体の実装を読むと面白いです.

2f1d4fc6f4ea7890bba90f7600efbd587bf93f42

static struct file *readfile_open(int dfd, const char __user *filename,
                                  struct open_flags *op)
{
        struct filename *tmp;
        struct file *f;

        tmp = getname(filename);
        if (IS_ERR(tmp))
                return (struct file *)tmp;

        f = do_filp_open(dfd, tmp, op);
        if (!IS_ERR(f))
                fsnotify_open(f);

        putname(tmp);
        return f;
}

SYSCALL_DEFINE5(readfile, int, dfd, const char __user *, filename,
                char __user *, buffer, size_t, bufsize, int, flags)
{
        struct open_flags op;
        struct open_how how;
        struct file *file;
        loff_t pos = 0;
        int retval;

        /* only accept a small subset of O_ flags that make sense */
        if ((flags & (O_NOFOLLOW | O_NOATIME)) != flags)
                return -EINVAL;

        /* add some needed flags to be able to open the file properly */
        flags |= O_RDONLY | O_LARGEFILE;

        how = build_open_how(flags, 0000);
        retval = build_open_flags(&how, &op);
        if (retval)
                return retval;

        file = readfile_open(dfd, filename, &op);
        if (IS_ERR(file))
                return PTR_ERR(file);

        retval = vfs_read(file, buffer, bufsize, &pos);

        filp_close(file, NULL);

        return retval;
}

非常にシンプルな実装ですね.

Linux kernelのソースコードは深追いするとキリがないのでSYSCALL_DEFINE5の流れを軽く説明します.

最初にflagsのチェックが行われ,O_NOFOLLOWでもO_NOATIMEでも無かった場合は-EINVALが返ります.

build_open_howbuild_open_flagsreadfile_openでいい感じに該当ファイルを開いた後,vfs_readで読み,filp_closeで閉じています.

まとめ

readfileは機能も実装もシンプルなシステムコールで,非常に読みやすく,システムコールを追加する方法を学ぶのにもちょうど良い題材の様に思えます.

mainlineにマージされるかはわかりませんが,非常に勉強になったのでありがたいです.

Greg K-Hに感謝.