在linux中fork函数时非常重要的函数,它从已存在进程中创建一个新进程。 新进程为子进程,而原进程为父进程。
#include
pid_t fork(void); //pid_t是一个无符号整数
返回值:子进程中返回0,父进程返回子进程id,出错返回-1 ⭐
举例:
#include
#include
int main()
{
printf("我是父进程!\n");
pid_t id = fork();
if (id < 0)
{
printf("创建子进程失败!\n");
return 1;
}
else if (id == 0)
{
while (1)
{
printf("我是子进程:pid:%d,ppid:%d\n", getpid(), getppid());
sleep(1);
}
}
else
{
while (1)
{
printf("我是父进程:pid:%d,ppid:%d\n", getpid(), getppid());
sleep(1);
}
}
return 0;
}
当在Linux中写入上述代码👆的文件时,会在命令框循环打印下面的文字👇

getpid(),也使用系统命令查看进程,可以看出使用fork()使该进程生成子进程。
🌟进程 =内核数据结构+进程代码和数据
内核数据结构由OS来维护,包括PCB结构体、进程地址空间结构体、页表等等,进程代码和数据一般从磁盘中来,也就是C/C++程序加载后的结果
fork( )返回后等待操作系统或调度器来调度。解释说明:

写时拷贝原理:
为什么要使用写时拷贝:
写时拷贝的优点:
- 代码汇编之后会有很多行代码,而且每行代码加载到内存之后都有对应的地址。
- 进程可能被中断,没有执行完,下次回来还要从刚才的位置继续运行就要求cpu必须记录下当前进程执行的位置,所以cpu有对应的寄存器(EIP / pc指针:程序计数器)数据用来记录当前进程的执行位置。
- 寄存器在cpu内只有一份,但是寄存器中的数据(进程的上下文数据)是可以有很多份的。
💡所以:当fork时,寄存器中的数据也要给子进程,子进程认为自己的EIP起始值就是fork之后的代码! 但是实际上fork之后子进程可以看到全部的代码(包括fork执行前的代码)!
进程 =内核数据结构+进程代码和数据
进程有三种常见的中止方式:
第一种和第二种进程中止方式主要的区别是结果是否正确,那么代码跑完结果是否正确应该怎么判定呢❓
💡答:代码跑完结果是否正确是由进程的退出码标识的
- main函数的返回值叫做进程的退出码,返回给上一级进程,表示进程返回时结果是否正确,从而评判该进程执行的结果。常见的main函数返回值都是0,但是它也可以是其他值。
⭐ 不同的非零值就可以标识不同的错误原因,从而当我们的程序运行结束之后,退出码可以定位错误的原因。
对话框输入命令echo $?获取最近一个进程执行完毕的退出码
当不知道退出码的含义时可以使用c语言提供的strerror函数将退出码转换成字符串描述退出码含义
# include
streror(退出码)
正常终止:
return 退出码中止进程👀其他函数内部return叫函数返回,只有main函数内的return语句是进程退出
#include
void exit(int status);//参数:status 定义了进程的终止状态
exit 和 return区别:
(1)exit在代码的任何地方调用都是直接中止进程
(2)return是语句,exit是函数。
#include
void _exit(int status);//参数:status 定义了进程的终止状态
⭐_exit是系统调用接口,exit函数是c语言提供的库函数

