Linxu设备驱动开发
Linux内核
内核简介
操作系统和内核
操作系统 是指在整个系统中负责完成最基本功能和系统管理的那些部分,包括内核、设备驱动程序、启动引导程序、命令行Shell或者其他种类的用户界面、基本的文件管理工具和系统工具。
内核 是操作系统的内在核心,系统其他部分必须依靠内核这部分软件提供的服务,被称作是“管理者”、“操作系统核心”。
内核的组成
通常一个内核由负责响应中断的中断服务程序,负责管理多个进程从而分享处理器时间的调度程序,负责管理进程地址空间的内存管理程序和网络、进程间通信等系统服务程序共同组成。
内核空间
应用程序、内核和硬件的关系
内核一般处于系统态,独立于普通应用程序,拥有受保护的内存空间和访问硬件设备的所有权限。这种系统态和被保护起来的内存空间,统称为内核空间。
什么是系统态?
当内核运行时,系统将以内核态进入内核空间执行;而执行一个普通用户程序时,系统将以用户态进入以用户空间执行,它们只能看到允许它们使用的部分系统资源,并且只能使用某些特定的系统功能,不能直接访问硬件,也不能访问内核划给别人的内存范围还有其他一些使用限制。
应用程序与内核的通信
在系统中运行的应用程序通过系统调用来与内核通信。应用程序通常调用库函数(比如 C 库函数)再由库函数通过系统调用界面,让内核代其完成各种不同任务。
当一个应用程序执行一条系统调用,我们说内核正在代其执行。在这种情况下,应用程序被称为通过系统调用在内核空间运行,而内核被称为运行于进程上下文中。

中断处理
内核还要负责管理系统的硬件设备。当硬件设备想和系统通信的时候,它首先要发出一个异步的中断信号去打断处理器的执行,继而打断内核的执行。
中断通常对应着一个中断号,内核通过这个中断号查找相应的中断服务程序,并调用这个程序响应和处理中断。
为了保证同步,内核可以停用中止--既可以停止所有的中断也可以有选择地停止某个中断号对应的中断。许多操作系统的中断服务程序都不在进程上下文中执行,它们在一个与所有进程都无关的、专门的中断上下文中运行,保证了中断服务程序可以在第一时间响应和处理中断请求,然后快速退出。
处理器活动的情况
-
运行于用户空间,执行用户进程
-
运行于内核空间,处于进程上下文,代表某个特定的进程执行。应用程序、内核和硬件的关系
-
运行于内核空间,处于中断上下文,与任何进程无关,处理某个特定的中断。
以上三种情况几乎包括所有情况,即使是CPU空闲时,也会运行一个空进程,处于进程上下文,但运行于内核空间。
单内核与微内核
操作系统内核可以分为两大阵营:单内核和微内核(第三阵营是外内核,主要用在科研系统)
Linux是一个单内核,也就是说其内核运行在单独的内核地址空间上。不过Linux也汲取了微内核的精华:例如模块化设计、抢占式内核、支持内核线程以及动态装载内核模块。Linux也做了一些优化,让所有事情都运行在内核态,直接调用函数,无需消息传递。
单内核
单内核是一种较为简单的设计,就是把它从整体上作为一个单独的大过程来实现,同时也运行在一个单独的地址空间上。因此,这样的内核通常以单个静态二进制文件的形式存放于磁盘中。所有内核都在这样的一个大内核地址空间上运行。大多数Unix系统都设计为单模块。
微内核
微内核的功能被划分为多个独立的过程,每个过程叫做一个服务器。所有的服务器都保持独立并运行在各自的地址空间上,只能通过消息传递处理微内核间的通信。系统采用了进程间通信(IPC)机制,因此,各个服务器之间通过IPC机制互通消息。
因为IPC机制的开销多于函数调用,又因为会涉及内核空间与用户空间的上下文切换,因此消息传递需要一定的周期,而内核中简单的函数调用没有这些开销。结果,所有实际应用的基于微内核的系统都让大部分或全部服务器位于内核,这样就可以直接调用函数,消除频繁的上下文切换。
Linux内核版本
Linux内核分为稳定的和处于开发中的,通过一个简单的命名机制来区分稳定的和处于开发中的内核。
这种机制使用三个或者四个用 . 分割的数字来代表不同内核版本:
- 第一个数字是主版本号
- 第二个数字是从版本号
- 第三个数字是修订版本号
- 第四个可选的数字为稳定版本号
如果从版本号是偶数,那么此内核就是稳定版,如果是奇数就是开发版。
例如 2.6.26.1 的内核就是一个稳定版,主从版本号两个数字在一起描述了“内核系列”,该内核也就属于 2.6 版内核系列。

