IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 系统运维 -> 进程间通信 -> 正文阅读

[系统运维]进程间通信

进程间通信介绍

进程间通信简称IPC(interprocess communication),进程间通信就是多个进程在交换信息。

进程间通信目的

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

进程间通信的本质

要让两个进程通信的前提是:让两个进程看到同一个资源。
这个资源可以是文件,也可以是内存。

进程通信方式

有三大类

  1. 管道
  2. system v IPC
  3. POSIX IPC

管道

管道的定义

管道是最古老的进行通信的一种方式。
进程与进程的数据交流通过管道。
管道其实是一个文件。

举个例子:
下面命令行的功能是显示bash的进程信息
在这里插入图片描述
具体实现逻辑是这样的:
在这里插入图片描述
问题来了:具体ps是怎么把数据写进管道的,grep又是怎么把数据读出来的?

后面讲管道种类会讲。

匿名管道

匿名管道只能用于具有亲缘关系之间的进程进行通信。
比如说:兄弟进程,父子进程,爷孙进程等等…

为什么叫匿名管道?
因为这个文件没有名字,只有文件描述符。

匿名管道的原理

之前说过,进程通信的本质是让两个进程看到同一份资源。
而管道是一个文件,因此匿名管道的原理就是让两个进程看到同一个文件即可。(文件描述符角度)系统角度是让两个进程看到同一个inode即可。

注:

  1. 虽然说管道是一个文件,但是我们在把数据往管道文件写的时候,管道文件的大小并不会发生变化,把数据从管道上读出来的时候,管道文件大小也不会发生变化。因此管道的读写是与IO无关的。(不会把数据写到硬盘上面)
  2. 为什么子进程可以指向和父进程同样的文件。之前讲过,没有发生写时拷贝的数据是只读的,只读的数据其实是共享的。这里没有发生写时拷贝,因此父子进程看到的是同一份文件
  3. 管道只能单向通信,如果要双向通信,就要开两个管道
  4. inode是文件系统的数据结构,因此两个进程是共有的。

pipe函数

在这里插入图片描述
fd是输出型参数。打开pipe之后fd数组里面会放着两个新打开的文件描述符

数组元素含义
fd[0]管道读端的文件描述符
fd[1]管道写端的文件描述符

记忆方法:0像一张嘴,可以读。1像一只笔,可以写


图示:

在这里插入图片描述

匿名管道使用步骤

1.父进程创建管道
在这里插入图片描述

2.fork子进程
在这里插入图片描述
3.父进程关闭读端(或者写端),子进程关闭写端(或者读端)
具体谁写谁读都可以,只要让管道是单向流通的就可以。

在这里插入图片描述

使用pipe实现一个管道通信

在这里插入图片描述

代码:

#include <stdio.h>
#include <unistd.h>
#include <string.h>
int main()
{
  int fd[2] = {0};
  
  //使用pipe之后自动打开了两个文件描述符,并放在了输出型参数fd数组里
  int res =  pipe(fd);
  if(res < 0)
  {
    perror("pipe");
    return 1;
  }
  
  pid_t id = fork();
  if(id == 0)
  {
    //child,write
    close(fd[0]);
    const char* msg = "i am child : i'm writing ...\n";
    int count = 0;
    while(count++ < 5)
    {
      write(fd[1], msg, strlen(msg));
      sleep(1);
    } 
  }
  else if(id > 0)
  {
    //father,read
    close(fd[1]);
    char buffer[100];
    while(1)
    {
      ssize_t s = read(fd[0], buffer, sizeof(buffer) - 1);
      if(s > 0)
      {
        buffer[s] = 0;
        printf("%s", buffer);
      }
      else if(s == 0)
      {
        printf("read file end\n");
        break;
      }
      else
      {
        printf("read error");
        break;
      }
    }
  }
  return 0;
}

文件描述符理解匿名管道(重点)

在这里插入图片描述
同一份资源在文件描述符角度指的是管道这个文件

内核角度理解pipe(重点)

在这里插入图片描述
注:struct files_struct 和struct file都是进程管理文件的数据结构。inode是文件系统的数据结构。进程间的同一份资源在内核角度是指inode

管道读写特点,4个实验(重点)

有四个。

1.当管道为空,读端还在一直读,那么读端就会阻塞。

#include <stdio.h>
#include <unistd.h>
#include <string.h>
int main()
{
  int fd[2] = {0};

  int res =  pipe(fd);
  if(res < 0)
  {
    perror("pipe");
    return 1;
  }
  
  pid_t id = fork();
  if(id == 0)
  {
    //child,write
    close(fd[0]);
    const char* msg = "i am child : i'm writing ...\n";
    int count = 0;
    while(1)
    {
      write(fd[1], msg, strlen(msg));
      sleep(5);
    } 
  }
  else if(id > 0)
  {
    //father,read
    close(fd[1]);
    char buffer[100];
    while(1)
    {
      ssize_t s = read(fd[0], buffer, sizeof(buffer) - 1);
      if(s > 0)
      {
        buffer[s] = 0;
        printf("%s", buffer);
      }
      else if(s == 0)
      {
        printf("read file end\n");
        break;
      }
      else
      {
        printf("read error");
        break;
      }
    }
  }
  return 0;
}

现象:
子进程写入的时候sleep(5)秒,父进程读取的时候,虽然没有加上sleep,但是他也跟着sleep了五秒之后才读取并打印出来。

这验证了我们的结论:当管道为空的时候,如果读端继续读,那么读端会阻塞等待

注:管道不为空的时候,就算读端继续读且写端不写了。读端也不会堵塞


2.当管道被写满时,写端还在继续写,那么写端就会被阻塞
(写端狂写)

代码:
现在我让写端疯狂写,不sleep了。读端每读一次sleep(1)。

#include <stdio.h>
#include <unistd.h>
#include <string.h>
int main()
{
  int fd[2] = {0};

  int res =  pipe(fd);
  if(res < 0)
  {
    perror("pipe");
    return 1;
  }
  
  pid_t id = fork();
  if(id == 0)
  {
    //child,write
    close(fd[0]);
    const char* msg = "i am child : i'm writing ...\n";
    int count = 0;
    while(1)
    {
      write(fd[1], msg, strlen(msg));
      //sleep(5);
      printf("child: %d\n", count++);
    } 
  }
  else if(id > 0)
  {
    //father,read
    close(fd[1]);
    char buffer[100];
    while(1)
    {
      ssize_t s = read(fd[0], buffer, sizeof(buffer) - 1);
      if(s > 0)
      {
        buffer[s] = 0;
        printf("%s", buffer);
        sleep(5);
      }
      else if(s == 0)
      {
        //printf("read file end\n");
        //break;
      }
      else
      {
        printf("read error");
        break;
      }
    }
  }
  return 0;
}

结果:
我们发现写端写到2255行之后就不写了。
因此可以验证结论:当管道满了之后,写端还想继续写的话,会被阻塞。

注:管道不满时,就算写端还想写,还是可以写的,不会堵塞
在这里插入图片描述


3.如果把写端fd关闭之后,读端读完管道之后,会读到end of file。
(先关写端,再关读端)

代码:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
int main()
{
  int fd[2] = {0};

  int res =  pipe(fd);
  if(res < 0)
  {
    perror("pipe");
    return 1;
  }
  
  pid_t id = fork();
  if(id == 0)
  {
    //child,write
    close(fd[0]);
    const char* msg = "i am child : i'm writing ...\n";
    int count = 0;
    while(1)
    {
      write(fd[1], msg, strlen(msg));
      //sleep(5);
      printf("child: %d\n", count++);
      if(count == 5)
      {
        close(fd[1]);
        break;
      }
    }
    exit(2);
  }
  else if(id > 0)
  {
    //father,read
    close(fd[1]);
    char buffer[100];
    while(1)
    {
      ssize_t s = read(fd[0], buffer, sizeof(buffer) - 1);
      if(s > 0)
      {
        buffer[s] = 0;
        printf("%s", buffer);
        sleep(5);
      }
      else if(s == 0)
      {
        //printf("read file end\n");
        //break;
      }
      else
      {
        printf("read error");
        break;
      }
      printf("father exit return : %d\n", s);
      if(s == 0) break;
    }
  }
  return 0;
}

结果
在这里插入图片描述
大致讲一下过程:子进程写了5次之后关闭了fd。然后父进程读取信息,第一次读了99个字符。睡眠5秒,第二次读了46个字符睡眠5秒,此时已经读完了,管道已经为空了。因此第三次读了end of file,即0值。


4.写端还在写的时候,读端把fd关闭了。写端的进程可能会被直接杀掉
(先关读端再关写端)

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
int main()
{
  int fd[2] = {0};

  int res =  pipe(fd);
  if(res < 0)
  {
    perror("pipe");
    return 1;
  }
  
  pid_t id = fork();
  if(id == 0)
  {
    //child,write
    close(fd[0]);
    const char* msg = "i am child : i'm writing ...\n";
    int count = 0;
    while(1)
    {
      write(fd[1], msg, strlen(msg));
      //sleep(5);
      printf("child: %d\n", count++);
    }
    exit(2);
  }
  else if(id > 0)
  {
    //father,read
    close(fd[1]);
    char buffer[100];
    int count = 0;
    while(1)
    {
      ssize_t s = read(fd[0], buffer, sizeof(buffer) - 1);
      if(s > 0)
      {
        buffer[s] = 0;
        printf("%s", buffer);
        sleep(5);
      }
      else if(s == 0)
      {
        printf("read file end\n");
        close(fd[0]);
        break;
      }
      if(count++ == 3) close(fd[0]);
    }
  }
  return 0;
}

结果:
跑着跑着子进程变成僵尸了,意味着子进程退出了。
在这里插入图片描述
大致讲一下过程:
首先子进程一直写,写满了管道之后,由于写端没有关闭fd,因此写端堵塞了。读端开始读,读到第四次的时候,我们手动把读端的fd关闭了。此时系统检测到读端不读了,因此写也没有必要了,直接杀掉了子进程。由于父进程还在running,因此子进程变成僵尸了。

这个子进程是被信号杀掉的。我们实验一下是什么信号
用waitpid回收子进程
在这里插入图片描述

是13号进程。
在这里插入图片描述
是SIGPIPE信号
在这里插入图片描述

管道5个特征

1.管道只能让有亲缘关系的进程通信
2.管道是流式服务。流式服务是和数据报服务相反的。数据报是一次固定拿一定的数量,而流式服务是你想拿多少就拿多少。
3.进程退出,管道释放。因此管道文件的生命周期和进程一样
4.内核会对管道操作进行同步和互斥。

互斥:我读的时候,你就别写了。你写的时候,我就别读了。
同步:

5.管道的通信是单向的。要实现双向就要两个管道。

用实例解释匿名管道

在这里插入图片描述
这个命令的执行过程是怎样的?

首先,who和grep这两个进程是兄弟进程,因此可以用管道。

首先创建一个管道。

然后who因为要写入管道,因此关闭读端。close(fd[0])
因为grep要读入管道,因此要关闭写端.close(fd[1])
(单向性决定)。

因为who本来是写到显示器的,现在要写到管道。因此需要重定向,使用dup2
因为本来grep是从显示器中读入的,现在要从管道读,也要重定向,使用dup2


命名管道

匿名管道的限制是只能在有亲缘关系的进程之间通信。
命名管道可以在不相关的进程之间交换数据

