🌠 作者:@阿亮joy.
🎆专栏:《学会Linux》
🎇 座右铭:每个优秀的人都有一段沉默的时光,那段时光是付出了很多努力却得不到结果的日子,我们把它叫做扎根
目录
- 👉system V共享内存👈
- 共享内存的原理
- 对共享内存的认识
- 共享内存函数
- 👉system V消息队列(了解)👈
- 👉system V信号量👈
- 进程互斥
- 👉总结👈
👉system V共享内存👈
共享内存的原理
共享内存区是最快的 IPC 形式。一旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到内核。换句话说是进程不再通过执行进入内核的系统调用来传递彼此的数据,进程如果要读写,直接进行内存即的读和写接口。而之前学习的 pipe 和 fifo 都要通过 read 和 write 系统调用才能进行通信,原因就是管道的本质是文件,文件是内核中的一种数据结构,由操作系统维护(3 到 4 G的内核空间),用户无权进行直接访问,只能通过系统调用来进行访问。注:堆栈之间的共享内存属于用户空间,内核空间是 3 到 4 G 之间的 1 G 内存空间。
对共享内存的认识
- 共享内存不属于通信的任意一个进程,其属于操作系统,由操作系统所管理。
- 管道的本质是文件,操作系统已经有相应的内核数据结构来管理文件,因此不需要再去设计新的内核数据结构去管理管道。而共享内存是专门为了进程间通信而设计的,操作系统可能会有很多共享内存,那么操作系统就需要将这些共享内存管理起来。
- 管理的方式是先描述再组织,那么共享内存就等于共享内存块加上共享内存对应的内核数据结构。
- 对共享内存的修改包括对属性的修改和对内容的修改。
共享内存内核数据结构
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 */
};
共享内存函数
- shmget 函数的功能是创建或获取共享内存,需要指明共享内存的权限,失败时的返回值是 -1。
- ftok 函数的功能将 pathname 和 project id 经过一定的算法转化成 system V IPC key。pathname 必须存在,project id 不能为 0。失败时的返回值是 -1。
- shmat 函数的功能是将共享内存段连接到进程地址空间(建立页表映射关系)。第一个参数是共享内存的标识符 shmid;第二个参数是指定连接的地址,为 nullptr 时,则让操作系统指定连接到合适的地址上;第三个参数是 shmflg,它的两个可能取值是 SHM_RND 和 SHM_RDONLY。shmflg 等于 SHM_RDONLY 时,表示连接操作用来只读共享内存。成功返回一个指针,指向共享内存第一个字节;失败返回 (void*) -1。
- shmdt 函数的功能是将共享内存段与当前进程脱离。shmaddr 是由 shmat 函数所返回的指针,成功返回 0;失败返回 -1。注意:将共享内存段与当前进程脱离不等于删除共享内存段。
- shmctl 函数的功能是用于控制共享内存。shmid 是由 shmget 函数返回的共享内存标识符;cmd 是将要采取的动作(有三个可取值);buf 为指向一个保存着共享内存的模式状态和访问权限的数据结构,不关心共享内存的内核数据结果是,buf 可以设置为 nullptr。
注:当进程运行结束,进程创建的共享内存还会存在。这是因为 system V IPC 资源的生命周期是随着其内核的,其内核可以通过代码删除(上述的 shmctl 函数),也可以通过 ipcrm -m shmid 指令手动删除共享内存。使用 ipcs -m 指令可以查看操作系统中已经创建的共享内存。
- key 和 shmid 的区别:只有在创建共享内存时,使用到 key。大部分情况下,用户都是通过 shmid 来访问共享内存的。
客户端和服务端的模拟实现
# Makefile
.PHONY:all
all:shmClient shmServer
shmClient:shmClient.cc
g++ -o $@ $^ -std=c++11
shmServer:shmServer.cc
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -f shmClient shmServer
// 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
// Comm.h
#pragma once
#include <iostream>
#include <cstdio>
#include <cassert>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/stat.h>
#include <fcntl.h>
#include "Log.hpp"
using namespace std; // 将std直接展开简答,但不推荐
#define PATH_NAME "/home/Joy"
#define PROJ_ID 0x66
#define SHM_SIZE 4096 //共享内存的大小,最好是页(PAGE:4096KB)的整数倍
// 将十进制转为十六进制
string TransToHex(key_t k)
{
char buffer[32];
snprintf(buffer, sizeof buffer, "0x%x", k);
return buffer;
}
// shmServer.cc
#include "Comm.hpp"
int main()
{
// 1. 创建公共的key值
key_t k = ftok(PATH_NAME, PROJ_ID);
assert(k != -1);
Log("create key done", Debug) << "server key: " << TransToHex(k) << endl;
// 2. 创建共享内存(建议通信的发起者创建一个全新的共享内存)
// 创建共享内存是也要指定权限
int shmid = shmget(k, SHM_SIZE, IPC_CREAT | IPC_EXCL | 0666);
if(shmid == -1)
{
Log("create shm fail", Debug) << endl;
exit(1);
}
Log("create shm done", Debug) << "shmid: " << shmid << endl;
// 3. 建立页表映射(将共享内存挂接到当前进程地址空间)
char* shmaddr = (char*)shmat(shmid, nullptr, 0);
if(shmaddr == (char*)-1)
{
Log("attch shm fail", Debug) << "shmid: " << shmid << endl;
}
Log("attch shm done", Debug) << "shmid: " << shmid << endl;
// 4. 进程通信
// 可以将共享内存看成一个大的字符串
// 注意:共享内存创建好全部都会被置成0
for(;;)
{
printf("%s\n", shmaddr);
if(strcmp(shmaddr, "quit") == 0) break;
sleep(1);
}
// 5. 将指定的共享内存从自己的进程空间中去关联
int n = shmdt(shmaddr);
assert(n != -1);
(void)n;
Log("detach shm done", Debug) << "shmid: " << shmid << endl;
// 6. 删除共享内存,IPC_RMID即便还有进程挂接该共享内存,依旧删除该共享内存
n = shmctl(shmid, IPC_RMID, nullptr);
assert(n != -1);
(void)n;
Log("delete shm done", Debug) << "shmid: " << shmid << endl;
return 0;
}
// shmClient.cc
#include "Comm.hpp"
int main()
{
// 1. 客户端只需要获取服务端创建的共享内存即可
key_t k = ftok(PATH_NAME, PROJ_ID);
if(k == -1)
{
Log("create key fail", Error) << "client key: " << TransToHex(k) << endl;
exit(1);
}
Log("create key done", Debug) << "client key: " << TransToHex(k) << endl;
// 2. 获取共享内存
int shmid = shmget(k, SHM_SIZE, 0);
if(shmid == -1)
{
Log("create shm fail", Error) << "client key: " << TransToHex(k) << endl;
exit(2);
}
Log("create shm done", Debug) << "client key: " << TransToHex(k) << endl;
// 3. 挂接共享内存
char* shmaddr = (char*)shmat(shmid, nullptr, 0);
if(shmaddr == (char*)-1)
{
Log("attach shm failed", Error) << " client key : " << TransToHex(k) << endl;
exit(3);
}
Log("attach shm success", Debug) << " client key : " << TransToHex(k) << endl;
// 4. 使用,客户端将共享内存看做一个char类型的buffer
while(true)
{
ssize_t s = read(0, shmaddr, SHM_SIZE - 1);
if(s > 0)
{
shmaddr[s - 1] = '\0'; // 清除'\n'
if(strcmp(shmaddr, "quit") == 0) break;
}
}
// 以下代码是自动向共享内存中写入数据
// 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(5);
// }
// strcpy(shmaddr, "quit");
// 5. 去关联
int n = shmdt(shmaddr);
assert(n != -1);
Log("detach shm done", Debug) << "client key: " << TransToHex(k) << endl;
// 客户端不需要删除共享内存!
return 0;
}
现象:
- 就算客户端没有向共享内存中写入数据,服务端也会一直读取。
- 只要通信双方使用共享内存,一方直接向共享内存中写入数据,另一方就可以马上看到对方写入的数据。共享内存是所有进程间通信(IPC)中速度最快的!因为其不需要过多的拷贝(不需要将数据给操作系统)!!!
- 以共享内存的方式进行进程间通信缺乏访问控制,会带来并发控制!比如:写端还没将全部数据写入,读端就已经开始读取了,这将会带来巨大的问题!
给共享内存添加访问控制
// Comm.hpp
#pragma once
#include <iostream>
#include <cstdio>
#include <cassert>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/stat.h>
#include <fcntl.h>
#include "Log.hpp"
using namespace std; // 将std直接展开简答,但不推荐
#define PATH_NAME "/home/Joy"
#define PROJ_ID 0x66
#define SHM_SIZE 4096 //共享内存的大小,最好是页(PAGE:4096KB)的整数倍
#define FIFO_NAME "./fifo"
// 将十进制转为十六进制
string TransToHex(key_t k)
{
char buffer[32];
snprintf(buffer, sizeof buffer, "0x%x", k);
return buffer;
}
class Init
{
public:
Init()
{
umask(0);
int n = mkfifo(FIFO_NAME, 0666);
assert(n == 0);
(void)n;
Log("create fifo success", Notice) << endl;
}
~Init()
{
unlink(FIFO_NAME);
Log("delete fifo success", Notice) << endl;
}
};
#define READ O_RDONLY
#define WRITE O_WRONLY
int OpenFIFO(std::string pathname, int flags)
{
int fd = open(pathname.c_str(), flags);
assert(fd != -1);
return fd;
}
// 服务端等待客户端唤醒
void Wait(int fd)
{
Log("等待中......", Notice) << endl;
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) << endl;
}
void CloseFIFO(int fd)
{
close(fd);
}
// shmServer.cc
#include "Comm.hpp"
// 对应的程序在加载的时候,会自动构建全局变量,就要调用该类的构造函数 -- 创建管道文件
// 程序退出的时候,全局变量会被析构,自动调用析构函数,会自动删除管道文件
Init init; // 管道文件只有在服务端创建即可
int main()
{
// 1. 创建公共的key值
key_t k = ftok(PATH_NAME, PROJ_ID);
assert(k != -1);
Log("create key done", Debug) << "server key: " << TransToHex(k) << endl;
// 2. 创建共享内存(建议通信的发起者创建一个全新的共享内存)
// 创建共享内存是也要指定权限
int shmid = shmget(k, SHM_SIZE, IPC_CREAT | IPC_EXCL | 0666);
if(shmid == -1)
{
Log("create shm fail", Debug) << endl;
exit(1);
}
Log("create shm done", Debug) << "shmid: " << shmid << endl;
// 3. 建立页表映射(将共享内存挂接到当前进程地址空间)
char* shmaddr = (char*)shmat(shmid, nullptr, 0);
if(shmaddr == (char*)-1)
{
Log("attch shm fail", Debug) << "shmid: " << shmid << endl;
}
Log("attch shm done", Debug) << "shmid: " << shmid << endl;
// 4. 进程通信
// 可以将共享内存看成一个大的字符串
// 注意:共享内存创建好全部都会被置成0
// 服务端以读方式打开管道文件
int fd = OpenFIFO(FIFO_NAME, READ);
for(;;)
{
// 服务端等待客户端唤醒
Wait(fd);
printf("%s\n", shmaddr);
if(strcmp(shmaddr, "quit") == 0) break;
//sleep(1);
}
// 5. 将指定的共享内存从自己的进程空间中去关联
int n = shmdt(shmaddr);
assert(n != -1);
(void)n;
Log("detach shm done", Debug) << "shmid: " << shmid << endl;
// 6. 删除共享内存,IPC_RMID即便还有进程挂接该共享内存,依旧删除该共享内存
n = shmctl(shmid, IPC_RMID, nullptr);
assert(n != -1);
(void)n;
Log("delete shm done", Debug) << "shmid: " << shmid << endl;
CloseFIFO(fd);
return 0;
}
// shmClient.cc
#include "Comm.hpp"
int main()
{
// 1. 客户端只需要获取服务端创建的共享内存即可
key_t k = ftok(PATH_NAME, PROJ_ID);
if(k == -1)
{
Log("create key fail", Error) << "client key: " << TransToHex(k) << endl;
exit(1);
}
Log("create key done", Debug) << "client key: " << TransToHex(k) << endl;
// 2. 获取共享内存
int shmid = shmget(k, SHM_SIZE, 0);
if(shmid == -1)
{
Log("create shm fail", Error) << "client key: " << TransToHex(k) << endl;
exit(2);
}
Log("create shm done", Debug) << "client key: " << TransToHex(k) << endl;
// 3. 挂接共享内存
char* shmaddr = (char*)shmat(shmid, nullptr, 0);
if(shmaddr == (char*)-1)
{
Log("attach shm failed", Error) << " client key : " << TransToHex(k) << endl;
exit(3);
}
Log("attach shm success", Debug) << " client key : " << TransToHex(k) << endl;
// 客户端以写方式打开管道文件
int fd = OpenFIFO(FIFO_NAME, WRITE);
// 4. 使用,客户端将共享内存看做一个char类型的buffer
while(true)
{
ssize_t s = read(0, shmaddr, SHM_SIZE - 1);
if(s > 0)
{
shmaddr[s - 1] = '\0'; // 清除'\n'
Signal(fd); // 客户端唤醒服务端
if(strcmp(shmaddr, "quit") == 0) break;
}
}
// 以下代码是自动向共享内存中写入数据
// 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(5);
// }
// strcpy(shmaddr, "quit");
// 5. 去关联
int n = shmdt(shmaddr);
assert(n != -1);
Log("detach shm done", Debug) << "client key: " << TransToHex(k) << endl;
// 客户端不需要删除共享内存!
return 0;
}
因为管道具有访问控制,我们只要给共享内存加个管道就可以实现进程通信的访问控制了。该管道文件并不是用来通信的,而是用来访问控制的,管道文件内的数据是多少并不重要!
👉system V消息队列(了解)👈
- 消息队列(先进先出)提供了一个从一个进程向另外一个进程发送一块数据的方法。
- 每个数据块都被认为是有一个类型,接收者进程接收的数据块可以有不同的类型值。
- IPC 资源必须删除,否则不会自动清除,除非重启,所以 system V IPC 资源的生命周期随内核。
- 常用系统调用:ftok,msgget(创建消息队列),msgctl(控制消息队列),msgsnd(向消息队列发送数据),msgrcv(从消息队列中读取数据)等。
注:共享内存只有在当前映射连接数为 0 时才会被删除释放。
👉system V信号量👈
信号量主要用于同步和互斥的,下面先来看看什么是同步和互斥。
进程互斥
- 我们之前学习的所有的通信方式,本质都是优先解决一个问题:让不同的进程看到同一个资源!
- 让不同的进程看到同一个资源,比如共享内存,也带了一些时序问题,造成数据不一致问题。
- 多个进程(执行流)看到的公共的一份资源,称为临界资源或互斥资源。
- 进程中访问临界资源的代码,称为临界区。
- 多个执行流互相运行时互相干扰,是因为我们不加保护地访问了同样的资源(临界资源)。在非临界区,多个执行流不会互相干扰。
- 为了更好地进行临界区的保护,可以让多执行流在任何时候都只有一个进程进入临界区,这种特征称为互斥。
- 原子性是指事务的不可分割性,一个事务的所有操作要么不间断地全部被执行,要么一个也没有执行,没有任何其他的中间状态。
通过看电影买票的例子来理解信号量
信号量是对临界资源的预定机制!!!
👉总结👈
本篇博客主要讲解了什么是共享内存、共享内存的原理、用共享内存实现客户端和服务端的通信、什么是消息队列、消息量以及进程互斥等等。那么以上就是本篇博客的全部内容了,如果大家觉得有收获的话,可以点个三连支持一下!谢谢大家!💖💝❣️