一、实验目的
1、通过fork()与vfork()创建的进程理解进程与线程的行为特性;
2、理解wait和exec函数簇的功能与作用
3、掌握基本的多进程编程技术
4、掌握基本的进程间通信与同步技术
二、实验内容
(一):fork()和vfork()多进程实验
1、编写3个进程,1个父进程P和两个子进程C1、C2;
2、用fork()创建子进程C1,用vfork()创建子进程C2,观察父子进程执行的次序;
3、通过观察给定变量值经运算后的变化比较父子进程共享内存的属性;
4、通过子进程C1与父进程P打印输出信息的变化,观察让父进程先于子进程C1退出和通过wait()函数让父进程后于子进程C1退出有何不同;
5、在C2中调用exec函数,观察C2和P的执行过程;
6、将以上程序本地调试完成后加载到树莓派运行。
(二)进程间通信与同步实验
1、编写多个父进程,用fork()创建多个子进程;
2、进程间采用有名管道通信;
3、进程间通过传递信号通信,编写信号处理函数;
4、进程间采用消息队列通信;
5、进程间采用共享内存通信,用信号量实现共享内存的写同步操作;
6、将以上程序本地调试完成后加载到树莓派执行;
三、实验过程、结果与分析
(一)fork和vfork多进程实验
1. 程序需要包含的头文件:
#include
#include
#include
2. 根据实验要求,首先在父进程中创建两个子进程child1、child2,三个进程的结构如下:
第5行第11行中global为全局变量,variable为局部变量,在本程序中用来比较fork与vfork两种创建新进程的方式的资源共享情况;
9、10行定义后面execve()函数将要用到的参数,12行创建新进程child1同时判断是否创建成功,若未成功则返回-1后失败退出,15行若成功创建且返回值为0则进入child1进程执行else if后代码段,否则23行继续在父进程中执行esle后代码段;24行在父进程代码段中创建子进程child2过程与child1相同。27行else if后为子进程child2的代码段,36行else后为父进程代码段。
3. 下面先看child1子进程下的代码段:
在其中主要功能为对变量global和variable进行自加一,然后打印出进程的进程号pid、父进程号ppid、及两变量的值和地址;两个sleep函数为在需要时调整进程执行顺序以及后面观察wait()函数的影响而添加,暂不解释。
4. 接下来child2子进程下的代码段:
主要完成对global和variable变量自加一,打印进程号、父进程号,变量值和地址,第33行调用exevce()函数测试,这里的可执行文件为一个我自己编译生成的二进制文件,原文件内容如下:
在该test.c中打印进程号、父进程号以及传入的参数。
5. 最后父进程38行else后内容:
主要功能同样为打印出进程此时的进程号、父进程号、变量值、地址,这里42行在sleep 5s后再次打印是为了能够观察到子进程执行后对父进程是否产生影响,44行、45行、46行为等待回收两个子进程并打印出回收子进程的进程号;最后成功退出
6. 观察在没有wait()函数等待进程child1执行结束时的执行输出:
适当进行代码调整:去掉18行sleep(3);21行仍为sleep(10);在21行后添加一行printf语句打印此时的相关信息。去掉45行的一个wait函数,并调整46行只打印一个捕捉到的进程id。
执行结果:
由上图结果可以看出,父进程创建child1成功后调度到父进程中执行,父进程创建child2后进入child2代码段,打印出该子进程的pid=3953,ppid=3951,变量值和地址为:global=4,variable=2;&global=0x601080,&variable=0x7ffe1d0cbb30.
然后调用./test可执行文件开始执行,在该文件执行过程中又转回父进程,此时父进程的
pid=3951,ppid=2476;global=4,variable=2;&global=0x601080,&variable=0x7ffe1d0cbb30.可见其pid恰为child2的ppid,而此时父进程的变量值也由于子进程的操作而改变,变量地址也相同,说明vfork创建的子进程与父进程共享地址空间。父进程通过sleep(5)进入睡眠后,子进程child1执行,此时进程child1的相关信息:
pid=3952,ppid=3951;global=4,variable=2;&global=0x601080,&variable=0x7ffe1d0cbb30.
这里global和variable仍为4和2而没有变为5和3,正是因为fork创建进程时复制了父进程的资源即global=3,variable=1,这两个值并未随着父进程中变量值的变化而变化,而是在child1中自加一操作后变为4和2。为了更加明显的看出这个特点,可以将第一个printf操作放在自加一操作之前,打印输出如下:
可以看到第一次打印输出的child1进程中的变量值未受影响,但这里变量地址却与child2和父进程相同,这是由于该地址显示的为虚拟地址,而实际内存映射之后的物理地址是不同的。
接第一次的执行结果child1第一次打印输出后,又继续恢复execve()函数的执行,打印出该函数的相关信息:This is process execve:pid=3953,ppid=3951.the input parameter is execve test,可见此函数属于进程child2,另外字符串参数“execve test”成功传入。
Execve退出后返回父进程,父进程等待并捕捉到pid=3953的进程即child2进程后执行完毕退出,返回终端,而此时由于child1中添加的sleep(10)仍为结束故该进程还未退出,等待10s结束后,child1才打印出第二条printf语句,信息如下:
pid=3952,ppid=1423;global=4,variable=2;&global=0x601080,&variable=0x7ffe1d0cbb30.
Pid仍为3952,但其父进程号ppid变为1423,这是由于原来的父进程已经退出,此时child1变为孤儿进程,被另外一个进程收养。
7. 加上wait()后的执行结果:
可见此时父进程等待并回收两个子进程后才退出。
(二)进程间同步与通信实验
1. 程序需包含的头文件:
2. 程序整体结构:
第38行为定义的信号处理函数handler(),第48行为用于共享内存的子进程块,73行为用于共享内存的父进程块,接下来主函数中,首先在父进程中120行到138行是通过消息队列进行进程间通信部分创建消息并写入到消息队列的部分,接下来143至191行的child1子进程实现有名管道通信的读端,接下来192号至250行中包括父进程中包括有名管道的写端代码段,共享内存方式通信的写端函数parent_shm()调用,并且在其中创建child2子进程用来作为进程间通过信号方式同步的代码段,以及在父进程中创建子进程child3调用child_shm()用来在写入共享内存。
3. 首先关注通过有名管道实现进程间通信
首先在当前目录下创建Data.txt,写入数据“123456789”。
相关定义:
143-191行读端代码,144-151行通过access()函数判断fifo_name文件是否存在,并且在不存在时创建该文件,文件权限为0777,表示所有人可读可写可执行。接下来打开FIFO文件和Data.txt文件,打开成功后通过:
bytes_read = read(data_fd, buffer, PIPE_BUF);
buffer[bytes_read] = '\0';
与 write(pipe_fd, buffer, bytes_read);循环进行读到缓冲区并写到FIFO文件。
从read()函数从data_fd中读取PIPE_BUF个字节到buffer缓冲区返回值为实际读取的字节数,下面一行使缓冲区以空字节结尾。Write()函数将bytes_read个字节从buffer缓冲区写入到pipe_fd文件。全部写入完成后关闭两个文件退出子进程child1.
从有名管道文件中读取数据到DataFormFIFO.txt部分193-217行,同样在成功创建并打开两个文件后就可以通过read()和write()函数进行常规的读写。
4. 接下来看通过传递信号方式进行进程间通信
这部分通过定时器信号SIGALRM实现,定时时间10s,handler函数定义如下:
这个函数中除打印一些信息外还放了通过消息队列方式通信时消息队列接收端代码。主要就是功能函数就是msgrcv(),msgid为队列标志符,msgbuf为接收消息结构体的指针,下一个参数为消息字节数,这里即为结构体的大小,111为接收消息类型,IPC_NOWAIT表示若消息队列为空则不阻塞而立即返回-1.消息队列写入部分在下一部分介绍。
5. System v消息队列进行进程间通信
创建消息并写入到消息队列部分代码:
120行ftok()函数获取消息队列通讯时需要指定的ID值,121行msgget()函数创建key值指定的消息队列,IPC_CREAT|IPC_EXCL参数表示如果不存在键值与key相等的消息队列,则新建消息队列;如果存在这样的消息队列则报错。132行指定消息结构体的类型为111,133行指定消息内容为“message1 data”。134行采用非阻塞方式发送消息队列。
6. 共享内存方式实现进程间通信
Parent_shm()函数在父进程中被调用,负责将数据写入到共享内存中,若共享内存数据已满则等待;Child_shm()函数在子进程child3中被调用,负责从共享内存中将数据读取出来,如果没有数据,则等待。
重要定义:
首先获取共享内存通信ID,并进行一系列初始化:
231行shmget()创建一个内存段,创建模式为如果不存在键值与key相等的共享内存,则新建一个共享内存;如果存在这样的共享内存则报错。
236行semctl()函数创建一个信号量信号量标识符为semid,SETVAL表示用联合体中val成员的值设置信号量集合中单个信号量的值,这里即为第四个参数1。
Parent_shm():数据写入共享内存并在子进程结束后释放信号量和共享内存
While循环中依次循环向共享内存区中写入从a开始的26个字母,这里只取前20个查看效果。Buf.sem_op=-1时表示对信号量进行-1,消耗一个资源,buf.sem_op=1时表示对信号量进行+1,释放一个共享资源。97-99行semctl(block->semid,0,IPC_RMID用来删除信号量集合;Shmdt()函数将内存段映射到用户进程空间,shmctr()函数删除共享内存段。
Child_shm()函数将共享内存区中数据依次读出并打印输出读取出来的数据:
7. 将以上几部分综合到一起后执行输出:
有名管道输出文件内容:
从该结果可以看出四种进程间通信方式均成功实现!
注意:这里由于创建管道文件需要向/tmp中写入数据,执行时需要sudo权限。
9.这里程序f3执行时中存在一个问题,每次编译完成后第一遍执行在有名管道通信时打开fifo_fd总会出错,而第二遍执行及之后均正常执行,没有错误,未理解原因。
错误时打印输出如下: