进程创建
一、进程创建
1.fork函数初识
linux中,fork()函数非常重要,它从当前进程中创建一个新的进程。新进程为子进程,原始进程为父进程。
#include <unistd.h>
pit_t fork(void);
返回值:子进程中返回0,父进程返回子进程id,出错返回-1
2.fork函数的两个返回值
当一个进程调用fork之后,就有两个二进制代码相同的进程,自然也会被return两次。
具体的参考大佬的博客:进程控制
fork之前父进程独立执行,fork之后,父子两个执行流分别执行。但谁先执行完全由调度器决定。
3.写时拷贝
通常,父子代码共享。
1)修改内容之前,父子进程的数据段+代码段默认共享;
2)修改内容之后,谁写入,谁发生写时拷贝,即写入的数据开辟新的空间。
4.fork常规用法
- 一个父进程希望复制自己,使父子进程同时执行不同的代码段;例如,父进程等待客户端请求,生成子进程来处理请求。前面使用的 fork都属于这种情况。
- 一个进程要创建子进程来执行一个不同的程序;例如子进程从 fork 返回后,调用 exec 系列函数。即下面进程替换内容。
二、进程终止
1.进程退出场景
退出码0代表进程运行结果正确
退出码非0代表运行结果错误
- 代码运行完毕且结果正确 – 退出码为0;
- 代码运行完毕且结果不正确 – 退出码为非0;
- 代码异常终止 – 退出码无意义。
2.进程退出方法
正常终止:
可以通过echo $?
查看最近一个进程的退出码
1.从main返回
2.调用exit
3.调用_exit
exit函数:
#include <stdlib.h>
void exit(int status);
status:status 定义了进程的终止状态,父进程通过wait来获取该值
函数功能:终止进程
_exit函数:
#include <stdlib.h>
void _exit(int status);
status:status 定义了进程的终止状态,父进程通过wait来获取该值
函数功能:终止进程
区别:
- exit()是库函数,_exit()是系统调用, 库函数是对系统调用封装的一些接口。即exit 的底层是 _exit 函数,exit 是 _exit 的封装。
- exit 在终止程序后会刷新缓冲区,而 _exit 终止程序后不会刷新缓冲区。
- exit 的底层是 _exit,而 _exit 并不会刷新缓冲区,即缓冲区不在操作系统内部,而是在用户空间。
异常退出:
Ctrl C 终止进程
三、进程等待
1.进程等待的必要性
解决僵尸进程问题。父进程通过进程等待的方式,回收子进程资源,获取子进程信息。
进程的退出信息是存放在子进程的 task_struct 中的,所以进程等待的本质就是从子进程 task_struct 中读取退出信息,然后保存到相应变量中去。
2.进程等待的方法
wait方法:
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);
返回值:成功返回被等待进程的pid,失败返回-1;
status:输出型参数,获取子进程退出状态,不关心则可以设置成为NULL;
wait的使用:
开始时父子进程都处于休眠状态,之后子进程运行5s退出,此时由于父进程还要休眠5s,所以没有对子进程进行进程等待,所以子进程变成僵尸状态Z。5s过后,父进程使用 wait 系统调用对子进程进行进程等待,所以子进程由僵尸状态变为彻底死亡状态。
waitpid方法:
#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *status, int options);
返回值:成功返回被等待进程的pid,失败返回-1;
pid:pid=-1,等待任意一个子进程,与wait等效;pid>0.等待其进程id与pid相等的子进程
status:输出型参数,获取子进程退出状态,不关心则可以设置成为NULL;
options:等待方式,options=0,阻塞等待;options=WNOHANG,非阻塞等待
waitpid的使用:
waitpid 可以传递 id 来指定等待特定的子进程,也可以指定 options 来指明等待方式。
获取子进程status:
- wait 和 waitpid,都有一个 status 参数,该参数是一个输出型参数,由操作系统填充。
- 如果传递NULL,表示不关心子进程的退出状态信息 。
- 否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。
- status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究 status 低16比特位):
status 低两个字节的内容被分成了两部分 – 第一个字节前七位表示退出信号/终止信号,最后一位表示 core dump 标志;第二个字节表示退出状态,退出状态即代表进程退出时的退出码;
对正常退出的程序来说,退出信号和 core dump 标志都为0,退出状态等于退出码;对于异常终止的程序来说,退出信号为不同终止原因对应的数字,退出状态未用,无意义。
因此 status读取为:
printf("exit signal:%d, exit code:%d \n", (status & 0x7f), (status >> 8 & 0xff));
其中,status 按位与上 0x7f 表示保留低七位,其余九位全部置为0,从而得到退出信号;
status 右移8位得到退出状态,再按位与上 0xff 是为了防止右移时高位补1的情况;
Linux 提供了 WIFEXITED 和 WEXITSTATUS 宏 来帮助我们获取 status 中的退出状态和退出信号
该部分参考自:添加链接描述
3.阻塞与非阻塞等待
阻塞式等待即当父进程执行到 waitpid 函数时,如果子进程还没有退出,父进程就只能阻塞在 waitpid 函数,直到子进程退出,父进程通过 waitpid 读取退出信息后才能接着执行后面的语句;
而非阻塞式等待则不同,当父进程执行到 waitpid 函数时,如果子进程未退出,父进程会直接读取子进程的状态并返回,然后接着执行后面的语句,不会等待子进程退出。
轮询
轮询是指父进程在非阻塞式状态的前提下,以循环方式不断的对子进程进行进程等待,直到子进程退出。
原文链接:https://blog.csdn.net/m0_62391199/article/details/128033352
4.总结
- 为了读取子进程的退出结果以及回收子进程资源,需要进行进程等待。
- 进程等待的本质是父进程从子进程 task_struct中读取退出信息,然后保存到 status 中。
- 可以通过 wait 和 waitpid 系统调用进行进程等待。 status参数是一个输出型参数,父进程通过 wait/waitpid 函数将子进程的退出信息写入到 status 中。
- status 以位图方式存储,包括退出状态和退出信号,若退出信号不为0,则退出状态无效。
- 可以使用系统提供的宏 WIFEXITED 和WEXITSTATUS 来分别获取 status 中的退出状态和退出信号。
- 进程等待的方式分为阻塞式等待与非阻塞式等待,阻塞式等待用0来标识,非阻塞式等待用宏 WNOHANG 来标识。
- 由于非阻塞式等待不会等待子进程退出,所以我们需要以轮询的方式来不断获取子进程的退出信息。
四、进程程序替换
1.替换原理
进程程序替换是指父进程用 fork 创建子进程后,子进程通过调用 exec 系列函数来执行另一个程序;当进程调用某一种 exec 函数时,该进程的用户空间代码和数据完全被新程序替换,然后从新程序的启动例程开始执行;
但是原进程的 task_struct 和 mm_struct 以及进程 id 都不会改变,页表可能会变;所以调用 exec 并不会创建新进程,而是让原进程去执行另外一个程序的代码和数据。
程序替换是在当前进程pcb并不退出的情况下,替换当前进程正在运行的程序为新的程序,即
1、程序替换成功后,运行完新程序,依然会运行原有的代码
2、程序替换成功后,原进程没有退出,使用原进程运行新程序
2.进程替换操作
exec 系列函数
实现进程程序替换的系统调用函数就一个:execve,其他一系列的 exec 库函数都是为了满足不同的替换场景而对 execve 系统调用进行的封装;主要认识六个 exec 库函数。
#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[]);
这些函数调用成功则加载新的程序并启动代码开始执行,不再返回;如果调用出错则返回-1。
这些 函数一旦调用成功,就代表着原程序的代码和数据已经被新程序替换掉了,原程序后续的语句都不会再被执行了,所以 exec 调用成功后没有返回值,因为该返回值没有机会被使用;只有 exec 调用失败,原程序可以继续往下执行时,exec 返回值才会被使用。
exec 系列函数的使用
执行程序就两个步骤 :
一是找到该可执行程序;二是指定程序执行的方式。
对于 exec 函数来说,“p” 和非 “P” 用来找到程序,“l” “v” 用来指定程序执行方式;“e” 用来指定环境变量。
(1)execl && execlp
以ls
指令为例
execl("/usr/bin/ls", "ls", "-a", "-l", NULL);
execlp("ls", "ls", "-a", "-l", NULL); //函数带有 “p”,可以不带路径参数
可以看到,exec无非就两个参数:
第一个参数是路径参数。注意带 “p” 的 exec 函数可以不带路径的前提是被替换程序处于PATH环境变量中。
第二个参数是如何执行程序。即Linux 命令行中该程序如何执行就如何传参,要注意的是,exec 中需要对不同选项进行分割,即每一个选项都单独分为一个字符串,并且最后一个可变参数设置为 NULL,表示传参完毕。
进程程序替换时如果想要让不同类型文件表现为不同颜色的话,需要显示传递 “–color=auto” 选项。
实例如下:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
int main()
{
pid_t pid=fork();
if(pid==-1)
{
perror("fork"),exit(-1);
}
else if(pid==0)
{
printf("child process is running... pid:%d\n ", getpid());
int ret = execl("/usr/bin/ls", "ls", "-l", "-a", "--color=auto", NULL);
if(ret == -1)
{
printf("process exec fail....\n");
exit(1);
}
printf("child process is done... pid:%d\n ", getpid());
return 0;
}
else{
int status = 0;
pid_t ret = waitpid(pid, &status, 0); //进程等待
if(ret == -1)
{
perror("waitpid");
return 1;
}
else
{
printf("exit single:%d, exit code:%d\n", (status & 0x7f),(status >> 8 & 0xff));
}
}
return 0;
}
可以看到在命令行上使用 “ls -a -l” 和使用进程程序替换得到的结果一致。
(2)execv && execvp
“v” 代表参数采用数组的形式传递 – argv 是一个指针数组,数组里面的每一个元素都是指针,每一个指针都指向一个参数 (字符串),同样最后一个元素指向 NULL,代表参数传递完毕;
(3)execle && execvpe
“e” 代表环境变量 – 和 argv 一样,envp 也是一个指针数组,数组里面的每个元素都是一个指针,指向一个环境变量 (字符串),我们可以显式初始化 envp 来传递我们自定义的环境变量,但是这也代表着我们放弃了系统环境变量