一、waitpid
1. 如何获得对应退出码?
( status >> 8 ) & 0xFF 因为正常退出码是低16位,下标15 ~ 8对应的元素。
子进程退出的时候,不可以使用全局变量来获取子进程的退出码,因为进程具有独立性,地址一样但内容不同,所以只能通过系统调用获取子进程的退出信息。
2. 进程退出
退出码判定:(1)代码跑完,结果对,return 0;(2)代码跑完,结果不对,return 非0。
进程异常:OS提前使用信号终止了进程。进程退出信息中,会记录自己的退出信号。
$ kill -l
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR
31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3
38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7
58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
63) SIGRTMAX-1 64) SIGRTMAX
信号为 0 说明代码跑完,至于结果正确与否还需继续通过退出码判定。
当进程退出的时候,实际上它的退出码和退出信号都会在进程的PCB中维护。
WIFEXITED(status):若为正常终止子进程返回的状态,则为真(查看进程是否是正常退出)
WEXITSTATUS(status):若WIFEXITED非零,提取子进程退出码(查看进程的退出码)
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
int main(){
pid_t id = fork();
if(id < 0){
printf("errnum: %d, errstr: %s\n", errno, strerror(errno));
return errno;
}
else if(id == 0){
int cnt = 4;
for(; cnt > 0; --cnt){
printf("child process, pid: %d, ppid: %d\n", getpid(), getppid());
sleep(1);
}
exit(1);
}
else{
sleep(10);
int sta = 0;
pid_t rid = waitpid(id, &sta, 0);
if(rid > 0){
if(WIFEXITED(sta)){
printf("wait child process, rid: %d, status code: %d, raw status: %d\n", rid, WEXITSTATUS(sta), sta);
}
else{
printf("child process quits error!\n");
}
//printf("wait child process, rid: %d, status code: %d\n", rid, ((sta >> 8) & 0xFF));
}
else{
perror("waitpid");
}
while(1){
printf("parent process, pid: %d\n", getpid());
sleep(1);
}
}
return 0;
}
为何原始的 sta 值打印出来是 256?
- 子进程调用 exit(1):
子进程调用 exit(1) 表示正常退出,退出状态码为1。- 父进程调用 waitpid():
waitpid() 返回子进程的退出状态码,这个状态码是一个16位的整数。退出状态码1会被左移8位,存储在高8位中。因此,16位的状态码实际上是 0x0100(即十进制的256)。- 使用 WEXITSTATUS() 宏:
WEXITSTATUS() 宏的作用是从16位的状态码中提取出子进程的退出状态码。WEXITSTATUS() 宏的定义通常是 (status >> 8) & 0xFF,即将状态码右移8位,然后与 0xFF 进行按位与操作,从而提取出高8位的值。因此,WEXITSTATUS(0x0100) 的结果是1。
二、阻塞和非阻塞的问题
可以创建子进程,让子进程专门负责拷贝,而且fork()之后相当于对数据做了一次“快照”,不受父进程更改的影响。
#include <iostream>
#include <vector>
#include <cstdio>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
using namespace std;
vector<int> data;
const string gsep = " ";
enum{
OK = 0,
OPENFILE_ERR,
};
int SaveBegin()
{
string name = to_string(time(nullptr));
name += ".backup";
FILE *fp = fopen(name.c_str(), "w");
if(nullptr == fp)
return OPENFILE_ERR;
string datastr;
for(auto e : data)
{
datastr += to_string(e);
datastr += gsep;
}
fputs(datastr.c_str(), fp);
fclose(fp);
return OK;
}
void Save()
{
pid_t id = fork();
if(0 == id)
{
int scode = SaveBegin();
exit(scode);
}
int status = 0;
pid_t rid = waitpid(id, &status, 0);
if(0 < rid)
{
int code = WEXITSTATUS(status);
if(0 == code)
{
printf("备份成功,退出码:%d\n", code);
}
else
{
printf("备份失败,退出码:%d\n", code);
}
}
else
{
perror("waitpid");
}
}
int main()
{
int cnt = 1;
while(true)
{
data.push_back(cnt++);
sleep(1);
if(cnt % 10 == 0)
{
Save();
}
}
return 0;
}
等待的状态跟 waitpid 最后一个参数 int options 有关:
0:阻塞等待
WNOHANG:非阻塞等待
一般由自己循环调用非阻塞接口,完成轮询检测。在轮询检测期间可以做自己的事情。
服务器挂起(Hang):
通常是指服务器仍然在运行,但是无法响应用户的请求或者无法执行新的任务。服务器看起来像是停止了工作,但实际上它的进程可能还在运行。服务器宕机(Down):
指服务器完全停止工作,无法提供任何服务。这通常意味着服务器上的所有进程都已停止,并且可能无法通过远程方式重新启动。
waitpid 如果返回
> 0 说明 等待成功,得到目标子进程的 pid
== 0 说明 等待成功,但是子进程没有退出
< 0 说明 等待失败
非阻塞轮询
Non-blocking Polling
程序定期检查某个条件或状态,而不会因为等待该条件或状态而阻塞。
这种技术通常用于需要在等待某个事件的同时继续执行其他任务的场景。
父进程在一个无限循环中定期检查子进程的状态。
每次循环中,父进程调用 waitpid 检查子进程是否已经退出。
如果子进程已经退出,父进程打印相关信息并退出主循环。
如果子进程还在运行,父进程执行任务列表中的所有任务。
main.cc
#include <iostream>
#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <functional>
#include <vector>
#include "task.h"
typedef std::function<void()> task_t;
void Loadtask(std::vector<task_t> &tasks)
{
tasks.push_back(PrintLog);
tasks.push_back(Download);
tasks.push_back(BackUp);
}
int main()
{
std::vector<task_t> tasks;
Loadtask(tasks);
pid_t id = fork();
if (0 == id)
{
while (true)
{
printf("child proc, pid: %d\n", getpid());
sleep(1);
}
exit(0);
}
while (true)
{
sleep(1);
pid_t rid = waitpid(id, nullptr, WNOHANG);
if (0 < rid)
{
printf("waiting for the child proc (%d) to succeed\n", rid);
break;
}
else if (0 > rid)
{
printf("waiting for the child proc to fail\n");
break;
}
else
{
printf("child proc has not exited yet\n");
for (auto &e : tasks) { e(); }
}
}
return 0;
}
task.h
#pragma once
#include <cstdio>
void PrintLog();
void Download();
void BackUp();
task.cc
#include "task.h"
void PrintLog()
{
printf("TASK:PrintLog\n");
}
void Download()
{
printf("TASK:Download\n");
}
void BackUp()
{
printf("TASK:BackUp\n");
}
三、进程程序替换
1. 进程替换介绍
例如,可以通过如下接口(C语言做的封装)调用系统命令:
execl("/bin/ls", "ls", "-l", "-a", nullptr);
execl("/usr/bin/top", "top", nullptr);
参数:
(1)带路径的可执行程序:要执行谁?
(2)怎么执行?
(3)必须以 nullptr 结尾。
assist.c
#include <stdio.h>
#include <unistd.h>
int main(){
printf("I'm assist, pid: %d\n", getpid());
return 0;
}
nonew.c
#include <stdio.h>
#include <unistd.h>
int main(){
printf("I'm assist, pid: %d\n", getpid());
return 0;
}
[my@hcss-ecs-2ff4 exec]$ cat nonew.c
#include <stdio.h>
#include <unistd.h>
int main(){
printf("I'M NONEW-PROC, PID: %d\n", getpid());
execl("./assist", "assist", NULL);
return 0;
}
输出结果
I'M NONEW-PROC, PID: 32493
I'm assist, pid: 32493
进程替换没有创建新的进程!
execl 成功时没有返回值,失败返回 -1【只要返回,就是失败!】
Linux 下所有进程本质上都是 fork + exec 跑起来的。
创建进程,先有内核数据结构,再加载代码和数据。
2. 认识全部接口
常用的 exec 函数族,在当前进程中执行一个新的程序,替换当前进程的代码:
#include <unistd.h>
extern char **environ;
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[]);
int execvpe(const char *file, char *const argv[],
char *const envp[]);and execve:
int execve(const char *filename, char *const argv[],
char *const envp[]);注:... 是可变参数列表
(1)execv 和 execl
int execv(const char *path, char *const argv[]); 相当于把 int execl(const char *path, const char *arg, ...); 后面若干参数变成一个指针数组传进去。(最后字母 v 代表 vector,l 代表 list)
#include <iostream>
#include <cstdio>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
printf("PID: %d\n", getpid());
pid_t id = fork();
if(0 == id){
sleep(3);
char *const argv[] = {
(char*)"ls",
(char*)"--color",
(char*)"-l",
(char*)"-a",
(char*)nullptr
};
execv("/usr/bin/ls", argv);
//execl("/usr/bin/bash", "bash", "test.sh", nullptr);
//execl("/usr/bin/python", "python", "test.py", nullptr);
//execl("/bin/ls", "ls", "-l", "-a", nullptr);
exit(1);
}
pid_t rid = waitpid(id, nullptr, 0);
if(rid > 0){
printf("等待子进程成功\n");
}
//execl("/usr/bin/top", "top", nullptr);
return 0;
}
(2)execlp 和 execvp
int execlp(const char *file, const char *arg, ...);
想要运行谁?不要求带路径(从环境变量PATH中找)。
用例:execlp("ls", "ls", "-l", "-a", nullptr); 想执行谁,想怎么执行【两个 ls 不重复】
同理可得 execvp 的用法。
【注】execvp 第一个参数可以写成 argv[0] 这种形式。
(3)execvpe
int execvpe(const char *file, char *const argv[], char *const envp[]);
【Q】不传环境变量可以吗?【A】程序替换并不影响环境变量,环境变量有全局属性
手动传环境变量给 execvpe,使用全新的环境变量
关于环境变量:
a. 让子进程继承父进程全部的环境变量
b. 如果要传递全新的环境变量(自己定义和传递)
c. 如果单纯新增环境变量呢?使用 putenv(修改或增加)
const std::string myenv = "HELLO=MYLINUX";
putenv((char*)myenv.c_str());
父进程导入的环境变量子进程可以看到,那么shell能看到吗?看不到
extern char **environ;
execvpe(argv[0], argv, environ);
【!】程序替换不影响命令行参数和环境变量
3. exec 和 execve 总结
使用 man 命令查询时发现:
EXEC(3), EXECVE(2)
EXEC对应的6个函数是C标准库封装的接口,区别在于传参方式的不同,满足不同场景下的应用,是对 execve(真正意义上的系统调用)的封装。