处于安全的考虑,不同进程之间的内存空间是相互隔离的,也就是说 进程A
是不能访问 进程B
的内存空间,反之亦然。如果不同进程间能够相互访问和修改对方的内存,那么当前进程的内存就有可能被其他进程非法修改,从而导致安全隐患。
不同的进程就像是大海上孤立的岛屿,它们之间不能直接相互通信,如下图所示:
但某些场景下,不同进程间需要相互通信,比如:进程A
负责处理用户的请求,而 进程B
负责保存处理后的数据。那么当 进程A
处理完请求后,就需要把处理后的数据提交给 进程B
进行存储。此时,进程A
就需要与 进程B
进行通信。如下图所示:
由于不同进程间是相互隔离的,所以必须借助内核来作为桥梁来进行相互通信,内核相当于岛屿之间的轮船,如下图所示:
内核提供多种进程间通信的方式,如:共享内存
,信号
,消息队列
和 管道(pipe)
等。本文主要介绍 管道
的原理与实现。
管道
一般用于父子进程之间相互通信,一般的用法如下:
pipe
系统调用创建一个管道。fork
系统调用创建一个子进程。其原理如下图所示:
由于管道分为读端和写端,所以需要两个文件描述符来管理管道:fd[0]
为读端,fd[1]
为写端。
下面代码介绍了怎么使用 pipe
系统调用来创建一个管道:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>
#include <string.h>
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 内核作为分析对象。
在内核中,管道
使用了环形缓冲区来存储数据。环形缓冲区的原理是:把一个缓冲区当成是首尾相连的环,其中通过读指针和写指针来记录读操作和写操作位置。如下图所示:
在 Linux 内核中,使用了 16 个内存页作为环形缓冲区,所以这个环形缓冲区的大小为 64KB(16 * 4KB)。
当向管道写数据时,从写指针指向的位置开始写入,并且将写指针向前移动。而从管道读取数据时,从读指针开始读入,并且将读指针向前移动。当对没有数据可读的管道进行读操作,将会阻塞当前进程。而对没有空闲空间的管道进行写操作,也会阻塞当前进程。
注意:可以将管道文件描述符设置为非阻塞,这样对管道进行读写操作时,就不会阻塞当前进程。
在 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 的内存块,而是在进程向管道写入数据时,按需来申请内存。
那么当进程从管道读取数据时,内核怎么处理呢?下面我们来看看管道读操作的实现方式。
从 经典的环形缓冲区
中读取数据时,首先通过读指针来定位到读取数据的起始地址,然后判断环形缓冲区中是否有数据可读,如果有就从环形缓冲区中读取数据到用户空间的缓冲区中。如下图所示:
而 管道的环形缓冲区
与 经典的环形缓冲区
实现稍有不同,管道的环形缓冲区
其读指针是由 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) & (PIPE_BUFFERS - 1);
pipe->curbuf = curbuf; // 移动 pipe_inode_info 对象的 curbuf 指针
pipe->nrbufs = --bufs; // 减少 pipe_inode_info 对象的 nrbufs 字段
do_wakeup = 1;
}
total_len -= chars;
// 6. 如果读取到用户期望的数据长度, 退出循环
if (!total_len)
break;
}
...
}
...
return ret;
}
上面代码总结来说分为以下步骤:
inode
对象来获取到管道的 pipe_inode_info
对象。pipe_inode_info
对象的 nrbufs
字段获取管道未读数据占有多少个内存页。pipe_inode_info
对象的 curbuf
字段获取读操作应该从环形缓冲区的哪个内存页处读取数据。pipe_buffer
对象的 offset
字段获取真正的读指针, 并且从管道中读取数据到用户缓冲区。pipe_inode_info
对象的 curbuf
指针,并且减少其 nrbufs
字段的值。分析完管道读操作的实现后,接下来,我们分析一下管道写操作的实现。
经典的环形缓冲区
写入数据时,首先通过写指针进行定位要写入的内存地址,然后判断环形缓冲区的空间是否足够,足够就把数据写入到环形缓冲区中。如下图所示:
但 管道的环形缓冲区
并没有保存 写指针
,而是通过 读指针
计算出来。那么怎么通过读指针计算出写指针呢?
其实很简单,就是:
写指针 = 读指针 + 未读数据长度
下面我们来看看,向管道写入 200 字节数据的过程示意图,如下所示:
如上图所示,向管道写入数据时:
pipe_inode_info
的 curbuf
字段和 nrbufs
字段来定位到,应该向哪个 pipe_buffer
写入数据。pipe_buffer
对象的 offset
字段和 len
字段来定位到,应该写入到内存页的哪个位置。下面我们通过源码来分析,写操作是怎么实现的,代码如下(为了特出重点,代码有所删减):
static ssize_t
pipe_write(struct kiocb *iocb, const struct iovec *_iov, unsigned long nr_segs,
loff_t ppos)
{
...
struct pipe_inode_info *pipe;
...
pipe = inode->i_pipe;
...
chars = total_len & (PAGE_SIZE - 1); /* size of the last buffer */
// 1. 如果最后写入的 pipe_buffer 还有空闲的空间
if (pipe->nrbufs && chars != 0) {
// 获取写入数据的位置
int lastbuf = (pipe->curbuf + pipe->nrbufs - 1) & (PIPE_BUFFERS-1);
struct pipe_buffer *buf = pipe->bufs + lastbuf;
const struct pipe_buf_operations *ops = buf->ops;
int offset = buf->offset + buf->len;
if (ops->can_merge && offset + chars <= PAGE_SIZE) {
...
error = pipe_iov_copy_from_user(offset + addr, iov, chars, atomic);
...
buf->len += chars;
total_len -= chars;
ret = chars;
// 如果要写入的数据已经全部写入成功, 退出循环
if (!total_len)
goto out;
}
}
// 2. 如果最后写入的 pipe_buffer 空闲空间不足, 那么申请一个新的内存页来存储数据
for (;;) {
int bufs;
...
bufs = pipe->nrbufs;
if (bufs < PIPE_BUFFERS) {
int newbuf = (pipe->curbuf + bufs) & (PIPE_BUFFERS-1);
struct pipe_buffer *buf = pipe->bufs + newbuf;
...
// 申请一个新的内存页
if (!page) {
page = alloc_page(GFP_HIGHUSER);
...
}
...
error = pipe_iov_copy_from_user(src, iov, chars, atomic);
...
ret += chars;
buf->page = page;
buf->ops = &anon_pipe_buf_ops;
buf->offset = 0;
buf->len = chars;
pipe->nrbufs = ++bufs;
pipe->tmp_page = NULL;
// 如果要写入的数据已经全部写入成功, 退出循环
total_len -= chars;
if (!total_len)
break;
}
...
}
out:
...
return ret;
}
上面代码有点长,但是逻辑却很简单,主要进行如下操作:
pipe_buffer
还有空闲的空间,那么就将数据写入到此 pipe_buffer
中,并且增加其 len
字段的值。pipe_buffer
没有足够的空闲空间,那么就新申请一个内存页,并且把数据保存到新的内存页中,并且增加 pipe_inode_info
的 nrbufs
字段的值。管道读写操作的实现已经分析完毕,现在我们来思考一下以下问题。
这是因为父子进程通过 pipe
系统调用打开的管道,在内核空间中指向同一个管道对象(pipe_inode_info
)。所以父子进程共享着同一个管道对象,那么就可以通过这个共享的管道对象进行通信。
这是为了减少内存使用。
因为使用 pipe
系统调用打开管道时,并没有立刻申请内存页,而是当有进程向管道写入数据时,才会按需申请内存页。当内存页的数据被读取完后,内核会将此内存页回收,来减少管道对内存的使用。
本文由哈喽比特于3年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/Fy8XH-I8A1L1JWHQKPkOag
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。