linux内存管理(上)

84 篇文章 0 订阅

linux内存管理

 

对于一个进程来说,内存是最基本也是最重要的资源。这一章的内容包括:存储器分配、内存操控和内存释放以及内存释放

 

了解如何锁定内存,从而避免了你再程序中等待内核从交换区换页。

 

看到锁定内存我思考,锁定内存是什么?

 

 

学习自csdn

https://blog.csdn.net/jidonghui/article/details/7332266

交换对进程来说是透明的,应用程序一般都不需要关心(甚至不需要知道)内核页面调度的行为。

如果用户不希望某块内存在暂时不用时置换到磁盘上,可以对该内存进行内存锁定。

 

#include <sys/types.h>  
  
int mlock(const void *addr,size_t length)  
  
int munlock(void *addr,size_t length)  
  
int mlockall(int flag)  
  
int munlockall(void  )

 

函数:mlock锁定一片内存区域,addr为内存地址,length要锁定的长度。

munlock接触已锁定的内存

mlockall一次锁定多个内存页。flag取值有两个MCL_CURRENT锁定所用内存页,MCL_FUTURE锁定为进程分配的地址空间内存页。munlockall用于解除锁定的内存。这些函数会在后面的内容详细说

Linux 实现了请求页面调度,页面调度是说页面从硬盘按需交换进来,当不再需要的时候交换出去。这样做允许系统中每个进程的虚拟地址空间和实际物理内存的总量再没有直接的联系,因为在硬盘上的交换空间能给进程一个物理内存几乎无限大的错觉。


 

8.1进程的地址空间

 

和所有的现代操作系统一样,linux将物理内存虚拟化。进程并不能直接在物理内存上寻址,而是由linux内核为每个进程维护一个特殊的虚拟空间(也就是说我们平时写程序操作的是虚拟地址)。这个空间地址是线性的,从0开始,到某个最大值。

 

8.1.1页和页面调度

 

虚拟空间由许多页组成。系统的体系结构以及机型决定了也的大小(页的大小是固定的),典型的也的大小包括4k(32)位系统以及8k(64位系统).每一个页面只有无效和有效两种状态,一种是有效页面和一个物理页或者一些二级存储介质.

 

8.1.1页和页面调度

 

虚拟空间由许多页组成。系统的体系结构以及机型决定了也的大小(页的大小是固定的),典型的也的大小包括4k(32)位系统以及8k(64位系统).每一个页面只有无效和有效两种状态,一种是有效页面和一个物理页或者一些二级存储介质

 

如一个交换分区或者一个在硬盘上的文件。一个无效页面没有关联,代表它没有被分配使用。对于无效页面的访问会引发一个段错误。地址空间不需要是连续的。虽然是线性编址,但实际上中间有很多未编址的小区域。

 

一个进程不能访问一个处在二级存储中的页,除非这个页和物理内存中的页相互关联。如果一个进程尝试访问这样的页面会发生一个页错误。然后内核会透明的从二级存储换入需要的页面。因为一般来说虚拟存储器总是比物理内存大,所以内核也需要经常的把页面从物理内存换出到二级存储,从而为将要转入的页面腾出空间。内核总是将未来不可能使用的页换出来优化性能。

当没有足够的物理内存时,系统通过把进程的一部份转移到硬盘上

以设法容纳进程。当再次需要进程中的被转移到硬盘上的那一部分时,

再返回到物理内存中。这个上过程称为页面调度,它使得系统即使在有限的物理

内存的条件下也能够具备多任务处理的能力.

Unix中用作虚拟内存的硬盘分段称为交换空间,交换空间耗尽将引起严重的问题

直至使系统失效

 

8.1.1.1共享和写时复制

虚存中的多个页面,甚至是属于不通进程的虚拟地址空间,也有可能被反映射到同一个物理页面。
这样允许不通的虚拟地址空间共享物理内存上的数据。共享数据可能是只读的,或者是可读可写的。

当一个进程试图写某个共享的可写页的时候,可能发生以下两种情况。最简单的是内核允许这个操作,在这种情形下所有共享这个页的进程都将看到这次写操作的结果。通常大量进程对同一个页面进行读写的时候需要某种成都上的合作和同步操作机制。

这个我感觉有点像共享内存

