Linux(八) 进程间通信

目录

一、什么进程间通信

1.1 进程间通信的目的

1.2 进程间通信的概念

1.3 进程间通信的分类

二、 管道/匿名管道(pipe)

2.1 什么是管道

 2.2 管道的创建

2.3 站在文件描述符角度-深度理解管道

2.4 站在内核角度-管道本质

2.5 匿名管道的读写

2.6 匿名管道的读写规则

三、 命名管道(FIFO)

3.1 创建一个命名管道

3.2 匿名管道与命名管道的区别

3.3 命名管道的打开规则

3.4 用命名管道实现server&client通信

四、system V 共享内存

4.1 什么是共享内存

4.2 共享内存函数

4.3 实例代码

五、信号量和PV操作

5.1 进程互斥

5.2 使用信号量的基本流程 

5.3 对PV操作的理解

5.4 PV操作实现进程同步伪代码


一、什么进程间通信

1.1 进程间通信的目的

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

1.2 进程间通信的概念

顾名思义,进程通信( InterProcess Communication,IPC)就是指进程之间的信息交换。实际上,进程的同步与互斥本质上也是一种进程通信(这也就是待会我们会在进程通信机制中看见信号量和 PV 操作的原因了),只不过它传输的仅仅是信号量,通过修改信号量,使得进程之间建立联系,相互协调和协同工作,但是它缺乏传递数据的能力

虽然存在某些情况,进程之间交换的信息量很少,比如仅仅交换某个状态信息,这样进程的同步与互斥机制完全可以胜任这项工作。但是大多数情况下,进程之间需要交换大批数据,比如传送一批信息或整个文件,这就需要通过一种新的通信机制来完成,也就是所谓的进程通信。

再来从操作系统层面直观的看一些进程通信:我们知道,为了保证安全,每个进程的用户地址空间都是独立的,一般而言一个进程不能直接访问另一个进程的地址空间,不过内核空间是每个进程都共享的,所以进程之间想要进行信息交换就必须通过内核

进程间通信的本质:让不同的进程看见同一份资源 

所谓同一份资源,不能隶属于任何一个进程,更应该强调共享。

1.3 进程间通信的分类

管道 Linux系统原生提供的

  • 匿名管道pipe
  • 命名管道

System V IPC - 多进程 -单机通信

  • System V 消息队列 - 不常用
  • System V 共享内存
  • System V 信号量

POSIX IPC - 多线程 - 网络通信

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

二、 管道/匿名管道(pipe)

2.1 什么是管道

Linux 管道使用竖线 | 连接多个命令,这被称为管道符。
 

$ command1 | command2

以上这行代码就组成了一个管道,它的功能是将前一个命令(command1)的输出,作为后一个命令(command2)的输入,从这个功能描述中,我们可以看出管道中的数据只能单向流动,也就是半双工通信,如果想实现相互通信(全双工通信),我们需要创建两个管道才行。

另外,通过管道符 | 创建的管道是匿名管道,用完了就会被自动销毁。并且,匿名管道只能在具有亲缘关系(父子进程)的进程间使用。也就是说,匿名管道只能用于父子进程之间的通信
一般而言,进程退出,管道释放,管道的生命周期随进程。
内核会对管道操作进行同步和互斥

 2.2 管道的创建

在 Linux 的实际编码中,是通过 pipe 函数来创建匿名管道的,若创建成功则返回 0,创建失败就返回 -1:

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

对应代码 

#include <iostream>
#include <cstdio>
#include <string>
#include <cstring>
#include <sys/wait.h>
#include <sys/types.h>
#include <assert.h>
#include <unistd.h>
using namespace std;
int main()
{//  1.创建管道int pipefd[2] = {0};int n = pipe(pipefd);// pipefd[0]:读 pipefd[1]:写assert(n != 1); // 只在degug模式里是有效的(void)n; // 如果没有这句代码那n就是只被定义没有被使用,在release下编译就会产生大量告警情况,为了防止告警,就加上这句代码
#ifdef DEBUG // 条件编译 gcc 里加 -DDEBUG 才会执行此代码cout << "pipefd[0]:" << pipefd[0] << endl;cout << "pipefd[1]:" << pipefd[1] << endl;
#endif// 2.创建子进程pid_t id = fork();assert(id != -1);if(id == 0){// 子进程 - 读// 3.构建单向通信管道,父进程写入,子进程读取// 3.1 关闭子进程不需要的fdclose(pipefd[1]);char buffer[1024];while(true){ssize_t s =  read(pipefd[0],buffer,sizeof(buffer)-1);if(s > 0){// buffer[s]cout << "i am a child,pid:" << getpid() << "读到的数据:" << buffer << endl;}}// close(pipefd[0]);exit(0);}// 父进程 - 写// 3.构建单向通信管道,父进程写入,子进程读取// 3.1 关闭父进程不需要的fdclose(pipefd[0]);string message = "我是父进程,我给子进程发消息";int count = 0;char send_buffer[1024];while(true){// 3.2 构建一个变化的字符串snprintf(send_buffer,sizeof(send_buffer),"%s[%d] : %d",message.c_str(),getpid(),count++);// 3.3 写入write(pipefd[1],send_buffer,strlen(send_buffer));// 3.4 故意sleepsleep(1);}close(pipefd[1]);pid_t ret = waitpid(id,nullptr,0);assert(ret > 0);(void)ret;return 0;
}

管道的本质就是内核在内存中开辟了一个缓冲区,这个缓冲区与管道文件相关联,对管道文件的操作,被内核转换成对这块缓冲区的操作。 

2.3 站在文件描述符角度-深度理解管道

2.4 站在内核角度-管道本质

  • 在Linux中,匿名管道的实现并没有使用专门的数据结构,而是借助了文件系统的file结构,和VFS的索引节点inode。
  • 通过将两个file结构指向同一个临时的VFS节点,而这个VFS索引节点又指向了一个物理页面而实现的。

调用pipe()打开的两个文件描述符,对应的file结构体里只有f_op有差别,一个是从该文件里读,一个是写,其余没有区别。

那么以此延申,如果同一份文件在同一进程打开多次,内核是怎样维护的呢?

同一文件在不同进程内打开,内核是怎样维护的呢?

Linux应用编程之多次打开同一个文件 - yooooooo - 博客园 (cnblogs.com)

2.5 匿名管道的读写

  • 匿名管道的实现的源代码在fs/pipe.c中,在pipe.c中有很多函数,其中有两个函数比较重要,即匿名管道pipe_read()读函数和匿名管道写函数pipe_write()。
  • 匿名管道写函数通过将字节复制到VFS索引节点指向物理内存而写入数据,而匿名管道读函数则通过复制物理内存而读出数据。
    当然,内核必须利用一定的同步机制对管道的访问,为此内核使用了等待队列、和信号
  • 当写入进程向匿名管道中写入时,它利用标准的库函数write(),系统根据库函数传递的文件描述符,可找到该文件的file结构。
  • file结构中制定了用来进行写操作的函数(即写入函数)地址,于是,内核调用该函数完成写操作。
  • 写入函数在向内存中写入数据之前,必须首先检查VFS索引节点中的信息,同时满足如下条件时,才能进行实际的内存复制工作
  • 内存中有足够的空间可以容纳所有要写入的数据。
  • 内存没有被读程序锁定。
  • 如果同时满足上述条件,写入函数首先会锁定内存,然后从写进程的地址空间中复制数据到内存。
  • 否则,写进程就休眠在VFS索引节点的等待队列中,接下来,内核将调用调度程序,而调度程序会选择其他进程运行。
  • 写进程实际处于可中断的等待状态,当内存中有足够的空间可以容纳写入数据,或内存被解锁时,读取进程会唤醒写入进程,这时,写入进程将接受到信号。当数据写入内存之后,内存被解锁,而所有休眠在索引节点的读取进程会被唤醒。
  • 进程可以在没有数据或者内存被锁定时立即返回错误信息,而不是阻塞该进程,这一来于文件或管道的打开模式。
  • 进程可以休眠在索引节点的等待队列中等待写入进程写入数据。当所有的进程完成了管道操作之后,管道的索引节点被丢弃,而共享数据页被释放。

2.6 匿名管道的读写规则

管道读写规则

当没有数据可读时

  • 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将不再保证写入的原子性。
 

三、 命名管道(FIFO)

管道应用的一个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信。
如果我们想在不相关的进程之间交换数据,可以使用FIFO文件来做这项工作,它经常被称为命名管道。
命名管道是一种特殊类型的文件.
命名管道可以被打开,但是不会将内存数据刷新到磁盘。

3.1 创建一个命名管道

命名管道可以从命令行上创建,命令行方法是使用下面这个命令:

mkfifo filename

命名管道也可以从程序里创建,相关函数有:

int mkfifo(const char *filename,mode_t mode);

3.2 匿名管道与命名管道的区别

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

3.3 命名管道的打开规则

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

3.4 用命名管道实现server&client通信

服务端

#include "comm.hpp"
#include "log.hpp"string KeyToHex(key_t k)
{char buffer[32];snprintf(buffer,sizeof buffer ,"0x%x",k);return buffer;
}
// 是不是对应的程序,在加载的时候,会自动构建全局变量,就要调用该类的构造函数 -- 创建管道文件
// 程序退出的时候,全局变量会被析构,自动调用析构函数,会自动删除管道文件
Init init;int main()
{// 我们之前为了通信,所做的所有的工作,属于什么工作呢:让不同的进程看到了同一份资源(内存)// 1.创建公共的key值key_t key = ftok(PATH_NAME,PROJ_ID);assert(key != -1);log("创建key值成功",DEBUG) << "server key =" <<  KeyToHex(key) << endl;// 2. 创建共享内存 -- 建议要创建一个全新的共享内存 -- 通信的发起者int shmid = shmget(key,SHM_SIZE,IPC_CREAT | IPC_EXCL | 0666);if(shmid == -1){perror("shmget");exit(1);}log("创建共享内存成功",DEBUG) << "shmid =" << shmid << endl;//sleep(8);// 3. 将指定的共享内存,挂接到自己的地址空间char* shmaddr = (char*)shmat(shmid,nullptr,0);if(shmaddr == nullptr){perror("shmaddr");exit(2);}log("共享内存与地址空间映射建立成功",DEBUG) << "shmid =" << shmid << endl;    //sleep(8);// 这里就是通信的逻辑了// 接收// 将共享内存当成一个大字符串// char buffer[SHM_SIZE];// 结论1: 只要是通信双方使用shm,一方直接向共享内存中写入数据,另一方,就可以立马看到对方写入的数据。//         共享内存是所有进程间通信(IPC),速度最快的!不需要过多的拷贝!!(不需要将数据给操作系统)// 结论2: 共享内存缺乏访问控制!会带来并发问题 【如果我想一定程度的访问控制呢? 能】// 自动发送的接受int fd = OpenFIFO(FIFO_NAME,READ);while(true){wait(fd);printf("server :%s\n",shmaddr);if(strcmp(shmaddr,"quit") == 0){break;}sleep(1);}// 4. 将指定的共享内存,从自己的地址空间中去关联int ret = shmdt(shmaddr);if(ret == -1){perror("shmdt");exit(3);}log("共享内存与地址空间去关联成功",DEBUG) << "shmid =" << shmid << endl;  //sleep(8);// 5. 删除共享内存,IPC_RMID即便是有进程和当下的shm挂接,依旧删除共享内存ret = shmctl(shmid,IPC_RMID,nullptr);if(ret == -1){perror("shmctl");exit(3);}log("共享内存删除成功",DEBUG) << "shmid =" << shmid << endl;  closefd(fd);// client 要不要chmctl删除呢?不需要!!return 0;
}

客户端

#include "comm.hpp"
#include "log.hpp"int main()
{key_t key = ftok(PATH_NAME, PROJ_ID);assert(key != -1);(void)key;log("创建key值成功", DEBUG) << "client:key =" << key << endl;// 获取共享内存int shmid = shmget(key, SHM_SIZE, 0);if (shmid == -1){perror("shmget");exit(1);}log("获取共享内存成功", DEBUG) << "shmid =" << shmid << endl;//sleep(8);char *shmaddr = (char *)shmat(shmid, nullptr, 0);if (shmaddr == nullptr){perror("shmaddr");exit(2);}log("共享内存与地址空间映射建立成功", DEBUG) << "shmid =" << shmid << endl;//sleep(8);// 使用// 自动发送// for(int i = 'a';i < 'c';i++)// {//     snprintf(shmaddr,SHM_SIZE,"hello world char[%c]",i);//     sleep(2);// }// strcpy(shmaddr,"quit");// 手动发送// client将共享内存看做一个char 类型的bufferint fd = OpenFIFO(FIFO_NAME,WRITE);while(true){ssize_t s = read(0,shmaddr,SHM_SIZE - 1);if(s > 0){shmaddr[s-1] = '\0';send(fd);if(strcmp(shmaddr,"quit") == 0)break;}}int ret = shmdt(shmaddr);if (ret == -1){perror("shmdt");exit(3);}log("共享内存与地址空间去关联成功", DEBUG) << "shmid =" << shmid << endl;closefd(fd);return 0;
}

comm.hpp 

#ifndef _COMM_H_
#define _COMM_H_#include <iostream>
#include <cstdio>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/stat.h>
#include <cassert>
#include <string.h>
#include <fcntl.h>
#include "log.hpp"using namespace std; //不推荐#define PATH_NAME "/home/dgz"
#define PROJ_ID 0x66
#define SHM_SIZE 4096 //共享内存的大小,最好是页(PAGE: 4096)的整数倍 
#define FIFO_NAME "./fifo"
#define READ O_RDONLY
#define WRITE O_WRONLY
class Init
{
public:Init(){umask(0);int s = mkfifo(FIFO_NAME,0666);assert(s != -1);(void)s;log("创建管道文件成功",DEBUG) << endl;}~Init(){unlink(FIFO_NAME);log("移除管道文件成功",DEBUG) << endl;}};int OpenFIFO(string pathname,int flags)
{int fd = open(pathname.c_str(),flags);assert(fd >= 0);return fd;
}void wait(int fd)
{log("等待中....",DEBUG) << endl;uint32_t tmp = 0;ssize_t s = read(fd,&tmp,sizeof(uint32_t));assert(s == sizeof (uint32_t));(void)s;
}void send(int fd)
{uint32_t tmp = 0;ssize_t s = write(fd,&tmp,sizeof(uint32_t));assert(s == sizeof (uint32_t));(void)s;log("唤醒中",DEBUG) << endl;
}void closefd(int fd)
{close(fd);
}#endif

四、system V 共享内存

4.1 什么是共享内存

顾名思义,共享内存就是允许不相干的进程将同一段物理内存连接到它们各自的地址空间中,使得这些进程可以访问同一个物理内存,这个物理内存就成为共享内存。如果某个进程向共享内存写入数据,所做的改动将立即影响到可以访问同一段共享内存的任何其他进程。

集合内存管理的内容,我们来深入理解下共享内存的原理。首先,每个进程都有属于自己的进程控制块(PCB)和逻辑地址空间(Addr Space),并且都有一个与之对应的页表,负责将进程的逻辑地址(虚拟地址)与物理地址进行映射,通过内存管理单元(MMU)进行管理。两个不同进程的逻辑地址通过页表映射到物理空间的同一区域,它们所共同指向的这块区域就是共享内存
共享内存在用户区,不需要调用系统接口进入内核区,传输速度很快,效率很高

操作系统提供的共享内存,同时操作系统也要管理共享内存
共享内存 = 共享内存块 + 对应的共享内存的内核数据结构
共享内存的内核数据结构

 

 struct shmid_ds {//IPC对象都有struct ipc_perm shm_perm;    /* Ownership and permissions *///共享内存所特有size_t          shm_segsz;   /* Size of segment (bytes) */共享内存段的大小time_t          shm_atime;   /* Last attach time */最后一次映射共享内存的时间time_t          shm_dtime;   /* Last detach time */最后一次解除映射的时间time_t          shm_ctime;   /* Last change time */最后一次共享内存状态改变的时间pid_t           shm_cpid;    /* PID of creator */共享内存创建者的号码pid_t           shm_lpid;    /* PID of last shmat(2)/shmdt(2) */最后一次连接/脱离共享内存的号码shmatt_t        shm_nattch;  /* No. of current attaches */当前共享内存被连接的次数...};

4.2 共享内存函数

shmget函数
功能:用来创建共享内存
原型
int shmget(key_t key, size_t size, int shmflg);
参数
key:这个共享内存段名字,要通信的双方怎么保证看到的是同一块共享内存呢,通过key来实现,key是几不重要,只要能保证系统中唯一存在即可
size:共享内存大小
shmflg:由九个权限标志构成,它们的用法和创建文件时使用的mode模式标志是一样的

IPC_CREAT单独使用,创建共享内存,如果底层存在,则获取之,并且返回,如果不存在则创建并返回。

IPC_CREAT和IPC_EXCL,底层不存在,创建并返回,底层存在,出错返回,返回成功一定是一个全新的共享内存
返回值:成功返回一个非负整数,即该共享内存段的标识码,类似于fd;失败返回-1

ftok函数 

功能:创建key值

原型

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

shmat函数
功能:将共享内存段连接到进程地址空间
原型
void *shmat(int shmid, const void *shmaddr, int shmflg);
参数
shmid: 共享内存标识
shmaddr:指定连接的地址,虚拟地址空间
shmflg:它的两个可能取值是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,表示连接操作用来只读共享内存

shmdt函数
功能:将共享内存段与当前进程脱离
原型
int shmdt(const void *shmaddr);
参数
shmaddr: 由shmat所返回的指针
返回值:成功返回0;失败返回-1
注意:将共享内存段与当前进程脱离不等于删除共享内存段

shmctl函数
功能:用于控制共享内存
原型
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数
shmid:由shmget返回的共享内存标识码
cmd:将要采取的动作(有三个可取值)
buf:指向一个保存着共享内存的模式状态和访问权限的数据结构
返回值:成功返回0;失败返回-1 

当进程结束,我们的共享内存还存在,system V IPC资源,生命周期随内核,除非重启
1.手动删除 ipcrm -m shmid #删除id为shmid的共享内存
2.代码删除 shmctl 

shmid与key
只有创建共享内存的时候用key,大部分情况下用户访问共享内存都用的是shmid

4.3 实例代码

server
 

#include "comm.hpp"
#include "log.hpp"string KeyToHex(key_t k)
{char buffer[32];snprintf(buffer,sizeof buffer ,"0x%x",k);return buffer;
}
// 是不是对应的程序,在加载的时候,会自动构建全局变量,就要调用该类的构造函数 -- 创建管道文件
// 程序退出的时候,全局变量会被析构,自动调用析构函数,会自动删除管道文件
Init init;int main()
{// 我们之前为了通信,所做的所有的工作,属于什么工作呢:让不同的进程看到了同一份资源(内存)// 1.创建公共的key值key_t key = ftok(PATH_NAME,PROJ_ID);assert(key != -1);log("创建key值成功",DEBUG) << "server key =" <<  KeyToHex(key) << endl;// 2. 创建共享内存 -- 建议要创建一个全新的共享内存 -- 通信的发起者int shmid = shmget(key,SHM_SIZE,IPC_CREAT | IPC_EXCL | 0666);if(shmid == -1){perror("shmget");exit(1);}log("创建共享内存成功",DEBUG) << "shmid =" << shmid << endl;//sleep(8);// 3. 将指定的共享内存,挂接到自己的地址空间char* shmaddr = (char*)shmat(shmid,nullptr,0);if(shmaddr == nullptr){perror("shmaddr");exit(2);}log("共享内存与地址空间映射建立成功",DEBUG) << "shmid =" << shmid << endl;    //sleep(8);// 这里就是通信的逻辑了// 接收// 将共享内存当成一个大字符串// char buffer[SHM_SIZE];// 结论1: 只要是通信双方使用shm,一方直接向共享内存中写入数据,另一方,就可以立马看到对方写入的数据。//         共享内存是所有进程间通信(IPC),速度最快的!不需要过多的拷贝!!(不需要将数据给操作系统)// 结论2: 共享内存缺乏访问控制!会带来并发问题 【如果我想一定程度的访问控制呢? 能】// 自动发送的接受// 加了一层访问控制int fd = OpenFIFO(FIFO_NAME,READ);while(true){wait(fd);printf("server :%s\n",shmaddr);if(strcmp(shmaddr,"quit") == 0){break;}sleep(1);}// 4. 将指定的共享内存,从自己的地址空间中去关联int ret = shmdt(shmaddr);if(ret == -1){perror("shmdt");exit(3);}log("共享内存与地址空间去关联成功",DEBUG) << "shmid =" << shmid << endl;  //sleep(8);// 5. 删除共享内存,IPC_RMID即便是有进程和当下的shm挂接,依旧删除共享内存ret = shmctl(shmid,IPC_RMID,nullptr);if(ret == -1){perror("shmctl");exit(3);}log("共享内存删除成功",DEBUG) << "shmid =" << shmid << endl;  closefd(fd);// client 要不要chmctl删除呢?不需要!!return 0;
}

client
 

#include "comm.hpp"
#include "log.hpp"int main()
{key_t key = ftok(PATH_NAME, PROJ_ID);assert(key != -1);(void)key;log("创建key值成功", DEBUG) << "client:key =" << key << endl;// 获取共享内存int shmid = shmget(key, SHM_SIZE, 0);if (shmid == -1){perror("shmget");exit(1);}log("获取共享内存成功", DEBUG) << "shmid =" << shmid << endl;//sleep(8);char *shmaddr = (char *)shmat(shmid, nullptr, 0);if (shmaddr == nullptr){perror("shmaddr");exit(2);}log("共享内存与地址空间映射建立成功", DEBUG) << "shmid =" << shmid << endl;//sleep(8);// 使用// 自动发送// for(int i = 'a';i < 'c';i++)// {//     snprintf(shmaddr,SHM_SIZE,"hello world char[%c]",i);//     sleep(2);// }// strcpy(shmaddr,"quit");// 手动发送// client将共享内存看做一个char 类型的buffer// 加了一层访问控制int fd = OpenFIFO(FIFO_NAME,WRITE);while(true){ssize_t s = read(0,shmaddr,SHM_SIZE - 1);if(s > 0){shmaddr[s-1] = '\0';send(fd);if(strcmp(shmaddr,"quit") == 0)break;}}int ret = shmdt(shmaddr);if (ret == -1){perror("shmdt");exit(3);}log("共享内存与地址空间去关联成功", DEBUG) << "shmid =" << shmid << endl;closefd(fd);return 0;
}

comm.hpp 
 

#ifndef _COMM_H_
#define _COMM_H_#include <iostream>
#include <cstdio>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/stat.h>
#include <cassert>
#include <string.h>
#include <fcntl.h>
#include "log.hpp"using namespace std; //不推荐#define PATH_NAME "/home/dgz"
#define PROJ_ID 0x66
#define SHM_SIZE 4096 //共享内存的大小,最好是页(PAGE: 4096)的整数倍 
#define FIFO_NAME "./fifo"
#define READ O_RDONLY
#define WRITE O_WRONLY
// 加了一层访问控制
class Init
{
public:Init(){umask(0);int s = mkfifo(FIFO_NAME,0666);assert(s != -1);(void)s;log("创建管道文件成功",DEBUG) << endl;}~Init(){unlink(FIFO_NAME);log("移除管道文件成功",DEBUG) << endl;}};int OpenFIFO(string pathname,int flags)
{int fd = open(pathname.c_str(),flags);assert(fd >= 0);return fd;
}void wait(int fd)
{log("等待中....",DEBUG) << endl;uint32_t tmp = 0;ssize_t s = read(fd,&tmp,sizeof(uint32_t));assert(s == sizeof (uint32_t));(void)s;
}void send(int fd)
{uint32_t tmp = 0;ssize_t s = write(fd,&tmp,sizeof(uint32_t));assert(s == sizeof (uint32_t));(void)s;log("唤醒中",DEBUG) << endl;
}void closefd(int fd)
{close(fd);
}#endif

五、信号量和PV操作

信号量主要用于同步和互斥的,下面先来看看什么是同步和互斥。

5.1 进程互斥

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

5.2 使用信号量的基本流程 

信号量是一个计数器,用于控制多个进程对共享资源的访问,其主要目的是实现进程间的同步。

为什么要有信号量

如果有 A、B 两个进程分别负责读和写数据的操作,这两个线程是相互合作、相互依赖的。那么写数据应该发生在读数据之前。而实际上,由于异步性的存在,可能会发生先读后写的情况,而此时由于缓冲区还没有被写入数据,读进程 A 没有数据可读,因此读进程 A 被阻塞。
因此,为了解决上述这两个问题,保证共享内存在任何时刻只有一个进程在访问(互斥),并且使得进程们能够按照某个特定顺序访问共享内存(同步),我们就可以使用进程的同步与互斥机制,常见的比如信号量与 PV 操作。
进程的同步与互斥其实是一种对进程通信的保护机制,并不是用来传输进程之间真正通信的内容的,但是由于它们会传输信号量,所以也被纳入进程通信的范畴,称为低级通信

使用信号量的基本流程如下:

  1. 创建一个信号量:调用者需要指定初始值,通常为1或0,用于控制共享资源的访问权限。
  2. 等待信号量:该操作会测试信号量的值,如果其值小于等于0,进程将被阻塞,直到信号量的值大于0。这个操作也被称为P操作。
  3. 发送信号量:该操作将信号量的值加1,以允许其他等待进程继续执行。这个操作也被称为V操作。
  4. 为了确保信号量操作的原子性,信号量通常在内核中实现。在Linux环境中,有三种主要类型的信号量:Posix信号量(Portable Operating System Interface for Unix),有名信号量(使用Posix IPC命名标识),以及基于内存的Posix信号量(存储在共享内存区中)。此外,还有System V信号量,它也常用于进程间或线程间的同步。

P 操作和 V 操作必须成对出现。缺少 P 操作就不能保证对共享内存的互斥访问,缺少 V 操作就会导致共享内存永远得不到释放、处于等待态的进程永远得不到唤醒。

5.3 对PV操作的理解

用户进程可以通过使用操作系统提供的一对原语来对信号量进行操作,从而很方便的实现进程互斥或同步。这一对原语就是 PV 操作:

1)P 操作:将信号量值减 1,表示申请占用一个资源。如果结果小于 0,表示已经没有可用资源,则执行 P 操作的进程被阻塞。如果结果大于等于 0,表示现有的资源足够你使用,则执行 P 操作的进程继续执行。

可以这么理解,当信号量的值为 2 的时候,表示有 2 个资源可以使用,当信号量的值为 -2 的时候,表示有两个进程正在等待使用这个资源。不看这句话真的无法理解 V 操作,看完顿时如梦初醒。

2)V 操作:将信号量值加 1,表示释放一个资源,即使用完资源后归还资源。若加完后信号量的值小于等于 0,表示有某些进程正在等待该资源,由于我们已经释放出一个资源了,因此需要唤醒一个等待使用该资源(就绪态)的进程,使之运行下去。

我觉得已经讲的足够通俗了,不过对于 V 操作大家可能仍然有困惑,下面再来看两个关于 V 操作的问答:

问:信号量的值 大于 0 表示有共享资源可供使用,这个时候为什么不需要唤醒进程

答:所谓唤醒进程是从就绪队列(阻塞队列)中唤醒进程,而信号量的值大于 0 表示有共享资源可供使用,也就是说这个时候没有进程被阻塞在这个资源上,所以不需要唤醒,正常运行即可。

问:信号量的值 等于 0 的时候表示没有共享资源可供使用,为什么还要唤醒进程

答:V 操作是先执行信号量值加 1 的,也就是说,把信号量的值加 1 后才变成了 0,在此之前,信号量的值是 -1,即有一个进程正在等待这个共享资源,我们需要唤醒它。

5.4 PV操作实现进程同步伪代码

回顾一下进程同步,就是要各并发进程按要求有序地运行。

举个例子,以下两个进程 P1、P2 并发执行,由于存在异步性,因此二者交替推进的次序是不确定的。假设 P2 的 “代码4” 要基于 P1 的 “代码1” 和 “代码2” 的运行结果才能执行,那么我们就必须保证 “代码4” 一定是在 “代码2” 之后才会执行。

如果 P2 的 “代码4” 要基于 P1 的 “代码1” 和 “代码2” 的运行结果才能执行,那么我们就必须保证 “代码4” 一定是在 “代码2” 之后才会执行。

使用信号量和 PV 操作实现进程的同步也非常方便,三步走:

  • 定义一个同步信号量,并初始化为当前可用资源的数量
  • 在优先级较的操作的面执行 V 操作,释放资源
  • 在优先级较的操作的面执行 P 操作,申请占用资源
     

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

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

相关文章

运动耳机怎么选?五款新手必买的运动耳机盘点

运动耳机是专为运动爱好者设计的耳机&#xff0c;轻巧便携&#xff0c;佩戴稳固。无论你在跑步、健身还是骑行&#xff0c;它都能为你带来优质的音乐体验。那如何选择一款合适的运动耳机呢&#xff1f;这里&#xff0c;我结合自己和身边朋友平时选购经验&#xff0c;整理了一些…

编程实战:自己编写HTTP服务器(系列5:执行后台shell命令)

初级代码游戏的专栏介绍与文章目录-CSDN博客 我的github&#xff1a;codetoys&#xff0c;所有代码都将会位于ctfc库中。已经放入库中我会指出在库中的位置。 这些代码大部分以Linux为目标但部分代码是纯C的&#xff0c;可以在任何平台上使用。 系列入口&#xff1a;编程实战…

单片机负电压

在电子电路中我们常常需要使用负电压&#xff0c;比如说我们在使用运放的时候常常需要建立一个负电压。下面就简单的以正5V电压到负电压5V为例说一下它的电路。 通常需要使用负电压时一般会选择使用专用的负压产生芯片&#xff0c;但这些芯片都比较贵&#xff0c;比如电荷泵原理…

P1229 遍历问题

题目描述 我们都很熟悉二叉树的前序、中序、后序遍历&#xff0c;在数据结构中常提出这样的问题&#xff1a;已知一棵二叉树的前序和中序遍历&#xff0c;求它的后序遍历&#xff0c;相应的&#xff0c;已知一棵二叉树的后序遍历和中序遍历序列你也能求出它的前序遍历。然而给…

剪辑师创作必备声音素材,BGM背景音效素材合集1万款

一、素材描述 本套音效包含了全面丰富的声音效果&#xff0c;如动物、运输、人群、天气、航空、军事、Foley声音等&#xff0c;以及世界各地的场景声效等&#xff0c;可能是同类音效中最为全面的&#xff0c;共由三套声音素材组合而成&#xff0c;1、熊猫背景音乐3800首&#…

宁静致远(“静”)

宁静致远是一个成语&#xff0c;读音为nng jng zh yuǎn&#xff0c;意思是只有心境平稳沉着、专心致志&#xff0c;才能厚积薄发、 有所作为。出自《淮南子:主术训》。 出处 宁静致远张铭篆刻 此句最早出自西汉初年道家刘安的《淮南子:主术训》&#xff0c;蜀汉丞相诸葛亮的…

5年增100倍6秒卖1瓶酒,酣客的“FFC模式”是什么

酣客酱酒销售模式&#xff0c;白酒FFC模式&#xff0c;白酒新零售模式设计 坐标&#xff1a;厦门&#xff0c;我是易创客肖琳 深耕社交新零售行业10年&#xff0c;主要提供新零售系统工具及顶层商业模式设计、全案策划运营陪跑等。 不知从何时起&#xff0c;营销圈开始有这么一…

Verilog 实现 i2c 协议

在时钟&#xff08;SCL&#xff09;为高电平的时候&#xff0c;数据总线&#xff08;SDA&#xff09;必须保持稳定&#xff0c;所以数据总线&#xff08;SDA&#xff09;在时钟&#xff08;SCL&#xff09;为低电平的时候才能改变。 在时钟&#xff08;SCL&#xff09;为高电平…

诸葛智能携手五大银行,以数据驱动的营销中台带来可预见增长

对于银行来说&#xff0c;客户是赖以生存的基础&#xff0c;也是保持活力的关键。尤其是大数据、人工智能等新兴技术的推动下&#xff0c;通过数据赋能产品升级和服务创新&#xff0c;深挖客户潜能&#xff0c;更是助推银行快步迈入高质量发展的新阶段。 在银行加速拥抱新质生…

