Linux进程间通信(一)

简介: Linux进程间通信

一、认识进程间通信

1.1 概念

进程间通信简称IPC(Interprocess communication),进程间通信就是在不同进程之间传播或交换信息


1.2 通信目的

数据传输: 一个进程需要将它的数据发送给另一个进程

资源共享: 多个进程之间共享同样的资源

通知事件: 一个进程需要向另一个或一组进程发送消息,通知其发生了某种事件,比如进程终止时需要通知其父进程

进程控制: 有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变

1.3 通信本质

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


由于进程具有独立性,所以各个进程若想进进行通信一定要借助第三方资源。若可以对第三方资源进行写入或读取数据,就可以实现进程间通信。


e7f49cca316f46a998d482fc3dbbe76c.png


因此,进程间通信的本质就是,让不同的进程看到同一份资源(内存,文件内核缓冲区等)。 由于这份资源可以由操作系统中的不同模块提供,因此出现了不同的进程间通信方式。


1.4 通信分类

管道


匿名管道

命名管道

System V IPC


System V 消息队列

System V 共享内存

System V 信号量

POSIX IPC(这部分会在后面的文章进行讲解)


消息队列

共享内存

信号量

互斥量

条件变量

读写锁

二、管道

2.1 管道概念

管道是Unix中最古老的进程间通信形式,从一个进程连接到另一个进程的数据流被称为一个"管道"


c396b9494be2478e94621f3ab7881ff7.png


who 和 wc命令运行起来是两个进程,who进程将数据(运行结果)写入"管道”中,wc进程再从"管道"中读取数据,至此便完成了数据的传输,进而可以完成数据的进一步加工处理。


e778a7d803a94e829bc4e361b7577c71.png


注意:who命令用于查看当前服务器的登录用户(一行显示一个用户),wc -l用于统计行数


在使用命令的时候并不会生成命名管道文件,且who和wc进程都是通过bash进程的子进程程序替换得到的,因此命令行上的管道实际上是匿名管道。


2.2 匿名管道

2.2.1 匿名管道原理

匿名管道用于进程间通信,且仅限于本地父子进程之间的通信。其原理就是:让父子进程先看到同一份被打开的文件资源,然后父子进程就可以对该文件进行写入或是读取操作,进而实现父子进程间通信。


230bb453abf342ac9e95ff02b289fb07.png


父子进程看到的同一份文件资源由操作系统进行维护,所以当父子进程对该文件进行写入操作时,该文件缓冲区当中的数据并不会进行写时拷贝。

管道虽然用的是文件的方案,但操作系统不会把进程进行通信的数据刷新到磁盘当中,因为这样做有磁盘IO参与会降低效率。这种文件是一种内存级文件,并不会在磁盘中存在。


2.2.2 pipe、pipe2函数

可以使用pipe函数创建匿名管道

int pipe(int pipefd[2]);

pipe函数参数是一个输出型参数,数组pipefd由两个分别指向管道读端和写端的文件描述符组成


95472c2a7c7b4906ac6179010f0197aa.png


返回值:调用成功时返回0,调用失败时返回-1


pipe2函数与pipe函数类似,也是用于创建匿名管道

int pipe2(int pipefd[2], int flags);

pipe2函数的第二个参数用于设置选项。


1、当没有数据可读时:


O_NONBLOCK disable:read调用阻塞,即进程暂停执行,一直等到有数据来为止

O_NONBLOCK enable:read调用返回-1,errno值为EAGAIN

2、当管道满的时候:


O_NONBLOCK disable:write调用阻塞,直到有进程将数据读取

O_NONBLOCK enable:write调用返回-1,errno值为EAGAIN

2.2.3 匿名管道使用理解

若想实现父子进程间通信,需将pipe()和fork()搭配使用


父进程调用pipe()创建管道

6f9ad5a378f543a58eddf584e746f3f3.png


父进程创建子进程

4d6061115d7948069c3ac8117f993e59.png


父进程关闭写(读)端,子进程关闭读(写)端

cdb268cc9beb4c21ab7b86fb3dc7fb5a.png


操作案例:

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
    int fd[2] = { 0 };
    if (pipe(fd) < 0) { 
        perror("pipe");
        return 1;
    }
    pid_t id = fork();
    if (id == 0){
        close(fd[0]);
        const char* msg = "hello father, I am child...";
        int count = 10;
        while (count--){
            write(fd[1], msg, strlen(msg));
            sleep(1);
        }
        close(fd[1]);
        exit(0);
    }
    else if(id > 0) {
        close(fd[1]);
        char buff[64] = {'\0'};
        while (1){
            ssize_t s = read(fd[0], buff, sizeof(buff));
            if (s > 0){
                buff[s] = '\0';
                printf("child send to father:%s\n", buff);
            }
            else if (s == 0){
                printf("read file end\n");
                break;
            }
            else{
                printf("read error\n");
                break;
            }
        }
        close(fd[0]);
        waitpid(id, NULL, 0);
    }
    return 0;
}

20c4813a19094b43a7ddb5e69049b1ea.png


2.3 命名管道

2.3.1 命名管道的原理

匿名管道只能用于具有亲缘关系的进程之间的通信,若要实现两个毫不相关进程之间的通信,可以使用命名管道。命名管道就是一种特殊类型的文件,两个进程通过文件名打开同一个管道文件,此时这两个进程也就看到了同一份资源,进而可以进行通信。


注意:


普通文件可以做到通信,但无法解决一些安全问题,并会发生落盘导致效率极低

命名管道和匿名管道一样,都是内存文件,只不过命名管道在磁盘有一个简单的映像,但这个映像的大小永远为0,因为命名管道和匿名管道都不会将通信数据刷新到磁盘当中

2.3.2 创建命名管道

使用命令创建命名管道


可以使用 mkfifo 命令创建一个命名管道


[bjy@VM-8-2-centos fifo_test]$ mkfifo fifo


edf945fc31a44d3eb6005b60dc948dbe.png

e4693329536f4c9ba79ef9f23b9a5e77.png


在程序中使用mkfifo函数创建命名管道


int mkfifo(const char *pathname, mode_t mode);

pathname:


若pathname以路径的方式给出,则将命名管道文件创建在pathname路径下

若pathname以文件名的方式给出,则将命名管道文件默认创建在当前路径下

mode:


表示创建命名管道文件的默认权限,受umask(0002)影响。实际创建出来文件的权限为:mode&(~umask)


返回值:


命名管道创建成功,返回0

命名管道创建失败,返回-1

创建案例:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#define FILE_NAME "myfifo"
int main()
{
  umask(0); //将文件默认掩码设置为0
  if (mkfifo(FILE_NAME, 0666) < 0) {
    perror("mkfifo");
    return 1;
  }
  //... ...
  return 0;
}

e7f8a7abad1e44b9aebd902b9d10d930.png


2.3.3 命名管道的打开规则

1、若当前打开操作是为读而打开FIFO时


O_NONBLOCK disable:阻塞直到有相应进程为写而打开该FIFO。

O_NONBLOCK enable:立刻返回成功


2、若当前打开操作是为写而打开FIFO时。


O_NONBLOCK disable:阻塞直到有相应进程为读而打开该FIFO。

O_NONBLOCK enable:立刻返回失败,错误码为ENXIO


2.3.4 利用命名管道实现serve&&client通信

实现通信前先使得服务端运行起来,创建命名管道并打开,然后服务端会发生阻塞直到客户端打开命名管道。

//my_server.cc
#include "com.h"
int main()
{
  umask(0);
  if (mkfifo(FILE_NAME, 0666) < 0) {
    perror("mkfifo");
    return 1;
  }
  int fd = open(FILE_NAME, O_RDONLY); //以读的方式打开命名管道文件
  if (fd < 0) {
    perror("open");
    return 1;
  }
  char msg[128] = {0};
  while (1){
    memset(msg,'\0',128);
    ssize_t s = read(fd, msg, sizeof(msg)-1);
    if (s > 0) {
      printf("client: %s", msg);
    }
    else if (s == 0) {
      printf("client quit!\n");
      break;
    }
    else {
      printf("read error!\n");
      break;
    }
  }
  close(fd);
    unlink(FILE_NAME);
  return 0;
}


客户端运行起来后先打开命名管道,一直向管道中写入数据即可


//my_client.cc
#include "com.h"
int main()
{
  int fd = open(FILE_NAME, O_WRONLY); //以写的方式打开命名管道文件
  if (fd < 0) {
    perror("open");
    return 1;
  }
  char msg[128];
  while (1){
        memset(msg,'\0',sizeof(msg));
    printf("Please Enter :>");
        fflush(stdout);//printf字符串中没有'\n'
    ssize_t s = read(0, msg, sizeof(msg)-1);//从标准输出流文件中读取
    if (s > 0) {
      write(fd, msg, strlen(msg));
    }
  }
  close(fd);
  return 0;
}


客户端和服务端包含同一个头文件,该头文件当中提供命名管道文件的文件名,客户端和服务端即可通过这个文件名,打开同一个命名管道文件进行通信

#pragma once
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <fcntl.h>
#define FILE_NAME "myfifo" //让客户端和服务端使用同一个命名管道

fbe457ddef874872a07e2d83c7ae23a3.png


2.4 管道特点

1、管道内部自带同步与互斥机制

同一时刻只允许一个进程使用的资源被称为临界资源。管道在同一时刻只允许一个进程对其进行写入或是读取操作,因此管道也就是一种临界资源。


临界资源是需要被保护的,若不对管道这种临界资源进行任何保护机制,那么就可能出现同一时刻有多个进程对同一管道进行操作的情况,进而导致同时读写、交叉读写以及读取到的数据不一致等问题。

为了避免这些问题,内核会对管道操作进行同步与互斥:

同步: 两个或两个以上的进程在运行过程中协同步调,按预定的先后次序运行。比如,A任务的运行依赖于B任务产生的数据

互斥: 一个公共资源同一时刻只能被一个进程使用,多个进程不能同时使用公共资源

实际上,同步是一种更为复杂的互斥。对于管道的场景来说,互斥就是两个进程不可以同时对管道进行操作,它们会相互排斥,必须等一个进程操作完毕,另一个才能操作,而同步也是指两个进程不能同时对管道进行操作,但这两个进程必须要按照某种次序来对管道进行操作。即互斥具有唯一性和排它性,但互斥并不限制任务的运行顺序,而同步的任务之间则有明确的顺序关系。

2、管道的生命周期随进程

管道本质上是通过文件进行通信,那么当所有打开该文件的进程都退出后,该文件也就会被释放掉,所以说管道的生命周期随进程。

3、管道提供流式服务

对于进程A写入管道当中的数据,进程B每次从管道读取的数据的多少是任意的,这种被称为流式服务,与之相对应的是数据报服务:

流式服务: 数据没有明确的分割,不分一定的报文段。

数据报服务: 数据有明确的分割,拿数据按报文段拿。

4、管道是半双工通信的

单工通信(Simplex Communication):单工模式的数据传输是单向的。通信双方中,一方固定为发送端,另一方固定为接收端。

半双工通信(Half Duplex):半双工数据传输指数据可以在一个信号载体的两个方向上传输,但是不能同时传输。

全双工通信(Full Duplex):全双工通信允许数据在两个方向上同时传输,它的能力相当于两个单工通信方式的结合。全双工可以同时(瞬时)进行信号的双向传输。

管道是半双工的,数据只能向一个方向流动。需要双方通信时,通常会建立起两个管道


