1:进程 = 内核相关的数据结构(task_struct + mm_struct + 页表) + 代码 + 数据.
2:创建子进程会经过以下步骤.
如果理解进程具有独立性
进程调用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,为什么呢?如下图所示.

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

那么也就是说,在fork函数内部执行return语句以前,子进程就已经创建完毕了,那么之后的return语句不仅需要父进程执行,子进程也同样需要执行,这就是fork函数有两个返回值的原因.
1.3:写时拷贝
当子进程刚刚被创建时,子进程和父进程的数据和代码是共享的,即父子进程的代码和数据通过页表映射到物理内存的同一块空间。只有当父进程或子进程需要修改数据时,才将父进程的数据在内存当中拷贝一份,然后再进行修改。

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


1:为什么数据要进行写时拷贝?
进程具有独立性.多个进程在运行时,需要独享各种资源,多进程运行期间互不干扰,不能让子进程的改变影响到父进程.
2:为什么不在创建子进程的时候进行写时拷贝.
子进程不一定会使用父进程的所有数据,并且在子进程不对数据进行写入的情况下,没有必要对数据进行拷贝,那么应该按需分配,在需要修改数据的时候再分配(延时分配),这样可以高效的使用内存空间,避免空间的浪费.
3:代码会不会进行写时拷贝?
90%的情况下是不会的,但这并不代表代码不能进行写时拷贝,例如在进行进程替换的时候,则需要进行代码的写时拷贝.
fork函数创建子进程也可能会失败,有以下两种情况:
进程 = 内核相关的数据结构(task_struct + mm_struct + 页表) + 代码 + 数据. 创建进程时,会先创建进程的内核的相关管理数据结构.
那么终止是在做什么呢
那么进程终止存在以下三种情况
#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 $?命令查看最近一次进程退出的退出码信息.
#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;
}
父进程bash为什么要得到子进程的退出码呢?
异常这里后面会有专门的部分讲解,这里首先uu们简单理解下就好
#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;
}
#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;
}

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

进程正常终止一般有三种
#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;
}
使用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终止进程前会将缓冲区当中的数据输出.
使用_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终止进程,则缓冲区当中的数据将不会被输出。


return、exit、_exit之间的联系


结论:任何子进程,在退出的前提下,一般必须要被父进程进行等待 进程在退出的时候,如果父进程不管不顾,那么此退出进程, 就会进入Z状态(僵尸进程)
为什么要有进程等待.
进程等待的必要性.

#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);
}
}
#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");
}
函数原型:
pid_t waitpid(pid_t pid, int* status, int options);返回值:
参数: pid: Pid = -1,等待任何一个子进程,与wait等效. Pid > 0,等待其进程id与pid相等的子进程. status(输出型参数,获取子进程的退出状态): WIFEXITED(status):若为正常终止子进程返回的状态,则为真.(查看进程是否是正常退出) WEXITSTATUS(status):若WIFEXITED为非零,提取子进程退出码.(查看进程的退出码) 不关心则可设置为NULL. options: WNOHANG:若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待.若正常结束,则返回该子进程的id.
#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);
}
}
#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;
}
#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;
}



在status的低16比特位中,高8位表示进程的退出状态,即退出码.进程若是被信号所杀掉,则低7位表示终止信号,而第8位比特位是core dump标志.
我们通过一系列位操作,就可以根据status得到进程的退出码和退出信号。
exitCode = (status >> 8) & 0xFF;//退出码
exitSignal = status & 0x7f;//退出信号对于此,系统当中提供了两个宏来获取退出码和退出信号。
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;
}
在讲解阻塞等待与非阻塞等待前,我们先来看看子进程正常退出与非正常退出的情况
#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;
}
#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;
}
当子进程未退出时,父进程都在一直等待子进程退出,在等待期间,父进程不能做任何事情------>这种等待叫做阻塞等待.


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

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

向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;
}运行结果就是,父进程每隔一段时间就去查看子进程是否退出,若未退出,则父进程先去忙自己的事情,过一段时间再来查看,直到子进程退出后读取子进程的退出信息。


在讲进程程序替换前,我们先看一段代码
#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*系列函数的原理了
那么程序为什么要加载到内存当中
所以exec*系列的函数,执行完毕以后,后续的代码就不见了,因为旧进程代码和数据被exec*系列函数替换掉了,但是并没有替换旧进程的内核数据结构(因此在执行exec*系列函数的过程中,没有创建新进程).


#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并没有发生改变.
子进程进行进程程序替换后,是否影响父进程的代码和数据?
子进程刚被创建时,与父进程共享代码和数据,但当子进程需要进行进程程序替换时,也就意味着子进程需要对其数据和代码进行写入操作,这时便需要将父子进程共享的代码和数据进行写时拷贝,此后父子进程的代码和数据也就分离了,因此子进程进行程序替换后不会影响父进程的代码和数据。
其实有六种以 exec 开头的函数 , 统称 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 execve(const char *path, char *const argv[], char *const envp[]);
//其中...表示可变参数这些函数原型看起来很容易混 , 但只要掌握了规律就很好记。


#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;
}


#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;
}
#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;
}


#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;
}

