在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函数将退出码转换成字符串描述退出码含义
# includestreror(退出码)
正常终止:
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); //阻塞式的等待!一般都是在内核中阻塞,等待被唤醒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调度)若等待成功但pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若结束,则返回该子进程的ID。
举例:
#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 1111printf("等待子进程成功, ret: %d, 子进程收到的信号编号: %d,子进程退出码: %d\n", ret, status & 0x7F, (status >> 8) & 0xFF); //0xff --> 0000...000 1111 1111 //上面的获取子进程收到信号编号和退出码的方式比较繁琐,可以采用系统提供的status宏: if(WIFEXITED(status)){//子进程是正常退出的printf("子进程执行完毕,子进程退出码:%d\n",WEXITSTATUS(status));}else{printf("子进程异常退出:%d\n",WIFEXITED(status));}} }
}
💡wait(pid,NULL,0) == wait(NULL)
wait/waipid
就是读取子进程的tast_struct中的exit_code,exit_signal
,父进程没有读取内核数据结构对象的权限,但是wait/waipid
是系统调用,操作系统拥有这个权限。父进程非阻塞等待执行案例:
#include
#include
#include
#include
#include
#include
#include typedef void (*handler_t)(); //函数指针类型
std::vector 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); //以非阻塞方式等待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
#includeint main()
{pid_t id = fork();//如果不创建子进程,那么替换的只能是父进程,这样替换子进程而不影响父进程 //因为想让父进程聚焦再读取数据,解析数据,指派进程执行代码的功能if (id == 0){//子进程---子进程加载新程序的时候,是写入,发生写时拷贝并将父子代码分离//父子进程再代码和数据上就彻底分开了//ls-a-lprintf("子进程开始运行,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
#includeint main()
{pid_t id = fork();if (id == 0){ //ls-a-lprintf("子进程开始运行,pid:%d\n", getpid());sleep(3);//execl("/user/bin/ls", "ls", "-a", "-l", NULL);execlp("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 execle(const char *path, const char *arg, ...,char *const envp[]);
#include
#include
#include
#include
#define NUM 16int 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 16int 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
#includeint main()
{pid_t id = fork();if (id == 0){//ls-a-lprintf("子进程开始运行,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退出程序。
进程替换和应用场景有关,有时候必须让子进程执行新的程序。