🕺作者: 主页
😘欢迎关注:👍点赞🙌收藏✍️留言
🏇码字不易,你的👍点赞🙌收藏❤️关注对我真的很重要,有问题可在评论区提出,感谢阅读!!!
Linux 进程间通信是操作系统中非常重要的一个概念,它允许不同进程之间进行数据交换和协作,从而实现更高效、更复杂的应用程序。在计算机科学领域,进程间通信是一个广泛研究的主题,也是操作系统设计中的核心问题之一。
本文将介绍 Linux 中常见的几种进程间通信方式,包括管道、命名管道、共享内存等。我们将深入探讨每种方式的工作原理、优缺点以及适用场景,帮助读者更好地理解和运用这些技术。
无论你是初学者还是资深开发人员,本文都将为你提供有价值的知识和实用的技能。让我们开始吧!
单进程无法使用并发能力,更加无法实现多进程协同
有的是为了传输数据、同步执行流、消息通知等,这些都需要多进程协同才能实现
它不是目的,而是手段
1.进程是具有独立性的。虚拟地址空间+页表 保证进程运行的独立性
2.因为他的独立性导致通信成本会比较高
1.进程间通信的前提是让不同的进程看到同一块“内存”(特定的结构组织)
2.同一块“内存”不能隶属于任何一个进程,而应该强调共享
进程间通信方式的一些标准和分类
标准在我们使用者看来都是接口上的理解
比如天然气、石油等管道,它只能单向输出内容,它能传输资源
现在在计算机中最重要的资源是数据,数据是互联网时代的“石油”

管道通信背后是进程之间通过管道进行通信
举例:
1.父进程以读写的方式打开一个文件
2.fork()创建子进程
3.双方各自关闭自己不需要的文件描述符
它不会把数据存到磁盘上,都在内存中



#include
功能:创建一无名管道
原型
int pipe(int fd[2]);
参数
fd:文件描述符数组,其中fd[0]表示读端, fd[1]表示写端
返回值:成功返回0,失败返回错误代码

