进程创建是操作系统执行程序的需要或者用户或进程要求创建一个新的进程。进程创建首先是在进程表中为进程建立一个
进程控制块PCB,采用fork()系统调用将复制执行进程的PCB块,U区和内存图像到新的进程。 主要内容包括:进程创建原语、fork()系统调用的编程举例和UNIX V6的fork()源码分析。
进程创建时机
进程创建,是指操作系统创建一个新的进程。UNIX系统用fork()系统调用,而windows系统用CreatProcess()。进程创建的时机有:
(1)
系统初始化。系统的调度进程创建
init进程。
(2)执行中的进程调用了fork()系统函数。程序中有fork()函数。
(3)用户登录,用户命令请求创建进程。例如:用户双击一个图标。
(4)一个批处理作业初始化。大型机、高性能计算机用户提交一个课题,则系统建立
作业控制块,在作业调度后在系统内存中创建进程。
进程创建原语
进程借助创建原语实现创建一个新进程。首先为被创建进程在进程表集中区建立一个PCB--UNIX系统,还要为进程创建U区和内存映像,从进程表索取一个空白PCB表目,记录它的下标;然后,把调用者提供的所有参数(见PCB块的内容),操作系统分配给新进程的PID和调用者的PID,就绪状态和CPU记账数据填入该PCB块;最后,把此PCB块分别列置到就绪队列RQ和进程隶属关系族群中。
UNIX系统使用fork()函数创建新进程时,为子进程复制EP进程的内存映像并不是主要目标。这时,若用exec()执行一个新程序,则子进程的正文段将全部更换,而数据段也将更新。
创建原语可描述如下:
Procedurecreate(n,S0,K0,M0,R0,acc)
begin
i:=getinternalname(n);//进程表下标
i.id:=n;i.priority:=K0;//进程PID,进程优先级
i.CPUstate=S0;i.mainstore:=M0;//初始CPU状态,内存地址
i.resources:=R0;i.status:=readys;//资源清单,就绪状态
j:=EP;i.parent:=j;i.progeny:=φ;//父进程是EP进程,子进程空
j.progeny:=i;//进程隶属关系
i.sdata=RQ;insert(RQ,i);//到就绪进程队列排队
continue
end
程序设计方法
系统调用格式
系统调用1:fork
#include
#include
pid_tfork()
返回值:子进程返回0,父进程返回子进程ID,出错返回-1。
功能:创建一个进程。
fork的三个返回值
程序设计
源程序
#include
#include
#include
int main(intargc,char*argv[])
{
intvalue=5;
pid_tpid;
pid=fork();//fork一个子进程
if(pid<0){//返回值小于0
printf(“forkfailed
”);
exit(-1);
}
elseif(pid==0){//子进程
value+=15;
printf(“
childprocesspid=%d”,getpid());
printf(“
value=%d”,value);
exit(0);
}
elseif(pid>0){//EP进程(parentprocess)
value+=5;
pirntf(“
value=%d”,value);
printf(“
value=%d”,value);
exit(0);
}
}
在程序设计中,fork()的应用最重要的是:掌握利用对子进程和EP进程的两个不同返回值的方法。经典方式是使用 if语句,if((pid=fork())==0)是子进程完成任务的编程范围,应编写与子进程功能相关的语句;而elseif(pid>0)则是EP进程(parentprocess)完成任务的语句范围。在以上程序中,两个进程完成的功能相同,然而在大多数情况,它们的功能不同,这也是使用fork()的原因之一。应注意一个函数同时有不同返回值的问题,这是和以前的编程经验不同的地方。
为了避免操作系统崩溃,pid<0是必须考虑的情况,创建进程失败称为异常。在UNIX系统程序中,不能有考虑不到的异常情况,这是编程能力的高低区别。
新创建的子进程和EP进程有各相互独立的数据段,EP进程和子进程对同一个变量所做的任何改变都是独立的,不会放映到另一个进程的存储器中。因此,value在子进程中的值为20,而在EP进程中的值则为10。变量value在不同的进程中有不同的值,充分说明尽管子进程是复制EP进程而来,有相同的正文段,然而,它们的数据段各自独立。EP进程不能访问子进程的数据段,所以它的第一个printf()语句输出的value值为5。
源代码剖析
数据结构
UNIXV6采用
进程控制块PCB和U区管理和表示一个进程控制信息。PCB块使用结构proc表示,称为进程基本控制块,而U区使用结构user表示称为进程扩充控制块。进程控制信息分为两部分的原因是:结构proc常驻内存,管理经常被操作系统内核访问的那部分信息;而结构user管理进程分配的资源,包括打开的文件或目录等信息,有可能被移至外存交换空间。由于操作系统内核只需要当前执行进程的user,因此当某一进程被换出至外存时,对应的U区被移至交换空间。这是早年计算机内存容量紧张造成的。
PCB块原本是进程控制块的统称,但是常用来表示proc,所以今后PCB块都表示进程基本控制块。
一.PCB块
#include
#define NPROC50
structproc
{
charp_stat;//状态。CPU相关
SRUN:可执行。执行和就绪
SIDL:进程生成中。fork()
SSLEEP:高阻塞
SWAIT:低阻塞
SSTOP:trace
SZOME:终止还没回收
charp_flag;//标志。内存相关(是否调到外存)
SSYS:系统进程。procpid=0
init进程是proc,并不是系统进程
SLOAD:在内存中(可执行)
SLOCK:进程上锁不被调出
SSWAP:进程在外存
r5,r6
STRC:跟踪状态
SWTED:在被跟踪时使用
charp_pri;//进程优先级。可变。
charp_sig;//进程接收到的信号,软中断信号。
//记录其它进程发来的信号
charp_uid;//进程所属用户UID。哪一个用户登录。
charp_time;//进程的存在时间。
//给出进程的执行时间和对资源的利用情况。
当刚交换到内存或外存交换区时,p_time=0。
charp_cpu;//CPU累计时间,在CPU上运行的时间。
charp_nice;//用户调整优先级的值
//偏置值nice,固定值。用户的权限。
intp_ttyp://发出进程的终端号,uid所在终端。与信号相关。
intp_pid;//进程PID
intP_ppid;//父进程PID,创建进程的PID。
//当父进程退出,则子进程的父进程可为
init进程。
intp_addr;//数据段的物理地址。进程PPDA和U区的地址
intp_size;//数据段长度。可交换映像大小
intp_wchan;//阻塞原因。
//事件描述符,记录使进程进入阻塞状态的原因。
可为系统资源,例如:内存缓冲区。
int*p_textp;//代码段。源程序编译的可执行文件在text[]中的地址。
}proc[NPROC];
proc[NPROC]数组是进程表,NPROC规定了UNIX操作系统允许拥有的最多进程数。结构proc包含15个成员。PCB块的proc.p_stat和proc.p_flag常组合使用。根据PCB块的描述,它们分为七类:
1.进程标识符proc.p_pid它等于全局变量mpid。mpid的变化范围是0~(215-1)。尽管很长时间不会重复,然而当mpid是最大值是,它将又一次从0开始计数。应注意mpid与NPROC不同。
2.进程状态proc.p_stat和进程标志proc.p_flag
与进程能否被调度在CPU上运行密切相关,因此进程状态又称为进程调度状态。进程的各种调度状态可依据一定的原因和条件变化。一个已存在系统中的进程不断在这些状态中变化。
UNIXV6使用进程调入调出系统,并不是
虚拟内存管理系统。进程调入调出是完整的内存映像调出,包括PPDA。进程调出的标志并不是代码段调出到外存,而是PPDA调出到外存。Perprocessdataarea(PPDA)由进程的U区和内核栈区域构成,在数据段的首部,有1kB长度。
当EP进程生成子进程时,状态proc.p_stat=SIDL,而且当时它不可能被调出内存,所以标志proc.p_flag=SLOCK。因此,=。就绪进程很少被调出内存,因此大多数情况=。
3.CPU运行信息和进程优先数进程优先数动态变化,相关参数有三项。第一项proc.p_pri,值越小优先权越高。它是处理机调度的主要依据。数值变化范围-100~127,进程调度的优先权不能有很丰富的变动。第二项proc.p_cpu反映了进程使用处理机的程度。proc.p_cpu值越大表示进程使用CPU的时间越长,因此被调度的可能性就越小。它是UNIX操作系统计算p_pri的一个主要参考数据。第三项是proc.p_nice计算进程优先数时所用的一个偏置值。在三项中,是用户唯一能设置的一个值。
UNIX操作系统的优先级调度算法见第1.4节。
4.进程的内存映像地址表示进程图像最近一次调入调出后,在内存或外存交换区的时间。这是0#进程在内、外存之间传送继承的一个主要依据。proc.p_addr不仅是数据段,而且是栈区域和PPDA的物理地址。它们同在数据段中,根据APR页表和它们的区域长度,能计算出实际的物理地址。因此,不能说PCB块中没有给出栈区域的物理地址。proc.p_size=数据区域长度+栈区域长度+PPDA长度。根据proc.p_addr,proc.p_textp,proc.p_size能找到进程的内存图像,若进程被调出到外存则proc.p_addr是进程数据段在外存的地址。
7.进程的组织隶属关系用户标识符proc.p_uid保存在一份用户花名册文件中/etc/passwd,每一个合法用户在该文件中都有一个记录,格式为:
loginname:password:uid:gid::loginworkingdir:shell
loginname是用户进入系统时使用的登录名;password是密码形式的用户口令;uid、gid是高级用户或者系统管理员分配给该用户的标识符和所在组号(0~255);最后两项分别是用户工作目录和操作系统提供给用户的命令程序。系统管理员的proc.p_uid=0。
proc.p_ppid是进程的父进程标识符,在进程树中,除了0#进程以外,其它进程都是父进程要求生成的,因此都不是系统进程。
二、U区
#include
structuser
{
intu_rsav;//
进程切换时保存寄存器r5,r6的值(数据段)
intu_fsav;//处理器为PDP-11/40时不用
charu_segflg;//读写文件时使用的标志变量
charu_error;//出错时用来保存错误代码
charu_uid;//实效用户标识符
charu_gid;//实效组
charu_ruid;//实效用户
charu_rgid;//实际组
int*u_procp;//U区对应的PCB块
char*u_base;//读写文件时用于传递参数,起始地址
char*u_count;//读写文件时传递参数,长度
char*u_offset;//读写文件时传递参数,活动偏移量
int*u_cdir;//当前目录对应的数组inode[]的元素
charu_dbuf[DIRSIZ];//namei()
char*u_dirp;
struct{
intu_ino;
charu_name[DIRSIZ];
}u_dent;
int*u_pdir;
intu_uisa;
intu_uisd;
intu_ofile[NOFILE];//进程打开的文件。外部设备是设备文件/dev
intu_arg;
intu_tsize;//代码段长度
intu_dsize;//数据区域长度
intu_ssize;//栈区域长度
intu_sep;
intu_qsav;//处理信号时保存r5,r6当前值
intu_ssav;//进程调出时保存r5,r6当前值
intu_signal[NSIG];
intu_utime;
intu_stime;
intu_cutime;
intu_cstime;
intu_ar0;//系统调用,操作
通用寄存器或PSW时使用
intu_prof;
charu_intflg;
}u;
.globl–u
-u=140000
操作系统通过全局变量u访问执行进程的数据段。全局变量u的地址0140000是八进制数,高3位是110,为6。所以APR页表的第6页面被选择,而低位全部是0,所以u指向操作系统内核空间第6页的起始地址。所以操作系统通过内核空间第6页的起始地址找到执行进程数据段的位置,用proc.p_addr标识。
源程序分析
#include
#includeken.h>
fork()
{
registerstructproc*p1,*p2;
p1=u.u_procp;//执行进程u是全局变量
for(p2=&proc;p2<&proc[NPROC];p2++)//proc[]={NULL,proc[i]}
if(p2->p_stat==NULL)
gotofound
u.u_error=EAGAID;//进程数量超过系统规定
gotoout;
found://若进程表有空白PCB块
if(newproc()){//newproc()向EP进程返回0,向新进程返回1
u.u_ar0[R0]=p1->p_pid;//EP进程进程号
u.u_cstime=0;u.u_cstime=0;//设置CPU时间
u.u_stime=0;
u.u_cutime=0;u.u_cutime=0;
u.u_utime=0;
return;
}
u.u_ar0[R0]=p2->p_pid;
//fork()对EP进程的返回值是新进程的PID,存放在u.u_ar0[R0]
out:
u.u_ar0[R7]=+2;//指向EP进程的下一条指令地址
}
newproc()//创建新进程的函数
{
inta1,a2;
structproc*p;*up;
registerstructproc*rpp;
register*rip,n;
p=NULL;
retry:
mpid++;//mpid的值mpid={0}
if(mpid<0){//若mpid分配结束,则从0开始分配
mpid=0;
gotoretry;
}
for(rpp=&proc;rpp<&proc[NPROC];rpp++)
if(rpp->p_stat==NULL&p=NULL)//p_stat={NULL,pid}
p=rpp;//若有空白PCB块,记录在p中
if(rpp->p_pid==mpid)//若mpid已经分配
gotoretry;//重新分配mpid
}//总结进程表的三种情况proc[]={full,,null*}
If((rpp=p)==NULL)
panic(“noprocs”);
rip=u.u_procp;//赋值新进程的proc。Rip执行进程
up=rip;//保存EP进程的PCB块
rpp->p_stat=SRUN;//新进程的状态SRUN就绪
rpp->p_flag=SLOAD;//新进程在内存中SLOAD
rpp->p_uid=rip->p_uid;//复制EP进程PCB块中的值
rpp->p_ttyp=rip->p_ttyp;
rpp->p_nice=rip->p_nice;
rpp->p_textp=rip->p_textp;//新的进程复制EP进程正文段
rpp->p_pid=mpid;//新进程PID=mpid
rpp->p_ppid=rip->p_pid;//新进程的父进程是EP进程
rpp->p_time=0;//CPU执行时间为0
for(rip=&u.u_ofile;rip<&ofile[NOFILE];rip++)
//新进程复制EP进程分配的系统资源,是特殊文件。
if((rpp=*rip++)!=NULL)
rpp->f_count++;
if((rpp=up->p_textp)!=NULL){//复制共享代码段
rpp->x_count++;//text[]计数器增加1
rpp->x_ccount++;
}
u.u_cdir->i_count++;//目录inode节点计算器增加1
savu(u.u_rsav);//执行
进程切换时,保存EP进程的r5,r6到U区
rpp=p;
u.u_procp=rpp;//执行继承u是新的进程newproc
rip=up;
n=rip->p_size;//复制数据段长度
a1=rip->p_addr;//”原”EP进程数据段地址
rpp->p_size=n;
a2=malloc(coremap,n);//为新进程申请内存区域
if(a2==NULL){//若申请的内存区域为空,a2={null,address}
rip->p_stat=SIDL;//EP进程状态为SIDL:创建进程中
rpp->p.addr=a1;//新进程数据段地址=EP进程数据段地址
savu(u.u_ssav);//新的进程调出到外存时保存r5,r6到U区
xswap(rpp,0,0);//复制新进程的数据段到外存交换空间
rpp->p_flag=|SSWAP;//新进程标志:在外存
rip->p_stat=SRUN;//EP进程状态还原为SRUN
}else{//申请的内存区域不为空,在内存复制数据段
rpp->p_addr=a2;//新进程的数据段地址是申请到的内存地址
while(n--)copyseg(a1++,a2--);//复制EP进程的数据段
}
u.u_procp=rip;//执行进程u是EP进程
return(0);//向fork()返回0
}
注意:newproc通过swtch()切换执行,swtch()返回值为1,newproc获得返回值1。