进程间通信
- 进程间通信介绍
- 进程间通信目的
- 进程间通信本质
- 进程间通信分类
- 管道
- 管道概念
- 匿名管道
- pipe函数
- 管道特点
- 命名管道
- 创建命名管道
- 匿名管道与命名管道的区别
- 用命名管道实现serve&client通信
- system V进程间通信
- system V共享内存
- 共享内存数据结构
- 共享内存的建立与释放
- 共享内存函数
- 共享内存创建
- 共享内存的释放
- 共享内存的关联
- 共享内存的去关联
- 用共享内存实现serve&client通信
- 共享内存与管道进行对比
- System V消息队列
- 消息队列的基本原理
- System V信号量
- 信号量相关概念
- 信号量数据结构
- 进程互斥
进程间通信介绍
进程间通信简称IPC(Interprocess communication),进程间通信就是在不同进程之间传播或交换信息。
进程间通信目的
- 数据传输:一个进程需要将它的数据发送给另一个进程;
- 资源共享:多个进程之间共享同样的资源;
- 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程);
- 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变
进程间通信本质
进程间通信本质就是让不同进程看到同一块“内存”(特定结构组织的)
由于各个运行进程之间具有独立性,这个独立性主要体现在数据层面,而代码逻辑层面可以私有也可以公有(例如父子进程),因此各个进程之间要实现通信是非常困难的。
各个进程之间若想实现通信,一定要借助第三方资源,这些进程就可以通过向这个第三方资源写入或是读取数据,进而实现进程之间的通信,这个第三方资源实际上就是操作系统提供的一段内存区域。
进程间通信分类
管道
- 匿名管道pipe
- 命名管道
System V IPC
- System V 消息队列
- System V 共享内存
- System V 信号量
POSIX IPC
- 消息队列
- 共享内存
- 信号量
- 互斥量
- 条件变量
- 读写锁
管道
管道概念
管道是Unix中最古老的进程间通信的形式,我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”。
例如,统计我们当前使用云服务器上的登录用户个数。
who命令和wc命令都是两个程序,当它们运行起来后就变成了两个进程,who进程通过标准输出将数据打到“管道”当中,wc进程再通过标准输入从“管道”当中读取数据,至此便完成了数据的传输,进而完成数据的进一步加工处理。
匿名管道
匿名管道用于进程间通信,且仅限于本地父子进程之间的通信。
使用匿名管道实现父子进程间通信的原理就是,让两个父子进程先看到同一份被打开的文件资源,然后父子进程就可以对该文件进行写入或是读取操作,进而实现父子进程间通信。
- 这里父子进程看到的同一份文件资源是由操作系统来维护的,所以当父子进程对该文件进行写入操作时,该文件缓冲区当中的数据并不会进行写时拷贝。
- 管道虽然用的是文件的方案,但操作系统一定不会把进程进行通信的数据刷新到磁盘当中,因为这样做有IO参与会降低效率,而且也没有必要。也就是说,这种文件是一批不会把数据写到磁盘当中的文件,换句话说,磁盘文件和内存文件不一定是一一对应的,有些文件只会在内存当中存在,而不会在磁盘当中存在。
pipe函数
功能:pipe函数用于创建匿名管道
原型:int pipe(int fd[2]);
参数:fd
:文件描述符数组,其中fd[0]
表示读端,,fd[1]
表示写端;
返回值:成功返回0,失败返回错误代码;
匿名管道使用步骤:
- 父进程调用pipe函数创建管道文件
- 父进程调用fork函数创建子进程
- 父进程关闭写端,子进程关闭读端。
注意:
- 管道只能够进行单向通信,因此当父进程创建完子进程后,需要确认父子进程谁读谁写,然后关闭相应的读写端。
- 从管道写端写入的数据会被内核缓冲,直到从管道的读端被读取。
站在文件描述符角度-深度理解管道:
- 父进程调用pipe函数创建管道文件
- 父进程调用fork函数创建子进程
- 父进程关闭f[0],子进程关闭f[1]
我们看如下代码,父进程进行写入,子进程进行读取:
#include <iostream>
#include <stdio.h>
#include <unistd.h>
#include <assert.h>
#include <string>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
using namespace std;
int main()
{
// 1.创建管道文件
int pipefd[2] = {0};
int n = pipe(pipefd);
assert(n != -1);
(void)n;
#ifdef DEBUG
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 关闭子进程不需要的fd;
close(pipefd[1]);
char *buffer[1024 * 8];
while (true)
{
// 写入的一方,fd没有关闭,如果有数据就读,没有数据就等
// 写入的一方,fd关闭,read就会返回0,表示读到文件结尾
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(father), me quit!!!" << endl;
break;
}
}
exit(0);
}
// 父进程 写
// 3.构建单向通信的信道,父进程写入,子进程读取
// 3.1 关闭父进程不需要的fd;
close(pipefd[0]);
string message = "我是父进程,我正在给你发消息";
int count = 0;
char send_buffer[1024 * 8];
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 sleep
sleep(1);
cout << count << endl;
if (count == 5)
{
cout << "writer quit(father)" << endl;
break;
}
}
close(pipefd[1]);
pid_t ret = waitpid(id, nullptr, 0);
cout << "id:" << id << " ret:" << ret << endl;
assert(ret > 0);
(void)ret;
return 0;
}
当count == 5时,我们会发现,父进程停止写入,关闭fd,子进程也就读取完毕:
管道特点
- 只能用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信;通常,一个管道由一个进程创建,然后该进程调用fork,此后父、子进程之间就可应用该管道,我们上面的代码就很好的解释了这个问题。
- 管道具有通过让进程间协同,提供了访问控制;
a. 写快,读慢,写满就不能再写了;
b. 读快,写慢,管道没有数据,读必须等待;
c. 写关闭,读0,表示读到了文件结尾;
d. 读关,写继续写,OS会终止进程;
其中前面两种情况就能够很好的说明,管道是自带同步与互斥机制的,读端进程和写端进程是有一个步调协调的过程的,不会说当管道没有数据了读端还在读取,而当管道已经满了写端还在写入。读端进程读取数据的条件是管道里面有数据,写端进程写入数据的条件是管道当中还有空间,若是条件不满足,则相应的进程就会被挂起,直到条件满足后才会被再次唤醒。
第三种情况也很好理解,读端进程已经将管道当中的所有数据都读取出来了,而且此后也不会有写端再进行写入了,那么此时读端进程也就可以执行该进程的其他逻辑了,而不会被挂起。
第四种情况也不难理解,既然管道当中的数据已经没有进程会读取了,那么写端进程的写入将没有意义,因此操作系统直接将写端进程杀掉。而此时子进程代码都还没跑完就被终止了,属于异常退出,那么子进程必然收到了某种信号。
- 管道提供面向流式的通信服务;
对于进程A写入管道当中的数据,进程B每次从管道读取的数据的多少是任意的,这种被称为流式服务,与之相对应的是数据报服务:
- 流式服务: 数据没有明确的分割,不分一定的报文段。
- 数据报服务: 数据有明确的分割,拿数据按报文段拿。
4.管道是半双工通信的。
在数据通信中,数据在线路上的传送方式可以分为以下三种:
- 单工通信(Simplex Communication):单工模式的数据传输是单向的。通信双方中,一方固定为发送端,另一方固定为接收端。
- 半双工通信(Half Duplex):半双工数据传输指数据可以在一个信号载体的两个方向上传输,但是不能同时传输。
- 全双工通信(Full Duplex):全双工通信允许数据在两个方向上同时传输,它的能力相当于两个单工通信方式的结合。全双工可以同时(瞬时)进行信号的双向传输。
半双工通信:
- 管道的生命周期随进程。
管道本质上是通过文件进行通信的,也就是说管道依赖于文件系统,那么当所有打开该文件的进程都退出后,该文件也就会被释放掉,所以说管道的生命周期随进程。
接下来,我们来写一个进程池,来看看父进程是如何给子进程分配任务的:
ProcesPool.cc
#include <iostream>
#include <vector>
#include <cstdlib>
#include <ctime>
#include <cassert>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include "Task.hpp"
using namespace std;
#define PROCESS_NUM 5
int waitCommand(int waitFd, bool& quit)//如果对方不发,我们就阻塞
{
uint32_t command = 0;
ssize_t s = read(waitFd, &command, sizeof(command));
if(s == 0)
{
quit = true;
return -1;
}
assert(s == sizeof(uint32_t));
return command;
}
void sendAndWakeup(pid_t who, int fd, uint32_t command)
{
write(fd, &command, sizeof(command));
cout << "main process: call process " << who << " execute " << desc[command] << " through " << fd << endl;
}
int main()
{
load();
vector<pair<pid_t, int>> slots;
//先创建多个进程
for(int i = 0; i < PROCESS_NUM; i++)
{
//创建管道文件
int pipefd[2] = {0};
int n = pipe(pipefd);
assert(n != -1);
(void) n;
pid_t id = fork();
assert(id != -1);
//子进程我们进行读取
if(id == 0)
{
//关闭写端
close(pipefd[1]);
//child
while(true)
{
//pipefd[0]
//等命令
bool quit = false;
int command = waitCommand(pipefd[0], quit);//如果对方不发,我们就阻塞
if(quit)
break;
//执行对应的命令
if(command >= 0 && command < handlerSize())
{
callbacks[command];
}
else
{
cout << "非法command:" << command << endl;
}
}
exit(1);
}
//父进程写入,关闭读端
close(pipefd[0]);
slots.push_back(pair<pid_t, int>(id, pipefd[1]));
}
//父进程派发任务
srand((unsigned long)time(nullptr) ^ getpid() ^ 23323123123L); // 让数据源更随机
while(true)
{
//选择一个任务
int command = rand() % handlerSize();
//随机选择一个进程来完成任务
int choice = rand() % slots.size();
// 把任务给指定的进程
sendAndWakeup(slots[choice].first, slots[choice].second, command);
sleep(1);
}
// 关闭fd, 所有的子进程都会退出
for (const auto &slot : slots)
{
close(slot.second);
}
// 回收所有的子进程信息
for (const auto &slot : slots)
{
waitpid(slot.first, nullptr, 0);
}
return 0;
}
Task.hpp
#include <iostream>
#include <vector>
#include <string>
#include <unistd.h>
#include <functional>
#include <unordered_map>
typedef std::function<void()> func;
std::vector<func> callbacks;
std::unordered_map<int, std::string> desc;
void readMySQL()
{
std::cout << "sub process[" << getpid() << "] 执行访问数据库的任务\n" << std::endl;
}
void execuleUrl()
{
std::cout << "sub process[" << getpid() << " ] 执行url解析\n" << std::endl;
}
void cal()
{
std::cout << "sub process[" << getpid() << " ] 执行加密任务\n" << std::endl;
}
void save()
{
std::cout << "sub process[" << getpid() << " ] 执行数据持久化任务\n" << std::endl;
}
void load()
{
desc.insert({callbacks.size(), "readMySQL: 读取数据库"});
callbacks.push_back(readMySQL);
desc.insert({callbacks.size(), "execuleUrl: 进行url解析"});
callbacks.push_back(execuleUrl);
desc.insert({callbacks.size(), "cal: 进行加密计算"});
callbacks.push_back(cal);
desc.insert({callbacks.size(), "save: 进行数据的文件保存"});
callbacks.push_back(save);
}
void showHandler()
{
for(const auto &iter : desc )
{
std::cout << iter.first << "\t" << iter.second << std::endl;
}
}
int handlerSize()
{
return callbacks.size();
}
运行程序我们会发现,父进程给子进程分配任务是随机的,谁先拿到就是谁的:
命名管道
匿名管道只能用于具有共同祖先的进程(具有亲缘关系的进程)之间的通信,通常,一个管道由一个进程创建,然后该进程调用fork,此后父子进程之间就可应用该管道。
如果要实现两个毫不相关进程之间的通信,可以使用命名管道来做到。命名管道就是一种特殊类型的文件,两个进程通过命名管道的文件名打开同一个管道文件,此时这两个进程也就看到了同一份资源,进而就可以进行通信了。
命名管道和匿名管道一样,都是内存文件,只不过命名管道在磁盘有一个简单的映像,但这个映像的大小永远为0,因为命名管道和匿名管道都不会将通信数据刷新到磁盘当中。
创建命名管道
- 命名管道可以从命令行上创建,命令行方法是使用下面这个命令:
- 命名管道也可以从程序里创建,相关函数有:
int mkfifo(const char *filename,mode_t mode);
mkfifo函数的第一个参数是pathname,表示要创建的命名管道文件。
- 若pathname以路径的方式给出,则将命名管道文件创建在pathname路径下。
- 若pathname以文件名的方式给出,则将命名管道文件默认创建在当前路径下。(注意当前路径的含义)
mkfifo函数的第二个参数是mode,表示创建命名管道文件的默认权限。
mkfifo函数的返回值。
- 命名管道创建成功,返回0;
- 命名管道创建失败,返回-1。
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
int main()
{
int ret = mkfifo("myfifo", 0666);
if(ret < 0)
{
perror("mkfifo");
return -1;
}
//.....
return 0;
}
创建出来的文件类型是p,就表示该文件是管道文件:
使用这个命名管道文件,就能实现两个进程之间的通信了。我们在一个进程(进程A)中用shell脚本每秒向命名管道写入一个字符串,在另一个进程(进程B)当中用cat命令从命名管道当中进行读取。现象就是当进程A启动后,进程B会每秒从命名管道中读取一个字符串打印到显示器上。这就证明了这两个毫不相关的进程可以通过命名管道进行数据传输,即通信。
当管道的读端进程退出后,写端进程再向管道写入数据就没有意义了,此时写端进程会被操作系统杀掉,在这里就可以很好的得到验证:当我们终止掉读端进程后,因为写端执行的循环脚本是由命令行解释器bash执行的,所以此时bash就会被操作系统杀掉,我们的云服务器也就退出了。
匿名管道与命名管道的区别
- 匿名管道由pipe函数创建并打开;
- 命名管道由mkfifo函数创建,打开用open;
- FIFO(命名管道)与pipe(匿名管道)之间唯一的区别在它们创建与打开的方式不同,一但这些工作完 成之后,它们具有相同的语义。
用命名管道实现serve&client通信
实现服务端(server)和客户端(client)之间的通信之前,我们需要先让服务端运行起来,我们需要让服务端运行后创建一个命名管道文件,然后再以读的方式打开该命名管道文件,之后服务端就可以从该命名管道当中读取客户端发来的通信信息了。
服务端代码如下:
#include "comm.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)
{
cout << "[" << getpid() << "]"<< "read end of file, clien quit, server quit too!" << endl;
break;
}
else
{
perror("read");
exit(-1);
}
}
}
int main()
{
// 创建管道文件
if (mkfifo(ipcPath.c_str(), MODE) < 0)
{
perror("mkfifo");
exit(1);
}
Log("创建管道文件成功", Debug) << "step 1" << endl;
// 文件操作,读取文件
int fd = open(ipcPath.c_str(), O_RDONLY);
if (fd < 0)
{
perror("open");
exit(2);
}
Log("打开管道文件成功", Debug) << "step 2" << endl;
// 创建子进程
int num = 3;
for (int i = 0; i < num; i++)
{
pid_t id = fork();
if (id == 0)
{
// 编写正常通信代码
getMessage(fd);
exit(1);
}
}
// 父进程等待子进程
for (int i = 0; i < num; i++)
{
waitpid(-1, nullptr, 0);
}
// 关闭文件
close(fd);
Log("关闭管道文件成功", Debug) << "step 3" << endl;
// 通信完毕,删除文件
unlink(ipcPath.c_str());
Log("删除管道文件成功", Debug) << "step 4" << endl;
return 0;
}
客户端代码如下:
#include "comm.hpp"
int main()
{
//获取管道文件
int fd = open(ipcPath.c_str(), O_WRONLY);
if(fd < 0)
{
perror("open");
exit(1);
}
//文件操作,写文件
string buffer;
while(true)
{
cout << "Please Enter Message Line :> ";
std::getline(cin, buffer);
write(fd, buffer.c_str(), buffer.size());
}
//关闭文件
close(fd);
return 0;
}
共享头文件代码如下:
#ifndef _COMM_H_
#define _COMM_H_
#include <iostream>
#include <string>
#include <cstdio>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <fcntl.h>
#include "log.hpp"
using namespace std;
#define MODE 0666
#define SIZE 128
string ipcPath = "./fifo.ipc";
#endif
日志文件代码如下:
#ifndef _LOG_H_
#define _LOG_H_
#include <iostream>
#include <ctime>
#define Debug 0
#define Notice 1
#define Warning 2
#define Error 3
const std::string msg[] = {
"Debug",
"Notice",
"Warning",
"Error"
};
std::ostream &Log(std::string message, int level)
{
std::cout << " | " << (unsigned)time(nullptr) << " | " << msg[level] << " | " << message;
return std::cout;
}
#endif
代码书写完毕,我们打开服务端,就会发现管道文件创建成功,在客户端就可以看见这个创建的管道文件。
接下来我们也将客户端运行,然后在客户端写入信息,写入的信息会被放入管道文件中,服务端再从管道文件中将信息读取出来并打印到显示器上,该现象说明服务端是能够通过命名管道获取到客户端发来的信息的,换句话说,此时这两个进程之间是能够通信的。
客户端和服务端运行起来时,我们还可以通过ps命令查看这两个进程的信息,可以发现这两个进程确实是两个毫不相关的进程,因为它们的PID和PPID都不相同。也就证明了,命名管道是可以实现两个毫不相关进程之间的通信的。
服务端和客户端之间的退出关系
当客户端退出后,服务端将管道当中的数据读完后就再也读不到数据了,那么此时服务端也就会去执行它的其他代码了(在当前代码中是直接退出了)。
当服务端退出后,客户端写入管道的数据就不会被读取了,也就没有意义了,那么当客户端下一次再向管道写入数据时客户端就被操作系统强制杀掉了。
当客户端只向管道文件中写入数据,而服务端并不读取管道文件的数据,我们会发现管道文件的大小依然为0,也就说明了双方进程之间的通信依旧是在内存当中进行的,和匿名管道通信是一样的。
#include "comm.hpp"
static void getMessage(int fd)
{
char buffer[SIZE];
while (true)
{
//...
}
}
int main()
{
// 创建管道文件
if (mkfifo(ipcPath.c_str(), MODE) < 0)
{
perror("mkfifo");
exit(1);
}
Log("创建管道文件成功", Debug) << "step 1" << endl;
// 文件操作,读取文件
int fd = open(ipcPath.c_str(), O_RDONLY);
if (fd < 0)
{
perror("open");
exit(2);
}
Log("打开管道文件成功", Debug) << "step 2" << endl;
// 创建子进程
int num = 3;
for (int i = 0; i < num; i++)
{
pid_t id = fork();
if (id == 0)
{
// 编写正常通信代码
getMessage(fd);
exit(1);
}
}
// 父进程等待子进程
for (int i = 0; i < num; i++)
{
waitpid(-1, nullptr, 0);
}
// 关闭文件
close(fd);
Log("关闭管道文件成功", Debug) << "step 3" << endl;
// 通信完毕,删除文件
unlink(ipcPath.c_str());
Log("删除管道文件成功", Debug) << "step 4" << endl;
return 0;
}
需要注意的是两个进程之间的通信,并不是简单的发送字符串而已,服务端是会对客户端发送过来的信息进行某些处理的。
system V进程间通信
管道通信本质是基于文件的,也就是说操作系统并没有为此做过多的设计工作,而system V IPC是操作系统特地设计的一种通信方式。但是不管怎么样,它们的本质都是一样的,都是在想尽办法让不同的进程看到同一份由操作系统提供的资源。
system V IPC提供的通信方式有以下三种:
- system V共享内存
- system V消息队列
- system V信号量
其中,system V共享内存和system V消息队列是以传送数据为目的的,而system V信号量是为了保证进程间的同步与互斥而设计的,虽然system V信号量和通信好像没有直接关系,但属于通信范畴。
system V共享内存
共享内存让不同进程看到同一份资源的方式就是,在物理内存当中申请一块内存空间,然后将这块内存空间分别与各个进程各自的页表之间建立映射,再在虚拟地址空间当中开辟空间并将虚拟地址填充到各自页表的对应位置,使得虚拟地址和物理地址之间建立起对应关系,至此这些进程便看到了同一份物理内存,这块物理内存就叫做共享内存。
共享内存区是最快的IPC形式。一旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到内核,换句话说是进程不再通过执行进入内核的系统调用来传递彼此的数据。
共享内存数据结构
在系统当中可能会有大量的进程在进行通信,因此系统当中就可能存在大量的共享内存,那么操作系统必然要对其进行管理,所以共享内存除了在内存当中真正开辟空间之外,系统一定还要为共享内存维护相关的内核数据结构。
共享内存的数据结构如下:
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 */
};
当我们申请了一块共享内存后,为了让要实现通信的进程能够看到同一个共享内存,因此每一个共享内存被申请时都有一个key
值,这个key
值用于标识系统中共享内存的唯一性。
可以看到上面共享内存数据结构的第一个成员是shm_perm
,shm_perm
是一个ipc_perm
类型的结构体变量,每个共享内存的key
值存储在shm_perm
这个结构体变量当中,其中ipc_perm
结构体的定义如下:
struct ipc_perm{
__kernel_key_t key;
__kernel_uid_t uid;
__kernel_gid_t gid;
__kernel_uid_t cuid;
__kernel_gid_t cgid;
__kernel_mode_t mode;
unsigned short seq;
};
共享内存的建立与释放
共享内存的建立大致包括以下两个过程:
- 在物理内存当中申请共享内存空间。
- 将申请到的共享内存挂接到地址空间,即建立映射关系。
共享内存的释放大致包括以下两个过程:
- 将共享内存与地址空间去关联,即取消映射关系。
- 释放共享内存空间,即将物理内存归还给系统。
共享内存函数
首先,我们在这儿先介绍几个共享内存函数:
shmget函数
- 功能:用来创建共享内存
- 原型 :
int shmget(key_t key, size_t size, int shmflg);
- 参数:
key
:这个共享内存段名字 ;
size
:共享内存大小 ;
shmflg
:由九个权限标志构成,它们的用法和创建文件时使用的mode模式标志是一样的。 - 返回值:成功返回一个非负整数,即该共享内存段的标识码;失败返回 -1。
传入shmget函数的第一个参数key,需要我们使用ftok函数进行获取
ftok函数的函数原型:key_t ftok(const char *pathname, int proj_id);
ftok函数的作用就是,将一个已存在的路径名pathname
和一个整数标识符proj_id
转换成一个key
值,称为IPC
键值,在使用shmget
函数获取共享内存时,这个key
值会被填充进维护共享内存的数据结构当中。需要注意的是,pathname
所指定的文件必须存在且可存取。
注意:
- 使用ftok函数生成key值可能会产生冲突,此时可以对传入ftok函数的参数进行修改。
- 需要进行通信的各个进程,在使用ftok函数获取key值时,都需要采用同样的路径名和和整数标识符,进而生成同一种key值,然后才能找到同一个共享资源。
传入shmget函数的第三个参数shmflg,常用的组合方式有以下几种:
组合方式 | 作用 |
---|---|
IPC_CREAT | 如果内核中不存在键值与key相等的共享内存,则新建一个共享内存并返回该共享内存的句柄;如果存在这样的共享内存,则直接返回该共享内存的句柄; |
IPC_CREAT / IPC_EXCL | 如果内核中不存在键值与key相等的共享内存,则新建一个共享内存并返回该共享内存的句柄;如果存在这样的共享内存,则出错返回; |
- 使用组合IPC_CREAT,一定会获得一个共享内存的句柄,但无法确认该共享内存是否是新建的共享内存。
- 使用组合IPC_CREAT |IPC_EXCL,只有shmget函数调用成功时才会获得共享内存的句柄,并且该共享内存一定是新建的共享内存。
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)
;- s
hmflg=SHM_RDONLY
,表示连接操作用来只读共享内存。
其中,作为shmat函数的第三个参数传入的常用的选项有以下三个:
选项 | 作用 |
---|---|
SHM_RDONLY | 关联共享内存后只进行读取操作 |
SHM_RND | 若shmaddr不为NULL,则关联地址自动向下调整为SHMLBA的整数倍。公式:shmaddr-(shmaddr%SHMLBA) |
0 | 默认为读写权限 |
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。
其中,作为shmctl函数的第二个参数传入的常用的选项有以下三个:
选项 | 作用 |
---|---|
IPC_STAT | 获取共享内存的当前关联值,此时参数buf作为输出型参数 |
IPC_SET | 在进程有足够权限的前提下,将共享内存的当前关联值设置为buf所指的数据结构中的值 |
IPC_RMID | 删除共享内存段 |
共享内存创建
comm.hpp
#include <iostream>
#include <sys/types.h>
#include <sys/ipc.h>
#include <assert.h>
#include <sys/shm.h>
#include "log.hpp"
using namespace std;
#define PATH_NAME "/home/gtt"
#define PROJ_ID 0x66
#define SHM_SIZE 4096 //共享内存的大小,最好是页(PAGE: 4096)的整数倍
log.hpp
#ifndef _LOG_H_
#define _LOG_H_
#include <iostream>
#include <ctime>
#define Debug 0
#define Notice 1
#define Warning 2
#define Error 3
const std::string msg[] = {
"Debug",
"Notice",
"Warning",
"Error"
};
std::ostream &Log(std::string message, int level)
{
std::cout << " | " << (unsigned)time(nullptr) << " | " << msg[level] << " | " << message;
return std::cout;
}
#endif
shmServer.cc
#include "comm.hpp"
string TransToHex(key_t key)
{
char buffer[32];
snprintf(buffer, sizeof(buffer), "0x%x", key);
return buffer;
}
int main()
{
//1.创建一个公共的key值
key_t key = ftok(PATH_NAME, PROJ_ID);
assert(key != -1);
Log("create key dine ", Debug) << "server key:" << TransToHex(key) << endl;
// 2. 创建共享内存 -- 建议要创建一个全新的共享内存 -- 通信的发起者
int shmid = shmget(key, SHM_SIZE, IPC_CREAT | IPC_EXCL | 0666);
assert(shmid != -1);
Log("create shm dine ", Debug) << "shmid:" << shmid << endl;
return 0;
}
运行进程,我们就可以得到key值与句柄值shmd:
我们可以使用ipcs命令查看有关进程间通信设施的信息:
单独使用ipcs命令时,会默认列出消息队列、共享内存以及信号量相关的信息,若只想查看它们之间某一个的相关信息,可以选择携带以下选项:
-q
:列出消息队列相关信息。-m
:列出共享内存相关信息。-s
:列出信号量相关信息。
此时,根据ipcs命令的查看结果和我们的输出结果可以确认,共享内存已经创建成功了。
ipcs命令输出的每列信息的含义如下:
标题 | 含义 |
---|---|
key | 系统区别各个共享内存的唯一标识 |
shmid | 共享内存的用户层id(句柄) |
owner | 共享内存的拥有者 |
perms | 共享内存的权限 |
bytes | 共享内存的大小 |
nattch | 关联共享内存的进程数 |
status | 共享内存的状态 |
注意: key是在内核层面上保证共享内存唯一性的方式,而shmid是在用户层面上保证共享内存的唯一性,key和shmid之间的关系类似于fd和FILE*之间的的关系。
共享内存的释放
通过上面创建共享内存的实验可以发现,当我们的进程运行完毕后,申请的共享内存依旧存在,并没有被操作系统释放。实际上,管道是生命周期是随进程的,而共享内存的生命周期是随内核的,也就是说进程虽然已经退出,但是曾经创建的共享内存不会随着进程的退出而释放。
这说明,如果进程不主动删除创建的共享内存,那么共享内存就会一直存在,直到关机重启(system V IPC都是如此),同时也说明了IPC资源是由内核提供并维护的。
此时我们若是要将创建的共享内存释放,有两个方法,一就是使用命令释放共享内存,二就是在进程通信完毕后调用释放共享内存的函数进行释放。
使用命令释放共享内存资源
我们可以使用ipcrm -m shmid命令释放指定id的共享内存资源:
使用程序释放共享内存资源
控制共享内存我们需要用shmctl函数,shmctl函数的函数原型如下:
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
代码如下:
#include "comm.hpp"
string TransToHex(key_t key)
{
char buffer[32];
snprintf(buffer, sizeof(buffer), "0x%x", key);
return buffer;
}
int main()
{
//1.创建一个公共的key值
key_t key = ftok(PATH_NAME, PROJ_ID);
assert(key != -1);
Log("create key dine ", Debug) << "server key:" << TransToHex(key) << endl;
int shmid = shmget(key, SHM_SIZE, IPC_CREAT | IPC_EXCL | 0666);
assert(shmid != -1);
Log("create shm dine ", Debug) << "shmid:" << shmid << endl;
//删除共享内存,IPC_RMID即便是有进程和当下的shm挂接,依旧删除共享内存
int n = shmctl(shmid, IPC_RMID, nullptr);
assert(n != -1);
(void)n;
Log("delete shm dine ", Debug) << "shmid:" << shmid << endl;
return 0;
}
运行程序,我们会发现程序退出就删除了共享内存:
我们可以在程序运行时,使用以下监控脚本时刻关注共享内存的资源分配情况:
共享内存的关联
我们可以尝试使用shmat函数对共享内存进行关联:
#include "comm.hpp"
string TransToHex(key_t key)
{
char buffer[32];
snprintf(buffer, sizeof(buffer), "0x%x", key);
return buffer;
}
int main()
{
//1.创建一个公共的key值
key_t key = ftok(PATH_NAME, PROJ_ID);
assert(key != -1);
Log("create key dine ", Debug) << "server key:" << TransToHex(key) << endl;
//2. 创建共享内存 -- 建议要创建一个全新的共享内存 -- 通信的发起者
int shmid = shmget(key, SHM_SIZE, IPC_CREAT | IPC_EXCL | 0666);
assert(shmid != -1);
Log("create shm dine ", Debug) << "shmid:" << shmid << endl;
//3.关联共享内存,将指定的共享内存,挂接到自己的地址空间
sleep(2);
char* shmadder = (char*)shmat(shmid, nullptr, 0);
Log("attach shm done", Debug) << " shmid : " << shmid << endl;
sleep(2);
//删除共享内存,IPC_RMID即便是有进程和当下的shm挂接,依旧删除共享内存
int n = shmctl(shmid, IPC_RMID, nullptr);
assert(n != -1);
(void)n;
Log("delete shm dine ", Debug) << "shmid:" << shmid << endl;
return 0;
}
此时运行程序,即可发现关联该共享内存的进程数由0变成了1,而共享内存的权限显示也是666:
共享内存的去关联
取消共享内存与进程地址空间之间的关联我们需要用shmdt函数:
#include "comm.hpp"
string TransToHex(key_t key)
{
char buffer[32];
snprintf(buffer, sizeof(buffer), "0x%x", key);
return buffer;
}
int main()
{
//1.创建一个公共的key值
key_t key = ftok(PATH_NAME, PROJ_ID);
assert(key != -1);
Log("create key dine ", Debug) << "server key:" << TransToHex(key) << endl;
//2. 创建共享内存 -- 建议要创建一个全新的共享内存 -- 通信的发起者
int shmid = shmget(key, SHM_SIZE, IPC_CREAT | IPC_EXCL | 0666);
assert(shmid != -1);
Log("create shm dine ", Debug) << "shmid:" << shmid << endl;
//3.关联共享内存,将指定的共享内存,挂接到自己的地址空间
sleep(2);
char* shmadder = (char*)shmat(shmid, nullptr, 0);
Log("attach shm done", Debug) << " shmid : " << shmid << endl;
sleep(2);
//4.将指定的共享内存,从自己的地址空间中去关联
int n = shmdt(shmadder);
assert(n != -1);
(void) n;
Log("detach shm done", Debug) << " shmid : " << shmid << endl;
sleep(2);
//5.删除共享内存,IPC_RMID即便是有进程和当下的shm挂接,依旧删除共享内存
n = shmctl(shmid, IPC_RMID, nullptr);
assert(n != -1);
(void)n;
Log("delete shm dine ", Debug) << "shmid:" << shmid << endl;
return 0;
}
运行程序,通过监控即可发现该共享内存的关联数由1变为0的过程,即取消了共享内存与该进程之间的关联:
用共享内存实现serve&client通信
shmServer.cc
#include "comm.hpp"
// Init init;
string TransToHex(key_t key)
{
char buffer[32];
snprintf(buffer, sizeof(buffer), "0x%x", key);
return buffer;
}
int main()
{
//1.创建一个公共的key值
key_t key = ftok(PATH_NAME, PROJ_ID);
assert(key != -1);
Log("create key dine ", Debug) << "server key:" << TransToHex(key) << endl;
//2. 创建共享内存 -- 建议要创建一个全新的共享内存 -- 通信的发起者
int shmid = shmget(key, SHM_SIZE, IPC_CREAT | IPC_EXCL | 0666);
if (shmid == -1)
{
perror("shmget");
exit(1);
}
Log("create shm dine ", Debug) << "shmid:" << shmid << endl;
//3.关联共享内存,将指定的共享内存,挂接到自己的地址空间
//sleep(10);
char* shmadder = (char*)shmat(shmid, nullptr, 0);
Log("attach shm done", Debug) << " shmid : " << shmid << endl;
//sleep(10);
while(true)
{
printf("%s\n", shmadder);
sleep(1);
}
//4.将指定的共享内存,从自己的地址空间中去关联
int n = shmdt(shmadder);
assert(n != -1);
(void) n;
Log("detach shm done", Debug) << " shmid : " << shmid << endl;
//sleep(10);
//5.删除共享内存,IPC_RMID即便是有进程和当下的shm挂接,依旧删除共享内存
n = shmctl(shmid, IPC_RMID, nullptr);
assert(n != -1);
(void)n;
Log("delete shm dine ", Debug) << "shmid:" << shmid << endl;
return 0;
}
shmClient.cc
#include "comm.hpp"
int main()
{
//获取与shmServer进程相同的key值
Log("child pid is:", Debug) << getpid() << endl;
key_t key = ftok(PATH_NAME, PROJ_ID);
if(key < 0)
{
Log("create key failed:", Error) << "client key:" << key << endl;
exit(1);
}
Log("create key done:", Debug) << "client key:" << key << endl;
//获取共享内存
int shmid = shmget(key, SHM_SIZE, 0);
if(shmid < 0)
{
Log("create shm failed:", Error) << "client key:" << key << endl;
exit(2);
}
Log("create shm sucess:",Debug) << "client key:" << key << endl;
//关联共享内存
char* shmadder = (char*)shmat(shmid, nullptr, 0);
if(shmadder == nullptr)
{
Log("attach shm failed:", Error) << "client key:" << key << endl;
exit(3);
}
Log("attach shm sucess:", Debug) << "client key:" << key << endl;
//sleep(5);
//服务端写入数据
char a = 'a';
for(; a <= 'z'; a++)
{
shmadder[a-'a'] = a;
// 我们是每一次都向shmaddr[共享内存的起始地址]写入
snprintf(shmadder, SHM_SIZE - 1,\
"hello server, 我是其他进程,我的pid: %d, inc: %c\n",\
getpid(), a);
sleep(1);
}
//去关联
int n = shmdt(shmadder);
assert(n != -1);
Log("detach shm success", Error) << " client key : " << key << endl;
return 0;
}
先后运行服务端和客户端后,通过监控脚本可以看到服务端和客户端所关联的是同一个共享内存,共享内存关联的进程数也是2,表示服务端和客户端挂接共享内存成功。
此时先运行服务端创建共享内存,当我们运行客户端时服务端就开始不断输出数据,说明服务端和客户端是能够正常通信的。
但是我们会发现,共享内存缺乏访问控制,此时客户端进行一系列操作并不会影响服务端的运行,这就会带来并发问题,所以我们可以再进行一番改造,程序刚运行时就先创建命名管道文件,这样就可以实现服务端必须等待客户端的指令,才能进行接下来的操作:
comm.h
#pragma once
#include <iostream>
#include <cstdio>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <cassert>
#include <cstring>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include "log.hpp"
using namespace std;
#define PATH_NAME "/home/gtt"
#define PROJ_ID 0x66
#define SHM_SIZE 4096 //共享内存的大小,最好是页(PAGE: 4096)的整数倍
#define FIFO_NAME "./fifo"
class 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_WRONLY
int 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);
}
shmClient.cc
#include "comm.hpp"
int main()
{
//获取与shmServer进程相同的key值
Log("child pid is:", Debug) << getpid() << endl;
key_t key = ftok(PATH_NAME, PROJ_ID);
if(key < 0)
{
Log("create key failed:", Error) << "client key:" << key << endl;
exit(1);
}
Log("create key done:", Debug) << "client key:" << key << endl;
//获取共享内存
int shmid = shmget(key, SHM_SIZE, 0);
if(shmid < 0)
{
Log("create shm failed:", Error) << "client key:" << key << endl;
exit(2);
}
Log("create shm sucess:",Debug) << "client key:" << key << endl;
//关联共享内存
char* shmadder = (char*)shmat(shmid, nullptr, 0);
if(shmadder == nullptr)
{
Log("attach shm failed:", Error) << "client key:" << key << endl;
exit(3);
}
Log("attach shm sucess:", Debug) << "client key:" << key << endl;
//sleep(5);
//使用
int fd = OpenFIFO(FIFO_NAME, WRITE);
while(true)
{
ssize_t s = read(0, shmadder, SHM_SIZE-1);
if(s > 0)
{
shmadder[s-1] = 0;
Signal(fd);
if(strcmp(shmadder,"quit") == 0) break;
}
}
CloseFifo(fd);
// char a = 'a';
// for(; a <= 'z'; a++)
// {
// shmadder[a-'a'] = a;
// // 我们是每一次都向shmaddr[共享内存的起始地址]写入
// snprintf(shmadder, SHM_SIZE - 1,\
// "hello server, 我是其他进程,我的pid: %d, inc: %c\n",\
// getpid(), a);
// sleep(1);
// }
//去关联
int n = shmdt(shmadder);
assert(n != -1);
Log("detach shm success", Error) << " client key : " << key << endl;
return 0;
}
shmServer.cc
#include "comm.hpp"
// 对应的程序,在加载的时候,会自动构建全局变量,就要调用该类的构造函数 -- 创建管道文件
// 程序退出的时候,全局变量会被析构,自动调用析构函数,会自动删除管道文件
Init init;
string TransToHex(key_t key)
{
char buffer[32];
snprintf(buffer, sizeof(buffer), "0x%x", key);
return buffer;
}
int main()
{
//1.创建一个公共的key值
key_t key = ftok(PATH_NAME, PROJ_ID);
assert(key != -1);
Log("create key dine ", Debug) << "server key:" << TransToHex(key) << endl;
//2. 创建共享内存 -- 建议要创建一个全新的共享内存 -- 通信的发起者
int shmid = shmget(key, SHM_SIZE, IPC_CREAT | IPC_EXCL | 0666);
if (shmid == -1)
{
perror("shmget");
exit(1);
}
Log("create shm dine ", Debug) << "shmid:" << shmid << endl;
//3.关联共享内存,将指定的共享内存,挂接到自己的地址空间
//sleep(10);
char* shmadder = (char*)shmat(shmid, nullptr, 0);
Log("attach shm done", Debug) << " shmid : " << shmid << endl;
//sleep(10);
int fd = OpenFIFO(FIFO_NAME, READ);
for(;;)
{
Wait(fd);
// 临界区
printf("%s\n", shmadder);
if(strcmp(shmadder, "quit") == 0) break;
// sleep(1);
}
//4.将指定的共享内存,从自己的地址空间中去关联
int n = shmdt(shmadder);
assert(n != -1);
(void) n;
Log("detach shm done", Debug) << " shmid : " << shmid << endl;
//sleep(10);
//5.删除共享内存,IPC_RMID即便是有进程和当下的shm挂接,依旧删除共享内存
n = shmctl(shmid, IPC_RMID, nullptr);
assert(n != -1);
(void)n;
Log("delete shm dine ", Debug) << "shmid:" << shmid << endl;
CloseFifo(fd);
return 0;
}
此时先运行服务端创建共享内存,当我们运行客户端时,服务端等待客户端输入数据,客户端输入数据后服务端立马将数据打印出来,当客户端退出时,服务端紧接着也就退出。
共享内存与管道进行对比
我们先来看看管道间通信的过程:
从上图可以看出,使用管道通信的方式,将一个文件从一个进程传输到另一个进程需要进行四次拷贝操作:
- 服务端将信息从输入文件复制到服务端的临时缓冲区中。
- 将服务端临时缓冲区的信息复制到管道中。
- 客户端将信息从管道复制到客户端的缓冲区中。
- 将客户端临时缓冲区的信息复制到输出文件中。
接下来为我们来看共享内存通信:
从这张图可以看出,使用共享内存进行通信,将一个文件从一个进程传输到另一个进程只需要进行两次拷贝操作:
- 从输入文件到共享内存。
- 从共享内存到输出文件。
所以共享内存是所有进程间通信方式中最快的一种通信方式,因为该通信方式需要进行的拷贝次数最少。
但是共享内存也是有缺点的,我们知道管道是自带同步与互斥机制的,但是共享内存并没有提供任何的保护机制,包括同步与互斥。
System V消息队列
消息队列的基本原理
消息队列实际上就是在系统当中创建了一个队列,队列当中的每个成员都是一个数据块,这些数据块都由类型和信息两部分构成,两个互相通信的进程通过某种方式看到同一个消息队列,这两个进程向对方发数据时,都在消息队列的队尾添加数据块,这两个进程获取数据块时,都在消息队列的队头取数据块。
其中消息队列当中的某一个数据块是由谁发送给谁的,取决于数据块的类型。
总结一下:
- 消息队列提供了一个从一个进程向另一个进程发送数据块的方法。
- 每个数据块都被认为是有一个类型的,接收者进程接收的数据块可以有不同的类型值。
- 和共享内存一样,消息队列的资源也必须自行删除,否则不会自动清除,因为system V IPC资源的生命周期是随内核的。
System V信号量
信号量相关概念
- 由于进程要求共享资源,而且有些资源需要互斥使用,因此各进程间竞争使用这些资源,进程的这种关系叫做进程互斥。
- 系统中某些资源一次只允许一个进程使用,称这样的资源为临界资源或互斥资源。
- 在进程中涉及到临界资源的程序段叫临界区。
- IPC资源必须删除,否则不会自动删除,因为system V IPC的生命周期随内核。
信号量数据结构
在系统当中也为信号量维护了相关的内核数据结构:
信号量的数据结构如下:
struct semid_ds {
struct ipc_perm sem_perm; /* permissions .. see ipc.h */
__kernel_time_t sem_otime; /* last semop time */
__kernel_time_t sem_ctime; /* last change time */
struct sem *sem_base; /* ptr to first semaphore in array */
struct sem_queue *sem_pending; /* pending operations to be processed */
struct sem_queue **sem_pending_last; /* last pending operation */
struct sem_undo *undo; /* undo requests on this array */
unsigned short sem_nsems; /* no. of semaphores in array */
};
信号量数据结构的第一个成员也是ipc_perm类型的结构体变量,ipc_perm结构体的定义如下:
struct ipc_perm{
__kernel_key_t key;
__kernel_uid_t uid;
__kernel_gid_t gid;
__kernel_uid_t cuid;
__kernel_gid_t cgid;
__kernel_mode_t mode;
unsigned short seq;
};
进程互斥
信号量主要用于同步和互斥的:
- 由于各进程要求共享资源,而且有些资源需要互斥使用,因此各进程间竞争使用这些资源,进程的这种关系为进程的互斥;
- 系统中某些资源一次只允许一个进程使用,称这样的资源为临界资源或互斥资源;
- 在进程中涉及到互斥资源的程序段叫临界区;
- IPC资源必须删除,否则不会自动清除,除非重启,所以system V IPC资源的生命周期随内核。
信号量就是用来保护临界区的,信号量分为二元信号量和多元信号量。
比如当前有一块大小为100字节的资源,我们若是以25字节为一份,那么该资源可以被分为4份,那么此时这块资源可以由4个信号量进行标识:
信号量本质是一个计数器,在二元信号量中,信号量的个数为1(相当于将临界资源看成一整块),二元信号量本质解决了临界资源的互斥问题,以下面的伪代码进行解释:
当进程A申请访问共享内存资源时,如果此时sem为1(sem代表当前信号量个数),则进程A申请资源成功,此时需要将sem减减,然后进程A就可以对共享内存进行一系列操作,但是在进程A在访问共享内存时,若是进程B申请访问该共享内存资源,此时sem已经为0了,那么这时进程B会被挂起,直到进程A访问共享内存结束后将sem加加,此时才会将进程B唤起,然后进程B再对该共享内存进行访问操作。
在这种情况下,无论什么时候都只会有一个进程在对同一份共享内存进行访问操作,也就解决了临界资源的互斥问题。
实际上,代码中计数器sem减减的操作就叫做P操作,而计数器加加的操作就叫做V操作,P操作就是申请信号量,而V操作就是释放信号量: