进程退出
进程退出的时候,都会返回进程退出码,当你在命令行下执行命令的时候,同样会返回进程退出码,可以使用 echo $? 查看最近一个进程在命令行中执行完毕时对应的退出码,跟main函数的return返回值是一个道理。
进程退出的情况:1、main函数return正常返回0; 2、代码跑完了,结果不正确,return !0; 3、代码没跑完,程序异常结束,退出码无意义。而进程退出码最大的意义在于情况2,让你知道代码是为何出错了。
进程是如何退出的:1、main函数return返回;2、任意地方调用exit(code)返回;3、_exit()函数的调用。其中exit(code)终止进程,主动刷新缓冲区而_exit()终止进程,不会刷新缓冲区。其实exit()是c语言封装的函数,其底层是_exit()函数,是一个系统调用,归操作系统调用。而对于OS而言,在调用_exit()之后,是肯定会刷新缓冲区的,证明了用户的数据一定不是在系统调用及其往下,应该是在用户级的缓冲区---基础IO的时候。
进程等待:但进程完成自己的任务之后,需要将数据传输出去或者被回收资源的时候的状态称之为僵尸状态。可以通过进程等待(waitpid函数方法)的方式去解决僵尸进程的问题。当父子进程一起执行的时候,本该是由父进程负责回收子进程的资源,但是父进程率先退出的时候,此时的子进程便会被“寄养”在操作系统名下,此时的进程称之为孤儿进程,但子进程的任务完成之后,又操作系统回收资源,获取子进程退出信息。
如何获取子进程的退出信息:
#include<stdio.h>
#include<stdlib.h>
#include <string.h>
#include<unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
进程退出码///
pid_t id =fork();
if(id==0)
{
//child
int cnt=5;
while(cnt)
{
printf("child process id :%d ppid: %d cnt %d\n",getpid(),getppid(),cnt);
cnt--;
sleep(1);
}
}
//parent
int status=0; //在waitpid中是低16位用来做输出型参数,有自己的位图结构
pid_t pd=waitpid(id,&status,0); //waipid函数进程等待,status是一个输出型参数
if(id>0)
{
//sign number 是status的低7位,用来表示代码是否正常退出,0表示正常退出,其余的kill -l查看
//child exit code 是status的低16位的高8位,是自己自定义的错误退出码,根据退出码查询对应的错误
printf("wait success %d,sign number:%d child exit code: %d \n",pd,(status&0x7f),(status>>8)&0xff);
}
return 0;
}
//vim 的底行模式下的批量替换: %s/xxxx/替换字母/g
进程退出之后会变成僵尸--会把自己的退出结果写入到自己的PCB中,return的结果也是被放进到PCB里面,而上述代码提到的wait/waitpid是一个系统调用,OS也有资格去读取子进程的PCB,以及获取退出子进程PCB的进程退出码。系统的waipid有阻塞和非阻塞两种方式:
int ret = waitpid(id,&status,WNOHANG);//轮询检测
int ret = waitpid(id,&status,0); //阻塞检测
进程等待:通过系统调用让父进程等待子进程的一种方式,为了释放子进程僵尸以及获取子进程的进程退出码,方式是通过调用wait/waitpid的方式。
进程替换
创建子进程的目的不仅仅在于让子进程执行父进程代码的一部分,可以使用程序替换的方式去让子进程执行一个全新的程序,让子进程加载磁盘上的指定程序,执行新程序的代码和数据,这就是进程替换。简单的进程替换:
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <assert.h>
#include <stdlib.h>
int main()
{
//.c----.exe ----load -----processs-->运行-->执行替换好的代码
printf("process is running\n");
//只要是一个函数,调用就可能失败,失败了之后就不会被覆盖
//exec*为什么没有成功返回值呢?因为成功了就和接下来的代码,返回值毫无意义,所以exec*只要返回了一定是错误的
execl("/usr/bin/ls"/*执行程序的位置*/, "ls","-a" , NULL/*你想要怎么执行*/); //所有的exec* 程序必须都以NULL结尾
perror(execl);
printf("process is end\n"); //printf不会被执行,是因为被替换的代码覆盖了,无法被执行
exit(1);
}
对于上述的函数
int execl(const char *path, const char *arg, ...);
//传入替换进程的位置,并如何执行该进程,后面可以加参数,
//其中 ...表示可变参数列表,在传参的最后exec*类别的函数
//都要以NULL结尾
理解原理
在程序替换之前,如黑线所示,父子进程页表映射的是同一块儿物理地址空间,在程序替换的时候,因为需要覆盖原来程序的代码和数据,此时发生写时拷贝,出现红色线条发生的一切,主要是为了不影响父进程的执行。虚拟地址空间+页表保证进程的独立性,一旦有执行流想替换代码或者数据的时候,就会发生写时拷贝。
使用系统程序覆盖子进程的程序
int main()
{
int id =fork();
assert(id!=-1);
if(id==0)
{
//子进程
sleep(1);
printf("child process\n");
//execl("/usr/bin/ls","ls", "-a","-l","--color=auto",NULL);
//execlp("ls","ls", "-a","-l","--color=auto",NULL); //两个ls不重复,一个是告诉执行那个程序,一个是指令
char *const arg_[]={
"ls",
"-a",
"-l",
"--color=auto",
NULL
};
execv("/usr/bin/ls", arg_);
exit(1); //这里的代码在成功替换之后,是不会执行的
}
//父进程
int status=0;
pid_t ret = waitpid(id,&status,0); //阻塞等待
if(ret>0)
{
printf("wait sucess exit code:%d sig:%d\n",(status>>8)&0xff,status&0x7f);
}
return 0;
}
int execl(const char *path, const char *arg, ...); //l: list ,告诉程序的地址,并且如何执行,最后以NULL结尾
int execlp(const char *file, const char *arg, ...); //p:环境变量,只需告诉函数要执行程序的名称,会自己去环境变量中寻找,arg参数同上,最后以NULL结尾
int execv(const char *path, char *const argv[]);// v vcector,传入参/数列表即可,argv的参数必须是NULL结尾
int execvp(const char *file, char *const argv[]);//vector path均有
int execle(const char *path, const char *arg,..., char * const envp[]); //e:自定义环境变量
int execvpe(const char *file, char *const argv[],char *const envp[]);
除了可以调用系统的程序,还可以调用自己写的程序
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
int id =fork();
assert(id!=-1);
if(id==0)
{
execl("./mybin","./mybin",NULL);
exit(1); //这里的代码在成功替换之后,是不会执行的
}
//父进程
int status=0;
pid_t ret = waitpid(id,&status,0); //阻塞等待
if(ret>0)
{
printf("wait sucess exit code:%d sig:%d\n",(status>>8)&0xff,status&0x7f);
}
return 0;
}
不仅可以调用C语言,还可以调用C++,python,shell脚本等程序。
向子进程传入环境变量
int main()
{
int id =fork();
assert(id!=-1);
if(id==0)
{
extern char** environ;
putenv((char*)"MYENV=4444424142121"); //将指定的环境变量导入到系统中,,environ指向的环境变量中
execle("./mybin","./mybin",NULL,environ); //实际上。默认的环境变量你不传,子进程也可以获取
exit(1); //这里的代码在成功替换之后,是不会执行的
}
//父进程
int status=0;
pid_t ret = waitpid(id,&status,0); //阻塞等待
if(ret>0)
{
printf("wait sucess exit code:%d sig:%d\n",(status>>8)&0xff,status&0x7f);
}
return 0;
}
从argv中获取环境变量
int main(int argc,char* argv[])
{
int id =fork();
assert(id!=-1);
if(id==0)
{
//子进程
sleep(1);
printf("child process\n");
execvp(argv[1],&argv[1]); //这里argv[0]-->./sub argv[1] =ls ....
exit(1); //这里的代码在成功替换之后,是不会执行的
}
//父进程
int status=0;
pid_t ret = waitpid(id,&status,0); //阻塞等待
if(ret>0)
{
printf("wait sucess exit code:%d sig:%d\n",(status>>8)&0xff,status&0x7f);
}
return 0;
}
上述的六个exec*函数都是基于execve这个系统调用接口封装的。
路径
每个进程都有自己的工作路径,在根路径下的/proc/pid -al 的
其中cwd是当前进程的工作目录。
child("cwd");修改工作目录
之前写的shell,cd的时候,路径没有变化---->cd的时候,目录不改变的主要原因是子进程执行完cd命令之后,推出了,继续用的父进程工作路径。
fork()-->子进程执行的 cd->子进程有自己的工作目录 ->更改的是子进程的目录!-->子进程执行完毕-->继续用的父进程,即shell。