进程间通信(Inter-Process Communication,IPC) -- 匿名管道,命名管道,System V,共享内存,消息队列,信号量

Linux进程间通信(IPC)核心机制详解:从管道到共享内存实战指南 进程间通信是操作系统和分布式系统的基础概念,它定义了不同进程间交换数据和协调工作的机制。其核心原理在于突破进程隔离的内存空间,通过内核提供的多种通道实现数据传递。这项技术的工程价值在于构建模块化、可扩展的软件架构,是实现高性能服务器、桌面应用和嵌入式系统的关键技术。在实际应用场景中,开发者需要根据数据传输量、性能要求和进程关系,在匿名管道、命名管道、共享内存、消息队列、Unix域套接字和信号等机制中做出选择。例如,共享内存以其直接内存访问的特性,成为大数据交换和高性能计算场景的首选,但必须配合信号量等同步机 阅读详情

目录

1. 进程间通信介绍

1.1 进程间通信的目的

1.2 进程间通信分类

2. 管道

3. 匿名管道

3.1 匿名管道的demo代码

3.2 用 fork 来共享管道的原理

3.3 文件描述符角度 -- 理解管道 

3.4 内核角度 -- 理解管道本质 

3.5 管道读写规则

3.6 管道的五种特性

3.7 管道样例 -- 进程池

3.7.1 进程池demo代码

3.7.2 demo代码的一个bug

4. 命名管道

4.1 创建命名管道及删除

4.2 匿名管道和命名管道的区别

4.3 命名管道的打开规则

4.4 命名管道的实例

4.4.1 用命令管道实现文件拷贝

4.4.2 用命名管道实现 server 和 client 通信

5. system V共享内存

5.1 共享内存的原理

5.2 共享内存的内核数据结构

5.3 共享内存函数

5.4 共享内存的实例

6. system V消息队列(了解即可)

7. system V信号量(了解即可)

7.1 并发编程的概念铺垫

7.2 信号量

8. 内核是如何组织管理IPC资源的


1. 进程间通信介绍

1.1 进程间通信的目的

        1. 数据传输:⼀个进程需要将它的数据发送给另⼀个进程。

        2. 资源共享:多个进程之间共享同样的资源。

        3. 通知事件:⼀个进程需要向另⼀个或⼀组进程发送消息,通知它(它们)发⽣了某种事件(如进程终⽌时要通知⽗进程)。

        4. 进程控制:有些进程希望完全控制另⼀个进程的执⾏(如Debug进程),此时控制进程希望能够拦截另⼀个进程的所有陷⼊和异常,并能够及时知道它的状态改变。

1.2 进程间通信分类

        1. 管道:(1)匿名管道pipe(2)命名管道

        2. System IPC:(1)System V 消息队列(2)System V共享内存(3)System V 信号量

        3. POSIX IPC:(1)消息队列(2)共享内存(3)信号量(4)互斥量(5)条件变量(6)读写锁

2. 管道

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

        上图表示,将who进程输出的结果给到 wc -l 进程进行处理,然后输出对应输出。,其中 “|” 就是管道。who 命令显示关于当前在本地系统上的所有登录用户的信息,而 wc -l 统计文本文件的行数。

3. 匿名管道

        匿名管道是用于具有血缘关系间进程通信的一种方式,常用与父子进程。匿名管道也是一种内存级的文件,所以当进程打开该管道时,也会有对应的文件描述符。

#include <unistd.h>

//功能:创建⼀匿名管道
int pipe(int fd[2]);

//参数
//fd:⽂件描述符数组,其中fd[0]表⽰读端, fd[1]表⽰写端
//返回值:成功返回0,失败返回错误代码

        一般的进程的文件描述符0,1,2分别代表的是标准输入,标准输出,标准错误。所以调用pipe函数的进程给匿名管道分配的文件描述符一般为3,4,。3是读端,4是写端。 

3.1 匿名管道的demo代码

#include <iostream>
#include <cstdio>
#include <cstring>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

void ChildWrite(int wfd)
{
    char buffer[1024];
    int cnt = 0;
    while(true)
    {
        snprintf(buffer, sizeof(buffer), "I am child, pid: %d, cnt: %d", getpid(), cnt++);
        write(wfd, buffer, strlen(buffer));
        sleep(1);
    }
}

void FatherRead(int rfd)
{
    char buffer[1024];
    while(true)
    {
        buffer[0] = 0;
        ssize_t n = read(rfd, buffer, sizeof(buffer)-1);
        if (n > 0)
        {
            buffer[n] = 0;
            std::cout << "child say: " << buffer << std::endl;
        }
    }
}

int main()
{
    //1. 创建管道文件
    int fds[2] = {0};
    int i = pipe(fds);
    if (i < 0)
    {
        std::cout << "创建管道失败" << std::endl;
        return 1;
    }
    // std::cout << "fds[0]: " << fds[0] << std::endl;
    // std::cout << "fds[1]: " << fds[1] << std::endl;
    
    // 2. 创建子进程
    pid_t id = fork();
    if (id == 0)
    {
        // child
        // 子进程关闭读端
        close(fds[0]);
        ChildWrite(fds[1]);
    }

    //3. 关闭不需要的读写端,新城通信信道
    //父进程关闭写端
    close(fds[1]);
    FatherRead(fds[0]);
    waitpid(id, nullptr, 0);
    close(fds[0]);

    return 0;
}

         这段代码的意思就是通过子进程向管道文件中不断写入数据,然后父进程读取子进程写入的数据输出到标准输出中。 

3.2 用 fork 来共享管道的原理

3.3 文件描述符角度 -- 理解管道 

        父进程中创建管道文件,给其分配两个文件描述符,然后父进程中在创建一个子进程,子进程的文件描述符和父进程相同。分别关闭父进程的读端和子进程的写端,就能形成一个父进程写,子进程读的通信信道。

3.4 内核角度 -- 理解管道本质 

        从内核的角度上来开,父子进程都打开同一个管道文件,都为这一个管道文件创建了struct_file结构体,而两个结构体指向同一份内存资源,所以看待管道,就和看待文件一样。

3.5 管道读写规则

        1. 当写端较慢,读端较快时:读端就会阻塞在read函数处,等到写端写入。

        2. 当写端较快,读端较慢时:管道文件的缓冲区很容易被写满,这时写端就要等待读端将数据读走再写入。

        3. 当写端进程关闭指向管道写端的文件描述符,读端继续读:read函数就会返回0,表示读到文件结尾。

        4. 当写端继续写,读端进程关闭指向管道读端的文件描述符:操作系统会给写端进程发送13号信号 SIGPIPE ,杀掉写端进程。

        5. 当写入的数据量不大于管道的缓冲区,linux将保证写入的原子性;当写入的数据量大于管道的缓冲区,linux将不再保证写入的原子性。

3.6 管道的五种特性

        1. 匿名管道只能用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信;通常,一个管道由一个进程创建,然后该进程调用fork,此后父子进程之间就可应用该管道。

        2. 管道文件自带同步机制,这种情况就是当写端较慢,读端较快的时候,读端需要等写端写入。

        3. 管道是面向字节流的。

        4. 管道是半双工的工作模式,在同一个时刻,数据只能一个进程发送,一个进程接收;需要双方通信时,需要建立起两个管道。

        5. 一般而言,进程退出,管道文件就会释放,所以管道的什么周期随进程。

