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

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


🌏介绍

概念: 进程间通信(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 <unistd>
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 <stdio.h>
#include <unistd.h>

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这个数组里面放的是两个文件描述符
在这里插入图片描述

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

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>

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

🍯使用匿名管道进行通信

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

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

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

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

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>

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 <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>

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

代码运行结果如下: 读端处于阻塞
请添加图片描述
总结: 当读条件不满足时,读端进程会处于阻塞,从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);// 管道大部分时间都是满的,写条件不满足时,写端处于阻塞状态
    }
  }
}

代码运行结果如下: 写端写了一会后,管道满了,此时写端处于阻塞状态
请添加图片描述
总结: 当写条件不满足时,写端处于阻塞状态

  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);
}
// 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);
    sleep(5);// 管道大部分时间都是满的,写条件不满足时,写端处于阻塞状态
  }
  else if (s == 0){
    // 读到文件结尾  写端关闭文件描述符 读端会读到文件结尾
    printf("father read end of file...\n ");
   }
}

代码运行结果如下: 3s后,关闭写端,读端会读到文件结尾
请添加图片描述
总结: 如果关闭写端,读端进程会读到文件结尾

  1. 关闭读端 5秒后关闭读端
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>

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)杀死
请添加图片描述
总结: 读端关闭,写端进程会被操作系统发送信号杀死。
为什么写端进程会被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 <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);

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

实例演示
实例1: 使用命令创建管道
在这里插入图片描述
实例2: 使用mkfifo函数创建

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>

#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 <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

#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 <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

#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,让服务端进程也退出。
代码运行结果如下:
请添加图片描述
两个进程通信时,我们查看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 <sys/types.h>
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);

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

  1. shmget——创建共享内存

函数原型:

#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);

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

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

函数原型:

#include <sys/types.h>
#include <sys/shm.h>
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 <sys/types.h>
#include <sys/shm.h>
int shmdt(const void *shmaddr);

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

  1. shmdt——取消关联

函数原型:

#include <sys/types.h>
#include <sys/shm.h>
int shmdt(const void *shmaddr);

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

  1. shmctl——控制共享内存

函数原型:

#include <sys/types.h>
#include <sys/shm.h>
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 <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <stdlib.h>
#include <unistd.h>

#define PATHNAME "."
#define PROJ_ID 0x666
#define SIZE 4096

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);
  return 0;
}

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

#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <stdlib.h>
#include <unistd.h>

#define PATHNAME "."
#define PROJ_ID 0x666
#define SIZE 4096

int 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 <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <stdlib.h>
#include <unistd.h>

#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;
}

代码运行结果如下:
请添加图片描述
客户端每3s往共享内存多写入一个字符,服务端不断读取共享内存的数据。当其中一个进程终止,并不会影响另一个进程。
结论: 共享内存底层不提供任何同步与互斥的机制

🌐总结

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

  • 38
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 34
    评论
评论 34
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

呆呆兽学编程

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值