1.匿名管道
pipe 函数是 进程间通信(IPC) 和 数据流处理 中的核心工具,其核心思想是创建一个 “管道”,实现 单向数据流传递—— 数据从 “写端” 输入,从 “读端” 输出,类似现实中的管道(一端进水、一端出水)。
不同编程环境中,pipe 的实现和用途略有差异,但核心逻辑一致。
pipe 是内核提供的系统调用,用于创建 匿名管道,仅支持 亲缘进程(父子、兄弟进程)间的单向通信。
#include <unistd.h>
// 创建管道,参数 pipefd 是输出型数组,存储管道的读端和写端文件描述符
int pipe(int pipefd[2]);
pipefd[2] 是长度为 2 的整型数组,用于接收管道的两个 “端点”:
pipefd[0]:管道 读端(Read End),只能用 read() 函数读取数据;
pipefd[1]:管道 写端(Write End),只能用 write() 函数写入数据。
注意:管道是 半双工(Half-duplex)的 —— 同一时间只能单向传输数据(要么 “写→读”,要么 “读→写”,不能同时双向)。
成功:返回 0,并填充 pipefd 数组;
失败:返回 -1,并设置 errno(如 EMFILE 表示文件描述符耗尽、ENFILE 表示系统文件数上限)。
#include <iostream>
#include <cstdio>
#include <string>
#include <cstring>
#include <cstdlib> //stdlib.h
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#define N 2
#define NUM 1024
using namespace std;
// child
void Writer(int wfd)
{
string s = "hello, I am child";
pid_t self = getpid();
char buffer[NUM];
int number=0;
while (true)
{
//构建发送字符串
buffer[0] = 0; // 字符串清空, 只是为了提醒阅读代码的人,我把这个数组当做字符串了
snprintf(buffer, sizeof(buffer), "%s-%d-%d", s.c_str(), self, number++);
cout << buffer << endl;
//发送/写入给父进程, system call
write(wfd, buffer, strlen(buffer));
}
}
// father
void Reader(int rfd)
{
char buffer[NUM];
while(true)
{
buffer[0] = 0;
// system call
ssize_t n = read(rfd, buffer, sizeof(buffer)); //sizeof != strlen
if(n > 0)
{
buffer[n] = 0; // 0 == '\0'
cout << "father get a message[" << getpid() << "]# " << buffer << endl;
}
sleep(5);
}
}
int main()
{
int pipefd[N] = {0};
int n = pipe(pipefd);
if (n < 0)
return 1;
// cout << "pipefd[0]: " << pipefd[0] << " , pipefd[1]: " << pipefd[1] << endl;
// child -> w, father->r
pid_t id = fork();
if (id < 0)
return 2;
if (id == 0)
{
// child
close(pipefd[0]);
// IPC code
Writer(pipefd[1]);
close(pipefd[1]);
exit(0);
}
// father
close(pipefd[1]);
// IPC code
Reader(pipefd[0]);
pid_t rid = waitpid(id, nullptr, 0);
if(rid < 0) return 3;
close(pipefd[0]);
sleep(5);
return 0;
}
这段代码实现了 Linux 环境下父子进程通过管道(Pipe)的单向进程间通信(IPC),核心功能如下:
1.主进程:用 pipe() 创建管道(读端 pipefd[0]、写端 pipefd[1]),fork() 生成子进程,双方关闭无关管道端点(子关读端、父关写端),确保单向传输。
2.子进程(生产者):循环生成格式为「hello, I am child-自身PID-递增序号」的消息,通过管道写端持续写入。
3.父进程(消费者):每 5 秒从管道读端读取一次(阻塞等待数据),读取后打印消息。
4.关键特性:管道流式传输(无消息边界,可能拼接多条消息)、半双工通信、依赖亲缘进程共享文件描述符,父进程会等待子进程退出避免僵尸进程。

