740 likes | 1.02k Views
第 6 章. 进程控制. 本章重点. 进程的基本概念及进程的结构 Linux 环境下进程的相关函数的应用 守护进程的概念、启动和建立 进程操作程序的编写. 6.1 进程简介. 进程是正在执行中的程序。当我们在终端执行命令时, Linux 就会建立一个进程,而当我们的程序执行完成时,这个进程就被终止了。 Linux 是一个多任务操作系统,允许多个用户使用计算机系统,多个进程并发执行。 Linux 环境下启动进程有两种主要途径:手工启动和调度启动。. 6.1 进程简介. ( 1 )手工启动
E N D
第 6 章 进程控制
本章重点 • 进程的基本概念及进程的结构 • Linux环境下进程的相关函数的应用 • 守护进程的概念、启动和建立 • 进程操作程序的编写
6.1 进程简介 • 进程是正在执行中的程序。当我们在终端执行命令时,Linux 就会建立一个进程,而当我们的程序执行完成时,这个进程就被终止了。 • Linux是一个多任务操作系统,允许多个用户使用计算机系统,多个进程并发执行。 • Linux环境下启动进程有两种主要途径:手工启动和调度启动。
6.1 进程简介 (1)手工启动 • 前台启动:是手工启动一个进程的最常用方式。一般地,当用户输入一个命令,如“gedit”时,就已经启动了一个进程,并且是一个前台进程。 • 在后台启动进程的方法是用户在终端输入一个命令时同时在命令尾加上一个“&”符号。比如用户要启动一个需要长时间运行文本编辑器,可输入命令“gedit &”,表示这个进程在后台运行。在终端中显示的“[1] 4513”字样表示在后台运行的进程数及进程号。进程在后台运行,终端中仍可以运行其他进程。
6.1 进程简介 (2)调度启动 • 有时,系统需要进行一些比较费时而且占用资源的维护工作,并且这些工作适合在深夜无人值守的时候进行,这时用户就可以事先进行调度安排,指定任务运行的时间或者场合,到时候系统就会自动完成一切工作。
6.1 进程简介 • 例如,输入“at 17:30 8/8/2011”,指定在2011年8月8日下午5:30执行某命令。 [root@localhost root]# at 17:30 8/8/2011 warning: commands will be executed using (in order) a) $SHELL b) login shell c) /bin/sh at> service httpd start at> ls -l kk at> <EOT> job 2 at 2011-08-08 17:30 • 在输入调度命令结束时,按下组合键Ctrl+D退出at编辑状态,到了进程调度的时间计算机自动启动以上两个进程。
6.1 进程简介 • 进程操作包括终止进程、改变优先级、查看进程属性等 。 • Linux环境下常见的进程调用命令
6.2 Liunx进程控制 • 在Linux环境下进程创建时,系统会分配一个唯一的数值给每个进程,这个数值就称为进程标识符(PID)。 • 在Linux中进程标识有进程号(PID)和它的父进程号(PPID)。其中,PID唯一地标识一个进程。PID和PPID都是非零的正整数。在Linux中获得当前进程的PID和PPID的系统调用为getpid和getppid函数。
6.2 Liunx进程控制 • 例6-1:设计一个程序,要求显示Linux系统分配给此程序的进程号(PID)和它的父进程号(PPID)。 • 步骤 1:编辑源程序代码 [root@localhost root]#vim 6-1.c
6.2 Liunx进程控制 • 步骤 2:用gcc编译程序 [root@localhost root]#gcc 6-1.c –o 6-1 • 步骤 3:运行程序 [root@localhost root]#./6-1 终端中的显示如下: [root@localhost root]#./6-1 系统分配的进程号(PID)是:4601 系统分配的父进程号(PPID)是:4556 [root@localhost root]#./6-1 系统分配的进程号(PID)是:4602 系统分配的父进程号(PPID)是:4556 多次运行例6.1的程序,从程序运行结果可以看出,每一次运行的结果PID值都是不一样的,PID是唯一地标识一个进程。实际应用中可利用系统分配的PID值来建立临时文件,以避免临时文件相同带来的问题。
6.2 Liunx进程控制 • getpid函数说明 • getppid函数说明
6.2.1 进程的相关函数 • Linux c与进程相关的主要函数
6.2.2 进程创建 • fork函数 • 进程调用fork函数创建一个新进程,由fork创建的新进程被称为子进程(child process)。该函数被调用一次,但返回两次,两次返回的区别是子进程的返回值是0,而父进程的返回值则是新子进程的进程PID。 • 子进程和父进程继续执行fork之后的指令。子进程是父进程的复制品。例如,子进程获得父进程数据空间、堆和栈的复制品。注意,这是子进程所拥有的拷贝。父、子进程并不共享这些存储空间部分,通常父、子进程共享代码段。
6.2.2 进程创建 • 例6.2设计一个程序,用fork函数创建一个子进程,在子进程中给变量n赋值3,在父进程中给变量n赋值6,fork调用之后父进程和子进程的变量message和n被赋予不同的值,互不影响。请阅读程序,思考程序运行的结果。 • 步骤1 设计编辑源程序代码。 [root@localhost root]#vi 6-2.c
6.2.2 进程创建 • 步骤2 用gcc编译程序。 [root@localhost root]#gcc 6-2.c –o 6-2 • 步骤3 运行程序。 [root@localhost root]# ./6-2
6.2.2 进程创建 • 思考: • 在子进程中给变量n赋值6,在父进程中给变量n赋值3,请分析程序运行的结果。提示:,当父进程终止时shell进程认为命令执行结束了,于是打印shell提示符,而事实上子进程这时还没结束,所以子进程的消息打印到了shell提示符后面。最后光标停在This is the child的下一行,这时用户仍然可以敲命令,即使命令不是紧跟在提示符后面,Shell也能正确读取。 • 把程序中的sleep(1);去掉,查看程序的运行结果如何改变。 • 有如下程序代码: int main() { for(;;) fork(); return 0; } • 这个程序什么也不做,就是死循环地用fork创建子进程,其结果是程序不断产生进程,而这些进程又不断产生新的进程,系统的进程很快就满了,系统就被这些不断产生的进程“撑死了”。当然,只要系统管理员预先给每个用户设置可运行的最大进程数,这个恶意的程序就完成不了它的企图。
6.2.2 进程创建 • sleep函数说明
6.2.2 进程创建 • fork函数说明
6.2.2 进程创建 • exec函数 • 在Linux系统中,使程序执行的唯一方法是使用系统调用exec()。 • 系统调用exec()有多种使用形式,称为exec()族,它们只是在参数上不同,而功能是相同的。
6.2.2 进程创建 • 事实上,这六个函数中真正的系统调用函数只有execve,其他五个都是库函数,它们最终都会调用execve这个系统调用。
6.2.2 进程创建 • exec调用举例如下: char *const ps_argv[] ={"ps", "-o", "pid,ppid,pgrp, session,tpgid, comm", NULL}; char *const ps_envp[] ={"PATH=/bin:/usr/bin", "TERM=console", NULL}; execl("/bin/ps", "ps", "-o", "pid,ppid,pgrp,session,tpgid,comm", NULL); execv("/bin/ps", ps_argv); execle("/bin/ps", "ps", "-o", "pid,ppid,pgrp,session,tpgid,comm", NULL, ps_envp); execve("/bin/ps", ps_argv, ps_envp); execlp("ps", "ps", "-o", "pid,ppid,pgrp,session,tpgid,comm", NULL); execvp("ps", ps_argv);
6.2.2 进程创建 • 例6.3 设计一个程序,用fork函数创建一个子进程,在子进程中,要求显示子进程号与父进程号,然后显示当前目录下的文件信息,在父进程中同样显示子进程号与父进程号。 • 步骤1 设计编辑源程序代码。 [root@localhost root]#vi 6-3.c
6.2.2 进程创建 • 步骤2 用gcc编译程序。 [root@localhost root]#gcc 6-3.c –o 6-3 • 步骤3 运行程序。 • 编译成功后,执行6-3,此时系统会出现运行结果,根据result的值,先显示Linux系统分配给子进程的进程号(PID)和父进程号(PPID),接着运行ls程序,显示当前目录下的文件信息。再等待10秒钟后,显示父进程的进程号(PID)和父进程号(PPID)。
6.2.2 进程创建 • execv函数的应用,要在程序中执行命令:ps -ef,命令ps在"/bin"目录下。在这一函数中,参数v表示参数传递(含命令)为构造指针数组方式: char *arg[]={"ps","-ef",NULL}; 函数的使用为: execv("/bin/ps",arg); 参考程序: #include<stdio.h> /*文件预处理,包含标准输入输出库*/ #include<unistd.h> /*文件预处理,包含getpid、getppid函数库*/ int main () /*C程序的主函数,开始入口*/ { char *arg[]={"ls","-al",NULL}; execv("/bin/ls",arg); return 1; }
6.2.2 进程创建 • execlp 函数的应用,要在程序中执行命令:ps -ef,命令ps在"/bin"目录下。在这一函数中,参数l表示命令或参数逐个列举,参数p为文件查找方式(不需要给出路径)。因而此函数的调用形式为: execlp("ps","ps","-ef",NULL); 请编写一程序进行调试。 • execl 函数的应用,要在程序中执行命令:ps -ef,命令ps在"/bin"目录下。在这一函数中,参数l表示命令或参数逐个列举,文件需给定路径。因而此函数的调用形式为:execl("/bin/ps","ps","-ef",NULL); 请编写一程序进行调试。
6.2.2 进程创建 • execle 函数的应用 : execle("/bin/login", "login", "-p", username, NULL, envp); • 上述语句运行时,login程序提示用户输入密码(输入密码期间关闭终端的回显),然后验证帐号密码的正确性。如果密码不正确,login进程终止,init会重新fork/exec一个getty进程。如果密码正确,login程序设置一些环境变量,设置当前工作目录为该用户的主目录。 • 设计一个程序,在子进程中调用函数execl("/bin/ps","ps","-ef",NULL),而在父进程中调用函数execle("/bin/env","env",NULL,envp),其中定义:char *envp[]= {"PATH= /tmp","USER=liu",NULL}; 请编写并进行调试。
6.2.3 进程终止 (1) 正常终止: (a) 在main函数内执行return语句,这等效于调用e x i t。 (b) 调用exit函数。此函数由ANSI C定义,其操作包括调用各终止处理程序,然后关闭所有标准I/O流等。 (c) 调用_exit系统调用函数,此函数由exit调用。 (2) 异常终止: (a) 调用abort 。 (b) 由一个信号终止。
6.2.3 进程终止 • 例6.4 设计一个程序,要求子进程和父进程都在显示输出一些文字后分别用exit和_exit函数终止进程。 • 步骤 1:设计编辑源程序代码 [root@localhost root]#vim 6-4.c
6.2.3 进程终止 • 步骤 2:用gcc编译程序 [root@localhost root]#gcc 6-4.c –o 6-4 • 步骤 3:运行程序 [root@localhost root]#./6-4 测试终止进程的_exit函数! 测试终止进程的exit函数! 目前为父进程,这一行我们用缓存! [root@localhost root]# 可以看出,调用exit函数时,缓冲区中的记录能正常输出;而调用_exit时,缓冲区中的记录无法输出。
6.2.3 进程终止 • _exit()函数作用:直接使进程停止运行,清除其使用的内存空间,并清除其在内核中的各种数据结构; • exit()函数则在执行退出之前加了若干道工序,exit函数在调用exit系统之前要查看文件的打开情况,把文件缓冲区中的内容写回文件。
6.2.3 进程终止 • exit函数说明 • _exit函数说明
6.2.4 僵尸进程 • 一个已经终止运行、但其父进程尚未对其进行善后处理(获取终止子进程的有关信息、释放它仍占用的资源)的进程被称为僵死进程(zombie)。Linux的ps命令将僵死进程的状态显示为Z。 • 使用fork函数创建子进程时,由于子进程有可能比父进程晚终止,父进程终止后,子进程还没终止,子进程就会进入一种无父进程的状态,此时子进程就成为了僵尸进程。为避免这种情况,可以在父进程中调用wait或waitpid函数,使子进程比父进程早终止,而让父进程有机会了解子进程终止时的状态,并自动清除僵尸进程
6.2.4 僵尸进程 • wait函数是用于使父进程阻塞,直到一个子进程终止或者该进程接到了一个指定的信号为止。如果该父进程没有子进程或者他的子进程已经终止,则wait就会立即返回。 • waitpid的作用和wait一样,但它并不一定要等待第一个终止的子进程,它还有若干选项,也能支持作业控制。实际上wait函数只是waitpid函数的一个特例,在Linux内部实现wait函数时直接调用的就是waitpid函数。
6.2.4 僵尸进程 • 例6.5 僵尸进程产生。 #include <sys/types.h>#include <sys/wait.h>#include <unistd.h>#include <stdlib.h> int main (){ 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()); } else{ sleep(20); printf("This is partent with pid of %d\n",getpid()); } exit(0);}
6.2.4 僵尸进程 • 编译后运行 • [root@localhost root]#./6-5 • This is child process with pid of 2594 • This is partent with pid of 2593 • 程序执行后显示子进程的进程号2594,在父进程输出“This is partent with pid of 2593”前,在另一终端输入命令: • [root@localhost root]# ps 2594 • PID TTY STAT TIME COMMAND • 2594 pts/2 Z 0:00 [6-5 <defunct>] • ps命令显示2594进程的属性,其状态(STAT)为“Z”,为进程僵尸状态,如果在sleep(20);前添加语句wait(NULL);结果表明消除了僵尸进程。
6.2.4 僵尸进程 • 如果6-5进程退出而没有调用wait会出现什么情况?僵尸进程会停留在系统中吗? • 不,试着再次运行ps,你会发现两个6-5进程都消失了。 • 当一个程序退出,它的子进程被一个特殊进程继承,这就是init进程,它是Linux启动后运行的第一个进程。 • init 进程会自动清理所有它继承的僵尸进程。
6.2.4 僵尸进程 • 例6.6 通常在父进程中用wait函数等待子进程,父进程直到接收到子进程结束的信号后,父进程结束等待。请设计一个程序,要求创建一个子进程,子进程显示自己的进程号(PID)后暂停一段时间,父进程等待子进程正常结束,打印显示等待的进程号(PID)和等待的进程退出状态。 • 分析 先用fork函数创建子进程,在子进程中返回值为0,子进程用getpid函数显示自己的进程号(PID),用sleep函数暂停5秒;父进程中返回值大于0,父进程用wait函数等待子进程正常终止,避免子进程变成僵尸进程,父进程打印等待的进程的进程号(PID)和它的终止状态。
6.2.4 僵尸进程 • 步骤 1:设计编辑源程序代码 [root@localhost root]#vim 6-6.c
6.2.4 僵尸进程 • 步骤 2:用gcc编译程序 [root@localhost root]#gcc 6-6.c –o 6-6 • 步骤 3:运行程序 [root@localhost root]#./6-6 这是子进程,进程号(PID)是:4998 这是父进程,正在等待子进程…… 等待的进程的进程号(PID)是:4998 ,结束状态:6 此例中的子进程运行时间,明显比父进程时间长。为了避免子进程成为僵尸进程,父进程调用wait,阻塞父进程的运行,等待子进程正常结束,父进程才继续运行,直到正常结束。
6.2.4 僵尸进程 • 程序中也可使用语句: wpid=waitpid(pid,&status,0); • 子进程的结束状态返回后存于status,然后使用下列几个宏可判别子进程结束情况: • WIFEXITED(status),如果子进程正常结束则为非0值。 • WEXITSTATUS(status),取得子进程exit()返回的结束代码,一般先用WIFEXITED 来判断是否正常结束才能使用此宏。 • WIFSIGNALED(status),如果子进程是因为信号而结束则此宏值为真。 • WTERMSIG(status),取得子进程因信号而中止的信号代码,即信号的编号,一般会先用WIFSIGNALED 来判断后才使用此宏。 • WIFSTOPPED(status),如果子进程处于暂停执行情况则此宏值为真。一般只有使用WUNTRACED 时才会有此情况。 • WSTOPSIG(status),取得引发子进程暂停的信号代码,
6.2.4 僵尸进程 • 例6.7 在父进程中用waitpid函数等待子进程,子进程结束时通过调用函数exit(3)向父进程发送结束信号,父进程直到接收到子进程结束的信号后,父进程结束等待,并且通过宏WIFEXITED(stat_val),取得子进程exit(3)返回的结束代码。 • 步骤1 设计编辑源程序代码。 [root@localhost root]#vi 6-7.c
6.2.4 僵尸进程 • 步骤2 用gcc编译程序。 [root@localhost root]#gcc 6-7.c –o 6-7 • 步骤3 运行程序。 [root@localhost root]# ./6-7 This is the child This is the child This is the child Child exited with code 3 [root@localhost root]# ps PID TTY TIME CMD 32201 pts/2 00:00:00 bash 32416 pts/2 00:00:00 ps
6.2.4 僵尸进程 • wait函数说明 思考:改写程序6-6.c,程序设计要体现子进程与父进程迸发执行的效果,并且子进程退出后父进程才退出。 思考题:在例6.6中如果不用wait函数,子进程变成了僵尸进程,如何在系统找出这个僵尸进程?
6.2.4 僵尸进程 • 例6.8 设计一个程序,要求用户可以选择是否创建子进程,子进程模仿思科(Cisco)1912交换机的开机界面,以命令行的方式让用户选择进入,父进程判断子进程是否正常终止。 • 分析 先让用户选择是否创建子进程,如果不创建,打印子进程结束状态后退出;如果创建子进程,子进程模仿思科(Cisco)1912交换机的开机界面,以命令行的方式让用户选择进入,父进程用waitpid函数等待子进程正常终止,防止子进程变成僵尸进程,父进程打印子进程的终止状态。
6.2.4 僵尸进程 • 步骤 1:设计编辑源程序代码 [root@localhost root]#vim 6-8.c
6.2.4 僵尸进程 • 步骤 2:用gcc编译程序 [root@localhost root]#gcc 6-8.c –o 6-8 • 步骤 3:运行程序 [root@localhost root]#./6-8 1.创建子进程 2.不创建子进程 请输入您的选择:2 这是父进程(进程号:5028,父进程号:4739) 子进程非正常终止,子进程终止状态:0
6.2.4 僵尸进程 • 再次运行程序 [root@localhost root]#./6-8 1.创建子进程 2.不创建子进程 请输入您的选择:1 这是子进程(进程号:5044,父进程号:5043): 进入思科(Cisco)1912交换机开机界面。 1 user(s) now active on Management Console. User Interface Menu [0] Menus [1] Command Line [2] IP Configuration Enter Selection:0 您选择进入了菜单模式 这是父进程(进程号:5043,父进程号:4739) 子进程正常终止,子进程终止状态:1
6.2.4 僵尸进程 • 步骤4:修改程序 试着不用waitpid函数,如下所示: //waitpid(result,&status,0); /*父进程调用waitpid函数,消除僵尸进程*/ • 再次运行程序 [root@localhost root]#./6-8 1.创建子进程 2.不复创建子程 请输入您的选择:1 这是子进程(进程号:5067,父进程号:5064): 进入思科(Cisco)1912交换机开机界面。 1 user(s) now active on Management Console. User Interface Menu [0] Menus [1] Command Line [2] IP Configuration Enter Selection:这是父进程(进程号:5064,父进程号:4739) 子进程非正常终止,子进程终止状态:0 此例可以看出,在没有语法、语义等错误的情况下,程序还是没有完成设计要求。可见,在多进程程序设计时,除了养成使用完后就终止的良好习惯,还要让子进程工作完成后再终止,这个时候父进程就得灵活使用wait函数和waitpid函数。
6.2.4 僵尸进程 • waitpid函数说明
6.2.4 僵尸进程 • 思考题:waitpid函数的应用,要求子进程用sleep等待10秒,父进程用waitpid函数等待子进程正常结束,父进程在等待的时候不阻塞,每1秒在屏幕上输出一行文字,若发现子进程退出,打印出等待进程的进程号(PID)和退出状态。请编写一程序进行调试。