Linux系统编程(第九章)笔记

内存管理

进程地址空间

   像所有的现代操作系统一样,Linux将它的物理地址虚拟化。进程并不能直接在物理内存上寻址,而是由Linux内核为每个进程维护一个特殊的虚拟地址空间,这个地址空间是线性的,从0开始,一直到某个最大值。

页和页面调度

   内存是由比特位组成,8个比特组成一个字节,字节又组成字,字组成页。对于内存管理,页是最重要的:页是内存管理单元可以管理的最小可访问内存单元。因此,虚拟空间是由许多页组成的。系统的体系结构以及机型决定了页的大小(页的大小是固定的),典型的页大小包括4K(32位系统)和8K(64位系统)。
   页有两种状态:无效的和有效的,一个有效页和实际的数据页相关联,可能是物理内存,也可能是二级存储介质,比如交换分区或硬盘上的文件。一个无效页没有任何含义,表示它没有被分配或使用。访问一个无效的页会引发一个段错误。
   地址空间不需要是连续的。虽然是线性编址,但实际上中间有很多未编址的小区域。
   如果一个有效的页和二级存储的数据相关,进程不能访问该页,除非把这个页和物理内存中的数据关联。如果一个进程要访问这样的页,那么存储器管理单元(MMU)会产生页错误。然后,内核会介入,把数据从二级存储切换到物理内存中,而且对用户“透明”。由于虚拟内存要比物理内存大得多,内核可能需要把数据从内存中切换出来,从而为后面要Page in的页腾出更多的空间。因而,内核也需要把物理内存切换到二级存储,这个过程称为Paging out。为了优化性能,实现后续page in操作代价最低,内核往往是把物理内存中近期最不可能使用的页替换出来。

共享和写时复制

   虚拟内存中的多个页面,甚至是属于不同进程的虚拟地址空间,也有可能会映射到同一个物理页面。通过这种方式,可以支持不同的虚拟地址空间共享物理内存上的数据。
   共享的数据可能是只读,只写的,或者可读可写的。
   当一个进程试图写某个共享的可写页时,可能发生以下两种情况之一。最简单的是内核允许写操作,在这种场景下,所有共享这个页的进程都将看到这次写操作的结果。通常,允许大量的进程对同一页面读写需要某种程度上的合作和同步机制。但是在内核级别,写操作“正常工作”,共享数据的所有进程会立即看到修改。
   在另一种情况场景下,内存管理单元(MMU)会拦截这次写操作,并产生一个异常。内核会相应地“透明”创建一份该页的拷贝,支持继续对新的页面执行写操作。我们将这种方法称为“写时拷贝(copy-on-write)(COW)”

内存区域

   内核将具有某些相同特征的页组织成块,例如读写权限。这些块叫做内存区域,段,或者映射:

  • 文本段(text segment)包含这一个进程的代码、字符串、常量和一些只读数据。在Linux中,文本段被标记为只读,并且直接从目标文件(可执行程序或是库文件)映射到内存中。
  • 堆栈段(stack)包括一个进程的执行栈,随着栈的深度变化会动态伸长或收缩。执行栈中包括了程序的局部变量和返回值。
  • 数据段(data segment)又叫堆(heap),包含一个进程的动态内存空间。这个段是可写的,而且它的大小是可以变化的。这部分往往是由malloc分配的。
  • BSS段(bss segment)包含了没有被初始化的全局变量。根据不同的C标准,这些变量包含特殊的值(通常来说,这些值都是0)。
       Linux从两个方面优化这些变量。首先,因为附加段是用来存放没有被初始化的数据,所以链接器(ld)实际上并不会把这些特殊的值存储在对象文件中。这样,可以减少二进制文件的大小。其次,当这个段被加载到内存时,内核只需根据写时复制的原则,简单地将它们映射到一个全是0的页上,通过这种方式,可以高效的把这些变量设置成初始值。
动态内存分配

   C不提供支持动态内存的变量。例如,C不会提供在动态内存中获取结构体struct pirate_ship机制,而是提供了一种机制,可以在动态内存中分配一个足够大的空间来保存结构体private_ship。然后,编程人员可以通过指针对这块内存进行操作–在这个例子中,该指针即struct pirate_ship*。

#include<stdlib.h>

void *malloc(size_t size);

   成功调用malloc()时,会得到size大小的内存区域,并返回指向新分配的内存首地址的指针。每次调用时,C都会自动把返回值由void指针变为需要的类型。所以,这些例子在调用时并不需要把malloc()的返回值强制类型转换成一个左值类型。但是在C++中,并不提供这种自动转换。因而,C++的使用者需要对malloc()的返回值做强制类型转换。

