-
Notifications
You must be signed in to change notification settings - Fork 32
/
3、多进程起点 0 号和 1 号进程.md
99 lines (66 loc) · 4.84 KB
/
3、多进程起点 0 号和 1 号进程.md
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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
# 0号进程
* 多进程图像是操作系统的核心图像,而多进程图像得以不断延续、演变的核心就是创建进程的系统调用 fork()
* fork() 的核心是通过拷贝父进程来创建子进程,这样系统中的所有进程都是从 0 号进程和 1 号进程继承来的,因此这两个进程就显得非常重要。
0号进程信息
* fork() 的基本工作原理是通过拷贝父进程的信息来创建子进程,0 号进程是操作系统中的第一个进程,0 号进程不可能有父进程。所以在创建 0 号进程时需要手动设置进程信息,那么需要设置哪些信息呢?
* 无非还是那几样重要信息:PCB、内核栈、用户栈以及用户程序。
![在这里插入图片描述](https://img-blog.csdnimg.cn/20210121001842483.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70)
直接初始化 PCB 数据结构来产生 0 号进程的 PCB
```c
struct task_struct init_task =
······
{ {0,0}, {0x9f,0xc0fa00}, {0x9f,0xc0f200}, }, //LDT
{0,PAGE_SIZE+(long)&init_task,0x10,0,0,0··· } //tss
······
```
* task_struct 是 Linux 操作系统定义 PCB 的数据结构名称
* PCB 初始化设置中,LDT 部分用来设置进程使用的地址空间,当 0 号进程在用户态执行时就会根据这个表中给出的三个表项找到代码段和数据段(0 项保留未用)
* tss 结构可以让 PCB 找到内核栈,即在这样初始化设置以后,当需要用到进程的内核栈时会将内核栈段寄存器SS 设置为 0x10,将内核段栈顶寄存器 ESP 设置为 PAGE_SIZE+(long)&init_task(PAGE_SIZE = 4K)。
创建 0 号进程的用户栈(即用户栈 SS:ESP)
* 找到 0 号进程要执行的用户态程序(即用户程序 CS:EIP),找到 0 号进程要执行的用户态程序(即用户程序 CS:EIP),然后将这些信息放置到 0 号进程的内核栈中。
* 创建 0 号进程的用户栈很简单,直接用全局数组划分一段内存即可,即 `long user_stack [ PAGE_SIZE»2 ]`
* 操作系统的 system 模块要从物理内存的 0 地址处开始放置,所以编译后的 user_stack 地址也就是这个用户栈在物理内存中的地址。
* 再根据 0 号进程 PCB 设置中 LDT 表中的栈段的段表项设置 0x9f,0xc0f200,栈段的基地址就是 0,所以 0 号进程使用的地址空间也是从物理内存地址 0 处开始的一段空间,和操作系统使用同一段地址空间。
* 将 0 号进程的用户栈信息放在内核栈中(宏展开)
```c
#define move_to_user_mode()
__asm__ (”movl #user_stack+PAGE_SIZE,%%eax”
”pushl $0x17”
”pushl %%eax”
”pushfl”
”pushl $0x0f”
”pushl $1f”
”iret”
”1: movl $0x17,%%eax”
”movw %%ax,%%ds”
”movw %%ax,%%es”
”movw %%ax,%%fs”
”movw %%ax,%%gs”
:::”ax”)
```
* 栈顶开始 5 项内容依次是 0x17、用户栈位置、EFLAGS 寄存器、0x0f、标号 1,正好是内核栈顶中那关键的 5 个信息。
* 这样 0 号进程的用户栈就和内核栈关联在一起了,同时 0 号进程要执行的用户态程序也和内核栈关联了,这个用户态程序就是从标号 1 处开始的程序。
* 执行“iret”指令以后,上面的 5 个重要信息就会分别赋给 CS:EIP、SS:ESP、EFLAGS。
* 现在 0 号进程开始在用户态执行了,使用的就是分配给 0 号进程的用户栈 user_stack,执行的用户态程序就是从标号 1 开始的程序,即首先将 DS、ES 等寄存器设置为 0x17,然后在顺序执行 move_to_user_mode 后面的程序
* 由于现在 CS 被设置为 0x0f,DS 等被设置为 0x17,最后三位都是 111,说明是用户态程序的代码段和数据段,0 号进程的确进入了用户态,所以这个宏才被命名为 move_to_user_mode。
0 号进程接下来会还要做什么呢
* 即看 move_to_user_mode 后有什么
```
main()
{
······
move_to_user_mode();
if (!fork()) { init(); }
for(;;) pause();
}
```
* 0 号进程接下来会执行 if (!fork()) { init(); } for(;;)pause();
* 因为现在 0 号进程工作在用户态,所以可以调用 fork() 系统调用创建 1 号进程。
* 即 1 号进程会执行 init() 函数;而父进程,即 0 号进程要执行一个死循环,即总是通过 pause() 系统调用将自己暂停,让出 CPU 给别的进程。
* 1 号进程在 init() 函数里会执行 execve 系统调用
```c
void init(void)
{ execve(”/bin/sh”,argv_rc,envp_rc); ···
```
* 因此从现在起 1 号进程就变成 shell 进程了。
* shell 的工作就是不断地等待用户敲入命令,并用 fork() 创建一个进程、用exec() 执行用户命令对应的可执行程序。