共计 3458 个字符,预计需要花费 9 分钟才能阅读完成。
这篇文章给大家介绍为什么 Linux 内核常常用 Unsigned Long 来代替指针,内容非常详细,感兴趣的小伙伴们可以参考借鉴,希望对大家能有所帮助。
昨天我犯了一个错误把指针和整数“混淆”的错误,幸得队友王童鞋指正,今早起床,我把这个心得花一点时间记录下来。
大抵掌握一个技术或者知识都是这三个阶段:
不知道自己不知道;
知道自己不知道;
知道自己知道。
比较难突破的是“不知道自己不知道”的阶段,因为“不知道自己不知道”,所以才往往特别自信,觉得“老子天下第一”。基本上,本文要记录的一个小点,也是一个我从“不知道自己不知道”到“知道自己知道”的过程。
我们都知道(???),指针和整数在 C 语言里面是两种不同含义的:
指针:主要是为了方便引用 (Dereferencing) 一个内存地址, Dereferencing is used to access or manipulate data contained in memory location pointed to by a pointer。所以指针的目的其实就是为了这样的读写操作:
*p = a; b = *p;
整数:整数是一个数值,它的主要目的是为了加减等计算、比对、做数组下标、做索引之类的。它的目的不是为了引用一个内存。指针和整数 (这里主要是 unsigned long,因为 unsigned long 的位数一般等于 CPU 可寻址的内存地址位数) 本身是八竿子打不着的,但是它们之间的一个有趣联系是:
如果我们只是关心这个地址的值,而不是关心通过这个地址去访问内存,这个时候,内核经常喜欢用 unsigned long 代替指针。
我们下面来看 2 个不同的场景:
指针是指针?
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 个函数里面,void __user *from,void __user *to 都清楚地表明用户空间的虚拟地址是一个指针。这 2 个函数这样做的原因是非常清晰的,我就是要去 Dereference 用户空间的地址,进行内存拷贝的,所以它的目的是为了通过指针来访问内存。
类似的例子比如 file_operations 里面 read、write 什么的:
指针是整数?
/** * 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)
注释清楚地写明,start 是用户态的起始地址:@start: starting user address
所以,本质上,和 copy_from_user()里面的 void __user *from 一样的,但是这里它用的是 unsigned long start !!! 不是 void __user * start !!!
原因非常清楚,get_user_pages()只关心 start 这个数值本身,它用于去运算、查找、比对。它不是要:
*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);
它根据 addr 用户态地址,在进程的 mm 里面去找到 addr 位于的 VMA。显然,这个时候,它的目的是为了完成 addr 与进程每个 VMA 起始和结束地址的比多。
这个时候,我们来看看 VMA 结构体的长相,就更加有意思了。我们都知道,VMA 是为了记录进程每一段虚拟地址空间的(比如代码段、数据段、堆、栈、mmap 等):
然后我们看看 VMA 的定义:
看到没有,vm_start 和 vm_end 都是妥妥的 unsigned long 啊!!!
我于是试图弄清楚这么做的科学依据是什么,发现 LDD3 里面赫然写着这么一段话(LDD3 第 11 章 289 页):
它的科学依据是,既然你不是为了 dereferencing,我就让你 dereferencing 不了,免得你又跑去 dereferencing,从而导致 bug。有的人说,我强制转化 unsigned long 为指针,不就可以访问了吗?
你不是还是需要强制转换不是? 你强制转换之前,会想一下,这个地方指针为啥是个整数呢? 你想明白了,说不定就不去访问了。这样它实际达到了震慑心灵的效果。
到这里,我们谈的都还是虚拟地址,那么下面我们来谈下物理地址。
物理地址是指针?
在一个有 MMU 的系统中,物理地址从来都不是指针。物理地址,从骨子里就是一个整数!!! 我记得之前经常有人往内核发 patch,把物理地址用个指针
* p 来描述,这是错到根子里面的事情,所以每次都被骂地狗血淋头。
因为你根本不可能用物理地址去 Dereferencing 什么东西。物理地址在内核的描述是:
它要么是一个 32 位的整数,要么是一个 64 位的整数。
那么,物理地址什么时候是一个指针呢? 在我还是一个小屁孩在大学玩《仙剑奇侠传》和《轩辕剑:天之痕》的时候,我那个时候玩单片机,单片机里面没有 MMU,所以也没虚拟地址的概念,都是妥妥地通过物理地址“指针”来访问内存的。
所以,如果一个人,一辈子都是玩单片机,它肯定会觉得我这篇文章在胡扯,因为他还是一个“不知道自己不知道”的阶段。
模糊地带
这里面仍然有一些模糊地带,比如__get_free_page()、__get_free_pages()这样的 API,返回的也是 unsigned long 而不是指针:
往死里作也要把它弄成 unsigned long!!!
实际上,内核要有时候需要访问__get_free_page()返回的内存,此前它需要进行强制类型转换:
这看起来是不是特别地“精分”? 折腾来折腾去,折腾什么鬼呢? 这一篇文章有解释:An (unsigned) long story about page allocation
https://lwn.net/Articles/669015/
统计表明,90% 以上的情况下,__get_free_page()返回的 unsigned long 都会被强制转化为指针!!! 但是这个返回 unsigned long 是在历史的第一天,Linux 的 0.01 就这样了。Al Viro
https://lwn.net/Articles/668852/
但是改动实在太多了,改了接近 600 个文件,对此 Linus 的态度是:
No way in hell do we suddenly change the semantics of an interface that has been around from basically day #1.
所以 Linus 的建议是,你真的需要一个指针的时候,你还是去调用 kmalloc()吧:
*kmalloc(size_t size, gfp_t flags);
绝世好代码
很多工程师喜欢较真,就是必须在 0 和 1 之间做一个选择。这个选择有时候真的很难,所以 Linus 的意思是,0 和 1 踏马地都不要,我不去跟你争这个 0 和 1 的问题,我给你第三条路。这里,我看出了 Linus 的大智若愚啊!
我个人在工程里面对无意义的 0 和 1 的争论也也没什么好感,感觉在浪费我的时间。我对事情的看法是,争一个 0.7 或者 0.3 就 OK 了。0.7 就是真方向,0.3 就是假方向。
关于为什么 Linux 内核常常用 Unsigned Long 来代替指针就分享到这里了,希望以上内容可以对大家有一定的帮助,可以学到更多知识。如果觉得文章不错,可以把它分享出去让更多的人看到。