一、认识进程间通信
1.为什么要有进程间通信
2.进程间通信的内容
3.进程之间如何通信
二、管道
1.管道的概念
2.匿名管道
(1)双进程匿名管道通信
(2)多进程匿名管道通信
3.命名管道
三、共享内存
1.共享内存的概念
2.共享内存相关函数
(1)shmget
(2)ftok
(3)shmctl
(4)shmat
(5)shmdt
(6)用共享内存实现进程间通信
3.共享内存的细节把控
(1)key值的作用
(2)shmid与key的关系
(3)共享内存的优缺点
下讲解进程间通信之前,必须要强调一点:进程具有独立性。
既然它具有独立性,那通信一定会让二者产生联系,那又何谈独立性呢?
进程的独立性是指不同进程的资源都是彼此独立的,但进程不是孤立的。进程与进程间是需要信息交互与状态传递等的通信。比如说,父进程等待子进程查看子进程状态,在子进程完成任务时回收子进程信息。
而且有些程序的运行也需要多个进程进行配合才能完成。
比如说这个指令:cat file | grep 'hello'
它可以查看一个文件中的 "hello",这个时候就需要一个进程专门负责打印文件,另一进程负责进行字符过滤。这种情况就需要多进程协同去完成了。
进程间通信基本有以下内容:
数据传输:一个进程需要将它的数据发送给另一个进程。
资源共享:多个进程之间共享一份资源。
通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止等)。
进程控制:有些进程希望完全控制另一个进程的运行(Debug进程就需要完全控制我们写的代码的进程的运行),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它状态的改变情况。
在进程具有独立性,每个进程的资源都各自独立的大前提下,我们想要让进程之间进行通信,就必须让两个进程看到同一份内容,。操作系统为两个进程提供一个共享的资源,将数据放在这个资源内,多个进程都可以读取,这也是通信最本质的方法。
进程间通信的方式有三种:管道、System V和POSIX IPC。
前两者一般用于一台主机内的进程通信,第三个一般用于跨主机通信。我们主要学习管道和系统V通信。
我们把从一个进程连接到另一个进程的一个数据流称为管道,它是Unix操作系统中最古老的通信方式。数据可以从进程A通过管道单向传递到进程B。
管道的本质是一个文件,进程A把数据写入到文件中,进程B把数据从文件中读取获取数据,而且这两个进程一般是父子进程。
管道分为匿名管道和命名管道,这两种管道通信的方式都会讲解。
实现匿名管道通信需要通过父进程使用管道特定的系统调用,两个进程都以可读可写的方式打开一个内存级文件,并通过fork创建子进程继承原来的文件标识符,根据传输数据的方向父子进程各自关闭自己的读端或者写端进从而形成一条单向的通信通道。由于这个管道文件是没有名字的,所以就叫做匿名管道。
int pipe(int fds[2]);
该函数在unistd.h内,用于创建一个内存级管道文件,调用该函数的进程可以对该文件进行读写操作,创建成功返回0,失败则返回对应的错误码。
int fds[2]表示一个内含两个元素的储存文件标识符的整形数组,其中fd[0]传递读端的文件描述符,fd[1]传递写端的文件描述符。
这是一个父进程发消息给子进程的代码:
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
int main()
{
int fds[2];
int n = pipe(fds);//传数组指针,此时父进程可以读写管道文件
pid_t id = fork();//创建子进程
if(id == 0)//子进程进行读操作
{
close(fds[1]);//子进程关闭写端
while(true)
{
sleep(1);
char buffer[1024];
ssize_t s = read(fds[0], buffer, sizeof(buffer)-1);
//将管道文件的内容读到buffer,s是读到的字节数
if(s > 0)//有内容
{
buffer[s] = '\0';
//C语言字符串才以\0结尾,系统调用不会加\0,在这里加上
cout << "Message getted:" << buffer << " | My pid is " << getpid() << endl;
}
else if(s == 0)//读到文件结尾了
{
cout << "read返回值为0,读到文件结尾" << endl;
break;
}
}
close(fds[0]);//子进程完成任务,关闭读端
exit(0);
}
//父进程进行写操作
close(fds[0]);//父进程关闭读端
const char* str = "i am father";
while(true)
{
sleep(1);
char buffer[1024];
snprintf(buffer, sizeof(buffer), "%s", str);
write(fds[1], buffer, strlen(buffer));
//将字符串打印到buffer中再写入管道文件中
}
close(fds[1]);//父进程完成任务,关闭写端
int status = 0;
int a = waitpid(id ,&status, 0);//获取父进程退出的返回值
assert(a == id);//等待必须成功
cout << "pid:" << a << " | error code:" << (status & 0x7f) << endl;
//打印错误码
return 0;
}
运行结果:
上面的代码运行大致可以用下面的图来表示:
默认进程标识符数组的下标0、1、2分别表示标准输入流、标准输出流、标准错误流。所以fd[0]一般是3,fd[1]一般是4。
当然子进程也可以给父进程发消息。
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
int main()
{
//创建管道文件
int fds[2];
int n = pipe(fds);
assert(n == 0);//返回值为0,管道创建成功,-1失败
//fork创建子进程
pid_t id = fork();
assert(id >= 0);
if(id == 0)
{
//子进程代码执行
//子进程负责写
close(fds[0]);
const char* str = "我是子进程,现在正在和你通信";
int cnt = 0;
while(true)
{
sleep(1);
char buffer[1024];
snprintf(buffer, sizeof(buffer), "Child->parent say:%s[%d][%d]", str, ++cnt, getpid());
write(fds[1], buffer, strlen(buffer));
}
//关闭管道
close(fds[1]);
exit(0);//正常退出
}
//父进程代码执行
//父进程负责读
close(fds[1]);//父进程关闭读端
while(true)
{
sleep(1);
char buffer[1024];
ssize_t s = read(fds[0], buffer, sizeof(buffer)-1);
if(s > 0)
{
buffer[s] = '\0';
cout << "Message getted:" << buffer << " | My pid is " << getpid() << endl;
}
else if(s == 0)//读到文件结尾了
{
cout << "read返回值为0,读到文件结尾" << endl;
break;
}
}
close(fds[0]);
int status = 0;
n = waitpid(id ,&status, 0);
assert(n == id);
cout << "pid:" << n << " | error code:" << (status & 0x7f) << endl;
return 0;
}
编写代码实现功能:父进程通过管道随机给不同子进程分配随机的任务,子进程接受后执行对应任务。
multicom.cc
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
#define NUM 5
typedef void (*fun_t)();
//三种任务的执行函数
void DownloadTask()
{
cout << "pid:"<< getpid() <<"————下载任务\n" << endl;
}
void FlushTask()
{
cout << "pid:"<< getpid() <<"————刷新任务\n" << endl;
}
void IOTask()
{
cout << "pid:"<< getpid() <<"————IO任务\n" << endl;
}
//加载任务的函数
void LoadFunc(vector* pv)
{
assert(pv);
pv->push_back(DownloadTask);
pv->push_back(FlushTask);
pv->push_back(IOTask);
}
//一个进程类的定义
struct EndPoint
{
public:
EndPoint(pid_t subId, int writeFd)
:_subId(subId)
,_writeFd(writeFd)
{
char buffer[1024];//定义一个buffer作为缓冲区
snprintf(buffer ,sizeof(buffer), "process-%d[pid(%d)-fd(%d)]", num++, _subId, _writeFd);
_name = buffer;
}
static int num; //全部进程总数
std::string _name; //进程信息
pid_t _subId; //进程的pid
int _writeFd; //子进程的文件描述符
};
int EndPoint::num = 0;
int GetTaskCode(int readFd);
//创建进程
void CreateProcess(vector& TaskFunc, vector& Process)
{
std::vector DelFd;//储存需要被关闭的读写端
for (int i = 0; i= 0 && pos < TaskFunc.size())
TaskFunc[pos]();//执行任务代码
else if(pos == -1)
break;
}
exit(0);
}
//父进程
close(fd[0]);//关闭读端
Process.push_back(EndPoint(id, fd[1]));
DelFd.push_back(fd[1]);
}
}
//发送任务
void SendTask(int x, EndPoint& process)
{
std::cout << "send task num :"<< x <<" send to " << process._name << std::endl;
int n = write(process._writeFd, &x,sizeof(x));
//通过文件描述符(process.writeFd_就代表是哪个进程)发送(int类型-taskNum就代表几号任务)
assert(n == sizeof(int));
}
//接收任务
int GetTaskCode(int readFd)
{
int code = 0;
ssize_t s = read(readFd, &code, sizeof code);//读取4byte(文件描述符(int类型))
if(s == 4)//s表示读取的字节数,4字节是整形的大小
return code;
else if(s <= 0)
return -1;
else
return 0;
}
void LoadBalance(vector& TaskFunc, vector& Process, int count)
{
bool forever = (count == 0 ? 1 : 0);
int tasknum = TaskFunc.size();
int processnum = Process.size();
while(true)
{
//选择一个进程
int processx = rand() % processnum;
//选择一个任务
int taskx = rand() % tasknum;
//发送任务给进程
SendTask(taskx, Process[processx]);
sleep(1);
if(!forever)
{
--count;
if(count == 0)
break;
}
}
}
void WaitProcess(vector& Process)
{
for(int i = 0; i TaskFunc;//创建一个函数指针数组
LoadFunc(&TaskFunc);//将所有任务函数插入vector
//创建子进程并为父子进程创建好管道
std::vector Process;//创建一个任务数组
CreateProcess(TaskFunc, Process);//创建子进程与管道
int taskcnt = 10;//执行taskcnt次任务,如果为0就表示永远运行
LoadBalance(TaskFunc, Process, taskcnt);//父进程均衡地向子进程发送信息
//等待回收所有子进程
WaitProcess(Process);
return 0;
}
命名管道与匿名管道的原理一致,只不过我们会直接建立一个管道文件,通过这个管道文件进行进程间通信。
语法: mkfifo filename
功能:在当前目录创建和一个管道文件
我输入mkfifo mypipe,此时在目录下就建立了一个管道文件,管道文件的属性字母是p,而且也有高亮。
除了使用指令创建,也可以使用系统调用创建。
int mkfifo(const char *filename,mode_t mode);
在sys/types.h和sys/stat.h中定义,创建一个文件名为filename,初始权限为mode的管道文件。
int unlink(const char *path);
在unistd.h中定义,删除一个路径下的管道文件。
实现一个程序使server可以接收并打印client发来的信息。
comm.hpp
#pragma once
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define NAMED_PIPE "/tmp/mypipe"
bool createfifo(const std::string& path)//创建管道文件
{
int n = mkfifo(path.c_str(), 0666);
if(n == 0)//创建成功返回值为n
{
return true;
}
else
{
std::cout << "error:" << errno << " | error string:" << strerror(errno) << std::endl;
return false;
}
}
void removefifo(const std::string& path)//移除管道文件
{
int n = unlink(path.c_str());//移除管道文件且必须成功
assert(n == 0);
(void)n;
}
clinet.cc
#include "comm.hpp"
using namespace std;
int main()
{
int fd = open(NAMED_PIPE, O_WRONLY);//以只写的方式打开管道文件
if(fd < 0)//必须打开成功
exit(1);
while(true)
{
char buffer[1024];
fgets(buffer, sizeof(buffer), stdin);//从标准输入读取字符串
buffer[strlen(buffer)-1] = '\0';
//在输入字符串的时候还要多打一个回车,我们把回车用\0覆盖掉
ssize_t s = write(fd, buffer, strlen(buffer));//写入buffer的内容
assert(s == strlen(buffer));//写入的内容必须是全部字符
(void)s;
}
close(fd);
return 0;
}
server.cc
#include "comm.hpp"
using namespace std;
int main()
{
bool ret = createfifo(NAMED_PIPE);//创建管道
assert(ret);
(void)ret;
int fd = open(NAMED_PIPE, O_RDONLY);//只读方式打开
if(fd < 0)
exit(1);
char buffer[1024];
while(true)
{
ssize_t s = read(fd, buffer, sizeof(buffer)-1);//少读取一个\0
if(s > 0)
{
buffer[s] = 0;//读取到内容还需要加上\0
cout << buffer << ". is sent to server by client " << endl;
}
else if(s == 0)
{
//都v不到内容就退出
cout << "client quitted,I also need to quit." << endl;
break;
}
else
{
//都不是就出错了
cout << "error string:" << strerror(errno) << endl;
break;
}
}
close(fd);
removefifo(NAMED_PIPE);//移除管道文件
return 0;
}
共享内存是专门设计用于进程间通信(IPC)的,它与两进程地址空间的共享区有关。
我们之前学习过进程地址空间,它是在进程PCB中定义的一个数据结构。进程地址空间认为每一个程序都可以分配4G的空间,这4G空间通过两个地址进行区域划分,其中主要包括内核空间、栈区、共享区、堆区、代码区等。但进程地址空间的地址是虚拟的,进程要想使用空间必须通过页表映射到物理内存。为了节省资源,计算机还出现了写时拷贝的技术以节省空间。
以前c语言中学的malloc是在堆区开辟一块空间,进程PCB会从进程地址空间的堆区通过页表映射到内存开辟一块空间,但是这块空间是不被其他进程所共享的。
共享内存作为一种通信方式,所有关联了这块内存的进程都可以使用它,此时的内存不再是只属于一个进程的。操作系统中一定会同时存在很多的共享内存,它们都有各自的shmid,可以通过shmid对共享内存进行管理。
int shmget(key_t key, size_t size, int shmflg);
头文件:sys/ipc.h、sys/shm.h
功能:创建一块共享内存或者获取已创建共享内存的shmid
参数:key是这个共享内存的标识符,size是共享内存空间的字节大小,shmflg是一个包含九个权限标志的整形,它们的用法和创建文件时的权限标识是一样的,主要使用两个默认选项:IPC_CREAT(如果进程没有创建共享内存就创建并返回shmid;如果进程已经创建共享内存则获取它的shmid),IPC_EXCL(它无法单独使用),IPC_CREAT | IPC_EXCL (如果该进程没有创建共享内存,就创建并返回shmid;如果共享内存已经被创建就返回错误)。
返回值:创建成功返回一个该共享内存段的标识码(一个非负整数),失败返回-1
key_t ftok(const char *pathname, int proj_id);
头文件:sys/types.h、sys/ipc.h
功能:将路径名和项目标识符转换为唯一标识符key返回
参数:pathname是文件路径名 ,proj_id是项目标识符
key的作用:key可以对共享内存进行唯一性识别
返回值:标识某一个共享内存的key值
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
头文件:sys/ipc.h、sys/shm.h
功能:用于控制共享内存
参数:shmid是共享内存的标识码,cmd是将要执行的动作,一般使用三个选项:IPC_SET、IPC_RMID和IPC_INFO,buf是一个指向保存共享内存各种属性的结构体指针。
返回值:成功返回0;失败返回-1
void *shmat(int shmid, const void *shmaddr, int shmflg);
头文件:sys/types.h、sys/shm.h
功能:使共享内存与某个进程关联起来
参数:shmid是共享内存的标识码;shmaddr设置为nullptr就可以,由于系统需要选择一个合适的(未使用的)地址来附加共享内存段,这个传入的地址就是干这个用的,但是我们不知道哪个地址合适,所以传nullptr让系统自己去找就可以了;shmflg表示共享内存的操作权限,默认设为0表示只读就可以了。
返回值:成功返回它附加的共享内存段地址,失败则返回-1
int shmdt(const void *shmaddr);
头文件:sys/types.h、sys/shm.h
功能:取消共享内存与某个进程的关联
参数:shmaddr表示附加共享内存段的地址,也是shmat的返回值。
返回值:成功返回0;失败返回-1
comm.hpp
#include
#include
#include
#include
#include
#include
#include
#include
#define PATHNAME "."
#define PROJ_ID 0x66
//系统共享内存的分配是以4KB为一个单位内存块
#define MAX_SIZE 4096
//int shmget(key_t key, size_t size, int shmflg); ———— 创建共享内存或获取共享内存的shmid
//void *shmat(int shm_id, const void *shm_addr, int shmflg); ———— 链接共享内存和进程
//int shmdt(const void *shmaddr); ———— 除去共享内存与进程的链接
//int shmctl(int shm_id, int command, struct shmid_ds *buf); ———— 删除共享内存
//为共享内存获取key
key_t getkey()
{
//key_t ftok(const char *pathname, int proj_id);
key_t key = ftok(PATHNAME, PROJ_ID);
if(key < 0)
{
std::cout << errno << ":" << strerror(errno) << std::endl;
exit(2);
}
return key;
}
int _getSHM(key_t key, int flags)
{
int shmid = shmget(key, MAX_SIZE, flags);
if(shmid < 0)
{
std::cout << errno << ":" << strerror(errno) << std::endl;
exit(2);
}
return shmid;
}
//当只有IPC_CREAT选项打开时,不管是否已存在该块共享内存,则都返回该共享内存的ID,
int getSHM(key_t key)
{
return _getSHM(key, IPC_CREAT);
}
//当IPC_CREAT | IPC_EXCL时, 如果没有该块共享内存,则创建,并返回共享内存ID。
int creatSHM(key_t key)
{
return _getSHM(key, IPC_CREAT | IPC_EXCL | 0600);
}
void *attach_memroy(int shmid)
{
void* m = shmat(shmid, nullptr, 0);
if((long long)m == -1L)
{
std::cout << "shmat" <
server.cc
#include"comm.hpp"
using namespace std;
int main()
{
//获取key值
key_t key = getkey();
cout << "获取key值:" << key << endl;
//创建共享内存
int shmid = creatSHM(key);
cout << "共享内存的shmid为:" << shmid << endl;
//将共享内存与当前进程进行关联
char* start = (char*)attach_memroy(shmid);
printf("关联成功");
int n = 0;
while(true)
{
printf("message getted:%s\n", start);
struct shmid_ds ds;
shmctl(shmid, IPC_STAT, &ds);
printf("属性:\nsize:%d\ncreator pid:%d\nmy pid:%d\nkey:%d\n", ds.shm_segsz, ds.shm_cpid, getpid(), ds.shm_perm.__key);
sleep(1);
n++;
if(n == 11)
break;
}
//去关联
detach_memroy(start);
sleep(2);
delshm(shmid);
return 0;
}
client.cc
#include"comm.hpp"
using namespace std;
int main()
{
//获取key值
key_t key = getkey();
cout << "获取key值:" << key << endl;
//获取被创建的共享内存的shmid
int shmid = getSHM(key);
cout << "共享内存的shmid为:" << shmid << endl;
//将共享内存与当前进程进行关联
char* start = (char*)attach_memroy(shmid);
cout << "关联成功" << endl;
const char* m = "I am process A,正在和你通信";
int cnt = 0;
while(true)
{
snprintf(start, MAX_SIZE, "%s | pid:%d | message number:%d", m, getpid(), ++cnt);
sleep(1);
if(cnt == 10)
break;
}
//去关联
detach_memroy(start);
return 0;
}
共享内存和malloc、new开辟的内存本质上是一样的,都是普通的内存空间,只不过一个独占而另一个共享。我们每开辟一块空间,操作系统都需要维护它,而只要是资源管理就离不开先描述再组织的方式建立储存内存必要属性的数据结构然后通过一定数据结构管理,在Linux中它是struct shmid_ds。
struct shmid_ds
{
struct ipc_perm shm_perm;/* 操作权限*/
int shm_segsz; /*段的大小(以字节为单位)*/
time_t shm_atime; /*最后一个进程附加到该段的时间*/
time_t shm_dtime; /*最后一个进程离开该段的时间*/
time_t shm_ctime; /*最后一个进程修改该段的时间*/
unsigned short shm_cpid; /*创建该段进程的pid*/
unsigned short shm_lpid; /*在该段上操作的最后1个进程的pid*/
short shm_nattch; /*当前附加到该段的进程的个数*/
unsigned short shm_npages; /*段的大小(以页为单位)*/
unsigned long *shm_pages; /*指向frames->SHMMAX的指针数组*/
struct vm_area_struct *attaches; /*对共享段的描述*/
};
所以我们应当有这样的认识:开辟一块空间的内存占用应当是物理内存空间和共享内存的相关属性。
操作系统管理共享内存属性就是对其数据结构进行增删查改,从而对它对应的物理内存空间进行实时监控管理。
因为操作系统内一定会同时存在许多共享内存,所以凡是被创建共享内存,相关属性结构体中必须有一个能体现出该共享内存的唯一性的数据,这个数据就是key。
key不光是为了保证共享内存的唯一性,也是为了通过key能够让其他进程识别该共享内存,从而进程才能对其进行操作。
打个比方,小刚和小明两个人去饭店吃饭,他们提前定了一个包间。饭店肯定有一个标着同样包间号的房间。他们就能够通过包间号找到该房间,然后在里面吃饭。这里就餐的过程就是进程找寻并操作共享内存的过程,包间号就相当于key。
你可能发现了key并没有出现在上面的结构体中,其实key被封装在struct ipc_perm结构体内。
struct ipc_perm
{
key_t key; 调用shmget()时给出的关键字
uid_t uid; /*共享内存所有者的有效用户ID */
gid_t gid; /* 共享内存所有者所属组的有效组ID*/
uid_t cuid; /* 共享内存创建 者的有效用户ID*/
gid_t cgid; /* 共享内存创建者所属组的有效组ID*/
unsigned short mode; /* Permissions + SHM_DEST和SHM_LOCKED标志*/
unsignedshort seq; /* 序列号*/
};
我们在前面学习了shmid就是标识一块共享内存唯一性的,在功能上key与shmid好像是重复的。而它们的存在一定都是有意义的,那么二者的关系到底是什么呢?
key和shmid的关系就类似文件系统中的fd与inode一样,fd与key是暴露给用户使用的,而shmid与inode被封装在内核数据结构中,它只向用户展示内容而不被用户使用。
这个很像在学校我们用学号标识自己,而在社会上用身份证号。当毕业后学校删除我们的学号时,我们在社会上对个人唯一性的标识也不会受影响,但身份证号被删除就无法在整个社会标识你自己,造成巨大的麻烦。
优点:因为拷贝次数少,共享内存是所有进程间通信方式中速度最快的。在同样的代码下,考虑键盘输入和显示器输出共享内存与管道数据拷贝次数。
管道通信会经过四次拷贝:输入文件->输入缓冲区->管道文件->输出缓冲区->输出文件
而共享内存通信只需要两次拷贝:输入文件->共享内存->输出文件
缺点:共享内存不进行同步和互斥操作,没有对数据进行保护。