18d6a48efa9d4566a3cd280d2857e753.png


2.5 管道读写规则

  1. 写端进程不写,读端进程一直读,那么会因为管道中没有数据可读,对应的读端进程会被挂起,直到管道里面有数据后,读端进程才会被唤醒。
  2. 读端进程不读,写端进程一直写,那么当管道被写满后,对应的写端进程会被挂起,直到管道当中的数据被读端进程读取后,写端进程才会被唤醒。
  3. 写端进程将数据写完后将写端关闭,那么读端进程将管道当中的数据读完后,就会继续执行该进程之后的代码逻辑,而不会被挂起。
  4. 读端进程将读端关闭,而写端进程还在一直向管道写入数据,则操作系统可能产生SIGPIPE信号将写端进程杀死。
  5. 当要写入的数据量不大于PIPE_BUF时,Linux将保证写入的原子性
  6. 当要写入的数据量大于PIPE_BUF时,Linux将不再保证写入的原子性

管道自带同步与互斥机制,读端进程和写端进程存在一个步调协调的过程,不会发生管道中没有数据但读端还在读取,或管道已经满了但写端还在写入的情况。读端进程读取数据的条件是管道里面有数据,写端进程写入数据的条件是管道当中还有空间,若是条件不满足,则相应的进程就会被挂起,直到条件满足后才会被再次唤醒。


读端进程已经将管道当中的所有数据都读取出来了,而且此后也不会有写端再进行写入了,那么此时读端进程也就可以执行该进程的其他逻辑了,而不会被挂起。


管道中的数据已经没有进程读取了,那么写端进程的写入将没有意义,因此操作系统直接将写端进程杀掉。进程代码都还没跑完就被终止,属于异常退出,那么进程必然是收到了某种信号。

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
    int fd[2] = {0};
    if (pipe(fd) < 0) {
        perror("pipe");
        return 1;
    }
    pid_t id = fork();
    if (id == 0) { // child
        close(fd[0]);
        const char *msg = "hello father, I am child...";
        for (int i = 0; i < 10; ++i) {
            write(fd[1], msg, strlen(msg));
            sleep(1);
        }
        close(fd[1]);
        exit(0);
    }
    else if (id > 0) { // father
        close(fd[1]);
        close(fd[0]); //父进程直接关闭读端(导致子进程被操作系统杀死)
        int status = 0;
        waitpid(id, &status, 0);
        printf("child get signal:%d\n", WTERMSIG(status));
    }
    return 0;
}

52c592010b3741588cc0cdeb8b8cb33d.png


2.6 管道大小

通过下面的代码来测试本机器上的管道大小

#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <unistd.h>
int main()
{
  int fd[2] = { 0 };
  if (pipe2(fd , O_NONBLOCK) < 0) {
    perror("pipe");
    return 1;
  }
  pid_t id = fork();
  if (id == 0) { //child 
    close(fd[0]); //子进程关闭读端
    char c = 'a';
    int count = 0;
    while (true) {
            int num = write(fd[1], &c, 1);
            if(num == -1) break;
      count++;
    }
        printf("%d\n", count); //打印当前写入的字节数
    close(fd[1]);
    exit(0);
  }
    else if(id > 0) {//father
        close(fd[1]);
      waitpid(id, NULL, 0);
      close(fd[0]);
    }
    else {
        perror("fork");
        exit(0);
    }
  return 0;
}

78df15442bbd433db4647142a92391e5.png


使用ulimit -a命令也可以查看管道大小,但与实际情况有所不同


目录
相关文章
|
7月前
|
存储 Linux API
【Linux进程概念】—— 操作系统中的“生命体”,计算机里的“多线程”
在计算机系统的底层架构中,操作系统肩负着资源管理与任务调度的重任。当我们启动各类应用程序时,其背后复杂的运作机制便悄然展开。程序,作为静态的指令集合,如何在系统中实现动态执行?本文带你一探究竟!
【Linux进程概念】—— 操作系统中的“生命体”,计算机里的“多线程”
|
5月前
|
并行计算 Linux
Linux内核中的线程和进程实现详解
了解进程和线程如何工作,可以帮助我们更好地编写程序,充分利用多核CPU,实现并行计算,提高系统的响应速度和计算效能。记住,适当平衡进程和线程的使用,既要拥有独立空间的'兄弟',也需要在'家庭'中分享和并行的成员。对于这个世界,现在,你应该有一个全新的认识。
237 67
|
4月前
|
Web App开发 Linux 程序员
获取和理解Linux进程以及其PID的基础知识。
总的来说,理解Linux进程及其PID需要我们明白,进程就如同汽车,负责执行任务,而PID则是独特的车牌号,为我们提供了管理的便利。知道这个,我们就可以更好地理解和操作Linux系统,甚至通过对进程的有效管理,让系统运行得更加顺畅。
117 16
|
4月前
|
Unix Linux
对于Linux的进程概念以及进程状态的理解和解析
现在,我们已经了解了Linux进程的基础知识和进程状态的理解了。这就像我们理解了城市中行人的行走和行为模式!希望这个形象的例子能帮助我们更好地理解这个重要的概念,并在实际应用中发挥作用。
93 20
|
9月前
|
算法 Linux 调度
深入理解Linux操作系统的进程管理
本文旨在探讨Linux操作系统中的进程管理机制,包括进程的创建、执行、调度和终止等环节。通过对Linux内核中相关模块的分析,揭示其高效的进程管理策略,为开发者提供优化程序性能和资源利用率的参考。
260 1
|
3月前
|
监控 Shell Linux
Linux进程控制(详细讲解)
进程等待是系统通过调用特定的接口(如waitwaitpid)来实现的。来进行对子进程状态检测与回收的功能。
75 0
|
3月前
|
存储 负载均衡 算法
Linux2.6内核进程调度队列
本篇文章是Linux进程系列中的最后一篇文章,本来是想放在上一篇文章的结尾的,但是想了想还是单独写一篇文章吧,虽然说这部分内容是比较难的,所有一般来说是简单的提及带过的,但是为了让大家对进程有更深的理解与认识,还是看了一些别人的文章,然后学习了学习,然后对此做了总结,尽可能详细的介绍明白。最后推荐一篇文章Linux的进程优先级 NI 和 PR - 简书。
102 0
|
3月前
|
存储 Linux Shell
Linux进程概念-详细版(二)
在Linux进程概念-详细版(一)中我们解释了什么是进程,以及进程的各种状态,已经对进程有了一定的认识,那么这篇文章将会继续补全上篇文章剩余没有说到的,进程优先级,环境变量,程序地址空间,进程地址空间,以及调度队列。
66 0
|
3月前
|
Linux 调度 C语言
Linux进程概念-详细版(一)
子进程与父进程代码共享,其子进程直接用父进程的代码,其自己本身无代码,所以子进程无法改动代码,平时所说的修改是修改的数据。为什么要创建子进程:为了让其父子进程执行不同的代码块。子进程的数据相对于父进程是会进行写时拷贝(COW)。
68 0
|
6月前
|
存储 Linux 调度
【Linux】进程概念和进程状态
本文详细介绍了Linux系统中进程的核心概念与管理机制。从进程的定义出发,阐述了其作为操作系统资源管理的基本单位的重要性,并深入解析了task_struct结构体的内容及其在进程管理中的作用。同时,文章讲解了进程的基本操作(如获取PID、查看进程信息等)、父进程与子进程的关系(重点分析fork函数)、以及进程的三种主要状态(运行、阻塞、挂起)。此外,还探讨了Linux特有的进程状态表示和孤儿进程的处理方式。通过学习这些内容,读者可以更好地理解Linux进程的运行原理并优化系统性能。
221 4