Lab Week 05

关于进程

  • 在多道程序环境下,允许多个程序并发执行,此时它们将失去封闭性,并具有间断性及不可再现性的特征。为此引入了进程(Process)的概念,以便更好地描述控制程序的并发执行,实现操作系统的并发性和共享性最基本的两个特性)。
  • 为了使参与并发执行的程序(含数据)能独立地运行,必须为之配置一个专门的数据结构,称为进程控制块(Process ControlBlock, PCB)。系统利用PCB来描述进程的基本情况和运行状态,进而控制和管理进程。相应地,由程序段、相关数据段和PCB三部分构成了进程映像(进程实体)。所谓创建进程,实质上是创建进程映像中的PCB;而撤销进程,实质上是撤销进程的PCB。值得注意的是,进程映像是静态的,进程则是动态的。
  • 注意: PCB是进程存在的唯一标志!
  • 从不同的角度,进程可以有不同的定义,比较典型的定义有:
  1. 进程是程序的一次执行过过程。
  2. 进程是一个程序及其数据在处理机上顺序执行时所发生的活动
  3. 进程是具有独立功能的程序在一个数据集合上运行的过程,它是系统进行资源分配和调度的一个独立单位。
  • 引入进程实体的概念后,我们可以把传统操作系统中的进程定义为:“进程是进程实体的运行过程,是系统进行资源分配和调度的一个独立单位。”
  • 相关函数调用说明
childpid = fork();
/*
child duplicates parent’s address space 
fork() returns 2 values: 0 for child pro and childpid for parent pro 
alg.6-1-fork-demo.c
*/
 childpid = vfork(); 
 /* 
 child shares parent’s address space 
 vfork() returns 2 values: 0 for child pro and childpid for parent pro
 */
wait(0);
 /* 
 waiting for children terminated except vforked one 
 */

在这里插入图片描述

实验环境

  • 架构:Intel x86_64 (虚拟机)
  • 操作系统:Ubuntu 20.04
  • 汇编器:gas (GNU Assembler) in AT&T mode
  • 编译器:gcc

实验内容

进程的创建和终止

  • 编译运行课件 Lecture06 示例代码:alg.6-1 ~ alg.6-6
  • 指出示例代码中你认为不合适的地方并加以改进。

实验报告

  • 实验内容的原理性和实现细节的解释,对示例代码的改进,每个系统调用的作用过程和结果,以及必要的运行截图。

验证分析alg.6-1-fork-demo.c

  • 实验截图
    在这里插入图片描述

  • 实验分析

代码分析

  • fork()函数会创建一个新进程,即子进程,这个子进程直接拷贝父进程的数据段、代码段、堆、栈,父子进程并不共享这些存储空间,子进程有独立的地址空间,子进程和父进程一样都启动一个从fork系统调用之后的下一条指令开始执行的线程,父子进程的执行次序是不确定的(异步)。

  • 如果变量childpid小于0,说明在fork()的过程中出错,则进行错误处理,将错误原因"fork()"函数出错输出到标准设备(stderr),并返回EXIT_FAILURE代表异常退出。

  • 如果fork()函数在执行过程中没有出错,则继续进入一个判断变量childpid值的选择分支语句,若变量childpid为0,说明是子进程,执行结果为变量count自增,打印当前进程的进程号(即子进程),变量count的值及其虚拟地址

  • 若变量childpid大于0,说明是父进程,执行结果为直接打印当前进程的进程号(即父进程),变量childpid(子进程ID),变量count的值及其虚拟地址,然后使用sleep(5)让父进程休眠5s,并用wait(0)让父进程等待所有子进程结束后再执行,这样就保证了父进程在子进程结束之后再执行。

  • 跳出这个判断一个进程是子进程还是父进程的分支语句后,无论是子进程还是父进程,程序都会执行一个打印测试点语句Testing point by,打印当前进程的进程号,并返回EXIT_SUCCESS代表正常退出。

实验结果分析

