> 文档中心 > 【Linux篇】第十一篇——进程间通信(管道+system V共享内存)

【Linux篇】第十一篇——进程间通信(管道+system V共享内存)


⭐️ 本篇博客要给大家介绍一些关于进程间通信的一下知识。Linux下进程通信常见的几种方式,例如管道、共享内存等。

目录

  • 🌏介绍
  • 🌏管道
    • 🌲认识管道
    • 🌲匿名管道
      • 🍯创建匿名管道——pipe
      • 🍯管道的本质
      • 🍯使用匿名管道进行通信
      • 🍯匿名管道的读写规则
      • 🍯管道的特点
    • 🌲命名管道
      • 创建命名管道FIFO--mkfifo
      • 🍯匿名管道读写规则
      • 🍯使用匿名管道
    • 🌲命名管道和匿名管道的区别
  • 🌏system V共享内存
    • 🌲认识共享内存
    • 🌲共享内存函数
    • 🌲使用共享内存实现进程通信
  • 🌐总结

🌏介绍

概念: 进程间通信(IPC,Interprocess communication)是一组编程接口,让程序员能够协调不同的进程,使之能在一个操作系统里同时运行,并相互传递、交换信息。这使得一个程序能够在同一时间里处理许多用户的要求。IPC方法包括管道(PIPE)、消息排队、旗语、共用内存以及套接字(socket)(本篇博客只介绍共享内存和管道两种)。
通信目的:

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

如何实现通信?
要让两个不同的进程实现通信,前提条件是让它们看到同一份资源。所以要想办法让他们看到同一份资源,就需要采取一些手段,可以分为下面几种
通信方式分类:

  1. 管道
  • 匿名管道pipe
  • 命名管道
  1. System V IPC
  • System V 消息队列
  • System V 共享内存
  • System V 信号量
  1. POSIX IPC
  • 消息队列
  • 共享内存
  • 信号量
  • 互斥量
  • 条件变量
  • 读写锁

🌏管道

🌲认识管道

概念: 管道是Unix中最古老的进程间通信的形式。我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”。它的特点是单向传输数据的,先进先出。
管道相信大家之前都知道一些。我们之前也会用到管道命令‘|’。例如:cat file.txt | head -1。
cat是一个进程,这个进程先处理,然后将处理后得到的标准输出到管道中,再由head进程通过标准输入将管道中的数据读出,再进行处理。
在这里插入图片描述

🌲匿名管道

概念: 匿名管道用于进程之间通信,这两个进程需要具有亲缘关系(父子进程等)。

🍯创建匿名管道——pipe

这里介绍一个系统调用接口——pipe。

功能: 创建一个匿名管道
函数原型:

#include int pipe(int pipefd[2])

参数:
fd:文件描述符数组,这是一个输出型参数,fd[0]表示读端,fd[1]表示写端
返回值:
创建管道成功返回0,失败返回-1

匿名管道创建原理:
调用pipe函数后,OS会在fd_array数组中分配两个文件描述符给管道,一个是读,一个是写,并把这两个文件描述符放到用户传进来的数组中,fd[0]代表管道读端,fd[1]代表管道写端。这样一个管道就创建好了。
在这里插入图片描述
实例演示:
实例1: 观察fd[0]和fd[1]的值

#include #include int main(){int pipefd[2];int ret = pipe(pipefd);if (ret == -1){  // 管道创建失败  perror("make piep");  exit(-1);}// 成功返回0// pipefd[0] 代表读端// pipefd[1] 代表写端printf("fd[0]:%d, fd[1]:%d\n", pipefd[0], pipefd[1]);return0;}

代码运行结果: 显然,pipefd这个数组里面放的是两个文件描述符
【Linux篇】第十一篇——进程间通信(管道+system V共享内存)

实例2: 尝试使用管道读写数据

#include #include #include #include #include int main(){  int pipefd[2];  int ret = pipe(pipefd);  if (ret == -1){    // 管道创建失败    perror("make piep");    exit(-1);  }  char buf[64] = "hello world";  // 写数据  write(pipefd[1], buf, sizeof(buf)/sizeof(buf[0]));  // 读数据  buf[0] = 0;// 清空buf  ssize_t s = read(pipefd[0], buf, 11);  buf[s] = '\0';  printf("%s\n", buf);  return 0;}

代码运行结果如下: 可以看出,管道也可以读写数据,和文件使用方法是一致的
【Linux篇】第十一篇——进程间通信(管道+system V共享内存)

上面介绍的都是关于管道如何创建,接下来就要介绍如何使用管道进行通信。

🍯管道的本质

Linux下一切皆文件,看待管道,其实时可以像看待文件一样。且管道和文件使用方法是一致的。管道的生命周期随进程
在这里插入图片描述

🍯使用匿名管道进行通信

匿名管道是提供给有亲缘关系两个进程进行通信的。所以我们可以在创建管道之后通过fork函数创建子进程,这样父子进程就看到同一份资源,且父子进程都有这个管道的读写文件描述符。我们可以关闭父进程的读端,关闭子进程的写端,这样子进程往管道里面写数据,父进程往管道里面读数据,这样两个进程就可以实现通信了。
具体过程如下:

  1. 父进程创建管道
    在这里插入图片描述

  2. foek创建子进程
    在这里插入图片描述

  3. 关闭父进程的写端,子进程的读端
    在这里插入图片描述
    实例演示: 子进程每隔1秒往管道里面写数据,父进程每隔1秒往管道里读数据

#include #include #include #include #include int main(){  int pipefd[2];  int ret = pipe(pipefd);  if (ret == -1){    // 管道创建失败    perror("make piep");    exit(-1);  }  pid_t id = fork();  if (id < 0){    perror("fork failed");    exit(-1);  }  else if (id == 0){    // child    // 关闭读端    close(pipefd[0]);    const char* msg = "I am child...!\n";    //int count = 0;    // 写数据    while (1){      ssize_t s = write(pipefd[1], msg, strlen(msg));      printf("child is sending message...\n");      sleep(1);    }  }  else{    // parent    close(pipefd[1]);    char buf[64];    while (1){      ssize_t s = read(pipefd[0], buf, sizeof(buf)/sizeof(buf[0])-1);      if (s > 0){ buf[s] = '\0';// 字符串后放一个'\0' printf("father get message:%s", buf);      }      else if (s == 0){ // 读到文件结尾  写端关闭文件描述符 读端会读到文件结尾 printf("father read end of file...\n ");      }      sleep(1);    }  }  return 0;}

代码运行结果如下:
在这里插入图片描述

🍯匿名管道的读写规则

在这里我们分四种情况来进行研究:

  1. 写端速度小于读端速度,管道大部分时间内为空,即读条件不满足 让子进程每5秒写一次,父进程每1秒读一次,观察现象
#include #include #include #include #include #include int main(){  int pipefd[2];  int ret = pipe(pipefd);  if (ret == -1){    // 管道创建失败    perror("make piep");    exit(-1);  }  pid_t id = fork();  if (id < 0){    perror("fork failed");    exit(-1);  }  else if (id == 0){    // child    // 关闭读端    close(pipefd[0]);    const char* msg = "I am child...!\n";    //int count = 0;    // 写数据    while (1){      ssize_t s = write(pipefd[1], msg, strlen(msg));      sleep(5);// 管道大部分时间是空的,读条件不满足时,读端处于阻塞状态      printf("child is sending message...\n");    }  }  else{    // parent    close(pipefd[1]);    char buf[64];    //int count = 0;    while (1){      ssize_t s = read(pipefd[0], buf, sizeof(buf)/sizeof(buf[0])-1);      if (s > 0){ buf[s] = '\0';// 字符串后放一个'\0' printf("father get message:%s", buf);      }      else if (s == 0){ // 读到文件结尾  写端关闭文件描述符 读端会读到文件结尾 printf("father read end of file...\n ");      }      sleep(1);    }  }  return 0;}

代码运行结果如下: 读端处于阻塞
【Linux篇】第十一篇——进程间通信(管道+system V共享内存)
总结: 当读条件不满足时,读端进程会处于阻塞,从task_struct会从运行队列调到等待队列,知道有数据来,才会转移到运行队列中。

  1. 写端速度大于读端速度,管道大部分时间内是满的,即写调整不满足 让子进程一直,父进程每5秒读一次,观察现象
    这里这方核心代码,在上面那个例子的代码进行了一定改造
pid_t id = fork();if (id < 0){  perror("fork failed");  exit(-1);}else if (id == 0){  // child  // 关闭读端  close(pipefd[0]);  const char* msg = "I am child...!\n";  //int count = 0;  // 写数据  while (1){    ssize_t s = write(pipefd[1], msg, strlen(msg));    printf("child is sending message...\n");  }}else{  // parent  close(pipefd[1]);  char buf[64];  //int count = 0;  while (1){    ssize_t s = read(pipefd[0], buf, sizeof(buf)/sizeof(buf[0])-1);    if (s > 0){      buf[s] = '\0';// 字符串后放一个'\0'      printf("father get message:%s", buf);    }    else if (s == 0){      // 读到文件结尾  写端关闭文件描述符 读端会读到文件结尾      printf("father read end of file...\n ");      sleep(5);// 管道大部分时间都是满的,写条件不满足时,写端处于阻塞状态    }  }}

代码运行结果如下: 写端写了一会后,管道满了,此时写端处于阻塞状态
【Linux篇】第十一篇——进程间通信(管道+system V共享内存)
总结: 当写条件不满足时,写端处于阻塞状态

  1. 关闭写端 让写端先写5秒,然后关闭写端,观察现象
// child// 关闭读端close(pipefd[0]);const char* msg = "I am child...!\n";int count = 0;// 写数据while (1){  ssize_t s = write(pipefd[1], msg, strlen(msg));  printf("child is sending message...\n");    printf("CHILD: %d\n", count++);  if (count == 5){    close(pipefd[1]);    exit(-1); }  sleep(1);}// parentclose(pipefd[1]);char buf[64];while (1){  ssize_t s = read(pipefd[0], buf, sizeof(buf)/sizeof(buf[0])-1);  if (s > 0){    buf[s] = '\0';// 字符串后放一个'\0'    printf("father get message:%s", buf);    sleep(5);// 管道大部分时间都是满的,写条件不满足时,写端处于阻塞状态  }  else if (s == 0){    // 读到文件结尾  写端关闭文件描述符 读端会读到文件结尾    printf("father read end of file...\n ");   }}

代码运行结果如下: 3s后,关闭写端,读端会读到文件结尾
【Linux篇】第十一篇——进程间通信(管道+system V共享内存)
总结: 如果关闭写端,读端进程会读到文件结尾

  1. 关闭读端 5秒后关闭读端
#include #include #include #include #include #include int main(){  int pipefd[2];  int ret = pipe(pipefd);  if (ret == -1){    // 管道创建失败    perror("make piep");    exit(-1);  }  pid_t id = fork();  if (id < 0){    perror("fork failed");    exit(-1);  }  else if (id == 0){    // child    // 关闭读端    close(pipefd[0]);    const char* msg = "I am child...!\n";    // int count = 0;    // 写数据    while (1){      ssize_t s = write(pipefd[1], msg, strlen(msg));      printf("child is sending message...\n");     sleep(1);    }  }  else{    // parent    close(pipefd[1]);    char buf[64];    int count = 0;    while (1){      ssize_t s = read(pipefd[0], buf, sizeof(buf)/sizeof(buf[0])-1);      if (s > 0){ buf[s] = '\0';// 字符串后放一个'\0' printf("father get message:%s", buf); //sleep(5);// 管道大部分时间都是满的,写条件不满足时,写端处于阻塞状态      }      else if (s == 0){ // 读到文件结尾  写端关闭文件描述符 读端会读到文件结尾 printf("father read end of file...\n ");      }      sleep(1);      if (count++ == 3){ close(pipefd[0]);// 读端关闭文件描述符,写端进程后序会被操作系统直接杀掉,没有进程读,写时没有意义的 break;      }    }    int status;    pid_t ret = waitpid(id, &status, 0);    if (ret > 0){      // 等待成功      printf("child exit singal is %d\n", status&0x7f);    }    else{      // 等待失败      perror("wait failed");      exit(-1);    }  }  return 0;}

代码运行结果如下: 可以看出,关闭读端后,子进程收到操作系统发送的13号信号(SIGPIPE)杀死
【Linux篇】第十一篇——进程间通信(管道+system V共享内存)
总结: 读端关闭,写端进程会被操作系统发送信号杀死。
为什么写端进程会被OS杀死?

操作系统不做任何浪费空间和低效的事情,如果读端关闭,那么写还有什么意义呢?所以操作系统会通过信号把写端进程干掉

读写规则总结:

  • 当没有数据可读时
    O_NONBLOCK disable:read调用阻塞,即进程暂停执行,一直等到有数据来到为止。
    O_NONBLOCK enable:read调用返回-1,errno值为EAGAIN。
  • 当管道满的时候
    O_NONBLOCK disable: write调用阻塞,直到有进程读走数据
    O_NONBLOCK enable:调用返回-1,errno值为EAGAIN
  • 如果所有管道写端对应的文件描述符被关闭,则read返回0
  • 如果所有管道读端对应的文件描述符被关闭,则write操作会产生信号SIGPIPE,进而可能导致write进程退出
  • 当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性。
  • 当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性。

🍯管道的特点

有以下几点:

  • 只能用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信;通常,一个管道由一个进程创
    建,然后该进程调用fork,此后父、子进程之间就可应用该管道。
  • 管道提供流式服务。也就是你想往管道里读写多少数据是根据自身来定的
  • 一般而言,进程退出,管道释放,所以管道的生命周期随进程
  • 一般而言,内核会对管道操作进行同步与互斥
  • 管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道。

全双工和半双工:

  1. 半双工是指传输过程中同时只能向一个方向传输,一方的数据传输结束之后,另外一方再回应。双方传输数据是不可以同时进行的。
  2. 全双工是指两方能同时发送和接受数据。在这种情况下就没有拥堵的危险,数据的传输也就更快。

🌲命名管道

概念: 命名管道是一种特殊类型(符号性)的文件。在不相关(没有亲缘关系)的进程之间交换数据,可以使用FIFO文件来做这项工作,

创建命名管道FIFO–mkfifo

命名管道可以通过命令行创建,指令如下:

mkfifo filename

也可以通过mkfifo函数创建:

函数原型:

#include #include int mkfifo(const char *pathname, mode_t mode);

功能: 创建一个命名管道
参数:
pathname: 管道名称
mode: 权限
返回值: 创建成功返回0,失败返回-1

实例演示
实例1: 使用命令创建管道
【Linux篇】第十一篇——进程间通信(管道+system V共享内存)
实例2: 使用mkfifo函数创建

#include #include #include #include #include #include #define FIFO "./fifo"int main(){  umask(0);  // 创建管道  int ret = mkfifo(FIFO, 0666);  if (ret == -1){    perror("make fifo");    exit(-1);  }}

代码运行结果如下:
在这里插入图片描述

🍯匿名管道读写规则

  • 如果当前打开操作是为读而打开FIFO时
    O_NONBLOCK disable:阻塞直到有相应进程为写而打开该FIFO
    O_NONBLOCK enable:立刻返回成功
  • 如果当前打开操作是为写而打开FIFO时
    O_NONBLOCK disable:阻塞直到有相应进程为读而打开该FIFO
    O_NONBLOCK enable:立刻返回失败,错误码为ENXIO

🍯使用匿名管道

接下来,我会创建两个文件,一个是server.c,还有一个是client.c,用两个进程来模拟客户端和服务端进行通信,客户端往管道发消息,服务端读消息。
两段代码如下:
client.c

#include #include #include #include #include #include #include #define FIFO "./fifo"int main(){  // 以写的方式打开管道文件  int fd = open(FIFO, O_WRONLY);  if (fd < 0){    perror("open pipefile");    exit(-1);  }  char buf[64];  while (1){    printf("Please Enter Message# ");    fflush(stdout);    // 使用read读取用户输入的数据    ssize_t s = read(0, buf, sizeof(buf)/sizeof(buf[0])-1);    if (s > 0){      buf[s] = 0;      write(fd, buf, s+1);    }    else{      perror("read");      exit(-1);    }  }  return 0;}

server.c

#include #include #include #include #include #include #include #define FIFO "./fifo"int main(){  umask(0);  // 创建管道  int ret = mkfifo(FIFO, 0666);  if (ret == -1){    perror("make fifo");    exit(-1);  }    // 以读的方式打开管道  int fd = open(FIFO, O_RDONLY);  if (fd < 0){    perror("open fail");    exit(-1);  }   char buf[64];  while (1){    printf("wait client...\n");    ssize_t s = read(fd, buf, sizeof(buf)/sizeof(buf[0])-1);    if (s > 0){      // 正常读取      buf[s] = '\0';      printf("client say# %s", buf);    }    else if (s == 0){      // 客服端写端关闭,服务器读端读到文件末尾      printf("server exit...\n");      exit(0);    }    else{      // 读错误      perror("read");      exit(-1);    }  }  return 0;}

注意,这里我们需要先将服务进程跑起来,这样管道才可以被创建,以读得方式打开管道,然后将客户端进程跑起来,以写的方式打开管道。客户端只要不写入数据,服务端读会处于阻塞状态,如果客户端退出,此时管道为空,服务端会读到文件结尾,此时read返回0,让服务端进程也退出。
代码运行结果如下:
【Linux篇】第十一篇——进程间通信(管道+system V共享内存)
两个进程通信时,我们查看fifo的大小:
在这里插入图片描述
可以发现,管道的大小没有发生变化。其实两个进程通信是在内存中进行的,并没有把数据写到管道中,因为管道只是一个符号性的文件。如果是在管道写数据,那么IO次数会很多,效率太低了。

🌲命名管道和匿名管道的区别

  • 匿名管道由pipe函数创建并打开。
  • 命名管道由mkfifo函数创建,打开用open
  • FIFO(命名管道)与pipe(匿名管道)之间唯一的区别在它们创建与打开的方式不同,一但这些工作完成之后,它们具有相同的语义

🌏system V共享内存

🌲认识共享内存

共享内存区是最快的IPC形式。共享内存是在物理内存上申请一块空间,再让两个进程各自在页表建立虚拟地址和这块空间的映射关系。这样两个进程看到的就是同一份资源,这一份资源就叫做共享内存。
在这里插入图片描述
共享内存的数据结构: 其中shm_perm这个结构体中有key值(共享内存唯一标识符)

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 */};

