当前位置:   article > 正文

136 Linux 系统编程13 ,进程控制 fork()创建父子进程,循环创建5个子进程,进程读时共享写时复制,刚fork后父子进程相同和不同的地方,父子进程真正共享文件描述符 和 mmap映射区_fork函数 5个子进程

fork函数 5个子进程

一 fork()函数创建父子进程

fork函数

创建一个子进程。

pid_t fork(void); 失败返回-1;

成功返回:① 父进程返回子进程的ID(非负) ②子进程返回 0

pid_t类型表示进程ID,但为了表示-1,它是有符号整型。(0不是有效进程ID,init最小,为1)

注意返回值,不是fork函数能返回两个值,而是fork后,fork函数变为两个,父子需【各自】返回一个。

getpid函数

获取当前进程ID

    pid_t getpid(void);

getppid函数

获取当前进程的父进程ID

pid_t getppid(void);

区分一个函数是“系统函数”还是“库函数”依据:

  •  是否访问内核数据结构

② 是否访问外部硬件资源 二者有任一 → 系统函数;二者均无 → 库函数

getuid函数

获取当前进程实际用户ID

uid_t getuid(void);

获取当前进程有效用户ID

uid_t geteuid(void);

getgid函数

获取当前进程使用用户组ID

gid_t getgid(void);

获取当前进程有效用户组ID

gid_t getegid(void);

例子1  //fork函数创建子进程

  1. //fork函数创建子进程
  2. int main() {
  3. int ret = 0;
  4. cout << "test1" << " get pid() = " << getpid()<< " getppid() = " << getppid()<< endl;
  5. cout << "test2" << endl;
  6. pid_t pid = fork();
  7. if (pid==-1) {
  8. perror("fork error");
  9. ret = pid;
  10. return ret;
  11. }
  12. if (pid==0) {//fork函数后,子进程的返回值是0
  13. cout << "i am child process getpid() = " << getpid() << " getppid() = " << getppid() << endl;
  14. }
  15. if (pid>0) {//fork函数后,父进程的返回值是 >0 的值,且这个返回值一般都是 子进程的pid
  16. cout << "i am father process getpid() = " << getpid() << " getppid()= " << getppid() << endl;
  17. }
  18. cout << "这行代码会被执行两遍,因为父进程要走一遍,子进程也要走一遍 getpid()" <<getpid()<< endl;
  19. return 0;
  20. }

例子2  循环创建5个子进程

  1. //循环创建5个子进程
  2. int main() {
  3. int ret = 0;
  4. int i = 0;
  5. for (; i < 5; i++) {
  6. pid_t pid = fork();
  7. if (pid==-1) {
  8. perror("fork error ");
  9. ret = pid;
  10. return ret;
  11. }
  12. else if (pid ==0 ) {
  13. //子进程,如果是子进程,不能参与下一次的 fork,只让主进程fork,因此在这里要break出去
  14. break;
  15. }
  16. }
  17. //因为 i = 0,1,2,34 的时候,这个i都是子进程break出来的,因此i在等于 01,2,34,时,都是子进程,只有i=5的时候,fork并没有执行,那么i=5的时候,就是父进程
  18. if (i==5) {
  19. cout << "我是父进程 getpid() = " << getpid() << " getppid()=" << getppid() << endl;
  20. sleep(1);
  21. }
  22. else {
  23. cout << "我是子进程 getpid() = " << getpid() << " getppid()=" << getppid() << " i = " << i << endl;
  24. }
  25. return ret;
  26. }

如下是 for 循环时,fork()的代码流程分析图

例子3 循环创建5个进程,记住5个子进程的进程id,坑点记录

//循环创建5个子进程,记录子进程的pid,这里要注意的坑较多
坑点1。我们之所以记录子进程的pid,是因为在学习 waitpid函数后,
我们需要在父进程中使用waitpid来 销毁子进程,使用的函数是 pid_t waitpid(pid_t pid, int *status, int options);

由于使用waitpid要在父进程,那么在create 子进程的时候,也要在父进程记录 子进程的pid。
这是因为 父子进程在fork之后的一些数据,是读时共享,写时复制的,
而记录pid是一个写的过程,因此在父进程记录的,这个pid就会保留在父进程,最后在父进程用 ,就没有问题。

坑点2:
当i=0,在父进程记录的时候,由于会再循环fork,因此下一次(i=1)create的子线程就会保留父进程记录的pid
这就是我们在子进程打印也能看到pid的原因。

