Skip to content

Latest commit

 

History

History
executable file
·
234 lines (178 loc) · 11.7 KB

新手上路:内核模块入门.md

File metadata and controls

executable file
·
234 lines (178 loc) · 11.7 KB

新手上路:内核模块入门

从最初学习使用Linux OS,到学习Linux内核,再到自己编写内核模块,顺利实现模块的装载和卸载,这是一个非常有趣的过程。下面我将内核模块的学习内容和大家分享,将学习Linux内核的快乐简单的传递。

构造和运行模块的过程

模块源代码 hds.c文件

# include <linux/module.h>  				//任何模块都必须包含,定义了可动态加载到内核的模块所需要的必要信息
# include <linux/init.h>    				//必须包含,包含了宏__init(指定初始化函数)和__exit(指定清除函数)
# include <linux/kernel.h>  				//里面包含常用的内核API,例如内核打印函数printk()
static int __init hds_init(void)    		//__init将函数hds_init()标记为初始化函数,在模块被装载到内核时调用hds_init()
{
    int sum = 0;
	int i;
    for(i = 1; i < 11; i++)		   			//函数功能为1-10累加求和
		sum +=i;
	printk(KERN_CRIT "Hello kernel\n");		//注意末尾不要忘记加换行\n,否则打印会出现某些小的错误
	printk(KERN_ALERT "sum is %d\n",sum);   
                      //打印级别设为<1>,将求和结果立即打印,可以在插入模块后,在用户态下用命令dmesg查看打印效果
	return 0;
}  
static void __exit hds_exit(void)			//清除函数,在模块被卸载之前调用
{
	printk(KERN_ALERT "Goodbye kernel\n");  //在模块卸载时,将Goodbye kernel这句话打印到日志
}	
module_init(hds_init);   					//引导内核 模块从这里进来
module_exit(hds_exit);   					//引导内核 模块从这里出去
MODULE_LICENSE("GPL");   					//(必选项) 模块许可证,缺省此句,将导致内核被污染
MODULE_AUTHOR("hds");	 					//(可选项) 描述模块作者
MODULE_DESCRIPTION("for fun");  			//(可选项) 描述模块功能 

Makefile文件

obj-m:=hds.o						#根据make的自动推导原则,make会自动将源程序hds.c编译成目标程序hds.o。
                                    #所有在配置文件中标记为-m的模块将被编译成可动态加载进内核的模块。即后缀为.ko的文件。
CURRENT_PATH:=$(shell pwd)  		#参数化,将模块源码路径保存在CURRENT_PATH中
LINUX_KERNEL:=$(shell uname -r) 	#参数化,将当前内核版本保存在LINUX_KERNEL中
LINUX_KERNEL_PATH:=/usr/src/linux-headers-$(LINUX_KERNEL) 	
                                    #参数化,将内核源代码的绝对路径保存在LINUX_KERNEL_PATH中
all:
	make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) modules 	#编译模块
clean:
	make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) clean  	#清理

编译模块

$ make      

为方便在当前终端查看日志打印信息,在装载模块前输入此命令

$ tail -f /var/log/kern.log &

装载模块

$ sudo insmod hds.ko

查看装载的模块

$ lsmod

卸载模块

$ sudo rmmod hds

查看模块是否已卸载

$ lsmod

模块的组成

基本结构:

头文件+初始化函数+清除函数+引导内核的模块入口+引导内核的模块出口+模块许可证

头文件

编写内核代码所用到的头文件包含在内核代码树的include/及其子目录中,例如module.h,kernel.h,init.h,这三个头文件全部包含在/include/linux/中。这三个头文件以预处理指令的形式写在模块源代码的首部:

 # include <linux/module.h>
 # include <linux/init.h>
 # include <linux/kernel.h>

在编译模块源文件之前,由预处理程序对预处理指令进行预处理。对于#include <linux/module.h>来说,就是把module.h中的内容读进来,放在#include <linux/module.h>的位置,取代了#include <linux/module.h>指令行。然后由这些头文件的内容和其他部分一起组成一个完整的,可以用来编译的最后的源程序,然后由编译程序对该源程序正式进行编译,才得到目标程序。内核模块代码编译后得到目标文件后缀为.o

# include <linux/module.h>

在内核代码树的位置为linux-2.6.0/include/linux/module.h,头文件module.h包含了对模块的结构定义以及模块的版本控制,可装载模块需要的大量符号和函数定义(初学阶段知道写模块必须包含这个头文件,还有头文件大致的内容,先知道是什么,后面再深入分析源代码)。module.h的源码如下(只是开头部分):

#ifndef _LINUX_MODULE_H
#define _LINUX_MODULE_H
/*
 * Dynamic loading of modules into the kernel.  //动态加载到内核的模块
 *
 * Rewritten by Richard Henderson <[email protected]> Dec 1996
 * Rewritten again by Rusty Russell, 2002
 */
#include <linux/config.h>
#include <linux/sched.h>
#include <linux/spinlock.h>
#include <linux/list.h>
#include <linux/stat.h>
#include <linux/compiler.h>
#include <linux/cache.h>
#include <linux/kmod.h>
#include <linux/elf.h>
#include <linux/stringify.h>
#include <asm/local.h>

