匿名管道pipe
//pipe.cpp
#include <iostream>
#include <string>
#include <cerrno>
#include <cassert>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
int main()
{
// 让不同的进程看到同一份资源!!!!
// 任何一种进程间通信中,一定要先保证不同的进程之间看到同一份资源
int pipefd[2] = { 0 };//将元素初始化为0
//1. 创建管道
int n = pipe(pipefd);//pipefd是输出型参数
if (n < 0)
{
std::cout << "pipe error, " << errno << ": " << strerror(errno) << std::endl;//errno是一个全局变量
return 1;
}
std::cout << "pipefd[0]: " << pipefd[0] << std::endl; // 0下标是读端, 0->嘴巴->读书,一般情况下会给我们的管道nfd分别是3和4。
std::cout << "pipefd[1]: " << pipefd[1] << std::endl; // 1下标是写端, 1->笔->写东西的
//2. 创建子进程
pid_t id = fork();
assert(id != -1); //正常应该用判断,我这里就断言:意料之外用if,因为fork有可能失败。意料之中用assert
if (id == 0)// 子进程
{
//3. 关闭不需要的fd,让父进程进行读取,让子进程进行写入
close(pipefd[0]);
//4. 开始通信 -- 结合某种场景
// const std::string namestr = "hello, 我是子进程";
// int cnt = 1;
// char buffer[1024];
int cnt = 0;
while (true)
{
char x = 'X';
write(pipefd[1], &x, 1);
std::cout << "Cnt: " << cnt++ << std::endl;
sleep(1);
if (cnt > 5) break;
// break;
// snprintf(buffer, sizeof buffer, "%s, 计数器: %d, 我的PID: %d", namestr.c_str(), cnt++, getpid());//将不同变量信息按照字符串格式化
// write(pipefd[1], buffer, strlen(buffer));
}
close(pipefd[1]);
exit(0);
}
//父进程
//3. 关闭不需要的fd,让父进程进行读取,让子进程进行写入
close(pipefd[1]);
//4. 开始通信 -- 结合某种场景
char buffer[1024];
int cnt = 0;
while (true)
{
// sleep(10);
// sleep(1);
int n = read(pipefd[0], buffer, sizeof(buffer) - 1);//最多读取1023个字符,最后一个一定要留下来放值\0。read是按照字节流进行读取的,不会你是字符还是别的。
if (n > 0)
{
buffer[n] = '\0';
std::cout << "我是父进程, child give me message: " << buffer << std::endl;
}
else if (n == 0)
{
std::cout << "我是父进程, 读到了文件结尾" << std::endl;
break;
}
else
{
std::cout << "我是父进程, 读异常了" << std::endl;
break;
}
sleep(1);
//if(cnt++ > 5) break;
}
close(pipefd[0]);
// int status = 0;
//waitpid(id, &status, 0);
//std::cout << "sig: " << (status & 0x7F) << std::endl;
sleep(100);
return 0;
}
ctrlProcess
//task.cpp
#pragma once
#include <iostream>
#include <vector>
#include <unistd.h>
// typedef std::function<void ()> func_t;
typedef void (*fun_t)(); //函数指针
void PrintLog()
{
std::cout << "pid: " << getpid() << ", 打印日志任务,正在被执行..." << std::endl;
}
void InsertMySQL()
{
std::cout << "执行数据库任务,正在被执行..." << std::endl;
}
void NetRequest()
{
std::cout << "执行网络请求任务,正在被执行..." << std::endl;
}
//约定,每一个command都必须是4字节
#define COMMAND_LOG 0
#define COMMAND_MYSQL 1
#define COMMAND_REQEUST 2
class Task
{
public:
Task()
{
funcs.push_back(PrintLog);
funcs.push_back(InsertMySQL);
funcs.push_back(NetRequest);
}
void Execute(int command)
{
if (command >= 0 && command < funcs.size()) funcs[command]();
}
~Task()
{}
public:
std::vector<fun_t> funcs;
};
//ctrlProcess.cpp
//实现父进程通过向子进程写入特定的消息唤醒子进程,甚至让子进程定向执行某种任务。
#include <iostream>
#include <string>
#include <vector>
#include <cassert>
#include <unistd.h>
#include "Task.hpp"
using namespace std;
const int gnum = 5;//总共创建5个子进程
Task t;
class EndPoint//管理,父进程需要知道自己有哪些子进程、哪些管道、子进程和管道的对用关系。
{
public:
pid_t _child_id;//子进程的pid
int _write_fd;//父进程需要向哪一个管道里写
public:
EndPoint(int id, int fd) : _child_id(id), _write_fd(fd)
{
}
~EndPoint()
{
}
};
// 子进程要执行的方法
void WaitCommand()
{
while (true)
{
int command = 0;
int n = read(0, &command, sizeof(int));//子进程现在只要从0号文件描述符里面读取数据,管道读取的文件描述符重定向到了0。
if (n == sizeof(int))
{
t.Execute(command);
}
else if (n == 0)
{
break;
}
else
{
break;
}
}
}
void createProcesses(vector<EndPoint>* end_points)
{
for (int i = 0; i < gnum; i++)
{
// 1.1 创建管道
int pipefd[2] = { 0 };
int n = pipe(pipefd);
assert(n == 0);
(void)n;
// 1.2 创建进程
pid_t id = fork();
assert(id != -1);
// 一定是子进程
if (id == 0)
{
// 1.3 关闭不要的fd
close(pipefd[1]);
// 我们期望,所有的子进程读取"指令"的时候,都从标准输入读取,也就是不在从pipefd[0]描述符进行读取而是从直接从0号描述符读取更好理解,
// 1.3.1 输入重定向
dup2(pipefd[0], 0);
// 1.3.2 子进程开始等待获取命令
WaitCommand();
close(pipefd[0]);
exit(0);
}
// 一定是父进程
// 1.3 关闭不要的fd
close(pipefd[0]);
// 1.4 将新的子进程和他的管道写端,构建对象
end_points->push_back(EndPoint(id, pipefd[1]));
}
}
int main()
{
// 1. 先进行构建控制结构, 父进程写入,子进程读取 , bug?
vector<EndPoint> end_points;//父进程通过该结构进行管理子进程和对应的管道
createProcesses(&end_points);
// 2. 我们的得到了什么?end_points
int num = 0;
while (true)
{
//1. 选择任务
int command = COMMAND_LOG;
//2. 选择进程
int index = rand() % end_points.size();
//3. 下发任务
write(end_points[index]._write_fd, &command, sizeof(command));
sleep(1);
}
return 0;
}
ctrlProcess2.0
//task.h
#pragma once
#pragma once
#include <iostream>
#include <vector>
#include <unistd.h>
#include <unordered_map>
// typedef std::function<void ()> func_t;
typedef void (*fun_t)(); //函数指针
void PrintLog()
{
std::cout << "pid: " << getpid() << ", 打印日志任务,正在被执行..." << std::endl;
}
void InsertMySQL()
{
std::cout << "执行数据库任务,正在被执行..." << std::endl;
}
void NetRequest()
{
std::cout << "执行网络请求任务,正在被执行..." << std::endl;
}
// void ExitProcess()
// {
// exit(0);
// }
//约定,每一个command都必须是4字节
#define COMMAND_LOG 0
#define COMMAND_MYSQL 1
#define COMMAND_REQEUST 2
class Task
{
public:
Task()
{
funcs.push_back(PrintLog);
funcs.push_back(InsertMySQL);
funcs.push_back(NetRequest);
}
void Execute(int command)
{
if (command >= 0 && command < funcs.size()) funcs[command]();
}
~Task()
{}
public:
std::vector<fun_t> funcs;
// std::unordered_map<std::string, fun_t> funcs;
};
//ctrlProcess.cpp
//通过父进程对不同的管道发送消息来控制子进 程的行为
#include <iostream>
#include <string>
#include <vector>
#include <cassert>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include "task.h"
using namespace std;
const int gnum = 3;
Task t;
class EndPoint
{
private:
static int number;
public:
pid_t _child_id;
int _write_fd;
std::string processname;
public:
EndPoint(int id, int fd) : _child_id(id), _write_fd(fd)
{
//process-0[pid:fd]
char namebuffer[64];
snprintf(namebuffer, sizeof(namebuffer), "process-%d[%d:%d]", number++, _child_id, _write_fd);
processname = namebuffer;
}
std::string name() const
{
return processname;
}
~EndPoint()
{
}
};
int EndPoint::number = 0;
// 子进程要执行的方法
void WaitCommand()
{
while (true)
{
int command = 0;
int n = read(0, &command, sizeof(int));
if (n == sizeof(int))
{
t.Execute(command);
}
else if (n == 0)
{
std::cout << "父进程让我退出,我就退出了: " << getpid() << std::endl;
break;
}
else
{
break;
}
}
}
void createProcesses(vector<EndPoint>* end_points)
{
vector<int> fds;//存储父进程所有的写端描述符,父进程创建了几个管道就会有几个写端的文件描述符。这时候父进程在创建子进程的时候,子进程继承父进程的写端文件描述符,这时候子进程可以借助这个容器将之前进程的写端文件描述符关闭。
for (int i = 0; i < gnum; i++)
{
// 1.1 创建管道
int pipefd[2] = { 0 };
int n = pipe(pipefd);
assert(n == 0);
(void)n;
// 1.2 创建进程
pid_t id = fork();
assert(id != -1);
// 一定是子进程
if (id == 0)
{
for (auto& fd : fds) close(fd);//将之前进程的写端文件描述符关闭
// std::cout << getpid() << " 子进程关闭父进程对应的写端:";
// for(auto &fd : fds)
// {
// std::cout << fd << " ";
// close(fd);
// }
// std::cout << std::endl;
// 1.3 关闭不要的fd
close(pipefd[1]);
// 我们期望,所有的子进程读取"指令"的时候,都从标准输入读取
// 1.3.1 输入重定向,可以不做
dup2(pipefd[0], 0);
// 1.3.2 子进程开始等待获取命令
WaitCommand();
close(pipefd[0]);
exit(0);
}
// 一定是父进程
// 1.3 关闭不要的fd
close(pipefd[0]);
// 1.4 将新的子进程和他的管道写端,构建对象
end_points->push_back(EndPoint(id, pipefd[1]));
fds.push_back(pipefd[1]);
}
}
int ShowBoard()
{
std::cout << "##########################################" << std::endl;
std::cout << "| 0. 执行日志任务 1. 执行数据库任务 |" << std::endl;
std::cout << "| 2. 执行请求任务 3. 退出 |" << std::endl;
std::cout << "##########################################" << std::endl;
std::cout << "请选择# ";
int command = 0;
std::cin >> command;
return command;
}
void ctrlProcess(const vector<EndPoint>& end_points)
{
// 2.1 我们可以写成自动化的,也可以搞成交互式的
int num = 0;
int cnt = 0;
while (true)
{
//1. 选择任务
int command = ShowBoard();
if (command == 3) break;
if (command < 0 || command > 2) continue;
//2. 选择进程
int index = cnt++;
cnt %= end_points.size();
std::string name = end_points[index].name();
std::cout << "选择了进程: " << name << " | 处理任务: " << command << std::endl;
//3. 下发任务
write(end_points[index]._write_fd, &command, sizeof(command));
sleep(1);
}
}
void waitProcess(const vector<EndPoint>& end_points)
{
for (int end = 0; end < end_points.size(); end++)
{
std::cout << "父进程让子进程退出:" << end_points[end]._child_id << std::endl;
close(end_points[end]._write_fd);
waitpid(end_points[end]._child_id, nullptr, 0);
std::cout << "父进程回收了子进程:" << end_points[end]._child_id << std::endl;
}//上面的写法是有问题的,因为父进程第一次创建管道,加入写端是4,第二次创建管道写端是6,此时第二个子进程对应的是第二个管道,但是由于他继承于父进程,所以他也有第一个子进程的写端也就是文件描述符4。
//当父进程关闭第一个子进程的写端4的时候,其实第二个子进程也继承了这个文件描述符4的写端,所以就导致写端没有关完,就直接在waitpid这里阻塞住了。
//解决方法一:到这关闭写端,也就是先关闭进程2管道的写端,再关闭进程1管道的写端
//解决方法二:创建子进程的时候,将之前子进程的写端进行关闭。
sleep(10);
// 1. 我们需要让子进程全部退出 --- 只需要让父进程关闭所有的write fd就可以了!
// for(const auto &ep : end_points)
// for(int end = end_points.size() - 1; end >= 0; end--)
// 2. 父进程要回收子进程的僵尸状态
// for(const auto &ep : end_points) waitpid(ep._child_id, nullptr, 0);
// std::cout << "父进程回收了所有的子进程" << std::endl;
// sleep(10);
}
// #define COMMAND_LOG 0
// #define COMMAND_MYSQL 1
// #define COMMAND_REQEUST 2
int main()
{
vector<EndPoint> end_points;
// 1. 先进行构建控制结构, 父进程写入,子进程读取 , bug?
createProcesses(&end_points);
// 2. 我们的得到了什么?end_points
ctrlProcess(end_points);
// 3. 处理所有的退出问题
waitProcess(end_points);
return 0;
}
命名管道namepipe
//common.h
#pragma once
#pragma once
#include <iostream>
#include <string>
#define NUM 1024
const std::string fifoname = "./fifo";
uint32_t mode = 0666;
//server.cpp
//实现两个没有血缘关系的进程之间的通信,所以需要两个cpp文件分别形成两个可执行
#include <iostream>
#include <cerrno>
#include <cstring>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include "common.h"
int main()
{
// 1. 创建管道文件,只需要一次创建
umask(0); //这个设置并不影响系统的默认配置,只会影响当前进程
int n = mkfifo(fifoname.c_str(), mode);//这是系统调用,当然也存在一个命令叫做mkfifo。fifoname.c_str()是文件路径。
if (n != 0)
{
std::cout << errno << " : " << strerror(errno) << std::endl;
return 1;
}
std::cout << "create fifo file success" << std::endl;
// 2. 让服务端直接开启管道文件
int rfd = open(fifoname.c_str(), O_RDONLY);//打开命名管道文件,返回对应的文件描述符。如果只运行server,不运行clien,函数会卡在这一行,知道clien运行,该函数打开管道才能执行。
if (rfd < 0)
{
std::cout << errno << " : " << strerror(errno) << std::endl;
return 2;
}
std::cout << "open fifo success, begin ipc" << std::endl;
// 3. 正常通信
char buffer[NUM];
while (true)
{
buffer[0] = 0;
ssize_t n = read(rfd, buffer, sizeof(buffer) - 1);
if (n > 0)//读取成功
{
buffer[n] = 0;//和buffer[n]='/0'是一个意思
//std::cout << "client# " << buffer << std::endl;
printf("%c", buffer[0]);
fflush(stdout);
}
else if (n == 0)//当对端把写关闭了,我们读端就会读到头返回0
{
std::cout << "client quit, me too" << std::endl;
break;
}
else
{
std::cout << errno << " : " << strerror(errno) << std::endl;
break;
}
}
// 关闭不要的fd
close(rfd);
unlink(fifoname.c_str());//删除一个文件,也就是文件的引用计数减一,这样可以不用每次手动rm fifo了。
return 0;
}
//client.cpp
#include <iostream>
#include <cstdio>
#include <cerrno>
#include <cstring>
#include <cassert>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
// #include <ncurses.h>
#include "common.h"
int main()
{
//1. 不需创建管道文件,我只需要打开对应的文件即可!
int wfd = open(fifoname.c_str(), O_WRONLY);
if (wfd < 0)
{
std::cerr << errno << ":" << strerror(errno) << std::endl;
return 1;
}
// 可以进行常规通信了
char buffer[NUM];
while (true)
{
// std::cout << "请输入你的消息# ";
// char *msg = fgets(buffer, sizeof(buffer), stdin);//sizeof(buffer)不用减 一,应为fgets在获取的时候会自动再末尾留一个空格来放'\0'
// assert(msg);
// (void)msg;//防止一个变量被定义了但是没有被使用
// int c = getch();
// std::cout << c << std::endl;
// if(c == -1) continue;
system("stty raw");
int c = getchar();
system("stty -raw");
//上面代码实现clien端实时写入,服务器端实时显示。
//std::cout << c << std::endl;
//sleep(1);
//buffer[strlen(buffer) - 1] = 0;//因为客户端输入的时候会带上一个回车,这样服务显示的时候就会有空行,所以我们这里将\n设置成为\0。实例如下
// abcde\n\0
// 012345
//if(strcasecmp(buffer, "quit") == 0) break;//strcasecmp是忽略大小写的比较
//ssize_t n = write(wfd, buffer, strlen(buffer));
ssize_t n = write(wfd, (char*)&c, sizeof(char));
assert(n >= 0);
(void)n;
}
close(wfd);
return 0;
}
共享内存shm
//common.h
#pragma once
#ifndef __COMM_HPP__
#define __COMM_HPP__
#include <iostream>
#include <cerrno>
#include <cstdio>
#include <cstring>
#include <cassert>
#include <string>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <sys/stat.h>
using namespace std;
// IPC_CREAT and IPC_EXCL
// 单独使用IPC_CREAT: 创建一个共享内存,如果共享内存不存在,就创建之,如果已经存在,获取已经存在的共享内存并返回
// IPC_EXCL不能单独使用,一般都要配合IPC_CREAT
// IPC_CREAT | IPC_EXCL: 创建一个共享内存,如果共享内存不存在,就创建之, 如果已经存在,则立马出错返回 -- 如果创建成功,对应的shm,一定是最新的!
#define PATHNAME "."
#define PROJID 0x6666
const int gsize = 4096; //暂时
//共享内存的代销是以PAGE页(4kb)为单位的
key_t getKey()
{
key_t k = ftok(PATHNAME, PROJID);//根据路径字符串和项目id通过一定的算法结合形成冲突概率低的key值。该key值会在shmget函数中做为参数传入,创建共享内存并用key来对该共享内存的数据结构起到一定的标识作用,此时clien就可以通过key值找到这份共享内存,这样不同的进程就可以key值找到同一份共享内存。
if (k == -1)
{
cerr << "error: " << errno << " : " << strerror(errno) << endl;
exit(1);
}
return k;
}
string toHex(int x)//将一个整数x转换成为16进制字符串
{
char buffer[64];
snprintf(buffer, sizeof buffer, "0x%x", x);
return buffer;
}
static int createShmHelper(key_t k, int size, int flag)
{
int shmid = shmget(k, gsize, flag);//gsize是想要的内存大小,字节为段位返回共享内存标志符。创建和获取共享内存用的都是这个函数,只是第三个参数会有差别。
if (shmid == -1)
{
cerr << "error: " << errno << " : " << strerror(errno) << endl;
exit(2);
}
return shmid;
}
int createShm(key_t k, int size)//创建共享内存
{
umask(0);
return createShmHelper(k, size, IPC_CREAT | IPC_EXCL | 0666);//创建内存的时候还要给权限参数0666,通过命令ipcs -m可以查看共享内存的权限perm项
}
int getShm(key_t k, int size)//通过key值来获取通过key值创建的共享内存
{
return createShmHelper(k, size, IPC_CREAT);
}
char* attachShm(int shmid)//我创建的共享内存并不一定我就能使用,有可能我创建的共享内存是给别人使用的,所以想要用共享内存还要进行关联。
{
char* start = (char*)shmat(shmid, nullptr, 0);//返回值start就是共享内存虚拟地址的起始地址,是void*类型
return start;
}
void detachShm(char* start)//和共享内存去关联
{
int n = shmdt(start);
assert(n != -1);
(void)n;
}
void delShm(int shmid)//
{
int n = shmctl(shmid, IPC_RMID, nullptr);//该函数可以对共享内存进行很多操作,这便是IPC_RMID对共享内存执行删除操作
assert(n != -1);
(void)n;
}
#define SERVER 1
#define CLIENT 0
class Init
{
public:
Init(int t) :type(t)
{
key_t k = getKey();
if (type == SERVER) shmid = createShm(k, gsize);
else shmid = getShm(k, gsize);
start = attachShm(shmid);
}
char* getStart() { return start; }
~Init()
{
detachShm(start);
if (type == SERVER) delShm(shmid);
}
private:
char* start;
int type; //server or client
int shmid;
};
#endif
//server.cpp
//并非以文件的方式让不同的进程看到同一份资源,而是通过让我们的进程看到同一个内存块的方式实现的
#include "common.h"
#include <unistd.h>
int main()
{
Init init(SERVER);
char* start = init.getStart();
int n = 0;
// 我们在通信的时候,没有使用任何write和read的接口?一旦共享内存映射到进程的地址空间,该共享内存就直接被所有的进程 直接看到了!不会牵涉到从用户到内核以及内核到用户的拷贝
// 因为共享内存的这种特性,可以让进程通信的时候,减少拷贝次数,所以共享内存是所有进程间通信,速度最快的
// 共享内存没有任何的保护机制(同步互斥)现象就是:如果clien没有写入数据,server还是会一直从共享内存中进行读取,如果是管道的话clien读取会阻塞 -- 为什么?管道通过系统接口通信,共享内存直接通信。
while (n <= 30)
{
cout << "client -> server# " << start << endl;//这里将共享内存的内容看成一个字符串
sleep(1);
n++;
}
// //1. 创建key
// key_t k = getKey();
// cout << "server key: " << toHex(k) << endl;
// //2. 创建共享内存
// int shmid = createShm(k, gsize);
// cout << "server shmid: " << shmid << endl;
// sleep(3);
// //3. 将自己和共享内存关联起来
// char* start = attachShm(shmid);
// sleep(20);
// // 通信代码在这里!
// // 4. 将自己和共享内存去关联
// detachShm(start);
// sleep(3);
// struct shmid_ds ds;
// int n = shmctl(shmid, IPC_STAT, &ds);//该函数也可以获取共享内存的属性,通过IPC_STA参数并且传一个共享内存对应数据结构的指针就可以了
// if(n != -1)
// {
// cout << "perm: " << toHex(ds.shm_perm.__key) << endl;
// cout << "creater pid: " << ds.shm_cpid << " : " << getpid() << endl;
// }
// ?. 删除共享内存
//delShm(shmid);
return 0;
}
//client.cpp
#include "common.h"
#include <unistd.h>
int main()
{
Init init(CLIENT);
char* start = init.getStart();
char c = 'A';
while (c <= 'Z')
{
start[c - 'A'] = c;//client向共享内存不断的写入内容
c++;
start[c - 'A'] = '\0';
sleep(1);
}
// key_t k = getKey();
// cout << "client key: " << toHex(k) << endl;
// int shmid = getShm(k, gsize);
// cout << "client shmid: " << shmid << endl;
// //3. 将自己和共享内存关联起来
// char* start = attachShm(shmid);
// sleep(15);
// // 4. 将自己和共享内存去关联
// detachShm(start);
return 0;
}