进程间的通信方式(PIC):管道、共享内存、消息队列、信号量、套接字
假设 A、B两个进程
单工:单向性;确定通信方向后固定,不能相互传输数据
全双工:双向性;同一时刻,两方可以互相传输数据
半双工:双向性;同一时刻,只能一方给另一方传输数据
一、管道
管道可以用来在两个进程之间传递数据。
特性:
(1)管道是单向的,数据只能从左到右
(2)管道会创建子进程来执行命令
(3)管道中的命令是并行执行的
(4)管道可以串联,形成管道链
(一)有名管道
1.有名管道
通过 mkfifo命令创建管道;可以在任意两个进程之间通信。
2.特性
(1)有可见的文件名(存在于文件系统中)
(2)可以被任意进程(需要有权限)访问
(3)数据传输遵循先进先出原则
(4)本质上是内存中的特殊文件,不占用磁盘空间
(5)关闭所有引用它的进程后,管道文件仍然存在(需要手动删除)
3.测试
(1)测试一:在命令行输入
读端 :持续监听管道 ;cat < 管道文件
持续读取:while true; do cat < 管道文件; done
写端 :多次写入内容 ;cat > 管道文件

(2)测试二:在文件写入和读取
读文件:使用read函数读文件
注意:read函数的第三个参数不能是初始化的strlen
写文件:使用write函数写数据到管道文件中


有名管道的双向阻塞特性
(1)以
O_WRONLY打开管道的写端,会阻塞等待 ,直到有进程以O_RDONLY方式打开同一个管道。(2)以
O_RDONLY打开管道的读端,也会阻塞等待 ,直到有进程以O_WRONLY方式打开同一个管道。只有当读写两端都成功打开管道时,双方的
open()调用才会解除阻塞,继续执行后续代码。
(二)无名管道
1.无名管道
无名称,仅通过文件描述符访问;主要应用于父子进程间的通信
2.特性
(1)仅支持亲缘进程:父子进程、兄弟进程
(2)创建方式:**pipe()**系统调用
(3)生命周期:随进程的结束而销毁
3.无名管道的使用

(三)管道的特点
|-----------|--------------|------------|
| | 有名管道 | 无名管道 |
| 名称 | 有名,存在文件系统中 | 无名,不在文件系统中 |
| 进程的关系 | 任意进程(有权限) | 父子进程、兄弟进程 |
| 生命周期 | 随文件系统的销毁而i销毁 | 随进程的销毁而销毁 |
| 创建方式 | mkfifo | pipe |
注意事项:
(1)避免管道破裂 :读端已关闭的情况下,写端还在输出数据时,会触发 SIGPIPE 信号,终止写端进程
(2)数据无边界 :管道是字节流数据;若按照消息的格式,一条一条发送,需要自定义信息格式(例如长度+数据),避免多读少读
(3)缓冲区阻塞问题 :读空、写满会阻塞进程,所以需要 **fcunt()**设置属性进行处理
(4)资源泄露问题:进程退出前提前关闭管道描述符,会导致缓冲区中的数据丢失
管道的通信方式为半双工
二、共享内存
(一)共享内存原理
共享内存为多个进程之间共享和传递数据提供了一种有效的方式。
共享内存是先在物理内存上申请一块空间,多个进程可以将其映射到自己的虚拟地址空间中。所有进程都可以访问共享内存中的地址,就好像它们是由malloc分配的一样。如果某个进程向共享内存写入了数据,所做的改动将立刻被可以访问同一段共享内存的任何其他进程看到。
由于它并未提供同步机制,所以我们通常需要用其他的机制来同步对共享内存的访问。

(二)共享内存的函数与使用
ipcs:可以查看当前系统中进程间通信资源
1.shmget
创建一块共享内存空间。
bash
int shmget(key_t key, size_t size, int shmflg);
(1)key:共享内存段的唯一标识
(2)size:表示创建的共享内存空间的大小
创建新共享空间时,该参数填正整数;获取已有共享空间时,该参数可以填0
(3)shmflg:标志位,用来控制函数行为
创建标志:
IPC_CREAT:如果 key 不存在,则创建
IPC_EXCL:与 IPC_CREAT 配合使用,如果 key 已经创建则报错并设置错误原因给errno
权限控制:需要指定权限位(例如0600)
(4)返回值:-1表示共享内存创建失败

2.shmat
将一个已存在的共享内存映射到进程的地址空间中。
bash
void *shmat(int shm_id, const void *shm_addr, int shmflg);
(1)shm_id:共享内存段的标识符,通常是 shmget 函数的返回值
(2)shm_addr:指定共享内存段要附加到进程地址空间的哪个地址
NULL:让系统内核自动选择一块合适的,未使用的地址来附加共享内存段
非NULL(填具体地址):系统会将共享内存段附加到指定位置;如果指定位置不能使用,则报错,附加失败
(3)shmflg:控制附加操作标志位
0:默认权限,根据 shmget 创建时的权限进行读写
SHM_RDONLY :以只读方式附加共享内存段,如果该进程尝试写入,则引发段错误
SHM_EXEC:允许在共享内存段上执行代码

