Linux进程间通信

进程间通信的目的

  • 数据传输:一个进程需要将它的数据发送给另一个进程
  • 资源共享:多个进程之间共享同样的资源。
  • 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
  • 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。

本文中主要介绍以下三种通信方式:

  • 匿名管道
  • 命名管道
  • 共享内存

匿名管道

我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”。匿名管道用于具有血缘关系的进程间通信(兄弟,父子…)本文中用父子,原因在下文版本4中介绍。

通信前提:不同进程看到同一份资源
在这里插入图片描述

匿名管道通信步骤:

  1. 父进程创建管道。(匿名管道,适合具有血缘关系的进程进行通信)
  2. 父进程fork出子进程,本质是为了让父子进程看到同一份资源。
  3. 确认谁读谁写,父进程关闭一个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

结果演示:
在这里插入图片描述
共享内存特点:

  • 共享内存没有提供同步与互斥(不安全)
  • 共享内存是所有通信方式中速度最快的(减少拷贝)
  • 生命周期随内核(查看共享内存部分介绍)
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值