进程管理模块 进程是拥有执行资源的最小单位,它为每个程序维护着运行时的各种资源,如进程ID、进程的页表、进程执行现场的寄存器值、进程各个段地址空间分布信息以及进程执行时的维护信息等,它们在程序的运行期间会被经常或实时更新。这些资源被结构化到PCB(Process Control Block,进程控制结构体)内,PCB作为进程调度的决策信息供调度算法使用。
进程调度策略负责将满足运行条件或迫切需要执行的进程到空闲处理器中执行。进程调度策略直接影响程序的执行效率。
PCB PCB用于记录进程的资源使用情况(包括软件资源是硬件资源)和运行状态等。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 struct task_struct { struct list list ; volatile long state; unsigned long flags; struct mm_struct *mm ; struct thread_struct *thread ; unsigned long addr_limit; long pid; long counter; long signal; long priority; };
成员变量mm与thread负责在进程调度过程中保存或还原CR3控制寄存器的页目录基地址和通用寄存器值
state成员变量使用volatile关键字修饰,说明该变量可能会在意想不到的情况下修改,因此编译器不要对此成员变量进行优化。处理器每次使用这个变量前,必须重新读取该变量的值,而不能使用保存在寄存器的值。
内存空间分布结构体struct mm_struct描述了进程的页表结构和各程序段信息,其中有页目录基地址、代码段、数据段、只读数据段、应用层栈顶地址等信息。
1 2 3 4 5 6 7 8 struct mm_struct { pml4t_t *pgd; unsigned long start_code, end_code; unsigned long start_data, end_data; unsigned long start_rodata, end_rodata; unsigned long start_brk, end_brk; unsigned long start_stack; };
mm_struct结构体各个成员变量的功能说明,其中成员变量pgd保存在CR3控制寄存器值(页目录基地址与页表属性的组合值),成员变量start_stack记录应用程序在应用层的栈顶地址,其他成员变量描述了应用程序的各段地址空间。
每当进程发生调度切换时,都必须将执行现场的寄存器保存起来,已备再次执行时使用。
这些数据都保存在struct thread_struct结构体内:
1 2 3 4 5 6 7 8 9 10 struct thread_struct { unsigned long rsp0; unsigned long rip; unsigned long rsp; unsigned long fs; unsigned long gs; unsigned long cr2; unsigned long trap_nr; unsigned long error_code; };
其中成员变量rsp0记录应用程序在内核层使用的栈基地址,rsp保存这进程切换时的栈指针值,rip成员保存着进程切换回来时执行代码的地址。
关于进程的内核层栈空间实现,借鉴Linux内核设计思想,把进程控制结构体struct task_struct与进程的内核层栈空间融为一体。其中,低地址处存放struct task_struct结构体,余下高地址空间作为进程内核层栈空间使用,如下:
1 2 3 4 union task_union { struct task_struct task ; unsigned long stack [STACK_SIZE / sizeof (unsigned long )]; } __attribute__((aligned(8 )));
借助联合体,把进程控制结构体struct task_struct与进程的内核层栈空间连续到了一起,其中宏常量TASK_SIZE被定义为32768B(32KB),它表示进程的内核栈空间和struct task_struct结构体占用的存储空间总量为32KB,在Intel i386处理器架构的Linux内核中默认使用8KB的内核栈空间。由于64位处理器的寄存器位宽扩大一倍,相应的栈空间也必须扩大,此处暂时设定为32KB,待到存储空间不足再扩容
这个联合体占用32KB,并将这段空间按8B进行对齐,实际上这个联合体的起始地址必须按照32KB进行对齐。
初始化全局变量init_task_union,并作为系统的第一个进程。进程控制结构体数组init_task(指针数组)是为各个处理器创建的初始控制结构体,当前只有第0个元素使用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 struct mm_struct init_mm ;struct thread_struct init_thread ;#define INIT_TASK(tsk) \ { \ .state = TASK_UNINTERRUPTIBLE, .flags = PF_KTHREAD, .mm = &init_mm, \ .thread = &init_thread, .addr_limit = 0xffff800000000000, .pid = 0, \ .counter = 1, .signal = 0, .priority = 0 \ } union task_union init_task_union __attribute__ (( __section__ (".data .init_task "))) = {INIT_TASK(init_task_union.task)};struct task_struct *init_task [NR_CPUS ] = {&init_task_union.task, 0 };struct mm_struct init_mm = {0 }; struct thread_struct init_thread = { .rsp0 = (unsigned long )(init_task_union.stack + STACK_SIZE / sizeof (unsigned long )), .rsp = (unsigned long )(init_task_union.stack + STACK_SIZE / sizeof (unsigned long )), .fs = KERNEL_DS, .gs = KERNEL_DS, .cr2 = 0 , .trap_nr = 0 , .error_code = 0 };
init_task_union使用__attribute__((__section__(".data.init_task")))修饰,从而将该全局变量链接到一个特别的程序段内。
链接脚本kernel.lds为这个程序规划地址空间:
1 2 3 4 5 6 7 8 9 10 11 12 13 SECTIONS { ... .rodata : { _rodata = .; *(.rodata) _erodata = .; } . = ALIGN(32768); .data.init_task : {*(.data.init_task)} ... }
.data.init_task被放置在只读数据段rodata之后,并按照32KB对齐。此处采用32KB对齐而非8B对齐,因为处理init_task_union联合体都使用kmalloc函数申请,函数kmalloc返回的内存空间起始地址均按照32KB对齐。如果把.data.init_task段按8B对齐,在使用宏current和GET_CURRENT的过程中会有隐患。
IA-32e模式下的TSS结构,INIT_TSS初始化宏以及各处理器的TSS结构体数组init_tss:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 struct tss_struct { unsigned int reserved0; unsigned long rsp0; unsigned long rsp1; unsigned long rsp2; unsigned long reserved1; unsigned long ist1; unsigned long ist2; unsigned long ist3; unsigned long ist4; unsigned long ist5; unsigned long ist6; unsigned long ist7; unsigned long reserved2; unsigned short reserved3; unsigned short iomapbaseaddr; } __attribute__((packed)); #define INIT_TSS \ { \ .reserved0 = 0, \ .rsp0 = (unsigned long)(init_task_union.stack + \ STACK_SIZE / sizeof(unsigned long)), \ .rsp1 = (unsigned long)(init_task_union.stack + \ STACK_SIZE / sizeof(unsigned long)), \ .rsp2 = (unsigned long)(init_task_union.stack + \ STACK_SIZE / sizeof(unsigned long)), \ .reserved1 = 0, .ist1 = 0xffff800000007c00, .ist2 = 0xffff800000007c00, \ .ist3 = 0xffff800000007c00, .ist4 = 0xffff800000007c00, \ .ist5 = 0xffff800000007c00, .ist6 = 0xffff800000007c00, \ .ist7 = 0xffff800000007c00, .reserved2 = 0, .reserved3 = 0, \ .iomapbaseaddr = 0 \ } struct tss_struct init_tss [NR_CPUS ] = {[0 ... NR_CPUS - 1 ] = INIT_TSS};
__attribute__((packed))修饰这个结构体,表示编译器不会对此结构体内的成员变量进行字节对齐。
将执行现场的数据组织成一个结构体:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 struct pt_regs { unsigned long r15; unsigned long r14; unsigned long r13; unsigned long r12; unsigned long r11; unsigned long r10; unsigned long r9; unsigned long r8; unsigned long rbx; unsigned long rcx; unsigned long rdx; unsigned long rsi; unsigned long rdi; unsigned long rbp; unsigned long ds; unsigned long es; unsigned long rax; unsigned long func; unsigned long errcode; unsigned long rip; unsigned long cs; unsigned long rflags; unsigned long rsp; unsigned long ss; };
get_current函数和GET_CURRENT宏的实现:
1 2 3 4 5 6 7 8 9 10 11 12 static inline struct task_struct *get_current () { struct task_struct *current = NULL ; __asm__ __volatile__("andq %%rsp,%0 \n\t" : "=r" (current) : "0" (~32767UL )); return current; } #define current get_current() #define GET_CURRENT \ "movq %rsp, %rbx \n\t" \ "andq $-32768,%rbx \n\t"
借助struct task_union时使用的32KB对齐技巧实现。get_current与GET_CURRENT均是在当前栈指针寄存器RSP的基础上,按32KB下边界对齐实现的。实现方法是将数值32767(32KB-1)取反,再将取得的结果0xffffffffffff8000与栈指针寄存器RSP的值执行逻辑与计算,结果就是当前进程struct task_struct结构体基地址。(将32KB对齐的地址清零,之后则是task的起始地址)
init进程 进程切换 进程切换示意图:
prev进程通过调用switch_to模块来保存RSP寄存器的当前值,并指定切换会prev进程时的RIP寄存器值,此处默认将其指定在标识符1:处。随后将next进程的栈指针恢复到RSP寄存器中,再把next进程执行现场的RIP寄存器值压入next进程的内核层栈空间中(RSP寄存器的恢复在前,此后的数据将压入next进程的内核层栈空间)。最后借助JMP指令执行__switch_to函数,__switch_to函数执行完成返回执行RET指令,进而跳转到next进程继续执行(恢复执行现场的RIP寄存器)。至此,进程间的切换完毕。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #define switch_to(prev, next) \ do { \ __asm__ __volatile__("pushq %%rbp \n\t" \ "pushq %%rax \n\t" \ "movq %%rsp, %0 \n\t" \ "movq %2, %%rsp \n\t" \ "leaq 1f(%%rip), %%rax \n\t" \ "movq %%rax, %1 \n\t" \ "pushq %3 \n\t" \ "jmp __switch_to \n\t" \ "1: \n\t" \ "popq %%rax \n\t" \ "popq %%rbp \n\t" \ : "=m" (prev->thread->rsp), "=m" (prev->thread->rip) \ : "m" (next->thread->rsp), "m" (next->thread->rip), \ "D" (prev), "S" (next) \ : "memory" ); \ } while (0)
RDI和RSI寄存器分别保存宏参数prev和next所代表的进程控制块结构体,执行__switch_to函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 void __switch_to(struct task_struct *prev, struct task_struct *next) { init_tss[0 ].rsp0 = next->thread->rsp0; set_tss64(init_tss[0 ].rsp0, init_tss[0 ].rsp1, init_tss[0 ].rsp2, init_tss[0 ].ist1, init_tss[0 ].ist2, init_tss[0 ].ist3, init_tss[0 ].ist4, init_tss[0 ].ist5, init_tss[0 ].ist6, init_tss[0 ].ist7); __asm__ __volatile__("movq %%fs, %0 \n\t" : "=a" (prev->thread->fs)); __asm__ __volatile__("movq %%gs, %0 \n\t" : "=a" (prev->thread->gs)); __asm__ __volatile__("movq %0, %%fs \n\t" ::"a" (next->thread->fs)); __asm__ __volatile__("movq %0, %%gs \n\t" ::"a" (next->thread->gs)); color_printk(WHITE, BLACK, "prev->thread->rsp0:%#018lx\n" , prev->thread->rsp0); color_printk(WHITE, BLACK, "next->thread->rsp0:%#018lx\n" , next->thread->rsp0); }
%rdi,%rsi,%rdx,%rcx,%r8,%r9 用作函数参数,依次对应第1参数,第2参数。。。。当参数超过6个时,参数会向地址压栈
1、%rax作为函数返回值使用 2、%rsp指向栈顶 3、%rdi、%rsi、%rdx、%rcx、%r8、%r9、%r10等寄存器用于存放函数参数
执行过程如下:
把rbp, rax入栈(执行switch_to函数的进程的栈,一般为prev)
把当前rsp、1f(%%rip)的值都存入prev->thread对应变量中(若切换回prev进程执行,则执行1:处的指令,即从栈中弹出rax, rbp)
把next->thread->rsp的值放入rsp中
把next->thread->rip的值入栈
执行jmp __switch_to指令,跳转到 task.c 中执行,使用RDI(=prev)和RSI(=next)传递参数
设置tss
把当前fs, gs的值都存入prev->thread对应变量中
把next->thread对应值存入fs, gs中
执行ret指令,从栈中弹出next->thread->rip的值,CPU跳转至next->thread->rip处执行
现在假设 1号进程 已经初始化完毕,有了自己的32-kb栈和进程信息。需要 0号进程 执行switch_to(prev, next),进行进程切换,其中prev是0号进程,next是1号进程。切换结果是 当前CPU的rsp, rip, fs, gs值存入prev->thread对应变量中,加载next->thread的对应值到rsp, rip, fs, gs中,并开始执行next->thread->rip处的指令。这样,栈(rsp)和指令(rip)都切换到了next进程,开始执行next进程。
初始化第一个进程 对第一个进程进行初始化,调用kernel_thread为系统创建处一个新进程,随后借助switch_to执行进程切换:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 void task_init () { struct task_struct *p = NULL ; init_mm.pgd = (pml4t_t *)global_cr3; init_mm.start_code = memory_management_struct.start_code; init_mm.end_code = memory_management_struct.end_code; init_mm.start_data = (unsigned long )&_data; init_mm.end_data = memory_management_struct.end_data; init_mm.start_rodata = (unsigned long )&_rodata; init_mm.end_rodata = (unsigned long )&_erodata; init_mm.start_brk = 0 ; init_mm.end_brk = memory_management_struct.end_brk; init_mm.start_stack = _stack_start; set_tss64(init_thread.rsp0, init_tss[0 ].rsp1, init_tss[0 ].rsp2, init_tss[0 ].ist1, init_tss[0 ].ist2, init_tss[0 ].ist3, init_tss[0 ].ist4, init_tss[0 ].ist5, init_tss[0 ].ist6, init_tss[0 ].ist7); init_tss[0 ].rsp0 = init_thread.rsp0; list_init(&init_task_union.task.list ); kernel_thread(init, 10 , CLONE_FS | CLONE_FILES | CLONE_SIGNAL); init_task_union.task.state = TASK_RUNNING; p = container_of(list_next(¤t->list ), struct task_struct, list ); switch_to(current, p); }
_stack_start在header.S中实现:
1 2 3 .global _stack_start _stack_start: .quad init_task_union + 32768
全局变量_stack_start保存的数值与init_thread结构体变量中rsp0变量的数值是一样的,都指向了系统第一个进程的内核栈基地址。定义全局变量_stack_start可让内核执行头程序直接使用该进程的内核层栈空间,进而减少栈空间切换带来的隐患。
init进程 系统的第二个进程,无实际功能,只是打印由创建者传入的参数并返回1以证明运行 其实init函数和日常编写的main主函数一样,经过编译器编译生成若干个程序片段并记录程序的入口地址,当操作系统为程序创建进程控制结构体时,操作系统会取得程序的入口地址,并从这个入口地址处执行
1 2 3 4 unsigned long init (unsigned long arg) { color_printk(RED, BLACK, "init task is running, arg:%#018lx\n" , arg); return 1 ; }
创建进程 kernel_thread创建进程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 int kernel_thread (unsigned long (*fn)(unsigned long ), unsigned long arg, unsigned long flags) { struct pt_regs regs ; memset (®s, 0 , sizeof (regs)); regs.rbx = (unsigned long )fn; regs.rdx = (unsigned long )arg; regs.ds = KERNEL_DS; regs.es = KERNEL_DS; regs.cs = KERNEL_CS; regs.ss = KERNEL_DS; regs.rflags = (1 << 9 ); regs.rip = (unsigned long )kernel_thread_func; return do_fork(®s, flags, 0 , 0 ); }
其中RBX寄存器保存程序入口,RDX寄存器保存着进程创建者传入的参数,RIP寄存器保存着引导程序,这段引导程序会在目标程序(保存在参数fn中)执行前运行。
随后将参数传递给do_fork函数来创建进程结构体完成运行前的初始化工作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 unsigned long do_fork (struct pt_regs *regs, unsigned long clone_flags, unsigned long stack_start, unsigned long stack_size) { struct task_struct *tsk = NULL ; struct thread_struct *thd = NULL ; struct page *p = NULL ; color_printk(WHITE, BLACK, "alloc_pages, bitmap:%#018lx\n" , *memory_management_struct.bits_map); p = alloc_pages(ZONE_NOMAL, 1 , PG_PTABLE_MAPED | PG_ACTIVE | PG_KERNEL); color_printk(WHITE, BLACK, "alloc_pages, bitmap:%#018lx\n" , *memory_management_struct.bits_map); tsk = (struct task_struct *)PHY_TO_VIRT(p->phy_address); color_printk(WHITE, BLACK, "struct task_struct address:%#018lx\n" , (unsigned long )tsk); memset (tsk, 0 , sizeof (*tsk)); *tsk = *current; list_init(&tsk->list ); list_add_to_before(&init_task_union.task.list , &tsk->list ); tsk->pid++; tsk->state = TASK_UNINTERRUPTIBLE; thd = (struct thread_struct *)(tsk + 1 ); tsk->thread = thd; memcpy (regs, (void *)((unsigned long )tsk + STACK_SIZE - sizeof (struct pt_regs)), sizeof (struct pt_regs)); thd->rsp0 = (unsigned long )tsk + STACK_SIZE; thd->rip = regs->rip; thd->rsp = (unsigned long )tsk + STACK_SIZE - sizeof (struct pt_regs); if (!(tsk->flags & PF_KTHREAD)) { thd->rip = regs->rip = (unsigned long )ret_from_intr; } tsk->state = TASK_RUNNING; return 0 ; }
do_fork基本实现进程控制结构体的创建以及相关数据的初始化。
kernel_thread_func负责还原进程执行现场、运行进程以及退出进程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 extern void kernel_thread_func (void ) ;__asm__(".global kernel_thread_func; kernel_thread_func: \n\t" " popq %r15 \n\t" " popq %r14 \n\t" " popq %r13 \n\t" " popq %r12 \n\t" " popq %r11 \n\t" " popq %r10 \n\t" " popq %r9 \n\t" " popq %r8 \n\t" " popq %rbx \n\t" " popq %rcx \n\t" " popq %rdx \n\t" " popq %rsi \n\t" " popq %rdi \n\t" " popq %rbp \n\t" " popq %rax \n\t" " movq %rax, %ds \n\t" " popq %rax \n\t" " movq %rax, %es \n\t" " popq %rax \n\t" " addq $0x38, %rsp \n\t" " movq %rdx, %rdi \n\t" " callq *%rbx \n\t" " movq %rax, %rdi \n\t" " callq do_exit \n\t" );
负责还原进程执行现场/运行进程以及退出进程 当处理器执行kernel_thread_func模块时,RSP正指向当前进程的内核层栈顶地址处,此刻栈顶位于栈基地址向下偏移pt_regs结构体处,经过若干个POP,最终将RSP平衡到栈基地址处 进而达到还原进程执行现场的目的,这个执行现场是在kernel_thread中伪造的(通过构造pt_regs结构体,之后传递给do_fork函数),其中的RBX保存着程序执行片段,RDX保存着传入的参数 进程执行现场还原后,将借助CALL执行RBX保存的程序执行片段(init进程),一旦程序片段返回便执行do_exit函数退出进程
1 2 3 4 5 unsigned long do_exit (unsigned long code) { color_printk(RED, BLACK, "exit task is running,arg:%#018lx\n" , code); while (1 ) ; }
do_exit用于释放进程控制结构体,现在只是打印init进程的返回值
总结进程创建过程:
1号进程init 的初始化过程: 新申请一个内存页,复制当前进程的struct task_struct结构,在之上新建struct thread_struct结构;构造一个执行现场放到新进程的栈中,其中rbx执行新进程的执行代码入口,rdx保存传入参数;设置rip,若为内核进程,则指向kernel_thread_func,若为应用层进程,则指向ret_from_intr;当进程切换完成后,执行rip指向的位置的代码;开始执行新进程
执行 kernel_thread() 来初始化一个进程
构造一个 struct pt_regs regs
执行 do_fork()
申请一个内存页(2M)
这个页的起始位置设置为 struct task_struct 结构,使用当前进程的task结构初始化这个结构,因此内存结构 mm_struct和当前进程一样
把 task.list添加到 init_task_union.task.list之前
在 struct task_struct 结构之上申请 struct thread_struct 结构
在栈顶处,复制 struct pt_regs regs 结构,并设置rsp指向栈顶。这是一个在 kernel_thread() 中伪造的执行现场,其中rbx保存着执行程序片段的入口地址,rdx保存着传入的参数
若为应用层进程,则进程执行入口 thd->rip 设置为 ret_from_intr 处;若为内核层进程,则设置为 kernel_thread_func 处
进程初始化完成之后,就可以执行switch_to(prev, next)切换进程了
切换到 init 进程后,由于是内核进程,所以执行 kernel_thread_func() ,从栈中恢复执行现场,并通过call *rbx开始执行init进程。执行完毕后,执行do_exit
运行结果 如图所示: