記錄一下!
https://github.com/scottt/debugbreak
2020年8月5日 星期三
2011年2月9日 星期三
TI 發表 OMAP 5 處理器:雙核 Cortex A15 核心
或許是受到Nvidia的壓力,TI也公佈下一代採用多核架構的OMAP 5了,
TI OMAP 5這一系列有兩個型號,OMAP 5430(手持裝置)及OMAP 5432(平版裝置)。
採用28柰米製程,在耗電量上比採用45柰米製程的OMAP4要更省電40%,時脈最高可達2GHz
在圖形處理器上面,內建的是 PowerVR SGX 544 GPU
最多可支援8GB記憶體
硬體虛擬化能力
支援4台攝影機,包括S3D攝影機,支援能即時將2D影像透過S3D技術轉為1080p的3D影像(將2D變3D??這個我還沒看過實物@@)
DLP 微型投影機
OMAP5 另外還有一些很酷的功能,像是免觸控的手勢操作支援、支援多作業系統同時運作,真實的 3D 顯示遊戲、無線影音傳輸、多螢幕顯示、電子錢包功能等等。
下面有TI OMAP5的影片,介紹了上面所說的功能,也許這就是我們未來的行動生活也不一定喔!

資料來源:
官方OMAP™ Applications Processors: OMAP™ 5 Platform
TI's OMAP 5 processors will sport dual 2GHz ARM Cortex A-15 cores
TI發表感覺好厲害的OMAP 5,用雙老鷹與雙低功耗組成四核架構
TI 發表 OMAP 5 處理器:雙核 Cortex A15 核心
TI OMAP 5這一系列有兩個型號,OMAP 5430(手持裝置)及OMAP 5432(平版裝置)。
採用28柰米製程,在耗電量上比採用45柰米製程的OMAP4要更省電40%,時脈最高可達2GHz
在圖形處理器上面,內建的是 PowerVR SGX 544 GPU
最多可支援8GB記憶體
硬體虛擬化能力
支援4台攝影機,包括S3D攝影機,支援能即時將2D影像透過S3D技術轉為1080p的3D影像(將2D變3D??這個我還沒看過實物@@)
DLP 微型投影機
OMAP5 另外還有一些很酷的功能,像是免觸控的手勢操作支援、支援多作業系統同時運作,真實的 3D 顯示遊戲、無線影音傳輸、多螢幕顯示、電子錢包功能等等。
下面有TI OMAP5的影片,介紹了上面所說的功能,也許這就是我們未來的行動生活也不一定喔!
資料來源:
官方OMAP™ Applications Processors: OMAP™ 5 Platform
TI's OMAP 5 processors will sport dual 2GHz ARM Cortex A-15 cores
TI發表感覺好厲害的OMAP 5,用雙老鷹與雙低功耗組成四核架構
TI 發表 OMAP 5 處理器:雙核 Cortex A15 核心
2010年3月16日 星期二
Keil DS 5 & Beagle Board Demo
Development Tools for ARM Linux
DS-5 Application Edition makes it easy to develop Linux applications for ARM-based platforms. It reduces your learning curve, shortens the development and testing cycle, and helps you build reliable applications quickly.
Availability
The DS-5 Application Edition is available for download today from http://www.keil.com/arm/ds5/ and can be used for free until September 30, 2010.
資料來源:
DS-5 Application Edition
ARM Launches DS-5 Development Tools for ARM Linux-Based Systems
ARM AMBA 4發揮極大化效能及功耗表現
DS-5 Application Edition makes it easy to develop Linux applications for ARM-based platforms. It reduces your learning curve, shortens the development and testing cycle, and helps you build reliable applications quickly.
Availability
The DS-5 Application Edition is available for download today from http://www.keil.com/arm/ds5/ and can be used for free until September 30, 2010.
資料來源:
DS-5 Application Edition
ARM Launches DS-5 Development Tools for ARM Linux-Based Systems
ARM AMBA 4發揮極大化效能及功耗表現
2008年8月27日 星期三
C語言裏嵌入組合語言
C下的inline組語
在C語言中嵌入組語的程式碼加個__asm__("asm code");
第一個__asm__是說input的東西把operand1(C的變數)放到r,也就是%1, operand2(C的變數)放到r,也就是%2,然後執行assembly code的 movl與addl, 然後結果放到sum(C的變數)=r 也就是%0, 在這邊我們沒有指定eax ebx,只是很單純的%0 %1 %2 r。 %0 %1 %2 分別對應了output r, input r, input r, %0 %1 %2.....會先對應output裡的register,再對應input裡的register, 就是它們出現的順序。 gcc會幫我們最佳化register的使用。 clobbered operands是一堆registers, 我們告訴gcc說這些registers的值已經被暴力的摧毀(被改變了), 你要重新考慮它們的合法性才行, 在這邊0就是%0,gcc會特別照顧一下它所選的%0的值。 有一些規則要說明
r, g, 0這些東西叫constraints(限制的意思),每一個符號有特殊意義 代表這個暫存器必需是什麼型式的(暫存器有通用暫存器,符點運算暫存器, cpu指令有的允許直接對memory做運算,有的不準等等條件的暫存器, r代表通用型暫存器)。
output operand前一定要有個"="表示這個constraint是read-ony, "="叫constraint modifier。
input output operands後一定要跟著相對應的C 變數的參數, 這是給asm的參數。
如果statement裡真要指定register要多加個%變成%%eax
通用constraints
參考資料:
GNU Compiler Collection
AT&T ASM
ARM GCC Inline Assembler Cookbook
GCC-Inline-Assembly-HOWTO
Using Inline Assembly With gcc
在C語言中嵌入組語的程式碼加個__asm__("asm code");
__asm__(注意quote 與\n\t的位置 \n\t是因為gcc其實根據這些與其它c code產生一個.s檔 \n\t只是在.s檔產生newline與tab這在每一行都要除了最後一行不用 另外inline assembly的通式是
"movl $1,%eax\n\t" // SYS_exit
"xor %ebx,%ebx\n\t"
"int $0x80"
);
__asm__(asm statements : outputs : inputs : clobber);通式有兩個重要的概念, 一個是可以和C程式傳變數來溝通,另外 如果我們指定了eax ebx...等register,則這下可完了,如果其他的C code 也正在用eax ebx,則compiler必須先把這些值推進stack才能跑你的asm code, 所以我們可以不特別指定register,讓gcc自動作register運用的最佳化。 其實這是其他mips和其他CPU的作法,別種CPU的register命名沒 eax, ebx.....這麼死與囉唆, $1 $2 $3...就搞定,還可以換來換去很有彈性。 這個通式就是在做這樣的事,請看一個例子
int main (void) {
int operand1, operand2, sum, accumulator;
operand1 = rand (); operand2 = rand ();
__asm__ ("movl %1, %0\n\t"
"addl %2, %0"
: "=r" (sum) /* output operands */
: "r" (operand1), "r" (operand2) /* input operands */
: "0"); /* clobbered operands */
accumulator = sum;
__asm__ ("addl %1, %0\n\t"
"addl %2, %0"
: "=r" (accumulator)
: "0" (accumulator), "g" (operand1), "r" (operand2)
: "0");
return accumulator;
}
第一個__asm__是說input的東西把operand1(C的變數)放到r,也就是%1, operand2(C的變數)放到r,也就是%2,然後執行assembly code的 movl與addl, 然後結果放到sum(C的變數)=r 也就是%0, 在這邊我們沒有指定eax ebx,只是很單純的%0 %1 %2 r。 %0 %1 %2 分別對應了output r, input r, input r, %0 %1 %2.....會先對應output裡的register,再對應input裡的register, 就是它們出現的順序。 gcc會幫我們最佳化register的使用。 clobbered operands是一堆registers, 我們告訴gcc說這些registers的值已經被暴力的摧毀(被改變了), 你要重新考慮它們的合法性才行, 在這邊0就是%0,gcc會特別照顧一下它所選的%0的值。 有一些規則要說明
r, g, 0這些東西叫constraints(限制的意思),每一個符號有特殊意義 代表這個暫存器必需是什麼型式的(暫存器有通用暫存器,符點運算暫存器, cpu指令有的允許直接對memory做運算,有的不準等等條件的暫存器, r代表通用型暫存器)。
output operand前一定要有個"="表示這個constraint是read-ony, "="叫constraint modifier。
input output operands後一定要跟著相對應的C 變數的參數, 這是給asm的參數。
如果statement裡真要指定register要多加個%變成%%eax
通用constraints
I, J, K .... P 是根據不同cpu可以做不同的解釋來表示一個範圍的立即定址整數
Q, R, S .... U
0, 1, 2 .... 相對於assembly statement裡的%0 %1 %2 ....
a, b, c .... f 可以根據不同cpu可以做不同定義的registers
m 表示這是一個memory的operand 例如mov eax, data中的data
p 合法的記憶體位址
r 一般通用型register
g 任何的通用型register, memory operand, 立即定址的整數
constraints在386上有
a eax
b ebx
c ecx
d edx
S esi
D edi
I 表示只有constant value (0 to 31)才行
r 可以是eax ebx ecx edx esi edi
q 可以是eax ebx ecx edx
g eax, ebx, ecx, edx or variable in memory
A eax and edx combined into a 64-bit integer (use long longs)
參考資料:
GNU Compiler Collection
AT&T ASM
ARM GCC Inline Assembler Cookbook
GCC-Inline-Assembly-HOWTO
Using Inline Assembly With gcc
2008年8月15日 星期五
Introduction to AMBA Bus System
本篇文章主要是介紹ARM Limited.公司所推出的AMBA 協定(Advanced Micro-controller Bus Architecture)。AMBA 協定目前是open 且free 的,讀者可從ARM 的網站(www.arm.com)下載完整的Specification。這篇文章並沒有打算說明完整的AMBA 協定內容,詳細的Spec.還是請讀者閱讀ARM所提供的文件。原本的AMBA 協定包含了四大部分: AHB, ASB, APB, Test Methodology,限於篇幅的關係,我們挑選較重要的AHB, APB 加以基本的介紹,並探討AHB 的一些重要的特性。
資料來源:
http://tpe-wh3.dwins.net/download/member_file/2002/soc/2002-5-1.pdf
資料來源:
http://tpe-wh3.dwins.net/download/member_file/2002/soc/2002-5-1.pdf
2008年8月12日 星期二
JTAG模擬器
以往當硬體完成,需要移植或發展驅動程式時,都需要用到ICE。所以當ARM可以內建部份ICE功能時,確實可為發展雛型硬體,軟體及系統晶片整合的工程師節省了開發時間。尤其是現在,微處理器只是系統整合晶片的一部份,也不可能將微處理器獨立出來,或是將其它部份移出,還是要ICE來協助系統晶片開發。所有的ARM核心只要有包含嵌入式ICE(Embedded-ICE)邏輯都建置JTAG模擬器(emulator)。
JTAG模擬器包含下列能力:
(1)依照連接的除錯軟體的控制啟動及停止ARM核心的動作。
(2)開發工程師可以檢視及修改暫存器及記憶體內容,而且可以設定程式的中斷點(breakpoints)及觀察點(watch points)。
(3)在Multi-ICE的支援下,也提供下載程式,追蹤程式(trace),還有即時監控程式(Real Monitor)提供即時應用偵錯的能力。
補充資料:
一份簡體中文的JTAG詳細介紹
http://www.micetek.com.cn/technic/jtag.pdf
http://moe.easylearn.org/e_book/%B2%C43%B3%B9%20ARM%20%C2%B2%A4%B6.doc
JTAG模擬器包含下列能力:
(1)依照連接的除錯軟體的控制啟動及停止ARM核心的動作。
(2)開發工程師可以檢視及修改暫存器及記憶體內容,而且可以設定程式的中斷點(breakpoints)及觀察點(watch points)。
(3)在Multi-ICE的支援下,也提供下載程式,追蹤程式(trace),還有即時監控程式(Real Monitor)提供即時應用偵錯的能力。
補充資料:
一份簡體中文的JTAG詳細介紹
http://www.micetek.com.cn/technic/jtag.pdf
http://moe.easylearn.org/e_book/%B2%C43%B3%B9%20ARM%20%C2%B2%A4%B6.doc
2008年8月7日 星期四
ARM Register
ARM的暫存器,全部共有31個32位元的暫存器。但ARM核心同時最多只可以有18個同時執行的暫存器:這包含了16個資料暫存器和2個處理器狀態暫存器,16個資料暫存器中包含了3個專用暫存器,R13、R14和R15。


- R0-R7:這8個暫存器是所有模式都共用的。
- R8-R12:則會依照模式去切換Bank Register。
- R13通常做為堆疊指標Stack Pointer,保存目前處理器模式的堆疊的堆疊頂端。(by convention)
- R14為Link Register,保存副程式的返回位址,比如在BL指令時,會將PC的值複製到R14,作為返回(Return)的位址。(hardwired)
- R15為PC(Program Counter),內容存放處理器要存取的下一道指令位址。(hardwired)

- R13(Stack Pointer:SP):用途大約可分三大類
- 中斷產生時,不想改變目前模式下的暫存器,把所有暫存器資料儲存起來
- 當函式被呼叫時,一樣也不想改變呼叫者的暫存器,也會把所有暫存器資料儲存起來
- C語言的區域變數(Local Variable)會使用

- 有一個Current Program Status Register (CPSR)
- 在所有處理器模式下都可以存取當前的程式狀態暫存器CPSR。
- CPSR包含條件碼旗標,中斷禁止位元,當前處理器模式以及其他狀態和控制資訊。
- 有五個Saved Program Status Registers (SPSRs)
- 每種例外模式都有一個程式狀態保存暫存器SPSR。
- SPSR用於保留CPSR的狀態。
- 條件碼旗標(Condition Code Flag):
N,Z,C,V 大多數指令可以測試這些條件碼旗標以決定程式指令如何執行
控制位元(Control Bit):
最低8位元 I,F,T和M位元用做控制位元。當例外出現時改變控制位元。當處理器在特權模式下也可以由軟體改變。
中斷禁止位元(Interrupt disable bit):I設成1則禁止IRQ中斷。F設成1則禁止FIQ中斷。
T位元(State Bit或Thumb Bit):
T=0 指示ARM執行。T=1指示Thumb執行。在這些架構系統中,可自由地使用能在ARM和Thumb狀態之間切換的指令。
模式位元(Mode Bit):
M0, M1, M2, M3和M4 (M[4:0]) 是模式位元.這些位元決定處理器的工作模式。
I/O對應的方式
- I/O mapped I/O(port-mapped I/O或Direct I/O)
- Memory Mapped I/O
I/O與memory均擁有自己的記憶體空間
需要特別的指令來處理I/O
好處是完全不用考慮記憶體空間被I/O佔用,缺點需要額外的指令專門處理I/O存取。

I/O與memory共用記憶體空間
不需要特別指令來處理I/O
其實Memory mapped I/O只是將I/O的port或memory 映射(mapping)到記憶體位址(memory address)上,
其好處就是可以把I/O存取直接當成存取記憶體來用,缺點是有映射到的區域原則上就不能放真正的記憶體。

