<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>基础 on 元闰子的邀请</title>
    <link>https://yrunz.com/categories/%E5%9F%BA%E7%A1%80/</link>
    <description>Recent content in 基础 on 元闰子的邀请</description>
    <generator>Hugo -- gohugo.io</generator>
    <language>en-us</language>
    <lastBuildDate>Sun, 19 Oct 2025 00:00:00 +0000</lastBuildDate><atom:link href="https://yrunz.com/categories/%E5%9F%BA%E7%A1%80/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>怎么让程序更高效地连起来？</title>
      <link>https://yrunz.com/p/%E6%80%8E%E4%B9%88%E8%AE%A9%E7%A8%8B%E5%BA%8F%E6%9B%B4%E9%AB%98%E6%95%88%E5%9C%B0%E8%BF%9E%E8%B5%B7%E6%9D%A5/</link>
      <pubDate>Sun, 19 Oct 2025 00:00:00 +0000</pubDate>
      
      <guid>https://yrunz.com/p/%E6%80%8E%E4%B9%88%E8%AE%A9%E7%A8%8B%E5%BA%8F%E6%9B%B4%E9%AB%98%E6%95%88%E5%9C%B0%E8%BF%9E%E8%B5%B7%E6%9D%A5/</guid>
      <description>前言  本文将最简单的共享内存开始，讲述共享内存/TCP/DPDK/RDMA/CXL/NVLink/UB 等通信方式的基本原理，探讨如何让程序更高效地连起来。
 上图为计算机的简易组成，核（Core）是 CPU 的大脑，执行程序的业务逻辑；
内存 DRAM 通过 DDR 接口连接到 CPU 内的内存控制器（Memory Controller）中，存放程序运行所需的数据；
而网卡 NIC、SSD 等高速外围设备则通过 PCIe 接口接入 CPU，由 PCIe Root Complex 进行协议解析、路由等。
在《探索OS的内存管理原理》里我们提到，每个程序都有独立的内存地址空间，以防止其他程序的篡改。所以，程序 A 保存在内存里的业务数据，程序 B 是无法访问的。
然而，复杂的业务往往需要多个程序协同合作，比如，数据需要先后经过程序 A 和程序 B 的处理。
这时，要怎么把程序连起来，让程序 B 可以访问程序 A 的数据呢？
共享内存 这是一个进程间通信的问题，回忆操作系统原理，我们能想到的最简单的方式就是共享内存。
POSIX 提供了 shm_open 和 mmap 系统调用，它们配合可实现进程间的通信，简易代码如下：
// 程序A的代码 int procA(void) { // 1. 双方约定好的共享内存key  const char *key = &amp;#34;/proc_shm&amp;#34;; const size_t sz = sizeof(int); // 2. 创建并打开共享内存对象  int fd = shm_open(key, O_CREAT | O_RDWR, 0666); // 3.</description>
    </item>
    
    <item>
      <title>走技术路线需要些什么？</title>
      <link>https://yrunz.com/p/%E8%B5%B0%E6%8A%80%E6%9C%AF%E8%B7%AF%E7%BA%BF%E9%9C%80%E8%A6%81%E4%BA%9B%E4%BB%80%E4%B9%88/</link>
      <pubDate>Sun, 09 Mar 2025 00:00:00 +0000</pubDate>
      
      <guid>https://yrunz.com/p/%E8%B5%B0%E6%8A%80%E6%9C%AF%E8%B7%AF%E7%BA%BF%E9%9C%80%E8%A6%81%E4%BA%9B%E4%BB%80%E4%B9%88/</guid>
      <description>荐语 本文要介绍的是O&amp;rsquo;Reilly在2022年出版的书籍，《The Staff Engineer&amp;rsquo;s Path》。
书中主要介绍走技术路线的工程师，或者说技术专家（Staff Engineer），需要具备哪些能力他们不仅仅要会写代码、做系统设计这么简单，技术能力只是基础，还要具备大局观、执行力和影响力。
如果你想走技术路线，那么本书值得一读。
前言 程序员的发展，总会遇到一个分叉路口，走技术路线，或者管理路线。
程序员总有股技术情怀，相信很多人最初都有坚定走技术路线的想法，但走着走着，最后都走上管理路线。
一方面，管理路线是清晰的，且有着大量成功的案例。对于沟通能力强、临危不乱、善于帮助周边同事成长的人来说，似乎成为管理者是一个常见，也是默认的职业发展规划。
另一方面，走技术路线，在很多人的理解里，就是不断提升技术能力。但是，这样走了一段路后，很可能就会遇到了向上发展的瓶颈，而转向管理路线。于是就有了所谓“不能一直搞技术”的结论。
然而，走技术路线遇到发展瓶颈的原因，并非是“不能一直搞技术”，而是“不能只搞技术！”
要走好技术路线，成为一名技术专家，拥有扎实的技术功底只是基础。除此之外，还需要具备全面的大局观、强大的执行力、良好的影响力三大能力支柱。
下面，我们将从技术专家的角色定位讲起，随后介绍这三大能力支柱，让你对“技术路线”有更深刻的理解。
技术专家是个什么角色？ 提到技术专家的角色定位，我们可能首先会想到“关键方案设计和开发”、“疑难问题解决”、“技术决策”，但还不止这些。
不是Manager，但也是个Leader 首先，技术专家也是一个Leader。
在技术路线上，深入技术领域只能让你走到高级工程师这一步，要想成为技术专家，主导更大的项目，你需要与更多的人沟通协作、说服他们接受你的方案、成为他们的榜样。
所以，技术专家不可避免地要成为一个Leader。
与Manager不同，技术专家通常没有直接下属，不负责人员管理，但他们同样需要在技术决策、设计和代码review等方面表现出领导力。
是个技术牛人 技术专家是一个高度专业化的角色。
你提出的设计和代码review意见，要有指导意义，能够让产品的代码和架构变得更好；做技术决策时，要理解各类因素的权衡，必要时能够深入代码细节，提出正确的问题。
所以，技术专家必须要有扎实的技术基础。
但这并不意味着技术专家要写大量的代码。为了高效地解决问题，很多时候，他们负责方案设计、指导中高级工程师完成编码，可能更有意义，这也是给其他人更多的成长机会。
牢记一点，技术专家最重要的是解决问题，而不是如何解决，代码、文档，都是一种方式。
了解了技术专家的角色定位，接下来本文将介绍技术专家所需要的，除技术基础之外的，三大能力支柱：大局观、执行力、影响力。
大局观 一个不断发展的组织，总会进行各种各样的决策：选择什么技术、打造什么产品、继续投资或者否决它。一个好的决策需要有详细、准确、全局的上下文知识来支撑。
一名技术专家很可能只是负责组织下某个产品或特性的技术方案，因此，很容易陷入局部最优的困境。
为保证能做出前瞻性的决策，技术专家需要成为一个纵观全局的人。而建立下面的这三张地图，也许能够帮你构筑大局观。
定位图（Locator Map）：广阔的视角 要避免局部最大化，需要建立一张定位图（Locator Map）以获得更广阔的视角：
 认清组织目标，找到真正重要的是什么。程序员很容易陷入唯技术论，技术专家要避免这一点。技术只是达成目标的手段，最重要的是搞清楚目标，是组织的目标，不是某个特性或小团队的目标。 理解客户的真正痛点。产品是为用户服务的，就算你的产品能够提升XX百倍性能，没有用户用，一样是没有价值。 了解领域行业动态。你正在解决的问题，很可能业界已经有人解决过。时刻了解你所在行业的动态，才能更高效地解决问题，更好地实现创新。  地形图（Topographical Map）：正确的导航 即使你有很强的技术能力，也可能会遇到好想法得不到重视、风险/困难无法提前识别、项目进展难以推动的情况。这时，你需要建立一张地形图（Topographical Map），帮助你实现技术落地、项目成功：
 理清组织结构，建立良好关系。了解你所在组织的结构，理清每个子团队所负责的业务以及关键角色，并于他们建立良好的关系。没错，技术专家也需要懂得“搞政治”，这样才能更好地开展工作。 了解组织文化。你的组织是倡导口头汇报还是书面汇报？鼓励快速试错还是深思熟虑后的变革？了解并融入你所在组织的文化，金子才能更好地发光。 保持地形图更新。为了更好地支撑商业成功，组织结构和人员的变化很正常，时刻更新你的地形图。  藏宝图（Treasure Map）：长远的目标 最后，你需要一张藏宝图（Treasure Map），找到自己的长远目标（通常是组织目标或者能够支撑组织目标的达成），并为之奋斗。
人们很容易过度关注短期目标，比如，当前版本发布的功能、某个用户对产品的差评等。寻求短期成功可能会限制你的发展。
要从更宏观的角度思考：
你想要达到什么目标？
你为什么要做这些事情？
这些事情能支撑你的目标达成吗？
本周的工作是否偏离了你的目标？
你是否还在不断接近你的目标？
执行力 技术专家需要很强的自驱力，要学会自己给自己安排任务，主动、提前为组织出谋划策，而不是等待被安排。
所以，技术专家通常会参与多个项目，处理很多事情。但人的精力总是有限的，学会时间和价值管理很重要。
可以尝试按照以下几个维度，为你所参与的项目/事宜进行打分，在有限的时间内完成最有价值的事情：
 能量（Energy）：这件事情将会耗费你多大的精力。 生活质量（Quantity of Life）：如果你的工作恰好是你的兴趣，那么你每天的生活质量将会非常的高。但这种机会是可遇不可求的，很多时候工作会降低你的幸福感。 信誉（Credibility）：信誉是领导力的一种体现，主导重要的项目、解决关键的难题，都能帮你提升在组织里的信誉。 社交资本（Social capital）：花时间与他人相处、与他们一起工作，建立良好的社交关系，互为战友。 技能（Skills）：技能是一种需要反复练习才能长久拥有的东西，你的工作内容一定程度上决定了你拥有哪些技能。  如何进行事情的优先级排序，一方面是要契合组织的目标，另一方面，也是你自己的职业规划。</description>
    </item>
    
    <item>
      <title>为什么CPU不能有更多的核？</title>
      <link>https://yrunz.com/p/%E4%B8%BA%E4%BB%80%E4%B9%88cpu%E4%B8%8D%E8%83%BD%E6%9C%89%E6%9B%B4%E5%A4%9A%E7%9A%84%E6%A0%B8/</link>
      <pubDate>Sun, 02 Mar 2025 00:00:00 +0000</pubDate>
      
      <guid>https://yrunz.com/p/%E4%B8%BA%E4%BB%80%E4%B9%88cpu%E4%B8%8D%E8%83%BD%E6%9C%89%E6%9B%B4%E5%A4%9A%E7%9A%84%E6%A0%B8/</guid>
      <description>前言 与GPU动辄成千上万个核不同，CPU核数目前仍处于百核级别。AMD基于最新Zen 5架构发布的产品中，核数最多的型号AMD EPYC™ 9965也只有192个核。
