毎日(?) システムコール
1日目 (read)
openシステムコール
ssize_t read(int fd, void *buf, size_t count);
file descriptor、書き込み先のバッファ、最大何byteまで読み込むかのカウントを渡す。
ファイルがシークできる場合は、ファイルオフセットは読み取られたバイト数だけインクリメントされる。オフセットがファイルの終端もしくは終端を過ぎているときは 0を返す。
countに0を渡した場合は、read()はエラーを返すかもしれない。
成功したときは読み込んだバイト数が返されるが、このバイト数がカウントより少なくなることもある。
例えば、ファイル終端に近かった場合、パイプから読んでいる場合、端末から呼んでいる場合、シグナルによって中断されたときなど。
エラーには-1が返され、errnoが適切に設定される。
このとき、ファイル位置が変更されたかどうかは未定義になる。
返すエラー
- EAGAIN: non-blocking関連(openでやる)
- EWOULDBLOCK: non-blocking関連(openでやる)
- EBADF: ファイルディスクリプタがおかしい
- EFAULT: バッファがアクセスできない(nullとか、解放済みとか?)
- EINTR: シグナルで中断された
- EINVAL: O_DIRECTでオープンされて、アドレス、カウント、オフセットのどれかが適切にalignされていないなど
- EIO: バックグランドのプロセスグループに属しているときに、制御端末から読んだり、低レベルのIOエラーが発生した場合など
- EISDIR
標準入力から読み取って出力するコード
int fd = STDIN_FILENO;
char buf[4096];
ssize_t nread;
// read from stdin
while ((nread = read(fd, buf, sizeof(buf))) > 0) {
printf("%.*s", (int)nread, buf);
}
2日目 (write)
writeシステムコール
ssize_t write(int fd, const void *buf, size_t count);
書き込み先 file descriptor, 書き込む内容のバッファ、カウントを渡す。
bufで始まるバッファからcountバイトまで書き込む。
書き込まれるバイト数はcountより少なくなることがある。(例えば、リソース制限に達した場合、シグナルによって中断された場合など)
シーク可能なファイルでは、書き込みはファイルオフセットに行われて、実際に書き込まれたバイト数だけオフセットは増加する。ファイルが O_APPEND
で open (2)
されている場合は、書き込みの前にファイルオフセットが最後に設定される。ファイルオフセットの調整と書き込みはアトミックに実行される。
write()
が成功したとしても実際にファイルに書き込みが実行されているとは限らない。
カーネルがI/Oをバッファリングしているため(明示的に書き込むときは fsync(2)
を使う?)
direct ioを使って書き込みを行っている場合、書き込みに失敗したとき全体が失敗したわけではなく、一部のみ書き込まれる可能性もある。(その場合はおそらくデータが壊れている)
エラーをいくつか抽出
- EDQUOT: ディスクのquotaに制限された系のエラー
- EFBIG: ファイルサイズが大きすぎる(プロセスごとの最大サイズとか実装の上限とか)
- ENOSPC: ディスクがいっぱい
- EPERM: 権限がない
- EPIPE: 書き込み先がパイプかソケットに繋がっていて、読み込み側がクローズされている。ただ、そのときはSIGPIPEシグナルを受け取るので、プログラムがこのシグナルをキャッチするか、無視しないとこのエラーは見れない
標準出力に "Hello World" を出力するコード
int fd = STDOUT_FILENO;
char *buf = "Hello World\n";
ssize_t nwrites;
nwrites = write(fd, buf, strlen(buf));
assert(nwrites == strlen(buf));
3日目 (signal, pause)
signalシステムコールは、シグナルが送られたときの動作を設定する。
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
ただし、UNIXのバージョンやLinuxのバージョンによって微妙に動作が異なるので、sigaction(2)
を使ったほうが良い。
設定するハンドラは、 SIG_IGN
, SIG_DFL
もしくはシグナルハンドラのアドレスを指定する。
SIG_IGN
が設定されている場合、単にシグナルを無視する。
SIG_DFL
はシグナルごとのデフォルトのアクションが発生する。
例えば、 SIGWINCH
は Ign
になったり、SIGINT
は Term
だったりする。 man 7 signal
で確認できる。
writeシステムコールをパイプに対して呼び出しているときに、パイプが閉じられている場合は EPIPE
エラーになるらしいけど、SIGPIPEのシグナルがデフォルトで Term なのでエラーを取得できなかった。
SIGPIPE
を無視して、EPIPE
を受け取ってみる
volatile sig_atomic_t e_flag = 0;
void catch(int signum) {
signal(SIGPIPE, SIG_IGN);
fprintf(stderr, "**** signal(%d) is received !!! ****\n", signum);
e_flag = 1;
}
void usr(int signum) {
fprintf(stderr, "**** signal(%d) is received !!! ****\n", signum);
}
int main() {
signal(SIGPIPE, catch);
signal(SIGUSR1, usr);
ssize_t nwrites;
while(!e_flag) {
fprintf(stderr, "before write: e_flag = %d\n", e_flag);
nwrites = write(STDOUT_FILENO, "hello\n", 6);
fprintf(stderr, "after write: e_flag = %d\n", e_flag);
if (nwrites == -1) {
fprintf(stderr, "error: %s\n", strerror(errno));
assert(errno == EPIPE);
}
pause();
}
return 0;
}
signal handlerで出力するのは本来やってはいけない?はずだけど、検証のために一旦無視する。
write(2)は POSIX.1 で async-signal-safe であることが要求されているので、そちらを使えば問題ないはず
fprintf(3) は内部でバッファを管理しているのでだめ
詳しくは signal-safety(7) を見たら書いてある。
SIGPIPEが発行されるように、 head -2
パイプして実行する。
別の端末からシグナル USR1 を実行していくと、headが終了してSIGPIPEが発生する。
- helloが出力される
- シグナルを待つ
- USR1 シグナルを受け取る
- helloが出力され、headプロセスが終了する
- USR1シグナルを受け取る
- helloを書き込もうとして、SIGPIPEシグナルが発生する
- e_flag = 1がセットされる
- write(2)で
EPIPE
エラーが発生する
$ ./signal.exe | head -2
before write: e_flag = 0
after write: e_flag = 0
hello
**** signal(10) is received !!! ****
before write: e_flag = 0
after write: e_flag = 0
hello
**** signal(10) is received !!! ****
before write: e_flag = 0
**** signal(13) is received !!! ****
after write: e_flag = 1
error: Broken pipe
**** signal(10) is received !!! ****
$ pkill --signal USR1 signal.exe
$ pkill --signal USR1 signal.exe
$ pkill --signal USR1 signal.exe
3日目(sigaction)
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
};
sigactionはシグナルを受信したときの動作を設定することができる。 SIGKILL
と SIGSTOP
以外のvalidなシグナルに対して動作を設定できる。
signum
は設定したいシグナルの番号を指定する。act
が NULLでなければ、signum
に対する新しい動作を act
から設定する。 oldact
が NULLでなければ設定前の act
の設定を取得する。
SIG_DFL
もしくは SIG_IGN
は sa_handler
に設定する。handlerの関数ポインタを指定する場合は、 sa_sigaction
に設定し、 sa_flags
に SA_SIGINFO
を設定する。
sa_flags
は 設定値を bitで設定する。
handler の関数ポインタのシグネチャ
void
handler(int sig, siginfo_t *info, void *ucontext);
sig
: handlerを呼び出す原因となったシグナルの番号
info
: siginfo_t
へのポインタで、シグナルの情報を保持している。
siginfo_t {
int si_signo; /* Signal number */
int si_errno; /* An errno value */
int si_code; /* Signal code */
int si_trapno; /* Trap number that caused
hardware-generated signal
(unused on most architectures) */
pid_t si_pid; /* Sending process ID */
uid_t si_uid; /* Real user ID of sending process */
int si_status; /* Exit value or signal */
clock_t si_utime; /* User time consumed */
clock_t si_stime; /* System time consumed */
union sigval si_value; /* Signal value */
int si_int; /* POSIX.1b signal */
void *si_ptr; /* POSIX.1b signal */
int si_overrun; /* Timer overrun count;
POSIX.1b timers */
int si_timerid; /* Timer ID; POSIX.1b timers */
void *si_addr; /* Memory location which caused fault */
long si_band; /* Band event (was int in
glibc 2.3.2 and earlier) */
int si_fd; /* File descriptor */
short si_addr_lsb; /* Least significant bit of address
(since Linux 2.6.32) */
void *si_lower; /* Lower bound when address violation
occurred (since Linux 3.19) */
void *si_upper; /* Upper bound when address violation
occurred (since Linux 3.19) */
int si_pkey; /* Protection key on PTE that caused
fault (since Linux 4.6) */
void *si_call_addr; /* Address of system call instruction
(since Linux 3.5) */
int si_syscall; /* Number of attempted system call
(since Linux 3.5) */
unsigned int si_arch; /* Architecture of attempted system call
(since Linux 3.5) */
}
si_signo
si_errno
si_code
はすべてのシグナルで設定されるが、その他の項目については、シグナルの種類によって設定されたりされなかったりする。
si_code
は、特定のシグナルの場合に対応したコードが設定される。
例えば、 SIGIO/SIGPOLL
シグナルに対応するコードは以下の様になる。
POLL_IN
POLL_OUT
POLL_MSG
POLL_ERR
POLL_PRI
POLL_HUP
詳細については、 man を見る。
シグナルを見てると、SIGILL
や SIGFPE
とか、 SIGSEGV
だけじゃなく、SIGPOLL
みたいなものもあるので、カーネルから何かしらのイベントを通知するために汎用的に使われているっぽい。
スレッドとか、forkした場合の動きなどはまた調べる。
(シグナルが関係している内容多すぎるので、他のシステムコール調べるときに関連があったら調べる)
SIGUSR1
を受け取ったら情報を表示するプログラム
void handler(int signum, siginfo_t *info, void *context) {
printf("signum: %d\n", signum);
printf("signo: %d, errno: %d, code: %d\n", info->si_signo, info->si_errno, info->si_code);
}
int main() {
struct sigaction act;
bzero(&act, sizeof(struct sigaction));
act.sa_sigaction = handler;
sigemptyset(&act.sa_mask);
act.sa_flags = SA_SIGINFO;
sigaction(SIGUSR1, &act, NULL);
pause();
}
4日目(open)
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
open(2)
は、指定された pathname
のファイルを開く。指定されたファイルが存在せず、 O_CREAT
フラグが指定されている場合はファイルが作成される。
open
はファイルディスクリプタを返す。ファイルディスクリプタは、非負な小さい整数値で、read(2)
や write(2)
で特定のファイルを指すために使用される。ファイルディスクリプタの数値は、その時点で使用されていない最小の数値が使用されることが保証される。
デフォルトで、ファイルディスクリプタは execve(2)
が実行されてもopenされたままになる。ファイルのオフセットはファイルの先頭になる。
open()
は、open file description(オープンファイル記述)を作成するが、これはシステム全体で共有なオープンファイルテーブルのエントリである。
オープンファイル記述は、ファイルのオフセットとファイルのステータスフラグを保持している。
ファイルディスクリプタは、オープンファイル記述へのへの参照であり、これはパス名が変更されたり別のファイルを参照するようになっても影響を受けない。
続くフラグは、以下のどれかのアクセスモードに関するフラグが設定されている必要がある。
O_RDONLY
O_WRONLY
O_RDWR
その他のフラグは、論理和で指定することができ、オープンの動作を制御するもので以降のI/Oにも影響を与えるフラグ(file creation flags)と、その後のI/O操作に影響を与えるフラグ(file status flag)に分かれている。
file creation flags
-
O_CLOEXEC
: exec時にファイルをcloseする -
O_CREAT
: pathnameのファイルが存在しなければファイルを作成する。 -
O_DIRECTORY
: pathnameがディレクトリでなければオープンに失敗する。 -
O_EXCL
:O_CREAT
と使用された場合、pathnameのファイルが存在していた場合、オープンに失敗する。O_CREAT
を指定せずにO_EXCL
のみ使用した場合の動作は未定義である。 -
O_NOCTTY
: pathnameが端末である場合、制御端末になることを抑制する。端末でない場合は単に無視される -
O_TMPFILE
: pathnameに指定されたディレクトリのinodeに、名前を持たないテンポラリファイルのエントリを作成する。何か書き込みが行われても、ファイルがクローズされると書き込んだ内容は消える。名前を設定することで永続化することができる。 -
O_TRUNC
: pathnameのファイルが存在し、書き込み許可があれば、サイズを0にする。ファイルがFIFOや制御端末だった場合、このフラグは無視される
file status flag
-
O_APPEND
: 書き込み操作時にアトミックにファイルオフセットがファイルの最後に設定される。 -
O_ASYNC
: 入力や出力が可能になった時点でSIGIOシグナルが発生するようにする。この昨日はターミナルや擬似端末、ソケット、パイプ、FIFOキューに適用できる。 -
O_DIRECT
: I/Oにバッファキャッシュを用いないようにする。基本的にはパフォーマンスが悪化するが、DBなど独自のキャッシュをもつアプリケーションでは有効に働く -
O_DSYNC
: ファイルへの書き込みを同期I/Oとする。書込み後に、fdatasync(2)
が実行されたかのように振る舞う -
O_LARGEFILE
: 32ビットで表すことができないが64ビットで表すことのできるサイズのファイルをオープンできるようにする。64ビット環境では無視される -
O_NOATIME
: ファイルがread(2)
された場合に atimeを更新しない。(ただしファイルオーナーが自分もしくは特権を持っている場合のみ指定可能で、条件を満たしていない場合はEPERM
を返す。 -
O_NOFOLLOW
: シンボリックリンクの場合に、シンボリックリンクを辿らずにELOOP
を返す。 -
O_NONBLOCK
: ノンブロッキングモードを有効にする。open()自体も、ファイルディスクリプタへのIO操作もプロセスを待たせることはない。 -
O_PATH
: ファイルを読み書きする必要はないが、pathnameに対する参照を使用したい場合に使用する。*at() 系のシステムコールに使用するのが一般的で、 pathnameにはディレクトリを指定し、そのディレクトリのファイルをオープンするときにopenat(2)
などが使用される。その他読み込み権限はないが実行権限がある実行ファイルをオープンする際などに使用される。このオプション付きでオープンされたfdは、いくつかのシステムコールにおいてディレクトリ用のfdとして指定できる。 -
O_SYNC
: ファイルを同期I/O用にオープンする。書込み後にfsync(2)
が実行されたかのように振る舞う
エラーは多いのでまた今度調べる。
O_DIRECT
を使ってみる
単に O_DIRECT
を使ってもエラーが起きる
const char *pathname = "src/open.c";
int fd = openat(AT_FDCWD, pathname, O_RDONLY|O_DIRECT);
if (fd == -1) {
fprintf(stderr, "%s\n", strerror(errno));
exit(1);
}
char buf[4096];
ssize_t nreads;
while((nreads = read(fd, buf, sizeof(buf))) > 0) {
write(STDOUT_FILENO, buf, nreads);
}
if (nreads == -1) {
fprintf(stderr, "%s\n", strerror(errno));
close(fd);
exit(1);
}
close(fd);
$ ./o_direct_open.exe
Invalid argument
read(2)
を見るとこうある
EINVAL fd is attached to an object which is unsuitable for reading; or the file was opened with the O_DIRECT flag, and either the address specified in buf, the
value specified in count, or the file offset is not suitably aligned.
つまり、 O_DIRECT
フラグを設定している場合、バッファのアドレス、カウント、ファイルオフセットすべてがアラインメントされている必要がある。 512バイトにアラインして実行してみるとうまく動く。
const int BUF_SIZE = 4096;
const int ALIGN = 512;
const char *pathname = "src/open.c";
int fd = openat(AT_FDCWD, pathname, O_RDONLY|O_DIRECT);
if (fd == -1) {
fprintf(stderr, "%s\n", strerror(errno));
exit(1);
}
char *buf = aligned_alloc(ALIGN, BUF_SIZE);
ssize_t nreads;
while((nreads = read(fd, buf, BUF_SIZE)) > 0) {
write(STDOUT_FILENO, buf, nreads);
}
if (nreads == -1) {
fprintf(stderr, "%s\n", strerror(errno));
free(buf);
close(fd);
exit(1);
}
free(buf);
close(fd);
O_ASYNC
を使用しようと思ったが、open(2)
時には指定できないバグがあるらしい。
fcntl(2)
でしかフラグを設定できない。
BUGS
Currently, it is not possible to enable signal-driven I/O by specifying O_ASYNC when calling open(); use fcntl(2) to enable this flag.
O_TMPFILE
と O_PATH
を使ってみる
const char *pathname = "/tmp";
int fd = open(pathname, O_TMPFILE|O_RDWR);
if (fd == -1) {
fprintf(stderr, "%s\n", strerror(errno));
exit(1);
}
const char *message = "Hello from open\n";
ssize_t nwrites = write(fd, message, sizeof(message));
if (nwrites == -1) {
fprintf(stderr, "%s\n", strerror(errno));
exit(1);
}
close(fd);
$ ./open.exe
$ ls /tmp/open*
やはり O_TMPFILE
を指定しただけだとファイルが存在しない。
linkat(2)
でファイルを設置してみる
int fd = open("/tmp", O_TMPFILE|O_RDWR, 0644);
if (fd == -1) {
fprintf(stderr, "open fail: %s\n", strerror(errno));
exit(1);
}
const char *message = "Hello from open\n";
ssize_t nwrites = write(fd, message, strlen(message));
if (nwrites == -1) {
fprintf(stderr, "write fail: %s\n", strerror(errno));
exit(1);
}
char path[2048];
snprintf(path, 2048, "/proc/self/fd/%d", fd);
if (linkat(AT_FDCWD, path, AT_FDCWD, "/tmp/open.tmp", AT_SYMLINK_FOLLOW) == -1) {
fprintf(stderr, "linkat fail: %s\n", strerror(errno));
exit(1);
}
close(fd);
$ cat /tmp/open.tmp
Hello from open
これをうまいこと使えば、アトミックな書き込みが実現できるかもしれない
5日目 (open)
オープンのエラーについて調べる
- EACCES: 要求されたファイルへのアクセスが許可されていないか、pathnameのディレクトリに対して検索の権限がないか、ファイルが存在していなくて親ディレクトリへの書き込み権限がないかその他権限のエラー
- EBUSY: O_EXCLが指定されていて、pathnameがブロックデバイスを指している場合
- EDQUOT: O_CREAT が指定されていてファイルが存在しないとき、ユーザーの ディスクブロックもしくはi-nodeのquotaが足りない
- EEXIST: O_CREATとO_EXCLを指定しているときに、pathnameがすでに存在している(O_CREATは、ファイルがしない場合にファイルを作成するので、O_EXCLを指定しない場合は単に無視される)
- EFAULT: pathnameがアクセス不可能なポインタ(nullポインタとか)
- EFBIG: EOVERFLOWを参照
- EINTR: 遅いデバイス(FIFOとか)のオープンを待っている間に、システムコールがシグナルハンドラによって割り込まれたとき
- EINVAL: O_DIRECTがサポートされていないとき, フラグに無効な値がある, O_TMPFILEが指定されているのに、書き込みのフラグが立っていない(O_WRONLYもO_RDWRも指定されていない)、basename部分に許可されていない文字が含まれているとき、
- EISDIR: pathnameがディレクトリを指しているときに、O_WRONLYが指定されているとき、pathnameがディレクトリを指し O_TMPFILEと O_WRONLYかO_RDWRのどちらかが指定されているがカーネルのバージョンがO_TMPFILEをサポートしていないとき
- ELOOP: pathnameの解決時に、大量のシンボリックの解決が必要になったとき, pathnameがシンボリックリンクを指しているが O_NO_FOLLOWを指定しているとき(O_PATHを指定している場合はエラーは起きない)
- EMFILE: プロセスごとのオープンできるファイルの上限に達している
- ENAMETOOLONG: pathnameが長すぎる
- ENFILE: システム全体でオープンできるファイルの上限に達した
- ENOENT: O_CREATが指定されず、ファイルが存在しない, pathnameのどれかのディレクトリが存在しない、もしくは参照先が存在しないシンボリックリンク, O_TMPFILEが指定され、必要なフラグがセットされているが カーネルがO_TMPFILEをサポートしていない場合
- ENOMEM: カーネルのメモリが足りていない, FIFOのためのユーザーごとのメモリリミットに達した
- ENOSPC: pathnameを作成するが、pathnameを含んでいるデバイスに新しいファイルのための空間がない
- ENOTDIR: O_DIRECTORYが指定されているがpathnameがディレクトリでない, pathnameに含まれるディレクトリ部分のどれかがディレクトリではない
- ENXIO: O_NONBLOCK|O_WRONLYが指定されており、オープンしたFIFOをreadしているプロセスが存在しない, またはデバイススペシャルファイルでファイルが存在しない, ファイルがunix domain socketである
- EOPNOTSUPP: pathname を含んでいるファイルシステムが O_TMPFILE をサポートしていない。
- EOVERFLOW: 大きすぎてオープンできないファイル。32bitシステムでファイルサイズが (1<<31) -1バイトを超えるファイルを開こうとしたときに発生する。2.6.24より前のlinuxではEBIGを返していた
- EPERM: O_NOATIMEを指定した際に、実行ユーザーIDがファイルのオーナーと一致しない, 操作が file seal により禁止されている
- EROFS: pathnameがread-onlyなファイルシステムを指しているが、書き込みのリクエストを受け取った
- ETXTBSY: pathnameが実行ファイルを参照していて、実行中に書き込みリクエストを受け取った, pathnameが現在使用されているスワップファイルを参照しており、O_TRUNCが指定されている, pathnameがカーネルにより読み取られているファイルを参照しており、書き込みリクエストを受け取った(例えば、ファームウェアのロード)
- EWOULDBLOCK: O_NONBLOCKが指定されたが、無効なリースを保持している
- EBADF: 無効なファイルディスクリプタ
EBUSYを起こすコード
ブロックデバイスファイルに対して、O_EXCL
を指定してopenすると発生した
int main() {
int fd = open("/dev/nvme0n1", O_RDONLY | O_EXCL);
if (fd == -1) {
printf("open fail: %s\n", strerror(errno));
}
close(fd);
return 0;
}
$ cc open.c -o /tmp/open
$ sudo /tmp/open
open fail: Device or resource busy
EMFILEを起こすコード
setrlimitでRLIMIT_NOFILEを強制的に3にして、openすることで発生した
struct rlimit limit;
// get
if (getrlimit(RLIMIT_NOFILE, &limit) == -1) {
fprintf(stderr, "get rlimit fail: %s\n", strerror(errno));
exit(1);
}
printf("%ld %ld\n", limit.rlim_cur, limit.rlim_max);
// set
limit.rlim_cur = 3;
if (setrlimit(RLIMIT_NOFILE, &limit) == -1) {
fprintf(stderr, "set rlimit fail: %s\n", strerror(errno));
exit(1);
}
int fd = open("/tmp/open.c", O_PATH);
if (fd == -1) {
fprintf(stderr, "open fail: %s\n", strerror(errno));
exit(1);
}
return 0;
$ cc /tmp/open.c -o /tmp/open
$ /tmp/open
1024 1048576
open fail: Too many open files
同期IOに関して
O_SYNCとO_DSYNCの違いとして、O_DSYNCは後続の読み取り操作に関連するデータだけをflushすることを保証する。
たとえば、ファイルの更新時には確実に最終更新時刻が更新されるが、ファイル長はファイルの末尾に書き込みが発生する場合のみ更新される。
最終更新時刻は、後続の読み取りが正常に完了するためには必要ないが、ファイル長は必須なので、O_DSYNCはファイル長のメタデータは確実に更新する。
fifoをオープンする場合、自分が書き込む場合は読み込み側が、自分が読み込む場合は書き込む側が開かれるまでopenはブロックされる