這樣理解mmap,挺有意思!
大概雍正皇帝怎么也不會想到,自己在西歷2022年的男生和女生眼里,會是截然不同的兩種形象。
1
以我對身邊同學朋友的觀察,男生們大多愛看《雍正王朝》,他們眼中的雍正,大約是個推行了“火耗歸公”、“攤丁入畝”等遏制貪腐,減輕稅收之類政策的改革家,是個經歷了九子奪嫡的驚心動魄、腹黑深沉的政治家,是個登基后也兢兢業業,熬夜加班996的工作狂。
而女生們大多愛看《甄嬛傳》,她們眼里的雍正,是“大胖橘”,是“大豬蹄子”,是被后宮一眾妃嬪玩弄于股掌之中,戴了N頂綠帽,最后還被鈕鈷祿甄嬛氣死的渣男。
我沒完整的看過甄嬛傳,但是有幸在吃飯的時候陪我家那位看過幾集。
正所謂后宮佳麗三千人,鐵杵磨成繡花針... (不是妃嬪太多了,皇帝就一個,難免會互相爭風吃醋。位份高的貴妃的仗勢欺人,一些小妃嬪無法正面回擊,自然會用點別的奇淫技巧,扎小人就是其中一個出現頻率較高的方法。
據我總結,扎小人這個技術的核心思想是:用戶這邊由于無法扎到正主,只能拿個自己身邊的布片等物品模擬一個小人出來,在上面畫上正主的經筋脈絡,寫上名字,施以某種魔法,然后用針扎自己手邊的小人的某個穴道,遠程那位正主的對應部位就會受到同樣的折磨。
雖然有點神乎其技,令人羨慕而不可得。但是在linux內核開發里面,卻可以用mmap的機制實現類似的效果。
2
mmap的核心思想是:用戶這邊由于在用戶態無法直接操作寄存器的物理地址,于是通過mmap方法進行內存映射,將物理地址映射到用戶態的虛擬地址上,然后用戶通過讀寫自己手邊的虛擬地址,就可以實現對物理地址的讀取/寫入。
兩者的共同點是,由于無法直接操作目標,所以通過某種方法,將自己能操作的事物和目標建立一種映射關系,從而達到如臂使指,指哪打哪,打哪哪疼的效果。
只要能建立起對目標的映射,我們借此映射能做什么文章,自然有很多想象空間。所以mmap有很多用途,有人用它來實現進程間通信,有人用它搬運數據,對于我們嵌入式工程師來說,我們可以用它來點燈。嘿嘿,想不到吧!
且聽我慢慢道來。
3
作為一個嵌入式工程師,花式點燈是必備技能。無論是寫裸機代碼操作GPIO口,還是通過物聯網云端遠程控制LED,從硬件的角度講,核心原理都是找到連接LED的GPIO口,讓它輸出一個電信號。而從軟件的角度講,最終目的就是找到這個GPIO口對應寄存器的地址,根據實際的電路要求,讓CPU給它寫入一個1或者0。
裸機開發的時候,我們可以直接找到物理地址進行操作。而在Linux系統里卻略有不同。因為在操作系統里有內核空間的存在,我們寫的程序都是運行在用戶態的,需要經過內核來對硬件進行驅動,無法直接操作物理地址。
你當然可以選擇為這個LED寫一個驅動,從而在用戶空間通過read,write來操作它的狀態。不過有些同學一聽要寫驅動,就想吟一首蜀道難來表達自己的望而卻步。所以沒了解過驅動的同學你也可以選擇用一種更直接的方式:mmap。
就好像你可以選擇給貴妃下藥來控制她,不過下藥這種方式需要精通藥理、掌控時機,成本較高,難度較大。只要能達到目的,有時候扎個小人或許更加經濟適用。
我在上家公司的時候用ARM Cortex-A9芯片做過一個項目,開發過程大概是先和硬件同事約定好一個協議,然后我通過GPIO口的輸入輸出模擬出這個協議,通過它對寄存器進行讀寫配置,驅動硬件ADC采樣,然后將采回來的數據通過DMA傳輸,最終到應用層進行分析處理。其中驅動GPIO口的部分就用了mmap。
項目很大,做了半年多,想完全講明白也不現實,不過我們可以從驅動GPIO口這個點切入,體會一下軟件驅動硬件中間這玄妙的過程。聰明的你一定可以舉一反三。
4
作為一個軟件工程師,拿到板子的時候,硬件工程師一般會給你一份文檔,類似這樣:
這個文檔會指明,如果想操作這個GPIO口的話,你需要用GPIO外設的基地址加上偏移地址找到對應的寄存器地址,再用位操作給指定的bit寫入命令。
不過我由于FPGA也會一些,所以我們公司里FPGA的Block Diagram都是我來建的。建好FPGA的硬件工程后做一下綜合,從Address Map里就能看到我想用的GPIO口地址了。如圖:
無論怎樣,你現在拿到這個所謂的硬件寄存器地址了,接下來我們就可以拿小人扎它了。
以上圖我拿到的0x43C00000為例,這是寄存器的地址,那我能否直接在應用程序里把0x43C00000賦值給一個指針,然后對它進行讀寫呢?
在玩裸機的時候確實是這樣的。但是上面說了,Linux系統有虛擬內存的存在,就不能這么做了。因為理論上我可以在系統里開100個進程,這100個進程里都有0x43C00000這個地址,那這100個地址哪個是真正的寄存器地址呢?可能都不是。因為進程里的0x43C00000是虛擬的,它真正對應的物理地址在哪里,沒人知道。要想把虛擬地址和物理地址對應起來,就得用mmap進行內存映射。
5
mmap的函數接口定義如下:
void mmap(void addr,size_t length,
int prot,int flags,
int fd,off_t offset);
這里面參數比較多。其中addr一般指定為NULL,prot則用于設置映射區域的權限,比如是否可讀可寫;flags則用于指定是共享映射還是私有映射;而fd,offset,length這三個參數表示將fd對應的文件,從offset位置起,將長度為length的內容映射到進程的地址空間。
需要注意mmap的操作單元是頁,即最后映射的offset參數必須是內存頁大小的整數倍,而Linux系統內存頁大小一般為4096字節。
一個我在程序中的調用示例:
#define AXI_GPIO_BASEADDR 0x43C00000
int memfd = open("/dev/mem", O_RDWR
| O_SYNC);
if (-1 == memfd) {
printf("Can't open /dev/mem\n");
return -1;
}
unsigned int* led_gpio =
(unsigned int*)(mmap(
NULL, MMAP_SIZE,
PROT_READ | PROT_WRITE,
MAP_SHARED, memfd,
AXI_GPIO_BASEADDR));
調用mmap后,我們拿到一個指針,通過這個指針對指向的地址做任何操作,對應的寄存器物理地址也會有相同的效果。于是我們將它循環賦值0101,相應的寄存器控制的GPIO口輸出電信號,于是板卡上的燈成功的閃爍起來,類似奧特曼體力不支時的能量燈。
6
多說兩句,除了用來操作GPIO/字符設備外,mmap還有個常用的場景是操作塊設備。它和傳統的用read,write的區別,最關鍵的是省一次拷貝。
比如要讀取磁盤上某個文件的數據,用read write的話,由于會涉及到系統調用,進程是無法直接訪問內核的,所以在read系統調用返回前,內核需要將數據從內核復制到進程指定的buffer里。
但如果用mmap的話,那么這段數據會首先拷貝到內存中作為頁緩存(即page cache)。用mmap將這段內存映射到用戶空間,則進程可以通過指針直接讀寫page cache,不再需要多余的系統調用和內存拷貝。
不過雖然少了一次拷貝,但mmap會觸發缺頁中斷(page fault),相比于內存拷貝而言,缺頁中斷的開銷更大。所以性能而言mmap大部分情況下并不會比read/write要好。
說到頁緩存,我在上家公司開發項目的時候,還被臟頁延遲這玩意坑過。篇幅所限,頁緩存涉及到的缺頁中斷,臟頁,延遲寫回,sync強制寫回等內容,我們下次再詳細聊聊。
7
好了,于是我們學會用mmap點亮一個燈了。想象一下接下來的場景:
你跟公司研發部最漂亮的女同事說,
“hi,領導那邊給了我們組一個新任務,你寫個驅動控制一下LED吧?”
“???驅動這么難,我不會啦”
“哦沒事,那你用mmap吧”
“誒你怎么罵人呢!”