参照元

説明

超マニアックです。

Linux カーネルの pte_t に設定する L_PTE_MT_BUFFERABLE などの L_PTE_MT_ 系マクロが指定するメモリタイプと、ARM のページテーブルエントリに指定するビット値をどうやってマッピングしているか?を解説します。

前置き

L_PTE_MT_ なんちゃらにどこで出会うかから説明します。

ドライバにて mmap() システムコールを実装する際に、下記のように実装することがあります。

int hogedrv_mmap(struct file *filp, struct vm_area_struct *vma)
{
        if (filp->f_flags & O_SYNC) {
                vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot); //★これ★
        }

        return remap_pfn_range(vma, vma->vm_start, vma->vm_pgoff,
                vma->vm_end - vma->vm_start, vma->vm_page_prot);
}

やりたいことをざっと説明すると、このドライバを open() したとき O_SYNC を指定されていなければキャッシュを有効にして mmap() し、O_SYNC を指定されたらキャッシュを無効にして、つまり pgprot_noncached() を呼んでから mmap() しています。

なお vma->vm_page_prot は pgprot_t 型の変数で、ページをマップするときの属性を指定するために使います。

前置き その 2: 追ってみよう pgprot_noncached()

ページをマップするときの属性を指定を MMU にどうやって教えているかは後で言うとして、ひとまず pgprot_xxxx() が何をしているか?どんな実装か?を追ってみます。

arch/arm/include/asm/pgtable.h:

#define __pgprot_modify(prot,mask,bits)         \
        __pgprot((pgprot_val(prot) & ~(mask)) | (bits))

#define pgprot_noncached(prot) \
        __pgprot_modify(prot, L_PTE_MT_MASK, L_PTE_MT_UNCACHED)

難しく見えますが大したことはなく、prot の値を L_PTE_MT_MASK でマスク(つまり bit5~bit2 の値だけを 0 に書き換え)して、そこに L_PTE_MT_UNCACHED を OR するだけのマクロです。ここでやっと L_PTE_MT_ なんちゃらが出てきます。

なお L_PTE_MT_ 系マクロの代表的なシンボルと値は下記の通りです。他の定義もありますが、後述する通り ARMv7 では最上位ビットに意味がないので、無視して構いません。

#define L_PTE_MT_UNCACHED       (_AT(pteval_t, 0x00) << 2)      /* 0000 */
#define L_PTE_MT_BUFFERABLE     (_AT(pteval_t, 0x01) << 2)      /* 0001 */
#define L_PTE_MT_WRITETHROUGH   (_AT(pteval_t, 0x02) << 2)      /* 0010 */
#define L_PTE_MT_WRITEBACK      (_AT(pteval_t, 0x03) << 2)      /* 0011 */
#define L_PTE_MT_MINICACHE      (_AT(pteval_t, 0x06) << 2)      /* 0110 (sa1100, xscale) */
#define L_PTE_MT_WRITEALLOC     (_AT(pteval_t, 0x07) << 2)      /* 0111 */
#define L_PTE_MT_DEV_SHARED     (_AT(pteval_t, 0x04) << 2)      /* 0100 */

ですので、pgprot_noncached() だと bit5~bit2 が 2進数の 0000 に書き換えられます。他にも pgprot_writecombine() というのもあって、

#define pgprot_writecombine(prot) \
        __pgprot_modify(prot, L_PTE_MT_MASK, L_PTE_MT_BUFFERABLE)

この場合は bit5~bit2 が 2進数の 0001 に書き換えられます。

追ってみよう remap_pfn_range()

次に pgprot_t の値がなぜキャッシュ有効/無効の設定に寄与するのか?を追います。

手がかりは remap_pfn_range() で、第五引数に渡した vma->vm_page_prot の値がどこで使われるか?カーネルのコードを追いかけます。

-- remap_pfn_range()
  -- remap_pud_range()
     -- remap_pmd_range()
        -- remap_pte_range()
           pte_t *pte;
           ...
           pte = pte_alloc_map_lock(mm, pmd, addr, &ptl);
           ...
           set_pte_at(mm, addr, pte, pte_mkspecial(pfn_pte(pfn, prot)));

いくつかの関数を経由しますが、最終的にはマップしたいアドレスの pte(Linux のページテーブルエントリ、pte_t 型)を取ってきて、pgprot_t の値と一緒に set_pte_at() 関数に渡します。

関数がゴチャゴチャ呼ばれてややこしいので、先に pfn_pte() と pte_mkspecial() をやっつけます。

arch/arm/include/asm/pgtable-2level.h: 

static inline pte_t pte_mkspecial(pte_t pte) { return pte; }

ARM では素通しするだけの実装なので、無視して良いです。

arch/arm/include/asm/pgtable.h: 

#define pfn_pte(pfn,prot)       __pte(__pfn_to_phys(pfn) | pgprot_val(prot))

include/asm-generic/memory_model.h: 

#define __pfn_to_phys(pfn)      PFN_PHYS(pfn)

include/linux/pfn.h: 

#define PFN_PHYS(x)     ((phys_addr_t)(x) << PAGE_SHIFT)

まず pgprot_val() ですが pgprot_t から値を取り出すマクロです。実は pgprot_t は構造体などの typedef となっていて、prot | 1 のように直接演算できません。察するに、間違って int などを足して、値をぶっ壊す事故を防ぐためだと思われます。

次に __pfn_to_phys() ですが PFN(Page Frame Number, ページフレーム番号)から物理アドレスを得るマクロです。といっても、ページサイズ分だけ左シフトするだけです。ARM の場合はページサイズが 4KB なので、12bit 左シフトします。

最後に __pte() は pte_t 型にキャストするためのマクロです。pgprot_t と同様に pte_t も構造体などの typedef であることが多いので、(pte_t) でキャストするとコンパイラに怒られるのです。

以上を総合すると、pte_mkspecial(pfn_pte(pfn, prot)) がやっていることは、要は (pfn << 12 | prot) なので、

set_pte_at(mm, addr, pte, (pfn << 12 | prot));

意味だけを見ればこんなもんです(型の問題で実際にこう書くとコンパイルエラーですが…)。

追ってみよう set_pte_at()

渡されたページテーブルエントリと pgprot_t の値の行方を見てみます。

-- set_pte_at(..., pte_t *ptep, pte_t pteval)
   -- set_pte_ext(ptep, pteval, ext)
      -- cpu_set_pte_ext(ptep, pteval, ext)
         -- cpu_v7_set_pte_ext(ptep, pteval, ext)

※ext は基本的には 0 です。

最終的にはアセンブラの関数にたどり着きます。実装は arch/arm/mm/proc-v7-2level.S にあります。この関数は第二引数 pteval の値を使って、第一引数 ptep の指す pte_t を書き換えると共に、ハードウェア MMU に渡しているページテーブルの「第 2レベル記述子」も同時に変更する関数です。

前置き その 3: 追ってみよう ARMv7 の MMU

この関数を追う前に、ARMv7 の MMU について少し説明しておきます。

ARM MMU の第 2レベル記述子の構成は下記の通りです。 (ARM DDI406B B3-10 - B3.3.1 Translation table entry formats の表 B3-2より)

bit98 7 65 43210
Lv2 entryAP[2]TEX[2:0]AP[1:0]CB1XN

一方で Linux の pte_t の構成は下記の通りです。

bit98765 4 3 210
ARM Linux pte_tXNUSERRDONLYDIRTYMT[3:0]YOUNGVALID

並べてみます。

bit98765 43210
ARM Linux pte_tXNUSERRDONLYDIRTYMT[3:2]MT[1]MT[0]YOUNGVALID
Lv2 entryAP[2]TEX[2]TEX[1]TEX[0]AP[1:0]CB1XN

以降、この表で Linux の pte_t の値から、ARM MMU の第 2レベル記述子をどうやって作っていくか?を説明します。

追ってみよう cpu_v7_set_pte_ext()

前置きはこのくらいにして cpu_v7_set_pte_ext() 関数を見ていきます。

この関数はページテーブルエントリのうち、bit9~bit4, bit1~bit0 を 0 クリアして、bit1 と AP[0] (bit4) に 1 をセットします。

bit9876543210
ARM Linux pte_tXNUSERRDONLYDIRTYMT[3]MT[2]MT[1]MT[0]YOUNGVALID
Lv2 entryAP[2]TEX[2]TEX[1]TEX[0]AP[1]AP[0]CB1XN
-----
Clear?YesYesYesYesYesYesNoNoYesYes
After Clear------xx--
After Set-----1xx1-

次に pte_t の MT[2] を TEX[0] にコピーします。

bit9876543210
ARM Linux pte_tXNUSERRDONLYDIRTYMT[3]MT[2]MT[1]MT[0]YOUNGVALID
Lv2 entryAP[2]TEX[2]TEX[1]TEX[0]AP[1]AP[0]CB1XN
-----
MT2 -> TEX0---MT[2]-1xx1-

さらに pte_t が !RDONLY && DIRTY なら AP[2] を 0、それ以外は 1 にし、pte_t の USER を AP[1] にコピーします。

