レガシーガジェット研究所

気になったことのあれこれ。

Linux Kernel ~ プロセスアドレス空間 ~

概要

「詳解Linux Kernel」を参考にVersion 2.6.11のコードリーディングをしていく。CPUのアーキテクチャは書籍に沿ってIntelx86とする。

今回はプロセスのアドレス空間について見ていく。

メモリ割り当ての指針

カーネルからメモリ要求は最優先とし即座にメモリ割り当てを行い、且つ当該処理にバグが含まれないことを前提にエラーへの対策は必要以上には行わない。

一方でユーザプロセスからのメモリ要求は緊急でないとし、リニアアドレスに対するアクセス権のみを付与し割り当ては遅延させる。実際にアクセスがあるまでページフレームの割り当てを行わず、アドレッシングエラーなども必ず補足する。

プロセスのアドレス空間

プロセスのアドレス空間はリニアアドレスを用いたアドレス空間として独立しており、カーネルはそのアドレス空間に対してリージョンの追加・削除を動的に行う。メモリリージョンは先頭リニアアドレス、サイズ、アクセスを保持する。サイズは4KBである。

以下のような場合にプロセスは新たなにメモリリージョン(アドレス空間)を得る。

  • コンソールにコマンドを入力するとシェルがプロセスを生成する過程で実行のためのアドレス空間を割り当てる
  • 実行中のプロセスが新たなプログラムを実行する際に、既存のメモリリージョンを開放し、新たなメモリリージョンがプロセスに割り当てられる。
  • プロセスのユーザモードスタックにデータが増え続け、拡張が必要になった際に新たなメモリリージョンを割り当てる。
  • 他のプロセスとデータを共有するためにIPC共有メモリリージョンを作成することがある。
  • プロセスがmalloc()を使用して動的メモリ領域(ヒープ)を拡張する。

メモリリージョンの作成と削除には以下のようなシステムコールが挙げられる

システムコール 説明
brk プロセスのヒープサイズを変更する
execve 新たな実行ファイルをロードしアドレス空間を変更する
_exit カレントプロセスを終了しアドレス空間を解放する
fork 新たなプロセスに新たなアドレス空間を割り当てる
mmap, mmap2 ファイルをメモリにマッピjングし、アドレス空間を拡張する
mremap リージョンの拡張または縮小を行う
remap_file_pages ファイルの非線形的なメモリマッピングを行う
munmap ファイルのメモリマッピングを開放しアドレス空間を縮小する
shmat 共有メモリリージョンを取り付ける
shmdt 共有メモリリージョンを取り外す

カーネルはプロセスのアドレス空間を常に認識する必要があり、以下の無効なリニアアドレスへのアクセスは例外ハンドラによって補足する必要がある。

  • プログラミングエラーにより発生する場合
  • リニアドレスはアドレス空間内のものだがページフレームが割り当てられていない場合

ページフレームが割り当てられていない場合は異常ではなく、ページフォルトの例外によって実際にページフレームが割り当てられた後処理を再開する。

メモリディスクリプタ

アドレス空間の情報はメモリディスクリプタ(mm_struct)により管理され、プロセスディスクリプタ(task_struct)のメンバであるmmから参照される。

