用fork可以创建新进程,用exec可以执行新程序,exit函数和两个wait函数可以处理终止和等待终止。这些是所需基本的进程控制原语。
1、进程标识符
每个进程都有一个非负整型表示的唯一进程ID。
进程ID可以重用,但多数UNIX系统实现延迟重用算法,使得赋予新进程的ID不同于最近终止进程所使用的ID,以防止将新进程误认为是使用同一ID的某个已经终止的先前进程。
专用进程的ID:
进程ID | 进程名称 | 进程类型 |
0 | 调度进程,也称为交换进程swapper | 系统进程 |
1 | init进程 | 用户进程 |
进程相关的标识符:
标识符名称 | 获取该标识符的函数#include<unistd.h> |
进程ID | pid_t getpid(void); |
父进程ID | pid_t getppid(void); |
调用进程的实际用户ID | uid_t getuid(void); |
调用进程的有效用户ID | uid_t geteuid(void); |
调用进程的实际组ID | gid_t getgid(void); |
调用进程的有效组ID | gid_t getegid(void); |
2、fork函数与vfork函数
现有进程可以调用fork函数创建一个新进程,该新进程被称为子进程。
#include<unistd.h> pid_t fork(void);/*返回值:子进程中返回0,父进程中返回子进程ID,出错返回-1*/ |
fork函数被调用一次,但返回两次的解读:
操作系统对进程的管理是通过进程表完成的,进程表中的每一个表项,记录的是当前OS中一个进程的情况。每添加一个进程,就往进程表里添加一个进程表项。单CPU情况下,每个时刻只有一个进程占用CPU,其他进程只能等该进程的CPU时间用完才可能占用CPU,得到执行。
当程序执行pid=fork()语句时,OS创建一个新的进程(子进程),并往进程表中添加一个新的进程表项。因子进程是父进程的副本,故新进程和原进程的执行的是同一个可执行程序,但这两个进程是相互独立的。
父进程继续执行,OS对fork的实现,使这个调用在父进程中返回子进程的ID,所以只执行之后pid>0的分支。
当子进程被OS调度,它的上下文被换入,占据CPU得到执行,OS对fork的实现,使这个调用在子进程中返回0,所以只执行pid==0
的分支。
#include "apue.h"
int glob = 6;
char buf[] = "a write to stdout\n";
int main(void)
{
int var;
pid_t pid;
var = 88;
if (write(STDOUT_FILENO, buf, sizeof(buf)-1) != sizeof(buf)-1)
err_sys("write error");
printf("before fork!\n");//printf("before fork! ");
if ((pid = fork()) < 0)
err_sys("fork error");
else if (pid == 0)
{
glob++;
var++;
}
else
{
sleep(2);
}
printf("pid = %d, glob = %d, var = %d\n", getpid(), glob, var);
exit(0);
}
printf(“before fork\n”);的输出:只输出一次before fork。因为标准输出连到终端设备是行缓冲,标准输出缓冲区由换行符冲洗。
[root]# ./a.out
a write to stdout
before fork!
pid = 2602, glob = 7, var = 89
pid = 2601, glob = 6, var = 88
[root]#
printf(“before fork ”);的输出:输出两次before fork。因为调用fork时,该数据仍在缓冲区中,然后在将父进程数据空间复制到子进程中时,该缓冲区也被复制到子进程中。在exit之前的第二个printf将该数据添加到现有的缓冲区中。当每个进程终止时,最终会冲洗其缓冲区中的副本。
[root]# ./a.out
a write to stdout
before fork! pid = 2615, glob = 7, var = 89
before fork! pid = 2614, glob = 6, var = 88
[root]#
至于
a write to stdout
只输出一次,是因为
write
函数是不带缓冲的。
fork失败的两个原因:
1)系统中有太多的进程
2)该实际用户ID的进程总数超过了系统限制
fork有两种常见用法:
1)一个进程希望赋值自己,使父、子进程同时执行不同的代码段。常见于网络服务进程,父进程等待客户端的服务请求,当这种请求到达时,父进程调用fork,使子进程处理此请求。父进程则继续等待下一个服务请求到达。
2)一个进程要执行一个不同的程序。常见于shell,这种情况下,子进程从fork返回后立即调用exec。
vfork与fork的区别:
vfork用于创建一个新进程,该新进程的目的是exec一个新程序。
1)vfork并不将父进程的地址空间完全复制到子进程中,因为子进程会立即调用exec或exit,于是也就不会存放该地址空间。相反,在子进程调用exec或exit之前,它在父进程的空间中运行。示例如下:
#include "apue.h"
int glob = 6;
int main(void)
{
int var;
pid_t pid;
var = 88;
printf("before fork!\n");
if ((pid = vfork()) < 0)
err_sys("vfork error");
else if (pid == 0)
{
glob++;
var++;
_exit(0);
}
printf("pid = %d, glob = %d, var = %d\n", getpid(), glob, var);
exit(0);
}
[root]# ./a.out
before fork!
pid = 2632, glob = 7, var = 89
子进程对变量glob和var做增1操作,结果改变了父进程中的变量值。因为在调用exec或exit之前,子进程在父进程的地址空间中运行。
2)vfork保证子进程先运行,在它调用exec或exit之后父进程才可能被调度运行。
子进程从父进程继承:
(1)实际用户ID、实际组ID、有效用户ID、有效组ID、附加组ID、进程组ID、会话ID
(2)控制终端、根目录、当前工作目录、文件模式创建屏蔽字
(3)打开的文件描述符、文件描述符执行时关闭标志
(4)信号屏蔽和安排
(5)堆、栈、数据段
(6)内存
(7)进程调度类型
(8)环境
(9)连接的共享存储段、存储映射
子进程与父进程共享:
(1)正文段
(2)字符串常量
子进程不从父进程继承:
(1)进程号
(2)不同的父进程号
(3)tms结构中的系统时间
(4)定时器
(5)文件锁
(6)未决信号
3、exit函数
进程有5种正常终止方式:
1)return语句、
2)exit函数、
3)_exit或_Exit函数
4)进程的最后一个线程再起启动例程中执行return语句,但线程的返回值不会用作进程的返回值,进程是以终止状态0返回。
5)进程的最后一个线程调用pthread_exit()函数。进程的终止状态仍为0
进程有3种异常终止方式:
6)调用abort,产生SIGABRT信号。
7)当进程接收到某些信号
8)最后一个线程对“取消”请求做出响应。
在正常终止情况下,将退出状态作为参数传给三个终止函数exit、_exit、_Exit,最后调用_exit时,内核将退出状态转换成终止状态。
在异常终止情况下,内核(不是进程本身)产生一个指示其异常终止原因的终止状态。
【1】子进程在父进程之前终止
内核为每个终止子进程保存了一定量的信息,所以当终止进程的父进程调用wait或waitpid时,可以得到这些信息——包括:进程ID、进程终止状态、进程使用CPU时间总量等。内核可以释放终止进程所使用的所有存储区,关闭其所有打开文件。
而一个已经终止,但其父进程尚未对齐进行善后处理(获取终止子进程的有关信息,释放它仍占用的资源,即调用wait或waitpid函数)的进程,被称为僵尸进程。
unix提供了一种机制可以保证只要父进程想知道子进程结束时的状态信息,就可以得到。这种机制就是: 在每个进程退出的时候,内核释放该进程所有的资源,包括打开的文件,占用的内存等。但是仍然为其保留一定的信息(包括进程号the process ID,退出状态the termination status of the process,运行时间the amount of CPUtime taken by the process等)。直到父进程通过wait / waitpid来取时才释放。但这样就导致了问题,如果进程不调用wait / waitpid的话, 那么保留的那段信息就不会释放,其进程号就会一直被占用,但是系统所能使用的进程号是有限的,如果大量的产生僵死进程,将因为没有可用的进程号而导致系统不能产生新的进程. 此即为僵尸进程的危害,应当避免。
【2】父进程在子进程之前终止
一个父进程退出,而它的一个或多个子进程仍在运行,那么这些子进程将成为孤儿进程。孤儿进程将被init进程收养,即这些孤儿进程的父进程都改变为init进程。这些被init进程收养的孤儿进程,在终止时会不会变成僵尸进程呢?不会,因为init被编写成无论何时只要有一个子进程终止,init就会调用一个wait函数取得其终止状态。
4、wait函数与waitpid函数
当一个进程正常或异常终止时,内核就向其父进程发送SIGCHLD信号。因为子进程终止是个异步事件,所以这种信号也是内核向父进程发的异步通知。对于这种信号的系统默认动作是忽略它。
(1)wait函数
如果进程由于接收到SIGCHLD信号而调用wait,则可期望wait会立即返回。但是如果在任意时刻调用wait,则进程可能阻塞。
#include <sys/wait.h> pid_t wait(int *statloc); 参数:获取终止进程的终止状态,若不关心终止状态,可将其设为NULL。 返回:若成功则返回终止子进程的ID,若出错则返回-1 |
1)如果它没有任何子进程,则立即出错返回
2)如果一个子进程已经终止,并且是一个僵尸进程,则wait立即返回并取得该子进程的状态。
3)如果其所有子进程都还在运行,则阻塞
#inclue "apue.h"
#include <sys/wait.h>
void pr_exit(int status)
{
if (WIFEXITED(status)) /*若为正常终止子进程返回的状态,则为真*/
printf("normal termination, exit status = %d\n", WEXITSTATUS(status));
else if (WIFSIGNALED(status)) /*若为异常终止子进程返回的状态,则为真*/
printf("abnormal termination, signal number = %d%s\n", WTERMSIG(status),
#ifdef WCOREDUMP
WCOREDUMP(status) ? " (core file generated)" : "");
#else
"");
#endif
else if (WIFSTOPPED(status))
printf("child stopped, signal number = %d\n", WSTOPSIG(status));
}
int main(void)
{
pid_t pid;
int status;
if ((pid = fork()) < 0)
{
err_sys("fork error");
}
else if (pid == 0)
{
exit(7);
}
if (wait(&status) != pid)
{
err_sys("wait error");
}
pr_exit(status);
if ((pid = fork()) < 0)
{
err_sys("fork error");
}
else if (pid == 0)
{
abort();
}
if (wait(&status) != pid)
{
err_sys("wait error");
}
pr_exit(status);
if ((pid = fork()) < 0)
{
err_sys("fork error");
}
else if (pid == 0)
{
status /= 0;
}
if (wait(&status) != pid)
{
err_sys("wait error");
}
pr_exit(status);
exit(0);
}
[root]# ./a.out
normal termination, exit status = 7
abnormal termination, signal number = 6
abnormal termination, signal number = 8
(2)waitpid函数
等待一个指定的进程终止。
#include <sys/wait.h> pid_t waitpid(pid_t pid, int *statloc, int option); 参数pid: pid==-1:等待任一子进程,此时waitpid与wait等效。 pid>0:等待其进程ID与pid相等的子进程 pid==0:等待其组ID等于调用进程组ID的任一子进程 pid<-1:等待其组ID等于pid绝对值的任一子进程 参数statloc:获取终止进程的终止状态,若不关心终止状态,可将其设为NULL。 参数option:该参数是常量WNOHANG和WUNTRACED按或运算,不想用它们则可用0 WNOHANG 若由pid指定的子进程并不是立即可用的,则waitpid不阻塞,此时其返回值为0 WUNTRACED 若实现支持作业控制,pid指定的子进程处于暂停状态,返回其状态 返回:若成功则返回终止子进程的ID,若出错则返回-1 |
static inline pid_t wait(int * wait_stat)
{
return waitpid(-1,wait_stat,0);
}
waitpid
函数提供了
wait
函数没有提供的三个功能:
(1)waitpid可等待一个特定的进程,而wait则返回任一终止子进程的状态。
(2)waitpid提供了一个wait的非阻塞版本
(3)waitpid支持作业控制(利用WCNOTINUED和WUNTRACED选项)
如果一个进程fork一个子进程,但不要它等待子进程终止,也不希望子进程处于僵尸状态直到父进程终止,实现这一要求的技巧是调用fork两次,让子进程的父进程改变为init进程,当该子进程终止时,init进程会调用一个wait函数取得其终止状态。
#include "apue.h"
#include <sys/wait.h>
int main(void)
{
pid_t pid;
if ((pid = fork()) < 0)
{
err_sys("fork error");
}
else if (pid == 0)
{
if ((pid = fork()) < 0)
{
err_sys("fork error");
}
else if (pid > 0)
{
exit(0); /*第1子进程退出*/
}
/*第2子进程的父进程变为init进程,故而在退出时可避免成为僵尸进程*/
sleep(2);
printf("second child, parent id = %d\n", getppid());
exit(0);
}
if (waitpid(pid, NULL, 0) != pid)
err_sys("waitpid error");
exit(0);
}
waitpid(pid, NULL, 0);//阻塞
waitpid(pid, NULL, WNOHANG);//非阻塞
5、exec函数
fork函数创建子进程后,子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程执行的程序完全替换为新程序,而新程序则从main函数开始执行。因为调用exec并不创建新进程,所以前后的进程ID并未改变。exec只是用一个全新的程序替换了当前进程的正文、数据、堆和栈段。因为当前进程的正文都被替换了,所以exec后面的语句就不会执行,除非是exec出错返回-1的情况才会执行。
exec函数有6种不同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[]); 参数: pathname:可执行文件的全路径名 arg0:子进程mian函数的argv[0] 返回值:若出错则返回-1,若成功则不返回值 |
5.1、 6个exec函数之间的关系
在很多UNIX实现中,这6个函数中只有execve是内核的系统调用。另外5个只是库函数,它们最终都要调用该系统调用。
5.2、 6个exec函数之间的区别
区别1:即pathname与filename的区别
前四个函数取路径名作为参数,后两个则取文件名作为参数。
当指定filename作为参数时,如果filename中包含/,则将其视为路径名,否则就按PATH环境变量,在它所指定的各目录中搜寻可执行文件。
如果找到了可执行文件,但是该文件不是有链接编辑器产生的机器可执行文件,则认为该文件是一个shell脚本,于是试着调用/bin/sh,并以该filename作为shell的输入。
区别2:即与参数表的传递有关。(l表示list,v表示矢量vector,e表示环境变量environ)
函数execl、execlp和execle要求蒋欣程序的每个命令行参数都说明为一个单独的参数,且以空指针结尾。
函数execv、execvp和execve要求先构造一个指向各参数的指针数组,然后将该数组地址作为参数。
区别3:即与向新程序传递环境表相关。
函数execle和execve可以传递一个指向环境字符串指针数组的指针,其他4个函数则使用调用进程中的environ变量为新程序复制现有的环境。
5.3、exec函数的使用
示例1:
#include "apue.h"
#include <sys/wait.h>
char *env_init[] = {"USER=unknown", "PATH=/tmp", NULL};
int main(void)
{
pid_t pid;
if ((pid = fork()) < 0)
{
err_sys("fork error");
}
else if (pid == 0)
{
if (execle("/bin/echo", "echo", "myarg1","MY ARG2",(char *)0, env_init) < 0)
err_sys("execle error");
}
if (waitpid(pid, NULL, 0) < 0)
err_sys("wait error");
if ((pid = fork()) < 0)
{
err_sys("fork error");
}
else if (pid == 0)
{
if (execlp("echo", "echo", "only 1 arg", (char *)0) < 0)
err_sys("execlp error");
}
exit(0);
}
[root]# ./a.out
myarg1 MY ARG2
only 1 arg
示例2:
#include <sys/wait.h>
#include <errno.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
void pr_exit(int status)
{
if (WIFEXITED(status))
printf("normal termination, exit status = %d\n", WEXITSTATUS(status));
else if (WIFSIGNALED(status))
printf("abnormal termination, signal number = %d%s\n", WTERMSIG(status),
#ifdef WCOREDUMP
WCOREDUMP(status) ? " (core file generated)" : "");
#else
"");
#endif
else if (WIFSTOPPED(status))
printf("child stopped, signal number = %d\n", WSTOPSIG(status));
}
int system(const char *cmdstring)
{
pid_t pid;
int status;
if (cmdstring == NULL)
return 1;
if ((pid = fork()) < 0)
{
status = -1;
}
else if (pid == 0)
{
execl("/bin/sh", "sh", "-c", cmdstring, (char *)0);
_exit(127);/*execl出错才会执行*/
}
else
{
while (waitpid(pid, &status, 0) < 0)
{
if (errno != EINTR)
{
status = -1;
break;
}
}
}
return status;
}
int main(void)
{
int status;
if ((status = system("date")) < 0)
err_sys("system() error");
pr_exit(status);
if ((status = system("nosuchcmd")) < 0)
err_sys("system() error");
pr_exit(status);
if ((status = system("who; exit 44")) < 0)
err_sys("system() error");
pr_exit(status);
exit(0);
}
[root]# ./a.out
2014年 09月 14日 星期日 07:17:55 CST
normal termination, exit status = 0 /*对于date*/
sh: nosuchcmd: command not found
normal termination, exit status = 127 /*对于无此种命令*/
root pts/0 2014-09-14 07:14 (192.168.0.99)
root pts/1 2014-09-14 07:14 (192.168.0.99)
normal termination, exit status = 44 /*对于exit*/