android 為了高效的 IPC 通信做了很多工作膜钓,內(nèi)存管理就屬于其中之一。傳統(tǒng)的 IPC 傳遞數(shù)據(jù)卿嘲,至少需要2次拷貝颂斜,一次為進(jìn)程1到內(nèi)核,一次為內(nèi)核到進(jìn)程2拾枣,但是得益 android binder 的內(nèi)存管理沃疮,數(shù)據(jù)拷貝只有1次,就從這里速度比傳統(tǒng)的要快1倍梅肤。這里慢慢分析司蔬。還是先說下相關(guān)代碼的位置(其實還有很多 linux 編程的相關(guān)基礎(chǔ)知識):
# native binder 頭文件
frameworks/native/include/binder
# native binder 實現(xiàn)
frameworks/native/libs/binder
# kernel binder 驅(qū)動
kernel/drivers/staging/android/binder.h
kernel/drivers/staging/android/binder.c
基礎(chǔ)知識
首先得說下一些相關(guān)的基礎(chǔ)知識。這里很多我都是找網(wǎng)上現(xiàn)成的姨蝴,因為這方面的基礎(chǔ)知識我?guī)缀鯙? -_-||俊啼。先說下,為什么 linux 要使用虛擬地址映射物理地址左医,內(nèi)存為什么要分頁:Linux中的內(nèi)存管理
然后還有這篇有介紹 linux 用戶空間和內(nèi)核空間的:Linux用戶空間與內(nèi)核空間
然后 linux 中的內(nèi)存是分頁的授帕,也就是說要按照頁大小對齊。這個在后面內(nèi)存分配那里能夠體現(xiàn)出來浮梢,這里先提前說一下跛十。
原理
前面幾篇也說過了,IPC 中最基本的問題在于進(jìn)程間使用的虛擬地址空間是相互獨立的秕硝,不能直接訪問芥映,所以要相互訪問,就要借助 kernel ,就是要讓數(shù)據(jù)用用戶空間進(jìn)入到內(nèi)核空間奈偏,然后再去到另一個進(jìn)程的用戶空間坞嘀。傳統(tǒng)的 IPC 是這樣的,其實 binder 也是這樣的霎苗,不過它把內(nèi)核空間的地址和用戶空間的虛擬地址映射到了同一段物理地址上姆吭,所以就只需要把數(shù)據(jù)從原始用戶空間復(fù)制到內(nèi)核空間榛做,把目標(biāo)進(jìn)程用戶空間和內(nèi)核空間映射到同一段物理地址唁盏,這樣第一次復(fù)制到內(nèi)核空間,其實目標(biāo)的用戶空間上也有這段數(shù)據(jù)了检眯。這就是 binder 比傳統(tǒng) IPC 高效的一個原因厘擂。
這么抽象的文字,不太好理解吧锰瘸,下面從代碼慢慢看吧:
只復(fù)制一次的實現(xiàn)
首先 ProcessState 初始化的構(gòu)造函數(shù):
ProcessState::ProcessState()
: mDriverFD(open_driver())
, mVMStart(MAP_FAILED)
, mManagesContexts(false)
, mBinderContextCheckFunc(NULL)
, mBinderContextUserData(NULL)
, mThreadPoolStarted(false)
, mThreadPoolSeq(1)
{
if (mDriverFD >= 0) {
// XXX Ideally, there should be a specific define for whether we
// have mmap (or whether we could possibly have the kernel module
// availabla).
#if !defined(HAVE_WIN32_IPC)
// mmap the binder, providing a chunk of virtual address space to receive transactions.
mVMStart = mmap(0, BINDER_VM_SIZE, PROT_READ, MAP_PRIVATE | MAP_NORESERVE, mDriverFD, 0);
if (mVMStart == MAP_FAILED) {
// *sigh*
ALOGE("Using /dev/binder failed: unable to mmap transaction memory.\n");
close(mDriverFD);
mDriverFD = -1;
}
#else
mDriverFD = -1;
#endif
}
}
那個 open_driver()
是打開 /dev/binder 這個設(shè)備節(jié)點刽严。額,這里多扯下這個設(shè)備節(jié)點相關(guān)的知識避凝。這個是 android 為 binder 驅(qū)動創(chuàng)建的虛擬設(shè)備節(jié)點舞萄。什么叫虛擬的咧,像觸摸屏管削、傳感器這些設(shè)備節(jié)點是有真實的物理設(shè)備的倒脓,但是 binder 確沒有,只是為 IPC 創(chuàng)建的驅(qū)動而已含思,所以是虛擬的崎弃。android 上 linux 不支持動態(tài)創(chuàng)建設(shè)備節(jié)點,所有的設(shè)備節(jié)點都是通過 system/core/init 這個 init 程序創(chuàng)建的含潘。這個東西有個配置文件饲做,可以配置要創(chuàng)建哪里設(shè)備節(jié)點,默認(rèn)的在 system/core/rootdir/ueventd.rc 這個文件中:
/dev/null 0666 root root/dev/zero 0666 root root/dev/full 0666 root root/dev/ptmx 0666 root root/dev/tty 0666 root root/dev/random 0666 root root/dev/urandom 0666 root root/dev/ashmem 0666 root root/dev/binder 0666 root root
可以在 devices 下面 ueventd.xx.rc 加上自己機器板子上相關(guān)的設(shè)備節(jié)點遏弱。
好盆均,回到打開 /dev/binder 那,這里有個 mmap 漱逸,前面有一篇說到這里看注釋這是映射給 binder 驅(qū)動接受數(shù)據(jù)用的 buffer泪姨,但是你搜索完整個 binder 模塊(frameworks/native/lib/binder)發(fā)現(xiàn)沒一個地方使用 mVMStart 這個返回的映射地址空間。注意一下 mmap 設(shè)置的標(biāo)志: PROT_READ
虹脯。這個標(biāo)志說明映射的這段內(nèi)存是只讀的驴娃,當(dāng)然在這個模塊沒使用(其實讀也沒用到)。
其實映射的這段內(nèi)存是內(nèi)核的 binder 驅(qū)動在使用循集,同時也在管理唇敞,而且這里也是前面提到的 IPC 中只復(fù)制一次的實現(xiàn)的地方。
我們?nèi)?binder 驅(qū)動中看下, mmap 經(jīng)過系統(tǒng)調(diào)用疆柔,最后會調(diào)用 binder 驅(qū)動的 binder_mmap:
static int binder_mmap(struct file *filp, struct vm_area_struct *vma)
{
int ret;
struct vm_struct *area;
struct binder_proc *proc = filp->private_data;
const char *failure_string;
struct binder_buffer *buffer;
// 限制下映射內(nèi)存的大小咒精,最大不超過 4M
if ((vma->vm_end - vma->vm_start) > SZ_4M)
vma->vm_end = vma->vm_start + SZ_4M;
if (vma->vm_flags & FORBIDDEN_MMAP_FLAGS) {
ret = -EPERM;
failure_string = "bad vm_flags";
goto err_bad_arg;
}
vma->vm_flags = (vma->vm_flags | VM_DONTCOPY) & ~VM_MAYWRITE;
if (proc->buffer) {
ret = -EBUSY;
failure_string = "already mapped";
goto err_already_mapped;
}
// 在內(nèi)核中申請一段內(nèi)存空間,和用戶空間的 malloc 差不多吧旷档,我對內(nèi)核方面的 api 了解基本為 0 -_-||
area = get_vm_area(vma->vm_end - vma->vm_start, VM_IOREMAP);
if (area == NULL) {
ret = -ENOMEM;
failure_string = "get_vm_area";
goto err_get_vm_area_failed;
}
proc->buffer = area->addr;
proc->user_buffer_offset = vma->vm_start - (uintptr_t)proc->buffer;
ifdef CONFIG_CPU_CACHE_VIPT
if (cache_is_vipt_aliasing()) {
while (CACHE_COLOUR((vma->vm_start ^ (uint32_t)proc->buffer))) {
printk(KERN_INFO "binder_mmap: %d %lx-%lx maps %p bad alignment\n", proc->pid, vma->vm_start, vma->vm_end, proc->buffer);
vma->vm_start += PAGE_SIZE;
}
}
endif
// 申請好內(nèi)存頁面結(jié)構(gòu)所占用的內(nèi)存
proc->pages = kzalloc(sizeof(proc->pages[0]) * ((vma->vm_end - vma->vm_start) / PAGE_SIZE), GFP_KERNEL);
if (proc->pages == NULL) {
ret = -ENOMEM;
failure_string = "alloc page array";
goto err_alloc_pages_failed;
}
proc->buffer_size = vma->vm_end - vma->vm_start;
vma->vm_ops = &binder_vm_ops;
vma->vm_private_data = proc;
// 映射一個內(nèi)存頁(把內(nèi)核空間和用戶空間同時映射到同一物理地址)
if (binder_update_page_range(proc, 1, proc->buffer, proc->buffer + PAGE_SIZE, vma)) {
ret = -ENOMEM;
failure_string = "alloc small buf";
goto err_alloc_small_buf_failed;
}
// 把分配好內(nèi)存插入到對應(yīng)的表中(空閑內(nèi)存表)
buffer = proc->buffer;
INIT_LIST_HEAD(&proc->buffers);
list_add(&buffer->entry, &proc->buffers);
buffer->free = 1;
binder_insert_free_buffer(proc, buffer);
proc->free_async_space = proc->buffer_size / 2;
barrier();
proc->files = get_files_struct(current);
proc->vma = vma;
printk(KERN_INFO "binder_mmap: %d %lx-%lx maps %p\n",
proc->pid, vma->vm_start, vma->vm_end, proc->buffer);
return 0;
err_alloc_small_buf_failed:
kfree(proc->pages);
proc->pages = NULL;
err_alloc_pages_failed:
vfree(proc->buffer);
proc->buffer = NULL;
err_get_vm_area_failed:
err_already_mapped:
err_bad_arg:
printk(KERN_ERR "binder_mmap: %d %lx-%lx %s failed %d\n",
proc->pid, vma->vm_start, vma->vm_end, failure_string, ret);
return ret;
}
額模叙,從這開始要涉及到內(nèi)核的一些相關(guān)知識,我其實對這些一竅不通鞋屈,這里推薦去看下這里:[binder驅(qū)動———-之內(nèi)存映射篇](http://blog.csdn.net/xiaojsj111/article/details/31422175)
其實我能弄明白 binder 的內(nèi)存管理范咨,很大程序上得感覺這篇博文的博,他已經(jīng)說得挺清楚了的厂庇,但是還是自己再整一次記得比較深渠啊。
對應(yīng)程序 map 內(nèi)存的情況,可以通過 cat /proc/pid/maps 查看权旷,普通走 android binder 封裝的程序(就是 ProcessState 那)映射的內(nèi)存大小是 1M 左右:
#define BINDER_VM_SIZE ((1*1024*1024) - (4096 *2))
唯獨特殊的 servicemanager (它是直接通過 ioctl 來使用 binder 的)映射的內(nèi)存比較刑骝取(frameworks/base/cmds/servicemanager/service_manager.c):
```java
int main(int argc, char **argv)
{
struct binder_state *bs;
void *svcmgr = BINDER_SERVICE_MANAGER;
bs = binder_open(128*1024);
if (binder_become_context_manager(bs)) {
ALOGE("cannot become context manager (%s)\n", strerror(errno));
return -1;
}
svcmgr_handle = svcmgr;
binder_loop(bs, svcmgr_handler);
return 0;
}
這片內(nèi)存是接收 IPC 中客戶發(fā)送過來的數(shù)據(jù)的,所以之前網(wǎng)上有人說使用 bundle 在 activity 之間傳遞數(shù)據(jù)不能太大拄氯,如果超過 1M 就會出現(xiàn)錯誤《悴椋現(xiàn)在明白了吧,bundle 最后是通過 binder 來傳遞數(shù)據(jù)的译柏,底層接收數(shù)據(jù)的 buffer 一共才 1M(其實比1M小點镣煮,還減了8k去咧),超過 1M 肯定就失敗啦艇纺。那 servicemanager 為什么才開 128k 的空間呢怎静,去看看 servicemanager 的接口就知道了,它一共才3個接口:addService黔衡、getSerivce蚓聘、checkSerivce,參數(shù)都沒幾個盟劫,所以 128k 肯定夠了夜牡。上張圖來看看吧:
servermanager /dev/binder map 的內(nèi)存區(qū)域是: 40185000 - 401a5000(16進(jìn)制的) 正好是 128*1024。那個 k7service 是我寫的一個測試的 native 小程序(Bn端的)侣签,它 /dev/binder map 的內(nèi)存區(qū)域是: 4020e000 - 4030c000塘装,也正好是 1M - 4k。
vma 這個變量是 mmap 調(diào)用后影所,系統(tǒng)傳過來的蹦肴,包含了內(nèi)核分配給這次 mmap 映射的內(nèi)存地址的一些信息,其中就有比較重要的起始和結(jié)束地址: vma->vm_start
和 vma->vm_end
猴娩。然后我們慢慢往下看阴幌,后面通過傳遞過來的地址計算出要映射的內(nèi)存大猩撞(這里注意下,vma 里面地址是用戶空間的地址)矛双,然后使用 get_vm_area
這個調(diào)用向內(nèi)核申請一片內(nèi)存空間渊抽。這個申請的是內(nèi)核空間的內(nèi)存,感覺有點像上層的 malloc议忽,我對這個也不是很了解懒闷,只能先這么認(rèn)為了。再注意一點這里只是向內(nèi)核申請了一片內(nèi)存空間而已栈幸,還是真正的分配物理地址(建立虛擬地址到物理地址的映射關(guān)系)愤估。
申請成功后,會返回一個 vm_struct
的結(jié)構(gòu)侦镇,里面有描述這片內(nèi)存區(qū)域的信息灵疮,這里把內(nèi)核這片區(qū)域的起始地址 addr 保存在了 proc->buffer
這個變量里面织阅。然后后面那個 proc->user_buffer_offset
這個變量的計算很關(guān)鍵:
proc->user_buffer_offset = vma->vm_start - (uintptr_t)proc->buffer;
拿用戶空間的地址去減內(nèi)核空間的地址壳繁,得到這2個地址的偏移量(這個名字取得也很直接: user_buffer_offset
)。前面給的一篇參考文章里說到荔棉,32位的 linux(目前的 android 還都是32位的)總共可用內(nèi)存空間 4G闹炉,0G~3G 為用戶空間(也就是應(yīng)用程序使用的空間),3G~4G 為內(nèi)核空間润樱,用戶空間無法訪問內(nèi)核空間渣触,但是內(nèi)核空間可以訪問用戶空間。所以這里申請到內(nèi)核空間的地址壹若,然后拿 mmap 傳遞過來的用戶空間的地址去減得到偏移嗅钻。但是這里有個奇怪的地方我不太理解的,這里是拿低地址 - 高地址店展,得到的應(yīng)該是負(fù)數(shù)养篓,但是地址是無符號的,所以得到的是 0xffffffff 減去的數(shù)值赂蕴,然后后面拿這個值加上用戶空間地址還能正確的得到內(nèi)核空間地址柳弄,反正我是醉了。在 binder 里加了點打印概说,上個圖來點真相(是我寫的那個 k7service 的小程序):
1198 是 k7service 的 pid碧注,然后分配的用戶空間范圍是: 4020e000 - 4030c000(和前面 cat 看到的一樣),然后分配的內(nèi)核空間起始地址是: e2f00000糖赔,確實是從 3G(0xc0000000)開始的萍丐。
后面 proc->buffer_size
就是用 mmap 傳過來的地址一減就能得到映射內(nèi)存的大小。后面 binder_update_page_range
這個函數(shù)是同時映射用戶空間和內(nèi)核空間放典,調(diào)用這個才是真正的分配物理地址逝变,這個后面再說船万。
proc->pages 是一個指向指針的指針,struct page 是內(nèi)核的代表內(nèi)存頁的數(shù)據(jù)結(jié)構(gòu)骨田。前面也說了 linux 帶 MMU 的內(nèi)存管理都是分頁的(現(xiàn)在跑 android 的芯片基本都帶 MMU)耿导。這里是拿內(nèi)核一頁的大小(經(jīng)過打印我手上的板子上 PAGE_SIZE
是 4096态贤,也就是 4k舱呻,一般都是這大小吧),算出 mmap 映射的內(nèi)存一共可以分為幾頁悠汽,然后事先先把保存內(nèi)核頁的數(shù)據(jù)結(jié)構(gòu)的數(shù)組分配好箱吕。
然后后面那句: buffer = proc->buffer;
。proc->buffer
是這片內(nèi)存內(nèi)核空間的首地址柿冲,這個 buffer 是一個叫 binder_buffer
的結(jié)構(gòu)體茬高,用來表示 binder 分配內(nèi)存的塊的一個塊:
struct binder_buffer {
struct list_head entry; /* free and allocated entries by addesss */
struct rb_node rb_node; /* free entry by size or allocated entry */
/* by address */
unsigned free:1;
unsigned allow_user_free:1;
unsigned async_transaction:1;
unsigned debug_id:29;
struct binder_transaction *transaction;
struct binder_node *target_node;
size_t data_size;
size_t offsets_size;
uint8_t data[0];
};
前面的變量都先不說,注意看最后那個叫 data[0] 的變量假抄。這個 data 其實表示的是地址來的怎栽。這個是怎么回事咧,其實 binder mmap 這一片內(nèi)存中宿饱,是分成一塊塊的(binder_buffer
)熏瞄,內(nèi)存中的排列是這樣的:
每一塊數(shù)據(jù)前面跟著一個 binder_buffer
,然后緊著下一個谬以,一個跟一個保存在 proc->buffers
這個鏈表里强饮。然后再看看剛剛說的那句:buffer = proc->buffer;
。這句話相當(dāng)于是說:
分配內(nèi)核第一塊 binder_buffer
为黎。
同時邮丰, proc->buffer
這個首地址就是第一個塊 binder_buffer
,并且 binder_buffer
的 data 就是這塊 binder_buffer
指向的數(shù)據(jù)地址铭乾。所以這個 data 必須要放在這個結(jié)構(gòu)體的最后(kernel 的鏈表實現(xiàn)則是利用結(jié)構(gòu)體的首變量(地址)剪廉,在這種淫蕩的技能上,c 秒殺 java)片橡。
這種簡潔妈经、高效的辦法,kernel 的代碼中有不少捧书,可以好好運用一下吹泡。
然后后面是把剛剛分配好的這塊 binder_buffer
分別插入到 proc 的 buffers 鏈表和 free_buffers
紅黑樹中。這里稍微看下 binder_proc
中幾個相關(guān)的成員變量:
struct binder_proc {
... ...
struct list_head buffers;
struct rb_root free_buffers;
struct rb_root allocated_buffers;
size_t free_async_space;
... ...
};
這幾個變量分別代表:
buffers: 這個鏈表保存了所有已經(jīng)分配的 binder_buffer
內(nèi)存塊经瓷。
free_buffers
: 這個紅黑樹保持了還未使用的 binder_buffer
內(nèi)存塊爆哑,就是已經(jīng)分配了,但是沒還創(chuàng)建物理內(nèi)存映射的舆吮,說是分配倒不如說創(chuàng)建的好理解點揭朝,就是說 binder_buffer
這個結(jié)構(gòu)體已經(jīng)創(chuàng)建队贱,然后在那片內(nèi)存中占了個空位,要申請內(nèi)存潭袱,可以不用重新創(chuàng)建對象柱嫌,不用重新再那片內(nèi)存重新分配。使用紅黑樹提高查找速度屯换,按 binder_buffer
的 size 排列(后面的查找算法编丘,會發(fā)現(xiàn)使用紅黑樹的好處)。
allocated_buffers
:這里保存的是已經(jīng)建立好物理內(nèi)存映射的 binder_buffer
內(nèi)存塊彤悔,也是說正在使用中的 binder_buffer
嘉抓。也是一顆紅黑樹。
所以理論上晕窑,proc->buffers
里有所有的 binder_buffer
抑片,然后 proc->buffers
的里的是 proc->free_buffers
和 allocated_buffers
之和。
一般用法是剛開始新分配一個 binder_buffer
插入到 proc->buffers
里杨赤,同時也插入 proc->free_buffers
里敞斋。binder_mmap
就是一開是分配了一個 binder_buffer
。然后后面數(shù)據(jù)來了望拖,要申請使用 binder_buffer
會先在 proc->free_buffers
里查找大小最接近要求的 binder_buffer
塊渺尘,然后調(diào)用 binder_update_page_range
將這塊 binder_buffer
的用戶空間地址和內(nèi)核地址映射到物理地址(真正的分配內(nèi)存)权她,然后把這塊 binder_buffer
從 proc->free_buffers
中刪掉昌抠,再插入到 allocated_buffers
中诗赌。然后重復(fù)。當(dāng)然里面還有不少細(xì)節(jié)盔沫,還有內(nèi)存管理相關(guān)的,這些后面再說枫匾。
這里 binder_mmap
差不多看完了架诞,那回去看看那個 binder_update_page_range
,這個是將用戶地址和內(nèi)核地址映射到同一物理地址上干茉,來看看是怎么做到的:
static int binder_update_page_range(struct binder_proc *proc, int allocate,
void *start, void *end,
struct vm_area_struct *vma)
{
void *page_addr;
unsigned long user_page_addr;
struct vm_struct tmp_area;
struct page **page;
struct mm_struct *mm;
if (end <= start)
return 0;
if (vma)
mm = NULL;
else
mm = get_task_mm(proc->tsk);
if (mm) {
down_write(&mm->mmap_sem);
vma = proc->vma;
}
// 第二個參數(shù)為 0 則是解除內(nèi)存映射(釋放內(nèi)存)
if (allocate == 0)
goto free_range;
if (vma == NULL) {
printk(KERN_ERR "binder: %d: binder_alloc_buf failed to "
"map pages in userspace, no vma\n", proc->pid);
goto err_no_vma;
}
// 循環(huán)分配內(nèi)存頁面
for (page_addr = start; page_addr < end; page_addr += PAGE_SIZE) {
int ret;
struct page **page_array_ptr;
// 根據(jù)地址計算出所處的內(nèi)存頁面數(shù)組的索引
page = &proc->pages[(page_addr - proc->buffer) / PAGE_SIZE];
BUG_ON(*page);
// 向內(nèi)核申請內(nèi)存頁面
*page = alloc_page(GFP_KERNEL | __GFP_ZERO);
if (*page == NULL) {
printk(KERN_ERR "binder: %d: binder_alloc_buf failed "
"for page at %p\n", proc->pid, page_addr);
goto err_alloc_page_failed;
}
// 設(shè)置要映射的內(nèi)核地址
tmp_area.addr = page_addr;
tmp_area.size = PAGE_SIZE + PAGE_SIZE /* guard page? */;
page_array_ptr = page;
// 將設(shè)置好的內(nèi)存頁做物理內(nèi)存映射(這里是內(nèi)核的)
ret = map_vm_area(&tmp_area, PAGE_KERNEL, &page_array_ptr);
if (ret) {
printk(KERN_ERR "binder: %d: binder_alloc_buf failed "
"to map page at %p in kernel\n",
proc->pid, page_addr);
goto err_map_kernel_failed;
}
// 根據(jù)前面保存的地址偏移谴忧,計算出內(nèi)核地址對應(yīng)的用戶地址
user_page_addr =
(uintptr_t)page_addr + proc->user_buffer_offset;
// 這里是做用戶地址到物理內(nèi)存的映射
ret = vm_insert_page(vma, user_page_addr, page[0]);
if (ret) {
printk(KERN_ERR "binder: %d: binder_alloc_buf failed "
"to map page at %lx in userspace\n",
proc->pid, user_page_addr);
goto err_vm_insert_page_failed;
}
/* vm_insert_page does not seem to increment the refcount */
}
if (mm) {
up_write(&mm->mmap_sem);
mmput(mm);
}
return 0;
free_range:
// 這里是釋放映射的
for (page_addr = end - PAGE_SIZE; page_addr >= start;
page_addr -= PAGE_SIZE) {
page = &proc->pages[(page_addr - proc->buffer) / PAGE_SIZE];
if (vma)
zap_page_range(vma, (uintptr_t)page_addr +
proc->user_buffer_offset, PAGE_SIZE, NULL);
err_vm_insert_page_failed:
unmap_kernel_range((unsigned long)page_addr, PAGE_SIZE);
err_map_kernel_failed:
__free_page(*page);
*page = NULL;
err_alloc_page_failed:
;
}
err_no_vma:
if (mm) {
up_write(&mm->mmap_sem);
mmput(mm);
}
return -ENOMEM;
}
一開始有個 end <= start 的判斷,后面會發(fā)現(xiàn)角虫,當(dāng) end == start 的時候是表示這段內(nèi)存已經(jīng)映射過了沾谓。然后是那第二參數(shù),為 0 的時候表示釋放映射戳鹅,我們先看映射的情況均驶。下面是一個循環(huán),前面說了 linux 內(nèi)存是分頁的枫虏,所以就要一頁一頁的映射妇穴,這里就是從映射起始地址到結(jié)束看需要映射幾頁爬虱。然后循環(huán)開始通過地址算出當(dāng)前地址所在的頁在之前創(chuàng)建的頁數(shù)組中的位置,proc->pages 前面 mmap 那里提前創(chuàng)建好了腾它。然后是 alloc_page
申請內(nèi)核頁面跑筝,以后有時間補補相關(guān)知識,現(xiàn)在暫時理解為 malloc 差不多就行了瞒滴。后面設(shè)置這個頁面的地址继蜡,然后大小的時候不知道為什么多加了 PAGE_SIZE
大小,看注釋是說防止頁面越界逛腿?稀并? map_vm_area
把剛剛設(shè)置的內(nèi)核頁面做物理內(nèi)存映射,到這里才算真正分配內(nèi)存单默。
這里 proc->user_buffer_offset
這個前面保存的內(nèi)核到用戶地址的偏移終于派上用場了碘举。通過這個偏移可以算得出內(nèi)核空間地址對應(yīng)的用戶空間地址。然后 vm_insert_page
把這段用戶空間地址也做一次物理內(nèi)存映射搁廓。這樣內(nèi)核空間地址和用戶空間的地址就映射到同一塊物理內(nèi)存上了引颈。這里由于缺少相關(guān)的知識我還是不怎么理解,這幾個內(nèi)核的 api 調(diào)用是咋回事境蜕,但是簡單來說:如果你在 binder 驅(qū)動對內(nèi)核這段地址的內(nèi)存寫入數(shù)據(jù)蝙场,對應(yīng)用戶空間的那段內(nèi)存也會有同樣的數(shù)據(jù)。這樣就省去了一次 copy_to_user
的從內(nèi)核空間到用戶空間的數(shù)據(jù) copy粱年。
最后來張圖吧售滤,這樣比較簡單明了:
實際運用
前面說了那么多,來點例子看看是怎么運用只 copy 一次的台诗。假設(shè)有個 proc A 發(fā)起了一次 IPC 調(diào)用完箩,那么根據(jù)前面的講解會通過 IPCThreadState.transact 發(fā)送到 binder 的 binder_thread_write
寫請求,然后是跑到了 binder_thread_transaction
中拉队。我們這里來看看前面 binder_transaction
中忽略的一些細(xì)節(jié)弊知。在 binder_transaction
有這么一句:
t->buffer = binder_alloc_buf(target_proc, tr->data_size, tr->offsets_size, !reply && (t->flags & TF_ONE_WAY));
這里跑到了 binder_alloc_buf
里面,這個函數(shù)后面在內(nèi)存管理那里再分析粱快,這里向跳過秩彤,反正記住調(diào)用這個會給你返回一塊符合你指定大小的 buffer_size
(當(dāng)然得有足夠的內(nèi)存空間)。然后這里注意一點事哭,傳遞的一個參數(shù)是 **target_proc
**漫雷,這個是從目標(biāo)進(jìn)程分配的 buffer,也就是 proc B慷蠕。這個很關(guān)鍵珊拼,后面就能知道數(shù)據(jù)是怎么傳遞的了。然后接著看后面的:
offp = (size_t *)(t->buffer->data + ALIGN(tr->data_size, sizeof(void *)));
if (copy_from_user(t->buffer->data, tr->data.ptr.buffer, tr->data_size)) {
binder_user_error("binder: %d:%d got transaction with invalid "
"data ptr\n", proc->pid, thread->pid);
return_error = BR_FAILED_REPLY;
goto err_copy_data_failed;
}
if (copy_from_user(offp, tr->data.ptr.offsets, tr->offsets_size)) {
binder_user_error("binder: %d:%d got transaction with invalid "
"offsets ptr\n", proc->pid, thread->pid);
return_error = BR_FAILED_REPLY;
goto err_copy_data_failed;
}
這個 buffer->data
的淫蕩前面分析過了流炕,這個就是這塊 buffer 的存放數(shù)據(jù)的首地址澎现。唉仅胞,這里其實調(diào)用了2次 copy_from_user
一次 copy parcel 的 data 數(shù)據(jù),一次 copy parcel 里 flat_binder_object
的偏移地址的數(shù)據(jù)(-_-||)剑辫。其實我們就將就認(rèn)為只有一次 copy 吧干旧。這里就把 proc A 從用戶空間傳遞過來的數(shù)據(jù)(parcel 打包)copy 到內(nèi)核空間了。而且這個內(nèi)核空間的內(nèi)存是 proc B 提供的妹蔽,而且這個塊內(nèi)核空間還和 proc B 的用戶空間共同映射到了同一塊物理內(nèi)存上椎眯。
但是別激動先,我們把整個流程看完胳岂。根據(jù)前面的分析 binder_transaction
后面把從 proc B 獲取了 binder_buffer
的 binder_transaction
這個數(shù)據(jù)結(jié)構(gòu)插入到 proc B 的 work 隊列中并且喚醒阻塞等待數(shù)據(jù)的 proc B 的 binder_thread_read
编整。我們來看看 binder_thread_read
中前面忽略的一些比較重要的地方:
tr.data_size = t->buffer->data_size;
tr.offsets_size = t->buffer->offsets_size;
tr.data.ptr.buffer = (void *)t->buffer->data +
proc->user_buffer_offset;
tr.data.ptr.offsets = tr.data.ptr.buffer +
ALIGN(t->buffer->data_size,
sizeof(void *));
if (put_user(cmd, (uint32_t __user *)ptr))
return -EFAULT;
ptr += sizeof(uint32_t);
if (copy_to_user(ptr, &tr, sizeof(tr)))
return -EFAULT;
ptr += sizeof(tr);
t 就是從前面 proc A binder_transaction
插到 proc B 的 work 隊列里的,然后從它這里獲取 data_size
, offsets_size
之類乳丰,這些都是前面設(shè)置好的掌测。然后觀點的地方來了:
tr.data.ptr.buffer = (void *)t->buffer->data + proc->user_buffer_offset;
tr 是 binder_transaction_data
這個數(shù)據(jù)結(jié)構(gòu),是在用戶層和 binder 驅(qū)動傳遞數(shù)據(jù)的數(shù)據(jù)結(jié)構(gòu)产园。直接取 buffer->data + user_buffer_offset
這個地址汞斧。根據(jù)前面的分析 buffer->data
是前面 proc A 塞數(shù)據(jù)的內(nèi)核地址,user_buffer_offset
是用內(nèi)核地址到用戶空間地址的偏移什燕,一加就得到了同一物理地址的用戶空間地址粘勒。這里其實就差不多相當(dāng)于數(shù)據(jù)從 proc A 傳遞到 proc B 了。這里就相當(dāng)于傳遞 IPC 內(nèi)核到用戶空間的那一次 copy屎即,但是這里只是計算了一個地址偏移而已庙睡。
然后看看后面, put_user
是把 binder 命令(cmd)返回給用戶空間剑勾。還有后面有一個 copy_to_user
但是這個不是 copy 數(shù)據(jù)的埃撵,而是 copy binder_transaction_data
這個數(shù)據(jù)結(jié)構(gòu),只不過這個數(shù)據(jù)結(jié)構(gòu)里有傳遞數(shù)據(jù)的地址虽另,所以這個不算在 binder 數(shù)據(jù)傳遞的復(fù)制次數(shù)中。也就是說就算傳遞比較大的數(shù)據(jù)饺谬,這次復(fù)制只是復(fù)制一個數(shù)據(jù)結(jié)構(gòu)的大小捂刺。根據(jù)前面的分析,binder_thread_read
返回募寨,binder_ioctl
就返回了族展,然后就到用戶空間的 IPCThreadState 的 talkWithDriver,然后 proc B(從假設(shè)的例子看是 Bn 端)就該執(zhí)行 executeCommand 的 BR_TRANSACTION
命令:
binder_transaction_data tr;
result = mIn.read(&tr, sizeof(tr));
ALOG_ASSERT(result == NO_ERROR,
"Not enough command data for brTRANSACTION");
if (result != NO_ERROR) break;
Parcel buffer;
buffer.ipcSetDataReference(
reinterpret_cast<const uint8_t*>(tr.data.ptr.buffer),
tr.data_size,
reinterpret_cast<const size_t*>(tr.data.ptr.offsets),
tr.offsets_size/sizeof(size_t), freeBuffer, this);
看 binder_transaction_data
從 mIn 中讀出來了吧拔鹰,這個是前面 binder_thread_read copy_to_user
傳遞到用戶空間的仪缸。然后這個里面的 tr.data.ptr.buffer 就是 proc A 傳遞的 IPC 函數(shù)調(diào)用參數(shù)數(shù)據(jù)啦。
內(nèi)存管理
前面把數(shù)據(jù)傳遞的基本流程走完了列肢,最后看看內(nèi)存管理恰画。每一個 binder 通信的進(jìn)程都 mmap 了一片內(nèi)存(目前來看是 1M)宾茂,然后在這片內(nèi)存上按照請求分塊(binder_buffer
)。那一般就涉及到拴还,如何分塊跨晴,如果查找合適大小的 binder_buffer
塊,以及使用完成后片林,碎片合并的問題《伺瑁現(xiàn)在就來看看。
前面說了费封,一開始 binder 會映射一頁的內(nèi)存(一般是 4k)焕妙,然后插入到 proc->free_buffers
中去。然后要需要使用的時候先從 free_buffers
里找大小最接近的弓摘,我們看看是怎么查找的焚鹊,就是前面說的那個 binder_alloc_buf
函數(shù)啦:
static struct binder_buffer *binder_alloc_buf(struct binder_proc *proc,
size_t data_size,
size_t offsets_size, int is_async)
{
struct rb_node *n = proc->free_buffers.rb_node;
struct binder_buffer *buffer;
size_t buffer_size;
struct rb_node *best_fit = NULL;
void *has_page_addr;
void *end_page_addr;
size_t size;
if (proc->vma == NULL) {
printk(KERN_ERR "binder: %d: binder_alloc_buf, no vma\n",
proc->pid);
return NULL;
}
// 計算總共需要 buffer 的大小,字節(jié)對齊
size = ALIGN(data_size, sizeof(void *)) +
ALIGN(offsets_size, sizeof(void *));
if (size < data_size || size < offsets_size) {
binder_user_error("binder: %d: got transaction with invalid "
"size %zd-%zd\n", proc->pid, data_size, offsets_size);
return NULL;
}
if (is_async &&
proc->free_async_space < size + sizeof(struct binder_buffer)) {
binder_debug(BINDER_DEBUG_BUFFER_ALLOC,
"binder: %d: binder_alloc_buf size %zd"
"failed, no async space left\n", proc->pid, size);
return NULL;
}
// 在 free_buffers 紅黑樹查找大小合并的 buffer 塊
while (n) {
buffer = rb_entry(n, struct binder_buffer, rb_node);
BUG_ON(!buffer->free);
buffer_size = binder_buffer_size(proc, buffer);
if (size < buffer_size) {
best_fit = n;
n = n->rb_left;
} else if (size > buffer_size)
n = n->rb_right;
else {
best_fit = n;
break;
}
}
if (best_fit == NULL) {
printk(KERN_ERR "binder: %d: binder_alloc_buf size %zd failed, "
"no address space\n", proc->pid, size);
return NULL;
}
if (n == NULL) {
buffer = rb_entry(best_fit, struct binder_buffer, rb_node);
buffer_size = binder_buffer_size(proc, buffer);
}
binder_debug(BINDER_DEBUG_BUFFER_ALLOC,
"binder: %d: binder_alloc_buf size %zd got buff"
"er %p size %zd\n", proc->pid, size, buffer, buffer_size);
// 這個好像是用來地址是不是越界的吧
has_page_addr =
(void *)(((uintptr_t)buffer->data + buffer_size) & PAGE_MASK);
// n == NULL 表示沒有大小精確的塊衣盾,需要拆分
if (n == NULL) {
if (size + sizeof(struct binder_buffer) + 4 >= buffer_size)
buffer_size = size; /* no room for other buffers */
else
buffer_size = size + sizeof(struct binder_buffer);
}
// 計算所需要映射的內(nèi)存的結(jié)束地址寺旺,注意頁對齊
end_page_addr =
(void *)PAGE_ALIGN((uintptr_t)buffer->data + buffer_size);
if (end_page_addr > has_page_addr)
end_page_addr = has_page_addr;
// 做物理內(nèi)存映射
if (binder_update_page_range(proc, 1,
(void *)PAGE_ALIGN((uintptr_t)buffer->data), end_page_addr, NULL))
return NULL;
// 從 free_buffers 中把剛剛分配的 buffer 塊刪掉
rb_erase(best_fit, &proc->free_buffers);
// 標(biāo)志這塊 buffer 正在使用
buffer->free = 0;
// 把這塊 buffer 插入到 allocated_buffers 中
binder_insert_allocated_buffer(proc, buffer);
// 如果要分配的 buffer_size 和原來 buffer 塊不一樣就要拆分
if (buffer_size != size) {
struct binder_buffer *new_buffer = (void *)buffer->data + size;
// 新塊插入到 proc->buffers 中
list_add(&new_buffer->entry, &buffer->entry);
new_buffer->free = 1;
// 新塊插入到 free_buffers
binder_insert_free_buffer(proc, new_buffer);
}
binder_debug(BINDER_DEBUG_BUFFER_ALLOC,
"binder: %d: binder_alloc_buf size %zd got "
"%p\n", proc->pid, size, buffer);
buffer->data_size = data_size;
buffer->offsets_size = offsets_size;
buffer->async_transaction = is_async;
if (is_async) {
proc->free_async_space -= size + sizeof(struct binder_buffer);
binder_debug(BINDER_DEBUG_BUFFER_ALLOC_ASYNC,
"binder: %d: binder_alloc_buf size %zd "
"async free %zd\n", proc->pid, size,
proc->free_async_space);
}
return buffer;
}
先說說這個函數(shù)的參數(shù),第一個前面說了是目標(biāo)進(jìn)程的势决。后面2個 size 分別是 proc A 使用 parcel 打包的 data size 和 flat_binder_object
偏移數(shù)據(jù)的 size(見前一篇分析)(最后那個異步釋放先不管)阻塑。這2個 size 加起來就是總共需要空間。然后后面那個 while 循環(huán)果复,是在從 free_buffers 的根開始查找大小合適的 buffer陈莽。free_buffers
紅黑樹按照大小排列,左子樹一定比當(dāng)前節(jié)點小虽抄,右子樹一定比當(dāng)前節(jié)點大走搁。然后我們看下 binder_buffer_size
這個函數(shù):
static size_t binder_buffer_size(struct binder_proc *proc,
struct binder_buffer *buffer)
{
if (list_is_last(&buffer->entry, &proc->buffers))
return proc->buffer + proc->buffer_size - (void *)buffer->data;
else
return (size_t)list_entry(buffer->entry.next,
struct binder_buffer, entry) - (size_t)buffer->data;
}
這個函數(shù)是獲取指定 binder_buffer
的大小,從實現(xiàn)可以看得出:
如果這塊 buffer 是最后那塊迈窟,那么返回的后面剩下整塊內(nèi)存空間的大小私植。
如果這塊 buffer 在中間,那么大小就是后面那塊地址 - 當(dāng)前這塊地址车酣。
結(jié)合這2點可以看出這些 binder_buffer
在那 1M 的內(nèi)存塊中是連續(xù)排列的(這也為后面合并碎片塊提供了便利性)曲稼。
從上面就能知道,如果這 1M 當(dāng)中還有大于要求 size 的大小湖员,就一定能找得到贫悄,就算分配的 buffer 沒有這么大,如果找到最后那塊娘摔,就是整個剩余空間的大小了窄坦。當(dāng)然如果整個剩余空間都不夠那就沒辦法了。所以上層應(yīng)用寫一些跨進(jìn)程的功能的時候不要直接使用 binder 傳遞大于 1M 的數(shù)據(jù)(上面是 Bundle、Parcel 之類的)鸭津,應(yīng)該使用共享內(nèi)存來傳遞(見前面一篇的分析)彤侍。
繼續(xù)往下走,best_fit == NULL
表示沒有足夠的空間了(找不到比要求 size 大或者等于的 buffer 塊)曙博。然后能往后走說明 best_fit
這塊至少是不小于要求 size 大小的拥刻。 n == NULL 這個判斷就是說沒找一塊 buffer 大小正好是要求的 size 大小(實際上大小正好相等的情況是很少的)父泳,那么就意味要從這一塊中分出 size 大小的出去另外做一塊 buffer般哼。所以后面重新計算了下 buffer_size
的大小,要加上 binder_buffer
結(jié)構(gòu)體的大小惠窄,前面分析了蒸眠,一塊 buffer 前面是 binder_buffer
信息。
然后后面調(diào)用 binder_update_page_range
去映射物理內(nèi)存杆融。注意下楞卡,前面分析的,因為 linux 的內(nèi)存是按頁分的脾歇,所以映射的時候也要按頁去映射蒋腮,那就要按頁對齊,一般一頁是 4k藕各,但是很多參數(shù)其實就幾個池摧、十幾個字節(jié)。所以這里起始地址和結(jié)束地址一對齊很多情況都是相等的激况。所以前面那個 binder_update_page_range
有個判斷是 end >= start 就返回作彤,這里如果 end == start 就表示這段內(nèi)存已經(jīng)映射過了(在同一頁中)。
再后面就是把要用的那快 buffer 從 free_buffers
中刪掉乌逐,然后把 free 標(biāo)志改成正在使用的竭讳,插入到 allocated_buffers
中:
static void binder_insert_allocated_buffer(struct binder_proc *proc,
struct binder_buffer *new_buffer)
{
struct rb_node **p = &proc->allocated_buffers.rb_node;
struct rb_node *parent = NULL;
struct binder_buffer *buffer;
BUG_ON(new_buffer->free);
while (*p) {
parent = *p;
buffer = rb_entry(parent, struct binder_buffer, rb_node);
BUG_ON(buffer->free);
// 按地址排列
if (new_buffer < buffer)
p = &parent->rb_left;
else if (new_buffer > buffer)
p = &parent->rb_right;
else
BUG();
}
rb_link_node(&new_buffer->rb_node, parent, p);
rb_insert_color(&new_buffer->rb_node, &proc->allocated_buffers);
}
可以看到 allocated_buffers
是按地址排列的。然后后面那個判斷 buffer_size != size
浙踢,如果前面找不到大小一樣的 buffer 塊绢慢,然后重新計算了 buffer_size
, 那么如果這塊 buffer 的大小比原來 size 大(這里肯定是大的洛波,如果小的話就表示內(nèi)存不夠了)就要原來那塊一分為二呐芥,前面拿 size 大小去用,后面剩下的作為空閑塊奋岁。所以新弄了一個 binder_buffer
出來,然后把插入到 free_buffers
和 proc->buffers
里去了荸百。那個 proc->buffers
是個鏈表闻伶,這里 list_add(&new_buffer->entry, &buffer->entry);
這種寫法就是插入到這個鏈表的最后。然后來看插入到 free_buffers
static void binder_insert_free_buffer(struct binder_proc *proc,
struct binder_buffer *new_buffer)
{
struct rb_node **p = &proc->free_buffers.rb_node;
struct rb_node *parent = NULL;
struct binder_buffer *buffer;
size_t buffer_size;
size_t new_buffer_size;
BUG_ON(!new_buffer->free);
// 計算新 buffer 的大小
new_buffer_size = binder_buffer_size(proc, new_buffer);
binder_debug(BINDER_DEBUG_BUFFER_ALLOC,
"binder: %d: add free buffer, size %zd, "
"at %p\n", proc->pid, new_buffer_size, new_buffer);
while (*p) {
parent = *p;
buffer = rb_entry(parent, struct binder_buffer, rb_node);
BUG_ON(!buffer->free);
buffer_size = binder_buffer_size(proc, buffer);
if (new_buffer_size < buffer_size)
p = &parent->rb_left;
else
p = &parent->rb_right;
}
rb_link_node(&new_buffer->rb_node, parent, p);
rb_insert_color(&new_buffer->rb_node, &proc->free_buffers);
}
這個函數(shù)主要注意 new_buffer_size = binder_buffer_size(proc, new_buffer);
够话。從這里可以看得出蓝翰,這些空閑的 buffer 是不保存本塊的大小的光绕,都是要用的時候現(xiàn)場計算的。然后這里證實了 free_buffers
是按大小排列的了畜份。
然后我們來看看使用完之后釋放這些 buffer 塊的情況诞帐。前一篇 parcel 那里說到 Parcel 里有一個叫 mOwner 的函數(shù)指針,如果設(shè)置了的話爆雹,會在 parcel 的析夠函數(shù)里調(diào)用:
Parcel::~Parcel()
{
freeDataNoInit();
}
void Parcel::freeDataNoInit()
{
if (mOwner) {
//ALOGI("Freeing data ref of %p (pid=%d)\n", this, getpid());
mOwner(this, mData, mDataSize, mObjects, mObjectsSize, mOwnerCookie);
} else {
releaseObjects();
if (mData) free(mData);
if (mObjects) free(mObjects);
}
}
在 parcel 中設(shè)置 mOwner 的地方是 ipcSetDataReference 這個方法停蕉。然后在 IPCThreadState 有2個地方會調(diào)用 ipcSetDataReference 設(shè)置一個叫 freeBuffer 的函數(shù):
一個是在 waitForResponse 中的 BR_REPLY 的命令那里:
case BR_REPLY:
{
binder_transaction_data tr;
err = mIn.read(&tr, sizeof(tr));
ALOG_ASSERT(err == NO_ERROR, "Not enough command data for brREPLY");
if (err != NO_ERROR) goto finish;
if (reply) {
if ((tr.flags & TF_STATUS_CODE) == 0) {
reply->ipcSetDataReference(
reinterpret_cast<const uint8_t*>(tr.data.ptr.buffer),
tr.data_size,
reinterpret_cast<const size_t*>(tr.data.ptr.offsets),
tr.offsets_size/sizeof(size_t),
freeBuffer, this);
} else {
err = *static_cast<const status_t*>(tr.data.ptr.buffer);
freeBuffer(NULL,
reinterpret_cast<const uint8_t*>(tr.data.ptr.buffer),
tr.data_size,
reinterpret_cast<const size_t*>(tr.data.ptr.offsets),
tr.offsets_size/sizeof(size_t), this);
}
} else {
freeBuffer(NULL,
reinterpret_cast<const uint8_t*>(tr.data.ptr.buffer),
tr.data_size,
reinterpret_cast<const size_t*>(tr.data.ptr.offsets),
tr.offsets_size/sizeof(size_t), this);
continue;
}
}
goto finish;
一個是在 executeCommand 的 BR_TRANSACTION 的命令那:
case BR_TRANSACTION:
{
binder_transaction_data tr;
result = mIn.read(&tr, sizeof(tr));
ALOG_ASSERT(result == NO_ERROR,
"Not enough command data for brTRANSACTION");
if (result != NO_ERROR) break;
Parcel buffer;
buffer.ipcSetDataReference(
reinterpret_cast<const uint8_t*>(tr.data.ptr.buffer),
tr.data_size,
reinterpret_cast<const size_t*>(tr.data.ptr.offsets),
tr.offsets_size/sizeof(size_t), freeBuffer, this);
... ...
if (tr.target.ptr) {
sp<BBinder> b((BBinder*)tr.cookie);
const status_t error = b->transact(tr.code, buffer, &reply, tr.flags);
if (error < NO_ERROR) reply.setError(error);
} else {
const status_t error = the_context_object->transact(tr.code, buffer, &reply, tr.flags);
if (error < NO_ERROR) reply.setError(error);
}
... ...
}
break;
然后回到通信篇那里去看看我之前畫的那張圖,注意設(shè)置 freeBuffer 全都是 BR 命令钙态,一個是 executeCommand慧起, 這個是 Bn 端那里。在 BR_TRANSACTION
命令里定義了一個本地變量 Parcel buffer册倒,并且給這個 parcel 設(shè)置了 feeBuffer蚓挤。然后作為參數(shù)傳遞給后面執(zhí)行 Bn 端的 transaction 實現(xiàn)遠(yuǎn)程調(diào)用。然后這個 executeCommand 執(zhí)行完后驻子,就會執(zhí)行 Parcel 的析夠函數(shù)從而觸發(fā) freeBuffer 的調(diào)用灿意。freeBuffer 的實現(xiàn)我們后面再說,里面是讓 binder 去釋放之前申請的 buffer 塊崇呵。然后前面說了 proc A 發(fā)起 IPC缤剧,binder 里面是用 proc B(也就是 Bn 端)來的內(nèi)存來申請 buffer 的。所以這里在 proc B 設(shè)置釋放 buffer 的函數(shù)是合理的演熟。
然后第二個 waitForResponse 也是差不多的鞭执。前面那個是在 Bn 端釋放,是 Bp —> Bn 發(fā)送請求芒粹,target 是 Bn兄纺,這里呢是從 Bn 返回結(jié)果到 Bp,就是 Bn —> Bp化漆, target 就是 Bp 了(proc A)估脆。waitForResponse 的參數(shù) Parcel reply 是從 Bp 的 IPCThreadState 的 transact 傳遞過來的,這個就是上層發(fā)起 IPC 那個接口函數(shù)傳遞過來的座云,也是一個本地變量疙赠,transact 調(diào)用完成后就會調(diào)用 Parcel 的析夠函數(shù)觸發(fā)釋放函數(shù)。這里是從 Bn 返回到 Bp朦拖,申請的內(nèi)存就是 Bp 端的圃阳,所以在 Bp 釋放也是對的。
通過上面先得搞清楚璧帝,內(nèi)存從哪個進(jìn)程來的捍岳,在哪個進(jìn)程釋放。基本上在誰那拿的锣夹,就由誰來釋放页徐。搞清楚后,現(xiàn)在可以看看 freeBuffer 的實現(xiàn)了:
void IPCThreadState::freeBuffer(Parcel* parcel, const uint8_t* data, size_t dataSize,
const size_t* objects, size_t objectsSize,
void* cookie)
{
//ALOGI("Freeing parcel %p", &parcel);
IF_LOG_COMMANDS() {
alog << "Writing BC_FREE_BUFFER for " << data << endl;
}
ALOG_ASSERT(data != NULL, "Called with NULL data");
if (parcel != NULL) parcel->closeFileDescriptors();
IPCThreadState* state = self();
state->mOut.writeInt32(BC_FREE_BUFFER);
state->mOut.writeInt32((int32_t)data);
}
這個通過 mOut 對 binder 寫了一個 BC_FREE_BUFFER
的命令银萍,然后把保存的 buffer 塊的用戶空間的地址也寫了進(jìn)去变勇。這些內(nèi)存都是由 kernel 的 binder 驅(qū)動管理的,所以只能由 binder 驅(qū)動來釋放贴唇,用戶空間無法釋放的搀绣。這樣的話這個進(jìn)程下次和 binder 進(jìn)行通信的時候,就會由 BINDER_WRITE_READ
ioctl 把這條命令寫到 binder 驅(qū)動中去滤蝠。binder 驅(qū)動就會執(zhí)行 BC_FREE_BUFFER
釋放使用完的 binder_buffer
內(nèi)存塊豌熄。還記得前面說 parcel 打包 binder 命令(或是解析)可以打包多條命令的么,這里就體現(xiàn)出來了物咳。釋放命令是隨真正的業(yè)務(wù)命令一起打包發(fā)送過去的锣险。
然后我們可以回到 binder 驅(qū)動里,看看 BC_FREE_BUFFER
的處理览闰,這個在 binder_thread_write
里:
case BC_FREE_BUFFER: {
void __user *data_ptr;
struct binder_buffer *buffer;
// 獲取用戶傳遞過來 buffer 的地址
if (get_user(data_ptr, (void * __user *)ptr))
return -EFAULT;
ptr += sizeof(void *);
// 通過地址查找 buffer 塊
buffer = binder_buffer_lookup(proc, data_ptr);
if (buffer == NULL) {
binder_user_error("binder: %d:%d "
"BC_FREE_BUFFER u%p no match\n",
proc->pid, thread->pid, data_ptr);
break;
}
if (!buffer->allow_user_free) {
binder_user_error("binder: %d:%d "
"BC_FREE_BUFFER u%p matched "
"unreturned buffer\n",
proc->pid, thread->pid, data_ptr);
break;
}
binder_debug(BINDER_DEBUG_FREE_BUFFER,
"binder: %d:%d BC_FREE_BUFFER u%p found buffer %d for %s transaction\n",
proc->pid, thread->pid, data_ptr, buffer->debug_id,
buffer->transaction ? "active" : "finished");
if (buffer->transaction) {
buffer->transaction->buffer = NULL;
buffer->transaction = NULL;
}
if (buffer->async_transaction && buffer->target_node) {
BUG_ON(!buffer->target_node->has_async_transaction);
if (list_empty(&buffer->target_node->async_todo))
buffer->target_node->has_async_transaction = 0;
else
list_move_tail(buffer->target_node->async_todo.next, &thread->todo);
}
// 釋放資源
binder_transaction_buffer_release(proc, buffer, NULL);
binder_free_buf(proc, buffer);
break;
}
通過 get_user
得到從用戶空間傳遞過來要釋放的 buffer 的地址芯肤。然后調(diào)用 binder_buffer_lookup
查找 buffer 塊:
static struct binder_buffer *binder_buffer_lookup(struct binder_proc *proc,
void __user *user_ptr)
{
struct rb_node *n = proc->allocated_buffers.rb_node;
struct binder_buffer *buffer;
struct binder_buffer *kern_ptr;
// 通過偏移由用戶空間地址計算出內(nèi)核空間地址
kern_ptr = user_ptr - proc->user_buffer_offset
- offsetof(struct binder_buffer, data);
// 在已經(jīng)映射的 buffer 紅黑樹中查找
while (n) {
buffer = rb_entry(n, struct binder_buffer, rb_node);
BUG_ON(buffer->free);
if (kern_ptr < buffer)
n = n->rb_left;
else if (kern_ptr > buffer)
n = n->rb_right;
else
return buffer;
}
return NULL;
}
user_buffer_offset
的作用又來了,這里是通過用戶空間地址計算出對應(yīng)的內(nèi)核空間地址压鉴。前面說了 allocated_buffers
是按地址排列的崖咨,所以通過地址查找。
然后后面先看看 binder_transaction_buffer_release
的釋放:
static void binder_transaction_buffer_release(struct binder_proc *proc,
struct binder_buffer *buffer,
size_t *failed_at)
{
size_t *offp, *off_end;
int debug_id = buffer->debug_id;
binder_debug(BINDER_DEBUG_TRANSACTION,
"binder: %d buffer release %d, size %zd-%zd, failed at %p\n",
proc->pid, buffer->debug_id,
buffer->data_size, buffer->offsets_size, failed_at);
if (buffer->target_node)
binder_dec_node(buffer->target_node, 1, 0);
offp = (size_t *)(buffer->data + ALIGN(buffer->data_size, sizeof(void *)));
if (failed_at)
off_end = failed_at;
else
off_end = (void *)offp + buffer->offsets_size;
for (; offp < off_end; offp++) {
struct flat_binder_object *fp;
if (*offp > buffer->data_size - sizeof(*fp) ||
buffer->data_size < sizeof(*fp) ||
!IS_ALIGNED(*offp, sizeof(void *))) {
printk(KERN_ERR "binder: transaction release %d bad"
"offset %zd, size %zd\n", debug_id,
*offp, buffer->data_size);
continue;
}
fp = (struct flat_binder_object *)(buffer->data + *offp);
switch (fp->type) {
case BINDER_TYPE_BINDER:
case BINDER_TYPE_WEAK_BINDER: {
struct binder_node *node = binder_get_node(proc, fp->binder);
if (node == NULL) {
printk(KERN_ERR "binder: transaction release %d"
" bad node %p\n", debug_id, fp->binder);
break;
}
binder_debug(BINDER_DEBUG_TRANSACTION,
" node %d u%p\n",
node->debug_id, node->ptr);
binder_dec_node(node, fp->type == BINDER_TYPE_BINDER, 0);
} break;
case BINDER_TYPE_HANDLE:
case BINDER_TYPE_WEAK_HANDLE: {
struct binder_ref *ref = binder_get_ref(proc, fp->handle);
if (ref == NULL) {
printk(KERN_ERR "binder: transaction release %d"
" bad handle %ld\n", debug_id,
fp->handle);
break;
}
binder_debug(BINDER_DEBUG_TRANSACTION,
" ref %d desc %d (node %d)\n",
ref->debug_id, ref->desc, ref->node->debug_id);
binder_dec_ref(ref, fp->type == BINDER_TYPE_HANDLE);
} break;
case BINDER_TYPE_FD:
binder_debug(BINDER_DEBUG_TRANSACTION,
" fd %ld\n", fp->handle);
if (failed_at)
task_close_fd(proc, fp->handle);
break;
default:
printk(KERN_ERR "binder: transaction release %d bad "
"object type %lx\n", debug_id, fp->type);
break;
}
}
}
前面 buffer 后面保存了 parcel flat_binder_object
的偏移數(shù)據(jù)油吭,這里是去取這個偏移击蹲,然后通過這些偏移取到打包在 parcel 里面的 flat_binder_object
數(shù)據(jù),然后去根據(jù)不同的類型去減少引用之類的(前面使用的時候會增加相應(yīng)的引用)婉宰。我是比較討厭這些啥引用計算的歌豺,這里就隨便過過就行了。
然后看后面的 binder_free_buf
心包, 這個和 binder_alloc_buf
真對應(yīng)袄噙帧:
static void binder_free_buf(struct binder_proc *proc,
struct binder_buffer *buffer)
{
size_t size, buffer_size;
// 獲取這塊 buffer 的大小
buffer_size = binder_buffer_size(proc, buffer);
// 計算 buffer 的 size 大小,注意字節(jié)對齊
size = ALIGN(buffer->data_size, sizeof(void *)) +
ALIGN(buffer->offsets_size, sizeof(void *));
binder_debug(BINDER_DEBUG_BUFFER_ALLOC,
"binder: %d: binder_free_buf %p size %zd buffer"
"_size %zd\n", proc->pid, buffer, size, buffer_size);
BUG_ON(buffer->free);
BUG_ON(size > buffer_size);
BUG_ON(buffer->transaction != NULL);
BUG_ON((void *)buffer < proc->buffer);
BUG_ON((void *)buffer > proc->buffer + proc->buffer_size);
if (buffer->async_transaction) {
proc->free_async_space += size + sizeof(struct binder_buffer);
binder_debug(BINDER_DEBUG_BUFFER_ALLOC_ASYNC,
"binder: %d: binder_free_buf size %zd "
"async free %zd\n", proc->pid, size,
proc->free_async_space);
}
// 釋放物理映射
binder_update_page_range(proc, 0,
(void *)PAGE_ALIGN((uintptr_t)buffer->data),
(void *)(((uintptr_t)buffer->data + buffer_size) & PAGE_MASK),
NULL);
// 從 allocated_buffers 刪除這塊 buffer
rb_erase(&buffer->rb_node, &proc->allocated_buffers);
// 設(shè)置空閑標(biāo)志位
buffer->free = 1;
// 向后查看有沒有可以合并的空閑塊
if (!list_is_last(&buffer->entry, &proc->buffers)) {
struct binder_buffer *next = list_entry(buffer->entry.next,
struct binder_buffer, entry);
if (next->free) {
rb_erase(&next->rb_node, &proc->free_buffers);
binder_delete_free_buffer(proc, next);
}
}
// 向前查看下有沒有可以合并的空閑塊
if (proc->buffers.next != &buffer->entry) {
struct binder_buffer *prev = list_entry(buffer->entry.prev,
struct binder_buffer, entry);
if (prev->free) {
binder_delete_free_buffer(proc, buffer);
rb_erase(&prev->rb_node, &proc->free_buffers);
buffer = prev;
}
}
// 把合并好的塊插入到 free_buffers 中蟹腾,以供下次使用
binder_insert_free_buffer(proc, buffer);
}
這里調(diào)用 binder_update_page_range
是第二個參數(shù)是傳遞 0 了痕惋,就表示是要釋放物理內(nèi)存映射,這個前面已經(jīng)分析過了娃殖。然后從 allocated_buffers
中刪掉這塊 buffer值戳,把 free 標(biāo)志也設(shè)置一下。重點在后面:
判斷這塊是不是最后一塊 buffer炉爆,這里其實就是向后查看后面那塊 buffer 是不是也是 free 的(前面說 proc->buffers
是一個鏈表)述寡。如果是的話柿隙,表示可以把這2塊合并成一個更大的 buffer 塊。這里插一句內(nèi)存管理方面的常識鲫凶,空閑的 buffer 塊越大,下次申請成功概率就越大衩辟,所以要保證空閑 buffer 塊盡量的大螟炫。如果 buffer 都是零零星星很小、數(shù)量很多的小塊艺晴,那么下次申請很可能會失敗昼钻,但是總體空間的大小卻是夠的,也就是我們常說的內(nèi)存碎片封寞。要盡可能的避免內(nèi)存碎片然评,所以才需要有內(nèi)存合并的處理。
那看看怎么合并的狈究。要合并的話碗淌,先把后面那塊從 free_buffers
中刪掉,然后調(diào)用 binder_delete_buffer
去刪除:
static void binder_delete_free_buffer(struct binder_proc *proc,
struct binder_buffer *buffer)
{
struct binder_buffer *prev, *next = NULL;
// 這2個標(biāo)志抖锥,表示要釋放的 buffer 是否于其他的 buffer 共用一個內(nèi)存頁
int free_page_end = 1;
int free_page_start = 1;
BUG_ON(proc->buffers.next == &buffer->entry);
prev = list_entry(buffer->entry.prev, struct binder_buffer, entry);
// 要釋放這塊 buffer亿眠,前面那個塊必須是 free 的
BUG_ON(!prev->free);
// 查看這塊 buffer 是否與前面一塊 buffer 共用一個內(nèi)存頁
if (buffer_end_page(prev) == buffer_start_page(buffer)) {
free_page_start = 0;
if (buffer_end_page(prev) == buffer_end_page(buffer))
free_page_end = 0;
binder_debug(BINDER_DEBUG_BUFFER_ALLOC,
"binder: %d: merge free, buffer %p "
"share page with %p\n", proc->pid, buffer, prev);
}
// 查看這塊 buffer 是否于后面一塊 buffer 共用一個內(nèi)存頁
if (!list_is_last(&buffer->entry, &proc->buffers)) {
next = list_entry(buffer->entry.next,
struct binder_buffer, entry);
if (buffer_start_page(next) == buffer_end_page(buffer)) {
free_page_end = 0;
if (buffer_start_page(next) ==
buffer_start_page(buffer))
free_page_start = 0;
// PS 這個打印寫錯了吧,不是 prev 而是 next 吧 -_-||
binder_debug(BINDER_DEBUG_BUFFER_ALLOC,
"binder: %d: merge free, buffer"
" %p share page with %p\n", proc->pid,
buffer, prev);
}
}
// 從 proc->buffers 刪除這塊 buffer
list_del(&buffer->entry);
// 如果這塊 buffer 不與前面和后面的 buffer 共用一個內(nèi)存頁的話磅废,
// 那就釋放掉這塊 buffer 所在的內(nèi)存頁的映射
if (free_page_start || free_page_end) {
binder_debug(BINDER_DEBUG_BUFFER_ALLOC,
"binder: %d: merge free, buffer %p do "
"not share page%s%s with with %p or %p\n",
proc->pid, buffer, free_page_start ? "" : " end",
free_page_end ? "" : " start", prev, next);
binder_update_page_range(proc, 0, free_page_start ?
buffer_start_page(buffer) : buffer_end_page(buffer),
(free_page_end ? buffer_end_page(buffer) :
buffer_start_page(buffer)) + PAGE_SIZE, NULL);
}
}
看到那個 BUG_ON(!prev->free)
感覺這個函數(shù)就是內(nèi)存合并專用的纳像,要調(diào)用這個釋放 buffer 塊,還必須前面那個塊是 free 的拯勉,合并的時候就是這樣(仔細(xì)看下前面竟趾,向前合并的時候,刪除的是自己宫峦,所以前面那個也是 free 的)岔帽。然后后面的的判斷是檢測要釋放的這塊 buffer 所在的頁面是不是別的 buffer 也在用。因為前面說了好幾次了斗遏,一般一頁是 4k山卦, IPC 的參數(shù)經(jīng)常只有幾個、十幾個字節(jié)而已诵次,所以經(jīng)常會一個頁里面有好塊 buffer账蓉。所以就向前(前一塊)和向后(后一塊)檢測下自己的鄰居是否和自己共用一個內(nèi)存頁:
static void *buffer_start_page(struct binder_buffer *buffer)
{
return (void *)((uintptr_t)buffer & PAGE_MASK);
}
static void *buffer_end_page(struct binder_buffer *buffer)
{
return (void *)(((uintptr_t)(buffer + 1) - 1) & PAGE_MASK);
}
檢測方法和前面差不多就是拿地址進(jìn)行頁面內(nèi)存對齊∮庖唬看對齊后的地址是不是落在一起铸本。然后設(shè)置2個標(biāo)志。在最后判斷遵堵,只要前后有一塊 buffer 和自己共用一頁就不釋放這片地址的物理內(nèi)存映射箱玷。否則就調(diào)用 binder_update_page_range
去把自己映射的這片物理內(nèi)存釋放掉怨规。以前在大學(xué)里面聽老師說內(nèi)存對齊的問題是最麻煩的,后來敲了幾年代碼沒啥感覺锡足,現(xiàn)在深深感受到惡意啦波丰,頁面對齊、字節(jié)對齊 -_-||舶得。
回到 binder_free_buf
掰烟,向后合并是刪掉后面那塊(next),向前合并是刪掉自己(buffer)沐批,拿 prev 重新當(dāng)作自己纫骑。最后 binder_insert_free_buffer
把合并之后的 buffer 重新插入到 proc->free_buffers
中供下次申請的時候使用。
這個做法其實我和以前弄 MiniGUI 的一個 GAL 中的顯存管理很類似九孩,簡單但是有效先馆,可以來這里對比一下(那里還有圖說明): STi7167 GAL 顯存管理
總結(jié)
感覺 binder 下了不少功夫進(jìn)行效率的優(yōu)化:
通過將內(nèi)核地址和用戶地址映射到同一個物理地址以減少數(shù)據(jù)傳遞中 copy 的次數(shù)。
自己進(jìn)行內(nèi)存管理躺彬、快速查找(紅黑樹)煤墙、合并碎片。
線程池支持顾患,提供高服務(wù)端并發(fā)的響應(yīng)能力(后面再分析)番捂。
以及等等 … …
另外 binder 還提供調(diào)用者和目的地的 pid 驗證,對 IPC 的安全性也有提高江解。并且對上層提供友好的封裝接口以及偷懶的代碼自動生成工具(aidl)设预,易用上也比傳遞 IPC 好。現(xiàn)在稍微能理解點 android 為什么要自己搞一套 IPC 的機制了犁河。據(jù)說標(biāo)準(zhǔn)的 linux kernel 3.xx 好像加入 binder 了鳖枕。