当前位置:首页 > 公众号精选 > CPP开发者
[导读]处于安全的考虑,不同进程之间的内存空间是相互隔离的,也就是说 进程A 是不能访问 进程B 的内存空间,反之亦然。如果不同进程间能够相互访问和修改对方的内存,那么当前进程的内存就有可能被其他进程非法修改,从而导致安全隐患。不同的进程就像是大海上孤立的岛屿,它们之间不能直接相互通信,...


处于安全的考虑,不同进程之间的内存空间是相互隔离的,也就是说 进程A 是不能访问 进程B 的内存空间,反之亦然。如果不同进程间能够相互访问和修改对方的内存,那么当前进程的内存就有可能被其他进程非法修改,从而导致安全隐患。

不同的进程就像是大海上孤立的岛屿,它们之间不能直接相互通信,如下图所示:

但某些场景下,不同进程间需要相互通信,比如:进程A 负责处理用户的请求,而 进程B 负责保存处理后的数据。那么当 进程A 处理完请求后,就需要把处理后的数据提交给 进程B 进行存储。此时,进程A 就需要与 进程B 进行通信。如下图所示:

由于不同进程间是相互隔离的,所以必须借助内核来作为桥梁来进行相互通信,内核相当于岛屿之间的轮船,如下图所示:

内核提供多种进程间通信的方式,如:共享内存信号消息队列 和 管道(pipe) 等。本文主要介绍 管道 的原理与实现。

一、管道的使用

管道 一般用于父子进程之间相互通信,一般的用法如下:

  • 父进程使用 pipe 系统调用创建一个管道。
  • 然后父进程使用 fork 系统调用创建一个子进程。
  • 由于子进程会继承父进程打开的文件句柄,所以父子进程可以通过新创建的管道进行通信。
其原理如下图所示:

由于管道分为读端和写端,所以需要两个文件描述符来管理管道:fd[0] 为读端,fd[1] 为写端。

下面代码介绍了怎么使用 pipe 系统调用来创建一个管道:

#include 
#include 
#include 
#include 
#include 

int main()
{
    int ret = -1;
    int fd[2];  // 用于管理管道的文件描述符
    pid_t pid;
    char buf[512] = {0};
    char *msg = "hello world";

    // 创建一个管理
    ret = pipe(fd);
    if (-1 == ret) {
        printf("failed to create pipe\n");
        return -1;
    }
  
    pid = fork();     // 创建子进程

    if (0 == pid) {   // 子进程
        close(fd[0]); // 关闭管道的读端
        ret = write(fd[1], msg, strlen(msg)); // 向管道写端写入数据
        exit(0);
    } else {          // 父进程
        close(fd[1]); // 关闭管道的写端
        ret = read(fd[0], buf, sizeof(buf)); // 从管道的读端读取数据
        printf("parent read %d bytes data: %s\n", ret, buf);
    }

    return 0;
}
编译代码:

[root@localhost pipe]# gcc -g pipe.c -o pipe
运行代码,输出结果如下:

[root@localhost pipe]# ./pipe
parent read 11 bytes data: hello world

二、管道的实现

每个进程的用户空间都是独立的,但内核空间却是共用的。所以,进程间通信必须由内核提供服务。前面介绍了 管道(pipe) 的使用,接下来将会介绍管道在内核中的实现方式。

本文使用 Linux-2.6.23 内核作为分析对象。

1. 环形缓冲区(Ring Buffer)

在内核中,管道 使用了环形缓冲区来存储数据。环形缓冲区的原理是:把一个缓冲区当成是首尾相连的环,其中通过读指针和写指针来记录读操作和写操作位置。如下图所示:


在 Linux 内核中,使用了 16 个内存页作为环形缓冲区,所以这个环形缓冲区的大小为 64KB(16 * 4KB)。

当向管道写数据时,从写指针指向的位置开始写入,并且将写指针向前移动。而从管道读取数据时,从读指针开始读入,并且将读指针向前移动。当对没有数据可读的管道进行读操作,将会阻塞当前进程。而对没有空闲空间的管道进行写操作,也会阻塞当前进程。

注意:可以将管道文件描述符设置为非阻塞,这样对管道进行读写操作时,就不会阻塞当前进程。

2. 管道对象

在 Linux 内核中,管道使用 pipe_inode_info 对象来进行管理。我们先来看看 pipe_inode_info 对象的定义,如下所示:

struct pipe_inode_info {
    wait_queue_head_t wait;
    unsigned int nrbufs,
    unsigned int curbuf;
    ...
    unsigned int readers;
    unsigned int writers;
    unsigned int waiting_writers;
    ...
    struct inode *inode;
    struct pipe_buffer bufs[16];
};
下面介绍一下 pipe_inode_info 对象各个字段的作用:

  • wait:等待队列,用于存储正在等待管道可读或者可写的进程。
  • bufs:环形缓冲区,由 16 个 pipe_buffer 对象组成,每个 pipe_buffer 对象拥有一个内存页 ,后面会介绍。
  • nrbufs:表示未读数据已经占用了环形缓冲区的多少个内存页。
  • curbuf:表示当前正在读取环形缓冲区的哪个内存页中的数据。
  • readers:表示正在读取管道的进程数。
  • writers:表示正在写入管道的进程数。
  • waiting_writers:表示等待管道可写的进程数。
  • inode:与管道关联的 inode 对象。
由于环形缓冲区是由 16 个 pipe_buffer 对象组成,所以下面我们来看看 pipe_buffer 对象的定义:

struct pipe_buffer {
    struct page *page;
    unsigned int offset;
    unsigned int len;
    ...
};
下面介绍一下 pipe_buffer 对象各个字段的作用:

  • page:指向 pipe_buffer 对象占用的内存页。
  • offset:如果进程正在读取当前内存页的数据,那么 offset 指向正在读取当前内存页的偏移量。
  • len:表示当前内存页拥有未读数据的长度。
下图展示了 pipe_inode_info 对象与 pipe_buffer 对象的关系:

管道的环形缓冲区实现方式与经典的环形缓冲区实现方式有点区别,经典的环形缓冲区一般先申请一块地址连续的内存块,然后通过读指针与写指针来对读操作与写操作进行定位。

但为了减少对内存的使用,内核不会在创建管道时就申请 64K 的内存块,而是在进程向管道写入数据时,按需来申请内存。

那么当进程从管道读取数据时,内核怎么处理呢?下面我们来看看管道读操作的实现方式。

3. 读操作

从 经典的环形缓冲区 中读取数据时,首先通过读指针来定位到读取数据的起始地址,然后判断环形缓冲区中是否有数据可读,如果有就从环形缓冲区中读取数据到用户空间的缓冲区中。如下图所示:


而 管道的环形缓冲区 与 经典的环形缓冲区 实现稍有不同,管道的环形缓冲区 其读指针是由 pipe_inode_info 对象的 curbuf 字段与 pipe_buffer 对象的 offset 字段组合而成:

  • pipe_inode_info 对象的 curbuf 字段表示读操作要从 bufs 数组的哪个 pipe_buffer 中读取数据。
  • pipe_buffer 对象的 offset 字段表示读操作要从内存页的哪个位置开始读取数据。
读取数据的过程如下图所示:


从缓冲区中读取到 n 个字节的数据后,会相应移动读指针 n 个字节的位置(也就是增加 pipe_buffer 对象的 offset 字段),并且减少 n 个字节的可读数据长度(也就是减少 pipe_buffer 对象的 len 字段)。

当 pipe_buffer 对象的 len 字段变为 0 时,表示当前 pipe_buffer 没有可读数据,那么将会对 pipe_inode_info 对象的 curbuf 字段移动一个位置,并且其 nrbufs 字段进行减一操作。

我们来看看管道读操作的代码实现,读操作由 pipe_read 函数完成。为了突出重点,我们只列出关键代码,如下所示:

static ssize_t
pipe_read(struct kiocb *iocb, const struct iovec *_iov, unsigned long nr_segs,
          loff_t pos)

{
    ...
    struct pipe_inode_info *pipe;

    // 1. 获取管道对象
    pipe = inode->i_pipe;

    for (;;) {
        // 2. 获取管道未读数据占有多少个内存页
        int bufs = pipe->nrbufs;

        if (bufs) {
            // 3. 获取读操作应该从环形缓冲区的哪个内存页处读取数据
            int curbuf = pipe->curbuf;  
            struct pipe_buffer *buf = pipe->bufs   curbuf;
            ...

            /* 4. 通过 pipe_buffer 的 offset 字段获取真正的读指针,
             *    并且从管道中读取数据到用户缓冲区.
             */

            error = pipe_iov_copy_to_user(iov, addr   buf->offset, chars, atomic);
            ...

            ret  = chars;
            buf->offset  = chars; // 增加 pipe_buffer 对象的 offset 字段的值
            buf->len -= chars;    // 减少 pipe_buffer 对象的 len 字段的值

            /* 5. 如果当前内存页的数据已经被读取完毕 */
            if (!buf->len) {
                ...
                curbuf = (curbuf   1
本站声明: 本文章由作者或相关机构授权发布,目的在于传递更多信息,并不代表本站赞同其观点,本站亦不保证或承诺内容真实性等。需要转载请联系该专栏作者,如若文章内容侵犯您的权益,请及时联系本站删除。
换一批
延伸阅读

9月2日消息,不造车的华为或将催生出更大的独角兽公司,随着阿维塔和赛力斯的入局,华为引望愈发显得引人瞩目。

关键字: 阿维塔 塞力斯 华为

加利福尼亚州圣克拉拉县2024年8月30日 /美通社/ -- 数字化转型技术解决方案公司Trianz今天宣布,该公司与Amazon Web Services (AWS)签订了...

关键字: AWS AN BSP 数字化

伦敦2024年8月29日 /美通社/ -- 英国汽车技术公司SODA.Auto推出其旗舰产品SODA V,这是全球首款涵盖汽车工程师从创意到认证的所有需求的工具,可用于创建软件定义汽车。 SODA V工具的开发耗时1.5...

关键字: 汽车 人工智能 智能驱动 BSP

北京2024年8月28日 /美通社/ -- 越来越多用户希望企业业务能7×24不间断运行,同时企业却面临越来越多业务中断的风险,如企业系统复杂性的增加,频繁的功能更新和发布等。如何确保业务连续性,提升韧性,成...

关键字: 亚马逊 解密 控制平面 BSP

8月30日消息,据媒体报道,腾讯和网易近期正在缩减他们对日本游戏市场的投资。

关键字: 腾讯 编码器 CPU

8月28日消息,今天上午,2024中国国际大数据产业博览会开幕式在贵阳举行,华为董事、质量流程IT总裁陶景文发表了演讲。

关键字: 华为 12nm EDA 半导体

8月28日消息,在2024中国国际大数据产业博览会上,华为常务董事、华为云CEO张平安发表演讲称,数字世界的话语权最终是由生态的繁荣决定的。

关键字: 华为 12nm 手机 卫星通信

要点: 有效应对环境变化,经营业绩稳中有升 落实提质增效举措,毛利润率延续升势 战略布局成效显著,战新业务引领增长 以科技创新为引领,提升企业核心竞争力 坚持高质量发展策略,塑强核心竞争优势...

关键字: 通信 BSP 电信运营商 数字经济

北京2024年8月27日 /美通社/ -- 8月21日,由中央广播电视总台与中国电影电视技术学会联合牵头组建的NVI技术创新联盟在BIRTV2024超高清全产业链发展研讨会上宣布正式成立。 活动现场 NVI技术创新联...

关键字: VI 传输协议 音频 BSP

北京2024年8月27日 /美通社/ -- 在8月23日举办的2024年长三角生态绿色一体化发展示范区联合招商会上,软通动力信息技术(集团)股份有限公司(以下简称"软通动力")与长三角投资(上海)有限...

关键字: BSP 信息技术
关闭