【Linux】一文掌握嵌入式 Linux 进程间通信(IPC):原理拆解 + 函数实战

 

目录

1.  概述

1.1  Unix Domain Socket (UDS)

1.2  管道

1.2.1  无名管道

1.2.2  有名管道

1.3  共享内存

1.4  消息队列

1.5  信号量

1.6  总结对比

2.  发展(了解一下即可)

2.1  System V

2.2  System V IPC

2.3  POSIX IPC

2.4  总结

3.  Unix Domain Socket (UDS)

4.  管道通信

4.1  无名管道(匿名管道,Pipe)

4.2  有名管道(命名管道,FIFO)

5.  共享内存

5.1  System V IPC

5.1.1  shmget()函数——创建或打开共享内存

5.1.2  shmctl()函数——删除共享内存

5.1.3  shmat()函数——将共享内存段附加到进程的地址空间

5.1.4  shmdt()函数——进程分离共享内存段

5.2  POSIX IPC

5.2.1  shm_open()函数——创建或打开一个共享内存对

5.2.2  shm_unlink()函数——删除共享内存

5.2.3  truncate()和ftruncate()——将文件缩放到指定大小

5.2.4  mmap()函数——将一组设备或者文件映射到内存地址

5.2.5  总结使用

6.  消息队列

6.1  System V

6.1.1  msgget()函数——创建或获取消息队列

6.1.2  msgsnd()函数——发送消息

6.1.3  msgrcv()函数——接收消息

6.1.4  msgctl函数()——控制操作(删除、获取信息等)

6.2  POSIX

6.2.1  mq_open()函数——打开/创建消息队列

6.2.2  mq_timedsend()函数——发送消息

6.2.3  mq_timedreceive()——接收消息

6.2.4  mq_unlink()——删除队列

7.  信号


1.  概述

        进程间通信(IPC)是操作系统和多进程编程中的核心概念。由于每个进程都有自己独立的虚拟地址空间,由一个进程创建的数据对于另一个进程通常是不可见的,因此必须通过这种由操作系统提供的特殊机制来交换数据。

1.1  Unix Domain Socket (UDS)

        这是在同一台主机上进行进程间通信的一种高效方式。虽然名为“Socket”,但它与网络Socket不同,它不需要经过网络协议栈,数据不会出主机,只是将数据从一个进程的缓冲区复制到另一个进程的缓冲区。

其特点:

  • 高性能:比基于网络的TCP/IP Socket要快得多。

  • 支持多种通信模式:流式(SOCK_STREAM,类似TCP,可靠)和数据报式(SOCK_DGRAM,类似UDP,不可靠)。

  • 可以传递文件描述符进程凭证(如PID),这是其他一些IPC机制不具备的强大功能。

  • 面向连接(流式)或无连接(数据报式)。

        主要适用于需要类似网络通信模型(如客户端/服务器模型)但又在本机进行的进程间通信。例如,数据库服务、图形界面程序与后台进程的通信。

1.2  管道

1.2.1  无名管道

        也叫匿名管道,一种最基本的IPC机制,由pipe()系统调用创建。它提供一个单向数据流——一端用于写入,另一端用于读取。

        需要注意,无名管道,是单向通信的,且只能在具有亲缘关系的进程之间使用,通常是在一个父进程fork()出子进程后,父子进程之间进行通信,并且其生命周期随进程的结束而结束。

        主要用于,Shell中的命令管道(ls | grep "test")就是典型的无名管道应用。

1.2.2  有名管道

        也称为命名管道,通过mkfifo命令或系统调用创建。它在文件系统中有一个路径名,就像一个特殊的文件。

        其也是单向通信,和无名管道不同的是,有名管道允许无亲缘关系的进程之间通信,因为任何进程只要知道这个“文件名”,都可以打开它进行读写,并且其生命周期是持久的,直到被显式删除。

        主要适用于,需要在不相关的进程之间进行简单的、单向数据流传输的场景。

1.3  共享内存

        这是最快的IPC方式。它让多个进程将同一块物理内存映射到它们各自的虚拟地址空间。这样,一个进程写入的数据,另一个进程立刻就能看到。

特点:

  • 极高性能:因为数据不需要在进程间复制,而是直接对同一块内存进行操作。

  • 需要同步机制:由于共享内存本身没有提供同步,多个进程同时读写会导致数据混乱。因此必须配合使用信号量、互斥锁等同步机制

  • 生命周期与内核一致,除非显式删除,否则会一直存在。

        主要适用于,对性能要求极高的场景,如大型数据处理、科学计算、图形图像处理等。

1.4  消息队列

        一个由内核维护的消息链表。进程可以向队列中写入消息或从队列中读取消息。每个消息都是一个数据块,并且有特定的类型。

特点:

  • 面向消息,数据有边界。

  • 异步通信:写入者可以写入后立刻返回,不需要等待读取者。

  • 支持优先级:消息可以赋予类型/优先级,读取时可以按类型读取,而不一定是严格的FIFO。

  • 生命周期与内核一致

        主要适用于,需要按特定顺序或优先级处理消息,且不希望进程间有紧密耦合的场景。

1.5  信号量

        它本身不用于传递数据,而是作为一种同步原语,用于协调多个进程(或线程)对共享资源(如共享内存)的访问,防止出现竞态条件。

特点:

  • 是一个计数器,用于控制多个进程对共享资源的访问。

  • 提供P(等待,信号量减一)和V(发送,信号量加一)两种原子操作。

  • 通常与共享内存配合使用。

        主要作为其他IPC机制(尤其是共享内存)的“保镖”,实现进程间的互斥与同步。

1.6  总结对比

机制数据传输亲缘关系要求性能关键特点
无名管道单向字节流必须较低简单,用于父子进程
有名管道单向字节流不需要较低有文件名,用于不相关进程
消息队列有格式的消息不需要中等异步,支持优先级,内核持久
共享内存直接内存访问不需要最高需要同步,数据无需复制
Unix Domain Socket字节流/数据报不需要功能全面,可传递文件描述符
信号量无(仅同步)不需要协调者,用于互斥与同步

2.  发展(了解一下即可)

2.1  System V

        System V(读作System Five)是一种基于UNIX的操作系统版本,最初由AT&T(American TelePhone and Telegraph Company,美国电话电报公司,由Bell TelePhone Company发展而来)开发。它在1983年首次发布,对UNIX操作系统的发展产生了深远的影响。SystemV引入了许多新的特性和标准,后来被许多UNIX系统和类UNIX系统(如Linux)采纳。

2.2  System V IPC

        System V IPC(Inter-Process Communication,进程间通信)是System V操作系统引入的一组进程间通信机制,包括消息队列、信号量和共享内存。这些机制允许不同的进程以一种安全且高效的方式共享数据和同步操作。

  • 消息队列:允许进程以消息的形式交换数据,这些消息存储在队列中,直到它们被接收。
  • 信号量:主要用于进程间的同步,防止多个进程同时访问相同的资源。
  • 共享内存:允许多个进程访问同一块内存区域,提供了一种非常高效的数据共享方式。

        System V IPC是UNIX和类UNIX系统中常用的IPC方法之一,它通过关键字(key)来标识和访问IPC资源。

2.3  POSIX IPC

        POSIX IPC是POSIX标准中的一部分,提供了一种更现代和标准化的进程间通信方式,同样包括消息队列、信号量和共享内存三种方式。

  • 消息队列:类似于System V,但通常具有更简洁的API和更好的错误处理能力。
  • 信号量:提供了更多的功能和更高的性能,支持更大范围的操作。
  • 共享内存:提供了更多的控制和配置选项,以支持更复杂的应用场景。

        POSIX IPC 使用名字(name)作为唯一标识。这些名字通常是以正斜杠(/)开头的字符串,用于唯一地识别资源如消息队列、信号量或共享内存对象

2.4  总结

        System V IPC和POSIX IPC在功能上有所重叠,但它们在实现和API设计上有明显的区别。

        POSIX IPC通常被视为更现代、更标准化的解决方案,提供了更好的跨平台支持和更易于使用的API。

        System V IPC在历史上更早地被大量UNIX系统所采用,因此在一些旧的或特定的环境中仍然非常重要。在选择使用哪种IPC机制时,应考虑应用程序的具体需求、目标系统的支持程度以及开发者的熟悉程度。

        System V IPC和POSIX IPC各自提供了一组API,如果全部介绍未免太过冗长,他们实现的效果是类似的。本文只介绍POSIX IPC提供的API。

3.  Unix Domain Socket (UDS)

        这是在同一台主机上进行进程间通信的一种高效方式。虽然名为“Socket”,但它与网络Socket不同,它不需要经过网络协议栈,数据不会出主机,只是将数据从一个进程的缓冲区复制到另一个进程的缓冲区。

这个回头和网络编程一起,会发到下面博客当中:

嵌入式Linux·应用开发_时光の尘的博客-优快云博客

4.  管道通信

        管道是Linux中最早支持的IPC机制,是一个链接两个进程的连接器,它实际上是在进程间开辟一个固定大小的缓冲区,需要发布信息的进程运行写操作,需要接收信息的进程运行读操作。管道是半双工的,输入输出原则是先入先出FIFO(First In First On)。

        为什么要使用管道呢?举个例子,之前我们在进行fork介绍的时候知道,fork创建的子进程和父进程数据是不互通的,如:

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


int main(int argc, char const *argv[])
{
    pid_t cpid = fork();
    int sum = 0;

    if(cpid < 0)
    {
        perror("fork");
        return 1;
    }
    else if (cpid == 0)
    {
        sum = 1;
        printf("子进程%d此时sum的数据为:%d\n",getpid(),sum);
    }
    else
    {       
        sum = 10;
        sleep(1);
        printf("父进程%d此时sum的数据为:%d\n",getpid(),sum);
    }
    
    return 0;
}

        可以发现,父子进程间各自赋值,并不影响到对方的值,那么如果我们想要进程数据间的传递怎么办呢?

        我么就可以使用管道进程传递,下面我们来试用一下。

4.1  无名管道(匿名管道,Pipe)

        也叫匿名管道,由pipe()系统调用创建。它提供一个单向数据流——一端用于写入,另一端用于读取。只能在具有亲缘关系的进程之间使用,通常是在一个父进程fork()出子进程后,父子进程之间进行通信,并且其生命周期随进程的结束而结束。

        表头文件:

#include <unistd.h>

        函数定义:

/* Create a one-way communication channel (pipe).
   If successful, two file descriptors are stored in PIPEDES;
   bytes written on PIPEDES[1] can be read from PIPEDES[0].
   Returns 0 if successful, -1 if not.  */
extern int pipe (int __pipedes[2]) 

        对于其参数__pipedes[2],可以将其看做文件描述符,有函数填写数据,其中pipedes[0]用于管道的read端,pipedes[1]用于管道的writer端。如果返回值为0表示成功,如果返回值为-1,则表示失败。

        管道的读写使用 read() 函数和 write() 函数,其采用字节流的方式。

  • 读管道时,若管道为空,则会被阻塞,直到管道另一段 write() 函数将数据写入到管道为止,若写段已关闭,则返回 0;
  • 写管道时,若管道已满,则会被阻塞,直到管道另一端 read() 函数将管道内数据读走为止,若读已关闭,则写端返回21,error被设为EPIPE。

        管道关闭用close函数,在创建管道时,写端需要关闭pipedes[0]描述符,读端需要关闭pipedes[1]描述符。当进程关闭前,每个进程需要把没有关闭的描述符都进行关闭。无名管道需要注意以下问题:

  • 管道是半双工方式,数据只能单向传输。如果在两个进程之间相互传送数据,要建立两条管道。
  • 调用 pipe() 函数必须在调用 fork() 函数以前进行,否则子进程将无法继承文件描述符。
  • 使用无名管道互相连接的任意进程必须位于一个相关的进程组中。

        我们来进行一下数据的传递,对上面的代码进行一个简单的修改:

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


int main(int argc, char const *argv[])
{
    int pipefd[2];

    // 创建管道(必须在fork之前)
    if (pipe(pipefd) == -1) {
        perror("pipe");
        return 1;
    }

    pid_t cpid = fork();
    int sum = 0;

    if(cpid < 0)
    {
        perror("fork");
        return 1;
    }
    else if (cpid == 0)
    {
        sum = 1;
        printf("\n读取之前子进程%d此时sum的数据为:%d\n",getpid(),sum);
        printf("子进程开始读取数据\n");

        // 关闭写端
        close(pipefd[1]);
        read(pipefd[0], &sum, sizeof(sum));

        printf("子进程从管道读取的数据:%d\n", sum);

        write(STDOUT_FILENO, &sum, sizeof(sum));        
 
        printf("读取之后子进程%d此时sum的数据为:%d\n",getpid(),sum);

        close(pipefd[0]);
        _exit(EXIT_SUCCESS);
    }
    else
    {       
        sum = 10;

        //关闭读端
        close(pipefd[0]);
        printf("父进程%d此时sum的数据为:%d\n",getpid(),sum);

        //写入传入的参数到管道的写端
        write(pipefd[1],&sum,sizeof(sum));

        //写完之后关闭写端  读端会返回0
        close(pipefd[1]);
        
        // 等待子进程结束
        waitpid(cpid,NULL,0); 
        exit(EXIT_SUCCESS);
    }
    
    return 0;
}

        可以看到此时我们可以将父进程写入的数据,赋值到子进程使用:

        这里其实就是简单的 read() 和 writer() 的调用,不熟悉的可以参考文件IO的介绍:

【Linux应用开发·入门指南】详解文件IO以及文件描述符的使用-优快云博客

使用管道的限制: 

        两个进程通过一个管道只能实现单向通信,比如上面的例子,父进程写子进程读,如果有时候也需要子进程写父进程读,就必须另开一个管道。

        管道的读写端通过打开的文件描述符来传递,因此要通信的两个进程必须从它们的公共祖先那里继承管道文件描述符。上面的例子是父进程把文件描述符传给子进程之后父子进程之间通信,也可以父进程fork两次,把文件描述符传给两个子进程,然后两个子进程之间通信,总之需要通过fork传递文件描述符使两个进程都能访问同一管道,它们才能通信。

4.2  有名管道(命名管道,FIFO)

        上面介绍的Pipe是匿名管道,只能在有父子关系的进程间使用,某些场景下并不能满足需求。与匿名管道相对的是有名管道,在Linux中称为FIFO,即First In First Out,先进先出队列。

        FIFO和Pipe一样,提供了双向进程间通信渠道。但要注意的是,无论是有名管道还是匿名管道,同一条管道只应用于单向通信,否则可能出现通信混乱(进程读到自己发的数据)。

        有名管道可以用于任何进程之间的通信。它和无名管道不同之处在于:命名管道提供了一个路径名与之关联,以FIFO的文件形式存在于文件系统当中,在文件系统当中产生一个物理文件,其他进程只要访问该文件路径,就能彼此通过管道通信。在读数据端以只读方式打开管道文件,在写数据端以只写方式打开管道文件。

