内核模块 VS 应用程序
起始与结束
应用程序一般从main()开始, 它执行一些指令, 再结束(返回值).
Every module must have an entry function and an exit function.
不同于应用程序, 模块从初始化函数开始, 这个初始化函数名可以是init_module(), 也可以
是你通过module_init宏注册的其他函数名. 该初始化函数被称为模块的入口函数.
入口函数的作用: 它告诉内核, 该模块提供了哪些功能, 并设置内核以便在需要这些功能的时候调用模块的函数. 这一过程完毕之后, 入口函数马上返回, 直到内核需要调用模块提供的函数时, 模块才开始被加载, 执行.
有入口, 就自然有出口.
出口函数可以是cleanup_module(), 也可以是你通过
module_exit宏注册的其他函数名. 出口函数"undo"入口函数执行的所有工作: 清除入口函数注册的模块功能.
可调用的函数
在应用程序或模块程序中都可调用程序本身为定义的函数. 不同的是:
- 应用程序可调用C库所提供的函数(比如libc提供的printf()).
- 模块程序只能调用内核所提供的函数(比如内核提供的printk()).
由于内核无法访问C库, 所以模块程序中无法调用C库中的函数. 好在内核本身提供了一些C库中的函数或者替代函数.
查看 /proc/kallsyms , 可直到内核输出了那些函数.
|
由于调用了函数本身未调用的外部函数, 所以应用程序和模块程序都需要解析这些外部函数, 它们也互不相同:
- 应用程序在linking阶段, linker解析外部函数, 将C库所提供的函数添加到对应位置.
- 模块程序在insmod时, insmod解析内核所提供的外部函数.
实际上, 上述的应用程序和模块程序的函数调用还是有联系的. 让我们来回忆一下库函数和系统调用的关系. 库函数可能最终调用一个系统调用(比如printf(), 会调用write()), 而系统调用实际上就是内核所输出的函数!
写一个hello, world的应用程序, 编译它, 并运行 $ strace ./hello 看看它调用了那些系统函数!
|
用户空间 VS 内核空间
用户程序运行于用户空间, 模块运行于内核空间.
内核的功能之一就是实现资源资源分配, 资源包括CPU, 内存, 硬件资源... 在执行一个程序时, 内核为程序分配资源, 为了便于管理资源分配, 内核划分了两个运行级别: 管理员模式和用户模式, 分别对应内核空间和用户空间.
这两个运行级别由CPU来实现, 比如: X86的CPU划分了4个运行级别, 这些级别以ring命名. Linux只使用其中的两个级别: 最高的为管理员模式(ring0), 最低的为用户模式(ring3).
用户模式与管理员模式的区别在于它们具有不同的内存映射: 用户模式只能运行于受保护的用户空间中,它所能访问的资源是受限的. 而管理员模式运行于内核空间, 它能访问所有资源.
以最终调用系统调用的库函数为例, 当库函数调用系统调用时, 运行级别切换到管理员模式, 即程序运行于内核空间. 此时"内核代应用程序执行某些功能". 当系统调用返回时, 切换回用户模式. 此时的进程上下文未变, 只是运行级别发生了改变.
实际上, 在Linux中, 处理器总是处于下列三个状态之一:
1, 在内核空间中, 处于进程上下文, 内核代进程执行.
2, 在内核空间中, 处于中断上下文, 运行中断处理程序, 此时没有对应的进程.
3, 在用户空间中, 执行进程中的用户代码. |
命名空间 ( Name Space)
不管是写用户程序还是些模块程序, 都要注意变量的命名.
用户程序:
写比较小的C程序时, 你可以选择你觉得方便的变量命名. 如果程序的规模比较大, 命名就要遵循一定的标准, 如果定义的全局变量中有重名的,
就会造成"命名空间污染 (namespace pollution). 所以在开发用户程序时要注意变量的命名问题: 最好遵循一定的标准,
避免C语言的保留字.
模块程序:
开发模块程序的时候更要注意变量命名: 因为所有的模块都要和整个内核连接.
最好的方法是给你模块代码中所有的变量加上static限定符, 并给你的符号(symbol)添加合适的前缀. (所有的内核前缀应该是小写!)
如果你不想声明static, 你也可以声明一个符号表, 并想内核注册它. 符号表后面再讨论.
/proc/kallsysm列出了内核知道的符号, 而且模块于内核共享代码空间, 所以你可以在模块程序中访问它们.
代码空间 ( Code Space)
要理解代码空间就要理解Linux的内存管理. 理解MM("memeory management", 或者"美妹"

) 难度比较大, 这里只涉及皮毛.
应用程序
当进程被创建时, 内核为该进程分配物理内存. 这些内存被进程用以存放执行代码, 变量, 堆栈... 该内存从0x00000000开始,
扩展到某个需要的地方. 每个进程只能访问一个内存空间, 任何两个进程的内存空间不能重叠. (这里不讨论访问另一个进程的地址空间的情况).
注意, 进程可见的地址和内存实际的物理地址是不相同的! 比如, 某个进程访问地址为0xbffff978的内存时,
它所访问的实际物理内存地址并不是该地址. 这都是内核的内存映射造成的. 它所访问的实际内存地址可能是以0xbffff978为名的一个索引,
它指向内存物理地址的某处.
内核
内核具有自己的内存空间. 由于内核能动态地加载到内核中或从内核中卸载,
模块与内核共享代码空间. 所以, 当模块程序段错误时, 即引发整个内核的段错误, 所以编写模块程序的时候要格外小心溢出引发段错误!