内核模块编译与内核编程总结

1 linux内核

在Linux内核中,使用task_struct结构体来描述进程,该结构体中包含描述该进程内存资源、文件系统资源、文件资源、tty资源、信号处理等的指针。Linux的线程采用轻量级进程模型来实现,在用户空间通过pthread_create() API创建线程的时候,本质上内核只是创建了一个新的task_struct,并将新task_struct的所有资源指针都指向创建它的那个task_struct的资源指针

绝大多数进程(以及进程中的多个线程)是由用户空间的应用创建的,当它们存在底层资源和硬件访问的需求时,会通过系统调用进入内核空间。

有时候,在内核编程中,如果需要几个并发执行的任务,可以启动内核线程,这些线程没有用户空间。
pid_t kernel_thread(int (*fn)(void *), void *arg, unsigned long flags);

在Linux内核中增加程序需要完成以下3项工作。

  • 将编写的源代码复制到Linux内核源代码的相应目录中。
  • 在目录的Kconfig文件中增加关于新源代码对应项目的编译配置选项。
  • 在目录的Makefile文件中增加对新源代码的编译条目。

一般而言,驱动开发者会在内核源代码的drivers目录内的相应子目录中增加新设备驱动的源代码或者在arch/arm/mach-xxx下新增加板级支持的代码,同时增加或修改Kconfig配置脚本和Makefile脚本,具体执行完全仿照上述过程即可

2 内核编译

2.1、必须使用Makefile

2.2、脚本模板:

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


MODULE_LICENSE("GPL v2");
/*
Linux内核提供一个宏来实现模块的参数传递
module_param函数 (内核源码/include/linux/moduleparam.h)

#define module_param(name, type, perm)
以上代码中的module_param函数需要传入三个参数:
name: 我们定义的变量名;
type: 参数的类型,目前内核支持的参数类型有byte,short,ushort,int,uint,long,ulong,charp,bool,invbool。
其中charp表示的是字符指针,bool是布尔类型,其值只能为0或者是1;
invbool是反布尔类型,其值也是只能取0或者是1,但是true值表示0,false表示1。
变量是char类型时,传参只能是byte,char * 时只能是charp。
perm: 表示的是该文件的权限。

注意 :每个模块的参数最后在sysfs文件系统的**/sys/module/模块名字/parameters/**路径下看到以参数名命名的文件。这个文件的权限就是perm指定的权限。如果perm的值为0,则在sysfs文件系统中不会生成参数对应的文件。

B.在模块里面,声明一个数组(全局数组),用来接收用户加载模块时传递的参数 module_param_array**(name, type, nump, perm)**
	@name 模块的数组名,也是外部指定的参数名
	@type 模块数组的数据类型
	@nump 用来获取用户在加载模块时传递的参数个数,NULL:不关心用户传递的参数个数
	@perm 访问权限

C.在模块里面,声明一个字符数组(全局数组),用来接收用户加载模块时传递的参数module_param_string**(name, string, len, perm)**
	@name 在加载模块时,参数的名字
	@string 模块内部字符数组的名字
	@len 模块内部字符数组的大小
	@perm 访问权限
*/
static int x =0;
module_param(x, int, 0); // 权限为0,无法查看参数

static char y = 'A';
module_param(y, byte, 0660);

static char *z = "ABCD";
module_param(z, charp, 0660);

static bool a = 0;
module_param(a, bool, 0660);

/*
在C语言中,static关键字的作用如下:
static修饰的静态局部变量直到程序运行结束以后才释放,延长了局部变量的生命周期;
static的修饰全局变量只能在本文件中访问,不能在其它文件中访问;
static修饰的函数只能在本文件中调用,不能被其他文件调用。

内核模块的代码,实际上是内核代码的一部分, 
假如内核模块定义的函数和内核源代码中的某个函数重复了, 编译器就会报错,导致编译失败,
因此我们给内核模块的代码加上static修饰符的话, 那么就可以避免这种错误。
*/
static int __init helloinit(void)
{
    /*
    printf:glibc实现的打印函数,工作于用户空间
    内核模块无法使用glibc库函数,内核自身实现的一个类printf函数,但是需要指定打印等级。
    #define KERN_EMERG “<0>” 通常是系统崩溃前的信息
    #define KERN_ALERT “<1>” 需要立即处理的消息
    #define KERN_CRIT “<2>” 严重情况
    #define KERN_ERR “<3>” 错误情况
    #define KERN_WARNING “<4>” 有问题的情况
    #define KERN_NOTICE “<5>” 注意信息
    #define KERN_INFO “<6>” 普通消息
    #define KERN_DEBUG “<7>” 调试信息
    */
    printk(KERN_INFO"hello module init\n");
    printk(KERN_WARNING"module param x: %d\n", x);
    printk(KERN_WARNING"module param y: %c\n", y);
    printk(KERN_WARNING"module param z: %s\n", z);
    printk(KERN_WARNING"module param a: %d\n", a);
    return 0;
}

