CPP内存管理

从内核到C++应用

关于C++内存管理方面的面试题有很多,但是这些问题的答案不能仅靠学习C++就能得出比较全面的答案 ,它还涉及C运行库和内核的方方面面。

本文给自己理一下思路,并在最后尝试回答几个常见的关于C++内存管理方面的面试题。

如果在linux开发环境下,整个系统的不同层级有着不同的内存管理器:

  • linux内核: 伙伴系统(以页为单位进行管理)、slab分配器(定制化的内存管理器)。
  • malloc\free库函数:使用系统调用mmap、sbrk,以及bins管理多个空闲链表(内存池)。有合并空闲块的操作。有两种方式管理内存:
    • 如果分配内存大于mmap分配阈值(见下一节),直接使用mmap分配;如果释放的内存大于mmap分配阈值,直接使用mummap释放,直接返还给操作系统
    • 否则,通过内存池分配算法分配,且用户层free的内存块,不一定立刻返回给操作系统,而是缓存在内存池中以供后续的请求。
  • C++ stl分配器:16个链表,分别管理16种小于128 B的对象(内存池),没有合并空闲块操作,如果分配\释放的内存大于128B直接调用malloc\free交由库函数处理。

image-20221116215651343

glibc 内存管理

参考资料:

glibc的mallocfree使用的ptmalloc实现,详见这篇文章的解析:Glibc内存管理 —— 华庭

首先,glibc的设计假设是这样的,我把我觉得比较重要的内容高亮了出来:

image-20230404210044038

“malloc如果分配的内存大于128KB,则使用mmap分配内存,否则使用sbrk分配内存”。正确吗?

这句话并不绝对正确,如上图所示,使用mmap分配的阈值是动态变化的,因此不一定分配128KB的malloc请求一定会使用mmap系统调用。请参考下图,假设malloc的初始mmap阈值为128KB,表示分配内存大于128KB时直接使用mmap而不用通过内存池机制;如果某一时刻释放了一个大于128KB的页,比如256KB,那么mmap阈值将被调整为256KB,表示分配内存大于256KB时才使用mmap,此时如果分配的内存大小为128KB,那么这个请求还是会走内存池机制,而且释放128Kb的内存块时也不会使用mumap直接将其返还给操作系统了。当然mmap阈值不能无限增加,见下图的描述。

image-20230404213030113

而且通过mmap分配的内存如果被释放了,那么这片地址就直接返回给操作系统了,如果再次引用这篇地址,则会引发segmentation fault

glibc内存池机制

除了使用mmap分配大内存块,glibc还有一种内存池机制,使用一种chunk的数据结构来管理堆内存。chunk结构中包括用户数据,以及为了维护内存池的一些其他信息(比如下一个chunk的指针、本chunk的大小等),在侯捷老师的课程中,这些除用户信息以外的管理信息被称作cookie,在glibc中以32位系统为例,每个chunk的cookie大小位16B。

空闲的chunk使用bins来管理。用户free掉的内存并不会马上归还给操作系统,而是缓存在各种bin中。ptmalloc会统一管理 heap 和 mmap 映射区间的空闲chunk,当用户需求来临时,它首先在bins中找到空闲的chunk返回给用户,如果找不到则使用系统调用再向操作系统申请内存。这样就避免了频繁的系统调用,较少分配开支。

bins有很多种:

  • fast bins, 存放大小小于等于64B的chunk, 这个bin是为了加快热点小内存块的分配
  • unsorted bins,可以看做是 bins 的一个缓冲区,增加它只是为了加快分配的速度
  • small bins,有62种规格,每一种规格相差8B,(16B - 576B),每个small bin管理的chunk大小都相同
  • large bins,有63个,每个bin都管理在一定范围内的内存块,也就是说每个large bin种的chunk大小不同,但是会按照大小从大到小排列。

image-20230404214933178

还有两种不属于任何bin的chunk:

  • mmaped chunk: 直接使用mmap分配的内存块,在释放时使用ummap直接返还给操作系统
  • top chunk: heap最高处的一块空闲chunk,它的大小是随着分配和回收不停变化的。

具体的分配和回收算法参考Glibc内存管理 —— 华庭 3.2.4和3.2.5节。

那么,通过内存池分配的内存会返还给操作系统吗?什么情况下会返还?

还是在上面的参考资料的3.2.5节的free流程中有详细解释:

image-20230404222041634

简单讲,就是在free时会判断释放块前后是否有空闲块,如果有则迭代地进行合并,如果最后合并到到的空闲块与topchunk相邻(topchunk是一定为空闲的),那么将它与topchunk合并,如果合并而得的topchunk大于mmap的收缩阈值,则归还topchunk中的一部分给操作系统。

从这里可以隐隐看出 Glibc内存管理 —— 华庭 这篇文章中提到的内存暴增问题的一个原因了:

  • 收缩内存(将内存归还给操作系统)是从topchunk开始的,如果topchunk相邻的chunk不是空闲的,那么topchunk以下的chunk都不能归还给操作系统,因此会堆积越来越多的空闲chunk而不返还给操作系统。

    image-20230404223143072

  • 一种缓解方法:绕过glibc的内存池系统,每次分配的内存都大于等于mmap分配阈值的最大值(在64为系统上是32MB),那么malloc将直接使用mmap分配内存,且free时一定会将内存返还给操作系统。这样的话malloc\free就是mmap和mumap系统调用的简单封装,可以考虑直接使用系统调用mmap和mumap进行内存分配管理。

C++ primitives

参考资料:

  • 侯捷老师的视频讲解

new delete表达式

image-20221116220736011

new表达式(new后不加其他任何参数)会被编译器转化为两个步骤

  • 调用operator new 分配内存,调用的函数版本为(operator new(size_t)),对象大小由编译器算出并自动传入
  • 然后调用对象的构造函数,在得到的内存上创建object

此外,operator new 可能抛出异常,需要用try catch处理异常。

而operator new 内部直接调用C运行库的 malloc, 当内存分配失败时,执行_callnewh, 这个函数主要释放一些内存,看看能不能空出一些来给新对象

如果operator new函数分配内存成功了,但是构造函数却抛出异常。这时候,就要取消第一个步骤中分配的内存,否则就会导致内存泄漏。

这个责任落在C++运行库上,由它调用相应的operator delete(什么叫相应的delete,见effective C++,条款52)取消已经分配的内存。

delete表达式会也被编译器转化为两个步骤

  • 调用对象的析构函数,调用的函数版本为(operator new(size_t))
  • 调用 operator delete

operator delete内部调用C运行库的 free 释放内存

image-20221116221103756

array new

使用了array new 之后就要使用array delete,否则就会只调用一次析构函数,这样有可能发生内存泄漏,仅限于class with pointer member 的对象,见下图示意。泄漏的内存不是array本身,而是array的元素所指向的内存。

下图的cookie大多是记录了array new 操作分配了多少个objext

image-20221117201830011

如果

int* p = new int[10] // array new
delete p // 但不 array delete

或者向上图那样的无指针成员的对象Complex,不会造成内存泄漏。

palcement new

operator new函数如果接收的参数除了一定会有的哪个size_t之外,如果还有其他参数,那么它就属于placement new(见重载oeprator new 一节)

placement new 等同于直接调用构造函数,不会像new 表达式那样先分配内存。

// 像这样使用placement new
char* buf = new char[sizeof(Complex)]; // 首先分配内存
Complex* p = new(buf)Compelx(1,1) // new后传入参数buf指针,表示在这个地址上构造Complex(1,1)

重载operator new

image-20221117205909352

new表达式内部调用 operator new 函数,侯捷老师说我们可以在两个地方重载operator new 函数来实现自己的内存分配函数。

一开始不理解什么较做“在两个地方”,后来查了些资料明白了这与C++的名称查找规则(Name lookup rule)有关。当C++编译器在class scope内碰到一个name后,首先在class内搜寻这个name,看看有没有它的declaration,如果没有再去全局域找找。

因此,如果我们在类内重载了operator new,那么全局的operator new根本不会被搜寻,更不用说执行了。

相比重载全局的operator new,重载class scope的做法更安全些。

下例摘自 cppreference,注意类内的operator new函数一定是static的,无论定义式加不加static关键词,编译器一律视类内的operator new函数为static

operator new 函数的参数一般是对象的大小,当我们写 new Obj() 时, Obj对象的大小会自动被当作operator new的第一个参数。

struct X
{
    static void* operator new(std::size_t count)
    {
        std::cout << "custom new for size " << count << '\n';
        return ::operator new(count); // 调用全局的operator new
    }
 
    static void* operator new[](std::size_t count)
    {
        std::cout << "custom new[] for size " << count << '\n';
        return ::operator new[](count);//// 调用全局的operator new
    }
};
 
int main()
{
    X* p1 = new X;
    delete p1;
    X* p2 = new X[10];
    delete[] p2;
}

也可以在重载函数中直接调用malloc,而不使用全局operator new,因为operator new只是分配了内存我们自己可以实现它。如果对应构造函数也不抛出异常,那么整个new表达式就不会抛出异常!(前面说过,new 表达式会转化为 1.operator new,分配内存 + 2.调用构造函数两部,其中全局operator new会有异常的抛出)

struct X
{
    static void* operator new(std::size_t count)
    {
        std::cout << "custom new for size " << count << '\n';
        return malloc(count);
    }
 
    static void* operator new[](std::size_t count)
    {
        std::cout << "custom new[] for size " << count << '\n';
        return malloc(count);
    }
};

我们可以重载多个operator new 版本:

image-20221121113240277

其中第一个版本就是new 表达式(new后不加任何其他参数)会自动转化成的版本

注意当在class内声明operator的各种函数时,可能会被继承增加复杂性

class Base {
public: 
    static void* operator new(size_t size) throw(std:: badalloc){
        // 一些针对Base Class的操作
    }
    ...
};
public Derived {
public:
    // 未申明operator new ,从分类继承operator new
}

这样当new一个Derived对象时,调用的是Baseclass中定义的operator new,二这可能带来很多不确定因素。

解决这样的做法是在Baseclass的operator new中加入判断 size是否等于Baseclass大小的逻辑

class Base {
public: 
    static void* operator new(size_t size) throw(std:: badalloc){
        if (size != sizeof(Base)) { // 如果创建的不是Basevlass对象
            return ::operator new(size); // 转发至全局的operator new 函数
        }
    }
    ...
};

这样当使用BaseClass定义的operator new 函数去分配一个Derived对象时,改用全局operator new 函数,这样可万无一失。

GNUC 2.9版本std::alloc

主要看SGI的实现,有两个空间配置器

  • _malloc_alloc_template<0>
  • __default_alloc_template<...>

用户可以选择单独使用第一个分配器,或者一起使用两个分配器。

当用户选择使用两个分配器时,编译器会分别将上述两个分配器typedef成 malloc_allocalloc, 容器的分配器默认使用alloc,即第二个分配器。

两个配置器的接口都有allocate() deallocate() reallocate(),这里主要聚焦于前两个接口。

第一个配置器(malloc_alloc)的allocate()从typedef的名字上可以看出,它只是简单调用malloc(), deallocate()也只是简单调用free(),唯一的特别之处是,这个配置器能够模拟C++ new 运算符的set_new_handler()以处理内存不足的情况。

而第二个配置器(alloc), 当内存小于128字节时则由自己管理这些内存块,会自己管理一个内存池当分配的内存大于128字节时,直接调用malloc::allocate()。如果系统空间不足,那么也调用malloc::allocate(),因为它有处理程序处理内存不足的情况。

为什么对于小于128bytes的内存块使用内存池来管理?

1.防止内存碎片

2.若使用malloc直接分配的内存,每块都带有一些cookie,若小内存偏分配次数多,那么cookie的占用空间相比于有用空间会很大,空间利用率不高。

alloc配置器如何管理内存池?这里只记个大概,细节看书。

alloc管理一个16个长度的数组,数组的每个元素都指向一个free_list, 每个free list都管理一种大小的空闲数据块。

内存块大小有8bytes 、16bytes、24bytes ... 128bytes。 因此一共要16个free list来进行空闲块的管理。

最初,alloc的内存池空无一物,当有请求来时,比如要申请8字节的空间,就调用malloc向系统申请空间大小为8 * 40,将其中的1块返回给用户,其中19块做切割处理后交给对应的free list管理,剩余的20块留给内存池的备用。当再有8字节申请时,则直接从这个free list中拨给用户空闲空间。当有16字节申请时,从内存池中的备用空间中找空闲内存,如果不够则再调用malloc重复刚刚的操作,但是在当前情景中,确实是存在备用内存的(刚刚分配8字节内存时剩余的20块)。

image-20221121201129114

容器使用该分配器分配内存,而不是直接用malloc向操作系统索要,这样会节省很多存放cookie的内存空间。

