1.基本概念
1.进程间通信是什么?
进程间通信(Inter-Process Communication,IPC)是指在不同进程之间传递数据或信号的机制。由于进程是操作系统中独立执行的单元,它们拥有各自独立的内存空间,因此不能直接访问彼此的内存。
2.为什么要有进程间通信
- 数据传输:一个进程需要将它的数据发送给另一个进程
- 资源共享:多个进程之间共享同样的资源
- 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)
- 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变
3.如何实现进程间通信
a. 进程间通信的本质: 让不同的进程看到同一份"资源"
b. “资源是什么”:是一段特定形式的内存空间
c. 这个"资源"谁提供:一般是操作系统
为什么不是两个进程中的一个呢? 假设其中一个进程提供资源,那这份资源属于这个进程,当其它进程访问时,实际上破坏了进程的独立性,所以需要第三方空间(os)
所以当进程访问这段内存空间,进行通信时,本质就是访问os,os为了安全起见,提供了一系列调用接口,包括但不限于"资源"的创建,使用,释放
一般os会有一个独立的通信模块,隶属于文件系统,叫做IPC通信模块.进程间通信是有标准的,主要有System V IPC(主要用于本机内部)和POSIX IPC (主要用于网络)
e. 基于文件的通信方式:管道
2.匿名管道
2.1 什么是匿名管道
- 管道是Unix中最古老的进程间通信的形式
- 我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”
- 管道实际就是一个内存级别的文件,具体见2.2
2.2原理
2.3站在内核的角度理解匿名管道
这种单向通信的方式叫做管道.
当我们想要双向通信时,可以使用两个管道
注意:上面两个进程是父子关系,如果两个进程没有任何关系,则不能用上面我们讲的原理进行通信
必须是父子关系,兄弟关系…(进程之间需要有血缘关系,一般进程间通信常用于父子关系)
至此,进程之间还是没有进行通信,我们只是在建立通信信道
2.4接口&&编码实现
看下linux的官方手册
NAME
pipe, pipe2 - create pipe
SYNOPSIS
#include <unistd.h>
int pipe(int pipefd[2]); /* 这是一个输出型参数,目的是把文件的文件描述符数字(fd)带出来,供用户使用
一般来说, pipefd[0]:读下标
pipefd[1]:写下标
记忆技巧: 0是嘴巴,所以是读,1是笔,所以是写*/
RETURN VALUE
On success, zero is returned. On error, -1 is returned, and errno is set appropriately.
感受输出型参数
int pipefd[2] = {0};
int ret = pipe(pipefd);
if(ret = -1) return 1;
cout << "pipefd[0]:" << pipefd[0] << " pipefd[1]:" << pipefd[1] << endl;
子进程写,父进程读
#define NUM 1024
using namespace std;
void WriteTo(int wfd)
{
// 构建发送字符串
string s = "I am child";
pid_t self = getpid();
int num = 0;
char buffer[NUM];
while(true) {
buffer[0] = 0; // 字符串清空,目的是为了告诉阅读代码的人,我把这个数组当成字符串了,没有什么实际意义,去掉也可以
snprintf(buffer, sizeof(buffer), "%s-%d-%d\n", s.c_str(), self, num++);
// cout<<buffer;
// 发送/写给父进程
write(wfd, buffer, strlen(buffer));
sleep(1);
}
}
void ReadFrom(int rfd)
{
char buffer[NUM] = {0};
while(true) {
size_t n = read(rfd, buffer, sizeof(buffer)); // 这里是sizeof,因为read的时候不知道文件有多少数据,但是我们接收数据的大小就只有数组那么大,所以要读最大值
if(n > 0) {
buffer[n] = 0; // 0 == '\0'
cout << "father[" << getpid() << "] get a message: " << buffer;
}
}
}
int main()
{
int pipefd[2] = {0};
int ret = pipe(pipefd);
if(ret == -1) return 1;
// cout << "pipefd[0]:" << pipefd[0] << " pipefd[1]:" << pipefd[1] << endl;
pid_t id = fork();
if(id == -1) return 2;
// 我们想让子进程写,父进程读
if(id == 0) {
// 子进程,关闭读的文件描述符
close(pipefd[0]);
// IPC code
WriteTo(pipefd[1]);
// 关闭资源
close(pipefd[1]);
exit(0);
}
// 父进程,关闭写的文件描述符
close(pipefd[1]);
// IPC code
ReadFrom(pipefd[0]);
// 进程等待
pid_t wRet = waitpid(id, nullptr, 0);
if(wRet == -1) return 3;
close(pipefd[0]);
return 0;
}
2.5 匿名管道的特征
- 具有血缘关系的进程才能进行进程间通信
- 管道只能单向通信
- 父子进程是会进程协同, 同步互斥的, 目的是为了保护管道文件的数据安全
- 管道是面向字节流的
- 管道是基于文件的, 而文件的生命周期是随进程的, 即进程结束, 管道也会被关闭
2.6 匿名管道中的4种情况
- 读写端正常,管道如果为空,则读端阻塞
- 读写端正常,管道如果被写满,则写端阻塞(第1点和第2点对应2.5的第3点)
- 读端正常读,写端关闭,读端就会读到0, 表明读到了文件(pipe)末尾, 不会被阻塞
- 写端正常写,读端关闭了, 操作系统会杀掉正在写入的进程, 通过信号杀掉该进程
// 验证第4点
void WriteTo(int wfd)
{
// 构建发送字符串
string s = "I am child";
pid_t self = getpid();
int num = 0;
char buffer[NUM];
while(true) {
buffer[0] = 0; // 字符串清空,目的是为了告诉阅读代码的人,我把这个数组当成字符串了,没有什么实际意义,去掉也可以
snprintf(buffer, sizeof(buffer), "%s-%d-%d\n", s.c_str(), self, num++);
// cout<<buffer;
// 发送/写给父进程
write(wfd, buffer, strlen(buffer));
sleep(1);
// write(wfd, "test\n", 5);
// num++;
// if(num == 5)
// break;
}
}
void ReadFrom(int rfd)
{
char buffer[NUM] = {0};
int cnt = 0;
while(true) {
// sleep(1);
size_t n = read(rfd, buffer, sizeof(buffer)); // 这里是sizeof,因为read的时候不知道文件有多少数据,但是我们接收数据的大小就只有数组那么大,所以要读最大值
if(n > 0) {
buffer[n] = 0; // 0 == '\0'
cout << "father[" << getpid() << "] get a message: " << buffer;
cnt++;
if(cnt == 5) break;
} else if(n == 0) {
// 子进程读到0, 表示读到文件末尾
cout << "father read file done";
break;
} else {
// 其他代码
break;
}
}
}
int main()
{
int pipefd[2] = {0};
int ret = pipe(pipefd);
if(ret == -1) return 1;
// cout << "pipefd[0]:" << pipefd[0] << " pipefd[1]:" << pipefd[1] << endl;
pid_t id = fork();
if(id == -1) return 2;
// 我们想让子进程写,父进程读
if(id == 0) {
// 子进程,关闭读的文件描述符
close(pipefd[0]);
// IPC code
WriteTo(pipefd[1]);
// 关闭资源
close(pipefd[1]);
exit(0);
}
// 父进程,关闭写的文件描述符
close(pipefd[1]);
// IPC code
ReadFrom(pipefd[0]);
close(pipefd[0]);
cout<<"father closed read fd:" << pipefd[0] << endl;
sleep(5); // 目的是为了观察僵尸进程
// 进程等待
int status;
pid_t wRet = waitpid(id, &status, 0);
if(wRet == -1) return 3;
// 获取信号和退出码
cout<<"exit code:" << ((status>>8) & 0xff) << " exit signal:" << (status & 0x7f) << endl;
sleep(5);
cout<<"father quit" << endl;
return 0;
}
可以看到发出的是13号信号
2.7 匿名管道的大小
使用ulimate -a
命令查看
大小为4kb,验证一下
将2.4中第二个代码中的WriteTo改写为下面的程序, 父进程阻塞等待
void WriteTo(int wfd)
{
while(true) {
write(wfd, "a", 1);
num++;
cout << num << endl;
}
}
发现结果为65536byte,64kb,为什么会这样呢?看官方的文档,使用man 7 pipe命令
Pipe capacity
A pipe has a limited capacity. If the pipe is full, then a write(2) will block or fail, depending on whether the O_NONBLOCK flag is set (see below). Different implementations have different limits for the pipe capacity. Applications should not rely on a particular capacity: an application should be designed so that a reading process consumes data as soon as it is available, so that a writing process does not remain blocked.
In Linux versions before 2.6.11, the capacity of a pipe was the same as the system page size (e.g., 4096 bytes on i386). Since Linux 2.6.11, the pipe capacity
is 65536 bytes.
管道容量管道的容量有限。如果管道已满,则write(2)将阻塞或失败,这取决于是否设置了O_NONBLOCK标志(见下文)。不同的实现对管道容量有不同的限制。应用程序不应该依赖于特定的容量:应用程序的设计应该使读进程在数据可用时立即消耗数据,这样写进程就不会一直阻塞。
在2.6.11之前的Linux版本中,管道的容量与系统页面大小相同(例如:(在i386上为4096字节)。从Linux 2.6.11开始,管道容量为65536字节。
PIPE_BUF
POSIX.1-2001 says that write(2)s of less than PIPE_BUF bytes must be atomic: the output data is written to the pipe as a contiguous sequence. Writes of more than PIPE_BUF bytes may be nonatomic: the kernel may interleave the data with data written by other processes. POSIX.1-2001 requires PIPE_BUF to be at least 512 bytes. (On Linux, PIPE_BUF is 4096 bytes.) The precise semantics depend on whether the file descriptor is nonblocking (O_NONBLOCK), whether there are multiple writers to the pipe, and on n, the number of bytes to be written:
POSIX.1-2001规定,少于PIPE_BUF字节的write(2)必须是原子的:输出数据将作为连续序列写入管道。
超过PIPE_BUF字节的写入可能是非原子的:内核可能会将这些数据与其他进程写入的数据交织在一起。
POSIX.1-2001要求PIPE_BUF至少为512字节。(在Linux上,PIPE_BUF是4096字节。)精确的语义取决于文件描述符是否是非阻塞的(O_NONBLOCK),是否有多个写入器到管道,以及n,要写入的字节数:
所以,我们可以将ulimate -a
中管道的大小看做PIPE_BUF的大小
2.8 使用匿名管道实现一个进程池
// ProcessPool.cpp
#include "Task.hpp"
const int processNum = 10;
const int taskNum = 8;
const int N = 5;
// 先描述
struct Channal
{
Channal(int cmdFd, pid_t slaverId, const string &name)
: _cmdFd(cmdFd), _slaverId(slaverId), _name(name)
{
}
int _cmdFd; // 发送任务的文件描述符
pid_t _slaverId; // 子进程的pid
string _name; // 子进程的名字,方便我们打印日志
};
// 再组织
vector<Channal> channals;
vector<task_t> tasks;
void Slaver(int rfd)
{
while(true) {
int cmdCode = 0;
// 由于做了dup2,所以可以直接从0里读, 规定一次读4字节
int n = read(0, &cmdCode, sizeof(int));
if(n == sizeof(int)) {
// 执行cmd对应的任务列表
cout<<"child say@ "<< getpid() << " has been received" <<" cmdCode:" << cmdCode<< endl;
if(cmdCode >= 0 && cmdCode < taskNum) tasks[cmdCode]();
}
else if(n == 0) break;
sleep(1);
}
}
void PrintInfo()
{
for (const auto &it : channals) {
cout << it._cmdFd << " " << it._slaverId << " " << it._name << endl;
}
}
/*
我们想让子进程读,父进程写
*/
void InitProcessPool()
{
for (int i = 0; i < processNum; ++i) {
// 创建管道
int pipeFd[2];
int n = pipe(pipeFd);
assert(!(n==-1));
pid_t id = fork();
if (id == 0) {
// child
close(pipeFd[1]);
// 关闭标准输入,用pipeFd[0]替换
dup2(pipeFd[0], 0);
Slaver(pipeFd[0]);
cout<<"process " << getpid() << " quit" << endl;
exit(0);
}
// father
close(pipeFd[0]);
string name = "process-" + to_string(i);
channals.push_back(Channal(pipeFd[1], id, name));
}
}
// 随机选择进程
void CtrlSlavers1()
{
for(int i = 1; i <= N; ++i) {
// a.选择任务
int cmdCode = rand() % taskNum;
// b.选择进程
int processPos = rand() % processNum;
// c.发送任务
cout << "father say@ cmdCode " << cmdCode << " has been send to " <<
channals[processPos]._name << " pid is " << channals[processPos]._slaverId << endl;
write(channals[processPos]._cmdFd, &cmdCode, sizeof(int));
sleep(1);
}
}
// 轮询选择进程
void CtrlSlavers2()
{
// a. 选择进程
int which = 0;
for(int i = 1; i <= N; ++i) {
// b.选择任务
int cmdCode = rand() % taskNum;
// c.发送任务
cout << "father say@ cmdCode " << cmdCode << " has been send to " <<
channals[which]._name << " pid is " << channals[which]._slaverId << endl;
write(channals[which]._cmdFd, &cmdCode, sizeof(int));
which++;
which %= processNum;
sleep(1);
}
}
void QuitProcess()
{
// 关闭写端 version1
for(const auto& it : channals) close(it._cmdFd);
sleep(5);
for(const auto& it : channals) {
// wait子进程
waitpid(it._slaverId, nullptr, 0);
}
sleep(5);
}
int main()
{
srand(time(nullptr));
LoadTasks(&tasks);
// 1.初始化
InitProcessPool();
// PrintInfo();
// 2.开始控制子进程
CtrlSlavers2();
// 3.清理收尾
QuitProcess();
return 0;
}
// Task.hpp
#include <unistd.h>
#include <iostream>
#include <string>
#include <vector>
#include <fcntl.h>
#include <assert.h>
#include <ctime>
#include <sys/types.h>
#include <sys/wait.h>
using namespace std;
// 一个无返回值,无参数的函数指针
typedef void (*task_t)();
void Task1()
{
cout << "任务1..." << endl;
}
void Task2()
{
cout << "任务2..." << endl;
}
void Task3()
{
cout << "任务3..." << endl;
}
void Task4()
{
cout << "任务4..." << endl;
}
void Task5()
{
cout << "任务5..." << endl;
}
void Task6()
{
cout << "任务6..." << endl;
}
void Task7()
{
cout << "任务7..." << endl;
}
void Task8()
{
cout << "任务8..." << endl;
}
// 将任务添加到tasks中
void LoadTasks(vector<task_t> * tasks)
{
tasks->push_back(Task1);
tasks->push_back(Task2);
tasks->push_back(Task3);
tasks->push_back(Task4);
tasks->push_back(Task5);
tasks->push_back(Task6);
tasks->push_back(Task7);
tasks->push_back(Task8);
}
上面的代码有点小小的问题, 看下面的图
这样,一个管道就会有多个写端(随着子进程数量的增多而增加), 这不是我们想要的
当我们把之前代码的 void QuitProcess()
更改为下面的代码后, 程序退出时就会卡住
void QuitProcess()
{
// 关闭写端
// test
for(const auto& it : channals) {
close(it._cmdFd);
// wait子进程
waitpid(it._slaverId, nullptr, 0);
}
}
因为我们看上面的图可以知道, 当我们只关闭一个fd时, 其实写端并没有全部关闭, 还有好多子进程指向该管道的写端, 子进程不会break, 依然会阻塞等待
解决方案1: 由于最后一个子进程只有一个写端, 所以我们可以倒着close
// version2
int last = channals.size() - 1;
for(int i = last; i >= 0; --i) {
close(channals[i]._cmdFd);
waitpid(channals[i]._slaverId, nullptr, 0);
}
解决方案2: 非阻塞等待
// version3
for(const auto& it : channals) {
close(it._cmdFd);
// wait子进程
waitpid(it._slaverId, nullptr, WNOHANG);
}
解决方案3: 更改void InitProcessPool()
中的代码, 确保每一个子进程只有一个写端
void InitProcessPool()
{
vector<int> oldFds; // 记录子进程管道会重复写的fd
for (int i = 0; i < processNum; ++i) {
// 创建管道
int pipeFd[2];
int n = pipe(pipeFd);
assert(!(n==-1));
pid_t id = fork();
if (id == 0) { // child
// 关闭从父进程继承下来的,指向上一个管道的写端fd
for(auto fd : oldFds) close(fd);
close(pipeFd[1]);
// 关闭标准输入,用pipeFd[0]替换
dup2(pipeFd[0], 0);
Slaver(pipeFd[0]);
cout<<"process " << getpid() << " quit" << endl;
exit(0);
}
// father
close(pipeFd[0]);
string name = "process-" + to_string(i);
channals.push_back(Channal(pipeFd[1], id, name));
oldFds.push_back(pipeFd[1]);
}
}
输出一下, 更直观一些
// 关闭从父进程继承下来的,指向上一个管道的写端fd
cout << "child close history: ";
for(auto fd : oldFds){
cout << fd << ' ';
close(fd);
}
cout << endl;
已将N设置为2, 所以只跑了2个任务就退出了
3. 命名管道
- 管道应用的一个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信。
- 如果我们想在不相关的进程之间交换数据,可以使用FIFO文件来做这项工作,它经常被称为命名管道。
- 命名管道是一种特殊类型的文件
3.1创建一个命名管道
- 命名管道可以从命令行上创建,命令行方法是使用下面这个命令:
$ mkfifo filename
- 命名管道也可以从程序里创建,相关函数有:
int mkfifo(const char *filename,mode_t mode);
3.2理解两个问题
- 如果两个不同的进程,打开同一文件的时候,在内核中,操作系统会打开个文件?
每个进程都有一个独立的files_struct, 进程和进程共享文件的方法和缓冲区 ,就和2.3的图片一样
管道文件和磁盘文件的区别是, 管道文件不需要刷盘, 两个进程想要通信, 一个进程把文件写到内存级缓冲区里, 另一个进程从缓冲区中读,管道文件属于内存级文件
- os如何知道命名管道打开的是同一个文件? 为什么需要打开同一个文件
可以通过 路径+文件名确定是同一个文件; 进程通信的前提是让不同的进程看到同一份资源
3.3编码
// common.hpp
#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <cerrno>
#include <cstring>
#include <string>
#include <unistd.h>
#include <cstdlib>
#include <fcntl.h>
using std::cout;
using std::endl;
#define FIFO_FILE "./myfifo"
#define MODE 0666
const int N = 1024;
enum {
FIFO_CREAT_ERR = 1,
FIFO_DEL_ERR,
FIFO_OPEN_ERR,
FIFO_READ_ERR
};
// 用于处理server.cc部分代码的初始化和析构处理
struct Init
{
Init()
{
// 创建信道
int n = mkfifo(FIFO_FILE, MODE);
if (n == -1) {
perror("mkfifo");
exit(FIFO_CREAT_ERR);
}
}
~Init()
{
// 删除信道
int m = unlink(FIFO_FILE);
if (m == -1) {
perror("unlink");
exit(FIFO_DEL_ERR);
}
}
};
// server.cc
#include "common.hpp"
int main()
{
Init init;
// 打开信道
int fd = open(FIFO_FILE, O_RDONLY);
if(fd == -1) {
perror("open");
exit(FIFO_OPEN_ERR);
}
cout << "server open file done" << endl;
// 开始通信, 从管道中读数据
while(true) {
char buffer[N] = {0};
int x = read(fd, buffer, sizeof(buffer));
if(x > 0) {
buffer[x] = 0;
cout << "client say@ " << buffer << endl;
}
else if(x == 0) {
cout << "client quit, me too!" << endl;
break;
}
else {
perror("read");
exit(FIFO_READ_ERR);
}
}
return 0;
}
// client.cc
#include "common.hpp"
int main()
{
// 打开信道
int fd = open(FIFO_FILE, O_WRONLY);
if(fd == -1) {
perror("open");
exit(FIFO_OPEN_ERR);
}
cout << "client open file done" << endl;
// 开始通信, 向管道中写数据
std::string line;
while(true) {
cout << "Please enter@ ";
getline(std::cin, line);
write(fd, line.c_str(), line.size());
}
return 0;
}
.PHONY:all
all : server client
server : server.cc
g++ -o $@ $^ -std=c++11
client : client.cc
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -rf server client
4.日志系统
#pragma once
#include <time.h>
#include <iostream>
#include <stdio.h>
#include <string>
#include <stdarg.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <cstdlib>
using std::cout;
using std::endl;
const int MAX_LOG_SIZE = 1024;
// 打开的文件名
const char* LOG_FILE_NAME = "log.txt";
// 日志等级
enum LEVEL
{
INFO = 1,
WARNING,
ERROR,
FATAL,
DEBUG
};
// 打印方式
enum PRINT_METHOD
{
SCREEN = 1,
ONE_FILE,
MULTIPLE_FILE
};
class Log
{
public:
Log(PRINT_METHOD printMethod = SCREEN)
: _printMethod(printMethod), _path("./log/"){}
std::string LevelToString(LEVEL level)
{
switch (level)
{
case INFO:
return "INFO";
break;
case WARNING:
return "WARNING";
break;
case ERROR:
return "ERROR";
break;
case FATAL:
return "FATAL";
break;
case DEBUG:
return "DEBUG";
break;
default:
return "NONE";
break;
}
}
// 更改打印方式
void ChangePrintMethod(PRINT_METHOD printMethod)
{
_printMethod = printMethod;
}
/*需要将字符串处理为: 默认部分+自定义部分*/
void LoadMessage(LEVEL level, const char *format, ...)
{
// 默认部分, 时间
char leftBuffer[MAX_LOG_SIZE] = {0};
time_t t = time(nullptr);
struct tm *ctime = localtime(&(t));
snprintf(leftBuffer, sizeof(leftBuffer), "[%s][%d-%d-%d %d:%d:%d]",
LevelToString(level).c_str(), ctime->tm_year + 1900,
ctime->tm_mon + 1, ctime->tm_mday, ctime->tm_hour,
ctime->tm_min, ctime->tm_sec, LevelToString(level).c_str());
// 自定义部分, 用户输入
char rightBuffer[MAX_LOG_SIZE] = {0};
// 因为定义了可变参数, 所以需要处理一下
va_list args;
va_start(args, format);
vsnprintf(rightBuffer, sizeof(rightBuffer), format, args);
va_end(args);
// 将两者加到一起
char logTxt[MAX_LOG_SIZE * 2] = {0};
snprintf(logTxt, sizeof(logTxt), "%s %s\n", leftBuffer, rightBuffer);
// cout << logTxt;
PrintLog(level, logTxt);
}
// 通过不同的输出方式, 将logTxt打印到不同的地方
void PrintLog(LEVEL level, const std::string& logTxt)
{
switch (_printMethod)
{
case SCREEN:
cout << logTxt;
break;
case ONE_FILE:
PrintOneFile(LOG_FILE_NAME, logTxt);
break;
case MULTIPLE_FILE:
PrintMultipleFile(level, logTxt);
break;
default:
break;
}
}
// 向一个文件中写
void PrintOneFile(const std::string& fileName, const std::string& logTxt)
{
std::string logName = _path + fileName; // 将日志文件写到log文件夹下
int fd = open(logName.c_str(), O_CREAT | O_APPEND | O_WRONLY, 0666);
if(fd == -1) return;
write(fd, logTxt.c_str(), logTxt.size());
close(fd);
}
// 向多个文件中写
void PrintMultipleFile(LEVEL level, const std::string& logTxt)
{
// 新的文件名: level+LOG_FILE_NAME
std::string fileName = LevelToString(level) + LOG_FILE_NAME;
PrintOneFile(fileName, logTxt);
}
/*
重载(), 调用更方便一点, 可变参数不允许二次传参, 所以代码与前面的LoadMessage()重复
现在若想要打日志, 可以有两种方法
首先先定义对象 Log log
1. log.LoadMessage(level, const char *format, ...)
2. log(level, const char *format, ...)
*/
void operator()(LEVEL level, const char *format, ...)
{
// 默认部分, 时间
char leftBuffer[MAX_LOG_SIZE] = {0};
time_t t = time(nullptr);
struct tm *ctime = localtime(&(t));
snprintf(leftBuffer, sizeof(leftBuffer), "[%s][%d-%d-%d %d:%d:%d]",
LevelToString(level).c_str(), ctime->tm_year + 1900,
ctime->tm_mon + 1, ctime->tm_mday, ctime->tm_hour,
ctime->tm_min, ctime->tm_sec, LevelToString(level).c_str());
// 自定义部分, 用户输入
char rightBuffer[MAX_LOG_SIZE] = {0};
// 因为定义了可变参数, 所以需要处理一下
va_list args;
va_start(args, format);
vsnprintf(rightBuffer, sizeof(rightBuffer), format, args);
va_end(args);
// 将两者加到一起
char logTxt[MAX_LOG_SIZE * 2] = {0};
snprintf(logTxt, sizeof(logTxt), "%s %s\n", leftBuffer, rightBuffer);
// cout << logTxt;
PrintLog(level, logTxt);
}
private:
PRINT_METHOD _printMethod;
std::string _path;
};
5. System V共享内存
共享内存区是最快的IPC形式。一旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到内核,换句话说是进程不再通过执行进入内核的系统调用来传递彼此的数据
5.1 原理, 让不同进程看到同一份资源, 通过进程地址空间映射
申请内存的步骤
- 申请共享物理内存
- 挂接到进程地址空间
释放共享内存的步骤
- 让进程的地址空间和物理内存去关联
- 释放共享内存
5.2 共享内存函数
5.2.1 shmget函数
功能:
用来创建共享内存
函数原型:
int shmget(key_t key, size_t size, int shmflg);
参数:
key:这个共享内存段名字
size:共享内存大小
shmflg:由九个权限标志构成,它们的用法和创建文件时使用的mode模式标志是一样的
返回值:
成功返回一个非负整数,即该共享内存段的标识码;失败返回-1
shmflg常用的两个参数
- IPC_CREAT: 如果你申请的共享内存不存在,就创建, 存在, 就获取并返回
- IPC CREAT|IPC EXCL:如果你申请的共享内存不存在, 就创建, 存在, 就出错返回。这样能确保如果我们申请成
功了一个共享内存,这个共享内存一定是一个新的!key的话题
- key是一个数字,这个数字是几,不重要。关键在于它必须在内核中具有唯一性,能够让不同的进程进行唯一性标识
- 第一个进程可以通过key创建共享内存. 第二个之后的进程, 只要拿着同一个key就可以和第一个进程看到同一个共享内存了
- 对于一个已经创建好的共享内存, key在描述那个共享内存的数据结构中
- 第一次创建的时候,必须有一个key了。
- key类似绝对路径, 特点是唯一
5.2.2 ftok函数
功能:
将路径名和项目标识符转换为System V IPC密钥
函数原型:
key_t ftok(const char *pathname, int proj_id);
参数:
pathname: 路径
proj_id: 项目id
返回值:
如果成功,则返回生成的key_t值。
如果失败,返回-1,errno表示stat(2)系统调用的错误。
描述:
ftok()函数使用给定路径名命名的文件的标识(必须引用一个现有的、可访问的文件)和proj_id的最低有效8位(必须是非零的)来生成一个密钥t类型的System V IPC密钥,适合与msgget(2)、senget(2)或shnget(2)一起使用。当使用proj_id的相同值时,对于命名相同文件的所有路径名,结果值是相同的。当(同时存在的)文件或项目id不同时,返回的值应该不同。
- 问题:为什么不让系统直接生成一个key,而需要我们手动生成呢?
因为系统直接生成的key不容易传给另一个我们想要通信的进程, 所以需要用户约定同一个key
key_t GetKey()
{
key_t k = ftok(PATH.c_str(), PROID);
if(k == -1) {
log(FATAL, "ftok error, %s", strerror(errno));
exit(1);
}
log(INFO, "Creat key sucess, key is %d", k);
return k;
}
int GetShareMem()
{
int shmid = shmget(GetKey(), SIZE, IPC_CREAT | IPC_EXCL);
if(shmid == -1) {
log(FATAL, "shmid error, %s", strerror(errno));
exit(2);
}
log(INFO, "Creat shmid sucess, shmid is %d", shmid);
return shmid;
}
- 问题:这里的
shmid
和key
的区别key: 只在操作系统内标定唯一性
shimid: 只在你的进程内,用来表示资源的唯一性!
5.2.3 查看和删除当前系统的共享内存
ipcs -m
共享内存的生命周期是随内核的
除非用户主动关闭, 或者内核重启, 否则共享内存会一直存在
ipcrm -m shmid
用来删除当前系统的共享内存
5.2.3 设置权限
只需要在shmget();
第三个参数 或 上权限的数字即可
int shmid = shmget(GetKey(), SIZE, IPC_CREAT | IPC_EXCL | 0666);
5.2.4shmat函数
功能:将共享内存段连接到进程地址空间
原型:
void *shmat(int shmid, const void *shmaddr, int shmflg);
参数:
shmid: 共享内存标识
shmaddr:指定连接的地址
shmflg:它的两个可能取值是SHM_RND和SHM_RDONLY, 也可以是0, 表示共享内存段将被附加到由shmat()函数返回的地址处
返回值:成功返回一个指针,指向共享内存第一个节;失败返回-1
shmaddr为NULL,核心自动选择一个地址
shmaddr不为NULL且shmflg无SHM_RND标记,则以shmaddr为连接地址。
shmaddr不为NULL且shmflg设置了SHM_RND标记,则连接的地址会自动向下调整为SHMLBA的整数倍。
公式:shmaddr - (shmaddr % SHMLBA)shmflg=SHM_RDONLY,表示连接操作用来只读共享内存
5.2.5shmdt函数
功能:将共享内存段与当前进程脱离
原型:
int shmdt(const void *shmaddr);
参数:
shmaddr: 由shmat所返回的指针
返回值:成功返回0;失败返回-1
注意:将共享内存段与当前进程脱离不等于删除共享内存段
5.2.6 shmctl函数
功能:用于控制共享内存
原型:
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数:
shmid:由shmget返回的共享内存标识码
cmd:将要采取的动作
buf:指向一个保存着共享内存的模式状态和访问权限的数据结构
返回值:成功返回0;失败返回-1
cmd的几个取值
5.2.7 一个保存着共享内存的模式状态和访问权限的数据结构
参数buf(5.2.6中的buf)是一个指向shmid结构体的指针,在<sys/shm.h>中定义如下:
struct shmid_ds {
struct ipc_perm shm_perm; /* Ownership and permissions */
size_t shm_segsz; /* Size of segment (bytes) */
time_t shm_atime; /* Last attach time */
time_t shm_dtime; /* Last detach time */
time_t shm_ctime; /* Last change time */
pid_t shm_cpid; /* PID of creator */
pid_t shm_lpid; /* PID of last shmat(2)/shmdt(2) */
shmatt_t shm_nattch; /* No. of current attaches */
...
};
ipc_perm结构定义如下(突出显示的字段可以使用IPC_SET设置):
struct ipc_perm {
key_t __key; /* Key supplied to shmget(2) */
uid_t **uid**; /* Effective UID of owner */
gid_t **gid**; /* Effective GID of owner */
uid_t cuid; /* Effective UID of creator */
gid_t cgid; /* Effective GID of creator */
unsigned short **mode**; /* Permissions + SHM_DEST and
SHM_LOCKED flags */
unsigned short __seq; /* Sequence number */
};
5.2.8当前的代码
// common.hpp
#ifndef __COMMON__H__
#define __COMMON__H__
#include <iostream>
#include <string>
#include <cstdlib>
#include <cstring>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include "log.hpp"
using namespace std;
const string PATH = "/home/lyf";
const int PROID = 0x1234;
// 共享内存的大小建议是4096的整数倍
// 当我们将SIZE改为4097时,OS实际给我们的是4096*2的大小, 但多出来的那4095byte我们并不能使用
// 这就造成了浪费
const int SIZE = 4096;
Log log;
key_t GetKey()
{
key_t k = ftok(PATH.c_str(), PROID);
if(k == -1) {
log(FATAL, "ftok error, %s", strerror(errno));
exit(1);
}
log(INFO, "Creat key sucess, key is 0x%x", k);
return k;
}
int ShareMemHelper(int flag)
{
int shmid = shmget(GetKey(), SIZE, flag);
if(shmid == -1) {
log(FATAL, "shmid error, %s", strerror(errno));
exit(2);
}
log(INFO, "Creat shmid sucess, shmid is %d", shmid);
return shmid;
}
// 创建共享内存
int CreatShareMem()
{
return ShareMemHelper(IPC_CREAT | IPC_EXCL | 0666);
}
// 获取共享内存的shimid
int GetShareMem()
{
return ShareMemHelper(IPC_CREAT);
}
#endif
// processA.cc
#include "common.hpp"
int main()
{
sleep(3);
// 申请共享物理内存
int shmid = CreatShareMem();
sleep(3);
// 挂接到进程地址空间
char* shmaddr = (char*)shmat(shmid, nullptr, 0);
log(INFO, "Attach shm done, shmaddr is 0x%x", shmaddr);
sleep(3);
// 让进程的地址空间和物理内存去关联
shmdt(shmaddr);
log(INFO, "Detatch shm done, shmaddr is 0x%x", shmaddr);
sleep(3);
// 释放共享内存
shmctl(shmid, IPC_RMID, nullptr);
log(INFO, "Free shm done, shmaddr is 0x%x", shmaddr);
sleep(3);
log(INFO, "Process quit");
return 0;
}
5.2.9 再加一个程序
// processB.cc
#include "common.hpp"
int main()
{
sleep(3);
// 获取共享物理内存
int shmid = GetShareMem();
sleep(3);
// 挂接到进程地址空间
char* shmaddr = (char*)shmat(shmid, nullptr, 0);
log(INFO, "Attach shm done, shmaddr is 0x%x", shmaddr);
sleep(3);
// 让进程的地址空间和物理内存去关联
shmdt(shmaddr);
log(INFO, "Detatch shm done, shmaddr is 0x%x", shmaddr);
sleep(3);
log(INFO, "Process quit");
return 0;
}
5.2.10进程间通信
我们上面做的工作, 已经可以让不同的进程看到同一份资源了, 下面进行通信
// processA.cc
#include "common.hpp"
int main()
{
int shmid = CreatShareMem();
char* shmaddr = (char*)shmat(shmid, nullptr, 0);
// 一旦有人把数据写入到共享内存,其实我们立马能看到了
// 不需要经过系统调用,直接就能看到数据了!
// IPC code, 让A进程读
while(true) {
cout << "Process say@ " << shmaddr; // 直接访问共享内存
sleep(1);
}
shmdt(shmaddr);
shmctl(shmid, IPC_RMID, nullptr);
return 0;
}
// processB.cc
#include "common.hpp"
int main()
{
int shmid = GetShareMem();
char* shmaddr = (char*)shmat(shmid, nullptr, 0);
// 一旦有了共享内存,挂接到自己的地址空间中,你直接把他当成你的内存空间来用即可!
// 不需要调用系统调用
// IPC code, 让B进程写
while(true) {
// 直接访问共享内存
// cout << "Please enter@ ";
// fgets(shmaddr, SIZE, stdin);
// 通过一个buffer来访问
char buffer[1024];
cout << "Please enter@ ";
fgets(buffer, sizeof(buffer), stdin);
// strlen+1, 让读取的时候读到\0
memcpy(shmaddr, buffer, strlen(buffer)+1);
}
shmdt(shmaddr);
return 0;
}
可以看到, 进程A在一直读取
5.3共享内存的特点
共享内存没有同步互斥之类的保护机制
共享内存是所有的进程间通信中,速度最快的
因为拷贝少, 对比管道, 管道当我们像管道写数据, 需要write(), 数据从用户级缓冲区拷贝到内核级缓冲区, 从管道中读数据, 需要read(), 数据再次拷贝, 从内核级缓冲区拷贝到用户级缓冲区
- 共享内存内部的数据,由用户自己维护
5.4添加管道
修改一下5.2.10中的代码, 不让processA一直刷屏, 引入了之前写的命名管道的代码
// common.hpp
#ifndef __COMMON__H__
#define __COMMON__H__
#include <iostream>
#include <string>
#include <cstdlib>
#include <cstring>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include "log.hpp"
using namespace std;
const string PATH = "/home/lyf";
const int PROID = 0x1234;
// 共享内存的大小建议是4096的整数倍
// 当我们将SIZE改为4097时,OS实际给我们的是4096*2的大小, 但多出来的那4095byte我们并不能使用
// 这就造成了浪费
const int SIZE = 4096;
Log log;
key_t GetKey()
{
key_t k = ftok(PATH.c_str(), PROID);
if(k == -1) {
log(FATAL, "ftok error, %s", strerror(errno));
exit(1);
}
log(INFO, "Creat key sucess, key is 0x%x", k);
return k;
}
int ShareMemHelper(int flag)
{
int shmid = shmget(GetKey(), SIZE, flag);
if(shmid == -1) {
log(FATAL, "shmid error, %s", strerror(errno));
exit(2);
}
log(INFO, "Creat or Get shmid sucess, shmid is %d", shmid);
return shmid;
}
// 创建共享内存
int CreatShareMem()
{
return ShareMemHelper(IPC_CREAT | IPC_EXCL | 0666);
}
// 获取共享内存的shimid
int GetShareMem()
{
return ShareMemHelper(IPC_CREAT);
}
#define FIFO_FILE "./myfifo"
#define MODE 0666
const int N = 1024;
enum {
FIFO_CREAT_ERR = 1,
FIFO_DEL_ERR,
FIFO_OPEN_ERR,
FIFO_READ_ERR
};
// 用于处理server.cc部分代码的初始化和析构处理
struct Init
{
Init()
{
// 创建信道
int n = mkfifo(FIFO_FILE, MODE);
if (n == -1) {
perror("mkfifo");
exit(FIFO_CREAT_ERR);
}
}
~Init()
{
// 删除信道
int m = unlink(FIFO_FILE);
if (m == -1) {
perror("unlink");
exit(FIFO_DEL_ERR);
}
}
};
#endif
// processA.cc
#include "common.hpp"
int main()
{
Init init;
int shmid = CreatShareMem();
char* shmaddr = (char*)shmat(shmid, nullptr, 0);
// 一旦有人把数据写入到共享内存,其实我们立马能看到了
// 不需要经过系统调用,直接就能看到数据了!
// IPC code, 让A进程读
struct shmid_ds shmds;
// 打开信道
int fd = open(FIFO_FILE, O_RDONLY); // 等待写入方打开之后, 自己才会打开文件, 向后执行, open 阻塞了!
if(fd == -1) {
log(FATAL, "open error, %s", strerror(errno));
exit(FIFO_OPEN_ERR);
}
while(true) {
char c;
ssize_t s = read(fd, &c, 1);
if(s == 0) break;
else if(s == -1) break;
cout << "Process say@ " << shmaddr; // 直接访问共享内存
// shmctl(shmid, IPC_STAT, &shmds);
// cout << "No. of current attaches" << shmds.shm_nattch<<endl;
// cout << "key is" << shmds.shm_perm.__key<<endl;
// sleep(1);
}
shmdt(shmaddr);
shmctl(shmid, IPC_RMID, nullptr);
close(fd);
return 0;
}
// processB.cc
#include "common.hpp"
int main()
{
int shmid = GetShareMem();
char* shmaddr = (char*)shmat(shmid, nullptr, 0);
// 一旦有了共享内存,挂接到自己的地址空间中,你直接把他当成你的内存空间来用即可!
// 不需要调用系统调用
// IPC code, 让B进程写
// 打开信道
int fd = open(FIFO_FILE, O_WRONLY);
if(fd == -1) {
log(FATAL, "open error, %s", strerror(errno));
exit(FIFO_OPEN_ERR);
}
while(true) {
// 直接访问共享内存
cout << "Please enter@ ";
fgets(shmaddr, SIZE, stdin);
write(fd, "c", 1); // 通知另一个进程
// 通过一个buffer来访问
// char buffer[1024];
// cout << "Please enter@ ";
// fgets(buffer, sizeof(buffer), stdin);
// // strlen+1, 让读取的时候读到\0
// memcpy(shmaddr, buffer, strlen(buffer)+1);
}
shmdt(shmaddr);
return 0;
}
6. 信号量
6.1 几个概念
- 当我们的A进程正在写入,写入了一部分,就被B进程拿走了,导致双方发和收的数据不完整 - 数据不一致问题. A B看到的同一份资源,共享资源, 如果不加保护,会导致数据不一致问题
- 加任何时刻,只允许一个执行流访问共享资源 — 互斥
- 共享的,任何时刻只允许一个执行流访问(就是执行访问代码)的资源就是临界资源, 一般般是内存空间
- 访问临界资源的代码 ---- 临界区
6.2 理解信号量
信号量/信号灯的本质是一把计数器,类似 int cnt = n
用来描述临界资源中资源数量的多少!
- 申请计数器成功,就表示我具有访问资源的权限了
- 申请了计数器资源,我当前并没有访问我想要的资源。申请了计数器资源是对资源的预订机制
- 计数器可以有效保证进入共享资源的执行流的数量
- 所以每一个执行流,想访问共享资源中的一部分的时候,不是直接访问,而是先申请计数器资源。
类似看电影的先买票!
6.3 二元信号量
我们把临界资源位1,信号量值只能为1,0两态的计数器叫做 二元信号量,本质就是一个锁
将计数器设置为1,资源为1的本质:其实就是将临界资源不要分成很多块了,而是当做一个整体。整体申请,整体释放
6.4PV操作
申请信号量,本质是对计数器进行–操作,也就是P操作
释放资源,释放信号量,本质是对计数器进行++操作,也就是V操作
为了保证–操作和++操作不会被其他进程打扰,我们让该操作变成原子操作
即:要么不做,要做就做完,两态的,没有“正在做”这样的概念!
6.5 信号量凭什么是进程间通信的一种
- 通信不仅仅是通信数据,互相协同也是
- 要协同,本质也是通信,信号量首先要被所有的通信进程看到
6.6总结
- 信号量本质是一把计数器,PV操作,原子的。
- 执行流申请资源,必须先申请信号量资源,得到信号量之后,才能访问临界资源
- 信号量值1,0两态的,二元信号量,就是互斥功能
- 申请信号量的本质: 是对临界资源的预订机制