通信背景
进程是具有独立性的,每个进程都有独立的PCB,独立的数据和数据结构,因此进程间想要交互数据,成本会非常高,但有时候需要多进程协同处理同一件事情,这个时候就要进程间通信了,
进程通信的目的
数据传输:一个进程需要将它的数据发给另一个进程;
通知事件:一个进程需要向另一个或一组进程发送消息,通知它们发生了某种事情
进程控制:有些进程希望完全控制另一个进程的执行。
管道
由于进程间具有独立性,在通信之前必须让不同的进程看到同一份资源,而资源的不同也就决定了通信方式的不同---------管道
就是通信方式的一种.
把一个进程连接到另一个进程的一个数据流称为一个管道
匿名管道
管道是一个内存级的文件
在内核中文件结构体有对应的缓冲区和该文件是何种文件。
匿名管道的实现
#include <time.h>
#include <iostream>
#include <cstdio>
#include <unistd.h>
#include <cstring>
#include <sys/types.h>
#include <sys/wait.h>
using namespace std;
int main()
{
int pipefd[2] = {0};
if (pipe(pipefd) != 0)
{
cerr << "pipe error" << endl;
return 1;
}
// 创建子进程
pid_t id = fork();
if (id < 0)
{
cerr << "fork error" << endl;
return 2;
}
else if (id == 0)
{
// child
// 子进程读取
close(pipefd[1]);
#define NUM 1024
char buffer[NUM];
while (true)
{
cout << "时间戳" << (uint64_t)time(nullptr) << endl;
// 子进程没有带sleep为什么也会休眠?
memset(buffer, 0, sizeof(buffer));
size_t s = read(pipefd[0], buffer, sizeof(buffer) - 1);
if (s > 0)
{
// 读取成功
buffer[s] = '\0';
cout << "子进程收到消息,消息的内容是" << buffer << endl;
}
else if (s == 0)
{
cout << "父进程写完了,我也读完了" << endl;
break;
}
else
{
// DO nothing
cerr << "读取错误" << endl;
}
}
close(pipefd[9]);
exit(0);
}
else
{
// parent
// 父进程写入
close(pipefd[0]);
string msg = "你好,儿子,我是你爹";
int cnt = 5;
while (cnt--)
{
write(pipefd[1], msg.c_str(), msg.size());
sleep(1); // 这里是为了一会看现象明显
}
close(pipefd[1]);
cout << "父进程写完了" << endl;
}
// 0 -> 嘴巴 ->读
// 1 -> 写
pid_t res = waitpid(id, nullptr, 0);
if (res > 0)
{
cout << "等待子进程成功" << endl;
}
cout << "fd[0]:->" << pipefd[0] << endl;
cout << "fd[1]:->" << pipefd[1] << endl;
return 0;
}
为什么父进程要分别打开读和写?
因为子进程是继承自父进程的,进程通信的时候我们不知道是父进程读还是子进程读,因此需要打开两文件描述符。
父子进程为什么要分别要关闭读写?
因为管道的特点是单向的,所以管道传输数据应该也是单向的,所以当父进程写的时候,其要关闭读端,子进程关闭写端,这样就可以构成一个单向的数据传输了。至于谁来写谁来读,完全是由需求决定的。
由运行结果可以得出:
当父进程没有写入数据的时候,子进程在等,父进程写入数据之后,子进程才能读(read)到数据,子进程打印读取数据要以父进程的节奏为主!
管道内部:如果没有数据的话,reader就必须阻塞式等待数据
管道内部:如果数据被写满,writer就必须阻塞式等待数据被读后才能写
阻塞就是将该进程的task_struct
放入等待队列中。
pipe
是自带访问控制的,同步和互斥机制,即避免两个进程同时访问共享资源的一种机制
子进程读,父进程写,如何保证读完了呢?
在父进程完成写后关闭了文件描述符,子进程是能感受到的,此时文件描述符file结构体中管道的引用计数就变成了1,表示此时只有子进程一个进程指向他,子进程就知道现在文件只有他一个人读了,此时读完,就表示数据读完了。
给其中一个进程指派任务
#include <time.h>
#include <cstdlib>
#include <sys/types.h>
#include <sys/wait.h>
#include <cassert>
#include <vector>
#include <iostream>
#include <unistd.h>
using namespace std;
using functor = void (*)();
vector<functor> functors; // 方法集合
void f1()
{
cout << "这是第一个任务,执行的进程ID是"
<< getpid() << "执行的时间是:" << time(nullptr) << endl;
}
void f2()
{
cout << "这是第二个任务,执行的进程ID是"
<< getpid() << "执行的时间是:" << time(nullptr) << endl;
}
void f3()
{
cout << "这是第三个任务,执行的进程ID是"
<< getpid() << "执行的时间是:" << time(nullptr) << endl;
}
void loadfunctor()
{
functors.push_back(f1);
functors.push_back(f2);
functors.push_back(f3);
}
int main()
{
loadfunctor();
// 创建管道
int pipefd[2] = {0};
if (pipe(pipefd) != 0)
{
cerr << "pipe error" << endl;
return 1;
}
// 创建子进程
pid_t fd = fork();
if (fd < 0)
{
cerr << "fork error" << endl;
return 2;
}
else if (fd == 0)
{
// 关闭不需要的文件fd
// child read
close(pipefd[1]);
while (1)
{
uint32_t operatorType = 0;
// 如果有数据就读取,如果没有数据就阻塞等待
// 等待任务的到来
ssize_t s = read(pipefd[0], &operatorType, sizeof(uint32_t));
if (s == 0)
{
cout << "已全部读取完毕" << endl;
break;
}
assert(s == sizeof(uint32_t));
// assert断言,是编译有效,debug模式有效
// release模式,断言就没有了
// 一旦断言没有了,s变脸就是只被定义了,没有被使用
// release模式中可能会有warning,所以加一个(void)s
(void)s; // 防止警告
if (operatorType < functors.size())
{
functors[operatorType]();
}
else
{
cerr << "bug? operatorType = " << operatorType << endl;
}
}
close(pipefd[0]);
exit(1);
}
else
{
srand((long long)time(nullptr)); // 种一颗随机数种子
// parent write
close(pipefd[0]);
// 指派任务
int num = functors.size();
int cnt = 10;
while (cnt--)
{
uint32_t commandCode = rand() % num;
write(pipefd[1], &commandCode, sizeof(uint32_t));
sleep(1);
}
close(pipefd[1]);
pid_t res = waitpid(fd, nullptr, 0);
if (res)
cout << "wait success" << endl;
}
return 0;
}
如何控制一批进程呢?
首先加载业务方法
循环创建三个子进程,创建子进程的时候也创建三个管道,每个子进程分别对应一个管道,再创建pair<int, int>分别表示子进程的pid,和该子进程对应管道的写端。
#include <time.h>
#include <cstdlib>
#include <sys/types.h>
#include <sys/wait.h>
#include <cassert>
#include <vector>
#include <iostream>
#include <unistd.h>
using namespace std;
using functor = void (*)();
vector<functor> functors; // 方法集合
void f1()
{
cout << "这是第一个任务,执行的进程ID是"
<< getpid() << "执行的时间是:" << time(nullptr) << endl;
}
void f2()
{
cout << "这是第二个任务,执行的进程ID是"
<< getpid() << "执行的时间是:" << time(nullptr) << endl;
}
void f3()
{
cout << "这是第三个任务,执行的进程ID是"
<< getpid() << "执行的时间是:" << time(nullptr) << endl;
}
void loadfunctor()
{
functors.push_back(f1);
functors.push_back(f2);
functors.push_back(f3);
}
using elem = pair<int32_t, int32_t>; // 进程pid,该进程对应管道写端fd
int processNum = 5;
vector<elem> assignMap;
void work(int blockFd)
{
cout << "进程[ " << getpid() << "]"
<< "开始工作" << endl;
// 子进程核心工作的代码
while (true)
{
// 阻塞等待,获取任务信息
uint32_t operatorCode = 0;
ssize_t s = read(blockFd, &operatorCode, sizeof(uint32_t));
if (s == 0)
break;
assert(s == sizeof(uint32_t));
(void)s;
// 处理任务
if (operatorCode < functors.size())
functors[operatorCode]();
cout << "进程[ " << getpid() << "]"
<< "结束工作" << endl;
}
}
// pair--> 子进程的pid,子进程管道fd
void sentTask(const vector<elem> &processFd)
{
srand((long long)time(nullptr));
int cnt = 5;
while (cnt--)
{
sleep(1);
// 选择一个进程,选择进程是随机的,没有压着一个进程给任务
// 较为均匀的将任务给所有的子进程-- 可以说是某种负载均衡
uint32_t pick = rand() % processFd.size();
// 选择一个任务
uint32_t task = rand() % functors.size();
//
// 把任务给一个指定的进程
write(processFd[pick].second, &task, sizeof(uint32_t));
// 打印对应的提示信息
cout << "父进程指派任务-> " << endl;
cout << "cnt" << cnt << endl;
}
}
int main()
{
loadfunctor();
// 创建processNum个进0程
for (int i = 0; i < processNum; i++)
{
// 定义保存管道fd的对象
int pipefd[2] = {0};
// 创建管道0
pipe(pipefd);
// 创建子进程
pid_t id = fork();
if (id == 0)
{
// 子进程读取
close(pipefd[1]);
// 子进程执行
work(pipefd[0]);
exit(0);
}
// 父进程做的事情
close(pipefd[0]);
elem e({id, pipefd[1]});
assignMap.push_back(e);
}
cout << "创建所有子进程成功" << endl;
// 父进程派发任务
sentTask(assignMap);
sleep(1);
cout << "出来了?" << endl;
for (int i = 0; i < processNum; i++)
{
//此时子进程只能用kill用命令行杀死,所以此时我们设置为非阻塞式等待,父进程
//直接退出,此时父五个子进程就会变成孤儿进程被bash领养
//bash会使他们也退出
if (waitpid(assignMap[i].first, nullptr, WNOHANG) > 0)
cout << "wait for :" << assignMap[i].first << "wait success!"
<< "number: " << i << endl;
close(assignMap[i].second);
}
return 0;
}
管道特征总结:
- 管道只能用来进行具有亲子关系的进程,进行进程间通信,常用于父子进程
- 管道只能单向通信,(不仅仅是因为现实生活中管道是单向的,Linux内核的实现也是单向),管道是半双工的。即每次只有一方能够发送消息,而另一方必须等待接受到消息后才能发送信息。
- 管代自带同步机制(pipe满,writer等,pipe空,reader等)–自带访问控制,eg如果创建一个子进程,让父子进程都打印,此时不能判断哪个进程先打印,这个就叫做没有访问控制
- 管道是面向字节流的—先写的字符一定先被读取,没有格式边界,需要用户自己定义却分内容的边界
- 管道也是文件,所以管道的生命周期随进程,进程退出了,管道也就退出了
面向字节流的理解
size_t s = read(pipefd[0], buffer, sizeof(char) * 3);
如果每次读取三个字节,此时输出的是
每次读取的是一个汉字
如果读取的是六个字节,则
size_t s = read(pipefd[0], buffer, sizeof(char) * 3 * 2);
每次读取的都是两个汉字。
此时我们的Linux的编码方式是UTF-8,该编码方式下,大部分常用汉字都是占三个字节
命名管道
上述的命名管道主要用于有血缘关系的通信 ,毫不相干的两个进程之间的通信用命名管道。
进程通信的本质:不同的进程要看到同一份资源
匿名管道:子进程继承自父进程,直接就能看到了
命名管道:一个fifo文件具有唯一路径,通过路径,就能使得两个进程找到同一个资源。
注:命名管道虽然是通过文件路径找到资源的,其是一个文件毫无疑问,但是这个文件的TCB表示其是一个pipe文件,其只是在磁盘上有一个符号,磁盘上不会有数据,因此命名管道也是内存级别的,OS看过看到一个文件是管道文件,就不会使文件往磁盘定期刷新了。
comm.h
#pragma once
#include<iostream>
#include<cstdio>
#include<cstring>
#include<cerrno>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#include<string>
using namespace std;
#define IPC_PATH "./.fifo"
clientFifo.cc
#include "comm.h"
int main()
{
int pipeFd = open(IPC_PATH, O_WRONLY);
if (pipeFd < 0)
{
cerr << "open " << strerror(errno) << endl;
return 1;
}
#define NUM 1024
char line[NUM] = "sasakjsiqskjqsbkqjs";
int sum = 0;
while (true)
{
// 测试管道的大小有多大
// if (write(pipeFd, line, strlen(line)))
// {
// sum += strlen(line);
// cout << "写入了多少个字节?" << sum << "字节" << endl;
// }
// else
// {
// break;
// }
cout << "请输入你的消息#";
fflush(stdout);
memset(line, 0, sizeof(line));
// fgets会在line结尾自动添加\0;
if (fgets(line, sizeof(line), stdin) != nullptr)
{
line[strlen(line) - 1] = '\0';
write(pipeFd, line, strlen(line));
}
else
{
break;
}
}
cout << "管道的大小是: " << sum << endl;
close(pipeFd);
cout << "客户端推出啦" << endl;
return 0;
}
serverFifo.cc
#include "comm.h"
int main()
{
umask(0);
if (mkfifo(IPC_PATH, 0666) != 0)
{
cerr << "mkfifo error " << endl;
return 1;
}
int pipefd = open(IPC_PATH, O_RDONLY);
if (pipefd < 0)
{
cerr << "open fifo error" << endl;
return 2;
}
#define NUM 1024
char buffer[NUM];
while (true)
{
ssize_t s = read(pipefd, buffer, sizeof(buffer) - 1);
if (s > 0)
{
buffer[s] = '\0';
cout << "客户端 -> 服务器#" << buffer << endl;
}
else if (s == 0)
{
cout << "客户端退出啦,我也推出啦!" << endl;
break;
}
else
{
cout << "read: " << strerror(errno) << endl;
break;
}
}
close(pipefd);
cout << "服务器也退出了!" << endl;
unlink(IPC_PATH);
return 0;
}
运行截图如下
我还想测试一下管道的大小,一直向管道写入,不读取
共享内存
基本概念
什么是共享内存
共享区能共享库也一定能共享其他东西,因此我们把内存创建好,某一进程通过页表映射到我们的进程地址空间,然后把这份空间的起始地址返回给用户,此时该进程就能通过自己的页表找到这份空间,此时另一进程也通过同样的方式将内存通过页表映射到自己的虚拟地址空间,再将该空间地址返回给自己
- 此时各自完成了共享内存的创建
- 分别把共享内存挂接到各自的进程上下文,这种就叫做共享内存
共享内存如何管理
问题来了:共享内存存在哪里?我如何知道共享内存存在还是不存在
存在内核中,–内核会给我们维护共享内存的结构。
问题又来了,操作系统中有多个进程通信,也就有多个共享内存,所以共享内存也是要被管理起来的。“先描述再组织”。
问题:共享内存是否存在我们该如何知道?
通过共享内存的唯一标识符key,有key说明有,并且该值一定是用户提供的。
为什么一定是用户提供的呢?
如果是操作系统提供key值,操作系统在当前进程的内存空间中创建一块共享内存,但由于进程间具有独立性,其他进程无法知晓这个key是否已经被使用了,所以key需要用户自己提供,并且与另外一个进程约定,该共享内存的key值。
内核代码
/* Obsolete, used only for backwards compatibility and libc5 compiles */
struct shmid_ds {
struct ipc_perm shm_perm; /* operation perms */
int shm_segsz; /* size of segment (bytes) *///共享内存空间大小
__kernel_time_t shm_atime; /* last attach time *///挂接时间
__kernel_time_t shm_dtime; /* last detach time *///取消挂接时间
__kernel_time_t shm_ctime; /* last change time *///改变时间
__kernel_ipc_pid_t shm_cpid; /* pid of creator */
__kernel_ipc_pid_t shm_lpid; /* pid of last operator */
unsigned short shm_nattch; /* no. of current attaches *///进程挂接数
unsigned short shm_unused; /* compatibility */
void *shm_unused2; /* ditto - used by DIPC */
void *shm_unused3; /* unused */
};
描述共享内存的数据结构里还保存了一个ipc_perm结构体,这个结构体保存了IPC(进程间通信)的一些权限关键信息
/* Obsolete, used only for backwards compatibility and libc5 compiles */
struct ipc_perm
{
__kernel_key_t key;//共享内存的唯一标识符
__kernel_uid_t uid;
__kernel_gid_t gid;
__kernel_uid_t cuid;
__kernel_gid_t cgid;
__kernel_mode_t mode; //权限
unsigned short seq;
};
匿名管道VS共享内存
匿名管道是约定好使用同一个文件通信
共享内存是约定好使用同一个key值通信
共享内存的相关函数
获得key值
贡献内存的创建
共享内存的申请理解:
虚拟地址空间的大小是4GB,操作系统一次读写磁盘是4kb,所以4GB–>1038576页,这么多页一定是要管理起来的,那么如何“先管理再组织”呢?
在操作系统中相当于是有struct page mem[1038576]数组,当我们为共享内存申请两页代码时,就相当于是在这个数组中分配了两个页。
如何知道共享内存没有被删除呢?
命令ipcs -m
还有两个函数在下面。控制函数和挂接函数
共享内存的删除
- 直接用命令删除
ipcrm -m (shimid的值)
所以此时我的共享内存应该是这样删除的
ipcrm -m 0
为什么不用key值而用shmid
因为key是在操作系统内核的,但是我们的ipc命令是在用户层的,所以只能用shimd值
还可以用其他方法吗?
还可以用系统接口进行删除
共享内存的使用
共享内存虽然是进程创建的但是共享内存不属于进程,它属于内核,即使进程退出了,共享内存依然存在
所以我们要将进程与共享内存产生关联
共享内存与进程产生关联
去关联
将客户端和服务端连接起来
总结:
我们把共享内存实际上是映射到了用户的进程地址空间,对每一个进程而言,挂接到自己的上下文中的共享内存,属于自己的空间,类似于堆空间或占空间,用户可以不使用系统调用接口直接使用。
共享内存由于他自身的特性,他没有任何访问限制,共享内存直接被双方看到,属于用户的空间,可以直接通信,所以不安全!。
共享内存还是所有进程间通信中速度最快的
对比管道而言:
进程A写信息到管道,此时进程B不能直接看到信息,还需要进程A拷贝到管道,再由管道复制拷贝到进程B。
相关源码
源码点这!
共享内存的控制用的是信号量。
信号量后续线程见!