进程等待、进程替换

目录

进程等待

waitpid函数

wait函数

进程替换


进程等待

进程等待的意义

如果子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏。 另外,进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程。 最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对, 或者是否正常退出。 父进程需要通过进程等待的方式,回收子进程资源,获取子进程退出信息。

waitpid函数

#include <sys/types.h>

#include <sys/wait.h>

pid_ t waitpid(pid_t pid, int *status, int options);

返回值:

  • 当正常返回的时候waitpid返回收集到的子进程的进程PID;
  • 如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;
  • 如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;

参数:

1、pid(判定等待集合的成员)

如果pid=-1,那么等待集合就是由父进程所有的的子进程组成的。与wait等效。

如果pid>0,那么等待集合是一个单独的子进程,它的进程ID等于pid。

2、status(修改默认行为):

如果status参数是非空的,那么waitpid就会在status中放上关于导致返回的子进程的状态信息 。wait.h头文件定义了解释status参数的几个宏:

  • WIFEXITED(status):如果子进程通过调用exit或者 一个返回(return)正常终止,就返回真。
  • WEXITSTATUS(status):返回一个正常终止的子进程的退出状态,只有在WIFEXITED()返回为真时,才会定义这个状态。
  • WIFSIGNALED(status) :如果子进程是因为一个未被捕获的信号终止的,那么就返回真。
  • WTERMSIG( status):返回导致子进程终止的信号的编号。只有在WIFSIGNALED()返回为真时,才定义这个状态。
  • WIFSTOPPED(status):如果引起返回的子进程当前是停止的,那么就返回真。
  • WSTOPSIG(status): 返回导致子进程停止的信号的编号。只有在WIFSTOPPED()返回为真时,才定义这个状态。
  • WIFCONTINUED(status):如果子进程收到SIGCONT信号重新启动,则返回真。

如果调用进程没有子进程,那么waitpid返回-1,并且设置errno为ECHILD。如果waitpid函数被一个信号中断,那么它返回-1,并设置errno为EINTR。

3、options(检查已回收子进程的退出状态):

可以通过将options设置为常量WNOHANG、WUNTRACED和WCONTINUED的各种组合来修改默认行为:

  • WNOHANG:如果等待集合中的任何子进程都还没有终止,那么就立即返回(返回值为0)。默认的行为是挂起调用进程,直到有子进程终止。在等待子进程终止的同时,如果还想做些有用的工作,这个选项会有用。
  • WUNJTRACED: 挂起调用进程的执行,直到等待集合中的一个进程变成已终止或者被停止。返回的PID为导致返回的已终止或被停止子进程的PID。默认的行为是只返同已终止的子进程。当你想要检查已终止和被停止的子进程时,这个选项会有用。
  • WCONTINUED:挂起调用进程的执行,直到等待集合中一个正在运行的进程终止或等待集合中个被停止的进程收到SIGCONT信号重新开始执行。

    可以用或运算把这些选项组合起来。例如:

  • WNOHANG | WUNTRACED:立即返回,如果等待集合中的子进程都没有被停止或终止,则返回值为0;如果有一个停止或终止,则返回值为该子进程的PID。

wait函数

wait函数是waitpid函数的简单版本。

#include <sys/types.h>

#include <sys/wait.h>

pid_t wait(int*status);  

返回值: 成功返回被等待进程pid,失败返回-1。

参数: 输出型参数,获取子进程退出状态,不关心则可以设置成为NULL

调用wait(&status)等价于调用waitpid(-1,&status,0)。

如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退出信息。

如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞。

如果不存在该子进程,则立即出错返回。

父进程按照顺序存储了它的子进程的PID,然后通过用适当的PID作为第一个参数来调用waitpid,按照同样的顺序来等待每个子进程。 

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
 #define N 2
