操作系统实验1 并发程序设计

Posted 上山打老虎D

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了操作系统实验1 并发程序设计相关的知识,希望对你有一定的参考价值。

并发程序设计

一、 实验目的与要求

  通过进程的创建、撤销和运行加深对进程概念和进程并发执行的理解,明确进程与程序之间的区别。
 

二、 实验内容与方法

  1. 掌握在linux中编程编译运行的方法,试验你的第一个helloworld程序。
  2. 学习预备材料和后面的阅读例程,理解函数fork()、execl()、exit()、getpid()和waitpid()的功能和用法
  3. 编写hello-loop.c程序(在helloworld例程基础上加一个死循环)。使用gcc hello-loop.c –o helloworld生成可执行文件hello-loop。并在同一个目录下,通过命令“./hello-loop”执行之。使用top和ps命令查看该进程,记录进程号以及进程状态。(5分) 查看运行着的hello-loop进程对应的/porc/PID/maps,记录其输出,并绘制出进程用户空间的布局图。 指出可执行文件的用户代码和数据所在区间,并告知你的判断依据。(10分)
  4. 使用kill命令终止hello-loop进程。(5分)
  5. 使用fork()创建子进程,形成以下父子关系:(5+10+15分)

  通过检查进程的pid和ppid证明你成功创建相应的父子关系并用pstree验证其关系。注意在执行前后,分别检查/proc/slabinfo中进程控制块PCB的数量是否能反映出你创建的进程数量变化?
  提示使用cat /proc/slabinfo |grep task_struct命令输出的第一个整数.

  1. 编写代码实现孤儿进程,用pstree查看孤儿进程如何从原父进程位置转变为init进程所收养的过程(注意如果是ubuntu你看到的有可能不是被init接收的情况,请如实记录);编写代码创建僵尸进程,用ps j查看其运行状态。(10分)
  2. (选做,可加10分直至加满)
    编写一个代码,使得进程循环处于以下状态5秒钟运行5秒钟阻塞(例如可以使用sleep( )),并使用top或ps命令检测其运行和阻塞两种状态,并截图记录;
     

三、 实验步骤与过程

1 Linux下程序运行与编译

图 1 创建一个C文件
 

打开Linux,在cmd窗口中通过如图 1的指令创建一个c文件。

图 2 C程序代码
 

打开vim编辑器后,在文本框中输入如图 2的代码。

图 3 使用gcc编译c文件

完成代码编写后,使用gcc命令编译代码并运行,运行结果如下。

图 4 代码运行结果
 

至此,我的第一个helloword程序成功编译并执行。
 

2 fork()、execl()、exit()、getpid()与waitpid()的函数用法

2.1 fork(建立一个新的进程)

  • 函数形式:pid_t fork(void);
  • 函数作用:fork()会产生一个新的子进程,其子进程会复制父进程的数据与堆栈空间,并继承父进程的用户代码,组代码,环境变量、已打开的文件代码、工作目录和资源限制等。
  • 返回值:如果fork()成功则在父进程会返回新建立的子进程代码(PID),而在新建立的子进程中则返回0。如果fork 失败则直接返回-1,失败原因存于errno中。

2.2 execl(执行文件)

  • 函数形式:int execl(const char * path,const char * arg,…);
  • 函数作用:execl()用来执行参数path字符串所代表的文件路径,接下来的参数代表执行该文件时传递过去的argv(0)、argv[1]……,最后一个参数必须用空指针(NULL)作结束。
  • 返回值:如果执行成功则函数不会返回,执行失败则直接返回-1,失败原因存于errno中。调用ls命令范例: execl(“/bin/ls”, “/bin/ls”, “-l” , “/etc”, NULL);

2.3 exit(正常结束进程)

  • 函数形式:void exit(int status);
  • 函数作用:exit()用来正常终结目前进程的执行,并把参数status返回给父进程,而进程所有的缓冲区数据会自动写回并关闭未关闭的文件。
  • 返回值:无返回值

