进程间通信——匿名管道

进程间通信

进程是一个独立的资源分配单元,不同进程(用户进程)之间的资源是独立的,没有关联的,不能在一个进程中直接访问另一个进程中的资源。
但是进程不是孤立的,不同的进程需要进行信息的交互和状态的传递,因此需要进程间通信(IPC(inter processes communication)).

进程间通信的目的
  1. 数据传输。一个进程需要将他的数据传输给另一个进程。
  2. 通知事件。一个进程需要向另一个/一组进程发送消息,通知他/他们发生了某种事件(比如进程终止时要通知父进程)
  3. 资源共享。多个进程之间共享同样的资源(需要内核提供互斥和同步机制)。
  4. 进程控制。有些进程希望完全控制另一个进程的执行(比如Debug进程),此时控制进程希望能够拦截另一个进程的缺陷和异常,并能够及时知道他的状态改变。
linux进程间的通信方式

Image

匿名管道

管道也叫做无名(匿名)管道,他是 unix 系统 IPC 最古老形式,所有的 unix 系统都支持这种通信方式。
例如,统计一个目录中文件的数目命令:ls | wc -l,为了执行这个命令,shell创建了两个进程来分别执行 ls 和 wc 命令

Image

管道的特点

  1. 管道其实是一个在内核中维护的缓冲器,这个缓冲期的存储能力是有限的,不同的操作系统下大小不一定相同。
  2. 管道拥有文件读写操作的特性,可以按照操作文件的方式来操作管道。匿名管道没有文件实体,有名管道有文件实体,但是不存储数据。
  3. 管道是一个字节流,使用管道时不存在消息或者消息边界的概念,从管道中读取数据的进程可以读取任意大小,而不管写入数据的进程写入了多少数据。
  4. 通过管道传输的数据是顺序的,从管道读出数据的顺序和写入管道中数据的顺序一致。
  5. 管道中数据单向传输,从一端写入从另一端读取,是半双工的。
  6. 管道中的数据只能被读取一次,读完之后被抛弃以便写入新的数据,管道中不能使用lseek来随机的读取数据。
  7. 匿名管道只能用在同一个公共祖先的管道之间(父子、兄弟等具有亲缘关系的管道之间)。

管道的数据结构

管道的数据结构是一个环形队列。

Image

匿名管道的使用

创建与使用管道
#include<nistd.h>
int pipe(int pipefd[2]);

作用: 创建一个匿名管道用于进程间通信
参数: 一个传入值传出结果的数组参数,pipefd[0]对应的是管道的读端,pipefd[1]对应的是管道的写端。
返回: 成功0,失败-1
管道是默认阻塞的: 如果管道中没有数据,read阻塞。如果管道满了,write阻塞。
注意: 匿名管道只能用于有公共祖先的进程之间通信(父子进程,兄弟进程等)。

查看管道缓冲大小的shell命令:

ulimit -a

查看管道缓冲大小的函数:

#include <unistd.h>
long fpathconf(int fd, int name);

子进程发送数据给父进程,父进程读取数据的例子:

#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main()
{
    // 需要在 fork 之前创建管道
    int aiPipeFd[2];
    int iRet = pipe(aiPipeFd);
    if (-1 == iRet) {
        perror("pipe create failed!\n");
        exit(0);
    }

    // 创建子进程
    pid_t tPid = fork();
    if (tPid > 0) {
        printf("I am parent, pid:%d\n", getpid());
        // 关闭管道的写端
        close(aiPipeFd[1]);
        char acBuf[1024] = {0};
        while(1) {
            int iLen = read(aiPipeFd[0], acBuf, sizeof(acBuf));
            printf("parent pid:%d, recv:%s\n", getpid(), acBuf);
            bzero(acBuf, 1024);
        }
    }
    else if (tPid == 0) {
        printf("I am child, pid:%d\n", getpid());
        // 关闭管道的读端
        close(aiPipeFd[0]);
        char* pStr = "Hello, I am child!";
        while(1) {
            write(aiPipeFd[1], pStr, strlen(pStr));
            sleep(1);
        }
    }
    return 0;
}

输出:

Image

获取管道的大小,其中 name 参数可以从 man fpathconf 中查询

#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main()
{
    int aiPipeFd[2];
    int iRet = pipe(aiPipeFd);
    if (-1 == iRet) {
        perror("pipe created failed!\n");
        exit(0);
    }

    // 获取管道的大小
    long lSize = fpathconf(aiPipeFd[0], _PC_PIPE_BUF);
    printf("pipe size:%ld\n", lSize);
    return 0;
}
管道的读写特点

使用管道时需要注意以下几种特殊情况(假设都是阻塞I/O操作):

  1. 如果所有的指向管道写端的文件描述符都被关闭了(管道写端引用计数为0),当有进程从管道读端读数据,管道中剩余的数据被读完之后,再次 read 会返回0,就像读到文件结尾一样。
  2. 如果指向管道写端的文件描述符没有被关闭(管道写端引用计数大于0),且持有管道写端描述符的进程没有向管道中写数据。此时如果有进程从管道中读数据,当管道中的数据被读完之后,再次 read 将会阻塞,直到管道中被写入了新数据才会读到数据并返回。
  3. 如果所有指向管道读端的文件描述符都被关闭了(管道读端引用计数为0),当有进程向管道中写数据,那么该进程会收到一个 SIGPIPE 信号,通常会导致进程异常终止。
  4. 如果指向管道读端的文件描述符没有被关闭(管道读端引用计数大于0),且持有管道读端描述符的进程没有从管道中读数据。此时如果有进程向管道中写数据,当管道被写满之后,再次 write 将会阻塞,直到管道中有空位置才会写入数据并返回。

总结:
读管道:

  1. 管道中有数据,read返回实际读到的字节数。
  2. 管道中无数据:
    1. 写端被全部关闭,read返回0(相当于读到文件末尾)
    2. 写端没有完全关闭,read阻塞等待。
      写管道:
  3. 管道读端全部被关闭,进程异常终止(进程收到 SIGPIPE 信号)
  4. 管道读端没有全部被关闭
    1. 管道已满,write阻塞
    2. 管道没有满,write写入数据并返回实际写入的字节数
posted @ 2025-04-22 09:21  luckilzy  阅读(21)  评论(0)    收藏  举报