进程间通信的本质是让 不同的进程看到同一份资源(内存 , 文件,内核缓冲等)
资源由谁(OS的哪些模块)提供 , 就有了不同的进程间通信方式!
这里的模块可以是:(文件也就是管道) ,(OS内核IPC提供即SystemV IPC) 。(网络–套接字)
管道
System V IPC
POSIX IPC
只能用于具有亲缘关系的进程之间进行通信的管道称为匿名管道,其常用于父子。
进程间通信的本质就是,让不同的进程看到同一份资源,使用匿名管道实现父子进程间通信的原理就是,让两个父子进程先看到同一份被打开的文件资源,然后父子进程就可以对该文件进行读写操作,进而实现父子进程间通信
int pipe(int fd[2]);
功能:创建一个无名管道。
参数fd:文件描述符数组 , 其中 fd[0] 表示读端,fd[1] 表示写端(快速记忆方法:0像一张嘴巴:读,1像一根笔:写)。
返回值:成功返回 0 ,失败返回错误代码。
(1)代码演示:
#include
#include
#include
#include
#include
#include
#include
using namespace std;
int main()
{
int fd[2] = {0};
int p = pipe(fd);
if (p < 0)
{
perror("pipe");
}
pid_t id = fork();
if (id == 0)
{
close(fd[0]); // 子进程关闭读端
// 子进程向管道写入数据
const char *buf = "hello father, I am child...";
int count = 5;
while (count--)
{
write(fd[1], buf, strlen(buf));
sleep(1); // 每隔一秒写一条数据
}
close(fd[1]); // 子进程写入完毕,关闭文件
exit(0);
}
else
{
close(fd[1]); // 父进程关闭写端
char buff[64] = {0};
while (1)
{
ssize_t s = read(fd[0], buff, sizeof(buff));
if (s > 0)
{
buff[s] = '\0'; // C语言读写规则
cout << "child send to father:" << buff << endl;
}
else if (s == 0)
{
cout << "read file end" << endl;
break;
}
else
{
cout << "read error" << endl;
break;
}
}
close(fd[0]);
pid_t pid = waitpid(id, NULL, 0);
}
return 0;
}
(2)运行结果:
创建匿名管道实现父子进程间通信的过程中,需要pipe函数和fork函数搭配使用,具体步骤如下:
具体步骤如下:
如图:
所以,看待管道,就如同看待文件一样!管道的使用和文件一致,迎合了“Linux一切皆文件思想”。
demo版的进程池代码:
//test.cpp
#include "Task.hpp"
#include
#include
#include
#include
#include
#include
#include
using namespace std;
vector<task_t> tasks;
//先描述
class channel
{
public:
channel(int cmdfd, int slaverid, const string& processname)
:_cmdfd(cmdfd)
, _slaverid(slaverid)
, _processname(processname)
{}
int _cmdfd; // 发送任务的文件描述符
pid_t _slaverid; // 子进程的PID
std::string _processname; // 子进程的名字 -- 方便我们打印日志
};
void slaver()
{
while(1)
{
int cmdcode = 0;
int n = read(0, &cmdcode, sizeof(int));
if(n == sizeof(int))
{
//执行cmdcode对应的任务列表
cout <<"slaver say@ get a command: "<< getpid() << " : cmdcode: " << cmdcode << endl;
if(cmdcode >= 0 && cmdcode < tasks.size())
{
tasks[cmdcode]();
}
}
if(n == 0)
{
break;
}
}
}
void InitProcessPool(vector<channel>* channels)
{
for(int i = 0; i < 10; i++)
{
int pipefd[2]; // 临时空间
int n = pipe(pipefd);
assert(!n); // 演示就可以
(void)n;
pid_t id = fork();
if(id == 0)
{
close(pipefd[1]);
dup2(pipefd[0], 0);
close(pipefd[0]);
slaver();
cout << "process : " << getpid() << " quit" << endl;
exit(0);
}
// father
close(pipefd[0]);
// 添加channel字段了
std::string name = "process-" + std::to_string(i);
channels->push_back(channel(pipefd[1], id, name));
}
}
void Menu()
{
cout << "################################################" << endl;
cout << "# 1. 刷新日志 2. 刷新出来野怪 #" << endl;
cout << "# 3. 检测软件是否更新 4. 更新用的血量和蓝量 #" << endl;
cout << "# 0. 退出 #" << endl;
cout << "#################################################" << endl;
}
void ctrlSlaver(const vector<channel>& channels)
{
// 选择几号进程
int which = 0;
while(1)
{
int select = 0;
Menu();
cout << "Please Enter@ ";
cin >> select;
while(select < 0 || select >= 5)
{
cout << "Enter error" << endl;
cout << "Please again Enter@ ";
cin >> select;
}
if(select == 0)
{
break;
}
// 选择任务
// int cmdcode = rand() % tasks.size(); --> 随机挑选
int cmdcode = select - 1;
cout << "father say: " << " cmdcode: " <<
cmdcode << " already sendto " << channels[which]._slaverid << " process name: "
<< channels[which]._processname << endl;
write(channels[which]._cmdfd, &cmdcode, sizeof(cmdcode));
which++;
which %= channels.size();
}
}
void QuitProcess(vector<channel>& channels)
{
for(auto& e : channels)
{
close(e._cmdfd);
}
for(auto& e : channels)
{
waitpid(e._slaverid, nullptr, 0);
}
}
int main()
{
LoadTask(&tasks);
//srand(time(nullptr) ^ getpid() ^ 1023); // 种一个随机数种子
vector<channel> channels;
//初始化创建管道和子进程
InitProcessPool(&channels);
// 开始控制子进程
ctrlSlaver(channels);
// 清理
QuitProcess(channels);
return 0;
}
//Task.hpp
#pragma once
#include
#include
typedef void (*task_t)();
void task1()
{
std::cout << "lol 刷新日志" << std::endl;
}
void task2()
{
std::cout << "lol 更新野区,刷新出来野怪" << std::endl;
}
void task3()
{
std::cout << "lol 检测软件是否更新,如果需要,就提示用户" << std::endl;
}
void task4()
{
std::cout << "lol 用户释放技能,更新用的血量和蓝量" << std::endl;
}
void LoadTask(std::vector<task_t> *tasks)
{
tasks->push_back(task1);
tasks->push_back(task2);
tasks->push_back(task3);
tasks->push_back(task4);
}
pipe2函数与pipe函数类似,用于创建匿名管道,其函数原型如下:
int pipe2(int pipefd[2], int flags);
pipe2函数的第二个参数用于设置选项。
当没有数据可读时:
当管道满的时候:
如果所有管道写端对应的文件描述符被关闭,则read返回0。
如果所有管道读端对应的文件描述符被关闭,则write操作会产生信号SIGPIPE,进而可能导致write进程退出。
当要写入的数据量不大于PIPE_BUF时,Linux将保证写入的原子性。
当要写入的数据量大于PIPE_BUF时,Linux将不再保证写入的原子性。
(1)管道内部自带同步与互斥机制。
(2)管道的生命周期随进程。
管道本质上是通过文件进行通信的,也就是说管道依赖于文件系统,那么当所有打开该文件的进程都退出后,该文件也就会被释放掉,所以说管道的生命周期随进程。
(3)管道提供的是流式服务。
我们一般所谓的流式概念就是,给你提供一个通信的信道,你的写端就直接写,读端直接读,但是具体写多少,读多少完全有上层决定。底层就只是提供一个数据通信的信道就完了,它不关心数据本身的一些细节格式,这叫做面向字节流。
流式服务: 数据没有明确的分割,一次拿多少数据都行。
数据报服务: 数据有明确的分割,拿数据按报文段拿。
(4)管道是半双工通信的。
在数据通信中,数据在线路上的传送方式可以分为以下三种:
(1)前面两种情况就能够很好的说明,管道是自带同步与互斥机制的,读端进程和写端进程是有一个步调协调的过程的,不会说当管道没有数据了读端还在读取,而当管道已经满了写端还在写入。读端进程读取数据的条件是管道里面有数据,写端进程写入数据的条件是管道当中还有空间,若是条件不满足,则相应的进程就会被挂起,直到条件满足后才会被再次唤醒。
(2)如何理解阻塞挂起和唤醒
进程先立即停止执行,然后将PCB的状态改为阻塞状态,并将PCB插入相应的阻塞队列。
当被阻塞进程所期待的事情发生,将阻塞进程从阻塞队列中移出,将其PCB的状态改为就绪状态®,然后将PCB插入到就绪队列中.
(3)对第三种情况的理解,读端进程已经将管道当中的所有数据都读取出来了(读端就会read返回值0,代表文件结束),而且此后也不会有写端再进行写入了,那么此时读端进程也就可以执行该进程的其他逻辑了,而不会被挂起。
(4) 对第四种情况的理解,既然管道当中的数据已经没有进程会读取了,那么写端进程的写入将没有意义,因此操作系统直接将写端进程杀掉。而此时子进程代码都还没跑完就被终止了,属于异常退出,那么子进程必然收到了某种信号。
(5)管道是单向通信,如果读端不读数据且把文件描述符关闭,那么写端做的就没有意义了。
写端相当于废弃的动作,浪费资源,所以OS直接将子进程干掉。为什么?OS不做不做任何浪费空间或者低效的事情,只要发现OS一定要把这个事情修正了。
(6)验证OS发送信号杀掉进程
#include
#include
#include
#include
#include
#include
using namespace std;
int main()
{
int fd[2] = {0};
int p = pipe(fd);
if(p < 0)
{
perror("pipe");
}
pid_t id = fork();
if (id == 0)
{
//child
close(fd[0]); //子进程关闭读端
//子进程向管道写入数据
const char* buf = "I am child...";
while (1)
{
write(fd[1], buf, strlen(buf));
sleep(1);
}
close(fd[1]); //子进程写入完毕,关闭文件
exit(0);
}
//father
close(fd[1]); //关闭写端
int count = 5;
char buf[64] = {0};
while(count--)
{
buf[0] = 0;
read(fd[0], buf, 13);
cout << buf << endl;
sleep(1);
}
close(fd[0]); //关闭读端
int status = 0;
waitpid(id, &status, 0);
printf("child get signal:%d\n", status & 0x7F); //打印子进程收到的信号
return 0;
}
(7)运行结果:
(8)使用命令查看信号 kill - l
管道的容量是有限的,如果管道已满,那么写端将阻塞或失败,需要了解一下管道的大小。
(1)方法一:使用man手册
根据man手册,在2.6.11之前的Linux版本中,管道的最大容量与系统页面大小相同,从Linux 2.6.11往后,管道的最大容量是65536字节。
查看Linux系统版本
这里使用的是Linux 2.6.11之后的版本,因此管道的最大容量是65536字节。
(2)方法二:使用ulimit命令
可以使用ulimit -a 命令,查看当前资源限制的设定, 管道的最大容量是 512 × 8 = 4096 字节
(3)代码验证管道容量
#include
#include
#include
#include
int main()
{
int fd[2] = { 0 };
if (pipe(fd) < 0)
{
//使用pipe创建匿名管道
perror("pipe");
return 1;
}
pid_t id = fork(); //使用fork创建子进程
if (id == 0)
{
//child
close(fd[0]); //子进程关闭读端
char c = '.';
int count = 0;
while (1)
{
write(fd[1], &c, 1);
count++;
printf("%d\n", count); //打印当前写入的字节数
}
close(fd[1]);
exit(0);
}
//father
close(fd[1]); //父进程关闭写端
//父进程不读取数据
waitpid(id, NULL, 0);
close(fd[0]);
return 0;
}
(1)命名管道从命令行上创建,命令行方法是使用下面这个命令:
mkfifo filename
(2)通过命令使用命名管道:
①一个进程通过shell脚本重定向到管道 ,不断地往 pipe管道里面写数据 ;另一个进程使用cat命令 不断地读数据。这两个毫不相关的进程可以通过命名管道进行数据传输,完成了通信。
②使用cat命令的读进程主动退出,另一个写进程就被杀掉了; 因为写端执行的循环脚本是由命令行解释器bash执行的,所以此时bash就会被操作系统杀掉,我们的云服务器也就退出了。(当管道的读端进程退出后,写端进程再向管道写入数据就没有意义了,此时写端进程会被操作系统杀掉)
(3)命名管道从程序里创建,相关函数有:
int mkfifo(const char* filename,mode_t mode);
mkfifo函数的第一个参数是:pathname,表示要创建的命名管道文件。
mkfifo函数的第二个参数mode:表示创建命名管道文件的默认权限。
返回值: 命名管道创建成功,返回0;命名管道创建失败,返回-1。
(1)如果当前打开操作是为读而打开FIFO时。
(2)如果当前打开操作是为写而打开FIFO时。
(1)通过命名管道 ,client & server进行通信
①comm.hpp
#pragma once
#include
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
#define FIFO_FILE "./myfifo"
#define MODE 0664
enum
{
FIFO_CREATE_ERR = 1,
FIFO_DELETE_ERR,
FIFO_OPEN_ERR
};
class Init
{
public:
Init()
{
int n = mkfifo(FIFO_FILE, MODE);
if(n < 0)
{
perror("mkfifo");
exit(FIFO_CREATE_ERR);
}
}
~Init()
{
int m = unlink(FIFO_FILE);
if (m == -1)
{
perror("unlink");
exit(FIFO_DELETE_ERR);
}
}
};
②server.cpp
实现服务端(server)和客户端(client)之间的通信之前,我们需要先让服务端运行起来,我们需要让服务端运行后创建一个命名管道文件,然后再以读的方式打开该命名管道文件,之后服务端就可以从该命名管道当中读取客户端发来的通信信息了。
#include "comm.hpp"
int main()
{
Init init;
// 打开管道
int fd = open(FIFO_FILE, O_RDONLY);
if (fd < 0)
{
perror("open");
exit(FIFO_OPEN_ERR);
}
// 开始通信
while(1)
{
char buf[1024];
int x = read(fd, buf, sizeof(buf));
if (x > 0)
{
buf[x] = 0;
cout << "client say# " << buf << endl;
}
else if (x == 0)
{
perror("read");
break;
}
else
break;
}
close(fd);
return 0;
}
③client.cpp
对于客户端来说,因为服务端运行起来后命名管道文件就已经被创建了,所以客户端只需要以写的方式打开该命名管道文件,之后客户端就可以将通信信息写入到命名管道文件当中,进而实现和服务端的通信。
#include "comm.hpp"
int main()
{
int fd = open(FIFO_FILE, O_WRONLY);
if(fd < 0)
{
perror("open");
exit(FIFO_OPEN_ERR);
}
cout << "client open file done" << endl;
string line;
while(1)
{
cout << "Please Enter@ ";
getline(cin, line);
write(fd, line.c_str(), line.size());
}
close(fd);
return 0;
}
Makefile快速编译:
.PHONY:all
all:server client
server:server.cpp
g++ -o $@ $^ -g -std=c++11
client:client.cpp
g++ -o $@ $^ -g -std=c++11
.PHONY:clean
clean:
rm -f server client
④运行结果:
⑤client 和 server 谁先退出?
如果客户端先退出,服务端将管道当中的数据读完后就再也读不到数据了,那么此时服务端也就会去执行它的其他代码了(在当前代码中则是直接退出了)。
如果服务端先退出,客户端写入管道的数据就不会被读取了,也就没有意义了,那么当客户端下一次再向管道写入数据时,就会收到操作系统发来的13号信号(SIGPIPE),此时客户端就被操作系统强制杀掉了。
⑥通信是在内存当中进行的
运行程序前后两次查看myfifo管道文件的大小始终为0,说明了双方进程之间的通信依旧是在内存当中进行的,和匿名管道通信是一样的。
(2)通过命名管道,派发计算任务
(3)通过命名管道,进行命令操作
(4)通过命名管道,进行文件拷贝
(1)使用cat 和 grep 命令 , 利用 管道 “ | ” 对信息进行过滤
(2)管道“ | ” 是匿名管道还是命名管道 ?
①由于匿名管道只能用于有亲缘关系的进程之间的通信,而命名管道可以用于两个毫不相关的进程之间的通信,因此我们可以先看看命令行当中用管道(“|”)连接起来的各个进程之间是否具有亲缘关系。
②通过管道连接了三个进程,通过ps命令查看这三个进程可以发现,这三个进程的PPID是相同的,也就是说它们是由同一个父进程创建的子进程。
③即这三个sleep进程的父进程是bash ,三个sleep进程互为兄弟
所以若是两个进程之间采用的是命名管道,那么在磁盘上必须有一个对应的命名管道文件名,而实际上我们在使用命令的时候并不存在类似的命名管道文件名,因此命令行上的管道实际上是匿名管道。
管道通信本质是基于文件的,也就是说操作系统并没有为此做过多的设计工作,而system V IPC是操作系统特地设计的一种通信方式。但是不管怎么样,它们的本质都是一样的,都是让不同的进程看到同一份资源。
system V IPC提供的通信方式有以下三种:
其中,system V共享内存和system V消息队列是以传送数据为目的的,而system V信号量是为了保证进程间的同步与互斥而设计的,虽然system V信号量和通信好像没有直接关系,但属于通信范畴。
共享内存让不同进程看到同一份资源的方式 : 在物理内存当中申请一块内存空间,然后将这块内存空间分别与需要进行进程间通信的进程的页表之间建立映射,再将虚拟地址空间当中开辟空间并将虚拟地址填充到各自页表的对应位置,使得虚拟地址和物理地址之间建立起对应关系,至此这些进程便看到了同一份物理内存,这块物理内存就叫做共享内存。
①如何实现: 本质就是修改页表,虚拟地址空间中开辟空间
②是谁做的: 开辟物理空间开辟虚拟地址、建立映射等操作都是调用系统接口完成的,也就是说这些动作都由操作系统来完成。
(1)在系统当中可能会有大量的进程在进行通信,因此系统当中就可能存在大量的共享内存,那么操作系统必然要对其进行管理,所以共享内存除了在内存当中真正开辟空间之外,系统一定还要为共享内存维护相关的内核数据结构。
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 */
};
(2)当我们申请了一块共享内存后,为了让要实现通信的进程能够看到同一个共享内存,因此每一个共享内存被申请时都要有一个key值,这个key值用于标识系统中共享内存的唯一性。也就是让两个进程看到的是同一个共享内存。
如上共享内存数据结构的第一个成员是shm_perm,shm_perm是一个ipc_perm类型的结构体变量,每个共享内存的key值都是存储在shm_perm这个结构体变量当中,其中ipc_perm结构体的定义如下:
struct ipc_perm {
__kernel_key_t key;
__kernel_uid_t uid;
__kernel_gid_t gid;
__kernel_uid_t cuid;
__kernel_gid_t cgid;
__kernel_mode_t mode; //权限
unsigned short seq;
};
(1)共享内存的建立大致有如下两个过程:
(2)共享内存的释放大致有如下两个过程:
int shmget(key_t key, size_t size, int shmflg);
参数说明:
返回值说明:
shmget函数的第三个参数shmflg,常用的组合方式:
使用结果:
key_t ftok(const char *pathname, int proj_id);
(1)ftok函数和shmget函数代码演示:
#include
#include
#include
#include
#include
#define PATHNAME "./server.cpp" //路径名
#define PROJ_ID 0x1234 //整数标识符
#define SIZE 4096 //共享内存的大小
int main()
{
key_t k = ftok(PATHNAME, PROJ_ID); // 生成唯一key
if (k < 0)
{
perror("ftok error!\n");
return 1;
}
printf("key: %x\n", k);
int shmid = shmget(k, SIZE, IPC_CREAT | IPC_EXCL | 0644); // 创建共享内存
if (shmid < 0)
{
perror("shmget");
return 2;
}
printf("shmid: %d\n", shmid);
return 0;
}
(2)运行结果:
(3)再次运行:
这是进程退出后,申请的共享内存依旧存在,并没有被操作系统释放,在次get后就会出错。
(4)查看共享内存信息
①使用ipcs命令查看
单独使用ipcs命令时,会默认列出消息队列、共享内存以及信号量相关的信息,若只想查看它们之间某一个的相关信息,可以选择携带以下选项:
②使用ipcs -m查看共享内存
上图各个标题的含义分别是:
标题 | 含义 |
---|---|
key | 系统区别共享内存的唯一标识 |
shmid | 共享内存的用户层id |
owner | 共享内存的拥有者 |
perms | 共享内存的权限 |
bytes | 共享内存的大小 |
nattch | 关联共享内存的进程数 |
status | 共享内存的状态 |
注意: key是在内核层面上保证共享内存唯一性的方式,而shmid是在用户层面上保证共享内存的唯一性,key和shmid之间的关系相当于fd和FILE*之间的的关系。
(5)第二个参数:size
如果我们把size设置成4097 ,在操作系统底层会给你分配2页(按页对齐),但是你要4097字节那么我就只让你看到4097个字节的空间,绝对不少给你但也不多给你,少给了可能会出问题,多给了也可能出问题,所以最好设置4096的整数倍。
使用ipcrm -m shmid命令释放指定id的共享内存资源
[xiaomaker@VM-28-13-centos test_11_21]$ ipcrm -m 0
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数:
返回值:
(1)shmctl函数的cmd参数传入的常用的选项:
(2)shmctl函数代码演示:
#include
#include
#include
#include
#include
#define PATHNAME "./server.cpp" //路径名
#define PROJ_ID 0x1234 //整数标识符
#define SIZE 4096 //共享内存的大小
int main()
{
key_t k = ftok(PATHNAME, PROJ_ID); // 生成唯一key
if (k < 0)
{
perror("ftok error!\n");
return 1;
}
printf("key: %x\n", k);
int shmid = shmget(k, SIZE, IPC_CREAT | IPC_EXCL | 0644); // 创建共享内存
if (shmid < 0)
{
perror("shmget");
return 2;
}
printf("shmid: %d\n", shmid);
sleep(5);
shmctl(shmid, IPC_RMID, NULL); // 释放共享内存
return 0;
}
(3)运行server.cpp5s后释放共享内存,使用shell脚本监视:
[xiaomaker@VM-28-13-centos test_11_21]$ while :; do ipcs -m; echo "##################" ; sleep 1 ;done
(4)运行结果:
void* shmat(int shmid, const void *shmaddr, int shmflg);
参数:
返回值:(和malloc很像)
参数shmflg传入的常用的选项:
(1)shmat函数代码演示:
#include
#include
#include
#include
#include
#define PATHNAME "./server.cpp" //路径名
#define PROJ_ID 0x1234 //整数标识符
#define SIZE 4096 //共享内存的大小
int main()
{
key_t k = ftok(PATHNAME, PROJ_ID); // 生成唯一key
if (k < 0)
{
perror("ftok error!\n");
return 1;
}
printf("key: %x\n", k);
int shmid = shmget(k, SIZE, IPC_CREAT | IPC_EXCL | 0664); // 创建共享内存
if (shmid < 0)
{
perror("shmget");
return 2;
}
printf("shmid: %d\n", shmid);
printf("attach begin!\n");
char* mem = (char*)shmat(shmid, NULL, 0); // 关联共享内存
if (mem == "-1")
{
perror("shmat");
return 3;
}
printf("attach end!\n");
sleep(5);
shmctl(shmid, IPC_RMID, NULL); // 释放共享内存
return 0;
}
(2)运行结果:
int shmdt(const void *shmaddr);
参数:
返回值:
注意:将共享内存段与当前进程脱离不等于删除共享内存,只是取消了当前进程与该共享内存之间的联系。
(1)comm.hpp共同包含的头文件
为了让client和server在使用ftok函数获取key值时,能够得到同一种key值,那么服务端和客户端传入ftok函数的路径名和和整数标识符必须相同,这样才能生成同一种key值,进而找到同一个共享内存进行挂接。
#pragma once
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
const int size = 4096;
#define pathname "/home/xiaomaker"
const int proj_id = 0x6666;
key_t Getkey() //创建唯一标识符k值
{
key_t k = ftok(pathname, proj_id);
if(k < 0)
{
perror("ftok");
exit(1);
}
return k;
}
int GetShareMemHelper(int flag) //创建共享内存
{
key_t k = Getkey();
int shmid = shmget(k, size, flag);
if(shmid < 0)
{
perror("shmget");
exit(1);
}
return shmid;
}
int CreateShm()
{
return GetShareMemHelper(IPC_CREAT | IPC_EXCL | 0666);
}
int GetShm()
{
return GetShareMemHelper(IPC_CREAT);
}
#define FIFO_FILE "./myfifo"
#define MODE 0664
class Init
{
public:
Init()
{
int n = mkfifo(FIFO_FILE, MODE);
if(n < 0)
{
perror("mkfifo");
exit(1);
}
}
~Init()
{
int m = unlink(FIFO_FILE);
if (m == -1)
{
perror("unlink");
exit(1);
}
}
};
(3)server.c 建立共享内存,并建立关联,从共享内存中接收数据
#include "comm.hpp"
int main()
{
Init init;
int shmid = CreateShm();
char* shaddr = (char*)shmat(shmid, nullptr, 0);
int fd = open(FIFO_FILE, O_RDONLY);
if (fd < 0)
{
perror("open");
exit(1);
}
//struct shmid_ds shmds;
while (1)
{
char c;
ssize_t s = read(fd, &c, 1); //看client是否写入了数据
if (s == 0)
{
break;
}
else if (s < 0)
{
break;
}
std::cout << "client say@ " << shaddr; //直接访问共享内存
sleep(1);
shmctl(shmid, IPC_RMID, nullptr);
//shmctl(shmid, IPC_STAT, &shmds);
//std::cout << "shm size: " << shmds.shm_segsz << std::endl;
//std::cout << "shm nattch: " << shmds.shm_nattch << std::endl;
//printf("shm key: 0x%x\n", shmds.shm_perm.__key);
//std::cout << "shm mode: " << shmds.shm_perm.mode << std::endl;
}
shmdt(shaddr); //去关联
shmctl(shmid , IPC_RMID , nullptr); //删除共享内存
close(fd);
return 0;
}
(4)client.c 通过k找到共享内存建立关联,向共享内存中发送数据
#include "comm.hpp"
int main()
{
int shmid = GetShm();
char* shaddr = (char*)shmat(shmid, nullptr, 0);
int fd = open(FIFO_FILE, O_WRONLY); // 等待写入方打开之后,自己才会打开文件,向后执行, open 阻塞了!
if (fd < 0)
{
perror("open");
exit(1);
}
while (1)
{
std::cout << "Please Enter@ ";
fgets(shaddr, 4096, stdin);
write(fd, "c", 1); // 通知对方
//shaddr[i] = 'A' + i;
sleep(1);
//i++;
//shaddr[i] = '\0';
}
shmdt(shaddr); //去关联
close(fd);
return 0;
}
(4)运行结果:
当共享内存创建好后就不再需要调用系统接口进行通信了(直接对地址空间进行操作),而管道创建好后仍需要read、write等系统接口进行通信。实际上,共享内存是所有进程间通信方式中最快的一种通信方式
read是把数据从内核缓冲区复制到进程缓冲区 , write是把进程缓冲区复制到内核缓冲区
(1)所以共享内存是所有进程间通信方式中最快的一种通信方式,因为该通信方式需要进行的拷贝次数最少。
(2)但是共享内存也是有缺点的,我们知道管道是自带同步与互斥机制的,但是共享内存并没有提供任何的保护机制,包括同步与互斥。
共享内存的拷贝次数少
在使用共享内存时不涉及系统调用接口(也就是不会有内核态到用户态之间的转化,因为都是在用户层进行操作的)
不提供任何保护机制(没有同步与互斥)
共享内存的使用方法就和使用堆空间类似,直接向共享内存中写入,另一个进程直接就能看到。它与管道不同,管道需要拷贝数据到管道,另一个进程再从管道中拷贝数据到自己当中。
消息队列实际上就是在系统当中创建了一个队列,队列当中的每个成员都是一个数据块,这些数据块都由类型和信息两部分构成,两个互相通信的进程通过某种方式看到同一个消息队列,两个进程向对方发数据时,都在消息队列的队尾添加数据块,两个进程获取数据块时,都在消息队列的队头取数据块。其中消息队列当中的某一个数据块是由谁发送给谁的,取决于数据块的类型。
上面代码的本质是:当进程A申请访问共享内存资源时,如果此时sem为1(sem代表当前信号量个数),则进程A申请资源成功,此时这个资源就被A进程占有,此时需要将sem- -,然后进程A就可以对共享内存进行一系列操作,如果在进程A在访问共享内存时,进程B想要申请访问该共享内存资源,此时sem就为0了,那么这时进程B会被挂起,直到进程A访问共享内存结束后将sem++,此时才会将进程B唤起,然后进程B再对该共享内存进行访问操作。
在这种情况下,无论什么时候都只会有一个进程在对同一份共享内存进行访问操作,也就解决了临界资源的互斥问题。