目录
进程创建
进程终止
进程等待
输出型参数status
wait
waitpid
阻塞等待
非阻塞等待
进程程序替换
替换原理
程序替换函数
execl
execlp
execv
execvp
execle
execvpe
fork函数可以在已存在的进程中创建一个新的进程,新创建的为子进程,原来的进程是父进程。
pid_t id = fork();
fork()函数有两个返回值,如果进程创建成功,给父进程返回子进程的pid给子进程返回0。那么也就是说同一个id(同一个id,地址一样,返回值不一样)变量,会保存两个不同的值!
父子进程代码共享,父子进程再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本。
int main()
{
return 0;
}
return 0;这条代码几乎是每天都要写的,实际上返回的数据,我们可以将其当做退出时,对应的退出码。其作用可以表示进程执行的结果是否正确。
●退出码:0表示成功,非0表示失败,非0具体是几表示不同的错误!
echo $? //查看最近一个进程在命令行中执行完毕时对应的退出码
ls 后乱输一个字符串,会出现一个报错。此时查看到错误码是2,也就是说2号错误码表示的错误就是下图红框中的错误。
exit
exit能终止一个进程,传的参数就是对应的退出码:
总结:程序退出的几种可能包括正常退出(main函数中return)和强制退出(任意地方调用exit)。进程退出会返回退出码,退出码为0表示正常,非0表示不正常。
僵尸进程问题:当子进程退出,而父进程不对其进行数据的读取,那么该子进程就会进入僵尸状态。僵尸进程会带来资源泄露的问题,对于僵尸进程问题的解决,可以让父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息。
输出型参数status
wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。如果传递NULL,表示不关心子进程的退出状态信息。反之,操作系统会根据该参数,将子进程的退出信息反馈给父进程。但是,退出信息并不是直接获取的,status有自己的位图结构。
如果我们像奥获取退出状态和终止信号的时候都要用上图中的位运算的话有一点点的麻烦,
其实还可以使用下述的宏:
●WIFEXITED(status): 若为正常终止子进程(终止信号为0)返回的状态,则为真。(查看进程是否是正常退出)
●WEXITSTATUS(status): 若WIFEXITED非零(正常终止),提取子进程退出码。(查看进程的退出码)
●WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID。(非阻塞访问时,如果返回0,表示子进程还未退出,轮询等待,当正常结束的时候等待成功)测试异常终止:
int c = cnt/0;
在上述测试代码中,添加一句除零操作。最终程序异常终止,打印sig大于0!
wait
pid_t wait(int *status);
参数:获取子进程的退出状态
返回值:成功,返回被等待子进程的pid。失败,返回-1。
#include
#include #include #include #include int main() { pid_t id = fork(); if(id == 0) { //子进程 int cnt = 10; while(cnt) { printf("我是一个子进程:pid=%d,ppid=%d,cnt=%d\n",getpid(),getppi d(),cnt--); sleep(1); } exit(-1); } //子进程不会执行到这里。 sleep(15); //当不关心退出状态时可以传NULL pid_t ret = wait(NULL); if(id>0) { printf("等待成功!ret = %d\n",ret); } return 0; } 循环查看进程状态的脚本:
while :; do ps axj | head -1 && ps axj | grep main | grep -v grep; sleep 1; done
测试结果:
观察测试结果可以发现:子进程执行一段时间退出后,父进程还没退出,也没对子进程回收资源。这时子进程进入了僵尸状态,过了一段时间后,父进程调用wait(),检测到子进程程已经退出,wait立即返回,并且释放资源,获得子进程退出信息。
waitpid
pid_t waitpid(pid_t pid, int *status, int options);
Pid=-1,等待任一个子进程。与wait等效。Pid>0.等待其进程ID与pid相等的子进程。
当正常返回的时候waitpid返回收集到的子进程的进程ID;如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;如果调用中出错,则返回-1。阻塞等待
简单的理解,就是一直检查子进程的状态,直至子进程退出,在这期间处于阻塞。
int main() { pid_t id = fork(); if(id == 0) { //子进程 int cnt = 5; while(cnt) { printf("我是子进程,pid=%d,ppid=%d,cnt=%d\n",getpid(),getppid(),cnt--); sleep(1); } exit(10); } //阻塞式等待 int status = 0; int ret = waitpid(id,&status,0); if(ret > 0) { //是否正常退出 if(WIFEXITED(status)) { //判断子进程运行结果 printf("exit code:%d\n",WEXITSTATUS(status)); } else { printf("child exit not normal!\n"); } } return 0; }
非阻塞等待
采用轮询的方式,如果子进程还没退出,父进程可以做些其他的事情,不会将所有精力放在等待上。过段时间再次检测子进程。
int main() { pid_t id = fork(); if(id == 0) { //子进程 int cnt = 5; while(cnt) { printf("我是子进程,pid=%d,ppid=%d,cnt=%d\n",getpid(),getppid(),cnt--); sleep(3); } exit(10); } //父进程 //非阻塞等待 int status = 0; while(1)//轮询 { pid_t ret = waitpid(id,&status,WNOHANG);//非阻塞 if(ret == 0) { //等待成功,子进程未退出 printf("wait done,but child id running....,parent running other thing\n"); } else if(ret >0) { //等待成功,子进程成功 printf("wait sucess,exit code:%d,sig:%d\n",(status>>8)&0xFF,status&0x7F); break; } else { //等待失败 printf("wait call failed\n"); } sleep(1); } return 0; }
当使用fork()创建一个进程后,通常情况下要么让子进程执行父进程代码的一部分,要么让子进程执行一个全新的程序。要想实现后者需要加载磁盘上的指定程序和执行新程序的代码和数据两个步骤,这一过程叫做进程程序替换。
程序替换的本质就是将指定程序的代码和数据加载(覆盖)到原进程的代码和数据位置。在这个过程中并没有创建新的进程!
观察下述代码在结合上图理解程序替换,代码中的execl是程序替换函数,后面会详细介绍。
#include
#include
int main()
{
printf("程序替换\n");
execl("/usr/bin/ls","ls","-a","-l",NULL);
printf("Hello\n");
printf("Hello\n");
printf("Hello\n");
printf("Hello\n");
printf("Hello\n");
printf("Hello\n");
return 0;
}
观察程序结果不难发现,程序替换后,只执行了新的程序。后面的一串打印并没有执行,由此可以得出结论:程序替换是对原代码和数据的覆盖。
图解分析再次理解替换原理:
#include
#include
int main()
{
pid_t id = fork();
if(id == 0)
{
//子进程
printf("子进程进行程序替换\n");
execl("./new","new",NULL);
}
else
{
printf("我是父进程,pid=%d\n",getpid());
}
return 0;
}
new.c
#include
int main()
{
printf("\n我是一个被替换进来的新程序!\n");
printf("我是一个被替换进来的新程序!\n");
printf("我是一个被替换进来的新程序!\n");
printf("我是一个被替换进来的新程序!\n");
return 0;
}
上述函数统称为exec函数,它们有一个统一的特点就是exec函数调用成功的话是没有返回值的。这个也很合理,程序替换以后要执行的是新程序,设置返回值的话也没什么价值。但是如果失败的话返回-1。
关于exec系列函数,可以根据它们的名字了解到它们的用法:
l(list) : 表示参数采用列表
v(vector) : 参数用数组
p(path) : 有p自动搜索环境变量PATH
e(env) : 表示自己维护环境变量
execl(const char *path, const char *arg, ...);
要传的参数的参数分别是新程序的路径,程序名称,要执行程序的选项,三个点表示可变参数,最后以NULL结尾。
#include
#include int main() { pid_t id = fork(); if(id == 0) { //子进程 printf("子进程开始程序替换\n"); execl("/usr/bin/ls","ls","-a","-l",NULL); } else { printf("父进程执行!\n"); } return 0; }
execlp(const char *file, const char *arg, ...);
p表示path, 会自动搜索环境变量。需要注意的是,在下述代码的传参中前两个参数重复,但是不能省略,它们一个告诉操作系统要执行谁,一个告诉怎样执行。
#include
#include int main() { pid_t id = fork(); if(id == 0) { //子进程 printf("子进程开始程序替换\n"); execlp("pwd","pwd",NULL); } else { printf("父进程执行!\n"); } return 0; }
execv(const char *path, char *const argv[]);
v代表vector,将要执行的程序名和选项都存在数组中!第一个参数传要替换的程序路径,他第二个参数直接传数组。
#include
#include int main() { pid_t id = fork(); if(id == 0) { //子进程 printf("子进程开始程序替换\n"); char* const argv[] = { "ls", "-a", "-l", NULL }; execv("/usr/bin/ls",argv); } else { printf("父进程执行!\n"); } return 0; }
execvp(const char *file, char *const argv[]);
p表示会自动搜索环境变量,v表示直接传包含数组即可。
#include
#include int main() { pid_t id = fork(); if(id == 0) { //子进程 printf("子进程开始程序替换\n"); char* const argv[] = { "ls", "-a", "-l", NULL }; execvp("ls",argv); } else { printf("父进程执行!\n"); } return 0; }
execle(const char *path, const char *arg, ..., char * const envp[]);
execle三个三数分别是要执行程序的路径,和要执行的程序,可变参数和环境变量表。下述代码中,调用自己写的new程序。
#include
#include #include int main() { printf("PATH:%s\n",getenv("PATH")); printf("HOME:%s\n",getenv("HOME")); printf("MYENV:%s\n",getenv("MYENV")); printf("我是一个新程序!\n"); printf("我是一个新程序!\n"); printf("我是一个新程序!\n"); printf("我是一个新程序!\n"); printf("我是一个新程序!\n"); printf("我是一个新程序!\n"); return 0; } #include
#include #include int main(int arg,char* arec,char* arev) { pid_t id = fork(); if(id == 0) { //子进程 printf("子进程开始程序替换\n"); char* const env[]= { (char*)"MYENV=1003", NULL }; extern char** environ; putenv((char*)"MYENV=1003");//自定义环境变量导入到环境变量表中 execle("./new","new",NULL,environ); } else { printf("父进程执行!\n"); } return 0; }
execvpe(const char *file, char *const argv[], char *const envp[]);
和上述传参规律一样,第一个参数告诉操作系统要执行谁,第二个参数传一个表,表中内容包括要执行的程序名和选项表的最后以NULL结尾。最后的参数为环境变量表。
#include
#include #include int main(int arg,char* arec,char* arev) { pid_t id = fork(); if(id == 0) { //子进程 printf("子进程开始程序替换\n"); char* const env[]= { (char*)"MYENV=1003", NULL }; extern char** environ; putenv((char*)"MYENV=1003");//自定义环境变量导入到环境变量表中 char* const ggv[]={"new",NULL}; execvpe("./new",ggv,environ); } else { printf("父进程执行!\n"); } return 0; }