《Linux内核修炼之道》精华分享与讨论(7)——分析内核源码如何入手?(下)

本文探讨了Linux内核中USB子系统的初始化过程,详细解析了subsys_initcall宏的作用及其实现方式,并介绍了如何利用模块参数定制内核行为。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

下面的分析,米卢教练说了,内容不重要,重要的是态度。就像韩局长对待日记的态度那样,严谨而细致。

只要你使用这样的态度开始分析内核,那么无论你选择内核的哪个部分作为切入点,比如USB,比如进程管理,在花费相对不算很多的时间之后,你就会发 现你对内核的理解会上升到另外一个高度,一个抱着情景分析,抱着0.1内核完全注释,抱着各种各样的内核书籍翻来覆去的看很多遍又忘很多遍都无法达到的高 度。请相信我!

让我们在Linux社区里发出号召:学习内核源码,从学习韩局长开始!

态度决定一切:从初始化函数开始
任小强们说房价高涨从现在开始,股评家们说牛市从5000点开始。他们的开始需要我们的钱袋,我们的开始只需要一台电脑,最好再有一杯茶,伴着几支小曲儿,不盯着钱总是会比较惬意的。生容易,活容易,生活不容易,因为总要盯着钱。

有了地图Kconfig和Makefile,我们可以在庞大复杂的内核代码中定位以及缩小了目标代码的范围。那么现在,为了研究内核对USB子系统的实现,我们还需要在目标代码中找到一个突破口,这个突破口就是USB子系统的初始化代码。

针对某个子系统或某个驱动,内核使用subsys_initcall或module_init宏指定初始化函数。在drivers/usb/core/usb.c文件中,我们可以发现下面的代码。

940 subsys_initcall(usb_init);
941 module_exit(usb_exit);

我们看到一个subsys_initcall,它也是一个宏,我们可以把它理解为module_init,只不过因为这部分代码比较核心,开发者们 把它看作一个子系统,而不仅仅是一个模块。这也很好理解,usbcore这个模块它代表的不是某一个设备,而是所有USB设备赖以生存的模块,Linux 中,像这样一个类别的设备驱动被归结为一个子系统。比如PCI子系统,比如SCSI子系统,基本上,drivers/目录下面第一层的每个目录都算一个子 系统,因为它们代表了一类设备。

subsys_initcall(usb_init)的意思就是告诉我们usb_init是USB子系统真正的初始化函数,而usb_exit() 将是整个USB子系统的结束时的清理函数。于是为了研究USB子系统在内核中的实现,我们需要从usb_init函数开始看起。

865 static int __init usb_init(void)
866 {
867   int retval;
868   if (nousb) {
869    pr_info("%s: USB support disabled/n", usbcore_name);
870    return 0;
871   }
872
873   retval = ksuspend_usb_init();
874   if (retval)
875    goto out;
876   retval = bus_register(&usb_bus_type);
877   if (retval)
878    goto bus_register_failed;
879   retval = usb_host_init();
880   if (retval)
881    goto host_init_failed;
882   retval = usb_major_init();
883   if (retval)
884    goto major_init_failed;
885   retval = usb_register(&usbfs_driver);
886   if (retval)
887    goto driver_register_failed;
888   retval = usb_devio_init();
889   if (retval)
890    goto usb_devio_init_failed;
891   retval = usbfs_init();
892   if (retval)
893    goto fs_init_failed;
894   retval = usb_hub_init();
895   if (retval)
896    goto hub_init_failed;
897   retval = usb_register_device_driver(&usb_generic_driver, THIS_MODULE);
898   if (!retval)
899    goto out;
900
901   usb_hub_cleanup();
902  hub_init_failed:
903   usbfs_cleanup();
904 fs_init_failed:
905   usb_devio_cleanup();
906 usb_devio_init_failed:
907   usb_deregister(&usbfs_driver);
908 driver_register_failed:
909   usb_major_cleanup();
910 major_init_failed:
911   usb_host_cleanup();
912 host_init_failed:
913   bus_unregister(&usb_bus_type);
914 bus_register_failed:
915   ksuspend_usb_cleanup();
916 out:
917   return retval;
918 } 

(1)__init标记。

关于usb_init,第一个问题是,第865行的__init标记具有什么意义?

写过驱动的应该不会陌生,它对内核来说就是一种暗示,表明这个函数仅在初始化期间使用,在模块被装载之后,它占用的资源就会释放掉用作它处。它的暗 示你懂,可你的暗示,她却不懂或者懂装不懂,多么让人感伤。它在自己短暂的一生中一直从事繁重的工作,吃的是草吐出的是牛奶,留下的是整个USB子系统的 繁荣。

受这种精神所感染,我觉得有必要为它说的更多些。__init的定义在include/linux/init.h文件里

43 #define __init          __attribute__ ((__section__ (".init.text")))

