Linux - copy_mm()あたりを眺めてみる

![Tux_Enhanced.svg.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/258841/9e74dcfb-0943-567a-6b17-eb75e95c5a0f.png)

## 概要

マルチプロセス/スレッドあたりの違いの雰囲気を掴むために読んでみたのでまとめ。
見るバージョンは5.4を読んでみる
[torvalds/linux](https://github.com/torvalds/linux/tree/v5.4)

## Linuxにおけるスレッドとプロセス

#### グリーンスレッド/ネイティブスレッド

* グリーンスレッド

OSではなく仮想マシンなどよってスケジュールされるスレッド。
OSの機能によらずにマルチスレッド環境をエミュレートする。

* ネイティブスレッド(軽量プロセス,LWP)

スレッドの実行スケジュールはOSのスケジューラが行う
(psコマンドでは-Lを付けて表示可能)

#### (余談)スレッドモデルの話

M:NスレッドとはカーネルスレッドがMに対しユーザースレッドがNとなるそれぞれが非対称なスレッドモデル
複数のユーザスレッドに対して、複数のカーネルスレッドをマップして動作する
golangではこちらを採用していてスイッチコストがとても低いらしい。

* [goroutineはなぜ軽量なのか](https://christina04.hatenablog.com/entry/why-goroutine-is-good)

#### カーネルソースを眺めてみた

基本的にはfork(2)でもpthred_create()でも内部的にはdo_fork()を呼び出している。
cloneは呼び出された後はdo_forkを呼び出す。
プロセスとスレッドの生成の違いはこのdo_fork()を呼び出す際のフラグに差異があって区別されている模様。

``` c
long sys_clone(unsigned long clone_flags, unsigned long newsp,
	       void __user *parent_tid, void __user *child_tid)
{
	long ret;

	if (!newsp)
		newsp = UPT_SP(&current->thread.regs.regs);
	current->thread.forking = 1;
	ret = do_fork(clone_flags, newsp, &current->thread.regs, 0, parent_tid,
		      child_tid);
	current->thread.forking = 0;
	return ret;
}
```

処理の流れの概要

① [do_fork()]()
② [_do_fork()](https://github.com/torvalds/linux/blob/v5.4/kernel/fork.c#L2339-L2403)
③ [copy_process()](https://github.com/torvalds/linux/blob/v5.4/kernel/fork.c#L1764-L2298)
④ [copy_mm()](https://github.com/torvalds/linux/blob/v5.4/kernel/fork.c#L1383-L1428)
⑤ [dup_mmap()] (https://github.com/torvalds/linux/blob/v5.4/kernel/fork.c#L477-L627) (LWPではない場合は以下でcowの処理をする)
⑥ [copy_page_range()](https://github.com/torvalds/linux/blob/7e0165b2f1a912a06e381e91f0f4e495f4ac3736/mm/memory.c#L955-L1020)
⑦ [copy_one_pte()](https://github.com/torvalds/linux/blob/v5.4/mm/memory.c#L677-L782) vmエリアを1つのタスクから別のタスクにコピー

``` c
static int copy_mm(unsigned long clone_flags, struct task_struct *tsk)
{
    struct mm_struct *mm, *oldmm;
    int retval;

    tsk->min_flt = tsk->maj_flt = 0;
    tsk->nvcsw = tsk->nivcsw = 0;
#ifdef CONFIG_DETECT_HUNG_TASK
    tsk->last_switch_count = tsk->nvcsw + tsk->nivcsw;
    tsk->last_switch_time = 0;
#endif

    tsk->mm = NULL;
    tsk->active_mm = NULL;

    oldmm = current->mm;
    if (!oldmm)
        return 0;

    // vmacacheエントリの初期化
    vmacache_flush(tsk);

    // CLONE_VM が設定された場合、呼び出し元のプロセスと子プロセスは
    // 同じメモリー空間で 実行される。
    if (clone_flags & CLONE_VM) {
        mmget(oldmm);
        // 親プロセスのmmを子にセットしgood_mmへ
        mm = oldmm;
        goto good_mm;
    }

    retval = -ENOMEM;
    // CLONE_VMがセットされていない場合は既存のmm構造体を複製する
    mm = dup_mm(tsk, current->mm);
    if (!mm)
        goto fail_nomem;

good_mm:
    tsk->mm = mm;
    tsk->active_mm = mm;
    return 0;

fail_nomem:
    return retval;
}
```

clone(2)の先で呼ばれるcopy_mmの処理。
CLONE_VMが設定されている(スレッド生成時のフラグ)場合は親プロセスとアドレスを共有していることがわかった。
https://linuxjm.osdn.jp/html/LDP_man-pages/man2/clone.2.html

``` c
static struct mm_struct *dup_mm(struct task_struct *tsk,
                struct mm_struct *oldmm)
{
    struct mm_struct *mm;
    int err;

    mm = allocate_mm();
    if (!mm)
        goto fail_nomem;

    // 親プロセス のメモリを生成されるプロセスへコピー
    memcpy(mm, oldmm, sizeof(*mm));

    if (!mm_init(mm, tsk, mm->user_ns))
        goto fail_nomem;

    err = dup_mmap(mm, oldmm);
    if (err)
        goto free_pt;

    mm->hiwater_rss = get_mm_rss(mm);
    mm->hiwater_vm = mm->total_vm;

    if (mm->binfmt && !try_module_get(mm->binfmt->module))
        goto free_pt;

    return mm;

free_pt:
    /* don't put binfmt in mmput, we haven't got module yet */
    mm->binfmt = NULL;
    mm_init_owner(mm, NULL);
    mmput(mm);

fail_nomem:
    return NULL;
}
```

copy_mmのメインの処理。
前段の処理の時点でCLONE_VM(スレッド)が設定されている場合はここの処理は行われない。
スレッドはスレッド作成元のデータにアクセスが可能。プロセスの場合は親プロセスのメモリをコピーしていく処理を行なっていく。(後述予定。。)

## 補足

mm_struct - アドレス空間の情報を管理する構造体

|項目|概要|
|----|-----------------------------|
|mmap|vm_area_struct の先頭を保持する|
|mm_rb|メモリエリアを高速に探すための red-black tree を保持する|
|mmap_cache|メモリ参照の局所生(locality)を生かして高速化するためのもの|
|mm_count|この構造体の参照カウンタ。0ならどのオブジェクトがも指されていない|
|mm_list|mm_struct 構造体のリストを作るためのフィールド|
|start_code, end_code|テキストセグメントの開始番地と終了番地|
|start_brk, brk|ヒープの開始番地と終了番地|
|start_stack|スタックの開始番地|

## 参考

* [Linuxカーネルを読んで改めて知るプロセスとスレッドの違い](https://www.slideshare.net/Retrieva_jp/linux-107399833)
* [マルチスレッドのコンテキスト切り替えに伴うコスト](https://naoya-2.hatenadiary.org/entry/20071010/1192040413)