為什么Linux內(nèi)核常常用Unsigned Long來(lái)代替指針
本文目錄
- 不知道自己不知道!
- 指針何時(shí)指針?
- 指針何時(shí)是整數(shù)?
- 物理地址是指針?
- 模糊地帶
- 絕世好代碼?
昨天我犯了一個(gè)錯(cuò)誤把指針和整數(shù)“混淆”的錯(cuò)誤,幸得隊(duì)友王童鞋指正,今早起床,我把這個(gè)心得花一點(diǎn)時(shí)間記錄下來(lái)。
大抵掌握一個(gè)技術(shù)或者知識(shí)都是這三個(gè)階段:
- 不知道自己不知道;
- 知道自己不知道;
- 知道自己知道。
比較難突破的是“不知道自己不知道”的階段,因?yàn)?ldquo;不知道自己不知道”,所以才往往特別自信,覺(jué)得“老子天下第一”?;旧?,本文要記錄的一個(gè)小點(diǎn),也是一個(gè)我從“不知道自己不知道”到“知道自己知道”的過(guò)程。
我們都知道(???),指針和整數(shù)在C語(yǔ)言里面是兩種不同含義的:
- 指針:主要是為了方便引用(Dereferencing)一個(gè)內(nèi)存地址, Dereferencing is used to access or manipulate data contained in memory location pointed to by a pointer。所以指針的目的其實(shí)就是為了這樣的讀寫(xiě)操作:
- *p = a;
- b = *p;
- 整數(shù):整數(shù)是一個(gè)數(shù)值,它的主要目的是為了加減等計(jì)算、比對(duì)、做數(shù)組下標(biāo)、做索引之類的。它的目的不是為了引用一個(gè)內(nèi)存。指針和整數(shù)(這里主要是unsigned long,因?yàn)閡nsigned long的位數(shù)一般等于CPU可尋址的內(nèi)存地址位數(shù))本身是八竿子打不著的,但是它們之間的一個(gè)有趣聯(lián)系是:
如果我們只是關(guān)心這個(gè)地址的值,而不是關(guān)心通過(guò)這個(gè)地址去訪問(wèn)內(nèi)存,這個(gè)時(shí)候,內(nèi)核經(jīng)常喜歡用unsigned long代替指針。
我們下面來(lái)看2個(gè)不同的場(chǎng)景:
指針是指針?
- copy_from_user(void *to,
- const void __user *from,
- unsigned long n);
- copy_to_user(void __user *to,
- const void *from,
- unsigned long n);
在這2個(gè)函數(shù)里面,void __user *from,void __user *to都清楚地表明用戶空間的虛擬地址是一個(gè)指針。這2個(gè)函數(shù)這樣做的原因是非常清晰的,我就是要去Dereference用戶空間的地址,進(jìn)行內(nèi)存拷貝的,所以它的目的是為了通過(guò)指針來(lái)訪問(wèn)內(nèi)存。
類似的例子比如file_operations里面read、write什么的:
指針是整數(shù)?
- /**
- * get_user_pages() - pin user pages in memory
- * @start: starting user address
- * ...
- */
- long get_user_pages(
- unsigned long start,
- unsigned long nr_pages,
- unsigned int gup_flags, struct page **pages,
- struct vm_area_struct **vmas)
注釋清楚地寫(xiě)明,start是用戶態(tài)的起始地址:@start: starting user address
所以,本質(zhì)上,和copy_from_user()里面的void __user *from一樣的,但是這里它用的是unsigned long start !!!不是void __user * start !!!
原因非常清楚,get_user_pages()只關(guān)心start這個(gè)數(shù)值本身,它用于去運(yùn)算、查找、比對(duì)。它不是要:
- *start = 100;
類似的例子還有:
- long pin_user_pages(
- unsigned long start, unsigned long nr_pages,
- unsigned int gup_flags, struct page **pages,
- struct vm_area_struct **vmas);
更加不要提著名的find_vma():
- /* Look up the first VMA which satisfies addr < vm_end,
- NULL if none. */
- struct vm_area_struct *find_vma(
- struct mm_struct *mm,
- unsigned long addr);
它根據(jù)addr用戶態(tài)地址,在進(jìn)程的mm里面去找到addr位于的VMA。顯然,這個(gè)時(shí)候,它的目的是為了完成addr與進(jìn)程每個(gè)VMA起始和結(jié)束地址的比多。
這個(gè)時(shí)候,我們來(lái)看看VMA結(jié)構(gòu)體的長(zhǎng)相,就更加有意思了。我們都知道,VMA是為了記錄進(jìn)程每一段虛擬地址空間的(比如代碼段、數(shù)據(jù)段、堆、棧、mmap等):
然后我們看看VMA的定義:
看到?jīng)]有,vm_start和vm_end都是妥妥的unsigned long啊!!!
我于是試圖弄清楚這么做的科學(xué)依據(jù)是什么,發(fā)現(xiàn)LDD3里面赫然寫(xiě)著這么一段話(LDD3第11章289頁(yè)):
它的科學(xué)依據(jù)是,既然你不是為了dereferencing,我就讓你dereferencing不了,免得你又跑去dereferencing,從而導(dǎo)致bug。有的人說(shuō),我強(qiáng)制轉(zhuǎn)化unsigned long為指針,不就可以訪問(wèn)了嗎?
你不是還是需要強(qiáng)制轉(zhuǎn)換不是?你強(qiáng)制轉(zhuǎn)換之前,會(huì)想一下,這個(gè)地方指針為啥是個(gè)整數(shù)呢?你想明白了,說(shuō)不定就不去訪問(wèn)了。這樣它實(shí)際達(dá)到了震懾心靈的效果。
到這里,我們談的都還是虛擬地址,那么下面我們來(lái)談下物理地址。
物理地址是指針?
在一個(gè)有MMU的系統(tǒng)中,物理地址從來(lái)都不是指針。物理地址,從骨子里就是一個(gè)整數(shù)!!!我記得之前經(jīng)常有人往內(nèi)核發(fā)patch,把物理地址用個(gè)指針
*p來(lái)描述,這是錯(cuò)到根子里面的事情,所以每次都被罵地狗血淋頭。
因?yàn)槟愀静豢赡苡梦锢淼刂啡ereferencing 什么東西。物理地址在內(nèi)核的描述是:
它要么是一個(gè)32位的整數(shù),要么是一個(gè)64位的整數(shù)。
那么,物理地址什么時(shí)候是一個(gè)指針呢?在我還是一個(gè)小屁孩在大學(xué)玩《仙劍奇?zhèn)b傳》和《軒轅劍:天之痕》的時(shí)候,我那個(gè)時(shí)候玩單片機(jī),單片機(jī)里面沒(méi)有MMU,所以也沒(méi)虛擬地址的概念,都是妥妥地通過(guò)物理地址“指針”來(lái)訪問(wèn)內(nèi)存的。
所以,如果一個(gè)人,一輩子都是玩單片機(jī),它肯定會(huì)覺(jué)得我這篇文章在胡扯,因?yàn)樗€是一個(gè)“不知道自己不知道”的階段。
模糊地帶
這里面仍然有一些模糊地帶,比如__get_free_page()、__get_free_pages()這樣的API,返回的也是unsigned long而不是指針:
往死里作也要把它弄成unsigned long!!!
實(shí)際上,內(nèi)核要有時(shí)候需要訪問(wèn)__get_free_page()返回的內(nèi)存,此前它需要進(jìn)行強(qiáng)制類型轉(zhuǎn)換:
這看起來(lái)是不是特別地“精分”?折騰來(lái)折騰去,折騰什么鬼呢?這一篇文章有解釋:An (unsigned) long story about page allocation
https://lwn.net/Articles/669015/
統(tǒng)計(jì)表明,90%以上的情況下,__get_free_page()返回的unsigned long都會(huì)被強(qiáng)制轉(zhuǎn)化為指針!!!但是這個(gè)返回unsigned long是在歷史的第一天,Linux的0.01就這樣了。Al Viro
https://lwn.net/Articles/668852/
但是改動(dòng)實(shí)在太多了,改了接近600個(gè)文件,對(duì)此Linus的態(tài)度是:
"No way in hell do we suddenly change the semantics of an interface that has been around from basically day #1."
所以Linus的建議是,你真的需要一個(gè)指針的時(shí)候,你還是去調(diào)用kmalloc()吧:
- *kmalloc(size_t size, gfp_t flags);
絕世好代碼
很多工程師喜歡較真,就是必須在0和1之間做一個(gè)選擇。這個(gè)選擇有時(shí)候真的很難,所以Linus的意思是,0和1踏馬地都不要,我不去跟你爭(zhēng)這個(gè)0和1的問(wèn)題,我給你第三條路。這里,我看出了Linus的大智若愚啊!
我個(gè)人在工程里面對(duì)無(wú)意義的0和1的爭(zhēng)論也也沒(méi)什么好感,感覺(jué)在浪費(fèi)我的時(shí)間。我對(duì)事情的看法是,爭(zhēng)一個(gè)0.7或者0.3就OK了。0.7就是真方向,0.3就是假方向。
“絕世好代碼”是不存在的,“水至清則無(wú)魚(yú)”,往死里爭(zhēng)反而陷入了鉆牛角尖。等你寫(xiě)出絕對(duì)完美代碼的時(shí)候,黃花菜早就歇了。別忘了,還有最重要的一招,“天下武功,無(wú)堅(jiān)不破,唯快不破。”
本文轉(zhuǎn)載自微信公眾號(hào)「Linux閱碼場(chǎng)」,可以通過(guò)以下二維碼關(guān)注。轉(zhuǎn)載本文請(qǐng)聯(lián)系Linux閱碼場(chǎng)公眾號(hào)。