进程间通信的目的
- 数据传输:一个进程需要将它的数据发送给另一个进程
- 资源共享:多个进程之间共享同样的资源。
- 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
- 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。
本文中主要介绍以下三种通信方式:
- 匿名管道
- 命名管道
- 共享内存
匿名管道
我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”。匿名管道用于具有血缘关系的进程间通信(兄弟,父子…)本文中用父子,原因在下文版本4中介绍。
通信前提:不同进程看到同一份资源
匿名管道通信步骤:
- 父进程创建管道。(匿名管道,适合具有血缘关系的进程进行通信)
- 父进程fork出子进程,本质是为了让父子进程看到同一份资源。
- 确认谁读谁写,父进程关闭一个fd[(0或者1)],子进程关闭一个fd[(1或者0)]。fd[0]保存读文件描述符,fd[1]保存写文件描述符。
注意:本文中采取父子间通信,子进程负责写,父进程负责读
函数原型:
#include <unistd.h>
int pipe(int pipefd[2]);
函数返回值:
成功返回0,失败返回-1
为了了解管道特性,下文将用大量篇幅介绍匿名管道通信的四种情况,下面分别来介绍:
版本1:
子进程每隔一秒向管道写入数据,父进程循环等待数据读取(写端不写或写的慢,读端要等待)
代码:
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
int pipefd[2] = {0};
//pipefd[0]读,pipefd[1]写
if (pipe(pipefd) != 0){
perror("pipe errer...");
return 1;
}
printf("pipefd[0] = %d\n",pipefd[0]);
printf("pipefd[1] = %d\n",pipefd[1]);
//创建子进程
pid_t id = fork();
if (id == 0){
//child
close(pipefd[0]); //子进程向管道写,关闭读端
const char* msg = " hello father! I am child...";
while (1){
write(pipefd[1], msg, strlen(msg));
sleep(1);
}
close(pipefd[1]);
exit(0);
}
//parent
close(pipefd[1]); //父进程从管道读,关闭写端
while (1){
char buff[64] = {0};
ssize_t s = read(pipefd[0], buff, sizeof(buff)-1);
//-1是因为后面设置\0,避免后面\0越界
if (s > 0){
buff[s] = '\0'; //手动在字符串添加\0
printf("child send to father:%s\n", buff);
}else if (s == 0){
printf("read file end...\n");
break;
}else{
printf("read error...\n");
break;
}
}
close(pipefd[0]);
return 0;
}
运行结果:
通过结果来看,我们发现pipefd[0]是3,因为0,1,2已经被占用(详见文件描述符),子进程作为写端每隔1秒向管道写数据(在这里没有动图,大家可以在自己的环境下运行上面代码测试),父进程作为读端要等待数据来再读出。
版本2:
子进程循环向管道写入数据,父进程每隔1秒读数据(读端不读或读的慢,写端要等读端)
代码:
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
int pipefd[2] = {0};
//pipefd[0],pipefd[1]写
if (pipe(pipefd) != 0){
perror("pipe errer...");
return 1;
}
printf("pipefd[0] = %d\n",pipefd[0]);
printf("pipefd[1] = %d\n",pipefd[1]);
//创建子进程
pid_t id = fork();
if (id == 0){
//child
close(pipefd[0]); //子进程向管道写,关闭读端
const char* msg = " hello father! I am child...";
while (1){
write(pipefd[1], msg, strlen(msg));
}
close(pipefd[1]);
exit(0);
}
//parent
close(pipefd[1]); //父进程从管道读,关闭写端
while (1){
sleep(1);
char buff[64] = {0};
ssize_t s = read(pipefd[0], buff, sizeof(buff)-1);
//-1是因为后面设置\0,避免后面\0越界
if (s > 0){
buff[s] = '\0'; //手动在字符串添加\0
printf("child send to father:%s\n", buff);
}else if (s == 0){
printf("read file end...\n");
break;
}else{
printf("read error...\n");
break;
}
}
close(pipefd[0]);
return 0;
}
运行结果:
只要管道中有空间,子进程就写数据,但是父进程每隔1秒才读数据,所以子进程必须得等父进程读完数据后才能写。而且可以看出,send to father:后面的开头每一行不一样,这是什么原因呢?其实管道提供的是面向字节流服务,父进程不管读出来的数据是什么样,他只以字节为单位读取。
小插曲:
子进程循环向管道写入数据,父进程不读
代码演示:
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
int pipefd[2] = {0};
//pipefd[0],pipefd[1]写
if (pipe(pipefd) != 0){
perror("pipe errer...");
return 1;
}
printf("pipefd[0] = %d\n",pipefd[0]);
printf("pipefd[1] = %d\n",pipefd[1]);
//创建子进程
pid_t id = fork();
if (id == 0){
//child
close(pipefd[0]); //子进程向管道写,关闭读端
char c = 'p';
int count = 0;
while (1){
write(pipefd[1], &c, 1);
count++;
printf("count = %d\n",count);
}
close(pipefd[1]);
exit(0);
}
//parent
close(pipefd[1]); //父进程从管道读,关闭写端
while (1){
sleep(1);
}
close(pipefd[0]);
return 0;
}
运行结果:
子进程循环写入1个字符(1个字节),通过count++发现到达65536就不再加了,可以认为向管道写入65536字节就不再写了(65536字节 = 64KB)。为什么到65536就不在增加了呢?原因是本次测试下管道的大小是64KB,管道满了要等读端来读数据,也就是说写端不写的本质是等读端来读。那么是不是父进程一读,子进程立刻写呢?写个代码测试一下。
代码:
子进程写入,父进程等待10秒后,读出1个字符
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
int pipefd[2] = {0};
//pipefd[0],pipefd[1]写
if (pipe(pipefd) != 0){
perror("pipe errer...");
return 1;
}
printf("pipefd[0] = %d\n",pipefd[0]);
printf("pipefd[1] = %d\n",pipefd[1]);
//创建子进程
pid_t id = fork();
if (id == 0){
//child
close(pipefd[0]); //子进程向管道写,关闭读端
char c = 'p';
int count = 0;
while (1){
write(pipefd[1], &c, 1);
count++;
printf("count = %d\n",count);
}
close(pipefd[1]);
exit(0);
}
//parent
close(pipefd[1]); //父进程从管道读,关闭写端
while (1){
sleep(10);
char c = '0';
read(pipefd[0],&c,1);
printf("father read: %c\n",c);
}
close(pipefd[0]);
return 0;
}
结果演示:
可以看到,父进程读出1个字节大小的数据,子进程并没有立刻向管道写。
我们此时改变父进程读取大小
代码如下:
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
int pipefd[2] = {0};
//pipefd[0],pipefd[1]写
if (pipe(pipefd) != 0){
perror("pipe errer...");
return 1;
}
printf("pipefd[0] = %d\n",pipefd[0]);
printf("pipefd[1] = %d\n",pipefd[1]);
//创建子进程
pid_t id = fork();
if (id == 0){
//child
close(pipefd[0]); //子进程向管道写,关闭读端
char c = 'p';
int count = 0;
while (1){
write(pipefd[1], &c, 1);
count++;
printf("count = %d\n",count);
}
close(pipefd[1]);
exit(0);
}
//parent
close(pipefd[1]); //父进程从管道读,关闭写端
while (1){
sleep(10);
char c[1024*4+1] = {0};
read(pipefd[0],c,sizeof(c));
int cnt = 1;
printf("father read %d 4KB: %c\n",cnt++,c);
}
close(pipefd[0]);
return 0;
}
运行结果:
此处没有动图,没法演示,大家可以利用上面代码在自己的环境下测试,大概过程如下:子进程开始写满管道,父进程等待10秒后读取1024*4+1个字节数据后子进程立刻再次写入数据。为什么是这么多字节后子进程立刻就写呢?
通过查看man手册:
大概意思就是:当要写入的数据量不大于PIPE_BUF时,Linux保证写入的原子性;当要写入的数据量大于PIPE_BUF时,Linux不再保证写入的原子性,而Linux下PIPE_BUF的大小为4096字节。
版本3:
子进程写入数据后等待5秒钟退出,父进程读完管道数据后也退出(写端关闭,读端读完管道数据后再读会读到0,读到文件末尾)
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
int pipefd[2] = {0};
//pipefd[0],pipefd[1]写
if (pipe(pipefd) != 0){
perror("pipe errer...");
return 1;
}
//创建子进程
pid_t id = fork();
if (id == 0){
//child
close(pipefd[0]); //子进程向管道写,关闭读端
const char *msg = "hello father! ...";
while (1){
write(pipefd[1], msg, strlen(msg));
sleep(5);
break;
}
close(pipefd[1]);
exit(0);
}
//parent
close(pipefd[1]); //父进程从管道读,关闭写端
while (1){
char buff[64] = {0};
ssize_t s = read(pipefd[0], buff, sizeof(buff)-1);
//-1是因为后面设置\0,避免后面\0越界
if (s > 0){
buff[s] = '\0'; //手动在字符串添加\0
printf("child send to father:%s\n", buff);
}else if (s == 0){
printf("read file end...\n");
break;
}else{
printf("read error...\n");
break;
}
}
close(pipefd[0]);
return 0;
}
结果演示:
父进程读完数据后走s=0条件判断之后退出。
版本4:
父进程读取1条数据后关闭,子进程也退出(读端关闭,写端收到信号后终止)
代码:
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
int pipefd[2] = {0};
//pipefd[0],pipefd[1]写
if (pipe(pipefd) != 0){
perror("pipe errer...");
return 1;
}
//创建子进程
pid_t id = fork();
if (id == 0){
//child
close(pipefd[0]); //子进程向管道写,关闭读端
const char *msg = "hello father! ...";
while (1){
write(pipefd[1], msg, strlen(msg));
}
close(pipefd[1]);
exit(0);
}
//parent
close(pipefd[1]); //父进程从管道读,关闭写端
while (1){
sleep(5);
char buff[64] = {0};
ssize_t s = read(pipefd[0], buff, sizeof(buff)-1);
//-1是因为后面设置\0,避免后面\0越界
if (s > 0){
buff[s] = '\0'; //手动在字符串添加\0
printf("child send to father:%s\n", buff);
}else if (s == 0){
printf("read file end...\n");
break;
}else{
printf("read error...\n");
break;
}
//读端读1条消息后退出
break;
}
close(pipefd[0]);
return 0;
}
结果演示:
可以看到父进程退出后,子进程也退出了,事实上子进程是收到操作系统的信号SIGPIPE而终止。怎么验证呢?在本文中我们采用父子通信,那么想要获取子进程退出信号,只需要父进程等待子进程即可。
代码:
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
int pipefd[2] = {0};
//pipefd[0],pipefd[1]写
if (pipe(pipefd) != 0){
perror("pipe errer...");
return 1;
}
//创建子进程
pid_t id = fork();
if (id == 0){
//child
close(pipefd[0]); //子进程向管道写,关闭读端
const char *msg = "hello father! ...";
while (1){
write(pipefd[1], msg, strlen(msg));
}
close(pipefd[1]);
exit(0);
}
//parent
close(pipefd[1]); //父进程从管道读,关闭写端
while (1){
sleep(5);
char buff[64] = {0};
ssize_t s = read(pipefd[0], buff, sizeof(buff)-1);
//-1是因为后面设置\0,避免后面\0越界
if (s > 0){
buff[s] = '\0'; //手动在字符串添加\0
printf("child send to father:%s\n", buff);
}else if (s == 0){
printf("read file end...\n");
break;
}else{
printf("read error...\n");
break;
}
//读端读1条消息后退出
break;
}
close(pipefd[0]);
int status = 0;
pid_t pid = waitpid(-1,&status,0);
if(pid > 0){
//wait成功
if((status & 0x7f) == 0){
//正常退出
printf("child[%d] exit code: %d\n",pid,(status>>8 & 0xff));
}else{
//异常退出
printf("子进程异常退出,退出信号为:%d \n",status & 0x7f);
}
}
return 0;
}
运行结果:
关于waitpid的使用可参见前面的文章进程等待
至此匿名管道的四种情况已经全部介绍。
匿名管道的5个特点:
- 只能用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信;通常,一个管道由一个进程创建,然后该进程调用fork,此后父、子进程之间就可应用该管道。
- 管道提供流式服务
- 一般而言,进程退出,管道释放,所以管道的生命周期随进程
- 一般而言,内核会对管道操作进行同步与互斥
- 管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道
命名管道
匿名管道应用的一个限制就是只能在具有亲缘关系的进程间通信。如果我们想在不相关的进程之间交换数据,可以使用FIFO文件来做这项工作,也就是命名管道。
前文说过,通信的前提是不同进程看到同一份资源。Linux下采用路径+文件名标识唯一的文件,只要让两个进程找到这个唯一文件,就可以读写,实现不具有血缘关系的进程间通信。接下来就介绍命名管道的相关知识。
函数原型:
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
函数参数:
pathname:管道文件名
mode:管道文件权限(这里注意自己环境下的umask值会影响mode)
函数返回值:
创建成功返回0,失败返回-1
在接下来演示中使用server和client进行通信,server启动创建命名管道文件等待client写,自己则等待读取。
server.c代码:
#include "common.h"
#include <string.h>
int main()
{
if (mkfifo(MY_FIFO, 0664) < 0){ //使用mkfifo创建命名管道文件
perror("mkfifo error...");
return 1;
}
int fd = open(MY_FIFO,O_RDONLY);
if(fd < 0){
perror("open error...");
return 2;
}
//打开成功
while(1){
char buff[64] = {0};
ssize_t s = read(fd,buff,sizeof(buff)-1);
//-1是为了在最后增加'\0'
if(s > 0){
//读取成功
buff[s] = '\0';
printf("client# %s\n",buff);
}else if(s == 0){
//写端关闭
printf("client quit...\n");
break;
}else{
//读取出错
perror("read error...");
break;
}
}
close(fd);//关闭文件描述符
return 0;
}
client.c代码:
#include "common.h"
#include <string.h> // for strlen
int main()
{
int fd = open(MY_FIFO,O_WRONLY);
if(fd < 0){
perror("open error...");
return 1;
}
//打开成功
while(1){
printf("请输入# ");
fflush(stdout);//刷新缓冲区数据
char buff[64] = {0};
//把数据从标准输入读取到buff中
ssize_t s = read(0,buff,sizeof(buff)-1);
if(s > 0){//只关心读取成功场景
buff[s-1] = '\0';
//这里-1是因为,我们在输入完后会回车,read会读取这个回车
//在这里我们不需要所以不读这个字符'\n'
//写入到文件中
write(fd,buff,strlen(buff));
}
}
close(fd);
return 0;
}
common.h代码:
#pragma once
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#define MY_FIFO "./fifo"
Makefile:
.PHONY:all
all:client server
client:client.c
gcc $^ -o $@
server:server.c
gcc $^ -o $@
.PHONY:clean
clean:
rm -f fifo client server
结果演示:
注意,因为我们在server.c中创建的命名管道,所以我们必须先启动server
client端从标准输入读取数据后写入到文件,server从文件中读取。大家可以在自己环境下测试代码。
最后:
命名管道中的数据不存储在磁盘中,而是在内存中。(关于验证只需要让server读的时候等待30秒,client不断写,在期间观察pipe文件大小即可,在此就不占用篇幅验证了)
命名管道的特点和匿名管道基本相同,区别在于命名管道可以实现无关系进程间通信。
共享内存
操作系统在内存中开辟一段空间,让参与通信的多个进程挂接到这段新开辟的空间上去。不同的进程找到了同一份资源,也就能实现进程间通信。
进程间通信草图:
那么怎么保证不同的进程找到的是同一份资源呢?
在申请完共享内存后会获取一个key值,这个key值用来保证唯一性。
ftok函数的功能就是返回一个key值
#include <sys/types.h>
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);
pathname:路径名
proj_id:自定义标识id
返回值:成功返回key值,失败返回-1
主要步骤:shmget创建共享内存 —》 shmat挂接共享内存 —》 shmdt 取消挂接 —》shmctl删除共享内存
函数原型:
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg); 创建共享内存
key:这个共享内存段名字
size:共享内存大小(建议4KB的整数倍)
shmflg:共享内存创建方式(IPC_CREAT|IPC_EXCL)此外新建时还需要加上文件权限mode
返回值:成功返回一个非负整数,即该共享内存段的标识码;失败返回-1
#include <sys/types.h>
#include <sys/shm.h>
void *shmat(int shmid, const void *shmaddr, int shmflg); 将共享内存段连接到进程地址空间
shmid: 共享内存标识
shmaddr:指定连接的地址,shmaddr为NULL,自动选择一个地址
shmflg:关联时设置操作属性(本文中简单应用设置为0,如果想要了解可查看man手册)
返回值:成功返回共享内存的起始地址;失败返回-1
#include <sys/types.h>
#include <sys/shm.h>
int shmdt(const void *shmaddr); 将共享内存段与当前进程脱离
shmaddr: 由shmat所返回的指针
返回值:成功返回0;失败返回-1
注意:将共享内存段与当前进程脱离不等于删除共享内存段,而是将当前进程与共享内存取消联系
#include <sys/ipc.h>
#include <sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds *buf);用于控制共享内存(本文只了解删除)
shmid:由shmget返回的共享内存标识码
cmd:将要采取的动作(有三个可取值)
buf:指向一个保存着共享内存的模式状态和访问权限的数据结构
返回值:成功返回0;失败返回-1
注意:
在shmget中如果单独使用IPC_CREAT创建共享内存,如果新创建的共享内存已经存在,则直接返回当前已经存在的共享内存;
IPC_EXCL(单独使用无意义)与IPC_CREAT联合使用:如果不存在共享内存则创建,如果存在则返回出错。(保证调用成功后一定会得到一个最新的没被使用过的共享内存)
查看共享内存
简单示例:
server.c
#include "common.h" //包含上述接口头文件
int main()
{
key_t key = ftok(PATH_NAME,PROJ_ID);
if(key < 0){
//ftok 失败
perror("ftok error...");
return 1;
}
//printf("%u \n" ,key);
int shmid = shmget(key,SIZE,IPC_CREAT|IPC_EXCL);
//IPC_CREAT|IPC_EXCL联合使用保证成功创建的shmid一定是最新的
if(shmid < 0){
//shmget 失败
perror("shmget error...");
return 2;
}
printf("key : %u shmid : %d \n",key,shmid);
return 0;
}
common.h
#pragma once
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/types.h>
#include <sys/shm.h>
#define PATH_NAME "./"
#define PROJ_ID 0x1234
#define SIZE 4096
运行:
在这里多次运行了server进程,我们可以发现第一次运行后会打印shmid 和 key的值,但是第二次运行的时候就出错了,什么原因呢?
我们利用ipcs -m命令查看此时的共享内存情况:
shmid = 8的这一段共享内存还在,所以得出结论,共享内存生命周期随内核,只能通过自己释放(ipcrm 或者 shmctl),或者操作系统重启。
这里的perms表示权限(在shmget中设置,上面的代码中我们还没设置),bytes表示共享内存大小,owner表示共享内存拥有者,nattch表示连接这段共享内存的进程数,status表示共享内存状态。
删除共享内存
使用命令ipcrm -m shmid 来删除自己创建的共享内存:
使用shmctl删除共享内存:
代码:
server.c(其余代码与上面相同)
#include "common.h"
#include <unistd.h>
int main()
{
key_t key = ftok(PATH_NAME,PROJ_ID);
if(key < 0){
//ftok 失败
perror("ftok error...");
return 1;
}
//printf("%u \n" ,key);
int shmid = shmget(key,SIZE,IPC_CREAT|IPC_EXCL);
//IPC_CREAT|IPC_EXCL联合使用保证成功创建的shmid一定是最新的
if(shmid < 0){
//shmget 失败
perror("shmget error...");
return 2;
}
printf("key : %u shmid : %d \n",key,shmid);
sleep(5);
if(shmctl(shmid,IPC_RMID,NULL) == 0){
//shmctl 成功
printf("shmid = %d delete success! ...\n",shmid);
}
sleep(5);
return 0;
}
运行结果:
总结: 用户可以利用命令ipcrm -m 和 shmctl删除共享内存。
关联共享内存
使用shmat关联和shmdt去关联
代码示例:
server.c其余代码与上面相同
#include "common.h"
#include <unistd.h> //for sleep
int main()
{
key_t key = ftok(PATH_NAME,PROJ_ID);
if(key < 0){
//ftok 失败
perror("ftok error...");
return 1;
}
//printf("%u \n" ,key);
int shmid = shmget(key,SIZE,IPC_CREAT|IPC_EXCL|0664);
//IPC_CREAT|IPC_EXCL联合使用保证成功创建的shmid一定是最新的
if(shmid < 0){
//shmget 失败
perror("shmget error...");
return 2;
}
printf("key : %u shmid : %d \n",key,shmid);
sleep(2);
char* mem = (char*)shmat(shmid,NULL,0);//挂接共享内存
printf("server shmat success! ...\n");
sleep(10);
//取消挂接
if(shmdt(mem) == -1){
//返回值 == -1 表示取消错误
perror("server shmdt error ...\n");
return 3;
}
sleep(2);
if(shmctl(shmid,IPC_RMID,NULL) == 0){
//shmctl 成功
printf("shmid = %d delete success! ...\n",shmid);
}
sleep(5);
return 0;
}
结果:
server连接10秒后取消关联之后退出
加上client端:
client.c
#include "common.h"
#include <unistd.h> //for sleep
int main()
{
key_t key = ftok(PATH_NAME,PROJ_ID);
if(key < 0){
//ftok 失败
perror("ftok error...");
return 1;
}
int shmid = shmget(key,SIZE,IPC_CREAT);
if(shmid < 0){
//shmget 失败
perror("shmget error...");
return 1;
}
//挂接
sleep(4);
char *mem = (char*)shmat(shmid,NULL,0);
printf("client shmat success! ...\n");
sleep(5);
if(shmdt(mem) == -1){
//取消挂接出错
perror("shmdt error...");
return 2;
}
sleep(5);
printf("client shmdt success! ...\n");
return 0;
}
server.c
#include "common.h"
#include <unistd.h> //for sleep
int main()
{
key_t key = ftok(PATH_NAME,PROJ_ID);
if(key < 0){
//ftok 失败
perror("ftok error...");
return 1;
}
//printf("%u \n" ,key);
int shmid = shmget(key,SIZE,IPC_CREAT|IPC_EXCL|0664);
//IPC_CREAT|IPC_EXCL联合使用保证成功创建的shmid一定是最新的
if(shmid < 0){
//shmget 失败
perror("shmget error...");
return 2;
}
printf("key : %u shmid : %d \n",key,shmid);
sleep(2);
char* mem = (char*)shmat(shmid,NULL,0);//挂接共享内存
printf("server shmat success! ...\n");
sleep(10);
//取消挂接
if(shmdt(mem) == -1){
//返回值 == -1 表示取消错误
perror("server shmdt error ...\n");
return 3;
}
sleep(2);
if(shmctl(shmid,IPC_RMID,NULL) == 0){
//shmctl 成功
printf("shmid = %d delete success! ...\n",shmid);
}
sleep(5);
return 0;
}
运行结果:server先连接client后连接
client先取消连接server取消连接,server删除共享内存
通信示例
client向server每秒发送一个字符
client.c
#include "common.h"
#include <unistd.h> //for sleep
#include <string.h> //for strlen
int main()
{
key_t key = ftok(PATH_NAME,PROJ_ID);
if(key < 0){
//ftok 失败
perror("ftok error...");
return 1;
}
int shmid = shmget(key,SIZE,IPC_CREAT);
if(shmid < 0){
//shmget 失败
perror("shmget error...");
return 1;
}
//挂接
//sleep(4);
char *mem = (char*)shmat(shmid,NULL,0);
printf("client shmat success! ...\n");
//sleep(5);
//业务逻辑start
char *str = "Linux is fun ... ";
int c = 0;
while(c < strlen(str))
{
mem[c] = str[c];
c++;
mem[c] = '\0';
sleep(1);
}
//业务逻辑end
if(shmdt(mem) == -1){
//取消挂接出错
perror("shmdt error...");
return 2;
}
//sleep(5);
printf("client shmdt success! ...\n");
//client端不需要删除共享内存
return 0;
}
server.c
#include "common.h"
#include <unistd.h> //for sleep
int main()
{
key_t key = ftok(PATH_NAME,PROJ_ID);
if(key < 0){
//ftok 失败
perror("ftok error...");
return 1;
}
//printf("%u \n" ,key);
int shmid = shmget(key,SIZE,IPC_CREAT|IPC_EXCL|0664);
//IPC_CREAT|IPC_EXCL联合使用保证成功创建的shmid一定是最新的
if(shmid < 0){
//shmget 失败
perror("shmget error...");
return 2;
}
printf("key : %u shmid : %d \n",key,shmid);
//sleep(2);
char* mem = (char*)shmat(shmid,NULL,0);//挂接共享内存
printf("server shmat success! ...\n");
//sleep(10);
//业务逻辑start
while(1)
{
printf("%s \n",mem);
sleep(1);
}
//业务逻辑end
//取消挂接
if(shmdt(mem) == -1){
//返回值 == -1 表示取消错误
perror("server shmdt error ...\n");
return 3;
}
//sleep(2);
if(shmctl(shmid,IPC_RMID,NULL) == 0){
//shmctl 成功
printf("shmid = %d delete success! ...\n",shmid);
}
//sleep(5);
return 0;
}
common.h
#pragma once
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/types.h>
#include <sys/shm.h>
#define PATH_NAME "./"
#define PROJ_ID 0x1234
#define SIZE 4096
Makefile:
.PHONY:all
all:server client
server:server.c
gcc $^ -o $@
client:client.c
gcc $^ -o $@
.PHONY:clean
clean:
rm -f server client
结果演示:
共享内存特点:
- 共享内存没有提供同步与互斥(不安全)
- 共享内存是所有通信方式中速度最快的(减少拷贝)
- 生命周期随内核(查看共享内存部分介绍)