处理内存申请时,如果申请的内存块小于128bytes, alloc将从以下几方面递进式地申请内存

  • 首先看看对应freelist有无空闲空间
  • alloc的内存池的备用内存是否有空余块(end_free - start_free)
  • 如果没有,则使用malloc向系统申请内存
  • 如果malloc还失败了,那么再看看其他freelist是否还有没有划分出去的内存块
  • 最后,已然山穷水尽,调用malloc_alloc(第一个配置器)的allocate()看看它的 handler 处理程序能否空出一些内存

有几个问题值得探讨

  1. sgi版本的stl管理内存的方式乍一看和linux内核的伙伴系统很像,但是stl内存池根本不涉及连续内存块合并的操作,也就是说没有伙伴的概念
  2. 内存池管理的内存在程序运行期间,并没有被调用free()库函数,因为如果要free,则必须要传回紧挨着cookie之后的那根指针,但是这根指针在分配过程中早已丢失。所以也更谈不上这些内存会归还给操作系统了。

常见面试题

不停地new\malloc会发生什么?

首先,new和malloc是有区别的,new关键词会先调用malloc分配内存,然后调用构造函数,注意这里的调用构造函数这个步骤,会将对象初始化!而malloc那就仅仅是malloc。

有什么不同?操作系统对内存执行“懒分配”策略,尤其是mmap系统调用,懒到了极致,它在一开始并不会正真地给进程分配内存,甚至页表都不会分配,而是等进程发生缺页中断之后才会进行分配。那么对于malloc分配的内存,如果之后不去动它,那么什么事情也不会发生,但对于new来说,相当于是malloc后又对内存执行了memset操作,这里发生缺页中断,实际分配了内存。

下面来看几组实验,总体上分成new和malloc两组。

实验环境:

  • Ubuntu 18
  • 4GB内存

new

class test {
    public:
    + char a[1000 * 1000 * 33]; // 大于32MB时, 使用这个定义
    + char a[1000 * 33];             //  小于128KB时,使用这个定义
    test() {

    }
};

实验一:不停地new大于32MB的内存块,不释放(因为32MB是64为系统的mmap分配阈值的最大值,见上文glibc内存管理)

int main() {    
    for (unsigned long long i = 0; i < 300000; i++) {
        test* a=  new test;
    }
    return 0;
}

结果:由于内存不足,进程被杀死。

实验二:不停地new小于128KB地内存块,不释放

int main() {    
    for (unsigned long long i = 0; i < 300000000; i++) { // 多三个0
        test* a=  new test;
        // 直接崩了
    }
    return 0;
}

结果:由于内存不足,进程被杀死。

实验三:不停地new大于32MB的内存块,释放

int main() {    
    for (unsigned long long i = 0; i < 300000; i++) {
        test* a=  new test;
        delete a;
    }
    return 0;
}

结果: 正常退出,无事发生,用free观察剩余内存容量,也没有明显减少的现象

实验四:不停地new小于128KB的内存块,释放

int main() {    
    for (unsigned long long i = 0; i < 300000000; i++) {
        printf("i = %lld\n", i);
        test* a=  new test;
        delete a;
    }
    return 0;
}

结果: 无事发生,用free观察剩余内存容量,也没有明显减少的现象。因为这里分配释放的都是同一大小的内存块,glibc的缓存效应没有体现。

如何体现malloc的缓存效应?见下面的malloc实验

malloc

实验一:不停地malloc大于32MB的内存块,不操纵内存不释放

using namespace std;
int main() {
    for (unsigned long long i = 0; i < 300000; i++) {
        printf("i = %lld\n", i);
         char* a = (char*)malloc(1000 * 1000 * 33);
    }
    return 0;
}

实验结果视overcommit_memory参数的不同而不同

cat /proc/sys/vm/overcommit_memory

有三种值

#define OVERCOMMIT_GUESS		0
#define OVERCOMMIT_ALWAYS		1
#define OVERCOMMIT_NEVER		2
  1. OVERCOMMIT_GUESS 表示根据系统当前可用page frame进行判断,如果可用page frame大于申请的虚拟内存,则允许申请虚拟内存;
  2. OVERCOMMIT_ALWAYS 表示总是允许申请虚拟内存,没有任何限制;
  3. OVERCOMMIT_NEVER 表示不允许超过系统设置的虚拟内存限制。

可以这样修改这个值:

echo 1 > /proc/sys/vm/overcommit_memory

如果overcommit_memory = 0, 则进程被kill,即时只是分配虚拟内存,在这种情况下也会考虑物理内存的容量。

如果overcommit_memory = 1, 进程没有奔溃,这表示我们可以申请大于物理内存的虚拟内存,但是运行后的可用内存空间急剧减少:

image-20230515204704425

猜想占用的内存空间都是被内核申请的,因为需要大量的数据结构描述本进程对内存的需要,比如 struct mm_struct, struct vm_area等结构

与new的实验一相比, 可以知道,OS执行的是懒分配策略,如果不去操作新分配的内存,那么OS不会为用户进程分配内存空间.

实验二:不停地malloc小于128KB的内存块,不操纵内存,不释放

using namespace std;
int main() {

    for (unsigned long long i = 0; i < 300000000; i++) { // 比实验一多3个0
         char* a = (char*)malloc(1000  * 33);  // 比实验一少3个0
    }
   
    return 0;
}

结果: 进程奔溃,与实验一相比,分配同样的内存,且不操作内存也不释放内存,但实验一没有崩溃,但是实验二奔溃了,这其中差别就在于mmap和brk的差距,实验一分配的内存块大于阈值因此使用mmap分配内存,但实验二分配的内存块较小,使用brk来分配内存。这可以说明mmap"很懒",它只分配必要的数据结构,而brk则显得比较"勤快". 之前看过linux内核源码情景解析中的mmap分析,发现mmap在一开始连页表都没有创建, 但是brk会手动地调用hanle_mm_fault分配物理页面:

// linux2.4 内核源码
// sys_brk() => do_brk() => make_pages_present => handle_mm_fault
int make_pages_present(unsigned long addr, unsigned long end)
{
	int write;
	struct mm_struct *mm = current->mm;
	struct vm_area_struct * vma;

	vma = find_vma(mm, addr);
	write = (vma->vm_flags & VM_WRITE) != 0;
	if (addr >= end)
		BUG();
	do {
		if (handle_mm_fault(mm, vma, addr, write) < 0)  // 立刻将物理页面分配
			return -1;
		addr += PAGE_SIZE;
	} while (addr < end);
	return 0;
}

而使用mmap分配的内存块,将要等到触发缺页处理时才会调用handle_mm_fault。

实验三:不停地malloc大于32MB的内存块,操纵内存释放

与new的对应实验相同. 正常退出

实验四: 不停地malloc小于128KBMB的内存块,操纵内存释放

如果内存块大小相同与new的对应实验相同.

在这里试一下不同内存块,看看glibc是否有缓存效应

int main() {    
    int mod = 1024 * 100; // 随即分配 1 - 100 KB的内存块
    for (unsigned long long i = 0; i < 300000000; i++) {
         printf("i = %lld\n", i);
         char* a = (char*)malloc(rand() % mod);
         free(a);
    }
    return 0;
}

结果:剩余空间会减少,但是每当剩余空间很少时,下一刻的剩余空间又多了起来。这表明了glib的缓存效应,而且当内存不足时,glibc会从fastbins中合并空闲内存块。至于耗尽所有内存的情况,我不能复现出来,因为这种情况的复现还需要避免空闲块与topchunk的合并。

malloc 一共能分配多少内存

对于这个问题,我想面试官主要想问的是最大可以申请多少虚拟内存,之后也可以引申出虚拟内存和实际物理内存之类的话题。

理论上,我们可以申请OS允许的全部虚拟内存(如果仅仅是申请,而不使用它的话)。在64位的linux机器上,规定的用户使用的虚拟内存大小为128T,所以按理说,malloc最多能够分配接近128TB(但一定比理想值小,因为进程本身资源也占用虚拟地址空间,所以最大分配内存约为127TB)的虚拟内存。

但是实际上,这个问题受很多方面的制约。下面开始我自己的简陋实验。

实验环境:

  • Ubuntu 18
  • 4GB内存

程序:

int main (int argc, char* argv[])
{
    unsigned int block_size = 1024*1024*1024;
    void* addr = NULL;    
    unsigned long long i = 0;
    while (1) {
        addr = malloc(block_size);
         // memset(addr, 0, block_size); // 不使用内存 !
        if (!addr) break;
        i++;
        printf("i = %lld\n", i);
    }
    printf("malloc %lld GB, aroud %lf TB\n", i, i / 1024.0);
    getchar();
     return 0;
 }
  1. 实际结果受overcommit_memory参数的影响(见上一个实验),该参数为0时 ,我们在4GB的内存上怎么都不可能分配127TB的虚拟空间。

    当该参数为1时,可得总共的可申请虚拟内存空间为127TB左右:

image-20230516110521845

  1. 但是,仅从这个现象得出“64位linux上,最多能分配127TB虚拟空间”的结论并不严谨。

    通过上一个实验,我们知道即使我们只分配空间但不去使用它,也会消耗实际物理空间,这些实际物理空间是内核使用的,用来记录用户的对内存的需求。

    可以使用free查看程序运行前后的剩余地址空间:

    image-20230516111239152

    可以看到程序后的剩余空间发生急剧变化,内核大约使用了2GB的物理内存来记录用户对内存的需求。

    由于我的实验环境的内存大小为4GB,因此能够顺利地分配127TB的虚拟空间,但假设使用的环境的内存只有2GB,那么用户能够分配的最大的虚拟空间应该是不足127TB的,因为内核没有办法申请更多的物理内存去记录进程对内存的需求了。

上述讨论的大前提是用户只剩请空间但不使用,但在绝大部分情况下,我们是要去使用的。

int main (int argc, char* argv[])
{
    unsigned int block_size = 1024*1024*1024;
    void* addr = NULL;    
    unsigned long long i = 0;
    while (1) {
        addr = malloc(block_size);
        memset(addr, 0, block_size); // 使用内存 !
        if (!addr) break;
        i++;
        printf("i = %lld\n", i);
    }
    printf("malloc %lld GB, aroud %lf TB\n", i, i / 1024.0);
    getchar();
     return 0;
 }

在这种情况下,根据之前的实验,我们应该只能申请真实物理内存 + swap空间大小的空间。

在运行程序前查看可用的空间,剩余内存 + swap空间的可用大小大约有3GB左右。

image-20230516113616672

然后运行程序:

image-20230516113732800

分配了3次,也就是3GB后,被OS杀死,符合我的猜想。

此外,分配内存块的大小也影响实验结果,下面的代码每次分配1KB的内存,且不去使用它:

int main (int argc, char* argv[])
{
    unsigned int block_size = 1024; // 1 KB 小内存块
    void* addr = NULL;    
    unsigned long long i = 0;
    while (1) {
        addr = malloc(block_size);
        // memset(addr, 0, block_size); // 不使用内存 !
        if (!addr) break;
        i++;
        printf("i = %lld\n", i);
    }
    printf("malloc %lld GB, aroud %lf TB\n", i / 1024/ 1024, i / 1024.0/ 1024/1024);
    getchar();
     return 0;
 }

在运行程序前查看可用的空间,剩余内存 + swap空间的可用大小大约有1.5GB左右:

image-20230713200017970

然后运行程序:

image-20230713200038283

当i增加到1586608时,进程被杀,换算一下也就是分配1.5GB左右的内存。也就说我们只能申请真实物理内存 + swap空间大小的空间。为什么?我们明明没有去使用这块内存,操作系统却已经把对应内存加载了?

这就是brk与mmap的区别。当申请小内存块时,我们首先向GLIBC库的缓冲中取得空闲块,如果GLIBC缓冲内有空闲块,则会调用brk申请内存。而brk不同于mmap,它真的会建立虚实地址映射关系,然后将物理内存载入。这也就是为什么,用户代码明明没有去操作分配的内存块,但是分配内存大小却受限于真实物理内存大小。

总结

对于malloc最大能够分配的多少虚拟内存这个问题,理论上64位Linux操作系统最多能够分配128TB的虚拟内存内存,但是实际上又受很多限制:

  • 分配内存块的大小,小内存块通过brk分配,大内存块通过mmap分配,这两个系统调用的区别较大。
  • malloc内存块后是否会使用这块内存。
  • 物理内存的大小。是的物理内存确实也能限制虚拟内存的最大总数,如果物理内存太小,以至于内核不能申请vm_area结构来表达用户进程的内存需求,那么进程能够申请的最大虚拟内存同样会减小。
posted @ 2023-04-10 21:27  别杀那头猪  阅读(240)  评论(0编辑  收藏  举报