文章目录
- 一、进程等待
- 1.1 意义 / 必要性
- 1.2 进程等待的函数(wait / waitpid)
- 1.3 status参数
- 1.4 获取子进程status
- 1.5 进程的阻塞等待与非阻塞等待
- 二、进程替换
- 2.1 引言
- 2.2 进程替换原理
- 2.3 替换函数
一、进程等待
1.1 意义 / 必要性
为什么要有进程等待? 我们知道,当子进程退出时,如果父进程不对其进行操作,子进程会变为僵尸进程。
而且当子进程结束时,我们如何知道子进程的最后执行情况呢?
父进程通过进程等待,可以:
- 回收子进程资源
- 获取子进程退出信息
- 同步操作
1.2 进程等待的函数(wait / waitpid)
wait函数
pid_t wait(int *status);
- 该函数会暂停当前进程的执行,直到一个子进程结束。
- 如果子进程已经结束,该函数会立即返回。
- 函数返回值:执行成功 返回被等待进程pid,失败返回-1。
- status参数:获取子进程的退出状态,不需要则设为null。
waitpid函数
pid_t waitpid(pid_t pid, int *status, int options);
-
参数:
- pid
pid = -1,表示等待任意子进程
pid = 0,等待进程id=pid 的子进程 - status
WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码) - option
WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID。
- pid
-
该函数会暂停当前进程的执行,直到指定的子进程结束。
-
返回值是结束子进程的进程ID,或者出错时的返回值。
主要的区别和用法总结如下:
- wait函数只能等待任意子进程结束,而waitpid函数可以指定具体的子进程进行等待。
- waitpid函数提供了更多的选项,可以在等待子进程时指定不同的行为。
- waitpid函数可以通过设置options参数来控制等待的方式,例如非阻塞、只等待指定状态等。
- 在多进程的场景中,可以使用waitpid函数更灵活地管理子进程的状态。
1.3 status参数
- wait 和 waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。
- 如果 传递NULL,表示不关心子进程的退出状态信息。
- 否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程
status是int型的整数,用于存储子进程的退出状态信息。其低7位(0-6位)第7位(第7位),其他位(8位及以上),分别存放不同信息。
进程正常终止情况下:
- 低八位(0-7位) 存储的是进程的退出状态。这个状态是由exit系统调用或者C语言中的exit函数传递给操作系统的。
- 第八位:存储是否生成了core dump文件,如果生成了core dump文件,则该位会被置为1。
- 高八位(16-31位) 通常被设置为0,用于标识进程的终止状态。
被信号终止情况下:
- 低八位 :存储的是导致进程终止的信号编号。当进程被信号终止时,状态的低八位会存储信号的编号,例如SIGSEGV表示段错误,SIGKILL表示进程被强制终止等。
- 第8位 :存储是否生成了core dump文件,如果生成了core dump文件,则该位会被置为1。
- 高八位 :通常被设置为0,用于标识进程的终止状态。
我们用位图来解释status
1.4 获取子进程status
我们通过下面一段代码 获取子进程status
// 程序获取进程status
int main() {
pid_t id = fork();
if (id == -1) {
// 创建子进程失败
perror("fork");
exit(EXIT_FAILURE);
} else if (id == 0) {
// 子进程
printf("子进程正在运行\n");
exit(10); // 子进程退出状态设为 10
} else {
// 父进程
int status;
pid_t iid = waitpid(id, &status, 0);
printf("After the wait function\n");
if(iid == id)
{
printf("wait success, pid: %d, status: %d\n", iid, status);
}
sleep(10);
}
return 0;
}
代码执行结果如下:
为什么我们给出退出码exit(10)而最后status值为2560?
根据上面status的存储分析:如果子进程正常退出,status 的值将会是子进程退出码的左移 8 位。
即前八位存储的是退出状态, 0000 1010 0000 0000,当我们返回其十进制后,即为2560。
1.5 进程的阻塞等待与非阻塞等待
意义
进程的阻塞等待和非阻塞等待是指在等待某个事件完成时,进程所采取的不同等待方式。
1. 阻塞等待(Blocking Wait)
- 当一个进程进行阻塞等待时,它会暂时停止执行,并将控制权交给操作系统。进程将进入睡眠状态,直到所等待的事件发生或满足某个条件。
- 在阻塞等待期间,操作系统会将进程从可运行状态转换为阻塞状态,并在事件发生后将进程重新唤醒。
- 阻塞等待是一种同步等待方式,即进程会一直等待直到事件完成或超时。
2. 非阻塞等待(Non-blocking Wait)
- 当一个进程进行非阻塞等待时,它会继续执行其他任务,而不会暂停等待。进程会周期性地检查所等待的事件是否已经发生或满足某个条件。
- 如果事件已经发生,则进程可以立即处理该事件并继续执行。如果事件还未发生,则进程可能会继续轮询等待,或者进行其他操作。
- 非阻塞等待是一种异步等待方式,即进程可以同时进行其他任务,而不必一直等待。
代码实现
1.5.1 进程阻塞等待
下面是代码实现:
int main()
{
pid_t id = fork();
if(id < 0) {
// fork error
perror("fork error");
exit(EXIT_FAILURE);
}
else if(id == 0) {
// 子进程
std::cout << "子进程正在运行, pid: " << getpid() << std::endl;
exit(10);
}
else{
// 父进程
int status;
pid_t rid = waitpid(id, &status, 0); // 阻塞等待,options设为0
std::cout << "after wait" <<std::endl;
if(rid == id && WIFEXITED(status))
{
std::cout << "wait success, pid: " << rid << ", status: " << status << std::endl;
}
else
{
std::cout << "子进程等待失败, return." << std::endl;
return 1;
}
sleep(10);
}
return 0;
}
代码执行结果如下:
1.5.2 进程非阻塞等待
代码实现如下:
int main()
{
pid_t id = fork();
if(id < 0){
// error
perror("fork erorr");
exit(EXIT_FAILURE);
}else if(id == 0){
printf("子进程正在运行,pid:%d \n", getpid());
exit(10);
}else{
int status;
pid_t rid;
do{ // 有子进程退出时 rid返回其pid
pid_t rid = waitpid(id, &status, WNOHANG);
if(rid == 0)
{
printf("child process is running\n");
}
sleep(5);
}while(rid == 0);
if(rid == id && WIFEXITED(status))
{
printf("wait success, child return code is :%d.\n",WEXITSTATUS(status));
}else{
printf("wait child failed, return.\n");
return 1;
}
}
return 0;
}
代码执行结果:
二、进程替换
2.1 引言
首先,我们看下面一段代码:
#include <iostream>
#include <unistd.h>
int main() {
std::cout << "This is the original program." << std::endl;
// 执行程序替换,将当前进程替换为/bin/ls(列出当前目录内容)
char *args[] = {"/bin/ls", "-l", NULL};
execv("/bin/ls", args);
// 如果execv执行成功,下面的代码不会被执行
std::cerr << "Failed to execute /bin/ls!" << std::endl;
return 1;
}
代码的执行结果如下:
我们可以看到,程序运行后,执行了 ls -l
操作,而 Failed to execute /bin/ls!
并没有被打印到下面。
而ls本身也是一个程序,可以理解为,进程 被替换为了 ls 程序
2.2 进程替换原理
根据上面的图,我们分析进程替换原理,进程替换可以理解为下面的步骤:
- 加载新程序映像:操作系统会从磁盘上读取新程序的可执行文件,并将其加载到内存中。
- 创建新的页表:在加载新程序映像时,操作系统会创建一个新的页表来管理新程序所需的内存空间。
- 替换进程内容:一旦新程序映像加载到内存中,操作系统 会将当前进程的内容替换为新程序的内容(代码、数据和堆栈等)
- 开始执行新程序:替换完成后,控制权转移到新程序的入口点,新程序从那里开始执行
2.3 替换函数
像上面代码中所使用的execv
函数,叫做程序替换函数:
下面列举这些程序替换函数,定义如下:
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execve(const char *filename, char *const argv[], char *const envp[]);
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[]);
这些函数都是程序替换函数,它们的区别在于传递参数的方式和环境变量的处理:
- execv:
- 参数:接受一个参数数组 argv,以及程序路径 path。
- 示例:
execv("/bin/ls", argv);
- execvp:
- 参数:与execv 类似,但是可以通过环境变量 PATH 搜索可执行文件。
- 示例:
execvp("ls", argv);
- execve:
- 参数:接受一个参数数组 argv,一个环境变量数组 envp,以及程序路径 filename。
- 示例:
execve("/bin/ls", argv, envp);
- execl:
- 参数:接受多个参数,最后一个参数必须是 NULL,不接受参数数组,需要将每个参数单独列出。
- 示例:
execl("/bin/ls", "ls", "-l", NULL);
- execlp:
- 参数:与execl 类似,但是可以通过环境变量 PATH 搜索可执行文件。
- 示例:
execlp("ls", "ls", "-l", NULL);
- execle:
- 参数:与execl 类似,但是可以传递自定义的环境变量数组 envp。
- 示例:
execle("/bin/ls", "ls", "-l", NULL, envp);