Linux进程间通讯

文章目录

  • Linux进程间通讯
    • 1、进程间通信介绍
      • 1.1、进程间通信目的
      • 1.2、进程间通信发展
      • 1.3、进程间通信分类
    • 2、管道
      • 2.1、什么是管道
      • 2.2、匿名管道
        • 2.2.1、标准输入stdin和标准输出stdout通信
        • 2.2.2、父子进程通信
        • 2.2.3、父子进程通信现象
        • 2.2.4、父子进程通信特性
        • 2.2.5、进程池
      • 2.3、命名管道
        • 2.3.1、命名管道的创建
        • 2.3.2、命名管道和匿名管道的区别
        • 2.3.3、用命名管道实现server和client间通信
    • 3、System V 共享内存
      • 3.1、共享内存的介绍
      • 3.2、共享内存的使用
    • 4、System V 消息队列(了解)
    • 5、System V 信号量(了解)

img

Linux进程间通讯

1、进程间通信介绍

让不同进程看到同一份资源的行为通常称为进程间通信(Inter-Process Communication,IPC)。IPC 是在操作系统中用于实现不同进程之间数据传输和共享资源的机制。

1.1、进程间通信目的

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

1.2、进程间通信发展

  • 管道
  • System V 进程间通信
  • POSIX进程间通信

1.3、进程间通信分类

  • 管道

    • 匿名管道
    • 命名管道
  • System V IPC

    • System V 消息队列
    • System V 共享内存
    • System V 信号量
  • POSIX IPC

    • 消息队列
    • 共享内存
    • 信号量
    • 互斥量
    • 条件变量
    • 读写锁

2、管道

2.1、什么是管道

  • 管道是Unix中最古老的进程间通信的形式
  • 我们把从一个进程连接到另一个进程的一个数据流称为一个“管道“


2.2、匿名管道

使用系统调用接口pipe:我们可以看到,pipe接口需要传入一个大小为2的整型数组。并且在调用pipe后,这数组中的两个位置的值都会被设置,第一个值是作为读端,第二个值是作为写端。

匿名管道通常是父子进程进行通信的管道。


2.2.1、标准输入stdin和标准输出stdout通信
// 从键盘读取数据,写入管道,读取管道,写到屏幕

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>


int main()
{
    int fds[2];
    char buf[100];
    int len;
    int n = pipe(fds);
    if (n == -1)
        perror("make pipe"), exit(1);

    // read from stdin
    while (fgets(buf, 100, stdin))
    {
        len = strlen(buf);

        // write into pipe
        n = write(fds[1], buf, len);
        if (n != len)
        {
            perror("write to pipe");
            break;
        }
        memset(buf, 0x00, sizeof(buf)); // 重置buff

        // read from pipe
        n = read(fds[0], buf, 100);
        if (n == -1)
        {
            perror("read from pipe");
            break;
        }
        // write to stdout

        printf("%s\n", buf);

        // n = write(1, buf, len);
        // if (n != len)
        // {
        //     perror("write to stdout");
        //     break;
        // }
    }
}


2.2.2、父子进程通信

使用前面我们学到的fork来创建子进程,那么子进程会继承父进程的读写端,该父子进程的读写端是指向同一份文件的(匿名管道)。这时候只需要父子进程各关闭一个读端和写端就能实现父子通信了。比如父进程关闭写端,子进程关闭读端,那么子进程就可以往管道中写数据了!

站在文件描述符的角度理解匿名管道

这里是父进程写,子进程读。

站在内核角度理解匿名管道

可以看到父子进程指向的文件是同一个inode,也就是同一个文件。

所以,我们可以看到,管道其实和文件没有什么两样,后面我们将命名管道就更能理解Linux中一切皆文件的思想。

下面是父子进程通信的代码

#include <stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<stdlib.h>
#include<string.h>


void writer(int wfd){
    const char* msg = "hello father";
    char buff[128];
    pid_t pid  = getpid();
    int cnt = 0;
    while(1){
        snprintf(buff,sizeof(buff),"sent message: %s :%d,pid: %d\n",msg,cnt,pid);
        write(wfd,buff,strlen(buff)+1);
        cnt++;
        sleep(1);
    }
}

void reader(int rfd){
    char buff[1024];
    while(1){
        ssize_t n = read(rfd,buff,sizeof(buff)-1);
        printf("get message: %s",buff);
    }

}


int main(){
    int pipefd[2];
    int ret = pipe(pipefd);
    if(ret == -1)
        return -1;
    
    printf("pipefd[0]:%d,pipefd[1]:%d\n",pipefd[0],pipefd[1]);

    pid_t id = fork();
    if(id == 0){
        close(pipefd[0]);
        // w
        writer(pipefd[1]);
        exit(10);
    }
    // 父进程
    close(pipefd[1]);
    reader(pipefd[0]);

    wait(NULL);
    return 0;
}


2.2.3、父子进程通信现象

这里以读端是父进程,写端是子进程为例

现象一:管道内部没有数据并且子进程不关闭自己的写端文件描述符pipefd[1],读端(父进程)就要阻塞等待,直到pipe有数据。

代码:

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <string.h>

void writer(int wfd)
{
   const char *msg = "hello father";
   char buff[128];
   pid_t pid = getpid();
   int cnt = 0;
   while (1)
   {
       snprintf(buff, sizeof(buff), "sent message: %s:,pid: %d,cnt: %d\n", msg, pid, cnt);
       write(wfd, buff, strlen(buff));
       char c = 'A';
       cnt++;

       // write(wfd, &c, 1);
       // printf("%d\n",cnt);
       sleep(10);
       // if (cnt == 10)
       //     break;
   }
   close(wfd);
}

