本文目录
前述:内核框架图
一、Linux 内核模块概述
嵌入式设备驱动开发中将驱动程序以模块的形式发布,更是极大地提高了设备使用的灵活性,用户只需要拿到相关驱动模块,再安装到用户的内核中,即可灵活地使用你的设备。
二、Linux 模块的优点
- 用户可以随时扩展 Linux 系统的功能。
- 当设备驱动有更新时,只需要卸载旧模块,重新安装即可。
- 系统需要增加新的模块功能,不必重新编译内核,只要安装相应模块文件即可。
- 使用模块,还可以减小 Linux 内核的体积,节省 flash。
- 可以在不影响核心系统的情况下开发和测试新功能,降低系统崩溃或不稳定的风险。
三、知识点
1. GPL开源协议
我们很熟悉的Linux就是采用了GPL。GPL协议和BSD, Apache Licence等鼓励代码重用的许可很不一样。GPL的出发点是代码的开源/免费使用和引用/修改/衍生代码的开源/免费使用,但不允许修改后和衍生的代码做为闭源的商业软件发布和销售。这也就是为什么我们能用免费的各种linux,包括商业公司的linux和linux上各种各样的由个人,组织,以及商业软件公司开发的免费软件了。
GPL具有“传染性”,只要在一个软件中使用(“使用”指类库引用,修改后的代码或者衍生代码)GPL协议的产品,则该软件产品必须也采用 GPL协议,既必须也是开源和免费。
由于GPL严格要求使用了GPL类库的软件产品必须使用GPL协议,对于使用GPL协议的开源代码,商业软件或者对代码有保密要求的部门就不适合集成/采用作为类库和二次开发的基础。其它细节如再发布的时候需要伴随GPL协议等和BSD/Apache等类似。
GPL的出发点是代码的开源/免费使用和引用/修改/衍生代码的开源/免费使用,但不允许修改后和衍生的代码做为闭源的商业软件发布和销售。
2. 查看已安装的模块文件:lsmod
四、驱动常用API
1. 入口函数
static int __init 函数名(void)
{
return 0;
}
2. 出口函数
static void __exit 函数名(void)
{
}
3. 声明驱动模型出/入口函数
module_init(入口函数名);
module_exit(出口函数名);
4. printk内核输出函数
printf
函数用于应用层,printk
函数用于内核层。
printk函数是Linux内核中用于输出消息的函数。它类似于C语言中的printf函数,但是用于内核空间的打印,因此它有一些特定的用法和限制。printk不能使用浮点功能,不能有 %f,%lf,其余功能和printf一样。
printk是有消息等级的,消息等级如下所示。只有当消息等级大于控制台等级时(值越低等级越高),printk输出的消息会被打印到终端。
宏定义 | 消息等级 | 描述 |
---|---|---|
KERN_EMERG | “0” | 紧急级别消息(Emergency) |
KERN_ALERT | “1” | 警报级别消息(Alert) |
KERN_CRIT | “2” | 严重级别消息(Critical) |
KERN_ERR | “3” | 错误级别消息(Error) |
KERN_WARNING | “4” | 警告级别消息(Warning) |
KERN_NOTICE | “5” | 注意级别消息(Notice) |
KERN_INFO | “6” | 信息级别消息(Info) |
KERN_DEBUG | “7” | 调试级别消息(Debug) |
使用命令:cat /proc/sys/kernel/printk
查看内核打印等级。一般内核的输出内容会存到日志文件中,使用命令:dmesg
查看日志输出内容。
●如何将内核打印信息输出到控制台?
(1)方法一:设置printk的消息等级:printk(KERN_ERR "hello\n");
在输出内容前添加消息等级的宏定义。若不设置消息等级,则为默认的消息等级。
(2)方法二:修改内核的消息等级:echo 0 4 1 7 >/proc/sys/kernel/printk
,修改控制台等级或默认消息等级。
5. 导出符号
用于多个ko文件公用一个函数时,需要使用导出符号,这里传入函数名即可。
EXPORT_SYMBOL( )
6. copy_from_user
是 Linux 内核中用于从用户空间拷贝数据到内核空间的函数。这个函数是确保内核空间和用户空间之间数据安全传输的重要机制之一。因为直接访问用户空间内存是不安全的,内核必须通过这些函数来处理数据的传递,以确保不会因为非法内存访问导致系统崩溃。
unsigned long copy_from_user(void *to, const void __user *from, unsigned long n);
//to: 指向内核空间的目标缓冲区。
//from: 指向用户空间的源缓冲区。
//n: 要拷贝的字节数。
7. copy_to_user
用于将数据从内核空间拷贝到用户空间。这个函数用于内核模块和设备驱动程序中,以安全的方式将数据传递给用户空间。
unsigned long copy_to_user(void __user *to, const void *from, unsigned long n);
//to: 指向用户空间的目标缓冲区。
//from: 指向内核空间的源缓冲区。
//n: 要拷贝的字节数。
8. container_of(已知结构体某成员,获取结构体首地址)
是一个非常有用的宏,广泛用于 Linux 内核代码中。它用于从结构体成员的指针获取包含该成员的结构体的指针。
//宏定义原型:
#define container_of(ptr, type, member) ({ \
const typeof( ((type *)0)->member ) *__mptr = (ptr); \
(type *)( (char *)__mptr - offsetof(type,member) );})
/*
ptr:已知的结构体成员的指针。
type:结构体类型。
member:成员在结构体中的名字。
*/
举例:
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/init.h>
#include <linux/list.h>
struct my_struct {
int field1;
struct list_head list; //假设已知该成员
};
static int __init my_module_init(void)
{
struct my_struct *ptr; //获取结构体首地址。
//初始化并填充结构体
struct my_struct my_instance;
my_instance.field1 =1;
INIT_LIST_HEAD(&my_instance.list); //初始化为空链表。
// 使用 container_of 从 my_instance.list 指针获取 my_instance 的指针
ptr = container_of(&my_instance.list , struct my_struct, list);
printk(KERN_INFO "field1: %d\n", ptr->field1);
return 0;
}
static void __exit my_module_exit(void)
{
printk(KERN_INFO "Module exit\n");
}
module_init(my_module_init);
module_exit(my_module_exit);
MODULE_LICENSE("GPL");
使用:dmesg
查看内核日志。
五、内核源码头
★重要:源码头路径:/usr/src/
或/lib/modules
下。使用uname -r
查看当前系统内核版本。
在编写内核时,必须要用到开发板的内核源码文件中的最顶层Makefile文件。这里我使用香橙派5Plus开发,具体使用详情查看官方开发手册即可。里面有详细的步骤操作。
对于香橙派5plus,通常我们在开发板中安装的镜像包会有包含内核的头文件的.deb包 (路径查看开发板手册获得) ,我们只需要安装这个.deb包即可。安装完成后,会有一个文件夹。这个文件夹里就包含了我们所需要的最底层的Makefile文件。如果没有这个.deb包,则需要我们手动下载源码文件,然后编译生成.deb文件,然后在开发板上安装。
每个开发板不一样。最终思想就是获取到开发板内核源码文件并编译好(生成一些必须的文件)即可。
●注意:通常我们会将源码包文件放在Linux_X86上进行开发(有交叉编译器),这是因为如果我们在开发板上开发的话,就需要将源码文件拷贝在开发板上,这时如果源码文件较大,而开发板内存又小时,会浪费开发板资源。所以通常我们会在Linux_X86上开发,然后将生成的模块包文件上传至开发板安装。
六、编写/生成内核模块文件
Linux内核模块编写1(一个c文件生成一个ko文件)
Linux内核模块编写2(多个c文件生成一个ko文件)
Linux内核模块编写3(多个c文件生成多个ko文件)
七、注意
Makefile
改:此处为最顶层的Makefile文件。
hello.c