文章目录
- 进程
- 进程相关的系统调用
- wait函数
- waitpid函数
- 示例--使用wait fork函数创建子进程并使用宏验证子进程的退出状态信息
- 示例--使用waitpid函数检测子进程是否进入暂停状态
- exec族函数
- 示例--exec族函数的使用
- system函数
- 示例--使用system函数执行外部指令
- 进程状态切换
进程
进程相关的系统调用
wait函数
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);
//功能:等待子进程退出并回收,防止僵尸进程产生
//参数:指向status的指针,用于存储子进程的退出状态信息
//返回值:成功执行返回子进程ID,出错返回-1
waitpid函数
#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid(pid_t pid,int *status,int options);
//功能:等待指定子进程退出并回收,防止僵尸进程产生
//参数1:要等待的子进程的进程标识符
//参数2:指向status的指针,用于存储子进程的退出状态信息
//参数3:选项标志,用于控制waitpid的行为
//返回值:成功执行返回子进程的ID,出错返回-1
参数status
:status
包含了子进程的退出状态信息,在系统中有几个常用的宏来检查和提取特定的状态值。
WIFEXITED(status)
: 检查子进程是否正常退出。如果子进程正常结束(通过调用exit()
或调用return
),则此宏返回非零值。WEXITSTATUS(status)
: 如果子进程正常退出,此宏返回子进程的退出状态码。这是一个整数值,通常用于表示程序执行的结果(可以将exit(EXIT_SUCCESS)
这里的宏解析出来)。WIFSIGNALED(status)
: 检查子进程是否因接收到信号而终止。如果子进程是由于接收到一个信号而被终止,则此宏返回非零值(例如除0操作会触发信号SIGFPE
,造成段错误会触发信号SIGSEGV
)。WTERMSIG(status)
: 如果子进程是由于接收到一个信号而被终止,此宏返回导致子进程终止的信号编号(会返回终止它的信号的编号,在Linux系统中可以使用kill -l
列出当前系统中所有的信号来查看)。WIFSTOPPED(status)
: 检查子进程是否被暂停。如果子进程由于接收到一个暂停信号(如SIGSTOP
)而被暂停,则此宏返回非零值。WSTOPSIG(status)
: 如果子进程被暂停,此宏返回导致子进程暂停的信号编号(SIGSTOP
信号的编号)。
参数options
:用于控制waitpid
的行为,主要支持WNOHANG
和WUNTRACED
两个选项
-
WNOHANG
:若由pid
指定的子进程没有退出则立即返回,则waitpid
不阻塞,此时返回值为0 -
WUNTRACED
:若由pid
指定的子进程进入暂停状态时立即返回,并且可以使用WIFSTOPPED
和WSTOPSIG
判断子进程是否进入到暂停状态并将使子进程进入暂停状态的信号编号解析出来。 -
wait
和waitpid
函数的区别- 在一个子进程终止前,
wait
使其调用者(父进程)阻塞,父进程会每隔一段时间检查子进程是否退出,如果退出就通知内核去释放子进程的资源 waitpid
有一个选项,可使调用者不阻塞waitpid
等待一个指定的子进程,而wait
等待所有的子进程,返回任一终止子进程的状态
- 在一个子进程终止前,
示例–使用wait fork函数创建子进程并使用宏验证子进程的退出状态信息
#include "header.h"
void normal_exit_func()
{
exit(EXIT_SUCCESS);
}
void abnormal_exit_func()
{
char *p = NULL;
*p = 'c';
puts(p); //对空指针操作肯定会造成段错误,由此引发SIGSEGV信号
}
void print_exit_mesg_func(int status)
{
if(WIFEXITED(status))
printf("child process exit normally and the status is %d\n",WEXITSTATUS(status)); //正常退出会将退出码打印出来
else if(WIFSIGNALED(status))
printf("child process exit abnormally and the signal number is %d\n",WTERMSIG(status)); //如果是异常导致信号退出的话,会将信号的编号打印出来
else if(WIFSTOPPED(status))
printf("child process stopped by the signal number is %d\n",WSTOPSIG(status)); //如果子进程在运行过程中发生过停止就会将使它停止的信号的编号打印出来
else
printf("unknown status\n");
}
int main(int argc, char **argv)
{
if(argc < 2)
{
fprintf(stderr,"usage: %s [normal | abnormal | stop]\n",argv[0]);
exit(EXIT_FAILURE);
}
pid_t pid;
int status;
pid = fork();
if(pid < 0)
{
perror("fork error");
exit(EXIT_FAILURE);
}
else if(pid == 0)
{
printf("child pid is %d and the parent pid is %d\n",getpid(),getppid());
if(!(strcmp(argv[1],"normal"))) //根据外部传参来确定子进程的退出方式
normal_exit_func();
else if(!(strcmp(argv[1],"abnormal")))
abnormal_exit_func();
else if(!strcmp(argv[1],"stop"))
pause(); //pause函数会让它一直卡着,作用和while加sleep函数一样
else
printf("input error,there is no such choice\n");
}
else
{
wait(&status); //将子进程的退出状态收集,然后写入到status这片空间里,然后使用宏对status进行判断和解析
print_exit_mesg_func(status);
}
return 0;
}
通过执行结果可以发现wait
函数能通过status
获取到子进程的退出状态,代码里正常退出使用的是EXIT_SUCCESS
这个宏,经过比对发现正确解析出了宏。当传入abnormal
参数的时候,子进程会去执行会造成段错误的代码,然后会收到信号处理机制的编号,这里段错误对应的信号编号是11,通过kill -l
查询编号为11的就是SIGSEGV
信号,执行正确。至于使用信号使子进程停止运行的功能,wait
函数做不到,必须使用waitpid
函数。
示例–使用waitpid函数检测子进程是否进入暂停状态
#include "header.h"
void normal_exit_func()
{
exit(EXIT_SUCCESS);
}
void abnormal_exit_func()
{
char *p = NULL;
*p = 'c';
puts(p); //对空指针操作肯定会造成段错误,由此引发SIGSEGV信号
}
void print_exit_mesg_func(int status)
{
if(WIFEXITED(status))
printf("child process exit normally and the status is %d\n",WEXITSTATUS(status)); //正常退出会将退出码打印出来
else if(WIFSIGNALED(status))
printf("child process exit abnormally and the signal number is %d\n",WTERMSIG(status)); //如果是异常导致信号退出的话,会将信号的编号打印出来
else if(WIFSTOPPED(status))
printf("child process stopped by the signal number is %d\n",WSTOPSIG(status)); //如果子进程在运行过程中发生过停止就会将使它停止的信号的编号打印出来
else
printf("unknown status\n");
}
int main(int argc, char **argv)
{
if(argc < 2)
{
fprintf(stderr,"usage: %s [normal | abnormal | stop]\n",argv[0]);
exit(EXIT_FAILURE);
}
pid_t pid;
int status;
pid = fork();
if(pid < 0)
{
perror("fork error");
exit(EXIT_FAILURE);
}
else if(pid == 0)
{
printf("child pid is %d and the parent pid is %d\n",getpid(),getppid());
if(!(strcmp(argv[1],"normal"))) //根据外部传参来确定子进程的退出方式
normal_exit_func();
else if(!(strcmp(argv[1],"abnormal")))
abnormal_exit_func();
else if(!strcmp(argv[1],"stop"))
pause(); //pause函数会让它一直卡着,作用和while加sleep函数一样
else
printf("input error,there is no such choice\n");
}
else
{
waitpid(pid,&status,WUNTRACED);
print_exit_mesg_func(status);
}
return 0;
}
通过waitpid
函数指定options
选项为WUNTRACED
,就可以使WIFSTOPPED
和WSTOPSIG
能够捕捉到SIGDTOP
信号并且将使子进程暂停运行的信号的编号打印出来。如果想要指定父进程为非阻塞模式的话可以使用按位或的方式通过设置options
来实现要求。
之前的僵尸进程通过Ctrl+C
或者杀死父进程的方法来回收僵尸进程,可以使用wait
或者waitpid
函数来回收子进程,然后告诉内核将子进程的资源(进程表项)释放掉。
这里的wait(NULL)
就是父进程不关心子进程的退出方式,不接收来自子进程的退出状态码。
通过观察可以发现并没有产生僵尸进程,僵尸进程被父进程回收掉了。
exec族函数
上边的父进程用fork
函数创建子进程,在实际开发中一般是使用子进程实现和父进程不一样的功能,那么在Linux中有一个系统调用exec
可以供用户去调用别的可执行程序。例如在实际开发中父进程去实现一个功能,然后使用子进程去调用别人已经实现的功能,增强自己代码的功能。exec
函数一般在子进程中运行,因为如果要添加的功能不知一个,那么可以使用父进程创建多个子进程来实现多个功能,而如果在父进程中执行exec
函数则不能实现这样的操作。
特点
- 在用
fork
函数创建子进程后,子进程往往要调用一种exec函数以执行另一个程序。 - 当进程调用一种
exec
函数时,该进程完全由新程序替换,替换原有进程的代码段,而新程序则从其main函数开始执行。因为调用exec
并不创建进程,所以前后的进程ID并未改变。exec
知识用另一个新程序替换了当前进程的代码段、数据段、堆、栈。
exec族函数实现流程
exec族函数
#include <unistd.h>
int execl(const char *path, const char *arg0, ...); //库函数
int execlp(const char *file, const char *arg0, ...); //库函数
int execv(const char *path, char *const argv[]); //库函数
int execvp(const char *file, char *const argv[]); //库函数
int execle(const char *path, const char *arg0, ..., char *const envp[]); //库函数
int execve(const char *path, char *const argv[], char *const envp[]); //系统调用
int execvpe(const char *file, char *const argv[], char *const envp[]); //库函数
//返回值:出错返回-1,成功不返回
参数
path
参数是一个字符串,表示要执行的可执行文件的路径。
arg...
参数是一个字符串,表示新程序的名称。
envp
参数是一个字符串数组,每个字符串表示一个环境变量,以NULL作为结束标志。
file
参数是一个字符串,表示要执行的可执行文件的名称。
argv
参数是一个字符串数组,每个字符串表示一个命令行参数,以NULL作为结束标志。
exec族函数的后缀
l
(list): 表示后面的参数列表是要传递给新程序的参数列表,参数列表的第一个参数必须是可执行程序的路径,最后一个参数必须是 NULL 结尾表示参数传递完毕。
v
(vector): 表示参数通过一个指针数组传递,其实就是把上边的参数列表构建成一个指针数组然后传递给新程序的argv
,第一个参数必须是可执行程序的路径,最后一个参数必须是 NULL 结尾表示参数传递完毕。
p
(path): 表示在系统环境变量 $PATH
指定的目录中搜索可执行文件。
e
(environment): 表示提供一份环境变量列表,这份列表是一个包含指向每个环境变量字符串的指针数组。
返回值
exec
函数如果成功执行,会将子进程的代码段、数据段、堆、栈全部替换为新程序的。而且执行成功不返回,因为调用成功后之前子进程的代码不复存在,所以它不会返回。而如果exec
函数执行出错,返回-1,可以继续执行之前的代码,但是执行出错以后一般都使用perror
函数查看出错的原因,并且使用exit
函数使子进程退出运行。
示例–exec族函数的使用
#include "header.h"
int main(void)
{
char *cmd1 = "/bin/ls";
char *cmd2 = "ls";
char *argv1 = "/home/dx";
char *argv2 = "/";
pid_t pid;
/**************execl函数******************/
if((pid = fork()) < 0)
{
perror("Fork failed");
exit(EXIT_FAILURE);
}
else if(pid == 0)
{
printf("this is the child process and the pid is %d,parent pid is %d\n",getpid(),getppid());
if(execl(cmd1, cmd1, argv1, argv2, NULL) < 0)
{
perror("execl error");
exit(EXIT_FAILURE);
}
}
else
{
printf("this is parent process and the pid is %d,the child process is %d\n",getpid(),pid);
wait(NULL); //等待子进程退出,防止子进程如果execl失败变成一个僵尸进程
}
/**************execlp函数******************/
if((pid = fork()) < 0)
{
perror("Fork failed");
exit(EXIT_FAILURE);
}
else if(pid == 0)
{
printf("this is the child process and the pid is %d,parent pid is %d\n",getpid(),getppid());
if(execlp(cmd2, cmd2, argv1, argv2, NULL) < 0)
{
perror("execlp error");
exit(EXIT_FAILURE);
}
}
else
{
printf("this is parent process and the pid is %d,the child process is %d\n",getpid(),pid);
wait(NULL); //等待子进程退出,防止子进程如果execl失败变成一个僵尸进程
}
/**************execv函数******************/
char *argvs[] = {cmd1,argv1,argv2,NULL};
if((pid = fork()) < 0)
{
perror("Fork failed");
exit(EXIT_FAILURE);
}
else if(pid == 0)
{
printf("this is the child process and the pid is %d,parent pid is %d\n",getpid(),getppid());
if(execv(cmd1, argvs) < 0)
{
perror("execv error");
exit(EXIT_FAILURE);
}
}
else
{
printf("this is parent process and the pid is %d,the child process is %d\n",getpid(),pid);
wait(NULL); //等待子进程退出,防止子进程如果execl失败变成一个僵尸进程
}
/**************execvp函数******************/
char *argvp[] = {cmd2,argv1,argv2,NULL};
if((pid = fork()) < 0)
{
perror("Fork failed");
exit(EXIT_FAILURE);
}
else if(pid == 0)
{
printf("this is the child process and the pid is %d,parent pid is %d\n",getpid(),getppid());
if(execvp(cmd2, argvp) < 0)
{
perror("execvp error");
exit(EXIT_FAILURE);
}
}
else
{
printf("this is parent process and the pid is %d,the child process is %d\n",getpid(),pid);
wait(NULL); //等待子进程退出,防止子进程如果execl失败变成一个僵尸进程
}
return 0;
}
由编译结果可知execl
函数和execlp
函数它们的使用区别主要是在是否要指定可执行程序的路径。execl
函数可以使用相对路径也可以使用绝对路径,具体使用哪一个主要是根据场景来决定的,相对路径是的解析是针对当前目录来进行查找可执行程序的位置,而绝对路径是从根目录/
开始查找的。这样对比下来的话,绝对路径不会受当前工作目录的影响,相对路径如果切换了工作目录以后其指定的参数也需要改变。而execlp
函数是根据环境变量来查找可执行程序的路径,可以使用echo $PATH
来打印系统的环境变量。虽然说execlp
函数使用起来比execl
函数方便,但是execlp
函数依赖于环境变量,如果环境变量中没有可执行程序的路径,那么当通过子进程执行exec
族函数的时候就会出错显示找不到可执行程序的路径,没有此文件。它们的使用方法与execv
函数和execvp
函数的用法是一样的,execv
函数需要指定可执行程序的路径,而execvp
函数通过环境变量$PATH
来查找可执行程序。
execl
和execv
函数它们使用的主要区别是execv
将参数构建成了一个指针数组argv
然后使用,而execl
函数使用的是参数列表。这个用法与execlp
函数和execvp
函数的用法一样。都是将参数列表构建成一个指针数组argv
然后传给调用的可执行程序里main
函数的argv
。
system函数
通过上边的exec
族函数可以在子进程中启动另外的可执行程序,方便增加程序的功能。但是在调用过程中发现使用exec
族函数是有一点繁琐的。在Linux系统中提供了一个和exec
族函数功能一样的一个库函数调用system
,它的底层实现就是使用exec
族函数,只不过系统已经帮我们封装好了,我们直接使用它即可。
#include <stdlib.h>
int system(const char *command);
//功能:执行系统命令,这些命令可以是内置命令、可执行文件或脚本。
//参数:一个命令字符串(可以是由用户自己构建的)
//返回值:如果命令执行成功,返回值是shell指令的退出码,如果调用失败,返回-1并设置errno
示例–使用system函数执行外部指令
#include "header.h"
void mysystem_func(char *cmd)
{
pid_t pid;
if((pid = fork()) < 0)
{
perror("fork error");
exit(EXIT_FAILURE);
}
else if(pid == 0)
{
char *buffer[] = {"bash","-c",cmd,NULL}; //这里的参数bash表示使用bash来解析命令行,-c参数表示后面的字符串作为命令来执行
if(execvp("bash",buffer) < 0) //这里的execvp函数表明将后边的参数构建出一个指针数组,然后当作参数使用
//由于这里使用的是p,所以会默认在系统环境变量里查找指定的可执行程序
{
perror("execlp error");
exit(EXIT_FAILURE);
}
}
else
{
wait(NULL);
}
}
int main(void)
{
char *cmd1 = "ls";
char *cmd2 = "ls -l > ls.log";
system(cmd1);
mysystem_func(cmd2);
return 0;
}
通过编译执行,发现system
函数执行的结果和exec
组函数的结果是一样的。这里system
函数执行的字符串可以向代码里这样直接写成一个整体,也可以通过后边使用sprintf
函数将格式化的数据写入到字符串里然后调用system
函数,两种效果是一样的。