内核基本常识
获取内核源码
注意
内核源码一般安装在/usr/src/linux目录下,但不要把这个源码树用于开发或者修改,因为编译你的C库所用的内核版本就链接到这个树。
应当建立自己的主目录安装新内核。
内核源码树
| 目录 | 描述 |
|---|---|
| arch | 特定体系结构的源码 |
| block | 块设备I/O层 |
| crypto | 加密API |
| Documentation | 内核源码文档 |
| drivers | 设备驱动程序 |
| firmware | 使用某些驱动程序而需要的设备固件 |
| fs | VFS和各种文件系统 |
| include | 内核头文件 |
| init | 内核引导和初始化 |
| ipc | 进程间通信代码 |
| kernel | 像调度程序这样的核心子系统 |
| lib | 通用内核函数 |
| mm | 内存管理子系统和VM |
| net | 网络子系统 |
| samples | 示例,示范代码 |
| scripts | 编译内核所用的脚本 |
| security | Linux 安全模块 |
| sound | 语音子系统 |
| usr | 早期用户空间代码(所谓的initramfs) |
| tools | 在 Linux 开发中有用的工具 |
| virt | 虚拟化基础结构 |
编译内核
配置内核
在编译内核前,必须要进行配置。由于内核提供了很多功能,支持了很多硬件,所以有很多东西需要配置。可以配置的各种选项以 CONFIG_XXX 表示。配置选项既可以用来决定哪些文件编译进内核,也可以通过预处理命令处理代码。
这些配置选项分为“二选一”和“三选一”。二选一就是 yes/no,表示该功能是否开启;三选一就是yes/no/module,module 意味着该配置项被选定了,但编译的时候这部分功能的实现代码是以模块的形式生成。
内核提供了各种不同的工具来简化内核
- 逐一遍历所有配置项,自己选择
yes/no/module
make config
- 基于ncurse库编制的图形界面工具
make menuconfig
- 基于 gtk+ 的图形工具
make gconfig
- 创建一个默认配置
make defconfig
这些配置项被存放在内核代码树根目录下的 .config 文件中,并可以直接修改。在修改过配置文件后,或者在用已有的配置文件配置新的代码树时,应该验证和更新配置(事实上,在编译内核前都应该这么做):
make oldconfig
配置选项 CONFIG_IKCONFIG_PROC
就是把完整的压缩过的内核配置文件存放在/proc/config.gz下,这样当编译一个新内核的时候就可以方便地克隆当前的配置。zcat /proc/config.gz > .config make oldconfig
编译内核
一旦内核配置好了,就可以用一个简单的命令来编译:
make
如果需要尽量少看到垃圾信息,可以重定向到一个文件或者 null
make > ../detritus
make > /dev/null
make 也可以将编译过程拆分成多个并行的作业,每个作业独立并发地运行,极大加快了多处理器系统上的编译过程。
# n是要衍生出的作业数,一般每个处理器衍生一个或两个作业
# 比如在一个16核处理器上,可以用 -j32
make -jn
安装内核
内核编译好后需要安装,需要查阅启动引导工具的说明,按照它的指导将内核映像拷贝到合适的位置,并且按照启动要求安装它。
一定要保证随时有一个或两个可以启动的内核,以防新编译的内核出现问题。
模块的安装是自动的,也独立于体系结构。需要以root身份运行命令,就可以把所有已编译的模块安装到正确的主目录 /lib/modules 下
make modules_install
编译时也会在内核代码树的根目录下创建一个System.map 文件。这是一份符号对照表,用来将内核符合和它们的起始地址对应起来。
进程
进程是处于执行期的程序以及相关的资源的总称,包含可执行程序代码、打开的文件、挂起的信号、内核内部数据、处理器状态、内存地址空间、执行线程以及存放全局变量的数据段等等。
执行线程
简称线程,是在进程中活动的对象。每个线程都有一个独立的程序计数器、进程栈和一组进程寄存器。内核调度的对象是线程,而不是进程。
进程管理
进程在创建它的时刻开始存活。在Linux中,这通常是调用 fork() 系统的结果,该系统调用通过复制一个现有进程来创建一个全新的进程。fork() 系统调用从内核返回两次:一次回到父进程,另一次回到子进程。
最终,程序通过 exit() 系统调用退出执行。这个函数会终结进程并将其占用的资源释放。进程退出执行后被设置为僵死状态,直到它的父进程调用 wait() 或 waitpid() 为止。
进程描述符及任务结构
内核把进程的列表存放在叫做 任务队列(task list) 的双向循环链表中。链表中的每一项都是类型为 task_struct、称为进程描述符的结构。
该结构定义在
<linux/sched.h>文件中
进程描述符中包含的数据能完整地描述一个正在执行的程序:它打开的文件、进程的地址空间、挂起的信号以及进程的状态等等。

分配进程描述符
设备驱动程序简介
内核功能划分
Linux的每个进程都需要请求系统资源,比如运算、内存、网络连接或其他一些资源等。内核负责处理所有这些请求,根据内核完成任务的不同,可以将内核功能分为下面几部分:
-
进程管理
负责创建和销毁进程,并处理它们和外部之间的连接(输入输出);通过信号、管道等进行不同进程之间的通信;控制进程共享CPU的调度器。 -
内存管理
内核在有限的可用资源上为每个进程都创建了一个虚拟地址空间。内核的不同部分在内存管理子系统交互时使用一组函数调用,包括简单的malloc/free函数对以及其他一些复杂的函数。 -
文件系统
Linux中的每个对象都可以当作文件看待,同时Linux也支持多种文件系统类型,也就是在物理介质上组织数据的不同方式。例如,磁盘可以格式化为符合Linux标准的ext3文件系统,也可格式化为常用的FAT文件系统或者其他种类。 -
设备控制
所有设备操控操作都由与被控制设备相关的代码来完成,这段代码就叫做驱动程序。内核必须为系统中的每件外神嵌入相应的驱动程序,包括硬盘驱动器、键盘和磁带驱动器等。 -
网络功能
系统负责在应用程序和网络接口之间传递数据包,并根据网络活动控制程序的执行。所有的路由和地址解析问题都由内核处理。
设备和模块的分类
-
字符设备
字符设备是个能像字节流一样被访问的设备,由字符设备驱动程序来实现。字符设备可以通过文件系统节点来访问,这些设备文件和普通文件之间唯一的差别在于普通文件的访问可以前后移动访问位置,而大多数字符设备只能顺序访问的数据通道。(也存在具有数据区特性的字符设备,可以前后移动访问位置,比如帧抓取器) -
块设备
和字符设备类似,块设备也是通过/dev目录下的文件系统节点来访问。块设备(例如磁盘)上可以容纳文件系统。Linux可以让应用程序像字符设备一样地读写块设备,允许一次传递任意多字节的数据。 -
网络接口
任何网络事物都经过一个网络接口形成,即一个能够和其他主机交换数据的设备。接口是个硬件设备,也可能是个纯软设备。网络接口由内核中的网络子系统驱动,负责发送和接收数据包,但它不需要了解每项事务如何映射到实际传送的数据包。
进程
进程是处于执行期的程序以及相关的资源的总称,包含可执行程序代码、打开的文件、挂起的信号、内核内部数据、处理器状态、内存地址空间、执行线程以及存放全局变量的数据段等等。
执行线程
简称线程,是在进程中活动的对象。每个线程都有一个独立的程序计数器、进程栈和一组进程寄存器。内核调度的对象是线程,而不是进程。
进程管理
进程在创建它的时刻开始存活。在Linux中,这通常是调用 fork() 系统的结果,该系统调用通过复制一个现有进程来创建一个全新的进程。fork() 系统调用从内核返回两次:一次回到父进程,另一次回到子进程。
进程描述符及任务结构
内核把进程的列表存放在叫做 任务队列(task list) 的双向循环链表中。链表中的每一项都是类型为 task_struct、称为进程描述符的结构。
该结构定义在
<linux/sched.h>文件中
进程描述符中包含的数据能完整地描述一个正在执行的程序:它打开的文件、进程的地址空间、挂起的信号以及进程的状态等等。
分配进程描述符
Hello World模块
简单的写一个hello的模块作为示例。
hello_world.c
#include <linux/init.h>
#include <linux/module.h>
MODULE_LICENSE("Dual BSD/GPL");
static int hello_init(void)
{
printk(KERN_ALERT "Hello world\n");
return 0;
}adb shell cat sys/class/thermal/thermal_zone19/temp
static void hello_exit(void)
{
printk(KERN_ALERT "Goodbye, cruel world\n");
}
module_init(hello_init);
module_exit(hello_exit);
这个模块定义了两个函数,hello_init在模块被装在到内核时调用,hello_exit在模块被移除时调用。
module_init和module_exit使用了内核的特殊宏来表示上述两个函数所扮演的角色。
MODULE_LICENSE用来告诉内核,该模块采用自由许可证。
printk函数在Linux内核中定义,功能与printf类似。在insmod函数装入模块后,模块就连接到内核,由于内核运行时不能依赖于C库,需要自己单独打印输出函数,所以模块在连接内核后就需要访问内核的公用符号(包括函数和变量)来进行打印。
代码中KERN_ALERT定义了这条消息的优先级。这里显式地指定高优先级的原因是因为具有默认优先级的消息可能不会输出在控制台上,这依赖于内核版本等一些配置。
Makefile
# 如果已定义 KERNELRELEASE,则说明是从内核构造系统调用的
# 因此可以利用其内建语句
ifneq ($(KERNELRELEASE),)
obj-m := hello.o
hello-objs := hello_world.o
# 否则,是直接从命令行调用的
# 这时要调用内核构造系统
else
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
default:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
endif
obj-m := hello.o 表示有一个模块hello.ko需要从目标文件 hello.o中构造。
hello-objs := hello_world.o 表示需要的目标文件由hello_world.c生成。这里可以添加多个文件,中间以空格分割。
为了让该makefile文件正常工作,必须在大的内核构造系统环境中调用它们。
在上面这个典型的构造过程中,该makefile将被读取两次。
当makefile从命令行调用时,它注意到 KERNELRELEASE变量尚未设置。我们可以注意到,在已安装的模块目录中存在一个符号链接,它指向内核的构造树,这样makefile就可以定位内核的源代码目录KERNELDIR。
uname -r用来输出内核发行号,如果实际运行的内核并不是要构造的内核,则可以在命令行提供KERNELDIR=选项或者设置KERNELDIR环境变量,也可以修改用来设置KERNELDIR的行
在找到内核源代码树后,这个makefile会调用default:目标,这个目标会再一次运行make命令,以便运行内核构造系统。
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
这里make命令被参数化成了$(MAKE)
这条命令首先改变目录到-C选项指定的位置(即内核源代码目录),其中保存有内核的顶层makefile文件。
M=选项让该makefile在构造modules目标之前回到模块源代码目录。
然后,modules目标指向obj-m变量中设定的模块-->hello.o,而内核的makefile负责真正构造模块。
装载和卸载模块
在make之后会生成hello.ko模块,现在可以简单操作一下这个模块。
只有超级用户才有权加载和卸载模块
加载模块
sudo insmod ./hello.ko
卸载模块
sudo rmmod hello