0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

内存剖析:从用户态到内核态内存都做了什么?

Linux阅码场 来源:Linux阅码场 作者:Linux阅码场 2023-01-06 11:04 次阅读

编者按:本文顺着c++关键字new向下,旨在分析介绍底层各层到底做了什么,为什么这么做。

1.c++用户层

1.1提供的接口

1.1.1new

l 调用operator new 从自由存储区分配一块足够大的内存(sizeof(结构))

l 调用相应的构造函数

l 构造完成后返回指向该对象的指针

1.1.2delete

l 调用相应的析构函数

l 调用operator delete将内存归还给自由存储区

1.1.3new数组

l 调用operator new[] 从自由存储区分配一块足够大的内存(sizeof(结构)+用区分对象数组指针和对象指针以及对象数组大小的额外数据),注意简单对象(即不需要构造函数的类型)将不会有额外数据的申请。

l 依次在内存中调用相应的构造函数

l 构造完成后返回指向该对象数组的起始地址,不包括前面的额外数据部分。

1.1.4delete数组

l 获取数组起始地址前面的额外数据,计算出数组长度

l 根据数据长度依次调用相应的析构函数

l调用operator delete将内存归还给自由存储区

1.2operator new 的三种形式

形式1.void* operator new (std::size_t size)throw (std::bad_alloc);

形式2.void* operator new (std::size_t size,const std::nothrow_t& nothrow_value) throw();

形式3.void* operator new (std::size_t size,void* ptr) throw();

形式1跟形式2的区别仅仅是是否抛出异常,当分配失败时,前者会抛出bad_alloc异常,后者返回NULL,不会抛出异常。它们都分配一个固定大小的连续内存。

形式3又被称为placement new,它多接收一个ptr参数,并且只是简单地返回该ptr。调用形式为 A* a=new(ptr)A()。在内存池中有广泛应用,ptr即来自自由存储区,可以是堆、栈或者预分配的内存块。

上述形式1和形式2都可以被重载,遵循作用域覆盖原则,即在里向外寻找operator new的重载时,只要找到operator new()函数就不再向外查找,如果参数符合则通过,如果参数不符合则报错,而不管全局是否还有相匹配的函数原型。

注意在形式1中,如果new分配异常,将抛出异常导致后续代码不能被正常执行。即如果在new操作后有解锁操作,该解锁操作将不会执行导致死锁。

1.3设定内存分配失败入口函数

poYBAGO3kQCAC5egAACcaTL7jkU638.jpg

1.4自由存储区和堆的区别

从技术上来说,堆是C语言操作系统的术语。堆是操作系统所维护的一块特殊内存,它提供了动态分配的功能,当运行程序调用malloc()时就会从中分配,稍后调用free可把内存交还。而自由存储是C++中通过new和delete动态分配和释放对象的抽象概念,通过new来申请的内存区域可称为自由存储区。基本上,所有的C++编译器默认使用堆来实现自由存储,也即是缺省的全局运算符new和delete也许会按照malloc和free的方式来被实现,这时藉由new运算符分配的对象,说它在堆上也对,说它在自由存储区上也正确。但程序员也可以通过重载操作符,改用其他内存来实现自由存储,例如全局变量做的对象池,这时自由存储区就区别于堆了。

我们只需要记住:堆是操作系统维护的一块内存,而自由存储是C++中通过new与delete动态分配和释放对象的抽象概念。堆与自由存储区并不等价。这种区分大概是不同语言背景造成的。

1.5默认内存初始值

在vs2008(32bit)的debug模式下,由堆分配的内存初始值为0xcdcd,中文“屯”;由栈分配的内存初始值为0xcccc,中文“烫”。

1.6重载::operator new的理由

l 定位检查代码中内存错误

l 优化内存分配性能

l 获得内存使用统计数据

1.7重载::operator new的两种方式

方式1:不改变签名,替换系统现有版本

void* operator new(size_t size);

void operator delete(void* p);

使用方不需要包含任何特殊的头文件,也就是说不需要看见这两个函数声明。“性能优化”通常用这种方式。

方式2:增加新参数

// 其返回的指针必须能被普通的 ::operator delete(void*) 释放

void* operator new(size_t size, const char* file, int line);

Foo* p = new (__FILE, __LINE__) Foo;