void reader(int rfd)
{
   char buff[1024];
   int cnt = 10;
   while (1)
   {
       sleep(1);
       ssize_t n = read(rfd, buff, sizeof(buff) - 1);
       if (n > 0)
       {
           buff[n] = '\0'; // Add null terminator after reading
           printf("get message: %s ,n: %ld\n", buff, n);
       }
       else if (n == 0)
       {
           printf("read file done!\n");
           break;
       }
       else
       {
           printf("read error!\n");
           break;
       }

       --cnt;
       if (cnt == 0)
           break;
   }
   close(rfd);
   printf("read endpoint done!\n");
}

int main()
{
   int pipefd[2];
   int ret = pipe(pipefd);
   if (ret == -1)
       return -1;

   printf("pipefd[0]:%d,pipefd[1]:%d\n", pipefd[0], pipefd[1]);

   pid_t id = fork();
   if (id == 0)
   {
       close(pipefd[0]);
       // w
       writer(pipefd[1]);
       exit(10);
   }
   // 父进程
   close(pipefd[1]);
   reader(pipefd[0]);

   // wait(NULL);
   int status = 0;
   pid_t rid = waitpid(id, &status, 0);
   if (rid == id)
   {
       printf("exit code:%d ,exit signal:%d\n", WEXITSTATUS(status), status & 0x7F);
   }
   return 0;
}

可以看到,子进程每10秒写一次数据,那么父进程在读完数据后(管道没有数据了)需要等10秒才能读到数据(等子进程写)。

现象二:管道内部被写满并且父进程不关闭自己的读端文件描述符pipefd[0],写端(子进程)写满之后就要阻塞等待,读端读了部分数据(这个每次读多少是看编译器)后写端才可以写。

代码:

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <string.h>

void writer(int wfd)
{
   const char *msg = "hello father";
   char buff[128];
   pid_t pid = getpid();
   int cnt = 0;
   while (1)
   {
       // snprintf(buff, sizeof(buff), "sent message: %s:,pid: %d,cnt: %d\n", msg, pid, cnt);
       // write(wfd, buff, strlen(buff));
       char c = 'A';
       cnt++;

       write(wfd, &c, 1);
       printf("%d\n",cnt);
       // sleep(10);
       // if (cnt == 10)
       //     break;
   }
   close(wfd);
}

void reader(int rfd)
{
   char buff[1024];
   int cnt = 10;
   while (1)
   {
       sleep(10);
       ssize_t n = read(rfd, buff, sizeof(buff) - 1);
       if (n > 0)
       {
           buff[n] = '\0'; // Add null terminator after reading
           printf("get message: %s ,n: %ld\n", buff, n);
       }
       else if (n == 0)
       {
           printf("read file done!\n");
           break;
       }
       else
       {
           printf("read error!\n");
           break;
       }

       --cnt;
       if (cnt == 0)
           break;
   }
   close(rfd);
   printf("read endpoint done!\n");
}

int main()
{
   int pipefd[2];
   int ret = pipe(pipefd);
   if (ret == -1)
       return -1;

   printf("pipefd[0]:%d,pipefd[1]:%d\n", pipefd[0], pipefd[1]);

   pid_t id = fork();
   if (id == 0)
   {
       close(pipefd[0]);
       // w
       writer(pipefd[1]);
       exit(10);
   }
   // 父进程
   close(pipefd[1]);
   reader(pipefd[0]);

   // wait(NULL);
   int status = 0;
   pid_t rid = waitpid(id, &status, 0);
   if (rid == id)
   {
       printf("exit code:%d ,exit signal:%d\n", WEXITSTATUS(status), status & 0x7F);
   }
   return 0;
}

可以看到,当前Linux内核的管道大小是4KB,当子进程写满后,子进程就阻塞了,在等父进程读一部分数据。

现象三:对于写端(子进程)而言,不写了并且关闭写文件描述符,读端将会把管道中的数据读完,最后就会读到返回值为0,表示读结束,类似读到了文件的结尾。

代码:

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <string.h>

void writer(int wfd)
{
   const char *msg = "hello father";
   char buff[128];
   pid_t pid = getpid();
   int cnt = 0;
   while (1)
   {
       // snprintf(buff, sizeof(buff), "sent message: %s:,pid: %d,cnt: %d\n", msg, pid, cnt);
       // write(wfd, buff, strlen(buff));
       char c = 'A';
       cnt++;

       write(wfd, &c, 1);
       printf("%d\n",cnt);
       sleep(1);
       if (cnt == 10)
           break;
   }
   close(wfd);
}

void reader(int rfd)
{
   char buff[1024];
   int cnt = 10;
   while (1)
   {
       // sleep(10);
       ssize_t n = read(rfd, buff, sizeof(buff) - 1);
       if (n > 0)
       {
           buff[n] = '\0'; // Add null terminator after reading
           printf("get message: %s ,n: %ld\n", buff, n);
       }
       else if (n == 0)
       {
           printf("read file done!\n");
           break;
       }
       else
       {
           printf("read error!\n");
           break;
       }

       --cnt;
       if (cnt == 0)
           break;
   }
   close(rfd);
   printf("read endpoint done!\n");
}

