内核模块实验 通过module添加Hello World内核模块

内核模块实验 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目录下安装insmodrmmodksymslsmodmodprobe等实用程序
  • 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);

在这里插入图片描述

  • 1
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值