fork
子进程复制父进程的0到3g空间和父进程内核中的PCB,但id号不同。
程序一讲解:
1. #include <stdio.h> 2. #include <sys/types.h> 3. #include <unistd.h> 4. 5. int main(void) 6. { 7. //fork创建一个新进程 8. pid_t p1 = -1; 9. 10. //fork之前的操作只有父父进程才有 11. p1 = fork(); //调用一次fork函数会返回两次 12. 13. if(p1 == 0) 14. { 15. //这里一定是子进程 16. //先sleep一下,让父进程先死 17. sleep(1); 18. 19. printf("子进程, pid = %d.\n", getpid()); 20. printf("子进程, p1 = %d.\n", p1); 21. printf("子进程中父进程的ID = %d.\n", getppid()); 22. } 23. if(p1 > 0) 24. { 25. //这里一定是父进程 26. printf("父进程, pid = %d.\n", getpid()); 27. printf("父进程, p1 = %d.\n", p1); 28. } 29. 30. if(p1 < 0) 31. { 32. 33. //这里一定是出错了 34. } 35. 36. //在这里的操作,父子进程都有 37. //printf("hello world, pid = %d.\n", getpid()); 38. 39. return 0; 40. }
输出结果如下
结论:
(1)fork函数调用一次会返回两次,返回值等于0的就是子进程,返回值大于0的就是父进程
(2)fork的返回值在子进程中等于0, 在父进程中返回值等于本次fork创建的子进程的PID
(3)这里在子进程中打印父进程PID,为什么跟在父进程中打印父进程的PID结果不一样?
是因为在执行的时候,父进程先执行完,然后子进程就没有父进程了;这是在子进程中打印的父进程PID就是init进程
程序二讲解:
1. #include <stdio.h> 2. #include <unistd.h> 3. #include <stdlib.h> 4. #include <sys/types.h> 5. 6. int main(void) 7. { 8. pid_t pid ; 9. //调用一次返回2次,在父进程返回子进程的PID,在子进程中返回0 10. 11. int n = 10; 12. pid = fork(); 13. 14. if(pid > 0) 15. {/*in parent*/ 16. while(1) 17. { 18. printf("I am parent %d\n", n++); 19. printf("my pid = %d.\n", getpid()); 20. printf("my parent pid = %d.\n", getppid()); 21. sleep(1); 22. } 23. } 24. 25. else if(pid == 0) 26. {/*in child*/ 27. while(1) 28. { 29. printf("I am child %d\n", n++); 30. printf("my pid = %d.\n", getpid()); 31. printf("my parent pid = %d.\n", getppid()); 32. sleep(3); 33. 34. } 35. } 36. 37. else 38. { 39. perror("fork"); 40. exit(0); 41. } 42. 43. return 0; 44. }
(1)父子进程中n值分别独立加,说明子进程从父进程中克隆过来的数据,已经不是共享的了。两者有自己独立的空间。
(2)0-3G放应用层代码,子进程直接从父进程中拷贝,3-4G是内核空间
(3)读时共享,写时复制(copy on write)(好处:节省物理内存开销,省去直接复制的时间)
fork创建了子进程之后,调用exec函数执行子进程
exec函数
用fork创建子进程后执行的是和父进程相同的程序,可以通过if判断pid的返回值让子进程执行不同的代码分支,这样设计程序不灵活。通过调用exec函数,用新程序将子进程的用户空间代码和数据替换,直接去执行新程序。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。
其实有六种以exec开头的函数,统称exec函数:
1. #include <unistd.h> 2. 3. int execl(const char *path, const char *arg, ...); 4. 5. int execlp(const char *file, const char *arg, ...); 6. 7. int execle(const char *path, const char *arg, ..., char *const envp[]); 8. 9. int execv(const char *path, char *const argv[]); 10. 11. int execvp(const char *file, char *const argv[]); 12. 13. int execve(const char *path, char *const argv[], char *const envp[]);
这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回,如果调用出错
则返回-1,所以exec函数只有出错的返回值而没有成功的返回值。
程序1:
1. #include <stdio.h> 2. #include <unistd.h> 3. 4. 5. int main(void) 6. { 7. printf("hello.\n"); 8. execl("/bin/ls", "ls", "-l", NULL); 9. printf("world.\n"); 10. return 0; 11. }
执行结果:
分析:
可以看到没有执行 printf(“world.\n”);
这是因为/bin/ls加载到用户用户空间替换了原来代码段,所以execl下面的代码就不会执行了。所以程序是从ls的return 0退出程序的。
函数辨析
execl与execlp
execl("/bin/ls", "ls", "-l", NULL);
execlp("ls", "ls", "-l", NULL);
execl要给全执行进程的路径,而execlp除了在当前路径下找,还可以去PATH环境变量底下找。p就是去环境变量底下找,不加p就是在当前路径下找,所以要给全路径
execv与execvp
这两个函数的功能跟excel与execlp的功能一样,只是将参数列表以数组指针的方式给出。
还是用上面执行ls举例
char *argvv[] = {“ls”, “-l”, NULL};
execv(“/bin/ls”, argvv); execvp(“ls”, argvv);
execle execve中添加了一个替换环境变量的参数,在加载新的程序的时候,我们可以不用原来的环境变量。(这两个函数实际用的很少)
wait & waitpid
僵尸进程
(1)子进程先于父进程结束。子进程结束后父进程此时并不一定立即就能帮子进程“收尸”,在这一段(子进程已经结束且父进程尚未帮其收尸)子进程就被称为僵尸进程。
(2)子进程除 task_struct 和栈外其余内存空间皆已清理
(3)父进程可以使用 wait 或 waitpid 以显式回收子进程的剩余待回收内存资源并且获取子进程退出状态。【父进程帮子进程收尸是要调用函数的】
(4)父进程也可以不使用 wait 或者 waitpid 回收子进程,此时父进程结束时一样会回收子进程的剩余待回收内存资源。(这样设计是为了防止父进程忘记显式调用 wait/waitpid 来回收子进程从而造成内存泄漏)
孤儿进程
(1)父进程先于子进程结束,子进程成为一个孤儿进程。
(2)linux 系统规定:所有的孤儿进程都自动成为一个特殊进程(进程 1,也就是 init 进程)的子进程。
1. #include <sys/types.h> 2. 3. #include <sys/wait.h> 4. 5. pid_t wait(int *status); 6. 7. pid_t waitpid(pid_t pid, int *status, int options);
wait返回值:成功回收子进程,返回终止的子进程的pid,错误返回-1
(1)waitpid参数含义:
参数值 |
含义 |
pid<-1 |
等待进程组号为pid绝对值的任何子进程。 |
pid=-1 |
等待任何子进程,此时的waitpid()函数就退化成了普通的wait()函数。 |
pid=0 |
等待进程组号与目前进程相同的任何子进程,也就是说任何和调用waitpid()函数的进程在同一个进程组的进程。 |
pid>0 |
等待进程号为pid的子进程。 |
(2)int *status
这个参数将保存子进程的状态信息,有了这个信息父进程就可以了解子进程为什么会退出,是正常退出还是出了什么错误。如果status不是空指针,则状态信息将被写入寄存器指向的位置。当然,如果不关心子进程为什么推出的话,也可以传入空指针。
Linux提供了一些非常有用的宏来帮助解析这个状态信息,这些宏都定义在sys/wait.h头文件中。主要有以下几个:
宏 |
说明 |
WIFEXITED(status) |
如果子进程正常结束,它就返回真;否则返回假。 |
WEXITSTATUS(status) |
如果WIFEXITED(status)为真,则可以用该宏取得子进程exit()返回的结束代码。 |
WIFSIGNALED(status) |
如果子进程因为一个未捕获的信号而终止,它就返回真;否则返回假。 |
WTERMSIG(status) |
如果WIFSIGNALED(status)为真,则可以用该宏获得导致子进程终止的信号代码。 |
WSTOPSIG(status) |
如果当前子进程被暂停了,则返回真;否则返回假。 |
WIFSTOPPED(status) |
如果WIFSTOPPED(status)为真,则可以使用该宏获得导致子进程暂停的信号代码。 |
(3)int options
参数options提供了一些另外的选项来控制waitpid()函数的行为。如果不想使用这些选项,则可以把这个参数设为0。主要使用的有以下两个选项:
参数 |
说明 |
WNOHANG |
如果pid指定的子进程没有结束,则waitpid()函数立即返回0,而不是阻塞在这个函数上等待;如果结束了,则返回该子进程的进程号。 |
WUNTRACED |
如果子进程进入暂停状态,则马上返回。 |
这些参数可以用“|”运算符连接起来使用。
如果waitpid()函数执行成功,则返回子进程的进程号;如果有错误发生,则返回-1,并且将失败的原因存放在errno变量中。
失败的原因主要有:没有子进程(errno设置为ECHILD),调用被某个信号中断(errno设置为EINTR)或选项参数无效(errno设置为EINVAL)
如果像这样调用waitpid函数:waitpid(-1, status, 0),这此时waitpid()函数就完全退化成了wait()函数。
程序1
1. #include <stdio.h> 2. #include <unistd.h> 3. #include <stdlib.h> 4. #include <sys/types.h> 5. #include <wait.h> 6. 7. int main(void) 8. { 9. pid_t pid, pid_c; 10. //调用一次返回2次,在父进程返回子进程的PID,在子进程中返回0 11. 12. int n = 10; 13. pid = fork(); 14. 15. if(pid > 0) 16. {/*in parent*/ 17. while(1) 18. { 19. printf("I am parent pid = %d\n", getpid()); 20. pid_c = wait(NULL); //wait是一个阻塞函数,等待回收子进程资源 21. 22. //如果没有子进程,wait返回-1 23. printf("wait for child %d.\n", pid_c); 24. sleep(1); 25. } 26. } 27. 28. else if(pid == 0) 29. {/*in child*/ 30. //printf("I am child %d\n", n++); 31. printf("I am child pid = %d.\n", getpid()); 32. //printf("my parent pid = %d.\n", getppid()); 33. sleep(10); 34. } 35. 36. else 37. { 38. perror("fork"); 39. exit(0); 40. } 41. 42. return 0; 43. }
编译之后,程序执行结果如下:
刚进来的时候打印一次父子进程的ID号,然后子进程slepp 10s,父进程执行wait等待回收子进程,阻塞在这里,直到子进程结束。子进程结束之后,wait的返回值就是-1.
僵尸进程对内存的影响:子进程如果没有回收,子进程占了一个PCB,浪费内存,造成内存泄露。
程序2:
1. #include <stdio.h> 2. #include <sys/types.h> 3. #include <unistd.h> 4. #include <wait.h> 5. 6. 7. int main(void) 8. { 9. 10. pid_t pid; 11. 12. int n = 3; 13. 14. while(n--) 15. { 16. pid = fork(); 17. if(pid == 0) break; 18. } 19. 20. if(pid == 0) 21. { 22. //while(1) 23. { 24. printf("I am child %d.\n", getpid()); 25. sleep(3); 26. } 27. } 28. 29. else if(pid > 0) 30. { 31. 32. pid_t pid_c; 33. while(1){ 34. 35. //pid_c = wait(NULL); 36. printf("I am parent.\n"); 37. pid_c = waitpid(0, NULL, WNOHANG); 38. 39. if(pid_c == -1) 40. continue; 41. else 42. printf("wait for child %d.\n", pid_c); 43. if(pid_c > 0){ 44. n++; 45. } 46. if(n == 2) 47. break; 48. sleep(1); 49. } 50. } 51. 52. return 0; 53. }
一个父进程fork了三个子进程,通过ps – ajx查看
可以看到三个子进程的pid号依次递增,,父进程与三个子进程同属于一个进程组。(父进程创建子进程之后,子进程跟父进程默认是一个组)
如何kill掉一个进程组,命令【kill -9 -14943】
注释掉while(1),执行之后可以看到,通过WNOHANG确实将waitpid设置成了非阻塞的。