3.7 管道样例 -- 进程池

        这里使用一个进程池的例子来说明上述的内容。这里创建的进程池的进程数量为5个,然后父进程是写端,子进程是读端,父进程通过管道给子进程发送任务码,然后子进程解析父进程发来的任务码执行相应的任务。

        下图表示父子进程使用管道通讯的示意图。父进程循环创建管道文件和子进程。

        第一次父进程创建管道文件时,占用3,4 文件描述符,但是父进程为写端,所以关闭3号文件描述符,而子进程1关闭4号文件描述符;第二次创建管道文件时,因为4号文件描述符指向了管道文件1的写端,所以父进程占用3,5文件描述符,这是父进程3号文件描述符指向管道文件2的读端,4号文件描述指向管道文件1的写端,5号文件描述指向管道文件2的写端,创建子进程2时,子进程继承父进程的文件描述符表,所以将不要的文件描述符关闭之后,父进程的4,5号文件描述符分别指向管道文件1的写端和管道文件2的写端,但是子进程2的文件描述符表中,3号文件描述符指向管道文件2的读端,而4号文件描述符指向管道文件1的写端。同理,子进程3也一样。

        所以下图中,黑线是正常文件描述符的指向,而红线是每个子进程多的文件描述符的指向。这个问题后续再进行处理。

3.7.1 进程池demo代码

        main.cc中是整个进程池运行的主要流程,一是创建出进程池对象ProcessPool,二是启动并创建进程池,三是给进程池中的子进程派发任务并执行,四是关闭指向子进程的文件描述符,子进程退出之后,通过waitpid回收子进程。

//main.cc

#include "ProcessPool.hpp"

int main()
{
    //  1. 创建进程池对象
    ProcessPool pp(gdefalutnum);

    //  2. 启动进程池
    pp.Start();

    // pp.Debug();

    //  3. 给进程池派发任务并执行
    int cnt = 10;
    while(cnt--)
    {
        pp.Run();
        sleep(1);
    }

    //  4. 回收资源,结束进程池
    pp.Stop();
    
    return 0;
}

         ProcessPool.hpp是将.h文件和.cpp文件结合起来,直接在文件中声明并定义类和函数了。这个文件主要包含Channel类和ChannelManager类和ProcessPool类。Channel类主要是对进程池中的每一个信道的信息进行描述。ChannelManager类是将每一个信道统一管理起来的类。ProcessPool类是进程池类,里面包含了进程池中通道的个数,ChannelManager对象以及管理分配给子进程的任务对象TaskManager类对象。

//ProcessPool.hpp

#ifndef __PROCESS_POOL_HPP__
#define __PROCESS_POOL_HPP__

#include <iostream>
#include <cstdlib>
#include <vector>
#include <unistd.h>
#include <sys/wait.h>
#include "Task.hpp"

//  描述一个信道的类,从父进程视角看,只需要看到一个信道中管道的写端描述符和对应子进程的pid
class Channel
{
public:
    Channel(int fd, pid_t id) : _wfd(fd), _subId(id)
    {
        _name = "Channel-" + std::to_string(_wfd) + "-" + std::to_string(_subId);
    }
    ~Channel()
    {
    }
    void Send(int code)
    {
        int n = write(_wfd, &code, sizeof(code));
        void(n);
    }
    void Close()
    {
        close(_wfd);
    }
    void Wait()
    {
        pid_t rid = waitpid(_subId, nullptr, 0);
    }
    int Fd()
    {
        return _wfd;
    }
    pid_t SubId()
    {
        return _subId;
    }
    std::string Name()
    {
        return _name;
    }

private:
    // 一个信道的写端文件描述符,子进程pid以及名字
    int _wfd;
    pid_t _subId;
    std::string _name;
};

//  一个管理进程池进程的类
class ChannelManager
{
public:
    ChannelManager() : _next(0)
    {
    }
    ~ChannelManager()
    {
    }
    void Insert(int wfd, pid_t subId)
    {
        Channel c(wfd, subId);
        _channels.push_back(c);
    }
    Channel &Select()
    {
        //  使用轮询的方式选择信道
        auto &c = _channels[_next];
        _next++;
        _next %= _channels.size();
        return c;
    }
    void PrintChannel()
    {
        for (auto &channel : _channels)
        {
            std::cout << channel.Name() << std::endl;
        }
    }
    void StopSubProcess()
    {
        for (auto &channel : _channels)
        {
            channel.Close();
            std::cout << "关闭:" << channel.Name() << std::endl;
        }
    }
    void WaitSubProcess()
    {
        for (auto &channel : _channels)
        {
            channel.Wait();
            std::cout << "回收:" << channel.Name() << std::endl;
        }
    }

private:
    std::vector<Channel> _channels;
    int _next;
};

#define gdefalutnum 5

//  描述一个进程池的类
class ProcessPool
{
public:
    //  初始化进程池进程个数以及注册各种任务
    ProcessPool(int num) : _process_num(num)
    {
        _tm.Register(PrintLog);
        _tm.Register(Download);
        _tm.Register(Upload);
    }
    //  子进程进行读任务
    void Work(int rfd)
    {
        while (true)
        {
            int code = 0;
            ssize_t n = read(rfd, &code, sizeof(code));
            if (n > 0)
            {
                if (n != sizeof(code))
                {
                    continue;
                }
                std::cout << "子进程[" << getpid() << "]收到一个任务码: " << code << std::endl;
                _tm.Excute(code);
            }
            else if (n == 0)
            {
                std::cout << "子进程" << getpid() << "退出" << std::endl;
                break;
            }
            else
            {
                std::cout << "读取错误" << std::endl;
                break;
            }
        }
    }

    //  创建并启动进程池
    bool Start()
    {
        for (int i = 0; i < _process_num; i++)
        {
            //  1. 创建管道
            int pipefd[2] = {0};
            int n = pipe(pipefd);
            if (n < 0)
            {
                return false;
            }

            //  2. 创建子进程
            pid_t subId = fork();
            if (subId < 0)
            {
                return false;
            }
            else if (subId == 0)
            {
                //  子进程
                //  3. 关闭不需要的文件描述符
                close(pipefd[1]);
                Work(pipefd[0]);
                close(pipefd[0]);
                exit(0);
            }
            else
            {
                //  父进程
                //  3. 关闭不需要的文件描述符
                close(pipefd[0]);
                _cm.Insert(pipefd[1], subId);
            }
        }
        return true;
    }
    //  测试进程池是否创建成功
    void Debug()
    {
        _cm.PrintChannel();
    }
    void Run()
    {
        //  1. 选择一个任务
        int taskCode = _tm.Code();

        //  2. 选择一个子进程,负载均匀的选择一个子进程(轮询)完成任务
        auto &c = _cm.Select();
        std::cout << "选择了一个子进程:" << c.Name() << std::endl;
        //  3. 发送任务
        c.Send(taskCode);
        std::cout << "发送了一个任务码:" << taskCode << std:: endl;

    }
    void Stop()
    {
        //  关闭父进程所有的wfd
        _cm.StopSubProcess();
        //  回收所有子进程
        _cm.WaitSubProcess();
    }
    ~ProcessPool()
    {
    }

private:
    ChannelManager _cm;
    int _process_num;
    TaskManager _tm;
};

#endif

         Task.hpp中定义了TaskManager类,用于管理子进程需要执行的任务。

//Task.hpp

#pragma once

#include <iostream>
#include <vector>
#include <ctime>

typedef void (*task_t)(); 

void PrintLog()
{
    std::cout << "打印日志的任务" << std::endl;
}

void Download()
{
    std::cout << "下载的任务" << std::endl;
}

void Upload()
{
    std::cout << "上传的任务" << std::endl;
}

class TaskManager
{
public:
    TaskManager()
    {
        srand(time(nullptr));
    }
    void Register(task_t t)
    {
        _tasks.push_back(t);
    }
    int Code()
    {
        return rand() % _tasks.size();
    }
    void Excute(int code)
    {
        if (code >= 0 && code < _tasks.size())
        {
            _tasks[code]();
        }
    }
private:
    std::vector<task_t> _tasks; 
};

        这里对整个项目代码做一定的解释。

        1. 创建进程池对象pp,在ProcessPool的构造函数中执行需要创建的进程池有gdefalutnum个信道,然后将待执行任务注册到TaskManager对象中。

        2. 调用Start函数启动进程池,启动的过程就是循环创建管道文件和子进程的过程,创建的时候同时关闭各个进程中不需要的文件描述符。 

        3. 循环给子进程派发任务,循环执行run函数,在Run函数中,首先随机选择一个任务码,然后轮询选择一个子进程,将任务码交给子进程后,子进程通过read函数从rfd文件描述符指向管道文件的读端读取任务码,并进行解析指向任务。这里在父进程中的写入相对于子进程读取来说是较慢的,所以当父进程未写入的时候,子进程会阻塞在read函数中等待父进程进行写入。

        4. 循环结束之后,调用Stop函数,循环关闭父进程中指向管道文件的文件描述符,然后循环调用waitpid函数对子进程进行回收。

        执行上述代码会得到如下结果,下图表示结果的一部分。

        可以看到,这里循环分配任务结束之后,父进程关闭了指向管道文件写端的文件描述符,然后子进程从后到前依次退出,最后回收子进程程序结束。 

3.7.2 demo代码的一个bug

        从上述结果可以看到,通信信道是依次关闭的,子进程也是依次回收的,但是子进程退出的顺序是反的。为什么会出现这种情况,可以从下图进行分析。

        当任务执行完毕之后,前面的管道文件的写端,除了会被父进程的文件描述符指向外,都会被它的弟弟进程的文件描述符指向,所以,当父进程关闭指向管道文件的文件描述符之后,前面四个管道文件都有弟弟进程的文件描述符指向,所以进程还是会阻塞在read函数那等待输入。

        但是最后一个管道文件的写端则没有进程的文件描述符指向,所以最后一个子进程的read函数返回0,然后子进程退出,当该子进程退出之后,它的上一个子进程对应的管道文件的写端也没有进程指向,所以上一个子进程也接着退出,这样依次,从后到前,所以子进程依次退出。

        因为上述代码中,文件描述符的关闭和子进程的回收是分开写的,当合在一起写的时候,修改成下述代码,就是关闭父进程指向管道文件1写端的文件描述符立即回收子进程1,这样的话,子进程1就会阻塞在read函数处,而父进程也会阻塞在waitpid函数处。

    void StopAndWait()
    {
        for (auto &channel : _channels)
        {
            channel.Close();
            std::cout << "关闭:" << channel.Name() << std::endl;
            channel.Wait();
            std::cout << "回收:" << channel.Name() << std::endl;
        }
    }

         这就会出现阻塞的情况。

        如果要将关闭文件描述符和回收子进程合在一起写,并且不出现上述阻塞的情况,有两种方法:

                1. 和原始的代码一样,从后到前对通道进行关闭并回收子进程。

                2. 在子进程创建的时候就对其子进程文件描述符表中指向哥哥进程对应的管道文件写端的文件描述符关闭。

        注:子进程的_cm也是继承自父进程,当父进程对_cm进行插入时,发生写时拷贝,所以后面的子进程中的_cm里面,只有指向哥哥进程对应的管道文件写端的文件描述符,所以可以实现在创建子进程的时候就可以进行关闭,结果如下。 

4. 命名管道

        匿名管道的一个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信。如果想在不相关的进程之间交换数据,可以使用FIFO文件来做这项工作,它经常被称为命名管道。命名管道是一种特殊类型的文件。只需要被打开,文件缓冲区的数据并不会刷新到磁盘中。

4.1 创建命名管道及删除

        1. 命令行创建管道文件:

mkfifo filename

        删除管道文件可以使用如下两个命令:

rm filename

unlink filename

         2. 程序里创建管道文件,相关函数如下:

#include <sys/types.h>
#Include <sys/stat.h>

int mkfifo(const char *filename, mode_t mode);
#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>

int main()
{
    mkfifo("pipe_2", 0644);
    return 0;
}

4.2 匿名管道和命名管道的区别

        1. 匿名管道由pipe函数创建并打开;命名管道由mkfifo函数创建,打开用open。

        2. FIFO(命名管道)与pipe(匿名管道)之间唯一的区别在它们创建与打开的方式不同,它们的使用都是相同的。 

4.3 命名管道的打开规则

        1. 如果当前打开操作是为读而打开FIFO时:阻塞直到有相应进程为写⽽打开该FIFO。

        2. 如果当前打开操作是为写而打开FIFO时:阻塞直到有相应进程为读⽽打开该FIFO。

4.4 命名管道的实例

4.4.1 用命令管道实现文件拷贝

//read.cc -- 读取文件,写入命名管道中

#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>

//  封装一下错误退出的宏
#define ERR_EXIT(m) \
do \
{   \
    perror(m);  \
    exit(EXIT_FAILURE);  \
} while(0)

//  读取文件,写入命令管道
int main()
{
    //  1. 创建命名管道文件fifo
    int ret = mkfifo("fifo", 0664);
    if (ret < 0)
    {
        ERR_EXIT("mkfifo");
    }

    //  2. 以读方式打开待读文件abc
    int infd = open("abc", O_RDONLY);
    if (infd == -1)
        ERR_EXIT("open");

    //  3. 以写方式打开命名管道fifo
    int outfd;
    outfd = open("fifo", O_WRONLY);
    if (outfd == -1)
        ERR_EXIT("open");

    //  4.从待读文件中读取数据,写入管道文件中
    char buf[1024];
    int n;
    while((n = read(infd, buf, 1024)) > 0)
    {
        write(outfd, buf, n);
    }

    close(infd);
    close(outfd);
    return 0;
}
//write.cc -- 读取管道文件,写入目标文件中

