目录
一、基本概念
进程具有独立性,所以进程间通信成本很高:让不同的进程看到同一份资源;进行通信
1.进程间通信的目的
数据传输、资源共享、通知事件、进程控制
2.进程间通信的分类
(1)POSIX -> 可以跨主机通信
(2)System V -> 聚焦本地通信
(3)管道 -> 基于文件系统的通信方式;分为匿名管道和命名管道两种
二、管道通信
1.匿名管道
(1)匿名管道示意图
(2)匿名管道原理
父子进程要分别以读和写的方式打开同一个文件!要创建管道文件,要使用pipe()函数!
#include <iostream>
#include <unistd.h>
#include <cassert>
#include <sys/types.h>
#include <sys/wait.h>
#include <cstring>
#include <cstdio>
//父进程进行读取,子进程进行写入
int main()
{
//第一步:创建管道文件,打开读写端
int fds[2];
int n = pipe(fds);
assert(n == 0);
//第二步:fork()
pid_t id = fork();
assert(id >= 0);
if(id == 0)
{
//子进程的通讯代码
//子进程进行写入 -> 关闭读
close(fds[0]);
const char* msg = "i am child";
int count = 0;
while(true)
{
count++;
char buffer[1024];//只有子进程能看到
snprintf(buffer, sizeof(buffer), "child say: %s", msg);
//写端写满的时候,在写就会阻塞,等待对方读取
write(fds[1], buffer, strlen(buffer));
sleep(1);//每隔一秒写一次
}
exit(0);
}
//父进程进行读取 -> 关闭写
close(fds[1]);
//父进程的通讯代码
while(true)
{
char buffer[1024];
//如果管道里没有数据了,读端在读,默认会直接阻塞当前正在读取的进程
ssize_t s = read(fds[0], buffer, sizeof(buffer)-1);
if(s > 0) buffer[s] = '\0';
std::cout << "Get Massage# " << buffer << std::endl;
//父进程没有进行sleep
}
//等待子进程
n = waitpid(id, nullptr, 0);
assert(n == id);
return 0;
}
(3)管道读写特征
①读慢 写快:write调用阻塞,一直等到有读进程来读取数据
②读快 写慢:read调用阻塞,一直等到有写进程来写入数据
③写关闭:读到结束
④读关闭:操作系统会给写进程发送信号,终止写端
(4)管道特征
①进程退出,管道就被释放了,所以管道的生命周期随进程
②管道可以用来进行具有血缘关系的进程之间的通信,常用于父子通信
③管道是面向字节流的
④管道是半双工的,数据只能向一个方向流动
⑤内核会对管道操作进行同步和互斥
2.命名管道
匿名管道只能用于具有血缘关系的进程;那不相关的进程之间的通信怎么办?命名管道!
命名管道是如何做到让不同的进程,看到了同一份资源?
让不同的进程打开同一个文件(路径 + 文件名)。因为:路径 + 文件名 = 唯一性
(1)命名管道示意图
(2)命名管道的实现
①$ mkfifo filename // 创建命名管道的Linux指令
②int mkfifo(const char *pathname, mode_t mode); // 创建命名管道的函数
a.pathname:命名管道的路径
b.mode:命名管道的起始权限
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>
#include <cstdio>
#include <cassert>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#define NAMED_PIPE "/tmp/mypipe"
// 创建命名管道
bool createFIfo(const std::string &path)
{
umask(0);
int n = mkfifo(path.c_str(), 0600);
if(n == 0)
{
return true;
}
else
{
std::cout << errno << strerror(errno) << std::endl;
return false;
}
}
// 删除命名管道
void removeFilo(const std::string &path)
{
int n = unlink(path.c_str());
assert(n == 0);
}
三、共享内存
共享内存、消息队列、信号量 都是 system V 进程间通信
1.共享内存概念
让不同的进程看到同一个内存块的方式 -> 就是共享内存
2.共享内存原理
(1)首先要在内存中申请一块内存(共享内存)
(2)将创建好的内存映射到进程地址空间 -> 进程和共享内存挂接
(3)未来不在通信时:取消进程和内存的映射关系(去关联);释放内存(释放共享内存)
3.共享内存理解
(1)共享内存是为了进程间通信专门设计的
(2)共享内存是一种通信方式,所有想通信的进程都可以用共享内存通信
(3)因为会有很多进程在通信,所以操作系统中一定会同时存在很多共享内存
4.共享内存相关接口
(1)ftok
key_t ftok(const char *pathname, int proj_id); // 生成一个独一无二的key
①pathname:路径,必须是真实存在且可以访问的路径
②proj_id:int类型数字,且必须传入非零值
ftok()函数通过 pathname 和 proj_id 来生成一个独一无二的key
(2)shmget
int shmget(key_t key, size_t size, int shmflg); // 创建共享内存
①key:共享内存唯一的标识(利用ftok()函数创建)
②size:共享内存的大小(一般建议是4KB的整数倍)
③shmfig:IPC_CREAT -> 不存在创建;存在则获取
IPC_EXCL | IPC_CREAT ->不存在创建;存在就出错返回
④返回值:返回一个有效的共享内存标识符shmid
(3)shmctl
int shmctl(int shmid, int cmd, struct shmid_ds *buf); // 控制共享内存(通常用来删除)
①shmid ②cmd:删除 -> IPC_RMID ③buf设置为nullptr即可
(4)shmat
void *shmat(int shmid, const void *shmaddr, int shmflg); // 进程和共享内存挂接
①shmid ②shmaddr设置为nullptr即可 ③shmflg设置为0即可
返回值:返回共享内存的起始地址
(5)shmdt
int shmdt(const void *shmaddr); // 断开进程和共享内存挂接
shmaddr:要去关联的共享内存的首地址
#pragma once
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <cerrno>
#include <cstring>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#define PATHNAME "." // 上级目录
#define PROJ_ID 0x66 // 自定义
#define MAX_SIZE 4096 // 共享内存大小
key_t getKey()
{
key_t key = ftok(PATHNAME, PROJ_ID);
if (key < 0)
{
std::cerr << errno << ":" << strerror(errno) << std::endl;
exit(1);
}
return key;
}
int getShmHelper(key_t key, int flags)
{
int shmid = shmget(key, MAX_SIZE, flags);
if (shmid < 0)
{
std::cerr << errno << ":" << strerror(errno) << std::endl;
exit(2);
}
return shmid;
}
// 获取到共享内存
int getShm(key_t key)
{
return getShmHelper(key, IPC_CREAT);
}
// 创建共享内存
int createShm(key_t key)
{
// 0600:把共享内存的权限设置为0600,这样拥有者就可以读写了(和文件权限一样)
// 不加权限无法完成 进程和共享内存挂接
return getShmHelper(key, IPC_CREAT | IPC_EXCL | 0600);
}
// 删除共享内存
void delShm(int shmid)
{
if (shmctl(shmid, IPC_RMID, nullptr) == -1)
{
std::cerr << errno << ":" << strerror(errno) << std::endl;
}
}
// 完成进程和共享内存挂接
void *shmAttach(int shmid)
{
void *mem = shmat(shmid, nullptr, 0);
if ((long long)mem == -1L)
{
std::cerr << " shmat: " << strerror(errno) << std::endl;
exit(3);
}
return mem;
}
// 去关联
void shmDetach(void *start)
{
if (shmdt(start) == -1)
{
std::cerr << "shmdt: " << strerror(errno) << std::endl;
}
}
5.共享内存特点
(1)共享内存的生命周期是随OS的,不是随进程的(system V 通信的生命周期都是随进程的)
(2)共享内存在所有的进程间通信中,速度是最快的
(3)没有同步和互斥的操作,没有对数据做任何保护
四、消息队列
1.消息队列原理
2.消息队列相关接口
(1)int msgget(key_t key, int msgflg); // 创建消息队列
(2)int msgctl(int msqid, int cmd, struct msqid_ds *buf); // 控制消息队列
(3)int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg); // 放数据
(4)ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);//读
五、信号量
信号量主要是用于同步和互斥的
1.信号量概念
(1)相关概念
①公共资源:可以被多个进程"同时"访问的资源(访问没有保护的公共资源会出问题)
②临界资源:我们将被保护起来的公共资源 称为 临界资源
③临界区:访问临界资源的代码
④非临界区:访问非临界资源的代码
⑤原子操作:要么不做,要做就做完,只有两态
(2)信号量
本质上是一个计数器,通常用来表示公共资源中 资源数量的多少
为什么要有信号量?类比看电影买票:对影院的座位预订->信号量就相当于是一场电影的总票数
同理:当我们想要某种资源的时候,我们可以进行预定(参照下图流程)
所有的进程在访问公共资源之前,都必须先申请sem信号量 -> 申请sem信号量的前提,是所有
进程必须先得看到同一个信号量 -> 信号量本身就是公共资源 -> 信号量也要保证自己的安全
-> 所以信号量的 ++ / -- 操作是原子操作!
2.信号量相关接口
(1)int semget(key_t key, int nsems, int semflg); // 创建信号量
(2)int semctl(int semid, int semnum, int cmd, ...); // 控制信号量(可以用来删除信号量)
(3)int semop(int semid, struct sembuf *sops, unsigned nsops); // 实现PV操作