Linux内核开发_内核模块与驱动模块

内核模块是什么?

Linux下的内核模块类似于Windows下的DLL动态链接库技术,和我们平常所使用的一些动态链接的SDK库一样,只是调用者是内核而已,不是用户态的程序。

内核模块拥有的的权限是和用户态一样吗?

内核模块是活跃在Linux内核中的,它的权限更多,与内核空间下的进程一样的权限,比用户态的权限要高,意味着它拥有对物理设备以及内存中其它空间的访问权限,一般嵌入式开发时内核模块用到的比较多,用于开发LED模块等等。

内核是如何维护模块的?

内核在加载模块时会将模块的符号信息写入一张符号表里,这个表里记录着所有模块的符号地址,用于维护函数之间的调用。

此外内核还会去分析模块之间的依赖性,所以内核还要去维护模块之间的依赖性,如有些模块依赖别的模块的功能,内核加载模块前需要校验当前内核是否有依赖的模块在运行,否则停止加载此模块,不然会产生出乎意料的问题。

内核模块在内核中是如何运行的?

当内核把模块加载到内核空间时,模块就像用户态的程序一样,被加载到内存空间了,只是加载到的空间地址不一样,只是把这一个程序加载到内核空间去了,而不是用户空间,所以内核模块其实运行和用户态的程序没有区别,只是权限不一样。

内核模块有什么优点?

对于Linux内核来说,这样的好处在于可以将一部分功能以模块的方式分割出去,从而节省内核体积,因为有许多功能可能平常内核压根用不到,如果加载到内核空间去,会浪费系统资源,第一占用内存,第二内核态的程序会被单独一个核执行,即便这个内核模块一直执行的是while(0)这样的指令,也会浪费CPU效率,所以这样做法能节省内存资源,以及系统调用。

内核模块有什么缺点?

缺点显而易见,内核模块拥有和内核态一样的权限,如果一些不好的内核模块可能导致电脑死机,或者一些出乎意料的事情发生,让整个系统混乱或者崩溃,访问了一些PC机上规定一些只读区域,或者修改了一些硬件资源,修改GDT与LDT等都有可能导致系统混乱。

其次是内核模块编写时只能使用内核态提供的函数,而且不能去进行浮点数运算,至于为什么可见我写的这一篇文章:Linux内核开发_2_Initramf_17岁boy想当攻城狮的博客-优快云博客_initram文件系统原理

刚刚说内核模块必须使用内核提供的函数,如果内核版本与内核模块版本不一致,就会导致一些奇怪的问题,如内核迭代过程中某些函数被废弃,或者内核的一些特性改了,都会导致内核模块无法正常执行。

内核模块与驱动模块的区别?

内核模块与驱动模块其实没有任何区别,编写方法都是一样的,都是.ko文件,只不过功能不一样,内核模块只是利用自身的优先级来去执行一些在应用层没法实现的事情,例如拦截内核中断处理实现钩子技术等,同时内核模块不会去访问硬件设备,它仅为应用层提供一些应用层没有权限去做的事情或为内核提供一些其它功能,例如hash算法之类的,内核与应用层是完全隔离开的只能通过中断来切换用户态和内核态,而驱动则是实打实的为应用层提供一层访问硬件的API并且创建设备描述文件,例如/dev下的某个设备描述文件,同时会实现通用VFS接口例如:open、wirte、read这些接口给到应用层,内核模块一般是通过中断来与应用层进行调用交互的。

内核模块如何编写?

我们可以在任何Linux发行版上进行内核模块开发,内核模块开发流程比较简单,就像我们平时写c语音代码那样就可以了,只是需要符合一些规则,其次不能使用自定编译器编译,我们只能使用makefile进行编译,且makefile需要符合kbuild规则。

什么是kbuild?

kbuild是Linux内核项目里所使用的一种Makefile编译脚本,它基于Makefile,扩展了一些写法,也就是说我们开发的内核模块需要符合Linux内核的代码规则,写法是很统一的,这是Linux为了防止不同开发者有不同的编写风格,为了风格一致而提出的解决方案,不然那么多开发者们所编写代码,岂不是乱糟糟的,我相信这一点对于已经工作的小伙伴们来说非常容易理解,因为公司都有自己的代码风格,Linux内核也是。

