Linux--进程间通信

1.进程间通信

进程间通信的背景:

进程之间是相互独立的,进程由内核数据结构和它所对应的代码和数据,通信成本比较高。

进程间通信目的:

数据传输:一个进程需要将它的数据发送给另一个进程
资源共享:多个进程之间共享同样的资源。
通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。

进程间通信本质:

进程间通信的前提就是让不同的进程看到相同的一份“内存”,这块“内存”不属于任何一个进程,属于操作系统。

2.进程间通信的方式

1.管道 (匿名管道 命名管道)

2.System V通信 (多进程 单机通信)

3.POSIX 通信 (多线程 网络结构)(在这里不讲)

3.管道讲解

1.管道分类:匿名管道,命名管道。

2.什么是管道

        管道是一种古老的传输资源的方式,是UNIX中过来的传输方式,是从一个进程传递到另一个进程的方法。管道是单向通信的,传输的都是资源,不能同时完成双向通信。

3.实现原理:

匿名管道:

Linux--进程间通信_第1张图片  如何做到不同进程看到相同的内存呢?

fork()函数让具有血缘关系的进程进行进程间通信,常用于父子进程。

创建管道文件,int pipe(int pipefd[2]);

具体实现看代码:

#include 
#include 
#include 
#include 
#include 
#include 
#include 


using namespace std;

int main()
{
    int pipefd[2]={0}; pipefd[0(嘴巴,读书)]: 读端 , pipefd[1(钢笔,写)]: 写端
    int n = pipe(pipefd);
    assert(n != -1); // debug assert, release assert
    (void)n;
    #ifdef DEBUG
        cout << "pipefd[0]: " << pipefd[0] << endl; // 3
        cout << "pipefd[1]: " << pipefd[1] << endl; // 4
    #endif

    pid_t id=fork();
    if(id==0)
    {
        close(pipefd[1]);
        char buff[1024*8];
        while(true)
        {
            ssize_t s = read(pipefd[0], buff, sizeof(buff) - 1);
            if(s>0)
            {
                buff[s] = 0;
                cout << "child get a message[" << getpid() << "] Father# " << buff << endl;
            }
            else
            {
                cout << "writer quit(father), me quit!!!" << endl;
                break;
            }
        }
        close(pipefd[0]);//关闭文件,可以不用
        exit(0);
        //return 0;
    }

    else if(id>0)
    {
        close(pipefd[0]);
        string message = "我是父进程,我正在给你发消息\n";
        int count = 0;
        char send_buffer[1024 * 8];

        while(true)
        {
            //构建一个变化的字符串
            snprintf(send_buffer, sizeof(send_buffer), "%s[%d] : %d",
                 message.c_str(), getpid(), count++);//写到一个字符串
           write(pipefd[1], send_buffer, strlen(send_buffer));

           if(count==5)
           {
                cout< 0); 
        (void)ret;

    }
    return 0;
}

结论:

  • 管道是用来进行具有血缘关系的·进程实现进程间通信--常用于父子进程。
  • 具有让进程间协同通信,提供了访问控制。
  • 提供面向流的听信服务,面向字节流的服务。
  • 管道是基于文件的,文件的生命周期是基于进程的,所以管道的生命周期是基于进程的。
  • 管道是单向通信的,就是半双工通信的一种特殊形式。

如果写得快,读得慢,则写满管道之后不再写入。

如果写的慢,读得快,则管道没有数据是,则停下来等待。

如果写入端关闭,则读到文件末尾结束

关闭读,则OS会关闭写进程。

命名管道:

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

命名管道的创建:

直接在命令行上创建:mkfifo filename

也可以在程序中创建:int mkfifo(const char *filename,mode_t mode);

第一个参数为文件名,第二个为权值

创建命名管道:

int main(int argc, char *argv[])
{
    mkfifo("p2", 0644);
    return 0;
}

匿名管道与命名管道之间的区别:

  • 匿名管道由pipe函数创建并打开。
  • 命名管道由mkfifo函数创建,打开用open。
  • FIFO(命名管道)与pipe(匿名管道)之间唯一的区别在它们创建与打开的方式不同,一但这些工作完。
  • 成之后,它们具有相同的语义。

读写规则:

如果当前打开操作是为读而打开FIFO时:
        O_NONBLOCK disable:阻塞直到有相应进程为写而打开该FIFO
        O_NONBLOCK enable:立刻返回成功
如果当前打开操作是为写而打开FIFO时:
        O_NONBLOCK disable:阻塞直到有相应进程为读而打开该FIFO
        O_NONBLOCK enable:立刻返回失败,错误码为ENXIO

代码实现:

#ifndef _COMM_H_
#define _COMM_H_
//公共文件
#include 
#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
//日志文件

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

//读文件
#include "comm.hpp"
#include "log.hpp"

static void getMessage(int fd)
{
    char buffer[SIZE];
    while(true)
    {
        memset(buffer,'\0',sizeof buffer);
        int s=read(fd,buffer,sizeof(buffer)-1);
        if(s>0)
        {
            cout <<"["  << getpid() << "] "<< "client say> " << buffer << endl;//还有文件
        }

        else if(s=0)
        {
            cerr <<"["  << getpid() << "] " << "read end of file, clien quit, server quit too!" << endl;//读到文件末尾
            break;
        }
        else if(s<0)
        {
            perror("read s");
            break;
        }
    }
}
int main()
{
    int id=mkfifo("textfifo.txt",0666);//创建命名管道文件
    if(id<0)//创建失败
    {
        perror("mkfifo id");
        return 0;
    }
    Log("创建管道文件成功", Debug) << " step 1" << endl;//打印日志

    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 ";
        std::getline(std::cin, buffer);//写文件
        write(id,buffer.c_str(),buffer.size());
    }
    close(id);//关闭
    //unlink(id);
    return 0;
}


结论:双方进程,可以看到同一份文件,,该文件一定在系统路径中,路径具有唯一性,管道文件可以被打开,但是不会将内存中的数据刷新到磁盘中。且有名字。

3.System V 通信

共享内存:共享内存区是最快的IPC形式。共享内存是在物理内存上申请一块空间,再让两个进程各自在页表建立虚拟地址和这块空间的映射关系。这样两个进程看到的就是同一份资源,这一份资源就叫做共享内存。

原理:

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

共享内存的提供者是操作系统,操作系统通过先描述再组织的方式管理共享内存。

共享内存=共享内存块+对应的共享内存的内核数据结构。

两个进程创建共享内存需要以下步骤:

  1. 创建共享内存
  2. 将两个进程关联到共享内存
  3. 取消两个进程和共享内存的关联
  4. 删除共享内存

注意: 前两个步骤是为了让两个进程实现通信,后面两个步骤是释放共享内存空间,要不然就会内存泄漏了。(与我们之前用的malloc是类似的)。

创建共享内存所需要的函数:

1.ftok——获取一个共享内存的唯一标识符

函数:key_t ftok(const char *pathname, int proj_id);

功能:获取一个共享内存的唯一标识符 key

参数: pathname 文件名 ;proj_id 只有是一个非0的数都可以 .

返回值:成功返回key;失败返回 -1

2..shmget——创建共享内存

函数:int shmget(key_t key, size_t size, int shmflg);

key:传入ftok函数获取的共享内存唯一标识符
size:共享内存的大小(页(4kb)的整数倍)
shmflg:权限,由9个权限标准构成

这里介绍两个选项
IPC_CREAT: 如果底层存在这个标识符的共享内存空间,就打开返回,不存在就创建
IPC_EXCL: 如果底层存在这个标识符的共享内存空间,就出错返回
两个选项合起来用就可以穿甲一个权限的共享内存空间
返回值:
成功返回共享内存标识码值(给用户看的),失败返回-1.

3.shmat——将共享内存空间关联到进程地址空间

函数:void *shmat(int shmid, const void *shmaddr, int shmflg);

功能: 将共享内存空间关联到进程地址空间
参数:
shmid:共享内存标识符
shmaddr:指定连接地址。
shmfig:两个可能取值是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,表示连接操作用来只读共享内存。

4.shmdt——取消关联

函数:int shmdt(const void *shmaddr);

功能: 取消共享内存空间和进程地址空间的关联
参数:
shmaddr:共享内存的起始地址(shmat获取的指针)
返回值: 成功返回0,失败返回-1

5.shmctl——控制共享内存

函数:int shmctl(int shmid, int cmd, struct shmid_ds *buf);

功能: 控制共享内存
参数:
shmid:共享内存标识符
cmd:命令,有三个
IPC_STAT: 把shmid_ds结构中设置为共享内存当前关联值
IPC_SET: 把共享内存的当前关联值设置为shmid_ds数据结构中的值
IPC_RMID:删除共享内存段
buf:指向一个报错这共享内存的模式状态和访问权限的数据结构
返回值: 成功返回0,失败返回-1。

共享内存的特性:

对于共享内存来说,它与管道有不同的特性,导致共享内存不同的使用方法

1.管道需要使用系统接口来调用,但是共享内存可以不用经过系统调用,直接可以访,双方进程如果要进行通信,直接进行内存级的读和写即可。共享内存实在堆栈之间的区域的,堆栈相对而生,中间区域为共享内存,不用经过操作系统。

共享内存是最快的,为什么呢?

因为如果是管道,需要从键盘写入,然后再拷贝到自己定义的缓冲区中,然后再次拷贝到内存中,再从内存中拷贝到用户级缓冲区中,最后再拷贝到屏幕中,需要经历最少4词的拷贝过程。

