进程间通信

一、初步了解进程间通信

1、进程间通信的目的

  • 数据传输:一个进程需要将自身数据发送至另一个进程,完成数据交互。

  • 资源共享:多个进程共用系统同一类资源,避免资源重复分配、浪费。

  • 通知事件:一个进程向单个或一组进程发送消息,传递状态变更通知,典型场景为子进程退出后通知父进程。

  • 进程控制:部分进程需要全权管控另一个进程的执行流程、运行状态。

2、进程间通信的分类

  • 管道通信:匿名管道、命名管道

下文将按上述分类,逐一详细介绍各类进程间通信方式的原理、使用规则、代码实例及核心特点。

二、匿名管道

头文件:#include <unistd.h>

功能:创建无名管道,实现进程间数据传输

函数原型:int pipe(int fd2);

参数说明:fd 为文件描述符数组,其中 fd0 对应管道读端,fd1 对应管道写端

返回值:创建成功返回 0,失败返回错误编码

1、初识匿名管道

匿名管道无文件名、文件路径等可被其他进程检索的标识,无法被任意进程直接访问。它主要依靠 fork() 函数,让子进程继承父进程的管道文件描述符,因此仅支持有亲缘关系的进程通信

匿名管道通信的核心本质:通信双方必须持有指向内核中同一个管道对象的文件描述符,才能完成数据交互。

2、父子进程共享管道的操作流程

  1. 父进程调用 pipe() 函数创建匿名管道,将管道读写两端绑定到文件描述符数组 fds 中;

  2. 父进程调用 fork() 创建子进程,子进程会完整继承父进程的 fds 数组,即同时拥有管道的读写文件描述符;

  3. 父子进程根据通信需求,各自关闭自身不需要的管道端口,形成单向通信的管道链路。

3、基于文件描述符的全程解析

系统默认进程的 0、1、2 号文件描述符分别对应标准输入、标准输出、标准错误,处于已占用状态。

第一步:父进程调用 pipe() 创建管道,系统会分配空闲的文件描述符,通常读端为 3 号、写端为 4 号,并将 3 号存入 fd0、4 号存入 fd1,此时父进程持有管道完整的读写权限。

第二步:父进程执行 fork() 生成子进程,子进程会完全复刻父进程的文件描述符表,同步继承管道的 3 号读端、4 号写端。此时内核中的同一个管道对象,被父子进程共同持有,存在两组读写端口。

第三步:为实现规范的单向通信,父子进程分别关闭无用端口。最终形成一个进程独占读端、一个进程独占写端的通信结构,避免数据读写混乱。

4、内核角度本质示意图

5、测试样例

cpp 复制代码
#include <unistd.h> 
#include <stdlib.h> 
#include <stdio.h> 
#include <errno.h> 
#include <string.h> 
#define ERR_EXIT(m) \ 
do \ 
{ \ 
perror(m); \ 
exit(EXIT_FAILURE); \ 
} while(0) 
int main(int argc, char *argv[]) 
{ 
int pipefd[2]; 
// 创建匿名管道 
if(pipe(pipefd)==-1) 
ERR_EXIT("pipe error"); 
pid_t pid; 
// 创建子进程 
pid=fork(); 
if(pid==-1) 
ERR_EXIT("fork error"); 
// 子进程逻辑:关闭读端,向管道写入数据 
if(pid==0) 
{ 
close(pipefd[0]); 
write(pipefd[1],"hello",5); 
close(pipefd[1]); 
exit(EXIT_SUCCESS); 
} 
// 父进程逻辑:关闭写端,从管道读取数据 
close(pipefd[1]); 
char buf[10] = {0}; 
read(pipefd[0], buf, 10); 
printf("buf=%s\n", buf); 
return 0; 
}

6、管道读写规则

(1)管道无数据可读时

  • 非阻塞模式关闭(默认状态):read 函数阻塞,进程暂停执行,持续等待直到有进程向管道写入数据;

  • 非阻塞模式开启:read 函数直接返回 -1,错误码 errno 赋值为 EAGAIN。

(2)管道缓冲区写满时

  • 非阻塞模式关闭(默认状态):write 函数阻塞,进程暂停执行,等待其他进程读取数据、释放缓冲区空间;

  • 非阻塞模式开启:write 函数直接返回 -1,错误码 errno 赋值为 EAGAIN。

(3)特殊读写场景

  • 若管道所有写端文件描述符全部关闭,read 函数读取完现有数据后会返回 0,代表管道数据传输结束;

  • 若管道所有读端文件描述符全部关闭,进程执行 write 操作会触发 SIGPIPE 信号,默认会导致当前写进程异常退出;

  • 单次写入数据量小于等于 PIPE_BUF 时,Linux 系统保证数据写入的原子性,数据不会穿插混乱;

  • 单次写入数据量大于 PIPE_BUF 时,系统不再保证写入原子性,多进程并发写入时可能出现数据交错。

7、匿名管道核心特点

  • 通信范围受限:仅支持有共同祖先、存在亲缘关系的进程通信,通常由父进程创建管道,通过 fork 传递给子进程使用;

  • 服务类型:提供流式数据传输服务,数据以字节流形式传递;

  • 生命周期:管道资源随进程消亡,所有关联进程退出后,内核自动释放管道资源;

  • 同步互斥机制:内核自动对管道的读写操作做同步与互斥处理,避免并发读写冲突;

  • 通信方式:半双工通信,数据仅能单向流动;若需双向交互,必须创建两个独立管道。

8、必须掌握的四种核心状态规则

管道状态 执行操作 最终执行结果
读端正常,管道缓冲区已满 write() 进程阻塞,等待缓冲区释放空闲空间
所有写端全部关闭 read() 管道有数据则正常读取,数据读取完毕后返回 0
所有读端全部关闭 write() 触发 SIGPIPE 信号;若忽略该信号,函数返回 -1,错误码为 EPIPE

三、命名管道(FIFO)

匿名管道的核心局限是仅支持亲缘进程通信,为实现无关联、独立进程之间的通信,系统提供了命名管道(FIFO)。命名管道本质是一种特殊的文件类型,可通过文件路径被任意进程访问。

命名管道的特殊性:普通文件的文件名对应磁盘上的实体数据,而命名管道的文件名仅存储在文件系统目录项中,用于让进程定位内核管道对象;真正的通信数据不存储在磁盘,而是存放在内核的管道缓冲区中。

简单映射关系: 普通文件:文件名 → 磁盘文件数据 命名管道:文件名 → 内核管道对象 → 内核管道缓冲区

1、命名管道的创建方式

(1)终端命令创建

$ mkfifo filename

(2)系统调用函数创建

函数原型:int mkfifo(const char *filename,mode_t mode);

功能:创建指定权限的命名管道文件

2、命名管道与匿名管道的核心区别

  • 匿名管道:通过 pipe() 函数直接创建并完成初始化打开,无实体文件;

  • 命名管道:通过 mkfifo() 函数创建管道文件,需通过 open() 函数手动打开后才能使用;

  • 核心共性:二者内核缓冲区、数据读写语义、通信规则完全一致;

  • 唯一差异:获取管道对象的方式不同,匿名管道依靠进程继承文件描述符,命名管道依靠文件路径查找内核管道对象。

3、命名管道打开规则

(1)以读模式打开 FIFO

  • 非阻塞模式关闭:进程阻塞等待,直到有其他进程以写模式打开该管道;

  • 非阻塞模式开启:直接打开成功,可立即等待读取数据。

(2)以写模式打开 FIFO

  • 非阻塞模式关闭:进程阻塞等待,直到有其他进程以读模式打开该管道;

  • 非阻塞模式开启:打开失败,函数返回 -1,错误码为 ENXIO。

4、代码实例

实例1:读取本地文件,写入命名管道

