【内核模块的出现】
编译内核时,用户可以把所有的代码编译进内核,但是这样会引起两个问题:
一是内核过大;
二是当需要添加或者删除内核时,需要重新再编译内核。
所以有了 内核模块 的概念。
模块并不编译到内核中,编译后存放在指定的目录,当需要使用时动态加载。
【内核模块的优点】
在嵌入式设备驱动开发中将驱动程序以模块的形式发布,极大地提高了设备使用的灵活性。
用户只需要拿到相关驱动模块,再插入到用户的内核中,即可灵活地使用你的设备。
【编写内核模块】
内核编程总是异步的,没有一个 main()函数来让 Linux顺序执行自定义的模块。
取而代之的是我们要为各种事件提供 回调函数,比如:
static int __init test_init(void)、static void __exit test_exit(void)
示例:
#include <linux/init.h> // 用于指定初始化函数和清除函数
#include <linux/module.h> // 包含了很多装载模块需要的符号和函数的定义
MODULE_LICENSE("GPL");
static int __init test_init(void) // 内核初始化函数
{
printk(KERN_ALERT "hello world!\n"); // 打印函数,和prinft类似
return 0;
}
static void __exit test_exit(void) // 内核清除函数
{
printk(KERN_ALERT "good bye!\n");
}
module_init(test_init); // 指定初始化函数
module_exit(test_exit); // 指定清除函数
其中,module_init() 函数将 test_init() 函数标记为模块初始化函数,这样当我们调用 test模块时,模块加载器会自动进行调用。
同理,module_exit() 函数用于标记模块清除函数 test_exit(),在卸载模块时 text_exit()会被模块加载器自动调用。
一个没有定义清除函数的模块,是不允许被加载的。
在 linux 内核中,所有标示为 __init 的函数在连接的时候都放在 .init.text 区段内
所有的 __init 函数在区段 .initcall.init 中还保存了一份函数指针。
在初始化时内核会通过这些函数指针调用这些 __init 函数,并在初始化完成后释放 init 区段(包括 .init.text,.initcall.init 等)。
宏 MODULE_LICENSE("GPL") 用于指定代码使用的许可证。
如果没有这条语句在编译时会提示模块污染了内核。
printk() 函数相较于 printf() 函数,
在格式上多了“输出级别”控制;
在结果上,printk() 输出内容被定向到 /var/log/syslog 文件,也可以使用 dmesg命令 查看。
我们需要在模块代码中显式地指定高优先级的原因是:具有默认优先级的消息可能不会输出在控制台上。
【导出函数到符号表】
因为 内核模块不能独立运行,所有只将模块编写和注册好还不够, 需要将模块里的函数实例导出到符号表中,以供别的函数、模块调用。
使用 EXPORT_SYMBOL 宏将函数以符号的形式导出给其它模块使用。
文件 /proc/kallsyms 为内核的 符号表,记录了符号及对应的函数所在的内存地址。
示例:
// hello.c文件,定义2个函数,用于导出
#include <linux/init.h>
#include <linux/module.h>
MODULE_LICENSE("Dual BSD/GPL");
int add_integar(int a,int b)
{
return a + b;
}
int sub_integar(int a,int b)
{
return a - b;
}
EXPORT_SYMBOL(add_integar); // 导出函数到符号表
EXPORT_SYMBOL(sub_integar);
// test.c 用于调用hello模块导出的函数
#include <linux/init.h>
#include <linux/module.h>
MODULE_LICENSE("Dual BSD/GPL");
extern int add_integar(int, int); // 声明要调用的函数
extern int sub_integar(int, int); // 声明要调用的函数
int result(void)
{
int a,b;
a = add_integar(1,1); // 调用符号表中的函数
b = sub_integar(1,1);
printk("%d\n",a);
printk("%d\n",b);
return 0;
}
【内核模块调用与函数调用的区别】
调用函数的不同:应用程序可以调用它未定义的函数,因为应用程序可以在连接过程中解析外部引用从而使用适当的函数库。
反观内核模块,它仅仅被连接到内核,因此只能调用由内核导出的函数,不存在任何可连接的函数库。
【注意事项】
所有模块都要使用头文件 module.h,此文件必须被包含。
头文件 kernel.h包含了常用的内核函数。
头文件 init.h包含了宏_init和_exit,它们允许释放内核占用的内存。
【加载内核模块】
加载模块的操作最好在设备启动过程中实现。在嵌入式产品的 Linux系统启动过程中,
加载自定义模块的最简单的方法是修改启动过程的 rc脚本,增加 insmod /.../xxx.ko 这样的命令。
用 busybox 做出的文件系统,通常修改 etc/init.d/rcS 文件。
操作步骤:
1、内核编译时选择“可以加载模块”。嵌入式产品一般都不需要卸载模块,则可以不选择“可卸载模块”。
2、将我们的 .ko 文件放在文件系统中。
3、Linux系统实现了 insmod、rmmod 等工具。
4、使用时可以用 insmod 手动加载模块,也可以修改 /etc/init.d/rcS 文件,从而在系统启动的时候就加载模块。
编译内核时,用户可以把所有的代码编译进内核,但是这样会引起两个问题:
一是内核过大;
二是当需要添加或者删除内核时,需要重新再编译内核。
所以有了 内核模块 的概念。
模块并不编译到内核中,编译后存放在指定的目录,当需要使用时动态加载。
【内核模块的优点】
在嵌入式设备驱动开发中将驱动程序以模块的形式发布,极大地提高了设备使用的灵活性。
用户只需要拿到相关驱动模块,再插入到用户的内核中,即可灵活地使用你的设备。
【编写内核模块】
内核编程总是异步的,没有一个 main()函数来让 Linux顺序执行自定义的模块。
取而代之的是我们要为各种事件提供 回调函数,比如:
static int __init test_init(void)、static void __exit test_exit(void)
示例:
#include <linux/init.h> // 用于指定初始化函数和清除函数
#include <linux/module.h> // 包含了很多装载模块需要的符号和函数的定义
MODULE_LICENSE("GPL");
static int __init test_init(void) // 内核初始化函数
{
printk(KERN_ALERT "hello world!\n"); // 打印函数,和prinft类似
return 0;
}
static void __exit test_exit(void) // 内核清除函数
{
printk(KERN_ALERT "good bye!\n");
}
module_init(test_init); // 指定初始化函数
module_exit(test_exit); // 指定清除函数
其中,module_init() 函数将 test_init() 函数标记为模块初始化函数,这样当我们调用 test模块时,模块加载器会自动进行调用。
同理,module_exit() 函数用于标记模块清除函数 test_exit(),在卸载模块时 text_exit()会被模块加载器自动调用。
一个没有定义清除函数的模块,是不允许被加载的。
在 linux 内核中,所有标示为 __init 的函数在连接的时候都放在 .init.text 区段内
所有的 __init 函数在区段 .initcall.init 中还保存了一份函数指针。
在初始化时内核会通过这些函数指针调用这些 __init 函数,并在初始化完成后释放 init 区段(包括 .init.text,.initcall.init 等)。
宏 MODULE_LICENSE("GPL") 用于指定代码使用的许可证。
如果没有这条语句在编译时会提示模块污染了内核。
printk() 函数相较于 printf() 函数,
在格式上多了“输出级别”控制;
在结果上,printk() 输出内容被定向到 /var/log/syslog 文件,也可以使用 dmesg命令 查看。
我们需要在模块代码中显式地指定高优先级的原因是:具有默认优先级的消息可能不会输出在控制台上。
【导出函数到符号表】
因为 内核模块不能独立运行,所有只将模块编写和注册好还不够, 需要将模块里的函数实例导出到符号表中,以供别的函数、模块调用。
使用 EXPORT_SYMBOL 宏将函数以符号的形式导出给其它模块使用。
文件 /proc/kallsyms 为内核的 符号表,记录了符号及对应的函数所在的内存地址。
示例:
// hello.c文件,定义2个函数,用于导出
#include <linux/init.h>
#include <linux/module.h>
MODULE_LICENSE("Dual BSD/GPL");
int add_integar(int a,int b)
{
return a + b;
}
int sub_integar(int a,int b)
{
return a - b;
}
EXPORT_SYMBOL(add_integar); // 导出函数到符号表
EXPORT_SYMBOL(sub_integar);
// test.c 用于调用hello模块导出的函数
#include <linux/init.h>
#include <linux/module.h>
MODULE_LICENSE("Dual BSD/GPL");
extern int add_integar(int, int); // 声明要调用的函数
extern int sub_integar(int, int); // 声明要调用的函数
int result(void)
{
int a,b;
a = add_integar(1,1); // 调用符号表中的函数
b = sub_integar(1,1);
printk("%d\n",a);
printk("%d\n",b);
return 0;
}
【内核模块调用与函数调用的区别】
调用函数的不同:应用程序可以调用它未定义的函数,因为应用程序可以在连接过程中解析外部引用从而使用适当的函数库。
反观内核模块,它仅仅被连接到内核,因此只能调用由内核导出的函数,不存在任何可连接的函数库。
【注意事项】
所有模块都要使用头文件 module.h,此文件必须被包含。
头文件 kernel.h包含了常用的内核函数。
头文件 init.h包含了宏_init和_exit,它们允许释放内核占用的内存。
【加载内核模块】
加载模块的操作最好在设备启动过程中实现。在嵌入式产品的 Linux系统启动过程中,
加载自定义模块的最简单的方法是修改启动过程的 rc脚本,增加 insmod /.../xxx.ko 这样的命令。
用 busybox 做出的文件系统,通常修改 etc/init.d/rcS 文件。
操作步骤:
1、内核编译时选择“可以加载模块”。嵌入式产品一般都不需要卸载模块,则可以不选择“可卸载模块”。
2、将我们的 .ko 文件放在文件系统中。
3、Linux系统实现了 insmod、rmmod 等工具。
4、使用时可以用 insmod 手动加载模块,也可以修改 /etc/init.d/rcS 文件,从而在系统启动的时候就加载模块。