static void __exit helloexit(void)
{
    printk(KERN_INFO"hello module exit\n");
}

MODULE_AUTHOR("jk-luo");
MODULE_ALIAS("first module");
MODULE_DESCRIPTION("THIS IS A HELLO MODULE\n");
module_init(helloinit);
module_exit(helloexit);

2.3、Makefile模板

ifneq ($(KERNELRELEASE),)

PWD := $(shell pwd)
KVER ?= $(shell uname -r)
KDIR := /lib/modules/$(KVER)/build
all:
	$(MAKE) -C $(KDIR) M=$(PWD)
clean:
	rm -rf .*.cmd *.o *.mod.c *.ko .tmp_versions

else
	obj-m := hello.o
	hello-objs := hello.o

make时可能遇到的报错make:*** No targets specified and no makefile found.Stop.“。原因:

  1. 未安装内核编译的头文件等:yum install kernel-devel
  2. /lib/modules/$(shell uname -r)/build目录为空,此时可修改makefile文件中的路径为/usr/src/kernels/$(shell uname -r)

2.4、内核头文件

编译内核模块需安装kernel-devel***.rpm包:yum install kernel-devel

2.5、卸载驱动模块ko

卸载命令:rmmod <module_name>

注:module_name是lsmod显示的模块名称,而不是对应的ko文件名

2.6、加载驱动模块test.ko

  • 方法一
    进入test.ko驱动模块文件所在的目录,然后直接insmod test.ko

  • 方法二
    将test.ko文件拷贝到/lib/module/#uname-r#/目录下,这里,#uname -r#意思是,在终端中输入
    uname -r后显示的内核版本及名称,例如mini2440#uname-r#就是2.6.32.2-FriendlyARM。然后 depmod(会在/lib/modules/#uname -r#/目录下生成modules.depmodules.dep.bb文件,表明模块的依赖关系)。最后 modprobe test(注意这里无需输入.ko后缀) 即可。

两种方法的区别

  1. modprobe和insmod类似,都是用来动态加载驱动模块的,区别在于modprobe可以解决load module时的依赖关系,它是通过/lib/modules/#uname -r/modules.dep(.bb)文件来查找依赖关系的;而insmod不能解决依赖问题。也就是说,如果你确定你要加载的驱动模块不依赖其他驱动模块的话,既可以insmod也可以modprobe,当然insmod可以在任何目录下执行,更方便一些。而如果你要加载的驱动模块还依赖其他ko驱动模块的话,就只能将模块拷贝到上述的特定目录,depmod后再modprobe。

  2. 使用modprobe命令加载的模块若以modprobe -r filename的方式卸载,将同时卸载其依赖的模块.

2.7、查询已加载的ko

查询kolsmod|grep test_module

lsmod命令实际上是读取并分析/proc/modules文件

查询ko加载日志dmesg

内核中已加载模块的信息也存在于/sys/module目录下,加载hello.ko后,内核中将包含/sys/module/hello目录,该目录下又有一个refcnt文件和一个sections目录,在/sys/module/hello目录下运行tree –a可得到目录树

查询模块的信息modinfo <模块名>

加载ko遇到的问题及解决方法:

insmod: ERROR: could not insert module /root/dpdk/build/kernel/linux/igb_uio/igb_uio.ko: Required key not available
#环境执行后显示下面内容,说明打开了UEFI Secure Boot,再BIOS 菜单Boot中关闭可以解决。


3 设备驱动模型

3.1 设备与驱动分离

驱动注册的时候寻找设备

设备注册的时候寻找驱动

总线驱动来匹配match

linux内核代码,虽然使用了C语言,但是处处体现了面向对象的设计思想,不然无法兼容那么多平台。

总线、设备、驱动都继承自kernel_object。

3.1 主机驱动与client驱动分离

I2c adpater与I2c client

Spi adpater和Spi client

3.1 复杂设备驱动

层次化、结构化,上层不依赖于具体硬件,下层与硬件接口


4 Linux内核模块

Linux内核中80%都是驱动代码。内核模块三种形态:

第一种独立编译加载,类似于动态链接库,在需要的时候insmod,好处是控制了内核的大小;第二种直接编译进kernel;第三种形态是编译内核的时候编译为一个内核模块,而不是编译到kernel。

modInfo:查看模块信息,如modinfo module_name.ko

file:查看目标文件信息,如file obj-file

模块的加载函数和卸载函数:module_init(foo_Init);module_exit(foo_Exit)加载和卸载的时候回执行。

4.1 导出模块的symbol

使用EXPORT_SYMBOL(func_name),将函数导出到全局,供其他模块调用。

对于一个内核模块,加载函数和卸载函数并不一定是必须的。比如一个模块只是为了导出符号供全局调用,就不需要module_init和module_exit

4.2 模块可以接受参数

模块中可以通过module_param(num, int, S_IRUGO)来声明接受的参数。

5 Linux内核编程与调试

多个线程并发访问同一个结构体,要解决并发问题。

5.1 原子操作:针对一个整型变量

#include "linux/atomic.h"

atomic_set;
atomic_add;
atomic_sub;
atomic_inc;
atomic_inc_and_test;
atomic_sub_and_test;
atomic_add_return;

5.1 自旋锁和信号量:针对多个东西

这里的多个东西,是指由多行代码组成的语义完整的整体,比如:账户A向账户B转100元,账户C也向账户B转100元,账户D向账户A转10元,为了保证转账正确性,必须对账户A向账户B转100元这一操作加锁,这一个操作就是一个语义完整的整体,也是一个原子任务。为了解决这种并发问题,可以使用自旋锁或信号量。

5.1.1 自旋锁spinlock

当访问一个互斥的临界区资源时,如果没有拿到锁,就在原地打转,直到拿到锁为止。(忙等待,无调度开销;进程抢占被禁止,锁定期间不能睡觉)

  • spinlock更多的是在多核系统使用;
  • 单核的spinlock,不可能出现spinlock拿不到的情况;因为spinlock拿不到的情况是进程在原地打转,但是单核时别人拿到了自己打转的机会都没有。所以单核情况spinlock锁住的临界区域,退化为禁止内核调度或者禁止内核抢占
  • 多核情况,两个线程可能访问同一片临界区域;当一个线程拿到了spinlock,另外一个想拿就原地打转。
#include "linux/spinlock_types.h"

spinlock_t lock;
spin_lock_init(&lock);
spin_lock(&lock); // 获取自旋锁,保护临界区
spin_unlock(&lock); // 解锁

spinlock适用于锁保持时间很短的场景,调度开销比锁等待更大。spinlock锁住的区域一定不要引起睡眠。对于锁住的区域可能睡眠,就不要用spinlock,比如等待一个队列、信号量、互斥量、申请一片内存都可能引起睡眠。

5.1.2 信号量

当访问一个互斥的临界区资源时,如果拿不到锁,不会原地打转(拿不到就切进程,有调度开销;锁定期间可以睡觉,不用于终端上下文),而是放弃CPU让别人去跑;直到资源满足要求,并唤醒之后,再去拿锁。

5.2 completion模型

completion 主要用于同步,执行当前任务依赖于某一个任务执行完成的模型。

#include <linux/completion.h>
// completion 主要用于同步,执行当前任务依赖于某一个任务执行完成的模型。
struct completion done;
init_completion(&done)
wait_for_completion(&done);

5.3 Linux进程状态:等待队列

5.4 Linux内核延时

5.4.1 忙等待

等待时间很短,不引起睡眠时使用忙等待。 忙等待消耗CPU,等待期间CPU空转,所以不要轻易用。
void ndelay(unsigned long nsecs)

5.4.2 睡着延时

5.5 Linux内核定时器

5.6 Linux中断底半部机制

5.7 DMA

直接内存访问。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

一个两个四个三

你的鼓励将是我创作的最大动力

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

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

打赏作者

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

抵扣说明:

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

余额充值