执行流程:

mkfifo(创建有名管道)→open(打开管道)→read/writer(读写管道)→close(关闭管道)→unlink(文件移出)

        表头文件:

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

        定义函数:

/* Create a new FIFO named PATH, with permission bits MODE.  */
extern int mkfifo (const char *__path, __mode_t __mode)

/**
 * @brief 用于创建有名管道。该函数可以创建一个路径为pathname的FIFO专用文件,mode指定了FIFO的权限,FIFO的权限和它绑定的文件是一致的。FIFO和pipe唯一的区别在于创建方式的差异。一旦创建了FIFO专用文件,任何进程都可以像操作文件一样打开FIFO,执行读写操作。
 * 
 * @param pathname 有名管道绑定的文件路径,为管道建立的临时文件,文件名在创建管道之前不能存在
 * @param mode 有名管道绑定文件的权限
 * @return int 
 */

命名管道只需要在读写一端创建,不过读写两端都需要知道这个命名管道的存在。例如在写端创建后,就不需要在读端创建。

        我们根据上面流程来写一下写端的代码,首先是创建有名管道,通过mkfifo创建,路径自己取一个,命名管道可以创建在任何你有写权限的目录中,这里我就复制我当前路径了:

    char *pipe_path = "/home/dky/Linux_task/perror_example/example";

    if(mkfifo(pipe_path,0664) != 0)
    {
        perror("mkfifo failed");
        if(errno != 17)
        {
            exit(EXIT_FAILURE);
        }
    }

其中errno 17 含义:17 是 EEXIST,表示文件已存在。

        然后通过open()函数打开管道,默认设置为阻塞模式,不需要使用创建的方法,也不需要设置访问权限,在读端以只读方式打开,在写端以只写方式打开下面我们是写端:

    int fd;

    fd = open(pipe_path, O_WRONLY);
    if (fd == -1)
    {
        perror("open failed");
        exit(EXIT_FAILURE);
    }

        对于读写使用 read() 和 writer(),进行数据读写的时候有两种工作模式:

阻塞模式:

        读取数据时,以只读方式打开,若管道为空,则被阻塞,直到写数据端写入数据为止。读取数据端时,可能有多个进程读取管道,所有的读进程都被阻塞。当有任意一个进程能读取数据时,其他所有进程都被解阻,只不过返回值为 0,数据只能被其中一个进程读走。

        写入数据时,以只写方式打开,若管道已满,则被阻塞,直到读进程将数据读走,管道最大长度为 4096 B,有些操作系统为 512 B.如果写入端是多个进程,当管道满时,Lima保证了写入的原子性,即采用互斥方式实现。

非阻塞模式:

        读取数据时,立即返回,管道没有数据时,返回 0,且 ermo 值为EAGAIN,有数据时,返回实际读取的字节数。

        写入数据时,当要写入的数据量不大于 PIPE_BUF 时,Linux 将保证写入的原子性如果当前 FIFO 空闲缓冲区能够容纳请求写入的字节数,写完后则成功返回;如果当前FIFO 空闲缓冲区不能够容纳请求写入的字节数,则返回 EAGAIN 错误,提醒以后再写。

        写入数据:

    char writer_buf[100];
    ssize_t read_num;

    while ((read_num = read(STDIN_FILENO, writer_buf, 100)) > 0)
    {
        write(fd, writer_buf, read_num);
    }

    if (read_num < 0)
    {
        perror("read");
        printf("命令行数据读取异常,退出");
        close(fd);
        exit(EXIT_FAILURE);
    }

        关闭管道,就是调用close()函数,关闭各自描述符即可:

    close(fd);

        最后避免文件系统中留下无用的管道文件,我们可以调用 unlink() 函数,将刚刚创建的管道清理掉,在写端移除不需要再读端移除:

    unlink(pipe_path);

        完整写函数代码:

#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <sys/stat.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>

int main(int argc, char const *argv[])
{
    int fd;
    char *pipe_path = "/home/dky/Linux_task/perror_example/example";

    if(mkfifo(pipe_path,0664) != 0)
    {
        perror("mkfifo failed");
        if(errno != 17)
        {
            exit(EXIT_FAILURE);
        }
    }

    fd = open(pipe_path, O_WRONLY);
    if (fd == -1)
    {
        perror("open failed");
        exit(EXIT_FAILURE);
    }

    char writer_buf[100];
    ssize_t read_num;

    while ((read_num = read(STDIN_FILENO, writer_buf, 100)) > 0)
    {
        write(fd, writer_buf, read_num);
    }

    if (read_num < 0)
    {
        perror("read");
        printf("命令行数据读取异常,退出");
        close(fd);
        exit(EXIT_FAILURE);
    }

    printf("发送管道退出,进程终止\n");
    close(fd);

    unlink(pipe_path);

    return 0;
}


命名管道只需要在一端移除就可以了,通常是在创建管道的那一端进行移除。

        读函数按照上述思路同理:

#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <sys/stat.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>

int main(int argc, char const *argv[])
{
    int fd;
    char *pipe_path = "/home/dky/Linux_task/perror_example/example";

    fd = open(pipe_path, O_RDWR);
    if (fd == -1)
    {
        perror("open failed");
        exit(EXIT_FAILURE);
    }

    char read_buf[100];
    ssize_t read_num;

    while ((read_num = read(fd, read_buf, 100)) > 0)
    {
        write(STDOUT_FILENO, read_buf, read_num);
    }

    if (read_num < 0)
    {
        perror("read");
        printf("命令行数据读取异常,退出");
        close(fd);
        exit(EXIT_FAILURE);
    }

    printf("发送管道退出,进程终止\n");
    close(fd);

    return 0;
}


        Makefile函数中添加如下代码:

fifo_writer: fifo_writer.c
	$(cc) -o $@ $<

fifo_read: fifo_read.c
	$(cc) -o $@ $<

        运行生成可执行文件:

        开启两个终端,我们来试一下读写操作:

        我们左侧运行写,右侧运行读:

        可以看到当我们运行写后,会出现一个example的管道,我们在写端随意输入一段数据回车看一下:

        可以看到同步更新相同的数据,不过我们结束两个进程后会发现,example并没有被删除:

        主要问题,阻塞的 read() 调用,read(STDIN_FILENO, writer_buf, 100) 会一直等待标准输入,程序无法正常退出,除非收到 EOF 或错误,否则一直阻塞在 read(),unlink() 无法执行,程序卡在 read(),永远不会执行到清理代码。

        而如果我们在其中输入数据,read() 的阻塞解除,数据正常返回,注意我们此时想要退出输入ctrl+c就会出现上述删除不掉的情况。

        此时我们需要执行Ctrl+D正常退出才可以:

5.  共享内存

        共享内存是嵌入式 Linux 系统中一种非常重要且高效的进程间通信机制。它允许多个不相关的进程访问同一块物理内存区域,从而实现大规模数据的快速共享。

        其核心思想是,在物理内存中开辟一块区域,映射到多个进程各自的虚拟地址空间。这样,一个进程写入的数据,另一个进程立刻就能看到,无需任何形式的数据拷贝。

         共享内存是存在于内核级别的一种资源,在shell中可以使用ipcs命令来查看当前系统 IPC 中的状态,在文件系统/proc 目录下有对其描述的相应文件:

        共享内存的实现方式有两种标准:System V IPC 和 POSIX IPC。

