Linux学习篇(三)———Linux内核的内核模块

什么是Linux的内核模块呢?废话不多说,先来一段代码感受一下!

kernel programming : module.c

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

/** 编写内核模块程序所必须的 3 个头文件
    因为内核编程和用户层编程所用的库函数不一样,故头文件也不同
    
    a. 内核头文件的位置 : /usr/src/linux-2.6.x/include/

    b. 用户层头文件的位置 : /usr/include/
**/

MODULE_LICENSE("Dual BSD/GPL");



static int hello_init(void)
{
printk(KERN_ALERT "hello,I am greenbird/n");
return 0;
}

static void hello_exit(void)
{
printk(KERN_ALERT "goodbye,kernel/n");
}

module_init(hello_init);
module_exit(hello_exit);
 
MODULE_AUTHOR("Greenbird");
MODULE_DESCRIPTION("This is a simple module!/n");
MODULE_ALIAS("A simplest module");
————————————————

以前的hellowold的程序是这样的:

以前的c程序(用户层): hello.c

#include<stdio.h>

int main()

{

printf("hello world!/n");


return 0;
}

内核模块

    内核模块是Linux内核向外部提供的一个插口,其全称为动态可加载内核模块(Loadable Kernel Module,LKM),我们简称为模块。Linux内核之所以提供模块机制,是因为它本身是一个单内核(monolithic kernel)。单内核的最大优点是效率高,因为所有的内容都集成在一起,但其缺点是可扩展性和可维护性相对较差,模块机制就是为了弥补这一缺陷。模块是具有独立功能的程序,它可以被单独编译,但不能独立运行。

    内核模块特点:模块本身不被编译进内核映像,从而控制了内核的大小;  模块一旦被加载,就和内核中的其他部分完全一样。

模块开发常用指令


在内核模块开发的过程中常用的有以下指令。 
1) insmod: 将模块插入内核中,使用方法:insmod XXX.ko 
2) rmmod: 将模块从内核中删除,使用方法:rmmod XXX.ko 
3) lsmod: 列表显示所有的内核模块,可以和grep指令结合使用。使用方法:lsmod | grep XXX 
4) modprobe: modprobe可载入指定的个别模块,或是载入一组相依赖的模块。modprobe会根据depmod所产生的依赖关系,决定要载入哪些模块。若在载入过程中发生错误,在modprobe会卸载整组的模块。依赖关系是通过读取 /lib/modules/2.6.xx/modules.dep得到的。而该文件是通过depmod 所建立。 
使用方法:modprobe module [ module parameters] 
其中,参数module指定了需要载入的模块名称,后面的参数将在模块加载时传入内核 
5) modinfo: 查看模块信息。使用方法:modinfo XXX.ko 
6) tree –a: 查看当前目录的整个树结构。使用方法:tree -a

 为什么内核态使用 printk() 函数,而在用户态使用 printf() 函数

printk() 函数是直接使用了向终端写函数 tty_write() 。而 printf() 函数是调用 write() 系统调用函数向标准输出设备写。所以在用户态(如进程 0 )不能够直接使用 printk() 函数,而在内核态由于它已是特权级,所以无需系统调用来改变特权级,因而能够直接使用 printk() 函数。 printk 是内核输出,在终端是看不见的。我们可以看一下系统日志。但是我们可以使用命令: cat /var/log/messages ,或者使用 dmesg 命令看一下输出的信息

编写内核模块时必须要有的两个函数 :

1> 加载 函数:

static int init_fun(void)

{

// 初始化代码

}

函数实例:

static int hello_init(void)// 不加 void 在调试时会出现报警

{

printk("hello world!/n");

return 0;

}

2> 卸载函数 无返回值

static void cleaup_fun(void)

{

// 释放代码

}

函数实例:

static void hello_exit(void)// 不加 void 会出现报警 , 若改为 static int 也会报错 , 因为出口函数是不能返会值的

{

printk("goodbye!/n");

}

在模块编程中必须要有上面这两个函数;

补充:

注册函数和卸载函数还有另一中写法:

1> 模块加载 函数

static int __init init_fun(void)

{

// 初始化代码

}

函数实例:

static int __init hello_init(void)

{

printk("hello bird/n");

return 0;

}

2> 卸载函数 无返回值

static void __exit cleaup_fun(void)

{

// 释放代码

}

函数实例:

static void __exit exit(void)

{

printk("goodbye!/n");

}

注:

通过比较我们可以发现第二中函数的写法与第一中函数的写法主要不同就是加了 __init 和 __exit 前缀。 (init 和 exit 前面都是两个下划线 )

那么第二种方法比第一种有什么好处呢:

_init 和 __exit 是 Linux 内核的一个宏定义,使系统在初始化完成后释放该函数,并释放其所占内存。因此它的优点是显而易见的。所以建议大家啊在编写入口函数和出口函数时采用第二中方法。

(1) 在 linux 内核中,所有标示为 __init 的函数在连接的时候都放在 .init.text 这个区段内,此外,所有的 __init 函数在区段 .initcall.init 中还保存了一份函数指针,在初始化时内核会通过这些函数指针调用这些 __init 函数,并在初始化完成后释放 init 区段(包括 .init.text,.initcall.init 等)。

(2) 和 __init 一样, __exit 也可以使对应函数在运行完成后自动回收内存。

加载模块和卸载模块

1>module_init(hello_init)

a. 告诉内核你编写模块程序从那里开始执行。

b.module_init() 函数中的参数就是注册函数的函数名。

2>module_exit(hello_exit)

a. 告诉内核你编写模块程序从那里离开。

b.module_exit() 中的参数名就是卸载函数的函数名。

注:

我们一般在注册函数里进行一些初始化比如申请内存空间注册设备号等 。那么我们就要在卸载函数进行释放我们所占有的资源。

(1) 若模块加载函数注册了 XXX, 则模块卸载函数应该注销 XXX

(2) 若模块加载函数动态申请了内存,则模块卸载函数应该注销 XXX

(3) 若模块加载函数申请了硬件资源(中断, DMA 通道)的占用,则模块卸载函数应该释放这些硬件资源。

(4) 若模块加载函数开启了硬件,则卸载函数中一般要关闭硬件。

许可权限的声明

1> 函数实例:

MODULE_LICENSE("Dual BSD/GPL") ;

2> 此处可有可无,可以不加系统默认 ( 但是会报警   )

模块声明描述内核模块的许可权限,如果不声明 LICENSE ,模块被加载时,将收到内核的警告。

在 Linux2.6 内核中,可接受的 LICENSE 包括" GPL","GPL v2","GPL and additional rights","Dual BSD/GPL","Dual MPL/GPL","Proprietary" 。

模块的声明与描述(可加可不加)

MODULE_AUTHOR(“author”);// 作者

MODULE_DESCRIPTION(“description”);// 描述

MODULE_VERSION(”version_string“);// 版本

MODULE_DEVICE_TABLE(“table_info”);// 设备表

对于 USB , PCI 等设备驱动,通常会创建一个 MODULE_DEVICE_TABLE

MODULE_ALIAS(”alternate_name“);// 别名

一个完整的模块编程就完成了!!!

参考:https://blog.csdn.net/tigerjibo/article/details/6010997

猜你喜欢

转载自blog.csdn.net/qq_41899773/article/details/102548263