在这里插入图片描述

  • 从上图可以看到,子进程中的变量count与父进程中的变量count具有相同的虚拟地址,但是子进程中的count值与父进程中的count值不同,这是因为fork()得到的子进程具有独立的地址空间,子进程中的count变量被映射到了和父进程不同的物理地址,所以子进程中的count变量的值变为2后,并没有影响父进程中count变量的值仍为1。

  • 从上图可以看到,父进程的进程号为17734,子进程的进程号为17735,Testing point by后面接的数字是当前执行进程的进程号,可以看到,进程号为17735的进程先执行完打印测试点语句,然后进程号为17734的进程再执行打印测试点语句,即先执行的是子进程,子进程执行完后,父进程再执行,由父进程的选择分支语句中的sleep(5)和wait(0)保证了这一执行先后顺序。

验证分析alg.6-2-vfork-demo.c

  • 实验截图
    在这里插入图片描述

  • 实验分析

代码分析

  • vfork()函数会创建一个新进程,即子进程,这个子进程直接共享父进程的数据段,并且将父进程挂起,保证先执行子进程,在调用exec函数或_exit函数之前和父进程数据是共享的,在它调用exec函数或_exit函数之后父进程才会被恢复调度运行。

  • 和fork()函数一样,vfork()函数成功调用一次返回两次,一次在子进程中返回,一次在父进程中返回,返回的两个值,子进程返回0,父进程返回子进程的进程号ID,调用出错返回-1。

  • 接下来是选择分支语句,如果变量childpid小于0,说明在vfork()的过程中出错,则进行错误处理,将错误原因输出到标准设备(stderr),并返回EXIT_FAILURE代表异常退出。

  • 如果vfork()函数在执行过程中没有出错,则继续进入一个判断变量childpid值的选择分支语句,若变量childpid为0,说明是子进程,执行结果为变量count自增,打印当前进程的进程号(即子进程),变量count的值及其虚拟地址,再打印Child taking a nap …,使用sleep(10)语句让子进程休眠10s,此时父进程并没有执行,说明父进程被挂起,等待子进程调用exec或_exit函数后才接着执行,接着打印Child waking up!,最后使用_exit(0)直接退出子进程,同时使父进程恢复执行,跳过了最后的打印测试点Testing point by语句;

  • 若变量childpid大于0,说明是父进程,执行结果为直接打印当前进程的进程号(即父进程),变量childpid(子进程ID),变量count的值及其虚拟地址,然后使用wait(0)让父进程等待所有子进程结束后再执行。

  • 跳出这个判断一个进程是子进程还是父进程的分支语句后,程序会执行一个打印语句,打印当前进程的进程号,并返回EXIT_SUCCESS代表正常退出。

实验结果分析

在这里插入图片描述

  • 子进程中的变量count与父进程中的变量count具有相同的虚拟地址。子进程的count值与父进程的count值相同,它们共享同一存储空间,被映射到同一物理地址。
  • 父进程被挂起,直到vfork的子进程执行了execv函数才继续执行。
  • 子进程在Testing point by语句之前就退出,只有父进程执行了这一打印语句

验证分析alg.6-3-fork-demo-nowait.c

  • 实验截图
    在这里插入图片描述

  • 实验分析

代码分析

  • 使用fork()函数创建一个新的子进程,但不在父进程的选择分支语句中使用wait(0)语句保证父进程在子进程结束后执行,从而观察父进程和fork()创建出的子进程的执行次序。

  • 程序一开头,首先int count = 1声明变量count为1

  • 然后pid_t childpid和childpid = fork()使用fork()函数创建一个新进程,这个新建的子进程直接拷贝父进程的数据段和代码段,返回一个进程号并把这个进程号赋给pid_t进程号类型变量childpid

  • 接下来是选择分支语句,如果变量childpid小于0,说明在fork()的过程中出错,则进行错误处理,将错误原因输出到标准设备(stderr),并返回EXIT_FAILURE代表异常退出。

  • 如果fork()函数在执行过程中没有出错,则继续进入一个判断变量childpid值的选择分支语句,若变量childpid为0,说明是子进程,执行结果为变量count自增,打印当前进程的进程号(即子进程),变量count的值及其虚拟地址,再打印child sleeping …,使用sleep(10)语句让子进程休眠10s,此时因为父进程中没有wait(0)语句,父进程不会等待子进程执行终止再执行,所以父进程会在子进程休眠的这段时间执行,最后打印child waking up!;

  • 若变量childpid大于0,说明是父进程,执行结果为直接打印当前进程的进程号(即父进程),变量childpid(子进程ID),变量count的值及其虚拟地址

  • 跳出这个判断一个进程是子进程还是父进程的分支语句后,无论是子进程还是父进程,程序都会执行一个打印语句,打印当前进程的进程号,并返回EXIT_SUCCESS代表正常退出。

实验结果分析

在这里插入图片描述

  • 输入ps -l后,因为子进程中间休眠了10s,父进程中没有wait(0)语句,已经执行完后终止,剩下一个子进程处于休眠状态,变成孤儿进程,进程号pid=18213。
  • 在什么都没有输入的情况下,进程号为18213的孤儿进程重新工作,打印child waking up!和Testing point by语句,说明终端(bash)和分叉子级是异步工作的。

验证分析alg.6-4-fork-demo-wait.c

  • 实验截图
    在这里插入图片描述

  • 实验分析

代码分析

使用fork()函数创建一个新的子进程,且在父进程的选择分支语句中使用wait(0)语句保证父进程在子进程结束后执行,并将wait(0)的返回值,即最后一个终止的子进程的进程号,赋给一个进程号类型pid_t变量terminatedid,再观察父进程和fork()创建出的子进程的执行次序。

  • 程序一开头,首先int count = 1声明变量count为1

  • 然后pid_t childpid和childpid = fork()使用fork()函数创建一个新进程,这个新建的子进程直接拷贝父进程的数据段和代码段,返回一个进程号并把这个进程号赋给pid_t进程号类型变量childpid

  • 接下来是选择分支语句,如果变量childpid小于0,说明在fork()的过程中出错,则进行错误处理,将错误原因输出到标准设备(stderr),并返回EXIT_FAILURE代表异常退出。

  • 如果fork()函数在执行过程中没有出错,则继续进入一个判断变量childpid值的选择分支语句,若变量childpid为0,说明是子进程,执行结果为变量count自增,打印当前进程的进程号(即子进程),变量count的值及其虚拟地址,再打印child sleeping …,使用sleep(5)语句让子进程休眠5s,此时因为父进程中有wait(0)语句,父进程会等待子进程执行终止再执行,所以父进程不会在子进程休眠的这段时间执行,最后打印child waking up!;

  • 若变量childpid大于0,说明是父进程,执行结果为使用wait(0)函数,使子进程全部执行完后再执行父进程,并将返回值最后一个终止的子进程的进程号赋给一个进程号类型pid变量terminatedid,打印当前进程的进程号(即父进程),变量terminatedid,变量count的值及其虚拟地址

  • 跳出这个判断一个进程是子进程还是父进程的分支语句后,无论是子进程还是父进程,程序都会执行一个打印语句,打印当前进程的进程号,并返回EXIT_SUCCESS代表正常退出。

实验结果分析

在这里插入图片描述

  • 从上图可以看到,父进程等待子进程执行完语句Testing point by,正常返回终止后,wait(0)将最后一个终止的子进程的进程号18227返回给变量terminatedid后,再执行程序,输入ps可以看到,子进程和父进程都结束终止了。

验证分析alg.6-5-0-sleeper.c

  • 实验截图
    在这里插入图片描述

  • 实验分析

代码分析

实验结果分析

  • 这是一个程序开始后,打印当前进程的pid,ppid的值和休眠秒数secnd的值,然后休眠secnd秒后,再继续执行,打印出sleeper wakes up and returns的简单程序。

验证分析alg.6-5-vfork-execv-wait.c

  • 实验截图
    在这里插入图片描述

  • 与课件输出结果不同,通过输入如下指令解决
    gcc -o alg.6-5-0-sleeper alg.6-5-0-sleeper.c生成 alg.6-5-0-sleeper.o文件
    在这里插入图片描述

  • 实验分析

代码分析

fork()函数会创建一个新进程,即子进程,这个子进程直接共享父进程的数据段,并且将父进程挂起,保证先执行子进程,在调用exec函数或_exit函数之前和父进程数据是共享的,在它调用exec函数或_exit函数之后父进程才会被恢复调度运行。如果在vfork函数创建的子进程中使用了exec函数引发了一个新进程,那么这个新进程会继承原来的vfork出的子进程的进程号,新进程的父进程也为原来子进程的父进程,这个新进程并不与父进程共享一片存储空间,而是拥有独立的地址空间,并和父进程异步执行,但父进程中使用了wait(0),所以父进程会等待这个新的子进程执行完后,再执行终止。

实现细节解释:

首先pid_t childpid和childpid = vfork()使用vfork()函数创建一个新进程,这个新建的子进程共享父进程的数据段,返回一个进程号并把这个进程号赋给pid_t进程号类型变量childpid

接下来是选择分支语句,如果变量childpid小于0,说明在vfork()的过程中出错,则进行错误处理,将错误原因输出到标准设备(stderr),并返回EXIT_FAILURE代表异常退出。

如果vfork()函数在执行过程中没有出错,则继续进入一个判断变量childpid值的选择分支语句,若变量childpid为0,说明是子进程,打印当前进程的进程号(即子进程)和语句taking a nap for 2 sencods,使用sleep(2)语句让子进程休眠2s后继续执行,此时因为父进程被挂起,所以父进程并不会执行。

然后声明一个字符型数组,将字符串"./alg.6-5-0-sleeper.o"复制到这个数组中,并获取这个字符串代表的文件路径的文件信息,如果获取文件信息失败,进行错误处理,将错误原因输出到标准设备(stderr),并直接结束进程,否则打印出"./alg.6-5-0-sleeper.o"文件路径名和这个文件sleeper休眠秒数,execv(filename, argv)引发"./alg.6-5-0-sleeper.o"的文件sleeper作为新的子进程执行,同时父进程在execv()这个函数被调用后重新进行,和新的子进程异步执行。

若变量childpid大于0,说明是父进程,执行结果为直接打印当前进程的进程号(即父进程),然后使用wait(0),并把返回值最后一个结束的子进程的进程号赋给整型变量retpid,让父进程等待所有子进程结束后再执行,最后打印retpid。

跳出这个判断一个进程是子进程还是父进程的分支语句后,返回EXIT_SUCCESS代表正常退出。

实验结果分析

在这里插入图片描述

  • 可以看到,在原来子进程休眠结束并调用了execv()函数引发了一个新的子进程sleeper,父进程也恢复执行后,新的子进程和父进程wait(0)之前的语句异步执行,并不能判断执行的先后次序,有时候是父进程先执行完,有时候是sleeper先执行完。
    在这里插入图片描述

  • 从上图可以看出,原来的子进程调用了execv()后产生的新的子进程sleeper继承了原来vfork出的子进程的进程号pid(4201),它们的父进程进程号都为4200,说明新的子进程sleeper的父进程是原来vfork出的子进程的父进程。

在这里插入图片描述

  • 从上图可以看出,父进程在调用execv函数后重新运行,vfork出的子进程终止,sleeper被引发后作为新的子进程和之前的子进程有同一个进程号4201,也具有重复的地址空间,并且返回到父进程时,没有任何堆栈损坏。父进程和新的子进程异步执行。

在这里插入图片描述

  • 无论如何,父进程需要等待它的子进程执行完终止后再执行,不然原来的子进程引发的sleeper进程作为新的子进程可能会变成孤儿进程。

