目录
一、进程通信的初步认识
1.1 进程间通信目的
1.2 进程间通信的种类
Linux进程间通信(Inter-Process Communication, IPC)是操作系统中的一个核心概念,它允许运行在同一台机器上的不同进程之间进行数据交换。从历史的发展角度来看,Linux支持多种IPC机制,包括管道(Pipes)、System V IPC机制和POSIX IPC机制。这些机制各有特点,适用于不同的场景。
管道(Pipes)
管道是最早的Unix IPC机制之一,提供了一个单向通信的简单接口。管道可以是匿名的,也可以是命名的(也称为FIFO)。它们允许将一个进程的输出直接连接到另一个进程的输入。
- 匿名管道:仅限于有父子关系的进程间通信。
- 命名管道(FIFO):允许不相关的进程通信,因为它们通过文件系统中的名字进行识别。
管道是简单有效的数据流通信方式,但它们的功能相对有限,比如只支持单向通信,且数据流是无结构的字节流。
System V IPC
System V(System 5)IPC引入了更为复杂和灵活的通信机制,包括消息队列、信号量和共享内存。这些机制不仅支持不相关进程间的通信,还提供了更多的控制机制来同步进程和管理对共享资源的访问。
- 消息队列:允许进程将消息发送到一个队列中,其他进程可以从这个队列中读取消息,支持复杂的通信模式。
- 信号量:主要用于进程间的同步,控制多个进程对共享资源的访问。
- 共享内存:是一种高效的IPC方式,允许多个进程共享一个内存区域,适用于大量数据的交换。
System V IPC机制提供了较强的功能,但使用相对复杂,需要处理更多的资源管理工作。
POSIX IPC
为了解决System V IPC的一些不足,并提供一种更标准化的IPC机制,POSIX(Portable Operating System Interface)引入了自己的IPC方式,包括消息队列、信号量和共享内存。
- POSIX 消息队列:比System V消息队列提供了更好的性能和更强的特性,例如消息优先级。
- POSIX 信号量:提供了更灵活的同步机制,包括局部和命名信号量。
- POSIX 共享内存:提供了一种映射文件或匿名内存到进程地址空间的方式,使得进程间可以通过读写同一块内存来交换数据。
POSIX IPC机制提供了与System V 类似的功能,但具有更好的跨平台支持,并且在API的设计上更为一致和易用。
三、管道
3.1 知识铺垫
3.2 匿名管道
3.2.1 基本概念
匿名管道:仅限于有父子关系的进程间通信。
3.2.2 测试用例:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
int pipefd[2];
pid_t cpid;
char buf;
if (pipe(pipefd) == -1)
{ // 创建管道
perror("pipe");
exit(EXIT_FAILURE);
}
cpid = fork(); // 创建子进程
if (cpid == -1)
{
perror("fork");
exit(EXIT_FAILURE);
}
if (cpid == 0) /* 子进程 */
{
close(pipefd[1]); // 关闭写端
while (read(pipefd[0], &buf, 1) > 0) // 从管道读取数据
{
write(STDOUT_FILENO, &buf, 1);
}
write(STDOUT_FILENO, "\n", 1);
close(pipefd[0]); // 关闭读端
_exit(EXIT_SUCCESS);
}
else /* 父进程 */
{
close(pipefd[0]); // 关闭读端
write(pipefd[1], "Hello, Child!", 13); // 向管道写入数据
close(pipefd[1]); // 关闭写端,表示完成
wait(NULL); // 等待子进程退出
exit(EXIT_SUCCESS);
}
return 0;
}
另外,我们在命令行中使用的 | 就是匿名管道
3.3 管道的行为
我们可以一个父进程创建很多个子进程,这就形成了进程池:
观察进程池我们可以比较清楚的看到管道的实际应用。
3.4 命名管道
前面说了,匿名管道只用于存在血缘关系的进程之间的通信,那如果是不相干的两个进程应当如何通信呢?这就需要使用到命名管道。
这里做一个假设,需要两个进程对同一个文件进行操作,当工程量足够庞大时,我们如何能确定两个进程使用的一定是同一个文件?答案是肯定能确定的,文件的路径是唯一的!
知道了这点,基本的困惑应该也就消除了,下面来看看命名管道的原理:
3.4.1 基本概念
其实看图就可以看出来和匿名管道很像,只需要让两个进程对同一个文件进行操作,把文件的路径写对,然后把重点集中在红字部分就会发现,命名管道就是一个特殊文件,它可以不让缓冲区的数据立马刷到磁盘。
3.4.2 代码演示
下面直接根据代码来看命名管道:
宏定义:
const std::string comm_path = "./5-9Linux内存共享";
#define DefaultFd -1
#define Creater 1
#define User 2
#define Read O_RDONLY
#define Write O_WRONLY
#define BaseSize 4096
类的框架如下:
class NamePiped
{
private:
public:
private:
const std::string _fifo_path;//一个只读的类成员变量,用于存储命名管道的文件路径。
int _id;//类成员变量,用于标示当前对象在逻辑上是“创建者”还是“使用者”。
int _fd;//类成员变量,存储文件描述符(File Descriptor)。打开命名管道(无论是读还是写)后,系统调用 open() 将返回一个文件描述符,该描述符用于后续的读写操作。
};
NamePiped 构造函数
- 功能:根据角色(创建者或用户)创建或准备使用一个命名管道。
- 系统调用:
mkfifo(const char *pathname, mode_t mode)
:创建一个命名管道文件。pathname
指定命名管道文件的名称,mode
指定文件的权限。成功时返回0,失败时返回-1。
NamePiped(const std::string &path, int who)
: _fifo_path(path), _id(who), _fd(DefaultFd)//根据初始化信息构造类
{
if (_id == Creater)//如果识别为创建者,则创建管道
{
int res = mkfifo(_fifo_path.c_str(), 0666);//创建一个名字为path的管道并设置初始权限为0666,其中c_str()是为了统一函数传参类型和传参
if (res != 0)
{
perror("mkfifo");
}
std::cout << "creater create named pipe" << std::endl;
}
}
OpenForRead
- 功能:打开现有的命名管道以读取数据。
- 调用的函数:
OpenNamedPipe(int mode)
,间接使用了下面的系统调用。 - 系统调用:
open(const char *pathname, int flags)
:打开或创建一个文件。这里用于打开命名管道文件,flags
参数设置为O_RDONLY
,表示文件以只读方式打开。
OpenForWrite
- 功能:打开现有的命名管道以写入数据。
- 调用的函数:
OpenNamedPipe(int mode)
,间接使用了下面的系统调用。 - 系统调用:
open
:此处与OpenForRead
类似,但flags
参数设置为O_WRONLY
,表示文件以只写方式打开。
bool OpenForRead()
{
return OpenNamedPipe(Read);//Read在宏定义中,定义为只读
}
bool OpenForWrite()
{
return OpenNamedPipe(Write);//Write在宏定义中,定义为只写
}
ReadNamedPipe
- 功能:从命名管道读取数据。
- 系统调用:
read(int fd, void *buf, size_t count)
:从打开的文件或者设备(在这种情况下是命名管道)中读取数据。fd
是文件描述符,buf
是接收数据的缓冲区地址,count
是缓冲区的大小。返回读取的字节数,失败时返回-1。
int ReadNamedPipe(std::string *out)
{
char buffer[BaseSize];
int n = read(_fd, buffer, sizeof(buffer));//把_fd指向的文件读到buffer中
if(n > 0)
{
buffer[n] = 0;
*out = buffer;
}
return n;
}
WriteNamedPipe
- 功能: 向命名管道写入数据。
- 系统调用:
write(int fd, const void *buf, size_t count)
:写入数据到打开的文件或设备(这里是命名管道)。fd
是文件描述符,buf
是要写入的数据的缓冲区地址,count
是要写入的字节数。返回写入的字节数,失败时返回-1。
int WriteNamedPipe(const std::string &in)//调用函数时需要传入要写入文件的内容
{
return write(_fd, in.c_str(), in.size());//把传参的内容写入文件
}
NamePiped 析构函数
- 功能:销毁对象时关闭文件描述符,并由创建者删除命名管道文件。
- 系统调用:
close(int fd)
:关闭一个打开的文件描述符。成功时返回0,失败时返回-1。unlink(const char *pathname)
:删除一个文件的目录项,并减少文件的链接数。当文件的链接数减少到0,并且没有进程打开该文件时,释放文件占用的资源。此处用于删除命名管道文件。成功时返回0,失败时返回-1。
int WriteNamedPipe(const std::string &in)
{
return write(_fd, in.c_str(), in.size());
}
#pragma once
#include <iostream>
#include <cstdio>
#include <cerrno>
#include <string>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
const std::string comm_path = "./5-9Linux内存共享";
#define DefaultFd -1
#define Creater 1
#define User 2
#define Read O_RDONLY
#define Write O_WRONLY
#define BaseSize 4096
class NamePiped
{
private:
bool OpenNamedPipe(int mode)
{
_fd = open(_fifo_path.c_str(), mode);
if (_fd < 0)
return false;
return true;
}
public:
NamePiped(const std::string &path, int who)
: _fifo_path(path), _id(who), _fd(DefaultFd)
{
if (_id == Creater)
{
int res = mkfifo(_fifo_path.c_str(), 0666);
if (res != 0)
{
perror("mkfifo");
}
std::cout << "creater create named pipe" << std::endl;
}
}
bool OpenForRead()
{
return OpenNamedPipe(Read);
}
bool OpenForWrite()
{
return OpenNamedPipe(Write);
}
int ReadNamedPipe(std::string *out)
{
char buffer[BaseSize];
int n = read(_fd, buffer, sizeof(buffer));
if(n > 0)
{
buffer[n] = 0;
*out = buffer;
}
return n;
}
int WriteNamedPipe(const std::string &in)
{
return write(_fd, in.c_str(), in.size());
}
~NamePiped()
{
if (_id == Creater)
{
int res = unlink(_fifo_path.c_str());
if (res != 0)
{
perror("unlink");
}
std::cout << "creater free named pipe" << std::endl;
}
if(_fd != DefaultFd) close(_fd);
}
private:
const std::string _fifo_path;
int _id;
int _fd;
};
四、共享内存 Shm(Shared memory)
4.1 基本概念
共享内存区是最快的IPC形式。一旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到内核,换句话说是进程不再通过执行进入内核的系统调用来传递彼此的数据
4.2 相关函数
4.2.1 shmget
功能
函数原型
int shmget(key_t key, size_t size, int shmflg);
参数
返回值
使用场景
创建或访问共享内存用于存储进程间共享的数据。
4.2.2 shmat
功能
函数原型
void *shmat(int shmid, const void *shmaddr, int shmflg);
参数
返回值
使用场景
在进行进程间通信时,需要访问共享内存段中存储的数据。
4.2.3 shmdt
功能
函数原型
int shmdt(const void *shmaddr);
参数
返回值
使用场景
结束对共享内存的访问,通常在进程认为自己不再需要共享内存数据后调用。
4.2.4 shmctl
功能
函数原型
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数
返回值
使用场景
在需要检视或修改共享内存属性,或删除共享内存段时使用。
4.3 代码演示
4.3.1 shm.hpp
#ifndef __SHM_HPP__
#define __SHM_HPP__
#include <iostream>
#include <string>
#include <cerrno>
#include <cstdio>
#include <cstring>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
const int gCreater = 1;
const int gUser = 2;
const std::string gpathname =
"/home/Flash/studying/2024-5/5-9Linux内存共享";
const int gproj_id = 0x66;
const int gShmSize = 4097; // 4096*n
class Shm
{
public:
/*作用: 根据传入的路径、项目ID和用户角色(创建者或使用者)初始化共享内存。它首先获取键值,然后根据角色创建或连接共享内存,并最后将共享内存连接到进程的地址空间。
使用场景: 创建一个 Shm 对象,自动完成共享内存的创建或连接以及初始化操作。*/
Shm(const std::string &pathname, int proj_id, int who)
: _pathname(pathname), _proj_id(proj_id), _who(who), _addrshm(nullptr)
{
_key = GetCommKey();
if (_who == gCreater)
GetShmUseCreate();
else if (_who == gUser)
GetShmForUse();
_addrshm = AttachShm();
std::cout << "shmid: " << _shmid << std::endl;
std::cout << "_key: " << ToHex(_key) << std::endl;
}
/*作用: 断开共享内存的连接,并且如果是创建者,则删除共享内存段。
使用场景: 当 Shm 对象生命期结束时,自动清理资源,确保共享内存被正确管理。*/
~Shm()
{
if (_who == gCreater)
{
int res = shmctl(_shmid, IPC_RMID, nullptr);
}
std::cout << "shm remove done..." << std::endl;
}
/*作用: 将键值转换为十六进制字符串,用于打印日志。
使用场景: 在调试或记录日志时,显示键值。*/
std::string ToHex(key_t key)
{
char buffer[128];
snprintf(buffer, sizeof(buffer), "0x%x", key);
return buffer;
}
/*作用: 分别用于创建和获取共享内存段。GetShmUseCreate 使用 IPC_CREAT | IPC_EXCL 标志,确保只有在共享内存不存在时,才创建新的共享内存。
而 GetShmForUse 则用于连接到已经存在的共享内存。
使用场景: 根据进程的角色(创建者或使用者),选择合适的方法来获取共享内存标识符*/
bool GetShmUseCreate()
{
if (_who == gCreater)
{
_shmid = GetShmHelper(_key, gShmSize, IPC_CREAT | IPC_EXCL | 0666);
if (_shmid >= 0)
return true;
std::cout << "shm create done..." << std::endl;
}
return false;
}
bool GetShmForUse()
{
if (_who == gUser)
{
_shmid = GetShmHelper(_key, gShmSize, IPC_CREAT | 0666);
if (_shmid >= 0)
return true;
std::cout << "shm get done..." << std::endl;
}
return false;
}
/*作用: 将整个共享内存段的数据置零。
使用场景: 初始化共享内存内容,或者在某些操作完成后清理共享内存。*/
void Zero()
{
if (_addrshm)
{
memset(_addrshm, 0, gShmSize);
}
}
/*作用: 返回共享内存段在当前进程地址空间中的起始地址。
使用场景: 当需要操作共享内存中的数据时,可以通过此地址来访问。*/
void *Addr()
{
return _addrshm;
}
/*作用: 使用 shmctl 的 IPC_STAT 命令获取共享内存的状态,并打印相关信息。
使用场景: 调试或监控共享内存的使用情况。*/
void DebugShm()
{
struct shmid_ds ds;
int n = shmctl(_shmid, IPC_STAT, &ds);
/*int shmctl(int shmid, int cmd, struct shmid_ds *buf);
功能:对共享内存段执行控制操作,比如删除共享内存段。
参数:
shmid:共享内存标识符。
cmd:命令标志,例如IPC_STAT(获取共享内存的状态)、IPC_SET(设置共享内存的参数)或IPC_RMID(删除共享内存段)。
buf:指向shmid_ds结构体的指针,该结构体包含共享内存段的当前状态信息。
返回值:成功时返回0,失败时返回-1。*/
if (n < 0)
return;
std::cout << "ds.shm_perm.__key : " << ToHex(ds.shm_perm.__key) << std::endl;
std::cout << "ds.shm_nattch: " << ds.shm_nattch << std::endl;
}
private:
/*作用: 通过 ftok 函数生成一个唯一的键值,用于共享内存的创建或访问。
使用场景: 在创建共享内存之前需要先获取一个键值。*/
key_t GetCommKey()
{
key_t k = ftok(_pathname.c_str(), _proj_id);
/*key_t ftok(const char *pathname, int proj_id);
功能:生成一个System V IPC键值(key),用于shmget函数。需要给定一个路径名和一个项目ID(非零),通常用于确保生成的键值唯一。
参数:
pathname:指向一个存在的文件的路径字符串。
proj_id:一个非零整数,通常是一个字符常量,用于帮助生成唯一键。
返回值:成功时返回键值,失败时返回-1。*/
if (k < 0)
{
perror("ftok");
}
return k;
}
/*作用: 封装了 shmget 函数,根据提供的键值、大小和标志来获取共享内存标识符。
使用场景: 创建共享内存或获取访问既存共享内存的标识符。*/
int GetShmHelper(key_t key, int size, int flag)
{
int shmid = shmget(key, size, flag);
/*int shmget(key_t key, size_t size, int shmflg);
功能:根据指定的键值key获取共享内存标识符shmid(创建或访问共享内存段)。
参数:
key:共享内存段的键值。
size:共享内存段的大小,以字节为单位。
shmflg:权限标志,可以是权限位的组合,如0666(八进制),可能还会包括IPC_CREAT(不存在则创建)、IPC_EXCL(与IPC_CREAT同时使用,若已存在则失败)等。
返回值:成功时返回共享内存段的标识符,失败时返回-1。*/
if (shmid < 0)
{
perror("shmget");
}
return shmid;
}
/*作用: 将角色标识(创建者或使用者)转换为字符串表示,用于打印日志。
使用场景: 在日志输出时,标明当前操作是由创建者还是使用者进行。*/
std::string RoleToString(int who)
{
if (who == gCreater)
return "Creater";
else if (who == gUser)
return "gUser";
else
return "None";
}
/*作用: 通过 shmdt 函数断开共享内存段与当前进程的连接。
使用场景: 当完成对共享内存的操作后,为了避免资源泄露,需要将其从进程的地址空间断开。*/
void DetachShm(void *shmaddr)
{
if (shmaddr == nullptr)
return;
shmdt(shmaddr); // shmdt断开共享内存段与当前进程的连接
/*int shmdt(const void *shmaddr);
功能:断开共享内存段与当前进程的连接。
参数:
shmaddr:共享内存段在当前进程中的起始地址指针。
返回值:成功时返回0,失败时返回-1。*/
std::cout << "who: " << RoleToString(_who) << " detach shm..." << std::endl;
}
/*作用: 通过 shmat 函数将共享内存段连接到当前进程的地址空间。
使用场景: 当需要在进程中读写共享内存中的数据时,需要先将其连接到进程的地址空间。*/
void *AttachShm()
{
if (_addrshm != nullptr)
DetachShm(_addrshm);
void *shmaddr = shmat(_shmid, nullptr, 0);
/*void *shmat(int shmid, const void *shmaddr, int shmflg);
功能:将共享内存段连接(attach)到当前进程的地址空间。
参数:
shmid:共享内存标识符。
shmaddr:指定共享内存连接到当前进程中的地址位置,通常设为NULL,让系统选择该地址。
shmflg:操作标志,设置为0表示允许读写操作。
返回值:成功时返回指向共享内存第一个字节的指针,失败时返回-1。*/
if (shmaddr == nullptr) // 共享内存首字节为空,说明没有创建共享内存
{
perror("shmat");
}
std::cout << "who: " << RoleToString(_who) << " attach shm..." << std::endl;
return shmaddr;
}
private:
key_t _key;
int _shmid;
std::string _pathname;
int _proj_id;
int _who;
void *_addrshm;
};
#endif
4.3.2 server.cc(服务端)
#include "Shm.hpp"
int main()
{
//创建共享内存
Shm shm(gpathname, gproj_id, gCreater);
char *shmaddr = (char*)shm.Addr();
shm.DebugShm();
sleep(5);
return 0;
}
4.3.3 client.cc(客户端)
#include "Shm.hpp"
int main()
{
//创建共享内存
Shm shm(gpathname, gproj_id, gUser);
shm.Zero();
char *shmaddr = (char *)shm.Addr();
sleep(3);
return 0;
}