目录
进程间的通信
管道
1.概念
2.匿名管道
3.命名管道
4.匿名管道与命名管道的区别
5.总结管道的特点
共享内存
1.原理
2.共享内存的建立
3.代码
1.相关函数
2.总结
进程间的通信
1.进程间通信目的
- 数据传输:一个进程需要将它的数据发送给另一个进程
- 资源共享:多个进程之间共享同样的资源。
- 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
- 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变
2.进程间通信发展
- 管道
- System V进程间通信
- POSIX进程间通信
3.进程间通信分类
- 管道: 匿名管道pipe , 命名管道
- System V IPC:System V 消息队列, System V 共享内存 , System V 信号量
- POSIX IPC:消息队列 共享内存 信号量 互斥量 条件变量 读写锁
4.进程通信的本质理解
- 进程间通信的前提: 首先让不同进程看到同一块"内存"
- 看到的同一块"内存" 属于那一个进程? --> 不能隶属于任何一个进程,而应该强调共享
管道
1.概念
我们把从一个进程连接到另一个进程的一个数据流称为一个“管道"
管道都是单向通信, 传输内容的 管道中传输的都是"资源",管道通信的背后是进程通过管道通信,管道就是文件
2.匿名管道
1.概念:匿名管道是内存级文件, 没有名字 , 不会写入磁盘
如何做到让不同进程看到同一份"资源"?子进程继承
-- fork让子进程继承 --- 能够让具有血缘关系的进程进行进程间通信 ---常用于父子进程
2.代码编写
#include <unistd.h>
功能:创建一无名管道
原型
int pipe(int fd[2]); //fd[2]:输出型参数,期望通过调用它, 得到被打开的文件fd
参数
fd:文件描述符数组,其中fd[0]表示读端, fd[1]表示写端
返回值:成功返回0,失败返回错误代码
匿名管道底层也是文件,但是没有名字, 默认以读写方式打开pipefd[0]: 读端 pipefd[1]: 写端
a.从键盘读取数据,写入管道,读取管道,写道屏幕
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
int main()
{
int fds[2];
char buff[100];
int len;
if(pipe(fds) == -1)
perror("创建管道失败"),exit(-1);
//从stdin读取数据到buff,并写入管道fd[1]
while(fgets(buff,100,stdin))
{
len = strlen(buff);
if(write(fds[1],buff,len) != len)
{
perror("写入失败");
break;
}
memset(buff,0,sizeof(buff));
//从管道里读取数据,并写入到stdout
if(len = read(fds[0],buff,100) == -1)
{
perror("读取失败");
break;
}
if(write(1,buff,len) != len)
{
perror("写入stdout失败");
break;
}
}
return 0;
}
b.父进程写数据,子进程读数据
#include<iostream>
#include<string>
#include<cstdio>
#include<cstring>
#include<assert.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
using namespace std;
int main()
{
//创建管道
int pipefd[2] = {0};
int n = pipe(pipefd);
assert(n != -1);
(void)n;
//创建子进程
pid_t id = fork();
assert(id != -1);
if(id == 0)
{
//子进程 - 读
close(pipefd[1]);
//将数据读取到buff里,并打印
char buff[1024];
while(true)
{
ssize_t s = read(pipefd[0],buff,sizeof(buff)-1);
if(s > 0)
{
cout<<"Father:"<<buff<<endl;
}
}
exit(0);
}
//父进程 - 写
close(pipefd[0]);
string message = "我是父进程,我正在发送消息";
int count = 0;
char send_buff[1024];
while(true)
{
//往send_buff里面写要发送的信息
snprintf(send_buff,sizeof(send_buff),"%s[%d]:%d",message.c_str(),getpid(),count++);
//写入管道
write(pipefd[1],send_buff,strlen(send_buff));
sleep(1);
}
pid_t ret = waitpid(id,nullptr,0);
assert(ret > 0);
(void)ret;
return 0;
}
c.进程池
1.主进程派发任务,子进程接受任务(匿名管道实现)
2.使用循环创建多个子进程与管道,父进程保存其pipefd与pid (这里将其保存在vector中,构造pair)
3.Task.hpp:封装一个funtional包装器,将任务的执行方法放入vector里, 在创建一张hash表记录
4.子进程通pipefd[0],将要执行的任务编号读取,然后执行
父进程通过pipefd[1],将要执行的任务编号写入给子进程读取
5.关闭文件描述符,以及回收子进程: 根据vector里记录的pid与pipefd,回收
代码:Linux-test: Linux下,提交代码 - Gitee.com
3.命名管道
1.概念:命名管道是磁盘文件(本质也是内存文件, 不过在磁盘上构建了文件名)
--管道应用的一个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信。
--如果我们想在不相关的进程之间交换数据,可以使用FIFO文件来做这项工作,它经常被称为命名管道。该方式为:打开目录下同一文件
--命名管道是一种特殊类型的文件
2.代码编写
命令行创建命名管道
mkfifo filename
程序中创建
接口: int mkfifo(const char* pathname , mode_t mdoe)
头文件: <sys/types.h> <sys/stat.h>
返回值: 创建成功: 0 创建失败: -1
//示例
int main(int argc, char *argv[])
{
mkfifo("p2", 0644);
return 0;
}
a.创建命名管道,进程A写数据,进程B读取数据
mkfifo name_pipe
//A
echo "hello world" > name_pipe
//B
cat < name_pipe
删除管道文件: unlink 文件名(name_pipe) 也可以 rm 文件名
b.用命名管道实现server&client通信
server:创建管道文件, 打开管道文件, 从管道文件中读取信息到buff,并打印, 关闭文件,
client: 打开管道文件, 将buff的内容写入管道文件, 关闭文件
其中server端也可以创建多个子进程去读取管道文件里的信息
代码:Linux-test: Linux下,提交代码 - Gitee.com
4.匿名管道与命名管道的区别
1.它们让不同进程看到同一份资源的手段不一样,但它们的本质是一样的(都是文件)
命名管道可以让不同进程进行通信 ~~> 打开同一目录下相同的文件
匿名管道只能让父子进程进行通信 ~~> 子进程继承的方式
2.为什么匿名管道叫匿名?
--匿名管道 ~~> 内存级文件 ~~>在磁盘上没有对应的映像
--命名管道 ~~> 内存级文件 ~~>但在磁盘上给其构建了文件名
5.总结管道的特点
a.管道是用来进行具有血缘关系的进程进行进程间的通信 --- 常用于父子通信
b.管道具有通过让进程协同 , 提供了访问控制!
c.管道提供的是面向流式的通信服务 --- 面向字节流 --- 协议
d.管道是基于文件的,文件的生命周期是随进程的-->管道的生命周期是随进程的(如果通信双方退出,管道会释放)
e.管道是单向通信的 , 就是半双工通信的一种特殊情况
访问控制:
//写入的一方,fd没有关闭,如果有数据,就读,没有数据就等
//写入的一方,fd关闭,读取的一-方,read会返回0,表示读到了文件的结尾!
--写快,读慢,写满不能在写了
--写慢,读快,管道没有数据的时候,读必须等待
--写关,读0,标识读到了文件结尾
--读关,写继续写,OS终止写进程
共享内存
1.原理
2.共享内存的建立
共享内存的提供者: 操作系统
操作系统管理共享内存~~>先描述在组织 ~~> 共享内存 = 共享内存块 + 对应的内核数据结构
--创建共享内存 ~~>将该内存与地址空间建立映射 ~~> 进行通信 ~~> 去掉关联 ~~> 删除共享内存
3.代码
1.相关函数
ftok函数
功能:用于创建共享内存、消息队列和信号量等 IPC 对象的键值
原型
key_t ftok(const char *pathname, int proj_id);
参数
pathname:路径名
proj_id:项目标识符
返回值:成功返回一个键值,失败返回-1
//将 pathname 所指向文件的索引节点号(inode number)与 proj_id 结合起来,
//生成一个唯一的键值,这个键值可以用作创建或获取 IPC 对象的标识符。
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:指定连接的地址
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函数
功能:用于控制共享内存
原型
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数
shmid:由shmget返回的共享内存标识码
cmd:将要采取的动作(有三个可取值)
buf:指向一个保存着共享内存的模式状态和访问权限的数据结构
返回值:成功返回0;失败返回-1
当进程运行结束, 我们的共享内存还存在, IPC资源的生命周期随内核
命令行删除:
查看共享内存: ipcs -m
删除共享内存: ipcrm -m shmid (手动)
a.示例1
// 1. 创建公共的Key值
key_t k = ftok(PATH_NAME, PROJ_ID); // const char* path_name, int id
assert(k != -1); // key创建失败
// 2. 创建共享内存 -- 建议要创建一个全新的共享内存 -- 通信的发起者
int shmid = shmget(k, SHM_SIZE, IPC_CREAT | IPC_EXCL | 0666); // 最后可以给0
if (shmid == -1)
{
perror("shmget");
exit(1);
}
//IPC_CREAT: 如果创建共享内存,底层已经存在(直接获取并返回),如果不存在(创建后再返回)
//IPC_CREAT | IPC_EXCL :底层已经存在(出错返回),如果不存在(创建后再返回)
// 3.将指定的共享内存,挂接到自己的地址空间
char *shmaddr = (char *)shmat(shmid, nullptr, 0); // 类似于malloc
// 进行通信
//...
// 4. 将指定的共享内存,从自己的地址空间中去关联
int n = shmdt(shmaddr);
assert(n != -1);
// 5.删除共享内存,IPC_RMID即便是有进程和当下的shm挂接,依旧删除共享内存
n = shmctl(shmid, IPC_RMID, nullptr);
assert(n != -1);
b.用共享内存实现server&client通信
server:获取key,创建共享内存,挂上链接,进行通信(读取数据), 去除链接,删除共享内存
client:获取key,获取共享内存,挂上链接,进行通信(写数据),去除链接
由于共享内存没有访问控制, 可以利用管道具有访问控制的特性(实现其访问控制)
代码:Linux-test: Linux下,提交代码 - Gitee.com
结果:
2.总结
1.共享内存是所有进程通信(IPC)速度最快的,不需要过多的拷贝(不需要将数据给OS):
只要通信双方使用shm,一方直接向共享内存中写入数据,另一方,立马就可以看到对方写入的数据
(共享内存一旦映射进各进程的地址空间,双方进程如果想要通信,直接进行内存级的读写即可)
2.共享内存缺乏访问控制 ! ~~> 会带来并发问题 (但我们可以用管道来增加访问控制)
3.为什么之前的pipe,fifo都要通过read,write来进行通信?
使用管道让双方看到的公共资源属于文件,需要操作系统自己去维护(文件是属于内核中的一种数据结构)
管道
共享内存