関連記事
- Part 1: 環境セットアップ
- Part 2: System call Interface
- Part 3: VFS
- Part 4: ext2 (1) write_iter
- Part 5: ext2 (2) write_begin
- Part 6: ext2 (3) get_block
- Part 7: ext2 (4) write_end
- Part 8: writeback (1) work Queue
- Part 9: writeback (2) wb_writeback
- Part 10: writeback (3) writepages
- Part 11: writeback (4) write_inode
- Part 12: block (1) submit_bio
- Part 13: block (2) blk_mq
- Part 14: I/O scheduler (1) mq-deadline
- Part 15: I/O scheduler (2) insert_request
- Part 16: I/O scheduler (3) dispatch_request
- Part 17: block (3) blk_mq_run_work_fn
- Part 18: block (4) block: blk_mq_do_dispatch_sched
- Part 19: MMC (1) initialization
- Part 20: PL181 (1) mmci_probe
- Part 21: MMC (2) mmc_start_host
- Part 22: MMC (3) mmc_rescan
- 概要
- はじめに
- ユーザプログラム
- システムコールハンドラ
- ファイルディスクリプタからfd構造体を取得する
- ファイルディスクリプタからfile構造体を取得する
- ファイルの現在のオフセットを取得する
- VFSのwrite処理を呼び出す
- おわりに
- 変更履歴
- 参考
概要
QEMUの vexpress-a9 (arm) で Linux 5.15を起動させながら、ファイル書き込みのカーネル処理を確認していく。
本章では、ksys_write
関数からvfs_write
関数を呼ぶところまでを確認した。
はじめに
ユーザプロセスからファイルシステムという機構によって記憶装置上のデータをファイルという形式で書き込み・読み込みすることができる。
本調査では、ユーザプロセスがファイルに書き込み要求を実行したときにLinuxカーネルではどのような処理が実行されるかを読み解いていく。
調査対象や環境などはPart 1: 環境セットアップを参照。
ファイルアクセスの一連の処理をシーケンスとしてあらわしたときに、赤枠の部分が該当する。
本記事では、Syscall interfaceの途中 (SYSCALL_DEFINE
)からVFSの関数を呼び出すところまでとする。
ここより上のレイヤー(Application部やsystem call interfaceの上位部)の解説は今回は省略する。
ユーザプログラム
下記の記事では、システムコールについて説明されている。 システムコールを一から学習したい人におすすめ。
システムコールハンドラ
writeシステムコールは、SYSCALL_DEFINE
マクロによって定義される。
// 656: SYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf, size_t, count) { return ksys_write(fd, buf, count); }
writeシステムコールの実態は、ksys_write()
となっている。
ksys_write()
の定義は下記のようになっている。
// 636: ssize_t ksys_write(unsigned int fd, const char __user *buf, size_t count) { struct fd f = fdget_pos(fd); ssize_t ret = -EBADF; if (f.file) { loff_t pos, *ppos = file_ppos(f.file); if (ppos) { pos = *ppos; ppos = &pos; } ret = vfs_write(f.file, buf, count, ppos); if (ret >= 0 && ppos) f.file->f_pos = pos; fdput_pos(f); } return ret; }
ksys_write()
には、大きく分けて3つ処理がある。
ファイルディスクリプタからfd
構造体を取得する
ファイル構造体を取得するまでの流れは下記のようなものとなっており、マルチスレッドであった場合に限りファイルディスクリプタテーブルにロックをかけるといった処理がされる。
ちなみに、FMODE_PATH
はO_PATH
フラグを指定してファイルをopenしたときに付与されるフラグであり、その場合は書き込みを失敗させる。
file
構造体の取り扱い方については、カーネルドキュメントが用意されているので気になる人はそちらを読むことを推奨する。
ksys_write
関数で実行されるfdget_pos
関数について定義を確認する。
fdget_pos
関数は下記のような定義である。
// 73: static inline struct fd fdget_pos(int fd) { return __to_fd(__fdget_pos(fd)); }
fdget_pos
関数の処理は単純で下記の2点のみである。
- file構造体に
flags
を付与して、fd
構造体を呼び出し元に返す __fdget_pos
関数でファイルディスクリプタからfile構造体を取得する
まずは、__to_fd
関数の定義から確認する。
__to_fd
関数は下記のような定義である。
// 58: static inline struct fd __to_fd(unsigned long v) { return (struct fd){(struct file *)(v & ~3),v & 3}; }
引数として与えられたv
から、下位2bitに格納されたフラグを抽出する。(アドレス空間は4bytes or 8bytesの管理されるので、file構造体のアドレスに影響はない)
上記のフラグと下位2bitをマスクしたfile構造体へのポインタをfd
構造体に代入する。
ちなみにfd
構造体の定義は下記の通りである。
// 36: struct fd { struct file *file; unsigned int flags; };
ここまでの__to_fd
関数の流れのイメージを下記に示す。
__fdget_pos
関数(後述)によって、ファイルディスクリプタからファイル構造体のアドレスを取得する__fdget_pos
関数は、取得したアドレスをunsigned long
型の変数v
に代入し、下位2bitにフラグを代入する__to_fd
関数は、変数v
からfile
構造体とflags
を取得して、fd
構造体を返す
ファイルディスクリプタからfile構造体を取得する
fdget_pos
関数は、 ファイルディスクリプタからfile構造体を取得するために__fdget_pos
関数を呼び出す。
// 982: unsigned long __fdget_pos(unsigned int fd) { unsigned long v = __fdget(fd); struct file *file = (struct file *)(v & ~3); if (file && (file->f_mode & FMODE_ATOMIC_POS)) { if (file_count(file) > 1) { v |= FDPUT_POS_UNLOCK; mutex_lock(&file->f_pos_lock); } } return v; }
__fdget_pos
関数の処理は下記の2点のみである。
__fdget
関数でファイルディスクリプタからfile
構造体のアドレスを取得する (unsigned long
型bに代入していたり、~3
とAND演算しているのは前述の理由)- 同じファイルが複数openされている場合、オフセットのためのmutex_lockを取得する (取得したロックは、
ksys_write
関数の最後に実行しているfdput_pos
関数で解放する)
まず、__fdget
関数とその呼び出し先の__fget_light
関数の定義を確認する。
// 971: unsigned long __fdget(unsigned int fd) { return __fget_light(fd, FMODE_PATH); }
// 954: static unsigned long __fget_light(unsigned int fd, fmode_t mask) { struct files_struct *files = current->files; struct file *file; if (atomic_read(&files->count) == 1) { file = files_lookup_fd_raw(files, fd); if (!file || unlikely(file->f_mode & mask)) return 0; return (unsigned long)file; } else { file = __fget(fd, mask, 1); if (!file) return 0; return FDPUT_FPUT | (unsigned long)file; } }
__fget_light
関数では、現在のプロセスのファイルディスクリプタテーブルを取得して、状況に応じてロックを取得する。
ここで、task_struct
構造体からfile
構造体までの関係性についておさらいする。
current
変数は、現在のプロセスのtask_struct
構造体を指すtask_struct
構造体は、現在openしているファイルを管理するためにfiles_struct
型のポインタを変数files
で管理するfiles_struct
構造体は、fdtable
型の変数fdt
でファイルディスクリプタのテーブルを管理するfdtable
構造体は、file
構造体の配列を管理する。
これらの関係を図示したものが以下である。
__fget_light
関数でも上記の規則と同様に、current
変数からfiles_struct
構造体を取得している。
しかし、シングルスレッドかマルチスレッドの場合で、files_struct
以降の取り扱いが変わる。
シングルスレッドの場合
シングルスレッドの場合 (つまり、atomic_read(&files->count) == 1
) は、対象のファイルディスクリプタテーブルを保有しているは自分のみである。
そのため、それぞれの変数に対して、排他制御を意識する必要はない。
files_lookup_fd_raw
関数の定義は以下のようになっている。
// 83: static inline struct file *files_lookup_fd_raw(struct files_struct *files, unsigned int fd) { struct fdtable *fdt = rcu_dereference_raw(files->fdt); if (fd < fdt->max_fds) { fd = array_index_nospec(fd, fdt->max_fds); return rcu_dereference_raw(fdt->fd[fd]); } return NULL; }
files_lookup_fd_raw
関数は、Linuxカーネル v5.10まではfcheck_files
関数という名前で定義されていた。
fcheck_files
関数についての注意点は、カーネルドキュメントにも記載されている。
4 To look up the file structure given an fd, a reader must use either fcheck() or fcheck_files() APIs.
These take care of barrier requirements due to lock-free lookup. https://www.kernel.org/doc/Documentation/filesystems/files.txt
file
構造体を取得した後に、そのファイルのopenフラグを確認する。
FMODE_PATH
フラグが立っていた場合、ファイル操作は無効となっているので0を返す必要がある。
マルチスレッドの場合
マルチスレッドの場合 (つまり、atomic_read(&files->count) > 1
) は、排他制御を意識する必要がある。
__fget
関数と呼び出し先の__fget_files
関数の定義は以下のようになっている。
// 867: static inline struct file *__fget(unsigned int fd, fmode_t mask, unsigned int refs) { return __fget_files(current->files, fd, mask, refs); }
// 844: static struct file *__fget_files(struct files_struct *files, unsigned int fd, fmode_t mask, unsigned int refs) { struct file *file; rcu_read_lock(); loop: file = files_lookup_fd_rcu(files, fd); if (file) { /* File object ref couldn't be taken. * dup2() atomicity guarantee is the reason * we loop to catch the new file (or NULL pointer) */ if (file->f_mode & mask) file = NULL; else if (!get_file_rcu_many(file, refs)) goto loop; } rcu_read_unlock(); return file; }
files_lookup_fd_rcu
関数は、files_lookup_fd_raw
関数(シングルスレッドの場合を参照)のラッパー関数である。
シングルスレッドの場合と大きく異なるのは、rcu_read_lock
/rcu_read_unlock
命令を発行している点である。
rcu_read_lock()
はプリエンプションを無効にする命令rcu_read_unlock()
はプリエンプションを有効にする命令
コメントにも書いてあるが、dup2()
が要因でファイルカウントがおかしくなることがあるため、file
構造体の取得をループさせることがある。
file
構造体の取得後にプリエンプションが発生して、別にfile
構造体を操作されると整合性がとれなくなってしまう。
__fget_files
関数ではこの関数内でFMODE_PATH
フラグの確認もしている。
シングルスレッドの場合と同様に、上記のフラグが立っていたら0を返す。
また、マルチスレッドの場合にはFDPUT_FPUT
フラグを立てる。
ファイルの現在のオフセットを取得する
file
構造体には、現在のオフセットをpos
変数によって管理している。
file_ppos
関数は、file
構造体のpos
変数を返す関数である。
このとき、stream-like
のファイルの場合は、オフセットを持たないのでNULL
を返す必要がある。
// 618: static inline loff_t *file_ppos(struct file *file) { return file->f_mode & FMODE_STREAM ? NULL : &file->f_pos; }
VFSのwrite処理を呼び出す
ここまでで、file
構造体とオフセットが取得できたのでvfs_write
関数でVFSのレイヤに移る(次回説明)。
vfs_write
関数による書き込みをした後は、オフセットの更新と後処理(参照カウンタやロックの解放)が必要になる。
// 78: static inline void fdput_pos(struct fd f) { if (f.flags & FDPUT_POS_UNLOCK) __f_unlock_pos(f.file); fdput(f); }
FDPUT_POS_UNLOCK
は、__fdget_pos
関数内でfile_count(file) > 1)
の場合に立てられるフラグである。
このとき、__f_unlock_pos
関数でファイルオフセット用のmutexを解放する。
// 1009: void __f_unlock_pos(struct file *f) { mutex_unlock(&f->f_pos_lock); }
その後、fdput
関数でファイルの参照カウンタを一つ減らす。
// 43: static inline void fdput(struct fd fd) { if (fd.flags & FDPUT_FPUT) fput(fd.file); }
実際には、fput
関数で参照カウンタを減らす操作をしているが、ここでは割愛する。
おわりに
本記事では、write
システムコールの実態からvfs_write
関数を呼び出すまでを解説した。
次回の記事では、vfs_write
関数から各ファイルシステムのwrite操作を呼び出すまでの処理を追いかける。
変更履歴
- 2020/11/29: 記事公開
- 2022/08/18: カーネルバージョンを5.15に変更
参考
- http://delihiros.jp/#!io.md
- Linux システムコール 徹底入門
- 本章では説明していない、
syscall
命令の割り込みハンドラの解説が充実している。
- 本章では説明していない、
- https://www.kernel.org/doc/Documentation/filesystems/files.txt
- カーネルドキュメントで、file構造体の取り扱いに書いてあるので一読することをオススメ。
- openフラグのO_PATH - Linuxの備忘録とか・・・(目次へ)
O_PATH
フラグについて、openの内部でどのように扱っているか解説されている。
- fget関数とfget_light関数 - Linuxの備忘録とか・・・(目次へ)
file
構造体を取得するfget
関数の処理フローが解説されている。
- パイプの仕組みを図で説明してみる - sgyatto's blog
fork
やpipe
によるfiles_struct
構造体やそれに関連する構造体がどう変わるのかを解説されている。