第七节 进程间的通信
一、管道
管道:|----->所有管道的特性:①管道是单向的,数据只能从左到右;②管道会创建子进程执行命令;③管道中的命令时并行执行的;④管道可以串联,形成管道链。
管道可以用来在两个进程之间传递数据,如: ps -ef | grep “bash”, 其中‘|’就是管道,其作用就是将 ps 命令的结果写入管道文件,然后 grep 再从管道文件中该数据进行过滤。
(1)有名管道
通过mkfifo命令创建管道,形成管道名
专属于有名管道的特性:①有可见的文件名(存在于文件系统中);②可以被任意进程访问(需要权限);③数据传输遵循先进先出的原则;④本质上时内存中的特殊文件,不占用磁盘空间;⑤关闭所有引用它的进程后,管道文件任然存在(需要手动删除)。
测试一:
读端:持续监听管道;cat<管道文件:注意:持续读取:whiel true;do cat< 管道文件;done
写端:多次写入内容:cat>管道文件
需要开两个终端:
第一个终端:Cat > fifo(管道文件)开启写端,在屏幕上写
第二个终端:接收第一个终端的信息
测试二:
在文件读取和写入
读文件:使用read函数读文件,注意:read的第三个参数不能是初始化的strlen
写文件:使用write函数将数据写入到管道文件中
#include<stdio.h> #include<stdlib.h> #include<string.h> #include<unistd.h> #include<fcntl.h> void main() { int fdw=open("fifo",O_WRONLY);//只读的方式打开 if(fdw==-1) exit(1); printf("input:"); char buff[128]; fgets(buff,128,stdin); write(fdw,buff,strlen(buff)); close(fdw); exit(0); }#include<stdio.h> #include<stdlib.h> #include<string.h> #include<unistd.h> #include<fcntl.h> void main() { int fdr=open("fifo",O_RDONLY); if(fdr==-1) exit(1); char arr[128]; read(fdr,arr,sizeof(arr)); printf("%s\n",arr); close(fdr); exit(0); }注意:
两个进程通信成功,但是使用很多次管道文件fifo ,但是它的内存还是0,不占用空间
(2)无名管道
(无名称,仅通过文件描述访问)无名管道主要应用于父子进程间的通信。
无名管道的特性:①仅支持亲缘进程(父子进程、兄弟进程);②创建方式:pipe () 系统调用;③生命周期:随进程的结束而销毁。
测试:无名管道的创建
#include<stdio.h> #include<stdlib.h> #include<string.h> #include<unistd.h> #include<fcntl.h> int main() { int fd[2];//用来存储读端和写端的值 pid_t pid;//需要fork() char buff[128];//读端缓冲区 char*message="hello from parent";//写端发送的数据 ///1.创建无名管道 if(pipe(fd)==-1) { printf("pipe err!\n"); exit(1); } //2.创建子进程,继承管道文件描述符 if((pid=fork())==-1) { printf("fork err!\n"); exit(1); } //3.区分父子进程,设置通信方向 if(pid>0) { close(fd[0]);//关闭读端 write(fd[1],message,strlen(message)); printf("Parent finish!\n"); close(fd[1]);//关闭写端 } else { close(fd[1]);//关闭写端 int read_len=read(fd[0],buff,sizeof(buff)-1); if(read_len>0) printf("%s\n",buff); close(fd[0]); } exit(0); }(3)总结区别
| 有名管道 | 无名管道 | |
| 关系 | 有名(存在文件系统中) | 不在系统中 |
| 生命周期 | 随文件系统的销毁而销毁 | 随进程的销毁而销毁 |
| 进程关系 | 任意进程 | 父子进程、兄弟进程 |
| 创建方式 | mkfifo | pipe |
注意事项:
| ||
二、进程间通信的方式(PIC)
单工通信:单向性,确定方向后固定,不能相互传输数据
双工通信:双向性,同一时刻,两方可以相互传输数据,不同时刻也可以传
半双工通信:双向性,同一时刻,只能一方给另一方传输数据,例如:管道
进程间通信 的方式(简称:PIC):管道、共享内存、消息队列、信号量、套接字
(1)共享内存
共享内存为多个进程之间共享和传递数据提供了一种有效的方式。
①shmget 创建 / 获取一块共享内存段
函数类型:int shmget(key_t key,size_t size,int shmflg);
函数返回值:-1-->表示共享内存创建失败,如果是正数,表示该段的标识符,表示创建成功。
shmget函数:创建一块共享内存空间
key:表示内存段的唯一标识
size:表示创建的共享内存空间的大小,创建新共享空间时,该参数填正整数,获取已有共享空间时,该参数可以填0
shmflg:标志位,用来控制函数行为
创建标志:IPC_CREAT:如果不存在,则创建
IPC_EXCL:与IPC_CREAT配合使用,如果key已经创建,则报错并设置错误原因,给errno(linux系统中全体变量),保证全新创建。
权限控制:需要指定权限位(0777 0666 )八进制
ipcs:可以查看当前系统中进程间通信资源
测试:
#include<stdio.h> #include<stdlib.h> #include<unistd.h> #include<string.h> #include<pwd.h> #include<sys/wait.h> #include<signal.h> #include<sys/ipc.h> #include<sys/shm.h> int main() { int shmid=shmget((key_t)1234,128,IPC_CREAT|IPC_EXCL|0600); if(shmid==-1) { printf("shmget err!\n"); exit(1); } return 0; }②shmat,将共享内存段挂载(映射)到调用进程的虚拟地址空间
void* shmat(int shm_id,const void*shm_addr,int shmflg);
shmat函数作用:将一个已存在的共享内存段映射到地址空间中
shm_id:共享内存段的标识符,通常是shmget函数的返回值
shm_addr:指定共享内存段要附加到进程地址空间的哪个地址。NULL:让系统内存自动选择一块合适的,未使用的地址来附加到共享内存段;非NULL:(填具体位置)系统会将共享内存段附加到指定位置,如果指定位置不能使用,则报错,附加失败。
shmflg:控制附加操作的控制位
0:默认权限,根据shmget创建时的权限进行续写
SHM_RDONY:以只读的方式附加内存段,如果该进程尝试写入,则引入发段错误
SHM_EXEC:允许在共享内存段上执行代码
测试:
share.c
#include<stdio.h> #include<stdlib.h> #include<unistd.h> #include<string.h> #include<pwd.h> #include<sys/wait.h> #include<signal.h> #include<sys/ipc.h> #include<sys/shm.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); } strcpy(s,"hello");//写入一个数据s 字符串:hello return 0; }share1.c
#include<stdio.h> #include<stdlib.h> #include<unistd.h> #include<string.h> #include<pwd.h> #include<sys/wait.h> #include<signal.h> #include<sys/ipc.h> #include<sys/shm.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); } //strcpy(s,"hello");//写入一个数据s 字符串:hello printf("%s\n",s); return 0; }③练习共享内存
设置一个进程为 读取共享内存上的数值,持续读取,另外一个进程可以持续写入。在两个终端中使用
全双工:双向性
#include<stdio.h> #include<stdlib.h> #include<unistd.h> #include<string.h> #include<pwd.h> #include<sys/wait.h> #include<signal.h> #include<sys/ipc.h> #include<sys/shm.h> int main() { int shmid=shmget((key_t)1234,128,IPC_CREAT|0600); char buff[128]; 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); } while(1) { fgets(buff,128,stdin); strcpy(s,buff); if(strncmp(s,"end",3)==0) break; } }#include<stdio.h> #include<stdlib.h> #include<unistd.h> #include<string.h> #include<sys/ipc.h> #include<sys/shm.h> int main() { // key、内存大小 和写端完全一致 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); } while(1) { // 读取共享内存数据 printf("读到:%s", s); // 检测到 end 退出 if(strncmp(s,"end",3)==0) break; usleep(200000); // 轻微休眠,避免疯狂循环占用CPU } // 解除映射 shmdt(s); return 0; }(2)信号量
信号量:是操作系统用于进程间与互斥的关键机制(解决:多个进程对共享资源的竞争问题,避免数据冲突或不一致)。
原理:通过一个计数器对共享资源的访问。
信号量的本质:是一个受保护整数变量,其值反映了共享资源的可用数量,只能通过特定的原子操作(不可被中断的操作)修改。
原子操作:对信号量的修改(加、减),避免多进程并发修改导致数据不一致。
堵塞机制:当信号量的值为0时,试图获取信号量的进程会被诸塞,直到有其他进程释放资源。
信号量的类型:
①二值信号量:值只能时0和1,用于实现互斥(类似锁,一次只允许一个进程运行)
②计数信号量:值可以时任意非负数,同于控制对多个相同资源的并发访问。假设信号量为3,最大可支持3个进程哦同时运行。(信号量可以为4也可以,但是不要同时运行就行)
P、V 原语:信号量的行为由两个基本操作定义(称为“原语”确保原子性)
P操作:将信号量值减一,若结果继续执行,如果结果<0,进阻塞,放入信号量的等待的队列,拿取资源。
V操作:将信号量加一,若结果>=0,进程可继续执行,如果结果<0,从等待队列中唤醒一个进程,使其继续执行。
临界资源:同一时刻只允许一个进程访问的资源
临界区:访问临界资源的代码段
A.int semget(key_t key, int nsems, int semflg)
semget():用于获取信号量集的系统调用
key:用于表示信号量集的唯一性,确保不同进程能通过相同的Key访问同一信号量集
nsems:信号量的数量(要创建的信号量集中包含的信号量个数),若存在创建新信号量集,则nsems必须大于0;若是获取已存在的信号量集,则nsems可以设置为0
semflg:(标志位):控制信号量集的创建/获取行为。
IPC_CREAT:若对应的信号量不存在,则创建新集,若已存在,则会返回0
IPC_EXCL:与IPC_CREAT组合使用,若信号量集已存在,则创建失败。(确保获取新资源)
B.int semctl(int semid, int semnum, int cmd, ...)
semctl:用于控制操作信号量集,不同操作命令,需要传入不同类型的数据,所以用联合体共用一块内存。
semid:信号量集的标识符(semget()返回),指定要操作的信号量集。
semnum:信号量在集中的索引 (从0开始),指定要操作的单个信号量。
若对整个信号量集进程操作(IPC_RMID),则此参数忽略。
cmd:要执行的控制命令,决定semct的行为
SETVAL:设置semnum指定的信号量的初始值(需要配合第四个参数传递)
CETVAL:获取semnum指定的信号量的当前值(返回值为该信号量的值)
IPC_RMID:删除整个信号量集(所有进程都无法访问)
IPC_STAT:获取信号量集的状态信息,存入semid_ds的结构体(创建时间,权限)
IPC_SET:设置信号量集的属性,需要通过semid_ds的结构传递新属性
可变参数:(可选)根据cmd参数,可能需要传递一个union semnum类型的参数(需要自定义)
//union semun是semctl()系统调用专用联合体,用来给 semctl 传递参数。
union semun{
int val ;//用于SETVAL命令(设置信号量值)
struct semid_ds *buf;//IPC-STAT或者IPC_SET(状态信息)
unsigned short*array;//GETALL或者SETALL(批量操作)
struct seminfo*_buff;//用于IPC_INFO(系统限制信息)
}
C.int semop(int semid, struct sembuf *sops, size_t nsops)
semop:用于执行信号量的操作
semid:信号量集的标识
sops:指向struct sembu的结构体操作
struct sembuff{
unsigned short sem_num;//信号量在集中的索引
short sem_op;//操作类型+1 还是-1
short sem_flg;//操作系统,一般使用 SEM_UNION(确保进程异常退出时,系统撤销对信号量的修改,避免永久堵塞)
}
nsops:数组中nsops元素的数量(要执行的操作数量)
D.额外补充 lseek(int fd,off_t offset,int whence)
lseek()该函数可以改变文件中读写指针的位置
fd:表示要操作的文件
offset:偏移量(字节)
whence:起始位置
SEEK_SET:表示从文件开头开始
SEEK_CUK:表示从当前指针位置开始
SEEK_END:表示从文件结尾开始
(3)消息队列
消息队列的原理:是一种异步通信中间件,生产者把消息发送到队列,消费者从队列取消息处理,实现服务解耦、异步、削峰。
核心模型:生产者 → 队列 → 消费者。
核心角色:
①生产者 Producer:产生消息,发送给消息队列
②Broker:MQ 服务端,存储消息,管理队列、转发消息
③队列 Queue:消息的存储容器,先进先出 FIFO
④消费者 Consumer:从队列拉 / 接收消息,执行业务逻辑
核心工作原理:
a.生产者不直接调用消费者,把消息投递到 Broker 的队列;
b.Broker 把消息持久化保存到队列,等待消费;
c.消费者主动拉取(pull)或者 Broker 推送(push)消息给消费者;
d.消费者处理完成后,发送ACK 确认告诉 MQ:消息处理完毕;
e.MQ 收到 ACK,才删除这条消息;没收到 ACK,消息会重新投递。
三大核心能力原理:
解耦:服务 A 不用直接调用服务 B,A 发消息到 MQ 就返回,B 自己消费消息。A、B 可以独立上线、修改,互不依赖。
异步:A 不需要等待 B 处理完成,发完消息立刻返回,提升接口响应速度。例如:下单后发短信、生成订单日志,不阻塞下单主流程。
削峰填谷:流量突增时,大量消息先堆积在 MQ 队列,消费者按自己的处理速度慢慢消费,避免下游服务被瞬间大流量打垮。
注意出现问题:消息丢失、消息重复消费、消息积压
A.intmsgget(key_t key,intmsqflg)
key_t key :IPC 键值,用来唯一标识一个消息队列; 可以用ftok()生成 key;
特殊值IPC_PRIVATE,每次调用都创建全新私有消息队列。
msgget():创建或者获取一个消息队列
msgget():成功返回消息队列 ID,失败返回-1
msqflg:IPC_CREAT:不存在则创建;
存在直接打开 IPC_EXCL:必须和IPC_CREAT一起用;
如果队列已存在,直接报错返回‑1,防止重复创建。
B.intmsgsnd(intmsqid,constvoid*msqp,size_tmsqsz,intmsqflg)
函数作用:向 System V 消息队列发送一条消息,把用户空间的消息复制到内核的消息队列中。
int msqid:消息队列 ID,来自msgget()返回值。
const void *msqp:指向自定义消息结构体的指针。
size_t msqsz:mtext 有效数据的字节大小,不包含 mtype (long) 的大小。
int msqflg:标志
0:默认阻塞。队列满时,调用进程阻塞等待,直到队列有空位。 IPC_NOWAIT:非阻塞。队列满不等待,直接返回‑1,errno=EAGAIN。
#include <stdio.h> #include <string.h> #include <sys/types.h> #include <sys/ipc.h> #include <sys/msg.h> struct msgbuf { long mtype; char mtext[128]; }; int main(void) { key_t key = ftok("./test.c",100); //生成一个System V IPC 的 key 键值,用于消息队列、共享内存、信号量,给msgget()、 //shmget()、semget()使用。 //不同进程,传入同一个 pathname 和 proj_id,就会得到完全相同的 key_t 值,以此实现进程间定位同一个 //IPC 对象。 //返回值:成功返回 key_t 键;失败返回‑1。 int msgid = msgget(key, IPC_CREAT | 0666); struct msgbuf buf; //读取类型1的消息 msgrcv(msgid, &buf, sizeof(buf.mtext), 1, 0); printf("读到消息:%s\n", buf.mtext); msgctl(msgid, IPC_RMID, NULL); //删除消息队列 return 0; }C.ssize_t msgrcv(intmsqid,void*msgp,size_tmsqsz,longmsqtyp,intmsqflg)
函数作用:从 System V 消息队列中读取一条消息,将内核中的消息拷贝到用户缓冲区;读取成功后,该消息会从内核消息队列中移除。
msqid:消息队列 ID,来自msgget()返回值。
msgp:接收缓冲区指针,指向struct msgbuf结构体。
struct msgbuf{ long mtype; //消息类型 char mtext[128]; //消息数据 };
msqsz:mtext缓冲区大小,只算数据部分,不包含 long mtype。 如果队列里消息的数据长度 > msqsz,消息会被截断。
msqtyp:按类型选择接收消息(核心)
msqtyp = 0:读取队列第一条消息,不管什么类型;
msqtyp > 0:读取mtype 等于该值的消息;
msqtyp < 0:读取队列中mtype 小于等于 abs (msqtyp)的最小类型消息。
msqflg:标志
0:默认阻塞,没有匹配消息时,进程阻塞等待消息到来。
IPC_NOWAIT:非阻塞,没有匹配消息直接返回‑1,errno=ENOMSG。 MSG_NOERROR:消息数据超过 msqsz 时,自动截断,不报错。不加这个,超长消息会报错返回‑1。
D.intmsgctl(intmsqid,intcmd,structmsqid_ds *buf)
msgctl():控制消息队列
msgctl():成功返回 0,失败返回-1
msqid:消息队列 id,msgget()返回值。
cmd:命令,决定要做什么操作(三个最常用)
IPC_RMID:删除消息队列,内核立刻销毁队列,第三个参数buf传NULL。
IPC_STAT:获取消息队列状态信息,把队列信息存入buf指向的struct msqid_ds结构体 。IPC_SET:修改消息队列的权限、所有者等属性,从buf读取设置的值。