int main()
{                                                                                                                                                                                           
  int status,i;
   pid_t pid[N],retpid;
    //Parent creates N children
   for(i=0;i<N;i++)
      if((pid[i] = fork()) == 0)//child
       exit(100+i);

     //Parent reaps N children in order
     i=0;
    while((retpid = waitpid(pid[i++],&status,0)) > 0)
    {
         if(WIFEXITED(status))
           printf("child %d terminated normally with exit status=%d\n",retpid,WEXITSTATUS(status));
         else 
           printf("child %d terminated abnormally\n",retpid);
     }
 
     the only normal termination is if there are no more children
    if(errno != ECHILD)
      unix_error("waitpid error");
  
     exit(0);                                                       
 } 

当子进程未退出时,父进程都在一直等待子进程退出,在等待期间,父进程不能做任何事情,这种等待叫做阻塞等待。

实际上我们可以让父进程在等待子进程退出期间,执行一些自己的事情,当子进程退出时再读取子进程的退出信息,即非阻塞等待。

这时候我们就可以用到options参数

WNOHANG:如果等待集合中的任何子进程都还没有终止,那么就立即返回(返回值为0)。默认的行为是挂起调用进程,直到有子进程终止。在等待子进程终止的同时,如果还想做些有用的工作,这个选项会有用。

#include <stdio.h> 
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
int main()
{
	pid_t pid;
	pid = fork();
	if (pid < 0) {
		printf("%s fork error\n", __FUNCTION__);
		return 1;
	}
	else if (pid == 0) { //child
		printf("child is run, pid is : %d\n", getpid());
		sleep(5);
		exit(1);
	}
	else {
		int status = 0;
		pid_t ret = 0;
		do
		{
			ret = waitpid(-1, &status, WNOHANG);//非阻塞式等待
			if (ret == 0) {
				printf("child is running\n");
			}
			sleep(1);
		} while (ret == 0);
		if (WIFEXITED(status) && ret == pid) {
			printf("wait child 5s success, child return code is :%d.\n", WEXITSTATUS(status));
		}
		else {
			printf("wait child failed, return.\n");
			return 1;
		}
	}
	return 0;
}

进程替换

exec是程序替换函数,本身并不创建进程,exec生成的进程是当前进程的一个相同副本,pid并没有改变。

替换函数

int execve(const char *path, char *const argv[], char *const envp[]);

如果成功,则不返回,如果错误,则返回-1。 

#include <unistd.h>

int execl(const char *path, const char *arg, ...);

int execlp(const char *file, const char *arg, ...);

int execle(const char *path, const char *arg, ...,char *const envp[]);

int execv(const char *path, char *const argv[]);

int execvp(const char *file, char *const argv[]);

只有execve是真正的系统调用,其它函数都是对execve函数的封装,最终都是调用execve。

这些函数原型看起来很容易混,但只要掌握了规律就很好记。

l(list) : 表示参数采用列表 ;

v(vector) : 参数用数组 ;

p(path) : 有p自动搜索环境变量PATH ;

e(env) : 表示自己维护环境变量 

execl:参数格式为列表;不带路径;使用当前环境变量。

execlp:参数格式为列表;带路径;需要自己组装环境变量。

execle:参数格式为列表;不带路径;使用当前环境变量。

execv:参数格式为数组;不带路径;使用当前环境变量。

execvp:参数格式为数组;带路径;需要自己组装环境变量。

我们可以看到,后来打印的end...并没有被输出,当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,并从新程序的启动例程开始执行。后面的代码属于老代码直接被替换,不会再执行了。 

#include <unistd.h>

int main()

{

        char *const argv[] = {"ls", "-l", NULL};

        char *const envp[] = {"PATH=/bin:/usr/bin", "TERM=console", NULL};

        execl("/bin/ls", "ls", "-l", NULL); // 带p的,可以使用环境变量PATH,无需写全路径         execlp("ls", "ls", "-l", NULL); // 带e的,需要自己组装环境变量

        execle("ls", "ls", "-l", NULL, envp);

        execv("/bin/ls", argv); // 带p的,可以使用环境变量PATH,无需写全路径

        execvp("ls", argv); // 带e的,需要自己组装环境变量

        execve("/bin/ls", argv, envp);

        exit(0);

}  

 

 数组的最后一个元素最好为空。

猜你喜欢

转载自blog.csdn.net/m0_55752775/article/details/130296320