cpp 复制代码
#include <unistd.h> 
#include <stdlib.h> 
#include <stdio.h> 
#include <errno.h> 
#include <string.h> 
#define ERR_EXIT(m) \ 
do \ 
{ \ 
perror(m); \ 
exit(EXIT_FAILURE); \ 
} while(0) 
int main(int argc, char *argv[]) 
{ 
// 创建命名管道 
mkfifo("tp", 0644); 
// 打开本地待读取文件 
int infd; infd = open("abc", O_RDONLY); 
if (infd == -1) 
ERR_EXIT("open"); 
// 以写模式打开命名管道 
int outfd; 
outfd = open("tp", O_WRONLY); 
if (outfd == -1) 
ERR_EXIT("open"); 
// 循环读取文件数据,写入管道 
char buf[1024]; 
int n; 
while ((n=read(infd, buf, 1024))>0) 
{ 
write(outfd, buf, n); 
} 
// 关闭文件描述符 
close(infd); 
close(outfd); 
return 0; 
}

实例2:读取命名管道数据,写入目标文件

cpp 复制代码
#include <unistd.h> 
#include <stdlib.h> 
#include <stdio.h> 
#include <errno.h> 
#include <string.h> 
#define ERR_EXIT(m) \ 
do \ 
{ \ 
perror(m); \ 
exit(EXIT_FAILURE); \ 
} while(0) 
int main(int argc, char *argv[]) 
{ 
// 创建并打开备份目标文件 
int outfd; 
outfd = open("abc.bak", O_WRONLY | O_CREAT | O_TRUNC, 0644); 
if (outfd == -1) 
ERR_EXIT("open"); 
// 以读模式打开命名管道 
int infd; 
infd = open("tp", O_RDONLY); if (infd == -1) 
ERR_EXIT("open"); 
// 循环读取管道数据,写入目标文件 
char buf[1024]; 
int n;
while ((n=read(infd, buf, 1024))>0) 
{ 
write(outfd, buf, n); 
} 
// 关闭资源、删除管道文件 
close(infd); 
close(outfd); 
unlink("tp"); 
return 0; 
}

5、命名管道删除规则

通过 unlink 或 rm 命令删除命名管道文件时,仅会删除文件系统中的目录项,不会立刻回收内核中的管道资源。对于已经打开该管道的进程,仍可通过已持有的文件描述符正常通信。

只有当所有持有该管道文件描述符的进程全部关闭端口后,内核才会彻底回收管道缓冲区资源。删除目录项后,新的进程将无法通过路径找到该管道。

四、System V 共享内存

共享内存是 System V IPC 中效率最高的通信方式,核心原理是让多个进程映射同一块内核内存区域,进程可直接读写该内存,无需数据拷贝,极大提升通信效率。

1、共享内存示意图

2、共享内存核心数据结构

内核会为每一块共享内存维护一个 shmid_ds 结构体,用于记录内存属性、权限、运行状态等信息:

cpp 复制代码
struct shmid_ds 
{ 
struct ipc_perm shm_perm; // 共享内存操作权限 
int shm_segsz; // 共享内存段大小(字节) 
__kernel_time_t shm_atime; // 最后一次挂载时间 
__kernel_time_t shm_dtime; // 最后一次卸载时间 
__kernel_time_t shm_ctime; // 最后一次属性修改时间 
__kernel_ipc_pid_t shm_cpid; // 创建进程PID 
__kernel_ipc_pid_t shm_lpid; // 最后一次操作进程PID 
unsigned short shm_nattch; // 当前挂载的进程数量 
unsigned short shm_unused; // 兼容保留字段 
void *shm_unused2; // 兼容保留字段 
void *shm_unused3; // 闲置保留字段 
};

3、共享内存核心操作函数

复制代码
void *shmat(int shmid, const void *shmaddr, int shmflg);

功能:将共享内存连接到进程地址空间

  • shmid:共享内存标识

  • shmaddr:指定连接的地址

  • shmflg:可能取值是SHM_RNDSHM_RDONLY 返回值 :成功返回一个指针,指向共享内存首地址;失败返回(void *)-1

    int shmdt(const void *shmaddr);

