当前位置: 首页 > news >正文

进程相关的系统调用

文章目录

  • 进程
    • 进程相关的系统调用
      • 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

参数statusstatus包含了子进程的退出状态信息,在系统中有几个常用的宏来检查和提取特定的状态值。

  1. WIFEXITED(status): 检查子进程是否正常退出。如果子进程正常结束(通过调用 exit()或调用return),则此宏返回非零值。
  2. WEXITSTATUS(status): 如果子进程正常退出,此宏返回子进程的退出状态码。这是一个整数值,通常用于表示程序执行的结果(可以将exit(EXIT_SUCCESS)这里的宏解析出来)。
  3. WIFSIGNALED(status): 检查子进程是否因接收到信号而终止。如果子进程是由于接收到一个信号而被终止,则此宏返回非零值(例如除0操作会触发信号SIGFPE,造成段错误会触发信号SIGSEGV)。
  4. WTERMSIG(status): 如果子进程是由于接收到一个信号而被终止,此宏返回导致子进程终止的信号编号(会返回终止它的信号的编号,在Linux系统中可以使用kill -l列出当前系统中所有的信号来查看)。
  5. WIFSTOPPED(status): 检查子进程是否被暂停。如果子进程由于接收到一个暂停信号(如 SIGSTOP)而被暂停,则此宏返回非零值。
  6. WSTOPSIG(status): 如果子进程被暂停,此宏返回导致子进程暂停的信号编号(SIGSTOP信号的编号)。

参数options:用于控制waitpid的行为,主要支持WNOHANGWUNTRACED两个选项

  • WNOHANG:若由pid指定的子进程没有退出则立即返回,则waitpid不阻塞,此时返回值为0

  • WUNTRACED:若由pid指定的子进程进入暂停状态时立即返回,并且可以使用WIFSTOPPEDWSTOPSIG判断子进程是否进入到暂停状态并将使子进程进入暂停状态的信号编号解析出来。

  • waitwaitpid函数的区别

    • 在一个子进程终止前,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));		//如果子进程在运行过程中发生过停止就会将使它停止的信号的编号打印出来elseprintf("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函数一样elseprintf("input error,there is no such choice\n");	}else{wait(&status);		//将子进程的退出状态收集,然后写入到status这片空间里,然后使用宏对status进行判断和解析print_exit_mesg_func(status);}return 0;
}

image-20240906214058002

通过执行结果可以发现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));		//如果子进程在运行过程中发生过停止就会将使它停止的信号的编号打印出来elseprintf("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函数一样elseprintf("input error,there is no such choice\n");	}else{waitpid(pid,&status,WUNTRACED);print_exit_mesg_func(status);}return 0;
}

image-20240907114658715

通过waitpid函数指定options选项为WUNTRACED,就可以使WIFSTOPPEDWSTOPSIG能够捕捉到SIGDTOP信号并且将使子进程暂停运行的信号的编号打印出来。如果想要指定父进程为非阻塞模式的话可以使用按位或的方式通过设置options来实现要求。

之前的僵尸进程通过Ctrl+C或者杀死父进程的方法来回收僵尸进程,可以使用wait或者waitpid函数来回收子进程,然后告诉内核将子进程的资源(进程表项)释放掉。

image-20240907162701733

这里的wait(NULL)就是父进程不关心子进程的退出方式,不接收来自子进程的退出状态码。

image-20240907162928612

通过观察可以发现并没有产生僵尸进程,僵尸进程被父进程回收掉了。

exec族函数

上边的父进程用fork函数创建子进程,在实际开发中一般是使用子进程实现和父进程不一样的功能,那么在Linux中有一个系统调用exec可以供用户去调用别的可执行程序。例如在实际开发中父进程去实现一个功能,然后使用子进程去调用别人已经实现的功能,增强自己代码的功能。exec函数一般在子进程中运行,因为如果要添加的功能不知一个,那么可以使用父进程创建多个子进程来实现多个功能,而如果在父进程中执行exec函数则不能实现这样的操作。

特点

  • 在用fork函数创建子进程后,子进程往往要调用一种exec函数以执行另一个程序。
  • 当进程调用一种exec函数时,该进程完全由新程序替换,替换原有进程的代码段,而新程序则从其main函数开始执行。因为调用exec并不创建进程,所以前后的进程ID并未改变。exec知识用另一个新程序替换了当前进程的代码段、数据段、堆、栈。

exec族函数实现流程

exec族函数流程.drawio

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;
}

image-20240907205351773

由编译结果可知execl函数和execlp函数它们的使用区别主要是在是否要指定可执行程序的路径。execl函数可以使用相对路径也可以使用绝对路径,具体使用哪一个主要是根据场景来决定的,相对路径是的解析是针对当前目录来进行查找可执行程序的位置,而绝对路径是从根目录/开始查找的。这样对比下来的话,绝对路径不会受当前工作目录的影响,相对路径如果切换了工作目录以后其指定的参数也需要改变。而execlp函数是根据环境变量来查找可执行程序的路径,可以使用echo $PATH来打印系统的环境变量。虽然说execlp函数使用起来比execl函数方便,但是execlp函数依赖于环境变量,如果环境变量中没有可执行程序的路径,那么当通过子进程执行exec族函数的时候就会出错显示找不到可执行程序的路径,没有此文件。它们的使用方法与execv函数和execvp函数的用法是一样的,execv函数需要指定可执行程序的路径,而execvp函数通过环境变量$PATH来查找可执行程序。

execlexecv函数它们使用的主要区别是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;
}

image-20240908113326596

通过编译执行,发现system函数执行的结果和exec组函数的结果是一样的。这里system函数执行的字符串可以向代码里这样直接写成一个整体,也可以通过后边使用sprintf函数将格式化的数据写入到字符串里然后调用system函数,两种效果是一样的。

进程状态切换

进程状态.drawio


http://www.mrgr.cn/news/28598.html

相关文章:

  • Python 的 Pygame 库,编写简单的 Flappy Bird 游戏
  • 如何在OCI上配置并使用OCI GenAI服务的步骤
  • c++写一个死锁并且自己解锁
  • Go:文件输入输出以及json解析
  • 【LeetCode】【算法】11. 盛最多水的容器
  • openresty入门教程:rewrite_by_lua_block
  • redis实现分布式锁详细教程,可续锁(看门狗)、可重入
  • 鸿蒙读书笔记2:《鸿蒙操作系统设计原理与架构》
  • C++学习笔记----7、使用类与对象获得高性能(二)---- 理解对象生命周期(2)
  • 3176. 求出最长好子序列 I
  • 计算机组成原理——计算机硬件组成与原理
  • Docker 容器网络技术
  • 【例题】lanqiao4425 咖啡馆订单系统
  • 基于python+django+vue的学生管理系统
  • Great_Data
  • Redis 主从复制
  • MaintenanceController
  • 鱼类计数与识别系统源码分享
  • 英语学习之fruit
  • a√跳房子
  • 英语学习之vegetable
  • 设计模式之原型模式
  • 深度揭秘:日志打印的艺术与实战技巧,让你的代码会说话!
  • Vscode 中新手小白使用 Open With Live Server 的坑
  • Java之线程篇四
  • 基于python+django+vue的外卖管理系统