2.3.4:return、exit与_exit之间的区别与联系
1:进程创建
1:进程 = 内核相关的数据结构(task_struct + mm_struct + 页表) + 代码 + 数据.
2:创建子进程会经过以下步骤.
- 分配新的内存块和内核数据结构给子进程.
- 将父进程部分数据结构内容拷贝给子进程(子进程要继承于父进程).
- 添加子进程到系统的进程列表中
- 代码:子进程与父进程共享代码
- 数据:则通过写时拷贝的方式
如果理解进程具有独立性
- 根本原因在于:进程 = 内核的相关管理数据结构(task_struct + mm_struct(地址空间) + 页表) + 代码 + 数据.子进程有自己的task_struct + mm_struct(地址空间) + 页表.
- 代码虽然与父进程共享,但是与父进程之间互不影响.
- 而数据是通过写时拷贝的方式进行复用.
- 因此无论从内核的相关管理数据结构还是从代码以及数据,它都是独立的.
1.1:fork函数初识
-
在linux中fork函数时非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程.
进程调用fork,当控制转移到内核中的fork代码后,内核会做:
- 分配新的内存块和内核数据结构给子进程.
- 将父进程部分数据结构内容拷贝至子进程.
- 添加子进程到系统进程列表中.
- fork返回,开始调度器调度.

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <string.h>
#include <stdlib.h>
int main()
{
pid_t pid;
printf("Before: pid is %d\n", getpid());
if((pid = fork()) == -1)
{
perror("fork() fail");
exit(-1);
}
printf("After:pid is %d, fork return %d\n", getpid(), pid);
return 0;
}

这里看到了三行输出,一行 before ,两行 after 。进程114527 先打印 before 消息,然后它有打印 after 。另一个 after消息有114528 打印的。注意到进程114528 没有打印 before,为什么呢?如下图所示.

- fork之前父进程独立执行,而fork之后父子两个执行流分别执行.
PS:fork之后,父进程和子进程谁先执行完全由调度器决定.
1.2:fork函数返回值
fork函数为什么要给子进程返回0,给父进程返回子进程的PID?
一个父进程可以创建多个子进程,而一个子进程只能有一个父进程.因此,对于子进程来说,父进程是不需要被标识的;而对于父进程来说,子进程是需要被标识的,因为父进程创建子进程的目的是让其完成任务的,父进程只有知道了子进程的pid才能更加有针对性地去给子进程指定任务.
为什么fork函数有两个返回值
父进程调用fork函数后,为了创建子进程,fork函数内部将会进行一系列操作,包括创建子进程的进程控制块、创建子进程的地址空间、创建子进程对应的页表等等.子进程创建完毕后,操作系统还需要将子进程的进程控制块添加到系统进程列表中,此时子进程便创建完毕了

那么也就是说,在fork函数内部执行return语句以前,子进程就已经创建完毕了,那么之后的return语句不仅需要父进程执行,子进程也同样需要执行,这就是fork函数有两个返回值的原因.
1.3:写时拷贝

这种在需要进行数据修改时再进行拷贝的技术,称为写时拷贝.


1:为什么数据要进行写时拷贝?
进程具有独立性.多个进程在运行时,需要独享各种资源,多进程运行期间互不干扰,不能让子进程的改变影响到父进程.
2:为什么不在创建子进程的时候进行写时拷贝.
子进程不一定会使用父进程的所有数据,并且在子进程不对数据进行写入的情况下,没有必要对数据进行拷贝,那么应该按需分配,在需要修改数据的时候再分配(延时分配),这样可以高效的使用内存空间,避免空间的浪费.
3:代码会不会进行写时拷贝?
90%的情况下是不会的,但这并不代表代码不能进行写时拷贝,例如在进行进程替换的时候,则需要进行代码的写时拷贝.
1.3:fork函数常规用法
- 一个进程希望复制自己,使子进程同时执行不同的代码段。例如父进程等待客户端请求,生成子进程来处理请求。
- 一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数。
1.4:fork函数调用失败原因
fork函数创建子进程也可能会失败,有以下两种情况:
- 系统中有太多的进程,内存空间不足,子进程创建失败。
- 实际用户的进程数超过了限制,子进程创建失败
2:进程终止
进程 = 内核相关的数据结构(task_struct + mm_struct + 页表) + 代码 + 数据.
创建进程时,会先创建进程的内核的相关管理数据结构.
那么终止是在做什么呢
- 释放曾经的代码和数据所占据的空间.
- 释放内核数据结构.
那么进程终止存在以下三种情况
- 代码跑完,结果正确.
- 代码跑完,结果不正确.
- 代码执行的时候,出现了异常,提前退出了.
- 第一种情况和第二种情况可以通过进程的退出码来决定.
- 第三种情况,得通过OS发给进程的信号来确定.
2.1:退出码
- 我们都知道main函数是代码的入口,但实际上main函数只是用户级别代码的入口,main函数也是被其他函数调用的,例如在VS2013当中main函数就是被一个名为__tmainCRTStartup的函数所调用,而__tmainCRTStartup函数又是通过加载器被操作系统所调用的,也就是说main函数是间接性被操作系统所调用的.
- 既然main函数是间接性被操作系统所调用的,那么当main函数调用结束后就应该给操作系统返回相应的退出信息,而这个所谓的退出信息就是以退出码的形式作为main函数的返回值返回,一般以0表示代码成功执行完毕,以非0表示代码执行过程中出现错误,这就是为什么我们都在main函数的最后返回0的原因.
代码1
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
printf("I am process and My pid =%d,ppid == %d\n",getpid(),getppid());
sleep(1);
return 99;
}

当我们的代码运行起来就变成了进程,当进程结束后main函数的返回值实际上就是该进程的进程退出码,我们可以使用echo $?命令查看最近一次进程退出的退出码信息.
- 退出码存在的意义就是在于:告诉关心方,我将任务完成得怎么样了.
- 退出码为0则表示成功,为非0表示失败.(博主使用99是为了方便uu们查看).
代码2
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <string.h>
int main()
{
for (int errorcode = 0; errorcode <= 255; errorcode++)
{
printf("errorcode == %d:%s\n",errorcode,strerror(errorcode));
}
return 0;
}

- 1,2,3,4,5,6.....不同的非0值,一方面表示失败,另一方面表示失败的原因.
父进程bash为什么要得到子进程的退出码呢?
- 要知道子进程的退出情况(成功,失败,以及失败的原因是什么)------>核心宗旨是为用户负责.
2.2:异常
异常这里后面会有专门的部分讲解,这里首先uu们简单理解下就好
- 可以想象一个场景,编程运行的时候,突然崩溃了,这个时候的根本原因是:发生了异常,操作系统系统发现进程做了不该做的事情,从而杀死了进程.
- 那么为什么会出现异常,原因在于:进程出现了异常,本质是因为进程收到了OS发给进程的信号.
- 一旦出现了异常,退出码就没有任何意义了.
代码1
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
while (1)
{
printf("I am process and My pid =%d,ppid == %d\n",getpid(),getppid());
sleep(1);
}
return 0;
}

代码2
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
int * p = NULL;
while (1)
{
printf("I am a Process And My pid == %d\n",getpid());
*p = 100;
sleep(2);
}
return 0;
}


怎么判断异常.
在进程退出的时候,查看进程的退出信号是多少,就可以判断进程为什么异常了.
如何判断进程终止.
- 先确认是不是异常.
- 不是异常,就一定是代码跑完了,看退出码即可.
总结:衡量一个进程退出,我们只需要两个数字:退出码与退出信号

2.3:进程常见退出方法
进程正常终止一般有三种
- 从main函数return(非main函数return表示函数结束)
- 调用exit.
- _exit.
2.3.1:main函数return
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
printf("I am process and My pid =%d,ppid == %d\n",getpid(),getppid());
sleep(1);
return 0;
}

2.3.2:exit函数
使用exit函数退出进程也是我们常用的方法,exit函数可以在代码中的任何地方退出进程,并且exit函数在退出进程前会做一系列工作:
- 执行用户通过atexit或on_exit定义的清理函数。
- 关闭所有打开的流,所有的缓存数据均被写入。
- 调用_exit函数终止进程。

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>
void Test()
{
printf("hello world");
exit(1);
}
int main()
{
Test();
return 0;
}

