进程退出
进程退出表示进程即将运行结束。在Linux中退出分为正常退出和异常退出。
正常退出:
- 在main函数中执行return
- 调用exit函数
- 调用_exit函数
异常退出:
- 调用abort函数
- 收到某个信号,这个信号是程序终止
退出方式比较
- exit和return的区别:exit是一个函数,有参数;而return是函数执行完后的返回。exit把控制权交给系统,而return将控制权交给调用函数。
- exit和 abort的区别:exit是正常终止进程,而about是异常终止。
- exit(int exit_code): exit中的参数exit_code为0代表进程正常终止,若为其他值表示程序执行过程中有错误发生,比如溢出、除数为0。
- exit()和_exit()的区别:exit在头文件stdlib.h中声明,而_exit()声明在头文件unistd.h中。两个函数均能正常终止进程,但是_exit()会执行后立即返回给内核,而exit()要先执行一些清除操作,然后将控制权交给内核。
父子进程终止的先后顺序不同会产生不同的结果。在子进程退出前父进程先退出,则系统会让 init进程接管子进程(前面有代码演示过)。当子进程先于父进程终止,而父进程又没有调用wait 函数等待子进程结束,子进程进入僵死状态,并且会一直保持下去除非系统重启。子进程处于僵死状态时,内核只保存该进程的一些必要信息以备父进程所需。此时子进程始终占用着资源,同时也减少了系统可以创建的最大进程数。如果子进程先于父进程终止,且父进程调用了wait或waitpid函数,则父进程会等待子进程结束。
执行新程序
使用fork或vfork创建子进程后,子进程通常会调用exec函数来执行另外一个程序。系统调用exec用于执行一个可执行程序以代替当前进程的执行映像。
注意:exec调用并没有生成新进程。一个进程一旦调用exec函数,它本身就“死亡”了,系统把代码段替换成新的程序的代码,废弃原有的数据段和堆栈段,并为新程序分配新的数据段与堆栈段,惟一保留的就是进程ID。也就是说,对系统而言,还是同一个进程,不过执行的已经是另外一个程序了。
Linux下,exec函数族又6种不同的调用形式,如下:
execl
:
int execl(const char *path, const char *arg, ...);
- 这个函数接受一个以空格分隔的参数列表,最后一个参数必须为
NULL
。
execl("/bin/ls", "ls", "-l", (char *)NULL);
execv
:
int execv(const char *path, char *const argv[]);
- 这个函数接受一个参数数组,最后一个元素必须为
NULL
。
char *args[] = {"ls", "-l", NULL}; execv("/bin/ls", args);
execle
:
int execle(const char *path, const char *arg, ..., char *const envp[]);
- 与
execl
相似,但可以指定新程序的环境变量。
execle("/bin/ls", "ls", "-l", (char *)NULL, envp);
execve
:
int execve(const char *path, char *const argv[], char *const envp[]);
- 与
execv
相似,但可以指定新程序的环境变量。
char *args[] = {"ls", "-l", NULL}; execve("/bin/ls", args, envp);
execlp
:
int execlp(const char *file, const char *arg, ...);
- 与
execl
类似,但会在PATH
环境变量指定的目录中查找可执行文件。
execlp("ls", "ls", "-l", (char *)NULL);
execvp
:
int execvp(const char *file, char *const argv[]);
- 与
execv
类似,但会在PATH
环境变量指定的目录中查找可执行文件。
char *args[] = {"ls", "-l", NULL}; execvp("ls", args);
这些函数执行成功时不会返回,而是将当前进程的映像替换为新的程序。如果函数调用失败,它们会返回 -1,并设置
errno
变量以指示错误的原因。
为了更好理解exec,首先要理解环境变量这个概念。Linux引入了环境变量的概念,包括用户的主目录,终端类型、当前目录等,它们定义了用户的工作环境,所以称为环境变量。可以使用env命令查看环境变量值,用户也可以修改这些变量值以定制自己的工作环境
示例程序1
演示环境变量的应用
#include<cstdlib>
#include<malloc.h>
#include<cstring>
#include <cstdio>
#include<ctime>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#include<cerrno>
using namespace std;
extern char **environ;
int main(int argc,char *argv[]){
int i;
printf("Argument:\n");
for(int i=0;i<argc;i++)
printf("argv[%d] is %s\n",i,argv[i]);
printf("Environment:\n");
for(int i=0;environ[i]!=nullptr;i++)
printf("%s\n",environ[i]);
return 0;
}
还有很多环境变量,图片放不下了
命令行里输入命令env,环境变量和程序里应该是一样的
在C语言中,
extern char **environ
是一个用于访问进程环境变量的外部变量声明。这个变量通常由操作系统或C库提供,用于存储当前进程的环境变量。每个C程序在运行时都有一个环境变量表,其中包含了键值对形式的环境变量。
environ
是一个指向字符串指针数组的指针,每个字符串指针指向一个环境变量的字符串。通过访问
environ
变量,你可以迭代这个字符串指针数组,直到遇到一个空指针,表示环境变量列表的结束。每个字符串指针指向一个形如 "key=value" 的字符串,表示一个环境变量。
有一些系统和编译器提供了一个额外的参数,通常称为 envp
,它是一个指向环境变量的字符串数组的指针。
int main(int argc, char *argv[],char **envp);
打印envp,也可以得到环境变量
int main(int argc,char *argv[],char **envp){
int i;
printf("Argument:\n");
for(int i=0;i<argc;i++)
printf("argv[%d] is %s\n",i,argv[i]);
printf("Environment:\n");
for(int i=0;envp[i]!=nullptr;i++)
printf("%s\n",envp[i]);
return 0;
}
这是main函数为上面形式的运行结果,确实可以打印环境变量
事实上无论是哪个exec函数,都是将可执行程序的路径、命令行参数和环境变量3个参数传递给可执行程序的main函数。
上面说到如果失败,即遇到错误的事件,exec函数会返回-1,以下是一些错误
在 Linux 操作系统下,exec函数族可以执行二进制的可执行文件,也可以执行Shell脚本程序,但Shell脚本必须以下面所示的格式开头:第一行必须为:#! interpretername [arg]。其中 interpretername可以是Shell或其他解释器,例如,/bin/sh 或usr/bin/perl,arg是传递给解释器的参数。
示例程序2
演示exec函数的用法
processimage.cpp
这里写的是子进程到时候执行exec后执行的代码
#include<cstdio>
#include<sys/types.h>
#include<unistd.h>
int main(int argc,char *argv[],char **environ){
int i;
printf("I am a process image!\n");
printf("My pid= %d,parent=%d\n",getpid(),getppid());
printf("uid = %d,gid = %d\n",getuid(),getgid());
for(i=0;i<argc;i++)
printf("argv[%d]:%s\n",i,argv[i]);
}
getuid
和getpid
是两个与进程和用户身份相关的系统调用函数。
getuid
函数:
uid_t getuid(void);
- 用于获取当前进程的用户实际用户标识(User ID)。返回值是用户的实际用户ID。
getpid
函数:
pid_t getpid(void);
- 用于获取当前进程的进程ID。返回值是当前进程的进程ID。
execve.cpp
#include<cstdio>
#include<sys/types.h>
#include <sys/wait.h>
#include<unistd.h>
#include<cstdlib>
int main(int argc,char *argv[],char **environ){
pid_t pid;
int stat_val;
printf("Exec example\n");
pid=fork();
switch (pid)
{
case -1:
perror("Process Creation failed\n");
exit(1);
case 0:
printf("Child process is running\n");
printf("My pid =%d,parentpid= %d\n",getpid(),getppid());
printf("uid=%d,gid=%d\n",getuid(),getgid());
execve("processimage",argv,environ);
printf("process never go to here!\n");//是不会允许到这一句的
default:
printf("Parent process is running\n");
break;
}
wait(&stat_val);
exit(0);
}
注意顺序,先编译第一个程序,而且可执行文件名称要对应execve的参数
可以看到新程序进程的pid,ppid,uid和gid都保持了原来子进程的。调用execve之后,原有的子进程的映像被替代,所以那句打印永远不会执行
wait
函数的原型如下:
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);
pid_t
是进程ID的数据类型,通常是整数。int *status
是一个指向整数的指针,用于存储子进程的终止状态。
wait
函数用于等待任意一个子进程的退出,并获取子进程的退出状态。其返回值是已经终止的子进程的进程ID。如果调用失败,返回值为 -1。
status
参数用于存储有关子进程终止状态的信息。如果不关心子进程的退出状态,可以将status
设置为NULL
。如果status
不是NULL
,则可以使用一些宏来检查status
中的信息,例如:
WIFEXITED(status)
:如果子进程正常终止(不是信号终止),返回非零。WEXITSTATUS(status)
:获取子进程的退出状态,只有在WIFEXITED(status)
为真时才有效。
执行新程序后的进程除了保持了原来的进程ID、父进程ID、实际用户ID和实际组ID之外,进程还保持了许多原有特征,主要有。
- 当前工作目录。
- 根目录。
- 创建文件时使用的屏蔽字。
- 进程信号屏蔽字。
- 未决警告。
- 和进程相关的使用处理器的时间。
- 控制终端。
- 文件锁。
等待进程结束
之前提到的僵死状态,如果父进程调用了wait或waitpid,就不会使子进程编程僵尸进程
#include<sys/types.h>
#include <sys/wait.h>
pid_t wait(int *statloc);
pid_t waitpid(pid_t pid, int *statloc,int options);
wait函数使父进程暂停执行,直到它的一个子进程结束为止。该函数的返回值是终止运行的子进程的PID。参数 statloc所指向的变量存放子进程的退出码(注意statloc本身不等于退出码),即从子进程的main 函数返回的值或子进程中exit函数的参数。如果statloc不是一个空指针,状态信息将被写入它指向的变量。
头文件sys/wait.h中定义了解读进程退出状态的宏。
注:status就是通过statloc返回来的值
WIFEXITED(status)
:
- 如果子进程正常终止(不是由于信号),则返回非零值。
WEXITSTATUS(status)
:
- 如果
WIFEXITED(status)
为真,该宏返回子进程的退出状态。这是子进程传递给exit
或_exit
函数的值。
WIFSIGNALED(status)
:
- 如果子进程是因为信号而终止,而不是正常退出,则返回非零值。
WTERMSIG(status)
:
- 如果
WIFSIGNALED(status)
为真,该宏返回导致子进程终止的信号编号。
WIFSTOPPED(status)
:
- 如果子进程当前已经停止,则返回非零值。这通常是由于接收到一个信号,要求进程停止执行。
WSTOPSIG(status)
:
- 如果
WIFSTOPPED(status)
为真,该宏返回导致子进程停止的信号编号。
WIFCONTINUED(status)
:
- 如果子进程由于接收到
SIGCONT
信号而继续运行,则返回非零值。
waitpid也用来等待子进程的结束,但它用于等待某个特定进程结束。参数pid指明要等待的子进程的PID。 参数 statloc的含义与wait函数中的statloc相同。options参数允许用户改变waitpid的行为,若将该参数赋值为WNOHANG,则使父进程不被挂起而立即返回并执行其后的代码。
下面是
waitpid
函数中pid
参数不同取值的意义:
pid > 0:
- 表示等待具有进程ID为
pid
的子进程。- 如果该子进程还没有退出,则父进程会阻塞等待,直到子进程退出为止。
- 如果子进程已经退出,父进程会立即返回。
pid == -1:
- 表示等待任意子进程,类似于
wait
函数。- 父进程会等待第一个终止的子进程,无论其进程ID是多少。
pid == 0:
- 表示等待与调用
waitpid
的父进程在同一个进程组的任意子进程。- 这对于等待同一作业中的任意子进程很有用。
pid < -1:
- 表示等待进程组ID为
pid
的任意子进程。- 这对于等待特定进程组中的任意子进程很有用。
以下是一些常用的
waitpid
选项及其对应的宏:
WNOHANG:
- 启用非阻塞模式,即如果没有子进程退出,则立即返回,而不会阻塞父进程。
WUNTRACED:
- 用于获取已经停止但尚未进入终止状态的子进程的状态信息。
WCONTINUED:
- 用于获取已经继续运行的子进程的状态信息。
这些宏可以与位运算结合使用,以同时指定多个选项。
如果想让父进程周期性地检查某个特定的子进程是否已经退出,可以按如下方式调用waitpid。
waitpid(child_pid, NULL,WNOHANG);
如果子进程尚未退出,它将返回0;如果子进程已经结束,则返回child _pid。调用失败时返回-1。失败的原因包括没有该子进程、参数不合法等。
注意:wait等待第一个终止的子进程,而 waitpid则可以指定等待特定的子进程。waitpid 提供了一个wait的非阻塞版本。有时希望取得一个子进程的状态,但不想使父进程阻塞,waitpid 提供了一个这样的选项: WNOHANG,它可使调用者不阻塞。如果一个没有任何子进程的进程调用wait函数,会立即出错返回。
示例程序3
演示wait的使用和子进程退出码的获得
#include<cstdlib>
#include<malloc.h>
#include<cstring>
#include <cstdio>
#include<ctime>
#include<sys/types.h>
#include<sys/stat.h>
#include<sys/wait.h>
#include<fcntl.h>
#include<unistd.h>
#include<cerrno>
using namespace std;
int main(){
pid_t pid;
const char *msg;
int k;
int exit_code;
printf("Study how to get exit code\n");
pid=fork();
switch (pid)
{
case 0:
msg="Child process is running";
k=5;
exit_code=37;//这就是子进程的退出码
break;
case -1:
perror("Process creation failed\n");
exit(1);
default:
exit_code=0;
break;
}
if(pid!=0){//父进程会执行这里
int stat_val;
pid_t child_pid;
child_pid=wait(&stat_val);//父进程暂停等待子进程结束
printf("Child process has exited,pid=%d\n",child_pid);
if(WIFEXITED(stat_val))//通过这个判断子进程是否是正常结束的
printf("Child exited with code %d\n",WEXITSTATUS(stat_val));//获得退出码
else printf("Child exited abnormally\n");
}
else{//子进程会执行这里
while (k-->0)
{
puts(msg);
sleep(1);
}
}
exit(exit_code);
}
运行结果: