mmc驅動的讀寫過程解析
mmc io的讀寫從mmc_queue_thread()的獲取queue裡面的request開始。
先列出呼叫棧,看下大概的呼叫順序, 下面的內容主要闡述這些函式如何工作。
host->ops->request() // sdhci_request()
mmc_start_request()
mmc_start_req()
mmc_blk_issue_rw_rq()
mmc_blk_issue_rq()
Mmc_queue_thread()
mmc_queue_thread() struct request *req = NULL; 用來提取req
req = blk_fetch_request(q); 從塊裝置佇列提取儲存的req。儲存到這次處理mqrq_cur裡面mq->mqrq_cur->req = req; blk_fetch_request()可以多次呼叫,如果queue裡面沒有內容,req將返回NULL。
接下來呼叫mq->issue_fn()對req進行處理
處理完畢後把mq->mqrq_prev = mq->mqrq_cur, 然後清空mq->mqrq_cur。
倘若req || mq->mqrq_prev->req 這次獲取的req和上次的req都為NULL的話,執行緒進入睡眠狀態kthread_should_stop() –> schedule();
mmc_blk_issue_rq()
- if (req && !mq->mqrq_prev->req) 如果是第一次命令mmc_claim_host(card->host); 需要佔住host,啟用時鐘。
- ret = mmc_blk_part_switch(card, md); 選擇對應的分割槽。
- 根據req->cmd_flags的命令做不同的事情。REQ_SANITIZE、REQ_DISCARD、REQ_FLUSH
- 關注結構體host->context_info
mmc_blk_issue_rw_rq() 開始讀寫
- Req引數變換名稱struct request *rqc
- 如果req有值,則進入一個關鍵的函式mmc_blk_rw_rq_prep(mq->mqrq_cur, card, 0, mq);做一些準備工作。然後areq = &mq->mqrq_cur->mmc_active; 取到非同步request結構體 areq (struct mmc_async_req)。
- 正式啟動areq = mmc_start_req(card->host, areq, (int *) &status);
- 命令完成之後,對命令的完成狀態做各種判斷,是否正確完成,是否出錯,是否需要retry。
- 有幾個部分用於獲取執行狀態。mq_rq從areq反向抽取得到, brq = &mq_rq->brq; req = mq_rq->req; status變數。 通過switch case來判斷status返回的是什麼狀態,決定接下來如何做。可以mmc_blk_reinsert_req()重新把req放回queue裡面。可以 blk_end_request (req, 0, brq->data.bytes_xfered); 完成本次傳輸,說明資料已正確讀寫。該函式本質是req->end_io(req, error); 有上層request queue的時候註冊的回撥。一般可能是做unlock buffer或page的動作。 如果是MMC_BLK_CMD_ERR,則mmc_blk_reset()把控制器都reset一遍,要做重新上下電的動作。如果是retry MMC_BLK_RETRY,則迴圈體重試五次。如果是MMC_BLK_DATA_ERR 也要reset控制器。如果是MMC_BLK_ECC_ERR, 並且發現是多塊讀,則切換到單塊讀,如果還是失敗,沒辦法blk_end_request(req, -EIO, 給上層直接EIO的錯誤。如果是MMC_BLK_NOMEDIUM沒裝置了,直接退出。
- 剩餘的都是為了命令出錯處理,或者重試,start_new_req()。
mmc_blk_rw_rq_prep()
- 光從函式名就可以看出這是一個prepare的函式。注意這裡面的幾個結構體struct mmc_blk_request struct mmc_request。 brq = &mqrq->brq; brq->mrq.cmd = &brq->cmd; brq->mrq.data = &brq->data; 將來這個mrq將是承載命令傳送的結構。
- 整個函式的宗旨就是填充各種結構體,用正確的值,譬如cmd號,是讀還是寫,是單塊還是多塊。MMC_READ_MULTIPLE_BLOCK MMC_READ_SINGLE_BLOCK MMC_WRITE_BLOCK MMC_WRITE_MULTIPLE_BLOCK。也包含一些特殊情況需要做的事情,譬如特殊命令。
- 另一個該函式重要的工作,是mmc_queue_map_sg。要把request裡面包含的資料buffer指標,給map到data.sg結構裡面。struct scatterlist *sg; 結構是分散聚攏DMA的描述,從這點可以看出mmc host的處理是通過dma來完成,sgdma的好處是,它可以處理非連續的多個命令,而不需要cpu干擾。Cpu只需要填充好命令,剩下的事情交個dma處理即可。簡單說就是,普通dma可以處理單個命令,sgdma可以在一次dma裡面處理一組命令。
- mqrq->mmc_active.mrq = &brq->mrq; mqrq->mmc_active.cmd_flags = req->cmd_flags; 之後完成退出。
Mmc_start_req()
- 該函式的寫法有點饒,首先是看得出來,mmc_start_req()的目的其實是要處理本次areq。 所以一開始對areq做mmc_pre_req(host, areq->mrq, !host->areq); 預準備。
- 但接下來是一個if (host->areq) { err = mmc_wait_for_data_req_done (host, host->areq->mrq, 一個很明顯的等待操作,從函式名就可以得知這是一個同步等待,會放棄處理器等待命令完成。但從上文可以看到,一直還未正在處理命令,也沒往host傳送命令,此時就開始等待命令完成顯然是毫無道理。但有時候程式碼容易看漏,該等待是針對的host->areq,並不是引數傳遞的areq。本次等待的是上一次傳遞未完成的動作。如果上次的傳輸以及完成,則該等待函式會很快返回。並把成功還是錯誤的情況反饋給外面的mmc_blk_issue_rw_rq()
- 接下來的if (!err && areq) { 才是真正本次的處理如果不是urgent事件的話,start_err = __mmc_start_data_req(host, areq->mrq); 開始。
- 完成之後對應的做一個mmc_post_req(host, areq->mrq, -EINVAL); 和之前的mmc_pre_req(host, areq->mrq, !host->areq); 對應起來。試想一下,如果想在命令前或命令後做自定義的事情,則可以考慮在這裡新增。
- 如果這其中沒發生錯誤,host->areq = areq; 就儲存起來了,即current操作變成pre操作。並且這裡面不需要在做等待,因為等待的操作將在下次函式在進來時的第2步進行。可以看出設計者為了最大化資料吞吐量,把函式設計成最大限度的流水線處理,壓縮所有可能的耗時操作。試想如果不這麼做,則每次操作都需要完成準備,等待,準備,等待的迴圈。函式設計成這樣,則把同步等待的時間利用起來,做另一次傳輸的準備,減少無謂的頻寬損失。
- 把引數state賦值為函式返回值err,返回上一次的傳輸結果,host->areq ,為什麼?因為本次的傳輸肯定還未完成,需要等待硬體處理,但上次的host->areq已經完成,可以處理後續事情。這就是為什麼mmc_blk_issue_rw_rq()在發起命令後返回需要mq_rq = container_of(areq, struct mmc_queue_req, mmc_active);用這樣的方式獲得 mmc_queue_req。
mmc_start_request ()
- 第一件事情,我們觀察傳遞的引數,是areq->mrq。可以知道mmc最終命令的承載都是用struct mmc_request *mrq 這樣的結構完成。
- 在呼叫mmc_start_request()前,mrq->done=mmc_wait_data_done就確定了,是request完成之後的回撥函式。
- 函式開始就不斷的對mrq->cmd和mrq->data結構做判斷,mmc_start_request其實是個通用函式,我們知道mmc命令有些是單命令,有些是命令資料合併型,對於有資料傳輸要求的命令,要對mmc->data結構錯誤判斷。
- 如無意外的話,mmc_start_request要交給各個host完成處理了。Mmc驅動是一個通用框架驅動,不同的host對應的命令處理必定有所差別。針對sdhci標準的host mmc驅動。host->ops->request(host, mrq);的執行將交給,sdhci_request()完成。
sdhci_request
- 注意函式一進來,host結構體發生變化,已經不再是mmc_host結構,而是各具體的廠商的host,如這裡的struct sdhci_host *host; 其實是host = mmc_priv(mmc);這麼的得來的。
- host->mrq = mrq; 儲存起mrq結構。函式有不少對sdhci host暫存器的讀寫,此時開始真正與硬體裝置打交道,即準備把控制資訊交託給我們的mmc host控制器。
- 之後的sdhci_send_command(host, mrq->cmd); 控制host啟動命令。
- 最後的mmiowb();是為了保證編譯器順序編譯,防止編譯器優化打亂執行順序。
sdhci_send_command()
- 該函式還值得推敲,從上文看出,request裡面的buffer資料被放在mrq->data->sg裡面存好了,僅僅是存在程式碼結構體裡面,和真正的DMA還沒建立聯絡,此時說命令傳送出去,必定不夠合理。所以DMA的初始化必不可少。
- 前面的也主要做出錯檢查工作,把host->cmd = cmd;命令儲存起來。
- sdhci_prepare_data(host, cmd); 看這個函式名,準備資料,就知道個大概了。裡面的關鍵函式sdhci_pre_dma_transfer()就是準備DMA,dma_map_sg(),從data->sg裡面獲取到資訊,填充到DMA控制器裡面。
- 資料都準備好之後,sdhci_writew(host, SDHCI_MAKE_CMD(cmd->opcode, flags), SDHCI_COMMAND); 來個終極的,資料傳送,這才是真正的控制host傳送命令的操作,到這類,mmc控制器才開始跟sd卡做互動。
命令等待
- 前文說道,命令傳送之後是在Mmc_start_req()的第二步mmc_wait_for_data_req_done()裡面做等待。mmc_wait_for_data_req_done函式大量用到了host->context_info的結構體。context_info->wait是等待queue的標誌,__add_wait_queue(q, wait); 再io_schedule()出去。從此mmcqd執行緒將交換出去,知道有人喚醒wait queue。
- 如何能啟用等待佇列呢?還記得mmc_start_request()的第2步,mmc_wait_data_done回撥,wake_up_interruptible(&mrq->host->context_info.wait);wakeup這個 context_info.wait wait queue。說明命令結束之後,會有人呼叫該回調來喚醒mmcqd執行緒。
- 在哪裡呼叫回撥?既然mmc命令是有sdhci host啟動傳送,必定mrq->done這個回撥也要在sdhci host階段完成。而這個正是由sdhci host的irq中斷來完成的。想想也合理,執行緒啟動命令之後,由host控制器完成命令,然後觸發中斷通知cpu事情完成,中斷處理裡面啟動回撥函式,喚醒mmcqd執行緒。
- sdhci_irq就是上步我們說的中斷處理函式。根據中斷型別的不同,分為sdhci_cmd_irq()處理和sdhci_data_irq()處理。所以可以看出,命令處理中斷和資料處理中斷是不同的,一條既有命令又有資料的mmc cmd,會至少啟用2次中斷,1次給命令,1次給資料。
- done回撥的地方在tasklet_schedule(&host->finish_tasklet);的finish_tasklet裡面,中斷完成上部處理之後,啟動finish_tasklet完成後面的事情。finish_tasklet的定義是sdhci_tasklet_finish。 mmc_request_done() -> mrq->done(mrq);
並不是所有的mmc命令都是讀寫命令,那其他的命令該如何完成呢,他們與mmc的讀寫命令有什麼差別。我們用mmc的CMD8 SEND_IF_COND作為例子,mmc_send_if_cond()是傳送CMD8的函式。
- 函式很簡單,進來就初始化一個區域性變數struct mmc_command cmd。填好命令CMD8,給定返回的RSP引數值,無需初始化cmd->data,因為CMD8沒有資料階段。直接通過mmc_wait_for_cmd() 傳送出去。
- mmc_wait_for_cmd()裡面建立mrq結構變數,之前說過mrq變數的意義, mrq.cmd = cmd; cmd->data = NULL; mmc_wait_for_req(host, &mrq);
- __mmc_start_req() 啟動 mmc_start_request() 這基本跟讀寫命令的流程就一致了。
- mmc_wait_for_req_done() 等待wait_for_completion_io(&mrq->completion); 看得出來這裡面和讀寫流程的不同,在本次傳輸啟動後,立刻同步等待中斷到來。因為單次的CMD8命令並沒有其他的迴圈處理,因此如果不再本次處理等待,將來也沒有機會再進入同步等待階段。
- 本次的wait等待是mrq->completion,和讀寫命令的也有所不同。仔細看__mmc_start_req() mrq->done = mmc_wait_done; 而讀寫的是mrq->done=mmc_wait_data_done。剩下的事情就是返回處理結果。
- 對於又有命令又有資料的單次命令,譬如mmc_send_cxd_data(). mrq.data也需要賦值,我們知道讀寫命令裡面,需要初始化data->sg變數。這裡也不例外,data->sg的初始化由sg_init_one(&sg, data_buf, len);完成,看函式名就知道,這是一個初始化單一資料處理的dma。只需要傳輸一次,大部分是做讀取用。