实验报告
实验内容
进程间通信—共享内存。
(1) 验证:编译运行课件 Lecture 08 例程代码:
Linux 系统调用示例 reader-writer 问题:Algorithms 8-1 ~ 8-3.
POSIX API 应用示例 producer-consumer 问题:Algorithms 8-4 ~ 8-5.
(2) 设计:Alg.8-1 ~ 8-3 示例只解决单字符串的读写。修改程序将共享空间组织成一个结构类型(比如学号、姓名)的循环队列进行 FIFO 操作,采用共享内存变量控制队列数据的同步(参考数据结构课程有关内容)。
实验环境
Ubuntu 20.04.2.0(64位)
实验过程
一. alg.8-0 ~ alg.8-3
(一)对代码中不熟悉的内容进行解析:
#define PERM S_IRUSR|S_IWUSR|IPC_CREAT
(参考资料)
S_IRUSR
:用户读权限
S_IWUSR
:用户写权限
S_IRGRP
:用户组读权限
S_IWGRP
:用户组写权限
S_IROTH
:其他组都权限
S_IWOTH
:其他组写权限
IPC_CREAT
:如果共享内存不存在,则创建一个共享内存,否则打开操作。
creat()
(参考资料)
函数原型:int creat(const char * pathname, mode_t mode);
函数功能: 创建文件
参数: 参数 pathname 指向欲建立的文件路径字符串。参数 mode 创建模式,指定用户操作权限。
返回值: 成功返回文件标识符,失败返回-1
ftok()
(参考资料)
函数原型:key_t ftok(const char *pathname, int proj_id);
函数功能: 将 pathname 和 proj_id 转换为 IPC key。
参数: 参数 pathname 是指定的文件名,这个文件必须是存在的而且可以访问的。 proj_id 是子序号,它是一个8 bit的整数,即范围是0~255。
返回值: 成功返回 key_t 键值,失败返回-1
shmget()
(参考资料)
函数原型:int shmget(key_t key, size_t size, int flag);
函数功能: 创建一块新的共享内存。
参数:
key:IPC key 。
size:要建立共享内存的长度,所有的内存分配操作都是以页为单位的,所以如果一段进程只申请一块只有一个字节的内存,内存也会分配整整一页。
flag:标志。
返回值: 成功返回共享内存的标识符,失败返回-1
0666|PERM
(参考资料)
0666:
第一位:表示这是个八进制数 000
第二位:当前用户的权限:6=110(二进制),每一位分别对应可读,可写,可执行,6说明当前用户可读可写不可执行。
第三位:group组用户,6的意义同上。
第四位:其它用户,每一位的意义同上,0表示不可读不可写也不可执行。
shmat()
(参考资料)
函数原型:void *shmat( int shmid , char *shmaddr , int shmflag );
函数功能: 用来允许本进程访问一块共享内存。
参数:
shmid:共享内存的ID。
shmaddr:共享内存的起始地址,如果shmaddr为0,内核会把共享内存映像到调用进程的地址空间中的合适位置;如果shmaddr不为0,内核会把共享内存映像到shmaddr指定的位置。所以一般把shmaddr设为0。
shmflag:本进程对该内存的操作模式。
返回值: 成功返回共享内存的起始地址,失败返回-1
sprintf()
(参考资料)
函数原型:int sprintf(char *str, const char *format, ...);
函数功能: 发送格式化输出到 str 所指向的字符串。
返回值: 成功返回写入的字符总数,不包括字符串追加在字符串末尾的空字符,失败返回-1
例:
sprintf(str, "Pi 的值 = %f", M_PI);
此时,str = "Pi 的值 = 3.141593"
sscanf()
(参考资料)
函数原型:int sscanf(const char *str, const char *format, ...);
函数功能: 从字符串读取格式化输入。
返回值: 成功返回成功匹配和赋值的个数。如果到达文件末尾或发生读错误,则返回 EOF。
例:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main()
{
int day, year;
char weekday[20], month[20], dtm[100];
strcpy( dtm, "Saturday March 25 1989" );
sscanf( dtm, "%s %s %d %d", weekday, month, &day, &year );
printf("%s %d, %d = %s\n", month, day, year, weekday );
return(0);
}
输出: March 25, 1989 = Saturday
ipcs -m
(参考资料)
打印出使用共享内存进行进程间通信的信息。
grep 'shmid'
(参考资料)
打印出引用该共享内存的进程数。
shmdt()
(参考资料)
函数原型: int shmdt( char *shmaddr );
函数功能: 用来禁止本进程访问一块共享内存。
参数: shmaddr是那块共享内存的起始地址。
返回值: 成功返回0,失败返回-1
shmctl()
(参考资料)
函数原型: int shmctl( int shmid , int cmd , struct shmid_ds *buf );
函数功能: 控制对这块共享内存的使用
参数:
shmid:共享内存的ID。
cmd:控制命令,可取值如下:
IPC_STAT 得到共享内存的状态
IPC_SET 改变共享内存的状态
IPC_RMID 删除共享内存
注:IPC_RMID 命令实际上不从内核删除一个段,而是仅仅把这个段标记为删除,实际的删除发生在最后一个进程离开这个共享段时。
buf:一个结构体指针。IPC_STAT的时候,取得的状态放在这个结构体中。如果要改变共享内存的状态,用这个结构体指定。
返回值: 成功返回0,失败返回-1
(二)运行
命令行:
$ gcc -o alg.8-2-shmread.o alg.8-2-shmread.c
$ gcc -o alg.8-3-shmwrite.o alg.8-3-shmwrite.c
$ gcc -o alg.8-1-shmcon alg.8-1-shmcon.c
$ ./alg.8-1-shmcon myshm
输出结果:
max record number = 1, shm size = 4100
key generated: IPC key = 27052896
shmcon: shmid = 32777
shmcon: shared Memory attached at 0x7f87fb0ce000
------ Shared Memory Segments ------
0x27052896 32777 zhaowx9 666 4100 1
------ Shared Memory Segments ------
0x27052896 32777 zhaowx9 666 4100 0
shmread: IPC key = 27052896
shmread: shmid = 32777
shmread: shared memory attached at 0x7fe4055fc000
shmread process ready ...
shmwrite: IPC key = 27052896
shmwrite: shmid = 32777
shmwrite: shared memory attached at 0x7f06c1762000
shmwrite precess ready ...
Enter some text: test1: hello
shared buffer: test1: hello
You wrote: test1: hello
Enter some text: test2: Goodbye!
shared buffer: test2: Goodbye!
You wrote: test2: Goodbye!
Enter some text: end
shared buffer: end
You wrote: end
shmcon: shmid = 32777 removed
------ Shared Memory Segments ------
nothing found ...
(三)运行过程:
-
通过
creat(pathname, O_RDWR)
根据运行前的输入名(一开始存在 argv[1] 中,后来存在 pathname 中)创建相应名称的可读可写打开的共享文件,并将该文件的标识符赋给 ret。根据我的输入,该文件命名为myshm
。 -
通过
ftok(pathname, 0x27)
根据共享文件名 myshm 和自定义的 0x27(范围:0x0001 - 0xffff)生成 IPC key 并将其赋给 key。 -
通过
shmget((key_t)key, shmsize, 0666|PERM)
根据 key在共享文件 myshm 中创建共享内存,创建的内存大小为shmsize,当前用户、group组用户、其他用户的权限都为可读可写不可执行,并将共享内存的标识符赋给 shmid。 -
通过
shmat(shmid, 0, 0)
允许本进程访问 shmid 指向的共享内存,同时系统内核分配一个合适的虚拟地址给共享内存,并将该虚拟地址的起始地址赋给 shmptr。 -
通过
shared = (struct shared_struct *)shmptr
将共享内存的地址赋给 shared 结构体,使 shared 的地址就是共享内存的地址。并通过shared->written = 0
设置权限,使得当前用户可写 buffer ,其他用户可读 buffer。 -
通过
sprintf(cmd_str, "ipcs -m | grep '%d'\n", shmid)
将ipcs -m | grep '32777'\n
赋给 cmd_str。然后通过system(cmd_str)
进行系统调用,打印出使用共享内存进行进程间通信的信息,在最后一列打印出引用该共享内存的进程数。此时引用数为1,因为本进程在引用它。 -
通过
shmdt(shmptr)
禁止本进程访问 shmptr 指向的共享内存。 -
通过
system(cmd_str)
进行系统调用,打印出使用共享内存进行进程间通信的信息,在最后一列打印出引用该共享内存的进程数。此时引用数为0,因为前面禁止了本进程访问该共享内存。 -
通过
sprintf(key_str, "%x", key)
将 key(IPC key) 转为字符串赋给 key_str,再进行参数列表的定义char *argv1[] = {" ", key_str, 0}
。 -
用两次
vfork()
和execv()
创建两个子进程,同时执行程序alg.8-2-shmread
和alg.8-3-shmwrite
,参数列表都为 argv1。 -
父进程挂起,等待两个子进程结束。
-
子进程1:
alg.8-2-shmread
通过
sscanf(argv[1], "%x", &key)
将参数列表中的 IPC key 转化成十六进制数赋给 key。通过
shmid = shmget((key_t)key, TEXT_NUM*sizeof(struct shared_struct), 0666|PERM)
打开共享内存(由于该共享内存已经存在,所以直接打开),并将该内存的标识符赋给 shmid。通过
shmptr = shmat(shmid, 0, 0)
允许该子进程访问 shmid 指向的共享内存,并将该共享内存虚拟地址的起始地址赋给 shmptr。通过
shared = (struct shared_struct *)shmptr
将共享内存的地址赋给 shared 结构体,使 shared 的地址就是共享内存的地址。 -
子进程2:
alg.8-3-shmwrite
通过
sscanf(argv[1], "%x", &key)
将参数列表中的 IPC key 转化成十六进制数赋给 key。通过
shmid = shmget((key_t)key, TEXT_NUM*sizeof(struct shared_struct), 0666|PERM)
打开共享内存(由于该共享内存已经存在,所以直接打开),并将该内存的标识符赋给 shmid。通过
shmptr = shmat(shmid, 0, 0)
允许该子进程访问 shmid 指向的共享内存,并将该共享内存虚拟地址的起始地址赋给 shmptr。通过
shared = (struct shared_struct *)shmptr
将共享内存的地址赋给 shared 结构体,使 shared 的地址就是共享内存的地址。 -
两个子进程都已经允许访问共享内存,接下来就可以进行通讯了。
-
一开始
shared->written == 0
,此时子进程1挂起,子进程2正在执行并等待字符串的输入。 -
字符串输入完毕后回车,字符串通过
strncpy(shared->mtext, buffer, TEXT_SIZE)
传递到共享内存区的文本中,同时将 shared->written 设置为1。 -
子进程2挂起,子进程1开始执行,将共享内存区的文本输出,同时将 shared->written 设置为0。
-
循环执行14 ~ 16,直到输入 end 结束通信。
-
两个进程都通过
shmdt(shmptr)
将结束对共享内存的访问,然后结束进程。 -
父进程继续执行,通过
shmctl(shmid, IPC_RMID, 0)
删除此块共享内存。 -
通过
system(cmd_str)
再次进行系统调用,发现找不到此块共享内存的任何信息,说明共享内存已被删除。 -
父进程结束。
二. alg.8-4 ~ alg.8-6
(一)对代码中不熟悉的内容进行解析:
shm_open
(参考资料)
函数原型:int shm_open(const char *name,int oflag,mode_t mode)
函数功能: 打开或创建一个共享内存区。
参数:
name:共享内存区的名字
oflag:标志位
必选项:以下三个常数中必须指定一个,且仅允许指定一个。
O_RDONLY 只读打开
O_WRONLY 只写打开
O_RDWR 可读可写打开
可选项可以同时指定0个或多个
O_CREAT 若此文件不存在则创建它。使用此选项时需要提供第三个参数mode ,表示该文件的访问权限。
O_EXCL 如果同时指定了O_CREAT,并且文件已存在,则出错返回。
mode:权限位(如 0666)
返回值: 成功返回文件描述符,失败返回-1
ftruncate()
(参考资料)
函数原型:int ftruncate(int fd, off_t length);
函数功能: ftruncate()会将参数 fd 指定的文件大小改为参数 length 指定的大小。
参数: 参数 fd 为已打开的文件描述词,而且必须是以写入模式打开的文件。如果原来的文件大小比参数 length 大,则超过的部分会被删去。
返回值: 成功返回0,失败返回-1
shm_unlink()
(参考资料)
函数原型:int shm_unlink(const char *name);
函数功能: 删除一个共享内存区,即删除/dev/shm目录的文件,shm_unlink 删除的文件是由 shm_open 函数创建于 /dev/shm 目录的。
参数: name是共享内存区的名字。
返回值: 成功返回0,失败返回-1
mmap()
(参考资料)
函数原型:void *mmap(void *addr, size_t length, int prot, int flags,int fd, off_t offset);
函数功能: 将打开的文件映射到内存,一般是将 shm_open 打开的文件(共享内存区)映射到内存,当然也可以将硬盘上的用 open 打开的文件映射到内存。这个函数只是将文件映射到内存中,使得我们用操作内存指针的方式来操作文件。
参数:
addr:要将文件映射到的内存地址,一般应该传递NULL(或者0)来由Linux内核指定。
length:要映射的文件数据长度。
prot:映射的内存区域的操作权限(保护属性),包括PROT_READ、PROT_WRITE、PROT_READ|PROT_WRITE。
flags:标志位参数,包括:MAP_SHARED、MAP_PRIVATE与MAP_ANONYMOUS。
MAP_SHARED: 建立共享,用于进程间通信,如果没有这个标志,则别的进程即使能打开文件,也看不到数据。
MAP_PRIVATE: 只有进程自己用的内存区域
MAP_ANONYMOUS: 匿名映射区
fd:用来建立映射区的文件描述符,用 shm_open 打开或者 open 打开的文件。
offset:映射文件的偏移,应该按4096字节(1页)对齐。
返回值: 成功返回映射的内存地址,失败返回-1
-rw-rw-r-- 1 zhaowx9 zhaowx9 0 Mar 29 08:18 myshm
(参考资料)
“-”:该文件是一个普通文件。
“rw-”:文件拥有者的权限可读可写不可执行。
“rw-”:文件所属组的权限可读可写不可执行。
“r–”:其他用户的权限可读不可写不可执行。
“1”:表示这个文件只有 myshm 这一个文件名,即只有一个指向该链接的硬链接。
“zhaowx9”:文件拥有者。
“zhaowx9”:文件拥有者所在的组。
“0”:文件所占用的空间。
“Mar 29 08:18”:文件最近访问(修改)时间。
“myshm”:文件名。
(二)运行
命令行:
$ gcc -o alg.8-4-shmpthreadcon alg.8-4-shmpthreadcon.c -lrt
$ gcc -o alg.8-5-shmproducer.o alg.8-5-shmproducer.c -lrt
$ gcc -o alg.8-6-shmconsumer.o alg.8-6-shmconsumer.c -lrt
$ ./alg.8-4-shmpthreadcon myshm
输出结果:
total 0
-rw-rw-r-- 1 zhaowx9 zhaowx9 0 Mar 29 08:18 myshm
produced message: Hello World!
consumed message: Hello World!
total 0
(三)运行过程:
-
通过
fd = shm_open(argv[1], O_CREAT|O_RDWR, 0666)
根据运行前的输入名(存在 argv[1] 中)创建相应名称的可读可写打开的共享文件,并将其描述符赋给 fd。根据我的输入,该文件命名为myshm
。 -
通过
system("ls -l /dev/shm/")
进行系统调用,得到total 0
说明该目录下所有文件(只有myshm)所占用的空间总和为0,并将 myshm 的信息打印出来。 -
通过
ret = ftruncate(fd, shmsize)
将共享内存的大小改为 shmsize 的大小。 -
进行参数列表的定义
char *argv1[] = {" ", argv[1], 0}
。 -
用两次
vfork()
和execv()
创建两个子进程,同时执行程序alg.8-5-shmproducer
和alg.8-6-shmconsumer
,参数列表都为 argv1。 -
父进程挂起,等待两个子进程结束。
-
子进程1:
alg.8-5-shmproducer
通过
fd = shm_open(argv[1], O_RDWR, 0666)
打开共享内存 myshm(由于该共享内存已经存在,所以直接打开),并将其描述符赋给 fd。通过
shmptr = (char *)mmap(0, shmsize, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0)
内核把文件大小为 shmsize 的 myshm 映射到内存中,并将内存的基地址赋给 shmptr,设置该进程对共享内存的权限为可读可写。通过
sprintf(shmptr,"%s",message_0)
把 “Hello World!” 放到共享内存中。通过
printf("produced message: %s\n", (char *)shmptr)
输出共享内存区的文本。 -
子进程2:
alg.8-6-shmconsumer
通过
fd = shm_open(argv[1], O_RDWR, 0666)
打开共享内存 myshm(由于该共享内存已经存在,所以直接打开),并将其描述符赋给 fd。通过
shmptr = (char *)mmap(0, shmsize, PROT_READ, MAP_SHARED, fd, 0)
内核把文件大小为 shmsize 的 myshm 映射到内存中,并将内存的基地址赋给 shmptr,设置该进程对共享内存的权限为只读。通过
printf("consumed message: %s\n", (char *)shmptr)
输出共享内存区的文本。 -
两个子进程都结束,父进程继续执行,通过
ret = shm_unlink(argv[1])
删除此块共享内存。 -
通过
system("ls -l /dev/shm/")
进行系统调用,发现 myshm 共享文件已被删除。 -
父进程结束。
三. 改写代码 alg.8-0 ~ alg.8-3
(一)修改思路
使共享内存以循环队列的形式进行存储和操作,写进程可以在队列里面存储字符串直到队列满,读进程可以使队列输出直到空。
(二)修改过程
- 将共享内存结构体里的一维字符串改成循环队列型的二维字符串数组。队列的长度为 TEXT_NUM ,每个元素(字符串)的最大长度为 TEXT_SIZE。(每个变量的定义看注释)
-
alg.8-1无需修改。
-
alg.8-2(读进程)和alg.8-3(写进程)都主要在
while(1)
中修改。当shared->written
为0时,读进程挂起,写进程进行写队列操作(要进行队列满判断);当shared->written
为0时,写进程挂起,读进程进行将当前队列 out 指向的字符串输出(要进行队列空判断),当前输出完毕后,要将shared->written
设为0,使用户可以通过写进程来决定入队还是出队。重复进行以上操作,直到输入的shared->written
为-1时,结束通信。
代码如下:
/*alg.8-2*/
while (1) {
while(shared->written == 0){
sleep(1);
}
if(shared->written == 1){
if(shared->q.in == shared->q.out){
printf("%*sQueue is empty! There is no message!\n", 30, " ");
}
else{
printf("%*sMessage: %s\n", 30, " ", shared->q.data[shared->q.out++]);
}
}
else if(shared->written == -1){
break;
}
shared->written = 0;
} /* it is not reliable to use shared->written for process synchronization */
/*alg.8-3*/
while (1) {
printf("Enter flag(0: write; 1: read; -1: end)\n");
// fflush(stdin);
scanf("%d", &shared->written);
// fflush(stdin);
getchar();
if(shared->written == 1){
sleep(1);
}
else if(shared->written == 0){
if((shared->q.in + 1) % TEXT_NUM == shared->q.out){
printf("Queue is full!\n");
}
else{
printf("Enter some text: ");
fgets(shared->q.data[shared->q.in], TEXT_SIZE, stdin);
printf("shared buffer: %s\n",shared->q.data[shared->q.in++]);
}
}
else if(shared->written == -1){
break;
}
else{
printf("Invalid input.\n");
continue;
}
}
(三)运行
命令行:
$ gcc -o alg.8-2-shmread.o alg.8-2-shmread.c
$ gcc -o alg.8-3-shmwrite.o alg.8-3-shmwrite.c
$ gcc -o alg.8-1-shmcon alg.8-1-shmcon.c
$ ./alg.8-1-shmcon myshm
输出结果:
max record number = 5, shm size = 102460
key generated: IPC key = 27057365
shmcon: shmid = 65551
shmcon: shared Memory attached at 0x7fa8ea98f000
------ Shared Memory Segments ------
0x27057365 65551 zhaowx9 666 102460 1
------ Shared Memory Segments ------
0x27057365 65551 zhaowx9 666 102460 0
shmread: IPC key = 27057365
shmread: shmid = 65551
shmread: shared memory attached at 0x7f412e90f000
shmread process ready ...
shmwrite: IPC key = 27057365
shmwrite: shmid = 65551
shmwrite: shared memory attached at 0x7fe5c9bd0000
shmwrite precess ready ...
Enter flag(0: write; 1: read; -1: end)
1
Queue is empty! There is no message!
Enter flag(0: write; 1: read; -1: end)
0
Enter some text: test1
shared buffer: test1
Enter flag(0: write; 1: read; -1: end)
1
Message: test1
Enter flag(0: write; 1: read; -1: end)
0
Enter some text: test2
shared buffer: test2
Enter flag(0: write; 1: read; -1: end)
0
Enter some text: test 3
shared buffer: test 3
Enter flag(0: write; 1: read; -1: end)
0
Enter some text: test 4
shared buffer: test 4
Enter flag(0: write; 1: read; -1: end)
0
Enter some text: test5 5
shared buffer: test5 5
Enter flag(0: write; 1: read; -1: end)
0
Queue is full!
Enter flag(0: write; 1: read; -1: end)
1
Message: test2
Enter flag(0: write; 1: read; -1: end)
1
Message: test 3
Enter flag(0: write; 1: read; -1: end)
1
Message: test 4
Enter flag(0: write; 1: read; -1: end)
1
Message: test5 5
Enter flag(0: write; 1: read; -1: end)
1
Queue is empty! There is no message!
Enter flag(0: write; 1: read; -1: end)
-1
shmcon: shmid = 65551 removed
------ Shared Memory Segments ------
nothing found ...
实验心得
-
对父进程、子进程的理解更加深入。
-
通过对示例程序中不熟悉的知识点进行攻克学习,已经能完全看懂这些代码,并对针对进程间通过共享内存进行通信的编程有了初步的理解。
-
对示例代码的运行过程及系统调用基本理解,对其原理和实现细节也有了较为深刻的认识。
-
通过对代码的改写,更深入地理解了共享内存的原理和实现过程。