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.“
。原因:
- 未安装内核编译的头文件等:
yum install kernel-devel
/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.dep
和modules.dep.bb
文件,表明模块的依赖关系)。最后modprobe test
(注意这里无需输入.ko后缀) 即可。
两种方法的区别
-
modprobe和insmod类似,都是用来动态加载驱动模块的,区别在于modprobe可以解决load module时的依赖关系,它是通过
/lib/modules/#uname -r/modules.dep(.bb)
文件来查找依赖关系的;而insmod不能解决依赖问题。也就是说,如果你确定你要加载的驱动模块不依赖其他驱动模块的话,既可以insmod也可以modprobe,当然insmod可以在任何目录下执行,更方便一些。而如果你要加载的驱动模块还依赖其他ko驱动模块的话,就只能将模块拷贝到上述的特定目录,depmod后再modprobe。 -
使用modprobe命令加载的模块若以
modprobe -r filename
的方式卸载,将同时卸载其依赖的模块.
2.7、查询已加载的ko
查询ko:lsmod|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
直接内存访问。