当前位置:   article > 正文

计算机系统基础学习心得(其二)——由绝望中新生(fork函数相关)_学习计算机系统基础的体会

学习计算机系统基础的体会

距上一次写博客已经过去很久了,(这次也是要求的),不过这次的学习过程是真的就如标题一样,一开始万分绝望,当时的心境仿佛孑身一人于孤岛之上,放眼望去只有看不到边的海平面;而之后却也还是得到了新生,就仿佛眼见从那海平面上缓缓驶来一艘游船......

 

这一次的学习的主要内容是实操fork函数相关的代码,并对其进行分析。老实说对这个函数其实并不陌生,因为大一的操作系统课就曾学过了。但这么久以来,它就好像一个熟悉的陌生人一样(明明知道她的名字,却不知如何上前搭话),从那学期学完考完后就从来没有再用过它,所以也就才有了一开始的糗态。但真正动手了之后,才发现,它其实还蛮有趣的(有趣的灵魂)。

fork函数最重要,最有趣,也是最令人头痛的一点大概就是“一次调用,两次返回”了正是这个特性,使得代码仿佛有了生命一般,反复左右横跳,总让人捉摸不定。下面我们就直接用一些例子来说明吧。

  1. Fork.c
  2. #include <stdio.h>
  3. #include<unistd.h>
  4. pid_t Fork(void)
  5. {
  6. pid_t pid;
  7. if((pid = fork()) < 0){
  8. printf("Fork error");
  9. exit(0)
  10. )
  11. return pid;
  12. }

这个是之后所有例子都会用到的Fork函数的实现,简单说明一下:就是把原本的fork()函数重新包装了一下,使其具有了当pid号出现明显错误(pid < 0)时,会进行报错并直接退出程序。

那么首先是第一个非常简单的例子:

  1. fork.c
  2. #include <stdio.h>
  3. #include<unistd.h>
  4. pid_t Fork();
  5. int main(int argc, char *argv[])
  6. {
  7. pid_t pid;
  8. int x = 1;
  9. pid = Fork(); //forkreturn
  10. if (pid == 0) { /* Child */
  11. printf("child : x=%d\n", ++x); //childprint
  12. fflush(stdout);
  13. return 0;
  14. }
  15. /* Parent */
  16. printf("parent: x=%d\n", --x); //parentprint
  17. fflush(stdout);
  18. return 0;
  19. }

这个例子的输出结果如下:

  1. 输出结果
  2. parent: x=0
  3. qioqio@ubuntu:~/homework/chap8_code$ child : x=2

当然这只是在我的电脑上输出的结果,如果自己去试的话,可能会与这个结果有所不同(可能先输出第二行再输出第一行)这是因为利用fork()函数生成的子进程和其父进程本质上是两个相互独立的进程,并不能确定谁会先运行(在我这是长辈腿长走得快)。

然后我们再来分析下这个例子:

        这个例子很简单,没有什么嵌套或者循环,就是顺序执行,而且只有一个fork()函数,那么就是程序运行到fork()函数后一分为二,变成一个子进程,一个父进程;子进程打印“child: x = 2”,进程结束;父进程打印“parent: x = 0”,进程结束。进程图如下所示:

然后之后是第二个例子:

  1. forkx2.c
  2. #include <stdio.h>
  3. #include<unistd.h>
  4. pid_t Fork();
  5. int main(int argc, char *argv[])
  6. {
  7. pid_t pid;
  8. int x = 1;
  9. pid = Fork(); //forkreturn
  10. if (pid == 0) { /* Child */
  11. printf("child : x=%d\n", ++x);
  12. printf("child : x=%d\n", ++x);
  13. fflush(stdout);
  14. return 0;
  15. }
  16. /* Parent */
  17. printf("parent: x=%d\n", --x);
  18. printf("parent: x=%d\n", --x);
  19. fflush(stdout);
  20. return 0;
  21. }

这个例子在第一个例子上稍微进了一步,在每个进程上再增加了一次打印,所以输出结果是这样的:

  1. 输出结果
  2. parent: x=0
  3. parent: x=-1
  4. qioqio@ubuntu:~/homework/chap8_code$ child : x=2
  5. child : x=3

接下来还是来分析一下这个例子,这个例子还是只有一个fork()函数,所以还是一分为二,一个子进程,一个父进程。还是之前说的子进程和父进程本质上是两个独立的进程,所以它们所用的x值也是相互独立的,所以父进程打印的是"x = 0, x = -1",子进程打印的是"x = 2, x = 3"。进程图如下所示:

接下来是下一个例子:

  1. fork2.c
  2. void fork2()
  3. {
  4. printf("L0\n");
  5. fork();
  6. printf("L1\n");
  7. fork();
  8. printf("Bye\n");
  9. }

这个例子里没有用到Fork()(因为直接从老师那借来的),它较上两个例子就增加了一个fork()函数,所以难度也增加了一些,输出结果如下:

  1. 输出结果:
  2. L0
  3. L1
  4. Bye
  5. qioqio@ubuntu:~/homework/chap8_code$ Bye
  6. L1
  7. Bye
  8. Bye

接下来照例是对这个例子的分析,这个例子里有两个fork() 函数,所以在第一次遇到fork() 函数一分为二后,在子进程和父进程中都还分别有一个fork() 函数,也就是还要再一次一分为二。在第一次遇到fork() 函数前,程序就有一个打印"L0"的函数,所以"L0"一定是第一个打印的。之后又到了子进程和父进程的时间,所以打印的结果也就会有很多种,在我这只是其中的一种,遇到第一个fork()后有一个打印"L1"的函数,所以接着会打印"L1",然后再遇到第二个fork() 函数,再分别打印一次"Bye"。但切记不可能先打印"Bye"再打印"L1",因为进程图都是线性的不能回转!然后这里没有标注父子进程的语句,所以我们也不得而知是哪个进程先进行。进程图如下所示:

然后是一个关于嵌套的例子:

  1. fork4.c
  2. void fork4()
  3. {
  4. printf("L0\n");
  5. if (fork() != 0) {
  6. printf("L1\n");
  7. if (fork() != 0) {
  8. printf("L2\n");
  9. }
  10. }
  11. printf("Bye\n");
  12. }

同样是从老师那来的,所以也没用Fork()函数,在这个例子里有嵌套选择的存在,输出结果如下:

  1. 输出结果
  2. L0
  3. L1
  4. L2
  5. Bye
  6. qioqio@ubuntu:~/homework/chap8_code$ Bye
  7. Bye

照例的分析,因为存在嵌套选择,所以情况又有所不同,首先是在遇到fork() 函数之前打印的"L0",,然后就到了嵌套选择,第一次进行选择的条件是"fork() != 0", 然后我们知道对于fork() 函数来说子进程会返回0,而父进程会返回子进程的PID号,所以这个条件翻译过来就是“当此时不是子进程时”,就会执行"L1"的打印。然后再是同样的选择条件“fork() != 0”,则会打印"L2",之后就跳出选择,再打印"Bye"。总结一下就是只有父进程会打印"L1",然后父进程的父进程会打印"L2"其他就只会打印"Bye"。(其次注意这里只有第一次fork()产生的父进程才能进行第二次fork)进程图如下:

接下来我们连着看两个例子:

    首先是第一个:

  1. forkprob1.c
  2. #include <stdio.h>
  3. #include<unistd.h>
  4. #include<stdlib.h>
  5. pid_t Fork();
  6. void doit()
  7. {
  8. if(Fork() == 0){
  9. Fork();
  10. printf("hello\n");
  11. exit(0);
  12. }
  13. return;
  14. }
  15. int main()
  16. {
  17. doit();
  18. printf("hello\n");
  19. exit(0);
  20. }

    然后是第二个:

  1. forkprob2.c
  2. #include <stdio.h>
  3. #include<unistd.h>
  4. #include<stdlib.h>
  5. pid_t Fork();
  6. void doit()
  7. {
  8. if(Fork() == 0){
  9. Fork();
  10. printf("hello\n");
  11. return;
  12. }
  13. return;
  14. }
  15. int main()
  16. {
  17. doit();
  18. printf("hello\n");
  19. exit(0);
  20. }

怎么样,这两个例子乍眼看上去是不是好像完全一样(但如果一样我也不会放到这了),如果你再仔细看看的话,就会发现在两个例子里的doit() 函数里有一条语句是不同的,第一个例子if条件下是“exit(0);”,而第二个例子下的则是“return;”也许你会想这两个不都是退出吗,有什么区别?但事实是第二个例子比第一个例子整整多打印了两个“hello”!!!输出结果如下:

  1. forkprob1输出结果
  2. hello
  3. qioqio@ubuntu:~/homework/chap8_code$ hello
  4. hello
  1. forkprob6输出结果
  2. hello
  3. qioqio@ubuntu:~/homework/chap8_code$ hello
  4. hello
  5. hello
  6. hello

但这是为什么呢?明明都是退出,得到的结果却如此之大。原因当然就是在这两个“退出”上,exit()是系统调用级别的,它表示了一个进程的结束;return是语言级别的,它表示了调用堆栈的返回。用更通俗的话来讲就是,exit()是进程的结束,而return只是函数的结束(当然如果把return放在主函数main里它的效果就和exit()一样了,因为此时是退出主函数,也就是整个程序)。那么放在我们这两个例子里是什么情况呢?因为doit() 函数里的if条件调用了Fork() 函数,所以两个例子都会进行一次fork,然后因为子进程满足if条件,那么子进程会执行if下的语句,再进行一次fork。然后第一个例子里子进程打印完“hello”后就调用exit(),这样子进程就不复存在了,也就不能再执行主函数里打印“hello”的语句;而在第二个例子里子进程打印完后仅仅是调用return语句返回退出当前的doit()函数,其本身依旧存在,所以还是能执行主函数里打印“hello”的语句,所以就会多出两个“hello”了。以下为进程图:

例子1:

例子2:

至此,本次的学习笔记就告一段落了,总之能把fork弄明白真是太好了!

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

闽ICP备14008679号