#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>

#define ERR_EXIT(m) \
do \
{   \
    perror(m); \
    exit(EXIT_FAILURE); \
}while(0)


//  从管道文件中读数据,写入到目标文件中
int main()
{
    //  1. 以读的方式打开管道文件fifo
    int infd = open("fifo", O_RDONLY, 0644);
    if (infd == -1)
        ERR_EXIT("open");

    //  2. 以写方式打开目标文件abc.back
    int outfd = open("abc.back", O_WRONLY | O_CREAT | O_TRUNC, 0644);
    if (outfd == -1)
        ERR_EXIT("open");

    //  3.从管道文件fifo中读数据并写入到目标文件中
    char buf[1024];
    int n;
    while((n = read(infd, buf, 1024)) > 0)
    {
        write(outfd, buf, n);
    }
    close(infd);
    close(outfd);
    // 删除管道文件 -- 系统调用
    unlink("fifo");

    return 0;
}

        上述代码通过命名管道实现了一个从abc文件中读取内容,然后写入到abc.back中的demo。 

4.4.2 用命名管道实现 server 和 client 通信

        server作为读端,将client写入到管道文件的数据读出,显示到显示器上。

// comm.hpp -- 用于描述管道文件的类和用于描述对管道文件操作的类
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <string>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>

#define PATH "."
#define NAME "fifo"

#define ERR_EXIT(m)         \
    do                      \
    {                       \
        perror(m);          \
        exit(EXIT_FAILURE); \
    } while (0)

class NamedFifo
{
public:
    NamedFifo(const std::string path, const std::string name)
        : _path(path), _name(name)
    {
        // 创建管道文件
        _filename = _path + "/" + _name;
        umask(0);
        int n = mkfifo(_filename.c_str(), 0664);
        if (n < 0)
            ERR_EXIT("mkfifo");
        else
        {
            std::cout << "mkfifo success" << std::endl;
        }
    }

    ~NamedFifo()
    {
        int n = unlink(_filename.c_str());
        // 删除成功
        if (n == 0)
        {
            ERR_EXIT("unlink");
        }
        else    // 删除失败
        {
            std::cout << "remove fifo failed" << std::endl;
        }
    }

private:
    std::string _path;
    std::string _name;
    std::string _filename;
};

class FileOperation
{
public:
    FileOperation(const std::string path, const std::string name)
        : _path(path), _name(name), _fd(-1)
    {
        _filename = _path + "/" + _name;
    }

    void OpenForRead()
    {
        _fd = open(_filename.c_str(), O_RDONLY);
        if (_fd < 0)
            ERR_EXIT("open");
        else if (_fd > 0)
        {
            std::cout << "open success" << std::endl;
        }
        else
        {

        }
    }
    void OpenForWrite()
    {
        _fd = open(_filename.c_str(), O_WRONLY);
        if (_fd < 0)
            ERR_EXIT("open");
        else if (_fd > 0)
        {
            std::cout << "open success" << std::endl;
        }
        else
        {

        }
    }
    //  服务端进行读
    void Read()
    {
        char buf[1024];
        int n;
        while (true)
        {
            n = read(_fd, buf, sizeof(buf) - 1);
            buf[n] = 0;
            if (n > 0)
            {
                std::cout << "Client say # " << buf << std::endl;
            }
            else if (n == 0)
            {
                std::cout << "Client quit! me too !" << std::endl;
                break;
            }
            else
            {
                // TODO
            }
        }
    }
    //  客户端进行写
    void Write()
    {
        std::string message;
        while (true)
        {
            std::cin >> message;
            int n = 0;
            write(_fd, message.c_str(), message.size());
        }
    }
    void Close()
    {
        close(_fd);
    }

private:
    std::string _path;
    std::string _name;
    std::string _filename;
    int _fd;
};
// server.cc

#include "comm.hpp"

int main()
{
    NamedFifo nf(PATH, NAME);
    FileOperation fo(PATH, NAME);
    fo.OpenForRead();
    fo.Read();
    fo.Close();
    return 0;
}
// client.cc

#include "comm.hpp"

int main()
{
    FileOperation fo(PATH, NAME);
    fo.OpenForWrite();
    fo.Write();
    fo.Close();
    return 0;
}

5. system V共享内存

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

        共享内存,消息队列和信号量三种IPC资源的生命周期随内核的生命周期,IPC资源必须调用命令或者调用系统调用进行删除,否则不会自动清除,除非重启。

5.1 共享内存的原理

        使用共享内存在两个进程之间传递数据如下图所示。在物理内存空间开辟一块内存空间,将这块内存空间分别映射到两个进程虚拟内存空间的共享区中,进程就可以通过访问自己进程的虚拟地址空间进行进程间通信了。

5.2 共享内存的内核数据结构

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 */
    };

5.3 共享内存函数

shgmet函数
头文件:
    #include <sys/ipc.h>
    #include <sys/shm.h>
功能:⽤来创建共享内存
原型:
    int shmget(key_t key, size_t size, int shmflg);
参数:
    key:这个共享内存段名字
    size:共享内存⼤⼩
    shmflg:由九个权限标志构成,它们的⽤法和创建⽂件时使⽤的mode模式标志是⼀样的
           取值为IPC_CREAT:共享内存不存在,创建并返回;共享内存已存在,获取并返回。
           取值为IPC_CREAT | IPC_EXCL:共享内存不存在,创建并返回;共享内存已存在,出错返回。
返回值:
    成功返回⼀个⾮负整数,即该共享内存段的标识码;失败返回-1

ftok函数
头文件:
    #include <sys/types.h>
    #include <sys/pic.h>
功能:
    用于生成一个唯一标识的key值。
原型:
    key_t ftok(const char *pathname, int proj_id);
参数:
    使用传入的pathname和proj_id创建一个key值。
返回值:
    创建成功,key值被返回,创建失败返回-1.


注:ftok函数常用于生成shmget函数中的key参数。

        key是共享内存唯一性的标志,这个参数由用户传入,当两个进程要使用同一块共享内存进行通信时,该参数填为一样的值。        

        当shmfig参数的权限标志为IPC_CREAT | IPC_EXCL时,等同于创建一个全新的共享内存。

        当shmfig参数的权限标志只有IPC_CREAT时,用于获取已有的共享内存。

shmat函数
头文件:
    #include <sys/types.h>
    #include <sys/shm.h>

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

        一般情况下 shmaddr 参数为 nullptr,shmflg 参数为 0。

shmdt函数
头文件:
    #include <sys/types.h>
    #include <sys/shm.h>
功能:
    将共享内存段与当前进程脱离
原型:
    int shmdt(const void *shmaddr);
参数:
    shmaddr: 由shmat所返回的指针
返回值:
    成功返回0;失败返回-1

注意:将共享内存段与当前进程脱离不等于删除共享内存段
shmctl函数
头文件:
    #include <sys/types.h>
    #include <sys/shm.h>
功能:
    ⽤于控制或删除共享内存
原型:
    int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数:
    shmid:由shmget返回的共享内存标识码
    cmd:将要采取的动作(有三个可取值)
    buf:指向⼀个保存着共享内存的模式状态和访问权限的数据结构
返回值:
    成功返回0;失败返回-1

         ipcs -m 命令,查看当前系统中共享内存的信息。

        ipcrm -m [shmid] 命名,删除共享内存标识为shmid的共享内存。

5.4 共享内存的实例

        该实例使用共享内存技术进行进程间通信,因为共享内存没有进⾏同步与互斥! 共享内存缺乏访问控制!会带来并发问题,所以使用命名管道对其进程间做一定的访问控制。

//Comm.hpp

#pragma once

#include <cstdio>
#include <stdlib.h>

#define ERR_EXIT(m) \
do \
{ \
    perror(m); \
    exit(EXIT_FAILURE); \
} while(0)
//Fifo.hpp

#pragma once

#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <string>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include "Comm.hpp"

#define PATH "."
#define NAME "fifo"

class NamedFifo
{
public:
    NamedFifo(const std::string path, const std::string name)
        : _path(path), _name(name)
    {
        // 创建管道文件
        _filename = _path + "/" + _name;
        umask(0);
        int n = mkfifo(_filename.c_str(), 0664);
        if (n < 0)
            ERR_EXIT("mkfifo");
        else
        {
            std::cout << "mkfifo success" << std::endl;
        }
    }

    ~NamedFifo()
    {
        int n = unlink(_filename.c_str());
        // 删除成功
        if (n == 0)
        {
            perror("unlink");
            // ERR_EXIT("unlink");
            // 如果使用ERR_EXIT进行退出,服务端会直接调用exit函数进行退出,而不会调用shm的析构函数
        }
        else    // 删除失败
        {
            std::cout << "remove fifo failed" << std::endl;
        }
    }

private:
    std::string _path;
    std::string _name;
    std::string _filename;
};

class FileOperation
{
public:
    FileOperation(const std::string path, const std::string name)
        : _path(path), _name(name), _fd(-1)
    {
        _filename = _path + "/" + _name;
    }

    void OpenForRead()
    {
        _fd = open(_filename.c_str(), O_RDONLY);
        if (_fd < 0)
            ERR_EXIT("open");
        else if (_fd > 0)
        {
            std::cout << "open success" << std::endl;
        }
    }
    void OpenForWrite()
    {
        _fd = open(_filename.c_str(), O_WRONLY);
        if (_fd < 0)
            ERR_EXIT("open");
        else if (_fd > 0)
        {
            std::cout << "open success" << std::endl;
        }
    }
    //  服务端进行读
    void Read()
    {
        char buf[1024];
        int n;
        while (true)
        {
            n = read(_fd, buf, sizeof(buf) - 1);
            buf[n] = 0;
            if (n > 0)
            {
                std::cout << "Client say # " << buf << std::endl;
            }
            else if (n == 0)
            {
                std::cout << "Client quit! me too !" << std::endl;
                break;
            }
            else
            {
                // TODO
            }
        }
    }
    //  客户端进行写
    void Write()
    {
        std::string message;
        while (true)
        {
            std::cin >> message;
            int n = 0;
            write(_fd, message.c_str(), message.size());
        }
    }
    bool Wait()
    {
        char c;
        int number = read(_fd, &c, 1);
        if (number > 0)
        {
            return true;
        }
        return false;
    }
    void Wakeup()
    {
        char c = 'c';
        write(_fd, &c, 1);
    }
    void Close()
    {
        close(_fd);
    }

private:
    std::string _path;
    std::string _name;
    std::string _filename;
    int _fd;
};
//Shm.hpp

#pragma once

#include <iostream>
#include <cstdio>
#include <string>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include "Comm.hpp"
#include <unistd.h>

const int gdefaultid = -1;
const int gsize = 4096;
const std::string pathname = ".";
const int projid = 0x66;
const int gmode = 0666;
#define CREATER "creater"
#define USER "user"

//  描述共享内存的类
class Shm
{
private:
    void CreateHelper(int flg)
    {
        key_t _key = ftok(pathname.c_str(), projid);
        if (_key < 0)
        {
            ERR_EXIT("ftok");
        }
        printf("key: 0x%x\n", _key);
        _shmid = shmget(_key, _size, flg);
        if (_shmid < 0)
        {
            ERR_EXIT("shmget");
        }
        std::cout << "shmid: " << _shmid << std::endl;
    }
    //  创建共享内存资源
    void Create()
    {
        CreateHelper(IPC_CREAT | IPC_EXCL | gmode);
    }
    //  关联共享内存资源
    void Attach()
    {
        _start_mem = shmat(_shmid, nullptr, 0);
        if ((long long)_start_mem < 0)
        {
            ERR_EXIT("shmat");
        }
        std::cout << "attch success" << std::endl;
    }
    //  取消关联
    void Detach()
    {
        int n = shmdt(_start_mem);
        if (n == 0)
        {
            printf("detach success\n");
        }
    }
    //  获取共享内存资源
    void Get()
    {
        CreateHelper(IPC_CREAT);
    }
    //  释放共享内存资源
    void Destroy()
    {
        shmctl(_shmid, IPC_RMID, nullptr);
    }
public:
    // pathname 和 projid 用于构建 key 值,usertype用于标记使用者身份
    Shm(const std::string &pathname, const int projid, const std::string &usertype)
    : _shmid(gdefaultid)
    , _size(gsize)
    , _start_mem(nullptr)
    , _user_type(usertype)
    {
        if (_user_type == CREATER)
        {
            Create();
        }
        else if (_user_type == USER)
        {
            Get();
        }
        else
        {

        }
        Attach();
    }

    // 获取起始虚拟地址
    void* VirtualAddr()
    {
        printf("virtual address: %p\n", _start_mem);
        return _start_mem;
    }

    //  获取共享内存大小
    int size()
    {
        return _size;
    }

    //  获取共享内存信息
    void Attr()
    {
        struct shmid_ds ds;
        int n = shmctl(_shmid, IPC_STAT, &ds);
        printf("shm_segsz: %ld\n", ds.shm_segsz);
        printf("key: 0x%x\n", ds.shm_perm.__key);
    }
    // 取消共享内存的关联对象,如果使用者身份为创建者,则释放共享内存资源
    ~Shm()
    {
        std::cout << _user_type << std::endl;
        if (_user_type == CREATER)
        {
            Destroy();
        }
    }
private:
    int _shmid; //共享内存id号
    key_t _key; //共享内存的key值,标识唯一性
    int _size;  //共享内存的空间大小
    void *_start_mem;   //共享内存的起始虚拟地址
    std::string _user_type; //共享内存的使用者身份
};
//server.cc

#include "Comm.hpp"
#include "Shm.hpp"
#include "Fifo.hpp"

int main()
{
    Shm shm(pathname, projid, CREATER);
    // shm.Attr();
    // shm.VirtualAddr();

    NamedFifo nf(PATH, NAME);
    FileOperation fo(PATH, NAME);
    fo.OpenForRead();

    // 使用命名管道的方式,让数据的读取按照特定的格式读取
    char *mem = (char *)shm.VirtualAddr();
    while (true)
    {
        if (fo.Wait())
        {
            printf("%s\n", mem);
        }
        else
        {
            break;
        }
    }

    fo.Close();
    return 0;
}
//client.cc