那么，为什么CPU不能有更多的核呢？
这是一个系统工程的问题，涉及到多种因素的权衡。本文将结合AMD Zen 5 EPYC 9005系列（下文简称Zen 5）的架构，从CPU制造流程讲起，带读者理解现代CPU架构的同时，解释为什么CPU不能有更多的核。
从CPU制造流程讲起 CPU的制造过程可以简单总结如下：
晶圆是制作CPU的主要材料，它由圆柱形单晶硅锭（用直拉法从熔融后的高纯度多晶硅中拉出）切片而成，目前主流的晶圆尺寸为12英寸，约7万mm²。
一片晶圆能够光刻若干个芯片，我们把这种未经封装的单个功能芯片称为Die（裸片）。我们很容易想到，如果把Die设计得足够大，一片晶圆只光刻一个Die，那么单Die的核数就能更多了。
然而，受光刻技术限制，当前光刻掩膜版最大面积约858mm²，一旦超过这一阈值，需通过多次曝光拼接完成光刻，但会导致工艺复杂度增加和良率显著下降。
另外，即使在最大光刻掩膜版面积内，Die的面积也不是越大越好。通常来说，晶圆都有一定的缺陷密度，考虑极限场景，晶圆面积=光刻掩膜版最大面积，如下图所示，Die的面积越小，良品率就越高。
所以，出于成本的考虑，追求更高的良品率会限制芯片的面积，从而限制了芯片核的数量。
既然单块芯片面积受限，那能不能把多块小芯片组合成一块大芯片呢？
当然可以！
Chiplet架构：把小芯片组合起来 Chiplet是一种将传统单片式系统级芯片（SoC）拆分为多个功能模块化小芯片（Die），再通过先进封装技术（如2.5D/3D堆叠）互联集成的设计架构。其核心思想是通过标准化接口将不同工艺节点、不同功能的Die灵活组合，形成高性能异构系统芯片。
所以，Chiplet既突破了传统单芯片的物理极限（858mm²），也兼顾架构设计的灵活性。
Chiplet架构下，Die通常会被分成CPU Die（CCD）和IO Die（IOD）两类。
 CPU Die负责高频计算，集成CPU Core（核）、高速缓存（L2/L3 Cache）等，采用先进的工艺，提供更高的计算密度； IO Die负责资源管控，集成内存控制器、PCIe总线、SATA接口等，采用成熟的工艺，更好保证信号的完整性。  AMD Zen 5采用Chiplet架构设计，一块完整的芯片包含多个CPU Die和1个IO Die。其中，CPU Die采用3nm/4nm工艺，IO Die采用6nm工艺。
虽然Chiplet理论上支持，但通常不会为了得到更多的核而无限制地扩展CPU Die。比如，AMD Zen 5一个CPU Die上有8个核，当前最多支持集成16个CPU Die，共计128核。
其中原因主要受以下几点限制：
 功耗和散热问题。集成更多的CPU Die意味着更高的功耗，散热可能成为问题，会导致芯片温度过高，影响晶体管的寿命。 内存带宽受限。一块芯片上通常只有1个IO Die，其DDR内存通道数是固定的，比如，AMD Zen 5的内存通道数为12。更多的核意味着内存带宽竞争更大，平均单核内存带宽更低。 缓存一致性机制更复杂。虽然可以通过增加IO Die的数量来提升内存带宽，但这同样受功耗和散热问题限制，而且更多的CPU Die会导致缓存一致性机制过于复杂，得不偿失。  在拥有一定面积的芯片上，既然无法通过扩展更多的CPU Die来增加核数，那么能否提升单个CPU Die的核数呢？