数组分配
#include<stdlib.h>

void * calloc (size_t nr, size_t size);

   调用calloc()成功时会返回一个指针,指向一块可以存储下整个数组的内存(nr个元素,每个为size字节)。与malloc()直接分配计算出大小的内存不同的是,calloc将分配的区域全部用0初始化。

#include<stdlib.h>

void * realloc (void *ptr , size_t size);

   它可以改变(变大或变小)已分配的动态内存大小。成功调用realloc()会把ptr指向的内存区域的大小变为size字节。它返回一个指向新空间的指针,返回的指针可能是ptr,也可能不是。如果realloc()不能在已有的空间上增加到size大小,那么就会另外申请一块size大小的空间,将原本的数据拷贝到新空间中,然后再将旧的空间释放。

释放动态内存

   对与自动分配的内存。当栈不再使用,空间会自动释放。与之不同的是,动态内存将永久占有一个进程地址空间的一部分,直到显式释放。
   通过malloc(),calloc(),realloc()分配到的内存,当不再使用时,必须调用free()归还给系统:

#include<stdlib.h>

void free(void *ptr);
对齐

   数据对齐(alignment)是指数据在内存中的存储排列方式。如果内存地址A是2的n次幂的整数倍,我们就说A是n字节对齐。处理器、内存子系统以及系统中的其他组件都有特定的对齐需求。
   如果一个变量的内存地址是它大小的整数倍时,就称为“自然对齐”。例如,对于一个32位长的变量,如果它的地址是4(字节)的整数倍(也就是说,如果地址的低两位是0),那就是自然对齐了。因此,如果一个类型的大小是2n字节,那么它的内存地址至少低n位是0。
   在大多数情况下,编译器和C库会自动处理对齐问题。POSIX规定通过malloc()、calloc()和realloc()返回的内存空间对于C中的标准类型都应该是对齐的。在Linux中,这些函数返回的地址在32位系统是以8字节为边界对齐,在64位系统是以16字节为边界对齐的。
   非标准和复杂的数据类型的对齐比简单的自然对齐有更多的要求,可以遵循以下四条规则:

  • 结构体的对齐要求和它的成员中最大的那个类型是一样的。例如,一个结构中最大的是以4字节对齐的32bit的整型,那么这个结构至少以4字节对齐。
  • 结构体也带来了填充问题,以此来保证每一个成员都符合各自的对齐要求。因此,如果一个char(很可能是以1字节对齐)后跟着一个int(很可能是以4字节对齐),编译器会自动地插入3个字节作为填充来保证int以4字节对齐。编程人员有时需要注意一下结构体中成员变量的顺序,比如按成员变量类型大小降序来定义它们,从而减少由于填充所带来的空间浪费。使用GCC编译时,加入-Wpadded 选项可以帮助你实现这个优化,当编译器隐式填充时,它会发出警告。
  • 一个联合类型的对齐和联合类型里类型大小最大的一致。
  • 一个数组的对齐和数组里的基本元素类型一致。因此,除了对数组元素的类型做对齐外,数组没有其他的对齐需求。这样可以使数组里面的所有成员都是自然对齐的。
严格别名

   类型转换示例也破坏了严格别名规则,严格别名是C和C++中最不被了解的部分。“严格别名”要求一个对象只能用过该对象的实际类型来访问,包括类型的修饰符(如const或volatile)、实际类型的signed(或unsigned)修饰、包含实际类型作为成员变量的结构体或联合体,或者char类型指针。比如,访问uint32_t的一种常见方式是通过两个uint16_t指针,这就破坏了严格别名规则。
   记住下面这句箴言:间接引用把一个变量类型转换成另一个类型的指针往往会破坏严格命名规则。

数据段的管理
#include<unistd.h>

int brk(void *end);
void * sbrk(intptr_t increment);

   这些函数继承了一些老版本UNIX系统中函数的名字,当时堆和栈还在同一个段中。堆中动态内存的分配由数据段的底部一直往上,栈从数据段的顶部一直往下。堆和栈的分界线叫做中断(break)或中断点(break point)。在现代系统中,数据段存在于它自己的内存映射中,我们仍用中断点来标记映射的结束地址。
   调用brk()会把中断点(数据段的末端)的地址设置为end指定的值。成功时,该调用返回0。失败时,返回-1,并设置errno为ENOMEM。
   调用sbrk()会在数据段的末端增加increment个字节,参数increment值可正可负。sbrk()函数返回修改后的断点。

匿名内存映射

   glibc的内存分配使用了数据段和内存映射。实现malloc()最经典的方法就是将数据段切分为一系列2的整数幂大小的块,请求会返回符合要求的最小的那个块。内存释放则只是简单地将这块区域标记为“未使用”。如果相邻的分区都是空闲的,它们会被合成一个更大的分区。如果堆的最顶端是空的,系统可以用brk()来降低断点的地址值,缩小堆占用的空间,将内存返还给系统。
   这个算法称为“伙伴内存分配算法(buddy memory allocation scheme)”。它的优点是高速简单,缺点则是会产生两种类型的碎片。当使用的内存块大于请求的大小时则产生“内部碎片(Internal fragmentation)”。内部碎片会降低可用内存的使用率。“外部碎片(External fragmentation)”是指有足够的内存可以满足请求,但是这些内存被划分为两个或多个不相邻的块。外部碎片同样也会导致内存利用不足(因为可能会分配一个更大却并不合适的块)或是直接导致内存分配失败(如果已经没有其他可选的块了)。
   对于较大的内存分配,glibc并不使用堆,而是创建一个匿名内存映射(anonymous memory mapping)来满足分配请求。匿名内存映射和在第4章讨论的基于文件的映射很相似,但是它并不是基于文件——因此,称之为“匿名”。实际上,匿名内存映射只是一块已经用0初始化的大的内存块,以供用户使用。可以把它想象成单独为某次分配而使用的堆。由于这些映射并不是基于堆,所以不会造成数据段碎片。目前,这个临界值一般是128KB:分配小于等于128KB的空间是由堆实现,而对于更大空间的内存空间则由匿名内存映射来实现。

#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_t length);

   系统调用mmap()函数来创建内存映射,而用munmap()来销毁映射.

高级内存分配

   mallopt()函数会将由param指定的存储管理相关的参数的值设为value。使用   malloc_usable_size()和malloc_trim()进行调优。
   Linux提供了mallinfo()函数,可以获取动态内存分配系统相关的统计信息。

基于栈的分配

   要在一个栈中实现动态内存分配,可以使用系统调用alloca():

#include<alloca.h>

void * alloca(size_t size);

   成功时,alloca()调用会返回一个指向size字节大小的内存指针。这块内存是在栈中的,当调用它的函数返回时,这块内存会被自动释放。失败时,某些该函数的实现会返回NULL,但是alloca()在大多数情况下是不会失败或者无法报告失败。失败表现在出现栈溢出。

内存操作
#include<string.h>

void * memset(void *s, int c, size_t n);

   调用memset(),会把s指向区域开始的前n个字节设置为c,并返回s。该函数经常用来清零一块内存。

#include<string.h>

int memcmp(const void *s1,const void *s2, size_t n);

   调用memcmp()会比较s1和s2的前n个字节,如果两块内存相同就返回0,如果s1小于s2就返回一个小于0的数,反之则返回大于0的数。

#include<string.h>

void * memmove(void *dst, const void *src,size_t n);

   memmove()会把src的前n个字节复制到dst,并返回dst.

#include<string.h>

void * memcpy(void *dst, const void *src, size_t n);

   除了dst和src间不能重叠,这个函数基本和memmove()一样。如重叠了,函数的结果是未被定义的。另外一个安全的复制函数是memccpy().还有其他查找字节和加密就不列出了。

内存锁定
#include<sys/mman.h>

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

   调用mlock()将锁定addr开始,长度为len个字节的虚拟内存。成功时,函数返回0;失败时,函数返回-1,并会相应设置errno值。
   mlockall()函数会锁定一个进程在现有地址空间的物理内存中的所有页面。

#include<sys/mman.h>

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

   系统调用munlock(),会解除从addr开始,长为len的内存所在的页面的锁定。munlock()函数是取消mlock()的操作效果。而munlockall()则消除mlockall()的效果。两个函数在成功时都返回0,失败时返回-1.

#include<unistd.h>
#include<sys/mman.h>

int mincore(void *start,
            size_t length,
            unsigned char *vec);

   调用mincore(),会生成一个向量,表明调用时刻映射中哪个页面是在物理内存中.

内存分配策略

   Linux 使用了一种“投机性内存分配策略(opportunistic allocation strategy)”。当一个进程向内核请求额外的内存——如扩大它的数据段,或者创建一个新的内存映射——内核做出了分配承诺,但实际上并没有分给进程任何的物理存储。仅当进程对新“分配到”的内存区域执行写操作时,内核才履行承诺,分配一块物理内存。内核逐页完成上述工作,并在需要时进行请求页面调度和写时复制。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值