上述代码使用exit终止进程前会将缓冲区当中的数据输出.
2.3.3:_exit函数
使用_exit函数退出进程的方法我们并不经常使用,_exit函数也可以在代码中的任何地方退出进程,但是_exit函数会直接终止进程,并不会在退出进程前会做任何收尾工作.
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>
void Test()
{
printf("hello world");
_exit(1);
}
int main()
{
Test();
return 0;
}

使用_exit终止进程,则缓冲区当中的数据将不会被输出。

2.3.4:return、exit与_exit之间的区别与联系
- 只有在main函数当中的return才能起到退出进程的作用,子函数当中return不能退出进程.
- 而exit函数和_exit函数在代码中的任何地方使用都可起到退出进程的作用.
- 使用exit函数退出进程前,exit函数会执行用户定义的清理函数,冲刷缓冲区,关闭流等操作,然后再终止进程,而_exit函数会直接终止进程,不会做任何收尾工作.

return、exit、_exit之间的联系
- 执行return num等同于执行exit(num),因为调用main函数运行结束后,会将main函数的返回值当做exit的参数来调用exit函数。
- 使用exit函数退出进程前,exit函数会先执行用户定义的清理函数、冲刷缓冲,关闭流等操作,然后再调用_exit函数终止进程。


3:进程等待
结论:任何子进程,在退出的前提下,一般必须要被父进程进行等待
进程在退出的时候,如果父进程不管不顾,那么此退出进程, 就会进入Z状态(僵尸进程)
为什么要有进程等待.
- 父进程通过等待,解决子进程退出的僵尸问题,回收系统资源(一定要考虑的因素).
- 获取子进程的退出信息,得知子进程是因为什么原因而退出.
进程等待的必要性.
- 子进程退出,父进程如果不读取子进程的退出信息,子进程就会变成僵尸进程,进程造成了内存泄漏.
- 进程一旦变成僵尸进程,那么就算是kill - 9命令也无法将其杀死,因为谁也无法杀死一个死去的进程.
- 对于一个进程来说,最关心自己的就是其父进程,因为父进程需要知道自己安排给子进程的任务完成得如何.
- 父进程通过进程等待的方式,回收子进程资源,获取子进程的退出信息.
3.1:进程等待的方法
3.1.1:wait方法

- 作用:等待任意子进程.
- 返回值:等待成功返回被等待进程的pid,等待失败返回-1.
- 参数:输出型参数,获取子进程的退出状态,不关心可设置为NULL.
代码1
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>
void ChildRun()
{
int count = 5;
while(count)
{
printf("I am Child process and my pid == %d,ppid == %d\n",getpid(),getppid());
sleep(2);
count--;
}
}
int main()
{
printf("I am Father process and my pid == %d,ppid == %d\n",getpid(),getppid());
pid_t id = fork();
if(id == 0)
{
ChildRun();
printf("Child Quit\n");
exit(0);
}
//父进程在等待
pid_t rid = wait(NULL);
if(rid > 0)
{
printf("Wait Success,rid == %d\n",rid);
}
}

代码2
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>
void ChildRun()
{
int count = 5;
while(count)
{
printf("I am Child process and my pid == %d,ppid == %d\n",getpid(),getppid());
sleep(1);
count--;
}
}
int main()
{
printf("I am Father process and my pid == %d,ppid == %d\n",getpid(),getppid());
pid_t id = fork();
if(id == 0)
{
ChildRun();
printf("Child Quit\n");
exit(0);
}
sleep(10);
//父进程在等待
pid_t rid = wait(NULL);
if(rid > 0)
{
printf("Wait Success,rid == %d\n",rid);
}
sleep(3);
printf("Father Process Quit\n");
}

