共享内存就是OS在物理内存中开辟一段缓存空间,不过与管道、消息队列调用read、write、msgsnd、msgrcv等API来读写所不同的是,使用共享内存通信时,进程是直接使用地址来共享读写的。
当然不管使用那种方式,只要能够共享操作同一段缓存,就都可以实现进程间的通信。
直接使用地址来读写缓存时,效率会更高,但是如果是调用API来读写的话,中间必须经过重重的OS函数调用之后,直到调用到最后一个函数时,该函数才会通过地址去读写共享的缓存,中间的调用过程会降低效率。
对于小数据量的通信来说,使用管道和消息队列这种使用API读写的通信方式很合适,但是如果进程涉及到超大量的数据通信时,必须使用“共享内存”这种直接使用地址操作的通信方式,如果使用API来读写的话,效率会非常的低。
每个进程的虚拟内存只严格对应自己的那片物理内存空间,也就是说虚拟空间的虚拟地址,只和自己的那片物理内存空间的物理地址建立映射关系,和其它进程的物理内存空间没有任何的交集,因此进程空间之间是完全独立的。
共享内存的实现原理很简单,进程空间不是没有交集吗,让他们的空间有交集不就行了吗。
以两个进程使用共享内存来通信为例,实现的方法就是:
(1)调用API,让OS在物理内存上开辟出一大段缓存空间。
(2)让各自进程空间与开辟出的缓存空间建立映射关系
映射就是让虚拟地址和物理内存的实际物理地址建立一对一的对应关系,使用虚拟地址读写缓存时,虚拟地址最终是要转为物理地址的,转换时就必须参考这个映射关系。但是了两个进程空间的虚拟内存可能使用相同的虚拟地址,也可能使用不同的虚拟地址,但是在不同的内存空间,所以不会互相。
建立映射关系后,每个进程都可以通过映射后的虚拟地址来共享操作实现通信了。
A进程使用映射的虚拟地址操作的时候会直接操作共享的内存空间,B进程使用映射的虚拟地址操作的时候也会操作共享的内存空间。那么,通过对同一块共享的物理内存空间进行操作就实现了进程间的通信。
多个进程能映射到同一片空间,然后数据共享。
不过当多个进程映射并共享同一个空间时,在写数据的时候可能会出现相互干扰。
我们知道进程的运行时时间片切换运行的,比如A进程的数据写入的数据比较大,需要多个时间片才能写完,刚写了一部分,结果切换到B进程后,B进程又开始写,A的数据就被中间B的数据干扰到。这时往往需要加保护措施,让每个进程在没有操作时不要被别人干扰,等操作完以后,别的进程才能写数据。例如我们会使用到信号量和文件锁都能够加入保护措施。
(1) 进程调用shmget函数创建新的或获取已有共享内存
(2)进程调用shmat函数,将物理内存映射到自己的进程空间,说白了就是让虚拟地址和真实物理地址建议一一对应的映射关系。
(3) shmdt函数,取消映射
(4) 调用shmctl函数释放开辟的那片物理内存空间和消息队列的msgctl的功能是一样的,
只不过这个是共享内存的。
多个进程使用共享内存通信时,创建者只需要一个,同样的,一般都是谁先运行谁创建,其它后运行的进程发现已经被创建好了,就直接获取共享使用,大家共享操作同一个内存,即可实现通信。
#include
#include
int shmget(key_t key, size_t size, int shmflg);
创建新的,或者获取已有的共享内存。
如果key值没有对应任何共享内存创建一个新的共享内存,创建的过程其实就是os在物理内存上划出(开辟出)一段物理内存空间出来。
如果key值有对应某一个共享内存说明之前有进程调用msgget函数,使用该key去创建了某个共享内存,既然别人之前就创建好了,那就直接获取key所对应的共享内存。
(a)成功:返回共享内存的标识符,用以后续操作
(b)失败:返回-1,并且errno被设置。
int shmget(key_t key, size_t size, int shmflg);
用于生成共享内存的标识符
· IPC_PRIVATE:指定这个后,每次调用shmget时都会创建一个新共享内存。
· 自己指定一个长整型数
· 使用ftok函数,通过路径名和一个8位的整形数来生成key值
指定共享内存的大小,我们一般要求size是虚拟页大小的整数倍
一般来说虚拟页大小是4k(4096字节),如果你指定的大小不是虚拟页的整数倍,也会自动帮你补成整数倍。
与消息队列一样指定原始权限和IPC_CREAT,比如0664|IPC_CREAT。只有在创建一个新的共享内存时才会用到,否者不会用到。
进程结束时,system v ipc不会自动删除,进程结束后,使用ipcs依然能够查看到。
如何删除?
方法1:重启OS,很麻烦,服务器也不是随随便便就让你去重启的。
方法2:进程结束时,调用相应的API来删除
方法3:使用ipcrm命令删除
删除共享内存
+M:按照key值删除
ipcrm -M key
+m:按照标识符删除
ipcrm -m msgid
删除消息队列
+Q:按照key值删除
+q:按照标识符删除
删除信号量
+S:按照key值删除
+s:按照标识符删除
#include
#include
void *shmat(int shmid, const void *shmaddr, int shmflg);
将shmid所指向的共享内存空间映射到进程空间(虚拟内存空间),并返回影射后的起始地址(虚拟地址)。
有了这个地址后,就可以通过这个地址对共享内存进行读写操作。
void *shmat(int shmid, const void *shmaddr, int shmflg);
共享内存标识符。
指定映射的起始地址。
有两种设置方式
· 自己指定映射的起始地址(虚拟地址)。我们一般不会这么做,因为我们自己都搞不清哪些虚拟地址被用了,哪些没被用。
· NULL:表示由内核自己来选择映射的起始地址(虚拟地址)。这是最常见的方式,也是最合理的方式,因为只有内核自己才知道哪些虚拟地址可用,哪些不可用。
指定映射条件。
· 0:以可读可写的方式映射共享内存,也就是说映射后,可以读、也可以写共享内存。
· SHM_RDONLY:以只读方式映射共享内存,也就是说映射后,只能读共享内存,不能写。
· 1: 以只写方式映射共享内存,也就是说映射后,只能写共享内存,不能读。
a)成功:则返回映射地址
b)失败:返回(void *)-1,并且errno被设置。
#include
#include
int shmdt(const void *shmaddr);
取消建立的映射。
调用成功返回0,失败返回-1,且errno被设置。
shmaddr:映射的起始地址(虚拟地址)。
#include
#include
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
根据cmd的要求,对共享内存进行相应控制。
比如:
· 获取共享内存的属性信息
· 修改共享内存的属性信息
· 删除共享内存
· 等等
删除共享内存是最常见的控制。
· shmid:标识符。
· cmd:控制选项
IPC_STAT:从内核获取共享内存属性信息到第三个参数(应用缓存)。
IPC_SET:修改共享内存的属性。
修改方法与消息队列相同。
IPC_RMID:删除共享内存,不过前提是只有当所有的映射取消后,才能删除共享内存。
删除时,用不着第三个参数,所以设置为NULL
· buf
buf的类型为struct shmid_ds。
cmd为IPC_STAT时
buf用于存储原有的共享内存属性,以供查看。
cmd为IPC_SET时
buf中放的是新的属性设置,用于修改共享内存的属性。
struct shmid_ds结构体
struct shmid_ds
{
struct ipc_perm shm_perm; /* Ownership and permissions:权限 */
size_t shm_segsz; /* Size of segment (bytes):共享内存大小 */
time_t shm_atime; /* Last attach time:最后一次映射的时间 */
time_t shm_dtime; /* Last detach time:最后一次取消映射的时间 */
time_t shm_ctime; /* Last change time:最后一次修改属性信息的时间 */
pid_t shm_cpid; /* PID of creator:创建进程的PID */
pid_t shm_lpid; /* PID of last shmat(2)/shmdt(2) :当前正在使用进程的PID*/
shmatt_t shm_nattch; /* No. of current attaches:映射数量,标记有多少个进程空间映射到了共享内存 * 每增加一个映射就+1,每取消一个映射就-1 */
...
};
struct ipc_perm
{
key_t __key; /* Key supplied to shmget(2) */
uid_t uid; /* UID of owner */
gid_t gid; /* GID of owner */
uid_t cuid; /* UID of creator */
gid_t cgid; /* GID of creator */
unsigned short mode; /* Permissions + SHM_DEST andSHM_LOCKED flags */
unsigned short __seq; /* Sequence number */
};
调用成功0,失败则返回-1,并且errno被设置。
使用共享内存实现将A进程数据发送给B进程。
我这里只实现单向的通信,至于双向通信,读者可以自行实现。
向共享内存写入数据的程序:
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define SHM_FILE "./shmfile"
#define SHM_SIZE 4096
int shmid = -1;
void * shmaddr = NULL;
void signal_fun(int shgno) //捕获SIGINT信号取消映射
{
shmdt(shmaddr); //取消映射
//当所有的映射都取消之后才会删除共享内存
//即使我们不手动取消映射,也会在进程结束的时候取消映射
shmctl(shmid,IPC_RMID,NULL); //删除共享内存
//删除内存不会在进程结束之后自动完成
exit(-1);
}
void print_err(char *estr) //错误处理函数
{
perror(estr);
exit(-1);
}
void creat_or_get_shm(void) //创建共享内存
{
int fd = 0;
key_t key = -1;
fd = open(SHM_FILE,O_RDWR|O_CREAT,0664);
if(-1 == fd) print_err("open fail");
key = ftok(SHM_FILE,'b');
if(-1 == key) print_err("ftok fail");
shmid = shmget(key,SHM_SIZE, 0664|IPC_CREAT);
if(-1 == shmid) print_err("shmget fail");
}
char buf[300] = {"abcdefghijklmnopqrstuvwxyz123456789abcdefghijklmnopqrstuvwxyz"};
int main(void)
{
signal(SIGINT,signal_fun);
void * shmaddr = NULL;
creat_or_get_shm(); //创建或者获取共享内存
shmaddr = shmat(shmid,NULL,0);//建立映射
if(shmaddr == (void *)-1)print_err("shmat fail");
while(1)
{
memcpy(shmaddr, buf, sizeof(buf));
sleep(1);
}
return 0;
}
从共享文件读取数据的程序:
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define SHM_FILE "./shmfile"
#define SHM_SIZE 4096
int shmid = -1;
void * shmaddr = NULL;
void signal_fun(int shgno) //捕获SIGINT信号取消映射
{
shmdt(shmaddr); //取消映射
//当所有的映射都取消之后才会删除共享内存
//即使我们不手动取消映射,也会在进程结束的时候取消映射
shmctl(shmid,IPC_RMID,NULL); //删除共享内存
//删除内存不会在进程结束之后自动完成
exit(-1);
}
void print_err(char *estr) //错误处理函数
{
perror(estr);
exit(-1);
}
void creat_or_get_shm(void) //创建共享内存
{
int fd = 0;
key_t key = -1;
fd = open(SHM_FILE,O_RDWR|O_CREAT,0664);
if(-1 == fd) print_err("open fail");
key = ftok(SHM_FILE,'b');
if(-1 == key) print_err("ftok fail");
shmid = shmget(key,SHM_SIZE, 0664|IPC_CREAT);
if(-1 == shmid) print_err("shmget fail");
}
int main(void)
{
signal(SIGINT,signal_fun);
void * shmaddr = NULL;
creat_or_get_shm(); //创建或者获取共享内存
shmaddr = shmat(shmid,NULL,0);//建立映射
if(shmaddr == (void *)-1)print_err("shmat fail");
while(1)
{
if(strlen((char *)shmaddr)!=0)
{
printf("%s\n",(char *)shmaddr); //打印共享内存数据
bzero(shmaddr,SHM_SIZE);
}
}
return 0;
}
如果只是实现使用共享内存进行进程间的通信,那么上面的代码已经完成。如果你开始理解共享内存的机制那么上面代码看明白理解是没有问题的,接下来我们进行一些分析。
我们上面的代码有缺陷的:
缺陷1:strlen函数只能用于判断字符串
如果对方通过共享内存发送不是字符串,而是结构体、整形、浮点型数据, strlen将无法正确判断。
这个缺陷如果C语言没问题,这个缺陷就很容易解决。
缺陷2:没有数据时,cpu会一直循环的判断,这样会让cpu一直做好无意义的事情,非常浪费cpu资源。
所有我们需要对于代码进行改进,我们需要保证写完后再读数据,当共享内存没有数据时,读进程休眠,当写进程把数据写完后,将读进程唤醒。
说白了就是多个进程在操作时,涉及到一个谁先谁后的问题,其实就是同步问题,所谓同步就是保持一个谁先谁后的统一步调。
这就好比我踩一个脚步你跟着踩一个脚步,统一踩脚步的步调,这就是同步,否者我踩我的,你踩你的,各自的步调不一致,这就是异步。
那么通过信号量实现进程同步的方法读者可以在本机ICP信号量博客进行阅读。
接下来我们使用的一种方式是使用信号方式来实现读写共享内存的进程之间同步操作。
方式1:
给共享内存写入数据代码:
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define SHM_FILE "./shmfile"
#define SHM_SIZE 4096
int shmid = -1;
void *shmaddr = NULL;
void print_err(char *estr)
{
perror(estr);
exit(-1);
}
void create_or_get_shm(void) //创建共享内存
{
int fd = 0;
key_t key = -1;
fd = open(SHM_FILE, O_RDWR|O_CREAT, 0664);
if(fd == -1) print_err("open fail");
key = ftok(SHM_FILE, 'b');
if(key == -1) print_err("ftok fail");
shmid = shmget(key, SHM_SIZE, 0664|IPC_CREAT);
if(shmid == -1) print_err("shmget fail");
}
char buf[300] = {"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\
222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222\
ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff2222222222"};
void signal_fun(int signo)
{
shmdt(shmaddr); //取消映射
shmctl(shmid, IPC_RMID, NULL); //删除共享内存
remove("./fifo"); //删除用于创建同名管道的文件
remove(SHM_FILE); //删除用于创建共享内存的文件
exit(-1);
}
int get_peer_PID(void)
{
int ret = -1;
int fifofd = -1;
/* 创建有名管道文件 */
ret = mkfifo("./fifo", 0664);
if(ret == -1 && errno != EEXIST) print_err("mkfifo fail");
/* 以只读方式打开管道 */
fifofd = open("./fifo", O_RDONLY);
if(fifofd == -1) print_err("open fifo fail");
/* 读管道,获取“读共享内存进程”的PID */
int peer_pid;
ret = read(fifofd, &peer_pid, sizeof(peer_pid));
if(ret == -1) print_err("read fifo fail");
return peer_pid;
}
int main(void)
{
int peer_pid = -1;
/* 给SIGINT信号注册捕获函数,用于删除共享内存、管道、文件等 */
signal(SIGINT, signal_fun);
/* 使用有名管道获取读共享内存进程的PID */
peer_pid = get_peer_PID();
/* 创建、或者获取共享内存 */
create_or_get_shm();
//建立映射
shmaddr = shmat(shmid, NULL, 0);
if(shmaddr == (void *)-1) print_err("shmat fail");
while(1) //给共享内存写入数据
{
memcpy(shmaddr, buf, sizeof(buf));
kill(peer_pid, SIGUSR1);
sleep(1);
}
return 0;
}
从共享内存读取数据的代码:
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define SHM_FILE "./shmfile"
#define SHM_SIZE 4096
int shmid = -1;
void *shmaddr = NULL;
void print_err(char *estr)
{
perror(estr);
exit(-1);
}
void create_or_get_shm(void) //创建共享内存
{
int fd = 0;
key_t key = -1;
fd = open(SHM_FILE, O_RDWR|O_CREAT, 0664);
if(fd == -1) print_err("open fail");
key = ftok(SHM_FILE, 'b');
if(key == -1) print_err("ftok fail");
shmid = shmget(key, SHM_SIZE, 0664|IPC_CREAT);
if(shmid == -1) print_err("shmget fail");
}
void signal_fun(int signo) //信号处理函数
{
if(SIGINT == signo)
{
shmdt(shmaddr); //取消映射
shmctl(shmid, IPC_RMID, NULL); //删除共享内存
remove("./fifo"); //删除用于创建有名管道的文件
remove(SHM_FILE); //删除用于创建共享内存的文件
exit(-1);
}
else if(SIGUSR1 == signo)
{
}
}
void snd_self_PID(void)
{
int ret = -1;
int fifofd = -1;
/* 创建有名管道文件 */
mkfifo("./fifo", 0664);
if(ret == -1 && errno != EEXIST) print_err("mkfifo fail");
/* 以只写方式打开文件 */
fifofd = open("./fifo", O_WRONLY);
if(fifofd == -1) print_err("open fifo fail");
/* 获取当前进程的PID, 使用有名管道发送给写共享内存的进程 */
int pid = getpid();
ret = write(fifofd, &pid, sizeof(pid));//发送PID
if(ret == -1) print_err("write fifo fail");
}
int main(void)
{
/*给SIGUSR1注册一个空捕获函数,用于唤醒pause()函数 */
signal(SIGUSR1, signal_fun);
signal(SIGINT, signal_fun);
/* 使用有名管道,讲当前进程的PID发送给写共享内存的进程 */
snd_self_PID();
/* 创建、或者获取共享内存 */
create_or_get_shm();
//建立映射
shmaddr = shmat(shmid, NULL, 0);
if(shmaddr == (void *)-1) print_err("shmat fail");
while(1)//从共享内存读取数据
{
pause();
printf("%s\n", (char *)shmaddr);
bzero(shmaddr, SHM_SIZE);
}
return 0;
}
读者需要理解,与前面代码的运行结果相比较来看结果是一样,但是其实是有本质区别,前面实现的代码,在读取的时候,cpu会一直循环的判断一直做无意义的事情,非常浪费cpu资源。后面的改进只有在写共享内存程序写入数据之后才会给读共享内存进程发送信号进行读取,否则读共享内存会休眠(阻塞)。
那么我们对于代码再进行修改,我们通过自己输入字符串来更加深刻的感受同步过程:
代码演示:
sh_m1.c文件
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define SHM_FILE "./shmfile"
#define SHM_SIZE 4096
int shmid = -1;
void *shmaddr = NULL;
void print_err(char *estr)
{
perror(estr);
exit(-1);
}
void create_or_get_shm(void) //创建共享内存
{
int fd = 0;
key_t key = -1;
fd = open(SHM_FILE, O_RDWR|O_CREAT, 0664);
if(fd == -1) print_err("open fail");
key = ftok(SHM_FILE, 'b');
if(key == -1) print_err("ftok fail");
shmid = shmget(key, SHM_SIZE, 0664|IPC_CREAT);
if(shmid == -1) print_err("shmget fail");
}
void signal_fun(int signo)
{
shmdt(shmaddr); //取消映射
shmctl(shmid, IPC_RMID, NULL); //删除共享内存
remove("./fifo"); //删除用于创建同名管道的文件
remove(SHM_FILE); //删除用于创建共享内存的文件
exit(-1);
}
int get_peer_PID(void)
{
int ret = -1;
int fifofd = -1;
/* 创建有名管道文件 */
ret = mkfifo("./fifo", 0664);
if(ret == -1 && errno != EEXIST) print_err("mkfifo fail");
/* 以只读方式打开管道 */
fifofd = open("./fifo", O_RDONLY);
if(fifofd == -1) print_err("open fifo fail");
/* 读管道,获取“读共享内存进程”的PID */
int peer_pid;
ret = read(fifofd, &peer_pid, sizeof(peer_pid));
if(ret == -1) print_err("read fifo fail");
return peer_pid;
}
int main(void)
{
int peer_pid = -1;
/* 给SIGINT信号注册捕获函数,用于删除共享内存、管道、文件等 */
signal(SIGINT, signal_fun);
/* 使用有名管道获取读共享内存进程的PID */
peer_pid = get_peer_PID();
/* 创建、或者获取共享内存 */
create_or_get_shm();
//建立映射
shmaddr = shmat(shmid, NULL, 0);
if(shmaddr == (void *)-1) print_err("shmat fail");
while(1) //给共享内存写入数据
{
char buf[300] = {0};
printf("input:");
scanf("%s",buf);
memcpy(shmaddr, buf, sizeof(buf));
kill(peer_pid, SIGUSR1);
}
return 0;
}
sh_m2.c文件
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define SHM_FILE "./shmfile"
#define SHM_SIZE 4096
int shmid = -1;
void *shmaddr = NULL;
void print_err(char *estr)
{
perror(estr);
exit(-1);
}
void create_or_get_shm(void) //创建共享内存
{
int fd = 0;
key_t key = -1;
fd = open(SHM_FILE, O_RDWR|O_CREAT, 0664);
if(fd == -1) print_err("open fail");
key = ftok(SHM_FILE, 'b');
if(key == -1) print_err("ftok fail");
shmid = shmget(key, SHM_SIZE, 0664|IPC_CREAT);
if(shmid == -1) print_err("shmget fail");
}
void signal_fun(int signo) //信号处理函数
{
if(SIGINT == signo)
{
shmdt(shmaddr); //取消映射
shmctl(shmid, IPC_RMID, NULL); //删除共享内存
remove("./fifo"); //删除用于创建有名管道的文件
remove(SHM_FILE); //删除用于创建共享内存的文件
exit(-1);
}
else if(SIGUSR1 == signo)
{
}
}
void snd_self_PID(void)
{
int ret = -1;
int fifofd = -1;
/* 创建有名管道文件 */
mkfifo("./fifo", 0664);
if(ret == -1 && errno != EEXIST) print_err("mkfifo fail");
/* 以只写方式打开文件 */
fifofd = open("./fifo", O_WRONLY);
if(fifofd == -1) print_err("open fifo fail");
/* 获取当前进程的PID, 使用有名管道发送给写共享内存的进程 */
int pid = getpid();
ret = write(fifofd, &pid, sizeof(pid));//发送PID
if(ret == -1) print_err("write fifo fail");
}
int main(void)
{
/*给SIGUSR1注册一个空捕获函数,用于唤醒pause()函数 */
signal(SIGUSR1, signal_fun);
signal(SIGINT, signal_fun);
/* 使用有名管道,将当前进程的PID发送给写共享内存的进程 */
snd_self_PID();
/* 创建、或者获取共享内存 */
create_or_get_shm();
//建立映射
shmaddr = shmat(shmid, NULL, 0);
if(shmaddr == (void *)-1) print_err("shmat fail");
while(1)//从共享内存读取数据
{
pause();
printf("%s\n", (char *)shmaddr);
bzero(shmaddr, SHM_SIZE);
}
return 0;
}
通过实现进程同步,我们可以在给共享内存输入数据之后再进行读取,避免cpu会一直循环的判断一直做无意义的事情,浪费cpu资源。后面的改进只有在写共享内存程序写入数据之后才会给读共享内存进程发送信号进行读取,否则读共享内存会休眠(阻塞)。