【Linux】进程间通信 —— 管道

进程间通信介绍了其目的,如数据传输、资源共享和事件通知。文章详细讲解了匿名管道的原理,包括其作为内存级文件的特性,以及如何通过`pipe()`系统调用创建和使用匿名管道。同时,提到了命名管道,它是通过`mkfifo()`函数创建的,允许无亲缘关系的进程通信。最后,文章对比了匿名管道和命名管道的区别,主要在于创建和打开方式的不同。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

📕 进程间通信介绍

进程间通信,顾名思义,就是两个进程之间的 “交流” ,我们知道,进程是相互独立的,也就是说,正常情况下,两个进程之间无法传递消息,但是有时候又需要 进程间通信,如下,这是进程间通信的目的。

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

要达到这些目的,就需要使用一些技术来完成进程间通信。以下是三类技术,管道 、System V、POSIX 。本篇文章主要讲解管道的方法。

  • 管道
    • 匿名管道pipe
    • 命名管道
  • System V IPC
    • System V 消息队列
    • System V 共享内存
    • System V 信号量
  • POSIX IPC
    • 消息队列
    • 共享内存
    • 信号量
    • 互斥量
    • 条件变量
    • 读写锁

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

听起来很抽象,可以了解一下管道的原理,就明白了!

📕 匿名管道

原理