当然可以！通过把核做得更小，就能让一定面积下的CPU Die拥有更多的核。
更小的核 以AMD Zen 5 EPYC 9005系列为例。</description>
    </item>
    
    <item>
      <title>深入理解分支预测原理，揭开AMD Zen5的高性能秘诀</title>
      <link>https://yrunz.com/p/%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3%E5%88%86%E6%94%AF%E9%A2%84%E6%B5%8B%E5%8E%9F%E7%90%86%E6%8F%AD%E5%BC%80amd-zen5%E7%9A%84%E9%AB%98%E6%80%A7%E8%83%BD%E7%A7%98%E8%AF%80/</link>
      <pubDate>Sun, 10 Nov 2024 00:00:00 +0000</pubDate>
      
      <guid>https://yrunz.com/p/%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3%E5%88%86%E6%94%AF%E9%A2%84%E6%B5%8B%E5%8E%9F%E7%90%86%E6%8F%AD%E5%BC%80amd-zen5%E7%9A%84%E9%AB%98%E6%80%A7%E8%83%BD%E7%A7%98%E8%AF%80/</guid>
      <description>前言 今年，AMD Zen 5架构随着AMD旗下锐龙、霄龙等新一代处理器的发布而亮相。Zen 5对比上一代架构 Zen 4在性能上有了大幅提升，其中，IPC（每时钟周期执行的指令数量）更是提升了16%。
在Zen 5 高性能的背后，分支预测器的优化功不可没，其中，对2-ahead分支预测的支持尤为重要。
本文将从CPU分支预测原理讲起，试图带大家理解Zen 5在分支预测上的优化，揭开性能提升的秘密。
 分支预测大体可以分成两部分，分支方向的预测和分支目的地址的预测，本文主要介绍前者。
 从CPU流水线说起 在现代计算机体系结构下，程序都会被转换成一条条指令，在CPU中执行。
