Linux--进程间通信

进程间通信介绍

进程间通信(Inter-Process Communication,简称 IPC)是指在不同进程之间进行数据交换和信息传递的机制。

进程通信的目的

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

在多任务操作系统中,往往同时运行着多个进程。这些进程可能需要协同工作、共享数据或者进行同步操作。例如,一个图形处理软件可能有多个进程分别负责用户界面、图像渲染和文件存储等任务,它们之间需要进行通信来确保整个软件的正常运行。

进程通信的本质

进程通信的本质是在不同的独立运行的进程之间实现信息的交换和共享,以协调它们的行为和完成特定的任务。

资源共享与协作需求

在多任务操作系统中,各个进程通常被设计为独立执行的实体,以提高系统的并发性和资源利用率。然而,在很多情况下,这些进程需要相互协作才能实现更复杂的功能。

例如:一个数据分析程序可能由多个进程组成,其中一个进程负责读取数据,另一个进程进行数据处理,还有一个进程负责将结果输出。这些进程需要通过进程间通信来传递数据,以实现整个数据分析任务。

隔离与交互的矛盾

进程在操作系统中是相互隔离的,每个进程都有自己独立的地址空间、资源和执行上下文。这种隔离性保证了进程的稳定性和安全性,但也带来了交互的困难。进程通信的本质就是在这种隔离的环境中建立一种机制,使得不同的进程能够突破隔离,实现信息的传递和交互。

  1. 通过特定的通信机制,如管道、消息队列、共享内存等,进程可以将数据写入一个共享的区域或通道,其他进程可以从这个区域或通道读取数据,从而实现信息的交换。
  2. 进程通信还可以用于同步进程的执行。例如,一个进程可以等待另一个进程发送特定的信号或消息,然后再继续执行,以确保它们的操作按照正确的顺序进行。

进程间通信分类

管道:

  • 匿名管道pipe
  • 命名管道

System V IPC:

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

POSIX IPC:

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

管道

什么是管道

管道是一种进程间通信的方式,主要用于在具有父子进程或相关联的进程之间传递数据。

例如,“ls | grep.txt” 这个命令将 “ls” 命令的输出(列出当前目录下的文件)作为 “grep.txt” 命令的输入,查找包含 “.txt” 的文件名。

其中,ls命令和grep .txt命令是两个程序,当他们运行起来就变成了两个进程,ls进程通过标准输出将数据打印到“管道”中,grep .txt进程再读取过滤以.txt结尾的数据,至此便完成了数据的传输,进而完成数据的打印。

匿名管道

匿名管道的原理

在操作系统中,匿名管道通常由内核管理。当一个进程需要创建匿名管道与另一个相关进程(通常是父子进程)进行通信时,操作系统会在内核中创建一个特殊的管道结构。这个结构通常包括两个缓冲区,一个用于数据从一个进程写入(写端),另一个用于数据被另一个进程读取(读端)。

pipe函数

pipe函数通常用于创建一个匿名管道,实现进程间通信或在同一进程内不同部分之间传递数据。

int pipe(int pipefd[2]);

pipefd[2]是一个整数数组,用于接收两个文件描述符。
 

数组元素含义
pipefd[0]是管道的读取端
pipefd[1]是管道的写入端

成功时返回 0,失败时返回 -1,并设置errno来指示错误类型。

匿名管道使用步骤

在创建匿名管道实现父子进程间通信的过程中,需要pipe()函数和fork()函数搭配使用。

1.包含必要的头文件

#include <iostream>
#include <unistd.h>

2.创建管道

int pipefd[2];
if (pipe(pipefd) == -1) {
    std::cerr << "Error creating pipe." << std::endl;
    return 1;
}

3.使用 fork 创建子进程(如果用于进程间通信)

pid_t pid = fork();
if (pid == -1) {
    std::cerr << "Error creating child process." << std::endl;
    return 1;
}

4.子进程和父进程分别进行操作

