首页 > 系统相关 >操作系统:进程间通信方式详解(下:消息队列、信号量、共享内存、套接字)

操作系统:进程间通信方式详解(下:消息队列、信号量、共享内存、套接字)

时间:2024-09-18 11:49:01浏览次数:8  
标签:信号量 队列 间通信 消息 进程 接字 共享内存

每日一问:操作系统:进程间通信方式详解(下:消息队列、信号量、共享内存、套接字)

进程间通信(Inter-Process Communication,IPC)是操作系统中实现不同进程之间数据交换和协作的关键机制。本文详细介绍了几种常用的 IPC 方式,包括消息队列、信号量、共享内存和套接字。每种通信方式都有其独特的应用场景和优势,而现有的介绍往往局限于概念的介绍,本文则结合实际应用,通过极为详细的可以运行的 C++ 和 Java 示例代码,帮助读者理解这些机制的实现原理和应用场景。


文章目录


本文深入讲解了消息队列、信号量、共享内存和套接字的定义、特点及实际应用,结合代码示例展示了这些 IPC 方式在进程间数据传输与同步中的应用。文章适合对进程间通信感兴趣的初学者和开发人员,通过示例代码掌握 IPC 机制的具体实现。


一、进程间通信概述

进程是操作系统的基本执行单位,每个进程有独立的内存空间。由于这种独立性,进程之间无法直接访问对方的数据,进程间通信(IPC)机制因此应运而生。常见的 IPC 方式包括无名管道、有名管道、消息队列、信号量、共享内存和套接字等。这些方式在数据传输效率、同步机制和复杂度上各不相同,适用于不同的应用场景。

二、消息队列(Message Queue)

2.1 消息队列的定义与特点

消息队列是一种基于消息传递的通信机制,允许进程通过消息队列发送和接收消息。消息队列支持异步通信,发送方和接收方不需要同时工作。消息队列的特点是消息可以按优先级和顺序存储,便于进程之间有序交换数据。

2.2 消息队列的C++示例代码

以下是一个消息队列的 C++ 示例代码,通过 msggetmsgsndmsgrcv 系统调用创建和操作消息队列:

#include <iostream>  // 标准输入输出库
#include <sys/ipc.h>  // IPC 机制相关函数
#include <sys/msg.h>  // 消息队列相关函数
#include <cstring>  // 字符串操作库

// 定义消息结构体
struct msg_buffer {
    long msg_type;  // 消息类型,必须为正整数
    char msg_text[100];  // 消息内容
};

int main() {
    key_t key;
    int msgid;
    msg_buffer message;

    // 使用 ftok 生成消息队列的唯一键
    key = ftok("progfile", 65);  // "progfile" 文件名,65 是一个任意数值

    // 使用 msgget 创建消息队列,如果不存在则创建,权限设置为 0666
    msgid = msgget(key, 0666 | IPC_CREAT);
    message.msg_type = 1;  // 设置消息类型为 1

    // 写入消息到消息队列
    std::cout << "Write Message: ";
    std::cin.getline(message.msg_text, sizeof(message.msg_text));  // 从控制台读取消息
    msgsnd(msgid, &message, sizeof(message), 0);  // 发送消息到队列

    // 读取消息队列
    msgrcv(msgid, &message, sizeof(message), 1, 0);  // 接收消息类型为 1 的消息
    std::cout << "Received Message: " << message.msg_text << std::endl;  // 打印接收到的消息

    // 删除消息队列
    msgctl(msgid, IPC_RMID, NULL);  // 删除消息队列,清理资源

    return 0;
}

解释

  1. ftok():生成唯一的键值,用于识别消息队列。
  2. msgget():创建一个新的消息队列或获取一个已存在的消息队列。
  3. msgsnd():向消息队列发送消息。
  4. msgrcv():从消息队列接收消息。
  5. msgctl():控制消息队列,如删除队列。

2.3 消息队列的Java示例代码

Java 没有直接的消息队列实现,可以通过 BlockingQueue 类进行模拟:

import java.util.concurrent.BlockingQueue;  // 导入 BlockingQueue 接口,用于实现阻塞队列
import java.util.concurrent.LinkedBlockingQueue;  // 导入 LinkedBlockingQueue 类,实现线程安全的阻塞队列

public class MessageQueueExample {
    // 创建一个阻塞队列用于模拟消息队列
    private static BlockingQueue<String> queue = new LinkedBlockingQueue<>();

    public static void main(String[] args) throws InterruptedException {
        // 创建发送线程
        Thread sender = new Thread(() -> {
            try {
                queue.put("Hello from sender!");  // 向队列中放入消息
            } catch (InterruptedException e) {
                e.printStackTrace();  // 捕获并打印异常
            }
        });

        // 创建接收线程
        Thread receiver = new Thread(() -> {
            try {
                String message = queue.take();  // 从队列中取出消息
                System.out.println("Received: " + message);  // 输出接收到的消息
            } catch (InterruptedException e) {
                e.printStackTrace();  // 捕获并打印异常
            }
        });

        sender.start();  // 启动发送线程
        receiver.start();  // 启动接收线程

        sender.join();  // 等待发送线程结束
        receiver.join();  // 等待接收线程结束
    }
}

解释

  1. BlockingQueue:Java 中用于线程间通信的阻塞队列,模拟消息队列的异步特性。
  2. put()take():分别用于将消息放入队列和从队列中取出消息,实现发送和接收操作。

三、信号量(Semaphore)

3.1 信号量的定义与特点

信号量是一种用于进程间同步的计数器机制,可以控制多个进程对共享资源的访问。信号量经常与共享内存结合使用,解决并发访问问题,确保资源不会被多个进程同时访问而导致数据冲突。

3.2 信号量的C++示例代码

以下是一个简单的 C++ 信号量示例,演示如何使用信号量控制线程对临界区的访问:

#include <iostream>  // 标准输入输出库
#include <pthread.h>  // POSIX 线程库
#include <semaphore.h>  // 信号量库

sem_t semaphore;  // 定义信号量

// 线程执行的任务函数
void* task(void* arg) {
    sem_wait(&semaphore);  // 尝试获取信号量,信号量值减 1
    std::cout << "Entered critical section" << std::endl;  // 打印消息表示进入临界区
    sem_post(&semaphore);  // 释放信号量,信号量值加 1
    return NULL;
}

int main() {
    pthread_t t1, t2;  // 定义两个线程
    sem_init(&semaphore, 0, 1);  // 初始化信号量,0 表示信号量用于线程间同步,初始值为 1

    // 创建两个线程执行任务
    pthread_create(&t1, NULL, task, NULL);
    pthread_create(&t2, NULL, task, NULL);

    // 等待两个线程执行完毕
    pthread_join(t1, NULL);
    pthread_join(t2, NULL);

    sem_destroy(&semaphore);  // 销毁信号量,释放资源
    return 0;
}

解释

  1. sem_init():初始化信号量,指定信号量初始值。
  2. sem_wait():等待信号量,可进入临界区时信号量值减 1。
  3. sem_post():释放信号量,信号量值加 1。
  4. sem_destroy():销毁信号量,清理资源。

3.3 信号量的Java示例代码

Java 通过 java.util.concurrent.Semaphore 类实现信号量控制:

import java.util.concurrent.Semaphore;  // 导入 Semaphore 类

public class SemaphoreExample {
    private static Semaphore semaphore = new Semaphore(1);  // 创建信号量,初始值为 1

    public static void main(String[] args) {
        // 定义线程任务
        Runnable task = () -> {
            try {
                semaphore.acquire();  // 获取信号量,阻塞直到信号量可用
                System.out.println("Entered critical section");  // 打印进入临

界区的消息
                semaphore.release();  // 释放信号量
            } catch (InterruptedException e) {
                e.printStackTrace();  // 捕获并打印异常
            }
        };

        // 创建并启动两个线程
        Thread t1 = new Thread(task);
        Thread t2 = new Thread(task);

        t1.start();
        t2.start();
    }
}

解释

  1. acquire():尝试获取信号量,信号量不足时阻塞。
  2. release():释放信号量,允许其他线程进入临界区。