exit 也会调用exit, 但在调用exit之前还会:
异常退出:
ctrl + c上述问题在系统中由进程等待解决:
⭐父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息。
wait方法
当子进程已经退出了,但是父进程还在运行,子进程就会变为僵尸进程,为了解决僵尸进程造成的内存泄露,需要采用wait方法
#include
#include
pid_t wait(int*status); //阻塞式等待
返回值:成功返回被等待进程pid,失败返回-1。
参数:输出型参数,获取子进程退出状态,不关心则可以设置成为NULL
举例:
#include
#include
#include
#include
#include
#include
int main()
{
pid_t id = fork();
if (id < 0)
{
perror("fork");
exit(1); //标识进程运行完毕,结果不正确
}
else if (id == 0)
{
//子进程
int cnt = 5;
while (cnt)
{
printf("cnt: %d, 我是子进程, pid: %d, ppid : %d\n", cnt, getpid(), getppid());
sleep(1);
cnt--;
}
exit(0);
}
else
{
//父进程
printf("我是父进程, pid: %d, ppid: %d\n", getpid(), getppid());
sleep(7);
pid_t pid = wait(NULL); //阻塞式的等待!一般都是在内核中阻塞,等待被唤醒
//wait会使父进程阻塞的等待子进程退出,将子进程回收后,再执行后续代码
//编写多进程进本就是fork()+wait/waitpid
if (pid > 0)
{
printf("等待子进程成功, pid: %d\n", pid);
}
}
}
waitpid方法
pid_ t waitpid(pid_t pid, int *status, int options);
返回值:
> 0,表示正常返回,waitpid返回收集到的子进程的进程ID
= 0,如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0
< 0,如果调用中出错,则返回-1
参数:
Pid =-1,等待任一个子进程,与wait等效
Pid > 0,等待其进程ID与pid相等的子进程
status:
输出型参数,查看进程是否是正常退出以及进程的退出码
options:
options:默认为0,代表阻塞等待,设置为WNOHANG代表父进程非阻塞等待。
(系统提供的大写标记为其实就是宏,WAIT NO HANG,夯就是这个进程没有被CPU调度,CPU很忙,要么就是该进程在阻塞队列或等待被调度)
若等待成功但pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若结束,则返回该子进程的ID。
WNOHANG其实就是C语言定义的宏,#define WNOHANG 1
举例:
#include
#include
#include
#include
#include
#include
int main()
{
pid_t id = fork();
if (id < 0)
{
perror("fork");
exit(1); //标识进程运行完毕,结果不正确
}
else if (id == 0)
{
//子进程
int cnt = 5;
while (cnt)
{
printf("cnt: %d, 我是子进程, pid: %d, ppid : %d\n", cnt, getpid(), getppid());
sleep(1);
cnt--;
}
exit(0);
}
else
{
//父进程
printf("我是父进程, pid: %d, ppid: %d\n", getpid(), getppid());
int status = 0;
// 只有子进程退出的时候,父进程才会执行waipid函数,进行返回。父进程依然还活着!
// wait/waitpid可以在目前的情况下, 让进程退出有一定顺序性
// 可以让父进程进行更多的收尾工作
// id > 0,等待指定进程
// id =-1,等待任意一个子进程退出,等价于wait接口(wait接口属于waitpid的子集)
// options:默认为0,代表阻塞等待,设置为 WNOHANG代表父进程非阻塞等待
pid_t ret = waitpid(id, &status, 0); //默认是在阻塞状态区等待子进程状态变为退出
if (ret > 0)
{
// 0x7F -> 0000...000 0111 1111
printf("等待子进程成功, ret: %d, 子进程收到的信号编号: %d,子进程退出码: %d\n", ret, status & 0x7F, (status >> 8) & 0xFF); //0xff --> 0000...000 1111 1111
//(status >> 8) & 0xFF)会打印子进程的退出码 0; status & 0x7F会打印子进程收到的信号编号0表示正常跑完
//上面的获取子进程收到信号编号和退出码的方式比较繁琐,可以采用系统提供的status宏:
if(WIFEXITED(status))
{
//子进程是正常退出的
printf("子进程执行完毕,子进程退出码:%d\n",WEXITSTATUS(status));
}
else
{
printf("子进程异常退出:%d\n",WIFEXITED(status));
}
}
}
}
waitpid的伪代码
waitpid(pid,status,flag)
{
if(status退出)
return childpid 和 status
else if(status没退出)
{
if(flag==0)
挂起父进程
elseif(flag==WNOHANG)
return 0;
}
}
进程阻塞的本质就是堵塞在操作系统中系统函数的内部,等待被唤醒,当被唤醒时,从EIP寄存器中读取的代码的上下文,从if后面继续执行。
💡wait(pid,NULL,0) == wait(NULL)
父进程通过wait/waipid可以拿到子进程的退出结果,为什么不用全局变量呢?
既然进程有独立性?为什么父进程可以拿到子进程的退出信息?
wait/waipid就是读取子进程的tast_struct中的exit_code,exit_signal,父进程没有读取内核数据结构对象的权限,但是wait/waipid是系统调用,操作系统拥有这个权限。status 不能简单当作整形来看待,要按照比特位划分,最低的7个比特位表示进程接受到的信号,次低8位表示进程退出的退出码。

父进程非阻塞等待执行案例:
#include
#include
#include
#include
#include
#include
#include
typedef void (*handler_t)(); //函数指针类型
std::vector<handler_t> handlers; //函数指针数组
void fun_one()
{
printf("这是一个临时任务1\n");
}
void fun_two()
{
printf("这是一个临时任务2\n");
}
// 设置对应的方法回调
// 以后想让父进程闲了执行任何方法的时候,只要向Load里面注册,就可以让父进程执行对应的方法喽!
void Load()
{
handlers.push_back(fun_one);
handlers.push_back(fun_two);
}
int main()
{
pid_t id = fork();
if(id == 0)
{
// 子进程
int cnt = 5;
while(cnt)
{
printf("我是子进程: %d\n", cnt--);
sleep(1);
}
exit(11); // 11 仅仅用来测试
}
else
{
int quit = 0;
while(!quit)
{
int status = 0;
pid_t res = waitpid(-1, &status, WNOHANG); //以非阻塞方式等待
//非阻塞等待就是父进程调用waipid来等待,如果子进程没有退出,waitpid这个系统调用立马返回!
if(res > 0)
{
//等待成功 && 子进程退出
printf("等待子进程退出成功, 退出码: %d\n", WEXITSTATUS(status));//WEXITSTATUS(status)显示子进程退出码
quit = 1;
}
else if(res == 0)
{
//等待成功 && 但子进程并未退出
printf("子进程还在运行中,暂时还没有退出,父进程可以在等一等, 处理一下其他事情??\n");
if(handlers.empty())
Load();
for(auto iter : handlers)
{
//执行处理其他任务
iter();
}
}
else
{
//等待失败
printf("wait失败!\n");
quit = 1;
}
sleep(1);
}
}
}
引言
fork之后,父子进程各自执行父进程代码的一部分,但是如果子进程就想执行一个全新的程序呢❓
💡答:用进程的程序替换来完成这个功能
概念
程序替换是通过特定的接口,加载磁盘上的一个全新的程序(代码和数据)到调用进程的地址空间中,从而让子进程达到执行其他程序的目的。