2.4 getpid(取得进程识别码)

  • 函数形式:pid_t getpid(void);
  • 函数作用:getpid()用来取得目前进程的进程识别码,许多程序利用取到的此值来建立临时文件,以避免临时文件相同带来的问题。
  • 返回值:目前进程的进程识别码

2.5 waitpid(等待子进程中断或结束)

  • 函数形式:pid_t waitpid(pid_t pid,int * status,int options);
  • 函数作用:waitpid()会暂时停止目前进程的执行,直到有信号来到或子进程结束。如果在调用waitpid()时子进程已经结束,则wait()会立即返回子进程结束状态值。子进程的结束状态值会由参数status返回,而子进程的进程识别码也会一快返回。如果不在意结束状态值,则参数status可以设成NULL。参数pid为欲等待的子进程识别码,其他数值意义如下:
    pid<-1 等待进程组识别码为pid绝对值的任何子进程。
    pid=-1 等待任何子进程,相当于wait()。
    pid=0 等待进程组识别码与目前进程相同的任何子进程。
    pid>0 等待任何子进程识别码为pid的子进程。
  • 返回值:如果执行成功则返回子进程识别码(PID),如果有错误发生则返回-1。失败原因存于errno
     

3 编写并观察hello-loop.c程序

3.1 编译并执行hello-loop.c文件

重新使用vim在c文件中修改成如图 5的代码:

图 5 hello-loop程序

使用gcc进行编译并执行

图 6 使用gcc进行编译并运行
 

运行之后可以看到如图 7的输出,由于代码中包含死循环,程序会一直输出“loop”。

图 7 hellp_loop的输出
 

3.2 使用top观察hello_loop进程

  进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。
  进程的概念主要有两点:第一,进程是一个实体。每一个进程都有它自己的地址空间,一般情况下,包括文本区域(text region)、数据区域(data region)和堆栈(stack region)。文本区域存储处理器执行的代码;数据区域存储变量和进程执行期间使用的动态分配的内存;堆栈区域存储着活动过程调用的指令和本地变量。第二,进程是一个“执行中的程序”。程序是一个没有生命的实体,只有处理器赋予程序生命时(操作系统执行之),它才能成为一个活动的实体,我们称其为进程。
  在操作系统中,与进程匹配的数据结构称为进程控制块(Process Control Block, PCB),系统利用PCB来描述进程的基本情况和活动过程,进而控制和管理进程。这样,由程序段、相关数据段以及PCB三部分构成的实体便是所谓——进程实体(又称进程映像(process image))。通常所说的进程即是进程实体。
  在接下来的实验中,创建进程指的是创建进程实体中的PCB;撤销进程指的是撤销进程的PCB。
  在本节中,将仔细观察我们hello_loop的进程执行情况。首先,我们将使用top命令来观察进程的执行状态。在cmd中输入“top”,如下图所示:

图 8 执行top之后
 

  如图 8,第一行显示了系统的一些状态。从左到右依次为:当前系统时间,系统运行的时间,当前在线用户数,系统在之前1min、5min和15min内cpu的平均负载值。如图 8即表示当前的系统时间为03:23:18,系统已经运行了35分钟,当前有1个用户在线,1min、5min和15min内cpu的平均负载值分别为2.47%,0.82%和0.47%。
  下方的Task行中依次表示各个状态下的线程数量,分别为总进程数,运行中的进程数,休眠进程数,停止进程以及僵尸进程数。如图 8即表示统计周期内共有283个线程,4个线程正在运行。279个正在休眠,没有停止的进程以及僵尸进程。
  接下来是Cpu行,依次表示了用户态下进程、系统态下进程占用cpu时间比,nice值大于0的进程在用户态下占用cpu时间比,cpu处于idle状态、wait状态的时间比,以及处理硬中断、软中断的时间比以及虚拟 CPU 等待实际 CPU 的时间的百分比。如图 8即表示用户态下进程占14.3%,系统态下进程占比82.1%,cpu处于idle状态的时间比为3.6%,其余都为0%。
  接下来为Mem行,该行提供了内存统计信息,包括物理内存总量、空闲内存、已用内存以及用作缓冲区的内存量。如图 8即表示物理内存总量为3906.9MB,空闲内存为2284.1MB,已用内存为910.2MB,用作缓冲区的内存量为712.6buff/cache。
  最后是Swap行,为交换分区统计信息,包括交换空间总量、空闲交换区大小、已用交换区大小以及用作缓存的交换空间大小。如图 8即表示交换空间总量为923.3MB,空闲交换区为923.3MB,已用交换区为0MB,用作缓冲区的内存量为2765.2MB。
  下半部分即为进程信息表,表头从左到右依次为:
 

  • PID:进程号
  • USER:用户名
  • PR:优先级
  • NI:nice值。负值表示高优先级,正值表示低优先级
  • VIRT:进程使用的虚拟内存总量
  • RES:进程使用的、未被换出的物理内存大小,单位kb
  • SHR:共享内存大小,单位kb
  • S 进程状态。其中,D表示不可中断的睡眠状态,R表示运行,S表示睡眠,T表示跟踪/停止,Z表示僵尸进程
  • %CPU:CPU使用率
  • %MEM:进程使用的物理内存百分比
  • TIME+:进程使用的CPU时间总计,分度值为1/100秒
  • COMMAND:执行的命令

  因此如图 8中展示,我们运行的hello_loop的c程序的进程的进程号为2308,用户名为dongyunhao_2019284073,优先级为20,nice值为0,使用的虚拟内存总量为2496kb,使用的、未被换出的物理内存大小为584kb,共享内存大小为516kb,CPU使用率为53.3%,物理内存使用百分比为0%,程序已经运行44.48,秒的CPU时间,程序执行的命令是hello_loop。
此外,由于当前系统中存在很多进程,直接使用top查看所有进程常常不利于观察,因此可以使用-p参数指定要观察进程的编号实现过滤。

图 9 使用-p过滤进程
 

3.3 使用ps观察hello_loop进程

  除了top命令,还可以使用ps命令查看进程的状态。但与top不同的是,ps只能显示某一时刻的信息。输出的进程信息不能实时更新。

图 10 使用ps的j格式显示进程
 

  如图 10显示了hello_loop进程的相关信息。从左到右依次为:

  • PPID:父进程号
  • PID:进程号
  • PGID:进程组ID
  • SID:会话组ID
  • TTY:终端设备
  • TPGID:控制终端进程组ID
  • STAT:进程当前的状态
  • UID:用户ID
  • TIME:进程执行起到现在总CPU占用时间
  • COMMAND:启动命令

  因此如图 10中展示,我们运行的hello_loop的c程序进程的父进程号为2096,进程号为2308,进程组号为2308,会话组号为2096,终端设备是pts/0,控制终端进程组号为2308,进程的运行状态为运行中,发起进程的用户id为1001,进程已经运行了0.17秒的CPU时间,启动进程的命令为“./hello_loop”。

3.4 绘制进程用户空间布局图

  在进程运行过程中,操作系统使用线性结构为运行中的进程存储各种信息,如下图 11列举了操作系统中的线性结构为进程保存的各个信息。

图 11 进程线性空间图
 

  由低地址到高地址依次为保留区,代码段,数据段,BSS段,堆区,内存映射段,栈,命令行参数区和环境变量区。接下来依次介绍:

  • 保留区(Reserved):
      位于虚拟地址空间的最低部分,未赋予物理地址。任何对它的引用都是非法的,用于捕捉使用空指针和小整型值指针引用内存的异常情况。
      它并不是一个单一的内存区域,而是对地址空间中受到操作系统保护而禁止用户进程访问的地址区域的总称。大多数操作系统中,极小的地址通常都是不允许访问的,如NULL。C语言将无效指针赋值为0也是出于这种考虑,因为0地址上正常情况下不会存放有效的可访问数据。
      在32位X86架构的Linux系统中,用户进程可执行程序一般从虚拟地址空间0x08048000开始加载。该加载地址由ELF文件头决定,可通过自定义链接器脚本覆盖链接器默认配置,进而修改加载地址。0x08048000以下的地址空间通常由C动态链接库、动态加载器ld.so和内核VDSO(内核提供的虚拟共享库)等占用。通过使用mmap系统调用,可访问0x08048000以下的地址空间。

  • 代码段(Text Segment):
      代码段也称正文段或文本段,通常用于存放程序执行代码(即CPU执行的机器指令)。一般C语言执行语句都编译成机器代码保存在代码段。通常代码段是可共享的,因此频繁执行的程序只需要在内存中拥有一份拷贝即可。代码段通常属于只读,以防止其他程序意外地修改其指令(对该段的写操作将导致段错误)。
      代码段指令根据程序设计流程依次执行,对于顺序指令,只会执行一次(每个进程);若有反复,则需使用跳转指令;若进行递归,则需要借助栈来实现。
      代码段指令中包括操作码和操作对象(或对象地址引用)。若操作对象是立即数(具体数值),将直接包含在代码中;若是局部数据,将在栈区分配空间,然后引用该数据地址;若位于BSS段和数据段,同样引用该数据地址。

  • 数据段(Data Segment):
      数据段通常用于存放程序中已初始化且初值不为0的全局变量和静态局部变量。数据段属于静态内存分配(静态存储区),可读可写。
      数据段保存在目标文件中(在嵌入式系统里一般固化在镜像文件中),其内容由程序初始化。例如,对于全局变量int gVar = 10,必须在目标文件数据段中保存10这个数据,然后在程序加载时复制到相应的内存。

  • BSS段(Block Started by Symbol Segment):
      BSS段中通常存放未初始化的全局变量和静态局部变量,初始值为0的全局变量和静态局部变量(依赖于编译器实现)和未定义且初值不为0的符号(该初值即common block的大小)。
      C语言中,未显式初始化的静态分配变量被初始化为0(算术类型)或空指针(指针类型)。由于程序加载时,BSS会被操作系统清零,所以未赋初值或初值为0的全局变量都在BSS中。BSS段仅为未初始化的静态分配变量预留位置,在目标文件中并不占据空间,这样可减少目标文件体积。但程序运行时需为变量分配内存空间,故目标文件必须记录所有未初始化的静态分配变量大小总和(通过start_bss和end_bss地址写入机器代码)。当加载器加载程序时,将为BSS段分配的内存初始化为0。

  • 堆区(Heap):
      堆用于存放进程运行时动态分配的内存段,可动态扩张或缩减。堆中内容是匿名的,不能按名字直接访问,只能通过指针间接访问。当进程调用malloc©/new(C++)等函数分配内存时,新分配的内存动态添加到堆上(扩张);当调用free©/delete(C++)等函数释放内存时,被释放的内存从堆中剔除(缩减) 。
      分配的堆内存是经过字节对齐的空间,以适合原子操作。堆管理器通过链表管理每个申请的内存,由于堆申请和释放是无序的,最终会产生内存碎片。堆内存一般由应用程序分配释放,回收的内存可供重新使用。若程序员不释放,程序结束时操作系统可能会自动回收。

  • 内存映射段(Memory Mapping Segment):
      此处,内核将硬盘文件的内容直接映射到内存, 任何应用程序都可通过Linux的mmap()系统调用请求这种映射。内存映射是一种方便高效的文件I/O方式,因而被用于装载动态共享库。用户也可创建匿名内存映射,该映射没有对应的文件, 可用于存放程序数据。在 Linux中,若通过malloc()请求一大块内存,C运行库将创建一个匿名内存映射,而不使用堆内存。”大块”意味着比阈值 MMAP_THRESHOLD还大,缺省为128KB,可通过mallopt()调整。
      该区域用于映射可执行文件用到的动态链接库。在Linux 2.4版本中,若可执行文件依赖共享库,则系统会为这些动态库在从0x40000000开始的地址分配相应空间,并在程序装载时将其载入到该空间。在Linux 2.6内核中,共享库的起始地址被往上移动至更靠近栈区的位置。

  • 栈(Stack):
      栈又称堆栈,由编译器自动分配释放,行为类似数据结构中的栈(先进后出)。堆栈主要有三个用途,为函数内部声明的非静态局部变量提供存储空间,记录函数调用过程相关的维护性信息,称为栈帧或过程活动记录。它包括函数返回地址,不适合装入寄存器的函数参数及一些寄存器值的保存。除递归调用外,堆栈并非必需。因为编译时可获知局部变量,参数和返回地址所需空间,并将其分配于BSS段。
      临时存储区,用于暂存长算术表达式部分计算结果或函数分配的栈内内存。持续地重用栈空间有助于使活跃的栈内存保持在CPU缓存中,从而加速访问。进程中的每个线程都有属于自己的栈。向栈中不断压入数据时,若超出其容量就会耗尽栈对应的内存区域,从而触发一个页错误。

  完成了对各个部分的介绍后,接下来将记录hello_loop进程的用户空间。为了更准确的绘制用户空间布局图,可以使用cat /proc/pid/maps命令查看进程中的空间分布。结果如下:

图 12 用户空间分布
 

  每一行的数据依次代表着本段内存映射的虚拟地址空间范围,权限,本段映射地址在文件中的偏移,所映射的文件所属设备的设备号,文件的索引节点号,所映射的文件名。
  通过观察输出,可得出如下判断:

  • 第一行的地址最低,并且只可读,不可写不可执行,因此可能是ELF头;
  • 第二行可读可执行不可写,因此大概率为代码段
  • 第三第四行只可读,并且夹在第五行(可读可写数据区)与第二行间(代码段),因此为只读数据段
  • 第五行可读可写,因此为数据区
  • 第六行是堆
  • 第七行到第十二行,第十四行到第十八行为内存映射段
  • 第二十行为栈区
  • 第二十一行到第二十三行为系统调用区

综上,可做出如下进程用户空间布局图:

图 13 进程空间布局图
 

4 使用kill命令终止hello_loop进程

  由于hello_loop进程是死循环,我们可以使用kill命令来杀死这个进程,在命令行中输入kill+pid对进程进行杀死。

图 14 杀死hello_loop进程
 

  使用kill之后可以看到原本循环输出“loop”的界面已经停止,并输出了“Terminated”。

图 15 进程被杀死
 

5 使用fork创建子进程

  为了使并发执行的每个程序(含数据)能够独立运行以避免不可再现性(多次运行结果不同),需要对运行中的程序添加标识来供操作系统管理。
  在操作系统中,与此匹配的数据结构称为进程控制块(Process Control Block, PCB),系统利用PCB来描述进程的基本情况和活动过程,进而控制和管理进程。这样,由程序段、相关数据段以及PCB三部分构成的实体便是所谓——进程实体(又称进程映像(process image))。通常所说的进程即是进程实体。
  在接下来的实验中,创建进程指的是创建进程实体中的PCB;撤销进程指的是撤销进程的PCB。

5.1 创建1层50个子进程

使用fork创建如下图的一层50个子进程。

图 16 1层50个子进程

编写代码如下图:

图 17 1层50个子进程代码图
 

  由于生成子进程的时候,是将父进程现有的数据复制一份给子进程,让子进程从生成位置开始继续运行。故每一个新生成的子进程,都拥有变量father_pid且该值等于parent process的PID,那么,在循环中,也就只有父进程会进行fork。
  我们可以通过进程树等方式来验证我们的结果,首先,通过“pstree”查看进程树,可以看到如下输出:

图 18 1层50个子进程的进程树
 

  如图 18,我们的进程成功创建了50个子进程。此外,我们还可以通过对进程进行计数,完成对结果的验证,我们对进程中含有“fork1”的进程进行计数,结果如图 19。

图 19 1层50个子进程的进程数
 

  可以看到,一共有51个进程,其中,有一个为父进程,则剩下的50个为子进程。
  最后,我们也可以通过查看进程信息,来观察进程间的关系,来验证我们的实验结果,具体如图 20:

图 20 1层50个子进程的进程间关系
 

  可以看到,第一个进程为父进程,进程号为2972,其余子进程有各自的进程号,但其父进程都为2972。因此,我们使用了三个方法验证了我们的实验。

5.2 创建50层1个子进程

使用fork创建如下图的50层1个子进程。

图 21 50层1个子进程
 

编写代码如下图:

图 22 50层1个子进程代码图
 

  第一个进程首先将initial_id设置为自己本身的PID值,由于fork()的内容皆在初始化initial_id之后,故每个子进程栈里的𝑖𝑛𝑖𝑡𝑖𝑎𝑙_𝑝𝑖𝑑都将是初始父进程的PID。接着在for循环中改变深度,每轮循环的时候,验证

If(initial_pid+i==getpid())

  若是则生成新的子进程,然后将fork返回值赋予pid,用于使父进程退出for循环。
  我们可以通过进程树等方式来验证我们的结果,首先,通过“pstree”查看进程树,可以看到如下输出:

图 23 50层1个子进程的进程树
 

  如图 23我们的进程成功创建了50个子进程。但由于显示有限,不能全部显示所有的进程。我们将通过对进程进行计数,进一步完成对结果的验证,我们对进程中含有“fork2”的进程进行计数,结果如图 24。

图 24 50层1个子进程的进程数
 

  可以看到,一共有51个进程,其中,有一个为父进程,则剩下的50个为子进程。
  最后,我们也可以通过查看进程信息,来观察进程间的关系,来验证我们的实验结果,具体如下:

图 25 50层1个进程的进程间关系
 

  可以看到,除第一个父进程外,每一个进程都是上一个进程的子进程,除最后一个进程外,每一个进程都是下一个进程的父进程。因此,我们验证了我们的实验。

5.3 创建6层完全二叉树式进程

使用fork创建如下图的6层完全二叉树式子进程。

图 26 6层完全二叉树式子进程
 

编写代码如下图:

图 27 6层完全二叉树式子进程代码图
 

  思路上顺着上面两个程序的思路,我们充分利用好fork返回给父进程和子进程数值不同的这个特性,确保父进程先后生成子进程2个的时候,第一个生成的子进程不会执行for的后半段,导致生成额外的不被预期的子进程。
  我们可以通过进程树等方式来验证我们的结果,首先,通过“pstree”查看进程树,可以看到如下输出:

图 28 6层完全二叉树式子进程的进程树
 

  如图 28我们的进程成功创建了二叉树式的子进程。但由于显示有限,不能全部显示所有的进程。我们将通过对进程进行计数,进一步完成对结果的验证,我们对进程中含有“fork3”的进程进行计数,结果如图 29。

图 29 6层完全二叉树进程的进程数
 

  由于这是一个6层的完全二叉树,故应有2^7-1=127。因此我们的实验正确。最后,我们也可以通过查看进程信息,来观察进程间的关系,来验证我们的实验结果,具体如下:

图 30 6层完全二叉树的进程间关系
 

  可以看到,第一个父进程节点的进程号为3452,其余进程都呈现完全二叉树的结构。因此,我们验证了我们的实验。

6 孤儿进程与僵尸进程的创建

6.1 孤儿进程的创建与观察

  孤儿进程(Orphan Process)指的是在其父进程执行完成或被终止后仍继续运行的一类进程。在Linux操作系统中,为避免孤儿进程退出时无法释放所占用的资源而僵死,任何孤儿进程产生时都会立即为系统进程init或systemd自动接收为子进程。
  在此,我利用fork设计了如下的代码以实现孤儿进程:

图 31 孤儿进程代码
 

  子进程和父进程都会需要getchar来结束运行,若如预期,则运行程序后,第一次按下回车会结束父进程,子进程成为孤儿进程,并被init进程收养;再次按下回车,子进程死亡。
  通过使用ps,可以观察到如下结果:

图 32 孤儿进程的ps观察
 

  可以发现,进程并没有如期被init收养,而是被一个进程号为967的进程收养了。我们使用top来观察这个进程。