int main()
{
   int pipefd[2];
   int ret = pipe(pipefd);
   if (ret == -1)
       return -1;

   printf("pipefd[0]:%d,pipefd[1]:%d\n", pipefd[0], pipefd[1]);

   pid_t id = fork();
   if (id == 0)
   {
       close(pipefd[0]);
       // w
       writer(pipefd[1]);
       exit(10);
   }
   // 父进程
   close(pipefd[1]);
   reader(pipefd[0]);

   // wait(NULL);
   int status = 0;
   pid_t rid = waitpid(id, &status, 0);
   if (rid == id)
   {
       printf("exit code:%d ,exit signal:%d\n", WEXITSTATUS(status), status & 0x7F);
   }
   return 0;
}

可以看到,子进程写了10次数据后就退出了,父进程在读完10次数据也退出了。

现象四:对于读端(父进程)而言,不读了并且关闭读文件描述符,写端在写,操作系统会终止写端(子进程)的写入,通过信号13(SIGPIPE)信号杀掉子进程。

代码:

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <string.h>

void writer(int wfd)
{
   const char *msg = "hello father";
   char buff[128];
   pid_t pid = getpid();
   int cnt = 0;
   while (1)
   {
       // snprintf(buff, sizeof(buff), "sent message: %s:,pid: %d,cnt: %d\n", msg, pid, cnt);
       // write(wfd, buff, strlen(buff));
       char c = 'A';
       cnt++;

       write(wfd, &c, 1);
       printf("%d\n",cnt);
       sleep(1);
       if (cnt == 10)
       		break;
   }
   close(wfd);
}

void reader(int rfd)
{
   char buff[1024];
   int cnt = 10;
   while (1)
   {
       // sleep(1);
       ssize_t n = read(rfd, buff, sizeof(buff) - 1);
       if (n > 0)
       {
           buff[n] = '\0'; // Add null terminator after reading
           printf("get message: %s ,n: %ld\n", buff, n);
       }
       else if (n == 0)
       {
           printf("read file done!\n");
           break;
       }
       else
       {
           printf("read error!\n");
           break;
       }

       --cnt;
       if (cnt == 0)
           break;
   }
   close(rfd);
   printf("read endpoint done!\n");
}

int main()
{
   int pipefd[2];
   int ret = pipe(pipefd);
   if (ret == -1)
       return -1;

   printf("pipefd[0]:%d,pipefd[1]:%d\n", pipefd[0], pipefd[1]);

   pid_t id = fork();
   if (id == 0)
   {
       close(pipefd[0]);
       // w
       writer(pipefd[1]);
       exit(10);
   }
   // 父进程
   close(pipefd[1]);
   reader(pipefd[0]);

   // wait(NULL);
   int status = 0;
   pid_t rid = waitpid(id, &status, 0);
   if (rid == id)
   {
       printf("exit code:%d ,exit signal:%d\n", WEXITSTATUS(status), status & 0x7F);
   }
   return 0;
}

可以看到,父进程读了10次数据后就直接退出了,此时子进程本来还在写的,但是父进程退出了,子进程也被操作系统强制退出了!并且我们获取了子进程的退出信号为13!


2.2.4、父子进程通信特性
  • 自带同步机制
  • 血缘关系进程通信,常见于父子
  • pipe是面向字节流的
  • 父子进程退出,管道自动释放,文件的生命周期是随进程的
  • 管道只能单向通信,半双工的一种特殊情况

2.2.5、进程池

创建进程池,并使用进程池来执行任务。

大概有以下几个步骤:

  1. 创建通信信道和子进程
  2. 控制子进程
  3. 回收子进程

整体代码:

  • ProcessPool.cc文件

    #include <iostream>
    #include <unistd.h>
    #include <sys/wait.h>
    #include <sys/types.h>
    #include <vector>
    #include <ctime>
    
    using namespace std;
    
    #include "task.hpp"
    
    enum
    {
        UsageErr = 1,
        Sub_processNumErr,
        PipeErr
    };
    
    class Channel
    {
    public:
        Channel(int wfd, int sub_id, string name) : _wfd(wfd), _sub_process_id(sub_id), _name(name) {}
        ~Channel() {}
    
        void Debug()
        {
            cout << "_wfd :" << _wfd;
            cout << ",_sub_process_id :" << _sub_process_id;
            cout << ",_name :" << _name << endl;
        }
    
        int wfd() { return _wfd; }
        string name() { return _name; }
        pid_t pid() { return _sub_process_id; }
        void Close() { close(_wfd); }
    
    private:
        int _wfd;
        int _sub_process_id;
        string _name;
    };
    
    class ProcessPool
    {
    public:
        ProcessPool(int sub_porcessNum) : _sub_processNum(sub_porcessNum) {}
        ~ProcessPool() {}
        int CreateProcess(work_t work)
        {
            vector<int> fds; // 创建一个空的文件描述符数组,用来记录每个进程需要关闭的描述符
            // 因为子进程会继承父进程的文件描述符表,那么当父进程创建的越多,就越多子进程进程指向文件的写端,如果不一一关闭个子进程不属于自己的写文件描述符
            // 会导致如果按顺序关闭文件描述符的时候,子进程会阻塞,因为有写文件描述符没关完全。
            for (int i = 0; i < _sub_processNum; ++i)
            {
                int pipefd[2]{0};
                int n = pipe(pipefd);
                if (n < 0)
                    return PipeErr;
    
                pid_t id = fork();
                if (id == 0)
                {
                    if (!fds.empty())
                    {   
                        cout << "close fd: ";
                        
                        for (auto e : fds)
                        {
                            close(e);
                            cout << e << " ";
                        }
                        cout << endl;
                    }
                    close(pipefd[1]);
                    // child -- r
                    dup2(pipefd[0], 0);
                    work(pipefd[0]);
                    // sleep(100);
                    exit(0); // 正常退出
                }
                sleep(2);
    
                string cname = "Channel - " + to_string(i);
    
                close(pipefd[0]);
                // father -- w
    
                _channels.push_back(Channel(pipefd[1], id, cname));
                fds.push_back(pipefd[1]); // 记录文件描述符,给下一个子进程用来关闭该文件描述符
            }
            return 0;
        }
    
        void Debug()
        {
    
            for (auto &e : _channels)
            {
                e.Debug();
            }
        }
    
        int NextChannel()
        {
            static int next = 0;
            int c = next++;
            next %= _channels.size();
            return c;
        }
    
        int NextTask()
        {
            return rand() % 3;
        }
    
        void SendTaskCode(int channel, int code)
        {
            cout << "send code :" << code << " to " << _channels[channel].name() << "  sub process id: " << _channels[channel].pid() << endl;
            write(_channels[channel].wfd(), &code, sizeof(code)); // 把code写到_channels[channel].wfd()中,去给worker中的read读去code的值
        }
    
        // 先关闭子进程
        void KillAll()
        {
            for (auto &e : _channels)
            {
                e.Close();
                cout << e.name() << " close done "
                     << " sub process quit now:" << e.pid() << endl;
            }
        }
    
        // 再等待子进程,回收资源
        void Wait()
        {
    
            for (auto &e : _channels)
            {
                pid_t id = e.pid();
                pid_t rid = waitpid(id, nullptr, 0);
                if (id == rid)
                    cout << "wait sub process:" << id << " sucess..." << endl;
            }
        }
    
        //  直接关闭加等待一步到位
        void KillAndWait()
        {
            for (auto &e : _channels)
            {
                e.Close();
                cout << e.name() << " close done "
                     << " sub process quit now:" << e.pid() << endl;
                pid_t id = e.pid();
                pid_t rid = waitpid(id, nullptr, 0);
                if (id == rid)
                    cout << "wait sub process:" << id << " sucess..." << endl;
            }
        }
    
    private:
        vector<Channel> _channels;
        int _sub_processNum;
    };
    
    void CtrlProcessPool(ProcessPool *processpool_ptr, int cnt)
    {
    
        while (cnt)
        {
            // a.选择一个进程和通道
            int channel = processpool_ptr->NextChannel();
    
            // b.选择一个任务
            int code = processpool_ptr->NextTask();
    
            // c.发送任务
            processpool_ptr->SendTaskCode(channel, code);
    
            cnt--;
            sleep(1);
        }
    }
    
    void Usage_Command()
    {
        cout << "Usage: ./processpool number" << endl;
    }
    
    int main(int argc, char *argv[])
    {
    
        if (argc != 2)
        {
            Usage_Command();
            return UsageErr;
        }
    
        srand((uint32_t)time(nullptr));
        int sub_processNum = stoi(argv[1]);
        if (sub_processNum <= 0)
            return Sub_processNumErr;
    
        // 1.创建通信信道和子进程
        ProcessPool *processpool_ptr = new ProcessPool(sub_processNum);
        processpool_ptr->CreateProcess(worker);
        // processpool_ptr->Debug();
    
        // 2.控制子进程
        CtrlProcessPool(processpool_ptr, 10);
        cout << "task done ..." << endl;
    
        // sleep(100);
    
        // 3.回收子进程
        // processpool_ptr->KillAll();
        // processpool_ptr->Wait();
        processpool_ptr->KillAndWait();
        // sleep(100);
        cout << "wait sub process done ..." << endl;
        return 0;
    }
    
  • task.hpp文件

    #pragma once
              
    typedef void (*work_t)(int);
    typedef void (*task_t)(int, pid_t);
              
    void PrintLog(int fd, pid_t id)
    {
        cout << "sub process:" << id << " fd : " << fd << " task is : "
             << "PrintLog task\n" << endl;
    }
              
    void ConnectMysql(int fd, pid_t id)
    {
        cout << "sub process:" << id << " fd : " << fd << " task is : "
             << "ConnectMysql task\n" << endl;
    }
              
    void FlushPerson(int fd, pid_t id)
    {
        cout << "sub process:" << id << " fd : " << fd << " task is : "
             << "FlushPerson task\n" << endl;
    }
              
    task_t task[3] = {PrintLog, ConnectMysql, ConnectMysql};
              
    void worker(int fd)
    {
        // 这里为什么fd总是3? 因为每次父进程每创建一个子进程,关闭的读端都是3,那么下一次子进程的写端总是3
        // 从wfd = 0读取就行
        while (true)
        {
            uint32_t command_code = 0;
            ssize_t n = read(0, &command_code, sizeof(command_code)); // 从fd = 0读去放到command_code中
            if (n == sizeof(command_code))
            {
                if (command_code >= 3)
                    continue;
                task[command_code](fd, getpid());
                sleep(1);
            }
            else if (n == 0)
            {
                // 父进程没有任务了,管道里面没有数据了就直接退出
                break;
            }
            // cout << "I am worker"<<endl;
            sleep(1);
        }
    }
    