如果是父进程写入数据:

  • 关闭管道的读取端。
  • 使用写入函数向管道写入数据。
  • 关闭管道的写入端。
  •    if (pid > 0) {
           // 父进程
           close(pipefd[0]);
           const char *message = "Hello from parent!";
           write(pipefd[1], message, strlen(message));
           close(pipefd[1]);
       }

    如果是子进程读取数据:

    • 关闭管道的写入端。
    • 使用读取函数从管道读取数据。
    • 关闭管道的读取端。
  •    if (pid == 0) {
           // 子进程
           close(pipefd[1]);
           char buffer[100];
           ssize_t bytesRead = read(pipefd[0], buffer, sizeof(buffer));
           if (bytesRead > 0) {
               buffer[bytesRead] = '\0';
               std::cout << "Child received: " << buffer << std::endl;
           }
           close(pipefd[0]);
       }

注意事项

  • 确保在使用完管道后及时关闭不需要的文件描述符,以避免资源泄漏。
  • 匿名管道只能在具有共同祖先的进程之间使用,通常是通过fork创建的父子进程。
  • 管道是单向的,如果需要双向通信,可以创建两个管道。

管道的读写规则

pipe2函数与pipe函数类似,也是用于创建匿名管道,其函数原型如下:

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

参数说明:

1、pipefd[2]:这是一个整数数组,用于存储两个文件描述符。pipefd[0]是管道的读端,pipefd[1]是管道的写端。

2、flags:可以是以下标志的按位或组合:

  1. 读操作
  • O_NONBLOCK disable:可以理解为关闭(禁用)非阻塞标志(O_NONBLOCK)。如果管道或文件中没有数据可读,读操作会阻塞等待数据到来,不是立即返回错误。
  • O_NONBLOCK enable:如果管道或文件中没有数据可读,读操作不会阻塞等待数据到来,而是立即返回 -1,并将 errno 设置为 EAGAIN 或 EWOULDBLOCK,表示资源暂时不可用,需要稍后再尝试。
  1. 写操作        
  • O_NONBLOCK disable:如果管道或文件已满,写操作会阻塞等待空间可用,不是立即返回错误。
  • O_NONBLOCK enable:如果管道或文件已满,写操作不会阻塞等待空间可用,而是立即返回 -1,并将 errno 设置为 EAGAIN 或 EWOULDBLOCK,表示资源暂时不可用,需要稍后再尝试。
3、如果所有管道写端对应的文件描述符被关闭,则read返回0。
4、如果所有管道读端对应的文件描述符被关闭,则write操作会产生信号SIGPIPE,进而可能导致write进程 退出。
5、当要写入的数据量 不大于 PIPE_BUF时,linux将保证写入的原子性。
6、当要写入的数据量 大于 PIPE_BUF时,linux将不再保证写入的原子性。

管道特点

同步与互斥机制

一次仅允许一个进程使用的资源称为临界资源。管道在同一时刻只允许一个进程对其进行读写操作,所以管道是一种临界资源。

临界资源是需要被保护的,如果不对其保护,会出现多个进程对其读写操作,进而会导致同时读写、交叉读写以及读取的数据不一致等问题。

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

  • 同步:同步是指多个进程或线程之间协调它们的活动,以确保它们按照特定的顺序执行或者在特定的条件下执行。确保多个进程或线程之间的正确协作,避免出现竞争条件、死锁等问题,同时保证程序的正确性和可靠性。
  • 互斥:互斥是指多个进程或线程不能同时访问同一个临界资源。临界资源是一次只能被一个进程或线程使用的资源,例如共享内存中的一个变量、一个文件、一个设备等。防止多个进程或线程同时对临界资源进行读写操作,从而避免数据不一致、数据损坏或程序错误。

互斥是同步的一种特殊情况。互斥主要关注的是对临界资源的独占访问,而同步则更广泛地涉及多个进程或线程之间的协调和顺序执行。

在实现同步时,通常需要使用互斥机制来保护共享资源。例如,在使用条件变量进行同步时,通常需要使用互斥锁来保护条件变量和共享资源,以确保在检查条件和改变条件时不会出现数据不一致的情况。

