Linux进程间通信
文章目录
一:进程间通信介绍
进程之间具有独立性,每个进程都有自己的虚拟地址空间,两个进程之间互相不了解对方的虚拟地址空间中的数据内容
进程间通信需要"介质" ,两个进程都能访问到的公共资源
借助文件就可以完成进程间通信,最简单的进程间通信的方法
1.进程间通信目的
- 1.数据传输
- 2.资源共享
- 3.通知事件
- 4.进程控制
2.进程间通信的分类
- 管道
- System V进程间通信
System V 消息队列\ System V 共享内存\ System V 信号量
- POSIX 进程间通信
消息队列 \共享内存 \信号量 \互斥量 \条件变量\ 读写锁
System V 和 POSIX 两种不同的标准
二:操作系统专门提供的进程间通信方式
【通信方式介绍】
- 1.匿名管道
pa aux | grep test
此命令中的 | 既为匿名管道,前面进程的输出做为后面一个进程输入
- 2.命名管道
- 3.消息队列
- 4.共享内存
- 5.信号量
最重要的进程间通信方式:网络
三:管道
- 管道:从一个进程到另外一个进程的数据流叫做一个"管道"
who | wc -l: 数一数who有多少行
【管道特点】
- 只能单向操作
【管道与父子进程】
- 创建子进程的时候,子进程会继承父进程的文件描述符表,此时子进程也能访问到同一个管道
- 父进程往管道中写数据的时候,子进程就能从管道中读取数据,反之亦然
- 管道中的数据一旦被读了之后,就相当于出队列了
- 假设有多个进程同时去尝试读,只有一个进程能读到数据,其他进程就读不到.管道内置了“同步互斥机制”不会出现管道一个人读一半的情况
【管道使用的注意事项】
1.多个进程同时去读写管道,数据不会发生错乱
2.如果管道为空,尝试读,就会导致在read函数处阻塞
3.如果管道满了,尝试写,就会导致write函数处阻塞
1.匿名管道:必须用于具有亲缘关系的两个进程之间(父子进程)
【头文件】include <unistd.h>
- 功能:创建一无名管道
【原型】 int pipe(int fd[2]);
- 参数fd:文件描述符数组,其中fd[0]表示读端, fd[1]表示写端
返回值:成功返回0,失败返回错误代码
【总结】
- 管道就是内核中的一块内存(构成一个队列),使用一对文件描述符来访问这个内存,读文件描述符就是从队列中取数据;写文件描述符就是往队列中插入数据
【示例代码】
- 管道的使用
#include<stdio.h>
#include<unistd.h>
#include<string.h>
int main(){
//使用pipe函数创建一对文件描述符,通过这一对文件描述符就能
//操作内核中的管道
int fd[2];
int ret=pipe(fd);//管道的使用
if(ret<0){
perror("pipe");
return 1;
}
//fd[0]-->读数据
//fd[1]-->写数据
char buf[1024]="hehe";
write(fd[1],buf,strlen(buf));
char buf_output[1024]={0};
ssize_t n=read(fd[0],buf_output,sizeof(buf_output)-1);
//sizeof(buf_output-1)的原因是防止内存访问越界
buf_output[n]='\0';//double check
//C风格的字符串以\0结束
printf("%s\n",buf_output);
//管道使用完成后,需要及时关闭文件描述符
close(fd[0]);
close(fd[1]);
return 0;
}
【管道与父子进程】
#include<stdio.h>
#include<unistd.h>
#include<string.h>
int main(){
//使用pipe函数创建一对文件描述符,通过这一对文件描述符就能
//操作内核中的管道
int fd[2];
int ret=pipe(fd);//管道的使用
if(ret<0){
perror("pipe");
retrurn 1;
}
//fd[0]-->读数据
//fd[1]-->写数据
ret=fork();
if(ret>0){
//写数据
char buf[1024]="hehe";
write(fd[1],buf,strlen(buf));
}else if(ret==0){
//读数据
char buf_output[1024]={0};
read(fd[0],buf_output,sizeof(buf_output)-1);
printf("child read:%s\n",buf_output);
}else{
perror("fork");
}
//管道使用完成后,需要及时关闭文件描述符
close(fd[0]);
close(fd[1]);
return 0;
}
子进程复制了父进程的PCB和文件描述符表
创建子进程的时候,子进程会继承父进程的文件描述符表,此时子进程可以和父进程共享同一个管道
父进程写管道的时候,子进程就能从管道中读取出数据
管道中的数据一旦被读取之后,就相当于出队了.不能被再次读取
【同一份资源,两个进程都去读】
#include<stdio.h>
#include<unistd.h>
#include<string.h>
int main(){
//使用pipe函数创建一对文件描述符,通过这一对文件描述符就能
//操作内核中的管道
int fd[2];
int ret=pipe(fd);//管道的使用
if(ret<0){
perror("pipe");
retrurn 1;
}
//fd[0]-->读数据
//fd[1]-->写数据
ret=fork();
if(ret>0){
//写数据
char buf[1024]="hehe";
write(fd[1],buf,strlen(buf));
char buf_output[1024]={0};
read(fd[0],buf_output,sizeof(buf_output)-1);
printf("father read:%s\n";buf_output);
}else if(ret==0){
//读数据
char buf_output[1024]={0};
read(fd[0],buf_output,sizeof(buf_output)-1);
printf("child read:%s\n";buf_output);
}else{
perror("fork");
}
//管道使用完成后,需要及时关闭文件描述符
close(fd[0]);
close(fd[1]);
return 0;
}
//只有一个进程可以读到hehe
//如果父进程等待1秒,子进程执行了read则父进程读不到数据一直
//“卡”住,不能结束,且出现僵尸进程(子进程已结束,父进程未等待)
- 多个进程同时去尝试读,只有一个进程能够读到数据,其他进程就读不到(由图可以看出只有一个进程读到了管道中的数据)
gdb attach [进程id] :调试正在运行中的程序
bt
:查看调用栈
两个进程分别创建管道,两个进程都创建了自己的管道无法完成通信
命名管道的生命周期也是跟随进程。myfifo这个文件仅仅是一个入口,管道的本体仍然是内核中的一个内存。生命周期其实是围绕着这个内核中的内存来讨论的
管道内置了"同步互斥机制"不会出现两个进程个读一半数据的情况
【特点】
- 1.多个进程同时去读写管道,数据会发生错乱
- 2.如果管道为空,尝试读,就会在read函数处阻塞
- 3.如果管道满了,尝试写,就会在write函数处阻塞
【管道大小】
默认:64k
【匿名管道特点】
- 只能用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信;通常,一个管道由一个进程创建,然后该进程调用fork,此后父\子进程之间就可应用该管道。
- 管道提供流式服务(面向字节流)
- 一般而言,所有引用进程退出.管道释放,所以管道的生命周期随进程
- 一般而言,内核会对管道操作进行同步与互斥
- 管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道 ;
网线;一般8根线(造成冗余,防止一根出故障导致网线失效),接4根也能行(其他4根功能相同)
【总结】
- 两个进程分别创建管道,两个管道之间 没有任何联系,两个进程之间无法进行通信.
- 父进程创建子进程,子进程继承 父进程的文件描述符 ,父子进程公用同一个管道.两个进程使用同一个管道才能完成进程间通信。
- 创建孙子进程,和父子进程同享一个管道,可以对进程进行读写操作
四:命名管道
1.创建一个命名管道
$mkfifo filename
- 创建了一个管道文件
#include<stdio.h>
#include<unsed.h>
#include<fcntl.h>
int main(){
int fd=open("./myfifo",O_RDONLY);
if(fd<0){
perror("reader.c open");
return 1;
}
while(1){
char buf[1024]={0};
ssize_t n=read(fd,buf,sizeof(buf)-1);
if(n<0){
perror("read");
return 1;
}
buf[n]='\0';
printf("[reader.c]%s \n",buf);
}
close(fd);
return 0;
}
- 所有的写端关闭,此时读端的read才能返回0
2.命名管道特点
- 命名管道的生命周期也是跟随进程.myfifo 这个文件仅仅是一个入口,管道的本体仍然是内核中的一个内存.生命周期其实是围绕这个内核中的内存来讨论的
3.管道实现简单单方面交流
- 首先要创建一个管道文件,然后通过管道来实现简单的交流(从写端写入,从读端读出)
$mkfifo myfifo
【读端】
#include<stdio.h>
#include<unistd.h>
#include<fcntl.h>
int main(){
int fd=open("./myfifo",O_RDONLY);
if(fd<0){
perror("reader.c open");
return 1;
}
while(1){
char buf[1024]={0};
ssize_t n =read(fd,buf,sizeof(buf)-1);
if(n<0){
//读端失败
perror("read");
return 1;
}
if(n==0){
//var有写端关闭,读端已经读完了
printf("read done!\n");
//所有的写端关闭,此时读端的read才返回0
return 0;
}
buf[n]='\0';
printf("[reader.c]%s\n",buf);
}
close(fd);
return 0;
}
【写端】
#include<stdio.h>
#include<unistd.h>
#include<fcntl.h>
#include<string.h>
int main(){
int fd=open("./myfifo",O_WRONLY);
if(fd<0){
perror("open error");
return 1;
}
while(1){
printf("->");
fflush(stdout);
char buf[1024]={0};
//让用户输入一个字符串到 buf 中,然后再写
read(0,buf,sizeof(buf)-1);
write(fd,buf,strlen(buf));
}
close(fd);
return 0;
}
五:共享内存(System V 共享方式)
共享内存函数
- shmget 函数
功能:用来创建共享内存
原型 int shmget(key_t key, size_t size, int shmflg);
参数
key:这个共享内存段名字
size:共享内存大小
shmflg:由九个权限标志构成,它们的用法和创建文件时使用的mode模式标志是一样的
返回值:成功返回一个非负整数,即该共享内存段的标识码;失败返回-1
- shmat 函数
功能:将共享内存段连接到进程地址空间
原型 void *shmat(int shmid, const void *shmaddr, int shmflg);
参数
shmid: 共享内存标识
shmaddr:指定连接的地址
shmflg:它的两个可能取值是SHM_RND和SHM_RDONLY
返回值:成功返回一个指针,指向共享内存第一个节;失败返回-1
- 说明
shmaddr为NULL,核心自动选择一个地址
shmaddr不为NULL且shmflg无SHM_RND标记,则以shmaddr为连接地址。
shmaddr不为NULL且shmflg设置了SHM_RND标记,则连接的地址会自动向下调整为SHMLBA的整数倍。公式:
shmaddr - (shmaddr % SHMLBA)
shmflg=SHM_RDONLY,表示连接操作用来只读共享内存
- shmdt 函数
功能:将共享内存段与当前进程脱离
原型 int shmdt(const void *shmaddr);
参数 shmaddr: 由shmat所返回的指针
返回值:成功返回0;失败返回-1
注意:将共享内存段与当前进程脱离不等于删除共享内存段
- shmctl 函数
功能:用于控制共享内存
原型 int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数
shmid:由shmget返回的共享内存标识码
cmd:将要采取的动作(有三个可取值)
buf:指向一个保存着共享内存的模式状态和访问权限的数据结构
返回值:成功返回0;失败返回-1
【进程和物理内存】
- 两个进程不共享内存的时候,各自的虚拟地址指向不同的物理内存
- 两个进程实现共享内存之后,各自的页表指向相同的一块物理内存,从而实现了信息的共享
相比于管道,共享内存更加高效,直接访问内存即可完成通信
而管道涉及用户态和内核态之间的数据相互拷贝,效率比较低
1.共享内存的使用方式
- 1.在内核中先创建出共享内存对象
- 2.多个进程附加到这个共享内存对象上
- 3.就可以直接读写这个共享内存了
#include<stdio.h>
#include<unistd.h>
#include<sys/shm.h>
int main(){
//shmget:shared memory get(获取)
key_t key=ftok(".",0x1);
if(key==-1){
perror("ftok");
return 1;
}
printf("key:%d\n",key);
//使用格式:int shmget(key_t key,size_t size,int shmflg);
int ret=shmget(key,1024,IPC_CREAT | IPC_EXCL|0666);
//权限0666
if(ret<0){
perror("shmget");
return 1;
}
printf("ret=%d\n",ret);
return 0;
}
在内核中可以同时包含多个共性内存对象,使用不同的key进行区分.
ipcs -m
- 查看系统中的共享内存
共享内存的生命周期随内核,共享内存会一直存在到手动释放或者系统重启
2.共享内存使用步骤
- 1.创建/打开共享内存对象
- 2.附加到共享内存对象上(shmat–>attch,用法与malloc十分相似)
- 3.使用共享内存
【myshm.h】
#pragma once
#include<stdio.h>
#include<unistd.h>
#include<sys/shm.h>
static int CreateShm(){
//1.static修饰全局变量:改变变量的作用域
//2.修饰函数改变作用域
//3.修饰局部变量:修改生命周期
//4.C++中修饰类中的变量,使得变量变成类的属性
//5.修饰成员函数,变成类的方法
//shmget:shared memory get(获取)
key_t key=ftok(".",0x2);
//"."确实存在的路径
if(key==-1){
perror("ftok");
return 1;
}
printf("key=%d\n",key);
int ret=shmget(key,1024,IPC_CREAT |0666);
//权限0666
//IPC_EXCL可有可无
if(ret<0){
perror("shmget");
return 1;
}
printf("ret=%d\n",ret);
return ret;
}
【readr.c】
#include"myshm.h"
int main(){
//从共享内存中读数据
//1.创建打开共享内存对象
int shmid=CreateShm();
//2.附加到共享内存上
//void *shmat(int shmid, const void *shmaddr, int shmflg);
char *p=(char*)shmat(shmid,NULL,0);
//3.直接使用
printf("reader:%s\n",p);
return 0;
}
【writer.c】
#include"myshm.h"
#include<string.h>
int main(){
//往共享内存中写数据i
//1.创建/打开共享内存对象
int shmid=CreateShm();
//2.附加到共享内存对象
char *p=(char*)shmat(shmid,NULL,0);
//3.直接使用
strcpy(p,"将军的荣耀!\n");
return 0;
}
【运行结果】
- 在C语言中多个.c 包含同一个.h 文件中的一个函数的时候,会报出重定义的错误
【改正方法】:
- 用inline(c++)或者static修饰函数
一般在头文件中声明,在 .c文件中实现
用inline修饰的时候,当别的.c文件调用的时候,直接将函数的代码复制当调用的地方
在内核中可以创建很多个共享内存对象,使用不同的key进行区分
key_t 就是一个数字
ipcs -m
: 查看系统中的共享内存
共享内存的生命周期随内核:进程没了,共享内存任然存在,会一直存在到手动释放或者系统重启