共享内存的工作方式时全双工
三、信号量
(一)信号量
信号量,是操作系统用于进程间同步与互斥 的关键机制;解决多个进程对共享资源的竞争问题,避免数据冲突或不一致。
信号量的本质是一个受保护的整数变量 ,其值反映了共享资源的可用数量 ,只能通过特定的原子操作(不可被中断的操作)修改。
原子操作:对信号量的修改(加、减),避免多进程并发修改导致数据不一致。
堵塞机制:当信号量为0时,试图获取该信号量的进程会被阻塞,直到有其他进程释放资源。
(二)信号量的类型
1.二值信号量
值只能是0和1,用于实现互斥(类似锁,一次只允许一个进程运行)
2.计数信号量
值可以是任意非负数,用于控制对多个相同资源的并发访问(假设信号量值为3,则表示最大可支持3个进程同时运行)
(三)P、V原语
P、V原语,信号量的行为由两个基本操作定义(称为"原语",确保原子性)
1.P操作(获取资源)
将信号量值减1;
若结果 >= 0 ,进程可继续执行;若结果 < 0,进程被阻塞,放入信号量的等待队列
> 0:有可用资源,直接获取
= 0:资源刚好用完,进程也能通过,但下一个进程就会被阻塞
< 0:资源不足,进程必须阻塞等待
2.V操作(释放资源)
将信号量值加1;
若结果 > 0 ,进程可继续执行;若结果 <= 0,从等待队列中唤醒一个进程,使其继续执行
V操作不会阻塞,永远释放资源
(四)临界资源、临界区
1.临界资源
同一时刻,只允许被一个进程或线程访问的资源
2.临界区
访问临界资源的代码段
(五)信号量的函数
1.semget
用于创建或获取信号量集的系统调用
bash
int semget(key_t key, int nsems, int semflg);
(1)key :用于表示信号量集的唯一性,确保不同进程能通过相同的key访问同一信号量集
(2)nsems :信号量的数量(要创建的信号量集中包含的信号量个数)
若是创建新信号量集,则nsems必须大于0;
若是获取已存在的信号量集,则nsems可以设置为0
(3)semflg(标志位):控制信号量集的创建/获取行为
IPC_CREAT:若对应的信号量集不存在,则创建新集;若已存在,则返回其ID
IPC_EXCL:与 IPC_CREAT 组合使用;若信号量集已存在,则创建失败(确保获得新资源)
权限:例如0600
(4)返回值:成功返回信号量的ID,失败返回 -1
2.semctl
用于控制操作信号量集。
bash
int semctl(int semid, int semnum, int cmd, ...);
(1)semid:信号量集的标识符(由 semget() 返回),指定要操作的信号量集
(2)semnum:信号量在集中的索引(从0开始),指定要操作的单个信号量
若对整个信号量集进行操作(IPC_RMID),则此参数忽略
(3)cmd:要执行的控制命令,决定 semctl 的行为
SETVAL :设置 semnum 指定的信号量的初始值(需配合第四个参数传递)
GETVAL :设置 semnum 指定的信号量的当前值(返回值为该信号量的值)
IPC_RMID :删除整个信号量集(所有进程都无法访问)
IPC_STAT :获取信号量集的状态信息,存入 semid_ds 结构体(创建时间,权限)
IPC_SET :设置信号量集的属性,需要通过 semid_ds 结构体传递新属性
(4)可变参数(可选) :根据 cmd 参数,可能需要传递一个 union semun 类型的参数(需要自定义)
bashunion semun { int val; //用于 SETVAL 命令(设置信号量值) struct semid_ds *buf; //IPC_STAT 或者 IPC_SET(状态信息) unsigned short *array; //GETALL 或者 SETALL(批量操作) struct seminfo *_buf; //用于IPC_INFO(系统限制信息) }
(5)返回值:成功返回 0,失败返回 -1
3.semop
用于执行信号量的操作,对信号量进行改变,做P操作 或者V操作。
bash
int semop(int semid, struct sembuf *sops, size_t nsops);
(1)semid:信号量集的标识
(2)sops :指向struct sembuf 的结构体指针
bashstruct sembuf { unsigned short sem_num; //信号量在集中的索引 short sem_op; //操作类型:其值为-1,代表P操作;其值为+1,代表V操作 short sem_flg; //操作标志,一般使用 SEM_UNDO(确保进程异常退出时,系统撤销对信号量的修改,避免永久堵塞) }
(3)nsops :数组中 sops 元素的数量(要执行的操作总数)
(4)返回值:成功返回 0,失败返回 -1
(六)信号量的使用
bash
vi sem.h
cpp
/*sem.h*/
#include <stdio.h>
#include <sys/sem.h>
//信号量初始化联合体
union semun
{
int val;
};
void sem_init(); //信号量的创建、初始化
void sem_p(int val); //P操作
void sem_v(int val); //V操作
void sem_clear(); //清除信号量
bash
vi sem.c
cpp
/*sem.c*/
#include <stdio.h>
#include <stdlib.h>
#include <sys/sem.h>
#include "sem.h"
int semid; //用来保存信号量集的ID,全局变量方便所有函数操作
//信号量集的创建、初始化
void sem_init()
{
semid = semget((key_t)1234, 2, IPC_CREAT|IPC_EXCL|0600); //尝试全新创建信号量集(包含2个信号量)
if(semid == -1)
{
semid = semget((key_t)1234, 2, IPC_CREAT|0600); //信号量集已存在,直接获取ID
if(semid == -1)
{
exit(1);
}
}
else //如果该信号量集第一次创建,则需要初始化两个信号量的值
{
union semun a;
int arr[2] = {1, 0}; //s1: arr[0] = 1;s2: arr[1] = 0
for(int i = 0; i < 2; i++)
{
a.val = arr[i]; //给val赋值
//设置第i个信号量的值为a.val
if(semctl(semid, i, SETVAL, a) == -1) //判断信号量集操作是否成功
{
exit(1);
}
}
}
}
//P操作
void sem_p(int val)
{
struct sembuf buf; //用来描述一次信号量操作的结构体
buf.sem_num = val; //要操作的信号量下标(0 或 1)
buf.sem_op = -1; //P操作:信号量值减1
buf.sem_flg = SEM_UNDO; //进程异常退出时自动恢复信号量值
//执行信号量操作
if(semop(semid, &buf, 1) == -1)
{
printf("P err!\n");
}
}
//V操作
void sem_v(int val)
{
struct sembuf buf; //用来描述一次信号量操作的结构体
buf.sem_num = val; //要操作的信号量下标(0 或 1)
buf.sem_op = +1; //V操作:信号量值加1
buf.sem_flg = SEM_UNDO; //进程异常退出时自动恢复信号量值
//执行信号量操作
if(semop(semid, &buf, 1) == -1)
{
printf("V err!\n");
}
}
//清除信号量
void sem_clear()
{
//删除整个信号量集
if(semctl(semid, 0, IPC_RMID) == -1)
{
printf("rmid err!\n");
}
}
bash
vi 6.w.c
cpp
/*6.w.c*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/shm.h>
#include "sem.h"
int main()
{
//创建/获取共享内存段
int shmid = shmget((key_t)1234, 128, IPC_CREAT|0600);
if(shmid == -1)
{
printf("shmget err!\n");
exit(1);
}
//将共享内存映射到当前进程的虚拟地址空间
char *s = (void*)shmat(shmid, NULL, 0);
if(s == (char*) -1)
{
printf("shmat err!\n");
exit(1);
}
sem_init(); //初始化信号量集
char buff[128]; //用于存放用户输入的缓冲区
//循环写入数据
while(1)
{
fgets(buff, 128, stdin); //从键盘读取一行输入到buff
sem_p(0); //P操作:申请互斥锁(sem[0]减1)
strcpy(s, buff); //将buff中的数据写入共享数据
sem_v(1); //V操作:通知读端"数据已就绪"(sem[1]加1)
//如果写入的前3个字符是"end",退出循环
if(strncmp(s, "end", 3) == 0)
{
break;
}
}
}
bash
vi 6.r.c
cpp
/*6.r.c*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/shm.h>
#include "sem.h"
int main()
{
//获取共享内存段
int shmid = shmget((key_t)1234, 128, IPC_CREAT|0600);
if(shmid == -1)
{
printf("shmget err!\n");
exit(1);
}
//映射共享内存
char *s = (void*)shmat(shmid, NULL, 0);
if(s == (char*) -1)
{
printf("shmat err!\n");
exit(1);
}
sem_init(); //初始化信号量集
//循环读取数据
while(1)
{
sem_p(1); //P操作:等待写端数据就绪(sem[1]减1)
printf("%s\n", s); //打印共享内存中的数据
//如果读到的前3个字符是"end",退出循环
if(strncmp(s, "end", 3) == 0)
{
strcpy(s, "123"); //写入一个结束标记,避免写端再次读到"end"
break;
}
sem_v(0); //V操作:释放互斥锁(sem[0]加1)
}
//清理信号信号量集
sem_clear();
}