#include <asm/module.h>

/* Not Yet Implemented */
#define MODULE_SUPPORTED_DEVICE(name)
#define print_modules()

# include <linux/init.h>

在内核代码树的位置为linux-2.6.0/include/linux/init.h,在init.h这个文件中包含了两个非常重要的宏__init 和 __exit。在init.h的源代码中,对于两种宏的用法和作用给出了说明

源代码说明如下

/* These macros are used to mark some functions or 
 * initialized data (doesn't apply to uninitialized data)
 * as `initialization' functions. The kernel can take this
 * as hint that the function is used only during the initialization
 * phase and free up used memory resources after
 *
 * Usage:
 * For functions:
 * 
 * You should add __init immediately before the function name, like:
 *
 * static void __init initme(int x, int y)
 * {
 *    extern int z; z = x * y;
 * }
 * 
 */

宏__ init用于将一些函数标记为“初始化”函数。内核可以将此作为一个提示,即该函数仅在初始化阶段使用,并在初始化阶段之后释放使用的内存资源。模块被装载之后,模块装载器就会将初始化函数扔掉,这样可将该函数占用的资源释放出来。

宏__init的用法如下:

 static void __init initme(int x, int y)  //放在函数返回值类型和函数名之间
 {
	extern int z; z = x * y;
 }

宏__ exit的用法和__ init一样,它的作用是标记该段代码仅用于模块卸载(编译器将把该函数放在特殊的ELF段中)。即被标记为__ exit的函数只能在模块被卸载时调用。

# include <linux/kernel.h>

kernel.h包含了内核常用的API,比如printk()在kernel.h源代码的定义如下:

int printk(const char * fmt, ...)

模块功能函数

也可以称为初始化函数,模块功能函数的定义如下:

static int __init name_function(void)
{
	/* 模块要实现的功能 */
	return 0;		
}
module_init(name_function);

模块功能函数是在模块被装入内核后调用的,也就是在模块的代码被装入内核内存后,才调用模块功能函数。注意: __ init 标记只是一个可选项,并不是写所有模块代码都要加 __ init。但是在测试我们自己写的模块时,最好加上 __ init。因为我们在写一个模块功能函数的时候,可能这个函数里面有定义的变量,当调用这个函数的时候,就要为变量分配内存空间,但注意,此时分配给变量的内存,是在内核空间分配的,也就是说分配的是内核内存。所以说如果只是想要测试一下模块的功能,并不需要让模块常驻内核内存,那就应该在执行完函数后,将当初分配给变量的内存释放。为了达到这个效果,只需要把这个函数标记为 __init属性。

清除函数

清除函数的定义如下:

static void __exit name_function(void)
{
	/* 这里是清除代码*/
}
module_exit(name_function);

__ exit标记该段代码仅用于模块卸载,被标记为 __ exit的函数只能在模块被卸载或者系统关闭时调用。如果一个模块未被定义为清除函数,则内核不允许卸载该模块。

驱动程序初始化入口点

源码定义如下:

/**
 * module_init() - driver initialization entry point
 * @x: function to be run at kernel boot time or module insertion
 * 
 * module_init() will either be called during do_initcalls (if
 * builtin) or at module insertion time (if a module).  There can only
 * be one per module.
 */
# define module_init(x)	__initcall(x);

module _ init()——驱动程序初始化入口点。 在内核引导时运行的函数,或者在do _ initcalls期间调用module _ init(),或者在模块插入时(如果是模块)调用module _ init()。每个模块只能有一个。

驱动程序初始化出口点

源码定义如下:

/**
 * module_exit() - driver exit entry point
 * @x: function to be run when driver is removed
 * 
 * module_exit() will wrap the driver clean-up code
 * with cleanup_module() when used with rmmod when
 * the driver is a module.  If the driver is statically
 * compiled into the kernel, module_exit() has no effect.
 * There can only be one per module.
 */
 # define module_exit(x)	__exitcall(x);

module _ exit()-驱动程序出口点。当驱动程序被删除时运行的函数。当驱动程序是一个模块时,module _ exit()将使用cleanup _ module()包装驱动程序清理代码。如果驱动程序被静态编译到内核中,则module _ exit()没有作用。每个模块只能有一个。

模块许可证

编写内核模块,需要添加模块许可证。如果没有添加模块许可证,会收到内核被污染的警告

module license unspecified taints kernel

内核被污染可能会导致驱动程序的一些系统调用无法使用。

make和Makefile

make是内核的编译器,对Makefile文件进行编译。Makefile可以看成是针对make程序的配置文件,当我们执行make命令时,make就会在当前目录下寻找Makefile文件,然后根据Makefile的配置对源文件进行编译。

内核模块

Linux内核模块的概念:

  • 内核模块是一段可以动态加载进内核的代码
  • 在Linux内核源码树以外来开发并编译一个模块,称为树外开发
  • 之所以提供模块机制,是因为Linux内核本身是一个单内核,单内核由于所有内容都集成在一起, 效率很高,但可扩展性和可维护性较差,模块机制可弥补这一缺陷。

内核模块的两种加载方式

  • 静态加载:在内核启动过程中加载
  • 动态加载:在内核运行的过程中随时加载