理解Android Binder機(jī)制(1/3):驅(qū)動(dòng)篇

Binder 學(xué)習(xí) RF-link
Binder的實(shí)現(xiàn)是比較復(fù)雜的,想要完全弄明白是怎么一回事,并不是一件容易的事情。

這里面牽涉到好幾個(gè)層次,每一層都有一些模塊和機(jī)制需要理解。這部分內(nèi)容會(huì)分為三篇文章來講解。本文是第一篇,首先會(huì)對(duì)整個(gè)Binder機(jī)制做一個(gè)架構(gòu)性的講解,然后會(huì)將大部分精力用來講解Binder機(jī)制中最核心的部分:Binder驅(qū)動(dòng)的實(shí)現(xiàn)。

本系列的文章列表如下:

Binder機(jī)制簡介

Binder源自Be Inc公司開發(fā)的OpenBinder框架,后來該框架轉(zhuǎn)移的Palm Inc,由Dianne Hackborn主導(dǎo)開發(fā)。OpenBinder的內(nèi)核部分已經(jīng)合入Linux Kernel 3.19。

Android Binder是在OpneBinder上的定制實(shí)現(xiàn)。原先的OpenBinder框架現(xiàn)在已經(jīng)不再繼續(xù)開發(fā),可以說Android上的Binder讓原先的OpneBinder得到了重生。
Binder是Android系統(tǒng)中大量使用的IPC(Inter-process communication,進(jìn)程間通訊)機(jī)制。無論是應(yīng)用程序?qū)ο到y(tǒng)服務(wù)的請求,還是應(yīng)用程序自身提供對(duì)外服務(wù),都需要使用到Binder。
因此,Binder機(jī)制在Android系統(tǒng)中的地位非常重要,可以說,理解Binder是理解Android系統(tǒng)的絕對(duì)必要前提。
在Unix/Linux環(huán)境下,傳統(tǒng)的IPC機(jī)制包括:1) 管道. 2) 消息隊(duì)列 . 3) 共享內(nèi)存 . 4) 信號(hào) . 5) Socket.
由于篇幅所限,本文不會(huì)對(duì)這些IPC機(jī)制做講解,有興趣的讀者可以參閱《UNIX網(wǎng)絡(luò)編程 卷2:進(jìn)程間通信》。

Android系統(tǒng)中對(duì)于傳統(tǒng)的IPC使用較少(但也有使用,例如:在請求Zygote fork進(jìn)程的時(shí)候使用的是Socket IPC),大部分場景下使用的IPC都是Binder。
Binder相較于傳統(tǒng)IPC來說更適合于Android系統(tǒng),具體原因的包括如下三點(diǎn):

  • Binder本身是C/S架構(gòu)的,這一點(diǎn)更符合Android系統(tǒng)的架構(gòu)
  • 性能上更有優(yōu)勢:管道,消息隊(duì)列,Socket的通訊都需要兩次數(shù)據(jù)拷貝,而Binder只需要一次。要知道,對(duì)于系統(tǒng)底層的IPC形式,少一次數(shù)據(jù)拷貝,對(duì)整體性能的影響是非常之大的
  • 安全性更好:傳統(tǒng)IPC形式,無法得到對(duì)方的身份標(biāo)識(shí)(UID/GID),而在使用Binder IPC時(shí),這些身份標(biāo)示是跟隨調(diào)用過程而自動(dòng)傳遞的。Server端很容易就可以知道Client端的身份,非常便于做安全檢查

整體架構(gòu)

Binder整體架構(gòu)如下所示:


圖片.png

從圖中可以看出,Binder的實(shí)現(xiàn)分為這么幾層:

  • Framework層
    Java部分
    JNI部分
    C++部分
  • 驅(qū)動(dòng)層

驅(qū)動(dòng)層位于Linux內(nèi)核中,它提供了最底層的數(shù)據(jù)傳遞,對(duì)象標(biāo)識(shí),線程管理,調(diào)用過程控制等功能。驅(qū)動(dòng)層是整個(gè)Binder機(jī)制的核心。

Framework層以驅(qū)動(dòng)層為基礎(chǔ),提供了應(yīng)用開發(fā)的基礎(chǔ)設(shè)施。

Framework層既包含了C++部分的實(shí)現(xiàn),也包含了Java部分的實(shí)現(xiàn)。為了能將C++的實(shí)現(xiàn)復(fù)用到Java端,中間通過JNI進(jìn)行銜接。

開發(fā)者可以在Framework之上利用Binder提供的機(jī)制來進(jìn)行具體的業(yè)務(wù)邏輯開發(fā)。其實(shí)不僅僅是第三方開發(fā)者,Android系統(tǒng)中本身也包含了很多系統(tǒng)服務(wù)都是基于Binder框架開發(fā)的。

既然是“進(jìn)程間”通訊就至少牽涉到兩個(gè)進(jìn)程,Binder框架是典型的C/S架構(gòu)。在下文中,我們把服務(wù)的請求方稱之為Client,服務(wù)的實(shí)現(xiàn)方稱之為Server。

Client對(duì)于Server的請求會(huì)經(jīng)由Binder框架由上至下傳遞到內(nèi)核的Binder驅(qū)動(dòng)中,請求中包含了Client將要調(diào)用的命令和參數(shù)。請求到了Binder驅(qū)動(dòng)之后,在確定了服務(wù)的提供方之后,會(huì)再從下至上將請求傳遞給具體的服務(wù)。整個(gè)調(diào)用過程如下圖所示:


圖片.png

對(duì)網(wǎng)絡(luò)協(xié)議有所了解的讀者會(huì)發(fā)現(xiàn),這個(gè)數(shù)據(jù)的傳遞過程和網(wǎng)絡(luò)協(xié)議是如此的相似。

初識(shí)ServiceManager

前面已經(jīng)提到,使用Binder框架的既包括系統(tǒng)服務(wù),也包括第三方應(yīng)用。因此,在同一時(shí)刻,系統(tǒng)中會(huì)有大量的Server同時(shí)存在。那么,Client在請求Server的時(shí)候,是如果確定請求發(fā)送給哪一個(gè)Server的呢?
這個(gè)問題,就和我們現(xiàn)實(shí)生活中如何找到一個(gè)公司/商場,如何確定一個(gè)人/一輛車一樣,解決的方法就是:每個(gè)目標(biāo)對(duì)象都需要一個(gè)唯一的標(biāo)識(shí)。并且,需要有一個(gè)組織來管理這個(gè)唯一的標(biāo)識(shí)。
而Binder框架中負(fù)責(zé)管理這個(gè)標(biāo)識(shí)的就是ServiceManager。ServiceManager對(duì)于Binder Server的管理就好比車管所對(duì)于車牌號(hào)碼的的管理,派出所對(duì)于身份證號(hào)碼的管理:每個(gè)公開對(duì)外提供服務(wù)的Server都需要注冊到ServiceManager中(通過addService),注冊的時(shí)候需要指定一個(gè)唯一的id(這個(gè)id其實(shí)就是一個(gè)字符串)。

Client要對(duì)Server發(fā)出請求,就必須知道服務(wù)端的id。Client需要先根據(jù)Server的id通過ServerManager拿到Server的標(biāo)示(通過getService),然后通過這個(gè)標(biāo)示與Server進(jìn)行通信。

整個(gè)過程如下圖所示:


圖片.png

如果上面這些介紹已經(jīng)讓你一頭霧水,請不要過分擔(dān)心,下面會(huì)詳細(xì)講解這其中的細(xì)節(jié)。

下文會(huì)以自下而上的方式來講解Binder框架。自下而上未必是最好的方法,每個(gè)人的思考方式不一樣,如果你更喜歡自上而下的理解,你也按這樣的順序來閱讀。

對(duì)于大部分人來說,我們可能需要反復(fù)的查閱才能完全理解。

驅(qū)動(dòng)層

源碼路徑(這部分代碼不在AOSP中,而是位于Linux內(nèi)核代碼中):

/kernel/drivers/android/binder.c
/kernel/include/uapi/linux/android/binder.h

或者

/kernel/drivers/staging/android/binder.c
/kernel/drivers/staging/android/uapi/binder.h