共享内存直接面向用户,所以从键盘中输入的内容直接进入到内存中,然后经过内存到达显示器中,最少只有2次拷贝,所以他的速度是最快的。

对于共享内存的理解:

为了进行进程间通信,需要让不同的进程看到相同的一份资源,所以之前的管道,本质都是优先解决一个问题,让不同的进程看到同一份资源!!!

让不同的进程看到相同的内存,带来了有些时序问题,造成数据不一致问题。

结果:

我们把多个进程(执行流)看到的同一份资源称为临界资源。

我们把自己的进程,访问临界资源的代码,称为临界区。

为了更好地进行临界区的保护,可以让多执行流在任何时刻都只有一个进程进入临界区。即互斥!!!

原子性:要么不做,要么做完,没有中间状态,即为原子性!!

所以,多个执行流,互相运行的时候互相干扰,主要是我们不加保护的访问了相同的资源(临界资源),在非临界区多个执行流是互不干扰的。

代码演示:

#pragma once

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include "Log.hpp"

using namespace std; //不推荐

#define PATH_NAME "/home/SSS"//环境变量
#define PROJ_ID 0x66
#define SHM_SIZE 4096 //共享内存的大小,最好是页(PAGE: 4096)的整数倍 

#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));
    assert(s == sizeof(uint32_t));
    (void)s;
}

void Signal(int fd)
{
    uint32_t temp = 1;
    ssize_t s = write(fd, &temp, sizeof(uint32_t));
    assert(s == sizeof(uint32_t));
    (void)s;
    Log("唤醒中....", Notice) << "\n";
}

void CloseFifo(int fd)
{
    close(fd);
}

#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

#include "comm.hpp"
#include "log.hpp"


string TransToHex(key_t k)
{
    char buffer[32];
    snprintf(buffer, sizeof buffer, "0x%x", k);
    return buffer;
}

int main()
{
    key_t key=ftok(PATH_NAME,PROJ_ID);
    assert(key!=-1);
    Log("create key done", Debug) << " server key : " << TransToHex(k) << endl;
    //创建共享内存
    int shmid=shmget(key,SHM_SIZE, IPC_CREAT | IPC_EXCL | 0666);
    assert(shmid!=-1);
    Log("create shm done", Debug) << " shmid : " << shmid << endl;

    sleep(10);
    // 3. 将指定的共享内存,挂接到自己的地址空间
    char *shmaddr = (char *)shmat(shmid, nullptr, 0);
    Log("attach shm done", Debug) << " shmid : " << shmid << endl;

    sleep(10);

   // 这里就是通信的逻辑了
    // 将共享内存当成一个大字符串
    // char buffer[SHM_SIZE];
    // 结论1: 只要是通信双方使用shm,一方直接向共享内存中写入数据,另一方,就可以立马看到对方写入的数据。
    //         共享内存是所有进程间通信(IPC),速度最快的!不需要过多的拷贝!!(不需要将数据给操作系统)
    // 结论2: 共享内存缺乏访问控制!会带来并发问题 【如果我想一定程度的访问控制呢? 能】
    
    int fd = OpenFIFO(FIFO_NAME, READ);
    for(;;)
    {
        Wait(fd);

        // 临界区
        printf("%s\n", shmaddr);
        if(strcmp(shmaddr, "quit") == 0) break;
        // sleep(1);
    }
    

    // 4. 将指定的共享内存,从自己的地址空间中取消关联
    int n = shmdt(shmaddr);
    assert(n != -1);
    (void)n;
    Log("detach shm done", Debug) << " shmid : " << shmid << endl;
    sleep(10);

    // 5. 删除共享内存,IPC_RMID即便是有进程和当下的shm挂接,依旧删除共享内存
    n = shmctl(shmid, IPC_RMID, nullptr);
    assert(n != -1);
    (void)n;
    Log("delete shm done", Debug) << " shmid : " << shmid << endl;
    return 0;
}


#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, 0);
    if(shmid < 0)
    {
        Log("create shm failed", 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", Error) << " client key : " << k << endl;
        exit(3);
    }
    Log("attach shm success", Error) << " client key : " << k << endl;
    sleep(10);

    int fd = OpenFIFO(FIFO_NAME, WRITE);
    //使用
    // client将共享内存看做一个char 类型的buffer
    while(true)
    {
        ssize_t s = read(0, shmaddr, SHM_SIZE-1);
        if(s > 0)
        {
            shmaddr[s-1] = 0;
            Signal(fd);
            if(strcmp(shmaddr,"quit") == 0) break;
        }
    }

    CloseFifo(fd)
    // 去关联
    int n = shmdt(shmaddr);//取消关联
    assert(n != -1);
    Log("detach shm success", Error) << " client key : " << k << endl;
    sleep(10);

    // client 要不要chmctl删除呢?不需要!!

    return 0;
}

你可能感兴趣的:(linux,服务器,运维)