linux———进程间通信

目录

一、为什么需要进程间通信?

二、管道(Pipe)------最古老的IPC机制

[2.1 匿名管道原理](#2.1 匿名管道原理)

[2.3 命名管道(FIFO)](#2.3 命名管道(FIFO))

三、共享内存------最高效的IPC方式

[3.1 共享内存原理](#3.1 共享内存原理)

[3.2 共享内存+信号量同步实战](#3.2 共享内存+信号量同步实战)


这篇博客将深入剖析进程间通信的主流机制,每种方法均附有详细的C++代码演示与运行结果,与大家一起彻底掌握IPC编程。

一、为什么需要进程间通信?

在操作系统中,每个进程拥有独立的地址空间。这种隔离性保证了进程的安全,但也带来了数据交换的难题。进程间通信(Inter-Process Communication,IPC)正是为解决这一问题而生------让协作的进程能够安全、高效地交换数据。

根据通信双方的位置,IPC分为两类:

  • 同一主机通信:父子进程、非父子进程之间

  • 不同主机通信:通过网络进行(即网络通信)

二、管道(Pipe)------最古老的IPC机制

2.1 匿名管道原理

管道是Linux最基础的IPC方式,本质上是内核维护的一块缓冲区。匿名管道通过pipe()系统调用创建,返回两个文件描述符:fd[0]用于读,fd[1]用于写。

关键特点

  • 半双工:数据只能单向流动

  • 亲缘进程:通常用于父子进程间通信

  • 字节流:没有消息边界,需自行界定

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

int main() {
    int fd[2];          // fd[0]为读端,fd[1]为写端
    pid_t pid;
    char buffer[256];
    const char *message = "Hello from parent process!";
    int bytes_read;
   
    if (pipe(fd) == -1) {
        perror("pipe");
        exit(EXIT_FAILURE);
    }
    printf("[系统] 管道创建成功\n");
    
    pid = fork();
    
    if (pid < 0) {
        perror("fork");
        exit(EXIT_FAILURE);
    }
    
    if (pid == 0) {
        printf("[子进程] PID = %d,开始读取管道...\n", getpid());
        
        close(fd[1]);  // 关闭写端(子进程只读)
        
        bytes_read = read(fd[0], buffer, sizeof(buffer) - 1);
        if (bytes_read > 0) {
            buffer[bytes_read] = '\0';  // 确保字符串结束
            printf("[子进程] 收到消息:%s\n", buffer);
        }
        
        close(fd[0]);  // 读取完毕,关闭读端
        printf("[子进程] 工作完成,退出\n");
        exit(EXIT_SUCCESS);
        
    } else {
        printf("[父进程] PID = %d,准备发送消息...\n", getpid());
        
        close(fd[0]);  // 关闭读端(父进程只写)
        
        sleep(1);  
        write(fd[1], message, strlen(message) + 1);
        printf("[父进程] 消息已发送:%s\n", message);
        
        close(fd[1]);  // 写入完毕,关闭写端
        
        wait(NULL);
        printf("[父进程] 子进程已结束,程序退出\n");
    }
    
    return 0;
}
  1. pipe(fd) 系统调用在内核创建管道,fd[0]fd[1]分别对应读、写端。

  2. fork() 后,子进程继承父进程的文件描述符表,两个进程都能访问该管道。

  3. 父子进程各自关闭不用的端口:父关读、子关写。这是关键步骤 ------如果忘记关闭,read()将永远不会收到EOF。

  4. 子进程的read()会阻塞,直到父进程写入数据或写端被关闭。

2.3 命名管道(FIFO)

匿名管道的局限在于只能用于亲缘进程。命名管道通过文件系统中的一个特殊文件(FIFO文件)来解决此问题,任意进程都可以通过路径名访问

文件A:数据发送端

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>

#define FIFO_PATH "/tmp/my_fifo"

int main() {
    int fd;
    int counter = 0;
    char buffer[256];
    
    // 步骤1:创建命名管道文件
    if (mkfifo(FIFO_PATH, 0666) == -1) {
        perror("mkfifo");
        exit(EXIT_FAILURE);
    }
    printf("[发送端] FIFO文件创建成功:%s\n", FIFO_PATH);
    
    // 步骤2:以只写方式打开管道(会阻塞直到有读端打开)
    printf("[发送端] 等待接收端打开管道...\n");
    fd = open(FIFO_PATH, O_WRONLY);
    if (fd == -1) {
        perror("open");
        unlink(FIFO_PATH);
        exit(EXIT_FAILURE);
    }
    printf("[发送端] 管道已连接,开始发送数据...\n");
    
    // 步骤3:循环写入数据
    while (counter < 5) {
        sprintf(buffer, "Message #%d from writer", ++counter);
        write(fd, buffer, strlen(buffer) + 1);
        printf("[发送端] 已发送:%s\n", buffer);
        sleep(1);
    }
    
    // 步骤4:清理
    close(fd);
    unlink(FIFO_PATH);      // 删除FIFO文件
    printf("[发送端] 传输完成,退出\n");
    
    return 0;
}

文件B:数据接收端

cpp 复制代码
// fifo_reader.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>

#define FIFO_PATH "/tmp/my_fifo"

int main() {
    int fd;
    int bytes_read;
    char buffer[256];
    
    // 步骤1:以只读方式打开管道
    printf("[接收端] 等待发送端打开管道...\n");
    fd = open(FIFO_PATH, O_RDONLY);
    if (fd == -1) {
        perror("open");
        exit(EXIT_FAILURE);
    }
    printf("[接收端] 管道已连接,开始接收数据...\n");
    
    // 步骤2:循环读取数据
    while (1) {
        bytes_read = read(fd, buffer, sizeof(buffer) - 1);
        
        if (bytes_read > 0) {
            buffer[bytes_read] = '\0';
            printf("[接收端] 收到:%s\n", buffer);
        } else if (bytes_read == 0) {
            // 写端关闭,收到EOF
            printf("[接收端] 发送端已关闭连接\n");
            break;
        } else {
            perror("read");
            break;
        }
    }
    
    // 步骤3:清理
    close(fd);
    printf("[接收端] 接收完成,退出\n");
    
    return 0;
}

注意事项

  • 不能在共享文件夹中创建管道文件

  • 打开管道时,读端和写端必须同时打开,否则会阻塞

  • 先关闭读端会导致写端进程收到SIGPIPE信号而终止

  • 先关闭写端会导致读端收到EOF(返回0)

三、共享内存------最高效的IPC方式

3.1 共享内存原理

共享内存允许多个进程将同一块物理内存映射到各自的虚拟地址空间,进程间数据交换无需经过内核缓冲区,速度是所有IPC中最快的。但高速度的代价是:必须配合同步机制使用,否则会产生数据竞争。

3.2 共享内存+信号量同步实战

以下示例使用信号量确保读写操作的时序正确

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/sem.h>
#include <sys/wait.h>

// 信号量联合体
union semun {
    int val;
    struct semid_ds *buf;
    unsigned short *array;
};

// 信号量P操作(申请资源)
void sem_p(int semid) {
    struct sembuf sb = {0, -1, 0};
    semop(semid, &sb, 1);
}

// 信号量V操作(释放资源)
void sem_v(int semid) {
    struct sembuf sb = {0, 1, 0};
    semop(semid, &sb, 1);
}

int main() {
    key_t shm_key = ftok("/tmp", 'S');
    key_t sem_key = ftok("/tmp", 'M');
    int shmid, semid;
    char *shared_memory;
    pid_t pid;
    union semun sem_arg;
    
    shmid = shmget(shm_key, 1024, IPC_CREAT | 0666);
    if (shmid == -1) {
        perror("shmget");
        exit(EXIT_FAILURE);
    }
    printf("[系统] 共享内存创建成功,ID:%d\n", shmid);
    
    semid = semget(sem_key, 1, IPC_CREAT | 0666);
    if (semid == -1) {
        perror("semget");
        exit(EXIT_FAILURE);
    }
    // 信号量初始化为0(表示共享内存初始为空,不可读)
    sem_arg.val = 0;
    semctl(semid, 0, SETVAL, sem_arg);
    printf("[系统] 信号量创建成功,初始值:%d\n", 0);
    
    pid = fork();
    
    if (pid == 0) {
        // 附加共享内存
        shared_memory = (char *)shmat(shmid, NULL, 0);
        if (shared_memory == (char *)-1) {
            perror("shmat");
            exit(EXIT_FAILURE);
        }
        
        // 写入数据
        strcpy(shared_memory, "共享内存中的数据:来自子进程!");
        printf("[子进程] 数据已写入共享内存\n");
        
        // V操作:通知父进程数据可读
        sem_v(semid);
        printf("[子进程] V操作完成,通知父进程\n");
        
        // 分离共享内存
        shmdt(shared_memory);
        exit(EXIT_SUCCESS);
        
    } else {
        // 附加共享内存
        shared_memory = (char *)shmat(shmid, NULL, 0);
        if (shared_memory == (char *)-1) {
            perror("shmat");
            exit(EXIT_FAILURE);
        }
        
        // P操作:等待子进程写入完成
        printf("[父进程] 等待子进程写入数据...\n");
        sem_p(semid);
        printf("[父进程] P操作通过,读取数据\n");
        
        // 读取数据
        printf("[父进程] 共享内存内容:%s\n", shared_memory);
        
        // 分离共享内存
        shmdt(shared_memory);
        
        // 等待子进程结束
        wait(NULL);
        
        shmctl(shmid, IPC_RMID, NULL);
        semctl(semid, 0, IPC_RMID);
        printf("[系统] 共享内存和信号量已删除\n");
    }
    
    return 0;
}

运行机制解析

  1. 信号量初始值为0:表示"数据可读"这一资源的可用数量为0,父进程将阻塞在P操作

  2. 子进程写入后执行V操作:信号量变为1,唤醒阻塞的父进程

  3. 父进程P操作通过:消费信号量(恢复为0),安全读取数据

务必注意 :共享内存不会随进程结束而自动释放,必须显式调用shmctl(shmid, IPC_RMID, NULL)删除

以上就是这篇博客的全部内容啦

相关推荐
深蓝易网1 小时前
工厂目视化实操手册,告别形式主义
运维·网络·数据库·人工智能·汽车
ErizJ1 小时前
CN|腾讯面经总结
网络·计算机网络·面试
X7x51 小时前
广域网技术全解:从 CE/PE/P 设备到 PPP/PPPoE 协议与三厂商配置实战
网络·网络协议·信息与通信·广域网技术
南境十里·墨染春水1 小时前
linux学习进展 mysql视图详解
linux·学习·mysql
jimy11 小时前
Oracle的VM.Standard.E2.1.Micro虚拟机创建后,必要的安全设置,卸snap省内存
服务器·安全
songx_991 小时前
Linux基础3
linux·运维·服务器
半壶清水1 小时前
windows环境下,bmv2虚拟交换机的部署与使用方法
网络·windows·python·网络协议
TechWayfarer1 小时前
营销数据分析:如何利用IP归属地识别和规避虚假流量
网络·数据库·python·tcp/ip·数据分析
拾光Ծ2 小时前
【Linux系统】进程信号(下):信号处理与“操作系统运行原理”
linux·运维·服务器·信号处理·操作系统原理