catコマンドをつくってみる
まえがき
この動画を見てたら自分も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)
さて,はじめに紹介した動画では
open
read
write
の3つのシステムコールを主に用いてcatを実装しています.
ファイルを開き,そのファイルから読み出すというopen
とread
の組み合わせは非常によく出てくるパターンです.
何度も使うパターンはまとめておきたいと思いませんか.
カーネルの開発者も同じ考えで,ファイルを開き,バッファに読み出し,ファイルを閉じる
一連の流れを一つのシステムコールで実行できるような実装をしました.
それがこのブランチです.
sysfs
やprocfs
にある比較的サイズの小さなファイルを読むのに適しています.
システムコールの発行には大きなオーバーヘッドが発生するため,その回数を減らせるのはアドバンテージとなります.
実装
readfile
のmanを読むと使い方がわかります.
ssize_t readfile(int dirfd, const char *pathname, void *buf, size_t count, int flags);
引数はこんな感じです.
多くがopenat
と共通しています.dirfd
は今回はAT_FDCWD
で良いでしょう.
flags
に設定できるフラグは限られていて,O_NOFOLLOW
かO_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_how
やbuild_open_flags
,readfile_open
でいい感じに該当ファイルを開いた後,vfs_read
で読み,filp_close
で閉じています.
まとめ
readfile
は機能も実装もシンプルなシステムコールで,非常に読みやすく,システムコールを追加する方法を学ぶのにもちょうど良い題材の様に思えます.
mainlineにマージされるかはわかりませんが,非常に勉強になったのでありがたいです.
Greg K-Hに感謝.
Comments