linux內核的一些預定義

 

所有的內核代碼,基本都包含了linux\compile.h這個文件,所以它是基礎,打算先分析這個文件裏的代碼看看,有空再分析分析其它的代碼。

首先印入眼簾的是對__ASSEMBLY__這個宏的判斷,這個變量實際是在編譯彙編代碼的時候,由編譯器使用-D這樣的參數加進去的,AFLAGS這個變量也定義了這個變量,gcc會把這個宏定義爲1。用在這裏,是因爲彙編代碼裏,不會用到類似於__user這樣的屬性(關於 __user這樣的屬性是怎麼回子事,本文後面會提到),因爲這樣的屬性是在定義函數的時候加的,這樣避免不必要的在編譯彙編代碼時候的引用。
接下來是一個對__CHECKER__這個宏的判斷,這裏需要講的東西比較多。
當編譯內核代碼的時候,使用make C=1或C=2的時候,會調用一個叫Sparse的工具,這個工具對內核代碼進行檢查,怎麼檢查呢,就是靠對那些聲明過Sparse這個工具所能識別的特性的內核函數或是變量進行檢查。在調用Sparse這個工具的同時,在Sparse代碼裏,會加上#define __CHECKER__ 1的字樣。換句話說,就是,如果使用Sparse對代碼進行檢查,那麼內核代碼就會定義__CHECKER__宏,否則就不定義。
所以這裏就能看出來,類似於__attribute__((noderef, address_space(1)))這樣的屬性就是Sparse這個工具所能識別的了。
那麼這些個屬性是幹什麼用的呢,我一個個做介紹。
這樣的屬性說明,有一部分在gcc的文檔裏還沒有加進去,至少我在gcc 4.3.2的特性裏沒有看到,網上有哥們問類似的問題,Greg對他進行了解答,然後他對Greg抱怨文檔的事,Greg對他說,他有時間抱怨的話,還不如自己來更新文檔。他不能對一個免費工具的文檔有如此之高的要求,除非他付費。

# define __user  __attribute__((noderef, address_space(1)))

__user 這個特性,即__attribute__((noderef, address_space(1))),是用來修飾一個變量的,這個變量必須是非解除參考(no dereference)的,即這個變量地址必須是有效的,而且變量所在的地址空間必須是1,即用戶程序空間的。
這裏把程序空間分成了3個部分,0表示normal space,即普通地址空間,對內核代碼來說,當然就是內核空間地址了。1表示用戶地址空間,這個不用多講,還有一個2,表示是設備地址映射空間,例如硬件設備的寄存器在內核裏所映射的地址空間。

所以在內核函數裏,有一個copy_to_user的函數,函數的參數定義就使用了這種方式。當然,這種特性檢查,只有當機器上安裝了Sparse這個工具,而且進行了編譯的時候調用,才能起作用的。

# define __kernel /* default address space */

根據定義,就是默認的地址空間,即0,我想定義成__attribute__((noderef, address_space(0)))也是沒有問題的。

# define __safe  __attribute__((safe))

這個定義在sparse裏也有,內核代碼是在2.6.6-rc1版本變到2.6.6-rc2的時候被Linus加入的,經過我的艱苦的查找,終於查找到原因了,知道了爲什麼Linus要加入這個定義,原因是這樣的:
有人發現在代碼編譯的時候,編譯器對變量的檢查有些苛刻,導致代碼在編譯的時候老是出問題(我這裏沒有去檢查是編譯不通過還是有警告信息,因爲現在的編譯器已經不是當年的編譯器了,代碼也不是當年的代碼)。比如說這樣一個例子,
 int test( struct a * a, struct b * b, struct c * c ) {
  return a->a + b->b + c->c;
 }
這個編譯的時候會有問題,因爲沒有檢查參數是否爲空,就直接進行調用。但是呢,在內核裏,有好多函數,當它們被調用的時候,這些個參數必定不爲空,所以根本用不着去對這些個參數進行非空的檢查,所以呢,就增加了一個__safe的屬性,如果這樣聲明變量,
 int test( struct a * __safe a, struct b * __safe b, struct c * __safe c ) {
  return a->a + b->b + c->c;
 }
編譯就沒有問題了。

不過我在現在的代碼裏沒有發現有使用__safe這個定義的地方,不知道是不是編譯器現在已經支持這種特殊的情況了,所以就不用再加這樣的代碼了。

# define __force __attribute__((force))

表示所定義的變量類型是可以做強制類型轉換的,在進行Sparse分析的時候,是不用報告警信息的。

# define __nocast __attribute__((nocast))

這裏表示這個變量的參數類型與實際參數類型一定得對得上才行,要不就在Sparse的時候生產告警信息。

