3_Linux内核模块介绍

Linux内核模块介绍

  • 系统版本为:Ubuntu 18.04.1 LTS
  • 内核版本为:4.15.0-43-generic

为什么Linux系统会提供模块?

随着OS的演化,功能越来越多,就会有一个问题:Linux本身是一个单内核,它的效率高但是它的可扩展性和可维护性差,如果把所有的功能都放到OS的内核中,那么内核就会非常庞大并且笨拙,如果不放在内核中,那么这些功能要么不能运行,要么运行非常缓慢。

所以聪明的工程师们就发明了模块,来解决这个问题。

但是内核模块也是有缺点的,首先,加载卸载需要时间,会降低系统性能;模块需要管理结构和额外的代码,因此需要更多的内存;模块只能间接地访问内核资源,所以效率较低;最重要的是模块一旦被加载,就成了内核的一部分,就有可能使内核崩溃,也有安全隐患;最后还会有版本问题。

在开始模块的插入之前,我们需要先了解一些基本的内容,就是我们和Linux交互的方式,在用户态我们都是直接printf打印出来就好了,但是在Linux中,我们都是通过日志信息来了解系统状况的,Linux有专门的文件系统来给用户提供信息(/proc,之后会有专门的文章来介绍),在这里我们仅简单介绍一下。

1 从printf到printk

我们都知道,printf可以帮助我们在屏幕上打印出文字信息,但是这仅仅是在linux的用户态中使用的,不幸的是在内核中不能使用printf了。当linux运行起来以后,内核本身就会记录一些信息,并提供整个操作系统的状态信息,printk是最常见的linux内核向终端用户传递消息的方式。

printk和printf的区别就是printk可以指定消息的打印级别,内核根据这个级别来决定是否将消息打印到终端上:

内核使用printk()而不是printf()的原因是内核没有链接标准C函数库。但其实printk和printf函数的接口完全一样,可以在控制台显示1024个字符。

printk函数在运行的时候,首先设法获取控制台信号量,然后将要输出的字符存入控制台的日志缓冲区,再调用控制台驱动程序来刷新缓冲区。如果控制台无法获取控制台信号量,就只能把要输出的字符存储到日志缓冲区,并依赖拥有控制台信号量的进程来刷新这个缓冲区。注意在printk存储任何数据到日志缓冲区之前,必须使用日志缓冲区锁,这样才能保证并发调用printk时不会相互影响。如果已经获得了控制台信号量,那么刷新日志缓冲区之前,可以多次调用printk()函数,所以,printk不能来表明任何程序的运行时间。

2 dmesg

首先,linux内核有多种方式可以用于存储日志和信息。linux内核通过klogd()发送消息,并给它标上适当的警告级。所有级别的消息都存储在/proc/kmsg中。

dmesg是一个命令行工具,可以用于显示存储在/proc/kmsg中的缓冲区内容,并能够根据消息级别来选择是否要过滤这个缓冲区。

3 /var/log/meaasga

linux系统的/var/log/message下存储的是大多数已经记录的系统的信息。对于某些存储了已接收消息的特定位置,可以借助syslogd程序来读取/etc/syslogd.conf的内容。根据syslogd.conf中项的不同,日志信息可以存储到许多不同的文件中去,但是该文件在不同版本的linux系统中有所不同,不过一般是在/var/log/meaasga下。

4 编写模块

了解了以上内容之后,我们就可以来编写内核模块了,和每种编程语言类似,内核模块的编写也是有基本的框架的:c文件+Makefile文件。

C文件

我们在自己的文件夹下新建c文件,然后编写代码。

直接来看代码,我将所有需要知道的东西都写在注释中了:

# include <linux/module.h>	//对应使用module_init/module_exit,所有模块都要使用,这个文件必须被包含进来
# include <linux/kernel.h>	//包含常用的内核函数
# include <linux/init.h>	//包含了宏_init和_exit,告诉编译程序相关的函数和变量仅用于初始化,编译程序将标有_init的所有代码存储到特殊的内存段中,初始化结束后就释放这段内存


/*
 * 模块的初始化函数lkp_init():向内核注册模块提供新功能
 * __init是用于初始化的修饰符
 * printk()函数的使用:由内核定义,它把要打印的东西输出到终端或者日志里去,
 * */
static int __init lkp_init(void)
{
	printk("hello world\n");
	return 0;
}


/*
 *模块的退出和清理函数lkp_exit():注销由模块提供所有的功能
 * */
static void __exit lkp_exit(void)
{
	printk("<good googbye\n");
}

//模块初始化的入口点,对于内置模块,内核在引导时调用该入口点;对于可加载模块则在该模块插入内核时才调用。
module_init(lkp_init);
//对于可加载模块,内核在此处调用cleanup_module函数,而对于内置的模块,它没有什么用。
module_exit(lkp_exit);


/*
 * 模块的许可证声明GPL,提示可能没有GNU公共许可证
 * */
MODULE_LICENSE("GPL");
Makefile文件

注意,Makefile文件的M必须是大写,而且得和你要编译的c文件放在同一个目录。

接下来是代码:

obj-m:=first.o   

#这一行的-C的作用是告诉make程序读取Makefile或者做其他事情之前,先要改变Linux的源目录。
make -C /usr/src/linux-2.6.7 SUBDIRS=$PWD modules
  • 上面的obj-m :=这个赋值语句,意思是要使用目标文件first.o建立一个模块,最后生成的模块名是first.ko
  • 注意,Makefile的编写有一套专用的语法,如果有兴趣的话,可以去学习下。这里仅仅是简单的应用

来看这段代码,我们可以发现我们每次需要编译文件的时候,都需要确定一下我们自己的Linux系统中内核的版本,首先,查看内核的版本在终端中使用uname -r命令即可,但是我们并不需要这样子写,可以写成如下形式,移植性很强,以后只需要复制这个文档修改目标文件.o的名字即可使用:

#Makefile文件注意:假如前面的.c文件起名为first.c,那么这里的Makefile文件中的.o文件就要起名为first.o    只有root用户才能加载和卸载模块
obj-m:=first.o                          #产生first模块的目标文件
#目标文件  文件  要与模块名字相同
CURRENT_PATH:=$(shell pwd)             #模块所在的当前路径
LINUX_KERNEL:=$(shell uname -r)        #linux内核代码的当前版本
LINUX_KERNEL_PATH:=/usr/src/linux-headers-$(LINUX_KERNEL)

all:
    make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) modules    #编译模块
#[Tab]              内核的路径       当前目录编译完放哪  表明编译的是内核模块

clean:
    make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) clean      #清理模块
插入/检查/删除模块

在编写好c文件和Makefile文件之后,在终端中使用make命令,即可编译。编译后会生成一个.ko文件,这个就是模块。

在Linux中,插入删除模块要有超级用户权限,所以要在命令前加上sudo。插入模块:

sudo insmod first.ko

之后可以查看模块是否正确插入了:

lsmod

之后使用dmesg命令查看日志信息:

dmesg

删除模块只用写模块名字即可,不用写扩展名

sudo rmmod first
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值