好像这里引出了更多的疑问,__attribute__是什么?Linux内核代码使用了大量的GNU C扩展,以至于GNU C成为能够编译内核的唯一编译器,GNU C的这些扩展对代码优化、目标代码布局、安全检查等方面也提供了很强的支持。而__attribute__就是这些扩展中的一个,它主要被用来声明一些特 殊的属性,这些属性主要被用来指示编译器进行特定方面的优化和更仔细的代码检查。GNU C支持十几个属性,section是其中的一个,我们查看GCC的手册可以看到下面的描述

‘section ("section-name")'
   Normally, the compiler places the code it generates in the `text'
 section. Sometimes, however, you need additional sections, or you
need certain particular functions to appear in special sections.
   The `section' attribute specifies that a function lives in a
   particular section. For example, the declaration:

     extern void foobar (void) __attribute__ ((section ("bar")));

   puts the function ‘foobar' in the ‘bar' section.

   Some file formats do not support arbitrary sections so the
   ‘section' attribute is not available on all platforms. If you
   need to map the entire contents of a module to a particular
   section, consider using the facilities of the linker instead.

通常编译器将函数放在.text节,变量放在.data或.bss节,使用section属性,可以让编译器将函数或变量放在指定的节中。那么前面 对__init的定义便表示将它修饰的代码放在.init.text节。连接器可以把相同节的代码或数据安排在一起,比如__init修饰的所有代码都会 被放在.init.text节里,初始化结束后就可以释放这部分内存。

问题可以到此为止,也可以更深入,即内核又是如何调用到这些__init修饰的初始化函数?要回答这个问题,还需要回顾一下subsys_initcall宏,它也在include/linux/init.h里定义

125 #define subsys_initcall(fn)             __define_initcall("4",fn,4)

这里又出现了一个宏__define_initcall,它用于将指定的函数指针fn放到initcall.init节里 而对于具体的subsys_initcall宏,则是把fn放到.initcall.init的子节.initcall4.init里。要弄清 楚.initcall.init、.init.text和.initcall4.init这样的东东,我们还需要了解一点内核可执行文件相关的概念。

内核可执行文件由许多链接在一起的对象文件组成。对象文件有许多节,如文本、数据、init数据、bass等等。这些对象文件都是由一个称为链接器 脚本的文件链接并装入的。这个链接器脚本的功能是将输入对象文件的各节映射到输出文件中;换句话说,它将所有输入对象文件都链接到单一的可执行文件中,将 该可执行文件的各节装入到指定地址处。 vmlinux.lds是存在于arch/<target>/ 目录中的内核链接器脚本,它负责链接内核的各个节并将它们装入内存中特定偏移量处。

我可以负责任的告诉你,要看懂vmlinux.lds这个文件是需要一番功夫的,不过大家都是聪明人,聪明人做聪明事,所以你需要做的只是搜索initcall.init,然后便会看到似曾相识的内容

__inicall_start = .;
.initcall.init : AT(ADDR(.initcall.init) – 0xC0000000) {
*(.initcall1.init)
*(.initcall2.init)
*(.initcall3.init)
*(.initcall4.init)
*(.initcall5.init)
*(.initcall6.init)
*(.initcall7.init)
}
__initcall_end = .;

这里的__initcall_start指向.initcall.init节的开始,__initcall_end指向它的结尾。而.initcall.init节又被分为了7个子节,分别是

.initcall1.init
.initcall2.init
.initcall3.init
.initcall4.init
.initcall5.init
.initcall6.init
.initcall7.init

我们的subsys_initcall宏便是将指定的函数指针放在了.initcall4.init子节。其它的比如core_initcall将 函数指针放在.initcall1.init子节,device_initcall将函数指针放在了.initcall6.init子节等等,都可以从 include/linux/init.h文件找到它们的定义。各个字节的顺序是确定的,即先调用.initcall1.init中的函数指针再调 用.initcall2.init中的函数指针,等等。__init修饰的初始化函数在内核初始化过程中调用的顺序和.initcall.init节里函 数指针的顺序有关,不同的初始化函数被放在不同的子节中,因此也就决定了它们的调用顺序。

至于实际执行函数调用的地方,就在/init/main.c文件里,内核的初始化么,不在那里还能在哪里,里面的do_initcalls函数会直接用到这里的__initcall_start、__initcall_end来进行判断。

 (2)模块参数。

关于usb_init函数,第二个问题是,第868行的nousb表示什么?

知道C语言的人都会知道nousb是一个标志,只是不同的标志有不一样的精彩,这里的nousb是用来让我们在启动内核的时候通过内核参数去掉 USB子系统的,Linux社会是一个很人性化的世界,它不会去逼迫我们接受USB,一切都只关乎我们自己的需要。不过我想我们一般来说是不会去指定 nousb的吧。如果你真的指定了nousb,那它就只会幽怨的说一句“USB support disabled”,然后退出usb_init。