#include "Comm.hpp"
#include "Shm.hpp"
#include "Fifo.hpp"

int main()
{
    Shm shm(pathname, projid, USER);

    FileOperation fo(PATH, NAME);
    fo.OpenForWrite();
    char* mem = (char*)shm.VirtualAddr();
    int index = 0;
    for (char c = 'A'; c <= 'Z'; c++, index+=2)
    {
        sleep(1);
        mem[index] = c;
        sleep(1);
        mem[index+1] = c;
        fo.Wakeup();
    }

    fo.Close();
    return 0;
}

         这里对上述代码进行解释,上述代码分为服务端和客户端。当服务端启动的时候,创建共享内存并将共享内存和该进程进行关联,创建命名管道并以读方式打开命名管道,此时服务端进程会阻塞在Wait()函数的read()函数处,等待客户端启动;当客户端启动时,获取和服务端相同的共享内存资源并关联该共享内存,并且以写方式打开命名管道,然后向共享内存中依次写入AA BB CC ...,当同一个字母写了两次后,向命名管道中写入一个标记字符c,唤醒服务端,服务端收到该进程发送的标记字符,结束等待,从共享内存资源中读取数据。实验结果如下:

        当通讯结束之后,先析构命名管道对象,然后调用Shm的析构函数,删除共享内存资源。 

6. system V消息队列(了解即可)

        消息队列:提供了一种一个进程给另一个进程发送有类型数据块的方式。每个数据块都被认为是有一个类型,接收者进程接收的数据块可以有不同的类型值。

7. system V信号量(了解即可)

7.1 并发编程的概念铺垫

        1. 进程间通讯的前提条件:使不同的进程看到同一份资源。

        2. 数据不一致:当不同的进程访问同一份资源的时候,需要有“保护”机制。例如让读端以某一种形式对写端的传输的数据进行读取。如上述的共享内存,当两个进程之间没有“保护”机制的时候,写端一直向共享内存写入数据,而读端没有按照 AA BB..的形式读取,就会造成数据不一致的情况。

        3. 数据不一致产生情况:访问共享资源的代码段没有进行约束或者保护,意思就是两个进程对同一份资源访问的时候没有一定的顺序性。

        4. 临界区:当一份代码中某一段代码会对共享资源进行访问时,该代码段叫做临界区。同理没有对共享资源进行访问的代码段,叫做非临界区。

        5. 临界资源:系统中某些资源一次只允许一个进程访问,称这样的资源为临界资源或互斥资源。

        6. 同步:多个执行流访问临界(共享)资源时,具有一定的顺序性叫做同步。

        7. 互斥:任何一个时刻,至于性能一个执行流访问临界资源,叫做互斥。同步和互斥是最常见的对共享资源进行保护的方式。

        下面是一份代码的示意图,没有对临界资源访问的部分叫做非临界区,对临界资源访问的部分叫做临界区,通过加锁的方式,可以对共享资源进行保护。对共享资源进行保护,本质是对访问共享资源的代码进行保护。

7.2 信号量

         信号量可以理解为一个计数器。当系统中有一份共享资源时,整份资源不进行整体使用,而是进行分块使用。当外界进程进行资源申请的时候,计数器--,称为P操作,当资源被释放时,计数器++,称为V操作。

        信号量的本质是对资源的预定机制,可以类比为订电影票。

        信号量的作用是用于保护临界区。

8. 内核是如何组织管理IPC资源的

        在Linux内核中,System V标准的三种资源 -- 共享内存、消息队列、信号量,是进行统一管理的。

        在内核中,有一个 struct ipc_ids sem_ids 结构体,里面包含了一指针 *entries,指向一个 ipc_id_ary 结构体,而 ipc_id_ary 结构体中维护了一个柔性数组,数组的每一个元素存储着指向上述申请的各种资源的指针。

        问题是,为什么不同的结构体能被相同的指针所指向?原因是,三种共享资源的结构体中,第一个成员对象都是 kern_ipc_perm 结构体,所以在柔性数组中,存放的是 kern_ipc_perm类型的指针,使其能够指向不同类型的结构体。本质上,是C语言的一种多态形式。当需要进行访问将指针强转为对应类型即可进行访问。

进程通信IPC核心机制详解:从管道到共享内存的实战选型指南 进程通信是操作系统实现多进程协同工作的基础技术,它解决了进程间安全高效传递信息与协调工作的核心需求。其原理在于操作系统通过提供标准化的通信接口,在保证进程隔离安全的前提下实现数据交换。这项技术的价值在于支撑了从桌面应用到分布式系统的复杂软件架构,是现代计算不可或缺的基石。在应用场景上,无论是命令行工具协作、客户端/服务器模型,还是高性能数据处理,都需要选择合适的IPC机制。本文聚焦于**共享内存**和**消息队列**等核心机制,深入剖析其实现原理与适用场景,为工程实践提供清晰的选型指南。 阅读详情

相关推荐

Linux进程间通信(IPC)机制详解与实践指南

进程间通信(IPC)是操作系统实现多进程协作的核心技术,通过共享内存、管道、消息队列等机制突破进程隔离限制。从原理上看,IPC本质是内核提供的资源抽象,涉及内存映射、文件描述符传递等底层机制。在分布式系统和高性能计算场景中,合理选择IPC方式能显著提升吞吐量,如共享内存适合高频数据交换(实测速度可达管道10倍),而消息队列则保证结构化通信。现代开发中,Python的multiprocessing模块和Go语言的channel都封装了IPC底层细节,但理解System V与POSIX标准差异仍是解决容器化部署

weixin_30435261的博客 332

Linux进程间通信(IPC)机制全解析:从管道到共享内存的C语言实践

进程间通信是操作系统核心概念,用于协调多个独立进程间的数据交换与同步。其原理基于内核提供的多种IPC机制,每种机制在性能、复杂度与耦合度间进行权衡,技术价值在于实现模块解耦、提升系统并发能力与资源利用率。应用场景广泛,从简单的Shell管道到高并发的服务器架构、微服务通信及系统工具协同工作均依赖于此。本文聚焦Linux环境下主流IPC技术,深入探讨共享内存与消息队列等高性能方案,通过C语言实例详解其实现与同步机制,帮助开发者掌握系统级编程的关键技能。

weixin_30376509的博客 613

Linux进程间通信(IPC)机制全解析:从管道到共享内存的实战指南

进程间通信(IPC)是操作系统和分布式系统的核心基础概念,它解决了独立进程间如何安全高效地交换数据和协同工作的问题。其原理在于操作系统提供的内核级中介机制,使得不同进程的地址空间能够建立可控的数据通道。掌握IPC的技术价值在于能够设计出模块化、高并发且稳定的软件架构,例如微服务、数据库系统、图形界面应用等都深度依赖IPC。在Linux环境下,常见的应用场景包括Shell命令管道、客户端-服务器模型、生产者-消费者任务处理以及高性能数据处理。本文将深入探讨管道、消息队列、共享内存等核心机制,其中共享内存以其近

