前言
談到到Binder相對(duì)于其他傳統(tǒng)進(jìn)程間通信方式的優(yōu)點(diǎn)的時(shí)候,我們總會(huì)說(shuō)Binder只需要做“一次拷貝”就行了,而其他傳統(tǒng)方式需要“兩次拷貝”。這確實(shí)是Binder的優(yōu)點(diǎn),但再進(jìn)一步思考就會(huì)碰到兩個(gè)問(wèn)題:
- 這所謂的“一次拷貝”到底是發(fā)生在什么地方?
- 拷貝的到底是什么東西?
而很多介紹Binder的文章會(huì)列出“一次拷貝”是其優(yōu)點(diǎn),但對(duì)上面的兩個(gè)問(wèn)題要么一筆帶過(guò),要么就是回答的并不完全正確,造成一些理解上的混亂。
本篇文章意在探索這兩個(gè)問(wèn)題的正確答案,所以需要讀者對(duì)Binder驅(qū)動(dòng)的工作過(guò)程和Binder驅(qū)動(dòng)源碼有一個(gè)大致的了解,或者至少能看懂我的上一篇文章《聊聊怎樣學(xué)習(xí)Binder》。
那么接下來(lái)就讓我們帶著這兩個(gè)問(wèn)題去源碼的世界一探究竟。
源碼
在看源碼之前,我們需要先理一理一些關(guān)于Binder的預(yù)備知識(shí)。
- Binder的
mmap
發(fā)生在ProcessState
的構(gòu)造函數(shù)中,也就是一個(gè)進(jìn)程就這么一塊內(nèi)存映射,大小大概是1M左右。 - 內(nèi)核空間讀寫(xiě)用戶(hù)空間的數(shù)據(jù)是通過(guò)以下兩個(gè)函數(shù)完成的:
-
copy_from_user()
將數(shù)據(jù)從用戶(hù)空間拷貝到內(nèi)核空間。 -
copy_to_user()
將數(shù)據(jù)從內(nèi)核空間拷貝到用戶(hù)空間。
- Binder驅(qū)動(dòng)源碼中有很多地方都會(huì)出現(xiàn)這兩個(gè)函數(shù)的調(diào)用。我們需要搞清楚每次調(diào)用都是在拷貝些什么東西,拷貝到哪里去了。
- 為了抓住本文的“一次拷貝”這個(gè)點(diǎn),下面源碼引用會(huì)盡量集中在和內(nèi)存操作相關(guān)的代碼而暫時(shí)略過(guò)其他代碼。
下面我們就先從內(nèi)存映射說(shuō)起
Binder內(nèi)存映射
ProcessState構(gòu)造函數(shù)
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
上方的注釋?zhuān)呀?jīng)說(shuō)的很清楚了,這塊內(nèi)存映射只是作為接收transactions
來(lái)使用的,也就是說(shuō)往驅(qū)動(dòng)寫(xiě)數(shù)據(jù)的時(shí)候是與內(nèi)存映射無(wú)關(guān)的。記住這一點(diǎn)。
下面我們看一下內(nèi)核空間相應(yīng)的調(diào)用:
binder_mmap
static int binder_mmap(struct file *filp, struct vm_area_struct *vma)
{
...
ret = binder_alloc_mmap_handler(&proc->alloc, vma);
...
}
真正的映射操作由函數(shù)binder_alloc_mmap_handler()
完成。這里我們只需要記住這個(gè)函數(shù)的第一個(gè)入?yún)?code>&proc->alloc。通過(guò)這個(gè)結(jié)構(gòu)體我們就能找到映射好的內(nèi)存塊。
內(nèi)存映射完成之后,就讓我們看一下Binder的傳輸過(guò)程中哪里使用到了這塊特殊的內(nèi)存。
Binder傳輸過(guò)程
傳輸過(guò)程我們只關(guān)注內(nèi)存和數(shù)據(jù)。
發(fā)起方用戶(hù)空間
發(fā)起方用戶(hù)空間做的事情其實(shí)就像發(fā)快遞一樣不停的打包,注意一下都包了些啥。
IPCThreadState::writeTransactionData
// 我們要傳輸?shù)臄?shù)據(jù)在data這個(gè)入?yún)⒅?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寫(xiě)入mOut。
mOut.writeInt32(cmd);
mOut.write(&tr, sizeof(tr));
return NO_ERROR;
}
這里tr
只保存了指向數(shù)據(jù)的指針。然后tr
被寫(xiě)入mOut
這個(gè)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
。
所以在發(fā)起方:
-
bwr
含有指向tr
的指針。 -
tr
含有指向data
的指針。
記住上面兩點(diǎn),接下來(lái)我們看一下內(nèi)核空間是怎么取包的:
發(fā)起方內(nèi)核空間
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);
}
這里我們遇到了第一個(gè)copy_from_user()
調(diào)用。這個(gè)調(diào)用會(huì)把用戶(hù)空間的bwr
給拷貝到內(nèi)核空間。但是要注意,copy_from_user()
的第一個(gè)入?yún)⑹强截惖哪繕?biāo)地址,這里給的是&bwr
,函數(shù)內(nèi)部的一個(gè)結(jié)構(gòu)體。顯然此處和內(nèi)存映射沒(méi)有關(guān)系。接下來(lái)就進(jìn)入binder_thread_write
。入?yún)⒂?code>bwr.write_buffer,回頭看用戶(hù)空間最底層那里,指向的是不是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;
}
....
}
這里我們遇到了第二個(gè)copy_from_user()
。這回會(huì)把用戶(hù)空間的那個(gè)tr
,也就是IPCThreadState.mOut
,給拷貝到內(nèi)核中來(lái),看它的第一個(gè)入?yún)ⅲ€是和內(nèi)存映射沒(méi)有關(guān)系。接下來(lái)就進(jìn)入關(guān)鍵的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
,函數(shù)binder_alloc_new_buf()
的返回值會(huì)賦值給它,這里是我們第一次見(jiàn)到這個(gè)函數(shù),從名字看是分配內(nèi)存,看它的第一個(gè)入?yún)?code>&target_proc->alloc。現(xiàn)在回想前面說(shuō)mmap
的時(shí)候提到內(nèi)存映射的信息會(huì)保存到proc->alloc
這個(gè)結(jié)構(gòu)體中。所以這里我們就可以確定現(xiàn)在是在接收方進(jìn)程的內(nèi)存映射中分配了一塊內(nèi)存出來(lái)。t->buffer
就指向這塊有映射的內(nèi)存。
接下來(lái)就是我們遇到的第三次copy_from_user()
調(diào)用了。回想在用戶(hù)空間的時(shí)候tr.data.ptr.buffer
是指向我們要傳輸?shù)臄?shù)據(jù)的。所以這里可以看到這個(gè)copy_from_user()
的操作就是把發(fā)起方用戶(hù)空間的數(shù)據(jù)直接拷貝到了接收方內(nèi)核的內(nèi)存映射中。 這就是所謂“一次拷貝”的關(guān)鍵點(diǎn)。
緊接著還有一個(gè)copy_from_user()
調(diào)用,這里拷貝的是和數(shù)據(jù)相關(guān)的一些跨境程對(duì)象的偏移量,和前面拷貝bwr
和tr
在體量上來(lái)講與數(shù)據(jù)的體量相比不是主要矛盾,所以說(shuō)“一次拷貝”指的就是上面對(duì)數(shù)據(jù)的拷貝。
至此關(guān)于“一次拷貝”這個(gè)問(wèn)題我們應(yīng)該是已經(jīng)有了初步的答案了,但為了讓整個(gè)過(guò)程形成個(gè)閉環(huán),接下來(lái)我們?cè)賮?lái)看一下Binder傳輸過(guò)程的后半段。
接收方內(nèi)核空間
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));
}
這里我們遇到了第一個(gè)copy_to_user()
調(diào)用,這是把tr
給拷貝到接收方的用戶(hù)空間的IPCThreadState.mIn
。在此之前把內(nèi)核映射的數(shù)據(jù)地址指針轉(zhuǎn)換為用戶(hù)空間的指針賦值給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));
...
}
最后我們遇到了第二個(gè)copy_to_user()
。把bwr
又拷貝回用戶(hù)空間了,注意此時(shí)bwr
內(nèi)包含指向tr
的指針。也就是bwr.read_buffer
是指向這個(gè)tr
,或者說(shuō)IPCThreadState.mIn
。
接收方用戶(hù)空間
接下來(lái)就回到接收方的用戶(hù)空間了:
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
里面讀出來(lái)。然后就直接就把內(nèi)存映射過(guò)來(lái)的指針tr.data.ptr.buffer
,也就是那“一次拷貝”過(guò)來(lái)的地址,設(shè)置給buffer
這個(gè)Parcel
。這樣下面的實(shí)體Binder就可以調(diào)用transact
來(lái)處理發(fā)起方傳過(guò)來(lái)的數(shù)據(jù)了。到這里應(yīng)該明白最前面做mmap
的那個(gè)注釋了吧,內(nèi)存映射確實(shí)只是用來(lái)接收Binder傳輸過(guò)來(lái)的數(shù)據(jù)的。
總結(jié)
對(duì)Binder“一次拷貝”的兩個(gè)問(wèn)題(什么時(shí)候拷貝和拷貝的是什么東西),相信大家已經(jīng)有了一個(gè)初步的了解。這里我用一張圖來(lái)總結(jié)一下上面介紹的內(nèi)容:
圖中表示了文中所講的關(guān)鍵的copy_from_user
和copy_to_user
。斜著的那個(gè)綠色箭頭就是“一次拷貝”所在之處。右側(cè)接收方的兩個(gè)綠色塊代表內(nèi)存映射。
關(guān)于對(duì)“一次拷貝”的理解以及內(nèi)存映射在Binder通信中的作用如果不仔細(xì)去研究的話(huà)很容易被Binder驅(qū)動(dòng)源碼里那么多的copy_from_user
和copy_to_user
調(diào)用給搞混了。但是研究透了以后這個(gè)機(jī)制其實(shí)并不復(fù)雜。希望這篇文章能幫到大家。
我的博客即將同步至騰訊云+社區(qū),邀請(qǐng)大家一同入駐:https://cloud.tencent.com/developer/support-plan?invite_code=ioy2yvgosgha