
一、实验目的
1.熟悉和理解进程和进程树的概念,掌握有关进程的管理机制
2.通过进程的创建、撤销和运行加深对进程并发执行的理解
3.明确进程与程序、并行与串行执行的区别
4.掌握用C 程序实现进程控制的方法
二、实验学时
2学时
三、实验背景知识
所涉及的系统调用
1、exec( )系列(exec替换进程映像)
系统调用exec( )系列,也可用于新程序的运行。fork( )只是将父进程的用户级上下文拷贝到新进程中,而exec( )系列可以将一个可执行的二进制文件覆盖在新进程的用户级上下文的存储空间上,以更改新进程的用户级上下文。exec( )系列中的系统调用都完成相同的功能,它们把一个新程序装入内存,来改变调用进程的执行代码,从而形成新进程。如果exec( )调用成功,调用进程将被覆盖,然后从新程序的入口开始执行,这样就产生了一个新进程,新进程的进程标识符id 与调用进程相同。
exec( )没有建立一个与调用进程并发的子进程,而是用新进程取代了原来进程。所以exec( )调用成功后,没有任何数据返回,这与fork( )不同。exec( )系列系统调用在UNIX系统库unistd.h中,共有execl、execlp、execle、execv、execvp五个,其基本功能相同,只是以不同的方式来给出参数。
#include int execl(const char *pathname, const char *arg, …); int execlp(const char *filename, const char *arg, …); int execle(const char *pathname, const char *arg, …, const char *envp[ ]); int execv(const char *pathname, char *const argv[ ]); int execvp(const char *filename, char *const argv[ ]); 参数: path参数表示你要启动程序的名称包括路径名。 arg参数表示启动程序所带的参数,一般第一个参数为要执行命令名,不是带路径且arg必须以NULL结束。 返回值:成功返回0,失败返回-1 注:上述exec系列函数底层都是通过execve系统调用实现. 1)带l 的exec函数:execl,execlp,execle,表示后边的参数以可变参数的形式给出且都以一个空指针结束。 #include #include #include int main(void) { printf("entering main process---\\n"); execl("/bin/ls printf("exiting main process ----\\n"); return 0; 运行结果:利用execl将当前进程main替换掉,所有最后那条打印语句不会输出。 2)带 p 的exec函数:execlp,execvp,表示第一个参数path不用输入完整路径,只有给出命令名即可,它会在环境变量PATH当中查找命令 #include #include #include int main(void) { printf("entering main process---\\n"); if(execl("ls // if(execlp("ls perror("excl error"); return 0; } 结果不能替换,因没有指定路径名。若将蓝色语句换成红色部分内容执行,则可以替换成功。 3)不带 l 的exec函数:execv,execvp表示命令所需的参数以char *arg[]形式给出且arg最后一个元素必须是NULL。 #include #include #include int main(void) { printf("entering main process---\\n"); int ret; char *argv[] = {"ls ret = execvp("ls",argv); if(ret == -1) perror("execl error"); printf("exiting main process ----\\n"); return 0; } 4)带 e 的exec函数:execle表示,将环境变量传递给需要替换的进程 2、exec( )和fork( )联合使用 系统调用exec和fork( )联合使用能为程序开发提供有力支持。用fork( )建立子进程,然后在子进程中使用exec( ),这样就实现了父进程与一个与它完全不同子进程的并发执行。 一般,wait、exec联合使用的模型为: int status; ............ if (fork( )= =0) { ...........; execl(...); ...........; } wait(&status); 3、wait( ) 当一个子进程先于父进程结束运行时,它与其父进程之间的关联还会保持到父进程也正常地结束运行,或者父进程调用了wait才告终止。 子进程退出时,内核将子进程置为僵尸状态,这个进程称为僵尸进程,它只保留最小的一些内核数据结构,以便父进程查询子进程的退出状态。 进程表中代表子进程的数据项是不会立刻释放的,虽然不再活跃了,可子进程还停留在系统里,因为它的退出码还需要保存起来以备父进程中后续的wait调用使用。它将称为一个“僵进程”。 • 调用wait函数查询子进程退出状态,若子进程没有运行完,则父进程会被挂起,等待子进程运行结束。如果子进程没有完成,父进程一直等待。wait( )将调用进程挂起,直至其子进程因暂停或终止而发来软中断信号为止。如果在wait( )前已有子进程暂停或终止,则调用进程做适当处理后便返回。 系统调用格式: #include #include int wait(status) int *status; 其中,status是用户空间的地址。它的低8位反应子进程状态,为0表示子进程正常结束,非0则表示出现了各种各样的问题;高8位则带回了exit( )的返回值。exit( )返回值由系统给出。 当一个进程结束时,Linux 系统将产生一个SIGCHLD 信号通知其父进程。在父进程未查询子进程结束的原因时,该子进程虽然停止了,但并未完全结束。此时这一子进程被称为僵尸进程(zombie process)。例如,在有些情况下父进程先于子进程退出,于是会看到在系统提示符“$”后子进程仍然在连续输出信息,这对用户是非常不友好的。我们可以使用系统调用wait,来让父进程处于等待状态,直到子进程退出后才继续执行后面的语句。 参数status用来保存被收集进程退出时的一些状态,它是一个指向int类型的指针。但如果我们对这个子进程是如何死掉的毫不在意,只想把这个僵尸进程消灭掉,(事实上绝大多数情况下,我们都会这样想),我们就可以设定这个参数为NULL,就象下面这样: pid = wait(NULL); 如果成功,wait会返回被收集的子进程的进程ID,如果调用进程没有子进程,调用就会失败,此时wait返回-1。 如果参数status的值不是NULL,wait就会把子进程退出时的状态取出并存入其中,这是一个整数值(int),指出了子进程是正常退出还是被非正常结束的,以及正常结束时的返回值,或被哪一个信号结束的等信息。由于这些信息被存放在一个整数的不同二进制位中,所以用常规的方法读取会非常麻烦,人们就设计了一套专门的宏(macro)来完成这项工作, 4、exit( ) 进程结束可通过相应的函数实现: #include void exit(int status); //终止正在运行的程序,关闭所有被该文件打开的文件描述符。其中,status是返回给父进程的一个整数,以备查考。 int atexit(void (*function)(void)); //用于注册一个不带参数也没有返回值的函数以供程序正常退出时被调用。参数function 是指向所调用程序的文件指针。调用成功返回0,否则返回-1,并将errno 设置为相应值 int on_exit(void (*function)(int,void *),void *arg); //作用与atexit 类似,不同是其注册的函数具有参数,退出状态和参数arg 都是传递给该函数使用 void abort(void); //用来发送一个SIGABRT 信号,该信号将使当前进程终止 WIFEXITED(status):这个宏用来指出子进程是否为正常退出的,如果是,它会返回一个非零值。 WEXITSTATUS(status)取得子进程exit()返回的结束代码,一般会先用WIFEXITED 来判断是否正常结束才能使用此宏。 WIFSIGNALED(status)如果子进程是因为信号而结束则此宏值为真终止进程的执行。 为了及时回收进程所占用的资源并减少父进程的干预,UNIX/LINUX利用exit( )来实现进程的自我终止,通常父进程在创建子进程时,应在进程的末尾安排一条exit( ),使子进程自我终止。exit(0)表示进程正常终止,exit(1)表示进程运行有错,异常终止。 如果调用进程在执行exit( )时,其父进程正在等待它的终止,则父进程可立即得到其返回的整数。核心须为exit( )完成以下操作: (1)关闭软中断 (2)回收资源 (3)写记帐信息 (4)置进程为“僵死状态” 四、实验内容 1.了解系统调用(execl,execlp,execle,execv,execvp)使用 例 3.1:创建一个进程,并使用exec将其替换成其他程序的进程 #include #include #include main( ) { int pid; pid=fork( ); switch(pid) { case -1: printf("fork fail!\\n"); exit(1); case 0: printf(“Child process PID:%d.\\n”,getpid()); execl("/bin/ls printf("exec fail!\\n"); exit(1); default: printf(“Parent process PID: %4d.\\n”,getpid()); wait(NULL); printf("ls completed !\\n"); exit(0); } } 运行程序并回答以下问题: 问题1:该程序中一共有几个进程并发? 问题2:程序的运行结果为什么含义? 2. 父进程查询子进程的退出 例 3.2: #include #include #include #include #include int main() { pid_t pid; char *message; int n; printf(“Fork program starting\\n”); pid=fork(); switch(pid){ case -1: printf(“Fork error!\\n”); exit(1); case 0: message= “Child process is printing.”; n=5; break; default: message=“Parent process id printing.”; n=3; break; } for(;n>0;n--) { puts(message); sleep(1);} if(pid) { pid_t child_pid; int stat_val; child_pid=wait(&stat_val); printf(“Child has finished: PID=%d.\\n”,child_pid); } exit(0); } 运行程序并回答以下问题: 问题1:该程序如果不加红色代码部分其运行结果是什么?为什么结果会出现在下一行的提示符“#”或“$”后? 问题2:添加红色部分程序的作用是什么? 例 3.3: #include #include #include #include #include main() {int status; pid_t pc,pr; pc=fork(); if(pc<0) printf("error ocurred!\\n"); else if(pc==0){ printf("This is child process with pid of %d.\\n",getpid()); exit(3); } else{ pr=wait(&status); if(WIFEXITED(status)){ printf("the child process %d exit normally.\\n",pr); printf("the return code is %d.\\n",WEXITSTATUS(status)); }else printf("the child process %d exit abnormally.\\n",pr); }} 运行程序并回答以下问题: 问题1:该程序红色代码部分的含义是什么? 问题2:程序的运行结果是什么,理解wait函数的作用? 3.进程的终止 例 3.4: #include #include #include #include void h_exit(int status); static void forkerror(void); static void waiterror(void); int main(void) { pid_t pid; int status; if(pid=fork()<0) atexit(forkerror); else if(pid==0) abort(); if(wait(&status)!=pid) atexit(waiterror); h_exit(status); } void h_exit(int status) { if(WIFEXITED(status)) printf(“Normal termination, exit status=%d.\\n”, WEXITSTATUS(status)); else if(WIFSIGNALED(status)) printf(“Abnormal termination, exit status=%d.\\n”, WEXITSTATUS(status)); } void forkerror(void) { printf(“Fork error!\\n”); } void waiterror(void) { printf(“Wait error!\\n”);} 运行程序并回答以下问题: 问题1:该程序的含义是什么? 问题2:程序的运行结果是什么,请解释h_exit函数的作用? 4.了解system 函数的用法 用户可以使用该函数来在自己的程序中调用系统提供的各种命令。 #include int system(const char *cmdstring); 参数cmdstring 是一个字符串指针,指向表示命令行的字符串。该函数的实现是通过调用fork、exec 和waitpid 函数来完成的,其中任意一个调用失败则system 函数的调用失败,故返回值较复杂。 例 3.5: #include #include int main() { printf(“running ps with system.\\n”); system(“ps –ax”); printf(“Done.\\n”); exit(0); } 五、思考题 1.怎样用C 程序实现进程的控制? 当首次调用新创建进程时,其入口在哪里? 2.系统调用fork( )是如何创建进程的?系统调用exit( )是如何终止一个进程的? 3.系统调用exec 系列函数是如何更换进程的可执行代码的?
