🎉Linux:进程间通信(Inter-Process Communication,IPC)
博主主页:桑榆非晚ᴷ
博主能力有限,如果有出错的地方希望大家不吝赐教
给自己打气:每一点滴的进展,都是缓慢而艰苦的,祝我们都能在往后的生活里找到属于自己有意义的快乐🥰🎉✨
一、🍔进程间通信目的
1.1 🌭进程间通信的目的
我们知道,每一个进程都有自己的task_struct,每一个进程在产生的时候OS都会个各自的进程创属于自己的进程地址空间,这样一来就会确保进程之间是相互独立的。即使是父子进程,它们也会有各自的进程地址空间,父子之前代码共享,数据会发生写时拷贝,可想而知,就算是父子进程也是相互独立的,更不要说其他没有任何关系的进程了。但是在一些特定的需求下,必须要求进程之间可以发生信息交互,这些需求目的一般为:
- 数据传输:一个进程需要将它的数据发送给另一个进程
- 资源共享:多个进程之间共享同样的资源。
- 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
- 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。
二、🍔进程间通信分类
由于进程是具有独立性的,所以进程间发生信息交互是一件成本比较高的事情。想要发生进程间通信的技术这里介绍三类:
- 管道 (pipe)
- 匿名管道 (学习)
- 命名管道 (学习)
- System V IPC
- System V 消息队列
- System V 共享内存 (学习)
- System V 信号量
- POSIX IPC
- 消息队列
- 共享内存
- 信号量
- 互斥量
- 条件变量
- 读写锁
三、🍔进程间通信实例演示
3.1 🌭匿名管道
3.1.1 🍕匿名管道通信的原理
首先,我们抛开匿名管道来说,在我们日常中信息交互时,信息交互的双方得先有一个双方都可以通过数据交互的渠道吧。比如,QQ聊天的两个网友,他们的消息是通过网线把消息进程交互的。这个网线就是一个渠道。现在回到进程间通信,我们可以把两个进程看作为两个网友,匿名管道就是网线。现在两个进程之间有了一个通信的渠道,这个渠道使得两个进程可以看到了同一份资源。这样一来通过这个渠道,我们就可以把资源数据进行交互。所以进程间通信的本质就是:要让通信的进程通过一个渠道看到同一份资源。
之前说过,Linux下一切皆文件。所以说,匿名管道也是文件。
当我们利用管道进行通信时,管道内部的缓冲区就不会向磁盘刷新内容,而是向管道的另一个端口进行数据刷新,输出数据进行通信。OS会根据上图的进行对文件识别,pipe_inode_info为管道文件。原理就是把文件中的缓冲区当作管道,一边输入一边输出。
3.1.2 🍕匿名管道创建函数
系统接口 int pipe(int pipefd[2])函数就是用来创建匿名管道的。数组pipefd是一个输出型参数,数组里面存放的是两个文件描述符。其中pipefd[0]是用来读取管道里的文件描述符,pipefd[1]是用来向管道里写入的文件描述符。如果管道创建成功,则返回0,否则返回-1。匿名管道的通信本质:
由父进程通过系统调用接口int pipe(int pipefd[2])创建管道,把指向匿名管道文件的两个读写文件描述符通过输出型参数传出来(父进程已经分别打开匿名管道的读写两端),然后在fork()子进程,子进程继承父进程的task_struct的大部分内容,包括files_struct,所以子进程也有相同的文件描述符指向这个管道文件,这样一来,父子间通信就有了渠道,只要把父进程的读端关闭close(pipefd[0]),子进程的写端关闭close(pipefd[1]),父进程就可以向子进程发送数据了。也可以子进程向父进程发送数据,只要把上面的读写端反过来关闭即可。
下面为父进程向子进程发送数据,父进程关闭读端,子进程关闭写端:
匿名管道通信的本质就是父进程创建管道,子进程继承管道,建立起通信的渠道,即可发生信息交互。
3.1.3 🍕匿名管道通信代码实例
// 匿名管道初级版 -> 演示管道通信的基本过程 #include <iostream> #include <string> #include <cstdio> #include <ctime> #include <cstring> #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> using namespace std; int main() { // 1.创建管道 int pipefd[2] = { 0 }; if (pipe(pipefd) != 0) { cerr << "creating pipe error" << endl; return 1; } // 2.创建子进程 pid_t id = fork(); if (id < 0) { cerr << "fork error" << endl; return 2; } else if (0 == id) { // child process // 子进程来进行read,关闭write close(pipefd[1]); #define NUM 1024 char buffer[NUM]; while (true) { // 子进程没有带sleep,为啥子哦进程也会休眠 // 子进程体现休眠 -> 子进程三秒读一次消息 -> 当父进程没有写入数据的时候,子进程在等 -> 所以父进程写入之后,子进程才能read到数据, // 子进程打印读取数据要以父进程的节奏为主!父子进程在读写(信息交互,通信)的时候,是有一定的顺序的!!! // 管道内部,没有数据的时候,读端必须阻塞等待,等管道有数据 // 管道内部,如果数据被写满了,写端必须阻塞等待,等管道有空间 (管道大小一般为 4096 byte) // pipe内部,自带访问控制机制 -> 同步和互斥机制! cout << "时间戳 : " << (long long)time(nullptr) << endl; memset(buffer, 0, NUM); ssize_t s = read(pipefd[0], buffer, sizeof(buffer) - 1); if (s > 0) { // 读取成功 buffer[s] = '\0'; cout << "Child process received message: " << buffer << endl; } else if (0 == s) // 父进程把写端关闭了,子进程就读不到数据,返回0 { cout << "Parent process finished writing, i'm quitting" << endl; // 父进程把管道的写端关闭了,子进程就可以立马知道!那么子进程是怎么知道父进程把写端关闭了呢? // 其实很简单,文件会有文件计数器,父进程把写段关闭了,文件计数器就会发生变化,由于父子共用一个文件(管道),所以子进程立马会感知到 break; } else { cerr << "Reading is error" << endl; } } close(pipefd[0]); exit(0); } else { // parent process // 父进程来进行write close(pipefd[0]); // const char* msg = "Hello child process, i am parent proces!"; string msg("Hello child process, i am parent proces, message number"); int cnt = 0; while (cnt < 5) { char sendBuffer[1024]; sprintf(sendBuffer, "%s : %d", msg.c_str(), cnt); write(pipefd[1], sendBuffer, strlen(sendBuffer)); sleep(3); ++cnt; } close(pipefd[1]); cout << "Parent process finished writing" << endl; pid_t ret = waitpid(id, nullptr, 0); if (ret > 0) { cout << "Waitting child process is success!" << endl; } } // 0 -> 嘴巴 -> read // 1 -> 笔 -> write return 0; }
// 匿名管道 -> 匿名管道的应用 -> 父进程控制单个子进程做任务 #include <iostream> #include <string> #include <vector> #include <unordered_map> #include <cstring> #include <ctime> #include <cassert> #include <cstdlib> #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> using namespace std; typedef void (*functor)(); // 函数指针 typedef pair<pid_t, uint32_t> elem; // 存放进程pid 和 写端文件描述符 vector<functor> functors; // 方法集合 unordered_map<uint32_t, string> info; int processNum = 5; void fun1() { cout << "这是一个处理日志的任务, 执行的进程ID [" << getpid() << "]" << "执行时间是[" << (long long)time(nullptr) << "]" << endl; } void fun2() { cout << "这是一个备份数据的任务, 执行的进程ID [" << getpid() << "]" << "执行时间是[" << (long long)time(nullptr) << "]" << endl; } void fun3() { cout << "这是一个网络连接的任务, 执行的进程ID [" << getpid() << "]" << "执行时间是[" << (long long)time(nullptr) << "]" << endl; } void loadFunctor() { info.insert({functors.size(), "处理日志的任务"}); functors.push_back(fun1); info.insert({functors.size(), "备份数据的任务"}); functors.push_back(fun2); info.insert({functors.size(), "网络连接的任务"}); functors.push_back(fun3); } int main() { // 1.加载任务列表 loadFunctor(); // 2.创建管道 int pipefd[2] = {0}; if (pipe(pipefd) != 0) { cerr << "Creating pipe error" << endl; } // 3.创建子进程 pid_t id = fork(); if (id < 0) { cerr << "fork error" << endl; } else if (id == 0) { // child -> read close(pipefd[1]); while (true) { // 5.子进程执行任务 uint32_t operatorCode = 0; ssize_t s = read(pipefd[0], &operatorCode, sizeof(operatorCode)); if (s == 0) { cout << "Parent process writing finished, i'm quitting!" << endl; break; } assert(s == sizeof(operatorCode)); (void)s; // assert断言,是编译有效 debug 模式 // release 模式,断言就没有了. // 一旦断言没有了,s变量就是只被定义了,没有被使用。release模式中,可能会有warning if (operatorCode < functors.size()) { functors[operatorCode](); } else { cout << "bug?! operatorCode =" << operatorCode << endl; } } close(pipefd[0]); exit(0); } else { // parent ->write srand((long long)time(nullptr)); close(pipefd[0]); int cnt = 10; while (cnt--) { // 4.父进程指派任务 uint32_t taskIndex = rand() % functors.size(); cout << "父进程指派的任务是" << info[taskIndex] << "任务编号为" << taskIndex << endl; write(pipefd[1], &taskIndex, sizeof(taskIndex)); sleep(1); } cout << "Parent process writing finished" << endl; close(pipefd[1]); // 4.等待子进程 pid_t ret = waitpid(id, nullptr, 0); if (ret > 0) { cout << "Waitting child process success!" << endl; } } return 0; }
// 匿名管道 -> 匿名管道的应用 -> 父进程控制 多个子进程做任务(进程池) #include <iostream> #include <string> #include <vector> #include <unordered_map> #include <cstring> #include <ctime> #include <cassert> #include <cstdlib> #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> using namespace std; typedef void (*functor)(); // 函数指针 typedef pair<pid_t, uint32_t> elem; // 存放进程pid 和 写端文件描述符 vector<functor> functors; // 方法集合 unordered_map<uint32_t, string> info; int processNum = 5; void fun1() { cout << "这是一个处理日志的任务, 执行的进程ID [" << getpid() << "]" << "执行时间是[" << (long long)time(nullptr) << "]" << endl; } void fun2() { cout << "这是一个备份数据的任务, 执行的进程ID [" << getpid() << "]" << "执行时间是[" << (long long)time(nullptr) << "]" << endl; } void fun3() { cout << "这是一个网络连接的任务, 执行的进程ID [" << getpid() << "]" << "执行时间是[" << (long long)time(nullptr) << "]" << endl; } void loadFunctor() { info.insert({functors.size(), "处理日志的任务"}); functors.push_back(fun1); info.insert({functors.size(), "备份数据的任务"}); functors.push_back(fun2); info.insert({functors.size(), "网络连接的任务"}); functors.push_back(fun3); } void work(int readfd) { while (true) { uint32_t operatorCode = 0; ssize_t s = read(readfd, &operatorCode, sizeof(uint32_t)); if (s == 0) { cout << "Parent process finished writing, we're quitting!" << endl; break; } assert(s == sizeof(uint32_t)); (void)s; cout << "子进程[" << getpid() << "]开始工作" << endl; if (operatorCode < functors.size()) { functors[operatorCode](); } else { cerr << "bug? operatorCode = " << operatorCode << endl; } } } void sendTask(vector<elem> &processFds) { int cnt = 10; while (cnt--) { srand((long long)time(nullptr)); // 1.选择一个进程 int processIndex = rand() % processFds.size(); // 2.选择一个任务 uint32_t taskIndex = rand() % functors.size(); // 3.向子进程派发 cout << "父进程发派任务->" << info[taskIndex] << "给子进程: " << processFds[processIndex].first << "进程编号: " << processIndex << "任务编号: " << taskIndex << endl; sleep(1); write(processFds[processIndex].second, &taskIndex, sizeof(taskIndex)); } } void closeAll(vector<elem> &processFds) { for (int i = 0; i < processFds.size(); ++i) { close(processFds[i].second); } } int main() { // 1.加载任务列表 loadFunctor(); vector<elem> assignMap; // 2.创建子进程和管道 for (int i = 0; i < processNum; ++i) { int pipefd[2] = {0}; pipe(pipefd); if (pipe(pipefd) != 0) { cerr << "creating pipe error" << endl; return 1; } pid_t id = fork(); if (id < 0) { cerr << "fork error" << endl; } else if (id == 0) { // child -> read close(pipefd[1]); // 3.子进程执行任务 work(pipefd[0]); close(pipefd[0]); exit(0); } else { // parent -> write close(pipefd[0]); elem e(id, pipefd[1]); assignMap.push_back(e); } } cout << "Creating all processes is successful" << endl; // 4.父进程给子进程派发任务 sendTask(assignMap); closeAll(assignMap); cout << "Parent process finished writing" << endl; // 5.等待子进程 for (int i = 0; i < processNum; ++i) { pid_t ret = waitpid(assignMap[i].first, nullptr, 0); if (ret > 0) { cout << "Waitting child process: " << ret << "success!" << endl; } } return 0; }
3.1.4🌭 匿名管道通信的特点
匿名管道只能用来具有血缘关系的进程进行进程间通信。
匿名管道只能进行单向通信,半双工的一种特殊情况
匿名管道自带同步机制 — (如果管道被写满,write端阻塞等待,如果管道为空,read端阻塞等待)
匿名管道的生命周期是随进程的,因为管道是文件,进程一旦退出了,文件的引用计数为零,则就会结束管道生命周期。
管道是面向字节流的 (现在还解释不清楚)
3.2 🌭命名管道
3.2.1 🍕命名管道通信原理
命名管道通信原理还是利用了管道文件中的缓冲区,只是这个管道文件需要我们利用系统接口进行创建,然后两个进程可以一个读文件,一个写文件,这样就可以实现通信了。
3.2.2🍕操作函数
mkfifo函数:int mkfifo(const char *pathname, mode_t mode);
- 功能:创建一个管道文件
- 头文件: #include <sys/types.h> #include <sys/stat.h>
- 参数:int mkfifo(const char *pathname, mode_t mode);
- pathname:要创建管道文件的路进程
- mode:这个管道文件的rwx权限
- 返回值:success return 0,error return -1
3.2.3🍕命名管道代码编写
comm.h
#pragma once #include <iostream> #include <cstring> #include <cerrno> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> const char* IPC_PATH = "./.fifo"; const int NUM = 1024;
serverFifo.cpp
#include "comm.h" using namespace std; // 进行读取 int main() { umask(0); // 1.创建命名管道(Creating Name Pipe) if (mkfifo(IPC_PATH, 0600) != 0) { cerr << "mkfifo error: " << strerror(errno) << endl; return 1; } // 2.打开命名管道 int rdfd = open(IPC_PATH, O_RDONLY); if (rdfd < 0) { cerr << "open rdfd error: " << strerror(errno) << endl; return 2; } // 3.进行正常通信 char buffer[NUM]; while (true) { ssize_t s = read(rdfd, buffer, sizeof(buffer) - 1); if (s > 0) { buffer[s] = '\0'; cout << "客户端->服务端 # " << buffer << endl; } else if (s == 0) { cout << "客户端退出,我也退出啦!" << endl; break; } else { cerr << "read error: " << strerror(errno) << endl; break; } } close(rdfd); cout << "服务端退出啦" << endl; unlink(IPC_PATH); return 0; }
clientFifo.cpp
#include "comm.h" using namespace std; // 进行写入 int main() { // 1.打开写端 int wrfd = open(IPC_PATH, O_WRONLY); if (wrfd < 0) { cerr << "open wrfd error: " << strerror(errno) << endl; } // 2.进行正常通信 char line[NUM]; while (true) { printf("请输入你的消息# "); fflush(stdout); memset(line, 0, sizeof(line)); if (fgets(line, sizeof(line), stdin) != nullptr) { line[strlen(line) - 1] = '\0'; write(wrfd, line, strlen(line)); } else { cerr << "writing error" << strerror(errno) << endl; break; } } close(wrfd); cout << "客户端退出啦" << endl; return 0; }
3.2.4🍕命名管道特点
和匿名管道差不多,只不过命名管道支持非血缘关系的进程通信。
3.3🌭共享内存
3.3.1 🍕共享内存的原理
system -v ipc 共享内存的原理就是让多个进程通过个自己的虚拟地址空间的一部分映射到同一块物理地址空间,这样多个进程就可以通过这一块公共的物理内存进行通信了。
3.3.2🍕操作函数
shmget函数:int shmget(key_t key, size_t size, int shmflg);
- 功能:创建共享内存
- 头文件: #include <sys/ipc.h> #include <sys/shm.h>
- 参数:int shmget(key_t key, size_t size, int shmflg);
- key:这个共享内存的唯一值,由用户提供,用来标识这个共享内存,理论上key值可以由用户随便提供,但一般我们不这么做,我们会用ftok()函数生成key值;
- size:设置共享内存的大小,建议是页(4KB)的整数倍;
- shmflg:创建共享内存的选项模式。一共有两个,分别为IPC_CREAT和IPC_EXCL。IPC_CREAT:要创建共享内存如果已经存在,就获取之,如果不存在,就创建并获取之。IPC_EXCL:IPC_EXCL一般不单独使用,必须和IPC_CREAT配合使用(IPC_CREAT | IPC_EXCL)。如果要创建的共享内存不存在,就创建并获得之,如果要创建的共享内存已经存在了,就出错返回。IPC_CREAT和IPC_EXCL配合使用可以保证创建的共享内存一定是全新创建的共享内存,而不是之前已经存在的共享内存。
- 返回值:成功返回一个非负整数,即该共享内存段的标识码;失败返回-1
shmctl函数:int shmctl(int shmid, int cmd, struct shmid_ds *buf);
功能:用于控制共享内存
头文件:#include <sys/ipc.h> #include <sys/shm.h>
参数:int shmctl(int shmid, int cmd, struct shmid_ds *buf);
shmid:要控制的共享内存用户层的编号shmid。
cmd:要控制目标共享内存的方式,由于这里主要是为了删除共享内存,所以传入IPC_RMID即可。
cmd:控制命令
- IPC_STAT:获取属性信息,放置到buf中
- IPC_SET:设置属性信息为buf指向的内容
- IPC_RMID:删除该共享内存
- IPC_INFO:获得关于共享内存的系统限制值信息
- SHM_INFO:获得系统为共享内存消耗的资源信息
- SHM_STAT:与IPC_STAT具有相同的功能,但shmid为该SHM在内核中记录所有SHM信息的数组的下标, 因此通过迭代所有的下标可以获得系统中所有SHM的相关信息
- SHM_LOCK:禁止系统将该SHM交换至swap分区
- SHM_UNLOCK:允许系统将该SHM交换至swap分
buf:这个一个输出型参数,如果想获取shmid_ds中的内容(共享内存的属性)及cmd的方式为IPC_STAT,可以传入struct shmid_ds类型的指针buf,将与shmid关联的内核数据结构中的信息复制到shmid_ds中buf指向的结构。
- 返回值:成功返回0;失败返回-1
shmat函数:void *shmat(int shmid, const void *shmaddr, int shmflg);
功能:将共享内存段连接到进程地址空间
头文件: #include <sys/types.h> #include <sys/shm.h>
参数:void *shmat(int shmid, const void *shmaddr, int shmflg);
shmid:要关联的共享内存用户层编号shmid
shmadder:挂接在地址空间的位置,这里我们选择nullptr,让OS选择合适的地址挂接这段共享内存。
shmflg:把它设置为零,可以让进程对这段共享内存有读写权限。
返回值:success返回这段共享内存的起始地址,error返回(void*)-1。
shmdt函数:int shmdt(const void *shmaddr);
- 功能:将共享内存段与当前进程脱离
- 参数:int shmdt(const void *shmaddr);
- shmaddr:由shmat所返回的指针
- 头文件:#include <sys/types.h> #include <sys/shm.h>
- 返回值:成功返回0;失败返回-1
3.3.3🍕共享内存代码编写
comm.hpp
#pragma once #include <iostream> #include <ctime> #include <cstring> #include <cerrno> #include <sys/types.h> #include <sys/ipc.h> #include <sys/shm.h> #include <unistd.h> #include <sys/stat.h> #include <fcntl.h> const char* PATH_NAME = "/home/lk/Linux"; const int PROJ_ID = 0X14; const int SIZE_MEM = 4096; const char* PATH_FIFO = ".fifo"; key_t CreateKey() { key_t key = ftok(PATH_NAME, PROJ_ID); if (key < 0) { std::cerr << "CreateKey error: " << strerror(errno) << std::endl; exit(1); } return key; } std::ostream& Log() { std::cout << "For Debug |" << "timestamp " << (uint64_t)time(nullptr) << "| "; return std::cout; }
IpcShmSer.cpp
#include "Comm.hpp" using namespace std; // 创建共享内存的角色 // 创建一个全新的共享内存 const int flags = IPC_CREAT | IPC_EXCL; int main() { // 1.生成一个唯一的key值 key_t key = CreateKey(); Log() << "key: " << key << endl; // 2.创建共享内存 Log() << "Create shm begin" << endl; int shmid = shmget(key, SIZE_MEM, flags | 0666); if (shmid < 0) { cerr << "shm error " << strerror(errno) << endl; return 2; } Log() << "Create shmget: " << shmid << "success" << endl; // sleep(3); // 3.关联共享内存 Log() << "Attach shm begin" << endl; char* str = reinterpret_cast<char*> (shmat(shmid, nullptr, 0)); if (str == reinterpret_cast<void*>(-1)) { cerr << "Attach error: " << strerror(errno) << endl; } Log() << "Attach shm " << shmid << "success" << endl; // sleep(5); // 4.使用共享内存 // ...... Log() << "Using shm begin" << endl; while (true) { cout << "." << str << endl; sleep(1); } // 5.去关联共享内存 Log() << "Detach shm begin" << endl; int ret1 = shmdt(str); if (ret1 < 0) { cerr << "Detach shm error: " << strerror(errno) << endl; } Log() << "Detach shm " << shmid << "success" << endl; // sleep(5); // 6.删除共享内存 Log() << "Destory shm begin" << endl; int ret2 = shmctl(shmid, IPC_RMID, nullptr); if (ret2 < 0) { cerr << "Destory shm error" << strerror(errno) << endl; } Log() << "Destory shm " << shmid << "sucess" << endl; return 0; }
IpcShmCli.cpp
#include "Comm.hpp" using namespace std; int main() { // 1. 生成一个唯一的key值 key_t key = CreateKey(); Log() << "key: " << key << endl; // 2.创建共享内存 Log() << "Create shm begin" << endl; int shmid = shmget(key, SIZE_MEM, IPC_CREAT); if (shmid < 0) { cerr << "shm error" << strerror(errno) << endl; return 2; } Log() << "Create shmget: " << shmid << "success" << endl; // sleep(5); // 3.关联共享内存 Log() << "Attach shm begin" << endl; char* str = reinterpret_cast<char*> (shmat(shmid, nullptr, 0)); if (str == reinterpret_cast<void*>(-1)) { cerr << "Attach error: " << strerror(errno) << endl; } Log() << "Attach shm " << shmid << "success" << endl; // sleep(5); // 4.使用共享内存 // 我们没有使用任何系统接口,直接向共享内存写入 // 因为共享内存被挂接到了用户层,用户可以直接使用 Log() << "Using shm begin" << endl; int cnt = 5; while (cnt--) { printf("Please Enter# "); fflush(stdout); ssize_t s = read(0, str, SIZE_MEM); str[s] = '\0'; } // int cnt = 0; // while (cnt <= 26) // { // str[cnt] = 'A' + cnt; // ++cnt; // str[cnt] = '\0'; // sleep(1); // } // 5.去关联共享内存 Log() << "Detach shm begin" << endl; int ret1 = shmdt(str); if (ret1 < 0) { cerr << "Detach shm error: " << strerror(errno) << endl; } Log() << "Detach shm " << shmid << "success" << endl; return 0; }
3.3.4 🍕ipc指令(命令行)
ipcs -m:查看共享内存列表
ipcs -s:查看信号量列表
ipcs -q:查看消息队列列表
ipcrm -m shmid:删除编号为shmid的共享内存(还可以用系统接口shmctl()删除)
ipcs -a:删除所有的ipc资源(共享内存,消息队列,信号量)