🌲共享内存函数

两个进程通过共享内存进行通信需要经过以下几个步骤:

  1. 创建共享内存
  2. 将两个进程关联到共享内存
  3. 取消两个进程和共享内存的关联
  4. 删除共享内存

注意: 前两个步骤是为了让两个进程实现通信,后面两个步骤是释放共享内存空间,要不然就会内存泄漏了。(与我们之前用的malloc是类似的)

下面介绍共享内存函数:

  1. ftok——获取一个共享内存的唯一标识符

函数原型:

#include #include key_t ftok(const char *pathname, int proj_id);

功能: 获取一个共享内存的唯一标识符key
函数参数:
pathname:可以传入任何文件名
proj_id:只有是一个非0的数都可以
返回值:
成功返回key值,失败返回-1

  1. shmget——创建共享内存

函数原型:

#include #include int shmget(key_t key, size_t size, int shmflg);

功能: 创建共享内存
函数参数:
key:传入ftok函数获取的共享内存唯一标识符
size:共享内存的大小(页(4kb)的整数倍)
shmflg:权限,由9个权限标准构成
这里介绍两个选项
IPC_CREAT: 如果底层存在这个标识符的共享内存空间,就打开返回,不存在就创建
IPC_EXCL: 如果底层存在这个标识符的共享内存空间,就出错返回
两个选项合起来用就可以穿甲一个权限的共享内存空间
返回值:
成功返回共享内存标识码值(给用户看的),失败返回-1

  1. shmat——将共享内存空间关联到进程地址空间

