Linux核心常見巨集的作用
- __init
位置:
/include/linux/init.h
定義:
#define __init __attribute__ ((__section__ (".init.text")))
註釋:這個標誌符和函式宣告放在一起,表示gcc編譯器在編譯時,需要把這個函式放在.text.init Section 中,而這個Section 在核心完成初始化之後,就會被釋放掉。
舉例:
asmlinkage void __init star_kerne(void) { ... }
- __initdata
位置:
/include/ linux/init.h
定義:
#define __initdata __attribute__ ((__section__ (".init.data")))
註釋:這個標誌符和變數宣告放在一起,表示gcc編譯器在編譯時,需要把這個變數放在.data.init Section中,而這個Section 在核心完成初始化之後,會釋放掉。
舉例:
static struct kernel_param raw_params[] __initdata ={ ... }
- __initfunc()
位置:
/include/asm-i386/init.h
定義:
#define __initfunc(__arginit) \
__arginit __init; \
__arginit
註釋:__initfunc() 是一個自定義巨集,用來定義一個 __init 函式,在Linux-2.4中已被__init巨集所取代。
舉例:
__initfunc (void mem_init(unsigned long start_mem, unsigned long end_mem)) { ... }
- asmlinkage
位置:
/include/linux/linkage.h
定義:
#define asmlinkage CPP_ASMLINKAGE __attribute__((regparm(0)))
註釋:這個標誌符和函式宣告放在一起,帶regparm(0)的屬性宣告告訴gcc編譯器,該函式不需要通過任何暫存器來傳遞引數,引數只是通過堆疊來傳遞。gcc編譯器在彙編過程中呼叫c語言函式時傳遞引數有兩種方法:一種是通過堆疊,另一種是通過暫存器。
預設時採用暫存器,假如你要在你的彙編過程中呼叫c語言函式,並且想通過堆疊傳遞引數,你定義的 c 函式時要在函式前加上巨集asmlinkage。
舉例:
asmlinkage void __init start_kernel(void) { ... }
5.ENTRY()
位置:
/include/linux/linkage.h
定義:
#define ENTRY(name) \
.globl name; \
ALIGN; \
name:
註釋:將name 宣告為全域性,對齊,並定義為標號。
- FASTCALL ()
位置:
/include/linux/kernel.h
定義:
#define FASTCALL(x) x __attribute__((regparm(3)))
註釋:這個標誌符和函式宣告放在一起,帶regparm(3)的屬性宣告告訴gcc編譯器,這個函式可以通過暫存器傳遞多達3個的引數,這3個暫存器依次為EAX、EDX 和 ECX。更多的引數才通過堆疊傳遞。這樣可以減少一些入棧出棧操作,因此呼叫比較快。
舉例:
extern void FASTCALL(__switch_to(struct task_struct *prev, struct task_struct *next))
這個例子中,prev將通過eax,next通過edx傳遞。
- __sched
位置:
/include/linux/sched.h
定義:
/* Attach to any functions which should be ignored in wchan output. */
#define __sched __attribute__((__section__(".sched.text")))
可執行檔案的記憶體佈局對程式效能的影響是非常巨大的,因為最近一直在做效能優化,對這方面感觸頗深。要搞明白可執行檔案的記憶體佈局,就必須得了解編譯原理,當然編譯原理實在是太過於高深了,我所知也是皮毛,所以我就從最實用的地方開始入手一點點的分析。
就從我前文裡提到的__attribute__((section(“.sec_name”)))來說起吧,因為我使用這個東西確實給我們的效能帶來了一定的提升。
關於attribute section這個東西,你要google的話,能夠搜尋出來不少前人的分析,不過實在都是大同小異,你抄我來我抄你,毫無營養。在他們的部落格裡,無非是說,“將作用的函式或者資料放入指定名為‘.sec_name’ 的輸入段”,然後再巴拉巴拉一通什麼是輸入段,說的你雲裡霧裡一頭霧水分不清東西南北頓覺高大上。
那我們就來看下attribute section到底是什麼。
要知道attribute section, 就要先理解連結指令碼。連結指令碼即連結器在把.o檔案連結成最後的elf檔案所遵循的規則,也就是,最終的可執行檔案是什麼樣子的是由這個連結指令碼決定的。
連結指令碼的語法和C語言很類似,我們能夠很容易讀明白,所以從連結指令碼來入手分析這個東西會更清晰一些。
對應於 attribute((section(“.sec_name”)))這句話,它在連結的時候採取的預設規則是:
.sec_name
{
*(.sec_name)
}
即把.sec_name指向的內容放在.sec_name這個段裡面。我們再來稍微清晰化一些,下面舉個例子。
void foo(void) __attribute__((section(".in_name")));
void bar(void) __attribute__((section(".in_name")));
我們使用attribute section來聲明瞭兩個函式,然後我們在連結腳本里面做如下約束:
.out_name
{
*(.in_name)
}
這樣就把foor(),bar()這兩個函式給放在了最終elf檔案裡的.out_name這個section。而如果我們不再連結腳本里做這個約束,那麼它在連結過程中就會採用預設規則,即輸入段和輸出段的名字是一樣的:
.in_name
{
*(.in_name)
}
總結起來就是,attribute((section(“.in_name”)))的作用是把.in_name指向的符號給放在一起。
唔~ 仍然有點模糊是不? 那就好好讀讀《linkers and loaders》或者《程式設計師的自我修養》這兩本書吧。
然後我們來看下linux核心對於attribute section的應用, 以linux kernel的連結指令碼vmlinux.lds為例。先來看下linux kernel最終映象的程式碼段是如何規劃的。
SECTIONS
{
. = VMLINUX_LOAD_ADDRESS;
/* read-only */
_text = .; /* Text and read-only data */
.text : {
TEXT_TEXT
SCHED_TEXT
LOCK_TEXT
KPROBES_TEXT
IRQENTRY_TEXT
*(.text.*)
*(.fixup)
*(.gnu.warning)
} :text = 0
}
如上就是一個典型的linux kenrel連結指令碼的程式碼段部分。稍微解釋下。 . = VMLINUX_LOAD_ADDRESS;的意思是說,此處的地址是VMLINUX_LOAD_ADDRESS,接著又把該值賦給了_text,也就是核心程式碼段的其實地址是VMLINUX_LOAD_ADDRESS,就這就開始了程式碼段。
在程式碼段裡面我們可以很明顯的看到它劃分了TEXT_TEXT、SCHED_TEXT、LOCK_TEXT、KPROBES_TEXT、IRQENTRY_TEXT,這樣劃分的目的,就是為了合理規劃地址空間以提升效能。我們可以看下這幾個巨集到底表示什麼:
#define TEXT_TEXT ALIGN_FUNCTION(); *(.text.hot) *(.text) *(.ref.text) MEM_KEEP(init.text) MEM_KEEP(exit.text) *(.text.unlikely)
#define SCHED_TEXT ALIGN_FUNCTION(); VMLINUX_SYMBOL(__sched_text_start) = .; *(.sched.text) VMLINUX_SYMBOL(__sched_text_end) = .;
#define LOCK_TEXT ALIGN_FUNCTION(); VMLINUX_SYMBOL(__lock_text_start) = .; *(.spinlock.text) VMLINUX_SYMBOL(__lock_text_end) = .;
#define KPROBES_TEXT ALIGN_FUNCTION(); VMLINUX_SYMBOL(__kprobes_text_start) = .; *(.kprobes.text) VMLINUX_SYMBOL(__kprobes_text_end) = .;
#define IRQENTRY_TEXT ALIGN_FUNCTION(); VMLINUX_SYMBOL(__irqentry_text_start) = .; *(.irqentry.text) VMLINUX_SYMBOL(__irqentry_text_end) = .;
這些巨集其實就是定義了一些input section, 比如.text.hot等。
接著以.sched.text為例來看看到底是怎麼用的。
#define __sched __attribute__((__section__(".sched.text")))
void __sched notrace preempt_schedule_context(void);
static void __sched __schedule(void);
asmlinkage void __sched schedule(void);
asmlinkage void __sched schedule_user(void);
void __sched schedule_preempt_disabled(void);
asmlinkage void __sched notrace preempt_schedule(void);
asmlinkage void __sched preempt_schedule_irq(void);
....
一目瞭然了吧? 就是前面我們說的attribute section這個東西,核心就是使用了這個東西來規劃地址空間,將相互關聯的程式碼給放在一起,以達到提升效能並保持穩定的作用。
因為松柏公司的效能受程式碼check-in影響波動較大,所以我就想到了使用linux kernel的這種做法來規劃可執行檔案的地址空間,按照不同模組來劃分不同的section,這樣來避免頻繁code check-in對效能波動的影響。
.text
{
*(.module_a.text)
*(.module_b.text)
*(.module_c.text)
...
}
其實,再稍微的深入思考下,我們就能發現一個更細粒度的控制,那就是控制函式在可執行檔案裡的先後順序。
void foo(void) __attribute__((section(".in_name.1")));
void bar(void) __attribute__((section(".in_name.2")));
.text
{
*(.in_name.1)
*(.in_name.2)
...
}
這樣做之後,在可執行檔案裡,foo就會在bar的前面,及foo和bar的地址緊挨著,bar緊跟在foo的後面。我們在縮小一下我們的視角,從巨集觀上來看下這個連結指令碼。
SECTIONS
{
.text : {
...
}
.data : {
...
}
.bss:{
...
}
}
這也是為什麼可執行檔案的記憶體佈局先是程式碼段,接著資料段,再是bss段的原因,即連結指令碼決定可執行檔案的記憶體佈局。
想第一時間獲取嵌入式乾貨,請加公眾號baiwenkeji
有問題歡迎加WEI信討論交流:13266630429,驗證:CSDN