Linux进程间通信

Posted 蓝乐

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux进程间通信相关的知识,希望对你有一定的参考价值。

进程间通信(IPC)

🕐进程间通信简介

🕐目的

进程间通信是为了方便数据传输、资源共享、通知事件(子进程退出时需通知父进程退出信息)、进程控制(调试时Debug进程的控制)等功能而存在的。

🕐发展

进程间通信分为三个阶段:管道;System V IPC;POSIX IPC其中前两种通信方式为本地设备上的进程通信,而第三种通过网络实现跨设备的进程通信,本文将介绍本地的两种通信方式。

🕐分类

进程间通信也可以按发展进行分类:

🕒管道

管道是一种较老的进程间通信形式。

🕒特点

  • 管道是一种半双工通信方式,即数据只能向一个方向流动(即一个进程进行写操作,一个进程进行读操作);如果要进行双向通信,则需要建立起两个管道。
  • 一般而言,进程退出,管道随之释放,即管道生命周期随进程
  • 管道自带同步与互斥机制(后面详细介绍),从而保证了数据读取的原子性。

🕒分类

管道分为匿名管道和命名管道,二者的底层原理是一样的。我们首先来认识匿名管道:

🕒匿名管道(pipe)

顾名思义,匿名管道就是没有名字的管道,匿名管道就是没有血缘关系的进程,常见于父子进程之间。

🕒感性认识

父子进程创建匿名管道的过程如下:

🕒接口介绍

#include <unistd.h>
功能:创建一匿名管道
原型
int pipe(int fd[2]);
参数
fd:文件描述符数组,其中fd[0]表示读端, fd[1]表示写端
返回值:成功返回0,失败返回错误代码

🕒代码实现

#include <stdio.h>
#include <unistd.h>
#include <string.h>

int main()

  int pipe_fds[2];
  int ret = pipe(pipe_fds);//创建管道,0为读端,1为写端
  if(ret == -1)
  
    perror("pipe");
    return 1;
  
  int pid = fork();
  if(pid < 0)
  
    //error
    perror("fork");
    return 2;
  
  else if(pid == 0)
  
    //child
    close(pipe_fds[1]);//子进程关闭写端
    char buf[128];
    //子进程从管道中读取数据
    read(pipe_fds[0],buf, sizeof(buf) - 1);
    printf("%s\\n", buf);
    close(pipe_fds[0]);
  
  else
  
    //father
    close(pipe_fds[0]);//父进程关闭读端
    //父进程往管道内写数据
    const char* msg = "I am father.\\n";
    write(pipe_fds[1], msg, strlen(msg));
    close(pipe_fds[1]);
  
  return 0;

🕒命名管道(FIFO)

匿名管道只能在具有亲缘关系的进程之间进行通信,那么是否有可以实现任意进程间通信的方法呢?那就是命名管道(FIFO)

🕒命名管道的原理

我们知道文件在磁盘中存储有着其唯一的路径,毫无管理的两个进程通过这个路径找到相应的管道文件,再借助该管道文件在内核中的缓冲区就可以进行通信了,需要注意的是这个管道文件是不存储数据的,其大小始终是0字节。

🕒接口介绍

int mkfifo(const char *filename,mode_t mode);
功能:创建命名管道
参数:
filename:命名管道的文件名
mode:相应文件的权限
返回值:调用成功返回0,失败返回-1

🕒代码实现

我们创建两个进程A,B;其中A进程向命名管道写入数据,B进程从管道读取数据:

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

int main()

  int ret = mkfifo("./myfifo", 0644);//创建命名管道
  if(ret < 0)
  
    perror("mkfifo");
    return 1;
  
  int fd = open("./myfifo", O_WRONLY);//打开管道文件,以写方式打开
  if(fd < 0)
  
    perror("open");
    return 2;
  
  char buf[128];
  while(1)
  
    buf[0] = '\\0';//清空buf数组
    printf("Please Write:");
    fflush(stdout);
    ssize_t s = read(0, buf, sizeof(buf) - 1);
    if(s > 0)//读取成功
    
      buf[s] = '\\0';
      write(fd, buf, strlen(buf));//向管道写入数据
    
    else if(s == 0)
    
      break;
    
    else
    
      //error
      break;
    
  
  close(fd);
  return 0;

//pipe_B.c
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main()

  int fd = open("./myfifo", O_RDONLY);//以读方式打开文件
  if(fd < 0)
  
    perror("open");
    return 2;
  
  char buf[128];
  while(1)
  
    buf[0] = '\\0';//清空buf数组
    //printf("Please Write:");
    //fflush(stdout);
    ssize_t s = read(fd, buf, sizeof(buf) - 1);//从管道读取数据
    if(s > 0)//读取成功
    
      buf[s] = '\\0';
      printf("A say:\\n");
      write(1, buf, strlen(buf));
    
    else if(s == 0)
    
      printf("A process quit...\\n");
      break;
    
    else
    
      //error
      break;
    
  
  close(fd);
  return 0;

效果如下:

🕒读写规则

通过上面的演示我们可以得出一些管道的读写规则:

  • 当管道内没有数据可读时,进行读取数据的进程会一直等待直到数据写入管道,即read调用阻塞。
  • 而如果读端不读,写端一直在写,那么write调用会阻塞,直到读端将管道中的数据读走。
  • 当写端不写入或者被关闭,那么读端会一直读取直到管道文件末尾。
  • 当读端不读或者被关闭,而此时写端调用一直向管道中写入数据,这就造成了资源浪费的现象,操作系统是不容许资源浪费的存在的,因此会发送信号杀死写端。此时发送的信号为13号信号,即管道信号。


🕕System V

关于System V,本文将重点介绍共享内存,而其他二者由于较少使用故了解即可。

🕕共享内存

共享内存根据其名字就可以推测与内存中的共享区有关。实际上,共享内存的使用要比管道的简单。
32位系统下:

🕕指令及接口

  • 函数接口:
1. key_t ftok(const char *pathname, int proj_id);
功能:用来生成System V IPC密钥,key是用来唯一标识共享内存块的值
参数
pathname:共享内存文件的给定路径名
proj_id:project id
这两个参数可以随意设置,只不过要保证使用共享内存的进程这两个参数设置需一样。
返回值:成功返回生成的key值,失败返回-1
2. int shmget(key_t key, size_t size, int shmflg);
功能:用来创建共享内存
参数
 key:这个共享内存段名字
 size:共享内存大小
 shmflg:由九个权限标志构成,它们的用法和创建文件时使用的mode模式标志是一样的
返回值:成功返回一个非负整数,即该共享内存段的标识码;失败返回-1
3. void *shmat(int shmid, const void *shmaddr, int shmflg);
功能:将共享内存段连接到进程地址空间
参数
 shmid: 共享内存标识
 shmaddr:指定连接的地址
 shmflg:它的两个可能取值是SHM_RND和SHM_RDONLY
返回值:成功返回一个指针,指向共享内存第一个字节;失败返回-1
4. int shmdt(const void *shmaddr);
功能:将共享内存段与当前进程脱离
参数
 shmaddr: 由shmat所返回的指针
返回值:成功返回0;失败返回-1
注意:将共享内存段与当前进程脱离不等于删除共享内存段
5. int shmctl(int shmid, int cmd, struct shmid_ds *buf);
功能:用于控制共享内存
参数
 shmid:由shmget返回的共享内存标识码
 cmd:将要采取的动作(有三个可取值)
 buf:指向一个保存着共享内存的模式状态和访问权限的数据结构
返回值:成功返回0;失败返回-1

shmctl中cmd的几种命令:

命令说明
IPC_STAT把shmid_ds结构中数据设置为共享内存的当前关联之
IPC_SET在进程有足够权限的前提下,把共享内存的当前关联之设置为shmid_ds数据结构中给出的值
IPC_RMID删除共享内存段

对于共享内存的key和shmid,我们可以类比文件中的inode与fd的关系。
虽然文件系统一inode唯一标识文件,但在实际使用中仍是以fd文件描述符去操作文件。

  • 相关指令:
ipcs -m:查看当前共享内存的信息
ipcrm -m + shmid:删除对应shmid的共享内存块

代码实现

让server进程创建共享内存,获取key值及shmid,再让client进程通过shmid去挂接共享内存,然后观察两个进程通过共享内存进行通信的现象:

common.h
#pragma once 

#define PATH_NAME "/home/lyl/2022-3-20"
#define PROJ_ID 0x6666

#define SIZE 4097
//server.c
#define _SVID_SOURCE 1

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

int main()

  //获取key值
  key_t key = ftok(PATH_NAME, PROJ_ID);
  if(key == -1)
  
    perror("ftok");
    return 1;
  
  printf("key: %x\\n", key);
  //获取shmid
  int shmid = shmget(key, SIZE, IPC_CREAT | IPC_EXCL | 0644);//若不存在则创建共享内存,若存在则报错
  if(shmid == -1)
  
    perror("shmget");
    return 2;
  
  printf("shmid:%d\\n",shmid);
  //让进程挂接共享内存,形成关联
  char* addr = (char*)shmat(shmid, NULL, 0); 
  printf("server attached on shared memory\\n");
  if(addr == (char*)-1)
  
    perror("shmat");
    return 3;
  
  printf("addr:%p\\n", addr);

  //TODO
  
  while(1)
  
    printf("%s\\n", addr);
    sleep(1);
  

  //去关联
  shmdt(addr);
  printf("server attached off shared memory\\n");
  shmctl(shmid, IPC_RMID, NULL);
  printf("server deleted shared memory\\n");
  return 0;

//client.c
#define _SVID_SOURCE 1

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <string.h>
#include "common.h"

int main()

  //获取key值
  key_t key = ftok(PATH_NAME, PROJ_ID);
  if(key == -1)
  
    perror("ftok");
    return 1;
  
  //获取shmid
  int shmid = shmget(key, SIZE, IPC_CREAT);//不需要自己创建共享内存,直接获取shmid即可
  if(shmid == -1)
  
    perror("shmget");
    return 2;
  
  //让进程挂接共享内存,形成关联
  char* addr = (char*)shmat(shmid, NULL, 0); 
  printf("client attached on shared memory\\n"); 
  //TODO
  
  const char* msg = "I am process client\\n";

  for(size_t i = 0; i < strlen(msg); i++)
  
    addr[i] = *(msg + i);
    sleep(1);
  

  //去关联
  shmdt(addr);
  printf("client attached off shared memory\\n");

  return 0;

client进程向共享内存中写入数据,然后server进程从共享内存中读取数据。

可以看到,client进程每次写入一个数据时,server进程立即将数据从共享内存中读取并打印到标准输出中,因此可得出共享内存不存在同步与互斥机制,两个进程独立,不像管道会存在阻塞现象。
这里每次进程结束后都需要主动释放共享内存,否则再次执行进程时会报错。

🕕特点

在上面的演示中,我们可以得出共享内存的一下特点:

  • 共享内存的操作是非进程安全的。
  • 共享内存只有在当前映射链接数为0时,才能被被真正删除。
  • 共享内存的生命周期随内核,只要不主动删除,其就会随内核一直存在,除非重启系统。
  • 共享内存由于不需要拷贝数据,因此时进程通信中最快的形式。
  • 共享内存不存在同步与互斥机制,两个进程相互独立。

🕕消息队列

  • 消息队列提供了一个从一个进程向另外一个进程发送一块数据的方法
  • 每个数据块都被认为是有一个类型,接收者进程接收的数据块可以有不同的类型值
  • 特性:IPC资源必须删除,否则不会自动清除,除非重启,所以system V IPC资源的生命周期随内核

🕕信号量

信号量主要用于同步和互斥的,特性与其他二者相同。下面我们来看看什么是同步和互斥:
进程互斥:

  • 由于各进程要求共享资源,而且有些资源需要互斥使用,因此各进程间竞争使用这些资源,进程的这种关系为进程的互斥
  • 系统中某些资源一次只允许一个进程使用,称这样的资源为临界资源或互斥资源。
  • 在进程中涉及到互斥资源的程序段叫临界区

以上是关于Linux进程间通信的主要内容,如果未能解决你的问题,请参考以下文章

Linux管道和命名管道

思考总结Linux系统框架进程间通信

Linux 进程间通信方式都有哪些

简述Linux进程间通信的几种方式

Linux进程间通信简介

深刻理解Linux进程间通信(IPC)