文章目录

    • 一、实验目的
    • 二、实验要求
    • 三、实验过程
    • 四、思考

一、实验目的

加深对进程概念的理解,明确进程与程序的区别,并认识并发执行的实质。

二、实验要求

  1. 编写一段程序,使用系统调用fork()创建两个子进程。当此程序运行时,在系统中有一个父进程和两个子进程活动。让每个进程在屏幕上显示一个字符,父进程显示“a”;子进程1显示“b”;子进程2显示“c”。多运行几次,观察并分析显示结果。
  2. 修改程序,将每个进程输出一个字符改为每个进程输出一句话,观察分析显示结果;
  3. 如果在父进程fork之前,输出一句话,这句话后面不加“\n”或加“\n”,结果有什么不同,为什么?
  4. 如果在程序中使用系统调用lockf来给临界资源加锁,可以实现临界资源的互斥访问。将lockf加在输出语句前后运行试试;将一条输出语句变成多条输出语句,将lockf语句放在循环语句外部或内部试试,观察显示结果并分析原因。
  5. 以上各种情况都多运行几次,观察每次运行结果是否都一致?为什么?

三、实验过程

  1. 编写一段程序,使用系统调用fork()创建两个子进程。当此程序运行时,在系统中有一个父进程和两个子进程活动。让每个进程在屏幕上显示一个字符,父进程显示“a”;子进程1显示“b”;子进程2显示“c”。多运行几次,观察并分析显示结果。

    源程序:

    #include #include #include int main(){   pid_t pid1, pid2;   while ((pid1 = fork()) == -1);   if (pid1 == 0) printf("b");   else   {      while ((pid2 = fork()) == -1);      if (pid2 == 0) printf("c");      else printf("a");   }   return 0;}

    分析:
    程序输出为’bca’,其中’b’为pid1的输出,’c’为pid2的输出,’a’为父进程的输出。
    首先在执行语句pid1 = fork()时,创建了子进程pid1,此时打印’b’;
    然后在执行语句pid2 = fork()时,创建了子进程pid2,此时打印’c’;
    最后父进程运行,打印’a’。
    程序运行多次,发现结果一致,并未有改变。
    原因可能是我的操作系统(CentOS5.5)总是按照如上的顺序进行运行。

  2. 修改程序,将每个进程输出一个字符改为每个进程输出一句话,观察分析显示结果;

    源程序:

    #include #include #include int main(){   pid_t pid1, pid2;   while ((pid1 = fork()) == -1);   if (pid1 == 0) printf("pid1 here : b\n");   else   {      while ((pid2 = fork()) == -1);      if (pid2 == 0) printf("pid2 here : c\n");      else printf("father progress here : a\n");   }   return 0;}

    结果和(1)问类似,先由子进程pid1输出,再由子进程pid2输出,最后父进程输出。

  3. 如果在父进程fork之前,输出一句话,这句话后面不加“\n”或加“\n”,结果有什么不同,为什么?

    源代码:

    #include #include #include int main(){   pid_t pid1, pid2;   printf("This is a sentence.");   // printf("This is a sentence.\n");   while ((pid1 = fork()) == -1);   if (pid1 == 0) printf("pid1 here : b\n");   else   {      while ((pid2 = fork()) == -1);      if (pid2 == 0) printf("pid2 here : c\n");      else printf("father progress here : a\n");   }   return 0;}

    不加’\n’:

    加上’\n’:

    结果不同:
    第一种情况每个进程都打印了This is a sentence.这句话,而第二种情况只打印了一次。

    查阅相关资料后进行分析:
    (1)不加’\n’:
    由于输出时,我们要将内存中的数据输出到磁盘中,系统会先将要printf的内容存到磁盘缓冲区buffer,也就是将This is a sentence.这句话写入buffer。而子进程会继承父进程的buffer内容,于是在子进程中也会打印这句话,第1个子进程会输出,第2个子进程也会输出。

    (2)加’\n’:
    系统检测到“\n”会自动清空缓冲区,于是子进程继承父进程时,缓冲区里面没有要打印这句话的信息,于是在2个子进程中只分别输出c和b。

  4. 如果在程序中使用系统调用lockf来给临界资源加锁,可以实现临界资源的互斥访问。将lockf加在输出语句前后运行试试;将一条输出语句变成多条输出语句,将lockf语句放在循环语句外部或内部试试,观察显示结果并分析原因。
    (1)直接对stdout加锁,发现和之前的输出没有区别,因为进行互斥约束后仍然可以访问stdout中的资源:

    #include #include #include int main(){   pid_t pid1, pid2;   while ((pid1 = fork()) == -1);   if (pid1 == 0)   {      lockf(1, 1, 0);      printf("pid1 here : b\n");      lockf(1, 0, 0);   }   else   {      while ((pid2 = fork()) == -1);      if (pid2 == 0)      {         lockf(1, 1, 0);         printf("pid2 here : c\n");         lockf(1, 0, 0);      }      else      {         lockf(1, 1, 0);         printf("father progress here : a\n");         lockf(1, 0, 0);      }   }   return 0;}

    (2)进行循环访问并在内部加锁,发现子进程pid1先执行5次输出,然后轮到pid2,最后是parent输出:

    #include #include #include int main(){   pid_t pid1, pid2;   while ((pid1 = fork()) == -1);   if (pid1 == 0)   {      for (int i = 0; i < 5; ++i)      {         lockf(1, 1, 0);         printf("pid1 here : %d\n", i);         lockf(1, 0, 0);      }   }   else   {      while ((pid2 = fork()) == -1);      if (pid2 == 0)      {         for (int i = 0; i < 5; ++i)         {            lockf(1, 1, 0);            printf("pid2 here : %d\n", i);            lockf(1, 0, 0);         }      }      else printf("father progress here : a\n");   }   return 0;}

    (3)进行循环访问并在外部加锁,发现输出情况一样:

    #include #include #include int main(){   pid_t pid1, pid2;   while ((pid1 = fork()) == -1);   if (pid1 == 0)   {      lockf(1, 1, 0);      for (int i = 0; i < 5; ++i)         printf("pid1 here : %d\n", i);      lockf(1, 0, 0);   }   else   {      while ((pid2 = fork()) == -1);      if (pid2 == 0)      {         lockf(1, 1, 0);         for (int i = 0; i < 5; ++i)            printf("pid2 here : %d\n", i);         lockf(1, 0, 0);      }      else printf("father progress here : a\n");   }   return 0;}

    (4)上锁后令进程进行短暂休眠,发现pid1和pid2交替输出,并且父进程不是最后输出:

    #include #include #include int main(){   pid_t pid1, pid2;   while ((pid1 = fork()) == -1);   if (pid1 == 0)   {      for (int i = 0; i < 5; ++i)      {         lockf(1, 1, 0);         printf("pid1 here : %d\n", i);         lockf(1, 0, 0);         sleep(1);      }   }   else   {      while ((pid2 = fork()) == -1);      if (pid2 == 0)      {         for (int i = 0; i < 5; ++i)         {            lockf(1, 1, 0);            printf("pid2 here : %d\n", i);            lockf(1, 0, 0);            sleep(1);         }      }      else printf("father progress here : a\n");   }   return 0;}

    分析:
    pid1进程先用lockf(1,1,0)锁上stdout,执行输出之后,立即用lockf(1,0,0)释放stdout,而此时进程休眠了1秒,没有立即又给stdout加锁,在休眠的这段时间,pid2可以获得stdout资源,于是pid2执行输出;同理,pid2加锁,输出,释放锁之后一样也执行了休眠,于是pid1可以获得stdout资源,于是就出现了两个进程交替执行输出的情况。

  5. 以上各种情况都多运行几次,观察每次运行结果是否都一致?为什么?
    不都一致,原因是OS对进程执行的顺序管理和访问互斥共同造成的,具体分析如上。

四、思考

系统是如何创建进程?当父进程fork子进程后,父进程和子进程从程序什么位置开始执行?为什么?

(1)首先创建父进程,再根据父进程创建子进程。具体而言,是首先通过内核调用sys_clone来完成,先对父进程的数据和环境进行复制,然后修改新进程的一些属性,之后通过do_fork函数和ret_from_fork函数完成对新进程的创建以及运行。

(2)经过资料查阅,个人认为,fork创建的父子进程并不保证任何有规律的执行顺序。不同linux,不同的系统配置环境,不同的系统负载下很可能结果不一样。

(3)从实验的结果来推测,子进程和父进程都从调用fork函数的下一条语句开始执行。因为程序的顺序执行,也很难想象会从之前的语句重新执行。