<?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/tags/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/</link>
    <description>Recent content in 操作系统 on 元闰子的邀请</description>
    <generator>Hugo -- gohugo.io</generator>
    <language>en-us</language>
    <lastBuildDate>Sat, 05 Mar 2022 00:00:00 +0000</lastBuildDate><atom:link href="https://yrunz.com/tags/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/index.xml" rel="self" type="application/rss+xml" />
    <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>