nousb在drivers/usb/core/usb.c文件中定义为:

static int nousb; /* Disable USB when built into kernel image */
module_param_named(autosuspend, usb_autosuspend_delay, int, 0644);
MODULE_PARM_DESC(autosuspend, "default autosuspend delay");

 从中可知nousb是个模块参数。关于模块参数,我们都知道可以在加载模块的时候可以指定,但是如何在内核启动的时候指定?
打开系统的grub文件,然后找到kernel行,比如:

kernel  /boot/vmlinuz-2.6.18-kdb root=/dev/sda1 ro splash=silent vga=0x314

 其中的root,splash,vga等都表示内核参数。当某一模块被编译进内核的时候,它的模块参数便需要在kernel行来指定,格式为“模块名.参数=值”,比如:

 modprobe usbcore autosuspend=2

对应到kernel行,即为 :

 usbcore.autosuspend=2

 通过命令“modinfo -p ${modulename}”可以得知一个模块有哪些参数可以使用。同时,对于已经加载到内核里的模块,它们的模块参数会列举在/sys/module /${modulename}/parameters/目录下面,可以使用“echo -n ${value} > /sys/module/${modulename}/parameters/${parm}”这样的命令去修改。

 (3)可变参数宏。

 关于usb_init函数,第三个问题是,pr_info如何实现与使用?

pr_info只是一个打印信息的可辨参数宏,printk的变体,在include/linux/kernel.h里定义:

242 #define pr_info(fmt,arg...) /
243         printk(KERN_INFO fmt,##arg)

99年的ISO C标准里规定了可变参数宏,和函数语法类似,比如

#define debug(format, ...) fprintf (stderr, format, __VA_ARGS__)

里面的“…”就表示可变参数,调用时,它们就会替代宏体里的__VA_ARGS__。GCC总是会显得特立独行一些,它支持更复杂的形式,可以给可变参数取个名字,比如

#define debug(format, args...) fprintf (stderr, format, args)

有了名字总是会容易交流一些。是不是与pr_info比较接近了?除了‘##’,它主要是针对空参数的情况。既然说是可变参数,那传递空参数也总是可以的,空即是多,多即是空,股市里的哲理这里同样也是适合的。如果没有‘##’,传递空参数的时候,比如

debug ("A message");

展开后,里面的字符串后面会多个多余的逗号。这个逗号你应该不会喜欢,而‘##’则会使预处理器去掉这个多余的逗号。

 关于usb_init函数,上面的三个问题之外,余下的代码分别完成usb各部分的初始化,接下来就需要围绕它们分别进行深入分析。因为这里只是演示如何入手分析,展示的只是一种态度,所以具体的深入分析就免了吧。

1. 用户身体信息管理模块 用户信息管理: 注册登录:支持手机号 / 邮箱注册,密码加密存储,提供第三方快捷登录(模拟) 个人资料:记录基本信息(姓名、年龄、性别、身高、体重、职业) 健康目标:用户设置目标(如 “减重 5kg”“增肌”“维持健康”)及期望周期 身体状态跟踪: 体重记录:定期录入体重数据,生成体重变化曲线(折线图) 身体指标:记录 BMI(自动计算)、体脂率(可选)、基础代谢率(根据身高体重估算) 健康状况:用户可填写特殊情况(如糖尿病、过敏食物、素食偏好),系统据此调整推荐 2. 膳食记录食物数据库模块 食物数据库: 基础信息:包含常见食物(如米饭、鸡蛋、牛肉)的名称、类别(主食 / 肉类 / 蔬菜等)、每份重量 营养成分:记录每 100g 食物的热量(kcal)、蛋白质、脂肪、碳水化合物、维生素、矿物质含量 数据库维护:管理员可添加新食物、更新营养数据,支持按名称 / 类别检索 膳食记录功能: 快速记录:用户选择食物、输入食用量(克 / 份),系统自动计算摄入的营养成分 餐次分类:按早餐 / 午餐 / 晚餐 / 加餐分类记录,支持上传餐食照片(可选) 批量操作:提供常见套餐模板(如 “三明治 + 牛奶”),一键添加到记录 历史记录:按日期查看过往膳食记录,支持编辑 / 删除错误记录 3. 营养分析模块 每日营养摄入分析: 核心指标计算:统计当日摄入的总热量、蛋白质 / 脂肪 / 碳水化合物占比(按每日推荐量对比) 微量营养素分析:检查维生素(如维生素 C、钙、铁)的摄入是否达标 平衡评估:生成 “营养平衡度” 评分(0-100 分),指出摄入过剩或不足的营养素 趋势分析: 周 / 月营养趋势:用折线图展示近 7 天 / 30 天的热量、三大营养素摄入变化 对比分析:将实际摄入推荐量对比(如 “蛋白质摄入仅达到推荐量的 70%”) 目标达成率:针对健
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值