如下图,一个进程可以打开多个文件,其 file_struct 里面的文件描述符 0、1、2 分别指向了内存里的 三个 struct file,在文件描述符那篇文章有提到,每一个 struct file 都有对应的缓冲区,是内存级别的。(要理解这里的内容,首先要了解文件描述符哦,可以看一下这篇文章:【Linux】文件描述符

所以,我们可以用操作系统创建管道的系统调用,打开一个文件(下图中绿色部分),分别以 读、写 方式打开(具体原因下文会讲),然后这个打开的文件会对应有 struct file 和 缓冲区。这些都是操作系统的行为,所以即使磁盘上没有对应的文件,也可以这样打开。当不需要使用管道的时候,直接将内存里的对应 struct file 和缓冲区释放即可。

所以,管道就是一个妥妥的内存级的文件!!

请添加图片描述

此时,我们再 fork() ,产生一个子进程,子进程会继承父进程的绝大多数内容,包括 file_struct ,但是子进程并不会创建新的文件,因为这是属于文件系统范畴了, fork() 只是创建子进程。此时,子进程同样地也以读写的方式打开了父进程创建的管道文件
请添加图片描述

然后,由于管道是半双工的,所以要在父子进程里面,一个关闭读端,一个关闭写端,就可以完成进程间通信的基本条件。
比如这里,我需要父进程写入信息,子进程读取父进程的信息,那么就把父进程的读端关闭,子进程的写端关闭,这样子就是父进程写、子进程读!

请添加图片描述

使用

创建匿名管道需要用到 pipe() 系统调用。

头文件:#include <unistd.h>
功能:创建一个匿名管道
原型
int pipe(int fd[2]);
参数
fd:文件描述符数组,其中fd[0]表示读端, fd[1]表示写端
返回值:成功返回0,失败返回错误代码

使用匿名管道需要注意几点,首先是要在创建子进程之前,创建管道。否则子进程无法继承父进程的文件描述符。
其次,不要忘记关闭读端或者写端。

#include <iostream>
#include <unistd.h>
#include <cerrno>
#include <string.h>
#include <stdio.h>

using std::cout;
using std::endl;

int main()
{
    int pipefd[2] = {0};
    // 1.创建管道
    int n = pipe(pipefd); // pipefd[0] 写端   pipefd[1] 读端
    if (n == -1)
    {
        cout << "create pipe error," << errno << strerror(errno) << endl;
        exit(1);
    }
    // cout<<pipefd[0]<<endl<<pipefd[1];

    // 2.创建子进程
    pid_t pid = fork();

    // 子进程
    if (pid == 0)
    {
        // 3.子进程写,那么关闭读端
        close(pipefd[0]);

        // 4.通信
        char buf[128];
        int cnt = 1;
        while (true)
        {
            snprintf(buf, sizeof buf, "我是子进程,cnt:%d ,pid: %d\n", cnt++, getpid());
            write(pipefd[1], buf, strlen(buf));
        }

        close(pipefd[1]); // 管道声明周期随进程,所以不手动关闭,进程结束后也会自动关闭
        exit(1);
    }

    // 父进程

    // 3.父进程读,关闭写端
    close(pipefd[1]);

    // 4.通信
    char buf[128];
    while (true)
    {
        sleep(3);
        int n = read(pipefd[0], buf, sizeof(buf) - 1); // 留出一个位置放\0
        if (n > 0)
        {
            buf[n]='\0';
            cout << "我是父进程,子进程给的信息:" << buf << endl;
        }
    }

    close(pipefd[0]);
    return 0;
}

上面的 写入、读取 代码可以看出,写入速度快,读写数据慢。 写入了很多次,才读取一次。但是下图运行结果,并不是写入一次,就读取一次,而是可以写入多次,然后一次性读取。

从这里可以看出,读写不是强相关的。(这里指的是读写次数的多少)

请添加图片描述

读写规则

  • 如果读端读取了管道内的所有数据,而写端不写入,那么只能等待。
  • 如果写端将管道写满了,读端没有读取数据,那么就无法写入。
  • 如果写端关闭,读端依然打开,在读取完管道内剩余的数据之后,再次读取数据,则 read 返回0。
  • 如果读端关闭,而写端还向管道写入数据,毫无疑问这是没有意义的,操作系统不会运行这样的事情发送。所以,write 操作会产生信号SIGPIPE,进而可能导致write进程退出。

特点

  1. 单向通信(半双工)
  2. 管道的本质是文件,因为 fd 的生命周期随进程,所以管道的生命周期也是随进程的
  3. 管道通信,通常用来 对具有“血缘”关系的进程,进行进程间通信。常用于父子通信 – pipe 打开管道,并不清楚管道的名字(匿名管道)。
  4. 在管道通信中,写入的次数 和 读取的次数是不严格匹配的,读写次数的多少没有强相关,读取是按照字节流来读取的。
  5. 根据上面的读写规则,可以知道,管道具有一定的协同能力,能让 reader 和 writer 按照一定的步骤进行通信——自带同步机制。

📕 命名管道

原理

如下,一个进程打开了磁盘上的一个文件。当另一个进程(和之前的进程没有血缘关系),也打开同一个文件的时候,操作系统不会重新创建一个 struct file 对象,而是直接使用之前的。

那么,此时,两个进程就看到了同一个文件,也就可以进行进程间通信。
但是,如果这个文件是普通文件,那么数据会定期刷新的磁盘里。可是如果我们要进行进程间通信,数据就不应该被刷新到磁盘里面,而是在进程之间进行传输。所以,这就要求创建的文件是一个内存级别的文件,由此诞生了命名管道文件!不需要维护其 datablock,只需要告诉操作系统,这个文件存在!以后 两个进程可以打开这个文件,打开文件就会在内核匹配对应的缓冲区,所以两个进程就看到同一份资源!!

这个原理和上面匿名管道的原理其实是一样的!!
在这里插入图片描述

使用

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

  • $ mkfifo filename

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

  • int mkfifo(const char *filename,mode_t mode);

如下是在程序里面实现命名管道,以及进程间通信。 server 进程和 client 进程进行交互。

comm.hpp 头文件

#pragma once

#include<iostream>
#include<string>

#define NUM 1024

const std::string filename="./fifo";

mode_t mode=0666;

server.cc 文件


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

#include"comm.hpp"

using std::cin;
using std::cout;
using std::endl;

int main()
{
    // 1.创建命名管道
    umask(0);
    int n = mkfifo(filename.c_str(),mode);
    if(n < 0)
    {
        cout<<"create fifo error:"<<errno<<":"<<strerror(errno)<<endl;
        return 1;
    }
    cout<<"create fifo success"<<endl;

    // 2.开启管道文件
    int rfd=open(filename.c_str(),O_RDONLY);
    if(rfd < 0)
    {
        cout<<"open fifo file error:"<<errno<<":"<<strerror(errno)<<endl;
        return 1;
    }
    cout<<"open fifo file success"<<endl;

    // 3. 开始通信
    char buf[NUM];

    while(true)
    {
        // 先将缓冲区置0
        buf[0]=0;
        size_t n=read(rfd,buf,sizeof(buf)-1); // 读取是按字节流,所以去掉 \0
        if(n > 0)
        {
            buf[n]='\0';
            cout<<"client#"<<buf<<endl;
            fflush(stdout);
        }
        else if(n==0)
        {
            cout<<"client quit,i will quit either"<<endl;
            break;
        }
        else{
            cout<<errno<<":"<<strerror(errno)<<endl;
        }
    }

    close(rfd);
    unlink(filename.c_str());

     return 0;
}

client.cc 文件


#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <cerrno>
#include <cstring>
#include <unistd.h>
#include<cassert>

#include "comm.hpp"

using std::cin;
using std::cout;
using std::endl;

int main()
{
    // 打开fifo
    int wfd=open(filename.c_str(),O_WRONLY);
    if(wfd < 0)
    {
        cout<<"open fifo error"<<errno<<":"<<strerror(errno)<<endl;
        return 1;
    }
    cout<<"open fifo success"<<endl;

    // 写入
    char buf[NUM];
    while(true)
    {
        cout<<"请输入你的信息#";
        char* msg=fgets(buf,sizeof(buf),stdin); // C库的函数,会默认加上 \0
        assert(msg);
        (void*)msg;

        size_t n=write(wfd,buf,sizeof(buf)-1);
        if(strcasecmp(buf,"quit") == 0) break;

    }
    close(wfd);
    return 0;
}

如下,server 是读端, client 是写端,只有 client 写入消息, server 才会读取,实现进程间通信。
如果单单在 server 写入消息, client 是不会读取的,因为 server是读端, client 是写端。
在这里插入图片描述

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

  • 匿名管道由pipe函数创建并打开。
  • 命名管道由mkfifo函数创建,打开用open
  • FIFO(命名管道)与pipe(匿名管道)之间唯一的区别在它们创建与打开的方式不同,一但这些工作完成之后,它们具有相同的语义。
<think>好的,我现在需要帮用户解答关于工业机械臂、伺服电机、六轴机械臂在嵌入式MCU层面的面试考点和解答。首先,用户之前已经询问过PID控制器的输出结果,可能是在准备相关职位的面试,比如嵌入式系统工程师或者控制算法工程师。现在他转向更具体的应用场景,涉及机械臂和伺服电机的嵌入式开发。 首先,用户的问题涉及三个方面:工业机械臂、伺服电机、六轴机械臂,在嵌入式MCU层面的面试考点。我需要从这几个方面分别分析可能的考点,并提供解答思路。因为用户之前问的是PID,可能面试中会涉及到控制算法,比如PID在伺服电机中的应用,以及机械臂的运动控制。 接下来,我需要考虑嵌入式MCU的具体知识点。MCU层面可能涉及实时操作系统(RTOS)、中断处理、PWM生成、编码器接口、通信协议(如CANSPI、I2C)、电机驱动、运动学算法、多轴同步控制等。同时,机械臂的六轴结构可能涉及到逆运动学、轨迹规划、插补算法等。 然后,考虑用户可能的背景。他可能在准备面试,需要具体的考点和解答,所以需要结构清晰,分点列出,每个考点下给出解答示例。此外,用户可能希望了解常见面试问题以及如何回答,比如如何处理多轴同步、如何实现高精度控制、如何优化实时性能等。 还要注意用户提到的嵌入式层面,所以问题可能集中在硬件接口、驱动开发、实时性处理、低层算法实现等方面,而不是上层的应用开发。例如,如何用STM32的定时器生成PWM控制伺服电机,如何处理编码器反馈,如何设计通信协议与上位机交互等。 另外,六轴机械臂涉及较多的运动学计算,可能需要考察逆运动学的实现,如何在资源受限的MCU上高效计算,或者使用何种算法(如雅可比矩阵法、数值解法等)。同时,嵌入式系统中的内存管理、计算优化(如定点数运算代替浮点)也是考点。 伺服电机的控制可能涉及位置环、速度环、电流环的三环控制,以及如何调整PID参数。在MCU上实现这些控制环需要考虑中断频率、计算时间、实时性保证等问题。还可能问到故障保护机制,比如过流检测、堵转处理等。 其他可能的考点包括多任务调度(使用RTOS或裸机)、外设配置(如ADC读取力传感器数据)、通信总线(如CAN总线在工业机械臂中的应用)、EMC设计、低功耗考虑等。 在解答时,需要结合实际例子,比如使用STM32H7系列的高性能MCU,结合FreeRTOS进行任务调度,通过定时器中断实现精确的PWM输出,利用DMA传输数据减少CPU负担,或者使用硬件加速模块进行矩阵运算以提高逆运动学计算速度。 同时,应该注意面试中常问的项目经验问题,比如描述一个你参与过的机械臂控制项目,遇到的技术挑战及如何解决。这可能要求用户结合具体案例,展示问题分析和解决能力。 最后,需要提醒用户注意代码优化、实时性测试、调试方法(如使用逻辑分析仪或示波器排查时序问题),以及相关的行业标准或安全规范。这些都是嵌入式系统开发中不可忽视的部分。</think>以下是针对工业机械臂、伺服电机和六轴机械臂在嵌入式MCU层面的面试考点及解答框架,按技术层级分类解析: --- ### **一、基础硬件层考点** #### 1. 伺服电机驱动原理 **问题**:如何通过MCU实现伺服电WM信号控制? **解答**: - **PWM生成**:使用定时器配置为PWM模式(如STM32的TIMx_CHy),频率通常为1-20kHz,占空比对应电机目标位置。 - **死区控制**:互补PWM输出需配置死区时间(如高级定时器的BDTR寄存器),防止H桥上下管直通。 - **示例代码**: ```c HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1); // 启动PWM通道 __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, pulse_width); // 设置占空比 ``` #### 2. 编码器接口设计 **问题**:如何读取伺服电机的增量式编码器信号? **解答**: - **接口选择**:使用MCU的编码器模式(如STM32的TIMx编码器接口),直接捕获A/B相正交信号。 - **计数值处理**:配置定时器为上下计数模式,通过`TIMx->CNT`读取位置差值。 - **过速保护**:溢出中断处理(如ARR寄存器配置最大计数值)。 --- ### **二、控制算法层考点** #### 1. 多轴同步控制 **问题**:如何保证六轴机械臂的6个关节同步运动? **解答**: - **时间戳同步**:使用硬件定时器触发所有轴的PWM更新(如STM32的TRGO事件)。 - **插补算法**:在MCU中实现直线/圆弧插补,计算各轴步进量(需定点数优化)。 - **示例公式**(直线插补): $$x(t) = x_0 + vt\cdot \cos\theta,\quad y(t) = y_0 + vt\cdot \sin\theta$$ 需离散化为步进脉冲序列。 #### 2. 逆运动学实时计算 **问题**:如何在资源有限的MCU上实现逆运动学解算? **解答**: - **简化模型**:采用几何法替代雅可比矩阵(如PUMA型机械臂的封闭解)。 - **查表法**:预计算常用位姿的关节角,存储为查找表。 - **定点数优化**:使用Q格式(如Q15)代替浮点运算,减少计算耗时。 --- ### **三、实时系统层考点** #### 1. 任务调度设计 **问题**:如何设计嵌入式系统的实时任务优先级? **解答**: - **关键任务分级**: - 最高级:电机控制中断(<100μs响应) - 中级:轨迹规划(1ms周期) - 低级:通信处理(10ms周期) - **实现方案**:使用RTOS(如FreeRTOS)或裸机状态机。 #### 2. 中断冲突处理 **问题**:当PWM中断与编码器中断同时发生时,如何避免数据错误? **解答**: - **优先级配置**:编码器中断设为最高优先级(如NVIC_PriorityGroup_4)。 - **双缓冲机制**:在中断中更新"影子寄存器",主循环中同步实际控制量。 --- ### **四、调试与优化考点** #### 1. 控制环路延迟分析 **问题**:如何测量PID控制环的执行时间? **解答**: - **GPIO翻转法**:在控制环开始/结束时翻转引脚,用示波器测量脉冲宽度。 - **定时器计数**:在中断入口/出口读取`DWT->CYCCNT`(CPU周期计数器)。 #### 2. 抗干扰设计 **问题**:工业现场电磁干扰导致电机抖动,如何解决? **解答**: - **硬件层面**:增加磁环、优化PCB布局(模拟/数字地分割)。 - **软件层面**:对编码器信号进行数字滤波(如滑动平均滤波)。 --- ### **五、典型面试问题示例** **Q**: 机械臂运动时出现末端抖动,可能是什么原因? **A**: 1. **控制环路问题**:PID参数不当(如微分增益不足) 2. **机械共振**:需加入低通滤波器或Notch滤波器 3. **通信延迟**:CAN总线负载过高导致控制指令不同步 4. **电源干扰**:电机启停引起电压波动,需增加去耦电容 **Q**: 如何验证六轴机械臂的定位精度? **A**: 1. **激光跟踪仪**测量末端实际坐标 2. **重复性测试**:多次执行同一路径,统计偏差 3. **反向间隙补偿**:在固件中预存齿轮间隙补偿值 --- ### **六、加分项准备** 1. **行业协议**:熟悉EtherCAT/CANopen协议栈移植经验 2. **安全规范**:了解IEC 61508功能安全标准 3. **开源项目**:参考RT-Thread或ROS2-MicroXRCE的机械臂案例 通过上述分层解析,可系统覆盖从硬件驱动到运动控制的嵌入式MCU核心技术点,建议结合具体项目经验展开回答。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

努力努力再努力.xx

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值