能用【白話文】來分析Binder通訊機制?

image

Binder系列第一篇:《從getSystemService()開始,開擼Binder通訊機制》http://www.itdecent.cn/p/1050ce12bc1e

Binder系列第二篇:《能用【白話文】來分析Binder通訊機制?》http://www.itdecent.cn/p/fe816777f2cf

Binder系列第三篇:《Binder機制之一次響應(yīng)的故事》http://www.itdecent.cn/p/4fba927dce05

CoorChice在上次的文章 《從getSystemService()開始,開擼Binder通訊機制:http://www.itdecent.cn/p/1050ce12bc1e》 中留了一些關(guān)于Binder的坑,也許大家看的時候有些云里霧里的,這篇文章,CoorChice就開始填這些坑了。并開始逐步的深入Binder核心機制,讓你對Android中最重要的部分有所了解。

好了,咱們發(fā)車了!

由open_driver()開始

image


《從getSystemService()開始,開擼Binder通訊機制:http://www.itdecent.cn/p/1050ce12bc1e》這篇文章中,相信大家應(yīng)該看到在/frameworks/native/libs/binder/ProcessState.cpp文件中,有這樣一段代碼。

static int open_driver()
{
    //打開"/dev/binder"Binder驅(qū)動文件,并獲得其描述符
    int fd = open("/dev/binder", O_RDWR);
    ...
    //獲取Binder驅(qū)動程序的版本號
    status_t result = ioctl(fd, BINDER_VERSION, &vers);
    ...
    size_t maxThreads = 15;
    //告知驅(qū)動程序最多可以啟動15條線程處理事物
    result = ioctl(fd, BINDER_SET_MAX_THREADS, &maxThreads);
    ...
    return fd;
}

它在ProcessState創(chuàng)建的時候會被調(diào)用。它肩負(fù)了一項重要的使命,就是在該進程中打開/dev/binder設(shè)備文件,然后獲得該設(shè)備文件的描述符??梢钥吹?,打開設(shè)備文件是通過open()函數(shù)實現(xiàn)的,它是怎么實現(xiàn)的呢?

用戶空間函數(shù)與Binder驅(qū)動函數(shù)

首先打開/drivers/staging/android/binder.c文件,然后找到下面這個結(jié)構(gòu)體:

//這個結(jié)構(gòu)體中定義了文件操作符與Binder驅(qū)動的對應(yīng)函數(shù)關(guān)聯(lián)
static const struct file_operations binder_fops = {
    .owner = THIS_MODULE,
    .poll = binder_poll,
    .unlocked_ioctl = binder_ioctl,
    .compat_ioctl = binder_ioctl,
    //用戶空間的mmap()操作,會引起B(yǎng)inder驅(qū)動的binder_mmap()函數(shù)的調(diào)用
    .mmap = binder_mmap,
    //用戶空間的open()操作,會引起B(yǎng)inder驅(qū)動的binder_open()函數(shù)的調(diào)用
    .open = binder_open, 
    .flush = binder_flush,
    .release = binder_release,
};

這個結(jié)構(gòu)體會作為下面這個結(jié)構(gòu)體的一個成員,在Binder驅(qū)動注冊的時候被和Linux定義的文件操作符關(guān)聯(lián)。

static struct miscdevice binder_miscdev = {
    .minor = MISC_DYNAMIC_MINOR,
    //定義設(shè)備節(jié)點文件名。這里Binder驅(qū)動設(shè)備的文件路徑即為/dev/binder
    .name = "binder",  
    //關(guān)聯(lián)Linux文件操作符
    .fops = &binder_fops
};

這樣,在Binder驅(qū)動設(shè)備注冊完成后,在用戶空間調(diào)用poll()、open()等函數(shù)的時候,Binder驅(qū)動的對應(yīng)函數(shù)就會被調(diào)用。

open()函數(shù)的真面目

現(xiàn)在,我們知道了,當(dāng)我們在用戶空間調(diào)用open()函數(shù)時,Binder驅(qū)動層的binder_open()函數(shù)會被隨之調(diào)用。我們看看binder_open()函數(shù)做了些什么?

//用戶空間調(diào)用open()實際調(diào)用的是這里
//參數(shù)為打開設(shè)備文件后傳遞過來的
static int binder_open(struct inode *nodp, struct file *filp)
{
    //binder_proc儲存進程信息的結(jié)構(gòu)體
    //注意,這個進程結(jié)構(gòu)體是存在于Binder內(nèi)核空間中的
    struct binder_proc *proc;
    //將當(dāng)前進程的信息儲存到binder_proc中
    ...
    //鎖定同步
    binder_lock(__func__);
    ...
    //將該進程上下文信息proc保存到Binder驅(qū)動的進程樹中
    //以便查找使用
    hlist_add_head(&proc->proc_node, &binder_procs);
    // 設(shè)置進程id
    proc->pid = current->group_leader->pid;
    ...
    // 將進程信息結(jié)構(gòu)體賦值給文件私有數(shù)據(jù)
    filp->private_data = proc;
    //釋放鎖
    binder_unlock(__func__);
    ...
    //在/proc/binder/proc下創(chuàng)建名為進程id的文件,便于查看進程的通訊
    snprintf(strbuf, sizeof(strbuf), "%u", proc->pid);
    return 0;
}

這個函數(shù)主要的作用是為打開了/dev/binder設(shè)備文件的進程生成一個專屬的進程信息體,然后保存到驅(qū)動中。這樣,該進程就能和Binder驅(qū)動互動了。

可能有的細(xì)心的同學(xué)會發(fā)現(xiàn),open()函數(shù)會返回設(shè)備表述符,而binder_open()函數(shù)看起來只會返回0???CoorChice在前面說過,binder_open()只是和open()產(chǎn)生了關(guān)聯(lián),但實際打開設(shè)備文件的操作還是Linux再進行。想必你也可以看到,binder_open()函數(shù)的參數(shù)是在設(shè)備文件打開后才可能獲取的。所以,這個設(shè)備描述符應(yīng)該是由Linux來分配給進程的。

下面,接著看看在open_driver()中出現(xiàn)的另一個函數(shù)ioctl()。

ioctl()函數(shù)的真面目

如果你理解了上面的open()函數(shù),那么自然就知道,用戶空間的ioctl()函數(shù)會引起B(yǎng)inder驅(qū)動層的binder_ioctl()函數(shù)的調(diào)用。我們就看看驅(qū)動層的這個函數(shù)做了什么?這是一個十分重要的函數(shù)啊!

static long binder_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
    ...
    // 從file結(jié)構(gòu)體中取出進程信息
    struct binder_proc *proc = filp->private_data;
    struct binder_thread *thread;
    unsigned int size = _IOC_SIZE(cmd);
    //表明arg是一個用戶空間地址
    void __user *ubuf = (void __user *)arg;
    ...
    //取出線程信息
    thread = binder_get_thread(proc);
    ...
    switch (cmd) {
        ...
    }
    ...
}

同樣,用戶層調(diào)用ioctl(fd, cmd, arg)函數(shù),會先由Linux內(nèi)核根據(jù)設(shè)備描述符fd獲得對應(yīng)的設(shè)備文件體file,然后調(diào)用Binder驅(qū)動的binder_ioctl()函數(shù)。

這個函數(shù)比較重要,CoorChice再一步一步的解析一下。

image

獲得進程信息體

首先Binder驅(qū)動根據(jù)傳入的文件體獲得其中的進程信息。

struct binder_proc *proc = filp->private_data;

還記得在binder_open()中生成的那個進程信息結(jié)構(gòu)體嗎?

來自用戶空間的參數(shù)

void __user *ubuf = (void __user *)arg;

首先我們需要知道,這個arg是從用戶空間傳遞過來的地址。比如ioctl(fd, BINDER_VERSION, &vers)傳遞過來了一個地址,指向用來儲存Binder版本號的空間。

在Binder驅(qū)動層,需要對這個地址進行轉(zhuǎn)換一下,用__user給它做上標(biāo)記,表明它指向的是用戶空間的地址。那么,這個arg指向的空間與Binder驅(qū)動的內(nèi)存空間的數(shù)據(jù)傳遞就需要通過copy_from_user()或者copy_to_user()來進行了。

不同的cmd對應(yīng)不同的操作

switch (cmd) {
        ...
    }

switch中定義了幾個命令,分別對應(yīng)不同的操作,CoorChice不在這全部說了,后面遇到再說。

我們先看看在open_driver()中出現(xiàn)的兩個cmd就行了。

  • BINDER_VERSION
    這個命令用于獲取Binder驅(qū)動版本號。
//獲取Binder驅(qū)動的版本號
case BINDER_VERSION: {
    //表示用戶空間的binder版本信息
    struct binder_version __user *ver = ubuf;
    ...
    //把版本號賦值給binder_version的protocol_version成員
    if (put_user(BINDER_CURRENT_PROTOCOL_VERSION,
                 &ver->protocol_version)) {
        ...
    }
    break;
}

注意,上面不是直接賦值,而是使用了put_user()函數(shù)。因為這個值是需要寫到用戶空間去的。

  • BINDER_SET_MAX_THREADS
    設(shè)置進程可用于Binder通訊的最大線程數(shù)量。
//設(shè)置用戶進程最大線程數(shù)
case BINDER_SET_MAX_THREADS:
    //使用copy_from_user()函數(shù),將用戶空間的數(shù)據(jù)拷貝到內(nèi)核空間
    //這里就是把線程數(shù)拷貝給進程結(jié)構(gòu)體的max_threads
    if (copy_from_user(&proc->max_threads, ubuf, sizeof(proc->max_threads))) {
        ...
    }
    break;

注意,上面使用了copy_from_user()函數(shù),把用戶空間的值,寫到了驅(qū)動層的進程信息體的成員max_threads。

好了,上次open_driver()這個坑算是補上了。

image

接下來看看ProcessState::getStrongProxyForHandle()函數(shù)留下的坑吧。

接著getStrongProxyForHandle()說

注意啦,從這里開始是山路十八彎,抓好扶好了啊!

先來看一張流程圖。

image

不夠高清?點這個鏈接下載吧!http://ogemdlrap.bkt.clouddn.com/Binder%E8%BF%9B%E9%98%B6%E5%AE%8C%E6%95%B4.png。So Sweet!

圖中相同顏色的流程線表示同一個流程,上面標(biāo)有數(shù)字,你需要按照數(shù)字順序來看,因為這真的是一個復(fù)雜無比的流程!

另外,同一種顏色的雙向箭頭線指向的是同一個變量或者值相同的變量。同理,相同顏色的帶字空心箭頭指向的也是同一個變量或者相同的值。

每個函數(shù)框上部的框表示在我們這個流程中,傳入函數(shù)的參數(shù)。

溫習(xí)一下getStrongProxyForHandle()中的坑

sp<IBinder> ProcessState::getStrongProxyForHandle(int32_t handle)
{
    sp<IBinder> result;
    
    ...
    //嘗試獲取handle對應(yīng)的handle_entry對象,沒有的話會創(chuàng)建一個
    handle_entry* e = lookupHandleLocked(handle);
    
    if (e != NULL) {
        IBinder* b = e->binder;
        if (b == NULL || !e->refs->attemptIncWeak(this)) {
            // 上面的判斷確保了同一個handle不會重復(fù)創(chuàng)建新的BpBinder
            if (handle == 0) {
                Parcel data;
                //在handle對應(yīng)的BpBinder第一次創(chuàng)建時
                //會執(zhí)行一次虛擬的事務(wù)請求,以確保ServiceManager已經(jīng)注冊
                status_t status = IPCThreadState::self()->transact(0, IBinder::PING_TRANSACTION, data, NULL, 0);
                if (status == DEAD_OBJECT)
                    //如果ServiceManager沒有注冊,直接返回
                    return NULL;
            }
            //創(chuàng)建一個BpBinder
            //handle為0時創(chuàng)建的是ServiceManager對應(yīng)的BpBinder
            b = new BpBinder(handle);
            e->binder = b;
            if (b) e->refs = b->getWeakRefs();
            result = b;  //待會兒返回b
        }
        ...
    }
    
    return result;
}

上次CoorChice在getStrongProxyForHandle()函數(shù)中是把下面這段代碼省略了的,為了方便大家關(guān)注流程。

if (handle == 0) {
    Parcel data;
    //在handle對應(yīng)的BpBinder第一次創(chuàng)建時
    //會執(zhí)行一次虛擬的事務(wù)請求,以確保ServiceManager已經(jīng)注冊
    status_t status = IPCThreadState::self()->transact(0, IBinder::PING_TRANSACTION, data, NULL, 0);
    if (status == DEAD_OBJECT)
    //如果ServiceManager沒有注冊,直接返回
    return NULL;
}

由于我們發(fā)起了獲取ServiceManager的Binder的請求,所以handle是0的。還記得嗎?應(yīng)用進程在首次獲?。ɑ蛘哒f創(chuàng)建)ServiceManager的Binder前,會先和ServiceManager進行一次無意義的通訊(可以看到這次通訊的code為PING_TRANSACTION),以確保系統(tǒng)的ServiceManager已經(jīng)注冊。既然是在這第一次見到Binder通訊,那么我們就索性從這開始來探索Binder通訊機制的核心流程吧。

IPCThreadState的創(chuàng)建

IPCThreadState::self()->transact(0, IBinder::PING_TRANSACTION, data, NULL, 0)

這句代碼首先會獲取IPCThreadState單例。這是我在圖中省略了的。

IPCThreadState* IPCThreadState::self()
{
    if (gHaveTLS) {
    restart:
        const pthread_key_t k = gTLS;
        //先檢查有沒有,以確保一個線程只有一個IPCThreadState
        IPCThreadState* st = (IPCThreadState*)pthread_getspecific(k);
        if (st) return st;
        return new IPCThreadState; //沒有就new一個IPCThreadState
    }
    ...
}

很明顯,這段代碼確保了進程中每一線程都只會有一個對應(yīng)IPCThreadState。

接下來看看IPCThreadState的構(gòu)造函數(shù)。

IPCThreadState::IPCThreadState()
  //保存所在進程
: mProcess(ProcessState::self()),
mMyThreadId(androidGetTid()),
mStrictModePolicy(0),
mLastTransactionBinderFlags(0)
{
    pthread_setspecific(gTLS, this);
    clearCaller();
    //用于接收Binder驅(qū)動的數(shù)據(jù),設(shè)置其大小為256
    mIn.setDataCapacity(256);
    //用于向Binder驅(qū)動發(fā)送數(shù)據(jù),同樣設(shè)置其大小為256
    mOut.setDataCapacity(256);
}

CoorChice注釋的地方比較重要哦,想要看懂后面的流程,上面3個注釋的記住哦!

好了,我們的IPCThreadState算是創(chuàng)建出來了。事實上IPCThreadState主要就是封裝了和Binder通訊的邏輯,當(dāng)我們需要進行通訊時,就需要通過它來完成。

image

下面就來看看通訊是怎么開始的。

第一步 IPCThreadState::transact()發(fā)起通訊

你可以先在圖中找到對應(yīng)的流程線。transact()完整代碼的話你可以看圖中的,或者在/frameworks/native/libs/binder/IPCThreadState.cpp看源碼。由于流程復(fù)雜,CoorChice就以小片段來說明。

status_t IPCThreadState::transact(int32_t handle,
                                  uint32_t code, const Parcel& data,
                                  Parcel* reply, uint32_t flags)
{
    flags |= TF_ACCEPT_FDS;   //添加TF_ACCEPT_FDS
        ...
     if (err == NO_ERROR) {
         ...
         //將需要發(fā)送的數(shù)據(jù)寫入mOut中
         err = writeTransactionData(BC_TRANSACTION, flags, handle, code, data, NULL);
     }
    ...
}

首先,在傳入的flags參數(shù)中添加一個TF_ACCEPT_FDS標(biāo)志,表示返回數(shù)據(jù)中可以包含文件描述符。以下是幾個標(biāo)志位的意義:

enum transaction_flags {
    TF_ONE_WAY     = 0x01, /*異步的單向調(diào)用,沒有返回值*/
    TF_ROOT_OBJECT = 0x04, /*里面的數(shù)據(jù)是一個組件的根對象*/
    TF_STATUS_CODE = 0x08, /*數(shù)據(jù)包含的是一個32bit的狀態(tài)碼*/
    TF_ACCEPT_FDS  = 0x10, /*允許返回對象中,包含文件描述符*/
}

接著,會調(diào)用writeTransactionData()函數(shù),把需要發(fā)送的數(shù)據(jù)準(zhǔn)備好。注意這里的命令是BC_TRANSACTION哦。如果你隨時對照著圖查看參數(shù)的話,這個流程將會變的容易理解一些。

第二步 writeTransactionData()準(zhǔn)備發(fā)送數(shù)據(jù)

status_t IPCThreadState::writeTransactionData(int32_t cmd, uint32_t binderFlags,
    int32_t handle, uint32_t code, const Parcel& data, status_t* statusBuffer)
{
    //儲存通訊事務(wù)數(shù)據(jù)的結(jié)構(gòu)
    binder_transaction_data tr;
    tr.target.ptr = 0;  //binder_node的地址
    tr.target.handle = handle;  //用于查找目標(biāo)進程Binder的handle,對應(yīng)binder_ref
    tr.code = code; //表示事務(wù)類型
    tr.flags = binderFlags;
    tr.cookie= 0;
    ...
    //Parcel mOut,與之相反的有Parcel mIn
    //寫入本次通訊的cmd指令
    mOut.writeInt32(cmd);
    //把本次通訊事務(wù)數(shù)據(jù)寫入mOut中
    mOut.write(&tr, sizeof(tr));
    return NO_ERROR;
}

如你所見,這個函數(shù)主要創(chuàng)建了一個用于儲存通訊事務(wù)數(shù)據(jù)的binder_transaction_data結(jié)構(gòu)t,并把需要發(fā)送的事務(wù)數(shù)據(jù)放到其中,然后再把這個tr寫入IPCThreadState的mOut中。這樣一來,后面就可以從mOut中取出這個通訊事務(wù)數(shù)據(jù)結(jié)構(gòu)了。它非常重要,你一定要記住它是什么?以及從那來的?

此外,還需要把本次通訊的命令也寫入mOut中,這樣后面才能獲取到發(fā)送方的命令,然后執(zhí)行相應(yīng)的操作。

第三步 waitForResponse()等待響應(yīng)

第二步完成后,我們再次回到IPCThreadState::transact()函數(shù)中。

status_t IPCThreadState::transact(int32_t handle,
                                  uint32_t code, const Parcel& data,
                                  Parcel* reply, uint32_t flags)
{
    ...
    flags |= TF_ACCEPT_FDS;   //添加TF_ACCEPT_FDS
    ...
    //等待響應(yīng)
    if ((flags & TF_ONE_WAY) == 0) { //檢查本次通訊是否有TF_ONE_WAY標(biāo)志,即沒有響應(yīng)
        //reply是否為空
        if (reply) {
            err = waitForResponse(reply);
        } else {
            Parcel fakeReply;
            err = waitForResponse(&fakeReply);
        }
        ...
    }
    ...
    return err;
}

一般通訊都需要響應(yīng),所以我們就只看有響應(yīng)的情況了,即flags中不包含TF_ONE_WAY標(biāo)記。調(diào)用waitForResponse()函數(shù)時,如果沒有reply,會創(chuàng)建一個fakeReplay。我們回顧一下:

transact(0, IBinder::PING_TRANSACTION, data, NULL, 0)

看,我們上面?zhèn)魅氲膔eplay是一個NULL,所以這里是會創(chuàng)建一個fakeReplay的。

緊接著,我們就進入到IPCThreadState::waitForResponse()中了??梢钥聪聢D中的流程線哦,對應(yīng)紅色編號3的線。

status_t IPCThreadState::waitForResponse(Parcel *reply, status_t *acquireResult)
{
    ...
    while (1) {
        //真正和Binder驅(qū)動交互的是talkWithDriver()函數(shù)
        if ((err=talkWithDriver()) < NO_ERROR) break;
        ...
    }
    ...
}

這個方法中,一開始就有些隱蔽的調(diào)用了一個十分重要的方法IPCThreadState::talkWithDriver(),從名字也能看出來,真正和Binder驅(qū)動talk的邏輯是在這個函數(shù)中的。這個地方給差評!

image

順著代碼,我們進入talkWithDriver()看看用戶空間是如何和Binder驅(qū)動talk的。

第四步 talkWithDriver()和Binder talk!

注意,需要說明一下,IPCThreadState::talkWithDriver()這個函數(shù)的參數(shù)默認(rèn)為true!一定要記住,不然后面就看不懂了!

status_t IPCThreadState::talkWithDriver(bool doReceive)
{
    ...
    //讀寫結(jié)構(gòu)體,它是用戶空間和內(nèi)核空間的信使
    binder_write_read bwr;
    ...
    //配置發(fā)送信息
    bwr.write_size = outAvail;
    bwr.write_buffer = (uintptr_t)mOut.data();
    ...
    //獲取接收信息
    if(doReceive && needRead){
        bwr.read_size = mIn.dataCapacity();
        bwr.read_buffer = (uintptr_t)mIn.data();
    } else {
        bwr.read_size = 0;
        bwr.read_buffer = 0;
    }
    ...
    //設(shè)置消耗為0
    bwr.write_consumed = 0;
    bwr.read_consumed = 0;
    status_t err;
    do {
        ...
        //通過ioctl操作與內(nèi)核進行讀寫
        if (ioctl(mProcess->mDriverFD, BINDER_WRITE_READ, &bwr) >= 0)
            err = NO_ERROR;
        ...
    } while (err == -EINTR);
    ...
}

這個函數(shù)中,有一個重要結(jié)構(gòu)被定義,就是binder_write_read。它能夠儲存一些必要的發(fā)送和接收的通訊信息,它就像用戶空間和內(nèi)核空間之間的一個信使一樣,在兩端傳遞信息。

在這個函數(shù)中,首先會把用戶空間要傳遞/讀取信息放到bwr中,然后通過一句關(guān)鍵的代碼ioctl(mProcess->mDriverFD, BINDER_WRITE_READ, &bwr)與Binder驅(qū)動talk。ioctl()函數(shù)CoorChice已經(jīng)在上一篇中說了,它最終會調(diào)用到Binder內(nèi)核的binder_ioctl()函數(shù),至于為什么?你可以再看看上一篇文章回顧下。

image

注意這里我們給ioctl()函數(shù)傳遞的參數(shù)。

  • 第一個參數(shù),是從本進程中取出上面篇中打開并保存Binder設(shè)備文件描述符,通過它可以獲取到之前生成的file文件結(jié)構(gòu),然后傳給binder_ioctl()函數(shù)。沒印象的同學(xué)先看看上篇回顧下這里。
  • 第二個參數(shù),是命令,它決定了待會到內(nèi)核空間中要執(zhí)行那段邏輯。
  • 第三個參數(shù),我們把剛剛定義的信使bwr的內(nèi)存地址傳到內(nèi)核空間去。

這些參數(shù)是理解后面步驟的關(guān)鍵,不要忘了哦!現(xiàn)在,進入到老盆友binder_ioctl()函數(shù)中,看看收到用戶空間的消息后,它干了什么?

第五步 在binder_ioctl()中處理消息

static long binder_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
    int ret;
    // 從file結(jié)構(gòu)體中取出進程信息
    struct binder_proc *proc = filp->private_data;
    struct binder_thread *thread;
    unsigned int size = _IOC_SIZE(cmd);
    //表明arg是一個用戶空間地址
    //__user標(biāo)記該指針為用戶空間指針,在當(dāng)前空間內(nèi)無意義
    void __user *ubuf = (void __user *)arg;
    ...
    //鎖定同步
    binder_lock(__func__);
    //取出線程信息
    thread = binder_get_thread(proc);
    ...
    switch (cmd) {
        //讀寫數(shù)據(jù)
        case BINDER_WRITE_READ:
            ret = binder_ioctl_write_read(filp, cmd, arg, thread); 
            ...
        }
        ...
    }
    ...
    //解鎖
    binder_unlock(__func__);
    ...
}

這個函數(shù)看過《從getSystemService()開始,開擼Binder通訊機制:http://www.itdecent.cn/p/1050ce12bc1e》的同學(xué)應(yīng)該不會陌生。首先會根據(jù)文件描述符獲得的file結(jié)構(gòu),獲取到調(diào)用ioctl()函數(shù)的進程的進程信息,從而再獲得進程的線程。然后將arg參數(shù)地址轉(zhuǎn)換成有用戶空間標(biāo)記的指針。接著,在switch中根據(jù)cmd參數(shù)判斷需要執(zhí)行什么操作。這些步驟和上篇文章中是一樣的。不同的是,我們這次的cmd命令是BINDER_WRITE_READ,表示要進行讀寫操作。可以看到,接下來的讀寫邏輯是在binder_ioctl_write_read()函數(shù)中的。

