技术交流

好好学习,天天向上。

0%

符号依赖

模块版本校验通过内核选项**CONFIG_MODVERSIONS **打开,这是一项简单的ABI一致性检查。它会为每个导出的符号生成一个CRC值。当一个外部模块被加载使用的时候,内核导出符号的CRC值会和模块导出符号的CRC值做比较。如果两者不匹配,内核将拒绝将该模块加载到内核中。保存导出符号信息的是名为“Module.symvers”的文件,内核编译的时候就会生成一个Module.symvers文件,其中包含所有内核和各个模块导出的符号信息。其格式如下,如果内核没有开启CONFIG_MODVERSIONS,则CRC一栏是0x00000000.

1
2
3
<CRC>        <Symbol>           <module>

0x2d036834 scsi_remove_host drivers/scsi/scsi_mod

当编译一个外部模块的时候,编译系统需要读取内核编译时生成的Module.symvers文件,以检查模块需要的所有外部符号是否在内核中有定义且已导出。整个过程是在MODPOST阶段做的,MODPOST从内核源码树中获取内核的Module.symvers文件。如果在模块编译目录下也有Module.symvers文件,那该文件同样会被MODPOST读取。在MODPOST期间,会生成一个新的Module.symvers文件,其包含所有未在内核中定义的符号。

有时候,外部模块之间有依赖关系。比如有两个模块,foo.ko依赖bar.ko导出的文件。内核编译系统提供三种办法来满足这个需求

阅读全文 »

度量时间差

内核有个名为“HZ”宏,这个宏的值表征了系统的定时器中断产生的频率,内核用定时器中断跟踪时间。内核通过jiffies_64来记录系统的定时器中断产生了多少次,内核初始化时该值被初始化为0,然后之后每当时钟中断发生的时候将jiffies_64值加1。对于驱动程序而言,一般不直接读取jiffies_64,而是读取jiffies变量,因为读取jiffies更快。jiffies类型是unsigned long,根据平台不同,它要么是jiffies_64的低32位,要么就是jiffies_64。内核jiffies随时可以读,但是比较jiffies先后需要注意溢出。为了保证安全,一般用内核提供的宏来比较时间先后

1
2
3
4
int time_after(unsigned long a, unsigned long b);  // a比b时间靠后,该宏返回true,否则false
int time_before(unsigned long a, unsigned long b); // a比b时间靠前,该宏返回true,否则false
int time_after_eq(unsigned long a, unsigned long b);
int time_before_eq(unsigned long a, unsigned long b);

想要注解读取jiffies_64也不是不行,只是在32位架构上,jiffies_64是两个32位的变量拼合形成的。当读取jiffies_64的其中半个32位的时候,另一半可能会更新。为了保证读取原子性,内核提供了下面接口,该接口性能可能不高。

1
u64 get_jiffies_64(void);
阅读全文 »

介绍

本章涵盖ioctl、poll、select、llseek以及异步通知等操作。这篇笔记记得比较粗略,但是这没有什么关系。学习编程技术类的知识,最重要的是知道有某种方法可以实现某个功能,而不是细致到把每个遇到的功能都记在脑子里。

IOCTL

ioctl(io control),是struct file_operations中用于文件操作的接口之一(新版内核有改变,CentOS8 4.18内核中没有ioctl了,取而代之的是unlocked_ioctl和compat_ioctl),从这个角度来看虚拟文件系统中的文件都可以有ioctl操作。ioctl操作提供了命令扩展的机制,让驱动更灵活。前面的章节讲到,可以在ioctl中封装查询式调试命令。实时上ioctl还有其他的妙用,比如某个设备可以是字符流作为命令,也可以是ioctl作为控制命令。一般而言ioctl如果无法匹配到命令,返回-EINVAL是合理的,但是posix标准说应该返回-ENOTTY。

ioctl提供给用户态的接口,第三个参数是可变参数。但实际上ioctl只接收三个参数,不能将其理解为可接收任意参数。用于ioctl的部分命令有部分是系统内定的,可以查阅内核文档查看。内核将用于ioctl的命令分解为4个具有不同意义的字段看待,分别是type、number、direction以及size。其含义如下表

阅读全文 »

介绍

在SMP系统中,资源共享是很常见的事情;在DMA、抢占、中断的复杂环境中,资源共享是很常见的事情;硬件资源被多个程序共用,是很常见的事情。本章讲资源共享时的各种保护方式。

信号量和Mutex

信号量,本质就是一个变量。可以对其执行加减(P/V)操作来表示该信号量是可用还是空闲,进而表示软硬件资源是可用还是空闲。信号量的值是几,表示资源值最多可以被几个请求者使用。每个请求者用资源之前,都尝试对信号量执行P操作(原子操作,如果减去1结果大于等于0,则执行前去1,否则可能挂起进程),释放资源时都执行V操作(原子操作,信号量加1,然后按照设定唤醒挂在P操作上的进程)。当信号量的初始值被设备为1的时候,信号量事实上就成为了Mutex。

常规的信号量

阅读全文 »

介绍

第四章讲调试技术,事实上除了本章介绍的几种调试技术以外,还有利用Qemu、利用vmcore+crash的调试技术,后面会单独出博文来分别介绍。

内核调试配置项

内核提供了一些配置选项来支持内核调试,不知道这些选项在最新的内核还有用没有,用得时候最好再查一下。

选项名称 功能
CONFIG_DEBUG_KERNEL 内核调试总开关,要调试使用内核调试功能,该选项就必须打开
CONFIG_DEBUG_SLAB 用于检测SLAB内存泄漏,在内存被申请前和释放后,内核会分别填充0xa5和0x6b。如果内核踩内存,发生oops,则可以通过这些特殊值来判断踩踏位置和原因。
CONFIG_DEBUG_PAGEALLOC 当内存被释放的时候,将其完全移出内核地址空间。会降低内核性能,但也能快速定位内存错误。
CONFIG_DEBUG_SPINLOCK 开启该选项,内核将捕获对未初始化的自旋锁的使用,也会捕捉重复开锁等错误。
CONFIG_DEBUG_SPINLOCK_SLEEP 该选项让内核检查内核代码是否在持有锁的时候企图睡眠,哪怕是有“可能会睡眠”也报错。
CONFIG_INIT_DEBUG 标记为__init(或者__initdata)的那些代码和数据应该在初始化完成后被内核丢掉,开启该选项可以让内核检查对那些本该丢掉的内容的访问。
CONFIG_DEBUG_INFO 让内核代码包含完整的调试信息(调试符号表),如果内核将用gdb调试,还要打开CONFIG_DEBUG_INFO
CONFIG_MAGIC_SYSRQ Enables the “magic SysRq” key. We look at this key in the section “System Hangs,” later in this chapter. // TODO 讲到再说
CONFIG_DEBUG_STACKOVERFLOW 和下面的配置项一起使用
CONFIG_DEBUG_STACK_USAGE 上下两个选项,前一个选项打开内核栈溢出检查,这个选项打开内核栈用量统计。可以和SysRq配合使用,输出统计信息。
CONFIG_KALLSYMS This option (under “General setup/Standard features”) 默认是开启的,决定oops的时候打印的trace信息是函数名称还是地址数据。
CONFIG_IKCONFIG 和下面的配置项一起使用
CONFIG_PROC These options (found in the “General setup” menu) 将内核配置选项导出到/proc中,方便查看。
CONFIG_ACPI_DEBUG Under “Power management/ACPI.” This option turns on verbose ACPI (Advanced Configuration and Power Interface) debugging information, which can be useful if you suspect a problem related to ACPI.高级电源管理相关。
CONFIG_DEBUG_DRIVER Under “Device drivers.” 该选项使能driver core中的debug信息,该信息可以用于分析底层代码。
CONFIG_SCSI_CONSTANTS This option, found under “Device drivers/SCSI device support,” 详细记录SCSI错误信息,编写SCSI驱动可以启用该选项。
CONFIG_INPUT_EVBUG This option (under “Device drivers/Input device support”) 原封不动的记录输入内容,包括密码,可以用来调试输入设备的驱动代码。
CONFIG_PROFILING This option is found under “Profiling support.” ,用于系统性能调节,对跟踪内核挂死问题也有用。
阅读全文 »

SCULL

从本章开始,将会进入真正的驱动代码编写。作者通过将一片内存空间作为设备来演示如果编写设备驱动,驱动名称叫SCULL。SCULL包含多个子模块,分别代表着不同的设备驱动类型。

字符设备号

每个驱动和设备在内核中都由设备号联系起来,设备号分主设备号和次设备号。内核通过主设备号识别驱动,驱动通过次设备号识别设备。设备号由驱动申请,申请可以是静态申请(常量指定)的也可以是动态申请(内核分配)。有了设备号以后,就可以在/dev目录下用设备号创建设备文件节点。应用程序可以通过设备文件节点,用文件系统的接口调用内核态的驱动代码,实现硬件资源使用。

对于常规文件,ls -l在第五列显示文件大小,第六列显示修改日期。

阅读全文 »

注意事项

内核代码不能假定自己是单线程执行的,多核心系统中,内核同一处代码可能同时在多个核心上执行。哪怕系统只有一个核心,也是无法保证代码是单线程执行执行的。内核中往往有中断和抢占导致同一处内核代码被重复调用。所以内核代码应当考虑可重入、资源争抢等因素。如果内核代码以模块形式进入内核,那么在模块加载到内核之后,内核其他部分就可以看到该模块的所有导出内容了。如果模块加载过程发现异常,还要考虑怎么妥善的处理错误,尤其有可能其他内核代码在本模块尚未初始化完全时就已经在使用本模块的代码了、

内核编译

在内核编译之前,必须安装配套的构建软件,其中包括目标内核版本的头文件。 在CentOS下可以通过groupinstall一步到位安装。内核代码的各个模块是用make实现构建的,所以其基本构建配置文件是Makefile。内核自己实现了一套构建系统——KBuild,这套系统不仅可以简化内核的编译前配置过程,还可以简化内核模块的Makefile的编写方法。一个常用的Makefile模板如下所示

1
2
3
4
5
6
7
8
9
10
11
# 如果已经读取内核构建树,则KERNELRELEASE变量已定义,再次读取Makefile的时候,将走第一个分支
ifneq ($(KERNELRELEASE),)
obj-m := hello.o
# 若则KERNELRELEASE变量未定义,则让Make走下面这个分支
else
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
# default是Make中的默认构建目标,不是和if else同类型的控制语句
default:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
endif
阅读全文 »

设备驱动的作用

驱动就是软件和硬件之间的中间层。OS负责提供驱动框架,驱动填写该框架的向上接口,同时将接口转换为对硬件的操作。一般而言,驱动不实现策略,只实现机制。举个例子,驱动可以提供机制,实现IO的翻转。用户层可以实现策略,用IO翻转实现流水灯、呼吸灯等功能。

内核功能划分

作者认为,Linux的功能可以划分为5大块,分别是进程管理、内存管理、文件系统、设备控制、网络,就像下图所示。

image-20210124142510071
阅读全文 »

Linux设备驱动程序(Linux Device Drivers,LDD3)是一本关于Linux驱动开发的经典书籍,最后一次发行是第三版。虽然这本书是针对Linux2.6.10编写的,但是编写内核驱动的很多底层原理自2.6版以来就没有变过。所以,在内核版本已经是5.9的今天,通过本书入门内核驱动编写同样是可以的。部分本书随书附带的示例代码可能在较新的内核版本中已无法编译,但是没有关系,Github上有许多人将老旧的代码往新内核适配。

通过本书,读者不仅能够学习内核驱动的编写方法,还可以学习内核是如何开发的。驱动开发和内核其他部分的开发,在很多思路和注意事项上是相同的。此外,这本书是以GPL发布的,任何人都可以自由的获得这本书的电子版。

本系列博客是学习本书时的笔记,会更侧重技术相关的记录,作者提及的综合性描述会简略带过。

1 背景

在过去很长的一段时间,分析内核代码都依赖printk和读代码。而这个过程不仅耗时长,且需要对内核本身已经有一定的了解。

一种比较好的改进方法就是利用Qemu和GDB来对内核进行调试,Qemu内置了GDB Server。通过一系列配置,就可以通过GDB,像调试用户态程序那样调试内核。在开启嵌套虚拟化的情况下,甚至还可以调试KVM代码。

本文主要描述将Qemu和GDB相结合对内核代码进行调试的环境搭建步骤,总共包含三个部分。第一个部分描述基础的调试环境搭建,基础的调试环境可以对链接到内核的代码(相对于模块代码)进行调试。第二个部分,描述如何对模块代码进行调试,第三个部分讲如何调试KVM代码。

2 基础调试环境搭建

阅读全文 »