目次: Linux
超マニアックです。LinuxとARMが多少分かっていればわかるくらいに説明できていたら良いなあ…。
Linuxカーネルのpte_tに設定するL_PTE_MT_BUFFERABLEなどのL_PTE_MT_ 系マクロが指定するメモリタイプと、ARMのページテーブルエントリに指定するビット値をどうやってマッピングしているか?を解説します。
突然そんなこと言われてもL_PTE_MT_ なんちゃらというのは何なのか全く意味が分からないと思うので、どこで出会うかから説明します。
Linuxのデバイスドライバにて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型の変数で、ページをマップするときの属性を指定するために使います。
APIの詳細な意味は特に分からなくても、コードの書き方からしてpgprot_noncached() を呼ぶとpgprot_tの値が何か変化するのだろう、ということは分かると思います。
ページをマップするときの属性を変更する際にpgprot_tの値がどう変化するか?を追ってみます。
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_ なんちゃらが出てきました。長い導入でしたね。でも残念なことに、まだ話の1/3も終わってないんですよこれが。
なお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() を呼ぶとpgprot_tの値のbit5〜bit2が2進数の0000に書き換えられるってことです。
他にもpgprot_xxxx() 関数は色々ありますが、一例としてpgprot_writecombine() を見ると、
#define pgprot_writecombine(prot) \
__pgprot_modify(prot, L_PTE_MT_MASK, L_PTE_MT_BUFFERABLE)
このような実装になっていますので、この関数を呼ぶとpgprot_tの値のbit5〜bit2が2進数の0001に書き換えられます。
ここまででpgprot_xxxx() でpgprot_t型の値を書き換えられること、pgprot_xxxx() に対応したL_PTE_MT_XXXXマクロがあって、bit5〜bit2が書き換えられること、の2つが分かりました。
書き換える仕組みはわかっても、pgprot_t自体が何の役に立つのかがわからないですよね?次に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));
意味だけを見ればこんなもんです(型の問題で実際にこう書くとコンパイルエラーですが…)。
さらに渡されたページテーブルエントリと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を書き換えると共に、ARMのハードウェアMMUに渡しているページテーブルの「第2レベル記述子」も同時に変更する関数です。
この関数を追う前に、ARMv7のMMUについて少し説明しておきます。
ARMv7のMMUの第2レベル記述子の構成は下記の通りです。
(ARM DDI406B B3-10: B3.3.1 Translation table entry formatsの表B3-2より)
| bit | 9| 8 7 6| 5 4| 3| 2| 1| 0|
|----------+------+---------+--------+--+--+--+---|
| Lv2 entry| AP[2]| TEX[2:0]| AP[1:0]| C| B| 1| XN|
一方でLinuxのpte_tの構成は下記の通りです。
| bit | 9| 8| 7| 6| 5 4 3 2| 1| 0|
|----------------+---+-----+-------+------+--------+------+------|
| ARM Linux pte_t| XN| USER| RDONLY| DIRTY| MT[3:0]| YOUNG| VALID|
並べてみます。
| bit | 9| 8| 7| 6| 5 4| 3| 2| 1| 0|
|----------------+------+-------+-------+-------+--------+------+------+------+------|
| ARM Linux pte_t| XN| USER| RDONLY| DIRTY| MT[3:2]| MT[1]| MT[0]| YOUNG| VALID|
| Lv2 entry | AP[2]| TEX[2]| TEX[1]| TEX[0]| AP[1:0]| C| B| 1| XN|
以降、この表を使ってLinuxのpte_tの値から、ARM MMUの第2レベル記述子をどうやって作っていくか?を説明します。
前置きはこのくらいにしてcpu_v7_set_pte_ext() 関数を見ていきます。
この関数は、まず第2レベル記述子のbit9〜bit4, bit1〜bit0を0クリアして、bit1とAP[0] (bit4) に1をセットします。
| bit | 9| 8| 7| 6| 5| 4| 3| 2| 1| 0|
|----------------+------+-------+-------+-------+------+------+------+------+------+------|
| ARM Linux pte_t| XN| USER| RDONLY| DIRTY| MT[3]| MT[2]| MT[1]| MT[0]| YOUNG| VALID|
| Lv2 entry | AP[2]| TEX[2]| TEX[1]| TEX[0]| AP[1]| AP[0]| C| B| 1| XN|
|----------------+------+-------+-------+-------+------+------+------+------+------+------|
| Clear? | Yes | Yes | Yes | Yes | Yes | Yes | No | No | Yes | Yes |
| After Clear | -| -| -| -| -| -| x| x| -| -|
| After Set | -| -| -| -| -| 1| x| x| 1| -|
次にpte_tのMT[2] をTEX[0] にコピーします。
| bit | 9| 8| 7| 6| 5| 4| 3| 2| 1| 0|
|----------------+------+-------+-------+-------+------+------+------+------+------+------|
| ARM Linux pte_t| XN| USER| RDONLY| DIRTY| MT[3]| MT[2]| MT[1]| MT[0]| YOUNG| VALID|
| Lv2 entry | AP[2]| TEX[2]| TEX[1]| TEX[0]| AP[1]| AP[0]| C| B| 1| XN|
|----------------+------+-------+-------+-------+------+------+------+------+------+------|
| MT2 -> TEX0 | -| -| -| MT[2]| -| 1| x| x| 1| -|
さらにpte_tが!RDONLY && DIRTYならAP[2] を0、それ以外は1にし、pte_tのUSERをAP[1] にコピーします。
| bit | 9| 8| 7| 6| 5| 4| 3| 2| 1| 0|
|----------------+------+-------+-------+-------+------+------+------+------+------+------|
| ARM Linux pte_t| XN| USER| RDONLY| DIRTY| MT[3]| MT[2]| MT[1]| MT[0]| YOUNG| VALID|
| Lv2 entry | AP[2]| TEX[2]| TEX[1]| TEX[0]| AP[1]| AP[0]| C| B| 1| XN|
|----------------+------+-------+-------+-------+------+------+------+------+------+------|
| Set AP | 0or1| -| -| MT[2]| USER| 1| x| x| 1| -|
最初の処理でAP[0] を常に1にしていましたよね。その値と組み合わせるとAP[2:0] の値が完成します。AP[2:0] はARMのMMUに対して、ページのアクセス権を指示するためのビットフィールドです。
具体的な値の対応表は下記の通りです。値がどういう意味を持つか、横に補足しておきました。
|USER |RDONLY|DIRTY|AP[2:0] 結果|特権R|特権W|ユーザR|ユーザW|
|------+------+-----+------------+------+------+--------+--------|
|0 |0 |0 |101 |Yes |No |No |No |
|0 |0 |1 |001 |Yes |Yes |No |No |
|0 |1 |0 |101 |Yes |No |No |No |
|0 |1 |1 |101 |Yes |No |No |No |
|1 |0 |0 |111 |Yes |No |Yes |No |
|1 |0 |1 |011 |Yes |Yes |Yes |Yes |
|1 |1 |0 |111 |Yes |No |Yes |No |
|1 |1 |1 |111 |Yes |No |Yes |No |
続いてpte_tのXNビット(bit9)をXN(bit0)にコピーします。
| bit | 9| 8| 7| 6| 5| 4| 3| 2| 1| 0|
|----------------+------+-------+-------+-------+------+------+------+------+------+------|
| ARM Linux pte_t| XN| USER| RDONLY| DIRTY| MT[3]| MT[2]| MT[1]| MT[0]| YOUNG| VALID|
| Lv2 entry | AP[2]| TEX[2]| TEX[1]| TEX[0]| AP[1]| AP[0]| C| B| 1| XN|
|----------------+------+-------+-------+-------+------+------+------+------+------+------|
| Set XN | 0or1| -| -| MT[2]| USER| 1| x| x| 1| XN|
この後は、無効なページだったらAll 0にする処理とか、MMUが見ているページテーブルに実際に値を書く処理が続きますが、それは省略します。
既に誰も付いてきていないような気がしますが、ここからARMv7の魔術が始まります。
ARMv6までのMMUをご存じの方であれば、先ほど作成した第2レベル記述子の値を見たとき「その設定おかしいだろ?」と感じるはずです。私もその一人でした。
何がおかしいかといえば、
辺りがとても不思議に見えて、間違っているのではないかとすら思います。が、しかしこれで間違ってないんです。理由はARMv7のMMUには2つの動作モードがあって、ARMv6までの動作モードは使っていないからです。
TEX再マップが有効になると、指定できる属性が8パターンに制限され、TEXの意味が変わります。どの8パターンを選ぶかは自由で、選び方の作法もあるのですが、余りに細かすぎるのでここでは割愛します。どうしても気になる方はコメントください…。
TEX再マップを有効にしたとき、ARM MMUの第2レベル記述子のうち、使われるビットはTEX[0], C, Bの3つのみになります。TEX, C, Bビットの意味も変わり、ARMv6でのTEX, C, Bビットの意味は失われ、8パターンのうち何番目のパターンにするか?を表す数値として解釈されます。
以上を踏まえて先ほどのページテーブルエントリを見直すと、
TEX[0]: MT[2]
C: MT[1]
B: MT[0]
こうなってました。なのでMT[2:0] は8パターンのうち何番を使うか?を表すインデックスそのものを意味していたんですね。
こんな感じで良くできていて感心しますが、使われない悲しいビット達も居ます。
TEX再マップモードでは、第2レベル記述子のTEX[2:1] ビットに意味がありません。なので0のまま放置されています。
Linuxのpte_tのMT[3:0] は4ビットなので本来16パターンの表現力があります。しかしARMv7のTEX再マップモードの仕様で8パターンしか選べませんので、MT[3] は使い道がありません。
「前置き その2: 追ってみようpgprot_noncached()」にてL_PTE_MT_ 系マクロの値のうち、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、内部キャッシュ不可、外部キャッシュ不可です。
自分のスマホSH-01Fがヘタレてきて、ほぼ何も操作していないのに、半日で電池の容量が4割近く減るようになってしまいました…。
そろそろ2年経つし、もう電池が寿命なのかなあ。
メモ: 技術系の話はFacebookから転記しておくことにした。
EXPORT_SYMBOL(※)でググると、2年くらい前に自分が書いた解説が未だに一番上に出るんですね。かなり時間が経っているし、更新もしていないので、他のサイトが出るかと思いましたが、案外、誰も調べないもんですね。
(※)Linuxカーネル内のシンボルを、カーネルモジュールに公開するためのマクロ。
メモ: 技術系の話はFacebookから転記しておくことにした。
Device Treeを使ってARM Linuxを起動したとき、どうやってコマンドラインやinitramfsのアドレスをカーネルに伝えるんでしょうか?
U-bootのコードを見ていると *.dtbを一回メモリにロードして、わざわざメモリ上で書き換えているように見えたのですが、そんな面倒なことしないとダメなのかなあ。
ATAGSの時はコマンドラインとinitramfsの位置は簡単に伝えられたのに、新しいはずのDevice Treeが退化しているように見えるのは何故だろう…。
ちなみにx86でもDevice Treeはあまりメジャーではないものの、一応使えたはずですが、*.dtbの書き換えは誰がやるのだろう?GRUBやUEFIがやるのかなあ?
メモ: 技術系の話はFacebookから転記しておくことにした。
目次: ALSA
以前から気になっていたものの、調べずに放置していたALSAのSoC Audioについてサンプルを書いてみました。ついでに GitHub に置きました。
何もしない骨組みのドライバを書いただけですが、かなり複雑でした。コードを見ただけで「ああ、そういうことね!」って理解できる人が居たら尊敬します。師匠と崇めたいです。
取っ掛かりは難しい分、恩恵も大きいです。SoCにありがちだった問題(ボードによってSoCとDACの組み合わせが変わる問題)にエレガントに対応しており、同じような処理をするドライバを何度も書かずに済みます。素晴らしいです。
やっとボンヤリとわかってきた程度で理解が怪しいですが、次回以降、解説を書いてみようと思います。
< | 2016 | > | ||||
<< | < | 03 | > | >> | ||
日 | 月 | 火 | 水 | 木 | 金 | 土 |
- | - | 1 | 2 | 3 | 4 | 5 |
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 | 31 | - | - |
合計:
本日: