实验2 进程控制
一、实验目的
1、加深对进程的理解,掌握进程和程序的区别。
2、进一步认识进程并发执行的实质。
3、了解Linux系统中进程创建和执行的方法,掌握fork系统调用的用法。
4、了解Linux系统中进程通信的基本原理。Linux系统的进程通信机构(IPC) 允许在进程之间大批量地交换数据,通过实验掌握Linux支持的消息通讯机制和共享内存机制。
二、实验准备
(1)阅读进程PCB的数据结构,理解进程PCB的作用。Linux系统的进程控制块PCB用一个称为task-struct的结构体来描述。
http://www.cnblogs.com/hanxiaoyu/p/5549212.html
(2)Linux系统下进程相关命令练习,命令包括:ps,pstree,top, kill等。
三、实验内容
1、使用fork创建进程
编写程序,创建如下图的进程树,当此程序运行时,在系统中有一个父进程和多个子进程活动,父进程等子进程运行结束后退出。
在程序中,设置变量count=0,每一个进程在屏幕上显示不同的字符串,父进程的字符串内容要包括:父进程、PID、自己的学号、姓名、变量count的值;子进程字符串要包括:PID、子进程序号(子进程1或子进程2)、变量count的值,循环显示5次。变量count按一定的规律变化。记录屏幕上的显示结果,并分析变量count的变化规律。
getpid()获得进程的ID。
说明:学号最后一位为奇数,创建图1的进程树;若为偶数,创建图2的进程树。
2、使用exec替换子进程程序
修改任务1编写的程序,将子进程改为独立的程序,父进程创建子进程,并进行程序替换,观察程序执行时屏幕出现的现象,并分析原因。
3、分析进程的父进程
在系统中,所有的进程组成一个进程树,但在实际操作中,存在中孤儿进程情况,孤儿进程就是父进程已终止,但是子进程没终止,然后就成孤儿。分析孤儿进程的父进程如何变化。
编写程序,要求如下:
父进程创建子进程;
子进程每2秒输出父进程的ID,循环10次;
父进程3秒后,输出自己的ID,结束进程;
分析程序的运行结果,重点说明子进程的父进程如何变化的。
4、共享存储区机制进程通信
编写生产者和消费者通过共享存储区通信的程序。生产者随机产生10个整型数据,写入共享存储区;消费者读出数据,并进行平方和平方根运算后输出。使用系统调用shmget()、shmat()、sgmdt()、shmctl()等, 实现程序。
5、消息队列实现进程通信
编写生产者和消费者通过消息队列通信的程序。父进程从键盘上接受10个数据,对其求和sum1,子进程求这10个数平方和sum2,使用消息队列方式将结果传给父进程,父进程计算sum1+sum2,打印结果。
四、测试数据与实验结果分析
1:使用fork创建进程
(1)创建exp2-1.c文件:vim exp2-1.c
(2)按下i键,进行文件编写,底部变为【插入】字样
(3)编写程序代码
(4)按下escape键,进入命令模式,底部插入字样消失
(5)保存文件编写内容::wq
(6)使用gcc生成可执行文件:gcc exp2-1.c
(7)运行文件:./a.out
【分析】
父进程和子进程都有各自的地址空间,它们之间的count变化是独立的。按照if-else条件的判断顺序,创建的子进程1先执行,而子进程2后执行。最后由于父进程处有wait(NULL)操作,因此父进程会等所有子进程结束后执行。因此,终端依次输出子进程1在5个循环内的count自增变化、子进程2在5个循环内的count自减变化、父进程的count初始值内容。
2:使用exec替换子进程程序
(1)在文件界面,通过复制粘贴操作,创建exp2-2.c文件
(2)打开exp2-2.c文件:vim exp2-2.c
(3)编辑exp2-2.c文件(父进程文件)
(4)创建并编辑child-1.c文件(子进程1文件):vim child-1.c
(5)创建并编辑child-2.c文件(子进程2文件):vim child-2.c
(6)编译子进程文件和父进程文件,并生成【.o】文件:
子进程1文件:gcc -o child-1 child-1.c
子进程2文件:gcc -o child-2 child-2.c
父进程文件:gcc -o exp2-2 exp2-2.c
(7)运行父进程的可执行文件:./exp2-2
【分析】
父进程创建子进程后,子进程使用 execlp 函数将自身的程序替换为child-1.o和child-2.o的可执行文件。execlp函数会加载并执行指定的【.o】文件,因此子进程的代码会被替换。exec函数族可以在运行时替换进程的程序,使得进程可以执行不同的程序,有利于实现进程间通信和控制。
3:分析进程的父进程
(1)创建exp2-3.c文件,并编写相应的程序
(2)编译exp2-3.c文件:gcc -o exp2-3 exp2-3.c
(3)运行可执行文件:./exp2-3
等待文件运行,并输出最终的结果。
【分析】
在上述的程序中,父进程创建了子进程,并在3秒后终止。子进程会循环输出其父进程的PID,共执行10次,每次间隔2秒。父进程首先输出自己的PID。子进程在父进程终止后仍然继续执行,并且在每次循环中输出父进程的PID。上述结果体现了Linux 进程管理的一个重要特性:当父进程终止后,子进程的父进程会变为init进程,以确保子进程不会成为孤儿进程。
4:共享存储区机制进程通信
(1)创建producer2-4.c文件和consumer2-4.c文件,并编写相应的程序
producer2-4.c:
consumer2-4.c:
(2)编译producer2-4.c文件和consumer2-4.c文件
生产者文件:gcc -o producer2-4 producer2-4.c
消费者文件:gcc -o consumer2-4 consumer 2-4.c -lm
(3)运行生产者的可执行文件:./ producer2-4
(4)运行消费者的可执行文件:./ consumer2-4
【分析】
在上述的程序中,使用shmget创建共享内存段,然后使用shmat将共享内存连接到生产者和消费者进程的地址空间。生产者将数据写入共享内存中,而消费者从共享内存中读取数据。最后使用shmdt分离共享内存,并使用shmctl删除共享内存。通过运行上述程序,可以观察到生产者和消费者之间的数据共享和通信,学习并验证共享内存机制的工作原理。
5:消息队列实现进程通信
(1)创建exp2-5.c文件,并编写相应的程序
(2)编译exp2-5.c文件:gcc -o exp2-5 exp2-5.c
(3)运行可执行文件:./ exp2-5
根据打印字符的提示,输入用户想要计算的数字。
最后等待运行结果total sum。
【分析】
在上述的程序中,使用msgget创建消息队列,实现了父子进程之间的通信,父进程向子进程发送数据,子进程进行计算并返回结果。这种通信方式可以在不同的进程之间实现异步的数据交换,非常适合需要协调不同任务的情况。
五、实验心得与体会
1:基本的进程创建、终止和状态管理操作:学习了如何使用fork函数创建子进程,以及如何通过wait函数等待子进程的终止。
2:消息队列的灵活性:消息队列是一种异步通信方式,适用于需要松散耦合的进程通信。消息队列允许进程在不同的时间发送和接收消息,并实现进程间的解耦合。
3:exec 函数族的用途:exec 函数族(如 execl、execv)允许进程在运行时替换自身的程序。通过调用exec函数族,可以实现在父进程中创建子进程并替换为不同的程序。
4:孤儿进程与 init 进程:孤儿进程是指其父进程已经终止的子进程。在 Linux 系统中,孤儿进程会被 init 进程领养,init 进程成为其新的父进程,以确保孤儿进程不会成为僵尸进程。
5:进程通信的应用:进程通信是多进程操作系统中的重要概念,允许不同的进程之间共享数据、协同工作以及相互通信。不同的通信方式,如共享内存、消息队列、管道等,可以用于不同的应用场景。每种通信方式都有其特定的适用场景,根据需求选择合适的通信方式可以提高程序的性能和可维护性。
6:如果出现下图的报错,可以采用以下的方法进行解决——在编译时显式链接数学库,即在编译命令中添加【-lm】标志来链接数学库。在大多数情况下,数学库是libm。
六、实验源程序的代码
由于未配置ssh,因此无法将服务器上的代码文件拖出,因此以下部分采用手动敲代码的方法,如有错误还请查看实验过程中的截图。
实验1:
【exp2-1.c】
#include<unistd.h> #include<sys/wait.h> #include <sys/types.h> int main(){ int i,count=0; pid_t pid1,pid2; pid1=fork(); if(pid1==0){ for(i=0;i<5;i++){ printf("Child-1 pid=%d count=%d\n",getpid(),count); count++; } } else if(pid1>0){ pid2=fork(); if(pid2==0){ for(i=0;i<5;i++){ printf("Child-2 pid=%d count=%d\n",getpid(),count); count--; } } else if(pid2>0){ wait(NULL); printf("Parent pid=%d count=%d\n",getpid(),count); count+=2; printf("Student:Xia Wanke,ID:2020301010225"); } } return 0; } |
实验2:
【exp2-2.c】
#include <stdlib.h> #include <unistd.h> #include <sys/wait.h> #include <sys/types.h> #include <stdio.h> int main(){ int i,count=0; pid_t pid1,pid2; if(pid1==0){ execlp("./child-1",0); } else if(pid1>0){ pid2=fork(); if(pid2==0){ execlp("./child-2",0); } else if(pid2>0){ wait(NULL); printf("Parent pid=%d count=%d\n",getpid(),count); count+=2; printf("Student:Xia Wanke,ID:2020301010225"); } } return 0; } |
【child-1.c】
#include <stdlib.h> #include <unistd.h> #include <sys/wait.h> #include <sys/types.h> #include <stdio.h> int main(){ int count=0,i; for(i=0;i<5;i++){ printf("Child-1 pid=%d count=%d\n",getpid(),count); count++; } return 0; } |
【child-2.c】
#include <stdlib.h> #include <unistd.h> #include <sys/wait.h> #include <sys/types.h> #include <stdio.h> int main(){ int count=0,i; for(i=0;i<5;i++){ printf("Child-2 pid=%d count=%d\n",getpid(),count); count--; } return 0; } |
实验3:
【exp2-3.c】
#include <stdlib.h> #include <unistd.h> #include <sys/wait.h> #include <sys/types.h> #include <stdio.h> int main(){ int i; pid_t pid; pid=fork(); printf("begin\n"); if(pid==0){ for(i=0;i<10;i++){ printf("This is child. PID of the parent is %d\n",getppid()); sleep(2); } } else if(pid>0){ sleep(3); printf("This is parent. The parent process will stop running\n"); } printf("end\n"); printf("Student:Xia Wanke,ID:2020301010225"); return 0; } |
实验4:
【producer2-4.c】
#include <stdlib.h> #include <unistd.h> #include <sys/shm.h> #include <sys/types.h> #include <sys/ipc.h> #include <stdio.h> #define KEY 75 #define MEM 1024 int main(){ int shmid,*p,i; shmid=shmget(KEY,MEM,0777|IPC_CREAT); char *shmaddr; shmaddr=shmat(shmid,0,0); p=(int*)shmaddr;
for(i=0;i<10;i++){ *p=rand()%100; sleep(1); printf("%d\n",*p); p++; } sleep(5); shmdt(shmaddr); printf("end\n"); return 0; } |
【consumer2-4.c】
#include <stdlib.h> #include <unistd.h> #include <sys/shm.h> #include <sys/types.h> #include <sys/ipc.h> #include <stdio.h> #include <math.h> #define KEY 75 #define MEM 1024 int main(){ int mid,*q,*t,i; char *maddr; mid=shmget(KEY,MEM,0777); maddr=shmat(mid,0,0); q=(int*)maddr; sleep(12); for(i=0;i<10;i++){ printf("%d's square: %d, sqrt: %f\n",(*q),(*q)*(*q),sqrt(*q)); q++; } shmdt(maddr); shmctl(mid,IPC_RMID,0); return 0; } |
实验5:
【exp2-5.c】
#include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <sys/ipc.h> #include <sys/msg.h> #include <unistd.h> // 消息队列结构体 struct msg_buffer { long mtype; // 消息类型 int result; // 消息内容 }; int main() { int sum1 = 0; int sum2 = 0; int i; // 创建消息队列 int msgid = msgget(IPC_PRIVATE, 0666 | IPC_CREAT); if (msgid == -1) { perror("msgget"); exit(1); } // 创建子进程 pid_t pid = fork(); if (pid == -1) { perror("fork"); exit(1); } if (pid == 0) { // 子进程 // 接收父进程发送的10个数,计算平方和 struct msg_buffer msg; for (i = 0; i < 10; i++) { msgrcv(msgid, &msg, sizeof(int), 1, 0); sum2 += msg.result* msg.result; } // 将平方和发送给父进程 msg.mtype = 2; msg.result = sum2; msgsnd(msgid, &msg, sizeof(int), 0); } else { // 父进程 // 从键盘输入10个数,计算和并发送给子进程 struct msg_buffer msg; for (i = 0; i < 10; i++) { printf("Enter number %d: ", i + 1); scanf("%d", &msg.result); sum1 += msg.result; msg.mtype = 1; msgsnd(msgid, &msg, sizeof(int), 0); } // 等待子进程发送结果 msgrcv(msgid, &msg, sizeof(int), 2, 0); // 计算总和并打印结果 int total_sum = sum1 + msg.result; printf("Total sum: %d\n", total_sum); // 删除消息队列 msgctl(msgid, IPC_RMID, NULL); } return 0; } |