目录
4.4.2 用命名管道实现 server 和 client 通信
1. 进程间通信介绍
1.1 进程间通信的目的
1. 数据传输:⼀个进程需要将它的数据发送给另⼀个进程。
2. 资源共享:多个进程之间共享同样的资源。
3. 通知事件:⼀个进程需要向另⼀个或⼀组进程发送消息,通知它(它们)发⽣了某种事件(如进程终⽌时要通知⽗进程)。
4. 进程控制:有些进程希望完全控制另⼀个进程的执⾏(如Debug进程),此时控制进程希望能够拦截另⼀个进程的所有陷⼊和异常,并能够及时知道它的状态改变。
1.2 进程间通信分类
1. 管道:(1)匿名管道pipe(2)命名管道
2. System IPC:(1)System V 消息队列(2)System V共享内存(3)System V 信号量
3. POSIX IPC:(1)消息队列(2)共享内存(3)信号量(4)互斥量(5)条件变量(6)读写锁
2. 管道
管道是Unix中最古老的进程间通信的形式,我们把从一个进程连接到另一个进程的一个数据流称为一个管道。
上图表示,将who进程输出的结果给到 wc -l 进程进行处理,然后输出对应输出。,其中 “|” 就是管道。who 命令显示关于当前在本地系统上的所有登录用户的信息,而 wc -l 统计文本文件的行数。
3. 匿名管道
匿名管道是用于具有血缘关系间进程通信的一种方式,常用与父子进程。匿名管道也是一种内存级的文件,所以当进程打开该管道时,也会有对应的文件描述符。
#include <unistd.h>
//功能:创建⼀匿名管道
int pipe(int fd[2]);
//参数
//fd:⽂件描述符数组,其中fd[0]表⽰读端, fd[1]表⽰写端
//返回值:成功返回0,失败返回错误代码
一般的进程的文件描述符0,1,2分别代表的是标准输入,标准输出,标准错误。所以调用pipe函数的进程给匿名管道分配的文件描述符一般为3,4,。3是读端,4是写端。
3.1 匿名管道的demo代码
#include <iostream>
#include <cstdio>
#include <cstring>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
void ChildWrite(int wfd)
{
char buffer[1024];
int cnt = 0;
while(true)
{
snprintf(buffer, sizeof(buffer), "I am child, pid: %d, cnt: %d", getpid(), cnt++);
write(wfd, buffer, strlen(buffer));
sleep(1);
}
}
void FatherRead(int rfd)
{
char buffer[1024];
while(true)
{
buffer[0] = 0;
ssize_t n = read(rfd, buffer, sizeof(buffer)-1);
if (n > 0)
{
buffer[n] = 0;
std::cout << "child say: " << buffer << std::endl;
}
}
}
int main()
{
//1. 创建管道文件
int fds[2] = {0};
int i = pipe(fds);
if (i < 0)
{
std::cout << "创建管道失败" << std::endl;
return 1;
}
// std::cout << "fds[0]: " << fds[0] << std::endl;
// std::cout << "fds[1]: " << fds[1] << std::endl;
// 2. 创建子进程
pid_t id = fork();
if (id == 0)
{
// child
// 子进程关闭读端
close(fds[0]);
ChildWrite(fds[1]);
}
//3. 关闭不需要的读写端,新城通信信道
//父进程关闭写端
close(fds[1]);
FatherRead(fds[0]);
waitpid(id, nullptr, 0);
close(fds[0]);
return 0;
}
这段代码的意思就是通过子进程向管道文件中不断写入数据,然后父进程读取子进程写入的数据输出到标准输出中。
3.2 用 fork 来共享管道的原理
3.3 文件描述符角度 -- 理解管道
父进程中创建管道文件,给其分配两个文件描述符,然后父进程中在创建一个子进程,子进程的文件描述符和父进程相同。分别关闭父进程的读端和子进程的写端,就能形成一个父进程写,子进程读的通信信道。
3.4 内核角度 -- 理解管道本质
从内核的角度上来开,父子进程都打开同一个管道文件,都为这一个管道文件创建了struct_file结构体,而两个结构体指向同一份内存资源,所以看待管道,就和看待文件一样。
3.5 管道读写规则
1. 当写端较慢,读端较快时:读端就会阻塞在read函数处,等到写端写入。
2. 当写端较快,读端较慢时:管道文件的缓冲区很容易被写满,这时写端就要等待读端将数据读走再写入。
3. 当写端进程关闭指向管道写端的文件描述符,读端继续读:read函数就会返回0,表示读到文件结尾。
4. 当写端继续写,读端进程关闭指向管道读端的文件描述符:操作系统会给写端进程发送13号信号 SIGPIPE ,杀掉写端进程。
5. 当写入的数据量不大于管道的缓冲区,linux将保证写入的原子性;当写入的数据量大于管道的缓冲区,linux将不再保证写入的原子性。
3.6 管道的五种特性
1. 匿名管道只能用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信;通常,一个管道由一个进程创建,然后该进程调用fork,此后父子进程之间就可应用该管道。
2. 管道文件自带同步机制,这种情况就是当写端较慢,读端较快的时候,读端需要等写端写入。
3. 管道是面向字节流的。
4. 管道是半双工的工作模式,在同一个时刻,数据只能一个进程发送,一个进程接收;需要双方通信时,需要建立起两个管道。
5. 一般而言,进程退出,管道文件就会释放,所以管道的什么周期随进程。
3.7 管道样例 -- 进程池
这里使用一个进程池的例子来说明上述的内容。这里创建的进程池的进程数量为5个,然后父进程是写端,子进程是读端,父进程通过管道给子进程发送任务码,然后子进程解析父进程发来的任务码执行相应的任务。
下图表示父子进程使用管道通讯的示意图。父进程循环创建管道文件和子进程。
第一次父进程创建管道文件时,占用3,4 文件描述符,但是父进程为写端,所以关闭3号文件描述符,而子进程1关闭4号文件描述符;第二次创建管道文件时,因为4号文件描述符指向了管道文件1的写端,所以父进程占用3,5文件描述符,这是父进程3号文件描述符指向管道文件2的读端,4号文件描述指向管道文件1的写端,5号文件描述指向管道文件2的写端,创建子进程2时,子进程继承父进程的文件描述符表,所以将不要的文件描述符关闭之后,父进程的4,5号文件描述符分别指向管道文件1的写端和管道文件2的写端,但是子进程2的文件描述符表中,3号文件描述符指向管道文件2的读端,而4号文件描述符指向管道文件1的写端。同理,子进程3也一样。
所以下图中,黑线是正常文件描述符的指向,而红线是每个子进程多的文件描述符的指向。这个问题后续再进行处理。
3.7.1 进程池demo代码
main.cc中是整个进程池运行的主要流程,一是创建出进程池对象ProcessPool,二是启动并创建进程池,三是给进程池中的子进程派发任务并执行,四是关闭指向子进程的文件描述符,子进程退出之后,通过waitpid回收子进程。
//main.cc
#include "ProcessPool.hpp"
int main()
{
// 1. 创建进程池对象
ProcessPool pp(gdefalutnum);
// 2. 启动进程池
pp.Start();
// pp.Debug();
// 3. 给进程池派发任务并执行
int cnt = 10;
while(cnt--)
{
pp.Run();
sleep(1);
}
// 4. 回收资源,结束进程池
pp.Stop();
return 0;
}
ProcessPool.hpp是将.h文件和.cpp文件结合起来,直接在文件中声明并定义类和函数了。这个文件主要包含Channel类和ChannelManager类和ProcessPool类。Channel类主要是对进程池中的每一个信道的信息进行描述。ChannelManager类是将每一个信道统一管理起来的类。ProcessPool类是进程池类,里面包含了进程池中通道的个数,ChannelManager对象以及管理分配给子进程的任务对象TaskManager类对象。
//ProcessPool.hpp
#ifndef __PROCESS_POOL_HPP__
#define __PROCESS_POOL_HPP__
#include <iostream>
#include <cstdlib>
#include <vector>
#include <unistd.h>
#include <sys/wait.h>
#include "Task.hpp"
// 描述一个信道的类,从父进程视角看,只需要看到一个信道中管道的写端描述符和对应子进程的pid
class Channel
{
public:
Channel(int fd, pid_t id) : _wfd(fd), _subId(id)
{
_name = "Channel-" + std::to_string(_wfd) + "-" + std::to_string(_subId);
}
~Channel()
{
}
void Send(int code)
{
int n = write(_wfd, &code, sizeof(code));
void(n);
}
void Close()
{
close(_wfd);
}
void Wait()
{
pid_t rid = waitpid(_subId, nullptr, 0);
}
int Fd()
{
return _wfd;
}
pid_t SubId()
{
return _subId;
}
std::string Name()
{
return _name;
}
private:
// 一个信道的写端文件描述符,子进程pid以及名字
int _wfd;
pid_t _subId;
std::string _name;
};
// 一个管理进程池进程的类
class ChannelManager
{
public:
ChannelManager() : _next(0)
{
}
~ChannelManager()
{
}
void Insert(int wfd, pid_t subId)
{
Channel c(wfd, subId);
_channels.push_back(c);
}
Channel &Select()
{
// 使用轮询的方式选择信道
auto &c = _channels[_next];
_next++;
_next %= _channels.size();
return c;
}
void PrintChannel()
{
for (auto &channel : _channels)
{
std::cout << channel.Name() << std::endl;
}
}
void StopSubProcess()
{
for (auto &channel : _channels)
{
channel.Close();
std::cout << "关闭:" << channel.Name() << std::endl;
}
}
void WaitSubProcess()
{
for (auto &channel : _channels)
{
channel.Wait();
std::cout << "回收:" << channel.Name() << std::endl;
}
}
private:
std::vector<Channel> _channels;
int _next;
};
#define gdefalutnum 5
// 描述一个进程池的类
class ProcessPool
{
public:
// 初始化进程池进程个数以及注册各种任务
ProcessPool(int num) : _process_num(num)
{
_tm.Register(PrintLog);
_tm.Register(Download);
_tm.Register(Upload);
}
// 子进程进行读任务
void Work(int rfd)
{
while (true)
{
int code = 0;
ssize_t n = read(rfd, &code, sizeof(code));
if (n > 0)
{
if (n != sizeof(code))
{
continue;
}
std::cout << "子进程[" << getpid() << "]收到一个任务码: " << code << std::endl;
_tm.Excute(code);
}
else if (n == 0)
{
std::cout << "子进程" << getpid() << "退出" << std::endl;
break;
}
else
{
std::cout << "读取错误" << std::endl;
break;
}
}
}
// 创建并启动进程池
bool Start()
{
for (int i = 0; i < _process_num; i++)
{
// 1. 创建管道
int pipefd[2] = {0};
int n = pipe(pipefd);
if (n < 0)
{
return false;
}
// 2. 创建子进程
pid_t subId = fork();
if (subId < 0)
{
return false;
}
else if (subId == 0)
{
// 子进程
// 3. 关闭不需要的文件描述符
close(pipefd[1]);
Work(pipefd[0]);
close(pipefd[0]);
exit(0);
}
else
{
// 父进程
// 3. 关闭不需要的文件描述符
close(pipefd[0]);
_cm.Insert(pipefd[1], subId);
}
}
return true;
}
// 测试进程池是否创建成功
void Debug()
{
_cm.PrintChannel();
}
void Run()
{
// 1. 选择一个任务
int taskCode = _tm.Code();
// 2. 选择一个子进程,负载均匀的选择一个子进程(轮询)完成任务
auto &c = _cm.Select();
std::cout << "选择了一个子进程:" << c.Name() << std::endl;
// 3. 发送任务
c.Send(taskCode);
std::cout << "发送了一个任务码:" << taskCode << std:: endl;
}
void Stop()
{
// 关闭父进程所有的wfd
_cm.StopSubProcess();
// 回收所有子进程
_cm.WaitSubProcess();
}
~ProcessPool()
{
}
private:
ChannelManager _cm;
int _process_num;
TaskManager _tm;
};
#endif
Task.hpp中定义了TaskManager类,用于管理子进程需要执行的任务。
//Task.hpp
#pragma once
#include <iostream>
#include <vector>
#include <ctime>
typedef void (*task_t)();
void PrintLog()
{
std::cout << "打印日志的任务" << std::endl;
}
void Download()
{
std::cout << "下载的任务" << std::endl;
}
void Upload()
{
std::cout << "上传的任务" << std::endl;
}
class TaskManager
{
public:
TaskManager()
{
srand(time(nullptr));
}
void Register(task_t t)
{
_tasks.push_back(t);
}
int Code()
{
return rand() % _tasks.size();
}
void Excute(int code)
{
if (code >= 0 && code < _tasks.size())
{
_tasks[code]();
}
}
private:
std::vector<task_t> _tasks;
};
这里对整个项目代码做一定的解释。
1. 创建进程池对象pp,在ProcessPool的构造函数中执行需要创建的进程池有gdefalutnum个信道,然后将待执行任务注册到TaskManager对象中。
2. 调用Start函数启动进程池,启动的过程就是循环创建管道文件和子进程的过程,创建的时候同时关闭各个进程中不需要的文件描述符。
3. 循环给子进程派发任务,循环执行run函数,在Run函数中,首先随机选择一个任务码,然后轮询选择一个子进程,将任务码交给子进程后,子进程通过read函数从rfd文件描述符指向管道文件的读端读取任务码,并进行解析指向任务。这里在父进程中的写入相对于子进程读取来说是较慢的,所以当父进程未写入的时候,子进程会阻塞在read函数中等待父进程进行写入。
4. 循环结束之后,调用Stop函数,循环关闭父进程中指向管道文件的文件描述符,然后循环调用waitpid函数对子进程进行回收。
执行上述代码会得到如下结果,下图表示结果的一部分。
可以看到,这里循环分配任务结束之后,父进程关闭了指向管道文件写端的文件描述符,然后子进程从后到前依次退出,最后回收子进程程序结束。
3.7.2 demo代码的一个bug
从上述结果可以看到,通信信道是依次关闭的,子进程也是依次回收的,但是子进程退出的顺序是反的。为什么会出现这种情况,可以从下图进行分析。
当任务执行完毕之后,前面的管道文件的写端,除了会被父进程的文件描述符指向外,都会被它的弟弟进程的文件描述符指向,所以,当父进程关闭指向管道文件的文件描述符之后,前面四个管道文件都有弟弟进程的文件描述符指向,所以进程还是会阻塞在read函数那等待输入。
但是最后一个管道文件的写端则没有进程的文件描述符指向,所以最后一个子进程的read函数返回0,然后子进程退出,当该子进程退出之后,它的上一个子进程对应的管道文件的写端也没有进程指向,所以上一个子进程也接着退出,这样依次,从后到前,所以子进程依次退出。
因为上述代码中,文件描述符的关闭和子进程的回收是分开写的,当合在一起写的时候,修改成下述代码,就是关闭父进程指向管道文件1写端的文件描述符立即回收子进程1,这样的话,子进程1就会阻塞在read函数处,而父进程也会阻塞在waitpid函数处。
void StopAndWait()
{
for (auto &channel : _channels)
{
channel.Close();
std::cout << "关闭:" << channel.Name() << std::endl;
channel.Wait();
std::cout << "回收:" << channel.Name() << std::endl;
}
}
这就会出现阻塞的情况。
如果要将关闭文件描述符和回收子进程合在一起写,并且不出现上述阻塞的情况,有两种方法:
1. 和原始的代码一样,从后到前对通道进行关闭并回收子进程。
2. 在子进程创建的时候就对其子进程文件描述符表中指向哥哥进程对应的管道文件写端的文件描述符关闭。
注:子进程的_cm也是继承自父进程,当父进程对_cm进行插入时,发生写时拷贝,所以后面的子进程中的_cm里面,只有指向哥哥进程对应的管道文件写端的文件描述符,所以可以实现在创建子进程的时候就可以进行关闭,结果如下。
4. 命名管道
匿名管道的一个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信。如果想在不相关的进程之间交换数据,可以使用FIFO文件来做这项工作,它经常被称为命名管道。命名管道是一种特殊类型的文件。只需要被打开,文件缓冲区的数据并不会刷新到磁盘中。
4.1 创建命名管道及删除
1. 命令行创建管道文件:
mkfifo filename
删除管道文件可以使用如下两个命令:
rm filename
unlink filename
2. 程序里创建管道文件,相关函数如下:
#include <sys/types.h>
#Include <sys/stat.h>
int mkfifo(const char *filename, mode_t mode);
#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
int main()
{
mkfifo("pipe_2", 0644);
return 0;
}
4.2 匿名管道和命名管道的区别
1. 匿名管道由pipe函数创建并打开;命名管道由mkfifo函数创建,打开用open。
2. FIFO(命名管道)与pipe(匿名管道)之间唯一的区别在它们创建与打开的方式不同,它们的使用都是相同的。
4.3 命名管道的打开规则
1. 如果当前打开操作是为读而打开FIFO时:阻塞直到有相应进程为写⽽打开该FIFO。
2. 如果当前打开操作是为写而打开FIFO时:阻塞直到有相应进程为读⽽打开该FIFO。
4.4 命名管道的实例
4.4.1 用命令管道实现文件拷贝
//read.cc -- 读取文件,写入命名管道中
#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
// 封装一下错误退出的宏
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while(0)
// 读取文件,写入命令管道
int main()
{
// 1. 创建命名管道文件fifo
int ret = mkfifo("fifo", 0664);
if (ret < 0)
{
ERR_EXIT("mkfifo");
}
// 2. 以读方式打开待读文件abc
int infd = open("abc", O_RDONLY);
if (infd == -1)
ERR_EXIT("open");
// 3. 以写方式打开命名管道fifo
int outfd;
outfd = open("fifo", O_WRONLY);
if (outfd == -1)
ERR_EXIT("open");
// 4.从待读文件中读取数据,写入管道文件中
char buf[1024];
int n;
while((n = read(infd, buf, 1024)) > 0)
{
write(outfd, buf, n);
}
close(infd);
close(outfd);
return 0;
}
//write.cc -- 读取管道文件,写入目标文件中
#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
}while(0)
// 从管道文件中读数据,写入到目标文件中
int main()
{
// 1. 以读的方式打开管道文件fifo
int infd = open("fifo", O_RDONLY, 0644);
if (infd == -1)
ERR_EXIT("open");
// 2. 以写方式打开目标文件abc.back
int outfd = open("abc.back", O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (outfd == -1)
ERR_EXIT("open");
// 3.从管道文件fifo中读数据并写入到目标文件中
char buf[1024];
int n;
while((n = read(infd, buf, 1024)) > 0)
{
write(outfd, buf, n);
}
close(infd);
close(outfd);
// 删除管道文件 -- 系统调用
unlink("fifo");
return 0;
}
上述代码通过命名管道实现了一个从abc文件中读取内容,然后写入到abc.back中的demo。
4.4.2 用命名管道实现 server 和 client 通信
server作为读端,将client写入到管道文件的数据读出,显示到显示器上。
// comm.hpp -- 用于描述管道文件的类和用于描述对管道文件操作的类
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <string>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#define PATH "."
#define NAME "fifo"
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while (0)
class NamedFifo
{
public:
NamedFifo(const std::string path, const std::string name)
: _path(path), _name(name)
{
// 创建管道文件
_filename = _path + "/" + _name;
umask(0);
int n = mkfifo(_filename.c_str(), 0664);
if (n < 0)
ERR_EXIT("mkfifo");
else
{
std::cout << "mkfifo success" << std::endl;
}
}
~NamedFifo()
{
int n = unlink(_filename.c_str());
// 删除成功
if (n == 0)
{
ERR_EXIT("unlink");
}
else // 删除失败
{
std::cout << "remove fifo failed" << std::endl;
}
}
private:
std::string _path;
std::string _name;
std::string _filename;
};
class FileOperation
{
public:
FileOperation(const std::string path, const std::string name)
: _path(path), _name(name), _fd(-1)
{
_filename = _path + "/" + _name;
}
void OpenForRead()
{
_fd = open(_filename.c_str(), O_RDONLY);
if (_fd < 0)
ERR_EXIT("open");
else if (_fd > 0)
{
std::cout << "open success" << std::endl;
}
else
{
}
}
void OpenForWrite()
{
_fd = open(_filename.c_str(), O_WRONLY);
if (_fd < 0)
ERR_EXIT("open");
else if (_fd > 0)
{
std::cout << "open success" << std::endl;
}
else
{
}
}
// 服务端进行读
void Read()
{
char buf[1024];
int n;
while (true)
{
n = read(_fd, buf, sizeof(buf) - 1);
buf[n] = 0;
if (n > 0)
{
std::cout << "Client say # " << buf << std::endl;
}
else if (n == 0)
{
std::cout << "Client quit! me too !" << std::endl;
break;
}
else
{
// TODO
}
}
}
// 客户端进行写
void Write()
{
std::string message;
while (true)
{
std::cin >> message;
int n = 0;
write(_fd, message.c_str(), message.size());
}
}
void Close()
{
close(_fd);
}
private:
std::string _path;
std::string _name;
std::string _filename;
int _fd;
};
// server.cc
#include "comm.hpp"
int main()
{
NamedFifo nf(PATH, NAME);
FileOperation fo(PATH, NAME);
fo.OpenForRead();
fo.Read();
fo.Close();
return 0;
}
// client.cc
#include "comm.hpp"
int main()
{
FileOperation fo(PATH, NAME);
fo.OpenForWrite();
fo.Write();
fo.Close();
return 0;
}
5. system V共享内存
共享内存区是最快的IPC形式。一旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到内核,等同于进程不再通过执行进入内核的系统调用来传递彼此的数据。
共享内存,消息队列和信号量三种IPC资源的生命周期随内核的生命周期,IPC资源必须调用命令或者调用系统调用进行删除,否则不会自动清除,除非重启。
5.1 共享内存的原理
使用共享内存在两个进程之间传递数据如下图所示。在物理内存空间开辟一块内存空间,将这块内存空间分别映射到两个进程虚拟内存空间的共享区中,进程就可以通过访问自己进程的虚拟地址空间进行进程间通信了。
5.2 共享内存的内核数据结构
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 */
};
5.3 共享内存函数
shgmet函数
头文件:
#include <sys/ipc.h>
#include <sys/shm.h>
功能:⽤来创建共享内存
原型:
int shmget(key_t key, size_t size, int shmflg);
参数:
key:这个共享内存段名字
size:共享内存⼤⼩
shmflg:由九个权限标志构成,它们的⽤法和创建⽂件时使⽤的mode模式标志是⼀样的
取值为IPC_CREAT:共享内存不存在,创建并返回;共享内存已存在,获取并返回。
取值为IPC_CREAT | IPC_EXCL:共享内存不存在,创建并返回;共享内存已存在,出错返回。
返回值:
成功返回⼀个⾮负整数,即该共享内存段的标识码;失败返回-1
ftok函数
头文件:
#include <sys/types.h>
#include <sys/pic.h>
功能:
用于生成一个唯一标识的key值。
原型:
key_t ftok(const char *pathname, int proj_id);
参数:
使用传入的pathname和proj_id创建一个key值。
返回值:
创建成功,key值被返回,创建失败返回-1.
注:ftok函数常用于生成shmget函数中的key参数。
key是共享内存唯一性的标志,这个参数由用户传入,当两个进程要使用同一块共享内存进行通信时,该参数填为一样的值。
当shmfig参数的权限标志为IPC_CREAT | IPC_EXCL时,等同于创建一个全新的共享内存。
当shmfig参数的权限标志只有IPC_CREAT时,用于获取已有的共享内存。
shmat函数
头文件:
#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
shmaddr为NULL,内核自动选择一个地址。shmaddr不为NULL且shmflg⽆SHM_RND记,则以shmaddr为连接地址。shmaddr不为NULL且shmflg设置了SHM_RND标记,则连接的地址会⾃动向下调整为SHMLBA的整数倍。公式:shmaddr - (shmaddr % SHMLBA)。 shmflg=SHM_RDONLY,表⽰连接操作⽤来只读共享内存。
一般情况下 shmaddr 参数为 nullptr,shmflg 参数为 0。
shmdt函数
头文件:
#include <sys/types.h>
#include <sys/shm.h>
功能:
将共享内存段与当前进程脱离
原型:
int shmdt(const void *shmaddr);
参数:
shmaddr: 由shmat所返回的指针
返回值:
成功返回0;失败返回-1
注意:将共享内存段与当前进程脱离不等于删除共享内存段
shmctl函数
头文件:
#include <sys/types.h>
#include <sys/shm.h>
功能:
⽤于控制或删除共享内存
原型:
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数:
shmid:由shmget返回的共享内存标识码
cmd:将要采取的动作(有三个可取值)
buf:指向⼀个保存着共享内存的模式状态和访问权限的数据结构
返回值:
成功返回0;失败返回-1
ipcs -m 命令,查看当前系统中共享内存的信息。
ipcrm -m [shmid] 命名,删除共享内存标识为shmid的共享内存。
5.4 共享内存的实例
该实例使用共享内存技术进行进程间通信,因为共享内存没有进⾏同步与互斥! 共享内存缺乏访问控制!会带来并发问题,所以使用命名管道对其进程间做一定的访问控制。
//Comm.hpp
#pragma once
#include <cstdio>
#include <stdlib.h>
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while(0)
//Fifo.hpp
#pragma once
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <string>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include "Comm.hpp"
#define PATH "."
#define NAME "fifo"
class NamedFifo
{
public:
NamedFifo(const std::string path, const std::string name)
: _path(path), _name(name)
{
// 创建管道文件
_filename = _path + "/" + _name;
umask(0);
int n = mkfifo(_filename.c_str(), 0664);
if (n < 0)
ERR_EXIT("mkfifo");
else
{
std::cout << "mkfifo success" << std::endl;
}
}
~NamedFifo()
{
int n = unlink(_filename.c_str());
// 删除成功
if (n == 0)
{
perror("unlink");
// ERR_EXIT("unlink");
// 如果使用ERR_EXIT进行退出,服务端会直接调用exit函数进行退出,而不会调用shm的析构函数
}
else // 删除失败
{
std::cout << "remove fifo failed" << std::endl;
}
}
private:
std::string _path;
std::string _name;
std::string _filename;
};
class FileOperation
{
public:
FileOperation(const std::string path, const std::string name)
: _path(path), _name(name), _fd(-1)
{
_filename = _path + "/" + _name;
}
void OpenForRead()
{
_fd = open(_filename.c_str(), O_RDONLY);
if (_fd < 0)
ERR_EXIT("open");
else if (_fd > 0)
{
std::cout << "open success" << std::endl;
}
}
void OpenForWrite()
{
_fd = open(_filename.c_str(), O_WRONLY);
if (_fd < 0)
ERR_EXIT("open");
else if (_fd > 0)
{
std::cout << "open success" << std::endl;
}
}
// 服务端进行读
void Read()
{
char buf[1024];
int n;
while (true)
{
n = read(_fd, buf, sizeof(buf) - 1);
buf[n] = 0;
if (n > 0)
{
std::cout << "Client say # " << buf << std::endl;
}
else if (n == 0)
{
std::cout << "Client quit! me too !" << std::endl;
break;
}
else
{
// TODO
}
}
}
// 客户端进行写
void Write()
{
std::string message;
while (true)
{
std::cin >> message;
int n = 0;
write(_fd, message.c_str(), message.size());
}
}
bool Wait()
{
char c;
int number = read(_fd, &c, 1);
if (number > 0)
{
return true;
}
return false;
}
void Wakeup()
{
char c = 'c';
write(_fd, &c, 1);
}
void Close()
{
close(_fd);
}
private:
std::string _path;
std::string _name;
std::string _filename;
int _fd;
};
//Shm.hpp
#pragma once
#include <iostream>
#include <cstdio>
#include <string>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include "Comm.hpp"
#include <unistd.h>
const int gdefaultid = -1;
const int gsize = 4096;
const std::string pathname = ".";
const int projid = 0x66;
const int gmode = 0666;
#define CREATER "creater"
#define USER "user"
// 描述共享内存的类
class Shm
{
private:
void CreateHelper(int flg)
{
key_t _key = ftok(pathname.c_str(), projid);
if (_key < 0)
{
ERR_EXIT("ftok");
}
printf("key: 0x%x\n", _key);
_shmid = shmget(_key, _size, flg);
if (_shmid < 0)
{
ERR_EXIT("shmget");
}
std::cout << "shmid: " << _shmid << std::endl;
}
// 创建共享内存资源
void Create()
{
CreateHelper(IPC_CREAT | IPC_EXCL | gmode);
}
// 关联共享内存资源
void Attach()
{
_start_mem = shmat(_shmid, nullptr, 0);
if ((long long)_start_mem < 0)
{
ERR_EXIT("shmat");
}
std::cout << "attch success" << std::endl;
}
// 取消关联
void Detach()
{
int n = shmdt(_start_mem);
if (n == 0)
{
printf("detach success\n");
}
}
// 获取共享内存资源
void Get()
{
CreateHelper(IPC_CREAT);
}
// 释放共享内存资源
void Destroy()
{
shmctl(_shmid, IPC_RMID, nullptr);
}
public:
// pathname 和 projid 用于构建 key 值,usertype用于标记使用者身份
Shm(const std::string &pathname, const int projid, const std::string &usertype)
: _shmid(gdefaultid)
, _size(gsize)
, _start_mem(nullptr)
, _user_type(usertype)
{
if (_user_type == CREATER)
{
Create();
}
else if (_user_type == USER)
{
Get();
}
else
{
}
Attach();
}
// 获取起始虚拟地址
void* VirtualAddr()
{
printf("virtual address: %p\n", _start_mem);
return _start_mem;
}
// 获取共享内存大小
int size()
{
return _size;
}
// 获取共享内存信息
void Attr()
{
struct shmid_ds ds;
int n = shmctl(_shmid, IPC_STAT, &ds);
printf("shm_segsz: %ld\n", ds.shm_segsz);
printf("key: 0x%x\n", ds.shm_perm.__key);
}
// 取消共享内存的关联对象,如果使用者身份为创建者,则释放共享内存资源
~Shm()
{
std::cout << _user_type << std::endl;
if (_user_type == CREATER)
{
Destroy();
}
}
private:
int _shmid; //共享内存id号
key_t _key; //共享内存的key值,标识唯一性
int _size; //共享内存的空间大小
void *_start_mem; //共享内存的起始虚拟地址
std::string _user_type; //共享内存的使用者身份
};
//server.cc
#include "Comm.hpp"
#include "Shm.hpp"
#include "Fifo.hpp"
int main()
{
Shm shm(pathname, projid, CREATER);
// shm.Attr();
// shm.VirtualAddr();
NamedFifo nf(PATH, NAME);
FileOperation fo(PATH, NAME);
fo.OpenForRead();
// 使用命名管道的方式,让数据的读取按照特定的格式读取
char *mem = (char *)shm.VirtualAddr();
while (true)
{
if (fo.Wait())
{
printf("%s\n", mem);
}
else
{
break;
}
}
fo.Close();
return 0;
}
//client.cc
#include "Comm.hpp"
#include "Shm.hpp"
#include "Fifo.hpp"
int main()
{
Shm shm(pathname, projid, USER);
FileOperation fo(PATH, NAME);
fo.OpenForWrite();
char* mem = (char*)shm.VirtualAddr();
int index = 0;
for (char c = 'A'; c <= 'Z'; c++, index+=2)
{
sleep(1);
mem[index] = c;
sleep(1);
mem[index+1] = c;
fo.Wakeup();
}
fo.Close();
return 0;
}
这里对上述代码进行解释,上述代码分为服务端和客户端。当服务端启动的时候,创建共享内存并将共享内存和该进程进行关联,创建命名管道并以读方式打开命名管道,此时服务端进程会阻塞在Wait()函数的read()函数处,等待客户端启动;当客户端启动时,获取和服务端相同的共享内存资源并关联该共享内存,并且以写方式打开命名管道,然后向共享内存中依次写入AA BB CC ...,当同一个字母写了两次后,向命名管道中写入一个标记字符c,唤醒服务端,服务端收到该进程发送的标记字符,结束等待,从共享内存资源中读取数据。实验结果如下:
当通讯结束之后,先析构命名管道对象,然后调用Shm的析构函数,删除共享内存资源。
6. system V消息队列(了解即可)
消息队列:提供了一种一个进程给另一个进程发送有类型数据块的方式。每个数据块都被认为是有一个类型,接收者进程接收的数据块可以有不同的类型值。
7. system V信号量(了解即可)
7.1 并发编程的概念铺垫
1. 进程间通讯的前提条件:使不同的进程看到同一份资源。
2. 数据不一致:当不同的进程访问同一份资源的时候,需要有“保护”机制。例如让读端以某一种形式对写端的传输的数据进行读取。如上述的共享内存,当两个进程之间没有“保护”机制的时候,写端一直向共享内存写入数据,而读端没有按照 AA BB..的形式读取,就会造成数据不一致的情况。
3. 数据不一致产生情况:访问共享资源的代码段没有进行约束或者保护,意思就是两个进程对同一份资源访问的时候没有一定的顺序性。
4. 临界区:当一份代码中某一段代码会对共享资源进行访问时,该代码段叫做临界区。同理没有对共享资源进行访问的代码段,叫做非临界区。
5. 临界资源:系统中某些资源一次只允许一个进程访问,称这样的资源为临界资源或互斥资源。
6. 同步:多个执行流访问临界(共享)资源时,具有一定的顺序性叫做同步。
7. 互斥:任何一个时刻,至于性能一个执行流访问临界资源,叫做互斥。同步和互斥是最常见的对共享资源进行保护的方式。
下面是一份代码的示意图,没有对临界资源访问的部分叫做非临界区,对临界资源访问的部分叫做临界区,通过加锁的方式,可以对共享资源进行保护。对共享资源进行保护,本质是对访问共享资源的代码进行保护。
7.2 信号量
信号量可以理解为一个计数器。当系统中有一份共享资源时,整份资源不进行整体使用,而是进行分块使用。当外界进程进行资源申请的时候,计数器--,称为P操作,当资源被释放时,计数器++,称为V操作。
信号量的本质是对资源的预定机制,可以类比为订电影票。
信号量的作用是用于保护临界区。
8. 内核是如何组织管理IPC资源的
在Linux内核中,System V标准的三种资源 -- 共享内存、消息队列、信号量,是进行统一管理的。
在内核中,有一个 struct ipc_ids sem_ids 结构体,里面包含了一指针 *entries,指向一个 ipc_id_ary 结构体,而 ipc_id_ary 结构体中维护了一个柔性数组,数组的每一个元素存储着指向上述申请的各种资源的指针。
问题是,为什么不同的结构体能被相同的指针所指向?原因是,三种共享资源的结构体中,第一个成员对象都是 kern_ipc_perm 结构体,所以在柔性数组中,存放的是 kern_ipc_perm类型的指针,使其能够指向不同类型的结构体。本质上,是C语言的一种多态形式。当需要进行访问将指针强转为对应类型即可进行访问。