内核模块有几种编译方式?

有两种,第一种是基于Linux内核的编译,也就是说把我们的内核模块代码文件,放入到Linux内核项目里,然后在Makefile文件里增加我们的内核模块文件路径,最后在config里配置选上我们的内核模块,然后执行make编译Linux内核,这样我们的内核模块就会被附加到Linux内核里了。

第二种是单独编译,这个也是基于Linux内核,只是我们可以在任意磁盘文件上对这个内核模块进行编译,但是需要自己编写Makefile文件,并且在Makefile文件里需要指定Linux内核源码路径,因为kbuild编译需要加载Linux内核里的一些库,这也是我们上面说过,为什么某些模块只能在对应的内核版本里运行,因为编译依赖,我们我们直接make,就会生成.ko文件,这个是Linux内核模块文件扩展名,这样一个单独编译的内核模块就生成了,可以使用“insmod”命令去加载模块,在任意发行版上加载,需要root权限。

开始编写Linux内核模块

第一步创建.c文件,不能使用c++或者其它编程语言,需要符合Linux内核项目体系,Linux是c语言和少量汇编写的,所以编译器基本上是GCC还有GNU其它的一些项目编译器

创建一个.c文件,文件名可以随便起,这个是没有限制的

这里我创建一个名为“hello”的c语言源文件,并进入编辑模式

vim hello.c

写代码前,先介绍几个宏定义:

当我们编写完模块后使用“modinfo”命令可以查看模块的一些信息,使用这些宏可以定义我们的信息

MODULE_AUTHOR 定义模块作者

MODULE_LICENSE 协议许可证 一般定义为GPL,注意因为是Linux内核上,我们使用的都是GNU的编译器,一般只能是GPL,因为这是GNU组织的规定,如果你使用它们的产品开发软件,许可证上必须是GPL,GPL协议的作用可以互联网搜索一下

MODULE_DESCRIPTION 说明,模块说明

MODULE_ALIAS 模块别名

“MODULE_AUTHOR ”和“MODULE_LICENSE ”是必须要写的,这样才能符合Linux内核模块的标准,“MODULE_DESCRIPTION”和“MODULE_ALIAS ”可选

在介绍两个函数

module_init 定义入口函数,也就是定义当模块加载后执行的第一个函数,启动函数

module_exit 定义退出函数,当模块被卸载后,Linux内核会去调用这个函数

类似Windows上的进入与退出事件

在模块里是没有Main函数的,因为模块是被链接,而非直接编译成elf文件,模块最终会被编译成.o文件,然后在格式化成.ko文件,这些文件是库文件,不允许有主函数,它们只能被Linux内核加载,并调用,这相当于一个主从关系。

其次这里说一下编写init和exit函数时需要在后面加上_init

如hello_init,这样写函数命名符合Linux的一些关键函数规范,当然你也可以不加,这些是Linux内核的代码规范,建议加上

首先第一步我们需要包含三个常用的基本头文件:

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

介绍一下三个头文件的作用:

linux/module.h 包含了一些常用的宏函数还有一些基本数据结构,MODULE_AUTHOR 这些宏就包含在这个头文件上

linux/init.h 包含一些常用初始化函数,如module_init这些函数就存在于这个头文件下

linux/moduleparam.h 包含了一些用户态向内核态模块传递参数的函数,当模块被加载到内核态时,我们传递参数的话是不能像用户态那样,必须是使用内核态的一些获取参数方法

具体可以进入到Linux源代码下去看一下这些头文件里的函数定义

这里第一步我们不在定义main函数咯,而是先注册好一些基本信息,这里我就不注册别名之类的了

MODULE_AUTHOR("Kevin Taylor");
MODULE_LICENSE("GPL");

编写一个init函数:

static int __init hello_init(void){


}

这里要定义成静态函数,防止被其它源文件调用,__init是Linux内核下的一个宏,注册函数要求前面加上这个

在定义一个退出函数:

static void __exit hello_exit(void){

}

我们在使用module_init与module_exit注册一下:

