Linux 驅動架構簡析

這篇文章不是驅動開發教程,只不過做者讀過內核源碼後,想對知識作一個梳理,從源碼的角度分析一下Linux的驅動架構的實現。行文也不大講究,但願能夠把問題說清楚。本文使用的kernel 源碼版本是 3.13.3。node

學習kernel雖然沒有捷徑,可是有合理的方法。linux

1) 首先,須要熟悉操做系統的設計與實現,推薦你們看 MINIX做者的那部書,同時把MINIX的kernel代碼研讀一下。 否則,你不知道操做系統都有哪些模塊, 不知道操做系統要作些什麼事情,提供什麼功能。簡單地說,操做系統首先要驅動 CPU,而後提供那幾大管理(進程,內存,文件),實現一兩百個系統呼叫,提供驅動接口, 用戶態與內核之間進行切換。編程

2) 去intel的官網,找一下 ‘Intel® 64 and IA-32 Architectures Software Developer’s Manual’ , 瞭解一下 CPU的架構,工做模式,底層編碼。不然, 你不知道 gdt,ldt,page table,實地址,保護模式,定時器中斷都是什麼東西,爲何操做系統要這樣來設置寄存器。這塊基本上全彙編語言,對CPU的初始化,寄存器設置,手冊上面都有嚴格的時須要求。 哪些操做須要屏蔽中斷,哪些須要在一個指令週期完成等等。有了上面的基礎後,大概知道一個操做系統大概要作些什麼事情, 如何驅動底層的 CPU,這個時候閱讀 linux的kernel代碼,事半功倍。數據結構

kernel分爲兩個模塊:一個是core: cpu, 中斷,進程,內存幾大管理, 提供系統呼叫。另外一個是driver。 linux的driver 都是有架構的,不須要從底層作起。各種架構稱爲‘子系統’:如,block子系統,net子系統,usb子系統等。 別看操做系統的代碼量大,其實,driver佔了估計 80%的代碼量, 這些都是不須要去看的。驅動是否放在內核,就是微內核與宏內核的區別。架構

閱讀源碼過程當中,觀其大略便可,主要了解整個結構,以及程序的流程。如:系統呼叫的調用,追一個就能夠了—— 看看操做系統如何捕捉軟中斷, 根據中斷號,dispatch到相應的服務程序,如何保存現場, 完成後,又如何回到用戶態。 系統呼叫調用,核心就是 dispatch的流程。 追完一支系統呼叫,其它的大概就知道怎麼回事了。driver 也就同樣的, 找個簡單的驅動看看, 從驅動層一直到驅動的架構,流程清楚就能夠了。 如字符設備驅動, 追一下注冊後, 驅動框架如何把該設備放入 list,當有用戶請求的時候,它又如何查找到相應的設備,調用相應的操做函數, 一路追下來,流程大概就清楚了。app

不建議一開始就閱讀「linux內核源碼分析」 之類的書, 會讓讀者一頭霧水。 正確的方法應該是, 先了解相應的背景知識後,再來閱讀源碼。Driver 框架的源碼的位於 drivers/base/, 它是整個驅動模式的基礎框架,至關於OO語言的裏面的Object對象。 其實,Linux 的驅動框架就是一個OO的結構, core模塊定義數據結構,函數接口,實現各類通用的功能——至關於OO裏面的基類。各模塊的設備驅動程序則只須要實現 core模塊裏面定義的接口便可。框架

bus, driver, device 框架

linux的外圍設備驅動,都是經過 bus + driver + device來管理的,其實也好理解 ,外設都是經過總線來與cpu通信的。kernel會實現各類總線的規範以及設備管理(設備檢測,驅動綁定等),驅動程序只須要註冊本身的驅動,實現對設備的讀寫控制便可。函數

這類驅動一般是2個層次:總線子系統 + 驅動模塊,它的流程大概是:源碼分析

1) bus_register(xx)學習

kernel裏面的各bus子系統(如:serio, usb, pci, ...)會使用該函數來註冊本身。

2) driver_register(xx)

驅動模塊使用它來向總線系統註冊本身,這樣驅動模塊只須要關注相應driver接口的實現。一般,bus子系統會對 driver_register來進行封裝,如:

  • serio 提供serio_register_driver()
  • usb 提供usb_register_driver()
  • pci提供 pci_register_driver()

3) registe_device(xx)

各總線除了管理driver外,還管理device,一般會提供一支API來添加設備,如: input_register_device, serio_add_port.實現上都是經過一個鏈表對設備進行管理,一般是在初始化或者probe的時候, 添加設備。

設備(device)指的是具體實現總線協議的物理設備,如對serio總線而言,i8042就是它的一個設備,而該總線鏈接的設備(鼠標,鍵盤)則是一個serio driver。

註冊

bus.c 和 driver.c 分別對 bus,driver和device進行管理,提供註冊bus, driver和查找 device 功能。

bus_register(*bus) 這個函數會生成兩個list,用來保存設備和驅動。

INIT_LIST_HEAD(&priv->interfaces);
klist_init(&priv->klist_devices, klist_devices_get, klist_devices_put);
klist_init(&priv->klist_drivers, NULL, NULL);

* priv是 struct subsys_private定義在 driver/base/base.h

driver_register(*drv) 實際上就是調用 bus_add_driver(*drv) 把 drv 添加到 klist_drivers:

klist_add_tail(&priv->knode_bus, &bus->p->klist_drivers);

同理註冊device,也是經過 bus_add_device(*dev),添加到 klist_devices:

klist_add_tail(&dev->p->knode_bus, &bus->p->klist_devices);

以 hid_bus_type爲例,執行 bus_register(&hid_bus_type) 後, hid_bus_type->p->klist_devices 和 hid_bus_type->p->klist_klist_drivers 這兩個list 會被初始化,爲後面的 driver和 device 註冊作準備,driver數據結構以下:

static struct hid_driver tpkbd_driver = {
        .name = "lenovo_tpkbd",
        .id_table = tpkbd_devices,
        .input_mapping = tpkbd_input_mapping,
        .probe = tpkbd_probe,
        .remove = tpkbd_remove,
    };

註冊driver時,它先通過 __hid_register_driver(&tpkbd_driver),設置一些基本參數。

hdrv->driver.bus = &hid_bus_type;
.....    
driver_register(&hdrv->driver);

設置'driver.bus'字段後,driver和bus的對應關係就創建起來了。 而後, 通過 driver_register 後,hid_bus_type->p->list_drivers 保存了 tpkbd_driver.

Q: driver模塊是不知道 hid_driver 這個數據結構的,它如何能把它的指針放到list裏面呢?

答案是」不能」, list_drivers 是不能保存 hid_driver 指針的。driver模塊提供了一個接口: 'struct device_driver' , hid_driver 這個結構裏面須要包含該結構。

struct hid_driver {
              const struct hid_device_id *id_table;
                /* private: */  
                 struct device_driver driver;
      }

註冊的時候,取的是 driver 字段的地址,也就是 hid_driver.driver 的指針, driver_register(&hdrv->driver); 當從 driver模塊 callback 到 hid-core模塊的時候, 如

static int hid_bus_match(struct device *dev, struct device_driver *drv)
 {
         struct hid_driver *hdrv = container_of(drv, struct hid_driver, driver);
         struct hid_device *hdev = container_of(dev, struct hid_device, dev);

         return hid_match_device(hdev, hdrv) != NULL;
 }

使用 container_of 就把 hid_driver.driver 的指針轉換成了hid_driver 的指針--這個方法相似 OO編程裏面使用基類指針指向派生類對象。Linux普通使用這個方法,來構建框架。

device和driver綁定

當增長新device的時候,bus 會輪循它的驅動列表來找到一個匹配的驅動,它們是經過device id和 driver的id_table來進行 」匹配」的,主要是在 driver_match_device()[drivers/base/base.h] 經過 bus->match() 這個callback來讓驅動判斷是否支持該設備,一旦匹配成功,device的driver字段會被設置成相應的driver指針 :

