实验二进程管理实验目的通过进程的创建、撤销和运行加深对进程概念和进程并发执行的理解,明确进程与程序的区别。实验内容1、了解系统调用fork()、exec()、exit()和waitpid()的功能和实现过程。2、编写一段程序,使用系统调用fork()创建两个子进程。当此程序运行时,在系统中有一个父进程和两个子进程活动。让每一个进程在屏幕上显示一个字符:父进程显示字符“a”;子进程分别显示字符“b”和“c”。试观察记录屏幕上的显示结果,并分析原因。3、编写一段程序,使用系统调用fork()来创建一个子进程,子进程通过系统调用exec()更换自己的执行代码,显示新的代码“newprogram.”后,调用exit()结束。而父进程则调用waitpid()等待子进程结束,并在子进程结束后显示子进程的标识符,然后正常结束。实验指导一、所涉及的系统调用1、getpid在2.4.4版内核中,getpid是第20号系统调用,其在Linux函数库中的原型是:#includesys/types.h/*提供类型pid_t的定义*/#includeunistd.h/*提供函数的定义*/pid_tgetpid(void);getpid的作用很简单,就是返回当前进程的进程ID,请大家看以下的例子:/*getpid_test.c*/#includeunistd.hmain(){printf(ThecurrentprocessIDis%d\n,getpid());}这个程序的定义里并没有包含头文件sys/types.h,这是因为我们在程序中没有用到pid_t类型,pid_t类型即为进程ID的类型。事实上,在i386架构上(就是我们一般PC计算机的架构),pid_t类型是和int类型完全兼容的,我们可以用处理整形数的方法去处理pid_t类型的数据,比如,用%d把它打印出来。编译并运行程序getpid_test.c:$gccgetpid_test.c-ogetpid_test$./getpid_testThecurrentprocessIDis1980(你自己的运行结果很可能与这个数字不一样,这是很正常的。)再运行一遍:$./getpid_testThecurrentprocessIDis1981正如我们所见,尽管是同一个应用程序,每一次运行的时候,所分配的进程标识符都不相同。2、fork在2.4.4版内核中,fork是第2号系统调用,其在Linux函数库中的原型是:#includesys/types.h/*提供类型pid_t的定义*/#includeunistd.h/*提供函数的定义*/pid_tfork(void);创建一个新进程。系统调用格式:pid=fork()fork()返回值意义如下:0:在子进程中,pid变量保存的fork()返回值为0,表示当前进程是子进程。0:在父进程中,pid变量保存的fork()返回值为子进程的id值(进程唯一标识符)。-1:创建失败。如果fork()调用成功,它向父进程返回子进程的PID,并向子进程返回0,即fork()被调用了一次,但返回了两次。此时OS在内存中建立一个新进程,所建的新进程是调用fork()父进程(parentprocess)的副本,称为子进程(childprocess)。子进程继承了父进程的许多特性,并具有与父进程完全相同的用户级上下文。父进程与子进程并发执行。核心为fork()完成以下操作:(1)为新进程分配一进程表项和进程标识符进入fork()后,核心检查系统是否有足够的资源来建立一个新进程。若资源不足,则fork()系统调用失败;否则,核心为新进程分配一进程表项和唯一的进程标识符。(2)检查同时运行的进程数目超过预先规定的最大数目时,fork()系统调用失败。(3)拷贝进程表项中的数据将父进程的当前目录和所有已打开的数据拷贝到子进程表项中,并置进程的状态为“创建”状态。(4)子进程继承父进程的所有文件对父进程当前目录和所有已打开的文件表项中的引用计数加1。(5)为子进程创建进程上、下文进程创建结束,设子进程状态为“内存中就绪”并返回子进程的标识符。(6)子进程执行虽然父进程与子进程程序完全相同,但每个进程都有自己的程序计数器PC(注意子进程的PC开始位置),然后根据pid变量保存的fork()返回值的不同,执行了不同的分支语句。例:fork()调用前fork()调用后3、exit在2.4.4版内核中,exit是第1号调用,其在Linux函数库中的原型是:#includestdlib.hvoidexit(intstatus);不像fork那么难理解,从exit的名字就能看出,这个系统调用是用来终止一个进程的。无论在程序中的什么位置,只要执行到exit系统调用,进程就会停止剩下的所有操作,清除包括PCB在内的各种数据结构,并终止本进程的运行。请看下面的程序:/*exit_test1.c*/#includestdlib.hmain(){printf(thisprocesswillexit!\n);exit(0);printf(neverbedisplayed!\n);}编译后运行:$gccexit_test1.c-oexit_test1…..pid=fork();if(pid==0)printf(I'mthechildprocess!\n);elseif(pid0)printf(I'mtheparentprocess!\n);elseprintf(Forkfail!\n);………..pid=fork();if(pid==0)printf(I'mthechildprocess!\n);elseif(pid0)printf(I'mtheparentprocess!\n);elseprintf(Forkfail!\n);………..pid=fork();if(pid==0)printf(I'mthechildprocess!\n);elseif(pid0)printf(I'mtheparentprocess!\n);elseprintf(Forkfail!\n);……PCPCPC$./exit_test1thisprocesswillexit!我们可以看到,程序并没有打印后面的neverbedisplayed!\n,因为在此之前,在执行到exit(0)时,进程就已经终止了。exit系统调用带有一个整数类型的参数status,我们可以利用这个参数传递进程结束时的状态,比如说,该进程是正常结束的,还是出现某种意外而结束的,一般来说,0表示没有意外的正常结束;其他的数值表示出现了错误,进程非正常结束。我们在实际编程时,可以用wait系统调用接收子进程的返回值,从而针对不同的情况进行不同的处理。exit和_exit_exit在Linux函数库中的原型是:#includeunistd.hvoid_exit(intstatus);_exit()函数的作用最为简单:直接使进程停止运行,清除其使用的内存空间,并销毁其在内核中的各种数据结构;exit()函数则在这些基础上作了一些包装,在执行退出之前加了若干道工序,也是因为这个原因,有些人认为exit已经不能算是纯粹的系统调用。exit()函数与_exit()函数最大的区别就在于exit()函数在调用exit系统调用之前要检查文件的打开情况,把文件缓冲区中的内容写回文件。在Linux的标准函数库中,有一套称作“高级I/O”的函数,我们熟知的printf()、fopen()、fread()、fwrite()都在此列,它们也被称作“缓冲I/O(bufferedI/O)”,其特征是对应每一个打开的文件,在内存中都有一片缓冲区,每次读文件时,会多读出若干条记录,这样下次读文件时就可以直接从内存的缓冲区中读取,每次写文件的时候,也仅仅是写入内存中的缓冲区,等满足了一定的条件(达到一定数量,或遇到特定字符,如换行符\n和文件结束符EOF),再将缓冲区中的内容一次性写入文件,这样就大大增加了文件读写的速度,但也为我们编程带来了一点点麻烦。如果有一些数据,我们认为已经写入了文件,实际上因为没有满足特定的条件,它们还只是保存在缓冲区内,这时我们用_exit()函数直接将进程关闭,缓冲区中的数据就会丢失,反之,如果想保证数据的完整性,就一定要使用exit()函数。请看以下例程:/*exit2.c*/#includestdlib.hmain(){printf(outputbegin\n);printf(contentinbuffer);exit(0);}编译并运行:$gccexit2.c-oexit2$./exit2outputbegincontentinbuffer/*_exit1.c*/#includeunistd.hmain(){printf(outputbegin\n);printf(contentinbuffer);_exit(0);}编译并运行:$gcc_exit1.c-o_exit1$./_exit1outputbegin在一个进程调用了exit之后,该进程并非马上就消失掉,而是留下一个称为僵尸进程(Zombie)的数据结构。在Linux进程的5种状态中,僵尸进程是非常特殊的一种,它已经放弃了几乎所有内存空间,没有任何可执行代码,也不能被调度,仅仅在进程列表中保留一个位置,记载该进程的退出状态等信息供其他进程收集。/*zombie.c*/#includesys/types.h#includeunistd.hmain(){pid_tpid;pid=fork();if(pid0)/*如果出错*/printf(erroroccurred!\n);elseif(pid==0)/*如果是子进程*/exit(0);else/*如果是父进程*/sleep(60);/*休眠60秒,这段时间里,父进程什么也干不了*/wait(NULL);/*收集僵尸进程*/}sleep的作用是让进程休眠指定的秒数,在这60秒内,子进程已经退出,而父进程正忙着睡觉,不可能对它进行收集,这样,我们就能保持子进程60秒的僵尸状态。编译这个程序:$gcczombie.c-ozombie后台运行程序,以使我们能够执行下一条命令$./zombie&[1]1577列一下系统内的进程$ps-ax......1177pts/0S0:00-bash1577pts/0S0:00./zombie1578pts/0Z0:00[zombiedefunct]1579pts/0R0:00ps-ax系统调用exit,它的作用是使进程退出,但也仅仅限于将一个正常的进程变成一个僵尸进程,并不能将其完全销毁。僵尸进程虽然对其他进程几乎没有什么影响,不占用CPU时间,消耗的内存也几乎可以忽略不计,但有它在那里呆着,还是让人觉得心里很不舒服。而且Linux系统中进程数目是有限制的,在一些特殊的情况下,如果存在太多的僵尸进程,也会影响到新进程的产生。那么,我们该如何来消灭这些僵尸进程呢?僵尸进程的概念是从UNIX上继承来的。僵尸进程中保存着很多对程序员和系统管理员非常重要的信息,首先,这个进程是怎么死亡的?是正常退出呢,还是出现了错误,还是被其它进程强迫退出的?其次,这个进程占用的总系统CPU时间和总用户CPU时间分别是多少?发生页错误的数目和收到信号的数目。这些信息都被存储在僵尸进程中,试想如果没有僵尸进程,进程一退出,所有与之相关的信息都立刻归于无形,而此时程序员或系统管理员需要用到,就不行了。如何收集这些信息,并终结这些僵尸进程呢?就要靠waitpid调用和wait调用。这两者的作用都是收集僵尸进程留下的信息,同时使这个进程彻底消失。4、wait和waitpidwait的函数原型是:#includesys/types.h/*提供类型pid_t的定义*/#includesys/wait.hpid_twait(int*status)进程一旦调用了wait,就立即阻塞自己,由wa