数据传输:一个进程需要将它的数据发送给另一个进程
资源共享:多个进程之间共享同样的资源。
通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止
时要通知父进程)。
进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另
一个进程的所有陷入和异常,并能够及时知道它的状态改变
管道(古老的通信手段)
System V进程间通信(聚焦在本地)
POSIX进程间通信(跨主机间通信)
管道是Unix中最古老的进程间通信的形式。
我们把从一个进程连接到另一个进程的一个数据流称为一个“管道“
头文件:#include
功能:创建一无名管道
原型
int pipe(int fd[2]);
参数
fd:文件描述符数组,其中fd[0]表示读端, fd[1]表示写端
返回值:成功返回0,失败返回错误代码
看待管道,就如同看待文件一样!管道的使用和文件一致,这体现了Linux下一切皆文件的思想。
#include
#include
#include
#include
#include
#include
using namespace std;
int main()
{
//第一步,打开读写端
int fds[2];
int n = pipe(fds);
assert(n==0);
//创建子进程
pid_t id = fork();
assert(id >= 0);
if(id == 0)
{
//子进程写,关闭读端
close(fds[0]);
//子进程写入
int cnt = 0;
while(1)
{
char buffer[1024];
snprintf(buffer,sizeof buffer,"我是子进程,向父进程写入:%d id:%d",cnt++,getpid());
//通过write写入
write(fds[1],buffer,sizeof buffer);
sleep(1);
//break;
}
//子进程关闭写端
close(fds[1]);
cout<<"子进程关闭写端"<0)
{
buffer[s] = 0;
//向屏幕输出
cout<<"父进程接受信号:"<
我们可以通过上述代码控制读写端,就会出现下面几种情况:
当没有数据可读时
O_NONBLOCK disable:read调用阻塞,即进程暂停执行,一直等到有数据来到为止。
O_NONBLOCK enable:read调用返回-1,errno值为EAGAIN。
当管道满的时候
O_NONBLOCK disable: write调用阻塞,直到有进程读走数据
O_NONBLOCK enable:调用返回-1,errno值为EAGAIN
如果所有管道写端对应的文件描述符被关闭,则read返回0
如果所有管道读端对应的文件描述符被关闭,则write操作会产生信号SIGPIPE,进而可能导致write进程
退出
当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性。
当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性。
只能用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信;通常,一个管道由一个进程创
建,然后该进程调用fork,此后父、子进程之间就可应用该管道。
管道提供流式服务
一般而言,进程退出,管道释放,所以管道的生命周期随进程
一般而言,内核会对管道操作进行同步与互斥
管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道
如果我们刚刚只是设计了父子进程之间的通信,我们可以往更深层次去思考,我们可以通过父进程不断的创建子进程,然后控制子进程来完成相关的任务,这样不就是一个进程池了吗?
思路:1.我们可以先把任务写好,这里就简单的看到即可,不是重点。
2.创建子进程,然后维护好父子进程之间的fd的联系。
3.父进程分配任务,这里我们采用随机分配的思想,把任务随机的分配给子进程。
4.子进程完成任务,父进程关闭写端,子进程完成父进程分配的任务后退出
5.父进程等待成功子进程。
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
#define ProcessNum 4//创建子进程个数
typedef void(*func)();
#define makeseed() srand((unsigned long)time(nullptr)^getpid()^12494%1234)//使随机数更加的随机
//创建多个任务,模拟一下
void FuncTask1()
{
cout<& task)
{
task.push_back(FuncTask1);
task.push_back(FuncTask2);
task.push_back(FuncTask3);
}
//下面就是进程间通信部分
class subEp
{
public:
subEp(pid_t subid ,int writefd)
:_subid(subid),_writefd(writefd)
{
char name[1024];
snprintf(name,sizeof name,"process- %d [fd:%d ,id:%d]",_num++,_writefd,_subid);
_name = name;
}
public:
static int _num;//子进程个数
pid_t _subid;
string _name;//每个子进程的名字
int _writefd;//父进程给子进程发任务所需要的fd
};
int subEp::_num = 0;
int RecvTask(int fd)
{
int code;
int s = read(fd,&code,sizeof code);//接受父进程传过来的任务码
if(s == 4) return code;
else if(s <= 0) return -1;
else return 0;
}
void CreateProcess(vector& subs,vector& task)
{
for(int i = 0;i= 0);
if(id == 0)
{
//子进程接受任务
close(fds[1]);
while(1)
{
//接受命令码,并完成任务
int commandCode = RecvTask(fds[0]);
if(commandCode >= 0 && commandCode"<& task,vector& subs,int taskcnt)
{
int tasknum = task.size();
int subnum = subs.size();
//判断是否是永久进行
bool forever = (taskcnt == 0? true:false);
while(1)
{
//生成任务idx
int taskidx = rand()%tasknum;
//生成子进程idx
int processidx = rand()%subnum;
//传送任务
sendtask(subs[processidx],taskidx);
sleep(1);//让子进程完成任务
if(!forever)
{
--taskcnt;
if(taskcnt == 0)
{
break;
}
}
}
for(int i = 0; i < subnum; i++) close(subs[i]._writefd);//关闭写端
}
void WaitProcess(vector& subs)
{
for(int i = 0;i task;
//安排子进程需要完成的任务
LoadTask(task);
//创建子进程,并进行管理
vector subs;
CreateProcess(subs,task);
//父进程控制子进程,向子进程发送命令码
int taskcnt = 3;//发送任务的次数
ControlProcess(task,subs,taskcnt);
//回收子进程
WaitProcess(subs);
return 0;
}
这里虽然也能运行起来,但是其实这段代码还是有bug的,虽然这个bug对运行结果没什么影响。但是要了解:
在 创建子进程的时候,父进程创建子进程发生写实拷贝,所以后面的子进程也会有前面子进程的写端(但是这里并不影响程序的正常运行,因为后面的子进程不会向前面子进程发信息)
因为管道也是文件,所以按照上面的思路,管道的关闭应该是后创建的先关闭。就像栈一样。
那么我们该如何解决呢?
我们可以通过在创建子进程的时候保存父进程的写端,然后每次创建子进程的时候我们就把前面的子进程写实拷贝打开的写端关闭即可
具体代码:
void CreateProcess(vector& subs,vector& task)
{
vector deletefd;//这里是为了防止创建子进程的时候发生写时拷贝时
//导致后面的子进程也有连接到前面的子进程的写端
for(int i = 0;i= 0);
if(id == 0)
{
for(int i = 0;i= 0 && commandCode
管道应用的一个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信。
如果我们想在不相关的进程之间交换数据,可以使用FIFO文件来做这项工作,它经常被称为命名管道。
命名管道是一种特殊类型的文件
int mkfifo(const char *filename,mode_t mode);
匿名管道由pipe函数创建并打开。
命名管道由mkfifo函数创建,打开用open
FIFO(命名管道)与pipe(匿名管道)之间唯一的区别在它们创建与打开的方式不同,一但这些工作完
成之后,它们具有相同的语义。
假设我们是用户和服务者进行进程间通信,那么我们就应该要有如下过程:
首先就是服务者先创建好管道,然后用户进行发送,服务者接受到信号之后就可以完成相应的事情,然后关闭管道即可
这是comm.hpp里的代码:
#pragma once
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
#define NAME_PIPE "/home/liang/learning13/name_pipe/mypipe"
bool creat_pipe(const string& path)
{
umask(0);
int k = mkfifo(path.c_str(),0600);
//assert(k == 0);
cout<
服务者的代码server.cpp:
#include "comm.hpp"
int main()
{
//首先创建命名管道
bool n = creat_pipe(NAME_PIPE);
assert(n);
//准备接受信号
cout<<"server begin";
int rfd = open(NAME_PIPE,O_RDONLY);
assert(rfd>0);
cout<<"server end";
char buffer[1024];
while(true)
{
//接受信息
ssize_t s = read(rfd,buffer,sizeof(buffer)-1);
if(s > 0)
{
//接受到信息了
buffer[s] = 0;//buffer中没有拷贝\0
cout<<"client -> server"<
这是用户的代码:client.cpp:
#include "comm.hpp"
int main()
{
//发送信号
cout<<"client发送信号"<0);
char buffer[1024];
while(true)
{
//从屏幕中得到信息,打印到buffer中
cout<<"please say >>";
fgets(buffer,sizeof(buffer),stdin);
if(strlen(buffer)>0)
{
//把最后一个字符改成\0,屏幕输入默认加的是\n,所以要更新下
buffer[strlen(buffer)-1] = 0;
}
//写入管道中
ssize_t s = write(wfd,buffer,strlen(buffer));
assert(s == strlen(buffer));
(void)s;
}
close(wfd);
return 0;
}
共享内存区是最快的IPC形式。一旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到
内核,换句话说是进程不再通过执行进入内核的系统调用来传递彼此的数据
这是OS暴露给用户级的部分的数据结构,我们可以通过man手册,man shmctl 来获得
shmget函数:
功能:用来创建共享内存
原型
int shmget(key_t key, size_t size, int shmflg);
参数
key:这个共享内存段名字
size:共享内存大小
shmflg:由九个权限标志构成,它们的用法和创建文件时使用的mode模式标志是一样的
返回值:成功返回一个非负整数,即该共享内存段的标识码;失败返回-1
shmat函数:
功能:将共享内存段连接到进程地址空间
原型
void *shmat(int shmid, const void *shmaddr, int shmflg);
参数
shmid: 共享内存标识
shmaddr:指定连接的地址(一般我们不关心,直接使用nullptr)
shmflg:它的两个可能取值是SHM_RND和SHM_RDONLY(一般直接用0)
返回值:成功返回一个指针,指向共享内存第一个节;失败返回-1
shmdt函数:
功能:将共享内存段与当前进程脱离
原型
int shmdt(const void *shmaddr);
参数
shmaddr: 由shmat所返回的指针
返回值:成功返回0;失败返回-1
注意:将共享内存段与当前进程脱离不等于删除共享内存段
shmctl函数:
功能:用于控制共享内存
原型
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数
shmid:由shmget返回的共享内存标识码
cmd:将要采取的动作(有三个可取值)
buf:指向一个保存着共享内存的模式状态和访问权限的数据结构
返回值:成功返回0;失败返回-1
comm.hpp:
#pragma once
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
#define PATH_NAME "."
#define PROJECT_ID 0x66
#define SIZE 4096 //比较合理,效率高
key_t getkey(const string& path)
{
key_t k = ftok(path.c_str(),PROJECT_ID);
if(k == -1)
{
cerr<
shm_client:
#include "comm.hpp"
int main()
{
key_t k = getkey(PATH_NAME);
//创建共享内存
int shmid = getshm(k);
//关联
char* start = (char*)shmattach(shmid);
//通信
const char* str = "I am client say to server";
int cnt = 1;
int id = getpid();
while(1)
{
sleep(1);
snprintf(start,SIZE,"%s : id:%d 编号:%d",str,id,cnt++);
}
//去关联
detachshm(start);
return 0;
}
shm_server:
#include "comm.hpp"
int main()
{
//服务端先打开共享内存
//获取k
key_t k = getkey(PATH_NAME);
assert(k != -1);
//创建共享内存
int shmid = creatshm(k);
assert(shmid != -1);
//进行共享内存的关联
char* start = (char*)shmattach(shmid);
//使用共享内存进行进程间通信
while(1)
{
//我们可以打印出来其中的属性
printf("client say: %s\n",start);
struct shmid_ds ds;
shmctl(shmid,IPC_STAT,&ds);
printf("获得属性: 大小:%d,id:%d ,进程id:%d\n",ds.shm_segsz,ds.shm_cpid,getpid());
sleep(1);
}
//去关联
detachshm(start);
//关闭共享内存
delshm(shmid);
return 0;
}
在这里我们需要注意的是如果我们没有释放共享内存,那么共享内存就不会释放,也就是说共享内存不会随着进程的结束而释放,这是操作系统的资源,所以之后当系统重新启动的时候才会释放。
如果我们运行失败,有可能是你上次已经生成了那个文件,所以我们可以通过ipcs -m来查看共享内存的资源,如果想查看消息队列,就改成-q 如果想查看信号量,就改成-s
如果删除ipc的资源,我们就可以使用指令ipcrm -m即可
共享内存的缺点:
不给我们进行同步和互斥的操作,这样就没有办法对数据进行保护,也就是说在client发送信息的同时可能server也在接受信息,这样信息可能就不完整了。
为什么共享内存是最快的ipc呢,因为当我们往共享内存发送数据的时候不需要进行多次的拷贝,而是直接发送到共享内存中,对于管道来说就需要拷贝数据到临时区域,然后再拷贝到文件描述符中。
消息队列提供了一个从一个进程向另外一个进程发送一块数据的方法
每个数据块都被认为是有一个类型,接收者进程接收的数据块可以有不同的类型值
特性方面
IPC资源必须删除,否则不会自动清除,除非重启,所以system V IPC资源的生命周期随内核
消息队列两方都可以往队列中写,如果我们想要读到对方的数据,我们就可以使用标记,可以就加一个type来标记是谁送到队列中的。
我们可以通过man手册进行这些函数的学习,其实它们和共享内存的使用方式几乎相同
对于我们C++中的多态其实也是借鉴这里的思路来设计的。
由于各进程要求共享资源,而且有些资源需要互斥使用,因此各进程间竞争使用这些资源,进程的这种
关系为进程的互斥
系统中某些资源一次只允许一个进程使用,称这样的资源为临界资源或互斥资源。
在进程中涉及到互斥资源的程序段叫临界区
特性方面
IPC资源必须删除,否则不会自动清除,除非重启,所以system V IPC资源的生命周期随内核
要保护我们共享内存的资源我们就可以使用同步与互斥的方法来解决:
我们要么就把数据全部输送完,要么就不输入,体现原子性;
信号量的本质就是一个计数器,来计数公共资源的多少的问题的,和我们现实生活中的买票很像,我们如果想要占据电影院的座位(资源),我们就必须要买票,进行预订,而我们订了票其实就是让计数器count--(P操作),释放资源就是让count++一下(V操作)
而PV操作体现了原子性。为什么呢?因为计数器其实是一个公共的资源,而这份资源是需要被保护起来的,所以我们在操作信号量的时候要体现原子性。
可以通过以上3个函数进行学习,其实和共享内存十分相似,有着多态的思想。