linux内存

在Linux的世界中,从大的方面来讲,有两块内存,一块叫做内存空间,Kernel Space,另一块叫做用户空间,即User Space。它们是相互独立的,Kernel对它们的管理方式也完全不同

 

驱动模块和内核本身运行在Kernel Space当中

 

 

一 linux内存模型

 

Linux内存管理系统主要解决以下三个大的问题

 

  1. 1.    进程地址空间不能隔离

由于程序直接访问的是物理内存,这个时候程序所使用的内存空间不是隔离的。举个例子,就像上面说的A的地址空间是0-10M这个范围内,但是如果A中有一 段代码是操作10M-128M这段地址空间内的数据,那么程序B和程序C就很可能会崩溃(每个程序都可以系统的整个地址空间)。这样很多恶意程序或者是木 马程序可以轻而易举的破快其他的程序,系统的安全性也就得不到保障了,这对用户来说也是不能容忍的。

  1. 2.    内存使用的效率低

如上面提到的,如果我们要像让程序A、B、C同时运行,那么唯一的方法就是使用虚拟内存技术将一些程序暂时不用的数据写到磁盘上,在需要的时候再从磁盘读 回内存。这里程序C要运行,将A交换到磁盘上去显然是不行的,因为程序是需要连续的地址空间的,程序C需要20M的内存,而A只有10M的空间,所以需要 将程序B交换到磁盘上去,而B足足有100M,可以看到为了运行程序C我们需要将100M的数据从内存写到磁盘,然后在程序B需要运行的时候再从磁盘读到 内存,我们知道IO操作比较耗时,所以这个过程效率将会十分低下。

  1. 3.    程序运行的地址不能确定

程序每次需要运行时,都需要在内存中非配一块足够大的空闲区域,而问题是这个空闲的位置是不能确定的,这会带来一些重定位的问题,重定位的问题确定就是程序中引用的变量和函数的地址,如果有不明白童鞋可以去查查编译愿意方面的资料。

 

这里引用计算机界一句无从考证的名言:“计算机系统里的任何问题都可以靠引入一个中间层来解决。”

 

程序和物理内存之间引入了虚拟内存

 

1.分段

分段(Segmentation):这种方法是人们最开始使用的一种方法,基本思路是将程序所需要的内存地址空间大小的虚拟空间映射到某个
物理地址空间。

 

段映射机制

每个程序都有其独立的虚拟的独立的进程地址空间,可以看到程序A和B的虚拟地址空间都是从0x00000000开始的。我们将两块大小相同的虚拟地 址空间和实际物理地址空间一一映射,即虚拟地址空间中的每个字节对应于实际地址空间中的每个字节,这个映射过程由软件来设置映射的机制,实际的转换由硬件 来完成。

这种分段的机制解决了文章一开始提到的3个问题中的进程地址空间隔离和程序地址重定位的问题。程序A和程序B有自己独立的虚拟地址空间,而且该虚拟 地址空间被映射到了互相不重叠的物理地址空间,如果程序A访问虚拟地址空间的地址不在0x00000000-0x00A00000这个范围内,那么内核就 会拒绝这个请求,所以它解决了隔离地址空间的问题。我们应用程序A只需要关心其虚拟地址空间0x00000000-0x00A00000,而其被映射到哪 个物理地址我们无需关心,所以程序永远按照这个虚拟地址空间来放置变量,代码,不需要重新定位。

 

2分页

分页机制就是把内存地址空间分为若干个很小的固定大小的页,每一页的大小由内存决定,就像Linux中ext文件系统将磁盘分成若干个Block一样,这 样做是分别是为了提高内存和磁盘的利用率。试想以下,如果将磁盘空间分成N等份,每一份的大小(一个Block)是1M,如果我想存储在磁盘上的文件是 1K字节,那么其余的999字节是不是浪费了。所以需要更加细粒度的磁盘分割方式,我们可以将Block设置得小一点,这当然是根据所存放文件的大小来综 合考虑的,好像有点跑题了,我只是想说,内存中的分页机制跟ext文件系统中的磁盘分割机制非常相似。

分页机制的原理,当然Linux中的分页机制的实现还是比较复杂的,通过了也全局目录,也上级目录,页中级目录,页表等几级的分页机制来实现的,但是基本的工作原理是不会变的。

分页机制的实现需要硬件的实现,这个硬件名字叫做MMU(Memory Management Unit),他就是专门负责从虚拟地址到物理地址转换的,也就是从虚拟页找到物理页。

3缺页异常

缺页异常处理。基于 CPU 的这一特性,Linux 采用了请求调页(Demand Paging)和写时复制(Copy On Write)的技术

 1.请求调页是 一种动态内存分配技术,它把页框的分配推迟到不能再推迟为止。这种技术的动机是:进程开始运行的时候并不访问地址空间中的全部内容。事实上,有一部分地址 也许永远也不会被进程所使用。程序的局部性原理也保证了在程序执行的每个阶段,真正使用的进程页只有一小部分,对于临时用不到的页,其所在的页框可以由其 它进程使用。因此,请求分页技术增加了系统中的空闲页框的平均数,使内存得到了很好的利用。从另外一个角度来看,在不改变内存大小的情况下,请求分页能够 提高系统的吞吐量。当进程要访问的页不在内存中的时候,就通过缺页异常处理将所需页调入内存中。

  2.写时复制主 要应用于系统调用fork,父子进程以只读方式共享页框,当其中之一要修改页框时,内核才通过缺页异常处理程序分配一个新的页框,并将页框标记为可写。这 种处理方式能够较大的提高系统的性能,这和Linux创建进程的操作过程有一定的关系。在一般情况下,子进程被创建以后会马上通过系统调用 execve将一个可执行程序的映象装载进内存中,此时会重新分配子进程的页框。那么,如果fork的时候就对页框进行复制的话,显然是很不合适的。

   在上述的两种情况下出现缺页异常,进程运行于用户态,异常处理程序可以让进程从出现异常的指令处恢复执行,使用户感觉不到异常的发生。当然,也会有异常 无法正常恢复的情况,这时,异常处理程序会进行一些善后的工作,并结束该进程。也就是说,运行在用户态的进程如果出现缺页异常,不会对操作系统核心的稳定 性造成影响

 

二 linux内存管理

Free命令

作为一名Linux系统管理员,监控内存的使用状态是非常重要的,通过监控有助于了解内存的使用状态,比如内存占用是否正常,内存是否紧缺等等,监控内存最常使用的命令有free、top等,下面是某个系统free的输出:

 

每个选项的含义:

第一行:

total:物理内存的总大小

used:已经使用的物理内存大小

free:空闲的物理内存大小

shared:多个进程共享的内存大小

buffers/cached:磁盘缓存的大小

第二行Mem:代表物理内存使用情况

第三行(-/+ buffers/cached):代表磁盘缓存使用状态

第四行:Swap表示交换空间内存使用状态

free命令输出的内存状态,可以通过两个角度来查看:一个是从内核的角度来看,一个是从应用层的角度来看的。

内核的角度:内核目前可以直接分配到,不需要额外的操作,即为上面free命令输出中第二行Mem项的值,可以看出,此系统物理内存有3894036K,空闲的内存只有420492K,也就是40M多一点

应用层的角度:对于应用程序来说,buffers/cached占有的内存是可用的,因为buffers/cached是为了提高文件读取的性能,当应用程序需要用到内存的时候,buffers/cached会很快地被回收,以供应用程序使用

buffers与cached的异同

在Linux 操作系统中,当应用程序需要读取文件中的数据时,操作系统先分配一些内存,将数据从磁盘读入到这些内存中,然后再将数据分发给应用程序;当需要往文件中写 数据时,操作系统先分配内存接收用户数据,然后再将数据从内存写到磁盘上。然而,如果有大量数据需要从磁盘读取到内存或者由内存写入磁盘时,系统的读写性 能就变得非常低下,因为无论是从磁盘读数据,还是写数据到磁盘,都是一个很消耗时间和资源的过程,在这种情况下,Linux引入了buffers和 cached机制。

