目录
一、进程通信的初步认识
1.1 进程间通信目的
1.2 进程间通信的种类
管道(Pipes)
System V IPC
POSIX IPC
三、管道
3.1 知识铺垫
3.2 匿名管道
3.2.1 基本概念
3.2.2 测试用例:
3.3 管道的行为
3.4 命名管道
3.4.1 基本概念
3.4.2 代码演示
4.1 基本概念
4.2 相关函数
4.2.1 shmget
功能
函数原型
参数
返回值
使用场景
4.2.2 shmat
功能
函数原型
参数
返回值
使用场景
4.2.3 shmdt
功能
函数原型
参数
返回值
使用场景
4.2.4 shmctl
功能
函数原型
参数
返回值
使用场景
4.3 代码演示
4.3.1 shm.hpp
4.3.2 server.cc(服务端)
4.3.3 client.cc(客户端)
一、进程通信的初步认识
1.1 进程间通信目的
数据传输:一个进程需要将它的数据发送给另一个进程。
资源共享:多个进程之间共享同样的资源。
通知事件:一个进程需要向另一个或一组进程发送消息,通知它(们)发生了某种事件(如进程终止时要通知父进程)。
进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。
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 基本概念
匿名管道:仅限于有父子关系的进程间通信。
#include <unistd.h>
功能 : 创建一无名管道
原型 : int pipe(int fd[2]);当你调用
pipe(fd)
时,它会创建一个管道,并产生两个文件描述符:
fd[0]
用于读取管道。fd[1]
用于写入管道。数据写入
fd[1]
的一端可以从fd[0]
这端读取出来,实现了单向通信。需要注意的是,管道中的数据是按照先入先出(FIFO)的顺序处理的。
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 代码演示
下面直接根据代码来看命名管道:
宏定义:
1.const std::string comm_path = "./5-9Linux内存共享";
这是一个全局常量声明,不是传统意义上的宏。它定义了一个字符串常量 comm_path ,用于指定默认的命名管道文件路径。
2.#define DefaultFd -1
定义了一个宏 DefaultFd 其值为-1。-1通常用来表示无效的文件描述符(File Descriptor)。
3.#define Creater 1
#define User 2
这两个宏用来标识“创建者”和“使用者”角色的常量,也就是开发端和客户端,使代码更加美观
4.#define Read O_RDONLY
#define Write O_WRONLY
为了使代码更加具有可读性,使用宏定义分别替换了只读和只写的系统宏定义。
5.#define BaseSize 4096
这里使用了一个通常用作读写操作的基本缓冲区大小,缓冲区大小一般设置为4096的整数倍
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 4096class 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
功能
shmget
是一个System V共享内存系统调用,用于创建新的共享内存段或访问一个已存在的共享内存段。
函数原型
int shmget(key_t key, size_t size, int shmflg);
参数
key
: 这是共享内存段的识别符。可以由ftok
函数产生,或者可以指定一个明确的键值。size
: 共享内存段大小,单位是字节。当创建新的共享内存段时需要指定。shmflg
: 操作标志,由一个或多个权限位(如 0644)和可能的标志位(如IPC_CREAT
,IPC_EXCL
)组合而成。IPC_CREAT
表示如果指定的共享内存段不存在,则创建它;IPC_EXCL
与IPC_CREAT
同时使用时,如果共享内存已存在,则shmget
调用失败。
返回值
- 成功返回共享内存段的标识符(一个非负整数)。
- 失败返回
-1
并设置errno
以指示错误类型。
使用场景
创建或访问共享内存用于存储进程间共享的数据。
4.2.2 shmat
功能
shmat
是用于将共享内存段附加到当前进程的地址空间。通俗点将就是把调用该函数的进程链接到共享内存。
函数原型
void *shmat(int shmid, const void *shmaddr, int shmflg);
参数
shmid
: 使用shmget
函数获取的共享内存段标识符。shmaddr
: 指针,建议的附加地址。通常设置为NULL
,让操作系统选择地址。shmflg
: 操作标志,设置为SHM_RDONLY
立即将内存段设为只读,否则默认为读写。
返回值
- 成功时返回共享内存段附加后的地址指针。
- 失败时返回
(void *)-1
并设置errno
。
使用场景
在进行进程间通信时,需要访问共享内存段中存储的数据。
4.2.3 shmdt
功能
shmdt
用于断开当前进程与共享内存段的连接。
函数原型
int shmdt(const void *shmaddr);
参数
shmaddr
: 之前通过shmat
获得的共享内存段的地址。
返回值
- 成功返回
0
。- 失败返回
-1
并设置errno
。
使用场景
结束对共享内存的访问,通常在进程认为自己不再需要共享内存数据后调用。
4.2.4 shmctl
功能
shmctl
对共享内存段执行各种控制操作。
函数原型
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数
shmid
: 共享内存段的标识符。cmd
: 控制命令,如IPC_STAT
(获得共享内存段的状态),IPC_SET
(设置共享内存段的参数),IPC_RMID
(标记共享内存段删除)。buf
: 指向shmid_ds
结构体的指针,用来存储共享内存段的状态或设置状态,依赖于cmd
参数。
返回值
- 成功返回
0
。- 失败返回
-1
并设置errno
。
使用场景
在需要检视或修改共享内存属性,或删除共享内存段时使用。
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*nclass 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";elsereturn "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;
}