作者:姜亞華(@二如公子 ),《精通 Linux 核心——智慧裝置開發核心技術》的作者,一直從事與 Linux 內核和 Linux 程式設計相關的工作,研究核心程式碼十多年,對多數模組的細節如數家珍。曾負責華為手機 Touch、Sensor 的驅動和軟體最佳化(包括 Mate、榮耀等系列),以及 Intel 安卓平臺 Camera 和 Sensor 的驅動開發(包括 Baytrail、Cherrytrail、Cherrytrail CR、Sofia 等)。現負責 DMA、Interrupt、Semaphore 等模組的最佳化與驗證(包括 Vega、Navi 系列和多款 APU 產品)。
往期回顧:
Java 離內核有多遠?
在上一期內容中,我們介紹了從 JVM 到內核的編譯原理,告訴大家應用和系統工程師如何接觸到核心。本文將從一個簡單的底層硬體模組入手,一步步教大家如何梳理核心程式碼。適合精力集中在核心,不太需要關心使用者空間的工程師,比如驅動工程師、嵌入式工程師等,以及想往這方面學習發展的朋友。
Ubuntu (lsb_release -a) | Distributor ID: Ubuntu Description: Ubuntu 19.10 Release: 19.10 |
Linux (uname -a) | Linux yahua 5.5.5 #1 SMP … x86_64 x86_64 x86_64 GNU/Linux |
在往期的訪談中,我們討論過如何閱讀核心程式碼,在這裡按照之前討論的思路詳細擴充套件下。
在 drivers/input/keyboard 下面的檔案是鍵盤驅動,我們選擇 lm8333.c 吧(沒什麼特殊理由,其他的也可以)。
找到 module_init,xxx_init,module_xxx,這些就是模組(驅動也是一種模組)的入口(進階點 1,系統的啟動過程),lm8333.c 內對應的是 module_i2c_driver(lm8333_driver),註冊 driver。
lm8333_driver 定義如下:
static struct i2c_driver lm8333_driver = {
.driver = {
.name = "lm8333",
},
.probe = lm8333_probe,
.remove = lm8333_remove,
.id_table = lm8333_id,
};
驅動和裝置匹配後,會回撥 probe(進階點 2,Linux Device Driver,LDD),也就是 lm8333_probe,它的關鍵程式碼如下:
static int lm8333_probe(struct i2c_client *client, const struct i2c_device_id *id) //1
{
const struct lm8333_platform_data *pdata = dev_get_platdata(&client->dev);
struct lm8333 *lm8333;
struct input_dev *input;
lm8333 = kzalloc(sizeof(*lm8333), GFP_KERNEL); //7
input = input_allocate_device(); //8
lm8333->client = client; //10
lm8333->input = input; //11
input->name = client->name; //13
input->dev.parent = &client->dev; //14
input->id.bustype = BUS_I2C; //15
input_set_capability(input, EV_MSC, MSC_SCAN); //16
err = matrix_keypad_build_keymap(pdata->matrix_data, …, input); //18
if (pdata->debounce_time) {
err = lm8333_write8(lm8333, LM8333_DEBOUNCE,
pdata->debounce_time / 3); //22
}
if (pdata->active_time) {
err = lm8333_write8(lm8333, LM8333_ACTIVE,
pdata->active_time / 3); //27
}
err = request_threaded_irq(client->irq, NULL, lm8333_irq_thread,
IRQF_TRIGGER_FALLING | IRQF_ONESHOT,
"lm8333", lm8333); //32
err = input_register_device(input); //34
i2c_set_clientdata(client, lm8333); //36
return 0;
}
probe 的任務是驅動的初始化和設定,初學階段,並不需要每一行程式碼都深入學習,可以先嚐試將程式碼分類,以 lm8333_probe 為例。
第 1 行,函式的引數型別是固定的,背後是 LDD。
第 7 行,申請記憶體,背後是記憶體管理。暫且把它當成c語言的 malloc 也無妨。
第 8/13~16/18/34,input 相關,背後是 input 子系統。
第 22/27 行,寫暫存器,背後是 i2c 匯流排。
第 32 行,request_threaded_irq,背後是中斷處理。
這些背後的機制每一個都是一個進階點。
初始化完畢,中斷產生後,會呼叫 request_threaded_irq 時傳遞的 lm8333_irq_thread,繼續梳理它的邏輯。
static irqreturn_t lm8333_irq_thread(int irq, void *data)
{
struct lm8333 *lm8333 = data;
u8 status = lm8333_read8(lm8333, LM8333_READ_INT);
if (!status)
return IRQ_NONE;
if (status & LM8333_ERROR_IRQ) {
//省略
}
if (status & LM8333_KEYPAD_IRQ)
lm8333_key_handler(lm8333);
return IRQ_HANDLED;
}
可以看到 lm8333_irq_thread 先讀暫存器來判斷產生中斷的原因,是 ERROR 還是 KEYPAD,如果是後者,呼叫 lm8333_key_handler。
static void lm8333_key_handler(struct lm8333 *lm8333)
{
struct input_dev *input = lm8333->input;
u8 keys[LM8333_FIFO_TRANSFER_SIZE];
u8 code, pressed;
int i, ret;
ret = lm8333_read_block(lm8333, LM8333_FIFO_READ,
LM8333_FIFO_TRANSFER_SIZE, keys);
for (i = 0; i < LM8333_FIFO_TRANSFER_SIZE && keys[i]; i++) {
pressed = keys[i] & 0x80;
code = keys[i] & 0x7f;
input_event(input, EV_MSC, MSC_SCAN, code);
input_report_key(input, lm8333->keycodes[code], pressed);
}
input_sync(input);
}
lm8333_key_handler 讀暫存器,然後根據暫存器的值判斷實際的按鍵,呼叫 input_report_key 報告資料。
好了,lm8333.c 的邏輯我們清楚了:初始化、設定中斷、讀取資料並 report。
我們從 lm8333 的硬體角度看看,它是一個比較簡單的晶片,datasheet(資料手冊,下載地址)也並不複雜,摘取其中一段。
n ACCESS.bus (I2C-compatible) communication interface to the hostn Four general purpose host programmable I/O pins with two optional (slow) external Interruptsn 16 byte FIFO buffer to store key pressed and key released eventsn Host programmable active time and debounce time
相容 i2c 匯流排,支援中斷,16 位元組的 buffer,主機可程式設計有效時間和去抖時間。
再看看暫存器(這個文件稱之為 command)表。
CMD | Data Bits | Description |
0x20 FIFO_READ | 128 | Read an event from the FIFO. Maximum 14 event codes stored in the FIFO. MSB = 1: key pressed. MSB = 0: key released. |
0x22 DEBOUNCE | 8 | Default is 10 ms. Valid range 1255. Time ~ n x 4 ms |
… | … | … |
再看看程式碼裡出現的 i2c 讀寫的地址 LM8333_DEBOUNCE(0x22)和 LM8333_FIFO_READ(0x20)這些,這個表就是依據。
驅動做的事情可以分為兩個方面,一方面是處理晶片本身的邏輯,比如中斷、i2c、暫存器和時序等;另一方面是系統方面的,驅動和裝置匹配、中斷處理、資料傳遞(報告)等。
lm8333 比較簡單,但複雜的晶片多如牛毛,所以驅動工程師也可以分為兩類,一類比較專注於晶片本身的邏輯,另一類游到內核的大海中去了。
複雜的晶片本身就是一個完整的系統,成千上萬的暫存器,錯綜複雜的模組,能將這些弄清楚也是有很大挑戰的。除此之外,複雜的晶片很多都有配套的軟體架構,比如 ISP(Camera)相關的 V4L2(Video For Linux 2),GPU 相關的 DRM(Direct Rendering Manager)。
很明顯,晶片本身的邏輯並不是本文的重點,我們更關心如何游到核心。
再看 lm8333.c,大概清楚它的主要流程後,我們基本就算脫離新手村了,就像網遊一樣可以進入到下一階副本了。回憶一下,在新手村,我們只需要識別出哪些函式屬於其他模組,瞭解它們的基本原理,熟悉本身模組的邏輯即可。
進入第二個階段,最好先從與日常工作關係最密切的模組入手。比如 lm8333,連線在 i2c 匯流排上,獲取資料後透過 input 子系統 report,就可以從 i2c 和 input 入手。
學習 i2c 的過程中,還要解決 i2c 匯流排和 lm8333 的關係,這就涉及到 LDD。
深入 input 子系統的過程中,如果你對使用者空間得到資料的過程感興趣,就涉及到檔案系統、poll/epoll 等。
當然了,在這個階段,最好還是把檔案系統這些複雜的模組當作黑盒。小碎步前進,不斷有收穫。
稍微複雜些的驅動可能還會有電源管理、工作佇列和等待佇列等機制,也可以在這個階段內梳理它們的原理,至於它們背後的程序管理這些也可以先放放。
有了這一身裝備,應付副本里的小 BOSS 也綽綽有餘了,相比新手村那會也更有成就感,可以仗劍天涯了。
第三個階段就是解決之前遺留的疑問了,將記憶體管理、檔案系統和程序管理等一一拿下,比如 lm8333_probe 呼叫的 kzalloc、input 子系統涉及的 sysfs 檔案系統、工作佇列和中斷處理相關的程序排程,一步步深入挖掘。
在之前的問答活動裡我曾說過,“我已經把自己看過的程式碼的截圖放在隨書資料中了,算是一小段捷徑吧。這些截圖裡面,某函式、它呼叫的函式等函式呼叫關係使用紅線標示(如下圖),內容包括記憶體管理、檔案系統和程序管理三大模組。”
這些截圖是隨書資料,但並不是光碟那種。想要獲取資料的朋友歡迎在下面評論留言,或者郵件(linux_kernel_os@163.com)聯絡我,有任何疑問也可以找我共同探討。
往期回顧:
Java 離內核有多遠?
雲端計算時代,容器底層 cgroup 如何使用
雲端計算時代,容器底層 cgroup 的程式碼實現分析
雲端計算時代,容器底層 cgroup 如何實現資源分組?
[admin
]