管道的 4 种情况:
1.读写都正常,管道如果为空,读端就要阻塞
2.读写都正常,管道如果被写满,写端就要阻塞
3.读端正常,写端关闭,读端就会读到 0,表示读到了文件 (pipe) 结尾,不会被阻塞 此时read返回值是0
4.写端是正常写入,读端关闭了:操作系统就要杀掉正在写入的进程。如何干掉?通过信号杀掉
第四点为什么会被信号杀死???
核心原因:
读端关闭后,写端调用 write () 会触发不可恢复错误。
1.管道缓冲区数据无人接收,即便没满,写入的也是无效数据,堆积只会浪费内存;
2.操作系统无法解决供需失 衡:不终止写端的话,写端会写至缓冲区满后阻塞,变成僵尸进程浪费资源,多写端同时写还会引发数据混乱、影响管道稳定性,因此只能终止写端。
、
✓ 1. 具有血缘关系的进程进行进程间通信
✓ 2. 管道只能单向通信
✓ 3. 父子进程是会进程协同的,同步与互斥的 ---- 保护管道文件的数据安全
✓ 4. 管道是面向字节流的
✓ 5. 管道是基于文件的,而文件的生命周期是随进程的!
2.命名管道
我们学习了匿名管道 要学习命名管道也很简单 只需要学习其区别就可以了解命名管道

命名管道的接口 mkfifo(既可以是一个函数也可以是命令行的一个指令)
#include <sys/stat.h> // 包含 mkfifo 函数声明、权限位定义
#include <sys/types.h> // 包含数据类型(如 mode_t)定义
int mkfifo(const char *pathname, mode_t mode);
一.返回值
成功:返回 0,表示命名管道已在 pathname 指定路径创建成功;
失败:返回 -1,并设置全局变量 errno 标识错误原因(需通过 perror 或 strerror 查看具体错误)。
二.参数详解
1. const char *pathname:管道的路径与名称
作用:指定命名管道在文件系统中的 “唯一标识”,进程通过该路径打开管道进行通信;
命名规范:
需符合 Linux 文件命名规则(不能包含 / 以外的特殊字符,长度不超过 NAME_MAX);
建议放在临时目录 /tmp/ 下(如 /tmp/my_fifo),避免占用普通文件目录;
路径可以是绝对路径(推荐,如 /tmp/pipe_log)或相对路径(如 ./local_pipe,依赖当前工作目录);
注意:若路径已存在同名文件(无论是否为 FIFO),函数会失败(errno=EEXIST)。
2. mode_t mode:管道的访问权限
作用:设置命名管道的读写权限(类似普通文件的权限控制),决定哪些用户 / 进程可访问该管道;
数据类型:mode_t 是无符号整数,权限通过 “八进制数” 或 “权限宏组合” 指定;

权限宏说明(系统预定义,在 <sys/stat.h> 中):
S_IRUSR:所有者(owner)读权限;
S_IWUSR:所有者写权限;
S_IRGRP:组用户(group)读权限;
S_IWGRP:组用户写权限;
S_IROTH:其他用户(others)读权限;
S_IWOTH:其他用户写权限;
注意:实际生效的权限 = mode & ~umask(umask 是系统默认权限掩码,默认值通常为 0022,会屏蔽 “其他用户的写权限”)。例如:
若 mode=0666、umask=0022,实际权限为 0666 & ~0022 = 0644(其他用户无写权限);
若需强制生效 0666,可先通过 umask(0) 临时清除掩码(不推荐在生产环境使用,可能导致安全风险)。



匿名管道和普通管道一样
也遵循这四点
1.读写都正常,管道如果为空,读端就要阻塞
2.读写都正常,管道如果被写满,写端就要阻塞
3.读端正常,写端关闭,读端就会读到 0,表示读到了文件 (pipe) 结尾,不会被阻塞 此时read返回值是0
c语言命名管道代码Linux 命名管带代码-优快云博客
3.共享内存
共享内存接口比较多

1.ftok
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);
成功:返回非负的 key_t 类型值(唯一 IPC 键值);
失败:返回 -1,并设置 errno(可通过 perror() 查看错误原因,如文件不存在、权限不足)。
const char *pathname
必须是系统中已存在的文件 / 目录路径(如 /tmp),用其唯一的 inode 号作为生成 key 的核心依据
int proj_id
必须是非 0 值(常用 1-255),仅低 8 位有效,用于区分同一路径下的不同 IPC 资源(如同一 /tmp 对应共享内存和消息队列)
proj_id 的核心作用是:在同一路径(pathname)下,区分不同的 IPC 资源(如共享内存、消息队列),避免同一文件路径对应多个 IPC 资源时产生冲突。
IPC就是进程间通信英文缩写
ftok函数的作用是:
核心作用是将 “文件路径 + 项目 ID” 映射为唯一的 key_t 类型键值,用于标识一个 IPC 资源(如共享内存段),让多个进程能通过同一个 key 找到同一个 IPC 资源。
ftok() 生成 key 的逻辑很简单,核心是组合两个关键信息:
提取 pathname 对应文件的 inode 号(32 位系统中通常取低 16 位或 24 位);
提取 proj_id 的 低 8 位;
将上述两部分拼接成一个 32 位的 key_t 类型值,作为最终的 IPC 资源键。
2.shmget
shmget() 是 System V 共享内存的核心函数,核心作用是 根据唯一键值 key 创建新的共享内存段,或获取已存在的共享内存段,最终返回一个用于标识该共享内存段的唯一标识符 shmid(供后续 shmat()/shmctl() 等函数使用)。
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
成功:返回非负整数 shmid(共享内存段的唯一标识符,后续操作共享内存的核心 “凭证”);
失败:返回 -1,并设置 errno(常见错误如下,可通过 perror() 打印
key_t key
共享内存唯一标识(通常由ftok()生成);特殊值IPC_PRIVATE= 创建父子进程私有共享内存
size_t size
共享内存大小(字节),创建时必填(需页对齐),获取已有内存时设为 0 即可
int shmflg
权限位(如0666= 读写)+ 控制标志(IPC_CREAT= 不存在则创建;IPC_EXCL+IPC_CREAT= 防重复创建)
3.shmat
#include <sys/shm.h>
void *shmat(int shmid, const void *shmaddr, int shmflg);
成功:返回指向共享内存段的void*指针(需强制转换为对应数据类型后使用);
失败:返回 (void *) -1,并设置errno(如EINVAL:shmid 无效或 shmaddr 非法)。
shmid:目标共享内存段的唯一标识 ID(由shmget函数创建 / 获取);
shmaddr:期望附加到进程地址空间的地址,传NULL时由系统自动分配最优地址(推荐用法);
shmflg:附加标志,常用SHM_RDONLY(只读附加),默认 0(可读写附加)。
核心功能:将内核中的共享内存段,映射到当前进程的虚拟地址空间,建立进程与共享内存的访问关联。
4.shmdt
#include <sys/shm.h>
int shmdt(const void *shmaddr);
shmaddr:shmat函数成功返回的共享内存映射指针(必须是有效映射地址,不可随意传指针)。
成功:返回 0;
失败:返回 -1,并设置errno(如EINVAL:shmaddr 不是有效的共享内存映射地址)。
核心功能:断开进程与共享内存的地址映射,指针shmaddr失效;仅减少共享内存的 “附加计数”,不释放内核中的共享内存。
5.shmctl
#include <sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
shmid:共享内存段的唯一 ID;
cmd:控制命令(核心常用IPC_RMID:标记共享内存为待删除;其他常用IPC_STAT:获取共享内存属性,IPC_SET:修改共享内存属性);
buf:指向struct shmid_ds结构体的指针(存储 / 修改共享内存属性,使用IPC_RMID时可传NULL)。
成功:执行IPC_RMID/IPC_SET/IPC_STAT时返回0;执行IPC_INFO时返回最大共享内存 ID;
失败:返回 -1,并设置errno(如EACCES:无操作权限,EINVAL:shmid 无效或 cmd 非法)。
核心功能:对共享内存执行管理操作,核心场景是通过IPC_RMID标记删除 —— 标记后,共享内存不再允许新进程附加,等所有已附加的进程都分离后,内核自动释放该共享内存。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/shm.h>
int main() {
int key;
int shmid;
char *shm_ptr;
const char *msg = "Hello, CentOS共享内存!";
if ((key = ftok("/tmp", 1234)) == -1) {
perror("ftok failed");
exit(1);
}
if ((shmid = shmget(key, 1024, 0666 | IPC_CREAT)) == -1) {
perror("shmget failed");
exit(1);
}
printf("共享内存创建成功: shmid=%d\n", shmid);
if ((shm_ptr = shmat(shmid, NULL, 0)) == (void *)-1) {
perror("shmat failed");
exit(1);
}
strncpy(shm_ptr, msg, strlen(msg) + 1);
printf("已写入共享内存: %s\n", shm_ptr);
printf("从共享内存读取: %s\n", shm_ptr);
if (shmdt(shm_ptr) == -1) {
perror("shmdt failed");
exit(1);
}
printf("共享内存已分离\n");
if (shmctl(shmid, IPC_RMID, NULL) == -1) {
perror("shmctl delete failed");
exit(1);
}
printf("共享内存已标记删除\n");
return 0;
}
运行结果也符合我们的预期

但是我们接下来要思考一个问题
共享内存的生命周期和进程生命周期一样吗
答案 肯定不可以 因为共享内存可以多个进程使用 而每个进程的生命周期是不一样的
我们发现当我们程序创建好共享内存后强制停止进程
进程结束了 但是共享内存依然存在

那么有没有释放这些共享的指令呢
有
ipcrm -m shmid

ipcrm -M key

有进程附着(加载)共享内存时,可以执行删除操作,但共享内存不会立即释放,而是进入「待销毁」状态,直到所有附着进程脱离后才会真正销毁。
总结一下共享内存
通过ftok传入参数一致实现进程间先约定好同一 key,一方进程通过 shmget() 函数,依据该 key 让操作系统创建对应的共享内存,并获取到唯一标识 shmid;之后双方都需通过 shmat() 函数,将这块共享内存挂载到自身的虚拟地址空间(才能读写数据);另一进程同样通过相同的 key,调用 shmget() 找到对应的 shmid,再经 shmat() 挂载后,就能与对方读写同一块内存实现通信;通信结束后,用 shmdt() 卸载共享内存,最后通过 shmctl(IPC_RMID) 或 ipcrm 命令删除该共享内存。
几个问题
1.key可不可以由操作系统实现的
操作系统无法预知哪些进程需要通信、何时通信,因此无法主动分配一个让多个进程 “共同知晓” 的 key
哪些进程使用共享内存 什么时候使用共享内存 只有用户知道 操作系统不知道
2.共享内存为什么要有操作系统去实现创建 不能由进程实现创建呢
答:如果由进程创建 那么这个共享内存就是进程的 但是进程具有独立性
我们想在要实现通信 如果别的进程可以直接访问这个进程的内存 那么不仅破坏了进程的独立性
而且我们现在要实现的就是进程通信 如果允许直接访问这个进程的内存 那么不就意味着已经实现了进程通信吗
1.共享内没有像管道一样的同步互斥之类的保护机制
2.共享内存是所有的进程间通信中,速度最快的!
3.共享内存内部的数据,由用户自己维护!
1. 管道的互斥机制(避免 “同时操作冲突”)
当多个进程同时读写管道时,内核会自动保证:
写互斥:同一时间,只有一个进程的写操作能被执行(尤其是写数据≤PIPE_BUF时,原子性写本质就是互斥的体现)。
例:3 个进程同时向管道写短日志(≤4096 字节),内核会让它们 “排队写”,每个进程的日志完整存入,不会出现 “你写一半我插进来” 的情况。
读互斥:同一时间,只有一个进程能从管道读取数据(避免多个读进程同时读导致数据拆分混乱)。
例:2 个进程同时读管道,内核会让一个进程先读完部分 / 全部数据,另一个进程再读剩下的,不会出现 “同一字节被两个进程同时读取” 的情况。
2. 管道的同步机制(保证 “操作顺序合理”)
内核通过 “阻塞” 机制实现同步,核心规则:
管道缓冲区为空时:读进程会被阻塞(暂停执行),直到有写进程向管道写入数据后,读进程才被唤醒继续读。
→ 避免读进程 “读空”(读不到数据还白忙活)。
管道缓冲区满时:写进程会被阻塞(暂停执行),直到有读进程从管道读出数据、释放缓冲区空间后,写进程才被唤醒继续写。
→ 避免写进程 “写满”(数据溢出丢失)。
进程间通信还有许多方法 比如消息队列等待 消息队列接口使用方法和共享内存类似
我们这里就不过多介绍

4.初步信号量
信号量本质上就是一个计数器
我们害怕多个进程执行流访问同一个资源
因此就有信号量
每有一个资源申请成功就-- 每有一个资源释放成功就++
1.申请计数器成功,就表示我具有访问资源的权限了
2.申请了计教器资源,我当前访问我要的资源了吗?没有。申请了计教器资源是对资源的预订机制3,计数器可以有效保证进入共享资源的执行流的数量
4.所以每一个执行流,想访问共享资源中的一部分的时候,不是直接访问,而是先申请计数器资源,就像看电影的先买票!
当资源块为1的时候本质上就是互斥的就是一个锁
当然资源块也可以为很多小块
信号量的作用是确保资源不会被多个执行流使用
2244

被折叠的 条评论
为什么被折叠?



