目录
前言:
什么是进程间通信?顾名思义,就是进程之间的通信。
进程间通信介绍:
进程间通信的目的:
- 数据传输:一个进程需要将它的数据发送给另一个进程
- 资源共享:多个进程之间共享同样的资源。
- 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
- 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。
进程间通信发展
管道
System V进程间通信
POSIX进程间通信
进程间通信分类
管道
匿名管道pipe
命名管道
System V IPC
System V 消息队列
System V 共享内存(本文主讲)
System V 信号量
POSIX IPC(本文不讲)
消息队列
共享内存
信号量
互斥量
条件变量
读写锁
管道:
单通道,将一个进程与另外一个进程进行链接
匿名管道:
#include <unistd.h>
功能:创建一无名管道
原型
int pipe(int fd[2]);
参数
fd:文件描述符数组,其中fd[0]表示读端, fd[1]表示写端
返回值:成功返回0,失败返回错误代码
从父子进程间来理解管道通信:
父子进程都具有文件描述符数组。在通信的时候,只需要关闭其对应不要的文件描述符即可!
实例:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <unistd.h>
int main()
{
int pipefd[2] = {0}; //文件描述符数组
int n = pipe(pipefd); //创建匿名管道
assert(n != -1) //保险起见,加一个断言。
(void)n;
}
此时,匿名管道就已经创建好了。不过匿名管道的通信,只适用于父子间的通信。
此时再创建子进程,两者就可以相互通信了。
pid_t id = fork();
if(id == 0) //子进程
{
//关闭写文件
close(pipefd[1]);
while(true)
{
//设立缓冲区
char buffer[128];
//读取信息
size_t size = read(pipefd[0],buffer,sizeof(buffer));
buffer[size] = 0; //以 /0结尾
//将读取到的信息 进行打印
printf("%s\n",buffer);
}
}
//父进程
close(pipefd[0]);
while(true)
{
char buffer[1024];
cin>>buffer;
write(pipefd[1],buffer,strlen(buffer));
}
运行结果:
命名管道
匿名管道应用的一个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信
如果我们想在不相关的进程之间交换数据,可以使用FIFO文件来做这项工作,它经常被称为命名管道。
命名管道可以从命令行上创建,命令行方法是使用下面这个命令:
mkfifo filename
命名管道也可以从程序里创建,相关函数有:
int mkfifo(const char *filename,mode_t mode);
匿名管道与命名管道的区别:
匿名管道由pipe函数创建并打开。
命名管道由mkfifo函数创建,打开用open
FIFO(命名管道)与pipe(匿名管道)之间唯一的区别在它们创建与打开的方式不同,一但这些工作完成之后,它们具有相同的语义。
实例:
进程1:
int main()
{
// 1.创建管道文件
if(mkfifo(ipcPath.c_str(),MODE))
{
perror("mkfifo");
exit(1);
}
//2.创建缓冲区
char buffer[1024];
//3.打开管道文件
int fd = open(filename,O_RDONLY);
//4.进行进程间通信
while(true)
{
read(fd,buffer,sizeof(buffer)-1);
printf("%s\n",buffer);
}
close(fd)
return 0;
}
进程2:
int main()
{
//因为上一个进程已经创建好了管道文件,故这个进程不需要再创建管道文件。
int fd = open(filename,mode);
//上一个进程是选择读,那么这个进程选择写
char buffer[1024];
while(true)
{
cin>>buffer;
write(fd,buffer,sizeof(buffer));
}
close(fd);
return 0;
}
进程1和进程2之间可以进行通信了。
system V 共享内存
不同进程使用同一块共享内存~
共享内存的几个函数讲解:
1、shmget
#include <sys/ipc.h>
#include <sys/shm.h>
//获取共享内存
int shmget(key_t key, size_t size, int shmflg);
参数分析:
key 是作为分配共享内存的唯一标识! 每一块共享内存都有唯一的key值
而这个key值 可以通过函数 ftok(const char *pathname, int proj_id)进行产生
size:共享内存的大小
shmflg: IPC_CREAT IPC_EXCL
当只有IPC_CREAT选项打开时,不管是否已存在该块共享内存,则都返回该共享内存的ID,若不存在则创建共享内存
当只有IPC_EXCL选项打开时,不管有没有该快共享内存,shmget()都返回-1
所以当IPC_CREAT | IPC_EXCL时, 如果没有该块共享内存,则创建,并返回共享内存ID。
若已有该块共享内存,则返回-1;
2.shmat
void *shmat(int shmid,const void *shmaddr,int shmflg);
功能:将共享内存段连接到进程地址空间
参数分析:
shmid 是shmget返回的标识符!
shmaddr 有三种情况:
1.如果shmaddr 是NULL,系统将自动选择一个合适的地址!
2.如果shmaddr 不是NULL 并且没有指定SHM_RND则此段连接到addr所指定的地址上。
3.如果shmaddr非0 并且指定了SHM_RND 则此段连接到shmaddr -(shmaddr mod SHMLAB)所表示的地址上。这里解释一下SHM_RND命令,它的意思是取整,而SHMLAB的意思是低边界地址的倍数,它总是2的乘方,该算式是将地址向下取最近一个SHMLAB的倍数。 除非只计划在一种硬件上运行应用程序(在现在是不太可能的),否则不用指定共享段所连接到的地址。
!!!!所以一般指定shmaddr为0,以便由内核选择地址。
shmflg:
如果在flag中指定了SHM_RDONLY位,则以只读方式连接此段,否则以读写的方式连接此段。
shmat 如果挂起失败,则返回-1.
3.shmdt函数
int shmdt(const void *shmaddr);
功能:将共享内存段与当前进程脱离
shmaddr:shmat的返回值
返回值:成功返回0;失败返回-1。
将共享内存段与当前进程脱离不等于删除共享内存段
4.shmctl函数
功能:用于控制共享内存
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
shmid:由shmget返回的共享内存标识码
cmd:将要采取的动作(有三个可取值)
IPC_STAT 把shmid_ds结构中的数据设置为共享内存的当前关联值
IPC_SET 在进程有足够权限的前提下,把共享内存的当前关联值设置为shmid_ds数据结构中给出的值
IPC_RMID 删除共享内存段
buf:指向一个保存着共享内存的模式状态和访问权限的数据结构
返回值:成功返回0;失败返回-1
实战:
进程1:
int main()
{
int key = ftok(filename,proj_id);
int shmid = shmget(key,2048,IPC_CREAT|IPC_EXCL|0666);
char* shmaddr = shmat(shmid,nullptr,0);
//shmaddr 其实相当于就是共享内存段的首地址。我们可以直接通过shmaddr进行操作共享内存的内容
while(i<26)
{
addr[i] = 'A'+i;
i++;
addr[i] = 0;
sleep(1);
}
return 0;
}
进程2
int main()
{
int key = ftok(filename,proj_id);
int shmid = shmget(key,2048,0);//这里将不再创建共享内存,而是直接通过Key来找到共享内存块
char* shmaddr = shmat(shmid,nullptr,0);
//shmaddr 其实相当于就是共享内存段的首地址。我们可以直接通过shmaddr进行操作共享内存的内容
printf("%s\n",shmaddr);
shmdt(shmaddr);
return 0;
}