├── .eleventyignore ├── src ├── site │ ├── notes │ │ ├── 408 │ │ │ ├── 绪论进程.md │ │ │ ├── demo1111.md │ │ │ ├── 利用指令对齐特性实现地址压缩😲.md │ │ │ ├── Cache缺失竟对CPU寄存器暗送秋波😡.md │ │ │ ├── CDMA:在嘈杂的环境中我只想安静听你说话🥰.md │ │ │ ├── 不同层次之间交换数据的单位😡.md │ │ │ ├── 共享栈:根据想插入位置的不同导致的判满条件不同😈.md │ │ │ ├── 交换机在看到数据帧的MAC地址后就快速发射🥵.md │ │ │ ├── 用户级线程为什么阻塞一个就全部阻塞了.md │ │ │ ├── 主存和外存的数据交换单位.md │ │ │ ├── 通道技术.md │ │ │ ├── 指令集里的“断舍离”大师——Load Store.md │ │ │ ├── 页式管理与段式管理内存.md │ │ │ ├── 区分受限广播和直通广播.md │ │ │ ├── 资源分配图.md │ │ │ ├── 段页式管理下的地址映射表组织🥰.md │ │ │ ├── 成组链接法你怎么和空闲盘号栈有一腿😰.md │ │ │ ├── 住址决定命运:为什么你的变量要被内存访问两次才肯出门?😭.md │ │ │ ├── 内存索引节点和磁盘索引节点你俩到底是什么关系.md │ │ │ ├── 分页管理与分段管理下的动态链接.md │ │ │ ├── 死锁的四个必要条件.md │ │ │ ├── 一条指令的诞生和解剖从如何设计到CPU解码🤔.md │ │ │ ├── IO数据进入内存前,竟要在CPU的寄存器里先住一晚🥵.md │ │ │ ├── 通道技术和DMA的区别解释.md │ │ │ ├── read()操作的工作流程.md │ │ │ ├── close()与write()操作的思考🤔.md │ │ │ ├── 快递姬802.3、802.11和802.1Q三姐妹的underwear到底有啥不一样?🤔.md │ │ │ ├── 互斥信号量与同步信号量.md │ │ │ ├── IP地址块分配的地址不重叠和路由聚合的不引入多余地址.md │ │ │ ├── CSMACA:是自由吟唱的混沌魔法,还是遵循古老契约的序列仪式?🤔.md │ │ │ ├── ICMP交警何时对IP数据报发出罚单呢🤔.md │ │ │ ├── 在隐含寻址中第二操作数是怎么在ACC当中金屋藏娇呢🤔.md │ │ │ ├── 多线程模型.md │ │ │ ├── 虚拟地址的生命周期🥰.md │ │ │ ├── 进程映像的概念.md │ │ │ ├── 中断信号的优先级,多重中断的处理.md │ │ │ ├── 浮点数运算的全过程😡.md │ │ │ ├── STDM的异步你真的知道是什么的异步吗🤔.md │ │ │ ├── 设备驱动程序的生命周期😮‍💨.md │ │ │ ├── 磁盘的低级格式化和高级格式化都进行了什么工作.md │ │ │ ├── OSPF&BGP:OpenFlow我们会死吗😭.md │ │ │ ├── IO层次结构以及每层的作用.md │ │ │ ├── DRAM都会怎么洗澡🥵.md │ │ │ ├── 只会蒙头算数的弱智CPU是怎么识别出变量类型的呢🤔.md │ │ │ ├── 浮点数的规格化.md │ │ │ ├── 在cache小姐协助下的CPU访存全流程🤔.md │ │ │ ├── 中断处理和子程序调用对寄存器的保存情况.md │ │ │ ├── 结构体变量和基础类型变量的对齐准则.md │ │ │ ├── IP数据报的两张表:路由表&ARP表.md │ │ │ ├── 条件转移指令对应的转移条件分析🤔.md │ │ │ ├── 临界资源与共享资源.md │ │ │ ├── IO设备根据数据的存取和传输进行的分类.md │ │ │ ├── 文件的逻辑结构以及索引文件.md │ │ │ ├── 目录检索算法,一个文件检索的生命流程🤔.md │ │ │ ├── ROM酱的贞操锁:CPU的写指令是如何被无情拒绝的🛡️.md │ │ │ ├── signal操作与V操作.md │ │ │ ├── 细分两种变址寻址🤔.md │ │ │ ├── 加法器家族的内卷史:从傻等前任的串行废物到预知未来的并行天才🤔.md │ │ │ ├── 操作系统当中的引导程序.md │ │ │ └── DRAM结构组成以及一个数据流经她全身的例子🥵.md │ │ ├── notes.json │ │ └── notes.11tydata.js │ ├── graph.njk │ ├── _includes │ │ ├── components │ │ │ ├── lucideIcons.njk │ │ │ ├── searchButton.njk │ │ │ ├── navbar.njk │ │ │ ├── filetreeNavbar.njk │ │ │ ├── timestamps.njk │ │ │ ├── notegrowthhistory.njk │ │ │ ├── searchContainer.njk │ │ │ ├── references.njk │ │ │ ├── calloutScript.njk │ │ │ ├── pageheader.njk │ │ │ └── filetree.njk │ │ └── layouts │ │ │ ├── index.njk │ │ │ └── note.njk │ ├── img │ │ ├── outgoing.svg │ │ ├── default-note-icon.svg │ │ ├── tree-2.svg │ │ ├── tree-1.svg │ │ └── tree-3.svg │ ├── styles │ │ └── custom-style.scss │ ├── _data │ │ ├── eleventyComputed.js │ │ ├── dynamics.js │ │ └── meta.js │ ├── sitemap.njk │ ├── search-index.njk │ ├── 404.njk │ ├── feed.njk │ ├── get-theme.js │ └── favicon.svg └── helpers │ ├── userUtils.js │ ├── constants.js │ ├── userSetup.js │ ├── utils.js │ ├── linkUtils.js │ └── filetreeUtils.js ├── .gitignore ├── netlify.toml ├── vercel.json ├── .github └── dependabot.yml ├── .env ├── package.json ├── README.md └── plugin-info.json /.eleventyignore: -------------------------------------------------------------------------------- 1 | netlify/functions 2 | -------------------------------------------------------------------------------- /src/site/notes/notes.json: -------------------------------------------------------------------------------- 1 | { 2 | "tags": "note", 3 | "templateEngineOverride": "md" 4 | } 5 | -------------------------------------------------------------------------------- /src/site/notes/408/绪论进程.md: -------------------------------------------------------------------------------- 1 | --- 2 | {"dg-publish":true,"dg-permalink":"/408/绪论进程","permalink":"/408/绪论进程/"} 3 | --- 4 | 5 | -------------------------------------------------------------------------------- /src/site/graph.njk: -------------------------------------------------------------------------------- 1 | --- 2 | permalink: /graph.json 3 | eleventyExcludeFromCollections: true 4 | --- 5 | {{ graph | jsonify | safe }} -------------------------------------------------------------------------------- /src/site/notes/408/demo1111.md: -------------------------------------------------------------------------------- 1 | --- 2 | {"dg-publish":true,"dg-permalink":"/408/demo","permalink":"/408/demo/"} 3 | --- 4 | 5 | 11111 -------------------------------------------------------------------------------- /src/helpers/userUtils.js: -------------------------------------------------------------------------------- 1 | // Put your computations here. 2 | 3 | function userComputed(data) { 4 | return {}; 5 | } 6 | 7 | exports.userComputed = userComputed; 8 | -------------------------------------------------------------------------------- /src/site/notes/408/利用指令对齐特性实现地址压缩😲.md: -------------------------------------------------------------------------------- 1 | --- 2 | {"dg-publish":true,"dg-permalink":"/408/利用指令对齐特性实现地址压缩😲","permalink":"/408/利用指令对齐特性实现地址压缩😲/"} 3 | --- 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/site/_includes/components/lucideIcons.njk: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | netlify/functions/search/data.json 4 | netlify/functions/search/index.json 5 | src/site/styles/theme.*.css 6 | src/site/styles/_theme.*.css 7 | # Local Netlify folder 8 | .netlify 9 | .idea/ 10 | .vercel 11 | .cache 12 | _site/ 13 | **/.DS_Store 14 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | publish = "dist" 3 | command = "npm install && npm run build" 4 | 5 | [[redirects]] 6 | from = "/api/*" 7 | to = "/.netlify/functions/:splat" 8 | status = 200 9 | 10 | [[redirects]] 11 | from = "/*" 12 | to = "/404" 13 | status = 404 14 | 15 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "outputDirectory": "dist", 3 | "installCommand": "npm install", 4 | "buildCommand": "npm run build", 5 | "devCommand": "npm run start", 6 | "routes": [ 7 | { "handle": "filesystem" }, 8 | { "src": "/(.*)", "status": 404, "dest": "/404" } 9 | ] 10 | } -------------------------------------------------------------------------------- /src/helpers/constants.js: -------------------------------------------------------------------------------- 1 | exports.ALL_NOTE_SETTINGS= [ 2 | "dgHomeLink", 3 | "dgPassFrontmatter", 4 | "dgShowBacklinks", 5 | "dgShowLocalGraph", 6 | "dgShowInlineTitle", 7 | "dgShowFileTree", 8 | "dgEnableSearch", 9 | "dgShowToc", 10 | "dgLinkPreview", 11 | "dgShowTags" 12 | ]; -------------------------------------------------------------------------------- /src/site/img/outgoing.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/site/styles/custom-style.scss: -------------------------------------------------------------------------------- 1 | body { 2 | /*** 3 | ADD YOUR CUSTOM STYLING HERE. (INSIDE THE body {...} section.) 4 | IT WILL TAKE PRECEDENCE OVER THE STYLING IN THE STYLE.CSS FILE. 5 | ***/ 6 | // background-color: white; 7 | // .content { 8 | // font-size: 14px; 9 | // } 10 | // h1 { 11 | // color: black; 12 | // } 13 | } 14 | -------------------------------------------------------------------------------- /src/site/_data/eleventyComputed.js: -------------------------------------------------------------------------------- 1 | const { getGraph } = require("../../helpers/linkUtils"); 2 | const { getFileTree } = require("../../helpers/filetreeUtils"); 3 | const { userComputed } = require("../../helpers/userUtils"); 4 | 5 | module.exports = { 6 | graph: (data) => getGraph(data), 7 | filetree: (data) => getFileTree(data), 8 | userComputed: (data) => userComputed(data) 9 | }; 10 | -------------------------------------------------------------------------------- /src/site/sitemap.njk: -------------------------------------------------------------------------------- 1 | --- 2 | permalink: /sitemap.xml 3 | eleventyExcludeFromCollections: true 4 | --- 5 | 6 | 7 | {% for page in collections.all %} 8 | 9 | {{ meta.siteBaseUrl }}{{ page.url | url }} 10 | {{ page.date | dateToZulu }} 11 | 12 | {% endfor %} 13 | -------------------------------------------------------------------------------- /src/site/_includes/components/searchButton.njk: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 6 | Search 7 | 8 | CTRL + K 9 | 10 | 11 |
-------------------------------------------------------------------------------- /src/site/img/default-note-icon.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/helpers/userSetup.js: -------------------------------------------------------------------------------- 1 | function userMarkdownSetup(md) { 2 | // The md parameter stands for the markdown-it instance used throughout the site generator. 3 | // Feel free to add any plugin you want here instead of /.eleventy.js 4 | } 5 | function userEleventySetup(eleventyConfig) { 6 | // The eleventyConfig parameter stands for the the config instantiated in /.eleventy.js. 7 | // Feel free to add any plugin you want here instead of /.eleventy.js 8 | } 9 | exports.userMarkdownSetup = userMarkdownSetup; 10 | exports.userEleventySetup = userEleventySetup; 11 | -------------------------------------------------------------------------------- /src/site/_includes/components/navbar.njk: -------------------------------------------------------------------------------- 1 | {%if settings.dgHomeLink === true%} 2 | 12 | {%else%} 13 |
14 | {% if settings.dgEnableSearch === true%} 15 | {% include "components/searchButton.njk" %} 16 | {%endif%} 17 |
18 | {%endif%} -------------------------------------------------------------------------------- /src/site/search-index.njk: -------------------------------------------------------------------------------- 1 | --- 2 | permalink: /searchIndex.json 3 | eleventyExcludeFromCollections: true 4 | --- 5 | [{% for post in collections.note %} 6 | { 7 | "title": {% if post.data.title %}{{post.data.title | jsonify | safe }}{% else %}{{post.fileSlug | jsonify | safe }}{% endif %}, 8 | "date":"{{ post.date }}", 9 | "url":"{{ post.url }}", 10 | "content": {{ post.templateContent | striptags(true) | link | jsonify | safe }}, 11 | "tags": [{{post.templateContent | link | searchableTags | safe }} {% if post.data.tags %}{% for tag in post.data.tags %}"{{tag|validJson}}"{% if not loop.last %},{% endif %}{% endfor %}{% endif %}] 12 | }{% if not loop.last %},{% endif %} 13 | {% endfor %}] -------------------------------------------------------------------------------- /src/site/img/tree-2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/site/img/tree-1.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "npm" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | ignore: 13 | - dependency-name: "@sindresorhus/slugify" 14 | # For slugify, ignore all updates. 15 | - dependency-name: "@11ty/eleventy-plugin-rss" 16 | versions: [ "^1.2.0" ] 17 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | SITE_NAME_HEADER=Rāna(Bass Ver.) 2 | SITE_MAIN_LANGUAGE=en 3 | SITE_BASE_URL= 4 | SHOW_CREATED_TIMESTAMP=false 5 | TIMESTAMP_FORMAT=MMM dd, yyyy h:mm a 6 | SHOW_UPDATED_TIMESTAMP=false 7 | NOTE_ICON_DEFAULT= 8 | NOTE_ICON_TITLE=false 9 | NOTE_ICON_FILETREE=false 10 | NOTE_ICON_INTERNAL_LINKS=false 11 | NOTE_ICON_BACK_LINKS=false 12 | STYLE_SETTINGS_CSS= 13 | STYLE_SETTINGS_BODY_CLASSES= 14 | USE_FULL_RESOLUTION_IMAGES=true 15 | THEME=https://raw.githubusercontent.com/colineckert/obsidian-things/HEAD/theme.css 16 | BASE_THEME=light 17 | dgHomeLink=false 18 | dgPassFrontmatter=false 19 | dgShowBacklinks=false 20 | dgShowLocalGraph=false 21 | dgShowInlineTitle=false 22 | dgShowFileTree=false 23 | dgEnableSearch=true 24 | dgShowToc=false 25 | dgLinkPreview=false 26 | dgShowTags=false -------------------------------------------------------------------------------- /src/site/_includes/components/filetreeNavbar.njk: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/site/_includes/components/timestamps.njk: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/site/_includes/components/notegrowthhistory.njk: -------------------------------------------------------------------------------- 1 | {%- if collections.versionednote.length > 0 -%} 2 |
3 | 4 |

Notegrowth

5 | 15 |
16 | {%-endif-%} -------------------------------------------------------------------------------- /src/site/_includes/components/searchContainer.njk: -------------------------------------------------------------------------------- 1 |
2 | 22 |
23 | {%include "components/searchScript.njk"%} -------------------------------------------------------------------------------- /src/site/img/tree-3.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/site/notes/408/Cache缺失竟对CPU寄存器暗送秋波😡.md: -------------------------------------------------------------------------------- 1 | --- 2 | {"dg-publish":true,"dg-permalink":"/408/Cache缺失竟对CPU寄存器暗送秋波😡","permalink":"/408/Cache缺失竟对CPU寄存器暗送秋波😡/"} 3 | --- 4 | 5 | 6 | ### 一、 流程图的直接证据:不存在“二次读取” 7 | 8 | ![56ebde223774065e8a3a657b3e381e15.png](/img/user/%E9%99%84%E4%BB%B6/56ebde223774065e8a3a657b3e381e15.png) 9 | 当**“从主存取出AD所在块”**这个耗时最长的操作完成之后,流程图的箭头**分叉了**! 10 | 11 | 它分成了两条并行的路径: 12 | 13 | - **路径 A(通往CPU)**:**`将 AD 单元内容送 CPU`** 14 | 15 | - **路径 B(通往Cache)**:**`在 cache 中找到一个对应的空闲行`** → **`将主存块复制到 cache 空闲行中`** 16 | 17 | 18 | 这两条路径最终都指向**“结束”**。 19 | 20 | **这张图用最直观的方式告诉我们:** 21 | 22 | > 在处理Cache缺失时,从主存取回的数据块被**同时**送往了两个目的地。其中,CPU当前急需的那个特定单元(或字)的内容,被直接“抄近道”送给了CPU;而整个数据块,则被写入到Cache中对应的空行里。 23 | 24 | - “Cache处理时间”包括了什么? 25 | 26 | 它包括了虚线框内的所有操作。主要是“从主存取块”的延迟,以及后续并行处理的耗时。它是一个从“判断出未命中”到“CPU拿到数据且Cache完成或开始填充”的总时间。 27 | 28 | - 需要再重新读一遍Cache吗? 29 | 30 | 绝对不需要。 流程图中没有任何一个箭头,是从“将主存块复制到cache”这一步再指回到“从cache中取信息送CPU”的。两条路径是并行的,CPU拿到数据后,整个访存操作就对CPU而言结束了,它可以继续执行下一条指令了。让CPU在Cache填充完毕后再来读一次,是一种极其低效的设计,现代处理器不会这么做。 31 | 32 | -------------------------------------------------------------------------------- /src/site/notes/notes.11tydata.js: -------------------------------------------------------------------------------- 1 | require("dotenv").config(); 2 | const settings = require("../../helpers/constants"); 3 | 4 | const allSettings = settings.ALL_NOTE_SETTINGS; 5 | 6 | module.exports = { 7 | eleventyComputed: { 8 | layout: (data) => { 9 | if (data.tags.indexOf("gardenEntry") != -1) { 10 | return "layouts/index.njk"; 11 | } 12 | return "layouts/note.njk"; 13 | }, 14 | permalink: (data) => { 15 | if (data.tags.indexOf("gardenEntry") != -1) { 16 | return "/"; 17 | } 18 | return data.permalink || undefined; 19 | }, 20 | settings: (data) => { 21 | const noteSettings = {}; 22 | allSettings.forEach((setting) => { 23 | let noteSetting = data[setting]; 24 | let globalSetting = process.env[setting]; 25 | 26 | let settingValue = 27 | noteSetting || (globalSetting === "true" && noteSetting !== false); 28 | noteSettings[setting] = settingValue; 29 | }); 30 | return noteSettings; 31 | }, 32 | }, 33 | }; 34 | -------------------------------------------------------------------------------- /src/site/_includes/components/references.njk: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/site/404.njk: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Nothing here 6 | 7 | {%-if meta.themeStyle%} 8 | 9 | 10 | {% else %} 11 | 12 | {%endif%} 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | {%-if not meta.themeStyle%} 21 |
😎
22 | {%endif%} 23 |

There is nothing here

24 |

If you got here from a link, this note is probably not made public

25 | Go back home 26 |
27 | 28 | 29 | -------------------------------------------------------------------------------- /src/site/notes/408/CDMA:在嘈杂的环境中我只想安静听你说话🥰.md: -------------------------------------------------------------------------------- 1 | --- 2 | {"dg-publish":true,"dg-permalink":"/408/CDMA:在嘈杂的环境中我只想安静听你说话🥰","permalink":"/408/CDMA:在嘈杂的环境中我只想安静听你说话🥰/"} 3 | --- 4 | 5 | 6 | **CDMA(码分多路复用)是一种允许所有用户在同一时间、使用相同频率进行通信的介质访问控制技术,它通过为每个用户分配一个唯一的、相互正交的“密码”(码片序列),来实现对不同用户信号的区分。** 7 | ### 一、核心原理 8 | 9 | 两个核心概念:**码片序列(Chip Sequence)** 和 **正交性(Orthogonality)**。 10 | 11 | #### 1. 码片序列(The "Secret Language") 12 | 13 | 系统会为每一个要通信的用户分配一个独一无二的码片序列。这个序列由 `m` 个码片(chip)组成,每个码片的值是 `+1` 或 `-1`。 14 | 15 | 例如,一个 `m=8` 的码片序列S可以表示为:`S = (-1, +1, -1, -1, +1, +1, -1, +1)` 16 | 17 | **规定**: 18 | 19 | - 发送比特 **1** 时,就发送自己的码片序列 **S**。 20 | 21 | - 发送比特 **0** 时,就发送该码片序列的**反码 -S**(所有+1变-1,-1变+1)。 22 | 23 | - `-S = (+1, -1, +1, +1, -1, -1, +1, -1)` 24 | 25 | 26 | #### 2. 正交性(The "Magic Headset") 27 | 28 | 分配给不同用户的码片序列(比如S和T)必须是**相互正交**的。在数学上,“正交”意味着两个向量的**规格化内积(Inner Product)**为0。 29 | 30 | - 规格化内积计算:将两个序列对应位置的码片相乘,然后把所有乘积加起来,最后除以序列长度 m。 31 | - 自己和自己做内积:一个序列和它自己做内积,结果是1。 32 | - 自己和反码做内积:结果是-1。 33 | **这个性质是CDMA能够分离信号的根本!** 它保证了用一个用户的“钥匙”(码片序列)去解别的用户信号时,得到的结果是0,从而过滤掉干扰。 34 | -------------------------------------------------------------------------------- /src/site/notes/408/不同层次之间交换数据的单位😡.md: -------------------------------------------------------------------------------- 1 | --- 2 | {"dg-publish":true,"dg-permalink":"/408/不同层次之间交换数据的单位","permalink":"/408/不同层次之间交换数据的单位/"} 3 | --- 4 | 5 | 6 | ### 一句话核心答案 7 | 8 | CPU与Cache之间数据交换的基本单位是**块(Block)**;而虚拟存储器与主存之间的数据交换单位则由管理方式决定:在**页式**和**段页式**管理下,交换单位是定长的**页(Page)**;在**段式**管理下,交换单位是变长的**段(Segment)**。 9 | 10 | --- 11 | 12 | ### 二、 存储层次中的“数据快递包裹”详解 13 | 14 | | 交换层次 (Hierarchy Level) | 交换单位 (Exchange Unit) | 单位大小 (Unit Size) | 核心特点 (Key Characteristics) | 15 | | ---------------------- | ----------------------------- | ----------------- | -------------------------------- | 16 | | **CPU ↔ Cache** | **块 (Block) / Cache行 (Line)** | **定长** (Fixed) | 硬件控制,对程序员透明,利用空间局部性 | 17 | | **Cache ↔ 主存** | **块 (Block)** | **定长** (Fixed) | 与CPU-Cache单位一致,发生Cache Miss时进行交换 | 18 | | **主存 ↔ 辅存(虚拟存储)** | | | **由操作系统和硬件(MMU)协同控制** | 19 | | ↳ **页式管理** | **页 (Page) / 页面** | **定长** (Fixed) | 物理传输的基本单位,大小与页框相等 | 20 | | ↳ **段式管理** | **段 (Segment)** | **变长** (Variable) | 按程序的逻辑结构划分,是逻辑上的单位 | 21 | | ↳ **段页式管理** | **页 (Page) / 页面** | **定长** (Fixed) | 逻辑上分段,物理上传输和管理的基本单位是页 | 22 | -------------------------------------------------------------------------------- /src/site/feed.njk: -------------------------------------------------------------------------------- 1 | ---json 2 | { 3 | "permalink": "/feed.xml", 4 | "eleventyExcludeFromCollections": true 5 | } 6 | --- 7 | {%if meta.siteBaseUrl %} 8 | 9 | {{ meta.siteName}} 10 | 11 | 12 | {{ collections.notes | getNewestCollectionItemDate | dateToRfc3339 }} 13 | {{ meta.siteBaseUrl }} 14 | {%- for note in collections.note | reverse %} 15 | 16 | 17 | {% if note.title %}{{ note.title }} 18 | {% else %}{{ note.fileSlug }} 19 | {% endif %} 20 | 21 | {%if note.data.updated %}{{ note.data.updated | dateToZulu }}{%else%}{{ note.date | dateToRfc3339 }}{%endif%} 22 | {{ meta.siteBaseUrl }}{{note.url | url }} 23 | 24 | {{ note.templateContent | hideDataview | taggify | link | htmlToAbsoluteUrls(meta.siteBaseUrl) }} 25 | 26 | 27 | 28 | {%- endfor %} 29 | 30 | {% endif %} -------------------------------------------------------------------------------- /src/site/notes/408/共享栈:根据想插入位置的不同导致的判满条件不同😈.md: -------------------------------------------------------------------------------- 1 | --- 2 | {"dg-publish":true,"dg-permalink":"/408/共享栈:根据想插入位置的不同导致的判满条件不同😈","permalink":"/408/共享栈:根据想插入位置的不同导致的判满条件不同😈/"} 3 | --- 4 | 5 | 6 | 7 | | 特性 | 设计思想一 (最常考) | 设计思想二 | 8 | | --------------------- | ------------------ | ------------------- | 9 | | **指针含义** | 指向**栈顶元素** | 指向**下一个空闲位置** | 10 | | **栈1初始化 `top1`** | `-1` | `0` | 11 | | **栈2初始化 `top2`** | `MAXSIZE` | `MAXSIZE-1` | 12 | | **栈1入栈 `Push(S, x)`** | `S[++top1] = x;` | `S[top1++] = x;` | 13 | | **栈2入栈 `Push(S, x)`** | `S[--top2] = x;` | `S[top2--] = x;` | 14 | | **栈1判空** | `top1 == -1` | `top1 == 0` | 15 | | **栈2判空** | `top2 == MAXSIZE` | `top2 == MAXSIZE-1` | 16 | | **栈满条件** | `top1 + 1 == top2` | `top1 > top2` | 17 | 18 | --- 19 | 20 | 1. **核心记忆**: 牢牢记住**思想一**的所有细节,包括初始化、入栈/出栈操作和 `top1 + 1 == top2` 的判满条件。这是99%的考题所基于的模型。 21 | 22 | 2. **审题是关键**: 考试时,不要上来就写 `top1 + 1 == top2`。一定要先看清题目给出的**数据结构定义**和**初始化条件**。如果题目给出了一个非主流的初始化(如思想二),那就必须按照该设定来分析。 23 | 24 | 3. **现场推导能力**: 防止被“变种题”迷惑的最佳方法,不是去背诵所有可能的情况,而是理解判满的本质——**空间耗尽**。在草稿纸上画一个数组,模拟一两次入栈操作,观察指针的变化,就能轻松推导出正确的判满条件。这比死记硬背更可靠。 25 | 26 | 4. **易错点**: 27 | 28 | - 混淆 `top1 + 1 == top2` 和 `top1 == top2 - 1`。记住是栈1的下一个位置(`top1+1`)撞上了栈2的当前位置(`top2`)。 29 | 30 | - 数组大小为 `MAXSIZE`,下标却是 `0` 到 `MAXSIZE-1`。在计算 `top2` 初始值时要小心。 31 | -------------------------------------------------------------------------------- /src/site/notes/408/交换机在看到数据帧的MAC地址后就快速发射🥵.md: -------------------------------------------------------------------------------- 1 | --- 2 | {"dg-publish":true,"dg-permalink":"/408/直通交换","permalink":"/408/直通交换/"} 3 | --- 4 | 5 | 6 | #### 一、核心机制:直通交换(Cut-through Switching) 7 | 8 | 直通交换是以太网交换机采用的一种高速转发技术。其核心特征在于,交换机在接收数据帧时,无需等待整个帧完全进入其内部缓存,而是在读取到能够做出转发决策的关键信息后,便立刻开始将该帧向目标端口转发。 9 | 10 | 这个关键决策信息,在纯粹的直通交换模式中,就是数据帧头部的**目的MAC地址**。 11 | 12 | #### 二、转发时延的决定因素 13 | 14 | 直通交换的转发时延,指的是从数据帧的第一个比特到达交换机输入端口,到该帧的第一个比特从目标输出端口离开所经历的时间。这个时延是固定的,其长短主要由以下两个因素决定: 15 | 16 | 1. **信道的数据传输速率**:即网络的带宽,例如100Mbps或1Gbps。速率越高,接收单位数据所需的时间就越短。 17 | 18 | 2. **交换机转发决策所需的数据量**:在直通模式下,这个数据量就是**目的MAC地址的长度**,即**6个字节(48比特)**。 19 | 20 | 21 | 因此,在给定的网络速率下,直通交换的转发时延是一个极小且恒定的值。它仅取决于交换机接收并解析完这6字节MAC地址所需的时间。最重要的一点是,该时延**与整个数据帧的总长度(无论是64字节的短帧还是1518字节的长帧)无关**。 22 | 23 | #### 三、关键对比:不同的交换方式 24 | 25 | 为了更清晰地理解直通交换的特性,可与另外两种方式进行对比: 26 | 27 | - **存储转发(Store-and-Forward)** 28 | 29 | - **机制**:必须接收并存储**整个**数据帧,完成CRC(循环冗余校验)差错检测后,再进行转发。 30 | 31 | - **时延**:时延是**可变的**,与数据帧的**完整长度成正比**。帧越长,延迟越高。 32 | 33 | - **优点**:可靠性高,能过滤掉传输错误的损坏帧。 34 | 35 | - **无碎片交换(Fragment-Free)** 36 | 37 | - **机制**:这是直通交换的一种改良。交换机在接收完帧的前**64个字节**后才开始转发。 38 | 39 | - **时延**:时延同样是**固定的**,但高于纯粹的直通模式。 40 | 41 | - **优点**:它能有效避免转发由网络冲突产生的、小于64字节的“碎片帧”,在速度和可靠性之间取得了一定的平衡。 42 | 43 | 44 | #### 四、总结 45 | 46 | |交换方式|转发决策点|转发时延特性|是否校验错误| 47 | |---|---|---|---| 48 | |**直通交换**|接收完**目的MAC地址**(前6字节)|**固定**,极低,与帧长无关|否| 49 | |**无碎片交换**|接收完**前64字节**|**固定**,较低,与帧长无关|否| 50 | |**存储转发**|接收完**整个帧**|**可变**,与帧长成正比|是| 51 | -------------------------------------------------------------------------------- /src/helpers/utils.js: -------------------------------------------------------------------------------- 1 | const slugify = require("@sindresorhus/slugify"); 2 | 3 | function headerToId(heading) { 4 | var slugifiedHeader = slugify(heading); 5 | if(!slugifiedHeader){ 6 | return heading; 7 | } 8 | return slugifiedHeader; 9 | } 10 | 11 | function namedHeadings(md, state) { 12 | 13 | var ids = {} 14 | 15 | state.tokens.forEach(function(token, i) { 16 | if (token.type === 'heading_open') { 17 | var text = md.renderer.render(state.tokens[i + 1].children, md.options) 18 | var id = headerToId(text); 19 | var uniqId = uncollide(ids, id) 20 | ids[uniqId] = true 21 | setAttr(token, 'id', uniqId) 22 | } 23 | }) 24 | } 25 | 26 | function uncollide(ids, id) { 27 | if (!ids[id]) return id 28 | var i = 1 29 | while (ids[id + '-' + i]) { i++ } 30 | return id + '-' + i 31 | } 32 | 33 | function setAttr(token, attr, value, options) { 34 | var idx = token.attrIndex(attr) 35 | 36 | if (idx === -1) { 37 | token.attrPush([attr, value]) 38 | } else if (options && options.append) { 39 | token.attrs[idx][1] = 40 | token.attrs[idx][1] + ' ' + value 41 | } else { 42 | token.attrs[idx][1] = value 43 | } 44 | } 45 | 46 | //https://github.com/rstacruz/markdown-it-named-headings/blob/master/index.js 47 | exports.namedHeadingsFilter = function (md, options) { 48 | md.core.ruler.push('named_headings', namedHeadings.bind(null, md)); 49 | } 50 | 51 | exports.headerToId = headerToId; -------------------------------------------------------------------------------- /src/site/notes/408/用户级线程为什么阻塞一个就全部阻塞了.md: -------------------------------------------------------------------------------- 1 | --- 2 | {"dg-publish":true,"dg-permalink":"/408/用户级线程为什么阻塞一个就全部阻塞了","permalink":"/408/用户级线程为什么阻塞一个就全部阻塞了/"} 3 | --- 4 | 5 | 6 | ## 用户级线程:一个线程阻塞,整个进程阻塞的原因 7 | 你描述的现象是用户级线程的一个显著特点和主要缺点。根本原因在于: 8 | 9 | **用户级线程的调度和管理完全由用户空间的线程库(Thread Library)完成,内核对这些用户级线程的存在是“无感知”的。对于内核来说,它只知道有一个“进程”在运行,而不知道这个进程内部有多少个用户级线程。** 10 | 11 | 下面我们来详细解释这一点: 12 | 13 | 1. **内核的视角**: 14 | 15 | - 当一个进程被调度执行时,**内核调度器**(操作系统的核心部分)会将CPU时间片分配给这个**进程**。 16 | - 在内核看来,无论是这个进程内部有一个线程还是上百个用户级线程,它都只把它当做一个单一的执行实体。 17 | - 内核只会管理**进程**的调度、内存、I/O等资源。 18 | 2. **线程库的作用**: 19 | 20 | - 用户级线程的创建、销毁、调度、同步等操作,都是由用户空间的**线程库**(比如POSIX Threads库,即Pthreads)来实现的。 21 | - 线程库在进程内部维护着所有用户级线程的上下文信息(如程序计数器、寄存器值、栈指针等)。 22 | - 当一个用户级线程执行时,它实际上是运行在分配给**整个进程**的CPU时间片内。线程库会根据自己的调度算法,在这些用户级线程之间进行**用户态的上下文切换**。 23 | 3. **阻塞式系统调用(Blocking System Calls)**: 24 | 25 | - 问题的关键在于**阻塞式系统调用**。当用户级线程执行一个阻塞式系统调用时(例如,`read()`从磁盘读取数据,`write()`向网络发送数据,`sleep()`暂停执行,或者等待某个I/O完成),它必须通过**陷入内核**的方式来请求操作系统服务。 26 | - 一旦用户级线程发出阻塞式系统调用,内核就会认为**整个进程**进入了阻塞状态(因为内核不知道这个进程内部还有其他用户级线程)。 27 | - 内核会将这个进程从运行队列中移除,放入等待队列,直到它所请求的I/O操作完成或者等待的事件发生。 28 | 4. **结果:整个进程被挂起**: 29 | 30 | - 由于内核已经将整个进程置于阻塞状态,并停止为其分配CPU时间片,因此,即使这个进程内部还有其他**用户级线程是就绪状态**,它们也无法获得CPU执行,因为整个进程已经被内核“冻结”了。 31 | - 这意味着,当一个用户级线程因I/O或同步操作而阻塞时,整个进程都会被阻塞,导致进程内的其他用户级线程也无法执行,就好像整个进程都“停”下来了一样。 32 | 33 | --- 34 | 35 | 36 | 37 | ## 与内核级线程的对比 38 | 为了更好地理解这一点,我们可以简单对比一下**内核级线程**: 39 | 40 | - **内核级线程(KLTs)**:每个线程都是内核可感知的独立调度实体。当一个内核级线程执行阻塞系统调用时,只有这一个线程会被阻塞,而同一进程内的其他内核级线程仍然可以被内核调度执行,从而实现真正的并发。 41 | 42 | --- 43 | -------------------------------------------------------------------------------- /src/site/get-theme.js: -------------------------------------------------------------------------------- 1 | require("dotenv").config(); 2 | const axios = require("axios"); 3 | const fs = require("fs"); 4 | const crypto = require("crypto"); 5 | const {globSync} = require("glob"); 6 | 7 | const themeCommentRegex = /\/\*[\s\S]*?\*\//g; 8 | 9 | async function getTheme() { 10 | let themeUrl = process.env.THEME; 11 | if (themeUrl) { 12 | //https://forum.obsidian.md/t/1-0-theme-migration-guide/42537 13 | //Not all themes with no legacy mark have a theme.css file, so we need to check for it 14 | try { 15 | await axios.get(themeUrl); 16 | } catch { 17 | if (themeUrl.indexOf("theme.css") > -1) { 18 | themeUrl = themeUrl.replace("theme.css", "obsidian.css"); 19 | } else if (themeUrl.indexOf("obsidian.css") > -1) { 20 | themeUrl = themeUrl.replace("obsidian.css", "theme.css"); 21 | } 22 | } 23 | 24 | const res = await axios.get(themeUrl); 25 | try { 26 | const existing = globSync("src/site/styles/_theme.*.css"); 27 | existing.forEach((file) => { 28 | fs.rmSync(file); 29 | }); 30 | } catch {} 31 | let skippedFirstComment = false; 32 | const data = res.data.replace(themeCommentRegex, (match) => { 33 | if (skippedFirstComment) { 34 | return ""; 35 | } else { 36 | skippedFirstComment = true; 37 | return match; 38 | } 39 | }); 40 | const hashSum = crypto.createHash("sha256"); 41 | hashSum.update(data); 42 | const hex = hashSum.digest("hex"); 43 | fs.writeFileSync(`src/site/styles/_theme.${hex.substring(0, 8)}.css`, data); 44 | } 45 | } 46 | 47 | getTheme(); 48 | -------------------------------------------------------------------------------- /src/site/notes/408/主存和外存的数据交换单位.md: -------------------------------------------------------------------------------- 1 | --- 2 | {"dg-publish":true,"dg-permalink":"/408/主存和外存的数据交换单位","permalink":"/408/主存和外存的数据交换单位/"} 3 | --- 4 | 5 | 6 | 7 | ### 1. 主存 (Main Memory) 的访问单位 8 | 9 | - **从CPU(硬件)的角度看:字节 (Byte) 或 字 (Word)** 10 | 11 | - **可寻址的最小单位是“字节 (Byte)”** 12 | 13 | - **CPU进行数据存取的单位通常是“字 (Word)”**。 14 | 15 | 16 | --- 17 | 18 | ### 2. 外存 (External Storage) 的访问单位 19 | 20 | - **从硬件(磁盘驱动器)的角度看:扇区 (Sector)** 21 | 22 | - 磁盘上的数据在物理上被划分为一个个大小固定的**扇区**。扇区是磁盘可以进行读/写的**最小物理单位**。典型的扇区大小为512字节或4KB。磁盘控制器一次只能读/写整数个扇区,无法只读一个扇区中的某几个字节。 23 | 24 | - **从操作系统(文件系统)的角度看:块 (Block) 或 簇 (Cluster)** 25 | 26 | - 操作系统为了方便管理,屏蔽掉底层硬件的细节,它将磁盘空间在逻辑上划分为一个个的**“块”**(在UNIX/Linux中称为Block)或**“簇”**(在Windows中称为Cluster)。 27 | 28 | - 一个块通常由**一个或多个连续的扇区**组成(块的大小是扇区大小的整数倍,且通常是2的幂,如1KB, 2KB, 4KB)。 29 | 30 | - 操作系统对磁盘进行I/O操作(文件读写)的**基本单位是“块”**。即使你只想读取文件中的1个字节,操作系统也必须将包含这个字节的**整个块**从磁盘读入到内存的缓冲区中,然后再从中提取出你需要的字节。同理,写数据也是以块为单位。 31 | 32 | - **为什么这么做?** 33 | 34 | - **提高效率**: 磁盘I/O的寻道和旋转延迟非常高,远大于数据传输时间。如果每次只读写一个字节,那绝大部分时间都将浪费在寻道和旋转上。一次性读写一个较大的数据块,可以大大摊销这些机械延迟的成本,提高I/O吞吐率。 35 | 36 | - **简化管理**: 以块为单位进行地址映射和空间管理,比以字节或扇区为单位要简单得多,可以减少文件系统元数据(如FAT表、i-node)的大小。 37 | 38 | 39 | --- 40 | 41 | ### 总结与对比表格 42 | 43 | |存储介质|硬件/物理访问单位|操作系统/逻辑访问单位| 44 | |---|---|---| 45 | |**主存 (内存)**|**字节 (Byte)** 是最小可寻址单位。
**字 (Word)** 是CPU一次操作传送的数据单位。|通常与硬件层面一致,操作系统可以直接按**字节**或**字**进行内存分配和访问。| 46 | |**外存 (磁盘)**|**扇区 (Sector)** 是磁盘控制器进行读/写的最小物理单位。|**块 (Block) / 簇 (Cluster)** 是文件系统进行I/O操作的最小逻辑单位。| 47 | 48 | **核心要点**: 49 | 50 | - **CPU可以直接访问主存的任一字节。** 51 | 52 | - **CPU不能直接访问外存,必须通过操作系统发出I/O请求。** 53 | 54 | - **操作系统对外存的访问是成块进行的,无法只读/写一个字节或一个扇区。** -------------------------------------------------------------------------------- /src/site/_includes/components/calloutScript.njk: -------------------------------------------------------------------------------- 1 | 2 | 38 | -------------------------------------------------------------------------------- /src/site/notes/408/通道技术.md: -------------------------------------------------------------------------------- 1 | --- 2 | {"dg-publish":true,"dg-permalink":"/408/通道技术","permalink":"/408/通道技术/"} 3 | --- 4 | 5 | 6 | 操作系统中的**通道技术**是一种用于高效管理输入/输出(I/O)操作的硬件机制,其核心目标是**将I/O操作从CPU的直接控制中分离出来**,从而提升系统整体性能。以下是其详细解析: 7 | 8 | --- 9 | 10 | 11 | 12 | ### 1. **通道的定义与本质** 13 | 通道(I/O Channel)是一个**独立于CPU的专用处理机**,专门负责管理外部设备与内存之间的数据交换。它能够接收中央处理机(CPU)的命令,独立执行通道程序,协助CPU控制和管理外部设备 。 14 | 例如,在早期计算机中,CPU需要全程参与数据从磁盘读取到内存的过程,而通道技术引入后,CPU只需发出启动I/O的指令,后续数据传输由通道自动完成,无需CPU干预 。 15 | 16 | --- 17 | 18 | 19 | 20 | ### 2. **通道的核心功能** 21 | - **数据传输控制**: 22 | 通道负责在外设与内存之间直接传输数据,例如从磁盘读取文件或向打印机发送输出数据。这种方式避免了CPU频繁介入,显著减少CPU负载 。 23 | - **设备状态监控**: 24 | 通道会实时监控外设的状态(如是否空闲、是否发生错误),并在操作完成后向CPU发送中断信号,报告任务完成或异常情况 。 25 | - **错误处理**: 26 | 在数据传输过程中,通道能检测并处理部分硬件错误(如校验错误),确保数据完整性 。 27 | 28 | --- 29 | 30 | 31 | 32 | ### 3. **通道的引入目的** 33 | - **解放CPU资源**: 34 | 通过将I/O操作从CPU转移到通道,CPU得以专注于计算任务,而非等待外设完成数据传输。例如,若没有通道,CPU需等待磁盘读取数据完成才能继续执行,而通道可并行处理这一过程 。 35 | - **提升系统并行性**: 36 | 通道与CPU可同时工作,实现指令执行与数据传输的并行化。例如,CPU计算数据的同时,通道可将结果写入磁盘 。 37 | - **简化I/O管理**: 38 | 通道通过执行预定义的通道程序(Channel Program)自动完成复杂的I/O操作,减少了操作系统内核的管理复杂度 。 39 | 40 | --- 41 | 42 | 43 | 44 | ### 4. **通道的工作机制** 45 | - **通道程序**: 46 | 通道执行一段由操作系统预先编写的通道指令序列(Channel Command Word, CCW),描述数据传输的源地址、目标地址、数据长度等信息。例如,读取磁盘扇区的操作会被分解为多个通道指令 。 47 | - **中断通知**: 48 | 当通道完成数据传输或发生异常时,会通过中断机制通知CPU,由操作系统处理后续逻辑(如唤醒等待I/O的进程) 。 49 | - **直接内存访问(DMA)协作**: 50 | 部分通道结合DMA技术,进一步实现外设与内存之间的高速数据交换,完全绕过CPU 。 51 | 52 | --- 53 | 54 | 55 | 56 | ### 5. **通道技术的现实意义** 57 | - **历史演进**: 58 | 通道技术最早出现在大型机系统中(如IBM System/360),是计算机体系结构的重要创新。它标志着I/O操作从“CPU全程控制”向“独立硬件管理”的转变 。 59 | - **现代应用**: 60 | 尽管现代计算机更多依赖DMA和总线控制器(如PCIe)来管理高速设备,但通道技术的核心思想(如独立I/O处理单元)仍体现在许多硬件设计中,例如网络接口卡(NIC)的卸载功能 。 61 | 62 | --- 63 | 64 | 65 | [[408(demo)/通道技术和DMA的区别解释\|408(demo)/通道技术和DMA的区别解释]] 66 | -------------------------------------------------------------------------------- /src/site/notes/408/指令集里的“断舍离”大师——Load Store.md: -------------------------------------------------------------------------------- 1 | --- 2 | {"dg-publish":true,"dg-permalink":"/408/指令","permalink":"/408/指令/"} 3 | --- 4 | 5 | 6 | 相较于允许运算指令直接访问内存的指令系统,Load/Store型指令系统的核心优点在于:**指令格式更规整、控制器设计更简单,从而极大地有利于实现指令流水线,提升处理器吞吐率**。 7 | 8 | ### 一、Load/Store架构的优点详解 9 | 10 | 为了完成一个“内存变量A + 内存变量B -> 内存变量C”的操作,两种架构的指令序列对比如下: 11 | 12 | |寄存器-存储器型 (R-M, CISC)|Load/Store型 (R-R, RISC)| 13 | |---|---| 14 | |`MOV R1, A` ; 将内存A加载到R1|`LOAD R1, A` ; 将内存A加载到R1| 15 | |`ADD R1, B` ; R1 <- R1 + [B]|`LOAD R2, B` ; 将内存B加载到R2| 16 | |`MOV C, R1` ; 将R1结果存回内存C|`ADD R3, R1, R2` ; R3 <- R1 + R2| 17 | ||`STORE C, R3` ; 将R3结果存回内存C| 18 | 19 | 表面上看,Load/Store型指令数量更多,似乎更繁琐,但其优势体现在以下几个方面: 20 | 21 | #### 1. 指令格式规整,多为定长指令 22 | 23 | - **R-M架构**:`ADD R1, B` 这类指令,既要指定寄存器`R1`,又要指定内存地址`B`。内存地址的寻址方式多种多样(直接寻址、间接寻址等),导致指令长度可变,格式复杂。 24 | 25 | - **L/S架构**:算术指令如 `ADD R3, R1, R2`,操作数都在寄存器中,只需用固定的位数(如5位)指明寄存器号即可。这使得大部分指令可以设计成**长度固定的格式**。 26 | 27 | 28 | #### 2. 简化指令解码与控制器设计 29 | 30 | - **定长指令**意味着CPU的指令解码器(ID)无需判断当前指令到底有多长,可以直接按固定长度取指、解码,大大简化了控制逻辑。 31 | 32 | - 指令类型单一(运算类、访存类、转移类分明),功能正交,使得控制器可以用更简单、更高速的**硬布线逻辑**来实现,而非CISC中常用的、速度较慢的微程序控制器。 33 | 34 | 35 | #### 3. 极大地有利于指令流水线执行 36 | 37 | 这是Load/Store架构**最核心、最重要**的优势。一个典型的五段流水线(IF取指, ID解码, EX执行, MEM访存, WB写回)可以清晰地展示这一点。 38 | 39 | - R-M架构的流水线困境: 40 | 41 | ADD R1, B这条指令,在EX(执行)阶段需要计算地址和执行加法,紧接着又需要在MEM(访存)阶段从内存读取操作数B。这导致单个指令需要跨越多个流水段的核心功能区,或者说访存操作被嵌入到了运算指令中。这会造成严重的结构冒险(EX和MEM阶段争用内存地址总线)或数据冒险,并使得流水线各段执行时间极不均衡,大大增加了流水线设计的复杂性,充满了“气泡”(stall)。 42 | 43 | - L/S架构的流水线优势: 44 | 45 | 其设计将运算和访存彻底分离,完美契合流水线的分段思想: 46 | 47 | - `ADD R3, R1, R2`:这类指令只在EX阶段对寄存器内容进行运算,速度极快。它根本不涉及MEM阶段,可以直接“飘过”。 48 | 49 | - `LOAD R1, A` / `STORE C, R3`:这类指令的核心工作在MEM阶段完成,而EX阶段可能只做简单的地址计算。 50 | 51 | - **结果**:每条指令各司其职,在自己对应的流水段完成核心工作。流水线可以顺畅地流动,极少发生停顿,大大提高了**指令的吞吐率**(单位时间完成的指令数),实现了CPI≈1的目标。 52 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "prebuild": "rimraf dist", 8 | "start": "npm-run-all get-theme build:sass --parallel watch:*", 9 | "watch:sass": "sass --watch src/site/styles:dist/styles", 10 | "watch:eleventy": "cross-env ELEVENTY_ENV=dev eleventy --serve", 11 | "build:eleventy": "cross-env ELEVENTY_ENV=prod NODE_OPTIONS=--max-old-space-size=4096 eleventy", 12 | "build:sass": "sass src/site/styles:dist/styles --style compressed", 13 | "get-theme": "node src/site/get-theme.js", 14 | "build": "npm-run-all get-theme build:*" 15 | }, 16 | "keywords": [], 17 | "author": "", 18 | "license": "ISC", 19 | "devDependencies": { 20 | "@11ty/eleventy": "^2.0.1", 21 | "@11ty/eleventy-plugin-rss": "^1.2.0", 22 | "cross-env": "^7.0.3", 23 | "html-minifier-terser": "^7.2.0", 24 | "node-html-parser": "^6.1.13", 25 | "sass": "^1.49.9" 26 | }, 27 | "dependencies": { 28 | "@11ty/eleventy-img": "^4.0.2", 29 | "@sindresorhus/slugify": "^1.1.0", 30 | "axios": "^1.2.2", 31 | "dotenv": "^16.0.3", 32 | "eleventy-plugin-gen-favicons": "^1.1.2", 33 | "eleventy-plugin-nesting-toc": "^1.3.0", 34 | "fs-file-tree": "^1.1.1", 35 | "glob": "^10.2.1", 36 | "gray-matter": "^4.0.3", 37 | "markdown-it": "^14.1.0", 38 | "markdown-it-anchor": "^9.0.1", 39 | "markdown-it-attrs": "^4.1.6", 40 | "markdown-it-footnote": "^3.0.3", 41 | "markdown-it-mark": "^4.0.0", 42 | "markdown-it-mathjax3": "^4.3.1", 43 | "markdown-it-plantuml": "^1.4.1", 44 | "markdown-it-task-checkbox": "^1.0.6", 45 | "npm-run-all": "^4.1.5", 46 | "rimraf": "^4.4.1" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/site/_data/dynamics.js: -------------------------------------------------------------------------------- 1 | const fsFileTree = require("fs-file-tree"); 2 | 3 | const BASE_PATH = "src/site/_includes/components/user"; 4 | const STYLE_PATH = "src/site/styles/user"; 5 | const NAMESPACES = ["index", "notes", "common"]; 6 | const SLOTS = ["head", "header", "beforeContent", "afterContent", "footer"]; 7 | const FILE_TREE_NAMESPACE = "filetree"; 8 | const FILE_TREE_SLOTS = ["beforeTitle", "afterTitle"]; 9 | const SIDEBAR_NAMESPACE = "sidebar"; 10 | const SIDEBAR_SLOTS = ["top", "bottom"]; 11 | const STYLES_NAMESPACE = "styles"; 12 | 13 | const generateComponentPaths = async (namespace, slots) => { 14 | const data = {}; 15 | for (let index = 0; index < slots.length; index++) { 16 | const slot = slots[index]; 17 | try { 18 | const tree = await fsFileTree(`${BASE_PATH}/${namespace}/${slot}`); 19 | let comps = Object.keys(tree) 20 | .filter((p) => p.indexOf(".njk") != -1) 21 | .map((p) => `components/user/${namespace}/${slot}/${p}`); 22 | comps.sort(); 23 | data[slot] = comps; 24 | } catch { 25 | data[slot] = []; 26 | } 27 | } 28 | return data; 29 | }; 30 | 31 | const generateStylesPaths = async () => { 32 | try { 33 | const tree = await fsFileTree(`${STYLE_PATH}`); 34 | let comps = Object.keys(tree).map((p) => 35 | `/styles/user/${p}`.replace(".scss", ".css") 36 | ); 37 | comps.sort(); 38 | return comps; 39 | } catch { 40 | return []; 41 | } 42 | }; 43 | 44 | module.exports = async () => { 45 | const data = {}; 46 | for (let index = 0; index < NAMESPACES.length; index++) { 47 | const ns = NAMESPACES[index]; 48 | data[ns] = await generateComponentPaths(ns, SLOTS); 49 | } 50 | data[FILE_TREE_NAMESPACE] = await generateComponentPaths( 51 | FILE_TREE_NAMESPACE, 52 | FILE_TREE_SLOTS 53 | ); 54 | data[SIDEBAR_NAMESPACE] = await generateComponentPaths( 55 | SIDEBAR_NAMESPACE, 56 | SIDEBAR_SLOTS 57 | ); 58 | data[STYLES_NAMESPACE] = await generateStylesPaths(); 59 | return data; 60 | }; 61 | -------------------------------------------------------------------------------- /src/site/notes/408/页式管理与段式管理内存.md: -------------------------------------------------------------------------------- 1 | --- 2 | {"dg-publish":true,"dg-permalink":"/408/页式管理与段式管理内存","permalink":"/408/页式管理与段式管理内存/"} 3 | --- 4 | 5 | 6 | ### 正确的理解: 7 | **页式管理下的逻辑地址空间是“一维的”,指的是对于用户程序(程序员)而言,他们所使用的地址(即虚拟地址或逻辑地址)是一个单一的、连续的、从0开始的线性序列。** 8 | 9 | 让我们来详细解释: 10 | 11 | #### 1. 页式管理:一维的逻辑地址空间 12 | - **用户程序的视角**:当程序员编写代码时,他们定义变量、函数等,并使用指针等来访问内存。他们所使用的地址(无论是变量名、数组下标还是指针的值)都是**一个单一的数字**,这个数字表示从程序起始位置开始的偏移量。例如,一个变量 `int x` 的地址可能是 `0x1000`,程序员直接使用 `0x1000` 这个单一的数字来引用它。他们**不需要**说“我访问的是第N页的第M个字节”。 13 | 14 | - **地址的内部拆分**:当CPU生成这个**一维的逻辑地址**(例如 `0x1000`)后,是**硬件(MMU)**在底层**自动且透明地**将这个单一的数字拆分成“页号”和“页内偏移量”。这个拆分过程对程序员是完全**隐藏**的。程序员无需关心页的大小、页的边界、如何计算页号和偏移量。 15 | 16 | - **无逻辑意义的划分**:页的划分是**物理内存管理层面**的,是**固定大小**的。它不考虑程序本身的逻辑结构(比如,一个函数可能跨越两个页的边界)。对用户来说,页号和页内偏移量这两个概念**没有内在的逻辑意义**,它们只是为了方便硬件进行地址转换而人为进行的物理地址空间的划分。 17 | 18 | 19 | **简而言之,用户给出一个线性地址(一个数字),MMU将其解释为页号和偏移量。用户本身不关心这个解释过程,也不需要提供两个独立的数字。** 20 | 21 | 22 | #### 2. 段式管理:二维的逻辑地址空间 23 | - **用户程序的视角**:在段式管理中,逻辑地址被显式地分为“段号”和“段内偏移量”两个部分。这里的**段号是具有逻辑意义的**。 24 | 25 | - 例如,程序员可能知道他们的代码在“代码段”中,数据在“数据段”中,栈在“栈段”中。 26 | 27 | - 在一些支持段式寻址的体系结构(如早期的Intel x86处理器在实模式或保护模式的某些寻址方式下)中,程序员或编译器在生成地址时,**确实需要明确指定**是哪个段,以及在该段内的偏移量。例如,一个完整逻辑地址可能表示为 `CS:IP` (代码段寄存器:指令指针) 或 `DS:Offset` (数据段寄存器:偏移量)。这里的 `CS` 或 `DS` 就是段号(或段选择子)。 28 | 29 | - 用户程序在访问内存时,**会感知到**要访问的是哪个逻辑段,以及该段内的哪个位置。这对应了程序的**逻辑结构**。 30 | 31 | - **有逻辑意义的划分**:段的划分是**基于程序的逻辑结构**进行的,例如代码段、数据段、栈段、子程序段等。每个段都有其特定的功能和属性。因此,**段号本身携带着程序的逻辑信息**。 32 | 33 | 34 | **简而言之,用户需要提供两个有逻辑意义的数字:段号(表示哪个逻辑部分)和偏移量(表示该逻辑部分中的位置)。** 35 | 36 | 37 | 38 | ### 总结页和段的维度差异: 39 | |特性|页式管理|段式管理| 40 | |---|---|---| 41 | |**用户感知/给出地址**|**一维的线性地址**(一个单一的数字,例如 `0x12345`)|**二维的逻辑地址**(例如 `(代码段, 0x100)` 或 `(数据段, 0x50)`)| 42 | |**地址解析者**|**硬件(MMU)** 自动将一维地址拆分为页号和偏移量|**程序员/编译器/硬件** 共同管理,地址本身就带有段号和偏移量| 43 | |**划分依据**|**物理内存的固定大小划分**,与程序逻辑无关|**程序的逻辑结构**(代码、数据、栈等)划分,与物理内存无关| 44 | |**透明性**|**对用户透明**,用户无需关心页的存在|**对用户不透明**,用户感知并使用段的概念| 45 | 46 | **因此,当说页式管理下的地址空间是“一维的”时,强调的是用户程序所操作的逻辑地址是一个连续的、不区分内部结构的线性地址空间。而段式管理下是“二维的”,强调的是用户程序所使用的逻辑地址本身就包含两个独立的、具有逻辑意义的组成部分(段号和段内偏移)。** 47 | 48 | 希望这次的解释能彻底理清您关于地址维度的问题! 49 | -------------------------------------------------------------------------------- /src/site/notes/408/区分受限广播和直通广播.md: -------------------------------------------------------------------------------- 1 | --- 2 | {"dg-publish":true,"dg-permalink":"/408/区分受限广播和直通广播","permalink":"/408/区分受限广播和直通广播/"} 3 | --- 4 | 5 | 6 | #### 一、 受限广播 (Limited Broadcast) —— 在本间教室里吼一嗓子 7 | 8 | 受限广播,顾名思义,其传播范围被严格地**限制在当前所在的物理网段(广播域)内**。 9 | 10 | - IP地址格式:固定为 255.255.255.255。 11 | 12 | 这个地址的每一位都是1,它不代表任何特定的网络,而是泛指“本网络上的所有主机”。 13 | 14 | - 路由器行为:路由器永远不会转发目的地址为 255.255.255.255 的数据包。 15 | 16 | 这是IP协议的硬性规定。路由器是网络之间的边界,它会自觉地将这种“本地呐喊”隔离在本地,防止其泛滥到整个互联网。 17 | 18 | - 典型应用场景: 19 | 20 | 最经典的应用就是DHCP协议。当一台新主机接入网络,它一无所有——不知道自己的IP、不知道子网掩码、也不知道网关在哪。此时,它只能用255.255.255.255作为目的地址,向本地网络中发出一个DHCP Discover的“求救信号”,大喊:“有DHCP服务器吗?谁能给我分配个地址?” 21 | 22 | 23 | #### 二、 直接广播 (Directed Broadcast) —— 对着邻班教室全员广播 24 | 25 | 直接广播,是指一个主机向**另一个特定的、非本地的目标网络**上的所有主机发送数据包。 26 | 27 | - IP地址格式:{目标网络号}{主机号全为1}。 28 | 29 | 例如,对于网络 172.16.0.0/16,其直接广播地址就是 172.16.255.255。发送给这个地址的数据包,意图是让172.16.0.0网络中的每一台主机都收到。 30 | 31 | - **路由器行为**:(**这是关键区别,也是重要的安全考点**) 32 | 33 | - **理论上**:路由器**可以**转发直接广播报文。例如,主机A(在10.0.0.0/8网络)向`172.16.255.255`发送数据包。数据包会被路由到`172.16.0.0/16`网络的边界路由器上。该路由器识别出这是一个指向其所连网络的直接广播地址,于是将该数据包在其连接的`172.16.0.0/16`网络上以广播的形式发送出去。 34 | 35 | - **现实中**:由于直接广播极易被用于**Smurf(蓝精灵)攻击**等DDoS攻击手段,现代网络设备出于安全考虑,**默认情况下都禁止转发直接广播报文**(遵循RFC 2644建议)。管理员需要手动开启才会允许转发。因此,在今天的网络环境中,跨网段的直接广播基本已被弃用。 36 | 37 | 38 | #### 三、 核心区别总结 39 | 40 | 为了更清晰地对比,我们将两者放入表格中: 41 | 42 | |对比维度|受限广播 (Limited Broadcast)|直接广播 (Directed Broadcast)| 43 | |---|---|---| 44 | |**IP地址格式**|`255.255.255.255`|`{网络号}{主机号全1}` (如 `192.168.1.255`)| 45 | |**目标范围**|**当前**所在的物理网段|**指定**的、特定的目标网络| 46 | |**路由器行为**|**永远不会转发**|**默认禁止转发**(因安全风险),理论上可配置转发| 47 | |**典型应用**|主机在**未知网络参数**时(如DHCP)进行本地发现|(历史用途)网络管理员向**远程特定网络**发送管理信息| 48 | |**链路层封装**|目的MAC地址**永远**是广播MAC `FF:FF:FF:FF:FF:FF`|在到达最终网络**前**,目的MAC是下一跳路由器的MAC;在**最后一跳**时,目的MAC才变为 `FF:FF:FF:FF:FF:FF`| 49 | 50 | #### 四、 结论 51 | 52 | 简单来说,两者最本质的区别在于**作用范围和路由器的处理方式**: 53 | 54 | - **受限广播 (`255.255.255.255`)** 是一张**单程本地车票**,永远出不了当前这个“站”(广播域)。 55 | 56 | - **直接广播 (`{net-id}{host-id all 1s}`)** 是一张寄往**远方某个城市所有居民**的“明信片”,但如今的“邮局”(路由器)出于安全考虑,大多都已拒收并销毁这种明信片了。 57 | -------------------------------------------------------------------------------- /src/site/notes/408/资源分配图.md: -------------------------------------------------------------------------------- 1 | --- 2 | {"dg-publish":true,"dg-permalink":"/408/资源分配图","permalink":"/408/资源分配图/"} 3 | --- 4 | 5 | 6 | ### 1. 资源分配图 (RAG) 的构成 7 | 8 | 资源分配图是一个有向图,由两类节点和两种有向边构成: 9 | 10 | - **两类节点**: 11 | 12 | 1. **进程节点 (Process Node)**: 通常用**圆形**表示,内部写有进程的标识符(如 `P1`, `P2`)。 13 | 14 | 2. **资源节点 (Resource Node)**: 通常用**矩形**表示,内部写有资源的标识符(如 `R1`, `R2`)。矩形框中的**小圆点或方点**代表该类资源的**实例数量**。例如,一个矩形内有2个点,表示该类资源有2个实例(如系统有2台打印机)。 15 | 16 | - **两种有向边**: 17 | 18 | 1. **请求边 (Request Edge)**: 从**进程指向资源**的有向边 (`P_i \rightarrow R_j`)。它表示进程 `Pi` 正在**请求**一个 `Rj` 类的资源实例,但尚未获得,正处于等待状态。 19 | 20 | 2. **分配边 (Assignment Edge)**: 从**资源实例指向进程**的有向边 (`R_j \rightarrow P_i`)。它表示 `Rj` 类中的一个资源实例已经被**分配**给了进程 `Pi`,`Pi` 正在持有并使用它。 21 | 22 | 23 | ### 3. 如何用资源分配图检测死锁 24 | 25 | 使用资源分配图检测死锁的规则,根据资源实例的数量分为两种情况, 26 | 27 | #### 情况一:每类资源只有一个实例 28 | 29 | 当系统中每种类型的资源都只有一个实例时,检测规则非常简单: 30 | 31 | **规则**: **图中存在环路 (Cycle) 是死锁发生的充分必要条件。** 32 | 33 | - **有环路 ⇒ 必有死锁** 34 | 35 | - **有死锁 ⇒ 必有环路** 36 | 37 | 38 | #### 情况二:某些资源类型有多个实例 39 | 40 | 当系统中存在拥有多个实例的资源类型时,情况变得复杂: 41 | 42 | **规则**: **图中存在环路是死锁发生的必要不充分条件。** 43 | 44 | - **有环路 ⇒ 不一定有死锁** (这是你问题的关键) 45 | 46 | - **有死锁 ⇒ 必有环路** 47 | 48 | 49 | --- 50 | 51 | ### 4. 为什么“有环路”不一定是死锁?(考点与难点) 52 | 53 | 这种情况的本质在于:**虽然一个进程 `P_i` 在环路中等待的资源 `R_j` 被另一个进程 `P_j` 占用,但 `R_j` 可能还有其他空闲的实例,或者环路外的某个进程可能释放一个 `R_j` 的实例来满足 `P_i` 的请求。** 54 | 55 | 另一种无死锁的环路情况: 56 | 57 | 即使 R1 没有空闲实例,但如果有一个不在环路中的进程 P4 持有 R1 的一个实例,并且 P4 无需再申请其他资源即可运行完毕。那么当 P4 运行完并释放 R1 后,P1 的请求也能被满足,环路同样被打破。 58 | 59 | 如何判断这种复杂情况?——图简化法 (Graph Reduction) 60 | 61 | 为了在多实例场景下准确判断死锁,我们需要尝试简化资源分配图: 62 | 63 | 1. 首先找到一个**不被阻塞**的进程 `Pi`(即它的所有请求边指向的资源 `Rj` 都有空闲的实例)。 64 | 65 | 2. 如果找不到这样的进程,则图无法简化,图中所有剩余进程都是死锁进程。 66 | 67 | 3. 如果找到了,就**去掉**该进程 `Pi` 的所有请求边和分配边,假装它已运行完毕并释放了所有资源。 68 | 69 | 4. 重复步骤1,继续寻找新的不被阻塞的进程。 70 | 71 | 5. 如果最终能**消除图中所有的边**,则说明系统没有发生死锁;否则,发生了死锁。 72 | 73 | 74 | **总结**: 75 | 76 | - **单实例资源**: **环路 ⇔ 死锁**,检测非常简单。 77 | 78 | - **多实例资源**: **死锁 ⇒ 环路**,但**环路 ⇒ 不一定死锁**。必须通过图简化法或类似银行家算法的死锁检测算法来最终确认。这是因为环路中的进程所等待的资源,可能由**空闲实例**或**环路外即将结束的进程**来满足。 -------------------------------------------------------------------------------- /src/site/notes/408/段页式管理下的地址映射表组织🥰.md: -------------------------------------------------------------------------------- 1 | --- 2 | {"dg-publish":true,"dg-permalink":"/408/段页式管理下的地址映射表组织🥰","permalink":"/408/段页式管理下的地址映射表组织🥰/"} 3 | --- 4 | 5 | 6 | 在段页式管理中,每个进程的地址映射体系是这样组织的: 7 | 8 | - **一个进程对应一张段表。** 9 | 10 | - **每个段对应一张页表。** 11 | 12 | 13 | --- 14 | 15 | ### 2. 地址映射表的组织结构与原因 16 | 17 | #### 2.1 为什么一个进程只有一张段表? 18 | 19 | - **段表是进程逻辑地址空间的“目录”**: 在分段思想中,一个进程的整个逻辑地址空间是由它所拥有的所有“段”共同构成的。段表的作用就是这个进程所有段的总索引或总目录。CPU通过查询这张总目录,才能知道进程有哪些段,以及每个段的详细信息在哪里。 20 | 21 | - **与进程上下文绑定**: 段表是描述一个进程内存布局的核心数据结构之一,它和进程控制块(PCB)一样,是进程上下文的重要组成部分。每个进程都需要自己独立的逻辑地址空间视图,因此每个进程都必须拥有自己私有的、唯一的段表。操作系统通过段表基址寄存器(STBR)来定位当前运行进程的段表。 22 | 23 | 24 | 所以,**一个进程** 对应 **一张段表**,这是由“进程作为资源分配和独立调度的基本单位”这一根本原则决定的。 25 | 26 | #### 2.2 为什么每个段都有一张自己的页表? 27 | 28 | 这是段页式管理与单纯分页管理最核心的区别所在。 29 | 30 | - **分页的对象是“段”而非“整个进程”**: 在单纯的分页系统中,页表映射的是整个**一维、线性**的进程逻辑地址空间。但在段页式系统中,分页是在分段的基础上进行的。系统不再为整个巨大的、统一的逻辑空间建立一张可能非常稀疏的页表,而是为**每一个逻辑上独立的段**建立一张属于它自己的页表。 31 | 32 | - **段内地址的映射需求**: 逻辑地址被分为 `(段号, 段内页号, 页内偏移)`。当CPU通过段号在段表中找到对应表项后,它需要将地址的第二部分“段内页号”转换为物理页帧号。这个转换工作由谁来完成?答案就是这个段自己的页表。段表项中不再直接存放物理地址,而是存放该段对应的**页表的基地址**和**页表长度**。 33 | 34 | - **提高空间利用率**: 这样做极大地提高了内存空间的利用率。在单纯的分页系统中,如果一个32位进程的逻辑地址空间非常大(4GB),但实际只使用了几小块内存(例如,一小块代码区,一小块数据区),那么也需要一个巨大的页表来映射整个4GB空间,其中绝大部分表项是空的,造成巨大浪费。而在段页式中,我们只需要为实际存在的、大小不一的段创建大小合适的页表即可,没有使用的巨大虚拟地址空间空洞将不会产生任何页表开销。 35 | 36 | 37 | ### 3. 地址映射过程 38 | 39 | 40 | **逻辑地址: (段号 `S`, 段内页号 `P`, 页内偏移 `d`)** 41 | 42 | 1. **查段表**: 43 | 44 | - 用逻辑地址中的**段号 `S`** 去查询当前进程的**段表**。 45 | 46 | - 首先检查 `S` 是否越界(小于段表长度)。 47 | 48 | - 从段表中找到第 `S` 个表项,从中取出该段对应的**页表的基地址**和**页表长度**。 49 | 50 | 2. **查页表**: 51 | 52 | - 用逻辑地址中的**段内页号 `P`** 去查询上一步找到的**那个段的页表**。 53 | 54 | - 首先检查 `P` 是否越界(小于页表长度)。 55 | 56 | - 从该页表中找到第 `P` 个表项,从中取出该页对应的**物理页帧号**。 57 | 58 | 3. **形成物理地址**: 59 | 60 | - 将上一步得到的**物理页帧号**与逻辑地址中的**页内偏移 `d`** 进行拼接,最终形成访问主存的物理地址。 61 | 62 | 63 | ### 总结 64 | 65 | - **组织结构**: **每个进程一张段表,每个段一张页表**。 66 | 67 | - **原因**: 68 | 69 | - **一张段表**:因为段表是进程逻辑地址空间的总纲和目录,每个进程必须有自己独立的视图。 70 | 71 | - **多张页表**:因为分页是在分段的基础上对**各个独立的段**进行的,为每个段建立独立的页表可以有效避免为巨大而稀疏的虚拟地址空间建立一个同样巨大而浪费的单一页表,从而大大节省了存储页表的空间开销。 -------------------------------------------------------------------------------- /src/site/notes/408/成组链接法你怎么和空闲盘号栈有一腿😰.md: -------------------------------------------------------------------------------- 1 | --- 2 | {"dg-publish":true,"dg-permalink":"/408/成组链接法你怎么和空闲盘号栈有一腿😰","permalink":"/408/成组链接法你怎么和空闲盘号栈有一腿😰/"} 3 | --- 4 | 5 | 6 | ### **空闲盘块栈与成组链接法的关系:核心与框架** 7 | 8 | 核心关系: 9 | 10 | 空闲盘块栈是实现成组链接法在内存中的核心组件和高速缓冲区。成组链接法是一种宏观的、旨在高效管理整个磁盘空闲空间的算法框架,而空闲盘块栈则是这个框架在内存中进行快速操作的具体实现手段。它们是整体与部分、框架与核心的关系,而非两种并列的方法。 11 | 12 | 可以这样比喻: 13 | 14 | - **成组链接法**好比一个大型物流公司的**整套仓储管理系统**。 15 | 16 | - **空闲盘块栈**则是这个系统在每个发货点设置的**“快速拣货区”**。 17 | 18 | 19 | 这个“快速拣货区”(空闲盘块栈)地方不大,但存放着最常用、即将要发出的货物(空闲块编号)。绝大多数订单(分配请求)都能在这里快速完成。当拣货区空了,系统才会启动一个流程,去远方的大仓库(磁盘上的其他空闲块组)调拨一整批货过来补满。反之,退货(回收块)也优先放回这个拣货区,满了之后再整批运回大仓库。 20 | 21 | --- 22 | 23 | ### **成组链接法是如何使用“空闲盘块栈”的** 24 | 25 | 成组链接法通过在**内存中的超级块(Superblock)**里实现一个**固定大小的、小规模的空闲盘块栈**,来处理日常的分配与回收。这个栈就是它对“空闲盘块栈”思想的直接应用。 26 | 27 | 下面是它使用这个内存栈的完整工作流程: 28 | 29 | #### **1. 初始化与结构** 30 | 31 | - **内存中的组件**: 在文件系统的超级块中,有一个我们称之为**“当前可用栈”**的区域,它就是一个**空闲盘块栈**。这个栈的大小是固定的,比如 `N`(通常为100)。 32 | 33 | - **磁盘上的组件**: 磁盘上剩余的所有空闲块被分成若干“组”,每组的第一个块(头块)记录着组内其他空-闲块的地址和指向下一个组的指针。 34 | 35 | - **链接**: 内存中“当前可用栈”的**栈底**,始终保存着指向磁盘上第一个空闲块分组的“头块”的地址。 36 | 37 | 38 | #### **2. 分配空闲块时,如何使用内存栈?** 39 | 40 | 当一个进程需要一个空闲块时: 41 | 42 | 1. **检查内存栈**: 系统首先查看“当前可用栈”是否为空。 43 | 44 | 2. **情况A:栈不为空(最常见的情况)** 45 | 46 | - 直接对内存栈执行**出栈**操作,从栈顶弹出一个空闲盘块的编号。 47 | 48 | - 将这个编号返回给请求者。 49 | 50 | - **优势体现**: 这个过程**完全在内存中完成**,速度极快,无需任何磁盘I/O。这就是“空闲盘块栈”带来的核心好处。 51 | 52 | 3. **情况B:栈为空(需要与磁盘交互)** 53 | 54 | - 内存栈中的“快速拣货”已经用完。 55 | 56 | - 系统从内存栈的栈底取出之前保存的“下一组头块”的地址。 57 | 58 | - 启动一次磁盘I/O,将这个“头块”的内容(包含`N-1`个新的空闲块号和1个更下一组的头块地址)**一次性地读入**内存,**完全覆盖并填满**“当前可用栈”。 59 | 60 | - 现在内存栈又满了,回到情况A,从新的栈顶弹出一个块号进行分配。 61 | 62 | 63 | #### **3. 回收空闲块时,如何使用内存栈?** 64 | 65 | 当一个文件被删除,其占用的盘块被回收时: 66 | 67 | 1. **检查内存栈**: 系统首先查看“当前可用栈”是否已满。 68 | 69 | 2. **情况A:栈未满(最常见的情况)** 70 | 71 | - 直接对内存栈执行**入栈**操作,将回收的盘块编号压入栈顶。 72 | 73 | - **优势体现**: 同样,这个过程也**完全在内存中完成**,高效迅速。 74 | 75 | 3. **情况B:栈已满(需要与磁盘交互)** 76 | 77 | - 内存栈中的“临时退货区”已经堆满。 78 | 79 | - 系统将当前内存栈中的**全部 `N` 个空闲块编号**,作为一整个新的“分组”,**一次性地写入到刚刚回收的那个盘块中**。 80 | 81 | - 这样,这个被回收的盘块就成了一个新的“分组头块”,它现在记录了另外`N-1`个空闲块的信息以及一个(来自旧栈底的)指向下一组的指针。 82 | 83 | - 最后,清空内存栈,只在其中放入这个新头块的编号,表示现在只有一个“大包裹”可用。 84 | 85 | 86 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 6 |
7 | 8 | 9 |

10 | 项目Logo 11 |

408考研计算机启发性问题集(Demo版)

12 |

专为考研学子打造的408启发性问题笔记库,助你查漏补缺,提升思维深度!

13 |

14 | English | 简体中文 15 |

16 |
17 | 18 | --- 19 | 20 | ## 🧭 目标 21 | 22 | > 本项目旨在收集和整理 408 考研科目中的启发性问题,帮助同学们开阔思路、发现知识盲区、提升理解深度。 23 | > **注意:本项目不是全面复习指南,请结合权威教材和系统课程复习。** 24 | 25 | --- 26 | 27 | ## 📦 目录结构 28 | 29 | - `408/`:各科目启发性问题与知识点笔记 30 | 31 | --- 32 | 33 | ## 🚀 先决条件 34 | 35 | - 推荐使用 [Obsidian](https://obsidian.md/) 或任意 Markdown 编辑器浏览笔记 36 | - 建议配合教材、真题和课程资料一同使用 37 | 38 | --- 39 | 40 | ## 🛠️ 安装与使用 41 | 42 | ```bash 43 | git clone https://github.com/cat0825/rana408.git 44 | cd rana408 45 | # 用 Obsidian 或 Markdown 编辑器打开 webof408 目录 46 | ``` 47 | 48 | --- 49 | 50 | ## ✨ 功能特性 51 | 52 | - 启发性问题归纳,覆盖操作系统、组成原理、数据结构、计算机网络等 53 | - 重点难点、易错点、思维陷阱整理 54 | 55 | --- 56 | 57 | ## 💬 支持与交流 58 | 59 | - [⭐️ GitHub 仓库,欢迎点Star!](https://github.com/cat0825/rana408) 60 | - [💬 加入QQ群【408老兵连队】](https://qm.qq.com/q/2TfrHdTJJm) 61 | - WeChat: yuuri777 62 | 63 | --- 64 | 65 | ## 🧪 测试 66 | 67 | > 本项目为知识笔记库,无需代码测试。内容持续更新中,欢迎反馈建议! 68 | 69 | --- 70 | 71 | ## 🗺️ 路线图 72 | 73 | - [ ] 持续补充 408 各科目高频考点 74 | - [ ] 增加更多思维导图与结构化总结 75 | 76 | --- 77 | 78 | ## 🤝 贡献 79 | 80 | 欢迎提出 Pull Request 或 Issue。对于重大更改,请先开 Issue 讨论。 81 | 如有内容补充、错漏指正,欢迎参与共建! 82 | 83 | --- 84 | 85 | ## 👤 作者与致谢 86 | 87 | - 作者:cat0825 88 | - 感谢所有为 408 考研付出努力的同学们,愿你们都能顺利上岸! 89 | 90 | --- 91 | 92 | ## 📄 许可证 93 | 94 | MIT License 95 | 96 | 版权所有 (c) 2020-至今,cat0825 97 | 98 | --- 99 | 100 | ## 🏷️ 项目状态 101 | 102 | > 当前为 Demo 预览版,内容持续完善中,欢迎关注与参与! 103 | 104 | -------------------------------------------------------------------------------- /src/site/notes/408/住址决定命运:为什么你的变量要被内存访问两次才肯出门?😭.md: -------------------------------------------------------------------------------- 1 | --- 2 | {"dg-publish":true,"dg-permalink":"/408/住址决定命运:为什么你的变量要被内存访问两次才肯出门?😭","permalink":"/408/住址决定命运:为什么你的变量要被内存访问两次才肯出门?😭/"} 3 | --- 4 | 5 | 6 | ### 核心问题解答:为何地址会影响存取周期? 7 | 8 | 主存地址之所以影响存取周期数,是因为现代CPU与内存之间的数据交换并非一个字节一个字节地进行,而是一次取一个固定大小的“数据块”;如果一个变量的存放位置刚好跨越了两个“数据块”的边界,那么即使这个变量本身很小,也必须分两次访问才能把它完整地取出来。 9 | 10 | 这个现象,在计算机体系结构中被称为**数据对齐(Data Alignment)**问题。 11 | 12 | --- 13 | 14 | ### 分析 15 | 16 | 17 | #### 第一步:分析硬件配置 18 | 19 | - **存储器总线宽度是64位**:这意味着CPU与主存之间的数据通路是64位宽,即8个字节(8B)。可以把这条通路想象成一条8车道并排的高速公路,**一次数据传输(一个总线周期),最多可以运送8个字节的数据**。 20 | 21 | - **内存条由8个16M x 8位的DRAM芯片构成16M x 64位的内存**:这是典型的**位扩展**设计。8个8位的芯片并联工作,每个芯片贡献8位,共同组成一个64位的数据字。当CPU访问内存时,这8个芯片会被同时选中,同时工作,一次性提供64位的数据。 22 | 23 | - **支持突发传送方式(Burst Mode)**:这是一种高效的数据传输模式。当CPU需要读取一块连续数据时(比如一个缓存行),它只需给出首地址,内存系统就能自动地、连续地传输后续的数据块,而无需CPU为每个数据块都发出一次读命令。 24 | 25 | 26 | 从以上配置我们可以得出一个至关重要的结论:这个内存系统的**存取粒度**或**访问基本单位**是**64位(8字节)**。它在设计上就是为了高效地进行8字节对齐的访问。内存地址可以被看作是一系列连续的、8字节大小的“格子”。 27 | 28 | #### 第二步:剖析变量x和y的“住址”问题 29 | 30 | 1. **分析变量x (double)** 31 | 32 | - **大小**:`double`类型通常占用**8个字节**。 33 | 34 | - **地址**:`2026 0000H`。 35 | 36 | - **对齐分析**:这个地址的最后一位是`0`,可以被8整除。这意味着变量x的起始地址**恰好是一个8字节数据块的起始地址**。它完整地、不多不少地占据了从`2026 0000H`到`2026 0007H`这一个8字节的“格子”。 37 | 38 | - **存取过程**:CPU只需要发起一次对`2026 0000H`地址的读操作,内存系统就能在一个存取周期内,通过64位总线将这完整的8个字节一次性传送给CPU。 39 | 40 | - **结论**:因此,**读取变量x只需要一个存取周期**。 41 | 42 | 2. **分析变量y (int)** 43 | 44 | - **大小**:`int`类型通常占用**4个字节**。 45 | 46 | - **地址**:`2026 1006H`。 47 | 48 | - **对齐分析**:这个地址不能被8整除,甚至不能被4整除。让我们看看它在8字节的“格子”里是怎么存放的: 49 | 50 | - 我们知道内存的“格子”是从`...0H`和`...8H`开始的。地址`2026 1006H`属于从`2026 1000H`到`2026 1007H`这个格子。 51 | 52 | - 变量y占用的4个字节地址是:`2026 1006H`, `2026 1007H`, `2026 1008H`, `2026 1009H`。 53 | 54 | - **问题出现了!** 它的前2个字节(`...1006H`, `...1007H`)位于`2026 1000H`开始的内存块中,而后2个字节(`...1008H`, `...1009H`)则位于下一个内存块(从`2026 1008H`开始)中。 55 | 56 | - 这种情况,我们称之为**“跨界存储”**或**“非对齐访问”**。 57 | 58 | - **存取过程**:由于变量y的数据被分割在了两个不同的8字节对齐块中,CPU无法通过一次读操作就获取完整的数据。它必须: 59 | 60 | 1. **第一次存取**:发起对`2026 1000H`地址所在块的读取,得到y的前半部分。 61 | 62 | 2. **第二次存取**:发起对`2026 1008H`地址所在块的读取,得到y的后半部分。 63 | 64 | - **结论**:因此,**“读取变量y需要两个存取周期”是正确的**。 65 | 66 | -------------------------------------------------------------------------------- /src/site/notes/408/内存索引节点和磁盘索引节点你俩到底是什么关系.md: -------------------------------------------------------------------------------- 1 | --- 2 | {"dg-publish":true,"dg-permalink":"/408/内存索引节点和磁盘索引节点你俩到底是什么关系","permalink":"/408/内存索引节点和磁盘索引节点你俩到底是什么关系/"} 3 | --- 4 | 5 | 6 | ### **1. 为什么会有“内存中的索引节点”?** 7 | 8 | 首先,我们要明白索引节点(在UNIX/Linux中称为**i-node**,在一些教材中也泛称为**文件控制块FCB**)是存储在**磁盘**上的。它记录了文件的所有属性,如大小、权限、时间戳、以及指向数据块的指针等。 9 | 10 | - **读入内存**: 当一个文件被第一次`open()`时,操作系统会找到磁盘上对应的i-node,并将其**读入到内核的内存**中,形成一个“内存i-node副本”。 11 | 12 | - **性能优化**: 之后,所有对该文件的操作,都是直接针对这个**内存i-node**进行的。因为访问内存的速度比访问磁盘快成千上万倍。如果每次操作都去读写磁盘上的i-node,系统性能将无法忍受。 13 | 14 | 15 | ### **2. 哪些操作会改变“内存i-node”?** 16 | 17 | 只要文件的元数据发生变化,内存i-node就会被修改。例如: 18 | 19 | - **`write()`操作**: 20 | 21 | - 如果写入导致文件变大,内存i-node中的`i_size`(文件大小)字段会被更新。 22 | 23 | - 如果需要分配新的数据块,内存i-node中的数据块指针列表会被更新。 24 | 25 | - `i_mtime`(最后修改时间)会被更新。 26 | 27 | - **`read()`操作**: 28 | 29 | - `i_atime`(最后访问时间)会被更新。 30 | 31 | - **修改权限或所有者 (`chmod`, `chown`)**: 32 | 33 | - 对应的权限位或用户/组ID字段会被更新。 34 | 35 | 36 | 在这些操作发生后,内存中的i-node就已经和磁盘上的原始i-node不一致了。此时,内核会将这个内存i-node标记为**“脏”(Dirty)**,表示它“已被修改,需要写回磁盘”。 37 | 38 | ### **3. 什么时候“内存i-node”会被写回到“磁盘i-node”?** 39 | 40 | 这正是你问题的关键。操作系统不会在每次修改后都立即写回,而是会在以下几种关键时机,将“脏”的内存i-node同步到磁盘: 41 | 42 | 1. **文件正常关闭时 (`close()`)**: 43 | 44 | - 这是最常见和最重要的时机。我们之前讨论过,`close()`操作的一个核心职责就是确保数据和元数据的一致性。在关闭文件时,内核会检查其内存i-node是否为“脏”。如果是,就会将其内容**写回到磁盘上对应的i-node**中。 45 | 46 | 2. **内核定期同步**: 47 | 48 | - 现代操作系统(如Linux)中有名为`sync`、`bdflush`或`kupdated`的后台守护进程。它们会周期性地(例如每隔5秒或30秒)被唤醒,然后将在内存中所有“脏”的缓冲区数据和“脏”的i-node**批量地写回到磁盘**。这保证了即使系统长时间不关机,数据的安全性也能得到阶段性的保障。 49 | 50 | 3. **用户或管理员强制同步**: 51 | 52 | - 在Linux/UNIX系统中,执行`sync`命令会强制内核立即将所有脏数据和脏i-node写回磁盘。这在重要操作后或关机前非常有用。 53 | 54 | 4. **应用程序主动请求同步 (`fsync()`)**: 55 | 56 | - 对于需要极高可靠性的程序(如数据库),它可以调用`fsync()`系统调用。这个调用会强制将**指定文件**的脏数据和脏i-node**立即、同步地**写回磁盘,并等待磁盘操作完成后才返回。这为应用程序提供了更精细的控制。 57 | 58 | 5. **内存不足时**: 59 | 60 | - 当系统内存紧张,需要回收内存页面时,如果一个“脏”的内存i-node所在的内存页被选中换出,系统也会先将其内容写回磁盘。 61 | 62 | 63 | --- 64 | 65 | ### **一个生动的比喻** 66 | 67 | - **磁盘上的i-node**: 你保存在硬盘里的一个Word文档 (`MyReport.docx`)。 68 | 69 | - **内存中的i-node**: 你双击打开`MyReport.docx`后,在Word程序里正在编辑的文档内容。 70 | 71 | - **文件操作**: 你在Word里打字、修改格式。所有的这些修改都**立即**体现在你屏幕上看到的、内存里的这份文档中。但此时,你硬盘上的那个`.docx`文件**还没有发生任何改变**。 72 | 73 | - **写回磁盘**: 你点击**“保存”**按钮(相当于`fsync()`或`close()`),或者Word的自动保存功能启动(相当于内核定期同步)。这时,你在内存中所有的修改才会被一次性地写入到硬盘的`.docx`文件中,覆盖掉旧的版本。 74 | 75 | -------------------------------------------------------------------------------- /src/site/notes/408/分页管理与分段管理下的动态链接.md: -------------------------------------------------------------------------------- 1 | --- 2 | {"dg-publish":true,"dg-permalink":"/408/分页管理与分段管理下的动态链接","permalink":"/408/分页管理与分段管理下的动态链接/"} 3 | --- 4 | 5 | 6 | ### 1. 动态链接与逻辑地址空间 7 | 8 | 首先,我们必须理解动态链接做了什么。 9 | 10 | - **静态链接**: 在**编译链接阶段**,将程序所用到的库函数代码(如`printf`)完完整整地复制到最终生成的可执行文件中。这导致文件体积大,且多个进程运行时,内存中会有多份相同的库代码。 11 | 12 | - **动态链接**: 在编译链接阶段,只在可执行文件中保留一个“桩”(stub)或“符号引用”,记录下“我需要用到某个库的某个函数”。真正的链接过程被推迟到**程序加载时 (Load-time linking)** 或**首次运行时 (Run-time linking)**。 13 | 14 | ### 2. 动态链接在分段存储管理下的实现 15 | 16 | 分段管理与动态链接的结合非常**自然、直观**。 17 | 18 | - **关系**: 在分段系统中,一个进程的逻辑地址空间本身就是由多个**逻辑上独立的段**构成的集合。比如,主程序是一个代码段、全局变量是一个数据段、栈是一个栈段。这种模块化的结构与程序和库的组织方式完美契合。 19 | 20 | - **实现机制**: 21 | 22 | 1. 当加载器需要将一个共享库(如 `libc.so`)映射到进程 P1 的逻辑地址空间时,它可以非常干脆地为这个库**创建一个新的段**。 23 | 24 | 2. 加载器将 `libc.so` 加载到物理内存的某个位置。 25 | 26 | 3. 然后,在进程 P1 的**段表**中,增加一个新表项。假设分配的段号是`S`,表项内容就是 `libc.so` 所在的**物理基地址**和它的**长度**。 27 | 28 | 4. 从此,进程 P1 就可以通过二维的逻辑地址 `(段号S, 段内偏移量)` 来访问 `libc.so` 中的代码和数据了。 29 | 30 | 5. 当另一个进程 P2 也需要使用 `libc.so` 时,加载器**无需**在物理内存中重新加载一份。它只需在 P2 的段表中也增加一个表项(段号可以不同,比如是`S'`),但让这个表项指向**与 P1 相同的物理基地址**。 31 | 32 | 33 | **优点**: 34 | 35 | - **逻辑清晰**: “一个共享库就是一个独立的段”,这个模型非常符合程序员对模块化的认知。 36 | 37 | - **保护与共享方便**: 可以在段表中为共享库段设置独立的保护位(如只读、可执行),实现清晰的权限管理。 38 | 39 | 40 | --- 41 | 42 | ### 3. 动态链接在页式存储管理下的实现 43 | 44 | 虽然分页管理的逻辑地址空间是**一维、线性**的,不像分段那样在结构上“模块化”,但它同样能高效地支持动态链接。 45 | 46 | - **关系**: 在分页系统中,虽然整个逻辑地址空间是线性的,但操作系统依然可以将其中的**一段连续的虚拟地址范围**分配给共享库使用。 47 | 48 | - **实现机制**: 49 | 50 | 1. 当加载器需要将 `libc.so` 映射到进程 P1 的逻辑地址空间时,它首先会在 P1 的**一维线性虚拟地址空间**中,找到一片**尚未使用的、足够大的连续区域**(例如,从虚拟地址 `0xB7000000` 到 `0xB7180000`)。 51 | 52 | 2. 加载器将 `libc.so` 加载到物理内存中,占据若干个**物理页帧**(这些页帧在物理上可以不连续)。 53 | 54 | 3. 然后,在进程 P1 的**页表**中,将步骤1中选定的那段虚拟地址范围所对应的**页表项**,逐一填充,使其指向步骤2中 `libc.so` 所在的那些物理页帧。 55 | 56 | 4. 从此,进程 P1 就可以通过一维的逻辑地址(即虚拟地址,如 `0xB7001234`)来访问 `libc.so` 了。地址翻译机构会自动通过页表找到正确的物理位置。 57 | 58 | 5. 当另一个进程 P2 也需要 `libc.so` 时,加载器同样在 P2 的虚拟地址空间中找一片空闲区域,然后修改 **P2 的页表**,让相应的页表项指向**与 P1 相同的那些物理页帧**。 59 | 60 | 61 | **总结与对比**: 62 | 63 | |特性|分段管理下的动态链接|分页管理下的动态链接| 64 | |---|---|---| 65 | |**逻辑视图**|将共享库视为一个**新的、独立的段**,加入到进程的段集合中。|在进程**线性的虚拟地址空间**中,找一片**连续的空闲区域**来映射共享库。| 66 | |**映射单位**|**段**。整个库作为一个或多个段进行映射。|**页**。库所占的虚拟地址范围被逐页映射到物理页帧。| 67 | |**实现核心**|修改进程的**段表 (Segment Table)**,增加新的段描述符。|修改进程的**页表 (Page Table)**,填充虚拟页到物理帧的映射关系。| 68 | |**共享方式**|不同进程的段表项指向**同一个物理段基地址**。|不同进程的页表项指向**同一组物理页帧**。| 69 | -------------------------------------------------------------------------------- /src/site/_includes/components/pageheader.njk: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | {%include "components/calloutScript.njk"%} 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | {%-if meta.themeStyle%} 20 | 21 | 22 | {% else %} 23 | 24 | {%endif%} 25 | 26 | 27 | {%- for style in dynamics.styles -%} 28 | 29 | {%- endfor -%} 30 | 31 | {% favicons './src/site/favicon.svg', appleIconBgColor='#123' %} 32 | 33 | {% if metatags %} 34 | {% for name, content in metatags %} 35 | 36 | {% endfor %} 37 | {% endif %} 38 | 39 | {% if meta.styleSettingsCss %} 40 | 43 | {% endif %} 44 | -------------------------------------------------------------------------------- /src/site/notes/408/死锁的四个必要条件.md: -------------------------------------------------------------------------------- 1 | --- 2 | {"dg-publish":true,"dg-permalink":"/408/死锁的四个必要条件","permalink":"/408/死锁的四个必要条件/"} 3 | --- 4 | 5 | 6 | ### 1. 死锁产生的四个必要条件 7 | 8 | 这四个条件是考研的必考点,要求能准确默写、理解,并能结合实例进行分析。 9 | 10 | #### 1.1 条件定义与阐述 11 | 12 | 1. **互斥条件 (Mutual Exclusion)** 13 | 14 | - **含义**: 进程所申请的资源是独占的、排他性的,即一个资源在任意时刻只能被一个进程所使用。 15 | 16 | - **解释**: 这个条件是很多资源的固有属性,比如打印机、物理内存区域等。如果资源本身就是可共享的(如只读文件),那么该资源本身就不会引发死锁。这个条件通常是无法破坏的。 17 | 18 | 2. **请求与保持条件 (Hold and Wait)** 19 | 20 | - **含义**: 进程在已经保持了**至少一个**资源的情况下,又提出了新的资源请求,但该资源已被其他进程占用,此时请求进程被阻塞,但它在等待新资源时**并不会释放**自己已经保持的资源。 21 | 22 | - **解释**: 这是资源分配策略上的问题。“拿着碗里的,还看着锅里的”。 23 | 24 | 3. **不可剥夺条件 (No Preemption)** 25 | 26 | - **含义**: 进程已获得的资源,在未使用完毕之前,不能被其他进程强行剥夺,只能由获得该资源的进程自己主动释放。 27 | 28 | - **解释**: 这保证了进程对已分配资源的控制权,但同时也增加了死锁的风险。 29 | 30 | 4. **循环等待条件 (Circular Wait)** 31 | 32 | - **含义**: 存在一个进程-资源的循环等待链,即存在一个进程集合 `{P_0, P_1, ..., P_n}`,其中 P_0 正在等待 P_1 所占用的资源,P_1 正在等待 P_2 所占用的资源,...,P_n 正在等待 P_0 所占用的资源。 33 | 34 | - **解释**: 这是死锁状态的直接体现,形成了一个“你等我、我等你”的闭环。可以用“资源分配图”清晰地展示出来,如果图中出现了环路,则**可能**存在死锁(如果每类资源只有一个实例,则必为死锁)。 35 | 36 | 37 | #### 1.2 实例说明 38 | 39 | 我们用一个非常经典的例子来说明这四个条件是如何同时出现的。 40 | 41 | **场景**: 系统中有两个进程 `P1`、`P2`,以及两个互斥资源 `R1`(打印机)、`R2`(扫描仪)。 42 | 43 | **执行时序**: 44 | 45 | 1. **时刻 t1**: `P1` 成功申请到资源 `R1`(打印机)。 46 | 47 | 2. **时刻 t2**: `P2` 成功申请到资源 `R2`(扫描仪)。 48 | 49 | 3. **时刻 t3**: `P1` 在保持 `R1` 的同时,又去申请 `R2`。但 `R2` 已被 `P2` 占用,因此 `P1` 进入阻塞等待状态。 50 | 51 | 4. **时刻 t4**: `P2` 在保持 `R2` 的同时,又去申请 `R1`。但 `R1` 已被 `P1` 占用,因此 `P2` 也进入阻塞等待状态。 52 | 53 | 54 | **此刻,系统进入死锁状态。我们来分析四个条件是否满足**: 55 | 56 | - **满足互斥条件**: 打印机 `R1` 和扫描仪 `R2` 都是独占设备,一次只能给一个进程使用。 57 | 58 | - **满足请求与保持条件**: `P1` 保持着 `R1` 去请求 `R2`;`P2` 保持着 `R2` 去请求 `R1`。 59 | 60 | - **满足不可剥夺条件**: 系统不能强行将 `R1` 从 `P1` 手中夺走分配给 `P2`,也不能将 `R2` 从 `P2` 手中夺走分配给 `P1`。 61 | 62 | - **满足循环等待条件**: `P1` 等待 `P2` 释放 `R2`,而 `P2` 同时在等待 `P1` 释放 `R1`,形成了 `P1 -> R2 -> P2 -> R1 -> P1` 的循环等待链。 63 | 64 | 65 | 由于四个条件同时满足,死锁发生。两个进程都将永远等待下去,无法推进。 66 | 67 | ### 2. 信号量与死锁的关系 68 | 69 | 这是一个非常经典的考点,也是一个极易产生误解的地方。 70 | 71 | **明确结论**: **使用信号量机制并不能保证不出现死锁。恰恰相反,对信号量的不当使用是导致死锁的常见原因之一。** 72 | 73 | 74 | 1. 生产者接着执行 `P(empty)`。由于 `empty.value` 为0,生产者进程**阻塞**。**关键点:此时它因为阻塞,无法执行后面的 `V(mutex)`,所以它仍然“保持”着 `mutex` 信号量。** 75 | 76 | 2. 此时轮到消费者执行,它想要消费一个产品。 77 | 78 | 3. 消费者执行 `P(full)`,成功(因为缓冲区是满的),`full.value` 减1。 79 | 80 | 4. 消费者接着执行 `P(mutex)`,试图进入临界区取出产品。但由于 `mutex.value` 已被生产者置为0,所以消费者进程**阻塞**。 81 | -------------------------------------------------------------------------------- /src/site/_data/meta.js: -------------------------------------------------------------------------------- 1 | require("dotenv").config(); 2 | const { globSync } = require("glob"); 3 | 4 | module.exports = async (data) => { 5 | let baseUrl = process.env.SITE_BASE_URL || ""; 6 | if (baseUrl && !baseUrl.startsWith("http")) { 7 | baseUrl = "https://" + baseUrl; 8 | } 9 | let themeStyle = globSync("src/site/styles/_theme.*.css")[0] || ""; 10 | if (themeStyle) { 11 | themeStyle = themeStyle.split("site")[1]; 12 | } 13 | let bodyClasses = []; 14 | let noteIconsSettings = { 15 | filetree: false, 16 | links: false, 17 | title: false, 18 | default: process.env.NOTE_ICON_DEFAULT, 19 | }; 20 | 21 | const styleSettingsCss = process.env.STYLE_SETTINGS_CSS || ""; 22 | const styleSettingsBodyClasses = process.env.STYLE_SETTINGS_BODY_CLASSES || ""; 23 | 24 | if (process.env.NOTE_ICON_TITLE && process.env.NOTE_ICON_TITLE == "true") { 25 | bodyClasses.push("title-note-icon"); 26 | noteIconsSettings.title = true; 27 | } 28 | if ( 29 | process.env.NOTE_ICON_FILETREE && 30 | process.env.NOTE_ICON_FILETREE == "true" 31 | ) { 32 | bodyClasses.push("filetree-note-icon"); 33 | noteIconsSettings.filetree = true; 34 | } 35 | if ( 36 | process.env.NOTE_ICON_INTERNAL_LINKS && 37 | process.env.NOTE_ICON_INTERNAL_LINKS == "true" 38 | ) { 39 | bodyClasses.push("links-note-icon"); 40 | noteIconsSettings.links = true; 41 | } 42 | if ( 43 | process.env.NOTE_ICON_BACK_LINKS && 44 | process.env.NOTE_ICON_BACK_LINKS == "true" 45 | ) { 46 | bodyClasses.push("backlinks-note-icon"); 47 | noteIconsSettings.backlinks = true; 48 | } 49 | if (styleSettingsCss) { 50 | bodyClasses.push("css-settings-manager"); 51 | } 52 | if (styleSettingsBodyClasses) { 53 | bodyClasses.push(styleSettingsBodyClasses); 54 | } 55 | 56 | let timestampSettings = { 57 | timestampFormat: process.env.TIMESTAMP_FORMAT || "MMM dd, yyyy h:mm a", 58 | showCreated: process.env.SHOW_CREATED_TIMESTAMP == "true", 59 | showUpdated: process.env.SHOW_UPDATED_TIMESTAMP == "true", 60 | }; 61 | const meta = { 62 | env: process.env.ELEVENTY_ENV, 63 | theme: process.env.THEME, 64 | themeStyle, 65 | bodyClasses: bodyClasses.join(" "), 66 | noteIconsSettings, 67 | timestampSettings, 68 | baseTheme: process.env.BASE_THEME || "dark", 69 | siteName: process.env.SITE_NAME_HEADER || "Digital Garden", 70 | mainLanguage: process.env.SITE_MAIN_LANGUAGE || "en", 71 | siteBaseUrl: baseUrl, 72 | styleSettingsCss, 73 | buildDate: new Date(), 74 | }; 75 | 76 | return meta; 77 | }; 78 | -------------------------------------------------------------------------------- /src/site/notes/408/一条指令的诞生和解剖从如何设计到CPU解码🤔.md: -------------------------------------------------------------------------------- 1 | --- 2 | {"dg-publish":true,"dg-permalink":"/408/一条指令的诞生和解剖从如何设计到CPU解码🤔","permalink":"/408/一条指令的诞生和解剖从如何设计到CPU解码🤔/"} 3 | --- 4 | 5 | 6 | ### 一条指令的诞生与“宿命” 7 | --- 8 | 9 | ### 第一部分:指令的“诞生” 10 | 11 | #### 1. 顶层:“鸡生蛋还是蛋生鸡”的设计 12 | 13 | 在设计一套全新的指令集时,设计师首先面临一个类似“鸡生蛋还是蛋生鸡”的哲学抉择,这个抉择决定了指令长度的基本形态: 14 | 15 | - **RISC哲学(先定“饭碗”大小)**:以硬件执行效率为最高优先级。设计师会先定下一个**固定的、规整的指令长度**(例如,所有指令均为32位)。这个“饭碗”的大小是首要约束。然后,设计师在这个固定的空间内,去“精打细算”地分配给操作码、寄存器编号等字段,看最多能塞下多少种指令。**在这里,是指令字长和格式,反过来限制了指令的数量。** 16 | 17 | - **CISC哲学(先看“要吃多少菜”)**:以软件编程的便利性和代码密度为优先。设计师会先列出所有希望实现的强大功能,为每一条复杂指令“量身定做”最合适的格式。这导致了指令的长度根据其功能而**变化**。**在这里,是指令的数量和功能需求,共同决定了每一条指令各自的(可变的)长度。** 18 | 19 | 20 | #### 2. 具体因素:构成指令“胖瘦”的“零部件” 21 | 22 | 无论采用何种哲学,一条指令的长度都是其内部各个“零部件”长度的总和。 23 | 24 | - **操作码(Opcode)**:告诉CPU“做什么”。指令系统的**指令总数**决定了操作码的最小长度。指令越多,操作码字段就需要越长。 25 | 26 | - **操作数(Operand)**:告诉CPU“对谁做”。这是影响长度的最主要因素。 27 | 28 | - **操作数个数**:三地址、二地址、一地址、零地址指令所包含的地址字段数量不同,直接影响总长度。 29 | 30 | - **寻址方式**:获取操作数的方法极大地影响着地址字段的长度。 31 | 32 | - **寄存器寻址**:字段最短,只需几位来编码寄存器号。 33 | 34 | - **直接寻址**:字段最长,需要包含一个完整的主存地址(如32位或64位)。 35 | 36 | - **立即数寻址**:指令中直接包含了数据,数据多长,指令就相应地加长。 37 | 38 | ### 第二部分:指令的“解码” 39 | 40 | CPU(特别是处理变长指令的CISC CPU)是如何“测量”出一条指令的长度的。 41 | 42 | CPU事先并不知道指令有多长,它依赖于硬件**指令译码器(Instruction Decoder)**进行“走一步,看一步”的串行解码。 43 | 44 | 1. **取指与缓冲**:CPU从内存中一次性抓取一块数据(包含多条指令)到内部的指令缓冲区。 45 | 46 | 2. **前缀解析**:译码器从缓冲区的当前指针位置开始,逐字节检查是否存在可选的前缀(如x86的`66H`, `REX`等),每识别一个前缀,指针就后移一字节。 47 | 48 | 3. **操作码解码(最关键一步)**:译码器读取到第一个非前缀字节,即**操作码**。根据这个操作码的值,译码器通过内部的硬连线逻辑(像查字典一样)瞬间得知该指令的“模板”——它后面是否需要ModR/M字节?需要多长的立即数? 49 | 50 | 4. **后续字段解析**:根据操作码提供的“情报”,译码器继续向后读取并解析ModR/M、SIB、地址偏移量、立即数等后续字节。每解析一部分,它就更清楚指令还剩下多少未读部分。 51 | 52 | 5. **确定边界**:当一条指令所需的所有部分都被“吃掉”后,译码器就知道了这条指令的总长度。当前指针指向的位置,就是下一条指令的开始。这个“拆盲盒”的过程至此完成一轮。 53 | 54 | 55 | --- 56 | 57 | ### 第三部分:执行中的安全与保障 58 | 59 | - **“如果PC指针指错了,指到了一个数据(机器数)区怎么办?”** 60 | 61 | - **正常情况**:这不会发生。**程序计数器(PC)**是CPU执行流程的“神圣向导”,它被编译器、加载器和操作系统设置为永远指向代码段。数据和指令虽然共用内存,但在正常的程序流程中“井水不犯河水”。 62 | 63 | - **异常情况**:如果因程序BUG(如缓冲区溢出)导致PC被篡改,指向了数据区。CPU会“盲目信任”PC,将数据当成指令进行解码。这通常会因为数据组成不了合法指令而触发**“非法指令”硬件异常**,导致程序被操作系统强制终止,这是一种**硬件级的保护机制**。 64 | 65 | - **“可以通过PC的递增数量来判断指令长度吗?”** 66 | 67 | - **不能,因果关系正好相反**。正确的顺序是: 68 | 69 | 1. PC指向指令的**起始地址** `P_start`。 70 | 71 | 2. **指令译码器**工作,确定了该指令的**长度 `L`**。 72 | 73 | 3. PC根据这个已知的长度 `L` **进行更新**:`PC = P_start + L`。 74 | 75 | - 所以,是**译码器确定的指令长度,决定了PC应该增加多少**,而不是反过来。 76 | -------------------------------------------------------------------------------- /src/site/notes/408/IO数据进入内存前,竟要在CPU的寄存器里先住一晚🥵.md: -------------------------------------------------------------------------------- 1 | --- 2 | {"dg-publish":true,"dg-permalink":"/408/IO数据进入内存前,竟要在CPU的寄存器里先住一晚🥵","permalink":"/408/IO数据进入内存前,竟要在CPU的寄存器里先住一晚🥵/"} 3 | --- 4 | 5 | 6 | ### 二、 I/O控制方式与数据通路详解 7 | #### 1. 程序查询方式 (Programmed I/O) 8 | 9 | 这是最原始的I/O方式。 10 | 11 | - **过程**:CPU执行I/O指令后,会反复地“轮询”I/O设备的状态寄存器,看其是否准备就绪。一旦就绪,CPU再执行`IN`(输入)或`OUT`(输出)指令,以**CPU内部的累加器(Accumulator)或通用寄存器**为中介,一次传送一个字(Word)的数据。 12 | 13 | - **数据通路**: 14 | 15 | - **输入**:I/O设备 → I/O端口数据寄存器 → **CPU累加器/GPR** → 主存 16 | 17 | - **输出**:主存 → **CPU累加器/GPR** → I/O端口数据寄存器 → I/O设 18 | 19 | #### 2. 中断驱动方式 (Interrupt-Driven I/O) 20 | 21 | 22 | - **过程**:CPU发出I/O指令后,不再等待,而是继续执行其他程序。当I/O设备准备好数据后,会主动向CPU发送一个**中断请求**。CPU响应中断后,暂停当前任务,转而执行一段中断服务程序,在服务程序中完成一次数据的读写。 23 | 24 | - **数据通路**:**与程序查询方式完全相同!** 数据传送的动作本身,仍然需要CPU执行指令,通过**累加器/GPR**来完成。 25 | 26 | - **输入**:I/O设备 → I/O端口 → **CPU累加器/GPR** → 主存 27 | 28 | - **输出**:主存 → **CPU累加器/GPR** → I/O端口 → I/O设备 29 | 30 | 31 | #### 3. 直接存储器存取方式 (Direct Memory Access, DMA) 32 | 33 | - **过程**:CPU只需向**DMA控制器(DMAC)**下达指令,告诉它“你要把哪个I/O设备的数据,传送到主存的哪个地址,传送多少个字节”。设置完成后,CPU就可以“撒手不管”了。整个数据的传输过程由DMAC全权接管,在DMAC的控制下,数据直接在I/O设备和主存之间传送。传送结束后,DMAC再通过一个中断信号通知CPU“任务已完成”。 34 | 35 | - **数据通路**: 36 | 37 | - **输入/输出**:I/O设备 ↔ **主存** 38 | #### 图示:中断方式 vs DMA方式的数据通路 39 | 40 | ``` 41 | +-------+ 数据 +-----+ 数据 +------+ 42 | | I/O | <--------------> | CPU | <------------> | 主存 | 43 | +-------+ (寄存器中转) +-----+ +------+ 44 | | ^ 45 | |__中断请求/控制________| 46 | 图1:中断驱动方式的数据通路 (数据经过CPU) 47 | 48 | 49 | +-------+ +------+ 50 | | I/O | <------------ 数据直接传送 -------------> | 主存 | 51 | +-------+ +------+ 52 | | ^ 53 | | +---------+ | 54 | |__ DMA请求/控制 ____| DMAC |____ 总线控制 ____| 55 | +---------+ 56 | ^ 57 | |__ CPU发出指令/接收中断 __ 58 | | 59 | +-----+ 60 | | CPU | 61 | +-----+ 62 | 图2:DMA方式的数据通路 (数据不经过CPU) 63 | ``` 64 | 65 | --- 66 | 67 | ### 四、 总结与边界情况 68 | 69 | |I/O控制方式|数据是否经过CPU寄存器|CPU介入程度(数据传送阶段)|硬件复杂度|适用场景| 70 | |---|---|---|---|---| 71 | |**程序查询**|**是**|完全占用CPU|简单|古老、极低速设备| 72 | |**中断驱动**|**是**|每次传送一个字时介入|较简单|中低速设备| 73 | |**DMA**|**否**|**完全不介入**|复杂 (需DMAC)|高速设备 (硬盘、网卡)| 74 | -------------------------------------------------------------------------------- /src/site/notes/408/通道技术和DMA的区别解释.md: -------------------------------------------------------------------------------- 1 | --- 2 | {"dg-publish":true,"dg-permalink":"/408/通道技术和DMA的区别解释","permalink":"/408/通道技术和DMA的区别解释/"} 3 | --- 4 | 5 | 6 | ### 一、核心差异:**干预减少的程度与方式** 7 | 8 | #### 1. **通道技术**(Channel Technology) 9 | - **减少干预的方式**: 10 | - 通过**独立的硬件处理器**(通道)执行完整的I/O操作,CPU仅需启动通道程序,后续操作由通道全权负责。 11 | - 通道程序(Channel Program)包含多个I/O指令(如读写、跳转),可实现**多步骤、多设备协同的复杂数据流**。例如,从磁带读取数据→内存缓冲→打印机输出的链式流程 。 12 | - **干预减少的程度**: 13 | - CPU仅在启动I/O时介入一次,后续操作完全由通道接管,甚至支持**通道链式传输**(Chaining),进一步减少CPU干预 。 14 | - **适用场景**: 15 | - 大型机、专用系统的复杂I/O控制(如IBM System/360通道)。 16 | 17 | 18 | #### 2. **DMA技术**(Direct Memory Access) 19 | - **减少干预的方式**: 20 | - 通过**硬件控制器**(DMA控制器)直接管理设备与内存之间的数据传输,CPU仅需配置DMA参数(源地址、目标地址、数据长度)。 21 | - 数据传输完全由DMA控制器完成,无需CPU参与搬运过程 。 22 | - **干预减少的程度**: 23 | - CPU仅在传输开始前配置参数,传输完成后通过中断通知CPU,中间过程完全由硬件处理。例如,网卡通过DMA将数据包直接写入内存环形缓冲区 。 24 | - **适用场景**: 25 | - 通用系统的高速数据传输(如硬盘、网卡、GPU显存交换)。 26 | 27 | --- 28 | 29 | 30 | 31 | ### 二、关键区别总结 32 | | 维度 | DMA技术 | 通道技术 | 33 | | ----------- | ------------------ | ------------------ | 34 | | **核心机制** | 寄存器配置(硬件自动传输) | 程序控制(执行通道程序) | 35 | | **CPU干预程度** | 配置参数后零干预 | 启动通道程序后零干预 | 36 | | **数据流复杂度** | 点对点传输(设备↔内存) | 支持多步骤、多设备协同(如链式传输) | 37 | | **硬件复杂度** | 中(仅需DMA控制器) | 高(需专用处理器与指令集) | 38 | | **历史背景** | 普及于1980s后(PC与通用系统) | 大型机时代(1950s-1960s) | 39 | | | | | 40 | 41 | --- 42 | 43 | 44 | 45 | ### 三、为何需要两种技术? 46 | 47 | #### 1. **通道技术的不可替代性** 48 | - **复杂流程控制需求**: 49 | 在需要**多步骤I/O协同**的场景(如大型机磁带机多段传输),通道技术通过程序控制实现精细的数据流管理,而DMA仅能完成单一数据块传输 。 50 | - **虚拟化与隔离需求**: 51 | 现代虚拟通道技术(如Brocade的Virtual Channel)通过动态隔离数据流,确保关键业务流量的带宽和低延迟,这是DMA无法提供的功能 。 52 | 53 | 54 | #### 2. **DMA技术的普适性优势** 55 | - **简化硬件与软件模型**: 56 | DMA仅需配置寄存器即可启动传输,无需编写复杂通道程序,降低了驱动开发难度。例如,PCIe设备通过BAR空间配置DMA参数,无需操作系统理解设备内部逻辑 。 57 | - **适应高速设备需求**: 58 | 现代存储设备(如NVMe SSD)和网络设备(如千兆网卡)依赖DMA的突发传输(Burst Mode)模式,充分利用总线带宽,避免CPU成为瓶颈 。 59 | 60 | --- 61 | 62 | 63 | 64 | ### 四、边界条件与误用风险 65 | 1. **通道技术的局限性**: 66 | - **硬件绑定性强**:通道程序与硬件指令集紧密耦合,移植性差(如IBM通道程序无法在x86系统运行)。 67 | - **编程复杂度高**:错误的通道指令可能导致硬件死锁(如CCW链表循环)。 68 | 69 | 2. **DMA技术的潜在问题**: 70 | - **缓存一致性风险**:DMA写入内存可能绕过CPU缓存,需显式刷新(如`dma_wmb()`)保证一致性 。 71 | - **安全漏洞**:恶意设备可通过DMA访问任意内存区域(如Thunderbolt攻击),需IOMMU防护 。 72 | 73 | --- 74 | 75 | 76 | 77 | ### 五、结论:两种技术的互补性 78 | - **通道技术**: 79 | 以**流程控制为核心**,适用于需要精细管理I/O流程的专用系统(如大型机),注重数据流的逻辑路径规划与多设备协同。 80 | - **DMA技术**: 81 | 以**传输效率为核心**,适用于通用系统的高速设备数据传输,注重数据流的物理搬运速度与CPU卸载。 82 | 83 | 两者均旨在解除CPU对I/O的直接控制,但**通道技术偏向“流程驱动”**,而**DMA技术偏向“性能驱动”**。开发者需根据应用场景选择合适技术,并严格遵循边界条件(如DMA缓存一致性、通道程序安全性)以避免系统崩溃 。 84 | -------------------------------------------------------------------------------- /src/site/notes/408/read()操作的工作流程.md: -------------------------------------------------------------------------------- 1 | --- 2 | {"dg-publish":true,"dg-permalink":"/408/read()操作的工作流程","permalink":"/408/read()操作的工作流程/"} 3 | --- 4 | 5 | 6 | ### **读文件操作 (`read`) 的完整工作流程** 7 | 8 | 假设一个用户程序已经通过`open()`函数成功打开了一个文件,并获得了对应的文件描述符 `fd`。现在,程序调用 `read(fd, buf, n)`,意图从文件中读取 `n` 个字节到用户指定的缓冲区 `buf` 中。 9 | 10 | #### **第一步:用户程序发起调用** 11 | 12 | - 用户进程在**用户态**下,调用C库函数 `read()`。这个库函数会打包好参数(文件描述符`fd`、目标缓冲区地址`buf`、要读取的字节数`n`),准备请求操作系统服务。 13 | 14 | 15 | #### **第二步:系统调用与上下文切换** 16 | 17 | - 库函数通过一条特殊的**陷入指令 (trap)**,将CPU的执行状态从**用户态**切换到**内核态**。 18 | 19 | - 操作系统的**系统调用处理程序**接管控制权,根据传入的系统调用号,找到并开始执行内核中对应的 `sys_read()` 函数。 20 | 21 | 22 | #### **第三步:内核中定位文件和读取位置** 23 | 24 | - 内核首先需要知道“读哪里”。它会执行以下查找: 25 | 26 | 1. 使用 `fd` 作为索引,在当前进程的**“进程级打开文件表”**中找到对应的表项。 27 | 28 | 2. 通过该表项中的指针,找到在**“系统级打开文件表”**中的对应表项。 29 | 30 | 3. 这个系统级表项非常关键,它包含了文件的**当前读写位置(文件偏移量 a.k.a. file offset or pointer)**。同时,它还包含一个指向该文件在内存中的**i-node(或v-node)**的指针。 31 | 32 | 33 | #### **第四步:检查内核I/O缓冲区(页高速缓存)—— 关键性能点** 34 | 35 | - 这是整个流程的核心决策点。操作系统**不会**立即去访问磁盘。 36 | 37 | - 操作系统会根据上一步获取的**“文件偏移量”**和要读取的字节数 `n`,计算出需要读取的数据位于文件的哪一个或哪几个**逻辑块 (logical block)** 中。 38 | 39 | - 然后,它会去检查内核的 **I/O缓冲区(也称页高速缓存,Page Cache)**,看看这些逻辑块是否因为之前的读写操作而**已经存在于内存中**。 40 | 41 | - **情况A:缓存命中 (Cache Hit)** 42 | 43 | - 如果所有需要的数据块都已经在内核缓冲区中,这是**最理想**的情况。 44 | 45 | - 系统将**直接跳过第五步**(物理I/O),避免了与慢速磁盘的任何交互。 46 | 47 | - **情况B:缓存未命中 (Cache Miss)** 48 | 49 | - 如果需要的数据块(或其中一部分)不在缓冲区中,那么操作系统**必须**启动一次物理I/O操作,从磁盘上将数据加载进来。 50 | 51 | 52 | #### **第五步:启动物理I/O(仅在缓存未命中时发生)** 53 | 54 | - **地址转换**: 操作系统通过查询内存中的文件i-node,将文件的**逻辑块号**转换为实际的**物理磁盘块地址**。 55 | 56 | - **发出I/O请求**: 内核向**磁盘驱动程序**发出一个读请求,指明要从哪个磁盘的哪个物理地址开始,读取多少个数据块。 57 | 58 | - **DMA传输**: 现代操作系统普遍使用**DMA (Direct Memory Access)** 方式进行数据传输。 59 | 60 | 1. CPU向DMA控制器下达指令,告诉它“把磁盘的X地址的数据,传送到内存的Y地址(即内核I/O缓冲区的地址)”。 61 | 62 | 2. 之后,CPU就可以**被释放**去执行其他进程,无需再关心这个耗时的数据传输过程。 63 | 64 | 3. DMA控制器会全权负责将数据从磁盘搬运到内存的内核缓冲区。 65 | 66 | - **I/O完成中断**: 当DMA传输完成后,磁盘控制器会向CPU发送一个**中断信号**,通知操作系统“数据已经准备好了”。 67 | 68 | 69 | #### **第六步:数据从内核缓冲区复制到用户缓冲区** 70 | 71 | - 无论是缓存命中直接得到数据,还是缓存未命中后通过DMA从磁盘加载了数据,此时,要读取的数据都**已经位于内核的I/O缓冲区中**。 72 | 73 | - 内核将从内核缓冲区中,根据用户请求的字节数 `n`,将数据**复制**到用户程序在第一步调用时指定的 `buf` 缓冲区中。 74 | 75 | 76 | #### **第七步:更新状态并返回** 77 | 78 | - **更新文件偏移量**: 内核会更新系统级打开文件表中的**文件偏移量**,将其增加实际读取到的字节数,以便下一次`read`调用能从正确的位置开始。 79 | 80 | - **更新访问时间**: 可能会更新i-node中的“最后访问时间”。 81 | 82 | - **返回用户态**: 内核完成所有工作后,执行**上下文切换**,将CPU控制权交还给用户进程,并从内核态切换回用户态。 83 | 84 | - `read()`函数返回,其返回值是**实际读取到的字节数**(可能小于请求的`n`,例如读到了文件末尾)。 85 | 86 | -------------------------------------------------------------------------------- /src/site/notes/408/close()与write()操作的思考🤔.md: -------------------------------------------------------------------------------- 1 | --- 2 | {"dg-publish":true,"dg-permalink":"/408/close()与write()操作的思考🤔","permalink":"/408/close()与write()操作的思考🤔/"} 3 | --- 4 | 5 | 6 | ### 1. 写文件操作 (`write`):高效但“懒惰”的数据搬运工 7 | 8 | 当我们调用`write()`函数向一个文件写入数据时,为了**极大地提高I/O效率**,操作系统通常不会立即把数据写入到慢速的磁盘上。磁盘I/O是一项非常耗时的操作(涉及寻道、旋转等机械动作)。如果每次`write`一两个字节就启动一次磁盘操作,系统性能将不堪设想。 9 | 10 | 因此,`write()`的典型流程是: 11 | 12 | 1. **数据从用户空间到内核空间**:程序调用`write()`时,数据从用户程序的缓冲区被复制到操作系统内核开辟的**I/O缓冲区(或称为页高速缓存 Page Cache)**中。 13 | 14 | 2. **快速返回**:一旦数据被复制到内核缓冲区,`write()`系统调用就可以**立即返回**,告诉用户程序“写入成功”。 15 | 16 | 3. **延迟写入 (Delayed Write)**:此时,数据只是存在于内存的内核缓冲区中,并没有被真正写入磁盘。内核会“攒”着这些数据,等到缓冲区满了、或者系统空闲时、或者定时任务触发时,再把整个缓冲区的数据一次性地、以一个或多个磁盘块为单位,高效地写入磁盘。 17 | 18 | 19 | **`write()`操作的核心特征**: 20 | 21 | - **异步性**: 对用户程序来说,`write()`的返回并不意味着数据已经安全地落在了磁盘上。 22 | 23 | - **效率优先**: 它的主要目的是快速地将数据从用户进程“接管”过来,然后通过缓冲和批量写入的机制来优化性能。 24 | 25 | - **不保证最终一致性**: 如果在`write()`调用后、数据被刷盘前,系统突然断电,那么这部分写入的数据将会**丢失**。 26 | 27 | 28 | --- 29 | 30 | ### 2. 关闭文件操作 (`close`):确保善始善终的“收尾总管” 31 | 32 | `close()`操作标志着一个进程对该文件访问的结束。因此,它必须承担起“善后”的全部责任,确保文件在磁盘上的状态是完整和一致的。 33 | 34 | `close()`的典型流程是一个严谨的三部曲: 35 | 36 | 1. **第一步(最重要):刷新内核数据缓冲区 (Flush Dirty Buffers)** 37 | 38 | - 操作系统会检查与该文件相关联的内核I/O缓冲区中,是否存在“脏数据”(即被修改过但尚未写入磁盘的数据)。 39 | 40 | - 如果存在,操作系统会**强制启动磁盘I/O**,将所有这些脏数据块**全部写回**到磁盘上。这个过程是**同步**的,`close()`会等待这个写操作完成后再继续。 41 | 42 | - **这步操作保证了所有之前`write()`的数据被永久化存储。** 43 | 44 | 2. **第二步:写回文件的控制信息(元数据 Metadata)** 45 | 46 | - 在确保了文件**内容**的完整性之后,操作系统会更新并写回文件的**“属性”**。这些控制信息存储在磁盘上的**文件控制块(FCB)**或**i-node**中。 47 | 48 | - 需要更新的典型信息包括: 49 | 50 | - **文件大小**: 经过多次写入,文件的尺寸可能已经改变。 51 | 52 | - **最后修改时间**: 记录文件内容最后一次被改变的时间戳。 53 | 54 | - **磁盘块指针**: 如果文件变大,可能需要分配新的数据块,这些新块的地址需要记录到i-node的指针列表中。 55 | 56 | - **为什么在此时写回?** 因为只有在所有数据都写入完毕后,文件的最终大小和状态才被完全确定。在每次`write`后都更新元数据会带来巨大的、不必要的I/O开销。 57 | 58 | 3. **第三步:释放内存中的控制结构** 59 | 60 | - 在确保磁盘上的数据和元数据都一致后,操作系统会释放为这次文件打开而分配的内存资源。 61 | 62 | - **在进程的“打开文件表”中,删除对应的表项**,从而释放该进程的文件描述符(fd)。 63 | 64 | - **在系统的“打开文件表”中,将对应表项的引用计数减1**。如果引用计数变为0(表示系统中已没有任何进程打开该文件),则释放这个系统级的表项。 65 | 66 | 67 | --- 68 | 69 | ### **总结与辨析表格** 70 | 71 | |特性|写文件操作 (`write`)|关闭文件操作 (`close`)| 72 | |---|---|---| 73 | |**核心任务**|将数据从用户空间**传递**到内核缓冲区|**最终化**文件在磁盘上的状态,并**释放**内存资源| 74 | |**数据同步性**|**通常是异步的**。调用返回不代表数据已落盘|**同步的**。会强制将缓冲数据刷到磁盘| 75 | |**操作对象**|主要操作对象是**文件内容数据**|操作对象包括**数据**、**元数据**和**内核控制结构**| 76 | |**对控制信息**|可能只更新内存中的副本(如当前文件指针)|**将最终的控制信息(大小、时间等)写回磁盘**| 77 | |**I/O时机**|不一定会立即触发磁盘I/O|**会触发**必要的磁盘I/O以确保数据一致性| 78 | |**资源管理**|占用和使用内核资源(如文件表项、缓冲区)|**释放**内核为此次文件打开所占用的资源| 79 | -------------------------------------------------------------------------------- /src/site/notes/408/快递姬802.3、802.11和802.1Q三姐妹的underwear到底有啥不一样?🤔.md: -------------------------------------------------------------------------------- 1 | --- 2 | {"dg-publish":true,"dg-permalink":"/408/快递姬802.3、802.11和802.1Q三姐妹的underwear到底有啥不一样?🤔","permalink":"/408/快递姬802.3、802.11和802.1Q三姐妹的underwear到底有啥不一样?🤔/"} 3 | --- 4 | 5 | 6 | ### 一、经典款:IEEE 802.3 以太网帧 7 | 8 | `802.3` 是为稳定可靠的有线环境设计的标准“工服”,追求极致的简洁与效率。其最常见的 **Ethernet II** 帧结构如下: 9 | 10 | |字段 (Field)|长度 (Bytes)|功能描述 (Description)| 11 | |---|---|---| 12 | |**目的MAC地址**|6|接收方设备的物理地址(网卡地址)。| 13 | |**源MAC地址**|6|发送方设备的物理地址。| 14 | |**类型 (Type)**|2|标识上层协议类型,如 `0x0800` 代表IP协议。| 15 | |**数据载荷 (Data)**|46 ~ 1500|实际承载的上层数据。若数据不足46字节,则需用**填充(Padding)**补齐,以满足64字节最小帧长。| 16 | |**帧检验序列 (FCS)**|4|使用CRC算法对整帧进行校验,以检测传输过程中是否出错。| 17 | 18 | > **核心特点**:结构精简,控制开销低,是高速有线网络效率的基石。 19 | 20 | ### 二、特战防弹衣:IEEE 802.11 无线局域网帧 21 | 22 | `802.11` 面对的是开放、易受干扰的无线环境,其“工服”必须功能全面,如同特种部队的重型装备,以应对各种复杂状况。 23 | 24 | |字段 (Field)|长度 (Bytes)|功能描述 (Description)| 25 | |---|---|---| 26 | |**帧控制 (Frame Control)**|2|极为重要的控制字段,内含多个子域,定义了帧类型(数据/控制/管理)、去往/来自AP、是否加密等信息。| 27 | |**持续时间/ID (Duration/ID)**|2|用于CSMA/CA机制。设定一个网络分配向量(NAV),告知其他站点本次通信将占用信道多长时间,以避免冲突。| 28 | |**地址1 (Address 1)**|6|**接收方地址 (RA)**:直接接收本帧的无线设备(通常是AP或目的站点)。| 29 | |**地址2 (Address 2)**|6|**发送方地址 (TA)**:直接发送本帧的无线设备(通常是源站点或AP)。| 30 | |**地址3 (Address 3)**|6|根据通信场景,可能是最终的**目的地址(DA)**或**源地址(SA)**。例如,手机通过AP访问服务器时,此为服务器的MAC。| 31 | |**序列控制 (Sequence Control)**|2|用于数据帧的重组和去重,处理分片和重传。| 32 | |**地址4 (Address 4)**|6|(可选) 仅在特殊的无线分布式系统(WDS)桥接模式下使用。| 33 | |**帧主体 (Frame Body)**|0 ~ 2312|实际承载的上层数据。| 34 | |**帧检验序列 (FCS)**|4|对整帧进行CRC校验。| 35 | 36 | > **核心特点**:结构复杂,开销高昂。所有额外的控制字段都是为了在无线环境中实现可靠的信道协调与碰撞避免。 37 | 38 | ### 三、加急VIP工牌:IEEE 802.1Q VLAN标签 39 | 40 | `802.1Q` 并非一种独立的帧类型,而是在`802.3`帧的基础上,增加了一个4字节的“身份标签”,以实现VLAN(虚拟局域网)的划分。 41 | 42 | **【标签插入位置】** 43 | 44 | 它被精确地插入在源MAC地址和类型字段之间: 45 | 46 | `| ...源MAC |` **802.1Q Tag (4字节)** `| 类型... |` 47 | 48 | **【802.1Q Tag 结构详解】** 49 | 50 | 这个4字节的标签本身又由以下几个部分组成: 51 | 52 | |字段 (Field)|长度|功能描述 (Description)| 53 | |---|---|---| 54 | |**TPID (Tag Protocol ID)**|2字节|标签协议标识符。值固定为`0x8100`,用于声明这是一个携带VLAN标签的帧。| 55 | |**PCP (Priority Code Point)**|3比特|**优先级**。定义了8个服务等级(QoS),让交换机能优先处理语音等重要数据。| 56 | |**DEI (Drop Eligible Indicator)**|1比特|**可丢弃指示符**。在网络拥塞时,此位为1的帧可被优先丢弃。| 57 | |**VLAN ID (VID)**|12比特|**VLAN标识符**。核心字段,其值(1~4094)指明了该帧所属的VLAN。| 58 | 59 | > **核心特点**:对802.3帧的“微创”增强。以极小的4字节开销,实现了逻辑网络隔离和流量优先级划分。 60 | 61 | ### 四、终极对决:三姐妹横向对比 62 | 63 | |特性|IEEE 802.3 (以太网)|IEEE 802.11 (Wi-Fi)|IEEE 802.1Q (VLAN Tag)| 64 | |---|---|---|---| 65 | |**应用环境**|**有线局域网**|**无线局域网**|启用VLAN的有线局域网| 66 | |**帧开销**|**低**(18字节头尾)|**高**(28字节头+4字节尾)|**极低**(在802.3基础上+4字节)| 67 | |**地址字段**|2个 (源、目的)|最多4个|2个 (同802.3)| 68 | |**核心机制**|CSMA/CD (碰撞检测)|CSMA/CA (碰撞避免)|基于802.3,增加VLAN识别| 69 | |**解决的问题**|高速可靠的物理点对点/广播传输|混乱环境下的信道协调与可靠访问|物理网络之上的逻辑网络隔离| 70 | -------------------------------------------------------------------------------- /src/site/notes/408/互斥信号量与同步信号量.md: -------------------------------------------------------------------------------- 1 | --- 2 | {"dg-publish":true,"dg-permalink":"/408/互斥信号量与同步信号量","permalink":"/408/互斥信号量与同步信号量/"} 3 | --- 4 | 5 | 6 | 7 | ### 核心原则:信号量的初值代表其所管理“资源”的初始数量 8 | 9 | 理解这句话是掌握初值设定的关键。在P、V操作的语境下,一个信号量`S`的`value`值,代表了**当前可用的、由它管理的资源数量**。因此,`S`的**初值**就理所当然地代表了在程序开始执行时,**该资源的初始可用数量**。 10 | 11 | 为了在考试中快速、准确地确定初值,我们需要将信号量根据其**作用**进行分类讨论,主要分为两类:**互斥信号量**和**同步信号量**。 12 | 13 | --- 14 | 15 | ### 1. 互斥信号量 (Mutual Exclusion Semaphore) 16 | 17 | **作用**: 用于保护临界区,保证“任一时刻”只有一个进程能进入临界区。 18 | 19 | **初值设定**: **通常恒为 1**。 20 | 21 | 原因 (Why is it 1?): 22 | 23 | 互斥信号量管理的“资源”是什么?是**“进入临界区的许可”** 24 | 25 | - 我们希望在程序一开始时,临界区是空闲的,应该**允许一个进程**进入。所以,“进入许可”这份资源的初始数量就是1。 26 | 27 | - **流程推演**: 28 | 29 | 1. 初始时 `mutex.value = 1`。 30 | 31 | 2. 第一个进程 `P1` 到达,执行 `P(mutex)`。`mutex.value` 减为0。`P1` 不阻塞,进入临界区。 32 | 33 | 3. 在 `P1` 未退出时,第二个进程 `P2` 到达,执行 `P(mutex)`。`mutex.value` 减为-1。`P2` 发现 `value < 0`,于是阻塞等待。 34 | 35 | 4. `P1` 退出临界区,执行 `V(mutex)`。`mutex.value` 增加为0。由于 `value <= 0`,操作系统唤醒等待队列中的 `P2`。 36 | 37 | --- 38 | 39 | ### 2. 同步信号量 (Synchronization Semaphore) 40 | 41 | **作用**: 用于协调多个进程之间的执行次序,一个进程需要等待另一个进程完成某项任务后才能继续执行(即“前驱关系”)。 42 | 43 | **初值设定**: **根据具体场景的初始资源数决定,通常为 0 或 n**。 44 | 45 | 原因 (Why is it 0 or n?): 46 | 47 | 同步信号量管理的“资源”是一个事件是否发生、一个条件是否满足、或者某个池子里的产品/空位数。其初值的确定,需要你仔细分析问题的初始状态。 48 | 49 | **分析方法**: 问自己一个问题:“在程序刚开始运行时,消费者/等待者所等待的资源/事件,初始时有多少个?” 50 | 51 | **经典考题场景分析**: 52 | 53 | #### 场景一:生产者-消费者问题 54 | 55 | - **`empty` 信号量**: 56 | 57 | - **管理对象**: 缓冲区中**空闲的位置**数量。 58 | 59 | - **分析**: 生产者进程需要“消费”一个空位置来放产品。在程序开始时,缓冲区是完全空的。 60 | 61 | - **初始状态提问**: “程序开始时,有多少个空位置可供生产者使用?” 62 | 63 | - **答案**: 若缓冲区大小为 `n`,则有 `n` 个空位置。 64 | 65 | - **结论**: `semaphore empty = n;` 66 | 67 | - **`full` 信号量**: 68 | 69 | - **管理对象**: 缓冲区中**有产品的位置**数量(即产品数量)。 70 | 71 | - **分析**: 消费者进程需要“消费”一个产品。 72 | 73 | - **初始状态提问**: “程序开始时,缓冲区里有多少个产品可供消费者使用?” 74 | 75 | - **答案**: 0 个。 76 | 77 | - **结论**: `semaphore full = 0;` 78 | 79 | ### 总结:一套万能的设定方法 80 | 81 | 在考场上,当你面对一个信号量大题,按以下步骤思考初值: 82 | 83 | 1. **第一步:识别信号量的作用** 84 | 85 | - 这个信号量是为了让多个进程不冲突地访问同一个区域吗? 86 | 87 | - 是 -> **它是互斥信号量,初值为 1。** 88 | 89 | - 这个信号量是为了让一个进程等待另一个进程完成某件事吗? 90 | 91 | - 是 -> **它是同步信号量,进入第二步。** 92 | 93 | 2. **第二步:分析同步信号量的初始资源** 94 | 95 | - 明确该信号量代表的“资源”或“事件”究竟是什么(如:空缓冲区、产品、打印任务完成信号等)。 96 | 97 | - 分析整个系统的**初始状态**(通常是0时刻),判断这个“资源”或“事件”的**初始数量**是多少。 98 | 99 | - 这个数量就是该同步信号量的**初值**。 100 | -------------------------------------------------------------------------------- /src/site/notes/408/IP地址块分配的地址不重叠和路由聚合的不引入多余地址.md: -------------------------------------------------------------------------------- 1 | --- 2 | {"dg-publish":true,"dg-permalink":"/408/IP地址块分配的地址不重叠和路由聚合的不引入多余地址","permalink":"/408/IP地址块分配的地址不重叠和路由聚合的不引入多余地址/"} 3 | --- 4 | 5 | 6 | ### 一、 地址的不重叠 (Non-overlapping Addresses) 7 | 8 | #### 1. 定义与内涵 9 | 10 | “地址不重叠”原则,指的是在进行IP地址块**分配**时,任意两个分配出去的地址块,其覆盖的IP地址范围**不能有任何交集**。每一个IP地址在特定的管理域内,都必须被唯一地划分给一个子网或一个组织。 11 | 12 | 这确保了IP地址分配的**唯一性**和**排他性**。 13 | 14 | #### 2. 作用与重要性 15 | 16 | 此原则的根本目的是为了**消除路由的模糊性,保证转发的确定性**。 17 | 18 | 试想,如果网络中存在两个重叠的地址块,并且它们被分配给了不同的组织或网络区域,那么当一个数据包的目的地址恰好落在这个重叠区域内时,路由器会面临一个无法解决的难题:这个数据包到底应该发往哪个方向? 19 | 20 | - **路由冲突**:互联网上的路由器可能会学习到两条去往同一个地址(或地址段)但指向不同方向的路径。 21 | 22 | - **转发不一致**:这会导致数据包的转发路径变得不稳定,可能这次发往A处,下次发往B处,造成连接中断或数据丢失。 23 | #### 3. 示例 24 | 25 | 假设一个网络管理员需要为两个部门分配地址: 26 | 27 | - **正确的分配(不重叠)**: 28 | 29 | - 分配给部门A:`192.168.0.0/24` (范围: `192.168.0.0` ~ `192.168.0.255`) 30 | 31 | - 分配给部门B:`192.168.1.0/24` (范围: `192.168.1.0` ~ `192.168.1.255`) 32 | 33 | - 这两个地址块边界清晰,没有任何交集。 34 | 35 | - **错误的分配(重叠)**: 36 | 37 | - 分配给部门A:`192.168.0.0/23` (范围: `192.168.0.0` ~ `192.168.1.255`) 38 | 39 | - 分配给部门B:`192.168.1.0/24` (范围: `192.168.1.0` ~ `192.168.1.255`) 40 | 41 | - 此时,部门B的整个地址空间完全被包含在了部门A的地址空间内。当一个去往`192.168.1.50`的数据包到达路由器时,路由器会因无法确定唯一路径而产生路由冲突。 42 | 43 | 44 | --- 45 | 46 | ### 二、 聚合之后不引入多余地址 (No Extraneous Addresses after Aggregation) 47 | 48 | #### 1. 定义与内涵 49 | 50 | 此原则指的是,在将多个离散、连续的较小地址块**聚合(或称“超网化”,Supernetting)成一个更大的地址块,并向外界通告一条总的路由时,这个聚合后的“超网”地址块,其范围必须恰好且仅仅**覆盖了所有原始的小地址块。 51 | 52 | #### 2. 作用与重要性 53 | 54 | 此原则的根本目的是为了**保证路由的准确性,防止产生“路由黑洞”或“路由劫持”**。 55 | 56 | 如果聚合后的范围过大,包含了本不属于该组织或区域的“多余”地址,那么当这个聚合路由被通告到互联网后,会发生以下情况: 57 | 58 | - **吸引错误流量**:外部路由器会认为那些“多余”的地址也归属于这个组织,从而将去往这些地址的流量错误地发送过来。 59 | 60 | - **形成路由黑洞**:由于这些“多余”的地址在该组织内部实际上并不存在,或者没有具体的转发路径,这些被错误吸引过来的流量最终将被路由器丢弃,形成一个有去无回的“黑洞”。 61 | #### 3. 示例 62 | 63 | 假设一个机构拥有以下两个**连续的**C类地址块: 64 | 65 | - `202.118.16.0/24` 66 | 67 | - `202.118.17.0/24` 68 | 69 | 70 | 机构希望向其上游ISP通告一条聚合路由。 71 | 72 | - **二进制分析**: 73 | 74 | ``` 75 | 202.118.00010000.0 (16) 76 | 202.118.00010001.0 (17) 77 | ``` 78 | 79 | 观察其第三个字节的二进制位,可以发现它们的前23位是完全相同的 (`...0001000...`)。因此,可以将它们聚合。 80 | 81 | - **正确的聚合(精确)**: 82 | 83 | - 聚合后的路由为 `202.118.16.0/23`。 84 | 85 | - 这个`/23`的地址块覆盖的范围是 `202.118.16.0` ~ `202.118.17.255`,不多不少,**刚刚好**覆盖了原始的两个`/24`地址块。 86 | 87 | - **错误的聚合(引入多余地址)**: 88 | 89 | - 如果网络工程师错误地计算,将其聚合成 `202.118.16.0/22`。 90 | 91 | - 这个`/22`的地址块覆盖的范围是 `202.118.16.0` ~ `202.118.19.255`。 92 | 93 | - 这个聚合路由除了包含原有的`16`和`17`网段外,还**额外引入**了`202.118.18.0/24` 和 `202.118.19.0/24` 这两个可能属于其他机构的地址空间。这就违反了原则,会导致去往`18`和`19`网段的流量被错误地吸引过来并丢弃。 -------------------------------------------------------------------------------- /src/site/notes/408/CSMACA:是自由吟唱的混沌魔法,还是遵循古老契约的序列仪式?🤔.md: -------------------------------------------------------------------------------- 1 | --- 2 | {"dg-publish":true,"dg-permalink":"/408/CSMACA:是自由吟唱的混沌魔法,还是遵循古老契约的序列仪式?🤔","permalink":"/408/CSMACA:是自由吟唱的混沌魔法,还是遵循古老契约的序列仪式?🤔/"} 3 | --- 4 | 5 | 6 | ### 一、两种模式的核心概念与对比 7 | 8 | CSMA/CA 协议根据信道访问的控制方式,主要分为两种模式:基于竞争的普通模式和无竞争的预约模式。在 IEEE 802.11 标准中,这两种模式分别通过分布式协调功能(DCF)和点协调功能(PCF)等机制实现。 9 | ![image-1.png|319x180](/img/user/%E9%99%84%E4%BB%B6/image-1.png)![image-2.png|287x178](/img/user/%E9%99%84%E4%BB%B6/image-2.png) 10 | 11 | |对比项|普通模式 (基于DCF)|预约模式 (基于PCF/HCF)| 12 | |---|---|---| 13 | |**控制方式**|**分布式控制**:所有站点地位平等,自行竞争信道访问权。|**集中式控制**:由中心节点(如AP)统一协调和分配访问权限。| 14 | |**冲突可能性**|**存在冲突**:通过冲突避免(CA)和随机退避机制降低概率,但无法根除。|**无冲突**:由中心节点精确调度,从机制上避免了冲突的发生。| 15 | |**服务质量(QoS)**|**尽力而为**:无法提供时延或带宽保障。|**可提供保障**:能够为特定业务提供可控的低时延和确定性服务。| 16 | |**典型应用**|适用于非实时、突发性的数据业务,如网页浏览、文件下载。|适用于对时延和抖动敏感的实时业务,如VoIP语音、视频会议。| 17 | |**实现与支持**|**基础必备**:所有802.11设备必须支持。|**可选高级功能**:并非所有设备或AP都支持,或完全实现。| 18 | 19 | ### 二、普通模式(争用模式)详解 20 | 21 | 普通模式,即**分布式协调功能 (DCF)**,是 CSMA/CA 协议的基础和默认工作方式。 22 | 23 | **1. 适用场景** 24 | 25 | - 网络负载较轻,站点数量有限,冲突概率较低的环境。 26 | 27 | - 传输对时延不敏感的业务类型,如 Web 浏览、电子邮件和文件下载。 28 | 29 | - 网络中各站点以平等机会访问信道,无特殊优先级区分。 30 | 31 | - 在不包含接入点(AP)的自组织网络(Ad-hoc)中,或AP不支持高级调度功能时。 32 | 33 | 34 | **2. 工作机制** 35 | 36 | 其工作严格遵循 CSMA/CA 的核心流程: 37 | 38 | 1. **载波侦听**:节点在发送前持续侦听信道,以判断其是否空闲。 39 | 40 | 2. **帧间间隔与退避**:若信道从忙碌转为空闲,节点需等待一个分布式帧间间隔(DIFS)后,再启动一个随机退避计时器。 41 | 42 | 3. **发送与确认**:退避计时器最先归零的节点获得发送权,发送数据帧后,等待接收方返回ACK帧以确认成功。若未收到ACK,则认为发生冲突,将增大冲突窗口并重新进入退避流程。 43 | 44 | 45 | 此模式实现相对简单,控制开销小,但在高密度、高负载网络下,冲突概率剧增,性能会显著下降。 46 | 47 | ### 三、预约模式(非争用模式)详解 48 | 49 | 预约模式通过引入一个中心协调者来取代完全的自由竞争,从而为特定业务提供服务质量保障。 50 | 51 | **1. 适用场景** 52 | 53 | - 网络负载繁重,冲突现象频繁,导致整体性能下降的环境。 54 | 55 | - 承载需要严格时延和抖动保障的实时应用,如IP语音(VoIP)、实时视频会议、工业无线控制等。 56 | 57 | - 网络中的接入点(AP)支持并开启了点协调功能(PCF)或混合协调功能(HCF)等高级调度机制。 58 | 59 | 60 | **2. 典型机制:点协调功能 (PCF)** 61 | 62 | PCF 依赖于AP作为**点协调者(Point Coordinator, PC)**,将时间划分为**争用期(Contention Period, CP)**和**非争用期(Contention-Free Period, CFP)**。 63 | 64 | - 在**争用期(CP)**,网络运行方式与普通模式的DCF完全相同。 65 | 66 | - 在**非争用期(CFP)**,AP掌握信道的绝对控制权。它会通过**轮询(Polling)**的方式,依次向列表中的站点授予发送权限。被轮询到的站点无需竞争即可直接发送数据,从而实现了无冲突的确定性访问。 67 | 68 | ### 四、应用场景与选择策略 69 | 70 | 在实际应用中,两种模式的选择取决于网络环境和业务需求。 71 | 72 | - **家庭日常上网**(网页浏览、社交媒体、观看在线视频):网络负载通常不高,且业务对偶发延迟不敏感,因此完全运行在**普通模式(DCF)**下即可满足需求。 73 | 74 | - **企业视频会议**:一个会议室内,多人同时进行高清视频会议、共享桌面和文件传输。为保障视频和语音的流畅,支持QoS的AP会将这些实时流量划分到高优先级的队列,并可能通过**预约模式(如EDCA的机制)**为其提供优先或专有的传输机会(TXOP),而文件传输等非实时业务则继续在普通模式下竞争。 75 | 76 | - **工业自动化控制**(如无线机器人控制):这类场景对通信的确定性和实时性要求极为苛刻。必须采用**预约模式**或更专业的工业无线协议(常基于TDMA),任何由竞争退避带来的不确定延迟都可能导致生产事故。 77 | 78 | 79 | **总结而言,选择哪种模式的决策逻辑如下:** 80 | 81 | - 若网络中无特殊实时性业务,或设备/AP不支持高级调度功能,则默认工作在**普通模式**。 82 | 83 | - 若网络中承载有时延敏感型业务,且设备/AP支持相关QoS机制,则应配置并优先使用**预约模式**,以保障关键业务的服务质量。 -------------------------------------------------------------------------------- /src/site/notes/408/ICMP交警何时对IP数据报发出罚单呢🤔.md: -------------------------------------------------------------------------------- 1 | --- 2 | {"dg-publish":true,"dg-permalink":"/408/ICMP交警何时对IP数据报发出罚单呢🤔","permalink":"/408/ICMP交警何时对IP数据报发出罚单呢🤔/"} 3 | --- 4 | 5 | 6 | ICMP差错报告报文是在IP数据报的转发或交付过程中发生错误(如目的不可达、生存时间为0等)时,由路由器或目的主机发送给源主机的。但是,当网络层发现IP首部校验和错误时,会立即丢弃该数据报,并不会发送任何ICMP差错报文。 7 | ### 一、ICMP差错报文的发送情况 8 | 9 | 当路由器或目的主机在处理一个IP数据报时,如果遇到无法继续处理的问题,就会生成一个ICMP差错报文,发送给该IP数据报的源主机。这个ICMP报文相当于一份“错误告知书”。 10 | 11 | 1. **终点不可达 (Destination Unreachable, 类型3)**:当路由器或主机无法交付数据报时发送。常见原因有: 12 | 13 | - **网络不可达** (代码0):路由器找不到通往目的网络的路由。 14 | 15 | - **主机不可达** (代码1):路由器找到了目的网络,但在该网络内找不到目的主机。 16 | 17 | - **协议不可达** (代码2):目的主机的IP层收到了数据报,但其载荷应交付的运输层协议(如TCP/UDP)未在该主机上运行。 18 | 19 | - **端口不可达** (代码3):数据报已到达目的主机,指定的协议也存在,但相应的端口号没有进程在监听。 20 | 21 | - **需要分片但DF位为1** (代码4):路由器需要对数据报进行分片,但IP首部的“不分片(DF)”标志位被置为1。 22 | 23 | 2. **超时 (Time Exceeded, 类型11)**: 24 | 25 | - **TTL=0**:路由器收到一个IP数据报,发现其生存时间(TTL)字段为1或0。路由器将TTL减1后变为0,此时会丢弃该数据报,并向源主机发送ICMP超时报文。`traceroute`命令就利用了这个原理。 26 | 27 | - **分片重组超时**:目的主机在规定时间内没有收到一个被分片的数据报的所有分片,会丢弃已收到的分片,并向源主机发送此报文。 28 | 29 | 3. **参数问题 (Parameter Problem, 类型12)**:当路由器或目的主机发现IP首部中存在非法字段值或选项不完整等问题时,会丢弃该数据报并发送此报文。 30 | 31 | 4. **源点抑制 (Source Quench, 类型4)**:当路由器或主机由于拥塞而丢弃数据报时,向源点发送此报文,请求源点降低发送速率。**注意**:该报文在现在的互联网中已基本被弃用,被TCP的拥塞控制机制取代,但在考纲中仍可能作为历史知识点出现。 32 | 33 | 5. **重定向 (Redirect, 类型5)**:路由器发现一台主机使用了非最优的路径发送数据时,会向该主机发送重定向报文,告诉它下次应发往的“更近”的路由器。这要求主机和两个路由器在同一个局域网内。 34 | 35 | 36 | ### 二、IP首部校验和错误:为何是例外? 37 | 38 | 原因: 39 | 40 | IP首部的校验和(Checksum)字段是用于保证IP首部完整性的。路由器每转发一个数据报,都要重新计算首部校验和。如果计算出的校验和结果不为0,就说明IP首部在传输过程中出现了比特差错,已经损坏。 41 | 42 | 一个损坏的IP首部是不可信的。其中最关键的字段,如**源IP地址和目的IP地址**,可能已经出错了。 43 | 44 | - **如果源IP地址已经损坏**,那么ICMP差错报文将无法被正确地发送回真正的源主机,反而可能会被发送到一个无辜的、错误的地址,造成网络中不必要的垃圾流量。 45 | 46 | - **如果目的IP地址已经损坏**,数据报本身就已经迷路了,讨论后续处理已无意义。 47 | 48 | 49 | 因此,协议设计者做出了最安全、最合理的规定:**一旦发现IP首部校验和有误,路由器或主机会立即“沉默地”丢弃该数据报,不做任何进一步处理,包括不发送ICMP差错报文。** 50 | 51 | **流程图:路由器处理数据报的简化逻辑** 52 | 53 | 代码段 54 | 55 | ```mermaid 56 | graph TD 57 | A[数据报到达路由器] --> B{校验IP首部校验和}; 58 | B -- 错误 --> C[默默丢弃数据报]; 59 | B -- 正确 --> D{检查TTL是否 > 1}; 60 | D -- 是 --> E[TTL减1, 重新计算校验和, 查找路由表并转发]; 61 | D -- 否 (TTL ≤ 1) --> F[丢弃数据报]; 62 | F --> G[向源主机发送 ICMP 超时报文]; 63 | ``` 64 | 65 | ### 三、常考点分析与不发送ICMP的其它情况 66 | 67 | 除了IP首部校验和错误外,考研中还经常考察以下几种**不发送ICMP差错报文**的情况,必须牢记: 68 | 69 | 1. **对ICMP差错报文不再发送ICMP差错报文**:为了防止两个节点之间无限循环地发送ICMP差错报文,协议规定收到ICMP差错报文后即使发现它有问题,也不再为它生成新的ICMP差错报文。 70 | 71 | 2. **对第一个分片之后的分片不发送ICMP差错报文**:当一个数据报被分片后,只有第一个分片(片偏移为0)携带了完整的运输层首部信息(如TCP/UDP端口号)。如果后续分片出错,由于缺乏足够信息,且为了避免对同一个数据报的多个分片都发送差错报文而导致“ICMP风暴”,规定只对第一个分片出错时才发送ICMP报文。 72 | 73 | 3. **对具有多播或广播地址的数据报不发送ICMP差错报文**:向一个组或网络中的所有主机报告差错会导致网络流量的急剧增加(广播风暴),这是不被允许的。 74 | 75 | 4. **对作为链路层广播的数据报不发送ICMP差错报文**:这与上一条原理类似。 76 | -------------------------------------------------------------------------------- /src/site/notes/408/在隐含寻址中第二操作数是怎么在ACC当中金屋藏娇呢🤔.md: -------------------------------------------------------------------------------- 1 | --- 2 | {"dg-publish":true,"dg-permalink":"/408/在隐含寻址中第二操作数是怎么在ACC当中金屋藏娇呢🤔","permalink":"/408/在隐含寻址中第二操作数是怎么在ACC当中金屋藏娇呢🤔/"} 3 | --- 4 | 5 | 6 | ### 一、 答案 7 | 8 | 累加器ACC中的数据,是在执行这条隐含寻址指令**之前**,由另一条加载指令(如 `LDA`)从内存或其它地方送入的。 9 | 10 | ### 二、 相关原理或操作过程的详细讲解 11 | 12 | 要理解ACC中的值“何时进去”,我们必须明白,一条独立的指令通常只完成一个原子性的操作。将数据从内存加载到ACC,和利用ACC中的数据进行运算,是两条不同的指令完成的。 13 | 14 | 这个过程通常分为两步,我们以一个典型的单累加器结构的CPU为例,计算表达式 `C = A + B` (假设A, B, C是内存地址)。 15 | 16 | **第1步:加载操作数(让数据“进去”)** 17 | 18 | CPU首先需要执行一条**加载指令 (Load)**,把第一个操作数从内存地址 `A` 拿出来,送到累加器ACC中。 19 | 20 | - **指令:** `LDA A` 21 | 22 | - `LDA` 是 "LoaD Accumulator" 的缩写,是操作码。 23 | 24 | - `A` 是地址码,采用直接寻址方式,指明了操作数在主存中的位置。 25 | 26 | - **执行过程:** 27 | 28 | 1. CPU读取并译码 `LDA A` 指令。 29 | 30 | 2. CPU通过地址总线发送地址 `A`。 31 | 32 | 3. 主存根据地址 `A` 找到对应的数据 `M(A)`。 33 | 34 | 4. 主存将数据 `M(A)` 通过数据总线返回给CPU。 35 | 36 | 5. CPU将接收到的数据 `M(A)` 打入(写入)到累加器ACC中。 37 | 38 | 39 | **至此,第一个操作数已经进入了ACC。这是对您“何时进去”这个问题的直接回答。** 40 | 41 | **第2步:执行隐含寻址指令** 42 | 43 | 现在ACC中已经有了来自 `A` 的值,接下来CPU执行加法指令。 44 | 45 | - **指令:** `ADD B` 46 | 47 | - `ADD` 是加法操作码。 48 | 49 | - `B` 是地址码,采用直接寻址,指明了第二个操作数的位置。 50 | 51 | - **执行过程:** 52 | 53 | 1. CPU读取并译码 `ADD B` 指令。操作码 `ADD` 在这种体系结构中**隐含约定**了: 54 | 55 | - 一个操作数已经存放在ACC中。 56 | 57 | - 另一个操作数需要从指令的地址码部分获取。 58 | 59 | - 运算结果将存回ACC中。 60 | 61 | 2. CPU根据地址码 `B` 从主存中取出第二个操作数 `M(B)`。 62 | 63 | 3. CPU内部的算术逻辑单元 (ALU) 将ACC的现存内容和从主存取出的 `M(B)` 相加。 64 | 65 | 4. ALU将加法结果写回到ACC中,覆盖掉原来的值。 66 | 67 | 68 | **整个流程的伪代码与ACC状态变化:** 69 | 70 | | 汇编指令 | 操作解释 | ACC 内部值的变化 | 71 | | ------- | ------------------------------------ | ----------------------------------------- | 72 | | `LDA A` | Load Accumulator: 将内存地址A处的值加载到ACC。 | `ACC` <- `M(A)` | 73 | | `ADD B` | Add: 将ACC的值与内存地址B处的值相加,结果存回ACC。 | `ACC` <- `(ACC)` + `M(B)` (即 `M(A)+M(B)`) | 74 | | `STA C` | Store Accumulator: 将ACC的当前值存入内存地址C处。 | `ACC` 内容不变,`M(C)` <- `(ACC)` | 75 | 76 | 上图中,`ACC = M[A]` 这个状态就是ACC“进去”数据的时间点。 77 | 78 | 79 | ### 三、 边界情况、易错点、反例 80 | 81 | - **纯粹的隐含寻址:** 有些指令甚至不包含任何地址码字段,它们的操作数完全是隐含的。 82 | 83 | - **例:** `CMA` (Complement Accumulator,累加器取反)。这条指令只有一个操作码,它默认对ACC的内容进行按位取反,结果仍存回ACC。这里,源操作数和目的操作数都是ACC,且都被隐含了。 84 | 85 | - **例:** 堆栈操作中的 `PUSH` 和 `POP` 指令。它们隐含使用了栈顶指针 `SP` 来作为内存操作的地址。例如 `PUSH AX`,指令只说了要把 `AX` 的内容入栈,但入到哪里去呢?这个地址是由 `SP` 寄存器隐含提供的。 86 | 87 | - **易错点:** 将单地址指令与隐含寻址划等号。 88 | 89 | - 单地址指令 `OP Addr`(如 `ADD B`)确实使用了隐含寻址(隐含了ACC),但它的另一个操作数 `M(B)` 却采用了**直接寻址**。一条指令可以组合多种寻址方式。考题若问“ADD B指令使用了哪种寻址方式?”,严谨的答案是“隐含寻址和直接寻址”。如果选项中只有其一,通常是考察其最核心的特征,即作为单地址指令对ACC的隐含使用。 90 | -------------------------------------------------------------------------------- /src/site/notes/408/多线程模型.md: -------------------------------------------------------------------------------- 1 | --- 2 | {"dg-publish":true,"dg-permalink":"/408/多线程模型","permalink":"/408/多线程模型/"} 3 | --- 4 | 5 | 6 | ## 多线程模型概述 7 | 在现代操作系统中,为了支持并发执行,有两种主要的线程类型:**用户级线程(User-Level Threads, ULTs)** 和 **内核级线程(Kernel-Level Threads, KLTs)**。而**多线程模型**则描述了用户级线程与内核级线程之间的映射关系。换句话说,它定义了用户编写的线程(用户级线程)是如何被操作系统内核所管理的线程(内核级线程)来支持的。 8 | 9 | 理解这两种线程是理解多线程模型的基础。 10 | 11 | - **用户级线程 (ULTs)**: 12 | 13 | - **特点**:完全由**用户空间的线程库**管理。内核对它们的存在**无感知**。线程的创建、销毁、调度、同步都在用户空间完成,不需要内核介入。 14 | - **优点**: 15 | - **创建与管理开销小**:因为不需要内核模式切换,所以创建和销毁线程的速度快,上下文切换效率高。 16 | - **调度灵活**:用户线程库可以根据应用需求实现自定义的调度算法。 17 | - **缺点**: 18 | - **阻塞性**:正如我们之前讨论的,如果一个用户级线程执行**阻塞式系统调用**,整个进程(包括进程内的所有用户级线程)都会被内核阻塞,导致其他用户级线程也无法运行。 19 | - **无法利用多核CPU**:由于内核只将整个进程视为一个执行单元,因此在一个多核处理器上,一个进程内部的用户级线程无法同时在不同的CPU核心上并行执行。 20 | - **应用场景**:适合于I/O操作较少,计算密集型且需要频繁切换的场景。 21 | - **内核级线程 (KLTs)**: 22 | 23 | - **特点**:由**操作系统内核**管理和调度。每个内核级线程都是内核可独立调度的实体。 24 | - **优点**: 25 | - **非阻塞性**:当一个内核级线程执行阻塞式系统调用时,只有这个线程会被阻塞,同一进程内的其他内核级线程可以继续执行,提高了并发性。 26 | - **可利用多核CPU**:内核可以同时调度多个内核级线程在不同的CPU核心上并行执行,真正实现并行计算。 27 | - **缺点**: 28 | - **创建与管理开销大**:线程的创建、销毁、调度等操作都需要陷入内核,涉及**用户态/内核态的切换**,开销相对较大。 29 | - **可移植性差**:线程的实现细节与特定的操作系统内核紧密相关。 30 | - **应用场景**:适用于I/O密集型任务或需要充分利用多核CPU的场景。 31 | 32 | --- 33 | 34 | 35 | 36 | ## 常见的多线程模型 37 | 基于用户级线程和内核级线程的不同映射关系,主要有三种多线程模型: 38 | 39 | ### 1. 多对一模型 (Many-to-One Model) 40 | - **特点**: 41 | - 将**多个用户级线程**映射到**一个内核级线程**上。 42 | - 所有用户级线程的调度和管理都由用户空间的线程库完成。 43 | - 对于内核来说,只存在一个内核级线程,因此内核只知道一个进程在运行。 44 | - **优点**: 45 | - 用户级线程的管理开销小,创建和切换速度快。 46 | - **缺点**: 47 | - **最大的缺点就是阻塞问题**:如果任何一个用户级线程执行阻塞系统调用,整个进程及其所有用户级线程都会被阻塞。 48 | - 无法利用多核处理器的并行能力。 49 | - **典型实现**:较早期的线程库,如Solaris Green Threads。在现代操作系统中已经不常见,或仅用于特定目的(如Go语言的早期协程)。 50 | - **常考点**:强调其“一个阻塞则全部阻塞”的特性,以及无法利用多核的缺点。 51 | 52 | --- 53 | 54 | 55 | ### 2. 一对一模型 (One-to-One Model) 56 | - **特点**: 57 | - 将**每一个用户级线程**都独立地映射到**一个内核级线程**上。 58 | - 内核可以感知并调度每一个线程。 59 | - **优点**: 60 | - **真正的并发性**:一个线程阻塞不会影响其他线程的执行。 61 | - 可以充分利用多核处理器,实现真正的并行。 62 | - **缺点**: 63 | - **开销大**:线程的创建、销毁、上下文切换都需要内核介入,涉及系统调用,开销比用户级线程大。 64 | - 操作系统对可以创建的内核级线程数量通常有限制,创建过多可能导致性能下降。 65 | - **典型实现**:Linux的Pthreads(NPTL实现),Windows线程。这是现代大多数操作系统所采用的主流模型。 66 | - **常考点**:强调其高并发性、能利用多核的优点,以及系统开销大的缺点。 67 | 68 | --- 69 | 70 | 71 | ### 3. 多对多模型 (Many-to-Many Model) 72 | - **特点**: 73 | - 将**多个用户级线程**映射到**数量相等或更少(但大于一)的内核级线程**上。 74 | - 它试图结合前两种模型的优点,避免它们的缺点。 75 | - 用户线程库负责用户级线程的调度,而内核负责内核级线程的调度。 76 | - **优点**: 77 | - **灵活性**:用户级线程的创建和管理效率高。 78 | - 解决了多对一模型中一个线程阻塞导致整个进程阻塞的问题(因为有多个内核级线程可以接管)。 79 | - 在一定程度上可以利用多核处理器。 80 | - 可以为应用程序创建大量用户级线程,而只需要较少的内核级线程来支持。 81 | - **缺点**: 82 | - **实现复杂**:需要用户线程库和内核调度器之间的复杂协调。 83 | - 性能调优困难。 84 | - **典型实现**:Solaris的早期版本、Windows NT早期版本,现在较少有纯粹的实现。Go语言的Goroutines(协程)在底层也常被实现为类似多对多模型的调度方式。 85 | - **常考点**:通常作为一种理想模型出现,但其实现复杂性也是考点。 86 | 87 | --- 88 | -------------------------------------------------------------------------------- /src/site/notes/408/虚拟地址的生命周期🥰.md: -------------------------------------------------------------------------------- 1 | --- 2 | {"dg-publish":true,"dg-permalink":"/408/虚拟地址的生命周期🥰","permalink":"/408/虚拟地址的生命周期🥰/"} 3 | --- 4 | 5 | 6 | ### **虚拟地址的生命周期** 7 | 8 | 虚拟地址的生命周期与它所属的**进程 (Process)** 的生命周期是紧密绑定的。我们可以将其划分为四个主要阶段: 9 | 10 | #### **第一阶段:孕育期 (Gestation) — 在编译链接时** 11 | 12 | - **产生**: 严格来说,虚拟地址的“蓝图”在程序被编译和链接成可执行文件(如Windows的`.exe`或Linux的ELF文件)时就已经被规划好了。编译器将源代码转换为包含相对地址的目标代码,链接器则将多个目标文件和库组合起来,为整个程序创建一个**统一的、线性的逻辑地址空间映像**。例如,链接器会决定`.text`段(代码段)从虚拟地址`0x08048000`开始,`.data`段(数据段)从另一个地址开始,等等。 13 | 14 | - **状态**: 在这个阶段,虚拟地址仅仅是文件中的**元数据或偏移量**。它还没有与任何实际的物理内存产生关联,只是一个存在于磁盘文件中的“规划图”。 15 | 16 | 17 | #### **第二阶段:诞生期 (Birth) — 在进程创建与加载时** 18 | 19 | - **产生**: 当用户执行一个程序时,操作系统会创建一个新的进程。在这个过程中,虚拟地址空间才真正“活”了过来。操作系统的加载器(Loader)会: 20 | 21 | 1. 为新进程创建一个**进程控制块(PCB)**。 22 | 23 | 2. 为该进程创建一套**地址映射表**(即页目录和页表)。 24 | 25 | 3. 解析可执行文件,根据“规划图”在进程的虚拟地址空间中划定区域(如代码区、数据区、BSS区、堆、栈等)。 26 | 27 | 4. **将页目录表的基地址装入特定寄存器**(如x86的CR3寄存器)。 28 | 29 | - **状态**: 从此刻起,一个独立的、私有的虚拟地址空间正式诞生。CPU在执行这个进程时,其发出的所有地址都将被视为这个空间内的虚拟地址。 30 | 31 | 32 | #### **第三阶段:活跃期 (Active Life) — 在进程执行时** 33 | 34 | - **使用**: 这是虚拟地址最活跃的时期。当CPU执行该进程的指令时,它会不断地产生虚拟地址(用于取指令、读写数据)。这些虚拟地址会被CPU中的**内存管理单元(MMU)** 截获,并通过查询页表将其**翻译**成物理地址,最终访问物理内存。 35 | 36 | - **变化**: 这个时期的虚拟地址空间不是一成不变的: 37 | 38 | - **堆的增长**: 当程序调用`malloc()`或`new`时,操作系统会扩展堆区域,**凭空创造**出新的可用虚拟地址供程序使用。 39 | 40 | - **栈的增长**: 当函数调用发生时,栈会向低地址方向增长,使用新的虚拟地址。 41 | 42 | - **动态库加载**: 程序可能会在运行时加载新的动态链接库(DLL/.so),这也会将新的库映射到虚拟地址空间的某个区域。 43 | 44 | - **内存释放**: 当调用`free()`或`delete`时,对应的虚拟地址被标记为可用,但其映射关系通常不会立即撤销,可能会被后续的`malloc`重用。 45 | 46 | 47 | #### **第四阶段:消亡期 (Disappearance) — 在进程终止时** 48 | 49 | - **消失**: 当一个进程终止时(无论是正常退出还是异常崩溃),操作系统会回收其所有资源。在内存方面,操作系统会: 50 | 51 | 1. 遍历该进程的地址映射表。 52 | 53 | 2. 释放该进程占用的所有**物理内存页框**。 54 | 55 | 3. **销毁该进程的页目录和所有页表**。 56 | 57 | - **状态**: 一旦一个进程的页表被销毁,其对应的整个虚拟地址空间就彻底不复存在了。原来那些虚拟地址对于CPU和系统来说,重新变回了毫无意义的数字。 58 | 59 | 60 | --- 61 | 62 | ### **虚拟地址的使用范围** 63 | 64 | 理解了生命周期,虚拟地址的使用范围就非常清晰了。 65 | 66 | #### **1. 范围的起点:CPU** 67 | 68 | 虚拟地址的“管辖范围”始于**中央处理器(CPU)**。当一个进程在运行时,CPU内部的程序计数器(PC)存储的是下一条指令的**虚拟地址**,CPU执行的访存指令(如`mov`)操作的也都是**虚拟地址**。 69 | 70 | #### **2. 范围的终点:内存管理单元(MMU)** 71 | 72 | 虚拟地址的“生命”终结于**内存管理单元(MMU)**。MMU是CPU内部的一个硬件模块,它的职责就是接收来自CPU核心的虚拟地址,然后通过查询页表(和高速缓存TLB)将其翻译成物理地址。一旦地址离开了MMU,进入到**物理地址总线**上,它就变成了**物理地址**。主存(内存条)、内存控制器等硬件只认识物理地址。 73 | 74 | #### **3. 范围的边界:进程** 75 | 76 | 虚拟地址最核心的范围限定就是**进程 (Process)**。 77 | 78 | - **私有性**: 每个进程都拥有自己的一套独立的、从0开始的虚拟地址空间。 79 | 80 | - **隔离性**: 进程A的虚拟地址`0x1000`和进程B的虚拟地址`0x1000`是**完全不同**的两个地址,它们通过各自独立的页表,被映射到不同的物理内存位置。 81 | 82 | - **上下文**: 一个虚拟地址,如果脱离了它的进程上下文(即不知道它属于哪个进程,不知道该用哪张页表去翻译它),它就是**没有意义的**。操作系统在进行进程切换时,最关键的一步就是**切换CR3寄存器**,即更换当前生效的页目录表,从而切换到新进程的虚拟地址空间“宇宙”中。 83 | 84 | 85 | **总结一下范围**:虚拟地址是操作系统为**单个进程**提供的、在**CPU内部使用**的一种地址抽象,其有效性由该进程的**地址映射表**所定义,并在**MMU**处被转换为物理世界的地址。 -------------------------------------------------------------------------------- /plugin-info.json: -------------------------------------------------------------------------------- 1 | { 2 | "filesToDelete": [ 3 | "src/site/styles/style.css", 4 | "src/site/index.njk", 5 | "src/site/index.11tydata.js", 6 | "src/site/_data/filetree.js", 7 | "api/search.js", 8 | "netlify/functions/search/search.js", 9 | "src/site/versionednote.njk", 10 | "src/site/_includes/layouts/versionednote.njk", 11 | "src/site/lunr-index.js", 12 | "src/site/_data/versionednotes.js", 13 | "src/site/lunr.njk" 14 | 15 | ], 16 | "filesToAdd": [ 17 | "src/site/styles/custom-style.scss", 18 | ".env", 19 | "src/site/favicon.svg", 20 | "src/site/img/default-note-icon.svg", 21 | "src/site/img/tree-1.svg", 22 | "src/site/img/tree-2.svg", 23 | "src/site/img/tree-3.svg", 24 | "src/helpers/userUtils.js", 25 | "src/helpers/userSetup.js", 26 | "vercel.json", 27 | "netlify.toml" 28 | ], 29 | "filesToModify": [ 30 | ".eleventy.js", 31 | ".eleventyignore", 32 | "README.md", 33 | "package-lock.json", 34 | "package.json", 35 | "src/site/404.njk", 36 | "src/site/sitemap.njk", 37 | "src/site/feed.njk", 38 | "src/site/styles/style.scss", 39 | "src/site/styles/digital-garden-base.scss", 40 | "src/site/styles/obsidian-base.scss", 41 | "src/site/notes/notes.json", 42 | "src/site/notes/notes.11tydata.js", 43 | "src/site/_includes/layouts/note.njk", 44 | "src/site/_includes/layouts/index.njk", 45 | "src/site/_includes/components/notegrowthhistory.njk", 46 | "src/site/_includes/components/pageheader.njk", 47 | "src/site/_includes/components/linkPreview.njk", 48 | "src/site/_includes/components/references.njk", 49 | "src/site/_includes/components/sidebar.njk", 50 | "src/site/_includes/components/graphScript.njk", 51 | "src/site/_includes/components/filetree.njk", 52 | "src/site/_includes/components/filetreeNavbar.njk", 53 | "src/site/_includes/components/navbar.njk", 54 | "src/site/_includes/components/searchButton.njk", 55 | "src/site/_includes/components/searchContainer.njk", 56 | "src/site/_includes/components/searchScript.njk", 57 | "src/site/_includes/components/calloutScript.njk", 58 | "src/site/_includes/components/lucideIcons.njk", 59 | "src/site/_includes/components/timestamps.njk", 60 | "src/site/_data/meta.js", 61 | "src/site/_data/dynamics.js", 62 | "src/site/img/outgoing.svg", 63 | "src/helpers/constants.js", 64 | "src/helpers/utils.js", 65 | "src/helpers/filetreeUtils.js", 66 | "src/helpers/linkUtils.js", 67 | "src/site/get-theme.js", 68 | "src/site/_data/eleventyComputed.js", 69 | "src/site/graph.njk", 70 | "src/site/search-index.njk" 71 | ] 72 | } -------------------------------------------------------------------------------- /src/site/notes/408/进程映像的概念.md: -------------------------------------------------------------------------------- 1 | --- 2 | {"dg-publish":true,"dg-permalink":"/408/进程映像的概念","permalink":"/408/进程映像的概念/"} 3 | --- 4 | 5 | 6 | ## 进程映像的定义与组成 7 | 在计算机操作系统中,**进程映像(Process Image)** 是指进程执行时,其在内存中的**完整、静态副本**。它包含了进程在某一时刻的所有状态信息,是操作系统管理和调度进程的重要依据。 8 | 9 | 简单来说,进程映像主要由以下几个部分组成: 10 | 11 | 1. **用户程序(Text Segment / Code Segment)**: 12 | - **定义与要求**:这是程序的可执行代码,通常是只读的,在进程运行期间不会被修改。它可以被多个进程共享,从而节省内存。 13 | - **原理**:编译器将源代码编译成机器指令,这些指令就构成了用户程序。 14 | 2. **数据段(Data Segment)**: 15 | - **定义与要求**:存放已初始化的全局变量和静态变量。 16 | - **原理**:在程序加载时,这些变量会根据初始值被加载到内存中。 17 | 3. **BSS段(Block Started by Symbol Segment)**: 18 | - **定义与要求**:存放未初始化的全局变量和静态变量。 19 | - **原理**:与数据段不同,这些变量在程序加载时不会被加载具体的初始值,而是在运行时由系统自动清零(对于C/C++语言)。这样可以避免在可执行文件中存储大量的零值,从而减小程序文件的大小。 20 | 4. **堆(Heap)**: 21 | - **定义与要求**:用于程序运行时动态内存分配。 22 | - **原理**:程序通过`malloc`、`new`等函数向系统申请内存,这些内存就在堆中分配。堆的增长方向通常是向高地址方向增长。 23 | 5. **栈(Stack)**: 24 | - **定义与要求**:用于函数调用时的局部变量、函数参数、返回地址等的存储。 25 | - **原理**:栈是一种“后进先出”(LIFO)的数据结构。每次函数调用时,都会创建一个新的栈帧(Stack Frame)压入栈中;函数返回时,对应的栈帧被弹出。栈的增长方向通常是向低地址方向增长。 26 | 6. **进程控制块(Process Control Block, PCB)**: 27 | - **定义与要求**:这是进程的唯一标识,包含了进程的所有状态信息,是操作系统用来管理和控制进程的最重要数据结构。它**不属于进程本身的内存区域,而是操作系统维护的数据结构**,但它记录了进程映像在内存中的位置等信息。 28 | - **原理**:PCB包含了进程的唯一标识符(PID)、进程状态(运行、就绪、阻塞等)、程序计数器(PC,指向下一条要执行的指令地址)、寄存器值、内存管理信息(如页表、段表指针)、打开文件列表、I/O状态信息、调度信息(优先级、事件等)等。 29 | 30 | --- 31 | 32 | 33 | 34 | ## 进程映像的形成与作用 35 | 36 | ### 形成过程 37 | 1. **加载阶段**:当一个程序被执行时,操作系统会将程序的可执行文件(其中包含用户程序、数据段、BSS段的初始信息)从磁盘加载到内存中。 38 | 2. **创建PCB**:操作系统为新创建的进程分配一个PCB,并初始化其中的信息。 39 | 3. **分配内存**:操作系统根据进程的需要,在内存中为其分配栈和堆空间。 40 | 4. **初始化寄存器和PC**:将CPU的寄存器和程序计数器初始化为进程的起始状态。 41 | 42 | 至此,一个完整的进程映像就形成了。 43 | 44 | 45 | ### 作用 46 | - **进程的静态描述**:进程映像是进程在某一时刻的“快照”,它完整地记录了进程的所有信息,包括代码、数据、执行状态等。 47 | - **上下文切换的基础**:当操作系统进行进程切换(上下文切换)时,需要保存当前进程的CPU状态(寄存器值、PC等)到其PCB中,并加载下一个要执行进程的CPU状态到CPU中。这些状态信息都属于进程映像的一部分或与其密切相关。 48 | - **内存管理**:操作系统通过进程映像来管理进程的内存空间,包括虚拟内存和物理内存的映射。 49 | - **进程的创建与终止**:进程的创建就是为其分配和初始化进程映像,进程的终止则是释放其占用的进程映像资源。 50 | 51 | --- 52 | 53 | 54 | 55 | ## 常考点分析、历年命题方式与陷阱 56 | - **选择题**:经常考查进程映像的组成部分,例如: 57 | - “以下哪个不属于进程映像的一部分?”(选项可能包含PCB,因为PCB是操作系统维护的,不是进程内存空间的一部分,但它是管理进程映像的核心。) 58 | - “用户程序段的特点是?”(只读,可共享)。 59 | - “堆和栈的区别?”(动态分配vs函数调用,增长方向)。 60 | 61 | **常见陷阱与易错点**: 62 | 63 | 1. **PCB是否属于进程映像?** 64 | - **误解**:有些同学认为PCB是进程的一部分,所以也属于进程映像。 65 | - **正确做法**:**PCB是操作系统为管理进程而创建的数据结构,它不直接包含在进程的地址空间(进程映像的内存区域)中。** 但PCB中记录了进程映像在内存中的位置信息。可以理解为,PCB是操作系统给进程“办理的身份证和档案”,而进程映像是进程实际的“身体和物品”。 66 | 2. **堆和栈的增长方向**: 67 | - **误解**:不清楚堆和栈的增长方向或混淆。 68 | - **正确做法**:**堆通常向高地址方向增长,栈通常向低地址方向增长**。这是为了两者能更好地利用中间的地址空间,避免相互覆盖。 69 | 3. **用户程序(代码段)的特点**: 70 | - **误解**:认为代码段是可写的。 71 | - **正确做法**:**代码段通常是只读的**,以防止程序在执行过程中被意外修改。同时,多个进程可以共享同一个代码段,提高内存利用率。 72 | 73 | --- 74 | 75 | 76 | 77 | ## 边界情况与反例 78 | - **空进程**:一个什么也不做的空进程,其进程映像仍然会包含一个小的代码段(例如一个简单的返回指令),以及最小的栈和必要的PCB信息。 79 | - **共享库/动态链接库**:当多个进程使用同一个动态链接库时,该库的代码段通常是共享的,只会在内存中加载一份,但每个进程会有自己独立的库数据段和栈。这正是进程映像中代码段可共享性的一个具体体现。 80 | 81 | --- 82 | -------------------------------------------------------------------------------- /src/site/notes/408/中断信号的优先级,多重中断的处理.md: -------------------------------------------------------------------------------- 1 | --- 2 | {"dg-publish":true,"dg-permalink":"/408/中断信号的优先级,多重中断的处理","permalink":"/408/中断信号的优先级,多重中断的处理/"} 3 | --- 4 | 5 | 6 | ### 一、中断优先级的确定机制 7 | 8 | #### 1. **硬件优先级编码** 9 | - **中断控制器固定优先级**: 10 | 在传统8259A中断控制器中,IRQ0-IRQ7的优先级固定为递减顺序(IRQ0最高,IRQ7最低)。现代APIC架构支持动态配置优先级寄存器(如LAPIC的TPR,Task Priority Register),通过4位字段定义16级优先级。 11 | - **中断向量表自然顺序**: 12 | 当多个中断具有相同的用户分配优先级时,其冲突通过中断向量表(IVT)地址顺序解决,地址较低的中断优先级更高。例如,x86架构中除零异常(INT 0)的向量地址为0,优先级高于系统调用(INT 80h)。 13 | 14 | 15 | #### 2. **软件动态配置** 16 | - **优先级分组与仲裁**: 17 | 操作系统可通过中断控制器寄存器(如ARM GIC的IPRIORITYR)动态调整中断优先级。例如,Linux内核通过`irq_set_priority()`函数修改中断优先级。 18 | - **任务优先级匹配机制**: 19 | CPU通过TPR寄存器屏蔽低于当前任务优先级的中断。例如,当TPR=0x40时,仅允许优先级高于4(即数值小于4)的中断被处理。 20 | 21 | 22 | #### 3. **设备特性优先级** 23 | - **关键外设高优先级**: 24 | 系统通常将关键设备(如时钟中断、NMI)设为最高优先级。例如,x86架构的时钟中断(IRQ0)优先级高于键盘中断(IRQ1)。 25 | 26 | --- 27 | 28 | 29 | 30 | ### 二、多中断冲突的处理策略 31 | 32 | #### 1. **硬件仲裁机制** 33 | - **抢占式嵌套中断**: 34 | 高优先级中断可抢占低优先级中断处理流程。例如,PSoC® 4处理器支持嵌套中断,允许更高优先级中断在低优先级ISR执行期间被响应。 35 | - **中断屏蔽与排队**: 36 | 中断控制器(如GIC)通过挂起寄存器(Pending Register)记录未处理中断,按优先级排序并逐个提交给CPU。 37 | 38 | 39 | #### 2. **软件调度策略** 40 | - **中断下半部机制**: 41 | 将非紧急处理逻辑延迟到软中断(Softirq)或工作队列(Workqueue)中执行。例如,Linux网卡驱动的NAPI机制通过`napi_schedule()`将数据包处理推迟到软中断上下文。 42 | - **优先级继承与恢复**: 43 | 在实时系统中,通过优先级继承(Priority Inheritance)防止优先级反转。例如,当低优先级任务持有高优先级任务所需资源时,临时提升低优先级任务优先级。 44 | 45 | 46 | #### 3. **异常情况处理** 47 | - **中断丢失风险**: 48 | 若中断控制器未实现排队机制(如部分8位微控制器),多个同优先级中断可能因共享中断线导致丢失。需通过`request_irq()`注册共享中断处理链表解决。 49 | - **死锁预防**: 50 | 在原子上下文中(如ISR),禁止调用可能引起阻塞的操作(如`spin_lock()`无超时版本)。应改用`spin_lock_irqsave()`防止中断嵌套导致死锁。 51 | 52 | --- 53 | 54 | 55 | 56 | ### 三、典型实现与边界条件 57 | 58 | #### 1. **x86架构中断优先级管理** 59 | - **LAPIC优先级控制**: 60 | TPR寄存器的4位优先级字段(P[7:4])定义16级优先级(0-15,数值越小优先级越高)。例如,TPR=0x10时允许处理优先级0-3的中断。 61 | - **中断嵌套示例**: 62 | 当CPU处理低优先级中断(如IRQ11)时,若收到高优先级中断(如IRQ1),LAPIC会触发中断嵌套,保存当前ISR状态并跳转至高优先级ISR。 63 | 64 | 65 | #### 2. **ARM架构中断优先级管理** 66 | - **GIC优先级寄存器**: 67 | 每个中断源对应一个8位优先级寄存器(IPRIORITYR),支持256级优先级。通过`GICD_IPRIORITYRn`寄存器配置,数值越小优先级越高。 68 | - **抢占阈值设置**: 69 | CPU通过写`ICCPMR`寄存器设置中断屏蔽阈值,仅允许优先级高于该阈值的中断被处理。 70 | 71 | 72 | #### 3. **历史差异与误用风险** 73 | - **8259A固定优先级局限性**: 74 | 传统PC中,IRQ0(时钟)优先级固定高于IRQ7(并口),无法动态调整,可能导致非关键中断阻塞关键任务。 75 | - **KPTI对中断延迟的影响**: 76 | Meltdown漏洞修复后的内核页表隔离(KPTI)机制需切换CR3寄存器,增加中断处理延迟约15%。 77 | 78 | --- 79 | 80 | 81 | 82 | ### 四、设计原则与最佳实践 83 | | 原则 | 实现方法 | 84 | |--------------------|--------------------------------------------------------------------------| 85 | | **最小化ISR执行时间** | 将耗时操作(如数据拷贝)移至软中断或工作队列,确保ISR在原子上下文中快速返回 | 86 | | **优先级划分合理性** | 关键中断(如时钟、电源监控)设为最高优先级,避免低优先级中断阻塞系统核心功能 | 87 | | **并发控制** | 使用自旋锁(Spinlock)保护共享数据结构,防止中断嵌套导致的数据竞争 | 88 | | **动态调整能力** | 支持运行时优先级调整(如通过`irq_set_priority()`),适应动态负载变化 | 89 | 90 | **结论**:中断优先级的确定需结合硬件特性(如中断控制器架构)与软件策略(如优先级继承),并通过抢占式嵌套、下半部机制等技术平衡实时性与系统稳定性。开发者必须严格遵循原子上下文约束,合理划分中断处理逻辑,以避免死锁、中断丢失等关键错误。 91 | -------------------------------------------------------------------------------- /src/site/notes/408/浮点数运算的全过程😡.md: -------------------------------------------------------------------------------- 1 | --- 2 | {"dg-publish":true,"dg-permalink":"/408/浮点数运算的全过程😡","permalink":"/408/浮点数运算的全过程😡/"} 3 | --- 4 | 5 | 6 | ### 一、 浮点数加减运算的五个步骤 7 | 8 | #### 步骤 1:对阶 (Exponent Alignment) 9 | 10 | - **目的:** 只有当两个数的阶码相同时,它们的尾数才能直接相加减。对阶就是将其中一个数的阶码变得与另一个数相同,并相应地调整其尾数。 11 | 12 | - **原则:** **小阶向大阶看齐**。 13 | 14 | - 因为如果大阶向小阶看齐,尾数需要左移,可能会导致最高有效位丢失,造成严重误差。而小阶向大阶看齐,尾数是右移,丢失的是最低有效位,对精度的影响较小。 15 | 16 | #### 步骤 2:尾数求和/求差 (Mantissa Calculation) 17 | 18 | - **目的:** 对对阶后的两个尾数执行加法或减法运算。 19 | 20 | - **操作过程:** 将对齐后的两个尾数 Mx​ 和 My′​(或 Mx′​ 和 My​)按照定点数的加减法规则进行运算。减法通常通过加上操作数的补码来实现。 21 | 22 | #### 步骤 3:结果规格化 (Normalization) 23 | 24 | - **目的:** 将运算得到的尾数 Ms​ 转换成规格化形式。 25 | 26 | - **操作过程:** 27 | 28 | - **右规 (Overflow Handling):** 如果尾数运算结果发生溢出(例如,两个正尾数相加,结果符号位为1),则需要将**尾数右移一位,阶码加1**。 29 | 30 | - **左规 (Normalization):** 如果尾数运算结果不满足规格化要求(例如,补码 0.0... 或 1.1...),则需要**尾数左移一位,阶码减1**,此过程可能需要重复多次。 31 | 32 | - **例 (续上):** 33 | 34 | - 上一步得到的尾数 Ms​=0.101001 (补码)。 35 | 36 | - 其符号位为0,数值最高位为1,符号位与数值最高位不同。 37 | 38 | - 已经是规格化形式,无需调整。 39 | 40 | #### 步骤 4:舍入处理 (Rounding) 41 | 42 | - **目的:** 在对阶或右规(尾数右移)的过程中,可能会丢失一部分在尾数表示范围之外的低位。为了补偿这种精度损失,需要进行舍入操作。 43 | 44 | - **舍入方法:** 45 | 46 | 1. **0舍1入法 (恒舍/向零舍入, Round toward Zero):** 这是最简单粗暴的方法,直接将多余的位截断。无论多余位是什么,一概舍去。 47 | 48 | 2. **四舍五入法 (Round to Nearest):** 这是我们最熟悉的方法。当被舍弃的最高位为1时,就在尾数的末位加1。这种方法简单,但可能会引入微小的正向偏差。 49 | 50 | 3. **奇偶舍入法 (Round to Nearest, ties to Even):** 这是 **IEEE 754标准默认** 的方法,也是考研的重点。 51 | 52 | - **规则:** "四舍六入五看齐,五后非零就进一,五后为零看奇偶,五前为奇要进一,五前为偶舍去。" 53 | 54 | - **简化理解:** 55 | 56 | - 被舍弃的部分 > 一半,则入。 57 | 58 | - 被舍弃的部分 < 一半,则舍。 59 | 60 | - 被舍弃的部分 = 一半,则看保留部分的最低位,如果为1(奇数),则入;如果为0(偶数),则舍。目的是让最终结果的最低位为0。 61 | 62 | #### 步骤 5:溢出判断 (Overflow Detection) 63 | 64 | - **目的:** 检查最终的运算结果是否超出了机器浮点数能表示的范围。这个溢出指的是**阶码溢出**,不要与步骤3中的**尾数溢出**混淆。 65 | 66 | - **判断方法:** 67 | 68 | - 在规格化和舍入(可能导致尾数再次溢出并右规)之后,检查最终的阶码`E`。 69 | 70 | - **上溢 (Overflow):** 阶码 `E` 大于了它能表示的最大值 (例如,IEEE 754单精度中,实际阶码 > 127)。 71 | 72 | - **下溢 (Underflow):** 阶码 `E` 小于了它能表示的最小值 (例如,IEEE 754单精度中,实际阶码 < -126)。 73 | 74 | 75 | --- 76 | 77 | ### 二、 溢出处理与常考点 78 | 79 | #### 1. 两种溢出的辨析 (高频考点) 80 | 81 | |类型|发生阶段|现象 (以补码为例)|处理方式|结果| 82 | |---|---|---|---|---| 83 | |**尾数溢出**|步骤2: 尾数计算|两个正数相加得负数 (0.1...+0.1...→1.0...); 两个负数相加得正数 (1.0...+1.0...→0.0...)|**右规** (尾数右移,阶码加1)|属于**中间过程**,不是最终错误| 84 | |**阶码溢出**|步骤5: 最终判断|最终的阶码超出了表示范围|置**溢出标志位**,并进行**中断**处理|最终结果无效| 85 | #### 2. 溢出处理 86 | 87 | - **上溢处理:** 当发生阶码上溢时,机器会停止运算,进行**溢出中断处理**。通常将结果置为**无穷大**(+∞ 或 −∞)。 88 | 89 | - 在IEEE 754中,就是将结果的阶码部分置为全1,尾数部分置为全0。 90 | 91 | - **下溢处理:** 92 | 93 | - **按零处理:** 传统的处理方法是直接将结果视为**机器零**。这会引入较大误差。 94 | 95 | - **非规格化数处理 (IEEE 754):** 当发生下溢时,结果不直接置为0,而是作为**非规格化数**(Denormalized Number)处理。这使得数值可以平滑地过渡到0,提高了精度,是“平滑下溢”的核心思想。 96 | 97 | -------------------------------------------------------------------------------- /src/site/_includes/layouts/index.njk: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {% if title %}{{ title }}{% else %}{{ page.fileSlug }}{% endif %} 5 | {%include "components/pageheader.njk"%} 6 | {% for imp in dynamics.common.head %} 7 | {% include imp %} 8 | {% endfor %} 9 | {% for imp in dynamics.index.head %} 10 | {% include imp %} 11 | {% endfor %} 12 | 13 | 14 | {%include "components/notegrowthhistory.njk"%} 15 | {% if settings.dgShowFileTree !== true %} 16 | {%include "components/navbar.njk"%} 17 | {%else%} 18 | {%include "components/filetree.njk"%} 19 | {% endif %} 20 | {% if settings.dgEnableSearch === true %} 21 | {%include "components/searchContainer.njk"%} 22 | {% endif %} 23 |
24 |
25 | {% if settings.dgShowInlineTitle === true %} 26 |

{{ noteTitle }}

27 | {% endif %} 28 | 29 |
30 | {% if settings.dgShowTags === true and tags %} 31 |
32 | {% for tag in tags %} 33 | {% if tag != 'gardenEntry' and tag !='note' %} 34 | 35 | #{{tag}} 36 | 37 | {% endif %} 38 | {% endfor %} 39 |
40 | {% endif %} 41 |
42 | {% for imp in dynamics.common.header %} 43 | {% include imp %} 44 | {% endfor %} 45 | {% for imp in dynamics.index.header %} 46 | {% include imp %} 47 | {% endfor %} 48 |
49 | {% for imp in dynamics.common.beforeContent %} 50 | {% include imp %} 51 | {% endfor %} 52 | {% for imp in dynamics.index.beforeContent %} 53 | {% include imp %} 54 | {% endfor %} 55 | {{ content | hideDataview | taggify | link | safe}} 56 | {% for imp in dynamics.common.afterContent %} 57 | {% include imp %} 58 | {% endfor %} 59 | {% for imp in dynamics.index.afterContent %} 60 | {% include imp %} 61 | {% endfor %} 62 |
63 | 64 | {% if settings.dgShowBacklinks === true or settings.dgShowLocalGraph === true or settings.dgShowToc === true%} 65 | {%include "components/sidebar.njk" %} 66 | {%endif%} 67 | 68 | {% if settings.dgLinkPreview === true %} 69 | {%include "components/linkPreview.njk"%} 70 | {% endif %} 71 | {% for imp in dynamics.common.footer %} 72 | {% include imp %} 73 | {% endfor %} 74 | {% for imp in dynamics.index.footer %} 75 | {% include imp %} 76 | {% endfor %} 77 | {%include "components/lucideIcons.njk"%} 78 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /src/site/notes/408/STDM的异步你真的知道是什么的异步吗🤔.md: -------------------------------------------------------------------------------- 1 | --- 2 | {"dg-publish":true,"dg-permalink":"/408/STDM的异步你真的知道是什么的异步吗🤔","permalink":"/408/STDM的异步你真的知道是什么的异步吗🤔/"} 3 | --- 4 | 5 | 6 | ### 一、原理 7 | 8 | 要彻底理解STDM,最好的方法就是将它与我们熟悉的(同步)时分复用(TDM)**进行对比。 9 | 10 | #### 1. 回顾:传统的TDM(同步时分复用) 11 | 12 | 想象一个有4条车道(用户A, B, C, D)的收费站,要汇入一条主干道。TDM的规则是: 13 | 14 | - **严格轮流**:主干道的时间被切分成一个个“帧”,每个帧里有4个固定的“时隙”,分别**永久预留**给A, B, C, D。 15 | 16 | - **不管你来不来,位置都给你留着**:即使某一时刻C车道没有车,C的时隙也会在主干道上**空着跑过去**。 17 | 18 | - **结果**:信道利用率低,存在大量浪费。 19 | 20 | 21 | ``` 22 | TDM 帧结构: 23 | | A1 | B1 | C1 | D1 | A2 | B2 | C2 | D2 | ... 24 | ^ ^ ^ ^ 25 | 时隙1 时隙2 时隙3 时隙4 (固定分配) 26 | 27 | 如果C没有数据,则C1时隙为空,造成浪费。 28 | ``` 29 | 30 | #### 2. 登场:STDM(统计时分复用) 31 | 32 | STDM则像一个**智能的动态调度系统**,它的规则是: 33 | 34 | - **按需服务,先到先得**:不再为每个用户预留固定时隙。而是谁有数据要发,就立刻把它装入下一个可用的时隙。 35 | 36 | - **插队抢跑,提高效率**:如果C没有数据,系统会直接跳过C,让后面有数据的用户(比如D或下一个A)立刻“插队”使用这个时隙。 37 | 38 | - **结果**:信道几乎永远是“满载”的,利用率极高。 39 | 40 | 41 | #### 3. STDM是如何实现的? 42 | 43 | STDM的实现依赖于一个核心设备——**统计复用器(Statistical Multiplexer)**,其实现过程如下: 44 | 45 | 1. **设置缓冲区**:统计复用器为每条输入线路都提供一个缓冲区,用于暂存用户发来的数据。 46 | 47 | 2. **动态扫描**:复用器不断地循环扫描各条输入线路的缓冲区。 48 | 49 | 3. **按需组帧**: 50 | 51 | - 一旦发现某个用户的缓冲区中有数据,就立即取出这些数据。 52 | 53 | - **关键一步**:为取出的数据块附加上一个**地址信息**(或称为“标志”),指明这个数据块是属于哪个用户的。 54 | 55 | - 将“地址+数据”作为一个整体,放入一个STDM帧中。 56 | 57 | 4. **发送帧**:将组装好的STDM帧发送到高速线路上。 58 | 59 | 60 | 为什么必须加地址信息? 61 | 62 | 因为在STDM中,时隙的位置不再与用户身份绑定。接收端收到一个数据块时,它无法像TDM那样通过“数位置”(比如第3个时隙就是C的)来判断数据来源。因此,必须在数据中明确地“贴上标签”,告诉接收端“我是A发来的”、“我是D发来的”。 63 | 64 | ### 二、图示说明 65 | 66 | **STDM工作原理图** 67 | 68 | ``` 69 | +-----------+ 70 | A --> | Buffer A | --\ 71 | +-----------+ \ 72 | +-----------+ \ +-------------------+ 高速线路 73 | B --> | Buffer B | ------->| 统计复用器 |------------> | D1 | C1 | B1 | A1 | ... 74 | +-----------+ / | (扫描、加地址、组帧)| 75 | +-----------+ / +-------------------+ 76 | C --> | Buffer C | --/ 77 | +-----------+ 78 | +-----------+ 79 | D --> | Buffer D | --/ 80 | +-----------+ 81 | 82 | 假设某一时刻,A, B, C, D都有数据,则STDM帧可能是 | A1 | B1 | C1 | D1 | 83 | 假设下一时刻,只有A和D有数据,则STDM帧可能是 | A2 | D2 | A3 | D3 | (B和C的时隙被动态利用了) 84 | ``` 85 | 86 | - **注意**:这里的`A1`, `B1`等都代表一个包含**地址和数据**的完整数据单元。 87 | 88 | |特性|(同步)TDM|统计时分复用 (STDM)| 89 | |---|---|---| 90 | |**分配方式**|**静态分配**,预留固定时隙|**动态分配**,按需分配时隙| 91 | |**信道利用率**|低,有数据空闲时浪费严重|**高**,信道利用率得到极大提升| 92 | |**帧结构**|时隙位置隐含地址,无额外开销|**必须包含地址字段**,有额外开销| 93 | |**总速率关系**|输出线路速率 ≥ 所有输入线路速率之和|输出线路速率可以 < 所有输入线路速率之和| 94 | |**是否拥塞**|永远不会拥塞|当瞬时输入速率 > 输出速率时,**可能因缓冲区满而拥塞丢包**| 95 | ### 三、易错点 96 | 97 | - **STDM的开销**:STDM的高效率是有代价的,这个代价就是每个数据单元都必须增加地址信息,这本身是一种**开销(Overhead)**。当传输的数据块本身很小时,地址信息的占比就会变大,从而降低了实际的净荷效率。 98 | 99 | - **拥塞问题**:STDM的设计基于一个“统计”假设,即所有用户不太可能在同一时刻都达到峰值速率。但万一这个小概率事件发生了呢?如果所有用户同时以最大速率发送数据,其总和超过了输出线路的承载能力,统计复用器的缓冲区就会被迅速填满,最终导致**数据丢失(丢包)**。这是STDM为了追求高效率而必须承担的风险。 100 | 101 | - **名称混淆**:“异步时分复用”这个别名容易让人与物理层的“异步传输”(使用起始/停止位)混淆。一定要分清:STDM的“异步”指的是**时隙分配的非同步性**,而不是比特传输的同步方式。 102 | -------------------------------------------------------------------------------- /src/helpers/linkUtils.js: -------------------------------------------------------------------------------- 1 | const wikiLinkRegex = /\[\[(.*?\|.*?)\]\]/g; 2 | const internalLinkRegex = /href="\/(.*?)"/g; 3 | 4 | function caselessCompare(a, b) { 5 | return a.toLowerCase() === b.toLowerCase(); 6 | } 7 | 8 | function extractLinks(content) { 9 | return [ 10 | ...(content.match(wikiLinkRegex) || []).map( 11 | (link) => 12 | link 13 | .slice(2, -2) 14 | .split("|")[0] 15 | .replace(/.(md|markdown)\s?$/i, "") 16 | .replace("\\", "") 17 | .trim() 18 | .split("#")[0] 19 | ), 20 | ...(content.match(internalLinkRegex) || []).map( 21 | (link) => 22 | link 23 | .slice(6, -1) 24 | .split("|")[0] 25 | .replace(/.(md|markdown)\s?$/i, "") 26 | .replace("\\", "") 27 | .trim() 28 | .split("#")[0] 29 | ), 30 | ]; 31 | } 32 | 33 | function getGraph(data) { 34 | let nodes = {}; 35 | let links = []; 36 | let stemURLs = {}; 37 | let homeAlias = "/"; 38 | (data.collections.note || []).forEach((v, idx) => { 39 | let fpath = v.filePathStem.replace("/notes/", ""); 40 | let parts = fpath.split("/"); 41 | let group = "none"; 42 | if (parts.length >= 3) { 43 | group = parts[parts.length - 2]; 44 | } 45 | nodes[v.url] = { 46 | id: idx, 47 | title: v.data.title || v.fileSlug, 48 | url: v.url, 49 | group, 50 | home: 51 | v.data["dg-home"] || 52 | (v.data.tags && v.data.tags.indexOf("gardenEntry") > -1) || 53 | false, 54 | outBound: extractLinks(v.template.frontMatter.content), 55 | neighbors: new Set(), 56 | backLinks: new Set(), 57 | noteIcon: v.data.noteIcon || process.env.NOTE_ICON_DEFAULT, 58 | hide: v.data.hideInGraph || false, 59 | }; 60 | stemURLs[fpath] = v.url; 61 | if ( 62 | v.data["dg-home"] || 63 | (v.data.tags && v.data.tags.indexOf("gardenEntry") > -1) 64 | ) { 65 | homeAlias = v.url; 66 | } 67 | }); 68 | Object.values(nodes).forEach((node) => { 69 | let outBound = new Set(); 70 | node.outBound.forEach((olink) => { 71 | let link = (stemURLs[olink] || olink).split("#")[0]; 72 | outBound.add(link); 73 | }); 74 | node.outBound = Array.from(outBound); 75 | node.outBound.forEach((link) => { 76 | let n = nodes[link]; 77 | if (n) { 78 | n.neighbors.add(node.url); 79 | n.backLinks.add(node.url); 80 | node.neighbors.add(n.url); 81 | links.push({ source: node.id, target: n.id }); 82 | } 83 | }); 84 | }); 85 | Object.keys(nodes).map((k) => { 86 | nodes[k].neighbors = Array.from(nodes[k].neighbors); 87 | nodes[k].backLinks = Array.from(nodes[k].backLinks); 88 | nodes[k].size = nodes[k].neighbors.length; 89 | }); 90 | return { 91 | homeAlias, 92 | nodes, 93 | links, 94 | }; 95 | } 96 | 97 | exports.wikiLinkRegex = wikiLinkRegex; 98 | exports.internalLinkRegex = internalLinkRegex; 99 | exports.extractLinks = extractLinks; 100 | exports.getGraph = getGraph; 101 | -------------------------------------------------------------------------------- /src/site/notes/408/设备驱动程序的生命周期😮‍💨.md: -------------------------------------------------------------------------------- 1 | --- 2 | {"dg-publish":true,"dg-permalink":"/408/设备驱动程序的生命周期😮‍💨","permalink":"/408/设备驱动程序的生命周期😮‍💨/"} 3 | --- 4 | 5 | 6 | 7 | ### **设备驱动程序的完整工作流程** 8 | 9 | #### **第零步:驱动的初始化与注册 (在系统启动或模块加载时)** 10 | 11 | 在任何I/O操作发生前,驱动程序必须先让内核“认识”它。 12 | 13 | 1. **加载与初始化**: 系统启动时或管理员加载模块时,内核会运行驱动程序的`init()`初始化函数。 14 | 15 | 2. **探测硬件**: 驱动程序会探测总线,检查它所负责的硬件设备是否存在且工作正常。 16 | 17 | 3. **注册接口**: 这是最关键的一步。驱动程序向内核注册一个包含一系列**函数指针**的结构体(在Linux中称为`file_operations`)。 18 | 19 | - **作用**: 这相当于驱动程序向内核立下字据:“尊敬的内核,我是硬盘驱动。以后只要有针对硬盘的`open`, `read`, `write`, `ioctl`等操作请求,请调用我这里的`my_disk_open`, `my_disk_read`, `my_disk_write`等函数地址。” 20 | 21 | - 从此,内核就知道该如何将上层的通用I/O请求转发给这个具体的驱动程序了。 22 | 23 | 24 | --- 25 | 26 | ## _以下流程是一个具体的`read`请求到达后的生命周期_ 27 | 28 | #### **第一步:接收上层请求(“Top Half”的开始)** 29 | 30 | 1. 一个进程发起`read`系统调用,经过**设备无关的I/O层**处理后,内核确定需要从磁盘读取某个物理块。 31 | 32 | 2. 设备无关层会调用在第零步中注册的函数指针,即调用磁盘驱动程序的`my_disk_read()`函数,并将请求的块号、目标内存缓冲区地址等信息作为参数传入。 33 | 34 | 3. **驱动程序代码开始执行。** 这部分由系统调用直接触发,在进程的上下文中运行,我们称之为驱动的**“上半部 (Top Half)”**。 35 | 36 | 37 | #### **第二步:参数校验与设备状态检查** 38 | 39 | 1. 驱动程序首先会检查上层传来的参数是否合法。例如,请求的块号是否超出了磁盘的容量范围? 40 | 41 | 2. 接着,检查硬件设备当前是否正忙于处理上一个请求。如果设备忙,新的请求可能会被放入一个**请求队列**中等待。如果设备空闲,则继续。 42 | 43 | 44 | #### **第三步:对硬件编程(核心翻译工作)** 45 | 46 | 这是驱动程序的核心价值所在。它需要将上层“读逻辑块X”这样抽象的命令,翻译成磁盘控制器能听懂的“电子语言”。 47 | 48 | 1. **准备DMA传输**: 驱动程序向内核申请一块用于DMA(直接内存访问)的内存缓冲区,并获取其物理地址。 49 | 50 | 2. **设置控制器寄存器**: 驱动程序通过特定的I/O端口地址,向磁盘控制器的硬件寄存器中写入一系列值,例如: 51 | 52 | - **目标内存地址寄存器**: 写入DMA缓冲区的物理地址。 53 | 54 | - **扇区计数寄存器**: 写入要读取的扇区数量(例如一个块包含8个扇区,就写入8)。 55 | 56 | - **LBA寄存器**: 写入要读取的逻辑块地址(LBA)。 57 | 58 | - **命令寄存器**: 最后,向命令寄存器写入**“开始读”**的命令码。 59 | 60 | 61 | #### **第四步:阻塞当前进程** 62 | 63 | - 向命令寄存器写入命令后,硬件就开始了漫长的物理操作(寻道、旋转、读取)。这个过程可能耗时数毫秒,CPU不能在此空等。 64 | 65 | - 驱动程序会调用内核的调度器函数,将发起此次I/O请求的进程状态设置为**“阻塞(Blocked)”或“等待(Waiting)”**,并将其放入等待队列。 66 | 67 | - 然后,驱动程序的`my_disk_read()`函数**返回**,将CPU控制权交还给调度器。调度器会选择另一个处于“就绪”状态的进程投入运行。 68 | 69 | 70 | _(至此,上半部工作完成,CPU已转去处理其他任务,静待硬件佳音)_ 71 | 72 | --- 73 | 74 | #### **第五步:处理硬件中断(“Bottom Half”的开始)** 75 | 76 | 1. 磁盘硬件完成了数据读取,并通过DMA控制器将数据成功写入到了第三步中指定的内存DMA缓冲区。 77 | 78 | 2. 为了通知任务完成,磁盘控制器会向CPU发送一个**中断信号**。 79 | 80 | 3. CPU无论正在做什么,都会立即暂停,保存当前现场,然后根据中断向量表,跳转到该中断对应的**中断服务例程(ISR)**——这正是磁盘驱动程序预先注册好的中断处理函数。 81 | 82 | 4. 这部分在中断上下文中运行的代码,我们称之为驱动的**“下半部 (Bottom Half)”**。 83 | 84 | 85 | #### **第六步:中断善后与唤醒进程** 86 | 87 | 中断处理程序(下半部)必须**执行得非常快**。它通常会做以下几件事: 88 | 89 | 1. **读取状态**: 从磁盘控制器的状态寄存器中读取I/O操作的结果,检查是否发生了错误。 90 | 91 | 2. **数据处理**: 如果有错误,则记录错误信息。如果成功,则通知上层数据已准备好。 92 | 93 | 3. **唤醒进程**: 调用内核函数,将在第四步中被阻塞的那个进程从等待队列中移出,将其状态改为**“就绪(Ready)”**,并放入就绪队列,等待调度器再次垂青。 94 | 95 | 4. **返回**: 从中断处理程序返回。CPU恢复之前被中断的现场,继续执行。 96 | 97 | 98 | #### **第七步:请求完成与返回用户** 99 | 100 | - 当调度器再次选择运行我们那个刚刚被唤醒的进程时,它的执行会从当初`my_disk_read()`函数中被阻塞的地方继续。 101 | 102 | - 此时,驱动程序代码知道I/O已经完成,它会进行一些清理工作。 103 | 104 | - 数据此时已经位于内核的DMA缓冲区中,设备无关层会负责将其从内核缓冲区**复制**到用户最初指定的缓冲区`buf`中。 105 | 106 | - 最后,整个系统调用完成,返回到用户态,用户程序从`read()`函数调用处继续执行,并在`buf`中得到了它想要的数据。 -------------------------------------------------------------------------------- /src/site/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/site/notes/408/磁盘的低级格式化和高级格式化都进行了什么工作.md: -------------------------------------------------------------------------------- 1 | --- 2 | {"dg-publish":true,"dg-permalink":"/408/磁盘的低级格式化和高级格式化都进行了什么工作","permalink":"/408/磁盘的低级格式化和高级格式化都进行了什么工作/"} 3 | --- 4 | 5 | 6 | ### **一、 低级格式化 (Low-Level Formatting / 物理格式化)** 7 | 8 | 低级格式化是一个**面向硬件**的、**破坏性**的过程。它为磁盘的物理表面建立了最基础的“跑道和标线”,使得磁盘控制器能够知道该如何在这张“白纸”上定位和读写数据。 9 | 10 | **在现代,低级格式化通常在硬盘出厂时就已经由制造商完成,用户基本不需要也无法再进行真正的低级格式化。** 11 | 12 | 低级格式化主要进行了以下工作: 13 | 14 | 1. **划分磁道和扇区 (Creating Tracks and Sectors)** 15 | 16 | - 在磁盘的每个盘面上,划分出若干个同心圆,这就是**磁道 (Track)**。 17 | 18 | - 将每个磁道进一步划分为若干个固定大小的弧段,这就是**扇区 (Sector)**。扇区是磁盘进行物理读写的最小单位(通常为512字节或4KB)。 19 | 20 | 2. **写入控制结构 (Writing Control Structures)** 21 | 22 | - 为每个扇区写入**“头(Header)”**、**“尾(Trailer)”**和**“扇区间的间隙(Gap)”**。这些是给磁盘控制器看的“路标”,而不是给用户存数据的。 23 | 24 | - **头 (Header)**: 包含扇区的唯一地址信息(如柱面号、磁头号、扇区号)、同步信息等。 25 | 26 | - **尾 (Trailer)**: 主要包含**错误校验码 (ECC - Error-Correcting Code)**,用于在读取数据时校验数据是否出错。 27 | 28 | - 经过这一步,原本光滑的盘片表面就被“画”上了一个个带有地址和校验信息的、可供读写的“小格子”。 29 | 30 | 3. **坏道检测与映射 (Bad Sector Mapping)** 31 | 32 | - 在格式化过程中,会对磁盘表面进行检测,找出有物理缺陷、无法可靠存储数据的“坏扇区”(即**坏道**)。 33 | 34 | - 将这些坏道的地址记录在一个特殊的列表(如`P-list`或`G-list`)中,并用备用扇区进行替换。这样,磁盘控制器在后续操作中就会自动跳过这些坏道。 35 | 36 | 37 | **总结**: 低级格式化的工作对象是**整个物理硬盘**,其目的是**建立磁盘的物理结构**,让硬件可以工作。它不关心操作系统,不建立任何文件系统。这个过程会**彻底清空**磁盘上的所有数据,且通常是不可恢复的。 38 | 39 | --- 40 | 41 | ### **二、 高级格式化 (High-Level Formatting / 逻辑格式化)** 42 | 43 | 高级格式化是一个**面向操作系统**的、在已有分区上进行的**逻辑构建**过程。它的目的是在一个已经过低级格式化和分区的磁盘空间上,**建立文件系统的“账本和档案柜”**,使得操作系统能够在这里存储、组织和查找文件。 44 | 45 | 我们平常在Windows里“格式化一个D盘”或在Linux里`mkfs.ext4 /dev/sda1`,做的都是高级格式化。 46 | 47 | 高级格式化主要进行了以下工作: 48 | 49 | 1. **划分磁盘块 (Dividing into Blocks)** 50 | 51 | - 文件系统会以一个或多个连续的扇区组成一个**块 (Block)** 或 **簇 (Cluster)**。块是文件系统进行数据分配和I/O操作的基本单位。 52 | 53 | 2. **建立文件系统的核心数据结构**: 54 | 55 | - 这是高级格式化最核心的工作。它会在分区的特定位置写入一套完整的文件系统“管理账本”。以一个典型的UNIX文件系统(如ext4)为例,会创建: 56 | 57 | - **引导块 (Boot Block)**: 位于分区的最开始,包含了引导加载程序(boot loader)的信息,用于启动操作系统。 58 | 59 | - **超级块 (Super Block)**: 包含了整个文件系统的“全局”信息,如文件系统类型、块大小、i-node数量、空闲块数量等。超级块是文件系统的“户口本”,如果它损坏,整个文件系统都可能无法识别。 60 | 61 | - **空闲空间管理信息**: 用于记录哪些磁盘块是空闲的。这可以是**位图 (Bitmap)** 的形式,也可以是**空闲块链表**(如成组链接法所用的结构)。 62 | 63 | - **i-node区 (i-node Area)**: 划出一片区域,专门存放文件系统中所有文件的**索引节点(i-node)**。每个文件或目录都对应一个i-node,记录了文件的元数据(大小、权限、时间戳、数据块指针等)。 64 | 65 | - **根目录 (Root Directory)**: 创建文件系统的第一个目录——根目录。它本身也是一个文件,其i-node和数据块被创建,并至少包含`.`和`..`两个目录项。 66 | 67 | 68 | **总结**: 高级格式化的工作对象是**一个已存在的分区**,其目的是**建立文件系统的逻辑结构**。它让我们熟悉的`C:`盘、`D:`盘不再是一片混沌的存储空间,而是变成了一个有组织、有结构、可以存放文件和目录的“档案室”。 69 | 70 | - **快速格式化**: 只重写文件系统的核心管理结构(如FAT表、MFT或i-node区),而不清空数据区。这使得格式化速度很快,但旧数据仍可通过数据恢复软件找回。 71 | 72 | - **完全格式化**: 除了重写管理结构,还会检查整个分区的所有扇区是否有坏道,并用0填充数据区,更安全但耗时更长。 73 | 74 | 75 | --- 76 | 77 | ### **对比总结** 78 | 79 | |特性|低级格式化 (物理格式化)|高级格式化 (逻辑格式化)| 80 | |---|---|---| 81 | |**操作对象**|**整个物理硬盘**|**硬盘上的一个分区**| 82 | |**工作层面**|**硬件层面**,面向磁盘控制器|**操作系统层面**,面向文件系统| 83 | |**核心工作**|**划分磁道/扇区**,建立物理寻址基础|**创建文件系统结构**(引导块、超级块、i-node、目录等)| 84 | |**数据影响**|**彻底、物理性地清除**所有数据|**逻辑上清除**数据(快速格式化)或覆盖数据(完全格式化)| 85 | |**执行者**|**硬盘制造商**|**操作系统或用户**| -------------------------------------------------------------------------------- /src/site/_includes/components/filetree.njk: -------------------------------------------------------------------------------- 1 | {% macro menuItem(fileOrFolderName, fileOrFolder, step, currentPath) %} 2 | {%if fileOrFolder.isNote or fileOrFolder.isFolder%} 3 | 22 | {%endif%} 23 | {% endmacro %} 24 | 25 |
28 | 29 |
30 | {%include "components/filetreeNavbar.njk"%} 31 |
32 | 33 | 34 | 35 | 56 |
57 | -------------------------------------------------------------------------------- /src/site/notes/408/OSPF&BGP:OpenFlow我们会死吗😭.md: -------------------------------------------------------------------------------- 1 | --- 2 | {"dg-publish":true,"dg-permalink":"/408/OSPF&BGP:OpenFlow我们会死吗😭","permalink":"/408/OSPF&BGP:OpenFlow我们会死吗😭/"} 3 | --- 4 | 5 | 6 | 简单来说,答案是:**不完全是取代关系,而是一种根本性的架构变革。OpenFlow将传统路由协议的“决策”功能从路由器本身抽离,集中到了一个“大脑”中,从而改变了网络的控制方式。** 7 | 8 | ### 一、 传统路由协议:分布式的“割据” 9 | 10 | 在传统网络中,每一台路由器都是一个独立的“诸侯”,它既有负责打仗的“军队”(**数据平面**,负责根据指令快速转发数据包),也有负责思考战略的“军师”(**控制平面**,负责运行路由协议)。 11 | 12 | - **工作方式**: 13 | 14 | 1. **分布式智能**:每台路由器独立运行路由协议(如内部的OSPF、外部的BGP)。 15 | 16 | 2. **邻里交谈**:它们通过与相邻路由器“交谈”(交换路由信息),逐步学习并构建出自己视角下的“网络地图”。 17 | 18 | 3. **本地决策**:当收到一个数据包时,路由器根据自己本地维护的路由表(这份地图),独立做出下一跳的转发决策。 19 | 20 | - **特点**: 21 | 22 | - **健壮可靠**:去中心化的设计使得网络非常健壮,单点故障影响有限。 23 | 24 | - **配置复杂**:网络策略的变更需要在大量设备上逐一进行配置,效率低下且容易出错。 25 | 26 | - **视野局限**:每台路由器都只有“局部视野”,无法从全局视角进行最优的路径规划和流量调度。 27 | 28 | 29 | **传统路由协议(如OSPF/BGP)的角色**:它们就是这些“军师”们之间沟通、协商、学习网络地图时所使用的**语言和规则**。 30 | 31 | --- 32 | 33 | ### 二、 OpenFlow与SDN:中央集权的“帝国时代” 34 | 35 | **软件定义网络(Software-Defined Networking, SDN)** 是一种全新的网络架构思想,它提出要将“军师”和“军队”彻底分家。 36 | 37 | - **核心思想**:**控制平面与数据平面分离**。 38 | 39 | 1. **数据平面(军队)**:网络中的交换机、路由器被“简化”成纯粹的转发设备(称为**转发元件**),只负责忠实地执行指令。 40 | 41 | 2. **控制平面(大脑/中央司令部)**:所有“思考”和“决策”的工作,全部集中到一个基于软件的**SDN控制器**上。 42 | 43 | - OpenFlow的角色: 44 | 45 | OpenFlow协议,就是这个“中央司令部”向“军队”下达命令时所使用的标准通信协议。它是一种南向接口(Southbound Interface)。 46 | 47 | - 控制器通过OpenFlow协议,向网络中的交换机下发**流表(Flow Table)**。 48 | 49 | - 流表精确地定义了“符合什么样特征(如源IP、目的端口等)的数据包,应该执行何种操作(如转发到端口3、丢弃、修改头部等)”。 50 | 51 | - 交换机收到数据包后,不再自行思考,而是机械地查询流表并执行相应操作。 52 | 53 | 54 | **所以,OpenFlow本身不是路由协议,它是实现SDN集中控制的一种工具/语言。** 55 | 56 | --- 57 | 58 | ### 三、 OpenFlow与路由协议的真实关系 59 | 60 | #### 1. 架构上的“革命”而非功能的“替换” 61 | 62 | 在纯粹的SDN网络中,路由计算的逻辑被移入到了SDN控制器中。控制器可以基于全局网络拓扑(通过LLDP等协议发现),运行传统的路由算法(如Dijkstra),或者更复杂的自定义算法,来计算出最优路径。然后,它将计算结果转化为具体的流表规则,通过OpenFlow下发到各个交换机。 63 | 64 | 在这种模式下,路由器不再需要运行OSPF等协议,因为路径计算已经由“中央大脑”完成了。从这个角度看,**SDN控制器中的“路由应用”取代了分布式运行的“路由协议”**。OpenFlow则负责将这一决策结果传达下去。 65 | 66 | #### 2. “竞合”与“共存”的混合模式 67 | 68 | 在现实世界,尤其是大型网络中,纯粹的SDN网络很少见。更多的是SDN网络与传统网络共存的混合模式。在这种模式下,OpenFlow和传统路由协议可以协同工作。 69 | 70 | - **场景:SDN网络与外部互联网的边界** 71 | 72 | - SDN网络的边界设备(可以是支持OpenFlow的路由器)可以同时运行**BGP协议**。 73 | 74 | - 它通过BGP与外部的传统路由器“交谈”,学习来自互联网的路由信息。 75 | 76 | - 然后,它将这些路由信息上报给SDN控制器。 77 | 78 | - SDN控制器接收到这些外部路由后,结合其掌握的内部网络全局状态(如链路负载、时延等),做出更智能、更精细的流量工程决策,再通过OpenFlow协议调整内部网络的转发路径。 79 | 80 | 81 | 在这种模式下,BGP负责“对外沟通、获取情报”,SDN控制器负责“内部的全局运筹帷幄”,OpenFlow负责“命令的上传下达”。它们不是取代关系,而是**分工协作、优势互补**的关系。 82 | 83 | ### 四、 总结对比 84 | 85 | |特性|传统路由协议 (OSPF, BGP等)|OpenFlow / SDN| 86 | |---|---|---| 87 | |**控制平面**|**分布式**,位于每一台网络设备上|**集中式**,位于独立的SDN控制器上| 88 | |**角色定位**|定义**设备间**如何交换路由信息,以**构建转发表**的协议|定义**控制器与设备间**如何通信,以**下发流表**的协议| 89 | |**决策依据**|主要基于**目的IP地址**|可基于任意的**流信息**(L2/L3/L4多层信息)| 90 | |**网络视野**|**局部视野**,设备只了解其邻居和学习到的路径|**全局视野**,控制器俯瞰整个网络拓扑和状态| 91 | |**灵活性**|较低,策略变更需逐台配置|**极高**,可通过软件编程实现任意复杂的网络逻辑| 92 | |**关系**|**不是简单的取代关系**。SDN的路由应用在功能上取代了路由协议,而在混合网络中,两者可以**共存与协作**。|| 93 | 94 | **结论**:OpenFlow的出现,并非为了在功能上“一对一”地替换掉OSPF或BGP,而是伴随SDN架构,从根本上改变了网络的控制范式。它将路由的智能从分散的个体手中收归中央,实现了全局的视野和灵活的编程能力,从而开启了网络自动化和智能化的新时代。在可预见的未来,两者长期共存、协同工作的混合模式将是主流。 -------------------------------------------------------------------------------- /src/site/notes/408/IO层次结构以及每层的作用.md: -------------------------------------------------------------------------------- 1 | --- 2 | {"dg-publish":true,"dg-permalink":"/408/IO层次结构以及每层的作用","permalink":"/408/IO层次结构以及每层的作用/"} 3 | --- 4 | 5 | 6 | ### **I/O层次结构及各层作用** 7 | 8 | #### **第一层:用户层I/O软件 (User-Level I/O Software)** 9 | 10 | - **作用**: 这一层是与用户程序直接交互的部分。它为程序员提供了简单、易用的接口(API),并将这些调用转换为对操作系统的服务请求(即系统调用)。它还可能涉及数据的格式化和用户态的缓冲。 11 | 12 | - **在本例中的作用**: 13 | 14 | 1. 程序员编写的代码是 `read(fd, buf, 100)`。这是一个标准的库函数调用。 15 | 16 | 2. C库(`libc`)中的`read`函数实现,会将这些参数(文件描述符、缓冲区地址、字节数)打包好,然后通过一个**陷入(trap)指令**,触发一个**系统调用**,请求内核提供读文件服务。 17 | 18 | 3. **简单来说,这一层就是我们编程时使用的 `printf`、`scanf`、`read`、`write` 等函数,它们是通往操作系统内核的大门。** 19 | 20 | 21 | #### **第二层:设备无关的操作系统软件 (Device-Independent OS Software)** 22 | 23 | - **作用**: 这是操作系统内核中处理I/O的核心部分,其目标是提供一个统一的框架来管理所有设备,而无需关心具体设备的细节。它的职责包括: 24 | 25 | - **统一接口**: 为所有设备驱动提供一个统一的接口(如都支持`open`, `read`, `write`等)。 26 | 27 | - **设备命名与保护**: 将文件路径(如`/home/user/data.txt`)映射到具体的设备和文件。 28 | 29 | - **缓冲与高速缓存 (Buffering/Caching)**: 为了提高性能,实现内核I/O缓冲区(页高速缓存),减少对物理设备的访问次数。 30 | 31 | - **差错处理**: 提供通用的错误报告和处理框架。 32 | 33 | - **在本例中的作用**: 34 | 35 | 1. 系统调用进入内核后,这一层开始工作。它根据文件描述符`fd`找到对应的文件信息,确定需要读取的是磁盘上的哪个文件,以及从文件的哪个偏移位置开始读取。 36 | 37 | 2. 它计算出这100个字节的数据对应于文件的哪个**逻辑块**。 38 | 39 | 3. **(关键步骤)** 它首先检查**内核的页高速缓存**,看看这个逻辑块是否已经存在于内存中。 40 | 41 | - **如果命中缓存**:直接从内存中复制数据到用户缓冲区,整个I/O请求可能就此完成,无需访问磁盘。 42 | 43 | - **如果未命中缓存**:它必须向下一层发出请求。它会将“文件的逻辑块号”转换为“设备的物理块号”,然后向磁盘的设备驱动程序发出一个通用的、与具体磁盘型号无关的命令,例如:“请读取逻辑块号为`LBA 12345`的数据”。 44 | 45 | 46 | #### **第三层:设备驱动程序 (Device Drivers)** 47 | 48 | - **作用**: 这是与硬件直接相关的“翻译官”。它接收来自上层(设备无关层)的抽象命令,并将其翻译成特定硬件控制器能够理解的具体指令。每个设备(如NVIDIA的显卡、Intel的SATA控制器)都有其专属的驱动程序。 49 | 50 | - **在本例中的作用**: 51 | 52 | 1. 磁盘的设备驱动程序收到了“读取逻辑块号`LBA 12345`”的命令。 53 | 54 | 2. 驱动程序知道当前计算机上安装的是哪款硬盘,以及如何与其控制器通信。 55 | 56 | 3. 它将抽象的块号转换为该硬盘能懂的**柱面号(Cylinder)、磁头号(Head)和扇区号(Sector)**。 57 | 58 | 4. 然后,它通过I/O总线,向磁盘控制器的**寄存器**中写入一连串的命令和参数。 59 | 60 | 5. 发出命令后,驱动程序通常会**阻塞**等待该请求的进程(使其进入等待状态),并让出CPU。 61 | 62 | 63 | #### **第四层:中断处理程序 (Interrupt Handlers)** 64 | 65 | - **作用**: 当慢速的I/O设备完成其任务时,它需要一种方式来通知CPU。中断就是这个机制。中断处理程序是一段特殊的代码,它在I/O完成后被CPU执行,用于处理I/O完成后的“善后”工作。 66 | 67 | - **在本例中的作用**: 68 | 69 | 1. 磁盘硬件完成了读取数据块的任务(通常通过DMA直接将数据放入内存的内核缓冲区)。 70 | 71 | 2. 磁盘控制器向CPU发送一个**中断信号**。 72 | 73 | 3. CPU立刻暂停当前正在执行的任何任务,跳转到预设的**中断服务例程**(即中断处理程序)。 74 | 75 | 4. 该程序检查磁盘控制器的状态,确认数据传输成功且无错误。 76 | 77 | 5. 它会**唤醒**之前因为等待这个I/O而阻塞的进程(将其状态从“等待”改为“就绪”)。 78 | 79 | 6. 处理完毕后,从中断返回,CPU可以继续执行被中断的任务或调度刚被唤醒的进程。 80 | 81 | 82 | #### **第五层:硬件 (Hardware)** 83 | 84 | - **作用**: 物理设备本身,包括设备控制器(芯片)和设备主体(如磁盘盘片、打印机机械结构等)。它负责执行由设备驱动程序发来的电子命令。 85 | 86 | - **在本例中的作用**: 87 | 88 | 1. 磁盘控制器接收到寄存器中的命令。 89 | 90 | 2. 它驱动磁头臂移动到正确的柱面(**寻道**),选择正确的磁头。 91 | 92 | 3. 等待磁盘盘片旋转,使目标扇区转到磁头下方(**旋转延迟**)。 93 | 94 | 4. 读取扇区上的磁信号,转换为数据流,并通过**DMA**将其直接传输到内存中指定的位置。 95 | 96 | 5. 传输完成后,向CPU发出中断信号。 97 | 98 | 99 | 总结一下请求的流程: 100 | ``` 101 | 102 | 用户程序 -> C库 -> 系统调用 -> 设备无关层 -> 设备驱动层 -> 硬件 -> [I/O操作] -> 中断 -> 中断处理层 -> 设备驱动层 -> 设备无关层 -> 返回用户程序 103 | ``` 104 | -------------------------------------------------------------------------------- /src/site/notes/408/DRAM都会怎么洗澡🥵.md: -------------------------------------------------------------------------------- 1 | --- 2 | {"dg-publish":true,"dg-permalink":"/408/DRAM都会怎么洗澡🥵","permalink":"/408/DRAM都会怎么洗澡🥵/"} 3 | --- 4 | 5 | 6 | ### DRAM的三种基本刷新策略 7 | 8 | #### 1. 集中式刷新 (Centralized Refresh / Burst Refresh) 9 | 10 | - 原理与执行方式: 11 | 12 | 这是一种最简单直接但最“粗暴”的策略。DRAM芯片在规定的刷新周期(如64ms)内,绝大部分时间都正常响应CPU的读写请求。但在该周期的末尾,找到一个空闲时间,暂停所有对存储器的访问,然后启动一个连续的、不间断的**“刷新脉冲串 (Burst)”**,一次性地、连续地对所有行(例如全部8192行)进行刷新,直到所有行都被刷新完毕。 13 | 14 | - 流程示意: 15 | 16 | [---------- 正常读写周期 (接近64ms) ----------][-- 死区: 连续刷新所有行 --] 17 | 18 | - "死时间"分析: 19 | 20 | 假设刷新一个行需要一个时钟周期(例如100ns),那么刷新8K(8192)行就需要: 21 | 22 | T死区​=8192×100ns=819.2μs 23 | 24 | 在这长达819.2微秒的时间内,CPU或任何其他设备都无法访问内存,内存完全处于“死机”状态,不响应任何读写请求。 25 | 26 | - **优点:** 27 | 28 | - **控制简单:** 内存控制器的逻辑非常简单,只需要一个定时器,在刷新周期结束前触发一次集中的刷新操作即可。读写逻辑和刷新逻辑完全分离。 29 | 30 | - **缺点:** 31 | 32 | - **存在明显的“死区”**:这是其致命缺点。在刷新期间,内存完全不可用,对于需要实时响应的系统(如工业控制、实时操作系统)是绝对无法接受的。 33 | 34 | - **适用场景:** 35 | 36 | - 由于存在严重的“死区”问题,**集中式刷新在现代通用计算机系统中基本不被采用**。它更多是作为一个理论模型,用于教学和理解刷新概念。可能仅适用于某些对实时性要求极低、且允许有固定长暂停的嵌入式或专用系统中。 37 | 38 | 39 | #### 2. 分散式刷新 (Decentralized Refresh / Distributed Refresh) 40 | 41 | - 原理与执行方式: 42 | 43 | 这种策略将刷新操作均匀地分散到整个刷新周期内。它将刷新周期(如64ms)平分给所有需要刷新的行(如8192行),计算出两次刷新操作之间的最大时间间隔。 44 | 45 | 刷新间隔=8192行64ms​≈7.8μs 46 | 47 | 这意味着,内存控制器必须保证每7.8μs就执行一次行刷新操作。这个刷新周期被穿插在正常的读写周期之间。 48 | 49 | - 流程示意: 50 | 51 | [读/写] [读/写] ... [刷新行i] [读/写] [读/写] ... [刷新行i+1] ... 52 | 53 | 存储器的每个工作周期实际上由一个读/写操作和一个刷新操作构成。 54 | 55 | - "死时间"分析: 56 | 57 | 分散式刷新没有集中的、长的“死区”。但它把刷新的时间开销“摊派”到了每一次存储器访问中,使得每个存储周期的总时间被拉长了。例如,一个正常的读写周期可能是80ns,但为了插入刷新,整个系统周期可能被定义为100ns,预留了刷新操作的时间。 58 | 59 | - **优点:** 60 | 61 | - **无长“死区”**:系统响应不会有长时间的中断,实时性得到了保证。 62 | 63 | - **缺点:** 64 | 65 | - **降低了整体性能:** 即使在读写不频繁、总线空闲的时候,也必须严格按照固定的时间间隔插入刷新,这会拖慢系统运行速度。在需要密集读写的场景下,频繁的刷新插入会严重影响带宽。 66 | 67 | - **控制相对复杂:** 需要更精细的时序控制来保证刷新周期的精确插入。 68 | 69 | - **适用场景:** 70 | 71 | - 适用于**对实时性要求高**、不允许出现长暂停的系统。虽然它也不是现代主流PC的选择,但其设计思想对实时系统有重要意义。 72 | 73 | 74 | #### 3. 异步式刷新 (Asynchronous Refresh / Staggered Refresh) 75 | 76 | - 原理与执行方式: 77 | 78 | 这是集中式和分散式的折中方案,也是现代计算机系统普遍采用的策略。它同样要求在64ms内刷新完所有行(如8K行),因此平均下来也是每7.8μs需要刷新一行。但它不要求严格地在固定时间点进行刷新。控制器只需要保证在一个刷新周期(64ms)内,发出了足够次数(8192次)的刷新命令即可。 79 | 80 | - 流程示意: 81 | 82 | 它将刷新操作分解为对单行的刷新,可以在CPU不访问内存的空闲时间,或者对性能影响较小的时机,见缝插针地进行。控制器可以一次性刷新几行,也可以在一段时间内不刷新(只要不超出单行电荷泄漏的最大时限),只要最终能“完成任务”即可。 83 | 84 | - "死时间"分析: 85 | 86 | 异步刷新既避免了集中式的长“死区”,也避免了分散式的死板。单次刷新一行的时间非常短(如100ns),对系统的影响可以降到最低。 87 | 88 | - **优点:** 89 | 90 | - **性能影响最小:** 提供了极大的灵活性,可以在总线空闲时进行刷新,最大化了读写操作的有效带宽。这是**性能最好**的刷新方式。 91 | 92 | - **实时性好:** 避免了长“死区”,保证了系统的及时响应。 93 | 94 | - **缺点:** 95 | 96 | - **控制最复杂:** 内存控制器需要更智能的逻辑,不仅要跟踪刷新进度,还要判断总线状态,以寻找最佳的刷新时机。 97 | 98 | - **适用场景:** 99 | 100 | - **几乎所有现代计算机系统**,包括个人电脑、服务器、智能手机等。这是因为它在性能和实时性之间取得了最佳的平衡。 101 | 102 | 103 | --- 104 | 105 | ### 总结对比 106 | 107 | |特性|集中式刷新|分散式刷新|异步式刷新| 108 | |---|---|---|---| 109 | |**刷新方式**|周期末尾,一次性刷新所有行|均匀分散,穿插在读写周期中|在周期内,见缝插针地刷新各行| 110 | |**“死区”**|**长且固定**,整个系统暂停|**无**,但每个存储周期被拉长|**极短**,对系统影响微乎其微| 111 | |**对读写影响**|影响集中在“死区”时间段|影响分散到每一次操作,整体降速|影响最小,利用空闲周期| 112 | |**控制复杂度**|**简单**|较复杂|**最复杂**| 113 | |**适用场景**|理论教学,极少数特定系统|对实时性有严格要求的系统|**现代通用计算机系统标准方案**| -------------------------------------------------------------------------------- /src/site/notes/408/只会蒙头算数的弱智CPU是怎么识别出变量类型的呢🤔.md: -------------------------------------------------------------------------------- 1 | --- 2 | {"dg-publish":true,"dg-permalink":"/408/只会蒙头算数的弱智CPU是怎么识别出变量类型的呢🤔","permalink":"/408/只会蒙头算数的弱智CPU是怎么识别出变量类型的呢🤔/"} 3 | --- 4 | 5 | 6 | 在写代码的时候,我们清晰地定义了 `int`、`unsigned int`、`float` 等各种变量类型。但我们又被告知,计算机的CPU是一个只认识0和1的“铁憨憨”,它处理的所有东西本质上都是一串二进制数。 7 | 8 | 这就带来一个悖论:一个连正负号都要用`0`和`1`来表示的“笨蛋”CPU,是怎么在我们执行 `x >> 2` 这类操作时,神奇地知道该对有符号数执行“算术移位”,而对无符号数执行“逻辑移位”的呢? 9 | 10 | 难道CPU在运算前还会偷偷检查一下变量的“身份证”? 11 | 12 | **剧透一下:并不会。真正的“魔法”,发生在代码运行之前的编译阶段。** 13 | 14 | 为了揭开这个秘密,让我们跟随一个具体的例子,走完它从高级语言到CPU执行的全过程。 15 | 16 | #### **第一幕:我们的C语言剧本** 17 | 18 | ```c 19 | #include 20 | 21 | int main() { 22 | // 我们的两个主角,一个有符号,一个无符号 23 | // 在8位补码中,-8 是 11111000 24 | // 在8位无符号数中,248 是 11111000 25 | signed char signed_num = -8; 26 | unsigned char unsigned_num = 248; 27 | 28 | printf("原始二进制: 11111000\n\n"); 29 | 30 | // 1. 对有符号数进行右移 (我们期望得到 -4) 31 | signed char signed_result = signed_num >> 1; 32 | 33 | // 2. 对无符号数进行右移 (我们期望得到 124) 34 | unsigned char unsigned_result = unsigned_num >> 1; 35 | 36 | // 让我们看看结果 37 | printf("有符号数: %d >> 1 = %d\n", signed_num, signed_result); 38 | printf(" - 结果二进制: ...11111100\n\n"); 39 | 40 | printf("无符号数: %u >> 1 = %u\n", unsigned_num, unsigned_result); 41 | printf(" - 结果二进制: ...01111100\n\n"); 42 | 43 | return 0; 44 | } 45 | ``` 46 | 47 | 这段代码的意图很明确:虽然`-8`和`248`的8位二进制表示都是`11111000`,但我们期望得到完全不同的移位结果。 48 | 49 | #### **第二幕:编译器的“魔法”翻译** 50 | 51 | 当编译器读到这份代码时,它会做以下事情: 52 | 53 | 1. **记录身份**:它看到 `signed char signed_num`,就在自己的小本本(符号表)上记下:“`signed_num` 这个变量,之后所有操作都要按**有符号数**的规矩来办!”。看到 `unsigned char unsigned_num`,它也记下:“`unsigned_num` 要按**无符号数**的规矩来!” 54 | 55 | 2. **选择工具**:当编译器看到 `signed_num >> 1` 时,它会想:“嗯,这是对有符号数的右移,按照C语言标准,这代表除以2,我应该使用**算术右移**指令。” 于是,它生成了一条 `SAR` (Shift Arithmetic Right) 指令。 56 | 57 | 3. 当编译器看到 `unsigned_num >> 1` 时,它会想:“哦,这是对无符号数的右移,这只是一个纯粹的位操作,我应该使用**逻辑右移**指令。” 于是,它生成了一条 `SHR` (Shift Logical Right) 指令。 58 | 59 | 60 | 编译完成后,我们得到一个可执行文件。在这个文件里,`signed` 和 `unsigned` 这些类型信息**已经消失了**。它们的作用已经完成,就像建筑图纸在房子盖好后就可以收起来一样。留下的,是给CPU的具体施工指令。 61 | 62 | **C代码到汇编指令的翻译(简化示意):** 63 | 64 | 代码段 65 | 66 | ``` 67 | ; --- 处理有符号数的部分 --- 68 | mov al, -8 ; 将-8的二进制表示(11110000)放入AL寄存器 69 | sar al, 1 ; 对AL寄存器执行【算术右移】1位 70 | ; 结果al变为 11111000 -> 11111100 (-4) 71 | 72 | ; --- 处理无符号数的部分 --- 73 | mov bl, 248 ; 将248的二进制表示(11110000)放入BL寄存器 74 | shr bl, 1 ; 对BL寄存器执行【逻辑右移】1位 75 | ; 结果bl变为 11110000 -> 01111000 (120) 76 | ; (注:这里汇编结果与C语言例子有细微差异,为简化说明) 77 | ``` 78 | 79 | _(注:真实编译会更复杂,这里是为了清晰说明核心思想。)_ 80 | 81 | #### **第三幕:CPU的“傻瓜式”执行** 82 | 83 | 最后,轮到我们的“笨蛋”CPU登场了。CPU不关心变量的过去,也不关心它的未来,它只活在当下,忠实地执行收到的每一条指令。 84 | 85 | 1. CPU取到指令 `sar al, 1`。它的控制单元解码后,激活了内部的“算术右移”电路。这个电路的物理设计就是:把所有位向右移动,并在最高位**复制原来的符号位**。CPU不假思索地完成了这个操作。 86 | 87 | 2. 过了一会儿,CPU又取到指令 `shr bl, 1`。控制单元解码后,激活了另一组“逻辑右移”电路。这个电路的物理设计就是:把所有位向右移动,并在最高位**固定填充0**。CPU同样不假思索地完成了这个操作。 88 | 89 | 90 | 看到了吗?CPU自始至终没有做任何“判断”。它只是一个高效的执行者,根据收到的**不同指令**(`SAR` vs `SHR`),调用了内部**不同功能**的电路,从而对完全相同的二进制串 `11110000` 产生了截然不同的结果。 91 | 92 | ### **结论:到底谁是“笨蛋”?** 93 | 94 | 回到我们最初的问题:只会闷头算数的“笨蛋”CPU,如何“认识”变量类型? 95 | 96 | 答案是:**它根本不认识,也不需要认识。** 97 | 98 | - **类型**,是高级语言提供给**程序员**和**编译器**沟通的“契约”。 99 | 100 | - **编译器**,是遵守这份契约,将程序员的意图(比如“这是一个有符号数”)翻译成具体机器指令(比如“请使用算术移位”)的“智能翻译官”。 101 | 102 | - **CPU**,则是这个体系中最底层的“高效工人”,它只负责执行收到的明确指令,不多问一句为什么。 103 | 104 | -------------------------------------------------------------------------------- /src/site/notes/408/浮点数的规格化.md: -------------------------------------------------------------------------------- 1 | --- 2 | {"dg-publish":true,"dg-permalink":"/408/浮点数的规格化","permalink":"/408/浮点数的规格化/"} 3 | --- 4 | 5 | 6 | ### 二、 浮点数规格化的目的与原理 7 | 8 | 9 | 1. **保证表示的唯一性:** 如果不进行规格化,同一个数值可以有多种浮点表示。 10 | 11 | 12 | 2. **保证最高的表示精度:** 尾数(Mantissa/Significand)的位数是有限的。通过规格化,我们让尾数中尽可能多地包含有效数字,丢弃前导的0,从而充分利用有限的位数来表达最高的精度。 13 | 14 | 15 | 16 | ### 三、 规格化的两种主流形式 17 | 18 | 浮点数的规格化,本质上是对其**尾数(一个定点小数)** 进行规格化。但在浮点数运算的上下文中,增加了对**阶码(Exponent)** 的联动操作。 19 | 20 | #### 1. 尾数采用原码表示时的规格化 21 | 22 | 这是早期或教学模型中常见的方式,其规则简单直观。 23 | 24 | - **规格化形式:** 要求尾数(定点小数)的绝对值必须满足 $\\frac{1}{2} \\le |M| \< 1$。表现为**数值部分的最高位必须为1**。 25 | 26 | #### 2. 尾数采用补码表示时的规格化 27 | 28 | 这是现代计算机中更常见的设计思想,规则稍显复杂但逻辑性更强。 29 | 30 | - **规格化形式:** 要求尾数(补码定点小数)的符号位与数值最高位**不相同**。 31 | 32 | - **正数尾数:** 形如 0.1timestimesdotstimes 33 | 34 | - **负数尾数:** 形如 1.0timestimesdotstimes 35 | 36 | ### 四、 IEEE 754 标准中的规格化 37 | 38 | #### 1. 表示格式 39 | 40 | 以32位单精度浮点数为例: 41 | 42 | S (1位) | E (8位) | M (23位) 43 | 44 | - `S`: 符号位 (0为正, 1为负) 45 | 46 | - `E`: **阶码 (Exponent)**,采用**移码**表示,偏置值为127。`实际阶码 = 存储的阶码值 - 127`。 47 | 48 | - `M`: **尾数 (Mantissa/Fraction)**,只存储小数部分。 49 | 50 | 51 | #### 2. 规格化数的定义 52 | 53 | - **判断条件:** 阶码`E`的存储值不全为0,也不全为1 (即 `0 < E < 255`)。 54 | 55 | - **隐含位 (Hidden Bit) 技术:** 56 | 57 | - IEEE 754规定,任何一个规格化的二进制浮点数,其尾数部分(Significand)必然可以写成 1.M 的形式。 58 | 59 | - 例如,二进制数 1101.101 可以表示为 1.101101times23。 60 | 61 | - 既然小数点前的`1`永远存在,那么就**不必存储它**!这被称为“隐含位”。 62 | 63 | - 因此,23位的尾数`M`实际上表示了24位的精度 (1.M)。 64 | 65 | - **真值公式:** $V=(−1)Stimes(1.M)times2(E−127)$ 66 | 67 | #### 3. 非规格化数与特殊值 68 | 69 | IEEE 754还定义了当阶码`E`全为0或全为1时的特殊情况: 70 | 71 | - **非规格化数 (Denormalized Number):** 72 | 73 | - **条件:** `E`全为0,`M`不全为0。 74 | 75 | - **目的:** 用来表示那些比最小规格化数更接近0的数,填补了0和最小规格化数之间的“空隙”,这称为“平滑下溢”。 76 | 77 | - **真值公式:** V=(−1)Stimes(boldsymbol0.M)times2−126。(注意:此时隐含位是**0**,且阶码固定为-126) 78 | 79 | - **零 (Zero):** 80 | 81 | - **条件:** `E`全为0,`M`全为0。有$+0和-0$。 82 | 83 | - **无穷大 (Infinity):** 84 | 85 | - **条件:** `E`全为1,`M`全为0。有$+\infty和-\infty$。 86 | 87 | - **NaN (Not a Number):** 88 | 89 | - **条件:** `E`全为1,`M`不全为0。用于表示无效运算结果,如 sqrt−1。 90 | 91 | 92 | ### 五、 常考点、命题方式与陷阱 93 | 94 | 1. **直接转换(高频选择题):** 给你一个十进制数,让你转成IEEE 754表示;或者反过来,给你一个IEEE 754的十六进制码,让你求其十进制真值。 95 | 96 | - **陷阱:** 97 | 98 | - 忘记**隐含位**`1`。 99 | 100 | - 算错**阶码**,忘记减去或加上偏置值127。 101 | 102 | - 看到阶码全0或全1时,没有按**特殊值**(非规格化数、0、无穷大)的规则处理。 103 | 104 | 2. **浮点数运算过程(综合题):** 考查浮点数加减法运算的全过程,规格化是其中关键一步。 105 | 106 | - **流程:** ①对阶 -> ②尾数加/减 -> **③结果规格化** -> ④舍入 -> ⑤溢出判断。 107 | 108 | - **命题方式:** 给出两个浮点数,要求写出运算的详细步骤和最终结果。 109 | 110 | - **陷阱:** 111 | 112 | - **对阶**时,小阶向大阶看齐,尾数需要右移,可能导致精度损失(舍入)。 113 | 114 | - **结果规格化**时,可能需要多次左规(`阶码--`),也可能需要右规(`阶码++`)。 115 | 116 | - **阶码溢出/下溢**:规格化调整后,阶码超出了表示范围(例如单精度下 `E_实际 > 128` 或 `E_实际 < -126`)。 117 | 118 | 3. **边界情况分析(难题):** 119 | 120 | - **最小/最大规格化数:** 最小正规格化数是 `E=1, M=0`,即 1.0times21−127。最大正规格化数是 `E=254, M`全1。 121 | 122 | - **最小/最大非规格化数:** 最小正非规格化数是 `E=0, M`只有最低位是1。 123 | 124 | - **-0.5的补码尾数陷阱:** 在补码运算中,尾数`-0.5`(`1.100...`)是非规格化的。左规会变成`-1`(`1.000...`),这是一种**尾数下溢(变为-1)**,需要特殊处理,通常是右规并调整阶码。 125 | -------------------------------------------------------------------------------- /src/site/notes/408/在cache小姐协助下的CPU访存全流程🤔.md: -------------------------------------------------------------------------------- 1 | --- 2 | {"dg-publish":true,"dg-permalink":"/408/在cache小姐协助下的CPU访存全流程🤔","permalink":"/408/在cache小姐协助下的CPU访存全流程🤔/"} 3 | --- 4 | 5 | 6 | ### 一、 带 Cache 的 CPU 访存完整流程 7 | 8 | CPU执行一条访存指令(读或写)时,并不会直接与主存打交道,而是优先与Cache交互。整个流程如下: 9 | 10 | 1. **CPU发出请求:** CPU产生一个内存地址(逻辑地址,经过MMU转换为物理地址),并连同读/写信号一起发送给Cache控制器。 11 | 12 | 2. **Cache命中判断 (Cache Hit / Miss):** 这是流程的关键分水岭。 13 | 14 | - Cache控制器根据地址的**索引字段 (Index)** 定位到Cache中的某一个或某一组Cache行。 15 | 16 | - 然后,比较地址的**标记字段 (Tag)** 与该Cache行中存储的标记是否一致。 17 | 18 | - 同时,检查该Cache行的**有效位 (Valid Bit)** 是否为1。 19 | 20 | - **如果** `标记匹配` 且 `有效位为1`,则判断为 **“Cache命中 (Cache Hit)”**。 21 | 22 | - **否则**,即为 **“Cache缺失 (Cache Miss)”**。 23 | 24 | 3. **后续操作:** 25 | 26 | - **若命中 (Hit):** 27 | 28 | - **读命中:** Cache控制器直接从命中的Cache行中,根据地址的**块内偏移字段 (Offset)**,取出对应的字节或字,通过内部总线高速地传送给CPU。访存操作快速完成。 29 | 30 | - **写命中:** 根据写策略(Write Policy)进行操作。 31 | 32 | - **写直通 (Write-Through):** 同时写入Cache和主存。 33 | 34 | - **写回 (Write-Back):** 只写入Cache,并将该Cache行标记为“脏”(Dirty)。该行的数据只有在被替换时,才会被写回主存。 35 | 36 | - **若缺失 (Miss):** 系统将启动一套相对复杂的“缺失处理机制 (Miss Handling)”。 37 | 38 | 39 | --- 40 | 41 | ### 二、 缓存缺失 (Cache Miss) 的处理机制 42 | 43 | **如果Cache缺失,访问主存和更新Cache是同时完成的吗?** 44 | 45 | **不是同时完成的,这是一个有明确先后逻辑顺序的过程,但现代CPU通过优化技术使其部分操作可以并发执行,从而缩短延迟。** 46 | 47 | 下面是标准的、逻辑上的处理步骤: 48 | 49 | #### 步骤 1:暂停CPU (Stall the CPU) 50 | 51 | 当发生Cache Miss时,CPU无法立即获得它想要的数据,因此其执行流水线会**暂停 (Stall)**,等待数据从主存中取回。 52 | 53 | #### 步骤 2:访问主存 (Access Main Memory) 54 | 55 | Cache控制器会接管后续工作,向主存控制器发出一个**读请求**。 56 | 57 | - **关键点:** 这个读请求请求的**不是CPU最初需要的那个字**,而是包含了那个字在内的**一整个主存块 (Block/Line)**。这是利用了程序的**空间局部性原理**,即CPU访问了某个地址后,很可能在不久的将来访问其附近的地址。将整个块调入Cache可以提高后续访问的命中率。 58 | 59 | 60 | #### 步骤 3:数据从主存调往Cache (Block Transfer) 61 | 62 | 主存响应请求,找到对应的数据块,通过系统总线将其传输给Cache控制器。 63 | 64 | #### 步骤 4:数据交付与Cache更新 (重点!) 65 | 66 | 这是最关键的一步,它直接回答了你的问题。当数据块从主存传输过来时,发生了两件事: 67 | 68 | - A. 将CPU所需的字直接送往CPU (CPU Forwarding / Critical Word First) 69 | 70 | 为了尽快地让CPU从暂停状态中恢复,大多数Cache控制器采用了**“读穿 (Read-Through)”或称“关键宇优先”技术。即在整个数据块还在从主存流向Cache的途中,一旦CPU最先需要的那个字到达了,Cache控制器就会立即将这个字直接“转发”给CPU**。这样CPU就可以继续执行,不必等待整个块完全写入Cache。 71 | 72 | - B. 将整个数据块写入Cache (Cache Line Fill) 73 | 74 | 在将关键字发送给CPU的同时,或者之后,Cache控制器会执行以下操作来更新Cache: 75 | 76 | 1. **选择替换行:** 从Cache的对应组中,根据**替换算法 (Replacement Algorithm)**(如LRU, FIFO等)选择一个Cache行用于存放新调入的数据块。 77 | 78 | 2. **处理脏块 (Dirty Block):** 如果被选中的替换行是“脏”的(即在写回策略下,该行数据被修改过),则必须**先将这一整行旧的“脏”数据写回主存**,这个过程称为“写回 (Write-Back)”。这会增加本次Cache Miss的处理时间(Miss Penalty)。 79 | 80 | 3. **写入新块:** 将从主存调入的新数据块写入被选中的Cache行。 81 | 82 | 4. **更新元数据:** 更新该Cache行的**标记 (Tag)** 和**有效位 (Valid Bit)**。 83 | 84 | 85 | #### 流程总结与回答 86 | 87 | - **从逻辑上讲,是“先访问主存,再更新Cache”**。因为没有从主存取回数据,就无法进行更新。 88 | 89 | - **从执行效率上讲,为了缩短CPU的等待时间,将数据送往CPU和将数据块写入Cache这两个动作是高度并发的**。CPU不必等到整个Cache行更新完毕,通过“关键宇优先”技术,它可以提前拿到需要的数据。 90 | 91 | --- 92 | 93 | ### 三、 考点分析与陷阱 94 | 95 | 1. **Cache缺失代价 (Miss Penalty):** 这是衡量Cache性能的重要指标,指的是从发生缺失到CPU获得数据所需的时间。其主要构成是**访问主存的时间**。 96 | 97 | 2. **写回策略的额外开销:** 考题中经常会涉及计算Cache缺失的代价。一个常见的陷阱是,如果替换策略选中了一个“脏块”,考生必须记得加上**将脏块写回主存的时间**。 98 | 99 | 3. **“关键宇优先”技术:** 这个概念是理解Cache效率的关键。它解释了为什么在Cache缺失后,CPU的停顿时间可以小于“主存访问时间 + Cache更新时间”的总和。 100 | 101 | 4. **数据块传输:** 一定要牢记,Cache和主存之间的数据交换单位是**块 (Block)**,而不是字 (Word)。这是Cache工作的基础,也是利用空间局部性原理的体现。 -------------------------------------------------------------------------------- /src/site/notes/408/中断处理和子程序调用对寄存器的保存情况.md: -------------------------------------------------------------------------------- 1 | --- 2 | {"dg-publish":true,"dg-permalink":"/408/中断处理和子程序调用对寄存器的保存情况","permalink":"/408/中断处理和子程序调用对寄存器的保存情况/"} 3 | --- 4 | 5 | 6 | 子程序调用与中断处理在**寄存器值的保存机制**上存在显著差异,主要体现在**保存主体、保存范围、触发条件及恢复时机**等方面。以下是详细分析: 7 | 8 | --- 9 | 10 | 11 | 12 | ### 一、子程序调用的寄存器保存机制 13 | 14 | #### 1. **保存主体:程序员/编译器显式管理** 15 | - **调用约定(Calling Convention)决定保存策略**: 16 | 子程序调用需遵循特定调用约定(如System V AMD64 ABI),明确哪些寄存器由调用者(caller)保存,哪些由被调用者(callee)保存。例如: 17 | - **调用者保存寄存器**(如x86-64的`RAX`, `RCX`, `RDX`, `RDI`, `RSI`):子程序可能修改这些寄存器,调用方需在调用前压栈保存。 18 | - **被调用者保存寄存器**(如`RBP`, `RBX`, `R12-R15`):子程序内部若需使用这些寄存器,必须显式保存至栈,返回前恢复 。 19 | - **示例(x86-64汇编)**: 20 | ```asm 21 | call my_subroutine 22 | my_subroutine: 23 | push rbp ; 保存基址寄存器(被调用者保存) 24 | mov rbp, rsp ; 建立新栈帧 25 | ; ... 子程序逻辑 ... 26 | pop rbp ; 恢复基址寄存器 27 | ret 28 | ``` 29 | 30 | 31 | #### 2. **保存范围:有限且选择性保存** 32 | - **仅保存必要寄存器**: 33 | 子程序仅需保存其实际使用的寄存器,而非所有寄存器。例如,若子程序仅修改`RAX`,则无需保存`RBX` 。 34 | - **栈作为保存媒介**: 35 | 寄存器值通常压入调用栈(用户栈),通过`push`/`pop`指令或`mov`指令直接写入栈空间 。 36 | 37 | 38 | #### 3. **触发条件与恢复时机** 39 | - **显式调用与返回**: 40 | 通过`call`指令触发调用,`ret`指令恢复程序计数器(PC),并依赖程序员显式恢复寄存器 。 41 | 42 | --- 43 | 44 | 45 | 46 | ### 二、中断处理的寄存器保存机制 47 | 48 | #### 1. **保存主体:硬件与内核协同自动完成** 49 | - **硬件自动保存部分寄存器**: 50 | 当中断发生时,CPU自动将关键寄存器(如`EFLAGS`, `CS`, `EIP`)压入内核栈,确保中断返回后程序可继续执行。例如,x86架构通过任务状态段(TSS)切换栈,并自动保存上下文 。 51 | - **内核负责保存剩余寄存器**: 52 | 中断处理程序入口(如Linux的`entry_SYSCALL_64`)需通过宏(如`SAVE_ALL`)保存通用寄存器(`RAX`, `RCX`, `RDX`等),防止中断处理破坏用户态数据 。 53 | 54 | 55 | #### 2. **保存范围:全寄存器上下文** 56 | - **完整上下文保存**: 57 | 中断处理程序需保存所有可能被修改的寄存器,包括通用寄存器、状态寄存器及架构特定寄存器(如x86的`CR2`用于缺页异常地址)。例如,PA-RISC架构通过“影子寄存器”(Shadow Registers)直接保存通用寄存器,减少手动操作 。 58 | - **内核栈作为保存媒介**: 59 | 所有寄存器值保存至内核栈,与用户栈物理隔离,确保安全 。 60 | 61 | 62 | #### 3. **触发条件与恢复时机** 63 | - **异步中断触发**: 64 | 中断由外部事件(如I/O完成、定时器)或异常(如缺页、除零错误)触发,不可预测 。 65 | - **恢复由硬件与内核共同保障**: 66 | 通过`iret`(x86)或`eret`(MIPS)指令恢复上下文,硬件弹出自动保存的寄存器,内核代码恢复通用寄存器 。 67 | 68 | --- 69 | 70 | 71 | 72 | ### 三、关键区别与边界条件 73 | | 维度 | 子程序调用 | 中断处理 | 74 | | -------- | --------------------- | -------------------- | 75 | | **保存主体** | 程序员/编译器显式管理 | 硬件自动+内核代码显式保存 | 76 | | **保存范围** | 选择性保存(调用约定定义) | 全寄存器上下文保存 | 77 | | **触发条件** | 同步调用(`call`指令) | 异步中断信号或异常 | 78 | | **保存媒介** | 用户栈 | 内核栈 | 79 | | **恢复机制** | 显式`pop`或栈帧回退(`leave`) | 硬件指令(如`iret`)+内核代码恢复 | 80 | | **性能开销** | 低(仅必要操作) | 高(完整上下文切换) | 81 | 82 | #### 特殊架构差异: 83 | - **寄存器窗口(Register Windows)**: 84 | 如SPARC架构通过寄存器窗口切换减少保存/恢复操作,仅在窗口溢出时访问栈 。 85 | - **影子寄存器**: 86 | PA-RISC架构为中断处理提供专用影子寄存器,避免手动保存通用寄存器 。 87 | 88 | --- 89 | 90 | 91 | 92 | ### 四、误用风险与例外情况 93 | 1. **子程序调用中的未保存寄存器**: 94 | - 若被调用者未按约定保存`callee-saved`寄存器,可能导致调用方数据损坏(如`RBX`被意外修改)。 95 | 2. **中断处理中的嵌套中断**: 96 | - 若中断处理未正确屏蔽同级中断,可能导致寄存器保存冲突(需通过`CLI`/`STI`或优先级机制避免)。 97 | 3. **KPTI(内核页表隔离)的影响**: 98 | - 现代系统为防御Meltdown漏洞,中断返回时需切换页表(CR3),额外增加上下文保存开销 。 99 | 100 | --- 101 | 102 | 103 | 104 | ### 五、总结:设计原则与最佳实践 105 | 1. **子程序调用**: 106 | - 严格遵循调用约定,明确保存责任; 107 | - 避免过度保存(如无意义的`push`/`pop`),优化性能 。 108 | 2. **中断处理**: 109 | - 确保`SAVE_ALL`/`RESTORE_ALL`完整性,防止上下文丢失; 110 | - 优先使用硬件支持机制(如影子寄存器)降低延迟 。 111 | 3. **混合场景**: 112 | - 在中断处理中调用子程序时,需确保子程序为可重入(Reentrant),避免使用静态变量 。 113 | 114 | **最终结论**: 115 | 子程序调用的寄存器保存是**程序员可控的有限操作**,而中断处理的保存是**硬件与内核协同的全面保护**。两者的设计目标截然不同:前者追求效率,后者强调安全性与正确性。 116 | -------------------------------------------------------------------------------- /src/site/notes/408/结构体变量和基础类型变量的对齐准则.md: -------------------------------------------------------------------------------- 1 | --- 2 | {"dg-publish":true,"dg-permalink":"/408/结构体变量和基础类型变量的对齐准则","permalink":"/408/结构体变量和基础类型变量的对齐准则/"} 3 | --- 4 | 5 | 6 | **首先,一句话回答你的问题:** 结构体变量的对齐原则是为了优化CPU访问内存的效率,其核心是让每个成员变量的起始地址都是其自身大小的整数倍,同时整个结构体的总大小是其最宽成员大小的整数倍。 7 | 8 | ### 二、 对齐原则的详细讲解 9 | 10 | 在C语言中,数据对齐遵循以下三条基本原则: 11 | 12 | 1. **成员对齐规则 (Member Alignment Rule):** 结构体的第一个成员变量,其存放的起始地址与结构体变量的起始地址相同。从第二个成员变量开始,每个成员变量的**起始地址**必须是**该成员数据类型自身大小**与**编译器默认对齐数**中**较小值**的整数倍。 13 | 14 | - 在大多数32位/64位系统中,默认对齐数通常是4或8。考研中若无特殊说明,可以认为编译器默认对齐数足够大(例如8或更大),因此**可以直接简化为:每个成员的起始地址是其自身大小的整数倍**。 15 | 16 | 2. **结构体总大小对齐规则 (Struct Padding Rule):** 结构体成员全部放置完毕后,其**总大小**必须是**结构体中所有成员数据类型中最大那个成员的大小**与**编译器默认对齐数**中**较小值**的整数倍。不足的部分,将在结构体末尾进行填充(Padding)。 17 | 18 | - 同样,考研中可**简化为:结构体总大小必须是其最宽成员大小的整数倍**。 19 | 20 | 3. **嵌套结构体对齐规则 (Nested Struct Rule):** 如果结构体中包含了其他结构体,那么这个嵌套的结构体成员的**起始地址**必须是**其内部最宽成员的大小**与**编译器默认对齐数**中**较小值**的整数倍。整个结构体的总大小仍然遵循第二条规则。 21 | 22 | - 简化理解:将嵌套的结构体看作一个整体成员,它的“自身大小”就是它内部最宽成员的大小。 23 | 24 | 25 | **注意:** `#pragma pack(n)` 是一个预处理指令,可以用来改变编译器默认的对齐数。考研中偶尔会考察这种情况,此时,上述规则中的“编译器默认对齐数”就要换成 `n`。规则1、2、3中的“较小值”的比较逻辑变得至关重要。 26 | 27 | ### 三、 图示讲解 28 | 29 | #### 基础类型变量的对齐 30 | 31 | 基础类型变量(如 `int`, `char`, `double`)的对齐相对简单。假设内存地址从0开始,一个 `int` 变量(通常4字节)会存放在地址是4的倍数的地方(如0, 4, 8, ...);一个 `short`(通常2字节)会存放在地址是2的倍数的地方(如0, 2, 4, ...)。 32 | 33 | #### 结构体对齐示例 34 | 35 | **例题1:** 计算下面定义的结构体 `struct Test` 在32位系统下的大小。 36 | 37 | C 38 | 39 | ``` 40 | struct Test { 41 | char c1; 42 | int i; 43 | char c2; 44 | }; 45 | ``` 46 | 47 | **分析过程:** 48 | 49 | 假设该结构体变量 `t` 的起始地址为 `0x0000`。 50 | 51 | 1. **放置 `c1` (`char`, 1字节):** 52 | 53 | - `c1` 是第一个成员,放在结构体首地址。 54 | 55 | - 起始地址: `0x0000`。 56 | 57 | - 占用空间: `0x0000`。 58 | 59 | - 当前结构体大小: 1字节。 60 | 61 | - 内存布局: `| c1 |` 62 | 63 | 2. **放置 `i` (`int`, 4字节):** 64 | 65 | - `i` 的自身大小为4字节。根据**成员对齐规则**,其起始地址必须是4的倍数。 66 | 67 | - 当前地址为 `0x0001`,不是4的倍数。因此,编译器会向后填充(Padding)3个字节。 68 | 69 | - 填充后,下一个可用地址为 `0x0004`。 70 | 71 | - 起始地址: `0x0004`。 72 | 73 | - 占用空间: `0x0004` - `0x0007`。 74 | 75 | - 当前结构体大小: 1 (c1) + 3 (padding) + 4 (i) = 8字节。 76 | 77 | - 内存布局: `| c1 | P | P | P | i | i | i | i |` (P代表Padding) 78 | 79 | 3. **放置 `c2` (`char`, 1字节):** 80 | 81 | - `c2` 自身大小为1字节。其起始地址必须是1的倍数。 82 | 83 | - 当前地址为 `0x0008`,是1的倍数,可以直接放置。 84 | 85 | - 起始地址: `0x0008`。 86 | 87 | - 占用空间: `0x0008`。 88 | 89 | - 当前结构体大小: 8 + 1 = 9字节。 90 | 91 | - 内存布局: `| c1 | P | P | P | i | i | i | i | c2 |` 92 | 93 | 4. **最终检查总大小 (结构体总大小对齐规则):** 94 | 95 | - 结构体中所有成员(`char`, `int`, `char`)中最宽的是 `int`,大小为4字节。 96 | 97 | - 根据**结构体总大小对齐规则**,结构体总大小必须是4的倍数。 98 | 99 | - 当前大小为9字节,不是4的倍数。因此,需要在末尾填充3个字节,使其达到12字节(`9 -> 10, 11, 12`)。 100 | 101 | - 最终总大小: 12字节。 102 | 103 | - 最终内存布局: `| c1 | P | P | P | i | i | i | i | c2 | P | P | P |` 104 | 105 | 106 | --- 107 | 108 | ### 四、 边界情况与易错点总结 109 | 110 | 1. **误用成员个数求和:** 最常见的错误是简单地将所有成员的大小相加,完全忽略对齐和填充。 111 | 112 | 2. **忽略结构体总大小对齐:** 计算完所有成员布局后,忘记了检查并填充结构体末尾,使其总大小满足对齐要求。 113 | 114 | 3. **嵌套结构体对齐基准混淆:** 误将嵌套结构体的总大小作为其对齐基准,而不是其内部最宽成员的大小。 115 | 116 | 4. **数组作为成员:** `struct { char c; int arr[3]; }`,此时数组 `arr` 被看作一个整体,但其对齐基准是其元素类型 `int` 的大小,即4字节。 117 | 118 | 5. **指针作为成员:** 无论什么类型的指针,在32位系统中大小为4字节,64位系统中为8字节。考研若无特别说明,按32位系统(4字节)计算。 119 | -------------------------------------------------------------------------------- /src/site/notes/408/IP数据报的两张表:路由表&ARP表.md: -------------------------------------------------------------------------------- 1 | --- 2 | {"dg-publish":true,"dg-permalink":"/408/IP数据报的两张表:路由表&ARP表","permalink":"/408/IP数据报的两张表:路由表&ARP表/"} 3 | --- 4 | 5 | 6 | ### 一、 路由表 (Routing Table) 7 | 8 | 路由表是网络层进行路径选择的**核心依据**,它本质上是一份“网络交通地图”,告诉路由器去往不同目标网络应该走哪条路。 9 | 10 | - **作用位置**:主要存在于**路由器**中,主机中也有一张简化的路由表(通常只包含本地网络和默认网关)。 11 | 12 | - **核心作用**:当路由器收到一个IP数据包时,它会提取出数据包的**目的IP地址**,并在此表中查找匹配的条目,以决定将该数据包从哪个**物理接口**发送给**哪一个下一跳(Next Hop)**设备。 13 | 14 | - **表的结构与展示**: 15 | 16 | 17 | |目的网络地址 (Destination Network)|子网掩码 (Netmask)|下一跳 (Next Hop)|转发接口 (Interface)|度量值 (Metric)| 18 | |---|---|---|---|---| 19 | |`192.168.1.0`|`255.255.255.0`|`On-link` / `0.0.0.0`|`Eth0`|1| 20 | |`172.16.0.0`|`255.255.0.0`|`10.0.0.2`|`Eth1`|20| 21 | |`0.0.0.0`|`0.0.0.0`|`202.118.1.1`|`WAN0`|10| 22 | 23 | **【字段解析】** 24 | 25 | 1. **目的网络地址 (Destination Network)**:标识目标主机所在的网段地址。 26 | 27 | 2. **子网掩码 (Netmask)**:与目的IP地址进行“与”运算,以判断该IP是否属于此目的网络。这是进行**最长前缀匹配**的关键。 28 | 29 | 3. **下一跳 (Next Hop)**:指示数据包应该发往的下一个路由器的IP地址。 30 | 31 | - 如果目标网络与路由器**直接相连**(如上表第一行),此项通常为`On-link`或全零,表示可以直接通过接口发送到目标主机。 32 | 33 | - 如果目标网络是**远程网络**,此项则为通往该网络的下一个路由器的IP地址。 34 | 35 | 4. **转发接口 (Interface)**:路由器用于发送数据包的物理端口(如 `Eth0`、`WAN0` 等)。 36 | 37 | 5. **度量值 (Metric)**:(可选) 一个表示路径“成本”的数值,由路由协议(如RIP的跳数、OSPF的Cost)计算得出。当存在多条去往同一目的地的路径时,路由器会优先选择度量值最低的路径。 38 | 39 | 40 | **特别注意**:最后一行 `0.0.0.0/0` 是**默认路由(Default Route)**。当在路由表中找不到任何与目的IP匹配的条目时,数据包将按照默认路由的指示进行转发,这通常指向互联网出口。 41 | 42 | --- 43 | 44 | ### 二、 ARP缓存表 (ARP Cache) 45 | 46 | 如果说路由表是宏观的“跨省地图”,那么ARP表就是微观的“同城街道门牌号手册”。它的存在是为了解决一个根本问题:**IP地址和MAC地址的转换**。 47 | 48 | - **作用位置**:存在于所有启用IP协议的**主机**和**路由器**中。 49 | 50 | - **核心作用**:在**同一个局域网(广播域)**内,当一台设备(主机或路由器)知道目标设备的IP地址,但要实际发送数据(封装成以太网帧)时,它必须知道对方的**MAC地址**。ARP表就存储了这种 **IP地址到MAC地址的映射关系**。 51 | 52 | - **表的结构与展示**: 53 | 54 | 55 | |IP地址 (Internet Address)|MAC地址 (Physical Address)|类型 (Type)| 56 | |---|---|---| 57 | |`192.168.1.1`|`00-aa-00-62-c6-09`|`动态 (dynamic)`| 58 | |`192.168.1.101`|`00-bb-00-73-d8-12`|`动态 (dynamic)`| 59 | |`192.168.1.254`|`00-cc-00-84-e9-23`|`静态 (static)`| 60 | 61 | **【字段解析】** 62 | 63 | 1. **IP地址 (Internet Address)**:局域网内某台设备的IP地址。 64 | 65 | 2. **MAC地址 (Physical Address)**:与该IP地址对应的设备的物理网卡地址。 66 | 67 | 3. **类型 (Type)**: 68 | 69 | - **动态 (Dynamic)**:通过**ARP(地址解析协议)**广播请求,并从对方的ARP应答中自动学习并缓存的条目。动态条目有一个**生存时间(TTL)**,超时后会老化并被删除。 70 | 71 | - **静态 (Static)**:由网络管理员手动配置的永久性条目,不会老化。 72 | 73 | 74 | --- 75 | 76 | ### 三、 两张表的协同工作流程 77 | 78 | 这两张表在数据转发过程中紧密配合,缺一不可。我们以“主机A(`192.168.1.10`)向远程的主机B(`172.16.10.20`)发送数据”为例,看看路由器R1(网关`192.168.1.1`)是如何工作的: 79 | 80 | 1. **数据包到达路由器**:主机A将IP包封装在以太网帧中,目的MAC是其网关R1的MAC地址,发送给R1。 81 | 82 | 2. **查找路由表**:R1接收到帧,解封装后看到IP包的目的地址是`172.16.10.20`。它立即查询自己的**路由表**。 83 | 84 | 3. **匹配路由条目**:R1在路由表中进行最长前缀匹配,找到了 `172.16.0.0/16` 这一条目。 85 | 86 | - **获得转发信息**:路由表告诉R1:“你应该把这个包发给下一跳路由器,它的IP地址是`10.0.0.2`,并且要从你的`Eth1`接口发出去。” 87 | 88 | 4. **查找ARP缓存表**:现在,R1知道了下一跳的IP地址(`10.0.0.2`),但要封装成帧发送出去,它还需要`10.0.0.2`这个IP对应的**MAC地址**。于是,R1开始查询自己的**ARP缓存表**。 89 | 90 | 5. **获取下一跳MAC地址**: 91 | 92 | - **情况A:ARP表中有记录**。R1直接从表中查到`10.0.0.2`对应的MAC地址。 93 | 94 | - **情况B:ARP表中无记录**。R1会通过`Eth1`接口发送一个ARP广播请求:“谁是`10.0.0.2`?请把你的MAC地址告诉我。” 目标路由器`10.0.0.2`收到后会单播回复其MAC地址。R1收到后,将此映射关系存入ARP缓存表。 95 | 96 | 6. **封装并转发**:R1获取到下一跳的MAC地址后,将原始IP包重新封装成一个新的以太网帧,其中: 97 | 98 | - 源MAC地址是R1的`Eth1`接口的MAC地址。 99 | 100 | - 目的MAC地址是下一跳路由器`10.0.0.2`的MAC地址。 101 | 102 | - 最后,将这个新帧从`Eth1`接口发送出去。 103 | -------------------------------------------------------------------------------- /src/site/notes/408/条件转移指令对应的转移条件分析🤔.md: -------------------------------------------------------------------------------- 1 | --- 2 | {"dg-publish":true,"dg-permalink":"/408/条件转移指令对应的转移条件分析🤔","permalink":"/408/条件转移指令对应的转移条件分析🤔/"} 3 | --- 4 | 5 | 6 | 7 | 要准确判断两个带符号整数的大小关系,CPU必须同时参考符号标志位(SF)和溢出标志位(OF),而不能仅仅依赖符号标志位(SF)。 8 | 9 | ### 一、带符号整数比较原理详解 10 | 11 | 计算机中的比较操作通常由一条减法指令来实现,例如`CMP A, B`指令,其在硬件层面的实际操作是计算 A−B ,但结果不回存,只根据计算结果设置相应的标志位。后续的条件转移指令(如`jg`, `jl`等)则根据这些标志位来判断是否跳转。 12 | 13 | #### 1. 为什么不能只看SF标志位? 14 | 15 | 一个常见的误区是:既然SF(Sign Flag)标志位表示结果的符号(SF=1为负,SF=0为正),那么比较 A 和 B 的大小,不就是看 A−B 的结果是正是负吗? 16 | 17 | 这种想法在**不发生溢出**的情况下是正确的。但一旦发生**算术溢出**,结果的符号位就会变得不可靠,甚至与真实结果的符号完全相反。 18 | 19 | **溢出(Overflow)**:当两个同号的数相加(或两个异号的数相减)后,结果超出了机器数所能表示的范围。在带符号数运算中,溢出会导致结果的符号位不正确。 20 | 21 | - **正溢出**:两个正数相加,结果大于了能表示的最大正数,变成了一个负数(例如8位系统中 127+1=−128)。 22 | 23 | - **负溢出**:两个负数相加,结果小于了能表示的最小负数,变成了一个正数(例如8位系统中 −127+(−2)=127)。 24 | 25 | 26 | **溢出标志位(OF)** 就是用来检测这种情况的。当运算结果发生溢出时,`OF=1`,否则`OF=0`。 27 | 28 | #### 2. SF与OF的协同工作 29 | 30 | 为了正确判断带符号数的大小,必须将SF和OF结合起来。我们来分析 `jg` (Jump if Greater) 和 `jl` (Jump if Less) 的转移条件。 31 | 32 | **前提**:`CMP A, B` 执行 A−B 操作。我们期望判断 A 是否大于/小于 B。 33 | 34 | **A > B (对应指令 jg, jnle)** 35 | 36 | - **转移条件**: SF=OF AND ZF=0 37 | 38 | - **逻辑分析**: 我们希望 A−B 的真实结果是正数。 39 | 40 | 1. 情况一:不溢出 (OF=0) 41 | 42 | 此时,计算结果的符号是可信的。要想 A−B>0,其结果必须为正数,即 SF=0。此时条件 SF=OF (0=0) 成立。 43 | 44 | 2. 情况二:发生溢出 (OF=1) 45 | 46 | 此时,计算结果的符号是不可信的,与真实结果的符号相反。什么情况下 A−B 会溢出?只有“正数 - 负数”才可能导致正溢出(结果太大,看似为负)。例如,A 是一个较大的正数,B 是一个绝对值较大的负数,A−B 变成 A+(∣B∣),结果超出了最大正数范围,导致最终结果的符号位为1,即 SF=1。但我们知道,此时真实的 A 肯定是大于 B 的。此时条件 SF=OF (1=1) 也成立。 47 | 48 | - **结论**:无论是否溢出,只要 A>B,就必然满足 SF=OF。`ZF=0` 条件是为了排除 A=B 的情况。 49 | 50 | 51 | **A < B (对应指令 jl, jnge)** 52 | 53 | - **转移条件**: SF=OF AND ZF=0 54 | 55 | - **逻辑分析**: 我们希望 A−B 的真实结果是负数。 56 | 57 | 1. 情况一:不溢出 (OF=0) 58 | 59 | 此时,计算结果的符号是可信的。要想 A−B<0,其结果必须为负数,即 SF=1。此时条件 SF=OF (1=0) 成立。 60 | 61 | 2. 情况二:发生溢出 (OF=1) 62 | 63 | 此时,计算结果的符号是不可信的。什么情况下 A−B 会溢出?只有“负数 - 正数”才可能导致负溢出(结果太小,看似为正)。例如,A 是一个绝对值较大的负数,B 是一个较大的正数,A−B 的结果小于了最小负数范围,导致最终结果的符号位为0,即 SF=0。但我们知道,此时真实的 A 肯定是小于 B 的。此时条件 SF=OF (0=1) 也成立。 64 | 65 | - **结论**:无论是否溢出,只要 A B**|**不溢出**,结果为正|0|0|**SF=OF**|✅| 76 | |**A > B**|**正溢出**,“正-负”,结果看似为负|1|1|**SF=OF**|✅| 77 | |**A < B**|**不溢出**,结果为负|0|1|**SF=OF**|✅| 78 | |**A < B**|**负溢出**,“负-正”,结果看似为正|1|0|**SF=OF**|✅| 79 | 80 | 而对于 `jge` (大于等于) 和 `jle` (小于等于),逻辑是类似的,只是放宽了对 ZF 的要求: 81 | 82 | - `jge` (A ≥ B): SF=OF。 (允许 A=B 时结果为0,此时 ZF=1 但 SF=0,OF=0 依然满足 SF=OF) 83 | 84 | - `jle` (A ≤ B): SF=OF OR ZF=1。 (当 A=B 时,ZF=1,直接满足跳转条件) 85 | 86 | 87 | ### 三、常考点 88 | 89 | 1. **混淆无符号数与有符号数跳转**:这是最核心的考点。考生必须清晰地辨别指令助记符。 90 | 91 | - **无符号数**:`ja` (above), `jb` (below)。依据:`CF` 和 `ZF`。 92 | 93 | - **有符号数**:`jg` (greater), `jl` (less)。依据:`SF`, `OF` 和 `ZF`。 94 | 95 | - `je` (equal), `jne` (not equal) 对两者通用,只看 `ZF`。 96 | 97 | 2. **忽略CMP指令**:条件转移指令本身不进行比较,它只检查标志位。题目中一定会有一个先行指令(如`CMP`, `SUB`, `ADD`等)来设置标志位,分析时必须以该指令的执行结果为依据。 98 | 99 | 3. **对溢出判断不熟练**:考题常常会精心设计操作数,使其运算结果恰好在溢出的边界上。考生必须熟练掌握溢出的判断方法: 100 | 101 | - **方法一(双符号位法)**:使用补码的变形,用两个符号位表示。如果运算结果的两个符号位不同(01或10),则表示发生溢出。 102 | 103 | - **方法二(单符号位法)**:OF=Cs​⊕Cs−1​,即符号位的进位和最高数值位的进位是否不同。对于减法 A−B,等效于计算 A+[B]补​,此时 OF 是判断两个正数相加是否得到负数,或两个负数相加是否得到正数。 104 | -------------------------------------------------------------------------------- /src/site/notes/408/临界资源与共享资源.md: -------------------------------------------------------------------------------- 1 | --- 2 | {"dg-publish":true,"dg-permalink":"/408/临界资源与共享资源","permalink":"/408/临界资源与共享资源/"} 3 | --- 4 | 5 | 6 | ### 1. 概念层级与关系 7 | 8 | - **共享资源 (Shared Resource)**: 这是总称,指系统中可供多个进程共同使用的资源。其核心目的是提高资源利用率。根据其共享方式的不同,可进一步细分为以下两种。 9 | 10 | 1. **互斥共享资源 (Mutually Exclusive Shared Resource)**: 这就是我们通常所说的 **临界资源 (Critical Resource)**。 11 | 12 | 2. **同时共享资源 (Simultaneous Shared Resource)** 13 | 14 | 15 | 因此,你问题的核心,就是辨析“互斥共享”和“同时共享”这两种模式。 16 | 17 | ### 2. 两种共享方式的深度辨析 18 | 19 | 这两种共享方式的根本区别在于:**在“任一时刻”,资源是否允许被一个以上的进程访问。** 20 | 21 | #### 2.1 临界资源 (互斥共享方式) 22 | 23 | **官方定义**: 在一段时间内,只允许一个进程访问的资源。访问该资源的代码段称为临界区。 24 | 25 | **核心解读**: 26 | 27 | - **“任一时刻” (At any single moment)**: 严格限制**只有一个**进程能正在使用该资源。如果进程A正在使用打印机,那么在同一微秒,进程B绝对不能使用。这是物理或逻辑属性决定的,强行违背会导致数据错乱或设备损坏。 28 | 29 | - **“某段时间内” (Over a period of time)**: 多个进程可以通过**分时复用 (Time-Division Multiplexing)** 的方式来“共享”该资源。它们在时间上是**交替、串行**地使用该资源,而不是真正地“同时”使用。 30 | 31 | 32 | **图示与实例:以打印机为例** 33 | 34 | 假设有进程 `P1`, `P2`, `P3` 都需要打印文件。打印机本身是一个典型的临界资源。 35 | 36 | ``` 37 | | P1使用 | | P2使用 | | P3使用 | 38 | -----+--------+---------+--------+------+--------+-----> 时间轴 39 | t1 t2 t3 t4 t5 t6 40 | ``` 41 | 42 | - **在 `t1` 到 `t2` 这段时间的任意时刻**: 只有 `P1` 在向打印机发送数据,`P2` 和 `P3` 必须等待。 43 | 44 | - **在 `t3` 到 `t4` 这段时间的任意时刻**: 只有 `P2` 在使用,`P1` 已用完,`P3` 仍在等待。 45 | 46 | - **在 `t1` 到 `t6` 这段较长的时间内**: 我们看到 `P1`, `P2`, `P3` 都使用了打印机,完成了各自的任务。从宏观用户的角度看,打印机这个设备被“共享”了。 47 | 48 | 49 | 实现机制: 50 | 51 | 这种“共享”是由操作系统通过同步互斥机制(如我们之前讨论的信号量和P、V操作)来管理的。对于打印机这类设备,通常还会采用 SPOOLing 技术 (假脱机技术),将所有进程的打印请求先放入一个缓冲区(磁盘上的一个队列),然后由打印机进程依次从队列中取出并打印。这本质上就是将并发请求串行化,以适应临界资源的互斥属性。 52 | 53 | **常考点**: 54 | 55 | - 临界资源是引发进程同步与互斥问题的根源。 56 | 57 | - 所有关于P、V操作、死锁的讨论,都是围绕着如何管理对**临界资源**的访问。 58 | 59 | - 常见的临界资源:物理设备(打印机、扫描仪)、共享内存中由多个进程共同读写的变量、共享文件、消息队列等。 60 | 61 | 62 | #### 2.2 同时共享资源 63 | 64 | **官方定义**: 在一段时间内,允许多个进程“同时”访问的资源。 65 | 66 | **核心解读**: 67 | 68 | - **“任一时刻” (At any single moment)**: 允许**有多个**进程正在访问该资源。 69 | 70 | - **“某段时间内” (Over a period of time)**: 自然也允许多个进程访问。这里的“共享”是真正的并行或并发访问,而非时间上的交替。 71 | 72 | 73 | **实例:以只读文件为例** 74 | 75 | 假设一个可重入的代码文件(如库函数)或一个公共的只读数据文件(如配置文件)被加载到内存中。进程 `P1`, `P2`, `P3` 都需要执行这段代码或读取这份数据。 76 | 77 | ``` 78 | P1读取 --> | | 79 | P2读取 --> | 共享的只读文件 | <-- 在同一时刻 t 80 | P3读取 --> | | 81 | ``` 82 | 83 | - **在任意时刻 `t`**: `P1` 可以读取文件的A部分,`P2` 可以读取文件的B部分,`P3` 可以读取文件的C部分。它们的访问行为因为不改变资源状态,所以互相之间**没有冲突**。它们是真正在“同时”访问。 84 | 85 | 86 | 为什么可以同时? 87 | 88 | 关键在于访问的性质。如果所有访问都是“只读”的,不改变资源内容和状态,那么并发访问就是安全的。一旦有任何一个进程需要“写入”,那么这个资源在该“写入”操作期间就退化为了一个临界资源,需要进行互斥处理。(这也是“读者-写者问题”的由来,该问题是考研高频考点)。 89 | 90 | **常见的可同时共享资源**: 91 | 92 | - 可重入代码 (Reentrant Code)。 93 | 94 | - 只读的数据文件或内存区域。 95 | 96 | 97 | ### 3. 总结与辨析表格 98 | 99 | 为了让你更清晰地掌握其区别,我为你整理了以下表格: 100 | 101 | |特性|临界资源 (互斥共享)|同时共享资源| 102 | |---|---|---| 103 | |**本质定义**|一次仅允许一个进程访问的资源|允许一次有多个进程访问的资源| 104 | |**访问属性**|**互斥性 (Mutual Exclusion)**|**并行性 (Parallelism/Concurrency)**| 105 | |**“任一时刻”的访问**|**仅一个**进程|**可多个**进程| 106 | |**“某段时间内”的共享**|通过**分时复用**实现,进程**串行、交替**地使用|进程**并发或并行**地使用| 107 | |**实现方式**|需要**同步互斥机制**(如P/V操作、锁)来管理和调度|无需特殊的互斥控制(当访问性质为只读时)| 108 | |**典型实例**|打印机、磁带机、被多个进程修改的共享变量、文件|可重入代码、只读文件、ROM| 109 | 110 | ### 4. 关于“共享设备”的补充说明 111 | 112 | “共享设备”是一个广义的术语。一个设备是临界资源还是同时共享资源,取决于其物理特性和使用方式。 113 | 114 | - **打印机**: 是共享设备,但其管理方式是作为**临界资源**。 115 | 116 | - **硬盘**: 情况更复杂。从物理上讲,磁盘的磁头在“任一时刻”只能在一个位置读写,因此物理设备本身是**临界资源**。但从逻辑上看,操作系统通过文件系统可以将磁盘空间划分为不同的文件,只要进程 `P1` 和 `P2` 访问的是**不同的文件**,它们的访问请求就可以在宏观上并发进行(由磁盘调度算法进行优化),看起来像是“同时共享”。但如果 `P1` 和 `P2` 同时写**同一个文件**,那这个文件就成了临界资源。 117 | -------------------------------------------------------------------------------- /src/site/notes/408/IO设备根据数据的存取和传输进行的分类.md: -------------------------------------------------------------------------------- 1 | --- 2 | {"dg-publish":true,"dg-permalink":"/408/IO设备根据数据的存取和传输进行的分类","permalink":"/408/IO设备根据数据的存取和传输进行的分类/"} 3 | --- 4 | 5 | 6 | ### **一、 常见的I/O设备有哪些?** 7 | 8 | 首先,我们可以按功能直观地列举一些常见的I/O设备: 9 | 10 | 1. **人机交互设备 (Human Interface Devices)**: 11 | 12 | - **输入**: 键盘、鼠标、触摸板、触摸屏、麦克风、摄像头、扫描仪。 13 | 14 | - **输出**: 显示器、打印机、音箱、耳机、投影仪。 15 | 16 | 2. **存储设备 (Storage Devices)**: 17 | 18 | - 硬盘驱动器(HDD)、固态硬盘(SSD)、U盘、SD卡、光驱(CD/DVD/Blu-ray)、磁带机。 19 | 20 | 3. **网络通信设备 (Network Communication Devices)**: 21 | 22 | - 网卡(NIC)、调制解调器(Modem)、Wi-Fi适配器、蓝牙适配器。 23 | 24 | 4. **其他设备**: 25 | 26 | - 各种传感器(如温度、GPS)、时钟、定时器等。 27 | 28 | 29 | --- 30 | 31 | ### **二、 I/O设备的分类、特点及举例** 32 | 33 | 尽管I/O设备五花八门,但从操作系统I/O子系统的角度看,最重要、最根本的分类方式是根据其**数据交换的单位和特性**来划分。主要分为以下三大类: 34 | 35 | #### **1. 块设备 (Block Devices)** 36 | 37 | - **核心特点**: 38 | 39 | - **数据单位**: 信息存储在大小固定的**块 (Block)**中。块是这类设备进行数据读写的基本单位。 40 | 41 | - **可寻址性**: 每个块都有自己唯一的**地址**。你可以直接访问任意一个块,而无需访问它前面的块。 42 | 43 | - **访问方式**: 支持**随机访问 (Random Access)** 或直接访问。可以进行`seek`操作来定位到任意块。 44 | 45 | - **I/O命令**: 其接口中的命令通常是“读/写一整块”或“读/写连续的多个块”。 46 | 47 | - **独立性**: 通常是可以独立于操作系统工作的设备(例如,你可以把一块硬盘拆下来装到另一台电脑上)。 48 | 49 | - **传输速率**: 通常较高。 50 | 51 | - **典型设备举例**: 52 | 53 | - **硬盘驱动器 (HDD)** 54 | 55 | - **固态硬盘 (SSD)** 56 | 57 | - **U盘、SD卡等闪存设备** 58 | 59 | - **光盘 (CD/DVD/Blu-ray)** 60 | 61 | 62 | **本质上,所有用于持久化存储、构成文件系统的设备,都是块设备。** 63 | 64 | #### **2. 字符设备 (Character Devices)** 65 | 66 | - **核心特点**: 67 | 68 | - **数据单位**: 数据以**字符 (Character)或字节 (Byte)** 为单位,形成一个**数据流**。 69 | 70 | - **可寻址性**: **不可寻址**。它不具备块的概念,你无法直接定位到“第n个字节”。 71 | 72 | - **访问方式**: 主要是**顺序访问 (Sequential Access)**。数据像水流一样,只能一个接一个地被读取或写入。通常不支持`seek`操作。 73 | 74 | - **I/O命令**: 其接口中的命令是`get`(获取一个字符)或`put`(发送一个字符)。操作系统和库函数通常会对此进行缓冲,使用户可以按行读写。 75 | 76 | - **传输速率**: 差异极大,从很慢(键盘)到很快(显卡帧缓冲)都有可能。 77 | 78 | - **典型设备举例**: 79 | 80 | - **键盘、鼠标**: 产生一个字符/坐标的数据流。 81 | 82 | - **打印机**: 接收一个字符流并打印。 83 | 84 | - **串行端口 (COM)**: 在设备间传输字节流。 85 | 86 | - **声卡**: 产生或接收音频采样数据流。 87 | 88 | 89 | **本质上,所有以数据流方式进行交互的、非存储类的设备,大多属于字符设备。** 90 | 91 | #### **3. 网络设备 (Network Devices)** 92 | 93 | - **核心特点**: 94 | 95 | - 网络设备足够特殊,以至于操作系统通常将其作为独立的一类来处理,而不是简单归为块设备或字符设备。 96 | 97 | - **数据单位**: 数据以大小可变的**网络包 (Packet)** 为单位进行交换。 98 | 99 | - **寻址方式**: 它有自己独特的地址(如MAC地址、IP地址),但这不是块地址。 100 | 101 | - **访问方式**: 它的接口(API)与前两者完全不同。程序员不使用标准的`read`/`write`,而是使用专门的**套接字接口 (Socket API)**,如`send()`和`receive()`。 102 | 103 | - **交互特性**: 交互是**异步和不可靠**的(对于UDP等协议)。数据包可能丢失、乱序或重复。需要复杂的协议栈(如TCP/IP)来处理这些问题。 104 | 105 | - **典型设备举例**: 106 | 107 | - **以太网卡 (NIC)** 108 | 109 | - **Wi-Fi 适配器** 110 | 111 | - **蜂窝网络调制解调器 (Cellular Modem)** 112 | 113 | 114 | --- 115 | 116 | ### **总结与对比表格** 117 | 118 | |特性|块设备 (Block Device)|字符设备 (Character Device)|网络设备 (Network Device)| 119 | |---|---|---|---| 120 | |**基本数据单位**|**块 (Block)**,大小固定|**字节/字符 (Byte/Character)**,形成数据流|**包 (Packet)**,大小可变| 121 | |**是否可寻址**|**是**,每个块都有唯一的地址|**否**|**是**,但使用IP/MAC等网络地址| 122 | |**主要访问方式**|**随机访问** (Random Access)|**顺序访问** (Sequential Access)|**面向报文/流** (Message/Stream Oriented)| 123 | |**标准I/O接口**|`read`, `write`, `seek`|`read`, `write` (通常不支持`seek`)|独立的**套接字(Socket)接口** (`send`, `receive`)| 124 | |**典型设备**|硬盘 (HDD/SSD)、U盘、光驱|键盘、鼠标、打印机、串口|网卡、Wi-Fi适配器| 125 | -------------------------------------------------------------------------------- /src/site/notes/408/文件的逻辑结构以及索引文件.md: -------------------------------------------------------------------------------- 1 | --- 2 | {"dg-publish":true,"dg-permalink":"/408/文件的逻辑结构以及索引文件","permalink":"/408/文件的逻辑结构以及索引文件/"} 3 | --- 4 | 5 | 6 | ### **文件的逻辑结构 (Logical File Structure)** 7 | 8 | 文件的**逻辑结构**是指在用户看来,文件是由什么信息成分构成的,以及成分之间的关系。它关注的是文件的**组织方式**,而不是存储方式。从用户的角度,文件主要可以分为以下两种逻辑结构: 9 | 10 | #### **1. 无结构文件 (流式文件 - Stream of Bytes File)** 11 | 12 | 这是**最简单、也是当今最常用**的文件逻辑结构。 13 | 14 | - **核心思想**: 文件就是一个连续的、无差别的**字节序列 (Stream of Bytes)**。操作系统不关心文件中存储的是什么,无论是文本、图像还是可执行代码,在操作系统看来,它都只是一个长长的字节流。 15 | 16 | - **结构**: 文件内部没有结构。任何结构和意义都由**访问该文件的应用程序**来解释和管理。例如,文本编辑器知道换行符`\n`的含义,而编译器知道`.c`文件中`main()`函数的语法结构,但操作系统本身对此一无所知。 17 | 18 | - **访问方式**: 基本的访问单位是字节。可以通过一个读写指针来顺序地访问文件,也可以通过`lseek`等操作将指针移动到任意字节位置,实现随机访问。 19 | 20 | - **典型例子**: 所有在UNIX/Linux和Windows中的文件,本质上都是流式文件。例如 `.txt`, `.c`, `.jpg`, `.mp3`, `.exe` 等。 21 | 22 | 23 | #### **2. 有结构文件 (记录式文件 - Record-based File)** 24 | 25 | 在这种结构中,文件不再是无差别的字节流,而是由一组具有相似结构的**记录 (Record)** 构成的集合。一条记录通常用于描述一个实体对象的信息。 26 | 27 | - **核心思想**: 文件是记录的序列,操作系统需要理解“记录”是文件的基本组成单位。 28 | 29 | - **结构**: 30 | 31 | - **定长记录**: 文件中所有记录的长度都是相同的。这种结构简单,易于管理。要访问第 `i` 条记录,可以直接计算其地址:`地址 = i * 单条记录长度`。这使得随机访问非常高效。 32 | 33 | - **变长记录**: 文件中各条记录的长度可以不同。这种结构存储效率高,不浪费空间,但管理复杂,实现随机访问的难度较大,通常需要在每条记录的开头存储其长度信息。 34 | 35 | - **访问方式**: 基本的访问单位是记录。 36 | 37 | - **典型例子**: 这种结构在早期的操作系统(如大型机的文件管理系统)和某些特定的商业应用(如COBOL语言处理的文件、数据库文件)中比较常见。 38 | 39 | 40 | --- 41 | 42 | ### **索引文件 (Indexed File) 是什么?** 43 | 44 | 现在,我们来重点讲解“索引文件”。**索引文件是“有结构文件”的一种高级、复杂且高效的组织形式**,其设计的核心目的就是为了解决对文件中记录的**快速随机访问**问题。 45 | 46 | #### **1. 核心思想与类比** 47 | 48 | 想象一本非常厚的书,如果你想找一个特定的知识点,从第一页翻到最后一页会非常慢。你会怎么做?你会去翻书最后的**索引**。索引告诉你“某个关键词在第几页”,然后你直接翻到那一页即可。 49 | 50 | **索引文件就是采用了完全相同的思想。** 51 | 52 | #### **2. 结构组成** 53 | 54 | 一个索引文件通常由两部分构成: 55 | 56 | 1. **数据文件 (Data File)**: 存放着所有的实际**记录**。这些记录可以顺序存放,也可以无序存放。 57 | 58 | 2. **索引表 (Index Table)**: 这是索引文件的精华所在。它是一张独立的、通常比数据文件小得多的表。表中的每一项都是一个**索引项**。 59 | 60 | - **索引项的结构**: `(关键字, 指针)` 61 | 62 | - **关键字 (Key)**: 是记录中的某个数据项,用于**唯一标识**这条记录。例如,在一个学生信息文件中,关键字可以是“学号”。 63 | 64 | - **指针 (Pointer)**: 指向该关键字对应的记录在**数据文件**中的**存储地址**(可以是逻辑记录号或物理地址)。 65 | 66 | 67 | #### **3. 工作流程(如何访问)** 68 | 69 | 假设我们要查找学号为`“20250702”`的学生记录: 70 | 71 | 1. **不读数据文件**: 程序**不会**去遍历庞大的学生数据文件。 72 | 73 | 2. **查找索引表**: 程序会直接在**索引表**中查找关键字`“20250702”`。由于索引表通常是按关键字排好序的,所以可以使用**二分查找**等高效算法,查找速度非常快。 74 | 75 | 3. **获取指针**: 在索引表中找到该关键字后,从中取出与之对应的**指针**(例如,指针告诉我们该记录是数据文件中的第158条记录)。 76 | 77 | 4. **直接访问数据**: 程序利用这个指针,直接计算出第158条记录在数据文件中的确切位置,并将其读入内存。 78 | 79 | 80 | #### **4. 图示理解** 81 | 82 | ``` 83 | 索引表 (Index Table) 数据文件 (Data File) 84 | +----------------+---------+ +--------------------------------+ 85 | | 关键字(Key) | 指针 | | 记录1 (Record 1) | 86 | +----------------+---------+ +--------------------------------+ 87 | | “20250001” | 1 | -- 指向 --> | 记录2 (Record 2) | 88 | +----------------+---------+ +--------------------------------+ 89 | | “20250002” | 2 | | ... | 90 | +----------------+---------+ +--------------------------------+ 91 | | ... | ... | | 记录158 (学号为“20250702”) | <--+ 92 | +----------------+---------+ +--------------------------------+ | 93 | | “20250702” | 158 | -----------------------------------------------------+ 94 | +----------------+---------+ | ... | 95 | | ... | ... | +--------------------------------+ 96 | +----------------+---------+ 97 | ``` 98 | -------------------------------------------------------------------------------- /src/site/notes/408/目录检索算法,一个文件检索的生命流程🤔.md: -------------------------------------------------------------------------------- 1 | --- 2 | {"dg-publish":true,"dg-permalink":"/408/目录检索算法,一个文件检索的生命流程🤔","permalink":"/408/目录检索算法,一个文件检索的生命流程🤔/"} 3 | --- 4 | 5 | 6 | ### **一、 目录内检索算法** 7 | 8 | 目录本质上也是一个文件,它的内容是一些**目录项 (Directory Entry)** 的列表。每个目录项通常包含两部分:**文件名**和指向该文件**索引节点(i-node)的编号**。目录检索算法,就是在这个列表中根据文件名查找对应目录项的方法。 9 | 10 | 主要有以下几种算法: 11 | 12 | #### **1. 线性查找 (Linear Search)** 13 | 14 | 这是最简单、最直观的算法。 15 | 16 | - **实现方式**: 将目录中的所有目录项以线性列表的形式存储。当需要查找一个文件时,从列表的第一个目录项开始,逐个向后比较文件名,直到找到匹配的项或者搜索完整个列表。 17 | 18 | - **优点**: 19 | 20 | - 实现简单,易于管理。 21 | 22 | - 创建文件时,只需在列表末尾追加一个新的目录项即可。 23 | 24 | - **缺点**: 25 | 26 | - **效率低下**。对于包含大量文件的目录,平均查找长度为 `n/2`(n为目录项数量),时间复杂度为 O(n)。当目录很大时,性能会急剧下降。 27 | 28 | - 删除文件后,可能会留下空洞,需要有机制来管理这些空洞(例如,标记为未使用,或移动后续项来填补),增加了管理的复杂性。 29 | 30 | 31 | #### **2. 哈希查找 (Hash Search)** 32 | 33 | 为了提高查找效率,现代文件系统通常采用基于哈希表的方法。 34 | 35 | - **实现方式**: 目录文件本身被组织成一个哈希表。当查找一个文件时,系统会先对**文件名进行哈希运算**,得到一个哈希值,这个哈希值直接或间接地指向了目录项在哈希表中的位置。 36 | 37 | - **优点**: 38 | 39 | - **查找效率极高**。在没有哈希冲突或冲突很少的情况下,查找时间复杂度接近 O(1),几乎与目录中的文件数量无关。 40 | 41 | - **缺点**: 42 | 43 | - **哈希冲突处理**: 必须有解决哈希冲突的机制(如链地址法、开放定址法),这增加了实现的复杂性。 44 | 45 | - 哈希表的大小需要预先确定或支持动态扩展,管理起来比线性列表复杂。 46 | 47 | 48 | --- 49 | 50 | ### **二、 访问文件的完整检索流程** 51 | 52 | 现在我们来看一个更宏观的过程:当用户或程序需要访问一个文件(例如,通过路径 `/home/user/report.txt`)时,操作系统的完整检索流程是怎样的。这是一个自顶向下的、逐级解析的过程。 53 | 54 | **场景**: 访问绝对路径 `/home/user/report.txt` 55 | 56 | #### **第一步:路径类型判断与起点定位** 57 | 58 | 1. 操作系统首先解析路径字符串。发现它以 `/` 开头,这是一个**绝对路径**。 59 | 60 | 2. 因此,检索必须从文件系统的**根目录 (`/`)** 开始。 61 | 62 | 3. 操作系统知道根目录的i-node信息(通常其i-node编号是固定的,如2,并且这些信息存储在磁盘的**超级块(Superblock)**中),并将其读入内存。 63 | 64 | 65 | _(如果路径是相对路径,如 `docs/file.c`,则起点是当前进程的**当前工作目录(Current Working Directory, CWD)**,其i-node信息保存在进程的PCB中。)_ 66 | 67 | #### **第二步:循环解析路径分量 (Path Traversal)** 68 | 69 | 这是一个循环往复的过程,直到路径的最后一个分量被处理。 70 | 71 | **循环1:解析 `/home`** 72 | 73 | 1. **获取当前目录数据**: 操作系统根据根目录的i-node,找到存放根目录内容的**数据块**(这些数据块里就是根目录下的所有目录项,如 `(‘bin’, i-node号)`, `(‘etc’, i-node号)`, `(‘home’, i-node号)` 等)。 74 | 75 | 2. **目录内检索**: 在根目录的数据块中,使用上面讲的**目录检索算法**(如线性查找或哈希查找)搜索名为 **`"home"`** 的目录项。 76 | 77 | 3. **获取下一级i-node**: 找到后,从该目录项中取出 `"home"` 目录对应的 **i-node编号**。 78 | 79 | 4. **加载i-node**: 操作系统根据这个编号,从磁盘的i-node表中将 `"home"` 的i-node读入内存。 80 | 81 | 5. **权限检查**: 检查当前用户是否有权限进入(执行)`"home"` 目录。如果有,则继续;如果没有,则返回“权限不足”错误。 82 | 83 | 84 | **循环2:解析 `user`** 85 | 86 | 1. **获取当前目录数据**: 现在,`"home"` 目录成了“当前目录”。操作系统根据刚刚加载的 `"home"` 的i-node,找到存放其内容的**数据块**(里面是`home`目录下的所有用户目录项)。 87 | 88 | 2. **目录内检索**: 在 `"home"` 目录的数据块中,搜索名为 **`"user"`** 的目录项。 89 | 90 | 3. **获取下一级i-node**: 找到后,取出 `"user"` 目录对应的 **i-node编号**。 91 | 92 | 4. **加载i-node**: 将 `"user"` 的i-node读入内存。 93 | 94 | 5. **权限检查**: 检查用户是否有权限进入 `"user"` 目录。 95 | 96 | 97 | **循环3:解析 `report.txt`** 98 | 99 | 1. **获取当前目录数据**: `"user"` 目录成了“当前目录”。根据 `"user"` 的i-node,找到其数据块。 100 | 101 | 2. **目录内检索**: 在 `"user"` 目录的数据块中,搜索名为 **`"report.txt"`** 的目录项。 102 | 103 | 3. **获取最终i-node**: 找到后,取出 **`"report.txt"`** 文件对应的 **i-node编号**。 104 | 105 | 4. **加载i-node**: 将 **`"report.txt"`** 的i-node读入内存。 106 | 107 | 108 | #### **第三步:获得目标文件i-node并访问** 109 | 110 | - 路径字符串此时已解析完毕。最后一步得到的i-node,就是目标文件 **`"report.txt"`** 的i-node。 111 | 112 | - 这个i-node包含了文件的所有元数据(大小、权限、时间戳等)以及最重要的——**指向存放文件实际内容的数据块的指针列表**。 113 | 114 | - 操作系统现在可以根据这个最终的i-node和用户请求(读或写),进行后续的数据操作了。 115 | 116 | 117 | #### **补充:高速缓存的作用** 118 | 119 | 为了避免每次路径解析都进行大量重复的磁盘I/O,操作系统会缓存: 120 | 121 | - **i-node缓存**: 最近访问过的i-node会保存在内存中。 122 | 123 | - 目录项缓存 (DNLC): 最近的“文件名 -> i-node编号”的解析结果也会被缓存。 124 | -------------------------------------------------------------------------------- /src/helpers/filetreeUtils.js: -------------------------------------------------------------------------------- 1 | const sortTree = (unsorted) => { 2 | //Sort by folder before file, then by name 3 | const orderedTree = Object.keys(unsorted) 4 | .sort((a, b) => { 5 | 6 | let a_pinned = unsorted[a].pinned || false; 7 | let b_pinned = unsorted[b].pinned || false; 8 | if (a_pinned != b_pinned) { 9 | if (a_pinned) { 10 | return -1; 11 | } else { 12 | return 1; 13 | } 14 | } 15 | 16 | const a_is_note = a.indexOf(".md") > -1; 17 | const b_is_note = b.indexOf(".md") > -1; 18 | 19 | if (a_is_note && !b_is_note) { 20 | return 1; 21 | } 22 | 23 | if (!a_is_note && b_is_note) { 24 | return -1; 25 | } 26 | 27 | //Regular expression that extracts any initial decimal number 28 | const aNum = parseFloat(a.match(/^\d+(\.\d+)?/)); 29 | const bNum = parseFloat(b.match(/^\d+(\.\d+)?/)); 30 | 31 | const a_is_num = !isNaN(aNum); 32 | const b_is_num = !isNaN(bNum); 33 | 34 | if (a_is_num && b_is_num && aNum != bNum) { 35 | return aNum - bNum; //Fast comparison between numbers 36 | } 37 | 38 | if (a.toLowerCase() > b.toLowerCase()) { 39 | return 1; 40 | } 41 | 42 | return -1; 43 | }) 44 | .reduce((obj, key) => { 45 | obj[key] = unsorted[key]; 46 | 47 | return obj; 48 | }, {}); 49 | 50 | for (const key of Object.keys(orderedTree)) { 51 | if (orderedTree[key].isFolder) { 52 | orderedTree[key] = sortTree(orderedTree[key]); 53 | } 54 | } 55 | 56 | return orderedTree; 57 | }; 58 | 59 | function getPermalinkMeta(note, key) { 60 | let permalink = "/"; 61 | let parts = note.filePathStem.split("/"); 62 | let name = parts[parts.length - 1]; 63 | let noteIcon = process.env.NOTE_ICON_DEFAULT; 64 | let hide = false; 65 | let pinned = false; 66 | let folders = null; 67 | try { 68 | if (note.data.permalink) { 69 | permalink = note.data.permalink; 70 | } 71 | if (note.data.tags && note.data.tags.indexOf("gardenEntry") != -1) { 72 | permalink = "/"; 73 | } 74 | if (note.data.title) { 75 | name = note.data.title; 76 | } 77 | if (note.data.noteIcon) { 78 | noteIcon = note.data.noteIcon; 79 | } 80 | // Reason for adding the hide flag instead of removing completely from file tree is to 81 | // allow users to use the filetree data elsewhere without the fear of losing any data. 82 | if (note.data.hide) { 83 | hide = note.data.hide; 84 | } 85 | if (note.data.pinned) { 86 | pinned = note.data.pinned; 87 | } 88 | if (note.data["dg-path"]) { 89 | folders = note.data["dg-path"].split("/"); 90 | } else { 91 | folders = note.filePathStem 92 | .split("notes/")[1] 93 | .split("/"); 94 | } 95 | folders[folders.length - 1]+= ".md"; 96 | } catch { 97 | //ignore 98 | } 99 | 100 | return [{ permalink, name, noteIcon, hide, pinned }, folders]; 101 | } 102 | 103 | function assignNested(obj, keyPath, value) { 104 | lastKeyIndex = keyPath.length - 1; 105 | for (var i = 0; i < lastKeyIndex; ++i) { 106 | key = keyPath[i]; 107 | if (!(key in obj)) { 108 | obj[key] = { isFolder: true }; 109 | } 110 | obj = obj[key]; 111 | } 112 | obj[keyPath[lastKeyIndex]] = value; 113 | } 114 | 115 | function getFileTree(data) { 116 | const tree = {}; 117 | (data.collections.note || []).forEach((note) => { 118 | const [meta, folders] = getPermalinkMeta(note); 119 | assignNested(tree, folders, { isNote: true, ...meta }); 120 | }); 121 | const fileTree = sortTree(tree); 122 | return fileTree; 123 | } 124 | 125 | exports.getFileTree = getFileTree; 126 | -------------------------------------------------------------------------------- /src/site/notes/408/ROM酱的贞操锁:CPU的写指令是如何被无情拒绝的🛡️.md: -------------------------------------------------------------------------------- 1 | --- 2 | {"dg-publish":true,"dg-permalink":"/408/ROM酱贞操锁:CPU的写指令是如何被无情拒绝的🛡️","permalink":"/408/ROM酱贞操锁:CPU的写指令是如何被无情拒绝的🛡️/"} 3 | --- 4 | 5 | 6 | 7 | **RAM和ROM确实统一编址,但写保护并非由指令决定,而是由“硬件物理特性”和“操作系统逻辑权限”这两把锁共同实现的;当有指令试图写入ROM地址时,要么被ROM芯片在物理上直接无视,要么被操作系统的内存管理单元(MMU)提前拦截并产生异常。** 8 | 9 | --- 10 | 11 | ### 一、 统一编址:一个地址空间下的“两家人” 12 | 13 | 首先,我们来解决“统一编址”的问题。 14 | - **地址译码器 (Address Decoder)**:这是关键的硬件电路,相当于小区的“物业管理员”。当CPU在地址总线上发出一个地址时(比如`0x0000_1234`),地址译码器会根据这个地址的高位部分,判断它属于哪个区域。 15 | 16 | - 例如,假设系统设计`0x0000_0000`到`0x0007_FFFF`是ROM的地址范围,`0x0008_0000`到`0xFFFF_FFFF`是RAM的范围。 17 | 18 | - 当地址落在前一个范围时,译码器会发出一个**片选信号(Chip Select, CS)**去“激活”ROM芯片。 19 | 20 | - 当地址落在后一个范围时,它就去激活对应的RAM芯片。 21 | 22 | 23 | 这样,尽管RAM和ROM是不同的物理器件,但在CPU看来,它们共同组成了一个连续、无缝的地址空间,这就是“统一编址”。 24 | 25 | 代码段 26 | 27 | ``` 28 | +-------+ 地址总线 (例如: 0x0001000) 29 | | CPU |---------------------------------+ 30 | +-------+ | 31 | | ^ V 32 | 控制总线 | | 数据总线 +-------------------+ 33 | (R/W#) | | | 地址译码器 | 34 | V | +-------------------+ 35 | +-------+ CS_ROM=1 (激活) | | CS_RAM=0 (不理) 36 | | ROM |<----------------------------+ | 37 | +-------+ | 38 | +-------+ 39 | | RAM |<--+ 40 | +-------+ 41 | ``` 42 | 43 | --- 44 | 45 | ### 二、 访问权限:硬件与软件的双重保险 46 | 47 | 如果CPU想改写一个属于ROM的地址,会发生什么?这里有两道防火墙。 48 | 49 | #### 第一道防火墙:ROM芯片的物理特性(硬件层 - 硬道理) 50 | 51 | 这是最底层、最根本的保护。 52 | 53 | - **CPU的写操作**:当CPU执行一条写指令(如 `STORE R1, 0x1234`)时,它会做三件事: 54 | 55 | 1. 在**地址总线**上放出地址 `0x1234`。 56 | 57 | 2. 在**数据总线**上放出要写入的数据。 58 | 59 | 3. 在**控制总线**上发出一个**写信号**(例如,将`R/W#`信号线置为低电平,表示“写”)。 60 | 61 | - **ROM的反应**: 62 | 63 | 1. 地址译码器看到地址 `0x1234`,激活了ROM芯片(`CS_ROM=1`)。 64 | 65 | 2. ROM芯片接收到了写信号(`R/W#`为低电平)。 66 | 67 | 3. **关键来了**:ROM(Read-Only Memory)的内部电路是被物理“写死”的(通过熔丝或掩膜技术)。它的设计**根本就没有响应“写信号”的功能**。无论控制总线上是什么信号,它的存储内容都不会、也不可能改变。 68 | 69 | 4. **结果**:写指令“执行”了,CPU的时钟周期照常走完,但对于ROM芯片来说,这个写信号就像耳边风一样,它完全无动于衷。数据总线上的数据被白白晾在那里,最终消失。**写操作在物理层面静默地失败了(Silently Fails)**。 70 | 71 | 72 | #### 第二道防火墙:操作系统的内存保护(软件层 - 软约束) 73 | 74 | 在有操作系统的现代计算机中,还有一层更智能、更主动的保护。 75 | 76 | - **内存管理单元 (MMU)**:这是CPU内部的一个硬件模块,它与操作系统协同工作,负责将程序使用的**虚拟地址**翻译成**物理地址**。 77 | 78 | - **页表 (Page Table)**:操作系统为每个进程维护一个页表,记录了虚拟页面到物理页面的映射关系。关键在于,页表的每一项(Page Table Entry, PTE)中都包含了**访问权限位(Permission Bits)**,如:`可读(R)`、`可写(W)`、`可执行(X)`。 79 | 80 | - **操作系统的工作**:在系统启动时,操作系统会把存放固件(如BIOS)的ROM区域映射到自己的内核空间,并在对应的页表项中,将该区域的权限明确设置为**只读(W=0)**。 81 | 82 | - **试图写入ROM时的过程(在有OS的机器上)**: 83 | 84 | 1. 一个程序(通常在用户态)试图执行一条写指令,写入一个指向ROM的虚拟地址。 85 | 86 | 2. CPU将这个虚拟地址交给MMU进行翻译。 87 | 88 | 3. MMU在页表中找到对应的PTE,检查其权限位。 89 | 90 | 4. MMU发现该页面的**`W`位是0**,表示“不可写”。 91 | 92 | 5. **关键来了**:MMU不会继续把地址发往总线,而是立即**中止**这个操作,并向CPU核心发出一个**内部中断/异常(Exception/Trap)**,这个异常通常被称为**“页错误(Page Fault)”**或**“保护错误(Protection Fault)**。 93 | 94 | 6. CPU捕获到这个异常后,会立即从用户态切换到**内核态**,并跳转到操作系统预设好的异常处理程序。 95 | 96 | 7. **结果**:操作系统接管控制权,它发现是一个非法的写操作,通常会向该程序发送一个信号(如Linux下的`SIGSEGV`),导致程序因**“段错误(Segmentation Fault)”**而被强制终止。 97 | 98 | 99 | --- 100 | 101 | ### 三、 总结与考点分析 102 | 103 | |保护层次|实现者|保护机制|发生时机|结果| 104 | |---|---|---|---|---| 105 | |**硬件物理保护**|ROM芯片自身|物理电路无写入功能|写信号到达芯片时|写操作静默失败,数据未改变| 106 | |**OS逻辑保护**|操作系统 + MMU|页表权限位检查|MMU地址翻译时|产生硬件异常,程序被OS终止| 107 | -------------------------------------------------------------------------------- /src/site/notes/408/signal操作与V操作.md: -------------------------------------------------------------------------------- 1 | --- 2 | {"dg-publish":true,"dg-permalink":"/408/signal操作与V操作","permalink":"/408/signal操作与V操作/"} 3 | --- 4 | 5 | 6 | ### 核心区别:有无“记忆”功能与状态改变 7 | 8 | 这是两者最根本的区别,也是所有其他差异的根源。 9 | 10 | - **V 操作 (信号量)**: **具有记忆性**。`V(S)` 操作的本质是 `S.value++`。即使当前没有任何进程在等待该信号量,`V` 操作执行后,信号量 `S` 的值依然会加1。这个增加的值会被“记忆”下来,供后续的 `P` 操作使用。**它改变的是信号量本身的状态(资源计数)**。 11 | 12 | - **比喻**: `V` 操作就像往一个票箱里放一张票。不管现在有没有人在排队等票,这张票都会被放进去,供下一个来的人取用。 13 | 14 | - **signal 操作 (管程)**: **不具有记忆性**。`signal(c)` 操作的本质是唤醒一个因条件 `c` 不满足而等待的进程。如果当前**没有**任何进程在条件变量 `c` 的等待队列上等待,那么 `signal` 操作将**不产生任何效果**,如同空操作一样,这个信号会立即被“丢弃”。**它试图改变的是另一个进程的状态(从等待到就绪),而不是条件变量本身的状态**。 15 | 16 | - **比喻**: `signal` 操作就像在候车室里喊一声:“车来了!” 如果有人在等车,他就会被唤醒准备上车;如果候车室里根本没人等这趟车,你喊的这一声就没有任何作用,声音消散了就没了。 17 | 18 | 19 | --- 20 | 21 | ### 运行环境与耦合度的区别 22 | 23 | - **V 操作**: 是一个独立的操作,可以在程序的任何地方调用(只要能访问到信号量变量)。它与 `P` 操作松散地耦合,共同作用于一个全局或共享的信号量。 24 | 25 | - **signal 操作**: 必须在**管程内部**使用。它总是与管程的互斥锁以及特定的条件变量 (`condition variable`) 紧密耦合。一个进程必须首先获得管程的互斥访问权,才能在管程的某个过程中调用 `wait` 或 `signal`。 26 | 27 | 28 | --- 29 | 30 | ### 导致进程状态改变的逻辑区别 31 | 32 | - **V 操作**: `V(S)` 执行后,`S.value++`。如果 `S.value <= 0`,则说明之前有进程因 `P(S)` 而阻塞,此时 `V` 操作会唤醒一个等待的进程。唤醒的**直接原因**是 `S.value` 的值从负数向0靠近,代表“资源”从无到有。 33 | 34 | - **signal 操作**: `signal(c)` 的执行逻辑是直接检查条件变量 `c` 的等待队列是否为空。如果不为空,则唤醒一个进程。唤醒的**直接原因**是另一个进程认为“等待的条件可能已经满足了”,并发出一个“通知”。 35 | 36 | 37 | --- 38 | 39 | ### 对当前执行进程的影响区别 40 | 41 | `V` 操作执行后,当前进程会继续执行,这是毫无疑问的。但 `signal` 操作执行后,当前进程(发信号者)和被唤醒进程(等待者)谁继续在管程内执行,这是一个重要的分歧点,在理论上分为两种模型: 42 | 43 | 1. **Hoare 风格 (霍尔风格)**: **立即切换,发信号者让权**。 44 | 45 | - 当进程 `A` 执行 `signal(c)` 唤醒了等待的进程 `B` 时,`A` 会**立即阻塞**自己,将管程的控制权(互斥锁)直接交给 `B`。 46 | 47 | - `B` 被唤醒后立刻在管程内继续执行。 48 | 49 | - **优点**: `B` 被唤醒时,它所等待的条件**一定**是真的。因为它是在 `A` 创造条件后、其他任何进程进入前立即执行的。因此,等待处的代码可以用 `if` 来判断。 50 | 51 | - **缺点**: 实现复杂,涉及两次额外的进程上下文切换(A->B,之后B退出或等待时又要唤醒A),开销较大。 52 | 53 | 2. **Mesa 风格 (米萨风格)**: **发信号者继续,等待者等待**。 54 | 55 | - 当进程 `A` 执行 `signal(c)` 唤醒了 `B` 时,`A` **并不会阻塞**,而是继续执行它在管程内的后续代码。 56 | 57 | - 被唤醒的 `B` 只是从条件等待队列移动到管程的**入口等待队列**,与其他希望进入管程的进程一起排队,等待 `A` 最终释放管程锁。 58 | 59 | - **缺点**: 当 `B` 最终被调度并重新获得管程锁时,它等待的条件**可能已经再次变为假**(因为在 `A` 发信号和 `B` 恢复执行之间,可能有其他进程进入管程并改变了状态)。因此,等待处的代码必须使用 `while` 循环来重新检查条件。 60 | 61 | - **优点**: 实现简单,上下文切换开销小。现代编程语言(如 Java 的 `notify()`)大多采用这种风格。 62 | 63 | 64 | ### 总结对比表格 65 | 66 | | 特性 | V 操作 (信号量) | signal 操作 (管程) | 67 | | ----------- | ------------------------------------ | -------------------------------------------------------------------------------------------- | 68 | | **核心机制** | **有记忆性**,对资源计数器 `S.value` 进行 `++` 操作 | **无记忆性**,仅当有进程等待时才起作用,否则信号丢失 | 69 | | **作用对象** | 直接改变信号量 `S` 的状态值 | 试图改变另一个进程的状态,对条件变量 `c` 本身无状态改变 | 70 | | **运行环境** | 独立,可在任何能访问信号量的地方调用 | 必须在管程内部,与管程互斥锁和条件变量强耦合 | 71 | | **对当前进程影响** | 发信号的进程**总是继续执行** | **不一定**。Hoare风格下发信号者阻塞,Mesa风格下发信号者继续执行 | 72 | | **被唤醒后的状态** | 被唤醒的进程变为就绪态,等待CPU调度 | 被唤醒的进程状态取决于管程风格 (Hoare:立即执行;Mesa:变为就绪态,等待管程锁) | 73 | | **使用范式** | 成对的 `P`/`V` 操作,用于实现复杂的同步互斥逻辑 | `wait`/`signal` 成对出现,用于在管程内部管理"条件不满足则等待"的逻辑 | 74 | | **常见代码模式** | `P(S)` ... `V(S)` | `while(!condition) wait(c);` ... `signal(c);` (Mesa风格) / `if(!condition) wait(c);` (Hoare风格) | 75 | 76 | 总而言之,`V` 操作是一个相对“低级”和“原始”的原子操作,它通过直接增减资源计数来协调进程。而 `signal` 是一个“高级”的、封装在管程结构内的通知机制,它将互斥和同步逻辑分离,使得程序结构更清晰,更不易出错。 -------------------------------------------------------------------------------- /src/site/_includes/layouts/note.njk: -------------------------------------------------------------------------------- 1 | --- 2 | permalink: "notes/{{ page.fileSlug | slugify }}/" 3 | --- 4 | 5 | 6 | 7 | {% if title %}{{ title }}{% else %}{{ page.fileSlug }}{% endif %} 8 | {%include "components/pageheader.njk"%} 9 | {% for imp in dynamics.common.head %} 10 | {% include imp %} 11 | {% endfor %} 12 | {% for imp in dynamics.notes.head %} 13 | {% include imp %} 14 | {% endfor %} 15 | 16 | 17 | {%include "components/notegrowthhistory.njk"%} 18 | 19 | {% if settings.dgShowFileTree !== true %} 20 | {%include "components/navbar.njk"%} 21 | {%else%} 22 | {%include "components/filetree.njk"%} 23 | {% endif %} 24 | 25 | {% if settings.dgEnableSearch === true %} 26 | {%include "components/searchContainer.njk"%} 27 | {% endif %} 28 | 29 |
30 |
31 | {% if settings.dgShowInlineTitle === true %} 32 |

{% if title %}{{ title }}{% else %}{{ page.fileSlug }}{% endif %}

33 | {% endif %} 34 |
35 | {% if settings.dgShowTags === true and tags %} 36 |
37 | {% for tag in tags %} 38 | {% if tag != 'gardenEntry' and tag !='note' %} 39 | 40 | #{{tag}} 41 | 42 | {% endif %} 43 | {% endfor %} 44 |
45 | {% endif %} 46 | {%- if meta.timestampSettings.showCreated or meta.timestampSettings.showUpdated -%} 47 |
48 | {%- if meta.timestampSettings.showCreated and created -%} 49 |
50 | {%- endif -%} 51 | {%- if meta.timestampSettings.showUpdated and updated -%} 52 |
53 | {%- endif -%} 54 |
55 | {%- endif -%} 56 |
57 | {% for imp in dynamics.common.header %} 58 | {% include imp %} 59 | {% endfor %} 60 | {% for imp in dynamics.notes.header %} 61 | {% include imp %} 62 | {% endfor %} 63 |
64 | {% for imp in dynamics.common.beforeContent %} 65 | {% include imp %} 66 | {% endfor %} 67 | {% for imp in dynamics.notes.beforeContent %} 68 | {% include imp %} 69 | {% endfor %} 70 | {{ content | hideDataview | taggify | link | safe}} 71 | {% for imp in dynamics.common.afterContent %} 72 | {% include imp %} 73 | {% endfor %} 74 | {% for imp in dynamics.notes.afterContent %} 75 | {% include imp %} 76 | {% endfor %} 77 |
78 | 79 | {% if settings.dgShowBacklinks === true or settings.dgShowLocalGraph === true or settings.dgShowToc === true%} 80 | {%include "components/sidebar.njk"%} 81 | {% endif %} 82 | 83 | {% if settings.dgLinkPreview === true %} 84 | {%include "components/linkPreview.njk"%} 85 | {% endif %} 86 | {% include "components/references.njk" %} 87 | {% include "components/timestamps.njk" %} 88 | {% for imp in dynamics.common.footer %} 89 | {% include imp %} 90 | {% endfor %} 91 | {% for imp in dynamics.notes.footer %} 92 | {% include imp %} 93 | {% endfor %} 94 | {%include "components/lucideIcons.njk"%} 95 | 96 | 97 | -------------------------------------------------------------------------------- /src/site/notes/408/细分两种变址寻址🤔.md: -------------------------------------------------------------------------------- 1 | --- 2 | {"dg-publish":true,"dg-permalink":"/408/关于寻址方式","permalink":"/408/关于寻址方式/"} 3 | --- 4 | 5 | 6 | ### 1. 变址寻址 (Indexed Addressing) 7 | 8 | - **一句话说明:** 有效地址由“基准地址(在指令中)”加上“偏移量(在变址寄存器中)”得到。 9 | 10 | - **生活比喻:** 我告诉你我们的基地在“人民路”(基准地址),你要找的人住在从基地出发沿着门牌号走“100号”的位置(偏移量)。你的最终目的地 = 人民路 + 100号。 11 | 12 | - **指令示例:** `MOV AX, 100[SI]` 13 | 14 | - `SI` 是变址寄存器 (Source Index)。 15 | 16 | - **含义:** 有效地址 `EA = 100 + (SI)`,其中 `(SI)` 表示 `SI` 寄存器中的内容。然后取出主存地址 `EA` 处的数据放入 `AX`。 17 | 18 | - **执行分析:** 19 | 20 | 1. CPU取指、译码。 21 | 22 | 2. CPU将指令中的形式地址 `100` 和变址寄存器 `SI` 的内容在ALU中相加,得到有效地址 `EA`。 23 | 24 | 3. CPU使用 `EA` 访问主存,取出操作数。 25 | 26 | 4. 将操作数送入 `AX`。 27 | 28 | - **主要用途:** 处理数组。指令中的 `100` 是数组的首地址,变址寄存器 `SI` 中存放元素的下标(乘以元素大小后的偏移)。通过循环改变 `SI` 的值,就可以方便地遍历数组。 29 | 30 | - **考点陷阱:** 变址寄存器的内容是可变的(通常是循环变量),而指令中的地址是固定的(通常是数组首地址)。 31 | 32 | 33 | ### 总结对比表 34 | 35 | |寻址方式|有效地址 EA 的计算公式|操作数位置|访存次数|主要用途| 36 | |---|---|---|---|---| 37 | |**立即**|(无,操作数在指令中)|指令内部|0|赋常量| 38 | |**直接**|`EA = D`|主存|1|访问静态变量| 39 | |**间接**|`EA = (D)`|主存|2|指针、动态内存| 40 | |**寄存器**|(无,操作数在寄存器中)|寄存器|0|高频数据操作| 41 | |**寄存器间接**|`EA = (R)`|主存|1|按地址访问数据结构| 42 | |**变址**|`EA = D + (IX)`|主存|1|数组遍历| 43 | |**基址**|`EA = (BR) + D`|主存|1|程序重定位| 44 | |**相对**|`EA = (PC) + D` (这是转移地址)|指令流(下一条指令)|1 (取指)|程序分支、循环| 45 | 46 | **注:** `D`=指令中的形式地址, `R`=通用寄存器, `IX`=变址寄存器, `BR`=基址寄存器, `PC`=程序计数器, `(X)`=X的内容。访存次数指为获取一个操作数而访问主存的次数。 47 | 48 | ### 一、 答案 49 | 50 | 在现代计算机中,变址寄存器通常**存放的是原始的数组下标**,由CPU硬件在计算有效地址时**自动完成“下标乘以元素大小”**这个操作;但在简化的教学模型或一些早期设计中,也可能是要求程序员(或编译器)**事先算好偏移量**(即下标 * 元素大小)再放入变址寄存器。 51 | 52 | ### 二、两种变址寻址 53 | 54 | 变址寻址的两种实现模式:**基础变址寻址**和**比例变址寻址 (Scaled Indexed Addressing)**。 55 | 56 | #### 模式一:基础模型(编译器/程序员负责计算偏移量) 57 | 58 | 在这种简化的、经典的教学模型中,CPU的地址生成单元只包含一个加法器。 59 | 60 | - **变址寄存器的角色:** 存放的是**字节偏移量** (Byte Offset)。 61 | 62 | - **有效地址计算公式:** `EA = Base Address + (IX)` 63 | 64 | - 这里的 `(IX)` 就是最终的字节偏移。 65 | 66 | - **操作过程:** 67 | 68 | 1. **程序员/编译器层面:** 程序员使用逻辑下标 `i` (例如 `i = 3`)。如果要访问一个4字节的整型数组 `int arr[]`,编译器需要生成额外的指令,计算出字节偏移量 `Offset = i * 4 = 12`。 69 | 70 | 2. **指令执行前:** 必须有一条指令将计算出的 `Offset` (12) 载入变址寄存器 `IX`。 71 | 72 | 3. **变址寻址指令执行:** CPU执行 `MOV AX, arr_base[IX]` 时,直接将 `arr_base` 和 `IX` 中的 `12` 相加得到有效地址。 73 | 74 | 75 | **伪代码示例 (访问 `arr[i]`, int类型):** 76 | 77 | 代码段 78 | 79 | ``` 80 | ; 假设 i 存放在 CX 寄存器中 81 | MOV AX, CX ; 将 i 复制到 AX 82 | SHL AX, 2 ; 左移两位,等效于 AX = AX * 4,计算字节偏移量 83 | MOV SI, AX ; 将计算好的字节偏移量放入变址寄存器 SI 84 | MOV BX, arr[SI] ; 执行变址寻址,EA = arr基地址 + (SI) 85 | ``` 86 | 87 | - **结论:** 在这种模式下,变址寄存器里存放的是**已经乘好的**字节偏移量。乘法操作是在执行变址寻址**之前**由独立的指令完成的。 88 | 89 | 90 | #### 模式二:比例变址模型(CPU硬件负责计算偏移量) 91 | 92 | 在现代CPU(如x86系列)中,为了提高处理数组的效率,地址生成单元硬件本身就包含了移位器或乘法器,可以直接处理比例因子。 93 | 94 | - **变址寄存器的角色:** 存放的是**逻辑下标** (Logical Index)。 95 | 96 | - **有效地址计算公式:** `EA = Base Address + (IX) * Scale` 97 | 98 | - `Scale` 是比例因子,大小等于数组元素的大小(1, 2, 4, 8)。这个比例因子通常由操作码或指令的其他部分隐式或显式地指定。 99 | 100 | - **操作过程:** 101 | 102 | 1. **程序员/编译器层面:** 程序员使用逻辑下标 `i`。编译器直接将 `i` 的值生成指令载入变址寄存器 `IX`。 103 | 104 | 2. **指令执行前:** `IX` 中存放的就是 `i` 本身(例如 `3`)。 105 | 106 | 3. **变址寻址指令执行:** CPU在**一条指令的地址计算周期内**,自动完成 `(IX) * Scale` 和加法操作。硬件电路并行地或流水地完成这些计算。 107 | 108 | 109 | **x86汇编示例 (访问 `arr[i]`, int类型, 即dword):** 110 | 111 | 代码段 112 | 113 | ``` 114 | ; 假设 i 存放在 ECX 寄存器中, arr 基地址在 EBX 中 115 | MOV EAX, [EBX + ECX * 4] ; 一条指令完成所有操作 116 | ``` 117 | 118 | - **解释:** 这条指令明确告诉CPU: 119 | 120 | - 基地址在 `EBX` 中。 121 | 122 | - 逻辑下标在 `ECX` 中。 123 | 124 | - 比例因子是 `4` (因为是 `dword` 数组)。 125 | 126 | - CPU硬件会计算 `(EBX) + (ECX) * 4` 来得到最终的有效地址。 127 | 128 | - **结论:** 在这种模式下,变址寄存器里存放的是**原始的数组下标**。乘法操作是作为地址计算的一部分,由CPU硬件在执行该指令**期间自动完成的**。 129 | 130 | 131 | --- 132 | -------------------------------------------------------------------------------- /src/site/notes/408/加法器家族的内卷史:从傻等前任的串行废物到预知未来的并行天才🤔.md: -------------------------------------------------------------------------------- 1 | --- 2 | {"dg-publish":true,"dg-permalink":"/408/加法器进化史:从手拉手传话到开全员广播大会!🤯","permalink":"/408/加法器进化史:从手拉手传话到开全员广播大会!🤯/"} 3 | --- 4 | 5 | 6 | 7 | ![image-3.png|423x290](/img/user/%E9%99%84%E4%BB%B6/image-3.png) 8 | 9 | --- 10 | 11 | 从全加器到并行进位加法器的核心改进思想,是通过增加复杂的“预判”逻辑来解除对进位信号的线性、串行依赖,从而实现各位并行计算,以空间换时间,大幅提升运算效率。 12 | 13 | ### 一、 要掌握什么 14 | 15 | - **全加器与串行加法器**:理解一位全加器的逻辑结构,并掌握由多位全加器构成串行进位加法器(又称行波进位加法器)的原理及其延迟分析。 16 | 17 | - **并行进位加法器**:理解并行进位加法器的设计思想,重点在于理解其如何通过进位生成(Generate)和进位传递(Propagate)的概念来加速进位链的产生,从而实现快速加法。 18 | 19 | - **带符号数的加减法运算**:掌握补码加减法运算规则,深刻理解“减法变加法”的硬件实现。 20 | 21 | - **溢出判断**:掌握补码加法运算中溢出的概念、原因和硬件检测方法(双符号位法和单符号位法),这是绝对的重点与难点。 22 | 23 | 24 | ### 二、 演进过程详解 25 | 26 | 我们来一步步看这个家族是如何“内卷”和“进化”的。 27 | 28 | #### 1. 卑微的基石:全加器 (Full Adder) 29 | 30 | 一切始于最基础的“工人”——**一位全加器**。你可以把它想象成一个只负责处理个位数加法的初级会计。 31 | 32 | - **输入**:他需要三个数字:当前位的两个加数(Ai​ 和 Bi​),以及来自更低一位的进位(Ci​)。 33 | 34 | - **输出**:他只做两件事:算出当前位的和(Si​),以及要不要向更高一位进位(Ci+1​)。 35 | 36 | 37 | 这个小工人本身效率很高,但他的致命弱点是:**他必须等拿到低位传来的进位信号后,才能开始工作**。 38 | 39 | #### 2. 效率低下的流水线:串行进位加法器 (Ripple-Carry Adder) 40 | 41 | 如果我们想算一个多位数(比如8位数)的加法,最直观的方法就是找8个“一位全加器”工人,让他们排成一队。这就是**串行进位加法器**,也因为其进位信号像波浪一样一波波向高位传递,而被称为**行波进位加法器**。 42 | 43 | - **工作模式**: 44 | 45 | 1. 第0位工人(处理最低位)最先开工,因为它没有“更低一位”,我们可以认为它的进位输入 C0​ 为0。 46 | 47 | 2. 它算出结果 S0​ 和给第1位的进位 C1​。 48 | 49 | 3. 第1位工人一直在等 C1​,拿到后立刻开工,算出 S1​ 和给第2位的进位 C2​。 50 | 51 | 4. …… 52 | 53 | 5. 这个过程一直持续到最高位(第7位)工人拿到第6位传来的进位 C7​,完成计算后,整个8位加法才算结束。 54 | 55 | - 核心矛盾:**依赖** 56 | 57 | 这里的效率瓶颈非常明显:严重的串行依赖。第 i 位的计算结果,完全依赖于第 i−1 位的进位输出。就像一条生产线,后一道工序必须等前一道工序完成后才能启动。如果加法器有 n 位,而每个全加器的延迟是 T,那么总延迟大约就是 n×T。位数越多,等待时间越长,CPU完全无法忍受这种“龟速”。 58 | 59 | 60 | #### 3. 天才的诞生:并行进位加法器 (Parallel-Carry Adder) 61 | 62 | 为了打破这种“我等你,你等他”的僵局,天才的工程师想出了一个办法:我们能不能不傻等,而是**提前预测出每一位的进位**?这就是**并行进位加法器**(也称先行进位加法器,Carry-Lookahead Adder)的核心思想。 63 | 64 | - 解除依赖的武器: 65 | 66 | 它引入了一个独立、且更聪明的“领导”部门——进位生成逻辑 (Carry-Lookahead Logic)。这个部门不直接算加法,而是专门用来光速计算出所有位的进位信号。它是怎么做到的呢?它发现,对于任何一位 i 来说,它未来是否会产生进位,只取决于两种情况: 67 | 68 | 1. **“本地自产” (Generate)**:不管低位给不给进位,我自己就能产生一个进位。这种情况只发生在 Ai​=1 且 Bi​=1 时。我们把这个叫做**进位生成信号** gi​。 69 | 70 | 2. **“上游传递” (Propagate)**:我自己不一定能产生进位,但如果低位给了我一个进位,我保证能把它原封不动地传给下一位。这种情况发生在 Ai​ 和 Bi​ 中有一个是1时(Ai​⊕Bi​=1 或 Ai​+Bi​=1)。我们把这个叫做**进位传递信号** pi​。 71 | 72 | - **工作模式革命**: 73 | 74 | 1. 加法开始时,所有位的 Ai​ 和 Bi​ 都是已知的。于是,我们可以在一瞬间,并行地计算出所有位的 gi​ 和 pi​ 信号。 75 | 76 | 2. 那个聪明的“领导部门”拿到所有的 gi​ 和 pi​ 后,通过一个(虽然复杂但极快)的纯组合逻辑电路,**同时推导出所有位的最终进位 C1​,C2​,…,Cn​**。 77 | 78 | - 比如 C1​ 产生,要么是第0位“本地自产”(g0​),要么是第0位能“传递”(p0​)且最初就有个进位(C0​)。 79 | 80 | - C2​ 产生,要么是第1位“本地自产”(g1​),要么是第1位能“传递”(p1​)且它收到了来自第0位的进位(C1​)。把 C1​ 的逻辑代入,就可以发现 C2​ 完全可以由 g1​,p1​,g0​,p0​,C0​ 直接算出,无需等待。 81 | 82 | 3. 一旦所有进位 Ci​ 都被“预知”并送达,那8个“工人”(全加器)就不再需要排队了,他们可以**同时开工**,用各自的 Ai​,Bi​ 和被光速送达的 Ci​ 来计算最终的和 Si​。 83 | 84 | - 核心改进:空间换时间 85 | 86 | 并行加法器通过增加一套复杂的、专门用于“预判”进位的逻辑电路,打破了进位信号的逐级传递依赖,实现了所有进位的同时计算。这是一种典型的**以硬件的复杂度(空间)换取运算速度(时间)**的优化策略。 87 | 88 | 89 | #### 4. 灵魂升华:带符号加法器与溢出判断 90 | 91 | 前面的加法器本质上只处理无符号数。要让它能处理带符号数(通常用补码表示),硬件本身**不需要做任何改变**。一个n位的加法器,既可以计算无符号数加法,也可以计算补码表示的带符号数加法。 92 | 93 | 真正的区别在于**对结果的解读和对“错误”的判断**。这个“错误”就是**溢出 (Overflow)**。 94 | 95 | - **什么是溢出?** 对于一个n位的补码数,它能表示的范围是有限的。当两个正数相加,结果超出了能表示的最大正数(正溢出);或两个负数相加,结果小于了能表示的最小负数(负溢出),就发生了溢出。注意:**一个正数和一个负数相加,永远不会溢出**。 96 | 97 | - 如何判断溢出?(考研核心) 98 | 99 | 硬件必须提供一个信号来告诉CPU“算错了!”。主要有两种判断方法: 100 | 101 | 1. **双符号位法(变形补码)**:用两位来表示符号位,如`00`表示正,`11`表示负。运算后,如果新的双符号位变成`01`(正溢出)或`10`(负溢出),则表示发生了溢出。这种方法直观,但会多占用一位。 102 | 103 | 2. **单符号位法(最常用)**:这是考研最常考的方法。它通过比较**最高数值位(符号位前一位)向符号位的进位(Cn−1​)**和**符号位产生的进位(Cn​)**来判断。 104 | 105 | - **判断公式**:V=Cn−1​⊕Cn​ (⊕ 代表异或)。 106 | 107 | - **解读**:如果这两个进位**不相同**(一个为0,一个为1),则表示**溢出**;如果**相同**(都为0或都为1),则表示**未溢出**。 108 | 109 | -------------------------------------------------------------------------------- /src/site/notes/408/操作系统当中的引导程序.md: -------------------------------------------------------------------------------- 1 | --- 2 | {"dg-publish":true,"dg-permalink":"/408/操作系统当中的引导程序","permalink":"/408/操作系统当中的引导程序/"} 3 | --- 4 | 5 | 6 | ### 二、引导过程详解 (Booting Process) 7 | 计算机的启动是一个“接力”过程,由一系列程序按顺序执行,前一个程序为后一个功能更强的程序准备好运行环境,并最终将控制权交给操作系统内核。整个过程可以分解为以下几个核心步骤: 8 | 9 | #### 1. 硬件加电,执行BIOS/UEFI (固件阶段) 10 | - **过程描述**:当你按下电源按钮,主板通电。CPU被重置,其内部的程序计数器(PC)被强制设置为一个预设的内存地址。这个地址指向主板上一块只读存储器(ROM)中的程序,这个程序就是**基本输入输出系统(BIOS)** 或其现代替代品**统一可扩展固件接口(UEFI)**。 11 | - **核心功能**: 12 | - **加电自检 (POST, Power-On Self-Test)**:BIOS首先会对计算机的关键硬件(如CPU、内存、显卡、磁盘控制器等)进行检测,确保它们能正常工作。 13 | - **初始化硬件**:对硬件进行一些基本的初始化设置。 14 | - **查找并加载引导程序**:BIOS会根据预设的启动顺序(如硬盘 -> U盘 -> 光驱)去检查存储设备的第一个扇区。 15 | 16 | 17 | #### 2. 加载并执行主引导记录 (MBR) 18 | - **过程描述**:BIOS在找到一个可启动的磁盘后,会读取该磁盘的**第一个扇区(0磁道0柱面1扇区)**,这个扇区被称为**主引导记录 (Master Boot Record, MBR)**。然后,BIOS将这512字节的内容完整地加载到内存的一个约定好的地址(通常是 `0x7C00`)。最后,BIOS会通过一条跳转指令,将CPU的控制权交给这段刚刚加载到内存的代码。 19 | - **MBR结构 (重要考点)**:一个标准的MBR扇区大小为512字节,其结构如下: 20 | - **主引导程序 (Bootloader)**:占大约446字节,这是我们通常所说的**引导程序**。它的代码非常精简。 21 | - **磁盘分区表 (Partition Table)**:占64字节,描述了磁盘上的分区信息(如哪个是活动分区)。 22 | - **结束标志 (Magic Number)**:占2字节,必须为 `55AAH`。BIOS通过检查这个标志来确认这是否是一个有效的MBR。 23 | - **核心功能**: MBR中的引导程序功能非常单一,因为它太小了。它的核心任务是:**利用分区表找到“活动分区”(即操作系统所在的分区),然后加载并执行该活动分区中的另一个更强大的加载程序(有时称为“分区引导记录”或“操作系统加载程序”)。** 24 | 25 | 26 | #### 3. 加载并执行操作系统加载程序 (OS Loader) 27 | - **过程描述**:MBR中的引导程序将控制权交给活动分区中的操作系统加载程序(例如Windows的 `bootmgr` 或Linux系统中的GRUB第二阶段加载程序)。 28 | - **核心功能**:这个加载程序比MBR中的引导程序强大得多。它通常**具备识别文件系统的能力**。它的核心任务是: 29 | - 在文件系统中找到并读取操作系统的**内核文件**(例如Linux的 `vmlinuz`、Windows的 `ntoskrnl.exe`)。 30 | - 将内核文件完整地加载到内存中。 31 | 32 | 33 | #### 4. 加载并启动操作系统内核 34 | - **过程描述**:操作系统加载程序在完成内核加载后,会将CPU的控制权彻底交给内存中的内核代码的入口点。 35 | - **核心功能**:从这一刻起,**操作系统正式开始启动**。内核会接管所有硬件控制权,并执行以下操作: 36 | - **自身初始化**:建立内核所需的数据结构(如进程表、内存管理表等)。 37 | - **硬件驱动加载**:探测并加载系统中所有硬件的驱动程序。 38 | - **创建初始进程**:创建操作系统的第一个用户态进程(在类Unix系统中通常是 `init` 或 `systemd` 进程),此进程是所有其他用户进程的祖先。 39 | - **启动系统服务,等待用户登录**:启动各种后台服务,最终进入我们熟悉的登录界面或命令行。 40 | 41 | --- 42 | 43 | 44 | 45 | ### 三、流程图 46 | 为了更清晰地理解这个“接力”过程,请看下面的流程图: 47 | 48 | 代码段 49 | 50 | ```mermaid 51 | graph TD 52 | A[用户按下电源] --> B{CPU执行BIOS/UEFI}; 53 | B --> C{加电自检 POST}; 54 | C --> D{按启动顺序查找设备}; 55 | D --> E[找到启动盘, BIOS加载其MBR到内存]; 56 | E --> F{CPU执行MBR中的引导程序}; 57 | F --> G{引导程序扫描分区表, 找到活动分区}; 58 | G --> H[加载并执行活动分区的OS Loader]; 59 | H --> I{OS Loader加载OS内核到内存}; 60 | I --> J{OS Loader交出控制权, CPU执行内核代码}; 61 | J --> K[内核初始化, 加载驱动, 创建init进程]; 62 | K --> L[系统启动完成, 等待用户登录]; 63 | 64 | style A fill:#f9f,stroke:#333,stroke-width:2px 65 | style L fill:#ccf,stroke:#333,stroke-width:2px 66 | ``` 67 | 68 | --- 69 | 70 | 71 | 72 | ### 四、常考点分析、命题方式与陷阱 73 | 1. **概念辨析(选择题)** 74 | 75 | - **陷阱**:混淆BIOS和引导程序。BIOS是固化在主板上的固件,它的**作用是加载第一个引导程序(MBR)**,但它本身不是我们通常意义上讨论的、位于硬盘上的引导程序。 76 | - **考点**:MBR的结构和作用。题目会问MBR的大小(512B)、结束标志(`55AAH`)、包含的内容(引导代码+分区表)。会考察MBR引导程序的核心功能是加载下一个加载程序,而不是直接加载内核。 77 | 2. **流程排序(选择题/综合题)** 78 | 79 | - **命题方式**:给出一系列事件,要求按正确的时间顺序排列。例如:①加载内核 ②执行POST ③加载MBR ④执行init进程。正确顺序为 ② -> ③ -> ① -> ④。 80 | - **陷阱**:注意区分**程序**和**进程**。在内核完全启动并创建第一个进程(如init)之前,整个引导过程中执行的代码(BIOS、MBR、OS Loader)都只是**程序**,还不存在操作系统“进程”这个概念。进程是操作系统创建并管理的概念。 81 | 3. **位置辨析(选择题)** 82 | 83 | - **考点**:BIOS存在哪里?(主板ROM芯片)。MBR存在哪里?(启动磁盘的第一个物理扇区)。内核文件存在哪里?(磁盘特定分区的文件系统中)。它们最终被加载到哪里去执行?(RAM/内存)。 84 | 85 | --- 86 | 87 | 88 | 89 | ### 五、边界情况与易错点 90 | 1. **用户态与核心态**:在整个引导过程中,直到操作系统内核完全接管之前,CPU通常运行在**实模式**或**保护模式**下,但始终处于最高权限级别,可以执行任何指令。进入核心态/用户态的划分,是操作系统内核建立起来的概念。内核本身的代码运行在**核心态**。 91 | 92 | 2. **引导失败**:如果在任何一个环节出错(如POST失败、找不到启动设备、MBR损坏、内核文件丢失),计算机会停机并显示相应的错误信息,例如 "Operating System not found"。 93 | 94 | ```mermaid 95 | graph TD 96 | A[用户按下电源] --> B{CPU执行BIOS/UEFI}; 97 | B --> C{加电自检 POST}; 98 | C --> D{按启动顺序查找设备}; 99 | D --> E[找到启动盘, BIOS加载其MBR到内存]; 100 | E --> F{CPU执行MBR中的引导程序}; 101 | F --> G{引导程序扫描分区表, 找到活动分区}; 102 | G --> H[加载并执行活动分区的OS Loader]; 103 | H --> I{OS Loader加载OS内核到内存}; 104 | I --> J{OS Loader交出控制权, CPU执行内核代码}; 105 | J --> K[内核初始化, 加载驱动, 创建init进程]; 106 | K --> L[系统启动完成, 等待用户登录]; 107 | 108 | style A fill:#f9f,stroke:#333,stroke-width:2px 109 | style L fill:#ccf,stroke:#333,stroke-width:2px 110 | ``` 111 | -------------------------------------------------------------------------------- /src/site/notes/408/DRAM结构组成以及一个数据流经她全身的例子🥵.md: -------------------------------------------------------------------------------- 1 | --- 2 | {"dg-publish":true,"dg-permalink":"/408/DRAM结构组成以及一个数据流经她全身的例子🥵","permalink":"/408/DRAM结构组成以及一个数据流经她全身的例子🥵/"} 3 | --- 4 | 5 | 6 | ![Pasted image 20250703210835.png](/img/user/%E9%99%84%E4%BB%B6/Pasted%20image%2020250703210835.png) 7 | ### 一、 DRAM 内部结构图解读 8 | #### 1. 核心部件分析 9 | 10 | - **定时和控制 (Timing and Control):** 这是DRAM的大脑。它接收外部控制信号,并生成所有内部操作所需的时间序列和控制逻辑。 11 | 12 | - **RAS (Row Address Strobe, 行地址选通)**: 低电平有效。当RAS信号下降时,DRAM会锁存(Latch)地址线上的信号作为**行地址**。 13 | 14 | - **CAS (Column Address Strobe, 列地址选通)**: 低电平有效。当CAS信号下降时,DRAM会锁存地址线上的信号作为**列地址**。 15 | 16 | - **WE (Write Enable, 写使能)**: 控制读写模式。WE为低电平时为**写操作**,高电平时为**读操作**。 17 | 18 | - **OE (Output Enable, 输出使能)**: 控制数据输出缓冲器。只有在读操作且OE有效时,数据才会被驱动到数据引脚上。 19 | 20 | - **地址输入 (A₀ ~ A₁₀):** 如图所示,该芯片有11条地址线。通过地址复用技术,这11条线既可以传输行地址,也可以传输列地址。 21 | 22 | - **地址缓冲器与MUX:** 23 | 24 | - **行/列地址缓冲器**: 临时存储从地址引脚上接收到的行地址和列地址。 25 | 26 | - **MUX (Multiplexer, 多路选择器)**: 这是一个关键部件。它负责选择将哪个地址送入行译码器:是来自**地址缓冲器**的地址(用于正常读写),还是来自**刷新计数器**的地址(用于刷新)。 27 | 28 | - **刷新计数器 (Refresh Counter):** 29 | 30 | - 这是一个内部计数器,它会自动、依次地生成所有行的地址(0, 1, 2, ..., 2047)。其唯一的目的就是为**刷新操作**提供行地址。 31 | 32 | - **存储阵列 (Storage Array): `2048 × 2048 × 4`** 33 | 34 | - 这是DRAM的核心,由我们上一问中讲的1T1C存储元组成。 35 | 36 | - `2048 × 2048`:表示这个阵列有 **2048 行** 和 **2048 列**。这与地址线数量完美对应:211=2048,因此需要11位行地址和11位列地址。 37 | 38 | - `× 4`:表示这个芯片有**4个这样的存储阵列**(也称位平面)。当给出一个行、列地址时,会同时在4个阵列的对应位置进行操作,一次可以读/写**4位数据**。这决定了芯片的数据位宽为4。 39 | 40 | - **译码器与读出放大器:** 41 | 42 | - **行译码器 (Row Decoder)**: 接收11位行地址,从2048条**字选择线 (Word Line)** 中唯一选中一条,激活一整行的存储元。 43 | 44 | - **读出放大器 (Sense Amplifier) / I/O门**: 当一行被激活时,这一整行(2048×4个)存储元的数据都会被读入到读出放大器中。读出放大器负责检测微弱的电压信号、将其放大并作为临时缓存。 45 | 46 | - **列译码器 (Column Decoder)**: 接收11位列地址,从读出放大器缓存的一整行数据中,选择出目标列对应的4位数据,并将其连接到数据输入/输出缓冲器。 47 | 48 | - **数据缓冲器与数据线 (D₁ ~ D₄):** 49 | 50 | - **数据输入/输出缓冲器**: 负责管理数据进出芯片。 51 | 52 | - **D₁ ~ D₄**: 芯片的4位数据引脚,用于与外部(如内存控制器)交换数据。 53 | 54 | 55 | --- 56 | 57 | ### 二、 实例:数据的流动与生命周期 58 | 59 | 让我们以一个具体的例子,追踪4位数据 `1011` 在这个DRAM芯片中的完整生命旅程。 60 | 61 | 假设我们要将数据 `1011` **写入**到由 **行地址 5 (二进制 `0...00101`)** 和 **列地址 88 (二进制 `0...1011000`)** 指定的单元中,然后再将其**读出**。 62 | 63 | #### 阶段一:写入操作 (数据的诞生) 64 | 65 | 1. **准备阶段**: 内存控制器准备好了地址(行5,列88)和数据(`1011`)。 66 | 67 | 2. **发送行地址**: 控制器将**行地址 5** 放到地址线 `A₀~A₁₀` 上。 68 | 69 | 3. **RAS 选通**: 控制器拉低 `RAS` 信号。DRAM芯片内部的**行地址缓冲器**立即锁存地址 `5`。 70 | 71 | 4. **行激活**: **行译码器**解码地址 `5`,激活存储阵列中的第5条**字选择线(Word Line)**。此时,第5行所有的 `2048 × 4` 个存储元都被连接到各自的**读出放大器**上,整行数据被载入并暂存其中(这是读操作的前半部分,写操作也需要这一步,称为Read-Modify-Write)。 72 | 73 | 5. **发送列地址**: 控制器将**列地址 88** 放到同样的地址线 `A₀~A₁₀` 上。 74 | 75 | 6. **发送数据**: 控制器将数据 `1011` 放到数据线 `D₁~D₄` 上。 76 | 77 | 7. **CAS 与 WE 选通**: 控制器同时拉低 `CAS` 和 `WE` 信号。 78 | 79 | - `CAS` 的下降使**列地址缓冲器**锁存地址 `88`。 80 | 81 | - `WE` 的低电平告知**定时和控制**单元,这是一个**写操作**。 82 | 83 | 8. **列选择与写入**: **列译码器**解码地址 `88`。**数据输入缓冲器**接收 `1011`,并通过I/O门将其写入到**读出放大器**中第88列的位置,覆盖了原来暂存的数据。 84 | 85 | 9. **写回存储元**: 读出放大器中被更新的数据(`1011`)被重新写回到第5行、第88列的4个物理**存储电容 `C_s`** 中。 86 | 87 | 10. **操作结束**: `RAS`, `CAS`, `WE` 信号恢复高电平,一次写周期完成。数据 `1011` 作为电荷被存储在四个微小的电容器中。 88 | 89 | 90 | #### 阶段二:静默与刷新 (数据的维生) 91 | 92 | - **电荷泄漏**: 存储着'1'的电容开始缓慢漏电,电压下降。若不干预,数据将在几十毫秒内丢失。 93 | 94 | - **刷新周期**: 在数据丢失前,DRAM芯片的**定时和控制**单元会启动一次刷新。 95 | 96 | 1. 内部**刷新计数器**提供一个行地址(假设恰好是 `5`)。 97 | 98 | 2. `MUX` 切换到刷新计数器通路。 99 | 100 | 3. 控制器执行一个“仅RAS”周期,即只拉低`RAS`。 101 | 102 | 4. 行译码器激活第5行,将整行数据(包括我们那个电压已略微衰减的 `1011`)读入**读出放大器**。 103 | 104 | 5. 读出放大器将这个衰减的信号**重新放大**到标准的逻辑高/低电平,并**立即写回**到原来的存储元中。 105 | 106 | - **生命延续**: 我们的数据 `1011` 被成功“续命”,电容被重新充满。这个过程对外部系统是透明的。 107 | [[408/DRAM都会怎么洗澡🥵\|DRAM都会怎么洗澡🥵]] 108 | 109 | #### 阶段三:读出操作 (数据的价值实现) 110 | 111 | 1. **行地址选通**: 与写入操作的步骤1-4完全相同。控制器发送行地址 `5`,`RAS` 下降,第5行的全部内容被读入并暂存在**读出放大器**中。这个过程也刷新了第5行的数据。 112 | 113 | 2. **列地址选通**: 控制器发送列地址 `88`,然后拉低 `CAS` 信号 (`WE` 保持高电平,表示读取)。 114 | 115 | 3. **列选择**: **列译码器**解码地址 `88`,从读出放大器中选中暂存的4位数据 `1011`。 116 | 117 | 4. **输出使能**: 控制器拉低 `OE` 信号,打开**数据输出缓冲器**的三态门。 118 | 119 | 5. **数据上路**: 数据 `1011` 被驱动到外部数据线 `D₁~D₄` 上。 120 | 121 | 6. **读取成功**: 内存控制器从数据线上成功读取 `1011`。 122 | 123 | 7. **操作结束**: `RAS`, `CAS`, `OE` 恢复高电平。读周期结束。 124 | 125 | --------------------------------------------------------------------------------