函数原型:

#include #include void *shmat(int shmid, const void *shmaddr, int shmflg);

功能: 将共享内存空间关联到进程地址空间
参数:
shmid:共享内存标识符
shmaddr:指定连接地址。
shmfig:两个可能取值是SHM_RND和SHM_RDONLY
返回值: 成功返回一个指针(虚拟地址空间中共享内存的地址,是一个虚拟地址),失败返回-1
说明:
shmaddr为NULL,核心自动选择一个地址
shmaddr不为NULL且shmflg无SHM_RND标记,则以shmaddr为连接地址。
shmaddr不为NULL且shmflg设置了SHM_RND标记,则连接的地址会自动向下调整为SHMLBA的整> 数倍。公式:shmaddr -(shmaddr % SHMLBA)
shmflg=SHM_RDONLY,表示连接操作用来只读共享内存

  1. shmdt——取消关联

函数原型:

#include #include int shmdt(const void *shmaddr);

功能: 取消共享内存空间和进程地址空间的关联
参数:
shmaddr:共享内存的起始地址(shmat获取的指针)
返回值: 成功返回0,失败返回-1

  1. shmdt——取消关联

函数原型:

#include #include int shmdt(const void *shmaddr);

功能: 取消共享内存空间和进程地址空间的关联
参数:
shmaddr:共享内存的起始地址(shmat获取的指针)
返回值: 成功返回0,失败返回-1

  1. shmctl——控制共享内存

