该博文转自:https://blog.csdn.net/wennuanddianbo/article/details/55107046
一、初识0号任务
打开linux系统的命令行,敲下“ps -A”命令,就能看到系统中的所有任务:其中有一个pid号为0任务格外的显眼。
我们知道linux中的task都是通过fork族函数由父进程创造出来,而这个0号进程却是个例外,它由人类捏造出来,没有parent。
Linux中所有task都由一个task_struct结构来代表,一旦task有了task_struct,我们就能够看得见摸得着;一个新task的创建过程首先就是为其分配task_struct结构,使它的灵魂有所寄托。
而这个0号进程(在linux中一般采用“任务”来指代一个进程,这里“进程”的说法和“任务”一个意思)的task_struct结构的对象通过静态全局变量的方式创建,这个对象实例叫init_task。它随内核代码一起编到内核镜像之中,内核镜像一旦编译完成0号进程的task_struct实例init_task也随之生成,即在内核中已经有存在。这个实例init_task的定义在init/init_task.c目录下:struct task_struct init_task = INIT_TASK(init_task);
这个init_task就是0号进程task_struct结构对象实例,init_task中的各个成员通过INIT_TASK(init_task)这个宏来初始化,我们列举出一些我们关注的初始化成员值,没有列出的用省略号代替:
#define INIT_TASK(tsk) \
{ \
.state = 0, \
.stack = &init_thread_info, \
.flags = PF_KTHREAD, \
......
.prio = MAX_PRIO-20, \
.static_prio = MAX_PRIO-20, \
.normal_prio = MAX_PRIO-20, \
.policy = SCHED_NORMAL, \
......
.real_parent = &tsk, \
.parent = &tsk, \
......
.comm = INIT_TASK_COMM, \
......
}
可以看出init_task的成员就是通过上面的宏展开后被一一赋予了给定的值:
1. 内核堆栈
一个任务内核堆栈指针由task_struct.stack来指定,init_task的stack指针也是静态分配的:.stack = &init_thread_info
2. 任务标志
Init_task是一个内核任务,其标志也初始设置为PF_KTHREAD。.flags = PF_KTHREAD
3. 调度策略
Init_task虽然是内核中的第一个任务,但它并不高特殊,不论是调度策略(SCHED_NORMAL)还是优先级(120),都是最普通的:.prio = MAX_PRIO-20, \
.static_prio = MAX_PRIO-20, \
.normal_prio = MAX_PRIO-20, \
.policy = SCHED_NORMAL, \4. init_task的父辈
Init_task是系统中的第一个任务,所以它就是系统中的始祖:.real_parent = &tsk, \
.parent = &tsk, \5. 任务名字
Init_task的名字通过成员"comm"表示,其值就是"swapper"。#define INIT_TASK_COMM "swapper"
.comm = INIT_TASK_COMM一旦内核编译完成生成镜像,这个init_task就已经在其中占有自己的一席之地,内核已启动它便存在!
二、认识0号任务的堆栈
2.1 初识
从上面我们已经看到0号任务的堆栈,准确的叫内核堆栈,已经初始化为init_thread_info:
.stack = &init_thread_info
这个init_trhead_info实际上是一个宏,其定义与架构相关,但是大部分架构的定义都一样(x86、arm、mips以及powerpc)如下:
#define init_thread_info (init_thread_union.thread_info)
看到没?这个init_thread_info只是一个幌子,实际上stack指向的是 .stack = &init_thread_union.thread_info;而这个init_thread_union是一个与架构无关的全局union,定义在init/init_task.c中:
union thread_union init_thread_union __init_task_data =
{ INIT_THREAD_INFO(init_task) };这个init_thread_union又是一个宏,我们再将他展开看看.....等等,思路似乎有些乱。我们先缕一缕:
1) 前面已经确定了0号任务的内核堆栈指针init_task.stack指向的是&init_thread_union.thread_info;
2) 一个task的内核堆栈指针指向一片连续的内存区域(即内核堆栈使用区域),这片堆栈区域的大小随架构的不同其size也有所差异,一般通过宏THREAD_SIZE来表示。
3) 综合1)和2)可以知道0号进程内核堆栈指针与&init_thread_union.thread_info相等,即这片内核堆栈区域的底端实际放的就是init_thread_union.thread_info。而实际在一般情况下一个task的内核堆栈底部确实放着一个thread_info结构!
4) 内核堆栈的作用就是用来保存数据用的,而0号任务的这块堆栈区域却是静态分配的;那这块区域是在哪里分配的呢??
好,我们就再来深挖一下0号任务堆栈以及init_thread_union.thread_info的真面目。
2.2 深挖
既然0号task的堆栈指针指向init_thread_union.thread_info的地址,那就拨开云雾见天日,研究一下我们深挖的重点:0号任务init_task.stack指针指向的大小THREAD_SIZE的区域究竟在哪里分配。
2.2.1 init_thread_union的真面目
首先,init_task.stack是与init_thread_union这个变量关联,我们先来关注它。
union thread_union init_thread_union __init_task_data =
{ INIT_THREAD_INFO(init_task) };这个thred_union联合体定义如下:
union thread_union {
struct thread_info thread_info; /* init_task.stack 指向这个成员 */
unsigned long stack[THREAD_SIZE/sizeof(long)]; /* stack[]将thread_union扩张为THREAD_SIZE大小 */
};
即这个静态全局变量 init_thread_union实际上已经静态分配了THREAD_SIZE的大小,由于它是一个联合体,所以我们引用init_thread_union.thread_info就可以够着这个THREAD_SIZE的内存区域,我们的init_task.stack也就在这里着落。
一般情况下全局变量都放在内核镜像的数据段,但是这里可以的init_thread_union通过__init_task_data属性使其init_thread_union最终链接到镜像中的__init_task_data这个段中,即init_task的内核堆栈THREAD_SIZE大小的区域最终放在这个__init_task_data属性段中。
2.2.2 init_task内核堆栈的实际位置
既然我们知道0号任务内核堆栈是在__init_task_data属性段中,那我们就来研究一下:
#define __init_task_data __attribute__((__section__(".data..init_task")))
即init_thread_union最终是链接到.data..init_task这个数据段中,即init_task的堆栈指向.data..init_task这个位置。
而.data..init_task这个段的位置是在vmlinux.lds.S文件中决定的,不同的架构其位置不一样:
在arm架构中如下所示:
.data : AT(__data_loc) {
_data = .; /* address in memory */
_sdata = .;
INIT_TASK_DATA(THREAD_SIZE) /* .data..init_task段 */
NOSAVE_DATA
宏INIT_TASK_DATA的定义在include/asm-generic/vmlinux.lds.h中,它是与架构无关的:
#define INIT_TASK_DATA(align) \
. = ALIGN(align); \
*(.data..init_task)
这里的align=THREAD_SIZE,即.data..init_task段是THREAD_SIZE大小对齐的,对,这就是我们想要的结果,完全吻合。
从上面可以看出.data..init_task实际上是在内核镜像中.data靠前的地方定义的一个大小为THREAD_SIZE的大小数据块。
THREAD_SIZE的大小随架构的不同其值不同,arm架构一般是8k(也就是2个page),这样一来,一旦内核编译完成,系统中的0号进程也就真的“凭空”创造出来了!
三、内核启动初期sp初始化
从上面已经了解到内核中静态创建了一个0号进程init_task,其堆栈init_task.stack指向.data..init_task段中一个大小为THREAD_SIZE大小的区域,在这个栈的底端放着全局变量init_thread_info。
需要指出一点,在内核完成初始化之前,内核会使用这个栈来进行工作---这是通过将系统中的堆栈指针sp指向init_task的堆栈来实现的。
这个过程的实现具体的架构相关,机器上电后会在bios或者uboot或者其他固件中执行,最终跳转到内核入口执行,这个入口点在做完一些重要的初始化工作后就会跳转到start_kernel入口开始初始化内核。而堆栈指针sp的初始化就是在内核入口点的初期、跳转到start_kernel之前进行。
arm架构中会先将 init_thread_union+THREAD_START_SP放到__mmap_switched_data标号处后面16字节的位置,然后通过ld指令加载到sp寄存器中。
__mmap_switched:
adr r3, __mmap_switched_data /* 将__mmap_switched_data标号对应地址赋给r3 */
ldmia r3!, {r4, r5, r6, r7} /* 把r3指向的内容赋值给r4,r5,r6,r7,同时把r3指向__mmap_switched_data + 4*sizeof(long) */
cmp r4, r5 @ Copy data segment if needed
......
/* 将 init_thread_union+THREAD_START_SP 装入sp寄存器 */
ARM( ldmia r3, {r4, r5, r6, r7, sp})
........
b start_kernel
.....................
__mmap_switched_data:
.long __data_loc @ r4
.long _sdata @ r5
.long __bss_start @ r6
.long _end @ r7
/* 执行ldmia r3!, {r4, r5, r6, r7}后r3指向这里 */
.long processor_id @ r4
.long __machine_arch_type @ r5
.long __atags_pointer @ r6
#ifdef CONFIG_CPU_CP15
.long cr_alignment @ r7
#else
.long 0 @ r7
#endif
/* 这里存放值 init_thread_union+THREAD_START_SP */
.long init_thread_union + THREAD_START_SP @ sp