目录
一、什么进程间通信
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或0,用于控制共享资源的访问权限。
- 等待信号量:该操作会测试信号量的值,如果其值小于等于0,进程将被阻塞,直到信号量的值大于0。这个操作也被称为P操作。
- 发送信号量:该操作将信号量的值加1,以允许其他等待进程继续执行。这个操作也被称为V操作。
- 为了确保信号量操作的原子性,信号量通常在内核中实现。在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 操作,申请占用资源