文章目录
- 1 管道(匿名管道)
- 1.1 管道抽象
- 1.2 接口——`pipe`
- 1.3 管道的特征
- 1.4 管道的四种情况
- 1.5 匿名管道用例
- 2 命名管道
- 2.1 创建一个命名管道——`mkfifo`
- 2.2 关闭一个管道文件——unlink
- 2.3 管道和命名管道的补充
- 2.4 命名管道用例
- 3 共享内存
- 3.1 原理
- 3.2 系统调用接口——`shmget`
- 3.2.1 `key`
- 3.2.2 `ftok`——返回key
- 3.2.3 `size`
- 3.2.4 `shmflg`
- 3.2.5 返回值——shmid
- 3.3 主动释放共享内存
- 3.3.1 使用命令——`ipcrm`
- 3.3.2 系统调用函数——`shmctl`
- 3.4 将进程与共享内存挂接
- 3.4.1 关联——`shmat`
- 3.4.2 去关联——`shmdt`
- 3.5 共享内存的特点
- 3.6 共享内存属性 —— `chmctl`
- 3.5 共享内存用例
- 4 消息队列
- 5 IPC在内核中的数据结构
进程间的通信标准
- system V IPC
- 消息队列、共享内存、信号量
- POSIX IPC
- 消息队列、共享内存、信号量、互斥量、条件变量、读写锁
- 基于文件的通信方式——管道
1 管道(匿名管道)
当创建子进程时,进程中的files_struct同样被赋值了一份,如果此时内存中打开了一份内存级文件(该文件同样包含:cnt、inode、file_operators、缓冲区,但是该内存级文件不需要向磁盘刷新缓冲区),并且在父进程的文件描述符表中,那么子进程也会赋值也会同样打开这份文件,此时父子进程可以通过该文件进行通信
1.1 管道抽象
- 管道只能进行单向通信
1.2 接口——pipe
#include <unistd.h>
int pipe(int pipefd[2]);
//pipefd为输出型参数,函数通过该参数将两个文件描述符输出出来,给用户使用
//pipefd[0] : 读下标
//pipefd[1] : 写下标
1.3 管道的特征
- 具有血缘关系的进程进行进程间通信
- 管道只能单向通信
- 父子进程之间是会协同的,同步与互斥 —— 保护管道文件的数据安全
- 管道是面向字节流的
- 管道是基于文件的,而文件的生命周期是随进程的,当进程退出时系统自动释放文件内存
查看一些操作系统的限制,例如管道的大小
ulimit -a
1.4 管道的四种情况
- 读写端正常,管道为空,读端阻塞
- 读写端正常,管道被写满,写端阻塞
- 读写端正常,写端关闭,读端
read
返回0,表示读到文件(pipe
)结尾,不会被阻塞 - 写端正常,读端关闭,操作系统会通过信号(13: SIGPIPE)杀掉正在写入的进程
1.5 匿名管道用例
//匿名管道用例
#include <iostream>
#include <cstdlib>
#include <unistd.h>
#include <cstdio>
#include <cstring>
#include <sys/wait.h>
using namespace std;
void Writer(int write_fd) {
char buffer[1023] = {'\0'};
snprintf(buffer, sizeof(buffer), "%s-%d", "message, pid: ", getpid());
write(write_fd, buffer, strlen(buffer));
}
void Reader(int read_fd) {
char buffer[1023] = {'\0'};
ssize_t n = read(read_fd, buffer, sizeof(buffer));
buffer[n] = '\0';
cout << "child output: " << buffer << endl;
}
int main() {
int pipefd[2];
int ret = pipe(pipefd);
pid_t id = fork();
if (id == 0) {
close(pipefd[1]);
Reader(pipefd[0]);
}
close(pipefd[0]);
Writer(pipefd[1]);
int wait_ret = waitpid(id, nullptr, 0);
return 0;
}
2 命名管道
- 如果两个不同的进程,打开同一个文件的时候,在内核中,操作系统只会打开一个文件
- 管道文件
- 内存级文件,只用文件缓冲区,不用写入到磁盘中
- 两个进程如何知道打开的是同一份文件:使用同一个文件名
2.1 创建一个命名管道——mkfifo
- 与文件相同,不能重复创建文件名相同的管道文件!!
- 命令
- 系统调用函数
2.2 关闭一个管道文件——unlink
2.3 管道和命名管道的补充
-
多个进程在通过管道通信时,删除管道文件则无法继续通信? —— 错误
管道的生命周期随进程,本质是内核中的缓冲区,命名管道文件只是标识,用于让多个进程找到同一块缓冲区,删除后,之前已经打开管道的进程依然可以通信
-
命名管道的本质和匿名管道的本质相同都是内核中的一块缓冲区
2.4 命名管道用例
//命名管道用例
//comm.hpp
#pragma once
#include <sys/types.h>
#include <iostream>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <signal.h>
using namespace std;
const string pathname = "./myfifo";
class Init {
public:
Init() {
mkfifo(pathname.c_str(), 0666);
}
~Init() {
unlink(pathname.c_str());
}
};
//server.cc
#include "comm.hpp"
//读数据
int main() {
Init init; //创建管道文件,进程结束调用unlink销毁管道文件
int fd = open(pathname.c_str(), O_RDONLY);
while (true) {
char buffer[1024] = {0};
ssize_t n = read(fd, buffer, sizeof(buffer));
if (n == 0) {
cout << "write close -> read close" << endl;
break;
}
buffer[n] = 0;
cout << "[server read]$ " << buffer << endl;
}
close(fd);
return 0;
}
//client.cc
#include "comm.hpp"
//写数据
int main() {
//读端关闭,发送SIGPIPE信号
signal(SIGPIPE, [](int){
cout << "read close -> write close" << endl;
// unlink(pathname.c_str());
exit(0);
});
int fd = open(pathname.c_str(), O_WRONLY);
string input;
while (true) {
cout << "[client write]$ ";
getline(cin, input);
write(fd, input.c_str(), input.size());
}
close(fd);
return 0;
}
3 共享内存
3.1 原理
- 申请内存(通过系统调用, 通过操作系统管理所有共享内存)
- 挂接到进程地址空间
- 返回首地址(虚拟地址)
3.2 系统调用接口——shmget
//创建共享内存用例
const string pathname = "/home/dusong";
const int proj_id = 0x12345;
const int size = 4096;
int main() {
key_t k = ftok(pathname.c_str(), proj_id);
int shmid = shmget(k, size, IPC_CREAT|IPC_EXCL|0X666);
}
3.2.1 key
- 通过key标识系统中的一块共享内存
- 对于一个已经创建好的共享内存,key在哪里? key在共享内存的描述对象中
3.2.2 ftok
——返回key
通过pathname
和proj_id
生成一个哈希值,作为key
- 若返回值<0,则key获取失败
3.2.3 size
共享内存大小,单位字节
- 共享内存的大小一般建议是4096(4Kb)的整数倍
- 如果size为4097,操作系统会给4096*2的空间
3.2.4 shmflg
IPC_CREAT
:单独使用时,如果申请的共享内存不存在,则创建,存在则获取并返回IPC_CREAT|IPC_EXCL
:如果申请的共享内存不存在,则创建,存在则出错返回,确保每次申请都是一块新的共享内存IPC_EXCL
不单独使用- 权限(八进制)
3.2.5 返回值——shmid
共享内存标识符shmid
key
与shmid
的区别key:用于操作系统内标定唯一性;
shmid(shmget的返回值):只在进程内,用来表示资源的唯一性
- 查看所有shmid:
ipcs -m
3.3 主动释放共享内存
共享内存被删除后,则其它进程直接无法实现通信?——错误
共享内存的删除操作并非直接删除,而是拒绝后续映射,只有在当前映射链接数为0时,表示没有进程访问了,才会真正被删除
当进程还在通过共享内存通信时,通过ipcrm
删除共享内存,此时任然可以通信
3.3.1 使用命令——ipcrm
- 使用
shmid
关闭共享内存
ipcrm -m [shmid]
- 删除所有进程间通信资源
ipcrm -a
3.3.2 系统调用函数——shmctl
-
参数2:cmd
IPC_RMID
:删除共享内存IPC_STAT
:查看属性
-
参数3:buf
- 删除共享内存时,置为nullptr
- 查看属性时作为输出型参数,传入一个
shmid_ds
结构体(描述管理共享内存的属性的结构体)
3.4 将进程与共享内存挂接
3.4.1 关联——shmat
参数2:shmaddr
: 指定将共享内存挂接到地址空间的哪个部分,一般设为nullptr
参数3:shmflg
: 权限设置, 通常设置为0
返回值:连接到虚拟地址的首地址(作为shmaddr)
- 查看一个共享内存被进程挂接的数量:
ipcs -m
- :
shmat
之后nattch
+1
1
3.4.2 去关联——shmdt
- 进程退出时会自动去关联
3.5 共享内存的特点
- 共享内存没有同步互斥之类的保护机制,没有内容时不阻塞(管道要阻塞)
- 共享内存是所有进程间通信中速度最快的,因为拷贝少,对比管道,管道需要先将内容写到用户级缓冲区中,再通过write写到文件缓冲区中
- 共享内存内部的数据由用户自己维护
3.6 共享内存属性 —— chmctl
const string pathname = "/home/dusong";
const int proj_id = 0x12345;
const int size = 4096;
int main() {
key_t k = ftok(pathname.c_str(), proj_id);
int shmid = shmget(k, size, IPC_CREAT|IPC_EXCL|0X666);
struct shmid_ds shmds;
shmctl(shmid, IPC_STAT, &shmds);
cout << shmds.shm_stgsz << << endl; //该共享内存的大小
cout << shmds.shm_prem.__key << << endl; //key
cout << shmds.shm_prem.mode << << endl; //权限
}
3.5 共享内存用例
//comm.hpp
#pragma once
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <cstring>
#include <unistd.h>
#include <iostream>
#include <sys/stat.h>
#include <fcntl.h>
#include <signal.h>
using namespace std;
const string pathname = "./home/for_pipe/sharedmemory";
const int proj_id = 1234;
const size_t size = 4096;
const string pipepath = "./myfifo";
class Shm {
public:
Shm(){
int ret = mkfifo(pipepath.c_str(), 0666);
if (ret < 0) {
cout << "make namedfifo fail"<< endl;
exit(1);
}
if (shmid < 0) {
cout << "shared_memory have created" << endl;
exit(1);
}
}
~Shm() {
shmctl(shmid, IPC_RMID, nullptr);
unlink(pipepath.c_str());
}
static int key;
static int shmid;
};
int Shm::key = ftok(pathname.c_str(), proj_id);
int Shm::shmid = shmget(Shm::key, size, IPC_CREAT|IPC_EXCL|0666);
//server.cc
#include "comm.hpp"
//读端
int main() {
Shm shm;
int fd = open(pipepath.c_str(), O_RDONLY); //通过管道保证互斥与同步
char* st = (char*)shmat(shm.shmid, nullptr, 0);
char buffer[2] = {0};
while(true) {
int n = read(fd, buffer, sizeof(buffer));
if (n == 0) {
cout << "write close -> read close" << endl;
break;
}
cout << "[sever read]$ " << st << endl;
sleep(1);
}
shmdt(st);
}
//client.cc
#include "comm.hpp"
//写端
int main() {
//默认信号处理为忽略
signal(SIGPIPE, [](int){
shmctl(Shm::shmid, IPC_RMID, nullptr);
unlink(pipepath.c_str());
cout << "read close -> write close" << endl;
exit(0);
});
int key = ftok(pathname.c_str(), proj_id);
int shmid = shmget(key, size, IPC_CREAT|0666);
char* st = (char*)shmat(shmid, nullptr, 0); //得到共享内存在虚拟内存空间中的起始地址
int fd = open(pipepath.c_str(), O_WRONLY);
string input = "";
while (true) {
cout << "[client write]$ ";
getline(cin, input);
write(fd, "c", 1);
memcpy(st, input.c_str(), input.size()); //写数据
}
shmdt(st);
}
4 消息队列
- 创建——
msgget
<-----ftok
-
删除——
msgctl
-
发送/接收数据块——
msgsnd
、msgrcv
- 查看消息队列
ipcs -q
- 删除消息队列
ipcrm -q [msqid]
5 IPC在内核中的数据结构
系统中维护一个struct ipc_prem*
数组,通过shmid
、msqid
、semid
作为下标访问,因为每一个shmid_ds
、msqid_ds
、semid_ds
结构体的第一个字段均为ipc_prem
,所以系统可以通过将ipc_prem*
强制类型转换为对应消息队列、共享内存或者信号量的结构体,从而进行管理
图片中两个进程通过共享内存建立通信,此时共享内存的连接数为2 ↩︎