我们能想到的最简单的指令执行流程是串行执行，比如现有指令A，B，C和D，那么，串行执行流程如下：
然而，串行执行的效率无疑是低下的。为了充分榨干CPU的性能，指令流水线出现了，它把指令执行的流程划分成多个阶段，每个阶段可以独立执行。比如，下图中为2级流水线的示例图：
在流水线下，虽然单个指令执行的耗时不变，但指令执行的吞吐量增大了。
分支指令是众多指令中的一种，它伴随着程序中 if/for/while 等可能出现分支跳转的代码而出现。
if (x == 0) { // doTingA } else { // doTingB } 上述的代码，可能会转换成如下的汇编代码：
branch_if_not_equal x, 0, else_label // doTingA goto end_label else_label: // doTingB end_label: // whatever happens later 分支指令的出现使得流水线执行变回了串行，因为在branch_if_not_equal执行完之前，CPU无法不知道下一步应该是doTingA，还是doTingB。
我们可以加入分支预测的机制，提前“猜测”下一条指令，虽然猜错了会有惩罚代价，但只要我们猜得足够准确，CPU的性能就会得到大幅的提升。
分支预测的发展 静态分支预测 静态分支预测机制最简单，可认为是工程师根据大多数程序的特征总结出来的规律，常见的有2种机制：
 Alway taken。每次都执行跳转操作，对循环逻辑效果很好。 BTFNT。如果目的地址为向前，则选择不跳转，向后则选择跳转。  静态分支预测机制实现简单、硬件开销低，但在复杂的程序上预测成功率低。因此，在现代处理器中占主导地位的为动态分支预测。
动态分支预测 动态分支预测基本都基于分支执行历史进行，这里有个关键前提**，分支行为是重复的，这意味着分支行为是可以学习和预测的**！
最简单的预测策略是基于最近一次分支执行的结果。在实现上通常使用1 bit来存储最近一次分支执行结果，1表示跳转（taken），0表示不跳转（no taken）。
上图中，分支执行历史被存放到PHT（Pattern History Table）上，并通过分支指令的地址（PC）进行索引。</description>
    </item>
    
    <item>
      <title>深入理解计算机系统的数值类型</title>
      <link>https://yrunz.com/p/%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%B3%BB%E7%BB%9F%E7%9A%84%E6%95%B0%E5%80%BC%E7%B1%BB%E5%9E%8B/</link>
      <pubDate>Thu, 18 Aug 2022 00:00:00 +0000</pubDate>
      
      <guid>https://yrunz.com/p/%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%B3%BB%E7%BB%9F%E7%9A%84%E6%95%B0%E5%80%BC%E7%B1%BB%E5%9E%8B/</guid>
      <description>前言 在日常编程中，数值类型（numeric types）是我们打交道最多的类型，可能没有之一。除了最熟悉的 int，还有 long、float、double 等。正因太熟悉，我们往往不会深究它们的底层原理。因为平时的工作中，知道个大概，也够用了。
但，在某些业务场景下，比如金融业务，数值运算不准确会带来灾难性的后果。这时，你就必须清楚数值类型的二进制表示、截断、转型等原理，否则很难保证运算结果的正确性。
另外，数值类型也是一个容易被黑客攻击的点，考虑如下一段代码：
// C++ /* Declaration of library function memcpy */ void *memcpy(void *dest, void *src, size_t n); /* Kernel memory region holding user-accessible data */ #define KSIZE 1024 char kbuf[KSIZE]; /* Copy at most maxlen bytes from kernel region to user buffer */ int copy_from_kernel(void *user_dest, int maxlen) { /* Byte count len is minimum of buffer size and maxlen */ int len = KSIZE &amp;lt; maxlen ?</description>
    </item>
    
    <item>
      <title>深入理解 SQL 中的 Grouping Sets 语句</title>
      <link>https://yrunz.com/p/%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3-sql-%E4%B8%AD%E7%9A%84-grouping-sets-%E8%AF%AD%E5%8F%A5/</link>
      <pubDate>Sun, 03 Jul 2022 00:00:00 +0000</pubDate>
      
      <guid>https://yrunz.com/p/%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3-sql-%E4%B8%AD%E7%9A%84-grouping-sets-%E8%AF%AD%E5%8F%A5/</guid>
      <description>前言 SQL 中 Group By 语句大家都很熟悉，根据指定的规则对数据进行分组，常常和聚合函数一起使用。
比如，考虑有表 dealer，表中数据如下：
   id (Int) city (String) car_model (String) quantity (Int)     100 Fremont Honda Civic 10   100 Fremont Honda Accord 15   100 Fremont Honda CRV 7   200 Dublin Honda Civic 20   200 Dublin Honda Accord 10   200 Dublin Honda CRV 3   300 San Jose Honda Civic 5   300 San Jose Honda Accord 8    如果执行 SQL 语句 SELECT id, sum(quantity) FROM dealer GROUP BY id ORDER BY id，会得到如下结果：</description>
    </item>
    
    <item>
      <title>假如让你来设计SSL/TLS协议</title>
      <link>https://yrunz.com/p/%E5%81%87%E5%A6%82%E8%AE%A9%E4%BD%A0%E6%9D%A5%E8%AE%BE%E8%AE%A1ssl/tls%E5%8D%8F%E8%AE%AE/</link>
      <pubDate>Sat, 05 Mar 2022 00:00:00 +0000</pubDate>
      
      <guid>https://yrunz.com/p/%E5%81%87%E5%A6%82%E8%AE%A9%E4%BD%A0%E6%9D%A5%E8%AE%BE%E8%AE%A1ssl/tls%E5%8D%8F%E8%AE%AE/</guid>
      <description>前言 说起网络通信协议，相信大家对 TCP 和 HTTP 都很熟悉，它们可以说是当今互联网通信的基石。但是，在网络安全方面，它们却是有着很大安全风险：
 窃听风险。第三方攻击者可以随意窃听通信内容，比如获取支付账号密码。 冒充风险。第三方攻击者可以冒充他人身份与你通信，比如冒充银行网站以窃取银行账号密码。 篡改风险。第三方攻击者可以随意修改通信内容，比如在响应上加入钓鱼网址。  为此，SSL/TLS 协议应运而生。SSL/TLS 是建立在传输层之上、应用层之下的安全通信协议，它主要的设计意图就是消除上述几种安全风险，保证网络通信安全。我们熟知的 HTTPS 就是 HTTP + SSL/TLS 构建的，可以说 SSL/TLS 是当今互联网安全通信的基石。
那么，现在假如让你来设计 SSL/TLS 协议，你会怎么设计呢？
 本文将从设计者的视角介绍如何一步步设计出一个简易版的 SSL/TLS 的过程，在文章的最后，再简单介绍 TLS 1.2 版本的工作机制，以此帮助大家对 SSL/TLS 协议的基本原理有一个更深入的理解。
 基于对称加密算法的数据加密 窃听风险主要是因为通信双方在网络上明文传输数据，导致攻击者可以通过简单网络抓包就能获取到通信的内容。
要解决窃听风险，就最好的方法就是对数据进行加密。也即客户端在把数据发送出去之前，先对数据进行加密；服务端收到密文之后，再进行解密还原数据。这样就能避免在网络上传播明文，从而可以防止第三方攻击者的窃听。
提到加密算法，很多人首先会想到对称加密算法，它以简单和高效著称。对称加密指的是加密和解密都使用同一份密钥，常见的算法有 DES、AES 等。
现在，我们试着使用对称加密算法来实现安全通信：
使用对称密钥加密的前提是，通信双方都必须用同一份密钥来对数据进行加密。主要有线下和线上密钥交换两种方案可以达到该目的：
 线下密钥交换，也即通信双方线下约定好当面交换密钥（比如通过U盘作为媒介）。该方案可以保证密钥交换的安全性，但是很难推广使用。因为在绝大多数场景中，客户端和服务端都不可能碰面。 线上密钥交换，也即通过网络来传输密钥。但在网络明文传输密钥同样也会被攻击者拦截，这样的加密也没有意义了。  因此，单纯的对称加密并不能满足通信安全的要求，我们还要继续优化&amp;hellip;&amp;hellip;
基于非对称加密算法的数据加密 非对称加密算法指的是加密和解密使用不同的密钥，这两个不同的密钥组成一个密钥对，也即公钥和私钥。公钥是公开的密钥，所有人都能获取到；私钥则是保密的。当我们使用公钥对数据进行加密后，只有对应的私钥才能完成解密。常见的非对称加密算法有 RSA、ECC 等。
现在，我们试着使用非对称加密算法来实现安全通信：
通过非对称加密算法，我们既能实现对数据的加密，又能解决密钥交换的问题，从而消除了窃听风险。但是，非对称加密算法最大的缺点，就是加解密速度很慢，相比于对称加密算法要慢1000多倍。因此，非对称加密算法通常只适用于对少量数据的加密。
到目前为止，单纯地使用对称加密算法或非对称加密算法都无法满足要求，还需要继续优化&amp;hellip;&amp;hellip;
基于对称加密+非对称加密算法的数据加密 既然对称加密算法加解密速度快，但存在密钥交换的问题；而非对称加密算法可以解决密钥交换问题，但加解密速度慢。那么我们可以把两种算法结合起来，也即通过对称加密算法进行数据加密，在交换对称密钥时，使用非对称加密算法来加密对称密钥，确保密钥在网络传输过程中不会被攻击者窃听。
现在，我们试着使用对称加密+非对称加密算法来实现安全通信：
使用对称加密+非对称加密算法的方案，我们消除了窃听风险，也不会存在加解密性能问题，但是还是无法消除冒充风险。
考虑如下场景：
 攻击者把服务端的公钥拦截，并保存下来。 攻击者伪造成服务端，把自己的公钥发送给客户端。 攻击者拦截使用非法公钥加密后的对称密钥，解密后得到对称密钥明文，并保存下来 攻击者使用服务端公钥重新加密对称密钥，伪造成客户端发送给服务端。  这番操作后，攻击者就能在客户端和服务端都不知情的情况下，得到了对称密钥。在这种场景下，攻击者从被动攻击的窃听，转为主动攻击的冒充，让客户端和服务端都误以为一直在跟对方通信。
因此，我们需要找到一种方法，让客户端能够确保自己收到的公钥，一定是真实的服务端发送过来的，也即能够认证“服务端”的真实身份&amp;hellip;&amp;hellip;
基于CA证书的身份认证 数字证书概述 引用百度百科的定义：</description>
    </item>
    
    <item>
      <title>探索OS的内存原理</title>
      <link>https://yrunz.com/p/%E6%8E%A2%E7%B4%A2os%E7%9A%84%E5%86%85%E5%AD%98%E5%8E%9F%E7%90%86/</link>
      <pubDate>Sun, 09 Jan 2022 00:00:00 +0000</pubDate>
      
      <guid>https://yrunz.com/p/%E6%8E%A2%E7%B4%A2os%E7%9A%84%E5%86%85%E5%AD%98%E5%8E%9F%E7%90%86/</guid>
      <description>前言 内存作为计算机系统的组成部分，跟开发人员的日常开发活动有着密切的联系，我们平时遇到的Segment Fault、OutOfMemory、Memory Leak、GC等都与它有关。本文所说的内存，指的是计算机系统中的主存（Main Memory），它位于存储金字塔中CPU缓存和磁盘之间，是程序运行不可或缺的一部分。
在计算机系统中，主存通常都是由操作系统（OS）来管理，而内存管理的细则对开发者来说是无感的。对于一个普通的开发者，他只需懂得如何调用编程语言的接口来进行内存申请和释放，即可写出一个可用的应用程序。如果你使用的是带有垃圾回收机制的语言，如Java和Go，甚至都不用主动释放内存。但如果你想写出高效应用程序，熟悉OS的内存管理原理就变得很有必要了。
下面，我们将从最简单的内存管理原理说起，带大家一起窥探OS的内存管理机制，由此熟悉底层的内存管理机制，写出高效的应用程序。
独占式内存管理 早期的单任务系统中，同一时刻只能有一个应用程序独享所有的内存（除开OS占用的内存），因此，内存管理可以很简单，只需在内存上划分两个区域：
在多任务系统中，计算机系统已经可以做到多个任务并发运行。如果还是按照独占式的管理方式，那么每次任务切换时，都会涉及多次内存和磁盘之间的数据拷贝，效率极其低下：
最直观的解决方法就是让所有程序的数据都常驻在内存中（假设内存足够大），这样就能避免数据拷贝了：
但这样又会带来另一个问题，程序之间的内存地址空间是没有隔离的，也就是程序A可以修改程序B的内存数据。这样的一个重大的安全问题是用户所无法接受的，要解决该问题，就要借助虚拟化的力量了。
虚拟地址空间 为了实现程序间内存的隔离，OS对物理内存做了一层虚拟化。OS为每个程序都虚拟化出一段内存空间，这段虚拟内存空间会映射到一段物理内存上。但对程序自身而言，它只能看到自己的虚拟地址空间，也就有独占整个内存的错觉了。
上图中，虚拟内存空间分成了三个区域，其中Code区域存储的是程序代码的机器指令；Heap区域存储程序运行过程中动态申请的内存；Stack区域则是存储函数入参、局部变量、返回值等。Heap和Stack会在程序运行过程中不断增长，分别放置在虚拟内存空间的上方和下方，并往相反方向增长。
从虚拟地址空间到物理地址空间的映射，需要一个转换的过程，完成这个转换运算的部件就是MMU（memory management unit），也即内存管理单元，它位于CPU芯片之内。
要完成从虚拟地址到物理地址到转换，MMU需要base和bound两个寄存器。其中base寄存器用来存储程序在物理内存上的基地址，比如在图5中，程序A的基地址就是192KB；bound寄存器（有时候也叫limit寄存器）则保存虚拟地址空间的Size，主要用来避免越界访问，比如图5中程序A的size值为64K。那么，基于这种方式的地址转换公式是这样的：
 物理地址 = 虚拟地址 + 基地址
 以图5中程序A的地址转换为例，当程序A尝试访问超过其bound范围的地址时，物理地址会转换失败：
现在，我们再次仔细看下程序A的物理内存分布，如下图7所示，中间有很大的一段空闲内存是“已申请，未使用”的空闲状态。这也意味着即使这部分是空闲的，也无法再次分配给其他程序使用，这是一种巨大的空间浪费！为了解决这个内存利用率低下的问题，我们熟悉的段式内存管理出现了。
段式内存管理 在上一节中，我们发现如果以程序为单位去做内存管理，会存在内存利用率不足的问题。为了解决该问题，段式内存管理被引入。段（Segment）是逻辑上的概念，本质上是一块连续的、有一定大小限制的内存区域，前文中，我们一共提到过3个段：Code、Heap和Stack。
段式内存管理以段为单位进行管理，它允许OS将每个段灵活地放置在物理内存的空闲位置上，因此也避免了“已申请，未使用”的内存区域出现：
地址转换 从上图8可知，段式内存管理中程序的物理内存空间可能不再连续了，因此为了实现从虚拟地址到物理地址到转换，MMU需要为每个段都提供一对base-bound寄存器，比如：
给一个虚拟地址，MMU是如何知道该地址属于哪个段，从而根据对应的base-bound寄存器转换为对应的物理地址呢？
假设虚拟地址有16位，因为只有3个段，因此，我们可以使用虚拟地址的高2位来作为段标识，比如00表示Code段，01表示Heap段，11表示Stack段。这样MMU就能根据虚拟地址来找到对应段的base-bound寄存器了：
但这样还不是能够顺利的将虚拟地址转换为物理地址，我们忽略了重要的一点：Heap段和Stack段的增长方向是相反的，这也意味着两者的地址转换方式是不一样的。因此，我们还必须在虚拟地址中多使用一位来标识段的增长方向，比如0表示向上（低地址方向）增长，1表示向下（高地址方向）增长：
下面，看一组段式内存管理地址转换的例子：
那么，总结段式内存管理的地址转换算法如下：
// 获取当前虚拟地址属于哪个段 Segment = (VirtualAddress &amp;amp; SEG_MASK) &amp;gt;&amp;gt; SEG_SHIFT // 得到段内偏移量 Offset = VirtualAddress &amp;amp; OFFSET_MASK // 获得内存增长的方向 GrowsDirection = VirtualAddress &amp;amp; GROWS_DIRECTION_MASK // 有效性校验 if (Offset &amp;gt;= Bounds[Segment]) RaiseException(PROTECTION_FAULT) else if (GrowsDirection == 0) { PhysAddr = Base[Segment] + Offset } else { PhysAddr = Base[Segment] - Offset } 内存共享和保护 段式内存管理还可以很方便地支持内存共享，从而达到节省内存的目的。比如，如果存在多个程序都是同一个可执行文件运行起来的，那么这些程序是可以共享Code段的。为了实现这个功能，我们可以在虚拟地址上设置保护位，当保护位为只读时，表示该段可以共享。另外，如果程序修改了只读的段，则转换地址失败，因此也可以达到内存保护的目的。</description>
    </item>
    
    <item>
      <title>探索CPU的调度原理</title>
      <link>https://yrunz.com/p/%E6%8E%A2%E7%B4%A2cpu%E7%9A%84%E8%B0%83%E5%BA%A6%E5%8E%9F%E7%90%86/</link>
      <pubDate>Sun, 25 Jul 2021 00:00:00 +0000</pubDate>
      
      <guid>https://yrunz.com/p/%E6%8E%A2%E7%B4%A2cpu%E7%9A%84%E8%B0%83%E5%BA%A6%E5%8E%9F%E7%90%86/</guid>
      <description>前言 软件工程师们总习惯把OS（Operating System，操作系统）当成是一个非常值得信赖的管家，我们只管把程序托管到OS上运行，却很少深入了解操作系统的运行原理。确实，OS作为一个通用的软件系统，在大多数的场景下都表现得足够的优秀。但仍会有一些特殊的场景，需要我们对OS进行各项调优，才能让业务系统更高效地完成任务。这就要求我们必须深入了解OS的原理，不仅仅只会使唤这个管家，还能懂得如何让管家做得更好。
OS是一个非常庞大的软件系统，本文主要探索其中的冰山一角：CPU的调度原理。
说起CPU的调度原理，很多人的第一反应是基于时间片的调度，也即每个进程都有占用CPU运行的时间片，时间片用完之后，就让出CPU给其他进程。至于OS是如何判断一个时间片是否用完的、如何切换到另一个进程等等更深层的原理，了解的人似乎并不多。
其实，基于时间片的调度只是众多CPU的调度算法的一类，本文将会从最基础的调度算法说起，逐个分析各种主流调度算法的原理，带大家一起探索CPU调度的奥秘。
CPU的上下文切换 在探索CPU调度原理之前，我们先了解一下CPU的上下文切换，它是CPU调度的基础。
如今的OS几乎都支持&amp;quot;同时&amp;quot;运行远大于CPU数量的任务，OS会将CPU轮流分配给它们使用。这就要求OS必须知道从哪里加载任务，以及加载后从哪里开始运行，而这些信息都保存在CPU的寄存器中，其中即将执行的下一条指令的地址被保存在程序计数器（PC）这一特殊寄存器上。我们将寄存器的这些信息称为CPU的上下文，也叫硬件上下文。
OS在切换运行任务时，将上一任务的上下文保存下来，并将即将运行的任务的上下文加载到CPU寄存器上的这一动作，被称为CPU上下文切换。
 CPU上下文属于进程上下文的一部分，我们常说的进程上下文由如下两部分组成：
 用户级上下文：包含进程的运行时堆栈、数据块、代码块等信息。 系统级上下文：包含进程标识信息、进程现场信息（CPU上下文）、进程控制信息等信息。   这涉及到两个问题：（1）上一任务的CPU上下文如何保存下来？（2）什么时候执行上下文切换？
问题1: 上一任务的CPU上下文如何保存下来？
CPU上下文会被保存在进程的内核空间（kernel space）上。OS在给每个进程分配虚拟内存空间时，会分配一个内核空间，这部分内存只能由内核代码访问。OS在切换CPU上下文前，会先将当前CPU的通用寄存器、PC等进程现场信息保存在进程的内核空间上，待下次切换时，再取出重新装载到CPU上，以恢复任务的运行。
问题2: 什么时候执行上下文切换？
OS要想进行任务上下文切换，必须占用CPU来执行切换逻辑。然而，用户程序运行的过程中，CPU已经被用户程序所占用，也即OS在此刻并未处于运行状态，自然也无法执行上下文切换。针对该问题，有两种解决策略，协作式策略与抢占式策略。
协作式策略依赖用户程序主动让出CPU，比如执行系统调用（System Call）或者出现除零等异常。但该策略并不靠谱，如果用户程序没有主动让出CPU，甚至是恶意死循环，那么该程序将会一直占用CPU，唯一的恢复手段就是重启系统了。
抢占式策略则依赖硬件的定时中断机制（Timer Interrupt），OS会在初始化时向硬件注册中断处理回调（Interrupt Handler）。当硬件产生中断时，硬件会将CPU的处理权交给来OS，OS就可以在中断回调上实现CPU上下文的切换。
调度的衡量指标 对于一种CPU调度算法的好坏，一般都通过如下两个指标来进行衡量：
 周转时间（turnaround time），指从任务到达至任务完成之间的时间，即$T_{turnaround}=T_{completiong}-T_{arrival}$ 响应时间（response time），指从任务到达至任务首次被调度的时间，即$T_{response}=T_{firstrun}-T_{arrival}$  两个指标从某种程度上是对立的，要求高的平均周转时间，必然会降低平均响应时间。具体追求哪种指标与任务类型有关，比如程序编译类的任务，要求周转时间要小，尽可能快的完成编译；用户交互类的任务，则要求响应时间要小，避免影响用户体验。
工作负载假设 OS上的工作负载（也即各类任务运行的状况）总是千变万化的，为了更好的理解各类CPU调度算法原理，我们先对工作负载进行来如下几种假设：
 假设1：所有任务都运行时长都相同。 假设2：所有任务的开始时间都是相同的 假设3：一旦任务开始，就会一直运行，直至任务完成。 假设4：所有任务只使用CPU资源（比如不产生I/O操作）。 假设5：预先知道所有任务的运行时长。  准备工作已经做好，下面我们开始进入CPU调度算法的奇妙世界。
FIFO：先进先出 FIFO（First In First Out，先进先出）调度算法以原理简单，容易实现著称，它先调度首先到达的任务直至结束，然后再调度下一个任务，以此类推。如果有多个任务同时到达，则随机选一个。
在我们假设的工作负载状况下，FIFO效率良好。比如有A、B、C三个任务满足上述所有负载假设，每个任务运行时长为10s，在t=0时刻到达，那么任务调度情况是这样的：
根据FIFO的调度原理，A、B、C分别在10、20、30时刻完成任务，平均周转时间为20s（ $\frac {10+20+30}{3}$），效果很好。
然而现实总是残酷的，如果假设1被打破，比如A的运行时间变成100s，B和C的还是10s，那么调度情况是这样的：
根据FIFO的调度原理，由于A的运行时间过长，B和C长时间得不到调度，导致平均周转时间恶化为110（ $\frac {100+110+120}{3}$）。
因此，FIFO调度策略在任务运行时间差异较大的场景下，容易出现任务饿死的问题！
针对这个问题，如果运行时间较短的B和C先被调度，问题就可以解决了，这正是SJF调度算法的思想。
SJF：最短任务优先 SJF（Shortest Job First，最短任务优先）从相同到达时间的多个任务中选取运行时长最短的一个任务进行调度，接着再调度第二短的任务，以此类推。
针对上一节的工作负载，使用SJF进行调度的情况如下，周转时间变成了50s（ $\frac {10+20+120}{3}$），相比FIFO的110s，有了2倍多的提升。</description>
    </item>
    
  </channel>
</rss>