假設有個PCI的Device它的PFA(PCI Function Address)為(0,6,0)[bus/dev/function]
我們想要對PCI Register 43h bit1寫入1
範例程式碼如下:
mov eax, 80003040h
mov dx, 0cf8h
out dx, eax
mov dx, 0cffh
in al, dx
or al, 00000010b
out dx, al
IO Port 0x0cf8/0x0cfc為PCI Config Address/Data Port
這種方式就是I/O Mapped I/O
如果是PCIe Device的話,原則上原本的0x0cf8/0x0cfc還是可以用,但它只能存取Offset 00h~FFh,
要存取100h以上的空間時,就必需要用MMIO了。
範例程式如下:
假設我們要讀取Device(4,0,0)的register 0,讀2bytes
mov ax,[50400000h]
假設0x50000000是PCIe的Memory Base Address
PCIe PFA[27:20]: Bus information
PCIe PFA[19:15]: Device information
PCIe PFA[14:12]: Function information
PCIe PFA[11: 8]: Extended Register
PCIe PFA[7:2]: DW number
PCIe PFA[1:0]: Byte enable
參考資料來源:
http://biosengineer.blogspot.com/2007/10/bios-pci-scan-9.html
http://www.ltivs.ilc.edu.tw/kocp/mpu/m2/m2-3-2.htm
http://www.ltivs.ilc.edu.tw/kocp/mpu/m2/m2-3-3.htm
2008年8月5日 星期二
ARM的CPU比較表
Processor Family | #of pipeline stages | Memory Organization | Clock Rate | MIPS/MHz |
ARM6 | 3 | Von Neumann | 25MHz | |
ARM7 | 3 | Von Neumann | 66MHz | 0.9 |
ARM8 | 5 | Von Neumann | 72MHz | 1.2 |
ARM9 | 5 | Harvard | 200MHz | 1.1 |
ARM10 | 6 | Harvard | 400MHz | 1.25 |
StrongARM | 5 | Harvard | 233MHz | 1.15 |
ARM11 | 8 | Von Neumann/Harvard | 550MHz | 1.2 |
所謂的 Von Neumann 架構與 Harvard 架構。兩者的不同點在於:傳統的 Von Neuman 架構(由 Von Neuman 於 1940 年代提出),程式與資料共用相同的匯流排(bus),而 Harvard 架構的程式與資料走的是不同的匯流排(也可能有不只一條的資料匯流排)。Harvard 架構明顯的好處就是在於讀取程式碼的同時也可以進行資料的存取。8051 是典型的 Von Neuman 架構,ALE(Address Latch Enable)控制信號就是特徵之一。現代的 DSP 則多是採用 Harvard 架構以加速其在算數運算上的能力。(文字說明來源:http://www.ialbert.net/index.php?load=read&id=34)
ARM中斷流程
程式的執行過程中,CPU經常會收到硬體中斷的訊號,告知硬體有些事情需要去處理,比如:OS的System Clock...等等。而ARM的外部硬體中斷訊號由IRQ及FIQ兩支訊號腳來控制,當IRQ(Interrupt Request)或FIQ(Fast Interrupt Request),當IRQ或FIQ被觸發時,則ARM CPU將會強制的進入IRQ或FIQ模式,在ARM裏IRQ有r13/r14/spsr是banked register,FIQ則有r8/r9/r10/r11/r12/r13/r14/spsr,所以沒有banked register的部份,程式就必需自行處理保存資料,所以FIQ可提供比IRQ還要好的效能。
先看一下中斷向量表IVT:interrupt vector table
在x86的IVT範圍是:0x00000000~0x000003FF
而ARM的IVT則是0x00000000~0x0000001F
0x00000000 硬體重置SVC 10011
0x00000004 未定義指令Undefined Mode 11011
0x00000008 軟體中斷指令(SWI) SVC 10011
0x0000000C 預先擷取中止Abort Mode 10111
0x00000010 資料中止Abort Mode 10111
0x00000014 保留reserved 11011
0x00000018 IRQ IRQ Mode 10010
0x0000001C FIQ FIQ Mode 10001
0x0000001F 保留reserved
說明如下:
微處理器進入中斷時(硬體本身的中斷處理動作):
(1)自動將原程式中下一個準備要執行的指令位址儲存至R14(Link Register)
(2)將CPSR複製到適當的SPSR
(3)依中斷的要求設定CPSR mode bits
(4)強迫PC擷取相對應的中斷向量指令。
微處理器從中斷返回時(硬體本身的中斷處理動作):
(1)將Link Register減去相對應之偏移值後寫入PC
(2)將SPSR值搬回CPSR
(3)如果在中斷進入點設定了interrupt disable flags,此時還原。
當系統重置有效後會使微處理器執行(硬體本身的中斷處理動作):
(1)將目前之PC及CPSR覆蓋R14_svc及SPSR_svc
(2)CPSR[4:0]=0b10011(進入supervisor模式)、CPSR[5]=0(在ARM模式下執行)、CPSR[6]=1(disable FIQ)、CPSR[7]=1(disable IRQ)
(3)強迫PC擷取位址0x0之中斷向量指令
(4)回到ARM模式下繼續執行。
所以通常在0x0~0x1F我們會放如下的程式碼:
b HandlerReset
b HandlerUndef
b HandlerSWI
b HandlerPabort
b HandlerDabort
b .;保留沒有使用
b HandlerIRQ
b HandlerFIQ
當相對應的中斷發生時,CPU切換模式,然後fetch IVT裏面的程式,然後跳到你自己定義的位址去執行你要處理的事情。到目前為止處理的步驟我們通常稱之為一級中斷向量表,但一個中斷向量表應該都不夠用,因為像IRQ,通常硬體不可能只會有一個,所以還會有一個中斷控制器(interrupt controller)去管更多的硬體,而這麼多的硬體中斷則由二級中斷向量表去處理,也就是進入HandlerIRQ後,要再去判斷現在是什麼硬體發出中斷的,再跳去該硬體的中斷服務常式去處理。
我們拿個u-boot loader來當範例看一下IRQ的部份
底下是IVT定義,_start當然是0x0開始囉!
_start: b reset
ldr pc,=HandleUndef
ldr pc,=HandleSWI
ldr pc,=HandlePabort
ldr pc,=HandleDabort
b .
ldr pc,=HandleIRQ;你要自己定義HandleIRQ的實際位置在那裏
ldr pc,=HandleFIQ
real_vectors的位置也是自己定義,但要跟上面的HandleXXXX對映起來
real_vectors:
ldr pc,=reset
ldr pc,=undefined_instruction
ldr pc,=software_interrupt
ldr pc,=prefetch_abort
ldr pc,=data_abort
ldr pc,=not_used
ldr pc,=irq
ldr pc,=fiq
這裏的IRQ處理取得IRQ的堆疊指標,備份暫存器,執行真正的IRQ ISR,恢復暫存器值
irq:
get_irq_stack
irq_save_user_regs
bl do_irq
irq_restore_user_regs
底下的部份就是上面irq裏使用的程式碼
.macro irq_save_user_regs
sub sp, sp, #S_FRAME_SIZE
stmia sp, {r0 - r12} @ Calling r0-r12
add r8, sp, #S_PC
stmdb r8, {sp, lr}^ @ Calling SP, LR
str lr, [r8, #0] @ Save calling PC
mrs r6, spsr
str r6, [r8, #4] @ Save CPSR
str r0, [r8, #8] @ Save OLD_R0
mov r0, sp
.endm
.macro irq_restore_user_regs
ldmia sp, {r0 - lr}^ @ Calling r0 - lr
mov r0, r0
ldr lr, [sp, #S_PC] @ Get PC
add sp, sp, #S_FRAME_SIZE
subs pc, lr, #4 @ return & move spsr_svc into cpsr
.endm
.macro get_irq_stack @ setup IRQ stack
ldr sp, =STACK_BOTTOM @IRQ_STACK_START
/*ldr sp, =IRQ_STACK_START @IRQ_STACK_START*/
.endm
上圖是一張我蠻喜歡的圖,呈現了ARM裏面各中斷與各模式的關係圖
上面這張則是介紹當IRQ引發後(1)先跳到0x18的位址(2)由0x18再轉跳到ISR去。
先看一下中斷向量表IVT:interrupt vector table
在x86的IVT範圍是:0x00000000~0x000003FF
而ARM的IVT則是0x00000000~0x0000001F
0x00000000 硬體重置SVC 10011
0x00000004 未定義指令Undefined Mode 11011
0x00000008 軟體中斷指令(SWI) SVC 10011
0x0000000C 預先擷取中止Abort Mode 10111
0x00000010 資料中止Abort Mode 10111
0x00000014 保留reserved 11011
0x00000018 IRQ IRQ Mode 10010
0x0000001C FIQ FIQ Mode 10001
0x0000001F 保留reserved
說明如下:
- 重置向量 (Reset Interrupt Vector) :重置向量是處理器開電後執行的第一道指令的位址,這條指令將使處理器跳躍到初始化程式碼處。
- 未定義指令向量(Undefined Interrupt Vector):未定義指令向量是在處理器不能對第一道指令解碼時使用。通常當我們在開發新的DSP指令時,可以使用這個部份來進行模擬測試
- 軟體中斷向量(Software Interrupt Vector:Trap):軟體中斷向量是執行 SWI 指令時被呼叫的。 SWI 指令經常用於呼叫一個作業系統常式的機制(System Call)。
- 預先存取中止向量(Prefetch Abort Interrupt Vector):發生在試圖從一個未獲得正確存取許可權的位址去存取時,而實際上中止發生在解碼階段。
- 資料中止向量(Data Abort Interrupt Vector):和預先存取中止向量類似,發生在一道指令試圖存取未獲得正確存取許可權的資料記憶體時。
- 中斷請求向量(Interrupt Request Vector):用於外部硬體中斷處理器的正常執行流,只有當 CPSR 中的 IRQ 位元未被遮罩時才發生。
- 快速中斷請求向量(Fast Interrupt RequestVector):和中斷請求向量累類似,是為了要求更快速的中斷反應時間,有當 CPSR 中的 FIQ 位元未被遮罩時才發生。
微處理器進入中斷時(硬體本身的中斷處理動作):
(1)自動將原程式中下一個準備要執行的指令位址儲存至R14(Link Register)
(2)將CPSR複製到適當的SPSR
(3)依中斷的要求設定CPSR mode bits
(4)強迫PC擷取相對應的中斷向量指令。
微處理器從中斷返回時(硬體本身的中斷處理動作):
(1)將Link Register減去相對應之偏移值後寫入PC
(2)將SPSR值搬回CPSR
(3)如果在中斷進入點設定了interrupt disable flags,此時還原。
當系統重置有效後會使微處理器執行(硬體本身的中斷處理動作):
(1)將目前之PC及CPSR覆蓋R14_svc及SPSR_svc
(2)CPSR[4:0]=0b10011(進入supervisor模式)、CPSR[5]=0(在ARM模式下執行)、CPSR[6]=1(disable FIQ)、CPSR[7]=1(disable IRQ)
(3)強迫PC擷取位址0x0之中斷向量指令
(4)回到ARM模式下繼續執行。
所以通常在0x0~0x1F我們會放如下的程式碼:
b HandlerReset
b HandlerUndef
b HandlerSWI
b HandlerPabort
b HandlerDabort
b .;保留沒有使用
b HandlerIRQ
b HandlerFIQ
當相對應的中斷發生時,CPU切換模式,然後fetch IVT裏面的程式,然後跳到你自己定義的位址去執行你要處理的事情。到目前為止處理的步驟我們通常稱之為一級中斷向量表,但一個中斷向量表應該都不夠用,因為像IRQ,通常硬體不可能只會有一個,所以還會有一個中斷控制器(interrupt controller)去管更多的硬體,而這麼多的硬體中斷則由二級中斷向量表去處理,也就是進入HandlerIRQ後,要再去判斷現在是什麼硬體發出中斷的,再跳去該硬體的中斷服務常式去處理。
我們拿個u-boot loader來當範例看一下IRQ的部份
底下是IVT定義,_start當然是0x0開始囉!
_start: b reset
ldr pc,=HandleUndef
ldr pc,=HandleSWI
ldr pc,=HandlePabort
ldr pc,=HandleDabort
b .
ldr pc,=HandleIRQ;你要自己定義HandleIRQ的實際位置在那裏
ldr pc,=HandleFIQ
real_vectors的位置也是自己定義,但要跟上面的HandleXXXX對映起來
real_vectors:
ldr pc,=reset
ldr pc,=undefined_instruction
ldr pc,=software_interrupt
ldr pc,=prefetch_abort
ldr pc,=data_abort
ldr pc,=not_used
ldr pc,=irq
ldr pc,=fiq
這裏的IRQ處理取得IRQ的堆疊指標,備份暫存器,執行真正的IRQ ISR,恢復暫存器值
irq:
get_irq_stack
irq_save_user_regs
bl do_irq
irq_restore_user_regs
底下的部份就是上面irq裏使用的程式碼
.macro irq_save_user_regs
sub sp, sp, #S_FRAME_SIZE
stmia sp, {r0 - r12} @ Calling r0-r12
add r8, sp, #S_PC
stmdb r8, {sp, lr}^ @ Calling SP, LR
str lr, [r8, #0] @ Save calling PC
mrs r6, spsr
str r6, [r8, #4] @ Save CPSR
str r0, [r8, #8] @ Save OLD_R0
mov r0, sp
.endm
.macro irq_restore_user_regs
ldmia sp, {r0 - lr}^ @ Calling r0 - lr
mov r0, r0
ldr lr, [sp, #S_PC] @ Get PC
add sp, sp, #S_FRAME_SIZE
subs pc, lr, #4 @ return & move spsr_svc into cpsr
.endm
.macro get_irq_stack @ setup IRQ stack
ldr sp, =STACK_BOTTOM @IRQ_STACK_START
/*ldr sp, =IRQ_STACK_START @IRQ_STACK_START*/
.endm


2008年5月13日 星期二
uClinux内核移植相關代碼分析
uClinux内核移植相關代碼分析
本文通過整理之前研發的一個項目(ARM7TDMI + uCLinux),分析内核啓動過程及需要修改的文件,以供内核移植者參考。整理過程中也同時參考了衆多網友的帖子,在此謝過。由于整理過程匆忙,難免錯誤及講解的不够清楚之處,請各位網友指正,這裏提前謝過。本文分以下部分進行介紹:
Bootloader及内核解壓
内核啓動方式介紹
内核啓動地址的確定
arch/armnommu/kernel/head-armv.S分析
start_kernel()函數分析
1. Bootloader及内核解壓
Bootloader將内核加載到内存中,設定一些寄存器,然後將控制權交由内核,該過程中,關閉MMU功能。通常,内核都是以壓縮的方式存放,如zImage,這裏有兩種解壓方法:
使用内核自解壓程序。
arch/arm/boot/compressed/head.S或arch/arm/boot/compressed/head-xxxxx.S
arch/arm/boot/compressed/misc.c
在Bootloader中增加解壓功能。
使用該方法時内核不需要帶有自解壓功能,而使用Bootloader中的解壓程序代替内核自解壓程序。其工作過程與内核自解壓過程相似:Bootloader把壓縮方式的内核解壓到内存中,然後跳轉到内核入口處開始執行。
2. 幾種内核啓動方式介紹
XIP (EXECUTE IN PLACE) 是指直接從存放代碼的位置上啓動運行。
2.1 非壓縮,非XIP
非XIP方式是指在運行之前需對代碼進行重定位。該類型的内核以非壓縮方式存放在Flash中,啓動時由Bootloader加載到内存後運行。
2.2 非壓縮,XIP
該類型的内核以非壓縮格式存放在ROM/Flash中,不需要加載到内存就能運行,Bootloader直接跳轉到其存放地址執行。Data段復制和BSS 段清零的工作由内核自己完成。這種啓動方式常用于内存空間有限的系統中,另外,程序在ROM/Flash中運行的速度相對較慢。
2.3 RAM自解壓
壓縮格式的内核由開頭一段自解壓代碼和壓縮内核數據組成,由于以壓縮格式存放,内核只能以非XIP方式運行。RAM自解壓過程如下:壓縮内核存放于 ROM/Flash中,Bootloader啓動後加載到内存中的臨時空間,然後跳轉到壓縮内核入口地址執行自解壓代碼,内核被解壓到最終的目的地址然後運行。壓縮内核所占據的臨時空間隨後被Linux回收利用。這種方式的内核在嵌入式産品中較為常見。
2.4 ROM自解壓
解壓縮代碼也能够以XIP的方式在ROM/Flash中運行。ROM自解壓過程如下:壓縮内核存放在ROM/Flash中,不需要加載到内存就能運行, Bootloader直接跳轉到其存放地址執行其自解壓代碼,將壓縮内核解壓到最終的目的地址并運行。ROM自解壓方式存放的内核解壓縮速度慢,而且也不能節省内存空間。
3. 内核啓動地址的確定
内核自解壓方式
Head.S/head-XXX.S獲得内核解壓後首地址ZREALADDR,然後解壓内核,并把解壓後的内核放在ZREALADDR的位置上,最後跳轉到ZREALADDR地址上,開始真正的内核啓動。
arch/armnommu/boot/Makefile,定義ZRELADDR和 ZTEXTADDR。ZTEXTADDR是自解壓代碼的起始地址,如果從内存啓動内核,設置為0即可,如果從Rom/Flash啓動,則設置 ZTEXTADDR為相應的值。ZRELADDR是内核解壓縮後的執行地址。
arch/armnommu/boot/compressed/vmlinux.ld,引用LOAD_ADDR和TEXT_START。
arch/armnommu/boot/compressed/Makefile, 通過如下一行:
SEDFLAGS = s/TEXT_START/$(ZTEXTADDR)/;s/LOAD_ADDR/$(ZRELADDR)/;
使得TEXT_START = ZTEXTADDR,LOAD_ADDR = ZRELADDR。
説明:
執行完decompress_kernel函數後,代碼跳回head.S/head-XXX.S中,檢查解壓縮之後的kernel起始地址是否緊挨着 kernel image。如果是,beq call_kernel,執行解壓後的kernel。如果解壓縮之後的kernel起始地址不是緊挨着kernel image,則執行relocate,將其拷貝到緊接着kernel image的地方,然後跳轉,執行解壓後的kernel。
Bootloader解壓方式
Bootloader把解壓後的内核放在内存的TEXTADDR位置上,然後跳轉到TEXTADDR位置上,開始内核啓動。
arch/armnommu/Makefile,一般設置TEXTADDR為PAGE_OFF+0x8000,如定義為0x00008000, 0xC0008000等。
arch/armnommu/vmlinux.lds,引用TEXTADDR
4. arch/armnommu/kernel/head-armv.S
該文件是内核最先執行的一個文件,包括内核入口ENTRY(stext)到start_kernel間的初始化代碼,主要作用是檢查CPU ID,Architecture Type,初始化BSS等操作,并跳到start_kernel函數。在執行前,處理器應滿足以下狀態:
r0 - should be 0
r1 - unique architecture number
MMU - off
I-cache - on or off
D-cache – off
/* 部分源代碼分析 */
/* 内核入口點 */
ENTRY(stext)
/* 程序狀態,禁止FIQ、IRQ,設定SVC模式 */
mov r0, #F_BIT | I_BIT | MODE_SVC@ make sure svc mode
/* 置當前程序狀態寄存器 */
msr cpsr_c, r0 @ and all irqs disabled
/* 判斷CPU類型,查找運行的CPU ID值與Linux編譯支持的ID值是否支持 */
bl __lookup_processor_type
/* 跳到__error */
teq r10, #0 @ invalid processor?
moveq r0, #p @ yes, error p
beq __error
/* 判斷體系類型,查看R1寄存器的Architecture Type值是否支持 */
bl __lookup_architecture_type
/* 不支持,跳到出錯 */
teq r7, #0 @ invalid architecture?
moveq r0, #a @ yes, error a
beq __error
/* 創建核心頁表 */
bl __create_page_tables
adr lr, __ret @ return address
add pc, r10, #12 @ initialise processor
/* 跳轉到start_kernel函數 */
b start_kernel
__lookup_processor_type 這個函數根據芯片的ID從proc.info獲取proc_info_list結構,proc_info_list結構定義在include/asm- armnommu/proginfo.h中,該結構的數據定義在arch/armnommu/mm/proc-arm*.S文件中,ARM7TDMI系列芯片的proc_info_list數據定義在arch/armnommu/mm/proc-arm6,7.S文件中。函數 __lookup_architecture_type從arch.info獲取machine_desc結構,machine_desc結構定義在 include/asm-armnommu/mach/arch.h中,針對不同arch的數據定義在arch/armnommu/mach- */arch.c文件中。
在這裏如果知道processor_type和architecture_type,可以直接對相應寄存器進行賦值。
5. start_kernel()函數分析
下面對start_kernel()函數及其相關函數進行分析。
5.1 lock_kernel()
/* Getting the big kernel lock.
* This cannot happen asynchronously,
* so we only need to worry about other
* CPUs.
*/
extern __inline__ void lock_kernel(void)
{
if (!++current->lock_depth)
spin_lock(&kernel_flag);
}
kernel_flag 是一個内核大自旋鎖,所有進程都通過這個大鎖來實現向内核態的遷移。只有獲得這個大自旋鎖的處理器可以進入内核,如中斷處理程序等。在任何一對 lock_kernel/unlock_kernel函數裏至多可以有一個程序占用CPU。進程的lock_depth成員初始化為-1,在kerenl/fork.c文件中設置。在它小于0時(恒為 -1),進程不擁有内核鎖;當大于或等于0時,進程得到内核鎖。
5.2 setup_arch()
setup_arch()函數做體系相關的初始化工作,函數的定義在arch/armnommu/kernel/setup.c文件中,主要涉及下列主要函數及代碼。
5.2.1 setup_processor()
該函數主要通過
for (list = &__proc_info_begin; list < &__proc_info_end ; list++)
if ((processor_id & list->cpu_mask) == list->cpu_val)
break;
這様一個循環來在.proc.info段中尋找匹配的processor_id,processor_id在head_armv.S文件
中設置。
5.2.2 setup_architecture(machine_arch_type)
該函數獲得體系結構的信息,返回mach-xxx/arch.c 文件中定義的machine結構體的指針,包含以下内容:
MACHINE_START (xxx, “xxx”)
MAINTAINER ("xxx")
BOOT_MEM (xxx, xxx, xxx)
FIXUP (xxx)
MAPIO (xxx)
INITIRQ (xxx)
MACHINE_END
5.2.3内存設置代碼
if (meminfo.nr_banks == 0)
{
meminfo.nr_banks = 1;
meminfo.bank[0].start = PHYS_OFFSET;
meminfo.bank[0].size = MEM_SIZE;
}
meminfo 結構表明内存情况,是對物理内存結構meminfo的默認初始化。 nr_banks指定内存塊的數量,bank指定每塊内存的範圍,PHYS _OFFSET指定某塊内存塊的開始地址,MEM_SIZE指定某塊内存塊長度。PHYS _OFFSET和MEM_SIZE都定義在include/asm-armnommu/arch-XXX/memory.h文件中,其中PHYS _OFFSET是内存的開始地址,MEM_SIZE就是内存的結束地址。這個結構在接下來内存的初始化代碼中起重要作用。
5.2.4 内核内存空間管理
init_mm.start_code = (unsigned long) &_text; 内核代碼段開始
init_mm.end_code = (unsigned long) &_etext; 内核代碼段結束
init_mm.end_data = (unsigned long) &_edata; 内核數據段開始
init_mm.brk = (unsigned long) &_end; 内核數據段結束
每一個任務都有一個mm_struct結構管理其内存空間,init_mm 是内核的mm_struct。其中設置成員變量* mmap指向自己, 意味着内核只有一個内存管理結構,設置 pgd=swapper_pg_dir,
swapper_pg_dir是内核的頁目録,ARM體系結構的内核頁目録大小定義為16k。init_mm定義了整個内核的内存空間,内核綫程屬于内核代碼,同様使用内核空間,其訪問内存空間的權限與内核一様。
5.2.5 内存結構初始化
bootmem_init (&meminfo)函數根據meminfo進行内存結構初始化。bootmem_init(&meminfo)函數中調用 reserve_node_zero(bootmap_pfn, bootmap_pages) 函數,這個函數的作用是保留一部分内存使之不能被動態分配。這些内存塊包括:
reserve_bootmem_node(pgdat, __pa(&_stext), &_end - &_stext); /*内核所占用地址空間*/
reserve_bootmem_node(pgdat, bootmap_pfn << PAGE_SHIFT, bootmap_pages << PAGE_SHIFT)
/*bootmem結構所占用地址空間*/
5.2.6 paging_init(&meminfo, mdesc)
創建内核頁表,映射所有物理内存和IO空間,對于不同的處理器,該函數差彆比較大。下面簡單描述一下ARM體系結構的存儲系統及MMU相關的概念。
在ARM 存儲系統中,使用内存管理單元(MMU)實現虚擬地址到實際物理地址的映射。利用MMU,可把SDRAM的地址完全映射到0x0起始的一片連續地址空間,而把原來占據這片空間的FLASH或者ROM映射到其他不相衝突的存儲空間位置。例如,FLASH的地址從0x0000 0000~0x00FFFFFF,而SDRAM的地址範圍是0x3000 0000~0x3lFFFFFF,則可把SDRAM地址映射為0x0000 0000~0xlFFFFFF,而FLASH的地址可以映射到0x9000 0000~0x90FFFFFF(此處地址空間為空閑,未被占用)。映射完成後,如果處理器發生异常,假設依然為IRQ中斷,PC指針指向0xl8處的地址,而這個時候PC實際上是從位于物理地址的0x3000 0018處讀取指令。通過MMU的映射,則可實現程序完全運行在SDRAM之中。在實際的應用中.可能會把兩片不連續的物理地址空間分配給SDRAM。而在操作系統中,習慣于把SDRAM的空間連續起來,方便内存管理,且應用程序申請大塊的内存時,操作系統内核也可方便地分配。通過MMU可實現不連續的物理地址空間映射為連續的虚擬地址空間。操作系統内核或者一些比較關鍵的代碼,一般是不希望被用户應用程序訪問。通過MMU可以控制地址空間的訪問權限,從而保護這些代碼不被破壞。
MMU的實現過程,實際上就是一個查表映射的過程。建立頁表是實現MMU功能不可缺少的一步。頁表位于系統的内存中,頁表的每一項對應于一個虚擬地址到物理地址的映射。每一項的長度即是一個字的長度(在ARM中,一個字的長度被定義為4Bytes)。頁表項除完成虚擬地址到物理地址的映射功能之外,還定義了訪問權限和緩衝特性等。
MMU的映射分為兩種,一級頁表的變换和二級頁表變换。兩者的不同之處就是實現的變换地址空間大小不同。一級頁表變换支持1 M大小的存儲空間的映射,而二級可以支持64 kB,4 kB和1 kB大小地址空間的映射。
動態表(頁表)的大小=表項數*每個表項所需的位數,即為整個内存空間建立索引表時,需要多大空間存放索引表本身。
表項數=虚擬地址空間/每頁大小
每個表項所需的位數=Log(實際頁表數)+適當控制位數
實際頁表數 =物理地址空間/每頁大小
下面分析paging_init()函數的代碼。
在paging_init中分配起始頁(即第0頁)地址:
zero_page = 0xCXXXXXXX
memtable_init(mi); 如果當前微處理器帶有MMU,則為系統内存創建頁表;如果當前微處理器不支持MMU,比如ARM7TDMI上移植uCLinux操作系統時,則不需要此類步驟。可以通過如下一個宏定義實現靈活控制,對于帶有MMU的微處理器而言,memtable_init(mi)是paging_init()中最重要的函數。
#ifndef CONFIG_UCLINUX
/* initialise the page tables. */
memtable_init(mi);
……(此處省略若乾代碼)
free_area_init_node(node, pgdat, 0, zone_size,
bdata->node_boot_start, zhole_size);
}
#else /* 針對不帶MMU微處理器 */
{
/*****************************************************/
定義物理内存區域管理
/*****************************************************/
unsigned long zone_size[MAX_NR_ZONES] = {0,0,0};
zone_size[ZONE_DMA] = 0;
zone_size[ZONE_NORMAL] = (END_MEM - PAGE_OFFSET) >> PAGE_SHIFT;
free_area_init_node(0, NULL, NULL, zone_size, PAGE_OFFSET, NULL);
}
#endif
uCLinux與其它嵌入式Linux最大的區彆就是MMU管理這一塊,從上面代碼就明顯可以看到這點區彆。下面繼續討論針對帶MMU的微處理器的内存管理。
void __init memtable_init(struct meminfo *mi)
{
struct map_desc *init_maps, *p, *q;
unsigned long address = 0;
int i;
init_maps = p = alloc_bootmem_low_pages(PAGE_SIZE);
/*******************************************************/
其中map_desc定義為:
struct map_desc {
unsigned long virtual;
unsigned long physical;
unsigned long length;
int domain:4, // 頁表的domain
prot_read:1, // 讀保護標志
prot_write:1, // 寫保護標志
cacheable:1, // 是否使用cache
bufferable:1, // 是否使用write buffer
last:1; //空
};init_maps /* map_desc是區段及其屬性的定義 */
下面代碼對meminfo的區段進行遍歷,在嵌入式系統中列舉所有可映射的内存,例如32M SDRAM, 4M FLASH等,用meminfo記録這些内存區段。同時填寫init_maps 中的各項内容。meminfo結構如下:
struct meminfo {
int nr_banks;
unsigned long end;
struct {
unsigned long start;
unsigned long size;
int node;
} bank[NR_BANKS];
};
/********************************************************/
for (i = 0; i < mi->nr_banks; i++)
{
if (mi->bank.size == 0)
continue;
p->physical = mi->bank.start;
p->virtual = __phys_to_virt(p->physical);
p->length = mi->bank.size;
p->domain = DOMAIN_KERNEL;
p->prot_read = 0;
p->prot_write = 1;
p->cacheable = 1; //使用Cache
p->bufferable = 1; //使用write buffer
p ++; //下一個區段
}
/* 如果系統存在FLASH,執行以下代碼 */
#ifdef FLUSH_BASE
p->physical = FLUSH_BASE_PHYS;
p->virtual = FLUSH_BASE;
p->length = PGDIR_SIZE;
p->domain = DOMAIN_KERNEL;
p->prot_read = 1;
p->prot_write = 0;
p->cacheable = 1;
p->bufferable = 1;
p ++;
#endif
/***********************************************************/
接下來的代碼是逐個區段建立頁表
/***********************************************************/
q = init_maps;
do {
if (address < q->virtual || q == p) {
/*******************************************************************************/
由于内核空間是從某個地址開始,如0xC0000000,所以0xC000 0000 以前的頁表項全部清空
clear_mapping在mm-armv.c中定義,其中clear_mapping()是個宏,根據處理器的不同,可以被展開為如下代碼
cpu_XXX_set_pmd(((pmd_t *)(((&init_mm )->pgd+ (( virt) >> 20 )))),((pmd_t){( 0 )}));
其中init_mm為内核的mm_struct,pgd指向 swapper_pg_dir,在arch/arm/kernel/init_task.c中定義。cpu_XXX_set_pmd定義在 proc_armXXX.S文件中,參見ENTRY(cpu_XXX_set_pmd) 處代碼。
/*********************************************************************************/
clear_mapping(address);
/* 每個表項增加1M */
address += PGDIR_SIZE;
} else {
/* 構建内存頁表 */
create_mapping(q);
address = q->virtual + q->length;
address = (address + PGDIR_SIZE - 1) & PGDIR_MASK;
q ++;
}
} while (address != 0);
/ * create_mapping函數也在mm-armv.c中定義 */
static void __init create_mapping(struct map_desc *md)
{
unsigned long virt, length;
int prot_sect, prot_pte;
long off;
/*******************************************************************************/
大部分應用中均采用1級section模式的地址映射,一個section的大小為1M,也就是説從邏輯地址到物理地址的轉變是這様的一個過程:
一個32位的地址,高12位决定了該地址在頁表中的index,這個index的内容决定了該邏輯section對應的物理section;低20位决定了該地址在section中的偏移(index)。例如:從0x0~0xFFFFFFFF的地址空間總共可以分成0x1000(4K)個 section(每個section大小為1M),頁表中每項的大小為32個bit,因此頁表的大小為0x4000(16K)。
每個頁表項的内容如下:
bit: 31 20 19 12 11 10 9 8 5 4 3 2 1 0
content: Section對應的物理地址 NULL AP 0 Domain 1 C B 1 0
最低兩位(10)是section分頁的標識。
AP:Access Permission,區分只讀、讀寫、SVC&其它模式。
Domain:每個section都屬于某個Domain,每個Domain的屬性由寄存器控制。一般都只要包含兩個Domain,一個可訪問地址空間; 另一個不可訪問地址空間。
C、B:這兩位决定了該section的cache&write buffer屬性,這與該段的用途(RO or RW)有密切關系。不同的用途要做不同的設置。
C B 具體含義
0 0 無cache,無寫緩衝,任何對memory的讀寫都反映到總綫上。對 memory 的操作過程中CPU需要等待。
0 1 無cache,有寫緩衝,讀操作直接反映到總綫上。寫操作CPU將數據寫入到寫緩衝後繼續運行,由寫緩衝進行寫回操作。
1 0 有cache,寫通模式,讀操作首先考慮cache hit;寫操作時直接將數據寫入寫緩衝,如果同時出現cache hit,那麽也更新cache。
1 1 有cache,寫回模式,讀操作首先考慮cache hit;寫操作也首先考慮cache hit。
由于ARM中section表項的權限位和page表項的位置不同, 以下代碼根據struct map_desc 中的保護標志,分彆計算頁表項中的AP, Domain和CB標志位。
/*******************************************************************************/
prot_pte = L_PTE_PRESENT | L_PTE_YOUNG | L_PTE_DIRTY |
(md->prot_read ? L_PTE_USER : 0) |
(md->prot_write ? L_PTE_WRITE : 0) |
(md->cacheable ? L_PTE_CACHEABLE : 0) |
(md->bufferable ? L_PTE_BUFFERABLE : 0);
prot_sect = PMD_TYPE_SECT | PMD_DOMAIN(md->domain) |
(md->prot_read ? PMD_SECT_AP_READ : 0) |
(md->prot_write ? PMD_SECT_AP_WRITE : 0) |
(md->cacheable ? PMD_SECT_CACHEABLE : 0) |
(md->bufferable ? PMD_SECT_BUFFERABLE : 0);
/********************************************************************/
設置虚擬地址,偏移地址和内存length
/********************************************************************/
virt = md->virtual;
off = md->physical - virt;
length = md->length;
/********************************************************************/
建立虚擬地址到物理地址的映射
/********************************************************************/
while ((virt & 0xfffff || (virt + off) & 0xfffff) && length >= PAGE_SIZE) {
alloc_init_page(virt, virt + off, md->domain, prot_pte);
virt += PAGE_SIZE;
length -= PAGE_SIZE;
}
while (length >= PGDIR_SIZE) {
alloc_init_section(virt, virt + off, prot_sect);
virt += PGDIR_SIZE;
length -= PGDIR_SIZE;
}
while (length >= PAGE_SIZE) {
alloc_init_page(virt, virt + off, md->domain, prot_pte);
virt += PAGE_SIZE;
length -= PAGE_SIZE;
}
/*************************************************************************/
create_mapping的作用是設置虚地址virt 到物理地址virt + off_set的映射頁目録和頁表。
/*************************************************************************/
/* 映射中斷向量表區域 */
init_maps->physical = virt_to_phys(init_maps);
init_maps->virtual = vectors_base();
init_maps->length = PAGE_SIZE;
init_maps->domain = DOMAIN_USER;
init_maps->prot_read = 0;
init_maps->prot_write = 0;
init_maps->cacheable = 1;
init_maps->bufferable = 0;
create_mapping(init_maps);
中斷向量表的虚地址init_maps,是用alloc_bootmem_low_pages分配的,通常是在PAGE_OFF+0x8000前面的某一頁, vectors_base()是個宏,ARM規定中斷向量表的地址只能是0或0xFFFF0000,所以上述代碼映射一頁到0或0xFFFF0000,中斷處理程序中的部分代碼也被拷貝到這一頁中。
5.3 parse_options()
分析由内核引導程序發送給内核的啓動選項,在初始化過程中按照某些選項運行,并將剩餘部分傳送給init進程。這些選項可能已經存儲在配置文件中,也可能是由用户在系統啓動時敲入的。但内核并不關心這些,這些細節都是内核引導程序關注的内容,嵌入式系統更是如此。
5.4 trap_init()
這個函數用來做體系相關的中斷處理的初始化,在該函數中調用__trap_init((void *)vectors_base())函數將exception vector設置到vectors_base開始的地址上。__trap_init函數位于entry-armv.S文件中,對于ARM處理器,共有復位、未定義指令、SWI、預取終止、數據終止、IRQ和FIQ幾種方式。SWI主要用來實現系統調用,而産生了IRQ之後,通過exception vector進入中斷處理過程,執行do_IRQ函數。
armnommu的trap_init()函數在 arch/armnommu/kernel/traps.c文件中。vectors_base是寫中斷向量的開始地址,在include/asm- armnommu/proc-armv/system.h文件中設置,地址為0或0XFFFF0000。
ENTRY(__trap_init)
stmfd sp!, {r4 - r6, lr}
mrs r1, cpsr @ code from 2.0.38
bic r1, r1, #MODE_MASK @ clear mode bits /* 設置svc模式,disable IRQ,FIQ */
orr r1, r1, #I_BIT|F_BIT|MODE_SVC @ set SVC mode, disable IRQ,FIQ
msr cpsr, r1
adr r1, .LCvectors @ set up the vectors
ldmia r1, {r1, r2, r3, r4, r5, r6, ip, lr}
stmia r0, {r1, r2, r3, r4, r5, r6, ip, lr} /* 拷貝异常向量 */
add r2, r0, #0x200
adr r0, __stubs_start @ copy stubs to 0x200
adr r1, __stubs_end
1: ldr r3, [r0], #4
str r3, [r2], #4
cmp r0, r1
blt 1b
LOADREGS(fd, sp!, {r4 - r6, pc})
__stubs_start到__stubs_end的地址中包含了异常處理的代碼,因此拷貝到vectors_base+0x200的位置上。
5.5 init_IRQ()
void __init init_IRQ(void)
{
extern void init_dma(void);
int irq;
for (irq = 0; irq < NR_IRQS; irq++) {
irq_desc[irq].probe_ok = 0;
irq_desc[irq].valid = 0;
irq_desc[irq].noautoenable = 0;
irq_desc[irq].mask_ack = dummy_mask_unmask_irq;
irq_desc[irq].mask = dummy_mask_unmask_irq;
irq_desc[irq].unmask = dummy_mask_unmask_irq;
}
CSR_WRITE(AIC_MDCR, 0x7FFFE); /* disable all interrupts */
CSR_WRITE(CAHCNF,0x0);/*Close Cache*/
CSR_WRITE(CAHCON,0x87);/*Flush Cache*/
while(CSR_READ(CAHCON)!=0);
CSR_WRITE(CAHCNF,0x7);/*Open Cache*/
init_arch_irq();
init_dma();
}
這個函數用來做體系相關的irq處理的初始化,irq_desc數組是用來描述IRQ的請求隊列,每一個中斷號分配一個irq_desc結構,組成了一個數組。NR_IRQS代表中斷數目,這裏只是對中斷結構irq_desc進行了初始化。在默認的初始化完成後調用初始化函數init_arch_irq,先執行arch/armnommu/kernel/irq-arch.c文件中的函數genarch_init_irq(),然後就執行 include/asm-armnommu/arch-xxxx/irq.h中的inline函數irq_init_irq,在這裏對irq_desc進行了實質的初始化。其中mask用阻塞中斷;unmask用來取消阻塞;mask_ack的作用是阻塞中斷,同時還回應ack給硬件表示這個中斷已經被處理了,否則硬件將再次發生同一個中斷。這裏,不是所有硬件需要這個ack回應,所以很多時候mask_ack與mask用的是同一個函數。
接下來執行init_dma()函數,如果不支持DMA,可以設置include/asm-armnommu/arch-xxxx/dma.h中的 MAX_DMA_CHANNELS為0,這様在arch/armnommu/kernel/dma.c文件中會根據這個定義使用不同的函數。
5.6 sched_init()
初始化系統調度進程,主要對定時器機制和時鐘中斷的Bottom Half的初始化函數進行設置。與時間相關的初始化過程主要有兩步:(1)調用init_timervecs()函數初始化内核定時器機制;(2)調用 init_bh()函數將BH向量TIMER_BH、TQUEUE_BH和IMMEDIATE_BH所對應的BH函數分彆設置成timer_bh()、 tqueue_bh()和immediate_bh()函數
5.7 softirq_init()
内核的軟中斷機制初始化函數。調用tasklet_init初始化tasklet_struct結構,軟中斷的個數為32個。用于bh的tasklet_struct結構調用 tasklet_init()以後,它們的函數指針func全都指向bh_action()。bh_action就是tasklet實現bh機制的代碼,但此時具體的bh函數還没有指定。
HI_SOFTIRQ用于實現bottom half,TASKLET_SOFTIRQ用于公共的tasklet。
open_softirq(TASKLET_SOFTIRQ, tasklet_action, NULL); /* 初始化公共的tasklet_struct要用到的軟中斷 */
open_softirq(HI_SOFTIRQ, tasklet_hi_action, NULL); /* 初始化tasklet_struct實現的bottom half調用 */
這裏順便講一下軟中斷的執行函數do_softirq()。
軟中斷服務不允許在一個硬中斷服務程序内部執行,也不允許在一個軟中斷服務程序内部執行,所以通過in_interrupt()加以檢查。h-> action 就是串行化執行軟中斷,當bh 的tasklet_struct 鏈入的時候,就能在這裏執行,在bh裏重新鎖定了所有CPU,導致一個時間只有一個CPU可以執行bh 函數,但是do_softirq()是可以在多CPU 上同時執行的。而每個tasklet_struct在一個時間上是不會出現在兩個CPU上的。另外,只有當Linux初始化完成開啓中斷後,中斷系統才可以開始工作。
5.8 time_init()
這個函數用來做體系相關的timer的初始化,armnommu的在 arch/armnommu/kernel/time.c。這裏調用了在include/asm-armnommu/arch-xxxx/time.h中的inline函數setup_timer,setup_timer()函數的設計與硬件設計緊密相關,主要是根據硬件設計情况設置時鐘中斷號和時鐘頻率等。
void __inline__ setup_timer (void)
{
/*----- disable timer -----*/
CSR_WRITE(TCR0, xxx);
CSR_WRITE (AIC_SCR7, xxx); /* setting priority level to high */
/* timer 0: 100 ticks/sec */
CSR_WRITE(TICR0, xxx);
timer_irq.handler = xxxxxx_timer_interrupt;
setup_arm_irq(IRQ_TIMER, &timer_irq); /* IRQ_TIMER is the interrupt number */
INT_ENABLE(IRQ_TIMER);
/* Clear interrupt flag */
CSR_WRITE(TISR, xxx);
/* enable timer */
CSR_WRITE(TCR0, xxx);
}
5.9 console_init()
控制台初始化。控制台也是一種驅動程序,由于其特殊性,提前到該處完成初始化,主要是為了提前看到輸出信息,據此判斷内核運行情况。很多嵌入式Linux操作系統由于没有在/dev目録下正確配置console設備,造成啓動時發生諸如unable to open an initial console的錯誤。
/*******************************************************************************/
init_modules()函數到smp_init()函數之間的代碼一般不需要作修改,
如果平台具有特殊性,也只需對相關函數進行必要修改。
這裏簡單注明了一下各個函數的功能,以便了解。
/*******************************************************************************/
5.10 init_modules()
模塊初始化。如果編譯内核時使能該選項,則内核支持模塊化加載/卸載功能
5.11 kmem_cache_init()
内核Cache初始化。
5.12 sti()
使能中斷,這裏開始,中斷系統開始正常工作。
5.13 calibrate_delay()
近似計算BogoMIPS數字的内核函數。作為第一次估算,calibrate_delay計算出在每一秒内執行多少次__delay循環,也就是每個定時器滴答(timer tick)―百分之一秒内延時循環可以執行多少次。這種計算只是一種估算,結果并不能精確到納秒,但這個數字供内核使用已經足够精確了。
BogoMIPS的數字由内核計算并在系統初始化的時候打印。它近似的給出了每秒鐘CPU可以執行一個短延遲循環的次數。在内核中,這個結果主要用于需要等待非常短周期的設備驅動程序――例如,等待幾微秒并查看設備的某些信息是否已經可用。
計算一個定時器滴答内可以執行多少次循環需要在滴答開始時就開始計數,或者應該盡可能與它接近。全局變量jiffies中存儲了從内核開始保持跟踪時間開始到現在已經經過的定時器滴答數, jiffies保持异步更新,在一個中斷内——每秒一百次,内核暫時挂起正在處理的内容,更新變量,然後繼續剛才的工作。
5.14 mem_init()
内存初始化。本函數通過内存碎片的重組等方法標記當前剩餘内存, 設置内存上下界和頁表項初始值。
5.15 kmem_cache_sizes_init()
内核内存管理器的初始化,也就是初始化cache和SLAB分配機制。
5.16 pgtable_cache_init()
頁表cache初始化。
5.17 fork_init()
這裏根據硬件的内存情况,如果計算出的max_threads數量太大,可以自行定義。
5.18 proc_caches_init();
為proc文件系統創建高速緩衝
5.19 vfs_caches_init(num_physpages);
為VFS創建SLAB高速緩衝
5.20 buffer_init(num_physpages);
初始化buffer
5.21 page_cache_init(num_physpages);
頁緩衝初始化
5.22 signals_init();
創建信號隊列高速緩衝
5.23 proc_root_init();
在内存中創建包括根結點在内的所有節點
5.24 check_bugs();
檢查與處理器相關的bug
5.25 smp_init();
5.26 rest_init();
此函數調用kernel_thread(init, NULL, CLONE_FS | CLONE_FILES | CLONE_SIGNAL)函數。
5.26.1 kernel_thread()函數分析
這裏調用了arch/armnommu/kernel/process.c中的函數kernel_thread,kernel_thread函數中通過 __syscall(clone) 創建新綫程。__syscall(clone)函數參見armnommu/kernel目録下的entry-common.S文件。
5.26.2 init()完成下列功能:
Init()函數通過kernel_thread(init, NULL, CLONE_FS | CLONE_FILES | CLONE_SIGNAL)的回調函數執行,完成下列功能。
do_basic_setup()
在該函數裏,sock_init()函數進行網絡相關的初始化,占用相當多的内存,如果所開發系統不支持網絡功能,可以把該函數的執行注釋掉。
do_initcalls()實現驅動的初始化, 這裏需要與vmlinux.lds聯系起來看才能明白其中奥妙。
static void __init do_initcalls(void)
{
initcall_t *call;
call = &__initcall_start;
do {
(*call)();
call++;
} while (call < &__initcall_end);
/* Make sure there is no pending stuff from the initcall sequence */
flush_scheduled_tasks();
}
查看 /arch/i386/vmlinux.lds,其中有一段代碼
__initcall_start = .;
.initcall.init : { *(.initcall.init) }
__initcall_end = .;
其含義是__initcall_start指向代碼節.initcall.init的節首,而__initcall_end指向.initcall.init的節尾。
do_initcalls所作的是系統中有關驅動部分的初始化工作,那麽這些函數指針數據是怎様放到了.initcall.init節呢?在include/linux/init.h文件中有如下3個定義:
1. #define __init_call __attribute__ ((unused,__section__ (".initcall.init")))
__attribute__的含義就是構建一個在.initcall.init節的指向初始函數的指針。
2. #define __initcall(fn) static initcall_t __initcall_##fn __init_call = fn
##意思就是在可變參數使用宏定義的時候構建一個變量名稱為所指向的函數的名稱,并且在前面加上__initcall_
3. #define module_init(x) __initcall(x);
很多驅動中都有類似module_init(usb_init)的代碼,通過該宏定義逐層解釋存放到.initcall.int節中。
blkmem相關的修改(do_initcalls()初始化驅動時執行此代碼)
在blkmem_init ()函數中,調用了blk_init_queue()函數,blk_init_queue()函數調用了blk_init_free_list()函數, blk_init_free_list()函數又調用了blk_grow_request_list()函數,在這個函數中會 kmem_cache_alloc出nr_requests個request結構體。
這裏如果nr_requests的值太大,則將占用過多的内存,將造成硬件内存不够,因此可以根據實際情况將其替换成了較小的值,比如32、16等。
free_initmem
這個函數在arch/armnommu/mm/init.c文件中,其作用就是對init節的釋放,也可以通過修改代碼指定為不釋放。
5.26.3 init執行過程
在内核引導結束并啓動init之後,系統就轉入用户態的運行,在這之後創建的一切進程,都是在用户態進行。這裏先要清楚一個概念:就是init進程雖然是從内核開始的,即在前面所講的init/main.c中的init()函數在啓動後就已經是一個核心綫程,但在轉到執行init程序(如 /sbin/init)之後,内核中的init()就變成了/sbin/init程序,狀態也轉變成了用户態,也就是説核心綫程變成了一個普通的進程。這様一來,内核中的init函數實際上只是用户態init進程的入口,它在執行execve("/sbin/init",argv_init, envp_init)時改變成為一個普通的用户進程。這也就是exec函數的乾坤大挪移法,在exec函數調用其他程序時,當前進程被其他進程“靈魂附體”。
除此之外,它們的代碼來源也有差彆,内核中的init()函數的源代碼在/init/main.c中,是内核的一部分。而/sbin/init程序的源代碼是應用程序。
init程序啓動之後,要完成以下任務:檢查文件系統,啓動各種後台服務進程,最後為每個終端和虚擬控制台啓動一個getty進程供用户登録。由于所有其它用户進程都是由init派生的,因此它又是其它一切用户進程的父進程。
init進程啓動後,按照/etc/inittab的内容進程系統設置。很多嵌入式系統用的是BusyBox的init,它與一般所使用的init不一様,會先執行/etc/init.d/rcS而非/etc/rc.d/rc.sysinit。
小結:
本想多整理一些相關資料,無奈又要開始新項目的奔波,start_kernel()函數也剛好差不多講完了,分析的不是很深入,希望對嵌入式Linux移植的網友們有一些幫助。最後列舉下面幾處未整理的知識點,有興趣的網友可作進一步探討。
text.init和data.init説明
__init 標示符在gcc編譯器中指定將該函數置于内核的特定區域。在内核完成自身初始化之後,就試圖釋放這個特定區域。實際上,内核中存在兩個這様的區域,. text.init和.data.init――第一個是代碼初始化使用的,另外一個是數據初始化使用的。另外也可以看到__initfunc和 __initdata標志,前者和__init類似,標志初始化專用代碼,後者則標志初始化專用數據。
System.map内核符號表
irq的處理過程
Linux内核調度過程
本文通過整理之前研發的一個項目(ARM7TDMI + uCLinux),分析内核啓動過程及需要修改的文件,以供内核移植者參考。整理過程中也同時參考了衆多網友的帖子,在此謝過。由于整理過程匆忙,難免錯誤及講解的不够清楚之處,請各位網友指正,這裏提前謝過。本文分以下部分進行介紹:
Bootloader及内核解壓
内核啓動方式介紹
内核啓動地址的確定
arch/armnommu/kernel/head-armv.S分析
start_kernel()函數分析
1. Bootloader及内核解壓
Bootloader將内核加載到内存中,設定一些寄存器,然後將控制權交由内核,該過程中,關閉MMU功能。通常,内核都是以壓縮的方式存放,如zImage,這裏有兩種解壓方法:
使用内核自解壓程序。
arch/arm/boot/compressed/head.S或arch/arm/boot/compressed/head-xxxxx.S
arch/arm/boot/compressed/misc.c
在Bootloader中增加解壓功能。
使用該方法時内核不需要帶有自解壓功能,而使用Bootloader中的解壓程序代替内核自解壓程序。其工作過程與内核自解壓過程相似:Bootloader把壓縮方式的内核解壓到内存中,然後跳轉到内核入口處開始執行。
2. 幾種内核啓動方式介紹
XIP (EXECUTE IN PLACE) 是指直接從存放代碼的位置上啓動運行。
2.1 非壓縮,非XIP
非XIP方式是指在運行之前需對代碼進行重定位。該類型的内核以非壓縮方式存放在Flash中,啓動時由Bootloader加載到内存後運行。
2.2 非壓縮,XIP
該類型的内核以非壓縮格式存放在ROM/Flash中,不需要加載到内存就能運行,Bootloader直接跳轉到其存放地址執行。Data段復制和BSS 段清零的工作由内核自己完成。這種啓動方式常用于内存空間有限的系統中,另外,程序在ROM/Flash中運行的速度相對較慢。
2.3 RAM自解壓
壓縮格式的内核由開頭一段自解壓代碼和壓縮内核數據組成,由于以壓縮格式存放,内核只能以非XIP方式運行。RAM自解壓過程如下:壓縮内核存放于 ROM/Flash中,Bootloader啓動後加載到内存中的臨時空間,然後跳轉到壓縮内核入口地址執行自解壓代碼,内核被解壓到最終的目的地址然後運行。壓縮内核所占據的臨時空間隨後被Linux回收利用。這種方式的内核在嵌入式産品中較為常見。
2.4 ROM自解壓
解壓縮代碼也能够以XIP的方式在ROM/Flash中運行。ROM自解壓過程如下:壓縮内核存放在ROM/Flash中,不需要加載到内存就能運行, Bootloader直接跳轉到其存放地址執行其自解壓代碼,將壓縮内核解壓到最終的目的地址并運行。ROM自解壓方式存放的内核解壓縮速度慢,而且也不能節省内存空間。
3. 内核啓動地址的確定
内核自解壓方式
Head.S/head-XXX.S獲得内核解壓後首地址ZREALADDR,然後解壓内核,并把解壓後的内核放在ZREALADDR的位置上,最後跳轉到ZREALADDR地址上,開始真正的内核啓動。
arch/armnommu/boot/Makefile,定義ZRELADDR和 ZTEXTADDR。ZTEXTADDR是自解壓代碼的起始地址,如果從内存啓動内核,設置為0即可,如果從Rom/Flash啓動,則設置 ZTEXTADDR為相應的值。ZRELADDR是内核解壓縮後的執行地址。
arch/armnommu/boot/compressed/vmlinux.ld,引用LOAD_ADDR和TEXT_START。
arch/armnommu/boot/compressed/Makefile, 通過如下一行:
SEDFLAGS = s/TEXT_START/$(ZTEXTADDR)/;s/LOAD_ADDR/$(ZRELADDR)/;
使得TEXT_START = ZTEXTADDR,LOAD_ADDR = ZRELADDR。
説明:
執行完decompress_kernel函數後,代碼跳回head.S/head-XXX.S中,檢查解壓縮之後的kernel起始地址是否緊挨着 kernel image。如果是,beq call_kernel,執行解壓後的kernel。如果解壓縮之後的kernel起始地址不是緊挨着kernel image,則執行relocate,將其拷貝到緊接着kernel image的地方,然後跳轉,執行解壓後的kernel。
Bootloader解壓方式
Bootloader把解壓後的内核放在内存的TEXTADDR位置上,然後跳轉到TEXTADDR位置上,開始内核啓動。
arch/armnommu/Makefile,一般設置TEXTADDR為PAGE_OFF+0x8000,如定義為0x00008000, 0xC0008000等。
arch/armnommu/vmlinux.lds,引用TEXTADDR
4. arch/armnommu/kernel/head-armv.S
該文件是内核最先執行的一個文件,包括内核入口ENTRY(stext)到start_kernel間的初始化代碼,主要作用是檢查CPU ID,Architecture Type,初始化BSS等操作,并跳到start_kernel函數。在執行前,處理器應滿足以下狀態:
r0 - should be 0
r1 - unique architecture number
MMU - off
I-cache - on or off
D-cache – off
/* 部分源代碼分析 */
/* 内核入口點 */
ENTRY(stext)
/* 程序狀態,禁止FIQ、IRQ,設定SVC模式 */
mov r0, #F_BIT | I_BIT | MODE_SVC@ make sure svc mode
/* 置當前程序狀態寄存器 */
msr cpsr_c, r0 @ and all irqs disabled
/* 判斷CPU類型,查找運行的CPU ID值與Linux編譯支持的ID值是否支持 */
bl __lookup_processor_type
/* 跳到__error */
teq r10, #0 @ invalid processor?
moveq r0, #p @ yes, error p
beq __error
/* 判斷體系類型,查看R1寄存器的Architecture Type值是否支持 */
bl __lookup_architecture_type
/* 不支持,跳到出錯 */
teq r7, #0 @ invalid architecture?
moveq r0, #a @ yes, error a
beq __error
/* 創建核心頁表 */
bl __create_page_tables
adr lr, __ret @ return address
add pc, r10, #12 @ initialise processor
/* 跳轉到start_kernel函數 */
b start_kernel
__lookup_processor_type 這個函數根據芯片的ID從proc.info獲取proc_info_list結構,proc_info_list結構定義在include/asm- armnommu/proginfo.h中,該結構的數據定義在arch/armnommu/mm/proc-arm*.S文件中,ARM7TDMI系列芯片的proc_info_list數據定義在arch/armnommu/mm/proc-arm6,7.S文件中。函數 __lookup_architecture_type從arch.info獲取machine_desc結構,machine_desc結構定義在 include/asm-armnommu/mach/arch.h中,針對不同arch的數據定義在arch/armnommu/mach- */arch.c文件中。
在這裏如果知道processor_type和architecture_type,可以直接對相應寄存器進行賦值。
5. start_kernel()函數分析
下面對start_kernel()函數及其相關函數進行分析。
5.1 lock_kernel()
/* Getting the big kernel lock.
* This cannot happen asynchronously,
* so we only need to worry about other
* CPUs.
*/
extern __inline__ void lock_kernel(void)
{
if (!++current->lock_depth)
spin_lock(&kernel_flag);
}
kernel_flag 是一個内核大自旋鎖,所有進程都通過這個大鎖來實現向内核態的遷移。只有獲得這個大自旋鎖的處理器可以進入内核,如中斷處理程序等。在任何一對 lock_kernel/unlock_kernel函數裏至多可以有一個程序占用CPU。進程的lock_depth成員初始化為-1,在kerenl/fork.c文件中設置。在它小于0時(恒為 -1),進程不擁有内核鎖;當大于或等于0時,進程得到内核鎖。
5.2 setup_arch()
setup_arch()函數做體系相關的初始化工作,函數的定義在arch/armnommu/kernel/setup.c文件中,主要涉及下列主要函數及代碼。
5.2.1 setup_processor()
該函數主要通過
for (list = &__proc_info_begin; list < &__proc_info_end ; list++)
if ((processor_id & list->cpu_mask) == list->cpu_val)
break;
這様一個循環來在.proc.info段中尋找匹配的processor_id,processor_id在head_armv.S文件
中設置。
5.2.2 setup_architecture(machine_arch_type)
該函數獲得體系結構的信息,返回mach-xxx/arch.c 文件中定義的machine結構體的指針,包含以下内容:
MACHINE_START (xxx, “xxx”)
MAINTAINER ("xxx")
BOOT_MEM (xxx, xxx, xxx)
FIXUP (xxx)
MAPIO (xxx)
INITIRQ (xxx)
MACHINE_END
5.2.3内存設置代碼
if (meminfo.nr_banks == 0)
{
meminfo.nr_banks = 1;
meminfo.bank[0].start = PHYS_OFFSET;
meminfo.bank[0].size = MEM_SIZE;
}
meminfo 結構表明内存情况,是對物理内存結構meminfo的默認初始化。 nr_banks指定内存塊的數量,bank指定每塊内存的範圍,PHYS _OFFSET指定某塊内存塊的開始地址,MEM_SIZE指定某塊内存塊長度。PHYS _OFFSET和MEM_SIZE都定義在include/asm-armnommu/arch-XXX/memory.h文件中,其中PHYS _OFFSET是内存的開始地址,MEM_SIZE就是内存的結束地址。這個結構在接下來内存的初始化代碼中起重要作用。
5.2.4 内核内存空間管理
init_mm.start_code = (unsigned long) &_text; 内核代碼段開始
init_mm.end_code = (unsigned long) &_etext; 内核代碼段結束
init_mm.end_data = (unsigned long) &_edata; 内核數據段開始
init_mm.brk = (unsigned long) &_end; 内核數據段結束
每一個任務都有一個mm_struct結構管理其内存空間,init_mm 是内核的mm_struct。其中設置成員變量* mmap指向自己, 意味着内核只有一個内存管理結構,設置 pgd=swapper_pg_dir,
swapper_pg_dir是内核的頁目録,ARM體系結構的内核頁目録大小定義為16k。init_mm定義了整個内核的内存空間,内核綫程屬于内核代碼,同様使用内核空間,其訪問内存空間的權限與内核一様。
5.2.5 内存結構初始化
bootmem_init (&meminfo)函數根據meminfo進行内存結構初始化。bootmem_init(&meminfo)函數中調用 reserve_node_zero(bootmap_pfn, bootmap_pages) 函數,這個函數的作用是保留一部分内存使之不能被動態分配。這些内存塊包括:
reserve_bootmem_node(pgdat, __pa(&_stext), &_end - &_stext); /*内核所占用地址空間*/
reserve_bootmem_node(pgdat, bootmap_pfn << PAGE_SHIFT, bootmap_pages << PAGE_SHIFT)
/*bootmem結構所占用地址空間*/
5.2.6 paging_init(&meminfo, mdesc)
創建内核頁表,映射所有物理内存和IO空間,對于不同的處理器,該函數差彆比較大。下面簡單描述一下ARM體系結構的存儲系統及MMU相關的概念。
在ARM 存儲系統中,使用内存管理單元(MMU)實現虚擬地址到實際物理地址的映射。利用MMU,可把SDRAM的地址完全映射到0x0起始的一片連續地址空間,而把原來占據這片空間的FLASH或者ROM映射到其他不相衝突的存儲空間位置。例如,FLASH的地址從0x0000 0000~0x00FFFFFF,而SDRAM的地址範圍是0x3000 0000~0x3lFFFFFF,則可把SDRAM地址映射為0x0000 0000~0xlFFFFFF,而FLASH的地址可以映射到0x9000 0000~0x90FFFFFF(此處地址空間為空閑,未被占用)。映射完成後,如果處理器發生异常,假設依然為IRQ中斷,PC指針指向0xl8處的地址,而這個時候PC實際上是從位于物理地址的0x3000 0018處讀取指令。通過MMU的映射,則可實現程序完全運行在SDRAM之中。在實際的應用中.可能會把兩片不連續的物理地址空間分配給SDRAM。而在操作系統中,習慣于把SDRAM的空間連續起來,方便内存管理,且應用程序申請大塊的内存時,操作系統内核也可方便地分配。通過MMU可實現不連續的物理地址空間映射為連續的虚擬地址空間。操作系統内核或者一些比較關鍵的代碼,一般是不希望被用户應用程序訪問。通過MMU可以控制地址空間的訪問權限,從而保護這些代碼不被破壞。
MMU的實現過程,實際上就是一個查表映射的過程。建立頁表是實現MMU功能不可缺少的一步。頁表位于系統的内存中,頁表的每一項對應于一個虚擬地址到物理地址的映射。每一項的長度即是一個字的長度(在ARM中,一個字的長度被定義為4Bytes)。頁表項除完成虚擬地址到物理地址的映射功能之外,還定義了訪問權限和緩衝特性等。
MMU的映射分為兩種,一級頁表的變换和二級頁表變换。兩者的不同之處就是實現的變换地址空間大小不同。一級頁表變换支持1 M大小的存儲空間的映射,而二級可以支持64 kB,4 kB和1 kB大小地址空間的映射。
動態表(頁表)的大小=表項數*每個表項所需的位數,即為整個内存空間建立索引表時,需要多大空間存放索引表本身。
表項數=虚擬地址空間/每頁大小
每個表項所需的位數=Log(實際頁表數)+適當控制位數
實際頁表數 =物理地址空間/每頁大小
下面分析paging_init()函數的代碼。
在paging_init中分配起始頁(即第0頁)地址:
zero_page = 0xCXXXXXXX
memtable_init(mi); 如果當前微處理器帶有MMU,則為系統内存創建頁表;如果當前微處理器不支持MMU,比如ARM7TDMI上移植uCLinux操作系統時,則不需要此類步驟。可以通過如下一個宏定義實現靈活控制,對于帶有MMU的微處理器而言,memtable_init(mi)是paging_init()中最重要的函數。
#ifndef CONFIG_UCLINUX
/* initialise the page tables. */
memtable_init(mi);
……(此處省略若乾代碼)
free_area_init_node(node, pgdat, 0, zone_size,
bdata->node_boot_start, zhole_size);
}
#else /* 針對不帶MMU微處理器 */
{
/*****************************************************/
定義物理内存區域管理
/*****************************************************/
unsigned long zone_size[MAX_NR_ZONES] = {0,0,0};
zone_size[ZONE_DMA] = 0;
zone_size[ZONE_NORMAL] = (END_MEM - PAGE_OFFSET) >> PAGE_SHIFT;
free_area_init_node(0, NULL, NULL, zone_size, PAGE_OFFSET, NULL);
}
#endif
uCLinux與其它嵌入式Linux最大的區彆就是MMU管理這一塊,從上面代碼就明顯可以看到這點區彆。下面繼續討論針對帶MMU的微處理器的内存管理。
void __init memtable_init(struct meminfo *mi)
{
struct map_desc *init_maps, *p, *q;
unsigned long address = 0;
int i;
init_maps = p = alloc_bootmem_low_pages(PAGE_SIZE);
/*******************************************************/
其中map_desc定義為:
struct map_desc {
unsigned long virtual;
unsigned long physical;
unsigned long length;
int domain:4, // 頁表的domain
prot_read:1, // 讀保護標志
prot_write:1, // 寫保護標志
cacheable:1, // 是否使用cache
bufferable:1, // 是否使用write buffer
last:1; //空
};init_maps /* map_desc是區段及其屬性的定義 */
下面代碼對meminfo的區段進行遍歷,在嵌入式系統中列舉所有可映射的内存,例如32M SDRAM, 4M FLASH等,用meminfo記録這些内存區段。同時填寫init_maps 中的各項内容。meminfo結構如下:
struct meminfo {
int nr_banks;
unsigned long end;
struct {
unsigned long start;
unsigned long size;
int node;
} bank[NR_BANKS];
};
/********************************************************/
for (i = 0; i < mi->nr_banks; i++)
{
if (mi->bank.size == 0)
continue;
p->physical = mi->bank.start;
p->virtual = __phys_to_virt(p->physical);
p->length = mi->bank.size;
p->domain = DOMAIN_KERNEL;
p->prot_read = 0;
p->prot_write = 1;
p->cacheable = 1; //使用Cache
p->bufferable = 1; //使用write buffer
p ++; //下一個區段
}
/* 如果系統存在FLASH,執行以下代碼 */
#ifdef FLUSH_BASE
p->physical = FLUSH_BASE_PHYS;
p->virtual = FLUSH_BASE;
p->length = PGDIR_SIZE;
p->domain = DOMAIN_KERNEL;
p->prot_read = 1;
p->prot_write = 0;
p->cacheable = 1;
p->bufferable = 1;
p ++;
#endif
/***********************************************************/
接下來的代碼是逐個區段建立頁表
/***********************************************************/
q = init_maps;
do {
if (address < q->virtual || q == p) {
/*******************************************************************************/
由于内核空間是從某個地址開始,如0xC0000000,所以0xC000 0000 以前的頁表項全部清空
clear_mapping在mm-armv.c中定義,其中clear_mapping()是個宏,根據處理器的不同,可以被展開為如下代碼
cpu_XXX_set_pmd(((pmd_t *)(((&init_mm )->pgd+ (( virt) >> 20 )))),((pmd_t){( 0 )}));
其中init_mm為内核的mm_struct,pgd指向 swapper_pg_dir,在arch/arm/kernel/init_task.c中定義。cpu_XXX_set_pmd定義在 proc_armXXX.S文件中,參見ENTRY(cpu_XXX_set_pmd) 處代碼。
/*********************************************************************************/
clear_mapping(address);
/* 每個表項增加1M */
address += PGDIR_SIZE;
} else {
/* 構建内存頁表 */
create_mapping(q);
address = q->virtual + q->length;
address = (address + PGDIR_SIZE - 1) & PGDIR_MASK;
q ++;
}
} while (address != 0);
/ * create_mapping函數也在mm-armv.c中定義 */
static void __init create_mapping(struct map_desc *md)
{
unsigned long virt, length;
int prot_sect, prot_pte;
long off;
/*******************************************************************************/
大部分應用中均采用1級section模式的地址映射,一個section的大小為1M,也就是説從邏輯地址到物理地址的轉變是這様的一個過程:
一個32位的地址,高12位决定了該地址在頁表中的index,這個index的内容决定了該邏輯section對應的物理section;低20位决定了該地址在section中的偏移(index)。例如:從0x0~0xFFFFFFFF的地址空間總共可以分成0x1000(4K)個 section(每個section大小為1M),頁表中每項的大小為32個bit,因此頁表的大小為0x4000(16K)。
每個頁表項的内容如下:
bit: 31 20 19 12 11 10 9 8 5 4 3 2 1 0
content: Section對應的物理地址 NULL AP 0 Domain 1 C B 1 0
最低兩位(10)是section分頁的標識。
AP:Access Permission,區分只讀、讀寫、SVC&其它模式。
Domain:每個section都屬于某個Domain,每個Domain的屬性由寄存器控制。一般都只要包含兩個Domain,一個可訪問地址空間; 另一個不可訪問地址空間。
C、B:這兩位决定了該section的cache&write buffer屬性,這與該段的用途(RO or RW)有密切關系。不同的用途要做不同的設置。
C B 具體含義
0 0 無cache,無寫緩衝,任何對memory的讀寫都反映到總綫上。對 memory 的操作過程中CPU需要等待。
0 1 無cache,有寫緩衝,讀操作直接反映到總綫上。寫操作CPU將數據寫入到寫緩衝後繼續運行,由寫緩衝進行寫回操作。
1 0 有cache,寫通模式,讀操作首先考慮cache hit;寫操作時直接將數據寫入寫緩衝,如果同時出現cache hit,那麽也更新cache。
1 1 有cache,寫回模式,讀操作首先考慮cache hit;寫操作也首先考慮cache hit。
由于ARM中section表項的權限位和page表項的位置不同, 以下代碼根據struct map_desc 中的保護標志,分彆計算頁表項中的AP, Domain和CB標志位。
/*******************************************************************************/
prot_pte = L_PTE_PRESENT | L_PTE_YOUNG | L_PTE_DIRTY |
(md->prot_read ? L_PTE_USER : 0) |
(md->prot_write ? L_PTE_WRITE : 0) |
(md->cacheable ? L_PTE_CACHEABLE : 0) |
(md->bufferable ? L_PTE_BUFFERABLE : 0);
prot_sect = PMD_TYPE_SECT | PMD_DOMAIN(md->domain) |
(md->prot_read ? PMD_SECT_AP_READ : 0) |
(md->prot_write ? PMD_SECT_AP_WRITE : 0) |
(md->cacheable ? PMD_SECT_CACHEABLE : 0) |
(md->bufferable ? PMD_SECT_BUFFERABLE : 0);
/********************************************************************/
設置虚擬地址,偏移地址和内存length
/********************************************************************/
virt = md->virtual;
off = md->physical - virt;
length = md->length;
/********************************************************************/
建立虚擬地址到物理地址的映射
/********************************************************************/
while ((virt & 0xfffff || (virt + off) & 0xfffff) && length >= PAGE_SIZE) {
alloc_init_page(virt, virt + off, md->domain, prot_pte);
virt += PAGE_SIZE;
length -= PAGE_SIZE;
}
while (length >= PGDIR_SIZE) {
alloc_init_section(virt, virt + off, prot_sect);
virt += PGDIR_SIZE;
length -= PGDIR_SIZE;
}
while (length >= PAGE_SIZE) {
alloc_init_page(virt, virt + off, md->domain, prot_pte);
virt += PAGE_SIZE;
length -= PAGE_SIZE;
}
/*************************************************************************/
create_mapping的作用是設置虚地址virt 到物理地址virt + off_set的映射頁目録和頁表。
/*************************************************************************/
/* 映射中斷向量表區域 */
init_maps->physical = virt_to_phys(init_maps);
init_maps->virtual = vectors_base();
init_maps->length = PAGE_SIZE;
init_maps->domain = DOMAIN_USER;
init_maps->prot_read = 0;
init_maps->prot_write = 0;
init_maps->cacheable = 1;
init_maps->bufferable = 0;
create_mapping(init_maps);
中斷向量表的虚地址init_maps,是用alloc_bootmem_low_pages分配的,通常是在PAGE_OFF+0x8000前面的某一頁, vectors_base()是個宏,ARM規定中斷向量表的地址只能是0或0xFFFF0000,所以上述代碼映射一頁到0或0xFFFF0000,中斷處理程序中的部分代碼也被拷貝到這一頁中。
5.3 parse_options()
分析由内核引導程序發送給内核的啓動選項,在初始化過程中按照某些選項運行,并將剩餘部分傳送給init進程。這些選項可能已經存儲在配置文件中,也可能是由用户在系統啓動時敲入的。但内核并不關心這些,這些細節都是内核引導程序關注的内容,嵌入式系統更是如此。
5.4 trap_init()
這個函數用來做體系相關的中斷處理的初始化,在該函數中調用__trap_init((void *)vectors_base())函數將exception vector設置到vectors_base開始的地址上。__trap_init函數位于entry-armv.S文件中,對于ARM處理器,共有復位、未定義指令、SWI、預取終止、數據終止、IRQ和FIQ幾種方式。SWI主要用來實現系統調用,而産生了IRQ之後,通過exception vector進入中斷處理過程,執行do_IRQ函數。
armnommu的trap_init()函數在 arch/armnommu/kernel/traps.c文件中。vectors_base是寫中斷向量的開始地址,在include/asm- armnommu/proc-armv/system.h文件中設置,地址為0或0XFFFF0000。
ENTRY(__trap_init)
stmfd sp!, {r4 - r6, lr}
mrs r1, cpsr @ code from 2.0.38
bic r1, r1, #MODE_MASK @ clear mode bits /* 設置svc模式,disable IRQ,FIQ */
orr r1, r1, #I_BIT|F_BIT|MODE_SVC @ set SVC mode, disable IRQ,FIQ
msr cpsr, r1
adr r1, .LCvectors @ set up the vectors
ldmia r1, {r1, r2, r3, r4, r5, r6, ip, lr}
stmia r0, {r1, r2, r3, r4, r5, r6, ip, lr} /* 拷貝异常向量 */
add r2, r0, #0x200
adr r0, __stubs_start @ copy stubs to 0x200
adr r1, __stubs_end
1: ldr r3, [r0], #4
str r3, [r2], #4
cmp r0, r1
blt 1b
LOADREGS(fd, sp!, {r4 - r6, pc})
__stubs_start到__stubs_end的地址中包含了异常處理的代碼,因此拷貝到vectors_base+0x200的位置上。
5.5 init_IRQ()
void __init init_IRQ(void)
{
extern void init_dma(void);
int irq;
for (irq = 0; irq < NR_IRQS; irq++) {
irq_desc[irq].probe_ok = 0;
irq_desc[irq].valid = 0;
irq_desc[irq].noautoenable = 0;
irq_desc[irq].mask_ack = dummy_mask_unmask_irq;
irq_desc[irq].mask = dummy_mask_unmask_irq;
irq_desc[irq].unmask = dummy_mask_unmask_irq;
}
CSR_WRITE(AIC_MDCR, 0x7FFFE); /* disable all interrupts */
CSR_WRITE(CAHCNF,0x0);/*Close Cache*/
CSR_WRITE(CAHCON,0x87);/*Flush Cache*/
while(CSR_READ(CAHCON)!=0);
CSR_WRITE(CAHCNF,0x7);/*Open Cache*/
init_arch_irq();
init_dma();
}
這個函數用來做體系相關的irq處理的初始化,irq_desc數組是用來描述IRQ的請求隊列,每一個中斷號分配一個irq_desc結構,組成了一個數組。NR_IRQS代表中斷數目,這裏只是對中斷結構irq_desc進行了初始化。在默認的初始化完成後調用初始化函數init_arch_irq,先執行arch/armnommu/kernel/irq-arch.c文件中的函數genarch_init_irq(),然後就執行 include/asm-armnommu/arch-xxxx/irq.h中的inline函數irq_init_irq,在這裏對irq_desc進行了實質的初始化。其中mask用阻塞中斷;unmask用來取消阻塞;mask_ack的作用是阻塞中斷,同時還回應ack給硬件表示這個中斷已經被處理了,否則硬件將再次發生同一個中斷。這裏,不是所有硬件需要這個ack回應,所以很多時候mask_ack與mask用的是同一個函數。
接下來執行init_dma()函數,如果不支持DMA,可以設置include/asm-armnommu/arch-xxxx/dma.h中的 MAX_DMA_CHANNELS為0,這様在arch/armnommu/kernel/dma.c文件中會根據這個定義使用不同的函數。
5.6 sched_init()
初始化系統調度進程,主要對定時器機制和時鐘中斷的Bottom Half的初始化函數進行設置。與時間相關的初始化過程主要有兩步:(1)調用init_timervecs()函數初始化内核定時器機制;(2)調用 init_bh()函數將BH向量TIMER_BH、TQUEUE_BH和IMMEDIATE_BH所對應的BH函數分彆設置成timer_bh()、 tqueue_bh()和immediate_bh()函數
5.7 softirq_init()
内核的軟中斷機制初始化函數。調用tasklet_init初始化tasklet_struct結構,軟中斷的個數為32個。用于bh的tasklet_struct結構調用 tasklet_init()以後,它們的函數指針func全都指向bh_action()。bh_action就是tasklet實現bh機制的代碼,但此時具體的bh函數還没有指定。
HI_SOFTIRQ用于實現bottom half,TASKLET_SOFTIRQ用于公共的tasklet。
open_softirq(TASKLET_SOFTIRQ, tasklet_action, NULL); /* 初始化公共的tasklet_struct要用到的軟中斷 */
open_softirq(HI_SOFTIRQ, tasklet_hi_action, NULL); /* 初始化tasklet_struct實現的bottom half調用 */
這裏順便講一下軟中斷的執行函數do_softirq()。
軟中斷服務不允許在一個硬中斷服務程序内部執行,也不允許在一個軟中斷服務程序内部執行,所以通過in_interrupt()加以檢查。h-> action 就是串行化執行軟中斷,當bh 的tasklet_struct 鏈入的時候,就能在這裏執行,在bh裏重新鎖定了所有CPU,導致一個時間只有一個CPU可以執行bh 函數,但是do_softirq()是可以在多CPU 上同時執行的。而每個tasklet_struct在一個時間上是不會出現在兩個CPU上的。另外,只有當Linux初始化完成開啓中斷後,中斷系統才可以開始工作。
5.8 time_init()
這個函數用來做體系相關的timer的初始化,armnommu的在 arch/armnommu/kernel/time.c。這裏調用了在include/asm-armnommu/arch-xxxx/time.h中的inline函數setup_timer,setup_timer()函數的設計與硬件設計緊密相關,主要是根據硬件設計情况設置時鐘中斷號和時鐘頻率等。
void __inline__ setup_timer (void)
{
/*----- disable timer -----*/
CSR_WRITE(TCR0, xxx);
CSR_WRITE (AIC_SCR7, xxx); /* setting priority level to high */
/* timer 0: 100 ticks/sec */
CSR_WRITE(TICR0, xxx);
timer_irq.handler = xxxxxx_timer_interrupt;
setup_arm_irq(IRQ_TIMER, &timer_irq); /* IRQ_TIMER is the interrupt number */
INT_ENABLE(IRQ_TIMER);
/* Clear interrupt flag */
CSR_WRITE(TISR, xxx);
/* enable timer */
CSR_WRITE(TCR0, xxx);
}
5.9 console_init()
控制台初始化。控制台也是一種驅動程序,由于其特殊性,提前到該處完成初始化,主要是為了提前看到輸出信息,據此判斷内核運行情况。很多嵌入式Linux操作系統由于没有在/dev目録下正確配置console設備,造成啓動時發生諸如unable to open an initial console的錯誤。
/*******************************************************************************/
init_modules()函數到smp_init()函數之間的代碼一般不需要作修改,
如果平台具有特殊性,也只需對相關函數進行必要修改。
這裏簡單注明了一下各個函數的功能,以便了解。
/*******************************************************************************/
5.10 init_modules()
模塊初始化。如果編譯内核時使能該選項,則内核支持模塊化加載/卸載功能
5.11 kmem_cache_init()
内核Cache初始化。
5.12 sti()
使能中斷,這裏開始,中斷系統開始正常工作。
5.13 calibrate_delay()
近似計算BogoMIPS數字的内核函數。作為第一次估算,calibrate_delay計算出在每一秒内執行多少次__delay循環,也就是每個定時器滴答(timer tick)―百分之一秒内延時循環可以執行多少次。這種計算只是一種估算,結果并不能精確到納秒,但這個數字供内核使用已經足够精確了。
BogoMIPS的數字由内核計算并在系統初始化的時候打印。它近似的給出了每秒鐘CPU可以執行一個短延遲循環的次數。在内核中,這個結果主要用于需要等待非常短周期的設備驅動程序――例如,等待幾微秒并查看設備的某些信息是否已經可用。
計算一個定時器滴答内可以執行多少次循環需要在滴答開始時就開始計數,或者應該盡可能與它接近。全局變量jiffies中存儲了從内核開始保持跟踪時間開始到現在已經經過的定時器滴答數, jiffies保持异步更新,在一個中斷内——每秒一百次,内核暫時挂起正在處理的内容,更新變量,然後繼續剛才的工作。
5.14 mem_init()
内存初始化。本函數通過内存碎片的重組等方法標記當前剩餘内存, 設置内存上下界和頁表項初始值。
5.15 kmem_cache_sizes_init()
内核内存管理器的初始化,也就是初始化cache和SLAB分配機制。
5.16 pgtable_cache_init()
頁表cache初始化。
5.17 fork_init()
這裏根據硬件的内存情况,如果計算出的max_threads數量太大,可以自行定義。
5.18 proc_caches_init();
為proc文件系統創建高速緩衝
5.19 vfs_caches_init(num_physpages);
為VFS創建SLAB高速緩衝
5.20 buffer_init(num_physpages);
初始化buffer
5.21 page_cache_init(num_physpages);
頁緩衝初始化
5.22 signals_init();
創建信號隊列高速緩衝
5.23 proc_root_init();
在内存中創建包括根結點在内的所有節點
5.24 check_bugs();
檢查與處理器相關的bug
5.25 smp_init();
5.26 rest_init();
此函數調用kernel_thread(init, NULL, CLONE_FS | CLONE_FILES | CLONE_SIGNAL)函數。
5.26.1 kernel_thread()函數分析
這裏調用了arch/armnommu/kernel/process.c中的函數kernel_thread,kernel_thread函數中通過 __syscall(clone) 創建新綫程。__syscall(clone)函數參見armnommu/kernel目録下的entry-common.S文件。
5.26.2 init()完成下列功能:
Init()函數通過kernel_thread(init, NULL, CLONE_FS | CLONE_FILES | CLONE_SIGNAL)的回調函數執行,完成下列功能。
do_basic_setup()
在該函數裏,sock_init()函數進行網絡相關的初始化,占用相當多的内存,如果所開發系統不支持網絡功能,可以把該函數的執行注釋掉。
do_initcalls()實現驅動的初始化, 這裏需要與vmlinux.lds聯系起來看才能明白其中奥妙。
static void __init do_initcalls(void)
{
initcall_t *call;
call = &__initcall_start;
do {
(*call)();
call++;
} while (call < &__initcall_end);
/* Make sure there is no pending stuff from the initcall sequence */
flush_scheduled_tasks();
}
查看 /arch/i386/vmlinux.lds,其中有一段代碼
__initcall_start = .;
.initcall.init : { *(.initcall.init) }
__initcall_end = .;
其含義是__initcall_start指向代碼節.initcall.init的節首,而__initcall_end指向.initcall.init的節尾。
do_initcalls所作的是系統中有關驅動部分的初始化工作,那麽這些函數指針數據是怎様放到了.initcall.init節呢?在include/linux/init.h文件中有如下3個定義:
1. #define __init_call __attribute__ ((unused,__section__ (".initcall.init")))
__attribute__的含義就是構建一個在.initcall.init節的指向初始函數的指針。
2. #define __initcall(fn) static initcall_t __initcall_##fn __init_call = fn
##意思就是在可變參數使用宏定義的時候構建一個變量名稱為所指向的函數的名稱,并且在前面加上__initcall_
3. #define module_init(x) __initcall(x);
很多驅動中都有類似module_init(usb_init)的代碼,通過該宏定義逐層解釋存放到.initcall.int節中。
blkmem相關的修改(do_initcalls()初始化驅動時執行此代碼)
在blkmem_init ()函數中,調用了blk_init_queue()函數,blk_init_queue()函數調用了blk_init_free_list()函數, blk_init_free_list()函數又調用了blk_grow_request_list()函數,在這個函數中會 kmem_cache_alloc出nr_requests個request結構體。
這裏如果nr_requests的值太大,則將占用過多的内存,將造成硬件内存不够,因此可以根據實際情况將其替换成了較小的值,比如32、16等。
free_initmem
這個函數在arch/armnommu/mm/init.c文件中,其作用就是對init節的釋放,也可以通過修改代碼指定為不釋放。
5.26.3 init執行過程
在内核引導結束并啓動init之後,系統就轉入用户態的運行,在這之後創建的一切進程,都是在用户態進行。這裏先要清楚一個概念:就是init進程雖然是從内核開始的,即在前面所講的init/main.c中的init()函數在啓動後就已經是一個核心綫程,但在轉到執行init程序(如 /sbin/init)之後,内核中的init()就變成了/sbin/init程序,狀態也轉變成了用户態,也就是説核心綫程變成了一個普通的進程。這様一來,内核中的init函數實際上只是用户態init進程的入口,它在執行execve("/sbin/init",argv_init, envp_init)時改變成為一個普通的用户進程。這也就是exec函數的乾坤大挪移法,在exec函數調用其他程序時,當前進程被其他進程“靈魂附體”。
除此之外,它們的代碼來源也有差彆,内核中的init()函數的源代碼在/init/main.c中,是内核的一部分。而/sbin/init程序的源代碼是應用程序。
init程序啓動之後,要完成以下任務:檢查文件系統,啓動各種後台服務進程,最後為每個終端和虚擬控制台啓動一個getty進程供用户登録。由于所有其它用户進程都是由init派生的,因此它又是其它一切用户進程的父進程。
init進程啓動後,按照/etc/inittab的内容進程系統設置。很多嵌入式系統用的是BusyBox的init,它與一般所使用的init不一様,會先執行/etc/init.d/rcS而非/etc/rc.d/rc.sysinit。
小結:
本想多整理一些相關資料,無奈又要開始新項目的奔波,start_kernel()函數也剛好差不多講完了,分析的不是很深入,希望對嵌入式Linux移植的網友們有一些幫助。最後列舉下面幾處未整理的知識點,有興趣的網友可作進一步探討。
text.init和data.init説明
__init 標示符在gcc編譯器中指定將該函數置于内核的特定區域。在内核完成自身初始化之後,就試圖釋放這個特定區域。實際上,内核中存在兩個這様的區域,. text.init和.data.init――第一個是代碼初始化使用的,另外一個是數據初始化使用的。另外也可以看到__initfunc和 __initdata標志,前者和__init類似,標志初始化專用代碼,後者則標志初始化專用數據。
System.map内核符號表
irq的處理過程
Linux内核調度過程
2007年6月5日 星期二
GNUARM命令行工具基本使用入門
GNUARM命令行工具基本使用入門
調用格式:
arm-elf-gcc [stage-opt] [other-opts] -mcpu=arm7tdmi in-file -o out-file
常見用法:
將C代碼編譯為二進制目標文件:
arm-elf-gcc -c -O2 -g -mcpu=arm7tdmi filename.c -o filename.o
將多個二進制目標文件合并為一個可執行文件:
arm-elf-ld filename1.o filename2.o … -o filename.elf
將C代碼直接編譯生成可執行文件:
arm-elf-gcc -O2 -g -mcpu=arm7tdmi filename.c -o filename.elf
將C代碼編譯生成匯編代碼:
arm-elf-gcc -S -fverbose-asm -mcpu=arm7tdmi filename.c -o filename.s
arm-elf-objdump option filename | more
例如:arm-elf-objdump -S a2.o
使用readelf查看elf文件的内容,例如:arm-elf-readelf -a a2.elf
arm-elf-objcopy有一個很重要的作用是把代碼從elf文件中抽取出來,形成可執行的機器碼:
例如:arm-elf-objcopy -O binary -R .comment -R .note -S a2.elf a2.bin
形成的結果文件a2.bin可以焼到flash或下載到内存中去.
arm-elf-nm用來列出elf文件中使用到的symbol,例如:arm-elf-nm a1.o
http://www.micetek.com.cn/technic/gcc.pdf
調用格式:
arm-elf-gcc [stage-opt] [other-opts] -mcpu=arm7tdmi in-file -o out-file
常見用法:
將C代碼編譯為二進制目標文件:
arm-elf-gcc -c -O2 -g -mcpu=arm7tdmi filename.c -o filename.o
將多個二進制目標文件合并為一個可執行文件:
arm-elf-ld filename1.o filename2.o … -o filename.elf
將C代碼直接編譯生成可執行文件:
arm-elf-gcc -O2 -g -mcpu=arm7tdmi filename.c -o filename.elf
將C代碼編譯生成匯編代碼:
arm-elf-gcc -S -fverbose-asm -mcpu=arm7tdmi filename.c -o filename.s
arm-elf-objdump option filename | more
例如:arm-elf-objdump -S a2.o
使用readelf查看elf文件的内容,例如:arm-elf-readelf -a a2.elf
arm-elf-objcopy有一個很重要的作用是把代碼從elf文件中抽取出來,形成可執行的機器碼:
例如:arm-elf-objcopy -O binary -R .comment -R .note -S a2.elf a2.bin
形成的結果文件a2.bin可以焼到flash或下載到内存中去.
arm-elf-nm用來列出elf文件中使用到的symbol,例如:arm-elf-nm a1.o
http://www.micetek.com.cn/technic/gcc.pdf
訂閱:
文章 (Atom)
-
昨天差點昏倒, 因為Chrome Browser一開啟後居然一片白畫面. 雖然還有IE可以用, 但就是習慣Chrome了啊, 然後開始Google別人如何解決, 不過看起來不少人遇到相同的問題, 但都沒什麼解決方法. 什麼掃毒啦, 重新安裝Chrome啦, 砍掉Default啦....
-
I/O mapped I/O(port-mapped I/O或Direct I/O) I/O與memory均擁有自己的記憶體空間 需要特別的指令來處理I/O 好處是完全不用考慮記憶體空間被I/O佔用,缺點需要額外的指令專門處理I/O存取。 Memory Mapped I/O I/...
-
好像不少人會找這個Sample Code, 小修改一下好了. 先前的Code有不少的Warning出現而且會Crash耶! 底下分別列出UDP Server及Client的範例程式. UDP Server (udp-server.c) 利用 socket 介面設計網路應用程...
一個小故事讓我們明白資金流通的意義
“又是炎熱小鎮慵懶的一天。太陽高掛,街道無人,每個人都債台高築,靠信用度日。這時,從外地來了一位有錢的旅客,他進了一家旅館,拿出一張1000 元鈔票放在櫃檯,說想先看看房間,挑一間合適的過夜,就在此人上樓的時候---- 店主抓了這張1000 元鈔,跑到隔壁屠戶那裡支付了他欠的肉錢...