Linux: mmap(2) したメモリに書き込みした際の Copy On Wirte を観察する

イントロ

ペパボ社内 Slack で Linux の CoW = Copy On Write について、 id:ryuichi1208 id:udzura とディスカッションして盛り上がっていた。カーネル内で CoW を処理する関数を追えないか? という話があがったので、調べてみた次第。

( なぜ CoW の話が出てきたのか / どんなことをディスカッションしてたのかは id:ryuichi1208 がまとめくれるかも? )

結論

CoW を観察するには do_wp_pageを観察するといいみたい

wiki.bit-hive.com

( いつもお世話になっております )

do_wp_page のソース

https://elixir.bootlin.com/linux/v5.11.22/source/mm/memory.c#L3085

検証環境

Vagrant で用意した

$script = <<-SCRIPT
sudo apt-get update && apt-get install -y build-essential bpftrace bpfcc-tools linux-headers-$(uname -r)
SCRIPT

Vagrant.configure("2") do |config|
  config.vm.box = "bento/ubuntu-21.04"
  config.vm.provision "shell", inline: $script
end

検証コード

以下の検証コードを用意した

  1. 親プロセスで mmap(2) して minor page fault を起こしておく
  2. 親プロセスが fork(2) する
  3. 子プロセスが 1 のメモリに書き込みして minor page fault を起こす
  4. カーネル内で do_wp_page で CoW が実行される

を期待する動作とする。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/wait.h>

int main(int argc, char *argv[])
{
    /* CoW を起こしたいページ数 */
    int pages = 100;

    /* getconf PAGESIZE */
    size_t page_size = 4096;

    /* 観察しやすいようにアドレスを固定する */
    char *p1 = (char*)mmap((void *)0x100000000000, page_size * pages,
                   PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, 0, 0);

    if (p1 == MAP_FAILED) {
        perror("failed to mmap");
        exit(1);
    }

    printf("mmap: %p\n", p1);

    /* 親プロセス: minor page fault */
    for (int i = 0; i < pages; i++) {
        p1[i * page_size] = 'p';
    }

    pid_t pid = fork();
    if (pid == -1) {
        perror("failed to fork");
        exit(1);
    } else if (pid == 0) {
        printf("child pid:%d\n", getpid());

        /* 子プロセス: minor page fault -> cow -> do_wp_page */
        for (int i = 0; i < pages; i++) {
            printf("cow address: %p\n", &p1[i * page_size]);
            p1[i * page_size] = 'c';
            sleep(1);
        }
    } else {
        printf("parent pid:%d\n", getpid());
        waitpid(pid, NULL, 0);
    }
}

観察

bpftrace で do_wp_page をトレースする

kprobe:do_wp_page /comm == "cow"/ {
  printf("do_wp_page > pid:%d comm:%s address:%p\n", pid, comm, ((struct vm_fault *)arg0)->address)
}
  • その他プロセスの CoW が邪魔しないよう comm でフィルタする
  • struct vm_fault の address で fault を起こした仮想アドレスが取り出せる
    • printf して検証コードと付き合わせて確認する

単に do_wp_page が呼び出されているかどうかだけ確かめるなら perf-tools なんかでも OK

github.com

実験

f:id:hiboma:20210901122423p:plain

こんな感じで観察できました。とりあえず このエントリはここまで

perf-tools の場合

perf-tools の functrace だと ↓ みたいにトレースできる

$ sudo ./bin/functrace do_wp_page


             cow-25935   [000] .... 24930.105521: do_wp_page <-handle_pte_fault
             cow-25935   [000] .... 24930.105522: do_wp_page <-handle_pte_fault
             cow-25935   [000] .... 24930.105534: do_wp_page <-handle_pte_fault
             cow-25935   [000] .... 24931.122937: do_wp_page <-handle_pte_fault
             cow-25935   [000] .... 24932.158144: do_wp_page <-handle_pte_fault
             cow-25935   [000] .... 24933.165625: do_wp_page <-handle_pte_fault
             cow-25935   [000] .... 24934.167118: do_wp_page <-handle_pte_fault
             cow-25935   [000] .... 24935.168714: do_wp_page <-handle_pte_fault
             cow-25935   [000] .... 24936.177183: do_wp_page <-handle_pte_fault

その他

最初は do_cow_fault() を調べていたのだが、こちらは anonymous ページでなく file backed な ページの CoW を処理する関数ぽい? (まだ調べてないので宿題)

memory.c - mm/memory.c - Linux source code (v5.11.22) - Bootlin

名前にまんま cow が含まれているので、「Copy on Write 処理するのは これなのだろう」と思い込んでしまった。検証コードが期待したように動作せずハマってしまった。

参考

cstmize.hatenablog.jp

linuxjm.osdn.jp

qiita.com

qiita.com

lore.kernel.org