LeavaTailの日記

LeavaTailの日記

Linuxエンジニアを目指した技術者の備忘録

Linuxカーネルのファイルアクセスの処理を追いかける (2) System call

はじめに

ユーザプロセスからファイルシステムという機構によって記憶装置上のデータをファイルという形式で書き込み・読み込みすることができる。
本調査では、ユーザプロセスがファイルに書き込み要求を実行したときにLinuxカーネルではどのような処理が実行されるかを読み解いていく。

調査対象や環境などはPart 1: 環境セットアップを参照。

f:id:LeavaTail:20201128233057p:plain
調査対象

本記事では、Syscall interfaceの途中 (SYSCALL_DEFINE)からVFSの関数を呼び出すところまでとする。
ここより上のレイヤー(Application部やsystem call interfaceの上位部)の解説は今回は省略する。

変更履歴

  • 2020/11/29: 記事公開

Application

下記の記事では、ユーザプロセスがfwrite()関数を実行したときのフローを詳細にまとめてある。
特に、glibcまで追いかけているため、一から処理を追いかけたい人におすすめ。

delihiros.jp

下記の記事では、システムコールについて説明されている。 システムコールを一から学習したい人におすすめ。

www.kimullaa.com

System call Interface

writeシステムコールは、SYSCALL_DEFINEマクロによって定義される。

// 667:
SYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf,
        size_t, count)
{
    return ksys_write(fd, buf, count);
}

writeシステムコールの実態は、ksys_write()となっている。
ksys_write()の定義は下記のようになっている。

// 647:
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つ処理がある。

  1. 現在のプロセスが保有しているファイルディスクリプタテーブルからfd構造体を取得する
  2. ファイルの現在のオフセットを取得する
  3. VFSのwrite処理を呼び出す

ファイルディスクリプタからfd構造体を取得する

ファイル構造体を取得するまでの流れは下記のようなものとなっており、マルチスレッドであった場合に限りファイルディスクリプタテーブルにロックをかけるといった処理がされる。
ちなみに、FMODE_PATHO_PATHフラグを指定してファイルをopenしたときに付与されるフラグであり、その場合は書き込みを失敗させる。

f:id:LeavaTail:20201127235227p:plain
ファイル構造体を取得するまでのフロー

file構造体の取り扱い方については、カーネルドキュメントが用意されているので気になる人はそちらを読むことを推奨する。

脱線してしまったが、ksys_write関数で実行されるfdget_pos関数について定義を確認する。 fdget_pos関数は下記のような定義である。

// 73:
static inline struct fd fdget_pos(int fd)
{
    return __to_fd(__fdget_pos(fd));
}
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関数の流れのイメージを下記に示す。

f:id:LeavaTail:20201128190451p:plain
file構造体のアドレスとfd構造体の対応

  1. __fdget_pos関数(後述)によって、ファイルディスクリプタからファイル構造体のアドレスを取得する
  2. __fdget_pos関数は、取得したアドレスをunsigned long型の変数vに代入し、下位2bitにフラグを代入する
  3. __to_fd関数は、変数vからfile構造体とflagsを取得して、fd構造体を返す
ファイルディスクリプタからfile構造体を取得する

fdget_pos関数は、 ファイルディスクリプタからfile構造体を取得するために__fdget_pos関数を呼び出す。

// 924:
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関数の定義を確認する。

// 913:
unsigned long __fdget(unsigned int fd)
{
    return __fget_light(fd, FMODE_PATH);
}
// 896:
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 = __fcheck_files(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構造体の配列を管理する。

これらの関係を図示したものが以下である。

f:id:LeavaTail:20201126232954p:plain
ファイルディスクリプタからfile構造体を取得するまでの各構造体の関係性

__fget_light関数でも上記の規則と同様に、current変数からfiles_struct構造体を取得している。
しかし、シングルスレッドかマルチスレッドの場合で、files_struct以降の取り扱いが変わる。

シングルスレッドの場合

シングルスレッドの場合 (つまり、atomic_read(&files->count) == 1) は、対象のファイルディスクリプタテーブルを保有しているは自分のみである。 そのため、それぞれの変数に対して、排他制御を意識する必要はない。

__fcheck_files関数の定義は以下のようになっている。

// 83:
static inline struct file *__fcheck_files(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;
}

ちなみに、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.

file構造体を取得した後に、そのファイルのopenフラグを確認する。 FMODE_PATHフラグが立っていた場合、ファイル操作は無効となっているので0を返す必要がある。

マルチスレッドの場合

マルチスレッドの場合 (つまり、atomic_read(&files->count) > 1) は、排他制御を意識しなけらばならない。

__fget関数と呼び出し先の__fget_files関数の定義は以下のようになっている。

// 845:
static inline struct file *__fget(unsigned int fd, fmode_t mask,
                  unsigned int refs)
{
    return __fget_files(current->files, fd, mask, refs);
}
// 822:
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 = fcheck_files(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;
}

fcheck_files関数は、__fcheck_files関数(シングルスレッドの場合を参照)のラッパー関数である。

シングルスレッドの場合と大きく異なるのは、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を解放する。

// 938:
void __f_unlock_pos(struct file *f)
{
    mutex_unlock(&f->f_pos_lock);
}

その後、fput関数でファイルの参照カウンタを一つ減らす。

// 43:
void fput(struct file *file)
{
    fput_many(file, 1);
}

実際には、fput_many関数で参照カウンタを減らす操作をしているが、ここでは割愛する。

まとめ

本記事では、writeシステムコールの実態からvfs_write関数を呼び出すまでを解説した。 次回の記事では、vfs_write関数から各ファイルシステムのwrite操作を呼び出すまでの処理を追いかける。

参考