目录
接博客:进程间通信之管道
一.共享内存实现进程间通信的原理
共享内存实际是操作系统在实际物理内存中开辟的一段内存。
共享内存实现进程间通信,是操作系统在实际物理内存开辟一块空间,一个进程在自己的页表中,将该空间和进程地址空间上的共享区的一块地址空间形成映射关系。另外一进程在页表上,将同一块物理空间和该进程地址空间上的共享区的一块地址空间形成映射关系。
当一个进程往该空间写入内容时,另外一进程访问该空间,会得到写入的值,即实现了进程间的通信。
要实现进程间通信需要两个进程能够看到同一块空间,系统开辟的共享内存就是两进程看到的同一资源。
注意:共享内存实现进程间通信是进程间通信最快的。
关于页表的补充:
进程地址空间里有一个内核区域,它们也会在实际物理内存开辟空间,也会有页表与那块空间形成映射关系,这个页表叫做内核级页表。因为内核只有一个,所以每个进程都相同的。说明进程都共用实际物理内存上的内核空间。
除内核空间以外的空间,与实际物理空间之间的页表,称为用户级页表。每个进程可能不同。
二.管理共享内存的数据结构
共享内存实现进程间通信不只仅限于两个进程之间,可以用于多个进程之间。并且系统中可能会有多个进程在进行多个通信。所以系统需要将这些通信的进程管理起来。如果不管理,操作系统怎么知道这块共享内存挂接了哪个进程等信息。
如何管理?先描述和组织。
查看内核源代码,可以看到系统描述共享内存的数据结构如下:
/* Obsolete, used only for backwards compatibility and libc5 compiles */
struct shmid_ds {
struct ipc_perm shm_perm; /* operation perms */
int shm_segsz; /* size of segment (bytes) *///共享内存空间大小
__kernel_time_t shm_atime; /* last attach time *///挂接时间
__kernel_time_t shm_dtime; /* last detach time *///取消挂接时间
__kernel_time_t shm_ctime; /* last change time *///改变时间
__kernel_ipc_pid_t shm_cpid; /* pid of creator */
__kernel_ipc_pid_t shm_lpid; /* pid of last operator */
unsigned short shm_nattch; /* no. of current attaches *///进程挂接数
unsigned short shm_unused; /* compatibility */
void *shm_unused2; /* ditto - used by DIPC */
void *shm_unused3; /* unused */
};
描述共享内存的数据结构里保存了一个ipc_perm结构体,这个结构体保存了IPC(进程将通信)的关键信息。
/* Obsolete, used only for backwards compatibility and libc5 compiles */
struct ipc_perm
{
__kernel_key_t key;//共享内存的唯一标识符
__kernel_uid_t uid;
__kernel_gid_t gid;
__kernel_uid_t cuid;
__kernel_gid_t cgid;
__kernel_mode_t mode; //权限
unsigned short seq;
};
其中的key是共享内存的唯一标识。
三.共享内存函数
- ftok函数
作用:算出一个唯一的key返回。
参数:第一个是地址,第二个是至少8为的项目id,不能为0,两参数可以是任意值,但是要符合格式。
返回值:ftok如果成功返回一个key值,如果失败返回-1。如果失败了再重新填写参数即可。
ftok中的参数可以随便填写,但是要符合格式,ftok只是利用参数,再运用一套算法,算出一个唯一的key值返回。这个key值可以传给共享内存参数,作为struct ipc_perm中唯一标识共享内存的key。
ftok函数并没有涉及内核层面。
#pragma once
#define PATHNAME "./"
#define PROJ_ID 0x666
#include<stdio.h>
#include<sys/types.h>
#include<sys/ipc.h>
#include"com.h"
int main(){
key_t k = ftok(PATHNAME,PROJ_ID);
if(k==-1){
perror("ftok error");
return 1;
}
printf("ftok : %d\n",k);
return 0;
}
- shmget函数
作用:创建一个共享内存
参数:
key:为共享内存的名字,一般是ftok的返回值。
size:共享内存的大小,以page为单位,大小为4096的整数倍。
shmflg:权限标志,常用两个IPC_CREAT和IPC_EXCL,一般后面还加一个权限,相当于文件的权限。
IPC_CREAT:创建一个共享内存返回,已存在打开返回
IPC_EXCL:配合着IPC_CREAT使用,共享内存已存在出错返回。
使用:IPC_CREAT | IPC_EXCL | 0666
返回值:
成功返回一个非负整数,即共享内存的标识码,失败返回-1。
为什么已经有一个key来标识共享内存,还需要一个返回值来标识共享内存?因为key是内核级别的,供内核标识,shmget返回值是用户级别的,供用户使用的。
#include<stdio.h>
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/shm.h>
#include"com.h"
int main(){
key_t k = ftok(PATHNAME, PROJ_ID);
if (k == -1){
perror("ftok error");
return 1;
}
printf("ftok : %d\n", k);
int shmid = shmget(k, SIZE, IPC_CREAT | IPC_EXCL | 0666);
if (shmid == -1){
perror("shmget error");
return 1;
}
printf("shmid : %d\n", shmid);
return 0;
}
我们发现当进程创建了一个共享内存,没有释放,进程结束后,共享内存还在,所以第二次执行程序,会报错(报错是因为IPC_EXCL)。
这里得出一个结论:IPC(进程将通信)资源生命周期不随进程,而是随内核的,不释放会一直占用,除非重启。所以,shmget创建的共享内存要释放掉,不然会内存泄漏。
可以用命令行来释放共享内存:ipcrm -m shmid(shmget返回值)
也可以用下面的函数来释放共享内存
- shmctl函数
作用:用于控制共享内存
参数: shmid:共享内存的标识
cmd:以什么方式来控制共享内存。IPC_RMID是释放共享内存
buf:指向一个共享内存的数据结构 。struct shmid_ds
返回值:成功返回0,失败返回-1。
1 #include<stdio.h>
2 #include<sys/types.h>
3 #include<sys/ipc.h>
4 #include<sys/shm.h>
5 #include"com.h"
6
7 int main(){
8 key_t k = ftok(PATHNAME,PROJ_ID);//获取一个唯一标识符key
9 if(k==-1){
10 perror("ftok error");
11 return 1;
12 }
13
14 printf("ftok : %d\n",k);
15
16 int shmid = shmget(k,SIZE,IPC_CREAT | IPC_EXCL | 0666);//创建共享内存
17 if(shmid == -1){
18 perror("shmget error");
19 return 1;
20 }
21 printf("shmid : %d\n",shmid);
22
23 int sh = shmctl(shmid,IPC_RMID,NULL);//删除共享内存
24 if(sh == -1){
25 perror("shmctl");
26 return 1;
27 }
28 return 0;
29 }
现在就可以执行多次:
- shmat函数
作用:使创建的共享内存与调用该函数进程的进程地址空间参数关联。
参数:
shmid:共享内存的标识,shmget的返回值。
shmaddr:指定进程地址空间连接的地址。如果设置为null,默认让系统定要关联的地址。
shmflg: 权限,常见有两个SHM_RDONLY(只读)和SHM_REMAP(重新映射一个进程地址空间没这样shmaddr不能为空)。设为0,系统默认。
返回值:
返回映射到进程地址空间共享区的开始地址。
- shmdt函数
作用:删除共享内存与进程地址空间的映射关系,将页表映射关系删除,释放进程地址空间。
参数:
shmaddr:共享内存映射到进程地址空间的地址。shmat返回值。
返回值:
成功返回0,失败返回-1
shmat和shmdt要一起使用才起作用。
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<sys/types.h>
4 #include<sys/ipc.h>
5 #include<sys/shm.h>
6 #include"com.h"
7
8 int main(){
9 key_t k = ftok(PATHNAME,PROJ_ID);
10 if(k==-1){
11 perror("ftok error");
12 return 1;
13 }
14
15 printf("ftok : %d\n",k);
16 int shmid = shmget(k,SIZE,IPC_CREAT | IPC_EXCL | 0666);
17 if(shmid == -1){
18 perror("shmget error");
19 return 1;
20 }
21 printf("shmid : %d\n",shmid);
22 //与进程地址空间产生关联
23 char *str = (char *)shmat(shmid, NULL, 0);
24 //10秒后删除关联
25 sleep(10);
26 shmdt(str);
27
28 //int sh = shmctl(shmid,IPC_RMID,NULL);
29 //if(sh == -1){
31 // return 1;
32 //}
33 return 0;
34 }
程序10秒钟删除共享内存与进程地址空间的关联:
四.实现进程间通信
实现进程间通信步骤:
- 创建共享内存
- 共享内存关联进程
- 删除共享内存与进程的关联
- 释放共享内存
使用到上面的函数。
实现一个用户client与服务器server之间的简单通信。
需要client和server都和共享内存关联。client端不创建共享内存,不释放共享内存,server端创建共享内存,并且释放共享内存。
1 #pragma once
2
3 #define PATHNAME "./"
4 #define PROJ_ID 0x666
5
6 #define SIZE 4096
client代码:写入端
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<sys/types.h>
4 #include<sys/ipc.h>
5 #include<sys/shm.h>
6 #include"com.h"
7
8 int main(){
9 key_t k = ftok(PATHNAME,PROJ_ID);
10 if(k==-1){
11 perror("ftok error");
12 return 1;
13 }
14 //不创建共享内存,只是为了得到shmid
15 int shmid = shmget(k, SIZE, 0);
16 if(shmid == -1){
17 perror("shmget error");
18 return 1;
19 }
20 //与进程地址空间产生关联
21 char *str = (char *)shmat(shmid, NULL, 0);
22 char c='a';
23 for(;c<='z';c++){
24 str[c-'a']=c;
25 sleep(5);
26 }
27 //删除关联
28 shmdt(str);
29 //不用释放共享内存,服务器端释放
30
31 return 0;
32 }
server代码:读出端
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<sys/types.h>
4 #include<sys/ipc.h>
5 #include<sys/shm.h>
6 #include"com.h"
7
8 int main(){
9 key_t k = ftok(PATHNAME,PROJ_ID);
10 if(k==-1){
11 perror("ftok error");
12 return 1;
13 }
14 //服务器关不要加IPC_EXCL,已存在不要保存,直接返回
15 int shmid = shmget(k,SIZE,IPC_CREAT | IPC_EXCL | 0666);
16 if(shmid == -1){
17 perror("shmget error");
18 return 1;
19 }
20 //与进程地址空间产生关联
21 char *str = (char *)shmat(shmid, NULL, 0);
22 //读数据
23 while(1){
24 printf("%s\n",str);
25 sleep(1);
26 }
27 //删除关联
28 shmdt(str);
29 int sh = shmctl(shmid,IPC_RMID,NULL);
30 if(sh == -1){
31 perror("shmctl");
32 return 1;
33 }
34 return 0;
35 }
代码输出,每隔5秒写端往共享内存中写一个字符,读端每个一秒读一下数据。
这里有一个问题:因为写端比读端慢,相比较于管道,当写端比读端慢时,没有写入时,读端要进入阻塞状态,但是上面发现使用共享内存的读端一直在读,并没有阻塞。
这里得出一个结论:共享内存实现的进程间通信底层不提供任何同步与互斥机制。如果想让两进程很好的合作起来,在IPC里要有信号量来支撑。
注意:两进程之间用通过同样的规则即ftok参数要一样,来获取同样的key值,然后在创建共享内存时,用这个key值唯一标识共享内存,所以两进程时通过同样的key值来实现看到同一份资源。