文章目录
- 3.6 systemv共享内存
- 3.6.1 共享内存函数
- 3.6.3 一个简单的共享内存代码实现
- 3.6.4 一个复杂的共享内存代码实现
- 3.6.4 key和shmid的主要区别:
- 3.7 systemv消息队列(了解)
- 3.8 systemv信号量(了解)
- 进程互斥
- 四个问题
- 理解信号量
3.6 systemv共享内存
共享内存区是最快的IPC形式。一旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到内核,换句话说是进程不再通过执行进入内核的系统调用来传递彼此的数据。
进程间通信的本质是:先让不同的进程,看到同一份资源。
如果要释放共享内存:要去除关联,释放共享内存
上面的操作都是进程直接做的吗?
不是。直接由操作系统来做。
共享内存的生命周期是随内核的。
用户不主动关闭,共享内存会一直存在。除非内核重启(用户释放)
生成IPC(进程间通信)的
key
值:函数原型:
key_t ftok(const char *pathname, int proj_id);
参数含义:
pathname
:一个已存在的文件路径
- 必须是一个已存在的文件或目录的路径
- 用于生成唯一的
key
值- 程序需要有该路径的访问权限
proj_id
:项目标识符
- 项目标识符,用于区分不同的IPC资源
- 只有低
8
位有效(1-255
)- 通常使用字符或数字
返回值:
- 成功:成功:返回一个非负的 key 值
- 失败:返回-1
3.6.1 共享内存函数
shmget
函数
功能:用来创建共享内存
原型
int shmget(key_t key, size_t size, int shmflg);
参数
key:这个共享内存段名字
size:共享内存大小
shmflg:由九个权限标志构成,它们的用法和创建文件时使用的mode模式标志是一样的
返回值:成功返回一个非负整数,即该共享内存段的标识码;失败返回-1
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:将要采取的动作(有三个可取值)
#define IPC_STAT 2 // 获取共享内存状态
#define IPC_SET 1 // 设置共享内存状态
#define IPC_RMID 0 // 删除共享内存段
buf:指向一个保存着共享内存的模式状态和访问权限的数据结构
返回值:成功返回0;失败返回-1
shmget
- 创建/获取共享内存
int shmid = shmget(key, 1024, IPC_CREAT | 0666);
- 相当于"申请"一块共享内存
- 类比文件操作中的
open
创建文件 - 返回共享内存标识符(shmid),用于后续操作
shmat
- 挂载/连接共享内存
void *addr = shmat(shmid, NULL, 0);
- 将共享内存映射到进程的地址空间
- 类比把硬盘上的文件加载到内存
- 返回可以直接操作的内存指针
shmdt
- 断开连接
shmdt(addr);
- 解除进程与共享内存的映射关系
- 类比关闭打开的文件
- 不会删除共享内存,只是当前进程不再使用
shmctl
- 控制共享内存
shmctl(shmid, IPC_RMID, NULL); // 删除共享内存
- 用于删除或管理共享内存
- 类比文件的删除、权限修改等操作
3.6.3 一个简单的共享内存代码实现
写进程 (writer.cpp):
#include <iostream>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <string.h>
#include <unistd.h>
#define SHM_SIZE 1024
int main() {
// 1. 生成key
key_t key = ftok(".", 'x');
// 这里的 "." 表示当前目录必须是一个存在且可访问的路径
// 这里的 'x' 是一个字符,会被转换为8位整数,范围是1-255,只有低8位有效
if(key == -1) {
std::cout << "ftok失败" << std::endl;
return 1;
}
// 2. 创建共享内存
int shmid = shmget(key, SHM_SIZE, IPC_CREAT | 0666);
if(shmid == -1) {
std::cout << "shmget失败" << std::endl;
return 1;
}
// 3. 连接共享内存
void* shmaddr = shmat(shmid, NULL, 0);
if(shmaddr == (void*)-1) { //(void*)-1 是 shmat 失败时的返回值,等同于 MAP_FAILED 或 -1
std::cout << "shmat失败" << std::endl;
return 1;
}
// 4. 写入数据
const char* message = "Hello from shared memory!";
strcpy((char*)shmaddr, message); // shmaddr 是共享内存的起始地址
// (char*) 是类型转换,将 void* 转换为 char*
// strcpy 将字符串复制到共享内存中
std::cout << "写入数据: " << message << std::endl;
// 5. 分离共享内存
shmdt(shmaddr);
return 0;
}
读进程 (reader.cpp):
#include <iostream>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <string.h>
#include <unistd.h>
#define SHM_SIZE 1024
int main() {
// 1. 生成相同的key
key_t key = ftok(".", 'x');
if(key == -1) {
std::cout << "ftok失败" << std::endl;
return 1;
}
// 2. 获取共享内存
int shmid = shmget(key, SHM_SIZE, 0666);
if(shmid == -1) {
std::cout << "shmget失败" << std::endl;
return 1;
}
// 3. 连接共享内存
void* shmaddr = shmat(shmid, NULL, 0);
if(shmaddr == (void*)-1) {
std::cout << "shmat失败" << std::endl;
return 1;
}
// 4. 读取数据
std::cout << "读取数据: " << (char*)shmaddr << std::endl;
// 5. 分离共享内存
shmdt(shmaddr);
// 6. 删除共享内存
shmctl(shmid, IPC_RMID, NULL);
return 0;
}
使用方法:
- 编译:
g++ writer.cpp -o writer
g++ reader.cpp -o reader
- 运行:
# 终端1
./writer
# 终端2
./reader
主要函数说明:
ftok()
: 生成IPC
键值shmget()
: 创建或获取共享内存段shmat()
: 连接共享内存段到进程地址空间shmdt()
: 断开共享内存段连接shmctl()
: 控制共享内存段(如删除)
执行时序:
写进程 共享内存 读进程
| | |
| | |
|---ftok(".", 'x') | |
|生成key | |
| | |
|---shmget() | |
|创建共享内存--------------->| |
| | |
|---shmat() | |
|连接共享内存<-------------->| |
| | |
|---strcpy() | |
|写入数据------------------>| |
| | |
|---shmdt() | |
|断开连接------------------>| |
| | |
| | ftok(".", 'x')----------|
| | 生成相同的key |
| | |
| | shmget()----------------|
| |<--获取共享内存 |
| | |
| | shmat()-----------------|
| |<->连接共享内存 |
| | |
| | 读取数据-----------------|
| |-->读取内容 |
| | |
| | shmdt()-----------------|
| |<--断开连接 |
| | |
| | shmctl()----------------|
| |x--删除共享内存 |
| | |
3.6.4 一个复杂的共享内存代码实现
makefile
.PHONY:all
all:processa processb
processa:processa.cc
g++ -o $@ $^ -g -std=c++11
processb:processb.cc
g++ -o $@ $^ -g -std=c++11
.PHONY:clean
clean:
rm -f processa processb
log.hpp
#pragma once // 防止头文件重复包含
// 包含必要的系统头文件
#include <iostream>
#include <time.h>
#include <stdarg.h> // 用于可变参数
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#define SIZE 1024 // 缓冲区大小
// 定义日志级别
#define Info 0
#define Debug 1
#define Warning 2
#define Error 3
#define Fatal 4
// 定义日志输出方式
#define Screen 1 // 输出到屏幕
#define Onefile 2 // 输出到单个文件
#define Classfile 3 // 根据日志级别输出到不同文件
#define LogFile "log.txt" // 默认日志文件名
class Log
{
public:
Log()
{
printMethod = Screen; // 默认输出到屏幕
path = "./log/"; // 默认日志路径
}
// 设置日志输出方式
void Enable(int method)
{
printMethod = method;
}
// 将日志级别转换为字符串
std::string levelToString(int level)
{
switch (level)
{
case Info:
return "Info";
case Debug:
return "Debug";
case Warning:
return "Warning";
case Error:
return "Error";
case Fatal:
return "Fatal";
default:
return "None";
}
}
// 根据不同的输出方式打印日志
void printLog(int level, const std::string &logtxt)
{
switch (printMethod)
{
case Screen:
std::cout << logtxt << std::endl;
break;
case Onefile:
printOneFile(LogFile, logtxt);
break;
case Classfile:
printClassFile(level, logtxt);
break;
default:
break;
}
}
// 输出到单个文件
void printOneFile(const std::string &logname, const std::string &logtxt)
{
std::string _logname = path + logname;
int fd = open(_logname.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0666);
if (fd < 0)
return;
write(fd, logtxt.c_str(), logtxt.size());
close(fd);
}
// 根据日志级别输出到不同文件
void printClassFile(int level, const std::string &logtxt)
{
std::string filename = LogFile;
filename += ".";
filename += levelToString(level);
printOneFile(filename, logtxt);
}
// 重载函数调用运算符,支持可变参数的日志打印
void operator()(int level, const char *format, ...)
{
// 获取当前时间
time_t t = time(nullptr);
struct tm *ctime = localtime(&t);
char leftbuffer[SIZE];
// 格式化时间和日志级别信息
snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]", levelToString(level).c_str(),
ctime->tm_year + 1900, ctime->tm_mon + 1, ctime->tm_mday,
ctime->tm_hour, ctime->tm_min, ctime->tm_sec);
// 处理可变参数
va_list s;
va_start(s, format);
char rightbuffer[SIZE];
vsnprintf(rightbuffer, sizeof(rightbuffer), format, s);
va_end(s);
// 组合完整的日志文本
char logtxt[SIZE * 2];
snprintf(logtxt, sizeof(logtxt), "%s %s\n", leftbuffer, rightbuffer);
printLog(level, logtxt);
}
~Log()
{
}
// void logmessage(int level, const char *format, ...)
// {
// time_t t = time(nullptr);
// struct tm *ctime = localtime(&t);
// char leftbuffer[SIZE];
// snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]", levelToString(level).c_str(),
// ctime->tm_year + 1900, ctime->tm_mon + 1, ctime->tm_mday,
// ctime->tm_hour, ctime->tm_min, ctime->tm_sec);
// // va_list s;
// // va_start(s, format);
// char rightbuffer[SIZE];
// vsnprintf(rightbuffer, sizeof(rightbuffer), format, s);
// // va_end(s);
// // 格式:默认部分+自定义部分
// char logtxt[SIZE * 2];
// snprintf(logtxt, sizeof(logtxt), "%s %s\n", leftbuffer, rightbuffer);
// // printf("%s", logtxt); // 暂时打印
// printLog(level, logtxt);
// }
private:
int printMethod; // 日志输出方式
std::string path; // 日志文件路径
};
// int sum(int n, ...)
// {
// va_list s; // char*
// va_start(s, n);
// int sum = 0;
// while(n)
// {
// sum += va_arg(s, int); // printf("hello %d, hello %s, hello %c, hello %d,", 1, "hello", 'c', 123);
// n--;
// }
// va_end(s); //s = NULL
// return sum;
// }
comm.hpp
#ifndef __COMM_HPP__
#define __COMM_HPP__
// 包含必要的头文件
#include <iostream>
#include <string>
#include <cstdlib>
#include <cstring>
#include <sys/ipc.h> // 系统IPC功能
#include <sys/shm.h> // 共享内存
#include <sys/types.h>
#include <sys/stat.h>
#include "log.hpp"
using namespace std;
Log mylog; // 全局日志对象
// 共享内存的大小一般建议是4096的整数倍
const int size = 4096;
const string pathname="/home/ydk_108"; // 用于生成key的路径
const int proj_id = 0x6666; // 项目ID
// 获取IPC key
key_t GetKey()
{
key_t k = ftok(pathname.c_str(), proj_id);
if(k < 0)
{
mylog(Fatal, "ftok error: %s", strerror(errno));
exit(1);
}
mylog(Info, "ftok success, key is : 0x%x", k);
return k;
}
// 获取共享内存的辅助函数
int GetShareMemHelper(int flag)
{
key_t k = GetKey();
int shmid = shmget(k, size, flag);
if(shmid < 0)
{
mylog(Fatal, "create share memory error: %s", strerror(errno));
exit(2);
}
mylog(Info, "create share memory success, shmid: %d", shmid);
return shmid;
}
// 创建新的共享内存
int CreateShm()
{
return GetShareMemHelper(IPC_CREAT | IPC_EXCL | 0666);
}
// 获取已存在的共享内存
int GetShm()
{
return GetShareMemHelper(IPC_CREAT);
}
#define FIFO_FILE "./myfifo" // 命名管道文件路径
#define MODE 0664 // 文件权限
// 错误码枚举
enum
{
FIFO_CREATE_ERR = 1,
FIFO_DELETE_ERR,
FIFO_OPEN_ERR
};
// 初始化类,用于创建和清理命名管道
class Init
{
public:
Init()
{
// 先尝试删除已存在的管道文件
unlink(FIFO_FILE); // 忽略返回值,因为文件可能不存在
// 创建命名管道
int n = mkfifo(FIFO_FILE, MODE);
if (n == -1)
{
perror("mkfifo");
exit(FIFO_CREATE_ERR);
}
}
~Init()
{
// 删除命名管道
int m = unlink(FIFO_FILE);
if (m == -1)
{
perror("unlink");
exit(FIFO_DELETE_ERR);
}
}
};
#endif
processa.cc
#include "comm.hpp"
extern Log mylog;
int main()
{
Init init; // 创建命名管道
int shmid = CreateShm(); // 创建共享内存
// 将共享内存映射到进程地址空间
char *shmaddr = (char*)shmat(shmid, nullptr, 0);
// 以只读方式打开命名管道
int fd = open(FIFO_FILE, O_RDONLY);
if (fd < 0)
{
mylog(Fatal, "error string: %s, error code: %d", strerror(errno), errno);
exit(FIFO_OPEN_ERR);
}
struct shmid_ds shmds;
while(true)
{
// 读取管道中的通知
char c;
ssize_t s = read(fd, &c, 1);
if(s == 0) break; // 写端关闭
else if(s < 0) break; // 读取错误
// 直接从共享内存读取数据
cout << "client say@ " << shmaddr << endl;
sleep(1);
// 获取并打印共享内存的状态信息
shmctl(shmid, IPC_STAT, &shmds);
cout << "shm size: " << shmds.shm_segsz << endl;
cout << "shm nattch: " << shmds.shm_nattch << endl;
printf("shm key: 0x%x\n", shmds.shm_perm.__key);
cout << "shm mode: " << shmds.shm_perm.mode << endl;
}
// 清理资源
shmdt(shmaddr); // 解除内存映射
shmctl(shmid, IPC_RMID, nullptr); // 删除共享内存
close(fd); // 关闭管道
return 0;
}
processb.cc
#include "comm.hpp"
int main()
{
int shmid = GetShm(); // 获取已存在的共享内存
// 将共享内存映射到进程地址空间
char *shmaddr = (char*)shmat(shmid, nullptr, 0);
// 以只写方式打开命名管道
int fd = open(FIFO_FILE, O_WRONLY);
if (fd < 0)
{
mylog(Fatal, "error string: %s, error code: %d", strerror(errno), errno);
exit(FIFO_OPEN_ERR);
}
while(true)
{
cout << "Please Enter@ ";
// 读取用户输入并直接写入共享内存
fgets(shmaddr, 4096, stdin);
// 向管道写入一个字符,通知接收端
write(fd, "c", 1);
}
// 清理资源
shmdt(shmaddr); // 解除内存映射
close(fd); // 关闭管道
return 0;
}
打印:
关于
key
:
key
是一个数字,这个数字是几,不重要。关键在于它必须在内核中具有唯一性,能够让不同的进程进行唯一性标识。第一个进程可以通过
kev
创建共享内存,第二个之后的进程,只要拿着同一个key
就可以和第一个进程看到同一个共享内存了。对于一个已经创建好的共享内存,
key
在哪?
key
在共享内存的描述对象中。第一次创建的时候,必须有一个
key
了。怎么有?
key
类似路径唯一
3.6.4 key和shmid的主要区别:
-
基本概念
key
:是一个用户定义的值,用来标识共享内存段的访问权限,类似于文件路径名shmid
:是系统分配的共享内存段标识符,是系统内部使用的唯一标识符 -
使用时机
key
:在创建或获取共享内存时使用shmid
:在共享内存创建后由系统返回,后续操作都使用shmid
-
代码示例
#include <sys/shm.h>
// 使用key创建共享内存
key_t key = ftok("/tmp", 'A'); // 创建key
int shmid = shmget(key, 1024, IPC_CREAT | 0666); // 用key获取shmid
// 后续操作使用shmid
void *addr = shmat(shmid, NULL, 0); // 连接共享内存
shmctl(shmid, IPC_RMID, NULL); // 删除共享内存
-
关系
一个
key
可以对应一个shmid
key
是用户层面的标识shmid
是系统层面的标识 -
生命周期
key
:可以重复使用shmid
:随共享内存段的存在而存在,删除后失效
3.7 systemv消息队列(了解)
消息队列提供了一个从一个进程向另外一个进程发送一块数据的方法
每个数据块都被认为是有一个类型,接收者进程接收的数据块可以有不同的类型值
特性方面:
IPC
资源必须删除,否则不会自动清除,除非重启,所以system V IPC
资源的生命周期随内核
通过消息队列想让
A
和B
进行通信那么首先要让不同进程看到同一份资源。
必须让不同进程看到同一个队列
允许不同的进程,向内核中发送带类型的数据块(通过类型来区分数据块是属于谁的)
A
进程可以把它的数据块放到队列中,B
进程可以把它的数据块放到队列中。
A
进程就可以从队列中拿B
进程给A
进程发的数据块,反之亦然。
可以让A进程 <--以数据块的形式发送数据-->
B进程。
3.8 systemv信号量(了解)
信号量主要用于同步和互斥的。什么是同步和互斥?
进程互斥
由于各进程要求共享资源,而且有些资源需要互斥使用,因此各进程间竞争使用这些资源,进程的这种关系为进程的互斥。
系统中某些资源一次只允许一个进程使用,称这样的资源为临界资源或互斥资源。
在进程中涉及到互斥资源的程序段叫临界区。
特性方面:
IPC
资源必须删除,否则不会自动清除,除非重启,所以system V IPC
资源的生命周期随内核
四个问题
当我们的A正在写入,写入了一部分,就被B拿走了,导致双方发和收的数据不完整 – 数据不一致问题
A B看到的同一份资源,共享资源,如果不加保护,会导致数据不一致问题
我们可以通过“加锁”形成互斥访问 – 任何时刻,只允许一个执行流访问共享资源 – 互斥
共享的,任何时刻只允许一个执行流访问(就是执行访问代码)的资源我们一般称为:临界资源(一般是操作系统和用户维护的内存空间)(管道也是临界资源)
举例:100行代码,5~10行代码才在访问临界资源。 我们访问临界资源的代码在:临界区
理解信号量
信号量的本质是一个计数器。
描述临界资源数量的多少。
申请计数器成功,就表示我具有访问资源的权限了
申请了计数器资源,我当前访问我要的资源了吗?没有。申请了计数器资源是对资源的预订机制
计数器可以有效保证进入共享资源的执行流的数量
所以每一个执行流,想访问共享资源中的一部分的时候,不是直接访问,而是先申请计数器资源。
程序员把这个"计数器",叫做信号量。
申请信号量,本质是对计数器
--
,P操作释放资源,释放信号量,本质是对计数器进行
++
操作,V操作申请和释放
PV
操作是原子性操作。要么不做,要做就做完 — 两态的。没有“正在做”这样的概念。
信号量本质是一把计数器,
PV
操作,原子的。执行流申请资源,必须先申请信号量资源,得到信号量之后,才能访问临界资源。
信号量值1,0两态的,二元信号量,就是互斥功能
申请信号量的本质:是对临界资源的预订机制。
信号量凭什么是进程间通信的一种?
通信不仅仅是通信数据,双方互相协同也是。
要协同,本质也是通信,信号量首先要被所有的通信进程看到。
mmap
函数 – 也是共享内存。(仅作了解)
后面学习的信号和这里的信号量没有任何关系。