Linux下“进程间通信”相关内容整理分析

1. 进程间通信介绍

在这里插入图片描述
我们的目的:让不同的进程想办法看到一份公共的资源!

1.1 进程间通信的目的:

数据传输:一个进程需要将它的数据发送给另一个进程
资源共享:多个进程之间共享同样的资源。
通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。

1.2 进程间通信发展

管道
System V进程间通信
POSIX进程间通信

1.3进程间通信分类

管道

  1. 匿名管道pipe
  2. 命名管道

System V IPC

  1. System V 消息队列
  2. System V 共享内存
  3. System V 信号量

POSIX IPC

  1. 消息队列
  2. 共享内存
  3. 信号量
  4. 互斥量
  5. 条件变量
  6. 读写锁

2. 匿名管道

什么是管道?
父子进程是两个独立的进程,那么父子通信也是进程通信的一种

匿名管道:
只能单向通信
管道是面向字节流的!
仅限于父子通信

2.1小测试1

下面代码简述:
子进程每隔一秒进行写入内容
父进程不间断的读取内容

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>

int main()
{
    int pipefd[2] = { 0 };
    if (pipe(pipefd) != 0) {
        perror("pipe error!");
        return 1;
    }

    printf("pipefd[0]:%d\n", pipefd[0]);//3读取
    printf("pipefd[1]:%d\n", pipefd[1]);//4写入

    //我们想让父进程进行读取,子进程写入
    if (fork() == 0)
    {
        //子进程---写入
        close(pipefd[0]);

        const char* msg = "hello sakeww!";
        while (1) {
            write(pipefd[1], msg, strlen(msg));
            sleep(1);
        }

        exit(0);
    }
    //父进程--read
    close(pipefd[1]);
    while (1)
    {
        //我们没有让父进程sleep
        char buffer[64] = { 0 };
        //zero indicates end of file,如果此时read的返回值是0,意味着子进程关闭文件描述符了
        ssize_t s = read(pipefd[0], buffer, sizeof(buffer) - 1);
        if (s == 0) {
            printf("child quit ...\n");
            break;
        }
        else if (s > 0) {
            buffer[s] = 0;
            printf("child say to father# %s\n", buffer);
        }
        else {
            printf("read error...\n");
            break;
        }
    }


    return 0;
}

//pipefd[2]:是一个输出性参数!我们想通过这个参数读取到打开的两个fd
//int pipe(int pipefd[2]);

在这里插入图片描述

2.2小测试2

下面代码基本解释:
子进程不停顿的写入信息
父进程停顿一秒然后读取信息
接受信息(也就是在屏幕上显示信息)

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>

int main()
{
    int pipefd[2] = { 0 };
    if (pipe(pipefd) != 0) {
        perror("pipe error!");
        return 1;
    }

    printf("pipefd[0]:%d\n", pipefd[0]);//3读取
    printf("pipefd[1]:%d\n", pipefd[1]);//4写入

    //我们想让父进程进行读取,子进程写入
    if (fork() == 0)
    {
        //子进程---写入
        close(pipefd[0]);

        const char* msg = "hello sakeww!";
        while (1) {
            write(pipefd[1], msg, strlen(msg));
            //sleep(1);
        }

        exit(0);
    }
    //父进程--read
    close(pipefd[1]);
    while (1)
    {
        sleep(1);

        char buffer[64] = { 0 };
        ssize_t s = read(pipefd[0], buffer, sizeof(buffer) - 1);
        if (s == 0) {
            printf("child quit ...\n");
            break;
        }
        else if (s > 0) {
            buffer[s] = 0;
            printf("child say to father# %s\n", buffer);
        }
        else {
            printf("read error...\n");
            break;
        }
    }
    return 0;
}

在这里插入图片描述

2.3小测试3

下面代码简单解释:
子进程写入内容内容
父进程只是停留一秒,但是不读取内容

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>