另一种情况是MMU会截取这次写操作并产生一个异常;作为回应,内核会透明的创造一份这个页的拷贝以供给这个进程进行写操作,这就叫做写时复制。允许读取共享的数据可以节省空间。当一个进程试图写这个共享页面的时候,可以立刻获得一个这个页的拷贝,是的进程内核工作起来像每个进程都始终有自己的私有拷贝。写时拷贝是以页为单位进行的,因此一个大文件可以有效的被众多进程共享。而每个进程只有在对共享页写时才会获得一份新的拷贝。

 

####8.1.2存储器区域

 

内核将具有某些相同特征的页组织成块,例如读写权限。这些块叫做存储器区域,段(segments),或者映射,下面是一些在每个进程都可以见到的存储器区域:

 

1)文本段包含着一个进程的代码,字符串,常量和一些只读数据。

 

2)堆栈段包含一个进程的执行栈,随着栈的深度动态的伸长或者收缩。执行栈中包含了程序的局部变量和函数的返回值。

 

3)数据段 又叫堆,包含这一个进程的动态存储空间。这个端是科协的,而且它的大小是可变化的。这部分空间往往是由malloc分配的。

 

4)BSS段,包含了没有被初始化的全局变量。这些变量根据不通的c标准都有特殊的值。

 

linux从两个方面优化这些变量。首先,因为附加段是用来存放没有被初始化的数据的,所以连接器实际上并不会将特殊值存储在对象文件中。这样可以减少二进制文件的大小。其次,当这个段被加载到内存时候,内核只是需要简单的根据写时复制的原则将他们映射到一个全是0的页上,这样十分有效的设置了这些变量的初始值。

 

大多数地址空间中包含了很多映射文件,比如可执行文件自己,c或者是其他的可链接库和数据文件。可以看看/proc/self/maps, 或者pmap程序的输出,我们可以看到一个进程里面有很多映像文件。

 

/proc/meminfo可以看到整个系统内存消耗情况,使用top可以看到每个进程的VIRT(虚拟内存)和RES(实际占用内存),基本上就可以将泄漏内存定位到进程范围。

 

linux提供了/proc/self/目录,这个目录比较独特,不同的进程访问该目录时获得的信息是不同的,内容等价于/proc/本进程pid/。进程可以通过访问/proc/self/目录来获取自己的系统信息,而不用每次都获取pid。

 

也就是说/proc/self等价于/proc/pid

我们使用

man 5 proc 

查看maps 的详细说明

 /proc/[pid]/maps
              A file containing the currently mapped memory regions and their access permissions.  See mmap(2) for some further information about memory mappings.

              Permission to access this file is governed by a ptrace access mode PTRACE_MODE_READ_FSCREDS check; see ptrace(2).

              The format of the file is:

    address           perms offset  dev   inode       pathname
    00400000-00452000 r-xp 00000000 08:02 173521      /usr/bin/dbus-daemon
    00651000-00652000 r--p 00051000 08:02 173521      /usr/bin/dbus-daemon
    00652000-00655000 rw-p 00052000 08:02 173521      /usr/bin/dbus-daemon
    00e03000-00e24000 rw-p 00000000 00:00 0           [heap]
    00e24000-011f7000 rw-p 00000000 00:00 0           [heap]
    ...
    35b1800000-35b1820000 r-xp 00000000 08:02 135522  /usr/lib64/ld-2.15.so
    35b1a1f000-35b1a20000 r--p 0001f000 08:02 135522  /usr/lib64/ld-2.15.so
    35b1a20000-35b1a21000 rw-p 00020000 08:02 135522  /usr/lib64/ld-2.15.so
    35b1a21000-35b1a22000 rw-p 00000000 00:00 0
    35b1c00000-35b1dac000 r-xp 00000000 08:02 135870  /usr/lib64/libc-2.15.so
    35b1dac000-35b1fac000 ---p 001ac000 08:02 135870  /usr/lib64/libc-2.15.so
    35b1fac000-35b1fb0000 r--p 001ac000 08:02 135870  /usr/lib64/libc-2.15.so
    35b1fb0000-35b1fb2000 rw-p 001b0000 08:02 135870  /usr/lib64/libc-2.15.so
    ...
    f2c6ff8c000-7f2c7078c000 rw-p 00000000 00:00 0    [stack:986]
    ...
    7fffb2c0d000-7fffb2c2e000 rw-p 00000000 00:00 0   [stack]
    7fffb2d48000-7fffb2d49000 r-xp 00000000 00:00 0   [vdso]

              The address field is the address space in the process that the mapping occupies.  The perms field is a set of permissions:

                  r = read
                  w = write
                  x = execute
                  s = shared
                  p = private (copy on write)

              The  offset  field  is  the  offset into the file/whatever; dev is the device (major:minor); inode is the inode on that device.  0 indicates that no inode is associated with the memory
              region, as would be the case with BSS (uninitialized data).

              The pathname field will usually be the file that is backing the mapping.  For ELF files, you can easily coordinate with the offset field by looking at the Offset field in the ELF  pro‐
              gram headers (readelf -l).

              There are additional helpful pseudo-paths:

                   [stack]
                          The initial process's (also known as the main thread's) stack.

                   [stack:<tid>] (since Linux 3.4)
                          A thread's stack (where the <tid> is a thread ID).  It corresponds to the /proc/[pid]/task/[tid]/ path.

                   [vdso] The virtual dynamically linked shared object.  See vdso(7).

                   [heap] The process's heap.

              If  the  pathname field is blank, this is an anonymous mapping as obtained via mmap(2).  There is no easy way to coordinate this back to a process's source, short of running it through
              gdb(1), strace(1), or similar.

              Under Linux 2.0, there is no field giving pathname.

 

我可以通过这个文件很轻松的发现这个二进制程序除了自己本身还链接过哪些动态库。

 

一张图我们就很容易看出一个进程的存储区域的划分

 

8.2动态内存分配

 

内存同样可以通过自动变量和静态变量获得,但是所有内存管理系统的基础都是动态的内存分配,使用以及动态的返回。动态内存是在进程运行时候才分配的,而不是在编译的时候就分配好的,而分配的大小只有在分配的时候才知道的。作为一个程序员在程序员你不会知道程序占用了多少内存,或者你使用这块内存的时间不定,则需要使用动态内存

例如c不会提供在动态内存中获取结构体struct priate_ship的机制,而是提供一种机制在动态内存中分配一个足够大的空间来保存priate_ship的机制,而是提供了一种机制在动态内存中分配一个足够大的空间来保护priate_ship。程序员通过一个指针来对这块内存进行操作,这个指针就是struct priate_ship*。
 

malloc 失败的时候会返回NULL,并把errno设置为ENOMEN。

 

8.2.1数组分配

当分配数据内存本身大小可变,动态分配内存更加复杂。为数组动态分配内存就是一个更好的例子

	#include <stdlib.h>
	void *calloc(size_t nr,size_t size);

calloc与malloc的区别,calloc分配的区域全部用0进行了初始化,因此y中的50个元素都被赋值为0,但是x数组里面的元素却是未定义的。注意calloc会比memset快

 

8.2.2调整已经分配的内存大小

 

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

 

成功调用realloc将ptr指向的内存区域大小变为size字节。他返回了一个指向新空间的指针,当试图扩大内存的时候返回的指针可能不在是ptr。如果realloc不能在已经由的空间上增加到size大小,那么就会额外申请一块size大小的空间,并且将原本内容拷贝到新的空间。因为有潜在的拷贝操作所以realloc的操作是比较耗时的。

如果ptr是NULL,结果就会跟malloc一样。如果ptr是非NULL的,那么他必须是之前调用的malloc,calloc或realloc之一的返回值。当失效的
c不会提供支持动态内存的变量。

 

8.2.3动态内存的释放free

 

free掉对应地址之后就可以

 

8.2.4对齐

 

数据对齐是指数据地址和硬件确定的内存块之间的关系。一个变量地址是它大小的倍数时,就叫做自然对其,如果一个变量长32位是4的倍数就是自然对其。如果一个类型大小是2n个直接,那么他的地址中,至少低n位是0.对齐规则是根据硬件制定的。在一些系统中载入一个没对齐的数据会发生错误。在可移植的代码中对齐问题一定要注意!!!

 

32位系统是8字节对齐,64位系统是16字节对齐。

 