需要注意的是:在ProcessPool.cc文件中创建了一个vector<int> fds数组,原因是,创建一个空的文件描述符数组,用来记录每个进程需要关闭的描述符。因为子进程会继承父进程的文件描述符表,那么当父进程创建的越多,就越多子进程进程指向文件的写端,如果不一一关闭个子进程不属于自己的写文件描述符会导致如果按顺序关闭文件描述符的时候,子进程会阻塞,因为有写文件描述符没关完全。

还有一种解决方案就是先关闭子进程,在等待子进程。

还有一种解决方案是从后往前等待子进程。

该进程池的运行情况gif图如下:


2.3、命名管道

匿名管道是一般在父子进程之间通信。

如果我们想在不相关的进程之间交换数据,可以使用FIFO文件来做这项工作,它经常被称为命名管道。

命名管道是一种特殊类型的文件。

2.3.1、命名管道的创建

命名管道可以在命令行创建,使用命令:mkfifo filename

命名管道也可以在程序里创建,相关函数:

比如创建命名管道:

int main(){
       umask(0);
       mkfifo("fifo",0666);
       return 0;
}

2.3.2、命名管道和匿名管道的区别

匿名管道由pipe函数创建并打开

匿名管道由mkfifo函数创建,由open函数打开

但是两者在通讯上没有区别,都是使用管道


2.3.3、用命名管道实现server和client间通信
  • Comm.hpp文件
#pragma once
#include <iostream>
#include <cstring>
#include <string>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <error.h>
#include <errno.h>

using namespace std;

#define PATH "fifo"
#define MODE 0666

class Fifo
{
public:
   Fifo(const string &path) : _path(path)
   {
       //  创建管道文件
       umask(0);
       int n = mkfifo(path.c_str(), MODE);
       if (n == 0)
       {
           cout << "make fifo file:" << path << " sucess .." << endl;
       }
       else
       {
           //  创建失败
           cerr << "make fifo file "
                << "errorno:" << errno << " error info:" << strerror(errno) << endl;
       }
   }
   ~Fifo()
   {
       int n = unlink(PATH);
       if (n < 0)
       {
           cerr << "remove fifo file "
                << "errorno:" << errno << " error info:" << strerror(errno) << endl;
       }
       else
       {
           cout << "remove file:" << _path << " sucess ..." << endl;
       }
   }

private:
   string _path;
};
  • pipe_server.cc文件
#include "Comm.hpp"

int main()
{
   // 创建管道文件
   Fifo fifo(PATH);

   int rfd = open(PATH, O_RDONLY);
   if (rfd < 0)
   {
       cerr << "open error,  "
            << "errorno:" << errno << " error info:" << strerror(errno) << endl;
   }
   char buff[1024];
   while (true)
   {
       ssize_t n = read(rfd, buff, sizeof(buff) - 1);
       if (n > 0)
       {
           buff[n] = 0;
           cout << "get message :" << buff <<endl;
       }
       else if (n == 0)
       {
           cout << "read done .." << endl;
           break;
       }
       else
       {
           cerr << "read error,  "
                << "errorno:" << errno << " error info:" << strerror(errno) << endl;
           break;
       }
   }
   close(rfd);
   return 0;
}
  • pipe.client.cc文件
#include "Comm.hpp"

int main()
{
   int wfd = open(PATH, O_WRONLY);
   if (wfd < 0)
   {
       cerr << "open error,  "
            << "errorno:" << errno << " error info:" << strerror(errno) << endl;
       return 1;
   }
   string str;
   while (true)
   {
       cout << "please start write: ";
       getline(cin, str);
       if(str == "quit") break;
       ssize_t n = write(wfd, str.c_str(), str.size());
       if (n < 0)
       {
           cerr << "write error,  "
                << "errorno:" << errno << " error info:" << strerror(errno) << endl;
           break;
       }
   }
   close(wfd);
   return 0;
}

运行结果:


3、System V 共享内存

3.1、共享内存的介绍

共享内存区是最快的IPC(进程间通信)形式。一旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到内核,换句话说是进程不再通过执行进入内核的系统调用来传递彼此的数据。

共享内存示意图

共享内存数据结构

共享内存函数

shmget函数

功能:用来创建共享内存
原型
	 int shmget(key_t key, size_t size, int shmflg);
参数
    key:这个共享内存段名字
    size:共享内存大小
    shmflg:由九个权限标志构成,它们的用法和创建文件时使用的mode模式标志是一样的
返回值:成功返回一个非负整数,即该共享内存段的标识码;失败返回-1

shmat函数

功能:将共享内存段连接到进程地址空间
原型
	 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,表示连接操作用来只读共享内存

shmdt函数

功能:将共享内存段与当前进程脱离
原型
 	int shmdt(const void *shmaddr);
参数
   shmaddr: 由shmat所返回的指针
   返回值:成功返回0;失败返回-1
注意:将共享内存段与当前进程脱离不等于删除共享内存段

shmctl函数

功能:用于控制共享内存
原型
 	int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数
   shmid:由shmget返回的共享内存标识码
   cmd:将要采取的动作(有三个可取值)
   buf:指向一个保存着共享内存的模式状态和访问权限的数据结构
   返回值:成功返回0;失败返回-1


3.2、共享内存的使用

  • Comm.hpp文件
#include <iostream>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include "Fifo.hpp"

using namespace std;

const char *Path = "/home/xp2";
int proj_id = 0x168;
const int defaultSize = 4096;

