您现在的位置是:首页 > 文章详情

从新手村开始,手把手带你入门梳理内核代码

日期:2020-08-10点击:717
作者:姜亚华(@二如公子 ),《精通 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 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)表。

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 如何实现资源分组?

 

 

原文链接:https://www.oschina.net/question/2918182_2318001
关注公众号

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。

持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。

转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。

文章评论

共有0条评论来说两句吧...

文章二维码

扫描即可查看该文章

点击排行

推荐阅读

最新文章