也可以用宏替换 new 来节省打字。此种方式使用方需要看到这两个函数声明,也就是说要主动包含提供的头文件。“检测内存错误”和“统计内存使用情况”通常会用这种方式重载。

1.8重载::operator new的困境

1.8.1绝不能在library中重载::operator new

如果以上文提到的方式1来重载全局的::operator new,非常具有侵略性。使用该library的程序被迫使用了被重载的::operator new,并且一旦有另外的library也同样重载了::operator new,就将会导致链接问题。

那么如果采用上文提到的方式2来额外提供一个::operator new 版本呢,那就需要考虑重载后的::operator new 返回的指针能否被系统默认的::operator delete释放。如果不兼容系统则需要以方式1重载::operator new ,回到了上文提过的问题。如果兼容,那么在新版本的::operator new中能做的事比较有限,比如不能额外申请内存记录统计信息,除非定义一个包含统计信息的基类来作为所有申请对象的父类,但这样就相当于设定了开发规范,稍有不注意可能就会出错。

1.8.2使用重载带新参数的版本会有什么影响

如果使用方式1重载::operator new 使用起来似乎没有什么问题,但要考虑上节中提到的链接问题。

如果使用方式2来重载::operator new,分成以下两种场合。

对于以头文件形式提供的library,可以在所有的cpp实现文件起始部分包含重载::operator new 的头文件,但这具有侵略性。

对于以头文件加二进制库提供的library,实际上带新参数的版本并不会被这些库使用。

1.9单独为特定类重载成员函数operator new怎么样

与全局 ::operator new() 不同,per-class operator new() 和 operator delete () 的影响面要小得多,它只影响本 class 及其派生类。似乎重载 member operator new() 是可行的。但是我并不赞同这种做法。

如果一个类需要重载成员函数operator new(),说明它用到了特殊的内存分配策略,常见的情况是使用了内存池或对象池。宁愿把这一事实明显地摆出来,而不是改变 new的默认行为。

这可以归结为最小惊讶原则:如果我们在代码里读到 Node* p = new Node,通常我们会认为它在堆上分配了内存,如果 Node 类重载了成员函数operator new(),那么就需要事先仔细阅读 node.h 才能发现其实这行代码使用了私有的内存池。为什么不写得明确一点呢?如果写成Node*p = Node::createNode(),那么我们可能能猜到 Node::createNode() 肯定做了什么与 new不一样的事情,免得将来大吃一惊。

1.10代替重载::operator new的方案

从glibc的malloc入手,替换掉malloc。具体方式参考tcmalloc中的override方式,点此链接[1]。

主要使用了gcc提供的alias别名属性和weak属性,我们能实现替换掉系统默认的malloc原因在于系统提供的malloc系列函数都是被weak属性修饰的。

对于全局函数,如果没有显示修饰称weak属性,那么他属于强符号;对于全局变量,已初始化完毕的属于强符号,没有初始化完毕的则属于弱符号。

有如下3点规则:

l 链接时强弱符号都存在时以强符号为准;

l 链接时如果只有弱符号时以弱符号为准;

l 链接时如两个都是弱符号,则以内存占用大小较大的那个符号为准;

2.glibc层

2.1概述

实际上glibc采用了一种批发和零售的方式来管理内存。glibc每次通过系统调用的方式申请一大块内存(虚拟内存),当进程申请内存时,glibc就从自己获得的内存中取出一块给进程。

glibc对于heap内存申请大于128k的内存申请,glibc采用mmap的方式向内核申请内存,也就是此时的malloc是由mmap来实现的,这不能保证内存地址向上增长;小于128k的则采用brk,malloc调用系统调用brk来实现向内核批发虚拟内存,对于它来讲是正确的。128k的阀值,可以通过glibc的库函数进行设置。

审核编辑:汤梓红

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 内核
    +关注

    关注

    3

    文章

    1359

    浏览量

    40183
  • 内存
    +关注

    关注

    8

    文章

    2962

    浏览量

    73803
  • 函数
    +关注

    关注

    3

    文章

    4276

    浏览量

    62314
  • C++
    C++
    +关注

    关注

    21

    文章

    2097

    浏览量

    73452

原文标题:内存剖析:从用户态到内核态内存都做了什么?