buffers与cached都是内存操作,用来保存系统曾经打开过的文件以及文件属性信息,这样当操作系统需要读取某些文件时,会首先在 buffers与cached内存区查找,如果找到,直接读出传送给应用程序,如果没有找到需要数据,才从磁盘读取,这就是操作系统的缓存机制,通过缓 存,大大提高了操作系统的性能。但buffers与cached缓冲的内容却是不同的。

buffers是用来缓冲块设备做的,它只记录文件系统的元数据(metadata)以及 tracking in-flight pages,而cached是用来给文件做缓冲。更通俗一点说:buffers主要用来存放目录里面有什么内容,文件的属性以及权限等等。而cached直接用来记忆我们打开过的文件和程序

top命令

使用top命令。在top的输出中,SIZE显示了每个程序的虚地址空间的大小(您的整个程序代码、数据、栈,其中一些应该已被交换出到交换区间)。RSS 列(Resident set size,持久集合大小)显示了程序所占用的的物理内存大小。所有当前运行程序的 RSS 数值总和不会超过您的计算机物理内存大小,并且所有地址空间的大小限制值为2GB(对于32字节版本的Linux来说)

 

三 linux内存实现

glibc 内存管理的实现,特别是高并发性能低下和内存碎片化问题都比较严重,因此,陆续出现一些第三方工具来替换 glibc 的实现,最著名的当属 google 的tcmalloc和facebook 的jemalloc 。

tcmalloc主要是为了多线程设计的

 

 malloc调用之后

在glibc实现的内存管理算法中,

Malloc小块内存是在小于0x4000 0000的内存中分配的,通过brk/sbrk不断向上扩展,

请求内存大于128K,而分配大块内存,malloc直接通过系统调用mmap实现,分配得到的地址在文件映射区

 

brk分配的内存需要等到高地址内存释放以后才能释放(例如,在B释放之前,A是不可能释放的),mmap分配的内存可以单独释放

 

Linux下默认栈的大小限制是10M

window下默认栈的大小限制是1M 可以修改

Mmap 分配大空间内存

mmap将一个文件或者其它对象映射进内存。文件被映射到多个页上,如果文件的大小不是所有页的大小之和,最后一个页不被使用的空间将会清零。mmap在用户空间映射调用系统中作用很大

#include<sys/mman.h>

 

Void *mmap(void*start,

size_t length,

int prot,

int flags,

int fd,

off_t offset);

 

int munmap(void*start,size_tlength);

 

start:映射区的开始地址,设置为0时表示由系统决定映射区的起始地址。

 

length:映射区的长度。//长度单位是 以字节为单位,不足一内存页按一内存页处理

 

prot:期望的内存保护标志,不能与文件的打开模式冲突。是以下的某个值,可以通过or运算合理地组合在一起

PROT_EXEC //页内容可以被执行

PROT_READ //页内容可以被读取

PROT_WRITE //页可以被写入

PROT_NONE //页不可访问

 

flags:指定映射对象的类型,映射选项和映射页是否可以共享。它的值可以是一个或者多个以下位的组合体

MAP_FIXED //使用指定的映射起始地址,如果由start和len参数指定的内存区重叠于现存的映射空间,重叠部分将会被丢弃。如果指定的起始地址不可用,操作将会失败。并且起始地址必须落在页的边界上。

MAP_SHARED //与其它所有映射这个对象的进程共享映射空间。对共享区的写入,相当于输出到文件。直到msync()或者munmap()被调用,文件实际上不会被更新。

MAP_PRIVATE //建立一个写入时拷贝的私有映射。内存区域的写入不会影响到原文件。这个标志和以上标志是互斥的,只能使用其中一个。

MAP_DENYWRITE //这个标志被忽略。

MAP_EXECUTABLE //同上

