操作系统实验报告
实验二:进程通信(一)——管道及共享内存
一、实验目的
- 熟悉并掌握管道机制,并实现进程间通信;
- 熟悉并掌握共享内存机制,并实现进程间通信。
二、实验内容和步骤
2.1 任务一
- 阅读以上父子进程利用管道进行通信的例子(例1),写出程序运行结果并分析。
- 编写程序:父进程利用管道将一字符串交给子进程处理。子进程读字符串,将里面的字符反向后再交给父进程,父进程最后读取并打印反向的字符串。
2.2 任务二
- 阅读例2的程序,运行一次该程序,然后用ipcs命令查看系统中共享存储区的情况,再次执行该程序,再用ipcs命令查看系统中共享内存的情况,对两次的结果进行比较,并分析原因。最后用ipcrm命令删除自己建立的共享存储区。
- 登陆两个窗口,先在一个窗口中运行例3程序1(或者只登陆一个窗口,先在该窗口中以后台方式运行程序1),然后在另一个窗口中运行例3程序2,观察程序的运行结果并分析。运行结束后可以用ctrl+c结束程序1的运行。
- 编写程序:使用系统调用shmget(),shmat(),shmdt(),shmctl(),编制程序。要求在父进程中生成一个30字节长的私有共享内存段。接下来,设置一个指向共享内存段的字符指针,将一串大写字母写入到该指针指向的存贮区。调用fork()生成子进程,让子进程显示共享内存段中的内容。接着,将大写字母改成小写,子进程修改共享内存中的内容。之后,子进程将脱接共享内存段并退出。父进程在睡眠5秒后,在此显示共享内存段中的内容(此时已经是小写字母)。
三、代码及运行结果分析
3.1 任务1(2)
3.1.1 代码
3.1.2 运行结果
3.1.3 结果分析
总结:利用系统调用pipe(fd)可创建一个简单的管道。其中,fd[0]存放供读进程使用的文件描述符(管道出口),fd[1]存放供写程使用的文件描述符(管道入口)。首先执行父进程,但因管道中无内容暂时阻塞;然后执行子进程,向父进程发送消息,即在管道中写入数据,再关闭子进程;而后管道中有了数据,父进程继续执行,接收管道消息并打印输出。
3.2 任务1(2)
3.2.1 代码
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
int main() {
int x, fd1[2], fd2[2]; //fd1父进程写,fd2子进程写
char buf[30], s[30], temp;
char c;
pipe(fd1);
pipe(fd2); //创建两个管道
printf( "请输入一串字符串\n" );
scanf( "%s", buf);
while((x=fork()) == -1 );
//执行子进程
if(x == 0) {
close(fd1[1]); //关闭父进程写
close(fd2[0]); //关闭子进程读,让子进程写和父进程读
printf( "子进程!\n" );
read(fd1[0], s, 30);
printf( "从父进程获得字符串:%s\n", s);
//反转字符串
for(int i = 0; i <= strlen(s)/2; i++){
temp = s[i];
s[i] = s[strlen(s) - i - 1];
s[strlen(s) - i - 1] = temp;
}
write( fd2[1], s, 30);
exit(0);
} else {
//执行父进程
close(fd1[0]); //关闭父进程读
close(fd2[1]); //关闭子进程读,让子进程写和父进程读
printf( "父进程! \n");
write(fd1[1], buf, 30);
wait(NULL);
read(fd2[0], buf, 30);
printf( "从子进程获得反向字符串:%s\n" , buf);
}
}
3.2.2 运行结果
3.2.3 结果分析
3.3 任务2(1)
3.3.1 代码
3.3.2 运行结果
第一次执行并使用ipcs命令
第二次执行并使用ipcs命令
利用ipcrm命令删除建立的共享区
3.3.3 结果分析
- 指定共享区号004;
- 定义shmid_1和shmid_2作为接收创建共享区后的描述字;
- 在共享区004下,以0644为权限值创建长度为1000的共享区,如果失败则报错;
- 打印输出第一个共享区段的描述字54;
- 在共享区004下,以0644为权限值再次创建长度为20的共享区,如果失败则报错;
- 打印输出第二个共享区段的描述字55;
- 使用ipcs命令查看存储情况,可以看到两个共享区段的键值分别为0x00000004、0x00000000,拥有者为rql,权限为644,连接共享区数分别为1000、20;
- 再次执行程序,查看打印输出的两个共享区段描述字为54和56;
- 使用ipcs命令查看存储情况,可以看到两个共享区段的键值分别为0x00000004、0x00000000,拥有者为rql,权限为644,连接共享区数分别为1000、20。
- 利用ipcrm -M 004命令根据关键字删除建立的共享区。
- 使用ipcs命令查看存储情况,已经没有键值为0x00000004的共享区,删除成功。
两次运行结果比较:两次运行结束后的第一个共享标识符相同,第二个共享标识符不同。
原因分析:创建第一个共享区域时,shmflag为“权限值|IPC_CREAT”,如果key值是新的(第一次执行程序),则返回新创建的内存段标识符;如果key值是旧的(第二次执行程序),则返回已存在内核中的具有相同关键字值的内存段标识符,故均输出54,即同一共享区域段。而第二个返回值不同,原因是调用shmget方法时,设置key值为IPC_PRIVATE,它将生成一个新的共享内存段,故两次返回值输出分别是55和56,即两个不同的共享区域段。
3.4 任务2(2)
3.4.1 代码
例3-程序1
例3-程序2
3.4.2 运行结果
登陆两个窗口,首先在一个窗口中运行例3程序1,结果如下图所示:
例3-程序1运行结果
然后在另一个窗口中运行例3程序2,结果如下图所示:
例3-程序2运行结果
3.4.3 结果分析
- 在程序1段中指定共享区号SHMKEY为004,宏定义常量K为1024;定义i变量,指针pint(int类型)和地址指针addr(char类型)。
- 引入cleanup()函数,该函数实现以下功能:删除shmid指向的内存段,但并非真正删除,只有当前连接到该内存段的最后一个进程正确地断开了与它的连接,实际的删除操作才会发生。
- for循环实现signal信号量中断。
- 在共享区SHMKEY下,以0777为权限值创建长度为16K的共享区,并将标识符返回给shmid;
- 进程挂接内存段,由系统得到共享区首地址赋值给addr,并映射到自己的内存空间;
- 打印输出首地址号为“0x4d599000”;
- 将本地指针addr强制类型转换为int指针类型,并赋值给pint,此时pint指向共享区的首地址;
- 通过for循环,将i变量从0到255逐一写入pint指向的共享区,每写入一个数据,地址向后偏移一次。
- pause()函数等待进程读,终端暂停被占用,即此时打开另一个终端(程序2执行)。
- 在程序2段,在共享区SHMKEY下,以0777为权限值创建长度为8K的共享区,并将标识符返回给shmid;
- 该进程挂接内存段,系统得到共享区首地址赋值给addr,并映射到自己的内存空间;
- 将本地指针addr强制类型转换为int指针类型,并赋值给pint,此时又有一pint指向共享区的首地址(与上述pint不同);
- 通过for循环从首地址开始访问共享区内容,每读一个数据,地址指针向后偏移一位,结果输出为程序段1写入的0、1、2、…、255;
- 程序2段执行完毕,通过ctrl+c结束程序1段的执行。
3.5 任务2(3)
3.5.1 代码
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#define SHMKEY 004 //学号末三位
#define K 1024
int pshmid; //parent
int cshmid; //childint
int main() {
int i, *pint, x;
int delete;
char *paddr , *caddr , *paddr2, c;
char words[26]= {'A','B','C','D','E','F','G','H' ,'I','J',
'K','L','M','N','O','P','Q','R','S',
'T','U','V','W','X','Y','Z'};
while((x = fork()) == -1 );
if(x == 0) {
printf( "子进程!\n");
cshmid = shmget(SHMKEY, 30*K, 0666);//子进程建立30共享区
caddr = shmat(cshmid, 0, 0); //挂接得到共享区首地址
for(i = 0; i <= 25; i++) {
printf( "%c ", *caddr); //读出数据
*caddr = (*caddr + 'a'-'A'); //取到小写
caddr++;
if(i == 25)
printf( "\n");
}
delete = shmdt(caddr); //释放
exit(0);
}else {
printf( "父进程!\n" );
pshmid = shmget(SHMKEY, 30*K, 0666| IPC_CREAT); //建立30K共享区
paddr = shmat(pshmid, 0, 0); //挂接得到共享区首地址
paddr2 = paddr; //挂接得到读小写
for(i = 0; i <= 25; i++) {
*paddr++ = words[i]; //往共享区写入大写字母
}
sleep(5);
for(i = 0; i <= 25; i++){
printf( "%c ", *paddr2++); //读出数据
if(i == 25)
printf( "\n" );
}
}
return 0;
}
3.5.2 运行结果
3.5.3 结果分析
四、实验心得
这是操作系统的第二次课内实验,主要通过程序分析和代码设计编写,熟悉掌握管道和共享内存两种新的进程通信机制,实现进程间的通信工作。
实验开始,我先对ppt中的知识点进行阅读与学习,掌握了管道机制中的pipe()、read()、write()函数的用法,了解了共享内存机制中的shmget()、shmat()、shmdt()、shmctl()函数的用法,为后续开展实验做足准备。
针对任务一(1),对ppt给出的代码运行后进行结果分析。起初,我错误地运用了进程并发知识,认为系统先执行父进程,当父进程读入空串后系统并发执行切换到子进程,然后向管道内部传入数据,再让父进程打印输出。虽然这个分析和运行的结果一致,但其实内部原理完全错误。后来,我意识到了自己的问题,正确的分析应该是父进程先被调用,但是由于管道中没有数据,父进程被阻塞了才会调用子进程,而不是系统并发切换到子进程。这让我不仅更深入地理解了系统并发性,也补习到了管道中无数据进程会被阻塞的知识。
针对任务一(2),编写代码实现父进程传递数据给子进程,子进程修改后再传递还给父进程的双向通信。经过分析,我以例1的案例为模板,创建了两个管道,通过read()和write()函数完成数据的传输。这里的难点是要区分清楚哪些端口是用不到的需要close()的。
在任务二(1)中,我对shmget()函数有了更深刻的认识。该函数的第一个参数若为IPC_PRIATE则是创建新的共享内存段,若为自定义key才是没有就创建新的、有就沿用;第二个参数则是长度大小;第三个参数为权限,想要一同使用同一共享区必须保证权限一致,若为权限值|IPC_CREAT则与IPC_PRIVATE不同,它会判断内存中是否有旧的,如果有旧的就继续使用,如果没有才会创建新的共享内存区,所以ipcs的两次结果不一样。
在任务二(2)中,我学习到了如何访问共享内存区进行读写操作。由于没有父子进程,故使用了pause函数进行多终端测试。此题还用到了shmctl()和shmat()函数,shmat返回值是系统给出的共享区首地址,故需要对指针操作熟练。
任务二(3)是对于上述题目的改进,在进程并发中加入了共享内存机制。我在父进程中创建共享区并写入大写字母,然后由于并发性需要给出sleep函数,让系统调用子进程从共享区中读出数据并转换为小写覆盖掉原来的内容。
于此本次实验顺利完成,经过分析和实践,从这五个内容中,我分别学习到了:进程的并发和阻塞、pipe函数的使用、shmget参数的理解、shmctl和shmat以及多终端操作、地址指针的理解和代码编写,受益颇多。