文章目录
02.第一个模块HelloWorld
2.1 内核划分
2.1.1 根据系统结构划分(5)
- 进程管理:内核负责进程创建与销毁,处理输入输出,进程间通信,共享CPU控制等
- 内存管理:内核为每个进程建立虚拟地址空间,内核与内存管理系统交互
- 文件系统:内核在非结构化的硬件上建立了结构化文件系统
- 设备控制:除处理器、内存和少见的实体外,其他任何设备控制操作由设备驱动完成
- 网络:报文是异步事件,在某一进程接手前必须被收集、识别和分发;系统负责在程序和网络间递送报文;所有路由和地址解析问题都在内核中实现

2.1.2 根据模块划分
Linux优良特性之一:在系统正在运行时,仍然可以增加或去除内核的部分功能;每块可以在运行时添加到内核的代码即为一个模块;模块并不是一个完成的可执行文件,它需要通过insmod(rmmod)动态链接(移除)到内核中

2.1.3 设备分类(3)
- 字符设备:可当做字节流存取的设备(如同一个文件),驱动至少要实现
open、close、read、write系统调用;文本控制台/dev/console和串口/dev/ttyS*是典型实例;字符设备通过文件系统结点存取,如/dev/tty1;与普通文件区别在于,字符设备只能顺序存取 - 块设备:同字符设备,块设备通过
/dev/下的文件系统结点存取;块设备驻有文件系统;大部分Unix系统块设备仅能传送2^n字节的整块,而Linux可传送任意字节;块和字符设备的区别仅仅在于内核在内部管理数据的方式上,以及软件接口上 - 网络接口:网络接口负责发送和接收报文;非面向流的设备;Unix提供的对接口的存取方式仍然是分配一个名字(如
eth*)但这个名字在文件系统中并没有对应入口
除设备驱动外,不论硬件和软件,在内核中都是模块化的(如文件系统)
2. HelloWorld模块
2.2.1 源文件
- 创建hello.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");//printk内核打印,KERN_ALERT消息优先级
return 0;
}
static void hello_exit(void)
{
printk(KERN_ALERT "Goodbye, cruel world\n");
}
module_init(hello_init);//特别的内核宏
module_exit(hello_exit);
- 创建makefile
obj-m := hello.o
KERNELDIR := /home/fa/linux-3.4.y
PWD := $(shell pwd)
modules:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
modules_install:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules_install
clean:
$(MAKE) -C $(KERNELDIR) M=$(PWD) clean
2.2.2 模块安装与卸载
- make编译后,使用
insmod、modprobe、rmmod加载或卸载模块 insmod加载模块的代码段和数据段到内核,加载中有类似ld的函数,它连接模块中的符号到内核的符号表上,内核不修改模块的磁盘文件,而是内存内的拷贝;insmod可接收许多命令行选项insmod依赖kernel/module.c中定义的系统调用,函数sys_init_module分配内核内存来存放模块(vmalloc),它接着拷贝模块的代码段到这块内存区,借助内核符号表解决模块中的内核引用,并且调用模块的初始化函数来启动所有东西sys_前缀的代码对所有系统调用都是成立的- 模块加载时,会有
vermagic检查,vermagic.o目标文件中包含了目标内核版本、编译器版本,以及许多重要配置变量的设置信息,这些信息会与运行内核进行匹配,若不匹配,模块将不会加载,可通过/var/log/message查看加载日志 - 模块代码必须要为每个它要连接的内核版本重新编译,模块紧密结合到特殊内核版本的数据结构和函数原型之上
2.2.3 注意
- 模块没有外部库,只能调用内核中函数,因此源文件中也不应包含通常的头文件;大部分头文件位于内核源码
include/linux以及include/asm目录下,其余部分已经关联到特定内核子系统内 printk不支持浮点- 模块加载到内核的过程