如果我们想在不相关的进程之间交换数据,可以使用FIFO文件来做这项工作,它经常被称为命名管道

  • 命名管道可以从命令行上创建,命令行方法是使用下面这个命令:
  mkfifo filename
  • 命名管道也可以从程序里创建,相关函数有:
int mkfifo(const char *filename,mode_t mode);

mode是创建的时候的权限,这个权限受umask影响。

创建fifo代码:

#include <stdio.h>
#include <sys/types.h>

int main()
{
  mkfifo("fifo", 0666);
}

结果:
在这里插入图片描述

用代码fifo实现通信程序

创建两个客户端
在这里插入图片描述
server端读,client写
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

注:
1.这两个进程是没有关系的。因为它们两个的bash不同。

2.fifo只是一个标志,没有发生IO行为,因此不管输入什么fifo的大小都是0
3.当read返回值大于0的时候,证明读取成功。
4.当read返回值等于0的时候,证明写端关闭了,读端读到了end of file

指令mkfifo

创建一个管道。


在这里插入图片描述

在这里插入图片描述


一边一直向fifo里面打印
在这里插入图片描述
一直循环打印
在这里插入图片描述

匿名管道和命名管道对比

  • 匿名管道由pipe函数创建并打开。

  • 命名管道由mkfifo函数创建,打开用open

  • FIFO(命名管道)与pipe(匿名管道)之间唯一的区别在它们创建与打开的方式不同,一但这些工作完成之后,它们具有相同的语义XXX

system V 共享内存

简单来说,就是很多个进程。它们的共享区经过映射指向同一块物理内存。
在这里插入图片描述

共享内存数据结构

共享内存可以在物理内存中存在很多份,因此要用ds来管理起来。

shmid_ds

在这里插入图片描述
shm_atime 上一次挂接时间
shm_dtime 上一次取消挂接时间
shm_ctime 上一次改变时间
shm_nattach 当前这块共享内存有多少个进程在挂接使用
shm_segsz 共享内存块的大小 shm_segments size

ipc_perm

在这里插入图片描述

  • key

每一个共享内存都有一个key,进程如何知道它挂接到哪一块共享内存了?就是根据这个key

  • mode
    创建的共享内存的权限

ftok

key_t ftok(const char *pathname, int proj_id)

ftok可以根据你提供的这两个参数,得到一个唯一的key值,用来表示这一块共享区。(原理可能是使用了字符串哈希)

注:只是拿到一个唯一的key,系统什么也没干


shmget

int shmget(key_t key, size_t size, int shmflg)
  • key
    创建共享内存。
    拿到一份内存之后,把这份内存编上号,号码是key
    在这里插入图片描述

  • size
    想申请的内存大小,系统只会为你申请你申请4096的整数倍。但是显示给你看的时候还是你申请的大小

  • shmflg
    有九个权限标识。
    常用的是IPC_CREAT IPC_EXCL。
    IPC_CREAT 没有这块内存就创建。
    IPC_EXCL 有这块内存就报错
    IPC_CREAT | IPC_EXCL 没有就创建,有就报错。因此创建的绝对是一块新的内存。

实验:如果存在了一块对应key的共享内存,就会返回这样的错误信息。
在这里插入图片描述

在这里插入图片描述

返回值

查找共享内存命令

ipcs -m

在这里插入图片描述
在这里插入图片描述
key 系统层面标识唯一性
shmid 系统层面定位共享内存
owner 拥有者
perms 权限
bytes 大小
nattch 挂接的数量

删除共享内存命令

ipcrm -m -shmid

在这里插入图片描述

注:共享内存是需要手动删除的,共享内存的生命周期随内核

shmctl删除共享内存

int shmctl(int shmid, int cmd, struct shmid_ds *buf)

这个接口有点复杂,我们只需要知道cmd填IPC_RMID,第三个参数填NULL即可。
在这里插入图片描述

IPC_RMID命令解析:

shmat

void * shmat(int shmid, const void *shmaddr, int shmflg)

这个接口用来给共享内存挂接进程的。shmaddr直接给NULL即可,不用关心。
shmflg是挂接方式,默认挂接方式是读写。因此给0即可。

关于shmaddr给null的解释:
在这里插入图片描述
关于shmflg给0的解释:
在这里插入图片描述
返回值:返回的是共享内存对应挂接进程的虚拟地址。和malloc一样性质,返回的都是虚拟地址。

我这里把返回的地址当成字符串了。
在这里插入图片描述

shmdt

用处:取消挂架

int shmdt(const void *shmaddr);

在这里插入图片描述
参数是挂接函数shmat的返回值。

在这里插入图片描述

共享内存没有同步和互斥

做一个实验:
让client端5秒输入数据进内存一次,让server端1秒从内存读一次数据。

发现结果:在client不写的时候,server端并不会等待,会一直打印老旧信息。

证明共享内存没有同步和互斥。

实验代码:
server端:

#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include "comm.h"
#include <sys/shm.h>
#include <unistd.h>
int main()
{
  key_t k = ftok(PATHNAME, PROJ_ID);
  printf("key : %x\n", k);

  int shmid = shmget(k,SIZE, IPC_CREAT | IPC_EXCL | 0666);

  if(shmid < 0)
  {
    perror("shmget");
    return 1;
  }

  printf("shmid:%d\n", shmid);

  
  char* s = (char*)shmat(shmid, NULL, 0);//挂接完成
  while(1)
  {
    sleep(1);//一秒读一次
    printf("%s\n", s);
  }

  shmdt(s);//取消挂接


  shmctl(shmid, IPC_RMID, NULL);


  return 0;
}

client端:

#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include "comm.h"
#include <sys/shm.h>
#include <unistd.h>
int main()
{
  key_t k = ftok(PATHNAME, PROJ_ID);
  printf("key : %x\n", k);

  int shmid = shmget(k,SIZE,IPC_CREAT);

  if(shmid < 0)
  {
    perror("shmget");
    return 1;
  }

  printf("shmid:%d\n", shmid);

   
  char* s = (char*)shmat(shmid, NULL, 0);//挂接完成

  char ch = 'a';
  for(; ch <= 'z'; ch++)
  {
    s[ch - 'a'] = ch;//client端写入数据,server端打印数据
    sleep(5);//五秒写一次
  }

  shmdt(s);//取消挂接


  return 0;
}


system V 消息队列

进程和进程之间用队列进行通信。

有很多消息队列,因此要用ds管理起来

在这里插入图片描述
里面有一个msg_perm 和shm_perm是同样类型的。

system V 信号量

信号量主要用于同步和互斥的,下面先来看看什么是同步和互斥。

进程互斥

因为要通信,所以要看到同一份资源。因为看到了同一份资源(这种资源被称为临界资源,我们把访问临界资源的代码叫临界区)。

有可能发生一种情况:我在写的时候你在读,这样可能造成数据不一致。

为了解决这个问题:要进程互斥。任何一个进程在操作的时候,其他进程都不能操作。

什么是system V

我们发现shm,msq,sem三种system V 通信方式里面的ds和接口参数都很像,这是因为system V就是一个标准,标准要求它们相似。

  系统运维 最新文章
配置小型公司网络WLAN基本业务(AC通过三层
如何在交付运维过程中建立风险底线意识,提
快速传输大文件,怎么通过网络传大文件给对
从游戏服务端角度分析移动同步(状态同步)
MySQL使用MyCat实现分库分表
如何用DWDM射频光纤技术实现200公里外的站点
国内顺畅下载k8s.gcr.io的镜像
自动化测试appium
ctfshow ssrf
Linux操作系统学习之实用指令(Centos7/8均
上一篇文章      下一篇文章      查看所有文章
加:2022-03-03 16:53:58  更:2022-03-03 16:54:47 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/16 3:26:11-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码