为了讲清楚这个例子,我们需要先做一些知识铺垫。

当然我们这里讲Linux驱动中的这个案例,实际上也是在为后面Linux驱动的课程打基础,
有一定的知识铺垫后,对于大家后面学习驱动的课程,也是非常有帮助的


一、入口函数

1. 什么是入口函数

入口函数就是程序启动起来后,第一个被调用的函数,这个函数就是入口函数,对于我们C应用程序来说,
main 函数就是我们自己应用程序的入口函数,我们自己写的应用代码,就是从main这个入口开始执行的,
其它所有的子函数,都是靠这个入口函数来调用的

2. 入口函数的名字怎么来的

比如main这个名字怎么来的,通过第1章课程的学习我们都知道,main函数是由启动代码调用的,
所以入口函数叫什么名字,实际上是由启动代码决定的,启动代码规定这个名字应该叫xxxx
那我们写main函数时main就应该写为xxxx

对于C系的语言来说基本已经约定俗成,应用代码的入口函数大多都叫main,如果你写其它的名字,
编译时就直接提示你没有main入口函数,没有main函数的话,启动代码根本就没办法调用我们自己写的应用代码

3. 入口函数有没有不叫 main

还真有,比如win32图形界面编程,虽然图形库的接口函数都是c语言写的,
但是我们在写win32的图形界面程序时,它的入口函数就不叫main,而是叫WinMain

win32图形界面库是windows的基础图形界面库,windows上运行的c++、java、c#程序时,
它们的图形界面库基本都是基于win32图形界面库进行二次封装得到的,
实现图形界面时最终调用的还是windows图形界面库

不过这个情况在Linux这边就有所不同,这里我们不深究这个问题,只是顺带提下

4. 驱动程序的入口函数

我们这里主要讲的是Linux驱动程序,驱动程序写完之后是需要加入Linux内核,
成为Linux内核的一部分,加入后内核就可以调用驱动程序去控制我们的硬件了,为了能够调用驱动程序,
驱动程序必然也要有入口函数,以供Linux内核调用

应用程序(如果是C写的话,一般main是入口函数)
        |
        | OS API
        |
Linux 内核
        |
驱动程序(驱动入口函数)
        |
        |
硬件(通过读写寄存器去控制硬件)

Linux驱动程序的入口函数又叫什么名字呢?
不是一个固定的名字,名字是由我们驱动开发者自己给的,只要通过module_init(驱动入口函数名)向内核
提交这个名字,Linux内核就知道应该通过调用这个函数,去调用驱动程序了。

module_init是由谁提供的?
肯定是由Linux内核提供的,只有这样才能向Linux内核提交驱动的入口函数。

为什么允许Linux驱动的入口函数可以自己命名?
这样更人性化,你可以根据你自己所实现的驱动的用途不同,起一个更好识别的更贴切名字,可以更好的见
名识意思


二、module_init() 宏位置

module_init 是一个带参宏,只不过Linux内核把它写成了小写的,
不过我们说过,在我们在自己的代码中应该尽量将宏写成大写

为什么内核喜欢将好些带参宏写成小写?

就是想让你把它看成是一个函数,或者说就是希望你把它理解为一个函数,当成一个函数来用,
不过它其实是一个宏,但是我们还是建议,在我们自己的程序里面,宏尽可能的大写

module_init() 定义在了什么位置?

/include/linux/init.h中,我们在ubuntu下是找不到这个头文件的,因为这个是Linux内核源码文件,
ubunuLinux内核早就被编译好了,所以ubuntu下没有Linux的源码,所以找不到这个头文件

假如我写了一个鼠标驱动程序,入口函数叫mouse_device,现在想引用module_init告诉内核,
驱动的入口函数是mouse_device,那应该怎么做呢?

很简单,引用这个带参宏来实现:

module_init(mouse_device)

三、分析 module_init(mouse_device)

这里我们要借助 Source Insight 编辑器来查看源码,大家根据网上资料了解下怎么使用这个编辑器

如果内核版本不一样,可能得到的结果就会不一样,比如一些 函数、宏定义找不到等情况,
所以我们这里以 Linux3.10 的内核代码为例,下载地址:https://www.kernel.org/

我们找到/include/linux/init.h文件,并打开它,在 268 行找到这个宏定义

268 #define module_init(x)    __initcall(x);

再根据前面的宏体找到在 214__initcall 的宏定义

214 #define __initcall(fn) device_initcall(fn)

以此类推,我们依次可以找到

268 #define module_init(x)    __initcall(x);

214 #define __initcall(fn) device_initcall(fn)

209 #define device_initcall(fn)        __define_initcall(fn, 6)

