(一)fork
#include <unistd.h>
pid_t fork(void);
返回值:有两个,一个是子进程的ID,另一个为零。当返回值为0时,进入子进程,大于0进入父进程,返回-1时创建进程错误。
子进程与父进程的比较:
(1)子进程与父进程的进程ID不同。且父进程ID不同。
(2)内存布局: 子进程是父进程的副本,其中子进程的数据空间,堆,栈是父进程的副本,但是共享真正文段。
(3)执行顺序:子进程与父进程执行顺序不确定,取决于内核所使用的调度算法。
(4)文件共享:父进程所有打开的文件描述符都会赋值到子进程,父进程与子进程每个相同的打开文件描述符共享一个文件表项。即子进程操作文件直接影响父进程对该文件的操作,最明显的是文件偏移量收到影响。其图如下所示:
例如:
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main()
{
int x = 33,y = 44;
pid_t pid;
if( (pid = fork()) == -1 )
{
perror("fork err\n");
exit(EXIT_FAILURE);
}
else if( pid == 0 )
{
x ++;
y ++;
printf("child : x = %d y = %d\n",x,y);
exit( EXIT_SUCCESS);
}
wait();
printf("parent: x = %d y = %d\n",x,y);
return 0;
}
fork的使用场景:
(1)父进程希望复制自己,使得父进程和子进程同时执行不同的代码段,即在网络服务中是最常见的,父进程等待客户端的服务请求,当请求到来时,父进程fork一个子进程来处理子进程的请求,这时父进程可以继续等待下一个服务请求。
(2)使得子进程执行一个不同的程序,需要fork一个子进程,在此子进程中执行exec执行新的程序。
(二)vfork函数
#include <sys/types.h>
#include <unistd.h>
pid_t vfork(void);
返回值为:与fork几乎相同
与fork函数的不同:
(1)vfork一般是创建一个进程来执行exec函数,即执行一个新的程序。
(2)vfork中,子进程不将父进程的地址空间完全复制到子进程中,其子进程在父进程空间中执行。
(3)vfork保证子进程先运行,父进程等待子进程运行完成后在再运行。
(4)vfork子进程的资源与父进程是共用的,因此父进程中变量经过子进程的对其的改变则父进程也会改变。
例如:
//注意与上个fork的例子进行比较
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main()
{
int x = 33,y = 44;
pid_t pid;
if( (pid = vfork() ) == -1 )
{
perror("fork err\n");
exit(EXIT_FAILURE);
}
else if( pid == 0 )
{
x++;
y++;
printf("child : x = %d y = %d\n",x,y);
exit(0);
}
wait();
printf("parent : x = %d y = %d\n",x,y);
return 0;
}
(三)wait函数
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);
该函数的都是等待子进程的退出,如果成功返回退出进程的ID,如果出错返回-1。
(1)调用wait函数
- 如果所有的子进程都在运行,则此时父进程阻塞。
- 如果一个子进程终止,等待的父进程立即获取其终止状态并立即返回。
- 如果没有任何子进程则出错返回。
(2)获得终止状态
- WIFEXITED(status) 判断子进程是否正常终止。若为正常终止则是真,可以执行WEXITSTATUS(status)来获得子进程传送给exit或者_exit参数低8位。
- WIFSIGNALED(status) 判断是否为异常终止,若为异常终止则为真,可以用WTERMSIG(status)获得子进程终止的信号编号。
- WIFSTOPPED(status) 判断子进程暂停,暂停则是真。可以使用WSTOPSIG(status)获得子进程暂停的信号编号。
例如:
#include <unistd.h>
#include <stdio.h>
#include <stdlib.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\n",WTERMSIG(status));
}
else if( WIFSTOPPED(status) )//子进程暂停
{
printf("child stopped ,signal number = %d\n",WSTOPSIG(status));
}
}
int main()
{
pid_t pid;
int status;
if( ( pid = fork()) == -1 )
{
perror("fork err\n");
exit(EXIT_FAILURE);
}
else if( pid == 0 )
{
exit(7);
}
if( wait(&status) != pid )
{
perror("wait err\n");
}
pr_exit(status);
if( ( pid = fork()) == -1 )
{
perror("fork err\n");
exit(EXIT_FAILURE);
}
else if( pid == 0 )
{
abort();
}
if( wait(&status) != pid )
{
perror("wait err\n");
}
pr_exit(status);
if( ( pid = fork()) == -1 )
{
perror("fork err\n");
exit(EXIT_FAILURE);
}
else if( pid == 0 )
{
status = status / 0;
}
if( wait(&status) != pid )
{
perror("wait err\n");
}
pr_exit(status);
return 0;
}
注意: 如果有多个子进程退出,而需要等待特定的子进程退出时,可以使用wait的返回值与某个期望的ID比较。
(三)waitpid函数
#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *status, int
options);
- pid == -1 等待任一个进程,此情况和wait等效。
- pid > 0 等待进程ID与pid相同的进程
- pid == 0 等待组ID等于调用进程组ID的任一个子进程。
- pid < -1 等待组ID等于pid绝对值的任一个子进程
- options:可以进一步控制waitpid的操作,一般为0。
该函数的都是等待特定子进程的退出,如果成功返回退出进程的ID,如果出错返回-1。
(1)调用waitpid函数
- 等待特定的子进程终止。
- 如果所有的子进程都在运行,则此时父进程阻塞。但是有一个选项可以使得不阻塞
(2)获得终止状态与wait相同
(四)exec函数
#include <unistd.h>
extern char **environ;
int execl(const char *path, const char *arg, ...);
int execv(const char *path, char *const argv[]);
int execle(const char *path, const char *arg,
..., char * const envp[]);
int execlp(const char *file, const char *arg,
...);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[],
char *const envp[]);
(1)当进程调用exec函数时,exec并不创建新的进程,前后ID不改变,该进程执行的程序被完全替代为exec新程序,新的程序从main函数开始执行,exec只是用磁盘上的一个新的程序替换了当前进程的证正文段,数据段,堆段和栈段。
(2)函数区别:
- 字母p代表这些函数取filename作为参数。并且用PATH环境变量寻找可执行文件。
- 字母l表示函数取一个参数表,与字V互斥。
- v表示该函数取argv[]矢量。
- 字母e表示函数取envp[]数组,而不适用当前的环境。
例如:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
char * env_init[] = {"USER=unkonwn","PATH=/tmp",NULL};
int main()
{
pid_t pid;
if( (pid = fork()) == -1 )
{
perror("fork err\n");
exit(EXIT_FAILURE);
}
else if( pid == 0 )
{
if( (execle("/home/kaye/APUE/unit8/show","arg1","arg2","arg3",(char*)0,env_init)) < 0 )
{
perror("execle err\n");
exit(1);
}
}
if( waitpid( pid,NULL,0) != pid )
{
perror("waitpid fail\n");
exit(3);
}
if( (pid = fork()) == -1 )
{
perror("fork err\n");
exit(2);
}
else if( pid == 0 )
{
if( execlp("show","arg4","arg5","arg6",(char *)0) < 0 )
{//必须把该可执行文件的路径加入到环境变量中,这样才能找到该可执行文件
perror("execlp err\n");
exit(3);
}
}
return 0;
}
(五)典型的进程
(1)孤儿进程
产生原因: 父进程终止,子进程会被Init进程收养,此时的子进程就是孤儿进程。
产生过程: 进程终止时,内核逐个检查所有活动进程,以判断他是否是正要终止进程的子进程,如果是,则该进程的父进程,ID就更改为1,这样就保证了每个进程的都有一个父进程。
(2)僵死进程
产生原因:一个进程已经终止,但是其父进程,尚未对其进行善后处理(获取终止子进程的有关信息,释放他仍占用的资源)的进程被称为僵死进程。父进程没有等待取得子进程的终止状态。
避免方法: 内核为每个终止子进程保存了一定量的信息,所以当终止进程的父进程调用wait或waitpid时,可以得到这些信息,这些信息包括进程ID,进程的终止状态,以及进程使用的CPU时间总量。
注意:如果一个孤儿进程终止时会成为僵死进程么?:其实是不会的,原因为init进程中的子进程终止时,init会调用一个wait函数取得其终止状态,这样可以防止产生僵死进程。
例如:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
pid_t pid ;
if( ( pid = fork() ) == -1 )
{
perror("fork err\n");
exit(EXIT_FAILURE);
}
else if( pid == 0 )
{
printf("aaaa\n");
exit(1);
}
else
{
system("ps");//此时僵死进程
wait();
system("ps");//经过wait后,没有僵死进程
sleep (2);
}
return 0;
}