2.3 带参HelloWorld模块
#include <linux/init.h>
#include <linux/module.h>
#include <linux/moduleparam.h>
MODULE_LICENSE("Dual BSD/GPL");
static char *whom = "world";
static int howmany = 1;
//insmod hello.ko howmany=10 whom="hello"改变模块参数
module_param(howmany, int, S_IRUGO);
module_param(whom, charp, S_IRUGO);
static int hello_init(void)
{
int i;
for (i = 0; i < howmany; i++)
printk(KERN_ALERT "(%d) Hello, %s\n", i, whom);
return 0;
}
static void hello_exit(void)
{
printk(KERN_ALERT "Goodbye, cruel world\n");
}
module_init(hello_init);
module_exit(hello_exit);
- 参数用
moudle_param宏定义来声明,它定义在moduleparam.h中,module_param使用了3个参数,变量名、类型,以及一个权限掩码用来做一个辅助的sysfs入口 - 模块参数支持类型:
bool、invbool、charp、int、int、long、short、uint、ulong、ushort - 数组参数使用
module_param_array(name,type,num,perm);,perm是权限值,在<linux/stat.h>中定义,S_IRUGO、S_IWUSR
2.4 源码说明
2.4.1 Document目录
- 通过
Document/kbuild目录下文件查看模块如何被建立,包括kbuild、kconfig、makefiles、modules文件 - 通过
Document/Changes查看各种工具版本
2.4.2 与模块相关的头文件
module.h中包含了大量加载模块需要的函数和符号的定义,init.h指定初始化和清理函数,moduleparam.h在模块加载时传递参数- 可以在模块中包含其他描述性定义,如
MODULE_AUTHOR、MODULE_DESCRIPION、MODULE_VERSION、MODULE_ALIAS、MODULE_DEVICE_TABLE(告知用户空间模块支持那些设备),惯例这些声明置于文件末尾 struct task_struct *current;(#include <linux/sched.h>,包含很多驱动使用的内核API的定义)module_param(variable, type, perm);(#include <linux/moduleparam.h>)int printk(const char * fmt, ...);(#include <linux/kernel.h>)
2.4.3 初始化&退出
- 模块初始化函数注册模块提供的任何功能
- 初始化函数应声明为
static,因为它们不会在特定文件之外可见 __init标志给内核暗示,给定的函数只是在初始化使用,__initdata给只在初始化时用的数据__exit、__exitdata- 使用
moudle_init是强制的,它表明增加了特别的段到模块目标代码中,表明在哪里找到模块的初始化函数 - 模块可以注册许多的不同设施,包括不同类型的设备、文件系统、加密转换等;对每一个设施,有一个特定的内核函数来完成这个注册,传给内核注册函数的参数常常是一些数据结构的指针,描述新设施以及要注册的新设施的名字,数据结构常常包含模块函数指针
static int __init initialization_function(void)
{
/* Initialization code here */
}
module_init(initialization_function);
static void __exit cleanup_function(void)
{
/* Cleanup code here */
}
module_exit(cleanup_function);
2.5 内核空间与用户空间
- 模块在内核空间运行,应用程序在用户空间运行
- Unix下,内核在最高级操作形态运行,允许任何事情,而应用程序在最低级运行,这里处理器控制了对硬件的直接存取以及对内存的非法存取;内核空间和用户空间都有各自的内存映射(自己的地址空间)
- Unix 从用户空间转换执行到内核空间,无论何时一个应用程序发出一个系统调用或者被硬件中断挂起时,执行系统调用的内核代码在进程的上下文中工作(它代表调用进程并且可以存取该进程的地址空间),即处理中断的代码对进程来说是异步的,不和任何特别的进程有关
- 模块通常的任务,一是作为系统调用的一部分执行,二是负责中断处理
- Linux内核代码包括驱动代码,必须是可重入的

2.6 在用户空间编写驱动
- 用户空间设备驱动优点:可使用C库,可运行常用调试器、可杀掉挂起的用户空间驱动、用户内存可交换、允许对设备并行存取
- 用户空间设备驱动缺点:无法使用中断、只能通过内存映射
/dev/mem使用DMA、存取I/O端口只能在调用ioperm或iopl之后、响应时间慢、最重要的设备不能在用户空间处理
2.7 实操
- 开发板:NanoPi-NEO-Air
- 内核源码版本:4.14.52 (从源码顶层Makefile的首部可以看到源码的版本号,开发板中使用
uname -r可以看到系统运行的版本号
VERSION = 4
PATCHLEVEL = 14
SUBLEVEL = 52
- 使用
make编译hello.c时,报错
./arch/x86/include/asm/arch_hweight.h:55:42: error: expected ‘:’ or ‘)’ before ‘POPCNT64’
./arch/x86/include/asm/atomic64_64.h:22:22: error: request for member ‘counter$
./arch/x86/include/asm/atomic64_64.h:22:22: error: request for member ‘counter$
...
这是由于make是没有指定编译平台和编译工具链所致,使用make ARCH=arm CROSS_COMPILE=arm-linux-即可解决
- 使用
sudo insmod hello.ko时报错
insmod: ERROR: could not insert module hello.ko: Unknown symbol in module
通过dmesg|tail查看日志
[ 116.880560] hello: loading out-of-tree module taints kernel.
[ 116.880736] hello: Unknown symbol __gnu_mcount_nc (err 0)
百度搜索其原因,Unknown symbol __gnu_mcount_nc (err 0) ,是顶层Makefile开启了gprof(并不知道是什么东西),只需要把CFLAGS的-pg或-p参数去掉即可,即注释掉第763
763 #CC_FLAGS_FTRACE := -pg
再次编译、insmod成功,通过dmesg|tail可以查看到
[ 272.028920] hello: loading out-of-tree module taints kernel.
[ 272.030778] Hello, world
- 使用
sudo rmmod -f hello卸载模块时报错
rmmod: ERROR: ../libkmod/libkmod-module.c:793 kmod_module_remove_module() could not remove 'hello': Device or resource busy
rmmod: ERROR: could not remove module hello: Device or resource busy
百度后,这种无法卸载模块的原因有:设备号冲突(hello模块并无设备号)、别的进程正在使用等,Linux强制卸载内核模块(由于驱动异常导致rmmod不能卸载,该文描述的很清楚,但这里仍旧没找到无法卸载的原因,日后再说!
- 调试工具
lsmod#查看已安装的模块
cat /proc/modules#查看设备详细信息(模块名字 内存大小 load次数 依赖 状态 内存偏移位置)
dmesg#查看内核日志(-T)
cat /proc/devices#查看设备以及设备号
modinfo hello.ko#查看模块信息
3200

被折叠的 条评论
为什么被折叠?