#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
// 为什么不定义全局buffer来进行通信呢?? 因为有写时拷贝的存在,无法更改通信!
int main()
{
// 1. 创建管道
int pipefd[2] = {0}; // pipefd[0(嘴巴,读书)]: 读端 , pipefd[1(钢笔,写)]: 写端
int n = pipe(pipefd);
assert(n != -1); // debug assert, release assert
(void)n;//避免n只被定义没有被使用导致大量报红
#ifdef DEBUG
cout << "pipefd[0]: " << pipefd[0] << endl; // 3
cout << "pipefd[1]: " << pipefd[1] << endl; // 4
#endif
// 2. 创建子进程
pid_t id = fork();
assert(id != -1);
if (id == 0)
{
//子进程 - 读
// 3. 构建单向通信的信道,父进程写入,子进程读取
// 3.1 关闭子进程不需要的fd
close(pipefd[1]);
char buffer[1024 * 8];
while (true)
{
// sleep(20);
// 写入的一方,fd没有关闭,如果有数据,就读,没有数据就等
// 写入的一方,fd关闭, 读取的一方,read会返回0,表示读到了文件的结尾!
ssize_t s = read(pipefd[0], buffer, sizeof(buffer) - 1);
//读取pipefd[0]文件描述符,到buffer中,读取sizeof(buffer) - 1个
//最后一个位置自己设置\0
if (s > 0)
{
buffer[s] = 0;
cout << "child get a message[" << getpid() << "] Father# " << buffer << endl;
}
else if(s == 0)
{
cout << "writer quit(father), me quit!!!" << endl;
break;
}
}
// close(pipefd[0]);
exit(0);
}
//父进程 - 写
// 3. 构建单向通信的信道
// 3.1 关闭父进程不需要的fd
close(pipefd[0]);
string message = "我是父进程,我正在给你发消息";
int count = 0;
char send_buffer[1024 * 8];
while (true)
{
// 3.2 构建一个变化的字符串
snprintf(send_buffer, sizeof(send_buffer), "%s[%d] : %d",
message.c_str(), getpid(), count++);
// 3.3 写入
write(pipefd[1], send_buffer, strlen(send_buffer));
// 3.4 故意sleep
sleep(1);
cout << count << endl;
if (count == 5){
cout << "writer quit(father)" << endl;
break;
}
}
close(pipefd[1]);
pid_t ret = waitpid(id, nullptr, 0);
cout << "id : " << id << " ret: " << ret <<endl;
assert(ret > 0);
(void)ret;
return 0;
}
#include
#include
#include
#include
#include
#include
#include
#include
#include "Task.hpp"
#define PROCESS_NUM 5
//进程个数
using namespace std;
int waitCommand(int waitFd, bool &quit) //如果对方不发,我们就阻塞
{
uint32_t command = 0;
ssize_t s = read(waitFd, &command, sizeof(command));
if (s == 0)
{
quit = true;
return -1;
}
assert(s == sizeof(uint32_t));
return command;
}
void sendAndWakeup(pid_t who, int fd, uint32_t command)
{
write(fd, &command, sizeof(command));
cout << "main process: call process " << who << " execute " << desc[command] << " through " << fd << endl;
}
int main()
{
// 代码中关于fd的处理,有一个小问题,不影响我们使用,但是你能找到吗??
load();
// pid: pipefd
vector<pair<pid_t, int>> slots;
// 先创建多个进程
for (int i = 0; i < PROCESS_NUM; i++)
{
// 创建管道
int pipefd[2] = {0};
int n = pipe(pipefd);
assert(n == 0);
(void)n;
pid_t id = fork();
assert(id != -1);
// 子进程我们让他进行读取
if (id == 0)
{
// 关闭写端
close(pipefd[1]);
// child
while (true)
{
// pipefd[0]
// 等命令
bool quit = false;
int command = waitCommand(pipefd[0], quit); //如果对方不发,我们就阻塞
if (quit)
break;
// 执行对应的命令
if (command >= 0 && command < handlerSize())
{
callbacks[command]();
}
else
{
cout << "非法command: " << command << endl;
}
}
exit(1);
}
// father,进行写入,关闭读端
close(pipefd[0]); // pipefd[1]
slots.push_back(pair<pid_t, int>(id, pipefd[1]));
}
// 父进程派发任务
srand((unsigned long)time(nullptr) ^ getpid() ^ 23323123123L); // 让数据源更随机
while (true)
{
// 选择一个任务, 如果任务是从网络里面来的?
int command = rand() % handlerSize();
// 选择一个进程 ,采用随机数的方式,选择进程来完成任务,随机数方式的负载均衡
int choice = rand() % slots.size();
// 把任务给指定的进程
sendAndWakeup(slots[choice].first, slots[choice].second, command);
sleep(1);
// int select;
// int command;
// cout << "############################################" << endl;
// cout << "# 1. show funcitons 2.send command #" << endl;
// cout << "############################################" << endl;
// cout << "Please Select> ";
// cin >> select;
// if (select == 1)
// showHandler();
// else if (select == 2)
// {
// cout << "Enter Your Command> ";
// // 选择任务
// cin >> command;
// // 选择进程
// int choice = rand() % slots.size();
// // 把任务给指定的进程
// sendAndWakeup(slots[choice].first, slots[choice].second, command);
// }
// else
// {
// }
}
// 关闭fd, 所有的子进程都会退出
for (const auto &slot : slots)
{
close(slot.second);
}
// 回收所有的子进程信息
for (const auto &slot : slots)
{
waitpid(slot.first, nullptr, 0);
}
}
#pragma once
#include
#include
#include
#include
#include
#include
typedef std::function<void()> func;
std::vector<func> callbacks;
std::unordered_map<int, std::string> desc;
void readMySQL()
{
std::cout << "sub process[" << getpid() << " ] 执行访问数据库的任务\n" << std::endl;
}
void execuleUrl()
{
std::cout << "sub process[" << getpid() << " ] 执行url解析\n" << std::endl;
}
void cal()
{
std::cout << "sub process[" << getpid() << " ] 执行加密任务\n" << std::endl;
}
void save()
{
std::cout << "sub process[" << getpid() << " ] 执行数据持久化任务\n" << std::endl;
}
void load()
{
desc.insert({callbacks.size(), "readMySQL: 读取数据库"});
callbacks.push_back(readMySQL);
desc.insert({callbacks.size(), "execuleUrl: 进行url解析"});
callbacks.push_back(execuleUrl);
desc.insert({callbacks.size(), "cal: 进行加密计算"});
callbacks.push_back(cal);
desc.insert({callbacks.size(), "save: 进行数据的文件保存"});
callbacks.push_back(save);
}
void showHandler()
{
for(const auto &iter : desc )
{
std::cout << iter.first << "\t" << iter.second << std::endl;
}
}
int handlerSize()
{
return callbacks.size();
}
ProcessPool:ProcessPool.cc
g++ -o $@ $^ -std=c++11 #-DDEBUG
.PHONY:clean
clean:
rm -f ProcessPool
命令行方法是使用下面这个命令:
mkfifo filename

相关函数有:
int mkfifo(const char *filename,mode_t mode);
创建命名管道:
int main(int argc, char *argv[])
{
mkfifo("p2", 0644);
return 0;
}
如果当前打开操作是为读而打开FIFO时
如果当前打开操作是为写而打开FIFO时
#include "comm.hpp"
#include
static void getMessage(int fd)
{
char buffer[SIZE];
while (true)
{
memset(buffer, '\0', sizeof(buffer));
ssize_t s = read(fd, buffer, sizeof(buffer) - 1);
if (s > 0)
{
cout <<"[" << getpid() << "] "<< "client say> " << buffer << endl;
}
else if (s == 0)
{
// end of file
cerr <<"[" << getpid() << "] " << "read end of file, clien quit, server quit too!" << endl;
break;
}
else
{
// read error
perror("read");
break;
}
}
}
int main()
{
// 1. 创建管道文件
if (mkfifo(ipcPath.c_str(), MODE) < 0)
{
perror("mkfifo");
exit(1);
}
Log("创建管道文件成功", Debug) << " step 1" << endl;
// 2. 正常的文件操作
int fd = open(ipcPath.c_str(), O_RDONLY);
if (fd < 0)
{
perror("open");
exit(2);
}
Log("打开管道文件成功", Debug) << " step 2" << endl;
int nums = 3;
for (int i = 0; i < nums; i++)
{
pid_t id = fork();
if (id == 0)
{
// 3. 编写正常的通信代码了
getMessage(fd);
exit(1);
}
}
for(int i = 0; i < nums; i++)
{
waitpid(-1, nullptr, 0);
}
// 4. 关闭文件
close(fd);
Log("关闭管道文件成功", Debug) << " step 3" << endl;
unlink(ipcPath.c_str()); // 通信完毕,就删除文件
Log("删除管道文件成功", Debug) << " step 4" << endl;
return 0;
}
#include "comm.hpp"
int main()
{
// 1. 获取管道文件
int fd = open(ipcPath.c_str(), O_WRONLY);
if(fd < 0)
{
perror("open");
exit(1);
}
// 2. ipc过程
string buffer;
while(true)
{
cout << "Please Enter Message Line :> ";
std::getline(std::cin, buffer);
write(fd, buffer.c_str(), buffer.size());
}
// 3. 关闭
close(fd);
return 0;
}
#ifndef _COMM_H_
#define _COMM_H_
#include
#include
#include
#include
#include
#include
#include
#include
#include "Log.hpp"
using namespace std;
#define MODE 0666
#define SIZE 128
string ipcPath = "./fifo.ipc";
#endif
#ifndef _LOG_H_
#define _LOG_H_
#include
#include
#define Debug 0
#define Notice 1
#define Warning 2
#define Error 3
const std::string msg[] = {
"Debug",
"Notice",
"Warning",
"Error"
};
std::ostream &Log(std::string message, int level)
{
std::cout << " | " << (unsigned)time(nullptr) << " | " << msg[level] << " | " << message;
return std::cout;
}
#endif
.PHONY:all
all:client mutiServer
client:client.cxx
g++ -o $@ $^ -std=c++11
mutiServer:server.cxx
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -f client mutiServer
共享内存区是最快的IPC形式。一旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到内核,换句话说是进程不再通过执行进入内核的系统调用来传递彼此的数据
查看共享内存资源的脚本命令:while : ; do ipcs -m; sleep 1; done


