进程通信
本节重点:
- 进程间通信介绍。
- 掌握匿名命名管道原理操作。
- 编写进程池。
- 掌握共享内存。
- 了解消息队列。
- 了解信号量。
- 理解内核管理IPC资源的方式,了解C实现多态。
一.进程通信介绍
之前的进程都是各玩各的,但是两个进程有时需要进行一 些协作。如下:
- 数据传输:一个进程需要将它的数据发送给另一个进程。
- 资源共享:多个进程之间共享同样的资源。
- 通知事件:一个进程需要向另一个或一组进程发送消息,通知发生了某种事件(如进程终止时要通知父进程)
- 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。
但是进程具有独立性(内核数据结构task_struct、代码和数据、虚拟地址空间、页表、打开的文件等等),两个进程进行通信时是比较困难的,即便是父子进程。
进程间通信(IPC)的前提:先得让不同的进程,看到同一份资源(某种形式的内存空间),该资源只能由操作系统提供(调用系统调用)。公共资源是文件时:管道。内存块时:共享内存。队列时:消息队列。计数器时:信号量。
本地通信:同一台主机,同一个操作系统,不同的进程进行通信。其中包括三种标准:
- 管道:匿名管道pipe、命名管道。
- System V 进程间通信:System V 消息队列、System V 共享内存、System V 信号量。
- POSIX 进程间通信:消息队列、共享内存、信号量、互斥量、条件变量、读写锁。
网络通信:TODO
二.管道
管道是一种半双工的通信方式,数据只能在一个方向上流动。它提供了一种简单而有效的方法,让不同的进程可以协同工作,一个进程产生的数据能够直接被另一个进程处理,避免了数据在磁盘上的临时存储,提高了数据处理的效率。
1.匿名管道
匿名管道没有名字,只能用于具有亲缘关系的进程之间通信,通常是父子进程或者兄弟进程。这是因为匿名管道是在进程创建子进程时通过复制文件描述符的方式传递给子进程的。
父子进程的读写位置时独立的(在struct file中),创建子进程时,struct file也要拷贝一份。此时父子进程的公共资源是文件内核缓冲区。子进程关闭文件时,struct file中的引用计数-1,只有引用计数为0时,才是真正关闭文件。
管道只能进行单向通信,该管道是某一个被打开文件的内核级缓冲区。管道是内存级别的,不需要刷新到磁盘中,也不需要名字,匿名管道。
问题:
- 不关闭 fd?fd 泄漏,容易造成误操作。
- rw 同时打开?若只以 r 方式打开,子进程也是 r 方式,没有数据交互。
1.pipe接口
#include <unistd.h>
功能: 创建匿名管道
原型: int pipe(int fd[2]);
输出型参数fd: ⽂件描述符数组, 其中fd[0]表⽰读端, fd[1]表⽰写端
返回值: 成功返回0, 失败返回错误代码
2.管道读取的4大现象
- 管道为空 && 管道正常:read 会阻塞。
- 管道为满 && 管道正常:write 会阻塞。
- 管道写端关闭 && 读端继续:read 返回0,表示读到文件结尾。
- 管道读端关闭 && 写端继续:操作系统会直接杀掉写端的进程(发送13信号,kill -l 查看)
注意:写入的数据,全部读走了,再次读时,也是管道为空的一种。
#include <iostream>
#include <string>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
// Father process -> read -> fd[0]
// Child process -> write -> fd[1]
int main()
{
// 1.创建管道
int fds[2] = {0};
int n = pipe(fds); // fds:输出型参数
if (n != 0)
{
std::cerr << "pipe error" << std::endl;
return 1;
}
std::cout << "fd[0]:" << fds[0] << std::endl; // fd[0]:3
std::cout << "fd[1]:" << fds[1] << std::endl; // fd[1]:4
// 2.创建子进程
pid_t id = fork();
if (id < 0)
{
std::cerr << "fork error" << std::endl;
return 2;
}
else if (id == 0)
{
// 子进程
// 3.关闭不需要的fd, 关闭read
::close(fds[0]);
int cnt = 0;
int total = 0;
while (true)
{
std::string message = "H";
message += std::to_string(getpid());
message += ", ";
message += std::to_string(cnt);
// fd[1]
total += ::write(fds[1], message.c_str(), message.size());
cnt++;
std::cout << "total: " << total << std::endl;
sleep(1);
}
exit(0);
}
else
{
// 父进程
// 3.关闭不需要的fd, 关闭write
::close(fds[1]);
char buffer[1024];
while (true)
{
sleep(1);
ssize_t n = ::read(fds[0], buffer, 1024);
if (n > 0)
{
buffer[n] = 0;
std::cout << "child->father, message:" << buffer << std::endl;
}
else if (n == 0)
{
// 当读到文件末尾时: 说明子进程的写端关闭, 此时父进程的读端读取数据无意义
std::cout << "n: " << n << std::endl;
std::cout << "child quit after father quit too" << std::endl;
break;
}
else
{
// 读取错误...
}
// 父进程读端关闭, 此时子进程写入数据无意义, 操作系统直接杀掉子进程
::close(fds[0]);
break;
}
int status = 0;
pid_t rid = waitpid(id, &status, 0);
std::cout << "father wait child success:" << rid;
std::cout << " exit code: " << ((status >> 8) & 0xFF);
std::cout << " exit signal: " << (status & 0x7F) << std::endl;
}
return 0;
}
阻塞:将进程状态由 R 变成 S,将进程 task_struct 链入到管道文件 struct file 的等待队列中,当管道有数据时才让读 / 当管道写满时不让写,保证管道内数据的安全。
在Ubuntu系统,管道的上限是64KB。
3.匿名管道的5大特性
- 面向字节流。
- 用来进行具有血缘关系的进程进行IPC,常用于父子进程。
- 文件的声明周期随进程,管道也是。
- 单向数据通信。
- 管道自带同步互斥等保护机制,对共享资源的保护。
4.场景:进程池
- 问题1:派发任务结束后,如何关闭子进程?
- 答案:关闭管道的所有写端,当读端读取的返回值为0时,让子进程退出,父进程回收(waitpid)子进程即可。
- 问题2:如何关闭管道的所有写端?(据图可知:第一个管道3个写端,第二个管道2个写端,第三个管道1个写端)
- 答案:倒着关闭。先关闭最后一个管道的写端,子进程退出,此时间接关闭了第一、二个管道各自的1个写端…
// Makefile
BIN=processpool
CC=g++
FLAGS=-c -Wall -std=c++11
LDFLAGS=-o
# SRC=$(shell ls *.cc)
SRC=$(wildcard *.cc)
OBJ=$(SRC:.cc=.o)
$(BIN):$(OBJ)
$(CC) $(LDFLAGS) $@ $^
%.o:%.cc
$(CC) $(FLAGS) $<
.PHONY:clean
clean:
rm -f $(BIN) $(OBJ)
.PHONY:test
test:
@echo $(SRC)
@echo $(OBJ)
// Channel.hpp
#ifndef __CHANNEL_HPP__
#define __CHANNEL_HPP__
#include <iostream>
#include <string>
#include <unistd.h>
class Channel
{
public:
Channel(int wfd, pid_t who)
: _wfd(wfd), _who(who)
{
_name = "Channel——写端:" + std::to_string(_wfd) + "——子进程pid:" + std::to_string(_who);
}
~Channel()
{}
std::string Name()
{
return _name;
}
pid_t Id()
{
return _who;
}
int Wfd()
{
return _wfd;
}
void Send(int task)
{
::write(_wfd, &task, sizeof(task));
}
void Close()
{
::close(_wfd);
}
private:
int _wfd;
std::string _name;
pid_t _who;
};
#endif
// Task.hpp
#pragma once
#include <iostream>
#include <unordered_map>
#include <functional>
#include <ctime>
#include <sys/types.h>
#include <unistd.h>
using task_t = std::function<void()>;
static int number = 0;
void Download()
{
std::cout << "我是下载任务..., pid:" << getpid() << std::endl;
}
void Log()
{
std::cout << "我是日志任务..., pid:" << getpid() << std::endl;
}
void Sql()
{
std::cout << "我是数据框同步任务..., pid:" << getpid() << std::endl;
}
class TaskManger
{
public:
TaskManger()
{
srand(time(nullptr));
InsertTask(Download);
InsertTask(Log);
InsertTask(Sql);
};
~TaskManger(){};
void InsertTask(task_t t)
{
tasks[number++] = t;
}
int SelectTask()
{
return rand() % number;
}
void ExcuteTask(int number)
{
if(tasks.find(number) == tasks.end()) return;
tasks[number]();
}
private:
std::unordered_map<int, task_t> tasks;
};
TaskManger tm;
void Worker()
{
while (true)
{
int task = 0;
int n = ::read(0, &task, sizeof(task));
if (n == sizeof(task))
{
tm.ExcuteTask(task);
}
else if (n == 0)
{
std::cout << "child pid: " << getpid() << " quiting..." << std::endl;
break;
}
else
{
}
}
}
// ProcessPool.hpp
#include <iostream>
#include <string>
#include <vector>
#include <cstdlib>
#include <functional>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include "Task.hpp"
#include "Channel.hpp"
using work_t = std::function<void()>;
enum
{
OK = 0,
UsageError,
PipeError,
ForkError
};
class ProcessPool
{
public:
ProcessPool(int processnum, work_t work)
: _processnum(processnum), _work(work)
{}
~ProcessPool()
{}
int InitProcessPool()
{
for (int i = 0; i < _processnum; i++)
{
// 1.创建管道
int pipefd[2] = {0};
int n = pipe(pipefd);
if (n < 0)
return PipeError;
// 2.创建子进程
pid_t id = fork();
if (id < 0)
return ForkError;
// 3.建立通信信道
if (id == 0)
{
// version 3
// 关闭历史的wfd
std::cout << "child: " << getpid() << ", close history fd: ";
for(auto& c : _channels)
{
std::cout << c.Wfd() << " ";
c.Close();
}
std::cout << "over" << std::endl;
// 子进程: read
::close(pipefd[1]);
std::cout << "child pid: " << getpid() << ", read: " << pipefd[0] << std::endl;
::dup2(pipefd[0], 0);
_work();
::exit(0);
}
// 父进程: write
::close(pipefd[0]);
_channels.emplace_back(pipefd[1], id);
}
return OK;
}
void Debug()
{
for (auto &c : _channels)
{
std::cout << c.Name() << std::endl;
}
}
void DispatchTask()
{
int who = 0;
int num = 10;
while (num--)
{
// 1.选择一个任务
int task = tm.SelectTask();
// 2.选择一个管道
Channel &curr = _channels[who++];
who %= _channels.size();
std::cout << "########################################################" << std::endl;
std::cout << "send " << task << " to " << curr.Name() << ", 任务还剩: " << num << std::endl;
std::cout << "########################################################" << std::endl;
// 3.通过管道派发任务给子进程
curr.Send(task);
sleep(1);
}
std::cout << std::endl;
}
void CleanProcessPool()
{
// version 1
// // 1.关闭所有的写端
// for (auto &c : _channels)
// {
// c.Close();
// }
// // 2.等待子进程退出
// for (auto &c : _channels)
// {
// pid_t rid = ::waitpid(c.Id(), nullptr, 0);
// if (rid > 0)
// {
// std::cout << "child pid: " << rid << " wait success" << std::endl;
// }
// }
// version 2
// for (int i = _channels.size() - 1; i >= 0; i--)
// {
// // 倒着关闭所有管道的写端
// _channels[i].Close();
// pid_t rid = ::waitpid(_channels[i].Id(), nullptr, 0);
// if (rid > 0)
// {
// std::cout << "child pid: " << rid << " wait success" << std::endl;
// }
// }
// version 3
for (auto &c : _channels)
{
c.Close();
pid_t rid = ::waitpid(c.Id(), nullptr, 0);
if (rid > 0)
{
std::cout << "child pid: " << rid << " wait success" << std::endl;
}
}
}
private:
std::vector<Channel> _channels;
int _processnum;
work_t _work; // 回调函数
};
// Main.cc
#include "ProcessPool.hpp"
#include "Task.hpp"
void Usage(std::string process)
{
std::cout << "Usage: " << process << "number" << std::endl;
}
int main(int argc, char *argv[])
{
if (argc != 2)
{
Usage(argv[0]);
return UsageError;
}
int num = std::stoi(argv[1]);
ProcessPool *pp = new ProcessPool(num, Worker);
// 1.初始化进程池
pp->InitProcessPool();
// 2.派发任务
pp->DispatchTask();
// 3.清理进程池
pp->CleanProcessPool();
delete pp;
return 0;
}
2.命名管道
命名管道有一个对应的文件名,存储在文件系统中,因此可以用于无亲缘关系的进程之间通信。不同进程可以通过打开同一个命名管道文件来进行数据交换。
1.mkfifo接口
命名管道可以从命令行上创建,命令行方法是使用下面这个命令:
xzy@hcss-ecs-b3aa:~$ mkfifo fifo
xzy@hcss-ecs-b3aa:~$ ls -l
prw-rw-r-- 1 xzy xzy 0 Feb 21 22:20 fifo
# 终端1: w端打开fifo文件, 但是r端未打开fifo文件, 此时w端打开阻塞
xzy@hcss-ecs-b3aa:~$ echo "Hello World" > fifo
# 终端2: r端打开fifo文件, 此时w端阻塞停止
xzy@hcss-ecs-b3aa:~$ cat fifo
Hello World
命名管道也可以从程序里创建,相关函数有:
#include <sys/types.h>
#include <sys/stat.h>
功能: 创建命名管道
原型: int mkfifo(const char *pathname, mode_t mode);
参数pathname: 创建命名管道文件的路径名
参数mode: 创建命名管道文件的权限(拥有者, 所属组, other)
返回值: 成功返回0, 失败返回错误代码
文件的属性inode会记录UID(ls -n查看),进程task_struct也会记录UID,进程是否有权限访问文件就是通过UID来判断的。
- 问题1:为什么叫命名管道?
- 答案:真正存在的文件,具有自己的inode,具有路径 + 文件名(唯一性),让不同的进程用同一个文件系统路径标志同一个资源。
- 问题2:普通文件可以进行IPC?
- 答案:可以,普通文件也具有唯一的路径 + 文件名。但命名管道只使用文件内核缓冲区,不做磁盘刷新,效率高一些。
2.匿名管道与命名管道的区别
- 匿名管道由pipe函数创建并打开。
- 命名管道由mkfifo函数创建,打开用open。
- FIFO(命名管道)与pipe(匿名管道)之间唯一的区别在它们创建与打开的方式不同,一但这些工作完成之后,它们具有相同的语义。
3.命名管道的打开规则
- 如果当前打开操作是为读而打开FIFO时,阻塞直到有相应进程为写而打开该FIFO
- 如果当前打开操作是为写而打开FIFO时,阻塞直到有相应进程为读而打开该FIFO
4.场景:客户端/服务器
- 创建两个进程,一个进程创建命名管道(文件)并以 r 方式打开,另一个进程获取命名管道(文件)并以 w 方式打开。
- 客户端/服务器场景:服务器进程创建命名管道(文件)并且接收信息,客户端进程获取命名管道(文件)并且发送信息。
// Makefile
SERVER=server
CLIENT=client
CC=g++
SERVER_SRC=Server.cc
CLIENT_SRC=Client.cc
.PHONY:all
all:$(SERVER) $(CLIENT)
$(SERVER):$(SERVER_SRC)
$(CC) -o $@ $^
$(CLIENT):$(CLIENT_SRC)
$(CC) -o $@ $^
.PHONY:clean
clean:
rm -f $(SERVER) $(CLIENT)
// Client.hpp
#pragma once
#include <iostream>
#include "Common.hpp"
class Client
{
public:
Client()
: _fd(gdefaultfd)
{}
~Client()
{}
bool OpenPipeForWrite()
{
_fd = OpenPipe(gForWrite);
if (_fd < 0)
return false;
return true;
}
void ClosePipe()
{
ClosePipeHelper(_fd);
}
int SendPipe(const std::string &in)
{
return ::write(_fd, in.c_str(), in.size());
}
private:
int _fd;
};
// Server.hpp
#pragma once
#include <iostream>
#include "Common.hpp"
class Init
{
public:
Init()
{
umask(0);
int n = ::mkfifo(gpipeFile.c_str(), gmode);
if (n < 0)
{
std::cerr << "mkfifo error" << std::endl;
return;
}
std::cout << "mkfifo success" << std::endl;
}
~Init()
{
// unlink: 删除命名管道
int n = ::unlink(gpipeFile.c_str());
if (n < 0)
{
std::cerr << "unlink error" << std::endl;
return;
}
std::cout << "unlink success" << std::endl;
}
};
Init init;
class Server
{
public:
Server()
: _fd(gdefaultfd)
{}
~Server()
{}
bool OpenPipeForRead()
{
_fd = OpenPipe(gForRead);
if (_fd < 0)
return false;
return true;
}
void ClosePipe()
{
ClosePipeHelper(_fd);
}
// std::string *: 输出型参数
// const std::string &: 输入型参数
// std::string &: 输入输出型参数
int ReceivePipe(std::string *out)
{
char buffer[gsize];
ssize_t n = ::read(_fd, buffer, sizeof(buffer) - 1);
if (n > 0)
{
buffer[n] = 0;
*out = buffer;
}
return n;
}
private:
int _fd;
};
// Common.hpp
#pragma once
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
const std::string gpipeFile = "./fifo";
const mode_t gmode = 0600;
const int gdefaultfd = -1;
const int gsize = 1024;
const int gForRead = O_RDONLY;
const int gForWrite = O_WRONLY;
int OpenPipe(int flag)
{
// 如果读端打开文件时, 写端还没打开, 此时读端对应的open就会阻塞(读端读不了数据)
int fd = ::open(gpipeFile.c_str(), flag);
if (fd < 0)
{
std::cerr << "open error" << std::endl;
}
return fd;
}
void ClosePipeHelper(int fd)
{
if (fd >= 0)
::close(fd);
}
// Client.cc
#include <iostream>
#include "Client.hpp"
int main()
{
Client client;
client.OpenPipeForWrite();
std::string message;
while (true)
{
std::cout << "please enter# ";
getline(std::cin, message);
client.SendPipe(message);
}
client.ClosePipe();
return 0;
}
// Server.cc
#include <iostream>
#include "Server.hpp"
int main()
{
Server server;
std::cout << "pos1" << std::endl;
server.OpenPipeForRead();
std::cout << "pos2" << std::endl;
std::string message;
while (true)
{
int n = server.ReceivePipe(&message);
if (n > 0)
{
std::cout << "client say# " << message << std::endl;
}
else
{
break;
}
std::cout << "pos3" << std::endl;
}
std::cout << "client quit, me too" << std::endl;
server.ClosePipe();
return 0;
}
注意:如果读端打开文件时,写端还没打开,此时读端对应的open就会阻塞,需要写端打开,读端打开才会完毕。
三.System V
1.共享内存
共享内存区是最快的IPC形式。一旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到内核,换句话说是进程不再通过执行进入内核的系统调用来传递彼此的数据。它允许不同进程访问同一块物理内存区域,从而实现高效的数据共享。
依旧是一个进程通过系统调用创建共享内存并发送数据,另一个进程获取共享内存并接受数据。
- 开辟共享内存(shmget)
- 将共享内存挂接到两进程的地址空间中(shmat)
- 两个进程进行进程通信。
- 将共享内存段与当前两进程地址空间脱离(shmdt)
- 删除共享内存(shmctl)
共享内存再任何时刻,可以在操作系统内部同时存在多个共享内存,操作系统通过先描述,在组织的方式管理共享内存(共享内存的内核数据结构 + 内存块)
1.系统调用接口
#include <sys/ipc.h>
#include <sys/shm.h>
功能: 创建共享内存
原型: int shmget(key_t key, size_t size, int shmflg);
参数key: 共享内存段的标识符
参数size: 共享内存的大小
参数shmflg: 由九个权限标志构成, 它们的用法和创建文件时使⽤的mode模式标志是一样的
返回值: 成功返回共享内存段的标识符shmid; 失败返回-1
- shmflg取值为IPC_CREAT:共享内存不存在,创建并返回;共享内存已存在,获取并返回(保证进程能拿到共享内存)
- shmflg取值为IPC_CREAT | IPC_EXCL:共享内存不存在,创建并返回;共享内存已存在,出错返回(保证进程能拿到新创建的共享内存)
- shmflg也需要按位或上权限(例如:0600,表明拥有者可以读写共享内存),创建共享内存也需要加上权限(拥有者、所属组、other)
问题1:为什么需要用户输入 key?内核自己生成不行?
答案:进程间独立,内核帮一个进程生成,另一个进程拿不到。
问题2:如何设置 key?冲突了怎么办?
答案:系统提供了接口 ftok,冲突了手动重新设置。
#include <sys/types.h>
#include <sys/ipc.h>
功能: 创建key
原型: key_t ftok(const char *pathname, int proj_id);
参数pathname: 公共的路径
参数proj_id: 公共的项目id
返回值: 成功时返回key, 失败时返回-1
- 命名管道通过 路径 + 项目id,保证进程能够看到同一份资源。
- 文件的生命周期随进程。
- 共享内存的生命周期随内核,释放时需要:调用系统调用 / 使用指令 / 操作系统重启。
# 查看 System V 进程间通信: 消息队列、共享内存、信号量
xzy@hcss-ecs-b3aa:~$ ipcs
------ Message Queues --------
key msqid owner perms used-bytes messages
------ Shared Memory Segments --------
key shmid owner perms bytes nattch status
0x6601153b 0 xzy 0 4096 0
------ Semaphore Arrays --------
key semid owner perms nsems
# 查看共享内存
xzy@hcss-ecs-b3aa:~$ ipcs -m
------ Shared Memory Segments --------
key shmid owner perms bytes nattch status
0x6601153b 0 xzy 0 4096 0
# 删除共享内存
xzy@hcss-ecs-b3aa:~$ ipcrm -m 0
- key:只作为内核中区分共享内存的唯一标识符,不作为用户管理共享内存的标识符。
- shmid:作为用户管理共享内存的标识符(类似 fd / FILE*)
- owner:创建共享内存的用户。
- perms:拥有者、所属组、other权限。
- bytes:共享内存的大小。
- nattch:挂接该共享内存的进程数。
- status:状态
#include <sys/types.h>
#include <sys/shm.h>
功能: 将共享内存挂接到进程地址空间中
原型: void *shmat(int shmid, const void *shmaddr, int shmflg);
参数shmid: 共享内存标识符
参数shmaddr: 手动设置共享内存的虚拟起始地址
参数shmflg: 它的两个可能取值是SHM_RND和SHM_RDONLY
返回值: 成功时返回共享内存的虚拟起始地址, 失败返回-1
#include <sys/types.h>
#include <sys/shm.h>
功能: 将共享内存与进程的虚拟地址空间去关联
原型: int shmdt(const void *shmaddr);
参数shmaddr: 共享内存起始虚拟地址
返回值: 成功返回0; 失败返回-1
#include <sys/ipc.h>
#include <sys/shm.h>
功能: 控制共享内存
原型: int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数shmid: 共享内存标识符
参数cmd: 将要采取的动作
参数buf: 指向一个保存着共享内存的模式状态和访问权限的数据结构
返回值: 成功返回0; 失败返回-1
#include <iostream>
#include <string>
#include <cstdio>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
const std::string gpath = "/home/xzy/code";
const int gprojId = 0x6666;
// 操作系统申请空间是按照块(4KB)为单位申请空间的, 申请空间最好是4KB的倍数
const int gshmsize = 4096;
const mode_t gmode = 0600;
std::string ToHex(key_t k)
{
char buffer[64];
snprintf(buffer, sizeof(buffer), "0x%x", k);
return buffer;
}
int main()
{
// 1.创建key
key_t k = ::ftok(gpath.c_str(), gprojId);
if (k < 0)
{
std::cerr << "ftok error" << std::endl;
return 1;
}
std::cout << "key: " << ToHex(k) << std::endl;
// 2.创建共享内存 && 获取
// 注意: 共享内存也有权限
int shmid = ::shmget(k, gshmsize, IPC_CREAT | IPC_EXCL | gmode);
if (shmid < 0)
{
std::cerr << "shmget error" << std::endl;
return 2;
}
std::cout << "shmid: " << shmid << std::endl;
// 3.将共享内存挂接到进程的虚拟地址空间
void *addr = ::shmat(shmid, nullptr, 0);
std::cout << "attach shm done" << std::endl;
std::cout << "shm addr: " << addr << std::endl;
// 4.在这里进行通信...
// 5.将共享内存与进程的虚拟地址空间去关联
::shmdt(addr);
std::cout << "detach shm done" << std::endl;
// 6.删除共享内存
::shmctl(shmid, IPC_RMID, nullptr);
std::cout << "delete shm done" << std::endl;
return 0;
}
2.共享内存的特点
- 单向通信。
- 共享内存通信速度最快:传统的进程间通信方式,如管道,数据在传输过程中通常需要多次复制,从发送进程的用户空间复制到内核空间(write),再从内核空间复制到接收进程的用户空间(read),而共享内存允许不同进程通过虚拟地址直接访问同一块物理内存,避免了数据的复制操作,从而显著提高了数据传输的效率。
- 共享内存没有任何的保护机制:当一个进程挂接共享内存,不会等待另一个进程挂接共享内存。一个进程写到一半,可能就被另一个进程读走了,导致数据不一致问题。为了保证数据的一致性和完整性,需要使用同步机制(如信号量、互斥锁等)来协调多个进程对共享内存的访问。
共享内存的保护机制需要由用户自己完成保护。被保护起来的共享内存称为:临界资源;访问共享内存的代码称为:临界区。
3.场景:客户端/服务器
server.cc、client.cc 中使用的shm对象中的成员变量类似写时拷贝的形式在各自的进程中准备两份。
- 创建两个进程,一个进程创建共享内存并以读数据,另一个进程获取共享内存并以写数据。
- 客户端/服务器场景:服务器进程创建共享内存并且接收信息,客户端进程获取共享内存并且发送信息(这里通过创建两个命名管道,增加保护机制)
利用命名管道 -> 管道正常 && 写端为空时: 读端阻塞的性质,控制数据的一致性:先运行server时,程序阻塞在read管道A处,接着运行client时,client访问共享内存(写数据),此时client先往管道A中write,然后server的管道A读数据,接着client阻塞在read管道B处,server访问共享内存(读数据),最后server先往管道B中write,然后client的管道B读数据。进入下一个循环!
// Makefile
SERVER=server
CLIENT=client
CC=g++
SERVER_SRC=Server.cc
Client_SRC=Client.cc
.PHONY:all
all:$(SERVER) $(CLIENT)
$(SERVER):$(SERVER_SRC)
$(CC) -o $@ $^ -std=c++11
$(CLIENT):$(Client_SRC)
$(CC) -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -f $(SERVER) $(CLIENT)
// ShareMemory.hpp
#pragma once
#include <iostream>
#include <string>
#include <cstdio>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
const std::string gpath = "/home/xzy/code";
const int gprojId = 0x6666;
// 操作系统申请空间是按照块(4KB)为单位申请空间的, 申请空间最好是4KB的倍数
const int gshmsize = 4096;
const mode_t gmode = 0600;
std::string ToHex(key_t k)
{
char buffer[64];
snprintf(buffer, sizeof(buffer), "0x%x", k);
return buffer;
}
struct data
{
char status[32];
char lasttime[48];
char image[1024];
};
class ShareMemory
{
private:
void CreatShmHelper(int shmflg)
{
_key = ftok(gpath.c_str(), gprojId);
if (_key < 0)
{
std::cerr << "ftok error" << std::endl;
return;
}
_shmid = shmget(_key, gshmsize, shmflg);
if (_shmid < 0)
{
std::cerr << "shmget error" << std::endl;
return;
}
std::cout << "shmid: " << _shmid << std::endl;
}
public:
ShareMemory()
: _shmid(-1), _key(0), _addr(nullptr)
{
}
~ShareMemory() {}
void CreatShm()
{
if (_shmid == -1)
{
CreatShmHelper(IPC_CREAT | IPC_EXCL | gmode);
}
std::cout << "key: " << ToHex(_key) << std::endl;
}
void GetShm()
{
CreatShmHelper(IPC_CREAT);
}
void AttachShm()
{
_addr = ::shmat(_shmid, nullptr, 0);
if ((long long)_addr == -1)
{
std::cout << "attach error" << std::endl;
}
}
void DetachShm()
{
if (_addr != nullptr)
{
::shmdt(_addr);
}
}
void DeleteShm()
{
::shmctl(_shmid, IPC_RMID, nullptr);
}
void *GetAddr()
{
return _addr;
}
void ShmMeta()
{
// 系统提供的数据类型
struct shmid_ds buffer;
int n = ::shmctl(_shmid, IPC_STAT, &buffer);
if(n < 0) return;
std::cout << "################" << std::endl;
std::cout << buffer.shm_atime << std::endl;
std::cout << buffer.shm_cpid << std::endl;
std::cout << buffer.shm_ctime << std::endl;
std::cout << buffer.shm_nattch << std::endl;
std::cout << ToHex(buffer.shm_perm.__key) << std::endl;
std::cout << "################" << std::endl;
}
private:
int _shmid;
key_t _key;
void *_addr;
};
ShareMemory shm;
// Fifo.hpp
#pragma once
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
const std::string gfifoA = "./fifo.a";
const std::string gfifoB = "./fifo.b";
const mode_t gfifomode = 0600;
const int gdefaultfd = -1;
const int gsize = 1024;
const int gForRead = O_RDONLY;
const int gForWrite = O_WRONLY;
class Fifo
{
private:
void OpenFifo(int flag)
{
// 如果读端打开文件时,写端还没打开,读端对用的open就会阻塞
_fd = ::open(_fifo.c_str(), flag);
if (_fd < 0)
{
std::cerr << "open error" << std::endl;
return;
}
std::cout << _fd << std::endl;
}
public:
Fifo(std::string fifo)
: _fd(-1), _fifo(fifo)
{
umask(0);
int n = ::mkfifo(_fifo.c_str(), gfifomode);
if (n < 0)
{
// std::cerr << "mkfifo error" << std::endl;
return;
}
std::cout << "mkfifo success" << std::endl;
}
~Fifo()
{
if (_fd >= 0)
::close(_fd);
int n = ::unlink(_fifo.c_str());
if (n < 0)
{
std::cerr << "unlink error" << std::endl;
return;
}
std::cout << "unlink success" << std::endl;
}
bool OpenFifoForWrite()
{
OpenFifo(gForWrite);
if (_fd < 0)
return false;
return true;
}
bool OpenFifoForRead()
{
OpenFifo(gForRead);
if (_fd < 0)
return false;
return true;
}
int FifoRead()
{
int code = 0;
ssize_t n = ::read(_fd, &code, sizeof(code));
if (n == sizeof(code))
return 0;
else if (n == 0)
return 1;
else
return 2;
}
void FifoWrite()
{
int code = 1;
::write(_fd, &code, sizeof(code));
}
private:
int _fd;
std::string _fifo;
};
Fifo fifoA(gfifoA);
Fifo fifoB(gfifoB);
// Time.hpp
#pragma once
#include <iostream>
#include <string>
#include <ctime>
std::string GetCurTime()
{
time_t t = time(nullptr);
struct tm *cur = ::localtime(&t);
char curtime[32];
snprintf(curtime, sizeof(curtime), "%d-%d-%d %d:%d:%d",
cur->tm_year + 1900,
cur->tm_mon + 1,
cur->tm_mday,
cur->tm_hour,
cur->tm_min,
cur->tm_sec);
return curtime;
}
// Client.cc
#include <iostream>
#include <string.h>
#include "ShareMemory.hpp"
#include "Time.hpp"
#include "Fifo.hpp"
int main()
{
shm.GetShm();
shm.AttachShm();
shm.ShmMeta();
std::cout << "time: " << GetCurTime() << std::endl;
std::cout << "pid: " << getpid() << std::endl;
// 1.数据不一致: 刚写完一个字符, 立刻就读走了
// char* strinfo = (char*)shm.GetAddr();
// std::cout << "client addr: " << (void*)strinfo << std::endl;
// char ch = 'A';
// while(ch <= 'Z')
// {
// sleep(1);
// strinfo[ch - 'A'] = ch;
// ch++;
// }
// 2.添加两个命名管道, 模拟进程间同步, 保证数据一致性
fifoA.OpenFifoForWrite();
fifoB.OpenFifoForRead();
struct data *data = (struct data *)shm.GetAddr();
char ch = 'A';
while (ch <= 'Z')
{
strcpy(data->status, "最新");
strcpy(data->lasttime, GetCurTime().c_str());
strcpy(data->image, "xxxxxxxxxxxxxxxxxxxxx");
fifoA.FifoWrite();
fifoB.FifoRead(); // 当命名管道正常 && 写端为空时: 读端阻塞
sleep(3);
}
shm.DetachShm();
return 0;
}
// Server.cc
#include <iostream>
#include <cstring>
#include "ShareMemory.hpp"
#include "Fifo.hpp"
#include "Time.hpp"
int main()
{
shm.CreatShm();
shm.AttachShm();
// 1.数据不一致: 刚写完一个字符, 立刻就读走了
// char* strinfo = (char*)shm.GetAddr();
// std::cout << "server addr: " << (void*)strinfo << std::endl;
// while(true)
// {
// sleep(1);
// std::cout << strinfo << std::endl;
// }
// 2.添加两个命名管道, 模拟进程间同步, 保证数据一致性
fifoA.OpenFifoForRead();
fifoB.OpenFifoForWrite();
struct data *data = (struct data *)shm.GetAddr();
while (true)
{
fifoA.FifoRead(); // 当命名管道正常 && 写端为空时: 读端阻塞
std::cout << "status: " << data->status << std::endl;
std::cout << "lasttime: " << data->lasttime << std::endl;
std::cout << "image: " << data->image << std::endl;
fifoB.FifoWrite();
}
shm.DetachShm();
shm.DeleteShm();
return 0;
}
2.消息队列(选学)
- System V 消息队列是存于内核中的消息链表,由唯一的消息队列标识符(msgid)进行标识。每个消息由消息类型(mtype)和消息内容(mtext)构成,进程可依据消息类型有选择性地发送和接收消息,以此实现进程间的双向通信。
- 消息队列提供了一个进程向另外一个进程发送一块数据的方法,每个数据块都被认为是有一个类型,接收者进程接收的数据块可以有不同的类型值,IPC资源必须删除,否则不会自动清除,除非重启,所以systemV IPC资源的生命周期随内核。
3.信号量(选学)
1.并发编程,概念铺垫
- 共享资源:多个执行流(进程),能看到的同一份公共资源。
- 临界资源:被保护起来的资源。
- 保护的方式常见:互斥与同步。
- 互斥:任何时刻,只允许一个执行流访问资源。
- 同步:多个执行流,访问临界资源的时候,具有一定的顺序性。
- 系统中某些资源一次只允许一个进程使用,称这样的资源为临界资源或互斥资源。
- 在进程中涉及到临界资源的程序段叫临界区。你写的代码 = 访问临界资源的代码(临界区) + 不访问临界资源的代码(非临界区)
- 所谓的对共享资源进行保护,本质是对访问共享资源的代码进行保护。
2.信号量
信号量本质上是一个计数器,用于控制对共享资源的访问。它主要有两种操作:P 操作(也称为 semop 中的 sem_op 为 -1 的操作)和 V 操作(sem_op 为 +1 的操作)。P 操作会将信号量的值减 1,如果减 1 后信号量的值小于 0,则进程会被阻塞,直到信号量的值大于等于 0 为止;V 操作会将信号量的值加 1,如果有其他进程因为该信号量而被阻塞,那么会唤醒其中一个进程。
四.内核组织管理IPC资源的方式
- IPC资源一定是全局资源(不是全局变量),需要被所有的进程看到。
以下是IPC资源的属性:
从内核角度看IPC结构:
再谈共享内存:struct shmid_kernel 结构体中存在 struct file* shm_file 指向文件,存在文件内核缓冲区,这个文件必须被映射到进程的地址空间中(struct mm_struct),而 mm_struct 中存在 struct vm_area_struct,而 vm_area_struct 其中又存在 struct file* vm_file 指向该文件,unsigned long vm_start 和 unsigned long vm_end 指向文件内存块的开始和结束。实现文件到进程的映射。动态库的映射也是如此。