3.1.2:waitpid方法
函数原型:
pid_t waitpid(pid_t pid, int* status, int options);
返回值:
- 正常返回的时候waitpid返回收集到的子进程的进程pid
- 如果设置了选项WNOHANG,则调用中waitpid发现没有已退出的子进程可以收集,则返回0
- 如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在.
参数:
pid:
Pid = -1,等待任何一个子进程,与wait等效.
Pid > 0,等待其进程id与pid相等的子进程.
status(输出型参数,获取子进程的退出状态):
WIFEXITED(status):若为正常终止子进程返回的状态,则为真.(查看进程是否是正常退出)
WEXITSTATUS(status):若WIFEXITED为非零,提取子进程退出码.(查看进程的退出码)
不关心则可设置为NULL.
options:
WNOHANG:若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待.若正常结束,则返回该子进程的id.
代码1
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>
void ChildRun()
{
int count = 5;
while(count)
{
printf("I am Child process and my pid == %d,ppid == %d\n",getpid(),getppid());
sleep(1);
count--;
}
}
int main()
{
printf("I am Father process and my pid == %d,ppid == %d\n",getpid(),getppid());
pid_t id = fork();
if(id == 0)
{
ChildRun();
printf("Child Quit\n");
exit(0);
}
//Pid = -1,等待任何一个子进程,与wait等效.
pid_t rid = waitpid(-1,NULL,0);
if(rid > 0)
{
printf("Wait Success,rid == %d\n",rid);
}
}

代码2(等待指定进程退出)
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>
void ChildRun()
{
int count = 5;
while(count)
{
printf("I am Child process and my pid == %d,ppid == %d\n",getpid(),getppid());
sleep(1);
count--;
}
}
int main()
{
printf("I am Father process and my pid == %d,ppid == %d\n",getpid(),getppid());
pid_t id = fork();
if(id == 0)
{
ChildRun();
printf("Child Quit\n");
exit(0);
}
//等待指定进程退出
pid_t rid = waitpid(id,NULL,0);
if(rid > 0)
{
printf("Wait Success,rid == %d\n",rid);
}
sleep(3);
return 0;
}

代码3
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>
void ChildRun()
{
int count = 5;
while(count)
{
printf("I am Child process and my pid == %d,ppid == %d\n",getpid(),getppid());
sleep(1);
count--;
}
}
int main()
{
int status = 0;
printf("I am Father process and my pid == %d,ppid == %d\n",getpid(),getppid());
pid_t id = fork();
if(id == 0)
{
ChildRun();
printf("Child Quit\n");
exit(1);
}
sleep(10);
//等待指定进程退出,并且获取进程退出状态码
pid_t rid = waitpid(id,&status,0);
if(rid > 0)
{
printf("Wait Success,rid == %d\n",rid);
}
sleep(3);
printf("Father Process Quit and status == %d",status);
return 0;
}


- 如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退出信息.
- 如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞.
- 如果不存在该子进程,则立即出错返回~

3.2:获取进程status
- wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充.
- 如果传递NULL,表示不关心子进程的退出状态信息.
- 否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程.
- statu是一个整型变量,但status不能简单地当作整型来看待,status的不同比特位所代表的信息不同,具体细节如下.

在status的低16比特位中,高8位表示进程的退出状态,即退出码.进程若是被信号所杀掉,则低7位表示终止信号,而第8位比特位是core dump标志.
我们通过一系列位操作,就可以根据status得到进程的退出码和退出信号。
exitCode = (status >> 8) & 0xFF;//退出码
exitSignal = status & 0x7f;//退出信号
对于此,系统当中提供了两个宏来获取退出码和退出信号。
- WIFEXITED(status):用于查看进程是否是正常退出,本质是检查是否收到信号。
- WEXITSTATUS(status):用于获取进程的退出码。
exitNormal = WIFEXITED(status); //是否正常退出
exitCode = WEXITSTATUS(status); //获取退出码
需要注意的是,当一个进程非正常退出时,说明该进程是被信号所杀,那么该进程的退出码也就没有意义了.
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>
void ChildRun()
{
int count = 5;
while(count)
{
printf("I am Child process and my pid == %d,ppid == %d\n",getpid(),getppid());
sleep(1);
count--;
}
}
int main()
{
int status = 0;
printf("I am Father process and my pid == %d,ppid == %d\n",getpid(),getppid());
pid_t id = fork();
if(id == 0)
{
ChildRun();
printf("Child Quit\n");
exit(1);
}
sleep(10);
//等待指定进程退出,并且获取进程退出状态码
pid_t rid = waitpid(id,&status,0);
if(rid > 0)
{
printf("Wait Success,rid == %d\n",rid);
}
sleep(3);
printf("Father Process Quit and status == %d,child quit code:%d,child quit signal:%d\n",status,(status >> 8) & 0xFF,status & 0x7F);
return 0;
}

3.3:阻塞等待与非阻塞等待
在讲解阻塞等待与非阻塞等待前,我们先来看看子进程正常退出与非正常退出的情况
代码1(子进程正常退出)
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>
void ChildRun()
{
int count = 5;
while(count)
{
printf("I am Child process and my pid == %d,ppid == %d\n",getpid(),getppid());
sleep(1);
count--;
}
}
int main()
{
int status = 0;
printf("I am Father process and my pid == %d,ppid == %d\n",getpid(),getppid());
pid_t id = fork();
if(id == 0)
{
ChildRun();
printf("Child Quit....\n");
_exit(123);
}
//父进程休眠7s
sleep(7);
//等待指定进程退出,并且获取进程退出状态码
pid_t rid = waitpid(id,&status,0);
if(rid > 0)
{
printf("Wait Success,rid == %d\n",rid);
//wifexited判断子进程是否正常退出
if(WIFEXITED(status))
{
//wexitstatus获取进程的退出码
printf("Child Quit Success,Child exit Code == %d\n",WEXITSTATUS(status));
}
else
{
printf("Child Quit Unnormal\n");
}
}
else
{
printf("Wait Faild\n");
}
sleep(3);
return 0;
}

代码2(子进程非正常退出)
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>
void ChildRun()
{
int * p = NULL;
int count = 5;
while(count)
{
printf("I am Child process and my pid == %d,ppid == %d\n",getpid(),getppid());
sleep(1);
count--;
*p = 150;
}
}
int main()
{
int status = 0;
printf("I am Father process and my pid == %d,ppid == %d\n",getpid(),getppid());
pid_t id = fork();
if(id == 0)
{
ChildRun();
printf("Child Quit....\n");
_exit(123);
}
//父进程休眠7s
sleep(7);
//等待指定进程退出,并且获取进程退出状态码
pid_t rid = waitpid(id,&status,0);
if(rid > 0)
{
printf("Wait Success,rid == %d\n",rid);
//wifexited判断子进程是否正常退出
if(WIFEXITED(status))
{
//wexitstatus获取进程的退出码
printf("Child Quit Success,Child exit Code == %d\n",WEXITSTATUS(status));
}
else
{
printf("Child Quit Unnormal\n");
}
}
else
{
printf("Wait Faild\n");
}
sleep(3);
return 0;
}

3.3.1:阻塞等待
当子进程未退出时,父进程都在一直等待子进程退出,在等待期间,父进程不能做任何事情------>这种等待叫做阻塞等待.


3.3.2:非阻塞等待
父进程没有一直等待子进程退出,而是当子进程未退出时父进程可以做一些自己的事情,当子进程退出时再读取子进程的退出信息------>非阻塞等待.

因此waitpid如果是以阻塞等待的方式的调用的话,本质是在检测子进程的状态.

3.3.3:基于非阻塞接口的轮询检测方案
向waitpid函数的第三个参数potions传入
WNOHANG,这样一来,等待的子进程若是没有结束,那么waitpid函数将直接返回0,不予以等待。而等待的子进程若是正常结束,则返回该子进程的pid。
例如,父进程可以隔一段时间调用一次waitpid函数,若是等待的子进程尚未退出,则父进程可以先去做一些其他事,过一段时间再调用waitpid函数读取子进程的退出信息。
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>
typedef void(*Fun_T)();
#define N 3
Fun_T task[N] = {NULL};
void PrintLog()
{
printf("PrintLog\n");
}
void DownLoad()
{
printf("DownLoad\n");
}
void MySQLDataSync()
{
printf("MySQLDataSync\n");
}
//加载任务
void LoadTask()
{
//函数名表示地址
task[0] = PrintLog;
task[1] = DownLoad;
task[2] = MySQLDataSync;
}
void HandleTask()
{
for (size_t i = 0; i < N; i++)
{
//拿到函数对应的地址,然后进行调用
task[i]();
}
}
//非阻塞轮询等待父进程做其他事情
void DoOtherThing()
{
HandleTask();
}
void ChildRun()
{
int count = 5;
while(count)
{
printf("I am Child process and my pid == %d,ppid == %d\n",getpid(),getppid());
sleep(1);
count--;
}
}
int main()
{
printf("I am Father process and my pid == %d,ppid ==%d\n",getpid(),getppid());
pid_t id = fork();
if(0 == id)
{
ChildRun();
printf("Child Quit\n");
exit(123);
}
LoadTask();
//非阻塞轮询
while (1)
{
int status = 0;
//WNOHANG表示非阻塞等待
pid_t Return_id = waitpid(id,&status,WNOHANG);
//若pid指定的子进程没有结束,则waitpid函数返回0,不予以等待
if (Return_id == 0)
{
sleep(1);
printf("Child is Running,Please Check Next Time\n");
DoOtherThing();
}
//大于0表示等待成功
else if(Return_id > 0)
{
printf("Wait Success,rid == %d\n",Return_id);
//wifexited判断子进程是否正常退出
if(WIFEXITED(status))
{
//wexitstatus获取进程的退出码
printf("Child Quit Success,Child exit Code == %d\n",WEXITSTATUS(status));
}
else
{
printf("Child Quit Unnormal\n");
}
break;
}
else
{
printf("Wait Failed\n");
break;
}
}
return 0;
}
运行结果就是,父进程每隔一段时间就去查看子进程是否退出,若未退出,则父进程先去忙自己的事情,过一段时间再来查看,直到子进程退出后读取子进程的退出信息。


4:进程程序替换
在讲进程程序替换前,我们先看一段代码
代码1
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
printf("Test Execl Begin\n");
execl("/usr/bin/ls","ls","-l","-a",NULL);
printf("Test Execl End\n");
return 0;
}

我们可以清晰地看到,上述代码并未执行 printf("Test Execl End\n");这段代码,那么这是为什么呢?这就牵扯到exec*系列函数的原理了
替换原理
- 进程 = 内核数据结构 + 代码 + 数据,而进程替换的本质是将进程的代码与数据进行了替换,站在被替换进程的角度:本质就是这个程序(在上述例子是ls指令)被加载到内存中.
那么程序为什么要加载到内存当中
- 因为冯诺依曼体系结构规定代码和数据只能够被CPU访问,CPU只能够就近访问内存,不能够访问外设,因此必须将磁盘里的代码和数据加载到内存当中.
所以exec*系列的函数,执行完毕以后,后续的代码就不见了,因为旧进程代码和数据被exec*系列函数替换掉了,但是并没有替换旧进程的内核数据结构(因此在执行exec*系列函数的过程中,没有创建新进程).


代码2
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
int main()
{
printf("Test Execl Begin\n");
pid_t id = fork();
if(id == 0)
{
sleep(3);
execl("/usr/bin/ls","ls","-l","-a",NULL);
exit(1);
}
int status = 0;
//为0表示阻塞等待
pid_t Rid = waitpid(id,&status,0);
if (Rid > 0)
{
printf("Father Wait Success,Child Exit Code:%d\n",WEXITSTATUS(status));
}
printf("Test Execl End\n");
return 0;
}

我们可以看到,退出码是0而不是0,这是因为我们创建了子进程,子进程执行了execl函数,进行了程序替换,以至于没有执行execl函数后面的代码.
- 创建子进程,目的是为了让子进程完成任务.但完成任务的方式有两种.
- 让子进程执行父进程代码的一部分.
- 让子进程执行一个全新的程序(程序替换).
- 父子进程之间要保证各自进程的独立性,默认不修改的情况下,父子进程共享代码和数据.
- 一旦要让子进程执行一个全新的程序,不仅仅是将数据进行了写时拷贝,然后将新程序的数据对其进行覆盖,同样也将代码进行了写时拷贝,并且将新程序的代码对其进行进行了覆盖,并且通过页表重新建立映射关系.