weixin_34186128的博客 352

Linux系统中进程间通信(Inter-Process Communication, IPC)

操作系统的进程间通信

qq_51536567的博客 1571

深入解析进程间通信:从IPC原理到工业应用实战

进程间通信是操作系统与分布式系统的核心基础,它定义了不同进程间交换数据与协调工作的机制。其原理在于通过共享内存、消息传递或信号等内核提供的原语,绕过低效的网络协议栈,实现本地进程间的直接数据交换。这项技术的核心价值在于为高性能计算、实时系统及模块化应用提供了无可替代的低延迟、高吞吐量通信能力,尤其适用于图形渲染、数据库缓存、工业控制等对性能与确定性要求极高的场景。本文聚焦于IPC 7077等工业协议及共享内存等关键技术,通过分析典型错误如ipc connection error,系统阐述在Linux/Uni

weixin_33700350的博客 340

C++进程通信实战:从管道到共享内存的高性能日志系统设计

进程间通信(IPC)是操作系统与多进程应用开发中的核心基础概念,它解决了独立进程间数据交换与协同工作的根本问题。其原理在于通过操作系统提供的共享资源或内核机制,如内存映射、消息队列或网络套接字,实现进程隔离环境下的信息传递。掌握IPC技术对于构建高性能、高可靠性的分布式或模块化系统具有重要价值,尤其在系统编程、中间件开发和高性能计算等应用场景中不可或缺。本文聚焦于C++语言环境,深入探讨了管道、消息队列、共享内存等主流IPC机制,并重点剖析了如何利用共享内存与信号量构建一个高性能的日志系统,其中涉及环形缓冲

weixin_33898876的博客 481

进程通信(IPC-Inter Process Communication)

进程之间的通信通过内核空间实现①管道(匿名管道/命名管道-FIFO队列) ②(消息队列、信号量和共享内存) ③套接字(UNIX套接字&Internet套接字)

weixin_73223235的博客 1782

C++父子进程通信:从匿名管道到本地套接字的实战指南

进程间通信(IPC)是操作系统为隔离进程提供的核心机制,它解决了独立内存空间之间的数据交换问题。其基本原理是通过操作系统内核作为中介,提供管道、共享内存、套接字等通信原语,实现进程间的协同工作。掌握IPC技术对于构建高性能、模块化的C++应用具有重要价值,尤其在服务器开发、任务并行化和系统工具链等场景中不可或缺。本文将聚焦于父子进程这一特定关系,深入探讨匿名管道的高效单向数据流实现,并解析本地套接字如何提供可靠的全双工通信能力,帮助开发者根据实际需求在简单性、性能和功能完备性之间做出权衡。

weixin_34195142的博客 413

Linux 进程间通信:匿名管道、命名管道、共享内存、消息队列与信号量完全指南

本文总结Linux进程间通信(IPC)的5种主要方式:匿名管道、命名管道、共享内存、消息队列和信号量。重点讲解了匿名管道的原理与使用,包括pipe函数创建管道、fork共享管道、管道读写规则与特性。文章还通过代码示例展示了父子进程通过管道通信的实现方式,并分析了管道在内核中的本质是内存缓冲区+文件描述符。最后介绍了如何用管道构建进程池的进阶应用场景。全文以"Linux一切皆文件"思想贯穿,帮助读者从会用管道到理解其底层原理。

2601_95483298的博客 503

Python数据分析实战:用TIGRAMITE库5步搞定时间序列因果分析(附完整代码)

本文详细介绍了如何使用TIGRAMITE库进行时间序列因果分析,通过5个步骤从环境配置到模型优化,帮助读者掌握因果关系库的应用。附完整代码示例,适合数据分析师和研究人员快速实现复杂时间序列的因果发现。

weixin_29050271的博客 161

基于 Quest 摄像头数据开发的原理介绍【Unity Meta Quest MR 开发教程】

比如想在识别到的现实物体上叠加虚拟物体,需要知道这个现实物体在 Unity 的世界坐标下的哪个位置,是带有 x,y,z 三个轴的数据的。而 WebCamTexture 捕捉到的图像只是个 2D 的平面,没有三维世界的深度信息,我们通过物体识别算法识别到的物体坐标只是在这个 2D 平面上的一个屏幕坐标,可以理解为 Quest 拍摄了一张照片,我们识别到这张照片上的某个像素点为需要识别的现实物体的中心。是不是看起来比较复杂?Quest 的相机访问功能是在安卓的。

qq_46044366的博客 2105

Linux进程间通信(IPC)机制详解:从管道到共享内存的实战选型指南

进程间通信是操作系统实现多进程协同工作的核心技术,它解决了进程间数据交换与同步的难题。其核心原理在于通过操作系统内核提供的多种机制,跨越进程间的内存隔离墙,实现信息的安全高效传递。这项技术的价值在于它是构建复杂软件系统的基石,从简单的命令行工具协作到大型分布式系统、微服务架构都离不开它。在实际应用场景中,开发者需要根据通信关系、数据格式、性能要求等因素,在匿名管道、命名管道、消息队列、共享内存、信号量、信号和域套接字等机制中做出选择。例如,对于需要传递结构化消息并希望具备内核持久化的场景,消息队列是理想选择

weixin_30522095的博客 291

步进电机驱动算法——梯形加减速算法

目录步进电机梯形加减速梯形加减速算法原理分析算法实现 步进电机梯形加减速 电机的控制方式一般分为开环控制与闭环控制两种控制方式,其中开环控制原理框图如下: 这种种控制方式的特点是:控制简单、实现容易、价格较低,这种开环控制方式,负载位置对控制电路没有反馈。 对于步进电机,控制脉冲的输入并不依赖于转子的位置,而是按一固定的规律发出控制脉冲,如果励磁变化太快,电机不能移动到新的位置,那么实际负载位置与理想位置就会产生一个偏差,就有可能出现失步或者过冲现象,所以需要使用加减速算法。 梯形加减速算法,S加减速算法等

aaa376070331的博客 3万+

【Linux系统】进程间通信(补充 struct file 的知识)

管道通信(匿名管道 和 命名管道详解)、System V进程间通信(共享内存详解) 以及 介绍内核是如何组织管理system V资源的(补充:共享内存映射到进程虚拟地址空间的底层实现)

无限进步!!! 1351

蓝桥杯STM32G431RBT6学习——GPIO

蓝桥杯STM32G431RBT6学习——GPIO

weixin_44098974的博客 2235

ESP32编译卡在‘Cannot establish a connection to the component registry’?别急着重装,先试试这两个国内镜像配置

本文详细解析了ESP32编译过程中常见的‘Cannot establish a connection to the component registry’报错问题,并提供了国内镜像源的配置方案。通过对比Gitee、清华大学、阿里云和中科大等镜像源的性能,帮助开发者快速解决网络连接问题,提升编译效率。文章还包含手把手的配置教程和进阶技巧,适用于ESP-IDF开发环境的优化。

weixin_34088048的博客 341

