從微觀角度來看Linux內核設計
來自:Linux內核之旅(微信號:LinuxKernelTravel)
作者:趙晨雨
◆◆
從微觀角度來看Linux內核設計
◆◆
餘生皆歡喜
最近總結出來學習內核有兩個大的角度,一種就是從宏觀角度來看,總的來說就是順著抽象,管理,操作來看,這種角度更多的是內核中應用層面的內容,用來理解內核中是怎麼運轉起來的。第二種就是從內核的最細節部分出發,深入到一個個具體的宏,看看內核設計者在細節部分有著怎麼樣的巧妙之處,這樣也有助於我們夯實C語言基礎,也可以學習到GNU C的用法。
最近學習了如下的GNU C的內容:
指定初始化
語句表達式
typeof關鍵字
內核第一宏
我們來看看這些內容是怎麼設計的,GNU C就是打輔助的,專門為了OS而存在,(為什麼全世界不統一使用GNU C呢?)它帶來了太多的方便,換句話說,它幫助內核設計人員解決了很多內核設計者在設計內核時所遇到的問題,我這樣認為,GNU C中每一條功能,就是內核設計者在實際設計中遇到的問題。
這裡再次分析總結gitbook中的兩個宏,一個是max/min宏,一個是內核第一宏container_of。
max/min宏
內核中的樣子:
這裡的max宏可以讓我們學會語句表達式,typeof關鍵字;基礎方面可以鞏固運算符優先順序。
這個宏是怎麼得到的呢?
我們來寫一個宏,用來比較兩個變數的大小,我一定會這麼寫:
那麼我們來比較一下
4!=4
和
2!=3
,結果是錯誤的,原因是運算符優先順序出了問題。那麼我們來解決,使用括弧是最簡單的方法:
我們來運行一條語句:
printf("max = %d
",3 + MAX(4,5));
,結果是7,這裡是因為+的運算優先順序大於>了,換句話說,是因為外部的語句,影響到了宏,那麼我就把自己隔離起來:
再來運行一下
printf("max=%d
",MAX(2++,3++));
,輸出的會是4,但我們只想要比較2和3的值,這裡是因為自增自減運算符導致的問題,那麼怎麼解決呢?和交換兩個數字的想法一樣,通過一個中轉值來存放,就可以隔離影響了
這裡就有一些內核代碼中的味道了,注意一個細節,這裡的第四行沒有括弧了,為什麼?這裡就是因為語句表達式了,不存在上面的影響了。這裡我們回顧一下代碼,再看看目前這個宏的第二三行,是
int
,也就是我們這個宏只能比較int類型的變數,而在內核中需要比較大小的變數有很多,那麼我們來提高一下:
這個宏就可以用來比較任意類型的變數了,再來看一下代碼,我們需要替換的變數有
type
,
x
,
y
三個,如果有了typeof關鍵字,我們還可以減少一個:
接著來,如果我們使用了一次宏,是
MAX(i,j)
,其中i是int類型,j是float類型,這樣比較是可以的,但是在內核的設計過程之中,很有可能有些地方會出現問題,所以還需要改造:
這就是究極形態了,我們添加了第四行的代碼,來看
&_min1
,它的意思是取
_min1
的地址,而
&_min2
的意思是取
_min2
的地址,我們也知道,這兩個地址肯定不可能是一樣的,那為什麼還要這樣寫呢?這裡就很巧妙了,當兩個變數的類型不同時,對應的地址,也就是指針類型也不相同,比如一個是int類型,一個是char類型,那麼指向他們的指針就是int *和char *,這兩個指針在比較的時候,就比較的是類型了。如果比較的類型不一樣,gcc會警告的。
我們來看這一系列改進,我相信內核設計人員也想把代碼寫成
# define MAX(x,y) x > y? x : y
的樣子,但是現實是殘酷的,我們為了代碼的健壯性,就必須這樣一步一步來改進,所以,內核代碼看起來很複雜,又很巧妙,是因為我們直接看到的是究極形態的代碼,它是向現實妥協了多次以後的產物,也就是健壯性+GNU C。但是,內核設計者的初衷,或者說最初的想法和我們都是一樣的。
有內核源碼在旁邊,鞏固基礎知識就不用像以前的學習模式了,可以在源碼中代入學習,增添一份趣味性,並且可以很快理解。
在以後處理因為運算符而導致的問題的時候,使用括弧是最方便的,內核就這麼幹了。
在寫程序的時候,要巧用中轉變數,雖然只是簡單的存入另一個變數之中,但是代碼的健壯性提高了很多。
兩個地址在進行比較的時候,我們可以得知這兩個指針類型是否一致。
內核第一宏
gitchat中把container_of宏叫做內核第一宏,我也很喜歡這個稱號,因為學內核兩個月里見這個宏的次數太多了。在陳老師講list.h的時候,就學習過這個宏,但是並沒有完完全全地剖析開。
高能預警:
這個宏的作用我們已經很清楚了,根據結構體中某一成員的地址,就可以獲得這個結構體的首地址,再說的明白一點,假如你是內核設計人員,前面也說道了,我們已經對數據進行了多次封裝,我們一定會遇到這種情況:傳給某個函數的參數是某個結構體成員變數,但是我們在這個函數中還想使用這個結構體的其它成員變數,這個時候就需要想辦法,於是才有了我們現在看到的這個內核第一宏。
它的三個參數是:
ptr:此結構體內成員member的地址
type:此結構體類型
member:此結構體內的成員
我們直接看代碼,這個宏的最後的值,就是最後一條語句,
(type *)( (char *)__mptr - offsetof(type,member) );})
,這條語句也是這個宏的中心思想
拿結構體成員的地址減去此成員的偏移
,這裡也體現了指針做減法是很有意義的。成員的地址好說,我們直接傳進來了,偏移是通過offsetof來實現的,來看看這個offsetof:將0強制類型轉換成這個結構體的指針類型,然後訪問這個成員,加上&得到它的偏移,返回。這裡要注意一下,那就是為什麼只通過TYPE和MEMBER就可以得到偏移,我一開始認為的是內核中這個類型的結構體多了,到底用的是哪一個結構體來得到的,最後發現,並沒有關係,因為我們需要的是位元組數,與實際這個欄位賦什麼樣的值並沒有關係,因為所有這個類型的結構體中,各成員的位元組大小是一樣的。
再來看
(char *)__mptr
,這個通過第四行代碼可以很容易得出它是成員的地址,為什麼要強制轉換成
char *
呢?轉換成
int *
不行嗎?這裡又可以學習一下C指針的基礎知識,通過代碼可以很容易知道有什麼區別:
列印出來的值,p(int *)類型,增加了4位元組,而q(char *)增加了1位元組,回到宏中,我們的偏移是按照位元組來算的,所以不能使用(int *),必須使用(char *)。在最後,再次強制類型轉換成指向這個結構體的指針類型。
回過頭來看第四行代碼,
const typeof( ((type *)0)->member ) *__mptr = (ptr);
,這裡和max宏之中類似,使用了中轉變數來存放,這裡為什麼要使用中轉變數?max宏中是為了防止自增自減的影響(當然只是原因之一了),但我們在使用的時候總不至於發過來成員的地址再加一個++運算符吧。我們可以從const的用法來思考,
const int * p //p可變,p指向的內容不可變
,所以,使用了const,我們就可以保證ptr指向的內容在這裡只是可讀的,這也許就是為什麼使用中轉變數的原因,為了防止我們通過指針改變了原有的成員的值,畢竟指針雖然強大,但也是很危險的,所以,這裡的中轉要配合const來使用。既然是中轉,那麼類型就必須要求一致了,所以我們要得到和這個成員一致的類型,就通過typeof來得到了,將0強制類型轉換成這個這個結構體的指針類型,然後訪問這個變數,(注意仔細看代碼,這裡的代碼和offsetof非常類似)這裡沒有使用&,所以只是訪問到變數了,沒有得到偏移。另外根據const的用法,第四行的代碼也可以寫成
typeof( ((type *)0)->member ) const *__mptr = (ptr);
也就是把const放到後面。
我們再來注意一個細節,就是offsetof里的
size_t
,這個是什麼,這裡在敲代碼的過程中偶然學到一個小技巧,就是這個size_t絕對是封裝,就是C語言中那幾種變數類型,我們可以
typedef int size_t;
然後運行,gcc就會報錯,並且會給你顯示:以前已經定義過:
typedef __SIZE_TYPE__ size_t
,並且會指定這個值在哪個文件,我們就可以知道它的真面目了。換句話說,gcc這麼強大,我們當然可以把它當做一個學習工具來使用。
另外還可以通過sublime,可以很快找到它的真面目(3.10版本):
最後,為了更深入理解這些知識的使用方法,還是需要自己動手來敲代碼的,尤其是內核第一宏,將代碼寫到用戶態下,然後瘋狂改造,這樣才會真正理解這個宏。
參考資料:
https://gitbook.cn/gitchat/column/5a5c61512be8c36114823584
●編號650,輸入編號直達本文
●輸入m獲取文章
目錄
推薦↓↓↓
運維
更多推薦
《
25個技術類公眾微信
》
涵蓋:程序人生、演算法與數據結構、黑客技術與網路安全、大數據技術、前端開發、Java、Python、Web開發、安卓開發、iOS開發、C/C++、.NET、Linux、資料庫、運維等。
![](https://pic.pimg.tw/zzuyanan/1488615166-1259157397.png)
![](https://pic.pimg.tw/zzuyanan/1482887990-2595557020.jpg)
TAG:Linux學習 |