Chinaunix

标题: 快速内存操作技术 [打印本页]

作者: obrire    时间: 2006-04-12 11:55
标题: 快速内存操作技术
我们总希望能在内核空间和用户空间自由交换数据,传统的方式是采用内核提供的vma机制,通过copy_to/from_user之类的方法来实现。这对于高速数据块传送是不可取的。
其实kswapd有时交换数据至外存时,性能相当差,一般而言,你的PC主存并不见得就用尽了,而且很多应用并是你所关心的,但却实实在在消耗你的计算资源,此时,你可能会想到采用实地址操作你的应用(尽管有MMU的支持),在uClinux中,当没有MMU时,工作起来是挺爽的。
最方便的,最高效的,无非是实地址下,操作物理内存,相当于DMA。尽管Linux 2.6内核在VMA方面的性能要优于2.4,但swap机制有一定的缺限。在VxWorks中,对内存的管理是很细致且精确的。如果你在Linux下申请大块内存操作时,当触发kswap快速交换回主存时,你会发现你的计算机
哪一段时间,几乎要休息几分钟,尽管你看起来free输出的頁面已经很多了,但此时的外存几乎一直忙着,且CPU负荷相当重,这时也可能你根本就没做任何操作。
有没有办法既在VMA机制下工作,又能直接处理主存数据呢?
回答是肯定的:
内核启动后,会将主存映射为/dev/mem,当我们在VMA中申请一块逻辑区间后,将转换的物理地址传与用户空间,用户空间就可以线性操作这段区间了,直接采用mmap就可以在物理主存中读写。
还有更快的方式吗?不采用mmap行吗?
当然有,当VMA将物理区块地址及大小传与用户空间后,用户完全可以采用地址读写模式,这几近于机器指令操作了。(可以独立于OS之外)
此种实现方式很简单,首先打开/dev/mem,直接定位至VMA传上来的地址就可以操作了,无需mmap。通知用户空间,可以采用procfs/sysfs等。
强烈建议:在边界控制上,一定要精确细致,不推荐初学者采用,一旦越界,可能引起“Kernel Panic”
更有可能就DOWN了。
对于此实现方式,建议参考mapper(LDD3)程序。
同时,也可以用mapper检查读写是否正确。

新的IA32中,PAGE_SIZE = 4K
            MAX_ORDER = 11
gfp最大申请4M线性逻辑空间,减去PAGE_OFFSET即可得到物理地址。
如果采用了HIMEM和NUMA,请参见Kernel关于内存映射的算法。
当主存达到1G或超过时,请减小用户空间大小。
随着硬件技术的发展,VMA在主存相当大时,可以考虑进行修正,完全可以集中采用物理映射方式。没必要交换了。否则,反而影响性能。
32位平台,如果主存2G,采用SWAP会使得性能有较大下降。
而在大容量数据传输时,也不可能采用SWAP方式的,不允许换出。
建议Linus在VMA中加入进程分类,不平类别的应用,分配不同的内存块。
小数据PAGE_SIZE = 4K/大数据4M/128M
地址模式全转换为64位,兼容32位,当应用于Embedded系统时,直接使用32位模式。
如将做成多个微系统,不同微核,运行不同应用(不同的进程管理,内存管理,文件管理),只是接口可以统一,不同标志而已。
作者: guotie    时间: 2006-04-12 12:36
非常好!
作者: xiaozhaoz    时间: 2006-04-13 09:40
个人觉得,根本不可行!

首先, 你提到的在vma存在的情况下, swap存在的情况下, 直接访问物理内存. 而且,看情况这块物理内存是用户空间分配的.
在mmu的CPU中, page frame映射到 page中, 不是固定的, 更确切的说, 一块物理页, 如果是用户空间分配的, 不是只给一个任务使用的, 因为swap处于运行中.  别以为你直接访问物理内存, swap就不运行了, 你要用内核的分配物理页函数, 就可能触发kswap

直接访问page frame, 可能导致访问到的page frame存放的是已经被切换出去的任务的数据. 即使你非常注意访问的准确性.

再者,  你这里说得所有东西都没考虑到, 用户任务分配好vma后, 物理地址根本是不确定的. 要不内核的lazy map机制就没用了.

最后, 内核现在的实现已经有非常好的扩展性, 可能是你了解不多. 根本不需要你说得这么多复杂的东西.
在嵌入式mmu CPU环境下,通过 mlock + 锁的技术,可以在内核和用户空间直接在有限的物理页上交换数据. 用户空间malloc几页物理内存, 然后在每页中写入一两个字符, 以完成内核的page frame 到page的映射, 然后用mlock()锁定这几页, 不然swap运行. 然后就可以用这几页来完成用户空间和内核的数据交互. 不过锁的实现都要考虑.

还有, 嵌入式环境没有磁盘, 一般需要将swap off, 就是设置内核的watermark level为0,这样kswap就不会将物理页交换. 直接调swapoff命令. 或者写/proc/sys/vm/swappiness

内核对vm分配也有限制, 所以用户空间malloc过大的空间,也可能不成功, 这时内核的 over commit功能.
作者: albcamus    时间: 2006-04-13 09:55
原帖由 xiaozhaoz 于 2006-4-13 09:40 发表
个人觉得,根本不可行!

首先, 你提到的在vma存在的情况下, swap存在的情况下, 直接访问物理内存. 而且,看情况这块物理内存是用户空间分配的.
在mmu的CPU中, page frame映射到 page中, 不是固定的, 更确切的说, ...


正看的过瘾呢, 是不是还没打完字呀?
作者: xiaozhaoz    时间: 2006-04-13 10:37
原帖由 albcamus 于 2006-4-13 09:55 发表


正看的过瘾呢, 是不是还没打完字呀?


没了, 就这些东西, 对内存这块不怎么懂, 不敢乱说.

不过将vm分成 3G/IG, 2G/2G, 1G/3G的选项已经合入最新的内核2.6.17-RC1, 正在看实现.