函数原型:

#include #include int shmctl(int shmid, int cmd, struct shmid_ds *buf);

功能: 控制共享内存
参数:
shmid:共享内存标识符
cmd:命令,有三个
IPC_STAT: 把shmid_ds结构中设置为共享内存当前关联值
IPC_SET: 把共享内存的当前关联值设置为shmid_ds数据结构中的值
IPC_RMID:删除共享内存段
buf:指向一个报错这共享内存的模式状态和访问权限的数据结构
返回值: 成功返回0,失败返回-1

实例演示:
实例1: 获取共享内存唯一标识符,并创建一块共享内存

#include #include #include #include #include #include #define PATHNAME "."#define PROJ_ID 0x666#define SIZE 4096int main(){  // 先通过ftok函数 利用pathname和proj_id来生成一个共享内存标识符key,用来标识共享内存(给OS看的)  key_t key = ftok(PATHNAME, PROJ_ID);  if (key == -1){    // 标识符生成失败    perror("ftol fail");    exit(-1);  }     printf("key:%p\n", key);    // 创建内存空间  // IPC_CREAT 要创建的共享内存如果存在,就打开返回,不存在就创建  // IPC_EXCL  如果底层共享内存已经存在就出错返回  // 结合使用可以创建一个全新的共享内存  int shmid = shmget(key, SIZE, IPC_CREAT|IPC_EXCL|0664);  if (shmid < 0){    perror("shmget");    exit(-1);  }  printf("shmid:%d\n", shmid);  return 0;}

代码运行结果如下:
【Linux篇】第十一篇——进程间通信(管道+system V共享内存)
实例2: 通过指令ipcs -m查看ipc资源
在这里插入图片描述
实例3: 通过指令ipcrm -m shmid删除共享内存,IPC的声明周期随内核
在这里插入图片描述
实例4: 开辟一块共享内存空间,然后将进程和这块共享内存关联起来,5秒后取消关联并删除共享内存空间

