内核模块实验 Hello World
文章目录
实验环境 ubuntu
前提配置
在系统命令行shell下安装当前版本得linux内核代码
sudo apt-get install linux-sorce
非常重要,后面源文件需要一些内核得库调用,提前配置好
程序编写
hello.c
#include <linux/init.h>
#include<linux/module.h>
MODULE_LICENSE("GPL");
static int hello_init(void)
{
// #define KERN_ALERT KERN_SOH "1" /*必须马上输出*/
printk(KERN_ALERT"Hello,world!\n");
return 0;
}
static void hello_exit(void)
{
printk(KERN_ALERT "Goodbye, cruel world\n");
}
module_init(hello_init);
module_exit(hello_exit);
Makefile
ifneq ($(KERNELRELEASE),)
obj-m :=hello.o
else
KDIR :=/lib/modules/$(shell uname -r)/build
all:
make -C $(KDIR) M=$(PWD) modules
clean:
rm -f *.ko *.o *.mod.o *.mod.c *.symvers *.order *.mod
endif
KERNELRELEASE
是在内核源码的顶层Makefile中定义的一个变量,如果没有找到就会跳到下面指定得路径去寻找
-C
选项的作用是指将当前工作目录转移到你所指定的位置
M=
的作用是,当用户需要以某个内核为基础编译一个外部模块的话,需要在make modules
命令中加入M=dir
,程序会自动到你所指定的dir目录中查找模块源码,将其编译,生成KO
文件。
编译与加载
- 编译、链接后生成的内核模块后缀为
.ko
,编译过程中首先会到内核源代码目录下,读取顶层makefile文件,然后返回模块源代码所在的目录继续编译。 - 当编译好内核模块后,用户以
root
身份就可将自己定义内核模块加载到内核里 - 内核提供
modutils
软件包供用户对内核模块进行管理,该软件包安装后会在/sbin
目录下安装insmod
、rmmod
、ksyms
、lsmod
、modprobe
等实用程序 - insmod命令
把需要载入的模块以目标代码形式加载进内核中,insmod
自动调用modules_init( )
函数中定义的过程运行
sudo insmod [path]modulename
- rmmod命令
将已经载入内核的模块从内核中卸载,rmmod
自动调用modules_exit( )
函数中定义的过程运行
rmmod [path]modulename
- lsmod命令
显示已经载入内核的所有模块信息,包括被载入模块的模块名、大小和引用计数
lsmod
内核符号表
- 内核符号表是一个用来存放所有模块可以访问的符号,以及对应地址的特殊数据结构,模块的链接是将模块插入到内核的过程,模块所导出的符号都将成为内核符号表的一部分。模块根据符号表从核心空间获取主存地址,从而确保在核心空间中正确地运行,对于从模块中导出的符号,在符号表中会包含第3列“所属模块”,在v2.6内核中,用户可从
/proc/kallsyms
中以文本方式读取内核符号表。
初始化与清理函数
- 内核模块必须调用宏module_init与module_exit去注册初始化与清理函数。
static int hello_init(void)
{
//初始化
}
module_init(hello_init);
- 内核模块必须调用宏module_init与module_exit去注册初始化与清理函数
static void hello_exit(void)
{
//清理
}
module_exit(hello_exit);
hello world 模块加载举例
这里可以看到执行make
生成了hello.ko
然后把他加入到内核模块中
可以看到内核模块加载成功
在用查看命令查看一下
再尝试一下卸载内核模块
卸载成功
内核模块机制的实现
下面一些概念介绍,便于理解
模块在内核中的表示
- 内核在管理模块时使用的管理数据结构为struct module,每一个内核模块被载入时,都要为其分配一个module对象,用一个双向链表把所有module对象组织起来,该链表的第1个元素为modules,开发者能够通过该元素依次访问内核中所有的module对象
- 内核通过module对象主要是为了记录模块的依赖,并进行模块导出符号的管理。
- stat成员表明当前模块的状态
- MODULE_STATE_LIVE(处于激活状态)
- MODULE_STATE_COMING(正在被初始化状态)
- MODULE_STATE_GOING(正在被卸载状态)
- 每个module对象都包含有多个引用计数,每个CPU都有一个引用计数
- 每次当模块被使用时,模块的引用计数都会加1
- 当模块不被使用时,模块的引用计数就会相应减少,仅当引用计数为0时,该模块才能被内核卸载
- 在内核代码段中,有3个段保存导出符号的相关信息:
- __kstrtab保存导出符号的名字
- __ksymtab保存供所有模块使用的符号的地址
- __ksymtab_gpl保存仅供GPL协议模块使用的符号的地址
- 只有被EXPORT_SYMBOL和EXPORT_SYMBOL_ GPL宏导出的符号才会被C编译器写入内核代码的相应段中,在加载内核时,会根据代码段中的符号信息创建符号表
模块的加载与卸载
-
模块的加载
- 用户通过insmod命令将模块载入内核,该命令的主要操作如下:
从命令行读入要被载入的模块名。 - 获得模块代码,它通常放在/lib/modules目录下。
- 调用init_module( )函数,将包含模块代码缓存的指针、模块代码长度和用户参数传递给函数,该函数将完成模块的加载工作
- init_module( )函数的主要工作流程
- 检查用户是否有权限加载模块(具有CAP_SYS_MODULE权限)
- 在核心空间中为模块申请主存,并将模块目标代码拷贝进入核心空间
- 检查模块代码是否是有效的ELF(Executable and Linking Format,可执行连接格式),如果不是,报错。
- 在核心空间为用户传递的模块参数申请主存,并将参数拷贝到核心空间。
- 内核通过模块名检查模块是否已经被载入内核。
- 为模块的可执行代码分配空间,并将模块目标代码中的相应段拷贝到该空间。
- 为模块的初始化代码分配空间,并将模块目标代码中的相应段拷贝到该空间。
- 获得模块的module对象的位置,在模块目标代码的正文段中存储着该对象。
- 初始化module对象中的module_code和module_init成员。
- 初始化modules_which_use_me,并把模块的引用计数置成0。
- 根据模块的授权协议,设置license_gplok,如果该模块不符合GPL协议,该标志置为0。
- 根据模块和内核符号表,对模块代码进行重定位,将代码中的符号解析成主存地址。
- 设置module对象中的syms和gpl_syms成员,这两个值被设置成模块导出符号表的地址。
- 解析用户传递过来的参数,并将值赋给模块中相应的符号。
- 注册module对象中的mkobj成员。注册后,在sysfs文件系统的module目录中会增加一个该模块的目录,该目录下包含有该模块的信息
- 将第2)步中申请的主存释放。
- 将module对象加入全局的模块对象双向链表
- 将模块状态设置为MODULE_STATE_COMING
- 如果模块自定义初始化函数,调用模块的初始化函数
- 设置模块状态为MODULE_STATE_LIVE
- 结束
-
模块的卸载
- 用户可以通过rmmod命令将内核模块卸载,该命令所做的操作是:
- 读取要被卸载的模块名
- 打开/proc/modules文件,查看该模块是否已经被卸载
- 调用delete_module( ),把模块名传递给该函数,该函数将完成模块的卸载工作
- sys_delete_module( )函数的主要工作流程
- 检查用户权限,只有有CAP_SYS_MODULE权限的用户才可以卸载模块。
- 将模块名拷贝进核心空间
- 在全局模块对象的双向链表中查找到该模块的module对象
- 通过module对象的modules_which_use_me成员检查是否有其他模块依赖该模块 ,只有在没有依赖的情况下,函数才能继续完成卸载
- 检查模块状态,只有处于MODULE_STATE_LIVE状态的模块才能被卸载
- 如果该模块有自定义初始化函数,该模块只有在也定义了清理函数的情况下才允许被卸载
- 为了防止竞争,将系统中的其他CPU停止
- 将模块的状态设置成MODULE_STATE_GOING
- 如果模块的引用计数大于0,模块不能被卸载
- 如果模块定义了清理函数,调用清理函数
- 将模块加载时注册的mkobj,取消掉。
- 如果有其他模块被该模块使用,修改其他模块的引用关系。
- 释放该模块的module对象
- 释放模块占用的主存(代码、符号表、异常表)
- 结束
内核模块显示进程控制块信息
- 在内核中,所有进程控制块都被一个双向链表连接起来,该链表中的第1个进程控制块为init_task
- 编写一个内核模块,模块接收用户传递的一个参数num,num指定要打印的进程控制块的数量;若用户不指定num或者num<0,模块则打印所有进程控制块的信息。需要打印的进程控制块信息有:进程PID和进程的可执行文件名
makefile 没有变
修改的hello.c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/init_task.h>
MODULE_LICENSE("GPL");
static int num = -1;
//moduule_param(num,int,S_IRUGO);
static int hello_init(void)
{
struct task_struct *p = NULL;
p = &init_task;
printk(KERN_ALERT"名称\t进程号\t状态\t优先级\t父进程号\t");
//for_each_process是宏循环控制语句,内核开发者可它扫描整个进程链表
for_each_process(p)
{
//mm是指向被映射的用户地址空间的内存管理结构的指针
//内核进程始终在内核空间运行,从来不切换到用户空间去,所以没有用户态地址空间,所以它们的mm成员总是为NULL
if(p->mm == NULL)
printk(KERN_ALERT"%s\t%d\t%ld\t%d\n",p->comm,p->pid, p->state,p->normal_prio,p->parent->pid);
}
return 0;
}
static void hello_exit(void)
{
printk(KERN_ALERT "Goodbye, cruel world\n");
}
//函数注册
module_init(hello_init);
module_exit(hello_exit);
查看日志,打印出了进程信息
ps查看进程一致
指定pid查找
输出
父进程
子进程
兄弟进程
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/sched.h>
#include <linux/moduleparam.h>
MODULE_LICENSE("GPL");
static int pid = -1;
//根据进程号pid得到进程描述符struct pid
module_param(pid,int,S_IRUGO);
static int hello_init(void)
{
struct task_struct *p = NULL,*parent,*children,*sibling;
//list_head 双向链表 便于遍历
struct list_head *list;
p = pid_task(find_get_pid(pid),PIDTYPE_PID);
if(p==NULL)
printk(KERN_ALERT"Pid does not exit\n");
else
{
parent=p->parent;
printk(KERN_ALERT"This is parent:\n");
printk(KERN_ALERT"程序名\t\tPID号\t进程状态\t优先级\n");
printk(KERN_ALERT"%-10s\t%5d\t%ld\t\t%d\n",parent->comm, parent->pid, parent->state,parent->prio);
}
printk(KERN_ALERT"This is sibling:");
printk(KERN_ALERT"程序名\t\tPID号\t进程状态\t优先级\n");
list_for_each(list,&parent->children)
{
//list_entry(ptr,type,member);
//ptr是一个指向list_head的指针,type是包含list_head的数据结构类型,而member是list_head在该数据结构中的成员名
sibling=list_entry(list,struct task_struct,sibling);
printk(KERN_ALERT"%-10s\t%5d\t%ld\t\t%d\n", sibling->comm, sibling->pid, sibling->state, sibling->prio);
}
printk(KERN_ALERT"This is children:\n");
printk(KERN_ALERT"程序名\t\tPID号\t进程状态\t优先级\n");
list_for_each(list,&p->children)
{
children=list_entry(list,struct task_struct,sibling);
printk(KERN_ALERT"%-10s\t%5d\t%ld\t\t%d\n", children->comm,children->pid, children->state, children->prio);
}
return 0;
}
static void hello_exit(void)
{
printk(KERN_ALERT "Goodbye, cruel world\n");
}
module_init(hello_init);
module_exit(hello_exit);