Linux内核是如何巧妙的初始化各个模块的

osc_lc29nall 2021-01-21 12:32:08
inet


相信很多在研究linux内核源码的同学,经常会发现一些模块的初始化函数找不到调用者,比如下面的网络模块的初始化函数:

// net/ipv4/af_inet.c
static int __init inet_init(void)
{
        ...
/*
* Set the IP module up
         */
ip_init();
/* Setup TCP slab cache for open requests. */
tcp_init();
/* Setup UDP memory threshold */
udp_init();
...
}
fs_initcall(inet_init);

即使你在整个内核代码中搜索,也找不到任何地方调用这个函数,那这个函数到底是怎么调用的呢?

秘密就在这个函数之后的一行代码里:

fs_initcall( inet_init);

在该行代码中,fs_initcall是一个宏,具体定义如下:

// include/linux/init.h
#define ___define_initcall(fn, id, __sec) \
static initcall_t __initcall_##fn##id __used \
__attribute__((__p__(#__sec ".init"))) = fn;
...
#define __define_initcall(fn, id) ___define_initcall(fn, id, .initcall##id)
...
#define fs_initcall(fn) __define_initcall(fn, 5)

在该宏展开后,上面宏调用的结果,大致像下面这个样子:

static initcall_t __initcall_inet_init5 __attribute__((__p__(".initcall5.init"))) = inet_init;

由上可见,fs_initcall宏最终是定义了一个静态变量,该变量的类型是initcall_t,值是宏参数表示的函数地址。

initcall_t类型的定义如下:

typedef int (*initcall_t)(void);

由上可见,initcall_t是一个函数指针类型,它定义的变量会指向一个函数,该函数的参数要为空,返回类型要为int。

我们可以再看下上面的 inet_init 方法,该方法确实符合这些要求。

综上可知,fs_initcall宏定义了一个变量 __initcall_inet_init5,它的类型为initcall_t,它的值为inet_init函数的地址。

到这里我相信很多同学会想,linux内核一定是通过这个变量来调用inet_init函数的,对吗?

对,也不对。

对是因为内核确实是通过该变量指向的内存来获取inet_init方法的地址并调用该方法的。

不对是因为内核并不是通过上面的__initcall_inet_init5变量来访问这个内存的。

那不用这个变量,还能通过其他方式访问这个内存吗?

当然可以,这正是linux内核设计的巧妙之处。

我们再来看下上面的宏展开之后,静态变量__initcall_inet_init5的定义,在该定义中有如下的一些代码:

__attribute__((__p__(".initcall5.init")))

该部分代码并不属于c语言标准,而是gcc对c语言的扩展,它的作用是声明该变量属于 .initcall5.init这个p。

所谓p,我们可以简单的理解为对程序所占内存区域的一种布局和规划,比如我们常见的 p有 .text用来存放我们的代码,.data或.bss用来存放我们的变量。

通过这些p的定义,我们可以把程序中的相关功能放到同一块内存区域中,这样来方便内存管理。

除了这些默认的p之外,我们还可以通过gcc的attribute来自定义p,这样我们就可以把相关的函数或变量放到相同的p中了。

比如上面的__initcall_inet_init5变量就属于.initcall5.init这个自定义p。

在定义了这些p之后,我们可以在链接脚本中告诉linker,这些p在内存中的位置及布局是什么样子的。

对于x86平台来说,内核的链接脚本是:

arch/x86/kernel/vmlinux.lds.S

在该脚本中,对.initcall5.init等这些p做了相关定义,具体逻辑如下:

// include/asm-generic/vmlinux.lds.h
#define INIT_CALLS_LEVEL(level) \
__initcall##level##_start = .; \
KEEP(*(.initcall##level##.init)) \
KEEP(*(.initcall##level##s.init)) \

#define INIT_CALLS \
__initcall_start = .; \
KEEP(*(.initcallearly.init)) \
INIT_CALLS_LEVEL(0) \
INIT_CALLS_LEVEL(1) \
INIT_CALLS_LEVEL(2) \
INIT_CALLS_LEVEL(3) \
INIT_CALLS_LEVEL(4) \
INIT_CALLS_LEVEL(5) \
INIT_CALLS_LEVEL(rootfs) \
INIT_CALLS_LEVEL(6) \
INIT_CALLS_LEVEL(7) \
__initcall_end = .;

由上可见,initcall相关的p有很多,我们上面例子中的.initcall5.init只是其中一个,除此之外还有 .initcall0.init,.initcall1.init等等这些p。

这些p都是通过宏INIT_CALLS_LEVEL来定义其处理规则的,相同level的p被放到同一块内存区域,不同level的p的内存区域按level大小依次连接在一起。

对于上面的__initcall_inet_init5变量来说,它的p是.initcall5.init,它的level是5。

假设我们还有其他方法调用了宏fs_initcall,那该宏为该方法定义的静态变量所属的p也是.initcall5.init,level也是5。

由于该变量和__initcall_inet_init5变量所属的initcall的level都相同,所以它们被连续放在同一块内存区域里。

也就是说,这些level为5的静态变量所占的内存区域是连续的,又因为这些变量的类型都为initcall_t,所以它们正好构成了一个类型为initcall_t的数组,而数组的起始地址也在INIT_CALLS_LEVEL宏中定义了,就是__initcall5_start。

如果我们想要调用这些level为5的initcall,只要先拿到__initcall5_start地址,把其当成元素类型为initcall_t的数组的起始地址,然后遍历数组中的元素,获取该元素对应的函数指针,就可以通过该指针调用对应的函数了。

来看下具体代码:

// init/main.c
extern initcall_entry_t __initcall_start[];
extern initcall_entry_t __initcall0_start[];
extern initcall_entry_t __initcall1_start[];
extern initcall_entry_t __initcall2_start[];
extern initcall_entry_t __initcall3_start[];
extern initcall_entry_t __initcall4_start[];
extern initcall_entry_t __initcall5_start[];
extern initcall_entry_t __initcall6_start[];
extern initcall_entry_t __initcall7_start[];
extern initcall_entry_t __initcall_end[];
static initcall_entry_t *initcall_levels[] __initdata = {
__initcall0_start,
__initcall1_start,
__initcall2_start,
__initcall3_start,
__initcall4_start,
__initcall5_start,
__initcall6_start,
__initcall7_start,
__initcall_end,
};
static void __init do_initcall_level(int level)
{
        initcall_entry_t *fn;
        ...
for (fn = initcall_levels[level]; fn < initcall_levels[level+1]; fn++)
do_one_initcall(initcall_from_entry(fn));
}
static void __init do_initcalls(void)
{
int level;
for (level = 0; level < ARRAY_SIZE(initcall_levels) - 1; level++)
do_initcall_level(level);
}

在上面的代码中,do_initcalls方法遍历了所有的合法level,对于每个level,do_initcall_level方法又调用了该level里所有函数指针指向的函数。

我们上面示例中的inet_init方法就属于level 5,也是在这里被调用到的。

linux内核就是通过这种方式来调用各个模块的初始化方法的,很巧妙吧。

最后我们再来总结下:

1. 在各模块的初始化方法之后,一般都会调用一个类似于fs_initcall(inet_init)的宏,该宏的参数是该模块的初始化方法的方法名。

2. 该宏展开后的结果是定义一个静态变量,该变量通过gcc的attribute来声明其所属的initcall level的p,比如inet_init方法对应的静态变量就属于.initcall5.init这个p。

3. 在linux的链接脚本里,通过INIT_CALLS_LEVEL宏告知linker,将属于同一level的所有静态变量放到连续的一块内存中,组成一个元素类型为initcall_t的数组,该数组的起始地址放在类似__initcall5_start的变量中。

4. 在内核的初始化过程中,会通过调用 do_initcalls方法,遍历各个level里的各个函数指针,然后调用该指针指向的方法,即各模块的初始化方法。

各个模块的初始化方法就是这样被调用的。

希望你喜欢。

(END)

相关阅读:深入浅出Linux内核模块

更多精彩,尽在"Linux阅码场",扫描下方二维码关注

版权声明
本文为[osc_lc29nall]所创,转载请带上原文链接,感谢
https://my.oschina.net/u/4321917/blog/4917855

  1. 【计算机网络 12(1),尚学堂马士兵Java视频教程
  2. 【程序猿历程,史上最全的Java面试题集锦在这里
  3. 【程序猿历程(1),Javaweb视频教程百度云
  4. Notes on MySQL 45 lectures (1-7)
  5. [computer network 12 (1), Shang Xuetang Ma soldier java video tutorial
  6. The most complete collection of Java interview questions in history is here
  7. [process of program ape (1), JavaWeb video tutorial, baidu cloud
  8. Notes on MySQL 45 lectures (1-7)
  9. 精进 Spring Boot 03:Spring Boot 的配置文件和配置管理,以及用三种方式读取配置文件
  10. Refined spring boot 03: spring boot configuration files and configuration management, and reading configuration files in three ways
  11. 精进 Spring Boot 03:Spring Boot 的配置文件和配置管理,以及用三种方式读取配置文件
  12. Refined spring boot 03: spring boot configuration files and configuration management, and reading configuration files in three ways
  13. 【递归,Java传智播客笔记
  14. [recursion, Java intelligence podcast notes
  15. [adhere to painting for 386 days] the beginning of spring of 24 solar terms
  16. K8S系列第八篇(Service、EndPoints以及高可用kubeadm部署)
  17. K8s Series Part 8 (service, endpoints and high availability kubeadm deployment)
  18. 【重识 HTML (3),350道Java面试真题分享
  19. 【重识 HTML (2),Java并发编程必会的多线程你竟然还不会
  20. 【重识 HTML (1),二本Java小菜鸟4面字节跳动被秒成渣渣
  21. [re recognize HTML (3) and share 350 real Java interview questions
  22. [re recognize HTML (2). Multithreading is a must for Java Concurrent Programming. How dare you not
  23. [re recognize HTML (1), two Java rookies' 4-sided bytes beat and become slag in seconds
  24. 造轮子系列之RPC 1:如何从零开始开发RPC框架
  25. RPC 1: how to develop RPC framework from scratch
  26. 造轮子系列之RPC 1:如何从零开始开发RPC框架
  27. RPC 1: how to develop RPC framework from scratch
  28. 一次性捋清楚吧,对乱糟糟的,Spring事务扩展机制
  29. 一文彻底弄懂如何选择抽象类还是接口,连续四年百度Java岗必问面试题
  30. Redis常用命令
  31. 一双拖鞋引发的血案,狂神说Java系列笔记
  32. 一、mysql基础安装
  33. 一位程序员的独白:尽管我一生坎坷,Java框架面试基础
  34. Clear it all at once. For the messy, spring transaction extension mechanism
  35. A thorough understanding of how to choose abstract classes or interfaces, baidu Java post must ask interview questions for four consecutive years
  36. Redis common commands
  37. A pair of slippers triggered the murder, crazy God said java series notes
  38. 1、 MySQL basic installation
  39. Monologue of a programmer: despite my ups and downs in my life, Java framework is the foundation of interview
  40. 【大厂面试】三面三问Spring循环依赖,请一定要把这篇看完(建议收藏)
  41. 一线互联网企业中,springboot入门项目
  42. 一篇文带你入门SSM框架Spring开发,帮你快速拿Offer
  43. 【面试资料】Java全集、微服务、大数据、数据结构与算法、机器学习知识最全总结,283页pdf
  44. 【leetcode刷题】24.数组中重复的数字——Java版
  45. 【leetcode刷题】23.对称二叉树——Java版
  46. 【leetcode刷题】22.二叉树的中序遍历——Java版
  47. 【leetcode刷题】21.三数之和——Java版
  48. 【leetcode刷题】20.最长回文子串——Java版
  49. 【leetcode刷题】19.回文链表——Java版
  50. 【leetcode刷题】18.反转链表——Java版
  51. 【leetcode刷题】17.相交链表——Java&python版
  52. 【leetcode刷题】16.环形链表——Java版
  53. 【leetcode刷题】15.汉明距离——Java版
  54. 【leetcode刷题】14.找到所有数组中消失的数字——Java版
  55. 【leetcode刷题】13.比特位计数——Java版
  56. oracle控制用户权限命令
  57. 三年Java开发,继阿里,鲁班二期Java架构师
  58. Oracle必须要启动的服务
  59. 万字长文!深入剖析HashMap,Java基础笔试题大全带答案
  60. 一问Kafka就心慌?我却凭着这份,图灵学院vip课程百度云