文章目录
进程创建
创建进程其实就是使用fork(),fork()有两个返回值。如果创建成功就返回给父进程返回子进程的pid,给子进程返回0;创建失败就给父进程返回-1。
因为有虚拟地址,父子进程看似代码和数据共享,其实会写时拷贝,在子进程修改数据时会开辟新空间。
进程终止
进程退出时的三种情况
- 代码运行完毕,结果正确。
- 代码运行完毕,结果不正确。
- 代码异常终止(进程崩溃)。
exit 和 _exit 函数
exit和_exit都会直接结束进程,不会执行后续任何代码
我们整个代码来看看exit和_exit的差别:
cpp
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main() {
printf("Hello"); // 无换行符,缓冲区未刷新
// exit(0); // 会触发清理和刷新缓冲区
_exit(0); // 直接退出,不执行清理和刷新
return 0;
}

exit和exit的不同点
exit是c标准库,_exit是系统调用
exit执行刷新缓冲区,_exit不执行

执行return num等同于执行exit(num),因为调用main函数运行结束后,会将main函数的返回值当做exit的参数来调用exit函数。
进程等待
为了解决僵尸进程,获取子进程的退出信息我们需要使用进程等待。其中进程等待有两个关键的函数wait与waitpid。
cpp
#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int*status);
//成功返回被等待进程pid,失败返回-1
//返回的进程pid是随机的,如果有多个被等待的pid,它会随机选择一个
//status是输出型参数,获取子进程退出状态,不关心则可以设置成为NULL
由上wait方法我们看到有个参数叫status,waitpid也有。
status
如果给status参数传NULL,表示会正常等待子进程结束,但是不获取子进程的退出状态。
否则操作系统会通过statu的参数,将子进程的退出信息反馈给父进程。
status虽然是一个整型变量,但status不能简单的当作整型来看待,因为status的不同比特位所代表的信息不同,一般我们只考虑低的16个比特位。
在status的低16比特位当中,高8位表示进程的退出状态,即退出码。进程若是被信号所杀,则低7位表示终止信号,而第8位比特位是core dump标志。

如果想要读取status退出状态或者终止信号的话,可以通过两个宏
WIFEXITED(status): 用于查看进程是否是正常退出,本质是检查是否收到信号。
WEXITSTATUS(status): 用于获取进程的退出码。
wait
wait的方法:
cpp
#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int*status);
//成功返回被等待进程pid,失败返回-1
//返回的进程pid是随机的,如果有多个被等待的pid,它会随机选择一个
//status是输出型参数,获取子进程退出状态,不关心则可以设置成为NULL
我用一段代码演示父进程等待子进程结束然后获得子进程退出信息:
cpp
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/wait.h>
#include<sys/types.h>
int main()
{
pid_t id = fork();//创建子进程
if(id==0)
{
//chlld
int count=10;
while(count--)
{
printf("I am child:PID:%d,PPID:%d\n",getpid(),getppid());
sleep(1);
}
exit(0);
}
//father
int status=0;
pid_t ret=wait(&status);
//如果等待成功
if(ret>0)
{
printf("wait child success\n");
if(WIFEXITED(status))
{
//退出正常
printf("exit code:%d\n",WEXITSTATUS(status));
}
else
{
printf("exit signal:%d\n",status&0x7f);
}
}
sleep(10);
return 0;
}
子进程正常退出,父进程成功获取退出信息,子进程就不会形成僵尸进程
如果杀死了子进程,父进程一样是可以等待成功获取退出信号的
waitpid
waitpid方法:
cpp
#include<sys/types.h>
#include<sys/wait.h>
pid_t waitpid(pid_t pid, int*status, int options);
//返回值和status都与wait是一样的,可以说wait是waitpid封装的一个函数,当pid==-1,option==0,那么waitpid==wait
//第一个参数pid:
//pid > 0:等待进程 ID 等于 pid 的子进程
//pid == -1:等待任意子进程
//pid == 0:等待与调用进程(父进程)属于同一进程组的所有子进程
//第三个参数options:
//options == 0,意味着这个选项没有任何用处,未开启选项
//options == WNOHANG,若pid指定的子进程没有结束,则waitpid直接返回0,不等待,若正常结束,则返回子进程pid
用以下代码演示一下waitpid获取子进程退出码:
cpp
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
int main()
{
pid_t id = fork();
if (id == 0){
//child
int count = 10;
while (count--){
printf("I am child...PID:%d, PPID:%d\n", getpid(), getppid());
sleep(1);
}
exit(0);
}
//father
int status = 0;
pid_t ret = waitpid(id, &status, 0);
if (ret >= 0){
//wait success
printf("wait child success...\n");
if (WIFEXITED(status)){
//exit normal
printf("exit code:%d\n", WEXITSTATUS(status));
}
else{
//signal killed
printf("eixt siganl %d\n", status & 0x7F);
}
}
sleep(10);
return 0;
}

非阻塞轮询
在之前的父子进程关系中,当子进程还未退出时,父进程通常处于阻塞状态,这个期间父进程不能进程其他操作。
但是,我们可以采取非阻塞等待的方式。其实就是在调用waitpid时,给第三个参数options 传WNOHANG:
//options == WNOHANG,若pid指定的子进程没有结束,则waitpid直接返回0,不等待,若正常结束,则返回子进程pid
不等待父进程就可以去做其他事。当被等待的子进程正常结束时,父进程就可以拿到子进程的退出信息:
cpp
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
pid_t id=fork();//创建子进程
if(id==0)
{
//child
int count=3;
while(count--)
{
printf("child do something\n");
sleep(3);
}
exit(0);
}
//father
while(1)
{
int status=0;
pid_t ret=waitpid(id,&status,WNOHANG);
if(ret>0)
{
printf("wait success\n");
printf("exit code:%d\n",WEXITSTATUS(status));
break;
}
else if(ret==0)
{
printf("father do other things\n");
sleep(1);
}
else
{
//wait error
break;
}
}
return 0;
}

进程替换
什么是进程替换:
共享代码和数据的父子进程,在修改子进程时就会发生写时拷贝。如果要修改子进程的代码,则需要进程替换 。
由下图可以了解到:
进程替换不是创建新的进程,仅仅是将代码和数据替换罢了,进程相关的PCB信息没有任何改变。

进程替换函数
进程替换函数可以让不同语言编写的程序串联起来!
进程替换可以使用的六个函数:
execl:执行指定路径的程序,参数以可变参数形式传递,必须以NULL结尾。
execlp:在系统的PATH环境变量中搜索程序并执行,参数以可变参数形式传递。
execle:执行指定路径的程序,允许传递环境变量数组envp,用于指定新程序的环境变量。
execv:执行指定路径的程序,参数以数组形式传递。
execvp:在系统的PATH环境变量中搜索程序并执行,参数以数组形式传递。
execvpe:在系统的PATH环境变量中搜索程序并执行,允许传递环境变量数组envp。
cpp
//库函数,它们6个最终都是调用的execve
#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[]);
//系统调用函数
int execve(const char *path, char *const argv[], char *const envp[]);
通过以上的介绍,我们可以发现它们的命名规律:
l: list,表示中间参数采用列表
p: path,自动搜索环境变量PATH
e: env,自己维护环境变量
v: vector,表示中间参数用数组
从最简单的execl函数介绍:
它的第一个参数就是路径,第一个参数到最后一个参数之间的参数叫做可变参数列表,也就是说可以有多个,它和我们在命令行使用的命令是一样的,只不过我们命令行的分隔是空格,而这里的分隔是逗号和引号,最后一个参数必是NULL,表示该表的末尾边界
execlp:
除了第一个参数和execl不一样以外,其他的格式都一样,p说明它会先从环境变量PATH中找,所以我们要执行命令可以不用带路径,直接写命令名字就行了,用execlp执行 ls -l -a 命令的等效代码就是:
execlp("ls","ls","-l","-a",NULL);
execle:
execle函数,除了比execl多了一个最后一个参数以外,其他都不变,只是NULL值要在倒数第二个参数,同上方的代码应该是如下所示:
extern char** environ; execlp("/usr/bin/ls","ls","-l","-a",NULL,envrion);
传入execle的环境变量表也可以用用户定义的:
c
char* const my_environ[]
{
"SUPER=1",
"LITTLE=2",
"MONSTER=3",
NULL
};
execle("/usr/bin/ls","ls","-l","-a",NULL,my_environ);
c
int main()
{
printf("my process\n");
char* const my_environ[]=
{
"SUPER=1",
"LITTLE=2",
"MONSTER=3",
NULL
};
execle("/usr/bin/ls","ls","-l","-a",NULL,my_environ);
printf("my process\n");
return 0;
}

看起来和命令相同,实际上用户自定义的环境变量已经成为替换进程的一部分了。
这三个进程替换函数掌握了,剩下的接口参数都是类似的,想要自定义参数也是一样的操作。