四、共享内存(Shared Memory)

4.1 共享内存的定义与特点

共享内存是最直接的进程间通信方式,通过多个进程共享一块内存区域来进行数据交换。共享内存提供了最快的通信速度,但需要借助同步机制来防止数据冲突。

4.2 共享内存的C++示例代码

以下代码展示了共享内存的使用方法,通过 shmgetshmat 系统调用来创建和连接共享内存:

#include <iostream>  // 标准输入输出库
#include <sys/ipc.h>  // IPC 机制相关函数
#include <sys/shm.h>  // 共享内存相关函数
#include <cstring>  // 字符串操作库

int main() {
    // 创建共享内存键值,"shmfile" 是用于生成键值的路径,65 是任意选定的整数
    key_t key = ftok("shmfile", 65);

    // 创建共享内存,大小为 1024 字节,权限为 0666,若不存在则创建
    int shmid = shmget(key, 1024, 0666 | IPC_CREAT);

    // 连接到共享内存,返回一个指向共享内存的指针
    char *str = (char*) shmat(shmid, (void*)0, 0);

    strcpy(str, "Hello Shared Memory!");  // 向共享内存写入数据
    std::cout << "Data written in memory: " << str << std::endl;  // 输出写入的数据

    shmdt(str);  // 断开共享内存连接
    shmctl(shmid, IPC_RMID, NULL);  // 删除共享内存,清理资源

    return 0;
}

解释

  1. shmget():创建共享内存段,指定大小和权限。
  2. shmat():将共享内存附加到进程地址空间,返回指向共享内存的指针。
  3. shmdt():将共享内存从当前进程地址空间分离。
  4. shmctl():控制共享内存,包括删除共享内存段。

4.3 共享内存的Java示例代码

Java 通过 MappedByteBuffer 类实现类似共享内存的功能:

import java.io.RandomAccessFile;  // 导入 RandomAccessFile 类,用于文件读写
import java.nio.MappedByteBuffer;  // 导入 MappedByteBuffer 类,用于内存映射
import java.nio.channels.FileChannel;  // 导入 FileChannel 类,用于文件通道操作

public class SharedMemoryExample {
    public static void main(String[] args) throws Exception {
        // 创建或打开文件 "shared_memory.bin",读写模式
        RandomAccessFile file = new RandomAccessFile("shared_memory.bin", "rw");

        // 将文件映射到内存,映射模式为读写,大小为 1024 字节
        MappedByteBuffer buffer = file.getChannel().map(FileChannel.MapMode.READ_WRITE, 0, 1024);

        buffer.put("Hello Shared Memory!".getBytes());  // 将数据写入内存映射
        buffer.flip();  // 重置缓冲区位置以便读取

        byte[] data = new byte[buffer.remaining()];  // 创建字节数组保存读取的数据
        buffer.get(data);  // 从缓冲区读取数据
        System.out.println("Data read from memory: " + new String(data));  // 输出读取的数据

        file.close();  // 关闭文件
    }
}

解释

  1. MappedByteBuffer:将文件的某一部分映射到内存,允许直接对文件数据进行读写。
  2. map():将文件通道中的数据映射到内存区域。
  3. flip():重置缓冲区位置,以便后续的读取操作。

五、套接字(Socket)

5.1 套接字的定义与特点

套接字(Socket)是一种支持本地和网络通信的进程间通信方式,可以在本地进程间或跨网络的不同计算机之间进行双向通信。套接字支持 TCP(可靠传输)和 UDP(不可靠但高效)两种协议。

5.2 套接字的实现

套接字是通信端点,通过绑定 IP 地址和端口号来进行数据交换。

C++ 示例代码(TCP 套接字通信)
#include <iostream>  // 标准输入输出库
#include <sys/socket.h>  // 套接字库
#include <arpa/inet.h>  // 地址转换库
#include <unistd.h>  // POSIX 操作库