嗯,接下來,我們即將進入第6步,看看Binder驅(qū)動中的這段讀寫通訊邏輯是怎樣的?

第六步 binder_ioctl_write_read() talking

static int binder_ioctl_write_read(struct file *filp,
                                   unsigned int cmd, unsigned long arg,
                                   struct binder_thread *thread)
{
    int ret = 0;
    //獲取發(fā)送進程信息
    struct binder_proc *proc = filp->private_data;
    unsigned int size = _IOC_SIZE(cmd);
    //來自用戶空間的參數(shù)地址
    void __user *ubuf = (void __user *)arg;
    //讀寫信息結(jié)構(gòu)體
    struct binder_write_read bwr;
    ...
    //拷貝用戶空間的通訊信息bwr到內(nèi)核的bwr
    if (copy_from_user(&bwr, ubuf, sizeof(bwr)))
    ...
    if (bwr.write_size > 0) {
        //寫數(shù)據(jù)
        ret = binder_thread_write(proc, thread,
                                  bwr.write_buffer,
                                  bwr.write_size,
                                  &bwr.write_consumed);
    ...
}

咱們先看上面這個片段。

首先自然是取出用戶空間的進程信息,然后轉(zhuǎn)換獲得用戶空間的參數(shù)地址(對應(yīng)本次通訊中為bwr的地址),這些都跟在binder_ioctl()中做的差不多。

接下來,你可以看到一個binder_write_read結(jié)構(gòu)的申明struct binder_write_read bwr,緊跟著通過copy_from_user(&bwr, ubuf, sizeof(bwr))把用戶空間的bwr拷貝到了當(dāng)前內(nèi)核空間的bwr。現(xiàn)在,Binder內(nèi)核空間的bwr就獲取到了來自用戶空間的通訊信息了。

獲取到來自用戶空間的信息后,先調(diào)用binder_thread_write()函數(shù)來處理,我看看是如何進行處理的。

第七步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)
{
    uint32_t cmd;
    void __user *buffer = (void __user *)(uintptr_t)binder_buffer;
    void __user *ptr = buffer + *consumed;    //起始地址
    void __user *end = buffer + size;         //結(jié)束地址
    while (ptr < end && thread->return_error == BR_OK) {
        //從用戶空間獲取cmd命令
        if (get_user(cmd, (uint32_t __user *)ptr)) -EFAULT;
        ptr += sizeof(uint32_t);
        switch (cmd) {
            case BC_TRANSACTION:
            case BC_REPLY: {
                //用來儲存通訊信息的結(jié)構(gòu)體
                struct binder_transaction_data tr;
                //拷貝用戶空間的binder_transaction_data
                if (copy_from_user(&tr, ptr, sizeof(tr)))   return -EFAULT;
                ptr += sizeof(tr);
                //處理通訊
                binder_transaction(proc, thread, &tr, cmd == BC_REPLY);
                break;
            }
                ...
        }
        *consumed = ptr - buffer;
    }
    return 0;
}

一開始就是對一些變量進行賦值。

首先,binder_buffer是啥?哪來的?快到到傳參的地方方看看bwr.write_buffer,它是寫的buffer。那么它里面裝了啥?這就得回到第4步中找了,因為bwr是在那個地方定義和初始化的。bwr.write_buffer = (uintptr_t)mOut.data(),嗯,它指向了mOut中的數(shù)據(jù)。那么問題又來了?mOut中的數(shù)據(jù)是啥?...

image

看,這就是為什么CoorChice一直在強調(diào),前面的一些參數(shù)和變量一定要記??!不然到后面就會云里霧里的!不過還好,有了CoorChcie上面那張圖,你隨時可以快速的找到答案。我們回到第二步writeTransactionData(),就是通訊事務(wù)結(jié)構(gòu)定義的那個地方??吹?jīng)],mOut中儲存的就是一個通訊事務(wù)結(jié)構(gòu)。

現(xiàn)在答案就明了了,buffer指向了用戶空間的通訊事務(wù)數(shù)據(jù)。

另外兩個參數(shù),ptr此刻和buffer的值是一樣的,因為consumed為0,所以它現(xiàn)在也相當(dāng)于是用戶空間的通訊事務(wù)數(shù)據(jù)tr的指針;而end可以明顯的看出,它指向了tr的末尾。

通過get_user(cmd, (uint32_t __user *)ptr)函數(shù),我們可以將用戶空間的tr的cmd拷貝到內(nèi)核空間。get_user()put_user()這對函數(shù)就是干這個的,拷貝一些簡單的變量?;氐降?步writeTransactionData()中,看看參數(shù)。沒錯,cmd為BC_TRANSACTION。所以,進到switch中,對應(yīng)執(zhí)行的就是case BC_TRANSACTION。

可以看到BC_TRANSACTION事務(wù)命令和BC_REPLAY響應(yīng)命令,執(zhí)行的是相同的邏輯。

//用來儲存通訊信息的結(jié)構(gòu)體
struct binder_transaction_data tr;
//拷貝用戶空間的binder_transaction_data
if (copy_from_user(&tr, ptr, sizeof(tr)))   return -EFAU
ptr += sizeof(tr);
//處理通訊
binder_transaction(proc, thread, &tr, cmd == BC_REPLY);

先定義了一個內(nèi)核空間的通訊事務(wù)數(shù)據(jù)tr,然后把用戶空間的通訊事務(wù)數(shù)據(jù)拷貝到內(nèi)核中tr。此時,ptr指針移動sizeof(tr)個單位,現(xiàn)在ptr應(yīng)該和end的值是一樣的了。然后,調(diào)用binder_transaction()來處理事務(wù)。

第八步 binder_transaction()來處理事務(wù)

順著流程線8看過去,WTF!這是一個復(fù)雜無比的函數(shù)!很多!很長!

函數(shù)一開始定義了一堆變量,我們先不管,用到時再說。先看第一個使用到的參數(shù)replay。由于上一步中的cmd為BC_TRANSACTION,所以很明顯,走的是false,所以我們直接看false里的邏輯。

static void binder_transaction(struct binder_proc *proc,
                               struct binder_thread *thread,
                               struct binder_transaction_data *tr, int reply){
    ...
    if (reply) {
        ...
    } else {
        if (tr->target.handle) {
            //如果參數(shù)事物信息中的進程的句柄不為0,即不是系統(tǒng)ServiceManager進程
            
            //定義binder引用
            struct binder_ref *ref;
            //根據(jù)參數(shù)binder進程和句柄handle來查找對應(yīng)的binder
            ref = binder_get_ref(proc, tr->target.handle);
            ...
            //設(shè)置目標(biāo)binder實體為上面找到的參數(shù)進程的binder引用的binder實體
            target_node = ref->node;
        } else {
            //如果參數(shù)事物信息中的進程的句柄為0,即是系統(tǒng)ServiceManager進程
            
            //設(shè)置通訊目標(biāo)進程的Binder實體為ServiceManager對應(yīng)的Binder
            target_node = binder_context_mgr_node;
        }
        //設(shè)置通訊目標(biāo)進程為target_node對應(yīng)的進程
        target_proc = target_node->proc;
        ...
}

一開始先判斷tr->target.handle為不為0。還記得上一篇說的嗎?handle為0表示的是ServiceManager,如果不為0表示的其它Service。那么這里為不為0呢?看圖!

我們順著指向tr的綠線一直找,可以看到。在用戶空間通訊事務(wù)數(shù)據(jù)被定義的地方,也就是第2步IPCThreadState::writeTransactionData()中,給tr->target_handle賦值了,往上看發(fā)現(xiàn),這個值來自IPCThreadState::transact()函數(shù)的參數(shù)handle。那么回到我們一開始調(diào)用這個函數(shù)的地方IPCThreadState::self()->transact(0, IBinder::PING_TRANSACTION, data, NULL, 0)。哦,handle為0。所以這里的target就是ServiceManager。那么直接把binder_context_mgr_node(它表示ServiceManager的Binder,在ServiceManager注冊的時候被緩存到了Binder內(nèi)核中)賦值給target_node,記住了哦!后面這些都會用到??傊?,我們就是需要先獲取到一個目標(biāo)進程。

接下來,通過target_node,也就是ServiceManager進程的Binder(其它情況就是對應(yīng)進程的Binder),我們可以獲取到目標(biāo)進程信息,然后賦值給target_proc。記住了哦!

繼續(xù)下一段代碼。

static void binder_transaction(struct binder_proc *proc,
                               struct binder_thread *thread,
                               struct binder_transaction_data *tr, int reply){
    ...
    //判斷目標(biāo)線程是否為空
    if (target_thread) {
        ...
        //目標(biāo)線程的todo隊列
        target_list = &target_thread->todo;
        target_wait = &target_thread->wait;
        ...
    } else {
        //獲得通訊目標(biāo)進程的任務(wù)隊列
        target_list = &target_proc->todo;
        //獲取通訊目標(biāo)進程的等待對象
        target_wait = &target_proc->wait;
    }
    ...
}

首先看target_thread是否為空,由于我們沒有走if(replay)的TRUE邏輯,所以這里target_thread是為空的。那么,從目標(biāo)進程信息target_proc分別去除todo任務(wù)隊列和wait對象,賦值給target_listtarget_wait。同樣需要記?。?/p>

繼續(xù)下一段代碼。

static void binder_transaction(struct binder_proc *proc,
                               struct binder_thread *thread,
                               struct binder_transaction_data *tr, int reply){
                               
   struct binder_transaction *t; //表示一個binder通訊事務(wù)
   struct binder_work *tcomplete; //表示一項work
   ...
   struct list_head *target_list;  //通訊目標(biāo)進程的事務(wù)隊列
   wait_queue_head_t *target_wait; //通訊目標(biāo)進程的等待對象
    ...
    //為本次通訊事務(wù)t申請空間
    t = kzalloc(sizeof(*t), GFP_KERNEL);
    ...
    tcomplete = kzalloc(sizeof(*tcomplete), GFP_KERNEL);
    ...
    if (!reply && !(tr->flags & TF_ONE_WAY))
        //采用非one way通訊方式,即需要等待服務(wù)端返回結(jié)果的通訊方式
        
        //設(shè)置本次通訊事務(wù)t的發(fā)送線程為用戶空間的線程
        t->from = thread;
    else
        t->from = NULL;
    ...
    //設(shè)置本次通訊事務(wù)的接收進程為目標(biāo)進程
    t->to_proc = target_proc;
    //設(shè)置本次通訊事務(wù)的接收線程為目標(biāo)線程
    t->to_thread = target_thread;
    //設(shè)置本次通訊事務(wù)的命令為用戶空間傳來的命令
    t->code = tr->code;
    //設(shè)置本次通訊事務(wù)的命令為用戶空間傳來的flags
    t->flags = tr->flags;
    ...
    //開始配置本次通訊的buffer
    //在目標(biāo)進程中分配進行本次通訊的buffer的空間
    t->buffer = binder_alloc_buf(target_proc, tr->data_size,
                                 tr->offsets_size, !reply && (t->flags & TF_ONE_WAY));
    
    t->buffer->allow_user_free = 0; //通訊buffer允許釋放
    t->buffer->transaction = t;  //把本次通訊存入buffer中
    //設(shè)置本次通訊的buffer的目標(biāo)Binder實體為target_node
    //如前面一樣,通過這個buffer可以找到對應(yīng)的進程
    t->buffer->target_node = target_node;
    ...
    offp = (binder_size_t *)(t->buffer->data + ALIGN(tr->data_size, sizeof(void *)));
    //將用戶空間發(fā)送來的數(shù)據(jù)拷貝到本次通訊的buffer的data中
    copy_from_user(t->buffer->data, (const void __user *)(uintptr_t)tr->data.ptr.buffer, tr->data_size);
    ...
    //將用戶空間發(fā)送來的偏移量offsets拷貝給起始o(jì)ffp
    copy_from_user(offp, (const void __user *)(uintptr_t)tr->data.ptr.offsets, tr->offsets_size);
    ...
    //計算結(jié)尾off_end
    off_end = (void *)offp + tr->offsets_size;
    ...
    //判斷是否是BC_REPLY
    if (reply) {
        ...
        
        binder_pop_transaction(target_thread, in_reply_to);
    } else if (!(t->flags & TF_ONE_WAY)) {
        //如果沒有ONE_WAY標(biāo)記,即需要等待響應(yīng)
        t->need_reply = 1;  //1標(biāo)示這是一個同步事務(wù),需要等待對方回復(fù)。0表示這是一個異步事務(wù),不用等對方回復(fù)
        //設(shè)置本次通訊事務(wù)的from_parent為發(fā)送方進程的事務(wù)
        t->from_parent = thread->transaction_stack;
        //設(shè)置發(fā)送方進程的事務(wù)棧為本次通訊事務(wù)
        thread->transaction_stack = t;
    }
    ...
    
    //將本次通訊事務(wù)的work類型設(shè)置為BINDER_WORK_TRANSACTION
    t->work.type = BINDER_WORK_TRANSACTION;
    //將本次通訊事務(wù)的work添加到目標(biāo)進程的事務(wù)列表中
    list_add_tail(&t->work.entry, target_list);
    
    //設(shè)置work類型為BINDER_WORK_TRANSACTION_COMPLETE
    tcomplete->type = BINDER_WORK_TRANSACTION_COMPLETE;
    //將BINDER_WORK_TRANSACTION_COMPLETE類型的work添加到發(fā)送方的事務(wù)列表中
    list_add_tail(&tcomplete->entry, &thread->todo);
    
    if (target_wait)
        //喚醒目標(biāo)進程,開始執(zhí)行目標(biāo)進程的事務(wù)棧
        wake_up_interruptible(target_wait);
    return;
}

在開始分析之前,大家先吧這段代碼開始的幾個變量定義記住,不然后面會很迷茫的!

這段代碼很多!很長!CoorChice已經(jīng)盡量的刪去一些沒那么重要的和我不知道是干啥的了?。?/p>

image

其實這么多代碼,主要使用在給binder事務(wù)t的成員賦值了。我們簡單看幾個我認(rèn)為重要的賦值。

首先為事務(wù)t和work tcomplete申請了內(nèi)存。然后設(shè)置事務(wù)t的from線程(也就是發(fā)送方線程)的值,如果不是BC_REPLAY事務(wù),并且通訊標(biāo)記沒有TF_ONE_WAY(即本次通訊需要有響應(yīng)),那么把參數(shù)thread賦值給t->from。前面說過,我們本次通訊是BC_TRANSACTION事務(wù),所以事務(wù)t就需要儲存發(fā)送方的線程信息,以便后面給發(fā)送方響應(yīng)使用。

參數(shù)thread是那來的呢?順著往回找,在第5步binder_ioctl()中,我們從用戶空間調(diào)用ioctl()函數(shù)的進程(即發(fā)送方進程)的進程信息中獲取到了thread。

接著設(shè)置事務(wù)t的目標(biāo)進程t->to_proc和目標(biāo)進程的線程t->to_thread為前面處理好的target_proctarget_thread(本次通訊,target_thread為空哦)。

然后把通訊事務(wù)數(shù)據(jù)tr中的code和flags賦值給事務(wù)t的code和flags。code和flags是什么呢?我們回到用戶空間,定義通訊事務(wù)數(shù)據(jù),即第2步IPCThreadState::writeTransaction()中可以看到,code和flags均是傳進來的參數(shù)。而的發(fā)源地是通訊的起始點IPCThreadState::self()->transact(0, IBinder::PING_TRANSACTION, data, NULL, 0),即code = IBinder::PING_TRANSACTION, flags = 0。記住了哦!后面還會用。

然后開始設(shè)置事務(wù)t的buffer信息。首先通過binder_alloc_buf()函數(shù),在目標(biāo)進程target_proc中為t->buffer申請了內(nèi)存,即t->buffer指向了目標(biāo)進程空間中的一段內(nèi)存。然后配置一下t->buffer的信息,這些信息后面也會用到。記住了哦!

接著通過copy_from_user()函數(shù),把用戶空間的需要發(fā)送的數(shù)據(jù)拷貝到t->buffer的data中。

再往下到了if(replay),本次通訊會走false邏輯。于是,事務(wù)t會把發(fā)送方的事務(wù)棧transaction_stack儲存在from_parent中,而發(fā)送方把自己的事務(wù)棧設(shè)置以成t開始。這些都需要記住,不然再往后你就會越來越迷糊!

最重要的部分來了!

//將本次通訊事務(wù)的work類型設(shè)置為BINDER_WORK_TRANSACTION
t->work.type = BINDER_WORK_TRANSACTION;
//將本次通訊事務(wù)的work添加到目標(biāo)進程的事務(wù)列表中
list_add_tail(&t->work.entry, target_list);
    
//設(shè)置work類型為BINDER_WORK_TRANSACTION_COMPLETE
tcomplete->type = BINDER_WORK_TRANSACTION_COMPLETE;
將BINDER_WORK_TRANSACTION_COMPLETE類型的work添加到發(fā)送方的事務(wù)列表中
list_add_tail(&tcomplete->entry, &thread->todo);

if (target_wait)
    //喚醒目標(biāo)進程,開始執(zhí)行目標(biāo)進程的事務(wù)棧
    wake_up_interruptible(target_wait);
return;

先把事務(wù)t的work.type類型設(shè)置為BINDER_WORK_TRANSACTION類型,這決定了該事務(wù)后面走的流程,然后把事務(wù)t的任務(wù)添加到目標(biāo)進程的任務(wù)棧target_list中。接著把work tcomplete的類型設(shè)置為BINDER_WORK_TRANSACTION_COMPLETE,用于告訴發(fā)送方,和Binder驅(qū)動的一次talk完成了,同樣,需要把這個項任務(wù)添加到發(fā)送方的任務(wù)列表里。

最后,通過wake_up_interruptible(target_wait)函數(shù)喚醒休眠中的目標(biāo)進程,讓它開始處理任務(wù)棧中的任務(wù),也就是剛剛我們添加到target_list中的任務(wù)。接著return結(jié)束該函數(shù)。

結(jié)束這個函數(shù)你以為就忘啦?Native!接著往下看。

第9步 binder_thread_read()讀取數(shù)據(jù)

上一個函數(shù)結(jié)束后回到第7步binder_thread_write()函數(shù)中,retrun 0;,binder_thread_write()函數(shù)結(jié)束。然后回到第6步binder_ioctl_write_read()函數(shù)中繼續(xù)執(zhí)行。

static int binder_ioctl_write_read(struct file *filp,
                                   unsigned int cmd, unsigned long arg,
                                   struct binder_thread *thread)
{
    ...
    if (bwr.read_size > 0) {
        //讀數(shù)據(jù)
        ret = binder_thread_read(proc, thread, bwr.read_buffer,
                                 bwr.read_size,
                                 &bwr.read_consumed,
                                 filp->f_flags & O_NONBLOCK);
        ...
}

Binder驅(qū)動會調(diào)用binder_thread_read()函數(shù),為發(fā)送進程讀取數(shù)據(jù)。我們看看是怎么讀取的。

static int binder_thread_read(struct binder_proc *proc,
                              struct binder_thread *thread,
                              binder_uintptr_t binder_buffer,
                              size_t size,
                              binder_size_t *consumed,
                              int non_block)
{
    ...
    while (1) {
        uint32_t cmd;
        struct binder_transaction_data tr;
        struct binder_work *w;
        struct binder_transaction *t = NULL;
        if (!list_empty(&thread->todo)) {
            //獲取線程的work隊列
            w = list_first_entry(&thread->todo, struct binder_work, entry);
        } else if (!list_empty(&proc->todo) && wait_for_proc_work) {
            //獲取從進程獲取work隊列
            w = list_first_entry(&proc->todo, struct binder_work, entry);
        } else {
            //沒有數(shù)據(jù),則返回retry
            if (ptr - buffer == 4 &&
                !(thread->looper & BINDER_LOOPER_STATE_NEED_RETURN))
                goto retry;
            break;
        }
    ...
}

我們先看這個片段,前面一堆代碼掠過了。首先,需要看看能不能從發(fā)送進程的線程thread的任務(wù)棧中取出任務(wù)來,回顧第8步binder_transaction()中,我們在最后往發(fā)送進程的線程thread的任務(wù)棧中添加了一個BINDER_WORK_TRANSACTION_COMPLETE類型的work。所以這里是能取到任務(wù)的,就直接執(zhí)行下一步了。

static int binder_thread_read(struct binder_proc *proc,
                              struct binder_thread *thread,
                              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; //
    void __user *end = buffer + size; //用戶空間結(jié)束
    ...
    while (1) {
        uint32_t cmd;
        struct binder_transaction_data tr;
        struct binder_work *w;
        ...
        switch (w->type) {
            ...
            case BINDER_WORK_TRANSACTION_COMPLETE:
                //設(shè)置cmd為BR_TRANSACTION_COMPLETE
                cmd = BR_TRANSACTION_COMPLETE;
                //將BR_TRANSACTION_COMPLETE寫入用戶進程空間的mIn中
                put_user(cmd, (uint32_t __user *)ptr);
                //從事務(wù)隊列中刪除本次work
                list_del(&w->entry);
                //釋放
                kfree(w);
                break;
            ...
        }
    }
    ...
}

由于這個流程中,我們知道work的type為BINDER_WORK_TRANSACTION_COMPLETE類型,所以就先只看這種情況了。在這段代碼中,cmd = BR_TRANSACTION_COMPLETE很重要,要記住!接著把cmd拷貝到用戶空間的發(fā)送進程,然后刪除任務(wù),釋放內(nèi)存。

一次和Binder驅(qū)動的通訊完成!

上面代碼執(zhí)行完后,binder_thread_read()函數(shù)差不多就結(jié)束了,接著又會回到binder_ioctl_write_read()函數(shù)。

static int binder_ioctl_write_read(struct file *filp,
                                   unsigned int cmd, unsigned long arg,
                                   struct binder_thread *thread)
{
    ...
    //將內(nèi)核的信使bwr拷貝到用戶空間
    if (copy_to_user(ubuf, &bwr, sizeof(bwr)))
    ...
}

上面函數(shù)最后會把內(nèi)核中的信使拷貝到用戶空間。

然后,我們直接的再次的回到第3步的函數(shù)IPCThreadState::waitForResponse()中。

status_t IPCThreadState::waitForResponse(Parcel *reply, status_t *acquireResult)
{
    ...
    while (1) {
        //真正和Binder驅(qū)動交互的是talkWithDriver()函數(shù)
        if ((err=talkWithDriver()) < NO_ERROR) break;
        err = mIn.errorCheck();
        ...
        if (mIn.dataAvail() == 0) continue;
        //取出在內(nèi)核中寫進去的cmd命令
        cmd = mIn.readInt32();
        ...
        
        switch (cmd) {
            //表示和內(nèi)核的一次通訊完成
            case BR_TRANSACTION_COMPLETE:
                if (!reply && !acquireResult) goto finish;
                break;
            ...
        }
        
    }
    ...
}

經(jīng)過剛剛的讀取,這次mIn中可是有數(shù)據(jù)了哦!我們從mIn中取出cmd命令。這是什么命令呢?就是剛剛寫到用戶空間的BR_TRANSACTION_COMPLETE。在這段邏輯中,由于之前我們傳入了一個fakeReplay進來,所以程序走bredk,然后繼續(xù)循環(huán),執(zhí)行下一次talkWithDriver()函數(shù)。到此,我們和Binder內(nèi)核的一次通訊算是完成了。

但是我們發(fā)起的這次通訊還沒有得到回應(yīng)哦!猜猜看回應(yīng)的流程是怎樣的呀?

image

文章太長了,回應(yīng)流程放到下一篇了。

總結(jié)

  • 抽出空余時間寫文章分享需要動力,還請各位看官動動小手點個贊,給我點鼓勵??
  • 我一直在不定期的創(chuàng)作新的干貨,想要上車只需進到我的【個人主頁】點個關(guān)注就好了哦。發(fā)車嘍~

本篇CoorChice填了上篇文章中的一些坑,并借此跑通了一遍客戶端和Binder驅(qū)動通訊的流程。這是個很復(fù)雜的過程,大家看著圖走一遍,再思考思考?;剡^頭來一想,其實也沒那么難了。

俗話說會者不難, 難者不會,大概就是這樣吧。

功力有限,有錯還請指出一起交流交流。

看到這里的童鞋快獎勵自己一口辣條吧!

想要看CoorChice的更多文章,請點個關(guān)注哦!

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

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

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