system V
system V共享内存是内核中专门设计的通信的方式, 粗粒度划分操作系统分为进程管理, 内存管理, 文件系统, 驱动管理.., 粒度更细地分还有 进程间通信模块.
对于操作系统, 通信的场景有很多, 有以传送数据, 快速传送数据, 传送特定数据块, 进程间协同与控制以目的, 它们在接口实现上都不相同, 所以把操作系统中通信的方式聚集在一块, 接口统一之后形成了限于本主机通信的一种模式叫systemV
管道不属于system V, 它们是复用操作系统源代码, 属于比较原始的通信方式.
进程间通信的前提:必须让不同的进程看到同一份资源(必须由OS提供)
共享内存
原理
共享内存区是最快的IPC形式. 一旦这样的内存 映射到共享它的进程的地址空间, 这些进程间数据递不再涉及到内核, 换句话说是进程不再通过执行进入内核的系统调用来传递彼此的数据.
共享内存是专门设计用于IPC的, 它与进程地址空间的共享区有关.
共享内存作为一种通信方式, 所有关联了这块内存的进程都可以使用它, 这块内存不再是只属于一个进程的.
1. 操作系统中一定会同时存在很多的共享内存, 所以共享内存也要被操作系统管理,
要认识到开辟一块空间的内存占用应当是物理内存空间和共享内存的相关属性. 操作系统管理共享内存属性就是对其数据结构进行增删查改, 从而对它对应的物理内存空间进行实时监控管理.
共享内存结构:
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 */
};
struct ipc_perm {key_t __key; /* Key supplied to shmget(2) */uid_t uid; /* Effective UID of owner */gid_t gid; /* Effective GID of owner */uid_t cuid; /* Effective UID of creator */gid_t cgid; /* Effective GID of creator */unsigned short mode; /* Permissions + SHM_DEST andSHM_LOCKED flags */unsigned short __seq; /* Sequence number */};
2. 因为操作系统内一定会同时存在许多共享内存, 那这么多的共享内存中如何保证 A进程创建的共享内存 和 需要和A通信的进程, 看到的是同一份共享内存呢?
凡是被创建共享内存, 相关属性结构体中必须有一个能体现出该共享内存的唯一性的数据,这个数据就是key. key不光是为了保证共享内存的唯一性, 也是为了通过 key 能够让其他进程识别该共享内存, 从而不同的进程才能选择特定的共享内存进行通信.
key被封装在上面的struct ipc_perm结构体内.
关于唯一性标识, 除了key还有一个叫shmid, 在应用这个共享内存的时候, 我们使用shmid来进行操作共享内存,类似文件操作的fd, 而 key 不要在应用层使用, 只用来在内核中标识shm的唯一性! 类似inode.
关于 key 和 shmid 需要介绍一下相关系统接口:
系统接口
ftok
功能:用 路径名 和 项目标识符 转换为唯一标识符key返回
函数原型: key_t ftok(const char *pathname, int proj_id);
参数:
- pathname: 文件路径名
- proj_id: 项目标识符
- 返回值: 标识某一个共享内存的key值
key的作用: 这里的key用于形成共享内存的唯一标识shmid, 而这个key的生成规则是通过路径名和项目标识符生成的, 所以对于同一个共享内存的使用者就可以通过规定这两个参数都能得到同样的key, 从而通过shmget创建/获取同样的共享内存.
shmget
功能: 用来创建共享内存
原型: int shmget(key_t key, size_t size, int shmflg);
参数:
- key: 这个共享内存段名字
- size: 共享内存大小, 建议为4096的倍数
- shmflg: 由九个权限标志构成, 它们的用法和创建文件时使用的mode模式标志是一样的
- 返回值:成功返回一个非负整数, 即该共享内存段的标识码;失败返回-1
创建共享内存需要用到这两个标志:
首先要明确, shmget接口可以实现创建和使用共享内存,
1. 单独传入IPC_CREAT, 共享内存 存在就返回, 不存在就创建并返回
2. 传入IPC_CREAT|IPC_EXCL, 共享内存 存在就报错, 不存在就创建并返回, 注意IPC_EXCL单独传入没有意义, 和IPC_CREAT一起传入才有意义, 这两个标志位保证了创建的共享内存是全新的.
3. 传入0, 共享内存存在就返回, 不存在就报错.
shmat(at = attach)
功能: 将共享内存段连接到进程地址空间
原型: void * shmat(int shmid, const void *shmaddr, int shmflg);
参数:
- shmid: 共享内存标识
- shmaddr: 指定连接的地址
- shmflg: 它的两个可能取值是 SHM_RND 和 SHM_RDONLY
- 返回值: 成功返回一个指针, 指向共享内存首地址; 失败返回-1
这个返回值和 malloc 是类似的, 都可以强转为指定类型的地址, 但是malloc是在堆区开辟一块空间, 并不是共享内存.
shmdt (dt = detach)
功能: 将共享内存段与当前进程脱离
原型: 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删除共享内存, 共享内存依然存在. 只要系统不重启或显式地删除该共享内存, 它就会一直存在, 一个进程退出, 其他进程仍然可以通过
shmat
连接到共享内存并使用它, 直到调用shmctl
删除共享内存为止.
代码
实现一个server端和client端, server作为读端每隔两秒打印共享内存中的内容, client每隔一秒进行写入:
comm.hpp
#include <iostream>
#include <cstring>
#include <cerrno>
const char* pathName = "/home/zzy/linux_system_programing/inter-process_communication";
const int project_id = 0x11223344;
// 共享内存的大小,强烈建议设置成为n*4096
const int size = 4096;//创建一个key
key_t GetKey()
{key_t key = ftok(pathName, project_id);if(key < 0){std::cerr << "errno:" << errno << ", errno string:" << strerror(errno)<<std::endl;exit(1);}return key;
}//将key转换为0x开头字符串
std::string ToHex(key_t key)
{char buffer[1024];snprintf(buffer, sizeof(buffer), "0x%x", key);return buffer;
}//子函数
int CreateShmHelper(key_t key, int shmflg)
{int shmid = shmget(key, size, shmflg);if(shmid < 0){std::cerr << "errno:" << errno << ", errno string:" << strerror(errno)<<std::endl;exit(2);}return shmid;
}//创建共享内存
int CreateShm(key_t key)
{return CreateShmHelper(key, IPC_CREAT|IPC_EXCL|0644);
}//获取共享内存
int GetShm(key_t key)
{return CreateShmHelper(key, IPC_CREAT);//传0也可以
}
server.cc
#include <iostream>
#include <cstring>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#include "comm.hpp"
using namespace std;int main()
{key_t key = GetKey();cout << "key: " << ToHex(key) << endl;// key vs shmid// shmid: 应用这个共享内存的时候,我们使用shmid来进行操作共享内存,类似文件操作的FILE*// key: 不要在应用层使用,只用来在内核中标识shm的唯一性! 类似文件操作的fdint shmid = CreateShm(key);cout << "shmid: " << shmid << endl;char* s = (char*)shmat(shmid, nullptr, 0);cout << "shm attach done: " << shmid << endl;while(true){cout << s << endl;sleep(2);}//取消共享内存与进程地址空间的映射shmdt(s);cout << "shm dettach done: " << shmid << endl;sleep(3);//删除共享内存shmctl(shmid, IPC_RMID, nullptr);cout << "shm remove done " << shmid << endl;return 0;
}
client.cc
#include <iostream>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#include "comm.hpp"
using namespace std;int main()
{key_t key = GetKey();cout << "key: " << ToHex(key) << endl;int shmid = GetShm(key);cout << "shmid: " << shmid << endl;char* s = (char*)shmat(shmid, nullptr, 0);cout << "shm attach done: " << shmid << endl;for(char c = 'a'; c<='z'; c++){s[c-'a'] = c;cout << "write " << c << " done" <<endl;sleep(1);}shmdt(s);cout << "shm dettach done: " << shmid << endl;return 0;
}
由结果可以看到, 当server端启动后, client也启动然后与shm连接, 并每隔1秒向shm中发送数据, 而server每隔2秒读取shm中的内容 :
共享内存的同步方式: 不提供同步机制, 共享内存是直接裸露给所有的使用者使用的, 一定要注意共享内存的使用安全问题.
借助管道的同步机制, 可以让共享内存实现同步:
comm.hpp添加新建管道文件的函数:
//创建管道
bool MakeFifo()
{int fd = mkfifo(filename.c_str(), 0666);if(fd < 0){std::cerr << "errno: " << errno << ", errstring: " << strerror(errno) << std::endl;return false;}std::cout << "mkfifo success... read" << std::endl;return true;
}
server.cc借助类修改了一下初始化和清理资源的方式, 然后在while循环中读取管道传来的内容以实现同步:
#include <iostream>
#include <cstring>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#include "comm.hpp"
using namespace std;class Init
{
public:Init(){bool r = MakeFifo();if (!r)return;key_t key = GetKey();cout << "key: " << ToHex(key) << endl;// key vs shmid// shmid: 应用这个共享内存的时候,我们使用shmid来进行操作共享内存,类似文件操作的FILE*// key: 不要在应用层使用,只用来在内核中标识shm的唯一性! 类似文件操作的fdshmid = CreateShm(key);cout << "shmid: " << shmid << endl;s = (char *)shmat(shmid, nullptr, 0);cout << "shm attach done: " << shmid << endl;fd = open(filename.c_str(), O_RDONLY);}~Init(){// 取消共享内存与进程地址空间的映射shmdt(s);cout << "shm dettach done: " << shmid << endl;// 删除共享内存shmctl(shmid, IPC_RMID, nullptr);cout << "shm remove done " << shmid << endl;close(fd);unlink(filename.c_str());}public:char *s;int shmid;int fd;
};int main()
{Init init;while (true){int code = 0;ssize_t n = read(init.fd, &code, sizeof(code));if(n > 0){cout << init.s << endl;//sleep(2);}else if(n == 0){break;}}return 0;
}
client.cc 向共享内存写入一个字符就向管道传入内容通知server可以读取数据了(同步):
#include <iostream>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#include "comm.hpp"
using namespace std;int main()
{key_t key = GetKey();cout << "key: " << ToHex(key) << endl;int shmid = GetShm(key);cout << "shmid: " << shmid << endl;char* s = (char*)shmat(shmid, nullptr, 0);cout << "shm attach done: " << shmid << endl;int fd = open(filename.c_str(), O_WRONLY);for(char c = 'a'; c<='z'; c++){s[c-'a'] = c;cout << "write " << c << " done" <<endl;//通知对方int code = 1;ssize_t n = write(fd, &code, sizeof(code));sleep(1);}shmdt(s);cout << "shm dettach done: " << shmid << endl;close(fd);return 0;
}
这次可以发现 client 向 shm 写入一个数据, 就发送一次接收的指令, server就输出一次共享内存的内容, 完成了同步.
关于共享内存的内容如何清理:
shm只涉及进程向 shm 中写入和读取内容, 而 shm 中的内容并不会因为内容的读取就被移除, 所以shm的内容需要用户自己清理, 如何清理? 可以规定shm的前8/16个字节存放两个写入和读取指针, 通过两个指针对shm的内容进行维护.
关于shmctl接口查看共享内存结构体:
上面使用过IPC_RMID删除共享内存, 现在用 IPC_STAT 查看shmid_ds中的内容:
#include <iostream>
#include <cstring>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#include "comm.hpp"
using namespace std;class Init
{
public:Init(){key_t key = GetKey();cout << "key: " << ToHex(key) << endl;// key vs shmid// shmid: 应用这个共享内存的时候,我们使用shmid来进行操作共享内存,类似文件操作的FILE*// key: 不要在应用层使用,只用来在内核中标识shm的唯一性! 类似文件操作的fdshmid = CreateShm(key);cout << "shmid: " << shmid << endl;s = (char *)shmat(shmid, nullptr, 0);cout << "shm attach done: " << shmid << endl; }~Init(){// 取消共享内存与进程地址空间的映射shmdt(s);cout << "shm dettach done: " << shmid << endl;// 删除共享内存shmctl(shmid, IPC_RMID, nullptr);cout << "shm remove done " << shmid << endl;}public:char *s;int shmid;int fd;
};int main()
{Init init;struct shmid_ds ds;shmctl(init.shmid, IPC_STAT, &ds);std::cout << ToHex(ds.shm_perm.__key) << std::endl;std::cout << ds.shm_segsz << std::endl;std::cout << ds.shm_atime << std::endl;std::cout << ds.shm_cpid << std::endl;std::cout << ds.shm_nattch << std::endl;return 0;
}
总结:
缺点:
共享内存的同步方式, 不会提供同步机制, 共享内存是直接裸露给所有的使用者使用的, 一定要注意共享内存的使用安全问题.
优点:
1. 共享内存是所有进程间通信中速度最快的
2. 共享内存可以提供较大的空间
第二点进行具体说明:
因为拷贝次数少, 共享内存是所有进程间通信方式中速度最快的. 在同样的代码下, 考虑键盘输入和显示器输出(不考虑printf和scanf的缓冲区). 对比数据通过 共享内存 与 管道 通信的拷贝次数.
首先要明确, 凡事涉及到数据的迁移, 都是拷贝.
管道通信会经过四次拷贝: 输入文件(键盘)->用户缓冲区->管道文件缓冲区(内核空间)->用户缓冲区->输出文件(显示器)
而共享内存通信只需要两次拷贝:输入文件(键盘)->共享内存->输出文件(显示器)
消息队列
功能:
- 消息队列提供了一个从一个进程向另外一个进程发送一个数据块的方法, 消息队列和共享内存在使用上有一定差别, 但是它们的共享机制是一样的.
- 每个数据块都被认为是有一个类型, 接收者进程接收的数据块可以有不同的类型值
特性:
同共享内存一样, 消息队列也属于systemV, 而IPC资源必须删除, 否则不会自动清除, 除非重启,所以system V IPC资源的生命周期随内核
消息队列的相关接口和共享内存都是相似的, 因为都属于systemV:
系统接口
msgget
功能: 用来创建消息队列
原型: int msgget(key_t key, int shmflg);
参数:
- key: 这个消息队列段名字
- shmflg: 和共享内存一样
- 返回值:成功返回一个非负整数, 即该消息队列段的标识码;失败返回-1
msgctl
功能: 用于控制消息队列
原型: int msgctl(int msqid, int cmd, struct msqid_ds *buf);
参数:
- msqid: 由msgget返回的共享内存标识码.
- cmd: 将要采取的动作
- buf: 指向一个保存着消息队列的模式状态和访问权限的数据结构.
- 返回值: 成功返回0;失败返回-1.
msgsnd
功能: 用于向消息队列发消息
原型: int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
参数:
msqid:msgget的返回值
msgp: 要发送的数据块, 其中包含了数据的类型和内容, 需要用户自己去定义
msgsz: 数据块大小
msgflg:
msgrcv
功能: 接收消息队列的消息
原型: ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
参数:
msgflg:
IPC_NOWAIT:如果没有符合条件的消息可用,不阻塞进程,而是立即返回 EAGAIN 错误
MSG_EXCEPT: msgtype大于0的前提下, 接收除msgtpe以外类型的所有消息
msgtyp:
- 如果
msgtyp
大于 0,则msgrcv
函数将接收消息队列中类型字段等于msgtyp
的第一条消息。- 如果
msgtyp
等于 0,则msgrcv
函数将接收消息队列中的第一条消息。- 如果
msgtyp
小于 0,则msgrcv
函数将接收消息队列中类型字段小于或等于msgtyp
绝对值的第一条消息更多可以查看手册
void MsgQueue()
{//创建消息队列key_t key = GetKey();std::cout << "key: " << ToHex(key) << std::endl;int msgid = msgget(key, IPC_CREAT|IPC_EXCL);std::cout << "msgid: " << msgid << std::endl;//读取消息队列结构体struct msqid_ds ds; msgctl(msgid, IPC_STAT, &ds);std::cout << ToHex(ds.msg_perm.__key) << std::endl;std::cout << ds.msg_qbytes << std::endl;sleep(10);//删除消息队列msgctl(msgid, IPC_RMID, nullptr);}
运行结果:
ipcs -q
不同于共享内存, 这里内核里的key和我们传入key是不一样的.
信号量
信号量的本质是一个计数器, 用于保护共享资源. 信号量主要用于同步和互斥的,下面先来看看什么是同步和互斥
多个执行流看到的同一份资源(公共资源), 在并发访问时很有可能会发生数据不一致的问题, 所以公共资源就需要被保护起来, 就有了互斥与同步. (匿名管道, 命名管道, 消息队列都由OS提供了保护措施, 而共享内存需要用户自己保证安全, 比如上面代码用管道实现同步)
互斥: 任何一个时刻只允许一个执行流(进程)访问公共资源, 加锁完成
同步: 多个执行流执行的时候, 按照一定的顺序执行.
原子性: 一个操作或者一系列操作要么全部执行成功, 要么全部不执行, 不存在中间状态或者部分执行的情况.
临界资源: 被保护起来的公共资源
临界区: 访问该临界资源的代码, 维护临界资源, 其实就是在维护临界区
如何理解信号量?
举个例子, 比如看电影
每个电影院的座位都是有限的, 如果我们买了票, 即使不去使用这个座位, 这个座位也已经预定为我们的座位, 电影院和内部的座位是多个人共享的资源--公共资源, 我们买票的本质, 则是对资源的预定机制. 而一场电影的票的数量是有限的, 所以就需要设置一个计数器表示公共资源的个数, 买了一张票, 计数器就减一;退一张票, 计数器就加1, 如果计数器为0, 则买票失败.
信号量:
表示资源数目的计数器, 每一个执行流想要访问公共资源内部的某一份资源, 不应该让执行流直接访问, 而是先申请信号量资源, 申请成功就对信号量计数器做--操作, 申请不成功, 执行流将被挂起阻塞.
本质上, 只要--成功, 就完成了对资源的预定机制.
假如一份资源的信号量为1, 它被称为二元信号量, 也叫互斥锁, 完成资源的互斥功能.
所以如果一份共享内存整体只想被一个进程使用,可以使用二元信号量,以信号量的方式实现加锁解锁;如果所有进程都只想使用公共资源的一部分,比如一块16kb的共享内存每个进程只使用局部的一部分比如1kb,所以信号量可以设置为16,每个进程想访问这块共享内存首先要去申请信号量,申请成功才能使用,不成功则阻塞。
关于信号量的细节问题:
关于信号量的描述只是以整数计数器的形式去描述,但是其实并不是。
1. 每个进程访问共享资源都要先申请信号量意味着每个进程都得先看到同一个信号量资源,这只能由OS提供,所以信号量就也属于IPC体系, 不仅仅进程间通信属于IPC体系, 保证通信的安全也属于.
2. 信号量本质也是公共资源, 信号量是为了保护公共资源的, 但是谁去保护信号量呢, 所以对于信号量内部计数器的++或--操作, 必须是原子性的! (具体在多线程部分说明)
所以原子性的申请资源(--)的操作, 称为P; 原子性的释放资源(++)的操作, 称为V
3. 对于进程挂起/阻塞如何理解? 对于单个信号量, 简单地理解其结构为struct sem{int count; task_struct* wait_queue;} , 当信号量申请失败, 把进程设为阻塞状态并加入到对应的阻塞队列即可.