文章出处:【微信号:LinuxDev,微信公众号:Linux阅码场】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    Linux 内存管理知识学习经验总结

    内核用户两部分,经典比例如下:用户
    发表于 02-25 17:08

    Linux内核下如何读写IIC

    目前在Linux3.12上,想在内核下读取LM75温度传感器的温度值,做了如下操作,但是读数据的时候i2c_transfer一直报错。先将LM75设备挂到IIC总线上:在sys下可以发现已经添加成功:但是读数据的时候就一直报错
    发表于 11-29 19:07

    Linux内存系统:内存使用场景

    文件映射、共享内存)· 程序的内存 map(栈、堆、code、data)· 内核用户的数据传递(copy_from_user、copy
    发表于 08-25 07:42

    Linux内存系统---走进Linux 内存

    内存区域· MMAP:共享库及匿名文件的映射区域· STACK:用户进程栈7、内核地址空间 · 直接映射区:线性空间中 3G 开始最大
    发表于 08-26 08:05

    操作系统为什么分内核用户?这两者如何切换?

    操作系统为什么分内核用户,这两者如何切换?进程在地址空间会划分为哪些区域?堆和栈有什么区别?
    发表于 07-23 09:01

    请问CPU与寄存器,内核用户及如何切换?

    计算机硬件系统由哪几部分构成?编程语言的作用及与操作系统和硬件的关系是什么?请问CPU与寄存器,内核用户及如何切换?
    发表于 10-25 06:31

    OpenHarmony喂狗源码解读之用户源码

    timeout\n"); } else {// 用户设置喂狗超时时间为大于gap 用户喂狗间隔时间为// 获取内核的超时间 - gap
    发表于 01-26 10:57

    鸿蒙内核实现用户快速互斥锁Futex设计资料合集

    Futex(Fast userspace mutex,用户快速互斥锁),系列篇简称 快锁 ,是一个在 Linux 上实现锁定和构建高级抽象锁如信号量和POSIX互斥的基本工具,它第一次出现在
    发表于 03-23 14:12

    Linux虚拟内存和物理内存的深刻分析

    内存用户进程总是先获得一个虚拟内存区的使用权,最终通过缺页异常获得一块真正的物理内存。物理内存内核
    发表于 05-31 08:00

    一个内核Key-Value存储系统

    的数据存储需求中尤为突出。针对该问题,给出了一个内核Key-Value存储系统的实现-KStore:提供内核空间的索引和内存分配机制,并在此基础上,通过基于
    发表于 01-19 16:37 0次下载
    一个<b class='flag-5'>内核</b><b class='flag-5'>态</b>Key-Value存储系统

    详解Linux的物理内存

    内核申请内存比在用户申请内存要更为直接,它没有采用用户
    的头像 发表于 01-18 17:45 2384次阅读
    详解Linux的物理<b class='flag-5'>内存</b>

    Linux内核缺页会发生什么 - 玩转Exception fixup表

    Linux内核的做法是提供了一张 异常处理表 ,使用专有的函数来访问用户内存。类似 try-catch块一般。具体详情可参见copy_to_user/copy_from_user的实
    的头像 发表于 06-03 15:08 2927次阅读

    探究slab在内核内存管理和用户Memcached的双重存在

    ,但是作为内核的堆用户本身,经常只是调用kmalloc()申请一个小内存,或者调用kmem_cache_alloc()申请一个数据结构,2^n页给它,会形成大量碎片浪费。所以slab找buddy要了2
    的头像 发表于 08-13 14:55 1425次阅读
    探究slab在<b class='flag-5'>内核</b><b class='flag-5'>内存</b>管理和<b class='flag-5'>用户</b><b class='flag-5'>态</b>Memcached的双重存在

    Linux内核用户是如何睡眠的

    clock_nanosleep系统调用来进行睡眠(也就是说用户任务睡眠需要调用系统调用陷入内核)。 下面我们来研究下clock_nanosleep的实现(这里集中睡眠的实现,先忽略
    的头像 发表于 08-16 15:06 1855次阅读

    如何实现一个高性能内存

    ,按照惯例先说内存池的应用场景。 为什么我们需要内存池? 因为malloc等分配内存的方式,需要涉及系统调用sbrk,频繁的malloc和free会消耗系统资源。 既然如此,我们就预
    的头像 发表于 11-10 11:11 612次阅读
    如何实现一个高性能<b class='flag-5'>内存</b>池