# define __iomem __attribute__((noderef, address_space(2)))

這個定義與__user, __user是一樣的,只不過這裏的變量地址是需要在設備地址映射空間的。

# define __acquires(x) __attribute__((context(x,0,1)))
# define __releases(x) __attribute__((context(x,1,0)))

這是一對相互關聯的函數定義,第一句表示參數x在執行之前,引用計數必須爲0,執行後,引用計數必須爲1,第二句則正好相反,這個定義是用在修飾函數定義的變量的。

# define __acquire(x) __context__(x,1)
# define __release(x) __context__(x,-1)

這是一對相互關聯的函數定義,第一句表示要增加變量x的計數,增加量爲1,第二句則正好相反,這個是用來函數執行的過程中。

以上四句如果在代碼中出現了不平衡的狀況,那麼在Sparse的檢測中就會報警。當然,Sparse的檢測只是一個手段,而且是靜態檢查代碼的手段,所以它的幫助有限,有可能把正確的認爲是錯誤的而發出告警。要是對以上四句的意思還是不太瞭解的話,請在源代碼裏搜一下相關符號的用法就能知道了。這第一組與第二組,在本質上,是沒什麼區別的,只是使用的位置上,有所區別罷了。

# define __cond_lock(x,c) ((c) ? ({ __acquire(x); 1; }) : 0)

這句話的意思就是條件鎖。當c這個值不爲0時,則讓計數值加1,並返回值爲1。不過這裏我有一個疑問,就是在這裏,有一個__cond_lock定義,但沒有定義相應的__cond_unlock,那麼在變量的釋放上,就沒辦法做到一致。而且我查了一下關於spin_trylock()這個函數的定義,它就用了__cond_lock,而且裏面又用了_spin_trylock函數,在_spin_trylock函數裏,再經過幾次調用,就會使用到 __acquire函數,這樣的話,相當於一個操作,就進行了兩次計算,會導致Sparse的檢測出現告警信息,經過我寫代碼進行實驗,驗證了我的判斷,確實是會出現告警信息,如果我寫兩遍unlock指令,就沒有告警信息了,但這是與程序的運行是不一致的。

extern void __chk_user_ptr(const volatile void __user *);
extern void __chk_io_ptr(const volatile void __iomem *);

這兩句比較有意思。這裏只是定義了函數,但是代碼裏沒有函數的實現。這樣做的目的,就是在進行Sparse的時候,讓Sparse給代碼做必要的參數類型檢查,在實際的編譯過程中,並不需要這兩個函數的實現。

#define notrace __attribute__((no_instrument_function))

這一句,是定義了一個屬性,這個屬性可以用來修飾一個函數,指定這個函數不被跟蹤。那麼這個屬性到底是怎麼回子事呢?原來,在gcc編譯器裏面,實現了一個非常強大的功能,如果在編譯的時候把一個相應的選擇項打開,那麼就可以在執行完程序的時候,用一些工具來顯示整個函數被調用的過程,這樣就不需要讓程序員手動在所有的函數裏一點點添加能顯示函數被調用過程的語句,這樣耗時耗力,還容易出錯。那麼對應在應用程序方面,可以使用Graphviz這個工具來進行顯示,至於使用說明與軟件實現的原理可以自己在網上查一查,很容易查到。對應於內核,因爲內核一直是在運行階段,所以就不能使用這套東西了,內核是在自己的內部實現了一個ftrace的機制,編譯內核的時候,如果打開這個選項,那麼通過掛載一個debugfs的文件系統來進行相應內容的顯示,具體的操作步驟,可以參看內核源碼所帶的文檔。那上面說了這麼多,與notrace這個屬性有什麼關係呢?因爲在進行函數調用流程的顯示過程中,是使用了兩個特殊的函數的,當函數被調用與函數被執行完返回之前,都會分別調用這兩個特別的函數。如果不把這兩個函數的函數指定爲不被跟蹤的屬性,那麼整個跟蹤的過程就會陷入一個無限循環當中。

#define likely(x) __builtin_expect(!!(x), 1)
#define unlikely(x) __builtin_expect(!!(x), 0)