Binder機(jī)制的實(shí)現(xiàn)中,最核心的就是Binder驅(qū)動(dòng)。 Binder是一個(gè)[miscellaneous類型的驅(qū)動(dòng)(https://blog.csdn.net/raidtest/article/details/40356757),本身不對(duì)應(yīng)任何硬件,所有的操作都在軟件層。 binder_init函數(shù)負(fù)責(zé)Binder驅(qū)動(dòng)的初始化工作,該函數(shù)中大部分代碼是在通過debugfs_create_dir和debugfs_create_file函數(shù)創(chuàng)建debugfs對(duì)應(yīng)的文件。 如果內(nèi)核在編譯時(shí)打開了debugfs,則通過adb shell連上設(shè)備之后,可以在設(shè)備的這個(gè)路徑找到debugfs對(duì)應(yīng)的文件:/sys/kernel/debug。Binder驅(qū)動(dòng)中創(chuàng)建的debug文件如下所示:

# ls -l /sys/kernel/debug/binder/
total 0
-r--r--r-- 1 root root 0 1970-01-01 00:00 failed_transaction_log
drwxr-xr-x 2 root root 0 1970-05-09 01:19 proc
-r--r--r-- 1 root root 0 1970-01-01 00:00 state
-r--r--r-- 1 root root 0 1970-01-01 00:00 stats
-r--r--r-- 1 root root 0 1970-01-01 00:00 transaction_log
-r--r--r-- 1 root root 0 1970-01-01 00:00 transactions

這些文件其實(shí)都在內(nèi)存中的,實(shí)時(shí)的反應(yīng)了當(dāng)前Binder的使用情況,在實(shí)際的開發(fā)過程中,這些信息可以幫忙分析問題。例如,可以通過查看/sys/kernel/debug/binder/proc目錄來確定哪些進(jìn)程正在使用Binder,通過查看transaction_log和transactions文件來確定Binder通信的數(shù)據(jù)。

binder_init函數(shù)中最主要的工作其實(shí)下面這行:

ret = misc_register(&binder_miscdev);

該行代碼真正向內(nèi)核中注冊了Binder設(shè)備。binder_miscdev的定義如下:

static struct miscdevice binder_miscdev = {
    .minor = MISC_DYNAMIC_MINOR,
    .name = "binder",
    .fops = &binder_fops
};

這里指定了Binder設(shè)備的名稱是“binder”。這樣,在用戶空間便可以通過對(duì)/dev/binder文件進(jìn)行操作來使用Binder。

binder_miscdev同時(shí)也指定了該設(shè)備的fops。fops是另外一個(gè)結(jié)構(gòu)體,這個(gè)結(jié)構(gòu)中包含了一系列的函數(shù)指針,其定義如下:

static const struct file_operations binder_fops = {
    .owner = THIS_MODULE,
    .poll = binder_poll,
    .unlocked_ioctl = binder_ioctl,
    .compat_ioctl = binder_ioctl,
    .mmap = binder_mmap,
    .open = binder_open,
    .flush = binder_flush,
    .release = binder_release,
};

這里除了owner之外,每一個(gè)字段都是一個(gè)函數(shù)指針,這些函數(shù)指針對(duì)應(yīng)了用戶空間在使用Binder設(shè)備時(shí)的操作。例如:binder_poll對(duì)應(yīng)了poll系統(tǒng)調(diào)用的處理,binder_mmap對(duì)應(yīng)了mmap系統(tǒng)調(diào)用的處理,其他類同。

這其中,有三個(gè)函數(shù)尤為重要,它們是:binder_open,binder_mmap和binder_ioctl。 這是因?yàn)椋枰褂肂inder的進(jìn)程,幾乎總是先通過binder_open打開Binder設(shè)備,然后通過binder_mmap進(jìn)行內(nèi)存映射。

在這之后,通過binder_ioctl來進(jìn)行實(shí)際的操作。Client對(duì)于Server端的請求,以及Server對(duì)于Client請求結(jié)果的返回,都是通過ioctl完成的。

這里提到的流程如下圖所示:


圖片.png

主要結(jié)構(gòu)

Binder驅(qū)動(dòng)中包含了很多的結(jié)構(gòu)體。為了便于下文講解,這里我們先對(duì)這些結(jié)構(gòu)體做一些介紹。
驅(qū)動(dòng)中的結(jié)構(gòu)體可以分為兩類:
一類是與用戶空間共用的,這些結(jié)構(gòu)體在Binder通信協(xié)議過程中會(huì)用到。因此,這些結(jié)構(gòu)體定義在binder.h中,包括:

結(jié)構(gòu)體名稱 說明
flat_binder_object 描述在Binder IPC中傳遞的對(duì)象,見下文
binder_write_read 存儲(chǔ)一次讀寫操作的數(shù)據(jù)
binder_version 存儲(chǔ)Binder的版本號(hào)
transaction_flags 描述事務(wù)的flag,例如是否是異步請求,是否支持fd
binder_transaction_data 存儲(chǔ)一次事務(wù)的數(shù)據(jù)
binder_ptr_cookie 包含了一個(gè)指針和一個(gè)cookie
binder_handle_cookie 包含了一個(gè)句柄和一個(gè)cookie
binder_pri_desc 暫未用到
binder_pri_ptr_cookie 暫未用到

這其中,binder_write_read和binder_transaction_data這兩個(gè)結(jié)構(gòu)體最為重要,它們存儲(chǔ)了IPC調(diào)用過程中的數(shù)據(jù)。關(guān)于這一點(diǎn),我們在下文中會(huì)講解。
Binder驅(qū)動(dòng)中,還有一類結(jié)構(gòu)體是僅僅Binder驅(qū)動(dòng)內(nèi)部實(shí)現(xiàn)過程中需要的,它們定義在binder.c中,包括:

結(jié)構(gòu)體名稱 說明
binder_node 描述Binder實(shí)體節(jié)點(diǎn),即:對(duì)應(yīng)了一個(gè)Server
binder_ref 描述對(duì)于Binder實(shí)體的引用
binder_buffer 描述Binder通信過程中存儲(chǔ)數(shù)據(jù)的Buffer
binder_proc 描述使用Binder的進(jìn)程
binder_thread 描述使用Binder的線程
binder_work 描述通信過程中的一項(xiàng)任務(wù)
binder_transaction 描述一次事務(wù)的相關(guān)信息
binder_deferred_state 描述延遲任務(wù)
binder_ref_death 描述Binder實(shí)體死亡的信息
binder_transaction_log debugfs日志
binder_transaction_log_entry debugfs日志條目

這里需要讀者關(guān)注的結(jié)構(gòu)體已經(jīng)用加粗做了標(biāo)注。

Binder協(xié)議

Binder協(xié)議可以分為控制協(xié)議和驅(qū)動(dòng)協(xié)議兩類。
控制協(xié)議是進(jìn)程通過ioctl(“/dev/binder”) 與Binder設(shè)備進(jìn)行通訊的協(xié)議,該協(xié)議包含以下幾種命令:

命令 說明 參數(shù)類型
INDER_WRITE_READ 讀寫操作,最常用的命令。IPC過程就是通過這個(gè)命令進(jìn)行數(shù)據(jù)傳遞 binder_write_read
BINDER_SET_MAX_THREADS 設(shè)置進(jìn)程支持的最大線程數(shù)量 size_t
BINDER_SET_CONTEXT_MGR 設(shè)置自身為ServiceManager
BINDER_THREAD_EXIT 通知驅(qū)動(dòng)Binder線程退出
BINDER_VERSION 獲取Binder驅(qū)動(dòng)的版本號(hào) binder_version
BINDER_SET_IDLE_PRIORITY 暫未用到 -
BINDER_SET_IDLE_TIMEOUT 暫未用到 -

Binder的驅(qū)動(dòng)協(xié)議描述了對(duì)于Binder驅(qū)動(dòng)的具體使用過程。驅(qū)動(dòng)協(xié)議又可以分為兩類:

  • 一類是binder_driver_command_protocol,描述了進(jìn)程發(fā)送給Binder驅(qū)動(dòng)的命令
  • 一類是binder_driver_return_protocol,描述了Binder驅(qū)動(dòng)發(fā)送給進(jìn)程的命令
    binder_driver_command_protocol共包含17個(gè)命令,分別是:
    | 命令 | 說明 | 參數(shù)類型 |
    |----------|:-------------:|:-------------:|
    | BC_TRANSACTION | Binder事務(wù),即:Client對(duì)于Server的請求 | binder_transaction_data |
    | BC_REPLY | 事務(wù)的應(yīng)答,即:Server對(duì)于Client的回復(fù) | binder_transaction_data |
    | BC_FREE_BUFFER | 通知驅(qū)動(dòng)釋放Buffer | binder_uintptr_t |
    | BC_ACQUIRE | 強(qiáng)引用計(jì)數(shù)+1 | __u32 |
    | BC_RELEASE | 強(qiáng)引用計(jì)數(shù)-1 | __u32 |
    | BC_INCREFS | 弱引用計(jì)數(shù)+1 | __u32 |
    | BC_DECREFS | 弱引用計(jì)數(shù)-1 | __u32 |
    | BC_ACQUIRE_DONE | BR_ACQUIRE的回復(fù) | binder_ptr_cookie |
    | BC_INCREFS_DONE | BR_INCREFS的回復(fù) | binder_ptr_cookie |
    | BC_ENTER_LOOPER | 通知驅(qū)動(dòng)主線程ready | void |
    | BC_REGISTER_LOOPER | 通知驅(qū)動(dòng)子線程ready | void |
    | BC_EXIT_LOOPER | 通知驅(qū)動(dòng)線程已經(jīng)退出 | void |
    | BC_REQUEST_DEATH_NOTIFICATION | 請求接收死亡通知 | binder_handle_cookie |
    | BC_CLEAR_DEATH_NOTIFICATION | 去除接收死亡通知 | binder_handle_cookie |
    | BC_DEAD_BINDER_DONE | 已經(jīng)處理完死亡通知 | binder_uintptr_t |
    | BC_ATTEMPT_ACQUIRE | 暫未實(shí)現(xiàn) | - |
    | BC_ACQUIRE_RESULT | 暫未實(shí)現(xiàn) | - |

binder_driver_return_protocol共包含18個(gè)命令,分別是:

命令 說明 參數(shù)類型
BR_OK 操作完成 void
BR_NOOP 操作完成 void
BR_ERROR 發(fā)生錯(cuò)誤 __s32
BR_TRANSACTION 通知進(jìn)程收到一次Binder請求(Server端) binder_transaction_data
BR_REPLY 通知進(jìn)程收到Binder請求的回復(fù)(Client) binder_transaction_data
BR_TRANSACTION_COMPLETE 驅(qū)動(dòng)對(duì)于接受請求的確認(rèn)回復(fù) void
BR_FAILED_REPLY 告知發(fā)送方通信目標(biāo)不存在 void
BR_SPAWN_LOOPER 通知Binder進(jìn)程創(chuàng)建一個(gè)新的線程 void
BR_ACQUIRE 強(qiáng)引用計(jì)數(shù)+1請求 binder_ptr_cookie
BR_RELEASE 強(qiáng)引用計(jì)數(shù)-1請求 binder_ptr_cookie
BR_INCREFS 弱引用計(jì)數(shù)+1請求 binder_ptr_cookie
BR_DECREFS 若引用計(jì)數(shù)-1請求 binder_ptr_cookie
BR_DEAD_BINDER 發(fā)送死亡通知 binder_uintptr_t
BR_CLEAR_DEATH_NOTIFICATION_DONE 清理死亡通知完成 binder_uintptr_t
BR_DEAD_REPLY 告知發(fā)送方對(duì)方已經(jīng)死亡 void
BR_ACQUIRE_RESULT 暫未實(shí)現(xiàn) -
BR_ATTEMPT_ACQUIRE 暫未實(shí)現(xiàn) -
BR_FINISHED 暫未實(shí)現(xiàn) -

單獨(dú)看上面的協(xié)議可能很難理解,這里我們以一次Binder請求過程來詳細(xì)看一下Binder協(xié)議是如何通信的,就比較好理解了。
這幅圖的說明如下:

  • Binder是C/S架構(gòu)的,通信過程牽涉到:Client,Server以及Binder驅(qū)動(dòng)三個(gè)角色
  • Client對(duì)于Server的請求以及Server對(duì)于Client回復(fù)都需要通過Binder驅(qū)動(dòng)來中轉(zhuǎn)數(shù)據(jù)
  • BC_XXX命令是進(jìn)程發(fā)送給驅(qū)動(dòng)的命令
  • BR_XXX命令是驅(qū)動(dòng)發(fā)送給進(jìn)程的命令
  • 整個(gè)通信過程由Binder驅(qū)動(dòng)控制


    圖片.png

這里再補(bǔ)充說明一下,通過上面的Binder協(xié)議的說明中我們看到,Binder協(xié)議的通信過程中,不僅僅是發(fā)送請求和接受數(shù)據(jù)這些命令。同時(shí)包括了對(duì)于引用計(jì)數(shù)的管理和對(duì)于死亡通知的管理(告知一方,通訊的另外一方已經(jīng)死亡)等功能。
這些功能的通信過程和上面這幅圖是類似的:一方發(fā)送BC_XXX,然后由驅(qū)動(dòng)控制通信過程,接著發(fā)送對(duì)應(yīng)的BR_XXX命令給通信的另外一方。因?yàn)檫@種相似性,對(duì)于這些內(nèi)容就不再贅述了。
在有了上面這些背景知識(shí)介紹之后,我們就可以進(jìn)入到Binder驅(qū)動(dòng)的內(nèi)部實(shí)現(xiàn)中來一探究竟了。
PS:上面介紹的這些結(jié)構(gòu)體和協(xié)議,因?yàn)閮?nèi)容較多,初次看完記不住是很正常的,在下文詳細(xì)講解的時(shí)候,回過頭來對(duì)照這些表格來理解是比較有幫助的。

打開Binder設(shè)備

任何進(jìn)程在使用Binder之前,都需要先通過open("/dev/binder")打開Binder設(shè)備。上文已經(jīng)提到,用戶空間的open系統(tǒng)調(diào)用對(duì)應(yīng)了驅(qū)動(dòng)中的binder_open函數(shù)。在這個(gè)函數(shù),Binder驅(qū)動(dòng)會(huì)為調(diào)用的進(jìn)程做一些初始化工作。binder_open函數(shù)代碼如下所示:

static int binder_open(struct inode *nodp, struct file *filp)
{
    struct binder_proc *proc;
   // 創(chuàng)建進(jìn)程對(duì)應(yīng)的binder_proc對(duì)象
    proc = kzalloc(sizeof(*proc), GFP_KERNEL);
    if (proc == NULL)
        return -ENOMEM;
    get_task_struct(current);
    proc->tsk = current;
    // 初始化binder_proc
    INIT_LIST_HEAD(&proc->todo);
    init_waitqueue_head(&proc->wait);
    proc->default_priority = task_nice(current);
  // 鎖保護(hù)
    binder_lock(__func__);
    binder_stats_created(BINDER_STAT_PROC);
    // 添加到全局列表binder_procs中
    hlist_add_head(&proc->proc_node, &binder_procs);
    proc->pid = current->group_leader->pid;
    INIT_LIST_HEAD(&proc->delivered_death);
    filp->private_data = proc;
    binder_unlock(__func__);
    return 0;
}

在Binder驅(qū)動(dòng)中,通過binder_procs記錄了所有使用Binder的進(jìn)程。每個(gè)初次打開Binder設(shè)備的進(jìn)程都會(huì)被添加到這個(gè)列表中的。
另外,請讀者回顧一下上文介紹的Binder驅(qū)動(dòng)中的幾個(gè)關(guān)鍵結(jié)構(gòu)體:

  • binder_proc
  • binder_node
  • binder_thread
  • binder_ref
  • binder_buffer
    在實(shí)現(xiàn)過程中,為了便于查找,這些結(jié)構(gòu)體互相之間都留有字段存儲(chǔ)關(guān)聯(lián)的結(jié)構(gòu)。
    下面這幅圖描述了這里說到的這些內(nèi)容:


    圖片.png

