下面我们来介绍进程创建
那进程创建呢
是操作系统提供给
用户使用的一个系统调用
完成新进程的创建工作
在不同的操作系统当中呢
进程创建的API呢
或者叫系统调用接口呢
是不一样的
比如说在Windows里头
那它的创建接口叫CreateProcess
而且在这里头呢
它可以有多种不同的参数
比如说在这里创建的时候
关闭所有的文件描述符
在创建的时候指定子进程的环境
运行环境
好这些呢都是在Windows里的做法
而另外一大类呢
是在unix系统里头
也包括我们用到的linux系统
它是采用fork execute
两个系统调用来
完成新进程的创建的
那么在这里头呢
fork完成把一个进程复制成两个进程
这时候呢
它们所执行的程序是一样的
但是在这里头呢
它的变量有一个地方是不一样
就是进程的ID PID
父进程里的是
原来执行那个进程的ID
子进程呢
是分配给它的一个新的ID
好完成这个复制之后
接下来由加载execute
把一个新的程序加载到
内存当中重写当前进程
也就是这里新创建子进程
好这时候呢它的进程ID呢
是没有发生改变的
好等这个系统调用返回的时候
那我们这个就变成是两个进程
并且第二个进程呢我已经变成
一个新的程序在运行了
那这是呢
新进程创建一个基本的做法
那这是一个示 例说明
我如何用fork和execute
来创建一个新的进程
执行一个新的映像
fork在这个地方完成复制
到这个地方回来的时候
这个系统调用返回时
那我们这个系统里呢
就已经有两个进程了
并且这两个进程当前指令指针呢
都指向fork完之后的这一行
好这一行接下来就说
你fork成两个之后同时往后走
一样的执行那有什么意义呢
实际上到这一步的时候呢
父进程和子进程的返回值是不一样的
子进程的返回值是零
好那执行红的这一段
如果父进程返回的是子进程的ID
那它就蹦过这一段
执行后面代码去了
好在这子进程里头呢
我再加一个execute
好这时候我可以
加载任何一个程序
指定它有多少个参数
这些参数分别是什么
这是它的基本的引用模式
好在这个引用模式当中呢
我们说fork是完成了
一个进程的子进程的创建
它的内存是完全复制的
它的CPU的状态是完全复制的
当然在这里呢
所谓的完全复制
是需要有加注解的
那这里呢
会有特殊一些寄存器有变化
那这时候以便于我们区别
到底父进程和子进程的ID的不一致
好这是呢这两个系统调用
这个系统调用它准确的区别
父进程返回子进程ID
子进程返回是零
好然后说我们在这里头
父进程这个fork
在进行子进程创建的时候
它的地址空间的复制
到底是怎么进行的
那我们说复制完了之后
它有一点小区别
那我们下面呢
通过一个图示 来说明这个
地址空间的复制过程
这是一个示例 程序
执行到S1 这一段代码的时候呢
整个进程还是一个
到fork的时候
这一句执行完就变成俩了
好然后这两个往下执行
由于返回值的不一致
子进程会进到这一段代码
父进程会进到下一段代码
好这两段都执行完了
如果说你在后面没有用execute
这时候再往后两个进程
可能直线代码也会一样
但是这时候
由于其中 的变量不一样
它俩的处理 结果也会不一样
好我们看一下
我把这个程序加载到内存
这是我们前面已经讲过的状态
好在这里执行的时候呢
执行到这一句的fork的时候
它做复制
复制一个跟它完全一样的
但是它俩有一个
不一样的就是这个地方
好那在这儿呢
这两个不一样
好不一样之后就会
导致后面的变量不一致
那它的执行内容
也就不一样了
这是呢 fork的地址空间复制的过程
那大家需要注意
这是一个完全的复制
但是有这两个的区别
好这两个区别
就导致后面它可以完全不一样
好然后我们说后面的加载呢
是用一个execute
用一个新的可执行文件
来替代当前的进程
那么在这个替代的过程当中呢
我们看到这是一个例子
红的这一段就是
我们加载新进程的代码
execute指定可执行的程序
有多少个参数
具体参数多少
那在这个执行的过程当中呢
我们看看它在操作系统里头的变化
fork进程创建起来之后
这个程序在执行的时候
操作系统内核里头呢
就给它维护了
一个相应的进程控制块
在这个进程控制块里头呢
有它的相关的ID和相关信息
那执行到fork它会是啥情况
fork执行的时候做了一个复制
复制完了之后给它一个新的ID
这个地方127
这变成128
然后其它都一样
然后接着往下执行
这一句的时候呢
父进程执行到这儿了
因为它不等于零
子进程执行进到这里头
然后执行这一句那是程序加载
那这个加载呢就会导致
把它地址空间里的代码都换掉
然后这个地方呢
加载的这个文件呢
也被换掉
好这样以来两个进程
就可以执行不同东西了
好那这是我们从代码的角度来说
而从完整地址空间来讲呢
这就是我们前面说
这地方的一点变化
我在这里头这是代码
然后上面是堆栈堆
好这是创建时候的情况
然后复制
两个一样
除了这个地方不一样
好那这时候呢我们这个地方
除了代码之外还有数据堆栈
这些都是一样的
然后我的加载什么呢
加载就把整个地址空间
内容全部换掉
除了保持这个ID继续一样
那这是我们这里fork和
execute的过程
那接下来我们用一个
稍微复杂一点的例子
让大家看一看这个
fork它在执行过程当中
这个复制到底是
保持了哪些是地方是一致的
哪些地方是变化的
这是一个例子
在这个例子当中呢有一个循环
我在循环内部呢有一个fork
假定说这个循环循环三次
大家想想一个循环
循环三次
这时候我会
这个fork会被执行几次
对于我们通常循环来说
循环三次
那这里头的fork函数
也会被执行三次
但是在这里头大家需要注意
fork一下的时候
我们整个就复制了一遍
这时候一个进程变成了俩
你再往后复制的时候
就是两个继续复制了
好正是由于这种原因
我们下面来看它到底有几个
好在这个进程里做的事情
比较简单
先上来做一个检查
我这个fork是否成功
不成功那直接就退出了
如果成功那这时候在
子进程里呢我打印一行信息
然后就开始等它的子进程结束
最后整个进程结束
那我们来看一下这个进程
在执行的时候它到底这个
进程的复制的过程是啥样子
好这是最初始的时候
这个进程开始执行的时候状态
假定它的进程ID是1166
好然后执行到循环体里头
第一次fork
fork就只有一个进程它在执行fork
fork完之后这一次变成俩了
那执行完是这个情况
好在子进程里头呢
它有一段代码说是
给出相应的一些提示信息
说自己的ID和父进程的ID
这是自己ID是1167
父进程的ID是1166
好然后进行第二次循环
第二次循环的时候呢
我们这个I实际上
已经复制了两份
在两个地址空间里头
所以它们的修改呢
是分别独立进行的
好那这时候这两个同时fork
那1166和1167分别进行fork
那这时候呢又出了两个新的进程
到现在有几个了
三个新进程
总共有四个进程了
好这个时候新创建出
来这两个进程呢
又各自给出一条提示信息
那这是1168 1170
它的父亲分别是1166和1167
好这时候第二次循环执行结束
然后那这时候我有四个进程
再做第三次循环的时候
这时候有几个再循环
四个好一二三四
它们同时进行fork
那又产生了四个
这新产生的四个呢
又在这儿呢又给出四行的提示
那从这儿看
我们循环第一遍的时候
一个变俩
第二遍 的时候两个变四个
第三遍 的时候四个变八个
那在这个过程当中
我们需要有一点注意
就是在这个地方
如果说按我刚才创建顺序
而我们新的PID的分配呢
是加一来给的
那你说好像你
这个顺序并不是顺序再加
这是为啥呢
实际上就相当于
我们创建的一个新的进程
它是新创建进程呢
放到就绪队列里面
那这时候由于调度算法的影响
所以新创建的这几个进程
并不是像刚才我们说的
严格按照我们刚才说的顺序在执行
第一步 只有一个创建出来
这个1166到1167没有问题
第二次执行的
那如果按照我们刚才说的
那就是父进程1168先算
好它创建了一个新进程
它创建完了之后
又放到就绪队列里头去
好实际上这个时候
这个进程呢它的执行
创建出来还执行之前
实际上已经进到这里的
第三次循环到1169
好那这样的话
创建顺序是它创建第一个
然后它创建第二个
它创建第三个是这个先完
然后是7创建70 71
然后是72 73
这样一个顺序
来执行新的进程了
所以这地方这个输出顺序呢
有可能会有些变化
好到这个地方呢
我们基本上说清楚了
从通常道理来讲fork
它是在怎么创建新的进程
这个创建行为有可能产生的变化
如果说我们在这里头
各个进程里头还有一些变量的话
这个变量的变化
搁到这一起
那这时候呢
你需要想清楚地方就会更多了
好那我们看实际的系统里头
它会是在怎么做
ucore里的fork是在怎么做
fork呢实际上我们就说
要进行准确的复制
并且给它一个新的ID
把它放到就绪队列里头
这是很粗的来说fork所做的功能
具体说起来呢
我们在fork里头呢
我们可以用这样两张图
来说明它的执行情况
左边这一个呢是说我们在系统里头
操作系统内核里头这是这个fork
fork它的实现调用了哪些函数
然后它是在什么情况下
哪些系统调用会产生进行fork
然后右边这个图呢
说明了fork它的内部的实现过程
它在这里头做哪样一些复制
和哪样一些改动
这是do_fork
ucore里实现进程创建的
最主要的函数
在这里它完成的功能是
分配进程控制块数据结构
然后创建它的内核堆栈
设置它的地址空间
那这时候设置地址空间呢
对于创建内核线程来说
它就是共享或者说复制
那创建独立的新的用户进程呢
那它就是复制
然后修改子进程的状态
变成运行状态
那这个函数呢
实际上我们可以看到
它在里头呢做的最主要的工作
我们对于创建来说
可以看到它的调用关系
在这张图当中这是do_fork
我们跟它相关的系统调用呢
有各种平台的fork clone
这些系统调用呢
最后都会转到do_fork
do_fork里做的主要的工作呢
我们从这里可以大致见到
说我进行相应的一些复制
这里头进行的几个复制
我们关心的主要的是copy memory
然后copy thread
这是我们看到的
跟它相关的最主要的数据结构
然后在这里头
在这个函数的实现当中
我们看到它的时间很长
我们转换成它的流程图这样的话
可以比较简洁的看到它的实现
那么在这个do_fork的实现当中
我们看到的最主要的函数呢
是在这里头
这是它的进程控制块的
相关信息的填写
比如说在这里头我们看到
它填的它的父进程
就是当前创建的这个进程在这儿
然后我们还关心一个新进程
在进行一系列的判断之后
我们看到的最主要的一件事情
那在这里呢
我们要去设置它的进程标识
那在这儿呢
我们就是创建了进程之后
给它设置相应的标识
在这个地方
好有了这些之后呢
我们一个进程就算是创建完了
这是关于的fork的ucore实现
好再有了fork的实现的环节之后
我们在系统里创建了哪些进程呢
比如说我系统起来的时候
第一个进程是啥样的
第一个线程是啥样的
好那我们在这儿呢
对这些做一个简要的说明
首先我们要说的是空闲进程
也就是说我们操作系统
它通常情况下执行用户的代码
如果用户没代码
用户的进程都执行完了
系统没有新的任务要执行了
这时候系统处于什么状态
那我们说处于暂停的状态
但实际上这时候暂停
CPU并没有完全停下来
它还是在执行指令
这执行是哪的指令呢
通常情况下我们就是
让它来执行空闲进程的处理
好空闲进程的创建是怎么来做呢
它是在我们的初始化文件里头
那proc_init
在这个函数里进行的
那它怎么做呢
它这里呢通过alloc_proc
这个函数来完成相应的创建过程
首先给它分配所需要的资源
比如说这函数会调用kmalloc
分配存储资源
分配完相应的资源之后呢
对它进程控制块进行初始化
填入它的一些相关信息
这是造出来的进程控制块
好然后呢
对这个进程呢
再在这个proc_init进行
完成剩下的部分的初始化
初始化之后放在就绪队列里头
没有其它进程的时候
那它是优先级最低的
然后就让它来执行
这是空闲进程
接下来我们看空闲进程的创建
空闲进程的创建呢
它首先是从kern_init()这个地方
这个函数呢开始内核的初始化
这个我们在前面的实验当中呢
已经多次看过这个函数了
其中有一个proc_init()
这是页表的初始化
我们找到它的定义
那这是它的页表的初始化
好首先在这里呢
它创建的第一个就是idle
那在这儿
然后创建的第二个呢
是用来执行init main这个函数
那我们看在这里头
首先是idle的创建
idle proc的创建
这一行是它最主要的工作
也就用alloc proc这个函数
来创建idle proc的相关信息
那这个函数呢我们对应过来
我们查看它的定义
这时候你会看到
它主要的工作是在这里头
对进程控制块的数据结构
进行初始化的设置
好完成这些设置之后呢
那我们一个进程的初始化呢
就基本上有了
然后这是设置这个进程的状态
另外一个呢是我们的初始化进程
那在这儿呢
用的是kernel_thread
这个创建函数来创建一个线程
执行init_main
作为它的内核函数
那这里头我们看它的实现
我们还是以它的调用图的形式来看
好我们看到在这里头
从我们这个地方initial
过来到这个地方
它在这里最后绕到哪去了
我们从这儿可以看到
最后绕到do_fork上去了
所以在这儿呢
创建内核线程和创建用户态进程
它的最后的实现呢
都在这个do_fork里头
好那么它们的区别在于
复制的时候这个地方的参数
是会不一样的
好我们再来看这是我创建完之后
第一个init_main()
好这是相应的创建的工作
就是我们用户态进程初始化之前
先创建了一个内核线程
这个内核线程呢
就是我们这里的initproc
这个也是在proc_init里头来创建的
在这里头怎么做呢
是通过kernel thread
这个函数来实现的
那我们说kernel thread
也是会调用do fork
和我们线程和进程都用一个
函数在ucore里来实现
但这时候它俩实现的时候
那进程创建和线程创建
我们说不是不一样嘛
实际上这时候不一样
是靠它里参数来区别的
这个过来的时候呢
它的地址空间是共享的
然后在这里头呢
拷贝现场所需要的信息就够了
比如说这里最主要一个
就是创建它的内核堆栈
好分配它相应的资源
然后在这里呢
初始化这个进程的进程控制块
然后在这里呢初始化它的内核堆栈
这地方没有拷贝了
那它的堆栈从哪来
它得新创建一个堆栈
好然后建立起它的内存的共享
也就说它和其它的内核线程
共享同一个内核地址空间
然后把它放到就绪队列当中
然后等到它下一次调度的时候
那调度它开始执行
那我们就可以创建
第一个用户态的进程了
我们刚才说的init proc
这一个函数呢实际上我们也是在
也从这个proc init这个函数开始来看
那到这儿来之后呢
我们有一个附值在这儿
我刚才创建的这个函数到这个地方
它把它这个地方创建了一个新的
它执行的函数是谁
然后它的ID
最后把它标识呢
设置成在这个地方
这一行最后给它附值为init proc
这是我们创建的第一个内核函数
它所执行的函数就是这个init proc
好在这里头呢
它执行的相关的操作是在这里
我们进行了相应的设置
好那在关于创建呢
我们还需要讨论一个问题是
fork的开销
那我们知道fork任务呢
是复制父进程的地址空间
内存和它的寄存器的状态
那这个复制的地址空间呢
通常情况下量是不小的
它的开销是非常大的
那我们开销大呢
我要创建这个东西
我必须干这个事
但实际上我们在这里
有这样一种情况
我们在fork完之后
马上会执行加载
这个加载呢
会把你刚才创建内容
复制内容又给覆盖掉
所以实际上在大多数情况下
你刚才这个复制都是没有必要的
没起到任何的作用
所以在这儿呢
这个开销呢
实际上是我们可以节约的
这种节约呢
就在Windows做法
它通过一个系统调用
用来完成创建和加载
那这时候呢
我们说在Unix里头呢
早的时候也有一种做法叫vfork
它是在创建的时候
不进行复制
等到你在用的时候
那再直接进行加载
那这个复制就省掉了
这种呢就称之为叫轻量级fork
而我们现在的系统里呢
通常都支持写时复制技术
那这样的话
任何一个进程创建的时候
都是在你后面要用的时候
它才延迟过来进行复制
那如果说
你在这里头直接就是覆盖
那这个地方这个复制它就不进行了
所以在这儿呢
我们这个地方这个开销呢
也是可以节约下来的
好这是关于fork的实现