這兩句是一對對應關係。__builtin_expect(expr, c)這個函數是新版gcc支持的,它是用來作代碼優化的,用來告訴編譯器,expr的期,非常有可能是c,這樣在gcc生成對應的彙編代碼的時候,會把相應的可能執行的代碼都放在一起,這樣能少執行代碼的跳轉。爲什麼這樣能提高CPU的執行效率呢?因爲CPU在執行的時候,都是有預先取指令的機制的,把將要執行的指令取出一部分出來準備執行。CPU不知道程序的邏輯,所以都是從可程序程序裏挨着取的,如果這個時候,能不做跳轉,則CPU預先取出的指令都可以接着使用,反之,則預先取出來的指令都是沒有用的。還有個問題是需要注意的,在__builtin_expect的定義中,以前的版本是沒有!!這個符號的,這個符號的作用其實就是負負得正,爲什麼要這樣做呢?就是爲了保證非零的x的值,後來都爲1,如果爲零的0值,後來都爲0,僅此而已。

#ifndef barrier
# define barrier() __memory_barrier()
#endif

這裏表示如果沒有定義barrier函數,則定義barrier()函數爲__memory_barrier()。但在內核代碼裏,是會包含 compiler-gcc.h這個文件的,所以在這個文件裏,定義barrier()爲__asm__ __volatile__("": : :"memory")。barrier翻譯成中文就是屏障的意思,在這裏,爲什麼要一個屏障呢?這是因爲CPU在執行的過程中,爲了優化指令,可能會對部分指令以它自己認爲最優的方式進行執行,這個執行的順序並不一定是按照程序在源碼內寫的順序。編譯器也有可能在生成二進制指令的時候,也進行一些優化。這樣就有可能在多CPU,多線程或是互斥鎖的執行中遇到問題。那麼這個內存屏障可以看作是一條線,內存屏障用在這裏,就是爲了保證屏障以上的操作,不會影響到屏障以下的操作。然後再看看這個屏障怎麼實現的。__asm__表示後面的東西都是彙編指令,當然,這是一種在C語言中嵌入彙編的方法,語法有其特殊性,我在這裏只講跟這條指令有關的。__volatile__表示不對此處的彙編指令做優化,這樣就會保證這裏代碼的正確性。""表示這裏是個空指令,那麼既然是空指令,則所對應的指令所需要的輸入與輸出都沒有。在gcc中規定,如果以這種方式嵌入彙編,如果輸出沒有,則需要兩個冒號來代替輸出操作數的位置,所以需要加兩個::,這時的指令就爲"" : :。然後再加上爲分隔輸入而加入的冒號,再加上空的輸入,即爲"" : : :。後面的memory是gcc中的一個特殊的語法,加上它,gcc編譯器則會產生一個動作,這個動作使gcc不保留在寄存器內內存的值,並且對相應的內存不會做存儲與加載的優化處理,這個動作不產生額外的代碼,這個行爲是由gcc編譯器來保證完成的。如果對這部分有更大的興趣,可以考察gcc的幫助文檔與內核中一篇名爲memory-barriers.txt的文章。

#ifndef RELOC_HIDE
# define RELOC_HIDE(ptr, off)     \
  ({ unsigned long __ptr;     \
     __ptr = (unsigned long) (ptr);    \
    (typeof(ptr)) (__ptr + (off)); })
#endif

這個沒有什麼太多值得講的,也能看明白,雖然不知道具體用在哪裏,所以留做以後遇到了再說吧。

接下來好多定義都沒有實現,可以看一看註釋就知道了,所以這裏就不多說了。唉,不過再插一句,__deprecated屬性的實現是爲deprecated。

#define noinline_for_stack noinline

#ifndef __always_inline
#define __always_inline inline
#endif

這裏noinline與inline屬性是兩個對立的屬性,從詞面的意思就非常好理解了。

#ifndef __cold
#define __cold
#endif

從註釋中就可以看出來,如果一個函數的屬性爲__cold,那麼編譯器就會認爲這個函數幾乎是不可能被調用的,在進行代碼優化的時候,就會考慮到這一點。不過我沒有看到在gcc裏支持這個屬性的說明。

#ifndef __section
# define __section(S) __attribute__ ((__section__(#S)))
#endif

這個比較容易理解了,用來修飾一個函數是放在哪個區域裏的,不使用編譯器默認的方式。這個區域的名字由定義者自己取,格式就是__section__加上用戶輸入的參數。

#define ACCESS_ONCE(x) (*(volatile typeof(x) *)&(x))

這個函數的定義很有意思,它就是訪問這個x參數所對應的東西一次,它是這樣做的:先取得這個x的地址,然後把這個地址進行變換,轉換成一個指向這個地址類型的指針,然後再取得這個指針所指向的內容。這樣就達到了訪問一次的目的,哈哈。

真不容易,終於把這個東西寫完了,僅僅幾十行的代碼,裏面所包含的知識真的是異常豐富,通過分析這個頭文件,我自己學得了不少東西,不敢獨享,拿出來給與興趣的朋友一同分享。
發佈了13 篇原創文章 · 獲贊 1 · 訪問量 5萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章