0 00:00:00,000 --> 00:00:06,680 1 00:00:06,880 --> 00:00:09,760 下面我们来介绍进程创建 2 00:00:09,800 --> 00:00:11,000 那进程创建呢 3 00:00:11,040 --> 00:00:13,120 是操作系统提供给 4 00:00:13,320 --> 00:00:15,960 用户使用的一个系统调用 5 00:00:16,120 --> 00:00:18,760 完成新进程的创建工作 6 00:00:18,800 --> 00:00:21,400 在不同的操作系统当中呢 7 00:00:21,560 --> 00:00:23,920 进程创建的API呢 8 00:00:23,960 --> 00:00:25,320 或者叫系统调用接口呢 9 00:00:25,360 --> 00:00:26,160 是不一样的 10 00:00:26,200 --> 00:00:27,600 比如说在Windows里头 11 00:00:27,640 --> 00:00:30,880 那它的创建接口叫CreateProcess 12 00:00:30,920 --> 00:00:32,160 而且在这里头呢 13 00:00:32,200 --> 00:00:34,920 它可以有多种不同的参数 14 00:00:34,960 --> 00:00:36,800 比如说在这里创建的时候 15 00:00:36,840 --> 00:00:40,160 关闭所有的文件描述符 16 00:00:40,200 --> 00:00:43,760 在创建的时候指定子进程的环境 17 00:00:43,800 --> 00:00:44,800 运行环境 18 00:00:44,840 --> 00:00:48,360 好这些呢都是在Windows里的做法 19 00:00:48,400 --> 00:00:50,040 而另外一大类呢 20 00:00:50,080 --> 00:00:52,120 是在unix系统里头 21 00:00:52,160 --> 00:00:54,160 也包括我们用到的linux系统 22 00:00:54,200 --> 00:00:56,080 它是采用fork execute 23 00:00:56,120 --> 00:00:57,240 两个系统调用来 24 00:00:57,280 --> 00:00:59,840 完成新进程的创建的 25 00:00:59,880 --> 00:01:01,160 那么在这里头呢 26 00:01:01,200 --> 00:01:05,360 fork完成把一个进程复制成两个进程 27 00:01:05,400 --> 00:01:06,360 这时候呢 28 00:01:06,400 --> 00:01:09,400 它们所执行的程序是一样的 29 00:01:09,440 --> 00:01:10,760 但是在这里头呢 30 00:01:10,800 --> 00:01:12,920 它的变量有一个地方是不一样 31 00:01:12,960 --> 00:01:16,040 就是进程的ID PID 32 00:01:16,080 --> 00:01:17,560 父进程里的是 33 00:01:17,600 --> 00:01:19,640 原来执行那个进程的ID 34 00:01:19,680 --> 00:01:20,760 子进程呢 35 00:01:20,800 --> 00:01:23,280 是分配给它的一个新的ID 36 00:01:23,320 --> 00:01:25,680 好完成这个复制之后 37 00:01:25,720 --> 00:01:28,400 接下来由加载execute 38 00:01:28,440 --> 00:01:30,520 把一个新的程序加载到 39 00:01:30,560 --> 00:01:33,160 内存当中重写当前进程 40 00:01:33,200 --> 00:01:35,480 也就是这里新创建子进程 41 00:01:35,520 --> 00:01:38,040 好这时候呢它的进程ID呢 42 00:01:38,080 --> 00:01:39,360 是没有发生改变的 43 00:01:39,400 --> 00:01:42,240 好等这个系统调用返回的时候 44 00:01:42,280 --> 00:01:45,160 那我们这个就变成是两个进程 45 00:01:45,200 --> 00:01:47,880 并且第二个进程呢我已经变成 46 00:01:47,920 --> 00:01:50,120 一个新的程序在运行了 47 00:01:50,160 --> 00:01:51,200 那这是呢 48 00:01:51,240 --> 00:01:54,080 新进程创建一个基本的做法 49 00:01:54,120 --> 00:01:57,080 那这是一个示 例说明 50 00:01:57,120 --> 00:01:59,960 我如何用fork和execute 51 00:02:00,000 --> 00:02:02,240 来创建一个新的进程 52 00:02:02,280 --> 00:02:04,200 执行一个新的映像 53 00:02:04,240 --> 00:02:06,520 fork在这个地方完成复制 54 00:02:06,560 --> 00:02:08,280 到这个地方回来的时候 55 00:02:08,320 --> 00:02:10,520 这个系统调用返回时 56 00:02:10,560 --> 00:02:11,800 那我们这个系统里呢 57 00:02:11,840 --> 00:02:13,520 就已经有两个进程了 58 00:02:13,560 --> 00:02:15,760 并且这两个进程当前指令指针呢 59 00:02:15,800 --> 00:02:19,680 都指向fork完之后的这一行 60 00:02:19,720 --> 00:02:21,200 好这一行接下来就说 61 00:02:21,240 --> 00:02:23,520 你fork成两个之后同时往后走 62 00:02:23,560 --> 00:02:25,440 一样的执行那有什么意义呢 63 00:02:25,480 --> 00:02:27,120 实际上到这一步的时候呢 64 00:02:27,160 --> 00:02:30,320 父进程和子进程的返回值是不一样的 65 00:02:30,360 --> 00:02:31,680 子进程的返回值是零 66 00:02:31,720 --> 00:02:33,280 好那执行红的这一段 67 00:02:33,320 --> 00:02:35,840 如果父进程返回的是子进程的ID 68 00:02:35,880 --> 00:02:36,920 那它就蹦过这一段 69 00:02:36,960 --> 00:02:38,440 执行后面代码去了 70 00:02:38,480 --> 00:02:40,320 好在这子进程里头呢 71 00:02:40,360 --> 00:02:42,520 我再加一个execute 72 00:02:42,560 --> 00:02:43,240 好这时候我可以 73 00:02:43,280 --> 00:02:44,880 加载任何一个程序 74 00:02:44,920 --> 00:02:46,520 指定它有多少个参数 75 00:02:46,560 --> 00:02:48,440 这些参数分别是什么 76 00:02:48,480 --> 00:02:52,600 这是它的基本的引用模式 77 00:02:52,640 --> 00:02:54,440 好在这个引用模式当中呢 78 00:02:54,480 --> 00:02:56,360 我们说fork是完成了 79 00:02:56,400 --> 00:02:59,320 一个进程的子进程的创建 80 00:02:59,360 --> 00:03:01,800 它的内存是完全复制的 81 00:03:01,840 --> 00:03:04,640 它的CPU的状态是完全复制的 82 00:03:04,680 --> 00:03:05,520 当然在这里呢 83 00:03:05,560 --> 00:03:06,440 所谓的完全复制 84 00:03:06,480 --> 00:03:10,400 是需要有加注解的 85 00:03:10,440 --> 00:03:11,160 那这里呢 86 00:03:11,200 --> 00:03:13,960 会有特殊一些寄存器有变化 87 00:03:14,000 --> 00:03:15,480 那这时候以便于我们区别 88 00:03:15,520 --> 00:03:20,640 到底父进程和子进程的ID的不一致 89 00:03:20,680 --> 00:03:22,320 好这是呢这两个系统调用 90 00:03:22,360 --> 00:03:25,600 这个系统调用它准确的区别 91 00:03:25,640 --> 00:03:27,560 父进程返回子进程ID 92 00:03:27,600 --> 00:03:30,600 子进程返回是零 93 00:03:30,640 --> 00:03:32,600 好然后说我们在这里头 94 00:03:32,640 --> 00:03:34,360 父进程这个fork 95 00:03:34,400 --> 00:03:36,920 在进行子进程创建的时候 96 00:03:36,960 --> 00:03:39,120 它的地址空间的复制 97 00:03:39,160 --> 00:03:41,000 到底是怎么进行的 98 00:03:41,040 --> 00:03:42,280 那我们说复制完了之后 99 00:03:42,320 --> 00:03:43,680 它有一点小区别 100 00:03:43,720 --> 00:03:44,840 那我们下面呢 101 00:03:44,880 --> 00:03:46,800 通过一个图示 来说明这个 102 00:03:46,840 --> 00:03:49,000 地址空间的复制过程 103 00:03:49,040 --> 00:03:51,000 这是一个示例 程序 104 00:03:51,040 --> 00:03:53,720 执行到S1 这一段代码的时候呢 105 00:03:53,760 --> 00:03:56,080 整个进程还是一个 106 00:03:56,120 --> 00:03:57,040 到fork的时候 107 00:03:57,080 --> 00:03:59,000 这一句执行完就变成俩了 108 00:03:59,040 --> 00:04:01,720 好然后这两个往下执行 109 00:04:01,760 --> 00:04:04,520 由于返回值的不一致 110 00:04:04,560 --> 00:04:06,480 子进程会进到这一段代码 111 00:04:06,520 --> 00:04:08,520 父进程会进到下一段代码 112 00:04:08,560 --> 00:04:10,240 好这两段都执行完了 113 00:04:10,280 --> 00:04:12,560 如果说你在后面没有用execute 114 00:04:12,600 --> 00:04:14,320 这时候再往后两个进程 115 00:04:14,360 --> 00:04:16,320 可能直线代码也会一样 116 00:04:16,360 --> 00:04:17,320 但是这时候 117 00:04:17,360 --> 00:04:18,600 由于其中 的变量不一样 118 00:04:18,640 --> 00:04:20,480 它俩的处理 结果也会不一样 119 00:04:20,520 --> 00:04:21,600 好我们看一下 120 00:04:21,640 --> 00:04:24,120 我把这个程序加载到内存 121 00:04:24,160 --> 00:04:26,320 这是我们前面已经讲过的状态 122 00:04:26,360 --> 00:04:27,960 好在这里执行的时候呢 123 00:04:28,000 --> 00:04:29,920 执行到这一句的fork的时候 124 00:04:29,960 --> 00:04:32,320 它做复制 125 00:04:32,360 --> 00:04:34,640 复制一个跟它完全一样的 126 00:04:34,680 --> 00:04:36,080 但是它俩有一个 127 00:04:36,120 --> 00:04:37,880 不一样的就是这个地方 128 00:04:37,920 --> 00:04:39,280 好那在这儿呢 129 00:04:39,320 --> 00:04:40,560 这两个不一样 130 00:04:40,600 --> 00:04:41,560 好不一样之后就会 131 00:04:41,600 --> 00:04:43,640 导致后面的变量不一致 132 00:04:43,680 --> 00:04:44,920 那它的执行内容 133 00:04:44,960 --> 00:04:46,200 也就不一样了 134 00:04:46,240 --> 00:04:50,600 这是呢 fork的地址空间复制的过程 135 00:04:50,640 --> 00:04:51,680 那大家需要注意 136 00:04:51,720 --> 00:04:53,040 这是一个完全的复制 137 00:04:53,080 --> 00:04:55,760 但是有这两个的区别 138 00:04:55,800 --> 00:04:56,640 好这两个区别 139 00:04:56,680 --> 00:05:01,240 就导致后面它可以完全不一样 140 00:05:01,280 --> 00:05:03,560 好然后我们说后面的加载呢 141 00:05:03,600 --> 00:05:05,160 是用一个execute 142 00:05:05,200 --> 00:05:07,280 用一个新的可执行文件 143 00:05:07,320 --> 00:05:09,560 来替代当前的进程 144 00:05:09,600 --> 00:05:11,440 那么在这个替代的过程当中呢 145 00:05:11,480 --> 00:05:14,680 我们看到这是一个例子 146 00:05:14,720 --> 00:05:16,440 红的这一段就是 147 00:05:16,480 --> 00:05:18,400 我们加载新进程的代码 148 00:05:18,440 --> 00:05:22,800 execute指定可执行的程序 149 00:05:22,840 --> 00:05:23,760 有多少个参数 150 00:05:23,800 --> 00:05:25,600 具体参数多少 151 00:05:25,640 --> 00:05:27,520 那在这个执行的过程当中呢 152 00:05:27,560 --> 00:05:30,920 我们看看它在操作系统里头的变化 153 00:05:30,960 --> 00:05:34,080 fork进程创建起来之后 154 00:05:34,120 --> 00:05:35,720 这个程序在执行的时候 155 00:05:35,760 --> 00:05:37,120 操作系统内核里头呢 156 00:05:37,160 --> 00:05:37,920 就给它维护了 157 00:05:37,960 --> 00:05:40,280 一个相应的进程控制块 158 00:05:40,320 --> 00:05:41,520 在这个进程控制块里头呢 159 00:05:41,560 --> 00:05:45,360 有它的相关的ID和相关信息 160 00:05:45,400 --> 00:05:47,720 那执行到fork它会是啥情况 161 00:05:47,760 --> 00:05:51,040 fork执行的时候做了一个复制 162 00:05:51,080 --> 00:05:53,160 复制完了之后给它一个新的ID 163 00:05:53,200 --> 00:05:53,960 这个地方127 164 00:05:54,000 --> 00:05:55,520 这变成128 165 00:05:55,560 --> 00:05:57,400 然后其它都一样 166 00:05:57,440 --> 00:05:58,920 然后接着往下执行 167 00:05:58,960 --> 00:06:00,440 这一句的时候呢 168 00:06:00,480 --> 00:06:01,800 父进程执行到这儿了 169 00:06:01,840 --> 00:06:03,600 因为它不等于零 170 00:06:03,640 --> 00:06:05,240 子进程执行进到这里头 171 00:06:05,280 --> 00:06:08,200 然后执行这一句那是程序加载 172 00:06:08,240 --> 00:06:10,560 那这个加载呢就会导致 173 00:06:10,600 --> 00:06:14,200 把它地址空间里的代码都换掉 174 00:06:14,240 --> 00:06:16,160 然后这个地方呢 175 00:06:16,200 --> 00:06:17,520 加载的这个文件呢 176 00:06:17,560 --> 00:06:18,920 也被换掉 177 00:06:18,960 --> 00:06:20,760 好这样以来两个进程 178 00:06:20,800 --> 00:06:22,440 就可以执行不同东西了 179 00:06:22,480 --> 00:06:24,720 好那这是我们从代码的角度来说 180 00:06:24,760 --> 00:06:28,320 而从完整地址空间来讲呢 181 00:06:28,360 --> 00:06:29,720 这就是我们前面说 182 00:06:29,760 --> 00:06:31,240 这地方的一点变化 183 00:06:31,280 --> 00:06:32,680 我在这里头这是代码 184 00:06:32,720 --> 00:06:34,720 然后上面是堆栈堆 185 00:06:34,760 --> 00:06:37,600 好这是创建时候的情况 186 00:06:37,640 --> 00:06:41,360 然后复制 187 00:06:41,400 --> 00:06:43,360 两个一样 188 00:06:43,400 --> 00:06:45,840 除了这个地方不一样 189 00:06:45,880 --> 00:06:47,560 好那这时候呢我们这个地方 190 00:06:47,600 --> 00:06:49,800 除了代码之外还有数据堆栈 191 00:06:49,840 --> 00:06:51,280 这些都是一样的 192 00:06:51,320 --> 00:06:53,040 然后我的加载什么呢 193 00:06:53,080 --> 00:06:54,640 加载就把整个地址空间 194 00:06:54,680 --> 00:06:57,120 内容全部换掉 195 00:06:57,160 --> 00:06:59,600 除了保持这个ID继续一样 196 00:06:59,640 --> 00:07:02,800 那这是我们这里fork和 197 00:07:02,840 --> 00:07:04,840 execute的过程 198 00:07:04,880 --> 00:07:06,280 那接下来我们用一个 199 00:07:06,320 --> 00:07:08,200 稍微复杂一点的例子 200 00:07:08,240 --> 00:07:10,080 让大家看一看这个 201 00:07:10,120 --> 00:07:11,880 fork它在执行过程当中 202 00:07:11,920 --> 00:07:13,360 这个复制到底是 203 00:07:13,400 --> 00:07:15,040 保持了哪些是地方是一致的 204 00:07:15,080 --> 00:07:18,440 哪些地方是变化的 205 00:07:18,480 --> 00:07:20,120 这是一个例子 206 00:07:20,160 --> 00:07:22,160 在这个例子当中呢有一个循环 207 00:07:22,200 --> 00:07:26,720 我在循环内部呢有一个fork 208 00:07:26,760 --> 00:07:29,320 假定说这个循环循环三次 209 00:07:29,360 --> 00:07:31,000 大家想想一个循环 210 00:07:31,040 --> 00:07:31,840 循环三次 211 00:07:31,880 --> 00:07:33,160 这时候我会 212 00:07:33,200 --> 00:07:35,680 这个fork会被执行几次 213 00:07:35,720 --> 00:07:37,440 对于我们通常循环来说 214 00:07:37,480 --> 00:07:38,280 循环三次 215 00:07:38,320 --> 00:07:40,120 那这里头的fork函数 216 00:07:40,160 --> 00:07:42,200 也会被执行三次 217 00:07:42,240 --> 00:07:44,640 但是在这里头大家需要注意 218 00:07:44,680 --> 00:07:46,000 fork一下的时候 219 00:07:46,040 --> 00:07:47,840 我们整个就复制了一遍 220 00:07:47,880 --> 00:07:49,680 这时候一个进程变成了俩 221 00:07:49,720 --> 00:07:51,160 你再往后复制的时候 222 00:07:51,200 --> 00:07:52,240 就是两个继续复制了 223 00:07:52,280 --> 00:07:54,800 好正是由于这种原因 224 00:07:54,840 --> 00:07:56,880 我们下面来看它到底有几个 225 00:07:56,920 --> 00:07:59,160 好在这个进程里做的事情 226 00:07:59,200 --> 00:08:00,360 比较简单 227 00:08:00,400 --> 00:08:01,480 先上来做一个检查 228 00:08:01,520 --> 00:08:03,080 我这个fork是否成功 229 00:08:03,120 --> 00:08:06,720 不成功那直接就退出了 230 00:08:06,760 --> 00:08:08,280 如果成功那这时候在 231 00:08:08,320 --> 00:08:12,120 子进程里呢我打印一行信息 232 00:08:12,160 --> 00:08:14,480 然后就开始等它的子进程结束 233 00:08:14,520 --> 00:08:17,360 最后整个进程结束 234 00:08:17,400 --> 00:08:19,160 那我们来看一下这个进程 235 00:08:19,200 --> 00:08:20,960 在执行的时候它到底这个 236 00:08:21,000 --> 00:08:24,680 进程的复制的过程是啥样子 237 00:08:24,720 --> 00:08:27,040 好这是最初始的时候 238 00:08:27,080 --> 00:08:30,400 这个进程开始执行的时候状态 239 00:08:30,440 --> 00:08:34,000 假定它的进程ID是1166 240 00:08:34,040 --> 00:08:36,840 好然后执行到循环体里头 241 00:08:36,880 --> 00:08:38,000 第一次fork 242 00:08:38,040 --> 00:08:41,040 fork就只有一个进程它在执行fork 243 00:08:41,080 --> 00:08:43,240 fork完之后这一次变成俩了 244 00:08:43,280 --> 00:08:45,160 那执行完是这个情况 245 00:08:45,200 --> 00:08:46,560 好在子进程里头呢 246 00:08:46,600 --> 00:08:48,320 它有一段代码说是 247 00:08:48,360 --> 00:08:49,840 给出相应的一些提示信息 248 00:08:49,880 --> 00:08:52,680 说自己的ID和父进程的ID 249 00:08:52,720 --> 00:08:54,640 这是自己ID是1167 250 00:08:54,680 --> 00:08:57,120 父进程的ID是1166 251 00:08:57,160 --> 00:09:00,680 好然后进行第二次循环 252 00:09:00,720 --> 00:09:02,200 第二次循环的时候呢 253 00:09:02,240 --> 00:09:03,720 我们这个I实际上 254 00:09:03,760 --> 00:09:05,080 已经复制了两份 255 00:09:05,120 --> 00:09:06,600 在两个地址空间里头 256 00:09:06,640 --> 00:09:07,680 所以它们的修改呢 257 00:09:07,720 --> 00:09:09,720 是分别独立进行的 258 00:09:09,760 --> 00:09:13,840 好那这时候这两个同时fork 259 00:09:13,880 --> 00:09:19,080 那1166和1167分别进行fork 260 00:09:19,120 --> 00:09:23,080 那这时候呢又出了两个新的进程 261 00:09:23,120 --> 00:09:24,880 到现在有几个了 262 00:09:24,920 --> 00:09:26,360 三个新进程 263 00:09:26,400 --> 00:09:28,720 总共有四个进程了 264 00:09:28,760 --> 00:09:30,520 好这个时候新创建出 265 00:09:30,560 --> 00:09:31,840 来这两个进程呢 266 00:09:31,880 --> 00:09:34,480 又各自给出一条提示信息 267 00:09:34,520 --> 00:09:37,800 那这是1168 1170 268 00:09:37,840 --> 00:09:41,800 它的父亲分别是1166和1167 269 00:09:41,840 --> 00:09:44,840 好这时候第二次循环执行结束 270 00:09:44,880 --> 00:09:47,640 然后那这时候我有四个进程 271 00:09:47,680 --> 00:09:49,720 再做第三次循环的时候 272 00:09:49,760 --> 00:09:51,560 这时候有几个再循环 273 00:09:51,600 --> 00:09:54,960 四个好一二三四 274 00:09:55,000 --> 00:09:59,040 它们同时进行fork 275 00:09:59,080 --> 00:10:01,320 那又产生了四个 276 00:10:01,360 --> 00:10:03,360 这新产生的四个呢 277 00:10:03,400 --> 00:10:07,600 又在这儿呢又给出四行的提示 278 00:10:07,640 --> 00:10:08,320 那从这儿看 279 00:10:08,360 --> 00:10:10,280 我们循环第一遍的时候 280 00:10:10,320 --> 00:10:11,400 一个变俩 281 00:10:11,440 --> 00:10:13,280 第二遍 的时候两个变四个 282 00:10:13,320 --> 00:10:17,280 第三遍 的时候四个变八个 283 00:10:17,320 --> 00:10:18,640 那在这个过程当中 284 00:10:18,680 --> 00:10:19,680 我们需要有一点注意 285 00:10:19,720 --> 00:10:20,720 就是在这个地方 286 00:10:20,760 --> 00:10:23,720 如果说按我刚才创建顺序 287 00:10:23,760 --> 00:10:25,640 而我们新的PID的分配呢 288 00:10:25,680 --> 00:10:27,480 是加一来给的 289 00:10:27,520 --> 00:10:28,280 那你说好像你 290 00:10:28,320 --> 00:10:31,360 这个顺序并不是顺序再加 291 00:10:31,400 --> 00:10:32,600 这是为啥呢 292 00:10:32,640 --> 00:10:33,360 实际上就相当于 293 00:10:33,400 --> 00:10:34,880 我们创建的一个新的进程 294 00:10:34,920 --> 00:10:36,320 它是新创建进程呢 295 00:10:36,360 --> 00:10:37,920 放到就绪队列里面 296 00:10:37,960 --> 00:10:39,720 那这时候由于调度算法的影响 297 00:10:39,760 --> 00:10:41,840 所以新创建的这几个进程 298 00:10:41,880 --> 00:10:43,560 并不是像刚才我们说的 299 00:10:43,600 --> 00:10:46,640 严格按照我们刚才说的顺序在执行 300 00:10:46,680 --> 00:10:48,400 第一步 只有一个创建出来 301 00:10:48,440 --> 00:10:50,840 这个1166到1167没有问题 302 00:10:50,880 --> 00:10:52,000 第二次执行的 303 00:10:52,040 --> 00:10:53,760 那如果按照我们刚才说的 304 00:10:53,800 --> 00:10:56,920 那就是父进程1168先算 305 00:10:56,960 --> 00:10:58,560 好它创建了一个新进程 306 00:10:58,600 --> 00:10:59,320 它创建完了之后 307 00:10:59,360 --> 00:11:01,320 又放到就绪队列里头去 308 00:11:01,360 --> 00:11:02,280 好实际上这个时候 309 00:11:02,320 --> 00:11:04,400 这个进程呢它的执行 310 00:11:04,440 --> 00:11:07,160 创建出来还执行之前 311 00:11:07,200 --> 00:11:09,040 实际上已经进到这里的 312 00:11:09,080 --> 00:11:12,440 第三次循环到1169 313 00:11:12,480 --> 00:11:13,120 好那这样的话 314 00:11:13,160 --> 00:11:16,120 创建顺序是它创建第一个 315 00:11:16,160 --> 00:11:18,560 然后它创建第二个 316 00:11:18,600 --> 00:11:21,600 它创建第三个是这个先完 317 00:11:21,640 --> 00:11:24,920 然后是7创建70 71 318 00:11:24,960 --> 00:11:28,280 然后是72 73 319 00:11:28,320 --> 00:11:29,440 这样一个顺序 320 00:11:29,480 --> 00:11:31,000 来执行新的进程了 321 00:11:31,040 --> 00:11:33,160 所以这地方这个输出顺序呢 322 00:11:33,200 --> 00:11:35,760 有可能会有些变化 323 00:11:35,800 --> 00:11:37,000 好到这个地方呢 324 00:11:37,040 --> 00:11:38,800 我们基本上说清楚了 325 00:11:38,840 --> 00:11:41,080 从通常道理来讲fork 326 00:11:41,120 --> 00:11:44,120 它是在怎么创建新的进程 327 00:11:44,160 --> 00:11:47,280 这个创建行为有可能产生的变化 328 00:11:47,320 --> 00:11:48,600 如果说我们在这里头 329 00:11:48,640 --> 00:11:50,440 各个进程里头还有一些变量的话 330 00:11:50,480 --> 00:11:51,680 这个变量的变化 331 00:11:51,720 --> 00:11:52,560 搁到这一起 332 00:11:52,600 --> 00:11:53,400 那这时候呢 333 00:11:53,440 --> 00:11:57,720 你需要想清楚地方就会更多了 334 00:11:57,760 --> 00:12:00,120 好那我们看实际的系统里头 335 00:12:00,160 --> 00:12:01,440 它会是在怎么做 336 00:12:01,480 --> 00:12:03,480 ucore里的fork是在怎么做 337 00:12:03,520 --> 00:12:04,800 fork呢实际上我们就说 338 00:12:04,840 --> 00:12:05,920 要进行准确的复制 339 00:12:05,960 --> 00:12:07,400 并且给它一个新的ID 340 00:12:07,440 --> 00:12:08,920 把它放到就绪队列里头 341 00:12:08,960 --> 00:12:12,720 这是很粗的来说fork所做的功能 342 00:12:12,760 --> 00:12:13,760 具体说起来呢 343 00:12:13,800 --> 00:12:15,240 我们在fork里头呢 344 00:12:15,280 --> 00:12:16,680 我们可以用这样两张图 345 00:12:16,720 --> 00:12:19,440 来说明它的执行情况 346 00:12:19,480 --> 00:12:25,400 左边这一个呢是说我们在系统里头 347 00:12:25,440 --> 00:12:27,680 操作系统内核里头这是这个fork 348 00:12:27,720 --> 00:12:30,880 fork它的实现调用了哪些函数 349 00:12:30,920 --> 00:12:34,560 然后它是在什么情况下 350 00:12:34,600 --> 00:12:38,320 哪些系统调用会产生进行fork 351 00:12:38,360 --> 00:12:40,000 然后右边这个图呢 352 00:12:40,040 --> 00:12:43,560 说明了fork它的内部的实现过程 353 00:12:43,600 --> 00:12:45,840 它在这里头做哪样一些复制 354 00:12:45,880 --> 00:12:51,120 和哪样一些改动 355 00:12:51,160 --> 00:12:53,600 这是do_fork 356 00:12:53,640 --> 00:12:56,320 ucore里实现进程创建的 357 00:12:56,360 --> 00:12:58,040 最主要的函数 358 00:12:58,080 --> 00:12:59,840 在这里它完成的功能是 359 00:12:59,880 --> 00:13:03,640 分配进程控制块数据结构 360 00:13:03,680 --> 00:13:06,280 然后创建它的内核堆栈 361 00:13:06,320 --> 00:13:09,360 设置它的地址空间 362 00:13:09,400 --> 00:13:10,880 那这时候设置地址空间呢 363 00:13:10,920 --> 00:13:13,920 对于创建内核线程来说 364 00:13:13,960 --> 00:13:17,000 它就是共享或者说复制 365 00:13:17,040 --> 00:13:22,200 那创建独立的新的用户进程呢 366 00:13:22,240 --> 00:13:23,800 那它就是复制 367 00:13:23,840 --> 00:13:26,360 然后修改子进程的状态 368 00:13:26,400 --> 00:13:29,320 变成运行状态 369 00:13:29,360 --> 00:13:30,680 那这个函数呢 370 00:13:30,720 --> 00:13:32,200 实际上我们可以看到 371 00:13:32,240 --> 00:13:34,840 它在里头呢做的最主要的工作 372 00:13:34,880 --> 00:13:38,200 我们对于创建来说 373 00:13:38,240 --> 00:13:41,480 可以看到它的调用关系 374 00:13:41,520 --> 00:13:43,880 375 00:13:43,920 --> 00:13:47,000 在这张图当中这是do_fork 376 00:13:47,040 --> 00:13:49,920 我们跟它相关的系统调用呢 377 00:13:49,960 --> 00:13:52,680 有各种平台的fork clone 378 00:13:52,720 --> 00:13:53,520 这些系统调用呢 379 00:13:53,560 --> 00:13:56,040 最后都会转到do_fork 380 00:13:56,080 --> 00:13:57,960 do_fork里做的主要的工作呢 381 00:13:58,000 --> 00:14:00,360 我们从这里可以大致见到 382 00:14:00,400 --> 00:14:04,760 说我进行相应的一些复制 383 00:14:04,800 --> 00:14:06,840 这里头进行的几个复制 384 00:14:06,880 --> 00:14:10,000 我们关心的主要的是copy memory 385 00:14:10,040 --> 00:14:13,840 386 00:14:13,880 --> 00:14:16,160 然后copy thread 387 00:14:16,200 --> 00:14:17,520 这是我们看到的 388 00:14:17,560 --> 00:14:20,440 跟它相关的最主要的数据结构 389 00:14:20,480 --> 00:14:21,920 然后在这里头 390 00:14:21,960 --> 00:14:25,400 在这个函数的实现当中 391 00:14:25,440 --> 00:14:27,240 我们看到它的时间很长 392 00:14:27,280 --> 00:14:32,000 我们转换成它的流程图这样的话 393 00:14:32,040 --> 00:14:35,040 可以比较简洁的看到它的实现 394 00:14:35,080 --> 00:14:38,600 那么在这个do_fork的实现当中 395 00:14:38,640 --> 00:14:40,640 我们看到的最主要的函数呢 396 00:14:40,680 --> 00:14:41,880 是在这里头 397 00:14:41,920 --> 00:14:44,520 这是它的进程控制块的 398 00:14:44,560 --> 00:14:47,160 相关信息的填写 399 00:14:47,200 --> 00:14:49,640 比如说在这里头我们看到 400 00:14:49,680 --> 00:14:51,760 它填的它的父进程 401 00:14:51,800 --> 00:14:55,080 就是当前创建的这个进程在这儿 402 00:14:55,120 --> 00:14:57,560 然后我们还关心一个新进程 403 00:14:57,600 --> 00:14:59,920 在进行一系列的判断之后 404 00:14:59,960 --> 00:15:04,120 我们看到的最主要的一件事情 405 00:15:04,160 --> 00:15:05,000 那在这里呢 406 00:15:05,040 --> 00:15:10,200 我们要去设置它的进程标识 407 00:15:10,240 --> 00:15:11,040 那在这儿呢 408 00:15:11,080 --> 00:15:12,600 我们就是创建了进程之后 409 00:15:12,640 --> 00:15:15,240 给它设置相应的标识 410 00:15:15,280 --> 00:15:16,400 在这个地方 411 00:15:16,440 --> 00:15:18,280 好有了这些之后呢 412 00:15:18,320 --> 00:15:22,800 我们一个进程就算是创建完了 413 00:15:22,840 --> 00:15:27,080 这是关于的fork的ucore实现 414 00:15:27,120 --> 00:15:31,040 好再有了fork的实现的环节之后 415 00:15:31,080 --> 00:15:34,240 我们在系统里创建了哪些进程呢 416 00:15:34,280 --> 00:15:35,960 比如说我系统起来的时候 417 00:15:36,000 --> 00:15:38,000 第一个进程是啥样的 418 00:15:38,040 --> 00:15:39,720 第一个线程是啥样的 419 00:15:39,760 --> 00:15:41,120 好那我们在这儿呢 420 00:15:41,160 --> 00:15:42,920 对这些做一个简要的说明 421 00:15:42,960 --> 00:15:45,120 首先我们要说的是空闲进程 422 00:15:45,160 --> 00:15:47,120 也就是说我们操作系统 423 00:15:47,160 --> 00:15:49,360 它通常情况下执行用户的代码 424 00:15:49,400 --> 00:15:50,960 如果用户没代码 425 00:15:51,000 --> 00:15:52,920 用户的进程都执行完了 426 00:15:52,960 --> 00:15:55,120 系统没有新的任务要执行了 427 00:15:55,160 --> 00:15:57,320 这时候系统处于什么状态 428 00:15:57,360 --> 00:15:59,440 那我们说处于暂停的状态 429 00:15:59,480 --> 00:16:00,800 但实际上这时候暂停 430 00:16:00,840 --> 00:16:02,960 CPU并没有完全停下来 431 00:16:03,000 --> 00:16:04,520 它还是在执行指令 432 00:16:04,560 --> 00:16:06,040 这执行是哪的指令呢 433 00:16:06,080 --> 00:16:07,160 通常情况下我们就是 434 00:16:07,200 --> 00:16:10,240 让它来执行空闲进程的处理 435 00:16:10,280 --> 00:16:12,640 好空闲进程的创建是怎么来做呢 436 00:16:12,680 --> 00:16:17,400 它是在我们的初始化文件里头 437 00:16:17,440 --> 00:16:19,240 那proc_init 438 00:16:19,280 --> 00:16:21,400 在这个函数里进行的 439 00:16:21,440 --> 00:16:23,360 那它怎么做呢 440 00:16:23,400 --> 00:16:26,760 它这里呢通过alloc_proc 441 00:16:26,800 --> 00:16:30,600 这个函数来完成相应的创建过程 442 00:16:30,640 --> 00:16:32,520 首先给它分配所需要的资源 443 00:16:32,560 --> 00:16:36,720 比如说这函数会调用kmalloc 444 00:16:36,760 --> 00:16:38,360 分配存储资源 445 00:16:38,400 --> 00:16:40,280 分配完相应的资源之后呢 446 00:16:40,320 --> 00:16:43,040 对它进程控制块进行初始化 447 00:16:43,080 --> 00:16:46,200 填入它的一些相关信息 448 00:16:46,240 --> 00:16:50,240 这是造出来的进程控制块 449 00:16:50,280 --> 00:16:52,240 好然后呢 450 00:16:52,280 --> 00:16:54,720 对这个进程呢 451 00:16:54,760 --> 00:16:57,080 再在这个proc_init进行 452 00:16:57,120 --> 00:17:00,040 完成剩下的部分的初始化 453 00:17:00,080 --> 00:17:01,680 初始化之后放在就绪队列里头 454 00:17:01,720 --> 00:17:03,280 没有其它进程的时候 455 00:17:03,320 --> 00:17:04,760 那它是优先级最低的 456 00:17:04,800 --> 00:17:06,800 然后就让它来执行 457 00:17:06,840 --> 00:17:08,600 这是空闲进程 458 00:17:08,640 --> 00:17:12,760 接下来我们看空闲进程的创建 459 00:17:12,800 --> 00:17:13,960 空闲进程的创建呢 460 00:17:14,000 --> 00:17:19,640 它首先是从kern_init()这个地方 461 00:17:19,680 --> 00:17:22,400 这个函数呢开始内核的初始化 462 00:17:22,440 --> 00:17:24,160 这个我们在前面的实验当中呢 463 00:17:24,200 --> 00:17:26,800 已经多次看过这个函数了 464 00:17:26,840 --> 00:17:29,600 其中有一个proc_init() 465 00:17:29,640 --> 00:17:32,240 这是页表的初始化 466 00:17:32,280 --> 00:17:35,240 我们找到它的定义 467 00:17:35,280 --> 00:17:38,560 那这是它的页表的初始化 468 00:17:38,600 --> 00:17:40,000 好首先在这里呢 469 00:17:40,040 --> 00:17:44,440 它创建的第一个就是idle 470 00:17:44,480 --> 00:17:46,400 那在这儿 471 00:17:46,440 --> 00:17:48,960 然后创建的第二个呢 472 00:17:49,000 --> 00:17:54,520 是用来执行init main这个函数 473 00:17:54,560 --> 00:17:56,240 那我们看在这里头 474 00:17:56,280 --> 00:17:59,760 首先是idle的创建 475 00:17:59,800 --> 00:18:01,360 idle proc的创建 476 00:18:01,400 --> 00:18:05,280 这一行是它最主要的工作 477 00:18:05,320 --> 00:18:09,360 也就用alloc proc这个函数 478 00:18:09,400 --> 00:18:12,720 来创建idle proc的相关信息 479 00:18:12,760 --> 00:18:17,160 那这个函数呢我们对应过来 480 00:18:17,200 --> 00:18:19,080 我们查看它的定义 481 00:18:19,120 --> 00:18:20,360 这时候你会看到 482 00:18:20,400 --> 00:18:22,080 它主要的工作是在这里头 483 00:18:22,120 --> 00:18:24,360 对进程控制块的数据结构 484 00:18:24,400 --> 00:18:27,000 进行初始化的设置 485 00:18:27,040 --> 00:18:28,840 好完成这些设置之后呢 486 00:18:28,880 --> 00:18:31,480 那我们一个进程的初始化呢 487 00:18:31,520 --> 00:18:33,320 就基本上有了 488 00:18:33,360 --> 00:18:38,120 然后这是设置这个进程的状态 489 00:18:38,160 --> 00:18:41,160 另外一个呢是我们的初始化进程 490 00:18:41,200 --> 00:18:42,640 那在这儿呢 491 00:18:42,680 --> 00:18:44,160 用的是kernel_thread 492 00:18:44,200 --> 00:18:47,040 这个创建函数来创建一个线程 493 00:18:47,080 --> 00:18:50,280 执行init_main 494 00:18:50,320 --> 00:18:52,800 作为它的内核函数 495 00:18:52,840 --> 00:18:56,040 那这里头我们看它的实现 496 00:18:56,080 --> 00:19:00,320 我们还是以它的调用图的形式来看 497 00:19:00,360 --> 00:19:02,160 好我们看到在这里头 498 00:19:02,200 --> 00:19:04,400 从我们这个地方initial 499 00:19:04,440 --> 00:19:05,880 过来到这个地方 500 00:19:05,920 --> 00:19:07,800 它在这里最后绕到哪去了 501 00:19:07,840 --> 00:19:09,120 我们从这儿可以看到 502 00:19:09,160 --> 00:19:11,320 最后绕到do_fork上去了 503 00:19:11,360 --> 00:19:12,000 所以在这儿呢 504 00:19:12,040 --> 00:19:15,240 创建内核线程和创建用户态进程 505 00:19:15,280 --> 00:19:17,080 它的最后的实现呢 506 00:19:17,120 --> 00:19:18,920 都在这个do_fork里头 507 00:19:18,960 --> 00:19:21,080 好那么它们的区别在于 508 00:19:21,120 --> 00:19:23,040 复制的时候这个地方的参数 509 00:19:23,080 --> 00:19:24,960 是会不一样的 510 00:19:25,000 --> 00:19:29,600 好我们再来看这是我创建完之后 511 00:19:29,640 --> 00:19:34,320 第一个init_main() 512 00:19:34,360 --> 00:19:37,200 好这是相应的创建的工作 513 00:19:37,240 --> 00:19:39,680 就是我们用户态进程初始化之前 514 00:19:39,720 --> 00:19:41,840 先创建了一个内核线程 515 00:19:41,880 --> 00:19:42,800 这个内核线程呢 516 00:19:42,840 --> 00:19:46,760 就是我们这里的initproc 517 00:19:46,800 --> 00:19:50,560 这个也是在proc_init里头来创建的 518 00:19:50,600 --> 00:19:52,640 在这里头怎么做呢 519 00:19:52,680 --> 00:19:54,440 是通过kernel thread 520 00:19:54,480 --> 00:19:56,720 这个函数来实现的 521 00:19:56,760 --> 00:19:58,240 那我们说kernel thread 522 00:19:58,280 --> 00:20:01,880 也是会调用do fork 523 00:20:01,920 --> 00:20:04,760 和我们线程和进程都用一个 524 00:20:04,800 --> 00:20:06,560 函数在ucore里来实现 525 00:20:06,600 --> 00:20:08,720 但这时候它俩实现的时候 526 00:20:08,760 --> 00:20:10,160 那进程创建和线程创建 527 00:20:10,200 --> 00:20:11,800 我们说不是不一样嘛 528 00:20:11,840 --> 00:20:12,680 实际上这时候不一样 529 00:20:12,720 --> 00:20:14,600 是靠它里参数来区别的 530 00:20:14,640 --> 00:20:15,960 这个过来的时候呢 531 00:20:16,000 --> 00:20:18,520 它的地址空间是共享的 532 00:20:18,560 --> 00:20:19,760 然后在这里头呢 533 00:20:19,800 --> 00:20:22,800 拷贝现场所需要的信息就够了 534 00:20:22,840 --> 00:20:24,240 比如说这里最主要一个 535 00:20:24,280 --> 00:20:27,240 就是创建它的内核堆栈 536 00:20:27,280 --> 00:20:29,440 好分配它相应的资源 537 00:20:29,480 --> 00:20:30,600 然后在这里呢 538 00:20:30,640 --> 00:20:34,160 初始化这个进程的进程控制块 539 00:20:34,200 --> 00:20:37,640 然后在这里呢初始化它的内核堆栈 540 00:20:37,680 --> 00:20:38,920 这地方没有拷贝了 541 00:20:38,960 --> 00:20:40,160 那它的堆栈从哪来 542 00:20:40,200 --> 00:20:42,520 它得新创建一个堆栈 543 00:20:42,560 --> 00:20:46,080 好然后建立起它的内存的共享 544 00:20:46,120 --> 00:20:48,360 也就说它和其它的内核线程 545 00:20:48,400 --> 00:20:51,040 共享同一个内核地址空间 546 00:20:51,080 --> 00:20:54,240 然后把它放到就绪队列当中 547 00:20:54,280 --> 00:20:56,680 然后等到它下一次调度的时候 548 00:20:56,720 --> 00:20:57,960 那调度它开始执行 549 00:20:58,000 --> 00:20:58,840 那我们就可以创建 550 00:20:58,880 --> 00:21:01,440 第一个用户态的进程了 551 00:21:01,480 --> 00:21:05,640 我们刚才说的init proc 552 00:21:05,680 --> 00:21:09,480 这一个函数呢实际上我们也是在 553 00:21:09,520 --> 00:21:15,320 也从这个proc init这个函数开始来看 554 00:21:15,360 --> 00:21:16,760 那到这儿来之后呢 555 00:21:16,800 --> 00:21:18,760 556 00:21:18,800 --> 00:21:21,960 我们有一个附值在这儿 557 00:21:22,000 --> 00:21:24,720 我刚才创建的这个函数到这个地方 558 00:21:24,760 --> 00:21:27,680 它把它这个地方创建了一个新的 559 00:21:27,720 --> 00:21:29,480 它执行的函数是谁 560 00:21:29,520 --> 00:21:30,800 然后它的ID 561 00:21:30,840 --> 00:21:31,840 最后把它标识呢 562 00:21:31,880 --> 00:21:33,720 设置成在这个地方 563 00:21:33,760 --> 00:21:40,920 564 00:21:40,960 --> 00:21:44,880 这一行最后给它附值为init proc 565 00:21:44,920 --> 00:21:47,240 这是我们创建的第一个内核函数 566 00:21:47,280 --> 00:21:52,160 它所执行的函数就是这个init proc 567 00:21:52,200 --> 00:21:53,880 好在这里头呢 568 00:21:53,920 --> 00:21:57,920 它执行的相关的操作是在这里 569 00:21:57,960 --> 00:22:01,760 我们进行了相应的设置 570 00:22:01,800 --> 00:22:03,520 好那在关于创建呢 571 00:22:03,560 --> 00:22:05,840 我们还需要讨论一个问题是 572 00:22:05,880 --> 00:22:08,360 fork的开销 573 00:22:08,400 --> 00:22:10,400 那我们知道fork任务呢 574 00:22:10,440 --> 00:22:13,680 是复制父进程的地址空间 575 00:22:13,720 --> 00:22:16,280 内存和它的寄存器的状态 576 00:22:16,320 --> 00:22:18,280 那这个复制的地址空间呢 577 00:22:18,320 --> 00:22:20,280 通常情况下量是不小的 578 00:22:20,320 --> 00:22:22,640 它的开销是非常大的 579 00:22:22,680 --> 00:22:24,360 那我们开销大呢 580 00:22:24,400 --> 00:22:25,480 我要创建这个东西 581 00:22:25,520 --> 00:22:26,520 我必须干这个事 582 00:22:26,560 --> 00:22:27,880 但实际上我们在这里 583 00:22:27,920 --> 00:22:29,320 有这样一种情况 584 00:22:29,360 --> 00:22:30,800 我们在fork完之后 585 00:22:30,840 --> 00:22:33,360 马上会执行加载 586 00:22:33,400 --> 00:22:34,080 这个加载呢 587 00:22:34,120 --> 00:22:35,720 会把你刚才创建内容 588 00:22:35,760 --> 00:22:37,400 复制内容又给覆盖掉 589 00:22:37,440 --> 00:22:39,560 所以实际上在大多数情况下 590 00:22:39,600 --> 00:22:43,080 你刚才这个复制都是没有必要的 591 00:22:43,120 --> 00:22:44,680 没起到任何的作用 592 00:22:44,720 --> 00:22:45,880 所以在这儿呢 593 00:22:45,920 --> 00:22:46,680 这个开销呢 594 00:22:46,720 --> 00:22:48,280 实际上是我们可以节约的 595 00:22:48,320 --> 00:22:50,400 这种节约呢 596 00:22:50,440 --> 00:22:51,680 就在Windows做法 597 00:22:51,720 --> 00:22:53,160 它通过一个系统调用 598 00:22:53,200 --> 00:22:56,400 用来完成创建和加载 599 00:22:56,440 --> 00:22:58,960 那这时候呢 600 00:22:59,000 --> 00:23:00,480 我们说在Unix里头呢 601 00:23:00,520 --> 00:23:02,800 早的时候也有一种做法叫vfork 602 00:23:02,840 --> 00:23:04,640 它是在创建的时候 603 00:23:04,680 --> 00:23:05,840 不进行复制 604 00:23:05,880 --> 00:23:08,640 等到你在用的时候 605 00:23:08,680 --> 00:23:10,880 那再直接进行加载 606 00:23:10,920 --> 00:23:12,080 那这个复制就省掉了 607 00:23:12,120 --> 00:23:16,320 这种呢就称之为叫轻量级fork 608 00:23:16,360 --> 00:23:18,360 而我们现在的系统里呢 609 00:23:18,400 --> 00:23:20,640 通常都支持写时复制技术 610 00:23:20,680 --> 00:23:21,360 那这样的话 611 00:23:21,400 --> 00:23:23,120 任何一个进程创建的时候 612 00:23:23,160 --> 00:23:25,000 都是在你后面要用的时候 613 00:23:25,040 --> 00:23:29,040 它才延迟过来进行复制 614 00:23:29,080 --> 00:23:29,760 那如果说 615 00:23:29,800 --> 00:23:31,160 你在这里头直接就是覆盖 616 00:23:31,200 --> 00:23:33,320 那这个地方这个复制它就不进行了 617 00:23:33,360 --> 00:23:34,160 所以在这儿呢 618 00:23:34,200 --> 00:23:35,560 我们这个地方这个开销呢 619 00:23:35,600 --> 00:23:37,760 也是可以节约下来的 620 00:23:37,800 --> 00:23:41,640 好这是关于fork的实现 621 00:23:41,720 --> 00:23:42,360 622 00:23:42,400 --> 00:23:42,440