今天,带来Linux下的进程间通信讲解。文中不足错漏之处望请斧正!
是什么
进程间通信,Inter-Process Communication(IPC):
进程间通过访问同一块内存空间来进行数据的交流。
为什么
为什么要有IPC,它的作用是什么,场景是什么?
- 数据传输
- 资源共享
- 通知时间
- 控制进程
怎么做
让要通信的进程们拥有公共资源。
*谁实现IPC,公共资源都得由OS来提供。
为什么?
进程具有独立性(数据结构独立,代码和数据也独立),任何由进程提供的资源都只能被自己看见(进程地址空间)。所以进程们能看到的同一份公共资源,绝对只能由OS提供。
通信方式/风格
不同的公共资源来源,对应出不同的通信方式。
主要有三种方式:
- 管道
- System V IPC(重本地)
- POSIX IPC(重网络)
管道
是什么
内核的一块缓冲区,在内存中。
虽然形式上是文件,但匿名或命名管道文件只是一种标识符,使得进程能通过这个标识符访问内存中的同一块缓冲区。
管道也是用文件作进程公共资源的IPC方式。
分类:
- 匿名管道
- 命名管道
匿名管道
是什么
没有也不需要名字的管道文件。
实现原理和过程
通过血缘关系,打开同一个管道文件。
创建子进程,子进程会继承父进程的文件描述符表。有了同样的表,就可指向同一被打开文件。
- 父进程创建管道文件(并以读和写的方式分别打开)
- 创建子进程(子进程拷贝父进程
fd_array
)
- 根据读写需求关闭父子的读/写端
相关接口
int pipe(int fildes[2]);
- 作用
- 创建并打开管道文件。
- 参数
fds[2]
:是输出型参数,内含两个fd,分别是以读和写打开pipe文件的fd[0]
:读(0像嘴巴,读)fd[1]
:写(1像钢笔,写)
- 返回值
- 成功返回0
- 失败返回-1,错误码被设置
测试代码
int main()
{
int fds[2];
int n = pipe(fds);
assert(n == 0);
pid_t id = fork();
assert(id >= 0);
if(id == 0)
{
close(fds[0]);
int cnt = 1;
//循环写,但父进程读端关闭,所以写端进程也被OS发信号终止(异常退出)
while(1)
{
cout << "count:" << cnt << endl;
char buf[1024];
snprintf(buf, sizeof buf, "|child(%d): %d|", getpid(), cnt++);
write(fds[1], buf, strlen(buf));
sleep(1);
}
close(fds[1]);
cout << "子进程写端关闭!" << endl;
exit(0);
}
//读3次后关闭读端
close(fds[1]);
for(int i = 0; i < 3; ++i)
{
char buf[1024];
ssize_t read_ret = read(fds[0], buf, sizeof(buf) - 1);
if(read_ret > 0)
buf[read_ret] = '\0';
printf("parent(%d) got msg -- %s\n", getpid(), buf);
}
close(fds[0]);
cout << "父进程读端关闭!" << endl;
//父进程关闭读端后,子进程写端直接被OS终止
int status = 0;
n = waitpid(id, &status, 0);
assert(n == id);
cout << "wait success!" << endl;
return 0;
}
[bacon@VM-12-5-centos mypipe]$ ./mypipe
count:1
parent(26629) got msg -- |child(26630): 1|
count:2
parent(26629) got msg -- |child(26630): 2|
count:3
parent(26629) got msg -- |child(26630): 3|
父进程读端关闭!
count:4
wait success!
应用:进程池
设计思路:
父进程通过管道向子进程发送任务码,子进程获取并执行任务。
#include <iostream>
#include <vector>
#include <string>
#include <ctime>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <cassert>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
typedef void(*ptask)();
#define PROCESS_NUM 5
#define PLANT_SEED() srand((unsigned int)time(nullptr) ^ rand() ^ rand())
//进程池:
//准备子进程并让他们等待执行任务 ==> 发送任务码给子进程 ==> 子进程执行 ==> 获取子进程信息
class subEP
{
public:
subEP(pid_t subId, int writeFd)
:_subId(subId), _writeFd(writeFd)
{
char numBuf[512];
snprintf(numBuf, sizeof(numBuf), "[process%d]: pid=%d | fd=%d", _nameNum++, subId, _writeFd);
_name = numBuf;
}
public:
std::string _name;
pid_t _subId;
int _writeFd; //父进程眼中管道的写端fd
static int _nameNum;
};
int subEP::_nameNum = 0;
void task1() {std::cout << getpid() << " 完成任务1\n" << std::endl; sleep(1);}
void task2() {std::cout << getpid() << " 完成任务2\n" << std::endl; sleep(1);}
void task3() {std::cout << getpid() << " 完成任务3\n" << std::endl; sleep(1);}
void loadTask(std::vector<ptask>& taskV)
{
taskV.push_back(task1);
taskV.push_back(task2);
taskV.push_back(task3);
}
//向父进程眼中管道的写端写入任务码
void sendTask(const subEP& proc, int taskCode)
{
int write_ret = write(proc._writeFd, &taskCode, sizeof(taskCode));
assert(write_ret == sizeof(taskCode));
std::cout << "TaskCode " << taskCode << " sent to " << proc._name << std::endl;
}
//从子进程眼中管道的读端读出任务码
int reciveTask(int readFd)
{
int taskCode = 0;
int read_ret = read(readFd, &taskCode, sizeof(taskCode));
//正常读到任务码
if(read_ret == sizeof(taskCode)) return taskCode;
//读到0,说明 写端关闭 && 该读的读完了
else if(read_ret == 0) return -1;
else return 8848; //不可能的情况
}
std::vector<subEP>& getSubProcessWaittingTask(std::vector<subEP>& subEPs, const std::vector<ptask>& taskV)
{
//创建子进程
//bug?
std::vector<int> fdToDelete;
for(int i = 0; i < PROCESS_NUM; ++i)
{
int fds[2];
int pipe_ret = pipe(fds);
assert(pipe_ret == 0);
pid_t fork_ret = fork();
//子进程等待任务
if(fork_ret == 0)
{
//关闭之前子进程的写端,免得影响别人自己的终止
for(int i = 0; i < fdToDelete.size(); ++i) close(fdToDelete[i]);
close(fds[1]);
while(true)
{
int taskCode = reciveTask(fds[0]);
if(taskCode >= 0 && taskCode < taskV.size()) taskV[taskCode]();
else if(taskCode == -1) break;
}
exit(0);
}
//父进程保存当前子端点,让父进程后续能均衡发送任务码
close(fds[0]);
subEPs.push_back(subEP(fork_ret, fds[1]));
//保存要删除的fd(下一个子进程要关闭的写端)
fdToDelete.push_back(fds[1]);
}
}
void balancedTaskSending(std::vector<subEP> subEPs, const std::vector<ptask>& taskV, int taskCount)
{
bool infinite = taskCount == 0 ? true : false;
while(infinite || taskCount)
{
//1. 选择一个子进程
int procIndex = rand() % subEPs.size();
//2. 选择一个任务
int taskCode = rand() % taskV.size();
//3. 将任务对应的任务码发送给子进程
sendTask(subEPs[procIndex], taskCode);
sleep(1);
--taskCount;
}
}
//1. 绕过bug
// void waitSubProcess(std::vector<subEP> subEPs)
// {
// for(int i = 0; i < subEPs.size(); ++i) close(subEPs[i]._writeFd);
// for(int i = 0; i < subEPs.size(); ++i)
// {
// waitpid(subEPs[i]._subId, nullptr, 0);
// std::cout << "wait sub process success: " << subEPs[i]._name << std::endl;
// }
// }
//2. 硬钢bug
void waitSubProcess(std::vector<subEP> subEPs)
{
for(int i = 0; i < subEPs.size(); ++i)
{
close(subEPs[i]._writeFd);
waitpid(subEPs[i]._subId, nullptr, 0);
std::cout << "wait sub process success: " << subEPs[i]._name << std::endl;
}
}
int main()
{
//0.准备任务,设置随机数
std::vector<ptask> taskV;
loadTask(taskV);
PLANT_SEED();
//1.获取正在等待执行任务的子进程
std::vector<subEP> subEPs; //等待执行任务的子进程
getSubProcessWaittingTask(subEPs, taskV);
//2.向subEPs发送任务
int taskCount = 5;
// int taskCount = 0; //无限个任务
balancedTaskSending(subEPs, taskV, taskCount);
//3.正在等待任务的子进程获取到任务并执行
//4.任务执行完毕,子进程退出
//5.获取子进程退出信息
waitSubProcess(subEPs);
return 0;
}
逻辑梳理:
- 准备任务,设置随机数
- 获取正在等待执行任务的子进程
- 向subEPs发送任务
- 正在等待任务的子进程获取到任务并执行
- 任务执行完毕,子进程退出
- 获取子进程退出信息
BUG:同一父进程的多个子进程写端不唯一
原因:
关闭父进程中关于第一个的写端,我们希望OS检测到写端关闭从而终止第一个子进程。
但第一个子进程的写端被后续的进程也拿了一份,就导致关了一个写端还有其他写端,该关的时候反而关不掉。总结一句话,某个子进程的写端被别的子进程拿了,该退退不掉。
为什么刚刚的代码没问题?
最后一个子进程的写端没有被别人拿,所以关了写端就会终止读端进程。
顺序把全部子进程的写端关闭,实质上先终止的是最后一个子进程,倒数第二个的写端变为0个,才关闭;倒数第三个的写端变为0,才关闭……
解决:
- 倒着关
- 每次创建子进程都把不属于自己的管道文件关了
//关闭之前子进程的写端,免得耦合
for(int i = 0; i < fdToDelete.size(); ++i) close(fdToDelete[i]);
fdToDelete.push_back(fds[1]);
命名管道
是什么
是通过mkpipe
创建的有名管道文件。
特点
- 内容不会刷新到磁盘,而是放在内存中
- 不会有IO
- 可以理解命名管道文件是一种内存级的缓冲区
实现原理
不同文件通过路径打开同一个命名管道文件。
具体步骤
- A进程创建/打开命名管道
- B进程打开命名管道
- 通信
- (删除管道文件)
相关接口
int mkfifo(const char *pathname, mode_t mode);
- 作用
- 创建一个命名管道
- 参数
- path:命名管道创建的路径
- mode:命名管道的权限
- 返回值
- 成功返回0
- 失败返回-1,错误码被设置
int unlink(const char *path);
- 作用
- 删除目录条录(可用于删除一个命名管道)
- 参数
- path:要删除的目录条录(命名管道)的路径
- 返回值
- 成功返回0
- 失败返回-1,错误码被设置
测试代码
comm.hpp
#define PIPE_PATH "/tmp/myPipe"
bool createFifo(const std::string& path)
{
umask(0);
int mkdfifoRet = mkfifo(path.c_str(), 0666);
if(mkdfifoRet == 0) return true;
else
{
std::cout << "err: " << strerror(errno) << std::endl;
exit(errno);
}
}
void removeFifo(const std::string& path)
{
int unlinkRet = unlink(path.c_str());
assert(unlinkRet == 0);
(void)unlinkRet; //象征性用一下,以防unused variable
}
server.cc
#include "comm.hpp"
int main()
{
createFifo(PIPE_PATH);
std::cout << "server will open soon" << std::endl;
int rfd = open(PIPE_PATH, O_RDONLY);
if(rfd < 0) exit(-1);
std::cout << "server open done" << std::endl;
//read
char buf[1024];
while(true)
{
int read_ret = read(rfd, buf, sizeof(buf));
if(read_ret > 0) std::cout << "server got msg: " << buf;
else if(read_ret == 0) //读到0说明写端不写了,就可以退出
{
std::cout << "communication complete!" << std::endl;
break;
}
else
{
std::cout << "err: " << strerror(errno) << std::endl;
}
}
close(rfd);
removeFifo(PIPE_PATH);
return 0;
}
client
#include "comm.hpp"
int main()
{
std::cout << "client will open soon" << std::endl;
int wfd = open(PIPE_PATH, O_WRONLY);
if(wfd < 0) exit(-1);
std::cout << "client open done" << std::endl;
//write
char buf[1024];
while(true)
{
std::cout << "please input msg:> ";
fgets(buf, sizeof(buf), stdin);
int write_ret = write(wfd, buf, strlen(buf));
assert(write_ret == strlen(buf));
}
close(wfd);
return 0;
}
匿名管道和命名管道打开区别
- 匿名管道的pipe会创建并打开管道。
- 命名管道的mkfifo只会创建命名管道,需要通过open打开。
管道读写特性:同步与互斥
- 读快,写慢 = 读端阻塞读
- 写快,读慢 = 写端写满阻塞
- 写关闭,读 = 读端读完退出
- 读关闭 = OS发信号终止写端(不写直接退出)
管道的特性
- 管道生命周期随进程
- 具有血缘关系的进程间都能用管道通信(常用于父子进程)
- 管道是面向字节流的(网络)
- 半双工 – 单向通信(半双工的一种特殊概念)
- 互斥与同步机制(相互照顾)
总结
IPC之管道:管道是一种利用文件进行通信的IPC方式,通常是父子进程使用这种方式,因为可以轻易看到同一份文件。
System V
是什么
OS给我们提供的一种本地IPC方案。
*System V的方式用得并不多,原因是有二:
- 它是本地IPC
- 共享内存标识符、消息队列标识符、信号量标识符对文件的标识符fd兼容得不好,而Linux下又一切皆文件
方式:
- 共享内存
- 消息队列
- 信号量
共享内存
是什么
共享内存,shared memory,一块能同时被不同进程看到的内存。是System V IPC的一种。
实现原理和过程
不同进程关联同一块内存空间。
同样的,只要是IPC,免不了的前提:进程间能有一份公共资源。
- 开辟物理上的内存空间
- 将物理内存空间映射给不同内存
共享内存的抽象
不免有个问题:多条通信同时进行,每一组通信的进程都会有自己的共享内存,OS是如何标识不同内存,如何区分不同shm?
struct shmid_ds {
struct ipc_perm shm_perm; /* Ownership and permissions */
size_t shm_segsz; /* Size of segment (bytes) */
time_t shm_atime; /* Last attach time */
time_t shm_dtime; /* Last detach time */
time_t shm_ctime; /* Last change time */
pid_t shm_cpid; /* PID of creator */
pid_t shm_lpid; /* PID of last shmat(2)/shmdt(2) */
shmatt_t shm_nattch; /* No. of current attaches */
...
};
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 and
SHM_LOCKED flags */
unsigned short __seq; /* Sequence number */
};
底层通过key_t key
来标识共享内存。
特点和优缺点
特点:生命周期随OS
优点:是所有IPC中最快的方式(一般没有不必要拷贝)
缺点:没有同步与互斥机制,对数据没有保护
相关接口
int shmget(key_t key, size_t size, int shmflg);
- 作用
- 获取一块共享内存段
- 参数
- key:即将开辟的内存的唯一标识(会被填入struct ipc_perm的第一个字段key)
- size:即将开辟的内存的大小(建议4KB的整数倍,因为内存也是以4KB划分)
- shmflg:二进制标志位
- IPC_CREAT
- 目标不存在:创建
- 目标存在:获取
- IPC_EXCL(和IPC_CREAT搭配使用,达到强制创建的效果)
- 目标不存在:创建
- 目标存在:出错
- 还可以添加一个
0XXX
:对内存的读写权限
- IPC_CREAT
- int:shm的用户级标识符,一般称
shmid
,是某个内核数据结构(数组)的下标
- 返回值
- 成功返回非负整数
- 失败返回-1,错误码被设置
#获取同一个key
说得挺好,key是共享内存的唯一标识,但不同进程怎么用同一个key创建/获取一块共享内存?
看一个接口:
key_t ftok(const char *pathname, int proj_id);
- 作用
- pathname和proj_id通过某种算法计算后得到一个SystemV IPC的key
- 参数
- 其实两者是什么无所谓,只要不同进程调用ftok传的这两个参数是一样的就行
- 返回值
- 成功返回创建好的key
- 失败返回-1,错误码被设置
怎么理解?key的值到底是多少根本不重要,只要不同进程能获取到同一个key就行——只需要传同一个pathname和proj_id就能得到同一个key。只要得到同一个key,也就能看到同一份内存。
#共享内存的唯一标识
key和shmid都是shm的“唯一”标识?
是的:
- key是底层OS层面的标识
- shmid是上层用户层面的标识
很像fd、inode和file的关系:
上层→下层:fd → inode → file
上层→下层:shmid → key → memBlock
为什么要搞俩,统一用一个不行吗?
分开:
- 学校 学号
- 公司 工号
- 社会 身份证号
工号改了,不影响我在学校由学号标识,在社会由身份证号标识。
不分开:
- 学校 身份证号
- 公司 身份证号
- 社会 身份证号
身份证号改了,影响我在学校由学号标识,在社会由身份证号标识。
分开 = 不互相影响,不分开 = 互相影响。前者其实就是不耦合,后者就是耦合。
如果不分开,唯一的key/shmid变了,其他地方全部受影响。
说简单点:shmid是用户管理共享内存用的,key是OS管理内存用的。
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
- 作用
- 控制shm
- 参数
- shmid:要控制的shm
- cmd
- IPC_STAT:获取shm属性到buf
- IPC_SET:把准备好的属性设置到shm
- IPC_RMID:删除shm
- buf:可传入一个结构体,获取属性
- 返回值
- 成功返回0
- 失败返回-1,错误码被设置
void *shmat(int shmid, const void *shmaddr, int shmflg);
- 作用
- 将进程和一块shm关联
- 参数
-
shmid:要关联的shm
-
shmaddr:要关联到进程空间的哪个地址?(一般传nullptr就行)
If shmaddr is a null pointer, the segment is attached at the first available address as selected by the system.
-
shmflg:以什么方式(读/写)关联(0代表读写)
-
- 返回值
- 成功返回shm的地址
- 失败返回-1,错误码被设置
int shmdt(const void *shmaddr);
- 作用
- 将进程和一块shm去关联
- 参数
- shmaddr
- 返回值
- 成功返回0
- 失败返回-1,错误码被设置
相关指令
ipcs -m
:查看shm属性ipcrm -m
:将共享内存的链接数-1(为0时共享内存会被释放)
测试代码
comm.h
#pragma once
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#define PATH_NAME "."
#define PROJ_ID 0x8848
#define SHM_SIZE 4096
key_t getKey()
{
key_t key = ftok(PATH_NAME, PROJ_ID);
if(key == -1)
{
std::cerr << "err: " << strerror(errno) << std::endl;
exit(1);
}
return key;
}
int shmHelper(key_t key, int shmFlag)
{
//创建共享内存时传入的key,会被填入共享内存的一个字段key_t k
//k : shm = 1 : 1
int shmId = shmget(key, SHM_SIZE, shmFlag);
if(shmId < 0)
{
std::cerr << "err:" << strerror(errno) << std::endl;
exit(2);
}
return shmId;
}
int getShm(key_t key)
{
return shmHelper(key, IPC_CREAT);
}
int createShm(key_t key)
{
//创建shm并填入key
return shmHelper(key, IPC_CREAT | IPC_EXCL | 0666); //对shm读写执行的权限
}
void* attachShm(int shmId)
{
void* mem = shmat(shmId, nullptr, 0);
// if((int)mem == -1) //64位系统,指针64bits ==> 32bits == err
if((long long)mem == -1L)
{
std::cerr << "err: " << strerror(errno) << std::endl;
exit(3);
}
return mem;
}
void detachShm(void* start)
{
if(shmdt(start) == -1)
{
std::cerr << "err: " << strerror(errno) << std::endl;
exit(4);
}
}
void removeShm(int shmId)
{
if(-1 == shmctl(shmId, IPC_RMID, nullptr))
{
std::cerr << "err: " << strerror(errno) << std::endl;
exit(-1);
}
}
shm_server.cc
#include "comm.hpp"
int main()
{
printf("sercer pid = %d", getpid());
//获取内核级shm标识:key
key_t key = getKey();
printf("server key = 0x%x\n", key);
//创建shm
int shmId = createShm(key);
// printf("server shmId = 0x%x\n", shmId);
//关联shm和进程
char* start = (char*)attachShm(shmId);
printf("server attatch shm success, address start: %p\n", start);
//通信
while(true)
{
printf("got msg: %s\n", start);
struct shmid_ds ds;
shmctl(shmId, IPC_STAT, &ds);
printf("server stat: |memSegSz=%d| |creator=%d| |key=0x%x|\n", ds.shm_segsz, ds.shm_cpid, ds.shm_perm.__key);
sleep(1);
}
//去关联shm和进程
detachShm(start);
//删除shm
removeShm(shmId);
return 0;
}
#include "comm.hpp"
int main()
{
//获取内核级shm标识:key
key_t key = getKey();
// printf("client key = 0x%x\n", key);
int shmId = getShm(key);
// printf("client shmId = 0x%x\n", shmId);
//关联shm和进程
char* start = (char*)attachShm(shmId);
printf("server attatch shm success, address start: %p\n", start);
//通信
int cnt = 1;
while(true)
{
snprintf(start, SHM_SIZE, "hello server, I'm [%d] | cnt=%d", getpid(), cnt++);
sleep(1);
}
//去关联shm和进程
detachShm(start);
return 0;
}
逻辑梳理:
- A、B进程通过同样的pathName和proj_id获取同一个key如8848
- A进程通过key(8848)创建共享内存
- B进程通过key(8848)获取共享内存
- 进程和共享内存关联
- 通信
共享内存和管道的对比
主要是公共资源用得不一样:
- 管道:文件
- 共享内存:内存
总结
IPC之共享内存:不同进程通过关联同一块内存空间来看到公共资源,进而完成通信。
消息队列
是什么
Linux的内核级队列。
消息队列中的结点主要的字段有type和buf,type是数据块的类型,buf是数据块内的数据。可以理解为:消息队列中存放类型为type的数据块buf。
进程可以根据type来区分数据块,来读写自己对应的数据块。
消息队列的抽象
struct msqid_ds {
struct ipc_perm msg_perm; /* Ownership and permissions */
time_t msg_stime; /* Time of last msgsnd(2) */
time_t msg_rtime; /* Time of last msgrcv(2) */
time_t msg_ctime; /* Time of last change */
unsigned long __msg_cbytes; /* Current number of bytes in
queue (nonstandard) */
msgqnum_t msg_qnum; /* Current number of messages
in queue */
msglen_t msg_qbytes; /* Maximum number of bytes
allowed in queue */
pid_t msg_lspid; /* PID of last msgsnd(2) */
pid_t msg_lrpid; /* PID of last msgrcv(2) */
};
struct ipc_perm {
key_t __key; /* Key supplied to msgget(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 */
unsigned short __seq; /* Sequence number */
};
有了抽象,管理就是水到渠成的事了。
怎么玩
int msgget(key_t key, int msgflg);
- 作用
- 获取消息队列
- 参数
- key:和shmget的一样
- msgflg:和shmget的一样
- 返回值
- 成功返回消息队列的标识符
- 失败返回-1,错误码被设置
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
- 作用
- 控制消息队列
- 参数
- msqid:msq的标识符
- cmd
- IPC_STAT
- IPC_SET
- IPC_RMID
- buf:msq属性
- 返回值
- 成功返回0
- 失败返回-1,错误码被设置
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
- 作用
- 发送消息给消息队列(消息队列会生成一个结点,填入属性和这段信息)
- 参数
-
msqid:消息队列的id
-
msgp:要发送的消息(是一个结构体struct msgbuf)
struct msgbuf { long mtype; /* message type, must be > 0 **/ char mtext[1]; /** message data */ };
-
msgsz:消息的大小
-
msgflg:给0就好
-
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
- 作用
- 从消息队列接收消息
- 参数
-
msqid:消息队列的id
-
msgp:输出型参数,用来接收消息(是一个结构体struct msgbuf)
struct msgbuf { long mtype; /* message type, must be > 0 **/ char mtext[1]; /** message data */ };
-
msgsz:消息的大小
-
msgflg:给0就好
-
具体的例子就不写了,了解个大概即可。
指令
ipcs -q
总结
IPC之消息队列:以系统维护的内核级队列——消息队列作为公共资源,不同通过系统调用读写消息队列来完成通信。
信号量
是什么
一个衡量共享资源使用情况的计数器,提供了一种“预定机制”。
- >0代表仍有资源
- =0代表没有资源
伪代码:
信号量 sem = 20; //有20块共享资源
//进程1预定一块资源(申请信号量)
sem--; //衡量共享资源使用情况的计数器sem就需要对应地自减
//进程2预定一块资源(申请信号量)
sem--;
//访问...
//进程1用完了,回收资源
sem++; //衡量共享资源使用情况的计数器sem就需要对应地自增
//进程2用完了,回收资源
sem++;
其中,
sem--;
就称为 P操作sem++;
就称为 V操作
怎么理解“预定机制”?
:P操作是对共享资源的预定,V操作是对共享资源的释放。
为什么
可以管理、保护好资源。
怎么说?
我们可以通过共享资源的使用来理解。
#概念铺垫
-
临界资源:受保护的共享资源
-
临界区:访问临界资源的代码片段
-
互斥:进程对公共资源的访问是相互排斥的(同一共享资源,同一时刻只能有一个进程访问)
-
原子性:独立不可分割的操作
可理解为只有两态(转账:要么转账成功,要么转账不成功,没有其他状态)
共享资源的使用
使用方式有两种:
- 整体使用:信号量初始值为1
- 拆分使用:信号量初始值大于1
初始值为1的信号量能实现互斥(我申请了,信号量从1减到0,别人就不能申请),这种信号量叫二元信号量。
进程访问共享资源需要先申请信号量,就像看电影,需要先“买票”。
- 申请到信号量,某一部分共享资源就属于进程,就像我买到票,电影院的一个座位就属于我。
- 若没申请到信号量,进程就无法访问共享资源——信号量的意义。
进程想访问共享资源,得先申请信号量,这需要所有进程都得能看到同一个信号量,所以信号量本身就是共享资源。
这样的话,信号量又是怎么管理和保护自己的呢?
只需要令P操作和V操作(对共享资源的申请和释放)都是原子,要么申请成功,要么申请失败;要么释放成功,要么释放失败。
信号量的抽象
struct semid_ds {
struct ipc_perm sem_perm; /* Ownership and permissions */
time_t sem_otime; /* Last semop time */
time_t sem_ctime; /* Last change time */
unsigned long sem_nsems; /* No. of semaphores in set */
};
struct ipc_perm {
key_t __key; /* Key supplied to semget(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 */
unsigned short __seq; /* Sequence number */
};
怎么玩
int semget(key_t key, int nsems, int semflg);
- 作用
- 申请信号量
- 参数
- key
- nsems:要申请的信号量数量
- semflg
- 返回值
- 成功返回信号量集的标识符
- 失败返回-1,错误码被设置
int semctl(int semid, int semnum, int cmd, ...);
- 作用
- 控制信号量
- 参数
- semnum:是信号量集合的下标,表示要控制哪个信号量
int semop(int semid, struct sembuf *sops, unsigned nsops);
- 作用
- P操作和V操作(- -和++)
- 参数
-
sops:sem 的options
-
struct sembuf
unsigned short sem_num; /* semaphore number */ short sem_op; /* semaphore operation */ short sem_flg; /* operation flags */
- set_num:下标,表示你要对信号量集中的哪一个操作
- sem_op:P or V
- -1 = P操作
- 1 = V操作
- sem_flg:设0即可
-
nspos:n个sops
-
具体怎么用,后面多进程时演示。
指令
ipcs -s
IPC资源的管理
先看共享内存、消息队列和信号量的抽象:
struct shmid_ds {
struct ipc_perm shm_perm; /* Ownership and permissions */
size_t shm_segsz; /* Size of segment (bytes) */
time_t shm_atime; /* Last attach time */
time_t shm_dtime; /* Last detach time */
time_t shm_ctime; /* Last change time */
pid_t shm_cpid; /* PID of creator */
pid_t shm_lpid; /* PID of last shmat(2)/shmdt(2) */
shmatt_t shm_nattch; /* No. of current attaches */
...
};
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 and
SHM_LOCKED flags */
unsigned short __seq; /* Sequence number */
};
struct msqid_ds {
struct ipc_perm msg_perm; /* Ownership and permissions */
time_t msg_stime; /* Time of last msgsnd(2) */
time_t msg_rtime; /* Time of last msgrcv(2) */
time_t msg_ctime; /* Time of last change */
unsigned long __msg_cbytes; /* Current number of bytes in
queue (nonstandard) */
msgqnum_t msg_qnum; /* Current number of messages
in queue */
msglen_t msg_qbytes; /* Maximum number of bytes
allowed in queue */
pid_t msg_lspid; /* PID of last msgsnd(2) */
pid_t msg_lrpid; /* PID of last msgrcv(2) */
};
struct ipc_perm {
key_t __key; /* Key supplied to msgget(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 */
unsigned short __seq; /* Sequence number */
};
struct semid_ds {
struct ipc_perm sem_perm; /* Ownership and permissions */
time_t sem_otime; /* Last semop time */
time_t sem_ctime; /* Last change time */
unsigned long sem_nsems; /* No. of semaphores in set */
};
struct ipc_perm {
key_t __key; /* Key supplied to semget(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 */
unsigned short __seq; /* Sequence number */
};
不仅接口相似度高(尤其是获取和删除),而且采用了类似的抽象方式。这叫什么?
这就叫 “标准”,所谓SystemV标准下的IPC方案,就是这个意思。
我们还发现,它们都有一个结构:
struct ipc_perm {
key_t __key; /* Key supplied to XXX(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 */
unsigned short __seq; /* Sequence number */
};
而且每个ipc_perm对象,都是共享内存这些结构的第一个字段,这有什么用?
通过类型转换实现一地址多用,就像多态一样。
今天的分享就到这里了,感谢您能看到这里。
这里是培根的blog,期待与你共同进步!