进程替换的原理⭐
用操作系统提供的接口exec函数将新的磁盘上的程序加载到内存,并与当前进程的页表重现建立映射
调用exec并不创建新进程,所以调用exec前后该进程的id并未改变
所谓的exec函数,本质就是如何加载程序的函数,为了满足不同的调用场景,有六种以exec开头的函数:
头文件:
#include
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 execl(const char *path, const char *arg, ...);#include
#include
#include
#include
int main()
{
pid_t id = fork();
//如果不创建子进程,那么替换的只能是父进程,这样替换子进程而不影响父进程
//因为想让父进程聚焦在读取数据,解析数据,指派进程执行代码的功能
//子进程加载新程序的时候,就是一种写入,发生写时拷贝,父子的代码分离。
//在程序替换时,代码和数据都要发生写时拷贝,而不是单纯的数据。这样的话,父子进程在代码和数据上就彻底分开了。
if (id == 0)
{
//子进程---子进程加载新程序的时候,是写入,发生写时拷贝并将父子代码分离
//父子进程再代码和数据上就彻底分开了
//ls-a-l
printf("子进程开始运行,pid:%d\n", getpid());
sleep(3);
execl("/user/bin/ls", "ls", "-a", "-l", NULL);
exit(1);
}
else
{
//父进程
printf("父进程开始运行,pid:%d\n", getpid());
int status = 0;
pid_t id = waitpid(-1, &status, 0);//阻塞等待,一定是子进程先运行完毕,父进程获取之后才退出!
if (id > 0)
{
printf("wait success ,exit code:%d\n", WEXITSTATUS(status));
}
}
return 0;
}
int execlp(const char *file, const char *arg, ...);#include
#include
#include
#include
int main()
{
pid_t id = fork();
if (id == 0)
{
//ls-a-l
printf("子进程开始运行,pid:%d\n", getpid());
sleep(3);
//execl("/user/bin/ls", "ls", "-a", "-l", NULL);
execlp("ls", "ls", "-a", "-l", NULL);
//p表示程序会在环境变量PATH中进行查找,不需要用户告诉执行程序的路径
//第一个参数表示你要执行谁--找到程序
//后面的参数表示你想怎么执行--传递选项
exit(1);
}
else
{
//父进程
printf("父进程开始运行,pid:%d\n", getpid());
int status = 0;
pid_t id = waitpid(-1, &status, 0);//阻塞等待,一定是子进程先运行完毕,父进程获取之后才退出!
if (id > 0)
{
printf("wait success ,exit code:%d\n", WEXITSTATUS(status));
}
}
return 0;
}
int execle(const char *path, const char *arg, ...,char *const envp[]);#include
#include
#include
#include
#define NUM 16
int main(int argc,int*argv[],int*env[])
{
pid_t id = fork();
if (id == 0)
{
printf("子进程开始运行,pid:%d\n", getpid());
//execl("/user/bin/ls", "ls", "-a", "-l", NULL);
execle("ls", "ls", "-a", "-l", NULL, env);
//环境变量具有全局属性,可以被子进程继承
exit(1);
}
else
{
//父进程
printf("父进程开始运行,pid:%d\n", getpid());
int status = 0;
pid_t id = waitpid(-1, &status, 0);//阻塞等待,一定是子进程先运行完毕,父进程获取之后才退出!
if (id > 0)
{
printf("wait success ,exit code:%d\n", WEXITSTATUS(status));
}
}
return 0;
}
int execv(const char *path, char *const argv[]);#include
#include
#include
#include
#define NUM 16
int main()
{
pid_t id = fork();
//如果不创建子进程,那么替换的只能是父进程,这样替换子进程而不影响父进程
//因为想让父进程聚焦再读取数据,解析数据,指派进程执行代码的功能
if (id == 0)
{
//子进程---子进程加载新程序的时候,是写入,发生写时拷贝并将父子代码分离
//父子进程再代码和数据上就彻底分开了
char* const _argv[NUM] = {
(char*)"ls",
(char*)"-a",
(char*)"-l",
NULL
};
printf("子进程开始运行,pid:%d\n", getpid());
sleep(3);
//execl("/user/bin/ls", "ls", "-a", "-l", NULL);
execv("/user/bin/ls", _argv);//这个接口和execl只有传参的差别
exit(1);
}
else
{
//父进程
printf("父进程开始运行,pid:%d\n", getpid());
int status = 0;
pid_t id = waitpid(-1, &status, 0);//阻塞等待,一定是子进程先运行完毕,父进程获取之后才退出!
if (id > 0)
{
printf("wait success ,exit code:%d\n", WEXITSTATUS(status));
}
}
return 0;
}
int execvp(const char *file, char *const argv[]);#include
#include
#include
#include
#define NUM 16
int main()
{
pid_t id = fork();
if (id == 0)
{
char* const _argv[NUM] = {
(char*)"ls",
(char*)"-a",
(char*)"-l",
NULL
};
printf("子进程开始运行,pid:%d\n", getpid());
//execl("/user/bin/ls", "ls", "-a", "-l", NULL);
execvp("ls", _argv);
exit(1);
}
else
{
//父进程
printf("父进程开始运行,pid:%d\n", getpid());
int status = 0;
pid_t id = waitpid(-1, &status, 0);//阻塞等待,一定是子进程先运行完毕,父进程获取之后才退出!
if (id > 0)
{
printf("wait success ,exit code:%d\n", WEXITSTATUS(status));
}
}
return 0;
}
int execvpe(const char*file, char*const argv[], char*const envp[])#include
#include
#include
#include
int main()
{
pid_t id = fork();
if (id == 0)
{
//ls-a-l
printf("子进程开始运行,pid:%d\n", getpid());
sleep(3);
execvpe("ls", "ls", "-a", "-l", env);
exit(1);
}
else
{
//父进程
printf("父进程开始运行,pid:%d\n", getpid());
int status = 0;
pid_t id = waitpid(-1, &status, 0);//阻塞等待,一定是子进程先运行完毕,父进程获取之后才退出!
if (id > 0)
{
printf("wait success ,exit code:%d\n", WEXITSTATUS(status));
}
}
return 0;
}
事实上,只有execve是真正的系统调用。为了满足不同的场景,其它六个都是系统提供的基本封装,最终都调用 int execve(const char*filename, char*const argv[], char*const envp[]

💡注意事项:
exex系列的程序就是加载器的底层接口
path—路径+目标文件名
*arg—传入的选项
...— 表示可变参数列表
char *const argv[]— 表示命令行参数的指针数组
最后必须以NULL结尾
也可以用来执行自己写的程序
exec系列的函数不需要返回值判定调用其他进程是否成功,因为一旦调用成功,exec代码也被替换了,没有能接收到exec返回值的变量。
可以在后面加exit,如果调用失败,exit没被替换,就会执行exit退出程序。
进程替换和应用场景有关,有时候必须让子进程执行新的程序。