互斥和同步都是为了确保多进程或多线程程序的正确性和可靠性。它们通过控制对共享资源的访问和协调进程或线程之间的活动,避免了竞争条件、死锁等问题的发生。

管道生命周期

正常结束:

  • 如果所有使用管道的进程都正确地关闭了管道的读端和写端,管道将被销毁。当一个进程关闭它所使用的管道文件描述符时,内核会检查是否还有其他进程在使用该管道。如果没有,内核会释放管道的内核缓冲区等相关资源。
  • 例如,父子进程通过管道通信完成后,双方都应该关闭管道的文件描述符,以确保管道被正确清理。
异常结束:
  • 如果一个进程意外退出而没有关闭管道的文件描述符,内核会自动关闭该进程使用的管道文件描述符。但是,如果还有其他进程在使用管道,管道可能不会立即被销毁。
  • 例如,一个子进程在运行过程中发生错误而崩溃,内核会清理该子进程所占用的资源,包括可能未关闭的管道文件描述符。

总之,管道的生命周期取决于创建它的进程的行为。正确地管理管道的文件描述符,及时关闭不再使用的端口,对于确保系统资源的有效利用和避免潜在的资源泄漏非常重要。

管道提供的流式服务
管道就像一个 “流”,数据在其中以连续的方式从一端流向另一端。当一个进程向管道写入数据时,这些数据会依次进入管道的缓冲区,等待被另一端的进程读取。数据的传输是动态的,随着写入和读取操作的进行,数据不断地在管道中流动。
数据在管道中的流动是按照写入的顺序进行的。先写入管道的数据会先被读取出来,类似于水流在管道中的流动,遵循先入先出(FIFO)的原则。这确保了数据的有序性,使得接收端的进程能够按照发送端进程写入的顺序获取数据。
管道是半双工的
管道在创建时就确定了数据的流动方向。如果一个进程向管道写入数据,另一个进程只能从管道读取数据,不能反过来进行写入。例如,进程 A 向管道写入数据,进程 B 从管道读取数据,这是一个典型的半双工通信模式。
在半双工的管道中,不能同时在两端进行读写操作。如果一个进程正在从管道读取数据,另一个进程不能同时向管道写入数据,反之亦然。这是为了避免数据的混乱和冲突。

命名管道

命名管道的原理

  • 命名管道是一种半双工通信方式,但可以通过建立两个命名管道来实现双向通信。有命名管道可以在不同的用户之间进行通信,只要他们具有对管道文件的访问权限。
  • 命名管道可以在不相关的进程之间进行通信。它有一个特定的文件名,不同的进程可以通过这个文件名来打开管道进行读写操作。

创建命名管道

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

mkfifo fifo

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

函数原型:

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

需要的头文件: 

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

参数说明 :

  • pathname:指定要创建的命名管道的路径名,可以是相对路径或绝对路径。
  • mode:指定命名管道的权限模式,类似于open()函数中使用的权限模式,例如0666表示所有者、所属组和其他用户都具有读写权限。

返回值:成功创建命名管道时返回 0;失败则返回 -1,并设置errno来指示错误类型。

mkfifo()会依参数pathname建立特殊的 FIFO 文件,该文件必须不存在,而参数mode为该文件的权限(mode%~umask),因此umask值也会影响到 FIFO 文件的权限。

如果想创建出来的命名管道文件的权限值不受umask的影响,则需要在创建文件前使用umask()函数将文件默认掩码设置为0。

umask(0);  //将文件默认掩码设置为0

创建命名管道示例:

使用以下代码可以在当前路径下创建一个名为fifo的命名管道。

 

#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include <cstring>

#define Path "./fifo"
#define Mode 0666

int main()
{
    umask(0);
    int n = mkfifo(Path, Mode);
    if(n < 0)
    {
        std :: cout<<"mkfifo failed errno:" << errno << ", errstring:" << strerror(errno) <<std :: endl; 
        return 1;
    }
    else
    {
        std :: cout << "mkfifo success..." << std::endl;
    }

    return 0;
}

 

运行代码后,会在当前路径下创建一个名为fifo的命名管道。

 用命名管道实现简易server&client通信

在实现服务端(server)和客户端(client)之间通信前,我们需要先人服务端运行起来,服务端运行起来后创建一个命名管道文件,然后在以读的方式打开命名管道文件,之后服务端就可以从该命名管道读取客户端发来的数据了。

用一个名为Fifo的类封装mkfifo()的创建:

这里我们在创建Fifo时候,会调用构造函数,构造函数不仅初始化成员属性,还创建了命名管道。

#ifndef __COMM_HPP__
#define __COMM_HPP__

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <cstring>
#include <string>
#include <iostream>

using namespace std;

#define Mode 0666
#define Path "./fifo"

class Fifo
{
public:
    Fifo(const string &path)
        : _path(path)
    {
        umask(0); // 将文件默认掩码设置为0
        int n = mkfifo(_path.c_str(), Mode);
        if (n == 0)
        {
            cout << "mkfifo success" << endl;
        }
        else
        {
            cerr << "mkfifo failed, errno:" << errno << "errstring:" << strerror(errno) << endl;
        }
    }
    ~Fifo()
    {
        int n = unlink(_path.c_str());
        if (n == 0)
        {
            cout << "remove fifo file" << endl;
        }
        else
        {
            cerr << "remove failed, erron:" << errno << ", errstring:" << strerror(errno) << endl;
        }
    }

private:
    string _path; // 文件路径+文件名
};

#endif

服务端代码:

#include "Comm.hpp"

int main()
{
    Fifo fifo(Path);

    int rfd = open(Path, O_RDONLY);
    if(rfd < 0)
    {
        cerr << "open failed, errno:" << errno <<", errstring:" << strerror(errno) <<endl;
        return 1;
    }
    //如果我们的写端没有打开,先读端打开,open的时候就会阻塞,直到把写端打开,读open才会返回
    cout << "opne success" <<endl;

    char buffer[1024];
    while(true)
    {
        ssize_t n = read(rfd, buffer,sizeof(buffer) - 1);
        if(n > 0)
        {
            buffer[n] = 0;
            cout << "client say:" << buffer << endl;
        }
        else if(n == 0)
        {
            cout << "client quit, me too!!!" << endl;
            break;
        }
        else{
            cerr << "read failed, erron:" << errno <<", errstring:" << strerror(errno)<<endl;
            break;
        }
    }

    close(rfd);
    return 0;
}

对于客户端来说,因为服务端运行起来就创建了命名管道,所以客户端只需要以写的方式打开命名管道,之后客户端就可以通过命名管道给服务端通信数据了。

客户端代码:

#include "Comm.hpp"

int main()
{
    int wfd = open(Path, O_WRONLY);
    if (wfd < 0)
    {
        cerr << "opne failed, errno:" << errno << ", errstring:" << strerror(errno) << endl;
        return 1;
    }

    string inbuffer;
    while (true)
    {
        cout << "Please Enter Your Messge#";
        std ::getline(cin, inbuffer);
        if (inbuffer == "quit")
            break;
        ssize_t n = write(wfd, inbuffer.c_str(), inbuffer.size());
        if (n < 0)
        {
            cerr << "write faile,errno:" << errno << ", errstring:" << strerror(errno) << endl;
            break;
        }
    }

    close(wfd);
    return 0;
}

让服务端和客户端open同一个文件,这就可以让两个不同的进程看见同一个命名管道文件,这样客户端和服务端就可以实现命名管道的进程间通信了。

运行结果:

接着再将客户端运行起来,从客户写入数据,数据会写入到命名管道当中,服务端就可以通过命名管道读来自客户端的数据,然后打印到显示器上,这样就是实现了两个进程之间的通信了。

总之,命名管道是可以实现两个毫无关系进程之间通信的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值