MAP_NORESERVE //不要为这个映射保留交换空间。当交换空间被保留,对映射区修改的可能会得到保证。当交换空间不被保留,同时内存不足,对映射区的修改会引起段违例信号。

MAP_LOCKED //锁定映射区的页面,从而防止页面被交换出内存。

MAP_GROWSDOWN //用于堆栈,告诉内核VM系统,映射区可以向下扩展。

MAP_ANONYMOUS //匿名映射,映射区不与任何文件关联。

MAP_ANON //MAP_ANONYMOUS的别称,不再被使用。

MAP_FILE //兼容标志,被忽略。

MAP_32BIT //将映射区放在进程地址空间的低2GB,MAP_FIXED指定时会被忽略。当前这个标志只在x86-64平台上得到支持。

MAP_POPULATE //为文件映射通过预读的方式准备好页表。随后对映射区的访问不会被页违例阻塞。

MAP_NONBLOCK //仅和MAP_POPULATE一起使用时才有意义。不执行预读,只为已存在于内存中的页面建立页表入口。

 

fd:有效的文件描述词。一般是由open()函数返回,其值也可以设置为-1,此时需要指定flags参数中的MAP_ANON,表明进行的是匿名映射。

 

off_toffset:被映射对象内容的起点。

 

mmap()必须以PAGE_SIZE()为单位进行映射,而内存也只能以页为单位进行映射,若要映射非PAGE_SIZE整数倍的地址范围,要先进行内存对齐,强行以PAGE_SIZE的倍数大小进行映射。

start:映射区的开始地址,设置为0时表示由系统决定映射区的起始地址。

length:映射区的长度。//长度单位是 以字节为单位,不足一内存页按一内存页处理

 

使用mmap分配大空间内存后,需要锁住内存,有mlock操作只能root用户进行

锁住内存是为了防止这段内存被操作系统swap掉。并且由于此操作风险高,仅超级用户可以执行

#include <sys/mman.h>

       int mlock(const void *addr, size_t len);

       int munlock(const void *addr, size_t len);

       int mlockall(int flags);

       int munlockall(void);

仅分配内存并调用 mlock 并不会为调用进程锁定这些内存,因为对应的分页可能是写时复制(copy-on-write)5。因此,你应该在每个页面中写入一个假的值这样针对每个内存分页的写入操作会强制 Linux 为当前进程分配一个独立、私有的内存页

 

既然堆内内存brk和sbrk不能直接释放,为什么不全部使用 mmap 来分配,munmap直接释放呢? 
        既 然堆内碎片不能直接释放,导致疑似“内存泄露”问题,为什么 malloc 不全部使用 mmap 来实现呢(mmap分配的内存可以会通过 munmap 进行 free ,实现真正释放)?而是仅仅对于大于 128k 的大块内存才使用 mmap ? 

        其实,进程向 OS 申请和释放地址空间的接口 sbrk/mmap/munmap 都是系统调用,频繁调用系统调用都比较消耗系统资源的。并且, mmap 申请的内存被 munmap 后,重新申请会产生更多的缺页中断。例如使用 mmap 分配 1M 空间,第一次调用产生了大量缺页中断 (1M/4K 次 ) ,当munmap 后再次分配 1M 空间,会再次产生大量缺页中断。缺页中断是内核行为,会导致内核态CPU消耗较大。另外,如果使用 mmap 分配小内存,会导致地址空间的分片更多,内核的管理负担更大。
        同时堆是一个连续空间,并且堆内碎片由于没有归还 OS ,如果可重用碎片,再次访问该内存很可能不需产生任何系统调用和缺页中断,这将大大降低 CPU 的消耗。 因此, glibc 的 malloc 实现中,充分考虑了 sbrk 和 mmap 行为上的差异及优缺点,默认分配大块内存 (128k) 才使用 mmap 获得地址空间,也可通过 mallopt(M_MMAP_THRESHOLD, <SIZE>) 来修改这个临界值。

 

 

四 内存分配多线程安全

原子操作,就是该操作绝不会在执行完毕前被任何其他任务或事件打断,也就说,它的最小的执行单位,不可能有比它更小的执行单位

 

原子操作需要硬件的支持,因此是架构相关的,其API和原子类型的定义都定义在内核源码树的include/asm/atomic.h文件中,它们都使用汇编语言实现,因为C语言并不能实现这样的操作

 

定义在include/asm/atomic.h中。 用户程序include它,在自己控制CONFIG_SMP定义。

在单处理器时 atomic_inc() 就是incl xxxx
在CONFIG_SMP时, atomic_inc()是 lock incl xxxx

因此incl XXXX在SMP时不是原子的。必须用lock.

 

 

 

typedef struct  {  volatile int counter;  }  atomic_t;


  volatile修饰字段告诉gcc不要对该类型的数据做优化处理,对它的访问都是对内存的访问,而不是对寄存器的访问。

 

  原子操作API包括:

atomic_read(atomic_t * v);


  该函数对原子类型的变量进行原子读操作,它返回原子类型的变量v的值。

 

 

atomic_set(atomic_t * v, int i);


  该函数设置原子类型的变量v的值为i。

 

 

void atomic_add(int i, atomic_t *v);


  该函数给原子类型的变量v增加值i。

 

 

atomic_sub(int i, atomic_t *v);


  该函数从原子类型的变量v中减去i。

 

 

int atomic_sub_and_test(int i, atomic_t *v);


  该函数从原子类型的变量v中减去i,并判断结果是否为0,如果为0,返回真,否则返回假。

 

 

void atomic_inc(atomic_t *v);


  该函数对原子类型变量v原子地增加1。

 

 

void atomic_dec(atomic_t *v);


  该函数对原子类型的变量v原子地减1。

 

 

int atomic_dec_and_test(atomic_t *v);


  该函数对原子类型的变量v原子地减1,并判断结果是否为0,如果为0,返回真,否则返回假。

 

 

int atomic_inc_and_test(atomic_t *v);


  该函数对原子类型的变量v原子地增加1,并判断结果是否为0,如果为0,返回真,否则返回假。

 

 

int atomic_add_negative(int i, atomic_t *v);


  该函数对原子类型的变量v原子地增加I,并判断结果是否为负数,如果是,返回真,否则返回假。

 

 

int atomic_add_return(int i, atomic_t *v);


  该函数对原子类型的变量v原子地增加i,并且返回指向v的指针。

 

 

int atomic_sub_return(int i, atomic_t *v);


  该函数从原子类型的变量v中减去i,并且返回指向v的指针。

 

 

int atomic_inc_return(atomic_t * v);


  该函数对原子类型的变量v原子地增加1并且返回指向v的指针。

 

 

int atomic_dec_return(atomic_t * v);

  该函数对原子类型的变量v原子地减1并且返回指向v的指针。

  原子操作通常用于实现资源的引用计数,在TCP/IP协议栈的IP碎片处理中,就使用了引用计数,碎片队列结构struct ipq描述了一个IP碎片,字段refcnt就是引用计数器,它的类型为atomic_t,当创建IP碎片时(在函数ip_frag_create中), 使用atomic_set函数把它设置为1,当引用该IP碎片时,就使用函数atomic_inc把引用计数加1。

  当不需要引用该IP碎片时,就使用函数ipq_put来释放该IP碎片,ipq_put使用函数atomic_dec_and_test把引用 计数减1并判断引用计数是否为0,如果是就释放IP碎片。函数ipq_kill把IP碎片从ipq队列中删除,并把该删除的IP碎片的引用计数减1(通过 使用函数atomic_dec实现)。

五Linux 下so文件的制作

所谓链接,也就是说编译器找到程序中所引用的函数或全局变量所存在的位置

 

程序的链接分为静态链接和动态链接

 

