文章目录
- 1.简单介绍
- 2.管道
- 2.1管道的基础概念
- **管道读写规则**:
- **管道特点**
- 2.2匿名管道
- 匿名管道父子进程间通信的经典案例:
- 2.3命名管道
- 基本概念:
- 命名管道的创建:
- 命名管道的打开规则:
- 匿名管道与普通管道的区别
- **例子:用命名管道实现server&client通信**
- 3.system v ipc
- 3.1system v 共享内存 基础介绍
- 3.2system v 共享内存常用接口介绍
- 3.3system v 信息队列 以及信号量
- 4.一些基础概念的讲解
1.简单介绍
在计算机中,进程间通信(Inter-Process Communication,IPC)是指两个或多个进程之间交换数据或信息的机制。
首先我们通过之前的学习知道进程是具有独立性的,其交互数据成本非常高,所以通信这一机制就诞生了。
通信的本质其实就是由os参与,提供一份所有通信进程能看到的公共资源。
进程间通信的方式非常多以下是对这些方式的梳理以及分类:
进程间通信分类
管道
-
匿名管道pipe
-
命名管道
System V IPC
-
System V 消息队列
-
System V 共享内存
-
System V 信号量
POSIX IPC
-
消息队列
-
共享内存
-
信号量
-
互斥量
-
条件变量
-
读写锁
2.管道
2.1管道的基础概念
管道是Unix中最古老的进程间通信的形式。
我们把从一个进程连接到另一个进程的一个数据流称为一个管道
管道读写规则:
当没有数据可读时
- O_NONBLOCK disable:read调用阻塞,即进程暂停执行,一直等到有数据来到为止。
- O_NONBLOCK enable:read调用返回-1,errno值为EAGAIN。
当管道满的时候
- O_NONBLOCK disable: write调用阻塞,直到有进程读走数据
- O_NONBLOCK enable:调用返回-1,errno值为EAGAIN
如果所有管道写端对应的文件描述符被关闭,则read返回0
如果所有管道读端对应的文件描述符被关闭,则write操作会产生信号SIGPIPE,进而可能导致write进程
退出
当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性。
当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性。
管道特点
-
管道提供流式服务
-
一般而言,进程退出,匿名管道释放,所以匿名管道的生命周期随进程
-
一般而言,内核会对管道操作进行同步与互斥
-
管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道
2.2匿名管道
#include <unistd.h>
功能:创建一无名管道,管道是一种特殊的文件,它具有两个端点,一个用于写入数据,另一个用于读取数据。
原型
int pipe(int fd[2]);
参数
fd:文件描述符数组,其中fd[0]表示读端, fd[1]表示写端
是一个包含两个整数的数组,用于接收管道的两个文件描述符
返回值:成功返回0,失败返回-1
匿名管道父子进程间通信的经典案例:
使用例子:从键盘读取数据,写入管道,读取管道,写到屏幕
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>
int main()
{
int pipe_fd[2] = {0};
if(pipe(pipe_fd) < 0){
perror("pipe");
return 1;
}
printf("%d, %d\n", pipe_fd[0], pipe_fd[1]);
pid_t id = fork();
if(id < 0){
perror("fork");
return 2;
}
else if(id == 0) { //write
//child
close(pipe_fd[0]);
//const char *msg = "hello parent, I am child";
char c = 'x';
int count = 0;
//while(count){
while(1){
write(pipe_fd[1], &c, 1); //strlen(msg) + 1??
// sleep(1);
count++;
printf("write: %d\n", count);
}
close(pipe_fd[1]);
exit(0);
}
else{ //read
//parent
close(pipe_fd[1]);
char buffer[64];
while(1){
sleep(100);
buffer[0] = 0;
ssize_t size = read(pipe_fd[0], buffer, sizeof(buffer)-1);
if(size > 0){
buffer[size] = 0;
printf("parent get messge from child# %s\n", buffer);
}
else if(size == 0){
printf("pipe file close, child quit!\n");
break;
}
else{
//TODO
break;
}
}
int status = 0;
if(waitpid(id, &status,0) > 0){
printf("child quit, wait success!, sig: %d\n", status&0x7F);
}
close(pipe_fd[0]);
}
return 0;
}
对上端代码用fork来共享管道的原理
2.3命名管道
基本概念:
匿名管道应用的一个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信。
如果我们想在不相关的进程之间交换数据,可以使用FIFO文件来做这项工作,它经常被称为命名管道。
命名管道是一种特殊类型的文件
命名管道的创建:
//命名管道可以从命令行上创建,命令行方法是使用下面这个命令:
$ mkfifo filename
//命名管道也可以从程序里创建,相关函数有:
int mkfifo(const char *filename,mode_t mode);
//filename:一个指向要创建的命名管道的路径和名称的字符串。它是一个以 null 结尾的字符数组。
//mode:用于指定创建的命名管道的权限(访问权限和文件类型)。mode 是一个 mode_t 类型的参数,通常使用八进制表示的权限值。可以使用一些预定义的常量(如 S_IRUSR、S_IWUSR、S_IRGRP 等)来设置权限。
//创建命名管道
int main(int argc, char *argv[])
{
mkfifo("p2", 0644);
return 0;
}
命名管道的打开规则:
如果当前打开操作是为读而打开FIFO时
O_NONBLOCK disable:阻塞直到有相应进程为写而打开该FIFO
O_NONBLOCK enable:立刻返回成功
如果当前打开操作是为写而打开FIFO时
O_NONBLOCK disable:阻塞直到有相应进程为读而打开该FIFO
O_NONBLOCK enable:立刻返回失败,错误码为ENXIO
匿名管道与普通管道的区别
匿名管道由pipe函数创建并打开(使用了pipe函数默认该描述符对应的文件打开了)。
命名管道由mkfififo函数创建,打开用open
FIFO(命名管道)与pipe(匿名管道)之间唯一的区别在它们创建与打开的方式不同,一但这些工作完
成之后,它们具有相同的语义。
tips:
通过 mkfifo
函数创建的命名管道在文件系统中持久存在,与创建它的进程无关。
使用 pipe
函数创建的管道是进程特有的,与创建它的进程相关联。当创建管道的进程结束时,管道会被自动关闭和销毁。而mkfifo函数所创建的管道是不会随着进程结束而销毁的,我们需要手动销毁。
//销毁命名管道所使用的函数
int unlink(const char *pathname);
pathname 是一个指向要删除的文件的路径和名称的字符串。它是一个以 null 结尾的字符数组。
//unlink 函数执行以下操作:
1.首先,它会检查指定路径的文件是否存在。如果文件不存在,unlink 函数会返回一个错误码,并不执行任何其他操作。
2.如果文件存在,并且调用进程对该文件具有足够的权限,unlink 函数将从文件系统中删除该文件。删除文件会将文件从目录中删除,并释放文件所占用的磁盘空间。
3.注意,删除文件并不会关闭已打开的文件描述符。如果有进程仍然持有对该文件的打开描述符,文件将继续存在于文件系统中,直到所有打开的描述符关闭。
4.当调用进程成功删除文件时,unlink 函数返回 0。如果出现错误,比如权限不足或指定的文件路径无效,unlink 函数返回 -1,并设置相应的错误码,可以通过 errno 全局变量获取错误信息。
//删除命名管道文件并不会自动关闭已打开的管道文件描述符。在删除命名管道文件之前,需要确保所有打开的文件描述符都已关闭,以免导致资源泄漏或其他问题。
例子:用命名管道实现server&client通信
例子中使用的一些函数的解析:
1.
mode_t umask(mode_t mask);
umask 函数接受一个参数 mask,它是一个无符号整数类型 mode_t 的值,表示要设置的权限掩码。mode_t 是一个用于表示文件权限和文件类型的数据类型。
2.
key_t ftok(const char *pathname, int proj_id);
用于生成一个唯一的键值(key)用于标识一个共享资源,例如共享内存、信号量或消息队列。
pathname:一个存在的文件路径名,可以是任意合法的文件路径。
proj_id:一个用户定义的整数,用于区分不同的共享资源。通常取值为一个非负整数。
生成的键值可以用于创建或访问共享资源,例如在调用 shmget 函数创建共享内存时使用。
3.
int shmdt(const void *shmaddr);
shmdt 函数用于将共享内存从当前进程的地址空间中分离,即解除共享内存的挂载。
shmdt 函数并不会删除共享内存段,只是将其与当前进程分离。
在调用 shmdt 函数之后,当前进程将无法再直接访问共享内存中的数据。如果需要重新访问共享内存,必须使用 shmat 函数重新将共享内存段挂载到当前进程的地址空间。
Log.hpp(一个简单的日志记录功能的实现)
#ifndef _LOG_H_
#define _LOG_H_
#include <iostream>
#include <ctime>
#define Debug 0
#define Notice 1
#define Warning 2
#define Error 3
const std::string msg[] = {
"Debug",
"Notice",
"Warning",
"Error"
};
std::ostream &Log(std::string message, int level)
{
std::cout << " | " << (unsigned)time(nullptr) << " | " << msg[level] << " | " << message;
return std::cout;
}
#endif
Makefile
.PHONY:all
all:shmClient shmServer
shmClient:shmClient.cc
g++ -o $@ $^ -std=c++11
shmServer:shmServer.cc
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -f shmClient shmServer
comm.hpp
#pragma once
#include <iostream>
#include <cstdio>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <cassert>
#include <cstring>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include "Log.hpp"
using namespace std; //不推荐
#define PATH_NAME "/home/whb"
#define PROJ_ID 0x66
#define SHM_SIZE 4096 //共享内存的大小,最好是页(PAGE: 4096)的整数倍
#define FIFO_NAME "./fifo"
class Init
{
public:
Init()
{
umask(0);//表示没有遮掩任何权限
int n = mkfifo(FIFO_NAME, 0666);
assert(n == 0);
(void)n;
Log("create fifo success",Notice) << "\n";
}
~Init()
{
unlink(FIFO_NAME);
Log("remove fifo success",Notice) << "\n";
}
};
#define READ O_RDONLY
#define WRITE O_WRONLY
int OpenFIFO(std::string pathname, int flags)
{
int fd = open(pathname.c_str(), flags);
assert(fd >= 0);
return fd;
}
void Wait(int fd)
{
Log("等待中....", Notice) << "\n";
uint32_t temp = 0;//为了确保读取的值可以正确地存储和比较
ssize_t s = read(fd, &temp, sizeof(uint32_t));
assert(s == sizeof(uint32_t));
(void)s;
}
void Signal(int fd)
{
uint32_t temp = 1; //为了确保读取的值可以正确地存储和比较
ssize_t s = write(fd, &temp, sizeof(uint32_t));
assert(s == sizeof(uint32_t));
(void)s;
Log("唤醒中....", Notice) << "\n";
}
void CloseFifo(int fd)
{
close(fd);
}
shmClient.cc
#include "comm.hpp"
int main()
{
Log("child pid is : ", Debug) << getpid() << endl;
key_t k = ftok(PATH_NAME, PROJ_ID);
if (k < 0)
{
Log("create key failed", Error) << " client key : " << k << endl;
exit(1);
}
Log("create key done", Debug) << " client key : " << k << endl;
// 获取共享内存
int shmid = shmget(k, SHM_SIZE, 0);
if(shmid < 0)
{
Log("create shm failed", Error) << " client key : " << k << endl;
exit(2);
}
Log("create shm success", Error) << " client key : " << k << endl;
// sleep(10);
char *shmaddr = (char *)shmat(shmid, nullptr, 0);
if(shmaddr == nullptr)
{
Log("attach shm failed", Error) << " client key : " << k << endl;
exit(3);
}
Log("attach shm success", Error) << " client key : " << k << endl;
// sleep(10);
int fd = OpenFIFO(FIFO_NAME, WRITE);
// 使用
// client将共享内存看做一个char 类型的buffer
while(true)
{
ssize_t s = read(0, shmaddr, SHM_SIZE-1);
if(s > 0)
{
shmaddr[s-1] = 0;
Signal(fd);
if(strcmp(shmaddr,"quit") == 0) break;
}
}
CloseFifo(fd);
// char a = 'a';
// for(; a <= 'z'; a++)
// {
// shmaddr[a-'a'] = a;
// // 我们是每一次都向shmaddr[共享内存的起始地址]写入
// // snprintf(shmaddr, SHM_SIZE - 1,\
// // "hello server, 我是其他进程,我的pid: %d, inc: %c\n",\
// // getpid(), a);
// sleep(5);
// }
// strcpy(shmaddr, "quit");
// 去关联
int n = shmdt(shmaddr);
assert(n != -1);
Log("detach shm success", Error) << " client key : " << k << endl;
// sleep(10);
// client 要不要chmctl删除呢?不需要!!
return 0;
}
shmServer.cc
#include "comm.hpp"
// 是不是对应的程序,在加载的时候,会自动构建全局变量,就要调用该类的构造函数 -- 创建管道文件
// 程序退出的时候,全局变量会被析构,自动调用析构函数,会自动删除管道文件
Init init;
string TransToHex(key_t k)
{
char buffer[32];
snprintf(buffer, sizeof buffer, "0x%x", k);
return buffer;
}
int main()
{
// 我们之前为了通信,所做的所有的工作,属于什么工作呢:让不同的进程看到了同一份资源(内存)
// 1. 创建公共的Key值
key_t k = ftok(PATH_NAME, PROJ_ID);
assert(k != -1);
Log("create key done", Debug) << " server key : " << TransToHex(k) << endl;
// 2. 创建共享内存 -- 建议要创建一个全新的共享内存 -- 通信的发起者
int shmid = shmget(k, SHM_SIZE, IPC_CREAT | IPC_EXCL | 0666); //
if (shmid == -1)
{
perror("shmget");
exit(1);
}
Log("create shm done", Debug) << " shmid : " << shmid << endl;
// sleep(10);
// 3. 将指定的共享内存,挂接到自己的地址空间
char *shmaddr = (char *)shmat(shmid, nullptr, 0);
Log("attach shm done", Debug) << " shmid : " << shmid << endl;
// sleep(10);
// 这里就是通信的逻辑了
// 将共享内存当成一个大字符串
// char buffer[SHM_SIZE];
// 结论1: 只要是通信双方使用shm,一方直接向共享内存中写入数据,另一方,就可以立马看到对方写入的数据。
// 共享内存是所有进程间通信(IPC),速度最快的!不需要过多的拷贝!!(不需要将数据给操作系统)
// 结论2: 共享内存缺乏访问控制!会带来并发问题 【如果我想一定程度的访问控制呢? 能】
int fd = OpenFIFO(FIFO_NAME, READ);
for(;;)
{
Wait(fd);
// 临界区
printf("%s\n", shmaddr);
if(strcmp(shmaddr, "quit") == 0) break;
// sleep(1);
}
// 4. 将指定的共享内存,从自己的地址空间中去关联
int n = shmdt(shmaddr);
assert(n != -1);
(void)n;
Log("detach shm done", Debug) << " shmid : " << shmid << endl;
// sleep(10);
// 5. 删除共享内存,IPC_RMID即便是有进程和当下的shm挂接,依旧删除共享内存
n = shmctl(shmid, IPC_RMID, nullptr);
assert(n != -1);
(void)n;
Log("delete shm done", Debug) << " shmid : " << shmid << endl;
CloseFifo(fd);
return 0;
}
3.system v ipc
3.1system v 共享内存 基础介绍
实际上,system v本质也是创建虚拟内存进而映射到实际内存上,不过跟上面管道不同的是,进程间信息交互的处理不在由内核系统那一套来处理,而是由操作系统直接处理,因此其是最快的ipc模式。
3.2system v 共享内存常用接口介绍
功能:用来创建共享内存
原型
int shmget(key_t key, size_t size, int shmflg);
参数
key:这个共享内存段名字
size:共享内存大小
shmflg:由九个权限标志构成,它们的用法和创建文件时使用的mode模式标志是一样的返回值:成功返回一个非负整数,即该共享内存段的标识码;失败返回-1
功能:将共享内存段连接到进程地址空间
原型
void *shmat(int shmid, const void *shmaddr, int shmflg);参数
shmid: 共享内存标识
shmaddr:指定连接的地址
shmflg:它的两个可能取值是SHM_RND和SHM_RDONLY
返回值:成功返回一个指针,指向共享内存第一个节;失败返回-1
shmaddr为NULL,核心自动选择一个地址
shmaddr不为NULL且shmflg无SHM_RND标记,则以shmaddr为连接地址。
shmaddr不为NULL且shmflg设置了SHM_RND标记,则连接的地址会自动向下调整为SHMLBA的整数倍。公式:shmaddr - (shmaddr % SHMLBA)
shmflg=SHM_RDONLY,表示连接操作用来只读共享内存
功能:将共享内存段与当前进程脱离
原型
int shmdt(const void *shmaddr);
参数
shmaddr: 由shmat所返回的指针
返回值:成功返回0;失败返回-1
注意:将共享内存段与当前进程脱离不等于删除共享内存段
功能:用于控制共享内存
原型
int shmctl(int shmid, int cmd, struct shmid_ds *buf);参数
shmid:由shmget返回的共享内存标识码
cmd:将要采取的动作(有三个可取值)
buf:指向一个保存着共享内存的模式状态和访问权限的数据结构
返回值:成功返回0;失败返回-1
3.3system v 信息队列 以及信号量
这一块学有余力的同学再去详细了解,这里便不在一一呈现了。
4.一些基础概念的讲解
为了之后获取更好的阅读文章体验,我将提前把一些经常出现的基础概念在此总结一下,强烈介意第一次打基础的同学阅读。
当多个进程对同一份资源进行来回操作时,因为时序问题,可能会造成数据不一致的问题,为了解决这一问题,我们将定制一套基础方案,下面先对其中涉及到的专有名词的含义进行讲解。
临界资源:多个进程执行流看到的公用的一份资源
临界区:进程访问临界资源的代码
互斥性:为了更好的临界区的保护,可以让各执行流在同一时刻只有一个进程进入临界区
原子性:一件事情要么不做,要么做完,没有中间状态
信号量:信号量是一种用于实现进程间同步和互斥的机制。可以将信号量看作是一个计数器,它的值可以被多个进程或线程修改和读取。信号量的值表示可用的资源数量或某种条件的状态。
主要有两种类型的信号量:
- 二进制信号量(Binary Semaphore):也称为互斥信号量,它的值只能为0或1。用于实现互斥访问共享资源,只允许一个进程或线程访问资源。
- 计数信号量(Counting Semaphore):它的值可以是任意非负整数。用于控制一定数量的资源,多个进程或线程可以同时访问资源,但受限于信号量的计数值。