【内核】深入分析内核panic(一)--内核问题的原因
1 概述
linux内核包括进程管理、内存管理、中断管理、设备驱动、同步机制等各种模块,它们共同运行在一个共享的地址空间中,因此在运行中一旦出现问题,彼此之间可能具有千丝万缕的联系。
而且与用户态不同,内核还需要与形形色色的硬件打交道,因此对于某些较为诡异的问题,除了软件以外还可能受到硬件的影响。如由于射线或电磁辐射的原因造成内存中某个bit翻转,或者某些非法总线地址的访问,导致总线挂死等。
更进一步,内核作为系统的基础服务提供者,若使用软件调试工具,则由于调试工具本身也运行在内核空间,因此有可能会受到其它模块非法操作的干扰。
同时内核挂死以后,其现场抓取也相对比较麻烦,在生产环境下很可能没有抓到出问题时的内存信息,从而给问题定位带来难度。
总之,相对于用户态,内核bug可能受到的影响因素更多,现场抓取更困难,调试手段也更有限,因此有些疑难问题的定位会比较困难。
为此社区也提供了一系列相关的工具,用于辅助分析内核问题,若能用好这些工具,就可以帮助开发人员更加快速、方便地定位问题原因。
由于这些工具种类繁多,适用场景不同,因此有必要对它们的原理和使用方法做一些总结,以帮助更多同学进入内核调试的大门。
当然,这个系列只是笔者在工作和学习中的一些心得体会,并不一定很全面,也可能在某些地方理解不太到位,若在论述中有错误或不严谨的地方,欢迎大家指正。
2 引起内核问题的原因
内核问题主要包括功能问题、内核运行异常和性能问题几种类型。其中功能问题主要指相关模块的运行结果与预期值不同,它可能由于代码逻辑不正确或硬件输出结果不正常等原因导致。
内核运行异常可能由非法指令、内存访问错误或死锁等原因引起。而性能问题则可能由某些低效的程序代码,或cache问题导致。
由于功能问题主要与具体模块的逻辑设计有关,故我们不做过多讨论。而性能问题主要是程序的执行效率达不到预期,严格来说并不属于bug。
因此本系列将分为两个部分,第一部分聚焦于内核运行异常相关的bug调试,而第二部分将介绍一些性能调优相关的工具。众所周知,引起内核bug的原因多种多样,接下来我们将简单介绍一些常见的类型。
2.1 非法内存访问
2.1.1 内存访问越界
用户态每个进程都有自己独立的地址空间,因此即使有进程执行了非法内存访问,最多只影响到进程本身,若其导致进程挂死,则还可以通过重启进程恢复相应的服务。而由于内核所有可用的物理地址都被映射到了线性映射区,且它们是所有模块共享的。

如上图所示,由于线性映射区是分段连续的,因此一旦某个模块在线性映射区的地址访问越界,就可能会破坏另一个与其完全不相关模块的地址空间。且直到受害者模块使用到被破坏的地址之后,该问题才可能被检测到,更糟糕的是内核此时会报告受害者模块的内存访问错误信息,而寻找真正的肇事者却可能并不容易。
内核运行过程中主要需要访问全局数据、栈和动态分配的内存,其中全局数据被定义在内核镜像中,如通过以下命令可查看内核包含的段信息:
readelf -S vmlinux
由于内核段的数量较多,为了方便阅读,下面只列出了一些重要的段:
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .head.text PROGBITS ffff800010000000 00010000
0000000000010000 0000000000000000 AX 0 0 65536
[ 2] .text PROGBITS ffff800010010000 00020000
00000000006d9868 0000000000000008 AX 0 0 65536
…
[ 4] .rodata PROGBITS ffff8000106f0000 00700000
000000000016eea8 0000000000000000 WA 0 0 4096
…
[13] .init.text PROGBITS ffff8000108b0000 008b0000
000000000003c2dc 0000000000000000 AX 0 0 4
[14] .exit.text PROGBITS ffff8000108ec2dc 008ec2dc
00000000000014d0 0000000000000000 AX 0 0 4
…
[16] .init.data PROGBITS ffff800010900000 00900000
000000000001197d 0000000000000000 WA 0 0 8
[17] .data..percpu PROGBITS ffff800010912000 00912000
000000000000bd18 0000000000000000 WA 0 0 64
readelf: Warning: [18]: Link field (0) should index a symtab section.
…
[19] .data PROGBITS ffff8000109d0000 009d0000
00000000000c2800 0000000000000000 WA 0 0 4096
…
[24] .bss NOBITS ffff800010aa3000 00aa2200
0000000000051eac 0000000000000000 WA 0 0 4096
…
即代码段、全局数据段和bss段都被打包到内核镜像中。其内存在系统启动阶段通过以下流程映射:

在4.x及之前的内核中,这部分内存地址位于线性映射区,因此是通过线性映射的。
而在当前5.14.0内核版本中,其已被修改为通过vmalloc方式映射,即其虚拟地址以0xffff 8xxx开头,在以下的内核虚拟地址布局中正好位于vmalloc区:

那么这种机制有什么优点呢?从上图可看到vmalloc虚拟地址一共占124T空间,故其虚拟地址空间远远大于实际需求。由于vmalloc是非线性映射,若在每次vmalloc映射的虚拟地址之间保留一些如下图所示的空洞作为guard page。

此后一旦cpu访问了超出当前分配空间的内存地址,就会越界到guard page中,显然因为并没有为它们建立页表,此时将会触发内存的abort异常,通过该异常可以很容易地定位到相关的错误位置。
同时每个进程都含有一个独立的内核栈,它用于保存函数的局部变量、传递函数参数以及保存其上一层栈指针和函数返回地址等。它在进程创建时通过以下流程创建:

同样在先前的内核版本中栈地址只能通过线性映射区分配,但在新版本中可通过设置CONFIG_VMAP_STACK配置选项,选择是从vmalloc空间还是线性映射区分配。其主要代码如下:
static unsigned long *alloc_thread_stack_node(struct task_struct *tsk, int node)
{
#ifdef CONFIG_VMAP_STACK
…
stack = __vmalloc_node_range(THREAD_SIZE, THREAD_ALIGN, (1)
VMALLOC_START, VMALLOC_END,
THREADINFO_GFP & ~__GFP_ACCOUNT,
PAGE_KERNEL,
0, node, __builtin_return_address(0));
if (stack) {
tsk->stack_vm_area = find_vm_area(stack);
tsk->stack = stack;
}
return stack;
#else
struct page *page = alloc_pages_node(node, THREADINFO_GFP, (2)
THREAD_SIZE_ORDER);
if (likely(page)) {
tsk->stack = kasan_reset_tag(page_address(page));
return tsk->stack;
}
return NULL;
#endif
}
(1)配置了CONFIG_VMAP_STACK选项,通过vmalloc方式分配栈内存
(2)未配置CONFIG_VMAP_STACK选项,且栈空间大于一个page,通过页分配器分配栈内存
其它的动态内存分配都是通过伙伴系统从动态映射区分配的,因此它们之间的越界访问定位难度会更加大。因此内核也提供了一系列的定位方法,如slub_debug、kasan、kfence等。
另外除了cpu之外,系统中还可能有一些其它硬件需要访问系统内存,如dma,异构系统中的异构核,以及一些硬件加速器等,若它们在内存访问中发生越界,则定位难度将更大
2.1.2 其它内存访问问题
除了访问越界之外,还有一些其它问题可能导致非法内存访问,它们包括:
(1)访问空指针
(2)访问已释放内存
(3)访问未初始化指针
(4)内存访问权限错误,如向一段只读的地址空间中写入数据,或者对不带有执行权限的内存 代码执行操作
2.2 内存泄漏
内存泄漏是指程序中的动态分配内存,由于编码错误或某些原因在使用完成之后,未能正确释放,造成系统内存浪费的问题。它具有隐蔽性和积累性的特点,即在程序执行过程中并不能及时检测到泄漏行为,而且若造成内存泄漏的代码需要重复执行,则随着系统的运行,其泄漏内存会不断累积,最终可能导致系统内存不足而触发oom。内核提供了kmemleak工具,可用于检测内核中的内存泄漏问题
2.3 cache问题
由于cpu的运行速度比主存速度快的多,因此cpu都会通过cache来提升系统的整体性能。若只有cpu执行内存访问操作,则cache对程序员是透明的,硬件会负责维护cache与主存之间数据的一致性。
如通过tag和index进行cache与内存地址的映射,通过cache替换算法执行cache加载与写回/写通操作,以及通过MESI协议执行smp系统中多核之间cache的一致性维护操作等。
除此之外,若某块地址被用于dma操作,或用于和其它异构核之间做通信时的共享内存,则软件需要维护这块地址的cache一致性。它包括在内存分配时需要注意相关内存必须要与cacheline长度对齐,以及在内存操作时,需要在适当的时候执行cache失效和cache刷新操作等
2.4 非法指令
cpu运行时会从pc指针指定的地址处加载指令,然后通过译码器解析其内容,并最终通过控制器和运算器执行。
若加载的指令不合法,显然会导致cpu无法执行该指令,此时cpu会抛出非法指令异常。内核通过异常处理流程接收到该异常后,则会进一步输出该异常相关的详细信息,一般情况下通过这些信息就可以定位到错误的原因
2.5 死锁问题
死锁是一种比较常见的内核卡死原因,它主要包括AB – BA死锁和重复加锁两种类型:
(1)AB – BA死锁:假设有两把锁A和B和两个进程X和Y,此时进程X持有了锁A,进程Y持有了锁B,而且进程X希望继续持有锁B,且进程Y希望继续持有锁A。因此导致它们谁都没办法释放自身持有的锁,从而没办法获取对方持有的锁而形成死锁
(2)内核不允许对spinlock和mutex的递归调用,即一个线程已经持有了一把锁之后,试图再次持有这把锁。一旦出现这种情况,就会导致内核死锁
为此,内核提供了一套死锁检测模块lockdep,可用于检测内核中可能的死锁行为,并在检测到死锁后输出相关的信息,以帮助分析其发生原因
2.6 长时间关抢占或关中断
抢占是调度器工作的基础,若长时间关闭某个cpu的抢占功能,会严重影响系统的实时性。而调度器是通过tick中断驱动的,因此长时间关中断同样会影响系统的实时性,且还使得硬件事件无法得到及时处理。
因此在内核中应该要避免长时间的关闭抢占或中断,为此内核分别为这两种情况实现了softlockup和hardlockup两种检测机制。
需要注意的是由于softlockup用于检测关抢占问题,因此需要通过中断机制来实现,而hardlockup用于监测关中断问题,因此需要非屏蔽中断(NMI)实现。
2.7 线程长时间处于D状态
由于处于D状态(TASK_UNINTERRUPTIBLE)的进程不能接收信号,因此也无法被kill掉。进程被设置为D状态一般是用于等待IO,正常情况下IO执行完成后就会唤醒该进程,使其继续执行。
但可能由于一些编码问题或硬件本身问题,导致某些IO操作无法成功,从而导致与其相关的D状态进程无法被唤醒。
显然这种情况是不正常的,为此内核提供了hungtask机制用于检测处于该状态超过120s的进程,并在检测到之后打印相关警告信息。若内核配置了hung_task_panic选项,则该问题还会触发panic使内核挂死
2.8 硬件问题
硬件问题种类繁多,且有些问题的现象非常诡异,如ddr不稳定可能会导致莫名其妙的死机,且死机时的现象毫无规律。
camera sensor的某些排线信号之间有干扰,可能会导致输出图像间歇性地花屏。访问芯片中处于关闭状态的IP时,可能会导致总线挂死,整个系统无响应等。因此,遇到硬件问题时需要根据实际情况具体分析,逐步缩小问题相关的范围,以定位其根因
原文链接:https://www.zhihu.com/column/c_1533871448917118976 版权归原作者所有,如有侵权,请联系作者删除
【内核】深入分析内核panic(一)--内核问题的原因的更多相关文章
- Linux内核源码分析--内核启动之(3)Image内核启动(C语言部分)(Linux-3.0 ARMv7)
http://blog.chinaunix.net/uid-20543672-id-3157283.html Linux内核源码分析--内核启动之(3)Image内核启动(C语言部分)(Linux-3 ...
- Linux内核源码分析--内核启动之(5)Image内核启动(rest_init函数)(Linux-3.0 ARMv7)【转】
前面粗略分析start_kernel函数,此函数中基本上是对内存管理和各子系统的数据结构初始化.在内核初始化函数start_kernel执行到最后,就是调用rest_init函数,这个函数的主要使命就 ...
- 【内核】linux2.6版本内核编译配置选项(二)
目录 Linux2.6版本内核编译配置选项(一):http://infohacker.blog.51cto.com/6751239/1203633 Linux2.6版本内核编译配置选项(二):http ...
- Linux内核Makefile文件(翻译自内核手册)
--译自Linux3.9.5 Kernel Makefiles(内核目录documention/kbuild/makefiles.txt) kbuild(kernel build) 内核编译器 Thi ...
- 戴文的Linux内核专题:05配置内核(1)
转自Linux中国 现在我们已经了解了内核,现在我们可以进入主要工作:配置并编译内核代码.配置内核代码并不会花费太长时间.配置工具会询问许多问题并且允许开发者配置内核的每个方面.如果你有不确定的问题或 ...
- 向linux内核加入系统调用新老内核比較
2.6内核 1>改动linux-source-2.6.31/kernel/sys.c文件,在文件末尾加入系统响应函数.函数实现例如以下: asmlinkage int sys_mycall(in ...
- 十天学Linux内核之第九天---向内核添加代码
原文:十天学Linux内核之第九天---向内核添加代码 睡了个好觉,很晚才起,好久没有这么舒服过了,今天的任务不重,所以压力不大,呵呵,现在的天气真的好冷,不过实验室有空调,我还是喜欢待在这里,有一种 ...
- 十天学Linux内核之第六天---调度和内核同步
原文:十天学Linux内核之第六天---调度和内核同步 心情大好,昨晚我们实验室老大和我们聊了好久,作为已经在实验室待了快两年的大三工科男来说,老师让我们不要成为那种技术狗,代码工,说多了都是泪啊,, ...
- 【内核】Linux内核Initrd机制解析,内核更新步骤,grub配置说明
什么是Initrd initrd的英文含义是 boot loader initialized RAM disk,就是由boot loader初始化的内存盘.在 linux内核启动前, boot loa ...
- 内核知识第六讲,内核编写规范,以及获取GDT表
内核知识第六讲,内核编写规范,以及获取GDT表 一丶内核驱动编写规范 我们都知道,在ring3下,如果我们的程序出错了.那么就崩溃了.但是在ring0下,只要我们的程序崩溃了.那么直接就蓝屏了. 那么 ...
随机推荐
- shell 脚本中的 '-f' 和 '-d' 分别代表什么意思
shell脚本中,'-f' 和 '-d'是用于测试文件类型的条件表达式. 1.'-f'表达式: 表达式: '[ -f file ]' 描述: 判断给定路径是否是一个常规文件 (regular file ...
- MySQL索引命名规范
[强制]主键索引名为 pk_字段名:唯一索引名为 uk_字段名:普通索引名则为 idx_字段名 说明:pk_ 即 primary key:uk_ 即 unique key:idx_ 即 index 的 ...
- LeetCode 503:下一个更大的元素|| (单调栈 or 线段树)
解题思路: 1.单调栈:因为是循环数组,因此把数组复制三遍,ans 数组复制为2倍长,维护一个单调非递增的栈,栈保存的元素是元组(a[i] , i ),如果后面的值有比栈顶元素的值大,栈顶元素出栈,更 ...
- django-filter的详细使用
有时候前端需要各种各样的过滤查询,如果自己写多少有点麻烦和冗余.使用django-filter就可以很好的解决这个问题. django-filter可以用在django上, 也与配合drf一起使用. ...
- 图纸安全管理:华企盾DSC数据防泄密系统的综合解决方案
企业设计人员创造出的图纸既是珍贵的知识产权,又承载着公司的创新力和竞争力.然而,随着信息技术的迅速发展,图纸泄密风险也在不断增加.为了保护这一重要的企业资产,华企盾DSC数据防泄密系统提供了一系列专业 ...
- 【Python】【ChatGPT】本地部署ChatGPT学习记录
学习一下GPT项目的相关使用和部署 一.GPT4ALL模型 Github:https://github.com/nomic-ai/gpt4all GPT4ALL项目部署简易,但是在运行体验上一般,并且 ...
- Educational Codeforces Round 159 总结
最失败的一集. C 开题顺序搞错,不小心先开了C,以为是A.还好C不难. 题意大概是在给定的数组最后添一个数(所有数两两不同),再自定义一个数 \(k\) ,数组中每个数可以加上若干个 \(k\) , ...
- Python趣味入门12:初遇类与实例
小牛叔用轻松有趣的故事,带你进入Python的编程世界. 1.类 一提到类大神们就经常说封装.说白了,封装即把围绕同一个对象相同的代码.数据整合在一起.比如在某段游戏代码中(比如熊猫厨房),有一个&q ...
- 完蛋,我被挖矿木马包围了|使用 TLS 连接 Docker
事故还原 近日,白泽在使用 docker 的时候,开放了防火墙的端口,以 SSH 方式访问远程服务器的 docker 守护进程(无需使用密钥即可建立连接),随后竟遭到了挖矿木马的攻击,好一顿折腾之后, ...
- 2023-09-10:用go语言编写。作为项目经理,你规划了一份需求的技能清单 req_skills, 并打算从备选人员名单 people 中选出些人组成一个「必要团队」 ( 编号为 i 的备选人员
2023-09-10:用go语言编写.作为项目经理,你规划了一份需求的技能清单 req_skills, 并打算从备选人员名单 people 中选出些人组成一个「必要团队」 ( 编号为 i 的备选人员 ...