Linux||进程间通信

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 : 查看系统中的共享内存

共享内存的生命周期随内核:进程没了,共享内存任然存在,会一直存在到手动释放或者系统重启

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值