它的本质上是一块内存被映射给了两个进程,这两个进程从而能够实现进程通信,前面讲的管道也是类似的原理,所以也能用来通信,但它们的本质上来说都是共享同一个文件。
共享内存是操作系统提供的,操作系统当然要管理共享内存,所以当我们申请共享内存的时候,得到不仅仅是共享内存块,还有管理共享内存的内核数据结构。
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 */
};
功能:用来创建共享内存
原型
int shmget(key_t key, size_t size, int shmflg);
参数
key:这个共享内存段名字,key的作用是作为标识,让通信的对方进程找到我创建的共享内存,
它是多少不重要,但是必须要在系统中唯一即可。
怎么取得唯一的key?
需要使用ftok函数来生成一个唯一的key
#include
key_t ftok(const char *pathname, int proj_id);
size:共享内存大小
shmflg:由九个权限标志构成,它们的用法和创建文件时使用的mode模式标志是一样的
返回值:成功返回一个非负整数,即该共享内存段的标识码shmid,类似曾经的fd;失败返回-1
shmflg常用两个选项:
1.IPC_CREAT:单独使用时,如果创建共享内存,如果底层已经存在,则获取后返回,
如果不存在,就创建后返回。
2.IPC_EXCL:单独使用没有意义
3.IPC_CREAT和IPC_EXCL一起使用:底层不存在就创建,底层存在出错返回,
也就是说返回成功一定是一个全新的shm
1.只有创建的时候用key,大部分情况用户访问共享内存都用的是shmid
2.key可以标识共享内存在系统层面的唯一性,shmid可以标识共享内存在用户层面上的唯一性
2.当进程运行结束后,共享内存依旧存在,system V IPC资源的生命周期随内核
3.想要删除可以手动删除,命令是ipcrm -m,也可以代码删除,使用函数shmctl
功能:将共享内存段连接到进程地址空间
原型
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,表示连接操作用来只读共享内存
功能:将共享内存段与当前进程脱离
原型
int shmdt(const void *shmaddr);
参数
shmaddr: 由shmat所返回的指针
返回值:成功返回0;失败返回-1
注意:将共享内存段与当前进程脱离不等于删除共享内存段
功能:用于控制共享内存
原型
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数
shmid:由shmget返回的共享内存标识码
cmd:将要采取的动作(有三个可取值)
buf:指向一个保存着共享内存的模式状态和访问权限的数据结构
返回值:成功返回0;失败返回-1

当为IPC_RMID选项时,即使有进程连接了共享内存,它也会删除

如上图所示,当我们使用其他的方式传输数据、信号的时候,会通过内核的处理再转给另一端,这之间会经历多次拷贝。
比如使用键盘写入,然后打印在显示器上,会经历4次拷贝如果直接使用共享内存的方式来进行相同的操作就省去了内核的处理,少了两次拷贝。
为了让进程间通信,需要让不同的进程之间看到同一份资源,在此之前讲的所有的通信方式本质都是优先解决一个问题:让不同的进程之间看到同一份资源
但是使用共享内存来让不同的进程之间看到同一份资源时会带来一些时序问题,造成数据不一致的问题,比如写端只写了一半就被读取可能会造成语义相反等情况。