目录
一.system V共享内存——先让不同的进程看到同一份资源
(1)查看共享内存的命令 ipcs -m,删除共享内存 ipcrm -m (要删的共享内存的shmid)
一.system V共享内存——先让不同的进程看到同一份资源
进程问通信的前提是:先让不同的进程,看到同一份资源!
1.共享内存原理
监控共享内存脚本
while :; do ipcs -m; sleep 1; echo "############################";done
2.创建/获取 共享内存接口—shmget函数(shared memory get)
①创建共享内存——②删除共享内存
③关联共享内存——④去关联共享内存
这些操作都是OS内部帮我们做,跟进程关联
shmget函数:
创建一个System V 级别的共享内存段
int shmget(key_t key, size_t size, int shmflg);
返回值:成功返回共享内存标识符shmid;错误返回-1
① key:共享内存内核的唯一值(用函数ftok获取key)详解看 3.—>(2)(3)
② size:创建的共享内存大小。(建议设置成为页(4KB)的整数倍,因为操作系统和磁盘IO时基本单位是4KB)
③ shmflg的选项:
IPC_CREAT:创建共享内存,如果已经存在就获取之,不存在就创建之
IPC_ EXCL:不单独使用,必须和IPC_CREAT配合。如果不存在指定的共享内存,创建之;如果存在了,出错返回
IPC_CREAT和IPC_CREAT配合:可以保证,如果shmget函数调用成功—— 一定是一个全新的share memory!(即:如果只是使用IPC_CREAT返回后无法区分是新创建的还是原来就存在的,如果配合使用返回成功,就是新的共享内存;不成功就是原来已经存在的)
shmflg | 0666 :支持异或上权限
3.参数key解释
(1)共享内存存在哪里?
——内核中,内核会给我们维护共享内存的结构!共享内存也要被管理起来! !先描述,再组织!
(2)第一个参数key的解释
想表示存在不存在,就要先有方法标识共享内存的唯一性! !——用key标识
共享内存要被管理
struct shmid_ ds{} 中包含一个结构体 struct ipc_ perm ,这个结构体 struct ipc_ perm 中包含一个成员值叫 key (shmget) (key的作用:共享内存的唯一值! !这个key一般由用户提供)
(3)key一般由用户提供 解释
进程问通信的前提是:先让不同的进程看到同一份资源。如何保证让不同的进程看到同一份共享内存?——做法是:让他们拥有同一个key即可!(用函数ftok获取key)
命名管道——>约定使用同一个文件
共享内存——>约定好使用同一个唯一key,来进行通信的! !
当我们运行完毕创建全新的共享内存的代码后(进程退出),但是第二(n)次的时候,该代码无法运行,告诉我们file存在,即:共享内存是存在的!
systen-V下的共享内存,生命周期是随内核的! !
如果不显示的删除,只能通过kernel (os) 重启来解决!
(4)函数ftok获取key
作用:把一个文件路径和项目标识符转化为一个具有唯一性的数字。
key_t ftok(const char *pathname, int proj_id);
返回值:成功返回生成的key;失败返回-1
例如:
#define PATH_NAME "/home/whb/104"
#define PROJ_ID 0x14
key_t key = ftok(PATH_NAME, PROJ_ID) PROJ_ID随便设一个
4.ipcs,ipcrm命令详解
(1)查看共享内存的命令 ipcs -m,删除共享内存 ipcrm -m (要删的共享内存的shmid)
perm:权限; nattch:挂接进程数
(一)ipcs
查看消息队列、共享内存、信号量的使用情况
命令 | 功能 |
---|---|
ipcs 或 ipcs -a | 查看消息队列、共享内存、信号量的使用情况 |
ipcs -s | 查看信号量(signal) |
ipcs -m | 查看共享内存(memory) |
ipcs -q | 查看消息队列(queue) |
(二)ipcrm
(管理员)移除消息队列、共享内存、信号量
(三)ipcs
输出选项
命令 | 功能 |
---|---|
ipcs -t | 输出详细时间变化 |
ipcs -p | 输出正在ipc的进程号 |
ipcs -c | 输出ipc对应信息的创建者 |
ipcs -l | 输出该系统ipc的限制信息 |
ipcs -u | 输出该系统ipc各种状态信息 |
4.删除共享内存的系统接口 shmctl
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
shmid:共享内存标识符,表示要删哪一个共享内存。
cmd:选项有常用的IPC_RMID(立即删除,此时buf传nullptr即可删除)(还有IPC_SET设置,IPC_STAT拷贝选项)
例如 shmctl(shmid, IPC_RMID, nullptr);
5.关联函数shmat
shmat (attach-附上),shmdt(detach-拆卸)
void *shmat(int shmid, const void *shmaddr, int shmflg);
shmid:共享内存标识符。shmaddr:设置在地址空间哪个位置,现在设置为nullptr。shmflg:读写,现在设置为0
返回值:成功返回挂接的共享内存地址,出错返回(void*)-1。
返回值和malloc一个意思,你怎么用malloc的空间,你就怎么用共享内存的空间! ( 肯定不能free)
例如:char *str = (char *)shmat(shmid, nullptr, 0);
————————————————————————
shmdt(detach-拆卸)
int shmdt(const void *shmaddr);
shmaddr:就传shmat的返回值。就能去关联
6.我怎么知道,这个共享内存属于存在还是不存在?
只需拿曾经设置过的key和你要设置的key作对比,来确认共享内存是否存在。
7.让进程看到同一份资源的代码
Log.hpp
#pragma once
#include <iostream>
#include <ctime>
std::ostream &Log()
{
std::cout << "Fot Debug |" << " timestamp: " << (uint64_t)time(nullptr) << " | ";
return std::cout;
}
Comm.hpp
(.hpp是声明和定义写在一起的文件)
#pragma once
#include <iostream>
#include <cstring>
#include <cstdlib>
#include <cerrno>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#define PATH_NAME "/home/whb/104"
#define PROJ_ID 0x14
#define MEM_SIZE 4096
key_t CreateKey()
{
key_t key = ftok(PATH_NAME, PROJ_ID);
if(key < 0)
{
std::cerr <<"ftok: "<< strerror(errno) << std::endl;
exit(1);
}
return key;
}
Makefile
.PHONY:all
all: IpcShmCli IpcShmSer
IpcShmCli:IpcShmCli.cc
g++ -o $@ $^ -std=c++11
IpcShmSer:IpcShmSer.cc
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -f IpcShmCli IpcShmSer
lpcShmCli.cc
#include "Comm.hpp"
#include "Log.hpp"
#include <unistd.h>
using namespace std;
// 充当使用共享内存的角色
int main()
{
// 创建相同的key值
key_t key = CreateKey();
Log() << "key: " << key << "\n";
// 获取共享内存
int shmid = shmget(key, MEM_SIZE, IPC_CREAT);
if (shmid < 0)
{
Log() << "shmget: " << strerror(errno) << "\n";
return 2;
}
// 挂接
char *str = (char*)shmat(shmid, nullptr, 0);
// 用它
sleep(5);
// 去关联
shmdt(str);
return 0;
}
IpcShmSer.cc
#include "Comm.hpp"
#include "Log.hpp"
#include <unistd.h>
using namespace std;
// 我想创建全新的共享内存
const int flags = IPC_CREAT | IPC_EXCL;
// 充当使用共享内存的角色
int main()
{
key_t key = CreateKey();
Log() << "key: " << key << "\n";
Log() << "create share memory begin\n";
int shmid = shmget(key, MEM_SIZE, flags | 0666);
if (shmid < 0)
{
Log() << "shmget: " << strerror(errno) << "\n";
return 2;
}
Log() << "create shm success, shmid: " << shmid << "\n";
sleep(5);
// 1. 将共享内存和自己的进程产生关联attach
char *str = (char *)shmat(shmid, nullptr, 0);
Log() << "attach shm : " << shmid << " success\n";
sleep(5);
// 用它
// 2. 去关联
shmdt(str);
Log() << "detach shm : " << shmid << " success\n";
sleep(5);
// 删它
shmctl(shmid, IPC_RMID, nullptr);
Log() << "delete shm : " << shmid << " success\n";
sleep(5);
return 0;
}
二.共享内存的使用
1.共享内存使用的现象
(1)共享内存使用时 无访问控制—不会堵塞等待
我们把共享内存实际上是映射到了我们进程地址空间的用户空间了(堆和栈之间)。对每一个进程而言,挂接到自己的上下文中的共享内存,属于自己的空间,类似于堆空间或者栈空间,可以被用户直接使用,不需要调用系统接口。
共享内存,因为他自身的特性,它没有任何访问控制,共享内存被双方直接看到,属于双方的用户空间,可以直接通信,但是不安全!
(2)共享内存 是所有进程间通信 速度最快的!解释
管道:父进程把外设的数据写入(拷贝)到自己进程的上下文代码中,再把自己进程上下文代码write写入(拷贝)进管道文件中,子进程管道文件中的数据read读入(拷贝)进子进程上下文代码中,再把子进程上下文代码刷(拷贝)到外设上,要进行4次拷贝;
而共享内存:进程1把外设的数据写入(拷贝)共享内存时,进程2就能立马看到,并使用共享内存的数据,如果不刷到外设上,只需1次拷贝即可,所以共享内存最快
2.管道对共享内存进行访问控制 代码
基于共享内存+管道的一个访问控制的效果!
Log.hpp
#pragma once
#include <iostream>
#include <ctime>
std::ostream &Log()
{
std::cout << "Fot Debug |" << " timestamp: " << (uint64_t)time(nullptr) << " | ";
return std::cout;
}
Makefile
.PHONY:all
all: IpcShmCli IpcShmSer
IpcShmCli:IpcShmCli.cc
g++ -o $@ $^ -std=c++11
IpcShmSer:IpcShmSer.cc
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -f IpcShmCli IpcShmSer
Comm.hpp
#pragma once
#include <iostream>
#include <cstring>
#include <cstdlib>
#include <cerrno>
#include <cassert>
#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"
#define PATH_NAME "/home/whb/104"
#define PROJ_ID 0x14
#define MEM_SIZE 4096
#define FIFO_FILE ".fifo"
key_t CreateKey()
{
key_t key = ftok(PATH_NAME, PROJ_ID);
if(key < 0)
{
std::cerr <<"ftok: "<< strerror(errno) << std::endl;
exit(1);
}
return key;
}
void CreateFifo()
{
umask(0);
if(mkfifo(FIFO_FILE, 0666) < 0)
{
Log() << strerror(errno) << "\n";
exit(2);
}
}
#define READER O_RDONLY
#define WRITER O_WRONLY
int Open(const std::string &filename, int flags)
{
return open(filename.c_str(), flags);
}
int Wait(int fd)
{
uint32_t values = 0;
ssize_t s = read(fd, &values, sizeof(values));
return s;
}
int Signal(int fd)
{
uint32_t cmd = 1;
write(fd, &cmd, sizeof(cmd));
}
int Close(int fd, const std::string filename)
{
close(fd);
unlink(filename.c_str());
}
IpcShmSer.cc
#include "Comm.hpp"
#include "Log.hpp"
#include <unistd.h>
using namespace std;
// 我想创建全新的共享内存
const int flags = IPC_CREAT | IPC_EXCL;
// 充当使用共享内存的角色
int main()
{
CreateFifo();
int fd = Open(FIFO_FILE, READER);
assert(fd >= 0);
key_t key = CreateKey();
Log() << "key: " << key << "\n";
Log() << "create share memory begin\n";
int shmid = shmget(key, MEM_SIZE, flags | 0666);
if (shmid < 0)
{
Log() << "shmget: " << strerror(errno) << "\n";
return 2;
}
Log() << "create shm success, shmid: " << shmid << "\n";
// sleep(5);
// 1. 将共享内存和自己的进程产生关联attach
char *str = (char *)shmat(shmid, nullptr, 0);
Log() << "attach shm : " << shmid << " success\n";
// sleep(5);
// 用它
while(true)
{
// 让读端进行等待
if(Wait(fd) <= 0) break;
printf("%s\n", str);
sleep(1);
}
// 2. 去关联
shmdt(str);
Log() << "detach shm : " << shmid << " success\n";
// sleep(5);
// 删它
shmctl(shmid, IPC_RMID, nullptr);
Log() << "delete shm : " << shmid << " success\n";
Close(fd, FIFO_FILE);
// sleep(5);
return 0;
}
IpcShmCli.cc
#include "Comm.hpp"
#include "Log.hpp"
#include <cstdio>
#include <unistd.h>
using namespace std;
// 充当使用共享内存的角色
int main()
{
int fd = Open(FIFO_FILE, WRITER);
// 创建相同的key值
key_t key = CreateKey();
Log() << "key: " << key << "\n";
// 获取共享内存
int shmid = shmget(key, MEM_SIZE, IPC_CREAT);
if (shmid < 0)
{
Log() << "shmget: " << strerror(errno) << "\n";
return 2;
}
// 挂接
char *str = (char*)shmat(shmid, nullptr, 0);
// 用它
// sleep(5);
// 竟然没有使用任何的系统调用接口!
// str
while(true)
{
printf("Please Enter# ");
fflush(stdout);
ssize_t s = read(0, str, MEM_SIZE);
if(s > 0)
{
str[s] = '\0';
}
Signal(fd);
}
// int cnt = 0;
// while(cnt <= 26)
// {
// str[cnt] = 'A' + cnt;
// ++cnt;
// str[cnt] = '\0';
// sleep(5);
// }
// 去关联
shmdt(str);
return 0;
}
3.临界资源,临界区
(1)临界资源
被多个进程能够看到的公共资源 叫做 临界资源。(管道和共享内存都叫临界资源,只是管道是安全的,共享内存是不安全的)
如果没有对临界资源进行任何保护,对于临界资源的访问,双方进程在进行访问的时候,就都是乱序的,可能会因为读写交叉而导致的各种乱码,废弃数据,等访问控制方面的问题。(例如父子进程的任意printf,因为显示器就是父子进程的临界资源)
(2)临界区
对多个进程而言,进程代码中,有大量的代码,只有一部分代码,会访问临界资源,这部分访问临界资源的代码 叫做 临界区
我的
我们把一件事情,要不没做,要么做完了--- 原子性
没有中间状态
任何时刻,只允许一个进程,访问临界资源
原子性:我们把一件事情,要不没做,要么做完了——没有中间状态
互斥:任何时刻,只允许一个进程,访问临界资源
三.信号量,信号灯
1.信号量
信号量本质是一个计数器。定义:信号量是一个计数器,这个计数器对应的PV操作(V ++ P --)是原子的。信号量的PV操作:V ++归还资源,P --申请资源
你怎么证明放映厅里面特定的座位是你的呢?——我只要买到了票,这个座位就是我的。
看电影为类比,假设一个放映厅(临界资源)有100个座位(座位就是临界资源的一小块),100个座位对应100张票,用cnt计数:cnt=100,if(cnt <= 0) wait ;else return cnt--;卖一张票,cnt--,卖完了就等待。这个cnt就类似于信号量。
二元信号量:只能为0或为1的信号量称为二元信号量。为1的时候,表现的就是互斥特性。——类比看电影,这时一个放映厅(临界资源)就一个座位,只能一个人(进程)进入,当一个人进入坐上这个座位看电影时,这个行为就是互斥访问。
常规信号量:多元信号量
多个人看电影不是直接冲进放映厅抢座位,而是去抢票 -> 本质就是对 信号量(int cnt = 100) 进行抢占申请
任何人想看电影——必须先申请计数器cnt——如果申请成功,就一定能看到电影
任何进程想访问临界资源——必须现申请信号量——如果申请成功,就一定能访问临界资源中的一部分资源[信号量(1,0)互斥]
你怎么证明放映厅里面特定的座位是你的呢?——我只要买到了票,这个座位就是我的。
2.信号量操作
sem: -- :申请资源: P
sem:++:释放资源:V
信号量对应的操作是PV操作。PV操作是原子的
共享内存不做访问控制,可以通过信号量进行对资源保护!
创建 释放 对应操作 查看 删除
shmget:创建共享内存 shmctl shmat,shmdt ipcs -m ipcrm -m
msgget:创建消息队列 msgctl msgsnd,msgrcv ipcs -q ipcrm -q
semget:创建信号量 semctl semop -> +1 -1 PV ipcs -s ipcrm -s
结构体的第一个元素的地址,在数字上和结构体整体的地址大小是一样的!
3.信号量允许多个线程同时使用共享资源
信号量主要用于实现同步操作,只要资源数大于0就表示可获取,可访问。若要使用信号量模拟实现互斥,则需要初始化资源计数为1,表示资源只有一个,则只有一个执行流能访问
(2)以下哪几种方式可用来实现线程间通知和唤醒:( ) [多选]
作业内容
A.互斥锁
B.条件变量
C.信号量
D.读写锁
答案:线程间的通知和唤醒以及线程的等待这是线程间同步实现的基础,而信号量和条件变量通过提供的使线程等待和唤醒功能被用于实现线程间的同步,因此选择B和C
而A中的互斥锁,和D中的读写锁都是为了实现对共享资源安全访问操作的锁技术,并不包含有通知和唤醒线程的功能。
(3)信号量实现与条件变量有什么区别 [多选]
A.信号量既可以实现同步还可以实现互斥
B.条件变量既可以实现同步还可以实现互斥
C.条件变量需要搭配互斥锁使用,信号量不需要
D.信号量需要搭配互斥锁使用,条件变量不需要
- 条件变量提供了一个pcb阻塞队列以及阻塞和唤醒线程的接口用于实现同步,但是什么时候该唤醒以及什么时候该阻塞线程由程序员进行控制,而这个控制通常需要一个共享资源的条件判断完成,因此条件变量还需要搭配互斥锁使用,来保护这个共享资源的条件判断及操作。
- 信号量提供一个pcb等待队列,以及一个实现了原子操作的对资源进行计数的计数器,通过自身计数器实现同步的条件判断,因此不需要搭配互斥锁使用,而且信号量在初始化计数为1的情况下也可以模拟实现互斥操作。