Linux——进程间通信2

 

目录

命名管道

管道测试小实验 

 模拟命名管道通信

 server.cxx

client.cxx 

 comm.hpp

Log.hpp 

makefile 

创建子进程

 system V共享内存

模拟实现共享内存

删除共享内存 

 将共享内存挂接到自己的地址空间

去除挂接 

通过共享内存进行通信

 comm.hpp

Log.hpp 

shmClient.cc(发送端) 

shmServer.cc (读取端)

习题 


命名管道

Linux——进程间通信2_第1张图片

A进程打开fifo.ipc文件后,若B进程也想打开,此时只需要把该文件的地址填到B进程的文件描述符表里面 。此时A,B俩进程可以看到同一份文件。

这里我们探究的目的不是A进程写数据后,把数据刷新到磁盘上,我们是为了A和B俩个进程进行互相通信。但是,我们打开的普通文件只要是在磁盘上,如果对文件进行写操作,必定要把文件刷新到磁盘上,但是刷盘的效率很低,因此操作系统设计了一种新的设计方案:在磁盘上创建一种文件,这种文件叫管道文件,该文件可以被打开,但是不会将内存数据刷新到磁盘,既然是在磁盘上的文件,就一定有所在路径和自己的名字,即该文件一定在系统路径中(路径具有唯一性)。

 由于路径具有唯一性,双方进程就可以通过管道文件的路径,看到同一份资源。

 管道应用的一个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信。
如果我们想在不相关的进程之间交换数据,可以使用FIFO文件来做这项工作,它经常被称为命名管道。

命名管道用于毫不相关的俩个进程。
命名管道是一种特殊类型的文件

命名管道可以从命令行上创建,命令行方法是使用下面这个命令:

$ mkfifo filename
命名管道也可以从程序里创建,相关函数有:
int mkfifo(const char *filename,mode_t mode);

Linux——进程间通信2_第2张图片

 mkfifo在指定路径下创建一个named pipes(命名的管道)

输入man 3 fifo,我们发现mkfifo还可以添加权限,创建成功返回0,失败了返回错误

Linux——进程间通信2_第3张图片

管道测试小实验 

由于俩个任意的指令一旦运行起来,就是俩个进程。 

先创建一个管道文件,管道名为name_pipe,权限里的第一个字母为p代表管道文件

Linux——进程间通信2_第4张图片

 我们给管道里写入hello world,由于我们写了,但对方没有打开,此时处于阻塞状态

Linux——进程间通信2_第5张图片

 此时对方从管道文件中读数据,状态不再阻塞

Linux——进程间通信2_第6张图片

 我们输入下面这条命令,循环式的往管道里写

while :; do echo "hello world"; sleep 1; done > name_pipe

 此时右边进行读取,输入cat > name_pipe右边会一直读取

Linux——进程间通信2_第7张图片

 删除管道文件 

unlink name_pipe

 模拟命名管道通信

 这里让client.cxx和server.cxx访问同一个文件,让server.cxx创建管道文件,让client发送内容,server读取内容

 Linux——进程间通信2_第8张图片

 运行server之后,默认会出现一个管道文件

Linux——进程间通信2_第9张图片

 server.cxx

#include"comm.hpp"
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"<0)//读取成功
        {
            std::cout<<"client say> "<

client.cxx 

#include"comm.hpp"
int main()
{
    //1.获取管道文件
    int fd=open(ipcPath.c_str(),O_WRONLY);
    if(fd<0)
    {
        perror("open");
        exit(1);
    }
    //2.进行通信
   string buffer;//将数据读取到这里
    while(true)
    {
        cout<<"please Ente Meassage Line:>";
        std::getline(cin,buffer);//将用户输入数据保存到BUFFER中
        write(fd,buffer.c_str(),buffer.size());//把buffer的内容转为C语言字符串格式,然后写到fd中,大小为buffer
    }
    //3.关闭
    close(fd);
    //4.通信完毕,删除通信文件
    unlink(ipcPath.c_str());
    return 0;
}

 comm.hpp

#ifndef _COMM_H_
#define _COMM_H_
#include"Log.hpp"
#include
#include//perror头文件
#include
#include
#include
#include//mkfifo的头文件 open的头文件
#include//mkfifo头文件 open的头 文件
#include//open的头文件
using namespace std;
#define SIZE 128
#define MODE 0666//管道文件的权限
string ipcPath="./fifo.ipc";//管道文件路径
#endif

Log.hpp 

#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

makefile 

.PHONY:all
all:client server
client:client.cxx
	g++ -o $@ $^ -std=c++11
server:server.cxx
	g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
	rm -rf client server

 

 运行程序

当左边输入./client时 ,右边才会显示打开文件成功

 此时在左边输入消息,右边就能收到

Linux——进程间通信2_第10张图片

 当左边退出时,右边就会读到0,右边就会退出

Linux——进程间通信2_第11张图片

创建子进程

 server.cxx,我们将生成的文件命名为mutiServer

#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"<

 只进行./mutiServer操作,当前进程也只有一个

Linux——进程间通信2_第12张图片

 执行./client

此时有3个进程

Linux——进程间通信2_第13张图片

我们发送多组数据,发现这里的pid在变化,这是因为我们派发了任务之后,这些任务由这几个进程去抢,谁抢到了就是谁的

Linux——进程间通信2_第14张图片退出之后,打印的消息,它们各自打各自的消息

Linux——进程间通信2_第15张图片 这里说明管道的读端可以有多个

 system V共享内存

 原理:俩个不同的进程申请同一块空间,之后在各自的页表建立映射关系。

Linux——进程间通信2_第16张图片

Linux——进程间通信2_第17张图片

有图可知,共享内存会被加载到堆栈之间的共享区域 ,堆栈之间的共享区属于在用户空间(这个空间可以不经过系统调用用户便可直接访问)双方进程如果要通信,直接进行内存级别的读和写即可。

我们之前的pipe,fifo都要通过read,write进行通信,这是为什么?

因为之前命名管道是文件,文件是内核中的特定数据结构,需要操作系统自己去维护即在3-4G范围内,而共享内存不需要调用这些接口,用户可直接进行调用。

每一个进程0-3G属于用户,3-4G属于内核,页表分为用户级和内核级页表,操作系统在进行调度或使用系统调用接口或库函数,都要将在自己的代码映射到自己的地址空间当中,因为动态库会加载到共享区,无论我们是执行库中的代码或操作系统中的代码,都是在自己的地址空间中完成,无论进程如何切换,每个进程3-4G的空间全部映射的是操作系统的代码和数据

Linux——进程间通信2_第18张图片

如何建立共享内存?

 共享内存的提供者是操作系统。操作系统负责管理共享内存->先描述再组织->重新理解共享内存:共享内存=共享内存快(申请的空间)+对应的共享内存的内核数据结构。

 ​shmget申请一个system V的共享内存段,表示创建并获取共享内存,size表示创建共享内存的大小Linux——进程间通信2_第19张图片

第三个参数有这俩种状态:IPC_CREAT, IPC_EXCL

IPC_CREAT:当单独使用时,如果要创建的共享内存已经存在,直接获取并返回,如果不存在就创建并返回。

IPC_EXCL:单独使用时,是没有意义的

IPC_CREAT, IPC_EXCL:合起来使用时,如果底层不存在我们要创建的共享内存,使用这俩个接口会创建共享内存并返回,如果底层存在,则出错返回

 返回值

如果成功返回合法标识符,失败返回-1,这个返回值时共享内存的用户层标识符,类似于曾经的fd,但俩者仍有差别。 

 第一个参数key

 通过key可以让对方看到共享内存,key的数据数据是几不重要,只要能够再系统唯一即可。

例如上面的server和client,只要保证使用的是同一个key即可,如server使用key去创建共享内存,client通过key获得共享内存并且和自己关联,这俩个进程就能看到同一个共享内存了。共享内存有自己的key(这个key在共享内存的数据结构当中)

 只要key值相同,就是看到了同一个共享内存

 如何产生key值?

Linux——进程间通信2_第20张图片

ftok需要传一个路径和项目ID,第一个参数是路径,跟文件路径不一样 ,ID由我们自己指定,ftok不进行任何的系统调用,它内部是一套算法,就是把这俩个参数合起来形成一个唯一值。第一个参数我们一般指明一个文件之后,ftok会读取该文件的inode编号,利用inode值和id值进行数学运算,得到一个随机值。ftok形成key之后,这个key可能会和操作系统里面有的key重复,因此ftok不一定会成功。对于俩个进程使用同样的pathname和id,使用同样的ftok,server和client就能形成唯一的key,server再根据shmget用这个key创建共享内存,并把这个key写入到操作系统的共享内存里面,client将来在获取的时候,只要找到这个key,就能获得这块共享内存。

如果key值创建成功,则返回key值,反之返回-1

模拟实现共享内存

 让shmServer.cc和shmClient.cc采用相同的算法和数据源,产生相同的key

Linux——进程间通信2_第21张图片

 Linux——进程间通信2_第22张图片

获取共享内存

Linux——进程间通信2_第23张图片

 此时获取共享内存失败,退出码为1,报错信息为没有这个文件或目录,这是因为我把shmid第三个参数穿错了,这里只能传IPC_CREAT, IPC_EXCL

Linux——进程间通信2_第24张图片

我们更换参数

Linux——进程间通信2_第25张图片

此时无报错

Linux——进程间通信2_第26张图片再次运行,报错该文件已存在,虽然第一次执行完以后共享内存已经跑完了,但是共享内存还存在

Linux——进程间通信2_第27张图片

ipcs -m//查看共享内存
ipcs查看ipc资源,-m查看共享内存

 查看共享内存之后,我们发现共享内存此时存在,我们需删掉该共享内存

Linux——进程间通信2_第28张图片

删除的时候输入ipcrm -m shmid值 

Linux——进程间通信2_第29张图片 当进程推出后共享内存还存在:这是因为system V IPC资源,生命周期随内核

 我们对共享内存进行手动删除或代码删除,上面的删除方式为手动删除。

删除共享内存 

 shmctl

Linux——进程间通信2_第30张图片

 第二个参数cmd

Linux——进程间通信2_第31张图片

第三个参数是 共享内存的属性和数据,可设为空

 如果删除失败就返回-1,成功返回0

 Linux——进程间通信2_第32张图片

 我们在最后一步删除之后,就不会出现上面的问题

Linux——进程间通信2_第33张图片 我们一直查ipc

while :; do ipcs -m; sleep 1; done

 我们写一个让key值变为16进制的函数,之后运行程序

 我们可以看到key是0x66011952,shimd是6,当前拥有者是LWX,所占字节大小是4096,这里共享内存的大小我们可以随意修改,但共享内存的大小,最好是页的整数倍,Linux下页(PAGE)大小最小是4096

这里我们看到的perm是权限,0代表无权力读写共享内存

Linux——进程间通信2_第34张图片

 我们可在创建共享内存的时候加一个权限选项,这里是0666,之后我们可以看到perms是0666

Linux——进程间通信2_第35张图片

Linux——进程间通信2_第36张图片 attach是把共享内存映射到进程对应的页表当中前面的n代表关联的个数,去掉这种映射detach

 将共享内存挂接到自己的地址空间

shmat,第一个参数是shmid,第二个参数是我们指定的虚拟地址,一般情况下我们对虚拟地址的使用情况不太清楚,这个参数一般设为nullptr,即让共享内存自由去挂接,第三个参数是挂接方式如只读或其它方式,也可设置为0(默认以读写方式挂接)。

Linux——进程间通信2_第37张图片 返回值:挂接成功的时候返回共享内存的地址(虚拟地址),失败返回-1,并且设置错误码

 这个接口很像malloc申请成功返回地址空间,失败返回null

当返回地址后,我们设置了共享内存的大小,这个大小也可看做是偏移量,用返回的地址+这个偏移量就是共享内存所在的整个地址。 

我们可以看到关联数量由0变为1

 Linux——进程间通信2_第38张图片Linux——进程间通信2_第39张图片

去除挂接 

 shmdt,只需传入shmat的返回值即可,去关联成功返回0,失败返回-1.

Linux——进程间通信2_第40张图片

Linux——进程间通信2_第41张图片

取消挂接之后,关联数量由1变为0

Linux——进程间通信2_第42张图片 Linux——进程间通信2_第43张图片

 当shmServer和shmClient同时跑起来时,挂接数变为2

Linux——进程间通信2_第44张图片

 上面的所有工作是为了让不同的进程看到同一份资源(内存)

通过共享内存进行通信

 我们这里将共享内存当成一个大字符串。

shm.Server.cc负责读取内容

Linux——进程间通信2_第45张图片

shmClient.cc每一次for循环都向shmaddr(共享内存的起始地址)写入

Linux——进程间通信2_第46张图片

只运行shmServer,我们发现在打东西但什么都没打出来,这是因为共享内存在被创建后默认是全0,所以这里打出来的字符串是空串

Linux——进程间通信2_第47张图片

我们可以看出写端没有启动,但读端可以一直读 

我们此时让写端运行起来,当读端读到z的时候不会退出,会一直读,一直打印z,也就是无法执行后面的删除程序,我们需用ipcrm -m shimd手动去删除

Linux——进程间通信2_第48张图片

若不手动删除,我们加一个标识符,如读到quit就退出读取

client.cc

Linux——进程间通信2_第49张图片 server.cc

Linux——进程间通信2_第50张图片

 我们让Client每五秒写一次

Linux——进程间通信2_第51张图片

此时识别到quit,直接退出读过程,执行后面的代码

Linux——进程间通信2_第52张图片

我们也可以像数组一样在使用共享内存

 

 Linux——进程间通信2_第53张图片

结论: 

 1.只要是通信双方使用共享内存,一方向共享内存中写入数据,另一方就可以立刻看到该数据,共享内存是所有进程间(IPC)通信速度最快的,不需要过多的拷贝(不需要将数据给操作系统)。

 管道拷贝次数Linux——进程间通信2_第54张图片

共享内存,共享内存可由键盘直接输入给内存,不需要缓冲区,减少了拷贝次数,只在键盘输入和往显示器上打印的时候进行了拷贝

Linux——进程间通信2_第55张图片 直接从键盘里读取数据到共享内存

Linux——进程间通信2_第56张图片

此时我们输入什么,它就读取什么

Linux——进程间通信2_第57张图片

稍作修改,输入quit 即可退出

Linux——进程间通信2_第58张图片

Linux——进程间通信2_第59张图片

2. 共享内存没有访问控制。共享内存无论有无数据,会一直读,读取端不会受写入端的影响。而管道当读取完毕后,若写端未退出会阻塞,若写端写满了,写端也会阻塞。共享内存由于缺乏访问控制,会引来并发问题。

若想进行访问控制,我们可以用管道实现共享内存进行同步。

Linux——进程间通信2_第60张图片 Linux——进程间通信2_第61张图片

 comm.hpp

#pragma once

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include "Log.hpp"
#include 
#include 
using namespace std; //不推荐

#define PATH_NAME "/home/LWX"//要保证pathname的路径有访问权限
#define PROJ_ID 0x66
#define SHM_SIZE 4096//shmget的第二个参数,共享内存的大小
#define FIFO_NAME "./fifo"//管道文件
class Init
{
public:
 Init()
    {
        umask(0);
        int n = mkfifo(FIFO_NAME, 0666);
        assert(n == 0);
        (void)n;
        Log("create fifo success",Notice) << "\n";
    }
~Init()
{
    unlink(FIFO_NAME);//删除管道
    Log("remove fifo success",Notice)<<"\n";
}
};

#define READ O_RDONLY
#define WRITE O_WRONLY
int OpenFIFO(std::string pathname,int flags)//打开文件
{
    int fd=open(pathname.c_str(),flags);
    assert(fd>0);
    return fd;
}
void Wait(int fd)//让一个进程等待
{
    Log("等待中...",Notice)<<"\n";
    uint32_t temp=0;
    ssize_t s=read(fd,&temp,sizeof(uint32_t));//把fd内容读取到temp中,大小为4字节
    assert(s==sizeof(uint32_t));
}
void Signal(int fd)//唤醒另一个进程,让另一个进程进行读或数据操作
{
    uint32_t temp=1;
    ssize_t s=write(fd,&temp,sizeof(uint32_t));//向fd中写temp值,大小为4字节
    assert(s==sizeof(uint32_t));
    (void)s;
     Log("唤醒中...",Notice)<<"\n";
}
void CloseFifo(int fd)//关闭管道文件
{
    close(fd);
}

Log.hpp 

#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

shmClient.cc(发送端) 

#include "comm.hpp"

int main()
{
    key_t k = ftok(PATH_NAME, PROJ_ID);//产生key值
    if(k<0)
    {
        Log("create key failed", Error) << " client key : " << k << endl;
        exit(1);
    }
     Log("create key done", Debug) << " client key : " << k << endl;
     int shmid=shmget(k,SHM_SIZE,IPC_CREAT);//获取共享内存
     if(shmid<0)
     {
         Log("create key done", Error) << " client key : " << k << endl;
         exit(2);
     }
        Log("create shm success", Error) << " client key : " << k << endl;
   //  sleep(10);
char *shmaddr=(char*)shmat(shmid,nullptr,0);//把自己当前进程和共享内存挂接
if(shmaddr==nullptr)
{
    Log("attach shm failed", Debug) << " client key : " << k << endl;
    exit(3);
}
 Log("attach shm sucess", Debug) << " client key : " << k << endl;
 //sleep(10);
 //使用,shmClient.cc往共享内存写东西
 //client将共享内存看作一个char类型的buffer
 //每次都向shmaddr(共享内存的起始地址)写入
 int fd=OpenFIFO(FIFO_NAME,WRITE);
 while(true)
 {
   ssize_t s=read(0,shmaddr,SHM_SIZE-1);
   if(s>0)//读取成功
   {
    shmaddr[s-1]=0;//我们在输入完数据后会按回车,此时给总大小-1就不接受回车了
    Signal(fd);//发送端写入成功,此时唤醒另一个进程
    if(strcmp(shmaddr,"quit")==0) break;
   }
 }
 CloseFifo(fd);
 //char a='a';
 //for(;a<='c';a++)
 /*{
    shmaddr[a-'a']=a;
    //snprintf(shmaddr,SHM_SIZE,"hello serve,我是其它进程,我的pid:%d,inc:%c\n",getpid(),a);//写到共享内存里
    sleep(5);
 }*/
 strcpy(shmaddr,"quit");//我们给写入quit
 //去关联
 int n=shmdt(shmaddr);
 assert(n!=-1);
  Log("detach shm sucess", Debug) << " client key : " << k << endl;
 // sleep(10);
  //cilent不需要删除共享内存,client只管使用
     return 0;
}

shmServer.cc (读取端)

#include "comm.hpp"
Init init;
string TransToHex(key_t k)//转为16进制
{
    char buffer[32];
    snprintf(buffer,sizeof(buffer),"0x%x",k);//把key转为16进制写到buffer里
    return buffer;
}

int main()
{
    Log("child pid is:",Debug)<

习题 

以下描述正确的有 C

A.使用ipcrm -m命令删除指定共享内存后,则会直接释放共享内存

B.使用ipcs -m命令删除指定共享内存后,则会直接释放共享内存

C.使用ipcrm -a选项可以删除所有进程间通信资源

D.使用ipcrm命令不指定选项可以删除所有进程间通信资源

A/B 共享内存只有在当前映射连接数为0时才会被删除释放

以下关于ipcrm命令描述正确的有  B

A.ipcrm命令不指定选项可以删除所有进程间通信

B.ipcrm -m命令可以删除共享内存

C.ipcrm -s命令可以删除共享内存

D.ipcrm -q命令可以删除管道

 以下关于ipc命令描述正确的有: B

A.ipcs -m用于查看消息队列的信息

B.ipcs -q可以查看消息队列的信息

C.ipcrm -s可以查看共享内存的信息

D.ipcrm -q可以查看共享内存的信息

  ipcs 查看进程间通信资源/ipcrm 删除进程间通信资源

  -m 针对共享内存的操作

  -q 针对消息队列的操作

  -s 针对信号量的操作

  -a 针对所有资源的操作

你可能感兴趣的:(Linux,linux,服务器,网络)