really_probe()
{
    dev->driver = drv;
    if (dev->bus->probe) {
        ret = dev->bus->probe(dev);
        ...
    } else if (drv->probe) {
        ret = drv->probe(dev);
        ...
    }
}

而後 callback 該 driver 的 probe 或者 connect 函數,進行一些初始化操做。

同理,當增長新的driver時,bus也會執行相同的動做,爲驅動查找設備。所以,綁定發生在兩個階段:

1: 驅動找設備,發生在driver向bus系統註冊本身時候,函數調用鏈是:

driver_register --> bus_add_driver --> driver_attach() [dd.c] -- 將輪循device鏈表,查找匹配的device。

2: 設備查找驅動,發生在設備增長到總線的的時候,函數調用鏈是:

device_add --> bus_probe_device --> device_initial_probe --> device_attach -- 將輪循driver鏈表,查找匹配的driver。

匹配成功後,系統繼續調用 driver_probe_device() 來 callback 'drv->probe(dev)' 或者 'bus->probe(dev) -->drv->connect(),在probe或者connect函數裏面,驅動開始實際的初始化操做。所以,probe() 或者 connect() 是真正的驅動'入口'。

對驅動開發者而言,最基本是兩個步驟:

  • 定義device id table.
  • probe()或connect()開始具體的初始化工做。

clipboard.png

(driver和device註冊流程圖)

實例分析:atkbd鍵盤驅動

Serio Bus 主要是支持 PS/2,串口等串行設備協議,物理上能夠經過i8042控制芯片來鏈接PS/2的鼠標或鍵盤,它的架構是:

  • serio.c 實現總線框架。
  • serio_register_port 註冊底層讀寫設備-- port就是 serio的底層通信設備, 它執行serio總線的底層讀寫。
  • serio_register_driver 註冊驅動,與port進行綁定,利用port進行底層的讀寫通信。

atkbd 驅動註冊的時侯,需指定它支持的port類型。

serio->id.type        = SERIO_8042; //代表驅動須要8042的支持。

serio_register_driver()     // 註冊本身, 根據設備id綁定相應的 port(本例中是8042)。

做爲serio的port, i8042 經過 serio_register_port 來註冊,生成serio對象,這樣驅動程序就能夠經過 serio->wirte/read 來調用i8042進行底層的通信。 數據流程框圖以下:

clipboard.png

driver 經過 bus 匹配 port,經過port與外設通信。

咱們能夠在sys接口 [/sys/bus/serio/] 目錄下找到設備和驅動的相關信息,裏面的內容能夠經過 DEVICE_ATTR_XX 系列宏定義來添加。 port 的命令規則是serio0, serio1, serioN 是自動增長的。

dev_set_name(&serio->dev, "serio%ld", (long)atomic_inc_return(&serio_no) - 1);

atkbd註冊serio驅動後,還須要註冊input設備,它須要實現input子系統的接口,做爲一個input設備工做。input.c 定義了callback 和公用接口,子模塊實現相應接口。

keybord:  drivers/input/keyboard/atkbd.c

註冊設備
atkbd.c: atkbd_connect  → input_register_device()

Input子系統經過event 字符設備來與應用程序進行通信。

evdev.c: evdev_init → input_register_handler → ....
cdev_init(xx, fops) // 處理  /dev/input/eventX的讀寫操做。

atkdb經過註冊Serio Bus 驅動和Input device來打通從應用程序到外設通信鏈路:

application --> /dev/input/eventX --> Input 子系統 --> atkdb驅動 --> serio bus --> i8042 port --> 物理鍵盤。

咱們能夠看到字符設備主要是用來提供應用層接口,Bus框架則用來管理外設驅動。

Input提供了proc文件接口,能夠查看相應的信息。

cat /proc/bus/input/devices : 能夠獲得某個設備的event number.
cat /proc/bus/input/handlers

經過cat eventX能夠獲得按鍵產生input_event,查看 event裏面的原始數據

sudo cat /dev/input/eventXX | hexdump
XX: event number.

參考資料

1: Linux 3.13源碼。

2: Linux Device Drivers。


歡迎你們來個人網站交流:般若程序蟬
qrcode_258.jpg

相關文章
相關標籤/搜索