Linux内核模块开发

内核模块简介

驱动程序通常是以内核模块的形式存在。
Linux内核的整体结构非常庞大,其包含的组件也非常多,如果把所有的组件都编译进内核文件,即:zImage或bzImage,这样会导致内核文件过大, 占用内存过多。解决方法是让内核文件本身并不包含某组件,而是在该组件需要被使用的时候,动态地添加到正在运行的内核中,而这些组件就是内核模块。

由此可得知内核模块的特点有:
①模块本身并不被编译进内核文件中
②根据需求,在内核运行期间动态地安装或卸载

一个内核模块开发例程:

#include <linux/init.h>
#include <linux/module.h>
static int hello_init(void)
{
    printk(KERN_WARNING"Hello, world !\n");
    return 0;
}
static void hello_exit(void)
{
    printk(KERN_INFO "Goodbye, world\n");
} 
module_init(hello_init);
module_exit(hello_exit);

由上面的例程,我们可以发现:
①一般的应用程序都有main()函数,该函数是程序的入口。但内核模块并没有main()函数。实际上,内核模块不仅有入口函数,还有出口函数,入口函数又叫做加载函数,出口函数又叫做卸载函数。当将编译后的内核模块安装到内核时,调用的函数就是加载函数;当将内核模块卸载时调用的函数就是卸载函数。通过module_init()来指明加载函数,module_exit()指明卸载函数。
②头文件:<linux/init.h><linux/module.h>。内核模块不能使用用户空间的库函数,即不能使用/usr/include下的头文件,只能使用/usr/src下包含的头文件。
③static是为了防止函数命名污染

这里写图片描述

对比应用程序,内核模块具有以下不同:
①应用程序是从头(main)到尾执行任务,执行结束后从内存中消失。
②内核模块的初始化函数结束时,模块仍然存在于内核中,直到卸载函数被调用,模块才从内核中消失(系统关闭也会使内核模块消失)


Makefile编写

编写好程序后,接下来就要编写Makefile文件来编译内核模块(ps:名称为Makefile,不能为makefile)。如需要编译的文件为hello.c,则Makefile内容可以为:

obj-m :=hello.o
KDIR :=/lib/modules/$(shell uname -r)/build
all :
    make -C $(KDIR) M=$(PWD) modules

如果需要编译的文件不止一个源文件,如有hello.c,printf.c,则Makefile可编写为:

obj-m :=test.o
test-objs :=hello.o printf.o
KDIR :=/lib/modules/$(shell uname -r)/build
all :
    make -C $(KDIR) M=$(PWD) modules

生成的内核模块文件为test.ko

一般采用的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 *.unsigned *.order
endif

①KERNELRELEASE是在内核源码的顶层Makefile中定义的一个变量。ifneq($(KERNELRELEASE),) 判断该变量是否为空。在第一次读取执行此Makefile时,KERNELRELEASE没有被定义,所以make将读取执行else之后的内容。
KDIR := /lib/modules/ $(shell uname -r) /build 是给KDIR这个变量赋值,值为当前linux运行的内核源码。
③当make的目标为all时,-C $(KDIR) 指明跳转到内核源码目录下读取那里的Makefile;M=$(PWD)表明然后返回到当前目录继续读入、执行当前的Makefile。当从内核源码目录返回时,KERNELRELEASE已经被定义,kbuild也被启动去解析kbuild语法的语句,make将继续读取else之前的内容。
④可以把上述的Makefile文件作为一个模版,只需要改动obj-m := hello.o这条语句就可以了(该Makefile编译生成的内核模块运行在pc机Linux系统上,可通过修改KDIR改换运行环境)


可选模块

安装内核模块使用的命令是insmod,如:insmod hello.ko
卸载内核模块使用的命令是rmmod,如:rmmod hello
查看安装的内核模块使用的命令是lsmod

内核模块有一些可选模块,包括
①模块声明
②模块参数
③符号输出

模块声明

在Linux的文件module.h中包含着一些宏,这些宏的作用是用来对模块的声明和描述。下面我们把这些宏中常用的罗列在下面。