string ToHex(key_t key)
{
    char buff[1024];
    snprintf(buff, sizeof(buff), "0x%x", key);
    return buff;
}

key_t GetshmKeyOrDie()
{
    key_t k = ftok(Path, proj_id);
    if (k < 0)
    {
        cerr << "ftok error, "
             << "error code: " << errno << ", error string :" << strerror(errno) << endl;
        exit(1);
    }
    return k;
}

int CreatShmOrDie(key_t key, int size, int flag)
{
    int shmid = shmget(key, size, flag);
    if (shmid < 0)
    {
        cerr << "shmget error, "
             << "error code: " << errno << ", error string :" << strerror(errno) << endl;
        exit(1);
    }
    return shmid;
}

int CreatShm(key_t key, int size)
{
    return CreatShmOrDie(key, size, IPC_CREAT | IPC_EXCL | 0666);
}

int GetShm(key_t key, int size)
{
    return CreatShmOrDie(key, size, IPC_CREAT);
}

// void *shmat(int shmid, const void *shmaddr, int shmflg);

//    int shmdt(const void *shmaddr);

void DeleteShm(int shmid)
{
    int n = shmctl(shmid, IPC_RMID, nullptr);
    if (n < 0)
    {
        cerr << "shmctl error, "
             << "error code: " << errno << ", error string :" << strerror(errno) << endl;
    }
    else
    {
        cout << "shmctl delete shm sucess,"
             << "shmid :" << shmid << endl;
    }
}

void *ShmAttach(int shmid)
{
    void *addr = shmat(shmid, nullptr, 0);
    if ((long long int)addr == -1)
    {
        cerr << "shmat error, "
             << "error code: " << errno << ", error string :" << strerror(errno) << endl;
        return nullptr;
    }
    return addr;
}

void ShmDetach(void *addr)
{
    int n = shmdt(addr);
    if (n < 0)
    {
        cerr << "shmdt error, "
             << "error code: " << errno << ", error string :" << strerror(errno) << endl;
    }
}

void DebugShm(int shmid)
{
    struct shmid_ds shmds;
    int n = shmctl(shmid, IPC_STAT, &shmds);
    if (n < 0)
    {
        cerr << "Debug error, "
             << "error code: " << errno << ", error string :" << strerror(errno) << endl;
        return;
    }
    else
    {
        cout << "shmds.shm_segsz: " << shmds.shm_segsz << endl;
        cout << "shmds.shm_nattch: " << shmds.shm_nattch << endl;
        cout << "shmds.shm_ctime: " << shmds.shm_ctime << endl;
        cout << "shmds.shm_perm.__key: " << ToHex(shmds.shm_perm.__key) << endl;
    }
}

  • Fifo.hpp文件:用来实现两个进程对共享内存读写的同步
#pragma once
#include <iostream>
#include <cstring>
#include <string>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <error.h>
#include <errno.h>
#include <assert.h>

using namespace std;

#define PATH "fifo"
#define MODE 0666

class Fifo
{
public:
    Fifo(const string &path) : _path(path)
    {
        //  创建管道文件
        umask(0);
        int n = mkfifo(path.c_str(), MODE);
        if (n == 0)
        {
            cout << "make fifo file:" << path << " sucess .." << endl;
        }
        else
        {
            //  创建失败
            cerr << "make fifo file "
                 << "errorno:" << errno << " error info:" << strerror(errno) << endl;
        }
    }
    ~Fifo()
    {
        int n = unlink(PATH);
        if (n < 0)
        {
            cerr << "remove fifo file "
                 << "errorno:" << errno << " error info:" << strerror(errno) << endl;
        }
        else
        {
            cout << "remove file:" << _path << " sucess ..." << endl;
        }
    }

private:
    string _path;
};

// 同步机制
class Sync
{
public:
    Sync() : rfd(-1), wfd(-1) {}

    void OpenReadOrDie()
    {
        rfd = open(PATH, O_RDONLY);
        if (rfd < 0)
            exit(1);
    }

    void OpenWriteOrDie()
    {
        wfd = open(PATH, O_WRONLY);
        if (wfd < 0)
            exit;
    }

    bool Wait()
    {
        bool ret = true;
        char c = 0; //  读一个字符
        ssize_t n = read(rfd, &c, sizeof(char));

        if (n == sizeof(char))
        {
            cout << "server wakeup, begin read shm..." << endl;
        }
        else
        {
            return false;
        }
        return ret;
    }

    void WakeUp()
    {
        char c = 0;
        ssize_t n = write(wfd, &c, sizeof(char));
        assert(n == sizeof(char));
        cout << "wakeup server...." << endl;
    }

    ~Sync() {}

private:
    int rfd;
    int wfd;
};
  • shm_server.cpp文件
#include "Comm.hpp"

int main()
{
    cout << ToHex(GetshmKeyOrDie()) << endl;

    key_t key = GetshmKeyOrDie();
    int shmid = CreatShm(key, defaultSize);

    if (shmid < 0)
    {
        cerr << "CreatShm error, "
             << "error code: " << errno << ", error string :" << strerror(errno) << endl;
        return 1;
    }
    else
    {
        cout << "shmid :" << shmid << endl;
    }

    cout << "server挂接前:" << endl;
    DebugShm(shmid);
    sleep(5);

    char *addr = (char *)ShmAttach(shmid);
    cout << "ShmAttach Address:" << ToHex((uint64_t)addr) << endl;

    cout << "server挂接后:" << endl;
    DebugShm(shmid);
    sleep(5);

    // 先创建管道
    Fifo fifo(PATH);
    Sync sync;
    sync.OpenReadOrDie();

    //  通信
    while (true)
    {
        bool ret = sync.Wait();
        if (ret == true)
        {
            cout << "shm content:" << addr << endl;
            sleep(1);
        }
        else
            break;
    }

    sleep(10);
    ShmDetach(addr);
    sleep(10);
    DeleteShm(shmid);
    sleep(5);
    return 0;
}
  • shm_client.cpp文件
#include "Comm.hpp"

int main()
{
    cout << ToHex(GetshmKeyOrDie()) << endl;

    key_t key = GetshmKeyOrDie();
    int shmid = GetShm(key, defaultSize);

    if (shmid < 0)
    {
        cerr << "GetShm error, "
             << "error code: " << errno << ", error string :" << strerror(errno) << endl;
        return 1;
    }
    else
    {
        cout << "shmid :" << shmid << endl;
    }

    char *addr = (char *)ShmAttach(shmid); // 页表映射的逻辑地址罢了
    cout << "ShmAttach Address:" << ToHex((uint64_t)addr) << endl;

    cout << "client挂接后:" << endl;
    DebugShm(shmid);
    sleep(5);

    // 通信
    memset(addr, 0, defaultSize); // 根据逻辑地址找到物理地址并初始化

    Sync sync;
    sync.OpenWriteOrDie();

    for (char c = 'A'; c <= 'Z'; ++c)
    {
        addr[c - 'A'] = c;
        sleep(1);
        sync.WakeUp();
    }

    sleep(10);
    ShmDetach(addr);
    sleep(10);
    return 0;
}

运行结果:


4、System V 消息队列(了解)

消息队列提供了一个从一个进程向另外一个进程发送一块数据的方法

每个数据块都被认为是有一个类型,接收者进程接收的数据块可以有不同的类型值

特性方面

​ IPC资源必须删除,否则不会自动清除,除非重启,所以system V IPC资源的生命周期随内核


5、System V 信号量(了解)

信号量主要用于同步(同步我们在管道中有涉及,也就是两个进程之间是有序的进行通信)和互斥的,下面先来看看什么是互斥。

进程互斥

  • 由于各进程要求共享资源,而且有些资源需要互斥使用,因此各进程间竞争使用这些资源,进程的这种关系为进程的互斥

  • 系统中某些资源一次只允许一个进程使用,称这样的资源为临界资源或互斥资源。

  • 在进程中涉及到互斥资源的程序段叫临界区

特性方面

​ IPC资源必须删除,否则不会自动清除,除非重启,所以system V IPC资源的生命周期随内核


那么好,Linux进程间通信就到这里,如果你对Linux和C++也感兴趣的话,可以看看我的主页哦。下面是我的github主页,里面记录了我的学习代码和leetcode的一些题的题解,有兴趣的可以看看。

Xpccccc的github主页

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/574332.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【window环境、Linux环境、QT三种方法实现TCP通信】

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、Windows环境下实现TCP通信1.服务器2.客户端3.运行 二、Linux环境下实现TCP通信1.服务端2.客户端 三、Qt实现TCP通信1.服务端1.客户端 总结 前言 大多数项目…

RAG文本解析工具open-parse

简介 对于RAG来说&#xff0c;将文本有效的分块(chucking)是很重要的一件事&#xff0c;open-parse是一个用来分块pdf的开源工具&#xff0c;它主要基于视觉驱动(Visually-Driven)的方式来将文档分块&#xff0c;也就是说它不仅仅是按照段落或者字数来对文档分块&#xff0c;而…

easyx 按键信息

前言 看看代码吧 ExMessage msg { 0 }; bool button(int x, int y, int w, int h, const char* text) {//绘制按钮setfillcolor(RGB(230, 231, 232));fillroundrect(x, y, x w, y h, 5, 5);if ((msg.x > x && msg.x<x w && msg.y>y && …

为什么要分库分表?(设计高并发系统的时候,数据库层面该如何设计?)

目录 1.分表 2.分库 说白了&#xff0c;分库分表是两回事儿&#xff0c;大家可别搞混了&#xff0c;可能是光分库不分表&#xff0c;也可能是光分表不分库&#xff0c;都有可能。 我先给大家抛出来一个场景。 假如我们现在是一个小创业公司(或者是一个 BAT …

java反序列化之URLDNS链学习

一、前言 近来学习java反序列化&#xff0c;听p神所说这个URLDNS利用链比较好理解&#xff0c;故决定由此进入学习的第一篇。 URLDNS是Java反序列化中比较简单的一个链&#xff0c;由于URLDNS不需要依赖第三方的包&#xff0c;同时不限制jdk的版本&#xff0c;所以通常用于检…

hertzbeat 源码阅读记录

关于自定义标签的说明 EmailValid.java HostValid PhoneNumValid 枚举值说明&#xff1a;

【OpenGL实践08】现代渲染管线在GLUT和Pygame和Qt.QOpenGLWidget上各自的实现代码

Qt.QOpenGLWidget进行现代渲染管线实验效果 一、说明 据说QOpenGLWidget是用来取代QGLWidget的继承者&#xff0c;我们试图将GLUT上的旧代码改成QOpenGLWidget&#xff0c;本以为差别不大&#xff0c;轻易搞定&#xff0c;经实践发现要付出极大努力才能完成。经多次实验发现G…

Java面试八股之Java中为什么没有全局变量

Java中为什么没有全局变量 Java中没有传统意义上的全局变量&#xff0c;这是因为Java语言设计遵循面向对象的原则&#xff0c;强调封装性和模块化&#xff0c;以及避免全局状态带来的副作用。 封装性&#xff1a; 全局变量违反了面向对象编程中的封装原则&#xff0c;即隐藏对…

【ZYNQ】zynq启动模式及程序固化

一、前言 由于zynq含有arm cpu ,其启动模式由ps主导&#xff0c;与纯逻辑的fpga不相同&#xff0c;此处做一个记录。 二、zynq启动模式 关于zynq的启动模式详细内容可以参考官方文档&#xff1a;ug585-Zynq 7000 SoC Technical Reference Manual&#xff0c;第六章。 2.1 启…

帮助中心系统搭建不再是难题,这几个工具来帮你

在面临客户服务挑战时&#xff0c;有效的帮助中心系统是提升用户满意度和解决问题效率的关键。幸运的是&#xff0c;搭建一个功能全面的帮助中心不再是什么难事。下面&#xff0c;我要为你介绍三款能够帮忙打造帮助中心的超实用工具&#xff0c;让你的客户支持体验迅速升级。 1…

网页使用之如何返回json/xml

后端返回json数据给前端进行渲染的方式比较熟悉&#xff0c;至于返回html页面&#xff0c;返回xml的方式接触逐渐减少&#xff0c;来在项目中熟悉这一点。 返回文本数据 json姿势的返回实属最简单的方式&#xff0c;在SpringBoot应用中&#xff0c;有两种简单的方式 1.直接在…

S32K的JLINK与PE接线方法与刷程序失败问题

S32K的JLINK与PE接线方法与刷程序失败问题 1、PE的接线方法2、JLINK的接线方法3、刷程序失败问题 1、PE的接线方法 2、JLINK的接线方法 3、刷程序失败问题 出现如下问题&#xff1a; Secure Debug might be enabled on this device.lf so.please unlock the device via PEmic…

一段音频驱动照片唱歌,EMO模型上线通义APP

把一段音频、一张照片输入AI模型&#xff0c;就能让图中人物开口唱歌说话&#xff0c;让奥黛丽赫本唱《上春山》、陶俑仕女说英文RAP、爱因斯坦说中文段子。不久前&#xff0c;这款名为EMO的模型因为阿里通义实验室的一篇论文火遍海内外&#xff0c;模型的产品化进程也广受关注…

运动耳机哪个牌子性价比高?推荐五款高性价比运动耳机

跑步、健身、游泳……无论你的运动喜好是什么&#xff0c;一款好的运动蓝牙耳机都能为你的运动体验加分。然而&#xff0c;市面上的运动蓝牙耳机品牌众多&#xff0c;如何选择一款既舒适又实用的产品呢&#xff1f;本文将为你提供一些选购运动蓝牙耳机建议&#xff0c;并为你推…

企业规模扩大,SD-WAN实现跨省快速组网

随着数字化时代的飞速发展&#xff0c;企业面临着前所未有的挑战与机遇。5G、VoIP、AI和物联网等新技术的兴起&#xff0c;不仅改变了商业格局&#xff0c;也对企业网络提出了更高的要求。随着企业规模的不断扩大&#xff0c;企业如何搭建跨省的、高性能、超融合、简化运维的组…

解决Jmeter 4.x 请求到elasticsearch 中文乱码的问题

文章目录 前言解决Jmeter 4.x 请求到elasticsearch 中文乱码的问题 前言 如果您觉得有用的话&#xff0c;记得给博主点个赞&#xff0c;评论&#xff0c;收藏一键三连啊&#xff0c;写作不易啊^ _ ^。   而且听说点赞的人每天的运气都不会太差&#xff0c;实在白嫖的话&#…

MOS产品在光伏逆变器上的应用与数据分析

2023年全球光伏装机量表现优异&#xff0c;根据BloombergNEF统计数据&#xff0c;2023年全球光伏新增装机量444GW&#xff0c;同比增长76.2%&#xff0c;其中约一半新增装机量来自中国。 中国光伏新技术迭代不断&#xff0c;产业链降本增效加速。根据CPIA数据&#xff0c;2022年…

Linux网络-DNS域名解析服务

目录 一.DNS相关介绍 1.DNS是什么 2.DNS系统的分布式数据结构 根域 顶级域 二级域 子域 主机 3.服务器类型 主域名服务器 从域名服务器 缓存域名服务器 转发域名服务器 二.DNS域名解析 1.DNS域名解析方式及功能 2.DNS域名解析查询方式 2.1.递归查询&#xff0…

【LLM多模态】Qwen-VL模型结构和训练流程

note 观点&#xff1a;现有很多多模态大模型是基于预训练&#xff08;和SFT对齐&#xff09;的语言模型&#xff0c;将视觉特征token化并对齐到语言空间中&#xff0c;利用语言模型得到多模态LLM的输出。如何设计更好的图像tokenizer以及定位语言模型在多模态LLM中的作用很重要…

零基础HTML教程(27)--表单元素属性

文章目录 1. 背景2. disabled:禁用3. readonly:直读4. checked:选中5. maxlength:最大输入字符数6. 小结 1. 背景 上一篇我们讲了表单元素的两个属性id和name&#xff0c;其实表单元素还有其他几个常用属性&#xff0c;我们逐一介绍。 2. disabled:禁用 disabled属性表示禁用…
最新文章