hello,各位读者大大们你们好呀
系列专栏:【Linux的学习】
本篇内容:进程间通信的目的;匿名管道;命名管道;共享内存
⬆⬆⬆⬆上一篇:C++的多态
作者简介:轩情吖,请多多指教(> •̀֊•́ ) ̖́-
①数据传输:一个进程需要将它的数据发送给另一个进程
②资源共享:多个进程之间共享同样的资源
③通知事件:一个进程需要向另一个或一组进程发送消息,通知它发生了某种事情
④进程控制:有些进程希望完全控制另一个进程的执行(如debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变
①进程是具有独立性的,这无疑增加了通信成本
②要让两个不同的进程进行通信,前提条件是先让两个进程看到同一份“资源”(OS直接或间接提供)
③任何进程通信手段
a.让不同的进程看到同一份资源
b.让一方写入,一方读取,完成通信过程,至于通信目的和后续工作,要结合具体场景
接下来用一幅图来展示一下管道(匿名管道)是如何工作的
创建子进程的时候,fork子进程,只会复制进程相关的数据结构对象,不会复制父进程曾经打开的文件对象
这就是为什么fork之后,父子进程都printf,cout,都会向同一个显示器终端打印数据的原因
函数原型:int pipe([int fd[2]]);
功能:创建一个无名管道
参数:fd:文件描述符数组,其中fd[0]表示读端,fd[1]表示写端
返回值:成功返回0,失败返回-1
#include
#include
#include
#include
#include
#include
using namespace std;
// 父进程写入,子进程读取
int main()
{
// 1.创建匿名管道
int fd[2] = {0};
int ret = pipe(fd);
if (ret < 0)
{
cout << errno << ":" << strerror(errno) << endl;
}
// 2.创建子进程
int n = fork();
assert(n>=0);
if (n == 0)
{
// 子进程
// 3.关闭不必要的fd
close(fd[1]);
// 4.开始通信
while (1)
{
char buff[128];
int n=read(fd[0], buff, 128);
if(n<0)
{
cout << errno << ":" << strerror(errno) << endl;
}
if(n==0)
{
cout<<"I have already read the end of file"<<endl;
}
buff[n]='\0';
cout<<"child:";
cout<<buff<<endl;
sleep(1);
}
close(fd[0]);
exit(0);
}
// 父进程
// 3.关闭不必要的fd
close(fd[0]);
// 4.开始通信
int message=0;
while (1)
{
char buff[128];
cout<<"parent:I am the parent process,I am preparing for write to the child"<<endl;
snprintf(buff,128,"The parent process writes message to my me:%d\n", message++);
int n = write(fd[1], buff, strlen(buff));
if (n < 0)
{
cout << errno << ":" << strerror(errno) << endl;
}
sleep(1);
}
close(fd[1]);
return 0;
}
特点:
①单向通信(管道是半双工的一种特殊情况)
②管道的本质是文件,因为fd的生命周期随进程,管道的生命周期是随进程的
③管道通信,通常用来进行具有“血缘”关系的进程,进行进程间通信。常用于父子通信——pipe打开管道,并不清楚管道名字,因此称为匿名管道
④在管道通信中,写入的次数和读取的次数,不是严格匹配的——字节流
⑤具有一定的协同能力,让read和write能够按照一定的步骤进行通信——自带同步机制
四种场景:
a.如果我们的read读取完毕了所有的管道数据,如果对方不发,我就只能等待
对代码稍作修改
b.如果我们的write端将管道写满了,我们不能继续写
③如果我关闭了写端,读取完毕管道数据,再读,就会read返回0,表明读到了文件结尾
#include
#include
#include
#include
#include
#include
using namespace std;
// 父进程写入,子进程读取
int main()
{
// 1.创建匿名管道
int fd[2] = {0};
int ret = pipe(fd);
if (ret < 0)
{
cout << errno << ":" << strerror(errno) << endl;
}
// 2.创建子进程
int n = fork();
assert(n >= 0);
if (n == 0)
{
// 子进程
// 3.关闭不必要的fd
close(fd[1]);
// 4.开始通信
while (1)
{
char buff[128];
// cout<<"child:我在等待父进程发送的消息"<
int n = read(fd[0], buff, 127);
cout << "child:我等到了父进程发送的消息" << endl;
if (n < 0)
{
cout << errno << ":" << strerror(errno) << endl;
break;
}
if (n == 0)
{
cout << "I have already read the end of file" << endl;
break;
}
buff[n] = '\0';
cout << "child:";
cout << buff << endl;
sleep(1);
}
close(fd[0]);
exit(0);
}
// 父进程
// 3.关闭不必要的fd
close(fd[0]);
// 4.开始通信
int message = 0;
int count = 5;
while (count--)
{
char buff[128];
cout << "parent:I am the parent process,I am preparing for write to the child" << endl;
snprintf(buff, 128, "The parent process writes message to my me:%d\n", message++);
int n = write(fd[1], buff, strlen(buff));
if (n < 0)
{
cout << errno << ":" << strerror(errno) << endl;
break;
}
// sleep(5);
}
close(fd[1]);
return 0;
}
④写端一直写,读端关闭,则没有意义,OS不会维护无意义,低效率或浪费资源的事情。OS会杀掉一直在写入的进程!OS会通过信号来终止进程13)SIGPIPE
如果我们想在不相关的进程之间交换数据,可以使用FIFO文件
命令:mkfifo filename
函数:int mkfifo(const char*filename,mode_t mode);
//header.h
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "fcntl.h"
using namespace std;
#define filename "fifo"
#define MAX 128
__mode_t mode=0666;
//server.cpp
#include "header.h"
int main()
{
// 1.创建命名管道
umask(0);
int ret = mkfifo(filename, mode);
if (ret < 0)
{
cout << errno << ":" << strerror(errno) << endl;
exit(1);
}
// 2.打开命名管道文件
int fd = open(filename, O_RDONLY);
assert(ret != -1);
// 3.准备通信
while (1)
{
cout << "server:I have already prepared for receiving the client message" << endl;
char buf[MAX] = {0};
int n = read(fd, buf, sizeof(buf) - 1);
if (n < 0)
{
cout << errno << ":" << strerror(errno) << endl;
break;
}
else if (n == 0)
{
cout << "读到文件尾" << endl;
break;
}
else
{
buf[n]='\0';
cout<<"server:I have received the client message"<<endl;
cout<<buf<<endl;
}
sleep(1);
}
unlink(filename);//删除命令管道
return 0;
}
//client.cpp
#include "header.h"
int main()
{
// 1.打开命名管道
int fd = open(filename, O_WRONLY | O_TRUNC);
cout<<"client:I have already opened the namefile"<<endl;
assert(fd != -1);
// 2.开始通信
int count = 0;
while (1)
{
char buf[MAX] = {0};
sprintf(buf, "the client send message is %d", count++);
cout<<"client:I have already prepared for sending message to the server"<<endl;
int ret = write(fd, buf, strlen(buf));
if (ret <= 0)
{
cout << errno << ":" << strerror(errno) << endl;
break;
}
sleep(1);
}
return 0;
}
如果当前打开操作是为读而打开FIFO时:
O_NONBLOCK disable:阻塞直到有相应进程为写而打开该FIFO
O_NONBLOCK enable:立刻返回成功
如果当前打开操作是为写而打开FIFO时:
O_NONBLOCK disable:阻塞直到有相应进程为读而打开该FIFO
O_NONBLOCK enable:立刻返回失败,错误码为ENXIO
可以看到我们的读端fifoserver程序开始运行时并没有反应,直到client也开始运行,这代表着读端阻塞到client为写而打开fifo
命名管道总结:使两个毫不相关的进程看到同一个文件靠的是文件的唯一性,路径,让不同的进程通过文件路径+文件名看到同一个文件,并打开,就是看到了同一个资源
共享内存就是在内存中找一块空间开辟出来,然后将共享内存映射到共享它的进程地址空间
在任意时刻,可能有多个共享内存被用来通信,因此系统为了管理共享内存,构建对应的描述共享内存的结构体对象
共享内存=共享内存的内核数据结构+真正开辟的内存空间
首先我们要使用ftok函数创建key值来区分不同的共享内存,当进程A创建共享内存后,进程B通过对应的key值来找到对应的共享内存
参数:
pathname:路径字符串
proj_id:项目id
返回值为一个共享内存段的名字(后面使用),它是通过上面的路径和项目id进行一个算法,来计算出的一个值(整数)
功能:用来创建共享内存
参数:key:这个共享内存的名字
size:共享内存的大小
shmflg:权限标志
常用的权限标志有:IPC_CREAT和IPC_EXCL
单独使用IPC_CREAT:创建一个共享内存,如果共享内存不存在,就创建,如果已经存在,获取已经存在的共享内存并返回
IPC_EXCL不能单独使用,一般都要配合IPC_CREAT
IPC_CREAT|IPC_EXCL:创建一个共享内存,如果共享内存不存在,就创建之,如果已经存在,则立马出错返回——如果创建成功,对应的shm一定是最新的
返回值:成功返回一个非负整数,即该共享内存段的标识码;失败返回-1
功能:控制共享内存
参数:shmid:由shmget返回的共享内存标识码
cmd:将要采取的动作,如下(常用)
IPC_STAT:有权限的情况下,可以使buf中存在共享内存相关属性并查看
IPC_RMID:删除共享内存
buf:指向一个保存着共享内存的模式状态和访问权限的数据结构
功能:将共享内存段连接到进程地址空间
参数:shmid:共享内存标识
shmaddr:指定链接的地址
shmflg:通常直接用0,既可以读也可以写
功能:将共享内存段与当前进程脱离
参数:shmaddr:又shamt返回的指针
返回值:成功0,失败-1
注意:将共享内存段与当前进程脱离不等于删除共享内存段
//server.cpp
#include "header.hpp"
int main()
{
// 1.获取key值
key_t key = Getkey();
// 2.创建共享内存
int shmid = Createshm(key);
// 3.和共享内存关联
char *start = Attachshm(shmid);
// 4.通信
int count = 30;
while (count--)
{
cout <<"client->server:" <<start << endl;
sleep(1);
}
// 5.脱离关联
Awayshm(start);
// 6.删除共享内存
Deleteshm(shmid);
return 0;
}
//client.cpp
#include "header.hpp"
int main()
{
//1.获取key值
key_t key=Getkey();
//2.获取共享内存
int shmid=Getshm(key);
//3.和共享内存关联
char* start=Attachshm(shmid);
//4.通信
char c = 'A';
while (c <= 'Z')
{
start[c - 'A'] = c;
c++;
start[c - 'A'] = '\0';
sleep(1);
}
//5.脱离关联
Awayshm(start);
return 0;
}
//header.hpp
#include
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
#define pathname "."
#define project_id 1
#define SIZE 4096
// 获取key值
key_t Getkey()
{
return ftok(pathname, 1);
}
// 创建共享内存
int Createshm(key_t k)
{
umask(0);
int n = shmget(k, SIZE, IPC_CREAT | IPC_EXCL|0666);
if (n < 0)
{
cout << errno << ":" << strerror(errno) << endl;
exit(-1);
}
return n;
}
// 获取共享内存
int Getshm(key_t k)
{
int n = shmget(k, SIZE, IPC_CREAT);
if (n < 0)
{
cout << errno << ":" << strerror(errno) << endl;
exit(-1);
}
return n;
}
//删除共享内存
void Deleteshm(int shmid)
{
shmctl(shmid,IPC_RMID,nullptr);
}
//连接共享内存
char* Attachshm(int shmid)
{
return (char*)shmat(shmid,nullptr,0);
}
//脱离共享内存
void Awayshm(char * addr)
{
shmdt(addr);
}
①ipcs
查看System V标准通信信息
②ipcs -m
查看共享内存信息
③ipcrm -m shmid
删除特定共享内存
perms指的是权限
nattch是连接进程的数量
①共享内存的大小是以PAGE(4KB)为单位
②一旦共享内存映射到进程的地址空间,该共享内存就直接被所对应的进程直接看到了
③因为共享内存的这种特性,可以让进程通信的时候,减少拷贝次数,所以共享内存是所有进程间通信中速度最快的
④共享内存没有任何保护机制(同步互斥)->由于管道通过系统接口通信,共享内存直接通信
⑤同步互斥:
我们把大家都能看到的资源:公共资源
a.互斥:任何一个时刻,都只允许一个执行流在进行共享资源的访问-加锁
b.临界资源:我们把任意时刻,都只允许一个执行流在进行访问的资源叫做临界资源
c.临界区:临界资源是要通过代码访问的,凡是访问临界资源的代码,叫做临界区
d.原子性:要么不做,要么做完,只有两种确定状态的属性
⑥信号量/信号灯
本质就是一个计数器。描述资源数量的计数器
任何一个执行流想要访问临界资源中的一个子资源时,不能直接访问
a.需要申请信号量才可以,先申请信号量资源,相等于(count–),只要我申请信号量成功,我就一定能在未来拿到一个子资源(p操作)
b.进入自己的临界区,访问对应的临界资源
c.释放信号量资源,相等于(count++),只要将计数器增加,就表示将我们对应的资源进行了归还(v操作)
进程通过执行代码来申请信号量,所有的进程都得先看到信号量,因为我们想让不同的进程看到同一个信号量,所以信号量也被归为了进程间通信
如果计数器为1时,称为二元信号量,有互斥功能
相等于信号量也是共享资源,因此要必须保证自己的操作++,–是原子性的
⑦理解IPC
以上是system V中的三种通信方式,我们只讲了共享内存,其他两种暂时了解即可,但是重点在于看他们的struct xxx_ds中的第一个成员,struct ipc_perm shm_perm,三个通信方式都是一样的,其实在内核中,所有的ipc资源统一以数组方式进行管理
可以看成类似于C++的多态
进程间通信的知识大概就讲到这里啦,博主后续会继续更新更多Linux的相关知识,干货满满,如果觉得博主写的还不错的话,希望各位小伙伴不要吝啬手中的三连哦!你们的支持是博主坚持创作的动力!