当前位置:   article > 正文

操作系统——Linux进程创建及同步实验_在linux下使用fork创建进程的实验步骤

在linux下使用fork创建进程的实验步骤

实验题目要求:

1.编写一段程序,使用系统调用fork( )创建两个子进程。当此程序运行时,在系统中有一个父进程和两个子进程活动。让每一个进程输出不同的内容。试观察记录屏幕上的显示结果,并分析原因。

2.修改上述程序,每一个进程循环显示一句话。子进程显示"daughter  …"及"son  ……",父进程显示"parent  ……",观察结果,分析原因。

3.再调用exec( )用新的程序替换该子进程的内容 ,并利用wait( )来控制进程执行顺序。调用Exit()使子进程结束。

4.利用linux的信号量机制实现生产者-消费者问题。(基于进程)


实验1:

先用在终端中输入 vim test01.c 编辑程序(在vim中的编译操作可参见我的前一篇博客: Linux系统添加系统调用  ,其中有具体操作)

我最终的test01.c的程序如下图所示:


程序结果如下:(我多输出了几次编译后的a.out程序,仔细观察可以发现每次执行结果中进程的执行顺序是不定的,且进程对应的进程标识符 即程序中输出的ID号 也不相同)

分析:

(1)首先了解一下fork()函数:

一个现有进程可以调用fork函数创建一个新进程。该函数定义如下:

  1. #include <unistd.h>
  2. pid_t fork(void);// 返回:若成功则在子进程中返回0,在父进程中返回子进程ID,若出错则返回-1

fork调用的一个奇妙之处就是它仅仅被调用一次,却能够返回两次,它可能有三种不同的返回值:

  • 在父进程中,fork返回新创建子进程的进程ID;
  • 在子进程中,fork返回0;
  • 如果出现错误,fork返回一个负值;

因此,可以通过返回值来判断当前是父进程还是子进程。

(2)其次,关于pid的值在父子进程中不同的原因,其实就相当于链表,进程形成了链表,父进程的pid(p 意味着point)指向子进程的进程ID,因为子进程没有子进程,所以它的pid为0。

(3)这次实验中每次输出结果中的顺序不一样的原因:

一般来说,在fork之后的父进程先执行还是子进程先执行是不确定的,取决于内核的调度算法,相互之间没有任何时序上的关系。所以在没有加入进程同步机制的代码的情况下,父进程与子进程的输出内容会叠加在一起,由此导致每次运行的结果之后出现了不一样的运行结果。


具体fork函数的创建过程及解析可参看:

fork()创建子进程步骤、函数用法及常见考点(内附fork()过程图)


实验2:

因为是在实验1的基础上进行的,所以,操作步骤与实验1差不多。

我的实验2的程序如下:


程序执行结果:

多次输出编译后的a.out程序,同样可以看到执行的顺序也是不定的,且进程的ID号也不相同。

分析:

我认为我的实验2的程序中进程的产生过程如下图所示:(排列不讲顺序)

   

所以,在最终的输出结果中parent,daughter,son进程的顺序不一定。

而由于函数printf( )在输出字符串时不会被中断,所以,字符串内部字符顺序输出不变。但由于进程并发执行的调度顺序和父子进程抢占处理机问题,输出字符串的顺序和先后随着执行的不同而发生变化。


实验3:

(1)exec函数说明:

fork函数是用于创建一个子进程,该子进程几乎是父进程的副本,而有时我们希望子进程去执行另外的程序,exec函数族就提供了一个在进程中启动另一个程序执行的方法。它可以根据指定的文件名或目录名找到可执行文件,并用它来取代原调用进程的数据段、代码段和堆栈段,在执行完之后,原调用进程的内容除了进程号外,其他全部被新程序的内容替换了。另外,这里的可执行文件既可以是二进制文件,也可以是Linux下任何可执行脚本文件。

(2)在Linux中使用exec函数族主要有以下两种情况:

a. 当进程认为自己不能再为系统和用户做出任何贡献时,就可以调用任何exec 函数族让自己重生。

b. 如果一个进程想执行另一个程序,那么它就可以调用fork函数新建一个进程,然后调用任何一个exec函数使子进程重生。

(3)exec函数族语法

实际上,在Linux中并没有exec函数,而是有6个以exec开头的函数族,下表列举了exec函数族的6个成员函数的语法。

所需头文件: #include <unistd.h>

函数说明: 执行文件

函数原型:

  1. int execl(const char *path, const char *arg, ...)
  2. int execv(const char *path, char *const argv[])
  3. int execle(const char *path, const char *arg, ..., char *const envp[])
  4. int execve(const char *path, char *const argv[], char *const envp[])
  5. int execlp(const char *file, const char *arg, ...)
  6. int execvp(const char *file, char *const argv[])

其中,这6个函数之间的调用关系如下图所示:

(4)exec()和fork()联合使用

系统调用exec()和fork()联合使用能为程序开发提供有力支持。用fork( )建立子进程,然后在子进程中使用exec(),这样就实现了父

进程与一个与它完全不同子进程的并发执行。

一般,wait、exec联合使用的模型为:

  1. int status;
  2. ............
  3. if (fork( )= =0)
  4. {
  5. ...........;
  6. execl(...);
  7. ...........;
  8. }
  9. wait(&status);
(5)wait()

等待子进程运行结束。如果子进程没有完成,父进程一直等待。wait()将调用进程挂起,直至其子进程因暂停或终止而发来软件

中断信号为止。如果在wait()前已有子进程暂停或终止,则调用进程做适当处理后便返回。

系统调用格式:

  1. int wait(status) 
  2. int *status;

其中,status是用户空间的地址。它的低8位反应子进程状态,为0表示子进程正常结束,非0则表示出现了各种各样的问题;高8

位则带回了exit()的返回值。exit()返回值由系统给出。

核心对wait()作以下处理:

  • 首先查找调用进程是否有子进程,若无,则返回出错码;
  • 若找到一处于“僵死状态”的子进程,则将子进程的执行时间加到父进程的执行时间上,并释放子进程的进程表项;
  • 若未找到处于“僵死状态”的子进程,则调用进程便在可被中断的优先级上睡眠,等待其子进程发来软中断信号时被唤醒。

(6)exit()

终止进程的执行。

系统调用格式:

  1. void exit(status)
  2. int status;

其中,status是返回给父进程的一个整数,以备查考。

为了及时回收进程所占用的资源并减少父进程的干预,UNIX/LINUX利用exit( )来实现进程的自我终止,通常父进程在创建子进程时,应在进程的末尾安排一条exit( ),使子进程自我终止。exit(0)表示进程正常终止,exit(1)表示进程运行有错,异常终止。


了解相关只是之后开始写程序,我的最终程序如下:

在最终这版之前,我之前少写了wait()函数的头文件,然后编译之后出现了警告,一开始有点慌,以为是导致编译没成功,所以上网搜了下警告里的那个很长的句子(-Wimplicit-function-declaration),然后网上的解决办法是在终端中输入man ×××(其中×××表示警告中上面这句话下面出现的东西),找到×××的头文件之后,在程序中加入即可。然后我在终端中输入man wait ,找到头文件为<sys/wait.h>和<sys/types.h>,然后在程序中加入就编译成功了。

后来,写实验4的时候才意识到警告不影响程序呢运行,可能是太久没有写代码了,连这个都忘记了大哭

程序运行结果以及编译中出错的地方都如下图所示:

我的程序执行的功能是执行命令ls  -l  -color ,(按倒序)列出当前目录下所有文件和子目录。

程序在调用fork( )建立一个子进程后,马上调用wait(),使父进程在子进程结束之前,一直处于睡眠状态。子进程用exec()装入命令ls ,exec()后,子进程的代码被ls的代码取代,这时子进程的PC指向ls的第1条语句,开始执行ls的命令代码。其中,wait( )给我们提供了一种实现进程同步的简单方法


关于fork()函数和exec()函数的详解参考:进程控制


实验4:

(1)生产者-消费者问题:

就是生产者和消费者共用一个缓冲区,生产者生产项目放进缓冲区,而消费者则从缓冲区中消费项目。当缓冲区满了的时候,生产者不能对其生产;而当缓冲区为空的时候,消费者不能作消费。

所以生产者与消费者模型是复合321原则:三种关系,两种角色,一种交易场所。

三种关系:

           生产者与消费者:互斥,同步

           生产者与生产者:互斥

           消费者与消费者:互斥

(2)需要的信号量(3个):

第一个信号量用于限制生产者必须在缓冲区不满时才能生产,是同步信号量;

第二个信号量用于限制消费者必须在缓冲区有产品时才消费,是同步信号量;

第三个信号量用于限制生产者和消费者在访问缓冲区时必须互斥,是互斥信号量。

(3)关于信号量的函数:

初始化信号量                  int sem_init (sem_t *sem, int pshared, unsigned int value) 

        第一个参数是信号量;第二个参数pshared设为0,意思是信号量用于同一进程间同步;第三个参数value是计数器的初始值。

P操作                               int sem_wait (sem_t *sem)

V操作                               int sem_post (sem_t *sem)         

删除信号量                     int sem_destory (sem_t *sem)


代码:

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <unistd.h>
  4. #include <pthread.h>
  5. #include <semaphore.h>
  6. #include <signal.h>
  7. #define N 5 // 消费者或者生产者的数目
  8. #define M 10 // 缓冲数目
  9. int in = 0; // 生产者放置产品的位置
  10. int out = 0; // 消费者取产品的位置
  11. int buff[M] = { 0 }; // 缓冲初始化为0,开始时没有产品
  12. sem_t empty_sem; // 同步信号量,当满了时阻止生产者放产品
  13. sem_t full_sem; // 同步信号量,当没产品时阻止消费者消费
  14. pthread_mutex_t mutex; // 互斥信号量,一次只有一个线程访问缓冲
  15. int product_id = 0; //生产者id
  16. int prochase_id = 0; //消费者id
  17. void Handlesignal(int signo)//信号处理函数
  18. {
  19. printf("程序退出\n",signo);
  20. exit(0);
  21. }
  22. void print() //打印缓冲情况
  23. {
  24. int i;
  25. printf("缓冲区队列为");
  26. for(i = 0; i < M; i++)
  27. printf("%d", buff[i]);
  28. printf("\n");
  29. }
  30. void *product() //生产者方法
  31. {
  32. int id = ++product_id;
  33. while(1)//重复进行
  34. {
  35. sleep(2);//用sleep的数量可以调节生产和消费的速度,便于观察
  36. sem_wait(&empty_sem);
  37. pthread_mutex_lock(&mutex);
  38. in= in % M;
  39. printf("生产者%d 在缓冲区中存放第%d个资源\t",id, in);
  40. buff[in]= 1;
  41. print();//转行,控制输入格式
  42. in++;
  43. pthread_mutex_unlock(&mutex);
  44. sem_post(&full_sem);
  45. }
  46. }
  47. void *prochase() //消费者方法
  48. {
  49. int id = ++prochase_id;
  50. while(1) //重复进行
  51. {
  52. sleep(4);//用sleep的数量可以调节生产和消费的速度,便于观察
  53. sem_wait(&full_sem);
  54. pthread_mutex_lock(&mutex);
  55. out= out % M;
  56. printf("消费者%d 从缓冲去中取走第%d个资源\t",id, out);
  57. buff[out]= 0;
  58. print();//转行,控制输入格式
  59. ++out;
  60. pthread_mutex_unlock(&mutex);
  61. sem_post(&empty_sem);
  62. }
  63. }
  64. int main()
  65. {
  66. printf("生产者和消费者数目都为5,缓冲区大小为10\n");
  67. printf("生产者每2秒生产一个资源,消费者每4秒消费一个资源,按Ctrl+C退出程序\n\n");
  68. pthread_t id1[N];
  69. pthread_t id2[N];
  70. int i;
  71. int ret[N];
  72. if(signal(SIGINT,Handlesignal)==SIG_ERR)//按ctrl+C产生SIGINT信号,进程结束
  73. printf("信号输入出错\n");
  74. // 初始化同步信号量
  75. int ini1 = sem_init(&empty_sem, 0, M);//缓冲区同步
  76. int ini2 = sem_init(&full_sem, 0, 0);//线程运行同步
  77. if(ini1 && ini2 != 0)
  78. {
  79. printf("同步信号量初始化失败!\n");
  80. exit(1);
  81. }
  82. int ini3 = pthread_mutex_init(&mutex, NULL);//初始化互斥信号量
  83. if(ini3 != 0)
  84. {
  85. printf("线程同步初始化失败!\n");
  86. exit(1);
  87. }
  88. for(i = 0; i < N; i++) // 创建N个生产者线程
  89. {
  90. ret[i]= pthread_create(&id1[i], NULL, product, (void *) (&i));
  91. if(ret[i] != 0)
  92. {
  93. printf("生产者%d 线程创建失败!\n", i);
  94. exit(1);
  95. }
  96. }
  97. for(i = 0; i < N; i++) //创建N个消费者线程
  98. {
  99. ret[i]= pthread_create(&id2[i], NULL, prochase, NULL);
  100. if(ret[i] != 0)
  101. {
  102. printf("消费者%d 线程创建失败!\n", i);
  103. exit(1);
  104. }
  105. }
  106. for(i = 0; i < N; i++)//等待线程销毁
  107. {
  108. pthread_join(id1[i], NULL);
  109. pthread_join(id2[i],NULL);
  110. }
  111. exit(0);
  112. }

运行结果:




参考:

OS: 生产者消费者问题(多进程+共享内存+信号量)

Linux下利用信号量函数和共享内存函数和C语言实现生产者消费者问题


总结:

此次实验,相较与上次实验而言,我觉得更难了一些,可能是这次用到了很多之前没怎么接触过的函数,不过还是很开心能接触到这些函数,并对这些函数进行学习和实际操作。

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

闽ICP备14008679号