第八部分:进程创建退出等待和替换
目录
1、进程创建回顾
1.1、进程创建
1.2、进程创建失败的原因
2、进程退出
2.1、进程退出的场景
2.2、进程常见退出方法
2.3、进程退出的方式
2.3.1、exit()函数
2.3.2、_exit()函数
3、进程等待
3.1、wait方法
3.2、waitpid方法
3.3、获取子进程的status
4、进程程序替换
4.1、替换函数
4.2、单进程的程序替换
4.3、多进程的程序替换
4.4、给子进程传递环境变量
1、进程创建回顾
1.1、进程创建
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include<sys/types.h>
#define N 5
void runchild()
{
int cnt=10;
while(cnt)
{
printf("I am a child:%d\n",getpid());
sleep(1);
cnt--;
}
}
int main()
{
for(int i=0;i<N;i++)
{
pid_t id=fork();
if(id==0)
{
runchild();
exit(0);
}
}
sleep(1000);
return 0;
}
1.2、进程创建失败的原因
1、系统中有太多进程。
2、实际用户的进程数超过了限制。
2、进程退出
在父子进程中,一般而言是父进程关心子进程的运行情况,因为父进程要知道交给子进程的任务完成的如何。父进程就是通过子进程的退出码来判断子进程任务完成的情况。
在main函数中,return 0;这个返回值就是进程的退出码,退出码用来表示进程的运行结果是不是正确的,如果返回的是0,就表示正确执行。可以使用return返回不同的数字,表征不同的出错原因,这就是进程的退出码。
2.1、进程退出的场景
第一种情况:代码运行完毕,结果正确。
第二种情况:代码运行完毕,结果不正确。
第三种情况:代码异常终止。进程出现异常本质就是我们的进程收到了对应的信号(信号后面再提)。
两者都是用来表示状态或错误信息,帮助开发者或调用者理解程序的执行结果。退出码通常是程序结束时返回的单一值,而错误码可能在程序运行时多次发生。退出码更多关注程序的整体执行结果,而错误码则关注具体的错误情境。可以使用strerror()函数来查看错误码的含义。系统提供的错误码和错误码的含义是一一对应的,其中errno是一个全局变量,是指最近一次执行的错误码。
2.2、进程常见退出方法
可以使用echo $?的方式来查看命令行中最近一个执行程序的退出码。其中$?中就保存着最近一次进程退出的时候的退出码。
例如:我们有这样的一个代码
#include<stdio.h>
int main()
{
printf("this is a test\n");
return 1;
}
运行后,在命令行输入下面的命令
echo $?
就可以看到如下结果:
我们是可以自己设计退出码的,例如:
const char* errString[]={
"sucess",
"error 1",
"error 2"
};
然后返回时返回数组的下标即可。 也就是用数组的下标,来代表各种可能出现的问题。
2.3、进程退出的方式
2.3.1、exit()函数
函数原型为:
#include <stdlib.h>
void exit(int status);
执行清理函数:调用所有通过 atexit()
或 on_exit()
注册的清理函数。这些函数可以用于释放资源或执行其他的善后工作。
关闭打开的流:关闭所有打开的文件流(如标准输入、输出、错误),并确保所有缓冲的数据被写入到相应的文件中。这一步确保数据不会丢失,并且文件能够正确关闭。
调用 _exit()
:最后,exit
函数会调用 _exit()
函数来实际终止进程。_exit()
不会执行任何清理或缓冲处理,因此是一个快速的结束进程的方式。
注意:exit函数在任意位置调用都表示调用进程退出,return则不同,return在main函数中表示进程退出,执行return n等同于执行exit(n),因为调用main的运行时函数会将main的返回值当做exit的参数,在非main函数仅表示当前函数返回,而不是进程退出。
2.3.2、_exit()函数
函数原型为:
#include <unistd.h>
void _exit(int status);
这个函数是一个系统调用。不会执行任何清理操作,直接终止进程。它用于确保程序立即终止,而不进行任何额外的操作。
一般代码异常本质可能就是代码没有跑完,所以在代码没有跑完的情况下就结束了看退出码是没有意义的。进程退出我们优先看的是进程是否异常,如果没有异常我们再看运行的结果是否正确。
进程的异常,比如:在代码中用0作为分母;再比如:在代码中对空指针进行解引用;这些操作都会引发异常。基本上代码异常都是发生了硬件级别的错误,比如:除0会导致CPU的状态寄存器出现溢出错误,然后操作系统就会给对应的的进程发送信号。比如:
8号信号就是指Floating point exception。
kill -8 PID
引发8号信号所对应的错误。
11号信号就表示Segmentation fault。
kill -11 PID
引发11号信号所对应的错误。
我们可以使用上面的方式,让正在执行的程序出现异常。
可以使用下面的方式列出信号。
kill -l
具体有关于信号的内容之后的文章再讲。
注意:缓冲区并不在进程地址空间中的内核空间,缓冲区就在用户空间中(后面再讲)。
3、进程等待
3.1、wait方法
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);
返回值:成功返回被等待进程pid,失败返回-1。
参数:输出型参数,获取子进程退出状态,不关心则可以设置成为NULL。
wait是等待任意一个子进程的退出,一次只等待一个进程。如果子进程不退出,父进程在wait的时候也就不会返回,也就是父进程会一直等待子进程的退出。也就是wait是阻塞等待进程的。
3.2、waitpid方法
#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *status, int options);
返回值:
当正常返回的时候waitpid返回收集到的子进程的进程ID;
参数:
注:可以使用WIFEXITED(status)来检测进程是否正常退出;若WIFEXITED(status)非零,WEXITSTATUS(status)是用来提取子进程退出码。这两个东西本质上就是宏。
3.3、获取子进程的status

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
int main()
{
pid_t id=fork();
if(id==-1)
{
perror("fork failed");
exit(1);
}
if(id==0)
{
sleep(5);
exit(10);
}
else
{
int st;
int ret=wait(&st);
if(ret>0 && (st&0x7f)==0)
{
printf("child exit:%d\n",(st>>8)&0xff);
}
else if(ret>0)
{
printf("sig code:%d\n",st&0x7f);
}
else
{
printf("wait failed\n");
}
}
return 0;
}
再比如:
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
#define TASK_NUM 10
typedef void(*task_t)();
task_t tasks[TASK_NUM];
void task1()
{
printf("这是一个执行打印日志的任务:%d\n",getpid());
}
void task2()
{
printf("这是一个执行检测网络健康状态的任务:%d\n",getpid());
}
void task3()
{
printf("这是一个绘制图形界面的任务:%d\n",getpid());
}
int AddTask(task_t t);
void InitTask()
{
for(int i=0;i<TASK_NUM;i++)
tasks[i]=NULL;
AddTask(task1);
AddTask(task2);
AddTask(task3);
}
int AddTask(task_t t)
{
int pos=0;
for(;pos<TASK_NUM;pos++)
{
if(!tasks[pos])
break;
}
if(pos==TASK_NUM)
return -1;
tasks[pos]=t;
return 0;
}
void DelTask(){}
void CheckTask(){}
void UpdateTask(){}
void ExecuteTask()
{
for(int i=0;i<TASK_NUM;i++)
{
if(!tasks[i])
continue;
tasks[i]();
}
}
int main()
{
pid_t id=fork();
if(id<0)
{
perror("fork");
return -1;
}
else if(id==0)
{
int cnt=5;
while(cnt)
{
printf("child pid:%d,ppid:%d,cnt:%d\n",getpid(),getppid(),cnt);
cnt--;
sleep(1);
}
exit(11);
}
else
{
int status=0;
InitTask();
while(1)
{
pid_t ret=waitpid(id,&status,WNOHANG);
if(ret>0)
{
if(WIFEXITED(status))
{
printf("正常跑完,退出码:%d\n",WEXITSTATUS(status));
}
else
{
printf("进程异常\n");
break;
}
}
else if(ret<0)
{
printf("wait failed\n");
break;
}
else
{
ExecuteTask();
usleep(500000);
}
}
}
return 12;
}
4、进程程序替换

