进程管理模块

进程是拥有执行资源的最小单位,它为每个程序维护着运行时的各种资源,如进程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; // 进程状态(运行态/停止态/可中断态)volatile修饰,处理器每次使用这个进程状态前都必须重新读取这个值而不能使用寄存器中的备份值
unsigned long flags; // 进程标志(进程/线程/内核线程)

struct mm_struct *mm; // 内存空间分布结构体,记录内存页表和程序段信息
struct thread_struct *thread; // 进程切换时保留的状态信息

// 0x0000000000000000 - 0x00007fffffffffff user
// 0xffff800000000000 - 0xffffffffffffffff kernel
unsigned long addr_limit; // 进程地址空间范围

long pid; // 进程ID号
long counter; // 进程可用时间片
long signal; // 进程持有的信号
long priority; // 进程优先级
};

成员变量mmthread负责在进程调度过程中保存或还原CR3控制寄存器的页目录基地址和通用寄存器值

Screenshot_20220821_194848

state成员变量使用volatile关键字修饰,说明该变量可能会在意想不到的情况下修改,因此编译器不要对此成员变量进行优化。处理器每次使用这个变量前,必须重新读取该变量的值,而不能使用保存在寄存器的值。

内存空间分布结构体struct mm_struct描述了进程的页表结构和各程序段信息,其中有页目录基地址、代码段、数据段、只读数据段、应用层栈顶地址等信息。

1
2
3
4
5
6
7
8
struct mm_struct {
pml4t_t *pgd; // page table point 内存页表指针,保存CR3控制寄存器值(页目录基地址与页表属性的组合值)
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记录应用程序在应用层的栈顶地址,其他成员变量描述了应用程序的各段地址空间。Screenshot_20220821_195640

Screenshot_20220821_195707

每当进程发生调度切换时,都必须将执行现场的寄存器保存起来,已备再次执行时使用。

这些数据都保存在struct thread_struct结构体内:

1
2
3
4
5
6
7
8
9
10
struct thread_struct {
unsigned long rsp0; // in tss 应用程序在内核层使用的栈基地址
unsigned long rip; // 内核层代码指针(进程切换回来时执行代码的地址)
unsigned long rsp; // 内核层当前栈指针(进程切换时的栈指针值)
unsigned long fs; // FS段寄存器
unsigned long gs; // GS段寄存器
unsigned long cr2; // CR2控制寄存器
unsigned long trap_nr; // 产生异常的异常号
unsigned long error_code; // 异常的错误码
};

其中成员变量rsp0记录应用程序在内核层使用的栈基地址,rsp保存这进程切换时的栈指针值,rip成员保存着进程切换回来时执行代码的地址。

Screenshot_20220821_200012

关于进程的内核层栈空间实现,借鉴Linux内核设计思想,把进程控制结构体struct task_struct与进程的内核层栈空间融为一体。其中,低地址处存放struct task_struct结构体,余下高地址空间作为进程内核层栈空间使用,如下:

Screenshot_20220821_200307

1
2
3
4
union task_union {
struct task_struct task;
unsigned long stack[STACK_SIZE / sizeof(unsigned long)]; // 这个stack数组将占用32KB以至于这个结构体实际上按32KB对齐
} __attribute__((aligned(8))); // 8 bytes align

借助联合体,把进程控制结构体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)};

// 进程控制结构体数组init_task(指针数组)是为各处理器创建的初始进程控制结构体,目前只有数组第0个投入使用,剩余成员将在多核处理器初始化后创建
struct task_struct *init_task[NR_CPUS] = {&init_task_union.task, 0};

struct mm_struct init_mm = {0}; // 由Start_Kernel调用task_init函数填充完整
//系统第一个进程的执行现场信息结构体
struct thread_struct init_thread = {
.rsp0 = (unsigned long)(init_task_union.stack +
STACK_SIZE / sizeof(unsigned long)), //in tss//应用程序在内核层使用的栈基地址
.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对齐,在使用宏currentGET_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
//IA-32e模式下的TSS结构
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 \
}
//各个处理器的TSS结构体数组
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_currentGET_CURRENT均是在当前栈指针寄存器RSP的基础上,按32KB下边界对齐实现的。实现方法是将数值32767(32KB-1)取反,再将取得的结果0xffffffffffff8000与栈指针寄存器RSP的值执行逻辑与计算,结果就是当前进程struct task_struct结构体基地址。(将32KB对齐的地址清零,之后则是task的起始地址)

init进程

进程切换

进程切换示意图:

Screenshot_20220822_220738

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)

RDIRSI寄存器分别保存宏参数prevnext所代表的进程控制块结构体,执行__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;
//首先将next进程的内核层栈基地址设置到TSS结构体中
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);
//保存当前进程的FS和GS段寄存器值
__asm__ __volatile__("movq %%fs, %0 \n\t" : "=a"(prev->thread->fs));
__asm__ __volatile__("movq %%gs, %0 \n\t" : "=a"(prev->thread->gs));
//将next进程保存的FS和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)
  • 把当前rsp1f(%%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; //PCB进程控制块指针

init_mm.pgd = (pml4t_t *)global_cr3; // 补充完系统第一个进程的页表结构和各个段信息的内存空间分布结构体

init_mm.start_code = memory_management_struct.start_code; // 内核代码段开始地址0xffff 8000 0010 0000
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;

// init_thread,init_tss 设置当前TSS为系统第一个进程的TSS
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); //初始化PCB链表,前驱指向自己,后继指向自己
//接下来task_init函数执行kernel_thread函数为系统创建第二个进程(通常称为init进程),对于调用kernel_thread函数传入的CLONE_FS | CLONE_FILES | CLONE_SIGNAL等克隆标志位,目前未实现相应功能,预留使用
kernel_thread(init, 10, CLONE_FS | CLONE_FILES | CLONE_SIGNAL);

init_task_union.task.state = TASK_RUNNING; //本来是不可中断等待状态,现在修改为可运行状态,以接受调度

p = container_of(list_next(&current->list), struct task_struct, list);

//取得init进程控制结构体后调用switch_to切换至init内核线程
switch_to(current, p);
}

_stack_startheader.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
//为操作系统创建进程,需要的参数:程序入口地址(函数指针)/参数/flags
int kernel_thread(unsigned long (*fn)(unsigned long), unsigned long arg,
unsigned long flags) {
struct pt_regs regs; //首先为新进程准备执行现场信息而创建pt_regs结构体
memset(&regs, 0, sizeof(regs)); //全部初始化置0

regs.rbx = (unsigned long)fn; //程序入口地址(函数指针)
regs.rdx = (unsigned long)arg;

regs.ds = KERNEL_DS; //#define KERNEL_DS (0x10)//选择子
regs.es = KERNEL_DS;
regs.cs = KERNEL_CS; //#define KERNEL_CS (0x08)//选择子
regs.ss = KERNEL_DS;
regs.rflags = (1 << 9); //位9是IF位,置1可以响应中断
regs.rip = (unsigned long)kernel_thread_func;
//引导程序(kernel_thread_func模块),这段引导程序会在目标程序(保存于参数fn内)执行前运行
//至此创建完新的执行现场主要信息,次要信息置0
//随后将执行现场数据传递给do_fork函数,来创建进程控制结构体并完成进程运行前的初始化工作,可见do_fork和kernel_thread模块才是创建进程的关键代码
return do_fork(&regs, 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
//目前的do_fork函数已基本实现进程控制结构体的创建以及相关数据的初始化工作,由于内核层尚未实现内存分配功能,内存空间的使用只能暂时以物理页为单位
//同时为了检测alloc_pages的执行效果,在分配物理页的前后打印出物理内存页的位图映射信息
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; //任务控制块PCB
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); //ZONE_NORMAL区域申请一个页面,属性为经过页表映射的页/使用中/内核层的页
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);

// 将当前进程控制结构体中的数据复制到新分配的物理页中(物理页的线性地址起始处),并进一步初始化相关成员变量信息
// 包括链接入进程队列/分配和初始化thread_struct结构体/伪造进程执行现场(把执行现场数据复制到目标进程的内核层栈顶处)
memset(tsk, 0, sizeof(*tsk));
*tsk = *current; //将当前进程控制结构体中的数据复制到新分配的物理页中(物理页的线性地址起始处),*current获得的是当前进程的PCB,以至于新进程的PCB是一份当前进程的克隆

list_init(&tsk->list); //初始化新进程PCB的链域
list_add_to_before(&init_task_union.task.list, &tsk->list); //作为前驱链接入当前进程链域
tsk->pid++; //设置进程id,克隆自当前进程的PCB,所以变成当前进程id号加1
tsk->state = TASK_UNINTERRUPTIBLE; //设置进程状态为未被中断,现在未初始化完成,不可接受调度进入运行

thd = (struct thread_struct *)(tsk + 1); //使用新申请的PCB后续的地址作为执行现场结构体thd的信息存放处
tsk->thread = thd; //将进程控制块中的进程切换时的保留信息结构体指向刚创建的thread_struct结构体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; //指向新的执行现场信息(由kernel_thread构造并传入)中的引导代码
thd->rsp = (unsigned long)tsk + STACK_SIZE - sizeof(struct pt_regs); //内核层当前栈指针(进程切换时的栈指针值),指向修改为指向新进程的regs最后一个元素ss(但是最先弹出),注意顺序问题

if (!(tsk->flags & PF_KTHREAD)) { //最后判断目标进程的PF_KTHREAD标志位以确定目标进程运行在内核层空间还是应用层空间
//如果复位PF_KTHREAD标志位说明进程运行于应用层空间,那么将进程的执行入口点设置在ret_from_intr处
//否则将进程的执行入口点设置在kernel_thread_func地址处
thd->rip = regs->rip = (unsigned long)ret_from_intr;
}
// 在初始化进程控制结构体时,未曾分配mm_struct的存储空间,而依然沿用全局变量init_mm,这是考虑到分配页表是一件无聊的体力活,既然init进程此时还运行在内核层空间,那么在实现内存分配功能前暂且不创建新的页目录和页表
tsk->state = TASK_RUNNING; //当do_fork函数将目标进程设置为可运行态后,在全局链表init_task_union.task.list中已经有两个可运行的进程控制结构体,一旦task_init函数执行switch_to模块, 操作系统便会切换进程,从而使得处理器开始执行init进程,由于init进程运行在内核层空间,因此init进程在执行init函数前会先执行kernel_thread_func模块

//do_fork才是创建进程的核心函数,而kernel_thread更像是对创建出的进程做了特殊限制,这个有kernel_thread函数创建出来的进程看起来更像是一个线程,尽管kernel_thread函数借助do_fork函数
//创建出了进程控制结构体,但是这个进程却没有应用层空间(复制系统第一个进程的PCB),其实kernel_thread只能创建出没有应用层空间的进程,如果有诸多这样的进程同时运行在内核中,看起来就像是内核主进程
//创建出的若干个线程一样,所以叫做内核线程,这段程序参考了Linux1到4各个版本中关于内核线程的函数实现,综上所述,kernel_thread函数的功能是创建内核线程,所以init此时是一个内核级的线程,但不会一直是
//当内核线程执行do_execve函数后会转变为一个用户级进程
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

运行结果

如图所示:

Screenshot_20220822_233247