![Android底层接口与驱动开发技术详解](https://wfqqreader-1252317822.image.myqcloud.com/cover/689/26178689/b_26178689.jpg)
1.6.4 学习Linux内核的方法
学习Linux内核的最大工作就是对内核代码进行分析,如果抱着走马观花、得过且过的态度,最终结果很有可能是没有多大的收获。学习内核应该遵循科学、严谨的态度,要做到真正理解每一段代码的实现,并且在学习过程中要多问、多想、多记。
上述学习Linux内核的方法非常重要,接下来通过两个具体的应用来演示学习Linux内核的过程。
1.分析USB子系统的代码
Linux内核中USB子系统的代码位于drivers/usb目录下,进入该目录,执行命令ls后将会显示如下结果。
![](https://epubservercos.yuewen.com/1E49FF/14615851904528606/epubprivate/OEBPS/Images/img00042001.jpg?sign=1739316029-cwDOPVgX05qfI7u4FRXL6qIAYGHeQSL8-0-4e809b017ace5508c0be48603cf78661)
目录drivers/usb包含10个子目录和4个文件,为了理解每个子目录的作用,有必要首先阅读README文件。根据README文件的描述,得知drivers/usb目录下各个子目录的作用,具体说明如下。
(1)core
core是内核开发者针对部分核心的功能特意编写的代码,用于为其他设备驱动程序提供服务,比如申请内存,实现一些所有的设备都会需要的公共函数,并命名为USB core。
(2)host
早期的内核结构并不像现在这样富有层次感,几乎所有的文件都直接堆砌在drivers/usb/目录下,其中包括usb core和其他各种设备驱动程序的代码。
后来在drivers/usb/目录下单独列出了core子目录,用于存放一些比较核心的代码,如整个USB子系统的初始化、root hub的初始化、host controller的初始化代码。
后来随着技术的发展,出现了多种USB host controller,于是内核开发者把host controller有关的公共代码保留在core目录下,而其他各种host controller对应的特定代码则移到host目录下,让相应的负责人去维护。为此,针对host controller单独创建子目录host,它用于存放与其相关的代码。
(3)gadget
gadget用于存放USB gadget的驱动程序,控制外围设备如何作为一个USB设备和主机通信。比如,嵌入式开发板通常会支持SD卡,使用USB连接线将开发板连接到PC时,通过USB gadget架构的驱动,可以将该SD卡模拟成U盘。
除core、host和gadget之外,其他几个目录分门别类地存放各种USB设备的驱动,如U盘的驱动位于storage子目录,触摸屏和USB键盘鼠标的驱动位于input子目录。
因为我们的目的是研究内核对USB子系统的实现,而不是特定设备或host controller的驱动,所以通过对README文件的分析,应该进一步关注core子目录。
2.分析USB系统的初始化代码
通过分析Kconfig和Makefile文件,可以用户在庞大复杂的内核代码中定位以及缩小目标代码的范围。为了研究内核对USB子系统的实现,需要在目标代码中找到USB子系统的初始化代码。
Linux内核针对某个子系统或某个驱动,使用subsys_initcall或module_init宏来指定初始化函数。在内核文件drivers/usb/core/usb.c中,可以发现以下代码。
![](https://epubservercos.yuewen.com/1E49FF/14615851904528606/epubprivate/OEBPS/Images/img00042002.jpg?sign=1739316029-fbGUk7JS1EePbEsnoUKjBAoib2X8IPw1-0-d4befe24e178e1351f74d622fd68a1eb)
在上述代码中,可以将subsys_initcall理解为module_init,只不过因为该部分代码比较核心,开发者们把它看作一个子系统,而不仅仅是一个模块。在Linux中,类似此类别的设备驱动被归结为一个子系统,如PCI子系统和SCSI子系统。通常drivers/目录下第一层的每个目录代表一个子系统,因为它们分别代表了一类设备。
subsys_initcall(usb_init)表示函数usb_init()是USB子系统的初始化函数,而module_exit则表示usb_exit函数是USB子系统结束时的清理函数。为了研究USB子系统在内核中的实现,需要从函数usb_init()开始分析,对应的内核代码如下。
![](https://epubservercos.yuewen.com/1E49FF/14615851904528606/epubprivate/OEBPS/Images/img00043001.jpg?sign=1739316029-7ugwOzoDcMMgFiU5EgFXTHLevYiclX7f-0-f4e11030f38de0066d2c6a0b5996609e)
![](https://epubservercos.yuewen.com/1E49FF/14615851904528606/epubprivate/OEBPS/Images/img00044001.jpg?sign=1739316029-r0Cuyj2H84sO4nLBIV2Fj2KgaLPERnyo-0-931aa41b0ef87600477365400a170270)
接下来开始分析上述代码。
(1)标记__init
关于usb_init,第一个问题是上述第一行代码中的__init标记有什么意义?在前面讲解GCC扩展的特殊属性section时曾经提到,__init修饰的所有代码都会被放在.init.text节,当初始化结束后就可以释放这部分内存。但是内核是如何调用到__init所修饰的这些初始化函数的呢?为了回答这个问题,需要用到subsys_initcall宏的知识,它在文件include/linux/init.h中的定义格式如下。
![](https://epubservercos.yuewen.com/1E49FF/14615851904528606/epubprivate/OEBPS/Images/img00044002.jpg?sign=1739316029-xSEXfRE6JZoBPYRQ2uKqUxFNoq66AecV-0-f84c82c385187b3ff44ca25aae82e205)
此时出现了一个新的宏__define_initcall,它用来将指定的函数指针fn存放到.initcall.init节。对于subsys_initcall宏,则表示把fn存放到.initcall.init的子节.initcall4.init。
为了理解.initcall.init、.init.text和.initcall4.init之类的符号,还需要了解和内核可执行文件相关的概念。内核可执行文件由许多链接在一起的对象文件组成。对象文件有许多节,如文本、数据、init数据、bass等。这些对象文件都是由一个称为链接器脚本的文件链接并装入的。这个链接器脚本的功能是将输入对象文件的各节映射到输出文件中。换句话说,它将所有输入对象文件都链接到单一的可执行文件中,将该可执行文件的各节装入指定地址处。vmlinux.lds是保存在arch/<target>/目录中的内核链接器脚本,它负责链接内核的各个节并将它们装入内存中特定偏移量处。
打开文件arch/i386/kernel/vmlinux.lds,搜索关键字initcall.init后便会看到以下结果。
![](https://epubservercos.yuewen.com/1E49FF/14615851904528606/epubprivate/OEBPS/Images/img00044003.jpg?sign=1739316029-mcjmS43K5ks8g9XVHBxJ7z5k6iVuEytO-0-6718599b58ed6819f9b49fb89d2ab8be)
其中__initcall_start指向.initcall.init节的开始,__initcall_end指向.initcall.init节的结尾。而.initcall.init节又被分为如下7个子节。
![](https://epubservercos.yuewen.com/1E49FF/14615851904528606/epubprivate/OEBPS/Images/img00044004.jpg?sign=1739316029-hY3m3MgDURHROQ0UuaOunXNkxLGytFJL-0-e44fa74bf1fc06b2d951ac3be340cb09)
宏subsys_initcall将指定的函数指针放在了.initcall4.init子节,至于其他宏的功能也类似,如core_initcall将函数指针放在了.initcall1.init子节,device_initcall将函数指针放在.initcall6.init子节等。
各个子节的顺序是确定的,即先调用.initcall1.init中的函数指针,然后调用.initcall2.init中的函数指针。__init修饰的初始化函数在内核初始化过程中调用的顺序和.initcall.init节里函数指针的顺序有关,不同的初始化函数被放在不同的子节中,因此也就决定了它们的调用顺序。
(2)模块参数
在前面usb_init函数()代码中,代码nousb在drivers/usb/core/usb.c文件中定义为如下格式。
![](https://epubservercos.yuewen.com/1E49FF/14615851904528606/epubprivate/OEBPS/Images/img00045001.jpg?sign=1739316029-WXK4L3VKTVvP3o4HtjVeyQjn4LQS4Ls9-0-795e1b2deb8282c77b73342edfd9d505)
从中可知nousb是个模块参数,用于在内核启动时禁止USB子系统。关于模块参数,可以在加载模块时可以指定,但是如何在内核启动时指定?打开系统的grub文件,然后找到kernel行,如下面的代码。
![](https://epubservercos.yuewen.com/1E49FF/14615851904528606/epubprivate/OEBPS/Images/img00045002.jpg?sign=1739316029-Sqil5Eu9OvVNd0GXv6EKAFFB9eC0tTeX-0-b82c36df14670942c3a18f5e1c6a152b)
其中的root、splash、vga等都表示内核参数。当某一模块被编译进内核时,它的模块参数便需要在kernel行来指定,其格式为
![](https://epubservercos.yuewen.com/1E49FF/14615851904528606/epubprivate/OEBPS/Images/img00045003.jpg?sign=1739316029-H1efcJvs0bQZTQHZmt9gH0nS1r5XWyqy-0-07b8c1c97f8f0fc3d7243f825a5c3747)
如下面的代码。
![](https://epubservercos.yuewen.com/1E49FF/14615851904528606/epubprivate/OEBPS/Images/img00045004.jpg?sign=1739316029-l5pciB2m8LEWG3o3al6D5AM3B8JslfBt-0-f311095f7a8d0513f2a3df85bed7cbbc)
对应到kernel行的代码如下。
![](https://epubservercos.yuewen.com/1E49FF/14615851904528606/epubprivate/OEBPS/Images/img00045005.jpg?sign=1739316029-lj7FJ6GewmdOJVoFqKCRa1GW1E1QNbYC-0-a767009fe0518908bdee4dd9c89ea8f0)
通过命令modinfo -p /parameters/目录,可以使用以下命令去修改。
![](https://epubservercos.yuewen.com/1E49FF/14615851904528606/epubprivate/OEBPS/Images/img00045006.jpg?sign=1739316029-KEEn8iXbvi3hrmoHkOXJgZnOP2z7NiZm-0-5ac89cc0730710629d57659e24a590d8)
关于函数usb_init(),除了上面介绍的代码外,余下的代码分别完成usb各部分的初始化。其他代码的具体分析工作可以参阅下载Linux内核代码,具体含义可以参阅相关的书籍和资料。在此不再详细介绍。