赞
踩
目录
进程运行具有独立性,进程想要直接进行进行通信难度比较大,进程间通信的前提是让不同的资源能够看到同一块资源。
单进程无法使用并发能力,无法实现多进程协同,传输数据,同步执行流,消息通知等。
想象现实中的管道,它一定是有一个出口有一个入口,用来传输资源,资源只能从一端流向另一端
进程间通信的管道也同理,一个进程发送数据另一个进程接收数据。
管道是由操作系统所提供的最古老的进程间通信的形式。
匿名管道只适用于具有亲缘关系的进程间通信——通常用于父子进程间的通信
一个进程创建子进程,父进程打开的文件也会被子进程所继承,因为是同一个文件,所以父子进程打开的同一个文件具有相同的struct file,因此父子进程就同时看到了同一份内存资源,也就可以借助这个文件来进行父子进程间的通信。
匿名管道创建步骤
1、分别以读写方式打开同一个文件
2、fork()创建子进程
3、双方进程各自关闭自己不需要的文件描述符
管道文件是不需要刷新到磁盘的,它是专门用来进行进程间通信的文件,刷新到磁盘,相当于进行IO使进程间通信的效率降低
我们可以使用pipe来创建匿名管道,这里要传入一个数组,它是输出线参数
下标为0的代表读端,1代表写端
这样前置工作就准备好了
接下来以一个比较复杂的例子来演示匿名管道
写一个简单的单机版负载均衡
创建一个进程池,父进程通过匿名管道随机向子进程派发任务
- //task.hpp
- #pragma once
-
- #include <iostream>
- #include <vector>
- #include <string>
- #include <functional>
- #include <unordered_map>
- #include <cassert>
- #include <ctime>
- #include <sys/wait.h>
- #include <unistd.h>
-
- typedef std::function<void()> func;
- std::vector<func> callbacks;
- std::unordered_map<int, std::string> desc;
-
- void readMySQL()
- {
- std::cout << "sub process [ " << getpid() << " ] 执行访问数据库任务\n" << std::endl;
- }
-
- void execuleUrl()
- {
- std::cout << "sub process [ " << getpid() << " ] 执行URL解析\n" << std::endl;
- }
-
- void cal()
- {
- std::cout << "sub process [ " << getpid() << " ] 执行加密任务\n" << std::endl;
- }
-
- void save()
- {
- std::cout << "sub process [ " << getpid() << " ] 执行持久化任务\n" << std::endl;
- }
-
- void Load()
- {
- desc.emplace(callbacks.size(), "readSQL: 读取数据库");
- callbacks.emplace_back(readMySQL);
-
- desc.emplace(callbacks.size(), "execuleURL: 进行url解析");
- callbacks.emplace_back(execuleUrl);
-
- desc.emplace(callbacks.size(), "cal: 进行加密计算");
- callbacks.emplace_back(cal);
-
- desc.emplace(callbacks.size(), "save: 进行数据的文件保存");
- callbacks.emplace_back(save);
- }
-
- void showHandler()
- {
- for(const auto& iter : desc)
- {
- std::cout << iter.first << "\t" << iter.second << std::endl;
- }
- }
-
- int handlerSize()
- {
- return callbacks.size();
- }
-
-
- //main.cpp
- #include "task.hpp"
-
- #define PROCESS_NUM 5
-
- // 父进程给子进程派发任务
-
- int waitCommind(int waitFd, bool &quit)
- {
- uint32_t command = 0;
- ssize_t s = read(waitFd, &command, sizeof(command));
- if (s == 0)
- {
- quit = true;
- return -1;
- }
-
- assert(s == sizeof(uint32_t));
- return command;
- }
-
- void sendAndWakeUp(pid_t who, int fd, uint32_t command)
- {
- write(fd, &command, sizeof(command));
- std::cout << "main process: call process " << who << "execute" << desc[command] << "through" << fd << std::endl;
- }
-
- int main()
- {
- Load();
-
- // 简单的线程池
- // 进程pid 及 文件描述符
- std::vector<std::pair<pid_t, int>> slots;
-
- // 创建子进程
- for (size_t i = 0; i < PROCESS_NUM; i++)
- {
- // 1、以读写方式打开文件
- int fd[2] = {0};
- int n = pipe(fd);
- assert(n == 0);
-
- pid_t id = fork();
- if (id < 0)
- {
- std::cerr << "fork" << std::endl;
- }
- else if (id == 0)
- {
- // child
- // 关闭写端
- close(fd[1]);
- while (true)
- {
- bool quit = false;
- int command = waitCommind(fd[0], quit);
- if (quit == true)
- {
- break;
- }
-
- if (command >= 0 && command < handlerSize())
- {
- callbacks[command];
- }
- else
- {
- std::cout << "非法command" << std::endl;
- }
- }
- exit(1);
- }
- else
- {
- // father
- // 关闭读端
- close(fd[0]);
- slots.emplace_back(id, fd[1]);
- }
- }
-
- // 派发任务
- size_t count = 0;
- srand(time(0));
- while (true)
- {
- int command = rand() % handlerSize();
-
- int choice = rand() % slots.size();
-
- sendAndWakeUp(slots[choice].first, slots[choice].second, command);
- sleep(1);
- count++;
-
- if (count == 10)
- {
- break;
- }
- }
-
- // 关闭文件描述符
- for (const auto &slot : slots)
- {
- close(slot.second);
- }
-
- // 回收子进程
- for (const auto &slot : slots)
- {
- waitpid(slot.first, nullptr, 0);
- }
-
- return 0;
- }
命名管道与匿名管道类似,不过命名管道可以让不具有亲缘关系的进程通信
首先也是要让不同的进程看到同一份资源,双方进程就可以通过管道文件的路径看到同一份资源
管道文件首先是一个文件,它是有名字的可以被打开,但是不会将内存数据进行刷新到磁盘
mkfifo命名可以创建一个管道文件
我们通过echo命令将hello通过管道文件发送过去,echo命令就进入了阻塞状态
这时通过管道文件就可以读取资源了
这样就完成了echo和cat的通信
删除管道文件可以使用unlink或者rm
接下来看代码的方式创建管道文件
mkfifo可以创建管道文件
成功返回0,失败返回-1
接下来还是举一个例子来演示命名管道进程间通信
server创建管道文件,从client读取资源,结束通信后,client关闭管道文件并且删除管道文件
client获取管道文件,进行正常通信,向server发送数据
- //comm.hpp
- #pragma once
-
- #include <iostream>
- #include <string>
- #include <cassert>
- #include <ctime>
- #include <unistd.h>
- #include <sys/types.h>
- #include <sys/stat.h>
- #include <sys/wait.h>
- #include <fcntl.h>
-
- #define MODE 0666
- #define SIZE 128
- #define PROCESS_NUM 3
-
- std::string ipcPath = "./fifo.ipc";
-
-
-
- //log.hpp
- #include <iostream>
- #include <string>
- #include <ctime>
-
- #define Debug 0
- #define Notice 1
- #define Warning 2
- #define Error 3
-
- const std::string msg[] = {
- "Debug",
- "Notice",
- "Warning",
- "Error"};
-
- std::ostream &Log(std::string message, int level)
- {
- std::cout << " | " << (unsigned)time(nullptr) << " | " << msg[level] << " | " << message;
- return std::cout;
- }
-
-
- //server.cpp
- #include "log.hpp"
- #include "comm.hpp"
-
- static void GetMessage(int fd)
- {
- char buffer[SIZE];
- while (true)
- {
- ssize_t s = read(fd, buffer, sizeof(buffer));
- if (s > 0)
- {
- buffer[s] = '\0';
- std::cout << "client call" << buffer << std::endl;
- }
- else if (s == 0)
- {
- std::cerr << "[ " << getpid() << " ]"
- << "read end of file server quit" << std::endl;
- break;
- }
- else
- {
- std::cerr << "read" << std::endl;
- break;
- }
- }
- }
-
- int main()
- {
- // 1、创建管道文件
- if (mkfifo(ipcPath.c_str(), MODE) < 0)
- {
- std::cerr << "mkfifo" << std::endl;
- exit(1);
- }
-
- Log("创建管道文件成功", Debug) << "step 1" << std::endl;
-
- // 2、正常文件操作
- int fd = open(ipcPath.c_str(), O_RDONLY);
- if (fd < 0)
- {
- std::cerr << "open" << std::endl;
- }
-
- Log("打开文件成功", Debug) << "step 2" << std::endl;
-
- // 3、正常通信代码
- // 使用进程池
-
- Log("进程间开始通信", Debug) << "step 3" << std::endl;
-
- for (size_t i = 0; i < PROCESS_NUM; i++)
- {
- pid_t id = fork();
- if (id < 0)
- {
- std::cerr << "fork" << std::endl;
- }
- else if (id == 0)
- {
- GetMessage(fd);
- exit(1);
- }
- }
-
- Log("进程间通信完成", Debug) << "step 4" << std::endl;
-
- // 4、回收子进程
-
- for (size_t i = 0; i < PROCESS_NUM; i++)
- {
- waitpid(-1, nullptr, 0);
- }
-
- Log("等待子进程成功", Debug) << "step 5" << std::endl;
-
- // 5、关闭文件
- close(fd);
-
- Log("关闭文件成功", Debug) << "step 6" << std::endl;
-
- // 5、删除文件
- unlink(ipcPath.c_str());
- Log("删除管道文件成功", Debug) << "step 7" << std::endl;
-
- return 0;
- }
-
-
- //clinet.cpp
- #include "comm.hpp"
-
- int main()
- {
- //1、获取管道文件
- int fd = open(ipcPath.c_str(), O_WRONLY);
- if(fd < 0)
- {
- std::cerr << "open" << std::endl;
- exit(1);
- }
-
- //2、IPC过程
- std::string buffer;
- while(true)
- {
- std::cout << "Please Enter Mseeage Line:> ";
- std::getline(std::cin, buffer);
- if(buffer == "quit")
- {
- break;
- }
- write(fd, buffer.c_str(), buffer.size());
- }
-
- //3、关闭管道文件,server自动停止读取
- close(fd);
- return 0;
- }
命名管道多个进程竞争读取,没有任何问题,发送数据大小是小于4096字节就是原子的
1、管道具有通过让进程间协同,提供了访问控制
2、管道提供的是面向流式的通信服务——面向字节流——协议
3、管道是基于文件的,文件的声明周期是随进程的,管道的声明周期是随进程的
4、管道是单向通信的,就是半双工通信的一种特殊情况
管道的访问控制
1、写快,读慢,写满就不能再写了
2、写慢,读快,管道没有数据的时候,读必须等待
3、写关,读0,标识读到了文件结尾
4、读关,写继续,OS终止写进程
还要说一下通信的前提:让不同的进程能够看到同一份资源共享内存也是同样道理
不过它是直接向OS申请一块空间,然后映射到进程的共享区,这样就使得进程间通信的效率提升,因为不用使用系统调用多次拷贝资源了,进程可以直接访问那块共享的内存资源
1)申请空间
2)建立映射(多个进程映射同一块共享内存)
3)通信
4)去掉映射
5)释放空间
共享内存的提供者是操作系统,操作系统也要管理共享内存,通过先描述在组织
共享内存 = 共享内存块 + 对应的共享内存的内核数据结构
我们可以使用shmget函数来申请共享内存
它的返回值shmid是共享内存的用户标识符,类似于文件描述符fd
它的最后一个参数有两个选项IPC_CREAT and IPC_EXCL
一般两者是组合使用的,单独使用IPC_EXCL是没有意义的
单独使用IPC_CREAT:如果创建共享内存,底层已经存在,那么就获取它,不存在就创建它
两者组合使用:如果底层不存在就创建它,并且返回新创建的shmid,如果底层存在,出错返回
接下来就是shmget的第一个参数key
key可以保证与我们进程通信的进程就是要通信的进程,并且能够看到我创建的共享内存
我们可以使用ftok函数来创建key,ftok是一种加密算法,使用同样的算法规则,传入的参数相同就能够形成唯一值,ftok的第一个参数pathname最好设定我们有访问权限的路径
shmid vs key
只有创建的时候使用key,大部分情况用户访问共享内存,都是使用的shmid
当我们的程序运行结束,我们的共享内存,还存在
System V IPC资源的生命周期随内核
解决办法
1、手动删除
ipcs -m可以获取共享内存相关属性
使用命令 ipcrm -m + shmid 可以删除共享内存
2、代码删除
shmctl + 选项IPC_RMID可以删除共享内存
共享内存链接方法
使用shmat可以将共享内存与进程建立映射,这个函数类似于malloc,返回值是void*可以强制转换为char*当作数组来使用共享内存
共享内存去关联方法
去除关联的方式是使用shmdt
注意:创建共享内存的大小最好是页(4096bytes)的整数倍
如果你申请4097个字节,操作系统会给你申请4096 * 2bytes,剩下的4095字节就浪费了
总结:
shmget返回值是用户层标识符
key是内核层面标定共享内存的标识符
想要自己挂接共享内存等对共享内存的操作要使用shmid,同时共享内存的生命周期是随内核的
shmat:挂接共享内存
shmdt:去关联
shmctl :删除共享内存
共享内存是在堆栈之间的共享区,它是属于用户的
用户空间:不用经过系统调用就可以访问的空间
管道的通信方式是文件,它是在内核空间中,它是内核的一种特定的数据结构,是操作系统维护的在[3G, 4G]的内核空间中,用户访问需要使用系统调用
下面看一个简单的例子
- //comm.hpp
- #pragma once
-
- #include <iostream>
- #include <string>
- #include <cassert>
- #include <cstring>
- #include "../named_pipe/log.hpp"
- #include <unistd.h>
- #include <sys/stat.h>
- #include <sys/types.h>
- #include <sys/ipc.h>
- #include <sys/shm.h>
- #include <fcntl.h>
-
- #define FIFO_NAME "./fifo"
- #define PROJ_ID 0x66
- #define SHM_SIZE 4096
- #define PATH_NAME "/home/ww"
-
- class Init
- {
- public:
- Init()
- {
- umask(0);
- int n = mkfifo(FIFO_NAME, 0666);
- assert(n == 0);
- Log("创建管道文件成功", Notice) << "\n";
- }
-
- ~Init()
- {
- unlink(FIFO_NAME);
- Log("删除管道文件成功", Notice) << "\n";
- }
- };
-
- #define READ O_RDONLY
- #define WRITE O_WRONLY
-
- int OpenFIFO(std::string pathname, int falgs)
- {
- int fd = open(pathname.c_str(), falgs);
- assert(fd >= 0);
- return fd;
- }
-
- void Wait(int fd)
- {
- Log("等待中", Notice) << "\n";
- uint32_t tmp = 0;
- ssize_t s = read(fd, &tmp, sizeof(uint32_t));
- assert(s == sizeof(uint32_t));
- }
-
- void Signal(int fd)
- {
- uint32_t tmp = 0;
- ssize_t s = write(fd, &tmp, sizeof(tmp));
- assert(s == sizeof(uint32_t));
- Log("唤醒中", Notice) << "\n";
- }
-
- void CloseFIFO(int fd)
- {
- close(fd);
- }
-
-
- //server.cpp
- #include "comm.hpp"
-
- Init init;
-
- int main()
- {
- // 1、生成相同的key
- key_t key = ftok(PATH_NAME, PROJ_ID);
-
- Log("生成key成功", Notice) << "key: " << key << std::endl;
-
- // 2、申请共享内存
- int shmid = shmget(key, SHM_SIZE, IPC_CREAT | IPC_EXCL | 0666);
- if (shmid < 0)
- {
- Log("申请共享内存失败", Error) << "shmid: " << shmid << std::endl;
- exit(2);
- }
-
- Log("申请共享内存成功", Notice) << "shmid: " << shmid << std::endl;
- // 3、共享内存与进程建立映射
-
- char *adder = (char *)shmat(shmid, nullptr, 0);
-
- Log("链接共享内存成功", Notice) << "shmAdder: " << adder << std::endl;
-
- // 4、正常通信
- Log("开始通信", Notice) << std::endl;
- // 共享内存是没有访问控制的,使用管道进行管道控制
-
- int fd = OpenFIFO(FIFO_NAME, READ);
- while (true)
- {
- Wait(fd);
- printf("%s\n", adder);
- if (strcmp(adder, "quit") == 0)
- break;
- }
-
- // 5、取消映射
-
- int ret = shmdt(adder);
- if (ret < 0)
- {
- Log("共享内存去关联失败", Error) << std::endl;
- }
-
- // 6、删除共享内存
-
- ret = shmctl(shmid, IPC_RMID, nullptr);
- if (ret < 0)
- {
- Log("删除共享内存失败", Error) << std::endl;
- exit(3);
- }
- return 0;
- }
-
-
-
- //client.cpp
- #include "comm.hpp"
-
- int main()
- {
- // 1、生成相同的key
- key_t key = ftok(PATH_NAME, PROJ_ID);
- Log("生成key成功", Notice) << "key: " << key << std::endl;
-
- // 2、获取共享内存
- int shmid = shmget(key, SHM_SIZE, 0);
- if (shmid < 0)
- {
- Log("获取共享内存失败", Error) << "shmid: " << shmid << std::endl;
- exit(2);
- }
-
- // 3、建立映射
- char *adder = (char *)shmat(shmid, nullptr, 0);
-
- Log("链接共享内存成功", Notice) << "shmAdder: " << adder << std::endl;
-
- // 4、通信
- Log("开始通信", Notice) << std::endl;
-
- int fd = OpenFIFO(FIFO_NAME, WRITE);
- while (true)
- {
- ssize_t s = read(0, adder, SHM_SIZE - 1);
- if (s > 0)
- {
- adder[s - 1] = 0;
- Signal(fd);
- if (strcmp(adder, "quit") == 0)
- break;
- }
- }
-
- // 5、取消映射
- int ret = shmdt(adder);
- if (ret < 0)
- {
- Log("取消共享内存失败", Error) << std::endl;
- }
-
- return 0;
- }
为了让进程间通信,让不同的进程之间看到同一份资源,之前所说的所有通信方式,本质都是优先解决一个问题,让不同的进程看到同一份资源
这样就会带来一些时序问题,造成数据不一致问题
1、多个进程看到的公共的一份资源叫做临界资源
2、把自己的进程访问临界资源的代码叫做临界区
3、为了更好的保护临界区,可以让在任何时刻都只能有一个进程进入临界区——互斥
4、原子性:要么不做,要么做完,没有中间状态
以上就是今天要讲的内容,本文仅仅简单介绍了进程间通信的管道和共享内存
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。