Linux内核Crash分析
在工作中经常会遇到一些内核crash的情况,本文就是根据内核出现crash后的打印信息,对其进行了分析,使用的内核版本为:Linux2.6.32。
每一个进程的生命周期内,其生命周期的范围为几毫秒到几个月。一般都是和内核有交互,例如用户空间程序使用系统调用进入内核空间。这时使用的不再是用户空间的栈空间,使用对应的内核栈空间。对每一个进程来说,Linux内核都会把两个不同的数据结构紧凑的存放在一个单独为进程分配的存储空间中:一个是内核态的进程堆栈,另一个是紧挨进程描述符的数据结构thread_info,叫线程描述符。内核的堆栈大小一般为8KB,也就是8192个字节,占用两个页。在Linux-2.6.32内核中thread_info.h文件中有对内核堆栈的定义:
- #define THREAD_SIZE 8192
在Linux内核中使用下面的联合结构体表示一个进程的线程描述符和内核栈,在内核中文件include/linux/sched.h。
- union thread_union {
- struct thread_info thread_info;
- unsignedlong stack[THREAD_SIZE/sizeof(long)];
- };
该结构是一个联合体,我们在C语言书上看到过关于union的解释,在在C Programming Language 一书中对于联合体是这么描述的:
1) 联合体是一个结构;
2) 它的所有成员相对于基地址的偏移量都为0;
3) 此结构空间要大到足够容纳最"宽"的成员;
4) 其对齐方式要适合其中所有的成员;
通过上面的描述可知,thread_union结构体的大小为8192个字节。也就是stack数组的大小,类型是unsigned long类 型。由于联合体中的成员变量都是占用同一块内存区域,所以,在平时写代码时总有一个概念,对一个联合体的实例只能使用其中一个成员变量,否则会把原先变量 给覆盖掉,这句话如果正确的话,必须要有一个前提假设,成员占用的字节数相同,当成员所占的字节数不同时,只会覆盖相应的字节。对于thread_union联合体,我们是可以同时访问这两个成员,只要能够正确获取到两个成员变量的地址。
在内核中的某一个进程使用了过多的栈空间时,内核栈就会溢出到thread_info部分,这将导致严重的问题(系统重启),例如,递归调用的层次太深;在函数内定义的数据结构太大。
图:进程中thread_info task_struct和内核栈中的关系
下面我们看一下thread_info的结构体:
- struct thread_info {
- unsignedlong flags;/* 底层标志,*/
- int preempt_count;/* 0 => 可抢占, <0 => bug */
- mm_segment_t addr_limit;/* 进程地址空间 */
- struct task_struct *task;/*当前进程的task_struct指针 */
- struct exec_domain *exec_domain;/*执行区间 */
- __u32 cpu;/* 当前cpu */
- __u32 cpu_domain;/* cpu domain */
- struct cpu_context_save cpu_context;/* cpu context */
- __u32 syscall;/* syscall number */
- __u8 used_cp[16];/* thread used copro */
- unsignedlong tp_value;
- struct crunch_state crunchstate;
- union fp_state fpstate __attribute__((aligned(8)));
- union vfp_state vfpstate;
- #ifdef CONFIG_ARM_THUMBEE
- unsignedlong thumbee_state;/* ThumbEE Handler Base register */
- #endif
- struct restart_block restart_block;/*用于实现信号机制*/
- };
PS:(1)flag 用于保存各种特定的进程标志,最重要的两个是:TIF_SIGPENDING,如果进程有待处理的信号就置位,TIF_NEED_RESCHED表示进程应该需要调度器选择另一个进程替换本进程执行。