mmu是arm64芯片上的一个内存管理单元,使用mmu的机制可以快速通过硬件将虚拟地址转换成物理地址,本文介绍crash工具的使用,可以比较方便解析内核的结构,故根据虚拟地址向物理地址转换的过程来梳理掌握crash工具的使用,并复习一下内存的基本知识。
对于arm64机器的页表设置,每个机器的环境不一致,但是原理都是一致的,本文机器环境如下
CONFIG_ARM64_VA_BITS_39=y CONFIG_ARM64_4K_PAGES=y CONFIG_PGTABLE_LEVELS=3
为了找到符合我机器的说明,我翻了很早的内核版本docs,可以参考信息如下
AArch64 Linux memory layout with 4KB pages + 3 levels: Start End Size Use ----------------------------------------------------------------------- 0000000000000000 0000007fffffffff 512GB user ffffff8000000000 ffffffffffffffff 512GB kernel Translation table lookup with 4KB pages: +--------+--------+--------+--------+--------+--------+--------+--------+ |63 56|55 48|47 40|39 32|31 24|23 16|15 8|7 0| +--------+--------+--------+--------+--------+--------+--------+--------+ | | | | | | | | | | | v | | | | | [11:0] in-page offset | | | | +-> [20:12] L3 index | | | +-----------> [29:21] L2 index | | +---------------------> [38:30] L1 index | +-------------------------------> [47:39] L0 index +-------------------------------------------------> [63] TTBR0/1
根据上面可以知道,当前环境最大内存大小是512GB,因为是三级页表,所以这里默认没有L0 index [47:39]
。为了概念的统一,我这里将三级页表也叫做Level 1和Level 2和Level 3以及 in-page offset,同四级页表一个叫法。
关于页表的转换,手册默认是按照四级页表计算,如下图示
我们根据三级页表计算,下面开始演示。
为了拿到一个已经映射的地址,我们可以加载一个ko,用其中的数据地址,或者直接使用某个进程的task_struct内存地址,本文获取的是test.ko启动kthread的task_struct
5190 2 5 ffffff80c0002b80 RU 0.0 0 0 [spinlock_thread]
可以看到此task_struct地址是ffffff80c0002b80,下面开始转换
对于上面的虚拟地址,我们按位可以解析如下
可以得到pgd_offset=3,pmd_offset=0,pte_offset=0xb80。并且我们知道其结构体大小如下
crash> struct pgd_t -x typedef struct { pgdval_t pgd; } pgd_t; SIZE: 0x8 crash> struct pud_t -x typedef struct { p4d_t p4d; } pud_t; SIZE: 0x8 crash> struct pmd_t -x typedef struct { pmdval_t pmd; } pmd_t; SIZE: 0x8 crash> struct pte_t -x typedef struct { pteval_t pte; } pte_t; SIZE: 0x8
可以看到,因为我们是64位系统,存放一个页表的size就是8,而我们页表换算的时候,是按照基地址+offset计算的,这个offset值的是多少个地址,而不是地址的size,所以我们获得基地址之后,计算下一级页表的存放物理地址的时候,应该乘上size,也就是8,因为实际存放这个64位地址要占用8个字节,那么这里我们就可以知道信息如下
我们获取的是内核的地址,那么其基地址来源于TTBR1_EL1,对于内核,其默认值存放在变量init_mm.pgd中,我们可以查看如下
crash> p init_mm.pgd $1 = (pgd_t *) 0xffffffc009eea000
我们可以根据这个基地址开始计算pgd
pgd的地址就是TTBR的地址 加上 pgd_offset * 8 ,所以得出如下
0xffffffc009eea018 = 0xffffffc009eea000 + 0x3 * 8
此时读取其值就是下一级页表的基地址
crash> rd ffffffc009eea018 -x ffffffc009eea018: 00000001ff20f003
值得注意的是,虚拟地址只是操作系统抽象的东西,所以存放的下一级页表的基地址是物理地址,不是虚拟地址,我们拿到地址 0x00000001ff20f003
对于arm64而言,内存不是恒等映射的,而是线性映射,也就是我们拿到的物理地址,其虚拟地址通常是 加上 ffffff8000000000
的offset 的线性映射的,所以我们拿到的地址0x00000001ff20f003 的虚拟地址是
0xffffff81ff20f003 = 0x00000001ff20f003 + 0xffffff8000000000
我们验证一下线性映射地址即可,如下
crash> ptov 0x00000001ff20f003 VIRTUAL PHYSICAL ffffff81ff20f003 1ff20f003
这里虚拟地址的bit [0:1]
用来表示是Block/Page,这里是3,也就是Page,我们寻找下一级页表。
pmd的地址是 对应的虚拟地址 去掉 bit0和bit1 后 加上 offset,这么说可能有点绕,转换计算公式如下
0xffffff81ff20f000 = 0xffffff81ff20f003 & ~0x3 + 0x0 * 8
然后读取其值即可获得下一级页表基地址的物理地址
crash> rd ffffff81ff20f000 -x ffffff81ff20f000: 00000001ff20e003
同样的,这里通过命令转换,当然也可以自己手动加上0xffffff8000000000的线性映射的offset。
crash> ptov 1ff20e003 VIRTUAL PHYSICAL ffffff81ff20e003 1ff20e003
这里看到还是3,说明不是Block,而是Page。
pmd的地址计算和pud一致,如下
0xffffff81ff20e010 = 0xffffff81ff20e003 & ~0x3 + 0x2 * 8
此时读取下一级页表
crash> rd ffffff81ff20e010 -x ffffff81ff20e010: 00680000c0002707
这里我们接下来读的是pte的地址,0x00680000c0002707 具备upper attr和lower attr。关于页属性的描述如下
对于pte地址,其值是0x00680000c0002707,那么其属性值如下表示
除了属性值,其他部分就是物理页面基地址了,我们需要去掉属性值,提取物理页面基地址,那么如下
680000c0002707 & ~0xfff = 680000c0002000 # 去掉低12位 680000c0002000 & 0x7fffffffff = c0002000 # 去掉高25位
通过上面的pte地址,去掉属性之后,我们得到了物理页也就是0xc0002000,我们知道page-in offset是0xb80,那么真实的页就是
0xc0002b80 = 0xc0002000 + 0xb80
这里就计算出来了虚拟地址对于的物理页地址了。
同样的,在crash中,默认提供的vtop会自动帮我们计算,vtop的结果如下
crash> vtop ffffff80c0002b80 VIRTUAL PHYSICAL ffffff80c0002b80 c0002b80 PAGE DIRECTORY: ffffffc009eea000 PGD: ffffffc009eea018 => 1ff20f003 PMD: ffffff81ff20f000 => 1ff20e003 PTE: ffffff81ff20e010 => 680000c0002707 PAGE: c0002000 PTE PHYSICAL FLAGS 680000c0002707 c0002000 (VALID|SHARED|AF|PXN|UXN) PAGE PHYSICAL MAPPING INDEX CNT FLAGS ffffffff02e00080 c0002000 dead000000000400 0 0 0
可以看到,我们手动计算的地址转换过程和vtop提供的信息一样。
上面已经介绍过转换过程了,下面用户地址为例的转换就快速过一下。做个验证
为了测试,提供一个简单的代码
#include <stdio.h> #include <unistd.h> char str[] = "hello kylin"; int main() { printf("vtop -c %d %p [%s] \n", getpid(), str, str); while(1); return 0; }
我们拿到信息如下
vtop -c 13725 0x411038 [hello kylin] crash> vtop -c 13725 0x411038 VIRTUAL PHYSICAL 411038 8c2c3038 PAGE DIRECTORY: ffffff81f262b000 PGD: ffffff81f262b000 => 1f2c5a003 PMD: ffffff81f2c5a010 => 1f5fd0003 PTE: ffffff81f5fd0088 => e800008c2c3f43 PAGE: 8c2c3000 PTE PHYSICAL FLAGS e800008c2c3f43 8c2c3000 (VALID|USER|SHARED|AF|NG|PXN|UXN|DIRTY) VMA START END FLAGS FILE ffffff805b21a840 411000 412000 100873 /root/test PAGE PHYSICAL MAPPING INDEX CNT FLAGS ffffffff0210b0c0 8c2c3000 ffffff806d097b29 1 1 80014 uptodate,lru,swapbacked
此时知道了物理地址是0x8c2c3038,读取内容即可
crash> rd -8 -p 8c2c3038 16 8c2c3038: 68 65 6c 6c 6f 20 6b 79 6c 69 6e 00 00 00 00 00 hello kylin.....
一切正常,下面开始手动推导
我们先获取基地址,对于进程,基地址在task_struct的struct_mm的pgd
crash> set 13725 PID: 13725 COMMAND: "test" TASK: ffffff81f0a09d00 [THREAD_INFO: ffffff81f0a09d00] CPU: 7 STATE: TASK_RUNNING (ACTIVE)
这里我们拿到了task_struct的地址是ffffff81f0a09d00,那么mm的值是0xffffff81f5fa2ec0
struct task_struct { [1160] struct mm_struct *mm; } crash> struct task_struct.mm ffffff81f0a09d00 -o struct task_struct { [ffffff81f0a0a188] struct mm_struct *mm; } crash> struct task_struct.mm ffffff81f0a09d00 -x mm = 0xffffff81f5fa2ec0
此时我们获得了进程的pgd地址如下
crash> struct mm_struct.pgd 0xffffff81f5fa2ec0 pgd = 0xffffff81f262b000
我们拿到地址是0xffffff81f262b000,此时虚拟地址是0x411038,计算如下
ffffff81f262b000 = ffffff81f262b000 + 0 crash> rd ffffff81f262b000 -x ffffff81f262b000: 00000001f2c5a003 crash> ptov 00000001f2c5a003 VIRTUAL PHYSICAL ffffff81f2c5a003 1f2c5a003
我们拿到地址0xffffff81f2c5a003,继续计算
ffffff81f2c5a010 = ffffff81f2c5a003 & ~0x3 + 0x2 * 8 crash> rd ffffff81f2c5a010 -x ffffff81f2c5a010: 00000001f5fd0003 crash> ptov 00000001f5fd0003 VIRTUAL PHYSICAL ffffff81f5fd0003 1f5fd0003
我们拿到地址0xffffff81f5fd0003,最后计算pte
ffffff81f5fd0088 = ffffff81f5fd0003 & ~0x3 + 0x11 * 8 crash> rd ffffff81f5fd0088 -x ffffff81f5fd0088: 00e800008c2c3f43 crash> ptov 00e800008c2c3f43 VIRTUAL PHYSICAL e7ff808c2c3f43 e800008c2c3f43
我们拿到了e800008c2c3f43的地址,需要去掉属性bit,计算可得
e800008c2c3f43 & ~0xfff = e800008c2c3000 # 去掉低12位 e800008c2c3f43 & 0x7fffffffff = 8c2c3000 # 去掉高25位
此时物理页起始是0x8c2c3000,对于数据的物理地址是
8c2c3038 = 8c2c3000 + 0x38
这样我们就计算出来物理页地址了,我们读取其内容验证一下
crash> rd -8 -p 8c2c3038 16 8c2c3038: 68 65 6c 6c 6f 20 6b 79 6c 69 6e 00 00 00 00 00 hello kylin.....
可以发现,其物理地址存放的内容正是我们设置的 "hello kylin"。 实验完成。
根据上面的内容,我们通过物理地址和虚拟地址将页表的转换过了一遍,借助工具crash可以随时挂上内核的kcore进行调试判断,不会像gdb内核一样复杂并且不方便。
之前有简单介绍过abba锁,在了解crash的时候,发现其特别方便用于熟悉和调试内核,能够查看其实时的变量状态和出现堆栈后的状态,本文基于锁来简单演示通过crash查看锁状态,从而提供调试锁的一种方法
在<ABBA锁介绍>的文章中提供了测试代码,复用即可
在测试之前,我们查看spinlock结构体如下
crash> struct qspinlock -o -x struct qspinlock { union { [0x0] atomic_t val; struct { [0x0] u8 locked; [0x1] u8 pending; }; struct { [0x0] u16 locked_pending; [0x2] u16 tail; }; }; } SIZE: 0x4
读取变量的信息如下
crash> rd -8 spinlock_a 4 ffffffc001542030: 00 00 00 00 .... crash> rd -8 spinlock_b 4 ffffffc001542018: 00 00 00 00 ....
我们开启spinlock测试
echo 1 > /sys/module/test/parameters/testsuite
此时我们可以观察到spinlock_a/b变量的值如下
crash> rd -8 spinlock_a 4 ffffffc001542030: 01 01 00 00 .... crash> rd -8 spinlock_b 4 ffffffc001542018: 01 01 00 00 ....
上面数据需要注意大小端,我们可以直接解析如下
crash> struct qspinlock spinlock_a -x struct qspinlock { { val = { counter = 0x101 }, { locked = 0x1, pending = 0x1 }, { locked_pending = 0x101, tail = 0x0 } } } crash> struct qspinlock spinlock_b -x struct qspinlock { { val = { counter = 0x101 }, { locked = 0x1, pending = 0x1 }, { locked_pending = 0x101, tail = 0x0 } } }
可以看到,spinlock_a/b 的信息如下:
可以看到,与代码现象ABBA锁相符
同样的,测试之前先读取数据结构
crash> struct mutex -o -x struct mutex { [0x0] atomic_long_t owner; [0x8] spinlock_t wait_lock; [0x20] struct optimistic_spin_queue osq; [0x28] struct list_head wait_list; } SIZE: 0x38
然后我们确定mutex的owner默认值是0
crash> struct mutex.owner mutex_a -x owner = { counter = 0x0 } crash> struct mutex.owner mutex_b -x owner = { counter = 0x0 }
开始测试
echo 2 > /sys/module/test/parameters/testsuite
此时我们看到mutex.owner变成了一个值,说明有人持有这个锁,如下
crash> struct mutex.owner mutex_a -x owner = { counter = 0xffffff804e286581 } crash> struct mutex.owner mutex_b -x owner = { counter = 0xffffff800dfd1d01 }
对于mutex,我们知道其flag如下
#define MUTEX_FLAG_WAITERS 0x01
其中含义: 是有任务正在等待锁
在这个flag之外,mutex的owner是一个task_struct结构体如下
static inline struct task_struct *__owner_task(unsigned long owner) { return (struct task_struct *)(owner & ~MUTEX_FLAGS); }
故我们计算出持有mutex_a的任务是
crash> struct task_struct.pid,comm 0xffffff804e286580 pid = 2818 comm = "spinlock_thread"
同样的持有mutex_b的任务是
crash> struct task_struct.pid,comm 0xffffff800dfd1d00 pid = 2819 comm = "spinlock_thread"
ps查看信息如下
crash> ps | grep 2819 2819 2 5 ffffff800dfd1d00 UN 0.0 0 0 [spinlock_thread] crash> ps | grep 2818 2818 2 4 ffffff804e286580 UN 0.0 0 0 [spinlock_thread]
这样我们就找到了谁在持有这个锁。
先查看semaphore的结构体
crash> struct semaphore -o -x struct semaphore { [0x0] raw_spinlock_t lock; [0x18] unsigned int count; [0x20] struct list_head wait_list; } SIZE: 0x30
其初始值如下
crash> struct semaphore.count semaphore_a count = 1 crash> struct semaphore.count semaphore_b count = 1
此时我们开启测试
echo 3 > /sys/module/test/parameters/testsuite
semaphore的作用是down占用了资源,需要等待up恢复资源,默认情况下count是1,如果down了则是0,我们查看死锁后状态如下
crash> struct semaphore.count semaphore_b count = 0 crash> struct semaphore.count semaphore_a count = 0
可以看到semaphore变量a/b都是被占座了,所以产生了ABBA锁。
本文主要是通过crash的方式来辅助定位死锁问题,在实际问题中,相当于多了一种方式排查问题,我们知道最有效排查死锁的问题还是内核提供的CONFIG,毋庸置疑。
我们在讨论linux的实时的时候,有时候会拿RTOS来做对比,根据最近对RTOS的理解,总结出来关于为什么linux实时不如RTOS的几个点,分享之
在rtems中我们看的中断在特殊情况如内核态是支持中断嵌套的,但是linux是不允许中断嵌套。
也就是说,在Linux中,如何一个中断触发来,那么必须等到同CPU的上一个中断处理完成。
这时候,中断是不确定的
linux支持中断上下文(hardirq和softirq),我们知道softirq的优先级是高于kthread这类线程任务的(tasklet,timer,netrx等)
如果某个外设驱动的softirq一直占用某个core,那么整个core上的thread很可能都得不到运行
我处理can的报文接收(netrx)的时候就遇到过这类情况,它直接导致来一个cpu core不工作。
那么这也就是另一个不确定性,也就是softirq是外设驱动代码,由客户自己编写的,如果softirq执行时间不确定,那么中断就是不确定的 。
例如某个cpu core上,有一个普通任务正在spin lock,那么core上的任务必须等spin unlock后,才能得以调度。
那么此时其任务就是不确定的。因为谁也没办法知道要等多久才能unlock。这样高优先级就不知道什么时候才能得以运行了。
在linux中内存是使用的时候才分配的,高优先级任务在使用内存的时候,内存分配过程只能保证你分配到,但是不能保证你在确定时间内申请到。 甚至可能没有内存导致oom出现。
基于上面提到的,PREEMPT_RT主要解决如下三个问题
对于中断风暴,做法是在线程内disable irq,线程化完成之后,自动enable irq
而softirq线程化,做法是全部将其添加到ksoftirqd(tasklet,timer,netrx等)执行
spinlock的做法就是将raw_spin_lock 替换成 rt_mutex (可睡眠的,支持优先级继承的mutex,避免优先级翻转)
根据上面就知道
所以可以得出结论,添加PREEMPT_RT的内核,它可以保证某个高优先级任务的一定的实时性,但其负面作用会导致其他所有常规任务性能低下。 所以只适合于定制的特殊场景。
根据上面提到的,使用PREEMPT-RT的内核,一定程度上保持了高优先级任务的实时性,那么基于此,我们还需要做哪些事情来定制操作系统,满足这个实时任务的确定性要求呢?
内核其实提供了EDF调度,也就是SCHED_DEADLINE。我们让这个实时任务在这个特殊的调度类中。
chrt工具动态设置进程的调度政策,如chrt --deadline / --fifo
当然pthread_setschedparam也可以设置调度器种类,如SCHED_DEADLINE/SCHED_FIFO传参
通过bootargs传参隔离某个cpu,然后通过taskset将实时任务绑定到这个cpu上跑
如果要高实时,建议直接汇编使用寄存器调用arm64的高精度定时器来封装特定代码。
对于多线程的实时任务,pthread线程创建前建议设置优先级继承,避免优先级翻转。 也就是pthread_muterattr_setprotocol中传参PTHREAD_PRIO_INHERIT。 其默认是NONE。
默认从内核申请的内存都可能被交换,那么申请的内存调用mlock锁住即可,这样这块内存不会被换出。同时,带mlock标志位的内存会在申请的时候,自动触发缺页异常,分配物理页面。相当于lazy机制的逃逸了。
malloc_trim(-1),这里-1就是trim的最大值,也就是gc不再帮你管理释放内存了,内存不会返回到操作系统中,如果内存通过mmap获得,那么就不会munmap了
堆和栈都是内存,对于堆好办,我们直接mlock即可,对于栈,这是操作系统分配的,我们设置了最大栈大小之后,例如是8M,如果是线程,那么可以设置pthread_attr_setstacksize
在所有函数调用之前,调用一个函数,然后通过局部变量申请一个8M,或者你设置的栈大小的空间,将其占住,然后进行memset一次调用。此时栈会通过缺页异常申请内存,后续就不会在缺页异常申请了。 文字表现力不强,如下是代码
void stack_prefetch() { const size_t size = 8 * 1024 * 1024 - 4096; char buffer[size]; memset(buffer, 0, sizeof(buffer)); }
跑在多核的程序,需要利用好cache一致性,例如局部性原理,但是这里要说的是避免数据伪共享,所以多线程访问同一个结构体时,最好cacheline对齐
__attribute__((aligned(cachesize)))
其他就是性能优化相关的了,通过性能优化可以保证任务的低时延。
本文初衷是介绍一下Linux实时内核,并说明Linux实时并不能硬实时的原因,但是为了表述清楚,也补充了使用PREEMPT-RT补丁的内核,和实时应用程序的编写建议,这些建议纯粹属于经验分享性质,不代表权威信息。
总之,PREEMPT-RT是基于linux内核上,为了保证某个实时任务的确定性做的优化,它需要配合该实时任务的很多其他措施一起使用,否则体现不了实时的确定性。 也就是说我们讨论实时任务的时候,应该讨论整个链路的实时性,而不是某一个方面的实时确定性,那没有任何意义
并且,PREEMPT-RT是牺牲整体系统性能为代价而提供的,使用时需要认清利弊。
PREEMPT-RT做不到完全的硬实时确定性,所以如果面临产品级的开发,推荐使用Linux + RTOS 双OS的策略,RTOS本身关注确定性任务,Linux本身关注复杂计算。
之前将了crash中查看结构体,这个是非常常用调试内核状态的方式,文章是《使用crash查看内核结构体》,但是大部分情况下,我们需要看的是堆栈信息。为了演示,这里根据crash中的bt信息,手动推导函数堆栈,用作熟悉crash工具的回溯栈区的方式
这里直接获取了ping的bt,如下
crash> bt 49105 PID: 49105 TASK: ffffff805e2ee580 CPU: 0 COMMAND: "ping" #0 [ffffffc0149b3830] __switch_to at ffffffc008017540 #1 [ffffffc0149b3850] __schedule at ffffffc0095162d0 #2 [ffffffc0149b38f0] schedule at ffffffc009516900 #3 [ffffffc0149b3910] schedule_timeout at ffffffc00951ac64 #4 [ffffffc0149b39a0] __skb_wait_for_more_packets at ffffffc0091a0248 #5 [ffffffc0149b3a20] __skb_recv_datagram at ffffffc0091a0e00 #6 [ffffffc0149b3a90] skb_recv_datagram at ffffffc0091a0ea0 #7 [ffffffc0149b3ac0] ping_recvmsg at ffffffc00928a7ec #8 [ffffffc0149b3b20] inet_recvmsg at ffffffc009279340 #9 [ffffffc0149b3b70] sock_recvmsg at ffffffc009188084 #10 [ffffffc0149b3ba0] ____sys_recvmsg at ffffffc009188d3c #11 [ffffffc0149b3c90] ___sys_recvmsg at ffffffc00918c410 #12 [ffffffc0149b3d80] __sys_recvmsg at ffffffc00918c7e4 #13 [ffffffc0149b3e20] __arm64_sys_recvmsg at ffffffc00918c864 #14 [ffffffc0149b3e30] el0_svc_common at ffffffc008025508 #15 [ffffffc0149b3e70] do_el0_svc at ffffffc008025690 #16 [ffffffc0149b3e80] el0_svc at ffffffc009513510 #17 [ffffffc0149b3ea0] el0_sync_handler at ffffffc009513d54 #18 [ffffffc0149b3fe0] el0_sync at ffffffc008011e14 PC: 0000007f86ec2994 LR: 000000558e2a9f6c SP: 0000007fcdff7a20 X29: 0000007fcdff7a20 X28: 00000000000000c0 X27: 0000007fcdff7ae0 X26: 0000007fcdff7bb8 X25: 000000558e2c1000 X24: 0000007fcdff7b38 X23: 0000000000000000 X22: 000000558e2c2078 X21: 0000007f870ca710 X20: 0000007fcdff7b00 X19: 0000000000000003 X18: 0000000000000001 X17: 0000007f86ec2960 X16: 000000558e2c1b48 X15: 000000007fffffde X14: 0000000000000001 X13: 0000000000000037 X12: 000000007fffffff X11: 00000012b9b749a3 X10: 0014d207f0963169 X9: 0000000000000018 X8: 00000000000000d4 X7: 00000000001d1c32 X6: 0000000029aaaaf1 X5: 0000000000000080 X4: 0000000000000001 X3: 0000007f870c9f10 X2: 0000000000000000 X1: 0000007fcdff7b00 X0: 0000000000000003 ORIG_X0: 0000000000000003 SYSCALLNO: d4 PSTATE: 60001000
我们知道crash更多是用于内核的问题排查,在使用ping的过程中,内核并没有死锁和堆栈,所以当前寄存器是用户空间的寄存器的值,这些不是很方便crash排查,但这不妨碍我们回溯内核栈,下面基于此bt的信息来回溯栈
我们知道aarch64的FP和LR寄存器他们的作用如下
同样的,我们还知道,aarch64中指令大小是32位,那么对应4字节。所以我们可以演示上述两点
根据上面的堆栈,我们知道最后是在ffffffc008017540也就是函数__switch_to中切出,那么我们知道__switch_to的x29寄存器是ffffffc0149b3830,于是我们读出其值如下
crash> rd ffffffc0149b3830 ffffffc0149b3830: ffffffc0149b3850
可以看到ffffffc0149b3850是__schedule的x29寄存器。可以发现x29具备list的特性,所以为了直接回溯,借用list指令可以一次性读出所有的x29寄存器,如下
crash> list ffffffc0149b3830 ffffffc0149b3830 ffffffc0149b3850 ffffffc0149b38f0 ffffffc0149b3910 ffffffc0149b39a0 ffffffc0149b3a20 ffffffc0149b3a90 ffffffc0149b3ac0 ffffffc0149b3b20 ffffffc0149b3b70 ffffffc0149b3ba0 ffffffc0149b3c90 ffffffc0149b3d80 ffffffc0149b3e20 ffffffc0149b3e30 ffffffc0149b3e70 ffffffc0149b3e80 ffffffc0149b3ea0 ffffffc0149b3fe0
我们批量得到了x29寄存器的值,那么我们可以知道x30寄存器是x29+8。同样因为是地址+8,我们借助list的特性,可以得到如下
crash> list -s list_head.prev ffffffc0149b3830 ffffffc0149b3830 prev = 0xffffffc0095162d4 <__schedule+692> ffffffc0149b3850 prev = 0xffffffc009516904 <schedule+68> ffffffc0149b38f0 prev = 0xffffffc00951ac68 <schedule_timeout+376> ffffffc0149b3910 prev = 0xffffffc0091a024c <__skb_wait_for_more_packets+276> ffffffc0149b39a0 prev = 0xffffffc0091a0e04 <__skb_recv_datagram+124> ffffffc0149b3a20 prev = 0xffffffc0091a0ea4 <skb_recv_datagram+60> ffffffc0149b3a90 prev = 0xffffffc00928a7f0 <ping_recvmsg+112> ffffffc0149b3ac0 prev = 0xffffffc009279344 <inet_recvmsg+76> ffffffc0149b3b20 prev = 0xffffffc009188088 <sock_recvmsg+72> ffffffc0149b3b70 prev = 0xffffffc009188d40 <____sys_recvmsg+128> ffffffc0149b3ba0 prev = 0xffffffc00918c414 <___sys_recvmsg+124> ffffffc0149b3c90 prev = 0xffffffc00918c7e8 <__sys_recvmsg+96> ffffffc0149b3d80 prev = 0xffffffc00918c868 <__arm64_sys_recvmsg+32> ffffffc0149b3e20 prev = 0xffffffc00802550c <el0_svc_common+108> ffffffc0149b3e30 prev = 0xffffffc008025694 <do_el0_svc+28> ffffffc0149b3e70 prev = 0xffffffc009513514 <el0_svc+28> ffffffc0149b3e80 prev = 0xffffffc009513d58 <el0_sync_handler+168> ffffffc0149b3ea0 prev = 0xffffffc008011e18 <el0_sync+344> ffffffc0149b3fe0 prev = 0x0
可以看到,这里的x30寄存器保存这返回地址的下一个指令,那么我们计算返回地址就是上述地址-4即可。
至此,我们通过一个例子,将crash的bt的回栈进行了解析。
在内核中调试会经常使用crash工具,此工具可以调试死锁,假死等问题,之前《RK平台上使用crash进行live debug》上已经分析了crash工具的安装和基本使用,本文作为加强理解篇,以读取系统的task_struct的tasks字段,从而获取当前进程的所有进程的task_struct。
我们通过ps可以查看到当前系统的进程信息,以前10个为例
PID PPID CPU TASK ST %MEM VSZ RSS COMM > 0 0 0 ffffffc00a6d23c0 RU 0.0 0 0 [swapper/0] > 0 0 1 ffffff81f0856580 RU 0.0 0 0 [swapper/1] > 0 0 2 ffffff81f0898000 RU 0.0 0 0 [swapper/2] 0 0 3 ffffff81f0898e80 RU 0.0 0 0 [swapper/3] 0 0 4 ffffff81f0899d00 RU 0.0 0 0 [swapper/4] > 0 0 5 ffffff81f089ab80 RU 0.0 0 0 [swapper/5] > 0 0 6 ffffff81f089ba00 RU 0.0 0 0 [swapper/6] > 0 0 7 ffffff81f089c880 RU 0.0 0 0 [swapper/7] 1 0 2 ffffff81f0808000 IN 0.1 245056 6372 systemd 2 0 6 ffffff81f0808e80 IN 0.0 0 0 [kthreadd] 3 2 0 ffffff81f0809d00 ID 0.0 0 0 [rcu_gp] 4 2 0 ffffff81f080ab80 ID 0.0 0 0 [rcu_par_gp] 8 2 0 ffffff81f080e580 ID 0.0 0 0 [mm_percpu_wq] 9 2 0 ffffff81f0850000 IN 0.0 0 0 [rcu_tasks_rude_]
通过上面信息可以发现,cpu0,1,2,5,6,7都是idle状态,只有cpu3和4是运行的状态。
从TASK一列,我们能够拿到struct task_struct的结构体地址,接下来我们基于此来进行实践crash工具
struct task_struct init_task start_kernel sched_init init_idle sprintf(idle->comm, "%s/%d", INIT_TASK_COMM, cpu); #define INIT_TASK_COMM "swapper"
对于此结构体我们关注tasks链表,所以我们需要得到pid和comm和tasks list,以swapper/0为例
crash> struct task_struct.pid,comm ffffffc00a6d23c0 pid = 0 comm = "swapper/0\000\000\000\000\000\000" tasks = { next = 0xffffff81f0808438, prev = 0xffffff80b2a82fb8 }
根据上面代码,我们知道每个cpu都有一个idle进程,所以包含cpu 1-7的信息如下
crash> struct task_struct.pid,comm,tasks ffffff81f0856580 pid = 0 comm = "swapper/1\000\000\000\000\000\000" tasks = { next = 0xffffff81f08092b8, prev = 0xffffffc00a6d27f8 <init_task+1080> } crash> struct task_struct.pid,comm,tasks ffffff81f0898000 pid = 0 comm = "swapper/2\000\000\000\000\000\000" tasks = { next = 0xffffff81f08092b8, prev = 0xffffffc00a6d27f8 <init_task+1080> } crash> struct task_struct.pid,comm,tasks ffffff81f0898e80 pid = 0 comm = "swapper/3\000\000\000\000\000\000" tasks = { next = 0xffffff81f08092b8, prev = 0xffffffc00a6d27f8 <init_task+1080> } crash> struct task_struct.pid,comm,tasks ffffff81f0899d00 pid = 0 comm = "swapper/4\000\000\000\000\000\000" tasks = { next = 0xffffff81f08092b8, prev = 0xffffffc00a6d27f8 <init_task+1080> } crash> struct task_struct.pid,comm,tasks ffffff81f089ab80 pid = 0 comm = "swapper/5\000\000\000\000\000\000" tasks = { next = 0xffffff81f08092b8, prev = 0xffffffc00a6d27f8 <init_task+1080> } crash> struct task_struct.pid,comm,tasks ffffff81f089ba00 pid = 0 comm = "swapper/6\000\000\000\000\000\000" tasks = { next = 0xffffff81f08092b8, prev = 0xffffffc00a6d27f8 <init_task+1080> } crash> struct task_struct.pid,comm,tasks ffffff81f089c880 pid = 0 comm = "swapper/7\000\000\000\000\000\000" tasks = { next = 0xffffff81f08092b8, prev = 0xffffffc00a6d27f8 <init_task+1080> }
我们知道所有的task通过tasks串起来,所以我们可以先定位tasks位于task_struct的举例,如下
crash> struct task_struct.tasks -o -x struct task_struct { [0x438] struct list_head tasks; }
这里可以知道位于task_struct的0x438个字节。我们打印tasks的链表,这里先以swapper/0为例如下
crash> struct task_struct.pid,comm,tasks ffffffc00a6d23c0 pid = 0 comm = "swapper/0\000\000\000\000\000\000" tasks = { next = 0xffffff81f0808438, prev = 0xffffff81f09e8438 }
此时我们知道其next指针是0xffffff81f0808438,它是其他进程task_struct.tasks的指针,所以我们可以通过计算偏移量来获得 task_struct。如下
>>> hex(0xffffff81f0808438-0x438) '0xffffff81f0808000'
此时我们获得了next的 task_struct指针,所以我们打印如下
crash> struct task_struct.pid,comm,tasks 0xffffff81f0808000 pid = 1 comm = "systemd\000\000\000\000\000\000\000\000" tasks = { next = 0xffffff81f08092b8, prev = 0xffffffc00a6d27f8 <init_task+1080> }
同样的,对于非swapper/0上的idle进程,我们可以获取其next的进程信息,它们默认是kthreadd如下
crash> struct task_struct.pid,comm,tasks ffffff81f0856580 pid = 0 comm = "swapper/1\000\000\000\000\000\000" tasks = { next = 0xffffff81f08092b8, prev = 0xffffffc00a6d27f8 <init_task+1080> }
计算地址
>>> hex(0xffffff81f08092b8-0x438) '0xffffff81f0808e80'
打印task_struct
crash> struct task_struct.pid,comm,tasks 0xffffff81f0808e80 pid = 2 comm = "kthreadd\000\000\000\000\000\000\000" tasks = { next = 0xffffff81f080a138, prev = 0xffffff81f0808438 }
根据上面我们可以简单通过tasks来获取next的task_struct,接下来我们使用list命令。对于swapper/0,我们list获取链表所有成员如下
crash> list -h 0xffffff81f0808438 ffffff81f0808438 ffffff81f08092b8 ffffff81f080a138
这样可以直接计算出所有的task_struct,通过如下
for addr in addresses: print("struct task_struct.pid,comm,tasks", hex(int(addr, 16) - 0x438))
这里粘贴前五个打印如下
crash> struct task_struct.pid,comm,tasks 0xffffff81f0808000 pid = 1 comm = "systemd\000\000\000\000\000\000\000\000" tasks = { next = 0xffffff81f08092b8, prev = 0xffffffc00a6d27f8 <init_task+1080> } crash> struct task_struct.pid,comm,tasks 0xffffff81f0808e80 pid = 2 comm = "kthreadd\000\000\000\000\000\000\000" tasks = { next = 0xffffff81f080a138, prev = 0xffffff81f0808438 } crash> struct task_struct.pid,comm,tasks 0xffffff81f0809d00 pid = 3 comm = "rcu_gp\000d\000\000\000\000\000\000\000" tasks = { next = 0xffffff81f080afb8, prev = 0xffffff81f08092b8 } crash> struct task_struct.pid,comm,tasks 0xffffff81f080ab80 pid = 4 comm = "rcu_par_gp\000\000\000\000\000" tasks = { next = 0xffffff81f080e9b8, prev = 0xffffff81f080a138 } crash> struct task_struct.pid,comm,tasks 0xffffff81f080e580 pid = 8 comm = "mm_percpu_wq\000\000\000" tasks = { next = 0xffffff81f0850438, prev = 0xffffff81f080afb8 }
对于swapper/1-7,这里以3个作为示例,如下
crash> struct task_struct.pid,comm,tasks 0xffffff81f0809d00 pid = 3 comm = "rcu_gp\000d\000\000\000\000\000\000\000" tasks = { next = 0xffffff81f080afb8, prev = 0xffffff81f08092b8 } crash> struct task_struct.pid,comm,tasks 0xffffff81f080ab80 pid = 4 comm = "rcu_par_gp\000\000\000\000\000" tasks = { next = 0xffffff81f080e9b8, prev = 0xffffff81f080a138 } crash> struct task_struct.pid,comm,tasks 0xffffff81f080e580 pid = 8 comm = "mm_percpu_wq\000\000\000" tasks = { next = 0xffffff81f0850438, prev = 0xffffff81f080afb8 }
可以看到,这里信息和最上面的ps得到的进程信息完全一致。
至此,我们根据crash做了一个简单的实验,通过task_struct的tasks遍历查找所有的pid和comm,它可以方便的实时查看内核的结构体数据,从而学习内核和定位内核问题。