4.1、替换函数
#include<unistd.h>
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[]);


例如:我们要替换ps这个可执行程序,也就是Linux中的命令
char *const argv[] = {"ps", "-ef", NULL};
char *const envp[] = {"PATH=/bin:/usr/bin", "TERM=console", NULL};
execl("/bin/ps", "ps", "-ef", NULL);//第一个参数是路径,中间的是可执行程序名和该可执行程序的
//选项,最后一个参数NULL是必须要带的,表示结束。
// 带p的,可以使用环境变量PATH,无需写全路径
execlp("ps", "ps", "-ef", NULL);
// 带e的,需要自己组装环境变量
execle("ps", "ps", "-ef", NULL, envp);
execv("/bin/ps", argv);
// 带p的,可以使用环境变量PATH,无需写全路径
execvp("ps", argv);
// 带e的,需要自己组装环境变量
execve("/bin/ps", argv, envp);
4.2、单进程的程序替换
#include<stdio.h>
#include<unistd.h>
int main()
{
printf("before:I am a process\n");
execl("/usr/bin/ls","ls","-a","-l",NULL);
printf("this is a test");//之所以不会执行该代码原因在于程序替换后跑的已经是替换后的程序了。
return 0;
}
对于单进程版本的程序替换,是直接拿别的代码和数据替换现有的代码和数据。
4.3、多进程的程序替换
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
int main()
{
pid_t id=fork();
if(id==0)
{
printf("I am a process\n");
execl("/usr/bin/bash","bash","shell.sh",NULL);
exit(1);
}
pid_t ret=waitpid(id,NULL,0);
if(ret>0)
{
printf("wait sucess\n");
}
sleep(5);
return 0;
}
shell.sh:其中#!是脚本语言的开头,后面跟的是脚本解释器,所谓的shell脚本语言其实就是我们写的命令。
#!/usr/bin/bash
function myfun()
{
cnt=1
while [ $cnt -le 10 ]
do
echo "hello $cnt"
let cnt++
done
}
echo "hello"
ls -a -l
myfun
对于多进程版本的程序替换,会发生代码和数据的写时拷贝。
实际上脚本语言的执行是脚本解释器在执行,例如:
python3 test.py
之所以可以跨语言调用,本质上是因为语言运行起来都是进程。
注:Linux中形成的可执行程序是有格式的,并不是杂乱无章的二进制,这个格式叫做ELF格式。在这个可执行程序的最开始有可执行程序的表头,可执行程序的入口地址就在表头当中。
注意:文件后缀为.cpp和.cc和.cxx都表示C++的源文件。程序替换不仅仅可以替换C或C++写的程序,也可以替换Java、python、shell等程序(前提是配置了相关的语言环境)。以.sh为后缀的文件是shell脚本文件,以.py为后缀的文件是python脚本文件。可以使用bash 文件名的方式执行脚本文件,python也是如此。
4.4、给子进程传递环境变量
环境变量也是数据,创建子进程的时候,环境变量就已经被子进程给继承下去了。程序替换环境变量信息不会被替换。
给子进程传递环境变量的方式:
第一种:在命令行中直接导入环境变量,环境变量自然会被后来的子进程给继承。
第二种,在父进程的代码中使用putenv函数导入环境变量,环境变量自然会被子进程给继承。
第三种:自己组装环境变量,通过程序替换函数,传递给子进程。
例如:
main.c
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<stdlib.h>
int main()
{
char *const envp[] = {"PATH=/bin:/usr/bin", "TERM=console", NULL};
pid_t id=fork();
if(id==0)
{
execle("./otherExe","otherExe","-a","-w","-v",NULL,envp);
exit(1);
}
pid_t ret=waitpid(id,NULL,0);
if(ret>0)
{
printf("wait sucess\n");
sleep(5);
}
return 0;
}
otherExe.cpp
#include<iostream>
using namespace std;
int main(int argc,char* argv[],char* env[])
{
cout<<"这是命令行参数:\n";
for(int i=0;argv[i];i++)
{
cout<<i<<":"<<argv[i]<<endl;
}
cout<<"这是环境变量:\n";
for(int i=0;env[i];i++)
{
cout<<i<<":"<<env[i]<<endl;
}
return 0;
}
运行结果为:
注:如果不传则默认继承父进程的环境变量,如果传了,则覆盖原本从父进程继承下来的环境变量。可以传环境变量表,也可以传自己定义的环境变量表,传自己定义的环境变量会覆盖系统原本的环境变量,而不是追加。
注意:环境变量表中存的不是环境变量的字符串,而是指针,这个指针指向的是环境变量的字符串,使用putenv函数添加环境变量,本质上是往环境变量表中添加一个指针,这个指针指向新环境变量的字符串。
当我们进行登录的时候,系统就会启动一个shell进程,shell进程会读取该用户目的.bash_profile文件,里面保存了导入环境变量的方式。
最后:区分xshell中的复制会话与复制SSH渠道,如下图: