Linux设备驱动程序 -- 构造和运行模块
内核模块与应用程序的不同
内核模块 | 应用程序 | |
---|---|---|
任务 | 大多数小规模及中规模的应用程序是从头到尾执行单个任务 | 模块只是先注册自己,以便服务于将来的某个请求,注册后初始化函数就立即结束 【模块初始化的任务就是为以后调用模块函数预先做准备;退出函数将在模块被卸载之前调用】 |
内存释放 | 应用程序在退出时,可以不管资源的释放或者其他的清除工作 | 模块的退出函数必须仔细撤销初始化函数所做的一切【否则,在系统重新引导之前,某些东西会一直残留在系统中】 |
函数调用 | 应用程序可以调用它并未定义的函数【因为链接过程能够解析外部引用,从而使用适当的函数库】 | 模块能调用的函数仅仅是由内核导出的函数,而不存在任何可链接的函数库【这是因为模块仅仅被链接到内核】 |
各环境下处理错误的方式 | 应用程序开发过程中的错误是无害的,并且总是可以使用调试器跟踪到源码中的问题所在 | 一个内核错误即时不影响整个系统,也至少会杀死当前进程 |
运行空间 | 用户空间 | 内核空间 |
并发 | 大部分应用程序(除了多线程应用程序之外),通常是按顺序执行的,从头到尾,而不需要关心因为其他一些事情的发生会改变它们的运行环境 | 内核代码并不在这样简单的环境中运行,即使是最简单的内核模块,都需要在编写时铭记:同一时刻,可能会有许多事情正在发生 |
用户空间和内核空间
模块运行在所谓的内核空间里,应用程序运行在所谓的用户空间中。 每个模式都有自己的内存映射,也即自己的地址空间。
操作系统的作用:
- 为应用程序提供一个对计算机硬件的一致视图
- 负责程序的独立操作并保护资源不受非法访问
- 以上任务只有在CPU能够保护系统软件不受应用程序破坏时才能完成。
在CPU中实现不同的操作模式/级别,不同级别具有不同功能,在较低的级别中将禁止某些操作。
- 内核运行在最高级别(也称作超级用户态),在这个级别中可以进行所有的操作。
- 应用程序运行在最低级别(即用户态),在这个级别中,处理器控制着对硬件的直接访问以及对内存的非授权访问。
模式切换:
- 每当应用程序执行系统调用或被硬件中断挂起时,Unix将执行模式从用户空间切换到内核空间。
- 执行系统调用的内核代码运行在进程上下文中,它代表调用进程执行操作,因此能够访问进程地址空间的所有数据。
- 处理硬件终端的内核代码和进程是异步的,与任何一个特定的进程无关。
模块化代码在内核空间中运行,用于扩展内核的功能。
一个驱动程序要执行的两类任务:
- 模块中的某些函数作为系统调用的一部分而执行
- 其他函数负责中断
内核中的并发
为什么要考虑并发问题
- Linux系统中通常正在运行多个并发进程,并且可能有多个进程同时会使用我们的驱动程序。
- 大多数设备能够中断处理器,而中断处理程序异步运行,并且可能在驱动程序正试图处理其他任务时被调用。
- 有一些软件抽象也在运行着。
- Linux可以运行在对称多处理器系统上,因此可能同时有不止一个CPU运行我们的启动程序。
- 在2.6的内核版本中,内核代码已经是可抢占的,这意味着即时在单处理器系统上也存在许多类似多处理器系统的并发问题。
Linux内核代码要求
- Linux内核代码(包括驱动程序代码)必须是可重入的,它必须能够同时运行在多个上下文中。
- 保证多个线程分开执行,访问共享数据的代码也必须避免破坏共享数据。
- 在处理并发问题时,同时需要避免竞态(竞态:不同的执行顺序导致不同的、非预期行为发生的情况)。
- 并发管理。
内核代码的当前进程
虽然内核模块不像应用程序那样顺序地执行,然而内核执行的大多数操作还是和某个特定的进程相关。
- 内核代码可以通过访问全局项current来获得当前进程。
- current在<asm.current.h>中定义,是一个指向struct task_struct的指针,而task_struct在<linux/sched.h>文件中定义。
- current指针指向当前正在运行的进程。
- 在open、read等系统调用的执行过程中,当前进程指的是调用这些系统调用的过程。
需要注意的其他细节
- 应用程序在虚拟内存中布局,并具有一块很大的栈空间。【栈是用来保存函数调用历史遗迹当前活动函数中的自动变量的】
- 内核具有非常小的栈,它可能只有一个页的大小。【我们的函数和整个内核空间调用链一同共享这个栈,因此,声明大的自动变量并不是一个好主意。(如果我们需要大的结构,应该在调用时动态分配该结构)】
编译和装载
编译模块
makefile – 环境搭建好了再学习、尝试
装载和卸载模块
装载
insmod
insmod程序和ld有些类似,它将模块的代码和数据装入内核,然后使用内核的符号表解析模块中任何未解析的符号。
与链接器不同:内核不会修改模块的磁盘文件,而仅仅修改内存中的副本。
rmmod
从内核中移除模块。
注意:如果内核认为模块仍然在使用状态(比如,某个程序正打开由该模块导出的设备文件),或者内核被配置为禁止移除模块,则无法移除该模块。
配置内核并使内核在模块忙的时候仍能“强制”移除也是可能的,但是后果需要考虑。该情况建议重新引导系统。【重启大法好o( ̄︶ ̄)o】
lsmod
lsmod程序列出当前装载到内核中的所有模块及其他的一些信息(比如其他模块是不是在使用某个特定模块等)。
lsmod通过读取/proc/modules虚拟文件来获得这些信息。
有关当前已装载模块的信息也可以在sysfs虚拟文件系统的/sys/modules下找到。
预备知识
头文件
- #include <Linux/module.h> //包含有可装载模块需要的大量符号和函数的定义。
- #include <Linux/init.h> //包含init.h的目的是指定初始化和清楚函数。
- #include <Linux/moduleparam.h> //可以在装载模块时向模块传递参数。
- 模块应该指定代码所使用的许可证,需要包含MODULE_LICENSE。比如: