目录
管道
管道是Unix中最古⽼的进程间通信的形式
我们把从⼀个进程连接到另⼀个进程的⼀个数据流称为⼀个“管道”
匿名管道
创建一无名管道
#include <unistd.h>
//创建管道,分别为读端与写端,创建成功返回0,失败返回错误代码
int pipe(int fd[2]);
管道本质上其实就是个文件
以读方法和写方法打开这个文件,只要父进程和子进程分别可以访问管道的读端或写端,那么父子进程就可以通过管道进行通信
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<string.h>
int main()
{
int pipe_fd[2];//先读端,后写端
int re=pipe(pipe_fd);
if(re)
{
perror("创建管道失败");
return 0;
}
pid_t pid=fork();
if(pid==-1)
{
perror("创建子进程失败");
return 0;
}
if(pid==0)
{
//此时为子进程,我们要让子进程给父进程发消息,关闭读端
close(pipe_fd[0]);
char buffer[100]="hello world";
write(pipe_fd[1],buffer,strlen(buffer)+1);
close(pipe_fd[1]);
exit(0);
}
//此时为父进程,关闭写端
close(pipe_fd[1]);
char buffer[100];
//memset(buffer,0,sizeof(buffer));
read(pipe_fd[0],buffer,sizeof(buffer));
printf("read:%s\n",buffer);
close(pipe_fd[0]);
return 0;
}
进程池
Channel.hpp
#pragma once
#include<iostream>
#include<unistd.h>
#include<string>
class Channel
{
public:
Channel(int wfd,pid_t who):_wfd(wfd),_who(who)
{
_name="Channel-" + std::to_string(wfd) + "-" + std::to_string(who);
}
void Close()
{
close(_wfd);
}
std::string Name()
{
return _name;
}
void Send(int cmd)//自己指定的指令的编号
{
::write(_wfd,&cmd,sizeof(cmd));
}
pid_t Id()
{
return _who;
}
int wFd()
{
return _wfd;
}
private:
int _wfd;
std::string _name;
pid_t _who;
};
Task.hpp
#pragma once
#include<iostream>
#include<unistd.h>
#include<sys/types.h>
#include<functional>
#include<ctime>
#include<unordered_map>
using task_t=std::function<void()>;
class TaskManger
{
public:
TaskManger()
{
srand(time(nullptr));
//随便写一些编号对应的任务
tasks.push_back([](){std::cout<<"sub process:"<<getpid()<<" 正在执行访问数据库任务"<<std::endl;});
tasks.push_back([](){std::cout<<"sub process:"<<getpid()<<" 正在执行url解析\n"<<std::endl;});
tasks.push_back([](){std::cout<<"sub process:"<<getpid()<<" 正在执行加密任务"<<std::endl;});
tasks.push_back([](){std::cout<<"sub process:"<<getpid()<<" 正在执行数据持久化任务"<<std::endl;});
}
int SelectTask()
{
return rand()%tasks.size();
}
void execute(unsigned long number)
{
if(number>=0&&number<tasks.size())
tasks[number]();
}
private:
std::vector<task_t> tasks;
};
TaskManger TM;
void Worker()
{
while(true)
{
int cmd;
int n=read(0,&cmd,sizeof(cmd));
if(n==sizeof(cmd))
TM.execute(cmd);
else if(n==0)
{
//另一端关闭了写端,即程序退出
std::cout << "pid: " << getpid() << " quit..." << std::endl;
break;
}
}
}
ProcessPool.hpp
#pragma once
#include <iostream>
#include <string>
#include <vector>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <functional>
#include "Task.hpp"
#include "Channel.hpp"
using work_t=std::function<void()>;
enum
{
OK=0,
UsageError,
PipeError,
ForkError
};
class ProcessPool
{
public:
ProcessPool(int n,work_t w)
:processnum(n),work(w)
{
}
int InitProcessPool()
{
for(int i=0;i<processnum;++i)
{
int pipefd[2];
int n=pipe(pipefd);
if(n<0)
return PipeError;
pid_t id=fork();
if(id<0)
return ForkError;
if(id==0)
{
std::cout<<getpid()<<",child close history fd:";
//关闭历史遗留进程fd,因为每增加一个子进程,它都会拥有与父进程的读写端以及与其他子进程的写端
for(auto& c:channels)
{
std::cout<<c.wFd()<<" ";
c.Close();
}
std::cout<<"over"<<std::endl;
::close(pipefd[1]);//关闭对父进程的写端
dup2(pipefd[0],0);
work();//执行work,一直等待父进程的指令,然后执行
::exit(0);//父进程关闭写端,退出
}
//这里为父进程
::close(pipefd[0]);
channels.emplace_back(pipefd[1],id);
}
return OK;
}
void Dispatcher()
{
int who=0;
int num=20;//一次执行20个任务
while(num--)
{
int task=TM.SelectTask();//随机选择一个任务
Channel& curr=channels[who++];
who%=channels.size();
std::cout << "######################" << std::endl;
std::cout << "send " << task << " to " << curr.Name() << ", 任务还剩: " << num << std::endl;
std::cout << "######################" << std::endl;
curr.Send(task);
sleep(1);
}
}
void CleanProcesPool()
{
for(auto& c:channels)//关闭对每一个子进程的写端,子进程会自动退出,然后父进程等待,回收子进程
{
c.Close();
pid_t rid=waitpid(c.Id(),nullptr,0);
if(rid>0)
{
std::cout << "child " << rid << " wait ... success" <<std::endl;
}
}
}
void DebugPrint()
{
for(auto& c:channels)
std::cout<<c.Name()<<std::endl;
}
~ProcessPool()
{
}
private:
std::vector<Channel> channels;
int processnum;
work_t work;
};
main.cc
#include"ProcessPool.hpp"
void Usage(std::string proc)
{
std::cout << "Usage: " << proc << " process-num" << std::endl;
}
int main(int argc,char* argv[])
{
if(argc!=2)
{
Usage(argv[0]);
return UsageError;
}
ProcessPool* pp=new ProcessPool(std::stoi(argv[1]),Worker);
pp->InitProcessPool();
pp->Dispatcher();
pp->CleanProcesPool();
delete pp;
return 0;
}
执行结果
管道读写规则
没有数据可读时
O_NONBLOCK disable:read调⽤阻塞,即进程暂停执⾏,⼀直等到有数据来到为⽌。
O_NONBLOCK enable:read调⽤返回-1,errno值为EAGAIN。
当管道满的时候
O_NONBLOCK disable:write调用阻塞,直到有进程读⾛数据
O_NONBLOCK enable:调用返回-1,errno值为EAGAIN
写端所有文件描述符关闭,则数据读完时,read返回0
读端所有文件描述符关闭,则数据读完时产生信号SIGPIPE,可能导致write进程退出
写入的原子性:要么数据写入完全成功,要不完全失败,不会出现成功一部分的情况
当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性
当要写入的数据量大于PIPE_BUF时,linux将不保证写入的原子性
但是原子性只适用于两个进程共用一个管道,多个进程共用一个管道不保证
管道的特点
通常管道由父进程创建,然后父进程创建子进程,进程间通过管道进行通信(即拥有共同祖先的进程)
进程退出,管道释放,不会像正常文件一样保留,生命周期随进程
内核会对管道操作进行同步与互斥(即不会出现同时读或者同时写导致数据出现问题)
管道是半双工的,数据只能向⼀个方向流动;需要双方通信时,需要建立起两个管道
命名管道
创建
两种创建方式,一是通过指令创建
我们通过mkfifo指令创建了一个名为test_pipe的管道文件
我们也可以通过程序创建
#include<iostream>
#include<sys/stat.h>
int main()
{
umask(0);
mkfifo("test_pipe_1",0644);
return 0;
}
命名管道的打开规则
注意:无论管道是否为空,以下规则都成立
为读打开
O_NONBLOCK disable:阻塞直到有相应进程为写而打开该FIFO
O_NONBLOCK enable:立刻返回成功
为写打开
O_NONBLOCK disable:阻塞直到有相应进程为读而打开该FIFO
O_NONBLOCK enable:立刻返回失败,错误码为ENXIO
接下来我们再通过test_pipe管道进行两个不相关进程通信的测试
测试
写端
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include<fcntl.h>
int main()
{
int fd=open("test_pipe",O_WRONLY);
write(fd,"hello",5);
return 0;
}
读端
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include<fcntl.h>
#include<stdio.h>
int main()
{
int fd=open("test_pipe",O_RDONLY);
char buffer[100];
read(fd,buffer,sizeof(buffer));
printf("receive:%s\n",buffer);
return 0;
}
结果
上文也提到过了,如果不设置O_NONBLOCK(即非阻塞时,只有读写都打开才能顺利写入或者读取,否则就会阻塞在那里)
匿名管道与命名管道的比较
共同点
1.均为FIFO(先进先出)
2.一个管道只能提供给两个进程之间进行通信
不同点
匿名管道
1.无法长存数据,当读写端均关闭时,管道就会被销毁,连带其中的数据
2.没有文件名,只有存在血缘关系的进程才能使用
3.生命周期随进程,当没有进程使用时就会被删除
命名管道
1.可以长存数据,即使读端或写端都不在了也会保存数据直到有新进程处理掉这些数据或者管道本身被删除
2.拥有文件名,这意味着即使是两个毫不相关的进程也可以使用命名管道也能进行通信
3.生命周期看它什么时候被手动删除
使用
命名管道
更适合需要不相关进程进行通信的任务时使用,例如服务端与客户端
匿名管道
更适合进行任务派发时使用,父进程派发任务,交给创建出来的子进程进行处理任务
systemV共享内存
共享内存区是最快的IPC形式。⼀旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到内核,换句话说是进程不再通过执行进⼊内核的系统调用来传递彼此的数据
简单来说就是多个进程可以通过映射的方式访问同一块数据,这样有了共同的空间,进程间就可以很方便的进行通信了
函数
//创建共享内存的函数:shmget
//即share memory get
int shmget(key_t key, size_t size, int shmflg);
//key:这个共享内存段名字
//size:共享内存⼤⼩
//shmflg:由九个权限标志构成,它们的⽤法和创建⽂件时使⽤的mode模式标志是⼀样的
//取值为IPC_CREAT:共享内存不存在,创建并返回;共享内存已存在,获取并返回。
//取值为IPC_CREAT | IPC_EXCL:共享内存不存在,创建并返回;共享内存已存在,出错返回。
//返回值:成功返回⼀个⾮负整数,即该共享内存段的标识码;失败返回-1
//将共享内存段连接到进程地址空间的函数:shmat
//即 share memory at
//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,表⽰连接操作⽤来只读共享内存
//将共享内存段与当前进程脱离函数:shmdt
//int shmdt(const void *shmaddr);
//shmaddr: 由shmat所返回的指针
//返回值:成功返回0;失败返回-1
//将共享内存段与当前进程脱离不等于删除共享内存段
//⽤于控制共享内存函数:shmctl
//即 share memory control
//int shmctl(int shmid, int cmd, struct shmid_ds *buf);
//shmid:由shmget返回的共享内存标识码
//cmd:将要采取的动作(有三个可取值)
//buf:指向⼀个保存着共享内存的模式状态和访问权限的数据结构
返回值:成功返回0;失败返回-1
//生成键值System V IPC键值函数ftok
//key_t ftok(const char* pathname,int proj_id);
//pathname:一个指向实际路径名的指针,例如"."
//proj_id:一个项目标识符,主要用来区分同一个pathname底下不同的键值,一般为一个字符大小
//成功时返回一个非负键值
//失败是返回-1并设置错误码
总结共享内存
1.先使用ftok根据实际路径与标识符创建一个唯一的键值
2.将键值 key交给shmget来创建或者是获取一个共享内存的标识码shmid
3.将shmid交给shmat来为进程设置一个指针用来访问共享内存区域
4.当前进程不再需要该共享内存时使用shmdt来解除与该共享内存的关系
5.所有进程不再需要该共享内存时调用shmctl来删除共享内存
案例
comm.hpp
#pragma once
#include<stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include<unistd.h>
#define Path "."
#define PROJ_ID 0x01
int createShm(int size);
int destroyShm(int shmid);
int getShm(int size);
#include "comm.hpp"
//创建共享内存
static int commShm(int size, int flags)
{
key_t key = ftok(Path, PROJ_ID);
if (key < 0)
{
perror("ftok");
return -1;
}
int shmid = 0;
if ((shmid = shmget(key, size, flags)) < 0)
{
perror("shmget");
return -2;
}
return shmid;
}
int createShm(int size)//要求创建
{
return commShm(size,IPC_CREAT|IPC_EXCL|0666);//创建共享内存,有则错误返回,设置权限
}
int destroyShm(int shmid)
{
int n=shmctl(shmid,IPC_RMID,nullptr);
if(n<0)
{
perror("destroyShm");
return -1;
}
return 0;
}
int getShm(int size)//要求获取
{
return commShm(size, IPC_CREAT);
}
client.cc
#include "comm.hpp"
int main()
{
int shmid = getShm(4096);
sleep(1);
char *addr = (char*)shmat(shmid, NULL, 0);
sleep(2);
int i = 0;
while (i < 26)
{
addr[i] = 'A' + i;
i++;
addr[i] = 0;
sleep(1);
}
shmdt(addr);
sleep(2);
return 0;
}
server.cc
#include "comm.hpp"
int main()
{
int shmid = createShm(4096);//创建共享内存
char *addr = (char*)shmat(shmid, NULL, 0);//连接
sleep(2);//等待两秒
int i = 0;
while (i++ < 26)
{
printf("client# %s\n", addr);//从共享内存中取值输出
sleep(1);
}
shmdt(addr);//分离
sleep(2);
destroyShm(shmid);//删除
return 0;
}
结果
查看共享内存
再次启动上文的客户端后我们可以使用ipcs -m 命令来查看创建的共享内存,包括键值和大小