从新手村开始,手把手带你入门梳理内核代码
喜欢就关注我们吧!
在上一期内容中,Java离Linux内核有多远?我们介绍了从 JVM 到内核的编译原理,告诉大家应用和系统工程师如何接触到内核。
本文将从一个简单的底层硬件模块入手,一步步教大家如何梳理内核代码。适合精力集中在内核,不太需要关心用户空间的工程师,比如驱动工程师、嵌入式工程师等,以及想往这方面学习发展的朋友。
1
初探内核
版本信息与往期一致:
在往期的访谈中,我们讨论过如何阅读内核代码,在这里按照之前讨论的思路详细扩展下。
在 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 host
n Four general purpose host programmable I/O pins with two optional (slow) external Interrupts
n 16 byte FIFO buffer to store key pressed and key released events
n Host programmable active time and debounce time
兼容 i2c 总线,支持中断,16 字节的 buffer,主机可编程有效时间和去抖时间。
再看看寄存器(这个文档称之为 command)表
再看看代码里出现的 i2c 读写的地址 LM8333_DEBOUNCE(0x22)和 LM8333_FIFO_READ(0x20)这些,这个表就是依据。
驱动做的事情可以分为两个方面,一方面是处理芯片本身的逻辑,比如中断、i2c、寄存器和时序等;另一方面是系统方面的,驱动和设备匹配、中断处理、数据传递(报告)等。
lm8333 比较简单,但复杂的芯片多如牛毛,所以驱动工程师也可以分为两类,一类比较专注于芯片本身的逻辑,另一类游到内核的大海中去了。
复杂的芯片本身就是一个完整的系统,成千上万的寄存器,错综复杂的模块,能将这些弄清楚也是有很大挑战的。
除此之外,复杂的芯片很多都有配套的软件架构,比如 ISP(Camera)相关的 V4L2(Video For Linux 2),GPU 相关的 DRM(Direct Rendering Manager)。
很明显,芯片本身的逻辑并不是本文的重点,我们更关心如何到内核。
再看 lm8333.c,大概清楚它的主要流程后,我们基本就算脱离新手村了,就像网游一样可以进入到下一阶副本了。回忆一下,在新手村,我们只需要识别出哪些函数属于其他模块,了解它们的基本原理,熟悉本身模块的逻辑即可。
2
进入第二个阶段,最好先从与日常工作关系最密切的模块入手。比如 lm8333,连接在 i2c 总线上,获取数据后通过 input 子系统 report,就可以从 i2c 和 input 入手。
学习 i2c 的过程中,还要解决 i2c 总线和 lm8333 的关系,这就涉及到 LDD。
深入 input 子系统的过程中,如果你对用户空间得到数据的过程感兴趣,就涉及到文件系统、poll/epoll 等。
当然了,在这个阶段,最好还是把文件系统这些复杂的模块当作黑盒。小碎步前进,不断有收获。
稍微复杂些的驱动可能还会有电源管理、工作队列和等待队列等机制,也可以在这个阶段内梳理它们的原理,至于它们背后的进程管理这些也可以先放放。
有了这一身装备,应付副本里的小 BOSS 也绰绰有余了,相比新手村那会也更有成就感,可以仗剑天涯了。
3
第三个阶段就是解决之前遗留的疑问了,将内存管理、文件系统和进程管理等一一拿下,比如 lm8333_probe 调用的 kzalloc、input 子系统涉及的 sysfs 文件系统、工作队列和中断处理相关的进程调度,一步步深入挖掘。
在之前的问答活动里我曾说过,“我已经把自己看过的代码的截图放在随书资料中了,算是一小段捷径吧。这些截图里面,某函数、它调用的函数等函数调用关系使用红线标示(如下图),内容包括内存管理、文件系统和进程管理三大模块。”
作者介绍:
姜亚华(@二如公子 ),《精通 Linux 内核——智能设备开发核心技术》的作者,一直从事与 Linux 内核和 Linux 编程相关的工作,研究内核代码十多年,对多数模块的细节如数家珍。曾负责华为手机 Touch、Sensor 的驱动和软件优化(包括 Mate、荣耀等系列),以及 Intel 安卓平台 Camera 和 Sensor 的驱动开发(包括 Baytrail、Cherrytrail、Cherrytrail CR、Sofia 等)。现负责 DMA、Interrupt、Semaphore 等模块的优化与验证(包括 Vega、Navi 系列和多款 APU 产品)Facebook 宣布:开源 Instagram 安全工具 Pysa张东升,我知道是你!如何使用GAN做一个秃头生产器
将会取代现有的开发人员?英特尔推出全新机器自动编程系统
谷歌与微软,勇士与恶龙的身份互换?Intel 20G 内部资料泄露,含源码、文档与培训视频等内容
觉得不错,请点个在看呀