首页 > 其他分享 >【内核】深入分析内核panic(一)--内核问题的原因

【内核】深入分析内核panic(一)--内核问题的原因

时间:2023-04-30 11:24:38浏览次数:60  
标签:映射 -- cache 访问 死锁 内核 panic 内存

1 概述

linux内核包括进程管理、内存管理、中断管理、设备驱动、同步机制等各种模块,它们共同运行在一个共享的地址空间中,因此在运行中一旦出现问题,彼此之间可能具有千丝万缕的联系。

而且与用户态不同,内核还需要与形形色色的硬件打交道,因此对于某些较为诡异的问题,除了软件以外还可能受到硬件的影响。如由于射线或电磁辐射的原因造成内存中某个bit翻转,或者某些非法总线地址的访问,导致总线挂死等。

更进一步,内核作为系统的基础服务提供者,若使用软件调试工具,则由于调试工具本身也运行在内核空间,因此有可能会受到其它模块非法操作的干扰。

同时内核挂死以后,其现场抓取也相对比较麻烦,在生产环境下很可能没有抓到出问题时的内存信息,从而给问题定位带来难度。

总之,相对于用户态,内核bug可能受到的影响因素更多,现场抓取更困难,调试手段也更有限,因此有些疑难问题的定位会比较困难。

为此社区也提供了一系列相关的工具,用于辅助分析内核问题,若能用好这些工具,就可以帮助开发人员更加快速、方便地定位问题原因。

由于这些工具种类繁多,适用场景不同,因此有必要对它们的原理和使用方法做一些总结,以帮助更多同学进入内核调试的大门。

当然,这个系列只是笔者在工作和学习中的一些心得体会,并不一定很全面,也可能在某些地方理解不太到位,若在论述中有错误或不严谨的地方,欢迎大家指正。

2 引起内核问题的原因

内核问题主要包括功能问题、内核运行异常和性能问题几种类型。其中功能问题主要指相关模块的运行结果与预期值不同,它可能由于代码逻辑不正确或硬件输出结果不正常等原因导致。

内核运行异常可能由非法指令、内存访问错误或死锁等原因引起。而性能问题则可能由某些低效的程序代码,或cache问题导致。

由于功能问题主要与具体模块的逻辑设计有关,故我们不做过多讨论。而性能问题主要是程序的执行效率达不到预期,严格来说并不属于bug。

因此本系列将分为两个部分,第一部分聚焦于内核运行异常相关的bug调试,而第二部分将介绍一些性能调优相关的工具。众所周知,引起内核bug的原因多种多样,接下来我们将简单介绍一些常见的类型。

2.1 非法内存访问

2.1.1 内存访问越界

用户态每个进程都有自己独立的地址空间,因此即使有进程执行了非法内存访问,最多只影响到进程本身,若其导致进程挂死,则还可以通过重启进程恢复相应的服务。而由于内核所有可用的物理地址都被映射到了线性映射区,且它们是所有模块共享的。

img

如上图所示,由于线性映射区是分段连续的,因此一旦某个模块在线性映射区的地址访问越界,就可能会破坏另一个与其完全不相关模块的地址空间。且直到受害者模块使用到被破坏的地址之后,该问题才可能被检测到,更糟糕的是内核此时会报告受害者模块的内存访问错误信息,而寻找真正的肇事者却可能并不容易。

内核运行过程中主要需要访问全局数据、栈和动态分配的内存,其中全局数据被定义在内核镜像中,如通过以下命令可查看内核包含的段信息:

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段都被打包到内核镜像中。其内存在系统启动阶段通过以下流程映射:

img

在4.x及之前的内核中,这部分内存地址位于线性映射区,因此是通过线性映射的。

而在当前5.14.0内核版本中,其已被修改为通过vmalloc方式映射,即其虚拟地址以0xffff 8xxx开头,在以下的内核虚拟地址布局中正好位于vmalloc区:

img

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

此后一旦cpu访问了超出当前分配空间的内存地址,就会越界到guard page中,显然因为并没有为它们建立页表,此时将会触发内存的abort异常,通过该异常可以很容易地定位到相关的错误位置。

同时每个进程都含有一个独立的内核栈,它用于保存函数的局部变量、传递函数参数以及保存其上一层栈指针和函数返回地址等。它在进程创建时通过以下流程创建:

img

同样在先前的内核版本中栈地址只能通过线性映射区分配,但在新版本中可通过设置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 版权归原作者所有,如有侵权,请联系作者删除

标签:映射,--,cache,访问,死锁,内核,panic,内存
From: https://www.cnblogs.com/dongxb/p/17365047.html

相关文章

  • 【内核】深入分析内核panic(三)--内核错误处理流程
    1内核错误处理方式当内核出现致命错误时,只要cpu还能正常运行,那么最重要的就是向用户输出详细的错误信息,以及保存问题出现时的错误现场。以上致命错误可包含以下两种类型:(1)硬件能检测到的错误,如非法内存访问,非法指令等,此时cpu会触发异常,并进入异常处理流程。在异常处理流程中会......
  • Edu 145 A~D
    A.Garland将情况按照相同的数字的个数进行分类讨论即可。如1113相同数字数最大为3对应的答案为6B.PointsonPlane通过画图和观察数据,可以发现答案等于sqrt(n)向上取整再减1值得注意的是浮点数会损失精度,保险起见,要用longdouble和sqrtl或者使用二分C.SumonSubarra......
  • 以太网扫盲(一)各种网络总线 mii总线,mdio总线介绍
    本文主要介绍以太网的MAC(MediaAccessControl,即媒体访问控制子层协议)和PHY(物理层)之间的MII(MediaIndependentInterface,媒体独立接口),以及MII的各种衍生版本——GMII、SGMII、RMII、RGMII等。简介从硬件的角度看,以太网接口电路主要由MAC(MediaAccessControl)控制器和物理层接口......
  • 【驱动】以太网扫盲(二)phy寄存器简介
    PHY寄存器的地址空间为5位,从0到31最多可以定义32个寄存器(随着芯片功能不断增加,很多PHY芯片采用分页技术来扩展地址空间以定义更多的寄存器),IEEE802.3定义了地址为0-15这16个寄存器的功能,地址16-31的寄存器留给芯片制造商自由定义,如下表所示。官方介绍请参考......
  • 从源码编译并安装LXQT
    平台:ubuntu-22.04-server-amd64.对象:LXQT.文件:lxqt.LXQt是由LXDE-Qt和RazorQt合并的项目,它的目标是创建一个轻量级、模块化、运行快并且简单易用的桌面环境。本例中会介绍多种发行版下的编译方法,本例中使用Ubuntu22.04,你也可以使用其它发行版。1.安装编译环境CMake版本≥3.......
  • Django的message组件(源码分析)
    Django的Message组件(源码分析)1.配置#MESSAGE_STORAGE='django.contrib.messages.storage.fallback.FallbackStorage'#MESSAGE_STORAGE='django.contrib.messages.storage.cookie.CookieStorage'MESSAGE_STORAGE='django.contrib.messages.stor......
  • 查看docker容器日志
    要查看Docker容器的日志,可以使用以下命令:dockerlogs[OPTIONS]CONTAINER其中,OPTIONS可以是以下选项之一:-f:跟踪日志输出,类似于tail-f命令。--since:仅显示指定时间之后的日志,格式为YYYY-MM-DDTHH:MM:SS。--until:仅显示指定时间之前的日志,格式为YYYY-MM-DDTHH:MM:SS。--ta......
  • 【驱动】以太网扫盲(四)phy驱动link up流程分析
    1.简介在调试网口驱动的过程中发现phy芯片的驱动框架结构还有点复杂,不仔细研究的话还不好搞懂,另外百度到的资料也不够全面,这篇就总结梳理一下这方面的知识。我们知道一个phy驱动的原理是非常简单的,一般流程如下:1、用轮询/中断的方式通过mdio总线读取phy芯片的状态。2......
  • 中国剩余定理
    中国剩余定理:代码实现://互质版中国剩余定理(CRT)#include<iostream>usingnamespacestd;typedeflonglongLL;constintN=20;LLa[N],b[N];intn;voidexgcd(LLa,LLb,LL&x,LL&y){if(!b){x=1,y=0;return;}exgcd(b,......
  • Kubernetes从入门到精通 集群安装
    kubernetes集群安装一准备集群镜像从阿里云镜像仓库拉镜像标签改名#在安装kubernetes集群之前,必须要提前准备好集群需要的镜像,所需镜像可以通过下面命令查看[root@master~]#kubeadmconfigimageslist#下载镜像#此镜像kubernetes的仓库中,由于网络......