Linux系统-进程间通信(上)

简介: Linux系统-进程间通信(上)

零、前言


本章主要讲解学习Linux中本系统下的进程间通信


一、进程间通信介绍


  • 概念:

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


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


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

由于进程之间具有独立性,代码数据独立拥有,若想实现通信,可以通过向第三方资源(实际上就是操作系统提供的一段内存区域)写入或是读取数据,进而实现进程之间的通信


  • 进程间通信发展:

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


  • 进程间通信分类:
  1. 管道

匿名管道pipe;命名管道


  1. System V IPC

System V 消息队列;System V 共享内存;System V 信号量


  1. POSIX IPC

消息队列;共享内存;信号量;互斥量;条件变量;读写锁


二、管道


  • 概念:

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

  • 示图:统计当前使用云服务器上的登录用户个数

7056934a6fd5785adcfe49ec6a6502a2.png

202203122241082.png


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


1、匿名管道

  • 概念:

匿名管道用于本地具有亲戚关系的进程之间通信,常用与父子进程间通信


  • pipe函数原型:
#include <unistd.h>
int pipe(int fd[2]);


  • 功能:

创建一无名管道


  • 参数:
  1. fd:文件描述符数组,是一个输出型参数,拿到打开的管道文件的问文件描述符,其中fd[0]表示读端文件,fd[1]表示写端文件
  2. 返回值:成功返回0,失败返回错误代码


  • 示图:

202203122328999.png

  • 示例:父子进程匿名管道通信
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<string.h>
#include<stdlib.h>
int main()
{
    int pipe_id[2]={0};
    //创建管道文件资源
    int ret=pipe(pipe_id);
    if(ret<0)
    {
        perror("pipe");
        exit(1);
    }
    printf("pipe_id[0]:%d pipe_id[1]:%d\n",pipe_id[0],pipe_id[1]);
    //创建子进程,共享管道资源
    pid_t id=fork();
    if(id==0)
    {
        //child->write
        //关闭子进程的读端
        close(pipe_id[0]);
        const char* msg="Hello father, I am child!\n";
        int cnt=0;
        while(1)
        {
            cnt++;
            printf("child write:%d\n",cnt);
            write(pipe_id[1],msg,strlen(msg));//结束符不用写入,结束符是C语言的规则不是系统的规则
            sleep(1);
            if(cnt==10)
                break;
        }
        close(pipe_id[1]);
        exit(0);
    }
    else if(id>0)
    {
        //father->read 
        //关闭父进程的写端
        close(pipe_id[1]);
        //进行读取管道信息
        while(1)
        {
            char buffer[128]={0};
            ssize_t s=read(pipe_id[0],buffer,sizeof(buffer)-1);//给结束符留一个位置
            if(s>0)
            {
                buffer[s]=0;//设置结束符
                printf("msg from child:%s",buffer);
            }
            else if(s==0)
            {
                printf("子进程写端关闭...\n");
                break;
            }
            else 
                break;
        }
        close(pipe_id[0]);
    }
    else 
    {
        perror("fork");
        exit(2);
    }
    //父进程等待
    int status=0;
    if(waitpid(id,&status,0)>0&&WIFEXITED(status))//等待成功并退出正常
    {
        printf("wait success! exit code:%d\n",WEXITSTATUS(status));
    }
    else  
    {
        printf("exit sign:%d\n",status&0x7F);
    }
    return 0;
}


  • 效果:

6f9692dded0b0bdaa470413929657b66.png

  • 共享管道原理:
  1. 对于同个文件可以以读方式和以写方式打开,文件在文件系统虽然只有一份,但是在进程的PCB中的文件结构体中的文件地址数组中可以保存两份,一份指向文件的读端口,一份指向文件的写端口
  2. 管道通过系统接口创建管道文件资源,并构建文件与PCB的映射关系,当fork创建子进程时父子进程就见到同一份文件资源,依靠管道文件的缓冲区选择性进行单向的实时读写


注:如果是刷新到磁盘上再进行读写非常影响效率


  • 单向读写:

父进程进行读,子进程进行写;父进程进行写,子进程进行读


  • 示图:

202203182346231.png

  • 注意:
  1. 只有在先fork之前读写打开文件,父子进程才能共享相同的文件指针数组,进一步灵活控制读写
  2. 管道只能够进行单向通信,关闭对应的读写端也是为了避免误操作
  3. 从管道写端写入的数据会被内核缓冲,直到从管道的读端被读取


  • 以文件描述符视角理解:

202203190930269.png

  • 以内核角度理解:

202203190932664.png

  • 注意:
  1. 管道就是特殊的文件,管道的使用和文件一致
  2. 但是依靠管道通信的本质上依靠管道的缓冲区进行读写,其缓冲并不会真正的刷新到磁盘上


  • 管道读写规则:
  1. 写端不写,读端无数据可读

O_NONBLOCK disable:read调用阻塞,即进程暂停执行,进行等待写端写入数据

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


  1. 写端不写,并将写端文件关闭

如果所有管道写端对应的文件描述符被关闭,则read返回0


  1. 读端不读,写端一直写

O_NONBLOCK disable: write调用阻塞,直到有进程读走管道缓冲区的数据

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


  1. 读端不读,并将读端文件关闭

如果所有管道读端对应的文件描述符被关闭,则write操作会产生信号SIGPIPE,进而可能导致write进程被终止退出

  • 示图:

202203191030473.png


  1. 数据写入的原子性

当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性

当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性


注:原子性是指 一个操作是不可中断的,要么全部执行成功要么全部执行失败,即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程所干扰


  • 管道特点:
  1. 只能用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信;通常父子进程之间就可应用该管道
  2. 管道提供流式服务,面向字节流,读写以字节为单位进行
  3. 进程退出,管道释放,所以管道的生命周期随进程内核会对管道操作进行同步与互斥,即保证数据的原子性
  4. 管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道


  • 示图:

202203191015434.png


2、命名管道

  • 概念:
  1. 对于匿名管道应用的一个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信
  2. 如果我们想在不相关的进程之间交换数据,可以使用FIFO文件来做这项工作,它经常被称为命名管道
  • 命名管道创建命令:
mkfifo filename


  • 示例:

202203191037337.png

  • 命名管道创建函数原型:
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *filename, mode_t mode);


注:第一个参数即为管道的名称,第二个参数即为创建管道文件的权限,创建成功返回0,否则返回-1


  • 示例:
int main()
{
    mkfifo("fifo", 0644);
    return 0;
}


  • 匿名管道与命名管道的区别
  1. 匿名管道由pipe函数创建并打开,依靠父子进程的共享特性看到同一份文件资源
  2. 命名管道由mkfifo函数创建并主动调用函数打开,依靠文件路径的唯一性让不同进行找到并打开同一份文件资源
  3. FIFO(命名管道)与pipe(匿名管道)之间唯一的区别在它们创建与打开的方式不同,一但这些工作完成之后,它们具有相同的语义


  • 命名管道的打开规则
  1. 如果当前打开操作是为读而打开FIFO时

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

O_NONBLOCK enable:立刻返回成功


  1. 如果当前打开操作是为写而打开FIFO时

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

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


  • 示例:用命名管道实现server&client通信
server.c:
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#define FIFO "fifo"
int main()
{
    //创建命名管道
    if(mkfifo(FIFO,0644)<0)
    {
        perror("mkfifo");
        exit(1);
    }
    //打开管道文件
    int fd=open(FIFO,O_RDONLY);
    if(fd<0)
    {
        perror("open");
        exit(2);
    }
    //服务端进行客户端信息
    while(1)
    {
        char buffer[128]={0};
        //输出标识词
        printf("client#");
        fflush(stdout);
        //读取管道数据
        ssize_t s=read(fd,buffer,sizeof(buffer)-1);
        if(s>0)
        {
            buffer[s]=0;
            printf("%s",buffer);
        }
        else if(s==0)
        {
            printf("write close,child quit\n");
            break;
        }
        else 
            break;
    }
    return 0;
}
client.c:
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#define FIFO "fifo"
int main()
{
    //打开管道文件
    int fd=open(FIFO,O_WRONLY);
    if(fd<0)
    {
        perror("open");
        exit(2);
    }
    //向服务端发送消息
    while(1)
    {
        char buffer[128]={0};
        //输出标识词
        printf("please enter#");
        fflush(stdout);
        //读入数据
        ssize_t s=read(0,buffer,sizeof(buffer)-1);
        if(s>0)//写入到管道
        {
            buffer[s]=0;
            write(fd,buffer,strlen(buffer));
        }
        else 
            break;
    }
    return 0;
}


  • 效果:

202203191145783.png

相关文章
|
1天前
|
SQL 监控 架构师
linux系统性能分析的目的
【4月更文挑战第19天】在Linux系统中,找到性能瓶颈是关键,涉及应用程序、操作系统、硬件和网络的全面排查。优化方案通常针对应用程序和操作系统,而硬件和网络问题较易定位。目标是平衡资源使用,确保系统响应和稳定性。系统管理员、架构设计人员和开发人员共同参与,通过监控硬件、网络、配置和代码来优化性能。流程包括管理员初步判断,架构师处理结构问题,开发人员优化代码,实现系统资源的均衡利用。
6 1
|
3天前
|
Ubuntu Linux
Linux(Ubuntu)系统临时IP以及静态IP配置(关闭、启动网卡等操作)
请注意,以上步骤是在临时基础上进行配置的。如果要永久保存静态IP地址,通常还需要修改 `/etc/network/interfaces`文件,以便在系统重启后保持配置。同时,确保备份相关配置文件以防止出现问题。
16 1
|
4天前
|
Linux 数据安全/隐私保护
Linux系统忘记密码的三种解决办法
这篇博客介绍了三种在Linux忘记密码时重置登录密码的方法:1) 使用恢复模式,通过控制台界面以管理员权限更改密码;2) 利用Linux Live CD/USB启动,挂载硬盘分区并使用终端更改密码;3) 进入单用户模式,自动以管理员身份登录后重置密码。每个方法都提供了详细步骤,提醒用户在操作前备份重要数据。
|
4天前
|
JSON Unix Linux
Linux系统之jq工具的基本使用
Linux系统之jq工具的基本使用
32 2
|
4天前
|
数据采集 监控 安全
linux系统被×××后处理经历
linux系统被×××后处理经历
|
4天前
|
监控 安全 Linux
Linux系统之安装ServerBee服务器监控工具
【4月更文挑战第22天】Linux系统之安装ServerBee服务器监控工具
43 2
|
4天前
|
缓存 Linux
linux系统缓存机制
linux系统缓存机制
|
5天前
|
NoSQL Linux 程序员
【linux进程信号(一)】信号的概念以及产生信号的方式
【linux进程信号(一)】信号的概念以及产生信号的方式
|
5天前
|
Linux
【linux进程间通信(一)】匿名管道和命名管道
【linux进程间通信(一)】匿名管道和命名管道
|
5天前
|
Java Shell Linux
【linux进程控制(三)】进程程序替换--如何自己实现一个bash解释器?
【linux进程控制(三)】进程程序替换--如何自己实现一个bash解释器?