Unix V6のpipe()と比べてLinuxのpipe()どう実装しているんですかねーなんて話を最近したのでちょっと見比べてみた。 V6のpipeははじめてのOSコードリーディング 読書会 (15)でやったところなんだけど、俺は風邪ひいて家で引きこもってたので・・・
V6のpipeの挙動は大体こんな感じ。
- pipeはルートディスクのストレージ領域を4096B(8ブロック)使用し実現する
- 4KBのデータ領域はひとつのファイルとして扱われる(inodeが割り当てられる)
- オンメモリではなくてストレージを経由するが、バッファサイズが小さいのでブロックデバイスのバッファキャッシュが効きやすいようになっている
- pipeの受け手がデータを読みだす前に他の優先度の高いプロセスがブロックデバイスを使用するとキャッシュが効かなくなる可能性あり
- でも、データはストレージに書かれているので内容が壊れると言ったことは無い
- pipeの送り手はinode[]エントリのアドレス+1、受け手は+2を引数にsleep()を呼び出す(本来の引数は秒数だけど、アドレスを渡す) * sleep()の使い方に驚いたけど、あとは妥当な感じかな。pipeのデータ領域としてストレージを使っているけど当時のメモリ容量とか考えるとストレージのほうが妥当だったのかもしれないし、この辺は当時の判断なので特に気にせずにいく。
では、Linuxのほう見ていこう。 大まかなところはこんな感じに。
- メモリ上にデータが置かれる
- pipefsという擬似ファイルシステムによって実現される
- pipefsはユーザーランドからは見ることができない
- pipeは擬似ファイルとして扱われるのでファイルに関するデータ構造を持つ(inode、dentry等)
大雑把な違いとしてはV6の頃のpipeはストレージ領域にデータを持つ実体のあるファイルだったのに対して、Linuxの場合は擬似ファイルシステムによって実現されるメモリ上のみのファイルというところかな。
pipeは擬似ファイルなのでfd、inodeがあるのでpipeを作ってinodeを表示させるこんなものを作ってみて、
#include <stdio.h> #include <unistd.h> #include <fcntl.h> int main(int argc, char **argv) { int i, apipe[2]; printf("pid: %d\n", getpid()); if (pipe(apipe) == -1) { printf("create pipe failed\n"); return -1; } for (i = 0; i < 2; i++) { struct stat st; fstat(apipe[i], &st); printf("pipe[%d] fd:%d inode:%d\n", i, apipe[i], st.st_ino); } sleep(120); close(apipe[0]); close(apipe[1]); return 0; }
実行するとこうなる。
masami@kerntest:~$ ./a.out pid: 1996 pipe[0] fd:3 inode:13738 pipe[1] fd:4 inode:13738
このプロセスが生きているうちに/procでfdを見るとこのようになっている。
[root@kerntest ~]# ls -lai /proc/1996/fd total 0 13740 dr-x------ 2 masami masami 0 May 31 16:59 . 13739 dr-xr-xr-x 8 masami masami 0 May 31 16:59 .. 13745 lrwx------ 1 masami masami 64 May 31 16:59 0 -> /dev/pts/0 13746 lrwx------ 1 masami masami 64 May 31 16:59 1 -> /dev/pts/0 13747 lrwx------ 1 masami masami 64 May 31 16:59 2 -> /dev/pts/0 13748 lr-x------ 1 masami masami 64 May 31 16:59 3 -> pipe:[13738] 13749 l-wx------ 1 masami masami 64 May 31 16:59 4 -> pipe:[13738]
pipeにはfd3と4が振られてinodeは13738という感じ。
pipefsの初期化コードはこのinit_pipe_fs()。
1312 static int __init init_pipe_fs(void) 1313 { 1314 int err = register_filesystem(&pipe_fs_type); 1315 1316 if (!err) { 1317 pipe_mnt = kern_mount(&pipe_fs_type); 1318 if (IS_ERR(pipe_mnt)) { 1319 err = PTR_ERR(pipe_mnt); 1320 unregister_filesystem(&pipe_fs_type); 1321 } 1322 } 1323 return err; 1324 }
register_filesystem()でファイルシステムを登録するのは通常のファイルシステムと一緒。違いはsys_mount()によるmountの流れを全く踏んでいないところ。 通常、ユーザーランドからmountを実行する場合はmountコマンドから色々合ってsys_mount()が実行されてという流れになる(ここはだいぶ前にはてダのほうに書いたんだけど、すっかり忘れてたw)。日記のタイトルはsys_mount()を読んでみる。なので興味のある人はどぞ。しかし、vfs_kern_mount()って何?と思ってググったら自分の日記が引っかかるのはね。。。
それはさておきsys_mount()を使ったmountの流れというのはsys_mount() -> do_mount() -> do_new_mount() -> vfs_kern_mount()という感じなんだけどpipefsのほうはvfs_kern_mount()が一気に呼ばれる形。
実際のところkern_mount()はkern_mount_data()のdefineだし、
1893 #define kern_mount(type) kern_mount_data(type, NULL)
kern_mount_data()はvfs_kern_mount()を呼ぶ位しか処理がない。
2867 struct vfsmount *kern_mount_data(struct file_system_type *type, void *data) 2868 { 2869 struct vfsmount *mnt; 2870 mnt = vfs_kern_mount(type, MS_KERNMOUNT, type->name, data); 2871 if (!IS_ERR(mnt)) { 2872 /* 2873 * it is a longterm mount, don't release mnt until 2874 * we unmount before file sys is unregistered 2875 */ 2876 real_mount(mnt)->mnt_ns = MNT_NS_INTERNAL; 2877 } 2878 return mnt; 2879 }
pipefsのmount処理はこれくらい。 pipeの作成はpipe(2)なのは当然として、システムコールは引数を1つとるsys_pipe()と引数を2つとるsys_pipe2()がある。ユーザーランド側のpiep(2)は引数はfdが入る配列のアドレスを渡す感じだけど、最終的には2個の引数が渡る形に。ただ、sys_pipe()はsys_pipe2()に引数を追加して呼び出しているだけなので実質はsys_pipe2()がpipe作成のシステムコールになっている。 ちなみに、GNUの拡張ではpipe2(2)があってこちらはfdの配列の後にflagsを渡すことができる。(man pipe)
pipe作成時の主要な関数は__do_pipe_flags()。処理内容はcreate_pipe_files()で擬似ファイルの作成、get_unused_fd_flags()で未使用のfd番号取得というところ。 create_pipe_files()でファイルを作っているのでここでinodeも取得するわけだけど、inodeはget_pipe_inode()という関数でpipe用にinodeの取得が行われる。実際のstruct inodeに関してはnew_inode_pseudo()という擬似ファイルシステム用にnew_inode_pseudo()というのがあるみたい。
836 static struct inode * get_pipe_inode(void) 837 { 838 struct inode *inode = new_inode_pseudo(pipe_mnt->mnt_sb);
これは確かにそうだなってところで、inodeの管理はファイルシステムが行うわけで(例えばext4ならこんな感じ ext4:ディスクレイアウト調査中めも1)、擬似ファイルシステム用にinode管理の機能が必要になりますね。
create_pipe_files()では(ユーザーランドからは見えないけど)dentryの設定もする。
888 path.dentry = d_alloc_pseudo(pipe_mnt->mnt_sb, &name);
そしてfile構造体、未使用のfdが見つかったところでsys_pipe2()に戻り、エラーが無ければcopy_to_user()を実行してここでもエラーがなければ990行目のelseに入って作成したfile構造体とfdが結びつく。
982 error = __do_pipe_flags(fd, files, flags); 983 if (!error) { 984 if (unlikely(copy_to_user(fildes, fd, sizeof(fd)))) { 〜略〜 990 } else { 991 fd_install(fd[0], files[0]); 992 fd_install(fd[1], files[1]); 993 } 994 }
これで後はlibc -> ユーザープログラムと戻ってpipeが使えるようになる。 ここでpipeがサポートしている操作(file_operations)を見てみるとこのように。
1145 const struct file_operations pipefifo_fops = { 1146 .open = fifo_open, 1147 .llseek = no_llseek, 1148 .read = do_sync_read, 1149 .aio_read = pipe_read, 1150 .write = do_sync_write, 1151 .aio_write = pipe_write, 1152 .poll = pipe_poll, 1153 .unlocked_ioctl = pipe_ioctl, 1154 .release = pipe_release, 1155 .fasync = pipe_fasync, 1156 };
割と色々な処理が使えることがわかり、read・writeに関してはfs共通の関数が設定されていて、asyncの場合にpipefs独自の関数が使われるようになっている。と言ってもdo_sync_read()、do_sync_write()ともにpipe_read()、pipe_write()を呼び出すんですけどね。 例えば、do_sync_write()だと見ての通りファイル構造体のfile_operationsにあるaio_write()を呼びます。
423 ssize_t do_sync_write(struct file *filp, const char __user *buf, size_t len, loff_t *ppos) 424 { 425 struct iovec iov = { .iov_base = (void __user *)buf, .iov_len = len }; 426 struct kiocb kiocb; 427 ssize_t ret; 428 429 init_sync_kiocb(&kiocb, filp); 430 kiocb.ki_pos = *ppos; 431 kiocb.ki_nbytes = len; 432 433 ret = filp->f_op->aio_write(&kiocb, &iov, 1, kiocb.ki_pos); 434 if (-EIOCBQUEUED == ret) 435 ret = wait_on_sync_kiocb(&kiocb); 436 *ppos = kiocb.ki_pos; 437 return ret; 438 }
実際にftraceで見るとsys_writeから最終的にpipe_write()が呼ばれているのがわかりますね。
a.out-2150 [002] .... 26366.889403: SyS_write <-system_call_fastpath a.out-2150 [002] .... 26366.889403: __fdget_pos <-SyS_write a.out-2150 [002] .... 26366.889403: __fget_light <-__fdget_pos a.out-2150 [002] .... 26366.889404: vfs_write <-SyS_write a.out-2150 [002] .... 26366.889404: rw_verify_area <-vfs_write a.out-2150 [002] .... 26366.889405: security_file_permission <-rw_verify_area a.out-2150 [002] .... 26366.889405: cap_file_permission <-security_file_permission a.out-2150 [002] .... 26366.889405: do_sync_write <-vfs_write a.out-2150 [002] .... 26366.889406: pipe_write <-do_sync_write
そんなわけで、pipe_read()、pipe_write()を見ようかなと思いつつ全部読むのも疲れてきたので概要だけ摘んでおく。
pipeのデータはfile構造体のprivate_dataという(void *)な変数で管理されていて、読み書きはここを通じて行われる。
500 struct pipe_inode_info *pipe = filp->private_data;
pipe_read()、pipe_write()なんかはファイル用のオペレーションでこれとは別にpipe用のオペレーション構造体としてpipe_buf_operationsと言うものがある。operationsという名前なのに状態を保持する変数もいるけど。。 この状態変数はcan_mergeという名前で、こいつは既存のバッファに新たに入ってきたデータをマージしてよいかどうかをセットする変数。 これ、通常は1がセットされていてmerge可だけど、pipeをパケットモードというモードで作る場合はmerge不可になる。
352 static const struct pipe_buf_operations anon_pipe_buf_ops = { 353 .can_merge = 1, 354 .map = generic_pipe_buf_map, 355 .unmap = generic_pipe_buf_unmap, 356 .confirm = generic_pipe_buf_confirm, 357 .release = anon_pipe_buf_release, 358 .steal = generic_pipe_buf_steal, 359 .get = generic_pipe_buf_get, 360 }; 361 362 static const struct pipe_buf_operations packet_pipe_buf_ops = { 363 .can_merge = 0, 364 .map = generic_pipe_buf_map, 365 .unmap = generic_pipe_buf_unmap, 366 .confirm = generic_pipe_buf_confirm, 367 .release = anon_pipe_buf_release, 368 .steal = generic_pipe_buf_steal, 369 .get = generic_pipe_buf_get, 370 };
これはさっきGNU拡張の話を出したんですがそれと絡んでいてflagsにO_DIRECTをセットするとpacket_pipe_buf_opsが作られるらしい。
その他にはpipeへの読み書きでメモリを確保するにはpipe_buf_operationsの->map()、->unmap()を使うなど。これはatomicに処理したいかどうかで実際に使われる関数は変わるけど、atmoicにやるならkmap_atomic_prot()、そうじゃなければkmap()を使う。この辺はメモリ確保のやり方の問題なのでpipe間でデータをやり取りするって言う意味ではちょっと別の話になるけど。
pipe_read()の大まかな流れはこのような形。
- バッファのデータを全部読み込む
- 書き手がいなければ読み出し終了
- バッファが書き込み可能になるのを待ってた書き手がいなければ終了
- signal割り込みが途中で合った場合に-ERESTARTSYSを返すようにして終了
- バッファを読みきった場合は書き手を起こす
- pipe_wait()で読み出し可能になるまで待つ
- 1〜6の手順中でreadのループを抜けたら書き手を起こす
pipe_write()の大まかな流れはこのような形。
- データのマージが可能なら(can_merge == 1)すべてのデータを書き込んで9に飛ぶ
- 書き込むデータがある限りデータの書き込みを実施
- file構造体のflags変数にO_NONBLOCKが立っていた場合は-EAGAINを返すようにして書き込み処理終了
- signal割り込みが途中で合った場合に-ERESTARTSYSを返すようにして書き込み処理終了
- データの書き込みが全部終わっていればここで読み手を起こす
- ここでwaiting_writersをインクリメントして俺は書き込みを待っていると宣言する
- pipe_wait()で読み出し可能になるまで待つ
- pipe_wait()での待ちが終わったらwaiting_writersをデクリメントして待ち人の数を減らす
- 2~8の処理で書き込みのループを抜けたら読み手を起こす
- エラーもなく書き込み完了できたらsb_end_write()を読んでpipefsに対しての書き込みを終了する
はじめてのOSコードリーディング ~UNIX V6で学ぶカーネルのしくみ (Software Design plus)
- 作者: 青柳隆宏
- 出版社/メーカー: 技術評論社
- 発売日: 2013/01/09
- メディア: 単行本(ソフトカバー)
- 購入: 56人 クリック: 1,959回
- この商品を含むブログ (26件) を見る