从栈到信号:操作系统如何操作、切换和修改执行流
零.引言在操作系统中有很多的概念:线程、进程、调度、中断、信号、系统调用……教材分别给它们下出定义:线程是CPU调度的单位,进程是资源分配的基本单位…..但当这些机制同时出现时,一个问题很容易产生:我只知道它们本身是什么,但是它们之间到底是什么关系?为什么要有这些概念? 再具体到实现层面,可能出现下面的问题:为什么线程切换需要保存寄存器和栈?信号处理函数是怎么“插入”到用户执行程序中的?为什么系统调用需要切换到内核栈?为什么调度的核心操作是保存和恢复上下文? 实际上,这些问题都在围绕着同一个事情:暂停一个正在执行的程序,然后在某个时刻恢复它。什么意思?无论是线程切换调度还是中断处理,操作系统都必须完成一个基本的操作:在某个时刻“冻结”当前程序的执行状态,并在未来恢复它。 这就引入了另一个更基础的问题:**“程序的执行状态”到底是由什么状态构成的?**换句话说,如果我们想暂停一个程序,然后在未来继续执行,究竟要保存哪些信息? 从计算机的体系结构来看,这些状态主要包括三部分: 程序计数器(PC):当前/下一步执行哪条指令 寄存器:当前计算的中间结果 调用栈:记录函数的...
回调 委托
本篇文章主要讲解委托的底层原理,但在此之前,我们必须先理清委托究竟是什么——其不过是 callback 在 C# 中的具象化体现。因此,在真正进入委托的底层实现之前,我们需要先回到 callback 本身:它是如何工作的、为什么需要它、以及它在异步与解耦中的角色。理解了这些,再看委托的内部结构、多播机制和调用流程,就会变得顺理成章。 理解callback【callback是什么?】callback是一种思想,其核心原理是:你定义一个函数,将其作为参数交给某个框架、库,对方将其保存起来,等到某个条件满足时,比如资源加载完成、状态发生变化时,再执行该函数。 【为什么要有callback?】而之所以要有callback,其根本动机是为了实现控制权的反转,callback函数就像是一个钩子函数一样,你将你自己的逻辑“挂到”一个更大的系统中,将逻辑的调用权从你手里,转换到了框架手里,由框架在它自己的某个生命周期节点调用你的函数。从callback这个名字中也能感受到这层意思:不是我主动call你,而是你在合适的时机call back我 这样做的意义是可以让模块间解耦,这也是callbac...
【内存分配与管理】Unity GC
在上一篇文章中,我们深入剖析了Unity托管内存的分配机制,从Boehm GC的ok_freelist与GC_hblkfreelist两级结构,到小对象与大对象的差异化分配策略,再到hblk与hblkhdr分离设计的精妙之处。我们看到了Unity如何在C++引擎层搭建起一套高效的内存分配基础设施,为C#层的对象创建提供支撑。 然而,分配只是内存管理的一半,另一半,也是我们更为关切的痛点——垃圾回收。当托管堆上的对象不再被引用时,它们占据的内存需要被回收以供后续分配使用。Unity采用的Boehm GC是一个基于Mark-Sweep的保守式GC,它的工作原理与传统GC有着显著的不同:它无法精确区分指针与非指针数据,无法对内存进行整理压缩,也无法像.NET的GC那样实现分代回收。这些特性决定了Unity GC的性能特征和局限性。 本文将从GC的触发条件入手,深入剖析Mark阶段与Reclaim阶段的完整流程,包括根节点扫描、地址到header的映射机制、标记位的管理与回收策略。通过理解这些底层原理,我们将能更好地把握Unity内存管理的本质,为后续的优化实践打下坚实基础。 托管内存管...
【内存分配与管理】Unity 增量式GC
在上一篇文章中,我们深入剖析了 Unity 传统 Boehm GC 的完整工作机制——从触发条件、STW(Stop-The-World)线程暂停,到 Mark 阶段的递归可达性分析,再到 Sweep 阶段的分级回收策略。我们看到了保守式 GC 在设计与实现上的诸多权衡:无法整理内存、无法精确区分指针与数据、依赖全局扫描来完成回收。这些特性使得传统 GC 在一次回收中往往带来不可忽视的卡顿,尤其是在托管堆较大、对象存活率较高的游戏场景下。 然而,游戏对帧率稳定性的要求极为苛刻。一次 50ms 甚至 100ms 的主线程停顿,足以让玩家明显感知到“掉帧”或“卡一下”。为了解决这个问题,Unity 引入了增量式 GC(Incremental GC)。它并没有改变 Boehm GC 的基本算法,而是通过一种精巧的工程手段:将原本一次性完成的 Mark 阶段拆分成多个短时间的增量步骤,穿插在多帧中执行,从而把一次长时间的 STW 卡顿,转化为多次几乎不可感知的微停顿。 增量式 GC 的核心挑战在于:如何在 GC 与用户线程并发运行的前提下,仍然保证标记结果的正确性? 答案就是本文要重点讲...
【内存分配与管理】Unity内存分配
本文作为Unity内存管理系列的开篇,将聚焦于Unity的内存分配原理,深入剖析Boehm GC在托管内存分配中的具体实现,包括小对象与大对象的分配策略、空闲链表与内存池的设计、以及这些机制背后的性能考量。后续文章将进一步讲解Unity GC的回收机制与优化实践。 整体认识Unity具有两套内存——托管内存Managed Memory和原生内存Native Memory。 Managed Memory即C#层的堆内存,其中存储了C#代码分配的对象,即class实例对象、string、装箱等,由C#运行时(Mono/iL2CPP)管理 Native Memory即Unity底层的C++层的堆内存,其中存储了Unity引擎内部的资源,比如纹理,模型,音频以及GameObject本体等,需要我们手动进行资源的卸载以及引擎内部的生命周期管理 ┌─────────────────────────────────────────────────────────────┐ │ Operating System ...
【内存分配与管理】移动语义——参数传递
在探讨了移动语义的机制之后,我们面临一个更实际的问题:如何在实际的函数设计中充分利用这些机制? 移动语义、右值引用、完美转发为我们提供了强大的工具,但这些工具的最佳使用场景是什么?不同的参数传递方式又有怎样的性能特性和设计考量? 三种传参方案现代C++提供了多种参数传递策略:左值引用重载、右值引用重载、万能引用完美转发、按值传递移动。每种策略都有其适用场景和权衡点。理解这些选择背后的性能模型和设计哲学,是编写高效、健壮C++代码的关键。 对于函数传参的参数设计,常见的有以下几种形式: class Widget { // 途径一: public: // 针对左值和右值重载 void addName(const std::string& newName) { names.push_back(newName); } void addName(std::string&& newName) { names.push_back(std::move(newName)); } ....
【内存分配与管理】移动语义
在探讨了RAII和智能指针之后,我们看到C++如何通过所有权模型来管理堆上资源的生命周期。unique_ptr的“独占所有权”和shared_ptr的“共享所有权”,将资源管理的责任从模糊的约定提升为编译器可检查的类型系统特性。 然而,所有权思想的价值远不止于智能指针。当我们审视值语义对象时,一个自然的优化思路浮现了:如果某些对象只是临时存在的“中间产物”,我们能否不进行昂贵的深拷贝,而是“移动”其内部资源的所有权? 这就是C++11引入移动语义的核心动机。移动语义实际上是所有权思想在值语义领域的自然延伸: unique_ptr的移动是“堆资源所有权的转移” 普通对象的移动是“对象内部资源所有权的转移” 通过移动语义,C++实现了一个精妙的平衡:既保持了值语义的简洁性和确定性,又在需要时避免了不必要的拷贝开销。配合RVO/NRVO等编译器优化,现代C++让值语义对象既安全又高效。 本文将探讨移动语义的完整技术栈:从左值/右值的本质区分,到右值引用如何支持移动语义,再到完美转发如何解决通用代码中的值类别保持问题。 左值、纯右值与将亡值这些值类别是表达...
【内存分配与管理】RAII与智能指针
在上一篇文章中,我们探讨了C++为何选择值语义作为其核心设计哲学,以及这种选择如何深刻影响了C++的内存模型。我们看到了值语义带来的高效性,但也意识到并非所有对象都能简单地“被复制”——线程、文件、网络连接等资源天然具有唯一性,必须通过引用语义来管理。 这引出了一个更深层的问题:当C++程序员不得不使用引用语义(堆上对象)时,如何避免值语义所不具备的那些内存管理复杂性——内存泄漏、悬空指针、双重释放? 答案就在C++另一项核心思想中:RAII(Resource Acquisition Is Initialization)。 本文将探讨RAII如何将资源生命周期绑定到对象生命周期,以及智能指针如何通过明确的所有权模型,让堆上资源也能享受到类栈的自动管理。 RAII思想为什么需要RAII? 在没有RAII的情况下,有以下问题: 资源所有权不清晰,很容易出现资源泄露或double free的问题,比如类中有裸指针成员 资源释放路径不唯一,函数退出的方式太多,比如return、分支内提前return、抛异常等等,在代码复杂的情况下,无法完全保证每条路径都正确地释放资源,可能没释放,也...
【内存分配与管理】从值语义出发,理解C++内存管理设计逻辑
在具体展开C++内存管理之前,本篇文章将从值语义这一C++设计基石出发,理解C++内存管理的根因缘由。你将明白为什么C++选择这条看似复杂但极致高效的道路,以及这种选择如何深刻影响着从对象生命周期到智能指针设计的每一个角落。 值语义与C++值语义是指对象的复制会创建一个独立的副本,每个副本都有自己的数据,修改一个副本不会影响其他副本。简单来说,值语义对象就像是数学中的数值,彼此独立,互不干扰。相对的,引用语义指的是对象不直接存储数据,而是持有对数据的引用(指针、引用等),多个对象可以共享相同的数据,修改其中一个会影响其他所有共享该数据的对象。 C++的设计初衷是让用户定义的类型(class)可以像内置类型一样工作,具有同等的地位,让class本质上是值语义 Bjarne Stroustrup在《C++语言的设计与演化》中多次强调这一点: “在C++中,用户定义类型应与内置类型具有相同的支持、相同的语法使用方式和相同的效率。” “让用户定义的类型与内置类型一样工作良好的目标,是C++抽象机制设计的中心思想。” “C++的类机制允许程序员定义与内置类型在使用方式、行为复杂性和效率方...
【内存分配与管理】理解new与placement new
前三篇文章中,我们已经了解了malloc的底层实现以及与我们平时接触最为密切的的堆栈内存分配,本篇文章将从malloc的底层机制到new的面向对象封装,了解new的设计理念、底层实现以及正确用法 new表达式我们先来从最表层来对比一下malloc与new:1.概念上的区别 malloc/free是C/C++标准库函数,而new/delete是C++关键字,或者说是new表达式。因此malloc仅仅只分配内存,而不会进行初始化类成员的工作,new不止分配内存,而且还调用类的构造函数。 2.返回类型的不同 new内存分配成功时候,返回的是对象类型的指针,不需要进行类型转换,从这个角度来说比较安全malloc内存分配成功则返回 void类型(泛型指针),必须通过强制类型转换将 void* 转换成需要的类型。 3.分配失败后的返回值不同 malloc失败,会返回空指针。new失败,默认是抛出异常,要捕获异常bad_alloc 4.是否需要指定内存大小 new操作符申请内存的时候不需要指定内存块的...