內(nèi)存映射(mmap)

在打開Binder設(shè)備之后,進(jìn)程還會(huì)通過mmap進(jìn)行內(nèi)存映射。mmap的作用有如下兩個(gè):

  • 申請一塊內(nèi)存空間,用來接收Binder通信過程中的數(shù)據(jù)
  • 對(duì)這塊內(nèi)存進(jìn)行地址映射,以便將來訪問
  1. binder_mmap函數(shù)對(duì)應(yīng)了mmap系統(tǒng)調(diào)用的處理,這個(gè)函數(shù)也是Binder驅(qū)動(dòng)的精華所在(這里說的binder_mmap函數(shù)也包括其內(nèi)部調(diào)用的binder_update_page_range函數(shù),見下文)。
    前文我們說到,使用Binder機(jī)制,數(shù)據(jù)只需要經(jīng)歷一次拷貝就可以了,其原理就在這個(gè)函數(shù)中。
  2. binder_mmap這個(gè)函數(shù)中,會(huì)申請一塊物理內(nèi)存,然后在用戶空間和內(nèi)核空間同時(shí)對(duì)應(yīng)到這塊內(nèi)存上。在這之后,當(dāng)有Client要發(fā)送數(shù)據(jù)給Server的時(shí)候,只需一次,將Client發(fā)送過來的數(shù)據(jù)拷貝到Server端的內(nèi)核空間指定的內(nèi)存地址即可,由于這個(gè)內(nèi)存地址在服務(wù)端已經(jīng)同時(shí)映射到用戶空間,因此無需再做一次復(fù)制,Server即可直接訪問,整個(gè)過程如下圖所示:


    圖片.png

這幅圖的說明如下:
1. Server在啟動(dòng)之后,調(diào)用對(duì)/dev/binder設(shè)備調(diào)用mmap
2. 內(nèi)核中的binder_mmap函數(shù)進(jìn)行對(duì)應(yīng)的處理:申請一塊物理內(nèi)存,然后在用戶空間和內(nèi)核空間同時(shí)進(jìn)行映射
3. Client通過BINDER_WRITE_READ命令發(fā)送請求,這個(gè)請求將先到驅(qū)動(dòng)中,同時(shí)需要將數(shù)據(jù)從Client進(jìn)程的用戶空間拷貝到內(nèi)核空間
4. 驅(qū)動(dòng)通過BR_TRANSACTION通知Server有人發(fā)出請求,Server進(jìn)行處理。由于這塊內(nèi)存也在用戶空間進(jìn)行了映射,因此Server進(jìn)程的代碼可以直接訪問