MODULE_AUTHOR (author);                    //声明模块的作者
MODULE_DESCRIPTION (description);          //声明模块的描述
MODULE_VERSION (version_string);           //声明模块的版本
MODULE_DEVICE_TABLE (table_info);          //声明模块的设备表
MODULE_ALIAS (alternate_name);             //声明模块的别名
MODULE_LICENSE("GPL");                     //声明模块的许可证

模块参数

通过宏module_param指定保存模块参数的变量。模块参数用于在加载模块时传递参数给模块。module_param(name,type,perm)
name:变量的名称
type:变量类型,bool:布尔型 int:整型 charp:字符串型
perm是访问权限。 S_IRUGO:读权限 S_IWUSR:写权限
例如:

int a;
module_param(a,int, S_IRUGO);
char *buf;
module_param(buf,charp, S_IRUGO);

一个例子:

#include <linux/init.h>
#include <linux/module.h>
MODULE_AUTHOR("jx");
MODULE_LICENSE("GPL");
int a;
module_param(a,int, S_IRUGO);
char *buf;
module_param(buf,charp, S_IRUGO);
static int hello_init(void)
{
    printk("<0>""a is :%d\n",a);
    printk("<0>""buf is :%s\n",buf);
    printk("<0>""Hello, world !\n");
    return 0;
}
static void hello_exit(void)
{
    printk(KERN_INFO "Goodbye, world\n");
} 
module_init(hello_init);
module_exit(hello_exit);

在安装该内核模块时,需要输入模块参数,,如:insmod hello.ko a=10 buf=hello

符号输出

假如有两个内核模块printf.ko,hello.ko,其中hello.ko中需要使用printf.ko里面定义的函数,可以在printf.c中将该函数导出,当编译并安装好printf.ko后,其它内核模块就可以使用该函数了。
内核符号的导出使用宏
EXPORT_SYMBOL(符号名)
EXPORT_SYMBOL_GPL(符号名)
说明:EXPORT_SYMBOL_GPL只能用于包含GPL许可证的模块

printf.c程序代码:

#include <linux/init.h>
#include <linux/module.h>
int printf_hello()
{
    printk("<0>""hello world\n");
    return 0;   
}
EXPORT_SYMBOL(printf_hello);
static int printf_init(void)
{
    return 0;
}
static void printf_exit(void)
{
    printk(KERN_INFO "Goodbye, world\n");
} 
module_init(printf_init);
module_exit(printf_exit);

hello.c程序代码:

#include <linux/init.h>
#include <linux/module.h>
MODULE_AUTHOR("jx");
MODULE_LICENSE("GPL");
extern int printf_hello(void);
static int hello_init(void)
{
    printf_hello();
    return 0;
}
static void hello_exit(void)
{
    printk(KERN_INFO "Goodbye, world\n");
} 
module_init(hello_init);
module_exit(hello_exit);

必须先安装好printf.ko,然后才能成功安装hello.ko


printk函数

printk是内核中出现最频繁的函数之一,它与printf不同之处有:
①printk用于内核中的打印信息;printf用于应用程序
②printk根据严重程度,可以对打印的信息添加优先级

#define KERN_EMERG         "<0>"       /* 致命级:紧急事件消息,系统崩溃之前提示,表示系统不可用*/
#define KERN_ALERT         "<1>"       /* 警戒级:报告消息,表示必须采取措施*/
#define KERN_CRIT          "<2>"       /* 临界级:临界条件,通常涉及严重的硬件或软件操作失败*/
#define KERN_ERR           "<3>"       /* 错误级:错误条件,驱动程序常用KERN_ERR来报告硬件错误*/
#define KERN_WARNING       "<4>"       /* 告警级:警告条件,对可能出现问题的情况进行警告*/
#define KERN_NOTICE        "<5>"       /* 注意级:正常但又重要的条件,用于提醒*/
#define KERN_INFO          "<6>"       /* 通知级:提示信息,如驱动程序启动时,打印硬件信息*/
#define KERN_DEBUG         "<7>"       /* 调试级:调试级别的信息*/

没有指定优先级的printk默认使用DEFAULT_MESSAGE_LOGLEVEL优先级,它是一个在kernel/printk.c中定义的整数。

#define DEFAULT_MESSAGE_LOGLEVEL 4   /* KERN_WARNING */

通过优先级可以控制信息打印的地方,当优先级过低时,信息不会打印到终端。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值