178 #define __define_initcall(fn, id) \
179     static initcall_t __initcall_##fn##id __used \
180     __attribute__((__section__(".initcall" #id ".init"))) = fn

我们把宏替换后,可以得到

module_init(mouse_device) 
__initcall(mouse_device) 
device_initcall(mouse_device)
__define_initcall(mouse_device, 6)
static initcall_t __initcall_##mouse_device##6 __used __attribute__((__section__(".initcall" #6 ".init"))) = mouse_device

其中 ## 是为了连接前后的,所以我们得到

module_init(mouse_device)

__initcall(mouse_device) 

device_initcall(mouse_device)

__define_initcall(mouse_device, 6)

static initcall_t __initcall_mouse_device6 __used __attribute__((__section__(".initcall" #6 ".init"))) = mouse_device

其中 initcall_t 是一个自定义的类型,这里暂时把 __used __attribute__((__section__(".initcall" #6 ".init"))) 去掉

最终经过几次宏替换后,我们就会得到:

static initcall_t __initcall_mouse_device6 = mouse_device

最终得到的表达式,其实就是把 mouse_device函数地址(这里的函数名代表的就是函数第一个地址),
把这个地址赋值给了自定义类型为initcall_t所定义的变量 __initcall_mouse_device6中去

其中 initcall_t 就是通过typedef自定义的类型,我们找到在 138 行中,他被定义为是一个函数指针

typedef int (*initcall_t)(void);

所以最终的表达式等价于

int (*__initcall_mouse_device6)(void) = mouse_device

四、__used, attribute 分析

现在重新分析我们前面忽略的:

__used __attribute__((__section__(".initcall" #id ".init")))

1. __used

我们查找到文件:

10 __used - Constant in compiler.h (include\linux) at line 233

__used宏:在/include/linux/compiler.h中,__used为空

2. attribute

__attribute__:这个是gcc编译器所支持的C关键字,与return int等一样,都是关键字

它表示要设置属性,具体设置什么属性,由后面的内容((__section__(".initcall" "6" ".init")))来决定

一般底层开发才会用到这个__attribute__关键字,如果你写的C程序只是一个应用程序的话,
基本是看不到这个关键字的,正是由于不常见,所以很多同学刚开始看到这个关键字的时候就蒙圈了,
因为大家大多学C的时候,写的都是应用程序,根本见不着这个关键字,所以不熟悉

有关这个关键字,我们后面的课程还会讲到

((__section__(".initcall" "6" ".init"))):代表具体要设置的属性

__section__:也是gcc支持的C关键字,基本只有底层开发才会用到这个c关键字,
这个关键字的作用是,用于说明你的“驱动入口函数的代码”放到.text中的某个位置

".initcall" "6" ".init" :说明具体放的位置
最简单的理解就是,表示放到内核二进制代码的.text中的.init中的.initcall中的第6个位置


为什么要把驱动的代码放到Linux内核代码.text中?

我们前面说过,驱动代码最后是要加入内核,成为内核代码一部分的,所以肯定是要放到内核代码的.text

如果我们不设置属性,指定入口函数具体放在.text的什么位置,
就会在.text中随机放,如果指定了,就会放到你指定的位置

为什么向Linux内核告诉驱动程序的入口函数时,要指定入口函数的代码在.text存放的位置呢?

作用有很多,比如,放到指定的位置,这个指定的位置对代码的访问权限会做相应限制,
有关这个问题,我们就到这里就不再深入,当后面Linux驱动课程涉及到后,我们再具体什么介绍

我们在写C应用程序,编译器编译时所有函数的代码放到.text中的什么位置,
这个是由编译器的链接脚本自行决定的,我们不需要指定位置

当然我们也可以自己在代码中加以指定,编译器编译时可以放到我们自己指定的位置,
但是在C应用程序中这么做意义不大,当然也正是由于这种用法很少见,
所以大家在看内核源码、碰到这类东西的时候,才会蒙圈


五、module_init()例子 总结

作用:

  1. 告诉内核,驱动程序的入口函数的地址
  2. 设置相应的属性

第一个:告诉内核,驱动程序的入口函数的地址

做法是定义一个函数指针变量,然后将入口函数地址保存到里面,
内核即可通过这个指针变量来调用,指针变量的名字中有一部分就是入口函数名字

static initcall_t __initcall_mouse_device6 = mouse_device

第二个:设置相应的属性

定义函数指针变量时,通过__used __attribute__((__section__(".initcall" "6" ".init")))设置相关属性


通过这个例子,我们可以看出,想要理解好module_init这个宏,不仅仅只是宏的问题,
还涉及到__attribute__等关键字的问题,不过如果你连最起码的宏这一关都过不了的话,你的分析讲无从下手

疑问:为什么内核搞得这么复杂?
引用module_init时,把module_init(mouse_device)直接换成如下写法也是可以,

static initcall_t __initcall_mouse_device6 __used \
__attribute__((__section__(".initcall" "6" ".init"))) = mouse_device

不过这个写法显然不够性化,Linux内核必须给提供最简单的接口,比如module_init,以方便驱动开发者使用


六、其它例子

例子:使用宏对了类型进行自定义命名

#define INT32_t int
INT32 a;

#define U32_t   usigned int 
U32_t a;

#define STUDENT_t struct info_student; 
STUDENT stu;

不过对类型自定义命名,最好还是使用typedef来实现,因为宏只是简单的替换,
如果使用不当的话,这种简单替换会导致bug,有关这个问题,我们后面讲typedef时再来对比介绍

不过使用宏这种方式来实现类型自定义命名方式,冷不丁的在有些源码中还是会看见的,
特别是在好些单片机的程序中,这种方式还是挺多的,所以这里要了解下

2.5.5 例子4:
(1)offsetof

#define offsetof(type, member) (size_t)&(((type*)0)->member)

(2)container_of

#define container_of(ptr, type, member) ({ \
    const typeof( ((type *)0)->member ) *__mptr = (ptr); \
    (type *)( (char *)__mptr - offsetof(type,member) );})

不过这两个宏分析起来就没有那么容易了,我们将这两个例子举出来,仅仅只是想让你知道,
宏这个东西在实际开发中的各种源码中经常看见,而且往往还比较古怪,
所以希望大家能够学会适应宏的存在,学会阅读宏,理解宏

由于这两个宏与结构体相关,所以有关这两个宏的分析,我们就留在讲结构体时再来分析,
其实分析的方法还是一样的,只是稍微麻烦些