int main() {
    int server_fd, new_socket;  // 定义服务器套接字和新连接套接字
    struct sockaddr_in address;  // 定义地址结构体
    int opt = 1;
    int addrlen = sizeof(address);
    char buffer[1024] = {0};  // 定义缓冲区用于接收数据
    const char *hello = "Hello from server";  // 定义发送给客户端的消息

    // 创建套接字,使用 IPv4 地址族,TCP 流式套接字
    server_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (server_fd == 0) {  // 检查套接字创建是否成功
        perror("socket failed");
        return 1;
    }

    // 设置套接字选项,允许地址和端口重用
    setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));

    // 绑定套接字到指定的 IP 地址和端口
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY;  // 使用本地所有可用的 IP 地址
    address.sin_port = htons(8080);  // 端口号 8080

    if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
        perror("bind failed");
        return 1;
    }

    // 监听端口,最大等待连接数为 3
    if (listen(server_fd, 3) < 0) {
        perror("listen failed");
        return 1;
    }

    // 接受客户端连接
    new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t *)&addrlen);
    if (new_socket < 0) {
        perror("accept failed");
        return 1;
    }

    // 读取客户端消息
    read(new_socket, buffer, 1024);
    std::cout << "Message from client: " << buffer << std::endl;

    // 发送回复给客户端
    send(new_socket, hello, strlen(hello), 0);
    std::cout << "Hello message sent" << std::endl;

    // 关闭套接字
    close(new_socket);
    close(server_fd);
    return 0;
}
Java 示例代码(TCP 套接字通信)
import java.io.*;  // 导入输入输出类
import java.net.*;  // 导入网络类

public class SocketServer {
    public static void main(String[] args) {
        try (ServerSocket serverSocket = new ServerSocket(8080)) {  // 创建服务器套接字绑定到端口 8080
            System.out.println("Server started, waiting for connection...");

            // 等待客户端连接
            Socket socket = serverSocket.accept();
            System.out.println("Client connected.");

            // 创建输入输出流
            BufferedReader input = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            PrintWriter output = new PrintWriter(socket.getOutputStream(), true);

            // 读取客户端消息
            String clientMessage = input.readLine();
            System.out.println("Received from client: " + clientMessage);

            // 回复客户端
            output.println("Hello from server!");

            // 关闭连接
            socket.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

解释

  1. ServerSocket:服务器端套接字,监听指定端口。
  2. accept():等待客户端连接,建立连接后返回客户端的套接字。
  3. BufferedReaderPrintWriter:用于处理输入输出流,读取客户端发送的数据并进行响应。

六、总结

进程间通信(IPC)是操作系统中实现进程间数据交换和同步的关键技术。不同的 IPC 方式在性能、适用场景、易用性上各有特点:

通信方式定义特点应用场景
无名管道单向数据传输,父子进程间简单、只支持亲缘进程父子进程间数据传输
有名管道有名且持久,支持无亲缘进程双向、需文件系统支持任意进程间的数据传输
高级管道通过子进程执行命令并传输数据创建灵活,可执行命令结果执行系统命令,获取输出
消息队列基于消息的通信异步、按优先级排序异步任务处理
信号量计数器机制,控制资源访问同步、解决并发冲突多进程资源访问控制
共享内存共享内存区域快速传输数据高速、需同步机制需高效通信的场景
套接字本地和网络通信支持双向、网络和本地通信网络应用、跨主机进程间通信

下面给出更复杂版本的对比表格:

通信方式数据传输方向是否支持无亲缘关系进程同步与异步速度数据持久性编程复杂性典型应用场景
无名管道单向同步中等不持久父子进程间简单数据传输
有名管道双向同步中等不持久无亲缘关系进程间数据传输
高级管道单向同步中等不持久父子进程间调用系统命令或可执行程序
消息队列单向异步中等不持久多个进程间复杂数据交换
信号量N/A同步N/AN/A进程/线程同步,解决资源争用问题
共享内存双向同步(需同步机制)不持久大量数据的快速读写,需同步控制
套接字双向是(支持网络通信)同步/异步视网络环境而定不持久本地或网络进程间的复杂数据通信

通过正确选择 IPC 机制,开发者可以有效实现进程间的数据交换和同步,提升系统的响应速度和稳定性。根据实际需求,选择合适的进程间通信方式,可以最大限度地提高应用程序的性能和可靠性。

✨ 我是专业牛,一个渴望成为大牛

标签:信号量,队列,间通信,消息,进程,接字,共享内存
From: https://blog.csdn.net/upgrador/article/details/142300470

相关文章

  • 详解IPC(进程间通信)
    进程间通信(IPC,Inter-ProcessCommunication)是指在不同进程之间传递数据或信号的机制。由于进程之间的地址空间是独立的,IPC提供了一种在进程之间进行数据交换的方法。以下是几种常见的IPC机制:1.管道(Pipes)匿名管道匿名管道是单向的通信通道,通常用于具有亲缘关系的进程之间(如......
  • 鸿蒙OS 线程间通信
    鸿蒙OS线程间通信概述在开发过程中,开发者经常需要在当前线程中处理下载任务等较为耗时的操作,但是又不希望当前的线程受到阻塞。此时,就可以使用EventHandler机制。EventHandler是HarmonyOS用于处理线程间通信的一种机制,可以通过[EventRunner]创建新线程,将耗时的操作......
  • Linux下通过命名管道实现进程间通信
    引入上一篇文章介绍了Linux中通过pipe创建匿名管道,并实现父子进程间通信的功能;当时我就提到了Linux中的另一种管道通信方式——命名管道,下面就来详细介绍一下;命名管道什么是命名管道命名管道(NamedPipe),也叫FIFO(FirstInFirstOut),是一种用于进程间通信(IPC)的机制。与匿名管......
  • Linux下使用pipe进行父子进程间通信
    引入之前我们介绍了多进程以及创建进程的函数fork,下面我们将继续深入,讨论一下多进程间的通信问题;pipe管道谈论多进程通信,就离不开pipe(管道),这是一个系统调用,用于在UNIX和类UNIX系统(如Linux)上创建一个管道(pipe),实现进程间通信。它创建了一个双向的通信通道,允许一个进程向另一......
  • PHP使用ipc进程间通信
    que.php<?phpclassMsgQueue{public$queue;publicfunction__construct($queue){$this->queue=$queue;}publicfunctionpush($data,$type=1){$result=msg_send($this->queue,$type,$data);......
  • 进程间通信-管道
    管道管道的由来:不同进程对于同一文件的读写时,进程一对文件读的时候,进程二需要等到进程一读完关闭文件,进程二再打开进行相应的操作;而管道却可以实现多个进程对同一文件边读边写;无名管道PIPE特征没有名字,无法使用open()(可以使用read\write)只能用于亲缘进程(父子进程、兄弟进......
  • 网络套接字编程(二)
    socket常见API创建套接字:(TCP/UDP,客户端+服务器)intsocket(intdomain,inttype,intprotocol);绑定端口号:(TCP/UDP,服务器)intbind(intsockfd,conststructsockaddr*addr,socklen_taddrlen);监听套接字:(TCP,服务器)intlisten(intsockfd,intbacklog);接收请......
  • springcloud间通信的方式
    在SpringCloud中,主要有以下几种通信方式:一、基于HTTP的RESTfulAPI工作原理:这是一种常见的通信方式,各个微服务通过发送HTTP请求来相互调用。服务提供者暴露RESTfulAPI接口,服务消费者通过HTTP客户端(如RestTemplate、Feign等)发送请求。例如,一个订单服务需要......
  • 使用信号量实现限流器:Python 实践指南
    使用信号量实现限流器:Python实践指南在现代应用程序中,限流器(RateLimiter)是一个非常重要的组件。它可以帮助我们控制对资源的访问频率,防止系统过载,确保服务的稳定性。本文将详细介绍如何使用Python中的信号量(Semaphore)来实现一个高效的限流器。什么是限流器?限流器是一......
  • 使用WM_COPYDATA实现进程间通信
    发送端LRESULTcopyData;//copyDataResulthasvaluereturnedbyotherappCWnd*pOtherWnd=CWnd::FindWindow(NULL,_T("窗体名"));CStringstrData;strData.Format(L"%.1lf",tickdata);if(pOtherWnd){ COPYDATASTRUCTcpd;//上面提到的结构体 cpd.......