1.6.4 学习Linux内核的方法
学习Linux内核的最大工作就是对内核代码进行分析,如果抱着走马观花、得过且过的态度,最终结果很有可能是没有多大的收获。学习内核应该遵循科学、严谨的态度,要做到真正理解每一段代码的实现,并且在学习过程中要多问、多想、多记。
上述学习Linux内核的方法非常重要,接下来通过两个具体的应用来演示学习Linux内核的过程。
1.分析USB子系统的代码
Linux内核中USB子系统的代码位于drivers/usb目录下,进入该目录,执行命令ls后将会显示如下结果。
目录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中,可以发现以下代码。
在上述代码中,可以将subsys_initcall理解为module_init,只不过因为该部分代码比较核心,开发者们把它看作一个子系统,而不仅仅是一个模块。在Linux中,类似此类别的设备驱动被归结为一个子系统,如PCI子系统和SCSI子系统。通常drivers/目录下第一层的每个目录代表一个子系统,因为它们分别代表了一类设备。
subsys_initcall(usb_init)表示函数usb_init()是USB子系统的初始化函数,而module_exit则表示usb_exit函数是USB子系统结束时的清理函数。为了研究USB子系统在内核中的实现,需要从函数usb_init()开始分析,对应的内核代码如下。
接下来开始分析上述代码。
(1)标记__init
关于usb_init,第一个问题是上述第一行代码中的__init标记有什么意义?在前面讲解GCC扩展的特殊属性section时曾经提到,__init修饰的所有代码都会被放在.init.text节,当初始化结束后就可以释放这部分内存。但是内核是如何调用到__init所修饰的这些初始化函数的呢?为了回答这个问题,需要用到subsys_initcall宏的知识,它在文件include/linux/init.h中的定义格式如下。
此时出现了一个新的宏__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后便会看到以下结果。
其中__initcall_start指向.initcall.init节的开始,__initcall_end指向.initcall.init节的结尾。而.initcall.init节又被分为如下7个子节。
宏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文件中定义为如下格式。
从中可知nousb是个模块参数,用于在内核启动时禁止USB子系统。关于模块参数,可以在加载模块时可以指定,但是如何在内核启动时指定?打开系统的grub文件,然后找到kernel行,如下面的代码。
其中的root、splash、vga等都表示内核参数。当某一模块被编译进内核时,它的模块参数便需要在kernel行来指定,其格式为
如下面的代码。
对应到kernel行的代码如下。
通过命令modinfo -p /parameters/目录,可以使用以下命令去修改。
关于函数usb_init(),除了上面介绍的代码外,余下的代码分别完成usb各部分的初始化。其他代码的具体分析工作可以参阅下载Linux内核代码,具体含义可以参阅相关的书籍和资料。在此不再详细介绍。