【Linux】进程间通信

1.进程间通信的目的

进程具有独立性,因此进程间想要通信,成本会非常高,但有时候又需要多个进程协同处理一件事,所以进程间通信是必不可少的

2.管道

从一个进程连接到另一个进程的一个数据流称为一个“管道”

2.1 匿名管道

显示器是缺乏管道控制的,所以父子进程在向显示写入的时候是无序的,而管道是自带访问控制机制的,如果管道内部没有数据,reader就必须阻塞等待;如果管道内部被写满了,writer就必须阻塞等待

代码:

#include<iostream>
#include<unistd.h>
#include<cstring>
#include<sys/types.h>
#include<sys/wait.h>
using namespace std;
int main()
{//1.创建管道int pipefd[2] = {0};if(pipe(pipefd) != 0)//创建一个管道成功返回0{cerr<<"pipe error\n"<<endl;return 1;}//cout<<pipefd[0]<<" "<<pipefd[1]<<endl;  pipefd[0] 是读端的文件描述符  pipefd[1] 是写端的文件描述符//2.创建子进程pid_t id = fork();if(id < 0){cerr<<"fork error" <<endl;return 2;}else if(id == 0){//child//子进程读取数据,关闭写端close(pipefd[1]);char buffer[1024];while(true){//清空缓冲区memset(buffer,0,sizeof(buffer));ssize_t s = read(pipefd[0],buffer,sizeof(buffer) - 1);//返回读到字节的个数if(s > 0){//读取成功buffer[s] = '\0';cout<<"子进程收到消息,内容是:"<<buffer<<endl;}else if(s == 0){cout<<"父进程写完了,我也退出了"<<endl;break;}else{}}close(pipefd[0]);exit(0);}else{//parent//父进程来进行写入,关闭读端close(pipefd[0]);const char *msg = "你好子进程,我是父进程,这次发送的信息编号是:";int cnt = 0;while(cnt < 5){sleep(1);char sendBuffer[1024];sprintf(sendBuffer, "%s : %d",msg, cnt);write(pipefd[1],sendBuffer,strlen(sendBuffer));cnt++;}close(pipefd[1]);cout<<"父进程写完了"<<endl;}pid_t res = waitpid(id,nullptr,0);if(res > 0){cout<<"等待子进程成功"<<endl;}return 0;
}

进程池:通过父进程控制一批子进程

#include<iostream>
#include<unistd.h>
#include<cstring>
#include<sys/types.h>
#include<sys/wait.h>
#include<vector>
#include<ctime>
#include<unordered_map>
#include<cassert>
#include<stdlib.h>using namespace std;typedef void (*functor)();//函数指针
vector<functor> functors;//方法集合
unordered_map<uint32_t,string> info; //uint32_t = unsigned intvoid f1()
{cout<<"这是一个处理日志的任务,执行进程ID ["<<getpid()<<"] 执行时间是 [" << time(nullptr) << "]\n"<<endl;
}
void f2()
{cout<<"这是一个备份数据任务,执行进程ID ["<<getpid()<<"] 执行时间是 [" << time(nullptr) << "]\n"<<endl;
}
void f3()
{cout<<"这是一个处理网络链接的任务,执行进程ID ["<<getpid()<<"] 执行时间是 [" << time(nullptr) << "]\n"<<endl;
}
void loadFunctor()
{info.insert({functors.size(),"处理日志的任务"});functors.push_back(f1);info.insert({functors.size(),"备份数据的任务"});functors.push_back(f2);info.insert({functors.size(),"处理网络链接的任务"});functors.push_back(f3);
}//pair<进程pid,进程对应的管道写端fd>
typedef pair<int32_t,int32_t> elem;
int processNum = 5;
void work(int blockFd)
{cout<<"进程["<<getpid()<<"]"<<"开始工作"<<endl;while(true){//a.阻塞等待 b.获取任务信息uint32_t operatorCode = 0;ssize_t s = read(blockFd,&operatorCode,sizeof(uint32_t));if(s == 0) break;assert(s == sizeof(uint32_t));//读到的不是四个字节报错(void)s;//c.处理任务if(operatorCode < functors.size()) functors[operatorCode]();}cout<<"进程["<<getpid()<<"]"<<"结束工作"<<endl;
}
void blanceSendTask(const vector<elem> &processFds)
{srand((long long)time(nullptr));while(true){sleep(1);//随机选择一个子进程//较为均匀的将任务给所有的子进程 ---- 负载均衡uint32_t pick = rand() % processFds.size();//选择一个任务uint32_t task = rand() % functors.size();//把任务给一个指定的进程write(processFds[pick].second,&task,sizeof(task));//打印对应的提示信息cout<<"父进程指派任务->"<<info[task]<<"给进程:"<<processFds[pick].first<<"编号:"<<pick<<endl;}
}
int main()
{loadFunctor();vector<elem> assignMap;//创建processNum个进程for(int i = 0;i < processNum; i++){//定义保存管道fd的对象int pipefd[2] = {0};//创建管道pipe(pipefd);//创建子进程pid_t id = fork();if(id == 0){//子进程读取,r->pipefd[0]close(pipefd[1]);//子进程执行work(pipefd[0]);close(pipefd[0]);exit(0);}//父进程做的事情close(pipefd[0]);elem e(id,pipefd[1]);assignMap.push_back(e);}cout<<"create all processs success!"<<endl;//父进程派发任务blanceSendTask(assignMap);//回收资源for(int i = 0; i < processNum;i++){if(waitpid(assignMap[i].first,nullptr,0) > 0){cout << "wait for: pid="<<assignMap[i].first<<"wait success!"<<"number:"<<i<<'\n';close(assignMap[i].second);}}return 0;   
}

管道的特征

1、管道只能用来进行具有血缘关系的进程之间,进行进程间通信,常用于父子通信

2、管道只能单向通信

3、管道自带同步机制(pipe满,writer等;pipe空,reader等)

4、管道是面向字节流的  ---- 先写的字符一定是先被读取的,没有格式边界,需要用户规定读多少字节

5、管道的生命周期   ---- 随进程

2.2 命名管道

继承了匿名管道的所有特性,并且允许两个没有血缘关系的进程进行通信

匿名管道是通过子进程继承父进程实现的,而命名管道是通过不同进程访问同一文件(fifo文件)实现的

//comm
#pragma once#include<iostream>
#include<cstdio>
#include<cstring>
#include<string>
#include<cerrno>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>#define IPC_PATH "./.fifo"//读取 serverFifo
#include "comm.h"
using namespace std;
#define NUM 1024int main()
{umask(0);if(mkfifo(IPC_PATH,0600) != 0)//成功返回0{cerr<<"mkfifo error"<<endl;return 1;}int pipeFd = open(IPC_PATH,O_RDONLY);if(pipeFd < 0){cerr<<"open fifo error"<<endl;return 2;}char buffer[NUM];while(true){ssize_t s = read(pipeFd,buffer,sizeof(buffer)-1);if(s > 0){buffer[s] = '\0';cout<<"客户端->服务器#"<<buffer<<endl;}else if(s == 0){cout<<"客户端退出了,我也退出吧"<<endl;break;}else{cout<<"read: "<<strerror(errno)<<endl;break;}}close(pipeFd);cout<<"服务器端退出"<<endl;unlink(IPC_PATH);//删除创建的.fifo文件return 0;
}//写入 clientFifo
#include "comm.h"
using namespace std;
int main()
{int pipeFd = open(IPC_PATH,O_WRONLY);if(pipeFd < 0){cerr<<"open:"<<strerror(errno)<<endl;return 1;}
#define NUM 1024char line[NUM];while(true){cout<<"请输入你的信息#";fflush(stdout);memset(line, 0, sizeof(line));if(fgets(line,sizeof(line),stdin) != nullptr){line[strlen(line)-1] = '\0'; //去掉输入时的回车write(pipeFd,line,strlen(line));}else{break;}}close(pipeFd);cout<<"客户端退出"<<endl;return 0;}

3.共享内存

创建共享内存 shmget

key可以自己设置人任意值,只要保证它的唯一性就好,但是一般都是用ftok()来进行设置

删除共享内存 shmctl

shmctl(shmid, IPC_RMID,nullptr)

使用共享内存 shmat 成功返回共享内存的地址,失败返回-1

取消关联 shmdt                                                                                                                           

ipc 命令:

ipcs -m : 显示当前用户创建的共享内存

ipcrm -m + shmid: 删除共享内存

共享内存属于双方的用户空间(堆、栈),可以直接访问共享内存,不用使用系统接口,但是没有任何访问控制,不安全

用管道来对共享内存增加访问控制

//comm
#pragma once
#include<iostream>
#include<cstring>
#include<cstdlib>
#include<cerrno>
#include<cassert>
#include<unistd.h>
#include<sys/types.h>
#include<sys/ipc.h>
#include"Log.hpp"
#include<sys/stat.h>
#include<fcntl.h>
#include<string>
#include<unistd.h>
#include<sys/shm.h>#define PATH_NAME "/home/ldx/code/2022_11_10"
#define PROJ_ID 0x14
#define MEM_SIZE 4096
#define  FIFO_FILE ".fifo"key_t CreateKey()
{key_t key = ftok(PATH_NAME,PROJ_ID);if(key < 0){std::cerr << "ftok:" << strerror(errno) << std::endl;exit(1);}return key;
}
void CreateFifo()
{umask(0);if(mkfifo(FIFO_FILE,0666) < 0) //创造管道文件 “.fifo”{Log() << strerror(errno) << '\n';exit(2);}
}#define READER O_RDONLY
#define WRITER O_WRONLYint Open(const std::string& filename,int flags)
{return open(filename.c_str(),flags);
}int Wait(int fd)
{uint32_t values = 0;ssize_t s = read(fd, &values, sizeof(values));return s;
}int Signal(int fd)
{uint32_t cmd = 1;write(fd, &cmd, sizeof(cmd));
}int Close(int fd, const std::string filename)
{close(fd);unlink(filename.c_str());
}//Log
#pragma once#include<iostream>
#include<ctime>
std::ostream& Log()
{std::cout<<"for debuge |" <<"timestamp"<<(uint64_t)time(nullptr)<<"|";return std::cout;
}//Cli
#include "Comm.hpp"
#include "Log.hpp"#include <cstdio>
#include <unistd.h>using namespace std;
// 充当使用共享内存的角色
int main()
{int fd = Open(FIFO_FILE, WRITER);// 创建相同的key值key_t key = CreateKey();Log() << "key: " << key << "\n";// 获取共享内存int shmid = shmget(key, MEM_SIZE, IPC_CREAT);if (shmid < 0){Log() << "shmget: " << strerror(errno) << "\n";return 2;}// 挂接char *str = (char*)shmat(shmid, nullptr, 0);while(true){printf("Please Enter# ");fflush(stdout);ssize_t s = read(0, str, MEM_SIZE);if(s > 0){str[s] = '\0';}Signal(fd);//标记写完了}// 去关联shmdt(str);return 0;
}//Ser
#include "Comm.hpp"
#include "Log.hpp"#include <unistd.h>using namespace std;// 我想创建全新的共享内存
const int flags = IPC_CREAT | IPC_EXCL;// 充当使用共享内存的角色
int main()
{CreateFifo();int fd = Open(FIFO_FILE, READER);assert(fd >= 0);key_t key = CreateKey();Log() << "key: " << key << "\n";Log() << "create share memory begin\n";int shmid = shmget(key, MEM_SIZE, flags | 0666);if (shmid < 0){Log() << "shmget: " << strerror(errno) << "\n";return 2;}Log() << "create shm success, shmid: " << shmid << "\n";// 1. 将共享内存和自己的进程产生关联attachchar *str = (char *)shmat(shmid, nullptr, 0);Log() << "attach shm : " << shmid << " success\n";// 用它while(true){// 让读端进行等待if(Wait(fd) <= 0) break; printf("%s\n", str);sleep(1);}// 2. 去关联shmdt(str);Log() << "detach shm : " << shmid << " success\n";// sleep(5);// 删它shmctl(shmid, IPC_RMID, nullptr);Log() << "delete shm : " << shmid << " success\n";Close(fd, FIFO_FILE);// sleep(5);return 0;
}

可以被多个程序看到的资源叫临界资源,访问临界资源的代码叫临界区 

信号量就是一个计数器,这个计算器对应的操作是原子的

信号量对应的操作:

sem:-- ,申请资源:P

sem:++,释放资源,V