5.1  System V IPC

        这是比较传统但广泛使用的方法。关键步骤:

  • ftok:生成一个唯一的键值。
  • shmget:根据键值创建或获取共享内存段。可以指定大小和权限。
  • shmat:将共享内存段“附加”到当前进程的地址空间,返回其虚拟地址。
  • 读写操作:通过 shm_addr 指针像操作普通内存一样进行读写。
  • shmdt:进程分离共享内存段。
  • shmctl:用于控制共享内存段,例如删除它(IPC_RMID)。

5.1.1  shmget()函数——创建或打开共享内存

        创建或打开一块共享内存区。

        表头文件:

#include<sys/shm.h>

        函数定义:

/* Get shared memory segment.  */
extern int shmget (key_t __key, size_t __size, int __shmflg) __THROW;
  • key:用于唯一标识共享内存段的键值;其有三种创建方式:
    ①使用 IPC_PRIVATE(推荐用于亲缘进程)
    ②使用 ftok() 生成键值(推荐用于无亲缘进程)
    ③使用固定整数值
  • size:请求的共享内存段大小(字节),如果是创建新段,必须指定 size > 0,如果是获取已存在的段,size 被忽略(但必须 ≤ 已存在段的大小),实际分配的大小会被向上取整到系统页大小的整数倍;
  • shmflg:控制共享内存的创建和访问权限,通过位或 | 组合,其可选标志位如下。
标志位值(十六进制)说明
IPC_CREAT0x200创建新段或获取已存在段
IPC_EXCL0x400与 IPC_CREAT 配合,确保创建新段
SHM_HUGETLB0x4000使用大页内存
SHM_NORESERVE0x1000不预留交换空间
SHM_HUGE_2MB(21 << 26)2MB 大页
SHM_HUGE_1GB(30 << 26)1GB 大页
S_IRUSR0x100用户读权限
S_IWUSR0x080用户写权限
S_IRGRP0x020组读权限
S_IWGRP0x010组写权限
S_IROTH0x004其他用户读权限
S_IWOTH0x002其他用户写权限

        函数的返回值,如果成功则返回内存IP,如果失败则返回-1。

        下面我们来使用shmget()函数来创建一块共享内存,这里我们使用IPC_PRIVATE进行创建:

#include<sys/shm.h>
#include<sys/ipc.h>
#include<sys/types.h>
#include<stdio.h>
#include<stdlib.h>

#define BUFF 4096

int main(int argc, char const *argv[])
{
    system("ipcs -m");//查看当前共享内存

    //共享内存标识符
    int shm_id;
    shm_id = shmget(IPC_PRIVATE,BUFF,IPC_CREAT | 0666);

    if(shm_id < 0)//创建共享内存失败
    {
        perror("shmget");
        exit(1);
    }

    printf("成功创建共享内存:%d\n",shm_id);
    system("ipcs -m");//查看当前共享内存

    return 0;
}

        编写Makefile:

cc := gcc

shmget_test : shmget_test.c
	-$(cc) -o $@ $^
	-./$@
	-rm ./$@

        可以看到创建了一个4096的共享内存:

        对于使用ftok()生成键值,部分代码如下:

#include <sys/ipc.h>

key_t key = ftok("/some/existing/file", 'A');
shm_id = shmget(key, BUFF, IPC_CREAT | 0666);

        对于使用固定数值(需要确保选择的键值不会与其他应用冲突):

#define MY_SHM_KEY 0x1234
// 或
#define MY_SHM_KEY 5678

shm_id = shmget(MY_SHM_KEY, BUFF, IPC_CREAT | 0666);
方法优点缺点适用场景
IPC_PRIVATE永不冲突,简单安全只能亲缘进程使用父子进程通信
ftok()标准方法,无亲缘关系可用依赖文件存在性任意进程通信
固定值完全控制,性能好可能冲突,需要协调内部系统,测试