图 33 进程号为967的进程信息
 

  如图 33,可以发现,这个进程为systemd系统进程。通过查看源码:

图 34 systemd源码
 

  如图 34,可以发现,这里使用了一个比较特殊的prctl机制。经查阅Linux文档,可以发现1为prctl的第二个参数,当第二个参数若为非零时,调用进程设置child subreaper属性,此时孤儿进程成会被祖先中距离最近的supreaper进程收养。不会被init收养。避免孤儿进程被systemd收养,从而观测到进程被init收养的情况,可以使用命令行进行操作即可,具体结果如下:

图 35 命令行下的孤儿进程
 

  可以看到,孤儿进程被pid为1的init收养了。

6.2 僵尸进程的创建与观察

  当子进程退出时,没有被父进程以wait ()或者waitpid ()回收,那么子进程变为僵尸进程(zombie process),状态栏会显示defunct。
  正常的进程在死亡之后,4G(32bit下为4G)的进程地址空间会主动释放,但是PCB仍然残留在内核中,等待父进程通过PCB了解它的死亡信息,也就是子进程的status。
  编写代码如下:

图 36 僵尸进程代码
 

  如上,在父进程通过fork生成子进程后,子进程结束而父进程进入15秒睡眠,此时子进程成为僵尸进程,状态为defunct,空留PCB于操作系统中。直到父进程醒来,
  程序在运行10s后将产生僵尸进程,此时使用ps进行观察,结果如下:

图 37 僵尸进程状态
 

  可以看到,在父进程睡眠的10秒内,子进程以僵尸进程身份存在。而在父进程苏醒(并立刻死亡)后,子进程被正确回收。图 37中,第二次执行ps j |grep zombie已经只剩下grep–color=auto zombie了。PCB被正确回收。

7 阻塞与运行状态的观察

  可以编写代码如下所示。其思路上利用time (0)来获取时间,运行过程中一直陷入无限的for循环中,直到时长达到5秒,否则不退出。

图 38 阻塞与运行代码图
 

  将该程序运行,可以观察到如下输出:

图 39 阻塞与运行的输出
 

  为了使用top对该进程进行观察,首先利用ps获取该进程的进程号如下:

图 40 ps获取该进程的进程号
 

  可以发现,该进程的进程号为4270,接下来我们使用top观察该进程。阻塞时的运行情况如下:

图 41 阻塞时运行情况
 

  运行时运行情况如下:

图 42 运行时运行情况如下
 

  可以发现在进程休眠时cpu占用为0,在运行时则有比较高的cpu占用。成功实现了阻塞与运行时的观察。

四、 实验结论或体会

  通过本次实验,我学习了如何在Linux下编译并运行C代码,学习了函数fork()、execl()、exit()、getpid()和waitpid()的功能和用法。我学习了内存中是如何存储进程的相关代码文件,也通过实验了解了进程的创建、撤销和运行加深了我对进程概念和进程并发执行的理解,这使我能够区分进程与程序。
  由于有着比较好的Linux基础,本次实验并没有遇到比较棘手的困难,有不了解的地方也通过询问上网查阅资料以及询问助教得以妥善解决。但是在本次实验中,我学会了如何使用命令行,在之前的学习中我都以为通过Linux的图形化界面调出来的就是命令行。通过这次实验的学习,我明白了,直接调出来的是伪命令行,而实际的命令行需要通过Ctrl+Alt+F3进行切换。

以上是关于操作系统实验1 并发程序设计的主要内容,如果未能解决你的问题,请参考以下文章

实验一 进程管理

2019-2020-1 20175202 20175204 20175216 《信息安全系统设计基础》实验三 并发程序

2019-2020-1 20175227张雪莹《信息安全系统设计基础》实验三 《并发程序》

2018-2019-1 20165311《信息安全系统设计基础》实验三 并发程序

2017-2018-1 20155235 实验三 实时系统 实验内容

2017-2018-1 20155214 《信息安全系统设计基础》实验三 并发程序