验证分析alg.6-6-vfork-execv-nowait.c

  • 实验截图
    在这里插入图片描述
  • 生成 alg.6-5-0-sleeper.o文件后重新运行代码截图
    在这里插入图片描述
  • 实验分析

代码分析

  • vfork()函数会创建一个新进程,即子进程,这个子进程直接共享父进程的数据段,并且将父进程挂起,保证先执行子进程,在调用exec函数或_exit函数之前和父进程数据是共享的,在它调用exec函数或_exit函数之后父进程才会被恢复调度运行。如果在vfork函数创建的子进程中使用了exec函数引发了一个新进程,那么这个新进程会继承原来的vfork出的子进程的进程号,新进程的父进程也为原来子进程的父进程,这个新进程并不与父进程共享一片存储空间,而是拥有独立的地址空间,并和父进程异步执行,这次父进程中不使用wait(0),那么由于执行次序不确定,所以新的子进程sleeper有可能成为孤儿进程,观察父进程和新的子进程sleeper的执行情况。

  • 首先pid_t childpid和childpid = vfork()使用vfork()函数创建一个新进程,这个新建的子进程共享父进程的数据段,返回一个进程号并把这个进程号赋给pid_t进程号类型变量childpid

  • 接下来是选择分支语句,如果变量childpid小于0,说明在vfork()的过程中出错,则进行错误处理,将错误原因输出到标准设备(stderr),并返回EXIT_FAILURE代表异常退出。

  • 如果vfork()函数在执行过程中没有出错,则继续进入一个判断变量childpid值的选择分支语句,若变量childpid为0,说明是子进程,打印当前进程的进程号(即子进程)和语句taking a nap for 2 sencods,使用sleep(2)语句让子进程休眠2s后继续执行,此时因为父进程被挂起,所以父进程并不会执行。

  • 然后声明一个字符型数组,将字符串"./alg.6-5-0-sleeper.o"复制到这个数组中,并获取这个字符串代表的文件路径的文件信息,如果获取文件信息失败,进行错误处理,将错误原因输出到标准设备(stderr),并直接结束进程,否则打印出"./alg.6-5-0-sleeper.o"文件路径名和sleeper休眠秒数,execv(filename, argv)引发"./alg.6-5-0-sleeper.o"的文件sleeper作为新的子进程执行,同时父进程在execv()这个函数被调用后重新进行,和新的子进程异步执行。

  • 若变量childpid大于0,说明是父进程,执行结果为直接打印当前进程的进程号(即父进程),打印parent calling shell ps并使用系统调用命令system(“ps -l”)直接查看和bash相关的进程,然后使用sleep(1)语句让父进程休眠1s后继续执行,返回EXIT_SUCCESS代表正常退出。

实验结果分析

在这里插入图片描述

  • 从上图可以看到,bash的父进程的进程号pid为3845,start main()的父进程的父进程的进程号ppid也为3835,说明bash是start main()的父进程。
    在这里插入图片描述

  • 而start main()的父进程的进程号pid为5684,从vfork出的子进程中的execv()语句引发的sleeper进程的父进程的父进程的进程号ppid也为5684,说明start main()是sleeper的父进程。
    在这里插入图片描述

  • start main()的父进程的进程号pid和system()父进程的父进程的进程号ppid相同,都为为5684,说明start main()是system()的父进程。
    在这里插入图片描述

  • system()的父进程的进程号pid和system(“ps -l”)父进程的父进程的进程号ppid相同,都为为5686,说明system()是system(“ps -l”)的父进程。
    在这里插入图片描述

  • start main()终止且控制权移交给了bash,从终端再输入"ps -l"
    在这里插入图片描述

  • 可以看到,由于父进程中没有使用wait(0),所以父进程在sleeper进程执行完前就终止了,sleeper成为了孤儿进程,又被进程号为1348的进程所收养作为子进程
    在这里插入图片描述

  • 终端(bash)和sleeper进程异步执行
    在这里插入图片描述

  • 进程号为1348的进程是"systemd"的守护进程(代替了"init")

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值