posix提供了一个叫做posix_memalign的函数:

 

#include <stdlib.h>
void* valloc(size_t size);
posix_memalign(void **memptr,size_t alignment,size_t size);

调用posix_memalign成功时候会返回size字节的动态内存,并保证是按照alignment进行对其的,参数alignment必须是2的幂次方,以及void指针大小的倍数。返回的内存块的地址保存在memptr里,函数返回0

调用失败返回下面这些错误码

EINVAL 参数不是2的幂次方,或者不是void指针的倍数

ENOMEM没有足够的内存去满足函数的申请

函数valloc的功能和malloc一模一样,但是返回的地址是页面对齐的。回顾一下第四章,页面的大小很容易通过getpagesize得到。

相似地,函数memealign是以boundary字节对齐的,而boundary必须是2的幂。在这个例子中,两个函数都返回一块足够大的内存去存放一个ship结构,并且地址都是在一个页面的边界上,这些函数申请的api都可以通过free来释放。

 

int main()
{
    void* buf[128];
    int i=0;
    for(i=0;i<128;i++)
    {
       int res = posix_memalign(&buf[i],8,1024);
       printf("%d\n",res);
    }

    for(i=0;i<128;i++)
    {
        free(buf[i]);
    }
}

 

在posix_memalign之前使用

#include <malloc.h>
void * valloc (size_t size);
void * memalign (size_t boundary, size_t size);

valloc和malloc一样但是返回的是字节对齐的malloc

 

下面是四条有用的规则:

一个结构的对其要求和它的成员中最大的类型是一样的。例如,一个结构中最大的是以4字节对齐的32bit的整形,那么这个结构至少要以4个字节对齐。

结构体引入了对填充的需求,以此来保证每一个成员都复合各自的对齐要求。所以如果一个char后面跟着一个int,编译器会自动插入3个字节来保证int以4个字节对齐。程序员要注意结构体中成员变量的顺序,来减少填充锁导致的空间浪费。

 

例子1:


#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int bss_end;

typedef struct _demoOne{
    char one;
    char two;
    int three;
}demoOne;

int main()
{
    int a;
    printf("%ld\n",sizeof(demoOne));
    return 0;
}

上面这个占用了8个字节,并不是12个字节因为是

 

例子2:

typedef struct _demoOne{
    char one;
    int three;
    char two;
}demoOne;

占用了12个字节

 

这样就会导致占用12个字节,所以我们在使用结构体的时候一定要注意变量顺序避免字节浪费!!!

一个联合的对齐和联合里面最大的类型一致。

一个数组的对齐和数组里的元素类型一致。所以对数组里面的元素做对齐以外,没有其他的对齐要求    。

使用指针。因为编译器透明的处理了绝大多数的对齐问题,所以要找到潜在的错误的时候比较困难,然而以这样的错误并不少见,特别是在处理指针和强转的时候。

假设一个指针从一个较少字节对齐类型强转为一个较多的字节对齐类型,通过这样的指针来访问的时候,会导致处理器不能对较多字节类型的数据正确对齐、例如下面的代码片段,c到badnews的强转使得程序将c转为unsigned long来读:

 

char greeting[] = ”Ahoy Matey”;
char *c = greeting[1];
unsigned long badnews = *(unsigned long *) c;

这个代码例子会导致段错误。

 

一个unsigned long 可能以4或8个字节对齐;而c当然只以1字节为边界对齐。因此当c被强转之后再进行读取将会导致对齐错误。

 

8.3数据段的管理

#include <unistd.h>
int brk(void* end);
void* sbrk(int ptr_t increment);

这些函数继承了一些老版本的unix系统中函数的名字,同事堆和栈还在同一个段中。堆中动态存储器的分配由数据段的底部向上生长;栈从数据段的顶部向下生长。栈和堆的分界线叫做中断或中断点。在现代系统中,数据段存在与自己的内存映射中,我们仍用中断点来标记映射结束地址。

调用brk会设置中断点的地址为end。在成功的时候返回0,失败的时候返回-1,并设置errno为ENOMEM

调用sbrk将数据段末端增加increment字节,increment可正可负。sbrk返回修改后的断点。所以increment为0的时候得到的是现在的断点地址:
 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值