module_init(hello_init);
module_exit(hello_exit);

这样一个基本的Linux模块代码基本架构就完成了。

注意这里不能使用任何用户态的代码,如printf,还有open等等这些函数,这些函数都是基于Linux内核里的函数实现的,所以这里我们需要使用Linux内核态的一些函数,如printk就是打印函数

所以这里我们使用printk进行打印:

先介绍一下printk吧:

printk参数与printf一样,但是printk的打印是有级别的,且打印出的数据是不会出现在终端tty的,而是放入到内核日志缓冲区里,
而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/*调试级别的消息*/

默认是KERN_WARNING,根据打印级别的不同在日志中显示的格式也不一样,数字越小,优先级越高,如果优先级低于4,那么Linux内核会除了将其输出到内核日志缓冲区里,也会打印到终端里。

这里我们在init里加入一行printk函数:

printk("hello linux kernel");

在退出也加入一行

printk("bye linux kernel");

我们现在无需指定内核的优先级,因为默认是KERN_WARNING,也就是说这种情况下只会输出到内核日志缓冲区里。

那么接下来就要开始写makefile了

vim Makefile

这里先给大家看一段kbuild的Makefile代码

KDIR = /home/zhihao/linux5.2.0
ifneq ($(KERNELRELEASE),)
        obj-m  := hello.o
else
        KDIR ?= /lib/modules/`uname -r`/build
all:
        $(MAKE) -C $(KDIR) M=$(PWD) modules
clean:
        $(MAKE) -C $(KDIR) M=$(PWD) clean
endif
obj-m  := hello.o

这里的obj-m是kbuild里的语法表示编译可生成的模块,这里不需要写hello.c,kbuild编译器会自动推到.c文件

如给的是hello.o,最后会寻找hello.c然后编译成hello.o

这里的

ifneq ($(KERNELRELEASE),)

这里是一个条件分支,用来判断KERNELRELEASE变量是否存在

KERNELRELEASE变量是存在于Linux内核目录下顶层目录里Makefile里的一个变量,作用就是用来判断是否为第一次编译

第一次如果没有执行的话,则转入到else里去,else执行完成之后则判断是all还是clean,如果什么都没有直接make则认为是all

那么问题来了,肯定会想,既然obj-m :=hello.o这个没有被执行,那么make是如何知道我们要编译hello.c呢?

答: obj-m属于一个变量,kbuild会读取这个变量里的值的,经过我测试,发现make里声明变量,无论你放在任何位置,make都会首先把变量声明出来,在去执行,也就是说即便obj-m放在ifneq下,即便表达式不成立make也会将其声明出来,上面这段代码可以改成这个样子:

KDIR = /home/zhihao/linux5.2.0
obj-m  := hello.o
ifneq ($(KERNELRELEASE),)
       
else
        KDIR ?= /lib/modules/`uname -r`/build
all:
        $(MAKE) -C $(KDIR) M=$(PWD) modules
clean:
        $(MAKE) -C $(KDIR) M=$(PWD) clean
endif

KDIR是内核目录,这里是调用内核里的kbuild来编译我们的模块,可以看到make里面的-C命令用来指定编译Makefile路径

如果不是第一次编译则不去执行内核目录下的模块编译make,否则就去执行一下

all和clean就比较容易理解了,就是去执行内核对应路径下的make而已

直接make一下:

编译完成,看一下生成的文件:

其中hello.ko就是我们需要的内核模块了,其它的都是临时文件

加载内核模块

加载命令是:insmod

卸载命令是:rmmod

查看已加载的模块:lsmod

用法:

注意要sudo,内核权限

sudo insmod hello.ko

加载完成后使用dmesg | tail命令查看模块输出信息

可以看到打印出来啦

使用lsmod可以查看已加载的模块,在里面找找我们的模块

格式:lsmod

可以看到模块非常多,我们联合grep来寻找我们的模块:

lsmod | grep hello 注意不需要.ko

找到啦,正在正常的运行

在卸载掉这个模块,然后看一下是否打印出退出的日志信息:

sudo rmmod hello 

看一下日志:

成功~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

17岁boy想当攻城狮

感谢打赏

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值