当前位置:网站首页>你真的懂 “Binder 一次拷贝吗“?
你真的懂 “Binder 一次拷贝吗“?
2022-06-29 06:38:00 【码中之牛】
前言
谈到到Binder相对于其他传统进程间通信方式的优点的时候,我们总会说Binder只需要做“一次拷贝”就行了,而其他传统方式需要“两次拷贝”。这确实是Binder的优点,但再进一步思考就会碰到两个问题:
这所谓的“一次拷贝”到底是发生在什么地方?
拷贝的到底是什么东西?
而很多介绍Binder的文章会列出“一次拷贝”是其优点,但对上面的两个问题要么一笔带过,要么就是回答的并不完全正确,造成一些理解上的混乱。
本篇文章意在探索这两个问题的正确答案,所以需要读者对Binder驱动的工作过程和Binder驱动源码有一个大致的了解。
那么接下来就让我们带着这两个问题去源码的世界一探究竟。
源码
在看源码之前,我们需要先理一理一些关于Binder的预备知识。
- Binder的mmap发生在ProcessState的构造函数中,也就是一个进程就这么一块内存映射,大小大概是1M左右。
- 内核空间读写用户空间的数据是通过以下两个函数完成的:
- copy_from_user() 将数据从用户空间拷贝到内核空间。
- copy_to_user() 将数据从内核空间拷贝到用户空间。
- Binder驱动源码中有很多地方都会出现这两个函数的调用。我们需要搞清楚每次调用都是在拷贝些什么东西,拷贝到哪里去了。
- 为了抓住本文的“一次拷贝”这个点,下面源码引用会尽量集中在和内存操作相关的代码而暂时略过其他代码。
下面我们就先从内存映射说起
Binder内存映射
ProcessState构造函数
ProcessState::ProcessState(const char *driver)
{
if (mDriverFD >= 0) {
// mmap the binder, providing a chunk of virtual address space to receive transactions.
mVMStart = mmap(nullptr, BINDER_VM_SIZE, PROT_READ, MAP_PRIVATE | MAP_NORESERVE, mDriverFD, 0);
}
}
注意mmap上方的注释,已经说的很清楚了,这块内存映射只是作为接收transactions来使用的,也就是说往驱动写数据的时候是与内存映射无关的。记住这一点。
下面我们看一下内核空间相应的调用:
binder_mmap
static int binder_mmap(struct file *filp, struct vm_area_struct *vma)
{
...
ret = binder_alloc_mmap_handler(&proc->alloc, vma);
...
}
真正的映射操作由函数binder_alloc_mmap_handler()完成。这里我们只需要记住这个函数的第一个入参&proc->alloc。通过这个结构体我们就能找到映射好的内存块。
内存映射完成之后,就让我们看一下Binder的传输过程中哪里使用到了这块特殊的内存。
Binder传输过程
传输过程我们只关注内存和数据。
发起方用户空间
发起方用户空间做的事情其实就像发快递一样不停的打包,注意一下都包了些啥。
IPCThreadState::writeTransactionData
// 我们要传输的数据在data这个入参中
status_t IPCThreadState::writeTransactionData(... const Parcel& data...)
{
binder_transaction_data tr;
...
//tr.data.ptr.buffer保存了指向data的指针
tr.data_size = data.ipcDataSize();
tr.data.ptr.buffer = data.ipcData();
tr.offsets_size = data.ipcObjectsCount()*sizeof(binder_size_t);
tr.data.ptr.offsets = data.ipcObjects();
...
// 将tr写入mOut。
mOut.writeInt32(cmd);
mOut.write(&tr, sizeof(tr));
return NO_ERROR;
}
这里tr只保存了指向数据的指针。然后tr被写入mOut这个Parcel。
IPCThreadState::talkWithDriver
status_t IPCThreadState::talkWithDriver(bool doReceive)
{
...
binder_write_read bwr;
...
bwr.write_size = outAvail;
bwr.write_buffer = (uintptr_t)mOut.data();
...
bwr.write_consumed = 0;
...
ioctl(mProcess->mDriverFD, BINDER_WRITE_READ, &bwr) >= 0
...
}
这里又包了一层,bwr。其中bwr.write_buffer保存了指向mOut.data()的指针。在这里也就是指向了tr。
所以在发起方:
- bwr含有指向tr的指针。
- tr含有指向data的指针。
记住上面两点,接下来我们看一下内核空间是怎么取包的:
发起方内核空间
binder_ioctl_write_read
static int binder_ioctl_write_read(struct file *filp,
unsigned int cmd, unsigned long arg,
struct binder_thread **threadp)
{ ...
void __user *ubuf = (void __user *)arg;
struct binder_write_read bwr;
...
if (copy_from_user(&bwr, ubuf, sizeof(bwr))) {
ret = -EFAULT;
goto out;
}
...
ret = binder_thread_write(proc, *threadp,
bwr.write_buffer,
bwr.write_size,
&bwr.write_consumed);
}
这里我们遇到了第一个copy_from_user()调用。这个调用会把用户空间的bwr给拷贝到内核空间。但是要注意,copy_from_user()的第一个入参是拷贝的目标地址,这里给的是&bwr,函数内部的一个结构体。显然此处和内存映射没有关系。接下来就进入binder_thread_write。入参有bwr.write_buffer,回头看用户空间最底层那里,指向的是不是tr?
binder_thread_write
static int binder_thread_write(struct binder_proc *proc,
struct binder_thread *thread,
binder_uintptr_t binder_buffer, size_t size,
binder_size_t *consumed)
{
void __user *buffer = (void __user *)(uintptr_t)binder_buffer;
void __user *ptr = buffer + *consumed;
...
case BC_TRANSACTION:
case BC_REPLY: {
struct binder_transaction_data tr;
if (copy_from_user(&tr, ptr, sizeof(tr)))
return -EFAULT;
ptr += sizeof(tr);
binder_transaction(proc, thread, &tr,
cmd == BC_REPLY, 0);
break;
}
....
}
这里我们遇到了第二个copy_from_user()。这回会把用户空间的那个tr,也就是IPCThreadState.mOut,给拷贝到内核中来,看它的第一个入参,还是和内存映射没有关系。接下来就进入关键的binder_transaction()了。
binder_transaction
static void binder_transaction(struct binder_proc *proc,
struct binder_thread *thread,
struct binder_transaction_data *tr, int reply,
binder_size_t extra_buffers_size)
{
...
struct binder_transaction *t;
...
t->buffer = binder_alloc_new_buf(&target_proc->alloc, tr->data_size,
tr->offsets_size, extra_buffers_size,
!reply && (t->flags & TF_ONE_WAY));
...
copy_from_user(t->buffer->data, (const void __user *)(uintptr_t)
tr->data.ptr.buffer, tr->data_size)
...
off_start = (binder_size_t *)(t->buffer->data +
ALIGN(tr->data_size, sizeof(void *)));
offp = off_start;
...
copy_from_user(offp, (const void __user *)(uintptr_t)
tr->data.ptr.offsets, tr->offsets_size);
}
首先看一下t->buffer,函数binder_alloc_new_buf()的返回值会赋值给它,这里是我们第一次见到这个函数,从名字看是分配内存,看它的第一个入参&target_proc->alloc。现在回想前面说mmap的时候提到内存映射的信息会保存到proc->alloc这个结构体中。所以这里我们就可以确定现在是在接收方进程的内存映射中分配了一块内存出来。t->buffer就指向这块有映射的内存。
接下来就是我们遇到的第三次copy_from_user()调用了。回想在用户空间的时候tr.data.ptr.buffer是指向我们要传输的数据的。所以这里可以看到这个copy_from_user()的操作就是把发起方用户空间的数据直接拷贝到了接收方内核的内存映射中。 这就是所谓“一次拷贝”的关键点。
紧接着还有一个copy_from_user()调用,这里拷贝的是和数据相关的一些跨境程对象的偏移量,和前面拷贝bwr和tr在体量上来讲与数据的体量相比不是主要矛盾,所以说“一次拷贝”指的就是上面对数据的拷贝。
至此关于“一次拷贝”这个问题我们应该是已经有了初步的答案了,但为了让整个过程形成个闭环,接下来我们再来看一下Binder传输过程的后半段。
接收方内核空间
binder_thread_read
static int binder_thread_read(struct binder_proc *proc,
struct binder_thread **threadp,
binder_uintptr_t binder_buffer, size_t size,
binder_size_t *consumed, int non_block)
{
....
void __user *buffer = (void __user *)(uintptr_t)binder_buffer;
void __user *ptr = buffer + *consumed;
....
tr.data_size = t->buffer->data_size;
tr.offsets_size = t->buffer->offsets_size;
tr.data.ptr.buffer = (binder_uintptr_t)
((uintptr_t)t->buffer->data +
binder_alloc_get_user_buffer_offset(&proc->alloc));
tr.data.ptr.offsets = tr.data.ptr.buffer +
ALIGN(t->buffer->data_size,
sizeof(void *));
...
copy_to_user(ptr, &tr, sizeof(tr));
}
这里我们遇到了第一个copy_to_user()调用,这是把tr给拷贝到接收方的用户空间的IPCThreadState.mIn。在此之前把内核映射的数据地址指针转换为用户空间的指针赋值给tr.data.ptr.buffer。
binder_ioctl_write_read
static int binder_ioctl_write_read(struct file *filp,
unsigned int cmd, unsigned long arg,
struct binder_thread **threadp)
{
...
copy_to_user(ubuf, &bwr, sizeof(bwr));
...
}
最后我们遇到了第二个copy_to_user()。把bwr又拷贝回用户空间了,注意此时bwr内包含指向tr的指针。也就是bwr.read_buffer是指向这个tr,或者说IPCThreadState.mIn。
接收方用户空间
接下来就回到接收方的用户空间了:
IPCThreadState::executeCommand
status_t IPCThreadState::executeCommand(int32_t cmd)
{
...
case BR_TRANSACTION:
{
binder_transaction_data tr;
result = mIn.read(&tr, sizeof(tr));
...
Parcel buffer;
buffer.ipcSetDataReference(
reinterpret_cast<const uint8_t*>(tr.data.ptr.buffer),
tr.data_size,
reinterpret_cast<const binder_size_t*>(tr.data.ptr.offsets),
tr.offsets_size/sizeof(binder_size_t), freeBuffer, this);
...
error = reinterpret_cast<BBinder*>(tr.cookie)->transact(tr.code, buffer, &reply, tr.flags);
}
...
}
这里首先把tr从mIn里面读出来。然后就直接就把内存映射过来的指针tr.data.ptr.buffer,也就是那“一次拷贝”过来的地址,设置给buffer这个Parcel。这样下面的实体Binder就可以调用transact来处理发起方传过来的数据了。到这里应该明白最前面做mmap的那个注释了吧,内存映射确实只是用来接收Binder传输过来的数据的。
总结
对Binder“一次拷贝”的两个问题(什么时候拷贝和拷贝的是什么东西),相信大家已经有了一个初步的了解。这里我用一张图来总结一下上面介绍的内容:

图中表示了文中所讲的关键的copy_from_user和copy_to_user。斜着的那个绿色箭头就是“一次拷贝”所在之处。右侧接收方的两个绿色块代表内存映射。
关于对“一次拷贝”的理解以及内存映射在Binder通信中的作用如果不仔细去研究的话很容易被Binder驱动源码里那么多的copy_from_user和copy_to_user调用给搞混了。但是研究透了以后这个机制其实并不复杂。希望这篇文章能帮到大家。
最后
小编在网上收集了一些 Android 开发相关的学习文档、面试题、Android 核心笔记等等文档,希望能帮助到大家学习提升,如有想参考小编 PDF学习文档的如果有需要的可以 私信回复我 666 即可取货 !!! 里面记录许多Android 相关学习知识点。


边栏推荐
- 消息队列之通过幂等设计和原子锁避免重复退款
- Redis (V) of NoSQL database: redis_ Jedis_ test
- Move disassembly of exclusive delivery of script (the first time)
- . NETCORE uses redis to limit the number of interface accesses
- Qt 程序打包发布-windeployqt工具
- [translation] [Chapter 2 ②] mindshare PCI Express technology 3.0
- Better than postman! Apipost knows more about Chinese programmers! How delicious!
- Qt 串口编程
- Database - Synonyms
- List collection implements paging
猜你喜欢

Redis of NoSQL database (II): introduction to redis configuration file

package. Are you familiar with all configuration items and their usage of JSON
![[when OSPF introduces direct connection routes, it makes a summary by using static black hole routes]](/img/a8/f77cc5e43e1885171e73f8ab543ee4.png)
[when OSPF introduces direct connection routes, it makes a summary by using static black hole routes]

【软件测试】接口——基本测试流程

Open source 23 things shardingsphere and database mesh have to say

NoSQL数据库之Redis(四):Redis新数据类型

Annual inventory review of Alibaba cloud's observable practices in 2021

As a qualified network worker, you must master DHCP snooping knowledge!

Suggestions on digital transformation of large chemical enterprises

多模态 —— Learnable pooling with Context Gating for video classification
随机推荐
Ci tool Jenkins II: build a simple CI project
Qt STL类型迭代器
JDBC连接数据库,socket发送客户端。
Ribbon service invocation and load balancing
Suggestions on digital transformation of large chemical enterprises
Draw multiple ROC curves on a graph
json tobean
开源二三事|ShardingSphere 与 Database Mesh 之间不得不说的那些事
Ci tools Jenkins installation configuration tutorial
Json tobean
Effective methods for construction enterprises to select smart construction sites
Qt 容器类
centos下配置mysql 5.7 和 8
How to fix Error: Failed to download metadata for repo ‘appstream‘: Cannot prepare internal mirrorli
Redis of NoSQL database (II): introduction to redis configuration file
jetson tx2
Exclusive download. Alibaba cloud native brings 10+ technical experts to bring new possibilities of cloud native and cloud future
Uniapp obtains the date implementation of the beginning and end of the previous month and the next month
List集合实现分页
【OSPF引入直连路由时巧借静态黑洞路由做汇总】