2013/10/27(日)LDD2_Chap13

本コンテンツは更新中です。
書籍を置いてきたので、和文(本)を読めば進みが早いかもしれません。
が、単語の役され方次第で混乱するので原文も参照する必要があるでしょう。
内容理解という点では、進みは同じかもしれません。。

前置き

ざっくりとここを流し読み。基本、ドライバ開発者向けの資料なので、何もなければkernel空間での話だと思う。
DMAを触るためには、kernelのメモリ管理を知らなければならない、ということですね。
メモリ枯渇も目にすることが多いけれど、意味がわからないことがあるので、
ちょっとお勉強…。このレベルでもguruには程遠いというので、精進が足りなさすぎる…のね。

http://www.xml.com/ldd/chapter/book/ch13.html

アドレスの種類

name 説明
User virtual addresses,仮想アドレスユーザ空間のプログラムから見える一般的なアドレスです。 ユーザアドレスは、32bitか64bitの長さで、ハードウェアアーキテクチャと 各プロセス毎の仮想アドレス空間に基づきます。
Physical addresses, 物理アドレスこのアドレスは、プロセッサとシステムのメモリとの間で使用される。 物理アドレスは32bitまたは64bitの量である。 32ビットシステムでも、いくつかの状況では、64ビットの物理アドレスを使用することができます。
Bus addresses, バスアドレスこのアドレスは周辺バスとメモリの間で使用される。 多くの場合、それらは、プロセッサによって使用される物理アドレスと同じであるが、それは必ずしもそうではない。 バスアドレスは、もちろん、非常にアーキテクチャに依存している。
Kernel logical addresses, カーネル論理アドレスこれらはカーネルの通常のアドレス空間を構成しています。 これらのアドレスは、ほとんどまたはメインメモリのすべてのマップします。 そして、これらは物理アドレスであるかのように頻繁に扱われます。 ほとんどのアーキテクチャでは、(カーネル)論理アドレスとそれに関連付けられた 物理アドレスは、一定のオフセットだけ異なる。 (カーネル)論理アドレスは、ハードウェアのネイティブポインタサイズを使用するので、 重度に使用する*1 32ビットシステム上の物理メモリのすべてに対処できない可能性があります。 論理アドレスは通常型unsigned long型またはvoid*の変数に格納されています。 kmallocのから返されたメモリは、(カーネル)論理アドレスを持っています。
Kernel virtual addresses, カーネル仮想アドレスこれらは、必ずしも物理アドレスに直接マッピングされていないという点で、(カーネル)論理アドレスとは異なる。 すべての論理アドレスは、カーネル仮想アドレスである; vmalloc()で割り当てられたメモリは、仮想アドレスも持ちます。ただし物理アドレスに直接マッピングされていません。 kmap関数も、仮想アドレスを返します。 仮想アドレスは、通常はポインタ変数に格納されています。

論理アドレスを使用している場合、マクロ __pa()(<asm/page.h>で定義されている)は、それに関連する物理アドレスを返します。
物理アドレスは、__va()マクロで(カーネル)論理アドレスにマッピングできます。ただしlow-memoryページに対してのみです。
異なるカーネル関数は、異なるアドレスの型を要求します。
必要なアドレスの種類が明示的となるように定義された別のCの型があったならば良いだろうが、我々はそのような運を持っていません。
どちらのアドレス型をどこで使うのかを明確にします。

High and Low Memory

論理アドレスと、カーネル仮想アドレスとの差は、大量のメモリが装備されている32bitシステム上で強調される。
32bitだと、4GBのメモリを表現することができる。
最近までは、32bitシステムのLinuxは、それよりも大幅に少ないメモリに限定されていました。
しかし、仮想アドレス空間を設定することで解決することができます。(超意訳)
そのシステムでは、論理アドレスを設定することができ、より多くのメモリを扱うことができませんでした。
それが必要なので、すべてのメモリをカーネルアドレスに直接マップしました。

最近の開発では、メモリの制限を排除しており、32bitシステムでは、今のシステムメモリである4GBを優に超えるメモリで作業できます。
(もちろん、プロセッサ自身がより多くのメモリをアドレスできると仮定する)
しなしながら、多くのメモリを直接論理アドレスにマッピングする方法についての制限は残っている。

メモリの最下部(上限1〜2GB、kernel config.とハードウェアに依存します)だけは、論理アドレスを持っています。残り(high-memory)は違います。
high-memoryは、64bit物理アドレスを必要とすることができます。
そして、カーネルは、それを操作するための明示的な仮想アドレスのマッピングを設定する必要があります。
したがって、多くのカーネル関数は、メモリ不足に制限される。
high memoryは、ユーザ空間プロセスページのために予約される傾向にある。

"high memory"という語は、PCの世界で別の意味を持っているため、誰かを混乱させることがあります。
ここでは、語彙を以下のように定義します。
Type describe
Low memoryカーネル空間に存在する、論理アドレスメモリ。 よく遭遇するほぼすべてのシステムで、すべてのメモリはlow memoryです。
On almost every system you will likely encounter, all memory is low memory.
High memoryカーネル空間に論理アドレスが存在しないメモリ。 32bitでアドレッシングできるよりも多い物理メモリを搭載したシステム。
と、いいつつも、実際には memory mapped I/Oが存在するので、
自由に使える空間は32bit全域では無いわけですが。。
時間も無いのでキーワードだけ。

The Memory Map and struct page

high memoryをサポートするため、struct pageが存在する。
参照カウントとwait queueを有しており、low-memoryにmapされていればそのアドレスを持つ。
low-memoryにmapされていない状態もある。

ruct page *virt_to_page(void *kaddr);
カーネル論理アドレスを引数に渡す。vmalloc()やhigh memoryのアドレスでは、機能しない。
void *page_address(struct page *page);
page構造体のポインタを渡して、このページが指すカーネルアドレスを返す。
high-memoryの場合、mapされている場合に返してくる。
#include <linux/highmem.h>
void *kmap(struct page *page);
void kunmap(struct page *page);
low-memoryでも、high-memoryでも適切に機能し、カーネル仮想アドレスを返す。
high-memoryの場合、適切な(システムに依存した定められた)空間にmapしてから返してくれる。
限られた空間なので、kunmap()の呼び出しは必須である。

Page Tables

kernelのメモリ管理にかかわる構造体。<asm/page.h>にて定義されている。
(ARMで云うなら、4Kpageまで分解するのにテーブルルックアップを3回必要とするので、そのソフトウェア版と見ればよい、か?(消化中))
略語 name description
?struct mm_struct
PGDpgd_tPage Directory.
PMDpmd_tPage mid-level Directory.
PTEpte_tPage Table.
?struct page



*1 : heavily equipped 32-bit systems.

補足事項?

"BSS"は、歴史的な遺物です。
古いアセンブリ演算子から来ていて、意味は"Block Started by Symbol"です。

[ARM] compressed kernel

2013/10/20linux::ARMimport

zImageの展開処理

調査対象は、【kernel 3.0.39 "Sneaky Weasel"】です。

例として、u-bootから起動する場合を考えます。
ここで上げるのは、とあるボードへの移植時に調査した値です。
ボード依存部の値は、デタラメに書いている可能性があるので、注意してください。


NOR bootの場合は、ROM上のコード実行が可能ですが、昨今の方式*1ではbootloaderがDRAM上に展開してから実行することが多いでしょう。

u-bootが読み込むファイル、uImageは、zImageにuImageヘッダがついたもの。
図にしてみると以下のイメージ。(AAでスミマセン)
+------------------------+
| u-boot header(64oct.)  |
+------------------------+
| zImage decompress code |
| .text                  |
| (with compressed data )|
| .got                   |
| .got.plt               |
| .bss                   |
+------------------------+
このデータの読み込みは、u-bootによって行われるため、任意のアドレスに展開されます。


ldscriptを見ると、ざっくりとこんな感じ。
  _text = .;
  .text : {}
  _etext = .;
  _got_start = .;
  .got			: { *(.got) }
  _got_end = .;
  .got.plt		: { *(.got.plt) }
  _edata = .;

  __bss_start = .;
  .bss			: { *(.bss) }
  _end = .;

*1 : NAND/SDから2nd boot programを読み出して、本当のローダを呼び出す。

事前準備

実際には、コードを読みながら参照していった結果です。

FILE: arch/arm/boot/compressed/Makefile
# Supply ZRELADDR to the decompressor via a linker symbol.
ifneq ($(CONFIG_AUTO_ZRELADDR),y)
LDFLAGS_vmlinux += --defsym zreladdr=$(ZRELADDR)
ZRELADDRは、以下で定義されます。
FILE: arch/arm/boot/Makefile
ZRELADDR    := $(zreladdr-y)
右辺は、SoC依存部にて定義されています。
FILE: arch/arm/mach-*/Makefile.boot
zreladdr-y	:= 0x40008000
params_phys-y	:= 0x40000100
initrd_phys-y	:= 0x40800000


