免费注册 查看新帖 |

Chinaunix

  平台 论坛 博客 文库
最近访问板块 发新帖
楼主: obrire
打印 上一主题 下一主题

快速内存操作技术 [复制链接]

论坛徽章:
0
11 [报告]
发表于 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 编辑 ]

论坛徽章:
0
12 [报告]
发表于 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等,没兴趣,因为好多同事都做过了,或者优化过了,只是商业代码不公开
而已。


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

看起老是长不大
我总以为自己是聪明的,其实发觉世界已经没有傻蛋

论坛徽章:
0
13 [报告]
发表于 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 编辑 ]

论坛徽章:
0
14 [报告]
发表于 2006-04-14 10:31 |只看该作者
原帖由 xiaozhaoz 于 2006-4-14 09:14 发表

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



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

论坛徽章:
0
15 [报告]
发表于 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, 而且 操作系统要扩展的足够好才行.

论坛徽章:
0
16 [报告]
发表于 2006-04-14 10:57 |只看该作者
如果lz对阅读代码没有兴趣,我想我是找错了讨论对象了. 我只懂技术,  只会看代码和些软件.

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

论坛徽章:
0
17 [报告]
发表于 2006-04-14 11:01 |只看该作者
如果lz对阅读代码没有兴趣,我想我是找错了讨论对象了. 我只懂技术,  只会看代码和些软件.

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

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

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

在Linux内核中实现的方案, 用商业技术来推脱, 有点可笑和羞辱. 如果是机密的商业方案, 还是不要拿出来说为妙.

论坛徽章:
0
18 [报告]
发表于 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

论坛徽章:
0
19 [报告]
发表于 2006-04-14 11:32 |只看该作者
我没看ldd3, 我只看过内核代码.

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

那么和我提出的方案一样,为什么不用mlock呢? 这不是更快更方便吗?

论坛徽章:
0
20 [报告]
发表于 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.)
您需要登录后才可以回帖 登录 | 注册

本版积分规则 发表回复

  

北京盛拓优讯信息技术有限公司. 版权所有 京ICP备16024965号-6 北京市公安局海淀分局网监中心备案编号:11010802020122 niuxiaotong@pcpop.com 17352615567
未成年举报专区
中国互联网协会会员  联系我们:huangweiwei@itpub.net
感谢所有关心和支持过ChinaUnix的朋友们 转载本站内容请注明原作者名及出处

清除 Cookies - ChinaUnix - Archiver - WAP - TOP