进程通信与信号:从原理到实践,一图掌握IPC核心机制

进程通信(IPC)是操作系统和并发编程的基石,它解决了进程间数据交换、资源共享和事件通知等核心问题。其基本原理在于,由于进程拥有独立的地址空间,无法直接访问彼此数据,因此需要操作系统提供内核中介或内存映射等机制来实现通信。从管道、消息队列到共享内存,不同IPC机制在通信效率、同步需求和进程关系上各有侧重,共同构成了一个完整的技术光谱。理解这些机制的设计初衷与底层关联,对于构建高性能、健壮的分布式系统或并发应用具有关键价值。在实际的工程实践中,例如高并发服务器或分布式计算场景,开发者需要根据数据量、延迟和同步

weixin_34209851的博客 437

PCL 快速均匀下采样

快速均匀下采样 是一种点云降采样技术,通过设置采样步长来实现等距降采样,从而减少点的数量。用户可以通过指定步长,选择每隔一定数量的点采样一个点,这样可以在保持点云几何特性的同时,显著减少点的数量。

纵马踏花向自由 871

一文搞懂 Linux 进程间通信:从管道到共享内存,再到 Socket

在 Linux 中,每个进程都有自己的地址空间。简单理解,一个进程中的变量,另一个进程不能直接访问。fill:#333;important;important;fill:none;color:#333;color:#333;important;fill:none;fill:#333;height:1em;进程 A独立地址空间进程 B独立地址空间Linux 内核提供 IPC 机制进程 B 不能直接通过变量名访问这个num。

2501_91839423的博客 817

Vue - 萤石云监控 ezuikit 视频实例销毁方案,解决使用stop方法无法销毁EZUIKit实例或销毁后依然占用内存并且监控画面依然有声音问题(解决页面多个监控摄像头画面同时播放,翻页或关闭页

vue2,vue3,萤石,监控摄像头,萤石云销毁视频实例,退出页面销毁,stop销毁原视频流,占用内存怎么办,销毁后视频依旧在后台播放有声音,销毁视频播放器,页面上有多个视频画面怎么销毁某一个,stop方法不生效怎么办,浏览器非常卡,dom销毁,ezuikit-js萤石云播放器Vue中使用beforeDestory销毁播放器实例,调用萤石云ezuikit-js的视频对象销毁,vue使用萤石云监控视频录像回放,不销毁实例会出现离开页面后依旧有监控声音并且占内存问题,不好使,页面崩了,崩溃,视频监控太多,没有用

🏆 前端领域优质创作者 1万+

【史上最全面esp32教程】网络篇和tcp/ip的使用

ESP32史上最全面wifi和tcp/ip连接及示例程序

m0_62599305的博客 9526

linux笔记归纳9:进程间通信

示例:进程A写"aaa",进程B写“bbb”,读端只会读到“aaabbb”或"bbbaaa",不会"aababba"IPC_CREAT(获取):如果目标共享内存不存在,就创建,否则打开这个存在的共享内存并返回。key值类似命名管道约定同一个管道文件,保证共享内存存在,标识共享内存的唯一性。最后一个进程只有父进程的一个写端,关闭后可以关闭该进程指向其他进程管道的写端。原子写入:多进程同时向同一个管道写数据时,单次写入的数据不会出现穿插和拆分。特性四:管道是单向通信的,子进程写数据,父进程读数据,属于半双工。

yonakaame的博客 265

C# WinForm 基于DirectShow.dll编写的程序,可边预览边保存视频,音频和图片

基于微软的DirectShow.dll的二次开发1.自动识别连接的摄像头跟麦克风且可以对其进行选择(默认选中第一个)2.可录制wmv和avi两种格式的视频并且都带有音频,avi可以录制的分辨率和帧数进行选择,wmv默认进行了淸晰度优化如不喜欢可去除代码中有相关提示3.可在预览点开之后进行拍照,也可边录制视频边拍照4.添加了视频和音频的压缩功能(酌情使用)这次二次开发发现C#对DirectShow的相关信息太少,这次做完也不敢藏私共享出来,还望大家指正

IPC管道

介绍进程间通信IPC,以及匿名管道和应用场景进程池,命名管道原理和应用场景,Client和Server通信以及文件拷贝

m0_74328241的博客 247

Statistics and Machine Learning Toolbox

Statistics and Machine Learning Toolbox(带书签),MATLAB统计学习工具

OV9734 DATASHEET,完整规格书

OV9734完整规格书,包括电气参数,尺寸规格,寄存器配置表等

Linux多线程与多进程通信机制全解析:从同步原语到IPC实战

并发编程是现代软件开发的基石,其核心在于如何让多个执行单元高效、安全地协同工作。从原理上讲,这涉及到对共享资源的访问控制与数据交换机制的设计。线程作为进程内的轻量级执行流,天然共享内存空间,其通信本质是同步问题,通过互斥锁、条件变量、信号量等同步原语来防止数据竞争和死锁,从而保障数据访问的正确性,这是构建高性能、高可靠服务端程序的关键技术。进程作为独立的执行实体,拥有隔离的地址空间,其通信(IPC)则需借助内核提供的管道、消息队列、共享内存等机制在隔离空间间搭建数据桥梁,尤其在微服务、分布式系统等架构中至关

weixin_33850015的博客 505

霍尼韦尔SoftMaster使用说明

霍尼韦尔SoftMaster使用说明

进程间通信:面试必问Linux管道底层原理与进程池设计(含踩坑记录)

即进程间通信,是指在不同进程之间传递数据、同步协作的一套机制。由于每个进程都拥有独立的地址空间,无法直接访问彼此的内存,因此通信必须通过内核中转来实现。让不同的进程看到同一份资源(内存),从而具备通信的条件。这份共享资源由操作系统提供,并需要配套创建、使用、销毁等管理能力。为此,操作系统必须提供一系列系统调用(System Call)来实现进程级的通信能力,这就要求设计出一套统一的通信接口。有了接口,自然需要相应的实现方案,进而催生了各类通信标准。例如经典的System V。

有且仅有质量分95以上的博客 858

进程通信与信号机制全景解析:从原理到实战避坑指南

进程间通信(IPC)是操作系统实现多进程协作与数据共享的核心机制,其本质在于解决独立地址空间进程间的信息传递问题。从底层原理看,IPC机制在性能、复杂度与适用场景间进行权衡,形成了从简单异步通知到复杂数据交换的技术光谱。信号作为一种轻量级软件中断,实现了异步事件通知,而管道、消息队列、共享内存等则提供了不同层级的数据交换能力。在工程实践中,理解共享内存的同步挑战与信号处理的安全限制至关重要,这直接关系到多进程程序的稳定性和性能。掌握这些基础概念与权衡逻辑,有助于开发者在实际场景中合理选择IPC方案,有效规避

weixin_30911451的博客 319
上一篇: C/C++动静态库的制作与原理 -- 静态库,动态库,目标文件,ELF文件,动态链接,静态链接
下一篇: Linux的进程信号 -- 信号产生,信号保存,信号捕捉,硬件中断,内核态和用户态,可重入函数,volatile,SIGCHLD
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值