Linux 进程间通信(IPC)

文章目录

  • 进程间通信介绍
    • 进程间通信的概念
    • 进程间通信的目的
    • 进程间通信的本质理解
    • 进程间通信分类
  • 管道
    • 匿名管道
      • 匿名管道的原理
    • pipe函数
      • 管道的特点
    • 命名管道
      • 命名管道的原理
      • 使用相关命令创建命名管道
      • 使用命名管道实现server端和client端通信
  • system V共享内存
    • 共享内存通信的基本原理
    • 共享内存的基本构成
    • 共享内存函数
      • 创建共享内存
      • 释放共享内存
      • 关联共享内存
      • 取消关联共享内存
      • 使用共享内存让shmServer和shmClien通信
      • 共享内存与管道的区别
    • 浅谈 system V信号量
      • 信号量的原理

进程间通信介绍

进程间通信的概念

进程间通信(ipc), 指不同进程之间的信息传输或交换。

进程间通信的目的

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

进程间通信的本质理解

我们知道进程间具有独立性,因为写时拷贝存在,父子进程之间向持续性的交换数据是不可能的。如果想让两个进程通信,必须先让不同的进程看到同一份资源作为数据交换的场所。

进程间通信分类

管道

  • 匿名管道
  • 命名管道

System V IPC

  • System V 消息队列
  • System V 共享内存
  • System V 信号量

POSIX IPC

  • 消息队列
  • 共享内存
  • 信号量
  • 互斥量
  • 条件变量
  • 读写锁

注意:针对以上分类,我们主要对管道和共享内存详细学习。

管道

什么是管道

  • 管道是Unix中最古老的进程间通信的形式,将数据单项传输,是一种单向通信的方式。
  • 我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”。

匿名管道

匿名管道的原理

匿名管道通信原理:进程之间通过管道进行通信。

主要步骤如下
假设,我们让父进程对目标文件写入数据,子进程对目标文件读取数据。

  • 那么此时父进程通过files_struct中的四号文件描述符(关闭三号文件描述符)找到struct_file文件指针,再通过inode找到目标文件加载到物理内存中文件缓冲区的数据。
  • 子进程通过files_struct中的三号文件描述符(关闭四号文件描述符)找到struct_file文件指针,再通过inode找到目标文件加载到物理内存中文件缓冲区的数据。
  • 综上,此时父子进程就看到了同一份“资源”,父子进程进而可以对该文件进行写,读。操作,从而父子进程打实现进程间通信。

在这里插入图片描述

注意

  • 在创建父子进程的时候 进程相关数据结构需要重新拷贝,被打开文件相关内核结构不会被拷贝。因为fork函数只是为了创建子进程,不会对文件相关数据结构作拷贝。

  • 此时父子进程看到的同一份文件资源,并对其进行写,读操作,并不会发生写时拷贝,因为被打开文件内核数据结构是由操作系统维护的,不受进程维护。

  • 文件描述符意义:0:标准输入,1:标准输出,2:标准错误,3:读文件描述符,4:写文件描述符。

  • 进程间通信是内存级通信,不需要将数据写入到磁盘文件中,因为反复的IO会降低效率,也没有必要。

pipe函数

pipe函数一般用于创建匿名管道,pipe函数原型如下:

int pipe( int fd[2] ) 

参数
fd: 文件描述符数组,其中fd[0]代表读端,fd[1]代表写端。

返回值
成功返回0,失败返回-1.

实践代码

//父进程写入文件,子进程读取文件#include <iostream>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <assert.h>
using namespace std;
int main()
{//创建管道int pipefd[2] = { 0 }; //pipefd[0]: 读端 //pipefd[1]: 写端.int n = pipe( pipefd );assert( n !=- 1 );(void)n;//创建子进程。pid_t id = fork();assert( id != -1 );if( id == 0 )      //子进程,子进程读取。{close(pipefd[1]);char buffer[1024];while( true )   //先将数据读取到缓冲区中打印出来。{ssize_t s = read( pipefd[0],buffer,sizeof(buffer)-1);if( s > 0 ){buffer[s] = '\0';cout << " child get a message[" << getpid() <<"] father#" << buffer << endl; }}exit(0);}close(pipefd[0]);  //父进程,父进程写入。string message = "我是父进程,我正在给你发信息";int count = 0; char send_buffer[1024];  while( true ){snprintf( send_buffer,sizeof(send_buffer), "%s: %d",message.c_str(),count++ );write(pipefd[1],send_buffer,strlen(send_buffer));sleep(1);}pid_t ret = waitpid(id,nullptr,0);(void ) ret;assert(ret > 0 );return 0;
}

运行结果如下

在这里插入图片描述

管道的特点

1 . 管道一般用来具有血缘关系的进程用来进程间通信---->常用于父子进程。

2 .管道具有通过让进程间协同,进而提供访问控制。

  • 如果父进程写数据慢(每一次写入暂停10秒),子进程读取数据快,那么子进程便会等待父进程将数据写入后再读取(子进程也暂停10秒)。
  • 但是如果父进程写得快,子进程读得慢(每读一次暂停10秒),这样父进程在缓冲区写满之后就不会继续写了,等子进程将数据全部读完,父进程再进行写入。

3.管道提供的是面向流式的通信服务(面向字节流,写一条都一条,如果写得快,读的慢,那么就会一次性读取数据)。

4.管道是基于文件的,文件的生命周期是基于进程的,从而管道的生命周期跟随于进程。如果在通信过程中,父子进程都退出,那么文描述符便会被关闭,管道也会自动退出。
例如,当父进程写入文件10次后便关闭文件描述符退出,而子进程此时read的返回值便会为零,那么子进程也会退出。

int main()
{//创建管道int pipefd[2] = { 0 }; //pipefd[0]: 读端 //pipefd[1]: 写端.int n = pipe( pipefd );assert( n !=- 1 );(void)n;//创建子进程。pid_t id = fork();assert( id != -1 );if( id == 0 )      //子进程,子进程读取。{close(pipefd[1]);char buffer[1024];while( true )   //先将数据读取到缓冲区中打印出来。{ssize_t s = read( pipefd[0],buffer,sizeof(buffer)-1);if( s > 0 ){buffer[s] = '\0';cout << " child get a message[" << getpid() <<"] father#" << buffer << endl; }else if(  s == 0 ){cout <<  "writer quit, i quit" << endl;break;}}close(pipefd[0]);exit(0);                       }close(pipefd[0]);  //父进程,父进程写入。string message = "我是父进程,我正在给你发信息";int count = 0; char send_buffer[1024];  while( true ){snprintf( send_buffer,sizeof(send_buffer), "%s: %d",message.c_str(),count++ );write(pipefd[1],send_buffer,strlen(send_buffer));sleep(1);cout << count << endl;if( count == 10 ){cout << "writer quit(father)" << endl;break;}}close( pipefd[1]);              //父进程准备退出,关闭文件描述符。pid_t ret = waitpid(id,nullptr,0);(void ) ret;assert(ret > 0 );return 0;
}

运行结果如下
在这里插入图片描述
5.管道是单项通信,本质上就是半双工通信的一种特殊情况,通信双方中,一方固定为读端,一方固定为写端。

6.两种特殊情况。

  • 父进程写端关闭,子进程读0,那么表示读到了文件结尾。
  • 子进程读端关闭,父进程继续读取,OS也会终止写进程。

命名管道

命名管道的原理

  • 由于匿名管道应用的一个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信。一般情况下,一个管道由一个进程创建,然后该进程调用fork,此后父子进程之间便可以通信。
  • 如果我们想在不相关的进程之间交换数据,可以使用FIFO文件来做这项工作。我们可以让两个进程打开该文件,此时进而让两个进程看到了同一份“资源”,进而实现进程通信。

注意

  • 命名管道是一种特殊类型的文件,可以被打开,但是不会将内存数据刷新到磁盘中,相对于匿名管道来说,命名管道有文件名,并且存在于系统路径中。

使用相关命令创建命名管道

我们可以通过mkfifo命令创建一个命名管道。

[yzh@yzh test1]$ mkfifo fifo

并且,我们可以看到创建出来的文件类型为p,即代表的是管道文件。
在这里插入图片描述
此时,我们便可以通过shell脚本命令行每秒将一个字符串写入到命名管道文件中,再从另一端读取该命名管道中的数据打印到显示器上,这也进而体现了两个毫不相关的进程可以通过命名管道通信。
在这里插入图片描述

使用命名管道实现server端和client端通信

为了让服务端(server)与客户端(client)进程间通信,服务端(client)步骤如下:

  • 创建命名管道
  • 创建多个子进程,让这些子进程通过文件描述符读端读取命名管道数据到缓冲区中。
  • 父进程等待子进程退出,关闭文件描述符读端,删除命名管道文件。
#include "common.hpp"
static void getMessage(int fd)
{char buffer[SIZE];while (true){memset(buffer, '\0', sizeof(buffer));ssize_t s = read(fd, buffer, sizeof(buffer) - 1);if (s > 0){cout <<"["  << getpid() << "] "<< "client say> " << buffer<< endl;}else if (s == 0){// end of filecerr <<"["  << getpid() << "] " << "read end of file, clien quit, server quit too!" << endl;break;}else{// read errorperror("read");break;}}
}int main()
{// 1. 创建管道文件umask(0);if (mkfifo(ipcPath.c_str(), MODE) < 0){perror("mkfifo");exit(1);}Log("创建管道文件成功", Debug) << " step 1" << endl;// 2. 正常的文件操作int fd = open(ipcPath.c_str(), O_RDONLY);if (fd < 0){perror("open");exit(2);}Log("打开管道文件成功", Debug) << " step 2" << endl;int nums = 3;for (int i = 0; i < nums; i++){pid_t id = fork();if (id == 0)             //随机让每一个子进程读取数据,当client端退出的时候{// 3. 编写正常的通信代码了getMessage(fd);exit(1);}}for(int i = 0; i < nums; i++)   //父进程随机等待子进程退出结果,当子进程全部退出完毕,父进程退出程序。{waitpid(-1, nullptr, 0);}// 4. 关闭文件close(fd);Log("关闭管道文件成功", Debug) << " step 3" << endl;unlink(ipcPath.c_str()); // 通信完毕,就删除文件Log("删除管道文件成功", Debug) << " step 4" << endl;return 0;
}

客户端(server)步骤如下:

  • 打开命名管道文件。
  • 通过getline函数将cin中的数据读取到缓冲区,再通过缓冲区写入到管道文件中。
  • 关闭文件描述符写端。
#include "common.hpp"
#include <sys/wait.h>
int main()
{// 1. 获取管道文件int fd = open(ipcPath.c_str(), O_WRONLY);if(fd < 0){perror("open");exit(1);}// 2. ipc过程string buffer;while(true){cout << "Please Enter Message Line :> ";std::getline(std::cin, buffer);                  //从cin中读取数据到buffer中。       write(fd, buffer.c_str(), buffer.size());             }// 3. 关闭close(fd);return 0;
}

log.hpp文件,主要包含打印命名管道文件提示符函数。

#define Debug   0
#define Notice  1
#define Warning 2
#define Error   3const std::string msg[] = {"Debug","Notice","Warning","Error"
};std::ostream& Log(std::string message, int level)
{std::cout << " | " << (unsigned)time(nullptr) << " | "<< msg[level] << " | " << message;return cout;
}

common.hpp文件,主要包含一些系统函数头文件。

#include <sys/types.h>
#include <sys/wait.h>
#include <iostream>
#include <string>
#include <cstdio>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include "log.hpp"using namespace std;#define MODE 0666
#define SIZE 128string ipcPath = "./fifo.ipc"; //命名管道文件的路径。

运行结果如下:
在这里插入图片描述
如果客户端(server)退出后,服务端read读取的返回值变为0,此时表示服务端已经读取到数据结尾,所以子进程也将逐步退出。
在这里插入图片描述
如果服务端(server)退出后,客户端(client)写入管道的数据将不会被服务端读取,当客户端(client)下一次再向管道写入数据时,就会收到操作系统发来的13号信号(SIGPIPE),此时客户端也被强制退出。
在这里插入图片描述

system V共享内存

共享内存通信的基本原理

  • 如果进程之间通过共享内存通信,那么需要先让进程之间看到一份内存“资源”。首先,操作系统必须在物理内存之中申请一块内存空间,并将这一块共享内存通过每一个进程的页表,映射到各自的虚拟地址空间(mm_struct)堆栈之间,进而映射到每一个进程之中。
    • 如果需要释放共享内存,那么必须先将共享内存与进程之间的映射关系解除,然后再释放共享内存。
      在这里插入图片描述

共享内存的基本构成

共享内存是由操作系统维护的,由于在操作系统中可能存在大量使用共享内存进行通信的进程,为了管理这些共享内存,操作系统除了要在物理内存中开辟共享内存之外,还必须通过有关共享内存的数据结构(描述共享内存的所有属性)进行管理。所以,共享内存 = 共享内存块 + 有关共享内存的数据结构。

共享内存函数

创建共享内存

shmget函数主要用来创建共享内存。

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

参数

  • key: 共享内存段名字。
  • size:共享内存大小。
  • s hmflg:由九个权限标志构成,它们的用法和创建文件时使用的mode模式标志是一样的。

返回值:成功返回一个非负整数,即该共享内存段的标识码;失败返回-1。

使用ftok函数获取key

  • ftok函数主要通过算法将已经存在的路径和一个项目ID(proj_id)转换为一个随机值(key)。如果我们通过传递参数key来使用shmget创建共享内存,那么该key会被写入到共享内存数据结构之中。
key_t ftok(const char *pathname, int proj_id);

参数

  • pathname: 一定为具有访问权限的路径
  • proj_id 项目ID

注意:

  • 使用ftok函数很有可能会创建失败(发生冲突),此时我们可以修改ftok函数参数重新创建。
  • 如果两个进程之间需要共享内存通信,那么使用在ftok函数来获取key时,必须使用相同的参数。因为只用使用同一种算法,才能获取到同一个key,进而才能识别并匹配到同一个共享内存。

shmget的常见组合方式

  • IPC_CREAT: 如果操作系统内核中不存在与key值相等的共享内存,则新建一个共享内存并返回共享内存的用户层ID。如果存在这样的共享内存,那么直接返回该共享内存的标句柄。————表明不管操作系统中是否含有与key相等的共享内存,都会返回一个共享内存,但是无法确定是否为新建的共享内存。

  • IPC_CREAT | IPC_EXCL: 如果操作系统内核中不存在与key值相等的共享内存,则新建一个共享内存,如果存在,则出错返回。————表明如果成功返回,该共享内存一定是那个新建的共享内存。

实践代码如下
我们创建一个共享内存,并打印该用户层ID和获取到的key值。

int main()
{//创建公共的key值key_t k = ftok(PATH_NAME,PROJ_ID);if (k == -1 ){perror("ftok");}Log("create key done",Debug) << "server key " << k << std::endl;//创建共享内存--必须要创建一个全新的共享内存。int shmid = shmget(k,SHM_SIZE,IPC_CREAT | IPC_EXCL | 0666);if (shmid == -1 ){perror("shmget");}  cout << "该共享内存的句柄 ->" << shmid << endl; }

运行结果如下:
在这里插入图片描述
当然,我们也可以使用ipcs -m 命令来查看共享内存相关信息。(包含共享内存的key和shmid)
在这里插入图片描述

释放共享内存

我们知道,管道的生命周期基于文件,文件的生命周期基于进程,所以管道的生命周期基于进程,当进程退出后,管道也将被释放大。
但是,共享内存的生命周期是基于操作系统内核,并不与进程相关联。所以,当相关进程退出时,共享内存并不会释放,直到关机重启。

如果我们需要及时将共享内存释放,有两种方法:

  • 使用命令释放共享内存
  • 在进程通信后结束进程时使用相关函数进行释放

使用命令行释放共享内存

我们可以通过ipcrm -m shmid 命令将共享内存释放。

在这里插入图片描述

使用shmctl函数释放共享内存

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

参数

  • shmid: 由shmget返回的共享内存标识码
  • cmd: 将要采取的动作
  • buf:指向一个保存着共享内存的模式状态和访问权限的数据结构。为了保证关联共享内存成功,我们一般设置权限为大0666。

返回值
成功返回0,失败返回-1.

以下为shmctl函数中cmd参数常见命令

命令说明
IPC_STAT获取共享内存的当前关联值,此时参数buf作为输出型参数
IPC_SET在进程有足够权限的前提下,将共享内存的当前关联值设置为buf所指的数据结构中的值
IPC_RMID删除共享内存段
int main()
{//创建公共的key值key_t k = ftok(PATH_NAME,PROJ_ID);if (k == -1 ){perror("ftok");}Log("create key done",Debug) << " server key " << k << std::endl;//创建共享内存--必须要创建一个全新的共享内存。int shmid = shmget(k,SHM_SIZE,IPC_CREAT | IPC_EXCL | 0666);if (shmid == -1 ){perror("shmget");}  sleep(10);//删除共享内存。int n = shmctl(shmid,IPC_RMID,NULL);assert( n != -1 );(void)n;Log("delete shm done",Debug) << " shmid " << shmid << endl;}

运行结果如下:
我们使用相关脚本命令监视共享内存,当共享内存创建完毕,shmid值变为1。当进程结束,共享内存释放完毕,shmid值变为0。
在这里插入图片描述

关联共享内存

我们可以通过shmat函数将共享内存段连接到对应的进程地址空间中。

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

参数

  • shmid: 共享内存标识
  • shmaddr: 指定共享内存映射到进程地址空间的某一地址中,通常设置为NULL,表示让操作系统内核决定一个合适的地址位置。
  • shmflg:关联共享内存时设置的某些属性,它的两个可能取值是SHM_RND和SHM_RDONLY,我们一般设置为0,默认设置为读写权限。

返回值
成功:返回指向共享内存的指针;失败:返回-1

以下我们便使用shmat函数对共享内存关联。

int main()
{//创建公共的key值key_t k = ftok(PATH_NAME,PROJ_ID);if (k == -1 ){perror("ftok");}Log("create key done",Debug) << " server key " << k << std::endl;//创建共享内存--必须要创建一个全新的共享内存。int shmid = shmget(k,SHM_SIZE,IPC_CREAT | IPC_EXCL | 0666);if (shmid == -1 ){perror("shmget");}  sleep(10);//关联共享内存char* shmaddr = (char*)shmat(shmid,nullptr,0);Log("attach shm",Debug) << "shmid " << shmid << std::endl;sleep(10);//删除共享内存。int n = shmctl(shmid,IPC_RMID,NULL);assert( n != -1 );(void)n;Log("delete shm done",Debug) << " shmid " << shmid << endl;}

运行结果如下:
此时,我们发现关联该共享内存进程数由0变为1,随着该进程退出,又变为了0。
在这里插入图片描述

取消关联共享内存

我们可以使用shmdt函数将共享内存段与当前进程脱离。

int shmdt(const void *shmaddr);

参数
shmaddr: 由shmat所返回的指向共享内存的指针。
返回值
成功返回0;失败返回-1.

注意
将共享内存段与当前进程脱离不等于删除共享内存段。

我们接下来增加shmdt函数对该共享内存去关联。

int main()
{//创建公共的key值key_t k = ftok(PATH_NAME,PROJ_ID);if (k == -1 ){perror("ftok");}Log("create key done",Debug) << " server key " << k << std::endl;//创建共享内存--必须要创建一个全新的共享内存。int shmid = shmget(k,SHM_SIZE,IPC_CREAT | IPC_EXCL | 0666);if (shmid == -1 ){perror("shmget");}  sleep(10);//关联共享内存。char* shmaddr = (char*)shmat(shmid,nullptr,0);Log("attach shm",Debug) << "shmid " << shmid << std::endl;sleep(10);//共享内存去关联//将指定的内存空间,去关联int n1 = shmdt(shmaddr);assert( n1 != -1 );Log("detach shm",Debug) << " shmid " << shmid << std::endl; sleep(10);//删除共享内存。int n2 = shmctl(shmid,IPC_RMID,NULL);assert( n2 != -1 );(void)n2;Log("delete shm done",Debug) << " shmid " << shmid << endl;}``

通过结果发现,随着shmdt函数去关联,nattch(关联数)也由1变为了0,即表示去关联成功。
在这里插入图片描述

使用共享内存让shmServer和shmClien通信

结论一:只要通信双方使用共享内存,一方直接向共享内存写入数据,另一方,就可以立马看到。因为共享内存是所有进程间通信(IPC)速度最快的,不需要过多的拷贝。

shmServer.cpp

int main()
{//创建公共的key值key_t k = ftok(PATH_NAME,PROJ_ID);if (k == -1 ){perror("ftok");}Log("create key done",Debug) << " server key " << k << std::endl;//创建共享内存--必须要创建一个全新的共享内存。int shmid = shmget(k,SHM_SIZE,IPC_CREAT | IPC_EXCL | 0666);if (shmid == -1 ){perror("shmget");}  sleep(10);//关联共享内存。char* shmaddr = (char*)shmat(shmid,nullptr,0);Log("attach shm",Debug) << "shmid " << shmid << std::endl;sleep(10);//向shamaddr中读取数据。for(;;){// 临界区printf("%s\n", shmaddr);if(strcmp(shmaddr, "quit") == 0) break;sleep(1);}//共享内存去关联//将指定的内存空间,去关联int n1 = shmdt(shmaddr);assert( n1 != -1 );Log("detach shm",Debug) << " shmid " << shmid << std::endl; sleep(10);//删除共享内存。int n2 = shmctl(shmid,IPC_RMID,NULL);assert( n2 != -1 );(void)n2;Log("delete shm done",Debug) << " shmid " << shmid << endl;}

shmClient.cpp

int main()
{//1.创建公共的key值。key_t k = ftok(PATH_NAME,PROJ_ID);assert( k != -1 );Log("create key done",Debug) << "client key " << k << std::endl;//2. 获取共享内存。int shmid = shmget(k, SHM_SIZE, 0);assert( shmid >= 0 );Log("create shm success", Error) << " client key : " << k << endl;//3.关联共享内存。char *shmaddr = (char *)shmat(shmid, nullptr, 0);if(shmaddr == nullptr){Log("attach shm failed", Error) << " client key : " << k << endl;exit(3);}Log("attach shm success", Error) << " client key : " << k << endl;//4.向共享内存写入数据char a = 'a';for(; a <= 'z'; a++){shmaddr[a-'a'] = a;// 我们是每一次都向shmaddr[共享内存的起始地址]写入snprintf(shmaddr, SHM_SIZE - 1,\"hello server, 我是其他进程,我的pid: %d, inc: %c\n",\getpid(), a);sleep(1);}return 0;
}

common.hpp

#include <stdio.h>
//#include <cassert>
#include <sys/types.h> 
#include <iostream>
#include <sys/ipc.h> 
#include <sys/shm.h> 
#include <unistd.h>
#include "log.hpp"
using namespace std;#define PATH_NAME "/home/yzh/test1"
#define PROJ_ID 0X66
#define SHM_SIZE 4096

由运行结果可知,当shmServer端运行,创建好共享内存后,随后运行shmClient端写入数据到共享内存时(每一次写入数据都写入到共享内存的初始地址),由shmServer读取内存数据打印在屏幕上。
在这里插入图片描述

共享内存与管道的区别

1.只要通信双方使用shm,一方直接向共享内存中写入数据,另一方就立马可以从共享内存中获取,因为相比于管道而言,共享内存是所有进程通信(IPC)中速度最快的。

管道通信过程
当我们使用管道通信,需要调用read,write接口进行数据传输。而在这个过程中,需要进行四次拷贝

  • 服务端读取输入文件中的数据到临时缓冲区中。
  • 服务端将临时缓冲区中的数据写入到管道文件中。
  • 客户端读取管道文件中的数据读取到临时缓冲区中
  • 客户端将临时缓冲区的数据写入到输出文件中。
    在这里插入图片描述
    共享内存通信过程

我们很明显的可以看出,两个进程间使用共享内存通信之间并不需要在客户端和服务端中建立临时缓冲区传输数据,所以只需要两次拷贝。
在这里插入图片描述

2.相比于管道而言,共享内存缺乏访问控制,会带来并发问题

并发问题,有可能会带来当一端还在写入数据时,还没有将数据写完,但是另一端就立马读取的问题。

为此,我们可以在shmServer端和shmClient端通信时增加一个命名管道通信(通信内容不重要),此时相当于也让两端在共享内存中通信时也具备着命名管道中进程控制的性质。

common.hpp
我们让命名管道在程序运行时就已经创建了,在进程退出时自动销毁并且对它一些函数进行包装。

#include <stdio.h>
//#include <cassert>
#include <sys/types.h> 
#include <iostream>
#include <sys/ipc.h> 
#include <sys/shm.h> 
#include <unistd.h>
#include <assert.h>
#include "log.hpp"
#include <string.h>
#include <fcntl.h>
#include <sys/stat.h>using namespace std;#define FIFO_NAME "myfifo"
#define PATH_NAME "/home/yzh/test1"
#define PROJ_ID 0X66
#define SHM_SIZE 4096#define READ  O_RDONLY
#define WRITE O_WRONLYclass Init
{
public:Init(){umask(0);int n = mkfifo(FIFO_NAME, 0666);assert(n == 0);(void)n;Log("create fifo success",Notice) << "\n";}~Init(){unlink(FIFO_NAME);Log("remove fifo success",Notice) << "\n";}
};#define READ O_RDONLY
#define WRITE O_WRONLYint OpenFIFO(std::string pathname, int flags)
{int fd = open(pathname.c_str(), flags);assert(fd >= 0);return fd;
}void Wait(int fd)
{Log("等待中....", Notice) << "\n";uint32_t temp = 0;ssize_t s = read(fd, &temp, sizeof(uint32_t));assert(s == sizeof(uint32_t));(void)s;
}void Signal(int fd)
{uint32_t temp = 1;ssize_t s = write(fd, &temp, sizeof(uint32_t));assert(s == sizeof(uint32_t));(void)s;Log("唤醒中....", Notice) << "\n";
}void CloseFifo(int fd)
{close(fd);
}

shmServer.cpp
在通信时,我们先能够读取命名管道中的数据,才能读取共享额你存中的数据。

Init init;
int main()
{Log("child pid is : ", Debug) << getpid() << endl;key_t k = ftok(PATH_NAME, PROJ_ID);if (k < 0){Log("create key failed", Error) << " client key : " << k << endl;exit(1);}Log("create key done", Debug) << " client key : " << k << endl;// 获取共享内存int shmid = shmget(k, SHM_SIZE, IPC_CREAT | IPC_EXCL | 0666); //if(shmid < 0){Log("create shm failed", Error) << " client key : " << k << endl;exit(2);}Log("create shm success", Error) << " client key : " << k << endl;// sleep(10);char *shmaddr = (char *)shmat(shmid, nullptr, 0);if(shmaddr == nullptr){Log("attach shm failed", Error) << " client key : " << k << endl;exit(3);}Log("attach shm success", Error) << " client key : " << k << endl;int fd = OpenFIFO(FIFO_NAME, READ);for(;;){Wait(fd);   // 临界区printf("%s\n", shmaddr);if(strcmp(shmaddr, "quit") == 0) break;// sleep(1);}// 去关联int n = shmdt(shmaddr);assert(n != -1);Log("detach shm success", Error) << " client key : " << k << endl;// sleep(10);// client 要不要chmctl删除呢?不需要!!return 0;
}

shmServer.cpp
当shmServer运行,并且输入数据,才能激活命名管道。

int main()
{Log("child pid is : ", Debug) << getpid() << endl;key_t k = ftok(PATH_NAME, PROJ_ID);if (k < 0){Log("create key failed", Error) << " client key : " << k << endl;exit(1);}Log("create key done", Debug) << " client key : " << k << endl;// 获取共享内存if(shmid < 0){Log("create shm failed", Error) << " client key : " << k << endl;exit(2);}Log("create shm success", Error) << " client key : " << k << endl;// sleep(10);char *shmaddr = (char *)shmat(shmid, nullptr, 0);if(shmaddr == nullptr){Log("attach shm failed", Error) << " client key : " << k << endl;exit(3);}Log("attach shm success", Error) << " client key : " << k << endl;// sleep(10);int fd = OpenFIFO(FIFO_NAME, WRITE);// 使用// client将共享内存看做一个char 类型的bufferwhile(true){ssize_t s = read(0, shmaddr, SHM_SIZE-1);if(s > 0){shmaddr[s-1] = 0;Signal(fd);if(strcmp(shmaddr,"quit") == 0) break;}}close(fd);// 去关联int n = shmdt(shmaddr);assert(n != -1);Log("detach shm success", Error) << " client key : " << k << endl;// sleep(10);// client 要不要chmctl删除呢?不需要!!return 0;
}

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

浅谈 system V信号量

在之前进程通信的几种方式中,本质上都是优先解决一个问题,让不同的进程看到同一份资源,比如共享内存,也带来了一些时序问题,造成两端数据不一致问题。

信号量的相关概念。

  • 临界资源: 多个进程(执行流)看到的公共的一份资源。
  • 临界区: 进程需要访问访问临界资源的代码段
  • 互斥:为了地对临界区的保护,在任何时刻,可以让多执行流都只能一个进程进入临界区。(为了不让进程之间互相打扰)
  • 原子性:信号量没有中间状态,要么不做,要么做完。

信号量的原理

每一个进程想访问临界资源,必须先申请信号量,而信号量本质上就是一个计数器(类似于 int count )。当申请信号量成功,本质上就是让信号量计数器–,当信号量减为0时,就不能再增加进程访问临资源了。

能不能用一个整数(n)去标识信号量呢?

cpu执行命令流程:
1.将内存中的数据加载到cpu内的寄存器中(读指令)

2:申请信号量( 分析 & 执行指令 )——> n–;

3:将CPU修改完毕的n写回内存。

然而执行流CPU执行命令的时候,每一步都有可能被另一个进程切换,此时寄存器中包含着执行流的上下文数据,被整个执行流所共享。

假设n = 2;
如果当一个进程正在执行到第2步( n = 1 ) ,然而又被另一个进程切换到第一步( 此时 n 又变成了 2 )并且执行完第三步,n 又变成了1,原本n = 2意味着只能让2个进程访问临界资源的某一部分,执行完毕后,n = 1,表明这还能容纳一个进程访问临界资源。

这就是说明整数count导致的时序问题,此时n有中间状态,进而造成数据并不一致问题。

信号量计数器

申请信号量 - > 计数器-- -> P操作(必须是具有原子性)
释放信号量 -> 计数器++ -> V操作(必须是原子性)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/49037.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

算法基础(1):排序和查找算法

1、排序算法 1.1、堆排序(大顶堆)-重点&#xff1a; 参考文章&#xff1a;堆排序1、堆排序二 前置知识&#xff1a; 大顶堆&#xff1a;完全二叉树&#xff0c;且父节点大于左右儿子&#xff0c;左右子树又是大顶堆&#xff0c;依赖数组来实现(vector)第一个节点的父节点&…

【Rust】Rust学习 第十六章无畏并发

安全且高效的处理并发编程是 Rust 的另一个主要目标。并发编程&#xff08;Concurrent programming&#xff09;&#xff0c;代表程序的不同部分相互独立的执行&#xff0c;而 并行编程&#xff08;parallel programming&#xff09;代表程序不同部分于同时执行&#xff0c;这两…

C# 反编译dll文件,将dll文件转为cs文件

工具名&#xff1a;reflector&#xff0c;可免费使用 工具下载地址使用方法 工具下载地址 下载地址&#xff1a; https://www.red-gate.com/products/reflector/ 使用方法 下载安装&#xff1b; 打开软件 打开dll文件 侧边栏找打刚打开的dll文件&#xff0c;直接就能导…

Redisson 分布式锁

Redis是基础客户端库&#xff0c;可用于执行基本操作。 Redisson是基于Redis的Java客户端&#xff0c;提供高级功能如分布式锁、分布式集合和分布式对象。 Redisson提供更友好的API&#xff0c;支持异步和响应式编程&#xff0c;提供内置线程安全和失败重试机制。 实现步骤…

密码学学习笔记(十八):Diffie–Hellman (DH) 密钥交换

DH算法是第一个密钥交换算法&#xff0c;也是第一个得到形式化描述的公钥密码算法。 群论 DH密钥交换算法基于数学中的群论&#xff0c;群论也是当今大多数公钥密码的基础。 要使集合及其运算成为一个群&#xff0c;需要满足以下性质&#xff1a; 封闭性&#xff1a;群中两…

使用three.js与WebGL相比有什么优势?

推荐&#xff1a;使用 NSDT场景编辑器 助你快速搭建可二次编辑的3D应用场景 特点 WebGL和Three.js是两个与网页3D图形渲染相关的技术。以下是它们的一些特点&#xff1a; WebGL: 基于OpenGL ES标准&#xff0c;为网页提供了一种基于GPU加速的图形渲染API。具有高性能的图像处…

【从零学习python 】75. TCP协议:可靠的面向连接的传输层通信协议

文章目录 TCP协议TCP通信的三个步骤TCP特点TCP与UDP的区别TCP通信模型进阶案例 TCP协议 TCP协议&#xff0c;传输控制协议&#xff08;英语&#xff1a;Transmission Control Protocol&#xff0c;缩写为 TCP&#xff09;是一种面向连接的、可靠的、基于字节流的传输层通信协议…

在思科(Cisco)设备上配置 DHCP 服务器

DHCP广泛用于LAN环境中&#xff0c;从集中式服务器动态分配主机IP地址&#xff0c;从而显着减少IP地址管理的开销。DHCP 还有助于节省有限的 IP 地址空间&#xff0c;因为不再需要将 IP 地址永久分配给主机&#xff0c;只有连接到网络的主机才会使用 IP 地址。DHCP 服务器将路由…

网络聊天室

一、项目要求 利用UDP协议&#xff0c;实现一套聊天室软件。服务器端记录客户端的地址&#xff0c;客户端发送消息后&#xff0c;服务器群发给各个客户端软件。 问题思考 客户端会不会知道其它客户端地址&#xff1f; UDP客户端不会直接互连&#xff0c;所以不会获知其它客…

详解使用SSH远程连接Ubuntu服务器系统

演示环境&#xff1a; 1.Windows10系统 2.VMware Workstation Pro虚拟机 2.Ubuntu16.04.6&#xff08;以上版本通用&#xff09; 回归正题 一、在Ubuntu端&#xff1a; 1.首先需要安装SSH服务器&#xff0c;在ubuntu终端输入以下指令 sudo apt-get install ssh2.输入你的ubu…

判断三角形

int main() {int a 0;int b 0;int c 0;scanf("%d%d%d", &a, &b, &c);if ((ab>c)&&(ac>b)&&(bc>a)){if (a b && b c){printf("等边三角形\n");}else if ((a b && b ! c) || (a c && c…

网约车接单神器:智能化技术与出行服务的完美结合

随着移动互联网的迅猛发展&#xff0c;网约车行业成为现代出行方式的主流之一。为了提高用户体验和服务效率&#xff0c;网约车接单神器应运而生。本文将探讨网约车接单神器的专业性、思考深度和逻辑性&#xff0c;以及其与智能化技术和出行服务的完美结合。 一、引言&…

leetcode 309. 买卖股票的最佳时机含冷冻期

2023.8.22 本题是买卖股票系列 冷冻期。 由于引入了冷冻期&#xff0c;并且这个冷冻期是在卖出股票才会出现&#xff0c;因此我dp数组设置了四种状态&#xff1a; 状态一&#xff1a;持有股票。状态二&#xff1a;不持有股票&#xff1a; 之前就卖了&#xff0c;所以今天不处…

论AI与大数据之间的关系

前言 在21世纪&#xff0c;"AI"和"大数据"已经成为科技领域的热门词汇。它们不仅是创新的代名词&#xff0c;更是现代技术发展的双翼。然而&#xff0c;很多人对于AI与大数据之间的关系仍然停留在表面的理解。本文旨在深入探讨这两者之间的深厚关系&#…

设置Windows主机的浏览器为wls2的默认浏览器

1. 准备工作 wsl是可以使用Windows主机上安装的exe程序&#xff0c;出于安全考虑&#xff0c;默认情况下改功能是无法使用。要使用的话&#xff0c;终端需要以管理员权限启动。 我这里以Windows Terminal为例&#xff0c;介绍如何默认使用管理员权限打开终端&#xff0c;具体…

[LeetCode111双周赛LeetCode359周赛] DP双指针

参考灵神和闫总的讲解和代码&#xff1a; https://www.bilibili.com/video/BV1rP411s7Z5 https://space.bilibili.com/206214 7006. 销售利润最大化 https://leetcode.cn/problems/maximize-the-profit-as-the-salesman/ Solution 动态规划 哈希表 首先按照 end 的顺序分…

kafka-- kafka集群 架构模型职责分派讲解

一、 kafka集群 架构模型职责分派讲解 生产者将消息发送到相应的Topic&#xff0c;而消费者通过从Topic拉取消息来消费 Kafka奇数个节点消费者consumer会将消息拉去过来生产者producer会将消息发送出去数据管理 放在zookeeper

jmeter HTTP请求默认值

首先&#xff0c;打开JMeter并创建一个新的测试计划。 右键单击测试计划&#xff0c;选择"添加" > “配置元件” > “HTTP请求默认值”。 在HTTP请求默认值中&#xff0c;您可以设置全局的HTTP请求属性&#xff0c;例如&#xff1a; 服务器地址&#xff1a…

数据结构——队列(C语言)

需求&#xff1a;无 本篇文章将解决一下几个问题&#xff1a; 队列是什么&#xff1f;如何实现一个队列&#xff1f;什么场景下会用队列&#xff1f; 队列的概念&#xff1a; 队列&#xff1a;一种只允许一端进行插入数据操作&#xff0c;在另一端进行删除操作的特殊线性表。…

Redis数据结构之List

Redis 中列表&#xff08;List&#xff09;类型是用来存储多个有序的字符串&#xff0c;列表中的每个字符串成为元素 Eelement&#xff09;&#xff0c;一个列表最多可以存储 2^32-1 个元素。 在 Redis 中&#xff0c;可以对列表两端插入&#xff08;push&#xff09;和弹出&am…