zImage先頭から

エントリーポイントはここ。

bootloaderから飛んでくるところ

アドレスは任意。

FILE: ./boot/compressed/head.S
start:
 ※cache有効, TLBはVA-PA一対一対応, 

restart:
		.align	2
		.type	LC0, #object
LC0:
		.word	LC0					@ r1
		.word	__bss_start			@ r2
		.word	_end				@ r3
		.word	_edata				@ r6
		.word	input_data_end - 4	@ r10 (inflated size location)
		.word	_got_start			@ r11
		.word	_got_end			@ ip(r12)

		.word	.L_user_stack_end	@ sp

		.size	LC0, . - LC0

		/*
		 * We might be running at a different address.  We need
		 * to fix up various pointers.
		 */
		sub	r0, r0, r1		@ calculate the delta offset
		add	r6, r6, r0		@ _edata
		add	r10, r10, r0	@ inflated kernel size location
LC0(DRAMの位置)から、"LC0"の配置した位置(ldscriptでは先頭0、コード配置後のLC0の位置)を差っ引いて、r0に保持する。
圧縮カーネルの、展開後のサイズは、build systemによって、圧縮カーネルの後ろにLEで付与される。したがって、r10へloadする値は、input_data_endから4引いてある。(32bit決め打ちだね)
spも、relocate後のアドレスをセット。r10にsp+64kのアドレスをセット(malloc用に64k確保)(CONFIG_ZBOOT_ROM未定義の場合。定義時はコード本体がROMに入るから、_edataの後ろに配置する)

このイメージがロードされる位置がどこでも動けるようになっている。

展開後のkernel位置と、この展開コード、edata類の空間が重なっているかをチェックする。重複している場合、reset〜edataまでを、後ろからコピーする(memmove相当)。その後、コピーした後のrestartへjumpする。(これで重複しない位置で再度relocateする)

※ユーザモードで入ってきた場合、SVCで特権を得るようになってます。
ヴェクタ位置へのロードを期待しているようですが、必ずしもそうではないから、ちょっと不味そうな気がするネ。

relocate後

decompress処理のソフトと、圧縮イメージとを再配置して、kernel imageを展開できる状態になって、wont_overwriteへたどり着く。前述のように、最初のバイナリがロードされる位置によって、ここへ直接来ることもあれば、relocateすることもありますね。
wont_overwrite:
/*
 * If delta is zero, we are running at the address we were linked at.
 *   r0  = delta
 *   r2  = BSS start
 *   r3  = BSS end
 *   r4  = kernel execution address
 *   r7  = architecture ID
 *   r8  = atags pointer
 *   r11 = GOT start
 *   r12 = GOT end
 *   sp  = stack pointer
 */
・GOT書き換え(オフセット分加算して、飛び先がrelocate先のアドレスになるように)
・BSSをゼロクリア


本来のImage(vmlinux)へ

kernelの展開先アドレスを第一引数、spからsp+64kを第二〜三引数に渡して、第四引数にアーキテクチャIDをのせてcall。
void decompress_kernel(unsigned long output_start, unsigned long free_mem_ptr_p, unsigned long free_mem_ptr_end_p, int arch_id)
展開が終わったら、cache clean、cache-offしてから、圧縮前のkernel先頭へ飛ぶ。


ARM boot

2013/10/19linux::ARMimport
kernel bootからの処理をみてみましょう。
kernel先頭へは、u-bootやbare-boxから飛んできます。

そこはそれ。wiki contentsとして記事を置いておきます。

2013/10/06(日)[dbg] kernel panicネタ

togetterのまとめ、カーネルパラメータとお茶目なガチャピン先生より。
@frsyukiさんのconsole logから、ざっくりとメモリ枯渇の推定がなされる様子がわかります。

要約すると、SoftIrq中、kmalloc(,GFP_KERNEL)したときに、メモリが足りなかった、という事象です。
通常空間から呼ばれる場合は、page cacheを開放して秋メモリを確保したりもできるのでしょうが、
softirqからでは、それができない様子。呼び出し元でGFP_*を選択できると思うけれど、
呼び出し時のコンテキストによって、使って良いか、エラーになるかはあるはず。(要確認)