用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec*系列函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec*系列函数并不创建新进程,所以调用exec前后该进程的id并未改变.
当进行进程程序替换时,是否创建新的进程?
进程程序替换之后,该进程对应的PCB、进程地址空间以及页表等数据结构都没有发生改变,只是进程在物理内存当中的数据和代码发生了改变,所以并没有创建新的进程,而且进程程序替换前后该进程的pid并没有发生改变.
子进程进行进程程序替换后,是否影响父进程的代码和数据?
子进程刚被创建时,与父进程共享代码和数据,但当子进程需要进行进程程序替换时,也就意味着子进程需要对其数据和代码进行写入操作,这时便需要将父子进程共享的代码和数据进行写时拷贝,此后父子进程的代码和数据也就分离了,因此子进程进行程序替换后不会影响父进程的代码和数据。
4.1:替换函数
#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 execve(const char *path, char *const argv[], char *const envp[]);
//其中...表示可变参数
函数解释
- 这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回。
- 如果调用出错则返回-1
- 所以exec函数只有出错的返回值而没有成功的返回值。
命名理解
- l(list) : 表示参数采用列表.
- v(vector) : 参数用数组.
- p(path) : 有p自动搜索环境变量PATH.
- e(env) : 表示自己维护环境变量.

4.1.1:execl函数

#include <unistd.h>
#include <stdio.h>
int main()
{
printf("Test_Execl Begin\n");
execl("/usr/bin/ls","ls","-l","-a",NULL);
printf("Test_Execl End\n");
return 0;
}

4.1.2:execlp函数


4.1.3:execv函数
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
int main()
{
printf("Test Execv Begin\n");
pid_t id = fork();
if(id == 0)
{
sleep(3);
//与execl函数一样,以NULL结尾
char * const argv[] = {"ls","-l","-a",NULL};
execv("/usr/bin/ls",argv);
exit(1);
}
int status = 0;
//为0表示阻塞等待
pid_t Rid = waitpid(id,&status,0);
if (Rid > 0)
{
printf("Father Wait Success,Child Exit Code:%d\n",WEXITSTATUS(status));
}
printf("Test Execv End\n");
return 0;
}

4.1.4:execvp函数
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
int main()
{
printf("Test Execvp Begin\n");
pid_t id = fork();
if(id == 0)
{
sleep(3);
char * const argv[] = {"ls","-l","-a",NULL};
execvp("ls",argv);
exit(1);
}
int status = 0;
//为0表示阻塞等待
pid_t Rid = waitpid(id,&status,0);
if (Rid > 0)
{
printf("Father Wait Success,Child Exit Code:%d\n",WEXITSTATUS(status));
}
printf("Test Execvp End\n");
return 0;
}
4.1.5:execvpe函数

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
int main()
{
printf("Test Execvpe Begin\n");
pid_t id = fork();
if(id == 0)
{
sleep(3);
//与execl函数一样,以NULL结尾
char * const argv[] = {"Cpp_Program.exe",NULL};
char * const envp[] = {"LiuJunhao = No.1","LuoAiTao = No.2",NULL};
execvpe("./Cpp_Program.exe",argv,envp);
exit(1);
}
int status = 0;
//为0表示阻塞等待
pid_t Rid = waitpid(id,&status,0);
if (Rid > 0)
{
printf("Father Wait Success,Child Exit Code:%d\n",WEXITSTATUS(status));
}
printf("Test Execvpe End\n");
return 0;
}
#include <iostream>
#include <unistd.h>
using namespace std;
int main(int argc,char * argv[],char* env[])
{
int i = 0;
//命令行参数表默认以NULL结尾
for(i = 0; argv[i]; i++)
{
printf("argv[%d]:>%s\n",i,argv[i]);
}
printf("-----------------------------\n");
//环境变量参数表也默认以NULL结尾
for(i = 0; env[i]; i++)
{
printf("env[%d]:>%s\n",i,env[i]);
}
printf("-----------------------------\n");
cout << "hello I am C++ Program and my pid == "<<getpid()<<endl;
cout << "hello I am C++ Program and my pid == "<<getpid()<<endl;
return 0;
}


4.2:小结
- 事实上,只有execve是真正的系统调用,其它五个函数最终都调用 execve,所以execve在man手册 第2节,其它函数在man手册第3节。这些函数之间的关系如下图所示。
Linux进程控制核心详解

1314