// include/linux/sched.h
truct mm_struct {
    struct vm_area_struct * mmap;      /* メモリリージョンオブジェクトリストのヘッド */
    struct rb_root mm_rb; /* メモリリージョンオブジェクトの赤黒木のルート要素を指す */
    struct vm_area_struct * mmap_cache;    /* 最後に利用したメモリリージョンオブジェクト */

    // プロセス空間から利用可能なリニアドレス区間を探す
    unsigned long (*get_unmapped_area) (struct file *filp,
                unsigned long addr, unsigned long len,
                unsigned long pgoff, unsigned long flags);
    
    // リニアアドレス区間の開放時に呼び出す
    void (*unmap_area) (struct vm_area_struct *area);


    unsigned long mmap_base;      /* 無名メモリリージョンまたはファイルがマッピングされたメモリリージョンのリニアアドレスの先頭 */
    unsigned long free_area_cache;        /* 空いているリニアアドレスを探し始めるアドレス */
    pgd_t * pgd; // ページグローバルデェレクトリのアドレス
    atomic_t mm_users;          /* 使用しているユーザ数 */
    atomic_t mm_count;          /* この構造体の参照数 */
    int map_count;             /* メモリリージョン数 */
    struct rw_semaphore mmap_sem; // メモリリージョンの読み書き用セマフォ
    spinlock_t page_table_lock;     /* メモリリージョン及びページテーブル用のスピンロック, mm->rss, mm->anon_rss */

    struct list_head mmlist; // メモリディスクリプタのリスト(次の要素を指す)
    /* List of maybe swapped mm's.  These are globally strung
   * together off init_mm.mmlist, and are protected
   * by mmlist_lock
   */

    /**
    * start_code: テキストセグメントの先頭アドレス
    * end_code: テキストセグメントの終端アドレス
    * start_data: データセグメントの先頭アドレス
    * end_data: データセグメントの終端アドレス
    */
    unsigned long start_code, end_code, start_data, end_data;

    /**
    * start_brk: ヒープの先頭アドレス
    * brk: ヒープの現時点での終端アドレス
    * start_stack: ユーザスタックの先頭アドレス
    */
    unsigned long start_brk, brk, start_stack;

    /**
    * arg_start: コマンドライン引数の先頭アドレス
    * arg_end: コマンドライン引数の終端アドレス
    * env_start: 環境変数の先頭アドレス
    * env_end: 環境変数の終端アドレス
    */
    unsigned long arg_start, arg_end, env_start, env_end;

    /**
    * rss: プロセスに割り当てられているページフレームの数
    * anon_rss: 無名メモリマッピングに割り当てたページフレーム
    * total_vm: アドレス空間全体のサイズ(ページサイズ単位)
    * locked_vm: スワップアウトされないページ数
    * shared_vm: 共有ファイルメモリマッピング領域のページ数
    */
    unsigned long rss, anon_rss, total_vm, locked_vm, shared_vm;

    /**
    * exec_vm: 実行可能メモリまピング領域のページ数
    * stack_vm: ユーザモードスタック領域のページ数
    * reserved_vm: 予約されたメモリリージョン及び特殊メモリリージョンの領域のページ数
    * def_flags: メモリリージョンの標準初期設定のアクセスフラグ
    * nr_ptes: PTEの数
    */
    unsigned long exec_vm, stack_vm, reserved_vm, def_flags, nr_ptes;

    unsigned long saved_auxv[42]; /* /proc/PID/auxv */

    unsigned dumpable:1; // コアダンプの取得が可能かどうか
    cpumask_t cpu_vm_mask; // 遅延TLB切り替えのためのビットマスク

    /* アーキテクチャ依存のコンテキスト情報 */
    mm_context_t context;

    /* Token based thrashing protection. */
    unsigned long swap_token_time; // スワップの優先権の資格を得る時刻(?)
    char recent_pagein; // 直近でページフォルトが発生したことを意味する

    /* coredumping support */
    int core_waiters; // アドレス空間のコンテキストをコアファイルにダンプしている軽量プロセスの数

    /**
    * core_startup_done: コアダンプ生成時に利用する完了通知用データ構造へのポインタ
    * core_done: コアファイル生成時に利用する完了通用データ構造体
    */
    struct completion *core_startup_done, core_done;

    /* aio bits */
    rwlock_t        ioctx_list_lock; // 非同期I/O(AIO)のコンテキストを保護するためのスピンロック
    struct kioctx      *ioctx_list; // AIOコンテキストのリスト

    struct kioctx      default_kioctx; // デフォルトのAIOコンテキスト

    unsigned long hiwater_rss;    /* 当該プロセスがある時点で使用していたページフレームの最大数 */
    unsigned long hiwater_vm; /* 当該プロセスのメモリリージョンがある時点で使用していたページフレームの数 */
};

メモリディスクリプタは双方向リストになっておりmmlistメンバで相互に接続されている。リストの先頭要素はinit_mm変数のmmlistメンバでシステムの起動時にプロセス0のメモリディスクリプタとして利用する。

// include/linux/sched.h
struct mm_struct init_mm = INIT_MM(init_mm);

メモリディスクリプタの獲得

新たなメモリディスクリプタの獲得にはmm_alloc()関数を使用する。

// kernel/fork.c
/*
 * Allocate and initialize an mm_struct.
 */
struct mm_struct * mm_alloc(void)
{
    struct mm_struct * mm;