文档分类DPCNN简介(pytorch实现)

文档分类DPCNN简介 DPCNN简介 模型结构区域嵌入等长卷积1/2池化DPCNN模型代码实现 DPCNN简介 论文中提出了一种基于 word-level 级别的网络-DPCNN&#xff0c;由于 TextCNN 不能通过卷积获得文本的长距离依赖关系&#xff0c;而论文中 DPCNN 通过不断加深网络&#xff0c;可以…

ATA-2021B高压放大器在光纤超声传感器中的应用

实验名称&#xff1a;超声传感性能研究 测试目的&#xff1a; 光纤马赫-曾德尔干涉仪是一种灵敏度高、结构灵活的传感结构。当在MZI上施加超声波信号时&#xff0c;会影响所涉及的干涉光之间的光程差&#xff0c;并导致干涉光谱的漂移。由于模式耦合是基于MZI的光纤传感器的关键…

脑中风也会出现眩晕?快速识别中风,一定要牢记这些!

眩晕是许多人都会经历的不适感&#xff0c;发作时仿佛整个世界都在旋转&#xff0c;可能还伴随着站立不稳、脚步虚浮、恶心等症状。然而&#xff0c;你可能不知道的是&#xff0c;这些症状在某些情况下可能是脑中风的前兆。如果不及时关注并采取相应措施&#xff0c;一旦发展为…

【算法】二分查找——在排序数组中查找元素的第一个和最后一个位置

本节博客主要是通过“在排序数组中查找元素的第一个和最后一个位置”总结关于二分算法的左右界代码模板&#xff0c;有需要借鉴即可。 目录 1.题目2.二分边界算法2.1查找区间左端点2.1.1循环条件2.1.2求中点的操作2.1.3总结 2.2查找区间右端点2.1.1循环条件2.1.2求中点的操作2.…

O2OA平台流程催办怎么做

O2OA平台设计了灵活的消息提醒数据交互方式&#xff0c;开发者可以根据自己的需要&#xff0c;来消费消息提醒数据&#xff0c;也可以将消息提醒数据接入到Kafka消息中间件来实现消息的准实时提醒。本篇主要介绍如何在O2OA服务器中设置流程的催办提醒消息。 催办提醒服务&#…

centos无法联网解决方案(9步完成

1.打开终端&#xff0c;输入 su - root 进入到管理员模式&#xff08;-的前后都有空格哈&#xff09; 切换后&#xff0c;显示的就是root... 2.. &#xff0c;输入命令ip addr 2. 切换当前目录 cd /etc/sysconfig/network-scripts/ 3.输入命令&#xff0c;打开文件 vi /etc…

一.常见算法--动态规划

&#xff08;1&#xff09;0-1背包问题 问题描述&#xff1a; 0-1背包问题的描述&#xff1a;在n种物品中选择1个或0个第i种物品&#xff0c;装入背包容量为m的背包&#xff0c;使得背包价值达到最大。 思路与关键点&#xff1a; 用到了max函数&#xff0c;用于返回两个数之中…

为何Linux成为你不可或缺的技能

在数字化飞速发展的今天&#xff0c;无论你是IT行业的精英&#xff0c;还是其他领域的专业人士&#xff0c;掌握Linux都已经成为一项至关重要的技能。那么&#xff0c;为什么一定要学会Linux呢&#xff1f;以下文章仅供参考 1. 开源的力量&#xff1a;无限的可能性 Linux是一…

工厂自动化升级改造(3)-Modbus与MQTT的转换

什么是MQTT,Modbus,见下面文章 工厂自动化升级改造参考(01)--设备通信协议详解及选型-CSDN博客文章浏览阅读608次,点赞9次,收藏6次。>>特点:基于标准的以太网技术,使用TCP/IP协议栈,支持高速数据传输和局域网内的设备通信。>>>特点:跨平台的通信协议,…

ssl证书价格一年多少钱?如何申请?

由于行业新规&#xff0c;现在阿里云、腾讯云等几乎所有平台都不再提供一年期免费证书&#xff0c;如果需要一年期证书则需要支付一定的费用。SSL证书的价格根据类型不同几十到几百上千不等。 一年期SSL证书申请通道https://www.joyssl.com/?nid16 一年期SSL证书申请流程&am…

人工智能(一)架构

一、引言 人工智能这个词不是很新鲜&#xff0c;早就有开始研究的&#xff0c;各种推荐系统、智能客服都是有一定的智能服务的&#xff0c;但是一直都没有体现出多高的智能性&#xff0c;很多时候更像是‘人工智障’。 但是自从chatGpt3被大范围的营销和使用之后&#xff0c;人…