功能:将共享内存段与当前进程脱离

  • shmaddr:由shmat返回的指针 返回值:成功返回0;失败返回-1

    int shmctl(int shmid, int cmd, struct shmid_ds *buf);

功能:用于控制共享内存

  • shmid:由shmget返回的共享内存标识码
  • cmd:将要采取的动作(有三个可取值)
    • IPC_STAT 获取共享内存属性
    • IPC_SET 修改共享内存属性
    • IPC_RMID 标记删除共享内存 补充:共享内存对象被标记为删除;已经映射它的进程仍然可以通过原来的映射访问这块内存,直到最后一个进程解除映射后,内核才真正回收它。(与命名管道类似)
  • buf:指向一个保存着共享内存的模式状态和访问权限的数据结构 返回值:成功返回0,失败返回-1

4、实例

makefile

复制代码
.PHONY:all
all:server client
client:client.c comm.c
	gcc -o $@ $^
server:server.c comm.c
	gcc -o $@ $^
.PHONY:clean
clean:
	rm -f client server

comm.h

cpp 复制代码
#ifndef _COMM_H_
#define _COMM_H_
# include <stdio.h>
# include <sys/types.h>
# include <sys/ipc.h>
# include <sys/shm.h>
# define PATHNAME "."
# define PROJ_ID 0x6666
int createShm(int size);
int destroyShm(int shmid);
int getShm(int size);
# endif

comm.c

cpp 复制代码
#include "comm.h"
static int commShm(int size, int flags)
{
key_t key = ftok(PATHNAME, PROJ_ID);
if(key < 0){
perror("ftok");
return -1;
}
int shmid = 0;
if( (shmid = shmget(key, size, flags)) < 0){
perror("shmget");
return -2;
}
return shmid;
}
int destroyShm(int shmid)
{
if(shmctl(shmid, IPC_RMID, NULL) < 0){
perror("shmctl");
return -1;
}
return 0;
}
int createShm(int size)
{
return commShm(size, IPC_CREAT|IPC_EXCL|0666);
}
int getShm(int size)
{
return commShm(size, IPC_CREAT);
}

server.c

cpp 复制代码
#include "comm.h"
int main()
{
int shmid = createShm(4096);
char *addr = shmat(shmid, NULL, 0);
sleep(2);
int i = 0;
while(i++<26){
printf("client# %s\n", addr);
sleep(1);
}
shmdt(addr);
sleep(2);
destroyShm(shmid);
return 0;
}

client.c

cpp 复制代码
#include "comm.h"
int main()
{
int shmid = getShm(4096);
sleep(1);
char *addr = shmat(shmid, NULL, 0);
sleep(2);
int i = 0;
while(i<26){
addr[i] = 'A'+i;
i++;
addr[i] = 0;
sleep(1);
}
shmdt(addr);
sleep(2);
return 0;
}
相关推荐
Liuqy-054 小时前
Linux IO编程——静态库、动态库
linux
彧azz4 小时前
Linux 环境下 Redis 学习总结:数据类型、持久化、锁、事务、主从与缓存问题
linux·redis·笔记·学习·面试
-梅5 小时前
linux(8) 软硬链接
linux·运维·服务器
AIgorithmGEEK5 小时前
[Linux]线程三部曲(上):一个执行流的诞生——从操作系统一路拆到 pthread_create
linux·线程·pid
琥珀色糖6 小时前
SimlpeHttp
linux·服务器
qeen876 小时前
【Linux】操作系统之进程介绍(二)
linux·笔记·学习·进程
Zenova EdgeOS6 小时前
Linux ss 工业边缘网络分析实战
linux·python·边缘计算·工业网关
第十人i6 小时前
Linux 日志管理系统:journald 配置与优化
linux·服务器
wuminyu6 小时前
HotSpot的轻量锁与膨胀后的ObjectMonitor状态重建原理
java·linux·c语言·jvm·c++