5.进程控制
进程控制
这一节主要介绍Unix系统的进程控制,包括创建新进程、执行进程和进程终止。由于前面Linux学习部分有了一定的进程控制的理论知识,本节更多是结合实践代码介绍。
1.fork
一个现有进程通过调用fork函数创建一个新进程。
#include<unistd.h>
pid_t fork(void)
/* 返回值:子进程返回0,父进程返回子进程ID;若出错返回-1 */
子进程ID为0原因是因为系统中ID为0的进程总是由内核交换程序使用,所以子进程ID不可能为0。执行fork之后,子进程是父进程的副本,获得父进程的数据空间、堆和栈的副本。下面通过一个实例演示fork函数:
#include "apue.h"
int globvar = 6; /* external variable in initialized data */
char buf[] = "a write to stdout\n";
int
main(void)
{
int var; /* automatic variable on the stack */
pid_t pid;
var = 88;
if (write(STDOUT_FILENO, buf, sizeof(buf)-1) != sizeof(buf)-1)
err_sys("write error");
printf("before fork\n"); /* we don't flush stdout */
if ((pid = fork()) < 0) {
err_sys("fork error");
} else if (pid == 0) { /* child */
globvar++; /* modify variables */
var++;
} else {
sleep(2); /* parent */
}
printf("pid = %ld, glob = %d, var = %d\n", (long)getpid(), globvar,
var);
exit(0);
}
运行程序得到:
fork上述结果中,先运行了子进程,再运行了父进程(一般希望子进程先运行,但是不一定),子进程拷贝了父进程的数据空间,并对globvar
和var
执行加1操作,此时子进程和父进程都有各自的数据空间。
父进程和子进程的文件共享如下图所示:
子进程和父进程文件共享fork主要有两种用法:
- 一个父进程希望复制自己,使父进程和子进程同时执行不同的代码段。在网络服务进程中是常见的——父进程等待客户端的服务请求,请求到达后,父进程调用fork,使子进程处理此请求。父进程则继续等待下一个服务请求
- 一个进程执行一个不同的程序。对shell常见,子进程从fork返回后立即调用exec
2.vfork
由Linux内核设计的理论知道,vfork的原理是,父进程产生一个子进程,并阻塞父进程,子进程使用父进程的地址空间,直到子进程执行完毕,父进程继续执行。通过下面代码分析:
#include "apue.h"
int globvar = 6; /* external variable in initialized data */
int
main(void)
{
int var; /* automatic variable on the stack */
pid_t pid;
var = 88;
printf("before vfork\n"); /* we don't flush stdio */
if ((pid = vfork()) < 0) {
err_sys("vfork error");
} else if (pid == 0) { /* child */
globvar++; /* modify parent's variables */
var++;
_exit(0); /* child terminates */
}
/* parent continues here */
printf("pid = %ld, glob = %d, var = %d\n", (long)getpid(), globvar,
var);
exit(0);
}
在子进程中对globvar
和var
执行了加1操作,因为使用的父进程地址空间,因此相对于修改了父进程数据,运行该程序可以得到结果:
3.进程终止
进程终止时,内核逐个检查所有活动进程,判断它是否是正要终止进程的子进程,如果是,则该进程的父进程ID就改为1,保证每个进程都有父进程。
如果子进程在父进程之前终止,内核为每个终止子进程保存一定量的信息,所以当终止进程的父进程调用wait或waitpid时,可以得到这些信息(包括进程ID、进程终止状态以及使用的CPU时间总量)。内核可以释放终止进程所使用的所有存储区,关闭其所有文件夹。一个已经终止,但是父进程未对其进行善后的进程称为僵尸进程。
4.wait和waitpid
当一个进程终止时,内核向其父进程发送SIGCHLD信号,因为子进程终止是一个异步时间,所以这个信号也是内核向父进程发的异步通知,父进程可以忽略该信号或者调用wait或waitpid。若是调用wait或waitpid,可能发生下面情况:
- 如果其所有子进程都在运行,则阻塞
- 如果一个子进程已经终止,正在等待父进程获取其终止状态,则取得该子进程的终止状态立即返回
- 如果它没有任何子进程,则立即出错返回
#include <sys/wait.h>
pid_t wait(int *statloc);
pid_t waitpid(pid_t pid, int *statloc, int options);
/* 返回值:成功返回进程ID,出错返回0或-1 */
这两个函数区别如下:
- 在一个子进程终止前,wait使其调用者阻塞,而waitpid有一选项,可使调用者不阻塞
- waitpid并不等待在其调用之后的第一个终止子进程,可以控制他所等待的进程
如果一个子进程已经停止,且是一个僵尸进程,则wait立即返回并取得该子进程的状态;否则wait使其调用者阻塞,直到一个子进程终止,返回终止子进程的ID。
5.竞争条件
当多个进程都企图对共享数据进行某种处理,而最后的结果又取决于进程运行的顺序时,我们认为发生了竞争条件。例如在fork后无法预料那个进程先运行。
这里给出一种解决方法:
#include "apue.h"
static void charatatime(char *);
int
main(void)
{
pid_t pid;
TELL_WAIT();
if ((pid = fork()) < 0) {
err_sys("fork error");
} else if (pid == 0) {
WAIT_PARENT(); /* parent goes first */
charatatime("output from child\n");
} else {
charatatime("output from parent\n");
TELL_CHILD(pid);
}
exit(0);
}
static void
charatatime(char *str)
{
char *ptr;
int c;
setbuf(stdout, NULL); /* set unbuffered */
for (ptr = str; (c = *ptr++) != 0; )
putc(c, stdout);
}
上述代码即在子进程执行时,先等待父进程执行,而父进程内容中执行完毕时通知子进程当前进程执行完毕。
6.exec
fork创建新进程之后,往往需要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程执行的程序完全替换为新程序,而新程序则从其main函数开始执行。因为调用exec并不创建新进程,所以前后进程ID并未改变。exec只是用磁盘上的一个新程序替换了当前进程的正文段、数据段、堆段和栈段。
有7种不同的exec可供使用,它们常称为exec函数:
#include <unistd.h>
int execl(const char *pathname, const char *arg0, ... /* (char *)0 */ );
int execv(const char *pathname, char *const argv[]);
int execle(const char *pathname, const char *arg0, ... /* (char *)0, char *const envp[] */ );
int execve(const char *pathname, char *const argv[], char *const envp[]); int execlp(const char *filename, const char *arg0, ... /* (char *)0 */ ); int execvp(const char *filename, char *const argv[]);
int fexecve(int fd, char *const argv[], char *const envp[]);
/* 返回值:出错返回-1,成功不返回*/
它们之间的关系可以由下图得到:
exec