bit9876543210
ARM Linux pte_tXNUSERRDONLYDIRTYMT[3]MT[2]MT[1]MT[0]YOUNGVALID
Lv2 entryAP[2]TEX[2]TEX[1]TEX[0]AP[1]AP[0]CB1XN
-----
Set AP0or1--MT[2]USER1xx1-

最初の方で AP[0] を常に 1 にしていましたよね。その値と組み合わせると AP[2:0] の値が完成します。AP[2:0] は ARM の MMU に対して、ページのアクセス権を指示するためのビットフィールドです。

具体的な値の対応表は下記の通りです。値がどういう意味を持つか、横に補足しておきました。

USERRDONLYDIRTYAP[2:0] 結果特権 R特権 Wユーザ Rユーザ W
000101YesNoNoNo
001001YesYesNoNo
010101YesNoNoNo
011101YesNoNoNo
100111YesNoYesNo
101011YesYesYesYes
110111YesNoYesNo
111111YesNoYesNo

続いて pte_t の XN ビット(bit9)を XN(bit0)にコピーします。

bit9876543210
ARM Linux pte_tXNUSERRDONLYDIRTYMT[3]MT[2]MT[1]MT[0]YOUNGVALID
Lv2 entryAP[2]TEX[2]TEX[1]TEX[0]AP[1]AP[0]CB1XN
-----
Set XN0or1--MT[2]USER1xx1XN

この後には、無効なページだったら All 0 にする処理とか、MMU が見ているページテーブルに実際に値を書く処理が続きますが、それは省略します。

追ってみようページテーブルエントリ

ARMv6 までの MMU をご存じの方であれば、先ほど作成したページテーブルエントリの値を見て「その設定おかしいだろ?」と感じるはずです。私もその一人でした。

何がおかしいかといえば、

  • ARM MMU にて、メモリタイプとキャッシュ属性を指示するための TEX[2:1] が 0 のまま。
  • Linux の pte_t にて、メモリタイプとキャッシュ属性を指示するための MT[3:0] のはずなのに、MT[3] を使わない。

辺りです。しかしこれで間違ってないんです。理由は ARMv7 の MMU には 2つの動作モードがあって、ARMv6 までの動作モードは使っていないからです。

  • モード 1つ目は ARMv6 までと同様の TEX[2:0] と C, B ビットを使って、キャッシュ属性を指定する方法です。
  • モード 2つ目は ARMv7 からの機能で TEX 再マップ(TEX remap)というモードです。実は Linux はこちらを使っています。

TEX 再マップが有効になると、指定できる属性が 8パターンに制限されます。どの 8パターンを選ぶかは自由で、選び方の作法もあるのですが、余りに細かすぎるのでここでは割愛します。どうしても気になる方はコメントください…。

TEX 再マップを有効にしたとき、ARM MMU の第 2レベル記述子のうち、使われるビットは TEX[0], C, B の 3つのみになります。ビットの意味も変わって、従来の TEX, C, B ビットの意味は失われ、8パターンのうち何番目のパターンにするか?を表す数値として解釈されます。

以上を踏まえて先ほどのページテーブルエントリを見直すと、

TEX[0]: MT[2] C: MT[1] B: MT[0]

こうなってましたね。なので MT[2:0] はなんと 8パターンのうち何番を使うか?を表すインデックスそのものになっていたんですね。

悲しみのビット達

こんな感じで良くできていて感心しますが、使われないビット達も居ます。

TEX 再マップモードでは TEX[2:1] ビットに意味がありません。なので 0 のまま放置されています。

Linux の pte_t の MT[3:0] は 4ビットなので 16パターンの表現力があります。しかし ARMv7 の仕様として 8パターンしか選べませんので MT[3] は意味を持ちません。

まとめ

まとめるの難しいですが、まとめると pgprot_noncached() を prot に指定すると、pte_t の MT[2:0] に 000 が入ります。

これは TEX 再マップモードの ARMv7 MMU にとって、属性のパターン 0 を選びなさいという意味になります。もし pgprot_writecombine() なら pte_t の MT[2:0] には 001 が入り、パターン 1を選びなさい、という意味になります。

参考までに Linux-4.4.1 でのパターン 0 の設定内容は、メモリタイプ Strongly-ordered、内部キャッシュ不可、外部キャッシュ不可です。パターン 1 は、メモリタイプ Normal Memory、内部キャッシュ不可、外部キャッシュ不可です。

参考

関連モジュール

コメント


トップ   編集 凍結 差分 バックアップ 添付 複製 名前変更 リロード   新規 一覧 単語検索 最終更新   ヘルプ   最終更新のRSS
Last-modified: 2016-03-09 (水) 10:32:24 (924d)