linux内核研究(一)
http://antkillerfarm.github.io/
驅動開發
推薦入門讀物《Beginning Linux Programming》,該書第3版已有中譯本。
但第3版中的例子在2.6以后的新內核中不能編譯。經研究發現,由于新版內核采用KBuild系統編譯內核,所以驅動也必須使用KBuild系統編譯。
驅動開發的頭文件可以在/usr/src下找到。
進階讀物有:
《LINUX設備驅動程序》
《Linux設備驅動開發詳解》
驅動開發和內核開發的聯系與區別
驅動是內核的一部分,驅動開發工程師所需的技能,和內核開發工程師相差無幾。但從工作內容來說,兩者還是有較大的差異。
驅動開發偏重于利用內核的現有驅動架構,給內核添加新的硬件支持,而內核開發,則主要是對系統架構進行修改,相當于為驅動開發提供彈藥。因此,從這個意義上來說,內核的開發更為困難,國內很少有這方面的人才。
永不返回的函數(never return function)
了解C語言的人都知道一個函數的最后一個語句通常是return語句。編譯器在處理返回語句時,除了將返回值保存起來之外,最重要的任務就是清理堆棧。具體來說,就是將參數以及局部變量從堆棧中彈出。然后再從堆棧中得到調用函數時的PC寄存器的值,并將其加一個指令的長度,從而得到下一條指令的地址。再將這個地址放入PC寄存器中,完成整個返回流程,接著程序就會繼續執行下去了。
對于返回值是void類型,也就是無返回值的函數,保存返回值是沒有意義的,但它仍然會執行清理堆棧的操作。
以上提到的這些,基本上適用于99.99%的場合。但凡事無絕對,在一些特殊的地方,例如操作系統內核中的某些函數,就不見得符合上邊所說的這些。永不返回的函數就是其中之一。
在Linux源代碼中,一個永不返回的函數通常擁有一個類似如下函數的聲明:
NORET_TYPE void do_exit(long code)
考慮到NORET_TYPE的定義:
#define NORET_TYPE /**/
因此,NORET_TYPE在這里僅僅起到方便閱讀代碼的作用,而并沒有什么其他的特殊作用。
看到do_exit函數,可能熟悉Linux內核的朋友已經猜出永不返回的函數和普通函數有什么區別了。沒錯,do_exit函數是銷毀進程的最后一步。由于進程已經銷毀,從進程堆棧中獲得下一條指令的地址就顯得沒有什么意義了。do_exit函數會調用schedule函數進行進程切換,從另一個進程的堆棧中獲得相關寄存器的值,并恢復那個進程的執行。因此do_exit函數在正常情況下是不會返回的,一個調用了do_exit函數的函數,其位于do_exit函數之后的語句是不會執行到的。因此那個函數也成為了永不返回的函數。
Linux鏈表實現
數據結構課本上教鏈表的時候,一般是這樣定義鏈表的數據結構的:
{% highlight c %}
typedef struct {
struct Node *next;
UserData data;
}Node;
{% endhighlight %}
其中,data字段包含了要保存到鏈表中的數據內容。使用這樣的數據結構實現的鏈表,通用性不好,需要針對不同的UserData類型定義不同的鏈表類型,盡管所有這些鏈表的操作都是類似的。當然這樣的定義在C++中不是太大的問題,使用模板就可以實現對不同UserData類型的處理,雖然這樣做無法避免代碼段的膨脹,但是僅就書寫使用來說,并沒有太大的不方便。
一種改進的辦法是將數據結構改為下面的樣子:
typedef struct {struct Node *next;void* data; }Node;用無類型的指針指向需要保存的數據內容,是一個通用性不錯的辦法。但是C語言本身沒有對元數據的支持,一旦指針退化成無類型的指針,再想恢復成原來的數據類型就比較困難了。(元數據就是所有數據類型的基類,例如Java語言的Object類、MFC的CObject類、GTK的GObject結構。雖然元數據本身并不要求包含數據的類型信息,但在上述這些元數據的實現中,都提供了這個功能。)
Linux的做法是:(為了便于理解,進行了一些改寫,以忽略與本話題無關的部分)
typedef struct {struct Node *next; }Node; typedef struct {Node *node;UserDataActual data; }UserData;這實際上是一種逆向思維,也就是將鏈表結點中包含用戶數據,改為用戶數據中包含鏈表結點。在鏈表處理時,將node傳給鏈表處理函數。而在引用用戶數據時,通過計算node和data的地址偏差,獲得data的實際地址。具體的技巧如下:
UserDataActual* p_data = (UserDataActual*)(((char*)node) - (int)(&(((UserData*)0)->node)) + (int)(&(((UserData*)0)->data)));
可以看出,這種實現方式對node在UserData中出現的位置也沒有什么額外的要求,有很好的靈活性。
同步鎖
read-write lock、RCU lock、spin lock
內核模塊的參數
用戶模塊可以通過main函數傳遞命令行參數。而內核模塊也有類似的用法:
insmod module.ko [param1=value param2=value ...]
為了使用這些參數的值,要在模塊中聲明變量來保存它們,并在所有函數之外的某個地方使用宏MODULE_PARM(variable, type)和MODULE_PARM_DESC(variable, description)來接收它們。
IO操作
readb 從 I/O 讀取 8 位數據 ( 1 字節 )
readw 從 I/O 讀取 16 位數據 ( 2 字節 )
readl 從 I/O 讀取 32 位數據 ( 4 字節 )
writeb(), writew(), writel()也是類似的。
IO操作之所以用宏實現,是由于這是和具體機器相關的操作,有的甚至要用到匯編來實現。從計算機體系結構來說,IO空間可以和內存空間屬于同一個地址空間,這樣就無需特殊的指令,直接使用C語言的賦值語句即可達到效果。IO空間也可以和內存空間采用不同的地址空間(比如x86就是這樣的),這時就需要特殊處理了。
內核模塊的條件編譯
內核代碼除了可以采用C語言的預處理命令,進行條件編譯之外。還可以在.o文件一級,實現條件編譯。
例如,在Kbuild系統的Makefile中:
obj-y += foo.o
該例子告訴Kbuild在這目錄里,有一個名為foo.o的目標文件。foo.o將從foo.c或foo.S文件編譯得到。obj-y表示編譯進內核,obj-m表示編譯成內核模塊。
將上面的例子稍微改一下:
obj-$(CONFIG_FOO) += foo.o
這里的$(CONFIG_FOO)可以為y(編譯進內核) 或m(編譯成模塊)。如果CONFIG_FOO不是y 和m,那么該文件就不會被編譯聯接了。通過控制$(CONFIG_FOO)的值,即可實現.o文件一級的條件編譯。
GPIO
GPIO相對來說是最簡單的一類驅動,代碼在drivers/gpio文件夾下。
從硬件來說,GPIO有兩種輸出模式:
push-pull模式電平轉換速度快,但是功耗相對會大些。
open-drain模式功耗低,且同時具有“線與”的功能。(同時注意GPIO硬件模塊內部是否有上拉電阻,如果沒有,需要硬件電路上添加額外的上拉電阻)
可在系統的/sys/class/gpio路徑下查看當前的GPIO設備。
參考文獻:
http://blog.csdn.net/mirkerson/article/details/8464290
http://www.cnblogs.com/lagujw/p/4226424.html
http://www.aichengxu.com/view/18529
http://wenku.baidu.com/view/a6c4b6bfc77da26925c5b001.html?re=view
從gpio_set_value到寄存器操作
include/linux/gpio.h: gpio_set_value
include/asm-generic/gpio.h: __gpio_set_value
drivers/gpio/gpiolib.c: gpiod_set_raw_value
這里會調用gpio_chip結構的set函數指針。我們只需要在定義gpio_chip結構的時候,將寄存器操作函數設置到set函數指針中即可。gpio_chip結構可在模塊初始化階段,使用gpiochip_add函數添加到系統中。
輸入事件處理
1.輪詢方式
drivers/input/keyboard/gpio_keys_polled.c中提供了輪詢方式的輸入事件處理。該實現主要是注冊了一個名為gpio-keys-polled的input_polled_dev。而input_polled_dev則利用queue_delayed_work實現任務的調度。
2.中斷方式
drivers/input/keyboard/gpio_keys_polled.c中提供了中斷方式的輸入事件處理。該實現最終調用了request_irq注冊中斷處理函數。
無論是輪詢還是中斷,處理的結果最終都會調用input_event函數生成應用層可訪問的事件。
代碼的編譯配置在:
Device Drivers ---> Input device support ---> Keyboards ---> <*> GPIO Buttons <*> Polled GPIO buttonsGPIO的使用方向
除了gpio_direction_input和gpio_direction_output之外,devm_gpio_request_one和gpio_request_one也可用于設置使用方向,只要設置好flag參數即可。內核中的leds-gpio和gpio-keys模塊都是使用后面的方法設置使用方向的。
此外,在board級的GPIO實現中,需要注意以下幾點:
1.是否有單獨的寄存器用于設置使用方向。這個問題與具體的硬件有關,有的硬件可根據賦值語句的方向,自動切換GPIO的使用方向。
2.如果有單獨的設置使用方向的寄存器的話,需要在gpio_set_value和gpio_get_value函數的實現中,將使用方向的設置操作添加進去。I2C的algo代碼并不會在set或著get操作時,修改GPIO的使用方向。
active_low
active_low的設置要根據硬件的連接,如果按下按鍵為高電平那么active_low=0,如果按下按鍵為低電平那么active_low=1.如果這個參數搞錯了,按鍵松開后就不斷發按鍵鍵碼,表現為屏幕上亂動作。
也因為active_low的存在,input_event返回的value實際上并不是GPIO的值,1表示按鍵按下,0表示按鍵抬起。
內核重啟
include/reboot.h里總有一個函數可以滿足你的需要。
總結
以上是生活随笔為你收集整理的linux内核研究(一)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 有用的网址集合, IT杂谈
- 下一篇: 从版本库看开源项目的发展史