LD_PRELOAD就是这样一个环境变量,它可以影响程序的运行时的链接(Runtime linker), 它允许你定义在程序运行前优先加载的动态链接库。这个功能主要就是用来有选择性的载入不同动态链接库中的相同函数。通过这个环境变量,我们可以在主程序和 其动态链接库的中间加载别的动态链接库,甚至覆盖正常的函数库。一方面,我们可以以此功能来使用自己的或是更好的函数(无需别人的源码),而另一方面,我 们也可以以向别人的程序注入恶意程序,从而达到那不可告人的罪恶的目的。

 

设置LD_PRELOAD变量:(使我们重写过的strcmp函数的hack.so成为优先载入链接库)

      $ export LD_PRELOAD="./hack.so"

 

libc.so.6(GLIBC_2.4) :    libc.so.6(GLIBC_2.14) :

 

生成动态库:libtest.so

# gcc test_a.c test_b.c test_c.c -fPIC -shared -o libtest.so

 

测试是否动态连接,如果列出libtest.so,那么应该是连接正常了

#  ldd test

 

-shared:

该选项指定生成动态连接库(让连接器生成T类型的导出符号表,有时候也生成弱连接W类型的导出符号);

不用该标志外部程序无法连接,相当于一个可执行文件;

 

-fPIC:

表示编译为位置独立的代码;

不用此选项的话编译后的代码是位置相关的所以动态载入时是通过代码拷贝的方式来满足不同进程的需要,而不能达到真正代码段共享的目的;

 

六 内存人工释放

Linux系统下,我们一般不需要去释放内存,因为系统已经将内存管理的很好。但是凡事也有例外,有的时候内存会被缓存占用掉,导致系统使用SWAP空间影响性能,此时就需要执行释放内存(清理缓存)的操作了。

 

Linux系统的缓存机制是相当先进的,他会针对 dentry(用于VFS,加速文件路径名到inode的转换)、Buffer Cache(针对磁盘块的读写)和Page Cache(针对文件inode的读写)进行缓存操作。但是在进行了大量文件操作之后,缓存会把内存资源基本用光。但实际上我们文件操作已经完成,这部分 缓存已经用不到了。这个时候,我们难道只能眼睁睁的看着缓存把内存空间占据掉么?

所以,我们还是有必要来手动进行Linux下释放内存的操作,其实也就是释放缓存的操作了。

要达到释放缓存的目的,我们首先需要了解下关键的配置文件/proc/sys/vm/drop_caches。这个文件中记录了缓存释放的参数,默认值为0,也就是不释放缓存。他的值可以为0~3之间的任意数字,代表着不同的含义:

0 – 不释放
1 – 释放页缓存
2 – 释放dentries和inodes
3 – 释放所有缓存

知道了参数后,我们就可以根据我们的需要,使用下面的指令来进行操作。

首先我们需要使用sync指令,将所有未写的系统缓冲区写到磁盘中,包含已修改的 i-node、已延迟的块 I/O 和读写映射文件。否则在释放缓存的过程中,可能会丢失未保存的文件。

#sync

接下来,我们需要将需要的参数写进/proc/sys/vm/drop_caches文件中,比如我们需要释放所有缓存,就输入下面的命令:

#echo 3 > /proc/sys/vm/drop_caches

此指令输入后会立即生效,可以查询现在的可用内存明显的变多了。

要查询当前缓存释放的参数,可以输入下面的指令:

#cat /proc/sys/vm/drop_caches

七 多线程内存申请

pthread不是Linux下的默认的库,也就是在链接的时候,无法找到phread库中哥函数的入口地址,于是链接会失败。

解决:在gcc编译的时候,附加要加 -lpthread参数即可解决。

 

几个好的关于linux内存讨论的文章

http://www.cnblogs.com/zhaoyl/p/3695517.html

http://www.ibm.com/developerworks/cn/linux/l-memmod/?S_TACT=105AGX52&S_CMP=tech-51CTO

目前除了glibc里面的ptmalloc(malloc)还有jemalloc,tcmalloc,但是他们大部分是针对小块内存单元的分配,大块内存单元都是调用mmap。

posted @ 2014-12-05 08:43  高山小路  阅读(2859)  评论(0编辑  收藏  举报