    mm = allocate_mm();
    if (mm) {
        memset(mm, 0, sizeof(*mm));
        mm = mm_init(mm);
    }
    return mm;
}

#define allocate_mm()  (kmem_cache_alloc(mm_cachep, SLAB_KERNEL))

上記を見ると内部でkmem_cache_alloc()を使用しており、スラブアロケータからメモリディスクリプタ用のオブジェクトを取得し、mmset()関数でゼロクリアしている。

メモリディスクリプタの参照の解放

mmput()関数は対象のメモリディスクリプタの参照カウンタ(mm_users)をデクリメントすし、結果が0になる場合にはメモリディスクリプタのリスト及び当該メモリディスクリプタを破棄する。

// kernel/fork.c
void mmput(struct mm_struct *mm)
{
    // デクリメントした結果が0になるかどうか
    if (atomic_dec_and_test(&mm->mm_users)) {
        exit_aio(mm);
        exit_mmap(mm);

        // メモリディスクリプタリストが空でない場合には削除
        if (!list_empty(&mm->mmlist)) {
            spin_lock(&mmlist_lock);
            list_del(&mm->mmlist);
            spin_unlock(&mmlist_lock);
        }
        put_swap_token(mm);
        mmdrop(mm); // メモリディスクリプタを破棄
    }
}

カーネルスレッドのメモリディスクリプタ

カーネルスレッドは通常のプロセスと異なりTASK_SIZEよりも下位のリニアアドレスにアクセスすることはなく、メモリリージョンも利用しない。

// include/asm-i386/processor.h
#define TASK_SIZE  (PAGE_OFFSET)

// include/asm-i386/page.h
#define PAGE_OFFSET        ((unsigned long)__PAGE_OFFSET)
#define __PAGE_OFFSET      (0xC0000000)

TASK_SIZEよりも上位のアドレスは(全てのプロセスで)カーネル空間となっているため、当該空間を管理するページテーブルはどのプロセスのモノでも関係がない。不必要なTLBやキャッシュのフラッシュを回避するため、カーネルスレッドは最後に実行した通常プロセスのページテーブルを利用する。これを達成するためにプロセスディスクリプタではmmactive_mmの2つのメモリディスクリプタポインタを用意している。

// include/linux/sched.h
struct task_struct {
:
struct mm_struct *mm, *active_mm;
:

active_mmは現在使用しているメモリディスクリプタを、mmはプロセスが保持してるメモリディスクリプタを指すポインタで、通常のプロセスであればmm及びactive_mmは同一のメモリディスクリプタを指している。カーネルスレッドはメモリディスクリプタを持たないためmmにはNULLが設定され、active_mmには直前に実行していたプロセスのメモリディスクリプタが設定される。

カーネルプロセスがTASK_SIZEよりも高位なアドレスのページテーブルを更新する際には、全プロセスのページテーブルのエントリも更新する必要があるが、一度に全てを書き換えるは現実的ではない・そのため書き換えの際にswapper_pg_dirが保持している基準ページテーブルを更新し、実際にアクセスする際にページフォルトを発生させることで遅延処理として基準ページテーブルの情報を反映する。

メモリリージョン

メモリリージョンはvm_area_struct構造体で実装されている。

struct vm_area_struct {
    struct mm_struct * vm_mm;  /* メモリリージョンを持つメモリディスクリプタを指すポインタ */
    unsigned long vm_start;       /* メモリリージョンの先頭リニアアドレス */
    unsigned long vm_end;     /* メモリリージョンの終端アドレスの次のアドレス */

    /* アドレスでソートされたプロセスリスト上の次のメモリリージョン */
    struct vm_area_struct *vm_next;

    pgprot_t vm_page_prot;      /* メモリリージョンのページフレームへのアクセス権 */
    unsigned long vm_flags;       /* メモリリージョンのフラグ */

    struct rb_node vm_rb; // 赤黒木用のデータ

    union {
        struct {
            struct list_head list; 
            void *parent;  /* aligns with prio_tree_node parent */
            struct vm_area_struct *head;
        } vm_set;

        struct raw_prio_tree_node prio_tree_node;
    } shared; // 逆マッピング用のデータ構造と対応

    struct list_head anon_vma_node;    /* 無名メモリリージョンのリストを指す */
    struct anon_vma *anon_vma; /* anon_vmaを指すポインタ */

    /* Function pointers to deal with this struct. */
    struct vm_operations_struct * vm_ops; // メモリリージョン操作用関数へのポインタ

    /* Information about our backing store: */
    unsigned long vm_pgoff;       /* マッピングしているファイル内オフセット */
    struct file * vm_file;     /* マッピングしているファイルのファイルオブジェクトのポインタ */
    void * vm_private_data;        /* メモリリージョン固有のデータ */
    unsigned long vm_truncate_count;/* ファイルの非線形マッピング用リニアアドレス空間を開放時に使用 */

#ifndef CONFIG_MMU
    atomic_t vm_usage;      /* refcount (VMAs shared if !MMU) */
#endif
#ifdef CONFIG_NUMA
    struct mempolicy *vm_policy;   /* NUMA policy for the VMA */
#endif
};

メモリリージョンディスクリプタはリニアアドレスの区画を表している。vm_startは開始アドレス、vm_endメンバは終端アドレスを指しており、vm_end - vm_startでこのメモリリージョンのサイズが算出できる。

カーネルは新たなメモリリージョンを割り当てる際に、隣のメモリリージョンを結合しようとする(アクセス権が異なる場合は結合できない)

詳解Linuxカーネル

vm_opsvm_operations_struct構造体のポインタとして定義されメモリリージョンの操作を保持する。当該構造体は以下のように定義されている。

// include/linux/mm.h
struct vm_operations_struct {
    /**
    * メモリリージョンを追加する際に呼び出す
    */
    void (*open)(struct vm_area_struct * area);

    /**
    * メモリリージョンを削除する際に呼び出す
    */
    void (*close)(struct vm_area_struct * area);

    /**
    * メモリリージョン内のリニアアドレスにアクセスする際に発生したページフォルトハンドラから呼び出される
    */
    struct page * (*nopage)(struct vm_area_struct * area, unsigned long address, int *type);

    /**
    * リニアアドレスに対応するページテーブルエントリを設定する(主にファイルの非線形なメモリマッピング)
    */
    int (*populate)(struct vm_area_struct * area, unsigned long address, unsigned long len, pgprot_t prot, unsigned long pgoff, int nonblock);
#ifdef CONFIG_NUMA
    int (*set_policy)(struct vm_area_struct *vma, struct mempolicy *new);
    struct mempolicy *(*get_policy)(struct vm_area_struct *vma,
                    unsigned long addr);
#endif
};

プロセスが保持するメモリリージョンはリストで管理されており、アドレスの昇順でソートされている。メモリディスクリプタ(mm_struct)のmmapメンバがリストの先頭を保持していて、メモリリージョンディスクリプタ(vm_are_struct)のメンバであるvm_nextがリスト内の次のメモリリージョンを指すことでリストを形成する。

詳解Linuxカーネル

メモリディスクリプタmap_countメンバは保持ているメモリリージョンの総数を保持しており、当該値は/proc/sys/vm/max_map_countで変更することも可能。

$ cat /proc/sys/vm/max_map_count
65530

特定のリニアアドレスを含むメモリリージョンの探索は簡単で、リストはアドレスの昇順に並んでいるため指定したアドレスよりも大きな終端アドレスを持つリージョンを見つけるだけとなる。しかし単方向リストであるため検索/挿入/削除の処理コストはリストのサイズに比例して大きくなってしまい、データベースやデバッガのような数百~数千のメモリリージョンを用いるようなアプリケーションでは非常に効率が悪い。

Linux 2.6では赤黒木のデータ構造を利用しメモリリージョンを管理する。参照や更新などの際は赤黒木からメモリリージョンの位置を特定する。メモリディスクリプタ(mm_struct)のmm_rbが赤黒木のルートノードを保持しており、各メモリリージョンはvm_rbでノードの色や親、個へのポインタなどを管理する。

アクセス権

メモリリージョンのアクセス権は以下のように定義されている。

// include/linux/mm.h
/*
 * vm_flags..
 */
#define VM_READ        0x00000001 // 読み書き可能
#define VM_WRITE   0x00000002 // 書き込み可能
#define VM_EXEC        0x00000004 // 実行可能
#define VM_SHARED  0x00000008 // 複数プロセスで共有可能

#define VM_MAYREAD 0x00000010 // VM_READフラグを設定可能
#define VM_MAYWRITE    0x00000020 // VM_WRITEフラグを設定可能
#define VM_MAYEXEC 0x00000040 // VM_EXECフラグを設定可能
#define VM_MAYSHARE    0x00000080 // VM_SHAREフラグを設定可能

#define VM_GROWSDOWN   0x00000100 // 低位アドレス方向に拡張可能
#define VM_GROWSUP 0x00000200 // 高位アドレス方向に拡張可能
#define VM_SHM     0x00000400 // IPC共有メモリ(スワップ不可)
#define VM_DENYWRITE   0x00000800 // 書き込み不可ファイル

#define VM_EXECUTABLE  0x00001000 // 実行可能ファイル
#define VM_LOCKED  0x00002000 // ロック状態(スワップ不可)
#define VM_IO           0x00004000    /* Memory Mapped I/O */

/* sys_madvise()によって使用される */
#define VM_SEQ_READ    0x00008000 /* シーケンシャルにアクセス */
#define VM_RAND_READ   0x00010000 /* ランダムアクセス */

#define VM_DONTCOPY    0x00020000 // fork時にリージョンを複製しない
#define VM_DONTEXPAND  0x00040000 // 拡張不可
#define VM_RESERVED    0x00080000 // スワップアウト禁止
#define VM_ACCOUNT 0x00100000 // Is a VM accounted object (?)
#define VM_HUGETLB 0x00400000 // Huge TLB */
#define VM_NONLINEAR   0x00800000 // 非線形マッピング

メモリリージョンに対応するページテーブルエントリに同じ値(読み込み可能/書き込み可能/実行可能)を設定することでページング回路が直接アクセス権を確認することが可能となる。

ページを追加する際にはメモリリージョンディスクリプタ(vm_area_struct)のvm_page_protメンバの値をページテーブルエントリのフラグに設定する。

メモリリージョンに対する処理

リニアアドレス空間の割り当て

当該処理にはdo_mmap()関数を使用する。以下のように定義されている。

// include/linux/mm.h

/**
 * file: マッピング対象のファイルオブジェクト
 * addr: 空き区画検索の開始リニアアドレス
 * len: リニアアドレスの区画の大きさ
 * prot: ページへのアクセス権
 * flag: フラグ
 * offset: ファイルオブジェクトのためのオフセット
 */
static inline unsigned long do_mmap(struct file *file, unsigned long addr,
    unsigned long len, unsigned long prot,
    unsigned long flag, unsigned long offset)
{
    unsigned long ret = -EINVAL;
    // バリデーション
    if ((offset + PAGE_ALIGN(len)) < offset)
        goto out;
    // offsetがページサイズ内
    if (!(offset & ~PAGE_MASK))
        ret = do_mmap_pgoff(file, addr, len, prot, flag, offset >> PAGE_SHIFT);
out:
    return ret;
}

file及びoffsetはファイルマッピング時に使用される、ここでは両者ともNULLであることを想定する。

内部で呼び出されているdo_mmap_pgoff()関数は以下のように定義されている。

// mm/mmap.c
/*
 * The caller must hold down_write(current->mm->mmap_sem).
 */
/**
 * file: マッピング対象のファイルオブジェクト
 * addr: 空き区画検索の開始リニアアドレス
 * len: リニアアドレスの区画の大きさ
 * prot: ページへのアクセス権
 * flags: フラグ
 * pgoff: ファイルオブジェクトのためのオフセット
 */
unsigned long do_mmap_pgoff(struct file * file, unsigned long addr,
            unsigned long len, unsigned long prot,
            unsigned long flags, unsigned long pgoff)
{
    struct mm_struct * mm = current->mm;
    struct vm_area_struct * vma, * prev;
    struct inode *inode;
    unsigned int vm_flags;
    int correct_wcount = 0;
    int error;
    struct rb_node ** rb_link, * rb_parent;
    int accountable = 1;
    unsigned long charged = 0;

    // ファイルマッピング
    if (file) {
        if (is_file_hugepages(file))
            accountable = 0;

        if (!file->f_op || !file->f_op->mmap)
            return -ENODEV;

        if ((prot & PROT_EXEC) &&
            (file->f_vfsmnt->mnt_flags & MNT_NOEXEC))
            return -EPERM;
    }
    /*
    * アプリケーションはPROT_READが暗黙的にPROT_EXECを意図することを想定している?
    */
    if ((prot & PROT_READ) && (current->personality & READ_IMPLIES_EXEC))
        if (!(file && (file->f_vfsmnt->mnt_flags & MNT_NOEXEC)))
            prot |= PROT_EXEC;

    if (!len)
        return addr;

    /* カーネル空間に入ってしまう場合はエラー */
    len = PAGE_ALIGN(len);
    if (!len || len > TASK_SIZE)
        return -EINVAL; // 無効な引数

    /* オフセットがオーバフローしていないか */
    if ((pgoff + (len >> PAGE_SHIFT)) < pgoff)
        return -EINVAL; // 無効な引数

    /* マッピング済みのリージョンが多すぎないか */
    /**
    * #define DEFAULT_MAX_MAP_COUNT    65536
    * int sysctl_max_map_count = DEFAULT_MAX_MAP_COUNT;
    */
    if (mm->map_count > sysctl_max_map_count)
        return -ENOMEM;

    /**
    * マッピングを行うアドレスを取得し
    * 有効なアドレス区画を指しているか確認する
    */
    addr = get_unmapped_area(file, addr, len, pgoff, flags);
    if (addr & ~PAGE_MASK)
        return addr;

    // 新たなメモリリージョンに設定するフラグ値を算出
    vm_flags = calc_vm_prot_bits(prot) | calc_vm_flag_bits(flags) |
            mm->def_flags | VM_MAYREAD | VM_MAYWRITE | VM_MAYEXEC;

    // スワップが禁止されている場合
    if (flags & MAP_LOCKED) {
        if (!can_do_mlock())
            return -EPERM; // 許可されていない操作
        vm_flags |= VM_LOCKED; // メモリリージョンをロック
    }

    /* mlock MCL_FUTURE? */
    if (vm_flags & VM_LOCKED) { // スワップを禁止
        unsigned long locked, lock_limit;
        locked = mm->locked_vm << PAGE_SHIFT;
        lock_limit = current->signal->rlim[RLIMIT_MEMLOCK].rlim_cur; // アドレス空間内でロックできるページの最大数
        locked += len;
        // ロックされているページ数がリソース制限値を超える若しくは適当な権限が存在しない場合
        if (locked > lock_limit && !capable(CAP_IPC_LOCK))
            return -EAGAIN; // 再度試みる
    }

    // ファイルをマッピングするの場合
    inode = file ? file->f_dentry->d_inode : NULL;

    // ファイルをマッピングする場合
    if (file) {
        switch (flags & MAP_TYPE) {
        case MAP_SHARED:
            if ((prot&PROT_WRITE) && !(file->f_mode&FMODE_WRITE))
                return -EACCES;

            if (IS_APPEND(inode) && (file->f_mode & FMODE_WRITE))
                return -EACCES;
                
            if (locks_verify_locked(inode))
                return -EAGAIN;

            vm_flags |= VM_SHARED | VM_MAYSHARE;
            if (!(file->f_mode & FMODE_WRITE))
                vm_flags &= ~(VM_MAYWRITE | VM_SHARED);

        case MAP_PRIVATE:
            if (!(file->f_mode & FMODE_READ))
                return -EACCES;
            break;

        default:
            return -EINVAL;
        }
    } else {
        switch (flags & MAP_TYPE) {
        case MAP_SHARED: // 共有ページ
            vm_flags |= VM_SHARED | VM_MAYSHARE;
            break;
        case MAP_PRIVATE: // 共有不可ページ
            pgoff = addr >> PAGE_SHIFT;
            break;
        default:
            return -EINVAL;
        }
    }

    // ファイルマッピング
    error = security_file_mmap(file, prot, flags);
    if (error)
        return error;
        
    /* Clear old maps */
    error = -ENOMEM; // Out of memory
munmap_back:
    // リスト及び赤黒木から新たなメモリリージョンの位置を見つける
    vma = find_vma_prepare(mm, addr, &prev, &rb_link, &rb_parent);
    // リージョンの存在及び新たなリージョンの開始アドレスが新しいメモリ空間の終端よりも小さいかどうか
    if (vma && vma->vm_start < addr + len) {
        if (do_munmap(mm, addr, len))
            return -ENOMEM; // Out of memory
        goto munmap_back;
    }

    /* アドレス空間サイズが制限値を超過していないか */
    if ((mm->total_vm << PAGE_SHIFT) + len
        > current->signal->rlim[RLIMIT_AS].rlim_cur)
        return -ENOMEM;

    // 空きページフレームの確認が必要もしくはオーバーコミットを行わない設定になっている場合
    if (accountable && (!(flags & MAP_NORESERVE) ||
                sysctl_overcommit_memory == OVERCOMMIT_NEVER)) {
        if (vm_flags & VM_SHARED) { // 共有ページである場合
            /* Check memory availability in shmem_file_setup? */
            vm_flags |= VM_ACCOUNT;
        
        // 共有ページでなく書き込み可能な場合
        } else if (vm_flags & VM_WRITE) {
            /*
            * Private writable mapping: check memory availability
            */
            charged = len >> PAGE_SHIFT;
            // 十分な空きページフレームが存在しない場合
            if (security_vm_enough_memory(charged))
                return -ENOMEM; // Out of memory
            vm_flags |= VM_ACCOUNT;
        }
    }

    // ファイルマッピングでなく且つ共有不可である場合
    if (!file && !(vm_flags & VM_SHARED) &&
            // 前後のメモリリージョンを結合する(可能であれば)
        vma_merge(mm, prev, addr, addr + len, vm_flags,
                    NULL, NULL, pgoff, NULL))
        goto out;

    // 新たなメモリリージョン用のメモリリージョンディスクリプタを生成
    vma = kmem_cache_alloc(vm_area_cachep, SLAB_KERNEL);
    if (!vma) {
        error = -ENOMEM;
        goto unacct_error;
    }
    memset(vma, 0, sizeof(*vma)); // ゼロクリアし
    // 構造体を初期化する
    vma->vm_mm = mm;
    vma->vm_start = addr;
    vma->vm_end = addr + len;
    vma->vm_flags = vm_flags;
    vma->vm_page_prot = protection_map[vm_flags & 0x0f];
    vma->vm_pgoff = pgoff;

    // ファイルマッピングである場合
    if (file) {
        error = -EINVAL;
        if (vm_flags & (VM_GROWSDOWN|VM_GROWSUP))
            goto free_vma;
        if (vm_flags & VM_DENYWRITE) {
            error = deny_write_access(file);
            if (error)
                goto free_vma;
            correct_wcount = 1;
        }
        vma->vm_file = file;
        get_file(file);
        error = file->f_op->mmap(file, vma);
        if (error)
            goto unmap_and_free_vma;
    } else if (vm_flags & VM_SHARED) { // ファイルマッピングでなく共有可能である場合==共有無名リージョン
        error = shmem_zero_setup(vma); // メモリリージョンの初期化(プロセス間通信などに使用される)
        if (error)
            goto free_vma;
    }

    if ((vm_flags & (VM_SHARED|VM_ACCOUNT)) == (VM_SHARED|VM_ACCOUNT))
        vma->vm_flags &= ~VM_ACCOUNT;

    addr = vma->vm_start;
    pgoff = vma->vm_pgoff;
    vm_flags = vma->vm_flags;

    // ファイルマッピングでない若しくはリージョンのマージができない場合は
    // メモリリージョンリスト及びその赤黒木を更新する
    if (!file || !vma_merge(mm, prev, addr, vma->vm_end,
            vma->vm_flags, NULL, file, pgoff, vma_policy(vma))) {
        file = vma->vm_file;
        vma_link(mm, vma, prev, rb_link, rb_parent); // リスト及び赤黒木の更新
        if (correct_wcount)
            atomic_inc(&inode->i_writecount);
    } else {
        if (file) {
            if (correct_wcount)
                atomic_inc(&inode->i_writecount);
            fput(file);
        }
        mpol_free(vma_policy(vma));
        kmem_cache_free(vm_area_cachep, vma);
    }
out:  
    mm->total_vm += len >> PAGE_SHIFT; // プロセスのアドレス空間サイズを更新
    __vm_stat_account(mm, vm_flags, file, len >> PAGE_SHIFT);
    if (vm_flags & VM_LOCKED) { // ロックされている(スワップ不可)の場合
        mm->locked_vm += len >> PAGE_SHIFT; // スワップ不可領域のサイズを更新
        make_pages_present(addr, addr + len); // スワップ不可になっているページをメモリ上に載せる
    }
    if (flags & MAP_POPULATE) {
        up_write(&mm->mmap_sem);
        sys_remap_file_pages(addr, len, 0,
                    pgoff, flags & MAP_NONBLOCK);
        down_write(&mm->mmap_sem);
    }
    acct_update_integrals();
    update_mem_hiwater();
    return addr; // メモリリージョンのリニアアドレスを返す

unmap_and_free_vma:
    if (correct_wcount)
        atomic_inc(&inode->i_writecount);
    vma->vm_file = NULL;
    fput(file);

    /* Undo any partial mapping done by a device driver. */
    zap_page_range(vma, vma->vm_start, vma->vm_end - vma->vm_start, NULL);
free_vma:
    kmem_cache_free(vm_area_cachep, vma);
unacct_error:
    if (charged)
        vm_unacct_memory(charged);
    return error;
}

上記では大まかに以下の処理を行う