坑点3:
我们在循环内部,如果是子进程,就会break出去,这是因为fork函数有两个返回值,因此在循环结束后,要判断i的值,确定主进程走的这块还是子进程走的这块
 

  1. int main() {
  2. int ret = 0;
  3. int i = 0;
  4. pid_t pids[5] = { 0 };//初始化所有的 childpid,初始值都为0
  5. for (; i < 5; i++) {
  6. pid_t pid = fork();
  7. if (pid==-1) {
  8. perror("fork error ");
  9. ret = pid;
  10. return ret;
  11. }
  12. else if (pid ==0 ) {
  13. //子进程,如果是子进程,不能参与下一次的 fork,只让主进程fork,因此在这里要break出去
  14. //pids[i] = pid; //在这里写没有,只会在子进程中起作用
  15. break;
  16. }
  17. else if (pid > 0 ) { //当pid >0的 时候,说明是父进程,那么要在父进程中记录 当前的pid
  18. pids[i] = pid;//注意,这是在父进程中记录了所有的子进程的pid,那么在使用的时候,也只能在父进程使用,因为 读时共享写时复制.另外要注意的是,由于在循环中fork,上一次在父进程改动的pids[0]的值,在下一次fork时候,会带入
  19. }
  20. }
  21. //因为 i = 0,1,2,34 的时候,这个i都是子进程break出来的,因此i在等于 01,2,34,时,都是子进程,只有i=5的时候,fork并没有执行,那么i=5的时候,就是父进程
  22. if (i==5) {
  23. cout << "我是父进程 getpid() = " << getpid() << " getppid()=" << getppid() << endl;
  24. sleep(2);
  25. //将记录的pids打印一下
  26. for (int j = 0; j < 5; j++) {
  27. cout << " fu pids[" << j << "] = " << pids[j];
  28. }
  29. cout << endl;
  30. }
  31. else {
  32. cout << "我是子进程 getpid() = " << getpid() << " getppid()=" << getppid() << " i = " << i << endl;
  33. //将记录的pids打印一下
  34. for (int j = 0; j < 5; j++) {
  35. cout << " zi pids[" << j << "] = " << pids[j];
  36. }
  37. cout << endl;
  38. }
  39. return ret;
  40. }

  1. 我是子进程 getpid() = 12479 getppid()=12475 i = 0
  2. zi pids[0] = 0 zi pids[1] = 0 zi pids[2] = 0 zi pids[3] = 0 zi pids[4] = 0
  3. 我是子进程 getpid() = 12480 getppid()=12475 i = 1
  4. zi pids[0] = 12479 zi pids[1] = 0 zi pids[2] = 0 zi pids[3] = 0 zi pids[4] = 0
  5. 我是子进程 getpid() = 12481 getppid()=12475 i = 2
  6. zi pids[0] = 12479 zi pids[1] = 12480 zi pids[2] = 0 zi pids[3] = 0 zi pids[4] = 0
  7. 我是子进程 getpid() = 12482 getppid()=12475 i = 3
  8. zi pids[0] = 12479 zi pids[1] = 12480 zi pids[2] = 12481 zi pids[3] = 0 zi pids[4] = 0
  9. 我是子进程 getpid() = 12483 getppid()=12475 i = 4
  10. zi pids[0] = 12479 zi pids[1] = 12480 zi pids[2] = 12481 zi pids[3] = 12482 zi pids[4] = 0
  11. 我是父进程 getpid() = 12475 getppid()=12473
  12. fu pids[0] = 12479 fu pids[1] = 12480 fu pids[2] = 12481 fu pids[3] = 12482 fu pids[4] = 12483

二 进程共享

父子进程之间在fork后。有哪些相同,那些相异之处呢?

刚fork之后:

父子相同处: 全局变量、.data、.text、栈、堆、环境变量、用户ID、宿主目录、进程工作目录、信号处理方式...

父子不同处 这个是重点: 1.进程ID   2.fork返回值   3.父进程ID    4.进程运行时间    5.闹钟(定时器)   6.未决信号集

7、父进程设置的锁,子进程不继承;
8、各自的进程ID和父进程ID不同;
9、子进程的未决告警被清除
10、子进程的未决信号集设置为空集。

父子进程共享

1.文件描述符,2 mmap映射区

似乎,子进程复制了父进程0-3G用户空间内容,以及父进程的PCB,但pid不同。真的每fork一个子进程都要将父进程的0-3G地址空间完全拷贝一份,然后在映射至物理内存吗?

当然不是!父子进程间遵循读时共享写时复制的原则。这样设计,无论子进程执行父进程的逻辑还是执行自己的逻辑都能节省内存开销。  

如果父进程改动了 全局变量 int a 的值,假设从100改成了200,实际上改动的是 复制的那份。

也就是说 子进程去读取 int a 的值,这时候a还是100。

也就是一旦有 父进程 或者子进程改动了 全局变量的值,那么父子进程就会各自维护一份。

结论一:父子进程 间 遵循 读时共享写实复制 机制

结论二:一旦有父进程或者子进程改动了值,那么父子进程就会各自维护一份

结论三:父子进程共享的的部分,最重要的是 文件描述符,mmap建立的映射区

结论四:fork之后,父进程先执行还是子进程先执行不确定。

三 gdb 对于 父子进程的 调试

使用gdb调试的时候,gdb只能跟踪一个进程。可以在fork函数调用之前,通过指令设置gdb调试工具跟踪父进程或者是跟踪子进程。默认跟踪父进程。

set follow-fork-mode child 命令设置gdb在fork之后跟踪子进程。

set follow-fork-mode parent 设置跟踪父进程。

注意,一定要在fork函数调用之前设置才有效。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/运维做开发/article/detail/882970
推荐阅读
相关标签
  

闽ICP备14008679号