#include #include #include #include #include #include #define PATHNAME "."#define PROJ_ID 0x666#define SIZE 4096int main(){  key_t key = ftok(PATHNAME, PROJ_ID);  if (key == -1){    // 标识符生成失败    perror("ftol fail");    exit(-1);  }     printf("key:%p\n", key);    int shmid = shmget(key, SIZE, IPC_CREAT|IPC_EXCL|0664);  if (shmid < 0){    perror("shmget");    exit(-1);  }  printf("shmid:%d\n", shmid);    // 连接共享内存(关联)  char* str = (char*)shmat(shmid, NULL, 0);   sleep(5);  // 取消关联  if (shmdt(str) == -1){    perror("shmdt");    exit(-1);  }    // 删除共享内存段  shmctl(shmid, IPC_RMID, NULL);  return 0;}

同时打开命令行监控脚本:

while :; do ipcs -m; echo "#####################"; sleep 1;done;

代码运行结果如下: 5秒后,关联数由1变0
在这里插入图片描述

🌲使用共享内存实现进程通信

和匿名管道那里一样,这里有client.c和server.c两个文件,还有一个comm.h一个头文件,里面存放两个进程公共的pathname和proj_id,这样两个进程就可以得到相同的共享内存唯一标识符。
这里我们选择使用服务端创建共享内存,然后连接到共享内存,让客户端也连接上这块共享内存,客户端写数据,服务端不断读
代码如下:
comm.h

#pragma once#include #include #include #include #include #include #define PATHNAME "."#define PROJ_ID 0x666#define SIZE 4096

server.c

#include "comm.h"int main(){  // 先通过ftok函数 利用pathname和proj_id来生成一个共享内存标识符key,用来标识共享内存(给OS看的)  key_t key = ftok(PATHNAME, PROJ_ID);  if (key == -1){    // 标识符生成失败    perror("ftol fail");    exit(-1);  }     printf("key:%p\n", key);    // 创建内存空间  // IPC_CREAT 要创建的共享内存如果存在,就打开返回,不存在就创建  // IPC_EXCL  如果底层共享内存已经存在就出错返回  // 结合使用可以创建一个全新的共享内存  int shmid = shmget(key, SIZE, IPC_CREAT|IPC_EXCL|0664);  if (shmid < 0){    perror("shmget");    exit(-1);  }  printf("shmid:%d\n", shmid);    // 连接共享内存(关联)  char* str = (char*)shmat(shmid, NULL, 0);   //sleep(5);    // 服务端每隔一秒在显示器上刷新共享内存段中的数据  while (1){    printf("client say# %s\n", str);    sleep(1);  }  // 取消关联  if (shmdt(str) == -1){    perror("shmdt");    exit(-1);  }    // 删除共享内存段  shmctl(shmid, IPC_RMID, NULL);  return 0;}

client.c

#include "comm.h"int main(){  // client通过相同的pathname 和 proj_id 可以创建出一个和server.c相同的共享内存唯一标识符  key_t key = ftok(PATHNAME, PROJ_ID);  if (key == -1){    // 标识符生成失败    perror("ftol fail");    exit(-1);  }    // 直接获取服务端创建的共享内存  int shmid = shmget(key, SIZE, 0);  if (shmid < 0){    perror("shmget");    exit(-1);  }      // 连接共享内存(关联)  char* str = (char*)shmat(shmid, NULL, 0);     // 客户端每个5s在共享内存写数据  char start = 'a';  while (start <= 'z'){    str[start-'a'] = start;    ++start;    sleep(3);  }  // 取消关联  if (shmdt(str) == -1){    perror("shmdt");    exit(-1);  }   return 0;}

代码运行结果如下:
【Linux篇】第十一篇——进程间通信(管道+system V共享内存)
客户端每3s往共享内存多写入一个字符,服务端不断读取共享内存的数据。当其中一个进程终止,并不会影响另一个进程。
结论: 共享内存底层不提供任何同步与互斥的机制

🌐总结

今天的内容就先介绍到这。喜欢的话,欢迎点赞支持和关注~
在这里插入图片描述