讨论在:
http://groups.google.com/group/l ... =2#f99732a73435db13
作者: 思一克    时间: 2006-04-13 11:15
争论的很激烈吗
作者: obrire    时间: 2006-04-13 23:07
标题: 回复 5楼 xiaozhaoz 的帖子
首先,在普通的PAGE_SIZE情况下,如4K不是4M,能得到的order最大为11,其实一般是<11
也就是取10,源码中有定义,只能用到4M Logical Memory,也就是只能得到4M线性物理地址.
而在uClinux中,虽然没有kswapd,但当你的应用free或destroy时,内核并不能如你所想,全
然释放.
至于上有人提到内存分配问题,这是在Module中分配的.不会有在初始化时加入mem=xxM的限制
这些内存在module unload时,可以回归内核管理.
至于用户空间所采用的malloc,其实只是让内核知道这段内存的申请是合法的,最后还是通过page
来实现的.
当然,如果纯64位平台,可以采用4M的PAGE_SIZE
如果定义MAX_ORDER <= 1024, 可以使用4G的Memory.
以前有Intel的朋友试着做过这些分配,或有人在2.6.14以后加入relayfs,其实这都不可能保证性线.

如果我们采用Multi Micro Kernel, 就有点像IBM的虚拟机了,可以同时运行多种OS,多种管理模式,
不过管理调度是相当复杂的, AIX中有加入对SMP和多种微环境支持.

VMware中有一定的实现功能, 性能也还可以,但在要求苛刻的应用下,如同步数传系统中,当前的内核
就不足以应对了. 2.6的VMA制式,在通常用户计算环境中,性能是出色,好像在2004年Moto中有人做
个这种测试,有机会传上来看看.

如果有两枚,或非SMP(3-5枚)CPU,有可能我要求一段内存完成100M的线性DMA转发数据,<1ms的
时延,这时可安排其中一枚来做这件事,这是可行的,有时更不需要1ms,只是数据后处理要求时间稍长.

非对称模式,或称混合网络计算模式,单主机也可以实现. 以前有些SuperComputer采用背板高速串行
数据交换,可以有首效隔离,采用BLADE单元计算,但这不能共享非空时段内的Memory资源.

如果没有负荷时,有些内存需要为主运算平台的诸如DB/WWW服务,可以通过一些机制来完成动态remap.

大家有兴趣研究一下SMP/及非对称多CPU共存计算环境.

CELL将很多核做进一个Chip, 但不如通过其它IO总线,扩展多路CPU,既可以每路CPU自带MEMORY和CACHE,
也可以动态向主OS动态申请,且加入是动态的,不需要主OS环境停机.

是不是很爽呀, 你突然发现图形处理不过来了,接上USB外置处理器,负荷转移置专用CPU上,一下快多了
不用时,unload就好了.主存自动回归原计算环境.真是CBL.

社会本就多态, 有什么花样, 就突破限制.
F.E.U
Future Extension Union

[ 本帖最后由 obrire 于 2006-4-13 23:24 编辑 ]
作者: obrire    时间: 2006-04-13 23:29
原帖由 xiaozhaoz 于 2006-4-13 09:40 发表
个人觉得,根本不可行!

首先, 你提到的在vma存在的情况下, swap存在的情况下, 直接访问物理内存. 而且,看情况这块物理内存是用户空间分配的.
在mmu的CPU中, page frame映射到 page中, 不是固定的, 更确切的说, ...

在内核空间申请
在用户空间不用mmap
直接open /dev/mem
lseek xxxxx
一切就哪样简单,就像汇编,写进去就行了.
这可是直接物理内存操作呀.
其实在性能要求高的环境,不需要外存的.有些Super Computer就是这样的.
结果直接就通过网络送走了.
作者: obrire    时间: 2006-04-13 23:39
标题: 回复 3楼 xiaozhaoz 的帖子
即便是MVista提供的核心中,也不见得保证VMA工作得很好.
内存一大,释放就可能变得很有效.当我用MPlayer static版时,退出时,好像内存并未很快释放.
如果还像MV采用spin_lock,就不用我写这部分了.在block操作时,只有memcpy,memset.

DMA操作时,如果映射都是10M以上的,怎么办?如果有硬时钟要求,每秒要提取序列,怎么实现???
目前有哪家的Linux能达到VMA全速实时?电信用户可不像IA产品.

即便是100Mbps的网卡,也不像10M同步信号要求那么高.

严重声明:
      采用这种方式, 是不需要在用户空间用malloc的(没有任何加锁时延及浪费),完全就是Register
     操作方式,直接置数.
    一般在procfs中读取时,如你用了系统的task_lock,这对要求不高的应用是可行的.
     至于在IA32中采用2G/2G/1G/3G,目前内核都没有问题.
     
当大容量内存在内核空间申请时,启用VMA,有时不见得成功,反而会触发系统自动将外存的数据换回主存.
这时系统几乎很少响应其它应用,且长时间调整(特别是换回在100M以上时).

至于细节,不便公开,望诸位见谅.以上只说明实现方式,是可行的.
有人问过Linus,但他只提及采用其它方式,或将用户空间缩小.所以我建议他们两个主创修改这部分实现.
反正他们自己也在争论.

尊重他们,体系可以不变,但可以变通的是方法.

如果大家对嵌入式哪么了解, 我可以推荐.

[ 本帖最后由 obrire 于 2006-4-13 23:56 编辑 ]
作者: guotie    时间: 2006-04-14 00:32
支持!

linux还是有很多可以改进德地方德,以前也遇到需要分配大于4M空间德情况,郁闷!
作者: xiaozhaoz    时间: 2006-04-14 09:14
说实在的, 看了一会儿, 不知道表述了什么意思.

但有几个说法我认为不正确.
1.  用户空间一次分配4M的空间分配不到?
在有mmu的CPU中, 400M我都分配过. 除非内核的VM分配限制启用,且用户空间分配的虚存 > 1/2物理内存+swap空间大小, 这是默认设置.
不知道你这个结论怎么得出来的. 你仔细看一下Linux libc库和内核如何分配用户空间内存就知道了. 我上面也说了, lazy tlb是内核分配虚存的策略之一.
至于在内核空间一次分配 >4M 物理内存, 这种代码我还没见过, 呵呵.

2. 你一直强调你在用户空间不用malloc, 那是我的理解错了, 不过这也和你顶楼的说法相悖.
如果你用户空间不用malloc, 直接访问物理内存, 也不用内核帮你分配物理内存. 那问题同样存在, 你如何知道你用的哪个物理页, 内核没有自己使用, 或者内核没有分配给其它用户空间任务??
我只知道, Linux物理内存是否被占用, 在物理内存中是不会有记录的, 这些信息记录在VM的低12位地址中. 内核中, 在flat mode的内存管理中, 内核只是用一个bitmap + 数组来管理物理内存.

3.  你说强调的电信设备对内存要求很高, 恰好,我一直在作电信设备, 应该算是核心网级别的. 我只知道这些和硬件总线有关, 向Linux这样的操作系统, 中间隔了一层虚存管理, 确实不如直接使用物理地址方便. 如果是这样,我们一般必须在内核中将虚存关闭, 然后CPU也关闭mmu. 就是把CPU当一个没有mmu的硬件来使用, 但这情况中,我们一般不用Linux, 因为Linux涉及到libc库的问题. 工作量很大.  我个人认为, 向你上面那样简单的直接访问物理地址, 想提高效率的做法, 是有错误的.

其它的东西, 我看不出来和这个主题有什么关系.

[ 本帖最后由 xiaozhaoz 于 2006-4-14 09:24 编辑 ]
作者: obrire    时间: 2006-04-14 10:05
原帖由 xiaozhaoz 于 2006-4-14 09:14 发表
说实在的, 看了一会儿, 不知道表述了什么意思.

但有几个说法我认为不正确.
1.  用户空间一次分配4M的空间分配不到?
在有mmu的CPU中, 400M我都分配过. 除非内核的VM分配限制启用,且用户空间分配的虚存 > 1 ...


libc要实现内存分配,首先要早内核申请(只是让内核觉得这段内存是合法引用),但管理是由libc来完成的。

在实模式下,无非采用pSOS/VxWorks,而VxWorks下,只用了一个malloc
采用Vrit_to_Phy就可以实现转换。

而当你获得物理内存区间后,你的操作和管理是你自己的事,与OS已经没关系了,这与libc有什么关系???
尽管此时你已经在用户空间。

我不管你是做SDH/DWDM或是其它诸如10GE/1394/usb的应用,老实说,STM-64->GE映射也需要时间的。在PMC自己的VoIP测试上,硬件本身的时延也有近300ms. 因此在内存原子极操作,快速成帧模型
是最重要的。

在这理只是简述了一种使用时尽可能绕开OS管理,但释放时,又自动回归OS管理的机制。

至于总有人谈到读内核哟,看libc等,没兴趣,因为好多同事都做过了,或者优化过了,只是商业代码不公开
而已。


===============================
对任何问题我都一知半解
多少像一个小孩

看起老是长不大
我总以为自己是聪明的,其实发觉世界已经没有傻蛋
作者: obrire    时间: 2006-04-14 10:17
原帖由 xiaozhaoz 于 2006-4-14 09:14 发表
说实在的, 看了一会儿, 不知道表述了什么意思.

但有几个说法我认为不正确.
1.  用户空间一次分配4M的空间分配不到?
在有mmu的CPU中, 400M我都分配过. 除非内核的VM分配限制启用,且用户空间分配的虚存 > 1 ...


1. 如果在用户空间分配400M的线性物理地址,你是高手(我的PC只有512M)
2. 本文所谈,是在内核空间分配,便于在用户空间自由使用和管理
3. 就是bio也不能按常规方式实现
4. 你所分配的400是VMA下的,放大了且不连续
5. 所有这些讨论都是基于内核空间的,只是要通知用户间知道内存所在地址和尺寸
6. 如果用libc在加载时性能有一点影响,但工作时不影响,这是严格定义和区分的
    如果你觉得这都不好,可以置入汇编,这没办法再快了,且工作时DMA传送不会影响
   CPU的性能的,哪除此之外,你还有什么建议和想法呢???
    在i386下,你觉得gcc不好,再建议使用Intel的C Compiler吧. 实在到了极限
   

Footprint one bye one

[ 本帖最后由 obrire 于 2006-4-14 10:19 编辑 ]
作者: albcamus    时间: 2006-04-14 10:31
原帖由 xiaozhaoz 于 2006-4-14 09:14 发表

3.  你说强调的电信设备对内存要求很高, 恰好,我一直在作电信设备, 应该算是核心网级别的. 我只知道这些和硬件总线有关, 向Linux这样的操作系统, 中间隔了一层虚存管理, 确实不如直接使用物理地址方便. 如果是这样,我们一般必须在内核中将虚存关闭, 然后CPU也关闭mmu. 就是把CPU当一个没有mmu的硬件来使用, 但这情况中,我们一般不用Linux, 因为Linux涉及到libc库的问题. 工作量很大.  我个人认为, 向你上面那样简单的直接访问物理地址, 想提高效率的做法, 是有错误的.



还有这样的事啊, linux关闭VM&&CPU关闭MMU, 开眼界
作者: xiaozhaoz    时间: 2006-04-14 10:43
原帖由 albcamus 于 2006-4-14 10:31 发表


还有这样的事啊, linux关闭VM&&CPU关闭MMU, 开眼界


老大啊,我可没说 关闭Linux的VM和 在CPU硬件上关闭MMU啊.  我是说在操作系统内核关闭这些东西.

Linux在内核中关闭VM我没有尝试过, 可能会涉及到很多东西.

不过一般的芯片关闭mmu还是有寄存器可以设置的.
Linux不适合这么作, 我认为是因为libc的原因,
一般的LIBC设置成nommu工作模式不太容易, 如ppc, 一般libc都是按照mmu启用情况下编写的.  我一直是这样认为的, 如果有人有更好的见解,欢迎另开主题讨论.

我们一般用 VxWorks, 或者其它一些嵌入式系统作这些事情. 即使有mmu的CPU, 我们也可以不启用. 直接访问实地址, 但必须在硬件中关闭mmu, 而且 操作系统要扩展的足够好才行.
作者: xiaozhaoz    时间: 2006-04-14 10:57
如果lz对阅读代码没有兴趣,我想我是找错了讨论对象了. 我只懂技术,  只会看代码和些软件.

你说得libc分配到物理内存后, 请问在有MMU的环境中libc怎么直接分配物理内存?
通过libc, 只能分配到虚存.
你怎么才知道你的东西已经分配到
作者: xiaozhaoz    时间: 2006-04-14 11:01
如果lz对阅读代码没有兴趣,我想我是找错了讨论对象了. 我只懂技术,  只会看代码和些软件.

你说得libc分配到物理内存后, 请问在有MMU的环境中libc怎么直接分配物理内存?
通过libc, 只能分配到虚存.
你怎么才知道你的东西已经分配到物理内存了? 内核的lazy tlb决定了虚拟内存分配到后,不会马上对应到物理内存(page frame)

在这个主题中,我只想讨论 你提出的在MMU CPU, VM工作, Swap 工作的情况下, 快速直接访问物理内存的方法. 其它的可以另开主题讨论.

BTW: 虽然我不懂硬件, 但是单节点300ms的延时让我震惊. 我想如果中间通过10个你们的voip设备的话 .....

在Linux内核中实现的方案, 用商业技术来推脱, 有点可笑和羞辱. 如果是机密的商业方案, 还是不要拿出来说为妙.
作者: obrire    时间: 2006-04-14 11:03
原帖由 xiaozhaoz 于 2006-4-14 10:57 发表
如果lz对阅读代码没有兴趣,我想我是找错了讨论对象了. 我只懂技术,  只会看代码和些软件.

你说得libc分配到物理内存后, 请问在有MMU的环境中libc怎么直接分配物理内存?
通过libc, 只能分配到虚存.
你怎么 ...

叫你看看ldd3的mapper,你不看。
怎么说呢?
/dev/mem是物理内存的全部映射。

一再申明,不需要在用户空间用malloc申请虚存。
只存定位/dev/mem下的地址,在内核空间申请,这样在用户使用才合法,不会引起kernel panic
作者: xiaozhaoz    时间: 2006-04-14 11:32
我没看ldd3, 我只看过内核代码.

和顶楼的不同, 你现在说得是不用vma, 直接在内核分配物理内存, 如果是这样的话, 你的用户空间要使用的内存都要内核直接分配物理内存, get_pages(), 通过系统调用进入内核的. 那么这些物理内存是带有__KERNEL__ 属性的.  

那么和我提出的方案一样,为什么不用mlock呢? 这不是更快更方便吗?
作者: obrire    时间: 2006-04-14 11:52
原帖由 xiaozhaoz 于 2006-4-14 11:32 发表
我没看ldd3, 我只看过内核代码.

和顶楼的不同, 你现在说得是不用vma, 直接在内核分配物理内存, 如果是这样的话, 你的用户空间要使用的内存都要内核直接分配物理内存, get_pages(), 通过系统调用进入内核的. 那 ...


关键是你mlock也要在分配之后呀
还有,你又能lock多少页
在内核中有是限制的

在2.4中有reserved memory方式,现在好像不用了
本来就是要让__KERNEL__知道,这些内存不用时,要他回收
这有点像libc一样,通知你,但不操作层不受控于你,就有点像reserved。
只是标志为内核空间,如果人为再加上mlock是多余的,因为控制权要交由用户空间程序
你不可能用mlock去锁定大块内存的,哪是不稳定,也不可靠的
这些修正,RMK,Linus他们也在考虑,因为有些应用,不是通用方式,管理机制要灵活才行
这方面,还争论不休。

与 Windows 的系统服务调用实现机制类似,Linux 内部为所有核心态系统调用,维护了一张按调用号排序的跳转表 (sys_call_table @ arch/i386/kernel/entry.S)。只不过对 Window 来说,类似的跳转表 (KeServiceDescriptorTable @ ntos/ke/kernldat.c) 按功能进一步细分为四部分,分别用于内核与 Win32 子系统等。而 Linux 的系统服务表,因为开放性、兼容性和移植性等问题,则相对稳定和保守得多。
以下内容为程序代码:


// sys_call_table @ arch/i386/kernel/entry.S

.data
ENTRY(sys_call_table)
.long sys_restart_syscall /* 0 - old "setup()" system call, used for restarting */
.long sys_exit
...
.long sys_request_key
.long sys_keyctl

syscall_table_size=(.-sys_call_table)





以下内容为程序代码:

// KSERVICE_TABLE_DESCRIPTOR @ ntos/ke/ke.h

#define NUMBER_SERVICE_TABLES 4

typedef struct _KSERVICE_TABLE_DESCRIPTOR {
    PULONG Base;
    PULONG Count;
    ULONG Limit;
    PUCHAR Number;
} KSERVICE_TABLE_DESCRIPTOR, *PKSERVICE_TABLE_DESCRIPTOR;

// KeServiceDescriptorTable @ ntos/ke/kernldat.c

KSERVICE_TABLE_DESCRIPTOR KeServiceDescriptorTable[NUMBER_SERVICE_TABLES];




而对用户态 API 来说,Linux 和 Windows NT/2K 下面都是通过传统的中断方式,Linux 使用 int 0x80;Windows 使用 int 0x2E。对 glibc 来说,其实就是一系列的宏定义,如 _syscall0 - _syscall6 等不同形式,如
以下内容为程序代码:

#define _syscall1(type,name,type1,arg1) \
type name(type1 arg1) \
{ \
long __res; \
__asm__ volatile ("int 0x80" \
: "=a" (__res) \
: "0" (__NR_##name),"b" ((long)(arg1))); \
__syscall_return(type,__res); \
}




而在系统加载的时候,接管 0x80 中断服务历程,完成基于 sys_call_table 跳转表的派发。如
以下内容为程序代码:

static void __init set_system_gate(unsigned int n, void *addr)
{
_set_gate(idt_table+n,15,3,addr,__KERNEL_CS); // 允许再 ring 3 调用此陷阱 (15) 的系统门
}

#define SYSCALL_VECTOR 0x80

asmlinkage int system_call(void);

void __init trap_init(void)
{
...
set_system_gate(SYSCALL_VECTOR,&system_call);
...
}




这里的 trap_init 函数 (arch/i386/kernel/traps.c) 负责初始化各种中断的处理例程,其将被系统初始化 start_kernel (init/main.c) 函数中调用。对 SYSCALL_VECTOR (0x80) 调用,由 system_call 函数 (arch/i386/kernel/entry.S) 进行实际处理工作。

而对 Windows 用户态 DLL 如 NtDll 来说,也是通过类似的方式实现。因为这方面讨论的文章较多,这里就不再罗嗦。有兴趣的朋友可以参考 Inside Win2K 一书,以及 《剖析Windows系统服务调用机制》 等文章。

但是这种基于中断的系统服务调用机制,对于 Intel P4 以上 CPU 来说存在着很大的性能隐患。根据实测结果 P3 850 在中断模式的系统调用上,比 P4 2G 有将近一倍的性能优势,而对 Xeon 等高端 CPU 来说中断的处理性能甚至更差。

Intel P6 vs P7 system call performance

这也是为什么从 Windows XP/2003 开始,MS 偷偷将 Intel 2E 的系统调用换成了 CPU 特殊的指令 sysenter (Intel) 和 syscall (AMD)。例如在 Win2003 系统中,NTDLL 中的系统调用已经不再使用 Int 0x2E,而改为调用固定地址上的系统调用代码:
以下内容为程序代码:

0:001> u ntdll!ZwSuspendProcess
ntdll!NtSuspendProcess:
77f335bb b806010000       mov     eax,0x106
77f335c0 ba0003fe7f       mov     edx,0x7ffe0300
77f335c5 ffd2             call    edx
77f335c7 c20400           ret     0x4

0:001> u 0x7ffe0300
SharedUserData!SystemCallStub:
7ffe0300 8bd4             mov     edx,esp
7ffe0302 0f34             sysenter
7ffe0304 c3               ret

0:001> u 7ffe0314
SharedUserData!SystemCallStub+0x14:
7ffe0314 8bd4             mov     edx,esp
7ffe0316 0f05             syscall
7ffe0318 c3               ret





对 Intel x86 架构来说,sysenter/sysexit 指令是从 PII 开始加入到指令集中,专门用于从用户态 (ring 1-3) 切换到核心态 (ring 0)。与普通的中断使用 IDT 或 call/jmp 直接给定目的地址不同,此系列命令直接从 CPU 相关的 MSR 寄存器中读取目标代码和堆栈的段选择符与地址偏移。因此只需要在系统加载的时候,一次性将这些设置好,就可以如上代码所示那样直接使用 sysenter 指令进行切换。正因为如此,使用 sysenter/sysexit 执行进行 ring 0 和 ring 3 之间切换,是在两个预定义好的稳定状态之间进行切换,所以无需进行中断处理时一系列的状态转换的特权检查,大大提高了切换处理的效率。而对 AMD 芯片,syscall 的实现原理基本类似。

为了适应这种变化,提高系统调用的效率,Linux 内核从 2.5.53 开始增加了对 sysenter/sysexit 模式的系统服务调用机制的支持。新增的 sysenter.c (arch/i386/kernel/) 中代码,会根据当前启动 CPU 是否支持 sysenter/sysexit 指令,动态判断是否启用支持。
以下内容为程序代码:

#define X86_FEATURE_SEP (0*32+11) /* SYSENTER/SYSEXIT */

static int __init sysenter_setup(void)
{
void *page = (void *)get_zeroed_page(GFP_ATOMIC);

__set_fixmap(FIX_VSYSCALL, __pa(page), PAGE_READONLY_EXEC);

if (!boot_cpu_has(X86_FEATURE_SEP)) {
  memcpy(page,
         &vsyscall_int80_start,
         &vsyscall_int80_end - &vsyscall_int80_start);
  return 0;
}

memcpy(page,
        &vsyscall_sysenter_start,
        &vsyscall_sysenter_end - &vsyscall_sysenter_start);

on_each_cpu(enable_sep_cpu, NULL, 1, 1);
return 0;
}

__initcall(sysenter_setup);




sysenter_setup 函数 (arch/i386/kernel/sysenter.c) 将在内核被加载时,获取一个只读并可执行的内存页,将基于 int 0x80 调用,或者 sysenter 调用的系统服务调用代码,加载到此内存页中。并根据情况,调用 on_each_cpu 函数对每个 CPU 启用 sysenter/sysexit 指令支持。
以下内容为程序代码:

#define MSR_IA32_SYSENTER_CS 0x174
#define MSR_IA32_SYSENTER_ESP 0x175
#define MSR_IA32_SYSENTER_EIP 0x176

extern asmlinkage void sysenter_entry(void);

void enable_sep_cpu(void *info)
{
int cpu = get_cpu();
struct tss_struct *tss = &per_cpu(init_tss, cpu);

tss->ss1 = __KERNEL_CS;
tss->esp1 = sizeof(struct tss_struct) + (unsigned long) tss;
wrmsr(MSR_IA32_SYSENTER_CS, __KERNEL_CS, 0);
wrmsr(MSR_IA32_SYSENTER_ESP, tss->esp1, 0);
wrmsr(MSR_IA32_SYSENTER_EIP, (unsigned long) sysenter_entry, 0);
put_cpu();
}




可以看到 enable_sep_cpu 函数 (arch/i386/kernel/sysenter.c) 实际上是为每个 CPU 设置其 MSR 寄存器的相关值,以便 sysenter 指令被调用时,能够直接切换到预定义(包括代码和堆栈的段选择符和偏移地址)的内核状态。关于 sysenter/sysexit 和 MSR 的相关资料,可以参考 Intel IA-32 开发手册的第三卷,系统编程手册,4.8.7 节和附录 B。
以下内容为程序代码:

.text
.globl __kernel_vsyscall
.type __kernel_vsyscall,@function
__kernel_vsyscall:
.LSTART_vsyscall:
push %ecx
.Lpush_ecx:
push %edx
.Lpush_edx:
push %ebp
.Lenter_kernel:
movl %esp,%ebp
sysenter

...




上述代码是 vsyscall-sysenter.S (arch/i386/kernel/) 中负责实际调用的,可以看到与前面 Win2003 的实现代码非常类似。

而与 Win2003 不同的是,Linux 的系统调用存在一个被中断的问题。也就是说在一个系统调用执行时,因为调用本身等待某种资源或被阻塞,调用没有完成时可能就被中断而强制返回 -EINTR。此时系统调用本身并没有发生错误,因此应该提供某种自动重试机制。这也就是为什么 vsyscall-sysenter.S 中,在 sysenter 下面还有如下处理代码的原因。
以下内容为程序代码:

.Lenter_kernel:
movl %esp,%ebp
sysenter

/* 7: align return point with nop's to make disassembly easier */
.space 7,0x90

/* 14: System call restart point is here! (SYSENTER_RETURN - 2) */
jmp .Lenter_kernel
/* 16: System call normal return point is here! */
.globl SYSENTER_RETURN /* Symbol used by entry.S.  */
SYSENTER_RETURN:
pop %ebp
.Lpop_ebp:
pop %edx
.Lpop_edx:
pop %ecx
.Lpop_ecx:
ret
.LEND_vsyscall:
.size __kernel_vsyscall,.-.LSTART_vsyscall




这里 sysenter 代码之后,实际上存在两个返回点:jmp .Lenter_kernel 指令是在调用被中断时的返回点;SYSENTER_RETURN 则是调用正常结束时的返回点。Linux 内核通过在实际调用函数中进行判断,调整返回地址 EIP 的方法,解决了这个自动重试的问题。
Linus 在一篇邮件列表的讨论中解释了这个问题。具体关于系统调用中断与重试等机制的解释,可以参考 《The Linux Kernel》一书的 4.5 节。

不过因为某些原因,Linux 2.6 内核仍然没有把 _syscall0 那套函数改用新的调用方法,而是在现有机制之外,增加了一套名为 vsyscall 的扩展机制,专供对系统服务调用效率要求较高的服务使用。这种机制实际上是将一部分固定地址的内核空间虚拟内存页面,直接暴露并允许用户态进行访问。也就是前面 sysenter_setup 函数中的 __set_fixmap 调用。
以下内容为程序代码:

#define __FIXADDR_TOP 0xfffff000

#define FIXADDR_TOP ((unsigned long)__FIXADDR_TOP)

#define PAGE_SHIFT 12

#define __fix_to_virt(x) (FIXADDR_TOP - ((x) << PAGE_SHIFT))

void __set_fixmap (enum fixed_addresses idx, unsigned long phys, pgprot_t flags)
{
unsigned long address = __fix_to_virt(idx);

if (idx >= __end_of_fixed_addresses) {
  BUG();
  return;
}
set_pte_pfn(address, phys >> PAGE_SHIFT, flags);
}

enum fixed_addresses {
FIX_HOLE,
FIX_VSYSCALL,
  ...
};

static int __init sysenter_setup(void)
{
...
__set_fixmap(FIX_VSYSCALL, __pa(page), PAGE_READONLY_EXEC);
...
}




可以看到 __set_fixmap 函数实际上是把 0xfffff000 开始的若干页虚拟内存地址,固定分配给 vsyscall 等不同用途的功能,由其自行分配物理内存并放置功能代码。对 FIX_VSYSCALL 这块内存,内存管理模块会特殊对待。
以下内容为程序代码:

/*
* This is the range that is readable by user mode, and things
* acting like user mode such as get_user_pages.
*/
#define FIXADDR_USER_START (__fix_to_virt(FIX_VSYSCALL))
#define FIXADDR_USER_END (FIXADDR_USER_START + PAGE_SIZE)

int in_gate_area(struct task_struct *task, unsigned long addr)
{
#ifdef AT_SYSINFO_EHDR
if ((addr >= FIXADDR_USER_START) && (addr < FIXADDR_USER_END))
  return 1;
#endif
return 0;
}

int get_user_pages(...)
{
...
vma = find_extend_vma(mm, start);
if (!vma && in_gate_area(tsk, start)) {
  ...
  if (write) /* user gate pages are read-only */
  return i ? : -EFAULT;
  ...
}
...
}




对 vsyscall 这一页内存,get_user_pages 函数将直接允许用户态的读操作。因此完全可以从用户态通过 call FIXADDR_USER_START 类似的代码,通过基于 sysenter/sysexit 的系统服务调用机制,快速执行指定服务号的系统服务。如 《The Linux Kernel》一书给出了一个例子:
以下内容为程序代码:

#include <stdio.h>

int pid;

int main() {
        __asm__(
                "movl 20, %eax    \n"
                "call 0xffffe400   \n"
                "movl %eax, pid    \n"
        ;
        printf("pid is %d\n", pid);
        return 0;
}




而这种机制的系统服务调用,据其测试能有将近一倍的性能提升。

以下为引用:

An example of the kind of timing differences: John Stultz reports on an experiment where he measures gettimeofday() and finds 1.67 us for the int 0x80 way, 1.24 us for the sysenter way, and 0.88 us for the vsyscall.)
作者: obrire    时间: 2006-04-14 12:14
当前的LKM中,Linux内核是尽可以地使用你的物理内存,以便系统响应快速
但有一个致命的缺限,如果你在启用X-Windows以及一系统大型应用时
从free看出你的物理内存已经很少了,这时采用LKM动态加载内存需求较大的内核模块时
不一定能成功,这非常要命,当不能得到页面时,__get_free_pages会触发VMA将
快速交换,此时大块物理内存被释放,但系统本身却一直与外存持续交换,系统响应变得
很慢,物别是在GUI模式下。即便如此,你再加载LKM时,还是不能成功,这是不可以理解的。

已经有不少朋友在mlist中提出,这必须得将VMA进行修正。

有时,我在init 3/init 1下工作正常,而init 5时,动态加载几乎不成功。
这对于开发而言,是不可思议的
作者: xiaozhaoz    时间: 2006-04-14 12:19
sysenter这种实现我早就知道了,这时一个几年前的东西了, 我也知道他的效率很高.
其实直接操作/dev/mem的事情我很早就用过, 但是对于实际开发来说.

更改所有用户空间的内存管理方法, 直接操作物理内存,  你怎么保证系统库能正常运行?  如果你不需要系统库, 你编写上层应用程序, 是否要替换所有内存操作代码? 对于分配的所有物理页,你是否要自己管理他的碎片, 等问题? 你是否考虑到了这样的改动对于所有的上层应用程序来说, 有多少附加的工作量?

为什么我用mlock, 只是说把mlock可以用来在用户和内核之间通信使用. 请问,你一次有多少数据量要在用户和内核之间交互?   我现在的使用中都是足够的. 而这种方法的改动是最小的, 效率也不比你提到的低.

对于用户空间任务之间的数据交互, 用线程可以减少很多麻烦.
作者: 思一克    时间: 2006-04-14 12:30
to obrire,

我看了你的做法,觉得不错。
比如,在用户空间malloc()了32M虚拟地址,如何在用户空间知道其物理开始地址并且打开/dev/mem线性操作相应的地址?
作者: xiaozhaoz    时间: 2006-04-14 12:30
另外, 你多次提到有人建议linus修改vma和mm的实现, 能否给出讨论的链接地址, 多谢!
作者: 思一克    时间: 2006-04-14 12:53
to orbire

是不是这样:

m = open("/dev/mem", ...);
lseek(m, 1204, 0);
read(m, buf, sizeof(buf));
write(m, "123456", 6);

直接操作物理内存?


"
在内核空间申请
在用户空间不用mmap
直接open /dev/mem
lseek xxxxx
一切就哪样简单,就像汇编,写进去就行了.
这可是直接物理内存操作呀.
其实在性能要求高的环境,不需要外存的.有些Super Computer就是这样的.
结果直接就通过网络送走了.
"
作者: obrire    时间: 2006-04-14 16:29
原帖由 思一克 于 2006-4-14 12:53 发表
to orbire

是不是这样:

m = open("/dev/mem", ...);
lseek(m, 1204, 0);
read(m, buf, sizeof(buf));
write(m, "123456", 6);

直接操作物理内存?


"
在内核空间申请 ...

正是,当你得先知道确切的物理地址, 也就是OFF_SET,一般我的系统分配在0x600000以后.
我采用在/proc/mem_window输出一个地址及尺寸, 然就这样操作了
作者: 思一克    时间: 2006-04-14 16:41
to orbire,

那你了解read write /dev/mem还要调用copy_fro_user, copy_to_user?

/dev/mem仅仅是一个影射物理内存的字符设备,读写它要通过文件系统的许多调用,最后才调用read_mem, 和write_mem, 然后就是copy_from/to_user,怎么能比mmap快?


原帖由 obrire 于 2006-4-14 16:29 发表

正是,当你得先知道确切的物理地址, 也就是OFF_SET,一般我的系统分配在0x600000以后.
我采用在/proc/mem_window输出一个地址及尺寸, 然就这样操作了

作者: obrire    时间: 2006-04-14 16:51
原帖由 思一克 于 2006-4-14 12:30 发表
to obrire,

我看了你的做法,觉得不错。
比如,在用户空间malloc()了32M虚拟地址,如何在用户空间知道其物理开始地址并且打开/dev/mem线性操作相应的地址?

首先,你要得到用户空间地址在内核空间的入口, 用逻辑地址并减去PAGE_OFFSET才能得到物理地址.
与高端内存的关系,你要看看你当前版本的源定义,不一而足

如果纯粹在用户空间用malloc, 并不能保证你能得到性线物理地址.

因此才有我说的,先在内核模块中规划, 然后在用户空间自由使用

所以在用户空间用malloc是无法保证性能和线性的.在VMA中可以保证你看起来是线性的.
但内核实现, 完全可以采用relayfs(2.6.14以后加入)方式,用多组不同大小的页面组来完成映射(list),
只是你在用户空间感觉不到.

如果你想这样使用, 建议你也先在内核空间划出一块来, 然后在用户空间操作.

我是因为我的PCI总线的上设备要求在相关Register置入物理地址, 并要求在块线性空间,才不得不这样呀.
主要是方便DMA传送. 如果全在内核模块中做,有些功能不方便,不如用户空间编程方便,所以行此法.

只是这方面你的代码千万不能越界,如果打开/dev/mem以后,在其它区间写入了数据,
会让系统变得不可思议了.

我所讲的无非三点
1. 方便在用户空间直接操作物理内存
2. 线性读写/块传输
3. 内核模块中实现了DMA传输完成IRQ响应,用户程序完全可以while(1)直接操作
使得编程接口简单高效, 就像实时preview高速视频一样.其它线程编码不受影响
至于同步问题, 用什么机制, 尊重你自己的选择,别让work_queue锁死了.如果有硬时钟就好了
应用层可以多路POOLING,内核层采用中断
作者: obrire    时间: 2006-04-14 16:52
标题: 回复 27楼 思一克 的帖子
当然不会了,就是不用copy_to_user这类函数了
要写什么你直接置入就可以

只是这种管理状态,如有些区域设置同步位,要求自己要有较高的编程管理能力
混合模型: 每次同记录在/proc/mem_window中,锁一打开,应用就读写或刷新.
这不影响系统其它应用,全在你自己的空间,或者同步块标志在这片物理区更好,性能
上更有效一些.

[ 本帖最后由 obrire 于 2006-4-14 16:56 编辑 ]
作者: 思一克    时间: 2006-04-14 16:55
to obrire,

内核中自己的VMA的物理地址是连续的吗?


"所以在用户空间用malloc是无法保证性能和线性的.在VMA中可以保证你看起来是线性的.
但内核实现, 完全可以采用relayfs(2.6.14以后加入)方式,用多组不同大小的页面组来完成映射(list),
只是你在用户空间感觉不到.
"
作者: 思一克    时间: 2006-04-14 16:57
可是你
m = open("/dev/mem", ...)
read(m, .....)

本身就是要调用copy_to_user. 那能快的起来吗,怎么能是“比mmap更快的方法”?
作者: obrire    时间: 2006-04-14 17:00
标题: 回复 30楼 思一克 的帖子
当然不连续,正是因为如此,我才改进的.

最初我也采用vmalloc在内核中分配,虽然linus也说这都会成功,的确如此.
不过我的CHIP中的Register中是不能填入vmalloc转换出来的物理地址的.
CHIP的驱动以为是线性块,实际上不是,全乱了.

因此我得100%保证我的地址是线性的,因此修改了实现,并不需对Kernel进行patch
不会影响别人.
作者: obrire    时间: 2006-04-14 17:03
原帖由 思一克 于 2006-4-14 16:57 发表
可是你
m = open("/dev/mem", ...)
read(m, .....)

本身就是要调用copy_to_user. 那能快的起来吗,怎么能是“比mmap更快的方法”?

所以我不会用read/write

memset area
*(DWORD *)addr = 0x000000f
这不会更简单吧.
作者: 思一克    时间: 2006-04-14 17:05
to obrire,

内核中的VMA (vmlist)的的虚拟地址 减去 PAGE_OFFSET 也不等于物理地址啊?你如何算出来的
作者: obrire    时间: 2006-04-14 17:06
原帖由 思一克 于 2006-4-14 17:05 发表
to obrire,

内核中的VMA (vmlist)的的虚拟地址 减去 PAGE_OFFSET 也不等于物理地址啊?你如何算出来的



_pa(addr)
作者: 思一克    时间: 2006-04-14 17:07
那你open("/dev/mem", ...)之后,又不read/write了。

去memset哪个地方?


"所以我不会用read/write

memset area
*(DWORD *)addr = 0x000000f
这不会更简单吧. "
作者: albcamus    时间: 2006-04-14 17:08
边看边学
作者: 思一克    时间: 2006-04-14 17:10
to orbire,

_pa(x)不能用于kernel的VMA(vmalloc得到的)地址。无效的
作者: 思一克    时间: 2006-04-14 17:11
下班了。
作者: obrire    时间: 2006-04-14 17:13
原帖由 思一克 于 2006-4-14 17:07 发表
那你open("/dev/mem", ...)之后,又不read/write了。

去memset哪个地方?


"所以我不会用read/write

memset area
*(DWORD *)addr = 0x000000f
这不会更简单吧. "

我当时测试只是稍修改了mapper
方便测试,我加了一句,置/dev/mem 0x600000 0x400000 c
后一看,这段内存全是c

如果没有文件的instance
直接赋予的地址Kernel会以为是虚地址
所以打开是必须的.
也就是说,open /dev/mem
lseek
这几句主要是定位
后面操作时,最好是memcpy/memset等.
如果采用vfs的read/write就没必要了

而且就是文件操作,也并非实现write/read就会调用copy_to_user之类呀
在我的/proc/mem_window中记录文件,实际只返回了物理地址指针及len
其实重写了read/write的operation

[ 本帖最后由 obrire 于 2006-4-14 17:27 编辑 ]
作者: HHLQ    时间: 2006-04-14 21:42
好精彩,好激烈,好佩服,好郁闷--不知道在说什么:D
作者: obrire    时间: 2006-04-17 10:37
原帖由 思一克 于 2006-4-14 17:10 发表
to orbire,

_pa(x)不能用于kernel的VMA(vmalloc得到的)地址。无效的


要得到连续的,线性逻辑地址,就不能用vmalloc方式。
驱动建议有两种,一种是采用预留,一种是采用scatter/gather,但要设备I/O支持才行,
如果支持Direct I/O,对于Huge Buffer可采用scatter/gather模式。

如果你要强行采用GFP_NOFAIL,这会冒很大风险,有时会严重阻塞系统。

要采用物理线性,得用kmalloc或者是__get_free_pages之类才行,这时利用_pa(x)
作者: obrire    时间: 2006-04-17 10:46
原帖由 思一克 于 2006-4-14 16:57 发表
可是你
m = open("/dev/mem", ...)
read(m, .....)

本身就是要调用copy_to_user. 那能快的起来吗,怎么能是“比mmap更快的方法”?


如果用mmap也是可以的,看你自己怎么去实现细节。
1. 可以直接操作文件/dev/mem
2. 直接操作地址
3. 物理地址采用ioremap重映射,在哪儿用都行

说明:
     之所以写这些,主要是为朋友们提供一种新的选择:本着实用、方便、快捷的宗旨。
     无非强调一点,在有些不适合VMA机制的模式下,又不能像mem=xxxM方式来预留,
     又需要huge buffer的线性模式(DMA),又不希望像scatter/gather I/O哪么麻烦,
     我们提供了一种新的可行路径。

     望对诸君有益
     ===================================================
     如果前面有些发言有过激之处,望海涵!
作者: 思一克    时间: 2006-04-17 10:48
那到底什么是快速操作物理内存?

open("/dev/mem",,,)  read/write已经被否定了。
mmap你说过不是最快的。

如何最快--物理内存
作者: obrire    时间: 2006-04-17 10:52
原帖由 思一克 于 2006-4-17 10:48 发表
那到底什么是快速操作物理内存?

open("/dev/mem",,,)  read/write已经被否定了。
mmap你说过不是最快的。

如何最快--物理内存

ioremap
只是让你的应用在用户空间及内核空间看起来是合法的
其实你都知道,这块内存你完全可以自己管理,只是不用
时,让内核回收。

有时纯地址操作是不允许的,如果可以,那当然最快了。
用ioremap,只是让映射后的虚址在你的应用所在的空间。

[ 本帖最后由 obrire 于 2006-4-17 10:54 编辑 ]
作者: 思一克    时间: 2006-04-17 11:40
ioremap好象不是做那个用途的吧。
另外,ioremap得到的虚拟地址如何转换为物理地址? 还有调用ioremap如何使返回地址在你的应用空间?
作者: obrire    时间: 2006-04-17 12:30
原帖由 思一克 于 2006-4-17 11:40 发表
ioremap好象不是做那个用途的吧。
另外,ioremap得到的虚拟地址如何转换为物理地址? 还有调用ioremap如何使返回地址在你的应用空间?


iomem *address = ioremap(0x600000/*物理地址*/, 0x200000/*尺寸*/)

此处的address已经是虚地址了
作者: obrire    时间: 2006-04-17 12:34
标题: 回复 46楼 思一克 的帖子
如果不习惯,用out/in之类都可以
不过还是memset/memcpy之类来的快
作者: 思一克    时间: 2006-04-17 12:38
address在应用的空间吗

原帖由 obrire 于 2006-4-17 12:30 发表


iomem *address = ioremap(0x600000/*物理地址*/, 0x200000/*尺寸*/)

此处的address已经是虚地址了

作者: obrire    时间: 2006-04-17 15:25
标题: 回复 49楼 思一克 的帖子
是的
作者: 思一克    时间: 2006-04-17 16:04
to obrire,

调用ioremap()返回的地址是VMALLOC_START以上的地址,根本不在用户空间。也不在KERNEL的固定映射空间。
作者: obrire    时间: 2006-04-17 21:04
标题: 回复 51楼 思一克 的帖子
LDD3中说过,这相当于I/O操作,而这是可行的.
而且是通过了测试的




欢迎光临 Chinaunix (http://bbs.chinaunix.net/) Powered by Discuz! X3.2