5.1.2  shmctl()函数——删除共享内存

        共享内存与消息队列以及信号量相同,在使用完毕后都应该进行释放,另外,当调用fork(函数创建子进程时,子进程会继承父进程已绑定的共享内存;当调用exec函数更改子进程功能以及调用exit()函数时,子进程中都会解除与共享内存的映射关系,因此在必要时仍应使用shmctl0函数对共享内存进行删除。

        表头文件:

#include <sys/shm.h>

        函数定义:

/* The following System V style IPC functions implement a shared memory
   facility.  The definition is found in XPG4.2.  */

/* Shared memory control operation.  */
#ifndef __USE_TIME_BITS64
extern int shmctl (int __shmid, int __cmd, struct shmid_ds *__buf) __THROW;
#else
  • shmid:共享内存标识符,指定要操作的共享内存段,其来源由 shmget() 函数返回的正整数;
  • cmd:控制命令,指定要执行的操作类型;
  • buf:缓冲区指针,根据不同的 cmd,此参数有不同的用途。

        其中对于cmd的一些类型:

命令参数类型作用返回值
IPC_STATstruct shmid_ds *获取段状态0/-1
IPC_SETstruct shmid_ds *设置段参数0/-1
IPC_RMIDNULL标记删除段0/-1
IPC_INFOstruct shminfo *获取系统限制最大索引/-1
SHM_INFOstruct shm_info *获取使用统计最大索引/-1
SHM_STATstruct shmid_ds *通过索引获取信息shmid/-1
SHM_LOCKNULL锁定内存0/-1
SHM_UNLOCKNULL解除锁定0/-1

        我们创建在删除:

#include <stdio.h>
#include <stdlib.h>
#include <sys/shm.h>

int main() 
{
    int shmid = shmget(IPC_PRIVATE, 4096, IPC_CREAT | 0666);
    
    printf("创建共享内存: %d\n", shmid);
    system("ipcs -m");
    
    // 删除共享内存
    if (shmctl(shmid, IPC_RMID, NULL) == -1) {
        perror("shmctl");
        exit(1);
    }
    
    printf("已标记删除共享内存\n");
    system("ipcs -m");
    
    return 0;
}

        可以看到上面创建了32829,下面给删除了:

        我们也可以直接输入相应的键值进行删除,如我们想要删除上方的32811,直接输进去即可:

#include <stdio.h>
#include <stdlib.h>
#include <sys/shm.h>

int main() 
{
    system("ipcs -m");
    
    // 删除共享内存
    if (shmctl(32811, IPC_RMID, NULL) == -1) {
        perror("shmctl");
        exit(1);
    }
    
    printf("已标记删除共享内存\n");
    system("ipcs -m");
    
    return 0;
}

5.1.3  shmat()函数——将共享内存段附加到进程的地址空间

        将共享内存段“附加”到当前进程的地址空间,返回其虚拟地址。

        表头文件:

#include <sys/shm.h>

        函数定义:

/* Attach shared memory segment.  */
extern void *shmat (int __shmid, const void *__shmaddr, int __shmflg)
  • shmid:共享内存段标识符,必须是已存在的共享内存段 ID,由 shmget() 函数返回的有效标识符;
  • shmaddr:指定期望的附加地址,NULL(推荐)让内核自动选择可用地址,或者尝试在指定地址附加;
  • shmflg:控制附加行为的标志位,常用标志位如下。
标志说明
00默认,读写方式附加
SHM_RDONLY010000只读方式附加
SHM_RND020000自动对齐 shmaddr
SHM_REMAP040000强制重新映射(Linux 扩展)
SHM_EXEC0100000内存可执行(Linux 扩展)

        其返回值,若是成功则返回实际引入的地址,如果失败,则返回-1。

        编写代码:

#include<sys/shm.h>
#include<sys/ipc.h>
#include<sys/types.h>
#include<stdio.h>
#include<stdlib.h>

int main() 
{
    int shmid = shmget(IPC_PRIVATE, 4096, IPC_CREAT | 0666);
    
    printf("创建共享内存: %d\n", shmid);
    system("ipcs -m");

    char* shm_addr = shmat(shmid, NULL, 0);
    if (shm_addr == (void *)-1) {
        perror("shmat");
        exit(EXIT_FAILURE);
    }
    printf("共享内存附加成功 at: %p\n", shm_addr);
    system("ipcs -m");
    
    // 删除共享内存
    if (shmctl(shmid, IPC_RMID, NULL) == -1) {
        perror("shmctl");
        exit(1);
    }
    
    printf("已标记删除共享内存\n");
    system("ipcs -m");
    
    return 0;
}

        我们首先成功创建 shmid=65554,其连接数为0,状态正常,而后成功附加到地址0x7dd226f2d000,此时的连接数从0变为1,当我们删除65554后,其状态变为"目标",连接数仍为1(因为进程还未分离),当整个工程结束,我们通过 ipcs 查看可以发现已经删除过了:

5.1.4  shmdt()函数——进程分离共享内存段

        头文件:

#include <sys/shm.h>

        函数原型:

/* Detach shared memory segment.  */
extern int shmdt (const void *__shmaddr) __THROW;

        shmaddr:要分离的共享内存地址指针,必须是 shmat() 函数返回的有效地址,必须是当前进程已附加的共享内存地址。

        成功返回 0,失败返回-1。

        基础用法,往共享内存中写入数据:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/shm.h>

int main() {
    int shmid;
    char *shm_addr;
    
    // 创建共享内存
    shmid = shmget(IPC_PRIVATE, 1024, IPC_CREAT | 0666);
    if (shmid == -1) {
        perror("shmget");
        exit(EXIT_FAILURE);
    }
    
    printf("创建共享内存成功: shmid=%d\n", shmid);
    
    // 附加共享内存
    shm_addr = shmat(shmid, NULL, 0);
    if (shm_addr == (void *)-1) {
        perror("shmat");
        exit(EXIT_FAILURE);
    }
    
    printf("共享内存附加成功 at: %p\n", shm_addr);
    
    // 使用共享内存
    strcpy(shm_addr, "Hello, Shared Memory!");
    printf("写入的数据: %s\n", shm_addr);
    
    // 分离共享内存 - 核心调用
    if (shmdt(shm_addr) == -1) {
        perror("shmdt");
        exit(EXIT_FAILURE);
    }
    
    printf("共享内存分离成功\n");
    
    // 删除共享内存段
    shmctl(shmid, IPC_RMID, NULL);
    
    return 0;
}

5.2  POSIX IPC

        这是更现代、符合 POSIX 标准的方法,推荐在新项目中使用,关键步骤:

  • shm_open:使用一个名字(如 /my_shm)来创建或打开一个共享内存对象。它返回一个文件描述符。
  • ftruncate:设置共享内存对象的大小。
  • mmap:将共享内存对象映射到进程的地址空间。
  • 读写操作:通过 shm_addr 指针进行。
  • munmap:解除内存映射。
  • close:关闭文件描述符。
  • shm_unlink:删除共享内存对象(当所有进程都关闭后,内核会释放资源)。

5.2.1  shm_open()函数——创建或打开一个共享内存对

        头文件:

#include <sys/mman.h>

        函数原型:

/* Open shared memory segment.  */
extern int shm_open (const char *__name, int __oflag, mode_t __mode);
  • name:共享内存对象的名字,其名字长度有限制(通常最多 255 字符),格式应该以 / 开头,如 "/my_shm";
  • oflag:打开标志,控制创建和打开行为;
O_RDONLY    // 只读
O_RDWR      // 读写
O_CREAT     // 如果不存在则创建
O_EXCL      // 与 O_CREAT 一起使用,如果已存在则失败
O_TRUNC     // 如果已存在,将其截断为0长度
  • mode:权限模式(当创建新对象时使用),八进制权限,类似文件权限。

5.2.2  shm_unlink()函数——删除共享内存

        删除一个先前由 shm_open() 创建的命名共享内存对象。尽管这个函数被称为“unlink”,但它并没有真正删除共享内存段本身,而是移除了与共享内存对象关联的名称,使得通过该名称无法再打开共享内存。当所有已打开该共享内存段的进程关闭它们的描述符后,系统才会真正释放共享内存资源。

        头文件:

#include <sys/mman.h>

        函数原型:

/* Remove shared memory segment.  */
extern int shm_unlink (const char *__name);
  • name: 要删除的共享内存对象名称

5.2.3  truncate()和ftruncate()——将文件缩放到指定大小

        truncate和ftruncate都可以将文件缩放到指定大小,二者的行为类似:如果文件被缩小,截断部分的数据丢失,如果文件空间被放大,扩展的部分均为\0字符。缩放前后文件的偏移量不会更改。缩放成功返回0,失败返回-1。

        不同的是,前者需要指定路径,而后者需要提供文件描述符;ftruncate缩放的文件描述符可以是通过shm_open()开启的内存对象,而truncate缩放的文件必须是文件系统已存在文件,若文件不存在或没有权限则会失败。

#include <unistd.h>
#include <sys/types.h>

/**
 * 将指定文件扩展或截取到指定大小
 * 
 * char *path: 文件名 指定存在的文件即可 不需要打开
 * off_t length: 指定长度 单位字节
 * return: int 成功 0
 *             失败 -1
 */
int truncate(const char *path, off_t length);
/**
 *  将指定文件描述符扩展或截取到指定大小
 * 
 * int fd: 文件描述符 需要打开并且有写权限
 * off_t length: 指定长度 单位字节
 * return: int 成功 0
 *             失败 -1
 */
int ftruncate(int fd, off_t length);

5.2.4  mmap()函数——将一组设备或者文件映射到内存地址

        mmap系统调用可以将一组设备或者文件映射到内存地址,我们在内存中寻址就相当于在读取这个文件指定地址的数据。父进程在创建一个内存共享对象并将其映射到内存区后,子进程可以正常读写该内存区,并且父进程也能看到更改。使用man 2 mmap查看该系统调用声明:

#include <sys/mman.h>

/**
 * 将文件映射到内存区域,进程可以直接对内存区域进行读写操作,就像操作普通内存一样,但实际上是对文件或设备进行读写,从而实现高效的 I/O 操作
 * 
 * void *addr: 指向期望映射的内存起始地址的指针,通常设为 NULL,让系统选择合适的地址
 * size_t length: 要映射的内存区域的长度,以字节为单位
 * int prot: 内存映射区域的保护标志,可以是以下标志的组合
 *          (1) PROT_READ: 允许读取映射区域
 *          (2) PROT_WRITE: 允许写入映射区域
 *          (3) PROT_EXEC: 允许执行映射区域
 *          (4) PROT_NONE: 页面不可访问
 * int flags:映射选项标志
 *          (1) MAP_SHARED: 映射区域是共享的,对映射区域的修改会影响文件和其他映射到同一区域的进程(一般使用共享)
 *          (2) MAP_PRIVATE: 映射区域是私有的,对映射区域的修改不会影响原始文件,对文件的修改会被暂时保存在一个私有副本中
 *          (3) MAP_ANONYMOUS: 创建一个匿名映射,不与任何文件关联
 *          (4) MAP_FIXED: 强制映射到指定的地址,如果不允许映射,将返回错误
 * int fd: 文件描述符,用于指定要映射的文件或设备,如果是匿名映射,则传入无效的文件描述符(例如-1)
 * off_t offset: 从文件开头的偏移量,映射开始的位置
 * return void*: (1) 成功时,返回映射区域的起始地址,可以像操作普通内存那样使用这个地址进行读写
 *               (2) 如果出错,返回 (void *) -1,并且设置 errno 变量来表示错误原因
 */
void *mmap(void *addr, size_t length, int prot, int flags,
           int fd, off_t offset);
/**
 * 用于取消之前通过 mmap() 函数建立的内存映射关系
 * 
 * void *addr: 这是指向之前通过 mmap() 映射的内存区域的起始地址的指针,这个地址必须是有效的,并且必须是 mmap() 返回的有效映射地址
 * size_t length: 这是要解除映射的内存区域的大小(以字节为单位),它必须与之前通过 mmap() 映射的大小一致
 * return: int 成功 0
 *             失败 -1
 */
int munmap(void *addr, size_t length);

5.2.5  总结使用

        简单来说就是父进程读取子进程在共享内存写入的数据:

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

int main()
{
    char *share;
    pid_t pid;
    char shmName[100] = {0};
    
    sprintf(shmName, "/letter%d", getpid());
    
    // 共享内存对象的文件标识符
    int fd;
    fd = shm_open(shmName, O_CREAT | O_RDWR, 0644);
    if (fd < 0)
    {
        perror("共享内存对象开启失败!\n");
        exit(EXIT_FAILURE);
    }
    
    // 将该区域扩充为100字节长度
    ftruncate(fd, 100);
    
    // 以读写方式映射该区域到内存,并开启父子共享标签 偏移量选择0从头开始
    share = mmap(NULL, 100, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    
    // 注意:不是p == NULL 映射失败返回的是((void *) -1)
    if (share == MAP_FAILED)
    { 
        perror("共享内存对象映射到内存失败!\n");
        exit(EXIT_FAILURE);
    }
    
    // 映射区建立完毕,关闭读取连接 注意不是删除
    close(fd);
    
    // 创建子进程
    pid = fork(); 
    
    if (pid == 0)
    {
        // 子进程写入数据作为回信 
        strcpy(share, "我收到信了!\n");
        printf("子进程%d完成回信!\n", getpid());
    }
    else
    {
        // 等待回信
        sleep(1);
        printf("父进程%d看到子进程%d回信的内容: %s", getpid(), pid, share);
        
        // 等到子进程运行结束
        wait(NULL);
        
        // 释放映射区
        int ret = munmap(share, 100); 
        if (ret == -1)
        {
            perror("munmap");
            exit(EXIT_FAILURE);
        }
    }
    
    // 删除共享内存对象
    shm_unlink(shmName);
    
    return 0;
}

6.  消息队列

        早期通信机制之一的信号能够传送的信息量有限,管道则只能传送无格式的字节流,这无疑会给应用程序开发带来不便。消息队列则克服了这些缺点。消息队列的实质就是一个存放消息的链表,该链表由内核维护。可以把消息看作一个记录,具有特定的将式。一些进程可以向其中按照一定的规则添加新消息;另一些进程则可以从消息队列中读取消息。

        目前主要有两种类型的消息队列: POSIX消息队列以及System V消息队列,System V消息队列目前被大量使用,该消息队列是随内核持续的,只有在内核重启或者人工删除时该消息队列才会被删除。消息队列的内核持续性要求每个消息队列都在系统范围内对应一的键值,所以,要获得一个消息队列的描述字,必须提供该消息队列的键值。

        消息队列就是消息的一个链表,它允许一个或者多个进程向它写消息,或一个或多个进程向它读消息。在内核中以队列的方式管理,队列先进先出,是线性表。消息信息写在结构体中,并送到内核中,由内核管理。

        消息的发送不是同步机制,而是先发送到内核,只要消息没有被清除,则另一个程序无论何时打开都可以读取消息。消息可以用在同一程序之间(多个文件之间的信息传递)也可以用在不同进程之间。消息结构体必须自己定义,并按系统的要求定义。

struct msgbuf//结构体的名称自己定义
{
    long mtype;//必须是long,变量名必须是mtype
    char mdata[256];//必须是char,数组名和数组长度自己定义
};

        常见使用生产者-消费者模型,我们可以把消息队列想象成一个邮局快递柜:

  • 发送者(进程/线程)把数据打包成一个“消息”,然后放入队列中,无需等待接收者立即处理,就可以继续去做其他事情。

  • 接收者(进程/线程)在方便的时候,从队列中取出消息进行处理。如果队列为空,接收者可以选择等待(阻塞)或立即返回。

特点:

异步通信:发送和接收不需要同步进行,发送者发送完即可返回,降低了进程间的耦合度。

消息有边界:读取操作总是以一条完整的消息为单位,不会出现半条消息的情况。这区别于管道和套接字中的字节流模型。

有优先级:Linux 消息队列支持给消息赋予优先级,允许高优先级的消息被优先处理。

内核持久性:消息队列存在于内核中,其生命周期与创建它的进程无关。只有当系统重启或显式地删除队列时,队列才会被销毁。

多对多通信:多个进程可以向同一个队列写入消息,多个进程也可以从同一个队列读取消息。

        其实现方式有两种标准:System V IPC 和 POSIX IPC。

6.1  System V

        这是 Linux 中传统的消息队列实现,使用一套标准的 System V IPC(进程间通信)函数。

6.1.1  msgget()函数——创建或获取消息队列

        头文件:

#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/msg.h>

        函数原型:

/* Get messages queue.  */
extern int msgget (key_t __key, int __msgflg) __THROW;
  • key:键值,有 ftok() 函数获得,通常为一个整数,若键值为IPC_PRIVATE,则会创建一个只能被创建消息列表的进程读写的消息队列。
  • msgflg:标志位,用于设置消息队列的创建方式或权限,通常有一个9为的权限与以下值进行位操作后获得:
    ①IPC_CREAT:若内核中不存在指定消息队列,该函数会创建一个消息队列;若内核中已存在执行消息队列,则获取该消息队列。
    ②IPC_EXCL:与IPC_CREAT一起使用,表示如果创建的消息队列已存在,则返回错误。
    ③IPC_NOWAIT:读写消息队列要求无法得到满足时,不阻塞。

        返回值,如果成功则返回消息队列标识符(非负整数),否则为-1。

        创建一个消息队列并打印其ID:

#include <stdio.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <errno.h>

int main() 
{
    key_t key;
    int msqid;

    // 1. 生成一个键值
    key = ftok("/tmp", 'A');
    if (key == -1) 
    {
        perror("ftok");
        exit(1);
    }

    // 2. 尝试创建消息队列,如果已存在则失败
    msqid = msgget(key, IPC_CREAT | IPC_EXCL | 0666);
    if (msqid == -1) 
    {
        // 如果失败是因为队列已存在,那我们尝试获取它
        if (errno == EEXIST) 
        {
            printf("消息队列已存在,正在获取它...\n");
            msqid = msgget(key, 0666); // 只获取,不创建
            if (msqid == -1) 
            {
                perror("msgget (get existing)");
                exit(1);
            }
        }
        else 
        {
            // 其他错误
            perror("msgget (create)");
            exit(1);
        }
    } 
    else 
    {
        printf("消息队列创建成功\n");
    }

    printf("消息队列ID: %d\n", msqid);

    // ... 这里可以使用 msqid 进行 msgsnd, msgrcv 等操作 ...

    return 0;
}

        Makefile函数如下:

cc := gcc

system_test : system_test.c
	-$(cc) -o $@ $^
	-./$@
	-rm ./$@

        运行两次,可以看到一次是创建,一次是获取:

6.1.2  msgsnd()函数——发送消息

        头文件:

#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/msg.h>

        函数原型:

/* Send message to message queue.

   This function is a cancellation point and therefore not marked with
   __THROW.  */
extern int msgsnd (int __msqid, const void *__msgp, size_t __msgsz,
		   int __msgflg);
  • msqid:消息队列标识符,指定要向哪个消息队列发送消息,由 msgget 函数返回;
  • msgp:指向要发送消息的指针,第一个字段必须是 long mtype(消息类型),后面跟着实际的数据内容,消息类型必须是一个大于 0 的整数,如:
struct msgbuf {
    long mtype;       // 消息类型,必须 > 0
    char mtext[1];    // 消息数据,可以是任意类型
};

// 或者更实用的定义:
struct my_message {
    long mtype;
    char text[100];
    int  number;
    // 可以添加更多字段...
};
  • msgsz:消息数据部分的大小(字节数),需要注意的是这个大小不包括 mtype 字段的大小,只计算 mtype 之后的数据部分;
  • msgflg:控制发送行为的标志位,常用标志:
    ①0:阻塞模式。如果队列已满,调用进程会阻塞(睡眠),直到有空间可用。
    ②IPC_NOWAIT:非阻塞模式。如果队列已满,函数立即返回 -1,并设置 errno 为 EAGAIN。

        返回值,成功返回0,失败返回-1。

        我们重新创建一个用于发送的.c文件,当做生产者:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <errno.h>

// 定义消息结构
struct message 
{
    long mtype;          // 消息类型,必须 > 0
    char mtext[100];     // 消息内容
};

int main() 
{
    key_t key;
    int msqid;
    struct message msg;

    // 1. 生成一个键值
    key = ftok("/tmp", 'A');
    if (key == -1) 
    {
        perror("ftok");
        exit(1);
    }

    // 2. 尝试创建消息队列,如果已存在则失败
    msqid = msgget(key, IPC_CREAT | IPC_EXCL | 0666);
    if (msqid == -1) 
    {
        // 如果失败是因为队列已存在,那我们尝试获取它
        if (errno == EEXIST) 
        {
            printf("消息队列已存在,正在获取它...\n");
            msqid = msgget(key, 0666); // 只获取,不创建
            if (msqid == -1) 
            {
                perror("msgget (get existing)");
                exit(1);
            }
        }
        else 
        {
            // 其他错误
            perror("msgget (create)");
            exit(1);
        }
    } 
    else 
    {
        printf("消息队列创建成功\n");
    }

    printf("消息队列ID: %d\n", msqid);

    // 3. 准备并发送消息
    msg.mtype = 1;  // 设置消息类型为1
    
    printf("请输入要发送的消息: ");
    fgets(msg.mtext, sizeof(msg.mtext), stdin);
    
    // 移除换行符
    msg.mtext[strcspn(msg.mtext, "\n")] = '\0';

    // 发送消息(不阻塞方式)
    if (msgsnd(msqid, &msg, strlen(msg.mtext) + 1, IPC_NOWAIT) == -1) 
    {
        perror("msgsnd");
        
        // 如果是因为队列满,可以尝试阻塞方式
        if (errno == EAGAIN) 
        {
            printf("消息队列已满,正在以阻塞方式重新发送...\n");
            if (msgsnd(msqid, &msg, strlen(msg.mtext) + 1, 0) == -1) 
            {
                perror("msgsnd (blocking)");
                exit(1);
            }
        }
        else 
        {
            exit(1);
        }
    }

    printf("消息发送成功: %s\n", msg.mtext);
    printf("消息大小: %zu 字节\n", strlen(msg.mtext) + 1);    

    return 0;
}

        我们现在将数据发送出来了,那么怎么查看呢?我们可以通过接收函数查看。

6.1.3  msgrcv()函数——接收消息

        头文件:

#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/msg.h>

        函数原型:

/* Receive message from message queue.

   This function is a cancellation point and therefore not marked with
   __THROW.  */
extern ssize_t msgrcv (int __msqid, void *__msgp, size_t __msgsz,
		       long int __msgtyp, int __msgflg);
  • msqid:消息队列标识符,指定要从哪个消息队列接收消息,由 msgget 函数返回;
  • msgp:指向接收消息缓冲区的指针,缓冲区必须足够大以容纳消息,第一个字段必须是 long mtype;
  • msgsz:接收缓冲区中数据部分的最大容量(字节数),这个大小不包括 mtype 字段的大小,只计算可用于存储数据部分的空间;
  • msgtyp:指定要接收的消息类型,这是 msgrcv 最强大的特性之一:
msgtyp 值行为描述
0读取队列中的第一条消息(不管什么类型)
> 0读取队列中第一条类型等于 msgtyp 的消息
< 0读取队列中类型值小于等于 |msgtyp| 的消息中类型值最小的第一条
  • msgflg:控制接收行为的标志位,常用标志:
    ①0:阻塞模式。如果队列中没有符合条件的消息,调用进程会阻塞;
    ②IPC_NOWAIT:非阻塞模式。如果没有消息,立即返回 -1,设置 errno 为 ENOMSG;
    ③MSG_NOERROR:如果消息数据实际长度大于 msgsz,则截断消息而不报错。如果没有这个标志,会返回 E2BIG 错误;
    ④MSG_EXCEPT(Linux特有):当 msgtyp > 0 时,接收第一条类型不等于 msgtyp 的消息。

        返回值,成功返回实际拷贝到 mtext 字段的字节数,失败返回-1。

        编写消费者,进行接收生产者发送的数据:

#include <stdio.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <errno.h>

// 定义消息结构(必须与发送端一致)
struct message {
    long mtype;
    char mtext[100];
};

int main() 
{
    key_t key;
    int msqid;
    struct message msg;

    // 1. 生成相同的键值
    key = ftok("/tmp", 'A');
    if (key == -1) 
    {
        perror("ftok");
        exit(1);
    }

    // 2. 获取消息队列
    msqid = msgget(key, 0666);
    if (msqid == -1) 
    {
        perror("msgget");
        exit(1);
    }

    printf("等待接收消息...\n");

    // 3. 接收消息(阻塞方式)
    // 参数说明:消息队列ID, 消息缓冲区, 消息数据大小, 消息类型, 标志位
    if (msgrcv(msqid, &msg, sizeof(msg.mtext), 1, 0) == -1) 
    {
        perror("msgrcv");
        exit(1);
    }

    printf("收到消息: %s\n", msg.mtext);
    printf("消息类型: %ld\n", msg.mtype);

    return 0;
}

        运行一下看一下:

6.1.4  msgctl函数()——控制操作(删除、获取信息等)

        头文件:

#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/msg.h>

        函数原型:

extern int msgctl (int __msqid, int __cmd, struct msqid_ds *__buf) __THROW;
  • msqid:消息队列标识符,指定要操作的消息队列,由 msgget 函数返回;
  • cmd:控制命令,指定要执行的操作;
命令描述
IPC_RMID0立即删除消息队列
IPC_STAT1获取消息队列的状态信息
IPC_SET2设置消息队列的参数
  • buf:指向 msqid_ds 结构体的指针。
    ①IPC_STAT:用于存储获取到的状态信息;
    ②IPC_SET:提供要设置的新参数;
    ③IPC_RMID:忽略(通常为 NULL)。

6.2  POSIX

6.2.1  mq_open()函数——打开/创建消息队列

#include <fcntl.h>    /* For O_* constants */
#include <sys/stat.h> /* For mode constants */
#include <mqueue.h>

/**
 * @brief 创建或打开一个已存在的POSIX消息队列,消息队列是通过名称唯一标识的。
 *
 * @param name 消息队列的名称
 * 命名规则:必须是以正斜杠/开头,以\0结尾的字符串,中间可以包含若干字符,但不能有正斜杠
 * @param oflag 指定消息队列的控制权限,必须也只能包含以下三者之一
 * O_RDONLY 打开的消息队列只用于接收消息
 * O_WRONLY 打开的消息队列只用于发送消息
 * O_RDWR 打开的消息队列可以用于收发消息
 * 可以与以下选项中的0至多个或操作之后作为oflag
 * O_CLOEXEC 设置close-on-exec标记,这个标记表示执行exec时关闭文件描述符
 * O_CREAT 当文件描述符不存在时创建它,如果指定了这一标记,需要额外提供mode和attr参数
 * O_EXCL 创建一个当前进程独占的消息队列,要同时指定O_CREAT,要求创建的消息队列不存在,否则将会失败,并提示错误EEXIST
 * O_NONBLOCK 以非阻塞模式打开消息队列,如果设置了这个选项,在默认情况下收发消息发生阻塞时,会转而失败,并提示错误EAGAIN
 * @param mode 每个消息队列在mqueue文件系统对应一个文件,mode是用来指定消息队列对应文件的权限的
 * @param attr 属性信息,如果为NULL,则队列以默认属性创建

* @return mqd_t 成功则返回消息队列描述符,失败则返回(mqd_t)-1,同时设置errno以指明错误原因
*/
mqd_t mq_open(const char *name, int oflag, mode_t mode, struct mq_attr *attr);

/**
 * @brief 当oflag没有包含O_CREAT时方可调用
 *
 * @param name 同上
 * @param oflag 同上
 * @return mqd_t 同上
 */
mqd_t mq_open(const char *name, int oflag);

6.2.2  mq_timedsend()函数——发送消息

#include <time.h>
#include <mqueue.h>

/**
 * @brief 将msg_ptr指向的消息追加到消息队列描述符mqdes指向的消息队列的尾部。如果消息队列已满,默认情况下,调用阻塞直至有充足的空间允许新的消息入队,或者达到abs_timeout指定的等待时间节点,或者调用被信号处理函数打断。需要注意的是,正如上文提到的,如果在mq_open时指定了O_NONBLOCK标记,则转而失败,并返回错误EAGAIN。
 * 
 * @param mqdes 消息队列描述符
 * @param msg_ptr 指向消息的指针
 * @param msg_len msg_ptr指向的消息长度,不能超过队列的mq_msgsize属性指定的队列最大容量,长度为0的消息是被允许的
 * @param msg_prio 一个非负整数,指定了消息的优先级,消息队列中的数据是按照优先级降序排列的,如果新旧消息的优先级相同,则新的消息排在后面。
 * @param abs_timeout 指向struct timespec类型的对象,指定了阻塞等待的最晚时间。如果消息队列已满,且abs_timeout指定的时间节点已过期,则调用立即返回。
 * @return int 成功返回0,失败返回-1,同时设置errno以指明错误原因。
 */
int mq_timedsend(mqd_t mqdes, const char *msg_ptr, size_t msg_len, unsigned int msg_prio, const struct timespec *abs_timeout);

6.2.3  mq_timedreceive()——接收消息

#include <time.h>
#include <mqueue.h>

/**
 * @brief 从消息队列中取走最早入队且权限最高的消息,将其放入msg_ptr指向的缓存中。如果消息队列为空,默认情况下调用阻塞,此时的行为与mq_timedsend同理。
 * 
 * @param mqdes 消息队列描述符
 * @param msg_ptr 接收消息的缓存
 * @param msg_len msg_ptr指向的缓存区的大小,必须大于等于mq_msgsize属性指定的队列单条消息最大字节数
 * @param msg_prio 如果不为NULL,则用于接收接收到的消息的优先级 
 * @param abs_timeout 阻塞时等待的最晚时间节点,同mq_timedsend
 * @return ssize_t 成功则返回接收到的消息的字节数,失败返回-1,并设置errno指明错误原因
 */
ssize_t mq_timedreceive(mqd_t mqdes, char *msg_ptr, size_t msg_len, unsigned int *msg_prio, const struct timespec *abs_timeout);

6.2.4  mq_unlink()——删除队列

#include <mqueue.h>

/**
 * @brief 清除name对应的消息队列,mqueue文件系统中的对应文件被立即清除。消息队列本身的清除必须等待所有指向该消息队列的描述符全部关闭之后才会发生。
 * 
 * @param name 消息队列名称
 * @return int 成功返回0,失败返回-1,并设置errno指明错误原因
 */
int mq_unlink(const char *name);

7.  信号

        在Linux中,信号是一种用于通知进程发生了某种事件的机制。信号可以由内核、其他进程或者通过命令行工具发送给目标进程。Linux系统中有多种信号,每种信号都用一个唯一的整数值来表示,如果想查看所有的Linux信号,请执行kill -l指令,会得到以下反馈:

        每种信号都有其特定的含义和行为,进程可以通过注册信号处理函数来捕获信号并执行相应的操作,例如终止进程、忽略信号或者执行特定的处理逻辑。常见的信号包括:

  • SIGINT(2):这是当用户在终端按下Ctrl+C时发送给前台进程的信号,通常用于请求进程终止。
  • SIGKILL(9):这是一种强制终止进程的信号,它会立即终止目标进程,且不能被捕获或忽略。
  • SIGTERM(15):这是一种用于请求进程终止的信号,通常由系统管理员或其他进程发送给目标进程。
  • SIGUSR1(10)和SIGUSR2(12):这两个信号是用户自定义的信号,可以由应用程序使用。
  • SIGSEGV(11):这是一种表示进程非法内存访问的信号,通常是由于进程尝试访问未分配的内存或者试图执行非法指令而导致的。
  • SIGALRM(14):这是一个定时器信号,通常用于在一定时间间隔后向目标进程发送信号。

        我们可以通过signal系统调用注册信号处理函数:

#include <signal.h>

// 信号处理函数声明
typedef void (*sighandler_t)(int);

/**
 *  signal系统调用会注册某一信号对应的处理函数。如果注册成功,当进程收到这一信号时,将不会调用默认的处理函数,而是调用这里的自定义函数
 * 
 * int signum: 要处理的信号
 * sighandler_t handler: 当收到对应的signum信号时,要调用的函数
 * return: sighandler_t 返回之前的信号处理函数,如果错误会返回SEG_ERR
 */
sighandler_t signal(int signum, sighandler_t handler);

        举个简单的例子,用户在终端按下Ctrl+C时发送给前台进程的信号,通常用于请求进程终止:

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

void sigint_handler(int sig) 
{
    printf("\n收到 SIGINT 信号,正在优雅退出...\n");
    // 清理资源
    exit(0);
}

int main() 
{
    signal(SIGINT, sigint_handler);
    
    while(1) 
    {
        printf("运行中... 按 Ctrl+C 中断\n");
        sleep(1);
    }
    return 0;
}

        按下Ctrl+C:

嵌入式Linux_时光の尘的博客-优快云博客

嵌入式Linux·应用开发_时光の尘的博客-优快云博客

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

时光の尘

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

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

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

打赏作者

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

抵扣说明:

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

余额充值