了解原理之后,我們再來看一下Binder驅(qū)動(dòng)的相關(guān)源碼。這段代碼有兩個(gè)函數(shù):

  • binder_mmap函數(shù)對(duì)應(yīng)了mmap的系統(tǒng)調(diào)用的處理
  • binder_update_page_range函數(shù)真正實(shí)現(xiàn)了內(nèi)存分配和地址映射
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)核空間獲取一塊地址范圍
    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;
    // 記錄內(nèi)核空間與用戶空間的地址偏移
    proc->user_buffer_offset = vma->vm_start - (uintptr_t)proc->buffer;
    mutex_unlock(&binder_mmap_lock);

  ...
    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;

    /* binder_update_page_range assumes preemption is disabled */
    preempt_disable();
    // 通過下面這個(gè)函數(shù)真正完成內(nèi)存的申請和地址的映射
    // 初次使用,先申請一個(gè)PAGE_SIZE大小的內(nèi)存
    ret = binder_update_page_range(proc, 1, proc->buffer, proc->buffer + PAGE_SIZE, vma);
    ...
}

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;

    ...

    for (page_addr = start; page_addr < end; page_addr += PAGE_SIZE) {
        int ret;
        struct page **page_array_ptr;
        page = &proc->pages[(page_addr - proc->buffer) / PAGE_SIZE];

        BUG_ON(*page);
        // 真正進(jìn)行內(nèi)存的分配
        *page = alloc_page(GFP_KERNEL | __GFP_HIGHMEM | __GFP_ZERO);
        if (*page == NULL) {
            pr_err("%d: binder_alloc_buf failed for page at %p\n",
                proc->pid, page_addr);
            goto err_alloc_page_failed;
        }
        tmp_area.addr = page_addr;
        tmp_area.size = PAGE_SIZE + PAGE_SIZE /* guard page? */;
        page_array_ptr = page;
        // 在內(nèi)核空間進(jìn)行內(nèi)存映射
        ret = map_vm_area(&tmp_area, PAGE_KERNEL, &page_array_ptr);
        if (ret) {
            pr_err("%d: binder_alloc_buf failed to map page at %p in kernel\n",
                   proc->pid, page_addr);
            goto err_map_kernel_failed;
        }
        user_page_addr =
            (uintptr_t)page_addr + proc->user_buffer_offset;
        // 在用戶空間進(jìn)行內(nèi)存映射
        ret = vm_insert_page(vma, user_page_addr, page[0]);
        if (ret) {
            pr_err("%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);
    }

    preempt_disable();

    return 0;
...

在開發(fā)過程中,我們可以通過procfs看到進(jìn)程映射的這塊內(nèi)存空間:
1. 將Android設(shè)備連接到電腦上之后,通過adb shell進(jìn)入到終端
2. 然后選擇一個(gè)使用了Binder的進(jìn)程,例如system_server(這是系統(tǒng)中一個(gè)非常重要的進(jìn)程,下一章我們會(huì)專門講解),通過 ps | grep system_server來確定進(jìn)程號(hào),例如是1889
3. 通過 cat /proc/[pid]/maps | grep "/dev/binder" 過濾出這塊內(nèi)存的地址

在我的Nexus 6P上,控制臺(tái)輸出如下:

angler:/ # ps  | grep system_server
system    1889  526   2353404 140016 SyS_epoll_ 72972eeaf4 S system_server
angler:/ # cat /proc/1889/maps | grep "/dev/binder"
7294761000-729485f000 r--p 00000000 00:0c 12593                          /dev/binder

PS:grep是通過通配符進(jìn)行匹配過濾的命令,“|”是Unix上的管道命令。即將前一個(gè)命令的輸出給下一個(gè)命令作為輸入。如果這里我們不加“ | grep xxx”,那么將看到前一個(gè)命令的完整輸出。

內(nèi)存的管理

上文中,我們看到binder_mmap的時(shí)候,會(huì)申請一個(gè)PAGE_SIZE(通常是4K)的內(nèi)存。而實(shí)際使用過程中,一個(gè)PAGE_SIZE的大小通常是不夠的。
在驅(qū)動(dòng)中,會(huì)根據(jù)實(shí)際的使用情況進(jìn)行內(nèi)存的分配。有內(nèi)存的分配,當(dāng)然也需要內(nèi)存的釋放。這里我們就來看看Binder驅(qū)動(dòng)中是如何進(jìn)行內(nèi)存的管理的。
首先,我們還是從一次IPC請求說起。
當(dāng)一個(gè)Client想要對(duì)Server發(fā)出請求時(shí),它首先將請求發(fā)送到Binder設(shè)備上,由Binder驅(qū)動(dòng)根據(jù)請求的信息找到對(duì)應(yīng)的目標(biāo)節(jié)點(diǎn),然后將請求數(shù)據(jù)傳遞過去。
進(jìn)程通過ioctl系統(tǒng)調(diào)用來發(fā)出請求:ioctl(mProcess->mDriverFD, BINDER_WRITE_READ, &bwr)
PS:這行代碼來自于Framework層的IPCThreadState類。在后文中,我們將看到,IPCThreadState類專門負(fù)責(zé)與驅(qū)動(dòng)進(jìn)行通信。
這里的mProcess->mDriverFD對(duì)應(yīng)了打開Binder設(shè)備時(shí)的fd。BINDER_WRITE_READ對(duì)應(yīng)了具體要做的操作碼,這個(gè)操作碼將由Binder驅(qū)動(dòng)解析。bwr存儲(chǔ)了請求數(shù)據(jù),其類型是binder_write_read。
binder_write_read其實(shí)是一個(gè)相對(duì)外層的數(shù)據(jù)結(jié)構(gòu),其內(nèi)部會(huì)包含一個(gè)binder_transaction_data結(jié)構(gòu)的數(shù)據(jù)。binder_transaction_data包含了發(fā)出請求者的標(biāo)識(shí),請求的目標(biāo)對(duì)象以及請求所需要的參數(shù)。它們的關(guān)系如下圖所示:

圖片.png

binder_ioctl函數(shù)對(duì)應(yīng)了ioctl系統(tǒng)調(diào)用的處理。這個(gè)函數(shù)的邏輯比較簡單,就是根據(jù)ioctl的命令來確定進(jìn)一步處理的邏輯,具體如下:

  • 如果命令是BINDER_WRITE_READ,并且
    • 如果 bwr.write_size > 0,則調(diào)用binder_thread_write
    • 如果 bwr.read_size > 0,則調(diào)用binder_thread_read
  • 如果命令是BINDER_SET_MAX_THREADS,則設(shè)置進(jìn)程的max_threads,即進(jìn)程支持的最大線程數(shù)
  • 如果命令是BINDER_SET_CONTEXT_MGR,則設(shè)置當(dāng)前進(jìn)程為ServiceManager,見下文
  • 如果命令是BINDER_THREAD_EXIT,則調(diào)用binder_free_thread,釋放binder_thread
  • 如果命令是BINDER_VERSION,則返回當(dāng)前的Binder版本號(hào)
    這其中,最關(guān)鍵的就是binder_thread_write方法。當(dāng)Client請求Server的時(shí)候,便會(huì)發(fā)送一個(gè)BINDER_WRITE_READ命令,同時(shí)框架會(huì)將將實(shí)際的數(shù)據(jù)包裝好。此時(shí),binder_transaction_data中的code將是BC_TRANSACTION,由此便會(huì)調(diào)用到binder_transaction方法,這個(gè)方法是對(duì)一次Binder事務(wù)的處理,這其中會(huì)調(diào)用binder_alloc_buf函數(shù)為此次事務(wù)申請一個(gè)緩存。這里提到到調(diào)用關(guān)系如下:


    圖片.png

    binder_update_page_range這個(gè)函數(shù)在上文中,我們已經(jīng)看到過了。其作用就是:進(jìn)行內(nèi)存分配并且完成內(nèi)存的映射。而binder_alloc_buf函數(shù),正如其名稱那樣的:完成緩存的分配。
    在驅(qū)動(dòng)中,通過binder_buffer結(jié)構(gòu)體描述緩存。一次Binder事務(wù)就會(huì)對(duì)應(yīng)一個(gè)binder_buffer,其結(jié)構(gòu)如下所示:

struct binder_buffer {
    struct list_head entry;
    struct rb_node rb_node;

    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];
};

而在binder_proc(描述了使用Binder的進(jìn)程)中,包含了幾個(gè)字段用來管理進(jìn)程在Binder IPC過程中緩存,如下:

struct binder_proc {
    ...
    struct list_head buffers; // 進(jìn)程擁有的buffer列表
    struct rb_root free_buffers; // 空閑buffer列表
    struct rb_root allocated_buffers; // 已使用的buffer列表
    size_t free_async_space; // 剩余的異步調(diào)用的空間

    size_t buffer_size; // 緩存的上限
  ...
};

進(jìn)程在mmap時(shí),會(huì)設(shè)定支持的總緩存大小的上限(下文會(huì)講到)。而進(jìn)程每當(dāng)收到BC_TRANSACTION,就會(huì)判斷已使用緩存加本次申請的和有沒有超過上限。如果沒有,就考慮進(jìn)行內(nèi)存的分配。
進(jìn)程的空閑緩存記錄在binder_proc的free_buffers中,這是一個(gè)以紅黑樹形式存儲(chǔ)的結(jié)構(gòu)。每次嘗試分配緩存的時(shí)候,會(huì)從這里面按大小順序進(jìn)行查找,找到最接近需要的一塊緩存。查找的邏輯如下:

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;
    }
}

找到之后,還需要對(duì)binder_proc中的字段進(jìn)行相應(yīng)的更新:

rb_erase(best_fit, &proc->free_buffers);
buffer->free = 0;
binder_insert_allocated_buffer(proc, buffer);
if (buffer_size != size) {
    struct binder_buffer *new_buffer = (void *)buffer->data + size;
    list_add(&new_buffer->entry, &buffer->entry);
    new_buffer->free = 1;
    binder_insert_free_buffer(proc, new_buffer);
}
binder_debug(BINDER_DEBUG_BUFFER_ALLOC,
         "%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,
             "%d: binder_alloc_buf size %zd async free %zd\n",
              proc->pid, size, proc->free_async_space);
}

下面我們再來看看內(nèi)存的釋放。
BC_FREE_BUFFER命令是通知驅(qū)動(dòng)進(jìn)行內(nèi)存的釋放,binder_free_buf函數(shù)是真正實(shí)現(xiàn)的邏輯,這個(gè)函數(shù)與binder_alloc_buf是剛好對(duì)應(yīng)的。在這個(gè)函數(shù)中,所做的事情包括:

  • 重新計(jì)算進(jìn)程的空閑緩存大小
  • 通過binder_update_page_range釋放內(nèi)存
    -更新binder_proc的buffers,free_buffers,allocated_buffers字段

Binder中的“面向?qū)ο蟆?/h2>

Binder機(jī)制淡化了進(jìn)程的邊界,使得跨越進(jìn)程也能夠調(diào)用到指定服務(wù)的方法,其原因是因?yàn)锽inder機(jī)制在底層處理了在進(jìn)程間的“對(duì)象”傳遞。
在Binder驅(qū)動(dòng)中,并不是真的將對(duì)象在進(jìn)程間來回序列化,而是通過特定的標(biāo)識(shí)來進(jìn)行對(duì)象的傳遞。Binder驅(qū)動(dòng)中,通過flat_binder_object來描述需要跨越進(jìn)程傳遞的對(duì)象。其定義如下:

struct flat_binder_object {
    __u32       type;
    __u32       flags;

    union {
        binder_uintptr_t    binder; /* local object */
        __u32           handle; /* remote object */
    };
    binder_uintptr_t    cookie;
};

這其中,type有如下5種類型。

enum {
    BINDER_TYPE_BINDER  = B_PACK_CHARS('s', 'b', '*', B_TYPE_LARGE),
    BINDER_TYPE_WEAK_BINDER = B_PACK_CHARS('w', 'b', '*', B_TYPE_LARGE),
    BINDER_TYPE_HANDLE  = B_PACK_CHARS('s', 'h', '*', B_TYPE_LARGE),
    BINDER_TYPE_WEAK_HANDLE = B_PACK_CHARS('w', 'h', '*', B_TYPE_LARGE),
    BINDER_TYPE_FD      = B_PACK_CHARS('f', 'd', '*', B_TYPE_LARGE),
};

當(dāng)對(duì)象傳遞到Binder驅(qū)動(dòng)中的時(shí)候,由驅(qū)動(dòng)來進(jìn)行翻譯和解釋,然后傳遞到接收的進(jìn)程。
例如當(dāng)Server把Binder實(shí)體傳遞給Client時(shí),在發(fā)送數(shù)據(jù)流中,flat_binder_object中的type是BINDER_TYPE_BINDER,同時(shí)binder字段指向Server進(jìn)程用戶空間地址。但這個(gè)地址對(duì)于Client進(jìn)程是沒有意義的(Linux中,每個(gè)進(jìn)程的地址空間是互相隔離的),驅(qū)動(dòng)必須對(duì)數(shù)據(jù)流中的flat_binder_object做相應(yīng)的翻譯:將type該成BINDER_TYPE_HANDLE;為這個(gè)Binder在接收進(jìn)程中創(chuàng)建位于內(nèi)核中的引用并將引用號(hào)填入handle中。對(duì)于發(fā)生數(shù)據(jù)流中引用類型的Binder也要做同樣轉(zhuǎn)換。經(jīng)過處理后接收進(jìn)程從數(shù)據(jù)流中取得的Binder引用才是有效的,才可以將其填入數(shù)據(jù)包binder_transaction_data的target.handle域,向Binder實(shí)體發(fā)送請求。
由于每個(gè)請求和請求的返回都會(huì)經(jīng)歷內(nèi)核的翻譯,因此這個(gè)過程從進(jìn)程的角度來看是完全透明的。進(jìn)程完全不用感知這個(gè)過程,就好像對(duì)象真的在進(jìn)程間來回傳遞一樣。

驅(qū)動(dòng)層的線程管理

上文多次提到,Binder本身是C/S架構(gòu)。由Server提供服務(wù),被Client使用。既然是C/S架構(gòu),就可能存在多個(gè)Client會(huì)同時(shí)訪問Server的情況。 在這種情況下,如果Server只有一個(gè)線程處理響應(yīng),就會(huì)導(dǎo)致客戶端的請求可能需要排隊(duì)而導(dǎo)致響應(yīng)過慢的現(xiàn)象發(fā)生。解決這個(gè)問題的方法就是引入多線程。

Binder機(jī)制的設(shè)計(jì)從最底層–驅(qū)動(dòng)層,就考慮到了對(duì)于多線程的支持。具體內(nèi)容如下:

  • 使用Binder的進(jìn)程在啟動(dòng)之后,通過BINDER_SET_MAX_THREADS告知驅(qū)動(dòng)其支持的最大線程數(shù)量
  • 驅(qū)動(dòng)會(huì)對(duì)線程進(jìn)行管理。在binder_proc結(jié)構(gòu)中,這些字段記錄了進(jìn)程中線程的信息:max_threads,requested_threads,requested_threads_started,ready_threads
  • binder_thread結(jié)構(gòu)對(duì)應(yīng)了Binder進(jìn)程中的線程
  • 驅(qū)動(dòng)通過BR_SPAWN_LOOPER命令告知進(jìn)程需要?jiǎng)?chuàng)建一個(gè)新的線程
  • 進(jìn)程通過BC_ENTER_LOOPER命令告知驅(qū)動(dòng)其主線程已經(jīng)ready
  • 進(jìn)程通過BC_REGISTER_LOOPER命令告知驅(qū)動(dòng)其子線程(非主線程)已經(jīng)ready
  • 進(jìn)程通過BC_EXIT_LOOPER命令告知驅(qū)動(dòng)其線程將要退出
  • 在線程退出之后,通過BINDER_THREAD_EXIT告知Binder驅(qū)動(dòng)。驅(qū)動(dòng)將對(duì)應(yīng)的binder_thread對(duì)象銷毀

再聊ServiceManager

上文已經(jīng)說過,每一個(gè)Binder Server在驅(qū)動(dòng)中會(huì)有一個(gè)binder_node進(jìn)行對(duì)應(yīng)。同時(shí),Binder驅(qū)動(dòng)會(huì)負(fù)責(zé)在進(jìn)程間傳遞服務(wù)對(duì)象,并負(fù)責(zé)底層的轉(zhuǎn)換。另外,我們也提到,每一個(gè)Binder服務(wù)都需要有一個(gè)唯一的名稱。由ServiceManager來管理這些服務(wù)的注冊和查找。
而實(shí)際上,為了便于使用,ServiceManager本身也實(shí)現(xiàn)為一個(gè)Server對(duì)象。任何進(jìn)程在使用ServiceManager的時(shí)候,都需要先拿到指向它的標(biāo)識(shí)。然后通過這個(gè)標(biāo)識(shí)來使用ServiceManager。

這似乎形成了一個(gè)互相矛盾的現(xiàn)象:

  • 通過ServiceManager我們才能拿到Server的標(biāo)識(shí)
  • ServiceManager本身也是一個(gè)Server

解決這個(gè)矛盾的辦法其實(shí)也很簡單:Binder機(jī)制為ServiceManager預(yù)留了一個(gè)特殊的位置。這個(gè)位置是預(yù)先定好的,任何想要使用ServiceManager的進(jìn)程只要通過這個(gè)特定的位置就可以訪問到ServiceManager了(而不用再通過ServiceManager的接口)。

在Binder驅(qū)動(dòng)中,有一個(gè)全局的變量:

static struct binder_node *binder_context_mgr_node;

這個(gè)變量指向的就是ServiceManager。
當(dāng)有進(jìn)程通過ioctl并指定命令為BINDER_SET_CONTEXT_MGR的時(shí)候,驅(qū)動(dòng)被認(rèn)定這個(gè)進(jìn)程是ServiceManager,binder_ioctl函數(shù)中對(duì)應(yīng)的處理如下:

case BINDER_SET_CONTEXT_MGR:
    if (binder_context_mgr_node != NULL) {
        pr_err("BINDER_SET_CONTEXT_MGR already set\n");
        ret = -EBUSY;
        goto err;
    }
    ret = security_binder_set_context_mgr(proc->tsk);
    if (ret < 0)
        goto err;
    if (uid_valid(binder_context_mgr_uid)) {
        if (!uid_eq(binder_context_mgr_uid, current->cred->euid)) {
            pr_err("BINDER_SET_CONTEXT_MGR bad uid %d != %d\n",
                   from_kuid(&init_user_ns, current->cred->euid),
                   from_kuid(&init_user_ns, binder_context_mgr_uid));
            ret = -EPERM;
            goto err;
        }
    } else
        binder_context_mgr_uid = current->cred->euid;
    binder_context_mgr_node = binder_new_node(proc, 0, 0);
    if (binder_context_mgr_node == NULL) {
        ret = -ENOMEM;
        goto err;
    }
    binder_context_mgr_node->local_weak_refs++;
    binder_context_mgr_node->local_strong_refs++;
    binder_context_mgr_node->has_strong_ref = 1;
    binder_context_mgr_node->has_weak_ref = 1;
    break;

ServiceManager應(yīng)當(dāng)要先于所有Binder Server之前啟動(dòng)。在它啟動(dòng)完成并告知Binder驅(qū)動(dòng)之后,驅(qū)動(dòng)便設(shè)定好了這個(gè)特定的節(jié)點(diǎn)。
在這之后,當(dāng)有其他模塊想要使用ServerManager的時(shí)候,只要將請求指向ServiceManager所在的位置即可。
在Binder驅(qū)動(dòng)中,通過handle = 0這個(gè)位置來訪問ServiceManager。例如,binder_transaction中,判斷如果target.handler為0,則認(rèn)為這個(gè)請求是發(fā)送給ServiceManager的,相關(guān)代碼如下:

if (tr->target.handle) {
    struct binder_ref *ref;
    ref = binder_get_ref(proc, tr->target.handle, true);
    if (ref == NULL) {
        binder_user_error("%d:%d got transaction to invalid handle\n",
            proc->pid, thread->pid);
        return_error = BR_FAILED_REPLY;
        goto err_invalid_target_handle;
    }
    target_node = ref->node;
} else {
    target_node = binder_context_mgr_node;
    if (target_node == NULL) {
        return_error = BR_DEAD_REPLY;
        goto err_no_context_mgr_node;
    }
}

結(jié)束語

本篇文章中,我們對(duì)Binder機(jī)制做了整體架構(gòu)和分層的介紹,也詳細(xì)講解了Binder機(jī)制中的驅(qū)動(dòng)模塊。對(duì)于驅(qū)動(dòng)之上的模塊,會(huì)在今后的文章中講解。

原文地址:《理解Android Binder機(jī)制(1/3):驅(qū)動(dòng)篇》 by 保羅的酒吧

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容