  1. 新たな新たなリージョンのためのリニアアドレス空間を確保し、もし既存リージョンと連結可能であれば連結を行う(③へ)。
  2. 連結に失敗した場合には新たにメモリリージョンとしてディスクリプタを作成しリスト及び赤黒木に繋ぐ。
  3. プロセスのアドレス空間サイズを更新し、確保したメモリリージョンのリニアアドレスを返す。

リニアアドレス空間の解放

リニアアドレス空間の解放にはdo_munmap()関数を使用する。削除対象の区間は単一のメモリリージョンとついになっている保証はなく、メモリリージョンの一部や複数のメモリリージョンにまたがっている場合がある。

// mm/mmap.c
int do_munmap(struct mm_struct *mm, unsigned long start, size_t len)
{
    unsigned long end;
    struct vm_area_struct *mpnt, *prev, *last;

    // 解放対象のリニアアドレスがページサイズでアラインメントされているか
    // 解放対象の開始アドレスがカーネル空間に入っていないか
    // 解放対象のリニアアドレス空間がカーネル空間の一部でないか
    if ((start & ~PAGE_MASK) || start > TASK_SIZE || len > TASK_SIZE-start)
        return -EINVAL; // 上記のいずれかに概要する場合はエラー

    // lenをページサイズでアラインメント
    if ((len = PAGE_ALIGN(len)) == 0)
        return -EINVAL;

    /* 解放区間よりも後ろの終端アドレスを持っている最初のメモリリージョンを見つける */
    mpnt = find_vma_prev(mm, start, &prev);
    if (!mpnt)
        return 0; // 無し

    /**
    * メモリリージョンの開始アドレスが解放区画の終端よりも大きい(後ろにある)場合
    * 解放対象区間とメモリリージョンが重なっていないことになる
    */
    end = start + len;
    if (mpnt->vm_start >= end) 
        return 0;

    /*
    * メモリリージョンを分割する
    * メモリリージョン先頭に解放区間と重ならない部分がある場合
    */
    if (start > mpnt->vm_start) {
        int error = split_vma(mm, mpnt, start, 0);
        if (error)
            return error;
        prev = mpnt; // 分割してできた解放区間の前に位置するメモリリージョン
    }

    /*
    * メモリリージョンを分割する
    * メモリリージョン末尾に解放区間と重ならない部分がある場合
    */
    last = find_vma(mm, end);
    if (last && end > last->vm_start) {
        int error = split_vma(mm, last, end, 1);
        if (error)
            return error;
    }
    mpnt = prev? prev->vm_next: mm->mmap; // 解放対象のメモリリージョンを更新

    detach_vmas_to_be_unmapped(mm, mpnt, prev, end); // メモリリージョンをプロセス空間から削除
    spin_lock(&mm->page_table_lock); // ロック
    unmap_region(mm, mpnt, prev, start, end); // リニアアドレス空間に対応するページテーブルのエントリを空に
    spin_unlock(&mm->page_table_lock); // アンロック

    unmap_vma_list(mm, mpnt); // メモリリージョンディスクリプタを解放

    return 0;
}

上記ではバリデーションとアラインメントを行ってからメモリリージョンを適切に分割し指定の区間を開放している。

参考