int main()
{
    int pipefd[2] = { 0 };
    if (pipe(pipefd) != 0) {
        perror("pipe error!");
        return 1;
    }

    printf("pipefd[0]:%d\n", pipefd[0]);//3读取
    printf("pipefd[1]:%d\n", pipefd[1]);//4写入

    if (fork() == 0)
    {
        //子进程---写入
        close(pipefd[0]);

        const char* msg = "hello sakeww!";
        int count = 0;
        while (1) {
            write(pipefd[1], "a", 1);
            count++;
            printf("count:%d\n", count);
        }

        exit(0);
    }
    //父进程--read
    close(pipefd[1]);
    while (1)
    {
        sleep(1);
    }
    return 0;
}

在这里插入图片描述
65536/1024=64
写满64kb的时候,write就不再写入了,为什么?
因为管道有大小!

当write写满的时候,为什么不写了?
当我们的缓冲区写满的时候,我们还是可以写的,因为可以把之前没有读取的数据进行覆盖,这个是在软件设计上是可以实现的,但是为什么不写了?
因为要让reader来读,
不写了的本质是:我要等对方来读!

管道自带同步机制,原则性写入
当缓冲区写满的时候,我们需要对方来读,然后才能写
经过测试,只有当我们读取到一定大小的内容(4kb),我们才可以继续写入

2.4小测试4

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>

int main()
{
    int pipefd[2]={0};
    if(pipe(pipefd)!=0){
        perror("pipe error!");
        return 1;
    }

    printf("pipefd[0]:%d\n",pipefd[0]);//3读取
    printf("pipefd[1]:%d\n",pipefd[1]);//4写入
    
    //我们想让父进程进行读取,子进程写入
    if(fork()==0)
    {
        //子进程---写入
        close(pipefd[0]);

        const char *msg="hello sakeww!";
        int count = 0;
        while(1){
            write(pipefd[1],msg,strlen(msg));
            sleep(10);
        }
        exit(0);
    }
    //父进程--read
    close(pipefd[1]);
    while(1)
    {
        char c[64]={0};
        ssize_t s = read(pipefd[0],&c,sizeof(c));
        c[s]=0;

        printf("father take:%s\n",c);
    }
    return 0;
}

在这里插入图片描述
输出描述:
开始的时候直接输出前三行
后续每间隔十秒输出一行

当我们实际写入的时候
我们写入缓冲区的内容数据,有数据就读,没有数据就等待
即:自动同步机制

2.5小测试5

下面代码简单描述:
子进程写入一个数据,等待十秒,然后退出
父进程不间断的读取数据,当读取的内容没有时,然后退出


#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>

int main()
{
    int pipefd[2]={0};
    if(pipe(pipefd)!=0){
        perror("pipe error!");
        return 1;
    }

    printf("pipefd[0]:%d\n",pipefd[0]);//3读取
    printf("pipefd[1]:%d\n",pipefd[1]);//4写入
    
    //我们想让父进程进行读取,子进程写入
    if(fork()==0)
    {
        //子进程---写入
        close(pipefd[0]);

        const char *msg="hello sakeww!";
        int count = 0;
        while(1){
            write(pipefd[1],msg,strlen(msg));
            sleep(10);
            break;
        }
        close(pipefd[1]);
        exit(0);
    }
    //父进程--read
    close(pipefd[1]);
    while(1)
    {
        char c[64]={0};
        ssize_t s = read(pipefd[0],&c,sizeof(c));
        if(s>0){
            c[s]=0;
            printf("father take:%s\n",c);
        }
        else if(s==0){
            printf("write quit...\n");
            break;
        }
        else break;
    }
    return 0;
}

在这里插入图片描述.
输出描述:
直接输出前三行
然后等待十秒后,出现第四行,并且退出

2.6小测试6

下面代码简单描述:
子进程不间断的写入
父进程读取一次内容,然后直接退出

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>

int main()
{
    int pipefd[2]={0};
    if(pipe(pipefd)!=0){
        perror("pipe error!");
        return 1;
    }

    printf("pipefd[0]:%d\n",pipefd[0]);//3读取
    printf("pipefd[1]:%d\n",pipefd[1]);//4写入
    
    //我们想让父进程进行读取,子进程写入
    if(fork()==0)
    {
        //子进程---写入
        close(pipefd[0]);

        const char *msg="hello sakeww!";
        int count = 0;
        while(1){
            write(pipefd[1],msg,strlen(msg));
            sleep(10);
        }
        close(pipefd[1]);
        exit(0);
    }
    //父进程--read
    close(pipefd[1]);
    while(1)
    {
        sleep(10);
        char c[64]={0};
        ssize_t s = read(pipefd[0],&c,sizeof(c));
        if(s>0){
            c[s]=0;
            printf("father take:%s\n",c);
        }
        else if(s==0){
            printf("write quit...\n");
            break;
        }
        else break;

        break;
        close(pipefd[0]);
    }
    return 0;
}

在这里插入图片描述
当我们的读端关闭,写端还在写入,此时站在OS的层面,是不合理的!
已经没有人读了,你还在写入,本质就是在浪费OS资源,OS会直接终止写入进程!
OS给目标发送信号SIGPIPE!

在这里插入图片描述

当子进程异常终止的时候,父进程是可以读取到终止信号的

总结:
管道的四种情况:

  1. 读端不读或者读的慢,写端要等读端
  2. 读端关闭写段,写端收到SIGPIPE信号直接终止
  3. 写端不写或者写的慢,读端要等写端
  4. 写端关闭,读端读完全部pipe内部的数据然后再读,会读到0,表明读到文件结尾!

匿名管道,五个特点:
a. 管道是一个只能单向通信的通信通道
b. 管道是面向字节流的
c. 仅限于父子通信–具有血缘关系的进程进行进程间通信
d. 管道自带同步机制,原子性写入
e. 管道的声明进程时随进程的!
管道是文件吗?
如果一个文件只被当前进程打开,相关进程退出了(会自动递减struct file的ref引用计数),被打开的文件呢?
会被OS自动关闭!当ref为零时

为了解决匿名管道只能父子通信,引入了命名管道

3. 命名管道

3.1引入

管道应用的一个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信。
如果我们想在不相关的进程之间交换数据,可以使用FIFO文件来做这项工作,它经常被称为命名管道。
命名管道是一种特殊类型的文件

在这里插入图片描述

在这里插入图片描述
问:
我们通常标识一个磁盘文件,我们用什么方案呢?
路径/文件名(具有唯一性吗?具有)

我们可以让A进程在磁盘写入
然后用B进程在磁盘进行读取
.
我们可以理解为:echo这个进程进行了写入,然后cat这个进程进行了读取

在这里插入图片描述
问题:
A和B如何看到同一份资源呢?== A和B如何看到并打开同一个文件呢?
路径/文件名 具有唯一性

所以现在有两个需求?

  1. 存在一个文件:当他被打开时,他的数据不要刷新到磁盘上,而只是在内存中所谓临时数据进行保存;
  2. 这个数据在磁盘也要有一个文件名,方便两个进程使用路径+文件名的方式看到同一份资源

3.2创建命名管道

3.2.1准备工作

在这里插入图片描述
在这里插入图片描述
问题:
我们给我们的fifo设置的权限是:0666,为什么给我们生成的是0644?
你在创建的时候,它本身是要收到系统umask限制的

在这里插入图片描述
紧接着我们继续执行./server ,系统会让我们不要在执行了
在这里插入图片描述
当我们下一次需要执行./server时,我们需要新的fifo,所以,,,

一旦我们具有了一个命名管道,此时,只需要让通信双方按照文件操作即可!
此处推荐使用系统接口,
系统调用接口没有缓冲区,没有用户层缓冲区,这个数据将来调用read,write就是直接在管道文件里面操作,不会有什么中间层干扰,这是一个干净的通信方式

3.2.2制作

client.c

#include"commit.h"//此时两个程序就能看到同一份文件了      
#include<string.h>      
      
int main()      
{      
  //此时不用创建fifo,我们只需要获取即可      
  int fd = open(MY_FIFO,O_WRONLY);//不需要O_CREAT      
  if(fd<0){      
    perror("open");      
    return 1;      
  }      
      
  //业务逻辑      
  while(1){      
    printf("请输入# ");                                                                     
    fflush(stdout);      
    char buffer[64]={0};//建立缓冲区                                                      
    //先把数据从标准输入拿到我们的client进程内部                                          
    ssize_t s = read(0,buffer,sizeof(buffer)-1);                                          
    if(s>0)                                                                               
    {                                                                                     
      buffer[s]=0;                                                                        
      printf("%s\n",buffer);                                                              
                                                                                          
      //拿到了数据                                                                        
      write(fd,buffer,strlen(buffer));//要不要-1,不需要。因为管道也是文件                
    }                                                                                     
  }                                                                                       
                                                                                          
                                                                                          
  close(fd);                                                                              
  return 0;                                                                               
} 

server.c

#include"commit.h"    
int main()    
{    
  if(mkfifo(MY_FIFO,0666)<0){ //管道文件的建立    
    perror("mkfifo");    
    return 1;    
  }    
    
  //只需要文件操作即可    
  int fd = open(MY_FIFO,O_RDONLY);    
  if(fd<0){    
    perror("open");    
    return 2;    
  }    
    
  //业务逻辑,可以进行对应的读写    
  while(1){//c89 90没有true和false bool类型
    char buffer[64]={0};//读取内容
    ssize_t s = read(fd,buffer,sizeof(buffer)-1);//-1目的:不想让他将缓冲区打满             
    //键盘输入的时候,\n也是输入字符的一部分    
    if(s>0){    
      //读取成功    
      buffer[s]=0;    
      printf("client# %s\n ",buffer);    
    }    
    else if(s==0){    
      //对方关闭    
      printf("client quit...\n");    
      break;    
    }    
    else{    
      //error    
      perror("read");    
      break;    
    } 
  }
  close(fd);
  return 0;
}  

Makefile

在这里插入代码片.PHONY:all    
all:client server    
# 上两行:当你输入make会同时生成两个可执行程序 client server    
    
client:client.c    
  gcc -o $@ $^    
    
server:server.c    
  gcc -o $@ $^    
    
.PHONY:clean    
clean:    
  rm -f client server fifo     

commit

#pragma once
#include<stdio.h>    
#include<sys/stat.h>    
#include<sys/types.h>    
#include<fcntl.h>  
#include<unistd.h>//close   
    
#define MY_FIFO "./fifo"

在这里插入图片描述
你在输入过程中,当你输入错误时,backspace是删除不了的
可以用 ctrl+ backspace 删除自己输入错误的

在这里插入图片描述
运行图片描述:
先执行./server
后执行./client
此时client框内有个请输入#,server框内是空白行
当你在client框内输入字符时,
回车
client显示 请输入# nihao (下一行)nihao
server显示 client# nihao

当你在client 输入: ctrl+c 会直接退出
并且:server 输出 client quit… 并直接退出

3.2.3 加点东西

因为命名管道也是基于字节流的,所以实际上,信息传递的时候,是需要通信双方定制“协议的”
在这里插入图片描述
延伸:Linux下有个包是:当你输入一个特定的指令时,你的屏幕上会显示 一个小火车在动
同理:上述也可以执行这样的

3.2.4 再加点

在server.c 里面加上这个
在这里插入图片描述
显而易见:当我们输入的时候,reverse窗口会一直不显示,等50秒之后才会显示
在这里插入图片描述

一个延伸小问题:
为什么我们之前的pipe叫做匿名管道,为什么现在的fifo叫做命名管道呢?
因为人家有名字,为什么呢?
为了保证不同的进程看到同一个文件,必须有名字!这是他们实现共享的方式

匿名管道为什么叫匿名呢?
这个文件没有名字,
为什么这个文件就可以没有名字呢?
因为他是通过父子继承的方式,看到同一份资源,不需要名字来标识同一份资源!

消息队列

接口认识:

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

共享内存

引入:

上面的都是基于文件的通信方式

SystemV标准的进程间通信方式

OS层面专门为进程间通信设计的一套方案
谁设计的?—计算机科学家+程序员
要不要给用户用?—肯定要!
如果要给用户用,那么用什么方式给用户用?
OS不相信任何用户,给用户提供功能的时候,采用系统调用!

SystemV 进程间通信,一定会存在专门用来通信的接口(System call)

就需要有个人,组织机构,等来进行定制标准!
在同一主机内的进程间通信方案:
system V 方案!


进程间通信的本质:先让不同的进程看到同一份资源
system V

  1. 共享内存
  2. 消息队列(有点落伍)
  3. 信号量

前两个以传送数据为主要目的
信号量:以实现进程间同步或互斥为目的

共享内存:

引入:

在这里插入图片描述


  1. 通过某种调用,在内存中创建一份内存空间
  2. 通过某种调用,让进程“挂接”到这份新开辟的内存空间上!
    可以让参与通信的多个进程挂接到同一份物理内存
    此时达到了:
    让不同进程看到了同一份资源!!!

这种方案就叫做共享内存

当我们不用共享内存的时候:
3. 去关联(去挂接)
4. 释放共享内存


准备工作:

1.在任何一个时间段,OS内可不可能存在多个进程,同时使用不同的共享内存来进行进程间通信呢?
答:有可能,
所以:共享内存在系统中可能存在多份!
我在进行进程间通信的同时,有没有可能我不想通信了,我想释放内存,我在释放内存的时候,别人来通信了,建立共享内存了
OS要不要管理这些不同的共享内存呢?必须要!
那么如何管理呢?先描述,在组织

2.你如何保证,两个或者多个进程,看到的是同一个共享内存呢?
共享内存一定要有一定的标识唯一性的ID,方便让不同的进程能识别统一个共享内存资源!
这个“ID”应该在哪里?
描述共享内存的数据结构中!

这个唯一的标识符,用来进行进程间通信的,本质:让不同的进程看到同一份资源
本质的前提:你需要先让不同的进程,看到同一个ID

这个ID是需要 :由用户自己设定的!

认识接口:

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述


在这里插入图片描述
在这里插入图片描述


共享内存的整个生命周期

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

简单测试逻辑

测试:

在这里插入图片描述
在这里插入图片描述

问题:

在这里插入图片描述


在这里插入图片描述


在这里插入图片描述

信号量

接口认识:

在这里插入图片描述

什么是信号量?

管道,匿名or命名,贡献内存,消息队列:都是以传输数据为目的的!

信号量不是以传输数据为目的的!
以通过共享“资源”的方式,来达到多个进程的同步和互斥的目的!

例子:
信号量的本质:是一个计数器,类似int count ;
衡量临界资源中资源数目的

感性认识:

1.什么时临界资源?
凡是被多个执行流同时能够访问的资源就是临界资源!

资源被多个进程同时访问的:
同时向显示器打印,这个显示器就是临界资源。
进程间通信的时候,管道,共享内存,消息队列等 都是临界资源
管道,内部提供了一些保护机制,将临界资源保护起来了
共享内存就是典型的临界资源,没有人保护,所以有可能出现,你写了一半就被我读走了的情况,这叫做数据不一致的问题。

问题:
凡是要进程间通信,必定要引入被多个进程看到的资源(通信需要),同时,也造就了引入一个新的问题,临界资源的问题。

2.什么时临界区?
进程的代码可是有很多的,其中,用来访问临界资源的代码,就叫做临界区
server.c
在这里插入图片描述
client.c
在这里插入图片描述

3.什么时原子性?
一件事情要么不做,要做就做完,没有中间态。

非原子性?
当我们进行某件事的时候,他有中间过程。

在多进程,父进程+子进程

4.什么是互斥?
在任意一个时刻,只能允许一个执行流进入临界资源,执行他自己的临界区

  • 35
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 25
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 25
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值