[{"content":"这个博客用的是 Hugo + Stack 主题，2026 年 1 月建站，到现在改过三轮。这篇把三个阶段的改动整理成一条线：换了什么、为什么换、踩了哪些坑。内容偏记录性质，不做教程。\n三个阶段大致是这样分的：\n阶段 时间 重点 搭建期 2026-01 框架落地、内容迁移、页面骨架 内容期 2026-05 ~ 07 学习笔记批量入库，前端顺手调整 重构期 2026-09 视觉体系重建：配色、手绘风、独立页、随心记 一、搭建期：框架落地 1.1 从零到能跑 2026 年 1 月 11 日那天的提交密度很高（十几条），基本是一天之内把站点跑起来：Initial commit 之后紧跟着 index、comment、post image、remover 标签 and add toc，然后是内容批量入库。\n技术选型上没有纠结太久。之前用宝塔面板 + Typecho 搭过一个动态博客（2023 年那篇《宝塔面板+typecho》记的就是那个），这次改用 Hugo 是因为它编译成纯静态，扔到 GitHub Pages 就完事，不需要维护服务器。\n1.2 内容结构化 建站初期最集中的工作是给文章补 front matter。1 月 13 日有一条提交叫 catrgory（拼错了，无伤大雅），一次性给 7 篇 AI 笔记补上分类字段。这种批量整理是必要的——分类、标签、目录这些结构信息如果不从第一天就规范化，后面会越积越乱。\n同期还删了主题自带的示例分类 content/categories/example-category/，以及做了两处页面调整：\ncontent/page/about/index.md 重写（49 行） content/page/travelling/index.md 新建，配 assets/icons/train-subway.svg 图标 到这一步，站点骨架成型：content/post/ 放文章，content/page/ 放独立页，四个栏目 Home / Archives / About / Travelling 挂在导航上。\n1.3 评论与部署 comment 这条提交接入了评论系统，最终用的是 Waline（serverURL 指向一个 Vercel 实例）。部署走 GitHub Actions，push 到 master 触发构建，产物推到同仓库的 gh-pages 分支。\n这里有个细节值得记一下：源码和站点产物放在同一个仓库的两个分支里（master 存源码、gh-pages 存构建结果），配置自洽，不需要额外的部署仓库。\n二、内容期：笔记入库与布局微调 5 月主要是往仓库里搬学习笔记，CS146S 的周记从 week1 一直补到 week9，中间夹着 LangChain、MCP 这些 AI 主题的整理。前端改动零散但有几个有意义：\n2.1 分类挂件与页脚 8359bff 这条「前端调整」给 custom.scss 加了 70 行，同时动了两处布局：layouts/partials/footer/footer.html 补了 26 行，layouts/partials/widget/categories.html 微调。这个阶段还是零敲碎打，没有形成体系——改一个地方就写一段 CSS，样式之间没有统一的设计变量。\n2.2 归档页与索引调整 5 月 14 日连续几条 adjust、archives、前端调整 提交，主要在调归档页的展示。同期还处理过一次 git 冲突（1cff054 fix leftover git conflict markers in posts），说明当时有过一次不太顺利的合并。这些痕迹后来都清掉了。\n2.3 关于主题目录 52428df 这条提交叫 ignore themes directory，把 themes/ 排除出版本控制。原因是主题通过 Hugo Modules 引入（config/_default/module.toml 里声明 github.com/CaiJimmy/hugo-theme-stack/v3），themes/Stack/ 是个空目录，真正的内容在模块缓存里。这意味着改主题只有两条路：覆盖 layouts/partials 里的同名文件，或者用 CSS 覆盖。 后面重构期的所有改动都遵循这个原则。\n7 月只有一条 add，基本是间歇期。\n三、重构期：视觉体系重建 9 月是改动最密集的时段，40 条提交。这不是零散的调整，是一次完整的视觉体系重建。\n3.1 配色：藕粉 + 米白纸张 起点是 c9ca501「藕粉配色 + 渐变封面 + 微交互，整体视觉焕新」。核心决策是换掉主题默认的灰蓝 #34495e，改用藕粉 #d49ba8，页面背景从纯白改成微暖的 #fdf8f9。\n选藕粉不是随手挑的。之前的头像是浅灰紫 / 浅粉紫调（主色约 #D0D0E0、#E0D0E0），饱和度极低、明度偏高，属于莫兰迪浅调。配色如果跟着这个走，就得保持低饱和 + 高明度，避免霓虹色和高饱和撞色。藕粉正好落在这个区间。\n同期修了一个隐蔽问题：主题没有定义 --accent-color-rgb，而 custom.scss 里有几处 rgba(var(--accent-color-rgb), .1) 依赖它。变量缺失时不会报错，只是那些背景色静默失效——侧边栏 hover 高亮就属于这种情况。补上定义之后才正常。\n3.2 手绘春日风 ea195f1「手绘春日风格改造」把配色推进到风格层，有四个动作：\n背景从 #fdf8f9 再调成米白纸张色 #fdf9f2，增加薄荷绿 / 淡黄 / 浅棕三个点缀色 标题字体换成手写体「霞鹜文楷」，通过 CDN 加载 卡片和挂件改用不规则边框（wobbly border），模拟手绘的不规则感 樱花元素统一加浅棕墨水描边，分隔线改成手绘波浪 这里踩了一个值得记的坑：字体规则不能写在 SCSS 里。Hugo 压缩 CSS 时会把带空格的字体名 \u0026quot;LXGW WenKai\u0026quot; 处理成小写无引号的形式，字体就失效了。最后把字体规则内联写进 layouts/partials/head/custom.html 的 \u0026lt;style\u0026gt; 标签里，绕过压缩器，问题解决。\n3.3 装饰体系 1c48adc 到 fdf1422 之间是一连串装饰相关的提交，最终保留下来的是：\n装饰 状态 樱花花瓣飘落 保留 标题花饰 保留 花枝横幅 + 底部小树 + 花枝分隔线 保留 导航栏花枝、云朵飘过 保留 页面底部左右溜达的小猫咪 做了 → 重画 → 移除 鼠标移动花瓣轨迹 做了 → 移除 卡片鼠标跟随光斑 保留 深色模式樱花 保留 有两条是做了又拆的。猫咪前后改了三次（新增 → 重画更可爱 + 放慢速度 → 移除蹲坐小猫），最后只保留云朵和导航栏花枝。鼠标花瓣轨迹则是被主动删掉的——它跟着光标撒花瓣，刚做出来觉得挺有意思，实际用起来干扰阅读，尤其影响文章页。\n这里有个判断：装饰的价值不在数量，在于它是否干扰内容。 背景元素（花瓣飘落、云朵）跟内容是分层的，不抢注意力；跟随鼠标的元素（花瓣轨迹、光斑）跟内容同层，就会让人分心。重构期最后的取舍基本是按照这条线来的。\n还有一处实现细节：花瓣轨迹的 CSS 里原本写的是 animation: petal-trail var(--dur-slow) var(--ease-out)-out forwards —— var(--ease-out)-out 是拼接错误，这条动画从写下去就没生效过。删的时候才发现。\n3.4 设计 token 落地 727c272「建立设计 token 并统一列表与正文排版」是个分水岭。在此之前所有样式都是散着写的（比如各处硬编码 border-radius: 12px），这条提交之后统一收口成变量：\n1 2 3 4 5 --radius-wobbly 手绘不规则圆角 --radius-wobbly-sm 小尺寸版本 --shadow-lift 悬浮阴影 --dur-base 基础动画时长 --ease-out 缓动曲线 这一步的实际收益不是「看起来整齐」，而是后面改视觉时只需要动变量值。比如从米白调到纸张色、从灰蓝调到藕粉，都是改几个变量的量级，不用去各处找硬编码。\n3.5 功能补全 同期还补了几个功能性组件：\n图片灯箱：点击正文图片放大查看 返回顶部进度环：按钮边缘的圆环跟随滚动进度 移动端底部导航：手机端底部固定栏 代码块美化：语言角标 + 复制按钮 阅读进度条：顶部横向进度（仅文章页） 这些的实现都遵循了「尊重 prefers-reduced-motion」和「IntersectionObserver 不可用时直接显示」两条兜底原则，避免在低端设备或特殊设置下出现内容不可见。\n3.6 封面：从有到无 封面这块改了很多轮，值得单独说，因为它反映了一个反复的决策过程：\n版本 做法 结果 1 主题默认，无封面 列表页略显单调 2 客户端随机动漫图 API 每次刷新都变，列表和详情不同图，否决 3 本地 16 张图随机 重复太多 4 本地 80 张图按哈希固定映射 每篇永久对应同一张，可用 5 「完整图 + 同图模糊铺底」双图层 解决竖图横图比例不一导致的裁切/留白 6 全部删除 最终方案 第 4 版的技术点是 mod (hash.FNV32a .RelPermalink) 80 —— 用页面路径的哈希取模，保证每篇文章永久对应同一张图，列表页和详情页天然一致，不会随机变化。\n第 5 版解决的是图片比例问题。那 80 张图宽高比从 0.71（竖）到 1.78（横）都有，单一 object-fit 无法兼顾：用 cover 会裁掉画面，用 contain 会留白。最后的方案是双层——底层用 cover + blur(14px) 铺满，上层用 contain 完整显示，两层叠加看不出留白。\n第 6 版全部删除（3c02650）。理由其实很简单：这些封面跟文章内容没有任何关系，纯粹是为了让列表页不空。而且 slug 哈希这个机制本身不透明——你没法通过修改文章来控制封面。删掉之后列表页靠排版和分类角标撑住，反而更干净。\n清得不太干净，header.html 简化后列表卡片上还挂着 has-cover class。因为 $image.exists 恒为 false，每张卡片都挂着一个空 class。后来 grep 编译后的 CSS 确认这个选择器出现 0 次，才删掉 else 分支。\n四、独立页：三个页面的改造 About / Links / Travelling 三个独立页一开始跟整体风格不搭。这个问题的诊断过程比修复过程更有意思。\n4.1 诊断：找到共同的病根 最初的判断是「字体和间距不对」，但动手前先做了一件事——把所有背景色变量拉出来对比：\n变量 值 页面背景 --body-background #fdf9f2 米白 卡片背景 --card-background #ffffff 纯白 问题出在这。文章页是一张纯白卡片浮在米白页面上，但文章内容足够密（正文、代码块、配图把卡片填满了），白底看起来像「纸张」；独立页内容稀疏，同样一张纯白卡片铺在那里就是一大片空白。\n所以「风格不搭」的表象，底层是内容密度差异把同一个颜色衬出了不同观感。判断的时候要分清是「颜色本身不对」还是「颜色在这个场景下不对」，这次是后者。\n4.2 --card-padding：理解横向对齐的钥匙 Stack 主题横向排版全靠一个变量 --card-padding（响应式取 20 / 25 / 30px）：\n1 2 3 4 5 6 7 8 9 10 11 12 // 内容靠 padding 内缩 .article-content { padding: 0 var(--card-padding); } // 但某些块要顶到满宽，用等量负 margin 反向拉出去 .article-content blockquote, .article-content figure, .article-content .highlight, .article-content pre { margin-left: calc(var(--card-padding) * -1); margin-right: calc(var(--card-padding) * -1); width: calc(100% + var(--card-padding) * 2); } 这是故意的设计：代码块、引用块、图片比正文宽出两侧 30px，形成通栏出血，把阅读节奏切成几段。\n理解这一条，所有横向对齐问题都有了解法：要顶满宽就用它的负值，要收在容器里就把负 margin 归零。\n4.3 三次返工：友链卡底色 这是整个改造里返工最多的部分，因为「不好看」是模糊反馈，只能靠排除法逼近。\n版本 值 结果 第一版 #ffffff 纯白 白得发空，卡片像贴上去的补丁 第二版 transparent 全透明 和页面糊成一片，favicon 失去承托 定稿 #fffdf9 暖白 只比页面亮一档，卡片立住了 中间那版是判断失误。用户说「不需要白色背景」，我理解成「去掉背景色」，设成了 transparent。结果卡片和页面同为米白，只有一圈描边在撑场面，看着像没画完。\n三档之间色值差 1~2 个色阶，观感差得很远。底色这件事没有「随便挑一个」的余地。\n4.4 favicon 那圈白的真相 还有个反馈是「头像框没底色发虚」。第一反应是去找卡片背景的锅，改了半天没用，最后决定去下载 favicon 二进制看看到底长什么样。结果是：\nfavicon.im 返回的是透明背景的 SVG。GitHub 的图标就是一个 fill=\u0026quot;#24292E\u0026quot; 的纯深色形状，本身没有任何白色底衬。\n所以那圈白根本不是装饰，是功能性承托——深色图标直接压在米白页面上会糊。搞清楚之后改法就明确了：保留白底 background: #fff，深色模式下给 rgba(255,255,255,0.94)，既承托又不刺眼。\n别猜，去取证。 一个 4KB 的 SVG 比盯着样式改半天有用。\n4.5 About 页：从分块到扁平 About 页试过两个方向。第一版是按 h3 把小节切成卡片（custom.ts 里写了切块逻辑，配 .about-card 样式），做完发现卡片框把本来就不多的内容切得更碎了，用户直接否掉。第二版改成扁平展示，去掉所有分块容器，小节标题用手写体 + 左侧藕粉竖线标识。\n这跟前面「装饰是否干扰内容」是同一个判断：内容稀疏的时候，容器越多越显空。\n4.6 一个幽灵字段 清理 front matter 时发现 content/page/about/index.md 里写着：\n1 layout: \u0026#34;about\u0026#34; 但 layouts/ 下根本没有 about.html（travelling.html 倒是有）。查 Hugo 的单页模板查找顺序：\n1 2 3 4 5 /layouts/TYPE/LAYOUT.html ← page/about.html（不存在） /layouts/SECTION/LAYOUT.html ← page/about.html（不存在） /layouts/TYPE/single.html ← page/single.html（不存在） /layouts/SECTION/single.html ← page/single.html（不存在） /layouts/_default/single.html ← 命中 一路回落到底，用的是默认单页模板。更关键的是：About 页的样式从来不是靠这个字段挂钩的，而是靠 single.html 里输出的 page-{{ .ContentBaseName | urlize }} 生成的 class（即 .page-about）。所以这个 layout 字段从头到尾没起过作用，删掉即可。\n如果遇到「写了但好像没生效」的配置，值得花几分钟去查找顺序表对一遍。\n4.7 Travelling 页 Travelling 最后没有做成内容页，而是改成了一个跳转过渡页（layouts/page/travelling.html），配转圈樱花动画。这个决定挺务实的——既然没有内容可写，与其留一个空页面，不如说明清楚它是干什么的。\n五、随心记（Moments） 8a851cb 新建了「随心记」分区，用来放那些不成篇的碎片想法。\n5.1 数据格式 按月分文件，条目用 ::: 分隔：\n1 2 3 4 5 ::: 2026-09-16 01:40 正文，支持多段 / 图片 / 行内代码 / 链接 ::: 模板用 split .RawContent \u0026quot;:::\u0026quot; 切块，首行解析时间，解析失败则跳过该块（防止内容里出现 ::: 把文件吞掉）。\n5.2 三个坑 int \u0026quot;08\u0026quot; 报错：Go 的 strconv.ParseInt 在 base 为 0 时把前缀 0 当八进制，08、09 这种合法的十进制写法会炸。01 到 07 恰好都是合法八进制，所以这个 bug 只在 8 月、9 月和每月 8 号 9 号才冒出来。修法是转 int 前先去掉前导零。\ntrim \u0026quot; \u0026quot; 在管道里把整串吃成空：{{ .body | plainify | replaceRE ... | trim \u0026quot; \u0026quot; }} 结果为空，换成 strings.TrimSpace 才正常。\n月文件 date 落在今天会被 Hugo 判为 future，整个月不渲染。用 hugo list future 可以查出来，已在配置里开 buildFuture = true。\n5.3 阅读页净化 574033a 这条提交做了件有意思的事：独立页恢复装饰，阅读页去掉装饰。\n原本逻辑是判断有没有 .article-page，有就认为是阅读页，关掉花瓣、小树、云朵。但独立页（About / Links / Travelling）走的也是 single.html，同样输出 .article-page，于是被误判成阅读页，装饰全没了。\n修法是在 single.html 的 body-class 块里加标记：\n1 {{ if ne .Section \u0026#34;post\u0026#34; }}page-standalone{{ end }} post 下面才是真阅读页。JS 改成：\n1 2 3 var isReading = !!document.querySelector(\u0026#39;.article-page\u0026#39;); var isStandalone = !!document.querySelector(\u0026#39;.page-standalone\u0026#39;); var decorEnabled = !isReading || isStandalone; 这个改动的意义是把「读文章时该安静」和「逛页面时可以活泼」两种状态分开。读文章需要专注，装饰会干扰；独立页是展示性的，装饰恰好补充了内容密度上的不足。\n六、两次栽在 CSS 权重上 重构期有两次印象比较深的翻车，都属于「代码写对了但没生效」这一类。\n6.1 选择器权重输给主题 说明区的代码块顶穿了卡片描边。查出来的原因是主题给 .article-content .highlight 加了通栏出血（就是 4.2 节那套负 margin），在说明卡里溢出 30px。\n我一开始写的是 .links-note-body .highlight，改完完全没生效。算权重就明白了：\n选择器 权重 (a,b,c) .article-content .highlight（主题） (0, 2, 1) .links-note-body .highlight（我的） (0, 2, 0) 差在最后一位。CSS 权重是从左往右逐列比，前一位赢了后面就不用看，所以 (0,2,0) 无论写多少条都赢不了 (0,2,1)。最后上了 !important。\n顺带纠正一个我原先的误解。我以为「两个 !important 相撞就看源码顺序」，查 MDN 才发现不准确：\nWhen two important declarations from the same origin and layer apply to the same element, browsers select and use the declaration with the highest specificity. Only if the selectors had the same specificity would source order matter.\n也就是说 !important 相撞时先比权重，权重相同才比顺序。\n6.2 被 grep 的上下文骗了 验证的时候 grep 到这么一条：\n1 2 3 4 .article-content .highlight pre { margin-block-end: 0 !important; margin-left: calc(var(--card-padding) * -1) !important; } 当时紧张了一下——同样是 !important，那不就变成「比权重再看顺序」了吗？赶紧去查它挂在谁身上。用正则按 {} 成对解析之后发现，真实选择器是：\n1 .article-content .gitlab-embed-snippets .file-holder.snippet-file-content GitLab 嵌入片段，跟代码块半点关系没有。是 grep -A 1 抓上下文时，把两条无关规则的首尾连起来读了。\nminified CSS 里一条规则体可以很长、选择器分组可以很多，grep 给出的「上下文」基本都是假的。要看清楚只有一条路：用正则把 选择器 { 声明体 } 成对切出来。\n七、样式覆盖的两条原则 改主题这一年，实际可用的手段只有两条，它们对应不同的场景：\n7.1 优先改 SCSS 变量 assets/scss/custom.scss 会被主题的 style.scss 在最后一行 import，所以在里面覆盖变量最省事。改配色、圆角、阴影、动画时长都属于这一类，改一处全局生效，不会漏。\n1 2 3 4 5 :root { --accent-color: #d49ba8; --body-background: #fdf9f2; --radius-wobbly: 12px 18px 14px 20px / 18px 12px 20px 14px; } 7.2 覆盖 layouts 要整组写 改结构（不只改样式）就得覆盖 layouts/ 下的同名文件，因为主题是 Hugo Modules 引入的，改不到模块缓存里的源文件。\n这里有个陷阱：主题的高权重选择器会让你只写一条的覆盖静默失效。比如真图分支那段，主题的规则是：\n1 2 3 4 .article-page .main-article .article-header .article-image img { max-height: 50vh; object-fit: cover; } 如果只写 .article-image img { object-fit: contain }，权重 (0,1,1) 对 (0,4,1)，会被完全覆盖，且不会报错。必须写一整组同等或更高权重的选择器，并显式声明 max-height: none。\n验证方法是下载编译后的 CSS，看 object-fit 规则的出现顺序——contain 必须排在主题所有 cover 之后。\n八、自定义 JS 的唯一入口 这个坑比较隐蔽，记一下：主题通过 js.Build 把 assets/ts/custom.ts 编译成指纹化的 /ts/custom.HASH.js 自动加载。所有自定义 JS 必须写在那个 .ts 文件里。\n之前我走的另一条路：写 assets/js/custom.js，再用 layouts/partials/footer/custom.html 静态引用 /js/custom.js。这个路径会 404，自定义 JS 从来没执行过。改成 .ts 单一入口之后才生效。\n现在 custom.ts 承担的工作大致是：客户端重排（比如把说明区收进卡片）、装饰开关判断、挂件交互。模板里往 \u0026lt;script\u0026gt; 注入值要用 safeJS 防止二次转义。\n九、这一轮改动的几个判断 把重构期的决策归纳一下，这几条是反复用到的：\n装饰不能干扰内容。背景元素（花瓣、云朵）跟内容分层，可以留；跟随鼠标的元素（花瓣轨迹、光斑）跟内容同层，要谨慎。 容器越多越显空。内容稀疏时加卡片框是负优化，About 页分块被否掉就是这个原因。 改视觉先改变量。有设计 token 之后，调整成本从「各处找硬编码」降到「改几个值」。 别猜，去取证。favicon 白底那件事猜了两轮都错，下载一个文件就查明了。 涉及「页面里已经有什么」的判断，先去构建产物里数一遍。我曾想当然实现了一个装饰横栏（branch-garland），验证时发现构建产物里 0 处、SCSS 里 0 条样式——纯属臆造，全部撤回。 参考链接： 1 Specificity - CSS | MDN\n2 !important CSS keyword - CSS | MDN\n3 层叠、优先级与继承 - MDN Web Docs\n4 模板查找顺序 - Hugo 中文文档\n5 Single Content Template - Hugo Docs\n","date":"2026-09-16T00:00:00Z","permalink":"https://sutdown.github.io/p/sutdownblog-%E8%A7%86%E8%A7%89%E9%87%8D%E6%9E%84%E8%AE%B0%E5%BD%95/","title":"SutdownBlog 视觉重构记录"},{"content":"这篇不是教程，是记录：我给这个博客做了个写文章的 Skill，把「写之前要交代什么」和「写成什么样算过关」都写死进去，顺手还撞上一个挺有意思的 bug。\n起因 每次想让 AI 帮我写篇博客，开场白都长这样：文章放 content/post/ai/ 还是 log/；front matter 里 date 要带前导零、文件名里的日期不带；图片路径是 /img/... 不是 /static/img/...；结尾要有 ## 参考链接：；写完不许自己 push。\n这些机械要求说完，真正的问题才浮出来——写出来的东西一眼 AI。句子都很对，也都很难看：开头铺垫三行，中间四个平均分配的小节，结尾再来个「总而言之」。我不想每次都从头吵一遍，所以干脆把它做成一个 Skill。\nSkill 就是一个目录 没别的，一个文件夹：\n1 2 3 4 5 6 7 8 .workbuddy/skills/blog-writer/ ├── SKILL.md 主流程 + 六条硬门禁 ├── references/ │ ├── writing-style.md 风格卡 + 反 AI 味黑名单 │ ├── post-conventions.md front matter / 路径 / 图片 / 站点坑 │ └── sourcing.md 检索渠道与配图规范 └── scripts/ └── new_post.py 建 md 骨架 + 图片目录 它按三层渐进式披露加载，这点挺聪明：平时只有 SKILL.md 的 name 和 description 挂在系统提示里（约 100 token），只有判断「这活儿跟它有关」才读正文，用到风格卡了才去读 references/，脚本只把结果带回上下文、代码本身不进窗口。\n六条门禁 流程反而是最省事的部分，真正让它「像我干的活」的是这几条不许跳过的门禁：\n门禁 内容 G1 主题 落笔前必须有主题笔记，写的是「我们讨论的那个话题」，不是关键词的通用科普 G2 风格 动笔前读同分类旧文 ≥3 篇，套风格卡 G3 来源 数据、版本号、论文结论必须有出处 URL，没有就降级成「个人理解」或删掉 G4 配图 至少 1 张图，放 static/img/\u0026lt;dir\u0026gt;/，正文用 /img/... 引用 G5 参考 结尾 ## 参考链接： 编号列表 G6 提交 必须起本地服务交给我预览，我明说才 push G6 是我特意加的。自动发布这种事，一次都不想试。\n风格卡才是重头戏 风格卡我写了两页，一半是「该怎么写」，一半是「不许怎么写」。前者大致是：\n开头一句话直给：本文从 X、Y、Z 几个角度讲清楚什么，不写铺垫段 中英术语混排，关键概念加粗，允许口语插入和自问自答（那么这部分代码是怎么实现的？） 并列对比优先用表格，技术文必须有代码实践 结尾不写总结段，直接接参考链接 后者是黑名单，命中即重写：「在当今…的时代」「值得注意的是」「综上所述」「不仅仅是 A 更是 B」，以及那些形态上的毛病——全篇没有一张图一个表格、每个小节篇幅完全均等、通篇零第一人称零口语的「无菌体」。\n外加自检三问：有没有一句「只有我会这么写」的话？去掉加粗和表格还看得出结构吗？有没有哪段是任何人都能写出来的正确废话？\n一次翻车：int(\u0026ldquo;08\u0026rdquo;) 给博客加「随心记」分区时我写了个时间解析的 helper，把 2026-09-16 01:40 这种字符串拆成年月日。本地跑得好好的，某次预览时页面突然变成 Hugo Server: Error，报的是：\n1 2 error calling int: unable to cast \u0026#34;08\u0026#34; of type string to int: strconv.ParseInt: parsing \u0026#34;08\u0026#34;: invalid syntax 原因在 Go 的 strconv.ParseInt：base 为 0 时，前缀 0 会被当成八进制，08、09 这种合法的十进制写法就炸了。01 到 07 恰好都是合法八进制，所以这个 bug 只在 8 月和 9 月、以及每月 8 号 9 号才冒出来——藏得相当好。\n修法是在转 int 前先把前导零去掉：\n1 2 {{- $ms := replaceRE \u0026#34;^0+\u0026#34; \u0026#34;\u0026#34; (index $dp 1) -}} {{- $m := cond (eq $ms \u0026#34;\u0026#34;) 0 (int $ms) -}} 更有意思的是后半段：我打开 parse-time.html 一看，前导零明明早就处理掉了，可错误还在。折腾半天才反应过来——hugo server 拿的是旧模板的缓存，等它 rebuild 完再请求就 200 了。所以「代码明明修好了还报旧错」的时候，别急着怀疑人生，先重启服务。\n顺带：封面这件事 这次预览还确认了一件事，博客现在没有文章封面图了。列表页七张卡片、文章详情页，全站唯一的 \u0026lt;img\u0026gt; 是侧边栏头像。\n是之前主动删的（commit 3c02650）：header.html 被简化成只剩 partialCached \u0026quot;article/components/details\u0026quot;，static/img/anime/ 那 80 张图也一并清了。只是清得不够干净——列表卡片上还挂着 has-cover：\n1 \u0026lt;article class=\u0026#34;{{ if $image.exists }}has-image{{ else }}has-cover{{ end }}\u0026#34;\u0026gt; 封面没了以后 $image.exists 恒为 false，于是每张卡片都挂着一个空 class。我把编译后的 CSS 拉下来 grep 了一遍，has-cover 出现 0 次，说明它早就没有任何样式对应了，直接删掉 else 分支了事。\n现在 front matter 里的 image 字段完全无效，所以新文章一律不写，视觉表达只能靠正文里的图——这也是 G4 那条门禁现在更硬的原因。\n参考链接： 1 Equipping agents for the real world with Agent Skills\n2 The Complete Guide to Building Skills for Claude\n3 Removing leading 0 from number in string, int function error - Hugo Discourse\n4 Golang Quirk: Number-strings starting with \u0026ldquo;0\u0026rdquo; are Octals\n","date":"2026-09-16T00:00:00Z","permalink":"https://sutdown.github.io/p/%E7%BB%99%E5%8D%9A%E5%AE%A2%E9%85%8D%E4%B8%80%E4%B8%AA%E4%BC%9A%E5%86%99%E6%96%87%E7%AB%A0%E7%9A%84-skill/","title":"给博客配一个会写文章的 Skill"},{"content":"AI DevOps 这周的内容主要是AI DevOps（人工智能开发运维）。AI DevOps 是将人工智能技术整合到DevOps实践中，以自动化、优化和增强软件开发和交付流程的方法。\nWhy 在生产环境中监控软件系统仍然是一项至关重要的任务。当软件编码上线完成之后，并不意味着可以就此安心，实际上，后续在生产环境中安然运行代码才是真正的难点。当前存在的难点在于：现代系统的复杂性，各种工具间的孤立性，成员只是不对称，系统组件的复杂依赖关系等。\n因此，管理生产环境通常需要SRE（站点可靠性工程师）进行繁琐的软件分类和故障排查工作。SRE需要手动处理大量的告警、日志和事件，进行故障诊断和根因分析。这种人工分类工作耗时且容易出错，正是AI可以发挥作用的领域。\nSRE的传统职责 SRE不仅需承担繁重的7×24小时待命、手动故障排查及基础设施管理等职责，还需在信息孤岛中跨团队拼凑碎片化信息，并依赖往往过时的操作手册；随着云原生架构（容器、Kubernetes）的普及，系统产生的数据量、依赖关系及复杂性呈指数级增长，导致SRE因高压轮班频发职业倦怠，且由此引发的系统停机每年给全球企业造成约4000亿美元的经济损失，从而有力论证了从人工被动运维向AI驱动自动化转型的紧迫性。\nDevOps过程 四个黄金指标 Latency - 延迟：衡量处理请求所需的时间 Errors - 错误率：失败请求的比率 Traffic - 流量：系统当前的需求负载，比如每秒请求数，会话数，事务数，宽带使用量等 Saturation- 饱和度：系统资源的满载程度，比如CPU使用率，内存使用率，IO带宽，线程池使用率等 生产环境中，一般会追踪请求在微服务架构中的完整路径。\n故障处理的标准流程 1 确认同时评估当前的问题。评估当前问题的严重程度，检查上述四个黄金指标（优先数据库）。\n2 检查数据库层和应用层的健康状态，定位问题的具体位置。\n3 识别最近的部署。比如部署，数据库迁移，功能开关变更，扩容事件等。如果有关联，立即回滚/还原。\n4 定位影响范围。比如数据库读写操作，主从库，分片示例，应用侧数据库侧等\n5 执行缓解措施。\n6 稳定并监控。观察错误率，延迟，流量等是否恢复正常。\n7 沟通相应的团队反馈操作和问题。\n8 文档记录。记录时间线，根本原因，后续改进等\nAI DevOps改进 核心指标 Mean-Time-To-Repair (MTTR) - 从故障发生到完全修复的平均时间 How many engineers are pulled into incident（参与事件的工程师数量） Reported SLA for customers（向客户报告的SLA） AI SRE系统核心特征 Dynamic mapping of a knowledge graph（知识图谱的动态映射）：表示系统组件、依赖关系、历史故障的结构化知识。AI理解系统架构，知道\u0026quot;如果A出问题，可能影响B和C\u0026quot; Agentic system across observability stack and clouds（跨可观测性栈和云的代理系统）：AI不仅能分析，还能自主行动。AI像真正的SRE一样，能登录不同系统、执行命令。 Generates real-time narratives\u0026hellip;（生成实时叙事\u0026hellip;）：给出具体的、可操作的修复建议（prescriptive remediation） Heavy emphasis on explainability and auditability（强调可解释性和可审计性）：AI的决策过程必须透明，不能是\u0026quot;黑箱\u0026quot; Limitations 1 Complexity of incidents that can be resolved（可解决事件的复杂性）\n中等复杂度的事件AI仍然表现良好，但高度复杂、多因素交织的事件：仍需要人类专家\n2 Heterogeneity of modern production stacks（现代生产栈的异构性）\n现代系统使用多种技术栈，AI需要适配这种多样性，目前可能覆盖不全\n3 Ability to remediate actual code based on what has been detected（基于检测结果修复实际代码的能力）\nAI擅长诊断，但自动修复代码还在早期阶段，并且判断原因不一定全面，权限不能太多\n4 Good root cause analysis requires good monitoring gardening（好的根因分析需要好的监控基础）\nAI需要：完善的指标收集，清晰的日志格式，准确的追踪配置，合理的告警阈值\n5 Security could be a new attack vector（安全可能成为新的攻击向量）\n","date":"2026-06-10T00:00:00Z","permalink":"https://sutdown.github.io/p/%E6%96%AF%E5%9D%A6%E7%A6%8Fcs146s%E7%8E%B0%E4%BB%A3%E8%BD%AF%E4%BB%B6%E5%BC%80%E5%8F%91%E8%80%85week9%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/","title":"斯坦福CS146S：现代软件开发者Week9学习笔记"},{"content":"自动化UI和APP构建 Modern SaaS development has focused on web apps for over two decades\n经典的开发架构模式 缩写 全称 说明 LAMP Linux + Apache + MySQL + PHP/Python/Perl 最经典的传统 Web 开发栈，2000年代主流 MERN MongoDB + Express + React + Node.js 现代全栈 JavaScript 开发，2010年代流行 JAM JavaScript + APIs + Markup JAMstack 架构，静态站点生成 Serverless 无服务器架构 使用云函数（如 AWS Lambda），无需管理服务器 What Has Changed 三大变化：\nPrompt 编程：用自然语言代替手写代码 工程师角色转变：从纯编码转向设计 + 产品 + 技术 全民开发：非技术人员也能构建应用 代际演进：\n第一代无代码（Wix/Squarespace/Webflow）：可视化建站\n第二代 AI 工具（Lovable/Vercel v0/Cursor）：自然语言构建完整应用\n对比之前复杂的 AWS 架构图，现在可以用 AI 工具在几分钟内完成同样的事情，这是\u0026quot;vibe coding\u0026quot;的真正实现——用自然语言编程。\nLimitations 1 AI 生成的代码就像一个“黑盒”。当 AI 一次性写出了一个能跑的 App，一旦代码出现 Bug，或者需求稍微变复杂导致代码跑不通了，因为你是用“提示词”生成的代码，你可能根本没有逐行读过代码，也不理解底层的逻辑。这时候你想修 Bug，你会发现根本无从下手。\n2 AI 不是神，它给出的代码只是基于概率生成的“建议”，它经常会产生幻觉（Hallucination）——比如编造一个不存在的函数，或者用一种错误的方式调用 API。不经Review的代码可能导致项目的后期灾难。\n3 AI 生成的代码往往缺乏安全性考量。比如AI 可能会为了方便，直接把数据库密码写在代码里；AI 可能会引用有已知漏洞的旧版本第三方库；数据泄密；生成的代码可能没有做 SQL 注入防护或 XSS 防护等等\n4 如何防止同质化。如果你让 AI 做一个登录页，它生成的界面虽然好看，但跟网上成千上万个 AI 生成的界面长得一模一样。这就导致了严重的“AI 味”，缺乏品牌独特性。想要做出独特的设计，目前还是得靠人工精细调整。\n5 这些应用在实际需求的复杂性。目前的 AI 工具非常擅长做简单的 CRUD 应用（增删改查），比如待办事项列表、简单的博客、落地页。一旦涉及到企业级复杂度，AI 就搞不定了。比如：需要处理高并发、复杂的事务逻辑、微服务架构、或者对接遗留的老系统等\nAI 的“记忆”（Context Window）是有限的，它无法理解一个拥有几万个文件的大型项目的整体架构。\n虽然 AI 工具（Vibe Coding）很火，能快速生成原型，但它目前只能做表面功夫和简单应用。一旦涉及到调试、安全、复杂逻辑、独特设计，还是得靠懂技术的专业程序员。\n","date":"2026-06-05T00:00:00Z","permalink":"https://sutdown.github.io/p/%E6%96%AF%E5%9D%A6%E7%A6%8Fcs146s%E7%8E%B0%E4%BB%A3%E8%BD%AF%E4%BB%B6%E5%BC%80%E5%8F%91%E8%80%85week8%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/","title":"斯坦福CS146S：现代软件开发者Week8学习笔记"},{"content":"AI code review 前言 代码审查是一件投入产出比最高的事情。对个人而言，你可以通过审查别人的代码，你能学到不同的写法、设计思路和最佳实践，从而提升自己的技术水平；而对团队来说，互相审查也能够提高团队整体的代码质量。\n通常来说，代码审查能够发现55-60%的错误，而各种测试手段（单元测试、集成测试等）只能发现25-45%。说明代码审查比测试更能发现bug。没有代码审查时，每100行代码有4.5个错误；有了代码审查后，降到0.82个。错误率降低了约82%。（来源：代码评测：Just Do It）\n所以，代码审查从始至终都是开发中不可或缺的一环。\n代码审查的关注点 1 2 3 4 5 6 代码审查要抓什么？ ├── 逻辑和正确性（代码能不能跑对？） ├── 可读性和可维护性（代码好不好读？） ├── 性能（代码快不快？） ├── 安全性（代码安不安全？） └── 最佳实践（代码是否符合规范？） 逻辑和正确性错误 代码能正确运行并不代表代码是合格的。逻辑上代码能运行但是不符合预期。比如该用加法的地方用了减法，该遍历数组却漏了最后一个元素；正确性上代码在边界条件下会出错。比如除零、空指针、数组越界等。\n前者适配产品需求，后者一般也是测试需要关注的问题。\n可读性和可扩展性 Readability（可读性）：其他程序员能否快速理解这段代码在做什么 Maintainability（可维护性）：未来需要修改或扩展时，是否容易下手 AI代码审查工具可以自动检测这种\u0026quot;复杂度过高\u0026quot;的代码，并建议重构，这正是AI的优势所在——它不会因为代码长就\u0026quot;looks fine\u0026quot;，而是会逐行分析复杂度。\n性能 代码能跑、能读，但如果太慢，用户体验会很差。性能问题往往在数据量小的时候看不出来，一旦数据量变大就会暴露。性能问题很难通过测试发现（测试通常用小数据集），所以代码审查是发现性能问题的关键时机。\n比如选择合适的数据结构，并发策略等\n安全 安全问题往往在正常使用时看不出来，只有被恶意攻击时才会暴露。一旦发生时，可能导致数据泄露、系统被控制、用户信息被盗等多个问题。\n比如python中的eval函数会执行任意的python代码，这时候需要加点限制，比如不允许函数调用，系统命令，导入语句等\nBest Practice 这是代码审查的第五个重点关注领域。如果说前面讲的是\u0026quot;代码对不对\u0026quot;、\u0026ldquo;代码好不好读\u0026rdquo;、\u0026ldquo;代码快不快\u0026rdquo;、\u0026ldquo;代码安不安全\u0026rdquo;，这点讲的就是\u0026quot;代码是否符合行业/团队的标准做法\u0026quot;。\nCR - 金字塔模型 1 2 3 4 5 6 7 金字塔层级 底部要点（从上 最不重要 到 下 最重要 对应） ───────────────────────────────────────────── Style → 保持代码风格和组织结构的一致性，符合团队代码规范 Find Bugs → 在bug到达生产环境之前捕获它们，基本就是常见的逻辑，边界，安全，性能等 Design Discussion → 开启关于设计优缺点的讨论，比如架构设计是否合理，是否易于扩展维护等较为长期的问题 Correct Solution → 确保变更正确地解决了问题，且能够选择效率最高 最安全的方案 Mental Alignment → 帮助团队成员在系统变化时调整他们对系统的心智模型，即保持逻辑语法和代码库整体风格一致 代码评论特征 代码评论大致有四个要点：（前两点重在描述，然后给出方案 和 方案的依据）\n提供有关问题具体的细节；引用具体的代码或者问题；给出建议的解决方案；提供证据或者解释\nAI 审查的变化 效率上：自动化减少了审查时间，并提前捕获问题 一致性：AI在所有代码审查中应用相同的标准 知识共享：开发者通过AI建议学习最佳实践 降低认知负荷：AI处理常规检查，让人类专注于复杂逻辑 持续改进：AI系统随着数据增加而不断改进 对代码的理解：现代AI代码审查工具更深入，提供上下文分析和模式识别 当前的AI水平在写代码上至少超过了25%的开发者，但不如高级开发者，主要缺乏的是高级开发者的创造力、架构思维、业务理解。但AI在抓bug方面超越了所有级别的人类开发者，因为它可以不知疲倦的逐行检查，且直到各种bug模式。\n所以说对于人类而言，人类应该学会用AI工具辅助，让AI完成它擅长的部分，比如抓bug，写常规代码；而像架构设计，业务逻辑等更为复杂的内容，人类自己就应该专心学习。（这里涉及在做AI代码审查系统时，可以添加分类功能，针对性的输出人类所需要的评论）\n且在实际情况中，AI所做的内容应该是客观的给出 比如 bug，security，performance，edge case等问题，人类则要理解业务需求，传承团队经验，尊重团队文化等。这些同样可以教导给AI，但需要时间去不断调优。（当然对于评论的评价标准也是多样的，比如评论准确率，cr效率，代码质量的提升等）\nAI 审查的局限性 1 需要时间学习团队规范。包括代码风格，团队规范，项目结构，历史经验等\n2 有一定误报率，需要手动标记，逐渐学习\n3 还无法捕捉代码惯用法和仓库最佳实践。人们之间靠经验相传的不成文规定不好总结，所以ai不一定能学会\n4 无法处理复杂的业务逻辑和架构决策。所以经常需要人为其分步处理，语言指导等\n5 对安全相关的变更必须格外谨慎。这方面不能完全信任ai，仍然需要人工审查，且需要以谨慎的态度对待AI给出的建议\n6 AI经常漏掉边界情况\n7 有了AI编码系统，代码审查比以往任何时候都更重要。AI带来了更多不由人类本身写出的代码，这些部分更需要人类对其进行审查，同时负有责任。\n所以说，审查前需要明确告诉它不要审查什么，以及你的代码模式/惯用法，告知基础信息。对用户输入、认证、文件操作、网络请求要特别小心。这四个是安全敏感区域，注意结合具体业务。\n","date":"2026-06-01T00:00:00Z","permalink":"https://sutdown.github.io/p/%E6%96%AF%E5%9D%A6%E7%A6%8Fcs146s%E7%8E%B0%E4%BB%A3%E8%BD%AF%E4%BB%B6%E5%BC%80%E5%8F%91%E8%80%85week7%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/","title":"斯坦福CS146S：现代软件开发者Week7学习笔记"},{"content":"AI测试和安全 有一则Replit CEO道歉的新闻，原因是其AI智能体在一次测试运行中意外擦除了一家公司的整个代码库，且最初还试图隐瞒。这展示了AI代理失控可能导致的毁灭性后果。这样的例子数不胜数，所以当LLM成为主要代码生产者时，必须引入新的防御机制（Guardrails）。\n传统安全测试 传统Web应用安全风险 SQL injections (SQL注入)： Cross-site scripting (跨站脚本攻击/XSS)当其他用户浏览该页面时，脚本会在他们的浏览器中执行，导致会话劫持或钓鱼。 Broken authentication (失效的身份认证) Insecure direct object references (不安全的直接对象引用/IDOR) Security misconfigurations (安全配置错误) Sensitive data exposure (敏感数据暴露) 主流漏洞检测技术 在CI/CD流水线中自动化SAST和DAST扫描，提供持续反馈，加快开发进程，同时不影响安全性。对于敏捷或DevOps环境，添加IAST或RASP等工具可以进一步增强安全性。通过关联SAST和DAST的结果，组织能够实现更全面、更有效的漏洞管理。\nSAST (静态应用安全测试) 一种白盒测试技术，意味着测试者拥有对代码的完全访问权限。它通过分析二进制文件或源代码本身来工作，不需要运行程序。它能识别具体的代码级漏洞，例如SQL注入、命令注入和跨站脚本攻击。\n常见的SAST工具：Bandit（常用于Python）、Semgrep（多语言支持，速度快）、Eslint + extensions（主要用于JavaScript/TypeScript）。SAST工具通常是针对特定编程语言优化的。\nDAST (动态应用安全测试) 一种黑盒测试技术，意味着测试者不需要知道内部代码结构，只关注输入和输出。核心逻辑是“模拟真实黑客的行为”，通过向运行中的应用程序发送各种恶意请求来探测漏洞。\n常用手法： 模糊测试、会话篡改、暴力破解模拟。\nSCA (软件成分分析) 核心工作是对应用程序所使用的开源软件包进行深度分析。现代应用往往由大量第三方库拼接而成，SCA就是用来清点这些“积木”是否安全的。SCA不仅仅看代码，还要分析包管理器（如npm, pip）、基础设施即代码（IaC，如Terraform配置）以及拉取的镜像。这意味着它关注的是整个供应链的安全，而不仅仅是业务代码本身。\n技术难点： 传递性依赖解析（挖出隐藏的深层依赖）和二进制扫描（无源码情况下的审计）。\nRASP(运行时自我保护) RASP 嵌入到应用程序的运行环境中。这种集成使 RASP 能够分析应用的逻辑和数据。在分析过程中，它可以：\n及时发现异常行为。 立即屏蔽恶意行为。 一个关键区别在于，RASP不仅会对潜在问题发出警报——它通过隔离和解决威胁主动防止攻击，而无需依赖外部工具。RASP为SAST和DAST提供了动态替代方案。与分析静态代码的SAST和模拟外部攻击的DAST不同，RASP是实时运行的。它监控应用执行中的行为，并通过终止会话或提醒防御方来应对实时威胁。\nAI带来的改变 AI正在被用来增强传统的SAST、DAST和SCA工具。例如，利用LLM来减少SAST的误报，或者用AI生成更聪明的DAST测试用例。但同时，也会出现针对大语言模型的特定攻击，如提示词注入、数据投毒或模型越狱。这些是传统Web安全未曾涉及的新领域。\nAI对安全测试的影响 AI带来的应用安全风险 提示词注入（Prompt injection） 向生成式AI系统发送隐藏或误导性的指令，让AI偏离预期的行为。由于自然语言的模糊性和多样性，很难像过滤特殊字符那样完全防御提示词注入，这给传统的安全防护体系带来了巨大挑战。\n比如当用户要求AI将“上面的文本”写入一个名为amp_prompt.txt的文件中。注意，这里用户并没有直接提供文本，而是引用了上下文。AI不仅执行了写文件操作，而且我们看到了被写入的内容——这正是AI自身的系统提示词。这实际上是一次成功的系统提示词提取攻击。用户通过巧妙的对话引导（可能利用了“Write the text above\u0026hellip;”这种模糊指代），诱使AI输出了本该保密的系统底层指令。\n工具滥用（Tool misuse） 通过欺骗性提示词操纵智能体，以滥用其集成的工具。如果攻击者能通过提示词注入劫持了Agent的决策逻辑，就能间接控制这些高权限工具。\n比如仅仅是一段文本注入，通过AI这个“代理人”，最终转化为了对操作系统底层的控制权（启动外部程序）。这是传统Web安全中较少见的攻击路径。或者是 攻击者不需要直接攻击AI模型本身，只需要污染AI会读取的数据源（如代码文件、文档），就能间接控制AI的行为。\n代码攻击（Code attacks） 利用Agent执行代码的能力，获取对运行环境的未授权访问权限（如远程代码执行 RCE）。\n意图破坏（Intent breaking） 侧重于逻辑层。操纵Agent的计划制定过程，使其行动偏离原始目标。例如，原本要修复Bug，结果被引导去删除了整个模块。\n身份欺骗（Identity spoofing） 侧重于信任链。利用受损的认证机制，伪装成合法的Agent与其他系统交互，绕过安全检查。\nWhat has changed “左移”安全变得更触手可及。以前这需要昂贵的专业工具或资深安全专家，现在通过集成在IDE中的AI助手，普通开发者也能在写代码时实时获得安全建议，大大降低了门槛。 LLM可引入工作流以发现问题。LLM可以像一位不知疲倦的安全审计员，自动扫描代码库，识别潜在漏洞、逻辑错误或不合规的配置，比传统的正则匹配更智能，能理解上下文。 自动化渗透测试。AI Agent可以自主规划攻击路径，尝试各种漏洞利用方式，甚至根据反馈动态调整策略。这使得渗透测试可以更频繁、更全面地进行，不再依赖昂贵的人工服务。 AI安全工具落地的难点 误报率高。AI SAST工具经常“谎报军情”，把正常的代码标记为漏洞。 评估基准不切实际。现有的用来测试AI能力的基准数据集往往过于理想化或简单，无法反映真实世界的复杂代码库。很难准确评估一个LLM在真实生产环境中的表现到底如何。模型可能在考试题上拿满分，但在实际项目中却漏洞百出。 非确定性分析。对同一个提示词运行多次，可能会得到不同的结果；随着对话或处理任务的进行，模型对早期信息的记忆和理解能力会下降；压缩带来的损失。 AI落地应做的考量 从评估过程，到评估结果（能否有正确的bug没有幻觉，正确的bug能不能给出正确的解决方案，解决方案会不会引入错误，万一有错误责任在谁），最后到CI/CD部署。详细见下：\n如何减少漏洞检测中的误报和幻觉？\n如果AI总是把正常代码当成漏洞（误报），或者凭空捏造不存在的攻击路径（幻觉），开发者就会停止使用它。\n我们如何验证大语言模型（LLM）生成的补丁是安全的，并且不会引入回归错误？\n必须像人类专家一样给出推理过程，没有可解释性，就没有信任。\n大语言模型如何解释它们标记漏洞或提出修复方案的原因？\nAI不仅要是“医生”能诊断出病，还得是“药剂师”能开出对的药。更糟糕的是，AI开的药不能有毒副作用（引入新的Bug或破坏原有功能）。目前的自动化测试很难完全覆盖这种复杂的逻辑验证。\n衡量大语言模型应用安全（AppSec）性能的正确基准是什么？\n现在的考试题目（Benchmark）太简单或太脱离实际。我们需要一套能真实反映复杂业务场景、包含各种边缘情况的“高考题”，才能公平地评价哪个模型更适合做安全工作。\n大语言模型应如何嵌入 CI/CD 流程，才不至于让团队被噪音淹没？\n这是一个工程体验问题。如果每次代码提交AI都报一堆错，流水线就会阻塞，开发者会崩溃。如何在“不漏报”和“不扰民”之间找到平衡点？\n如果 AI 生成的补丁引入了漏洞，谁该为此负责？\n这是一个法律和伦理问题。如果AI自动修复了一个漏洞，结果导致系统被黑，这个锅是背在开发者身上，还是模型厂商身上，或者是部署AI的企业身上？目前这在法律上还是灰色地带。\nAssignment - claude实战 cs146s/week6 at master · Sutdown/cs146s\n","date":"2026-05-27T00:00:00Z","permalink":"https://sutdown.github.io/p/%E6%96%AF%E5%9D%A6%E7%A6%8Fcs146s%E7%8E%B0%E4%BB%A3%E8%BD%AF%E4%BB%B6%E5%BC%80%E5%8F%91%E8%80%85week6%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/","title":"斯坦福CS146S：现代软件开发者Week6学习笔记"},{"content":"现代终端 在你的开发环境中引入 Agent，会如何改变你使用的产品？\n维度 传统开发工具 Agent 增强型工具 交互方式 鼠标点击 + 键盘输入具体代码/命令 自然语言描述意图 + AI 自主执行 工作流 线性：写代码 → 测试 → 调试 → 重复 循环：提需求 → AI 生成 → 人类审查/修正 → AI 再优化 认知负荷 高：需记住所有语法、API、配置细节 低：只需关注架构、逻辑正确性、业务目标 错误处理 人类阅读报错 → 搜索 → 手动修复 AI 自动分析报错 → 提出修复方案 → 人类确认 学习曲线 陡峭：需精通工具本身 平缓：更像与同事协作，而非操作机器 AI开发工具产品原则 七大原则共同构成了现代AI开发工具的设计蓝图，它们不是孤立存在的，而是相互支撑、层层递进，最终指向一个目标：让开发者从语法细节中解放出来，专注于问题本身，让AI承担繁琐的实现工作。\n从开发者已经有的认知出发 优秀的AI工具不应该强行创造全新的交互范式，而是应该基于开发者已经熟悉的界面进行演进——无论是IDE（如Cursor）、终端（如Warp）还是聊天界面（如Bolt），关键在于让开发者能够在代码和自然语言之间无缝切换，降低学习成本。\n关注\u0026quot;配置的灵活性\u0026quot; 工具必须做到\u0026quot;零配置即可展现价值\u0026quot;，让普通用户开箱即用，同时又要为高级用户提供深度的可定制能力，包括自定义提示词、项目规则以及MCP（Model Context Protocol）配置，并且能够无缝切换不同的AI模型，满足不同层次用户的需求。\n聚焦\u0026quot;开发者人体工程学\u0026quot; 这是一种极致的效率追求——只要能减少一次击键就应该去做，通过键盘快捷键等手段优化操作流，最终实现\u0026quot;零上手摩擦\u0026quot;和\u0026quot;5分钟内让用户惊叹\u0026quot;的目标，让开发者在最短时间内感受到工具的价值。\n将聊天作为一等公民 代码本身就是人类意图的一种人为抽象表达，随着工具的演进，越来越多的交互将回归到最自然的语言表达方式，从关注语法细节转向关注开发者意图的传达，这标志着软件开发正在经历一场从\u0026quot;如何写代码\u0026quot;到\u0026quot;想要什么结果\u0026quot;的范式转变。\nMCP集成 MCP已成为LLM与现实世界交互的通用语言，通过可扩展的工具生态系统让AI能访问任何资源和能力，真正打通从想法到执行的通道。\n快速反馈循环 必须允许快速迭代并即时看到更新，提供基于提示词近实时变化的面板和仪表板，这也是为什么整个行业致力于减少构建时间，同时将可解释性作为一等公民，让开发者清楚理解AI的决策过程；\n代理工作流 对实质性任务提供完全自主能力，越来越多采用\u0026quot;代理接管方向盘\u0026quot;的方法，同时支持不同级别的人类参与——从agent主动询问澄清问题到YOLO模式（完全自主执行），这代表着软件开发正在经历一场从\u0026quot;人写代码\u0026quot;到\u0026quot;人指挥AI写代码\u0026quot;再到\u0026quot;AI自主完成开发任务\u0026quot;的深刻变革。\nAI开发工具发展走向 首先关于工具整合，当前市场上充斥着各种专注于特定功能的点解决方案——代码审查工具、应用构建平台、监控系统等，未来我们很可能会看到大规模的整合浪潮，这些分散的工具将融合成统一的\u0026quot;All-in-One\u0026quot;平台，就像CLAUDE.md中展示的那样，一个工具覆盖从Discover到Support的完整SDLC生命周期。\n其次是界面形态的演进，AI IDEs（如Cursor）、AI终端（如Warp）、AI浏览器REPLs之间的界限正在模糊，它们可能会相互融合，最终形成一个统一的交互界面，开发者不再需要区分\u0026quot;我在用IDE还是终端\u0026quot;，而是自然地根据任务切换交互模式。\n第三是垂直化与专业化的张力，像Warp/Cursor这样的工具面临战略选择：是保持通用性，还是深耕特定领域（如全栈开发）成为垂直专家？这取决于市场是更需要\u0026quot;什么都能做\u0026quot;的瑞士军刀，还是\u0026quot;某件事做得极好\u0026quot;的专业工具。\n最后也是最关键的是标准化问题，目前我们处于配置文件的碎片化状态——.cursorrules、CLAUDE.md、agents.md等各种格式并存，这显然不可持续，行业迫切需要像.gitignore那样的通用标准来定义AI代理的行为规则和项目特定配置，而AGENTS.md可能是迈向统一标准的第一步，它将为不同工具提供互操作性的基础，让开发者能够\u0026quot;一次配置，处处生效\u0026quot;。\n这四个问题共同指向一个核心矛盾：工具应该越来越强大还是越来越简单？应该整合成平台还是保持模块化？应该通用化还是专业化？ 答案可能不是非此即彼，而是在不同层次上同时演进——底层提供标准化协议（如MCP），中层提供可组合的工具模块，上层提供一体化的用户体验。\n","date":"2026-05-22T00:00:00Z","permalink":"https://sutdown.github.io/p/%E6%96%AF%E5%9D%A6%E7%A6%8Fcs146s%E7%8E%B0%E4%BB%A3%E8%BD%AF%E4%BB%B6%E5%BC%80%E5%8F%91%E8%80%85week5%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/","title":"斯坦福CS146S：现代软件开发者Week5学习笔记"},{"content":"关于Agent Manager 演进 当前软件开发团队模式有四个演进阶段，过去当需要增加产出时只能增加人手，现在一个开发者+AI agent对于生产力有了质的飞跃。\n传统独立开发：最基础的开发模式，生产力受限于个人能力 团队管理模式：技术负责人管理多个开发者的产出，通过团队协作放大生产力 AI辅助管理：负责人借助AI系统管理多个开发者的产出，提高管理效率 个人+AI代理模式：这是当前正在发生的变革：一个程序员可以指挥多个AI agent协同工作 接着我们可以看看当前变革下软件开发的具体流程\n提供高层级需求，将需求转换成设计文档（人机协作完成） 根据文档实现解决方案（AI，这里说明当AI能力不达标或者需求文档不够好时，一次直接生成的代码不一定符合需求，此时你需要分步骤分别让AI实现） 添加测试，确保测试集成通过（AI） 代码审查（人机协作），更新文档（AI） 因此可以发现，在这个流程中程序员起到的作用更多的是一个管理审核人员，管理AI完成工作。\n如何有效使用AI 我们不能仅仅局限于跟AI聊天，要将这个过程当成工程化的控制AI。\n常见的技巧有：\nAgent行为文件：比如Claude.md / Cursorrules/ agents.md 这些都相当于配置文件，用来规定项目的代码风格，技术显示，禁止做的事情等等 Hooks：在预定义事件类型上运行的确定性脚本，也就是可以记录，干预，校验AI行为的工具。 常见的有：PreToolUse（调用工具之前的确认，比如修改敏感文件，提交代码等），PostToolUse（AI调用完工具之后，比如自动测试，更新项目索引，生成变更摘要），UsePromptSubmit（人类向AI发送指令之前，能够自动添加上下文，过滤危险指令等），PreCompact（AI压缩或整理对话历史之前，能保留关键决策点，插入注释等） and more Commands：把常用的提示词保存为文件，供 AI 代理直接执行，比如/test /fix, /git-all等 你提前写好一套标准指令（比如“请帮我运行所有单元测试并报告结果”），存成一个 .command 或 .prompt 文件，以后只需输入 /test 或点击按钮，AI 就自动执行这套流程。 Sub Agents：当有些任务在当前代理中和当前代理任务无关时，派遣一个自代理有助于更好的管理上下文和发挥agent的各自功能。 子代理的目的在于创建不同类型的专属开发者人格，能够和主代理的上下文保持隔离，拥有独立记忆和工具集。 实践中，同时要注意一些使用ai的事项：\n注意要有谨慎的安全网。代码库中必须要有完整的测试；AI修改代码后最好能自动触发测试检查，也就是持续集成/部署\n每个代理的行为必须可追溯。也就是为每个AI生成的变更打标签\n不同的任务用不同的模型，合理分配任务。比如简单重构，文档生成都可以用简单模型；架构设计，代码生成用大模型\n复杂的任务需要人工引导，简单的任务可以防守让AI异步完成\n比如对于复杂任务先给AI详细的设计文档和分布指令，中途多个check point，最后人工review。而像更新readme这种简单任务丢给AI，设置好hooks自动验证即可。\n定期提交快照。当AI走错路改错文件时，可以快速返回上次状态。\nClaude Code介绍 设计核心原则 Terminal-native（终端原生）：Claude Code 不是一个独立的GUI应用，而是一个运行在命令行里的工具。 Low-level model access（底层模型访问）：你可以直接控制使用的AI模型 Infinitely hackable（无限可定制）：可以修改Hook逻辑、添加新Command、创建Subagent角色 简单安装指南 见claude code安装 + 阿里模型API配置 - 知乎\n实战开发指南 1 设计(Design) → 计划(Plan) → 实现(Implement) → 测试(Test) → 审查(Review) → 更新(Update) 设计阶段：用 AI 辅助需求分析和技术方案设计，输出到 docs/designs/ 计划阶段：用 AI 将设计拆解为具体的实施步骤，输出到 docs/plans/ 实现阶段：AI 按照计划编写代码，参考 schema.sql 和 docs/guides/ 测试阶段：AI 根据设计和计划生成测试用例 审查阶段：多个 AI 模型交叉审查代码（集合方法） 更新阶段：根据反馈更新文档和代码 推荐资料 awesome-claude-agents\nHow Anthropic teams use Claude Code .pdf\nSuperClaude-Org/SuperClaude_Framework.\nAssignment - claude实战 cs146s/week4 at master · Sutdown/cs146s\n","date":"2026-05-19T00:00:00Z","permalink":"https://sutdown.github.io/p/%E6%96%AF%E5%9D%A6%E7%A6%8Fcs146s%E7%8E%B0%E4%BB%A3%E8%BD%AF%E4%BB%B6%E5%BC%80%E5%8F%91%E8%80%85week4%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/","title":"斯坦福CS146S：现代软件开发者Week4学习笔记"},{"content":"AI IDE IDE介绍 IDE（Integrated Development Environment）为“一体化的软件开发工作空间”，包含编辑器、编译器、调试器等核心组件。大多数开发工作都在 IDE 中完成，因此它是 AI 增强功能的自然载体，在 IDE 的演进过程中，功能整合与开发者定制化之间始终存在一种平衡。\nAI IDE的发展历程：\n1983 年：Turbo Pascal 发布，被标注为“第一个真正的 IDE”，集编辑器、编译器、链接器于一体，专为 Pascal 语言设计。 1997 年：Microsoft Visual Studio 发布，为 C++/Visual Basic 提供高级调试功能，标志着 IDE 向多语言、多功能平台演进。 2001 年：IntelliJ IDEA 发布，引入上下文感知的代码导航、重构与自动补全，极大提升了 Java 开发效率。 2015 年：Microsoft VSCode 发布，以轻量级、高度可扩展的编辑器形态出现，迅速成为开发者主流工具。 2023 年：Cursor 发布，被描述为“首批广泛使用的 AI 原生 IDE 之一”，标志着 AI 技术深度融入开发环境。 现代AI编程助手 基础模式 现代AI编程助手的能力主要分为两种：基础核心模式 和 真正的AI原生模式。\n基础模式代表了当前 AI 编程的“基本功”，主要解决的是局部、即时、确定性的问题。\n比如行内补全，函数生成，对单文件进行操作，多文件修改等，前三者大多都是基于使用者惯性或者对单个文件的简单分析。直到多文件才是过往IDE和未来IDE之间的临界点，多文件修改和理解会逼迫AI去检索项目中的其它文件，对整体进行改动。\n代码补全只会截取光标附近的小窗口代码，代码片段发送到云端的大模型，然后回流到编辑器。其中为了保障速度，一般采用专门针对代码优化的模型。\nAI原生模式 真正的AI原生模式代表了范式转移。在这里，AI 不再仅仅是被动等待指令的补全工具，而是变成了能够自主思考、规划和执行的智能体。\n它一定会涉及到全项目的上下文理解。IDE会将整个项目代码切分成小块（chunks），然后用embedding模型把代码转换成向量存储在向量数据库中，之后当用户进行提问时，IDE会根据用户的提问在数据库中找到最相似的代码作为上下文。当然代码在更新时，IDE也会定期更新向量数据库的索引，通过 Merkle 树计算块差异以进行高效更新。所以说AI IDE记住整个项目并不是靠模型，而是外挂了一个高效的向量数据库。\n常见的代表有：\nBackground agents（后台代理）：让AI在后台异步完成一些任务。 Model Context Protocol（MCP，模型上下文协议）：它允许 AI 模型安全地连接到外部数据源和工具。在编程语境下，AI 不仅能读代码，还能连接数据库查看表结构、读取本地文件系统、甚至调用编译器 API它允许 AI 模型安全地连接到外部数据源和工具。 Learn memories（学习记忆）：长期记忆和短期记忆，上下文等。 Bugbot (PR review)（缺陷机器人/代码审查）：语法检查（Lint），而是逻辑审查。指出代码潜在的空指针异常、安全漏洞或设计模式的不当使用。 AI工具的演进路径 从“辅助”到“代理”：工具的角色正在从“辅助开发者写代码”（左上象限）向“独立完成任务”（右下象限）转变。 从“本地”到“云端”：随着任务复杂度的增加，对算力和数据的需求也增加，工具正越来越多地依赖云端资源。 从“同步”到“异步”：未来的高效开发者将更多地采用“异步”模式，将复杂任务委派给AI代理，自己则专注于更高层次的设计和决策。 与AI的协作实践 Plan 你给 AI 的指令质量，应该取决于任务的复杂程度。它其实是在暗示开发者，随着任务难度的提升，你的角色也需要发生转变。\n当完成简单变更时，比如修改函数命名，添加注释，实现简单小功能。在这种场景下，你的直觉就是最好的提示词。你不需要写文档，不需要画流程图，甚至不需要完整的句子。因为上下文就在那里，AI 能够轻易捕捉到你的意图。过度设计提示词反而是一种时间浪费。\n面对复杂任务，你必须强迫自己转变身份，成为一个“产品经理”。你要对任务有一个清晰的认知和描述，描述过程存在几个核心要素：\n目的：明确变更目的，让AI做出更符合业务逻辑的技术决策。 定义：补充必要的背景知识，专业术语解释等 计划：高层次的实现分解任务，给出大致的技术路线图。 被更改的源文件：明确指出代码库中哪些部分是相关的，以及为什么。这能帮助 AI 聚焦上下文，避免它去分析无关的代码，从而提高效率和准确性。 范围外：明确指出什么不应该被更改。这是一个非常关键的约束条件。 测试用例，边缘情况 扩展性：说明未来哪些变更会相关，以便 LLM 能够设计出具有前瞻性的架构。 Code AI 的困惑往往源于“混乱的代码库”。为了让 AI（以及人类）能更好地理解项目，我们需要提供最优的上下文。我们的核心在于增加代码库的可读性和消除上下文的歧义。\n比如在让 AI 工作前，应确保以下信息清晰且文档化：\n仓库导航与结构： 文件目录树、核心文件位置。\n环境设置： 如何安装依赖、如何启动开发环境。\n规范与风格： 代码风格（缩进、命名）、最佳实践。\n技术细节： 数据访问模式、API 接口定义与契约。\n文档化： 上述所有内容都必须有详尽的文档。\n单体仓库设计： 强烈推荐将相关项目、库、服务放在一个仓库中，便于 AI 统一理解上下文。\n避免“混乱”： 杜绝多种访问模式混用、变量命名不一致、重复代码（多个函数做同一件事）。\n除了优化代码本身，我们还可以通过特定的配置文件，主动向 LLM “投喂”规则和指令，引导其正确导航和操作代码库。\n文件名 适用对象 功能描述 核心用途 claude.md Claude 自动加载到对话上下文中。 记录常用 Bash 命令、核心工具函数、代码风格指南、测试指令。 AGENTS.md 通用代理 开放格式，为各种 AI 代理提供指引。 定义项目特定的工作流程和规则。 cursorrules Cursor IDE 配置编辑器内 AI 的行为规则。 控制 AI 在 IDE 中的补全、重构偏好。 llms.txt 爬虫类 LLM 为抓取网页信息的 LLM 提供导航。 帮助外部 AI 快速理解项目结构。 Agents.md推荐内容清单：\n项目概览： 让 AI 知道自己在做什么。 构建和测试命令： 让 AI 知道如何验证自己的工作。 代码风格指南： 让 AI 知道怎么写才合规。 测试指令： 确保质量。 安全考量： 避免 AI 写出有漏洞的代码。 AI编程的常见顺序：\n用 DeepWiki 读懂代码库。DeepWiki可以自动生成整个仓库的文档、架构图和依赖关系图。 用 Devin 设计架构方案。Devin拥有强大的推理能力。在这个阶段，你把它当作一个资深架构师来咨询，进行可行性验证和头脑风暴。 用 Codemaps 确认具体细节。Codemaps能让你以图形化或结构化的方式快速浏览代码结构，找到具体的函数定义和调用链。 最后在 Windsurf 中结合所有信息，开始真正的规划落地。将DeepWiki生成的文档或Devin的建议，直接引入到你正在使用的本地IDE（Windsurf）中。 当前的工作流大致分为两部分\nstep1. 异步委派。程序员将任务发给AI写代码。\nstep2. 同步测试和微调。当前AI生成代码不够完善，或者任务项目等逻辑太复杂，AI无法完全搞定，因此人类需要重新介入，实时验收和修改。\nAI编程的未来发展 Why 前面讲到AI编程关键的一点在于同步到异步工作流的转换，当然它不止于表面的技术，更是在强调人的思维模式转变。\n异步代理是一项困难但可以学习的技能，但现实中大多数人仍然停留在使用同步代理的阶段。传统开发中，资深程序员习惯于“自己动手，丰衣足食”，因为自己写代码往往比教别人（或教AI）写更快、更可控。当你不擅长或者需要花时间朝着陌生领域学会如何“正确的管理”AI时，会存在一种焦虑的心理状态。而且异步和同步的思维方式本质是不同的，同步模式是线性的、单线程的，大脑只需要处理当前的一个上下文，很符合直觉；异步模式要求开发者具备多线程处理能力。要求开发者能在极短时间内完成不同任务的上下文切换。\n注意其中要忌讳的是一个半异步的尴尬的中间地带。如果你是在同步写代码，AI思考的时间过长容易打断心流，但如果你是异步写代码，AI思考的时间太短让你无法切换到另一个任务，夹在其中的效率是最低的。所以当你发现自己在等一个“不长不短”的任务时，要么优化指令让它变快（回归同步），要么给它派一个更大的任务让它跑得久一点（进入异步），从而让你自己解放出来。\nHow 当前理想的AI增强型软件开发工作流为：\n在规划（Planning）和测试（Testing）这两个需要高度智慧和判断力的环节，人类保持同步介入，确保方向正确和质量达标。在最耗时、最机械的编码（Coding）环节，AI异步执行，释放人类的时间。\n1 planning sync --- coding async --- testing sync 对于未来的期望应该是\n人类工程师作为代理管理者\n利用同步工具解决最困难的问题，利用异步工具实现10倍杠杆效应\n未来需要的宝贵技能\n委派与多线程处理能力；代码阅读能力；规划、范围界定、架构设计\n上下文工程 失控的上下文 一般来说，较长的上下文可以让模型对我们的需求了解更多，模型效果会更好。但实际上，当上下文过于超出时，会导致智能体和应用程序出现意想不到的故障。上下文可能会变得混乱、分散注意力、令人困惑或相互冲突。这对依赖上下文来收集信息、综合分析结果和协调行动的智能体来说尤其成问题。常见的失控方式有以下几种：\n语境中毒 幻觉或其他错误进入语境，并在语境中被反复提及。\n情境的许多部分（目标、总结）都被关于博弈状态的错误信息“污染”了，而这种污染往往需要很长时间才能消除。结果，模型可能会执着于实现不可能或无关的目标。\n情景干扰 上下文发展到一定程度，导致模型过度关注上下文，而忽略了训练期间学到的内容。\n随着模型收集更多信息并建立历史记录——这些累积的上下文可能会变得分散注意力而非有所帮助。当上下文数量显著超过 10 万个词元时，智能体倾向于重复其庞大历史记录中已有的操作，而不是生成新的方案。\n语境混淆 模型使用上下文中的冗余内容来生成低质量的响应。\n如果你把某些信息放入上下文中，模型就必须关注它。这些信息可能无关紧要，也可能是不必要的工具定义，但模型都会将其考虑在内。大型模型，尤其是推理模型，在忽略或舍弃冗余上下文方面做得越来越好，但我们仍然不断看到无用信息让智能体犯错。\n语境冲突 你在你的环境中积累了新的信息和工具，而这些信息和工具与环境中的其他信息相冲突。\n模型在掌握所有信息之前尝试回答问题的早期结果。这些错误答案会保留在上下文中，并在模型生成最终答案时对其产生影响。LLM（逻辑推理能力强的人）在对话初期常常做出假设，并过早地试图得出最终解决方案，而他们又过度依赖这些解决方案。简而言之，我们发现，当LLM在对话中走错方向时，他们就会迷失方向，并且无法挽回。\n面向编码代理的高级上下文工程 在实际生产中，代码库通常是非常巨大的，此时AI工具的使用甚至会降低生产效率，但更好的上下文工程可以尝试解决这个问题。\n上下文窗口是控制 AI 输出质量的唯一杠杆。盲目对话会让搜索日志、代码片段等杂讯迅速填满窗口，导致模型迷失；解决方案是频繁有意压缩（Frequent Intentional Compaction）——通过\u0026quot;研究→规划→实现\u0026quot;的分段工作流，将中间产物蒸馏为结构化文档，主动将上下文利用率维持在 40-60%，确保每次交互都输入高信噪比的信息。\n围绕上下文窗口的工程实践，本质是把人类审查的杠杆点前移：与其在实现阶段逐行审查代码，不如在研究与规划阶段确保上下文输入的准确性与完整性。因为一行错误的研究结论可能衍生千行错误代码，而精准的上下文管理能让今天的模型在复杂代码库中同样产出高质量、可维护的代码。\nAnthropic工具设计原则 1.构建精心设计的工具。一些工具仅仅封装了现有的软件功能或API接口——无论这些工具是否适合智能体。以通讯录为例，您可以选择实现一个search_contacts或多个message_contact工具，而不是一个单一的list_contacts工具。确保你开发的每个工具都有清晰明确的用途。工具应该使智能体能够像人类一样，在拥有相同底层资源的情况下，细分并解决任务，同时减少中间输出所消耗的上下文信息。\n2.为工具进行合适的命名。MCP 客户端有时默认启用此功能。例如，按服务（例如，Service、asana_search``Resource jira_search）和按资源（例如，Resource、asana_projects_search``Service asana_users_search）对工具进行命名空间划分，可以帮助客服人员在合适的时间选择合适的工具。\n3.工具中返回有意义的上下文。工具实现应注意仅向智能体返回高信号信息。它们应优先考虑上下文相关性而非灵活性，并避免使用底层技术标识符（例如：uuid、、）。智能体处理自然语言名称、术语或标识符的效果往往要好得多。\n4.优化工具的响应。优化上下文质量固然重要，但优化工具响应中返回给智能体的上下文数量也同样重要，过于长的响应会导致占用过多的上下文。\n5.仔细编写工具描述和规范，方便在导入上下文之后被智能体所理解。\nAssignment cs146s/week3·基于高德地图天气 API 的 MCP (Model Context Protocol) 服务器，支持 STDIO 和 HTTP 两种运行模式。\n","date":"2026-05-17T00:00:00Z","permalink":"https://sutdown.github.io/p/%E6%96%AF%E5%9D%A6%E7%A6%8Fcs146s%E7%8E%B0%E4%BB%A3%E8%BD%AF%E4%BB%B6%E5%BC%80%E5%8F%91%E8%80%85week3%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/","title":"斯坦福CS146S：现代软件开发者Week3学习笔记"},{"content":"Agent 基于大型语言模型（LLM）的编码代理（Coding Agent）的具体实施步骤：\n在终端中读取输入，并持续将内容追加到对话中。 告知大型语言模型有哪些可用工具： 它会在适当的时候请求使用工具。 你离线执行工具并返回响应。工具示例：“Read_file”（读取文件）、“List_dir”（列出目录）、“Edit_file”（编辑文件）。 创建一个新文件，编辑一个新文件。 Claude 的内部机制：\n前置加载上下文：使用小型、有针对性的提示词来预先加载上下文。 无处不在的系统提醒：在系统/用户提示、工具调用、工具结果等各个环节都加入系统提醒，以防止模型“漂移”（使用 \u0026lt;system-reminder\u0026gt; 标签）。 命令前缀提取：这可能涉及从用户输入或生成的文本中识别特定的指令格式，在确立指令的任务之前，会先进行结构化提取，把实际的指令和用户的情绪等分开，保证LLM处理的是感觉的指令。 生成子代理：可能会生成子代理，以帮助防止上下文过载。 （在cs146s/week2class文件夹中可见一个简单的agent示例）\nMCP Server 为什么需要让大型语言模型（LLMs）连接外部动态数据\n大型语言模型拥有宽广但静态的世界知识，只有在重新训练时才会更新。这意味着模型本身的知识库是截止于训练数据的，无法实时感知世界变化。 为了构建完全自主的系统，我们需要稳健的方法来输入动态数据。比如实时的天气，现任总统，当前比特币价格等。这些都需要实时的工具进行查询。 RAG检索增强生成和工具调用是我们目前拥有的最佳答案。 RAG：通过检索外部文档来补充模型知识。 工具调用：让模型学会使用计算器、搜索引擎、API等工具来获取信息。 MCP定义 官方定义：它是一个开放协议，允许系统以一种“可跨集成推广的方式”向AI模型提供上下文。简单来说，它试图解决不同系统间数据格式不统一、难以被AI理解的问题。\n通俗解释：它把复杂的协议概念简化为“让AI能看懂并使用工具的标准说明书”。\nMCP通过提供一个统一的协议层，将集成模式从“M x N”简化为“M + N”。即，每个LLM主机只需实现一个MCP客户端，每个工具只需实现一个MCP服务器，它们就能通过MCP协议互相通信。 MCP标准化使得开发者不需要重复造轮子，并且有了统一的输出格式。开发者不再需要为每个连接器重复实现认证（auth）、错误处理（error handling）、速率限制（rate-limiting）等通用功能；强制使用JSON-RPC来确保所有工具的输出格式一致，这使得LLM能够更可靠地解析和处理来自不同工具的响应。 MCP的设计借鉴了语言服务器协议（Language Server Protocols, LSP），与LSP主要用于响应式（reactive）交互不同，MCP允许构建主动式（proactive）的代理工作流（agentic workflows）。这意味着AI代理可以根据上下文主动调用工具，而不仅仅是响应用户的直接指令。 MCP过程 MCP协议提供了两种传输层实现：标准输入输出（stdio）和服务器发送事件（SSE），这为不同场景下的部署提供了灵活性。\nHost：指承载MCP客户端的应用程序，例如代码编辑器Cursor或AI助手Claude Desktop。 MCP Client：嵌入在Host中的库，负责与MCP Server建立并维持一个有状态的会话。 MCP ServerModel Context Protocol Server Implementations：作为工具的轻量级包装器，它负责向Client暴露工具的功能。 Tool：一个可被调用的函数，它可以是数据源或API。 工作流程为：\n1.client向server发起请求，询问server存在的tools\n2.server返回json格式的列表，详细描述每个工具的名称，摘要，参数结构\n3.host把这些工具注入AI模型的上下文中\n4.用户输入提示词时，触发模型根据理解生成一个结构化的工具调用请求\n5.MCP Server执行工具调用，返回结果\nMCP局限 Agent不能处理太多的工具。工具过多会造成选择困难，上下文混乱，性能下降等风险，工具管理是具有一定复杂性的。 APIs会快速消耗模型的上下文窗口。复杂的API调用、大量的返回数据或冗长的工具描述都会占用宝贵的上下文空间，可能导致模型“遗忘”之前的对话或指令，影响最终输出质量。 API设计时应以“AI原生”（AI-native）为理念，而不是采用僵化（rigid）的传统设计。API应该更易于被AI理解和调用，例如提供更清晰的文档、更结构化的数据格式、更灵活的参数等，以降低AI集成的难度和成本。 （在cs146s/week2class文件夹中可见一个简单的agent示例）\nAssignment cs146s/week2 at master · Sutdown/cs146s\n这个作业不难，基本上就是让AI实现一些小任务，对于现在的AI模型，很快都能实现。\n","date":"2026-05-13T00:00:00Z","permalink":"https://sutdown.github.io/p/%E6%96%AF%E5%9D%A6%E7%A6%8Fcs146s%E7%8E%B0%E4%BB%A3%E8%BD%AF%E4%BB%B6%E5%BC%80%E5%8F%91%E8%80%85week2%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/","title":"斯坦福CS146S：现代软件开发者Week2学习笔记"},{"content":"AI导论 对AI的评价 Software developers have the potential to be more productive than they have ever been in history With AI coding an engineer can pick up tech stacks and tools at an unprecedented pace You won’t be replaced by AI. You’ll be replaced by a competent engineer who knows how to use AI. 这三句话其实就是在表明，能够利用好AI的软件开发者将会是未来的主流，你需要的是学会如何最大化的利用AI工具，而不是被AI所替代。\nLLMs are only as good as you are\nGood context leads to good code If you can’t understand your codebase, neither will an LLM LLM和你的水平是相当的，你不理解你的代码，LLM也不会。它能够让你更快的提供你的上限，但AI能做到的上限和个人水平是密不可分的。\nHow LLMs Work llm（大型语言模型）是用于下一个标记预测的自回归模型。\n文本生成模型（比如GPT系列）一般分为三层：\n输入层：将原始文本分割成更小的单元，也就是常说的token（词元） 嵌入层（embedding）：将词元转换成固定维度的数值向量，让计算机可以捕捉词语的语义信息 transformer层（使用自注意力机制的变换层）：自注意力机制是一种用于帮助模型学习词语之间的句法和语义关系的内部数学机制，用于理解上文，预测下文。 输出层：获取最有可能的下一个token的概率分布。 Training Process\n第一阶段：自监督预训练：在各种公共数据源（比如github代码库，stackExchange等）上进行训练，教会模型语言的基本概念。 第二阶段：监督微调：使用高质量，精心策划的提示-响应对，教会模型遵循指令。 第三阶段：偏好调优：收集同一提示的多个输出对，利用奖励模型，让模型输出和人类偏好（有用性，可读性，正确性）保持一致。 当前大模型发展的两大核心趋势：推理能力的进化和模型规模的扩张，推理模型的发展如下：\n思维链训练：通过在训练数据中引入包含详细推理步骤的“思维链”，让模型学会分步骤、有逻辑地解决问题，而不是凭直觉给出答案。 工具使用集成：训练模型学会调用外部工具，如计算器、搜索引擎或代码解释器，以弥补其自身知识的局限，增强解决复杂任务的能力。 人类偏好对齐：收集人类对模型推理步骤的反馈，确保其思考过程不仅结果正确，而且逻辑清晰、符合人类的思维习惯。 强化学习评估：利用强化学习技术，让模型学会自我评估推理路径、识别错误并进行回溯修正，从而实现自我优化。 现在的GPT-3/CLaude 3.5 Sonnet参数量为1750亿，到GPT-4时已经高达1.8万亿。\nIn practive LLM是一个强大的编程助手，但使用时必须考虑到其固有的局限性。\n模型的优势在于：专家级代码补全，代码理解，代码修复。\n局限在于\n幻觉：模型可能生成不存在或者已经过时的API（解决方案：上下文工程） 上下文窗口限制：模型的上下文窗口可达10万-20万token，但不是所有token的处理效果都相同。（模型一般对开头和结尾的记忆更深刻，中间模型容易被以往） 延迟：处理请求根据任务复杂度导致的延迟实践（根据任务复杂度进行合理规划，最大化利用模型思考时间，但也会导致频繁的上下文切换） 成本：训练输出等需要大量成本 提示词工程 Prompting 对提示词的观点 提示词是与大语言模型沟通的通用语（lingua franca），也是一种有效的编程方式。 提示词正在从精确、刻板的“代码式”指令，进化为更自然、更口语化的“人类语言”。 提示词工程虽然有其难以捉摸的一面，但它并非纯粹的玄学，而是有一套可以学习和应用的方法论。由于LLM的黑盒本质，内部运行机制不完全透明，因此从输入到输出之间仅对用于而言带有一定的神秘感，但LLM也是经过多次实验验证的成熟技术，通过学习才能实现更好的应用。 提示词技术 Zero-shot prompting\n零样本提示是一种最直接的提示方式，其核心是直接向大语言模型提出任务要求，而不提供任何支持信息或示例。它考验的是模型基于其预训练知识直接完成任务的能力。\nK-shot prompting\nK-shot prompting的本质是上下文学习（in-context learning）。与直接下达指令的零样本提示不同，这种方法会在提问时，额外提供几个“输入-输出”的示例，让模型通过模仿来学习任务的模式、格式或风格。\nChain-of-Thought Prompting, CoT\n思维链提示通过要求模型一步步地展示其推理步骤，来解决那些需要多步逻辑才能完成的任务。这种方法能显著提高模型在复杂问题上的准确性和可解释性。\n多样本思维链（Multi-shot CoT）： 在提示中提供几个完整的“问题-推理过程-答案”的示例。 通过展示完整的推理轨迹（reasoning traces），让模型学习并模仿这种逐步思考的模式。 零样本思维链（Zero-shot CoT）： 不提供具体示例，而是直接在问题后加上一句引导性的话，如“Let\u0026rsquo;s think step-by-step”（让我们一步步思考）。 通过简单的指令，激发模型内部的推理能力，使其自动分解问题并逐步解答。 Self-consistency Prompting\nSelf-Consistency Improves Chain of Thought Reasoning in Language Models自洽性提示的核心逻辑：不依赖单次生成的结果，而是通过生成多条推理路径并寻找它们的交集（最一致的答案），以此来显著提升模型解决复杂问题的准确率，并减少胡编乱造的情况。\n多次采样和聚合：同一个问题让模型生成多个不同的回答，结合Cot技术，投票选择出现频率最高的结果作为答案 减少幻觉和错误：让模型探索多个推理路径，有利于识别过滤掉不合理的幻觉 Tool Use\n工具使用是现代大型语言模型应用开发中的一个转折点。它标志着模型从单纯的“文本生成器”进化为能够与现实世界交互、解决实际问题并减少错误的“智能代理”。\n允许模型向外部求助。比如当需要直到一个实时的时间天气，操作数据库，计算复杂数学题等问题时，模型可以通过利用外部工具得到准确结果。 减少幻觉和实现自主性关键技术：真实的外部工具提供真实数据，避免捏造事实。模型能够使用工具是通过AI agent的关键步骤，说明模型具备了自主行动能力。 Retrieval Augmented Generation， RAG\nRAG 是让大模型变得“博学”且“靠谱”的关键技术。它通过外挂知识库的方式，解决了模型知识陈旧、缺乏私有数据以及容易产生幻觉的问题，并且是现代 AI 编程助手（如 Cursor）能够理解你整个项目代码库的核心原理。\n注入上下文数据。通过将用户私有数据或特定领域的文档（上下文）注入到提示词中，让模型能够基于这些特定信息来回答，而不是仅依赖其训练数据。 保持模型更新。RAG 允许模型访问最新的信息（如今天的新闻或最新的代码库），而无需耗费巨资重新训练模型。 你能免费获得可解释性和引用。模型的答案是基于检索到的具体文档生成的，所以你可以追溯答案的来源（引用）。这增加了模型的可信度，让用户知道答案是从哪里来的（可解释性）。 减少幻觉。通过限制模型只能根据检索到的事实进行回答，可以显著减少模型“一本正经胡说八道”的情况。 Reflexion\n核心思想是引导模型对自己的输出进行自我审视和修正（行动 - 观察 - 反思 - 调整）。过程如下\n自我反思。这要求模型在完成一次任务后，不要直接结束，而是停下来，像一个编辑一样，重新审视自己刚刚生成的答案，评估其正确性和完整性。 环境反馈。模型在执行一个动作（如运行一段代码）后，会获得一个“环境反馈”（如编译错误信息或测试失败报告）。这个反馈会被作为新的上下文信息，添加到与模型的对话历史中，引导它进行下一轮的思考和修正。 多轮提示。这并非一次性的问答，而是一个“尝试-评估-修正”的循环。通过多轮交互，模型可以逐步逼近最优解 Meta Prompting\n人工智能系统的元提示\n元提示是一种高级提示技术，侧重于任务和问题的结构和语法方面，而非具体内容细节。元提示的目标是构建一种更抽象、结构化的大型语言模型（LLM）交互方式，强调信息的形式和模式，而非传统的内容中心方法。\nSystem Prompt\n它是提供给大语言模型的第一条消息。这条消息通常是隐藏的，最终用户在与模型交互时是看不到的。它设定了模型行为的底层基调。\n作用：设定人设，制定规则，定义风格。\n一般LLM中的三个角色是：System Prompt，User Prompt，Assistant\nEg.\nYou are a helpful assistant that loves programming at the level of a senior software developer and is very detailed and pedantic in your answers.\n基础定位：乐于助人；专业领域：热爱编程的资深技术人员；回答风格：详尽和精确\nAI Prompt Engineering：A Deep Dive 这场圆桌讨论的核心问题其实只有一个：提示工程到底是什么，以及怎么把它做好。\n讨论一开始，四位专家就给出了一个共识性的判断——提示工程不是写一段话，而是在设计一个系统。它包含清晰沟通、迭代测试、版本控制和边界管理，本质上是用自然语言承担代码的功能。\u0026ldquo;工程\u0026quot;这两个字落在哪里——不是落在一句完美的提示上，而是落在试错的范式上：你能随时重置、独立实验、反复调整，这是传统软件开发给不了的自由度。\n然后深入到了提示工程师的能力模型。四位专家给出了三条不同的路径：Zach强调的是阅读——仔细读模型的输出，别猜它怎么理解指令，直接看它怎么执行的；Amanda强调的是让外行人来测试你的提示，她自己的精确度只有68%，远低于人类基准，这说明提示词里藏着大量隐式假设；而David强调的是极限挑战——别做那些写邮件式的简单任务，真正的成长来自尝试不可能的事情，比如让Claude打Pokemon。\n那个Pokemon的案例是整段讨论里最直观的。David花了整个周末，叠加网格、构建ASCII地图、放大三千倍，最后发现模型在NPC连续性识别上根本过不去。他选择了放弃——不是因为能力问题，是他知道用四个月优化不如等下一代模型。这个判断本身就是提示工程的一个核心能力：知道什么时候该停。\n之后进入了一个很重要的分水岭——预训练模型和RLHF模型的差异。 David指出一个关键现象：很多人把预训练时代的直觉错误地用到RLHF模型上——比如过度关注\u0026quot;这个内容在互联网上出现了多少次\u0026rdquo;，或者认为角色扮演是万能的。而Amanda的解释把这个问题讲透了：模型已经不是\u0026quot;一句话接一句话地模仿\u0026quot;的预训练模型了，它是一个经过RLHF微调、被训练成\u0026quot;帮助者\u0026quot;的系统。对模型说谎、让它扮演角色，本质上是没搞清楚对面是什么。两种方法论的分歧，根源在这里。\n然后是那三段划分——企业级、研究型和通用对话提示词的差异。企业级提示词要被调用百万次，必须覆盖所有的边缘情况，提供明确的退路（比如\u0026quot;不确定就输出unsure\u0026quot;）；研究型提示词恰恰相反，只用一两个说明性示例，而且故意选和实际数据完全不同的例子（从事实文档提取信息，用儿童故事做示例），目的是不让模型被格式绑架；通用对话边界更宽松，人在循环中，错了随时改就好。整本书读下来，从定义到能力模型，从底层差异到场景分类，这个框架是完整的。\nAssignment 作业原仓库：modern-software-dev-assignments: Assignments for CS146S: The Modern Software Dev (Stanford University Fall 2025)\n作业个人提交仓库：Sutdown/cs146s\n推荐资料\nAnthropic官方 深入探讨prompt工程 | 全文脱水中文版-腾讯云开发者社区-腾讯云\n","date":"2026-05-09T00:00:00Z","permalink":"https://sutdown.github.io/p/%E6%96%AF%E5%9D%A6%E7%A6%8Fcs146s%E7%8E%B0%E4%BB%A3%E8%BD%AF%E4%BB%B6%E5%BC%80%E5%8F%91%E8%80%85week1%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/","title":"斯坦福CS146S：现代软件开发者Week1学习笔记"},{"content":"互联网应届生工资结构基础 纯常识科普，闲来无事查了一手:），某些地方有问题的话欢迎指正。刚开始看五险一金比例占了22.5%心里大为一惊，详细看了之后发现还好。五险占10.5%的比例，在实际需要时能够使用个人账户的部分，比如退休的养老，医疗，生育，工伤等。住房公积金包括个人缴纳和单位缴纳的总和，北京互联网基本都能到12%，当租房购房等情况下是可以提取的，满足租房需求绰绰有余。另外还有个专项附加标准，文章里有详细介绍，对应届生最有用的是租房补贴，这个一般年末申请，然后能够减少你所交的个人所得税，在次年3-6月个人所得税app申请可以返还，以上。\n以北京为例，假设工资为20k*16(12 + 4)：\n20k*12的部分中，月薪20k需要先扣除五险一金，扣除五险一金的剩余部分再减去需要缴纳的个人所得税即为最终到手薪资。 20k*4年终奖的部分中，仅需扣除个人所得税。 同时注意20k是否为固定薪资，常见互联网大厂20k为固定工资，年终奖部分会存在绩效的成分（到手年终奖=实际年终奖*绩效 - 所得税）。 部分公司会提供房补，餐补等，可能需要手动申请，可注意公司相关政策。 注意每年3-6月在个人所得税APP申报当前工资情况，会根据当前缴纳税务情况和实际情况对比，出现需要补交或者返还钱款。\n1 五险一金 北京可在京通小程序查看详细数据。\n险种 单位缴纳比例 个人缴纳比例 备注 养老保险 16% 8% 下限 6,326 元，上限 33,891 元。退休后可领取养老金 医疗保险+生育保险（合并征收） 9.8% 2%+3元/月 3元为大病统筹互助金。部分进入医保个人账户，社保卡可直接消费，另一部分进入统筹账户。 失业保险 0.5% 0.5% 农村户口个人不缴纳。失业后及时办理失业登记，可以领取失业金（非主动辞职） 工伤保险 0.2%-1.9% 0% 仅单位缴纳，按行业风险分类，基准比例浮动 住房公积金 5%-12% 5%-12% 下限 2,320 元，上限 33,891 元。详细见下面补充。 中华人民共和国社会保险法__中国政府网\n1.1 五险一金补充 1 住房公积金（北京公积金APP可查看当前公积金情况）\n申请人已连续足额缴存住房公积金3个月（含）以上，可申请提取住房公积金，常见的购房，租房等都满足公积金的提取要求，选择长时间不提取也是存在利息的。\n租房提取公积金时，可选择按月申请，也可以一次申请多个月份。\n无发票申请：每月最多2000元 有发票申请：双限规则，取月租金与月缴存额中的较低值 每月住房公积金：月缴纳存额 =月薪*（个人缴纳比例+单位缴纳比例）\n资料：租房申请提取住房公积金操作详解 住房公积金提取业务问答\n2 北京专项附加扣除标准 应届生均可申请每月1500的住房租金补贴，一般在每年12月底，需个人主动申报（个税 APP 或网页），各个城市标准不一定相同，通过减少应纳税所得额，降低你要缴纳的个人所得税，最终体现为到手工资增加或年度汇算退税。\n扣除项目 北京标准 扣除规则 住房租金 每月 1500 元（直辖市标准） 主要工作城市无自有住房，与住房贷款利息不可同时享受 子女教育 每月 1000 元 / 子女 3 岁至博士，父母各扣 50% 或一方全扣 继续教育 学历教育每月 400 元职业资格教育 3600 元 / 年 学历教育最长 48 个月 大病医疗 年度累计超 15,000 元部分，限额 80,000 元 汇算时扣除，可本人或配偶扣，未成年子女可由父母扣 住房贷款利息 每月 1000 元 首套房，最长 240 个月，夫妻一方扣 赡养老人 独生子女每月 3000 元非独生子女每月≤1500 元 父母≥60 岁，可兄弟姐妹分摊 3 岁以下婴幼儿照护 每月 1000 元 / 婴幼儿 父母各扣 50% 或一方全扣 3 个人所得税 扣除五险一金后为所需纳税的份额。\n级数 全年应纳税所得额区间 税率 (%) 速算扣除数 (元) 1 不超过 36,000 元的部分 3 0 2 超过 36,000 元至 144,000 元的部分 10 2,520 3 超过 144,000 元至 300,000 元的部分 20 16,920 4 超过 300,000 元至 420,000 元的部分 25 31,920 5 超过 420,000 元至 660,000 元的部分 30 52,920 6 超过 660,000 元至 960,000 元的部分 35 85,920 7 超过 960,000 元的部分 45 181,920 数据来源：国家税务总局 2026 年最新公告\n","date":"2026-01-18T00:00:00Z","permalink":"https://sutdown.github.io/p/%E4%BA%92%E8%81%94%E7%BD%91%E5%BA%94%E5%B1%8A%E7%94%9F%E5%B7%A5%E8%B5%84%E7%BB%93%E6%9E%84%E5%9F%BA%E7%A1%80/","title":"互联网应届生工资结构基础"},{"content":"MCP在agent中是个并不陌生的词语，本文将从定义，作用，代码等多个角度尽量详细讲述它最为完整的用法。\nMCP是什么？ MCP (Model Context Protocol) 是由 Anthropic 公司推出的一个开放标准，旨在解决 **AI 模型与外部数据源、工具（如本地文件、数据库、API）**之间的连接难题。它相当于为 AI 打造了一个“通用接口”，让开发者只需编写一次连接器，就能让不同的 AI 客户端（如 Claude、IDE 或其他助手）无缝、安全地访问各类实时数据和功能。\nMCP可以理解成 在AI领域发明了USB接口，让模型和工具之间实现了标准化。MCP本质上是模型调用数据，那么它跟tool，function call有什么区别呢？\nMCP VS function call VS tool tool泛指模型可以调用的外部能力。 function calling 是一种模型和外部函数通信的结构化接口标准。调用过程中，模型会输出json格式告诉系统需要用到的函数工具，之后再由系统调用函数获得结果，然后给模型生成最终回复。 MCP 是一个”模型-工具生态“的标准化协议。 初始化：client启动server进程，询问server所支持的tools，资源文件等 运行：当我们调用LLM时，LLM用来确定使用哪些MCP server，MCP server执行对应的函数得到执行结果返回给client 维度 Function Calling (传统方式) MCP (新一代标准) 本质 模型输出能力：模型根据你的指令生成一段 JSON，告诉你该调用哪个函数。 通用连接协议：一套连接模型、客户端与数据源的标准框架。 耦合度 紧耦合：你需要为每个模型（OpenAI, Anthropic 等）写特定的 API 代码和 Schema。 解耦/插拔：写一次 MCP Server，任何支持 MCP 的客户端（Claude, Cursor 等）都能直接用。 执行位置 开发者端：你需要自己写逻辑去解析 JSON 并运行函数。 客户端/Server 端：逻辑封装在 MCP Server 中，客户端只需“插上”即可自动运行。 交互模式 单次请求-响应：模型给指令，你执行，给结果。 持续会话/动态发现：支持资源订阅、实时更新和多工具链式调用。 安全性 应用级控制：安全性全靠你自己在代码里怎么写。 协议级隔离：通过标准化的权限管理和本地进程隔离，更安全地访问私有数据。 MCP的通信模式 在 Model Context Protocol (MCP) 的标准规范中，通信主要基于 JSON-RPC 2.0 协议，而其底层实现（Transport 层）主要通过三种常见的模式（或传输方式）来完成。\n1.标准输入输出模式\nMCP 宿主程序（如 Claude Desktop 或 IDE）直接启动 MCP 服务器作为一个子进程。双方通过操作系统的标准输入 (stdin) 和标准输出 (stdout) 流来交换 JSON-RPC 消息。\n适用场景：本地文件系统操作、本地脚本运行、开发者在自己机器上调试工具。\n2.HTTP+SSE 模式 (Server-Sent Events)\n利用sse建立一个持久的单向连接，服务器可以随时向客户端推送消息\n客户端通过标准的http请求发送消息到服务器\n适用场景：连接云端数据库、调用 SaaS 服务的 API、远程 AI Agent 的协作。\n3.可流式传输的 HTTP 模式 (Streamable HTTP)\n相比sse，处理大量并发请求时更高效\n适用场景：需要返回大量实时数据流（如实时日志、长文本生成）的复杂场景。\nMCP的运行逻辑 MCP client：负责启动并连接 Server，将用户的指令转发给 Server，并将 Server 返回的数据喂给 AI 大模型。\nMCP Server：负责连接具体的工具（如数据库、API、本地文件）\n可以在本地用@mcp.tool快速定义工具，也可以直接采用其它外部工具的mcp server。使用本地工具时，在握手阶段，server会自动将装饰器@mcp.tool转换成由name，description，inputSchema的json结构返回；随后在决策阶段，mcp收到来自client的问题时，LLM识别出意图，能够回复client调用哪些函数，随后client将该结果发送给server端执行，server执行后将结果返回给client。 上段中的LLM识别意图 是在我们直接用大模型配置MCP的过程中主动执行的，那么在实际写代码的时候，这部分是如何实现的？ 步骤 参与方 核心动作 1. 发现 Client $\\leftrightarrow$ Server 确认有哪些工具可以用（握手） 2. 决策 LLM 决定用哪个工具，生成调用参数 3. 执行 MCP Server 在本地/私有环境执行代码，获取真实数据 4. 完成 Client $\\rightarrow$ LLM 将真实数据喂回模型，生成最终回复 代码实践 function call调用流程 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 26 27 28 from langchain_core.tools import tool from langchain_openai import ChatOpenAI llm = ChatOpenAI( model=\u0026#34;deepseek-chat\u0026#34;, api_key=\u0026#34;sk-xxxxx\u0026#34;, base_url=\u0026#34;https://api.deepseek.com\u0026#34; ) @tool(description=\u0026#34;查询天气信息\u0026#34;) def query_weather(city_name: str) -\u0026gt; str: return f\u0026#34;{city_name}今天晴朗，气温 25 度，适合户外活动\u0026#34; llm_with_tools = llm.bind_tools([query_weather]) user_question = \u0026#34;北京今天天气怎么样？\u0026#34; messages = [{\u0026#34;role\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34;: user_question}] response = llm_with_tools.invoke(messages) print(response.tool_calls) # 执行工具调用 if response.tool_calls: tool_result = query_weather.invoke(response.tool_calls[0]) messages.append(response) messages.append(tool_result) final_response = llm_with_tools.invoke(messages) print(final_response.content) 独立的MCP示例 MCP Server\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 from mcp.server import Server from mcp.types import TextContent server = Server(name=\u0026#34;demo-mcp-server\u0026#34;) @server.tool( name=\u0026#34;analyze_text\u0026#34;, description=\u0026#34;Analyze text and detect whether it contains errors\u0026#34; ) async def analyze_text(text: str) -\u0026gt; str: if \u0026#34;error\u0026#34; in text.lower(): result = \u0026#34;❌ Detected error in text\u0026#34; else: result = \u0026#34;✅ Text looks normal\u0026#34; # 保存到服务器状态 server.state[\u0026#34;last_result\u0026#34;] = result return result @server.resource(\u0026#34;memory://last_result\u0026#34;) async def last_result(): return TextContent( type=\u0026#34;text\u0026#34;, text=server.state.get(\u0026#34;last_result\u0026#34;, \u0026#34;No result yet\u0026#34;) ) # 4️⃣ 运行 MCP Server if __name__ == \u0026#34;__main__\u0026#34;: server.run_stdio() MCP client\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 from mcp.client import Client client = Client( command=\u0026#34;python\u0026#34;, args=[\u0026#34;mcp_server.py\u0026#34;] ) text_to_check = \u0026#34;There is an error in the system\u0026#34; result = client.call_tool( tool_name=\u0026#34;analyze_text\u0026#34;, arguments={\u0026#34;text\u0026#34;: text_to_check} ) last_result = client.read_resource(\u0026#34;memory://last_result\u0026#34;) print(\u0026#34;Last result from resource:\u0026#34;, last_result.text) langchain和MCP 我们知道在MCP具体执行相应的server之前存在意图识别的过程，由于langchain的封装，意图识别的步骤不再需要人为完成，具体体现在当把tools绑定到agent时，langchain讲tools的schema注入agent prompt，再之后LLM看到user prompt时，能够自己判断需要的工具和对应的参数，然后由MCP server端执行。\n具体步骤如下：\n1 启动MCP server独立进程\n1 2 npx @modelcontextprotocol/server-filesystem ./data python mcp_server.py # 本地 2 初始化MCPClient\n1 2 3 4 5 6 from langchain_mcp import MCPClient mcp = MCPClient( command=\u0026#34;python\u0026#34;, args=[\u0026#34;mcp_server.py\u0026#34;], # MCP Server 启动方式 ) tools = mcp.get_tools() # 获取MCP tools，拉取tool原信息（tool/description/args schema），转换成langchain的StructuredTool 3 agent调用\n1 2 3 4 5 6 7 8 9 10 11 agent = create_react_agent( llm=llm, tools=tools, prompt=prompt ) # 修改prompt进行意图识别 result = agent.invoke({ \u0026#34;input\u0026#34;: \u0026#34;帮我分析这段文本：There is an error in the system\u0026#34; }) print(result) MCP局限 核心：关于tools的定义描述在prompt中会占用模型大量token。\n对于该问题，Anthropic推出Agent Skills系统，这是一套动态加载的指令和脚本集合，采用渐进式披露，它可以教会AI如何完成特定任务。\n综合 MCP 的出现是为了让 Function Call 变得更通用、更易于扩展，MCP 可以看作工具池，Function Call 是决策机制。\nFunction Call 解决了 “模型如何告诉我们它想干什么” 的格式问题。\nfunction call是经过意图识别返回这个llm所需要用到的外部工具，返回json进行调用；MCP则是执行外部工具返回结果的过程。\nMCP 解决了 “我们如何以统一的标准把各种工具和数据喂给模型” 的架构问题。\n在MCP server中，可以接收来自外界的工具 比如tavily搜索工具，google地图等，也可以接收本地工具，只要采用了mcp封装，服务端只要收到了需要运行的name和input schema，就能自动运行得到相应的结果。 在mcp client发送相应信息给server之前，需要llm进行意图识别，在langchain的create react agent中已经封装了相应的逻辑就无需手动识别，如果是自己写的agent注意进行意图识别然后调用相关的工具。 flowchart TD A[用户输入] --\u0026gt; B[LLM] B --\u0026gt;|Function Call 意图识别| C[生成工具调用 JSON] C --\u0026gt; D[MCP Client] D --\u0026gt; E[MCP Server / 工具执行] E --\u0026gt; D D --\u0026gt; B[LLM 继续处理结果] B --\u0026gt; F[最终输出给用户] subgraph LLM层 B C end subgraph MCP层 D E end 八股参考 1 MCP的核心组件 MCP 服务器 (Server)：数据的提供方。它负责连接具体的资源（如数据库、本地文件、API），并将其标准化。 MCP 客户端 (Client)：数据的消费者。通常是集成了 AI 的应用（如 Claude Desktop、IDE），负责发起请求。 本地/远程主机 (Host)：实际运行客户端并维持生命周期的环境。 2 MCP服务端提供哪些类型的资源 MCP 服务端提供的资源主要包括四类：工具执行结果（如上一次分析或计算的结果）、全局或内部状态（如 server.state、计数器、缓存）、静态数据（如配置文件、系统信息）以及动态计算资源（如实时时间或对状态数据的统计），本质上就是一组可被客户端查询的只读数据接口。\nMCP包括的数据：Tool = 执行动作 + 可修改状态；Resource = 查询数据 + 返回结果\nResources (资源)：只读的数据。比如读取一个本地文件内容、查看数据库表结构或获取日志。 Tools (工具)：可执行的操作。比如发送一封邮件、运行一段 Python 代码或修改某个系统设置。 3 MCP支持哪些通信方式和协议 MCP 建立在 JSON-RPC 2.0 基础之上，目前主要支持两种传输层协议：\nstdio (标准输入输出)：最常用的本地连接方式，客户端通过子进程直接启动 Server 并通过管道通信。 SSE (Server-Sent Events)：用于远程连接，基于 HTTP 协议实现服务器到客户端的流式传输。 可流式传输的 HTTP 模式 (Streamable HTTP) 4 MCP中的工具是什么，如何调用 工具是服务端注册的可执行模块，可以理解为 Server 提供的“能力单元”。\n调用过程：\n暴露：Server 告诉 Client ：“我有 read_file 这个工具”。 决策：LLM 通过 Function Calling 决定使用该工具并生成参数。 请求：Client 向 Server 发送 tools/call 请求。 执行：Server 运行代码并把结果传回给 Client，最终喂给 LLM 5 MCP客户端需要做什么 MCP 客户端的作用是负责发起工具调用和资源访问请求、管理通信协议，并获取 Server 执行结果，本身不执行工具逻辑，所有实际操作都在 MCP Server 端完成。\n生命周期管理：负责启动、连接和关闭 MCP Server。 权限管控：在工具真正执行前，向用户询问是否允许执行（安全关卡）。 上下文聚合：将 Server 返回的资源或工具结果转换为 LLM 能理解的消息格式（Prompt），并发送给大模型。 参考链接： 1 MCP (Model Context Protocol)，一篇就够了。 - 知乎\n2 Introducing the Model Context Protocol\n3 MCP market\n4 langgraph And MCP\n","date":"2026-01-08T00:00:00Z","permalink":"https://sutdown.github.io/p/mcp%E8%AF%A6%E8%A7%A3%E6%8C%87%E5%8D%97/","title":"MCP详解指南"},{"content":"python基础 基础语法 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 26 27 28 29 30 31 32 ## list 管理对话历史 messages = [{\u0026#34;role\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;你好\u0026#34;}, {\u0026#34;role\u0026#34;: \u0026#34;assistant\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;你好！有什么可以帮你的吗？\u0026#34;}] ## dict agent状态管理 agent_state = { \u0026#34;user_id\u0026#34;: \u0026#34;user_12345\u0026#34;, \u0026#34;session_id\u0026#34;: \u0026#34;sess_abc\u0026#34;, \u0026#34;messages\u0026#34;: [ {\u0026#34;role\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;帮我查天气\u0026#34;}, {\u0026#34;role\u0026#34;: \u0026#34;assistant\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;好的，请问是哪个城市？\u0026#34;} ], \u0026#34;context\u0026#34;: { \u0026#34;intent\u0026#34;: \u0026#34;weather_query\u0026#34;, \u0026#34;location\u0026#34;: None, \u0026#34;confidence\u0026#34;: 0.92 }, \u0026#34;tool_calls\u0026#34;: [], \u0026#34;metadata\u0026#34;: { \u0026#34;start_time\u0026#34;: \u0026#34;2024-10-27 10:00:00\u0026#34;, \u0026#34;turn_count\u0026#34;: 1 } } ## 元组 不可变数据 message_format = (\u0026#34;role\u0026#34;, \u0026#34;content\u0026#34;, \u0026#34;timestamp\u0026#34;) ## set 去重和快速查找 user_interests = {\u0026#34;科技\u0026#34;, \u0026#34;旅游\u0026#34;, \u0026#34;美食\u0026#34;, \u0026#34;科技\u0026#34;, \u0026#34;运动\u0026#34;} ## 【嵌套TypedDict】LangGraph状态的标准定义方式 ## 定义清晰的状态结构,便于团队协作和维护 class AgentState(TypedDict): messages: List[Message] # 消息列表,每个元素都是Message类型 intent: str next_node: str 注意异常处理。\n方式1 try \u0026hellip;raise\u0026hellip; except ， 方式2 自定义异常， 方式3 with进行上下文管理，保证资源用完一定能够释放 一般需要建立配置文件或者.env文件管理配置。\n添加适当的日志管理。\n装饰器是 Python 的\u0026quot;语法糖\u0026quot;，用于修改函数行为\nAI开发生态 Streamlit：快速构建Agent交互界面，展示对话流、状态可视化、调试工具\nChromaDB ：最简单的向量数据库，适合本地开发和原型。\nlangchain：链式调用基础，构建LLM应用的基础框架 langgraph：利用状态图构建复杂的多智能体系统 需要精确控制执行流程，复杂的条件分支和循环 生产环境的可靠性要求，可观测性和调试能力 需要human-in-the-loop CrewAI：专注于多智能体角色分工和任务编排，一般为顺序或者层级流程，角色分工清晰，适合写作，研究 AutoGen：多智能体自动对话和协作，支持代码执行，自动错误修复，支持人类参与，适合编程，数据分析等任务 Swarm：实验性项目，最小化实现 Gradio = 模型 Demo / Agent 快速展示 Streamlit = 完整应用 / 数据产品 / 控制台级 UI\n维度 Gradio Streamlit 设计初衷 让模型“马上能用” 把 Python 变成 Web App 关注点 模型输入 → 输出 应用流程、状态、页面 思维方式 ML / Agent 导向 App / Data App 导向 学习成本 极低 中等 1 2 3 4 5 LangGraph / LangChain ← 核心逻辑 ↓ Gradio ← Demo / 对话 ↓ Streamlit / FastAPI ← 产品化 基础 Checker Pointer Checkpointer 是 LangGraph 的 自动快照系统，在每个节点执行后保存状态，实现多轮对话和时间旅行。\n跨会话对话记忆通过 Checkpointer + Thread ID 实现状态持久化和恢复。\nCheckpointer = 自动快照系统 保存时机：每个节点执行后 Thread = 隔离不同会话的状态容器 用途：多轮对话、状态持久化、时间旅行 生产环境：使用 PostgresSaver 或 RedisSaver 链式架构，路由架构（分类器），agent（reAct架构，存在多次循环）\n生成环境部署 开发环境：可直接用InMemoryStore存储，单线程，本地访问，崩溃则停止 生产环境：用PostgreSQL/Redis存储，多进程/多线程并发，结构化日志和监控系统实现监控，错误时能够自动重启或者采用合适的降级策略，公网访问 state reducer memory 记忆起到的作用在于，让Agent从无状态服务变成有记忆的助手\nLangGraph 提供 Checkpointer（检查点器） 机制，在每个步骤后自动保存状态。 为了管理多个独立的对话，LangGraph 使用 Thread（线程） 概念。 checker pointer 短期记忆一般针对单个对话线程，可以在该线程内的任何时间被回忆。\nstate会通过checkpointer持久化数据库中，哪怕会话中断也可以随时回复。记忆在图被调用时或者某个步骤完成时更新，并在某个步骤开始时读取。记忆中为了避免过长的消息列表，通常存在几种解决方案：1 固定窗口 - trim裁剪，2 总结摘要 - summarization，3 利用相关性进行语义过滤\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 class ConversationState(BaseModel): messages: Annotated[List[str], trim_messages(20)] = [] def trim_messages(max_len: int): def reducer(old: List[str], new: List[str]): merged = old + new return merged[-max_len:] return reducer def summarizer_node(state: ConversationState): if len(state.messages) \u0026lt; 15: return {} prompt = f\u0026#34;\u0026#34;\u0026#34; 当前摘要： {state.summary or \u0026#34;无\u0026#34;} 请将以下对话合并成一个更精炼的摘要： {state.messages} \u0026#34;\u0026#34;\u0026#34; new_summary = llm(prompt) return { \u0026#34;summary\u0026#34;: new_summary, \u0026#34;messages\u0026#34;: [] # 清空窗口 } def semantic_filter(messages, query_embedding, k=5): scored = [ (cosine_sim(m.embedding, query_embedding), m) for m in messages ] return [m.content for _, m in sorted(scored, reverse=True)[:k]] 消息处理方式 消息删除，基于meessage 消息过滤，节点返回 消息裁剪，基于token langgraph与langchain memory LangChain 和 LangGraph 解决的是“不同层级的记忆问题”\nLangChain：偏“认知层长期记忆”（semantic memory） LangGraph：偏“系统层执行记忆”（execution state） LangChain Memory 体系本质在于如何把过去的信息提供给模型。langchain短期记忆对应memory，比如最近k条，对于会话的总结，限制token等方案；长期记忆则对应向量存储和各种数据库集成，通过retrievers，文档加载器，embedding，文档拆分等工具实现。\n对比维度 LangChain Vector Memory LangGraph Checkpointer 关注点 语义 执行 是否 embedding ✅ ❌ 是否参与 prompt ✅ ❌ 是否可检索 ✅ ❌ 是否能恢复执行 ❌ ✅ 是否长期运行 ⚠️（有限） ✅ langgraph 图 1 LangGraph 将 Agent 建模为状态机,执行过程是状态的连续转换。\n2 State = 数据 + Channels + Reducers (技术核心)\n3 Node = State → State (函数式思维)\n4 Conditional Edge = 动态路由 (控制流的灵魂)\n5 Human-in-the-Loop = 可控性 (生产环境的必需品)\nstate LangGraph 的状态管理基于 TypedDict + Annotated + Reducer 的三层架构。\nstate 定义 state是一种信息共享和上下文管理的工具，起源于状态机理论，广泛用于对话系统，任务自动化和复杂逻辑处理。\n1 存储上下文信息，实现短期记忆\n2 动态逻辑控制，决定系统的下一步行为\n3 任务管理，存储中间的结果\n4 通过存储状态，管理对话的分支逻辑\nState Schema 在定义state的过程中，存在三种方式：\nTypedDict：适用于简单快速开发，无运行时验证（性能最优） Dataclass：需要默认值/方法，无运行时验证 Pydantic：会进行严格的运行时验证（安全性最好） 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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 from pydantic import BaseModel, ConfigDict from datetime import datetime from typing import List class BaseState(BaseModel): \u0026#34;\u0026#34;\u0026#34;所有 State 的统一基类\u0026#34;\u0026#34;\u0026#34; model_config = ConfigDict( validate_assignment=True, # 赋值即校验 extra=\u0026#34;forbid\u0026#34;, # 禁止野字段 frozen=False # LangGraph 需要可变 ) trace: List[str] = [] updated_at: datetime = datetime.utcnow() class InputState(BaseState): query: str = Field(..., description=\u0026#34;User original input\u0026#34;) user_id: str | None = None class WorkingState(InputState): # Planner plan: Optional[str] = Field( None, description=\u0026#34;High level plan generated by planner\u0026#34; ) # Retriever documents: List[str] = Field( default_factory=list, description=\u0026#34;Retrieved documents\u0026#34; ) # Memory / Reasoning thoughts: List[str] = Field( default_factory=list, description=\u0026#34;Internal reasoning trace\u0026#34; ) # Control next_action: Optional[str] = None class OutputState(WorkingState): answer: str = Field(..., description=\u0026#34;Final user-facing answer\u0026#34;) @model_validator(mode=\u0026#34;after\u0026#34;) # 在所有字段都准备好之后，对整个 State 的‘存在合法性’做最终审判。 def check_ready(self): if not self.plan: raise ValueError(\u0026#34;Output generated without plan\u0026#34;) return self state reducers Reducer 决定：当 state 的同一个字段被多次写入时，最终 state 应该长什么样。\n在 LangGraph 里：\nNode：负责“产生增量状态（partial state）” Reducer：负责“把多个增量状态合并成一个最终 state” 常见reducer类型有：（也可以选择自定义，比如条件型，组合型，带日志等）\n1 累加型，比如add_messages, 2 覆盖型，3 合并型，4 去重型，5 决策型\nadd_messages 解决了对话系统中 状态覆盖导致历史丢失 的核心问题。\n1 2 3 4 5 6 7 from pydantic import BaseModel from typing import List, Annotated from langgraph.graph import add_messages class AgentState(BaseModel): messages: Annotated[List[str], add_messages] = [] current_step: str | None = None reducer \u0026mdash; add_messages add meaaages主要用于合并消息。但同时能实现\n消息重写： 如果新消息的 id 与现有消息相同，会覆盖旧消息 消息删除：采用RemoveMessage，可以实现滑动窗口，删除敏感信息，清理无关对话历史，优化上下文长度 1 2 3 4 5 6 7 8 9 10 ## 手动方式 class State(TypedDict): messages: Annotated[list[AnyMessage], add_messages] ## 快捷方式（推荐） class State(MessagesState): pass # 自动包含 messages 字段 result = add_messages(initial_messages, new_message) delete_messages = [RemoveMessage(id=m.id) for m in messages[:-2]] Multiple Schemas Multiple Schemas = 在同一个 LangGraph 中，为不同阶段 / 不同节点 / 不同角色使用不同的 State Schema。Schema 是“视图（View）”，不是“存储（Storage）”。只能看到自己需要的字段，只被允许返回自己负责的字段。\nPrivate State（私有状态）：节点间可以传递私有数据，不暴露给外部 Input Schema（输入模式）：限定用户输入的字段 Output Schema（输出模式）：限定返回给用户的字段 Internal State（内部状态）：图内部使用的完整状态 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 ## 在node中 graph.add_node(\u0026#34;planner\u0026#34;, planner_node, input_schema=PlannerState) graph.add_node(\u0026#34;executor\u0026#34;, executor_node, input_schema=ExecutorState) graph.add_node(\u0026#34;reviewer\u0026#34;, reviewer_node, input_schema=ReviewerState) ## 在state中 class ConversationState(BaseModel): messages: Annotated[List[str], add_messages] = [] class PlanningState(BaseModel): plan: str | None = None class ExecutionState(BaseModel): observations: Annotated[List[str], add_messages] = [] class ReviewState(BaseModel): final_answer: str | None = None ## State Nesting（嵌套 State），本质是 组合（composition）而不是继承 class GlobalState(BaseModel): conversation: ConversationState = ConversationState() planning: PlanningState = PlanningState() execution: ExecutionState = ExecutionState() review: ReviewState = ReviewState() external memory \u0026mdash; SQLite Checkpointer SQLite 是一个轻量级的嵌入式数据库：\n不需要独立的数据库服务器 整个数据库存储在单个文件中 性能优秀，广泛应用（被 Andrej Karpathy 称为\u0026quot;超级流行\u0026quot;） Python 内置支持 SqliteSaver 的作用：\n自动创建必要的数据库表 保存图的每一步状态（checkpoint） 支持状态查询和恢复 管理多个对话线程（thread） 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 import sqlite3 ## 创建内存数据库（程序结束后消失） conn = sqlite3.connect(\u0026#34;:memory:\u0026#34;, check_same_thread=False) ## 下载示例数据库（如果不存在） !mkdir -p state_db \u0026amp;\u0026amp; [ ! -f state_db/example.db ] \u0026amp;\u0026amp; wget -P state_db https://github.com/langchain-ai/langchain-academy/raw/main/module-2/state_db/example.db ## 连接到本地数据库文件 db_path = \u0026#34;state_db/example.db\u0026#34; conn = sqlite3.connect(db_path, check_same_thread=False) from langgraph.checkpoint.sqlite import SqliteSaver ## 创建 checkpointer memory = SqliteSaver(conn) CREATE TABLE checkpoints ( thread_id TEXT, -- 对话线程 ID checkpoint_id TEXT, -- 检查点 ID state BLOB, -- 序列化的状态数据 timestamp DATETIME -- 时间戳 ); Human-in-the-loop 人类协作目前存在两种方式：\n一是在图的编译时手动添加断点，常见的是interrupt+update state+invoke\n二是利用stream输出，在输出中的过程中捕捉特定的状态输出\n三是时间旅行，回到特定的状态，修改状态，创建新的分支执行\n前两种都是在于一次的运行中存在的中断，第三种侧重于分支。\nbreakpoints - 断点 在图的执行过程中设置断点，主动暂停等待人工干预。\n三大应用场景：审批，调试，编辑\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 ## interrupt_before 审批，在某个节点或工具真正执行之前，强制暂停整个 Graph 的运行。 ## interrupt_after 调试，某个节点执行完成之后，立刻中断流程 ## update_state 在前两者到达的中断状态下，你可以人为修改 State，然后让 Graph 继续跑。 ## interrupt 负责“停”，update_state 负责“改”，invoke 负责“继续” graph = builder.compile( interrupt_before=[\u0026#34;payment_tool\u0026#34;], # 在支付前暂停 checkpointer=memory # breakpoint依赖状态持久化，会将当前状态保存倒checkpoint ) graph.update_state( thread_id, {\u0026#34;approved\u0026#34;: True} ) graph.invoke(None, thread_id=thread_id) Node Interrupt 当 Graph 运行到某个 Node 时，主动暂停，让外部（人类 / 系统）介入，然后再决定是否继续。\ninterrupt() (推荐)：可在代码任意位置调用\n1 2 3 4 5 6 from langgraph.errors import Interrupt def node(state): if not state.get(\u0026#34;approved\u0026#34;): raise Interrupt(\u0026#34;waiting for approval\u0026#34;) return state Stream 对于langgraph：\n默认为执行事件试图，包括节点前，节点结束，中断，工具使用前，工具使用后状态； updates为state增量视图，在每个node执行完返回当前node修改的字段 values为完整state试图，在每个node执行完返回完整的字段 1 2 3 4 5 for event in graph.stream(inputs): ... for update in graph.stream(inputs, stream_mode=\u0026#34;updates\u0026#34;): print(update) Time travel LangGraph 的 Time Travel = 对 Graph 执行过程中每一次 State 的快照做版本化存储，并允许你回到任意历史点继续执行。\n前提：checkerpointer， thread ID\n能力：状态快照，回到过去，改写未来\n1 2 3 4 5 6 7 8 9 current = graph.get_state(thread_id) # 可以查看当前value和历史history state print(current.values) # 当前状态 for snapshot in current.history: print(snapshot.values, snapshot.branch_id) # 历史状态 graph.set_pointer(thread_id, snapshot=current.history[1]) # 回到step之后的快照 ## TODO 这里可以用update state更新状态 graph.update_state(thread_id, {\u0026#34;count\u0026#34;: 100, \u0026#34;note\u0026#34;: \u0026#34;corrected after step1\u0026#34;}) # 修改state graph.invoke(None, thread_id=thread_id) Memory 记忆系统\n短期记忆 + 长期记忆 的架构\nCheckpointer：保存对话历史（短期） Store：保存用户信息（长期） 两者配合实现完整的记忆系统 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 ┌─────────────────────────────────────-────┐ │ Memory Agent │ │ │ │ ┌────────────────────────────────────┐ │ │ │ 短期记忆 (MemorySaver) │ │ │ │ - 对话历史 │ │ │ │ - 当前会话状态 │ │ │ └────────────────────────────────────┘ │ │ │ │ ┌────────────────────────────────────┐ │ │ │ 长期记忆 (InMemoryStore) │ │ │ │ ┌──────────────────────────────┐ │ │ │ │ │ Profile (语义记忆) - profile，collection │ │ └──────────────────────────────┘ │ │ │ │ ┌──────────────────────────────┐ │ │ │ │ │ ToDo Collection (语义记忆) │ │ │ │ │ └──────────────────────────────┘ │ │ │ │ ┌──────────────────────────────┐ │ │ │ │ │ Instructions (程序性记忆) │ │ │ │ │ └──────────────────────────────┘ │ │ │ └────────────────────────────────────┘ │ └────────────────────────────────────────-─┘ Store in memory store\n在内存中保存数据 适合开发和测试 生产环境通常使用持久化存储（如数据库 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 ## 创建内存存储实例 in_memory_store = InMemoryStore() ## put - 保存数据 def put( namespace: tuple, # 命名空间（元组） key: str, # 键（字符串） value: dict # 值（字典） ) -\u0026gt; None: user_id = \u0026#34;1\u0026#34; namespace_for_memory = (user_id, \u0026#34;memories\u0026#34;) key = str(uuid.uuid4()) value = {\u0026#34;food_preference\u0026#34;: \u0026#34;I like pizza\u0026#34;} in_memory_store.put(namespace_for_memory, key, value) ## search - 获取用户所有记忆 memories = in_memory_store.search(namespace_for_memory) print(memories[0].dict()) ## get - 获取特定的一条记忆 memory = in_memory_store.get(namespace_for_memory, key) print(memory.dict()) 记忆的存储模式 热路径：在用户等待响应的过程中执行的操作。\n实时记忆，每次对话都会提取记忆，增加延迟，适用于对话频率低的情况 实现简单，无需考虑并发 冷路径：用消息队列， LangGraph 的 Background Execution，定义处理记忆更新等方式实现\n后台异步处理，响应速度快，适用于大规模or响应速度快的场景 实现复杂，有延迟 Schema Profile 1 简单模式：typedict + with_structured_output()\n2 复杂模式：trustcall（Trustcall 是一个专门用于创建和更新 JSON Schema 的开源库，由 LangChain 团队的 Will Fu-Hinthorn 开发。）\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 ┌─────────────────────────────────────────────┐ │ Trustcall Workflow │ │ │ │ 1. 接收输入 │ │ - 新的对话消息 │ │ - 现有的 Schema（如果有） │ │ │ │ 2. 分析变化 │ │ - 识别新信息 │ │ - 识别需要更新的字段 │ │ │ │ 3. 生成 JSON Patch │ │ - 创建精确的更新操作 │ │ - 只修改变化的部分 │ │ │ │ 4. 应用 Patch │ │ - 更新现有 Schema │ │ - 保留未变化的信息 │ │ │ │ 5. 验证结果 │ │ - 确保符合 Schema 定义 │ │ - 如果失败，自动修正 │ └─────────────────────────────────────────────┘ trustcall Trustcall 是一个让 LLM 在“结构化信息抽取 / 状态写入”场景中，输出“可验证、可约束、可合并”的结果的工具库。定位在于解决 LLM 在“写结构化状态”这件事上的不可信问题。\n当agent需要稳定profile，为长期记忆时，可以采用trustcall\nProfile 模式（enable_inserts=False）和 Collection 模式（enable_inserts=True）\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 ## 定义shcema class UserProfile(BaseModel): name: Optional[str] interests: List[str] = Field(default_factory=list) expertise_level: Optional[str] ## 创建extractor，输出一定符合Pydantic/JSON Schema profile_extractor = create_extractor( UserProfile, description=\u0026#34;\u0026#34;\u0026#34; Extract stable user profile information from conversation. Rules: - Only extract explicit facts - Do NOT guess or infer - Ignore temporary tasks or moods \u0026#34;\u0026#34;\u0026#34; ) ## 在agent中调用 def extract_profile(llm, user_input: str, existing_profile: UserProfile): profile = profile_extractor.invoke( llm, user_input, existing=existing_profile ) return profile Profile vs Collection 对比 Profile = 稳定的“用户画像 / Agent 自我认知” Collection = 可增长、可回放、可组合的“事实条目集合” 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 Profile 模式（5.3） Collection 模式（5.4） ↓ ↓ ┌────────────────┐ ┌────────────────┐ │ 用户资料 │ │ 记忆 1 │ │ ┌──────────┐ │ │ content: ... │ │ │ name │ │ └────────────────┘ │ │ location │ │ ┌────────────────┐ │ │ interests│ │ │ 记忆 2 │ │ └──────────┘ │ │ content: ... │ │ 单一对象 │ └────────────────┘ └────────────────┘ ┌────────────────┐ │ 记忆 3 │ 更新 = 修改字段 │ content: ... │ └────────────────┘ 多个独立对象 更新 = 修改现有项 插入 = 添加新项 Collection 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 from pydantic import BaseModel, Field class Memory(BaseModel): content: str category: Optional[str] = None # 可选的类别 importance: Optional[int] = None # 可选的重要性评分 timestamp: datetime = Field(default_factory=datetime.now) class MemoryCollection(BaseModel): memories: list[Memory] = Field( description=\u0026#34;A list of memories about the user.\u0026#34; ) model = ChatOpenAI(model=\u0026#34;gpt-5-nano\u0026#34;, temperature=0) model_with_structure = model.with_structured_output(MemoryCollection) memory_collection = model_with_structure.invoke([ HumanMessage(\u0026#34;My name is Lance. I like to bike.\u0026#34;) ]) for mem in memory_collection.memories: print(mem.model_dump()) ## 保存到store key = str(uuid.uuid4()) # 生成唯一 ID value = memory_collection.memories[0].model_dump() in_memory_store.put(namespace_for_memory, key, value) 高级图模式 Parallelization 核心在于让多个节点在同一时间步内并行执行,充分利用计算资源。\n在state上可以采取Reducer机制解决并行冲突，比如覆盖，拼接，去重，合并，自定义\nSub Graph 子图就是在一个大 Graph 内部的“可重用小 Graph”，它本身也有节点、状态和执行逻辑，可以像普通 Node 一样被调用。\n特点：可重用，可嵌套，可调试\n适用场景：\n独立的业务逻辑 需要状态隔离 可复用的流程 并行执行 节点和子图 节点是执行单元，主要做“具体操作”；子图是逻辑单元，管理“执行流程和状态”，支持嵌套、复用和局部 Time Travel。\n特性 Node 子图 (Subgraph) 定义粒度 单个执行单元 一段可重用、独立的 Graph State 管理 只能访问父 Graph 的 State 有自己的内部 State，可隔离或映射到父 Graph 可组合性 父 Graph 执行它 → 完 可以嵌套调用，组合成更大 Graph 可复用性 复用性低，逻辑固定 可以在多个父 Graph 中复用子图 Time Travel / checkpoint 依赖父 Graph，粒度粗 内部可独立 checkpoint，局部 Time Travel 调试可观测性 只能 stream 单节点变化 内部也可 stream / updates / events 嵌套深度 通常是“叶子节点” 可以包含多个节点，甚至子图嵌套子图 Map Reduce 定义 Map-Reduce 是一种将任务拆分成“多个小任务（Map）”，并最终汇总结果（Reduce）的方法。 在 LangGraph 中，它被用来处理批量输入、并行子任务，最终合并到父 Graph 的状态。\n组成 1 有Map function接受输入列表，执行节点或者子图；Reduce Function收集map阶段所有结果聚合\n2 每个map任务有独立的state\n适用场景 需要对多个数据项执行相同操作（如批量翻译、批量摘要） 任务可以自然分解为独立子任务（如分段处理文档） 需要从多个候选结果中筛选（如生成多个答案选最佳） 数据量大，需要并行加速（如分析多个数据源） 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 26 27 28 29 30 31 32 33 34 35 36 from langgraph.graph import StateGraph from langgraph.checkpoint.memory import MemorySaver from typing import TypedDict ## 父 Graph 的 State class ParentState(TypedDict): inputs: list results: list ## 定义 Map 子图：对每个元素做操作 def map_task(item: int): return item * 2 # 简单倍数处理 ## 定义 Reduce 节点 def reduce_task(state: ParentState, map_outputs): # 汇总所有 Map 输出 state[\u0026#34;results\u0026#34;] = map_outputs return state ## 构建父 Graph builder = StateGraph(ParentState) ## Map-Reduce 节点 builder.add_map_reduce( name=\u0026#34;map_reduce_example\u0026#34;, items_key=\u0026#34;inputs\u0026#34;, # 输入列表 map_func=map_task, # Map reduce_func=reduce_task # Reduce ) builder.set_entry_point(\u0026#34;map_reduce_example\u0026#34;) graph = builder.compile(checkpointer=MemorySaver()) ## 执行 res = graph.invoke({\u0026#34;inputs\u0026#34;: [1,2,3,4], \u0026#34;results\u0026#34;: []}) print(res) # {\u0026#39;inputs\u0026#39;: [1,2,3,4], \u0026#39;results\u0026#39;: [2,4,6,8]} 1 2 3 4 5 graph.add_conditional_edges( \u0026#34;source_node\u0026#34;, # 源节点 condition_function, # 返回 Send 列表的函数 [\u0026#34;target_node\u0026#34;] # 目标节点（Send 指向的节点） ) Map-reduce VS 并行 类比 send vs add edge\n动态处理任务 vs 静态处理任务\nOther 可视化图 1 display(Image(graph.get_graph().draw_mermaid_png())) create_agent和手动构建stategraph create_agent 是“行为封装”，StateGraph 是“流程编排” 两者不是同一层级的东西，而是 可以叠加使用 的。\n维度 create_agent 手动 StateGraph 抽象层级 高（Agent 行为） 低（执行流程） 关注点 怎么思考 / 用工具 怎么走流程 / 管状态 控制权 交给 LLM 交给开发者 可预测性 低 高 适合角色 单点智能体 系统级编排 参考资料 1 learnGraph.online\n2 AI-agents\n","date":"2025-12-26T00:00:00Z","permalink":"https://sutdown.github.io/p/langgraph%E6%A6%82%E8%BF%B0/","title":"langgraph概述"},{"content":"声明 有一定的编程基础，该篇属于学习笔记，如有理解不正确的地方欢迎各位指出。\nlangchain组件 在正式阅读langchain源码之前，先期望对于langchain有一个初步的理解，langchain往上为agent应用开发的基本框架，往下则是基于LLM，以及一些其它的工具实现。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 Models 主要涵盖大预言模型，为不同的基础模型提供统一接口，便于自由切换不同的模型 - LLMs - Chat Models - Embeddings 对文档转化成向量，总结等 Prompts 支持各种自定义模板 - templates - few-shot examples - examplate selector ... Indexs - docement loaders 文档加载器（如何从不同的数据源中加载数据，比如email，pdf，markdown，latex等） - text splitters 文档拆分器（当输入数据长度过大时的处理） - vectorstores 向量存储器（数据的搜索其实就是向量关系的匹配，即向量运算） - retrievers 检索器（对接向量存储器） ... memory - ConversationBufferMemory 所有聊天记录 - ConversationBufferWindowMemory 最近k轮聊天记录 - ConversationTokenBufferMemory 最近token条记录 - ConversationSummaryMemory 只存储一个用户和机器人之间聊天摘要 - ConversationSummaryBufferMemory - VectorStored-BackedMemory 通过向量的方式存储，匹配最相似k组对话 Chains - LLMChain - RouterChain - SimpleSequentialChain - SequentailChain - TransformChain ... Agents action agents plan-and-execute agents - conversational - openAIfunctions - self ask with search ... llangchain处理文本的流程。\nembedding之前需要split。原因在于：\n1 embedding本身有最大token的限制\n2 检索时，一般是能匹配到其中的某个chunk，有助于检索的精确性\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 Document Loader 文档加载 ↓ Raw Documents ↓ TextSplitter 接收Document，输出更细的document ↓ Document(chunks) ↓ Embeddings 文本转化成向量 ↓ Vectors（.from_documents) ↓ Vectorstore (FAISS/Chroma/Pinecone) ↓ Retriever(vectorstore.as_retriever) Agent agent核心 Agent 模块的核心函数用于创建 Agent、配置工具和执行任务。\n1 用户输入 → AgentExecutor（执行器） → Agent（决策器） → LLM（推理） + Tools（工具） → 输出结果 initialize_agent 快速初始化 Agent 的便捷函数，根据指定的工具、LLM 和 Agent 类型（如 AgentType.ZERO_SHOT_REACT_DESCRIPTION）创建 AgentExecutor（负责运行 Agent 的核心循环，协调 Agent 的决策、工具调用和结果处理。是用户调用 Agent 时的主要入口，封装了 “思考 - 行动” 的迭代过程）。\nlangchain源码分析-agent模块整体介绍【12】 - 知乎\n1 2 3 4 5 6 7 8 9 from langchain_classic.agents import initialize_agent, AgentType from langchain_openai import OpenAI from langchain_core.tools import Tool llm = OpenAI(temperature=0) tools = [Tool(name=\u0026#34;Calculator\u0026#34;, func=lambda x: eval(x), description=\u0026#34;用于计算数学问题\u0026#34;)] agent = initialize_agent( tools, llm, agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION, verbose=True ) create_react_agent/ create_structured_chat_agent 针对特定 Agent 类型（比如reAct）的创建函数，更灵活地配置提示词（Prompt）和解析器。\n1 2 3 4 5 from langchain_classic.agents import create_react_agent from langchain_core.prompts import PromptTemplate prompt = PromptTemplate.from_template(...) # 自定义 ReAct 提示词 agent = create_react_agent(llm, tools, prompt) AgentExecutor.invoke\n执行 Agent 并获取结果的核心方法，接收用户输入并返回最终答案。支持同步（invoke）和异步（ainvoke）调用。\n1 2 result = agent.invoke({\u0026#34;input\u0026#34;: \u0026#34;3的平方加上5的立方等于多少？\u0026#34;}) print(result[\u0026#34;output\u0026#34;]) # 输出计算结果 经典agent体系 1 2 3 4 5 6 7 8 9 10 11 12 # ReAct模式，多步骤推理 AgentType.ZERO_SHOT_REACT_DESCRIPTION # 基于zere-shot reAct，引入memory，适合聊天型工具调用 AgentType.CONVERSATIONAL_REACT_DESCRIPTION # 工具描述比ReAct更严格，输出强制JSON AgentType.STRUCTURED_CHAT_ZERO_SHOT_REACT_DESCRIPTION # OpenAI Function Calling流行后退出的agent，适合生产环境 AgentType.OPENAI_FUNCTIONS # 长任务拆分agent，适合大任务，多步骤自动化执行 from langchain.agents import PlanAndExecute # 存在历史基于，有向量库，主要用于知识库，企业文档AI助手之类的RAG系统 ConversationalRetrievalChain model 静态模型 常见的聊天模型，封装了对应模型和初始化。\ncreate_agent会创建一个可执行的状态图（StateGraph），核心目标在于构建一个能自主调用工具的智能体执行流程，本质是封装了 “语言模型调用→工具调用→结果处理→循环迭代” 的逻辑，最终返回一个编译后的StateGraph（状态图）。\ninit_chat_model是一个统一入口，通过参数动态选择模型提供商和模型，实现 “多对一” 的灵活调用。它可以根据输入的 model 或 model_provider 参数，自动初始化对应的具体模型类，无需手动显式导入不同的类。通常可以和bind_tools一起使用。\nChatOpenAI是针对特定模型提供商的直接封装（如 OpenAI、Anthropic 等），属于 “一对一” 的绑定关系。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 from langchain.agents import create_agent from langchain_openai import ChatOpenAI model = init_chat_model( \u0026#34;claude-sonnet-4-5-20250929\u0026#34;, # Kwargs passed to the model: temperature=0.7, timeout=30, max_tokens=1000, ) model = ChatOpenAI( model=\u0026#34;gpt-5\u0026#34;, temperature=0.1, max_tokens=1000, timeout=30 # ... (other params) ) # 创建智能体 graph = create_agent( model=\u0026#34;anthropic:claude-sonnet-4-5-20250929\u0026#34;, tools=[check_weather], system_prompt=\u0026#34;You are a helpful assistant\u0026#34;, ) # 调用智能体 inputs = {\u0026#34;messages\u0026#34;: [{\u0026#34;role\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;what is the weather in sf\u0026#34;}]} for chunk in graph.stream(inputs, stream_mode=\u0026#34;updates\u0026#34;): print(chunk) 动态模型 @wrap_model_call 装饰器创建中间件，主要作用是拦截模型调用过程进行拦截、修改或增强，例如实现重试逻辑、请求 / 响应改写、错误处理等。它允许开发者在不修改核心模型调用逻辑的前提下，灵活扩展模型交互的行为。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 @wrap_model_call def fallback_model(request, handler): try: return handler(request) # 尝试主模型 except Exception: # 切换到备用模型 request = request.override(model=fallback_model_instance) return handler(request) agent = create_agent( model=basic_model, # Default model tools=tools, middleware=[fallback_model] ) invoke invoke 函数是 Agent 与外部交互的主要入口，负责协调工具调用、模型推理和流程控制等核心流程。\n1 2 3 from langchain_openai import ChatOpenAI llm = ChatOpenAI(model=\u0026#34;gpt-3.5-turbo\u0026#34;, temperature=0) response = llm.invoke(\u0026#34;介绍一下 LangChain\u0026#34;) tools Tool 是代理（Agent）可调用的“函数能力单元”，帮助大模型与外部世界交互。\n模块路径 是否包含 Tool 说明 langchain.tools 有基础抽象、少量工具 数学\u0026amp;计算类工具，Calculator、Shell、Python REPL 等 langchain_community.tools ⭐绝大多数内置工具 python执行工具，搜索引擎工具、浏览器、文件类工具、代码、网络请求，文档加载工具等 langchain_community.agent_toolkits 工具包（多 Tool 打包） SQL、JSON、GSheets、Spark、Vector Store 定制Tool 简单场景 1 2 3 4 5 6 7 8 9 10 from langchain.tools import Tool def get_user(user_id: str): return {\u0026#34;user\u0026#34;: user_id, \u0026#34;name\u0026#34;: \u0026#34;Zhou Jiang\u0026#34;} user_tool = Tool( name=\u0026#34;get_user_info\u0026#34;, func=get_user, description=\u0026#34;Get user info by user_id\u0026#34; ) 复杂参数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 from langchain.tools import StructuredTool from pydantic import BaseModel class UserInput(BaseModel): user_id: int verbose: bool def fetch_user(user_id: int, verbose: bool): return {\u0026#34;id\u0026#34;: user_id, \u0026#34;detail\u0026#34;: verbose} user_tool = StructuredTool.from_function( func=fetch_user, args_schema=UserInput, name=\u0026#34;fetch_user\u0026#34;, ) prompt 基础prompt 基于 PromptTemplate 的基础文本模板生成，属于通用文本提示模板。\n1 2 3 # 基础文本生成 prompt = PromptTemplate.from_template(\u0026#34;Write a {length} poem about {topic}.\u0026#34;) prompt_text = prompt.format(length=\u0026#34;short\u0026#34;, topic=\u0026#34;spring\u0026#34;) 多角色prompt 基于 ChatPromptTemplate 的多角色对话支持，属于对话式消息提示模板。\n原生支持上下文插入，无需手动拼接。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 from langchain_core.prompts import ChatPromptTemplate, SystemMessagePromptTemplate, HumanMessagePromptTemplate system_template = \u0026#34;You are a helpful assistant that translates {input_language} to {output_language}.\u0026#34; human_template = \u0026#34;Translate: {text}\u0026#34; # AIMessagePromptTemplate：AI 回复（用于示例）（一般很少使用） system_prompt = SystemMessagePromptTemplate.from_template(system_template) human_prompt = HumanMessagePromptTemplate.from_template(human_template) chat_prompt = ChatPromptTemplate.from_messages([system_prompt, human_prompt]) result = chat_prompt.format_prompt( history=\u0026#34;\u0026#34;, # 历史消息 input_language=\u0026#34;English\u0026#34;, output_language=\u0026#34;French\u0026#34;, text=\u0026#34;Hello world\u0026#34; ).to_messages() FewShotPromptTemplate 基于 FewShotPromptTemplate 的少样本学习能力。当遇到提示词长度过长或者相关性不足时，langchain给出了示例选择器（Example Selector）\u0026mdash;\u0026mdash;- LengthBasedExampleSelector、MaxMarginalRelevanceExampleSelector 和 NGramOverlapExampleSelector。\nLengthBasedExampleSelector：根据提示词长度动态选择示例，确保最终生成的提示词不超过预设的最大长度（避免超出模型上下文限制）。 MaxMarginalRelevanceExampleSelector：基于语义相关性和多样性选择示例，优先保留与输入语义相似且彼此差异较大的示例（平衡相关性和多样性）。 NGramOverlapExampleSelector：基于N-gram 重叠度（文本表层特征的重合度）选择示例，优先保留与输入共享更多短语或词汇的示例。 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 26 from langchain.prompts import PromptTemplate from langchain.prompts.example_selector import NGramOverlapExampleSelector examples = [ {\u0026#34;query\u0026#34;: \u0026#34;如何学习Python编程\u0026#34;, \u0026#34;response\u0026#34;: \u0026#34;建议从基础语法开始...\u0026#34;}, {\u0026#34;query\u0026#34;: \u0026#34;Python入门教程\u0026#34;, \u0026#34;response\u0026#34;: \u0026#34;推荐使用官方文档...\u0026#34;}, {\u0026#34;query\u0026#34;: \u0026#34;Java学习方法\u0026#34;, \u0026#34;response\u0026#34;: \u0026#34;先掌握面向对象概念...\u0026#34;} ] example_prompt = PromptTemplate( input_variables=[\u0026#34;query\u0026#34;, \u0026#34;response\u0026#34;], template=\u0026#34;Query: {query}\\nResponse: {response}\u0026#34; ) # 初始化N-gram选择器（2-gram，选择1个最佳示例） selector = NGramOverlapExampleSelector( examples=examples, example_prompt=example_prompt, k=1, ngram_size=2 ) # 输入：与“Python学习”相关的查询 input = {\u0026#34;query\u0026#34;: \u0026#34;Python学习路径\u0026#34;} selected_example = selector.select_examples(input) print(selected_example[0][\u0026#34;query\u0026#34;]) # 输出：\u0026#34;如何学习Python编程\u0026#34;（与输入共享更多2-gram） chain [!CAUTION]\nLangChain 的 Chain 在 LangGraph 中不再作为核心概念出现，已经被更现代的 “Graph（图）” 和 “Runnable（可运行单元）” 体系替代。\nagent内部是依赖chain实现的，也就是封装好的chain，agent相当于在chain外面包装了一层“让模型自动决定下一步”的机制。但是chain太过于限定结构，不太灵活，agent的动态流程也比较难以控制，因此在新版langchain中提出用runnable+function calling替代agent，正式的写法迁移到langgraph中。\nchain是核心组件之一，主要用于将多个组件（比如model，prompt，检索器，tools等）按照特定逻辑串联，形成一个可执行的工作流。\n在 Chain 被调用前，需完成初始化并配置核心参数，确保运行时所需的组件和上下文就绪。\nmemory：可选的记忆组件（如 BaseMemory），用于保存上下文状态。 callbacks：回调管理器或处理器列表，用于监控运行过程（如日志、追踪）。 tags 和 metadata：用于标记和附加链的元数据，便于追踪和分类。 Chain 接收用户输入后，需进行预处理，确保输入格式符合要求，并整合记忆组件的上下文。运行的核心阶段，实际执行链的逻辑（如调用子链、LLM 等），并通过回调机制记录运行状态。\nLLMChain 基础链，一般是将提示词模板和语言模型组合，直接调用模型生成结果。\n1 2 3 from langchain_classic.chains import LLMChain chain = LLMChain(prompt=prompt, llm=llm) result = chain.invoke({\u0026#34;question\u0026#34;: \u0026#34;LangChain 有哪些特点？\u0026#34;}) SequentialChain 顺序链，前一个链的输出作为后一个链的输入，适用于处理多步任务\n另还有TransformChain: 对chains之间的输入和输出进行处理，便于chains之间进行数据传输。支持自定义的转换函数。可以理解为顺序链的升级版。\nSimpleSequentialChain: 每个步骤都有一个单一的输入/输出，并且一个步骤的输出是下一步的输入\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 prompt1 = ChatPromptTemplate.from_messages([ (\u0026#34;system\u0026#34;, \u0026#34;将中文翻译成英文。\u0026#34;), (\u0026#34;human\u0026#34;, \u0026#34;{chinese_text}\u0026#34;) ]) chain1 = LLMChain(llm=ChatOpenAI(), prompt=prompt1, output_key=\u0026#34;english_text\u0026#34;) prompt2 = ChatPromptTemplate.from_messages([ (\u0026#34;system\u0026#34;, \u0026#34;将英文翻译成法语。\u0026#34;), (\u0026#34;human\u0026#34;, \u0026#34;{english_text}\u0026#34;) ]) chain2 = LLMChain(llm=ChatOpenAI(), prompt=prompt2, output_key=\u0026#34;french_text\u0026#34;) sequential_chain = SequentialChain( chains=[chain1, chain2], input_variables=[\u0026#34;chinese_text\u0026#34;], output_variables=[\u0026#34;french_text\u0026#34;] ) result = sequential_chain.run(chinese_text=\u0026#34;我喜欢编程\u0026#34;) ConversationChain LangChain 中用于实现多轮对话功能的链（Chain），其核心功能是结合语言模型（LLM）和记忆组件（Memory），让对话能够保留上下文信息，实现连贯的多轮交互。\n1 2 3 4 5 class ConversationChain(LLMChain): memory: BaseMemory = Field(default_factory=ConversationBufferMemory) prompt: BasePromptTemplate = PROMPT input_key: str = \u0026#34;input\u0026#34; # 用户输入的键名 output_key: str = \u0026#34;response\u0026#34; # 模型输出的键名 RetrievalQA 结合检索器（Retriever）和问答链，先从知识库中检索相关文档，再基于文档回答问题（RAG 场景）。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 # 初始化向量数据库（示例数据） documents = [ Document(page_content=\u0026#34;LangChain 支持多种链类型，包括 LLMChain、SequentialChain 等。\u0026#34;), Document(page_content=\u0026#34;RetrievalQA 链用于检索增强问答，结合检索器和语言模型。\u0026#34;) ] embeddings = OpenAIEmbeddings() db = FAISS.from_documents(documents, embeddings) retriever = db.as_retriever() # 创建 RetrievalQA 链 qa_chain = RetrievalQA.from_chain_type( llm=ChatOpenAI(), chain_type=\u0026#34;stuff\u0026#34;, # 使用 Stuff 策略合并文档 retriever=retriever, return_source_documents=True # 返回用于回答的源文档 ) # 执行问答 result = qa_chain({\u0026#34;query\u0026#34;: \u0026#34;LangChain 有哪些链类型？\u0026#34;}) print(result[\u0026#34;result\u0026#34;]) # 输出：LangChain 支持多种链类型，包括 LLMChain、SequentialChain 等。 print(\u0026#34;源文档：\u0026#34;, [doc.page_content for doc in result[\u0026#34;source_documents\u0026#34;]]) RouterChain 根据输入动态选择合适的子链执行，适用于多场景任务分发。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 # 定义两个子链和路由规则 math_prompt = ChatPromptTemplate.from_messages([ (\u0026#34;system\u0026#34;, \u0026#34;解决数学问题：{input}\u0026#34;) ]) code_prompt = ChatPromptTemplate.from_messages([ (\u0026#34;system\u0026#34;, \u0026#34;生成 Python 代码：{input}\u0026#34;) ]) router_template = \u0026#34;\u0026#34;\u0026#34; 根据输入判断类型： - 若涉及数学计算，返回 `math` - 若涉及代码生成，返回 `code` 输入：{input} \u0026#34;\u0026#34;\u0026#34; router_prompt = PromptTemplate( template=router_template, input_variables=[\u0026#34;input\u0026#34;], output_parser=RouterOutputParser() ) # 初始化路由链和子链 llm = ChatOpenAI() router_chain = LLMRouterChain.from_llm(llm, router_prompt) math_chain = LLMChain(llm=llm, prompt=math_prompt) code_chain = LLMChain(llm=llm, prompt=code_prompt) chain_map = {\u0026#34;math\u0026#34;: math_chain, \u0026#34;code\u0026#34;: code_chain} multi_prompt_chain = MultiPromptChain( router_chain=router_chain, destination_chains=chain_map, default_chain=LLMChain(llm=llm, prompt=ChatPromptTemplate.from_messages([(\u0026#34;human\u0026#34;, \u0026#34;{input}\u0026#34;)])) ) 其余 另外还有文档处理链：\nStuffDocumentsChain：将所有文档一次性传入模型（适合文档量少的场景） MapReduceDocumentsChain：先单独处理每篇文档（Map），再合并结果（Reduce）（适合大量文档） RefineDocumentsChain：逐步迭代优化结果（适合需要精确结果的场景）。 memory LangChain 中的 memory 模块是实现对话状态管理的核心组件，用于存储和管理对话历史，使模型能够理解上下文并生成连贯的多轮对话。\n基类chain中，输入前会执行load_memory_variables，输出后会执行save_context。数据最终存储在chat_memory之中。\n基础逻辑 一般，所有记忆类均通过 chat_memory（默认 InMemoryChatMessageHistory）存储原始对话消息（HumanMessage/AIMessage），或通过自定义存储（如数据库）持久化。\n对话前，调用 load_memory_variables 时，记忆类将原始消息转换为模型可理解的格式（字符串、摘要或键值对），并通过 memory_key 暴露给链（如 ConversationChain）。\n对话后，save_context 会提取用户输入和模型输出，更新 chat_memory，并触发特殊处理（如摘要生成、窗口截断）。\n另外：可通过自定义 BaseChatMessageHistory 实现持久化存储（如 RedisChatMessageHistory、MongoDBChatMessageHistory），或继承 BaseMemory 实现特定记忆逻辑。\n分类 当前的会话信息保存方案为两种：\n内存（Memory）：把对话历史直接保存在程序内（或轻量持久化），在每次 prompt 中把“历史”按某种策略拼接进 prompt 里（完整/滑动窗口/按 token 限制/摘要化等）。适合短期上下文、低延迟、开发快速迭代。\n向量检索（RAG, Retriever / Index）：把对话（或更广义的知识）拆成文档块，定期把块做 embedding 存入向量数据库（FAISS/Chroma/Pinecone/Milvus 等）。每一轮对话用当前用户 query 去检索最相关的 k 条，再把这些检索到的“补充上下文”连同当前 query 一并送给 LLM（常见于长期记忆、知识库问答或大规模应用）。\n内存型的延迟比较低，实现简单，延迟低，适合短对话或者多轮对话中的短期记忆场景。但对话轮次变多时，消耗的token越来越多，模型也容易丢失早期的重要信息。对话量大时，比较建议采取滑动窗口 + 摘要 的混合：近期用窗口保留详细对话，早期历史做周期性摘要并把摘要保留为长期记忆。\n向量检索在其它模块会再次详细说明。\nUsage 现实系统常把两者结合起来：\n短期 memory（window）：把最近 2–5 轮的对话直接放进 prompt，保证对话连贯性与即时上下文。 长期 retriever（RAG）：针对需要回溯或查询的请求（“告诉我上个月的报表结论”），从向量库检索历史对话或知识并作为补充。 合并策略示例：[SYSTEM] + {recent_window_history} + {retrieved_docs} + user_question。 这种组合既保留低延迟的短期记忆，又能应对长期查询与知识库问答。 1 2 3 4 5 6 7 8 # 每轮： recent = window_memory.format() retrieved = retriever.get_relevant_docs(query, k=4) prompt = system + recent + format_retrieved(retrieved) + user_question answer = llm(prompt) # 保存新消息到 memory 和向量库（upsert） memory.save(user_message, assistant_message) vectorstore.add_texts([user_message, assistant_message], metadata=...) 内存型常见实现策略 ConversationBufferMemory 逐句记录所有对话内容（用户输入 + 模型回复），不做任何截断或总结。适合短对话需要完整保存上下文的场景。\nConversationBufferWindowMemory 仅保留最近 k 轮对话，避免历史过长导致的冗余。中等长度对话，需要控制上下文长度。\nConversationSummaryMemory 通过 LLM 动态总结对话历史，用摘要代替完整历史，减少上下文长度。长对话场景，需要压缩历史信息。\nConversationSummaryBufferMemory 结合摘要和窗口记忆的优点：用摘要保存早期对话，用窗口保留最近 k 轮对话，平衡信息完整性和长度。\nCombinedMemory 组合多个记忆组件\n1 2 3 4 5 6 7 8 9 10 from langchain_classic.memory import CombinedMemory, SimpleMemory # 组合对话记忆和简单键值对记忆 conv_memory = ConversationBufferMemory(memory_key=\u0026#34;history\u0026#34;) simple_memory = SimpleMemory(memories={\u0026#34;user_name\u0026#34;: \u0026#34;小明\u0026#34;}) combined = CombinedMemory(memories=[conv_memory, simple_memory]) combined.save_context({\u0026#34;input\u0026#34;: \u0026#34;你好\u0026#34;}, {\u0026#34;output\u0026#34;: \u0026#34;您好！\u0026#34;}) print(combined.load_memory_variables({})) # 输出：{\u0026#34;history\u0026#34;: \u0026#34;Human: 你好\\nAI: 您好！\u0026#34;, \u0026#34;user_name\u0026#34;: \u0026#34;小明\u0026#34;} SimpleMemory 存储固定的键值对（如用户信息），不随对话更新，适用于保存静态上下文。\nDocument Loaders Document Loaders（文档加载器）是处理数据输入的核心模块，负责从各种数据源（如文件、数据库、API 等）加载加载数据并转换为统一的 Document 格式，为后续的处理（如分割、嵌入、检索）提供基础。\nlangchain源码分析-文档加载【9】 - 知乎\n核心逻辑 针对不同类型的数据源实现特定的加载逻辑，会通过懒加载（lazy_load）或异步加载（alazy_load）方式高效处理大规模数据，避免内存占用过高。将原始数据统一转换为 Document 结构，方便下游组件（如文本分割器、向量存储）处理。\n文件加载：支持csv文件，pdf文件，markdown文件，notebook文件等 结构数据源加载：比如xml文件，git数据源，pandas数据源，pyspark.dataframe数据源加载等 其它数据源：比如email内容，html内容，云服务数据源（cos），视频加载等 懒加载 懒加载是一种 \u0026ldquo;按需加载\u0026rdquo; 机制，通过迭代器（Iterator）逐逐批返回文档，而非一次性一次性将所有文档一次性加载到内存中，从而有效减少内存占用，尤其适合处理大文件或海量数据。\n内存高效：避免一次性加载全部数据到内存，尤其适合 GB 级文件或大量小文件。 流式处理：支持边加载边处理（如即时分割、嵌入），减少等待时间。 兼容性：load() 方法默认基于 lazy_load 实现（通过 list(iterator) 转换），兼顾便捷性。 异步加载 异步加载是懒加载的异步版本，通过异步迭代器（AsyncIterator）实现非阻塞加载，适合需要异步操作的场景（如异步 Web 框架、并发 API 调用）。\n加载网络资源（如异步网页爬取 AsyncHtmlLoader）。 异步框架中处理文档（如 FastAPI 接口内加载数据）。 需要并发加载多个数据源的场景（如同时请求多个 API）。 embedding embedding主要用于将文本转换成稠密向量（dense vector），便于之后的查找。\nEmbedding 是一个抽象层，底层可以是任何模型（OpenAI、HuggingFace、本地模型、自定义模型）。用户在上层统一用：embed_query / embed_documents。\nEmbedding模型 官方内置 Embedding 类型 最适合的应用场景 优点 缺点 成本 性能（效果） OpenAIEmbeddings 商业级 RAG、FAQ、搜索、推荐、多语言检索 语义效果最强、稳定、无需运维 需联网、数据外发、成本较高 中等偏高 ⭐⭐⭐⭐⭐ AzureOpenAIEmbeddings 企业内网、金融/政府行业、合规要求高的知识库 与 OpenAI 效果相同；支持私有网络/VNet；合规性强 只适合 Azure 生态；成本略高 中高 ⭐⭐⭐⭐⭐ HuggingFaceEmbeddings 私有化部署、大规模向量生成、多语言检索、中文 RAG 多模型可选、可自部署、低成本、高灵活性 需要硬件资源（GPU 推荐） 低 ⭐⭐⭐⭐ GPT4AllEmbeddings 轻量级本地 demo、离线应用、资源受限设备 完全本地、CPU 可跑、隐私安全、免费/低成本 效果弱于前面三种 极低 ⭐⭐ 1 2 3 4 from langchain.embeddings import OpenAIEmbeddings from langchain.embeddings import AzureOpenAIEmbeddings from langchain.embeddings import GPT4AllEmbeddings from langchain.embeddings import HuggingFaceEmbeddings Community内置\n1 2 3 4 5 6 7 8 9 10 from langchain_community.embeddings import HuggingFaceHubEmbeddings from langchain_community.embeddings import SentenceTransformerEmbeddings from langchain_community.embeddings import CohereEmbeddings from langchain_community.embeddings import GooglePalmEmbeddings from langchain_community.embeddings import BedrockEmbeddings from langchain_community.embeddings import DashScopeEmbeddings from langchain_community.embeddings import MistralAIEmbeddings from langchain_community.embeddings import OllamaEmbeddings from langchain_community.embeddings import VoyageEmbeddings from langchain_community.embeddings import JinaEmbeddings 调用机制 1 2 3 4 5 6 embedding = embeddings.embed_query(\u0026#34;hello world\u0026#34;) vectors = embeddings.embed_documents([\u0026#34;a\u0026#34;, \u0026#34;b\u0026#34;, \u0026#34;c\u0026#34;]) # langchain新标准 embeddings.invoke(\u0026#34;text\u0026#34;) embeddings.batch([\u0026#34;t1\u0026#34;, \u0026#34;t2\u0026#34;]) retrieves 检索模块更多的是将索引和具体的检索方法作为一个整体，对外提供服务。核心方法是 get_relevant_documents(query)（同步）或 aget_relevant_documents(query)（异步），直接返回与查询相关的文档列表。\n检索器的建立依赖索引，一般和文档加载，embedding，索引，检索链共同出现，构成RAG（检索生成增强的核心组件，可以让LLM基于外部文档回答问题。\nas_retriever() VectorStore.as_retriever() → 将向量库包装成一个标准检索器（Retriever）对象\nvectorStore本身负责存储向量，为了能让vectorStore能被RAG，Chain，Agent统一调用，as_retriever将向量库封装成retrieve最为对外服务的搜索接口。\n整体逻辑 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 26 27 28 29 30 31 32 33 34 35 # 文档加载，加载csv格式的数据 from langchain.document_loaders import CSVLoader file = \u0026#39;OutdoorClothingCatalog_1000.csv\u0026#39; loader = CSVLoader(file_path=file) docs = loader.load() # OpenAIEmbeddings对文本进行向量化 # 调用 OpenAI 的嵌入 API（默认使用text-embedding-ada-002模型），将文本转换为 1536 维向量。 from langchain.embeddings import OpenAIEmbeddings embeddings = OpenAIEmbeddings() # 创建向量索引 # 将向量库转换为检索器，提供统一的检索接口，用于根据用户查询获取相关文档。 from langchain.vectorstores import DocArrayInMemorySearch db = DocArrayInMemorySearch.from_documents( docs, embeddings ) # 将索引转换为检索器，其实是将索引作为检索器的一个变量。检索器提供了不同相关性计算的方法 retriever = db.as_retriever() # 创建检索式问答链 from langchain.chat_models import ChatOpenAI llm_model = \u0026#39;gpt-3.5-turbo-0301\u0026#39; # 后续该模型会下线，替换成其他模型即可 llm = ChatOpenAI(temperature = 0.0, model=llm_model) from langchain.chains import RetrievalQA qa_stuff = RetrievalQA.from_chain_type( llm=llm, chain_type=\u0026#34;stuff\u0026#34;, retriever=retriever, verbose=True ) qa_stuff.query(\u0026#34;List all your shirts with sun protection in a table\u0026#34;) qa_stuff 是通过 RetrievalQA.from_chain_type 创建的问答链实例，query 方法是其对外提供的接口，接收用户输入的自然语言问题（如示例中的 “列出所有带防晒功能的衬衫，用表格展示”）。\n问答链首先会利用初始化传入的retriever从向量库中获取和用户查询相关的文档，检索过程中，会先将用户问题通过embedding向量化，然后默认用余弦相似度和向量库中的数据进行匹配，返回相关的文档作为后续回答生成的上下文。\n然后文档链会将检索到的文档和用户查询组合为一个完整的提示词，最后将构建的提示词传入初始化好的模型生成最终结果。\ncallbacks（可以总结一下分类） LangChain 的 callbacks 模块（现核心接口在 langchain_core.callbacks）是用于监控、记录和干预 LangChain 组件运行过程的核心工具。它基于观察者模式设计，允许开发者在 LLM 调用、链执行、Agent 决策、工具调用等关键节点插入自定义逻辑，实现日志记录、性能监控、数据持久化、流式输出等功能。\nCallback 可在任意 LangChain 组件中注册，包括 LLM、Chain、Agent、Retriever 等\n常用callbacks StdOutCallbackHandler 将组件运行的关键事件（如 LLM 调用、Agent 决策、工具执行）打印到控制台，适用于调试。\n1 2 3 4 5 6 7 8 9 10 11 12 13 from langchain_core.callbacks import StdOutCallbackHandler from langchain_openai import ChatOpenAI handler = StdOutCallbackHandler() llm = ChatOpenAI( model=\u0026#34;deepseek-chat\u0026#34;, temperature=0, callbacks=[handler], # 直接传入 Handler 列表（自动创建 CallbackManager） api_key=DEEPSEEK_API_KEY, base_url=DEEPSEEK_BASE_URL ) llm.invoke(\u0026#34;Hello World\u0026#34;) FileCallbackHandler 将事件日志写入指定文件，适用于生产环境的日志持久化。\n1 2 3 4 5 6 from langchain_core.callbacks import FileCallbackHandler import sys # 写入到标准输出（等同于 StdOutCallbackHandler） # 或写入到文件：handler = FileCallbackHandler(\u0026#34;logs.txt\u0026#34;) handler = FileCallbackHandler(sys.stdout) StreamingStdOutCallbackHandler 实现 LLM 输出的流式打印，适用于需要实时展示生成过程的场景（如聊天机器人）。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 from langchain_core.callbacks import StreamingStdOutCallbackHandler from langchain_openai import ChatOpenAI # 配置 DeepSeek 模型参数 DEEPSEEK_API_KEY = \u0026#34;\u0026#34; # 替换为你的 DeepSeek API Key DEEPSEEK_BASE_URL = \u0026#34;https://api.deepseek.com/v1\u0026#34; # DeepSeek 的 API 基础地址 # 初始化 DeepSeek 模型，启用流式输出 llm = ChatOpenAI( model=\u0026#34;deepseek-chat\u0026#34;, # 指定 DeepSeek 模型，可选 deepseek-chat/deepseek-coder temperature=0.7, # 控制生成随机性，0 为确定性输出，1 为最大随机性 streaming=True, # 必须开启，否则无法流式输出 callbacks=[StreamingStdOutCallbackHandler()], # 绑定流式回调处理器，控制台会实时打印内容 api_key=DEEPSEEK_API_KEY, base_url=DEEPSEEK_BASE_URL ) # 执行生成任务，控制台会实时打印内容 llm.invoke(\u0026#34;介绍你的功能\u0026#34;) 输出解析器Output-parses output_parser 模块用于用于将语言模型（LLM）的原始输出转换为结构化数据（如 JSON、Pydantic 模型、列表等），方便后续处理和使用。langchain源码剖析-output_parses各模块介绍【6】\n最基础 / 常见：StrOutputParser，适用于绝大多数不需要结构化的场景，是默认首选。 结构化需求：JsonOutputParser（简单键值对）和 PydanticOutputParser（带校验的复杂结构）。 列表类输出：CommaSeparatedListOutputParser (将文本串通过’, ‘分隔，转为list格式返回)简单高效。 装饰器 节点式（在特定执行点运行）\n@before_agent - 代理启动前（每次调用一次） @before_model - 每次模型调用前 @after_model - 每次模型响应后 @after_agent - 代理完成时（每次调用一次） 包装式（拦截和控制执行）\n@wrap_model_call - 每次模型调用前后 @wrap_tool_call - 每次工具调用前后 便利装饰器:\n@dynamic_prompt - 生成动态系统提示（相当于修改提示的 @wrap_model_call） 参考文章： 1 LangChain源码学习 | 李乾坤的博客\n2 langchain源码剖析-模块整体介绍 - 知乎\n3 LangChain 文档\n4 github- langchain\n5 LangChain 源码 深度历险：基于GOF的设计模式，穿透 LangChain 源码 - 技术自由圈 - 博客园\n","date":"2025-12-04T00:00:00Z","permalink":"https://sutdown.github.io/p/%E5%9F%BA%E4%BA%8Elangchain%E6%BA%90%E7%A0%81%E5%89%96%E6%9E%90%E5%B8%B8%E8%A7%81%E7%94%A8%E6%B3%95/","title":"基于langchain源码剖析常见用法"},{"content":"项目链接：https://github.com/Sutdown/sutdown.github.io.git\n基本分为：client，tools，memory，prompt，mcp，react agent四个部分，最终进行整合。\nagent有三种基本架构：ReAct，plan and solve，reflection三种模式。\n当前采用的是ReaAct架构，即边思考边行动。首先对于给出的问题利用planner进行分析规划成3-8个具体的步骤（每个步骤中包含具体的推理和行动），在每个步骤中利用现有的tools或者mcp进行执行，同时会memory上一个步骤中的结果，为了节省token，也会对整体内容进行compressor。\nReAct (Reasoning and Acting)： 一种将“思考”和“行动”紧密结合的范式，让智能体边想边做，动态调整。\n核心在于循环 Thought-Action-Observation 这整个过程，在思考当前情况的时，反思上一步结果指定下一步计划，形成一个不断增长的上下文。最终能够达到：推理让行动更具有目的性，行动为推理提供实时依据，观察则用于不断优化每次的推理。\nPlan-and-Solve： 一种“三思而后行”的范式，智能体首先生成一个完整的行动计划，然后严格执行。\n核心在于 原始问题，完整计划，历史步骤和结果，当前步骤 这整个思路\nReflection： 一种赋予智能体“反思”能力的范式，通过自我批判和修正来优化结果。\n先完成整个问题，再审视前面的结果进行反思，常见会通过 ”事实错误，逻辑漏洞，效率问题，遗漏信息等“ 多个常见的不同角度进行反思，对初稿进行反复修改，形成更完善的修订稿。\n每个具体的步骤有一个共同的system prompt和不同的user prompt，user prompt会结合当前任务描述，上个步骤的思考行动输入观察，以及当前的执行计划生成。\nclient 1 2 3 4 5 client - base_client： 抽象基类 - send_recv： 向LLM API发送和接收消息 - extract_txt：从响应消息中提取文本 - chat：send_recv and extract_txt tools 核心工具模块，提供了智能体（Agent）可调用的各类工具函数，支持文件操作、代码执行、代码分析等核心功能，是智能体与外部环境交互的主要接口。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 file_tools - create_file 创建或者覆盖文本文件 - read_file 读取文件内容 - list_directory 列出当前目录内容 - edit_file 编辑文件的指定行（插入，替换，删除） - search_in_file 在文件中搜索文本or正则表达式，支持上下文显示 execution_tools - run_python 运行python代码or脚本 - run_shell 运行shell命令 - run_tests 运行python测试套件 - run_linter 运行代码检查/格式化工具 code_analysis_tools - parse_ast 解析Python文件的AST抽象语法树提取代码信息 - get_function_signature 提取指定函数的签名 - find_dependencies 分析文件的依赖关系 - get_code_metrics 获取代码度量信息 prompt 提示词的基本要素在于：指令（模型需要执行的任务或命令），上下文（包含的外部信息或者额外的上下文信息），输入数据，输出指示。\n另外更加具体的描述：可以在提示词中添加角色，可用的额外工具，少量的样本提示等，以期达到最好的效果。\n角色定义 工具清单（tools） 格式规约（thought/action） 动态上下文（memory） 少样本提示 memory 1 2 3 4 5 context_compressor - should_compress 当对话轮数大于a时，需要压缩 - compress 保留最近b条对话和第1条系统prompt，其余压缩 - _extract_key_information 提取(a-b-1)条信息的摘要，包括文件路径，执行工具，错误信息，任务完成情况四类 - get_compression_status 获取压缩信息（原信息，压缩后信息，压缩率，节省的消息数量） mcp Model Context Protocol 是一个开放协议，它规范了应用程序如何为 LLMs 提供上下文。可以将 MCP 想象为 AI 应用的 USB-C 端口。就像 USB-C 提供了一种标准方式，让你的设备连接到各种外设和配件，MCP 也提供了一种标准方式，让你的 AI 模型连接到不同的数据源和工具。\n1 2 3 4 5 6 7 8 9 10 11 playwright - 提供全栈浏览器自动化能力，支持模拟用户在浏览器中的各类操作。 - 比如网页导航，交互操作，截图，生成pdf等 Context7 - 增强代码代理对长上下文的理解与处理能力，提供语义搜索和上下文压缩。 Filesystem - 提供高级文件系统交互能力，扩展基础文件操作的边界。 Puppeteer - 基于 Chrome 内核的浏览器自动化工具，与 Playwright 功能类似但专注于 Chromium。 SQLite - 提供轻量级数据库交互能力，无需额外部署数据库服务。 1 2 3 MCP Client: 和单个服务器通信，建立连接，发送请求和处理响应 MCP Config: MCP服务器的配置结构，从json配置加载，保存和解析 MCP Manager: 多MCP服务器的管理 MCP Client：\n在MCP客户端采用JSON-RPC格式发送请求，作为标准客户端和服务器之间的通信方式。\nJSON-RPC是一种基于JSON的简单远程过程调用协议。MCP客户端可以同时发送多个请求，服务器响应一般为无序返回，但是JSON-RPC的id字段能够确保每个响应准确对应发起的请求。\n因此对于每个MCP客户端都会开启一个线程，开启线程时通过同步接收来自服务器的信息，筛选其中为该MCP客户端id相同的信息，然后将接收到的完整信息放入输出队列之中。每个线程会拥有一个互斥锁，保证同一时刻只有一个线程能访问共享资源。\n初始化时会发送一个连接请求，以此确认连接成功同时获取可用的工具列表。\nMCP Manager：\nconfig中存在所有的服务器配置信息，clients中存在所有服务器名称对应的客户端\n启用和关闭服务器都是先通过config确认配置信息，之后通过client启用or关闭\n_tools_cache中存储着当前可用的工具，工具中的运行函数需要在manager中写，因为是通过client中的通信运行工具的。同时当服务器信息发生变更时，需要同步更新工具信息。\ncore re-act agent 运行流程。ReAct 框架为单步单动作循环逻辑，多动作场景会通过拆分为多个步骤来处理，而非在单个步骤中包含多个 action。\n1 通过规划器生成流程。planner中的变量包括：step_number, action, reason, completed, result\n2 每个步骤都需要调用LLM API，压缩器，prompt，每个步骤具体过程如下：\nsystem prompt + user prompt（包括planner生成的计划完成情况，ai对上一步生成的step，上一步执行工具后的返回结果） 调用API执行当前步骤得到返回结果，返回结果中存在 thought, action, action_input。 将返回结果的action input通过action执行得到观察结果observation 回调实时输出步骤 异常情况：1 调用api错误，直接记录当前error，重新规划。2 action为已完成时，可以直接return。3 检查工具是否正确，不对则重新规划当前步骤 ","date":"2025-12-01T00:00:00Z","permalink":"https://sutdown.github.io/p/code-agent/","title":"Code Agent"},{"content":"0 前言 主要参考这份资料datawhalechina/llm-cookbook: 面向开发者的 LLM 入门教程，吴恩达大模型系列课程中文版。类似于学习笔记，文字笔记部分摘自原文，代码的部分进行了修改，一个是重新修正了代码逻辑，关于一些库的更新也用了新的函数运行；另一个在于将openai的app key改成了阿里的通义千问，有部分免费额度，国内运行也比较稳定。\n1 面向开发者的提示工程 Prompt Engineering，即是针对特定任务构造能充分发挥大模型能力的 Prompt 的技巧。\n本部分内容基于吴恩达老师的《Prompt Engineering for Developer》课程进行编写。\n简介Introduction 对于开发人员，大语言模型（LLM） 的更强大功能是能通过 API 接口调用，从而快速构建软件应用程序。\n随着 LLM 的发展，其大致可以分为两种类型，后续称为基础 LLM 和指令微调（Instruction Tuned）LLM。\n基础LLM是基于文本训练数据，训练出预测下一个单词能力的模型。其通常通过在互联网和其他来源的大量数据上训练，来确定紧接着出现的最可能的词。\n指令微调 LLM 通过专门的训练，可以更好地理解并遵循指令。\n指令微调 LLM 的训练通常基于预训练语言模型，先在大规模文本数据上进行预训练，掌握语言的基本规律。在此基础上进行进一步的训练与微调（finetune），输入是指令，输出是对这些指令的正确回复。有时还会采用**RLHF（reinforcement learning from human feedback，人类反馈强化学习）**技术，根据人类对模型输出的反馈进一步增强模型遵循指令的能力。通过这种受控的训练过程。指令微调 LLM 可以生成对指令高度敏感、更安全可靠的输出，较少无关和损害性内容。\n1.1 提示原则Guidelines 本章讨论了设计高效 Prompt 的两个关键原则：编写清晰、具体的指令和给予模型充足思考时间。\n原则一：编写清晰具体的指令 下面的几点要求，对于指令清晰则是从输入表示，输出结构，输出检查和简单示例四部分构成，比较经典的过程流。\n1 使用分隔符清晰的表示输入的不同部分。有利于防止提示词注入。\n2 寻求结构化输出，比如JSON，HTML等格式。\n3 要求模型检查是否满足条件，检查不满足则不输出。\n4 提供少量示例。即\u0026quot;Few-shot\u0026quot; prompting，在要求模型执行实际任务之前，给模型一两个已完成的样例，让模型了解我们的要求和期望的输出样式。\n原则二：给模型时间去思考 1 制定完成任务所需的步骤。而不是直接让他盲目的得到最终结果。\n2 指导模型在下结论之前找出自己的解法。因为即使你给的是错误解法，大模型也很容易认为这是正确的，从而导致误判。\n局限性 模型偶尔会生成一些看似真实实则编造的知识。\n语言模型生成虚假信息的“幻觉”问题，是使用与开发语言模型时需要高度关注的风险。由于幻觉信息往往令人无法辨别真伪，开发者必须警惕并尽量避免它的产生。\n1.2 迭代优化Iterative 在开发大语言模型应用时，很难通过第一次尝试就得到完美适用的 Prompt。但关键是要有一个良好的迭代优化过程，以不断改进 Prompt。相比训练机器学习模型，Prompt 的一次成功率可能更高，但仍需要通过多次迭代找到最适合应用的形式。\n1.3 文本概括Summarizing 单一文本概括 多文本概括 1.4 推断Inferring 情感推断 信息提取 主题推断 1.5 文本转换Transforming 文本翻译 预期和写作风格调整 文件格式转换 拼写以及语法纠正 1.6 文本扩展Expanding 温度系数：一般来说，如果需要可预测、可靠的输出，则将 temperature 设置为0，如果需要更具创造性的多样文本，那么适当提高 temperature 则很有帮助。调整这个参数可以灵活地控制语言模型的输出特性。 1.7 聊天机器人Chatbot 2 搭建基于ChatGPT的问答系统 2.1 基础概念 大型语言模型主要可以分为两类:基础语言模型和指令调优语言模型。\n基础语言模型（Base LLM）通过反复预测下一个词来训练的方式进行训练，没有明确的目标导向。 指令微调的语言模型（Instruction Tuned LLM）则进行了专门的训练，以便更好地理解问题并给出符合指令的回答。 LLM 实际上并不是重复预测下一个单词，而是重复预测下一个 token 。\n这种提问格式，我们可以明确地角色扮演，让语言模型理解自己就是助手这个角色，需要回答问题。这可以减少无效输出，帮助其生成针对性强的回复。\n2.2 电商客服 AI 系统简单框架 这个搭建问答系统更偏向于应用层面，可以大体理解一下整体逻辑，从数据集准备（评估输入的分类），到输入检查（检查输入数据是否合法），再到ai的思考（其中涉及的思维链和prompt链），最后输出（检查输出信息是否有效，是否有害）。只是去简单的实现一个调用，缺少的像这篇文章里面的Langchain到简单Agent - 江舟的博客 | Sutdown Blog比如RAG，Memory之类的并没有详细阐述。建议大致看看这部分即可，用于基本了解。\n评估输入 - 分类 类似描述的数据集，将各种信息分类完成，比如如果只是一个简单的商品检索助手，那么分类有助于在ai查找信息时更加快速查找到需要的数据。\n检查输入 - 监督 审核 prompt注入 审核的在于审核两点：1 是否输入一些不安全信息，比如违法信息\n思维链推理 思维链提示设计 内心独白 Prompt链 提取产品和类别 检索详细信息 生成查询答案 总结 检查结果 检查有害内容 检查是否符合产品信息 搭建一个带评估的端到端系统 端到端实现问答系统 对用户的输入进行检验，验证其是否可以通过审核 API 的标准。 若输入顺利通过审核，我们将进一步对产品目录进行搜索。 若产品搜索成功，我们将继续寻找相关的产品信息。 我们使用模型针对用户的问题进行回答。 最后，我们会使用审核 API 对生成的回答进行再次的检验。 持续收集用户和助手消息 评估 针对不同的测试用例进行测试，比较理想答案和输出答案比较评估xia0guo 注意回归测试：验证模型在以前的代码上的效果 运用gpt进行自行评估 完整代码 这个主要实现一个完整的电商客服AI框架，基于datawhalechina/llm-cookbook的原代码，进行了适当变化，精简逻辑，添加注释，将openAI appkey改成了阿里的通义千问，openAI国内不太好访问。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 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 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 import json import os from langchain_community.chat_models import ChatTongyi from langchain_core.messages import SystemMessage, HumanMessage, AIMessage from collections import defaultdict from dotenv import load_dotenv load_dotenv() DASHSCOPE_API_KEY = os.getenv(\u0026#34;DASHSCOPE_API_KEY\u0026#34;) llm = ChatTongyi(model=\u0026#34;qwen3-max\u0026#34;, api_key=DASHSCOPE_API_KEY) # 商品和目录的数据文件 PRODUCTS_FILE = \u0026#34;products.json\u0026#34; CATEGORIES_FILE = \u0026#34;categories.json\u0026#34; DELIMITER = \u0026#34;####\u0026#34; # -------------------------- 系统提示词（修正原重复类别问题）-------------------------- # 第二步（抽取商品）系统信息文本，校验不同类别，并返回一个列表，其中包含所有类别。 # 提取问题分布解决，此为思维链 step_2_system_message_content = f\u0026#34;\u0026#34;\u0026#34; 您将获得一次客户服务对话。最近的用户查询将使用{DELIMITER}字符进行分隔。 输出一个Python对象列表，其中每个对象具有以下格式： \u0026#39;category\u0026#39;: \u0026lt;包括以下几个类别：Computers and Laptops、Smartphones and Accessories、Televisions and Home Theater Systems、Gaming Consoles and Accessories、Audio Equipment、Cameras and Camcorders \u0026#39;products\u0026#39;: \u0026lt;必须是下面的允许产品列表中找到的产品\u0026gt; 类别和产品必须在客户服务查询中找到。 如果提到了产品，它必须与下面的允许产品列表中的正确类别相关联。 如果未找到任何产品或类别，请输出一个空列表。 只列出之前对话的早期部分未提及和讨论的产品和类别。 允许的产品： Computers and Laptops类别： TechPro Ultrabook BlueWave Gaming Laptop PowerLite Convertible TechPro Desktop BlueWave Chromebook Smartphones and Accessories类别： SmartX ProPhone MobiTech PowerCase SmartX MiniPhone MobiTech Wireless Charger SmartX EarBuds Televisions and Home Theater Systems类别： CineView 4K TV SoundMax Home Theater CineView 8K TV SoundMax Soundbar CineView OLED TV Gaming Consoles and Accessories类别： GameSphere X ProGamer Controller GameSphere Y ProGamer Racing Wheel GameSphere VR Headset Audio Equipment类别： AudioPhonic Noise-Canceling Headphones WaveSound Bluetooth Speaker AudioPhonic True Wireless Earbuds WaveSound Soundbar AudioPhonic Turntable Cameras and Camcorders类别： FotoSnap DSLR Camera ActionCam 4K FotoSnap Mirrorless Camera ZoomMaster Camcorder FotoSnap Instant Camera 只输出对象列表，不包含其他内容。 \u0026#34;\u0026#34;\u0026#34; # 第四步（生成用户回答）的系统信息，添加身份，进一步区分，可以理解成prompt chain step_4_system_message_content = f\u0026#34;\u0026#34;\u0026#34; 你是一家大型电子商店的客户服务助理。 以友好和乐于助人的语气回答，回答保持简洁明了。 确保让用户提出相关的后续问题。 \u0026#34;\u0026#34;\u0026#34; # 第六步（验证模型回答）的系统信息，重新根据数据校验结果 # 思维链的一部分，检查结果 step_6_system_message_content = f\u0026#34;\u0026#34;\u0026#34; 你是一个助手，评估客户服务代理的回答是否足够回答客户的问题，并验证回答中所有产品信息是否与提供的商品数据一致。 请基于以下三部分内容进行判断： 1. 用户的问题 2. 客服的回答 3. 商品数据集（包含所有产品的真实信息） 输出格式： Y - 回答足够且所有产品信息与数据集一致 N - 回答不足够，或存在与数据集不符的信息 只输出一个字母。 \u0026#34;\u0026#34;\u0026#34; # -------------------------- 模型调用函数（基于 ChatTongyi）-------------------------- def call_llm(messages): \u0026#34;\u0026#34;\u0026#34;统一的模型调用接口\u0026#34;\u0026#34;\u0026#34; try: response = llm.invoke(messages) return response.content.strip() except Exception as e: print(f\u0026#34;[模型错误] {e}\u0026#34;) return \u0026#34;\u0026#34; # --------------------------基础数据加载-------------------------- def load_json_file(path): \u0026#34;\u0026#34;\u0026#34;从文件读取 JSON 数据\u0026#34;\u0026#34;\u0026#34; if not os.path.exists(path): raise FileNotFoundError(f\u0026#34;未找到文件：{path}\u0026#34;) with open(path, \u0026#34;r\u0026#34;, encoding=\u0026#34;utf-8\u0026#34;) as f: return json.load(f) def load_products(): return load_json_file(PRODUCTS_FILE) def load_categories(): return load_json_file(CATEGORIES_FILE) # ---------------------- 功能函数 ---------------------- def extract_products_and_categories(user_msg): \u0026#34;\u0026#34;\u0026#34;调用模型识别用户提到的产品和类别\u0026#34;\u0026#34;\u0026#34; messages = [ SystemMessage(content=step_2_system_message_content), HumanMessage(content=f\u0026#34;{DELIMITER}{user_msg}{DELIMITER}\u0026#34;) ] return call_llm(messages) def read_string_to_list(json_like_str): \u0026#34;\u0026#34;\u0026#34;将模型输出转为 Python 对象\u0026#34;\u0026#34;\u0026#34; if not json_like_str: return [] try: fixed = json_like_str.replace(\u0026#34;\u0026#39;\u0026#34;, \u0026#39;\u0026#34;\u0026#39;) return json.loads(fixed) except json.JSONDecodeError: print(\u0026#34;[警告] 无法解析模型输出：\u0026#34;, json_like_str) return [] def generate_product_info(data_list, products): \u0026#34;\u0026#34;\u0026#34;根据识别结果提取产品详情\u0026#34;\u0026#34;\u0026#34; info_text = \u0026#34;\u0026#34; for item in data_list: for pname in item.get(\u0026#34;products\u0026#34;, []): if pname in products: info_text += json.dumps(products[pname], ensure_ascii=False, indent=2) + \u0026#34;\\n\u0026#34; return info_text.strip() def answer_user_question(user_msg, product_info): \u0026#34;\u0026#34;\u0026#34;生成客服回答\u0026#34;\u0026#34;\u0026#34; messages = [ SystemMessage(content=step_4_system_message_content), HumanMessage(content=f\u0026#34;用户问题：{user_msg}\\n\\n相关产品信息：\\n{product_info}\u0026#34;) ] return call_llm(messages) def validate_answer(user_msg, answer, products): \u0026#34;\u0026#34;\u0026#34;验证客服回答是否正确（传入商品数据作为参考）\u0026#34;\u0026#34;\u0026#34; # 将商品数据转为字符串，作为参考信息传入 products_str = json.dumps(products, ensure_ascii=False, indent=2) messages = [ SystemMessage(content=step_6_system_message_content), HumanMessage(content=f\u0026#34;\u0026#34;\u0026#34; 用户问题：{user_msg} 客服回答：{answer} 商品数据集：{products_str} \u0026#34;\u0026#34;\u0026#34;.strip()) ] return call_llm(messages) def main(): \u0026#34;\u0026#34;\u0026#34; 电商客服 AI 系统主流程 \u0026#34;\u0026#34;\u0026#34; print(\u0026#34;=== Step 1: 初始化商品与分类数据 ===\u0026#34;) products = load_products() print(\u0026#34;=== Step 2: 模型识别用户提到的商品和类别 ===\u0026#34;) user_msg = \u0026#34;你好，我想了解一下 SmartX ProPhone 的电池续航，以及 CineView 8K TV 有没有HDR功能？\u0026#34; print(f\u0026#34;用户消息：{user_msg}\u0026#34;) response = extract_products_and_categories(user_msg) print(f\u0026#34;模型识别结果（原始文本）：\\n{response}\u0026#34;) data_list = read_string_to_list(response) print(f\u0026#34;解析后结构：{data_list}\u0026#34;) product_info_str = generate_product_info(data_list, products) print(f\u0026#34;生成的产品信息：\\n{product_info_str}\u0026#34;) print(\u0026#34;\\n=== Step 3: 生成客服回答 ===\u0026#34;) answer = answer_user_question(user_msg, product_info_str) print(f\u0026#34;客服回答：\\n{answer}\u0026#34;) print(\u0026#34;\\n=== Step 4: 检查回答质量 ===\u0026#34;) validation = validate_answer(user_msg, answer, product_info_str) print(f\u0026#34;验证结果（Y=合格，N=不合格）：{validation}\u0026#34;) if __name__ == \u0026#34;__main__\u0026#34;: main() 基于之前有篇文章写过langchainLangchain到简单Agent - 江舟的博客 | Sutdown Blog，都是些很基础的用法，这里偏向深层次的语法，可以看看，后续再出个具体的应用代码写的agent，用langgraph尽量。\n3 基于LangChain开发应用程序 3.1 基础介绍 LangChain 是用于构建大模型应用程序的开源框架，有Python和JavaScript两个不同版本的包。LangChain 也是一个开源项目，社区活跃，新增功能快速迭代。LangChain基于模块化组合，有许多单独的组件，可以一起使用或单独使用。\nLangChain 的常用组件：\n模型(Models)：集成各种语言模型与向量模型。 提示(Prompts)：向模型提供指令的途径。 索引(Indexes)：提供数据检索功能。 链(Chains)：将组件组合实现端到端应用。 代理(Agents)：扩展模型的推理能力 3.2 组件 模板 在应用于比较复杂的场景时，提示可能会非常长并且包含涉及许多细节。使用提示模版，可以让我们更为方便地重复使用设计好的提示。\n当然对于特定的条件，也可以采用输出解释器提取用户评价中的信息。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 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 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 import os from langchain.output_parsers import ResponseSchema, StructuredOutputParser from langchain_community.chat_models import ChatTongyi from dotenv import load_dotenv from langchain_core.prompts import ChatPromptTemplate, SystemMessagePromptTemplate, HumanMessagePromptTemplate load_dotenv() DASHSCOPE_API_KEY = os.getenv(\u0026#34;DASHSCOPE_API_KEY\u0026#34;) llm = ChatTongyi(model=\u0026#34;qwen3-max\u0026#34;, api_key=DASHSCOPE_API_KEY) \u0026#34;\u0026#34;\u0026#34; prompt template 默认进行了embedding \u0026#34;\u0026#34;\u0026#34; customer_style_res = \u0026#34;\u0026#34;\u0026#34;正式普通话 \\ 用一个平静、尊敬、有礼貌的语调 \u0026#34;\u0026#34;\u0026#34; customer_res = \u0026#34;\u0026#34;\u0026#34; 嗯呐，我现在可是火冒三丈，我那个搅拌机盖子竟然飞了出去，把我厨房的墙壁都溅上了果汁！ 更糟糕的是，保修条款可不包括清理我厨房的费用。 伙计，赶紧给我过来！ \u0026#34;\u0026#34;\u0026#34; service_reply = \u0026#34;\u0026#34;\u0026#34;嘿，顾客， \\ 保修不包括厨房的清洁费用， \\ 因为您在启动搅拌机之前 \\ 忘记盖上盖子而误用搅拌机, \\ 这是您的错。 \\ 倒霉！ 再见！ \u0026#34;\u0026#34;\u0026#34; service_style_pirate = \u0026#34;\u0026#34;\u0026#34;\\ 一个有礼貌的语气 \\ 使用海盗风格\\ \u0026#34;\u0026#34;\u0026#34; customer_review = \u0026#34;\u0026#34;\u0026#34;\\ 这款吹叶机非常神奇。 它有四个设置：\\ 吹蜡烛、微风、风城、龙卷风。 \\ 两天后就到了，正好赶上我妻子的\\ 周年纪念礼物。 \\ 我想我的妻子会喜欢它到说不出话来。 \\ 到目前为止，我是唯一一个使用它的人，而且我一直\\ 每隔一天早上用它来清理草坪上的叶子。 \\ 它比其他吹叶机稍微贵一点，\\ 但我认为它的额外功能是值得的。 \u0026#34;\u0026#34;\u0026#34; review_template = \u0026#34;\u0026#34;\u0026#34;\\ 对于以下文本，请从中提取以下信息： 礼物：该商品是作为礼物送给别人的吗？ \\ 如果是，则回答 是的；如果否或未知，则回答 不是。 交货天数：产品需要多少天\\ 到达？ 如果没有找到该信息，则输出-1。 价钱：提取有关价值或价格的任何句子，\\ 并将它们输出为逗号分隔的 Python 列表。 使用以下键将输出格式化为 JSON： 礼物 交货天数 价钱 文本: {text} \u0026#34;\u0026#34;\u0026#34; review_template_2 = \u0026#34;\u0026#34;\u0026#34;\\ 对于以下文本，请从中提取以下信息：： 礼物：该商品是作为礼物送给别人的吗？ 如果是，则回答 是的；如果否或未知，则回答 不是。 交货天数：产品到达需要多少天？ 如果没有找到该信息，则输出-1。 价钱：提取有关价值或价格的任何句子，并将它们输出为逗号分隔的 Python 列表。 文本: {text} {format_instructions} \u0026#34;\u0026#34;\u0026#34; \u0026#34;\u0026#34;\u0026#34; 功能函数 \u0026#34;\u0026#34;\u0026#34; def translate_email(style, customer_email): human_prompt = HumanMessagePromptTemplate.from_template(\u0026#34;\u0026#34;\u0026#34; 请把由三个反引号分隔的文本翻译成一种{style}风格。 文本: ```{customer_email}``` \u0026#34;\u0026#34;\u0026#34;) system_prompt = SystemMessagePromptTemplate.from_template(\u0026#34;你是一个ai助手。\u0026#34;) prompt = ChatPromptTemplate.from_messages([system_prompt, human_prompt]) messages = prompt.format_messages(style=style, customer_email=customer_email) response = llm.invoke(messages) print(\u0026#34; - messages\\n\u0026#34;, response.content) def translate_review(text): human_prompt = HumanMessagePromptTemplate.from_template(review_template) system_prompt = SystemMessagePromptTemplate.from_template(\u0026#34;你是一个ai助手。\u0026#34;) prompt = ChatPromptTemplate.from_messages([system_prompt, human_prompt]) messages = prompt.format_messages(text=text) response = llm.invoke(messages) print(\u0026#34; - messages\\n\u0026#34;, response.content) def translate_review_2(text): prompt = ChatPromptTemplate.from_template(template=review_template_2) gift_schema = ResponseSchema(name=\u0026#34;礼物\u0026#34;, description=\u0026#34;这件物品是作为礼物送给别人的吗？\\ 如果是，则回答 是的，\\ 如果否或未知，则回答 不是。\u0026#34;) delivery_days_schema = ResponseSchema(name=\u0026#34;交货天数\u0026#34;, description=\u0026#34;产品需要多少天才能到达？\\ 如果没有找到该信息，则输出-1。\u0026#34;) price_value_schema = ResponseSchema(name=\u0026#34;价钱\u0026#34;, description=\u0026#34;提取有关价值或价格的任何句子，\\ 并将它们输出为逗号分隔的 Python 列表\u0026#34;) response_schemas = [gift_schema, delivery_days_schema, price_value_schema] output_parser = StructuredOutputParser.from_response_schemas(response_schemas) format_instructions = output_parser.get_format_instructions() messages = prompt.format_messages(text=text, format_instructions=format_instructions) response = llm.invoke(messages) print(\u0026#34; - messages\\n\u0026#34;, response.content) if __name__ == \u0026#34;__main__\u0026#34;: # translate_email(customer_style_res, customer_res) # translate_email(service_style_pirate, service_reply) # translate_review(customer_review) translate_review_2(customer_review) Memory 在 LangChain 中，储存指的是大语言模型（LLM）的短期记忆。当使用 LangChain 中的储存(Memory)模块时，它旨在保存、组织和跟踪整个对话的历史，从而为用户和模型之间的交互提供连续的上下文。\nLangChain 提供了多种储存类型。这些记忆组件都是模块化的，可与其他组件组合使用，从而增强机器人的对话管理能力。储存模块可以通过简单的 API 调用来访问和更新，允许开发人员更轻松地实现对话历史记录的管理和维护。\n缓冲区储存允许保留最近的聊天消息， 摘要储存则提供了对整个对话的摘要。 实体储存则允许在多轮对话中保留有关特定实体的信息。 类型 保存方式 优点 缺点 InMemoryChatMessageHistory 内存 简单，快速 会话结束丢失数据 ConversationBufferMemory 内存，链友好 可以直接在 chain 中使用 长对话 token 会增大 ConversationSummaryMemory 内存 + LLM摘要 节省 token，保持核心上下文 需要额外调用 LLM 自定义 Memory 任意 灵活 需要自己管理逻辑 单对话单用户，下面代码尝试了一下简单的角色扮演，能够记住上下文，角色扮演的语气场景都不错，看来千问的效果很好。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 llm = ChatTongyi(model=\u0026#34;qwen3-max\u0026#34;, api_key=DASHSCOPE_API_KEY) # 存储对话的地方（取代 ConversationBufferMemory） store = {} # 模拟 session 存储 def get_session_history(session_id: str): \u0026#34;\u0026#34;\u0026#34;根据 session_id 获取或创建消息历史\u0026#34;\u0026#34;\u0026#34; if session_id not in store: # InMemoryChatMessageHistory() 是一个用来保存对话消息历史的容器。 # 可以保存，用户说的话 和 模型回答 store[session_id] = InMemoryChatMessageHistory() return store[session_id] def runnable_func(inputs): history = inputs.get(\u0026#34;chat_history\u0026#34;, []) prompt = \u0026#34;\u0026#34; for msg in history: prompt += f\u0026#34;{msg.type}: {msg.content}\\n\u0026#34; prompt += f\u0026#34;user: {inputs[\u0026#39;input\u0026#39;]}\\n\u0026#34; return llm.invoke(prompt) # runablewithmeaasgehistory是一个通用组件，用于处理会话历史 runnable = RunnableLambda(runnable_func) chain = RunnableWithMessageHistory( runnable=runnable, get_session_history=get_session_history, input_messages_key=\u0026#34;input\u0026#34;, # 输入字段名 history_messages_key=\u0026#34;chat_history\u0026#34; # 历史字段名 ) # 指定一个 session_id（比如不同用户或会话） session_id = \u0026#34;user1\u0026#34; print(\u0026#34;🧠 开始多轮对话（输入 \u0026#39;exit\u0026#39; 退出）\u0026#34;) while True: user_input = input(\u0026#34;👤 你：\u0026#34;).strip() if user_input.lower() in {\u0026#34;exit\u0026#34;, \u0026#34;quit\u0026#34;}: print(\u0026#34;🚪 结束对话\u0026#34;) break response = chain.invoke( {\u0026#34;input\u0026#34;: user_input}, config={\u0026#34;configurable\u0026#34;: {\u0026#34;session_id\u0026#34;: session_id}} ) print(f\u0026#34;🤖 AI：{response.content}\\n\u0026#34;) 模型链 - Chain 链（Chains）通常将大语言模型（LLM）与提示（Prompt）结合在一起，基于此，我们可以对文本或数据进行一系列操作。链（Chains）可以一次性接受多个输入。\n简单顺序链 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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 llm = ChatTongyi(model=\u0026#34;qwen3-max\u0026#34;, api_key=DASHSCOPE_API_KEY) first_prompt = ChatPromptTemplate.from_template( \u0026#34;把下面的评论review翻译成英文:\u0026#34; \u0026#34;\\n\\n{Review}\u0026#34; ) # prompt模板 2: 用一句话总结下面的 review second_prompt = ChatPromptTemplate.from_template( \u0026#34;请你用一句话来总结下面的评论review:\u0026#34; \u0026#34;\\n\\n{English_Review}\u0026#34; ) # prompt模板 3: 下面review使用的什么语言 third_prompt = ChatPromptTemplate.from_template( \u0026#34;下面的评论review使用的什么语言:\\n\\n{Review}\u0026#34; ) # prompt模板 4: 使用特定的语言对下面的总结写一个后续回复 fourth_prompt = ChatPromptTemplate.from_template( \u0026#34;使用特定的语言对下面的总结写一个后续回复:\u0026#34; \u0026#34;\\n\\n总结: {summary}\\n\\n语言: {language}\u0026#34; ) # 将 overall_chain 修改为直接串联各个步骤 # 使用 RunnableMap 一次性处理所有步骤 overall_chain = RunnableMap({ \u0026#34;English_Review\u0026#34;: lambda x: (first_prompt | llm).invoke({\u0026#34;Review\u0026#34;: x[\u0026#34;Review\u0026#34;]}), \u0026#34;summary\u0026#34;: lambda x: (second_prompt | llm).invoke({ \u0026#34;English_Review\u0026#34;: (first_prompt | llm).invoke({\u0026#34;Review\u0026#34;: x[\u0026#34;Review\u0026#34;]}) }), \u0026#34;language\u0026#34;: lambda x: (third_prompt | llm).invoke({\u0026#34;Review\u0026#34;: x[\u0026#34;Review\u0026#34;]}), \u0026#34;followup_message\u0026#34;: lambda x: (fourth_prompt | llm).invoke({ \u0026#34;summary\u0026#34;: (second_prompt | llm).invoke({ \u0026#34;English_Review\u0026#34;: (first_prompt | llm).invoke({\u0026#34;Review\u0026#34;: x[\u0026#34;Review\u0026#34;]}) }), \u0026#34;language\u0026#34;: (third_prompt | llm).invoke({\u0026#34;Review\u0026#34;: x[\u0026#34;Review\u0026#34;]}) }) }) review_text = \u0026#34;部员们都很有个性——真田那家伙，严肃得像块石头，但其实比谁都可靠；柳生总是冷静分析，却会在切原胡闹时默默帮他收拾烂摊子；仁王那狐狸，总爱捉弄人，可关键时刻从不含糊；还有丸井和桑原，一个爱吹泡泡糖，一个沉默却温柔，他们之间的默契谁都比不上。\u0026#34; result = overall_chain.invoke({\u0026#34;Review\u0026#34;: review_text}) print(\u0026#34;英文评论:\u0026#34;, result[\u0026#34;English_Review\u0026#34;].content) print(\u0026#34;评论总结:\u0026#34;, result[\u0026#34;summary\u0026#34;].content) print(\u0026#34;评论语言:\u0026#34;, result[\u0026#34;language\u0026#34;].content) print(\u0026#34;后续回复:\u0026#34;, result[\u0026#34;followup_message\u0026#34;].content) 路由链 如果你有多个子链，每个子链都专门用于特定类型的输入，那么可以组成一个路由链，它首先决定将它传递给哪个子链，然后将它传递给那个链。\n路由器由两个组件组成：\n路由链（Router Chain）：路由器链本身，负责选择要调用的下一个链 destination_chains：路由器链可以路由到的链 基于文档的问答 评估 - Evaluation 代理 - Agent 4 使用Langchain访问个人数据 4.1 基础介绍 4.2 组件 文档加载 文档分割 向量数据库与词向量 检索 问答 聊天 4.3 总结 ","date":"2025-11-01T00:00:00Z","permalink":"https://sutdown.github.io/p/llm_cookbook-%E9%9D%A2%E5%90%91%E5%BC%80%E5%8F%91%E8%80%85%E7%9A%84%E5%A4%A7%E6%A8%A1%E5%9E%8B%E5%85%A5%E9%97%A8%E6%95%99%E7%A8%8B/","title":"LLM_cookbook 面向开发者的大模型入门教程"},{"content":"0 前言 这篇主要作用是学习笔记. 大部分都是来源于该项目datawhalechina/happy-llm: 从零开始的大语言模型原理与实践教程.\n该篇仅包括上述项目前四章的部分，主概念。 前四章主要讲述ai的发展, 从最初的transformer架构，到预训练语言模型,，到如今的大语言模型。后三章则侧重于实现，比如如何搭建一个大模型，模型如何训练微调，大模型的评测，大模型Agent等实际应用的发展。后三章目前搁置，等看完另一篇llm入门之后再返回看看。\n前三章从比较通俗的角度介绍了相关概念，同时有部分代码和案例辅以说明，适合对ai了解不多的同学。transformer架构分为 embedding，位置编码，编码器和解码器四个部分。\n编码器和解码器内部由前馈神经网络和注意力机制组成。\nembedding用于转化自然语言； 位置编码在于确认各个token的相对位置； 编码器的倾向在于理解输入序列，解码器的倾向在于根据编码器的输出和上下文如何给出正确的结果，因此在现代数据量过多的情况下为了提升效率简化结构，常见的比如gpt，GLM等其实都输入only-decoder结构； 编码器和解码器核心都是注意力机制和前馈神经网络，差别在于用的具体分类不同，比如编码器常用自注意力机制训练，解码器则用掩码注意力训练。 前馈神经网络是最基本的神经网络结构，数据从输入层到输出层中经过若干隐藏层，信息沿着一个方向传播，保证输入维度不变的情况下对输入特征进行非线性变化，从而提取更抽象的特征表示；注意力机制的核心在于比较两个序列中元素的相关度，基于相关度进行加权分配注意力，上文提到的掩码也就是遮掩数据的部分进行训练。 另外还有两个是层归一化和残差连接。这是由于多层网络在训练时会产生损失，归一化核心是为了让不同层输入的取值范围或者分布能够比较一致。残差连接，即下一层的输入不仅是上一层的输出，还包括上一层的输入。 预训练语言模型中则是着重于 1 介绍数据集的作用，随着模型发展，越多的数据集有助于提示模型效果，起到量变导致的质变；2 预训练的方法，比如MLM掩码语言模型适用于文本理解，CLM自回归语言模型适用于文本生成，也有指令微调（区分任务或者区分文本），一些更细节的方案比如如何embedding和encoder拆开提升效率，多层神经网络设置共享变量减少内存等\n最后就是介绍了llm的三个阶段，预训练，监督微调sft，强化学习和人类反馈RLHF。简单理解就是\n预训练在于提高整体的数据量期望达到量变引起质变的想过， 监督微调则是让模型从多种类型，多种风格的指令中获取泛化的指令遵循能力，也就是能够更好更准确的理解和回复用户的指令。同时模型的多轮对话能力也是由该层控制。 人类反馈强化学习则是更深层次的让llm和人类价值观对其，不仅是输出回答，还要能输出更符合用户视角，用户需要的满意回答。主要由奖励模型和近端策略优化算法实现，后者属于经典的强化学习算法。 后三章其实更偏实践，且待我填坑。\n1 基础概念 以 GPT、BERT 为代表的 PLM 是上一阶段 NLP 领域的核心研究成果，以注意力机制为模型架构，通过预训练-微调的阶段思想通过在海量无监督文本上进行自监督预训练，实现了强大的自然语言理解能力。\nLLM 是在 PLM 的基础上，通过大量扩大模型参数、预训练数据规模，并引入指令微调、人类反馈强化学习等手段实现的突破性成果。相较于传统 PLM，LLM 具备涌现能力，具有强大的上下文学习能力、指令理解能力和文本生成能力。\n自然语言处理：\n模型：Word2Vec模型，Bert模型 任务：中文分词，子词切分，词性标注，文本分类，实体识别，关系抽取，文本摘要，机器翻译，自动问答， N-gram 模型： NLP 领域中一种基于统计的语言模型，广泛应用于语音识别、手写识别、拼写纠错、机器翻译和搜索引擎等众多任务。N-gram模型的核心思想是基于马尔可夫假设，即一个词的出现概率仅依赖于它前面的N-1个词。\nWord2Vec：一种流行的词嵌入（Word Embedding）技术，由Tomas Mikolov等人在2013年提出。它是一种基于神经网络NNLM的语言模型，旨在通过学习词与词之间的上下文关系来生成词的密集向量表示。Word2Vec的核心思想是利用词在文本中的上下文信息来捕捉词之间的语义关系，从而使得语义相似或相关的词在向量空间中距离较近。\nELMo：（Embeddings from Language Models）实现了一词多义、静态词向量到动态词向量的跨越式转变。首先在大型语料库上训练语言模型，得到词向量模型，然后在特定任务上对模型进行微调，得到更适合该任务的词向量，ELMo首次将预训练思想引入到词向量的生成中，使用双向LSTM结构，能够捕捉到词汇的上下文信息，生成更加丰富和准确的词向量表示。\n2 Transformer架构 注意力机制 神经网络的核心架构 FNN（前馈神经网络）是最基本的神经网络结构，信息从输入层到输出层单向传递，常用于结构化数据建模，如分类和回归任务。 CNN（卷积神经网络）通过卷积和池化操作自动提取局部特征，特别适合处理图像、视频等具有空间结构的数据，在计算机视觉领域应用最广。 RNN（循环神经网络）通过循环结构保留时间依赖信息，适合处理序列数据，如自然语言、语音识别和时间序列预测。 在注意力机制横空出世之前，RNN 以及 RNN 的衍生架构 LSTM 是 NLP 领域当之无愧的霸主。\n理解注意力机制 注意力机制有三个核心变量：查询值 Query，键值 Key 和 真值 Value。\n注意力机制的本质是对两段序列的元素依次进行相似度计算，寻找出一个序列的每个元素对另一个序列的每个元素的相关度，然后基于相关度进行加权，即分配注意力。 $$ attention(Q,K,V) = softmax(\\frac{QK^T}{\\sqrt{d_k}})V $$ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 \u0026#39;\u0026#39;\u0026#39;注意力计算函数\u0026#39;\u0026#39;\u0026#39; def attention(query, key, value, dropout=None): \u0026#39;\u0026#39;\u0026#39; args: query: 查询值矩阵 key: 键值矩阵 value: 真值矩阵 \u0026#39;\u0026#39;\u0026#39; # 获取键向量的维度，键向量的维度和值向量的维度相同 d_k = query.size(-1) # 计算Q与K的内积并除以根号dk，transpose——相当于转置 scores = torch.matmul(query, key.transpose(-2, -1)) / math.sqrt(d_k) # Softmax 将分数归一化为概率分布，使所有注意力权重之和为 1。 p_attn = scores.softmax(dim=-1) # Dropout 是一种正则化手段，用来随机丢弃部分注意力权重 if dropout is not None: p_attn = dropout(p_attn) # 根据计算结果对value进行加权求和 return torch.matmul(p_attn, value), p_attn 自注意力，掩码自注意力，多头注意力 自注意力，即是计算本身序列中每个元素对其他元素的注意力分布，即在计算过程中，Q、K、V 都由同一个输入通过不同的参数矩阵计算得到。\n掩码自注意力，即 Mask Self-Attention，是指使用注意力掩码的自注意力机制。掩码的作用是遮蔽一些特定位置的 token，模型在学习的过程中，会忽略掉被遮蔽的 token。使用注意力掩码的核心动机是让模型只能使用历史信息进行预测而不能看到未来信息。\n1 2 3 4 5 6 7 8 9 # 创建一个上三角矩阵，用于遮蔽未来信息。 # 先通过 full 函数创建一个 1 * seq_len * seq_len 的矩阵 mask = torch.full((1, args.max_seq_len, args.max_seq_len), float(\u0026#34;-inf\u0026#34;)) # triu 函数的功能是创建一个上三角矩阵 mask = torch.triu(mask, diagonal=1) # 此处的 scores 为计算得到的注意力分数，mask 为上文生成的掩码矩阵 scores = scores + mask[:, :seqlen, :seqlen] scores = F.softmax(scores.float(), dim=-1).type_as(xq) 多头注意力机制（Multi-Head Attention），即同时对一个语料进行多次注意力计算，每次注意力计算都能拟合不同的关系，将最后的多次结果拼接起来作为最后的输出，即可更全面深入地拟合语言信息。\nEncoder-Decoder 在 Transformer 中，使用注意力机制的是其两个核心组件——Encoder（编码器）和 Decoder（解码器）。事实上，后续基于 Transformer 架构而来的预训练语言模型基本都是对 Encoder-Decoder 部分进行改进来构建新的模型架构，例如只使用 Encoder 的 BERT、只使用 Decoder 的 GPT 等。\nEncoder 和 Decoder 内部传统神经网络的经典结构为——前馈神经网络（FNN）、层归一化（Layer Norm）和残差连接（Residual Connection），然后进一步分析 Encoder 和 Decoder 的内部结构。\nSeq2seq模型 Seq2Seq，即序列到序列，是一种经典 NLP 任务。对于 Seq2Seq 任务，一般的思路是对自然语言序列进行编码再解码。具体而言，是指模型输入的是一个自然语言序列， $$ input = (x_1, x_2, x_3...x_n) $$ 输出的是一个可能不等长的自然语言序列 。 $$ output = (y_1, y_2, y_3...y_m) $$ 事实上，Seq2Seq 是 NLP 最经典的任务，几乎所有的 NLP 任务都可以视为 Seq2Seq 任务。例如文本分类任务，可以视为输出长度为 1 的目标序列（如在上式中 m = 1）；词性标注任务，可以视为输出与输入序列等长的目标序列（如在上式中 m = n）。\n前馈神经网络 FNN 是最基础的神经网络结构，数据从输入层经过若干隐藏层，最后到达输出层，信息只沿一个方向传播，不会回传或循环。\n在保持输入维度不变的情况下，对输入特征进行非线性变换和重新组合，从而提取更丰富、更抽象的特征表示。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 class MLP(nn.Module): \u0026#39;\u0026#39;\u0026#39;前馈神经网络\u0026#39;\u0026#39;\u0026#39; def __init__(self, dim: int, hidden_dim: int, dropout: float): super().__init__() # 定义第一层线性变换，从输入维度到隐藏维度 self.w1 = nn.Linear(dim, hidden_dim, bias=False) # 定义第二层线性变换，从隐藏维度到输入维度 self.w2 = nn.Linear(hidden_dim, dim, bias=False) # 定义dropout层，用于防止过拟合 self.dropout = nn.Dropout(dropout) def forward(self, x): # 前向传播函数 # 首先，输入x通过第一层线性变换和RELU激活函数 # 最后，通过第二层线性变换和dropout层 return self.dropout(self.w2(F.relu(self.w1(x)))) \u0026#34;\u0026#34;\u0026#34; 输入 x → 线性层 (dim → hidden_dim) # 把输入特征从 dim 维映射到更高维的 hidden_dim 空间。 → ReLU 激活\t# 给网络引入非线性能力，让模型能学习复杂函数关系，而不仅仅是线性映射。 → 线性层 (hidden_dim → dim) # 再做一次线性变换，把高维特征重新压缩回原始维度 dim。 → Dropout\t# 在训练时随机“丢弃”一部分神经元（置为 0），防止模型对特定神经元依赖过强，从而提升泛化能力。 → 输出 y \u0026#34;\u0026#34;\u0026#34; 层归一化 归一化核心是为了让不同层输入的取值范围或者分布能够比较一致。\n相较于 Batch Norm 在每一层统计所有样本的均值和方差，Layer Norm 在每个样本上计算其所有层的均值和方差，从而使每个样本的分布达到稳定。 $$ \\widetilde{Z_j} = \\frac{Z_j - \\mu_j}{\\sqrt{\\sigma^2 + \\epsilon}} $$残差连接 残差连接，即下一层的输入不仅是上一层的输出，还包括上一层的输入。\n例如，在 Encoder 中，在第一个子层，输入进入多头自注意力层的同时会直接传递到该层的输出，然后该层的输出会与原输入相加，再进行标准化。在第二个子层也是一样。即：\n$$ x = x + MultiHeadSelfAttention(LayerNorm(x)) $$$$ output = x + FNN(LayerNorm(x)) $$Encoder, Decoder Transformer 的 Encoder。Encoder 由 N 个 Encoder Layer 组成，每一个 Encoder Layer 包括一个注意力层和一个前馈神经网络。\nDecoder 由两个注意力层和一个前馈神经网络组成。第一个注意力层是一个掩码自注意力层，即使用 Mask 的注意力计算，保证每一个 token 只能使用该 token 之前的注意力分数；第二个注意力层是一个多头注意力层，该层将使用第一个注意力层的输出作为 query，使用 Encoder 的输出作为 key 和 value，来计算注意力分数。最后，再经过前馈神经网络.\n类型 信息可见性 特点 常用场景 注意力层 (Self-Attention) 每个 token 可见整个序列 捕捉全局依赖 Transformer 编码器 掩码注意力层 (Masked Self-Attention) 只能看见前面 token，屏蔽未来 保证自回归生成 Transformer 解码器 / GPT 多头注意力层 (Multi-Head Attention) 并行多个注意力头，每头看不同特征子空间 增强模型表达能力 所有 Transformer 注意力模块 Transformer构建 Embedding - 分词 将 Encoder、Decoder 拼接起来再加入 Embedding 层就可以搭建出完整的 Transformer 模型。\nEmbedding 层需要将自然语言的输入转化为机器可以处理的向量.\nEmbedding 层其实是一个存储固定大小的词典的嵌入向量查找表。也就是说，在输入神经网络之前，我们往往会先让自然语言输入通过分词器 tokenizer，分词器的作用是把自然语言输入切分成 token 并转化成一个固定的 index。\n1 self.tok_embeddings = nn.Embedding(args.vocab_size, args.dim) 位置编码 位置编码，即根据序列中 token 的相对位置对其进行编码，再将位置编码加入词向量编码中。位置编码的方式有很多，Transformer 使用了正余弦函数来进行位置编码（绝对位置编码Sinusoidal），其编码方式为：\n$$ PE(pos, 2i) = sin(pos/10000^{2i/d_{model}})\\\\ PE(pos, 2i+1) = cos(pos/10000^{2i/d_{model}}) $$上式中，pos 为 token 在句子中的位置，2i 和 2i+1 则是指示了 token 是奇数位置还是偶数位置，从上式中我们可以看出对于奇数位置的 token 和偶数位置的 token，Transformer 采用了不同的函数进行编码。\ntransformer code 整体流程:\n输入文本 → Token Embedding → Positional Encoding → Encoder → Decoder → 输出层（lm_head）\n加载中文模型对词进行预处理；\n对每个 token 进行词向量嵌入（Embedding）\u0026mdash; 将每个token ID映射成一个高维向量；\n加入位置编码（Positional Encoding）\u0026mdash; 增加序列顺序感；\n输入 Encoder（提取上下文特征）；\n前馈网络：一般由两层线性+激活函数，增强模型表达能力\n多层自注意力：理解词和词之间的关系\nDecoder 根据 Encoder 输出 + 已生成的词预测下一个词；\n输出层（线性+Softmax）将结果映射为词表概率。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 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 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 import torch import math from torch import nn from dataclasses import dataclass from transformers import BertTokenizer import torch.nn.functional as F @dataclass class ModelArgs: n_embd: int # 嵌入维度 n_heads: int # 头数 dim: int # 模型维度 dropout: float max_seq_len: int vocab_size: int block_size: int n_layer: int \u0026#34;\u0026#34;\u0026#34; 核心注意力机制 \u0026#34;\u0026#34;\u0026#34; class MultiHeadAttention(nn.Module): def __init__(self, args: ModelArgs, is_causal=False): # 构造函数 # args: 配置对象 super().__init__() # 隐藏层维度必须是头数的整数倍，因为后面我们会将输入拆成头数个矩阵 assert args.dim % args.n_heads == 0 # 每个头的维度，等于模型维度除以头的总数。 self.head_dim = args.dim // args.n_heads self.n_heads = args.n_heads # Wq, Wk, Wv 参数矩阵，每个参数矩阵为 n_embd x dim # 这里通过三个组合矩阵来代替了n个参数矩阵的组合，其逻辑在于矩阵内积再拼接其实等同于拼接矩阵再内积， # 不理解的读者可以自行模拟一下，每一个线性层其实相当于n个参数矩阵的拼接 self.wq = nn.Linear(args.n_embd, self.n_heads * self.head_dim, bias=False) self.wk = nn.Linear(args.n_embd, self.n_heads * self.head_dim, bias=False) self.wv = nn.Linear(args.n_embd, self.n_heads * self.head_dim, bias=False) # 输出权重矩阵，维度为 dim x dim（head_dim = dim / n_heads） self.wo = nn.Linear(self.n_heads * self.head_dim, args.dim, bias=False) # 注意力的 dropout self.attn_dropout = nn.Dropout(args.dropout) # 残差连接的 dropout self.resid_dropout = nn.Dropout(args.dropout) self.is_causal = is_causal # 创建一个上三角矩阵，用于遮蔽未来信息 # 注意，因为是多头注意力，Mask 矩阵比之前我们定义的多一个维度 if is_causal: mask = torch.full((1, 1, args.max_seq_len, args.max_seq_len), float(\u0026#34;-inf\u0026#34;)) mask = torch.triu(mask, diagonal=1) # 注册为模型的缓冲区 self.register_buffer(\u0026#34;mask\u0026#34;, mask) def forward(self, q: torch.Tensor, k: torch.Tensor, v: torch.Tensor): # 获取批次大小和序列长度，[batch_size, seq_len, dim] bsz, seqlen, _ = q.shape # 计算查询（Q）、键（K）、值（V）,输入通过参数矩阵层，维度为 (B, T, n_embed) x (n_embed, dim) -\u0026gt; (B, T, dim) xq, xk, xv = self.wq(q), self.wk(k), self.wv(v) # 将 Q、K、V 拆分成多头，维度为 (B, T, n_head, dim // n_head)，然后交换维度，变成 (B, n_head, T, dim // n_head) # 因为在注意力计算中我们是取了后两个维度参与计算 # 为什么要先按B*T*n_head*C//n_head展开再互换1、2维度而不是直接按注意力输入展开，是因为view的展开方式是直接把输入全部排开， # 然后按要求构造，可以发现只有上述操作能够实现我们将每个头对应部分取出来的目标 xq = xq.view(bsz, seqlen, self.n_heads, self.head_dim) xk = xk.view(bsz, seqlen, self.n_heads, self.head_dim) xv = xv.view(bsz, seqlen, self.n_heads, self.head_dim) xq = xq.transpose(1, 2) xk = xk.transpose(1, 2) xv = xv.transpose(1, 2) # 注意力计算 # 计算 QK^T / sqrt(d_k)，维度为 (B, nh, T, hs) x (B, nh, hs, T) -\u0026gt; (B, nh, T, T) scores = torch.matmul(xq, xk.transpose(2, 3)) / math.sqrt(self.head_dim) # 掩码自注意力必须有注意力掩码 if self.is_causal: assert hasattr(self, \u0026#39;mask\u0026#39;) # 这里截取到序列长度，因为有些序列可能比 max_seq_len 短 scores = scores + self.mask[:, :, :seqlen, :seqlen] # 计算 softmax，维度为 (B, nh, T, T) scores = F.softmax(scores.float(), dim=-1).type_as(xq) # 做 Dropout scores = self.attn_dropout(scores) # V * Score，维度为(B, nh, T, T) x (B, nh, T, hs) -\u0026gt; (B, nh, T, hs) output = torch.matmul(scores, xv) # 恢复时间维度并合并头。 # 将多头的结果拼接起来, 先交换维度为 (B, T, n_head, dim // n_head)，再拼接成 (B, T, n_head * dim // n_head) # contiguous 函数用于重新开辟一块新内存存储，因为Pytorch设置先transpose再view会报错， # 因为view直接基于底层存储得到，然而transpose并不会改变底层存储，因此需要额外存储 output = output.transpose(1, 2).contiguous().view(bsz, seqlen, -1) # 最终投影回残差流。 output = self.wo(output) output = self.resid_dropout(output) return output class LayerNorm(nn.Module): \u0026#39;\u0026#39;\u0026#39; Layer Norm 层\u0026#39;\u0026#39;\u0026#39; def __init__(self, features, eps=1e-6): super().__init__() # 线性矩阵做映射 self.a_2 = nn.Parameter(torch.ones(features)) self.b_2 = nn.Parameter(torch.zeros(features)) self.eps = eps def forward(self, x): # 在统计每个样本所有维度的值，求均值和方差 mean = x.mean(-1, keepdim=True) # mean: [bsz, max_len, 1] std = x.std(-1, keepdim=True) # std: [bsz, max_len, 1] # 注意这里也在最后一个维度发生了广播 return self.a_2 * (x - mean) / (std + self.eps) + self.b_2 class MLP(nn.Module): \u0026#39;\u0026#39;\u0026#39;前馈神经网络\u0026#39;\u0026#39;\u0026#39; def __init__(self, dim: int, hidden_dim: int, dropout: float): super().__init__() # 定义第一层线性变换，从输入维度到隐藏维度 self.w1 = nn.Linear(dim, hidden_dim, bias=False) # 定义第二层线性变换，从隐藏维度到输入维度 self.w2 = nn.Linear(hidden_dim, dim, bias=False) # 定义dropout层，用于防止过拟合 self.dropout = nn.Dropout(dropout) def forward(self, x): # 前向传播函数 # 首先，输入x通过第一层线性变换和RELU激活函数 # 最后，通过第二层线性变换和dropout层 return self.dropout(self.w2(F.relu(self.w1(x)))) class EncoderLayer(nn.Module): def __init__(self, args): super().__init__() # 一个 Layer 中有两个 LayerNorm，分别在 Attention 之前和 MLP 之前 self.attention_norm = LayerNorm(args.n_embd) # Encoder 不需要掩码，传入 is_causal=False self.attention = MultiHeadAttention(args, is_causal=False) self.fnn_norm = LayerNorm(args.n_embd) self.feed_forward = MLP(args.dim, args.dim, args.dropout) def forward(self, x): # Layer Norm x = self.attention_norm(x) # 自注意力 h = x + self.attention.forward(x, x, x) # 经过前馈神经网络 out = h + self.feed_forward.forward(self.fnn_norm(h)) return out class Encoder(nn.Module): \u0026#39;\u0026#39;\u0026#39;Encoder 块\u0026#39;\u0026#39;\u0026#39; def __init__(self, args): super(Encoder, self).__init__() # 一个 Encoder 由 N 个 Encoder Layer 组成 self.layers = nn.ModuleList([EncoderLayer(args) for _ in range(args.n_layer)]) self.norm = LayerNorm(args.n_embd) def forward(self, x): \u0026#34;分别通过 N 层 Encoder Layer\u0026#34; for layer in self.layers: x = layer(x) return self.norm(x) class DecoderLayer(nn.Module): \u0026#39;\u0026#39;\u0026#39;Decoder 层\u0026#39;\u0026#39;\u0026#39; def __init__(self, args): super().__init__() # 一个 Layer 中有三个 LayerNorm，分别在 Mask Attention 之前、Self Attention 之前和 MLP 之前 self.attention_norm_1 = LayerNorm(args.n_embd) # Decoder 的第一个部分是 Mask Attention，传入 is_causal=True self.mask_attention = MultiHeadAttention(args, is_causal=True) self.attention_norm_2 = LayerNorm(args.n_embd) # Decoder 的第二个部分是 类似于 Encoder 的 Attention，传入 is_causal=False self.attention = MultiHeadAttention(args, is_causal=False) self.ffn_norm = LayerNorm(args.n_embd) # 第三个部分是 MLP self.feed_forward = MLP(args.dim, args.dim, args.dropout) def forward(self, x, enc_out): # Layer Norm x = self.attention_norm_1(x) # 掩码自注意力 x = x + self.mask_attention.forward(x, x, x) # 多头注意力 x = self.attention_norm_2(x) h = x + self.attention.forward(x, enc_out, enc_out) # 经过前馈神经网络 out = h + self.feed_forward.forward(self.ffn_norm(h)) return out class Decoder(nn.Module): \u0026#39;\u0026#39;\u0026#39;解码器\u0026#39;\u0026#39;\u0026#39; def __init__(self, args): super(Decoder, self).__init__() # 一个 Decoder 由 N 个 Decoder Layer 组成 self.layers = nn.ModuleList([DecoderLayer(args) for _ in range(args.n_layer)]) self.norm = LayerNorm(args.n_embd) def forward(self, x, enc_out): \u0026#34;Pass the input (and mask) through each layer in turn.\u0026#34; for layer in self.layers: x = layer(x, enc_out) return self.norm(x) class PositionalEncoding(nn.Module): \u0026#39;\u0026#39;\u0026#39;位置编码模块\u0026#39;\u0026#39;\u0026#39; def __init__(self, args): super(PositionalEncoding, self).__init__() # Dropout 层 # self.dropout = nn.Dropout(p=args.dropout) # block size 是序列的最大长度 pe = torch.zeros(args.block_size, args.n_embd) position = torch.arange(0, args.block_size).unsqueeze(1) # 计算 theta div_term = torch.exp( torch.arange(0, args.n_embd, 2) * -(math.log(10000.0) / args.n_embd) ) # 分别计算 sin、cos 结果 pe[:, 0::2] = torch.sin(position * div_term) pe[:, 1::2] = torch.cos(position * div_term) pe = pe.unsqueeze(0) self.register_buffer(\u0026#34;pe\u0026#34;, pe) def forward(self, x): # 将位置编码加到 Embedding 结果上 x = x + self.pe[:, : x.size(1)].requires_grad_(False) return x class Transformer(nn.Module): \u0026#39;\u0026#39;\u0026#39;整体模型\u0026#39;\u0026#39;\u0026#39; def __init__(self, args): super().__init__() # 必须输入词表大小和 block size assert args.vocab_size is not None assert args.block_size is not None self.args = args self.transformer = nn.ModuleDict(dict( wte=nn.Embedding(args.vocab_size, args.n_embd), # 词嵌入 wpe=PositionalEncoding(args), # 位置编码 drop=nn.Dropout(args.dropout), # dropout encoder=Encoder(args), # 编码器 decoder=Decoder(args), # 解码器 )) # 最后的线性层，输入是 n_embd，输出是词表大小 self.lm_head = nn.Linear(args.n_embd, args.vocab_size, bias=False) # 初始化所有的权重 self.apply(self._init_weights) # 查看所有参数的数量 print(\u0026#34;number of parameters: %.2fM\u0026#34; % (self.get_num_params() / 1e6,)) \u0026#39;\u0026#39;\u0026#39;统计所有参数的数量\u0026#39;\u0026#39;\u0026#39; def get_num_params(self, non_embedding=False): # non_embedding: 是否统计 embedding 的参数 n_params = sum(p.numel() for p in self.parameters()) # 如果不统计 embedding 的参数，就减去 if non_embedding: n_params -= self.transformer.wte.weight.numel() return n_params \u0026#39;\u0026#39;\u0026#39;初始化权重\u0026#39;\u0026#39;\u0026#39; def _init_weights(self, module): # 线性层和 Embedding 层初始化为正则分布 if isinstance(module, nn.Linear): torch.nn.init.normal_(module.weight, mean=0.0, std=0.02) if module.bias is not None: torch.nn.init.zeros_(module.bias) elif isinstance(module, nn.Embedding): torch.nn.init.normal_(module.weight, mean=0.0, std=0.02) \u0026#39;\u0026#39;\u0026#39;前向计算函数\u0026#39;\u0026#39;\u0026#39; def forward(self, idx, targets=None): # 输入为 idx，维度为 (batch size, sequence length, 1)；targets 为目标序列，用于计算 loss device = idx.device b, t = idx.size() assert t \u0026lt;= self.args.block_size, f\u0026#34;不能计算该序列，该序列长度为 {t}, 最大序列长度只有 {self.args.block_size}\u0026#34; # 通过 self.transformer # 首先将输入 idx 通过 Embedding 层，得到维度为 (batch size, sequence length, n_embd) print(\u0026#34;idx：\u0026#34;, idx.size()) # 通过 Embedding 层 tok_emb = self.transformer.wte(idx) print(\u0026#34;tok_emb：\u0026#34;, tok_emb.size()) # 然后通过位置编码 pos_emb = self.transformer.wpe(tok_emb) # 再进行 Dropout x = self.transformer.drop(pos_emb) # 然后通过 Encoder print(\u0026#34;x after wpe:\u0026#34;, x.size()) enc_out = self.transformer.encoder(x) print(\u0026#34;enc_out:\u0026#34;, enc_out.size()) # 再通过 Decoder x = self.transformer.decoder(x, enc_out) print(\u0026#34;x after decoder:\u0026#34;, x.size()) if targets is not None: # 训练阶段，如果我们给了 targets，就计算 loss # 先通过最后的 Linear 层，得到维度为 (batch size, sequence length, vocab size) logits = self.lm_head(x) # 再跟 targets 计算交叉熵 loss = F.cross_entropy(logits.view(-1, logits.size(-1)), targets.view(-1), ignore_index=-1) else: # 推理阶段，我们只需要 logits，loss 为 None # 取 -1 是只取序列中的最后一个作为输出 logits = self.lm_head(x[:, [-1], :]) # note: using list [-1] to preserve the time dim loss = None return logits, loss def main(): args = ModelArgs(100, 10, 100, 0.1, 512, 1000, 1000, 2) text = \u0026#34;我喜欢快乐地学习大模型\u0026#34; # 加载中文模型，进行预处理 tokenizer = BertTokenizer.from_pretrained(\u0026#39;bert-base-chinese\u0026#39;) # inputs_tokens 为字典，包含inputs_ids(词token id映射序列)和attention_mask inputs_token = tokenizer( text, return_tensors=\u0026#39;pt\u0026#39;, max_length=args.max_seq_len, truncation=True, padding=\u0026#39;max_length\u0026#39; ) # 将词表大小写入模型参数中 args.vocab_size = tokenizer.vocab_size # 创建模型 transformer = Transformer(args) inputs_id = inputs_token[\u0026#39;input_ids\u0026#39;] \u0026#34;\u0026#34;\u0026#34; 输入 token → 嵌入层 (Embedding) --- 将每个token ID映射成一个高维向量 → 位置编码 (Positional Encoding) --- 增加序列顺序感 → 多层自注意力 + 前馈网络 (Transformer Blocks) --- 多层自注意力：理解词和词之间的关系 --- 前馈网络：一般由两层线性+激活函数，增强模型表达能力 → 线性层映射到 vocab_size 维度 --- 映射回词表维度 → 输出 logits \u0026#34;\u0026#34;\u0026#34; logits, loss = transformer.forward(inputs_id) print(\u0026#34;logits: \u0026#34;, logits) # 取预测结果，解码 predicted_ids = torch.argmax(logits, dim=-1).item() output = tokenizer.decode(predicted_ids) print(\u0026#34;outputs: \u0026#34;, output) if __name__ == \u0026#34;__main__\u0026#34;: print(\u0026#34;开始\u0026#34;) main() 3 预训练语言模型 “PLM” 是 “Pre-trained Language Model” 的缩写\n模块 主要输入 主要机制 主要输出 核心作用 编码器 (Encoder) 输入序列（如英文句子） 自注意力（Self-Attention） 每个词的上下文语义表示 理解输入语义 解码器 (Decoder) 上下文表示 + 已生成的部分输出 Masked Self-Attention + Encoder-Decoder Attention 下一个词的概率分布 根据语义生成输出 Encoder-only PLM Bert 自 BERT 推出以来，预训练+微调的模式开始成为自然语言处理任务的主流.\ntransformer架构: BERT 正沿承了 Transformer 的思想，在 Transformer 的模型基座上进行优化，通过将 Encoder 结构进行堆叠，扩大模型参数，打造了在 NLU 任务上独居天分的模型架构； 预训练+微调范式: 模型整体既是由 Embedding、Encoder 加上 prediction_heads 组成; prediction_heads用于将多维度的隐藏状态通过线性层转换到分类维度, prediction_heads 其实就是线性层加上激活函数，一般而言，最后一个线性层的输出维度和任务的类别数相等.\n相较于基本沿承 Transformer 的模型架构，BERT 更大的创新点在于其提出的两个新的预训练任务上——MLM 和 NSP（Next Sentence Prediction，下一句预测）。\n预训练-微调范式的核心优势在于，通过将预训练和微调分离，完成一次预训练的模型可以仅通过微调应用在几乎所有下游任务上，只要微调的成本较低，即使预训练成本是之前的数倍甚至数十倍，模型仍然有更大的应用价值。预训练数据的核心要求即是需要极大的数据规模（数亿 token）。\n**MLM，也就是掩码语言模型作为新的预训练任务。**相较于模拟人类写作的 LM，MLM 模拟的是“完形填空”。MLM 的思路也很简单，在一个文本序列中随机遮蔽部分 token，然后将所有未被遮蔽的 token 输入模型，要求模型根据输入预测被遮蔽的 token。由于模型可以利用被遮蔽的 token 的上文和下文一起理解语义来预测被遮蔽的 token，因此通过这样的任务，模型可以拟合双向语义，也就能够更好地实现文本的理解。\n**NSP，即下一个句子预测。**NSP 的核心思想是要求模型判断一个句对的两个句子是否是连续的上下文，例如问答匹配、自然语言推理等。这样的任务都需要模型在句级去拟合关系，判断两个句子之间的关系，而不仅是 MLM 在 token 级拟合的语义关系。\nBERT 的一个重大意义就是正式确立了预训练-微调的两阶段思想，即在海量无监督语料上进行预训练来获得通用的文本理解与生成能力，再在对应的下游任务上进行微调。该种思想的一个重点在于，预训练得到的强大能力能否通过低成本的微调快速迁移到对应的下游任务上。\n所谓微调，其实和训练时更新模型参数的策略一致，只不过在特定的任务、更少的训练数据、更小的 batch_size 上进行训练，更新参数的幅度更小。\nRoBERTa 优化一：去掉 NSP 预训练任务.\n去掉NSP, 为MLM从静态遮蔽改为动态遮蔽.\n优化二：更大规模的预训练数据和预训练步长\n更大的预训练数据、更长的序列长度和更多的训练 Epoch，需要预训练阶段更多的算力资源。\n优化三：更大的 bpe 词表\n与 BERT 使用的 WordPiece 算法不同，RoBERTa 使用了 BPE 作为 Tokenizer 的编码策略。BPE，即 Byte Pair Encoding，字节对编码，是指以子词对作为分词的单位。越大的词表也会带来模型参数的增加。\nRoBERTa 成功地在 BERT 架构的基础上刷新了多个下游任务的 SOTA，也一度成为 BERT 系模型最热门的预训练模型。RoBERTa 的成功也证明了更大的预训练数据、更大的预训练步长的重要意义，这也是 LLM 诞生的基础之一。\nALBERT ALBERT 成功地以更小规模的参数实现了超越 BERT 的能力, 虽然 ALBERT 所提出的一些改进思想并没有在后续研究中被广泛采用，但其降低模型参数的方法及提出的新预训练任务 SOP 仍然对 NLP 领域提供了重要的参考意义。\n优化一：将 Embedding 参数进行分解\nEmbedding 层的参数矩阵维度为 V∗H，此处的 V 为词表大小 30K，H 即为隐藏层大小 1024，也就是 Embedding 层参数达到了 30M。\nALBERT 对 Embedding 层的参数矩阵进行了分解，让 Embedding 层的输出维度和隐藏层维度解绑，也就是在 Embedding 层的后面加入一个线性矩阵进行维度变换。ALBERT 设置了 Embedding 层的输出为 128，因此在 Embedding 层后面加入了一个 $128*1024$ 的线性矩阵来将 Embedding 层的输出再升维到隐藏层大小。也就是说，Embedding 层的参数从 $V*H$ 降低到了 $V*E + E*H$，当 E 的大小远小于 H 时，该方法对 Embedding 层参数的优化就会很明显。\n优化二：跨层进行参数共享\nALBERT 提出，可以让各个 Encoder 层共享模型参数，来减少模型的参数量。\n将24个Encoder层变成1个Encoder层, 虽然各层共享权重，但计算时仍然要通过 24次 Encoder Layer 的计算，也就是说训练和推理时的速度相较 BERT 还会更慢, 训练效率也只略微优于 BERT.\n优化三：提出 SOP 预训练任务\n在传统的 NSP 任务中，正例是由两个连续句子组成的句对，而负例则是从任意两篇文档中抽取出的句对，模型可以较容易地判断正负例，并不能很好地学习深度语义。而 SOP 任务提出的改进是，正例同样由两个连续句子组成，但负例是将这两个的顺序反过来。也就是说，模型不仅要拟合两个句子之间的关系，更要学习其顺序关系，这样就大大提升了预训练的难度。\nALBERT 通过实验证明，SOP 预训练任务对模型效果有显著提升。使用 MLM + SOP 预训练的模型效果优于仅使用 MLM 预训练的模型更优于使用 MLM + NSP 预训练的模型。\nEncoder-Decoder PLM T5 T5 基于 Transformer 架构，包含编码器和解码器两个部分，使用自注意力机制和多头注意力捕捉全局依赖关系，利用相对位置编码处理长序列中的位置信息，并在每层中包含前馈神经网络进一步处理特征。\nT5 模型的预训练任务是一个关键的组成部分，它能使模型能够学习到丰富的语言表示，语言表示能力可以在后续的微调过程中被迁移到各种下游任务。训练所使用的数据集是一个大规模的文本数据集，包含了各种各样的文本数据，如维基百科、新闻、书籍等等。其中包括多张输入格式,清洗数据,多任务与训练,微调等.\nT5模型的一个核心理念是**“大一统思想”**，即所有的 NLP 任务都可以统一为文本到文本的任务，这一思想在自然语言处理领域具有深远的影响。其设计理念是将所有不同类型的NLP任务（如文本分类、翻译、文本生成、问答等）转换为一个统一的格式：输入和输出都是纯文本。\n对于不同的NLP任务，每次输入前都会加上一个任务描述前缀，明确指定当前任务的类型。这不仅帮助模型在预训练阶段学习到不同任务之间的通用特征，也便于在微调阶段迅速适应具体任务。例如，任务前缀可以是“summarize: ”用于摘要任务，或“translate English to German: ”用于翻译任务。\nDecoder-Only PLM GPT GPT，即 Generative Pre-Training Language Model，GPT 的整体结构和 BERT 是有一些类似的，只是相较于 BERT 的 Encoder，选择使用了 Decoder 来进行模型结构的堆叠。由于 Decoder-Only 结构也天生适用于文本生成任务，所以相较于更贴合 NLU 任务设计的 BERT，GPT 和 T5 的模型设计更契合于 NLG 任务和 Seq2Seq 任务。\n**过程: **\n输入的 input_ids 首先通过 Embedding 层，再经过 Positional Embedding 进行位置编码。不同于 BERT 选择了可训练的全连接层作为位置编码，GPT 沿用了 Transformer 的经典 Sinusoidal 位置编码，即通过三角函数进行绝对位置编码，通过 Embedding 层和 Positional Embedding 层编码成 hidden_states 之后，就可以进入到解码器（Decoder），第一代 GPT 模型和原始 Transformer 模型类似，选择了 12层解码器层，但是在解码器层的内部，相较于 Transformer 原始 Decoder 层的双注意力层设计，GPT 的 Decoder 层反而更像 Encoder 层一点。由于不再有 Encoder 的编码输入，Decoder 层仅保留了一个带掩码的注意力层，并且将 LayerNorm 层从 Transformer 的注意力层之后提到了注意力层之前。hidden_states 输入 Decoder 层之后，会先进行 LayerNorm，再进行掩码注意力计算，然后经过残差连接和再一次 LayerNorm 进入到 MLP 中并得到最后输出。\n由于不存在 Encoder 的编码结果，Decoder 层中的掩码注意力也是自注意力计算。也就是对一个输入的 hidden_states，会通过三个参数矩阵来生成 query、key 和 value，而不再是像 Transformer 中的 Decoder 那样由 Encoder 输出作为 key 和 value。后续的注意力计算过程则和 BERT 类似，只是在计算得到注意力权重之后，通过掩码矩阵来遮蔽了未来 token 的注意力权重，从而限制每一个 token 只能关注到它之前 token 的注意力，来实现掩码自注意力的计算。\nCLM\nDecoder-Only 的模型结构往往更适合于文本生成任务，因此，Decoder-Only 模型往往选择了最传统也最直接的预训练任务——**因果语言模型，Casual Language Model，**下简称 CLM。\nCLM 可以看作 N-gram 语言模型的一个直接扩展。N-gram 语言模型是基于前 N 个 token 来预测下一个 token，CLM 则是基于一个自然语言序列的前面所有 token 来预测下一个 token，通过不断重复该过程来实现目标文本序列的生成。也就是说，CLM 是一个经典的补全形式。\nBERT 之所以可以采用预训练+微调的范式取得重大突破，正是因为其选择的 MLM、NSP 可以在海量无监督语料上直接训练——而很明显，CLM 是更直接的预训练任务，其天生和人类书写自然语言文本的习惯相契合，也和下游任务直接匹配，相对于 MLM 任务更加直接，可以在任何自然语言文本上直接应用。因此，CLM 也可以使用海量的自然语言语料进行大规模的预训练。\nGPT-1 是 GPT 系列的开山之作，也是第一个使用 Decoder-Only 的预训练模型。\nGPT-2 的核心改进是大幅增加了预训练数据集和模型体量。GPT-2 的另一个重大突破是以 zero-**shot（零样本学习）**为主要目标，也就是不对模型进行微调，直接要求模型解决任务。\nGPT-3 则是更进一步展示了 OpenAI“力大砖飞”的核心思路，也是 LLM 的开创之作。在 GPT-2 的基础上，OpenAI 进一步增大了模型体量和预训练数据量，整体参数量达 175B，是当之无愧的“大型语言模型”。在模型结构上，基本没有大的改进，只是由于巨大的模型体量使用了稀疏注意力机制来取代传统的注意力机制。之所以说 GPT-3 是 LLM 的开创之作，除去其巨大的体量带来了涌现能力的凸显外，还在于其提出了 few-shot 的重要思想。few-shot 是对 zero-shot 的一个折中，旨在提供给模型少样的示例来教会它完成任务。few-shot 一般会在 prompt（也就是模型的输入）中增加 3~5个示例，来帮助模型理解。\nLLaMa LLaMA 的全称是 Large Language Model Meta AI ，即大语言模型（Meta AI 研发 ).\nLLaMA模型的整体结构与GPT系列模型类似，只是在模型规模和预训练数据集上有所不同。\nGPT 追求商业级的通用智能与对齐安全，采用私有高质量数据、强化学习（RLHF）和多专家结构（MoE）以实现强泛化和稳健对话能力；而 LLaMA 注重研究开放与效率，依靠公开数据、自回归预训练与直接偏好优化（DPO）等方法，强调轻量化、可复现和开源生态。前者以“对齐人类价值”为核心，后者以“可被人类研究”为目标，因此 GPT 更像黑盒的工程奇迹，LLaMA 则是可验证的科研基石。\nGLM GLM全称是General Language Model，即通用语言模型 ，是由清华大学的KEG实验室和智谱AI公司联合研发的预训练语言模型。核心思路是在传统 CLM 预训练任务基础上，加入 MLM 思想，从而构建一个在 NLG 和 NLU 任务上都具有良好表现的统一模型。\nMLM：全称是 Masked Language Model，即掩码语言模型. 适用于编码器中的文本理解\nCLM：全称是 Causal Language Model，即自回归语言模型. 适用于解码器中的文本生成\nGLM系列模型融合了自回归和自编码器的训练目标，采用了独特的位置编码、旋转位置嵌入（RoPE ）等技术，具备文本生成、知识问答、阅读理解、代码生成等多种能力，在多个中文自然语言处理任务评估基准上表现出色。\n所谓自编码思想，其实也就是 MLM 的任务学习思路，在输入文本中随机删除连续的 tokens，要求模型学习被删除的 tokens；所谓自回归思想，其实就是传统的 CLM 任务学习思路，也就是要求模型按顺序重建连续 tokens。\nGLM 预训练任务更多的优势还是展现在预训练模型时代，迈入 LLM 时代后，针对于超大规模、体量的预训练，CLM 展现出远超 MLM 的优势。虽然从 LLM 的整体发展路径来看，GLM 预训练任务似乎是一个失败的尝试，但通过精巧的设计将 CLM 与 MLM 融合，并第一时间产出了中文开源的原生 LLM，其思路仍然存在较大的借鉴意义。\n4 大语言模型 从 NLP 的定义与主要任务出发，介绍了引发 NLP 领域重大变革的核心思想——注意力机制与 Transformer 架构。随着 Transformer 架构的横空出世，NLP 领域逐步进入预训练-微调范式，以 Transformer 为基础的、通过预训练获得强大文本表示能力的预训练语言模型层出不穷，将 NLP 的各种经典任务都推进到了一个新的高度。\n随着2022年底 ChatGPT 再一次刷新 NLP 的能力上限，大语言模型（Large Language Model，LLM）开始接替传统的预训练语言模型（Pre-trained Language Model，PLM） 成为 NLP 的主流方向，基于 LLM 的全新研究范式也正在刷新被 BERT 发扬光大的预训练-微调范式，NLP 由此迎来又一次翻天覆地的变化。\nLLM是什么 LLM基础介绍 LLM，即 Large Language Model，中文名为大语言模型或大型语言模型，是一种相较传统语言模型参数量更多、在更大规模语料上进行预训练的语言模型。\n一般认为，GPT-3（1750亿参数）是 LLM 的开端，基于 GPT-3 通过 **预训练（Pretraining）、监督微调（Supervised Fine-Tuning，SFT）、强化学习与人类反馈（Reinforcement Learning with Human Feedback，RLHF）**三阶段训练得到的 ChatGPT 更是主导了 LLM 时代的到来。\nLLM的能力 涌现能力（Emergent Abilities） \u0026mdash; 涌现能力是指同样的模型架构与预训练任务下，某些能力在小型模型中不明显，但在大型模型中特别突出。(可理解为量变产生之变) 上下文学习（In-context Learning）\u0026mdash; 上下文学习是指允许语言模型在提供自然语言指令或多个任务示例的情况下，通过理解上下文并生成相应输出的方式来执行任务，而无需额外的训练或参数更新。 指令遵循（Instruction Following）\u0026mdash; 经过指令微调的 LLM 能够理解并遵循未见过的指令，并根据任务指令执行任务，而无需事先见过具体示例，这展示了其强大的泛化能力。 逐步推理（Step by Step Reasoning）\u0026mdash; LLM 通过采用思维链（Chain-of-Thought，CoT）推理策略，可以利用包含中间推理步骤的提示机制来解决这些任务 LLM的特点 多语言支持 \u0026mdash; 多语言、跨语言模型曾经是 NLP 的一个重要研究方向，但 LLM 由于需要使用到海量的语料进行预训练，训练语料往往本身就是多语言的，因此 LLM 天生即具有多语言、跨语言能力，只不过随着训练语料和指令微调的差异，在不同语言上的能力有所差异。\n长文本处理 \u0026mdash; 由于能够处理多长的上下文文本，在一定程度上决定了模型的部分能力上限，LLM 往往比传统 PLM 更看重长文本处理能力。\nLLM 大部分采用了旋转位置编码（Rotary Positional Encoding，RoPE）（或者同样具有外推能力的 AliBi）作为位置编码，具有一定的长度外推能力，也就是在推理时能够处理显著长于训练长度的文本。\n拓展多模态 \u0026mdash; LLM 的强大能力也为其带来了跨模态的强大表现。随着 LLM 的不断改进，通过为 LLM 增加额外的参数来进行图像表示，从而利用 LLM 的强大能力打造支持文字、图像双模态的模型，已经是一个成功的方法。\n通过引入 Adapter 层和图像编码器，并针对性地在图文数据上进行有监督微调，模型能够具备不错的图文问答甚至生成能力。\n挥之不去的幻觉 \u0026mdash; 幻觉，是指 LLM 根据 Prompt 杜撰生成虚假、错误信息的表现。\n目前也有很多研究提供了削弱幻觉的一些方法，如 Prompt 里进行限制、通过 RAG（检索增强生成）来指导生成等，但都还只能一定程度减弱幻觉而无法彻底根除。\n如何训练llm Q: 如何训练一个llm, 训练llm和训练传统预训练模型的区别是什么\nPretrain Pretrain，即预训练，是训练 LLM 最核心也是工程量最大的第一步。同样是使用海量无监督文本对随机初始化的模型参数进行训练。正如我们在第三章中所见，目前主流的 LLM 几乎都采用了 Decoder-Only 的类 GPT 架构（LLaMA 架构），它们的预训练任务也都沿承了 GPT 模型的经典预训练任务——因果语言模型（Causal Language Model，CLM）。\nLLM 的核心特点即在于其具有远超传统预训练模型的参数量，同时在更海量的语料上进行预训练。\n分布式训练框架的核心思路是数据并行和模型并行。所谓数据并行，是指训练模型的尺寸可以被单个 GPU 内存容纳，但是由于增大训练的 batch_size 会增大显存开销，无法使用较大的 batch_size 进行训练；同时，训练数据量非常大，使用单张 GPU 训练时长难以接受。\n预训练数据的处理与清洗也是 LLM 预训练的一个重要环节。诸多研究证明，预训练数据的质量往往比体量更加重要。预训练数据处理一般包括以下流程：\n文档准备。由于海量预训练语料往往是从互联网上获得，一般需要从爬取的网站来获得自然语言文档。文档准备主要包括 URL 过滤（根据网页 URL 过滤掉有害内容）、文档提取（从 HTML 中提取纯文本）、语言选择（确定提取的文本的语种）等。 语料过滤。语料过滤的核心目的是去除低质量、无意义、有毒有害的内容，例如乱码、广告等。语料过滤一般有两种方法：基于模型的方法，即通过高质量语料库训练一个文本分类器进行过滤；基于启发式的方法，一般通过人工定义 web 内容的质量指标，计算语料的指标值来进行过滤。 语料去重。实验表示，大量重复文本会显著影响模型的泛化能力，因此，语料去重即删除训练语料中相似度非常高的文档，也是必不可少的一个步骤。去重一般基于 hash 算法计算数据集内部或跨数据集的文档相似性，将相似性大于指定阈值的文档去除；也可以基于子串在序列级进行精确匹配去重。 SFT SFT（Supervised Fine-Tuning，有监督微调）。所谓有监督微调，其实就是我们在第三章中讲过的预训练-微调中的微调，稍有区别的是，对于能力有限的传统预训练模型，我们需要针对每一个下游任务单独对其进行微调以训练模型在该任务上的表现。而面对能力强大的 LLM，我们往往不再是在指定下游任务上构造有监督数据进行微调，而是选择训练模型的“通用指令遵循能力”，也就是一般通过指令微调的方式来进行 SFT。\nSFT 的主要目标是让模型从多种类型、多种风格的指令中获得泛化的指令遵循能力，也就是能够理解并回复用户的指令。因此，类似于 Pretrain，SFT 的数据质量和数据配比也是决定模型指令遵循能力的重要因素。\n首先是指令数据量及覆盖范围。为了使 LLM 能够获得泛化的指令遵循能力，即能够在未训练的指令上表现良好，需要收集大量类别各异的用户指令和对应回复对 LLM 进行训练。一般来说，在单个任务上 500~1000 的训练样本就可以获得不错的微调效果。但是，为了让 LLM 获得泛化的指令遵循能力，在多种任务指令上表现良好，需要在训练数据集中覆盖多种类型的任务指令，同时也需要相对较大的训练数据量，表现良好的开源 LLM SFT 数据量一般在数 B token 左右。\n指令微调本质上仍然是对模型进行 CLM 训练，只不过要求模型对指令进行理解和回复而不是简单地预测下一个 token，所以模型预测的结果不仅是 output，而应该是 input + output，只不过 input 部分不参与 loss 的计算，但回复指令本身还是以预测下一个 token 的形式来实现的。\n模型是否支持多轮对话，与预训练是没有关系的。事实上，模型的多轮对话能力完全来自于 SFT 阶段。如果要使模型支持多轮对话，我们需要在 SFT 时将训练数据构造成多轮对话格式，让模型能够利用之前的知识来生成回答。\nRLHF 模型是否支持多轮对话，与预训练是没有关系的。事实上，模型的多轮对话能力完全来自于 SFT 阶段。如果要使模型支持多轮对话，我们需要在 SFT 时将训练数据构造成多轮对话格式，让模型能够利用之前的知识来生成回答。\nRLHF，全称是 Reinforcement Learning from Human Feedback，即人类反馈强化学习，是利用强化学习来训练 LLM 的关键步骤。\n从功能上出发，我们可以将 LLM 的训练过程分成预训练与对齐（alignment）两个阶段。预训练的核心作用是赋予模型海量的知识，而所谓对齐，其实就是让模型与人类价值观一致，从而输出人类希望其输出的内容。SFT 是让 LLM 和人类的指令对齐，从而具有指令遵循能力；而 RLHF 则是从更深层次令 LLM 和人类价值观对齐，令其达到安全、有用、无害的核心标准。\nRLHF 的思路是，引入强化学习的技术，通过实时的人类反馈令 LLM 能够给出更令人类满意的回复。RLHF 分为两个步骤：训练 RM 和 PPO 训练。\nRM，Reward Model，即奖励模型。\nPPO，Proximal Policy Optimization，近端策略优化算法，是一种经典的 RL 算法。\n5 动手搭建大模型 Meta（原Facebook）于2023年2月发布第一款基于Transformer结构的大型语言模型LLaMA，并于同年7月发布同系列模型LLaMA2。\n- 参考链接 datawhalechina/happy-llm: 从零开始的大语言模型原理与实践教程\n","date":"2025-10-10T00:00:00Z","permalink":"https://sutdown.github.io/p/%E5%A4%A7%E6%A8%A1%E5%9E%8B%E5%8E%9F%E7%90%86%E4%BB%8Etransformer%E5%88%B0llm/","title":"大模型原理：从transformer到llm"},{"content":"0 AI相关基础概念 LangChain 是一个用于开发由大型语言模型（LLMs）驱动的应用程序的框架。 **生成式AI：**使用大模型进行支持，在大量原始未标记的数据基础上对于深度学习模型进行预训练，从而让机器能够理解语言甚至图像，能根据需要自动生成内容。 **大模型的训练阶段：**预训练（提升本身的知识量），SFT（Supervised Fine-Tuning监督微调，专注于选择某一方面），RLF（Reinforcement Learning with Human Feedback基于人类反馈的强化学习） prompt：使用大模型时，向模型提供的一些指令或者问题，这些指令作为模型输入，引导模型产生必要的输出。 1 基础LLM和聊天模型调用 Chat API调用 LangChain 将底层 API 调用抽象为统一的接口（如ChatOpenAI、LLM类），屏蔽了不同模型 API 的差异（如阿里云、百度、OpenAI 的接口格式不同）。\n当前支持的模型：聊天模型 | 🦜️🔗 LangChain 框架\nAPP KEY 以通义千问为例，app key可以从阿里云百炼大模型服务平台中获取，存在一定的免费额度，app key建议新建一个.env文件存储，加上load_dotenv()预先加载即可。\nPrompt 模板 ChatPromptTemplate 是 LangChain 中用于管理对话提示词的核心工具，专为多角色（如 system、user、assistant）对话场景设计，其优点主要体现在结构化、灵活性、复用性和生态集成能力上。\n它的核心在于增加了提示词的灵活性，将其从”静态字符串“升级成了”可配置，可动态生成，可集成的结构化组件“。在需要处理多角色度化，动态调整提示词，复用模板或者集成langchain的其它功能时，能够显著提升代码质量。\n官方文档：提示模板 | 🦜️🔗 LangChain 框架\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 import os from langchain_community.chat_models import ChatTongyi from dotenv import load_dotenv from langchain_core.prompts import ChatPromptTemplate load_dotenv() os.environ[\u0026#34;DASHSCOPE_API_KEY\u0026#34;] = os.getenv(\u0026#34;DASHSCOPE_API_KEY\u0026#34;) def main(): chat = ChatTongyi(model=\u0026#34;qwen3-max\u0026#34;) prompt_template = ChatPromptTemplate.from_messages([ (\u0026#34;system\u0026#34;, \u0026#34;你是{role}，专业领域为{field}。回答需符合{style}风格，控制在{word_limit}字左右。\u0026#34;), (\u0026#34;user\u0026#34;, \u0026#34;这个{concept}的作用是什么，担任什么样的角色？\u0026#34;) ]) params1 = { \u0026#34;role\u0026#34;: \u0026#34;科普博主\u0026#34;, \u0026#34;field\u0026#34;: \u0026#34;人工智能\u0026#34;, \u0026#34;style\u0026#34;: \u0026#34;口语化、通俗易懂\u0026#34;, \u0026#34;word_limit\u0026#34;: \u0026#34;30\u0026#34;, \u0026#34;concept\u0026#34;: \u0026#34;通义千问\u0026#34; } params2 = { \u0026#34;role\u0026#34;: \u0026#34;AI工程师\u0026#34;, \u0026#34;field\u0026#34;: \u0026#34;大语言模型\u0026#34;, \u0026#34;style\u0026#34;: \u0026#34;专业、技术化\u0026#34;, \u0026#34;word_limit\u0026#34;: \u0026#34;50\u0026#34;, \u0026#34;concept\u0026#34;: \u0026#34;通义千问\u0026#34; } print(\u0026#34;=== 场景1：向普通用户解释 ===\u0026#34;) messages1 = prompt_template.format_messages(**params1) # 用参数填充模板 response1 = chat.invoke(messages1) print(response1.content) print(\u0026#34;\\n=== 场景2：向开发者解释 ===\u0026#34;) messages2 = prompt_template.format_messages(**params2) # 复用同一模板，仅换参数 response2 = chat.invoke(messages2) print(response2.content) if __name__ == \u0026#34;__main__\u0026#34;: main() \u0026#34;\u0026#34;\u0026#34; === 场景1：向普通用户解释 === 通义千问是个AI助手，能回答问题、写故事、编程，帮你搞定各种任务！ === 场景2：向开发者解释 === 通义千问是大语言模型，用于理解与生成人类语言，支持问答、创作、推理等任务，扮演智能助手角色。 \u0026#34;\u0026#34;\u0026#34; 2 简单Agent 基础概念 AI agents：基于LLM的能够自主理解，自主规划决策，执行复杂任务的智能体。\nAI agents流程\n规划（planing）：将任务分为子任务，对任务进行思考反思 \u0026mdash; 给一个合适的prompt 记忆（memory）：记住执行任务的上下文，有助于更好的理解当前任务 \u0026mdash; 短期记忆 长期记忆 工具（tools）：为智能体配备工具AI，比如计算器，搜索工具等 核心逻辑 Agent 的核心逻辑遵循 REACT 框架（Reason→Act→Observe→React），这是一种典型的链式流程。\n1 用户输入 → 记忆组件（加载历史对话） → Agent（分析是否调用工具） → 工具（执行任务） → Agent（处理结果） → 记忆组件（保存新对话） → 输出回答 LangChain.tools LangChain 的 Tools 是连接大模型与外部功能的核心组件。工具是一种封装函数及其模式的方式，以便可以将其传递给聊天模型。使用@tool装饰器创建工具，该装饰器简化了工具创建过程，支持以下功能：\n自动推断工具的名称、描述和预期参数，同时支持自定义。 定义返回工件（例如图像、数据框等）的工具 使用注入的工具参数从模式（从而从模型）中隐藏输入参数。 工具 | 🦜️🔗 LangChain 框架\n旅游助手agent 这段代码实现了一个基于 LangChain 和 通义千问（ChatTongyi） 的智能旅行助手，具备联网搜索、预算计算和多轮对话记忆功能。\n谷歌搜索工具（google_search）：通过 Serper API 调用 Google 搜索，获取实时信息如天气、景点、政策等。Serper API key，也可以用其余搜索工具，采用相应的api key即可。 预算计算工具（calculate_budget）：输入“天数,每日预算”格式，自动计算旅行总费用。 对话记忆（ConversationSummaryMemory）：保留历史对话摘要，帮助模型理解上下文（如“刚才的城市”） 自定义提示词模板（ChatPromptTemplate）：定义系统行为和工具使用逻辑，让模型决定何时调用工具。 智能体（Agent）初始化：通过 initialize_agent() 将 LLM、工具和记忆整合为具备推理和工具调用能力的对话体。 交互循环（chat_loop）：实现命令行多轮聊天界面，支持实时输入与退出（exit/退出）。 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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 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 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 import os import requests from dotenv import load_dotenv from langchain_community.chat_models import ChatTongyi from langchain.prompts import ChatPromptTemplate, MessagesPlaceholder from langchain.tools import tool from langchain.memory import ConversationSummaryMemory from langchain.agents import initialize_agent, AgentType from datetime import datetime load_dotenv() \u0026#34;\u0026#34;\u0026#34;tool\u0026#34;\u0026#34;\u0026#34; @tool(\u0026#34;谷歌搜索\u0026#34;, description=\u0026#34;使用 Google 搜索获取实时信息，如‘北京天气’、‘东京景点推荐’、‘签证政策’等\u0026#34;) def google_search(query: str) -\u0026gt; str: api_key = os.getenv(\u0026#34;SERPER_API_KEY\u0026#34;) if not api_key: return \u0026#34;未设置 SERPER_API_KEY 环境变量\u0026#34; url = \u0026#34;https://google.serper.dev/search\u0026#34; headers = {\u0026#34;X-API-KEY\u0026#34;: api_key, \u0026#34;Content-Type\u0026#34;: \u0026#34;application/json\u0026#34;} payload = {\u0026#34;q\u0026#34;: query, \u0026#34;num\u0026#34;: 5} try: res = requests.post(url, headers=headers, json=payload) data = res.json() if \u0026#34;organic\u0026#34; not in data: return \u0026#34;未找到搜索结果。\u0026#34; results = [ f\u0026#34;{r.get(\u0026#39;title\u0026#39;)}: {r.get(\u0026#39;snippet\u0026#39;)} ({r.get(\u0026#39;link\u0026#39;)})\u0026#34; for r in data[\u0026#34;organic\u0026#34;][:3] ] return \u0026#34;\\n\\n\u0026#34;.join(results) except Exception as e: return f\u0026#34;搜索出错: {e}\u0026#34; @tool(\u0026#34;计算旅行预算\u0026#34;, description=\u0026#34;计算旅行总预算，输入格式为“天数,每日预算”（如“3,500”），返回总预算结果\u0026#34;, return_direct=False) def calculate_budget(input_str: str) -\u0026gt; str: \u0026#34;\u0026#34;\u0026#34;计算旅行总预算，输入格式为“天数,每日预算”（如“3,500”）\u0026#34;\u0026#34;\u0026#34; try: days, daily_cost = input_str.split(\u0026#34;,\u0026#34;) total = int(days) * int(daily_cost) return f\u0026#34;总预算：{days}天×{daily_cost}元/天={total}元\u0026#34; except ValueError: return \u0026#34;输入格式错误，请使用“天数,每日预算”（如“3,500”）\u0026#34; tools = [google_search, calculate_budget] \u0026#34;\u0026#34;\u0026#34;llm\u0026#34;\u0026#34;\u0026#34; llm = ChatTongyi(model=\u0026#34;qwen3-max\u0026#34;, api_key=os.getenv(\u0026#34;DASHSCOPE_API_KEY\u0026#34;)) \u0026#34;\u0026#34;\u0026#34;memory\u0026#34;\u0026#34;\u0026#34; # 摘要记忆，节省token memory = ConversationSummaryMemory( llm=llm, memory_key=\u0026#34;chat_history\u0026#34;, return_messages=True ) \u0026#34;\u0026#34;\u0026#34;prompt\u0026#34;\u0026#34;\u0026#34; prompt = ChatPromptTemplate.from_messages([ (\u0026#34;system\u0026#34;, \u0026#34;\u0026#34;\u0026#34;你是一个智能旅行助手，拥有以下能力： 1. 当用户提出涉及实时信息（如天气、景点、签证等）的问题时，请调用【谷歌搜索】工具。 2. 当用户要求预算计算时，调用【计算旅行预算】工具。 3. 其它情况（例如行程建议、交通说明）直接回答。 4. 保持上下文一致性（chat_history），比如“刚才的城市”指之前提到的城市。 工作流程： - 先分析用户问题是否需要工具：需要则调用，不需要则直接回答 - 调用工具时严格遵循工具的输入格式 - 结合历史对话（chat_history）理解上下文（如“刚才说的城市”指之前提到的城市） - 用中文简洁回答，避免冗余 \u0026#34;\u0026#34;\u0026#34;), MessagesPlaceholder(variable_name=\u0026#34;chat_history\u0026#34;), # 插入历史对话 (\u0026#34;user\u0026#34;, \u0026#34;{input}\u0026#34;), # 当前用户输入 (\u0026#34;ai\u0026#34;, \u0026#34;{agent_scratchpad}\u0026#34;) # Agent思考过程（自动填充） ]) \u0026#34;\u0026#34;\u0026#34;agent\u0026#34;\u0026#34;\u0026#34; agent = initialize_agent( tools=tools, llm=llm, agent=AgentType.CHAT_CONVERSATIONAL_REACT_DESCRIPTION, # 带记忆的聊天型Agent memory=memory, # 使用记忆 # verbose=True, # 输出详细日志 agent_kwargs={ \u0026#34;prompt\u0026#34;: prompt, # 绑定提示词模板 \u0026#34;system_message\u0026#34;: prompt.messages[0].prompt.template, # 系统提示 \u0026#34;extra_prompt_messages\u0026#34;: [ MessagesPlaceholder(variable_name=\u0026#34;chat_history\u0026#34;) ] }, handle_parsing_errors=True, # 忽略解析错误 return_intermediate_steps=False ) \u0026#34;\u0026#34;\u0026#34;多轮对话\u0026#34;\u0026#34;\u0026#34; def chat_loop(): print(\u0026#34;智能旅行助手\u0026#34;) print(\u0026#34;提示：输入任意问题与我对话，例如：\u0026#39;我打算去东京玩三天\u0026#39; 或 \u0026#39;帮我查一下北京天气\u0026#39;\u0026#34;) print(\u0026#34;输入 \u0026#39;exit\u0026#39; 或 \u0026#39;退出\u0026#39; 可结束对话。\\n\u0026#34;) while True: user_input = input(\u0026#34;用户：\u0026#34;).strip() if user_input.lower() in [\u0026#34;exit\u0026#34;, \u0026#34;quit\u0026#34;, \u0026#34;退出\u0026#34;, \u0026#34;bye\u0026#34;]: print(\u0026#34;助手：好的，下次再见，祝你旅途愉快！👋\u0026#34;) break if not user_input: continue # 忽略空输入 current_time = datetime.now().strftime(\u0026#34;%Y年%m月%d日 %H:%M\u0026#34;) query = f\u0026#34;{user_input}\\n（当前时间：{current_time}）\u0026#34; try: response = agent.invoke({\u0026#34;input\u0026#34;: query}) result = response[\u0026#34;output\u0026#34;] if isinstance(response, dict) and \u0026#34;output\u0026#34; in response else response print(f\u0026#34;助手：{result}\\n\u0026#34;) except Exception as e: print(f\u0026#34;❌ 出错：{e}\\n\u0026#34;) if __name__ == \u0026#34;__main__\u0026#34;: chat_loop() 后话 LangChain 实现智能体的推理、工具调用与记忆，但其核心架构仍是「线性链式执行」。在复杂多轮推理、长上下文管理和并发场景下存在一定缺陷。langchain运行过程中会出现官方提示当前功能仍然可用，但推荐迁移到更新的模块或框架。\nLangGraph 是 LangChain 团队推出的下一代框架，它基于“有状态计算图（StateGraph）”思想，将每个步骤建模为节点，支持显式状态管理、并发执行、持久记忆与可视化调试，更适合用于创建实际需要的智能体。\n为什么选择 LangGraph？ - LangChain 框架官网\n参考文档 简介 | 🦜️🔗 LangChain 框架\n","date":"2025-10-06T00:00:00Z","permalink":"https://sutdown.github.io/p/langchain%E5%88%B0%E7%AE%80%E5%8D%95agent/","title":"Langchain到简单Agent"},{"content":"CUDA工具包： 选择cuda版本时，注意先在NVIDIA中看电脑显卡支持的CUDA版本。\n完整的 CUDA 开发工具集，包含编译器、库、驱动等，提供 CUDA 开发环境（编译 CUDA 代码、运行 CUDA 程序）。\nCUDA安装教程（超详细）-CSDN博客\n1 2 3 4 5 6 C:\\Users\\name\u0026gt;nvcc -V nvcc: NVIDIA (R) Cuda compiler driver Copyright (c) 2005-2023 NVIDIA Corporation Built on Wed_Feb__8_05:53:42_Coordinated_Universal_Time_2023 Cuda compilation tools, release 12.1, V12.1.66 Build cuda_12.1.r12.1/compiler.32415258_0 声明：系统 CUDA 版本可以高于 PyTorch 编译版本，且在多数情况下推荐这么做（尤其是新显卡），只要保证系统 CUDA 版本 ≥ PyTorch 要求的最低版本即可。这不会影响项目的正常运行，还可能带来更好的硬件适配性。\nPytorch相关包： 1 pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121 上述本质为预编译的 PyTorch 及其依赖库，仅包含运行时组件。有预编译的 torch._C 等二进制文件（依赖 CUDA 库），核心作用在于让 PyTorch 能调用 GPU 进行计算（依赖 CUDA 运行时）。\n","date":"2025-10-01T00:00:00Z","permalink":"https://sutdown.github.io/p/cuda%E5%AE%89%E8%A3%85%E5%92%8C%E5%85%A5%E9%97%A8%E4%BD%BF%E7%94%A8/","title":"cuda安装和入门使用"},{"content":"1 如何在两个大量的文件中找到相同的部分？ a和b两个文件，各自存放50亿个URL，每个URL占64B，内存限制为4G，请找出a，b两个文件共同的url。\n2^10 10^9 a，b文件大小为320GB\n分治+堆+哈希\n将a，b大文件拆分为多个小文件。该过程利用相同的哈希规则让url映射存放到某个文件之中，这样能够保证a和b中相同的url映射到相同的文件之中。拆分的过程主要在磁盘中进行，内存中边读边拆，逐行写入到磁盘之中。 将a和b中相对应的文件依次比较，最后合并结果即可。 优化点\n1 由于url经常出现公共前缀，可以优化url的存储方式\n2 布隆过滤器。（仅仅可用于前置筛选掉一定不存在的数据，之后还得进一步用分治+哈希、位图法等精确方案）\n布隆过滤器中是能够排除肯定不在文件的数据，但是有部分不在文件的数据可能判断不出来即误判率。\n因此根据A文件的url数量，设置一个误判率较低的布隆过滤器，然后将A文件中所有的值存入。将B文件中的url逐个读取逐个比较，最后得到最终筛选出来的url。 解决误判率：将该小文件的url，和A中的原始数据进行比对，可以分治法每次读取一部分A的数据，最后删除误判的url得到最终结果。 2 如何从大量数据中找出高频词？TOPN 单个文件大小为1GB，每个词大小不超过16B，内存大小限制为1MB，返回频数最高的100个词。\n分治+堆+哈希\n和上面思路相同，先采用哈希的方法分成多个小文件，如果小文件中存在大小超过限制的，那么对该小文件进行二次哈希，选择不同的哈希方案。 统计每个小文件的局部词频，直接存储在该文件中即可，写入磁盘。 维护一个小顶堆，当当前频数小于100时，直接加入该堆，大于100时，大于当前堆顶时加入该堆，反之去掉。 3 在2.5亿个整数中找到不重复的整数？ 1 哈希+分治成多个小文件，再用hashmap找出每个小文件中不重复的整数，最后合并结果。\n2 位图法。当存在2.5亿个整数时，从前到后依次用每个位判断该值是否存在（00不存在，01存在一次，11存在多次）缺点在于浪费了很多空间，数据必须是整数，整数范围不能过大。\n4 如何找到大量数据的中位数？ 1 分桶。预估范围分桶，得到每个桶中的数据量个数，遍历计算得到中位数的桶。将该桶加载到内存中排序即可。\n2 二分查找。选择一个值，将数据分成两部分。中位数一定在较大的一边中，之后递归可以找到中位数。缺点在于需要内存中能存储所有的数，很消耗内存。\n综合 大部分采用分治+哈希。\n另外如果内存够大，考虑位图，二分查找；\n对数据进行前置处理，考虑布隆过滤器，前缀树；\n找TOPK，用堆\n参考链接：\n1 海量数据处理| 阿秀的学习笔记\n","date":"2025-09-08T00:00:00Z","permalink":"https://sutdown.github.io/p/%E6%B5%B7%E9%87%8F%E6%95%B0%E6%8D%AE%E5%A4%84%E7%90%86/","title":"海量数据处理"},{"content":"二分查找详解 写法一： 35. 搜索插入位置 - 力扣（LeetCode）\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 int searchInsert(vector\u0026lt;int\u0026gt;\u0026amp; nums, int target) { int left = 0, right = (int)nums.size() - 1; while (left \u0026lt;= right) { int mid = (left + right) / 2; if (nums[mid] == target) return mid; if (nums[mid] \u0026gt; target) { right = mid - 1; } else { left = mid + 1; } } return left; } int searchInsert(vector\u0026lt;int\u0026gt;\u0026amp; nums, int target) { // 返回第一个值大于等于target的下标 int left = 0, right = (int)nums.size() - 1; while (left \u0026lt;= right) { int mid = (left + right) / 2; if (nums[mid] \u0026gt;= target) { right = mid - 1; } else { left = mid + 1; } } return left; // left右侧的位置都大于等于left，left左侧位置都小于left } 如果数组为[1,3,5,6]，target=2；\nleft = 0, right = 3（均为位置）\n第一轮 mid=1, left=0, right=0;\n第二轮 mid=0, left=1,right=1;\n总结：当target不在数组时，需要返回按顺序插入的位置，也就是第一个比target大的位置。因此返回left是最佳方案，right只会是比target小的第一个位置。\n写法二： 34. 在排序数组中查找元素的第一个和最后一个位置 - 力扣（LeetCode）\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 vector\u0026lt;int\u0026gt; searchRange(vector\u0026lt;int\u0026gt;\u0026amp; nums, int target) { if (nums.size() == 0) return vector\u0026lt;int\u0026gt;{-1, -1}; int l1 = -1, l2 = -1; int left = 0, right = (int)nums.size() - 1; while (left \u0026lt;= right) { int mid = (left + right) / 2; if (nums[mid] \u0026gt;= target) { // 找出第一个大于等于target的下标 right = mid - 1; } else { left = mid + 1; } } if (left \u0026lt; nums.size() \u0026amp;\u0026amp; nums[left] == target) l1 = left; left = 0, right = (int)nums.size() - 1; while (left \u0026lt;= right) { int mid = (left + right) / 2; if (nums[mid] \u0026lt;= target) { // 找出第一个小于等于target的下标 left = mid + 1; } else { right = mid - 1; } } if (right \u0026lt; nums.size() \u0026amp;\u0026amp; nums[right] == target) l2 = right; return vector\u0026lt;int\u0026gt;{l1, l2}; } 进阶：寻找峰值 162. 寻找峰值 - 力扣（LeetCode）\n1 2 3 4 5 6 7 8 9 10 11 12 int findPeakElement(vector\u0026lt;int\u0026gt;\u0026amp; nums) { int l = 0, r = nums.size() - 1; while (l \u0026lt; r) { int mid = (l + r) \u0026gt;\u0026gt; 1; if (nums[mid] \u0026gt; nums[mid + 1]) { r = mid; } else { l = mid + 1; } } return l; } ","date":"2025-08-21T00:00:00Z","permalink":"https://sutdown.github.io/p/%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE%E8%AF%A6%E8%A7%A3/","title":"二分查找详解"},{"content":"哈喽让我想想要怎么向你描述这个月呢\n相比较上一次实习的生疏，这一次的你显得更加的游刃有余，虽然依旧出现了一些问题，不过你依然并不是很慌张，毕竟问题终究都会有个结果的。这段时间你也结识了很多人，比如实习中的同事，合租的舍友等等。从前的朋友也还存在着交集，新朋友的相处看着也还不错，一切你应该都是很称心如意的。这样子说的话，那变化最多和最让你感到心慌的就是生活方式的转变了。每天见到的人变多，加上实习，总带着一点疲惫感，这就需要接下来的调整和适应了。\n不谈这个，谈点有兴致的，想记录一下我的二次元白月光了，写写这个灵感会多点。\n网球王子 幸存精市\n原神 温迪\n原神 神里绫人\n弦音 竹早静弥\n月影别墅 该隐\n灵契 端木熙\n原神 卡维\n失忆投捕 要圭\n","date":"2025-06-02T00:00:00Z","permalink":"https://sutdown.github.io/p/2025.%E4%BA%94%E6%9C%88%E5%B0%8F%E8%AE%B0/","title":"2025.五月小记"},{"content":"前置： 控制面板\u0026mdash;程序\u0026mdash;程序与功能 勾选 适用于Linux的Windows子系统 和 虚拟机平台（随后重启）\n之后如果出现 由于未安装所需的特性，无法启动操作。 错误代码: Wsl/InstallDistro/Service/RegisterDistro/CreateVm/HCS/HCS_E_SERVICE_NOT_AVAILABLE是由于虚拟机平台未成功打开，可 以管理员身份 打开 PowerShell 执行以下命令：\n1 2 3 4 5 6 7 8 # 启用 WSL 功能 dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart # 启用虚拟机平台 dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart # 启用 Hyper-V（部分系统需要） dism.exe /online /enable-feature /featurename:Hyper-V /all /norestart 安装过程： 1 2 3 4 wsl --install 该步骤需要输入 用户名 和两次相同密码 wsl --list --verbose\t可以查看可用的linux发行版 wsl --install Ubuntuxxx 选择发行版安装 wsl 启动wsl 验证安装： 1 2 wsl --status wsl --list --verbose 注意事项： 在第二步查看可用发行版时，容易出现 无法从“https://raw.githubusercontent.com/microsoft/WSL/master/distributions/DistributionInfo.json”提取列表分发。无法解析服务器的名称或地址 错误代码: Wsl/WININET_E_NAME_NOT_RESOLVED该问题，主要问题在于DNS污染，由于每次域名访问网站时会先查询本机的DNS，因此修改本机hosts文件即可，具体过程如下：\n1 raw.githubusercontent.com - GitHub · Build and ship software on a single, collaborative platform · GitHub该网站中可查询最新可用的raw.githubusercontent.comde ipv4地址\n2 以管理员权限打开本机hosts文件（一般位置在C:\\Windows\\System32\\drivers\\etc），文件最后加上 IP地址 raw.githubusercontent.com即可，无需重启。\n额外操作： 1 vscode 安装插件wsl，能够更方便的进行项目开发。\n2 wsl默认安装到C盘，该方案可以迁移至其它盘WSL默认安装目录 - CharyGao - 博客园。\n推荐文章： 1 [无法访问https://raw.githubusercontent.com/xx的解决方案（2025.2亲测有效）_raw.githubusercontent com-CSDN博客](https://blog.csdn.net/qq_73162098/article/details/145330440?ops_request_misc=%7B%22request%5Fid%22%3A%2215197b98d7c0420d5d962a1e6fa77eed%22%2C%22scm%22%3A%2220140713.130102334.pc%5Fall.%22%7D\u0026request_id=15197b98d7c0420d5d962a1e6fa77eed\u0026biz_id=0\u0026utm_medium=distribute.pc_search_result.none-task-blog-2~all~first_rank_ecpm_v1~rank_v31_ecpm-2-145330440-null-null.142^v102^control\u0026utm_term=无法从“https%3A%2F%2Fraw.githubusercontent.com%2Fmicrosoft%2FWSL%2Fmaster%2Fdistributions%2FDistributionInfo.json”提取列表分发。无法解析服务器的名称或地址 错误代码%3A Wsl%2FWININET_E_NAME_NOT_RESOLVED\u0026amp;spm=1018.2226.3001.4187)\n2 WSL默认安装目录 - CharyGao - 博客园\n","date":"2025-06-01T00:00:00Z","permalink":"https://sutdown.github.io/p/wsl2%E7%AE%80%E5%8D%95%E5%AE%89%E8%A3%85%E8%BF%87%E7%A8%8B%E4%BB%A5%E5%8F%8A%E6%B3%A8%E6%84%8F%E4%BA%8B%E9%A1%B9/","title":"WSL2简单安装过程以及注意事项"},{"content":" SQL全名 Structured Query Language。\n基本操作 CRUD增删改查 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 INSERT INTO user VALUES(10, \u0026#39;root\u0026#39;, \u0026#39;xxx@123.com\u0026#39;); -- 删除表中满足特定条件的行 DELETE FROM user WHERE username=\u0026#39;root\u0026#39;; -- 从表中删除满足特定条件的行，不指定表时会保留表的结构索引约束之类的，同时记录事务日志。 DROP TABLE IF EXISTS table_name; -- 直接释放表所占用的数据页，且不能用WHERE子句指定删除条件。同样保留表的结构索引约束，但不会记录每一行的删除操作。 TRUNCATE TABLE table_name; UPDATE user SET username=\u0026#39;root\u0026#39; WHERE username=\u0026#39;robot\u0026#39;; SELECT prod_name FROM products; 子查询 子查询和连接的区别\n子查询也可以成为内部查询，属于嵌套在较大查询中的SQL查询。\n1 2 3 4 5 6 SELECT cust_name, cust_contact FROM customers WHERE cust_id IN (SELECT cust_id FROM orders WHERE prod_id=\u0026#39;RGAN01\u0026#39; ) IN操作符在WHERE子句中使用，作用是几个特定值中任选一个值。\nBETWEEN操作符WHERE子句中使用，作用是选取介于某个范围内的值。\nAND，OR，NOT\nLIKE 正则表达式\n语法 连接：可以从多个表中获取相关的数据。\n子查询：嵌套在其它SQL语句中的查询。\n连接可以替换子查询，并且比子查询的效率一般更快。\n原因：\n减少查询次数。子查询会 减少中间数据传输。 JOIN 内连接 （inner join/join）\n外连接（左外连接left join，右外连接right join，全外连接full join）\n交叉连接（cross join）\n1 2 3 4 5 内连接（innner join，或称等值连接）：返回两张表中匹配的记录 左连接（left join）：返回两张表匹配的记录，以及左表中多余的记录 右连接（right join）：返回两张表匹配的记录，以及右表中多余的记录 全连接（full join）：返回两张表匹配的记录，以及左右两表中各自多余的记录 交叉连接（CROSS JOIN）：将一个表中的每一行和另一个表中的每一行进行结合，最终得到的结果集行数是两个表行数的乘积。 自然连接（natural join）：自动连接所有同名列\n1 2 3 4 5 6 7 8 9 10 SELECT up.university, qd.difficult_level, COUNT(qpd.question_id)/COUNT(distinct qpd.device_id) AS avg_answer_cnt FROM question_detail AS qd inner join question_practice_detail AS qpd inner join user_profile AS up ON up.device_id=qpd.device_id AND qpd.question_id=qd.question_id GROUP BY university, difficult_level UNION 组合查询：列数和列顺序相同；列数据类型相同或兼容。\n合并多个select语句的结果，会去除结果集中的重复行\nunion all，只是简单的合并结果\n查找山东大学或者性别为男生的信息_牛客题霸_牛客网\n1 2 3 4 5 6 7 8 9 10 -- 查找山东大学或者性别为男性的用户 SELECT device_id, gender, age, gpa FROM user_profile WHERE university=\u0026#39;山东大学\u0026#39; union all SELECT device_id, gender, age, gpa FROM user_profile WHERE gender=\u0026#39;male\u0026#39; 函数 AVG，COUNT，MAX，MIN，SUM\n1 2 3 SELECT SUBSTRING_INDEX(profile, \u0026#39;,\u0026#39;, -1)gender, COUNT(*)number FROM user_submit GROUP BY gender 排序和分组 ORDER BY（DESC，ASC）\nGROUP BY（先分组后排序）\nHAVING （对汇总的GROUP BY结果进行过滤，要求存在一个GROUPBY子句）\nHAVING 适用于汇总的组记录；而 WHERE 适用于单个记录。\n1 2 3 4 -- 多重排序 SELECT device_id, gpa, age FROM user_profile ORDER BY gpa ASC, age ASC 1 2 3 4 5 6 7 SELECT university, AVG(question_cnt) AS avg_question_cnt, AVG(answer_cnt) AS avg_answer_cnt FROM user_profile GROUP BY university HAVING avg_question_cnt\u0026lt;5 OR avg_answer_cnt\u0026lt;20 limit limit 5 offset 10\nlimit 10，5\n从第10条开始，返回之后的五条\ncase函数 SELECT case when age\u0026gt;12 then ‘\u0026gt;12age’ end age12\n1 2 3 4 5 SELECT CASE WHEN age\u0026lt;25 OR age IS NULL THEN \u0026#39;25岁以下\u0026#39; WHEN age\u0026gt;=25 THEN \u0026#39;25岁及以上\u0026#39; END age_cnt, COUNT(*) as number FROM user_profile GROUP BY age_cnt 约束 NOT NULL\nUNIQUE\nPRIMARY KEY（NOT NULL和UNIQUE的结合）\nFOREIGN KEY\nCHECK\nDEFAULT\n索引 一个表可以没有索引。\n索引（Index）是帮助MySQL高效获取数据的数据结构，索引对于良好的性能非常关键，尤其是当表中的数据量越来越大时，索引对于性能的影响愈发重要。索引优化应该是对查询性能优化最有效的手段了。索引能够轻易将查询性能提高好几个数量级。\n基础分类：主键索引，唯一索引，普通索引，全文索引，组合索引\n普通索引：找到满足条件的第一个记录之后，会查找下一个记录，直到不满足要求。\n唯一索引：查找到第一个满足条件的记录后，就会停止继续检索。\n（普通索引和唯一索引性能差距微乎其微）\n主键索引（unique，notnull）\n全文索引：全文索引是一种专门用于文本搜索的索引类型，它可以在文本列中快速查找包含特定关键词的记录。\n组合索引：组合索引是基于多个列创建的索引，它将多个列的值组合在一起作为索引键。\n索引存储的数据结构：B+树，哈希表，有序数组\n日志 binlog：数据备份和主从复制 redo log：持久性。用于崩溃掉电等事务恢复 undo log：原子性。用于事务回滚和MVCC HARD SQL 计算用户的平均次日留存率_牛客题霸_牛客网\n参考链接 12800字！SQL 语法速成手册（干货满满，建议收藏！） - 知乎\n牛客网在线编程_SQL篇_SQL快速入门\n","date":"2025-03-12T00:00:00Z","permalink":"https://sutdown.github.io/p/sql%E8%AF%A6%E8%A7%A3/","title":"SQL详解"},{"content":" qimi liwenzhou 博客 有很多点还没搞清楚，包括业务语法，有待加强 简历\n关键在于登录注册投票帖子展示等功能的实现 然后一些库函数，一些算法等 再就是性能分析 库函数 viper Viper是 Go 语言中一款功能强大的配置管理库，旨在简化应用程序的配置处理。它支持多种配置文件格式，包括 JSON、TOML、YAML、HCL、envfile 和 Java properties 等。此外，Viper 还提供了从环境变量、命令行标志、远程配置系统（如 etcd 或 Consul）以及直接在代码中设置配置值的功能。\nsqlx sqlx 是 Go 语言中的一个库，扩展了标准的 database/sql 库，简化了数据库操作，提供了更多的功能和便捷的方法，尤其是在处理结构体与数据库表之间的映射时。它的目标是提供比原生 database/sql 更简洁、更高效的数据库操作方式，同时仍保持与标准库的兼容性。\nlumberjack lumberjack 是 Go 语言中的一个库，主要用于 日志轮转（log rotation）和 日志文件管理。它提供了自动的日志文件切割功能，可以避免日志文件过大，从而有效管理日志的存储空间\nzap 能够将事件记录到文件中，而不是应用程序控制台。 日志切割-能够根据文件大小、时间或间隔等来切割日志文件。 支持不同的日志级别。例如INFO，DEBUG，ERROR等。 能够打印基本信息，如调用文件/函数名和行号，日志时间等 validator validator 是一个用于 Go 语言的强大且灵活的数据验证库，通常用于验证结构体中的字段是否符合某些规则。它通过标签（tags）来指定验证规则，支持各种常见的数据验证功能，例如字符串长度、邮箱格式、正则表达式匹配、数值范围等。\n设计理念 基于雪花算法生成用户id 分布式ID生成器 全局唯一性：不能出现有重复的ID标识，这是基本要求 递增性：确保生成ID对于用户或业务是递增的 高可用性：任何情况都能生成正确的ID 高性能性，高并发环境下表现良好 雪花算法 - 维基百科，自由的百科全书\nbwmarrn/snowflake 1 1bit Unused | 41bit 时间戳 | 10bit 机器ID | 12bit 序列号 snoy/snoyflake\n1 + 39 +8（序列号） + 16(机器id)\n用户认证 HTTP是一个无状态的协议，一次请求结束后，下次再发送服务器就不知道请求由谁发送了（一个IP不代表一个用户），在Web应用中，用户的认证和鉴权十分重要。\nCookie-Session认证模式 客户端使用用户名，密码验证 服务端验证用户名，密码正确后生成并存储Session，将SessionID通过Cookie返回客户端 客户端访问需要认证的接口时在Cookie中携带SessionID 服务端通过SessionID查找Session并进行鉴权，返回客户端需要的数据 但是，基于session方式存在多种问题：\n服务端需要存储session在内存中（需要快速查找），用户多时占用服务器资源较多。 当需要扩展时，创建session的服务器可能不是验证session的服务器，还需要共享所有的session 由于客户端使用cookie存储sessionID，跨域场景下需要进行兼容处理，同时这种方式以防范CSRF攻击 Token认证模式 基于Token的无状态会话管理，服务端不再存储信息，逻辑如下\n客户端使用用户名，密码认证 服务端验证用户名密码是否正确，生成Token返回客户端 客户端保存Token，访问需要认证的接口时在URL参数或者HTTP header种加入token 服务端解码Token进行鉴权，返回客户端需要的数据 解决了Session会话管理带来的问题\n服务端不需要存储和用户鉴权有关的信息，鉴权信息会被加密到token种，服务端只读取token包含的鉴权信息即可 避免了共享session导致的不易扩展 不需要依赖cookie，避免了相关的CSRF攻击 使用CORS可以快速解决跨域问题 JWT JWT（JSON Web Token）是一种开放标准（RFC 7519），用于安全地在各方之间传递信息。JWT 通常用于身份认证和授权场景，尤其是在 Web 应用和 API 中。它可以作为一种紧凑、URL 安全的方式来表示声明（claims），并且通常由三部分组成：头部、有效载荷和签名。\nJWT 的组成 一个 JWT 通常由三部分组成\nHeader通常包含算法和类型（JWT）（Base64 编码）：eyJhbGciOiAiSFMyNTYiLCJ0eXAiOiAiSldUIn0 Payload包含用户信息或声明（如用户 ID、角色等）（Base64 编码）：eyJzdWIiOiAiMTIzNDU2Nzg5MCIsIm5hbWUiOiAiSm9obiBEb2UiLCJpYXQiOiAxNTE2MjM5MDIyfQ Signature使用密钥对前两部分进行加密，确保数据完整性f：使用密钥 \u0026quot;secret\u0026quot; 对前两部分进行加密。 分别用点（.）分隔，格式如下：\n1 header.payload.signature 特点 自包含（Self-contained）：JWT 将所有必要的信息（如用户身份、权限等）存储在令牌中，因此服务器无需存储会话信息。它是自包含的。 无状态（Stateless）：JWT 不需要服务器保持会话状态。服务器只需要验证 JWT 的有效性，无需存储用户的任何会话数据。 紧凑性（Compact）：由于采用了 Base64 编码，JWT 非常小且适合在 URL、HTTP 头或 Cookie 中传输。 可验证性（Verifiable）：JWT 的签名部分确保了数据的完整性和真实性。通过公共密钥或共享密钥可以验证 JWT 是否被篡改。 跨域支持：JWT 是通过 HTTP 头部传递的，可以轻松支持跨域请求，在 RESTful API 或微服务架构中非常常见。 缺点 暴露信息：JWT 的有效载荷部分没有加密，任何人都可以解码 JWT 查看其中的信息。如果需要存储敏感信息，必须使用加密的 JWT（JWE）。 不可撤销：一旦 JWT 被签发，直到它过期之前，服务器无法撤销它。因此，如果用户登出或权限改变，需要采用额外的机制来处理。 过期问题：如果没有设置合理的过期时间，JWT 可能会在长时间内有效，这增加了安全风险。 后端需要对外提供一个刷新Token的接口；前端需要实现一个当access Token过期时，自动刷新Token接口获取新Assess Token的拦截器。\nredis实现投票，什么时候用mysql，什么时候用redis\n","date":"2025-03-01T00:00:00Z","permalink":"https://sutdown.github.io/p/go%E9%A1%B9%E7%9B%AE%E5%AE%9E%E6%88%98/","title":"Go项目实战"},{"content":"参考文章：\n在Go语言项目中使用Zap日志库\n在Go语言项目中使用Zap日志库 日志能够提供的功能：\n能够将事件记录到文件中，而不是应用程序控制台。 日志切割-能够根据文件大小、时间或间隔等来切割日志文件。 支持不同的日志级别。例如INFO，DEBUG，ERROR等。 能够打印基本信息，如调用文件/函数名和行号，日志时间等 Go logger 优点：使用简单。\n能够将事件记录到文件中。但是只有print选项，不支持info/debug等多个级别，对于错误日志有fatal（通过调用os.Exit(1)结束程序和panic。不过缺少一个error日志级别。同时不能进行日志格式化，比如记录调用者的函数名和行号；不提供日志切割的能力。\n1 2 3 4 5 6 7 8 9 10 func SetupLogger() { logFileLocation, _ := os.OpenFile(\u0026#34;/Users/q1mi/test.log\u0026#34;, os.O_CREATE|os.O_APPEND|os.O_RDWR, 0744) log.SetOutput(logFileLocation) } func main() { SetupLogger() simpleHttpGet(\u0026#34;www.google.com\u0026#34;) simpleHttpGet(\u0026#34;http://www.google.com\u0026#34;) } Uber-go zap 它同时提供了结构化日志记录和printf风格的日志记录 它非常的快 zap提供两种类型的日志记录器—Sugared Logger和Logger。对性能要求高用logger。\n通过调用zap.NewProduction()/zap.NewDevelopment()或者zap.Example()创建一个Logger。 上面的每一个函数都将创建一个logger。唯一的区别在于它将记录的信息不同。例如production logger默认记录调用函数信息、日期和时间等。 通过Logger调用Info/Error等。 默认情况下日志都会打印到应用程序的console界面。 logger 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 26 27 var logger *zap.Logger func main() { InitLogger() defer logger.Sync() simpleHttpGet(\u0026#34;www.google.com\u0026#34;) simpleHttpGet(\u0026#34;http://www.google.com\u0026#34;) } func InitLogger() { logger, _ = zap.NewProduction() } func simpleHttpGet(url string) { resp, err := http.Get(url) if err != nil { logger.Error( \u0026#34;Error fetching url..\u0026#34;, zap.String(\u0026#34;url\u0026#34;, url), zap.Error(err)) } else { logger.Info(\u0026#34;Success..\u0026#34;, zap.String(\u0026#34;statusCode\u0026#34;, resp.Status), zap.String(\u0026#34;url\u0026#34;, url)) resp.Body.Close() } } **日志记录器。**中MethodXXX是一个可变参数函数，可以是Info / Error/ Debug / Panic等。每个方法都接受一个消息字符串和任意数量的zapcore.Field场参数。每个zapcore.Field其实就是一组键值对参数。\n1 func (log *Logger) MethodXXX(msg string, fields ...Field) Sugared Logger 和logger对比。Sugar Logger 是 Logger 的一个增强版本。它通过一些“糖衣语法”提供了更方便的API，使得日志记录的操作更加简洁和易读。它通常自动处理一些默认配置（如日志格式、日志级别），并且支持更简化的日志记录方法。\n大部分的实现基本都相同。 惟一的区别是，我们通过调用主logger的. Sugar()方法来获取一个SugaredLogger。 然后使用SugaredLogger以printf格式记录语句 定制logger 将日志写入文件而不是终端\n我们将使用zap.New(…)方法来手动传递所有配置，而不是使用像zap.NewProduction()这样的预置方法来创建logger。 1 2 3 4 5 6 7 /* zapcore.Core需要三个配置——Encoder，WriteSyncer，LogLevel * Encoder:编码器(如何写入日志)。我们将使用开箱即用的NewJSONEncoder()，并使用预先设置的ProductionEncoderConfig() * WriterSyncer ：指定日志将写到哪里去。我们使用zapcore.AddSync()函数并且将打开的文件句柄传进去 * Log Level：哪种级别的日志将被写入 */ func New(core zapcore.Core, options ...Option) *Logger 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 26 27 28 29 30 31 32 33 34 35 // 重写上文的initlogger() func InitLogger() { writeSyncer := getLogWriter() encoder := getEncoder() core := zapcore.NewCore(encoder, writeSyncer, zapcore.DebugLevel) // logger := zap.New(core) // 添加将调用函数信息记录到日志中的功能。为此，我们将在zap.New(..)函数中添加一个Option // logger := zap.New(core, zap.AddCaller()) // 当我们不是直接使用初始化好的logger实例记录日志，而是将其包装成一个函数等，此时日录日志的函数调用链会增加，想要获得准确的调用信息就需要通过AddCallerSkip函数来跳过 logger := zap.New(core, zap.AddCaller(), zap.AddCallerSkip(1)) sugarLogger = logger.Sugar() } func getEncoder() zapcore.Encoder { // JSON Encoder：return zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()) // 普通 Encoder：return zapcore.NewConsoleEncoder(zap.NewProductionEncoderConfig()) // 修改默认时间编码器 encoderConfig := zap.NewProductionEncoderConfig() encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder return zapcore.NewConsoleEncoder(encoderConfig) } func getLogWriter() zapcore.WriteSyncer { file, _ := os.Create(\u0026#34;./test.log\u0026#34;) // return zapcore.AddSync(file) // 利用io.MultiWriter支持文件和终端两个输出目标 ws := io.MultiWriter(file, os.Stdout) return zapcore.AddSync(ws) } 有时候我们除了将全量日志输出到xx.log文件中之外，还希望将ERROR级别的日志单独输出到一个名为xx.err.log的日志文件中。我们可以通过以下方式实现。\n1 2 3 4 5 6 7 8 9 10 11 12 func InitLogger() { encoder := getEncoder() // test.log记录全量日志 logF, _ := os.Create(\u0026#34;./test.log\u0026#34;) c1 := zapcore.NewCore(encoder, zapcore.AddSync(logF), zapcore.DebugLevel) // test.err.log记录ERROR级别的日志 errF, _ := os.Create(\u0026#34;./test.err.log\u0026#34;) c2 := zapcore.NewCore(encoder, zapcore.AddSync(errF), zap.ErrorLevel) // 使用NewTee将c1和c2合并到core core := zapcore.NewTee(c1, c2) logger = zap.New(core, zap.AddCaller()) } 使用Lumberjack进行日志切割归档 Zap本身不支持切割归档日志文件\n因此用第三方库Lumberjack来实现。\nzap logger中加入Lumberjack 修改WriteSyncer代码\n1 2 3 4 5 6 7 8 9 10 func getLogWriter() zapcore.WriteSyncer { lumberJackLogger := \u0026amp;lumberjack.Logger{ Filename: \u0026#34;./test.log\u0026#34;, // 日志文件的位置 MaxSize: 10,\t// 在进行切割之前，日志文件的最大大小（以MB为单位） MaxBackups: 5,\t// 保留旧文件的最大个数 MaxAge: 30, // 保留旧文件的最大天数 Compress: false,\t// 是否压缩/归档旧文件 } return zapcore.AddSync(lumberJackLogger) } 使用zap接收gin框架默认的日志并配置日志归档 在gin框架项目中，如何使用zap日志库来接收并记录gin框架默认的日志，并配置日志归档。\ngin框架与日志库：\ngin框架：常用的Go语言Web框架。 日志库：go语言常用的日志库有zap、logrus等。 gin框架默认中间件：\ngin.Default()：使用了两个默认中间件Logger()和Recovery()。 Logger()：将gin框架的日志输出到标准输出。 Recovery()：在程序panic时恢复现场并写入500响应。 基于zap的中间件实现：\nGinLogger：自定义中间件，使用zap记录gin框架的默认日志，包括请求路径、查询参数、请求方法、客户端IP、User-Agent、错误信息、请求耗时等。 GinRecovery：自定义中间件，用于捕获并处理panic，记录错误信息。 日志归档配置（未详细展开）：\n暗示了配置日志归档是文章的一部分内容，但具体实现细节未给出。 Go语言配置管理神器——Viper中文教程 Viper官方文档 — 中文\nViper是 Go 语言中一款功能强大的配置管理库，旨在简化应用程序的配置处理。它支持多种配置文件格式，包括 JSON、TOML、YAML、HCL、envfile 和 Java properties 等。此外，Viper 还提供了从环境变量、命令行标志、远程配置系统（如 etcd 或 Consul）以及直接在代码中设置配置值的功能。go get github.com/spf13/viper\n主要特性：\n多种配置格式支持： Viper 能够读取并解析多种格式的配置文件，如 JSON、TOML、YAML、HCL、envfile 和 Java properties。 优先级管理： Viper 允许开发者设置配置项的优先级，确保高优先级的配置覆盖低优先级的配置。其优先级顺序如下： 通过 viper.Set 显式设置的值。 命令行参数（flags）。 环境变量。 配置文件。 key/value 存储。 默认值。 实时监控配置文件： Viper 支持监控配置文件的变化，并在文件更改时自动重新加载配置。 环境变量支持： Viper 可以从环境变量中读取配置，方便在不同环境下的配置管理。 远程配置系统支持： Viper 能够从远程配置系统（如 etcd 或 Consul）读取配置，并监控其变化。 命令行标志支持： Viper 可以绑定命令行标志，方便在启动时传递配置参数。 默认值设置： Viper 允许为配置项设置默认值，确保在未提供配置时使用预设值。 优雅关机和优雅重启 分别究竟是什么机制。\n优雅地关机或重启 | 李文周的博客\n优雅的关机指服务端关机命令发出后不是立即关机，而是等待当前还在处理的请求全部处理之后再退出程序。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 // 等待中断信号来优雅地关闭服务器，为关闭服务器操作设置一个5秒的超时 quit := make(chan os.Signal, 1) // 创建一个接收信号的通道 // kill 默认会发送 syscall.SIGTERM 信号 // kill -2 发送 syscall.SIGINT 信号，我们常用的Ctrl+C就是触发系统SIGINT信号 // kill -9 发送 syscall.SIGKILL 信号，但是不能被捕获，所以不需要添加它 // signal.Notify把收到的 syscall.SIGINT或syscall.SIGTERM 信号转发给quit signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) // 此处不会阻塞 \u0026lt;-quit // 阻塞在此，当接收到上述两种信号时才会往下执行 log.Println(\u0026#34;Shutdown Server ...\u0026#34;) // 创建一个5秒超时的context ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() // 5秒内优雅关闭服务（将未处理完的请求处理完再关闭服务），超过5秒就超时退出 if err := srv.Shutdown(ctx); err != nil { log.Fatal(\u0026#34;Server Shutdown: \u0026#34;, err) } 优雅的重启\n用 fvbock/endless 来替换默认的 ListenAndServe启动服务来实现，endless 是通过fork子进程处理新请求，待原进程处理完当前请求后再退出的方式实现优雅重启的。所以当你的项目是使用类似supervisor的软件管理进程时就不适用这种方式了。\n1 2 3 4 5 6 7 8 9 // 默认endless服务器会监听下列信号： // syscall.SIGHUP，syscall.SIGUSR1，syscall.SIGUSR2，syscall.SIGINT，syscall.SIGTERM和syscall.SIGTSTP // 接收到 SIGHUP 信号将触发`fork/restart` 实现优雅重启（kill -1 pid会发送SIGHUP信号） // 接收到 syscall.SIGINT或syscall.SIGTERM 信号将触发优雅关机 // 接收到 SIGUSR2 信号将触发HammerTime // SIGUSR1 和 SIGTSTP 被用来触发一些用户自定义的hook函数 if err := endless.ListenAndServe(\u0026#34;:8080\u0026#34;, router); err!=nil{ log.Fatalf(\u0026#34;listen: %s\\n\u0026#34;, err) } MVC模式 MVC 模式 | 菜鸟教程\n","date":"2025-02-27T00:00:00Z","permalink":"https://sutdown.github.io/p/go-web%E5%BC%80%E5%8F%91%E5%B8%B8%E7%94%A8%E7%BB%84%E4%BB%B6/","title":"Go Web开发常用组件"},{"content":" 最近在面试，连续两次关于mysql的基础题都没答上来，当初学的一般，也没有复习，太伤心了，但凡复习一下应该就没问题了/(ㄒoㄒ)/~~。痛定思痛，亡羊补牢。\nMySQL是基于客户机服务器的DBMS。\n主键是对于表中每一行的唯一标识，同一个表中某列代表的主键不能为空，不能重复。\n常见sql语法： 1 2 3 4 5 6 7 SHOW Datebase; SHOW COLUMNS FROM customers; SELECT prod_name FROM products ORDER BY prod_price, prod_name DESC # 如果有这两个键的联合索引，应该就能运行快点 LIMIT 1 OFFSET 2 # 从2开始的第一行，也就是第三行 1 2 3 4 SELECT prod_name, prod_price FROM products WHERE (vend_id = 1002 OR vend_id = 1003) AND prod_price \u0026gt;= 10 # AND的优先级比OR高 WHERE (vend_id IN (1002, 1003)) AND prod_price # IN和OR类似，但是比OR要快 通配符 like匹配整个列，如果被匹配的文本在列值中出现like不会找到它，相应的行也不被返回，除非使用通配符。 % 可以匹配0或者多个任意字符 _ 只能匹配单个字符\n1 2 3 4 5 6 SELECT prod_name FROM products WHERE prod_name LIKE \u0026#39;s%e\u0026#39; # s开头，e结尾的任意字符 # like不能匹配空 WHERE prod_name LIKE \u0026#39;_ ton anvil\u0026#39; 正则表达式 可以解决更加复杂的过滤，MySQL仅仅支持多数正则表达式实现的一个很小的子集。 正则表达式不区分大小写，如果需要大小写，则在REGEXP之后加上BINARY。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 SELECT prod_name FROM products WHERE prod_name REGEXP \u0026#39;1000\u0026#39; # 检索prod_name中包含文本1000的所有行，替代了like ORDER BY prod_name WHERE prod_name REGEXP \u0026#39;.000\u0026#39; # 检索prod_name中包含文本1000,2000,...,9000的所有行，替代了like WHERE prod_name REGEXP \u0026#39;1000|2000\u0026#39; # OR操作 WHERE prod_name REGEXP \u0026#39;[123] Ton\u0026#39; # 匹配 1 Ton或者2 Ton或者3 Ton WHERE prod_name REGEXP \u0026#39;[1-3] Ton\u0026#39; WHERE vend_name REGEXP \u0026#39;\\\\.\u0026#39; # 匹配\u0026#39;.\u0026#39; WHERE prod_name REGEXP \u0026#39;[[:digit:]]{4}\u0026#39; # 匹配连在一起的四个字符 元字符 说明 * 0个或者多个 + 1个或者多个 ？ 0个或者1个 {n} 指定数目的匹配 {n, } 不少于指定数目的匹配 {n, m} 匹配数目的范围（m不超过255） ^ / $ 文本开始 / 文本结束 [[:\u0026lt;:]] / [[:\u0026gt;:]] 词的开始 / 词的结尾 创建计算字段 通俗点可以理解成列。\n拼接字段，使用别名\n1 2 3 SELECT Concat(vend_name, \u0026#39; (\u0026#39;, vend_country), \u0026#39;)\u0026#39;) AS tmp FROM vendors ORDER BY vend_name 使用数据处理函数 处理文本串的文本函数 在数值数据上进行算术操作的数值函数 处理日期和时间值并从这些值中提取特定成分的日期和时间函数 返回DBMS正使用的特殊信息的系统函数\n1 SELECT Upper(vend_name) AS vend_name_upcase 汇总数据 确定表中行数 获得表中行组的和 找出表列的最大值，最小值和平均值\nAVG, COUNT, MAX, MIN, SUM\n分组数据 1 2 3 4 5 6 7 8 9 10 11 SELECT vend_id, COUNT(*) AS num_prods FROM products WHERE GROUP BY vend_id HAVING COUNT(*) \u0026gt;= 2 ORDER BY SELETC order_sum, SUM(quantity*item_price) AS ordertotal FROM orderitems GROUP BY order_num HAVING SUM(quantity*item_price) \u0026gt;= 50 1 2 3 4 5 6 7 SELECT 要返回的列或表达式 FROM 从中检索数据的表 WHERE 行级过滤 GROUP BY 分组说明 HAVING 组级过滤 ORDER BY 输出排序顺序 LIMIT 要检索的行数 使用子查询 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 # 利用子查询过滤 SELECT cust_id FROM orders WHERE order_num IN (SELECT order_num FROM orderitems WHERE prod_id = \u0026#39;TNT2\u0026#39;) # 从customer中检索客户列表，统计其在orders表中的订单数目 # 相关子查询 SELECT cust_name, cust_state, (SELECT COUNT(*) FROM orders WHERE orders.cust_id = customers.cust_id) AS orders FROM customers ORDER BY cust_name 联结表 外键 ：外键为某个表中的一列，它包含另一个表的主键值。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 # 创建联结（默认内连接） SELECT vend_name, prod_name, prod_price FROM vendors, products WHERE vendors.vend_id = products.vend_id ORDER BY vend_name, prod_name # 笛卡尔积 SELECT vend_name, prod_name, prod_price FROM vendors, products ORDER BY vend_name, prod_name # 内部联结 SELECT vend_name, prod_name, prod_price FROM vendors INNER JOIN products ON vendors.vend_id = products.vend_id; 创建高级联结 用自联结而不用子查询自联结通常作为外部语句用来替代从相同表中检索数据时使用的子查询语句。虽然最终的结果是相同的，但有时候处理联结远比处理子查询快得多。应该试一下两种方法，以确定哪一种的性能更好。\n1 2 3 4 5 6 7 8 9 10 11 # 自连结一定程度上可以代替子查询，比如以上两种等价 SELECT prod_id, prod_name FROM products WHERE vend_id = (SELECT vend_id FROM products WHERE prod_id = \u0026#39;DTNTR\u0026#39;) SELECT p1.prod_id, p1.prod_name FROM product AS p1, product AS p2 WHERE p1.vend_id = p2.vend_id AND p2.prod_id = \u0026#39;DTNTR\u0026#39; 无论何时对表进行联结，应该至少有一个列出现在不止一个表中。 迄今为止我们的每个内联结都是自然联结\n外部联结\n1 2 3 4 5 6 7 8 9 10 11 SELECT customers.cust_id, orders.order_num FROM customers INNER JOIN orders ON customers.cust_id = orders.cust_id SELECT customers.cust_id, orders.order_num FROM customers LEFT OUTER JOIN orders ON customers.cust_id = orders.cust_id SELECT customers.cust_id, orders.order_num FROM customers LEFT RIGHT JOIN orders ON customers.cust_id = orders.cust_id 组合查询 多数sql查询都只包含从一个或者多个表返回单挑SELECT语句。MySQL也允许执行多个查询，将结果作为单个查询结果集返回，适用情况： 在单个查询中，从不同的表返回类似结构的数据 对单个表执行多个查询，按照单个查询返回数据\n1 2 3 4 5 6 7 8 SELECT vend_id, prod_id, prod_price FROM products WHERE prod_price \u0026lt;= 5 UNION SELECT vend_id, prod_id, prod_price FROM products WHERE vend_id IN (1001, 1002) ORDER BY vend_id, prod_price UNION规则：\n必须由两条或两条以上的SELECT语句组成，语句之间用UNION分割 每个查询中必须包含相同的列，表达式，聚集函数 列数据类型必须兼容 自动去重，UNION ALL不会自动去重 全文本搜索 MyISAM支持全文本搜索，InnoDB不支持。\n全文本索引需要在创建表时指定FULLTEXT或者稍后指定。（会对结果自动排序）\n1 2 3 4 SELECT note_text FROM productnotes WHERE Match(note_text) Against(\u0026#39;rabbit\u0026#39;) # 可以和like子句等同 插入数据 1 2 3 4 5 6 7 INSERT INTO Customers(cust_name, cust_contact, cust_email) VALUES(NULL, NULL, NULL), (\u0026#39;Amy\u0026#39;, \u0026#39;12345\u0026#39;, \u0026#39;xxx@gmail.com\u0026#39;) # 最好提供插入时需要匹配的列名，同时和相应的值对应 # 一般不要使用没有明确给出列的列表的INSERT语句，使用列的列表能使SQL代码继续发挥作用，即使表结构发生变化 # 插入检索中的数据 INSERT INTO customers(cust_id, cust_contact, cust_email) SELECT cust_id, cust_contact, cust_email FROM custnew 更新和删除数据 1 2 3 4 5 6 7 8 # 更新表的内容 UPDATE customers SET cust_email = \u0026#39;xxx@fudd.com\u0026#39; WHERE cust_id = 10002 # 删除表的内容 DELETE FROM customers WHERE cust_id = 10002 创建和操纵表 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 CREATE TABLE customers ( cust_id int NOT NULL AUTO_INCREMENT, cust_name char(50) NOT NULL, PRIMARY KEY (cust_id) )ENGINE=InnoDB # 更新表 ALTER TABLE vendors ADD vend_phone CHAR(20) ALTER TABLE orders ADD CONSTRAINT fk_orders_customers FOREIGN KEY (cust_id) REFERENCES customers (cust_id) # 删除表 DROP TABLE customers # 重命名表 RENAME TABLE customers2 TO customers 表中的每个行必须具有唯一的主键值，如果主键使用单个列或多个列，单个列的值或者多个列的组合值必须唯一。 引擎： InnoDB 可靠的事务处理引擎，不支持全文本搜索 MEMORY 功能上等同于MyISAM，但由于数据存储在内存中，速度很快，适用于临时表 MyISAM 性能很高的引擎，支持全文本搜索 使用视图 视图本身并不包含数据，仅仅用来查看存储在别处的数据的一种设施。如果用多个连结和过滤创建了复杂的视图或者嵌套了视图，性能会下降的很厉害。因此在部署使用了大量视图的应用前，应该先进行测试。\n1 2 3 4 5 6 7 8 CREATE VIEW productcustomers AS SELECT cust_name, cust_contact, prod_id FROM customers, orders, orderitems WHERE customers.cust_id = orders.cust_id AND orderitems.order_num = orders.order_num SELECT * FROM productcustomers 大多视图都是不可更新的。\n如果视图定义中有以下操作，则不能进行更新：\n分组，联结，子查询，并，聚集函数，DISTINCT，导出计算列 创建存储过程 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 # 基础操作 CREATE PROCEDURE productpricing(OUT pa DECIMAL(8,2)) BEGIN SELECT AVG(prod_price) AS priceaverage FROM products END CALL productpricing(@priceaverage) SELECT @priceaverage DROP PROCEDURE productpricing SHOW CREATE PROCEDURE ordertotal # 创建游标 DECLARE ordernumbers CURSOR FOR SELECT order_num FROM orders ","date":"2025-02-23T00:00:00Z","permalink":"https://sutdown.github.io/p/mysql%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/","title":"mysql必知必会"},{"content":"Go操作MySQL 连接 Go语言中的database/sql包提供了保证SQL或类SQL数据库的泛用接口，并不提供具体的数据库驱动，使用该包时至少注入一个数据库驱动。\n初始化连接：Ping()\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 go get -u github.com/go-sql-driver/mysql // 下载依赖 func Open(driverName, dataSourceName string) (*DB, error) // 打开指定数据库，指定数据源 void db *sql.DB func initDB() (err error) { // 格式校验 dsn := \u0026#34;root:root1234@tcp(127.0.0.1:13306)/sql_demo\u0026#34; db, err = sql.Open(\u0026#34;mysql\u0026#34;, dsn) if err!= nil { panic(err) } // 做完错误检查之后，确保db不为nil // CLose() 用来释放数据库连接相关的资源 defer db.Close() // 初始化连接 err = db.Ping() if err != nil { return err } db.SetConnMaxLifetime(time.Second*10) db.SetMaxOpenConns(200) db.SetMaxIdleConns(1) return nil } func main() { if err :=initDB(); err != nil { fmt.Printf(\u0026#34;connect to db failed, err:%v\\n\u0026#34;, error) return } fmt.Println(\u0026#34;connect to db success\u0026#34;) } 其中sql.DB是表示连接的数据库对象（结构体实例），它保存了连接数据库相关的所有信息。它内部维护着一个具有零到多个底层连接的连接池，它可以安全地被多个goroutine同时使用。\n1 2 3 4 5 // SetMaxOpenConns设置与数据库建立连接的最大数目。 如果n大于0且小于最大闲置连接数，会将最大闲置连接数减小到匹配最大开启连接数的限制。 如果n\u0026lt;=0，不会限制最大开启连接数，默认为0（无限制）。 func (db *DB) SetMaxOpenConns(n int) // SetMaxIdleConns设置连接池中的最大闲置连接数。 如果n大于最大开启连接数，则新的最大闲置连接数会减小到匹配最大开启连接数的限制。 如果n\u0026lt;=0，不会保留闲置连接。 func (db *DB) SetMaxIdleConns(n int) CRUD mySQL\n1 2 3 4 5 6 7 8 CREATE DATABASE sql_test; use sql_test; CREATE TABLE `user` ( `id` BIGINT(20) NOT NULL AUTO_INCREMENT, `name` VARCHAR(20) DEFAULT \u0026#39;\u0026#39;, `age` INT(11) DEFAULT \u0026#39;0\u0026#39;, PRIMARY KEY(`id`) )ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4; 单行查询\nQueryRow会在连接池里调用连接，达到最大连接数后停止，scan()会关闭连接。\n1 func (db *DB) QueryRow(query string, args ...interface{}) *Row 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 type user struct { id int age int name string } func queryROwDemo() { sqlStr := \u0026#34;select id, name, age from user where id=?\u0026#34; var u user row := db.QueryRow(sqlStr, 1) err := row.Scan(\u0026amp;u.id, \u0026amp;u.name, \u0026amp;u.age) // 关闭连接 if err!=nil { fmt.Printf(\u0026#34;scan failed, err:%v\\n\u0026#34;, err) return } } 多行查询\n1 func (db *DB) Query(query string, args ...interface{}) (*Rows, error) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 // 查询多条数据示例 func queryMultiRowDemo() { sqlStr := \u0026#34;select id, name, age from user where id \u0026gt; ?\u0026#34; rows, err := db.Query(sqlStr, 0) if err != nil { fmt.Printf(\u0026#34;query failed, err:%v\\n\u0026#34;, err) return } // 非常重要：关闭rows释放持有的数据库链接 defer rows.Close() // 循环读取结果集中的数据 for rows.Next() { var u user err := rows.Scan(\u0026amp;u.id, \u0026amp;u.name, \u0026amp;u.age) if err != nil { fmt.Printf(\u0026#34;scan failed, err:%v\\n\u0026#34;, err) return } fmt.Printf(\u0026#34;id:%d name:%s age:%d\\n\u0026#34;, u.id, u.name, u.age) } } 插入数据，更新数据\n1 2 3 4 func (db *DB) Exec(query string, args ...interface{}) (Result, error) LastInsertId() RowsAffected() MySQL预处理 什么是预处理\n普通SQL语句执行过程：\n客户端对SQL语句进行占位符替换得到完整的SQL语句。 客户端发送完整SQL语句到MySQL服务端 MySQL服务端执行完整的SQL语句并将结果返回给客户端。 预处理执行过程：\n把SQL语句分成两部分，命令部分与数据部分。 先把命令部分发送给MySQL服务端，MySQL服务端进行SQL预处理。 然后把数据部分发送给MySQL服务端，MySQL服务端对SQL语句进行占位符替换。 MySQL服务端执行完整的SQL语句并将结果返回给客户端。 为什么要预处理？\n优化MySQL服务器重复执行SQL的方法，可以提升服务器性能，提前让服务器编译，一次编译多次执行，节省后续编译的成本。 避免SQL注入问题。 Go实现MySQL预处理\nPrepare方法会先将sql语句发送给MySQL服务端，返回一个准备好的状态用于之后的查询和命令。返回值可以同时执行多个查询和命令。\n1 2 // database/sql func (db *DB) Prepare(query string) (*Stmt, error) 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 26 27 // 预处理查询示例 func prepareQueryDemo() { sqlStr := \u0026#34;select id, name, age from user where id \u0026gt; ?\u0026#34; stmt, err := db.Prepare(sqlStr) if err != nil { fmt.Printf(\u0026#34;prepare failed, err:%v\\n\u0026#34;, err) return } defer stmt.Close() rows, err := stmt.Query(0) if err != nil { fmt.Printf(\u0026#34;query failed, err:%v\\n\u0026#34;, err) return } defer rows.Close() // 遍历结果集的每一行 for rows.Next() { var u user // Scan 方法用于从数据库查询结果中提取数据并存储到变量中。 err := rows.Scan(\u0026amp;u.id, \u0026amp;u.name, \u0026amp;u.age) if err != nil { fmt.Printf(\u0026#34;scan failed, err:%v\\n\u0026#34;, err) return } fmt.Printf(\u0026#34;id:%d name:%s age:%d\\n\u0026#34;, u.id, u.name, u.age) } } SQL注入\n我们任何时候都不应该自己拼接SQL语句！\nGo实现MySQL事务 在MySQL中只有使用了Innodb数据库引擎的数据库或表才支持事务。事务处理可以用来维护数据库的完整性，保证成批的SQL语句要么全部执行，要么全部不执行。事务必须满足4个条件（ACID）：原子性（Atomicity，或称不可分割性）、一致性（Consistency）、隔离性（Isolation，又称独立性）、持久性（Durability）。\n事务相关方法\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 func (db *DB) Begin() (*Tx, error) // 开始事务 func (tx *Tx) Commit() error\t// 提交事务 func (tx *Tx) Rollback() error\t// 回滚事务 // 实例 // 事务操作示例 func transactionDemo() { tx, err := db.Begin() // 开启事务 if err != nil { if tx != nil { tx.Rollback() // 回滚 } fmt.Printf(\u0026#34;begin trans failed, err:%v\\n\u0026#34;, err) return } sqlStr1 := \u0026#34;Update user set age=30 where id=?\u0026#34; ret1, err := tx.Exec(sqlStr1, 2) if err != nil { tx.Rollback() // 回滚 fmt.Printf(\u0026#34;exec sql1 failed, err:%v\\n\u0026#34;, err) return } affRow1, err := ret1.RowsAffected() if err != nil { tx.Rollback() // 回滚 fmt.Printf(\u0026#34;exec ret1.RowsAffected() failed, err:%v\\n\u0026#34;, err) return } sqlStr2 := \u0026#34;Update user set age=40 where id=?\u0026#34; ret2, err := tx.Exec(sqlStr2, 3) if err != nil { tx.Rollback() // 回滚 fmt.Printf(\u0026#34;exec sql2 failed, err:%v\\n\u0026#34;, err) return } affRow2, err := ret2.RowsAffected() if err != nil { tx.Rollback() // 回滚 fmt.Printf(\u0026#34;exec ret1.RowsAffected() failed, err:%v\\n\u0026#34;, err) return } fmt.Println(affRow1, affRow2) if affRow1 == 1 \u0026amp;\u0026amp; affRow2 == 1 { fmt.Println(\u0026#34;事务提交啦...\u0026#34;) tx.Commit() // 提交事务 } else { tx.Rollback() fmt.Println(\u0026#34;事务回滚啦...\u0026#34;) } fmt.Println(\u0026#34;exec trans success!\u0026#34;) } sqlx库使用指南 连接 1 go get github.com/jmoiron/sqlx # 下载sqlx依赖 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import ( \u0026#34;fmt\u0026#34; _ \u0026#34;github.com/go-sql-driver/mysql\u0026#34; // 不要忘了导入数据库驱动 \u0026#34;github.com/jmoiron/sqlx\u0026#34; ) var db *sqlx.DB func initDB() (err error) { dsn := \u0026#34;user:password@tcp(127.0.0.1:3306)/sql_test?charset=utf8mb4\u0026amp;parseTime=True\u0026#34; // 也可以使用MustConnect连接不成功就panic db, err = sqlx.Connect(\u0026#34;mysql\u0026#34;, dsn) if err != nil { fmt.Printf(\u0026#34;connect DB failed, err:%v\\n\u0026#34;, err) return } db.SetMaxOpenConns(20) db.SetMaxIdleConns(10) return } CRUD 查询\n1 2 err := db.Get(\u0026amp;u, sqlStr, 1)\t// select id, name, age from user where id=? err := db.Select(\u0026amp;users, sqlStr, 0)\t// select id, name, age from user where id \u0026gt; ? 插入，更新和删除\n1 2 3 4 5 6 7 8 9 /* Exec：用于执行不返回行数据的SQL语句，例如INSERT、UPDATE和DELETE。 */ ret, err := db.Exec(sqlStr, \u0026#34;沙河小王子\u0026#34;, 19) // insert into user(name, age) values (?,?) ret, err := db.Exec(sqlStr, 39, 6)\t// update user set age=? where id = ? ret, err := db.Exec(sqlStr, 6)\t// delete from user where id = ? /* 用于执行SQL语句，并使用命名参数（结构体或map）替代?占位符 */ // DB.NamedExec方法用来绑定SQL语句与结构体或map中的同名字段。 _, err = db.NamedExec(sqlStr, map[string]interface{}{ \u0026#34;name\u0026#34;: \u0026#34;七米\u0026#34;, \u0026#34;age\u0026#34;: 28, })\t// INSERT、UPDATE、DELETE rows, err := db.NamedQuery(sqlStr, map[string]interface{}{ \u0026#34;name\u0026#34;: \u0026#34;七米\u0026#34; })\t// select 事务操作 可以使用sqlx中提供的db.Beginx()和tx.Exec()方法。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 func transactionDemo2()(err error) { tx, err := db.Beginx() // 开启事务 if err != nil { fmt.Printf(\u0026#34;begin trans failed, err:%v\\n\u0026#34;, err) return err } defer func() { if p := recover(); p != nil { tx.Rollback() // 捕获到panic，回滚 panic(p) // re-throw panic after Rollback } else if err != nil { fmt.Println(\u0026#34;rollback\u0026#34;)\t// 发生一个预期的错误，回滚 tx.Rollback() // err is non-nil; don\u0026#39;t change it } else { err = tx.Commit() // err is nil; if Commit returns error update err fmt.Println(\u0026#34;commit\u0026#34;) // 没有任何错误 } }() sqlStr1 := \u0026#34;Update user set age=20 where id=?\u0026#34; rs, err := tx.Exec(sqlStr1, 1) if err!= nil{ return err } n, err := rs.RowsAffected() if err != nil { return err } if n != 1 { return errors.New(\u0026#34;exec sqlStr1 failed\u0026#34;) } sqlStr2 := \u0026#34;Update user set age=50 where i=?\u0026#34; rs, err = tx.Exec(sqlStr2, 5) if err!=nil{ return err } n, err = rs.RowsAffected() if err != nil { return err } if n != 1 { return errors.New(\u0026#34;exec sqlStr1 failed\u0026#34;) } return err } sqlx.In的批量插入实例 bindvars（绑定变量）\n查询占位符?在内部称为bindvars（查询占位符）,它非常重要。你应该始终使用它们向数据库发送值，因为它们可以防止SQL注入攻击。database/sql不尝试对查询文本进行任何验证；它与编码的参数一起按原样发送到服务器。除非驱动程序实现一个特殊的接口，否则在执行之前，查询是在服务器上准备的。因此bindvars是特定于数据库的:\nMySQL中使用? PostgreSQL使用枚举的$1、$2等bindvar语法 SQLite中?和$1的语法都支持 Oracle中使用:name的语法 bindvars的一个常见误解是，它们用来在sql语句中插入值。它们其实仅用于参数化，不允许更改SQL语句的结构。\n1 2 3 4 5 // ？不能用来插入表名（做SQL语句中表名的占位符） db.Query(\u0026#34;SELECT * FROM ?\u0026#34;, \u0026#34;mytable\u0026#34;) // ？也不能用来插入列名（做SQL语句中列名的占位符） db.Query(\u0026#34;SELECT ?, ? FROM people\u0026#34;, \u0026#34;name\u0026#34;, \u0026#34;location\u0026#34;) 自己拼接语句实现批量插入\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 // BatchInsertUsers 自行构造批量插入的语句 func BatchInsertUsers(users []*User) error { // 存放 (?, ?) 的slice valueStrings := make([]string, 0, len(users)) // 存放values的slice valueArgs := make([]interface{}, 0, len(users) * 2) // 遍历users准备相关数据 for _, u := range users { // 此处占位符要与插入值的个数对应 valueStrings = append(valueStrings, \u0026#34;(?, ?)\u0026#34;) valueArgs = append(valueArgs, u.Name) valueArgs = append(valueArgs, u.Age) } // 自行拼接要执行的具体语句 stmt := fmt.Sprintf(\u0026#34;INSERT INTO user (name, age) VALUES %s\u0026#34;, strings.Join(valueStrings, \u0026#34;,\u0026#34;)) _, err := DB.Exec(stmt, valueArgs...) return err } 使用sqlx.In实现批量插入\n结构体实现driver.Valuer接口，参数通常为切片\nNamedExec 更适合处理结构化的输入（如结构体或命名字段的 map），其优势在于能自动匹配结构体字段与 SQL 语句中的命名参数。映射更清晰，但是结构体也更复杂。 sqlx.In 则更适合处理位置参数，适合用于将一组数据传递给 SQL 语句，并自动生成批量插入的 SQL 语句。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 func (u User) Value() (driver.Value, error) { return []interface{}{u.Name, u.Age}, nil } // BatchInsertUsers2 使用sqlx.In帮我们拼接语句和参数, 注意传入的参数是[]interface{} func BatchInsertUsers2(users []interface{}) error { query, args, _ := sqlx.In( \u0026#34;INSERT INTO user (name, age) VALUES (?), (?), (?)\u0026#34;, users..., // 如果arg实现了 driver.Valuer, sqlx.In 会通过调用 Value()来展开它 ) fmt.Println(query) // 查看生成的querystring fmt.Println(args) // 查看生成的args _, err := DB.Exec(query, args...) // 执行查询 return err } 使用NameExec实现批量插入\n1 2 3 4 5 6 // BatchInsertUsers3 使用NamedExec实现批量插入 // 参数通常是结构体或者map func BatchInsertUsers3(users []*User) error { _, err := DB.NamedExec(\u0026#34;INSERT INTO user (name, age) VALUES (:name, :age)\u0026#34;, users) return err } sqlx.In的查询示例\n关于sqlx.In这里再补充一个用法，在sqlx查询语句中实现In查询和FIND_IN_SET函数。即实现SELECT * FROM user WHERE id in (3, 2, 1);和SELECT * FROM user WHERE id in (3, 2, 1) ORDER BY FIND_IN_SET(id, '3,2,1');。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 // in查询 查询id在给定id集合中的数据。 // QueryByIDs 根据给定ID查询 func QueryByIDs(ids []int)(users []User, err error){ // 动态填充id query, args, err := sqlx.In(\u0026#34;SELECT name, age FROM user WHERE id IN (?)\u0026#34;, ids) if err != nil { return } // sqlx.In 返回带 `?` bindvar的查询语句, 我们使用Rebind()重新绑定。 // 重新生成对应数据库的查询语句（如PostgreSQL 用 `$1`, `$2` bindvar） query = DB.Rebind(query) err = DB.Select(\u0026amp;users, query, args...) return } // in查询和FIND_IN_SET函数 查询id在给定id集合的数据并维持给定id集合的顺序。 // QueryAndOrderByIDs 按照指定id查询并维护顺序 func QueryAndOrderByIDs(ids []int)(users []User, err error){ // 动态填充id strIDs := make([]string, 0, len(ids)) for _, id := range ids { strIDs = append(strIDs, fmt.Sprintf(\u0026#34;%d\u0026#34;, id)) } query, args, err := sqlx.In(\u0026#34;SELECT name, age FROM user WHERE id IN (?) ORDER BY FIND_IN_SET(id, ?)\u0026#34;, ids, strings.Join(strIDs, \u0026#34;,\u0026#34;)) if err != nil { return } // sqlx.In 返回带 `?` bindvar的查询语句, 我们使用Rebind()重新绑定它 query = DB.Rebind(query) err = DB.Select(\u0026amp;users, query, args...) return } go-redis redis介绍 Redis（Remote Dictionary Server）是一个开源的内存数据库，支持键值（Key-Value）存储，可以用作缓存、消息队列、分布式锁等。它的特点是高性能、支持多种数据结构、持久化、分布式，适用于各种高并发场景。\n1 2 docker run --name redis507 -p 6379:6379 -d redis:5.0.7 // 名为 redis507 的 5.0.7 版本的 redis server环境。 docker run -it --network host --rm redis:5.0.7 redis-cli // 启动一个 redis-cli 连接上面的 redis server。 go-redis库 go-redis 这个库来操作 Redis 数据库。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 import \u0026#34;github.com/redis/go-redis/v9\u0026#34; // 普通连接 rdb := redis.NewClient(\u0026amp;redis.Options{ Addr: \u0026#34;localhost:6379\u0026#34;, Password: \u0026#34;\u0026#34;, // 密码 DB: 0, // 数据库 PoolSize: 20, // 连接池大小 }) // 解析数据源字符串 opt, err := redis.ParseURL(\u0026#34;redis://\u0026lt;user\u0026gt;:\u0026lt;pass\u0026gt;@localhost:6379/\u0026lt;db\u0026gt;\u0026#34;) if err != nil { panic(err) } rdb := redis.NewClient(opt) // TLS连接 rdb := redis.NewClient(\u0026amp;redis.Options{ TLSConfig: \u0026amp;tls.Config{ MinVersion: tls.VersionTLS12, // Certificates: []tls.Certificate{cert}, // ServerName: \u0026#34;your.domain.com\u0026#34;, }, }) // Redis Sentinel模式 rdb := redis.NewFailoverClient(\u0026amp;redis.FailoverOptions{ MasterName: \u0026#34;master-name\u0026#34;, SentinelAddrs: []string{\u0026#34;:9126\u0026#34;, \u0026#34;:9127\u0026#34;, \u0026#34;:9128\u0026#34;}, }) // Redis Cluster模式 rdb := redis.NewClusterClient(\u0026amp;redis.ClusterOptions{ Addrs: []string{\u0026#34;:7000\u0026#34;, \u0026#34;:7001\u0026#34;, \u0026#34;:7002\u0026#34;, \u0026#34;:7003\u0026#34;, \u0026#34;:7004\u0026#34;, \u0026#34;:7005\u0026#34;}, // 若要根据延迟或随机路由命令，请启用以下命令之一 // RouteByLatency: true, // RouteRandomly: true, }) 基本操作\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 // doCommand go-redis基本使用示例 func doCommand() { ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) defer cancel() // 执行命令获取结果 val, err := rdb.Get(ctx, \u0026#34;key\u0026#34;).Result() fmt.Println(val, err) // 先获取到命令对象 cmder := rdb.Get(ctx, \u0026#34;key\u0026#34;) fmt.Println(cmder.Val()) // 获取值 fmt.Println(cmder.Err()) // 获取错误 // 直接执行命令获取错误 err = rdb.Set(ctx, \u0026#34;key\u0026#34;, 10, time.Hour).Err() // 直接执行命令获取值 value := rdb.Get(ctx, \u0026#34;key\u0026#34;).Val() fmt.Println(value) } go-redis 还提供了一个执行任意命令或自定义命令的 Do 方法，特别是一些 go-redis 库暂时不支持的命令都可以使用该方法执行。具体使用方法如下。\n1 2 3 4 5 6 7 8 9 10 11 12 13 // doDemo rdb.Do 方法使用示例 func doDemo() { ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) defer cancel() // 直接执行命令获取错误 err := rdb.Do(ctx, \u0026#34;set\u0026#34;, \u0026#34;key\u0026#34;, 10, \u0026#34;EX\u0026#34;, 3600).Err() fmt.Println(err) // 执行命令获取结果 val, err := rdb.Do(ctx, \u0026#34;get\u0026#34;, \u0026#34;key\u0026#34;).Result() fmt.Println(val, err) } 扫描or遍历所有key\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 vals, err := rdb.Keys(ctx, \u0026#34;user:*\u0026#34;).Result() // 将redis中所有以prefix:为前缀的key都扫描出来 keys, cursor, err = rdb.Scan(ctx, cursor, \u0026#34;prefix:*\u0026#34;, 0).Result() // 但是如果需要扫描数百万的 key ，那速度就会比较慢。这种场景下你可以使用Scan命令来遍历所有符合要求的 key。 keys, cursor, err = rdb.Scan(ctx, cursor, \u0026#34;prefix:*\u0026#34;, 0).Result() if err != nil { panic(err) } // 针对这种需要遍历大量key的场景，go-redis中提供了一个简化方法——Iterator // delKeysByMatch 按match格式扫描所有key并删除 func delKeysByMatch(match string, timeout time.Duration) { ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() iter := rdb.Scan(ctx, 0, match, 0).Iterator() for iter.Next(ctx) { err := rdb.Del(ctx, iter.Val()).Err() if err != nil { panic(err) } } if err := iter.Err(); err != nil { panic(err) } } Redis Pipeline Redis Pipeline 允许通过使用单个 client-server-client 往返执行多个命令来提高性能。区别于一个接一个地执行100个命令，你可以将这些命令放入 pipeline 中，然后使用1次读写操作像执行单个命令一样执行它们。这样做的好处是节省了执行命令的网络往返时间（RTT）。\npipeline和exec的区别是什么\nPipeline：用于将多个命令打包并发送到 Redis，但并不立即执行命令。它是将命令排队的过程。\nExec：用于执行 pipeline 中的命令，最终将这些命令发送到 Redis 服务器，并等待其执行结果。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 /* exec */ pipe := rdb.Pipeline() incr := pipe.Incr(ctx, \u0026#34;pipeline_counter\u0026#34;) pipe.Expire(ctx, \u0026#34;pipeline_counter\u0026#34;, time.Hour) cmds, err := pipe.Exec(ctx) if err != nil { panic(err) } // 在执行pipe.Exec之后才能获取到结果 fmt.Println(incr.Val()) /* pipeline */ var incr *redis.IntCmd cmds, err := rdb.Pipelined(ctx, func(pipe redis.Pipeliner) error { incr = pipe.Incr(ctx, \u0026#34;pipelined_counter\u0026#34;) pipe.Expire(ctx, \u0026#34;pipelined_counter\u0026#34;, time.Hour) return nil }) if err != nil { panic(err) } // 在pipeline执行后获取到结果 fmt.Println(incr.Val()) 事务 Redis 是单线程执行命令的，因此单个命令始终是原子的，但是来自不同客户端的两个给定命令可以依次执行，例如在它们之间交替执行。但是，Multi/exec能够确保在multi/exec两个语句之间的命令之间没有其他客户端正在执行命令。\n在这种场景我们需要使用 TxPipeline 或 TxPipelined 方法将 pipeline 命令使用 MULTI 和EXEC包裹起来。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 // TxPipeline demo pipe := rdb.TxPipeline() incr := pipe.Incr(ctx, \u0026#34;tx_pipeline_counter\u0026#34;) pipe.Expire(ctx, \u0026#34;tx_pipeline_counter\u0026#34;, time.Hour) _, err := pipe.Exec(ctx) fmt.Println(incr.Val(), err) // TxPipelined demo var incr2 *redis.IntCmd _, err = rdb.TxPipelined(ctx, func(pipe redis.Pipeliner) error { incr2 = pipe.Incr(ctx, \u0026#34;tx_pipeline_counter\u0026#34;) pipe.Expire(ctx, \u0026#34;tx_pipeline_counter\u0026#34;, time.Hour) return nil }) fmt.Println(incr2.Val(), err) watch\n我们通常搭配 WATCH命令来执行事务操作。从使用WATCH命令监视某个 key 开始，直到执行EXEC命令的这段时间里，如果有其他用户抢先对被监视的 key 进行了替换、更新、删除等操作，那么当用户尝试执行EXEC的时候，事务将失败并返回一个错误，用户可以根据这个错误选择重试事务或者放弃事务。\nWatch方法接收一个函数和一个或多个key作为参数。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 Watch(fn func(*Tx) error, keys ...string) error // watchDemo 在key值不变的情况下将其值+1 func watchDemo(ctx context.Context, key string) error { return rdb.Watch(ctx, func(tx *redis.Tx) error { n, err := tx.Get(ctx, key).Int() if err != nil \u0026amp;\u0026amp; err != redis.Nil { return err } // 假设操作耗时5秒 // 5秒内我们通过其他的客户端修改key，当前事务就会失败 time.Sleep(5 * time.Second) _, err = tx.TxPipelined(ctx, func(pipe redis.Pipeliner) error { pipe.Set(ctx, key, n+1, time.Hour) return nil }) return err }, key) } last，go-redis 官方文档中使用 GET 、SET和WATCH命令实现一个 INCR 命令的完整示例。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 // 此处rdb为初始化的redis连接客户端 const routineCount = 100 // 设置5秒超时 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() // increment 是一个自定义对key进行递增（+1）的函数 // 使用 GET + SET + WATCH 实现，类似 INCR increment := func(key string) error { txf := func(tx *redis.Tx) error { // 获得当前值或零值 n, err := tx.Get(ctx, key).Int() if err != nil \u0026amp;\u0026amp; err != redis.Nil { return err } // 实际操作（乐观锁定中的本地操作） n++ // 仅在监视的Key保持不变的情况下运行 _, err = tx.TxPipelined(ctx, func(pipe redis.Pipeliner) error { // pipe 处理错误情况 pipe.Set(ctx, key, n, 0) return nil }) return err } // 最多重试100次 for retries := routineCount; retries \u0026gt; 0; retries-- { err := rdb.Watch(ctx, txf, key) if err != redis.TxFailedErr { return err } // 乐观锁丢失 } return errors.New(\u0026#34;increment reached maximum number of retries\u0026#34;) } // 开启100个goroutine并发调用increment // 相当于对key执行100次递增 var wg sync.WaitGroup wg.Add(routineCount) for i := 0; i \u0026lt; routineCount; i++ { go func() { defer wg.Done() if err := increment(\u0026#34;counter3\u0026#34;); err != nil { fmt.Println(\u0026#34;increment error:\u0026#34;, err) } }() } wg.Wait() n, err := rdb.Get(ctx, \u0026#34;counter3\u0026#34;).Int() fmt.Println(\u0026#34;最终结果：\u0026#34;, n, err) ","date":"2025-02-18T00:00:00Z","permalink":"https://sutdown.github.io/p/go%E6%93%8D%E4%BD%9C%E5%B8%B8%E8%A7%81%E6%95%B0%E6%8D%AE%E5%BA%93/","title":"Go操作常见数据库"},{"content":"普通索引和唯一索引 普通索引：找到满足条件的第一个记录之后，会查找下一个记录，直到不满足要求。 唯一索引：查找到第一个满足条件的记录后，就会停止继续检索。 两种索引的性能差距微乎其微。当需要读一条记录的时候，以页为单位，整体从磁盘读入内存。在InnoDB中，每个数据页的大小默认16KB，当找k=5的记录，它所在的数据页就都在内存里，对于普通索引，要多做一次“查找和判断下一条记录”的操作，就只需要一次指针寻找和一次计算；如果k=5这个记录刚好是这个数据页的最后一个记录，那么要取下一个记录，必须读取下一个数据页，这个操作会稍微复杂一些。\nchange buffer：当更新数据页时，如果数据页不在内存中，不影响数据一致性的情况下，innoDB会将该更新操作缓存在change buffer中，下次需要访问该数据页时，将其读入内存，执行change buffer中与这个页相关操作。\n对于唯一索引来说，所有的更新操作都要先判断这个操作是否违反唯一性约束。唯一索引的更新就不能使用change buffer，因为唯一索引一定要读入内存看有没有冲突，普通索引则是会将更新记录在change buffer，减少了磁盘的随机访问，提升部分性能。\n举个例子：\n某个业务的库内存命中率突然从99%降低到了75%，整个系统处于阻塞状态，更新语句全部堵住。而探究其原因后，这个业务有大量插入数据的操作，而某员工在前一天把其中的某个普通索引改成了唯一索引\nchange buffer使用较好的场景是写多读少，比如账单累，日志类。它在读时容易触发merge过程，频繁的读操作不会减少访问IO的次数，反而增加了change buffer的维护代价。\nQ：change buffer一开始是写内存的，那么如果这个时候机器掉电重启，会不会导致change buffer丢失呢？change buffer丢失可不是小事儿，再从磁盘读入数据可就没有了merge过程，就等于是数据丢失了。\nA：虽然是只更新内存，但是在事务提交的时候，我们把change buffer的操作也记录到redo log里了，所以崩溃恢复的时候，change buffer也能找回来。\nMySQL为什么有时候会选错索引 选择索引是优化器的工作。优化器选择索引的目的，是找到一个最优的执行方案，并用最小的代价去执行语句。在数据库里面，扫描行数是影响执行代价的因素之一。扫描的行数越少，意味着访问磁盘数据的次数越少，消耗的CPU资源越少。\n既然是统计信息不对，那就修正。analyze table t 命令，可以用来重新统计索引信息。\n解决方案：\n采用force index强行选择一个索引。 修改语句，引导MySQL使用我们期望的索引。 新建一个更合适的索引，来提供给优化器做选择，或删掉误用的索引。 为什么会选错索引\n如何给字符串字段加索引 直接创建完整索引，这样可能比较占用空间； 创建前缀索引，节省空间，但会增加查询扫描次数，并且不能使用覆盖索引； 倒序存储，再创建前缀索引，用于绕过字符串本身前缀的区分度不够的问题； 创建hash字段索引，查询性能稳定，有额外的存储和计算消耗，跟第三种方式一样，都不支持范围扫描。 为什么Mysql会抖动 内存中会存储信息，在空闲时刷新到磁盘，占用适当资源，因此会抖动。\n为什么表数据删掉一半，表文件大小不变 也就是删除了表但是没有回收空间，其中的原理需要从删除流程看起：删除其实是删除B+树的结点，回收空间过于的复杂，只会标记这个空间可以复用。\n重建表则是处理掉类似于这种只标记而没有存放实际有效数据的空间，以达到收缩表的目的。重建也是重新建立一个新表遍历原来的表，具体流程如下：\n建立一个临时文件，扫描表A主键的所有数据页； 用数据页中表A的记录生成B+树，存储到临时文件中； 生成临时文件的过程中，将所有对A的操作记录在一个日志文件（row log）中，对应的是图中state2的状态； 临时文件生成后，将日志文件中的操作应用到临时文件，得到一个逻辑数据上与表A相同的数据文件，对应的就是图中state3的状态； 用临时文件替换表A的数据文件。 在重建表的时候，InnoDB不会把整张表占满，每个页留了1/16给后续的更新用。也就是说，其实重建表之后不是“最”紧凑的。所以当原表没有什么空洞时重建可能导致表变大。\ncount(*)慢的原因 MyISAM引擎把一个表的总行数存在了磁盘上，因此执行count(*)的时候会直接返回这个数，效率很高； 而InnoDB引擎就麻烦了，它执行count(*)的时候，需要把数据一行一行地从引擎里面读出来，然后累积计数。 count(字段)\u0026lt;count(主键id)\u0026lt;count(1)≈count(*)\n日志和索引 两阶段提交：准备阶段（写入binlog）和提交阶段\nbinlog：记录数据库执行的增删改查，确保主从一致\n崩溃恢复时的判断规则：\n如果redo log里面的事务是完整的，也就是已经有了commit标识，则直接提交； 如果redo log里面的事务只有完整的prepare，则判断对应的事务binlog是否存在并完整： a. 如果是，则提交事务； b. 否则，回滚事务。 order by执行过程 会存在sort buffer size，在其中利用相应字段为某一行排序\n如果行的数值太大，则会只选择部分字段\n如果行数太多，会向磁盘借一些临时空间\n随机显示消息 1 mysql\u0026gt; select word from words order by rand() limit 3; 取得整个表的行数，并记为C。 取得 Y = floor(C * rand())。 floor函数在这里的作用，就是取整数部分。 再用limit Y,1 取得一行。 逻辑相似实践却相差大的操作 对索引字段做函数操作，可能会破坏索引值的有序性，因此优化器就决定放弃走树搜索功能 查一行也很慢的情况 1 等MDL锁\n现在有一个线程正在表t上请求或者持有MDL写锁，把select语句堵住了。\n在sys.SCHEMA_TABLE_LOCK_WAITS中查找阻塞的process id，\n1 select blocking_pid from sts.schema_table_lock_waits 2 等flush\n有一个flush tables命令被别的语句堵住了，然后它又堵住了我们的select语句。\nshow processlist\n3 查询慢\n执行时间慢，上一步更新，需要查询大量的undo log\n幻读 幻读指的是一个事务在前后两次查询同一个范围的时候，后一次查询看到了前一次查询没有看到的行。\n在可重复读隔离级别下，普通的查询是快照读，是不会看到别的事务插入的数据的。因此，幻读在“当前读”下才会出现。 上面session B的修改结果，被session A之后的select语句用“当前读”看到，不能称为幻读。幻读仅专指“新插入的行”。 事务的隔离级别：\n脏读，不可重复读，幻读，可重复读\n锁 难点，待办\nmysql提高性能的办法 1 处理占着连接不公正的线程\n2 减少连接过程的消耗。比如重启数据库时 采用-SKIP-GRANT-tables，mysql会跳过所有的权限验证阶段，但是风险高。\n慢查询可能产生的情况\n索引没有设计好； SQL语句没写好； MySQL选错了索引。 MYSQL如何保证数据不丢失 WAL机制，redo log和bin log持久化写入磁盘，就能确保mysql异常重启后数据可以恢复。\nbinlog的写入逻辑比较简单：事务执行过程中，先把日志写到binlog cache，事务提交的时候，再把binlog cache写到binlog文件中。\n对于redo log，分为三个区域，内存中的redo log buffer, FS page cache（写到磁盘，但是还没有持久化），hard dist\n一种是，redo log buffer占用的空间即将达到 innodb_log_buffer_size一半的时候，后台线程会主动写盘。 另一种是，并行的事务提交的时候，顺带将这个事务的redo log buffer持久化到磁盘。 mysql如何保证主备一致 在备库B上通过change master命令，设置主库A的IP、端口、用户名、密码，以及要从哪个位置开始请求binlog，这个位置包含文件名和日志偏移量。 在备库B上执行start slave命令，这时候备库会启动两个线程，就是图中的io_thread和sql_thread。其中io_thread负责与主库建立连接。 主库A校验完用户名、密码后，开始按照备库B传过来的位置，从本地读取binlog，发给B。 备库B拿到binlog后，写到本地文件，称为中转日志（relay log）。 sql_thread读取中转日志，解析出日志里的命令，并执行。 主备延迟 1 备用库机器规格差\n2 备库压力大\n3 大事务\n4 \u0026amp;\u0026amp;备库并行复制能力\n一主多从：读写分离 读写分离的主要目标就是分摊主库的压力。图1中的结构是客户端（client）主动做负载均衡，这种模式下一般会把数据库的连接信息放在客户端的连接层。也就是说，由客户端来选择后端数据库进行查询。\n还有一种架构是，在MySQL和客户端之间有一个中间代理层proxy，客户端只连接proxy， 由proxy根据请求类型和上下文决定请求的分发路由。\n由于主从可能存在延迟，客户端执行完一个更新事务后马上发起查询，如果查询选择的是从库的话，就有可能读到刚刚的事务更新之前的状态。这种“在从库上会读到系统的一个过期状态”的现象，在这篇文章里，我们暂且称之为“过期读”。\n强制走主库方案； sleep方案； 判断主备无延迟方案； 配合semi-sync方案； 等主库位点方案； 等GTID方案。 如何判断数据库的主库有没有出问题 **select1判断：**只能判断库的进程还在，不能说明主库没问题。虽然说等锁的线程不算在并发线程计数里，但如果它在真正地执行查询，就比如我们上面例子中前三个事务中的select sleep(100) from t，还是要算进并发线程的计数的。同时在执行的语句超过了设置的innodb_thread_concurrency的值，这时候系统其实已经不行了，但是通过select 1来检测系统，会认为系统还是正常的。\n查表判断：为了能够检测InnoDB并发线程数过多导致的系统不可用情况，我们需要找一个访问InnoDB的场景。一般的做法是，在系统库（mysql库）里创建一个表，比如命名为health_check，里面只放一行数据，然后定期执行。但是不能检测出来以下这种情况\n更新事务要写binlog，而一旦binlog所在磁盘的空间占用率达到100%，那么所有的更新语句和事务提交的commit语句就都会被堵住。但是，系统这时候还是可以正常读数据的。\n**更新判断：**常见做法是放一个timestamp字段，用来表示最后一次执行检测的时间。\n内部统计：可以通过MAX_TIMER的值来判断数据库是否出问题了。比如，你可以设定阈值，单次IO请求时间超过200毫秒属于异常\n加锁 原则1：加锁的基本单位是next-key lock。希望你还记得，next-key lock是前开后闭区间。 原则2：查找过程中访问到的对象才会加锁。 优化1：索引上的等值查询，给唯一索引加锁的时候，next-key lock退化为行锁。 优化2：索引上的等值查询，向右遍历时且最后一个值不满足等值条件的时候，next-key lock退化为间隙锁。 一个bug：唯一索引上的范围查询会访问到不满足条件的第一个值为止。 误删数据 1 预处理：\n使用规范 不太的开发人员权限不太 2 恢复方案\nbinlog日志恢复 全量备份 对重要数据的从库进行一个延迟处理，便于短时间快速恢复 某个结点误删切换其它结点，后台恢复该结点数据 kill失效 在MySQL中有两个kill命令：一个是kill query +线程id，表示终止这个线程中正在执行的语句；一个是kill connection +线程id，这里connection可缺省，表示断开这个线程的连接，当然如果这个线程有语句正在执行，也是要先停止正在执行的语句的。\nkill并不是马上停止的意思，而是告诉执行线程说，这条语句已经不需要继续执行了，可以开始“执行停止的逻辑了”。\n其实，这跟Linux的kill命令类似，kill -N pid并不是让进程直接停止，而是给进程发一个信号，然后进程处理这个信号，进入终止逻辑。只是对于MySQL的kill命令来说，不需要传信号量参数，就只有“停止”这个命令。\n把session B的运行状态改成THD::KILL_QUERY(将变量killed赋值为THD::KILL_QUERY)； 给session B的执行线程发一个信号。 **kill无效的第一类情况即：线程没有执行到判断线程状态的逻辑。**跟这种情况相同的，还有由于IO压力过大，读写IO的函数一直无法返回，导致不能及时判断线程的状态。\n**另一类情况是，终止逻辑耗时较长。**这时候，从show processlist结果上看也是Command=Killed，需要等到终止逻辑完成，语句才算真正完成。比较常见的场景有以下几种：\n超大事务执行期间被kill。这时候，回滚操作需要对事务执行期间生成的所有新数据版本做回收操作，耗时很长。 大查询回滚。如果查询过程中生成了比较大的临时文件，加上此时文件系统压力大，删除临时文件可能需要等待IO资源，导致耗时较长。 DDL命令执行到最后阶段，如果被kill，需要删除中间过程的临时文件，也可能受IO资源影响耗时较久。 连接数据库慢的原因 每个客户端在和服务端建立连接的时候，需要做的事情就是TCP握手、用户校验、获取权限。但这几个操作，显然跟库里面表的个数无关。当使用默认参数连接的时候，MySQL客户端会提供一个本地库名和表名补全的功能。为了实现这个功能，客户端在连接成功后，需要多做一些操作。如果在连接命令中加上-A，就可以关掉这个自动补全的功能，然后客户端就可以快速返回了。\n全表扫描的影响 ，对于正常的线上业务来说，如果一个查询的返回结果不会很多的话，我都建议你使用mysql_store_result这个接口，直接把查询结果保存到本地内存。\n由于MySQL采用的是边算边发的逻辑，因此对于数据量很大的查询结果来说，不会在server端保存完整的结果集。所以，如果客户端读结果不及时，会堵住MySQL的查询过程，但是不会把内存打爆。\n而对于InnoDB引擎内部，由于有淘汰策略，大查询也不会导致内存暴涨。并且，由于InnoDB对LRU算法做了改进，冷数据的全表扫描，对Buffer Pool的影响也能做到可控。\n当然，我们前面文章有说过，全表扫描还是比较耗费IO资源的，所以业务高峰期还是不能直接在线上主库执行全表扫描的。\njoin语句 第一个问题：能不能使用join语句？\n如果可以使用Index Nested-Loop Join算法，也就是说可以用上被驱动表上的索引，其实是没问题的； 如果使用Block Nested-Loop Join算法，扫描行数就会过多。尤其是在大表上的join操作，这样可能要扫描被驱动表很多次，会占用大量的系统资源。所以这种join尽量不要用。 所以你在判断要不要使用join语句时，就是看explain结果里面，Extra字段里面有没有出现“Block Nested Loop”字样。\n第二个问题是：如果要使用join，应该选择大表做驱动表还是选择小表做驱动表？\n如果是Index Nested-Loop Join算法，应该选择小表做驱动表； 如果是Block Nested-Loop Join算法： 在join_buffer_size足够大的时候，是一样的； 在join_buffer_size不够大的时候（这种情况更常见），应该选择小表做驱动表。 所以，这个问题的结论就是，总是应该使用小表做驱动表。\nInnoDB和Memory引擎 InnoDB的主键索引：B+树，把数据放在主键索引上，其他索引上保存的是主键id。这种方式，我们称之为索引组织表（Index Organizied Table）\nMemory引擎的数据和索引是分开的，把数据单独存放，索引上保存数据位置的数据组织形式，我们称之为堆组织表（Heap Organizied Table）\n因此，\nInnoDB表的数据总是有序存放的，而内存表的数据就是按照写入顺序存放的； 当数据文件有空洞的时候，InnoDB表在插入新数据的时候，为了保证数据有序性，只能在固定的位置写入新值，而内存表找到空位就可以插入新值； 数据位置发生变化的时候，InnoDB表只需要修改主键索引，而内存表需要修改所有索引； InnoDB表用主键索引查询时需要走一次索引查找，用普通索引查询的时候，需要走两次索引查找。而内存表没有这个区别，所有索引的“地位”都是相同的。 InnoDB支持变长数据类型，不同记录的长度可能不同；内存表不支持Blob 和 Text字段，并且即使定义了varchar(N)，实际也当作char(N)，也就是固定长度字符串来存储，因此内存表的每行数据长度相同。 自增主键为什么不连续 避免回滚：在关键数据插入时，尽量避免事务回滚。\n防止删除操作影响：通过逻辑删除（如设置状态字段 is_deleted = 1）替代物理删除。\n禁用批量插入：减少批量插入时的失败风险。\n设置自增 ID 策略：\n在主从复制中设置合理的 auto-increment-increment 和 auto-increment-offset。\n合理配置数据库重启后的 ID 分配策略。\n使用 UUID 替代自增主键：\n在分布式系统中，使用 UUID 或 雪花算法（Snowflake） 生成全局唯一 ID，避免自增主键不连续的问题。 怎么最快复制一张表 1 逻辑导数据的方法，也就是将数据从表db1.t中读出来，生成文本，然后再写入目标表db2.t中\nmysqldump方法：\n使用mysqldump命令将数据导出成一组INSERT语句\n导出CSV文件：\n直接将结果导出成.csv文件\n2 物理拷贝\nMySQL 5.6版本引入了可传输表空间(transportable tablespace)的方法，可以通过导出+导入表空间的方式，实现物理拷贝表的功能\n执行 create table r like t，创建一个相同表结构的空表； 执行alter table r discard tablespace，这时候r.ibd文件会被删除； 执行flush table t for export，这时候db1目录下会生成一个t.cfg文件； 在db1目录下执行cp t.cfg r.cfg; cp t.ibd r.ibd；这两个命令（这里需要注意的是，拷贝得到的两个文件，MySQL进程要有读写权限）； 执行unlock tables，这时候t.cfg文件会被删除； 执行alter table r import tablespace，将这个r.ibd文件作为表r的新的表空间，由于这个文件的数据内容和t.ibd是相同的，所以表r中就有了和表t相同的数据。 总结\n物理拷贝的方式速度最快，尤其对于大表拷贝来说是最快的方法。如果出现误删表的情况，用备份恢复出误删之前的临时库，然后再把临时库中的表拷贝到生产库上，是恢复数据最快的方法。但是，这种方法的使用也有一定的局限性： 必须是全表拷贝，不能只拷贝部分数据； 需要到服务器上拷贝数据，在用户无法登录数据库主机的场景下无法使用； 由于是通过拷贝物理文件实现的，源表和目标表都是使用InnoDB引擎时才能使用。 用mysqldump生成包含INSERT语句文件的方法，可以在where参数增加过滤条件，来实现只导出部分数据。这个方式的不足之一是，不能使用join这种比较复杂的where条件写法。 用select … into outfile的方法是最灵活的，支持所有的SQL写法。但，这个方法的缺点之一就是，每次只能导出一张表的数据，而且表结构也需要另外的语句单独备份。 grant之后要跟着flush privileges吗？ flush privileges命令使赋权语句生效\ngrant命令的动作：\n磁盘上，将mysql.user表里，用户’ua’@’%\u0026lsquo;这一行的所有表示权限的字段的值都修改为‘Y’； 内存里，从数组acl_users中找到这个用户对应的对象，将access值（权限位）修改为二进制的“全1”。 flush privileges命令会清空acl_users数组，然后从mysql.user表中读取数据重新加载，重新构造一个acl_users数组。也就是说，以数据表中的数据为准，会将全局权限内存数组重新加载一遍。\ngrant是即使更改表生效的，但是在比如删除用户这种不规范的操作则需要flush privileges刷新。\n分区表的使用 MySQL在第一次打开分区表的时候，需要访问所有的分区； 在server层，认为这是同一张表，因此所有分区共用同一个MDL锁； 在引擎层，认为这是不同的表，因此MDL锁之后的执行过程，会根据分区表规则，只访问必要的分区。 distinct和group by 如果只需要去重，不需要执行聚合函数，distinct 和group by哪种效率高一些呢？\n相同。\n创建一个临时表，临时表有一个字段a，并且在这个字段a上创建一个唯一索引； 遍历表t，依次取数据插入临时表中： 如果发现唯一键冲突，就跳过； 否则插入成功； 遍历完成后，将临时表作为结果集返回给客户端。 自增ID用完怎么办 表的自增id达到上限后，再申请时它的值就不会改变，进而导致继续插入数据时报主键冲突的错误。 row_id达到上限后，则会归0再重新递增，如果出现相同的row_id，后写的数据会覆盖之前的数据。 Xid只需要不在同一个binlog文件中出现重复值即可。虽然理论上会出现重复值，但是概率极小，可以忽略不计。 InnoDB的max_trx_id 递增值每次MySQL重启都会被保存起来，所以我们文章中提到的脏读的例子就是一个必现的bug，好在留给我们的时间还很充裕。 thread_id是我们使用中最常见的，thread_id_counter定义的大小是4个字节，因此达到232-1后，它就会重置为0，然后继续增加。但是，你不会在show processlist里看到两个相同的thread_id。 ","date":"2025-02-18T00:00:00Z","permalink":"https://sutdown.github.io/p/mysql%E5%AE%9E%E6%88%9845%E8%AE%B2%E7%AC%94%E8%AE%B0/","title":"MySql实战45讲笔记"},{"content":" 1 Gin源码解析\n1.1 Gin框架路由详解\n1.2 Gin框架中间件详解\n2 Go连接MySQL/Redis\n2.1 database/sql以及sqlx使用\n2.2 go-redis库使用\n3 搭建Go Web开发脚手架\n3.1 zap日志库使用\n3.2 Viper配置管理库使用\n3.3 优雅关机与平滑重启\n3.4 CLD代码分层\n4 仿Reddit论坛项目\n4.1 分布式ID生成\n4.2 JWT认证\n4.3 基于MySQL实现主业务\n4.4 基于Redis实现投票业务\n4.5 基于Docker搭建开发环境\n4.6 代码发布与项目部署\n4.5 实战经验与技巧\nGin源码解析 || 主要是Go Web开发进阶实战（gin框架） - 网易云课堂第一部分的笔记。\nGin作为web框架的原因在于\n支持中间件操作（ handlersChain 机制 ） 更方便的使用（ gin.Context ） 更强大的路由解析能力（ radix tree 路由树 ） 1.1 Gin框架路由详解 Radix树介绍 radix树可以认为是一种更节省空间的前缀树。\n为什么使用前缀树而不是哈希或者map？\n路由器为每个请求方法管理一个单独的树，为每个结点设置一个优先级。\n优先匹配被大多数路由路径包含的结点，让尽可能多的路由被快速定位。 类似于成本补偿，最长的路径被优先匹配，补偿体现在最长的路径需要更多的时间定位。 请求处理 1 go mod tidy Gin框架底层基于HTTP标准库开发，如何理解这句话\n接口和方法\n路由方法树 路由树是由一个个节点构成的，gin框架路由树的节点由node结构体表示，它有以下字段：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 // tree.go type node struct { // 节点路径，比如上面的s，earch，和upport path string // 和children字段对应, 保存的是分裂的分支的第一个字符 // 例如search和support, 那么s节点的indices对应的\u0026#34;eu\u0026#34; // 代表有两个分支, 分支的首字母分别是e和u indices string // 儿子节点 children []*node // 处理函数链条（切片） handlers HandlersChain // 优先级，子节点、子子节点等注册的handler数量 priority uint32 // 节点类型，包括static, root, param, catchAll // static: 静态节点（默认），比如上面的s，earch等节点 // root: 树的根节点 // catchAll: 有*匹配的节点 // param: 参数节点 nType nodeType // 路径上最大参数个数 maxParams uint8 // 节点是否是参数节点，比如上面的:post wildChild bool // 完整路径 fullPath string } 路由注册与路由匹配 注册路由\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 // gin.go gin框架会为每一个请求方法创建一棵对应的树。 func (engine *Engine) addRoute(method, path string, handlers HandlersChain) { // liwenzhou.com... // 获取请求方法对应的树 root := engine.trees.get(method) if root == nil { // 如果没有就创建一个 root = new(node) root.fullPath = \u0026#34;/\u0026#34; engine.trees = append(engine.trees, methodTree{method: method, root: root}) } root.addRoute(path, handlers) } // engine.trees type methodTree struct { method string root *node } type methodTrees []methodTree // slice // 获取请求方法对应树的get方法 func (trees methodTrees) get(method string) *node { for _, tree := range trees { if tree.method == method { return tree.root } } return nil } 注册路由的逻辑主要有addRoute函数和insertChild方法。\n1 2 3 4 5 6 7 addRoute 1. 第一次注册路由，例如注册search 2. 继续注册一条没有公共前缀的路由，例如blog 3. 注册一条与先前注册的路由有公共前缀的路由，例如support insertChild 根据path本身进行分割，将/分开的部分分别作为节点保存，形成一棵树结构。参数匹配中的:和*的区别是，前者是匹配一个字段而后者是匹配后面所有的路径。 路由匹配\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 // gin框架处理请求的入口函数ServeHTTP // gin.go func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) { // 这里使用了对象池 c := engine.pool.Get().(*Context) // 这里有一个细节就是Get对象后做初始化 c.writermem.reset(w) c.Request = req c.reset() engine.handleHTTPRequest(c) // 我们要找的处理HTTP请求的函数 engine.pool.Put(c) // 处理完请求后将对象放回池子 } // 路由匹配是由节点的 getValue方法实现的。getValue根据给定的路径(键)返回nodeValue值，保存注册的处理函数和匹配到的路径参数数据。如果找不到任何处理函数，则会尝试TSR(尾随斜杠重定向)。可以借助注释看一下路由查找及参数匹配的逻辑。 1.2 Gin框架中间件详解 gin框架涉及中间件相关有4个常用的方法，它们分别是c.Next()、c.Abort()、c.Set()、c.Get()\nDefault函数内部构造一个新的engine之后通过Use()函数注册Logger中间件和Recovery中间件。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 r := gin.Default() // 注册中间件其实就是将中间件函数追加到group.Handlers func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes { group.Handlers = append(group.Handlers, middleware...) return group.returnObj() } // 注册路由时会将对应路由的函数和之前的中间件函数结合到一起 func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes { absolutePath := group.calculateAbsolutePath(relativePath) handlers = group.combineHandlers(handlers) // 将处理请求的函数与中间件函数结合 group.engine.addRoute(httpMethod, absolutePath, handlers) return group.returnObj() } const abortIndex int8 = math.MaxInt8 / 2 func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain { finalSize := len(group.Handlers) + len(handlers) if finalSize \u0026gt;= int(abortIndex) { // 这里有一个最大限制 panic(\u0026#34;too many handlers\u0026#34;) } mergedHandlers := make(HandlersChain, finalSize) copy(mergedHandlers, group.Handlers) copy(mergedHandlers[len(group.Handlers):], handlers) return mergedHandlers } // 会将一个路由的中间件函数和处理函数结合到一起组成一条处理函数链条HandlersChain，而它本质上就是一个由HandlerFunc组成的切片 type HandlersChain []HandlerFunc c.Next()\n1 2 3 4 5 6 7 8 // 通过索引遍历HandlersChain链条，从而实现依次调用该路由的每一个函数（中间件或处理请求的函数） func (c *Context) Next() { c.index++ for c.index \u0026lt; int8(len(c.handlers)) { c.handlers[c.index](c) c.index++ } } c.Abort()\n1 2 3 4 // 中断整个调用链条，从当前函数返回 func (c *Context) Abort() { c.index = abortIndex // 直接将索引置为最大限制值，从而退出循环 } c.Set()和c.Get()这两个方法多用于在多个函数之间通过c传递数据的，比如我们可以在认证中间件中获取当前请求的相关信息（userID等）通过c.Set()存入c，然后在后续处理业务逻辑的函数中通过c.Get()来获取当前请求的用户。c就像是一根绳子，将该次请求相关的所有的函数都串起来了。\n总结 gin框架路由使用前缀树，路由注册的过程是构造前缀树的过程，路由匹配的过程就是查找前缀树的过程。 gin框架的中间件函数和处理函数是以切片形式的调用链条存在的，我们可以顺序调用也可以借助c.Next()方法实现嵌套调用。 借助c.Set()和c.Get()方法我们能够在不同的中间件函数中传递数据。 参考资料 Go Web开发进阶实战（gin框架） - 网易云课堂 gin框架源码解析 ","date":"2025-02-15T00:00:00Z","permalink":"https://sutdown.github.io/p/gin%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90/","title":"Gin源码解析"},{"content":"前言（碎碎念，可跳）： 之前学数据库的时候用的opengauss，还有个十几章的实验，那会很多东西都搞不太清楚，基本就是跟着文档走，也没留下什么印象。期末周靠着往年题和ppt苟活，导致数据库这门课学的着实不扎实。简历上有个lsmkv的项目，浅显的数据库理解必然是不够的，基础知识不够的情况下背八股更是显得无用，这两三天经人推荐看了看这本书，看完就完也很容易忘，所以打算写个学习笔记。\n整体评价，还不错，语言比较有趣，深度也可以，建议有一定数据库基础之后阅读。\n正文： 1.mysql的结构（主要是InnoDB） mysql分为server层和存储引擎层。\nserver层：连接管理，查询缓存，语法解析，查询优化 存储引擎层最常用的是InnoDB和MyISAM。其中InnoDB是Mysql的默认存储引擎。 InnoDB：具备外键支持功能的事务存储引擎。 MyISAM：主要的非事务处理存储引擎。 InnoDB行格式介绍：主要有四种Compact，Redundant，Dynamic，Compressed\nCompact格式：\n1 变长字段长度列表 | NULL值列表 | 记录头信息 | 记录真实数据：列1 | 列2 | ... 其中，由于初始指针位于头信息，因此为了便于访问，NULL和变长字段长度列表中对应列的数值都是逆序存放。为了最小化内存的使用，变长字段长度列表中，只存放varchar的类型，NULL值列表中对应为空标识1，因此通常最好创建类型的时候标明NOT NULL。\nCompact是如今最常用的格式，其它格式属于mysql之前的版本采用的，因此不过多介绍。\n页是mysql中MySQL中磁盘和内存交互的基本单位，也是管理存储空间的基本单位。一般大小为16KB\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 InnoDB数据页结构示意图 ----------- File Header 记录页的通用信息 ----------- Page Header 记录数据页的专有信息 ----------- Infimum + supremum 页中最小和最大伪记录 ----------- User Records 真实存入我们插入的记录 ----------- Free Space 页中尚未使用的部分 ----------- Page Directory 页面目录 ----------- File Tailer 校验页完整性 ----------- 这个和之前levelDB中SSTable的结构有点像，估计是统一结构，最下面放一个目录记录上方各个数据的偏移量和一个校验整体整性的两部分，头部存放通用信息和该页面的专有信息，比如在MySQL中会存放指针指向前一个页和后一个页（B+树），中间存放数据，至于user record和free space是属于统一空间，字节不明确，从free space中去空间存放记录，然后记录从上到下存放在user record中。\n2.一条mysql是如何运行的 建立连接；查询缓存（缓存效果不好，因为只会记录完全相同的语句的查找结果，但由于数据量很大，且现实中查找同一项的场景比较少，因此mysql最新版本已经取消了查询缓存）；解析sql；执行sql（预处理，优化，执行）。\n在执行mysql的过程中，首先会优化语句，也就是采取什么样的查找方式最有效率，其中考虑的因素有磁盘IO成本和CPU成本。顾名思义，前者指的是从磁盘中加载数据和讲数据加载回内存，后者则是读取以及检测记录是否满足对应的搜索条件，对结果集进行排序损耗的时间。然后再从InnoDB存储引擎中寻找，池化思想在这里也有点体现，那就是我们的BufferPool，后面详细讲解。我们知到页是基本单位，由于页过于多，因此提出了区的概念。\n基本就是一个表空间中，每256个区属于一组，每个区大概1MB，区中存放页。管理区的结构我们称为XDES Entry(Extent Descriptor Entry)，其中这个区属于一个结点，很多个Entry会构成链表，同时为了管理内存，设置了三个链表，分别是，完全空闲，存放但是有剩余空间，完全满。\n这样查询数据便于保证效率。\n3.B+树和索引 B树和B+树，B+树的优点\n由于冗余结点较多，增删改时不会出现太大的变动 只有叶子结点存放数据，能够存放更多的索引，减少磁盘IO次数。（有个record_type用来区分目录项记录和普通的用户记录，目录项记录record_type为1，用户记录该项为0） 叶子节点之间用链表连接，有利于范围查询。 常用的索引方案是聚簇索引，二级索引，以及联合索引。\n聚簇索引也就是利用B+树的结构查找，时间复杂度为O(logn)，只在搜索值是主键值的时候发挥作用。\n二级索引的结点中只会存储 键值+页号+主键值，以键值作为新的B+树，不会存储所有的数据以免造成冗余，后两者用于回表找到确切的条目。\n联合索引其实就是二级索引的扩展，没有很大特殊性。\n另外在使用联合索引和二级索引的时候，select后面最好写清楚想要查找的值，如果刚好在B+树中，就能避免回表的损耗。\n4.索引失效 索引失效就是在查找的时候不能使用表中有的树进行查找，只能进行全表扫描。（全表扫描不仅损耗时间，同时会为cache产生影响，这个cache不是查询缓存，是BufferPool中的cache，为了保证数据一致性）\n这里直接引用原文了：\nB+树索引在空间和时间上都有代价，所以没事儿别瞎建索引。 B+树索引适用于下边这些情况： 全值匹配 匹配左边的列 匹配范围值 精确匹配某一列并范围匹配另外一列 用于排序 用于分组 在使用索引时需要注意下边这些事项： 只为用于搜索、排序或分组的列创建索引 为列的基数大的列创建索引 索引列的类型尽量小 可以只对字符串值的前缀建立索引 只有索引列在比较表达式中单独出现才可以适用索引 为了尽可能少的让聚簇索引发生页面分裂和记录移位的情况，建议让主键拥有AUTO_INCREMENT属性。 定位并删除表中的重复和冗余索引 尽量使用覆盖索引进行查询，避免回表带来的性能损耗。 4.BufferPool 每次从磁盘中取出页之后，也不可能立马就放回去，那磁盘IO的开销就很大了，这就是BufferPool的出现了。\nBufferPool的空间应当是这样子的：\n1 控制块 | 控制块 | ... | 碎片 | 缓存页 | 缓存页 | ... free-list：将所有的缓存页作对应的控制块作为结点放到链表中，unordered_map\u0026lt;表空间号+页号，缓存页\u0026gt;确立该页在不在缓冲区中。（好像LRU Cache的结构，LRU缓存也是哈希表+链表的组合）\nflush-list：存储被修改了的缓存页的控制块，保证一致性。定时会从flush链表中刷新一部分数据到磁盘。\nLRU-list：用于管理BUffer Pool中的缓存页，缓存池的空间是有限的。\n我们所知的最简单的LRU Cache当然不适用于mysql，那肯定是要优化的，LRU是淘汰最近最不常使用的，但想像加入访问频率过高，比如全表扫描的情况，那LRU链表在不停的变动并且达不到保存使用频率较高的后淘汰的目的。因此对其增加了一个改动：\n一部分存储频率很高的缓存页，称为young区域；另一部分存储频率不上很高的缓存页，称为old区域。 初次加载存放在old头部，当一定时间之后（这是因为如果全表扫面，一个页面中有很多条数据，那么一段时间内这个页面能够被访问很多次），这个页面再次被访问，那么会移动到young头部。 5.InnoDB的统计数据 基于磁盘的永久性统计数据 基于内存的非永久性统计数据 （NULL值认为是不存在，还是完全相等，还是完全不等这个在mysql中有相应的候选值确立，用户可以自行选择，当然最好不要在索引中存放数据）\n这里具体的原理就不谈了，其实是自己看完也没什么很大印象）。感觉跟这个题有一定的关系。count(*) 和 count(1) 有什么区别？哪个性能最好？ | 小林coding，这里面的结论是count(1)==count(*)\u0026gt;count(主键字段)\u0026gt;count(普通字段)。前两个相等且大于后面的是因为InnoDB会自动优化选择最简单的方式，同时只用于统计不会真正的访问相关的字段。另外每个区每个表中应该会有相应的头信息或者指针记录相应的数目，和统计某个字段一定是有区别的。\n6.mysql单表查询 MySQL 单表不要超过 2000W 行，靠谱吗？ | 小林coding\n7.事务的四个特性 事务的四个特性：\n原子性：undo log 一致性：MVCC/锁 隔离性：原子性+一致性+持久性 持久性：redo log 事务的隔离级别：\n脏写：一个事务修改了另一个未提交事务修改过的数据 脏读：一个事务读取了另一个未提交事务修改过的数据 不可重复读：一个事务读到另一个已提交事务修改过的数据，并且其它事务对数据修改提交后，这个事务都能查到 幻读：一个事务按照某个相同条件多次读取数据时，后读取到了之前没有读取的记录。 四种隔离级别导致发生的问题：脏写，脏读，不可重复读，幻读\n8.MVCC 主要原理在于版本链和read view\n版本链也就是undo log中的roll pointer属性连接成一个链表，链表的头结点为最新值\nRead view：\n读未提交：直接读取最新版本\n串行：加锁\n读已提交和可重复读：要保证读到已经提交的事务修改的记录，也就是要判断哪个版本是事务可见的。\n对于读已提交隔离级别： 一般会再读取数据之前生成Read view，比如INSERT，DELETE，UPDATE。read view中有个活跃列表，在活跃列表之内的事务id的版本是读取不到的。因此解决了未提交的问题。\n对于可重复读隔离级别： 只在第一次进行普通SELECT操作前生成一个READ view，之后的查询一直使用这个read view，就解决了不可重复度的问题。\n9.undo log, redo log, binlog MySQL 日志：undo log、redo log、binlog 有什么用？ | 小林coding\n（书上讲了四章，没怎么看懂…，看看小林coding了就）\nredo log：把事务执行时对数据库的所有修改记录下来，便于系统崩溃重启后可以恢复事务的修改\nundo log：实现事务回滚，保障原子性；实现MVCC关键因素之一\n10.锁 MySQL在可重复读的隔离级别解决了幻读的问题\n读版本：MVCC（一致性读）；写：加锁。 读写都采用加锁。 读操作：S锁 写操作： delete：先在B+树种定位，再delete mark insert ：存在隐式锁 update：1.存储空间未改变，X素直接修改2.存储空间改变，先X锁删除，再insert3.键值改变，需要先delete再insert 参考： 1.图解mysql - 小林coding\n2.《mysql是怎样运行的》\n","date":"2025-02-10T00:00:00Z","permalink":"https://sutdown.github.io/p/mysql%E6%98%AF%E6%80%8E%E6%A0%B7%E8%BF%90%E8%A1%8C%E7%9A%84/","title":"mysql是怎样运行的"},{"content":"预计长久更新。这篇文章太泛了，知识点只适合于初步了解，会进一步加深的。\n2025.02.09 20:42 创建\n常见问题：\ngo中select+channel（goroutine）的实现机制\nGC垃圾回收原理\n内存模型\ngoroutine，GMP调度机制，调度流程\nCHAN原理，Context原理\n竞态，内存逃逸\n侵入式和非侵入式\ndefer+recover+panic，以及相应底层原理 方法的特别之处\n接口的作用和底层原理\nslice，map原理，为什么它们都是非线形安全的\n反射高级编程\n1 设计理念 go是一门面向对象的语言吗 以下为官方回答：\n是的，也不是。原因是： Go 有类型和方法，并且允许面向对象的编程风格，但没有类型层次。 Go 中的 “接口 “概念提供了一种不同的方法，我们认为这种方法易于使用，而且在某些方面更加通用。还有一些方法可以将类型嵌入到其他类型中，以提供类似的东西，但不等同于子类。 Go 中的方法比 C++ 或 Java 中的方法更通用：它们可以为任何类型的数据定义，甚至是内置类型，如普通的、“未装箱的 “整数。它们并不局限于结构（类）。 Go 由于缺乏类型层次，Go 中的 “对象 “比 C++ 或 Java 等语言更轻巧。\ngo的设计理念 go是一种基于连接（结构）与组合的语言。\ngo的设计理念在于工程化，更好直接的提升项目的效率。因此在并发上的封装尤其的多（比如goroutine)，同时为了语法的简明，去形成了一套完整的垃圾回收，内存模型，异常与错误（defer+panic+recover）等机制。不同于c++面向对象编程，封装继承多态三大特性的语言性质，go使用组合方法接口达成一套新的机制。\n优点：\n一种支持垃圾回收、静态编译的系统级编程语言 Go对**并发(concurrency)和并行(parallelism)**的原生支持有助于利用当时正在成为主流的多核机器的优势 Go没有提供类(class)，但允许将方法(method)绑定到任何类型上，包括结构体、数组、切片、map，甚至是基本类型，如整型。它没有类型层次体系；我们认为继承性往往会使程序在演化过程中更难适应。相反，Go鼓励类型的组合。 Go通过其接口类型提供面向对象的多态性。任何Go类型如果拥有与某个接口相同名称和签名的方法集合，就被认为是实现了该接口，而无需额外的显式声明。 2 复合数据类型（数组，slice，map） 数组 当元素数量小于或者等于 4 个时，会直接将数组中的元素放置在栈上； 当元素数量大于 4 个时，会将数组中的元素放置到静态区并在运行时取出； slice和数组的联系和区别？\nslice 的底层数据是数组，slice 是对数组的封装。\n数组是定长的，长度定义好之后，不能再更改。在 Go 中，数组是不常见的，因为其长度是类型的一部分，限制了它的表达能力，比如 [3]int 和 [4]int 就是不同的类型。而切片则非常灵活，它可以动态地扩容。切片的类型和长度无关。\nslice 一个slice类型一般写作[]T，其中T代表slice中元素的类型；由三个部分构成：指针、长度和容量。\n1 2 3 4 5 6 7 8 9 // runtime/slice.go type slice struct { array unsafe.Pointer // 元素指针 len int // 长度 cap int // 容量 } var s1 []int\t// nil切片 var s2 = []int{} // empty切片 为何slice不直接支持比较运算符呢？\n这方面有两个原因。第一个原因，一个slice的元素是间接引用的，一个slice甚至可以包含自身（译注：当slice声明为[]interface{}时，slice的元素可以是自身）；第二个原因，因为slice的元素是间接引用的，一个固定的slice值（译注：指slice本身的值，不是元素的值）在不同的时刻可能包含不同的元素，因为底层数组的元素可能会被修改\n创建slice\n类型转换\n打印语句\n栈空间扩容\n当原 slice 容量小于 1024 的时候，新 slice 容量变成原来的 2 倍；原 slice 容量超过 1024，新 slice 容量变成原来的1.25倍。后半部分还对 newcap 作了一个内存对齐，这个和内存分配策略相关。进行内存对齐之后，新 slice 的容量是要 大于等于 老 slice 容量的 2倍或者1.25倍。\n值的注意的是，当直接用切片作为函数参数时，可以改变切片的元素，不能改变切片本身；想要改变切片本身，可以将改变后的切片返回，函数调用者接收改变后的切片或者将切片指针作为函数参数。 扩容策略并不是简单的扩为原切片容量的 2 倍或 1.25 倍，还有内存对齐的操作。扩容后的容量 \u0026gt;= 原容量的 2 倍或 1.25 倍 多个切片可能共享同一个底层数组，这种情况下，对其中一个切片或者底层数组的更改，会影响到其他切片 map 最主要的数据结构有两种：哈希查找表（Hash table）、搜索树（Search tree）。Go 语言采用的是哈希查找表，并且使用链表解决哈希冲突。\n1 2 3 4 5 6 ageMp := make(map[string]int) // 指定 map 长度 ageMp := make(map[string]int, 8) // ageMp 为 nil，不能向其添加元素，会直接panic var ageMp map[string]int makemap 和 makeslice 的区别，带来一个不同点：当 map 和 slice 作为函数参数时，在函数参数内部对 map 的操作会影响 map 自身；而对 slice 却不会（之前讲 slice 的文章里有讲过）。\n主要原因：一个是指针（*hmap），一个是结构体（slice）。Go 语言中的函数传参都是值传递，在函数内部，参数会被 copy 到本地。*hmap指针 copy 完之后，仍然指向同一个 map，因此函数内部对 map 的操作会影响实参。而 slice 被 copy 后，会成为一个新的 slice，对它进行的操作不会影响到实参。\nhash map 的一个关键点在于，哈希函数的选择。在程序启动时，会检测 cpu 是否支持 aes，如果支持，则使用 aes hash，否则使用 memhash。\nhash 函数，有加密型和非加密型。加密型的一般用于加密数据、数字摘要等，典型代表就是 md5、sha1、sha256、aes256 这种；非加密型的一般就是查找。在 map 的应用场景中，用的是查找。选择 hash 函数主要考察的是两点：性能、碰撞概率。\nget Go 语言采用一个 bucket 里装载 8 个 key，定位到某个 bucket 后，还需要再定位到具体的 key，这实际上又用了时间换空间。当然，这样做，要有一个度，不然所有的 key 都落在了同一个 bucket 里，直接退化成了链表，各种操作的效率直接降为 O(n)，是不行的。\n装载因子：$loadFactor := count / (2^B)$\n触发扩容的条件\n装载因子超过阈值，源码里定义的阈值是 6.5。\noverflow 的 bucket 数量过多：当 B 小于 15，也就是 bucket 总数$ 2^B $小于 2^15 时，如果 overflow 的 bucket 数量超过$ 2^B$；当 B \u0026gt;= 15，也就是 bucket 总数$ 2^B$ 大于等于 2^15，如果 overflow 的 bucket 数量超过 2^15。\n对于条件 1，元素太多，而 bucket 数量太少，很简单：将 B 加 1，bucket 最大数量（2^B）直接变成原来 bucket 数量的 2 倍。于是，就有新老 bucket 了。注意，这时候元素都在老 bucket 里，还没迁移到新的 bucket 来。而且，新 bucket 只是最大数量变为原来最大数量（2^B）的 2 倍（2^B * 2）。\n对于条件 2，其实元素没那么多，但是 overflow bucket 数特别多，说明很多 bucket 都没装满。解决办法就是开辟一个新 bucket 空间，将老 bucket 中的元素移动到新 bucket，使得同一个 bucket 中的 key 排列地更紧密。这样，原来，在 overflow bucket 中的 key 可以移动到 bucket 中来。结果是节省空间，提高 bucket 利用率，map 的查找和插入效率自然就会提升。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 // src/runtime/hashmap_fast.go func main() { ageMap := make(map[string]int) ageMap[\u0026#34;qcrao\u0026#34;] = 18 // 不带 comma 用法 age1 := ageMap[\u0026#34;stefno\u0026#34;] fmt.Println(age1) // 带 comma 用法 age2, ok := ageMap[\u0026#34;stefno\u0026#34;] fmt.Println(age2, ok) } /* 运行结果 0 0 false */ // 底层函数 // src/runtime/hashmap.go func mapaccess1(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer func mapaccess2(t *maptype, h *hmap, key unsafe.Pointer) (unsafe.Pointer, bool) defer+panic+recover c++：RALL\ndefer是Go语言提供的一种用于注册延迟调用的机制：让函数或语句可以在当前函数执行完毕后（包括通过return正常结束或者panic导致的异常结束）执行。\n底层原理 每次defer语句执行的时候，会把函数“压栈”，函数参数会被拷贝下来；当外层函数（非代码块，如一个for循环）退出时，defer函数按照定义的逆序执行；如果defer执行的函数为nil, 那么会在最终调用函数的产生panic.\ndefer后面的语句在执行的时候，函数调用的参数会被保存起来，也就是复制了一份。真正执行的时候，实际上用到的是这个复制的变量，因此如果此变量是一个“值”，那么就和定义的时候是一致的。如果此变量是一个“引用”，那么就可能和定义的时候不一致。\n有关return\n1 2 3 4 5 6 7 return xxx /* 1. 返回值 = xxx 2. 调用defer函数 3. 空的return */ 闭包是什么 闭包=函数+引用环境\ndefer+recover panic会停掉当前正在执行的程序，不只是当前协程。在这之前，它会有序地执行完当前协程defer列表里的语句，其它协程里挂的defer语句不作保证。因此，我们经常在defer里挂一个recover语句，防止程序直接挂掉，这起到了try...catch的效果。\n3 方法 方法本质上是一种函数，但它们具有一个特定的接收者（receiver），也就是方法所附加到的类型。这个接收者可以是指针类型或值类型。方法与函数的区别是，函数不属于任何类型，方法属于特定的类型。\n1 2 3 4 // func receiver 方法名 参数列表 返回值列表 func (t *T/T) MethodName(参数列表) (返回值列表) { // 方法体 } 4 接口 接口是一组方法的集合\nGo语言中的接口（interface）是一组方法签名的集合，是一种抽象类型。接口定义了方法，但没有实现，而是由具体的类型（struct）实现这些方法，因此接口是一种实现多态的机制。\n1 2 3 4 5 type 接口名 interface { 方法名1(参数1 类型1, 参数2 类型2) 返回值类型1 方法名2(参数3 类型3) 返回值类型2 ... } Go 语言中，每个变量都有一个静态类型，在编译阶段就确定了的，比如 int, float64, []int 等等。注意，这个类型是声明时候的类型，不是底层数据类型。\n反射主要与 interface{} 类型相关。\n反射三大定律 反射将接口变量转换成反射对象 Type 和 Value； 反射可以通过反射对象 Value 还原成原先的接口变量； 反射可以用来修改一个变量的值，前提是这个值可以被修改。 第一条：反射是一种检测存储在 interface 中的类型和值机制。这可以通过 TypeOf 函数和 ValueOf 函数得到。\n第二条：它将 ValueOf 的返回值通过 Interface() 函数反向转变成 interface 变量。\n前两条就是说 接口型变量 和 反射类型对象 可以相互转化，反射类型对象实际上就是指的前面说的 reflect.Type 和 reflect.Value。\n第三条：如果需要操作一个反射变量，那么它必须是可设置的。反射变量可设置的本质是它存储了原变量本身，这样对反射变量的操作，就会反映到原变量本身；反之，如果反射变量不能代表原变量，那么操作了反射变量，不会对原变量产生任何影响，这会给使用者带来疑惑。所以第二种情况在语言层面是不被允许的。\n5 GPM调度 Golang 的协程本质上其实就是对 IO 事件的封装，并且通过语言级的支持让异步的代码看上去像同步执行的一样。\nGMP Go面试必问——GMP调度模型详解_golang面试 gmp-CSDN博客\nG：Goroutine 的缩写，每次 go func() 都代表一个 G，无限制，但受内存影响。使用 struct runtime.g，包含了当前 goroutine 的状态、堆栈、上下文 M：工作线程(OS thread)也被称为 Machine，使用 struct runtime.m，所有 M 是有线程栈的。M 的默认数量限制是 10000（来源），可以通过debug.SetMaxThreads修改。 GM模型的缺点\n单一全局互斥锁(Sched.Lock)和集中状态存储。导致所有 goroutine 相关操作，比如：创建、结束、重新调度等都要上锁。 Goroutine 传递问题。M 经常在 M 之间传递”可运行”的 goroutine，这导致调度延迟增大以及额外的性能损耗（刚创建的 G 放到了全局队列，而不是本地 M 执行，不必要的开销和延迟） Per-M 持有内存缓存 (M.mcache)。每个 M 持有 mcache 和 stack alloc，然而只有在 M 运行 Go 代码时才需要使用的内存(每个 mcache 可以高达2mb)，当 M 在处于 syscall 时并不需要。运行 Go 代码和阻塞在 syscall 的 M 的比例高达1:100，造成了很大的浪费。同时内存亲缘性也较差。G 当前在 M运 行后对 M 的内存进行了预热，因为现在 G 调度到同一个 M 的概率不高，数据局部性不好。 严重的线程阻塞/解锁。在系统调用的情况下，工作线程经常被阻塞和取消阻塞，这增加了很多开销。比如 M 找不到G，此时 M 就会进入频繁阻塞/唤醒来进行检查的逻辑，以便及时发现新的 G 来执行。 GMP模型\n再见 Go 面试官：GMP 模型，为什么要有 P？-CSDN博客\nP：Processor，是一个抽象的概念，并不是真正的物理 CPU，P 表示执行 Go 代码所需的资源，可以通过 GOMAXPROCS 进行修改。当 M 执行 Go 代码时，会先关联 P。当 M 空闲或者处在系统调用时，就需要 P。且在 Go1.5 之后GOMAXPROCS 被默认设置可用的核数，而之前则默认为1。 好处\n每个 P 有自己的本地队列，大幅度的减轻了对全局队列的直接依赖，所带来的效果就是锁竞争的减少。而 GM 模型的性能开销大头就是锁竞争。\n每个 P 相对的平衡上，在 GMP 模型中也实现了 Work Stealing 算法，如果 P 的本地队列为空，则会从全局队列或其他 P 的本地队列中窃取可运行的 G 来运行，减少空转，提高了资源利用率。\n设计策略\n1）work stealing 机制\n当本线程无可运行的 G 时，尝试从其他线程绑定的 P 偷取 G，而不是销毁线程。\n2）hand off 机制\n当本线程因为 G 进行系统调用阻塞时，线程释放绑定的 P，把 P 转移给其他空闲的线程执行。\n利用并行：GOMAXPROCS 设置 P 的数量，最多有 GOMAXPROCS 个线程分布在多个 CPU 上同时运行。GOMAXPROCS 也限制了并发的程度，比如 GOMAXPROCS = 核数/2，则最多利用了一半的 CPU 核进行并行。 抢占：在 coroutine 中要等待一个协程主动让出 CPU 才执行下一个协程，在 Go 中，一个 goroutine 最多占用 CPU 10ms，防止其他 goroutine 被饿死，这就是 goroutine 不同于 coroutine 的一个地方。 全局 G 队列：在新的调度器中依然有全局 G 队列，但功能已经被弱化了，当 M 执行 work stealing 从其他 P 偷不到 G 时，它可以从全局 G 队列获取 G。\n6 CSP模型 Channel · Go语言中文文档\nGo语言的并发模型是CSP（Communicating Sequential Processes），提倡通过通信共享内存而不是通过共享内存而实现通信。\nGo 语言中的通道（channel）是一种特殊的类型。通道像一个传送带或者队列，总是遵循先入先出（First In First Out）的规则，保证收发数据的顺序。每一个通道都是一个具体类型的导管，也就是声明channel的时候需要为其指定元素类型。\n1 2 3 4 5 6 7 8 9 10 11 12 // var 变量 chan 元素类型 // 声明通道类型 var ch1 chan int // 声明一个传递整型的通道 var ch2 chan bool // 声明一个传递布尔型的通道 var ch3 chan []int // 声明一个传递int切片的通道 // 声明的通道后需要使用make函数初始化之后才能使用。 // make(chan 元素类型, [缓冲大小]) // 创建channel ch4 := make(chan int) ch5 := make(chan bool) ch6 := make(chan []int) 通道有发送（send）、接收(receive）和关闭（close）三种操作。\n关于关闭通道需要注意的事情是，只有在通知接收方goroutine所有的数据都发送完毕的时候才需要关闭通道。通道是可以被垃圾回收机制回收的，它和关闭文件是不一样的，在结束操作之后关闭文件是必须要做的，但关闭通道不是必须的。\n1 2 3 4 5 6 7 8 9 10 11 12 13 ch := make(chan int) ch \u0026lt;- 10 // 把10发送到ch中 x := \u0026lt;- ch // 从ch中接收值并赋值给变量x \u0026lt;-ch // 从ch中接收值，忽略结果 close(ch) // 关闭 /* 1.对一个关闭的通道再发送值就会导致panic。 2.对一个关闭的通道进行接收会一直获取值直到通道为空。 3.对一个关闭的并且没有值的通道执行接收操作会得到对应类型的零值。 4.关闭一个已经关闭的通道会导致panic。 */ 一种方法是启用一个goroutine去接收值。无缓冲的通道只有在有人接收值的时候才能发送值。无缓冲通道上的发送操作会阻塞，直到另一个goroutine在该通道上执行接收操作，这时值才能发送成功，两个goroutine将继续执行。相反，如果接收操作先执行，接收方的goroutine将阻塞，直到另一个goroutine在该通道上发送一个值。 使用无缓冲通道进行通信将导致发送和接收的goroutine同步化。因此，无缓冲通道也被称为同步通道。\n解决上面问题的方法还有一种就是使用有缓冲区的通道。只要通道的容量大于零，那么该通道就是有缓冲的通道，通道的容量表示通道中能存放元素的数量。\n双向改单向通道（待办）\n7 内存分配 内存管理一般包含三个不同的组件，分别是用户程序（Mutator）、分配器（Allocator）和收集器（Collector），当用户程序申请内存时，它会通过内存分配器申请新内存，而分配器会负责从堆中初始化相应的内存区域。\n内存分配器 编程语言的内存分配器一般包含两种分配方法，一种是线性分配器（Sequential Allocator，Bump Allocator），另一种是空闲链表分配器（Free-List Allocator）。Go 语言的内存分配器就借鉴了 TCMalloc 的设计实现高速的内存分配，它的核心理念是使用多级缓存将对象根据大小分类，并按照类别实施不同的分配策略。\nTCMalloc 和 Go 运行时分配器都会引入**线程缓存（Thread Cache）、中心缓存（Central Cache）和页堆（Page Heap）**三个组件分级管理内存。\n三个组件分别有什么作用\n线程缓存属于每一个独立的线程，它能够满足线程上绝大多数的内存分配需求，因为不涉及多线程，所以也不需要使用互斥锁来保护内存，这能够减少锁竞争带来的性能损耗。当线程缓存不能满足需求时，运行时会使用中心缓存作为补充解决小对象的内存分配，在遇到 32KB 以上的对象时，内存分配器会选择页堆直接分配大内存。\n虚拟内存布局 在 Go 语言 1.10 以前的版本，堆区的内存空间都是连续的；但是在 1.11 版本，Go 团队使用稀疏的堆内存空间替代了连续的内存，使用稀疏的内存布局不仅能移除堆大小的上限，还能解决 C 和 Go 混合使用时的地址空间冲突问题。\n为什么这里会出现C和Go混用的地址空间冲突问题\n所有的 Go 语言程序都会在启动时初始化如上图所示的内存布局，每一个处理器都会分配一个线程缓存 runtime.mcache 用于处理微对象和小对象的分配，它们会持有内存管理单元 runtime.mspan。\n当内存管理单元中不存在空闲对象时，它们会从 runtime.mheap 持有的 134 个中心缓存 runtime.mcentral 中获取新的内存单元，中心缓存属于全局的堆结构体 runtime.mheap，它会从操作系统中申请内存。\n8 垃圾回收 垃圾回收的五种经典算法 标记-清扫 比如经典的三色标记算法。主要缺点在于可能产生内存碎片或者空洞导致新对象分配失败。\n标记-压缩 减少内存碎片，增加复杂度。\n半空间复制 空间换时间\n引用计数 无法解决并发，只能原子操作\n分代GC Go语言采用并发三色标记算法进行垃圾回收 为什么不选择压缩GC —— TCmalloc内存分配\n为什么不选择分代GC —— 内存逃逸\nGo垃圾回收演化 Go1.0 单协程垃圾回收\nGo1.1 多协程垃圾回收\nGo1.5 用户协程和后台的垃圾回收同时进行\nGo1.6 大幅减少STW期间的任务\nGo1.8 采用混合写屏障技术消除栈重新扫描的时间\n垃圾回收的阶段 1 2 3 触发垃圾回收 ---\u0026gt; 标记准备阶段 ---\u0026gt; 并行标记阶段 ---\u0026gt; 标记终止阶段 ---\u0026gt; 垃圾清扫阶段 | | --------------------------------------------------------------- 标记准备阶段\n重置各种状态和统计指标、启动专门用于标记的协程、统计需要扫描的任务数量、开启写屏障、启动标记协程等。\n9 反射 Go语言提供了一种机制，能够在运行时更新变量和检查它们的值、调用它们的方法和它们支持的内在操作，而不需要在编译时就知道这些变量的具体类型。这种机制被称为反射。\n参考书籍 《go底层原理剖析》（内容讲解比较通俗） go语言圣经（经典书籍，存在一定难度） go语言之旅（有代码讲解，最为简单，适合初步学习） Go 语言设计哲学（可浏览，很有意思） Go 程序员面试笔试宝典 Go语言中文文档 参考链接 深度解密Go语言之map - Stefno - 博客园 深度解密Go语言之Slice - Stefno - 博客园 Golang之轻松化解defer的温柔陷阱 - Stefno - 博客园 为什么go和rust语言都舍弃了继承？ - 知乎 理解Go协程调度的本质 - 知乎 深入分析Go1.18 GMP调度器底层原理 - 知乎 ","date":"2025-02-09T00:00:00Z","permalink":"https://sutdown.github.io/p/go%E5%85%A5%E9%97%A8%E5%AD%A6%E4%B9%A0/","title":"go入门学习"},{"content":"基础篇 01-08 Q：一条sql语句如何执行的？ A：mysql分为Server层和存储引擎层（默认InnoDB）。\n连接器 mysql -h $ip -P $port -u $user -p。建立连接，在tcp握手之后，连接器开始认证身份，用户输入用户名和密码认证。一般使用长连接。\nMySQL在执行过程中临时使用的内存是管理在连接对象里面的。这些资源会在连接断开的时候才释放。所以如果长连接累积下来，可能导致内存占用太大，被系统强行杀掉（OOM），从现象看就是MySQL异常重启了，如何解决呢？\n定期断开长连接。使用一段时间，或者程序里面判断执行过一个占用内存的大查询后，断开连接，之后要查询再重连。 如果你用的是MySQL 5.7或更新版本，可以在每次执行一个比较大的操作后，通过执行 mysql_reset_connection来重新初始化连接资源。这个过程不需要重连和重新做权限验证，但是会将连接恢复到刚刚创建完时的状态。 查询缓存。之前执行过的语句及其结果可能会以key-value对的形式，被直接缓存在内存中。key是查询的语句，value是查询的结果。\n大多数情况下不用查询缓存，原因是什么呢？\n查询缓存的失效非常频繁，只要有对一个表的更新，这个表上所有的查询缓存都会被清空。因此很可能你费劲地把结果存起来，还没使用呢，就被一个更新全清空了。对于更新压力大的数据库来说，查询缓存的命中率会非常低。除非你的业务就是有一张静态表，很长时间才会更新一次。比如，一个系统配置表，那这张表上的查询才适合使用查询缓存。MySQL 8.0版本直接将查询缓存的整块功能删掉了。\n分析器。词法分析，语法分析。\n优化器。优化器是在表里面有多个索引的时候，决定使用哪个索引；或者在一个语句有多表关联（join）的时候，决定各个表的连接顺序。\n执行器。执行语句。\nQ：更新语句呢，如何进行的？ A：与查询最大的不同在于，更新流程涉及两个重要模块：redo log（重做日志）和 binlog（归档日志）。redo log是InnoDB引擎特有的日志，而Server层自己的日志称为binlog。\n为什么会有两份日志呢\nredo log是InnoDB引擎特有的；binlog是MySQL的Server层实现的，所有引擎都可以使用。 redo log是物理日志，记录的是“在某个数据页上做了什么修改”；binlog是逻辑日志，记录的是这个语句的原始逻辑，比如“给ID=2这一行的c字段加1 ”。 redo log是循环写的，空间固定会用完；binlog是可以追加写入的。“追加写”是指binlog文件写到一定大小后会切换到下一个，并不会覆盖以前的日志。 WAL的全称是Write-Ahead Logging，它的关键点就是先写日志，再写磁盘。具体来说，当有一条记录需要更新，InnoDB引擎会先把记录写到redo log里并更新内存。同时，InnoDB引擎会在适当时，将操作记录更新到磁盘。\n与此类似，InnoDB的redo log是固定大小的，比如配置为一组4个文件，每个文件的大小是1GB，那么内存中总共就可以记录4GB的操作。从头开始写，写到末尾就又回到开头循环写。\nredo log有两个步骤，prepare和commit，也就是两阶段提交，这是为了让两份日志之间的逻辑保持一致。why？\n由于redo log和binlog是两个独立的逻辑，如果不用我们看看会有什么问题。仍然用update语句来做例。假设当前ID=2的行，字段c的值是0，再假设执行update语句过程中在写完第一个日志后，第二个日志还没有写完期间发生了crash，会出现什么情况呢？\n先写redo log后写binlog。假设在redo log写完，binlog还没有写完的时候，MySQL进程异常重启。由于我们前面说过的，redo log写完之后，系统即使崩溃，仍然能够把数据恢复回来，所以恢复后这一行c的值是1。 但是由于binlog没写完就crash了，这时候binlog里面就没有记录这个语句。因此，之后备份日志的时候，存起来的binlog里面就没有这条语句。 然后你会发现，如果需要用这个binlog来恢复临时库的话，由于这个语句的binlog丢失，这个临时库就会少了这一次更新，恢复出来的这一行c的值就是0，与原库的值不同。 先写binlog后写redo log。如果在binlog写完之后crash，由于redo log还没写，崩溃恢复以后这个事务无效，所以这一行c的值是0。但是binlog里面已经记录了“把c从0改成1”这个日志。所以，在之后用binlog来恢复的时候就多了一个事务出来，恢复出来的这一行c的值就是1，与原库的值不同。 redo log用于保证crash-safe能力。innodb_flush_log_at_trx_commit这个参数设置成1的时候，表示每次事务的redo log都直接持久化到磁盘。这个参数我建议你设置成1，这样可以保证MySQL异常重启之后数据不丢失。\nsync_binlog这个参数设置成1的时候，表示每次事务的binlog都持久化到磁盘。这个参数我也建议你设置成1，这样可以保证MySQL异常重启之后binlog不丢失。\nQ：事务隔离 A：当数据库上有多个事务同时执行的时候，就可能出现脏读（dirty read）、不可重复读（non-repeatable read）、幻读（phantom read）的问题。SQL标准的事务隔离级别包括：读未提交（read uncommitted）、读提交（read committed）、可重复读（repeatable read）和串行化（serializable ）。\n为什么尽量不要使用长事务？\n长事务意味着系统里面会存在很老的事务视图。由于这些事务随时可能访问数据库里面的任何数据，所以这个事务提交之前，数据库里面它可能用到的回滚记录都必须保留，这就会导致大量占用存储空间。除了对回滚段的影响，长事务还占用锁资源，也可能拖垮整个库，后面讲到锁时再具体展开。\n如何避免长事务呢？\n应用开发端：\n确认是否使用了set autocommit=0。 确认是否有不必要的只读事务。 业务连接数据库的时候，根据业务本身的预估，通过SET MAX_EXECUTION_TIME命令，来控制每个语句执行的最长时间，避免单个语句意外执行太长时间。 数据库端：\n监控 information_schema.Innodb_trx表，设置长事务阈值，超过就报警/或者kill； 在业务功能测试阶段要求输出所有的general_log，分析日志行为提前发现问题； 如果使用的是MySQL 5.6或者更新版本，把innodb_undo_tablespaces设置成2（或更大的值）。如果真的出现大事务导致回滚段过大，这样设置后清理起来更方便。 Q：索引 A：索引的出现其实就是为了提高数据查询的效率。实现方式：有序数组，哈希表，二叉搜索树，N叉树。InnoDB使用了B+树索引模型。\n根据叶子节点的内容，索引类型分为主键索引和非主键索引。基于非主键索引的查询需要多扫描一棵索引树，也就是多进行一次回表。\n若主键是乱序，插入一个值时对B+树的影响可能很大，因此有时会建议主键为自增主键。自增主键是指自增列上定义的主键，在建表语句中一般是这么定义的： NOT NULL PRIMARY KEY AUTO_INCREMENT。**显然，主键长度越小，普通索引的叶子节点就越小，普通索引占用的空间也就越小。**所以，从性能和存储空间方面考量，自增主键往往是更合理的选择。\n覆盖索引，联合索引\n最左前缀原则\n索引下推\n重建主键的过程不合理。不论是删除主键还是创建主键，都会将整个表重建。\nQ：全局锁和表锁（有点不清楚） A：MySQL提供了一个加全局读锁的方法，命令是 Flush tables with read lock (FTWRL)。全局锁的典型使用场景是，做全库逻辑备份。当你需要让整个库处于只读状态的时候，可以使用这个命令，之后其他线程的以下语句会被阻塞：数据更新语句（数据的增删改）、数据定义语句（包括建表、修改表结构等）和更新类事务的提交语句。\n表锁的语法是 lock tables … read/write。MySQL里面表级别的锁有两种：一种是表锁，一种是元数据锁（meta data lock，MDL)。MDL不需要显式使用，在访问一个表的时候会被自动加上。MDL的作用是，保证读写的正确性。在MySQL 5.5版本中引入了MDL，当对一个表做增删改查操作的时候，加MDL读锁；当要对表做结构变更操作的时候，加MDL写锁。\n读锁之间不互斥，因此你可以有多个线程同时对一张表增删改查。 读写锁之间、写锁之间是互斥的，用来保证变更表结构操作的安全性。因此，如果有两个线程要同时给一个表加字段，其中一个要等另一个执行完才能开始执行。 Q：行锁，如何减少行锁对性能的影响 A：MySQL的行锁是在引擎层由各个引擎自己实现的。行锁就是针对数据表中行记录的锁。\n（两阶段提交）**InnoDB事务中，行锁是在需要的时候才加上的，但并不是不需要了就立刻释放，而是要等到事务结束时才释放。这个就是两阶段锁协议。**因此尽量将最可能影响并发度的锁尽量往后放，减少该锁的等待时间，提高并发度。\n**（死锁和死锁检测）**死锁的两种解决策略：\n一种策略是，直接进入等待，直到超时。这个超时时间可以通过参数innodb_lock_wait_timeout来设置。在InnoDB中，innodb_lock_wait_timeout的默认值是50s。 另一种策略是，发起死锁检测，发现死锁后，主动回滚死锁链条中的某一个事务，让其他事务得以继续执行。将参数innodb_deadlock_detect设置为on，表示开启这个逻辑。（正常情况下采用该种策略居多） 但是检测是否是死锁的过程需要消耗大量的CPU资源，如何解决呢？\n一：关闭死锁检测。但是死锁的风险比消耗CPU资源更为大。 二：控制并发度。消耗大量的CPU资源的情况是并发度很高，如果在客户端进行并发控制去减少并发度。但是可行性仍然不高。有的应用有上百个客户端，即使每个客户端只有5个线程，峰值并发仍然可能达到数千。 基本思路在于：对于相同行的更新，在进入引擎之前排队，这样能够减少大量的死锁检测，这就需要更改数据库；另一种思路是将一行改成逻辑上的多行，减少锁冲突。\nQ：如果删除表的前10000行数据\n第一种，直接执行delete from T limit 10000; 第二种，在一个连接中循环执行20次 delete from T limit 500; 第三种，在20个连接中同时执行delete from T limit 500。 哪种方案最为合适？\n第二种方式相对较好。\n第一种方式，单个语句占用时间长，锁的时间也比较长；而且大事务还会导致主从延迟。\n第三种方式（即：在20个连接中同时执行delete from T limit 500），会人为造成锁冲突。\nQ：事务到底是隔离的还是不隔离的？ A：在可重复读隔离级别下，事务在启动的时候就“拍了个快照”。事务有三种：已提交事务，未提交事务，未开始事务。只有已提交事务可见。InnoDB利用了“所有数据都有多个版本”的这个特性，实现了“秒级创建快照”的能力。\n版本未提交，不可见； 版本已提交，但是是在视图创建后提交的，不可见； 版本已提交，而且是在视图创建前提交的，可见。 **更新数据都是先读后写的，而这个读，只能读当前的值，称为“当前读”（current read）。**除了update语句外，select语句如果加锁，也是当前读。\n事务的可重复读的能力是怎么实现的？\n可重复读的核心就是一致性读（consistent read）；而事务更新数据的时候，只能用当前读。如果当前的记录的行锁被其他事务占用的话，就需要进入锁等待。\n读提交的逻辑和可重复读的逻辑最主要的区别：\n在可重复读隔离级别下，只需要在事务开始的时候创建一致性视图，之后事务里的其他查询都共用这个一致性视图； 在读提交隔离级别下，每一个语句执行前都会重新算出一个新的视图。 “start transaction with consistent snapshot; ”的意思是从这个语句开始，创建一个持续整个事务的一致性快照。所以，在读提交隔离级别下，这个用法就没意义了，等效于普通的start transaction。\n对于可重复读，查询只承认在事务启动前就已经提交完成的数据； 对于读提交，查询只承认在语句启动前就已经提交完成的数据； ","date":"2025-02-01T00:00:00Z","permalink":"https://sutdown.github.io/p/mysql%E5%AE%9E%E6%88%9845%E8%AE%B2%E7%AC%94%E8%AE%B0/","title":"MySql实战45讲笔记"},{"content":"现在的我是大三，最近的一次坐火车应该是三年前，甚至和同学一道买的硬座，一整晚。\n这回却只有我自己，买火车也只是因为赶上春运高铁难买机票涨价，却意外想不到一切差了这么多。\n我记得我曾经坐火车的心情，一般会和家人，或者和朋友，父母那时候也并不放心我独自一人。大多时候情绪是欢欣雀跃，许是有人陪伴，许是前往未知的城市或者回到家乡，上了火车之后很快就睡着了，第二天再聊聊天就到了，从来不会觉得很难熬，我以为这次也会是一样的，所以买遍买了。\n这次的感受很不好。小小的床，乱七八糟的被子，很硬的床板，连睡衣都没换，甚至一路担心着行李，书包里是电脑，还有个斜挎包里装着些钱和证件，都是万万不可丢的，果然还是做小孩好，做小孩子不需要考虑这么多事情，也不会这么在意环境，最后一整晚几乎都没怎么睡着。只有四五点时慢慢习惯，然后在火车停止的间隙，那吵闹的轮轨声安静点的时候，会稍微的入眠点。\n不过糟糕也是接肘而至，有个大姨带着小孩，给小孩买了张卧铺，自己只是硬座，想在硬卧厢待着照顾小孩，话是这么说，事实当然不得而知，她跟列车员吵吵的时候这么说的，后来发觉她还和个更大的人一起，也可能是真的缺钱吧。我的位置就在车厢末，六七点大家也都起来了，陆陆续续的下车声，厕所的冲水声，吸烟难闻的气味……我觉得这个世界都面目可憎了起来，我的心情实在不怎么样。\n这个世界好嘈杂，我不再觉得它有趣了。\n那大姨的心思其实也不难理解，我想到了我的奶奶，小时候跟她一起坐火车了，那时候小，列车员会检查身份证和对应的位置，她让我藏到被子里，也只是想省个人的钱，这没什么。\n谁这么想坐长途火车回家呢，更快更安静的高铁火车不好吗，无非是想回家的人，抢不到票的人，想省钱的人，没钱的人。\n跟我对面大哥聊天，23岁，没读多少书，在加油站工作，月薪七八千包吃住，每个月能存个五千块钱，今年回家依旧相亲，据他描述他们那，两方看对眼了会很快订婚结婚一套流程走下来，甚至快点会有初中毕业辍学就结婚办酒的。之后忙不更迭的生孩子，照顾孩子，一辈子就这么过去了。\n现今很多年轻人都不太愿意结婚生子，我认识的有些人都是这样子，自己的生活遍地鸡毛，何必再拉个人来受这一生的苦。\n我对我的生活大体满意，我追求着我想要的目标，我想走到更远的地方，看更广阔的世界，婚姻一定概率上是意味着束缚，它给你绑定了一个必选项，你必须给它付出很多的心力，才有一定概率去取得一些乐趣，也可能没有乐趣。\n我大学期间的学习第一年还算得上活跃，做了很多加法，第二年面对完全陌生的新学校和新的同学，外来者其实仍旧和他们隔着一层始终存在的屏障。\n我的生活开始做减法，我尽量着和平时课堂，宿舍能见的人处理好关系，至于社团已经不再有心力再去参与什么了。\n第三年开始独来独往，跟人的交流愈发的少，那会我突然不想考研想就业了，学校给我的热情磋磨的太多了，我也更想自己有经济能力而不是伸手向父母要钱，家庭关系也是需要维护的一环。\n我父母不会干涉我很多，从小到大很少管过我，对我的学习也不会过问很多。我大多情况都是在自己摸黑做着决定。当初高考志愿也就用夸克瞎填一通，我觉得没什么，自己做出来的一切行为，我会为我自己负责，无论好坏。\n其实上大学后我很少愿意回家，连暑假都是只在家待半个月，寒假也就半个月毕竟要过年，这次都只有一周多了。比起我父母我更念叨我的爷爷奶奶，从小生活到大是更有感情的。\n许是我弟弟更加调皮，我很清楚的知道我的父母注意力更多的在他的身上。初中小学那会，和我弟我的父母从深圳开车回家的时候，我有点晕车很多时候只是闭着眼睛但没有睡着，和今天差不多，我也听到他们谈论很多东西，包括真的更挂念我弟弟多点。\n要问我嫉妒吗，我其实没什么感觉，毕竟二十岁的人了，我曾经希望我成为一个冷漠点的人这样子会伤心更少，现在成为了。我很大的符合我曾经期望的样子，我高兴嘛，当然。\n现今我只希望我能向上走，走的更远。\n我对着世间百态抱有同情心，但我更想远离这人生百态。\n很神奇，我妈妈希望我体验更好的比如租房支持我租近但贵的，交通支持我坐飞机高铁，但曾经她说更喜欢我弟，说我长大肯定是个冷漠的性子这也都是事实；我爸对我没有多加表态但是却总想让我租远点每日通勤多坐硬卧省点钱。\n人是这样子的，太立体了，我喜欢不上来我也讨厌不起来，我尽量去保持一个良好的关系，忘却掉那些我所不喜的，毕竟生活要做减法。\n除了与我自己密切相关的，其余以和为贵才是最不耗心力的做法。\n这里又回到硬卧了，这十七个小时的硬卧完全是在消磨我的精神，高铁机票起码会更安静点，起码能睡着更安心点。\n如果我是个男生就好了，如果我是我就不用忧心八九点下班时二十分钟从公司回到出租屋的路程，不用胡思乱想我点外卖明明备注了别敲门别打电话放门口那外卖员非得敲门敲个不停，不用晚上在房间睡觉时总是要看看房门窗户有没有锁好，不用每次打个出租都找着通讯录里能跟谁打个电话……\n在外生活和学校终究是不一样的，学校起码能保证更好的安全，更适宜的环境，真正需要想更多的也只有学习，早十晚十二的生活也不会经常觉得心累，如今上着早十晚八，明明更轻松了却更累了。\n我应该对生活更平和点的，而不是想这么多。这篇乱七八糟的碎碎念其实解答也不难，花更多的代价选择更省心的方式，一切便如此了。\n","date":"2025-01-27T00:00:00Z","permalink":"https://sutdown.github.io/p/%E7%9B%B8%E9%9A%94%E5%87%A0%E5%B9%B4%E7%9A%84%E4%B8%80%E6%AC%A1%E7%81%AB%E8%BD%A6%E7%A1%AC%E5%8D%A7/","title":"相隔几年的一次火车硬卧"},{"content":"HTTP基础 URL标准格式 方案/协议（TCP/IP传输协议） + 因特网地址（IP地址 / 数组形式的IP地址） + 资源路径\nHTTP方法 GET从服务器获取一份文档\nPUT将请求的主体存储到服务器上\nDELETE从服务器中删除命名资源\nPOST向服务器发送需要处理的数据\nHEAD只发送资源相应中的HTTP头部\n1.put和post的区别（命名资源和网关应用程序）\nHTTP状态码 1xx 信息提示 2xx成功 3xx重定向 4xx客户端错误 5xx服务器错误 HTTP报文结构 起始行，首部字段，主体\n请求报文：\n方法 路径 版本 ； 首部（Accept首部(比如Accept Encoding，Accept Language等)，条件请求首部(比如IF None match等)，安全请求首部(Cookie，Authorization)，代理请求首部 ； 主体 响应报文：\n版本 状态 ； 首部 （协商首部(Accept Range)，安全响应首部(set cookie, www Authenticate等)，实体首部(Content Type, Content Encoding, Etag, Last Modiied等) ）； 主体 响应报文中的10个基本字体首部字段：\ncontent type , content lenghth , content language , content encoding , content location , content range , content MD5 , last modified , expires , allow , Etag , cache control（HTTP/1.1)\nHTTP特性 简单灵活，易于扩展。\n基本报文格式header+body，头部信息以key-value的简单文本形式，易于理解。\nHTTP2.0之后提出压缩算法，针对头部信息中的冗余。\nHTTP中的各种请求方法等每个字段都允许开发人员自行定义和补充修改。\n应用广泛，跨平台。\n无状态，明文传输，不安全。\n无状态—\u0026gt;cookie/session\n明文传输—\u0026gt;SSL/TLS，基于UDP的QUIC都能起到加密的作用\nHTTP连接 从URL中解析出主机名，通过DNS协议查询主机名对应的IP地址，浏览器同时获得端口号； 浏览器根据相应的IP地址和端口号，建议一条到服务器的TCP连接 浏览器发送请求报文，服务器收到后发回响应报文 浏览器四次挥手关闭连接 TCP的数据通过IP分组的数据块发送 HTTP发送数据时以流的形式通过打开的TCP连接按序传输，TCP收到数据流后，将数据流分为断的数据块，封装在IP分组中，每个IP分组包括一个IP分组首部，一个TCP段首部，一个TCP数据块。(IP分组存在于网络层，TCP段存在于运输层)\nTCP连接通过四元组识别\u0026lt;源IP，源端口，目的IP，目的端口\u0026gt;\n提高HTTP的连接性能 并行连接。通过多条TCP连接发送并发的HTTP请求。（视觉上你能看到多个组件同时加载，但实际上还是并发）\n持久连接。重用TCP连接，消除连接，关闭时延。\nHTTP/1.0的“Keep alive” 和 HTTP/1.1的“persistent”。两者的差异在于前者默认关闭，后者默认开启。\n管道化连接。通过共享的TCP连接发起并发的HTTP请求。可以同时发出多个请求，避免了请求时的队头阻塞。\n复用连接。交替传送请求和响应报文。\n四次握手的关闭连接 TCP连接是双向的，服务器和客户端都存在输入和输出的信道。关闭连接的输出信道总是很安全的，关闭连接的输入信道存在很大的隐患，如果对方还有数据没有发送完，再次发送时会会送“连接被对端重置”的报文，此时操作系统会将其视为严重的错误，删除对端还未读取的所有缓存数据。\nHTTP缓存 缓存是数据传输中不可缺少的一环，优点如下：\n减少了冗余的数据传输 缓解了网络瓶颈的问题 降低了对原始服务器的要求 降低了距离时延 对于HTTP中的缓存，一般发生在客户端发起请求，缓存中的内容没有变化时，服务器会以一个304 Not Modified进行相应，这就是缓存命中。当然，如果收到404，说明对象被删除，缓存也会删除；收到200，说明服务器对象和缓存副本不同，服务器会重新发送一条带有内容的HTTP 200 OK相应。\n对于缓存的评判标准有两个，文档命中率 和 字节命中率。前者的提升对于降低整体延迟很有好处，后者对于节省带宽很有利。\n缓存的处理步骤 接收—缓存从网络中读取抵达的请求报文 解析—缓存对报文进行解析，提取出URL和各种首部 查询—缓存查看是否有本地副本，没有就获取一份副本 新鲜度检测—缓存查看已缓存的副本是否足够新鲜，如果不是，询问服务器是否有任何更新 创建响应—缓存会用新的首部和已缓存的主体构建一条响应报文 发送—缓存通过网络将响应发回客户端 日志—缓存可选地创建一个日志系统条目来描述这个事务 对于缓存的数据，首先查看文档是否过期\ncache control（定义了文档的最大使用期，也就是从第一次生成文档到文档不再新鲜无法使用为止） expires（指定一个绝对的过期日期,存在时钟不同步的问题）首部可以查看是否过期。 这里存在一个问题，cache control和expires谁的优先级高。\n应当是先看cache control 再看expires ，expires的结论可以覆盖cache control\n但是过期了也不能说明该 j 缓存一定无效，此时需要进行服务器再验证 1.如果内容变化，缓存会获取新的副本存储在就文档的位置发送给客户端 2.如果内容不变，缓存会获取新的首部，包括一个新的过期日期，更新首部。\nif modified since \u0026lt;last modified date\u0026gt;如指定日期之后文档被修改，它的条件就为真，新文档会返回缓存，同时返回新的指定日期,返回200；如果为假，返回304，同时返回需要修改的部分值，比如指定日期。\nif none match \u0026lt;tags\u0026gt;(上一种方法的缺陷很明显，那就是date，世界各地都存在时区，数据的传世也存在时延，那么这就是不准确的，因此HTTP重新提出了一种称为“版本标识符”的实体标签–ETag)。这里存的是已经存在的内容版本，可以看这个例子就明了了\n1 2 3 if none match : \u0026#34;v2.6\u0026#34; \u0026#34;v2.4\u0026#34; ETag : \u0026#34;v2.6\u0026#34; // 304 ETag : \u0026#34;v3.0\u0026#34; // 200 至于常说的强缓存和协商缓存，前者指的是判断缓存是否过期，后者用于服务器再次验证。\nHTTP版本 HTTP/0.9 只有GET方法，没有请求头，服务器只返回HTML\nHTTP/1.0 引入请求头和响应头，支持多种请求方法和状态码，依然是短连接\nHTTP/1.1 开始支持长连接；\n容易引申问题：HTTP的Keep alive和TCP的keep alive，前者指支持长连接，后者为保活机制\n管道网络传输：客户端可以同时发送多个请求，解决了请求的队头阻塞，但是服务端依然只能按顺序接收，依旧存在阻塞。\n队头阻塞：顺序发送的请求序列中的某个请求阻塞后，后面的请求也会一并被阻塞。\nHTTP/2.0 stream并发：引出stream概念，多个stream复用一条TCP连接，针对不同HTTP用独一无二的id区分，不同的stream可以乱序发送，因此可以并行交错的发送请求和响应。解决了请求和响应的队头阻塞。 但是存在传输层的队头阻塞，传输时要保证内核中的数据是顺序的，只有前1个数据到达，后面的才会到达缓冲区，因此也会阻塞。\n增加HPack对头部压缩。（静态字典，动态字典，Huffman编码）\n增加TLS 1.2+。\n缺点：\n队头阻塞 TCP和TLS握手时的延迟 网络迁移需要重新连接\nHTTP/3.0 HTTP/3 把 HTTP 下层的 TCP 协议改成了基于 QUIC 的 UDP\n特点：\n零RTT连接建立 无队头阻塞 连接迁移 向前纠错机制 用户识别机制 承载用户信息HTTP首部 From 用户的Email地址 User Agent 用户的浏览器软件 Referer 用户来源页面的URL 然而这三者都不能建立可靠的的识别。\n客户端IP地址 ip地址只能区分机器，不能区分不同用户 用户每次登录时，ip地址会发生变化 Web服务可能只会看见共享的（NAT）防火墙地址或者代理服务器的ip地址，并不能看见真正的地址 用户登录 HTTP中的www-Authenticate首部和Authorization首部向Web站点传送用户的相关信息。\n用户输入用户名和密码时，浏览器会重复原来的请求，添加Authorization作为身份的标识，在之后的每次请求，浏览器都会自动发起相应的用户名和密码，这样就可以在整个会话期间维持用户的身份。\n胖URL 在URL中添加了一些信息，将Web服务器上若干个HTTP事务捆绑成一个会话或者访问，首次访问时生成唯一ID，此后每次浏览该站点时都会采用胖URL进行识别。\ncookie cookie的基本思想在于让浏览器积累一组服务器特有的信息，每次访问服务器时提供这些信息。cookie规范的正式名称为HTTP状态管理机制。下面这两种唯一的区别在于他们的过期时间。\n会话cookie。只会持续到用户退出浏览器为止自动被破坏。 持久cookie。有过期日期，会在硬盘上存储到哪个日期为止。 用户首次访问Web站点时，服务器发出中的相应中带有set cookie，这也是对用户独有的标识码，浏览器也会记住这个cookie存储在浏览器的cookie数据库中，当用户返回同一站点时能够自动识别。\ncookie版本0 主要由set cookie响应首部，cookie请求头部以及用于控制cookie的字段构成。\n1 2 3 4 set cookie： NAME=VALUE（强制） Expires（可选） Domain（可选） Path（可选） Secure(可选，SSL) Cookie: session-id=0021145271-8271212; session-id-time=2881222899 cookie版本1 RFC 2965 能够与版本0相互操作 增加解释性文本，对目的进行解释 浏览器退出时可以将cookie强制销毁 用相对时间而不是绝对时间标识cookie的过期日期 控制cookie除了域和路径增加了URL端口号 增加了版本号 cookie：在客户端记录信息确定用户身份，是服务端发送存储在本地用户浏览器的小型文本文件。\nsession：在服务端记录信息确定用户身份。用于维护用户登录状态，存储用户临时数据，上下文信息等。会话标识符，记录当时的会话活动，时间等。\n认证 基本认证只会对用户名和密码进行一层的Base64编码，但是这个时很容易解码 的，所以基本也可以等同于明文运输，安全性极低。\nHTTPS HTTPS = HTTP + SSL/TLS(位于HTTP层和TCP层之间)\n对称加密：每对发送者和接收者之间存在共享的保密密钥。 非对称加密：每个发送者或者接收者都存在公钥和私钥，公钥加密，私钥解密 一般会采用非对称的公开密钥加密技术建立安全通信，再利用这条安全的通道发送临时的随机对称密钥对其余的数据进行加密。\n另外：\n数字签名用于判定谁编写的报文，同时证明报文未被篡改。（原理依旧是非对称加密）\n数字证书，由官方的“证书颁发机构”以数字方式签发的。\nHTTPS连接 客户端建立到服务器端口443的TCP连接 SSL安全参数握手（交换协议版本号，选择密码，身份认证（CA签发的证书），生成临时的会话密钥加密信道） 客户端SSL上发送HTTP请求/TCP上发送已加密的请求 服务端SSL上发送HTTP响应/TCP上发送已加密的响应 SSL关闭通知 TCP关闭通知 参考资料 很高兴你能看完我的文章，因为近期在找实习，所以重新看起了曾经学过的知识，沟通了很多，简历也投出去了一部分，但是杳无音讯，多少有点落寞了。\n重点回归，看八股的话，计网最为出名的就是小林coding的图解网络，写的确实好，但总感觉少了什么，这几天看了书之后明悟了很多，八股只能作为最后突击的材料，终究比不上自己去看经典书籍效果要好。我写的笔记也终究只是我个人总结的想法。\n推荐可以看看这本书《HTTP权威指南》，写的很好，提前也可以看看小林的图解网络，带着面试可能会问的问题读，效果会更好。\n缺点在于比较新版本的HTTP没有讲清楚，比如HTTP2.0，3.0几乎没怎么提到，或者跟RPC相关的之类的。以后再补充。\n1.图解网络 - 小林coding\n2.书籍《HTTP权威指南》\n","date":"2025-01-20T00:00:00Z","permalink":"https://sutdown.github.io/p/http%E8%AF%A6%E8%A7%A3/","title":"HTTP详解"},{"content":"2024.9-2024.12阶段性总结 先讲讲时间线吧，倒叙。\n2024.12.3 - 2024.12.25\n找实习。背景：cpp选手，本2有9的联合培养经历。\n百度测开12.19一面，12.23二面，12.24主管面，已有口头offer\n北京慧测java中间件12.20一面，12.23二面，12.25hr面已拒\n赫鲁丝网络游戏服务器开发12.20一面挂\n柠檬微趣12.9投递，笔试挂\nDolphinDB12.9投递，笔试挂\n小红书，快手，滴滴，momenta，百度等后端简历挂，其它都筛）\ncpp找实习太难了，机会少要求高，中大厂连简历都过不了，小厂的岗位少，boss投出去了十几份简历但是杳无音信，难过，所以最终决定百度测开了。\n总结反思：\n未来方向：\n如果走后端的话，那就要再学学go；或者学学测开的知识，走测开。或者all in，两份简历，这个还是需要好好考虑一下。\n面试中反应的不足：\n技术上：计网，操作系统，cpp，mysql相关的知识点不能单靠背八股，要多看各种经典书籍，去理解它们之间的关系。 算法：这几次的算法一半一半吧，好消息是都有思路，坏消息是思路不一定能完全用代码写出来。 表述能力上：针对有些知识点，大致了解，但是不能完全讲清楚，包括项目的框架讲述。 隐患：\n这次的几回面试都没有很深的问项目，但是项目仍然是很重要的一环。\n2024.9 - 2024.11\n由于9月份就投过简历，大致知道八股的重要性，因此9到11月也一直在有意无意的看了很多重点知识点。\n前后学习了C++演化(论文现代C++白皮书，书籍深入理解C++11特性)，解析MyTinySTL，由C++14实现的Webserver，Leveldb源码阅读，基于LSM结构实现kv存储引擎，Coroutinelib协程库项目实现，MySQL必知必会，《mysql是怎样运行的》阅读笔记，除了这些写过笔记的，也有没写笔记的，比如《HTTP权威指南》，《操作系统导论》，《linux高性能服务器编程》等。\n基本思路就是，cpp语法和新特性，stl容器，网络编程，数据库，协程这样子。因为还要并行本学期的课程，尤其11月考了三门期末，也开始大作业不断，忙麻了。12月则是先考期末，再考六级，六级之后就面试，时间线真紧密啊）。算上隔一两周会小摆的情况，日均学习时长应该有10h+了。基本宿舍食堂图书馆三点一线，课程有些去了有些没去，大部分的关注力都在自学上，其实这样的效果还是可以的。\n相比于大二，又想保研又想搞竞赛又想搞大创，最后什么都得不到的结果还是好点的。\n其实到现在还有点恍惚，除了等待offer正式发出的一点紧张之外，整个人还都在一种恍惚的状态。\n这周打算悠闲一下，主要补一下有几门课程的作业，看看原神纳塔没看完的剧情就差不多吧。如果快的话，明天offer结果应该就能出来，大约是下周一12.30入职，offer出来后想想怎么租房，然后估计周日去北京吧。\n很高兴这个阶段落下了帷幕，不过这也才是第一步，我仍在路上。\n（欸对，这周的事情差不多结束，大概在周末的时候想想之后的学习计划，比如测开和开发该如何偏好，测开的话是什么学习路线，开发的话是什么学习路线，比如怎么更好的理解八股，比如如何提高算法能力，比如如何提高表述能力……其实关键点在于实习这个过大的不确定因素，之后对测开越来越了解的我很容易推翻今日的计划，所以这点也需要考虑，先缓缓，周末考虑了）\n由于对测开的了解有限，年前也就是一月份的重点放在对于工作中测开的学习上，同时看些计网，os，操作系统，数据库等的书籍或者视频，最好能结合代码。leetcode每天坚持三道题左右，一月应该能将leetcode再刷一遍，注意复习。c++之前也搜集过一些有意思的视频。go的学习也可以考虑在内，具体计划周日晚上再写吧，下周执行。\n","date":"2024-12-30T00:00:00Z","permalink":"https://sutdown.github.io/p/2024.9-2024.12%E9%98%B6%E6%AE%B5%E6%80%A7%E6%80%BB%E7%BB%93/","title":"2024.9-2024.12阶段性总结"},{"content":"Docker （宿主机上的一个进程）\ndocker使用namespace去提供称为容器的工作空间\ndocker引擎在linux中会使用以下命名空间：\npid 进程id，进程隔离 (process id) net 管理网络接口 (networking) ipc 管理进程间通信资源 (interprocess communication) mnt 管理文件系统挂载(mount) uts 隔离内核和版本证明 （unix timesharing system) cd /proc/\ncd 1\nll ns/\n容器的命名空间和宿主机的命名空间是隔离的\nman手册\ncontrol groups\n限制应用的资源集合\n允许docker引擎共享容器的硬件资源\n限制，控制，分离一个进程组的资源\nunion file sysytem\n通过创建层使得它能轻量级和快\ncontainer format\ndocker引擎包含namespace,control groups和union file system将这三者进行一个封装，称为container format，这个默认的容器格式是libcontainer.\ndockerfile\ndocker build -t my-node-app . 构建镜像\ndocker pull\ndocker run -p 3000:3000 my-node-app 允许基于镜像的容器\n","date":"2024-12-29T00:00:00Z","permalink":"https://sutdown.github.io/p/docker/","title":"Docker"},{"content":"个人github链接：\nGitHub - Sutdown/coroutinelib: coroutine lib\n模块\nthread\n线程模块，封装了pthread里面的一些常用功能，Thread,Semaphore,Mutex,RWMutex,Spinlock等对象，可以方便开发中对线程日常使用 为什么不适用c++11里面的thread 本框架是使用C++11开发，不使用thread，是因为thread其实也是基于pthread实现的。并且C++11里面没有提供读写互斥量，RWMutex，Spinlock等，在高并发场景，这些对象是经常需要用到的。所以选择了自己封装pthread\n协程类\n协程：用户态的线程，相当于线程中的线程，更轻量级。后续配置socket hook，可以把复杂的异步调用，封装成同步操作。降低业务逻辑的编写复杂度。 目前该协程是基于ucontext_t来实现的，后续将支持采用boost.context里面的fcontext_t的方式实现\n协程调度\n协程调度器，管理协程的调度，内部实现为一个线程池，支持协程在多线程中切换，也可以指定协程在固定的线程中执行。是一个N-M的协程调度模型，N个线程，M个协程。重复利用每一个线程。\n协程IO\n继承与协程调度器，封装了epoll（Linux），并支持定时器功能（使用epoll实现定时器，精度毫秒级）,支持Socket读写时间的添加，删除，取消功能。支持一次性定时器，循环定时器，条件定时器等功能\n定时器\nhook\nhook系统底层和socket相关的API，socket io相关的API，以及sleep系列的API。hook的开启控制是线程粒度的。可以自由选择。通过hook模块，可以使一些不具异步功能的API，展现出异步的性能。如（mysql）\nthread 主要有两个类，Semaphore和Thread\nSemaphore 信号量，实现PV操作，主要用于线程同步\nThread 系统自动创建主线程t_thread\n由thread类创建的线程。\nm_thread 通常是线程类内部的成员变量，用来存储底层的线程标识符\nt_thread 可能是外部管理线程生命周期的对象或容器，它可以是线程池、线程列表、智能指针等，帮助你在类外部管理多个线程的创建、执行、销毁等操作。\n协程类 非对称模型 有栈协程，独立栈。 对于协程类，其中需要什么。协程首先需要随时切换和恢复，这里采用的是glibc的ucontext组件。\nucontext_t 这个类中有成员：\n1 2 3 4 5 6 7 8 // 当前上下文结束后下一个激活的上下文对象的指针，只在当前上下文是由makecontext创建时有效 struct ucontext_t *uc_link; // 当前上下文的信号屏蔽掩码 sigset_t uc_sigmask; // 当前上下文使用的栈内存空间，只在当前上下文是由makecontext创建时有效 stack_t uc_stack; // 平台相关的上下文具体内容，包含寄存器的值 mcontext_t uc_mcontext; 函数：\n1 2 3 4 5 6 7 8 9 10 11 // 获取当前上下文 int getcontext(ucontext_t *ucp); // 恢复ucp指向的上下文 int setcontext(const ucontext_t *ucp); // 修改当前上下文指针ucp，将其与func函数绑定 void makecontext(ucontext_t *ucp, void (*func)(), int argc, ...); // 将当前上下文保存到oucp中，将执行转到ucp中 int swapcontext(ucontext_t *oucp, const ucontext_t *ucp); 对于该协程类，有栈 or 无栈？对称 or 非对称？\n对于对称和非对称的话，对称协程更为灵活，非对称协程更为简单易实现。协程中一般存在协程调度器和协程两种角色，对称协程中相当于每个协程都要充当调度器的角色，程序设计复杂，程序的控制流也会复杂难以管理。\n常见的js中的async/await，go中的coroutine都是非对称协程，是因为非对称协程的切换过程是单项的，更适合事件驱动，任务队列等调度模型；但是c语言中的ucontext属于对称协程的经典实现，boost.context为对称协程的现代实现，更适合需要多个协程频繁通信的场景。\n有栈协程和无栈协程有栈和无栈的本质区别在于是否可以在任意嵌套函数中被挂起。一般有栈可以被挂起，无栈则不行。有栈比较适用于功能强大，支持嵌套调用和复杂控制流，灵活的操作上下文的需求，比如boost.COntext；无栈由于存储在内存中，适用于内存占用少，实现简单的场景，比如JavaScript async/await 和 Promise，Erlang 和 Go的Goroutine。\n这里我们的协程类，采用的是非对称模型，有栈协程。因此可以推导出所需要的私有成员：\n1 2 3 4 5 6 7 8 9 10 11 private: uint64_t m_id = 0; State m_state = READY; ucontext_t m_ctx; uint32_t m_stacksze = 0; // 栈大小 void *m_stack = nullptr; // 栈空间 std::function\u0026lt;void()\u0026gt; m_cb; // 运行函数 bool m_runInScheduler; 对于协程类，我们需要一个主协程和其它的用户协程，以及一个协程调度器。对于主协程则是直接无参构造函数直接创建，（由于只能创建一次，因此私有），有参构造函数创建其它协程。同时需要设置resume,yield其它函数调度协程的运行。大概这个样子：\n1 2 3 4 5 // 线程局部变量，当前线程正在运行的协程 static thread_local Fiber *t_fiber = nullptr; // 线程局部变量，当前线程的主协程，切换到这个协程，相当于切换到主线程 static thread_local std::shared_ptr\u0026lt;Fiber\u0026gt; t_thread_fiber = nullptr; static thread_local Fiber *t_scheduler_fiber = nullptr; 协程调度 一个线程只有一个协程，一个协程类中会包含三个协程，分别是主协程（main），调度协程和任务协程。其中任务协程是由协程类自主创建，主协程和调度协程都是静态变量，在多种类中其实只存在一个实体。\n协程调度致力于封装一些操作，因为调度协程本身需要创建协程，协程任务的执行顺序，如何利用多线程或者调度协程池保证效率，在协程任务结束之后也需要停止调度器释放资源。如果建立一个scheduler类封装这些操作，那么为用户开放的仅仅只有启动线程池，关闭线程池，添加任务三种操作了。\n其中main主协程可以选择是否参与调度，如果不参与，那么比如在main开始调度时创建其它协程进行协程调度；如果参与，多线程的情况下和不参与相同。如果是单线程，那么只能等到main结束时开始调度其它协程。\n虽然main是主协程（caller协程），不过main函数所在的线程也能执行任务，在实现相同调度能力的情况下，线程数越少，线程切换的开销也就更小。\n最终过程：\nmain函数主协程运行，创建调度器 向调度器添加任务，开始协程调度，main让出执行权，调度协程执行任务队列中的任务 每次执行任务时，调度协程都要让出执行权，再回到调度协程继续下一个任务 所有任务执行完后，调度协程让出执行权切回main函数主协程结束。 协程IO 在前面的协程调度模块中，调度器对协程的调度是无条件执行的，在调度器已经启动调度的情况下，任务一旦添加成功，就会排队等待调度器执行。调度器不支持删除调度任务，并且调度器在正常退出之前一定会执行完全部的调度任务，所以在某种程度上可以认为，把一个协程添加到调度器的任务队列，就相当于调用了协程的resume方法。\nIO协程调度支持为描述符注册可读和可写事件的回调函数，当描述符可读或可写时，执行对应的回调函数。\n有的库不仅可以处理socket fd事件，还可以处理定时器事件和信号事件。这些事件库的实现原理基本类似，都是先将套接字设置成非阻塞状态，然后将套接字与回调函数绑定，接下来进入一个基于IO多路复用的事件循环，等待事件发生，然后调用对应的回调函数。\n改造协程调度器，将epoll和协程调度结合。IO协程调度关注FdCOntext信息，也就是描述符，事件，回调函数三元组。 基于epoll实现IO事件的添加，删除，调度，取消等功能 timer会给协程IO外挂一个定时器管理模块，epoll会根据定时器的超时时间确定超时参数 pipe设置的作用\n类似于进程间通信.\n属于状态传递。比如p[0]和p[1]，p[0]为可读，p[1]为可写。在本过程中，p[0]中存放阻塞的协程，当有协程任务完成时会放入p[1]，p[1]会通知p[0]，从而让调度器run\n在 epoll 中，如果使用 边缘触发（ET） 模式，只有当管道的状态从不可读变为可读时，epoll 才会通知调度器。这就意味着，调度器需要及时读取管道中的数据，以确保不会错过事件。\n管道变为可读时，会唤醒协程的原因是管道充当了一个信号通知机制。通过向管道的写端写入数据，调度器可以通过读取管道来检测到事件的发生，从而恢复挂起的协程。在协程 I/O 模型中，管道的可读状态就是调度器知道某个事件发生并且可以继续执行协程的信号。这种机制使得协程的调度更加高效，避免了繁重的轮询操作，并且能够通过 I/O 多路复用和事件驱动的方式来处理并发任务。\n协程执行 I/O 操作并挂起： 一个协程在执行 I/O 操作时，可能会遇到阻塞情况（例如等待网络数据或磁盘读写），于是它会被挂起。挂起的协程会注册相关的文件描述符（如 m_tickleFds[0]）到 epoll，让调度器等待这些文件描述符的状态变化。 信号发送（管道写端）： 另一个协程或线程会向 m_tickleFds[1] 写入数据。这通常表示某个事件或任务已经完成，或者需要通知调度器去恢复某个协程的执行。 当数据写入 m_tickleFds[1] 时，管道的读端（m_tickleFds[0]）就变为可读。 epoll 通知调度器： epoll 会在 m_tickleFds[0] 变为可读时通知调度器，这时调度器知道管道中有数据，可以读取并继续执行后续操作。 调度器恢复协程： 调度器通过 epoll_wait() 等待管道事件的触发，当管道变为可读时，调度器会恢复挂起的协程。恢复后的协程将继续执行它的任务，直到下一个 I/O 操作发生，或者任务完成。 读取管道数据： 当管道可读时，调度器会调用 read(m_tickleFds[0], ...) 从管道中读取数据。此时，管道中的数据只是一个信号，指示协程应该恢复执行。读取数据的操作不会对协程本身产生影响，但它确保了管道的数据被消费，防止事件丢失。 epoll and scheduler\nepoll：当某个文件描述符准备好进行读写操作时，epoll 会通知应用程序。 scheduler：管理和调度多个协程的执行 协程执行 I/O 操作，发起 read 或 write 等阻塞操作。 协程调度器将该协程挂起，并将文件描述符注册到 epoll 中，等待 I/O 完成。 调度器继续调度其他协程或任务（例如，处理其他 I/O 请求）。 epoll 监听文件描述符的状态，发现某个文件描述符可读或可写时，通知调度器。 调度器根据 epoll 返回的事件唤醒对应的协程，恢复执行。 idle的触发，自动触发还是人为触发\nidle 协程的作用是作为一个事件循环，专门处理 I/O 事件和定时器超时事件。它通常在 没有其他待处理任务时 被调度运行。（调度器为空时，处理epoll的IO事件）\nidle 协程的执行流程\n进入 idle 协程：当协程调度器发现没有任何任务需要执行时，它会选择 idle 协程。idle 协程会通过 epoll_wait 等机制进入阻塞状态，等待 I/O 或定时器事件。 事件发生时唤醒 idle 协程： I/O 事件：当某个文件描述符变得可读或可写，或者出现连接事件时，epoll_wait 会返回相应的事件。idle 协程会从阻塞状态中唤醒并处理这些事件。 定时器事件：如果有定时器超时，idle 协程会处理定时器回调，并将相应的任务调度到执行队列中。 完成事件处理后重新挂起： 在处理完所有的事件之后，idle 协程会检查是否有新的任务需要处理。如果没有，它会调用 yield() 或其他方式挂起自己，等待下一次事件发生。 定时器 最小堆定时器\n创建协程和事件注册： 协程开始执行某些 I/O 操作时（例如，网络读取、文件读取等），如果该操作是阻塞的，它会通过 epoll 进行非阻塞的 I/O 多路复用，等待 I/O 完成。 同时，如果需要设置超时或定时任务，定时器会创建并开始计时。定时器会生成一个文件描述符，可以通过 epoll 监视。 调度器挂起协程： 调度器将执行中的协程挂起，并将其状态保存。 调度器通过 epoll 将相关的文件描述符（如网络套接字、定时器等）注册到 epoll 中，以便监听 I/O 事件和定时器事件。 epoll 等待事件： 调度器会调用 epoll_wait()，等待 I/O 事件或定时器事件的发生。 epoll_wait() 是阻塞的，它会一直等待，直到至少一个注册的文件描述符发生事件（如 I/O 准备好或定时器超时）。 事件发生，唤醒协程： 一旦某个文件描述符发生了事件（如 I/O 完成或定时器到期），epoll 会通知调度器。 调度器根据事件类型（如文件描述符是否可读、可写或定时器是否到期）选择唤醒对应的协程，并恢复执行。 协程继续执行： 恢复的协程可以继续执行其原本的 I/O 操作，或是进行其他任务。 hook hook是对系统调用API进行一次封装，将其封装成一个与原始的系统调用API同名的接口在应用这个接口时，会先执行封装中的操作，再执行原始的系统调用API。\n本项目中，出于保障协程能够在发生阻塞时随时切换，因此会对IO协程调度中相关的系统调用进行hook，让调度协程尽可能把时间片花在有意义的操作上。 hook的重点在于替换API的底层实现同时完全模拟原本的行为*/\n本项目中，关于hook模块和IO协程调度的整合，一共有三类接口需要hook：\nsleep延时系列接口。对于这些接口，给IO协程调度器注册一个定时事件，定时事件触发之后再执行当前协程即可。 注册完之后yield让出执行权。\nsocket IO系系列接口。包括read/write/recv/send/connect/accept\u0026hellip;这类接口的hook需要先判断fd是否为socket fd， 以及用户是否显式的对该fd设置过非阻塞模式，如果都不是，就不需要hook了。如果需要hook，现在IO协程调度器上注册对应读写事件，事件发生后再继续当前协程。当前协程注册完IO之后即可yield让出执行权。\nsocket/fcntl/ioctl/close\u0026hellip;这类接口主要用于处理边缘情况， 比如fd上下文，处理超时，显示非阻塞等。\n我的钩子函数如何覆盖系统调用的\n假设以sleep为例，下面可以确保sleep_f可以指向原始的系统调用sleep函数\n1 2 3 #define XX(name) name##_f = (name##_fun)dlsym(RTLD_NEXT, #name); HOOK_FUN(XX) #undef XX 参考 代码随想录 - coroutine-lib - github libco - github 出于什么样的原因，诞生了「协程」这一概念 协程理论 什么是协程 协程的好处 ","date":"2024-12-21T00:00:00Z","permalink":"https://sutdown.github.io/p/coroutinelib%E9%A1%B9%E7%9B%AE%E5%AE%9E%E7%8E%B0/","title":"Coroutinelib项目实现"},{"content":"B+ tree mysql，能够很好的组织磁盘数据。一般用B+树存储索引数据。结点映射相邻的磁盘页\n（从磁盘中读取数据的时候，需要经过内核，系统调用，读取一个字节时会返回一页的数据）\nB+树和红黑树的区别\n红黑树 平衡二叉搜索树 平衡规则在于从根节点到叶子结点所拥有的黑色结点数相同 查找时间复杂度O(logn)\nB+/B树 多路平衡搜索树\nB+树（数据只存储在叶节点中）没有与内部结点关联的数据，一页内存中可以容纳更多的键，访问叶节点上的数据所需的缓存未命中次数更少。\n叶结点之间相互链接，对树种所有对象进行全面扫描只需要线性遍历所有叶节点。B树则要遍历每一层，这可能存在更多的缓存未命中\nB树包含每个键的数据，经常访问的结点更靠近跟，可以更快访问\n由于多路平衡一般树高度不高，”矮胖“类型，层高也同时代表着磁盘IO的次数\n降低磁盘IO方法：\n减少次数 转化为顺序IO（能访问同一页的次数，减少频繁访问磁盘） 数据库查询方式：\n单点查询 范围查询 就地更新的数据结构\n时间轮 https://juejin.cn/post/7083795682313633822\n海量定时任务检测，多线程环境下定时器的设计。\n时间轮 是一种 实现延迟功能（定时器） 的 巧妙算法。如果一个系统存在大量的任务调度，时间轮可以高效的利用线程资源来进行批量化调度。把大批量的调度任务全部都绑定时间轮上，通过时间轮进行所有任务的管理，触发以及运行。能够高效地管理各种延时任务，周期任务，通知任务等\n定时器\n时间序，按照过期时间排序 执行序 指针数组，时间指针 分为时针，分针，秒针；每个小时都会有时针层级的任务重新映射到分针层级，每分钟都会有分针层级的任务重新映射到秒针层级，每秒钟都会取出当前任务执行。\n跳表 高并发有序存储https://www.jianshu.com/p/9d8296562806\n跳表是可以实现二分查找的有序链表； 每个元素插入时随机生成它的level； 最底层包含所有的元素； 如果一个元素出现在level(x)，那么它肯定出现在x以下的level中； 每个索引节点包含两个指针，一个向下，一个向右；（笔记目前看过的各种跳表源码实现包括Redis 的zset 都没有向下的指针，那怎么从二级索引跳到一级索引呢？留个悬念，看源码吧，文末有跳表实现源码） 跳表查询、插入、删除的时间复杂度为O(log n)，与平衡二叉树接近； LSM Tree 更高写性能以及更高空间利用率的数据存储结构https://cloud.tencent.com/developer/article/1441835\nLSM-Tree全称是Log Structured Merge Tree，是一种分层，有序，面向磁盘的数据结构，其核心思想是充分了利用了，磁盘批量的顺序写要远比随机写性能高出很多。这种结构虽然大大提升了数据的写入能力，却是以牺牲部分读取性能为代价，故此这种结构通常适合于写多读少的场景。\nSSTable是一种拥有持久化，有序且不可变的的键值存储结构，它的key和value都是任意的字节数组，并且了提供了按指定key查找和指定范围的key区间迭代遍历的功能。SSTable内部包含了一系列可配置大小的Block块，典型的大小是64KB，关于这些Block块的index存储在SSTable的尾部，用于帮助快速查找特定的Block。当一个SSTable被打开的时候，index会被加载到内存，然后根据key在内存index里面进行一个二分查找，查到该key对应的磁盘的offset之后，然后去磁盘把响应的块数据读取出来。当然如果内存足够大的话，可以直接把SSTable直接通过MMap的技术映射到内存中，从而提供更快的查找。\n如果SSTable的分层越多，那么最坏的情况下要把所有的分层扫描一遍，对于这种情况肯定是需要优化的，如何优化？在 Bigtable 论文中提出了几种方式：\n1，压缩\nSSTable 是可以启用压缩功能的，并且这种压缩不是将整个 SSTable 一起压缩，而是根据 locality 将数据分组，每个组分别压缩，这样的好处当读取数据的时候，我们不需要解压缩整个文件而是解压缩部分 Group 就可以读取。\n2，缓存\n因为SSTable在写入磁盘后，除了Compaction之外，是不会变化的，所以我可以将Scan的Block进行缓存，从而提高检索的效率\n3，索引，Bloom filters\n正常情况下，一个读操作是需要读取所有的 SSTable 将结果合并后返回的，但是对于某些 key 而言，有些 SSTable 是根本不包含对应数据的，因此，我们可以对每一个 SSTable 添加 Bloom Filter，因为布隆过滤器在判断一个SSTable不存在某个key的时候，那么就一定不会存在，利用这个特性可以减少不必要的磁盘扫描。\n4，合并\n这个在前面的写入流程中已经介绍过，通过定期合并瘦身， 可以有效的清除无效数据，缩短读取路径，提高磁盘利用空间。但Compaction操作是非常消耗CPU和磁盘IO的，尤其是在业务高峰期，如果发生了Major Compaction，则会降低整个系统的吞吐量，这也是一些NoSQL数据库，比如Hbase里面常常会禁用Major Compaction，并在凌晨业务低峰期进行合并的原因。\nLSM-Tree的设计思路是，将数据拆分为几百M大小的Segments，并是顺序写入。\nB+Tree则是将数据拆分为固定大小的Block或Page, 一般是4KB大小，和磁盘一个扇区的大小对应，Page是读写的最小单位。\n","date":"2024-12-18T00:00:00Z","permalink":"https://sutdown.github.io/p/%E5%90%8E%E7%AB%AF%E5%9B%9B%E7%A7%8D%E5%B1%82%E5%BC%8F%E7%BB%93%E6%9E%84/","title":"后端四种层式结构"},{"content":"协程库项目实现2 - thread，协程类 thread 主要有两个类，Semaphore和Thread\nSemaphore 信号量，实现PV操作，主要用于线程同步\nThread 系统自动创建主线程t_thread\n由thread类创建的线程。\nm_thread 通常是线程类内部的成员变量，用来存储底层的线程标识符\nt_thread 可能是外部管理线程生命周期的对象或容器，它可以是线程池、线程列表、智能指针等，帮助你在类外部管理多个线程的创建、执行、销毁等操作。\n协程类 非对称模型 有栈协程，独立栈。 对于协程类，其中需要什么。协程首先需要随时切换和恢复，这里采用的是glibc的ucontext组件。\nucontext_t 这个类中有成员：\n1 2 3 4 5 6 7 8 // 当前上下文结束后下一个激活的上下文对象的指针，只在当前上下文是由makecontext创建时有效 struct ucontext_t *uc_link; // 当前上下文的信号屏蔽掩码 sigset_t uc_sigmask; // 当前上下文使用的栈内存空间，只在当前上下文是由makecontext创建时有效 stack_t uc_stack; // 平台相关的上下文具体内容，包含寄存器的值 mcontext_t uc_mcontext; 函数：\n1 2 3 4 5 6 7 8 9 10 11 // 获取当前上下文 int getcontext(ucontext_t *ucp); // 恢复ucp指向的上下文 int setcontext(const ucontext_t *ucp); // 修改当前上下文指针ucp，将其与func函数绑定 void makecontext(ucontext_t *ucp, void (*func)(), int argc, ...); // 恢复ucp指向的上下文，同时将当前上下文存储到oucp中 int swapcontext(ucontext_t *oucp, const ucontext_t *ucp); 对于该协程类，有栈 or 无栈？对称 or 非对称？\n对于对称和非对称的话，对称协程更为灵活，非对称协程更为简单易实现。协程中一般存在协程调度器和协程两种角色，对称协程中相当于每个协程都要充当调度器的角色，程序设计复杂，程序的控制流也会复杂难以管理。\n常见的js中的async/await，go中的coroutine都是非对称协程，是因为非对称协程的切换过程是单项的，更适合事件驱动，任务队列等调度模型；但是c语言中的ucontext属于对称协程的经典实现，boost.context为对称协程的现代实现，更适合需要多个协程频繁通信的场景。\n有栈协程和无栈协程有栈和无栈的本质区别在于是否可以在任意嵌套函数中被挂起。一般有栈可以被挂起，无栈则不行。有栈比较适用于功能强大，支持嵌套调用和复杂控制流，灵活的操作上下文的需求，比如boost.COntext；无栈由于存储在内存中，适用于内存占用少，实现简单的场景，比如JavaScript async/await 和 Promise，Erlang 和 Go的Goroutine。\n共享栈 or 独立栈？\n这里我们的协程类，采用的是非对称模型，有栈协程。因此可以推导出所需要的私有成员：\n1 2 3 4 5 6 7 8 9 10 11 private: uint64_t m_id = 0; State m_state = READY; ucontext_t m_ctx; uint32_t m_stacksze = 0; // 栈大小 void *m_stack = nullptr; // 栈空间 std::function\u0026lt;void()\u0026gt; m_cb; // 运行函数 bool m_runInScheduler; 由于该类过程大致为：\n1 2 3 4 | 主协程 | 协程调度器 | 协程A | 协程调度器 | 主协程 | 协程调度器 | 协程B | 协程调度器 在这里存在两种协程调度模式：\n参与调度器，有调度器统一管理协程的切换 不参与调度器，直接与主线程切换上下文 1 2 3 4 5 6 // 当前正在运行的协程 static thread_local Fiber *t_fiber = nullptr; // 主协程，管理声明周期 static thread_local std::shared_ptr\u0026lt;Fiber\u0026gt; t_thread_fiber = nullptr; // 调度协程，管理指针访问 static thread_local Fiber *t_scheduler_fiber = nullptr; 整个流程：\n主协程创建：线程启动时创建主协程，保存主线程上下文。\n用户协程创建：通过分配栈空间和初始化上下文创建用户协程。\n协程运行：\nresume 将协程切换到 RUNNING，执行任务。\n任务完成后，协程状态变为 TERM，调用 yield 切换回调用方上下文。\n协程管理：支持通过调度器统一管理协程或直接与主线程切换。\n这里目前未能体现主协程和调度线程的差别，具体需要等待下一个部分\n协程类，写一段话 debug test 调度池和线程池比较 协程调度 一个线程只有一个协程，一个协程类中会包含三个协程，分别是主协程（main），调度协程和任务协程。其中任务协程是由协程类自主创建，主协程和调度协程都是静态变量，在多种类中其实只存在一个实体。\n协程调度致力于封装一些操作，因为调度协程本身需要创建协程，协程任务的执行顺序，如何利用多线程或者调度协程池保证效率，在协程任务结束之后也需要停止调度器释放资源。如果建立一个scheduler类封装这些操作，那么为用户开放的仅仅只有启动线程池，关闭线程池，添加任务三种操作了。\n引用：（来源：代码随想录）\n调度器内部维护一个任务队列和一个调度线程池。开始调度后，线程池从任务队列里按顺序取任务执行。调度线程可以包含caller线程。当全部任务都执行完了，线程池停止调度，等新的任务进来。添加新任务后，通知线程池有新的任务进来了，线程池重新开始运行调度。停止调度时，各调度线程退出，调度器停止工作。\nmain函数主协程运行，创建调度器 仍然是main函数主协程运行，向调度器添加一些调度任务 开始协程调度，main函数主协程让出执行权，切换到调度协程，调度协程从任务队列里按顺序执行所有的任务 每次执行一个任务，调度协程都要让出执行权，再切到该任务的协程里去执行，任务执行结束后，还要再切回调度协程，继续下一个任务的调度 所有任务都执行完后，调度协程还要让出执行权并切回main函数主协程，以保证程序能顺利结束。 主协程和调度协程的差异\n特性 主协程 调度协程 初始化方式 在线程启动时自动初始化 调度器初始化时创建 标识 t_thread_fiber t_scheduler_fiber 上下文栈空间 使用线程自身栈 使用独立分配的栈 主要功能 恢复线程的原始上下文，终止线程 切换并调度用户协程 运行逻辑 不主动运行任何任务逻辑 包含协程调度的逻辑 让出目标 让出后通常切换到主线程运行 让出后切换回调度器逻辑 用途 管理线程范围内的根协程 管理其他协程，选择运行协程 为什么有主协程，调度协程，任务协程三种设置\n协程的基础知识\n协程IO 定时器 hook 好的，以下是每个函数的含义，以及各个参数的解释：\n1. sleep 和 usleep，nanosleep 1.1 sleep(unsigned int seconds) 含义：使当前进程暂停执行指定的秒数。期间进程不执行任何操作，直到时间到期或被信号中断。 参数 seconds：要暂停的时间，单位为秒。 返回值：返回剩余的睡眠时间（秒）。如果被信号中断，返回的值是剩余的时间。 1.2 usleep(useconds_t usec) 含义：使当前进程暂停执行指定的微秒数。 参数 usec：要暂停的时间，单位为微秒。 返回值：成功时返回0。如果发生错误，返回-1，并设置 errno。 1.3 nanosleep(const struct timespec *req, struct timespec *rem) 含义：使当前进程暂停执行指定的时间，精度为纳秒。 参数 req：指定暂停的时间。类型为 struct timespec，其中 tv_sec 表示秒数，tv_nsec 表示纳秒数。 rem：如果系统调用被信号中断，返回未完成的时间。这个参数是输出参数，存放剩余时间。 返回值：成功时返回0，如果被信号中断，返回-1，并设置 errno。 2. Socket 函数 2.1 socket(int domain, int type, int protocol) 含义：创建一个新的套接字，用于网络通信。 参数 domain：协议族，指定套接字的协议类型。例如：AF_INET（IPv4地址族），AF_INET6（IPv6地址族），AF_UNIX（Unix本地通信）。 type：套接字类型，指定套接字的通信方式。例如：SOCK_STREAM（流式套接字，TCP连接），SOCK_DGRAM（数据报套接字，UDP协议）。 protocol：协议类型，通常设为0，表示选择与套接字类型相关的默认协议。 返回值：返回套接字的文件描述符（sockfd），失败时返回-1，并设置 errno。 2.2 connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen) 含义：连接一个套接字到指定的地址，通常用于客户端发起连接。 参数 sockfd：创建的套接字描述符。 addr：指向 struct sockaddr 的指针，包含目标地址的信息。 addrlen：addr 的长度。 返回值：成功时返回0，失败时返回-1，并设置 errno。 2.3 accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen) 含义：接受一个传入的连接请求，通常由服务器调用。 参数 sockfd：监听套接字的文件描述符，通常是通过 socket 创建并调用 bind 和 listen 后的套接字。 addr：输出参数，返回客户端的地址信息，类型为 struct sockaddr。 addrlen：输入输出参数，表示 addr 的大小。调用时传入 socklen_t 类型的值，返回时会修改为实际填充的字节数。 返回值：成功时返回一个新的套接字文件描述符，表示与客户端的连接；失败时返回-1，并设置 errno。 3. 读操作 3.1 read(int fd, void *buf, size_t count) 含义：从指定文件描述符 fd 中读取数据。 参数 fd：文件描述符，通常是文件、管道、设备或套接字。 buf：指向缓冲区的指针，数据将被读取到该缓冲区。 count：要读取的字节数。 返回值：成功时返回读取的字节数，失败时返回-1，并设置 errno。 3.2 readv(int fd, const struct iovec *iov, int iovcnt) 含义：从指定文件描述符读取多个缓冲区的数据（read 的扩展）。 参数 fd：文件描述符。 iov：指向 struct iovec 数组的指针。struct iovec 结构包含两个字段：iov_base（指向数据缓冲区的指针）和 iov_len（缓冲区的大小）。 iovcnt：iov 数组中的元素数量。 返回值：成功时返回读取的字节数，失败时返回-1，并设置 errno。 3.3 recv(int sockfd, void *buf, size_t len, int flags) 含义：从套接字接收数据，类似于 read，但用于网络套接字。 参数 sockfd：套接字的文件描述符。 buf：数据将被存储的缓冲区。 len：要接收的最大字节数。 flags：接收操作的选项，通常是0，也可以设置为其他值，例如 MSG_PEEK。 返回值：成功时返回接收的字节数，失败时返回-1，并设置 errno。 3.4 recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen) 含义：从套接字接收数据，并获取发送者的地址，通常用于 UDP 套接字。 参数 sockfd：套接字的文件描述符。 buf：数据将被存储的缓冲区。 len：要接收的最大字节数。 flags：接收操作的选项，通常是0。 src_addr：输出参数，接收方的地址信息。 addrlen：输入输出参数，表示 src_addr 的大小，调用时传入，返回时更新为实际地址大小。 返回值：成功时返回接收的字节数，失败时返回-1，并设置 errno。 3.5 recvmsg(int sockfd, struct msghdr *msg, int flags) 含义：接收多个消息或者带有复杂头部的消息，通常用于需要复杂协议的套接字（如 ICMP）。 参数 sockfd：套接字的文件描述符。 msg：指向 msghdr 结构的指针，msghdr 结构包含了消息的各个部分，如消息数据和头部信息。 flags：接收操作的选项。 返回值：成功时返回接收到的字节数，失败时返回-1，并设置 errno。 4. 写操作 4.1 write(int fd, const void *buf, size_t count) 含义：向文件描述符写入数据。 参数 fd：文件描述符。 buf：指向缓冲区的指针，包含要写入的数据。 count：要写入的字节数。 返回值：成功时返回写入的字节数，失败时返回-1，并设置 errno。 4.2 writev(int fd, const struct iovec *iov, int iovcnt) 含义：向文件描述符写入多个缓冲区的数据（write 的扩展）。\n参数\n：\nfd：文件描述符。 iov：指向 struct iovec 数组的指针。 iovcnt：iov 数组中的元素数量。 返回值：成功时返回写入的字节数，失败时返回-1，并设置 errno。\n4.3 send(int sockfd, const void *buf, size_t len, int flags) 含义：通过套接字发送数据，通常用于 TCP 连接。 参数 sockfd：套接字的文件描述符。 buf：要发送的数据。 len：数据的字节数。 flags：发送操作的选项。 返回值：成功时返回发送的字节数，失败时返回-1，并设置 errno。 4.4 sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen) 含义：通过套接字发送数据，通常用于 UDP 协议。 参数 sockfd：套接字的文件描述符。 buf：要发送的数据。 len：数据的字节数。 flags：发送操作的选项。 dest_addr：目标地址信息。 addrlen：目标地址的大小。 返回值：成功时返回发送的字节数，失败时返回-1，并设置 errno。 4.5 sendmsg(int sockfd, const struct msghdr *msg, int flags) 含义：发送带有复杂消息头的数据，通常用于需要复杂协议的套接字（如 ICMP）。 参数 sockfd：套接字的文件描述符。 msg：指向 msghdr 结构的指针。 flags：发送操作的选项。 返回值：成功时返回发送的字节数，失败时返回-1，并设置 errno。 5. 文件描述符操作 5.1 close(int fd) 含义：关闭文件描述符，释放相关资源。 参数 fd：文件描述符。 返回值：成功时返回0，失败时返回-1，并设置 errno。 6. Socket 控制函数 6.1 fcntl(int fd, int cmd, ... /* arg */) 含义：操作文件描述符的各种属性，如设置非阻塞模式等。 参数 fd：文件描述符。 cmd：命令，指定执行的操作（例如 F_GETFL 获取文件状态标志，F_SETFL 设置文件状态标志）。 arg：命令的附加参数，根据命令不同，可能是一个整数或指针。 返回值：操作的结果，具体取决于命令和操作。 6.2 ioctl(int fd, unsigned long request, ...) 含义：控制设备或套接字的行为。 参数 fd：文件描述符。 request：控制命令，指定要执行的操作。 ...：命令的附加参数。 返回值：成功时返回0，失败时返回-1，并设置 errno。 6.3 getsockopt(int sockfd, int level, int optname, void *optval, socklen_t *optlen) 含义：获取套接字的选项值。 参数 sockfd：套接字的文件描述符。 level：协议层级，指定选项的协议层级（例如 SOL_SOCKET）。 optname：选项名称，指定要获取的选项。 optval：输出参数，接收选项的值。 optlen：输入输出参数，表示 optval 的大小，调用时传入，返回时更新为实际大小。 返回值：成功时返回0，失败时返回-1，并设置 errno。 参考 代码随想录协程库项目精讲 ","date":"2024-12-18T00:00:00Z","permalink":"https://sutdown.github.io/p/%E5%8D%8F%E7%A8%8B%E5%BA%93%E9%A1%B9%E7%9B%AE%E5%AE%9E%E7%8E%B02/","title":"协程库项目实现2"},{"content":"协程库项目实现1 github项目链接：GitHub - Sutdown/coroutinelib: coroutine lib\n前言（碎碎念一下） 之前学着写了下lsm tree，学到的东西远比想象的要多，从九月份开始到现在，大概两三个月时间，和六级备考，一些课程期末作业并行，日均大概在10+h。两个月前写了个想法自嘲是虚假的科班选手，也没做，起码大学的前两年实在没走在学习cpp的正确道路上，不过感谢学校了，以前似懂非懂用的工具写的代码如今都慢慢理解了曾经是在做什么。这几个月从c++新特性，到linux内核，到看各种源码，到自己上手从0开始写着项目，深刻感觉此时彷佛才真正是开始入门。\n“道阻且长，行则降至。”\n11月马上也要结束了，害，时间过的真快啊，之前还计划着写这个之前看看libco，这个想法往后推迟吧。不过写协程库之前还是得大致对协程有个简单了解的，这篇文章着重于协程介绍。\n正文篇1 个人理解 进程，线程，协程之间的关系，出现的原因，历史渊源等\n进程是计算机中最先出现的概念，也就是当程序执行一个可执行文件时，cpu从前往后执行这个程序的文件，这个过程也就被成为进程。\n最刚开始我们默认单核cpu只能执行一个进程，但当进程被中断时，cpu会阻塞进程等待中断返回，中断一般由内核中的中断处理程序响应，一般中断会尽可能的短且块，这样尽量减少正常进程运行调度的时间。关于中断，中断其实是分为软中断和硬中断的。\n硬中断是由CPU给物理引脚施加电压变化实现的。一般位于linux内核实现的上半部分，快速处理一些简单的部分，之后发出软中断。 软中断是通过给内存中的一个变量赋予二进制值以标记有软中断的发生。软中断主要由ksoftirqd(kernel software interrupt queue daemon内核软中断队列守护)线程处理会用ringbuffer收包然后交给各个协议层处理，对于TCP则是socket队列。 软中断一般是由中断处理程序和软中断线程的情况下发生，很类似于单一生产者消费者模拟，和环形缓冲区的经典应用场景相匹配。软中断的过程为，硬件中断触发时，将接收的数据包放入环形缓冲区，协议栈读取环形缓冲区进行进一步处理。其优点在于连续内存的使用使得能够快速读取，固定容量无需动态分配内存，可以使用无锁实现高效并发（生产者和消费者线程的指针独立，不存在竞争）。 回到进程，为了高效利用中断时的时间，因此产生了并发。并发时，一个进程切换到另一个线程运行，这个成为进程的上下文切换，进程的上下文切换其实不仅包含了虚拟内存，栈，全局变量等用户空间的资源，还包含了内核堆栈，寄存器等内核空间的资源。\n由于进程之间无论是切换，还是数据其实都是单独的，如果想要并发运行或者共享数据都是极为复杂的，因此提出了线程的概念，线程之间可以并行运行，也可以共享相同的地址空间，同时将线程设置为进程当中的一条执行流程，也就是进程的下一个级别。\n因此我们常说，进程是资源分配的单位，线程是cpu调度的单位\n对于进程和线程，其实还有很多可以讨论的问题，比如进程间的通信方式；多线程需要共享数据，那么如何避免冲突；进程最多可以创建多少个线程；线程崩溃进程也会崩溃吗；死锁，悲观锁，乐观锁，共享锁，排他锁，这么多锁的说法到底是怎么一回事。这些如果讲起来就有点偏离本篇协程的主题了，因此估计会再写一篇文章吧，留个悬念。\n协程的本质是什么，为了解决什么事情\n下面2中有句话说的很合适，协程 的发明是为了解决线程的 Concurrency（并发），线程 的发明解决进程的 Parallelism（并行）。\n协程其实有点像线程下的级别，线程在解决某个任务时，仍然会有等待的时间，这段等待可能是线程加载时的IO操作，基本不消耗cpu资源，如果这段时间用于线程切换的话，可以，但是存在一定的开销。\n协程的创建并非操作系统层面，不涉及内核调度，一般直接用编程语言实现，属于用户态。这个过程类似于线程内实现的并发，协程作为一种特殊的subroutine，可以在执行一半时暂停，这样在遇到IO之类不消耗cpu的操作时，可以将其挂起，继续计算其它任务，充分利用cpu资源。\nsylar的协程实现使用了非对称模型，且保证子协程不能再创建新的协程，即协程不能嵌套调用，子协程只能与线程主协程进行切换，这种模型简单，非常容易理解。\n正文篇2 摘录 （来源见参考链接）\n协程 的发明主要是为了解决 Concurrency（并发） 问题，\n线程 的发明主要解决的 Parallelism（并行） 问题。\n协程简单介绍 “其实不应该把协程和多线程做类比，协程更多的是取代异步状态机的数据结构，如果明确这点，就能够清晰使用场景了。” —— from libco 的实现者\n[什么是协程](https:// zhuanlan.zhihu.com/p/172471249)\n协程是一类程序组件，它是对子过程概念的泛化，并且是属于非抢占的多任务处理。\n它的两个关键概念：\n泛化的子过程 非抢占的多任务处理 每个协程在创建时都会指定一个入口函数，这点可以类比线程。协程的本质就是函数和函数运行状态的组合 。协程和函数的不同之处是，函数一旦被调用，只能从头开始执行，直到函数执行结束退出，而协程则可以执行到一半就退出（称为yield），但此时协程并未真正结束，只是暂时让出CPU执行权，在后面适当的时机协程可以重新恢复运行（称为resume），在这段时间里其他的协程可以获得CPU并运行，所以协程被描述称为“轻量级线程”。\n协程能够半路yield、再重新resume的关键是协程存储了函数在yield时间点的执行状态，这个状态称为协程上下文。协程上下文包含了函数在当前执行状态下的全部CPU寄存器的值，这些寄存器值记录了函数栈帧、代码的执行位置等信息，如果将这些寄存器的值重新设置给CPU，就相当于重新恢复了函数的运行。\n单线程环境下，协程的yield和resume一定是同步进行的，一个协程的yield，必然对应另一个协程的resume，因为线程不可能没有执行主体。并且，协程的yield和resume是完全由应用程序来控制的。与线程不同，线程创建之后，线程的运行和调度也是由操作系统自动完成的，但协程创建后，协程的运行和调度都要由应用程序来完成，就和调用函数一样，所以协程也被称为“用户态线程”。\n协程理论 协程就是函数 协程是函数的一种泛化，允许暂停函数并稍后恢复。\n对称协程和非对称协程 对称协程，协程可以不受限制地将控制权交给任何其他协程。 非对称协程，是指协程之间存在类似堆栈的调用方-被调用方关系。 对称协程更灵活，非对称协程实现更简单。\n有栈协程和无栈协程 有栈协程：用独立的执行栈来保存协程的上下文信息。有栈协程的核心是io的异步化，要hook常用io. 无栈协程：它不需要独立的执行栈来保存协程的上下文信息，协程的上下文都放到公共内存中。 有栈协程 独立栈：独立栈，也就是每个协程的栈空间都是独立的，固定大小 共享栈：共享质就是所有的协程在运行的时候都使用同一个栈空间，每次协程切换时要把自身用的共享栈空间拷⻉。 独立栈和共享栈\n参考资料 代码随想录 - coroutine-lib - github libco - github 出于什么样的原因，诞生了「协程」这一概念 协程理论 什么是协程 协程的好处 ","date":"2024-12-10T00:00:00Z","permalink":"https://sutdown.github.io/p/%E5%8D%8F%E7%A8%8B%E5%BA%93%E9%A1%B9%E7%9B%AE%E5%AE%9E%E7%8E%B01/","title":"协程库项目实现1"},{"content":"共享锁和独占锁的互斥 terminate called after throwing an instance of \u0026lsquo;std::system_error\u0026rsquo; what(): Resource deadlock avoided Aborted (core dumped)（死锁导致中止）\n解决：\ngdb定位 idle中发生死锁\n1 2 3 4 IOManager::idle(),run in thread: 27027 terminate called after throwing an instance of \u0026#39;std::system_error\u0026#39; what(): Resource deadlock avoided [1] + Done \u0026#34;/usr/bin/gdb\u0026#34; --interpreter=mi --tty=${DbgTerm} 0\u0026lt;\u0026#34;/tmp/Microsoft-MIEngine-In-5lhsknfi.0tc\u0026#34; 1\u0026gt;\u0026#34;/tmp/Microsoft-MIEngine-Out-hcmtf4bw.gm1\u0026#34; 然而idle唯一的锁只有，经排查，应该跟idle无关，可能是其它部分\n1 std::lock_guard\u0026lt;std::mutex\u0026gt; lock(fd_ctx-\u0026gt;mutex); 单步调试发现是在构造函数时便出现了core dump,然而不是\n查看gdb的堆栈信息之后，迅速确认问题在于1.死锁可能在多个线程共享锁上冲突 2.异常发生在IOManager::addEvent方法中\n可能的死锁模式是：线程 A 持有共享锁，线程 B 持有共享锁，且线程 A 试图请求一个被线程 B 持有的锁，或者线程 B 试图请求线程 A 持有的锁。\n最终原因：\n本来为访问fdcontext数组的访问设置了一个共享（读）锁，由于fd不在数组中，之后又设置了一个（独占）写锁去为fdcontext数组扩容，由于共享锁和独占锁是互斥的，会出现core dump。\ngit:GnuTLS recv error (-110) 这个指的是在于Github建立TLS连接时发生的问题。原因是TLS连接关闭不正常，应该是我用代理vpn在网速比较满的时候git push太慢我就直接ctrl+z将它中止然后出了问题。\n其实也之前用https连接断断续续出现过其它错误，\n比如连接不上端口，但是此时配置代理就没问题了\n1 2 3 4 5 Failed to connect to github.com port 443 配置代理 git config --global http.proxy 127.0.0.1:7890 git config --global https.proxy 127.0.0.1:7890 但是当代理的网速太慢时，会发生超时\n1 2 3 4 5 6 fatal: unable to access \u0026#39;https://github.com/name/xxx.git/\u0026#39;: Failed to connect to 127.0.0.1 port 7890 after 0 ms: Couldn\u0026#39;t connect to server git config --global -l 查看代理 git config --global --unset http.proxy 清除代理 git config --global --unset https.proxy 可我清除代理之后会出现\n1 GnuTLS recv error (-110): The TLS connection was non-properly terminated. 第一个回答：给了个比较粗暴的方法，直接禁用TLS验证git config --global http.sslVerify \u0026quot;false\u0026quot;亲测，对我没用。\n第二个回答：尝试更新Git和GnuTLS版本，可能版本太旧了，下面的命令，亲测，再次没有效果。\n1 2 3 sudo apt-get update sudo apt-get install git sudo apt-get install --only-upgrade gnutls-bin 最后一次，gpt万岁，它推荐我用SSH协议代替HTTPS进行git操作，成功，具体流程如下：（我是ubuntu 24.04)\n查看是否有SSH密钥\n1 ls -al ~/.ssh 生成新的密钥，会让你输入一些路径之类的信息，最好直接enter默认即可\n1 ssh-keygen -t ed25519 -C \u0026#34;your_email@example.com\u0026#34; 将SSH添加到SSH代理\n1 2 eval \u0026#34;$(ssh-agent -s)\u0026#34; ssh-add ~/.ssh/id_ed25519 查看公钥，添加到github账户中，Github SSH 密钥设置页面\n1 cat ~/.ssh/id_ed25519.pub 测试SSH连接\n1 2 3 ssh -T git@github.com Hi your-username! You\u0026#39;ve successfully authenticated, but GitHub does not provide shell access. 更新Git远程连接的URL\n1 2 git remote -v 检查git的远程URL git remote set-url origin git@github.com:your-username/repository-name.git 更新 之后便好了。\n推荐链接：\nVScode中git push代码报错“Missing or invalid credentials.”问题解决记录 - 知乎\nGit报错： Failed to connect to github.com port 443 解决方案-CSDN博客\nGit Clone错误解决：GnuTLS recv error (-110): The TLS connection was non-properly terminated.-CSDN博客\n","date":"2024-12-07T00:00:00Z","permalink":"https://sutdown.github.io/p/debug-%E6%AD%BB%E9%94%81%E5%92%8Ctls%E8%BF%9E%E6%8E%A5/","title":"Debug-死锁和TLS连接"},{"content":"好久没更了，来更一条吧。\n这是写完后补的。我写这段的时候我的心情好差啊，类日记吧，不知道未来我的心态会不会平稳些，其实当初高三也是，压抑的高三我写完了两个很厚的本子，那里面包括我的每日计划，我的自我鼓励自我批评，也包括诸如这篇文章意义上的情绪倾诉。很明显，我高三养成的习惯页贯彻了我整个大学生活，看来我还是什么都没变啊。\n还是絮絮叨叨着写吧，我要是抱着想写得有逻辑想写的深刻的想法，反而什么都写不出来了。\n我在不同的平台记录了很多东西，比如在微北洋（校园论坛）更学习日志，在微信和公众号更每段时间的心情，在知乎更新学习笔记，我这似乎是一种拼命的想要证明什么，证明我没有荒废，证明我做了我想做的。\n是的，达到目的了，那又怎样。达到目的在于我深刻的认知到，我是清楚我某段时间该怎么做以及我的品行允许我达到什么样的成就，我所经历的每个过程在我的文字种均有体现，重来一百次，我都很难做的更好了，我就是在一条确切得道路上走，我知道我要做什么，我也一定会做。\n但也同样或许是认为未来的一切太过于确定，以至于我没有动力。甚至于我认为我没有动力，也是命，我没有动力，但是我会给自己压力，去逼迫我做些什么，这又何尝不是一种宿命。\n催促自己去做和真正去做是有差距的。其实我很难受，在每次面对一堆看不懂得代码，配不清楚的环境，完全听不懂的课程，面对这些，我依旧是存在压力的，我尽量克制着这些压力保持清明去抵抗。但压力是无穷尽伴随整个人生的。\n想到解决了当下还有未来，无数的困境会包裹我直到我麻木，直到我成为他人眼中成熟的大人。\n满打满算，我现在19出头不到两个月，人生才过去不到四分之一，我究竟要以什么样的心态去面临未来的所有难关，我感觉我要克服不了困难了。\n上述纯属情绪化且消极的态度，其实我更清楚，解决一个问题需要带有完全的理性思考，抛弃那些反复的犹豫，才能真正从当下走出来。但我会随心走，我会尽量选择那些让我更轻松的生活方式，反正困难是个无底洞，何必呢，想开点。\n其实写到这里，我依旧没想清楚，自嘲乘n，看来我还是一个容易内耗的人。\n回望我过去的文章也是，我过去的文字其实也是不太高兴的，我一直再写些什么，笔下的文字是倾倒那些不足为人说的那些可憎情绪。我是很厌恶，将个人糟糕的情绪带给身边人，每当压力大的时候，我其实更想逃离，尤其是逃离在意我的人，我不太想我无常的情绪接触到其他人，但其实我的逃离本身就是一种伤害，我是存在内疚的。\n我自认我做好了100%的我，我接受我自己的缺点，我也承认我的优点。但这句话未免有点不对，我有时候也希望我是完美的，希望我能面面俱到，可我不能。我始终在放弃一些事情，选择一些事情。\n读大学两年，我的悲观情绪没什么变化，倒是学会了放过自己，接受一切的得失，但其实还是没完全放过吧，因为生活摆在眼前。\n睡觉吧，睡一觉起来，明天再解决我当前所面临到问题，我是个普通人，喜怒哀乐，梦想与懒惰，志气与颓丧我都有，既如今存在于此，那也必然有存在的意义。好了睡觉吧。\n明天会是新的一天。\n","date":"2024-12-07T00:00:00Z","permalink":"https://sutdown.github.io/p/%E4%B8%80%E7%AF%87%E5%BF%83%E6%83%85%E4%B8%8D%E5%A4%AA%E5%A5%BD%E7%9A%84%E7%A2%8E%E7%A2%8E%E5%BF%B5/","title":"一篇心情不太好的碎碎念"},{"content":"协程详解 前言 最近在学协程库，因为计划是12月底之前，时间有限想着直接从代码起步，果然这样子还是不好的，这几天很多东西处于一种似懂非懂的状态，很难受。所以决定写篇文章重新捋一下，之前也写过一点点，但是太浅了。\n主要参考有没有C++大佬把C++20的协程讲解下？ - 知乎这个问题中南山烟雨珠江潮 - 知乎的回答，以及代码随想录的协程库源码。\n正文 进程线程协程 进程，线程，协程之间的关系，出现的原因，历史渊源等\n理解进程和线程从基本概念着手，\n进程可以简单认为是由程序代码，相关数据还有进程控制块组成的。操作系统基本职责是控制进程的执行，这包括交替执行的方式以及为进程分配资源。\n倘若我们要执行一个任务，如果全由进程从头至尾执行那必然效率一般，会思考如果将任务分成不同部分，交由不同的进程并行执行能不能提高效率？进程间的地址空间一般来说都是独立不能互相访问的，如果想要通信必须经过内核，那很明显执行一个任务多次进入内核是得不偿失的。因此出现了线程，线程是由线程id，程序计数器，寄存器集合和栈组成，一个进程可以有多个线程，线程之间是共享地址空间的，同时多核cpu能够让多线程并行执行，成功达到了提高性能的作用。所以也可以理解成，线程解决了进程的并行问题。\n对于进程和线程，其实还有很多可以讨论的问题，比如进程间的通信方式；多线程需要共享数据，那么如何避免冲突；进程最多可以创建多少个线程；线程崩溃进程也会崩溃吗；死锁，悲观锁，乐观锁，共享锁，排他锁，这么多锁的说法到底是怎么一回事。这些如果讲起来就有点偏离本篇协程的主题了，因此估计会再写一篇文章吧，留个悬念。\n协程的使用 https://www.cnblogs.com/blizzard8204/p/17563217.html\n语言：C++20 C++20的协程是一个无栈，非对称的协程。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 #include \u0026lt;iostream\u0026gt; #include \u0026lt;coroutine\u0026gt; #include \u0026lt;optional\u0026gt; /* * promise_type 定义协程的行为，管理协程生命周期中的各种状态。 * Generator 提供协程控制器和迭代器接口，方便协程的使用。 * sequence 是一个协程函数，用来生成一个值序列。 */ // 协程的返回类型 struct Generator { struct promise_type { std::optional\u0026lt;int\u0026gt; current_value; /* C++编译器通过函数返回值识别协程函数。 * 返回类型result中有一个子类型承诺对象（promise）， * 通过std::coroutine_handle\u0026lt;promise_type\u0026gt;::from_promise() * 可以得到协程句柄（coroutine handle）。 */ // 生成协程函数的返回对象 Generator get_return_object(){ return Generator{std::coroutine_handle\u0026lt;promise_type\u0026gt;::from_promise(*this)}; } std::suspend_always initial_suspend() { return {}; } std::suspend_always final_suspend() noexcept { return {}; } std::suspend_always yield_value(int value){ current_value = value; return {}; } void return_void() {} void unhandled_exception(){ std::exit(1); // 未处理异常时退出程序 } }; // 协程句柄 std::coroutine_handle\u0026lt;promise_type\u0026gt; handle; explicit Generator(std::coroutine_handle\u0026lt;promise_type\u0026gt; h) : handle(h) {} ~Generator(){ if (handle) handle.destroy(); } struct Iterator{ std::coroutine_handle\u0026lt;promise_type\u0026gt; handle; Iterator \u0026amp;operator++(){ handle.resume(); return *this; } int operator*() const{ return *handle.promise().current_value; } bool operator==(std::default_sentinel_t) const{ return handle.done(); } }; Iterator begin(){ handle.resume(); return Iterator{handle}; } std::default_sentinel_t end(){ return {}; } }; // 协程函数：生成一个从 1 到 n 的序列 Generator sequence(int n){ for (int i = 1; i \u0026lt;= n; ++i) { co_yield i; // 协程暂停并返回值 } } int main(){ for (int value : sequence(5)){ std::cout \u0026lt;\u0026lt; value \u0026lt;\u0026lt; \u0026#34; \u0026#34;; } return 0; } 协程学习 https://lewissbaker.github.io/2017/09/25/coroutine-theory\n协程是函数的泛化。\n对于函数而言，存在两个操作：call和return。\n发生call时，call创建一个激活框架，暂停调用函数的执行，同时将执行转移到调用函数的开始处；return时将返回值传递给调用者，恢复原本调用者的状态，销毁激活框架。这里的激活框架可以视为保存函数特定调用的当前状态的内存块，同时这套激活框架也被成为‘堆栈’。\n协程中有三个新的语言关键字：co_await、co_yield和co_return\n命名空间中的几种新类型std::experimental\u0026quot;\u0026quot;\ncoroutine_handle\u0026lt;P\u0026gt; coroutine_traits\u0026lt;Ts...\u0026gt; suspend_always suspend_never 库编写者可以使用该机制与协程交互并定制其行为。\n一种语言工具，使编写异步代码变得更加容易！\n当前的协程并没有定义协程的语义。它没有定义如何生成返回给调用者的值。它没有定义如何处理传递给语句的返回值co_return，或者如何处理从协程传播出去的异常。它没有定义应该在哪个线程上恢复协程。相反，它指定了一种通用机制，让库代码通过实现符合特定接口的类型来定制协程的行为。\n比如可以定义一个异步生成单个值的协程，或者一个延迟生成一系列值的协程，或者一个通过遇到值optional\u0026lt;T\u0026gt;时提前退出简化使用值的协程。\n协程TS定义了两种接口：Promise接口和Awaitable接口\nPromise接口指定了自定义协程本身行为的方法。库编写者能够自定义调用协程时发生的情况、协程返回时发生的情况（无论是通过正常方式还是通过未处理的异常），以及自定义协程中任何co_await或表达式的行为。co_yield Awaitable接口指定了控制表达式语义的方法co_await 。当传入一个值时co_await，代码将被转换为对 awaitable 对象上的一系列方法的调用，这些方法允许它指定：是否暂停当前协程、在暂停后执行某些逻辑以安排协程稍后恢复、以及在协程恢复后执行某些逻辑以产生表达式的结果co_await。 接口清单：\nAwaitable awaiter type需要实现如下名字的函数: await_ready await_suspend await_resume awaitable type需要实现如下的操作符重载: operator co_await() Promise promise type需要实现如下名字的函数： get_return_object initial_suspend final_suspend unhandled_exception return_void promise type可选实现如下名字的函数： return_value operater new operater delete get_return_object_on_allocation_failure yield_value（co_yield） await_transform https://yearn.xyz/posts/techs/%E5%8D%8F%E7%A8%8B/\nhttps://itnext.io/c-20-coroutines-complete-guide-7c3fc08db89d\n","date":"2024-12-02T00:00:00Z","permalink":"https://sutdown.github.io/p/%E5%8D%8F%E7%A8%8B%E8%AF%A6%E8%A7%A3/","title":"协程详解"},{"content":"惊讶的发现我之前看leveldb的时候没怎么仔细看这一部分），估计是看到后面懒了，现在补补。\n先讲讲sstable在·leveldb·中的位置\n(sstable全名Sorted String Table)\n在leveldb中，当将memory db的数据持久化文件中时，leveldb会以一定的规则进行文件组织，文件格式变为sstable。\n查询时，一般会通过footer先在meta block中利用bloom filter查询，如果不存在则直接返回，减少了磁盘IO。\n正式读取数据块中的数据时，通过``footer加载相应的过滤快和数据块的数据到达内存，为内存分配一个cache_id，读取数据的同时会拼接cache_id和block的偏移量作为blockcache的key`。\n正文开始：sstable的格式 )\n其中分为五个部分，\nDataBlock中存储数据，也就是键值对，那么为什么会分成n个，里面的schema是什么样子的。\nMetaBlock其中存储布隆过滤器的数据，如何存储的？\nXXXIndex中存放相关的索引，便于寻找；Footer中存放索引在整个stable中的偏移量。\n知道大致结构后，接下来看看具体的每个模块\nDataBlock 对于这个块，如果想要增加一个键值对，要求key必须比之前的所有键都大，同时需要在完成整个块之前构建。\n我们能够在private中观察到一个存放上一个键的字符串，这是因为添加键值对时，首先需要计算当前键和上一个键的公共长度进行前缀压缩；然后将公共长度，剩余长度，键的内容添加到数据块中；如果达到重启间隔，记录重启点到restart_offset中同时清空计数器。\nBlockBuilder 的缓冲区 buffer_ 和 DataBlock 直接对应。它负责在内存中构建 DataBlock 的完整内容并序列化，最后在调用 Finish() 时，将完整的二进制块返回，供 SSTable 文件写入或查询使用。\n对于使用缓冲区的原因在于，如果频繁的对sstable写入磁盘的IO操作，是极为低效的，因此也解释了为什么finish之后不能再添加键值对。其次，重启点的偏移量只有再完成一个块的组织之后才能确立，因此也只能计算完所有偏移量之后统一添加到缓冲区末尾。\n关键点：\n前缀压缩\nBlockBuilder 在添加键值对时，使用 前缀压缩 技术将键值对序列化到 buffer_ 中，每个记录包含：\n公共前缀长度（shared key length）。\n剩余键长度（non-shared key length）。\n剩余键内容（key delta）。\n值长度（value length）。\n值内容（value data）。\n重启点机制\n重启点是为了支持快速查找而设计的，通常每隔固定数量的键值对（如 16 个）插入一个重启点。\n缓冲区设计，在整个构建完全之后，才会将buffer的内容写入sstable\n1 2 3 4 5 6 7 +-----------------------------+ | 序列化的键值对数据 | \u0026lt;-- 对应 `buffer_` 的前半部分 +-----------------------------+ | 重启点偏移量数组 | \u0026lt;-- 对应 `restarts_` +-----------------------------+ | 重启点数量（整数） | \u0026lt;-- 对应 `restarts_.size()` +-----------------------------+ MetaBlock 占用1个block空间\n1 2 3 4 5 6 7 +-----------------------------------+ | 过滤器或属性信息数据（元数据） | \u0026lt;-- 由过滤器模块或属性模块构造 +-----------------------------------+ | 重启点偏移量数组 | \u0026lt;-- 用于快速定位过滤器索引 +-----------------------------------+ | 重启点数量（整数） | \u0026lt;-- 指示重启点偏移量的个数 +-----------------------------------+ DataBlockIndex 占用1个block空间。DataBlockIndex 是用来加速数据块（DataBlock）查找的重要结构，它存储了 键与数据块的映射关系，用于快速定位键所属的 DataBlock。\n1 2 3 4 5 6 7 8 9 +-------------------------------+ | Key_1 | Offset_1 | Size_1 | \u0026lt;-- 指向第 1 个 DataBlock +-------------------------------+ | Key_2 | Offset_2 | Size_2 | \u0026lt;-- 指向第 2 个 DataBlock +-------------------------------+ | ... | +-------------------------------+ | Key_n | Offset_n | Size_n | \u0026lt;-- 指向第 n 个 DataBlock +-------------------------------+ MetaBlcokIndex MetaBlockIndex 是 元数据块索引，用于存储和快速定位 **MetaBlock（元数据块）** 的信息。它的作用类似于 DataBlockIndex，但索引的对象不是普通的键值数据块，而是存储元数据的 MetaBlock。\n1 2 3 4 5 6 7 8 9 +-------------------------------+ | MetaBlock_Name_1 | Offset_1 | Size_1 | \u0026lt;-- 第 1 个 MetaBlock 索引 +-------------------------------+ | MetaBlock_Name_2 | Offset_2 | Size_2 | \u0026lt;-- 第 2 个 MetaBlock 索引 +-------------------------------+ | ... | +-------------------------------+ | MetaBlock_Name_n | Offset_n | Size_n | \u0026lt;-- 第 n 个 MetaBlock 索引 +-------------------------------+ Footer 固定48字节，位于文件底部.Footer 是 **SSTable** 文件的最后部分，包含一些 元信息，用于指示文件中其他重要结构的位置。Footer 是整个 SSTable 文件结构的关键组成部分，因为它提供了访问文件中数据块、索引块和元数据块的指针。\n1 2 3 4 5 6 7 +-------------------+ | DataBlockIndex | \u0026lt;-- 指向 DataBlockIndex 的偏移量 +-------------------+ | MetaBlockIndex | \u0026lt;-- 指向 MetaBlockIndex 的偏移量 +-------------------+ | Footer Size | \u0026lt;-- Footer 自身的大小 +-------------------+ 最后，SSTable 文件解析流程（以 Footer 为入口） 读取 Footer 打开 SSTable 文件，读取最后 48 字节（Footer）。 从中获取 DataBlockIndex 和 MetaBlockIndex 的位置。 读取索引块 根据 Footer 提供的偏移量，读取 DataBlockIndex 和 MetaBlockIndex。 读取数据和元数据块 使用 DataBlockIndex 定位到包含键值对的 DataBlock。 使用 MetaBlockIndex 定位到包含元数据（如 Bloom Filter）的 MetaBlock。 查询与处理 通过索引信息，快速定位需要的数据块，进一步进行查询操作。 参考链接：\n浅析RocksDB的SSTable格式 ","date":"2024-11-30T00:00:00Z","permalink":"https://sutdown.github.io/p/%E8%AF%A6%E8%B0%88leveldb%E4%B8%AD%E7%9A%84sstable/","title":"详谈leveldb中的sstable"},{"content":" 略有点烦躁，看到一篇讲c++多线程的文章，感觉还不错，大致的看看这部分。\n这篇写的有点潦草，之后再补充吧。\n前言 看到多线程其实容易想到多进程，还有几讲出现在标准库的协程，协程这块了解不多，大致是对线程之间更加细化的分配，深入学习的话可以看看网上的文章，或者腾讯的libco源码，这是我的打算，不过目前还没看。\n回到正题，多线程和多进程的多任务处理都是一种并发执行。\n**多进程：**由于进程之间的隔离性比较强，资源的获取所需的难度随着进程数目的增加会更加明显，效率弱于多线程，因此得到的谈论并不多。\n进程间通信也是面试的常考点。\n多线程：线程天然能够共享地址空间，全局变量，指针，引用，这些资源在线程中自然的传递，开销小很多，但是也容易引发线程安全的问题，操作系统中利用锁，条件变量等解决的多，大多利用os的API，\n比如linux的\u0026lt;pthread.h\u0026gt;，windows下的\u0026lt;windows.h\u0026gt;。\n直到C++11中推出一系列头文件支持多线程编程，比如\u0026lt;thread\u0026gt;,\u0026lt;mutex\u0026gt;,\u0026lt;atomic\u0026gt;,\u0026lt;condition_variable\u0026gt;,\u0026lt;future\u0026gt;，解决了跨平台的问题，同时提供管理线程，保护共享数据，线程间同步操作，原子操作等。\n单核下的多线程其实是一种同步的方式，多核的情况下可以实现真正的并行计算。\nC++多线程 \u0026lt;thread\u0026gt; 创建线程 join和detach this_thread：是一个类，有四个功能函数： get_id获取线程id yield放弃线程执行回到就绪状态 sleep_for暂停一秒 sleep_util一分钟后执行 \u0026lt;mutex\u0026gt; mutex\nlock_guard——RALL\n创建lock_guard对象时，它将尝试获取提供给它的互斥锁的所有权。当控制流离开lock_guard对象的作用域时，lock_guard析构并释放互斥量。lock_guard的特点：\n创建即加锁，作用域结束自动析构并解锁，无需手工解锁 不能中途解锁，必须等作用域结束才解锁 不能复制 unique_lock\n简单地讲，unique_lock 是 lock_guard 的升级加强版，它具有 lock_guard 的所有功能，同时又具有其他很多方法，使用起来更加灵活方便，能够应对更复杂的锁定需要。unique_lock的特点：\n创建时可以不锁定（通过指定第二个参数为std::defer_lock），而在需要时再锁定 可以随时加锁解锁 作用域规则同 lock_grard，析构时自动释放锁 不可复制，可移动 条件变量需要该类型的锁作为参数（此时必须使用unique_lock） 所有 lock_guard 能够做到的事情，都可以使用 unique_lock 做到，反之则不然。那么何时使lock_guard呢？很简单，需要使用锁的时候，首先考虑使用 lock_guard，因为lock_guard是最简单的锁。\n\u0026lt;atomic\u0026gt; 特点：1.可以实现内存占用极小的锁。 2.当临界区操作可以等价于一个原子操作时，性能会更高。\n链式数据结构的场景非常适合使用 atomic 变量。\n1.内存占用少：即使每个节点都实现一个自旋锁（SpinLock），也不会浪费太多内存。\n2.链式数据结构的临界区通常可以优化成一个指针的 CAS 操作。\natomic atomic_flag \u0026lt;condition_variable\u0026gt; condition_variable—condition_variable必须结合unique_lock使用。 condition_variable_any —condition_variable_any可以使用任何的锁。 \u0026lt;future\u0026gt; future shared_future promise packaged_task 综合：线程池 Threadpool\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 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 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 #include \u0026lt;iostream\u0026gt; #include \u0026lt;thread\u0026gt; #include \u0026lt;mutex\u0026gt; #include \u0026lt;chrono\u0026gt; // 处理时间相关的操作 #include \u0026lt;ctime\u0026gt; #include \u0026lt;vector\u0026gt; #include \u0026lt;queue\u0026gt; #include \u0026lt;string\u0026gt; #include \u0026lt;future\u0026gt; // 处理异步操作 #include \u0026lt;functional\u0026gt; #include \u0026lt;utility\u0026gt; // 提供通用工具类和函数，比如右值引用，完美转发等 #include \u0026lt;condition_variable\u0026gt; //用于线程同步，阻塞线程 #include \u0026lt;shared_mutex\u0026gt; using namespace std; template \u0026lt;typename T\u0026gt; struct safe_queue { queue\u0026lt;T\u0026gt; que; shared_mutex _m; // 互斥锁，支持共享锁个独占锁。 // 允许同时读取，但是不能写入 bool empty() { // 共享锁来锁定互斥量_m shared_lock\u0026lt;shared_mutex\u0026gt; lc(_m); return que.empty(); } auto size() { unique_lock\u0026lt;shared_mutex\u0026gt; lc(_m); return que.size(); } void push(T \u0026amp;t) { // 独占锁来锁定互斥量_m，保证只有一个线程能写入 unique_lock\u0026lt;shared_mutex\u0026gt; lc(_m); que.push(t); } bool pop(T \u0026amp;t) { unique_lock\u0026lt;shared_mutex\u0026gt; lc(_m); if (que.empty()) return false; // 右值引用，避免的队列元素的拷贝，利用的是移动复制而不是拷贝构造的思想 t = move(que.front()); que.pop(); return true; } }; // 积木2：线程池 // 通过复用一定数量的线程减少频繁创建和销毁线程的开销。 class ThreadPool { private: class worker { public: ThreadPool *pool; worker(ThreadPool *_pool) : pool{_pool} {} // 定义了每个线程的主要执行逻辑 // 允许线程池中的每个线程不断从任务队列中取出任务并执行，直到队列为空或者线程池关闭 void operator()() { while (!pool-\u0026gt;is_shut_down) { { unique_lock\u0026lt;mutex\u0026gt; lock(pool-\u0026gt;_m); // 条件变量的一个重载方法，使得当前线程进入等待状态 // 直到线程池关闭或者任务队列不为空时 结束等待 pool-\u0026gt;cv.wait(lock, [this]() { return this-\u0026gt;pool-\u0026gt;is_shut_down || !this-\u0026gt;pool-\u0026gt;que.empty(); }); } // 如果能够成功去除一个任务，就执行它。 function\u0026lt;void()\u0026gt; func; bool flag = pool-\u0026gt;que.pop(func); if (flag) func(); } } }; public: bool is_shut_down; safe_queue\u0026lt;std::function\u0026lt;void()\u0026gt;\u0026gt; que; // 队列 vector\u0026lt;std::thread\u0026gt; threads; // 线程 mutex _m; // 互斥锁 condition_variable cv; // 条件变量 // 构造函数 ThreadPool(int n) : threads(n), is_shut_down{false} { for (auto \u0026amp;t : threads) t = thread{worker(this)}; } // 禁止了ThreadPool对象的拷贝构造，移动构造，拷贝赋值和移动赋值操作 // 原因：ThreadPool管理线程，任务队列和同步原语。 // 1.如果允许拷贝或者移动，ThreadPool必须正确处理所有资源，防止资源泄露或者发生竞争条件。 // 2.ThreadPool的实例应该是唯一的，多个实例共享或者争夺资源会引发错误。 ThreadPool(const ThreadPool \u0026amp;) = delete; ThreadPool(ThreadPool \u0026amp;\u0026amp;) = delete; ThreadPool \u0026amp;operator=(const ThreadPool \u0026amp;) = delete; ThreadPool \u0026amp;operator=(ThreadPool \u0026amp;\u0026amp;) = delete; /* * submit函数通过function和packaged_task将任务和它的返回值封装， * 然后提交到线程池执行。调用者可以通过future异步获取任务执行结果。 * 允许用户灵活提交各种类型的任务，避免手动管理线程和任务的复杂性。 */ template \u0026lt;typename F, typename... Args\u0026gt; // future: 表示异步操作的结果 // 在不阻塞主线程的情况下启动异步任务，在未来某个时刻获取该任务结果 // future和packaged_task一起使用 auto submit(F \u0026amp;\u0026amp;f, Args \u0026amp;\u0026amp;...args) -\u0026gt; std::future\u0026lt;decltype(f(args...))\u0026gt; { // 封装任务 function\u0026lt;decltype(f(args...))()\u0026gt; func = [\u0026amp;f, args...]() { return f(args...); }; // 创建任务对象 /*packaged_task作用： * std::packaged_task 将一个可调用对象包装起来，并将其与 std::future 绑定。 * 包装的任务可以在不同的线程中异步执行，而主线程或其他线程可以通过 std::future 获取任务的结果 * 通过 std::packaged_task，可以在任务执行后，使用 std::future 对象来获取任务的返回值或处理异常。 */ auto task_ptr = std::make_shared\u0026lt;std::packaged_task\u0026lt;decltype(f(args...))()\u0026gt;\u0026gt;(func); // 包装任务 std::function\u0026lt;void()\u0026gt; warpper_func = [task_ptr]() { (*task_ptr)(); // 这个是对它指向的packaged_task对象的调用，变成了无参数的函数对象 }; que.push(warpper_func); cv.notify_one(); return task_ptr-\u0026gt;get_future(); } // 析构函数 ~ThreadPool() { // 主要目的是确保所有的工作线程都能接收到一个任务，以便随后被唤醒进行处理 // 保证线程处理完当前任务 auto f = submit([]() {}); f.get(); is_shut_down = true; cv.notify_all(); // 通知，唤醒所有工作线程 // 保证每个线程正常退出，阻塞和安全检查 for (auto \u0026amp;t : threads) { if (t.joinable()) t.join(); } } }; mutex _m; /* 整体流程： * 1.线程池初始化 * 2.提交任务 * 3.任务执行 * 4.互斥锁保护 */ int main() { ThreadPool pool(8); int n = 20; for (int i = 1; i \u0026lt;= n; i++) { pool.submit([](int id) { // 模拟延迟 if (id % 2 == 1) { this_thread::sleep_for(0.2s); } unique_lock\u0026lt;mutex\u0026gt; lc(_m); cout \u0026lt;\u0026lt; \u0026#34;id : \u0026#34; \u0026lt;\u0026lt; id \u0026lt;\u0026lt; endl; }, i); } } 多线程性能分析 lock contension — 用锁处理多线程同步问题 多个线程都尝试获得一个锁时，很容易发生竞争现象，甚至导致死锁，从而影响系统的性能和响应时间。目前可以尝试的优化方法：\n减少临界区大小。 对共享资源进行分桶操作。比如LevelDB中的LRUCache。 Cache Coherency— 用原子操作处理多线程同步问题 当一个共享变量的数据发生变化时，按照缓存一致性，这个变量的更新需要同步到其它线程的缓存中，否则会出现问题：\nCache Ping-pong多个处理器频繁地对同一个缓存行（Cache Line）进行读写操作，导致该缓存行在不同处理器的缓存之间频繁地来回传递。\nFalse Sharing多个处理器访问不同的数据，但这些数据恰好位于同一个缓存行中，导致该缓存行在不同处理器的缓存之间频繁传递。\n参考资料 深入探讨C++多线程性能优化 - 知乎 【NO.610】C++多线程详解（全网最全） C++多线程 - 菜鸟教程 ","date":"2024-11-29T00:00:00Z","permalink":"https://sutdown.github.io/p/c-%E5%A4%9A%E7%BA%BF%E7%A8%8B/","title":"c++多线程"},{"content":" 介绍一下这个项目（分模块）\n本项目主要参考Google开源项目levelDB，实现了一个基于LSM结构的键值存储引擎。\n项目中实现了日志、布隆过滤器、内存分配器、缓存管理、文件读写、SSTable存储、写前日志（WAL），MemTable管理等核心模块，对各模块进行了单元测试。用键值对存储，支持快速写入和高效查询，具备良好的稳定性和性能。\n项目介绍：\n本项目主要基于log structure merge实现一个键值对存储引擎。（具体的过程db.h里面还是写的很清楚的）\n写入时首先会写入日志，再回写到memtable中，再写到缓存（缓存满了怎么办）中。如果memtable（底层是跳表）超过原有的大小，会将其转换成sst，加入磁盘中（其实也就是写入某个路径下的文件里），并且创建一个新的memtable。\n读取时，首先从cache中读取，再从memtable中读取,最后从sst磁盘中读取，但是在真正进入磁盘读取之前会先用布隆过滤器确认磁盘中是否存在。\n根据以上的需求我们实现了内存管理，文件读写，sstable，memtable，跳表，日志等模块。可以进行快速写入和高效查询。\n内存管理（内存分配器，和缓存设计） 设计内存池机制和分片LRU缓存系统，优化内存管理，支持多分片减少锁竞争，并通过回调函数实现缓存项淘汰时的资源清理。\n内存池\n这个类实现了一个基于内存槽的内存分配策略。\n内存槽是一个数组，其中的每个结点块是不同大小的内存块链表，依次从8字节，16，32，…一直到4*1024字节，因此少于4*1024字节的内存不会用内存槽分配，直接malloc或者new分配即可。\n内存池则是一个char*，会为其预分配4*1024*1024字节的数据。\n内存池槽中的每个数组属所分配的每块空间都是在内存池中，内存池满时会重新分配新的内存池，剩余部分如果一个block都无法满足，会将其挂载到其它合适的数组的某个链表中；一般会直接申请十个内存块，以免频繁申请。\n公共接口：allocate，deallocate，reallocate\nallocate：满足大于0小于4kb时，找到相应的内存槽填入内存槽，内存槽指针指向下一个内存块。 deallocate：满足大于0小于4kb时，找到相应的内存槽，将该内存加入其中，内存槽指针指向新加入的位置，因为一般是头插法。 reallocate：先deallocate，再allocate 私有函数：fill_slot，fill_mem_pool\n第二个函数用于分配新的4MB的内存池。 第一个函数用于填内存槽，也就是当需要分配空间但是内存槽中没有相应字节的内存管理时，寻找到对应的sloti，一次性分配FILL_BLOCK_CNT也就是10个块。填内存槽时会出现三种情况： 内存池容量足够分配所有块，直接分配 内存池容量只够分配一部分块，先分配一部分块 内存池容量一块也不够，剩余部分挂在到slot上，重新申请内存池（内存池会修改指针和大小，但是slot作为某种形式的数组/链表，是始终存在的，挂载也可以避免内存碎片；之后重新运行该函数，继续分配。 cache\nCache中持有N（默认为5）个指向CachePolicy的指针，相当于5个分片，可以减少哈希冲突以及减少锁的范围；LRUCache和LFUCache都是CachePolicy的子类。\n为什么使用LRU缓存，LFU呢，其它缓存为什么不行？\nLRU:淘汰最久未被访问的数据项 LFU:淘汰访问频率最低的缓存项 LRU实现简单,并且更加适合具有局部性原理的访问模式.\nleveldb中为什么存在lru链表和in-use链表，两个链表？\n一个存放lru中的结点,另一个存放被lru淘汰的结点,结点被淘汰了但是不一定没有被其它引用,贸然删除可能造成不好的结果,每隔一段时间,遍历in-use链表,如果引用计数变为0,将其删除.\n两个哈希函数的作用；回调函数的作用。\n这两个哈希和leveldb中两个链表是一样的作用.\n回调函数: 在缓存条目被删除或替换时，执行一些清理操作，通常是在缓存项被淘汰、移除或替换后进行特定的资源释放或额外的操作。\nlrucache-leetcode 另：锁的设计\n文件读写和WAL（文件读写，WAL） 实现了高效的文件读写操作，优化了WAL和SSTable文件的持久化过程，同时支持并发读取。\n并发读取：\n从文件中读取指定数量的字节存储到缓冲区中\n​ 由于函数原型中自带偏移量，因此不会更新文件描述符的内部偏移量\n​ 以此把证线程安全，适合多线程环境\nfile_write\n主要用于对WAL Write-Ahead Log文件和Sorted String Table的持久化操作，实现关注于高效的文件写入，缓冲区，内存映射文件等技术，优化文件写入。\n将文件中的数据从用户空间写到文件描述符fd指向的文件， 将fd指向文件的缓冲数据同步到磁盘中， 在缓冲区中追加数据时，需要考虑到缓冲区内存是否可以容纳所有的数据，如果可以就直接写入，不行的话先将缓冲区中的数据刷新到磁盘再写入。\nfile_read\n主要用于打开文件，从缓冲区中读取数据，注意实现多线程环境下的并发读取。\n顾名思义，Write-Ahead Logging先写日志，再更新数据。\nlevelDB中的文件读写和WAL都是在log_writer和log_reader中实现的。leveldb中的结构为\n1 2 3 +--------------+---------+----------+------+ | checksum(4B) | len(2B) | type(1B) | data | +--------------+---------+----------+------+ checksum是校验数据的准确性；len为长度；type为类型，即如果一条数据在一个block中存放不下，这里会例句该block是数据的前面，中间或者后面部分；data为数据。\n这里将WAL和文件读写分开实现。\nWAL和文件读写分开实现有助于遵循单一职责原则和依赖注入原则。\nq1：WAL如何利用filewrite作为私有变量实现的\n利用filewriter作为私有成员，利用了crc循环冗余校验值存储，同时保留长度，作为wal读写。\n日志系统实现（日志） 使用单例模式设计日志系统，采用spdlog库实现高效的多线程日志记录，并确保日志实例的唯一性和高效性。\n实现方案：利用单例模式实现日志。\n单例模式的实现方式； spdlog - C++日志库； lock_guard and unique_ptr; 参考的库是典型的c++日志库spdlog，它使用内存映射文件和异步日志记录技术，能够快速记录；支持多线程，能够保障线程安全（手段为互斥锁）；具有多种日志级别，采取了灵活的日志格式化选项，支持跨平台，多后端。\n对于spdlog::logger加上共享指针shared_ptr，便携化资源释放。\n对于创建一个日志单例采取的是最为经典的静态局部变量的懒汉单例，static既能保证共享性下只存在一个实例，也能保证变量的创建不被打乱，相比于双重锁和智能指针实现的更完善也更简单。\n持久化（Mentable，sstable） 实现了SkipList作为Memtable的数据结构，通过优化算法提高内存中的数据存取效率，支持基本的读写操作。\n设计并实现了SkipList作为Memtable的数据结构，优化内存中的数据存取效率。 提高了跳表（SkipList）在并发读写下的线程安全性，使用锁机制避免数据竞争。 sstable 设计SSTable文件格式，实现了数据块（Data Block）、索引块（Index Block）、元数据块（Meta Block）等的存储，并使用多级索引优化查询性能。\n在leveldb中，当将memory db的数据持久化文件中时，leveldb会以一定的规则进行文件组织，文件格式变为sstable。这个部分也模仿一下leveldb，那先回顾一下leveldb中的结构。见leveldbS源码阅读3 - File System (sstable,cache,option)，详谈leveldb中的sstable\n查询时，一般会通过footer先在meta block中利用bloom filter查询，如果不存在则直接返回，减少了磁盘IO，然后再通过Index block找到之后对于相应的Data block即可。（通过 Footer -\u0026gt; IndexBlock -\u0026gt; DataBlock 的多级索引方式）\n有个问题，sstable中的地址是footer最小吗，如果是的话，那怎么存放datablock的，一个sstable是一整个完整的连续空间吗？\nSSTable 是一个完整的、连续的磁盘文件。Footer 是整个 SSTable 文件的最后一个固定部分，不是存储的最小地址。Footer 的大小是固定的，程序可以通过文件总大小减去 Footer 大小快速找到 Footer 的起始位置。\nData Block\n存储key，value数据对\nDataBlock_1 ~ DataBlock_N：即DataBlock，由DataBLockBuilder操作\n1 2 3 4 5 6 7 8 9 +-----------------------------+ | Record_1 - Record_n | \u0026lt;-- n条记录，每条记录是一个键值对 +-----------------------------+ | Restart Point_1 - ..._k | \u0026lt;-- k个重启点，从该位置开始一组记录 +-----------------------------+ | Restart_Num(4B) | \u0026lt;-- 重启点数量 +-----------------------------+ | Restart_Offset(8B) | \u0026lt;-- 重启点数组的起始偏移量 +-----------------------------+ datablock中主要需要存储数据，为了最有效的存储数据，我们在类的private中设置了pre_key，这是为了比较前缀，只存储相同的数据，在它的记录中，它的存储格式为shared_key_len,unshared_key_len,value_len,unshared_key_conten,value_content，同时为了查找时的效率加快，每相隔16条记录会设置一个重启点，同时利用一个数组记录重启点数组每次的偏移量；\n由于偏移量的大小只能写完16条才能记录，不能实时，同时为了防止多次进行磁盘IO，因此每个数据块都是先存放在缓冲区中，等到最后满时创建数据块，创建之后就不能再添加数据了。\nMeta Block\n存储filter相关信息\nMetaBlock：存放Filter等信息，这里每个SST只设置一个MetaBlock\nfilter_block和datablock的机制差不多，更简单一点，基本调用bloomfilter中的函数，创建过滤器，添加键，判断键是否存在之类的。\nIndex Block\n存储每个data block的索引信息\nIndexBlock_1 ~ IndexBlock_N：存放对应的DataBLock的Offset信息、最大Key信息\n1 2 3 +------------------------+---------------+-----------------+ | _shortest_key_size(4B) | _shortest_key | _offsetInfo(8B) | +------------------------+---------------+-----------------+ Footer\n存储meta index block和index block的索引信息\nFooter：存放MetaBlock、IndexBlock的Offset信息\n1 2 3 +---------------------------+----------------------------+ | MetaBlock_OffsetInfo (8B) | IndexBlock_OffsetInfo (8B) | +---------------------------+----------------------------+ 布隆过滤器 模拟LevelDB中的布隆过滤器，通过优化哈希函数的数量和位数组大小来保证在高假阳性率情况下的准确性。\nleveldb中时通过键的位数确立哈希函数的个数，这里的构造函数中直接通过传入的键的数量和假阳性率确立最佳的哈希函数数量和为数组大小（这些是论文中有结论，能确保最为准确。默认假阳性率为0.01\n创建过滤器时，会通过键和双重哈希增量模拟每次的哈希函数，哈希的方法采用的是murmur_hash，最终存储到相应大位中，同时判断是否存在其中也是对齐进行哈希计算，只要判断出一次不存在那就一定不存在与数组中，但是全部存在也不能证明一定在数组中。\n数据库接口 设计并实现了数据库的接口层，支持数据高效的Put，Delete，Get操作，完善整体逻辑。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 /* * Put逻辑： * 1. 写WAL(fsync同步); * 2. 写memtable; * 3. 写缓存(提高读性能); * 4. 如果memtable超限，应该落盘，并且开启一个新的memtable; * Delete逻辑： * 1. 写WAL; * 2. 写memtable; * 3. 删除缓存; * 4. 如果memtable超限，应该落盘，并且开启一个新的memtable; * Get逻辑： * 1. 读cache，有则直接返回，否则进入2; * 2. 依次从memtable、sst文件向下查找; * 3. 找到的数据写入缓存; * 4. 返回结果; */ 压测 手撕LRUcache\n手撕线程池https://zhuanlan.zhihu.com/p/1173596229\n手撕跳表\n","date":"2024-11-20T00:00:00Z","permalink":"https://sutdown.github.io/p/leveldb%E6%BA%90%E7%A0%81%E9%98%85%E8%AF%BB8-orient%E9%9D%A2%E8%AF%95/","title":"leveldb源码阅读8 Orient面试"},{"content":"https://segmentfault.com/a/1190000009707717# 我喜欢这篇文章，写的很有意思。\n1.讲讲leveldb是什么 LevelDB是Google开源的持久化KV单机数据库，特点在于使用键值对存储，支持快速随机读写，大容量存储操作。它使用LSM树结构，优化了写入性能，同时支持compaction以减少存储空间。但是LSM树的核心思想在于放弃部分读的性能换取最大写的能力。因此为了提升读取的能力，leveldb使用会布隆过滤器减少不必要的查找，同时提供了snapshot的机制用于在数据库读取时保持一致的视图，确保写入操作不影响读取。最后为了保持数据的一致性和可靠性，提供了版本控制和日志机制。\n2.常问知识点 LSMtree，读写，如何写，如何读，如何解决读的速度慢，读放大，写放大 引申：levelDB的存储结构 LSM levelDB的存储包括内存数据库memtable和磁盘数据库SSTable两部分。\n存储时会先将数据写到内存中的Memtable中，当Memtable满时会转变成Immutable Memtable，同时创建新的Memtable写入新的请求。而Immutable table会定期合并到磁盘的SSTable中，levelDB会合并和压缩SSTable，以减少磁盘的使用和提高读取性能。\n写放大 写放大指的是实际写入磁盘的数据量大于应用层请求写入的数据量。每写1byte数据带来n bytes的数据写盘，写放大为n。\n本质需要和全局有序做权衡，对顺序要求越高的系统写放大越严重，比如B+树，但是LSM树把顺序延迟到后台compaction，减少了写放大。\n分层存储。当一个层满了后，会将其数据合并并写入到下一个层，这个过程需要读取和写入大量的数据，导致写放大。 合并操作。每次合并会将多个 SSTable（Sorted String Table）文件合并成一个新文件，合并过程中需要读取多个文件并写入一个新的文件，这样增加了写入的数据量。 删除与更新。LevelDB 不会直接删除或更新数据，而是将其标记为删除（或过期），新数据会被写入到新的位置。这也导致了旧数据和新数据共存的情况，从而增加了写入次数。 读放大 完成每次读请求所需要的额外读盘次数。\n读取 LevelDB的读取过程是：\n在memory db中查找指定的key；\n在冻结的memory db中查找指定的key；\n按低层至高层的顺序在level i层的sstable文件中查找指定的key；\n但其实一般情况下在一二步的cache和第三步Bloom Filter的存在都能提高访问速度，减少不必要的磁盘访问。\nskipList，与红黑树的区别，有没有更好的 红黑树 二叉查找树 —\u0026gt; AVL树/rb树\n由于内存的存储结构中需要选择一个合适的数据结构，leveldb中我们需求的是高效查找，高效插入，高效顺序遍历。\n数据结构首先考虑二叉树，但是普通二叉树在顺序遍历时会出现严重失衡，因此考虑更加平衡的比如AVL，二叉树等，都能够保证插入查找时间复杂度都是lg(n)，相比平衡树，跳表最大的优势在于，保证了读写性能的同时简化了实现。\n结构 跳表是一种可以取代平衡树的数据结构。跳表使用概率均衡而非严格均衡策略，从而相对于平衡树，大大简化和加速了元素的插入和删除。\n跳表时一种带有额外指针的链表，思路为跳步采样，构建索引，逐层递减。\n额外指针：指针数变为正常指针的两倍。\n插入\n插入新节点的指针数通过独立的计算一个概率值决定，使全局节点的指针数满足几何分布即可。 插入时不需要做额外的节点调整，只需要先找到其需要放的位置，然后修改他和前驱的指向即可。 多线程并发访问 向外界提供保证：\nWrite：在修改跳表时，需要在用户代码侧加锁。 Read：在访问跳表（查找、遍历）时，只需保证跳表不被其他线程销毁即可，不必额外加锁。 自己内部代码实现：（还没看懂）\n非阻塞结构： Skip list 采用了随机化算法，可以在多线程环境中支持并发读写，而不需要全局锁。这样可以减少线程间的竞争，提高并发性能。 乐观并发控制： 在进行插入和删除操作时，Skip list 使用乐观策略，假设冲突不频繁。当一个线程在更新时，它不会立即加锁，而是完成操作后检查是否存在冲突。如果存在，操作将被重试。 局部锁定： Skip list 的节点在更新时只锁定涉及的特定节点，而不是整个列表。这种局部锁定的方式减少了锁的持有时间，提高了并发性。 指令重排： 在一些情况下，Skip list 可以利用指令重排来优化性能。在执行某些操作时，可以重排指令顺序以提高执行效率，同时通过适当的内存屏障确保数据一致性。 高效的搜索与插入： Skip list 的层次结构使得搜索和插入操作的平均时间复杂度为 O(log n)，这对于并发访问非常有效，能够快速定位元素，减少操作时间。 log怎么设计的，如何保证数据丢失，双缓冲吗 一条日志记录的内容包含：Header和Data。\n1 2 3 4 5 header： +-----------------+--------------+--------------+--------------+ | CRC32（4字节） | 长度（2字节） | 类型（1字节） | 保留（1字节） | +-----------------+--------------+--------------+--------------+ buf 数组的设计确保了记录的结构在物理存储中是固定的，使得在读取记录时能够快速解析出每个字段的内容。这样做的好处是提高了数据的可解析性和读取效率，同时通过 CRC 校验增强了数据的可靠性。 布隆过滤器的设计和应用，创建options对象时开启布隆过滤器，如何判断布隆过滤器的好坏 Bloom Filter使用场景 布隆过滤器需要在内存中维护一定的资源开销，因此默认是关闭的，需要用户手动开启。\n适合用布隆过滤器的场景：高读取频率，大数据集并且键的分布相对均匀，写少读多 不需要使用bloom filter的场景：高写入频率，小数据集，访问模式简单 Bloom Filter原理 布隆过滤器的核心在于位数组和哈希。通过多个哈希函数将输入映射到位数组中的位置，将该位设置为1，查询时使用同样的哈希函数计算该位索引，如果所有位对应为1，那么可能存在；如果有位为0，那么一定不存在。涉及的参数有：\n哈希函数个数k\n位数组容量m\n插入的数据数量n\n数学结论：k = ln2 * (m/n)时，布隆过滤器的准确率最高，m最少取到n的1.44倍时，错误率比较好。\nBloom Filter在levelDB中的应用 在每个sstable中都会有相应的布隆过滤器，布隆过滤器会保存其中所有的键。\n统一的布隆过滤器会占用大量的内存，并且在是stable写入或者压缩时容易发生变化，独立的布隆过滤器在数据的插入删除更好处理。\nBloom Filter支持删除操作吗 不支持，但是Counting Bloom Filter支持。为位数组中的每位增加了一个计时器。\nversion作用 LevelDB的多版本存储设计可分为三个层次：\n从key/value的角度： 每次变更操作的记录（Batch Writer可视为一次操作）都有不同且递增的SequenceNumber。对于一个UserKey，当存在SequenceNumber更高的的记录时，旧的记录不会被立即删除，至少要在该SequenceNumber之前的所有Snapshot都被释放后才能删除（具体删除时间与Compaction时间有关）。这是LevelDB实现Snapshot Read的基础。 从MemTable的角度： LevelDB中的MemTable通过引用计数来控制释放时间。在需要读取MemTable时（无论是Get操作还是Minor Compaction时），读取前会增大其引用计数，读取后减小引用计数。这样，即使MemTable已被通过Minor Compaction操作写入到Level-0文件，MemTable在被读取，它就不会被释放，保证能被多次读。 从数据库文件的角度： LevelDB的文件同样需要引用计数，当执行Major Compaction时，LevelDB不会立即删除已被合并的数据库文件，因为此时可能还有未完成的读取该文件的操作。 key/value的版本实际上也是依赖于内存与稳定存储，其分别在Compaction与Put/Get操作中体现，因此这里我们主要关注后两者。\nMemTable的多版本与Snapshot信息是不需要直接持久化的，因为数据库关闭时无法进行Snapshot Read，也就没有了Snapshot的概念，而最新的MemTable会通过WAL重建，旧的MemTable也不再会被依赖。\n数据库文件则不同，LevelDB必须记录数据库文件的版本信息，否则在数据库重启时无法快速确定哪些文件是有效的（LevelDB提供了文件版本信息损坏时的修复机制）。而LevelDB中Version及相关概念就是为此设计的。\nVersionSet是一个管理和维护多个版本的集合，\nversion文件主要用于存储数据库的某个具体版本，包括当前版本的所有文件，统计信息以及相关状态。其中维护了一个文件列表记录每个层级的sst文件，不同版本之间的连接方式为双向链表；通过引用计数管理version对象的生命周期等。 VersionEdit是对版本变化的具体描述和记录。通过这种结构，LevelDB能够有效地管理数据的版本控制和变化。\n通过LogAndApply()方法应用VersionEdit对象，创建新的Version并将其添加到VersionSet中 levelDB如何compaction 知乎 - compaction\n为什么需要compaction？ 何时需要做compaction 具体怎么做compaction 如何在compaction的同时保证服务可用 compaction对性能的影响 如何在服务的延迟和单次compaction的收益做trade off compaction分为两种，minor compaction和major compaction。compaction任务的运行期间会带来很大的资源开销，比如压缩，解压缩，数据拷贝，compare消耗大量cpu，读写数据引起diskIO，compaction策略约束了lsm-tree的形状，决定哪些文件需要合并、任务的大小和触发的条件，不同的策略对读写放大、空间放大和临时空间的大小有不同的影响。\nMinor Compaction \u0026gt; Manual Compaction \u0026gt; Size Compaction \u0026gt; Seek Compaction\nminor compaction 主要发生在将Immutable memtable转变成sstable的过程中。\nleveldb对该步骤进行了优化，不仅会将其转储为sst存到leveldb0，还会将其推至更高的层级，最多推到第二层级。\nmajor compaction Size compaction. level0层或者其它层超过预定上限时触发。主要在于均衡各个level的数据从而保证读写的性能均衡。\n起始层一般是score最高的一层（file size/level层的阈值）；确定起始层，然后向上查找，在没有合并的sstable中存在重叠文件，将起始层和该层合并然后放入起始层中。（第0层文件数过多，非0层总大小超过10^iMB)\nSeek compaction. 根据sstable的多次seek miss触发。\nallowed_seeks触发为0时，两层字段会合并。\nManual compaction. LevelDB使用者通过接口void CompactRange(const Slice* begin, const Slice* end)手动触发。\n发生在level i中存在sst文件和参数的begin，end重合时，会compact掉整个level中所有存在重叠的sst文件。\nlevelDB的snapshot snapshot是一种机制，用于在数据库读取时保持一致的视图，确保在读取数据时，写入操作不会影响读取结果，从而提供一种简单的并发控制方式。\nSnapshotImpl表示一个具体的快照实例，包含一个序列号和指向前后快照的指针；序列号的主要作用是提供快照创建时数据库状态的唯一标识；双链表，便于快速插入和删除\n3.levelDB缓存 LRU cache有优化吗\n100w条数据存储大概需要的空间\n4.levelDB重启过程 由于数据库启动前需要恢复数据，也就是利用Manifest信息重新构建一个最新的version。\n利用current文件读取最近使用的Manifest文件。\n创建一个空的version，利用manifest文件中的session record依次apply，还原出一个最新的version，注意manifest的第一条session record记录的是一个version的snapshot，后面记录的都是增量。\n（为了避免manifest过大，每次启动时重新创建的manifest的第一条都是当前版本的快照状态，其它过期的manifest文件会在下次启动的recover流程中自行删除）\n将非current文件指向的其它过期的manifest文件删除。\n将新建的version作为当前数据库的version。\n5.levelDB分层，为什么从上往下读，层与层之间重复 6.levelDB和redis 7.levelDB的优化 写入磁盘时的延迟 并发写加锁造成的竞争 读操作时如何通过索引降低查找延迟 如何更好地利用cache优化查询效率，增加命中 快速地从快照或者日志中恢复 后台工作如何保持服务可用 8.memtable的实现 9. Leveldb在用户视图中的基本单元是什么？ Leveldb一条记录在内存中的形式是什么，记录以怎样的方式被组织？ Leveldb的记录在文件中的存储格式是什么，多条记录在单文件中是如何被管理的，多文件又是如何被管理的？ Leveldb向用户做出了怎样的保证，在什么样的场景下提供了优化？ https://segmentfault.com/a/1190000009707717#\n","date":"2024-11-13T00:00:00Z","permalink":"https://sutdown.github.io/p/leveldb%E6%BA%90%E7%A0%81%E9%98%85%E8%AF%BB7-question/","title":"leveldb源码阅读7 Question"},{"content":" 本文分为两部分：\n现代c++白皮书：在纷繁多变的世界里茁壮成长：C++ 2006–2020论文笔记 书籍：深入理解c++11特性阅读笔记 找资料的过程中发现了现代c++白皮书，相见恨晚，github上还有各路大佬翻译的中文版，论文和《深入理解c++11》刚好可以同步学习，c++11应该属于现代版c++的开端，到了c++20，版本迭代属于更加完善了。\nc++11那本书可以看，如果学习过一些c++项目的话，很多常用好用特性已经反复出现多次了，可能以前没觉察，通过这本书很能让你回忆起以前的诸多代码。\n现代c++白皮书这个论文，它的重点不在于给你讲解知识点，而在于讲述它们出现的流程原因结果改善，以及各种特性的比较。想要完全看懂必然需要一定的水平，我也只是大概看了看，起码做个了解是没有坏处的。\n论文：在纷繁多变的世界里茁壮成长：C++ 2006–2020(摘录+个人思考) 具体的语言技术话题包括内存模型、并发并行、编译期计算、移动语义、异常、lambda 表达式和模块。要设计一种机制来指定模板对其参数的要求，既足够灵活和精确，又不会增加运行期开销，实践证明这很困难。设计“概念”来做到这一点的反复尝试可以追溯到 1980 年代，并触及到 C++ 和泛型编程的许多关键设计问题。\n1. 前言 c++发展的决定性性特征在于\n语言构件到硬件功能的直接映射（直接操作硬件） 零开销抽象（不用的东西不付出代价） c++年表 1979年，工作从“C with class”变成C++ 语言：class，构造析构，public/private，简单继承，函数参数类型检查 库：task（写出和仿真），vector 1985年：c++首次商业发行 语言：virtual函数，运算符重载，引用，常量 库：输入输出流 1989-91：ANSI和ISO标准化； 1998：c++98，第一个ISO C++标准 2011 年：C++11 [Becker 2011]，TC++PL4 [Stroustrup 2013] 语言：内存模型、auto、范围 for、constexpr、lambda 表达式、用户定义字面量…… 库：thread 和锁、future、unique_ptr、shared_ptr、array、时间和时钟、随机数、无序容器（哈希表）…… 2014 年：C++14 [du Toit 2014] 语言：泛型 lambda 表达式、constexpr 函数中的局部变量、数字分隔符…… 库：用户定义字面量…… 2017 年：C++17 [Smith 2017] 语言：结构化绑定、变量模板、模板参数的构造函数推导…… 库：文件系统、scoped_lock、shared_mutex（读写锁）、any、variant、optional、string_view、并行算法…… 2020 年：C++20 [Smith 2020] 语言：concept、module、协程、三路比较、改进对编译期计算的支持…… 库：概念、范围、日期和时区、span、格式、改进的并发和并行支持…… 从 2006 年到 2020 年，C++ 经历了两次主要修订：C++11 和 C++20；而论文的早期读者们也都要求获得更多的信息。结果就是论文的页数几乎翻倍。即使以目前的篇幅，读者也会发现某些重要的主题没有得到充分的展现，如并发和标准库。\n2. 背景：C++的1979-2006 c++98语言特性： 模板，支持泛型编程 异常处理 运行期类型识别，danamic_cast, typeid namespace命名空间 条件语句内的声明 具名类型转换，（static_cast、reinterpret_cast 和 const_cast） bool C++98 中最重要的技术之一是 RAII（Resource Acquisition Is Initialization, 资源获取即初始化）。想法就是每个资源都应该有一个所有者，它由作用域对象表示：构造函数获取资源、析构函数隐式地释放它。经常被用在标准文件流，file，智能指针中等\nC++98 标准库 STL——创造性的、通用的、优雅的、高效的容器、迭代器和算法框架，由 Alexander Stepanov 设计。 特征（trait）——对使用模板编程有用的编译期属性集（§4.5.1）。 string——一种用于保存和操作字符序列的类型。字符类型是一个模板参数，其默认值是 char。 iostream——由 Jerry Schwartz 和标准委员会精心制作，基于我 1984 年的简单的数据流，处理各种各样的字符类型、区域设置和缓冲策略。 bitset——一种用于保存和操作比特位集合的类型。 locale——用来处理不同文化传统的精致框架，主要与输入输出有关。 valarray——一个数值数组，带有可优化的向量运算，但遗憾的是，未见大量使用。 auto_ptr——早期的代表独占所有权的指针；在 C++11 中，它被 shared_ptr（共享所有权）和 unique_ptr（独占所有权）（§4.2.4）替代。 3. C++标准委员会 讲述了一些关于c++制定标准时的矛盾冲突，或者c++需要更好的特性适应发展，参与c++修订人员变多的管理等等，和语法没什么很大关系，简单浏览下。\n4. C++11感觉像是门新语言 §4.1：支持并发 内存模型 内存屏障和原子操作\n（无锁编程）\n线程和锁 thread——系统的执行线程，支持 join() 和 detach() mutex——系统的互斥锁，支持 lock()、unlock() 和保证 unlock() 的 RAII 方式 condition_variable——系统中线程间进行事件通信的条件变量 thread_local——线程本地存储 期值 future——一个句柄，通过它你可以从一个共享的单对象缓冲区中 get() 一个值，可能需要等待某个 promise 将该值放入缓冲区。 promise——一个句柄，通过它你可以将一个值 put() 到一个共享的单对象缓冲区，可能会唤醒某个等待 future 的 thread。 packaged_task——一个类，它使得设置一个函数在线程上异步执行变得容易，由 future 来接受 promise 返回的结果。 async()——一个函数，可以启动一个任务并在另一个 thread 上执行。 §4.2：简化使用 最常用的三种：\n§4.2.1：auto\nauto 是个纯粹的简化特性，而 decltype 的主要目的，则是让基础库可以使用复杂的元编程。然而，从语言使用的技术角度来看，它们是密切相关的。\n§4.2.2：范围 for\n§4.3.1：lambda 表达式\n移动语义：减少拷贝次数\n智能指针\nshared_ptr——代表共享所有权 unique_ptr——代表独占所有权（取代 C++98 中的 auto_ptr） {} 使用初始化器列表\nnullptr\nconstexpr\n让编译期计算达到类型安全 一般来说，通过将计算移至编译期来提高效率 支持嵌入式系统编程（尤其是 ROM） 直接支持元编程（而非模板元编程（§10.5.2）） 让编译期编程与“普通编程”非常相似 字符串字面量\n属性（）暂时不清楚具体用法\nC++11 增加了标准属性 [[noreturn]] 和 [[carries_dependency]]。 C++17 增加了 [[fallthrough]]、[[nodiscard]] 和 [[maybe_unused]]。 C++20 增加了 [[likely]]、[[unlikely]]、[[deprecated(message)]]、[[no_unique_address]] 和 [[using: …]]。 垃圾收集\n同时支持垃圾收集实现和基于可达性的泄漏检测器。这是通过把“隐藏指针”的程序定为未定义行为来实现的；举例来说，将指针与另一个值进行异或运算，然后将它转换回普通指针并对其进行解引用就是一种隐藏行为。\n§4.3：改进对泛型编程的支持 这些好处是\n超越以 C 风格或面向对象风格所可能获得的灵活性 更清晰的代码 更细的静态类型检查粒度 效率（主要来自内联、让编译器同时查看多处的源代码，以及更好的类型检查） C++11 中支持泛型编程的主要新特性有：\n§4.3.1：lambda 表达式\n把代码写在需要它的那个准确位置上（通常作为函数参数）。 从代码内部访问代码的上下文。 引发的讨论\n语法应该是富有表现力的还是简洁的？ lambda 表达式可以从哪个作用域引用什么名字？[Crowl 2009]。 从 lambda 表达式生成的函数对象应该是可变的吗？默认情况下不是。 lambda 表达式能是多态的吗？到 C++14 才可以（§5.4）。 lambda 表达式的类型是什么？独有的类型，除非它基本上是一个局部函数。 lambda 表达式可以有名字吗？不可以。如果你需要一个名字，就把它赋给一个变量。 名称是由值绑定还是由引用绑定？你来选择。 变量可以移动到 lambda 表达式中（相对于复制）吗？到 C++14 才可以（§5）。 语法是否会与各种非标准扩展发生冲突？（不严重）。 §4.3.2：变参模板\n直接解决两个问题：\n不能实例化包含任意长度参数列表的类模板和函数模板。 不能以类型安全的方式传递任意个参数给某个函数 变参模板的基本思路是，递归构造一个参数包，然后在另一个递归过程来使用它。递归技巧是必须的，因为参数包中的每个元素都有它自己的类型（和大小）。\n编译时间过长的问题随模板元编程的大量使用（§10.5.2）变得越来越严重，对此变参模板是一项重大（有时是 20 倍）改进。可惜，变参模板越变越流行，也成了 C++ 标准库中必需的部分，以至编译时间的问题又出现了。\n变参模板的缺点是容易导致代码膨胀，因为 N 个参数意味着模板的 N 次实例化。\n§4.3.3：template 别名\n1 2 3 typedef double (*analysis_fp)(const vector\u0026lt;Student_info\u0026gt;\u0026amp;); using analysis_fp = double (*)(const vector\u0026lt;Student_info\u0026gt;\u0026amp;); C 定义类型别名的机制是靠 typedef\n类型和模板别名是某些最有效的零开销抽象及模块化技巧的关键。别名让用户能够使用一套标准的名字而同时让各种实现使用各自（不同）的实现技巧和名字。这样就可以在拥有零开销抽象的同时保持方便的用户接口\n§4.3.4：tuple\n元组是大小固定而成员类型可以不同的容器。作为一种通用的辅助工具，它们增加了语言的表现力。举几个元组类型一般用法的例子：\n作为返回类型，用于需要超过一个返回类型的函数 编组相关的类型或对象（如参数列表中的各条目）成为单个条目 同时赋多个值 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 auto SVD(const Matrix\u0026amp; A) -\u0026gt; tuple\u0026lt;Matrix, Vector, Matrix\u0026gt; { Matrix U, V; Vector S; // ... return {U,S,V}; }; void use() { Matrix A; // ... auto [U,S,V] = SVD(A); // 使用元组形式和C++17结构化绑定， // 这写法怎么和matlab一样一样的 } 为什么 tuple 不是语言特性？我不记得当时有人这么问过，尽管一定有人想到过这一点。长期以来（自 1979 年），我们的策略就是，如果能合理地将新特性以库的形式加入 C++，就不要以语言特性加入；如果不能，就要改进抽象机制使其成为可能。这一策略有显而易见的优势：\n通常对一个库做试验比对一个语言特性做试验更容易，这样我们就更快地得到更好的反馈。 库可以早在所有编译器升级到支持新特性之前就得到严肃使用。 抽象机制（类，模板等）上的改进，能在眼前问题之外提供帮助。 §4.2.5：统一初始化\n§4.4：提高静态类型安全 依赖静态类型安全有两大好处：\n明确意图 帮助程序员直接表达想法 帮助编译器捕获更多错误 帮助编译器生成更好的代码。 C++11 中与类型安全直接相关的改进有：\n对于线程和锁的类型安全接口——避免 POSIX 和 Windows 在并发代码中对 void** 及宏的依赖（§4.1.2） 范围 for——避免错误地指定范围（§4.2.2） 移动语义——解决指针的过度使用问题（§4.2.3） 资源管理指针（unique_ptr 和 shared_ptr（§4.2.4）） 统一初始化——让初始化更通用，更一致，更安全（§4.2.5） constexpr——消除多处（无类型和无作用域的）宏的使用（§4.2.7） 用户定义的字面量——让用户定义类型更像内建类型（§4.2.8） enum class——消除一些涉及整型常量的弱类型做法 std::array——避免内建数组不安全地“退化”成指针 §4.5：支持对库的开发 设计 C++ 基础库，往往要在性能和易用性方面同 C++ 及其他语言的内置功能进行竞争。这时，查找规则、重载决策、访问控制、模板实例化规则等特性之中的微妙之处会组合起来，产生强大的表达能力，但同时也暴露出可怕的复杂性。\nSFINAE（Substitution Failure Is Not An Error，替换失败不是错误）。\n你如何表达一个当且仅当某个谓词为真时才有的操作？概念为 C++20 提供了这样的支持（GCC 自 2015 年开始支持），但在 21 世纪早期，人们不得不依赖于晦涩的语言规则。\n1 2 3 4 5 6 7 8 9 10 11 12 13 template\u0026lt;typename T, typename U\u0026gt; struct pair { T first; U second; // ... /*当且仅当 pair 的两个成员都有拷贝赋值操作时 pair 才有拷贝赋值操作。这超乎寻常的丑陋，但它对于定义和实现基础库也超乎寻常的有用——在概念还没有出现时。*/ /*如果成员都有拷贝赋值，enable_if\u0026lt;…,pair\u0026amp;\u0026gt;::type 会成为一个普通的 pair\u0026amp;，否则它的实例化就会失败（因为 enable_if 没有为赋值提供一个返回类型）。这里 SFINAE 就起作用了：替换失败不是错误；失败的结果就如同整条声明不曾出现一样。*/ /*这里的 is_copy_assignable 是一个 type trait（类型特征），C++11 提供了数十个这样的特征以便程序员在编译期询问类型的属性。*/ enable_if\u0026lt;is_copy_assignable\u0026lt;T\u0026gt;::value \u0026amp;\u0026amp; is_copy_assignable\u0026lt;U\u0026gt;::value,pair\u0026amp;\u0026gt;::type operator=(const pair\u0026amp;); //... }; enable_if 元函数由 Boost 开创并成为 C++11 的一部分。一个大致合理的实现：\n1 2 3 4 5 template\u0026lt;bool B, typename T = void\u0026gt; struct enable_if {}; // false 的情况：里面没有 type template\u0026lt;typename T\u0026gt; struct enable_if\u0026lt;true, T\u0026gt; { typedef T type; }; // type 是 T 元编程支持\n为了改进编译器资源的利用，改进尝试采用了两条（至少理论上）互补的路径：\n语言：概念（§6），编译期函数（§4.2.7），lambda 表达式（§4.3.1），模板别名（§4.3.3），以及更精确的模板实例化规范（§4.5.1）。 标准库：tuple（§4.3.4），类型特征（§4.5.1），以及 enable_if（§4.5.1）。 遗憾的是，概念在 C++11（§6.2）中失败了，这给（通常复杂得可怕而且容易出错的）权宜之计留下了生存空间，典型情况会涉及类型特征和 enable_if（§4.5.1）\nnoexcept\n§4.6：标准库组件 C++11 增加了几个关键的库组件来支持特定任务：\nthread——基于线程和锁的并发\nregex——正则表达式\nchrono——时间\nHoward Hinnant 的 chrono 库 [Hinnant et al. 2008] 处理时间点和时间间隔，在提供复杂功能的同时仍保持了易用性。\n到了 C++20，chrono 得到进一步增强，加入了处理日期和时区的功能\nrandom——随机数产生器和分布.\nrandom 库提供了分布函数和随机数产生器，其复杂性被誉为“每个随机数库都想长成的样子”。\n5. C++14：完成 C++11 二进制4字面量，例如 0b1001000011110011 §5.1：数字分隔符——为了可读性，例如 0b1001'0000'1111'0011 1 2 auto a = 1\u0026#39;234\u0026#39;567; // 1234567（整数） auto b = 1\u0026#39;234\u0026#39;567s; // 1234567 秒 §5.2：变量模板——参数化的常量和变量 1 2 3 4 5 6 7 8 template\u0026lt;typename T\u0026gt; constexpr T pi = T(3.1415926535897932385); // 模板常量 template\u0026lt;typename T\u0026gt; T circular_area(T r) { return pi\u0026lt;T\u0026gt; * r * r; } 将来可以联系concept或者constexpr一起使用，可以达到不错的效果。\n1 2 3 4 5 6 template\u0026lt;typename T\u0026gt; concept SignedIntegral = Signed\u0026lt;T\u0026gt; \u0026amp;\u0026amp; Integral\u0026lt;T\u0026gt;; // concept起到的作用为类型约束，编译时检查 template\u0026lt;typename T\u0026gt; constexpr T pi_v = unspecified; // pi_v为模板变量 constexpr double pi = pi_v\u0026lt;double\u0026gt;; // b §5.3：函数返回类型推导 C++11 引入了从 lambda 表达式的 return 语句来推导其返回类型的特性。C++14 将该特性扩展到了函数：\n1 2 template\u0026lt;typename T\u0026gt; auto size(const T\u0026amp; a) { return a.size(); } §5.4：泛型 lambda 表达式 lambda 表达式是函数对象（§4.3.1），因此它们显然可以是模板。在 C++14 中引入泛型 lambda 表达式，而没有引入概念，使得对受约束和不受约束的 lambda 表达式参数和函数参数的规则和写法就没有一起考虑.\n§5.5：constexpr 函数中的局部变量 该标准给了constexpr更宽松的限制，允许使用局部变量和for循环，比如：\n1 2 3 4 5 6 7 8 9 10 constexpr int min(std::initializer_list\u0026lt;int\u0026gt; xs) { int low = std::numeric_limits\u0026lt;int\u0026gt;::max(); for (int x : xs) if (x \u0026lt; low) low = x; return low; } constexpr int m = min({1,3,2,4}); 移动捕获——例如 [p = move(ptr)] {/* ... */}; 将值移入 lambda 表达式 按类型访问元组，例如 x = get\u0026lt;int\u0026gt;(t); 标准库中的用户定义字面量，例如：10i，\u0026quot;Hello\u0026quot;s，10s，3ms，55us，17ns 6. 概念 碎碎念：目前所见过的源码就没见过concept的一些使用，这篇好难，看的我头大，所以也看的好潦草，在网上搜索了一些相关文章的讲解，留些其它链接了。\nC++20新特性之concept - uint128’s Blog\n评价：一般般，不细致，讲的很简单，简单过过当作了解没问题。\nconcept for C++20用法简介 - 南山烟雨珠江潮\n评价：很强的佬，还看到这个博主的c++2x特性专栏，很值得订阅。\n这两文章都重在用法，基本就是依据require和SFINEA规则，可以实现约束和重载。concept可以理解为对SIFNAE机制的升级。\nC++20: Concept详解以及个人理解 - Lumi\n评价：最详细的一篇，作者网易游戏开发，图形学领域，好厉害。\n最终评价，与其探究concept的演化过程不如直接看当前结论，毕竟从0几年提出到20年才正式宣布，过程太长了，研究这个对初学者比如我，不太友好。\n由于模板极大的灵活性，编译器要做的时就是在函数体中使用模板参数。如果正确调用将生成出色的代码，不正确的调用会生成一塌糊涂的错误信息。因此衍生出来了concept。\n1 2 3 4 5 6 7 8 9 10 template\u0026lt;range R, typename Value\u0026gt; requires equality_comparable\u0026lt;Value, Range::value_type\u0026gt; // equity_comparable, 说明两个模板参数之间必须有关系，同时Ranges标准库组件规范了两者间的关系 forward_iterator find(R r, const Value\u0026amp; val) { auto first = begin(r); auto last = end(r); while (first!=last \u0026amp;\u0026amp; *first!=val) ++first; return first; } 6.1 概念的早期历史 早期认为C风格的宏能够有效支持泛型编程，但是宏只是预处理器定义的一种简单的文本替换机制，缺乏类型检查，调试困难。为了设计有合适接口的模板，我们的设计目标是\n全面的通用性/表现力——我明确不希望这些功能只能表达我想到的东西。 与手工编码相比，零额外开销——例如，我想构建一个能够与 C 语言的数组在时间和空间性能方面相当的 vector。 规范化的接口——我希望类型检查和重载的功能与已有的非泛型的代码相类似 Bjarne Stroustrup 和 Gabriel Dos Reis 在 2003 年发表的论文 [Stroustrup 2003; Stroustrup and Dos Reis 2003a,b] 明确指出，概念是简化泛型编程的宏伟计划的一部分。例如，一个 concept 可以被定义为一组使用模式的约束。下面是为concept提出的建议\n概念——用于指定对模板参数要求的编译期谓词。 根据使用模式来指定原始约束——以处理重载和隐式类型转换。 多参数概念——例如 Mergeable\u0026lt;In1,In2,Out\u0026gt;。 类型和值概念——也就是说，概念既可以将值也可以将类型当作参数，例如 Buffer\u0026lt;unsigned char,128\u0026gt;。 模板的“类型的类型”简略写法—例如 template\u0026lt;Iterator Iter\u0026gt; …。 “模板定义的简化写法”——例如 void f(Comparable\u0026amp;); 使泛型编程更接近于“普通编程”。 auto 作为函数参数和返回值中约束最少的类型。 统一函数调用（§8.8.3）——减少泛型编程与面向对象编程之间的风格差异问题（例如 x.f(y)、f(x,y) 和 x+y）。 6.2 C++0x 概念 概念被定义为一组操作和相关类型\n将概念定义为一组操作的方法中存在一个严重的问题，考虑在 C++ 中传递参数的方式。传递参数的不同方式的语义并不相同，因此我们自然而然地转向接受指定的参数类型，将匹配的负担推到了类型设计者和 concept_maps 的作者。\n概念使用，require 概念映射，concept_map 定义检查 为了完成从无约束的模板到使用概念的模板的转换，我们需要语言支持。在 C++0x 的设计中，这两类模板非常不同：\n受约束模板不能调用无约束模板，因为不知道无约束模板使用什么操作，因此无法对受约束模板进行定义检查。 无约束模板可以调用受约束模板，但是检查必须推迟到实例化的时候，因为在那之前我们不知道无约束模板在调用中使用什么类型。 概述了在我看来要让概念在 C++0x 中变得可接受所必须做的最小改进：\n尽量少使用 concept_map。 使所有 concept_map 隐式/自动化。 概念如需要 begin(x)，那它也得接受 x.begin()，反之亦然（统一函数调用）；（§6.1），（§8.8.3） 使所有标准库概念隐式/自动化。 对概念目标用户的分歧：\n显式还是隐式：为了安全和避免意外，程序员是否应该显式地说明如何从潜在可选方案中做决策？该讨论最终涉及有关重载决策、作用域决策、类型与概念的匹配、概念之间的关系，等等。 专家与普通人：关键语言和标准库工具是否应该设计为供专家使用？如果是这样，是否应该鼓励“普通程序员”只使用有限的语言子集，是否应该为“普通程序员”设计单独的库？这个讨论出现在类、类层次结构、异常、模板等的设计和使用的场景中。 6.3 Concepts TS 2009 年，几乎是在概念刚从 C++0x 移除之后，Gabriel Dos Reis、Andrew Sutton 和我开始重新设计概念。这次设计是根据我们最初的想法、从 C++0x 语言设计中得到的经验、使用 C++0x 概念的经验，以及标准委员会的反馈。我们的结论是\n概念必须有语义上的意义 概念数量应该相对较少 概念应该基本，而非最小 在 Concepts TS 中 [Sutton 2017]\n概念基于编译期谓词（包括多参数谓词和值参数）。 以使用模式来描述原始要求 [Dos Reis 和 Stroustrup 2006]（requires 表达式）。 概念可以用在一般的 requires 子句中，当作模板形参定义中 typename 的替代，也可以当作函数形参定义中类型名的替代。 从类型到概念的匹配是隐式的（没有 concept_map）。 重载中概念间是隐式的关系（通过计算得出，而不需要为概念进行显式细化）。 没有定义检查（至少目前还没有，所以也没有 late_check）。 没有 axiom，但这只是因为我们不想因为一个潜在有争议的特性而让设计更加复杂、产生拖延。C++0x 的 axiom 也可以是一个好起点。 7. 错误处理 对使用 C++ 异常处理应具有怎样的态度？\n没理解，这篇是真的不太理解，平时写代码也很少处理异常）\n仅仅知到只有noexcept可用，或者RALL管理内存之类的，待办，原文链接：错误处理.有兴趣的去看原文吧，看了几个回答貌似是说大部分代码也不太会进行异常处理。\nC++ 从 C 语言中继承了各种基于错误返回码的机制，错误可以用特殊值、全局状态、局部状态和回调等多种方式表达。\nC++ 异常机制的主要目标是使不完整或复杂的错误处理中的错误最小化。\nC++ 异常是在 1988–89 年设计的，旨在解决当时普遍存在的复杂且容易出错的错误处理技术。当前控制异常的机制：\nRALL C++11 引入了 noexcept 作为一种更简单、更有效的控制异常的机制（§4.5.3）。 C++ 函数中的异常不会消失，因此我们将有四种选择：错误码、编译期检查的异常（例如 [Sutter 2018b]）、当前异常和 noexcept。只有当前的异常和非本地错误码不会影响类型系统或调用约定（ABI 接口）。 从根本上讲， C++ 需要两种错误处理机制：\n异常——罕见的错误或直接调用者无法处理的错误。 错误码——错误码表示可以由直接调用者处理的错误（通常隐藏在易于使用的检测操作中或作为 (值,错误码) 对从函数返回）。 8. C++17：大海迷航 C++17提供的新特性也挺多，包括21个新的语言特性和13个标准库中增加的特性。令人困扰的是这些功能没有统一的主题，没有整体的规划，似乎只是由于可以达到投票多数而被扔进语言和标准库中的一组“聪明的想法”。这种状况可能给未来语言的发展带来更大的弊端，因此必须采取一些措施做出改变 。\n21 个新的语言特性 构造函数模板参数推导——简化对象定义（§8.1） 推导指引——解决构造函数模板参数推导歧义的明确写法（§8.1） 结构化绑定——简化写法并消除一种未初始化变量的来源（§8.2） inline 变量——简化了那些仅有头文件的库实现中的静态分配变量的使用 [Finkel and Smith 2016] 折叠表达式——简化变参模板的一些用法 [Sutton and Smith 2014] 条件中的显式测试——有点像 for 语句中的条件（§8.7） 保证的复制消除——去除了很多不必要的拷贝操作 [Smith 2015] 更严格的表达式求值顺序——防止了一些细微的求值顺序错误 [Dos Reis et al. 2016b] auto 当作模板参数类型——值模板参数的类型推导 [Touton and Spertus 2016] 捕捉常见错误的标准属性——[[maybe_unused]]、[[nodiscard]] 和 [[fallthrough]] [Tomazos 2015] 十六进制浮点字面量 [Köppe 2016a] 常量表达式 if——简化编译期求值的代码 标准库中增加大约 13 个新特性 optional、any 和 variant——用于表达“可选”的标准库类型（§8.3）\n替代union\nshared_mutex 和 shared_lock**（读写锁）**和 scoped_lock（§8.4）\n并行 STL——标准库算法的多线程及矢量化版本（§8.5）\n1 sort(par_unseq, begin(v), end(v)); // 考虑并行和向量化 当前并行版本并不完全，估计会在C++23中具体提出。\n文件系统——可移植地操作文件系统路径和目录的能力（§8.6）\nstring_view——对不可变字符序列的非所有权引用 [Yasskin 2014]\n数学特殊函数——包括拉盖尔和勒让德多项式、贝塔函数、黎曼泽塔函数 [Reverdy 2012]\n9. C++20：方向之争 设计原则 在《C++ 语言的设计和演化》[Stroustrup 1994]（§2.1）中提出的“经验法则”包括 RAII（§2.2.1）、面向对象编程、泛型编程和静态类型安全。 “简单的事情简单做！”（§4.2）则引出洋葱原则（§4.2）。 从代码到硬件的直接映射和零开销抽象（§1）（§11.2）。 基于意见反馈来发展 C++，以解决现实世界的实际问题（§11.2）。 保持稳定性和兼容性 [Koenig and Stroustrup 1991b; Stroustrup 1994]。 直接和硬件打交道的能力，强有力的可组合的抽象机制，以及最小化的运行时系统 认为重要且适合引入 C++17 的内容及其理由：\n概念——它让我们可以精确描述泛型程序，并解决对于错误信息质量的广泛抱怨。 模块——只要它可以显著地提高与宏的隔离并大大优化编译时间。 范围库和其他关键 STL 组件对概念的使用——为主流用户改进错误信息质量和提高库规范的精确性（“STL2”）。 统一调用语法——简化模板库的规范和使用。 协程——应该非常快速而简单。 网络库支持——基于 asio 库，如相应 TS 所描述。 契约——不一定需要在 C++17 的库规范中使用。 SIMD 向量和并行算法。 标准库词汇类型，比如 optional、variant、string_view 和 array_view。 一种在栈上提供数组（stack_array）的“魔法类型”，合理支持安全、便捷的使用。 C++20 特性包括：\n§6.4：概念——对泛型代码的要求进行明确规定\n§9.3.1：模块——支持代码的模块化，使代码更卫生并改善编译时间\nC++20 新特性: modules 及实现现状\n1 2 3 4 5 6 7 8 9 10 11 12 13 export module map_printer; // 定义一个模块 import iostream; // 使用 iostream import containers; // 使用我自己的 containers using namespace std; export // 让 print_map() 对 map_printer 的用户可用 template\u0026lt;Sequence S\u0026gt; requires Printable\u0026lt;Key_type\u0026lt;S\u0026gt;\u0026gt; \u0026amp;\u0026amp; Printable\u0026lt;Value_type\u0026lt;S\u0026gt;\u0026gt; void print_map(const S\u0026amp; m) { for (const auto\u0026amp; [key,val] : m) // 分离键和值 cout \u0026lt;\u0026lt; key \u0026lt;\u0026lt; \u0026#34; -\u0026gt; \u0026#34; \u0026lt;\u0026lt; val \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; } 关键思想：\nexport 指令使实体可以被 import 到另一个模块中。 import 指令使从另一个模块 export 出来的实体能够被使用。 import 的实体不会被隐式地再 export 出去。 import 不会将实体添加到上下文中；它只会使实体能被使用（因此，未使用的 import 基本上是无开销的）。 §9.3.2：协程——无栈协程\nc++20协程入门\n§9.3.3：编译期计算支持\nC++20 增加了好几个相关的特性：\nconsteval——保证在编译期进行求值的 constexpr 函数 [Smith et al. 2018a] constinit——保证在编译期初始化的声明修饰符 [Fiselier 2019] 允许在 constexpr 函数中使用成对的 new 和 delete [Dimov et al. 2019] constexpr string 和 constexpr vector [Dionne 2018] 使用 virtual 函数 [Dimov and Vassilev 2018] 使用 unions、异常、dynamic_cast 和 typeid [Dionne and Vandevoorde 2018] 使用用户定义类型作为值模板参数——最终允许在任何可以用内置类型的地方使用用户定义类型 [Maurer 2012] is_constant_evaluated() 谓词——使库实现者能够在优化代码时大大减少平台相关的内部函数的使用 [Smith et al. 2018b] §9.3.4：\u0026lt;=\u0026gt;——三路比较运算符\nC++中的三向比较运算符：\n在C++20及以后的版本中，\u0026quot;\u0026lt;=\u0026gt;\u0026quot; 是一个“ spaceship operator ”（太空船运算符），用于比较两个值并返回一个标准的比较结果。它能够实现全序比较，返回值为： -1 如果左边的值小于右边的值， 0 如果两边的值相等， 1 如果左边的值大于右边的值。 §9.3.5：范围——提供灵活的范围抽象的库\n比如\n1 2 3 4 void test(vector\u0026lt;string\u0026gt;\u0026amp; vs) { sort(vs); // 而不是 sort(vs.begin(),vs.end()) } range 本身是一种 concept（§6）。所有 C++20 标准库算法现在都使用概念进行了精确规定。\n§9.3.6：日期——提供日期类型、日历和时区的库\n§9.3.8：跨度——提供对数组进行高效和安全访问的库\n1 2 3 4 5 void f(span\u0026lt;int\u0026gt; a) // span 包含一根指针和一条大小信息 { for (int\u0026amp; x : a) x = 7; // 可以 } span的返回类型最终被定为无符号整数，认为与标准库容器保持一致性更重要，这点使得使用无符号整数是不是一个过去的失误变得无关紧要。\n§9.3.7：格式化——提供类型安全的类似于 printf 的输出的库\n1 2 string s = \u0026#34;foo\u0026#34;; cout \u0026lt;\u0026lt; format(\u0026#34;The string \u0026#39;{}\u0026#39; has {} characters\u0026#34;,s,s.size()); §9.4：并发改进——例如作用域线程和停止令牌\n当前改进：\njthread 和停止令牌 [Josuttis et al. 2019a] atomic\u0026lt;shared_ptr\u0026lt;T\u0026gt;\u0026gt; [Sutter 2017b] 经典的信号量 [Lelbach et al. 2019] 屏障和锁存器 [Lelbach et al. 2019] 小的内存模型的修复和改进 [Meredith and Sutter 2017] §9.5：很多次要特性——例如 C99 风格的指派初始化器和使用字符串字面量作为模板参数\nC99 风格的指派初始化器 [Shen et al. 2016]\n对 lambda 捕获的改进 [Köppe 2017b]\n泛型 lambda 表达式的模板参数列表 [Dionne 2017]\n范围 for 中初始化一个额外的变量（§8.7）\n不求值语境中的 lambda 表达式 [Dionne 2016]\nlambda 捕获中的包展开 [Revzin 2017]\n在一些情况下移除对 typename 的需要 [Vandevoorde 2017]\n更多属性：[[likely]] 和 [[unlikely]] [Trychta 2016]\n在不使用宏的情况下，source_location 给出一段代码中的源码位置 [Douglas and Jabot 2019]\n功能测试宏 [Voutilainen and Wakely 2018]\n条件 explicit [Revzin and Lavavej 2018]\n有符号整数保证是 2 的补码 [Bastien 2018]\n数学上的常数，比如 pi 和 sqrt2 [Minkovsky and McFarlane 2019]\n位的操作，比如轮转和统计 1 的个数 [Maurer 2019]\nC++20 时尚未准备就绪，但可能会成为 C++23 的主要特性：\n§8.8.1：网络——网络库（sockets 等） §9.6.2：静态反射——根据周围程序生成代码的功能 模式匹配——根据类型和对象值选择要执行的代码 10. 2020 年的 C++ c++应用领域： 工业界：电信（例如 AT\u0026amp;T、爱立信、华为和西门子）、移动设备（基本上是所有，信号处理、屏幕渲染、对性能或可移植性有重大要求的应用）、微电子（例如 AMD、英特尔、Mentor Graphics 和英伟达）、金融（例如摩根士丹利和文艺复兴）、游戏（几乎所有）、图形和动画（例如 Maya、迪士尼和 SideFx）、区块链实现（例如 Ripple）、数据库（例如 SAP、Mongo、MySQL 和 Oracle）、云（例如谷歌、微软、IBM 和 Amazon）、人工智能和机器学习（例如 TensorFlow 库）、运营支持（例如 Maersk 和 AT\u0026amp;T）。 科学：航空航天（例如 Space X、火星漫游者、猎户座载人飞行器、詹姆斯·韦伯太空望远镜）、高能物理（例如 CERN 欧洲核子研究中心、SLAC 国家加速器实验室、费米实验室）、生物学（遗传学、基因组测序）、超大规模计算。 教学：全球大多数工程院校。 软件开发：TensorFlow、工具、库、编译器、Emscripten（从 C++ 生成 asm.js 和 WebAssembly）、运行期代码生成、LLVM（许多新语言的后台支柱，也大量用于工具构建中）、XML 和 JSON 解析器、异构计算（例如 SYCL [Khronos Group 2014–2020] 和 HPX [Stellar Group 2014–2020]）。 Web 基础设施：浏览器（Chrome、Edge、FireFox 和 Safari）、JavaScript 引擎（V8 和 SpiderMonkey）、Java 虚拟机（HotSpot 和 J9）、谷歌和类似组织（搜索、map-reduce 和文件系统）。 主要 Web 应用：阿里巴巴、Amadeus（机票）、Amazon、苹果、Facebook、PayPal、腾讯（微信）、Yandex。 工程应用：达索（CAD/CAM）、洛克希德·马丁（飞机）。 汽车：辅助驾驶 [ADAS Wikipedia 2020; Mobileye 2020; NVIDIA 2020]、软件架构 [Autosar 2020; Autosar Wikipedia 2020]、机器视觉 [OpenCV 2020; OpenCV Wikipedia 2020]、宝马、通用、梅赛德斯、特斯拉、丰田、沃尔沃、大众、Waymo（谷歌自动驾驶汽车）。 嵌入式系统：智能手表和健康监控器（例如佳明）、相机和视频设备（例如奥林巴斯和佳能）、导航辅助设备（例如 TomTom）、咖啡机（例如 Nespresso）、农场动物监控器（例如 Big Dutchman）、生产线温度控制（例如嘉士伯）。 安全：卡巴斯基、美国国家安全局、赛门铁克。 医疗和生物学：医学监测和成像（例如西门子、通用电气、东芝和飞利浦）、断层扫描（例如 CT）、基因组分析、生物信息学、放射肿瘤学（例如 Elekta 和 Varian）。 C++ 语言相关的学术研究成果： 概念：泛型编程 [Dehnert and Stepanov 2000]、C++0x 概念 [Gregor et al. 2006]、使用模式 [Dos Reis and Stroustrup 2006]、库设计 [Sutton and Stroustrup 2011]。 理论与形式化体系：继承模型 [Wasserrab et al. 2006]、模板和重载 [Dos Reis and Stroustrup 2005a]、模板语义 [Siek and Taha 2006]、对象布局 [Ramananandro et al. 2011]、构造和析构 [Ramananandro et al. 2012]、用于代码处理的表示形式 [Dos Reis and Stroustrup 2009，2011]、资源模型 [Stroustrup et al. 2015]。 动态查找：快速动态类型转换 [Gibbs and Stroustrup 2006]、模式匹配 [Solodkyyet et al. 2012]、多重方法 [Pirkelbauer et al. 2010]。 静态分析：可靠的表示法 [Yang et al. 2012]、实践经验 [Bessey 2010]。 性能：代码膨胀 [Bourdev and Järvi 2006，2011]、异常实现 [Renwicket et al. 2019]。 语言比较：泛型编程 [Garcia et al. 2007]。 并发和并行编程：内存模型 [Batty et al. 2013，2012，2011]、HPX（一个适用于任何规模的并行和分布式应用程序的通用 C++ 运行时系统 [Kaiser et al. 2009Sept]）、STAPL（自适应泛型并行 C++ 库 [Zandifar et al. 2014]）、TBB（英特尔的任务并行库 [Reinders 2007]）。 协程：数据库优化 [Jonathan et al. 2018; Psaropoulos et al. 2017]。 软件工程：代码的组织和优化 [Garcia and Stroustrup 2015]、常量表达式求值 [Dos Reis and Stroustrup 2010] c++支持的工具： 工业级的集成软件开发环境：例如微软的 Visual Studio [Microsoft 2020; VStudio Wikipedia 2020] 和 JetBrains 的 CLion [CLion Wikipedia 2020; JetBrains 2020]。这些环境不仅支持编辑和调试，还支持各种形式的分析和简单的代码转换。 在线编译器：例如 Compiler Explorer [Godbolt 2016] 和 Wandbox [Wandbox 2016–2020]。这些系统允许从任何浏览器中编译 C++ 程序，有时甚至可以执行。它们可用于实验，检查代码质量，还有比较不同的编译器及编译器和库的不同版本。 GUI 库和工具：例如 Qt [Qt 1991–2020]、GTKmm [GTKmm 2005–2020] 和 wxWidgets [wxWidgets 1992–2020]。不幸的是，Qt 依赖于元对象协议（meta-object protocol，缩写为 MOP），因此 Qt 程序还不是标准的 ISO C++ 应用。静态反射（§9.6.2）使我们最终能够解决这个问题。C++ 社区的问题不是没有好的 GUI 库，而是太多了，因此会有选择困难。 分析器：例如 Coverity [Coverity 2002–2020]，Visual Studio 的 C++ Core Guidelines 分析器（§10.6）和 Clang Tidy [Clang Tidy 2007–2020]。 编译器工具支持：例如 LLVM 编译器后端基础设施，可简化代码生成和代码分析 [LLVM 2003–2020]。除了 C++ 本身，这为许多新语言提供了福利。 构建系统：例如 build2 [Build2 2014–2020] 和 CMake [CMake 2000–2020]，以及 GNUmake[GNUmake 2006–2020]。同样，在没有标准的情况下，选择会有困难。 包管理器：例如 Conan [Conan 2016–2020] 和 vcpkg [vcpkg 2016–2020]。 运行时环境：例如 WebAssembly：将 ISO C++ 编译为字节码以在浏览器中部署的系统 [WebAssembly 2017–2020]。 运行时编译、JIT 和链接：例如 Cling [Cling 2014–2020; Naumann 2012; Naumann et al. 2010] 和 RC++ [RC++ 2010–2020]。 编程范式\n显然，它是面向对象编程：使用了虚函数和类层次结构。 显然是泛型编程：使用了模板（通过使用 range 概念进行参数化，我们得到一个模板）。 显然，这是普通的命令式编程：使用了 for 循环，并按照常规 f(x) 语法定义了一个将要被调用的函数。 模板的泛型编程是 C++ 标准库的支柱：容器、范围（§9.3.5）、算法、iostream、文件系统（§8.6）、随机数（§4.6）、线程（§4.1.2）（§9.4）、锁（§4.1.2）（§8.4）、时间（§4.6）（§9.3.6）、字符串、正则表达式（§4.6）和格式化（§9.3.7）。\nC++ 语言的最终目标是： 使用和学习上都要比 C 或当前的 C++ 容易得多 完全类型安全——没有隐式类型违规，没有悬空指针 完全资源安全——没有泄漏，不需要垃圾收集器 为其构建工具要相对简单——不要有宏 跟当前 C++ 一样快或更快——零开销原则 性能可预测——适用于嵌入式系统 表达力不亚于当前的 C++——很好地处理硬件 11. 回顾 关键的语言技术贡献有：\n静态类型系统，对内置类型和用户定义类型具有同等支持（§2.1） 既有值语义，又有引用语义（§4.2.3） 系统和通用资源管理（RAII）（§2.2） 支持高效的面向对象编程（§2.1） 支持灵活的和高效的泛型编程（§10.5.1） 支持编译期编程（§4.2.7） 直接使用机器和操作系统资源（§1） 通过库提供并发支持（往往使用内建函数实现）（§4.1）（§9.4） C++ 成功的根本原因很简单——它填补了编程领域的一个重要的“生态位”：\n需要有效使用硬件和管理高复杂性的应用程序\n这里有一个现代（2014 年）的 C++ 总结：\n直接映射到硬件 指令和基本数据类型 最初来自于 C 语言 零开销抽象 带构造和析构函数的类、继承、泛型编程、函数对象 最初来自于 Simula 语言（当时还不是零开销的） 在过去几十年的基础上，21 世纪的关键技术进步包括：\n内存模型（§4.1.1） 类型安全的并发支持：线程和锁（§4.1.2）、并行算法（§8.5）、汇合线程（§9.4） 类型推导：auto（§4.2.1）、概念（§6）、模板参数推导（§8.1）、变参模板（§4.3.2） 简化使用：auto（§4.2.1）、范围 for（§4.2.2）、并行算法（§8.5）、范围（§9.3.5）、lambda 表达式（§4.3.1） 移动语义（§4.2.3） 编译期编程：constexpr（§4.2.7）、编译期循环（§5.5）、可确保的编译期求值和容器（§9.3.3）、元编程（§10.5.2） 泛型编程：STL（§10.5.1）、概念（§6）、用户定义类型作为模板参数（§9.3.3）、lambda 表达式（§4.3.1） 元编程（§10.5.2） 它们都与零开销原则相关，但最后两个有点令人惊讶，因为在 2006 至 2020 年期间内，C++ 对它们的支持并不完全。\n为了对委员会的流程进行组织约束，方向组提出 C++ 程序员的“权利法案” [Dawes et al. 2018]：\n编译期稳定性：新版本标准中的每一个重要行为变化都可以被支持以前版本的编译器检测到。 链接期稳定性：除极少数情况外，应避免 ABI 兼容性破坏，而且这些情况应被很好地记录下来并有书面理由支持。 编译期性能稳定性：更改不会导致现有代码的编译时间开销有明显增加。 运行期性能稳定性：更改不会导致现有代码的运行时间开销有明显增加。 进步：标准的每一次修订都会为某些重要的编程活动提供更好的支持，或为某些重要编程群体提供更好的支持。 简单性：每一次对标准的修订都会简化某些重要的编程活动。 准时性：每一次标准的修订都会按照公布的时间表按时交付 书籍：深入理解c++11note 大多就是些简单的笔记，更加具体的可以直接查阅书籍相关章节。\nchapter1 新标准的诞生 c++98/03和c++11 c++98/03的设计目标：\n比c语言更适合系统编程 支持数据抽象 支持面向对象编程 支持泛型编程 c++11的整体设计目标：\n成为更好的适用于系统开发以及库开发的语言 成为更易于教学的语言 保持语言的稳定性，兼容C++03和c语言 C++11相对C++98/03的显著增强\n通过内存模型，线程，原子操作等支持本地并行编程(Native Concurrency) 通过统一初始化表达式，auto，declytype，移动语义等来统一对泛型编程的支持 通过constexpr，POD等更好的支持系统编程 通过内联命名空间，继承构造函数，右值引用等，更好的支持库的构建。 编程中，程序员往往需要把实物，流程，概念等进行抽象描述。但通常我们所指需要抽象出来的不仅仅对象，还有些其它概念，比如类型，类型的类型，算法，甚至资源的生命周期。c++11通常将这些抽象概念实现在库中，从这个角度上看，c++11则是一种所谓的”Lightweight Abstraction Programming Language“，程序员可以将程序设计的重点更多的放在设计，实现以及各种抽象概念的运用上。\n因此，总的来说，灵活的静态类型，小的抽象概念，绝佳的时间和空间运行性能以及和硬件紧密结合工作的能力都是c++11突出的亮点。\nc++11特性一览 稳定性和兼容性之间的抉择 c++11引入的新关键字：\nalignas, alignof of decltype, auto, static_assert, using, noexcept, nullptr, constexpr, thread_local\n新标识符：\noverride, final\n更倾向于使用库而不是扩展语言实现特性 WG21会尽量将语言特性转为库特性实现，而不是优先编译器实现，比如c++11中的线程，正则表达式实现为regex库等。\n库可能通过提供头文件实现，当然也可能实现在编译器内部，比如c++11中的原子操作，编译器在内部将原子操作实现为具体的机器指令，而不是链接库进行存档，因此原子操作并非纯粹的库。\n更倾向于通用的而不是特殊的手段实现特性 比如explicit，继承构造函数，移动语义等\n专家新手一概支持 比如c++98/03中的双右尖括号，由于最长匹配的解析规则，vector\u0026lt;vector\u0026lt;int\u0026gt;\u0026gt;经常会发生不应该存在的右移的报错；统一初始化语法的引入等\n增强类型的安全性 比如强类型枚举\n与硬件紧密合作 比如常量表达式，原子操作等，有助于提高性能，降低存储空间\nconst：只在初始化后才确立类型\nconstexpr：让函数和变量可以在编译时的常量取代\n开发能够改变人们思维方式的特性 lambda函数，通常被处理为匿名的仿函数 如何让成员函数变得无效 融入编程现实 chapter2 保证稳定性和兼容性 保持和c99兼容 _func_功能是返回所在函数的名字，编译器隐式的在函数定义之后定义这个标识符，c++11甚至可以将其用在类或者结构体中。·\n#pragma预处理指令，向编译器传达语言标准以外的信息——C++11中，_Pragma (字符串字面量)操作符\n_VA_ARGS_在宏定义实现部分替换省略号所代表的字符串。\n变长参数的宏定义指宏定义中参数列表的最后一个参数为省略号\n静态断言 assert：将返回值总是需要为真的判别式放在语句中，用于排除设计的逻辑上不应该产生的情况。\n运行时断言 assert 模板实例化也就是编译时期产生断言 static_assert noexcept noexcept表示其修饰的函数不会抛出异常，当然如果出现异常，编译器直接调用std::terminate()函数终止程序运行，比throw()在效率上会高一些，这是因为异常机制会带来额外开销，异常时函数栈会被依次展开，并依帧调用在本帧中以构造的自动变量的析构函数。\nnoexcept\n有助于高效的性能优化。编译器可以进行优化，减少了需要为异常处理预留的额外代码。 提高代码的可预测性，确保不会发生未预期的异常。比如构造或者析构函数中有异常但是没有标记，很可能引发资源泄露或者对象不一致等 快速初始化成员变量 c++11：可以就地初始化静态成员变量和非静态成员变量，一般使用{}或者=；但是（）一般指对自定义变量的表达式列表初始化，在strcut中如此写代码会导致编译出错。\n初始化列表的效果总是优先于就地初始化的。\n非静态成员的sizeof 扩展了sizeof的定义\n扩展的friend语法 friend友元，技能可以无视类中成员的属性，但其实它的功能完全破坏了OOP中封装性的概念。其实也存在建议用Get/Set接口访问类的成员，但也会增加很多代码量。\n因此在c++11中对friend关键字做了些改进，声明一个类为另外一个类的友元时，不在需要使用class关键字。这个带来的最大改动就是程序员可以为类模板声明友元了。\n1 2 3 using DefenderTest = DefenderT\u0026lt;Validator\u0026gt;; using AttacterTest = AttacterT\u0026lt;Validator\u0026gt;; // Validator时DefenderT和AttacterT的友元类 final/override控制 重载：类A中声明的虚函数在派生类B中再次被定义，并且B中的函数fun跟A中的fun的原型一样（函数名，参数列表一样），那么我们就称B重载了A的fun函数。\nfinal：使派生类不可覆盖它修饰的虚函数。当然虚函数中也可也以使用final，但是这样子虚函数无法重载，就失去了虚函数的意义，若不想虚函数被重载，完全可以直接将该成员函数定义为非虚函数即可。\noverride：如果派生类在虚函数声明时使用了override描述符，那么该函数必须重载基类中的同名函数，否则代码无法通过编译。\n模板函数的默认模板参数 默认模板参数通常跟默认函数参数一起使用。\n外部模板 假设test.h中的模板在其它两个文件中需要用，那么它们会分别实例化模板，当然编译进行实例化后，链接器在链接时发现这个冗余会移除重复的实例化代码，会增加编译器的编译和链接时间。因此解决方法是外部模板。\n显式实例化和外部模板的声明，例子：\n1 2 3 4 5 6 7 8 9 10 11 12 //test.h template \u0026lt;typename T\u0026gt; void fun(T){} template void fun\u0026lt;int\u0026gt;(int); // 强制实例化 //test1.h #include \u0026#34;test.h\u0026#34; extern template void fun\u0026lt;int\u0026gt;(int); void test1(){ fun(3); } 外部模板声明出现在某个编译单元中，阈值对应的显示实例化必须出现在另一个编译单元中 外部模板声明不能用于静态函数，但是可以用于类静态成员。（静态函数没有外部链接属性，不能出现在编译单元之外） 外部模板即使不使用，也不会出现报错。外部模板定义更应噶当作一种针对编译器的编译时间以及空间的优化手段。 局部和匿名类型作模板实参 匿名类型：\n临时使用，快速创建 只在当前作用域中有效 总结 本章主要讲述了c++11在过往上做出了一些小改动，主要是为了保障语言的兼容性和稳定性。\n比如为了和c99兼容，c99中的预定的宏，_func_预定义标识符，_Pragma操作符，变长参数定义等，这些是错过c++98但是进入了c++99的一些标准，为了最大程度兼容c，因此也将其加入了c++！1标准。\n同时为了保障稳定性，在原有assert（运行时断言）加上了static_assert（编译断言），有助于对些模板中的断言进行编译时处理；将throw()的使用换成了判断是否抛出异常的描述符noexcept，避免了过多非必要开销；\n为friend语法进行扩展，可以省略class，因此可以适用于模板；sizeof扩展定义能够对非静态成员进行计算；增加了final/override更好的控制不同类中的重载；给类增加了快速初始化的操作，函数模板添加了默认参数的机制，而且还增加了外部模板，优化了编译时间和内存消耗。\nc++11通过这些小修小补，让已有的c++特性看上去更加成熟和完美。\nchapter3 通用为本，专用为末 继承构造函数 增加using-declaration，可用于继承构造函数时。\n注意当基类的构造函数为私有函数，或者是虚继承，那么就不能是在派生类中声明继承构造函数。同时，使用继承构造函数之后，编译器不会再为派生类使用默认构造函数了。\n委派构造函数 定义：它允许一个构造函数在同一个类中调用另一个构造函数，从而避免重复代码。\n注意：不能形成委派环\n实际应用：使用构造模板函数产生目标构造函数\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 public: ComplexClass(T val) : value(val) { std::cout \u0026lt;\u0026lt; \u0026#34;Called ComplexClass(T val): value = \u0026#34; \u0026lt;\u0026lt; value \u0026lt;\u0026lt; std::endl; } // 针对整型类型的构造函数，委托给通用的模板构造函数 ComplexClass(int intValue) : ComplexClass(static_cast\u0026lt;T\u0026gt;(intValue)) { std::cout \u0026lt;\u0026lt; \u0026#34;Called ComplexClass(int intValue)\u0026#34; \u0026lt;\u0026lt; std::endl; } // 处理两个参数的构造函数，最终委托给单参数的模板构造函数 ComplexClass(T val1, T val2) : ComplexClass(val1 + val2) { std::cout \u0026lt;\u0026lt; \u0026#34;Called ComplexClass(T val1, T val2): value = \u0026#34; \u0026lt;\u0026lt; value \u0026lt;\u0026lt; std::endl; } 右值引用：移动语义和完美转发 移动构造函数：接收一个所谓的“右值引用”的参数，使用参数的成员初始了本对象成员，将参数成员的指针设置为空，而后参数成员会被销毁。\n说明：\n右值引用指的是一个临时值，如果是拷贝构造就需要重新分配一个内存空间，移动构造只需要将现在的指针指向参数的值即可，不需要重新分配内存。移动构造的触发点也同样在于右值。\n左值和右值都会进行内存分配，但是右值往往意味着一个很短的临时对象，在表达式结束后就会销毁，开发者使用右值引用为其保存了更长的生命周期，主要的服务点在于移动语义进行资源的转移。\n移动构造主要进行资源的转移，而拷贝构造在于创建一个新的对象，它们的使用场景是不同的\n一般为了避免悬空指针会使用noexcept，标准库中提供了std::move_if_noexcept可以在没有noexcept时返回左值从而使变量可以使用拷贝语义。\n1 2 3 4 5 6 template\u0026lt;class T\u0026gt; void swap(T\u0026amp; a, T\u0026amp; b){ T tmp(move(a)); a = move(b); b = move(T); } 显示转换操作符 explicit显示转换操作符，防止不必要的隐式类型转换。\n列表初始化 增加了初始化方式； 增加了对vector，map等非内置的复杂数据类型的初始化 作用：\n可以防止类型收窄 POD类型 plain old data：描述一个类型的属性。\ntemplate\u0026lt;typename T\u0026gt; struct std::is_trivial辅助的类模板可以进行属性判断。\ntemplate\u0026lt;typename T\u0026gt; struct std::isstandard_layout可以使用模板类来帮助判断类型是否是一个标准布局的类型。\ntrival平凡：\n只有默认构造函数和默认析构函数 有默认拷贝构造函数和默认移动构造函数 有平凡的拷贝构造函数和移动构造函数 不包含虚函数和虚基类 standard layout标准布局：（标准布局的类或者结构体）\n标准布局类的主要目的是为了确保跨平台的一致性和可移植性。标准布局类保证了其内存布局的可预测性，使得这些类的对象可以安全地在不同的编译器和平台间共享。\n所有的非静态成员有相同的访问权限\n在类或者结构体继承时，只存在两种情况\n派生类中有非静态成员，且只有一个仅包含静态成员的基类 基类中有非静态成员，派生类中没有非静态成员 （非静态成员同时出现在派生类和基类，或者非静态成员出现在多个基类，就不属于标准布局）\n（第一个明确派生类的内存布局，第二个派生类的内存布局完全依赖于基类）\n类中的第一个非静态成员的类型和基类不同。\n（如果相同的话，非静态成员的类型和基类类型相同，则派生类不会覆盖基类的空间，而是会强制分配1字节\nc++标准中，如果基类没有成员，派生类的第一个成员和基类共享地址）\n没有虚函数和虚基类。\n所有非静态数据成员均符合标准布局\n在c++11中，\u0026lt;type_traits\u0026gt;头文件也为程序员提供了template \u0026lt;typename T\u0026gt; struct std::is_pod判断类型是否是POD。\nPOD使用好处：\n1）字节赋值，代码中可以安全使用memset和memcpy对POD类型进行初始化和拷贝等操作。\n2）提供对C内存布局兼容。C++程序可以和C函数相互操作，POD类型的数据在C和C++之间操作安全。\n3）保证了静态初始化的安全有效\n非受限联合体 union是构造类型的数据结构，属于联合体，可以定义多种不同的数据类型，这些数据会共享相同的内存空间，复用内存达到节省空间的目的。\n在c++98标准中，并不是所有的数据类型都能够成为联合体的数据成员，比如非POD类型，静态或者引用类型的成员。\n在新的c++11标准中，\n取消了联合体对于数据成员类型的限制，标准规定任何非引用类型都可以成为联合体的数据成员，当然仍然不允许静态成员变量的存在，这就是非受限联合体(Unrestricted Union)。 标准会默认删除一些非受限联合体的默认函数。因此也无法为其它变量声明，因此程序员自己为非受限联合体定义构造函数，利用placement new。 可以将匿名非受限联合体写成类的变长成员，灵活性更高。 用户自定义字面量literal 将声明了后缀标识的字面量转化为需要的类型。\nc++11中的具体规则：\n内联名字空间 namespace命名空间：分割全局共享的命名空间。\nc++98规定不允许在不同的名字空间中对模板进行特化。（避免名字冲突，增加语言复杂性）因此c++11引入了一个新特性：\ninline namespace内联的名字空间：内联的名字空间允许程序员在父名字空间定义或特化子名字空间的模板。它允许程序员在父命名空间定义或者特化子名字空间的模板。\n名称查找：在 inline namespace 中定义的模板和特化会在名称查找时自动被包含在外层命名空间中。这意味着，当你使用模板时，编译器会优先查找 inline namespace 中的定义，而不需要显式指定命名空间。 默认访问性：inline namespace 中的成员可以被外部代码直接访问，无需额外的命名空间前缀。这与普通命名空间不同，后者需要通过明确指定命名空间来访问其内容。 版本控制：inline namespace 允许你创建版本化的模板库。比如，可以在主命名空间中保留旧版本的实现，而在 inline namespace 中添加新的特化或版本，从而保证向后兼容性。 其它特性\nArgument-Dependent name Lookup参数关联名称查找：\n允许编译器在名字空间找不到函数名称时，在参数的名字空间内查找函数名字。\n模板别名 typedef和using\n别名声明与模板兼容，而 C 风格的 typedef 则不兼容。\n使用using定义的别名和使用typedef定以的类型别名都是一样的类型，两者效果相同。或者说在c++11中，using关键字的能力包含了typedef的部分。\n使用模板编程时，using的语法甚至比typedef更加灵活。\n1 2 template\u0026lt;typename T\u0026gt; using MapString = std::map\u0026lt;T, char*\u0026gt;; MapString\u0026lt;int\u0026gt; numberedString; 一般化的SFINEA规则 SFINEA - Substitution failure is not an error.匹配失败不是错误\n准确来说：针对重载的模板参数进行展开时，如果展开导致了类型不匹配，编译器不会报错。\n特点：\n灵活性，简化错误处理，提高可读性（可以明确表达对特定条件的依赖），增强类型安全，条件编译\n常见使用：\n1.STL中，检查布尔条件\n1 2 3 4 5 6 template\u0026lt;int I\u0026gt; void div(char(*)[I % 2 == 0] = 0) { /* this is taken when I is even */ } template\u0026lt;int I\u0026gt; void div(char(*)[I % 2 == 1] = 0) { /* this is taken when I is odd */ } 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 26 #include \u0026lt;iostream\u0026gt; void test(...) { std::cout \u0026lt;\u0026lt; \u0026#34;Catch-all overload called\\n\u0026#34;; } template \u0026lt;class C, class F\u0026gt; auto test(C c, F f) -\u0026gt; decltype((void)(c.*f)(), void()) { std::cout \u0026lt;\u0026lt; \u0026#34;Reference overload called\\n\u0026#34;; } template \u0026lt;class C, class F\u0026gt; auto test(C c, F f) -\u0026gt; decltype((void)((c-\u0026gt;*f)()), void()) { std::cout \u0026lt;\u0026lt; \u0026#34;Pointer overload called\\n\u0026#34;; } struct X { void f() {} }; int main(){ X x; test( x, \u0026amp;X::f); test(\u0026amp;x, \u0026amp;X::f); test(42, 1337); } 总结 右值引用，移动语义，完美抓发\nPOD\n列表初始化\n继承构造，委派构造\n显式类型转换，非受限联合体，内联的名字空间\nSFINAE规则\nchapter4 新手易学，老兵易用 右尖括号\u0026gt;的改进 c++98会将\u0026raquo;优先解析为右移。 c++11则会要求编译器智能的判断哪些情况下\u0026raquo;不是右移符号 auto类型推导 auto声明变量的类型必须由编译器在编译时期推导而成\n直观来说，auto推导的最大优势是在拥有初始化表达式的复杂类型变量声明时简化代码。\n1 for(auto it=vs.begin(); it!=vs.end(); i++){} 第二个优势在于可以免除程序员在一些类型声明时的麻烦，或者避免一些在类型声明时的错误。\n第三个有点在于“自适应”性能，能够在一定程度上支持泛型编程。\n1 2 3 4 5 template\u0026lt;typename T1, typename T2\u0026gt; double Sun(T1 \u0026amp;t1, T2 \u0026amp;t2) { auto s = t1 + t2; return s; } decltype c++中，类型推导是随着模板和泛型编程的广泛使用而引入的。\ndecltype的类型推导并不是像auto一样从变量声明的初始化表达式获得变量的类型，而是以一个普通的表达式为参数，返回该表达式的类型。\ndecltype可以将获得的类型定义另一个变量，也是在编译时进行的。\n应用场景：\ndecltype和typedef/using的合用。 1 using ptrdiff_t = decltype((int*)0 - (int*)0); 增加代码的灵活性和可读性。 重用匿名类型。比如匿名的结构体和联合体的重用 扩大模板泛型 decltype(e)获取类型时，编译器遵循的四个规则：\n如果e是一个不带括号的标记符表达式或者类成员访问表达式，那么decltype(e)就是e所命名的实体的类型。此外，如果e是一个被重载的函数，则会导致编译时错误。 否则，e的类型为T，如果e是一个将亡值，那么类型为T\u0026amp;\u0026amp; 否则，e的类型为T，如果e是一个左值，那么类型为T\u0026amp; 否则，e的类型为T，那么类型为T auto类型推导时不能带走cv限制符(const/volatile)。\ndecltype能够带走表达式的cv限制符，但是如果对象的定义中有const或者volatile限制符，用它进行推导时，不会继承限制符。\n二者在推导出类型之后，都会自动忽略一些冗余的符号。\n追踪返回类型 追踪返回类型配合auto和decltype会真正释放c++11中泛型编程的能力。\n复合符号-\u0026gt;decltype被称为追踪返回类型。\n1 2 3 auto Sum(T1 \u0026amp;t1, T2 \u0026amp;t2) -\u0026gt; decltype(t1 + t2) { return t1 + t2; } 基于范围的for循环 1 2 3 for(迭代变量 : 迭代范围) { action; } // 迭代器有自增指针，迭代范围需要是一个固定值 能否使用基于范围的for循环，需要依赖一些条件：\nfor循环迭代的范围是可确定的。\n比如类，需要有begin和end函数\n比如数组，就是数组的第一个元素和最后一个元素的范围\n要求迭代对象实现++和==操作符，对于标准库中的容器不会有问题，但是用户自己写的类就需要自行提供相关操作。\n总结 双右尖括号\u0026raquo;\n类型推导\n基于范围的for循环\nchapter5 提高类型安全 强类型枚举（mark） 为数值取名的方式有三种：\nenum define const static enum的缺点在于，允许隐式转换为整型，占用存储空间及符号性不确定\n因此c++11引进了一种新的枚举类型，enum class\n强作用域，强类型枚举成员的名称不会输出到它的父作用域空间 转换限制，不能和整型隐式相互转换 可以指定底层类型，默认int 堆内存管理 当写程序时，如果出现程序运行时突然退出，占用内存越来越多最后不得不重启的一些典型症状，有一定概率时C++中的显式堆内存管理并不完善，指针的自由度极高，对程序员的要求也更高。\n从语言层面可以归结以下几个问题：\n野指针：内存单元已经释放但是指针还在使用，当该内存重新被分配时，容易出现不可预测的错误。 重复释放：程序试图释放已经释放过的内存，或者释放已经被重新分配过的内存单元，就会出现重复释放错误。通常重复释放内存会导致C/C++运行时系统打印出大量错误和诊断信息。 **内存泄露：**不需要使用的内存单元没有被释放同样会造成内存谢罗。 为了更好的进行内存管理，c++提出了智能指针和垃圾回收的概念，但是直到c++11都还存在着完善空间。\nC++98中，采用模板类型auto_ptr实现。\nC++11中，采用unique_ptr，shared_ptr，weak_ptr实现。weak_ptr的出现很大程度是对于shared_ptr的环形操作进行处理的。\nunique_ptr：与所指对象的内存紧密绑定，删除了拷贝构造函数，但是保留了移动构造函数。移动构造可以结合move转移对象内存的所有权，原对象会失效。 shared_ptr：允许多个智能指针共享的拥有同一个堆分配对象的内存，实现上增加引用计数，只有当引用计数为0时才会释放内存。 weak_ptr：它可以指向Shared_ptr指向的对象内存，但是并不拥有该内存，而使用weak_ptr的成员lock，就可以返回指向内存的一个shared_ptr对象，并且在所指对象内存无效时，会返回指针空值。 而对于不再使用或者没有任何指针指向的内存空间称为垃圾，对于这些垃圾收集起来再次利用的机制称为垃圾回收。\n主要分为两大类：\n基于引用计数的垃圾回收器\nreference counting garbage collector\n采用引用计数，当计数变为0时可以回收，不会堆系统缓存或者交换空间存在冲击，实现简单，但是额外开销不小，也很难处理环形引用问题。\n基于跟踪处理的垃圾回收器\ntracing garbage collector\n标记-清除（Mark-Sweep）：根据正在使用的对象进行查找，标记正在使用的内存，有标记的就是可达对象或者或对象，没有标记的就是垃圾，会被清扫掉。\n特点：活得对象不会移动，但是存在内存碎片。\n标记-整理（Mark-Compact）\n和上一种基本相同，区别在于会将活对象向左靠齐，不再清扫垃圾，解决了内存碎片的问题\n特点：移动活的对象，但是程序中所有对堆内存的引用都需要更新。\n标记-拷贝（Mark-Copy）\nFrom分配满的时候开始垃圾回收，从From堆空间找出所有活的对象，拷贝到To的堆空间里，交换form和to的角色，重新分配。\n堆的利用率只有一半，而且也需要移动活的对象。\n因此，C++11中提出了最小垃圾支持的概念。只适用于new和delete。\n首先指定了一个安全派生(safely derived)的指针的概念。安全派生的指针是指向由new分配的对象或者子对象的指针。安全派生指针的操作包括：\n在解引用基础上的引用。比如\u0026amp;*p 定义明确的指针操作。比如p+1 定义明确的指针转换。比如static_cast\u0026lt;void*\u0026gt;(p) 指针和整型之间的reinterpret_case。比如reinterpret_cast\u0026lt;intptr_t\u0026gt;p 在c++11的规则中，最小垃圾回收是基于安全派生这个概念的。\n程序员可以通过**get_pointer_safety()**函数查询确认编译器是否支持这个特性：\npointer_safety::strict：表明编译器支持最小垃圾回收以及安全派生相关概念。 pointer_safety::relax或者pointer_safety::preferred，则表明编译器不支持垃圾回收。 此外，C++11允许程序员通过一些API通知垃圾回收器不得回收该内存。\n比如declare_reachable(void *p)函数和undeclare_reachable\u0026lt;class T\u0026gt; T *undeclare_reachable(T *p) noexcept；\nvoid declare_no_pointers(char* p, size_t n) noexcept;和void undeclare_no_pointers(char* p, size_t n) noexcept;\n总结 c++是一种静态类型的语言，一系列的改进使得c++的类型机制几乎完善，最后的枚举也以强类型枚举的方式进行了规范。\nc++11中仍然没有达成全面的垃圾回收，但是实现了最小化的垃圾回收支持，全面实现还需假以时日。\nchapter6 提高性能及操作硬件的能力 常量表达式 const描述的是“运行时常量性”的概念，即具有运行时数据的不可更改性。\n而对于编译时期常量的需求，曾经最为粗暴的方法是define，但是它只是简单的替换，安全性不强。因此c++11提出了新的关键字constexpr，即constant expression。当在函数表达式前加上这个声明时，编译器会在编译时期对函数进行值计算，从而将其视为编译时期常量。\n但是并不是所有的函数都有资格成为常量表达式函数，有几点要求：\n函数体只有单一的return语句 函数必须返回值 使用前必须已有定义 return返回语句表达式中不能使用非常量表达式的函数，全局数据，且必须是一个常量表达式。 常量表达式的构造函数也有使用上的约束：\n函数体必须为空 初始化列表只能由常量表达式赋值 1 2 3 4 5 struct MyType { constexpr MyType(int x) : i(x){} int i; }; constexpr MyType mt = {0}; 常量表达式同样可以用于模板函数，但是模板函数中类型的不确定性太强，因此如果类型失败会忽略constexpr。\n基于模板的编译时期运算的编程方式，成为模板元编程。\n使用constexpr进行编译时期运算的方式称为**constexpr元编程**。\n目前看来，使用了consexpr也不一定能做到编译时计算，也有可能被延迟推迟到运行时计算，此时会在性能上存在损耗。因此这个方式还是比不上模板元编程完善。\n变长模板hard variadic function变长函数，printf能接收任何长度的参数列表，但是以下写法是错误的：(原文举了个这样子的反例，没看懂)\n1 2 const char *msg = \u0026#34;hello %s\u0026#34;; printf(msg, std::string(\u0026#34;world\u0026#34;)); 变长模板类\n在一些情况下，类也需要不定长度的模板参数。最典型的就是c++11标准库中的tuple类模板，它可以接受任意多个不同类型元素的集合，也可以用make_tuple创建一个tuple模板类型。由于tuple包含的类型数量可以任意的多，客观上需要类模板接受变长参数。\nC++98中，由于没有变长模板，tuple能接受的参数数量是有限的，一般取决于标准库定义了多少个不同参数版本的tuple模板决定的。\n1 2 // Elements是模板参数包，tuple可以接受任意多个参数作为模板参数 template \u0026lt;typename... Elements\u0026gt; class tuple; 一个模板参数包在模板推导时会被认为是模板的单个参数。为了使用模板参数包，我们总是需要将其解包unpack。c++11中，通常通过一个名为包扩展pack expansion的表达式完成的。\n1 2 3 template\u0026lt;typename T1, typename T2\u0026gt; class B{}; template\u0026lt;typename... A\u0026gt; class Template: private B\u0026lt;A...\u0026gt;{}; template\u0026lt;X, Y\u0026gt; xy; 这个例子的实现只基于类模板B总是接受两个参数的前提下。\n如果想实现多个参数，也有一种通过定义递归的模板偏特化定义，使得模板参数包在实例化时能够层层展开，直到参数包中的参数逐渐耗尽或者到达某个数量的边界为止。代码如下:\n1 2 3 4 5 6 7 8 template \u0026lt;typename... Elements\u0026gt; class tuple; template\u0026lt;typename Head, typename... Tail\u0026gt; class tuple\u0026lt;Head, Tail\u0026gt; : private tuple\u0026lt;Tail...\u0026gt; { Head head; } template\u0026lt;\u0026gt; class tuple\u0026lt;\u0026gt; {}; 变长模板函数\n1 template\u0026lt;typename ... T\u0026gt; void f(T ... args); 原子类型和原子操作 6.3.1 并行编程，多线程和c++11\n常见的并行编程有多种模型，比如共享内存，多线程，消息传递等。\n多线程模型介绍：\nc++11中一个相当大的变化在于引入了多线程，本文主要讲述原子操作中的原子类型。\n6.3.2 原子操作与C++11原子类型\n通常原子操作是通过“互斥”的访问保证的。\nC++11之前，精细化实现需要嵌入内联汇编代码，粗粒度的互斥只需要借助POSIX标准的pthread库中的互斥锁即可。\n比较C++11之前的代码和C++11之后：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 // C++11之前 static long long total = 0; pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER; void* func(void *) { long long i; for (i = 0; i \u0026lt; 100000000LL; i++) { pthread_mutex_lock(\u0026amp;m); total += i; pthread_mutex_lock(\u0026amp;m); } } int main() { pthread_t thread1, thread2; if (pthread_create(\u0026amp;thread1, NULL, \u0026amp;func, NULL)) { throw; } if (pthread_create(\u0026amp;thread2, NULL, \u0026amp;func, NULL)) { throw; } pthread_join(thread1, NULL); pthread_join(thread2, NULL); cout \u0026lt;\u0026lt; total \u0026lt;\u0026lt; endl; return 0; } //c++11之后 atomic_llong total{0}; void func(int) { long long i; for (i = 0; i \u0026lt; 100000000LL; ++i) { total += i; } } int main() { thread t1(func, 0); thread t2(func, 0); t1.join(); t2.join(); cout \u0026lt;\u0026lt; total \u0026lt;\u0026lt; endl; return 0; } 明显c++11之后的代码步骤更加简单。\n如果想用到原子类型，程序员可以使用atomic类模板。\n1 2 std::stomin\u0026lt;T\u0026gt; t; // 如果想要定义原子的自定义类型，需要C++11中的新关键字_Atomic完成 当你对某个元素进行atomic的类模板时，看似正常的操作都是在执行原子操作，比如\n1 2 3 atomic\u0026lt;int\u0026gt; a; int b = a; // 相当于b=a.load() a = 1; // 相当于a.store() 在上表中，可以看到一个比较特殊的布尔型的atomic类型：atomic_flag。\natomic_flag是无锁的，即线程对它的访问不需要加锁，因此也不需要load，store等成员函数进行读写。\n因此，通过atomic_flag的成员test_and_set以及clear，我们尝试实现一个自旋锁。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 #include\u0026lt;thread\u0026gt; #include\u0026lt;atomic\u0026gt; #include\u0026lt;iostream\u0026gt; #include\u0026lt;unistd.h\u0026gt; using namespace std; std::atomic_flag lock = ATOMIC_FLAG_INIT; void f(int n) { // test_and_set向内存空间原子的写入新值返回旧值 while (lock.test_and_set(std::memory_order_acquire)) { cout \u0026lt;\u0026lt; \u0026#34;Waiting from thread\u0026#34; \u0026lt;\u0026lt; n \u0026lt;\u0026lt; endl; } cout \u0026lt;\u0026lt; \u0026#34;Thread \u0026#34; \u0026lt;\u0026lt; n \u0026lt;\u0026lt; \u0026#34;starts working\u0026#34; \u0026lt;\u0026lt; endl; } void g(int n) { cout \u0026lt;\u0026lt; \u0026#34;Thread \u0026#34; \u0026lt;\u0026lt; n \u0026lt;\u0026lt; \u0026#34; is going to start.\u0026#34; \u0026lt;\u0026lt; endl; lock.clear(); cout \u0026lt;\u0026lt; \u0026#34;Thread \u0026#34; \u0026lt;\u0026lt; n \u0026lt;\u0026lt; \u0026#34; starts working\u0026#34; \u0026lt;\u0026lt; endl; } int main() { lock.test_and_set(); thread t1(f, 1); thread t2(g, 2); t1.join(); // f中的函数一直返回true，不断打印信息，自选等待 usleep(100); t2.join(); // clear将lock的值设置为false，自旋锁终止 return 0; } 以上代码可以达到自旋锁的作用，如果再将lock封装一下，可以实现无锁编程，这样可以最大限度的挖掘并行编程的性能。\n1 2 void Lock(atomic_flag *lock){while(lock.test_and_set());} void UnLock(atomic_falg *lock){lock.clear();} 6.3.3 内存模型，顺序一致性和memory_order\n对于线程间数据同步，原子类型提供了一定的保障。不过这样做的安全性建立在顺序一致性的内存模型之上。\n对于编译器执行代码，这里我们需要理解一个概念，重排reorder。当编译器认定ab的赋值语句的执行先后顺序对输出结果有影响的话，会自动按情况重排以便提高性能，但如果ab赋值语句顺序不能改变，那么编译器就不会执行这样子的优化。\n对于C++11的原子类型的变量，它在线程中总是保持着顺序执行的特性，我们称这样的特性为“顺序一致”，即代码在线程中运行的顺序和程序员看到的代码顺序一致。\n但是顺序一致只是C++多种内存模型的一种，因为顺序一致往往意味着最抵消的同步方式。\n通常情况下内存模型只是一个硬件上的概念，表示机器指令是以何种顺序被处理器执行。\n在多线程情况下，多线程的程序总是共享代码的，强顺序意味着多线程之间的指令顺序一致；繁殖，如果线程间的内存数据被改变的数据和机器指令中声明的不一样说明是弱顺序。\n现实中的x86以及SPARC都被看做强顺序内存模型的平台。而对于像Alpha,PowerPC等弱顺序内存模型的平台，要想保证指令执行顺序，往往需要加上一条所谓的**内存栅栏(memory barrier)**指令。比如PowerPC中的sync。\n因此，对于C++11中的内存模型，要想保证顺序一致性，需要做到：\n编译器保证原子操作的指令间顺序不变。 处理器对于原子操作的汇编指令的执行顺序不变。 atomic默认会阻止编译器优化，自动加上内存栅栏，保证了数据一致性，但是增加了并行开销。\nC++11为此给出了一种可以指定内存顺序的松散的内存模型，memory_order\nmemory_order_seq_cst表示原子操作必须顺序一致，这也就是atomic的默认设置。\n通常情况下，我们会使用memory_order_release和memory_order_acquire结合使用，这样可以维持顺序一致和松散两边的共同好处，我们也称这个为release-acquire内存顺序。\nmemory_order_consume只保证原子操作发生在有关ptr的原子操作之前，相比acquire的先于发生的关系更加弱化，release-consume会建立类似于生产者消费者的同步顺序模型。\n顺序一致，松散，release-acquire和release-consume通常是最为典型的四种内存顺序。\n线程局部存储 Thread local storage线程局部存储，就是拥有线程生命期及线程可见性的变量。\n线程局部存储实际上是由单线程程序中的全局/静态变量被应用到多线程程序中被线程共享而来。\n通常情况下，线程会拥有自己的栈空间，但是堆空间，静态数据区则是共享的，这样一来，全局，静态变量在这种多线程模型下就总是在线程间共享的。\n当errno在多个线程中共享容易发生错误时，解决方案在于为每个线程指派一个TLS化的errno。\n虽然TLS变量的声明很简单，使用也很直观，不过实际上TLS的实现需要涉及编译器，连接器，加载器，甚至操作系统的组合。TLS中常讨论的问题就是TLS变量的静态/动态分配问题，即TLS变量的内存究竟是在程序一开始就被分配还是在线程开始运行时被分配。通常来说前者更容易实现。C++11中允许平台/编译器自行选择分配方式。\n快速退出quick_exit和at_quick_exit 各种表示终止的函数：\nterminate：\nC++语言中异常处理的一部分。一般而言，没有被捕捉的异常会导致terminate的调用。此外比如noexcept关键字声明的函数抛出异常会调用该函数。terminate默认情况下会调用abort函数，不过用户可以通过set_terminate函数改变默认行为。\nabort：\n该函数不会调用任何析构函数，默认情况下，向合乎POSIX标准的系统抛出一个信号：SIGABRT。如果程序员为信号设定一个信号处理程序的话，操作系统会默认释放进程的所有资源，终止进程。如若终止的进程和其它软件层之间存在交互，那么进程的意外终止会导致一些问题。\nexit：\nexit属于”正常退出”范畴的程序终止，不太可能有以上的问题。\n它会正确调用自动变量的析构函数，还会调用atexit注册的函数，这跟main函数结束时的清理工作相同。\n但是它执行的析构函数会依次将零散的内存还给操作系统，这是一件费时的工作，实际上这些堆内存在进程结束时也会由操作系统统一回收，而且在此之前exit也需要向线程发送信号等待线程结束后再析构，这个过程也可能存在问题，比如线程在等待IO或者信号顺序导致的死锁等也很容易卡死。\n因此\nc++11引入了quick_exit函数。该函数不执行析构函数只是让线程终止。解除了因为退出造成的死锁等不良状态，但是也可以免除大量不必要的析构函数的调用。\n快速性：quick_exit 更加快速，因为它不会执行任何清理操作。\n调试信息：abort 更适合调试，因为它可以产生核心转储，有助于查找程序崩溃的原因。\nexit：适合需要优雅地清理资源和执行必要的终止操作的场景。\nquick_exit：适合在特定情况下快速终止程序而不关心资源清理的场合。\n总结 常量表达式constexpr\n变长模板\n原子操作\n线程局部存储，quick_exit\nchapter7 为改变思考方式而改变 指针空值-nullptr 为了保持良好的c++编程习惯，声明变量时需要记得将合适的代码位置进行初始化，防止出现未初始化的悬挂指针。\n典型的初始化指针是将其指向一个NULL空的位置，比如0。由于大多数计算机系统不允许用户程序写地址为0的空间，如果程序无意对这个地址赋值会直接导致程序退出。在过去，字面常量0的类型既可以是整型，也可以是五类型指针，这种二义性会导致错误。\n在c++新标准中，出于对兼容性的考量，字面常量0仍然存在二义性。但同时也给出了nullptr作为“指针空值类型”的常量。nullptr_t为指针空值类型。\nnullptr是编译时期的常量，它的名字是一个编译时期的关键字，能够为编译器所识别。 (void*)()只是一个强制转换表达式，返回的也是一个void*指针类型。 不能直接对nullptr取地址，因为它是一个右值，但是如果去对它声明一个右值引用，还是可以取地址的。\n默认函数的控制 添加了关键字default和delete\nC++引入显式缺省和显式是为了增强对类默认函数的控制，让程序员更加精细的控制默认版本的函数。\n同时，显式缺省不仅可以在类的定义中修饰成员函数，也可以在类定义之外修饰成员函数。\ndelete显示删除时，应该避免explicit关键字修饰的函数，避免混乱\nplacement new 是 C++ 中的一种特殊的 new 操作符，允许你在指定的内存地址上构造对象，而不是在堆上分配内存。它主要用于在已分配的内存区域中创建对象，常见于性能优化和自定义内存管理。\n自定义内存管理：可以在预先分配的内存区域中构造对象，适用于内存池或其他自定义内存分配方案。 避免重复分配：使用 placement new 可以避免频繁的内存分配和释放，提高性能。 调用构造函数：在指定的内存位置上调用构造函数，可以进行对象的初始化。 需要手动析构：使用 placement new 创建的对象不会在作用域结束时自动释放内存，需要手动调用析构函数来清理对象 同时，显式删除析构函数可用于构建单例模式。\nlambda函数 以lambda概念为基础的**“函数式编程function Programming”是与命令式编程Imperative Programing**，面向对象编程Objective-oriented Programming等并列的一种编程范型Programming Paradigm。\n通常情况的lambda函数语法定义：\n[capture] (parameters) mutable -\u0026gt; return-type{statement}\n\\[capture\\]：它可以捕捉列表访问一些上下文中的数据，同时会描述上下文中的哪些数据可以被lambda使用，以及使用方式。 依照现行的c++标准，在块作用域以外的lambda函数捕捉列表必须为空，这样的话，跟普通函数的差别不大，而在块作用域中的lambda函数只能捕捉父作用域中的自动变量，捕捉任何非此作用域或者非自动变量（比如静态变量），都会导致编译器报错。 lambda函数和仿函数有相同的内涵，都可以捕捉一些变量作为初始状态，并接受参数进行运算。\nlambda会捕捉作用域中的变量，仿函数会捕捉类中的私有函数，在参数传递上两者保持一致。\n其实，仿函数是编译器实现lambda的一种方式，现阶段，编译器通常把lambda转化成一个仿函数对象，c++11中lambda可以视为仿函数的等价，或者称lambda是仿函数的“语法糖“。\n仿函数的私有变量和构造函数等价于lambda函数的捕获参数，捕获一个外界参数通过构造函数变成自己的私有变量。 仿函数的operator()类似于lambda的函数体，都是传入一个参数，然后对这个传入参数和捕获参数进行处理返回。 当函数的重用性不高，但是又大量使用内部参数时，lambda函数能够很好的实现，并且在父函数接结束之后，所用空间会被自动释放。lambda本身就是匿名的函数。对性能没有很大的提升，但是具有很好的代码可读性。\n另外，lambda可以实现，需要自己初始化状态的常量。\n对于值传递的捕捉列表，传递的值在函数定义时已经决定了 对于引用传递的捕捉列表，传递的值等于lambda函数调用时的值。 c++11的标准中：lambda的类型被定义为闭包的类，每个lambda表达式则会产生一个闭包类型的临时对象（右值）。\n**要点一：**lambda和仿函数之间的关系，但是lambda并不是仿函数的完全替代者。是由于lambda的捕捉列表仅能捕捉父作用域的自动变量。仿函数天生具有跨作用域共享的特征，可以定义以后在不同的作用域范围内取得初始值。\n**要点二：**lambda函数倾向于就地书写，就地使用，局部封装，局部共享。\n**要点三：**捕捉列表的值传递，通常容易造成过大开销。引用传递的话，函数和作用域对对象会相互影响，如果代码存在异步，也有可能产生问题。\n总结 nullptr指针初始化，排除字面常量0的二义性\n=default/=deleted显式缺省，保证自定义类型符合POD的定义。\nlambda函数 简化仿函数的使用；作为局部函数简化复杂代码，程序员可以轻松在函数内重用代码，无需分心涉及接口\nchapter8 融入实际应用 对齐支持 一般情况下，由于对齐而造成的内存留空为填充数据padding data。\n对齐数据在读写上会有性能的优势，比如频繁使用的数据和处理器的高速缓存器大小对齐，就有可能提高缓存性能。数据对齐对c++/c性能的提升有很大作用，但是语言一般会掩盖对齐的特性，只能通过大小sizeof和offset对对齐的方式进行判断。\nC++11中为了支持对齐，引入了两个关键字alignof操作符，alighment-specifier/alignas对齐描述符。\nalignof：表示一个定义完整的自定义类型或者内置类型或者变量，返回值是一个std::size_t类型的整型常量。如同sizeof操作符一样，alignof获得的也是一个和平台相关的值 alignas：对其描述符。它既可以接受常量表达式，也可以接受类型作为参数。 对齐值越大，对齐要求越高；对齐值越小，对齐要求越低。\n**C++11标准建议：**用户在声明同一个变量是时使用同样的对齐方式以免发生意外。\n另外\nC++11在STL库中，内建了std::align函数，动态的根据指定的对齐方式调整数据块的位置。\n1 void* align(std::size_t alignment, std::size_t size, void*\u0026amp; ptr, std::size_t\u0026amp; space); 该函数对ptr指向大小为space的内存中进行对齐方式的调整，将ptr开始的size大小的数据调整为按alignment对齐。\n通用属性 使用方式为： [[ attribute-list ]]\n好少见，感觉就没见过它的用法。。\nUnicode支持 Unicode 是一个字符编码标准，用于为世界上几乎所有字符分配唯一编号。UTF-8、UTF-16 和 UTF-32 是 Unicode 的不同编码形式。\nUTF-8：变长编码，每个字符占用 1 到 4 个字节，向后兼容 ASCII，广泛用于网页和文件。 UTF-16：通常使用 2 或 4 个字节表示字符，适合表示大部分常用字符，尤其是在东亚语言中。 UTF-32：固定长度编码，每个字符占用 4 个字节，简单直接但内存开销大，主要用于需要直接处理字符的场景。 原生字符串字面量 raw string literal\n1 2 3 regex pattern1 {\u0026#34;\\\\w{2}\\\\s*\\\\d{5}(-\\\\d{4})?\u0026#34;}; // 普通字符串字面量 regex pattern2 {R\u0026#34;(\\w{2}\\s*\\d{5}(-\\d{4})?)\u0026#34;}; // 原始字符串字面量 总结 对齐方式：提供给用户更底层的手段\n通用属性：关键字的包装器\n增强了对Unicode的支持\n支持了原生字符串字面量\n附录：c++常见面试问题 1.静态变量和全局变量局部变量的区别，内存如何分布。\n2.指针和引用\n3.c++内存分区\n4.static和const关键字作用\n5.常量指针和指针常量\n6.智能指针的实现原理\n7.new和malloc的区别\n8.delete和free\n9.堆和栈\n10.内存泄露是什么，如何检测和防止\n11.野指针是什么，如何避免\n12.c++面向对象三大特性\n13.c++的重载和重写，区别和实现方式\n14.c++多态的实现\n15.虚函数和纯虚函数区别\n16.虚函数如何实现\n17.虚函数表是什么\n18.构造函数和析构函数是什么，它们可以是虚函数吗\n19.c++有几种构造函数，分别什么作用\n20.深拷贝和浅拷贝\n21.stl容器\n22.vector和list区别\n23.vector底层原理和扩容过程\n24.push_back()和emplace_back()\n25.map deque list实现原理\n26.map unordered_map区别和实现机制\n27.c++11新特性\n28.移动语义的作用和原理\n29.左值引用和右值引用区别\n30.lambda函数\n31.c++实现单例模式\n32.菱形继承是什么\n33.c++中的多线程同步机制\n34.如何在c++中创建和管理线程\n参考资料 Thriving in a Crowded and Changing World: C++ 2006–2020 在纷繁多变的世界里茁壮成长：C++ 2006–2020（中译版） 书籍：《深入理解C++11：C++11新特性解析与应用》 腾讯技术工程对于c++17的看法 - 知乎 您好，我是向菲，本科为海南大学，目前是作为一名交换生在天津大学读大三。个人常用的编程语言为C++，go稍微会一点。\n曾在百度做过三个月的测试开发实习生，主要负责测试和开发一个风险定位工具。\n本人开源项目为参考levelDB实现的一个键值存储引擎，支持快速写入和高效查询，具备良好的稳定性和性能。\n","date":"2024-11-08T00:00:00Z","permalink":"https://sutdown.github.io/p/c-%E6%BC%94%E5%8C%96%E8%AE%BA%E6%96%87%E7%8E%B0%E4%BB%A3c-%E7%99%BD%E7%9A%AE%E4%B9%A6%E4%B9%A6%E7%B1%8D%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3c-11%E7%89%B9%E6%80%A7/","title":"C++演化(论文现代C++白皮书，书籍深入理解C++11特性)"},{"content":"LOG log结构和设计 Log有Record和一些为了对其而填充的gap组成的文件。levelDB在读取Log文件时，为了减少IO次数，每次读取都会读入一个32KB大小的块。因此在写入log文件时，leveldb也将数据按照32KB对齐。\n每个块中会很有多段，段的记录形式有三种：FirstType Middle Fragment Last Fragment。\n而对于段结构：\n当写入时，和32KB对其的剩余空间不足以放入7字节的header，会将剩余空间填入0X00，要继续写入。\n一条日志记录的内容包含：Header和Data。其中header中有（1）当前db的sequence number（2）本次日志记录中所包含的put/del操作的个数。\n源代码分析 log_writer 日志写入流程。leveldb内部实现了一个journal的writer。首先调用Next函数获取一个singlewriter，这个SingleWriter的作用就是写入一条journal记录。\nclass logwriter\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 /*提供高效的记录写入机制*/ class Writer { public: explicit Writer(WritableFile* dest); Writer(WritableFile* dest, uint64_t dest_length); Writer(const Writer\u0026amp;) = delete; Writer\u0026amp; operator=(const Writer\u0026amp;) = delete; ~Writer(); Status AddRecord(const Slice\u0026amp; slice); private: // 进行执行数据的物理写入，包括计算和处理记录类型。 Status EmitPhysicalRecord(RecordType type, const char* ptr, size_t length); WritableFile* dest_; // 目标文件的指针，数据写入这个文件 int block_offset_; // Current offset in block块中的偏移量 // crc32c values for all supported record types. These are // pre-computed to reduce the overhead of computing the crc of the // record type stored in the header.预计算的CRC32值 uint32_t type_crc_[kMaxRecordType + 1]; }; 对于其中的函数：可以得出头部信息buf的格式：\n1 2 3 4 5 +-------------+-------------+-----------+-----------+ |CRC32（4字节）| 长度（2字节）|类型（1字节）|保留（1字节）| +-------------+-------------+-----------+-----------+ \u0026gt; buf 数组的设计确保了记录的结构在物理存储中是固定的，使得在读取记录时能够快速解析出每个字段的内容。 \u0026gt; 这样做的好处是提高了数据的可解析性和读取效率，同时通过 CRC 校验增强了数据的可靠性。 对于logwrite.cc里面的文件逻辑：\nlog_reader 日志的读取也较为简单，为了避免频繁的IO读取，每次从文件中读取数据时，按block（32KiB）进行块读取。每次读取一条日志记录，reader调用Next函数返回一个singleReader。singleReader每次调用Read函数就返回一个chunk的数据。每次读取一个chunk，都会检查这批数据的校验码、数据类型、数据长度等信息是否正确，若不正确，且用户要求严格的正确性，则返回错误，否则丢弃整个chunk的数据。循环调用singleReader的read函数，直至读取到一个类型为Last的chunk，表示整条日志记录都读取完毕，返回。\nclass log_reader\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 class Reader { public: ... /* 类功能 * 读取日志记录 * 支持检查和校验 * 处理数据损坏 */ private: enum { kEof = kMaxRecordType + 1, kBadRecord = kMaxRecordType + 2 }; bool SkipToInitialBlock(); // 跳过所有initial_offset之前的块 unsigned int ReadPhysicalRecord(Slice* result); // 读取物理记录 // Reports dropped bytes to the reporter.报告数据损坏和丢失字节数量 // buffer_ must be updated to remove the dropped bytes prior to invocation. void ReportCorruption(uint64_t bytes, const char* reason); void ReportDrop(uint64_t bytes, const Status\u0026amp; reason); SequentialFile* const file_; // 指向要读取的文件指针 Reporter* const reporter_; // 报告错误和数据损坏的接口 bool const checksum_; // 指示是否读取时验证校验和 char* const backing_store_; // 存储读取记录的临时缓冲区 Slice buffer_; // 当前读取记录的切片 bool eof_; // 上一次读取是否到达文件末尾 // Offset of the last record returned by ReadRecord. uint64_t last_record_offset_; // 保存上一次返回记录的物理偏移量 // Offset of the first location past the end of buffer_. uint64_t end_of_buffer_offset_; // 缓冲区结尾的偏移量 // Offset at which to start looking for the first record to return uint64_t const initial_offset_; // 指定开始查找记录的初始偏移量 bool resyncing_; // 当前是否正在重新同步，处理随机读取 }; 在log_writer.cc文件中\nReadRecord: 读取下一个记录到 record 中，返回 true 表示成功，false 表示到达文件末尾。 处理记录类型，如完整记录、首部记录、中间记录和尾部记录，并相应地合并数据或报告损坏。 ReadPhysicalRecord: 从缓冲区读取物理记录，解析头部，验证长度和校验和。 如果记录有效，返回其类型；如果无效，则返回 kBadRecord。 ReportCorruption 和 ReportDrop: 报告数据损坏或丢失的字节数，通过 reporter_ 接口进行通知。 在 ReadRecord 方法中，通过 ReadPhysicalRecord 读取记录后，根据不同类型进行处理：\nkFullType：表示完整记录，直接返回。 kFirstType：表示开始的部分，存储到 scratch 中。 kMiddleType：表示中间部分，追加到 scratch。 kLastType：表示记录的最后部分，合并并返回。 kEof 和 kBadRecord：处理文件末尾和错误记录。 Read\u0026amp;Write 参考：levelDB - handbook\n读写操作过程 写操作 leveldb的一次写入分为两部分：\n将写操作写入日志； 将写操作应用到内存数据库中； PutAndDelete leveldb对外提供的写入接口有：（1）Put（2）Delete两种。这两种本质对应同一种操作，Delete操作同样会被转换成一个value为空的Put操作。\n除此以外，leveldb还提供了一个批量处理的工具Batch，用户可以依据Batch来完成批量的数据库更新操作，且这些操作是原子性的。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 // Set the database entry for \u0026#34;key\u0026#34; to \u0026#34;value\u0026#34;. Returns OK on success, // and a non-OK status on error. // Note: consider setting options.sync = true. virtual Status Put(const WriteOptions\u0026amp; options, const Slice\u0026amp; key, const Slice\u0026amp; value) = 0; // Remove the database entry (if any) for \u0026#34;key\u0026#34;. Returns OK on // success, and a non-OK status on error. It is not an error if \u0026#34;key\u0026#34; // did not exist in the database. // Note: consider setting options.sync = true. virtual Status Delete(const WriteOptions\u0026amp; options, const Slice\u0026amp; key) = 0; // Apply the specified updates to the database. // Returns OK on success, non-OK on failure. // Note: consider setting options.sync = true. 执行批量操作 virtual Status Write(const WriteOptions\u0026amp; options, WriteBatch* updates) = 0; // Convenience methods Status DBImpl::Put(const WriteOptions\u0026amp; o, const Slice\u0026amp; key, const Slice\u0026amp; val) { return DB::Put(o, key, val); } Status DBImpl::Delete(const WriteOptions\u0026amp; options, const Slice\u0026amp; key) { return DB::Delete(options, key); } batch结构 无论是Put/Del操作，还是批量操作，底层都会为这些操作创建一个batch实例作为一个数据库操作的最小执行单元。因此首先介绍一下batch的组织结构。\n在batch中，每一条数据项都按照上图格式进行编码。每条数据项编码后的第一位是这条数据项的类型（更新还是删除），之后是数据项key的长度，数据项key的内容；若该数据项不是删除操作，则再加上value的长度，value的内容。\nbatch中会维护一个size值，用于表示其中包含的数据量的大小。该size值为所有数据项key与value长度的累加，以及每条数据项额外的8个字节。这8个字节用于存储一条数据项额外的一些信息。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 /*批量写入操作*/ class LEVELDB_EXPORT WriteBatch { public: ... private: /*如果将所有逻辑放在一个类中， 任何内部修改都有可能影响公共接口。 将内部逻辑分离，可以减少对外部接口的影响。*/ friend class WriteBatchInternal; /*存储了该批次操作的具体格式和内容*/ std::string rep_; // See comment in write_batch.cc for the format of rep_ }; class WriteBatchInternal { public: static int Count(const WriteBatch* batch); // 计数 static void SetCount(WriteBatch* batch, int n); static SequenceNumber Sequence(const WriteBatch* batch); // 序列号 static void SetSequence(WriteBatch* batch, SequenceNumber seq); static Slice Contents(const WriteBatch* batch) { // 内容 return Slice(batch-\u0026gt;rep_); } static size_t ByteSize(const WriteBatch* batch) { // 字节大小 return batch-\u0026gt;rep_.size(); } static void SetContents(WriteBatch* batch, const Slice\u0026amp; contents); static Status InsertInto(const WriteBatch* batch, MemTable* memtable); // 插入 static void Append(WriteBatch* dst, const WriteBatch* src); }; writeBatch是用户直接使用的接口，而writeBatchInternal是支撑其实现的工具类，提供底层的操作方法。\nWriteBatch中有比如put，delete，clear，ApproximateSize等类似的公共接口，用户可以用其进行执行批量操作。WriteBatchInternal中存在一些关于writeBatch相关的条目数量，内部内容，将其插入内存表，实现计数和内存管理等功能的类。\nkey值编码 当数据项从batch中写入到内存数据库中时，需要将一个key值的转换，即在leveldb内部，所有数据项的key是经过特殊编码的，这种格式称为internalKey。\ninternalkey在用户key的基础上，尾部追加了8个字节，用于存储（1）该操作对应的sequence number（2）该操作的类型。\n其中，每一个操作都会被赋予一个sequence number。该计时器是在leveldb内部维护，每进行一次操作就做一个累加。由于在leveldb中，一次更新或者一次删除，采用的是append的方式，并非直接更新原数据。因此对应同样一个key，会有多个版本的数据记录，而最大的sequence number对应的数据记录就是最新的。此外，leveldb的快照（snapshot）也是基于这个sequence number实现的，即每一个sequence number代表着数据库的一个版本。\n合并写 leveldb中，在面对并发写入时，做了一个处理的优化。在同一个时刻，只允许一个写入操作将内容写入到日志文件以及内存数据库中。为了在写入进程较多的情况下，减少日志文件的小写入，增加整体的写入性能，leveldb将一些“小写入”合并成一个“大写入”。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 /*批量写*/ Status DBImpl::Write(const WriteOptions\u0026amp; options, WriteBatch* updates) { Writer w(\u0026amp;mutex_); w.batch = updates; // 写入批次 w.sync = options.sync; // 同步标志 w.done = false; // 完成状态 MutexLock l(\u0026amp;mutex_); writers_.push_back(\u0026amp;w); /* 当前请求未完成并且不是队列的第一个时只能淘汰 */ while (!w.done \u0026amp;\u0026amp; \u0026amp;w != writers_.front()) { w.cv.Wait(); // 等待信号量解锁 } if (w.done) { return w.status; } // May temporarily unlock and wait.、 /*MakeRoomForWrite方法会根据当前MemTable的使用率来选择是否触发Minor Compaction （如果当前updates为空，则为Manual Compaction用来强制触发Minor Compaction的操作）。*/ Status status = MakeRoomForWrite(updates == nullptr); // 获取空间 uint64_t last_sequence = versions_-\u0026gt;LastSequence(); // 获取当前序列号 Writer* last_writer = \u0026amp;w; if (status.ok() \u0026amp;\u0026amp; updates != nullptr) { // nullptr batch is for compactions WriteBatch* write_batch = BuildBatchGroup(\u0026amp;last_writer); WriteBatchInternal::SetSequence(write_batch, last_sequence + 1); last_sequence += WriteBatchInternal::Count(write_batch); // Add to log and apply to memtable. We can release the lock // during this phase since \u0026amp;w is currently responsible for logging // and protects against concurrent loggers and concurrent writes // into mem_. { // 处理队列 mutex_.Unlock(); /*释放锁，write线程根据传入的writeoptions中的sync判断是否需要等待WAL同步道稳定存储*/ status = log_-\u0026gt;AddRecord(WriteBatchInternal::Contents(write_batch)); bool sync_error = false; if (status.ok() \u0026amp;\u0026amp; options.sync) { status = logfile_-\u0026gt;Sync(); // 保持同步 if (!status.ok()) { sync_error = true; } } if (status.ok()) { // 将其插入到mem中 status = WriteBatchInternal::InsertInto(write_batch, mem_); } mutex_.Lock(); if (sync_error) { // The state of the log file is indeterminate: the log record we // just added may or may not show up when the DB is re-opened. // So we force the DB into a mode where all future writes fail. RecordBackgroundError(status); } } if (write_batch == tmp_batch_) tmp_batch_-\u0026gt;Clear(); versions_-\u0026gt;SetLastSequence(last_sequence); } // 完成后置done字段为true while (true) { Writer* ready = writers_.front(); writers_.pop_front(); if (ready != \u0026amp;w) { ready-\u0026gt;status = status; ready-\u0026gt;done = true; ready-\u0026gt;cv.Signal(); } if (ready == last_writer) break; } 原子性 leveldb的任意一个写操作（无论包含了多少次写），其原子性都是由日志文件实现的。一个写操作中所有的内容会以一个日志中的一条记录，作为最小单位写入。\n考虑以下两种异常情况：\n写日志未开始，或写日志完成一半，进程异常退出； 写日志完成，进程异常退出； 读操作 leveldb提供给用户两种进行读取数据的接口：\n直接通过Get接口读取数据； 首先创建一个snapshot，基于该snapshot调用Get接口读取数据； 由于两种方式本质都是基于快照进行读取的，因此在介绍读操作之前，首先介绍快照。\n快照 快照代表着数据库某一个时刻的状态，在leveldb中，作者巧妙地用一个整型数来代表一个数据库状态。\n在leveldb中，用户对同一个key的若干次修改（包括删除）是以维护多条数据项的方式进行存储的（直至进行compaction时才会合并成同一条记录），每条数据项都会被赋予一个序列号，代表这条数据项的新旧状态。一条数据项的序列号越大，表示其中代表的内容为最新值。\n1 2 3 4 5 6 7 8 9 10 const Snapshot* DBImpl::GetSnapshot() { MutexLock l(\u0026amp;mutex_); return snapshots_.New(versions_-\u0026gt;LastSequence()); } void DBImpl::ReleaseSnapshot(const Snapshot* snapshot) { MutexLock l(\u0026amp;mutex_); snapshots_.Delete(static_cast\u0026lt;const SnapshotImpl*\u0026gt;(snapshot)); } 读取 leveldb读取分为三步：\n在memory db中查找指定的key，若搜索到符合条件的数据项，结束查找；\n在冻结的memory db中查找指定的key，若搜索到符合条件的数据项，结束查找；\n在memory和Immutable MemTable上的查找都是在SkipList上的查找，这里不再赘述。\n按低层至高层的顺序在level i层的sstable文件中查找指定的key，若搜索到符合条件的数据项，结束查找，否则返回Not Found错误，表示数据库中不存在指定的数据；\n在sst中的查找是在current version的Version::Get上实现的。\nDBImpl::Get这个函数主要用于从数据库中获得指定键的值。\n在它的函数中，首先会寻找快照或者versions_的序列号，这是为了实现MVCC中的一致性存储。\n然后先后在mem当前内存表，imm不可变内存表，current的sstable文件中查找。\n这里面会用到cache。\n查找数据块：当 Get 操作试图从 SSTable 中读取数据时，如果目标数据块已经被缓存，LevelDB 会直接从缓存中读取，而不是访问磁盘。这可以显著提高读取性能。 序列号查找：在使用快照（snapshot）或版本号时，LevelDB 会使用缓存来加速对特定版本数据的查找，避免多次读取相同数据块。 合并读取：在合并操作（如从多个 SSTable 读取）时，LevelDB 也会优先检查缓存，以减少磁盘 I/O。 当进行多次数据读取时，读取操作会影响一些统计信息的更新，比如数据的访问频率，那么LRU cache中的数据块的位置会发生改变，也会影响到层级中的数据分布；还有删除操作显示减少数据块；频繁插入也会导致memtable增多，因此会调用MaybeScheduleCompaction();决定是否压缩。\n","date":"2024-11-04T00:00:00Z","permalink":"https://sutdown.github.io/p/leveldb%E6%BA%90%E7%A0%81%E9%98%85%E8%AF%BB6-wirteread-and-log/","title":"leveldb源码阅读6 wirte\u0026read and log"},{"content":"顾名思义，单例模式就是只提供一个类的实例。\n简单来说的需要思考到的要点有：\n全局只存在一个实例，不能拷贝和赋值 有实例时直接返回该实例，没有实例时创建实例 创建实例考虑线程安全 双重锁校验\n两次锁的出现其实都是为了确保线程安全，\n只有确保实例为空时，我们才需要加锁创立新的实例，\n加完第一次锁之后，可能其它线程已经创建实例，此时需要再次进行判断，判断为空后就可以创建实例。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 class Singleton { private: static volatile Singleton* instance; static std::mutex mtx; Singleton() {} public: Singleton(Singleton\u0026amp;) = delete; Singleton\u0026amp; operator=(const Singleton\u0026amp;) = delete; static Singleton* getInstance() { if (instance == nullptr) { std::lock_guard\u0026lt;std::mutex\u0026gt; lock(mtx); if (instance == nullptr) { instance = new Singleton(); } } return const_cast\u0026lt;Singleton*\u0026gt;(instance); } }; volatile Singleton* Singleton::instance = nullptr; std::mutex Singleton::mtx; 静态局部变量\n由于static只会在同一个地方分配内存，并且即使在局部函数中创建它的作用域也是在全局，利用这种特性，同样可以实现单例。\n并且巧妙的利用了c++11中的一个特性：magic static\n变量初始化时进入声明，并发线程会阻塞等待初始化结束。\n因此\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 class Singleton { private: Singleton() {}; public: ~Singleton() {}; Singleton(Singleton\u0026amp;) = delete; Singleton\u0026amp; operator=(const Singleton\u0026amp;) = delete; static Singleton\u0026amp; get_instance() { static Singleton instance; return instance; } }; int main() { Singleton\u0026amp; instance_1 = Singleton::get_instance(); Singleton\u0026amp; instance_2 = Singleton::get_instance(); return 0; } 注意使用时也需要采用\u0026amp;的方式。\n快速排序code: 1 2 3 4 5 6 7 8 9 10 11 12 13 void quick_sort(int q[], int l, int r){ if(l\u0026gt;=r) return; int i=l-1, j=r+1; x = q[l+r \u0026gt;\u0026gt; 1]; while(i\u0026lt;j) { do i++; while(q[i]\u0026lt;x); do j--; while(q[j]\u0026gt;x); if(i\u0026lt;j) swap(q[i], q[j]); } quick(q, l, j); quick(q, j+1, r); } 面试题8：旋转数组的最小数字 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 int Min(int numbers[], int length){ assert(numbers == NULL || length \u0026lt;= 0); int i = 0, j= length - 1; int index = i; while(numbers[i] \u0026gt;= numbers[j]){ if(j == i+1){ index = j; break; } index = i + j \u0026gt;\u0026gt; 1; // 数据大量重复，无法移动指针 if(numbers[i] == numbers[j] \u0026amp;\u0026amp; numbers[j] == numbers[index]){ return minorder(numbers, i, j); } if(numbers[index] \u0026gt;= numbers[i]){ i = index; }else if(numbers[index] \u0026lt;= numbers[j]){ j = index; } } return numbers[index]; } 参考资料： 1.剑指offer\n2.c++单例模式总结剖析\n","date":"2024-11-03T00:00:00Z","permalink":"https://sutdown.github.io/p/%E5%8D%95%E4%BE%8B%E6%A8%A1%E5%BC%8F/","title":"单例模式"},{"content":"赋值运算符函数\n1 2 3 4 5 6 7 8 9 10 11 class CMyString { public: CMyString(char* pData = NULL); CMyString(const CMyString\u0026amp; str); ~CMyString(void); CMyString\u0026amp; operator =(const CMyString \u0026amp;str); private: char* m_pData; }; 赋值运算符函数的目的在于将另一个对象拷贝到当前对象中，值得注意的点包括\n深拷贝和浅拷贝 内存分配 基于深拷贝和浅拷贝：\n如果只是简单的将拷贝对象的值赋值到被拷贝对象中，那么两个对象的指针指向同一块内存，内存释放时容易发生多次释放而导致出问题，对此选择的方式是深拷贝，也就是将当前对象原来的值释放，重新分配一块新的内存。\n同时需要注意，如果两个实例是同一个的话，直接返回实例就好，否则释放之后再进行拷贝会因为找不到实例的值而出错。\n1 2 3 4 5 6 7 8 9 10 11 12 // 深拷贝 CMyString\u0026amp; CMyString::operator =(const CMyString \u0026amp;str) { if (this == \u0026amp;str) return *this; delete []m_pData; // 释放当前内存时注意判断两个实例是不是同一个 m_pData = NULL; m_pData = new char[strlen(str.m_pData) + 1]; strcpy(m_pData, str.m_pData); return *this; } 关于内存分配：\n上面函数欠缺部分在于异常处理，如果new失败会造成不好的后果，因此可以考虑另一种方案，创建一个临时实例，和当前变量交换即可。临时实例创建在栈中，局部函数结束时会自动释放。\n1 2 3 4 5 6 7 if (*this != \u0026amp;str) { CMyString strTemp(str); char *pTemp = strTemp.m_pData; strTemp.m_pData = m_pData; m_pData = pTemp; } return *this; ","date":"2024-11-03T00:00:00Z","permalink":"https://sutdown.github.io/p/%E8%B5%8B%E5%80%BC%E8%BF%90%E7%AE%97%E7%AC%A6%E5%87%BD%E6%95%B0/","title":"赋值运算符函数"},{"content":"compaction 作用：leveldb - handbook 数据持久化。一次minor compaction的产出是一个0层的sstable文件，其中包含了所有的内存数据。但是若干个0层文件中是可能存在数据overlap的。\n提高读写效率。Major Compaction的过程，将0层中的文件合并为若干个没有数据重叠的1层文件。对于没有数据重叠的文件，一次查找过程就可以进行优化，最多只需要一个文件的遍历即可完成。因此，leveldb设计compaction的目的之一就是为了提高读取的效率。\n平衡读写差异。有了minor compaction和major compaction，所有的数据在后台都会被规定的次序进行整合。但是一次major compaction的过程其本质是一个多路归并的过程，既有大量的磁盘读开销，也有大量的磁盘写开销，显然这是一个严重的性能瓶颈。\n但是当用户写入的速度始终大于major compaction的速度时，就会导致0层的文件数量还是不断上升，用户的读取效率持续下降。所以leveldb中规定：\n当0层文件数量超过SlowdownTrigger时，写入的速度主要减慢； 当0层文件数量超过PauseTrigger时，写入暂停，直至Major Compaction完成； **整理数据。**leveldb的每一条数据项都有一个版本信息，标识着这条数据的新旧程度。这也就意味着同样一个key，在leveldb中可能存在着多条数据项，且每个数据项包含了不同版本的内容。为了尽量减少数据集所占用的磁盘空间大小，leveldb在major compaction的过程中，对不同版本的数据项进行合并。\nCompaction过程 minor compaction介绍 major compaction介绍 Size Compaction：根据每层总SSTable大小触发（level-0根据SSTable数）的Major Compaction。 Seek Compaction：根据SSTable的seek miss触发的Major Compaction。 Manual Compaction：LevelDB使用者通过接口void CompactRange(const Slice* begin, const Slice* end)手动触发。 错峰合并 对于错峰合并网上是这么讲解的，我暂时还没看懂：\n采样检测 每个sstable文件的元数据中，还有一个额外的字段seekLeft，默认为文件的大小除以16KB。leveldb在正常的数据访问时，会顺带进行采样探测。正常的数据访问包括（1）用户直接调用Get接口（2）用户使用迭代器进行访问。\n采样的规则：\n记录本次访问的第一个sstable文件。 若在该文件中访问命中，则不做任何处理；若在该文件中访问不命中，则对 该文件的seekLeft标志做减一操作。 某一个文件的seekLeft标志减少到0时，触发错峰合并。 故以上三种机制可以保障每次进行compaction的时候，总体开销不会呈现上升趋势。\n过程 寻找合适的输入文件；\n对于level 0层文件数过多引发的合并场景或由于level i层文件总量过大的合并场景，采用轮转的方法选择起始输入文件，记录了上一次该层合并的文件的最大key，下一次则选择在此key之后的首个文件。\n对于错峰合并，起始输入文件则为该查询次数过多的文件。\n根据key重叠情况扩大输入文件集合；\n多路合并；\n多路合并的过程比较简单，即将level i层的文件，与level i+1层的文件中的数据项，按序整理之后，输出到level i+1层的若干个新文件中，即合并完成。\n注意在整理的过程中，需要将冗余的数据进行清理，即同一条数据的多个版本信息，只保留最新的那一份。但是要注意，某些仍然在使用的旧版本的数据，在此时不能立刻删除，而得等到用户使用结束，释放句柄后，根据引用计数来进行清除。\n积分计算；\n每一次compaction都会消除若干source层的旧文件，新增source+1层的新文件，因此触发进行合并的条件状态可能也发生了变化。故在leveldb中，使用了计分牌来维护每一层文件的文件个数及数据总量信息，来挑选出下一个需要进行合并的层数。\n计分的规则很简单：\n对于0层文件，该层的分数为文件总数／4； 对于非0层文件，该层的分数为文件数据总量／数据总量上限； 将得分最高的层数记录，若该得分超过1，则为下一次进行合并的层数；\n由于leveldb内部进行compaction时有trivial move优化，且根据内部的文件格式组织，用户在使用leveldb时，可以尽量将大批量需要写入的数据进行预排序，利用空间局部性，尽量减少多路合并的IO开销。\nCompact源码分析 后台线程 为了防止Compaction执行时阻塞LevelDB的正常读写，LevelDB的所有Compaction都通过一个后台线程执行。接口定义在了include/leveldb/env.h中，在不同环境中的实现分别位于util/env_windows.cc与env_posix.cc中。\n在env.h的schedule接口中，首先检测后台线程是否创建，如果没有创建创建后台线程。接下来会将任务放入后台线程的任务队列中，并通过信号量唤醒后台线程执行。创建后台线程与操作任务队列都需要通过锁来保护，因此该方法全程加锁。\n后台线程会循环获取任务丢列中的任务，为了避免线程空转，在队列为空时通过信号量等待唤醒。如果队列中有任务，则获取该任务并将任务出队，然后执行任务。后台线程中操作队列的部分需要通过锁来保护，而执行任务时没有上锁，可以并行执行（但是LevelDB只使用了1个后台线程，因此Compaction仍是串行而不是并行的）。\n优先级 Minor Compaction \u0026gt; Manual Compaction \u0026gt; Size Compaction \u0026gt; Seek Compaction\n这里应当涉及几个问题：\n1.compaction什么时候会被触发，对应代码是什么。2.触发时的先后顺序什么样子，对应什么代码。\ndb_impl.cc 在 LevelDB 的 db_impl.cc 文件中，主要负责处理对数据库的主要操作.\n打开和关闭数据库：管理数据库的生命周期。 读写操作：提供对键值对的插入、查找和删除功能。 压缩和合并：实现后台压缩和合并的逻辑，优化存储。 状态管理：维护数据库的状态，包括错误状态和后台任务的调度。 事务处理：支持事务相关的操作。 db_impl.cc中的void MaybeScheduleCompaction() EXCLUSIVE_LOCKS_REQUIRED(mutex_);会判断是否执行Compaction：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 void DBImpl::MaybeScheduleCompaction() { mutex_.AssertHeld(); // 检查是否有后台压缩任务被调度，如果有就不做操作 if (background_compaction_scheduled_) { // Already scheduled } else if (shutting_down_.load(std::memory_order_acquire)) { // DB is being deleted; no more background compactions // DB正在关闭时，不能调度任何压缩任务 } else if (!bg_error_.ok()) { // Already got an error; no more changes // 有错误时，不能进行任何压缩任务 } else if (imm_ == nullptr \u0026amp;\u0026amp; manual_compaction_ == nullptr \u0026amp;\u0026amp; !versions_-\u0026gt;NeedsCompaction()) { // No work to be done // 没有任何待处理的压缩，则不调度压缩 } else { // 当避免了各种不必要的操作和潜在问题时，可以调度压缩任务 background_compaction_scheduled_ = true; env_-\u0026gt;Schedule(\u0026amp;DBImpl::BGWork, this); } } 上面的最后一个if判断待处理的压缩时，imm_==nullptr对应的是Minor Compaction；然后就是munual compaction（手动触发），以及最后的version所需要的Size Compaction（某层的大小超过限制）或者Seek Compaction（多次寻找miss）\n在versions中的对于后面两种情况的触发为：\n1 2 3 4 5 6 7 // Returns true iff some level needs a compaction. bool NeedsCompaction() const { Version* v = current_; // compaction_score_字段用来计算是否需要触发Size Compaction， // file_to_compact_用来计算是否需要触发Seek Compaction return (v-\u0026gt;compaction_score_ \u0026gt;= 1) || (v-\u0026gt;file_to_compact_ != nullptr); } Minor Compaction范围 在LSTtree的基本概念中，Minor Compaction只需要将Immutable转储为SSTable并将其推送到level0即可。\n而LevelDB对这一步骤进行优化，其会将MinorCompaction生成的SSTable推至更高的层级。\n为什么要推到更高的层级，这个步骤不是major compaction要做的吗\nmajor compaction需要对level中的多个文件进行复杂的合并和去重，涉及复杂的键比较和重复数据的处理，同时需要对Manifest进行频繁的更新去记录新的文件和合并后的状态，也有复杂的IO操作，开销较大。\nminor compaction只需要将memtable的数据进行写入即可，memtable也通常较小，不涉及复杂的操作，有利于提升性能。\n关于推到更高的层级最多也只能是两层的解释。\nlevel0层的文件通常是无序的，往上的更高层级则要求文件按照键进行排序。\n通常是对于数据进行读取和去重然后推送到更高的层级。随着层级的提高，文件数量越来越少，但是文件的数据量更多，访问的数据块也越多，这会增加IO开销。\n进行minor compaction时，本身文件量并不高，推导更高的层级反而导致了更大的开销，得不偿失。\n那如何确定讲memtable中的数据存放在哪个层级，看下面的代码：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 /*确立memtable输出的数据应该存放哪个层级*/ int Version::PickLevelForMemTableOutput(const Slice\u0026amp; smallest_user_key, const Slice\u0026amp; largest_user_key) { int level = 0; // 看level0是否与memtable输出的键有重叠，如果没有，继续向下查找 // 没有重叠向下查找是为了确保多个层级之间都没有重叠，减少对于major的调用，提升性能 // 存在重叠的愿意按：多版本的不同数据，同一个文件的多个用户间，文件分布不均等 // major就是拿来处理层与层之间的重叠，进而定期优化清理层级结构。 if (!OverlapInLevel(0, \u0026amp;smallest_user_key, \u0026amp;largest_user_key)) { // Push to next level if there is no overlap in next level, // and the #bytes overlapping in the level after that are limited. InternalKey start(smallest_user_key, kMaxSequenceNumber, kValueTypeForSeek); InternalKey limit(largest_user_key, 0, static_cast\u0026lt;ValueType\u0026gt;(0)); std::vector\u0026lt;FileMetaData*\u0026gt; overlaps; // 循环遍历，直到达到最大内存压缩层级 while (level \u0026lt; config::kMaxMemCompactLevel) { if (OverlapInLevel(level + 1, \u0026amp;smallest_user_key, \u0026amp;largest_user_key)) { break; // 有重叠时保持当层级退出 } // 如下面两层的重叠字数超过最大值时，容易导致性能问题，层级不能上升 if (level + 2 \u0026lt; config::kNumLevels) { // Check that file does not overlap too many grandparent bytes. GetOverlappingInputs(level + 2, \u0026amp;start, \u0026amp;limit, \u0026amp;overlaps); const int64_t sum = TotalFileSize(overlaps); if (sum \u0026gt; MaxGrandParentOverlapBytes(vset_-\u0026gt;options_)) { break; } } level++; } } return level; } 紧接着就又存在了一个问题，minor compaction和major compaction的执行过程究竟哪里不同，才会导致它们在不同的情况下使用。这个后面会结合代码解释，先大致说说。\nMajor Compaction\n目的: Major Compaction 主要是将 Level 0 中的所有文件合并到 Level 1，并将 Level 1 中的文件合并到 Level 2，以减少文件数量和重叠，提高查找效率。 执行过程 选择文件: 选择需要合并的文件，通常是 Level 0 中的所有文件。 读取数据: 读取所有选择的文件的数据，可能涉及多个文件的内容。 去重和排序: 对读取到的所有键值对进行去重和排序，以确保合并后的文件是有序的。 写入新文件: 将合并后的数据写入新文件，并将其放入更高层级（如 Level 1）。 更新状态: 更新 Manifest 文件，记录新文件的位置和状态。 删除旧文件: 旧的合并文件会被删除，释放存储空间。 Minor Compaction\n目的: Minor Compaction 主要是将 MemTable 中的数据写入 Level 0，并将 Level 0 中的一些文件推送到更高层级（如 Level 1），通常处理较小的数据量。 执行过程 选择 MemTable: 从 MemTable 中选择数据进行写入。 写入 Level 0: 将 MemTable 中的数据直接写入 Level 0 的新文件。 合并操作: 在 Level 0 中，可能会选择一些文件进行简单的合并（如果有）。 更新状态: 更新 Manifest 文件，记录新文件的位置。 释放空间: 旧的 MemTable 数据被替换或清理。 Major Compaction范围 LevelDB在进行Major Compaction时，至少需要确定以下参数：\n确定Compaction起始层级i，通常为level0，因为该层数量最多最容易重叠。 确定level_i层SSTable input，识别需要合并的sstable文件。 找到i+1层中与当前层级中，没有合并的sstable存在重叠的文件，讲这些重叠文件和当前文件一起处理生成新的文件放在i+1层文件中。 由于三种Major Compaction的起始条件与目标都不同，其确定这三个参数的方式稍有不同。\n后台线程中的compaction DBImpl::BGWork是后台线程的执行入口。具体情况从代码中分析：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 /*将db通过类型转换程DBImpl，然后调用BackGroundCall接口，启动和调度后台处理任务。*/ void DBImpl::BGWork(void* db) { reinterpret_cast\u0026lt;DBImpl*\u0026gt;(db)-\u0026gt;BackgroundCall(); } /*处理levelDB的后台任务*/ void DBImpl::BackgroundCall() { MutexLock l(\u0026amp;mutex_); // 创建互斥锁，保证线程安全。 assert(background_compaction_scheduled_); // 确保后台有任务被调度 if (shutting_down_.load(std::memory_order_acquire)) { // No more background work when shutting down. // 数据库正在关闭时不执行任何后台工作，避免关闭过程中继续处理数据导致数据损坏 } else if (!bg_error_.ok()) { // No more background work after a background error. // 排除后台错误 } else { // 执行合并操作，将levelDB中的数据整理并优化到适当的层级 // 只要满足条件触发合并，这个过程就会不断被调用 // 首次遍历，优化存储，减少重叠文件和荣誉数据 BackgroundCompaction(); } // 重置调度装置，表示任务完成，避免重复执行同一个合并任务 background_compaction_scheduled_ = false; // Previous compaction may have produced too many files in a level, // so reschedule another compaction if needed. MaybeScheduleCompaction(); // 检查是否需要重新调度合并，对sstable文件数量进行评估 background_work_finished_signal_.SignalAll(); // 唤醒等待线程，可以促进触发新的合并 } 我还挺好奇，BackgroundCompaction和MaybeScheduleCompaction的具体差异，后者前面分析过了，有待处理的压缩比如minor compaction或者major compaction时会调用。\n看看BackgroundCompaction()这个函数：代码比较长，这里就叙述一下大致逻辑：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 /*leveldb的核心部分，涵盖了合并的选择，执行和状态管理*/ void DBImpl::BackgroundCompaction() { // 上锁保障线程安全 mutex_.AssertHeld(); // 处理尚未合并的immutable memetable if (imm_ != nullptr) { CompactMemTable(); return; } // 处理尚未合并的 munual compaction // 根据is_munual选择对c进行手动合并还是自动合并 Compaction* c; bool is_manual = (manual_compaction_ != nullptr); // 对c进行再次检查的主要原因是为了确保合并操作的完整性和正确性。 // 第一次合并主要是处理选择出来的SSTable文件，执行合并操作。 // 第二次目的是确认合并结果 Status status; /*c为空时，跳过； 当前任务不是mutual compaction，则判断是不是is_trivial move(只需要从一层移动到下一层)，既不需要合并也不需要拆分SSTable，只通过Versionset修改一下level的文件编号即可。 否则，执行compaction的操作，依次执行DoCompactionWork、CleanupCompaction、RemoveObsoleteFiles。*/ if (c == nullptr) { // Nothing to do 没有合并文件 } else if (!is_manual \u0026amp;\u0026amp; c-\u0026gt;IsTrivialMove()) {...} // 错误处理，状态更新 } // is_manual为1则是手动合并，为0则是自动合并。 至于Compaction::IsTrivialMove的实现:\n1 2 3 4 5 6 7 8 9 10 11 12 13 /*判断当前的操作是否可以简单移动*/ bool Compaction::IsTrivialMove() const { const VersionSet* vset = input_version_-\u0026gt;vset_; // Avoid a move if there is lots of overlapping grandparent data. // Otherwise, the move could create a parent file that will require // a very expensive merge later on. // 当前层有文件而下一层没有文件，因为这确保了移动不会引入重叠，并简化了未来的合并流程 // 如果在进行简单移动时，当前层的文件与祖父层的文件重叠过多，这可能导致在未来需要进行昂贵的合并操作 // 如果当前层与下一层之间的重叠较少，则在合并时主要关注当前层和下一层之间的关系，而与祖父层的关系相对较小。也就是让祖父层和下一层重叠多是一个很糟的决定 return (num_input_files(0) == 1 \u0026amp;\u0026amp; num_input_files(1) == 0 \u0026amp;\u0026amp; TotalFileSize(grandparents_) \u0026lt;= MaxGrandParentOverlapBytes(vset-\u0026gt;options_)); } Minor Compaction 对于该步的直接理解在于，从immutable memtable转换成磁盘中的sstable文件，接下来看看具体过程。\n触发 首先我们了解一下Memtable转换成Immutable Memtable的过程，这个是在DBImpl::MakeRoomForWrite中。\n1 2 3 4 5 6 7 8 9 10 /* leveldb是流控和等待的方式。 在这个函数中，通过综合内存管理和文件状态，确保leveldb在写入时的高效性。 在循环写入的过程中，会对不同的情况进行判断，比如 1.level0文件数量接近上限(默认为8，超过4时会触发minor compaction），开始对写入操作施加延迟，减少坚持波动给压缩线程留出时间 2.内存表有足够空间时，继续写入，memtable有空间 3.内存表已满，并且之前的内存表正在压缩（memtable已满，immutable memtable没有完成minor compaction），等待 4.磁盘中level0文件太多，超过8时，等待 5.切换到新的内存表，压缩之前的内存表，更新状态 */ 在触发Minor Compaction前，就切断到新的WAL写入，如果Minor Compaction失败，就需要从这之前的WAL中恢复；如果Minor Compacton没有完成，leveldb不会删除旧的WAL，因此不会出现数据丢失问题，LevelDB这样做是为了在保证安全地情况下，避免Minor Compaction操作阻塞对LevelDB的正常读写。\nSize Compaction 触发 在非level0层根据总的sstable大小触发，在level0层根据该层的sstable数触发。也就是说只有发生了Compaction，才有可能触发Size Compaction.\nCompaction的执行会导致Version的更新，因此LevelDB在VersionSet::LogAndApply方法更新Version之后，让其调用VersionSet::Finalize计算每层SSTable看是否需要SIze，然后选出最需要size compaction的层作为下次目标。\n(这个应该就是根据每层的sstable的数量进行触发，当到达边缘时，会重排进行一些整体，具体操作后面讲，先看看触发条件)\n在后台线程的DBImpl::BackgroundCall方法中，该方法在完成Compaction操作后，会再次调用MaybeScheduleComapction方法，触发上次因Compaction而需要的Size Compaction操作。\n1 2 3 4 5 6 7 void DBImpl::BackgroundCall() { // ... ... // Previous compaction may have produced too many files in a level, // so reschedule another compaction if needed. MaybeScheduleCompaction(); background_work_finished_signal_.SignalAll(); } Seek Compaction 触发 当leveldb查找key时，会记录一些统计信息。当在SSTable上查找时，会记录发生seekmiss的SSTable，这样更新version中相应的FileMetaData中的allowed_seeks字段，并且通过MaybeScheduleCompaction检查是否需要触发Seek Compaction。\n在DBImpl::Get函数(读取）中，每次读取会对更新的信息进行统计，然后再判断是否需要进行seek compaction，以下为代码：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 Status DBImpl::Get(const ReadOptions\u0026amp; options, const Slice\u0026amp; key, std::string* value) { Status s; MutexLock l(\u0026amp;mutex_); // 加锁保护 SequenceNumber snapshot; if (options.snapshot != nullptr) { snapshot = // 确认快照 static_cast\u0026lt;const SnapshotImpl*\u0026gt;(options.snapshot)-\u0026gt;sequence_number(); } else { snapshot = versions_-\u0026gt;LastSequence(); } MemTable* mem = mem_; // 获取当前的内存表和不可变内存表 MemTable* imm = imm_; Version* current = versions_-\u0026gt;current(); // 当前版本的引用 mem-\u0026gt;Ref(); if (imm != nullptr) imm-\u0026gt;Ref(); current-\u0026gt;Ref(); bool have_stat_update = false; Version::GetStats stats; // Unlock while reading from files and memtables { mutex_.Unlock(); // First look in the memtable, then in the immutable memtable (if any). LookupKey lkey(key, snapshot); if (mem-\u0026gt;Get(lkey, value, \u0026amp;s)) { // Done 先在当前内存表找 } else if (imm != nullptr \u0026amp;\u0026amp; imm-\u0026gt;Get(lkey, value, \u0026amp;s)) { // Done 再在不可变内存表中找 } else { // 最后在当前版本的SSTable文件中查找 s = current-\u0026gt;Get(options, lkey, value, \u0026amp;stats); have_stat_update = true; } mutex_.Lock(); } // 如果查找时更新了统计信息，调用MaybeScheduleCompaction决定是否需要进行压缩 if (have_stat_update \u0026amp;\u0026amp; current-\u0026gt;UpdateStats(stats)) { MaybeScheduleCompaction(); // 这个会对各种压缩是否需要进行检查 } // 解除引用计数 mem-\u0026gt;Unref(); if (imm != nullptr) imm-\u0026gt;Unref(); current-\u0026gt;Unref(); return s; } 在Version::Get中，逐层遍历覆盖了给定的lookupkey的sstable，同时调用match判断是否有我们想要查找的internalkey，即如果发生了seek就会调用match。\n如果sstable中没有该key，则会继续查找，当找到了key时返回false。Match还会记录第一次seek miss的SSTable。DBImpl::Get会将SSTable的allowed_seeks减1，MaybeScheduleCompaction检查是否需要触发Seek Compaction。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 Status Version::Get(const ReadOptions\u0026amp; options, const LookupKey\u0026amp; k, std::string* value, GetStats* stats) { stats-\u0026gt;seek_file = nullptr; stats-\u0026gt;seek_file_level = -1; struct State { Saver saver; GetStats* stats; const ReadOptions* options; Slice ikey; FileMetaData* last_file_read; int last_file_read_level; VersionSet* vset; Status s; bool found; static bool Match(void* arg, int level, FileMetaData* f) { State* state = reinterpret_cast\u0026lt;State*\u0026gt;(arg); // 没有找到文件时，记录当前文件和层级 if (state-\u0026gt;stats-\u0026gt;seek_file == nullptr \u0026amp;\u0026amp; state-\u0026gt;last_file_read != nullptr) { // 检查是否有文件已经被访问过 // We have had more than one seek for this read. Charge the 1st file. state-\u0026gt;stats-\u0026gt;seek_file = state-\u0026gt;last_file_read; state-\u0026gt;stats-\u0026gt;seek_file_level = state-\u0026gt;last_file_read_level; } state-\u0026gt;last_file_read = f; state-\u0026gt;last_file_read_level = level; // 使用tablecache从缓存中获取数据 state-\u0026gt;s = state-\u0026gt;vset-\u0026gt;table_cache_-\u0026gt;Get(*state-\u0026gt;options, f-\u0026gt;number, f-\u0026gt;file_size, state-\u0026gt;ikey, \u0026amp;state-\u0026gt;saver, SaveValue); if (!state-\u0026gt;s.ok()) { state-\u0026gt;found = true; return false; } switch (state-\u0026gt;saver.state) { case kNotFound: // 继续找其它文件 return true; // Keep searching in other files case kFound: // 找到键更新状态返回 state-\u0026gt;found = true; return false; case kDeleted: // 返回，键已删除 return false; case kCorrupt: // 记录错误状态返回 state-\u0026gt;s = Status::Corruption(\u0026#34;corrupted key for \u0026#34;, state-\u0026gt;saver.user_key); state-\u0026gt;found = true; return false; } // Not reached. Added to avoid false compilation warnings of // \u0026#34;control reaches end of non-void function\u0026#34;. return false; } }; State state; state.found = false; state.stats = stats; state.last_file_read = nullptr; state.last_file_read_level = -1; state.options = \u0026amp;options; state.ikey = k.internal_key(); state.vset = vset_; state.saver.state = kNotFound; state.saver.ucmp = vset_-\u0026gt;icmp_.user_comparator(); state.saver.user_key = k.user_key(); state.saver.value = value; /*遍历版本中的文件，查找与给定的用户键和内部键重叠的文件，并通过回调函数处理这些文件。通过分层的方式，确保在最新文件优先的同时，也不遗漏其他层中的重叠文件。*/ ForEachOverlapping(state.saver.user_key, state.ikey, \u0026amp;state, \u0026amp;State::Match); return state.found ? state.s : Status::NotFound(Slice()); } get函数中，文件的访问和查找会统计访问过的文件。虽然不是直接的seek compaction，但是为后续的compaction提高了信息，判断哪些文件需要被合并以减少查找时的IO。\nForEachOverlapping函数中，查找过程会遍历多个层次的文件，通过重叠查找相关文件，这有助于进行seekcomapction时，确定哪些文件时重叠的进而决定compaction。\nManual Compaction 触发 Manual Comapction的触发时机比较简单。\n当LevelDB的用户调用DB::CompactRange接口时，LevelDB会检查用户给定的Compact范围与当前状态，判断是否需要执行Manual Compaction。\n如果确定执行Manual Compaction，则设置manual_compaction_，再调用MaybeScheduleCompaction方法以尝试触发Manual Compaction。\nMinor Compaction执行 以下两个函数是imm转变为sst的过程，\n文件添加：DBImpl::WriteLevel0Table 创建新的 SST 文件并将其添加到 Level 0或者其它层级。 触发条件：一旦 Level 0 中的文件数量达到预设阈值，系统会自动触发 minor compaction。 合并操作：在 minor compaction 过程中，Level 0 的 SST 文件会被合并并输出到 Level 1，减少 Level 0 的文件数量，提高性能。 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 26 27 28 29 30 31 32 33 /*将imm变为SSTable，同时添加到leveldb的版本控制和文件管理。*/ void DBImpl::CompactMemTable() { mutex_.AssertHeld(); // 操作期间有锁，避免并发问题（并发控制） assert(imm_ != nullptr); // Save the contents of the memtable as a new Table VersionEdit edit; // 版本编辑（内存管理） Version* base = versions_-\u0026gt;current(); base-\u0026gt;Ref(); // 引用版本 Status s = WriteLevel0Table(imm_, \u0026amp;edit, base); // 将imm写入level0，更新版本信息 base-\u0026gt;Unref(); // 如果写入成功并且正在关闭数据库，记录错误返回 if (s.ok() \u0026amp;\u0026amp; shutting_down_.load(std::memory_order_acquire)) { s = Status::IOError(\u0026#34;Deleting DB during memtable compaction\u0026#34;); } // 如果没有错误，更新版本信息（错误管理） // Replace immutable memtable with the generated Table if (s.ok()) { edit.SetPrevLogNumber(0); edit.SetLogNumber(logfile_number_); // Earlier logs no longer needed s = versions_-\u0026gt;LogAndApply(\u0026amp;edit, \u0026amp;mutex_); } // 清理空间，清理错误文件（性能优化） if (s.ok()) { // Commit to the new state imm_-\u0026gt;Unref(); imm_ = nullptr; has_imm_.store(false, std::memory_order_release); RemoveObsoleteFiles(); } else { RecordBackgroundError(s); } } 重点在WriteLevel0Table这个函数：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 /*要用于将内存表（MemTable）的数据写入到 Level0的SST文件中*/ /* * meta文件元数据，通过mem的迭代器保存到sst文件中 * edit描述数据库版本的更改，记录新文件的添加和删除等信息 * base当前数据库版本，参考已经存在的数据和文件信息 */ Status DBImpl::WriteLevel0Table(MemTable* mem, VersionEdit* edit, Version* base) { mutex_.AssertHeld(); // 确保有锁 const uint64_t start_micros = env_-\u0026gt;NowMicros(); // 记录开始时间 FileMetaData meta; // 创建文件元数据 meta.number = versions_-\u0026gt;NewFileNumber(); pending_outputs_.insert(meta.number); Iterator* iter = mem-\u0026gt;NewIterator(); // 创建迭代器遍历内存表 Log(options_.info_log, \u0026#34;Level-0 table #%llu: started\u0026#34;, (unsigned long long)meta.number); // 记录日志 Status s; { // 加锁建构表 mutex_.Unlock(); // （理清表中有什么，meta中有什么） /* BuildTable 函数负责实际的写入过程。 它将遍历迭代器中的数据，并写入到新SST文件中。 会在指定数据库目录中创建新文件，文件名基于meta.number生成。 */ s = BuildTable(dbname_, env_, options_, table_cache_, iter, \u0026amp;meta); mutex_.Lock(); } // 日志记录和删除迭代器 Log(options_.info_log, \u0026#34;Level-0 table #%llu: %lld bytes %s\u0026#34;, (unsigned long long)meta.number, (unsigned long long)meta.file_size, s.ToString().c_str()); delete iter; pending_outputs_.erase(meta.number); // 这个pending_outputs什么作用？ // 主要用于跟踪正在进行的写入操作，避免同一时刻对同一个文件进行多次写入 // 防止并发造成的数据破坏 // Note that if file_size is zero, the file has been deleted and // should not be added to the manifest. int level = 0; // 表格构建成功，文件大小大于0 if (s.ok() \u0026amp;\u0026amp; meta.file_size \u0026gt; 0) { const Slice min_user_key = meta.smallest.user_key(); const Slice max_user_key = meta.largest.user_key(); if (base != nullptr) { // 根据用户键选择写入层级 level = base-\u0026gt;PickLevelForMemTableOutput(min_user_key, max_user_key); } // 将文件信息添加到版本编辑中 edit-\u0026gt;AddFile(level, meta.number, meta.file_size, meta.smallest, meta.largest); } CompactionStats stats; stats.micros = env_-\u0026gt;NowMicros() - start_micros; stats.bytes_written = meta.file_size; stats_[level].Add(stats); return s; } CompactRange 函数主要用于执行 minor compaction，它将 Level 0 中的 SST 文件合并到 Level 1 或更高层级。其主要作用是优化存储结构，减少 Level 0 的文件数量，提升查询和写入性能。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 /*处理leveldb中的范围压缩*/ void DBImpl::CompactRange(const Slice* begin, const Slice* end) { int max_level_with_files = 1; { MutexLock l(\u0026amp;mutex_); Version* base = versions_-\u0026gt;current(); // 获取当前版本 for (int level = 1; level \u0026lt; config::kNumLevels; level++) { if (base-\u0026gt;OverlapInLevel(level, begin, end)) { max_level_with_files = level; // 查找有重叠的最高层级 } } } TEST_CompactMemTable(); // TODO(sanjay): Skip if memtable does not overlap for (int level = 0; level \u0026lt; max_level_with_files; level++) { TEST_CompactRange(level, begin, end); // 对每个层级进行范围压缩 // 将指定范围的sst文件合并，更新数据库存储结构 } } 于从 Level 0 到找到的最高层级，CompactRange 会调用 TEST_CompactRange 函数，依次压缩每个层级。\n压缩过程： 读取文件：选择与指定范围重叠的 SST 文件。 合并数据：将这些文件中的数据合并，进行排序和去重。 写入新文件：将合并后的结果写入新的 SST 文件，存储在目标层级。 更新版本信息：压缩完成后，更新数据库的版本信息，移除已合并的旧文件。 Major Compaction执行 关于major，我们来分析一个比较长的函数DBImpl::DoCompactionWork:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 /*执行具体的压缩工作，从输入迭代器中读取键值对，合并数据，写入输出文件，更新版本信息*/ Status DBImpl::DoCompactionWork(CompactionState* compact) { const uint64_t start_micros = env_-\u0026gt;NowMicros();// 初始化开始时间 int64_t imm_micros = 0; // Micros spent doing imm_ compactions // 记录日志 Log(options_.info_log, \u0026#34;Compacting %d@%d + %d@%d files\u0026#34;, compact-\u0026gt;compaction-\u0026gt;num_input_files(0), compact-\u0026gt;compaction-\u0026gt;level(), compact-\u0026gt;compaction-\u0026gt;num_input_files(1), compact-\u0026gt;compaction-\u0026gt;level() + 1); assert(versions_-\u0026gt;NumLevelFiles(compact-\u0026gt;compaction-\u0026gt;level()) \u0026gt; 0); assert(compact-\u0026gt;builder == nullptr); assert(compact-\u0026gt;outfile == nullptr); if (snapshots_.empty()) {// 设置快照 compact-\u0026gt;smallest_snapshot = versions_-\u0026gt;LastSequence(); } else { compact-\u0026gt;smallest_snapshot = snapshots_.oldest()-\u0026gt;sequence_number(); } // 为当前版本创建一个迭代器，用于遍历待压缩的键值对 Iterator* input = versions_-\u0026gt;MakeInputIterator(compact-\u0026gt;compaction); // Release mutex while we\u0026#39;re actually doing the compaction work mutex_.Unlock(); input-\u0026gt;SeekToFirst(); // 检查迭代器是否有效 Status status; ParsedInternalKey ikey; std::string current_user_key; bool has_current_user_key = false; SequenceNumber last_sequence_for_key = kMaxSequenceNumber; 这部分就是，断言避免编码，做好日志，初始化时间等，准备数据，设置快照…然后前面是需要加锁的，然后开始解锁，设置迭代器，seq。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 /*遍历处理键值对，根据压缩的需求决定哪些键值对保留，哪些需要抛弃*/ while (input-\u0026gt;Valid() \u0026amp;\u0026amp; !shutting_down_.load(std::memory_order_acquire)) { // Prioritize immutable compaction work优先处理imm if (has_imm_.load(std::memory_order_relaxed)) { const uint64_t imm_start = env_-\u0026gt;NowMicros(); mutex_.Lock(); if (imm_ != nullptr) { CompactMemTable(); // 这个函数可以压缩不可变内存表 // Wake up MakeRoomForWrite() if necessary.若imm为空格，唤醒相关函数 background_work_finished_signal_.SignalAll(); } mutex_.Unlock(); imm_micros += (env_-\u0026gt;NowMicros() - imm_start); // 记录处理不可变内存表所花费的时间 } Slice key = input-\u0026gt;key();// 通过迭代器获取当前键 if (compact-\u0026gt;compaction-\u0026gt;ShouldStopBefore(key) \u0026amp;\u0026amp; compact-\u0026gt;builder != nullptr) { // 到达停止条件时完成写入 status = FinishCompactionOutputFile(compact, input); if (!status.ok()) { break; } } // Handle key/value, add to state, etc. bool drop = false; // 解析当前键，获取用户键和序列号 if (!ParseInternalKey(key, \u0026amp;ikey)) { // Do not hide error keys 解析失败，重置相关变量 current_user_key.clear(); has_current_user_key = false; last_sequence_for_key = kMaxSequenceNumber; } else { // 处理解析成功的键 if (!has_current_user_key || user_comparator()-\u0026gt;Compare(ikey.user_key, Slice(current_user_key)) != 0) { // First occurrence of this user key current_user_key.assign(ikey.user_key.data(), ikey.user_key.size()); has_current_user_key = true; last_sequence_for_key = kMaxSequenceNumber; } if (last_sequence_for_key \u0026lt;= compact-\u0026gt;smallest_snapshot) { // Hidden by an newer entry for same user key drop = true; // (A) } else if (ikey.type == kTypeDeletion \u0026amp;\u0026amp; ikey.sequence \u0026lt;= compact-\u0026gt;smallest_snapshot \u0026amp;\u0026amp; compact-\u0026gt;compaction-\u0026gt;IsBaseLevelForKey(ikey.user_key)) { // For this user key: // (1) there is no data in higher levels // (2) data in lower levels will have larger sequence numbers // (3) data in layers that are being compacted here and have // smaller sequence numbers will be dropped in the next // few iterations of this loop (by rule (A) above). // Therefore this deletion marker is obsolete and can be dropped. drop = true; } last_sequence_for_key = ikey.sequence; } if (!drop) { // 继续处理 /*打开输出文件，更新输出文件最小最大键，将键值对添加到当前输出文件*/ // Open output file if necessary if (compact-\u0026gt;builder == nullptr) { status = OpenCompactionOutputFile(compact); if (!status.ok()) { break; } } // 如果为第一个条目，设置最小值 if (compact-\u0026gt;builder-\u0026gt;NumEntries() == 0) { compact-\u0026gt;current_output()-\u0026gt;smallest.DecodeFrom(key); } // 更新当前条目的最大键 compact-\u0026gt;current_output()-\u0026gt;largest.DecodeFrom(key); compact-\u0026gt;builder-\u0026gt;Add(key, input-\u0026gt;value()); // 添加键值对 // Close output file if it is big enough 关闭输出文件 if (compact-\u0026gt;builder-\u0026gt;FileSize() \u0026gt;= compact-\u0026gt;compaction-\u0026gt;MaxOutputFileSize()) { status = FinishCompactionOutputFile(compact, input); if (!status.ok()) { break; } } } input-\u0026gt;Next(); } 在处理过程中，函数会优先处理不可变内存表，确保在压缩过程中不会丢失重要数据。对于每个输入键，函数会解析其内部格式并判断是否需要丢弃，基于其序列号和类型。如果键有效，就将其添加到当前的输出文件中，必要时会打开新文件并检查文件大小以决定是否完成写入。整个过程中，函数还会处理各种状态检查，并在最后记录压缩的统计信息，以便分析性能和效果。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 /*处理完成情况*/ if (status.ok() \u0026amp;\u0026amp; shutting_down_.load(std::memory_order_acquire)) { status = Status::IOError(\u0026#34;Deleting DB during compaction\u0026#34;); } if (status.ok() \u0026amp;\u0026amp; compact-\u0026gt;builder != nullptr) { status = FinishCompactionOutputFile(compact, input); } if (status.ok()) { status = input-\u0026gt;status(); } delete input; input = nullptr; /*统计信息：包括压缩时间，字节读取，写入统计*/ CompactionStats stats; stats.micros = env_-\u0026gt;NowMicros() - start_micros - imm_micros; for (int which = 0; which \u0026lt; 2; which++) { for (int i = 0; i \u0026lt; compact-\u0026gt;compaction-\u0026gt;num_input_files(which); i++) { stats.bytes_read += compact-\u0026gt;compaction-\u0026gt;input(which, i)-\u0026gt;file_size; } } for (size_t i = 0; i \u0026lt; compact-\u0026gt;outputs.size(); i++) { stats.bytes_written += compact-\u0026gt;outputs[i].file_size; } mutex_.Lock(); stats_[compact-\u0026gt;compaction-\u0026gt;level() + 1].Add(stats); /*安装压缩结果：更新版本控制，记录错误*/ if (status.ok()) { status = InstallCompactionResults(compact); } if (!status.ok()) { RecordBackgroundError(status); } /*记录日志*/ VersionSet::LevelSummaryStorage tmp; Log(options_.info_log, \u0026#34;compacted to: %s\u0026#34;, versions_-\u0026gt;LevelSummary(\u0026amp;tmp)); return status; } 注：\n要注意一个误区是major compaction和minor compaction不是两个具体的过程。它们都是读取文件，合并数据，写入新文件，更新版本信息这几个过程。minor和major我刚开始以为是底层和高层的不同压缩，这样理解不准确，应该是微型和大型压缩更为合适。\nminor相对简单，主要涉及少量文件的合并。major会涉及更多，层之内，层与层之间的文件的合并等。\nversion 前言 leveldb每次生成或者删除sstable，都会从一个版本升级成另外一个版本。换句话说，每次都会从一个版本升级成另外一个版本。sstable的更替对于leveldb来说是一个最小的操作单元，具有原子性。。\n从leveldb的角度来看，MVCC机制不仅能够并行化实现读读和读写操作，还使其能够实现快照读。用户可以通过提供的接口保留一定时间的快照，在用户释放快照前，该快照创建时leveldb中存在的数据就不会被释放。\nLevelDB的多版本存储设计可分为三个层次：\n从key/value的角度： 每次变更操作的记录（Batch Writer可视为一次操作）都有不同且递增的SequenceNumber。对于一个UserKey，当存在SequenceNumber更高的的记录时，旧的记录不会被立即删除，至少要在该SequenceNumber之前的所有Snapshot都被释放后才能删除（具体删除时间与Compaction时间有关）。这是LevelDB实现Snapshot Read的基础。 从MemTable的角度： LevelDB中的MemTable通过引用计数来控制释放时间。在需要读取MemTable时（无论是Get操作还是Minor Compaction时），读取前会增大其引用计数，读取后减小引用计数。这样，即使MemTable已被通过Minor Compaction操作写入到Level-0文件，MemTable在被读取，它就不会被释放，保证能被多次读。 从数据库文件的角度： LevelDB的文件同样需要引用计数，当执行Major Compaction时，LevelDB不会立即删除已被合并的数据库文件，因为此时可能还有未完成的读取该文件的操作。 key/value的版本实际上也是依赖于内存与稳定存储，其分别在Compaction与Put/Get操作中体现，因此这里我们主要关注后两者。\nMemTable的多版本与Snapshot信息是不需要直接持久化的，因为数据库关闭时无法进行Snapshot Read，也就没有了Snapshot的概念，而最新的MemTable会通过WAL重建，旧的MemTable也不再会被依赖。\n数据库文件则不同，LevelDB必须记录数据库文件的版本信息，否则在数据库重启时无法快速确定哪些文件是有效的（LevelDB提供了文件版本信息损坏时的修复机制）。而LevelDB中Version及相关概念就是为此设计的。\nVersionSet是一个管理和维护多个版本的集合，\n而VersionEdit是对版本变化的具体描述和记录。通过这种结构，LevelDB能够有效地管理数据的版本控制和变化。\n源码分析 version_set.h 文件定义了 LevelDB 中版本管理和压缩的核心数据结构。Version 和 VersionSet 类负责维护数据库的版本及其文件信息，而 Compaction 类则处理压缩操作。这个结构确保了 LevelDB 能高效地管理数据的版本和执行压缩策略，从而优化性能。同时相关文件有Manifest和Current。\nclass compaction 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 26 27 28 29 30 31 32 33 34 35 class Compaction { public: ... private: friend class Version; friend class VersionSet; Compaction(const Options* options, int level); int level_; //正在进行压缩的级别 uint64_t max_output_file_size_; // 最大输出文件大小限制 Version* input_version_; // 指向当前压缩的输入版本 VersionEdit edit_; // 保存压缩操作所作的编辑内容，包含需要添加和删除的文件 // Each compaction reads inputs from \u0026#34;level_\u0026#34; and \u0026#34;level_+1\u0026#34; // 一个存储当前级别，一个存储下一个几倍 std::vector\u0026lt;FileMetaData*\u0026gt; inputs_[2]; // The two sets of inputs // State used to check for number of overlapping grandparent files // (parent == level_ + 1, grandparent == level_ + 2) std::vector\u0026lt;FileMetaData*\u0026gt; grandparents_; size_t grandparent_index_; // Index in grandparent_starts_ bool seen_key_; // Some output key has been seen int64_t overlapped_bytes_; // Bytes of overlap between current output // and grandparent files // State for implementing IsBaseLevelForKey // level_ptrs_ holds indices into input_version_-\u0026gt;levels_: our state // is that we are positioned at one of the file ranges for each // higher level than the ones involved in this compaction (i.e. for // all L \u0026gt;= level_ + 2). // 输入版本的文件列表中为每个级别保存指向当前状态的索引 size_t level_ptrs_[config::kNumLevels]; }; Manifest manifest文件专用于记录版本信息。leveldb采用了增量式的存储方式，记录每一个版本相较于上一个版本的变化情况。展开来说，一个Manifest文件中，包含了多条Session Record，其中第一条Session Record记载了当时leveldb的全量版本信息，其余若干条Session Record仅记录每次更迭的变化情况。\nversion_edit VersionEdit类在levelDB中与上面manifest文件管理密切相关。 manifest文件用于记录数据库的元数据和版本信息。每当数据库状态发生变化时，都会生成一个新的manifest文件，具体表现在versionset中有体现。\n1 2 3 4 5 6 7 8 9 10 11 enum Tag { kComparator = 1, // 比较器的名称字符串 kLogNumber = 2, // 文件编号 kNextFileNumber = 3, // 下一个文件分配的编号 kLastSequence = 4, // 当前版本的最后一个序列号 kCompactPointer = 5, // 压缩指针 kDeletedFile = 6, // 删除的元数据 kNewFile = 7, // 新文件 // 8 was used for large value refs kPrevLogNumber = 9 // 文件编号 }; version_edit文件\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 /*文件元数据，存储每个sst文件的元数据*/ struct FileMetaData { FileMetaData() : refs(0), allowed_seeks(1 \u0026lt;\u0026lt; 30), file_size(0) {} int refs; // 引用计数 int allowed_seeks; // Seeks allowed until compaction 容易触发seek compaction uint64_t number; // 文件的唯一编号 uint64_t file_size; // File size in bytes InternalKey smallest; // Smallest internal key served by table InternalKey largest; // Largest internal key served by table }; /*记录版本的变化信息*/ class VersionEdit { public: VersionEdit() { Clear(); } ~VersionEdit() = default; void Clear(); // 设置信息 void SetComparatorName(const Slice\u0026amp; name) { has_comparator_ = true; comparator_ = name.ToString(); } void SetLogNumber(uint64_t num) { has_log_number_ = true; log_number_ = num; } void SetPrevLogNumber(uint64_t num) { has_prev_log_number_ = true; prev_log_number_ = num; } void SetNextFile(uint64_t num) { has_next_file_number_ = true; next_file_number_ = num; } void SetLastSequence(SequenceNumber seq) { has_last_sequence_ = true; last_sequence_ = seq; } void SetCompactPointer(int level, const InternalKey\u0026amp; key) { compact_pointers_.push_back(std::make_pair(level, key)); } // 添加删除文件 // Add the specified file at the specified number. // REQUIRES: This version has not been saved (see VersionSet::SaveTo) // REQUIRES: \u0026#34;smallest\u0026#34; and \u0026#34;largest\u0026#34; are smallest and largest keys in file void AddFile(int level, uint64_t file, uint64_t file_size, const InternalKey\u0026amp; smallest, const InternalKey\u0026amp; largest) { FileMetaData f; f.number = file; f.file_size = file_size; f.smallest = smallest; f.largest = largest; new_files_.push_back(std::make_pair(level, f)); } // Delete the specified \u0026#34;file\u0026#34; from the specified \u0026#34;level\u0026#34;. void RemoveFile(int level, uint64_t file) { deleted_files_.insert(std::make_pair(level, file)); } // 编码解码 void EncodeTo(std::string* dst) const; Status DecodeFrom(const Slice\u0026amp; src); std::string DebugString() const; private: friend class VersionSet; typedef std::set\u0026lt;std::pair\u0026lt;int, uint64_t\u0026gt;\u0026gt; DeletedFileSet; std::string comparator_; //比较器名称 uint64_t log_number_; // 日志编号 uint64_t prev_log_number_; uint64_t next_file_number_; SequenceNumber last_sequence_; // 最后一个序列号 bool has_comparator_; bool has_log_number_; bool has_prev_log_number_; bool has_next_file_number_; bool has_last_sequence_; std::vector\u0026lt;std::pair\u0026lt;int, InternalKey\u0026gt;\u0026gt; compact_pointers_; // 存储压缩指针 DeletedFileSet deleted_files_; // 跟踪已删除的文件，便于版本更新时管理 std::vector\u0026lt;std::pair\u0026lt;int, FileMetaData\u0026gt;\u0026gt; new_files_; // 存储新添加的文件信息 }; 在创建新版本时，LevelDB首先构造VersionEdit，然后通过VersionSet::LogAndApply方法，先将VersionEdit应用到Current Version，然后将增量的VersionEdit写入Manifest文件中。\nversion_set class version version文件主要用于存储数据库的某个具体版本，包括当前版本的所有文件，统计信息以及相关状态。其中维护了一个文件列表记录每个层级的sst文件，不同版本之间的连接方式为双向链表；通过引用计数管理version对象的生命周期等。\n同时还有和compaction相关的成员变量，\n比如当前需要进行压缩的元数据以及所在层级（确定待压缩候选者）\n比如当前版本的压缩评分和下一个需要压缩的层级（管理压缩的紧迫性和优先级）\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 class Version{ public: ... private: friend class Compaction; friend class VersionSet; class LevelFileNumIterator; explicit Version(VersionSet* vset) : vset_(vset), next_(this), prev_(this), refs_(0), file_to_compact_(nullptr), file_to_compact_level_(-1), compaction_score_(-1), compaction_level_(-1) {} Version(const Version\u0026amp;) = delete; Version\u0026amp; operator=(const Version\u0026amp;) = delete; ~Version(); Iterator* NewConcatenatingIterator(const ReadOptions\u0026amp;, int level) const; // Call func(arg, level, f) for every file that overlaps user_key in // order from newest to oldest. If an invocation of func returns // false, makes no more calls. // // REQUIRES: user portion of internal_key == user_key. void ForEachOverlapping(Slice user_key, Slice internal_key, void* arg, bool (*func)(void*, int, FileMetaData*)); VersionSet* vset_; // VersionSet to which this Version belongs Version* next_; // Next version in linked list Version* prev_; // Previous version in linked list int refs_; // Number of live refs to this version // List of files per level std::vector\u0026lt;FileMetaData*\u0026gt; files_[config::kNumLevels]; // Next file to compact based on seek stats. FileMetaData* file_to_compact_; int file_to_compact_level_; // Level that should be compacted next and its compaction score. // Score \u0026lt; 1 means compaction is not strictly needed. These fields // are initialized by Finalize(). double compaction_score_; int compaction_level_; }; class versionset versionset主要用于管理多个version对象，负责维护数据库的版本历史和状态。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 class VersionSet { public: ... private: class Builder; friend class Compaction; friend class Version; bool ReuseManifest(const std::string\u0026amp; dscname, const std::string\u0026amp; dscbase); void Finalize(Version* v); // 进行压缩或者查询时，了解数据范围有助于优化性能 void GetRange(const std::vector\u0026lt;FileMetaData*\u0026gt;\u0026amp; inputs, InternalKey* smallest, InternalKey* largest); // 合并文件时考虑文件范围 void GetRange2(const std::vector\u0026lt;FileMetaData*\u0026gt;\u0026amp; inputs1, const std::vector\u0026lt;FileMetaData*\u0026gt;\u0026amp; inputs2, InternalKey* smallest, InternalKey* largest); // 为压缩操作设置其它输入文件 void SetupOtherInputs(Compaction* c); // Save current contents to *log Status WriteSnapshot(log::Writer* log); void AppendVersion(Version* v); Env* const env_; const std::string dbname_; // 数据库名称 const Options* const options_; // 数据库选项 TableCache* const table_cache_; // 表缓存 const InternalKeyComparator icmp_; // 内部键比较器 uint64_t next_file_number_; uint64_t manifest_file_number_; uint64_t last_sequence_; // 所有写操作中最后使用的序列号 uint64_t log_number_; uint64_t prev_log_number_; // 0 or backing store for memtable being compacted // Opened lazily WritableFile* descriptor_file_; // 数据库描述符文件 log::Writer* descriptor_log_; // 写入日志 Version dummy_versions_; // Head of circular doubly-linked list of versions.循环双链表的虚拟头 Version* current_; // == dummy_versions_.prev_指向当前版本 // Per-level key at which the next compaction at that level should start.压缩指针 // Either an empty string, or a valid InternalKey. std::string compact_pointer_[config::kNumLevels]; }; 通过LogAndApply()方法应用VersionEdit对象，创建新的Version并将其添加到VersionSet中。下面来看看LogAndApply函数的过程：\n保存状态：首先，它会保存当前的锁状态，以确保在写入文件时不会有其他线程干扰。\n更新版本：接着，通过传入的VersionEdit对象，更新当前的版本信息。这可能包括创建新版本，保存新版本到builder类中，删除旧文件、更新日志编号等。\n写入日志：在更新版本的同时，LogAndApply会将这些变更记录到持久化日志中，以便后续的恢复操作。\n1 2 3 4 5 6 7 8 9 10 11 12 13 Status s; if (descriptor_log_ == nullptr) { // No reason to unlock *mu here since we only hit this path in the // first call to LogAndApply (when opening the database). assert(descriptor_file_ == nullptr); // 日志为空时，生成新的描述符文件名，创建可写文件 new_manifest_file = DescriptorFileName(dbname_, manifest_file_number_); s = env_-\u0026gt;NewWritableFile(new_manifest_file, \u0026amp;descriptor_file_); if (s.ok()) { // 将其作为新的日志写入其，同时将当前版本快照写入日志 descriptor_log_ = new log::Writer(descriptor_file_); s = WriteSnapshot(descriptor_log_); } 应用更改：更新完成后，函数会应用这些更改到当前版本，确保数据库状态与日志一致。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 { mu-\u0026gt;Unlock(); // Write new record to MANIFEST log if (s.ok()) { std::string record; edit-\u0026gt;EncodeTo(\u0026amp;record); s = descriptor_log_-\u0026gt;AddRecord(record); //使用 descriptor_log_ 将该记录添加到MANIFEST日志中。 if (s.ok()) { // 同步到磁盘，确保写入持久化 s = descriptor_file_-\u0026gt;Sync(); } if (!s.ok()) { Log(options_-\u0026gt;info_log, \u0026#34;MANIFEST write: %s\\n\u0026#34;, s.ToString().c_str()); } } // If we just created a new descriptor file, install it by writing a // new CURRENT file that points to it. if (s.ok() \u0026amp;\u0026amp; !new_manifest_file.empty()) { // 更新current文件，指向新创建的描述符文件 s = SetCurrentFile(env_, dbname_, manifest_file_number_); } mu-\u0026gt;Lock(); } 释放锁：最后，释放锁并返回结果，完成整个过程。\nRecover 由于数据库启动前需要恢复数据，也就是利用Manifest信息重新构建一个最新的version。\n利用current文件读取最近使用的Manifest文件。\n创建一个空的version，利用manifest文件中的session record依次apply，还原出一个最新的version，注意manifest的第一条session record记录的是一个version的snapshot，后面记录的都是增量。\n（为了避免manifest过大，每次启动时重新创建的manifest的第一条都是当前版本的快照状态，其它过期的manifest文件会在下次启动的recover流程中自行删除）\n将非current文件指向的其它过期的manifest文件删除。\n将新建的version作为当前数据库的version。\nRepair repair.cc中只有一个公共接口，Run()\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 Status Run() { /*查找数据库目录中的文件 * 解析文件名，提取文件编号和类型 * 对于描述符文件，文件名存入manifest * 对于日志和表文件，存储在logs和table_number中 */ Status status = FindFiles(); if (status.ok()) { ConvertLogFilesToTables(); // 将日志文件转换成表文件 ExtractMetaData(); // 获取每个文件的元数据(key)以及最终数据库的元数据(seq) status = WriteDescriptor(); // 创建一个新的manifest文件，将扫描的数据库元数据进行记录 } if (status.ok()) { unsigned long long bytes = 0; for (size_t i = 0; i \u0026lt; tables_.size(); i++) { bytes += tables_[i].meta.file_size; } Log(options_.info_log, \u0026#34;**** Repaired leveldb %s; \u0026#34; \u0026#34;recovered %d files; %llu bytes. \u0026#34; \u0026#34;Some data may have been lost. \u0026#34; \u0026#34;****\u0026#34;, dbname_.c_str(), static_cast\u0026lt;int\u0026gt;(tables_.size()), bytes); } return status; } 但是该方法的效率十分低下，首先需要对整个数据库的文件进行扫描，其次0层的文件必然将远远大于4个，这将导致极多的compaction发生。\n有关这部分的大致内容就是这样子了，关于version的内容其实可以再次精细化，留个点位，leveldb整体一遍学完后也还会留时间再回头看看的。\n","date":"2024-10-27T00:00:00Z","permalink":"https://sutdown.github.io/p/leveldb%E6%BA%90%E7%A0%81%E9%98%85%E8%AF%BB5-compaction-and-version/","title":"leveldb源码阅读5 Compaction And Version"},{"content":"啊哈，我好懒，其实之前也搭建过网站，用的宝塔面板+typora，然后用了些域名还租了个服务器，一年大概两百吧，其实我搞不太懂宝塔面板，那会就照葫芦画瓢勉强弄了一个，后来出来点问题也不太会修，遂放弃，不过文章大多都有存档。\n今天看到了github可以直接生成静态页面，兴致上来了就想着弄弄，还不用花钱，搞了差不多两小时，emmm，git在用代理的时候总容易出问题，这个就消耗了好久。其实不难，就是套github上一个大佬的模板，但是也快弄了两小时，今天还有其它任务，我先放放吧，下回再继续弄。\n导入之前的文章\n调整相关个人信息\n写篇建站博客\n弄清楚这个代码的框架\n…\n目前就上面那些任务了，看什么时候有时间改改。\n","date":"2024-10-27T00:00:00Z","permalink":"https://sutdown.github.io/p/my-first-blog-based-github/","title":"My first blog based github"},{"content":"snapshot snapshot是什么，有什么用\nsnapshot是一种机制，用于在数据库读取时保持一致的视图，确保在读取数据时，写入操作不会影响读取结果，从而提供一种简单的并发控制方式。LevelDB 的快照机制通过版本管理和引用计数来实现一致性读取，确保读操作在写入操作发生时不会受到影响。版本管理机制在leveldb5中会讲解，这里暂时掠过。\n这个.h文件中的信息有限，主要是有三个类：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 /* `Snapshot` 是一个通用的快照接口或基类。 */ /* 表示一个具体的快照实例，包含一个序列号和指向前后快照的指针 序列号的主要作用是提供快照创建时数据库状态的唯一标识 双链表，便于快速插入和删除 */ class SnapshotImpl : public Snapshot{ public: ... private: friend class SnapshotList; // SnapshotImpl is kept in a doubly-linked circular list. The SnapshotList // implementation operates on the next/previous fields directly. SnapshotImpl* prev_; SnapshotImpl* next_; const SequenceNumber sequence_number_; } /* * 管理多个SnapshotImpl的链表结构，处理快照的创建和删除 */ class SnapshotList { public: ... private: // Dummy head of doubly-linked list of snapshots 虚拟头结点 SnapshotImpl head_; } 当leveldb进行读操作时，过程如下\n接收读取请求，查询Memtable和Immutable Memtable（内存） 查询blockcache（blockcache中存储了从sstable中读取的data数据块） 如果此时还没找到，就会进入磁盘中，通过布隆过滤器定位sstable文件 如果找到，读取sstable中的datablock，否则返回没有找到。 在这个过程中，为了减少读放大的两个措施：\n采用major compaction尽量减少sstable文件 利用布隆过滤器进行快速筛选，看key是否在某个sstable文件中 Bloom Filter bloom filter的数据结构 推荐文章：bloom filter\n初始状态：\nBloom Filter底层是一个m位的位数组，每一位置为0\n当插入某个元素时，会利用k个哈希函数对x进行散列，然后按照数组长度取余后置相应位置为1：\n查找和插入过程类似，也是利用相同的哈希对待元素查找顺序进行哈希，得到k个位置。\n如果所有位上均为1，那么元素有可能存在；如果有位上不为1，那么元素一定不存在。\n相对于其他表示数据集的数据结构，如平衡二叉搜索树、Trie 树、哈希表，甚至更简单的数组或者链表，Bloom Filter 有着巨大的时空优势。上述提到的表示数据集的数据结构，大都需要对数据项本身存储，可能有的做了压缩，比如 Trie 树。但是 Bloom Filter 走了另一条路，并不存储数据项本身，而是存储数据项的几个哈希值，并且用高效紧凑的位数组来存，避免了指针的额外开销。\n这种优势的获得，可以理解为在哈希表基础上，忽略了冲突处理，从而省下了额外开销。\n数学结论 跟布隆过滤器的误判率有关的参数有：\n哈希函数的个数 k 布隆过滤器位数组的容量m; 布隆过滤器插入的数据数量n; 因此根据一系列推导可以得到：\n为了获得最优的准确率，当k = ln2 * (m/n)时，布隆过滤器获得最优的准确性； 在哈希函数的个数取到最优时，要让错误率不超过є，m至少需要取到最小值的1.44倍； 源码实现 filter_policy抽象基类，定义过滤器策略 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 class Slice; class LEVELDB_EXPORT FilterPolicy { public: // 虚析构函数，保证派生类正确清理资源 virtual ~FilterPolicy(); // 返回过滤策略的名称，如果过滤器的编码方式发生不兼容的变化，该名称也需要更改 // 否则旧的不兼容的过滤器被错误的传递到方法中 virtual const char* Name() const = 0; // 根据给定的键列表生成过滤器并附加到dst中。 virtual void CreateFilter(const Slice* keys, int n, std::string* dst) const = 0; // 看给定的键是否可能在过滤器中 virtual bool KeyMayMatch(const Slice\u0026amp; key, const Slice\u0026amp; filter) const = 0; }; // 返回一个新的布隆过滤器的策略，用户可以直接调用函数来获取实例 // 创建布隆过滤器的逻辑与 FilterPolicy 类本身并不直接相关。将其提取为全局函数可以使其更模块化，便于将来对其他过滤策略的扩展。 LEVELDB_EXPORT const FilterPolicy* NewBloomFilterPolicy(int bits_per_key); bloom.cc布隆过滤器的实现 和上面对过滤器原理讲的差不多。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 class BloomFilterPolicy : public FilterPolicy { public: explicit BloomFilterPolicy(int bits_per_key) : bits_per_key_(bits_per_key) { // k太大，增加内存使用，减少假阳性率 // k太小，节省内存，但是增加假阳性率 k_ = static_cast\u0026lt;size_t\u0026gt;(bits_per_key * 0.69); // 0.69 =~ ln(2) if (k_ \u0026lt; 1) k_ = 1; if(k_ \u0026gt; 30) k_ = 30; } // 返回布隆过滤器名称 const char* Name() const override { return \u0026#34;leveldb.BuiltinBloomFilter2\u0026#34;; } //接受一个键的数组（keys）、键的数量（n），并将生成的布隆过滤器结果存储在 dst 中 void CreateFilter(const Slice* keys, int n, std::string* dst) const override { // Compute bloom filter size (in both bits and bytes) size_t bits = n * bits_per_key_; if (bits \u0026lt; 64) bits = 64; size_t bytes = (bits + 7) / 8; bits = bytes * 8; const size_t init_size = dst-\u0026gt;size(); dst-\u0026gt;resize(init_size + bytes, 0); dst-\u0026gt;push_back(static_cast\u0026lt;char\u0026gt;(k_)); // Remember # of probes in filter char* array = \u0026amp;(*dst)[init_size]; for (int i = 0; i \u0026lt; n; i++) { // Use double-hashing to generate a sequence of hash values. // See analysis in [Kirsch,Mitzenmacher 2006]. uint32_t h = BloomHash(keys[i]); // 使用Bloomhash哈希 const uint32_t delta = (h \u0026gt;\u0026gt; 17) | (h \u0026lt;\u0026lt; 15); // Rotate right 17 bits 为双重哈希生成增量，用于后续的哈希计算 for (size_t j = 0; j \u0026lt; k_; j++) { const uint32_t bitpos = h % bits; array[bitpos / 8] |= (1 \u0026lt;\u0026lt; (bitpos % 8)); // 将目标位设置为1 h += delta; } } } // 匹配数据，略 bool KeyMayMatch(const Slice\u0026amp; key, const Slice\u0026amp; bloom_filter) const override {...} // 双重哈希比对 uint32_t h = BloomHash(key); const uint32_t delta = (h \u0026gt;\u0026gt; 17) | (h \u0026lt;\u0026lt; 15); // Rotate right 17 bits for (size_t j = 0; j \u0026lt; k; j++) { const uint32_t bitpos = h % bits; if ((array[bitpos / 8] \u0026amp; (1 \u0026lt;\u0026lt; (bitpos % 8))) == 0) return false; h += delta; } return true; } private: size_t bits_per_key_; // 每个键使用的比特数 size_t k_; // 哈希函数的数量 }; } // namespace filter_block过滤器块，实现数据块的过滤器功能 filter_block.h 该文件定义了leveldb中的过滤器块的相关类，用于实现数据块的过滤器功能，比如布隆过滤器。核心功能是通过构建和读取过滤器块来优化LevelDB的查找性能。\nFilterPolicy抽象基类，负责定义过滤器的策略。它并没有直接的实现\nFilterBlockBuilder类负责创建过滤器，接受一个filterPolicy指针，指定过滤器策略\nFilterBlockReader类负责使用过滤器来判断某个键是否可能在特定数据块中,接收 FilterPolicy 指针和过滤器数据的切片，初始化读取器.\n通过有效利用过滤器，LevelDB能够减少不必要的磁盘读取，提高数据访问效率。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 class FilterBlockBuilder { public: ... private: void GenerateFilter(); const FilterPolicy* policy_; // 过滤器策略。 std::string keys_; // 已展平的键内容。 std::vector\u0026lt;size_t\u0026gt; start_; // 每个键在 keys_ 中的起始索引。 std::string result_; // 当前计算的过滤器数据。 std::vector\u0026lt;Slice\u0026gt; tmp_keys_; // policy_-\u0026gt;CreateFilter() argument 临时存储 std::vector\u0026lt;uint32_t\u0026gt; filter_offsets_; // 计算过滤器的辅助数据。 }; class FilterBlockReader { public: // REQUIRES: \u0026#34;contents\u0026#34; and *policy must stay live while *this is live. FilterBlockReader(const FilterPolicy* policy, const Slice\u0026amp; contents); /* 检查给定的键是否可能匹配指定偏移量的数据块。 如果返回 true，表示键可能存在于该块中， 否则可以确定该块中不存在该键。 */ bool KeyMayMatch(uint64_t block_offset, const Slice\u0026amp; key); private: const FilterPolicy* policy_; const char* data_; // Pointer to filter data (at block-start) const char* offset_; // Pointer to beginning of offset array (at block-end) size_t num_; // 偏移数组中的条目数。 size_t base_lg_; // 编码参数 (see kFilterBaseLg in .cc file) }; filter_block.cc 这里包含有关过滤块中对于过滤器的一些操作。过滤块的作用有：\n布隆过滤器占用内存较多，因此通过分块优化内存使用，以免生成过大的布隆过滤器。 为不同的区块采取不同的过滤策略，从而能根据不同的数据分布和访问模式进行调整以获得更好的性能。 过滤块通过过滤器来判断某个键是否可能存在于特定区块中，如果过滤器表明某个键不在该区块中，则可以避免不必要的磁盘读取，节省 I/O 资源。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 /*构造函数，一些需要初始化的私有成员*/ FilterBlockBuilder::FilterBlockBuilder(const FilterPolicy* policy); /*根据区块偏移量决定何时生成新的过滤器*/ void FilterBlockBuilder::StartBlock(uint64_t block_offset); /*将键添加到当前过滤器*/ void FilterBlockBuilder::AddKey(const Slice\u0026amp; key); /*完成当前过滤器构建*/ Slice FilterBlockBuilder::Finish(); /* * 生成当前键集的过滤器，并将其追加到结果中。 * 在没有键的情况下，会快速路径返回当前过滤器的偏移。 */ void FilterBlockBuilder::GenerateFilter(); /* * 负责读取过滤块并检查某个键是否可能在特定区块中。 * 在构造函数中，它解析过滤块的内容并设置相应的成员变量。 */ FilterBlockReader::FilterBlockReader(const FilterPolicy* policy, const Slice\u0026amp; contents); /*KeyMayMatch 方法通过查找特定区块的过滤器，来判断某个键是否可能匹配。*/ bool FilterBlockReader::KeyMayMatch(uint64_t block_offset, const Slice\u0026amp; key); void FilterBlockBuilder::GenerateFilter()这个生成过滤器的关键在于先要找到keys的数组，keys的slice应该由每个key的初始位置也就是字符串和长度构成，而这些参数可以从FilterBlockBuilder的私有成员starts_数组和keys_中找到。\n这个函数时从Slice中读取相关数据。\n在contents中，有 偏移数组，偏移数组起始位置（4字节），编码参数（1字节）组成，根据这一点在contents中读出相关数据。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 /* * 负责读取过滤块并检查某个键是否可能在特定区块中。 * 在构造函数中，它解析过滤块的内容并设置相应的成员变量。 */ FilterBlockReader::FilterBlockReader(const FilterPolicy* policy, const Slice\u0026amp; contents) : policy_(policy), data_(nullptr), offset_(nullptr), num_(0), base_lg_(0) { size_t n = contents.size(); // 过滤内容 if (n \u0026lt; 5) return; // 1 byte for base_lg_ and 4 for start of offset array base_lg_ = contents[n - 1]; // 编码参数 uint32_t last_word = DecodeFixed32(contents.data() + n - 5); // 偏移数组在原始数据中的起始位置 if (last_word \u0026gt; n - 5) return; data_ = contents.data(); offset_ = data_ + last_word; // 现指向偏移数组的位置 //FilterBlockBuilder时，偏移量可能是动态生成并存储在某个结构中 num_ = (n - 5 - last_word) / 4; // 偏移数组总字节数/4=过滤器的实际数量 } bool FilterBlockReader::KeyMayMatch这个里面key已经给出来了，从offset_中找到过滤器的起始和结束位置，从而定义一个filter，利用policy中的keymaymatch进行调用。\n","date":"2024-10-18T00:00:00Z","permalink":"https://sutdown.github.io/p/leveldb%E6%BA%90%E7%A0%81%E9%98%85%E8%AF%BB4-snapshotbloom-filter/","title":"leveldb源码阅读4 snapshot,Bloom Filter"},{"content":"sstable 感谢这个博主MrCroxx，看了这么多对leveldb的讲解，他的文章是最为明了的！本文中由部分引用他的原文，在此先声明。\n插入，对于以下部分内容的概览，有利于理解cache的实现。\n来源：一只安静的猫\n2.1 hash table数据结构 哈希表是一个很常见的结构了，存储的是key-value结构，一个key-value对常被称作entry，它最大的特点是查找快。在网上找了一张hash table的图 首先它是一个长为len的数组，每个数组中的元素都是一个链表。如图所示，john Smith和Sandra Dee都被hash到152这个地方，所以在152这个地方使用链表来存储这两个entry.查找的时候Sandra Dee的时候，先找到152这个地方，在遍历链表，直到找到key是Sandra的entry。\n2.2 LRU算法 在leveldb中,LRU算法的实现是使用两个双向环形链表，一个链表(in-use)存储当前正在使用的数据，另一个链表(lru)按照访问时间先后顺序存储缓存数据，每个数据都可以在in-use和lru之间切换。当我们需要使用LRU算法来淘汰数据时，只需要在lru上淘汰排序靠后的数据即可。\n2.3 分片LRU缓存 分片LRU缓存很简单，其实就是同时创建多个LRU缓存对象，然后使用hash将特定的缓存数据放置到相应的LRU缓存对象中。这个方式可以避免一个LRU缓存中存储过多的数据。\n前言 sstable的作用 对于redis，\n内存存储：数据主要存储在内存中，确保快速的读写性能。\n持久化机制：可选择性地将数据写入磁盘，以防数据丢失。\n快速响应：由于使用内存和高效的数据结构，Redis 能够快速响应读写请求。\n但是对于leveldb，会先将写操作写入日志文件，其次将写操作应用在memtablde上，当leveldb到达checkpoint点时，memtable会被冻结成一个不可更改的内存数据库immutable memory db，同时创建一个新的memtable供系统继续使用。\nimmutable memory db会在后台进行一次minor compaction，将内存数据库中的数据持久化到磁盘文件中。\n对于minor compaction，会在后文具体讲解，这里简单阐述一下它的目的：\n有效降低内存的使用率 避免日志文件过大，系统回复时间过长 当memory db的数据持久化到文件中时，leveldb会以一定的规则进行文件组织，文件格式变为sstable，本文详细介绍sstable的文件格式以及相关读写。\nlevel的层次结构 这个在布隆过滤器中会详述，为了快速读取数据，采取了读放大的用法。\n正文 好多文章都说sstable是很重要的一个模块，但是代码中也没有sstable.h这个文件。不过这个和file system是属于同一类，因此把它们放在一起，先理清知识结构，再结合代码看细节。\n这个看的逻辑不可尽信，因为leveldb的版本会进行更新，memtable中的结构和代码结构就略有不同。\nsstable sstable格式 物理结构 每个sstable文件按照固定大小进行块划分，默认每个block大小4KiB，在每个块中，存储三个部分：\ndata，压缩类型（说明block中存储的数据是否进行数据压缩，如果压缩，默认未snappy算法），CRC校验码（循环冗余校验的校验码，校验范围包括数据以及压缩类型）。\n逻辑结构 block格式 block格式中的restart可以对频繁出现的公共前缀进行压缩。Restart Entry的间隔leveldb::Options.block_restart_interval默认为16，以平衡缓存局部性。\nsstable中各类block的保存数据，仅关注key/value.\ncache 无论是BlockCache还是TableCache，核心都是实现分片的LRU缓存，这个LRU缓存实现了include/leveldb/cache.h定义的缓存接口。\ninclude/leveldb/cache.h：了解缓存接口和设计理念。\n这是缓存的抽象层，提供了一个通用的接口，以便在不同实现之间进行替换或扩展。\nutil/cache.cc：深入实现细节和逻辑。\n这是实际的缓存实现，负责在内存中管理缓存对象并优化访问速度。\ndb/table_cache.h：了解针对 SSTable 的具体缓存实现。\n这是一个更高层次的缓存，专门用于缓存 SSTable 文件的内容，定义了用于缓存 SSTable 数据块的类和方法，提供了与 SSTable 相关的缓存操作，包括从缓存中读取数据块，以提高读取性能。\ninclude/leveldb/cache.h 定义 LevelDB 的缓存接口。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 /*同步，回调函数，引用计数，深拷贝*/ #ifndef STORAGE_LEVELDB_INCLUDE_CACHE_H_ #define STORAGE_LEVELDB_INCLUDE_CACHE_H_ #include \u0026lt;cstdint\u0026gt; #include \u0026#34;leveldb/export.h\u0026#34; #include \u0026#34;leveldb/slice.h\u0026#34; namespace leveldb { // cache的缓存接口 class LEVELDB_EXPORT Cache; LEVELDB_EXPORT Cache* NewLRUCache(size_t capacity); class LEVELDB_EXPORT Cache { public: Cache() = default; Cache(const Cache\u0026amp;) = delete; Cache\u0026amp; operator=(const Cache\u0026amp;) = delete; // 回调函数销毁缓存的内容 virtual ~Cache(); // 指向cache中的一个缓存项 struct Handle {}; virtual Handle* Insert(const Slice\u0026amp; key, void* value, size_t charge, void (*deleter)(const Slice\u0026amp; key, void* value)) = 0; virtual Handle* Lookup(const Slice\u0026amp; key) = 0; virtual void Release(Handle* handle) = 0; virtual void* Value(Handle* handle) = 0;. virtual void Erase(const Slice\u0026amp; key) = 0 virtual uint64_t NewId() = 0; virtual void Prune() {} virtual size_t TotalCharge() const = 0; }; } // namespace leveldb #endif // STORAGE_LEVELDB_INCLUDE_CACHE_H_ util/cache.cc leveldb中有一个内建的Cache接口。其中有两个保存缓存项LRUHandle的链表：\nin-use链表：无序保存着在LRUCache中且正在被client使用的LRUHandle，链表配合引用计数确保了缓存项的有效管理，避免了因多个引用导致的内存泄漏或访问无效内存。 LRU链表：按照最近使用的顺序保存当前在LRUCache中但是目前没有被用户使用的LRUHandle。LRU 链表允许快速定位需要被淘汰的项。 LRUHandle在两个链表中的切换由Ref和UnRef实现。\n####### LRUHandle\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 // 缓存项的结构体 struct LRUHandle { void* value; void (*deleter)(const Slice\u0026amp;, void* value); LRUHandle* next_hash; // 哈希表handleTable为了解决哈希冲突采用拉链法的链指针 LRUHandle* next; // 双向链表 LRUHandle* prev; size_t charge; // TODO(opt): Only allow uint32_t? size_t key_length; bool in_cache; // Whether entry is in the cache. uint32_t refs; // References, including cache reference, if present.引用计数 uint32_t hash; // Hash of key(); used for fast sharding and comparisons char key_data[1]; // Beginning of key Slice key() const { // next is only equal to this if the LRU handle is the list head of an // empty list. List heads never have meaningful keys. assert(next != this); return Slice(key_data, key_length); } }; LRUCache为了能够快速根据key来找到相应的LRUHandle，而不需要遍历链表，其还组装了一个哈希表HandleTable。LevelDB的哈希表与哈希函数都使用了自己的实现。\n####### HandleTable\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 // 可扩展哈希表 class HandleTable { public: HandleTable() : length_(0), elems_(0), list_(nullptr) { Resize(); } ~HandleTable() { delete[] list_; } LRUHandle* Lookup(const Slice\u0026amp; key, uint32_t hash) { return *FindPointer(key, hash); } // ptr为空时，新缓存项作为头部，同时elem++检查是否需要扩充空间 // ptr不为空时，采用头插法 LRUHandle* Insert(LRUHandle* h) { LRUHandle** ptr = FindPointer(h-\u0026gt;key(), h-\u0026gt;hash); LRUHandle* old = *ptr; h-\u0026gt;next_hash = (old == nullptr ? nullptr : old-\u0026gt;next_hash); *ptr = h; if (old == nullptr) { ++elems_; if (elems_ \u0026gt; length_) { // Since each cache entry is fairly large, we aim for a small // average linked list length (\u0026lt;= 1). Resize(); } } return old; } LRUHandle* Remove(const Slice\u0026amp; key, uint32_t hash) { // 理解二重*的链表 LRUHandle** ptr = FindPointer(key, hash); // ptr为当前结点地址 LRUHandle* result = *ptr; if (result != nullptr) { // *ptr就是当前结点的前一个结点，ptr为当前结点地址 // 将当前节点的前一个节点的 next_hash 更新为要删除节点的下一个节点。 *ptr = result-\u0026gt;next_hash; --elems_; } return result; } private: // The table consists of an array of buckets where each bucket is // a linked list of cache entries that hash into the bucket. uint32_t length_; // slot的数量 uint32_t elems_; // 已经使用的slot数量 LRUHandle** list_; // handleTable的bucket数组 // Return a pointer to slot that points to a cache entry that // matches key/hash. If there is no such cache entry, return a // pointer to the trailing slot in the corresponding linked list. 正常的查找操作 LRUHandle** FindPointer(const Slice\u0026amp; key, uint32_t hash) { LRUHandle** ptr = \u0026amp;list_[hash \u0026amp; (length_ - 1)]; // 这里要求找到hash和key都相同的位置，然后建立链表 // hash是因为哈希冲突，jey则可能因为版本不同的缓存或者缓存项被频繁更新 while (*ptr != nullptr \u0026amp;\u0026amp; ((*ptr)-\u0026gt;hash != hash || key != (*ptr)-\u0026gt;key())) { ptr = \u0026amp;(*ptr)-\u0026gt;next_hash; } return ptr; } // 确立新空间 // 将旧的哈希表数据重新计算复制到新的哈希表中 void Resize() { uint32_t new_length = 4; while (new_length \u0026lt; elems_) { new_length *= 2; } LRUHandle** new_list = new LRUHandle*[new_length]; memset(new_list, 0, sizeof(new_list[0]) * new_length); uint32_t count = 0; // 这里使用头插法时，二重链表中的数据反了，但是并不影响LRU的使用，因为有另外的链表实现LRU机制 for (uint32_t i = 0; i \u0026lt; length_; i++) { LRUHandle* h = list_[i]; while (h != nullptr) { // 保存下一个元素地址 LRUHandle* next = h-\u0026gt;next_hash; uint32_t hash = h-\u0026gt;hash; // 取模，找一个位置 LRUHandle** ptr = \u0026amp;new_list[hash \u0026amp; (new_length - 1)]; h-\u0026gt;next_hash = *ptr; *ptr = h; h = next; count++; } } assert(elems_ == count); delete[] list_; list_ = new_list; length_ = new_length; } }; **list硬控我一个小时）。。基础不行应该是。\n####### LRUCache\n在LRUCache的实现中，\n在Inset方法插入LRUHandle时，只会从LRU链表中逐出LRUHandle。也就是说，对于LRUCache中的每个LRUHandle，其只有如下几种状态：\n对于还没存入LRUCache的LRUHandle，不在任一链表上（显然）。 当前在LRUCache中，且正在被client使用的LRUHandle，在in-use链表上无序保存。 当前在LRUCache中，当前未被client使用的LRUHandle，在LRU链表上按LRU顺序保存。 之前在LRUCache中，但①被用户通过Erase方法从LRUCache中删除，或②用户通过Insert方法更新了该key的LRUHandle，或③LRUCache被销毁时，LRUHandle既不在in-use链表上也不在LRU链表上。此时，该LRUHandle在等待client通过Release方法释放引用计数以销毁。 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 26 27 class LRUCache { private: // 删除结点，添加结点 void LRU_Remove(LRUHandle* e); void LRU_Append(LRUHandle* list, LRUHandle* e); void Ref(LRUHandle* e); // 引用结点，从lru list转移到in use list void Unref(LRUHandle* e); // 解引用，变为0时删掉，变为1则放进lru中便于以后变成0删掉 bool FinishErase(LRUHandle* e) EXCLUSIVE_LOCKS_REQUIRED(mutex_); // 完全移除e，在此之前已经从哈希表中移除 // Initialized before use. 容量 size_t capacity_; // mutex_ protects the following state. mutable port::Mutex mutex_; size_t usage_ GUARDED_BY(mutex_); // 当前用量 // Dummy head of LRU list. // lru.prev is newest entry, lru.next is oldest entry. // Entries have refs==1 and in_cache==true. LRUHandle lru_ GUARDED_BY(mutex_); // lru链表 ，缓存项 // Dummy head of in-use list. // Entries are in use by clients, and have refs \u0026gt;= 2 and in_cache==true. LRUHandle in_use_ GUARDED_BY(mutex_); // in-use链表，无序保存着在LRUCache中且正在被client使用的LRUHandle HandleTable table_ GUARDED_BY(mutex_); // 哈希表，按照最近使用的顺序保存当前在LRUCache中但是目前没有被用户使用的LRUHandl }; ####### ShardedLRUCache\n最终实现cache的接口。SharedLRUCache中保存了若干个LRUCache，根据插入的key的哈希分配到相应的LRUCache中，每个LRUCache都有独立的锁，因此可以减少锁的争用，优化程序性能。\nShardedLRUCache通过HashSlice方法对key进行一次哈希，并通过Shard方法为其分配shard。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 private: // 实现分片的LRU缓存 LRUCache shard_[kNumShards]; port::Mutex id_mutex_; // 保护对共享资源的访问，确保线程安全 uint64_t last_id_; // 管理缓存项的唯一标识 static inline uint32_t HashSlice(const Slice\u0026amp; s) { // 计算给定对象的哈希值 return Hash(s.data(), s.size(), 0); } // 分片，位数为KNumShardBits时，hash二进制右移32-kNumShardBits能得到0---2^kNumShardBits-1的值，类似于提取前kNumShardBits位数。 // 分片能让多线程访问不同分片，减少竞争； // 同时每个分片的数据减少，减少锁的竞争；数据分布均匀，也可以避免某个分片过于繁忙，实现负载均衡 static uint32_t Shard(uint32_t hash) { return hash \u0026gt;\u0026gt; (32 - kNumShardBits); } override 是 C++11 引入的一个关键字，用于在派生类中显式地标记一个虚拟函数，表示它重写了基类中的虚拟函数。使用 override 可以提高代码的可读性，并在编译时提供更强的类型检查。\npublic就是对之前class LRUCache和自己的私有函数的简单封装。\ndb/table_cache.h 为了减少热点数据访问磁盘IO频繁导致的效率问题。leveldb在访问sstable时加上了缓存。leveldb中的缓存功能上分为两种：\nBlockCache：缓存最近使用的SSTable中的DataBlock数据 TableCache：TableCache可以认为是一个双层cache。 第一层缓存最近打开的sstable中的部分元数据，减少频繁的磁盘访问 第二层即是BlockCache，缓存了当前SSTable中的DataBlock数据。TableCache提供的Get接口能够同时查询两层缓存。 tableCache对sharedLRUCache又进行了一次封装，二者的区别主要在于key/value的类型以及cache的大小\nBlockCache：用户通过Options.block_cache配置自定义BlockCache的实现，其默认实现为8MB的SharedLRUCache。其中key/value为(table.cache_id,block.offset)-\u0026gt;(Block*)。\nTableCache：用户可通过OptionTable.max_open_file配置来自定义TableCache的大小，其默认可以保存1000个Table的信息。其key/value为(SSTable.file_number)-\u0026gt;(TableAndFile*)。\n云里雾里，在此提出问题，blockcache和tablecache在整个leveldb中起到什么样子的作用，和我们最刚开始画出的磁盘和内存交换时，磁盘中的level1level2level0有关吗。\nAnswer：\nSStable中使用LSM树存储sstable文件，位于写入过程。\nLevel 0：\n通常用于存储新写入的 SSTable 文件。 Level 0 中的文件不一定是有序的，可能存在重叠，需要在合并时进行整理（即“压缩”）。 Level 1 及以上：\n每个更高的层级（Level 1、Level 2 等）通常包含经过合并和排序的 SSTable 文件。 在这些层级中，文件通常是非重叠的，每个层级的大小和数量都有一定的限制。 同时，内存中的 BlockCache 和 TableCache 提供快速的数据访问和元数据管理，旨在提高读取过程性能，加速读取操作，减少对磁盘的访问。\n当然leveldb中不止blockcache和tablecache这两种缓存系统，还有writer buffer，布隆过滤器，快照等都属于缓存系统。\n最终整体过程是：\n写入：将Immutable Memtable从内存中写入磁盘，形成sstable，然后访问level0，此后也会通过compaction到level1和level2中。\n读取：\n接收读取请求，查询Memtable和Immutable Memtable 查询blockcache（blockcache中存储了从sstable中读取的data数据块） 如果此时还没找到，就会进入磁盘中，通过布隆过滤器定位sstable文件 如果找到，读取sstable中的datablock，否则返回没有找到。 在tablecache中\n####### tablecache.h\n1 2 3 4 5 6 7 8 private: // 通过filename找到sstable文件，filesize可以用于验证或者缓存的查找，handle**用于返回找到的缓存项 Status FindTable(uint64_t file_number, uint64_t file_size, Cache::Handle**); Env* const env_; // 指向环境对象的指针 const std::string dbname_; // 存储数据库的名称或路径 const Options\u0026amp; options_; // 对于leveldb的配置选项的引用 Cache* cache_; // 指向缓存对象的指针 在它的创建迭代器中，涉及阅读模式的选择，leveldb提供snapshot和read-only两种模式。快照的代码等这部分看完再去看。\n####### tablecache.cc\n1 2 3 4 struct TableAndFile { RandomAccessFile* file; // 将数据以sstable的格式持久化存储到磁盘上。 Table* table; // 作为一个数据结构，封装了与sstable相关的逻辑，比如读取，查找，迭代等 }; 另外主要关注两个函数，findtable和get\nfindtable会先构造key在tablecache中查找是否已经缓存了该sstable，\n如果有，直接返回缓存结构； 否则根据传入的filename和filesize通过Table::open在磁盘中找到相应大sstable，将file和table写入tableandfile结构体，放入自己的sharedLRUCache缓存中。 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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 /* * 实现了查找sstable的功能 * 缓存中查找如果失败， * 会动态打开文件创建表对象，同时插入缓存 * 最终返回状态 */ Status TableCache::FindTable(uint64_t file_number, uint64_t file_size, Cache::Handle** handle) { // 初始化 Status s; char buf[sizeof(file_number)]; EncodeFixed64(buf, file_number); Slice key(buf, sizeof(buf)); // 查找 *handle = cache_-\u0026gt;Lookup(key); if (*handle == nullptr) { // 检查缓存结果，从文件中查找 std::string fname = TableFileName(dbname_, file_number); // 构造文件名 RandomAccessFile* file = nullptr; // 初始化指针 Table* table = nullptr; s = env_-\u0026gt;NewRandomAccessFile(fname, \u0026amp;file); if (!s.ok()) { // 打开文件失败的错误处理 std::string old_fname = SSTTableFileName(dbname_, file_number); if (env_-\u0026gt;NewRandomAccessFile(old_fname, \u0026amp;file).ok()) { s = Status::OK(); } } if (s.ok()) { s = Table::Open(options_, file, file_size, \u0026amp;table); } if (!s.ok()) { // 错误处理 assert(table == nullptr); delete file; // We do not cache error results so that if the error is transient, // or somebody repairs the file, we recover automatically. } else { // 创建新对象插入缓存 TableAndFile* tf = new TableAndFile; tf-\u0026gt;file = file; tf-\u0026gt;table = table; *handle = cache_-\u0026gt;Insert(key, tf, 1, \u0026amp;DeleteEntry); } } return s; } get中则是Tablecache暴露给caller的外部方法，\n首先通过findtable打开需要的sstable，然后通过table结构体的InternalGet结构获取给定key的value。table.h是实现tablecache的关键，因此接下来会介绍table.h.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 /*从缓存中获取指定键的值*/ Status TableCache::Get(const ReadOptions\u0026amp; options, uint64_t file_number, uint64_t file_size, const Slice\u0026amp; k, void* arg, void (*handle_result)(void*, const Slice\u0026amp;, const Slice\u0026amp;)) { Cache::Handle* handle = nullptr; Status s = FindTable(file_number, file_size, \u0026amp;handle); /*TableCache 的 Get 方法能够同时查询这两层缓存。 首先，它会查找第一层缓存（元数据），获取所需的 SSTable 信息。 如果该信息存在，接着会使用这些信息查询第二层缓存（BlockCache），获取实际的数据块。 */ if (s.ok()) { // 查找成功时，从缓存句柄中获取TableAndFile对象，通过类型转换获取内部的table指针 Table* t = reinterpret_cast\u0026lt;TableAndFile*\u0026gt;(cache_-\u0026gt;Value(handle))-\u0026gt;table; // 切片，参数，回调函数 s = t-\u0026gt;InternalGet(options, k, arg, handle_result); // 获取键值对，同时调用回调函数 cache_-\u0026gt;Release(handle); // 释放缓存句柄，减少引用计数 } return s; } /*驱逐指定的sstable*/ void TableCache::Evict(uint64_t file_number) { // 创建字符数组，为文件编号编码 char buf[sizeof(file_number)]; EncodeFixed64(buf, file_number); // 从缓存中删除 cache_-\u0026gt;Erase(Slice(buf, sizeof(buf))); } include/leveldb/table.h ####### table.h\nleveldb的用户可以通过这个接口中的open方法打开sstable并且通过迭代器访问其中的数据，或者估算key在sstable中的位置等。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 private: friend class TableCache; struct Rep; // 内部实现结构体，封装table的具体实现细节 static Iterator* BlockReader(void*, const ReadOptions\u0026amp;, const Slice\u0026amp;); /*私有构造函数，接收一个指向 Rep 结构体的指针，初始化 rep_ 成员*/ explicit Table(Rep* rep) : rep_(rep) {} // Calls (*handle_result)(arg, ...) with the entry found after a call // to Seek(key). May not make such a call if filter policy says // that key is not present. // 在表中查找给定键的条目。如果找到了，将调用 handle_result 函数处理结果 Status InternalGet(const ReadOptions\u0026amp;, const Slice\u0026amp; key, void* arg, void (*handle_result)(void* arg, const Slice\u0026amp; k, const Slice\u0026amp; v)); // 读取元数据和过滤器的相关函数 void ReadMeta(const Footer\u0026amp; footer); void ReadFilter(const Slice\u0026amp; filter_handle_value); Rep* const rep_; ####### table.cc\n在InternalGet函数中，大致过程如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 Status Table::InternalGet(const ReadOptions\u0026amp; options, const Slice\u0026amp; k, void* arg, oid (*handle_result)(void*, const Slice\u0026amp;, const Slice\u0026amp;)) { // 获取indexblock的iterator Iterator* iiter = rep_-\u0026gt;index_block-\u0026gt;NewIterator(rep_-\u0026gt;options.comparator); iiter-\u0026gt;Seek(k); /*1.键的查找*/ if (iiter-\u0026gt;Valid()) { Slice handle_value = iiter-\u0026gt;value(); /*2.获取与键关联的值，这个值中存储了datablock的位置，datablock存在磁盘中，减少io次数*/ FilterBlockReader* filter = rep_-\u0026gt;filter; /*3.获取过滤器，过滤器中可以查看数据是否存在*/ BlockHandle handle; if (filter != nullptr \u0026amp;\u0026amp; handle.DecodeFrom(\u0026amp;handle_value).ok() \u0026amp;\u0026amp; !filter-\u0026gt;KeyMayMatch(handle.offset(), k)) { // 过滤器为空且键可能不匹配 // not found } else { /*4.过滤器顺利找到，根据value查找相应的数据块中的迭代器*/ Iterator* block_iter = BlockReader(this, options, iiter-\u0026gt;value()); block_iter-\u0026gt;Seek(k); /* 5.在数据块中查找key*/ if (block_iter-\u0026gt;Valid()) { // 找到时采用handle_result 处理结果 (*handle_result)(arg, block_iter-\u0026gt;key(), block_iter-\u0026gt;value()); } // 更新状态 s = block_iter-\u0026gt;status(); delete block_iter; } } if (s.ok()) { s = iiter-\u0026gt;status(); } delete iiter; return s; } } 在上面的第4步中，我们看看BlockReader的具体过程\n在open方法中,\n1 2 3 4 5 6 7 /* * 读取sstable中的footer，加载其filter block和index block的数据到内存 * 为table分配一个cache_id * 在通过Table读取其中DataBlock的数据时， * 会拼接cache_id与Block的offset拼接作为BlockCache的key。 */ Status Table::Open(const Options\u0026amp; options, RandomAccessFile* file, uint64_t size, Table** table) {...} ####### option.h\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 在你提供的代码中，LevelDB 定义了几个关键的结构体，主要包括 `Options`、`ReadOptions` 和 `WriteOptions`。以下是对这些结构体的详细介绍： ### 1. `Options` `Options` 结构体用于控制数据库的行为，包含多个参数，影响数据库的打开、性能和数据存储等。主要字段包括： - **`comparator`**：用于定义键的顺序的比较器。 - **`create_if_missing`**：如果数据库不存在，是否创建新的数据库。 - **`error_if_exists`**：如果数据库已经存在，是否报错。 - **`paranoid_checks`**：是否进行严格的数据完整性检查。 - **`env`**：与环境交互的对象（如读写文件等）。 - **`info_log`**：用于记录信息和错误的日志记录器。 - **`write_buffer_size`**：内存中写缓冲区的大小。 - **`max_open_files`**：数据库可以打开的最大文件数量。 - **`block_cache`**：用于缓存块的对象。 - **`block_size`**：每个块的近似大小。 - **`block_restart_interval`**：用于键的增量编码的重启点之间的键数量。 - **`max_file_size`**：写入文件的最大字节数。 - **`compression`**：用于块的压缩算法。 - **`zstd_compression_level`**：zstd 压缩的压缩级别。 - **`reuse_logs`**：是否在打开数据库时追加到现有的 MANIFEST 和日志文件。 - **`filter_policy`**：用于减少磁盘读取的过滤策略。 ### 2. `ReadOptions` `ReadOptions` 结构体控制读取操作的行为，包含以下字段： - **`verify_checksums`**：是否在读取时验证校验和。 - **`fill_cache`**：读取的数据是否应被缓存。 - **`snapshot`**：用于读取的快照对象。 ### 3. `WriteOptions` `WriteOptions` 结构体控制写入操作的行为，包含以下字段： - **`sync`**：是否在写入完成前将数据刷新到操作系统的缓冲区。设置为 `true` 会使写入变慢，但可以确保写入数据的持久性。 这些结构体的设计使得 LevelDB 能够灵活配置数据库的行为，以适应不同的使用场景和性能需求。 snapshot下一节再讲。\n该文先暂时这样子。\n","date":"2024-10-10T00:00:00Z","permalink":"https://sutdown.github.io/p/leveldb%E6%BA%90%E7%A0%81%E9%98%85%E8%AF%BB3-filesysytem/","title":"leveldb源码阅读3 filesysytem"},{"content":"前言 leveldb是一个写性能十分优秀的存储引擎，是典型的LSM树(Log Structured-Merge Tree)实现。LSM树的核心思想就是放弃部分读的性能，换取最大的写入能力。\nleveldb如何读写的呢\nLSM树写性能极高的原理，简单地来说就是尽量减少随机写的次数。对于每次写入操作，并不是直接将最新的数据驻留在磁盘中，而是将其拆分成（1）一次日志文件的顺序写（2）一次内存中的数据插入。leveldb正是实践了这种思想，将数据首先更新在内存中，当内存中的数据达到一定的阈值，将这部分数据真正刷新到磁盘文件中，因而获得了极高的写性能（顺序写60MB/s, 随机写45MB/s）。\n由于LSM树读性能并不好，尤其是高并发读，因此采用了布隆过滤器减少不必要的查找，布隆过滤器下一篇讲。\nleveldb的内存结构用的是跳表，该文讲讲写入时涉及到的memtablde。\n正文 memtable leveldb的一次写入操作并不是直接将数据刷新到磁盘文件，而是首先写入到内存中作为代替，memtable就是一个在内存中进行数据组织与维护的结构。memtable中，所有的数据按用户定义的排序方法排序之后按序存储，等到其存储内容的容量达到阈值时（默认为4MB），便将其转换成一个不可修改的memtable，与此同时创建一个新的memtable，供用户继续进行读写操作。memtable底层使用了一种跳表skiplist数据结构，这种数据结构效率可以比拟二叉查找树，绝大多数操作的时间复杂度为O(log n)。\n在memtable.h中\n1 2 3 4 5 6 7 8 /*这个头文件定义了LevelDB中的数据格式，包括键值对的编码方式、序列化和反序列化操作。它通常涉及到如何存储和检索数据的结构信息，比如内部的版本控制和时间戳等。*/ #include \u0026#34;db/dbformat.h\u0026#34; /*skiplist.h 定义了跳表（Skip List）数据结构。LevelDB 使用跳表作为 MemTable 的实现，以提供快速的插入、查找和删除操作。*/ #include \u0026#34;db/skiplist.h\u0026#34; /*这个头文件是 LevelDB 的主要接口定义，包括数据库的打开、关闭、读写等操作的函数原型。它提供了与用户交互的 API，用户通过这些接口来操作 LevelDB 数据库。*/ #include \u0026#34;leveldb/db.h\u0026#34; /*arena.h 提供了内存池（Memory Arena）管理的实现。它用于高效地管理内存分配和释放，减少内存碎片。LevelDB 中的 MemTable 和其他结构体会使用内存池来加速内存操作，尤其是在高频率的写入场景下。*/ #include \u0026#34;util/arena.h\u0026#34; 由于skiplist中也会用到arena.h，因此先看看这个内存管理的文件。\narena.h arena和variant的讲解\nArena是leveldb中管理内存分配的类。所有的内存分配都通过Arena申请，可以根据申请的内存大小使用不同的内存分配策略，也可以避免过多的内存碎片问题，并且在内存释放时统一使用Arena来释放，方便管理。\n先看看arena的类。\n结构 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 26 27 28 29 30 31 32 33 34 35 36 37 38 class Arena { public: Arena(); Arena(const Arena\u0026amp;) = delete; Arena\u0026amp; operator=(const Arena\u0026amp;) = delete; ~Arena(); // Return a pointer to a newly allocated memory block of \u0026#34;bytes\u0026#34; bytes.记得更新alloc_ptr的指针和alloc_bytes_remaining_的大小 char* Allocate(size_t bytes); // Allocate memory with the normal alignment guarantees provided by malloc. 字节对齐 char* AllocateAligned(size_t bytes); // Returns an estimate of the total memory usage of data allocated // by the arena. 剩余可用内存 size_t MemoryUsage() const { return memory_usage_.load(std::memory_order_relaxed); } private: char* AllocateFallback(size_t bytes); // 处理无法在内存块中分配的情况，用于分配新内存块 char* AllocateNewBlock(size_t block_bytes); // 分配新内存块 // Allocation state char* alloc_ptr_; // 当前可分配指针 size_t alloc_bytes_remaining_; // 当前分配块的剩余字节 // Array of new[] allocated memory blocks 以及分配内存块的数组 std::vector\u0026lt;char*\u0026gt; blocks_; // Total memory usage of the arena. // 利用原子存储可用内存，因为顺序不影响，因此使用的是松散模型 // TODO(costan): This member is accessed via atomics, but the others are // accessed without any locking. Is this OK? std::atomic\u0026lt;size_t\u0026gt; memory_usage_; }; 一般函数的核心是在private中，public中是对private中的包装然后向外提供接口。\nprivate中包含两个处理分配内存的函数，一个内存指针，一份分配块可用内存书，一个已用内存，还有一个存储内存块的vector\u0026lt;char*\u0026gt;数组，这个数组也跟skiplist中每个结点的char*指针是同源的。\n需要初始化的肯定是内存指针alloc_ptr_，alloc_bytes_remaining_, memory_usage_这三部分，需要析构的也就是vector\u0026lt;char*\u0026gt;中的内存，这都是在堆中分配的。\n然后便是两个私有函数的实现了。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 char* Arena::AllocateNewBlock(size_t block_bytes) { char* result = new char[block_bytes]; // 分配char* blocks_.push_back(result); // 传入数组 memory_usage_.fetch_add(block_bytes + sizeof(char*), std::memory_order_relaxed); // 调整大小 return result; } char* Arena::AllocateFallback(size_t bytes) { if (bytes \u0026gt; kBlockSize / 4) { // Object is more than a quarter of our block size. Allocate it separately // 内存比较大时，分配特定大小然后传入数组 // to avoid wasting too much space in leftover bytes. char* result = AllocateNewBlock(bytes); return result; } // We waste the remaining space in the current block. // 内存比较小时，为了避免内存碎片，会直接分配一个新的块，然后更细剩余内存和指针位置 alloc_ptr_ = AllocateNewBlock(kBlockSize); alloc_bytes_remaining_ = kBlockSize; char* result = alloc_ptr_; alloc_ptr_ += bytes; alloc_bytes_remaining_ -= bytes; return result; } 有个问题，这个arena是为链表中每个结点的char*还是一个结点每层的char*\n破案，肯定不会是每个结点的char*，这个在node中是自己存储了的，那肯定是后者，这个也能从skiplist的newNode()函数中看出来。\n1 2 3 4 5 6 7 template \u0026lt;typename Key, class Comparator\u0026gt; typename SkipList\u0026lt;Key, Comparator\u0026gt;::Node* SkipList\u0026lt;Key, Comparator\u0026gt;::NewNode( const Key\u0026amp; key, int height) { char* const node_memory = arena_-\u0026gt;AllocateAligned( sizeof(Node) + sizeof(std::atomic\u0026lt;Node*\u0026gt;) * (height - 1)); return new (node_memory) Node(key); } 这个其实也就是leveldb的内存池。特点如下\n内存池特点 提供性能，可以批量分配内存，同时内存块连续分配，缓存利用率提高 降低内存碎片，内存大时直接分配，内存小时使用现有内存块 可以自动释放分配的内存块，实现RALL功能，便于实现内存管理中的构造和析构 原子操作可以保证线程安全。 random.h 1 2 3 4 5 uint32_t seed_; // 存储种子的值 uint32_t Next(); // 生成并返回下一个随机数。使用线性同余法（LCG）算法更新种子值并计算随机数。 uint32_t Uniform(int n); // bool OneIn(int n); // 以1/n的概率返回true，其余时间返回false uint32_t Skewed(int max_log); // 返回一个带偏斜的随机数。首先从范围[0, max_log]中均匀选择一个基数，然后生成该基数位数的随机数，从而产生在[0, 2^max_log - 1]范围内的数字，具有对小数字的指数偏好。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 uint32_t Next() { static const uint32_t M = 2147483647L; // 2^31-1 static const uint64_t A = 16807; // 乘数 uint64_t product = seed_ * A; // 计算乘积 // 计算新的种子值 seed_ = static_cast\u0026lt;uint32_t\u0026gt;((product \u0026gt;\u0026gt; 31) + (product \u0026amp; M)); // 确保新的种子在有效范围内 if (seed_ \u0026gt; M) { seed_ -= M; } return seed_; // 返回生成的随机数 } // 不直接调用系统函数的部分原因在于，性能优化，减少外部依赖 uint32_t Skewed(int max_log) { return Uniform(1 \u0026lt;\u0026lt; Uniform(max_log + 1)); } // 这个通过两次的uniform，达到了负载均衡中的偏斜的目的，生成的随机数更小。 skiplist.h skiplist\nLevelDB 中对 SkipList 的实现增加了多线程并发访问方面的优化的代码，提供以下保证：\nWrite：在修改跳表时，需要在用户代码侧加锁。 Read：在访问跳表（查找、遍历）时，只需保证跳表不被其他线程销毁即可，不必额外加锁。 跳表的提出，为William Pugh在1990年提出，论文为skiplist.\n文章中说到：\n跳表是一种可以取代平衡树的数据结构。跳表使用概率均衡而非严格均衡策略，从而相对于平衡树，大大简化和加速了元素的插入和删除。\n概率均衡是什么？这个问题后面解决，我们先看看跳表是什么\n简单来说：跳表就是带有额外指针的链表。\nskiplist的结构 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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 // (省略版) class SkipList{ private: struct Node; public: // 接收比较器和内存池 explicit SkipList(Comparator cmp, Arena* arena); // 禁止拷贝和构造 SkipList(const SkipList\u0026amp;) = delete; SkipList\u0026amp; operator=(const SkipList\u0026amp;) = delete; void Insert(const Key\u0026amp; key); bool Contains(const Key\u0026amp; key) const; class Iterator{...}; private: // 有些函数 Comparator const compare_; Arena* const arena_; // Arena used for allocations of nodes Node* const head_; // 链表 std::atomic\u0026lt;int\u0026gt; max_height_; Random rnd_; // Read/written only by Insert(). }; struct SkipList\u0026lt;Key, Comparator\u0026gt;::Node { explicit Node(const Key\u0026amp; k) : key(k) {} Key const key; // 适用于数据一致性 Node* Next(int n) { assert(n \u0026gt;= 0); return next_[n].load(std::memory_order_acquire); } void SetNext(int n, Node* x) { assert(n \u0026gt;= 0); next_[n].store(x, std::memory_order_release); } // 不保证其它操作顺序，保护共享资源，适用于性能优先 Node* NoBarrier_Next(int n) { assert(n \u0026gt;= 0); return next_[n].load(std::memory_order_relaxed); } void NoBarrier_SetNext(int n, Node* x) { assert(n \u0026gt;= 0); next_[n].store(x, std::memory_order_relaxed); } private: // 原子存储下一个结点的指针 std::atomic\u0026lt;Node*\u0026gt; next_[1]; }; 初始结点数目为n，加入的额外指针数也大致为n\n感觉看了结构还是不太明了，再看看构造：\n1 2 3 4 5 template \u0026lt;typename Key, class Comparator\u0026gt; typename SkipList\u0026lt;Key, Comparator\u0026gt;::Node* SkipList\u0026lt;Key, Comparator\u0026gt;::NewNode(const Key\u0026amp; key, int height) { char* const node_memory = arena_-\u0026gt;AllocateAligned(sizeof(Node) + sizeof(std::atomic\u0026lt;Node*\u0026gt;) * (height - 1)); return new (node_memory) Node(key); } **对比平衡树：**跳表在保证同样查询效率的情况下，使用了一种很巧妙的转化，大大简化了插入的实现。我们不能保证所有的插入请求在 key 空间具有很好地随机性，或者说均衡性；但我们可以控制每个节点其他维度的均衡性。比如，跳表中每个节点的指针数分布的概率均衡。\n插入节点的时间复杂度为查找的时间复杂度 O(log2n)，与修改指针的复杂度 O(1) 之和，即也为 O(log2n)，删除过程和插入类似，也可以转化为查找和修改指针值，因此复杂度也为 O(log2n)。\n跳表对外提供的接口\n1 2 3 4 5 6 7 8 9 10 11 12 // 插入 key 到跳表中. // 要求: 不能够插入和跳表中的节点判等的 key. template \u0026lt;typename Key, class Comparator\u0026gt; void SkipList\u0026lt;Key, Comparator\u0026gt;::Insert(const Key\u0026amp; key) // 当且仅当跳表中有和给定 key 判等的节点才返回真. template \u0026lt;typename Key, class Comparator\u0026gt; bool SkipList\u0026lt;Key, Comparator\u0026gt;::Contains(const Key\u0026amp; key) const // 返回给定跳表的迭代器 template \u0026lt;typename Key, class Comparator\u0026gt; inline SkipList\u0026lt;Key, Comparator\u0026gt;::Iterator::Iterator(const SkipList* list) inline函数的作用\n内部链接：所有匿名命名空间里的东西（哪怕声明成extern） + 标记成static的变量、变量模板、函数、函数模板 + 不是模板不是inline没有volatile或extern修饰的常量（const和constexpr）。（符号不能被编译单元看见，所以可能出现多个不同的实体）\n外部链接：非static函数、枚举和类天生有外部链接，除非在匿名命名空间里 + 排除内部链接规定的之后剩下的所有模板\n省去参数传递，和函数调用，直接内联到原位置。 c++17新加了通常情况下模板变量和inline变量是外部链接的规定，因此加上inline解决了模板变量常量链接性上的问题 指令重排 详细可见这篇文章深入理解C++11：原子类型和原子操作\nstd::memory_order_relaxed：不对重排做限制，只保证相关共享内存访问的原子性。（只关心操作的原子性不关心操作顺序） std::memory_order_acquire: 用在 load 时，保证同线程中该 load 之后的对相关内存读写语句不会被重排到 load 之前，并且其他线程中对同样内存用了 store release 都对其可见。（适用于加载，读取资源能看到之前的写入） std::memory_order_release：用在 store 时，保证同线程中该 store 之后的对相关内存的读写语句不会被重排到 store 之前，并且该线程的所有修改对用了 load acquire 的其他线程都可见。（适用于确保写入之后，对共享数据的修改能够被其它线程看到） skiplist的查找 见该图，有几个关键函数：代码还挺容易看懂的，这里直接给出来就不解释了。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 template \u0026lt;typename Key, class Comparator\u0026gt; typename SkipList\u0026lt;Key, Comparator\u0026gt;::Node*SkipList\u0026lt;Key, COmparator\u0026gt;::FindGreaterOrEqual(const Key\u0026amp; key, Node** prev) const{ Node* x = head_; int level = GetMaxHeight() - 1; while(true){ Node* next = x-\u0026gt;Next(level); if(KeyIsafterNode(key,next)){ x = next; }else{ // 存储了每一行中在key前面的那个那个函数 if(prev != nullptr)prev[level] = x; if(level == 0){ return next; }else{ level--; } } } } template \u0026lt;typename Key, class Comparator\u0026gt; bool SkipList\u0026lt;Key, Comparator\u0026gt;::KeyIsAfterNode(const Key\u0026amp; key, Node* n) const { // null n is considered infinite return (n != nullptr) \u0026amp;\u0026amp; (compare_(n-\u0026gt;key, key) \u0026lt; 0); } // 后面还有个SkipList\u0026lt;Key, Comparator\u0026gt;::FindLessThan(const Key\u0026amp; key) const，感觉和上面那个思路差不多，都是遍历。 // findlast同理，找x-\u0026gt;Next为空的即可 都写到这了，感觉iterator还是可以拿出来说说，虽然代码不难，不过贵在为skiplist量身定制。\n哦不对，还是没必要，它里面就是一些Next(), Prev()之类的函数，然后封装一下链表的指针和一些其它函数找到的值，都不怎么长，没什么说的必要。\n在SkipList类里面设置一个iterator真妙啊，这样无论是找前面结点后面结点，开头结点街尾节点等都可以直接调用到函数，结构清晰，思路满分。\n作者唠嗑：\n有种好奇妙的感觉，差不多一个月前还对很多东西一无所知，当时随便写了些跳表，json之类的小项目，不过那会都是第一次接触，看的我可迷茫，今天在leveldb里面又看到跳表了，两次的实现方式不怎么一样。我记得之前的优点在于使用了c++17之类的新特性，leveldb好像是14年开源的，所以当时写的时候就还没怎么用上。这个skiplist的优点在于自己定制了iterator和arena内存管理，一个月前看的那个都还是现成的。\n而且iterator和arena这两个和当初学mytinystl源码里的iterator和allocator有点点相似，不过明显这个简单多了，stl里面都有了一千行左右貌似，这才一两百行）。\n知识之间产生联系的感觉实在是太好了。\n接口：Insert，Contains 铺垫完了，最后两个接口实现这个skiplist就结束了。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 template\u0026lt;typename Key, class Comparator\u0026gt; void SkipList\u0026lt;Key, Comparator\u0026gt;::Insert(const Key\u0026amp; key) { Node* prev[KMaxHeight]; //找大于或等于 key 的第一个节点， //并将每行的前一个节点存储在 prev 数组中。 Node* x = FindGreaterOrEqual(key, prev); assert(x == nullptr || !Equal(key, x-\u0026gt;key)); // 随机化高度，使结点在不同层中的 int height = RandomHeight(); if (height \u0026gt; GetMaxHeight()) { for (int i = GetMaxHeight(); i \u0026lt; height; i++) { prev[i] = head; } max_height_.store(height, std::memory_order_relaxed); } x = NewNode(key, height); for (int i = 0; i \u0026lt; height; i++) { // 优先性能，更新了指针 // 优先性能 x-\u0026gt;NoBarrier_SetNext(i, prev[i]-\u0026gt;NoBarrier_Next(i)); // 优先数据一致性 prev[i]-\u0026gt;SetNext(i, x); } } 似懂非懂，再看看图。\ncontain简单。\n1 2 3 4 5 6 7 8 9 template \u0026lt;typename Key, class Comparator\u0026gt; bool SkipList\u0026lt;Key, Comparator\u0026gt;::Contains(const Key\u0026amp; key) const { Node* x = FindGreaterOrEqual(key, nullptr); if (x != nullptr \u0026amp;\u0026amp; Equal(key, x-\u0026gt;key)) { return true; } else { return false; } } cy，可以再了解一下它的定制random随机机制，放前面和arena一块吧。\n核心数据结构了解清楚了，那就直接上memtable了\nmemtable.h 看着两百多行还挺少的，结果看懵了，画张图我再理解一下：\n1 2 class InternalKeyComparator; class MemTableIterator; 先看到两个没定义的类，然后找了一下，在dbformat.h文件中。\ndbformat.h：主要用于定义和管理数据在存储中的组织方式。其中包含：\n键值对格式，key，value 内部表示，LevelDB使用一种特殊的内部格式来管理和存储数据，例如，如何将键值对组织在 SSTable 文件中，以便于高效查找和迭代。 dbformat可能涉及到版本信息，确保不同版本的LevelDB能够正确处理和解释数据。这对于数据迁移和升级是至关重要的。 在写入和读取过程中，dbformat也帮助管理数据的切分和合并，确保性能和存储效率。 dbformat在LevelDB中扮演着核心角色，它确保数据在存储和检索过程中的一致性和高效性，支持数据库的基本功能与性能优化。涉及的函数层面较多，因此仅在其它函数提到时去看，不单独讲解。\nmentable class 看上图，大概知道memtable中有三个类，从mentable class讲起。\n同样，我们先看这个类的private part:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 private: friend class MemTableIterator; friend class MemTableBackwardIterator; // 迭代器可以直接访问 /* 这里可以想到一个问题，在skiplist中，是将迭代器作为skiplist内嵌的一个类，但是这里是采用友元的方式，那么如何评价这两种方式？ 1.友元可以保持清晰的接口，将迭代器和mentable的实现分配，是代码易于维护，同时如果其它类的迭代器也可以用这个，那么可以直接被复用。但是友元会增加类之间的依赖性，设计更加复杂。 2.内嵌迭代器比较适用于迭代器和类的关系极其紧密，并且迭代器功能简单的时候。 所以整体来说，迭代器逻辑复杂并且和类相对独立，建议友元；迭代器逻辑简单并且和类紧密相关，建议内嵌。 */ struct KeyComparator { const InternalKeyComparator comparator; // 定义键的比较规则 explicit KeyComparator(const InternalKeyComparator\u0026amp; c) : comparator(c) {} int operator()(const char* a, const char* b) const; // 重载实现具体逻辑 }; typedef SkipList\u0026lt;const char*, KeyComparator\u0026gt; Table; // 存储类型，比较类型 ~MemTable(); // 设计的目的是为了确保 MemTable 的对象只能通过调用 Unref() 方法来删除，防止外部代码直接删除对象，确保内存管理的安全性。 KeyComparator comparator_; int refs_; // 引用计数，管理memtable的生命周期 /* 类似于shared_ptr，当有多个组件需要同时访问memtable实例时 使用引用计数可以安全的共享memtable，同时也避免了重复删除，实现了可靠的内存管理 */ Arena arena_; // 内存池 Table table_; // 键值对 public的接口暂时放放，然后回去看看两个友元的类的实现。\nMemTableIterator class 这个迭代器类大多数是对SkipList中的Iterator进行重写override，实际上变化不大，这个类的主要作用是修改构造函数中的数据，以更好的适配memtable。\n它的私有成员包括一个实际的迭代器和一个临时存储编码后的键。\n1 2 3 private: MemTable::Table::Iterator iter_; std::string tmp_; // For passing to EncodeKey 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 26 public: explicit MemTableIterator(MemTable::Table* table) : iter_(table) {} MemTableIterator(const MemTableIterator\u0026amp;) = delete; MemTableIterator\u0026amp; operator=(const MemTableIterator\u0026amp;) = delete; ~MemTableIterator() override = default; bool Valid() const override { return iter_.Valid(); } /*这里，为什么传入Slice的类型，然后需要对K进行编码*/ // 将Seek方法的输入键K进行编码，然后用编码后的键更新迭代器位置 // 传入切片的原因应该是本身我们封装的key的值太多，避免不必要的拷贝，提供需要的值， // 后面的encode就是调整为合适的格式 // 不过具体还是要看看代码 void Seek(const Slice\u0026amp; k) override { iter_.Seek(EncodeKey(\u0026amp;tmp_, k)); } void SeekToFirst() override { iter_.SeekToFirst(); } void SeekToLast() override { iter_.SeekToLast(); } void Next() override { iter_.Next(); } void Prev() override { iter_.Prev(); } Slice key() const override { return GetLengthPrefixedSlice(iter_.key()); } Slice value() const override { Slice key_slice = GetLengthPrefixedSlice(iter_.key()); return GetLengthPrefixedSlice(key_slice.data() + key_slice.size()); } Status status() const override { return Status::OK(); } 首先如果想要明白slice，先要搞清楚slice的对象，\n1 2 typedef SkipList\u0026lt;const char*, KeyComparator\u0026gt; Table; // 存储类型，比较类型 Table table_; // 键值对 然后slice介绍\nslice其实就是对整体的一个切片，memtable的逻辑如下：\n其中包含对add和get函数的解释。\n1 2 3 4 5 6 7 static Slice GetLengthPrefixedSlice(const char* data) { // 取出前缀中的长度，然后移动指针 uint32_t len; const char* p = data; p = GetVarint32Ptr(p, p + 5, \u0026amp;len); // +5: we assume \u0026#34;p\u0026#34; is not corrupted return Slice(p, len); } 切片的存在能够更好的对memtable结构中的某个部分进行处理。\nInternalKey是SkipList中Node的默认排序依据。LevelDB中SkipList的默认排序是通过leveldb::InternalKeyComparator实现的，其声明与实现在db/dbformat.h与db/dbformat.cc中。\nInternalKeyComparator的Compare方法按照如下优先级，依次对InternalKey进行排序：\n按照UserKey升序排序； 按照SequenceNumber降序排序； 按照ValueType降序排序（由于SequenceNumber已经足以对Key排序，因此这条规则永远不会用到）。 通过InternalKeyComparator，SkipList可以保证对于同一key（UserKey），新的操作永远在旧的操作的前面。因此，只要找到key（UserKey）在SkipList中第一次出现的位置，即可保证得到的是该key最新的版本。\n这段是在另外一个地方看到的，不过在memtable.cc和memtable.h的两个代码中，唯一对comparator的使用就是判断两个切片相等），至于sequenceNumber更是从没出现过，cy，看看后面会不会解决这个问题。\n这篇到这就结束了，本来还看的云里雾里，最后从add函数和构造函数彻底理清了memtable的结构，然后看其他的就得心应手了。\n其中涉及的知识点有，切片slice，跳表skiplist，内存池，随机数，迭代器等，还有一些细节，比如删除时并不是直接删除而是改变标记一段时间后删除（这个操作不是在本文提到的代码中实现），为什么需要引入slice，迭代器和类之间的关系，随机数的倾斜的实现和意义等等等等。\n其实就，重在见识了一下优美的代码。一个复杂的类由各个不同的部分组成，结合一些适当的语法，逻辑清晰，结构严密，多学习，希望我以后也能写的出来）。\n","date":"2024-10-02T00:00:00Z","permalink":"https://sutdown.github.io/p/leveldb%E6%BA%90%E7%A0%81%E9%98%85%E8%AF%BB2-memtableskiplist/","title":"leveldb源码阅读2 memtable(skiplist)"},{"content":"小时候，对事情都只有些许模糊的认知，每天一块钱，还能悄悄留五毛和我的弟弟傍晚在回家的路上买点零食，比如一毛的糖果或者辣条；也会有几个好友，在某个大石头附近当作一个秘密基地，那时网络还不怎么发达，虫儿飞，荷塘月色就是那时接触的最多的音乐。\n13年，是我人生的第一个转折点。\n那一年，从乡下的小学校去到了市里私立小学，同村那年因为学校的招生，去了三四个人吧，后来陆续的，有因为学费，也有因为跟不上各种原因，留下的只有我一个。\n那几年说不上快乐，比如因为想家和不适应基本上每天都在哭，比如因为并不标准的普通话总是遭人调侃，比如因为性子比较温和内向很难招架来自外界的恶意等等。我应该还算聪明吧，毕竟去了不久之后成绩这方面赶的很快。生活方面就一言难尽了，糟糕的宿舍关系，小团体，自下而上，老师的师德同样不怎么样，我几乎忘记那几年怎么过去的了。\n许是我呆吧，对于外界的好坏总是反应比较慢的，经过那几年也没有朝着坏的方向发展，反倒是更坚韧了。\n16年，我刚上初中。\n初中三年碰见的人和老师都是极好的。\n成绩，生活一路顺风顺水。连最后的中考和预录取得的成绩都是极好的。认识的很多同学直到现在都保留着联系，有时候再次见面时聊着这些年，或者聊聊过去，直观带来的感觉都是很好的。\n那时候发生过很多，比如六班和八班的老师，六班八班几名共同的同学，八班时的宿舍，八班时交到的朋友等等这些是迄今为止最难以忘却的回忆。\n19年，高中了，我走了三年的下坡路。从初进校时的七八十名，到高考的两三百名。\n网课的一年半大概是在疯狂的玩中度过的，高三那年很想提升上去，一整年的挣扎无果。有的事一直不太敢讲，后来跟有些高中同学聊的时候，发现原来很多人都有那样子的想法，“我曾经也想过一了百了”。直到高考成绩出来后，虽然很差，不过我许是真的释怀了。接受自己去到了靠南边的一个末流211，至此，理想化为灰烬。\n高中时连班级同学都不太熟悉，宿舍关系也比较一般，最后还在联系的之后当年的一个前桌和一个舍友，联系频率比较低。\n最后高中的一切尘埃落地的时候，我退了班群，也从未参加过一次高中同学聚会，这段回忆基本彻底被我抛弃了。\n22年，上天真是眷顾我，但也不知这眷顾是好是坏。\n释怀意味着看开，不意味着我放弃生活。绩点，班委，校级组织，竞赛，志愿各个方面我都参与了，无论怎么样，多尝试些，多见识些，才能知到自己真正想要的是什么。那年的气运实在太好，更方面都算得上小有成绩，甚至于实现了一个极小概率的跨越。\n24年，大三上了，大二一整年已经消磨了大一时对于大学的热情了。\n不过我觉得已经在形成了一个自己能够自洽的逻辑了。\n朝前走，好好生活，\n在完全了解自己内心的情况下，顺应着自然发展走自己的路。\n我认为这就是当前我所认知的最好的生活节奏了。\n我一定始终会保持一份认真的生活态度。\n普通的小镇做题家，走着一条起起伏伏道路，不断的寻找生活的意义。从前受困于农村和城市的差距，现在受困于学校和社会的变化，看得出来有在往前走了，这就够了。\n看看这些天吧，\n在无趣的生活里寻找着琐碎的快乐。\n","date":"2024-09-30T00:00:00Z","permalink":"https://sutdown.github.io/p/%E6%99%AE%E9%80%9A%E4%BA%BA%E7%9A%84%E8%87%AA%E8%BF%B0/","title":"普通人的自述"},{"content":"引言 一直听说这个项目的质量很高，终于来见识一下了。今天剩余时间不多，重在理清以下两个问题就行。\n为什么会出现leveldb，谷歌怎么想到实现它的，它的作用是什么。 leveldb的代码结构是什么样子，涉及到哪些熟悉或者不熟悉的知识点。 正文 Leveldb的特点 levelDb是能够处理十亿级别规模Key-Value型数据持久性存储的C++ 程序库。官方网站报道其随机写性能达到40万条记录每秒，而随机读性能达到6万条记录每秒。总体来说，LevelDb的写操作要大大快于读操作，而顺序读写操作则大大快于随机读写操作。\nleveldb是一个持久化存储的KV系统，Redis是一种内存型的KV系统，因此leveldb对内存消耗少，大部分数据存储在磁盘上。\nleveldb存储数据时，按照记录的key值有序存储，相邻的key值在存储文件中以此顺序存储，而应用可以自定义key大小笔记函数，leveldb可以按照用户定义的比较函数存储记录。\n对redis了解过少，需要看看。\n推荐文章：既生 Redis 何生 LevelDB ？\nRedis是一个基于内存的键值存储系统，支持持久化，复制，事务等功能。它的数据存储在内存中，读写性能很高。常用于缓存，消息队列等场景。\nRedis 和持久层数据库之间的数据一致性是由应用程序自己来控制的。应用程序会优先去缓存中获取数据，当缓存中没有数据时，应用程序需要从持久层加载数据，然后再放进缓存中。当数据更新发生时，需要将缓存置为失效。\nleveldb是一个持久化存储的KV系统，使用SSD硬盘存储。具有高性能，稳定性好等特点，适用于大规模数据存储和快速查询的场景。\nLevelDB 将 Redis 缓存和持久层合二为一，一次性帮你搞定缓存和持久层。\nleveldb的操作接口和大多数KV系统一样，基本操作包括写记录，读记录，针对多条操作的原子批量操作等。\nleveldb另外支持数据快照（读取操作不受写操作影响，可以在读时始终看到一致的数据），数据压缩（减少存储空间，增快io效率）等操作。\nLeveldb的结构 ####### 静态结构\nleveldb的静态结构主要有六个部分：\n内存中：MemTable, Immutable MemTable\n磁盘上：Current, Manifest, log, SSTable\nleveldb是一种基于operation log的文件系统，是Log-Structured-Merge Tree的典型实现.\n当op log文件大小超过限定值时，就定时做check point。Leveldb会生成新的Log文件和Memtable，后台调度会将Immutable Memtable的数据导出到磁盘，形成一个新的SSTable文件。SSTable就是由内存中的数据不断导出并进行Compaction操作后形成的，而且SSTable的所有文件是一种层级结构，第一层为Level 0，第二层为Level 1，依次类推，层级逐渐增高，这也是为何称之为LevelDb的原因。\n####### 基本操作\n**写入操作：**当应用写入一条Key:Value记录的时候，LevelDb会先往log文件里写入，成功后将记录插进Memtable中，只涉及一次磁盘顺序和一次内存写入。\n####### 文件夹\ndb/ 文件夹：这是 LevelDB 的核心部分，包含数据库的主要功能，如数据存储、查找、更新、删除等。重点阅读 db_impl.cc、version_set.cc 等文件，这些文件处理数据库的核心操作。\ntable/ 文件夹：处理 SSTable 文件（LevelDB 的数据格式）。table 模块管理数据的写入和读取，重要文件如 block.cc、table.cc。\nutil/ 文件夹：包含工具类和一些辅助功能的实现，如 env.cc 处理文件系统和操作系统的接口，coding.cc 包含编码和解码逻辑。\nport/ 文件夹：处理跨平台的细节实现，主要是系统相关的接口。如果不是做跨平台开发，可以略读。\nLeveldb的基础知识 Leveldb内部通过双向链表实现了一个标准版的LRUCache，内部自己实现了hashtable skip list是实现memtable的核心数据结构，memtable的KV数据都存储在skip list中 log和LSM树 util中的Arena内存池，CRC32，Random等 基本数据结构：slice，布隆过滤器，LRU sum 大致看了以下，有几篇文章还挺不错的，估计先按照别人博客的思路走。（果然经典源码肯定会有大佬的博客分析，太赞了）\n1.leveldbhandbook\n评价：各部分知识点讲解很全面，但是很少把源码拿出来分析，重在讲解思路\n2.leveldb源码阅读 - 一只安静的猫\n评价：知识不全面，但是整体框架很好，也联系了源码。\n3.深入浅出leveldb\n源码和图片都有，但是花里胡哨的字体代码排版），也可以看看。\n4.漫谈leveldb数据结构\n只讲了leveldb的数据结构，可供参考。\n最终决定自底而上看。因为还没看源码，先确立个大致路线，后面可能会有适当变化\ninclude文件夹下面的是公共接口，db，table，util文件夹下的为leveldb的底层实现\nslice — log(LSM) — memtable(skiplist) — sstable(Bloom Filter) — cache(LRU) — 基本操作(iterator) — compaction — read\u0026amp;write — version\n参考资料： code : github - google leveldb\n阅读笔记：\nLevelDB 源码分析「一、基本数据结构」\nLeveldb代码阅读笔记 - codedump\n数据分析与处理之二（Leveldb 实现原理） - Haippy - 博客园 (cnblogs.com)\nCppGuide/articles/leveldb源码分析 at master · balloonwj/CppGuide · GitHub\nleveldb-handbook — leveldb-handbook 文档\n扩展\nReveldb 与 Kyoto Tycoon 性能对比(一) - Haippy - 博客园 (cnblogs.com)\n","date":"2024-09-25T00:00:00Z","permalink":"https://sutdown.github.io/p/leveldb%E6%BA%90%E7%A0%81%E9%98%85%E8%AF%BB1-%E5%9F%BA%E6%9C%AC%E7%BB%93%E6%9E%84/","title":"leveldb源码阅读1 基本结构"},{"content":"MyTinySTL [!NOTE]\n学习基础：C++基础知识，数据结构和算法，C++新特性，模板元编程\n参考书籍：STL源码剖析\n根据阅读代码方法，文章会重点说明1. 各代码间的逻辑；2. 各模块的实现方式（细化，了解流程语法）\nSTL源码剖析 STL六大组件： containers，algorithm，\n**iterator：**扮演容器和算法之间的胶合剂，是所谓的“泛型指针”。\nfunctors：行为类似函数，可作为算法的某种策略\nadapters：修饰容器或者仿函数或者迭代器接口的东西\nallocators：负责空间配置和管理\n代码结构： allocator和iterator这部分有个作者专栏也讲了，可以看看，链接如下：MyTinySTL源码学习 - LetMETalK的专栏 - 掘金 (juejin.cn)\nallocator：主要用于空间的配置和清理 包括了 allocator 和 constructor，分别定义在 allocator.h 和 construct.h 中。 allocator 负责空间的配置与回收，定义了一个类 mystl::alloc 用于管理内存，定义在 alloc.h 。 constructor 负责对象的构造与析构，对应两个全局函数： construct 和 destroy。\nallocator是一种特定的内存模型，将对内存的所求转为对内存的调用。用于实现内存的分配和挥手以及对象的构造和销毁。\n1 2 3 allocator.h --- construct.h --- type_traits.h --- iterator.h --- util.h --- type_traits.h 流程：\nallocator.h 这个是SGI标准的空间配置器，一般不建议使用，只要是因为效率不佳。\n包括在源码中，也只是对::operator new和::operator delete做一层薄薄的包装。\n分配内存allocate 释放内存deallocate 构造对象construct 销毁对象destory 采用::operator new和::operator destory进行内存的分配和释放（全局的operator new和operator destory函数，用于分配内存和销毁内存）\n采用mystl::construct和mystl::destory进行对象的构造和销毁（mystl是改代码设置的命名空间）\n####### 完美转发，forward\n1 2 3 4 5 6 7 8 9 template \u0026lt;class T\u0026gt; template \u0026lt;class ...Args\u0026gt; void allocator\u0026lt;T\u0026gt;::construct(T* ptr, Args\u0026amp;\u0026amp; ...args) { // 可变模板参数的通用引用，不确定是左值还是右值 /*完美转发，保留参数的值类别（左值或右值）， 从而确保在调用目标函数时能正确地传递原始参数的语义。*/ mystl::construct(ptr, mystl::forward\u0026lt;Args\u0026gt;(args)...); } ####### 右值引用T\u0026amp;\u0026amp;，移动语义move\n1 2 3 4 5 6 7 template \u0026lt;class T\u0026gt; void allocator\u0026lt;T\u0026gt;::construct(T* ptr, T\u0026amp;\u0026amp; value) { /*T\u0026amp;\u0026amp;是右值引用，表示找打一个可以移动的对象，窃取临时对象的资源 move是一种显示的移动语义，可以安全的将value的值转移，不需要额外拷贝*/ mystl::construct(ptr, mystl::move(value)); } construct.h 该头文件中包含了对allocator.h中关于对对象的构造和销毁的两个函数的实现。\n**construct：**在已经分配的内存上构造对象\ndestory：\n该函数有两个版本，1.直接释放内存 2.释放first和last之间的内存\n同时注意销毁时要区分是平凡析构函数还是非平凡析构函数is_trivially_destructible，前者编译器自动调用，后者需要手动调用。\nutil.h 包含一些通用的工具，比如move，forward，swap等函数\n####### move\n1 2 3 4 5 6 7 8 9 /*自定义实现move，也就是将左值或者右值转换层右值引用，从而启动移动语义*/ template \u0026lt;class T\u0026gt; typename std::remove_reference\u0026lt;T\u0026gt;::type\u0026amp;\u0026amp; move(T\u0026amp;\u0026amp; arg) noexcept { /*noexcept表明这个函数不会抛出异常， 因为move这样的函数常用于性能优化，需要保证不会因为异常带来额外开销*/ /*强制类型转换将传入的对象视为右值，允许对象移动，而不是复制*/ return static_cast\u0026lt;typename std::remove_reference\u0026lt;T\u0026gt;::type\u0026amp;\u0026amp;\u0026gt;(arg); } ####### forward\n1 2 3 4 5 6 7 8 /*完美转发： 根据传入参数的类型，保持左值为左值，右值为右值*/ template \u0026lt;class T\u0026gt; T\u0026amp;\u0026amp; forward(typename std::remove_reference\u0026lt;T\u0026gt;::type\u0026amp; arg) noexcept { // 折叠，转发引用 return static_cast\u0026lt;T\u0026amp;\u0026amp;\u0026gt;(arg); } ####### swap\n移动语义和完美转发实现tmp\n####### 这份代码后边部分就是跟container有关，因此这里不写，见后\ntype_traits.h 1.定义了编译器常量结构体，用他的类型别名表示布尔常量，实现了编译器常量的类型封装，有助于写出安全高效的代码。\n编译器常量不可变 类型萃取，模板特化 条件编译 静态类型检查 2.通过模板特化机制定义一个类型特征 is_pair，用于判断一个类型是否为 mystl::pair 类型。\n这种类型特征和模板特化机制，您可以在编译期检测类型特性并做出相应的决策。这在模板编程中非常有用，可以使得模板代码更加灵活和强大，同时提高类型安全性。\nallo.h 该部分对于空间的配置分为两部分，一级配置器和二级配置器。\n一级配置器处理大且不频繁的内存分配需求，一般采用malloc或者直接调用系统接口。\n二级配置器处理小块内存分配，采用内存池和freelist机制，见下图。\n1 2 3 4 5 6 /*union 的使用使得同一个内存块可以在不同状态下（空闲或已分配）分别作为指向下一个块的指针或存储数据的区域，实现了内存的高效管理。*/ union FreeList { union FreeList* next; // 指向下一个区块 char data[1]; // 储存本块内存的首地址 }; Q：为什么要区分一级二级配置器，一级中的malloc也能分配小内存啊？\nA：malloc的设计是通用的，它需要处理不同大小的内存块，并且申请内存时可能会调用操作系统的内存管理接口，比如brk，mmap等，这些调用会降低程序的性能，并且小块可能导致内存碎片。\n（该图仅供参考理解，不够准确）\nSGI对alloc.h的设计哲学为：\n向system heap要求空间 考虑多线程的状态 考虑内存不足的应对措施 考虑过多小型区块可能造成的内存碎片 [!NOTE]\n这部分的源码没有考虑多线程的状态，可以加些锁或者互斥量之类的。\n同样，项目中的aloc.h这部分并没有考虑一二级配置器的区分，而是写在一起。并且没有考虑一级配置器失败后的处理。\n剖析一级配置器 一级配置器主要采用malloc(), free(), realloc()等c函数执行实际的内存配置，释放，重配置等操作。实现类似C++ new-handler的机制，但不是使用::operator new配置的内存。\n如果一级配置器不成功，该调用omm_malloc()和omm_realloc()，两者都有内循环，不断调用“内存不足处理例程”，期望在某次调用之后获取足够的内存完成任务。两个函数的逻辑相同，下面以其中一个为例：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 void * _malloc_alloc_template\u0026lt;inst\u0026gt;::omm_malloc(size_t n) { void (* my_malloc_handler)(); void *result; for(;;){ // 不断尝试释放配置，再释放再配置 // 调用处理例程，企图释放内存 // 再次尝试配置内存 my_malloc_handler = _malloc_alloc_omm_handler; if(0==my_malloc_handler){_THROW_BAD_ALLOC;} result = malloc(n); if(result) return (result); } } 剖析二级配置器 第二级多了些机制，避免小额区带来的内存碎片。因此在配置区块超过128bytes时，采用一级配置器allocator.h, 当配置区块小于128bytes时，为了降低额外负担，会采用memory pool的方式。\n这是为其配置的一些函数：\nM_align内存对齐，M_round_up上调到对应区间大小，M_freelist_index寻找索引。\n这以上几个函数都是对链表和内存池进行操作，看懂内存池和链表关系就不难写出来。\nM_chunk_alloc：从内存池中取出空间给freelist，分出n个大小为size的block\n1 2 3 4 5 6 7 内存池剩余空间完全满足需求量 不能完全满足需求量，但是可以供应一个以上的区块 一个区块的大小都不能提供 将剩余空间编入 配置堆空间补充内存池 检视我们有的，调整尚未用的区块 递归 M_refill：给定块大小的内存块重新填充\n申请新的内存块，当它没有可用内存时，M_refill调用M_chunk_alloc获取内存\n将内存块分割，一块给调用者，其余存入freelist用链表管理\n在需要分配时直接从 freelist 中取出，减少了 malloc 的调用开销，从而提高程序的性能。\nmemory.h unintialized.h 内存基本处理工具 该部分主要对于未初始化的空间构造函数\n分为五个部分，uninitialized_copy, uninitialized_fill, uninitialized_fill_n, unintialized_move, uninitialized_move_n\n在这个.h中的各个处理未初始化内存块的工具函数，如何高效的在其中构造对象，取决于对象的类型是否是POD类型。\n是POD类型时。可以直接用低级别的内存操作比如memcpy等快速初始化，优化内存管理 不是POD类型，必须调用对象的构造函数正确初始化对象，非POD类型的对象往往需要复杂的生命周期管理，比如析构，移动构造等，注意到正确的调用这些函数以确保内存安全。 将内存的配置和对象的构造分开\n注：C++ standard：uninitialized_copy要么构造出所有必要元素，要么不构造任何东西\nuninitialized_copy uninitialized_fill uninitialized_fill_n 这几个构造的思路是类似的，都是设置三个函数\n1 2 3 4 5 6 7 template \u0026lt;class InputIter, class ForwardIter\u0026gt; ForwardIter /*可平凡赋值*/ unchecked_uninit_copy(InputIter first, InputIter last, ForwardIter result, std::true_type); ForwardIter /*不是可平凡赋值*/ unchecked_uninit_copy(InputIter first, InputIter last, ForwardIter result, std::false_type); ForwardIter /*对传入的类型进行判断*/ uninitialized_copy(InputIter first, InputIter last, ForwardIter result); iterator iterator，连接着容器与算法，是一种泛型指针，定义在 iterator.h 中。每个容器都附带专属的迭代器，是一种重载了 operator*，operator-\u0026gt;，operator++，operator-- 等操作的模板类。\n（这里面有一些生疏的概念，看着好困难。。。）\n[!TIP]\nQ：迭代器的associated type是什么？\nA：associated type是为迭代器提供统一接口的基础。通过这些类型，c++能够对不同容器和数 据结构中的元素进行统一操作。\n以std::vector\u0026lt;int\u0026gt;::iterator it为例\niterator_category：迭代器的种类（输入迭代器，输出迭代器，前向迭代器，双向迭代器，随机访问迭代器）\nvalue_type：迭代器所指容器中元素的类型，也就是int。\ndifference_type：迭代器距离范围对应的类型，一般是内置类型ptrdiff_t有符号整数。\nreference：迭代器所指容器中元素的引用类型。\nc++中，函数如果传回左值，便以by reference的方式进行\np为mutable iterators，它的value_type为T，*p的型别就是T\u0026amp;\np为constant iterators，它的value_type为T，*p的型别就是const T\u0026amp;\n*p就是所谓的reference type\npointer：迭代器所指容器中元素的指针类型。\nQ：模板偏特化是什么？class iterator, native pointer, const native pointer是什么？\n偏特化就是对模板进行部分特化的机制。\n类迭代器常见于标准容器中；原生指针是一种内置指针类型，支持指针算术操作，可以作为高效的迭代器用于数组或者连续内存区域；常量原生指针是一种常量指针。\nQ：迭代器如何做到把容器和算法联系起来的？\niterator.h 迭代器是一种行为类似指针的对象，而指针的各种行为中最常见也最重要的是内容提领和成员访问，因此，迭代器最重要的编程工作就是对operator*和oerator-\u0026gt;进行重载工作。\n代码结构分析：\n首先是五个内嵌相应的型别：\n1 2 3 4 5 6 7 8 9 10 template \u0026lt;class Category, class T, class Distance = ptrdiff_t, class Pointer = T*, class Reference = T\u0026amp;\u0026gt; struct iterator { typedef Category iterator_category; typedef T value_type; typedef Pointer pointer; typedef Reference reference; typedef Distance difference_type; }; 定义五种迭代器种类，提前设计，使用类萃取，可以在编译时选择正确的版本\n1 2 3 4 5 6 7 8 // 迭代器种类，iterator_category /*提前设计便于编译，如果在运行期再选择哪种迭代器，会影响程序效率， 最好能在编译器就选择正确的版本*/ struct input_iterator_tag {}; struct output_iterator_tag {}; struct forward_iterator_tag : public input_iterator_tag {}; struct bidirectional_iterator_tag : public forward_iterator_tag {}; struct random_access_iterator_tag : public bidirectional_iterator_tag {}; iterator_traits_impl类，核心思想在于将类型信息和行为封装到独立的模板之中，编译时检查，提取用户类型的相关信息。\n1 2 3 4 5 6 7 8 9 10 11 12 template \u0026lt;class Iterator, bool\u0026gt; struct iterator_traits_impl {}; /*提取用户自定义类型的相关信息*/ template \u0026lt;class Iterator\u0026gt; struct iterator_traits_impl\u0026lt;Iterator, true\u0026gt; { typedef typename Iterator::iterator_category iterator_category; typedef typename Iterator::value_type value_type; typedef typename Iterator::pointer pointer; typedef typename Iterator::reference reference; typedef typename Iterator::difference_type difference_type; }; has_iterator_cat类利用SFINAE规则，使编译器根据迭代器有/没有iterator_catefory类型成员，分别送入两个test函数，这两个test函数并不会执行，而是在编译期间，通过test函数的返回值判断迭代器进了哪个函数，从而判断迭代器有没有iterator_category类。\niterator_traits_helper类进行迭代器类型萃取，主要依赖于偏特化和SFINAE技术，观察其迭代器是否是输入或者输出迭代器。\n在iterator_traits_helper的第二个参数就是判断是否有iterator_catefory类型成员，因此以上两个类可以嵌套使用，达成一个萃取迭代器iterator_traits，同时针对原生指针，常量指针，类指针进行一个偏特化版本。\n1 2 3 4 5 6 7 8 template \u0026lt;class Iterator\u0026gt; struct iterator_traits : public iterator_traits_helper\u0026lt;Iterator, has_iterator_cat\u0026lt;Iterator\u0026gt;::value\u0026gt; {}; template \u0026lt;class T\u0026gt; struct iterator_traits\u0026lt;T*\u0026gt;{...};// 省去的部分也用来提取用户的自定义类型 template \u0026lt;class T\u0026gt; struct iterator_traits\u0026lt;const T*\u0026gt;{...}; has_iterator_cat_of类用来判断一个迭代器可否被视为输入迭代器、输出迭代器、双向迭代器、或者随机访问迭代器，编译时确立迭代器类型。\n1 2 template \u0026lt;class T, class U, bool = has_iterator_cat\u0026lt;iterator_traits\u0026lt;T\u0026gt;\u0026gt;::value\u0026gt; struct has_iterator_cat_of : public m_bool_constant\u0026lt;std::is_convertible\u0026lt;typename iterator_traits\u0026lt;T\u0026gt;::iterator_category, U\u0026gt;::value\u0026gt;{};//这里面的m_bool_constant的实现在type_traits.h种实现的 后面会通过类型萃取技术，基于前面的 has_iterator_cat_of 模板类，判断 Iter 迭代器是否具有某种迭代器属性，针对不同的类型进行不同的操作。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 // 萃取某种迭代器 template \u0026lt;class T, class U\u0026gt; struct has_iterator_cat_of\u0026lt;T, U, false\u0026gt; : public m_false_type {}; template \u0026lt;class Iter\u0026gt; struct is_input_iterator : public has_iterator_cat_of\u0026lt;Iter, input_iterator_tag\u0026gt; {}; template \u0026lt;class Iter\u0026gt; struct is_output_iterator : public has_iterator_cat_of\u0026lt;Iter, output_iterator_tag\u0026gt; {}; template \u0026lt;class Iter\u0026gt; struct is_forward_iterator : public has_iterator_cat_of\u0026lt;Iter, forward_iterator_tag\u0026gt; {}; template \u0026lt;class Iter\u0026gt; struct is_bidirectional_iterator : public has_iterator_cat_of\u0026lt;Iter, bidirectional_iterator_tag\u0026gt; {}; template \u0026lt;class Iter\u0026gt; struct is_random_access_iterator : public has_iterator_cat_of\u0026lt;Iter, random_access_iterator_tag\u0026gt; {}; template \u0026lt;class Iterator\u0026gt; struct is_iterator : public m_bool_constant\u0026lt;is_input_iterator\u0026lt;Iterator\u0026gt;::value || is_output_iterator\u0026lt;Iterator\u0026gt;::value\u0026gt;{}; 这个采用了type_traits的技法，核心在于定义一些typedef，它的值不是true_type就是false_type，通过它我们就可以知到任意型别拥有哪些特性。将判别类在编译期间通过模板函数对object反向推导，怕毛短迭代器种类等融合其对象的特性。\n萃取迭代器category, difference_type, value_type,类似于下面，其它略。\n1 2 3 4 5 // 萃取某个迭代器的 category template \u0026lt;class Iterator\u0026gt; typename iterator_traits\u0026lt;Iterator\u0026gt;::iterator_category iterator_category(const Iterator\u0026amp;) {...}; 萃取distance_dispatch，这个要采取标签分发机制，对于不同的迭代器有不同的计算方式。\n利用iterator_category种的五个编辑类，让他们有继承关系，在利用萃取出来的迭代器选择重载函数，这样就能达到优先效率，选择最合适的函数调用。\n同样还有advance_dispatch,也就是对于不同的迭代器的不同版本实现。\n另外定义了一个反向迭代器的模板类reverse_iterator，前进为后退，后退为前进。\n重载了加减各种比较操作符，\n总结： iterator涉及类型萃取，模板特化/泛化，反向推导，继承，重载模板偏特化等各种机制。\n根本上想要在容器和算法间实现的是，\n对于容器间的associated type的五种类型的萃取，对于class iterator, native pointer, const native pointer这几种不同类型的处理；还有利用重载去对不同迭代器种类进行操作的选择。\n利用type_traits激发让编译器在编译期就能够知道实参的一些特性，这使得我们可以实现不同情况下效率最优的重载函数供编译器在编译期间选派，而不是仅仅用重载函数，那样就需要到执行期间才知道具体用哪个函数，影响效率。\ntype_traits.h iterator负责萃取迭代器特性，type_traits负责萃取型别的特性。\n这个型别指的时是否具备non-trivial defalt ctor, non-trivial copy ctor, non-trivial assignment operator, non-trivial dtor.答案shi否定的就可以采用内存直接处理操作获取最大效率\n前言：\nvector，list，deque\nmap，set，unordered_map，unordered_set，basic_string\n容器很重要的一点在于理解它们的struct，这样才能更好的推出函数的思想结构\n下图种，所谓的衍生并不是派生关系，而是内含关系。比如heap里内含一个vector，priority-queue内含一个heap，stack和queue都内含一个deque，set，map，multiset，multimap种内涵一个RB-tree，hast_x都内含一个hashtable。\nsequence containers 容器，置物之所也。STL容器是将运用最广的一些数据结构实现出来。\nvector.h vector的实现技术，关键在于对其大小的控制和重新配置时数据移动效率。注意该源码中SGI vector的空间配置策略。\n线性连续空间，空间配置。\n构造函数 这里的构造函数包含了很多情况，比如explicit vector(size_type n)直接初始化多少空间，比如 vector() noexcept，比如vector(Iter first, Iter last)，对于某个迭代器的复制（这里也有 vector(const vector\u0026amp; rhs)直接复制容器，函数内部也会转换为指针的开始到结束），下面选取几种典型进行分析：\n底层都是调用init_space和uninitialized_fill_n，前一个函数会用allocate直接分配空间，分配失败会置空；后一个函数则是位于uninitialized.h函数中，为初始化的空间填充值。\nemplace_back 有种底层都差不多的感觉，这里注意一个知识点的多次使用：\n####### 完美转发（不详述，可上网查）\n然后对于emplace_back就是为其construct在内存上构造对象，内存不足就reallocate，重新分配时会先采用allocate为其重新分配1.5倍或者当前所需容量，再定义新的begin，end，cap，用allocate移动前面的元素，中间元素重新construct，后面一部分再重新初始化。\n其实这就是调用了一块新的空间，时间复杂度为O(n)。\nemplace也差不多，区别在于emplace_back是在最后添加，emplace是可以在任意位置添加，不过它们在某种情况都是调用reallocate_emplace，在某个位置重新分配。\n只要是中间插入元素，都只能重新分配空间，改变指针位置。\npush_back 这跟上面差不多，就没有完美转发这一步。\npop_back() 调用allocator中的destory()\n1 2 3 4 5 6 template \u0026lt;class T\u0026gt; void vector\u0026lt;T\u0026gt;::pop_back(){ MYSTL_DEBUG(!empty()); data_allocator::destroy(end_ - 1); --end_; } erase 有两种，一种是删除pos上的元素，一种是删除某个范围内的元素。\n其实都是先将删除的元素那后面的往前移move，再将多余的元素删除destory\n最后改变指针位置即可。\nclear 也就是erase(begin(), end()); insert 有空间，分两类，然后初始化空间，copy，fill 没有空间，重新找新的空间从头开始分配。 list 相较于vector的连续线性空间，list就显得复杂更多，好处是每次插入或者删除元素，都会配置或者释放一个空间，因此对于空间的运用很精准，不浪费。\nvector中以普通指针作为迭代器，也可以理解成随机访问迭代器，节点不保证在存储空间中连续存在，因此list采用的是双向迭代器。\npush_back 后面的这些都有点大差不差的意味，记住底层是对空间的分配就好。\npush_back如果参数是左值，那就是创造节点，将其加入最后，在size++更新\n如果参数是右值，那么利用move右值引用，调用emplace_back，emplace_back同样是创造节点creat_node，link_nodes_at_back将节点加到最后，不过creat_node参数利用了forward达到了完美转发，省去了一次参数的复制，简直和vector的思想一模一样，就是从数组转成了链表，还不需要考虑重新拿新空间分配reallocate的问题了。\npop_back 我们记录了node_指向末尾节点，直接消除链接unlink()，再destory消除该地址的值，deallocate销毁内存即可。\nallocate和deallocate用于为其分配内存\nconstruct和destory则是在已有的内存上为其声明对象\n这两个都是在allocator.h之中的，至于aloc.h中的一二级分配器如今已经不怎么使用了，如今的allocate中无论是直接调用operator new和operator delete还是malloc，free，mmap等，这些机制内部已经进一步优化，而aloc.h中的一二级分配器反而需要内存池，多线程，自由链表等技术维护，增加复杂度和开销，多线程更容易增加安全问题，优势便不那么明显。\n现在cpp中，提供了更为灵活易用的内存接口，通过智能指针和标准化的allocator，内存管理更加安全简化。\nclear 从第一个节点到最后一个节点，挨个destory_node\n(destory_node会先调用allocator中的destory，去除地址中的value，再调用deallocate，销毁空间)\n其它相关操作remove,unique,merge,sort,reverse,splice remove，很显然，从头到尾遍历，碰见值相同调用erase，erase会先unlink，再destory_node;\nunique，同样是遍历，满足某个条件进行移除\nmerge，按照比较函数合并，这就是算法了，双指针同时遍历两个链表合并，最后加上没有遍历完链表的剩余部分。\nsort，链表排序的话，代码中用的是递归实现的归并排序，。\nreverse，翻转指针\nsplice，将链表的一部分移动到当前链表pos位置的函数（只涉及指针的移动，效率还不错）\n先判断移动节点数量是否超过最大允许大小\n再断开原链表中要移动的节点\n将断开的节点插入到目标链表中的指定位置\n更新链表大小等相关数值。\ndeque vector是单向开口的连续线性空间，deque是双向开口的连续线性空间。\n二者的最大差异在于\ndeque允许常数时间内对头端进行元素的插入或者移除 deque没有所谓容量的概念，因为它本身就是动态的以分段连续空间组合，随时可以增加新的空间链接。 虽然deque也是随机存取迭代器，但是它的迭代器并不是普通指针，vector的指针仅在连续空间内移动，因此运算层面也更加复杂，除非必要，尽可能使用vector而不是deque。\ndeque为了维护所谓连续线性空间的家乡，采用了一块所谓的map作为主控，map是一小块连续空间，其中每个元素都是指针，指向另一块较大的连续线性空间，称为缓冲区。缓冲区才是deque的存储空间主体。\n1 2 3 4 // deque类中 deque 的型别定义 typedef mystl::allocator\u0026lt;T\u0026gt; allocator_type; typedef mystl::allocator\u0026lt;T\u0026gt; data_allocator; typedef mystl::allocator\u0026lt;T*\u0026gt; map_allocator; 大致是下图：\n迭代器 比起vector真正的指针向后移一位，和list的指向下一个next，deque的operator++和operator- -显得更为复杂，下面看代码：\n先思考，对于deque的要求是什么\n判断每个缓冲区的边缘 map中的每个节点之相对应的缓冲区 1 2 3 4 5 6 7 8 typedef T* value_pointer; typedef T** map_pointer; value_pointer cur; // 指向所在缓冲区的当前元素 value_pointer first; // 指向所在缓冲区的头部 value_pointer last; // 指向所在缓冲区的尾部 map_pointer node; // 缓冲区所在节点 // deque iterator中 缓冲区内存分配：\n1 2 3 4 5 6 template \u0026lt;class T\u0026gt; struct deque_buf_size { static constexpr size_t value = sizeof(T) \u0026lt; 256 ? 4096 / sizeof(T) : 16; }; // T较小时，再4096内能放多少就放多少，当T较大时，固定放16个 operator++ 如果该节点当前值cur==last，那么指针随着setnode向后移动一个缓冲区\noperator- - 如果该节点当前值cur==first，那么指针随着setnode向前移动一个缓冲区\ndeque的数据结构 1 2 3 4 5 6 7 8 typedef pointer* map_pointer; typedef const_pointer* const_map_pointer; // 用以下四个数据来表现一个 deque iterator begin_; // 指向第一个节点 iterator end_; // 指向最后一个结点 map_pointer map_; // 指向一块 map，map 中的每个元素都是一个指针，指向一个缓冲区 size_type map_size_; // map 内指针的数目 有这些的话，那诸如begin(), end(), front(), back(), size(), empty()等操作便可以很容易的进行。\n输入迭代器：只能单向遍历，只读访问，不能多次遍历相同元素，适合一次性读取数据的场景。\n前向迭代器：也只能单向遍历，但支持多次遍历相同元素，可以读取和修改元素，适用于需要多次遍历的线性数据结构。\ndeque的构造和内存管理 construct，push_back，push_front\n####### construct，\n同样是fill_init和copy_init两种\n对于copy_init，为了高效对deque的初始化，它会根据迭代器的特性采用不同的实现策略，比如当为一次性数据，比如文件读取输入流等只能一次单向遍历不发事先确立数据量大小只能遍历一次时，采用输入迭代器；带数据源可以多次遍历且可以预先计算数据量的场景时，可以采用前向迭代器。 输入迭代器：首先map_init初始化map和缓冲区，再采用emplace逐个处理遍布到结尾。 前向迭代器：首先map_init初始化map和缓冲区，再针对每个节点，确立缓冲区大小，使用uninitialized_copy将元素复制到当前缓冲区，更新迭代器。 fill_init，则直接map_init初始化map和缓冲区，再直接利用uninitialized_fill填数据就行。没有迭代器，无需区分。 ####### push_back\n结尾有空间时，直接construct对象，再个更新指针\n结尾缺少空间时，先require_capacity，再在结尾分配好的空间上construct内存，更新指针\ndeque\u0026lt;T\u0026gt;::require_capacity 是在需要在双端队列（deque）的两端添加元素时，确保有足够的容量。如果容量不足，它会创建新的缓冲区或者重新分配 deque 的内部结构（map_）以容纳更多的元素。如果容量足，会直接创建新的缓冲区。\nreallocate_map_at_back 是 deque 内存管理的一部分，当需要在后端插入元素且现有 map_ 不足以容纳新的缓冲区时，它会创建一个更大的 map_ 并将原有的缓冲区指针迁移到新 map_ 中。该函数确保了 deque 能够在两端灵活扩展，同时避免频繁重新分配内存，提升了性能。\npush_front同理。\ndeque的元素操作 ####### pop_back\n两种情况\n最后一个缓冲区一个以上元素，直接destory析构。 最后一个缓冲区没有元素，需要deallocate释放最后一个缓冲区，setnode调整接节点状态，再回退到上一个缓冲区，destory上一个缓冲区的最后一个元素。 pop_front同理。\n####### clear\n注意需要保留一个缓冲区，这是deque的策略 首先除开头尾意外的两个缓冲区，其它都会直接destory所有值再deallocate；然后如果头尾缓冲区不同，destory两边的内容再deallocate尾缓冲区，如果只有一个缓冲区就直接析构destory；最后调整开头结尾指针即可。 ####### erase\n这个虽然说有链表的结构吧，但实际上又是个虚拟的内存空间，因此删除元素还是和vector一样通过将所有元素移动一个位置。\n因此会判断前面元素少还是后面元素少决定移动方向，有助于优化性能。\n移动之后记得destory最前面或者最后面的位置，可以使用pop。\n####### insert\n猜测一下这个插入是不是类似于erase，插入之后全部移动，也是先判断再移动这样子。\n添加:那也可以判断一下是不是在最前面和最后面，这样直接emplace_x就行。\nstack stack是一种FILO的数据结构。\n将deque作为底部结构封闭其头端开口，轻而易举的形成了一个stack，因此SGI STL便以deque作为缺省情况下的stack的底部结构。\n这种“修改某物接口，形成另一种风貌”的性质，称为adapter。因此STL stack往往不被归类为container，而是被归类为container adapter。\n这个就大部分要能继承deque（默认stack底层）就直接继承，比如empty，size，top，push，pop等都可以直接对应deque的back部分，封闭front的部分就是stack了。\nstack所有的元素都要符合先进后出的原则，只有顶端的元素才有被外界取用的机会，因此stack不提供走访功能，也不提供迭代器。\n该代码中的实现是以deque作为底层结构，其实还可以通过list或者vector作为stack的底层容器。\n[!TIP]\nQ：为什么现代stl中的stack使用deque作为底层，而不是vector或者list？\nA：std::stack 使用 deque 作为默认的底层容器，主要是因为 deque 提供了高效的双端操作能力、良好的内存管理和适中的实现复杂性。deque 能够在大多数栈操作中提供平衡的性能和灵活性，对于stack的扩容时，内存开销不大，时间开销也不大，同时具有局部缓存性。\n而 vector对于频繁的扩容操作， 扩展时需要重新分配整个内存区域，极其影响性能。\nlist 则是每个节点添加时都需要额外的内存开销，并且缓存局部性很差，性能也不好达到。\nqueue queue作为FIFO的数据结构，和stack面对deque是一模一样的状态，封闭了某些功能以此达到目的，同样也可以被归类为container adapter。同样，queue只有顶端的元素可以取用，不提供遍历功能，也不提供迭代器。\nheap heap不属于STL容器组件，一般扮演着priority queue的助手。\npriority queue允许用户以任何次序将元素推入容器内，但是取出的时候一定会从优先权最高的元素开始取，正是因为binary max heap有这样的特性，才会作为priority queue的底层机制。\n由于是完全二叉树之后的层序遍历结果由array存储，因此heap中只需要实现对heap的几个函数的算法就行，不需要实现结构。\nheap的具体算法属于数据结构和算法课程的知识，这里不详述。\n1 2 3 4 5 6 7 8 9 10 11 12 13 /*添加元素*/ void push_heap(RandomIter first, RandomIter last); void push_heap(RandomIter first, RandomIter last, Compared comp); /*删除元素*/ void pop_heap(RandomIter first, RandomIter last); void pop_heap(RandomIter first, RandomIter last, Compared comp); /*排序*/ void sort_heap(RandomIter first, RandomIter last); template \u0026lt;class RandomIter, class Compared\u0026gt; void sort_heap(RandomIter first, RandomIter last, Compared comp); /*创造堆*/ void make_heap(RandomIter first, RandomIter last); void make_heap(RandomIter first, RandomIter last, Compared comp); priority_queue priority是一个有权值观念的queue，可以加入新元素，移除旧元素之类的功能，它里面的元素不是按照自动排序，而是按照权值的高低，缺省情况下利用一个最大堆完成，因此底层是一个vector表现的complete binary tree。\n它是以缺省的vector为底部容器，同样也是被称为container adapter。\n其中的构造函数，析构函数，push，pop其实底部都是heap中的make_heap，push_heap，pop_heap等泛型算法。\npriority的元素内部有自己的规则，只有顶端元素可以被外界取用，因此没有遍历功能，也没有迭代器。\nassociative containers 关联式容器，观念上类似关联式数据库：每笔数据都有一个键值和一个实值，当元素被插入到关联式容器中，容器内部结构便依照其键值大小，以某种特定规则将元素放置在合适的位置，因此没有所谓的push_back(), push_front()等之类的操作行为。\nRB-tree 红黑树 - OI Wiki (oi-wiki.org)\n红黑树也是平衡二叉搜索树，但是和AVL树不同，详见下图：\n性质：\n根节点为黑色，所有叶子NIL节点为黑色。\n每个红色节点必须有两个黑色的子节点。\n任一节点到叶子节点的所有简单路径包含相同数目的黑色节点。\n红黑树的插入节点为红色\nrb_tree_node_base 1 2 3 4 base_ptr parent; // 父节点 base_ptr left; // 左子节点 base_ptr right; // 右子节点 color_type color; // 节点颜色 SGI把RB迭代器实现为两层，属于双向迭代器\nrb_tree_node继承自rb_tree_node_base\nrb_tre_Iterator继承自rb_tree_base_iterator\n对于迭代器的遍历时利用re_tree_node_iterator中的increment和decrement函数进行的。\nincrement\n有右节点，找右子树的最左节点 没有右节点，查找第一个大于当前节点的祖先节点. 左节点找它的父节点 右节点找它的某个父节点为左节点，那个左节点的父节点就是目标节点 右节点不等于此时的父节点，父节点为解答，否则node为解答 decrement 红节点，父节点的父节点为自己，右子节点为解答\n有左节点，找左节点的最右（大）节点\n没有左节点，查找第一个小于当前节点的祖先节点\n右节点找它的父节点 左节点找他的某个父节点为右节点，这个右节点的父节点就是目标节点 RBtree的数据结构 涉及一些专属的空间配置器，型别定义，简单的成员函数（极小值，极大值，插入，删除，构造，析构，空，大小等。\nRB-tree的构造函数有两种：\ncopy_from直接拷贝另一棵树 rb_tree_init初始化这个树的根节点为空 当然为了以防边界情况，也就是对根节点有特殊的处理，我们会创造一个哨兵节点header。\n1 2 3 4 5 6 7 rb_tree_init(){ header_ = base_allocator::allocate(1); header_-\u0026gt;color = rb_tree_red; // header_ 节点颜色为红，与 root 区分 root() = nullptr; leftmost() = header_; rightmost() = header_; node_count_ = 0; } 对于元素的基本操作，这里以正常情况为例，具体代码中会重载多种情况，比如插入的元素节点键值能不能重复之类的多种情况，这些是为了之后的set，map等一个container adapter能起到的一个作用。\ninsert 插入时可能出现的情况：\ncase 1: 新增节点位于根节点，令新增节点为黑 case 2: 新增节点的父节点为黑，没有破坏平衡，直接返回 case 3: 父节点和叔叔节点都为红，令父节点和叔叔节点为黑，祖父节点为红，然后令祖父节点为当前节点，继续处理 case 4: 父节点为红，叔叔节点为 NIL 或黑色，父节点为左（右）孩子，当前节点为右（左）孩子，让父节点成为当前节点，再以当前节点为支点左（右）旋 case 5: 父节点为红，叔叔节点为 NIL 或黑色，父节点为左（右）孩子，当前节点为左（右）孩子，让父节点变为黑色，祖父节点变为红色，以祖父节点为支点右（左） 在插入时会同步记录，键值是否重复，是在某个父子树的左节点还是右节点\nerase 删除节点可能出现的情况：\n此时，y 指向要删除的节点，x 为替代节点，从 x 节点开始调整。如果删除的节点为红色，树的性质没有被破坏，否则按照以下情况调整（x 为左子节点为例）：\ncase 1: 兄弟节点为红色，令父节点为红，兄弟节点为黑，进行左（右）旋，继续处理 case 2: 兄弟节点为黑色，且两个子节点都为黑色或 NIL，令兄弟节点为红，父节点成为当前节点，继续处理 case 3: 兄弟节点为黑色，左子节点为红色或 NIL，右子节点为黑色或 NIL，令兄弟节点为红，兄弟节点的左子节点为黑，以兄弟节点为支点右（左）旋，继续处理 case 4: 兄弟节点为黑色，右子节点为红色，令兄弟节点为父节点的颜色，父节点为黑色，兄弟节点的右子节点为黑色，以父节点为支点左（右）旋，树的性质调整完成，算法结束 最后再destory_node该节点，先destory，再deallocate\nfind RBtree是一个二叉搜索树，元素的搜寻正是拿手项目，只要从根节点向下遍历即可。\n这里稍微和正常遍历查找不同的就是加了一点点trait。\n这里总结一下类型萃取。\n类型萃取是一种模板元编程技巧，主要在于编译时获取类型信息和优化代码，为泛型编程提供额外的信息和功能，让模板更加灵活，可扩展，提升编译期的类型安全性。\n编译器通过模板特化匹配具体的类型，根据不同的类型编译器在编译时生成不同的代码，达到类型安全和优化的目的。\n常见的有：\n1.类型特征萃取，在编译器检查和操作类型\nstd::remove_reference\u0026lt;T\u0026gt;：去掉类型T的引用修饰符；std::remove_const\u0026lt;T\u0026gt;：去掉类型T的const修饰符；std::is_pointer\u0026lt;T\u0026gt;：判断类型T是否为指针类型…\n2.迭代器特性萃取，用于提取迭代器的相关信息\niterator_category：迭代器类型，如random_access_iterator_tag，bidirectional_iterator_tag等；difference_type：两个迭代器之间的距离类型…\n3.数值特性萃取，提取数值类型的特性信息\nstd::numeric_limits\u0026lt;T\u0026gt;::max()：类型T能表示的最大值；std::numeric_limits\u0026lt;T\u0026gt;::min()：类型T能表示的最小值；std::numeric_limits\u0026lt;T\u0026gt;::epsilon()：浮点数类型T的最小精度差值。\n再总结一下，常见的模板元编程：\n1.递归模板，可以进行编译期计算，类型推导等。\n2.模板特化，为特定的模板参数提供不同的实现\n3.SFINAE，替换失败不是错误，也就是在实例化模板时遇到不合法的模板替换出错后，会选择其它的模板重载而不是报错。\n4.类型萃取\n5.constexpr。允许在编译器执行函数。\n6.using模板别名，常用于模板，属于typedef的增强版。\nset/multiset set：\n自动排序，键值等于实值，键值不能重复。\n它的迭代器是const_iterator，杜绝写入操作，也就不是constant iterator，也不是mutable iterators。\nRBtree作为一种平衡二叉搜索树，其自动排序的效果很好，并且set的各种开放操作接口，都是直接调用RBtree中现成的。\nmultiset：允许键值重复，因此插入调用的是RBtree的insert_equal而不是insert_unique。\nmap/multimap 所有的元素根据键值自动排序，拥有实值和键值，第一个元素为键值。\n迭代器也同样，不能通过迭代器修改map的键值，因为键值会影响map元素的排列规则，但是在插入删除时，map里面的元素也会通过迭代器变动。详见代码。\nmultimap和map的差别，与set和multiset的差别是一样的。\nhashtable 它可以提供对于任何有名项的存取和删除操作，所以也可被视为一种字典结构，结构用意在于提供常数时间的基本操作。\nhash function，碰撞\n碰撞的解决方案：线性探测（可能导致主集团primary clustering问题），二次探测，开链等\n该代码中是以开链法完成hash table的图形表述。\n1 2 3 4 5 6 7 8 9 10 11 struct hashtable_node{ hashtable_node* next; // 指向下一个节点 T value; // 储存实值 }; /*如果在当前一个list的尾端，可以跳到下一个bucket身上，也就是重载operator++*/ /*bucket聚合体以vector完成*/ struct ht_iterator_base :public mystl::iterator\u0026lt;mystl::forward_iterator_tag, T\u0026gt;{ node_ptr node; // 迭代器当前所指节点 contain_ptr ht; // 保持与容器的连结 } 构造和内存管理 构造：\ninit建立数组，同时设立初值为0\ncopy_init双层循环，一层创造vector，一层创造link\ninsert和resize insert_unique\n先判断是否需要重建表格用rehash 再插入目标值 遍历如果发现重复直接返回（1） 未重复，指向对应bucket指向链表的头结点 （注意返回时的变量包含有没有插入成功） insert_multi\n在步骤(1)中如果发现相同键值的，也可以直接插入\nrehash\n比较元素个数和bucket vector的大小相比，如果前者大于后者，重建表格，表格大小设立成下一个质数（玄学bushi，质数的碰撞可能性或许会更低） 再循环旧的bucket，将每个旧bucket中的头结点让新的bucket指向它即可 最后对调新旧bucket 释放旧bucket的内存 copy和clear copy_from\nclear\nhash_set/hash_multiset 实值键值相同，不排序。\nhash_map/hash_multimap 用键值快速搜索元素。\nSummar： 关于STL的内容到这里就算结束了。代码也是只实现了allocator，iterator和container这几个部分。\n后面写一些自问自答算是一个总结了。\nQ：介绍一下allocator.h中代码的实现结构，说出关键点的细节。\nA：\n使用方法：\n重载和SFINAE使得在stl中会选择合适的重载函数，即使选择失败了也不会报错而是会继续尝试其它函数。 采用可变模板参数的通用引用+forward或者右值引用+移动语义，减少了额外拷贝，节约资源提升性能。 代码结构：\nallocate使用的是operator new，相对应的deallocate使用的是operator delete，对应的是空间的分配和释放。\nconstruct和destory在非平凡析构函数的是显式调用构造和析构函数，对于平凡析构函数，函数内部会自行调用构造和析构函数，用于对象的构造和析构\nQ：代码中还存在aloc.h中利用内存池和自由链表的另一种方式，如今已经被弃用，主要原因在于glibc的malloc/free的内存池设计已经实现的极为精巧，充分利用了是恐惧不行在内存管理和内存消耗的取舍，请详细介绍下两者的对比。\nA：\n这个文章里面好像写了，本身内存池和自由链表的创造是为了增加性能的，防止malloc时过多的内存碎片导致的内存泄露，但是随着后来malloc机制的逐渐优化，这种思路就被弃用了。\nglibc中的内存池代码分析链接：glibc malloc源码分析\nQ：iterator中有些模板元编程或者type_traits或者一些不太熟悉的特性，请介绍iterator的实现思路并且重点讲讲这些特性的使用和好处。\nA：\n结构：\niterator可以从五个内嵌相应型别入手，迭代器类别，值类型，指针，引用，距离范围。\n迭代器类别有五个，输入，输出，前向，双向，随机存取\n针对有没有迭代器，是什么迭代器，利用重载，偏特化和SFINA技术用于判断迭代器的种类。 对于原生指针，常量指针，类指针都会有对应的偏特化版本。 同样也有前进和后退的重载，引用和指针的重载等等，主要是采用类型萃取以便于在编译器确立类型。具体不同容器对于迭代器不同的使用在容器的代码中会详细说明。\nQ：容器中有几个最为底层的，比如vector,list,RBtree,hashable，这些都是heap，priority_queue，set，map等实现的基础，这些也成为container adapter，请介绍这些最为底层的容器。\nA：\n一般容器中分为迭代器和容器的基本函数实现两部分。 vector 由于vector时采用连续的内存空间，因此迭代器很类似于普通的常量指针 对于一些基础的迭代器时采用对于vector结构中的私有成员begin,end,cur指针的直接调用进行的。 基础函数的话就比如构造，析构，push，pop，erase，clear，insert在文章中都有过程的阐述。 欸对，还有push和emplace一般来说emplace会利用右值引用和完美转发所以性能更好。 构造析构其实都是allocate，deallocate，construct，destory函数的调用之类的。后面的基础函数也同样。和我们之前了解的vector的底层思想一致。 比如空间不够，了重新拿个空间重新分配，设置成两倍一点五倍；push时会判断有没有备用空间要不要重新分配；pop时直接destory，erase本质上是将后面元素整体向前移动，时间复杂度O(n)，在destory多余的部分；clear就是erase所有；insert同样是需要分配有无备用空间，有备用空间怎么移动之类的，详细看文章或者去看源码了。\nlist 迭代器的主要作用是遍历，访问和操作链表中的元素，在list中会重载解引用，自增自减之类的运算符，一般都是直接调用比如node的prev，next指针或者节点的值。不过源码中同时重载了普通指针和常量指针，普通指针用于修改容器中存储的元素，常量指针表明支持list的只读操作。 后面的构造析构pushpopremove等也是对于算法的要求更多，然后涉及空间的分配，list中主要是节点的前后指针值和list中的尾指针和大小。\ndeque 双端队列，这个就比以上两个都要更复杂了，这是一段虚假的连续空间，哪怕你迭代器加加减减的很顺利，其实都是无数行默默的代码换来的。 deque的迭代器是会直接包含list的相关特性的，当然也重载了普通指针和常量指针，设置的成员数据有缓冲区当前元素，头部元素，尾部元素所在节点之类的，内部重载运算符，这个是一定要区分指针++或者\u0026ndash;时还在不在当前缓冲区的。 至于deque本身那当然要充分符合它自身的特性，私人成员有，指向第一个和最后一个节点，每块map以及map的大小，然后基本函数中要注意缓冲区大小的设置，缓冲区边界，全部清空时记得保留一个缓冲区。\nRBtree 红黑树的迭代器提供了一种类似指针的方式来访问和遍历容器中的元素。其中也是直接包含红黑树的节点，红黑树的节点内部有父节点，左右子节点和它本身的值，红黑树本身则包括根节点（特殊节点），结点数，键值比较原则这些。迭代器则是要实现自增自减，还有插入删除时树涉及的旋转之类的，也要重载元素键值是否重复，每个节点的的键值和值之类的，这些都是为之后的set，map等提供基础。\nhashable 哈希表的主要优势应该是在于类似于数组的存取效果。不过当元素过于的散乱时数组显得太过于浪费空间，这也就是哈希表的阐述，哈希表一般会采用取模之类的在某个范围建立一个bucket，但是这样会发生哈希碰撞，然后相应的解决方式有线性探测，二次探测开链等。这里采用的是开链，也就是当重复时会直接在该位置建立链表。将键映射到bucket的索引。 同时哈希表会存在容量和负载因子，当负载因子(每个同的平均元素数量），当负载因子过高时链表变长查找效率下降便会自动调整容量，容量也是用某个数组自动由小到大存储。哈希表中的bucket_type对应的是vector\u0026lt;node_ptr\u0026gt;，而node_ptr中存储也是链表节点，后面的函数插入删除基本思路也就确定了。\n感觉在这些容器中频繁使用的知识有，namespace（stl的源码都是在mystl中实现的），typedef为类型重新命名，typename声明其是类型，完美转发，重载，模板元编程等等。\nQ：这些container adapter其实大多都是重载一下运算符，然后缺省其中的一部分函数最终实现的，仅仅这么简单吗，有没有什么注意点？\nA：\n就感觉，，，没什么注意点吧。\n设置一些相关型别，原生指针，常量指针，类指针\n然后构造复制移动析构函数，迭代器的开始结束加加减减，\n容器的容量，增删改查，特定容器接口的特定操作，\n就直接继承底层容器的相关部分，然后不相干的封闭之类的。\n作者唠唠嗑 那就暂时这样子了，中间零散着有些其它的事情不过主线是这个项目大概花了一周。之前其实一直记不太住stl的相关知识，那时候就了解一些语法之类的，不从源码或者底层入手很难有个深刻的印象。找工作嘛，天天念叨着要背八股，八股这词一听就不是什么好词，死记硬背着实对工科学生也不太友好，期待着还是最好从代码和书籍中同步学习知识，这样更落地也更深刻。\n祝愿我们都有美好的未来。\n","date":"2024-09-20T00:00:00Z","permalink":"https://sutdown.github.io/p/mytinystl/","title":"MyTinySTL"},{"content":"从面试角度重新看c++11的Webserver 从main函数开始审视整个流程 WebServer server\n端口1316，ET模式3，timeoutMs60000，优雅退出false；\nMysql配置端口3306，用户名“root”，用户密码“root”，数据库名称“webserver”；\n连接池数量12，线程池数量6，日志开关true，日志等级1，日志同步队列容量1024\n调用webserver的构造函数server() 获取当前工作目录，将资源文件夹加到当前工作目录之后进行处理客户端的资源文件；\n设置httpConn::userCount=0，初始化数据库连接池；\n事件模式初始化，设置为ET模式（边缘触发），同时初始化监听套接字；\n初始化日志系统，指定日志级别，日志路径，队列大小，同时记录相关信息。\n####### Q：线程池数量设置为6的原因\nA：假设有8个CPU和8个线程，每个线程占用一个CPU，同一时间内八个线程同时运行，那么相比较5/6/7个线程，8个线程的效率是最高的。如果线程设置比cpu数量少，那么不能完全利用CPU，但是若线程设置比cpu数量高，那么会出现对于多线程对于效率的提升和多线程之间发生线程切换的消耗的衡量，可能发生线程越多效率越低的情况，最坏程序可能崩溃或者阐述二义性。\n调用server.start(); ####### epoll用法\n只要服务器未关闭while(!isClose_):\n1.倘若超时，获取下一个超时定时器的剩余时间，清楚超时节点。\n作用：\n动态设置epoll_wait的超时时间 协调定时任务和IO时间处理 1 if(timeoutMS_ \u0026gt; 0) { timeMS = timer_-\u0026gt;GetNextTick(); } 2.会调用epoller_-\u0026gt;wait(timeMS)等待触发的事件数量，等待就绪事件\n依次处理每个事件，通过epoller得到事件的文件描述符和事件类型，大致就是下图：\n对于每件事情的处理，首先是处于监听状态DealListen\n然后三次判断，比如关闭连接CloseConn，处理读操作DealRead，处理写操作DealWrite。\n这里可以看出，整个项目的中心点在于：\nQ1：epoll和线程池如何接收来自客户端的请求，为什么说这是一个Reactor高并发模型 Q2：http连接对于请求的接收和处理的过程 Q3：定时器机制的优化的实现，单例和阻塞形成的异步日志系统 Q1epoll和线程池如何接收来自客户端的请求，为什么说这是一个Reactor高并发模型 ####### DealListen\nReactor会先通过监听套接字的可读事件，调用accept接收该事件生成新的连接描述符放入红黑树中；\n新连接建立后，Reactor开始监听该连接的可读事件，threadpool等待客户端发送请求之后调用处理器处理。\nepoll能过监听文件描述符上的事件，包括新连接事件和普通的套接字读写事件。\n如果用户数量在http能接收的范围内，那么调用addclient\naddclient中会将整个新事件利用epoll_ctl注册该监控事件，同时为它设置定时器timer，同时需要设置为非阻塞，默认为阻塞。\n####### DealRead/Dealwrite\n这里面会在threadpool中加上相应的读写任务，同时调用定时器timer中调整最大超时时间\n（注意掌握threadpool和timer，日志的结构，还有线程池中的任务什么时候被取出来执行）\n####### CloseConn\n从epoll中删除，关闭客户端。\n（定时器在到期之后为定时删除）\n####### 哇去，那http连接和sql连接什么时候调用\nsql连接在http连接之内\nHTTP连接： 在threadpool的onread,onwirte中调用了http连接，同时调用httpconn中的process()对client端的数据进行处理，处理完则让epoller修改状态到可写发送回client端，否则继续标识为可读。定时器和关闭连接中也都和httpconn有关。（又有问题，如何将EPOLLOUT的状态发回去）\n就是write函数啊，从write函数中写出client的数据，发送给client；从read中读出数据，然后on process（那为什么onwrite中也有on process）\n因为write中的onprocee的调用原因是在http处理长连接的时候，可以继续处理下一个请求，避免多次调用，当然这是在写完数据的情况下；如果因为写缓冲区满不能写时，会将其状态修改为可写，等待\n（仔细回忆回忆httpconn的结构）\nQ2：http连接对于请求的接收和处理的过程 httprequest是读取缓冲区中的可读数据进行解析；\nhttpresponse是对缓冲区中的可写数据进行写入。\n浏览器发出http连接请求，主线程创建http对象接收请求同时将所有数据存到对应的buffer，将该对象插入任务队列，工作线程从任务队列中取出一个任务进行处理。 工作线程取出任务后，调用process_read函数，通过主从状态机对请求报文进行解析。 解析完成后，跳转do_request函数生成响应报文，通过process_write写入buffer，返回给浏览器端。 ####### write：\n1 2 3 // write核心逻辑在于iov中的多缓冲区的写入。 // 将writev一次性写入文件描述符中，使用ET写数据，动态调整缓冲区指针和长度 // ET模式下，循环继续，直到全部数据写出或者数据小于设定阈值。 ####### read:\n1 2 // 循环读取到没有数据为止， // ET模式下，文件描述符的事件只触发一次，除非有新的数据到达 在ReadFd(int fd, int* saveErrno)中，读取的数据就是从iov中得到的，并且移动的写指针，增加的可读数据。\nReadFd在read中会被调用。所以在各个读写过程中都会有iov和buffer。\nQ3：定时器机制的优化的实现，线程池，单例和阻塞形成的异步日志系统 ####### 定时器\n这里定时器采用的是最小堆，因为本身并不需要多好的有序性，只需要知道有没有任务超时就可以。这个定时器的机制是：\n主循环通过getNextTick()清楚超时节点，关闭相应连接释放资源，然后获取如今最小堆的超时时间，在epoll_wait中设置，若这个连接有IO事件，超时时间延长，调整最小堆。\n定时器，将所有超时时间最小的设置为超时值，作为定时任务处理函数的定时时间。一旦定时任务处理函数被调用，超时时间最小的必然到期，那么可以处理这个定时器。\n然后再从剩余的定时器找出超时时间放在堆的最上面，同时将这段最小时间设置为下一次处理定时任务处理函数的定时值，如此反复实现精确的定时。\n####### 线程池\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 class ThreadPool { public: explicit ThreadPool(size_t threadCount = 8): pool_(std::make_shared\u0026lt;Pool\u0026gt;()) { assert(threadCount \u0026gt; 0); for (size_t i = 0; i \u0026lt; threadCount; ++i) { std::thread([pool = pool_] { std::unique_lock\u0026lt;std::mutex\u0026gt; locker(pool-\u0026gt;mtx); while (true) { if (!pool-\u0026gt;tasks.empty()) { auto task = std::move(pool-\u0026gt;tasks.front()); pool-\u0026gt;tasks.pop(); locker.unlock(); task(); locker.lock(); } else if (pool-\u0026gt;isClosed)break; else pool-\u0026gt;cond.wait(locker); } }).detach(); } } ThreadPool() = default; ThreadPool(ThreadPool\u0026amp;\u0026amp;) = default; ~ThreadPool() { if (static_cast\u0026lt;bool\u0026gt;(pool_)) { std::lock_guard\u0026lt;std::mutex\u0026gt; locker(pool_-\u0026gt;mtx); pool_-\u0026gt;isClosed = true; // 关闭线程池 } pool_-\u0026gt;cond.notify_all(); } template\u0026lt;class F\u0026gt; void AddTask(F\u0026amp;\u0026amp; task) { { std::lock_guard\u0026lt;std::mutex\u0026gt; locker(pool_-\u0026gt;mtx); pool_-\u0026gt;tasks.emplace(std::forward\u0026lt;F\u0026gt;(task)); } pool_-\u0026gt;cond.notify_one(); } private: struct Pool { std::mutex mtx; std::condition_variable cond; bool isClosed; std::queue\u0026lt;std::function\u0026lt;void()\u0026gt;\u0026gt; tasks; }; std::shared_ptr\u0026lt;Pool\u0026gt; pool_; }; ####### 单例模式和异步阻塞的日志系统\n使用单例模式创建日志系统，对服务器运行状态、错误信息和访问数据进行记录，该系统可以实现按天分类，超行分类功能，可以根据实际情况分别使用同步和异步写入两种方式。 其中异步写入方式，将生产者-消费者模型封装为阻塞队列，创建一个写线程，工作线程将要写的内容push进队列，写线程从队列中取出内容，写入日志文件。\n单例模式是什么，为什么设置这个模式？为什么要异步阻塞写日志？你设置了日志等级，什么作用？\n单例模式：某个类只有一个实例，提供一个全局访问点，能够被所有模块共享。由于一个实例，因此禁止了拷贝构造函数，不过好像没有禁止赋值构造，赋值构造应该能够通过move转移拥有者，串了，这是unique_ptr能这么做，单例模式应该不行吧，确实不行，它被禁用了，技术上可以，但是不建议。\n####### 异步和同步\n写入日志时会产生比较多的系统调用，若是某条日志信息过大，会阻塞日志系统，造成系统瓶颈。异步方式采用生产者-消费者模型，具有较高的并发能力。 生产者-消费者模型，并发编程中的经典模型。 以多线程为例，为了实现线程间数据同步，生产者线程与消费者线程共享一个缓冲区，其中生产者线程往缓冲区中push消息，消费者线程从缓冲区中pop消息。 阻塞队列，将生产者-消费者模型进行封装，使用循环数组实现队列，作为两者共享的缓冲区。\n异步日志，将所写的日志内容先存入阻塞队列，写线程从阻塞队列中取出内容，写入日志。可以提高系统的并发性能。 同步日志，日志写入函数与工作线程串行执行，由于涉及到I/O操作，当单条日志比较大的时候，同步模式会阻塞整个处理流程，服务器所能处理的并发能力将有所下降，尤其是在峰值的时候，写日志可能成为系统的瓶颈。 写入方式通过初始化时是否设置队列大小（表示在队列中可以放几条数据）来判断，若队列大小为0，则为同步，否则为异步。若异步，则将日志信息加入阻塞队列，同步则加锁向文件中写。\nwebserver 强推：这个博客写的很仔细，文章也有部分参考他的webserver面试题汇总-AWei’s blog\n总体 1.介绍一下这个项目的框架。 该项目是基于linux的轻量级多线程web服务器。在应用层实现了一个简单的HTTP服务器。\n利用epoll的IO复用技术同时监听多个请求，和线程池处理请求，共同实现了一个多线程的Reactor高并发模型；主线程负责监听，监听到有read/wirte事件之后从socket中读取事件，然后将读取的数据封装成一个请求对象放入队列，睡眠在请求队列上的工作线程被唤醒进行处理，使用状态机对报文进行处理。\n为了进一步对该过程进行优化，添加了定时器用于关闭超时的非活动连接；添加了利用单例模式和阻塞队列实现的异步日志系统，记录服务器的运行状态；添加了数据库连接池实现用户的注册登录功能。\n总结下来主要分为三个模块：\nIO处理单元：httpconn，httprequest，httpresponse，buffer\n逻辑单元：epoll，threadpool，timer，log，blockqueue\n网络存储单元：sqlconnRALL\n再次放上这两张图，感觉还是很好的。\n2.该项目利用c++14实现，如何在项目中体现的。 （2,3两个问题应该可以跳转到后面的相关问题）\n线程池中用了智能指针，lambda函数，右值引用 RALL，auto 原子类型，traits，模板别名，SFINEA 3.你认为你的项目好在哪里，有哪些不足（你做了什么优化） 使用的是“边沿触发（EPOLLET）+非阻塞IO”模式，但是只调用了一次IO，没有循环遍历直到数据为空。这样就产生了一个问题，如下：如果给了1000个连接请求，在60S时间内，但是实际接收的连接数不到一半。这是因为每次触发只调用一次IO时，一次只能accept一个连接请求，那么需要不断的连接请求触发，才能继续accept连接，效率非常低。但是使用了while循环遍历直至数据为空之后，同样的测试，服务器能接收全部的连接请求，其原因就是一次触发就可以处理该次触发所接收的所有连接请求，大大减少了epoll_wait系统调用，减小了内核资源消耗。\n「单 Reactor」的模式存在一个问题，因为一个 Reactor 对象承担所有事件的监听和响应，而且只在主线程中运行，在面对瞬间高并发的场景时，容易成为性能的瓶颈的地方。这个也是WebServer这个项目存在的瓶颈之一。\nEpoll 1.介绍一下你在项目中使用的IO复用模型，相比其它模型为什么选择这个。 1.1五种IO模型 阻塞 非阻塞 IO复用 信号驱动 异步 1.2IO多路复用技术 select/poll 数组链表\nepoll红黑树\n（之前文章里面有，就不再分析了，小林coding也讲的挺好）\n1.3事件处理模式Reactor和Proactor 之前文章也有，不讲了，详细说一下代码中Reactor的使用过程。\n主线程首先通过epoll_wait监听，等待事件发生。\n如果监听到的事件描述符仍然为监听，那么调用accept函数，接收到客户端信息后，分别在timer中添加计时器，epoller中加上文件描述符的信息，同时注意设置事件为非阻塞，主要是为了提高并发能力，阻塞会导致无法处理其它任务，同时可能导致死锁或者饥饿。\n如果监听的事件为读或者写的事件，更新该事件定时器，同时将事件传给线程池，线程池做的事情在线程池那部分再讲。\n2.讲一下ET和LT是什么，你怎么选择，使用的什么模式。对于缺点有什么解决方式。 阻塞和非阻塞，异步和同步\n假设委托内核检测读事件，检测fd的读缓冲区\n水平触发LT(Level Trigger)：只要缓冲区有数据，就一直触发，直到缓冲区无数据。如果用户只读一部分数据，或者用户不读数据，只有缓冲区还有数据，就会一直触发。除非缓冲区的数据读完了，才不通知。LT是一种默认的工作方式，同时支持block和non-block socket。（读一次）\n边缘触发ET(Edge Trigger): 缓冲区从无数据到有数据时，epoll检测到了会给用户通知。如果用户不读数据，数据一直在缓冲区，epoll下次检测的时候就不通知；如果用户只读了部分数据，epoll不通知。直到下一次客户端有新的数据包到达时，epoll_wait才会再次被唤醒。（循环读）\nET只支持no-block socket，在这种模式下，当描述符从未就绪变为就绪时，内核通过epoll告诉你，然后它会假设你知道文件描述符已经就绪，并且不会再为那个文件描述符发送更多的就绪通知，直到做了某些操作导致那个文件描述符不再为就绪状态。如果一直不对这个fd做IO操作（从而导致它在此变为未就绪），内核不会发送更多的通知。\nET在很大程度上减少了epoll事件被重复触发的次数，因此效率比LT高。epoll工作在ET模式的时候，必须使用非阻塞接口，以避免由一个文件句柄的阻塞读/写操作 而把多个文件描述符的任务饿死。\n3.详解一下epoll的底层原理。 去看源码。\n并行和并发\n深入理解epoll\nThreadpool 其实webserver中已经包含了常见的池化思想，比如数据库连接池，线程池\n内存池(Memory Pooling)：预先申请内存，提升申请内存速度，减少内存碎片。 连接池(Connection Pooling)：预先申请数据库连接，提升申请连接的速度，降低系统的开销。 实例池(Object Pooling)：循环使用对象，减少资源在初始化和释放时的昂贵损耗。 1.讲讲线程池是如何实现的，有没有其它实现方式。 Java线程池实现原理及其在美团业务中的实践语言为java，因此不参考代码，只参考理论。\n一方面避免了处理任务时创建销毁线程开销的代价， 另一方面避免了线程数量膨胀导致的过分调度问题，保证了对内核的充分利用。 线程池解决的核心问题就是资源管理问题。在内部实际上构建了一个生产者消费者模型，将线程和任务两者解耦，并不直接关联，从而良好的缓冲任务，复用线程。\n线程池的运行主要分成两部分：任务管理、线程管理。\n任务管理部分充当生产者的角色，当任务提交后，\n线程池会判断该任务后续的流转：（1）直接申请线程执行该任务；（2）缓冲到队列中等待线程执行；（3）拒绝该任务。线程管理部分是消费者，它们被统一维护在线程池内，根据任务请求进行线程的分配，当线程执行完任务后则会继续获取新的任务去执行，最终当线程获取不到任务的时候，线程就会被回收。\n2.手撕线程池 3.detach和join的区别 在声明一个std::thread对象之后，都可以使用detach和join函数来启动被调线程，区别在于两者是否阻塞主调线程。 （1）当使用join()函数时，主调线程阻塞，等待被调线程终止，然后主调线程回收被调线程资源，并继续运行；\n（2）当使用detach()函数时，主调线程继续运行，被调线程驻留后台运行，主调线程无法再取得该被调线程的控制权。当主调线程结束时，由运行时库负责清理与被调线程相关的资源。\njoin：线程执行完成时函数返回。这将会同步此函数返回的时刻和线程中所有操作的完成：这个会阻塞函数主线程的执行，直到构造时调用的函数返回。\n一旦thread对象调用了detach，线程与thread对象将不再有关联，我们也没有直接的方式与线程通信，也不再能join或detach该线程，此时线程的所有权属于C++运行时库，它保证在线程退出时相关资源被回收。分离的线程通常称为守护进程，它们通常在程序的整个生命周期运行，做一些监控、清理工作。同样的thread对象只能被detach一次。\n4.线程池的工作过程，是一直等待吗，处理完一个任务后什么状态，如果某个线程占用时间过久有考虑过这个问题吗 是在一直等待，直到有数据可以处理。处理完任务后继续处于等待状态，当某个线程占用时间过久可以设置超时机制，使用定时器强制终止或者重启该线程，避免长时间的资源浪费。\n5.1000个客户端同时访问请求，如何及时处理每一个呢 该项目是基于IO复用的并发模式。需要注意的是，不是一个客户连接就对应一个线程，本项目通过对子线程循环调用来解决高并发的问题的。 首先在创建线程的同时就调用了detach将线程进行分离，不用单独对工作线程进行回收，资源自动回收。 我们通过在子线程中进行while循环，让每一个线程池中的线程永远都不会停止，主线程监听到IO事件后将任务添加到任务队列中，如果没有任务，线程就一直阻塞等待，有任务线程就抢占式进行处理，直到请求队列为空，表示任务全部处理完成。 如果速度还是慢，那就只能够增大线程池容量，或者考虑集群分布式的做法。\n6.进程间通信方式 6.1简述进程和线程 ####### 进程\n进程是什么\n我们在运行一份有代码的可执行文件时，它会被装载到内存中，然后cpu会执行其中的指令，这个运行的程序成为进程。\n进程存在的过程是什么样子的\n01 创建。申请空白的PCB填写控制和管理信息，比如进程标识等；并且为它分配资源，然后插入就绪队列等待被调度运行。\nPCB是什么，就绪是什么\nPCB，进程控制块，是进程存在的唯一标识，其中包含进程标识符，用户标识符，进程当前状态，资源分配清单，cpu相关信息等。它的底层是链表，相同状态的进程通过链表放在一起，组成我们常说的就绪队列。\n就绪是进程的存在状态，进程还有就绪，运行，阻塞，创建结束这些状态。\n02 终止。三种方式，正常结束，异常结束，外界干预（比如kill）。终止过程，在归还资源前分配cpu资源，将子进程交给其它进程，同时终止PCB将其从相应队列中删除。\n03 阻塞。自己调用阻塞，找到PCB，保护现场，修改状态，停止运行，更换PCB为阻塞队列。\n04 唤醒。阻塞时等待其它进程唤醒，找到PCB，将其从阻塞队列中移除，，状态修改为就绪，进入就绪队列。\n####### 线程\n线程是什么\n线程是进程当中的一条执行流程。同一个进程多个线程之间可以共享代码段，数据段，打开的文件资源；线程也会有自己独立的寄存器和栈。某个进程下的线程之间可以并发，可以共享地址空间和文件资源。\n在此一问线程崩溃了，进程也崩溃吗\n用户线程，内核线程，轻量级线程的区分。\n进程的调度算法（主要是抢占资源），调度原则\n进程是资源分配的基本单位，线程是cpu调度的基本单位。\n6.2 linux进程间通信 进程间通信\n明确，进程间的用户地址空间是独立的，但是内核地址空间共享，需要通过内核\n管道。匿名管道和命名管道。\n消息队列：保存在内核中的消息列表。不适合大数据传输，存在内核和用户态之间的开销。\n共享内存：拿出两块虚拟空间，映射到相同的物理内存。\n信号量：信号量其实是一个整型的计数器，主要用于实现进程间的互斥与同步，而不是用于缓存进程间通信的数据。采用PV操作。\n信号：对于异常情况下的工作模式，就需要用「信号」的方式来通知进程。\n信号是进程间通信里唯一的异步通信机制。\nsocket：用于跨网络和不同主机之间的通信。也可以用于本地进程，有三种方式，基于TCP，基于UDP，本地进程间通信。\n跨进程之间的最大问题是通信。\n而对于线程而言，线程之间的共享变量很容易实现线程间通信，因此线程面临的最大问题是\n线程间冲突\nSqlconnRALL 1.这个数据库连接池是如何实现的，有没有其它方案，有没有改进点和优势点 池是资源的容器，这组资源在服务器启动之初就被完全创建好并初始化，本质上是对资源的复用。\n当系统开始处理客户请求的时候，如果它需要相关的资源，可以直接从池中获取，无需动态分配；当服务器处理完一个客户连接后,可以把相关的资源放回池中，无需执行系统调用释放资源。\n若系统需要频繁访问数据库，则需要频繁创建和断开数据库连接，而创建数据库连接是一个很耗时的操作，也容易对数据库造成安全隐患。\n在程序初始化的时候，集中创建多个数据库连接，并把他们集中管理，供程序使用，可以保证较快的数据库读写速度，更加安全可靠。\n使用单例模式和链表创建数据库连接池，实现对数据库连接资源的复用。\n连接池的功能主要有：初始化，获取连接、释放连接，销毁连接池 连接池中的多线程使用信号量进行通信，使用互斥锁进行同步。\n2.如何保障安全的数据输入（涉及数据库，上回面试碰到了，结果不会，痛定思痛） 3.介绍一下RALL RAII全称是“Resource Acquisition is Initialization”，直译过来是“资源获取即初始化”. RAII的核心思想是将资源或者状态与对象的生命周期绑定，通过C++的语言机制，实现资源和状态的安全管理,智能指针是RAII最好的例子 具体来说：构造函数的时候初始化获取资源，析构函数释放资源\nHTTPConn 1.介绍一下你的报文解析和响应过程，画一下相关的过程图。 2.HTTP的报文格式。GET和POST的区别。HTTP的协议版本。HTTP的状态码等等(都是HTTP计网的八股) 3.HTTP和HTTPs 八股\n4.介绍一下缓冲区的设计 在有IO事件发生时，要先把数据读取到缓冲区中，再从缓冲区读取出来进行处理。\n首先我们在内存中创建一个缓冲区，缓冲区的大小为1024字节，同时定义两个指针，一个读指针，表示的是可以读取的数据的开始位置，一个写指针表示的是可以写入数据的起始位置，开始的时候两根指针都是指向内存开始的地方。 因为缓冲区的大小是固定的大小，但ET模式需要一次性将数据全部读取到缓冲区，那么就有可能装不下数据，所以需要临时创建一个缓冲区来缓解，将存不下的放到临时缓冲区，这样就可以一次性将所有的数据读入，这里利用临时缓冲区的技术是一个分散读的技术，即将数据分散读取到内存中不同的位置，再使用Append将两块内存中的数据拼接到一起。 Append在缓冲区空间不足时会发生扩容，扩容原则：\nWritableBytes() \u0026gt;= len，无需扩容 WritableBytes() \u0026lt; len 已读+可写 \u0026gt;= len，覆盖掉已读内容 已读+可写 \u0026lt; len，resize数组 分散读和分散写 分散读发生在从socket缓冲区读取数据的阶段，第一块内存是用户缓冲区readBuff_，第二块内存是一块临时缓冲区，目的是为了保证将socket缓冲区的数据读完。\n分散写发生在将响应写入socket阶段，第一块内存是writeBuff_，保存着响应的一些信息，组成响应头部；第二块内存使用mmap将被请求的文件映射到共享内存，调用writev将两块内存分散写入socket缓冲区。\n5.状态机 在逻辑处理模块中，响应HTTP请求采用主从状态机来完成 传统的控制流程都是按照顺序执行的，状态机能处理任意顺序的事件，并能提供有意义的响应——即使这些事件发生的顺序和预计的不同。 项目中使用主从状态机的模式进行解析，从状态机（parse_line）负责读取报文的一行，主状态机负责对该行数据进行解析，主状态机内部调用从状态机，从状态机驱动主状态机。 每解析一部分都会将整个请求的check_state状态改变，状态机也就是根据这个状态来进行不同部分的解析跳转的\n状态机的转移图\nTimer 1.为什么用定时器，定时器如何实现的。 由于非活跃连接占用了连接资源，严重影响服务器的性能，通过实现一个服务器定时器，处理这种非活跃连接，释放连接资源。\n由于定时器的触发是由于时间到了，因此只有时间最短的定时器会首先被触发，通过这个原理，我们可以采用最小堆，将按时间顺序排序，堆顶元素是时间最短的定时器，因此只要判断堆顶元素是否被触发即可。只有堆顶定时器的时间到了，才会到其他时间较晚的定时器的时间。 定时器利用结构体将Http连接对应的fd、有效期、回调函数等封装起来。 服务器主循环每建立一个连接则为该连接创建一个定时器节点，插入到最小堆中，主循环通过GetNextTick()清除超时的节点，关闭对应连接，释放连接资源，然后获取最先要超时的连接的超时的时间，并设置下一次epoll_wait()的超时时间为该时间。当已建立的连接有IO事件时，延长这个连接的超时时间，调整最小堆\n2.最小堆和双向链表的相关知识。 时间复杂度：添加：O(logn)， 删除：O(logn) 工作原理： 将所有定时器中超时时间最小的一个定时器的超时值，作为定时任务处理函数的定时值。这样，一旦定时任务处理函数被调用，超时时间最小的定时器必然到期，我们就可以在定时任务处理函数中处理该定时器。 然后，再次从剩余的定时器中找出超时时间最小的一个（堆），并将这段最小时间设置为下一次定时任务处理函数的定时值。如此反复，就实现了较为精确的定时。\nLog 1.介绍一下单例模式和阻塞队列形成的日志，你为什么这么实现日志系统，有什么好处。 使用单例模式创建日志系统，对服务器运行状态、错误信息和访问数据进行记录，该系统可以实现按天分类，超行分类功能，可以根据实际情况分别使用同步和异步写入两种方式。 其中异步写入方式，将生产者-消费者模型封装为阻塞队列，创建一个写线程，工作线程将要写的内容push进队列，写线程从队列中取出内容，写入日志文件\n2.手写单例模式。单例模式的双重锁和静态成员变量 3.阻塞队列的原因 异步和同步 写入日志时会产生比较多的系统调用，若是某条日志信息过大，会阻塞日志系统，造成系统瓶颈。异步方式采用生产者-消费者模型，具有较高的并发能力。 生产者-消费者模型，并发编程中的经典模型。 以多线程为例，为了实现线程间数据同步，生产者线程与消费者线程共享一个缓冲区，其中生产者线程往缓冲区中push消息，消费者线程从缓冲区中pop消息。 阻塞队列，将生产者-消费者模型进行封装，使用循环数组实现队列，作为两者共享的缓冲区。\n异步日志，将所写的日志内容先存入阻塞队列，写线程从阻塞队列中取出内容，写入日志。可以提高系统的并发性能。 同步日志，日志写入函数与工作线程串行执行，由于涉及到I/O操作，当单条日志比较大的时候，同步模式会阻塞整个处理流程，服务器所能处理的并发能力将有所下降，尤其是在峰值的时候，写日志可能成为系统的瓶颈。 写入方式通过初始化时是否设置队列大小（表示在队列中可以放几条数据）来判断，若队列大小为0，则为同步，否则为异步。 若异步，则将日志信息加入阻塞队列，同步则加锁向文件中写\n其它 1.webbench的原理 WebBench是一款在Linux下使用非常简单的压力测试工具。 原理：父进程 fork 若干个子进程，每个子进程在用户要求时间或默认的时间内对目标 web 循环发出实际访问请求，父子进程通过管道进行通信，子进程通过管道写端向父进程传递在若干次请求访问完毕后记录到的总信息，父进程通过管道读端读取子进程发来的相关信息，子进程在时间到后结束，父进程在所有子进程退出后统计并给用户显示最后的测试结果，然后退出。Webbench最多可以模拟3万个并发连接去测试网站的负载能力。 2.web的域名是主动申请的吗 3.你做的这个项目测试性能了吗，测试性能的指标是什么，你的优化方式是什么提升了多少性能。 原项目用webbench测试，4核8G的服务器能达到百万级QPS，我在自己的服务器上使用webbench测试过，client数量达到4800是0 failed，但到4900，fork就会失败，提示进程已达上限。这时的QPS只有2500左右。\n4.为了减少系统调用所作的工作（线程池，缓存机制：零拷贝，读写锁） 线程池 零拷贝 ","date":"2024-09-18T00:00:00Z","permalink":"https://sutdown.github.io/p/webserver-sum/","title":"Webserver-sum"},{"content":" [!Note]\n推荐书籍：\nlinux高性能服务器编程\nUNIX环境高级编程\nUNIX网络编程\n具体模块：\n线程同步封装类，半同步半反应堆线程池\nHTTP连接管理\n服务器优化：定时器处理，日志系统，\n压测，数据库连接池，注册登录\n环境配置 绷，果然配环境始终都是很难的一步。\n这个说是要在ubuntu16.04环境上运行，搭建环境的话据说所知的方法还挺多，比如直接下载一个虚拟机软件再创建所需的虚拟机环境，可以用VMware或者VirtualBox，这两电脑上也都有，不过这两软件都用过但不是很熟悉，能再研究研究；要不然也可以在云平台上启动Ubuntu的实例，搭建远程的测试环境，没用过待定；再者就是用docker容器模拟环境，其实也可以。\ndocker和virtualbox有个坑点在于docker依赖底层的Hyper-v或者WSL 2，但是virtualbox的一些旧版本在启动hyper-v的情况下会遇到性能问题甚至无法正常启动。\nvirtualbox配置Ubuntu出现问题解决方案\n软件开发之前，程序员首先面临的第一个问题就是环境配置，一个软件能成功运行也需要依靠当前操作系统的设置，以及各种库和组件的安装。只有这些都正确，程序才能成功运行，如果不同的版本或者os和当前环境不兼容，那么程序便不能运行。\n对于此，从前采用的大多是virtual machine，也就是虚拟机，在操作系统里运行另一种操作系统，但是虚拟机的资源占用多，冗余步骤多，启动也慢，对于这些问题，linux发展了另一种虚拟化技术，也就是linux containers，即linux容器。它不模拟一个完整的操作系统，只是对进出进行隔离，对于其中的进程而言，它接触的都是虚拟的资源进而实现与底层系统之间的隔离。\nDocker vs virtual machine\nDocker入门教程 参考资料：阮一峰 - Docker 入门教程\nDocker 属于 Linux 容器的一种封装，提供简单易用的容器使用接口。\n它的用途：\n**（1）提供一次性的环境。**比如，本地测试他人的软件、持续集成的时候提供单元测试和构建的环境。\n**（2）提供弹性的云服务。**因为 Docker 容器可以随开随关，很适合动态扩容和缩容。\n**（3）组建微服务架构。**通过多个容器，一台机器可以跑多个服务，因此在本机就可以模拟出微服务架构。\n**Docker 把应用程序及其依赖，打包在 image 文件里面。**只有通过这个文件，才能生成 Docker 容器。image 文件可以看作是容器的模板。Docker 根据 image 文件生成容器的实例。同一个 image 文件，可以生成多个同时运行的容器实例。\nDockerfile 是一个文本文件，用来配置 image。\n虚拟机的选择：VirtualBox还是VMware 1.性能\nVirtualBox：性能适中，适合日常开发和测试任务。由于它是开源项目，可能在高性能任务中表现不如 VMware 稳定。 VMware：总体性能表现优于 VirtualBox，尤其是在运行多个虚拟机、复杂的虚拟化任务和大规模企业级应用时，VMware 的虚拟机在资源管理、响应速度和 I/O 性能上更加出色。 2.易用性\nVirtualBox：安装简单，用户界面直观，适合入门用户和中小型项目。配置文件（如 Vagrantfile）的支持使得虚拟机管理非常灵活，特别是结合 Vagrant 使用时。 VMware：界面更加专业化，功能强大，虽然略显复杂，但提供更多企业级管理和高级功能。适合需要更多虚拟化细节和控制的用户。 3.功能特性\nVirtualBox\n开源、跨平台（支持 Windows、Linux、macOS）。 支持虚拟快照功能，可以保存虚拟机不同的状态。 支持 3D 加速、USB 设备和共享文件夹等功能，但与 VMware 相比，某些高级功能如虚拟网络配置和设备支持上稍显不足。 VMware：\n提供更加成熟和高效的快照、克隆功能。 提供更好的 3D 加速、USB 设备支持和网络管理。 企业级产品（如 vSphere）支持集群、容错、高可用性等高级功能，非常适合大型企业级应用和数据中心虚拟化需求。 4.企业级应用\nVirtualBox：更适合个人开发、测试环境和小型企业的简单虚拟化需求。 VMware：提供丰富的企业级解决方案，尤其是 VMware vSphere 等企业级虚拟化平台，支持数据中心管理、云计算、高可用性等企业级功能，是大型企业和云服务提供商的首选。 总结：\nVirtualBox：免费、开源、跨平台，适合个人开发者、初学者和中小型项目的虚拟化需求。适合需要快速部署开发环境或简单测试任务的场景。 VMware：性能更优，功能更加丰富，特别是在高级虚拟化管理和企业级功能上领先。适合企业用户和对虚拟机性能、可靠性要求较高的场景，尤其是数据中心和生产环境。 Linux高性能服务器编程 第一篇 tcp/ip协议详解 Chapter 1 TCP/IP协议族 Chapter 2 IP协议 Chapter 3 TCP协议 Chapter 4 TCP/IP通信案例 第二篇 深入解析高性能服务器编程 服务器程序框架 CS模型 IO模型 线程接收整个过程 webserver讲解\n服务器如何接收客户端的报文？ 服务端使用socket监听来自用户的请求，epoll实现。\nepoll：IO复用技术，实现对监听socket和连接socket的同时监听。但是本身是堵塞的。因此需要用线程池采取多线程并发为每个就绪的文件描述符分配逻辑单元处理。\n服务器通常要处理三类事件：IO事件，信号以及定时事件。两种处理方式：\nReactor模式：主线程只负责监听文件描述符上是否有事件发生，有的话通知工作线程，工作线程中会完成读写数据，接受新连接以及处理客户请求。同步IO\nProactor模式：所有IO都交给主线程和内核处理，工作线程只负责业务逻辑。异步IO\nReactor和Proactor模式的区别。\n本项目采用同步IO模拟的Proactor事件处理模式。\n服务器如何处理以及响应接收的HTTP报文 并发：\n同步：内核向应用程序通知IO的就绪事件，由应用程序完成IO读写。\n异步：内核向应用程序通知IO的完成事件，由内核完成IO读写。\n并发中，同步指程序按照代码序列顺序执行，异步指程序的执行由系统事件驱动\n半同步/半异步模式：\n同步用于处理客户逻辑，异步用于处理IO事件。\n领导者/追随者模式\n提高服务器性能的相关建议： pool 线程池并发处理用户请求。主线程负责读写，工作线程 处理逻辑，（锁和信号量实现线程同步，保证原子性）\n线程池中线程数目的确定。\n线程池中的线程数量最直接的限制因素是中央处理器(CPU)的处理器(processors/cores)的数量N：如果你的CPU是4-cores的，对于CPU密集型的任务(如视频剪辑等消耗CPU计算资源的任务)来说，那线程池中的线程数量最好也设置为4（或者+1防止其他因素造成的线程阻塞）；对于IO密集型的任务，一般要多于CPU的核数，因为线程间竞争的不是CPU的计算资源而是IO，IO的处理一般较慢，多于cores数的线程将为CPU争取更多的任务，不至在线程处理IO的过程造成CPU空闲导致资源浪费。\n公式：最佳线程数 = CPU当前可使用的Cores数 * 当前CPU的利用率 * (1 + CPU等待时间 / CPU处理时间)\n数据复制 避免不必要的数据复制 上下文切换和锁 进程切换或者线程切换导致的系统开销。 对共享资源的加锁保护，锁通常是导致服务器效率低下的一个因素。 IO复用 如何同时监听多个过程 IO复用技术的常见情况：\n客户端\n同时处理多个socket，比如connect非阻塞技术 同时处理用户和网络连接，比如聊天室程序 服务端\n同时处理TCP和UDP请求，比如回射服务器 同时监听多个端口或者多种服务。比如xineted服务器 linux下有三种IO复用方式：epoll，select和poll\nepoll：底层红黑树。 用途\nepoll使用一组函数完成任务，而不是单个函数 epoll把用户关心的文件描述符的事件放在内核的一个事件表中，无需像select和poll在每次调用都重复传入文件描述符或事件集，但epoll会用一个额外的文件描述符，唯一标识内核中的事件表。 int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);\nint epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout);\nselect：底层线性表。 用途：在一段指定时间内，监听用户感兴趣的文件描述符上的可读，可写和异常事件。\nint select(int nfds, fd_set* readfds, fd_set* writefds, fd_set* exceptfds, struct timeval* timeout);\npoll：底层链表。 用途：指定时间内轮询一定数量的文件描述符，测试其中是否有就绪者。\nint poll(struct pollfd* fds, nfds_t nfds, int timeout);\n三种复用方式的区别。\n本项目采用epoll。\nselect的参数类型fd_set没有将文件描述符和事件绑定，需要提供三种类型的参数区分不同事件。\npoll的参数类型pollfd将文件描述符和事件都定义其中，时间统一处理。\n二者都返回整个用户注册的事件集合，通常工作在LT模式。\nepoll在内核中维护一个事件表，对事件表进行增删改查，每次从内核事件中取得用户注册的事件，无需反复从用户空间读入。工作在ET模式。\nLT和ET模式 LT：检测到有事件发生，应用程序可以不立即处理。\nET：检测到有事件发生，应用程序可以立即处理。\n线程池和进程池 进程池是由服务器预先创建的一组子进程，这些子进程的数目在3-10个之间。httpd守护进程就是使用包含7个子进程的进程池实现并发的。线程池中的线程数量应该和cpu数量差不多。\n进程池中的所有子进程有相同的代码和属性，当新任务到来时，主进程通过某种方式选择进程池中的某一个子进程为之服务。\n压测：webbench的原理 Code Buffer Non-blocking IO 的核心思想是避免阻塞在 read() 或 write() 或其他 IO 系统调用上，这样可以最大限度地复用 thread-of-control，让一个线程能服务于多个 socket 连接。IO 线程只能阻塞在 IO-multiplexing 函数上，如 select()/poll()/epoll_wait()。这样一来，应用层的缓冲是必须的，每个 TCP socket 都要有 stateful 的 input buffer 和 output buffer。\n对于线程安全上：\n对于 input buffer，onMessage() 回调始终发生在该 TcpConnection 所属的那个 IO 线程，应用程序应该在 onMessage() 完成对 input buffer 的操作，并且不要把 input buffer 暴露给其他线程。这样所有对 input buffer 的操作都在同一个线程，Buffer class 不必是线程安全的。 对于 output buffer，应用程序不会直接操作它，而是调用 TcpConnection::send() 来发送数据，后者是线程安全的。 [!Note]\n在不同线程之间实现数据转移时，我们的代码中仍然以数据拷贝的方式进行，这样是绝对安全的，但是略微存在些性能损失。\n有种思路是用swap实现高效的线程间数据转移，但是还未实现。\n在设计缓冲区大小时，\n一方面我们希望减少系统调用，也就是不要频繁调用read()，这样子似乎应该准备更大的缓冲区；但是另一方面我们希望系统减少内存占用，当缓冲区过大连接过多时，平白占用大量内存导致缓冲区利用率低同样是个很糟糕的问题。\n因此我们开辟了一个栈上临时空间，具体过程如下：\n在栈上准备一个 65536 字节的 stackbuf，然后利用 readv() 来读取数据，iovec 有两块，第一块指向 muduo Buffer 中的 writable 字节，另一块指向栈上的 stackbuf。这样如果读入的数据不多，那么全部都读到 Buffer 中去了；如果长度超过 Buffer 的 writable 字节数，就会读到栈上的 stackbuf 里，然后程序再把 stackbuf 里的数据 append 到 Buffer 中。\nBUFFER的设计 源码略。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 #ifndef BUFFER_H #define BUFFER_H #include ... class Buffer { public: /*构造和析构*/ Buffer(int initBuffSize = 1024); ~Buffer() = default; /*buffer中可写，可读，可预留空间*/ size_t WritableBytes() const; size_t ReadableBytes() const ; size_t PrependableBytes() const; /*可读的第一个地址，确保可读长度，移动可写长度*/ const char* Peek() const; void EnsureWriteable(size_t len); void HasWritten(size_t len); /*移动可读长度，读到末尾*/ void Retrieve(size_t len); void RetrieveUntil(const char* end); /*读完所有，读写下表全部归零*/ void RetrieveAll() ; std::string RetrieveAllToStr(); /*写指针位置*/ const char* BeginWriteConst() const; char* BeginWrite(); /*对stackbuf的操作，添加str到缓冲区*/ void Append(const std::string\u0026amp; str); void Append(const char* str, size_t len); void Append(const void* data, size_t len); void Append(const Buffer\u0026amp; buff); /*读写的交互接口*/ ssize_t ReadFd(int fd, int* Errno); ssize_t WriteFd(int fd, int* Errno); private: char* BeginPtr_(); const char* BeginPtr_() const; void MakeSpace_(size_t len); // 扩展空间 /*atomic是一种原子类型，保证多线程的情况下，安全高性能的执行程序更新变量*/ std::vector\u0026lt;char\u0026gt; buffer_; // 存储实体 std::atomic\u0026lt;std::size_t\u0026gt; readPos_; // 读缓冲区 std::atomic\u0026lt;std::size_t\u0026gt; writePos_; // 写缓冲区 }; #endif //BUFFER_H 参考链接：Muduo 设计与实现之一：Buffer 类的设计\nLog 本项目中，使用单例模式创建日志系统，对服务器运行状态、错误信息和访问数据进行记录，该系统可以实现按天分类，超行分类功能，可以根据实际情况分别使用同步和异步写入两种方式。\n其中异步写入方式，将生产者-消费者模型封装为阻塞队列，创建一个写线程，工作线程将要写的内容push进队列，写线程从队列中取出内容，写入日志文件。\n日志系统大致可以分成两部分，其一是单例模式与阻塞队列的定义，其二是日志类的定义与使用。\n日志，由服务器自动创建，并记录运行状态，错误信息，访问数据的文件。\n同步日志，日志写入函数与工作线程串行执行，由于涉及到I/O操作，当单条日志比较大的时候，同步模式会阻塞整个处理流程，服务器所能处理的并发能力将有所下降，尤其是在峰值的时候，写日志可能成为系统的瓶颈。\n异步日志，将所写的日志内容先存入阻塞队列，写线程从阻塞队列中取出内容，写入日志。\n阻塞队列，将生产者-消费者模型进行封装，使用循环数组实现队列，作为两者共享的缓冲区。\n单例模式，最简单也是被问到最多的设计模式之一，保证一个类只创建一个实例，同时提供全局访问的方法。私有化它的构造函数，以防止外界创建单例类的对象；使用类的私有静态指针变量指向类的唯一实例，并用一个公有的静态方法获取该实例。\n生产者-消费者模型，并发编程中的经典模型。以多线程为例，为了实现线程间数据同步，生产者线程与消费者线程共享一个缓冲区，其中生产者线程往缓冲区中push消息，消费者线程从缓冲区中pop消息。\nlog的设计 在init中，\n如果队列大于0则是异步方式，用unique_ptr创建日志和新线程；如果队列等于0则采取同步方式。更新isAysnc变量，然后初始化日志，设置文件名，后缀等，确保日志文件能够成功打开。\n然后调用write_log，\n先判断日期变化或者日志能容纳的总内容是否超过，日期变化或者超过都重新生成新的日志名。\n异步的话，将要写的内容放进队列，写线程从队列中取出再写入日志；如果同步，直接写入日志。\n逻辑： 获取当前时间、生成日志文件名、处理可变参数以格式化日志内容、以及决定写入方式（同步或异步）。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 #ifndef LOG_H #define LOG_H #include ... class Log { public: /*初始化日志，日志保存路径，文件后缀，最大容量*/ void init(int level, const char* path = \u0026#34;./log\u0026#34;, const char* suffix =\u0026#34;.log\u0026#34;, int maxQueueCapacity = 1024); /*懒汉模式，局部静态变量法*/ static Log* Instance(); /*异步日志的写线程函数*/ static void FlushLogThread(); /*写日志*/ void write(int level, const char *format,...); void flush(); int GetLevel(); void SetLevel(int level); bool IsOpen() { return isOpen_; } private: Log(); /*添加日志等级*/ void AppendLogLevelTitle_(int level); virtual ~Log(); /*异步写日志*/ void AsyncWrite_(); private: static const int LOG_PATH_LEN = 256; static const int LOG_NAME_LEN = 256; static const int MAX_LINES = 50000; const char* path_; // 路径 const char* suffix_; // 后缀 int MAX_LINES_; // 最大日志行数 int lineCount_; // 日志行数 int toDay_; // 按照当天日期区分文件 bool isOpen_; Buffer buff_; // 输出缓冲区 int level_; // 日志等级 bool isAsync_; // 是否异步 FILE* fp_; // 打开log的文件指针 std::unique_ptr\u0026lt;BlockDeque\u0026lt;std::string\u0026gt;\u0026gt; deque_; // 阻塞队列 std::unique_ptr\u0026lt;std::thread\u0026gt; writeThread_; // 写线程的指针 std::mutex mtx_; // 同步日志的锁 }; #define LOG_BASE(level, format, ...) \\ do {\\ Log* log = Log::Instance();\\ if (log-\u0026gt;IsOpen() \u0026amp;\u0026amp; log-\u0026gt;GetLevel() \u0026lt;= level) {\\ log-\u0026gt;write(level, format, ##__VA_ARGS__); \\ log-\u0026gt;flush();\\ }\\ } while(0); /* * 四个宏定义，用于不同类型日志输出，也就是外部使用日志接口 * ...表示可变参数，_VA_ARGS_就是将...的值复制到这里 * ##的作用在于可变参数为0时，去掉前面的逗号 */ #define LOG_DEBUG(format, ...) do {LOG_BASE(0, format, ##__VA_ARGS__)} while(0); #define LOG_INFO(format, ...) do {LOG_BASE(1, format, ##__VA_ARGS__)} while(0); #define LOG_WARN(format, ...) do {LOG_BASE(2, format, ##__VA_ARGS__)} while(0); #define LOG_ERROR(format, ...) do {LOG_BASE(3, format, ##__VA_ARGS__)} while(0); #endif //LOG_H threadpool和sqlconnpool threadpool 线程同步问题涉及到了互斥量，条件变量。在代码中，将互斥锁，条件变量，关闭状态，工作队列共同封装，由共享指针管理。\ndetach()的作用 detach() 是 C++ 标准库中的 std::thread 类的一个成员函数，用于将一个线程与其创建的 std::thread 对象分离。\n分离线程：调用 detach() 后，线程将变为后台线程，独立运行。主线程或其他线程不再需要等待它结束。线程的资源会在执行完任务后自动回收。\n与 join() 的区别：join() 会阻塞调用它的线程，直到目标线程结束，而 detach() 则让目标线程在后台独立运行，调用它的线程可以立即继续执行。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 #ifndef THREADPOOL_H #define THREADPOOL_H #include... class ThreadPool { public: /*explicit： *主要用于防止隐式类型转换.当构造函数被声明为explicit时， *编译器将禁止使用该构造函数进行隐式的类型转换。 *作用是防止编译器在某些情况下进行自动类型转换，导致不可预期的行为。 */ explicit ThreadPool(size_t threadCount = 8): pool_(std::make_shared\u0026lt;Pool\u0026gt;()) { assert(threadCount \u0026gt; 0); // 确保线程数目大于0 for(size_t i = 0; i \u0026lt; threadCount; i++) { std::thread([pool = pool_] { /*该线程的互斥锁*/ std::unique_lock\u0026lt;std::mutex\u0026gt; locker(pool-\u0026gt;mtx); while(true) { if(!pool-\u0026gt;tasks.empty()) {/*池子不为空时*/ /*从队列中获取一个任务然后执行 * 执行时可以加锁 * 获取任务时采用move可以节省资源 */ auto task = std::move(pool-\u0026gt;tasks.front()); pool-\u0026gt;tasks.pop(); locker.unlock(); task(); locker.lock(); } /*线程池关闭时跳出*/ else if(pool-\u0026gt;isClosed) break; /*线程池为空时，利用条件变量等待*/ else pool-\u0026gt;cond.wait(locker); } }).detach();/*线程类的一个成员函数，用于将线程和创建的thread对象分离*/ } } ThreadPool() = default; ThreadPool(ThreadPool\u0026amp;\u0026amp;) = default; ~ThreadPool() { if(static_cast\u0026lt;bool\u0026gt;(pool_)) { { /*c++11引入的RALL资源获取即初始化风格的做机制*/ /*当前作用域自动加锁，作用域结束自动释放锁*/ std::lock_guard\u0026lt;std::mutex\u0026gt; locker(pool_-\u0026gt;mtx); pool_-\u0026gt;isClosed = true; // 关闭线程池 } pool_-\u0026gt;cond.notify_all(); // 唤醒所有线程退出 } } template\u0026lt;class F\u0026gt; void AddTask(F\u0026amp;\u0026amp; task) { { /*完美转发添加任务*/ std::lock_guard\u0026lt;std::mutex\u0026gt; locker(pool_-\u0026gt;mtx); pool_-\u0026gt;tasks.emplace(std::forward\u0026lt;F\u0026gt;(task)); } // 唤醒一个线程 pool_-\u0026gt;cond.notify_one(); } private: struct Pool { std::mutex mtx; // 互斥锁，保护共享数据 std::condition_variable cond; // 条件变量，管理任务的等待和通知机制 bool isClosed; // 标志线程池是否关闭 std::queue\u0026lt;std::function\u0026lt;void()\u0026gt;\u0026gt; tasks; // 任务队列，存放待执行的任务 }; std::shared_ptr\u0026lt;Pool\u0026gt; pool_; }; #endif //THREADPOOL_H sqlconnpool 连接池存在的必要性 由于服务器需要频繁的访问数据库，也就是需要频繁创建和断开数据库连接，该过程是一个很耗时的操作，也容易对数据库造成安全隐患。 在程序初始化的时候，集中创建多个数据库连接，并把他们集中管理，供程序使用，可以保证较快的数据库读写速度，更加安全可靠。 RALL RALL是c++语言的一种管理资源，避免泄漏的惯用法。利用c++构造的对象最终会被销毁的原则，使得让对象在构造时获取对应的资源，在生命期结束时释放构造时获取的资源。\n在编程时，我们往往会利用申请资源，使用资源，释放资源三步骤。如果忘记释放资源极易造成资源泄漏。\n比如常见的unique_ptr，lock_guard采用了RALL的机制。因此避免了忘记释放的缺陷，c++新特性可以大大减少代码量。\n连接池的底层实现 在连接池中，使用了信号量管理资源的数量。锁则是为了访问公共资源的时候使用。\n不同的是，信号量的使用要先使用信号量sem_wait再上锁，而条件变量的使用要先上锁再使用条件变量wait。\npthread_cond_wait\n“ 调用者把锁住的互斥量传给函数，函数然后自动把调用线程放到等待条件的线程列表上，**对互斥量解锁。**这就关闭了条件检查和线程进入休眠状态等待条件改变这两个操作之间的时间通道，这样线程就不会错过条件的任何变化。pthread_cond_wait返回时，互斥量再次被锁住。”\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 #ifndef SQLCONNPOOL_H #define SQLCONNPOOL_H #include... class SqlConnPool { public: /*单例模式*/ static SqlConnPool *Instance(); /*得到连接和释放连接*/ MYSQL *GetConn(); void FreeConn(MYSQL * conn); int GetFreeConnCount(); void Init(const char* host, int port, const char* user,const char* pwd, const char* dbName, int connSize); void ClosePool(); private: SqlConnPool(); ~SqlConnPool(); int MAX_CONN_; // 最大连接数 int useCount_, freeCount_; std::queue\u0026lt;MYSQL *\u0026gt; connQue_; // 连接队列 std::mutex mtx_; // 互斥锁 sem_t semId_; // 信号量 }; /* 资源在对象构造初始化 资源在对象析构时释放*/ class SqlConnRAII { public: SqlConnRAII(MYSQL** sql, SqlConnPool *connpool) { assert(connpool); // 建立连接 *sql = connpool-\u0026gt;GetConn(); sql_ = *sql; connpool_ = connpool; } ~SqlConnRAII() { // 释放连接 if(sql_) { connpool_-\u0026gt;FreeConn(sql_); } } private: MYSQL *sql_; SqlConnPool* connpool_; }; #endif // SQLCONNPOOL_H HTTP httprequest 过程逻辑如下：\n利用search从缓冲区中读取响应的报文片段。\n1 2 const char* lineEnd = search(buff.Peek(), buff.BeginWriteConst(), CRLF, CRLF + 2); std::string line(buff.Peek(), lineEnd); 建立请求报文的有限状态机。请求报文中一般包含post和get。该有限状态机的作用原理在于每次读取缓冲区中的一行进行匹配，直至状态变成finish\nREQUEST_LINE——ParseRequestLine_(line)\n1 2 3 4 5 6 7 case REQUEST_LINE: /*解析请求行，method,path,version*/ if(!ParseRequestLine_(line)) { // 拆解请求行 return false; } ParsePath_(); // 进一步检查路径的有效性安全性 break; 正则表达式\n[^ ]：表示非空格的字符，[] 是字符集合，^ 在集合中表示取反，所以 [^ ] 代表任何不是空格的字符。\n*：表示前面的字符可以出现 零次或多次，即可以匹配任意长度的非空格字符（包括空）。\n()：圆括号表示捕获组，用于将匹配的内容保存起来，以供后续引用（如 subMatch[1]）。\n该部分代码直接匹配即可。\n1 2 regex patten(\u0026#34;^([^ ]*) ([^ ]*) HTTP/([^ ]*)$\u0026#34;); // method path HTTP/version HEADERS——ParseHeader_(line)\n1 2 3 4 5 6 7 8 9 10 // \u0026lt;字段名\u0026gt;:\u0026lt;可选空格\u0026gt;\u0026lt;字段值\u0026gt; regex patten(\u0026#34;^([^:]*): ?(.*)$\u0026#34;); smatch subMatch; if(regex_match(line, subMatch, patten)) { header_[subMatch[1]] = subMatch[2]; } else { // 匹配失败时转换状态 state_ = BODY; } BODY——ParseBody_(line)\n调用ParsePost();，因为只有post中有请求体\n1.解析 POST 请求：检查请求方法和 Content-Type 是否符合条件。\n2.解析表单数据：从 URL 编码的请求体中提取用户提交的数据。\n1 2 3 4 /*URL 编码 是一种用于将数据编码为有效 URL 字符串的方式，其中特殊字符（如空格、\u0026amp;、= 等）会被替换为特定的字符序列： *空格编码为 %20 *特殊字符（如 \u0026amp;, =）不会直接出现在值中，而需要进行编码 *其他特殊字符也会有类似的编码方式，如 %3A 表示冒号 : 3.路径判断：检查路径是否是需要处理的特定页面（如登录或注册页面）。\n4.登录或注册逻辑：根据路径进行用户验证，判断是登录还是注册，并根据结果决定跳转的页面。\n1 2 3 4 5 6 7 8 /* 从sql池中取出数据； 登录： 查询用户名以及密码，看用户名和密码是否和sql语句中查出来的表格匹配。 注册： 添加sql语句，插入自己的用户名和密码，用户名再其中存在则注册失败，否则注册成功 释放数据库连接池； */ 跳出循环，跳出末尾回车换行符，记录日志。\nhttpresponse httprequest是读取缓冲区中的可读数据进行解析；\nhttpresponse是对缓冲区中的可写数据进行写入。\n响应报文的四个部分：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 #ifndef HTTP_RESPONSE_H #define HTTP_RESPONSE_H #include... class HttpResponse { public: HttpResponse(); ~HttpResponse(); void Init(const std::string\u0026amp; srcDir, std::string\u0026amp; path, bool isKeepAlive = false, int code = -1); void MakeResponse(Buffer\u0026amp; buff); // 该函数中会确定状态码，调用private中的三个添加函数 void UnmapFile(); char* File(); size_t FileLen() const; void ErrorContent(Buffer\u0026amp; buff, std::string message); int Code() const { return code_; } private: void AddStateLine_(Buffer \u0026amp;buff); // 添加状态行 void AddHeader_(Buffer \u0026amp;buff); // 添加消息报头 void AddContent_(Buffer \u0026amp;buff); // 添加响应正文 void ErrorHtml_(); std::string GetFileType_(); int code_; // 状态码 bool isKeepAlive_; // http保活机制 /*生成状态码*/ std::string path_; std::string srcDir_; // 源目录 char* mmFile_; // 映射文件 struct stat mmFileStat_; // 文件状态信息 static const std::unordered_map\u0026lt;std::string, std::string\u0026gt; SUFFIX_TYPE; // 后缀类型 static const std::unordered_map\u0026lt;int, std::string\u0026gt; CODE_STATUS; // http状态码 static const std::unordered_map\u0026lt;int, std::string\u0026gt; CODE_PATH; // 400，403，404对应的错误路径 }; #endif //HTTP_RESPONSE_H 主要函数：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 void HttpResponse::MakeResponse(Buffer\u0026amp; buff) { /* 判断请求的资源文件 */ if(stat((srcDir_ + path_).data(), \u0026amp;mmFileStat_) \u0026lt; 0 || S_ISDIR(mmFileStat_.st_mode)) { code_ = 404; // 文件状态信息小于0或者文件是一个目录 } else if(!(mmFileStat_.st_mode \u0026amp; S_IROTH)) { code_ = 403; // 文件没有其它用户的读取权限时 } else if(code_ == -1) { code_ = 200; // 文件成功找到 } ErrorHtml_(); // 根据 HTTP 响应码查找并设置相应的错误页面路径 AddStateLine_(buff); // buff.Append(HTTP/1.1 状态码 状态) AddHeader_(buff); // 添加connection和content-type AddContent_(buff); // 映射内存，追加文件长度，错误页面单独处理 } httpconn HTTP整体流程： 浏览器发出http连接请求，主线程创建http对象接收请求同时将所有数据存到对应的buffer，将该对象插入任务队列，工作线程从任务队列中取出一个任务进行处理。 工作线程取出任务后，调用process_read函数，通过主从状态机对请求报文进行解析。 解析完成后，跳转do_request函数生成响应报文，通过process_write写入buffer，返回给浏览器端。 注意：在生成的响应报文中并没有返回消息的具体内容，文件也并不在缓冲区，因此传输的时候采取分块写的方式，一块传输buff里面的内容，另一块传输内存映射的文件指针。\nwrite： 1 2 3 // write核心逻辑在于iov中的多缓冲区的写入。 // 将writev一次性写入文件描述符中，使用ET写数据，动态调整缓冲区指针和长度 // ET模式下，循环继续，直到全部数据写出或者数据小于设定阈值。 read: 1 2 // 循环读取到没有数据为止， // ET模式下，文件描述符的事件只触发一次，除非有新的数据到达 httpconn 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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 #ifndef HTTP_CONN_H #define HTTP_CONN_H #include... class HttpConn { public: HttpConn(); ~HttpConn(); void init(int sockFd, const sockaddr_in\u0026amp; addr); // 将请求报文的内容读取到读缓冲区里面 ssize_t read(int* saveErrno); // 将从缓冲区中得到的响应报文写到浏览器 ssize_t write(int* saveErrno); void Close(); int GetFd() const; int GetPort() const; const char* GetIP() const; sockaddr_in GetAddr() const; // 解析请求报文和生成响应报文 bool process(); int ToWriteBytes() { return iov_[0].iov_len + iov_[1].iov_len; } bool IsKeepAlive() const { return request_.IsKeepAlive(); } static bool isET; static const char* srcDir; static std::atomic\u0026lt;int\u0026gt;; private: int fd_; struct sockaddr_in addr_; bool isClose_; int iovCnt_; struct iovec iov_[2]; Buffer readBuff_; // 读缓冲区 Buffer writeBuff_; // 写缓冲区 HttpRequest request_; HttpResponse response_; }; #endif //HTTP_CONN_H [!Warning]\n啊啊啊啊，有种没理清楚的感觉。虽然大致知到不过没连贯起来。\n比如为什么有两个缓冲区，buffer中不是一个就解决了吗？在http连接管理的过程中，肯定会涉及缓冲区，线程池，连接池，发送和接收，这些的逻辑关系是什么？还有日志的代码是怎么和log相关联的。如今你已经学完了buffer，pool，log，http，这四个如何将其连贯起来。后面还会多个timer,epoller两部分以及webserver最终的综合。\n先把已经学完的四个理清楚再去看最后的部分。\n详见下图：\nTimer 待办:https://zhuanlan.zhihu.com/p/668916073\n非活跃，是指客户端（这里是浏览器）与服务器端建立连接后，长时间不交换数据，一直占用服务器端的文件描述符，导致连接资源的浪费。\n定时事件，是指固定一段时间之后触发某段代码，由该段代码处理一个事件，如从内核事件表删除事件，并关闭文件描述符，释放连接资源。\n定时器，是指利用结构体或其他形式，将多种定时事件进行封装起来。具体的，这里只涉及一种定时事件，即定期检测非活跃连接，这里将该定时事件与连接资源封装为一个结构体定时器。\n定时器容器，是指使用某种容器类数据结构，将上述多个定时器组合起来，便于对定时事件统一管理。具体的，项目中使用堆将所有定时器串联组织起来。\n传统的定时方案是以固定频率调用起搏函数tick，进而执行定时器上的回调函数。而时间堆的做法则是将所有定时器中超时时间最小的一个定时器的超时值作为心搏间隔，当超时时间到达时，处理超时事件，然后再次从剩余定时器中找出超时时间最小的一个，依次反复即可。\n最小堆：用于快速找到最早超时的定时器（堆顶元素即为最早的定时器）。 回调机制：每个定时器有一个超时回调函数，一旦超时，定时器会被触发并执行回调。 堆优化：通过上浮和下沉操作保证每次插入、删除和调整操作都可以在对数时间内完成。 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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 #ifndef HEAP_TIMER_H #define HEAP_TIMER_H #include... typedef std::function\u0026lt;void()\u0026gt; TimeoutCallBack; typedef std::chrono::high_resolution_clock Clock; typedef std::chrono::milliseconds MS; typedef Clock::time_point TimeStamp; struct TimerNode { int id; TimeStamp expires; // 超时时间点 TimeoutCallBack cb; // 超时回调函数 bool operator\u0026lt;(const TimerNode\u0026amp; t) { return expires \u0026lt; t.expires; } }; /*基于对最小堆的定时器类*/ class HeapTimer { public: HeapTimer() { heap_.reserve(64); } ~HeapTimer() { clear(); } void adjust(int id, int newExpires) void add(int id, int timeOut, const TimeoutCallBack\u0026amp; cb); // 添加定时器结点，ID不存在则直接插入然后调整；id存在则更新超时时间 void doWork(int id);// 触发回调函数，删除该定时器 void clear(); // 清空 void tick(); // 检查堆顶元素是否超时，超时则回调删除 void pop(); // 删除最早超时的定时器 int GetNextTick(); // 获取下一个超时定时器的剩余时间 private: void del_(size_t i); // 删除定时器 void siftup_(size_t i); // 向上调整 bool siftdown_(size_t index, size_t n); // 向下调整 void SwapNode_(size_t i, size_t j); // 交换两个结点位置 std::vector\u0026lt;TimerNode\u0026gt; heap_; std::unordered_map\u0026lt;int, size_t\u0026gt; ref_; // 定时器ID到堆中结点的索引，便于快速查找定时器 }; #endif //HEAP_TIMER_H Epoller epoll是linux特有的IO复用函数。\nepoll使用一组函数来完成任务，而不是单个函数\nepoll把用户关心的文件描述符上的事件放在内核的一个事件表中，而无需像select和poll那样每次调用都要重复传入文件描述符集或事件集。但epoll需要使用额外的文件描述符唯一标识内核中的事件表。\nint epoll_creat(int size);\nint epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);// 操作epoll的内核事件表，成功时返回0，失效则返回-1并设置errno\nop为指定操作类型，比如EPOLL_CTL_ADD,EPOLL_CTL_MOD,EPOLL_CTL_DEL等\nint epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout);\n成功时返回就绪的文件描述符个数，失败则返回-1并设置errno。\n它会将所有的就绪事件从内核事件表中赋值到它的第二个参数events指向的数组中，这个数组只用于输出epoll_wait检测到的就绪事件，可以提高应用程序索引就绪文件描述符的效率。\n对于epoll的LT模式和ET模式\nLT模式：当epoll_wait检测到有事件发生并且通知应用程序之后，应用程序可以不立即处理该事件，等待下次应用程序调用epoll_wait时，还会再次通告，直到该事件被处理。 ET模式：当epoll_wait检测到有事件发生并且通知应用程序之后，应用程序必须立即处理该事件，因为后续的epoll_wait调用将不再向应用程序通知这个事件。 为了防止ET模式下，两个线程同时操作一个socket的局面,EPOLLONESHOT登场。\nEPOLLONESHOT能保证操作系统最多触发其上注册的一个可读，可写或者异常事件，并且只触发一次。\nPOLLIN ：表示对应的文件描述符可以读（包括对端 SOCKET 正常关闭）； EPOLLOUT：表示对应的文件描述符可以写； EPOLLPRI：表示对应的文件描述符有紧急的数据可读（这里应该表示有带外数据到来）； EPOLLERR：表示对应的文件描述符发生错误；\nEPOLLRDHUP：远端关闭了读方向（对端不再发送数据，但可能仍能接收数据）。\nEPOLLHUP：连接完全关闭，文件描述符不可用。 EPOLLET：将EPOLL设为边缘触发(Edge Triggered)模式，这是相对于水平触发(Level Triggered)来说的。 EPOLLONESHOT：只监听一次事件，当监听完这次事件之后，如果还需要继续监听这个socket的话，需要再次把这个socket加入到EPOLL队列里。\n客户端直接调用close，会触犯EPOLLRDHUP事件 通过EPOLLRDHUP属性，来判断是否对端已经关闭，这样可以减少一次系统调用。 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 26 27 28 29 30 #ifndef EPOLLER_H #define EPOLLER_H #include... /*基于epoll的事件处理类*/ class Epoller { public: explicit Epoller(int maxEvent = 1024); ~Epoller(); /*添加事件，修改事件，删除事件epoll_ctl*/ bool AddFd(int fd, uint32_t events); bool ModFd(int fd, uint32_t events); bool DelFd(int fd); /*等待事件发生，epoll_wait*/ int Wait(int timeoutMs = -1); /*第i个事件的类型or文件描述符*/ int GetEventFd(size_t i) const; uint32_t GetEvents(size_t i) const; private: int epollFd_; std::vector\u0026lt;struct epoll_event\u0026gt; events_; }; #endif //EPOLLER_H webserver 前置知识 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 /* 设置套接字 * SO_LINGER： 确保当服务器关闭时，未发送的数据能够尽量发送完毕， 提供一个“优雅关闭”的机制。 * SO_REUSEADDR： 允许多个套接字绑定到同一个端口，但同一时间只能有一个套接字接收数据 解决TIME_WAIT状态导致的端口绑定问题。 允许端口复用，避免端口被占用， 确保服务器能够快速重启并继续监听同一个端口。 * SO_REUSEPORT： 允许多个套接字同时绑定到同一个端口， 内核会将收到的数据包分发给不同的套接字，实现真正的负载均衡。 */ /* 确立事件模式 * EPOLLRDHUP:检测到对方关闭连接时触发，监控对端连接关闭的情况 * EPOLLONSHOT:事件触发后自动取消，防止多线程竞争 * EPOLLET:变韵出发，只有状态发生变化才会通知 */ 下面这图基本就是webserver的一些主要函数和大致用法了。\n再来张图，介绍一下所有函数的逻辑\n果然比起打字更喜欢直接写出来）\nLast 介绍一下每部分的逻辑，以及它们之间的联系。\n用到了哪些c++新特性\n","date":"2024-09-08T00:00:00Z","permalink":"https://sutdown.github.io/p/webserver/","title":"Webserver"},{"content":"（考试周记闻）\n啊啊啊啊啊学习好难过，学习数逻好痛苦😭，为什么啊，有的科目没怎么痛苦啊，难道我不喜欢硬件？\n喜欢不喜欢都是要好好学的，少年，你还有期末呢\nmealy状态机总是不会做，感觉有个虚浮的点我一直抓不住，思考一下，好难思考，我杠下这个知识点了，现在0点34分\n悲凄，十分钟尝试了一个想法，方向错了\n（已经不知道乱试的第几个思路了），不乱试也没找到下手的方向，害\n回归本源，再去翻翻那章的ppt，（悲，其实还有头歌往年题没看，差好多òᆺó）\n二十分钟，有点眉目但不多\n绝望，感觉有七成了，但是还是不行\n大致理解，还是有点错误\n1点34分，一个小时，还是感觉只有七成的样子，\n所以真的要去纠结一个问题，在时间不是很充裕的情况下，可放下太多也很容易失去的太多。\n两点四十，麻了，没早睡，期待早起\n五天假期倒计时\n当生理期和七天六科的期末周巧妙重合\n笑不出来，真的，麻了\n两次出师不利了，第三次\n其实都是对你能力的一个评估，你心里都有数的，卷子的难度和你学习的成果之间有个冥冥的对应，莫慌\n2024-6-22 20：03\n啊，好困，不知道听力困得还是本来就有点困，中午睡了二十分钟然后去考毛概，emm，就还是，一般般吧，大概15点交的卷子，回宿舍吃个饭洗个澡，然后五点半来的七星灯火，现在八点了，斯，两个半小时就听个听力睡个觉。。还打了下游戏。别以为我考完了，可恶，好摆\n明天有个英语\n后天人工智能基础（一点没学）\n空一天然后计网（学的不多但是知识点太多太散没底）\n别摆了姐姐，考完再摆吧\n今晚和明天上午把英语复习完，然后把人工智能基础ppt看完\n明天下午和后天上午计网和人工智能基础，计网ppt还差最后一章给补上就行\n最后专心复习计网\n暂定这样，开始学习，最后不到四天的时间了，莫摆莫摆\n啊对，留张图，纪念一下毛概的结束\n（诶嘿，背书的地方）\n图片\n如果做这个决定不好受\n那请停下五分钟来思考\n五分钟冷静后\n一切会明了很多\n2024.06.22 23：35\n人工智能基础这个课程吧，怎么说\n我很不喜欢这个老师的课堂方式，不怎么发课件，通过邮件联系，上完半学期的课程最后发了三个pdf，三个pdf涵盖不了考试60%的考点，真只按照这些复习，挺容易的估计\n他刚开始可能觉得他想单独考吧，所以很多统考的知识点略过的很简单，或者课堂直接说不考，也不明白最后怎么又想统考了\n其实统考难度应该比他自己出的简单，但是难学了，也算勉强一丢丢幸事\n不过由于我本身确实也没怎么听，他讲的也确实就那个样子，造成了期末周补天的惨剧吧\n也只能说，发的pdf上都看过，其它另一半考点都不会\n图片\n害\n今晚目标学完来着\n看看学到几点\n预言家属性发动：3点到4点\n第二天上午看完的，诶嘿\n还是不要熬太晚X﹏X\n悲戚，\n输出情绪实在是一件愚蠢的事情\n你还是多冷静，少干这种事情\n英语考完了\n继续留张照片\n图片 坦诚说考的不怎么样\n主要在前面三十个单选吧，好难，应该有部分是a篇的原文，但我复习的时候没想到会这么考\n人生滑铁卢，好悲伤\n信念，随缘\n得不到的始终想要\n得到了又开始追忆从前\n人啊\n2024.6.24 11：28\n这两天效率不太行，精神也不怎么好\n再加上昨晚发现了一件不大不小蛮难过的事实，今天上午又发生了一桩事\n我很讨厌这种短时期内发生大量事情的感觉，实在让我力不从心\n快了\n图片\n很糟糕，非常糟糕\n只能说，复习到的我就会，没有复习到的也是一点不会，听天由命\n希望这学期期末周的考试都没有挂科吧 图片\n吐槽不下去了 最后一门 好好考吧\n期末周总会胡思乱想一些想法 感觉创立wby的人简直是个天才 不仅能跟学校教务等部门，还有课程表GPA访问码等功能实时对接 也能实现学生线上交流学习讨论等多种话题 太绝了简直 感觉应该需要很强大的技术支撑 估计也很有学校的支持\n困，效率好低 诶嘿，多糖拿铁 我还是没那么想熬夜的 明天考完好好睡觉 图片\n心灰意冷X﹏X 图片\n结束了 不过不怎么样 好几科其实都挺有挂科的风险 很难说吧 不过挂了也是因果 一个是七天的连续学习到最后因为各种因素精神其实已经不怎么样了 真的挺难坚持住的 图片\n另一个是 许是我飘了\n前三学期的成绩基本不用担心 太过于顺风顺水 所以在今年不可避免的松懈了很多 我应该知道的\n无论怎样都要去接受 去看你这条路究竟是会通向哪里\n你认为福贵的命运苦吗\n我觉得那是他该得的，并且结局并不算非常糟。\n","date":"2024-06-19T00:00:00Z","permalink":"https://sutdown.github.io/p/%E5%8F%A8%E5%8F%A8%E5%94%A0%E5%94%A0%E5%8F%A8%E5%8F%A8%E5%94%A0%E5%94%A0/","title":"叨叨，唠唠，叨叨，唠唠"},{"content":"已经2024年了啊，前段时间还一直想写个年度总结来着，结果看着完全不会的大物课本都没什么写的心情和灵感了。\n不过大物也并没有想象中的那么可怕，大致的过一下应付期末基本倒也还好。前段时间总是熬夜，基本上很少十二点前，一两三点是常态，七八点的闹钟也叫的起来我，毕竟事情太多了，也没什么赖床的心情。到现在基本上大体的事情都结束了，专业课程也都考完了，大物基础知识也都过了一下，剩下就做做题目，看看题型，然后背几天马原考完试，大二的第一个学期就正式结束了。\n这样子看还有点恍惚，这学期几乎没什么成果，晃悠一下就过去了。不过其实也还好，归根结底也就是顺其自然的过了，就好像我这学期本就该这么过一样。现在就彷佛提上的心慢慢缓了下来。其实每经过一段着急的时间之后，心情就愈发的趋于平静。毕竟无论前段时间如何，现在的我回去也做不到更好了。\n我当前能做的无非就是，以当下的心情和心境过好现在，仅此而已。\n我会尽量的将生活过的好一点，但其实尽管在该紧张的时候我可能还是会习惯于熬夜什么之类有点消耗的事情，不过那些彷佛都只是必要的经历的事情，接受那些必然发生的，往前看。\n2024年，我希望在往前看的同时，过好当下，做好自己应做的，同时好好的生活。\n需要紧张的时候保持紧张和专注力，事情放松的时候好好的看看世界。\n各方面有兴趣的多多尝试，广泛温和的看待各项事情。\n面向未来，热爱当下。\n","date":"2024-01-05T00:00:00Z","permalink":"https://sutdown.github.io/p/%E4%B8%80%E4%B8%AA%E6%97%A0%E8%B6%A3%E7%9A%84%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/","title":"一个无趣的年终总结"},{"content":"[TOC]\n1.用户态和内核态，用户线程和内核线程；（笔记） 用户态智能执行非特权指令。如果用户态的程序试图执行特权指令，会引发异常或错误。用户态的程序需要通过访管指令或者系统调用请求操作系统执行特权指令。\n特权指令：只能在内核态执行的指令，它们通常涉及到系统资源的管理和保护，比如输入输出，中断控制，时钟设置等。\n**非特权指令：**在用户态执行的指令，比如算法运算，数据运输，跳转，trap（访管），除法错误异常（非特权指令，在用户态执行，触发中断后会使CPU从用户态切换成内核态）。\n明晰：非特权指令不是全程在用户态下发生，也可以在内核态下发生。特权指令只能在内核态发生。\n用户级线程与系统级线程\n线程的调度与切换时间 用户级线程的切换通常发生在一个应用进程的多个线程之间，无须通过中断进入OS的内核，且切换规则也简单，因此其切换速度特别快。而核心级线程的切换时间相对比较慢。\n系统调用 用户级线程调用系统调用时，内核不知道用户级线程的存在，只当作是整个进程行为，使进程等待并调度另一个进程执行，在内核完成系统调用而返回时，进程才能继续执行。而核心级线程则以线程为单位进行调度，当线程调度系统调用时，内核将其作为线程的行为，因此阻塞该线程，可以调度该进程中的其他线程执行。\n线程执行时间 如果用户设置了用户级线程，系统调用是以进程为单位进行的，但随着进程中线程数目的增加，每个线程得到的执行时间就少。而如果设置的是核心级线程，则调度以线程为单位，因此可以获得良好的执行时间\n2.单道批处理系统，多道批处理系统，分时系统，实时系统；（笔记） 3.fork()和exec()，另外还有waitpid()，execve()；（操作系统导论） 4.硬链接和符号链接，文件系统。(深入了解，不止于表面的复制和快捷方式) 5.进程间通信的三种方式的表现是什么？（通信的大概方式） 6.进程的三种调度指什么？ 长期调度：选择一个进程进入内存的就绪队列\n短期调度：从就绪队列中选择一个进程为其分配cpu。\n中期调度：将进程从cpu或者内存中移除，换到外存交换区，降低多道程序设计的难度。\n7.进程竞争内存时内存资源紧张的方案有哪些？ 1).swapping。换出一部分内存到外存。\n2).virtual memory。每个进程只装入一部分程序和数据。\n8.讲述对于父进程和子进程的理解。 fork()：fork()复制当前进程的所有状态和资源，生成一个与父进程完全相同的子进程。\n注：在父进程中，返回值是创建的子进程的PID，在子进程的返回值是0；返回值为负则创建失败。\n**wait()：**父进程等待子进程终止再继续运行。成功时返回子进程的pid，没有子进程返回0；\n**exec()：**子进程调用 exec() 执行新的程序，从而拥有自己独立的新代码段。\n注：exec() 成功，不会有返回值，因为原来的进程已经被新程序替换，不执行后续代码。exec() 失败，返回值是 -1，原进程继续执行后续代码。\n9.时钟页面置换算法，LRU近似的老化aging算法，工作集页面置换算法（工作集指最近k次访问近似最近多久时间内访问的页面），WSclock（工作集时钟，R和生存时间） 最好的页面置换算法是老化算法和工作集时钟算法，它们分别基于LRU和工作集，有良好的页面调度性能，可以有效实现。\n10.系统调度 处理机调度\n处理机调度是指操作系统根据一定的算法，从就绪队列中选择一个进程，并将处理机分配给它运行，以实现进程的并发执行。处理机调度的目的是提高处理机的利用率，提高系统的吞吐量和响应时间，以及保证系统的公平性和平衡性。\n处理机调度可以分为三个层次：高级调度、中级调度和低级调度，分别对应作业调度、内存调度和进程调度。\n系统调度算法\n是操作系统用来选择下一个要执行的作业或者进程的方法。可分为两类：作业调度和进程调度。\n作业调度是指从后备队列中选择一个或多个合适的作业，将它们调入内存，为它们创建进程。是外存和内存之间的调度，发生频率很低，使进程从创建态到就绪态的过程。\n作业调度算法：1.先来先服务算法.2.短作业优先算法.3.高响应比优先算法.\n进程调度是指从就绪队列中选择一个或多个合适的进程，将它们分配给处理器。是内存到cpu的调度，发生频率很高，使进程从就绪态到运行态的过程。\n进程调度算法：1.时间片轮转算法。2.优先级调度算法。3.多级反馈队列算法。\n11.经典同步问题（PV操作的基本含义,经典问题） 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 P:(wait) void wait(semaphore S){ S.value--; if(S.value\u0026lt;0){ add this process to S.L; block(); } } V:(signal) void signal(semaphore S){ S.value++; if(S.value\u0026lt;=0){ remove a process P from S.L; wakeup(P); } } 读者写者问题，生产者消费者问题，哲学家就餐问题等。\n12.临界互斥的基本方法的理解。 （空闲让进，忙则等待，有限等待，让权等待）\nI：软件上\n1）单标志法。进程一在访问完后会转交给进程二，但是如果进程二不想占用临界资源会导致临界区空闲。违反”空闲让进“原则。\n2）双标志法先检查。在进程一想要进入临界区时会先检查是否有其它的进程想进入临界区，如果没有进程一会访问临界区。由于在检查后的切换时会划分一定的时间，这段时间如果进程二也检查结束，回导致同时进入临界区，违反“忙则等待”原则。\n3）双标志法后检查。如果后检查的话，两个进程同时想进入临界区时，都会先打算进入再检查对方状态，然后两个进程相互谦让，最后导致“饥饿”现象，违反“空闲让进”和忙则等待“现象。\n(原子操作)\n4）皮特森算法。结合双标志法和但标志，二者同时想进入时发生孔融让梨。\nII：硬件上\n1）中断屏蔽法。仅对当前CPU有效，不适合多处理器。\n2）硬件指令法。不满足”让权等待“，可能发生”饥饿“。\n13.死锁（死锁预防，死锁避免，死锁检测，死锁解除，实际应对死锁的方法） 死锁的产生必要条件（互斥，占有并等待，不可抢占，循环等待）\n死锁预防（针对发生的必要条件）\n死锁避免（银行家算法，去除死锁的可能性）\n死锁检测和解除（检测：利用资源有向图；解除：进程终止，抢占资源（回退，饥饿））\n实际中操作系统应对死锁的可行方法。\n​ 鸵鸟算法。因为处理死锁成本太高，而死锁出现的频率较低，故可以忽略死锁的发生。\n​ (鸵鸟算法是一种不采取任何措施的方法，它假设死锁很少发生，或者死锁的代价可以接受)\n​ Spooling 技术。假脱机技术。为临界资源增加一个等待队列，使 其好像可以被共享使用，如打印机。将独占设备看成共享设备，\n​ (Spooling 技术是一种预防死锁的方法，它通过使用缓冲区来存储临界资源的请求，将独占设备看成共享设备，从而避免了进程之间的互斥和占有并等待的条件。)\n当死锁发生时 ，杀死运行时间较短的进程，损失较小， 因为容易恢复。 死锁的产生、防止、避免、检测和解除 - 知乎 (zhihu.com) 14.非连续分配的方式（基本分页存储管理，基本分段存储管理，段页式管理，TLB） 15.分段，分页，段页式优缺点 分页：提高内存利用率，用硬件机制实现，可以实现内存的动态分配，共享和保护（受到限制），对用户不可见。注意分页的大小时固定的，因此不利于数据的动态增长。\n缺点：逻辑地址空间划分只简单依靠页面大小，缺乏内在逻辑性，导致一方 面相关内容被分散 到多页上，页面置换不当时容易造成内存抖动，另一方面 不同性质的内容被分到同一页中，使得页面 权限保护设置困难。\n**分段：**便于编程，程序和数据的共享和保护，公共代码段可以通过映射共享到多个进程，动态链接实现方便，数据动态增长等。在内存中无法不连续存储，易形成内存外碎片，降低内存利用率。\n段页式：\n段是信息的逻辑单位，根据用户的需要划分，因此段对用户是可见的；页是信息的物理单位，是为了管理主存的方便而划分的，对用户是透明的。\n页的大小固定不变，由系统决定。段的大小是不固定的，由其完成的功能决定。\n由于段是信息的逻辑单位，因此便于存贮保护和信息的共享，页的保护和共享受到限制。\n以段为单位调入调出，以页为单位非连续存储\n16.概念 地址映射和地址变换 地址映射：为保证程序正常运行，必须将虚拟空间中已链接和划分好的内容装入内存，并将虚拟地址映射为内存地址。\n地址变换：要建立虚拟地址与内存地址的关系。\n重定位：把进程地址空间中使用的逻辑地址变成内存中物理地址的过程。\n静态地址重定位和动态地址重定位 静态地址重定位：在虚拟空间执行之前由装配程序完成地址映射工作。\n动态地址重定位：在程序执行过程中，在CPU访问内存之前，将要访问的程序或数据地址转换成内存地址。\n静态链接和动态链接 静态链接：为了程序正确执行，必须由连接装配程序把它们连接成一个可运行的目标程序。\n问题：花费时间，浪费空间\n**动态链接：**在程序开始运行时，只将主程序段装配好并调入内存，其它各段的装配是在主程序段的运行过程中逐步完成。每当需要调用一个新段时，再将这个新段装配好，并与主程序段链接\n加载：通过某种加法运算来将逻辑地址转换为物理地址的过程。\n临界资源：一次仅允许一个进程使用的共享资源，例如打印机、文件、内存等。临界资源的访问需要保证互斥和同步，以避免数据的破坏和冲突。\n临界区：每个进程中访问临界资源的那段程序，例如读写文件、分配内存等。\n同步：多个进程之间需要协调一致地执行，以保证数据的正确性和一致性。\n互斥：多个进程在访问某一共享资源时，需要保证在同一时刻只能有一个进程对其进行操作，以避免数据的破坏和冲突。\n调度：操作系统根据一定的算法，从就绪队列中选择一个进程，并将处理机分配给它运行，以实现进程的并发执行。\n抖动：由于内存不足，导致操作系统频繁地进行页面置换，使得进程在运行和等待之间不断切换，从而降低了系统的性能。\n抖动的原因是进程的工作集大小超过了可用的物理内存，使得缺页中断率过高。抖动的解决方法是增加物理内存、减少进程数、调整页面置换算法等。\n死锁：指的是一组进程中的每个进程都在等待一个事件，而这个事件只能由该组中的另一个进程触发，从而导致所有进程都无法继续执行的状态。\n17.进程特征 结构特征：程序段，数据段，进程控制块PCB 动态性：最基本的特征，进程是动态产生，动态消亡的；进程在其生命周期内，在三种基本状态之间转换（就绪、等待和执行）。 并发性：任何进程都可以通其它进程一起向前推进。 独立性：进程是一个能独立运行的基本单位，同时也是系统中独立获得资源和独立调度的基本单位。 异步性：每个进程都以其相对独立的，不可预知的速度向前推进。 操作系统 | 进程调度/切换时机、内核临界区与普通临界区_进程访问内核程序的临界区-CSDN博客\n18.倒排页表 一般页表是通过虚拟地址映射寻找物理地址，虚拟地址的空间比物理地址要大，因此消耗的空间资源更多。\n倒排页表则是通过物理地址映射虚拟地址，节约了空间资源，但是通过虚拟地址查找物理地址就变得困难，极其浪费时间。\n有两种解决方案，一种是TLB存储最近常用的页表项，另一种是利用散列表存储虚拟地址。\n19.虚拟内存的实现方式（按需调页，按需调段） Demand Paging：只在页被需要时，才调入内存。\n无效时——\u0026gt;发生中断（转24.页面故障处理）；不在内存中——\u0026gt;调入内存。\n20.页缓冲算法 页缓冲算法是一种操作系统中用于提高页面置换效率的方法。\n它的基本思想是将被淘汰的页面暂时保存在内存中，而不是立即写回磁盘，以便在需要时快速调入。\n页缓冲算法的目的是：\n提高页面置换的速度：当一个页面被淘汰时，如果它已经被修改过，那么就将它放入已修改页列表中，而不是直接写回磁盘。这样可以节省磁盘I/O的时间，提高页面置换的速度。 减少磁盘的碎片：当一个页面被修改过后，它的内容可能和磁盘上的原始内容不一致，如果频繁地写回磁盘，可能会导致磁盘的碎片增加，影响磁盘的性能。通过将修改过的页面放入已修改页列表中，可以延迟写回磁盘的时间，减少磁盘的碎片。 提高页面的命中率：当一个页面被淘汰后，如果它很快又被访问，那么就可以在已修改页列表中查找它，如果找到了，就可以直接将它调入内存，而不需要再从磁盘读取。这样可以提高页面的命中率，减少缺页中断的次数。 21.帧分配算法 帧分配是指如何将物理内存中的页帧（固定大小的内存块）分配给需要内存的进程。\n帧分配的目的是实现虚拟内存的映射，即让进程可以使用比物理内存更大的地址空间。\n帧分配涉及到以下几个方面：\n帧分配算法：决定给每个进程分配多少帧，以及如何选择被淘汰的帧。常见的帧分配算法有平均分配、按进程大小比例分配、按优先级分配等。\n帧分配策略：决定是否允许进程抢占其他进程的帧。常见的帧分配策略有全局分配（不能控制进程的缺页率）和局部分配（不能使用其它内存不常用的空间）。\n帧分配类型：决定是否需要为函数创建堆栈帧。常见的帧分配类型有帧函数和叶函数。\n帧分配对齐：决定是否需要使帧对齐到特定的边界。常见的帧分配对齐有基本对齐和扩展对齐。\n22.工作集模型原理 23.虚拟内存—交换技术（覆盖和交换） 操作系统原理：覆盖技术、交换技术、虚拟内存概要-CSDN博客\n24.页面故障处理 当访问无效页时，会陷入OS\n1.检查进程内部页表\nInvalid reference —\u0026gt; abort 非法访问-〉终止\nJust not in memory 不在内存中\n2.找到一个空闲帧\n3.换入页到该帧中\n4.修改表\n5.Set validation bit = v\n6.重新开始因陷阱而中断的指令\n25.页面置换算法（见9） 26.内核进行内存管理的方法（Buddy系统，Slab分配） 内核进行内存管理的两个方法是Buddy系统和Slab分配，它们都是基于虚拟内存的管理技术，但有不同的目的和方法。\nBuddy系统（产生碎片） 是一种以页为单位管理和分配内存的方法，它的基本思想是将所有的空闲页框分组为不同的大小，每个大小都是2的幂次方，然后根据需要分配或合并相邻的页框。\nBuddy系统的目的是实现虚拟内存的映射，即让进程可以使用比物理内存更大的地址空间。\nSlab分配器（不产生碎片） 是一种以字节为单位管理和分配内存的方法，它的基本思想是将从Buddy系统申请的大内存进一步细分成小内存分配。\nSlab分配器的目的是提高小对象的分配效率，减少内存碎片，维护常用对象的缓存，提高CPU硬件缓存的利用率。\n27. 文件目录和目录文件 文件目录 ：文件控制块(FCB)的有序集合，用于文件描述和文件控制，实现按名存取和文件信息共享与保护。\n**目录文件：**为了实现对文件目录的管理，通常将文件目录以文件的形式保存在外存，这个文件就叫目录文件\n28.基本文件操作（read,write,等） 29.inode介绍 30.文件存储空间管理（空闲表法，空闲链表法，空闲盘区链，位示图法） 位示图法：n个块的磁盘需要n位位图，n/8字节的空间。\n空闲文件目录是一张连续表，它要占用较大的辅存空间。\n空闲块链，每次释放物理块时要完成拉链工作，虽然只是在一块中写一个字节，但其工作量与写一块相差无几。\n位示图，分配时需要顺序扫描空闲区（标志为“0”），速度慢，而且物理块号并未在图中直接反映出来，需要进一步计算。\n31.文件保护（口令保护，加密保护，访问控制） 32.磁盘调度算法（FCFS，SSTF，SCAN，LOOK调度算法，C-SCAN循环扫描算法，C-LOOK算法） SCAN：磁盘的一端到另一端，到达后转向。(顶端，不循环)\nC-SCAN：当磁头移到另一端时，会马上返回到磁盘开始，返回时不处理请求\nLOOK：是改进的SCAN算法，处理过程与SCAN相似，只是每次都不运动到顶端，在最大磁道号和最小磁道号之间移动。\nC-LOOK：n磁头只移动到一个方向上最远的请求为止，接着马上返回，而不是移动到磁盘的尽头\n33.RAID廉价磁盘冗余阵列 定义：多个独立的物理硬盘按照不同的方式组合起来，形成一个虚拟的硬盘\n存取方式：\n并行存取方式：适用于大型的、以长时间顺序访问数据为特征的应用\n独立存取方式：适用于数据存取频繁，每笔存取数据量较小的应用\n**镜像冗余的概念：**磁盘镜像是一个简单的设备虚拟化技术，每个I/O操作都会在两个磁盘上执行，两个磁盘看起来就像一个磁盘一样。镜像冗余可以提高磁盘的读性能。\n校验冗余的概念：根据冗余算法计算阵列中成员磁盘上数据的校验信息，将校验信息保存在其他的磁盘资源上。\n**热换：**是指在不影响系统正常运转的情况下，用正常的磁盘物理替换RAID阵列中的失效磁盘。\nRAID0：均匀分布在各个磁盘。\nRAID1：镜像冗余，有2N个磁盘，100%冗余，空间利用率50%\nRAID2：校验冗余，并行存取。（了解）\nRAID3：校验冗余，分块（分成小块，同区域内块数多），读写性能好，并行存取，磁盘损坏时对整体吞吐量小。\nRAID4：XOR校验数据，分块（分成大块），独立存取。\nRAID5：独立存取，军训分散在阵列的磁盘，N+1个磁盘。（选学）\nRAID6：奇偶校验，N+2个磁盘。（选学）\nRAID10：先镜像再条带化=1+0（选学）\n34.IO软件层次结构 IO软件层次结构:\n用户级IO软件（与用户的调用接口）\n设备独立性软件（执行所有设备公共的IO功能）\n设备驱动（IO设备需要特定的代码控制）\n中断处理\n硬件。\n设备独立性软件：功能\n设备驱动的统一接口；缓冲；错误处理；独占设备的分配和释放；提供与设备无关的逻辑块。\n35.IO控制方式（程序直接控制方式，中断控制方式，DMA控制方式，通道控制方式） **程序直接控制方式：**用户进程直接控制内存或CPU和外围设备之间的信息传送。\n但是CPU和外设速度差异大，不能实现并行，不能对外部异常做出响应\n中断控制方式：（在以上前两者做出改变，减少CPU等待时间和提高并行工作程度）仅适合于中慢速设备。在高速外围设备中，中断可能由于来不及响应丢失数据；在希望成组数据交换时，多次中断速度也降低）。\n对于大批量成组数据交换，可以利用DMA和通道方式。\n**DMA控制方式：**在外围设备和内存中开辟直接的数据交换通路。\n优势：1.相比中断每次缓冲区满让CPU处理，DMA会在所有数据块结束一次险处理，减少CPU处理次数。\n2.中断中的数据传送每次有CPU控制处理，DMA自行处理，排除并行使CPU来不及处理或速度差距时发生的数据丢失。\n但是DMA对设备的管理和控制更为复杂，消耗更多，DMA过多容易引起内存地址冲突，并且不经济。\n**通道控制方式：**通道相当于一个功能单纯的处理机，只处理IO操作。有自己的运控部件和指令系统，没有专门的内存，通过“周期窃取”和主机共享内存。\n以内存为中心，实现设备和内存直接交换数据的控制方式。\n通道比DMA的自由度更高。DMA中每台设备至少需要一个DMA控制器，通道则是一个通道控制多台设备与内存进行数据交换，使得通道方式进一步减轻了CPU的工作负担和增加了计算机系统的并行工作程度。\n36.缓冲技术 1）无缓冲。（缓冲区在用户空间从而导致页面池收缩，页面性能下降）\n2）单缓冲。（内核中的缓冲区满时无法处理新字符）\n3）双缓冲。（两个缓冲区交替使用）\n4）其它：循环缓冲，缓冲池，\n注：数据被缓冲太多次时，页面性能会下降。\n比如：网络工作可能由于缓冲，进行了过多的复制操作，降低了传输速率。\n37.pv操作（联合11） 进程同步与互斥的三种基本问题： 1.利用信号量实现同步：\n1 2 3 4 5 6 7 8 9 10 11 12 13 Semaphore S=0; P1(){ x; V(S); } P2(){ ... P(S); y; ... } //注：只有先执行P1语句中的x才能执行到P2中的y //P2若是先执行到P(S),S为0，执行P操作会把进程P2堵塞，并放入堵塞队列；P1中的x执行完后，执行V操作，把P2从堵塞队列放回就绪队列，P2得到处理机，继续执行。 2.利用信号量实现进程互斥：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 Semaphore S=0; P1(){ ... P(S); P1进程的临界区; V(S); ... } P2(){ ... P(S); P2进程的临界区; V(S); ... } 3.利用信号量实现前驱关系：\n思想有点像拓扑排序。在一个有向图中，某某是某某的前驱，为其提供资源，这种资源在该思想中用信号量表示。\n经典同步问题 生产者消费者问题：\n同步关系12：生产者放了消费者才能取（可取资源）；消费者取了生产者才能放（可放资源）。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 生产者： full=0;empty=1; while(true){ 生产产品 P(empty); 将产品送到缓冲区 V(full); } 消费者： while(true){ P(full); 从缓冲区取出产品 V(empty); 消费产品 } 读者/写者问题\n进程关系分析：读操作可以同时进程，读写互斥，写写互斥。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 mutex=1; Reader： while (true) { P(mutex); read(); V(mutex); }; Writer： while (true) { P(mutex); write(); V(mutex); }; 哲学家进餐问题\n附：期末 日常作业考点：\n进程管理： 选择：信号量和条件变量；进程的三种状态转换；进程和线程的差异；临界区和临界资源；用户态和内核态；系统调度；死锁。\n大题：系统调度算法，银行家算法，pv操作\n内存管理： 选择：死锁，抖动，重定位；页面置换算法；空闲区分配；段页式管理；多级页表；缺页；按需调页\n大题：分页管理系统，段页式管理，倒排页表\n文件管理： 选择：RAID；磁盘；文件和目录；软连接和硬链接。\n大题：磁盘调度算法，磁盘访问。\n","date":"2023-12-28T00:00:00Z","permalink":"https://sutdown.github.io/p/os2%E7%9F%A5%E8%AF%86%E7%82%B9%E8%AF%A6%E8%A7%A3/","title":"OS2：知识点详解"},{"content":"目前对本文思路是在大纲的基础上，做题过程中对不熟悉的知识点进行扩展，12.28考试前每日更新。\n—2023.12.23 19:05\n多看书，研究概念，研究逻辑。\n[TOC]\nOS基础 1.计算机启动原理。 OS_1.3.pptx;\n2.操作系统历史。（单道批，多道批，分时，实时操作系统） OS_1.5.pptx;\n3.Linux基础。 OS_3.1ppt;—OS_4.2.pptx;\n（略过，暂时没看）\n进程管理 4.进程 进程和程序 并发和并行 进程的概念和特征（结构特征，动态性，并发性，独立性，异步性） 进程的状态转换（创建态，中止态，就绪态，运行态，阻塞态） 进程间通信（共享存储，消息传递，管道通信） 进程调度（长期调度，短期调度，中期调度） 进程竞争内存资源紧张的方案（swapping，virtual memory） Q:\n二进制代码和常量存放于正文段，动态分配的存储区位于数据堆段，临时使用的变量位于数据栈段，进程优先级在PCB内。 进程的实体是代码、数据和PCB。PCB中包含的数据结构内容主要有四大类：进程标志信息、进程控制信息、进程资源信息、CPU现场信息。因此全局变量和PCB无关。 5.线程 线程的引入（减少进程切换和创建开销） 线程与进程的关系 用户级线程和内核级线程 多线程模型（实现和好处，多对一，一对一，多对多，二级模型，线程池） fork()和exec()，另外还有waitpid()，execve()； Q：\n,用户级线程和内核线程的在中断时的区别。\nA：用户级线程是有应用程序通过线程库实现，所有的线程管理工作都由应用程序负责，包括线程间的切换都无需操作系统的干预，但是只要一个线程堵塞，整个进程都会阻塞，并发度低。\n内核级线程是由操作系统内核完成，因此它的切换需要在核心态完成，但是并发度高，一个线程堵塞其它的依然可以继续执行，但线程切换时候的成本高开销大。\n另外，在系统调用时，用户级线程不能被系统所看到，因此以进程为单位，随着线程的增多，单个线程的执行时间会变少；但是内核级线程以线程为单位，可以得到良好的执行时间.\n线程是CPU运行的一个基本单元：program counter 程序计数器，register set 寄存器集，stack space 栈空间\n一个线程与它的对等线程共享：code section 代码段，data section 数据段，operating-system resources 操作系统资源\n6.系统调度 cpu的调度可能发生在以下情况，运行到阻塞，阻塞到就绪（IO完成），运行到就绪（出现中断时）。 将CPU控制交由短期调度程序选择的进程，包括上下文切换，切换到用户模式，跳转到用户程序的合适为止以重新执行程序。 调度准则，评价指标 调度算法（FCFS（护航效果），SJF，最高响应比，优先级，轮转（合适时间片），多级反馈轮转（不同优先队列不同时间片））（SJF分为抢占和非抢占两种，没有特殊说明，默认非抢占） Q:\nCPU-I/O区间周期：进程执行由CPU执行和I/O等待周期组成。\nCPU调度算法是操作系统用来选择下一个要执行的进程的的方法。\n分时操作系统中进程调度算法中对普通进程常常采用的是优先级轮转法，请问如何保 证不会有进程因为优先级太低而饥饿？\nA: 采用动态调整进程优先级的方法。动态降低长时间占用 CPU 进程的优先级，低优先级的 进程的优先级则相对升高，最终得到运行。\n7.竞争与同步 同步与互斥 进入临界区的四个准则（空闲让进，忙则等待，有限等待，让权等待） 临界互斥基本方法 信号量机制（整形信号量，记录型信号量） 经典同步问题（理解PV操作） Q：\n临界互斥的基本方法的理解\nA：\nI：软件上\n1）单标志法。进程一在访问完后会转交给进程二，但是如果进程二不想占用临界资源会导致临界区空闲。违反”空闲让进“原则。\n2）双标志法先检查。在进程一想要进入临界区时会先检查是否有其它的进程想进入临界区，如果没有进程一会访问临界区。由于在检查后的切换时会划分一定的时间，这段时间如果进程二也检查结束，回导致同时进入临界区，违反“忙则等待”原则。\n3）双标志法后检查。如果后检查的话，两个进程同时想进入临界区时，都会先打算进入再检查对方状态，然后两个进程相互谦让，最后导致“饥饿”现象，违反“空闲让进”和忙则等待“现象。\n(原子操作)\n4）皮特森算法。结合双标志法和但标志，二者同时想进入时发生孔融让梨。\nII：硬件上\n1）中断屏蔽法。仅对当前CPU有效，不适合多处理器。\n2）硬件指令法。不满足”让权等待“，可能发生”饥饿“。\n什么时候需要用mutex也就是互斥锁，分清同步和互斥？\nA：mutex是一种操作系统提供的一种同步机制，用于保护多个线程共享的资源，防止数据的混乱和冲突。当多个线程需要访问同一个资源时，例如一个全局变量、一个文件、一个设备等，如果不加任何控制，可能会导致数据的不一致或者错误。为了解决这个问题，可以使用mutex来实现互斥访问，即一次只允许一个线程对资源进行操作，其他线程必须等待，直到该线程释放了资源。这样可以保证资源的完整性和正确性。\n理解PV操作的含义\nA：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 P:(wait) void wait(semaphore S){ S.value--; if(S.value\u0026lt;0){ add this process to S.L; block(); } } V:(signal) void signal(semaphore S){ S.value++; if(S.value\u0026lt;=0){ remove a process P from S.L; wakeup(P); } } 8.死锁 死锁的产生必要条件（互斥，占有并等待，不可抢占，循环等待） 死锁预防（针对发生的必要条件） 死锁避免（银行家算法，去除死锁的可能性） 死锁检测和解除（检测：利用资源有向图；解除：进程终止，抢占资源（回退，饥饿）） Q：\n实际中操作系统应对死锁的可行方法。\n鸵鸟算法。因为处理死锁成本太高，而死锁出现的频率较低，故可以忽略死锁的发生。\n(鸵鸟算法是一种不采取任何措施的方法，它假设死锁很少发生，或者死锁的代价可以接受)\nSpooling 技术。假脱机技术。为临界资源增加一个等待队列，使 其好像可以被共享使用，如打印机。\n(Spooling 技术是一种预防死锁的方法，它通过使用缓冲区来存储临界资源的请求，从而避免了进程之间的互斥和占有并等待的条件。)\n当死锁发生时，杀死运行时间较短的进程，损失较小， 因为容易恢复。\n死锁的产生、防止、避免、检测和解除 - 知乎 (zhihu.com)\n内存管理 9.内存管理 基础背景 内存的构成 连接方式（静态连接，装入时动态连接，运行时动态连接） 装入方式（直接装入，静态重定位，动态重定位） 内存连续分配的方式（单一连续分配，固定分区分配，动态分区分配） 动态分区分配的算法（首次适应，最佳适应，最坏适应，邻近适应） 非连续分配的方式（基本分页存储管理，基本分段存储管理，段页式管理）（重点） Q：\n背景的简单介绍。\nA：\n程序必须放入内存的进程空间中才能被执行\nCPU能直接访问的存储器只有内存和处理器内的寄存器\n直接存取要求内存速度能够和CPU取址速度相匹配，大到能装下当前运行的程序和数据，否则CPU执行速度就会收到内存速度和容量的影响而得不到充分发挥。\n最坏适应算法优先使用大分区的话，剩余的空间也多，从此减少碎片，但是不利于大进程。\n理解分段分页的优缺点：\n分页：提高内存利用率，用硬件机制实现，可以实现内存的动态分配，共享和保护（受到限制），对用户不可见。注意分页的大小时固定的，因此不利于数据的动态增长。\n缺点：逻辑地址空间划分只简单依靠页面大小，缺乏内在逻辑性，导致一方 面相关内容被分散 到多页上，页面置换不当时容易造成内存抖动，另一方面 不同性质的内容被分到同一页中，使得页面 权限保护设置困难。\n**分段：**便于编程，程序和数据的共享和保护，公共代码段可以通过映射共享到多个进程，动态链接实现方便，数据动态增长等。在内存中无法不连续存储，易形成内存外碎片，降低内存利用率。\n段页式：\n段是信息的逻辑单位，根据用户的需要划分，因此段对用户是可见的；页是信息的物理单位，是为了管理主存的方便而划分的，对用户是透明的。\n页的大小固定不变，由系统决定。段的大小是不固定的，由其完成的功能决定。\n由于段是信息的逻辑单位，因此便于存贮保护和信息的共享，页的保护和共享受到限制。\n以段为单位调入调出，以页为单位非连续存储\n多级页表优点的是减少页表所占的内存空间。\ncache\n10.虚拟内存 背景：虚拟空间和实地址空间 交换技术（覆盖技术和交换技术） 实现方式（请求式分页管理，请求式分段存储管理，请求式段页存储管理） 页面故障处理（重点） 页面置换算法（OPT，FIFO，LRU（Aging），CLOCK，改进的时钟置换算法） 页缓冲算法 帧分配（难点） 内核进行内存管理的方法（Buddy系统，Slab分配）（难点） 工作集 倒排页表 Q:\nAging老化算法与LRU类似。\n老化算法与 LRU 相比，主要有两点区别：\n（1）老化算法记录使用情况的 寄存器只有有限位， 比如 8 位，无法记录所有使用情况。\n（2）同一时间间隔 内只使用 0/1 区分页面使用情况，无法详 细区别间隔内的具体时间\n文件管理 11.File System Interface 文件分类：流式文件，记录式文件（顺序文件，索引文件，索引顺序文件） 文件目录结构（单级目录，两级目录，树形目录，无环图目录路径） 文件物理结构（连续分配，链接分配，显示链接，索引分配，多层索引） inode 硬链接和符号链接 12.File System Implementation 文件存储空间管理（空闲表法，空闲链表法，空闲盘区链，位示图法） 文件保护（口令保护，加密保护，访问控制） 13.Mass-storage system 磁盘结构 磁盘调度算法（FCFS，SSTF，SCAN，LOOK调度算法，C-SCAN循环扫描算法，C-LOOK算法） LVM RAID（了解） 设备管理 14.IO System-Devices IO设备分类（传输速率，信息交换单位，设备共享特性） IO控制方式（程序直接控制方式，中断控制方式，DMA控制方式，通道控制方式） 缓冲技术 附录： 帧分配和页缓冲算法是两种不同的操作系统技术，它们都涉及到虚拟内存的管理，但是有不同的目的和方法。\n1.帧分配 帧分配是指如何将物理内存中的页帧（固定大小的内存块）分配给需要内存的进程。\n帧分配的目的是实现虚拟内存的映射，即让进程可以使用比物理内存更大的地址空间。\n帧分配涉及到以下几个方面：\n帧分配算法：决定给每个进程分配多少帧，以及如何选择被淘汰的帧。常见的帧分配算法有平均分配、按进程大小比例分配、按优先级分配等。 帧分配策略：决定是否允许进程抢占其他进程的帧。常见的帧分配策略有全局分配和局部分配。 帧分配类型：决定是否需要为函数创建堆栈帧。常见的帧分配类型有帧函数和叶函数。 帧分配对齐：决定是否需要使帧对齐到特定的边界。常见的帧分配对齐有基本对齐和扩展对齐。 2.页缓冲算法 页缓冲算法是一种操作系统中用于提高页面置换效率的方法。\n它的基本思想是将被淘汰的页面暂时保存在内存中，而不是立即写回磁盘，以便在需要时快速调入。\n页缓冲算法的目的是：\n提高页面置换的速度：当一个页面被淘汰时，如果它已经被修改过，那么就将它放入已修改页列表中，而不是直接写回磁盘。这样可以节省磁盘I/O的时间，提高页面置换的速度。 减少磁盘的碎片：当一个页面被修改过后，它的内容可能和磁盘上的原始内容不一致，如果频繁地写回磁盘，可能会导致磁盘的碎片增加，影响磁盘的性能。通过将修改过的页面放入已修改页列表中，可以延迟写回磁盘的时间，减少磁盘的碎片。 提高页面的命中率：当一个页面被淘汰后，如果它很快又被访问，那么就可以在已修改页列表中查找它，如果找到了，就可以直接将它调入内存，而不需要再从磁盘读取。这样可以提高页面的命中率，减少缺页中断的次数。 3.内核的内存管理方法 内核进行内存管理的两个方法是Buddy系统和Slab分配，它们都是基于虚拟内存的管理技术，但有不同的目的和方法。\nBuddy系统 是一种以页为单位管理和分配内存的方法，它的基本思想是将所有的空闲页框分组为不同的大小，每个大小都是2的幂次方，然后根据需要分配或合并相邻的页框。\nBuddy系统的目的是实现虚拟内存的映射，即让进程可以使用比物理内存更大的地址空间。\nBuddy系统涉及到以下几个方面：\n帧分配算法：决定给每个进程分配多少帧，以及如何选择被淘汰的帧。常见的帧分配算法有平均分配、按进程大小比例分配、按优先级分配等。 帧分配策略：决定是否允许进程抢占其他进程的帧。常见的帧分配策略有全局分配和局部分配。 帧分配类型：决定是否需要为函数创建堆栈帧。常见的帧分配类型有帧函数和叶函数。 帧分配对齐：决定是否需要使帧对齐到特定的边界。常见的帧分配对齐有基本对齐和扩展对齐。 Slab分配器 是一种以字节为单位管理和分配内存的方法，它的基本思想是将从Buddy系统申请的大内存进一步细分成小内存分配。\nSlab分配器的目的是提高小对象的分配效率，减少内存碎片，维护常用对象的缓存，提高CPU硬件缓存的利用率。\nSlab分配器涉及到以下几个方面：\nkmem_cache：是一个描述一种对象类型的高速缓存的结构，每种对象类型的高速缓存由一连串的slab构成，每个slab由一个或多个连续的物理页面组成。 slab：是Slab分配器的最小单位，它可以分为三种状态：slabs_full（完全分配的slab），slabs_partial（部分分配的slab），slabs_empty（空slab或没有对象被分配）。 object：是Slab分配器分配的最小对象，它是一个结构体实例，如i节点、PCB等，它可以被打包到slab中，并使用构造函数和析构函数进行初始化和清理。 slab着色：是一种尝试使不同slab中的对象使用CPU硬件缓存中不同行的方案，它通过将对象放置在slab中的不同起始偏移处，来减少对象之间的缓存冲突。 4.用户级线程与系统级线程 线程的调度与切换时间 用户级线程的切换通常发生在一个应用进程的多个线程之间，无须通过中断进入OS的内核，且切换规则也简单，因此其切换速度特别快。而核心级线程的切换时间相对比较慢。\n系统调用 用户级线程调用系统调用时，内核不知道用户级线程的存在，只当作是整个进程行为，使进程等待并调度另一个进程执行，在内核完成系统调用而返回时，进程才能继续执行。而核心级线程则以线程为单位进行调度，当线程调度系统调用时，内核将其作为线程的行为，因此阻塞该线程，可以调度该进程中的其他线程执行。\n线程执行时间 如果用户设置了用户级线程，系统调用是以进程为单位进行的，但随着进程中线程数目的增加，每个线程得到的执行时间就少。而如果设置的是核心级线程，则调度以线程为单位，因此可以获得良好的执行时间\n关于指令： 用户态智能执行非特权指令。如果用户态的程序试图执行特权指令，会引发异常或错误。用户态的程序需要通过访管指令或者系统调用请求操作系统执行特权指令。\n特权指令：只能在内核态执行的指令，它们通常涉及到系统资源的管理和保护，比如输入输出，中断控制，时钟设置等。\n**非特权指令：**在用户态执行的指令，比如算法运算，数据运输，跳转，trap（访管），除法错误异常（非特权指令，在用户态执行，触发中断后会使CPU从用户态切换成内核态）。\n明晰：非特权指令不是全程在用户态下发生，也可以在内核态下发生。特权指令只能在内核态发生。\n5.系统调度算法 处理机调度\n处理机调度是指操作系统根据一定的算法，从就绪队列中选择一个进程，并将处理机分配给它运行，以实现进程的并发执行。处理机调度的目的是提高处理机的利用率，提高系统的吞吐量和响应时间，以及保证系统的公平性和平衡性。\n处理机调度可以分为三个层次：高级调度、中级调度和低级调度，分别对应作业调度、内存调度和进程调度。\n系统调度算法\n是操作系统用来选择下一个要执行的作业或者进程的方法。可分为两类：作业调度和进程调度。\n作业调度是指从后备队列中选择一个或多个合适的作业，将它们调入内存，为它们创建进程。是外存和内存之间的调度，发生频率很低，使进程从创建态到就绪态的过程。\n作业调度算法：1.先来先服务算法.2.短作业优先算法.3.高响应比优先算法.\n进程调度是指从就绪队列中选择一个或多个合适的进程，将它们分配给处理器。是内存到cpu的调度，发生频率很高，使进程从就绪态到运行态的过程。\n进程调度算法：1.时间片轮转算法。2.优先级调度算法。3.多级反馈队列算法。\n6.进程的特征 结构特征：程序段，数据段，进程控制块PCB 动态性：最基本的特征，进程是动态产生，动态消亡的；进程在其生命周期内，在三种基本状态之间转换（就绪、等待和执行）。 并发性：任何进程都可以通其它进程一起向前推进。 独立性：进程是一个能独立运行的基本单位，同时也是系统中独立获得资源和独立调度的基本单位。 异步性：每个进程都以其相对独立的，不可预知的速度向前推进。 7.文章链接 操作系统 | 进程调度/切换时机、内核临界区与普通临界区_进程访问内核程序的临界区-CSDN博客\n8.基础概念 调度：操作系统根据一定的算法，从就绪队列中选择一个进程，并将处理机分配给它运行，以实现进程的并发执行。\n同步：多个进程之间需要协调一致地执行，以保证数据的正确性和一致性。\n互斥：多个进程在访问某一共享资源时，需要保证在同一时刻只能有一个进程对其进行操作，以避免数据的破坏和冲突。\n抖动：由于内存不足，导致操作系统频繁地进行页面置换，使得进程在运行和等待之间不断切换，从而降低了系统的性能。\n抖动的原因是进程的工作集大小超过了可用的物理内存，使得缺页中断率过高。抖动的解决方法是增加物理内存、减少进程数、调整页面置换算法等。\n死锁：指的是一组进程中的每个进程都在等待一个事件，而这个事件只能由该组中的另一个进程触发，从而导致所有进程都无法继续执行的状态。\n临界资源：一次仅允许一个进程使用的共享资源，例如打印机、文件、内存等。临界资源的访问需要保证互斥和同步，以避免数据的破坏和冲突。\n临界区：每个进程中访问临界资源的那段程序，例如读写文件、分配内存等。\n重定位：把进程地址空间中使用的逻辑地址变成内存中物理地址的过程。\n加载：通过某种加法运算来将逻辑地址转换为物理地址的过程。\n9.UNIX创建并执行新程序 fork()系统调用：fork()系统调用是 UNIX 系统中创建新进程的一种方法，它会复制当前进程的所有状态和资源，生成一个与父进程完全相同的子进程。子进程的唯一区别是它的进程号不同，以及 fork() 的返回值不同。\nexec()系统调用：exec()系统调用是 UNIX 系统中执行新程序的一种方法，它会用新程序的代码和数据替换当前进程的代码和数据，从而改变当前进程的执行内容。exec() 系统调用有多种变体，如 execl(), execv(), execle(), execve() 等，它们的区别主要在于参数的传递方式和环境变量的设置方式。\nfork()和exec()的组合：在 UNIX 系统中，通常使用 fork() 和 exec() 的组合来创建并执行新的程序。首先，父进程调用 fork() 产生一个与自己一模一样的子进程，然后，子进程调用 exec() 执行新的程序，从而拥有自己独立的新代码段。父进程可以继续执行原来的程序，或者等待子进程的结束。\nfork()和exec()的返回值：\nfork() 系统调用的返回值有三种情况：\n如果 fork() 成功，那么在父进程中，返回值是子进程的进程号；在子进程中，返回值是 0。 如果 fork() 失败，那么在父进程中，返回值是 -1，同时设置 errno 变量表示错误原因；在子进程中，不会有返回值，因为子进程没有被创建。 如果 fork() 被信号中断，那么在父进程中，返回值是 -1，同时设置 errno 变量为 EINTR；在子进程中，不会有返回值，因为子进程没有被创建。 exec() 系统调用的返回值只有一种情况： 如果 exec() 成功，那么在执行 exec() 的进程中，不会有返回值，因为原来的进程已经被新程序替换，不会再执行后续的代码。 如果 exec() 失败，那么在执行 exec() 的进程中，返回值是 -1，同时设置 errno 变量表示错误原因，原来的进程继续执行后续的代码。 10.多级页表 逻辑地址：page\n物理地址：page frame\n注意：多级页表的出现主要并不是为了查找地址方便，而是减少页表所占用的过大空间。\n11.倒排页表 倒排页表是一种存储虚拟页号和物理页框的映射关系的数据结构，它与常规页表相反，每个表项对应一个物理页框，而不是一个虚拟页号。\n倒排页表的优点是减少了页表占用的内存空间，因为物理内存的大小一般远小于虚拟内存的大小。\n倒排页表的缺点是增加了地址转换的时间和复杂度，因为需要使用散列函数和链表来查找匹配的表项。倒排页表还增加了进程间共享内存的难度，因为需要在表项中增加进程标识和链接指针。\ntangthinker.work 虚拟内存\n12.分区管理的交换技术和段式管理的请求式分段技术 13.请求式分段和覆盖技术 14.页面集置换算法中工作集置换算法的工作原理 主要参考来源： 1.天津大学 邱铁 操作系统原理ppt\n2.操作系统复习文档\n","date":"2023-12-23T00:00:00Z","permalink":"https://sutdown.github.io/p/os1%E7%9F%A5%E8%AF%86%E7%82%B9%E6%A2%B3%E7%90%86/","title":"OS1：知识点梳理"},{"content":"家庭教师HITMAN REBORN!\n这是一部贯彻我这一整个学期的番剧了，总共有203集，每集去掉片头片尾回顾也都有一二十分钟，倘若按照15分钟，两百集来看，也有3000分钟，50个小时了。\n刚开始看它只是因为那种搞笑轻松的风格，甚至于有时候我也会对“废柴阿纲”怎么会是这部剧的主角而心生疑惑，毕竟在我看过的小说动漫的潜意识中，主角应该是要么智慧要么热忱的形象，比如喜羊羊比如我看过的那些网文，主角几乎都是全能没有缺点的，我并不是讨厌阿纲，因为我也知道，人是存在着很难改善的缺点的，我们并不能对一个普通人太过于苛刻。\n所以前期我一直是当着一个日常搞笑番看着，有时间看个一两集，不会有那种非追不可的欲望，当然后来是有的，比方一次性看个几十集之类的不在话下。\n就某些细节的剧情而言，它是经不起推敲的，但是整体的框架构思很有意思，最精彩的是未来选择篇最后的设定。\n世界由两个家族的截止和七个彩虹之子的奶嘴构成世界的基石，也同样对应着三位大空属性的角色。\n纲吉智慧，极善；尤尼牺牲自己保全所有人；白兰玩世不恭，极恶。\n其实我本身对白兰是没什么好感的，毕竟看故事是从主角团的角度出发，白兰的高颜值也只能让我对他不讨厌而已，可是当故事到达最后那一段自述后，我对他的偏向不再是无感了。\n在没有戒指和平行时空的记忆之前，他不过是一个感觉有利于世界，但又尽力感受着生活美好的普通人而已。但当无数平行时空的记忆涌入，在辅以戒指无上的力量，从此白兰便将生活当成了手握攻略的游戏。其实就哪现实世界的普通游戏为例，游戏自然代表着虚假无趣，特别你还当你知晓整个游戏线时，邪恶的滋生在所难免。\n白兰的经历契机促使了他的现在，那么一切便也无需多言，可怜又可恨。\n看每个角色不同的可能性同样我也是在说着每个人不同的可能性。\n接下来我就不谈家教了，返回到我自己。\n我很不相信那种所谓的顿悟，但我相信你一定有种某种潜意识偏向指引着你的道路，说是一年天堂一念地狱也不为过。\n现阶段的我处于一个随缘过好当下的样子，罢了好困，不想写了，下回再补了。\n","date":"2023-12-08T00:00:00Z","permalink":"https://sutdown.github.io/p/%E7%95%AA%E5%89%A7%E5%AE%B6%E5%BA%AD%E6%95%99%E5%B8%88hitman-reborn/","title":"番剧—家庭教师HITMAN REBORN!"},{"content":"考纲（参考王道2021）： 1.文件系统基础\n文件概念；文件逻辑结构；顺序文件；索引文件；索引顺序文件； 目录结构：文件控制块和索引节点，单级目录结构和两级目录结构，树形目录结构，图形目录结构 文件共享；文件保护；访问类型；访问控制； 2.文件系统实现\n文件系统层次结构；目录实现；文件实现； 3.磁盘组织和管理\n磁盘的结构；磁盘调度算法；磁盘管理； 基础知识点: 文件系统服务器管理两个部分：\n在应用层上：安全保护；文件访问控制；文件结构定义（针对数据文件）\n在物理层上：磁盘设备防护，磁盘数据存取，磁盘空间分配（针对磁盘空间）\n文件系统的逻辑结构：有结构文件（纪录式文件）和无结构文件（流式文件）。\n字符流式的无结构文件实质上是记录长度为1个字符的连续结构文件；\n记录式文件则是将文件中的记录按照不同方式排列，构成不同的逻辑结构。\n对于优缺点的讨论一般从词或块的查找，文件管理，适用范围三方面讨论。\n文件的定义：文件是以计算机硬盘为载体的存储在计算机上的信息集合。\n文件 = 文件体（文件的信息，逻辑结构） + 文件说明（文件控制块FCB，目录结构）\n文件目录：文件控制块的有序集合，用于文件描述和文件控制，实现按名存取和文件信息共享保护。\n目录文件：为了实现对文件目录的管理，通常文件目录以文件的形式保存在外存，这个文件叫做目录文件。\n文件基本操作：\ncreate：找到空间，创建条目\nwrite：系统调用查找文件位置，再使用写指针指向；read：系统调用查找位置，再使用读指针指向。\n由于文件通常只能读或者写，因此公用同一指针。\nreposition within file，delete，truncate，open，close（其余具体见王道考研）(该文章后面会具体讲解)\n打开文件表：包含所有的打开信息。\n文件打开计数器：记录多少进程打开了文件。\n文件系统的层次结构 （对于本节知识点的一个串联，具体细节见后）\n从内存向外存\n用户接口：文件系统需要向上层的用户提供一些简单易用的功能接口。这层用于处理一些简单易用的功能接口。（创建，删除，读，写，重定义，截断，打开，关闭）。\n文件目录系统：用户通过文件路径访问文件，因此需要根据用户给出的路径找到相应的FCB或者索引结点。所有和目录，目录项相关的管理工作在本层完成，如管理活跃的文件目录表，管理打开文件表等。\n存取控制模块：为了保证文件数据的安全，还需要验证用户是否有访问权限。这一层主要完成了文件保护相关功能。\n逻辑文件系统和文件信息缓冲区：用户指明想要访问文件记录号，这一层需要将记录好转译为对应的逻辑地址。\n物理文件系统：这一层将上一层提供的逻辑地址转化为实际的物理地址。\n辅助分配模块：负责文件存储空间的管理，即负责分配和回收存储空间。\n设备管理模块：直接与硬件交互，负责和硬件直接相关的一些管理工作。如：分配设备，分配设备缓冲区，磁盘调度，启动设备，释放设备等。\n附：参考资料： 1.2021王道考研操作系统\n2.TJU OS邱铁老师课堂PPT\n3.《Operating Systems：Three Easy Pieces》\n","date":"2023-12-02T00:00:00Z","permalink":"https://sutdown.github.io/p/os%E6%96%87%E4%BB%B6%E7%B3%BB%E7%BB%9F/","title":"OS：文件系统"},{"content":" emm，上回的方法，这回用的github上的框架了。 一.已知博客搭建方案： 1）hexo+github+Coding（静态）\n2）宝塔面板+typecho/wordpress（动态博客程序）\n注：有关wordpress和typecho差别：个人博客平台选择 Typecho 还是 WordPress ？ - 知乎 (zhihu.com)\n二.搭建过程 （这里采用宝塔面板+typecho，链接均可跳转）\n（如果有计算机网络和linux系统的基础知识，搭建过程会愉快很多，搭建的过程感觉在不断的看着计网的知识和linux的各种命令）\n1.域名和云服务器 选择： 前言：域名选择不要选择中文。域名和服务器可以在不同厂商购买，不过相同厂商更为便捷。\n1）购买网站：\n*常用：（国外）namesilo（域名）+vultr（服务器）\n文章参考：Namesilo域名申请与Vultr服务器申请 - 知乎 (zhihu.com)\n*在国内，阿里云，腾讯云，华为云等都可以同时购买域名和服务器（均有学生优惠）。\n2）购买建议：\n*域名中需要实名认真大概1-2个小时；云服务器需要备案大概3-30天不等，但如果选择港澳或者国外的服务器就不需要备案。\n*云服务器中会让你选择系统镜像（比如centos，Ubuntu等等），这些一定要根据个人需求，一些CPU核，速率等在个人博客前期不会有太大浏览量的情况下，不需要选择太好的。\n*云服务器中会有安全组的选择。安装组会对端口是有要求的，端口不匹配后面很可能登不上去。端口介绍：深入理解什么是端口(port) - 知乎 (zhihu.com)\n*云服务器购买时会填一个SSH账号名和密码，linux默认时root，这个后续有用。\n解析： 在域名和云服务器购买完成之后，\n1）域名需要解析才能和云服务器相连，也就是”解析“。在购买的域名查看界面会有解析，点进去按照引导操作即可。\n我是在阿里云中购买域名，然后解析，解析页面添加云服务器的公网IP地址，个人博客的业务需求选择**A（**将网站域名解析到IPv4地址）。这样添加完成即可。\n注：云服务器中会有公网IP和内网IP，只有公网IP能进行域名解析，参考文章：云服务器主机内网ip和外网ip的区别_自己弄的云服务器ip都不同吧-CSDN博客。\n2.环境配置 前言：出现任何的问题要多尝试才能找到解法，总能解决的，解决的过程中你会学会很多，我不能把我所有的问题都列出来，因为各人遇到的问题其实不同，可能我碰见了你很幸运的没有碰见，所以不赘述过多，只回忆大致思路。\n这个环境配置过程中如果出现问题，宝塔的官网论坛，宝塔的微信客服（虽然我是普通用户但是回答很快，也解决了我当时的问题），或则和浏览器都可以搜索。\n安装linux宝塔面板 直接搜索进入官网下载安装即可，云服务器centos+宝塔linux是一个不错的选择；宝塔也有windows的，但稳定性不如linux。\n文章推荐：宝塔面板Linux和Windows系统区别及选择哪个更好？-魏艾斯笔记 (vpsss.net)\n安装参考：（官网中有很具体的操作方案）\n1）自动安装，输入前面云服务购买时的密码即可。\n2）手动安装。（我自动安装时发生问题，于是手动下载宝塔终端进行操作，这里分享一下个人经验）\n*进入终端时不显示用户名，也就是整个终端全黑，什么都没有无法执行命令。我是下载xshell，然后在其中添加文件，里面填写了我的ip和密码等信息然后就可以显示了。参考文章：30分钟搭建 Typecho 个人博客教程 - 知乎 (zhihu.com)，该文章中有这一步骤。\n*如上文参考文章所述，然后会下载linux宝塔，我那时候大概下载了一两个小时，也可能我的网速不太好，接着会显示内网面板地址和外网面板地址。重点：只能通过外网面板地址进入。\n*然后出现了一个很头疼的问题，外网面板地址进入时显示无法访问网页，我那时候采取了很多的方法也有好几个小时，最后才勉强误打误撞解决。有可能的原因：\n–你域名的端口不在你电脑系统防火墙的安全范围内，这个时候可以选择关闭防火墙或者进入它的高级设置。\n–在宝塔的终端中输入命令bt可以查看面板的很多信息，也可以让它自动修复。\n–文件句柄，域名绑定限制，重启，端点匹配等等。\n如果遇到问题各位多尝试了，一定能尝试出来的，当然如果你也是这样搭建来找我我也能给些建议和帮助的。\nlinux宝塔安装后进入面板： 后面会提示安装LNMP（我是大概一两个小时安装好），直接默认设置安装即可。万字长文—LNMP架构的搭建及应用部署详解（超详细） - 知乎 (zhihu.com)；\n然后在面板左侧网站选项中添加网站填写信息（要新建数据库账号和密码），再在网站文件夹上上传从typecho中下载的压缩包然后在其中双击解压即可（可参考该文，文中说有build文件，其实没有解压后就不用管了30分钟搭建 Typecho 个人博客教程 - 知乎 (zhihu.com)）\n最后在浏览器中搜索自己的域名，会自动进入typecho的界面让你登录，登录后进入网站后台（也就是自己可以对网站外观或者编写文章 的位置）然后网站搭建成功了。\n3.个性化设置网站外观 前面进行完后，这个就很简单了，在github，typecho主题官网等中都可以搜索到免费或者付费的主题，自主选择即可。\n","date":"2023-10-01T00:00:00Z","permalink":"https://sutdown.github.io/p/%E5%AE%9D%E5%A1%94%E9%9D%A2%E6%9D%BF-typecho/","title":"宝塔面板+typecho"},{"content":"异常控制流（Exceptional Control Flow） 引 首先要认识，异常控制流并不是我们现实中所理解的出现“异常问题”，这里应该指代在进行一个完整的程序时发生的影响程序过程的情况。在书籍中，“现代系统通过使控制流发生突变来对这些情况作出反应。一般而言，我们把这些突变称为异常控制流（ECF）。”\n异常控制流与前面我们所了解的I/O，进程，虚拟内存的基本机制有着一定关联，还有助于理解应用程序和操作系统之间的交互，理解并发，编写应用程序，软件一场工作等。ECF与软硬件都有着一定关联。\n1.1异常 1.1.1基本概念的认知： 在处理器的运行过程中，从给其加电到断电，程序计数器假设一个值的序列a0,a1,a2……,其中ak是某个相应的指令Ik的地址，其中不同地址的过渡称为控制转移。这样的控制转移序列称为处理器的控制流。\n现代系统通过使控制流发生突变来对这些情况作出反应。一般而言，我们把这些突变称为异常控制流（ECF）。\n在运行时，如若发生异常，会由相应的异常号和异常表基址寄存器中异常表的起始地址共同找到相应的异常表进行异常处理。\n但要注意，异常和过程调用的相似之处在于都是中止当前过程进入另一个过程，但是两者在具体上存在差异，不可混淆。异常在结束时会返回断点，这个断点可以指代事件发生时正在执行的指令或者即将要执行的指令。而过程调用则是在调用是将当前地址和部分寄存器压入栈中，过程执行完后再出栈。\n异常是异常控制流的一种形式，异常分为中断（interrupt），陷阱（trap），故障（fault），终止（abort）四类。\n1.1.2异常（Exception）和中断（Interrupt）： 1）发生异常中断后，系统将进入OS内核态对相应事件进行处理，即改变处理器状态。内核态的权限更高，但同时比进程更“轻”。\n（用户态和内核态的区别，可参考链接用户态和内核态的区别 - Gizing - 博客园 (cnblogs.com)）\n2）外设通过中断请求信号线向cpu提出”中断“请求，不由指令引起，故中断也称为异步异常。\n3）Intel将中断分成可屏蔽中断（maskable interrupt）和不可屏蔽中断（nonmaskable interrupt，NMI）\n可屏蔽中断：通过INTR向cpu请求，可通过设置屏蔽字屏蔽请求，中断请求被屏蔽，则不会送到cpu。\n不可屏蔽中断：非常紧急的硬件故障。如电源断电，硬件线路故障等，通过NMI向cpu请求。产生后会立即送到cpu以便快速处理，此时，中断服务程序会保存系统重要信息，然后在屏幕上显示相应信息或者重启系统。\n1.1.3故障（Fault） 1）故障是由错误情况引起，它可能呗故障处理程序修正。\n2）异常举例—页故障\n缺页（可通过读磁盘恢复故障）：页表项有效位为0，也就是在虚拟内存寻址中，可见阅读 《深入理解计算机系统》（CSAPP）（一） - 知乎 (zhihu.com)其中的第三点虚拟内存中有对缺页进行说明。\nSegmentation fault（段故障，在leetcode中写的代码如果发生越界经常会出现这个报错）\n地址越界（不可恢复）：地址大于最大界限。\n访问越级或越权：越级：用户进程访问内核数据；越权：读写权限不相符。\n1.1.4陷阱（Trap） 1）陷阱是有意的异常。也可以被称为编译异常（programmed exception），这些指令包括INT 你，int 3，into（溢出检查），bound（地址越界检查）等。\n2）同时包括我们对源代码进行编译时的设置断点和单步跟踪，也就是调试功能，都是属于陷阱异常。\n单步跟踪原理：IA-32中，如果cpu处于单步跟踪状态（TF=1且IF=1，TF为陷阱标志Trap Flag，IF为中断允许标志Interrupt Flag）时，每条指令都被设置成了陷阱指令，执行每条指令后，都会发生中断类型号为1的调试异常，从而转向执行“单步跟踪处理程序。\n设置断点：指令为int 3，对应机器码为CCH。该断点设置会直接在程序的指令执行中加上该指令，运行时发出”EXCEPTION_BREAKPOINT“的异常，然后调处中断程序执行。\n1.1.5终止（Abort） 硬故障事件，此时机器将终止，调处终端服务程序来重启操作系统。\n1.2进程 1.2.1逻辑控制流，并发流，私有地址空间 **逻辑控制流：**一个独立的逻辑控制流，它提供一个假象，好像我们的程序独占地使用处理器。\n举例：当一个运行着三个进程的系统，处理器的一个物理控制流会分成三个逻辑控制流，每个进程一个。在这一过程中，进程1运行一会然后转到B的逻辑控制流运行一段时间再转到C~~，关键在于进程是轮流使用处理器的，但在每个逻辑控制流看来，像是当时的进程独占处理器。\n**并发流（concurrent flow）和并行流（parallel flow）：**一个逻辑流的执行时间和另一个流重叠，称为并发流。（可以宽泛的理解为逻辑控制流是在某个时间内只有一个进程，而并发流可以做到在某个时间内由多个逻辑流）不是要求两个逻辑流完全重合，存在时间重叠即可称为并发流。同时如果他们运行在不同的处理器或者计算机上，则成为并行流，它属于并发流的真子集。\n多任务（multitasking）（时间分片）：一个进程和其它进程轮流运行的概念。\n时间片（time slice）：一个进程执行它的逻辑控制流的一部分的每一个时间段。\n如我们用此图对逻辑控制流和并发流进行一个概述。\n自左向右，1）进程p1进行A11-A13；2）进程p1本欲进行A11-A14，却在A12时被进程p2打断，此时跳转到进程p2,p2先进行A21-A22，再在进行A23到A25的过程中，在A24处又跳转回进程p1中被打断的A12处，此时一直到A14该进程的第二的逻辑控制流运行完；3）然后开始第三个控制流A15-A6，执行完后跳转到进程p3，p3执行完A31-A32后，返回进程p2被打断的A24处，将该p2的控制流执行到A25执行完毕。\n在进行一个完整逻辑控制流的过程中，与其它进程的控制流发生交叉的现象叫做并发。因此p1和p2，p2和p3是并发执行。\n**私有地址空间：**一个私有的地址空间，它提供一个假象，好像我们的程序独占的使用内存系统。\n1.2.2用户模式和内核模式 用户模式：用户程序必须通过系统调用接口间接的访问内核代码和数据。\n内核模式：一个运行在内核模式的进程可以执行指令集中的任何指令，并且可以访问系统中的任何内存设置。内核模式可以执行特权指令。特权指令指的是比如停止处理去，改变模式为，或者发起I/O操作，冲刷cache等对系统运行影响很大的指令。\n初始时运行的是用户模式，但是当诸如中断，故障或者陷入系统调用这样的异常时，控制传到异常处理程序，处理器将模式从用户模式转为内核模式，而当异常结束返回应用程序代码是会回到用户模式。\n在Linux中，有一种机制叫做/proc文件系统，它允许用户模式进程访问内核数据结构的内容，它会将内核数据结构的内容输出为用户可读的文本文件层次结构。\n1.2.3进程的上下文切换（context switch） 对改图和标题分析：\n上下文，上下文切换是什么？\n上下文由进程维持，就是内核重新启动一个被抢占的进程所需的状态，包括寄存器，进程表，文件表等。\n当一个进程在运行时，内核可以抢占当前进程，喧一个先前被抢占的进程运行，这样的行为叫做调度。当内核发生调度时所用到的机制叫做上下文切换。\n上下文切换的过程：1）保存当前进程的上下文；2）恢复某个先前被抢占的进程被保存的上下文；3）将控制传递给这个新恢复的进程。\n为什么涉及到磁盘？\n该图并不是上下文切换的一般形式，只是因为进程A需要从磁盘中调出数据，此过程需要耗费十几秒的时间，于是内核选择让该程序休眠，利用上下文切换去进行进程B。此时需要进行上下文切换的过程，同时部分操作权限高，需要从用户模式到内核模式进行处理，切换完成后会恢复用户模式，而需要返回进程A的时候同理。\n什么时候会发生上下文切换？\n1）当内核代表用户执行系统调用时，可能发生上下文切换。\n2）中断也可能引发上下文切换。\n1.2.4进程的控制与回收 进程的控制和回收是操作系统管理和维护进程的重要任务，它们涉及到进程的创建、调度、终止和资源回收等方面的操作。\n1）进程的创建：\n进程的创建通常由父进程发起，父进程可以通过系统调用（如fork()或spawn()）创建新的子进程。新创建的子进程通常是父进程的副本，包括代码、数据、打开的文件描述符等。\n2）进程的控制：\n进程控制涉及到对进程的运行状态进行管理，包括启动、暂停、恢复和终止等操作。父进程可以通过系统调用（如exec()、kill()等）来控制子进程的行为。进程控制还包括进程的优先级调度、资源分配等管理操作。\n3）进程的终止：\n进程可以因为多种原因而终止，包括正常退出、异常终止、被其他进程终止等。进程可以通过系统调用（如exit()）来主动终止自己，也可以被操作系统或其他进程强制终止。终止的进程会释放占用的资源，包括内存、文件描述符、锁等。\n4）进程的回收：\n当一个进程终止后，其占用的资源需要被回收，以便系统能够继续管理其他进程。操作系统通常会维护一个进程表来记录所有活动进程的信息，包括进程ID、状态、父进程ID等。当一个进程终止时，操作系统会将其状态标记为\u0026quot;终止\u0026quot;，并将其资源回收。父进程可以通过系统调用（如wait()或waitpid()）来等待子进程的终止并获取其退出状态。\n进程的控制和回收是操作系统的核心功能之一，它们确保了多个进程能够有效地共享系统资源，并提供了对进程行为的管理和监控机制。通过这些机制，操作系统能够保持系统的稳定性和可用性。\n1.3非本地跳转 非本地跳转（Non-local jump）是一个计算机编程中的概念，通常用于描述程序在执行过程中跳转到不同的代码段或函数，而不是从当前执行位置进入的常规控制流。这种跳转是通过特殊的机制实现的，而不是通过常规的函数调用或条件语句来执行的。\n非本地跳转通常用于处理异常、错误处理、资源清理或其他需要在不同层次的调用堆栈中进行的操作。在这种情况下，程序需要跳出当前的执行上下文，然后转到另一个上下文中，而不是按照通常的线性控制流继续执行。\n一种常见的非本地跳转机制是异常处理，例如在C++中的try-catch块或Python中的try-except语句。当发生异常时，程序可以从当前代码块跳转到异常处理代码块，而不是按照正常的控制流继续执行。这允许程序在异常发生时执行特定的处理逻辑，而不必在每个函数中都显式检查错误条件。\n非本地跳转的使用应该谨慎，因为它可以使代码更难以理解和调试。它通常用于处理紧急情况或特殊情况，而不是用于正常的程序流程控制。在许多编程语言中，提供了结构化的异常处理机制，以使非本地跳转的使用更加安全和可维护。\n附： 图片和笔记均是来源以下内容，并且包含个人思考。\n1.《深入理解计算机系统》第八章异常控制流\n2.mooc计算机系统基础（三）袁春风老师\n计算机系统基础(三)：异常、中断和输入/输出_中国大学MOOC(慕课) (icourse163.org)\n","date":"2023-09-28T00:00:00Z","permalink":"https://sutdown.github.io/p/%E9%98%B6%E6%AE%B5%E6%80%A7%E9%98%85%E8%AF%BB-%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%B3%BB%E7%BB%9Fcsapp%E4%BA%8C/","title":"阶段性阅读 《深入理解计算机系统》（CSAPP）（二）"},{"content":"前言： 声明：以下内容是通过看些视频和书籍综合的知识点，所以一般不是书籍某一章节完整的笔记，仅凭借个人兴趣看某一节的内容得到。\n前言：我是在一边看南京大学袁春风老师的计算机系统基础这门课程的同时阅读这本书的，本篇仅仅记录这段时间产生的对于计算机系统基础的问题和一些自己看书后对自己的回答，水平有限，本意是用作督促自己的学习。\n对于书上的内容，会在章节中优先选择性的去看与我目前学习有关的部分，对于某些难以理解的概念，在前面会先选择性跳过，在这之后再新学到其它内容能看懂时后会进行补充。\n其中的视频推荐是指在学习某个部分时，单独看书过于难懂时查阅的合适的辅助学习视频。\n1.操作系统的基本抽象概念 （此问题1学习水平有限，为免理解偏差，部分参考chatgpt回答）\n操作系统可以理解为应用程序对硬件操作的中间商，它存在两个基本功能\n1）防止硬件被失控的应用程序滥用\n2）向应用程序提供简单一直的机制来控制复杂而又通常大不相同的低级硬件要求。\n操作系统有几个基本的抽象概念，分别是进程，虚拟内存，文件。\n进程 进程是操作系统中的一个基本概念，它代表了正在运行的程序的实例。每个进程都有自己的独立执行环境，包括内存空间、寄存器状态、打开的文件等。操作系统通过进程管理器来创建、调度、暂停、恢复和终止进程。多个进程可以并发运行（并发运行指的是一个进程的指令和另一个进程的指令交错执行），无论在单核处理器还是多核处理器中，操作系统通过分配时间片来实现进程之间的切换，从而实现多任务。\n虚拟内存 虚拟内存是一种内存管理技术，它允许一个进程访问超出其物理内存限制的内存空间。每个进程认为自己有一个连续的虚拟地址空间，而实际上这些地址可能映射到物理内存中的不同位置，甚至可以映射到硬盘上的一部分（称为页面交换）。虚拟内存的好处包括更好的内存利用率、更大的可用地址空间和更好的内存隔离。操作系统负责虚拟地址到物理地址的映射，以及数据的页面调度和页面置换。\n虚拟内存的运作需要硬件和操作系统软件之间精密复杂的交互，包括对处理器的生成的每个地址的硬件翻译。基本思想是把一个进程虚拟内存的内容存储在磁盘上，然后用主存作为一个磁盘的高速缓存。\n图二：进程的虚拟地址空间\n文件 在操作系统中，文件是存储在永久性存储介质（如硬盘、固态硬盘）上的数据集合，也就是字节序列。文件可以包含文本、图像、音频、视频等各种类型的数据。操作系统通过文件系统管理文件，提供了访问、创建、删除、修改文件的接口。文件系统还负责文件的组织、保护和共享。在许多操作系统中，文件是通过文件描述符或句柄来表示的，它是一个抽象的数据结构，用于跟踪文件的打开、读取、写入等操作。\n2.针对存储器的层次结构 该图为计算机的存储系统，我们再以另一种图观察，\n存储器结构的中心思想 这是存储器的层次结构。\n（注解：\nALU是算术逻辑单元，一般可用于存储临时地址和计算；register是寄存器，可见上一篇汇编语言中的详细介绍；\ncache高速缓存器；Memory主存储器；HardDisk硬盘；\nTLB转换旁路缓存；MMU内存管理单元；这两者设计虚拟地址和物理地址的转换，下一个问题讨论\n）\n需要理解，从Register，cache，Memory，HardDisk依次离CPU越来越远，受到CPU的控制越来越弱，所以存储空间越来越大，读取速度越来越低。\n中心思想：当我们需要取第k+1层的数据时，位于k层的更快更小的设备作为位于k+1层的更大更慢的存储设备的缓存；也就是层次结构中的每一层都缓存来自上一层的数据对象。\n比如当我买需要第k层的某个数据对象d时，我们会现在上一层中寻找（上一层相当于这一层的子集，上一层是更接近CPU的一层），如果刚好找到，这可以称为缓存命中，因此可以加快读取时间；否则称为缓存不命中，此时，会直接在第k层寻找，然后传给上一层，再逐渐传给CPU，那如何传给上一层？传给上一层的数据放在哪里？\n每一层的位置并不是空的，可以说，上一层是第k层的子集，而当传递数据时到上一层时只能覆盖上一层原有的数据，上一层被覆盖的数据称为牺牲快，其中的过程称为替换或者驱逐，那时如何覆盖的？有两种策略，一是随机覆盖，速度很快，但是不确定性很高，难以定位；二是映射的思想，第k层的哪些数据对应上一层的哪个快的部分，比如上一层是1 4 8，下一层是 2 3 5 6 7 9，那找数据4时，可以缓存命中直接在上一层找到，找数据3时，在下一层找到，可以假设2 3对应1，然后3覆盖上一层的1（只是这样子抽象举例）。\n局部性原理 但对于一个良好的计算机程序而言，通常具有良好的局部性，也就是当它们倾向于使用临近于其它最近引用过的数据项的数据项或者最近引用的数据项本身，这种倾向称为局部性原理。在硬件层，该原理允许通过引入cache的小而迅速的存储器保存最近引用的指令和数据项，从而提高对于主存的访问速度。局部性原理在操作系统的各个层次运用广泛。\n局部性通常分为时间局部性和空间局部性，时间局部性可以简单理解成一个相同位置的变量被引用多次；空间局部性就是对于步长为k的引用模式的程序，步长越小，空间局部性越小。\n对于空间局部性，我们可以举例，一个二维数组m行n列，双层for循环找到其中每一个数的值。很自然有两种写法，一是以行为单位，一行行找；另一种是一列列找；数组中虽说是二维，但在计算机中仍然是以一行连续的内存单元存储，所以每行找时它的步长为1，而以列找时步长为n，速度差异显而易见。\n高速缓存存储器（cache） 最初的存储器层次结构只有三层，寄存器，主存储器和磁盘存储，不过随着CPU和主存间的举例逐渐增大，二者这件被迫增加了L1高速缓存，而由于CPU和主存间性能差距继续增大，因此插入了L2高速缓存。有些现代系统还包括了L3高速缓存。\n通用结构 (a)高速缓存大小C=每个高速缓存的字节数B（Byte）×每个组的行数E×组数S（Series）；\n(b)高速缓存结构：地址位m=标记位t+组索引s+快位移b； $$ (B=2^b,S=2^s) $$直接映射高速缓存（E=1） 假设只有寄存器，L1高速缓冲，和主存时\nCPU向内存寻求w这个字，首先在L1中确认是否有这个字，若有则缓冲命中；若没有则缓冲不命中，L1向主存寻求w的副本并且存储到它的高速储存行再返回到CPU。在L1中确定是否命中的过程就是直接映射高速缓存的体现。\n字w中如上通用结构中，\n1）通过组索引确认在哪一组\n2）先后通过标记位和快偏移得到准确位置\n组相联映射高速缓存 这个与直接映射高速缓存的最大不同在于E，组相联放松了这个限制，E可以大于1，当每个组有n行时，称为n路组相联映射高速缓存。较高的关联度（E的值越大），可以降低高速缓存中由于冲突不命中出现的抖动的可能性。\n同样，当缓存不命中出现需要替换的行时，有两种策略：\n1.随机选择要替换的行\n2.利用局部性原理，使其选择在比较近的将来引用被替换的行的概率较小\n全相联高速缓存 特点：只有1组。\n由于该方式需要并行的搜索许多相匹配的标记，构造一个又大又快的相联高速缓存很困难，而且很昂贵。因此这种方法只适合于较小的高速缓存，例如虚拟内存系统中的快表（TLB），它缓存页表项等。\n关于写的相关思路 首先把要写入的字w的副本放在高速缓存器中，有两种方案进行接下来的处理： 1.（主流），利用写回和写分配的思路，\n写回：在替换算法要更新这个驱逐的快时，才把它写入紧接的低一层中。减少了总线流量，增加了复杂性\n写分配：加载相应的低一层中的块到高速缓存中，然后更新这个高速缓存快，不命中会导致一个块从低一级到达高速缓存。\n2.直写和非写分配。\n如何写一个高速缓存友好的代码 首先，我们要了解从哪个方面入手：\n从组相联映射高速缓存中我们不难看出，利用高速存储区的结构和访问数据的思路，让最常见的情况运行的快；\n然后同时也要减少每个循环内部的不命中数量。\n在代码上的改进有：\n一是对局部变量的反复引用是时间局部性好的体现；\n二是步长为1的引用模式是空间局部性好的体现。\n注：视频推荐： 1.B站理解分块和cache：【一张图解决主存和cache的映射问题】 https://www.bilibili.com/video/BV1h3411h7kV/?share_source=copy_web\u0026vd_source=3fb5d6e30320f23cfaa7814e883f9b2f\n2.B站up洛城花客视频：【《C专家编程》7. 对内存的思考(存储器的层次结构)】 https://www.bilibili.com/video/BV1Ed4y1Y7HN/?share_source=copy_web\u0026vd_source=3fb5d6e30320f23cfaa7814e883f9b2f\n3.虚拟内存 对其概念的基本问题：虚拟内存是如何产生的？虚拟内存是什么？什么时候涉及虚拟内存？虚拟内存有什么用？C语言中的malloc和free的动态内存分配和虚拟内存有什么关系？虚拟内存和物理内存有什么关联吗？\n基本概念 虚拟内存是硬件异常，硬件地址翻译，主存，磁盘文件和内核软件的完美交互，它为每一个进程提供了一个大的，一致的和私有的地址空间。早期PC大多采用物理地址寻址，现在大多是CPU通过生成虚拟地址经过地址翻译后访问主存。\n内存是用来存放正在进行或者将要进行的进程内容；\n磁盘是用来存放需要存储的内容。\n物理寻址：CPU通过内存总线，传递给主存，主存取出地址，返回给CPU，CPU将其存放在一个寄存器里。\n虚拟寻址：CPU通过经过地址翻译（将虚拟地址转换成为物理地址），再送到主存。\nDRAM（Dynamic Random Access Memory，动态随机存取存储器）是一种基于电容的内存，它使用电容来存储和表示数据。每个存储单元由一个电容和一个访问晶体管组成。电容在存储器中充电或放电来表示数据的0和1。由于电容会逐渐漏电，DRAM需要定期刷新以保持数据的正确性。DRAM缓存的优点是它可以提高地址翻译的速度，简化链接和加载，实现代码和数据共享，以及保护每个进程的地址空间不被其他进程破坏。故常用来表示虚拟内存系统的缓存。\nSRAM（Static Random Access Memory，静态随机存取存储器）是一种基于触发器的内存，它使用稳定的存储电路来存储和保持数据。每个存储单元由一个存储器单元和控制电路组成，其中存储器单元由多个触发器构成，能够存储比特数据。由于采用了触发器结构，SRAM在不断刷新的过程中保持数据的稳定性。常用SRAM缓存表示CPU和主存之间的L1，L2，L3高速缓存。\n页表也就是页表条目（Page Table Entry）其中包含有效位和地址信息。\n虚拟页面的集合也包括三个部分，未分配的（未分配和创建，没有任何磁盘空间），缓存的（已经缓存在物理系统中的已分配页），未缓存的（未缓存在物理内存中的已分配页）。\n虚拟内存的两个主要功能：\n一是将主存看作存储在磁盘上地址空间的高速缓存，再主存中只保留活动区域，并且根据需要在主存和磁盘之中传递数据，将主存中不常用的存入磁盘，将磁盘中的需要内容传给主存，以此高效利用主存。\n二是在多进程的情况下，为每个进程提供一致的地址空间，并且保护其不受破坏，简化内存管理。\n注：虚拟地址和物理地址都有相应的地址空间。也就是说，每个数据对象，每个数据对象有多个独立空间的地址，每个地址选自不同的地址空间，从而保证进程的独立性。但同时，虚拟地址所需的地址空间也会占内存（后面会有相应的解决方案）。\n虚拟内存系统（VM）的大概使用 当进程过多时，内存中容易发生内存泄露，部分内存无法被释放，逐渐堆积容易造成内存溢出。\nVM的出现可以解决这一问题（DRAM是全相联）。\n我们要认清，一般认为虚拟地址空间是一段连续的内存空间，但实际上，它通常是被分隔成多个物理内存碎片，还有部分暂时存储在外部磁盘存储器上，在需要时进行数据交换。页表和物理内存一般在DRAM中，虚拟内存在磁盘中。\n页表，物理内存，虚拟内存是并行的。CPU首先引用虚拟内存中的某个地址，然后在页表中读取它的状态，\n情况一：页命中。通过页表中的有效位判断发生命中，再根据地址信息找出物理地址的位置即可。\n情况二：缺页。（也可以理解成缓存不命中）通过页表的有效位发现不命中，然后当物理内存中是满的时候，需要选择一个牺牲页被我们所需要的地址替换。选择牺牲页是，一由于DRAM中是全相联，可以选择任意个；二为降低不命中率，只要局部性原理用的好，可以充分提高运行时的效率，局部性原理能够保证程序将趋向于在一个较小的活动页面的集合上工作，将工作集页面调度到内存之后，接下来对这个工作集的引用将直接命中，而不会产生额外的磁盘流量。也对应基本概念中虚拟内存两个主要功能之一。\n地址翻译 地址翻译：虚拟地址空间和物理地址空间的映射。\n一：从地址角度上看\n自行理解图片。\n二：计算机中对于虚拟地址的执行过程\na.页面命中\n1)处理器首先生成虚拟地址（Virtual Adress)给内存管理单元（Memory Manage Unit）；MMU利用虚拟地址中中的虚拟页号（Virtua Page Number）确立适当的页表项地址（Page Table Entry Adress），将PTEA传送给高速缓存/存储器然后高速缓存/存储器将页表项（Page Table Entry）给到MMU;\n2)MMU根据PTE中的物理页号（Physical Page Number）和虚拟地址中的虚拟地址偏移量（虚拟地址偏移量和物理地址偏移量相同），得到最终的物理地址（Physical Adress），再将其传送给高速缓存/存储器，高速缓存/存储器将该数据字给处理器。\n注（优化）：在图中的②位置中，如果MMU频繁的和内存取拿数据，每次取数据可能会有几十到上百周期时间的代价，因此为减少这样的开销，在MMU中有一个TLB（Translation Lookaside Buffer）块表，这其中包含多项单个PTE的值，这样所有的地址翻译步骤都在芯片上的MMU中执行，提高运行速度。\nb.缺页\n第一步和以上相同，但是在得到PTE时，发现其中的有效位为0，发生缺页的情况，此时发生异常，缺页异常处理程序启功，将牺牲页和新页在高速缓存/存储器和洗盘上交换位置再重新更新PTE的位置传给MMU，后面过程也与页面命中情况一致。\nSRAM中的高速缓存和DRAM中的虚拟内存的结合 主要思想：地址翻译发生在高速缓存查找之前。\n如右图：\n**注（优化）：**我们在上文提到，虚拟系统的存在在内存空间中占的空间较多，即使在时间性能上进行了优化也没有直接的解决这个问题。看虚拟内存系统，其中的虚拟地址大多在硬盘中，主要关注点在于页表。故此有压缩页表的想法被提出，也就是使用层次结构。这样有两个好处：\n一是如果一级页表的PTE是空的，那么二级页表就不存咋，不会为其分配空间。而不是将所有空间都先分配出来。\n二是我们只将一级页表和常用的二级页表放在主存中，其余情况只在需要时创建，调入或者调出二级页表即可，这样能降低主存的压力。\n进程和虚拟内存 内存映射\n在Linux上通过将一个虚拟内存区域与一个磁盘上的对象关联起来，以初始化这个虚拟内存区域上的内容，这个过程称为内存映射。\n它的方式有两种：1）Linux系统中的普通文件；2）匿名文件。\n此方法可以初始化虚拟页面，在任何时刻，交换空间都限制着当前运行着的进程能够分配的虚拟页面的总数。\n共享对象\n在进程中，一个对象被映射到虚拟内存的一个区域，要么作为共享对象，要么作为私有对象。\n共享对象是如果一个进程将一个共享对象映射到它的虚拟地址空间的一个区域内，那么这个进程对这个区域内的任何写操作，对于哪些也把这个共享对象映射到它们虚拟内存的其它进程而言，也是可见的。并且，页表的部分相同也是共享对象成立的条件。\n私有对象将对象作为私有的重要技术是叫做“写时复制”，也就是在原区域中试图修改内容时，回触发故障处理程序，故障处理程序检测到保护异常是由于进程试图写私有文件时，会在该物理内存中创建新的父母，更新页表条目指向新的副本，然后恢复这个页面的可写权限，如下图：\n虚拟内存的作用 虚拟内存是一种内存管理技术，它可以为每个程序提供一个独立的虚拟地址空间，使得程序不需要考虑物理地址的分配和重定位等问题，而只需要使用相对地址或者逻辑地址来表示代码和数据的位置。\n1）简化链接。\n链接是指将多个目标文件或者库文件连接成一个可执行文件的过程，它可以分为静态链接和动态链接两种方式。静态链接是指在编译时或者加载时，将所有的目标文件或者库文件合并成一个可执行文件，这个可执行文件包含了所有需要的代码和数据，可以独立运行。动态链接是指在运行时，根据需要动态地加载和连接目标文件或者库文件，这些文件不是包含在可执行文件中，而是存储在外部的共享库中，可以被多个程序共享。\n虚拟内存对于静态链接来说，可以简化加载和重定位的过程，因为程序可以使用相对地址或者逻辑地址来表示代码和数据的位置，而不需要修改为绝对地址或者物理地址。虚拟内存对于动态链接来说，可以简化共享库的管理和访问的过程，因为程序可以使用虚拟内存映射来加载和连接共享库，而不需要复制或者移动共享库的代码和数据。\n2）简化加载。\n加载是指将可执行文件从磁盘或者网络加载到内存中，并准备运行的过程，它包括分配内存空间、解析符号、重定位地址、初始化数据等步骤。\nLinux加载器为代码和数据段分配虚拟页，把他们标记为无效，将页表条目指向目标文件中适当的位置。加载器不需要从磁盘到内存中复制任何数据。在每个页面被初次引用时，一本是被CPU取指令时引用或者一条正在执行的指令引用一个内存位置时引用，虚拟系统会自动调入数据页。\n3）简化共享。\n独立地址空间为操作系统提供了一个管理用户进程和操作系统之间共享的一致机制。操作系统创建页表，将相应的虚拟页映射到不连续的物理页面。页面的一致性使得数据之间可以共享。\n4）简化内存分配。\n减少内存分配和回收的开销：虚拟内存可以通过按需加载技术，使得程序不需要一次性将所有的代码和数据都加载到内存中，而只需要在需要时按需加载所需的部分。这样可以节省内存空间，减少加载时间，以及避免不必要的内存回收操作。\n缓解物理内存不足的问题：虚拟内存可以通过页面置换技术，使得程序不需要受限于物理内存的大小，而可以使用外部磁盘作为扩展的内存空间。当物理内存不足时，虚拟内存可以将一些不常用的页面从物理内存中移出，并将一些需要用到的页面从磁盘中调入物理内存中。这样可以增加可用的内存空间，以及实现对大型程序或者数据结构的支持。\n支持动态内存分配技术：虚拟内存可以通过分段机制或者堆机制，使得程序可以根据需要动态地申请和释放可变大小的内存空间。这样可以灵活地适应不同的需求，以及实现对递归、动态数组、链表等数据结构的支持。\n动态内存分配 （这一部分跟C语言的指针没学好有点关系。。。应该没写清楚）\n动态内存分配器（Dynamic Memory Allocator）维护着一个进程的虚拟内存区域——堆（heap）。分配器将堆视为一组不同大小的块的集合进行维护。每个块就是一个连续的虚拟内存片（chunk）。其中分为已分配的和空闲的部分。已分配的要么是被应用程序显式已分配状态，要么是被释放，释放同样分为应用程序显式释放和内存分配器自身隐式执行两种。\n在C语言中，malloc只会为其分配空间而不进行初始化；calloc是基于malloc的瘦包装函数，会将分配的内存初始化为0；realloc则是可以改变一个以前已分配块的大小。\n使用原因，当我们想要输入一串数据时，如果采用硬编码的形式，数据过长需要重新分配空间，数据过短太浪费空间；更好的方法是在运行前，得知数组大小的值，然后动态的分配这个数组。\n注意：动态内存分配是在一个进程中的虚拟内存区域堆中进行的，所以虚拟内存不是一个无限的资源，一个系统中被所有进程分配的虚拟内存的全部数量受磁盘上交换空间的数量所限制的。\n分配器的目标\n1）最大化吞吐率。指每个单位时间内完成的请求的数量，通常用于衡量系统的性能和效率。请求可以是分配请求或释放请求，分别表示申请或释放内存空间。\n2）最大化内存利用率。内存利用率是指内存中被占用的空间与总空间的比例，通常用于衡量内存的使用情况和资源浪费程度。内存利用率越高，表示内存空间被有效利用的程度越高，反之则表示内存空间被浪费的程度越高。\n最大化吞吐率和最大化内存利用率是两个不同的目标，它们之间可能存在冲突或者平衡。一般来说，为了最大化吞吐率，需要减少内存分配和释放的开销，提高内存访问的速度，选择合适的分配算法和回收策略等。 而为了最大化内存利用率，需要减少内存碎片，提高内存空间的复用，选择合适的分配大小和对齐方式等。\n内存碎片\n内存碎片是指内存空间中由于分配和释放操作导致的小块空闲区域，它们无法被有效利用，造成内存资源的浪费。\n主要分为两种类型：外部碎片和内部碎片。外部碎片是指分配给进程的内存块之间的空闲区域，它们由于大小或位置不合适，无法满足新的分配请求；内部碎片是指分配给进程的内存块内部的空闲区域，它们由于分配大小超过进程实际需求，或者分配对齐方式导致的多余空间。\n它会影响内存利用率和系统性能，因为它们会减少可用的内存空间，增加内存分配和回收的开销，降低内存访问的速度等。但同样可以通过一些方法来减少或避免，比如选择合适的分配算法（如最佳适应、最坏适应、首次适应等），使用紧凑技术（将已分配的内存块移动到一起，消除外部碎片），使用分页或分段机制（将内存空间划分为固定大小或可变大小的单元，减少内部碎片），使用垃圾回收技术（自动检测和回收不再使用的内存空间）等。\n注：视频推荐： 1.【【操作系统】内存管理——地址空间】 https://www.bilibili.com/video/BV1oi4y1T7RP/?share_source=copy_web\u0026vd_source=3fb5d6e30320f23cfaa7814e883f9b2f\n2.【【操作系统】内存管理——虚拟内存】 https://www.bilibili.com/video/BV18v411a7Vk/?share_source=copy_web\u0026vd_source=3fb5d6e30320f23cfaa7814e883f9b2f\n3.【一分钟讲逻辑转换从虚拟内存到物理内存-动画版】 https://www.bilibili.com/video/BV1gV411u7sc/?share_source=copy_web\u0026vd_source=3fb5d6e30320f23cfaa7814e883f9b2f\n附录： 参考视频书籍声明：\n1.《深入理解计算机系统（原书第三版）》\n2.mooc南京大学袁春风老师计算机系统基础\n","date":"2023-09-11T00:00:00Z","permalink":"https://sutdown.github.io/p/%E9%98%B6%E6%AE%B5%E6%80%A7%E9%98%85%E8%AF%BB-%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%B3%BB%E7%BB%9Fcsapp%E4%B8%80/","title":"阶段性阅读 《深入理解计算机系统》（CSAPP）（一）"},{"content":"《雾山五行》观感 （包含剧透）\n整体来说，在美术，音乐，剧情上都是一部很好的动漫，目前只更了七集，还有个终章没出，林魂导演一人挑大部分大梁，制作团队非常用心，强烈推荐了，我从三个点展示个人观感。\n美术风格 （不懂美术，个人感觉）这是一部2D动画的样子，画风就是水墨画的感觉，后面也有纯黑白的打斗场面，如果单纯的冲着画风和打斗场面，就可以直接看这部动漫了。而且剧中角色的一些颜色特征也参考了脸谱中的颜色以及五行颜色，在角色名，角色外观，场景等都能感受到中国美术给人的独特的观感，言语表达有限，留下几张照片了。\n角色剧情 七集其实是分为两部分，跨越时间也很长，伏笔悬念很多，剧情线还有点复杂，目前刚好看完一遍其实并没有完全看懂所以根据现存印象讲述个人感受。\n整篇故事是以小麒麟为线索展开的，闻人翊悬为了救申屠文殊和他的母亲导致小麒麟走丢，一个村子以小麒麟的血和鳞片为源头制造了一条产业链，甚至牵扯到了朝廷，包括也有人猜测五行家族中有人对小麒麟图谋不轨，闻人翊悬因放出小麒麟导致妖界大乱被逐出家族，并且需要护送小麒麟重回到当初的地方的路程。\n小麒麟可以是人性欲望最深处的象征，它给一个平平的桃源村的人带来了妖力和一些治病的功效；给朝廷上渴望长生的官员带来了疯狂；麒麟作为圣兽，对妖界众人和五行家族的人的好处同样显而易见；在这种情况下，又怎么能够克制最深处的欲望对这个小麒麟视而不见呢，所以纷争就此展开。\n对闻人翊悬而言，小麒麟的所谓增强力量或者延年益寿的功效并不是他想要的，但是他同样有自己的执念。如果他没有执念，按照一眼望得到头的路程走下去，年少成名，功成身退或者羽化登仙对他而言都是唾手可得的东西，如果他没有执念，他的一生会因为他的天资顺风顺水，可是他有，他会为想为弟弟提升天赋闯无人敢闯的禁区，会因为想救他母亲的生命而去与麒麟争斗，落到被驱逐，隐居的下场，但当然也并不是说这些执念是坏事，执念是天生的。\n我们或许天生就有一条顺遂的路，不过大多人都不愿走在这条平和被安排的道路上。同样七情六欲都只是本性，《雾山五行》中最大程度的向我们反映了一个情感色彩更加鲜明的世界，我们所处的日常生活中，恶念会被社会主流所抑制，或者是说暂且处于我个人这个阶层或者时间段的人，恶念都还是一种萌芽的阶段，大家有，但是并不是那么深。多少人说过社会险恶，当进入社会，人与人之间的权力力量差别增大，恶念滋生的自然也会更快吧。\n动漫中最为直接展示的就是无数人对于小麒麟的渴望，但其中也不乏点滴的善意，所以接下来看看阵营。\n阵营 按照剧情的顺序，\n首先是桃源村（村子不知道有没有名字，我就暂且这么称呼），乍一看，真的很有《桃花源记》中描述的场景感，“土地平旷，屋舍俨然，有良田美池桑竹之属。阡陌交通，鸡犬相闻。其中往来种作，男女衣着，悉如外人。黄发垂髫，并怡然自乐。”,依山傍水，很有世外桃源的感觉，不过随着剧情发展，村子的面目遭人憎恶，小麒麟的鳞片掉到井水中，村民喝后病情痊愈，但是村长为首的几人就此将小麒麟囚禁，取血割鳞片造出无数的”聚仙丸“，以次寻求力量富贵，以背德的方式去满足欲望，人心的欲望过重必将自食恶果。\n然后的妖界和五行，他们其实都有量，没什么本质区别，不过分处不同立场而已。五行之人阻挡妖界和人界争斗，但其实动漫中刻画的妖和人都是两面色彩的，不同于刻板认知，人也有恶，妖也有善，情绪性格的双面性往往能更好的引人思考。不过妖和人的争斗，无非胜者为王败者为寇，妖界在动漫中处于下风，被困于方寸之地，以食人血而生的种族被迫灭绝，想要突破结界寻求生机可仍然不过飞蛾扑火，何其悲哉。最后的一小部分也牵扯到了朝廷，局面进一步扩大，但是没过多讲，这里也就不谈了。\n就先写这些了，推荐大家去看看这部动漫，也期待它的后序剧情。\n附录： 此篇文章出于在看完动漫全七集以及一位B站up主的视频之后有感，链接如下：\n雾山五行 _ 国创 _ bilibili _ 哔哩哔哩弹幕视频网\n【【全站最硬核】《雾山五行》伏笔深度详解\u0026amp;第三季预测\u0026amp;完结总评】 https://www.bilibili.com/video/BV15G411d7wB/?share_source=copy_web\u0026vd_source=3fb5d6e30320f23cfaa7814e883f9b2f\n","date":"2023-09-06T00:00:00Z","permalink":"https://sutdown.github.io/p/my-first-blog-based-github/","title":"My first blog based github"},{"content":"前言： 在读该书前1-9章时，难度尚可，但其实对于一个操作系统初学者来说，看懂并不一定等于完全理解，所以写下这篇文章是为了为前九章的学习做一个简单的回顾思考总结，在第10章时，书中明确写出该章节为高级章节，建议学习完第二部分并发后再阅读，所以现在的大体想法是写完今天这一篇后会从25章开始看起。\n由于刚开始学习并且没有进行很多代码类型的实验，大多都是看看书或者看看视频对于操作系统进行一个初步的理解，所以若是有错误欢迎指正。\n这本书分为三个部分：第1部分 虚拟化3-24章；第二部分 并发25-34章；第三部分 持久性35-50章。\n该书并不建议按照顺序阅读，一定要结合个人思考阅读所需要的章节。\n正文： 1.虚拟化，并发，持久性的来源，为什么是这三个部分？ 首先我们要认识什么是操作系统，操作系统是一种软件，负责让程序运行变得更容易，甚至允许你同时运行多个程序，允许程序共享内存，允许程序和其它设备交互，以及其它类似操作，它们负责确保系统既易于使用又正确高效的运行。\n很多程序运行时，它们需要同时访问计算机的指令和数据共享内存，需要访问设备共享磁盘……如何合理的安排好各个程序的内存空间，使得计算机内的空间能被最大化的利用，储存更多的事情，进行更好的运算时虚拟化所要进行的任务，也就是多个程序如何运行，按照什么样的方式运行能够最好的利用空间。\n当大量程序交替进行时，称为并发问题，随着指令执行次数的增多，并发过程中可能会出现问题，所以是在于程序如何并发的。\n磁盘中的空间比内存更大，但是内存更接近cpu，可如果在程序运行时发生中断，电脑断电或者长时间不用等意外事件发生时，电脑要如何更好的保持数据的存放，也就是持久性。\n2.进程 1）进程的存在是为了什么？ 当你在电脑上打开多个程序时，比如音乐，游戏，浏览器等等，它们每一个都是一个进程，进程是对这些软件的运行进行一个概括，从而更好的对电脑中各种程序的运行进行一个解释。进程从字面上理解也就是正在运行的程序。\n2）进程重要的机器状态有哪些？ 进程的机器状态主要分为两部分，\n一是内存，指令存在内存中，正在运行的程序的读取和写入的数据也都存在内存中，因此进程可以访问的内存（也可以说是地址空间）也是该进程的一部分。\n二是寄存器，程序在运行时，很多都需要及时的读取和更新寄存器，比如Instruction Pointer指令指针寄存器（可以知道程序即将执行哪个指令），栈指针，桢指针（可以管理函数参数栈，局部变量和返回地址）。\n3）进程是如何创建的？ 步骤一：加载到内存。程序最初以某种可执行格式驻留在磁盘中，因此，操作系统需要先从磁盘读取这些字节，在加载到内存中进程的地址空间处。\n在该过程中遵循两个原则，一是加载过程要尽早完成；二是惰性加载，即在程序执行期间，仅加载需要加载的代码或者数据片段。\n步骤二：为该进程分配空间。要为运行时栈分配内存；也要为堆分配空间；同时执行一些初始化任务，特别是和输入输出有关的任务以便程序更快的运行。\n以上两步骤完成之后，再跳转到main例程，cpu将控制权给到新创建的进程中，程序开始执行。\n4）进程和线程的区别是什么？ 进程是一个具有独立功能的程序关于某个数据集合的以此运行活动，是系统进行资源分配和调度的独立单位，也是基本的执行单元。\n线程是进程中的执行运算的最小单位，是进程中的一个实体，是被系统独立调度和分派的基本单位，线程自己不拥有系统资源，只拥有一点在运行中必不可少的资源（程序计数器、一组寄存器和栈），但它可与同属一个进程的其他线程共享进程所拥有的全部资源。\n进程属于大量级，执行开销大，创建或者撤销时系统要为之分配和回收资源，并且通信机制复杂，但是可以很好的进行资源管理和保护；线程属于轻量级，执行开销小，通信机制简单，线程之间会进行协作同步，但是不利于资源保护。\n5）进程的状态 进程由三个状态，分别是运行，就绪，阻塞。这三者和后面的调度密不可分，因为多个进程共用同一个cpu，只用通过调度才能在合适的时间将cpu的控制权给相应的进程。\n6）UNIX中的系统调用API fork, wait 和 exec 是 Linux 系统中常用的三个系统调用，它们可以用来创建和管理进程，以及执行不同的程序。下面是它们的用法和功能：\nfork() 用于创建一个新的进程，它会复制当前进程的代码段、数据段、堆栈段等，然后在子进程中返回 0，在父进程中返回子进程的进程 ID。fork() 的函数签名是： 1 2 3 #include \u0026lt;sys/types.h\u0026gt; #include \u0026lt;unistd.h\u0026gt; pid_t fork(void); wait() 用于等待一个子进程结束，并获取其退出状态。父进程调用 wait() 后会阻塞，直到有一个子进程结束，然后返回该子进程的进程 ID，并将其退出状态保存在 status 参数指向的变量中。wait() 的函数签名是： 1 2 3 #include \u0026lt;sys/types.h\u0026gt; #include \u0026lt;sys/wait.h\u0026gt; pid_t wait(int *status); exec() 是一组系统调用，它们可以用来执行一个新的程序，替换当前进程的代码段、数据段、堆栈段等。exec() 的函数签名有多种形式，其中一种是： 1 2 #include \u0026lt;unistd.h\u0026gt; int execvp(const char *file, char *const argv[]); 其中 file 参数是要执行的程序的文件名，argv 参数是一个字符串数组，表示传递给程序的参数列表，最后一个元素必须是 NULL。\nfork(), wait() 和 exec() 可以组合使用来实现多种功能，例如：\n创建一个和父进程完全相同的子进程，让它们同时或交替执行不同的任务。 创建一个子进程，并让它执行另一个程序，例如 shell 命令或其他可执行文件，然后让父进程等待子进程结束。 创建多个子进程，并让它们分别执行不同的程序，然后让父进程等待所有子进程结束。 如果你想了解更多关于 fork(), wait() 和 exec() 的用法和示例，你可以参考以下网页12345。\n3.如何更好的虚拟化cpu。 1）虚拟化时可能面临的困难。 虚拟化CPU指的是在多个进程运行时，给每个进程都有种独占CPU的错觉，采用了时分共享的方法，运行一个进程一段时间，再运行另一个进程。举个抽象例子，两个人吃一个桃子，桃子在两人不知情的情况下轮流在两人手中，然后在两人吃的时候都能吃到桃子，那么他们都以为自己是在独占桃子，不会想到有另一个人也在吃。桃子是CPU，人是进程，计算机中就是那么运作的。\n在构建这样的虚拟化机制时面临两个难点：\n一是性能；二是控制权。进程在切换时对计算机的资源的转换也是存在，频繁的切换进程也就是计算机在频繁的运作。而在运行进程时，计算机需要保持控制权，不能让进程无限制的运行接管机器，在保持控制权的同时也要有高性能，这就是计算机面临的难点。\n2）进程之间如何切换，如何重获CPU的控制权？ 在进程运行时，控制权在进程手上，在进程需要切换另一个进程时，切换的时候控制权就在操作系统手上。\n协作方式：等待系统调用 只要当进程进行系统调用或者执行了某些非法操作时，控制权会转移给操作系统。但是这种方式很难应对一些程序的恶意行为。\n非协作方式：操作系统进行控制 时钟中断：以固定的频率向cpu发送中断请求。防止某些恶意程序一直抢占控制权。当然时钟的中断频率和进程的切换频率的具体时间也会影响计算机的性能。\n3）用户态和内核态是在什么情况下切换？ 计算机一般处于用户态，只有当进行系统调用时，程序执行特殊的陷阱指令，该指令跳入内核并将特权级别提升到内核模式，进入内核模式后，系统执行相应的特权操作，从而为调用进程执行所需的工作，完成后操作系统调用特殊的从陷阱返回指令，然后回到发起调用的用户程序。\n注：\n要初始化陷阱表。陷阱表时处理中断和异常处理程序的入口地址，操作系统启动时会让cpu记住陷阱表位置以供后续使用。 在内核模式的例子，比如I/O输入输出就是位于内核态下，如果IO位于用户态，那么任何用户进程都可以随时向磁盘发出IO请求，如果IO处于特权态，那就只能在该模式时发出IO指令。 计算机如何区分过程调用和系统调用？系统调用的部分是既定的汇编手工代码，这部分需要仔细遵循规定，以便正确处理参数和返回值并且执行硬件特定的陷阱指令。 要明确陷阱表的位置也是一项特权指令，是在程序进入内核态后的第一件事情。 4）保护和恢复上下文 cpu获得控制权后，是继续运行当前进程还是切换下一个进程，是有调度程序决定的，如果要切换，就要执行上下文切换。\n在计算机系统中，上下文是指进程或线程的执行环境，包括寄存器、内存、I/O 状态等。这是操作系统中非常重要的一个概念，因为它直接影响到系统的性能和响应速度。\n在保护和恢复上下文的过程中，CPU 会将当前进程或线程的上下文信息保存到内存中，然后加载新进程或线程的上下文信息。这个过程需要保存和恢复大量的数据，包括 CPU 寄存器、内存、I/O 状态等。因此，上下文切换是一项非常耗费资源的操作。\n在 Linux 系统中，保护和恢复上下文的过程主要是通过保存和恢复 CPU 寄存器来实现的。当一个进程或线程被挂起时，CPU 会将当前寄存器中的值保存到内存中；当它再次被调度时，CPU 会从内存中读取之前保存的值，并将其恢复到寄存器中。这个过程需要非常高效和精确的处理，以确保系统能够快速地响应用户请求。\n4.进程的调度的发展历程，由简到繁。 1）两个看似矛盾的指标。 周转时间 周转时间指的是用任务完成的时间减去任务到达系统的时间。\n第一种最为原始的策略是先进先出（FIFO）。也就是先到的程序先执行，后到的程序需要等到先到的程序执行完后再执行。这样问题就来了，加入先到的程序执行时间很长，那么紧接着到的程序就要等很长的时间，周转时间就变大了，这对计算机并不是件好事。这也成为计算机中的一个护航效应，一些耗时较少的潜在资源消费者被排在重量级的资源消费者之后。所以这个方案很难对计算机有很大的好处。\n第二种是最短任务优先（SJF，shortest job first），这也貌似是个好方法，解决了先进先出中的重量级程序的问题，但其实并没有完全解决，只有在轻量级和重量级同时到达时，会让轻量级优先，要是轻量级晚了一点点，都是要等到重量级完成之后的，周转时间依旧很长。\n现在有没有注意到，以上两种方案都属于非抢占式调度程序（non-preemptive），这样的系统会等每项工作做完，再去考虑新的工作。但是几乎现在所有的程序都是抢占式程序（preemptive）也就是会进行上下文切换，临时停止当前进程启动新的进程。\n第三种是抢占式最短作业优先（PSJF，preemptive shortest job first）也可以被称为最短完成时间优先（SCJF，shortest time-to-completion job first）。也就是在新的工作进入系统时，会确定剩余工作和新的工作中谁的完成时间最少，然后调度该工作。\n响应时间 响应时间指的是从任务到达系统到首次运行的时间。\n这里采取的解决思路是轮转（Round-Robin，RR），也就是RR在一个时间片内运行，然后时间片结束后，切换下一个工作。时间片越短，响应时间越好，但是上下文切换的成本也就更高。这里出现了一种**摊销（amortization）**的方法，也就是设置更高的时间片时间，以减少在程序运行中上下文切换所消耗时间的比例。\n这两种指标在各自的方法领域中可以达到不错的效果，但是很难同时拥有较好的周转时间和响应时间，并且我们也没有考虑到IO的因素，在程序进行IO时，不会占用CPU的控制权，即会到磁盘中运行，那次是CPU的控制权如果交给其它程序，那么能更好的提高系统的利用率，这种操作也叫重叠（overlap）。\n2）多级反馈队列（Multi-level Feedback Queue，MLFQ） 要如何同时实现减少响应时间，减少周转时间，并且协调IO的调度程序？\nMLFQ的调度策略存在自己的规则，也就是存在多个队列，每个队列有不同的优先级，一个工作只存在一个队列中，一个队列中可以有多个工作，MLFQ总是执行优先级较高的工作，因此它的关键在于如何设置它的优先级，接下来我们来看看它的基本规则。\n规则1：如果A的优先级\u0026gt;B的优先级，运行A（不运行B）。\n规则2：如果A的优先级=B的优先级，轮转运行A和B。\n规则3：工作进入系统时，进入最高优先级（最上层队列）。\n如果新的工作是短工作，那么就能先得到运行，符合减少周期时间。但如果是长工作，也会在运行一部分之后降到低优先级，不影响其它工作，同时规则5有力的保障了如果长工作仍然很多，不会滞留在低优先级。\n规则4a：工作完整个时间片后，降低优先级（移入下一个队列）。\n规则4b：如果工作在其时间片以内主动释放CPU，则优先级不变。\n如果有IO操作时，也就是在时间片之内主动放弃CPU，那么不会影响它的优先级。交互型和短工作能够很好的在高优先级得到实现，长工作也能公平的共享cpu。\n规则4（修订）：一旦工作用完了其在某一层中的时间配额（无论中间主动放弃了多少次CPU），就降低其优先级（移入低一级队列）。\n在规则4ab中，如果主动释放之后再次进入，时间是会重新开始即使的，那么狡猾的程序如果设计主动释放CPU，以保持在高优先级，占用更多的CPU时间，那就大事不妙，所以修订的规则4中将时间修改成了时间配额的限制，以防止这种情况。\n规则5：经过一段时间S，就将系统中所有工作重新加入最高优先级队列。\n规则5避免了饥饿问题，也就是交互型工作太多，长工作滞留在低队列。\n以上只是基础规则，如果涉及具体的调度操作，时间片的时间，交互的时间，进程的时间，上下文切换的时间，规则5中的时间……这些都需要一个合适的时间结果，已取得更好的调度结果，提高计算机的性能。\n3）比例份额 比例份额调度（proportional-share）是一种调度程序，也被称为公平份额调度（fair-share）。它的目标是为每个进程分配一定比例的CPU使用时间，而不考虑周转时间和响应时间。比例份额调度有一个很优秀的例子，由Waldspurger和Weihl提出的彩票调度，顾名思义，就是让进程像彩票一样分配占用时间，哪个进程中奖就能获得更多的占用CPU时间，更越活越的进程，也就得到更多的抽奖机会。\n","date":"2023-08-30T00:00:00Z","permalink":"https://sutdown.github.io/p/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E5%AF%BC%E8%AE%BA%E9%98%85%E8%AF%BB/","title":"《操作系统导论》阅读"},{"content":"——2023.08.30\n其实不太记得从18号来到天津的日子怎么度过的了，反正就买些日常用品，然后熟悉学校必经的几个点食堂快递站教学楼校医院，再者图书馆宿舍轮流学习，看看天津的外出线路景点无非这些。这十天吧，也基本过完了一门课程一本书，有困惑有迷茫有害怕有憧憬，也就这那么过来了。这两天算是小摆了一下吧，听了听对诗人民国人物的一些介绍，也在B站看完了原神部分之间错过的剧情（好的我承认我是云玩家）。\n现在看来主线是过完了，传说任务和邀约还差一点，不过暂时放放了。我第一次接触这个游戏是在去年的高考结束后，那时候仅仅是玩游戏，其实并没有接触到很多，一直玩到了须弥，不过森林书现在还没做完，枫丹也是这个月中旬主线做了一半。这个游戏在剧情角色养成上都很吸引我，但是目前还不太追求强度，圣遗物各种材料好难刷，给个勉强的材料武器拉到九十级天赋象征性加加就是我的顶点了，毕竟其实大一那一年玩游戏的频率很低，基本只在新版本上线简单做个主线就溜之大吉，佛系游戏哈哈。\n在这个过程也见证了很多玩家对这个游戏的评价，从各种对于角色剧情的分析，对强度的评价或者对角色的调侃，或者一些好玩的视频，这个生态其实还是很好的。不过其实一直困惑的一点是后来对于原神恶意的调侃大于我所认为玩笑般的调侃，算是游戏出圈所必须经历的两面评价吗，应该了那就是。\n对这个游戏重复性的简单任务，或者不断的收集材料培养角色其实大多时候并没有这么高的兴致。我应该是更偏向于剧情和角色。所以还是聊聊提瓦特大陆吧。\n蒙德 从水里钓上派蒙开始，那时还没有意识到派蒙会陪伴旅行者所有旅程的意义，可现在看来，还是很希望米哈游未来不要为派蒙给各位旅行者发刀子了，毕竟七国旅行的陪伴。最先来到的是蒙德，对一个新游戏还有最开始接触的国家，都会有着现实中我们常说的那么一点“社恐”属性，不过蒙德是一个很平和的国家，风和自由的国家，风花节，苹果酒，吟游诗人，小可莉，雷泽，安柏，凯亚，迪卢克，琴团长等等，这个国家的人民都很好，即使现在走到了枫丹，依然忘不了蒙德城的果酒湖，那棵七天神像附近好抓晶蝶的树（忘了叫什么了），骑士团的琴团长和可莉，晨曦酒庄的凯亚和迪卢克，哦对还有占星术士莫娜，有点中二的菲谢尔，倒霉但是善良的班尼特，当然也有远征为了守护蒙德安宁至今未曾谋面的大团长，永远注视子民心声的温迪，在他的传说任务第一章中为冒险者解开心结，在风花节悄悄帮着忙，曾经和无名的少年友人一起追求名为自由的战争，在友人死后化为友人的样貌，也在后来发现蒙德的不对和奴隶少女温妮莎推翻暴虐的统治，它和璃月的帝君都是初代神明，参与了一个国家彻底的发展历程。温迪看似散漫好像相处但是给人靠谱并且有点距离感的样子，也有人猜测希望温迪的第二章传说任务不是刀子了，当然希望不是，可有时候悲剧也是故事的必要条件。\n璃月 稻妻 须弥 枫丹 。。。摸鱼。。。每个国家写一点好像太长了，以后有想法再补吧，也可能不补。。。\n在这差不多一年的时光里，其实五位神明和国家给人的印象是基本一致的，蒙德是温柔随性平和的；璃月是一种很有中国传统文化的影子，比如帝君钟离，比如海灯节在春节时的氛围；稻妻有种日本特色的感觉，当然也和雷电将军一样给人危险的感觉，主线做完迅速跑路；须弥的兰纳罗小吉祥草王还有散兵主线赋予他们的故事也是感人至深，不过须弥的地形真的。。复杂；枫丹有待探索，水下世界还没去过，不过他们的审判和戏剧混淆，和目前登场的水神一样给人一种正式但不正式的戏虐感，继续期待版本更新。\n我在隐隐的期待剧情未来的走向，期待一个又一个国家的相继，期待着故事的结局，可当某一天，故事真正到达了结局时，心愿已成之后必然是察觉什么突然空荡吧。其实当我有时候面对未来的迷茫时也会想到二次元的世界，现实的故事一个篇章又一个篇章的过去，相遇的人终将离别，在往前的故事中，最放不下的还是最初遇见的人，后来遇见的万叶，林尼，妮露，卡维，流浪者等等角色，再怎么令人感动，但其实最期待的还是在未来的路上遇见温迪和公子的剧情，今年春节在璃月碰见温迪，现在的枫丹见到公子，都有一种十分惊喜的感觉，最初的相遇往往是铭记最深的，故事的再次相遇是惊喜快乐，角色性格不变，依然是我记忆中的那个人，不过现实中就算了，多年过去，其实我们都会变化的，这也是二次元和现实最大的不同，二次元是一种更固定的给人以心安的感觉，现实更加的变幻莫测难以琢磨。\n旅行者，当你重新踏上旅途之后，一定要记得旅途本身的意义。提瓦特的飞鸟、诗歌和城邦、女皇、于人和怪物，都是你旅途的一部分。终点并不意味着一切。在抵达终点之前，用你的眼睛多多观察这个世界吧。\n——温迪\n谨以此句告诫自身。\n","date":"2023-08-30T00:00:00Z","permalink":"https://sutdown.github.io/p/%E6%B5%85%E8%B0%88%E5%8E%9F%E7%A5%9E/","title":"浅谈原神"},{"content":"前言： 本来时想补补计算机系统基础的课程,看到时mooc里面南京大学的袁春风老师所讲，在看到（一）的后面的时候，看的很吃力，有些汇编的寄存器或者一些指令的执行都是完全陌生的，决定中断几天看看王爽老师的书籍《汇编语言》，个人所看，确实很容易看懂，并且能对计算机有着进一步了解。看书时如果有些c语言的基础并且对计算机有基础了解，看完后都会收获很多的。\n1.一个源程序是如何运行的。 源程序的运行过程：\n编辑 1）使用文本编辑器（比如记事本）写下代码，再用相应的编译器编译\n2）或者直接在开发环境上进行编写程序直接编译运行（比如C++/C语言可用Visual Studio或者DevC++）\n预处理 预处理是由预处理器处理，将源文件中以#开头的某些数据转换成代码，它的逻辑与编辑中的源代码文件相同，最终会得到一个.i文件。\n编译汇编 经过特定的编译器编译源程序文件，我们一般可以得到最多三个文件，目标文件（.obj)，列表文件(.lst)，交叉引用文件（.crf),后两者属于中间结果，我们最终只能得到目标文件。\n每个.c程序经过编译之后都会得到一个.o文件，也就是可重定位目标文件，经过编译汇编两个操作后得到机器语言的文件。\n可重定位目标文件（ELF）格式包含ELF头（存放16字节标识信息，文件类型，机器类型，节头表偏移，节头表大小等多信息），.text节（编译后的代码部分存放），.rodata节（只读数据），.data节（已经初始化的全局变量），.bss节（未初始化的全局变量，不占磁盘空间），节头表(存放每个节的节名，偏移，带线啊哦，访问元素，对其信息，起始地址等)。\n但由于此时只是存放在磁盘中，并未在内存分配，起始地址均为0，并且在接下来的运行中，可以通过节头表找寻相应信息进行连接。\n链接 多个可重定位目标文件(.o)，系统的库文件经过链接可以合并为可执行目标文件（.exe)。\n此过程会将磁盘中的各类文件放在一起，比如ELF中，固定代码（.init,.text,.rodata)会被存放在只读代码段中，未初始化和已经被初始化的数据（.data,.bss)会被存放在读写数据段中等等，另外在这个内存区中还 存在用户栈区域和堆区域以及共享库区域。\n注：静态链接和动态链接\n静态链接库和动态链接库是两种不同的链接方式，它们的区别主要有以下几点：\n静态链接库是在编译时将库文件中的目标代码直接拷贝到可执行文件中，形成一个完整的程序，不再依赖于库文件。动态链接库是在运行时或加载时将库文件中的目标代码加载到内存中，供程序调用，仅保留接口信息，依然依赖于库文件。 静态链接库会增加可执行文件的大小，因为每个使用静态链接库的程序都会包含一份库文件的副本。动态链接库可以减少可执行文件的大小，因为多个使用动态链接库的程序可以共享一份库文件的副本。 静态链接库可以提高程序的运行速度，因为在编译时就已经确定了所有的符号地址，无需再进行重定位。动态链接库可能会降低程序的运行速度，因为在运行时或加载时还需要进行符号解析和重定位。 静态链接库不利于程序的更新和维护，因为每次修改了库文件就需要重新编译所有使用该库文件的程序。动态链接库有利于程序的更新和维护，因为只需要替换库文件就可以实现更新，无需重新编译程序。 静态链接库更安全和稳定，因为不会受到其他程序或系统对库文件的影响。动态链接库可能会出现兼容性或安全性问题，因为可能会遇到版本冲突或被恶意篡改的风险。 运行 可执行文件能够直接被CPU执行。\n在Windows下通常为.exe,.sys,.com,.dll文件；在Linux下为ELF文件，Mac中为.dmg或者.app文件等。\n2.计算机硬件和软件等基本组成和功能。 计算机系统抽象层的描述 1）计算机软件：application（应用程序），语言处理系统（语言处理程序，运行时系统），操作系统（虚拟机，人机交互界面，提供服务功能的内核编程）\n2）计算机硬件：微体系结构，功能部件，电路，器件等\n3）指令集体系结构（Instruction Set Architecture）\n电脑基础部件 CPU（中央处理器，执行程序和处理数据）\n主板（Motherboard连接和管理各种部件）\n内存（Memory暂时存放运行中的程序和数据）\n硬盘（Hard Disk永久存放文件和数据）\n显卡（Graphics Card电脑的图形处理器，负责输出电脑的图像和视频）\n声卡（Sound Card）网卡（Network Card）\n电源（Power Supply）光驱（Optical Drive）外接设备（Peripheral Devices)\n注：\n1）计算机中运行的数据先由磁盘读入内存之中，然后才能被CPU处理。\n但是为了提高磁盘的访问速度，可采用磁盘缓存技术，即先把磁盘经常访问的数据预先加载到内存中，以减少实际的磁盘读写操作。\n为了扩大内存容量，可以使用虚拟内存的技术，即把磁盘的一部分空间当作假想内存以增加可用内存。\n断电时，由于磁盘是外部存储设备其中的数据不会丢失，而对于内存，作为内部存储设备，断电就会消失，但如果当时作为虚拟内存存储在磁盘中，其中的数据不会丢失。\n2）对于想要计算机系统硬件编程时，CPU存在一个内存地址空间，内存地址空间的大小同样由地址总线的宽度决定。\n主板：其上的核心部件比如CPU，存储器，外围芯片组，扩展卡槽等通过总线相连\n接口卡：CPU通过控制接口卡（接口卡位于扩展插槽上，扩展卡槽通过总线和CPU相连）发送命令，从而实现对外设（比如打印机，耳机，音箱，显示器等）的间接控制。\n计算机中的基本部件 1）CPU通过地址总线将地址信息传入存储器，存储器通过数据总线将对应信息传送给CPU，CPU发出对应内存读写命令传给存储器进行操作。（每一条总线课传输二进制中的0或者1，由n条总线，可以找到2的n次方个内存单元）\n2）CPU主要由运算器，控制器，寄存器，总线等组成。\n运算器：CPU的运算部件，主要进行算术运算和逻辑运算。\n寄存器：CPU的存储部件，可以暂时存放指令，数据和状态信息。\n控制器：CPU的控制部件，负责解释指令，发出控制信号，协调各个部件的工作。\n3.各种语言的发展历程。 机器语言 最初计算机使用的是机器语言，即只有0或者1的二进制方式，但是这种方法可移植性性低，编程困难，可读性差，容易出错。但是由于是计算机能直接识别和执行，所以它的执行速度快。现在的大多高级程序设计语言都会经过编译器最终转化为机器语言的形式。\n汇编语言 汇编语言在不用的设备中对应不同的机器语言指令集，并不具备可移植性。汇编语言相对机器语言增加了字符便于理解，它主要由汇编指令，伪指令和其它符号三类指令组成。汇编指令和机器指令的差别在于指令的表示方法上，汇编指令是机器指令便于记忆的书写形式\n高级程序设计语言 高级程序设计语言是一种更接近人类的自然语言，一般只有经过编译器和解释器的转换，才能编程计算机能够执行的低级语言，比如机器语言或者汇编语言。\n它可以分为过程式语言（如C语言，Pascal，Fortran），面向对象语言（C++，Java，C#）等。\n4.寄存器的了解以及它们的访问机制等。 汇编语言的寄存器是一种用于存储数据或地址的CPU内部的部件，通过指令进行读写操作。\n不同类型的CPU有不同数量和结构的寄存器，但一般可以分为以下几类：\n通用寄存器：\n用于存放一般性的数据，如算术逻辑运算的操作数和结果，或内存单元的偏移量。通用寄存器的长度取决于机器字长，通常有8个或16个。例如，8086CPU有8个16位的通用寄存器：AX、BX、CX、DX、BP、SP、SI（源变址寄存器）、DI（目的变址寄存器）。其中，AX、BX、CX、DX可以分为两个独立的8位寄存器来使用，如AX可以分为AH和AL。16位寄存器用于存储字，8位寄存器用于存储字节，在此1个字等于2个字节。\n在串处理指令中，SI用作隐含的源串地址，默认在DS中；DI用作隐含的目的串地址，默认在ES中。\n段寄存器： 用于存放内存单元的段地址，与偏移地址相结合，形成物理地址。段寄存器是根据内存分段的管理模式而设置的，通常有4个或6个。例如，8086CPU有4个16位的段寄存器：CS（Code Segment)、DS(Data Segment)、SS(Stack Segment)、ES(附加寄存器）。其中，CS为代码段寄存器，DS为数据段寄存器，SS为堆栈段寄存器，ES为附加段寄存器。\n指令指针寄存器： 用于存放下一条要执行的指令在代码段中的偏移地址。指令指针寄存器与代码段寄存器相结合，指示了CPU当前要读取指令的物理地址。例如，8086CPU有一个16位的指令指针寄存器：IP（Instruction Pointer)。\n注：物理地址=段地址16+偏移地址：本质含义是CPU在访问内存时，用一个基础地址（段地址16）和一个相对于基础地址的偏移地址相加，给出内存单元的物理地址。\n标志寄存器： 用于存放CPU执行指令后产生的一些状态信息，\n如进位标志(carry flag)、零标志(zero flag)、符号标志(sign flag)、溢出标志(overflow flag),奇偶标志位（parity flag)，方向标志位（Direction flag)等。标志寄存器可以影响程序的控制流程，如条件转移或循环指令。例如，8086CPU有一个16位的标志寄存器：PSW。\n注：无符号时可以通过zf，cf比较大小；有符号时可以通过sf，of比较大小。\n5.指令集。 8086CPU的指令系统的总结。\n1.数据传送指令 比如mov,push,pop(栈),pushf(push far),popf(pop far远程)，xchg(用于交换两个数据的内容，不能用立即数和段寄存器作操作数)等，这些指令实现寄存器和内存，寄存器和寄存器间的数据传送。\n2.算术运算指令 比如add,sub,adc(add with carry进位加法)，sbb(sub with borrow带借位减法)，inc(increase),dec(decrease),cmp(compare),imul(符号乘法指令，用于两到三个操作数)，idiv,aaa(ascii调整加法指令)等，这些指令实现寄存器和内存中数据的算术运算。它们的执行结果影响标志寄存器的sf，zf，of，cf，pf，af位。\n3.逻辑指令 比如and,or,not,xor,test,shl,shr,sal,sar,rol,ror,rcl,rcr等都是逻辑指令，除了not指令外，它们的执行结果都会影响标志寄存器的相关标志位，逻辑运算也就是位运算的应用范围很广泛，比如字母的大小写转换等。\n4.转移指令 可以修改IP或者可以同时修改CS和IP的指令系统统称为转移指令。\n转移指令分为以下几类：\n1）无条件转移指令，比如jmp（转移时，并不给出转移的目的地址，而是告知CPU转移的位移）\n2）条件转移指令，比如jcxz，je，jb，ja，jnb，jna等\n3）循环指令，比如loop（cx存放循环次数）\n4）过程，比如call，ret，retf（call和ret类似于C语言中函数的调用，call调用时吗，先将下一步的CS和IP压入栈中，在进行转移，ret使用时，先出栈地址再进行返回）\n5）中断，比如int，iret\n它的转移行为可以分为段内转移（指修改IP）和段间转移（修改CS和IP），段内转移又分为短转移和近转移。注意如果使用转移指令时寄存器冲突，则可以在使用前先将寄存器入栈，指令执行完再出栈。\n5.处理机控制指令 这些指令对标志寄存器或其他处理机状态进行设置，比如cld，std，cli，sti，nop，clc，cmc，stc，hlt，wait，esc，lock等\n6.串处理指令 这些指令对内存中的批量数据进行处理，比如movsb，movsw，cmps，scas，lods，stos等。若要用这些指令方便进行批量数据处理，则需要和rep，repe，repne等前缀指令配合使用。\n6.汇编语言和高级程序设计语言C语言的共性。 由于高级程序设计语言也是由低级语言一步步转化的，所以它们之间在思想上的共同点很多，通过了解汇编语言，也能进一步的加深对于高级语言的认知。\n比如函数调用，在C语言中我们只能理解到参数的传递，然后调用函数，但是在了解之后，这个应该是于转移指令有关，比如call和ret，首先将主函数中的地址压入栈中，再通过转移指令到达函数中，转移指令并不是把你直接带入函数的地址，而是给你两者的相对地址，计算后到达函数，在它们的存储中分别为数据区和代码区，因此相对地址位移是不变的，在函数执行完后，将CS和IP地址出栈，回到原函数。\n比如我们在代码的第一行写的#include或者#define，这两者都只不过高级程序语言的一种写法，在编译连接时会找到相对于的库函数，和原函数进行数据的比对操作，对于define，则是直接将对应的字符转换，所以#define在代码中起到的作用仅是增强可读性。\n比如循环，书中只是简单的介绍了用cx存储次数的循环，并没有深入解析，但也思路和while或者for都相差无几，汇编语言还有待深入学习。\n比如你初始化数据和不初始化数据编译器的处理，它们在编译时是分别放在不同的空间，如果不初始化，在编译时并不会分配内存。\n比如在数据结构中了解到数组的寻址是通过计算，而在汇编中也分为直接变址寻址 等多种方式的寻址和数组也很类似。\n7.计算机中内存空间的理解。 计算机中的存储空间是由不同的硬件设备组成的，它们可以分为以下几类：\n寄存器：寄存器是CPU内部的最小存储单元，它们可以存放指令、数据或地址，通常有8位、16位或32位。寄存器的访问速度最快，但数量有限，只能由CPU直接访问。\n缓存：缓存是位于CPU和内存之间的一种高速缓冲存储器，它可以暂时存放CPU经常访问的数据或指令，以减少CPU和内存之间的数据传输时间。缓存的容量比寄存器大，但比内存小，它可以由CPU或操作系统自动管理。\n内存：内存是计算机中主要的随机存取存储器，它可以存放程序和数据，供CPU直接读写。内存的容量比缓存大，但比硬盘小，它通常是易失性的，即断电后数据会丢失。内存的访问速度比硬盘快，但比缓存慢。\n硬盘：硬盘是计算机中主要的辅助存储器，它可以永久地存放大量的程序和数据，供CPU间接读写。硬盘的容量比内存大，但比光盘小，它通常是非易失性的，即断电后数据不会丢失。硬盘的访问速度比光盘快，但比内存慢。\n光盘：光盘是一种利用光学原理进行信息记录和读取的外部存储器，它可以用于备份或传输程序和数据。光盘的容量比硬盘大，但比磁带小，它通常是只读或可擦写的。光盘的访问速度比磁带快，但比硬盘慢。\n磁带：磁带是一种利用磁性原理进行信息记录和读取的外部存储器，它可以用于归档或备份大量的程序和数据。磁带的容量最大，但访问速度最慢，它通常是顺序访问或随机访问的。\n8.外中断和内中断的理解。 说明：由于此问题理解程度不深，以下部分参考bing回答。\n外中断和内中断是两种不同类型的中断，它们都是指在程序执行过程中，由于某些原因而暂停当前程序，转而执行另一个程序的过程。中断的目的是为了提高计算机的效率和响应能力，以及处理异常情况。\n外中断 由外部设备或事件引起的中断，如键盘、鼠标、打印机、时钟、电源等。外中断通常是异步的，即它们不依赖于当前程序的状态，而是随机发生的。外中断需要硬件支持，即需要有专门的中断请求线和中断控制器来传递和处理中断信号。外中断可以分为可屏蔽中断和不可屏蔽中断，前者可以通过设置标志位来开启或关闭，后者则不能被屏蔽，必须立即处理。\n内中断 由程序本身引起的中断，如除零错误、溢出错误、非法指令、缺页异常等。内中断通常是同步的，即它们依赖于当前程序的状态，而是在特定条件下发生的。内中断不需要硬件支持，即不需要有专门的中断请求线和中断控制器来传递和处理中断信号。内中断通常由操作系统或编译器提供相应的处理程序来处理。\n中断的优先级 指在多个中断请求同时发生时，计算机按照一定的规则选择优先处理哪一个中断的顺序。中断的优先级是由硬件和软件共同决定的，具体的方法有以下几种：\n中断向量表法：计算机在内存中建立一个中断向量表，每个中断请求都有一个对应的中断向量号，通常越小的号码表示越高的优先级。当多个中断请求同时发生时，计算机按照中断向量号从小到大的顺序依次处理。 中断屏蔽字法：计算机在内存中设置一个中断屏蔽字，每个中断请求都有一个对应的屏蔽位，通常为0表示允许中断，为1表示屏蔽中断。当多个中断请求同时发生时，计算机按照屏蔽位从低到高的顺序依次处理，如果某个屏蔽位为1，则忽略该中断请求。 中断控制器法：计算机使用一个专门的硬件设备，即中断控制器，来管理和分配多个中断请求。每个中断请求都有一个对应的优先级码，通常越大的码表示越高的优先级。当多个中断请求同时发生时，中断控制器按照优先级码从高到低的顺序依次处理，并向CPU发送相应的信号。 CPU如何响应多个中断请求的问题， CPU如何检测中断请求：CPU有一个专门的引脚，即中断请求线，用于接收来自外部设备或内部程序的中断信号。当有一个或多个中断信号到达时，CPU会在每条指令执行完毕后，检测中断请求线的状态，如果为高电平，则表示有中断请求发生，需要进行处理。 CPU如何识别中断请求：CPU有一个专门的硬件设备，即中断控制器，用于管理和分配多个中断请求。每个中断请求都有一个对应的优先级码，通常越大的码表示越高的优先级。当多个中断请求同时发生时，中断控制器按照优先级码从高到低的顺序依次处理，并向CPU发送相应的信号。CPU根据信号的内容，确定是哪个中断请求，并找到相应的中断服务程序的地址。 CPU如何执行中断服务程序：CPU在执行中断服务程序之前，需要保存当前程序的执行状态，即将当前程序的标志寄存器、指令指针寄存器和其他相关寄存器的内容压入堆栈。然后，CPU根据中断服务程序的地址，跳转到该地址开始执行。在执行完毕后，CPU需要恢复原来程序的执行状态，即将堆栈中保存的内容弹出到相应的寄存器。最后，CPU返回到原来程序被中断的地方继续执行。 附录： 参考资料：\n1，《汇编语言（第四版）》王爽\n2，mooc南京大学袁春风老师计算机系统基础（一）\n","date":"2023-08-27T00:00:00Z","permalink":"https://sutdown.github.io/p/%E8%A7%82%E7%8E%8B%E7%88%BD%E6%B1%87%E7%BC%96%E8%AF%AD%E8%A8%80%E6%89%80%E5%BE%97/","title":"观王爽《汇编语言》所得"},{"content":"[TOC]\n一：绪论 1.算法和算法设计 算法是对特定问题求解的一种描述，是指令的有效序列。 算法设计的主要方法和基本思想为：贪心法，回溯法，递归和分治，动态规划法，分支限界法。 算法分类：精确算法，启发式算法，近似算法，随机算法。 算法的性质：输入（0或者多），输出（至少一个），确定性，能行性，有穷性。 算法表示：自然语言，编程语言，伪代码。 常见的算法应用：搜索问题，排序问题，图论问题，组合数学问题，几何问题，数值计算问题。 2.算法与程序 程序是算法用某种程序设计语言的具体实现。 算法具有有穷性，程序可以不满足有穷性。 Q：操作系统是程序还是算法？\nAnswer：操作系统是一个在无限循环中执行的程序，并不是算法。操作系统的各种任务都是单独的问题，每个问题通过特定的子程序中算法实现，子程序得到输出结果后终止。\n3.算法设计流程 理解问题，预测输入 选择精确解或者近似解 确定数据结构，选择算法 描述算法，跟踪算法 分析算法效率 根据算法编写代码 二：算法分析 1.算法分析 算法分析是对算法的执行时间和所需空间的估算。当算法写成程序后，可通过对程序的性能测量来分析算法。\n程序的性能指：程序运行时所需的内存空间量和计算时间，也就是系统开销和求解问题本身的开销。\n性能评价的方法：解析（算法课程以解析为主，也就是对程序进行分析），测量。\n2.算法性能分析 1）算法复杂性 算法复杂性依赖于问题规模，算法输入，算法本身的函数。\n2）时间复杂度和空间复杂度 时间复杂度（算法需要时间资源的量） 时间复杂度指程序执行所需要的时间 ####### 1.计算方法：\n操作计数（operation count）：找出关键步骤的执行时间；\n注：关键步骤可以选择不止一个。赋值，比较，加和乘的次数等都可供选择\n程序步计数（step count）：确定程序总的执行步数。\n注：\n程序步是指语法语义的片段，由基本指令构成，比如算术类指令，数据移动指令，控制指令等。\n执行时间独立于所选用的实例特征。\n####### 2.情况\n最好情况：不常出现，不具有普遍性。\n最坏情况：确定上界，更具有一般性。\n平均情况：情况复杂，分析难度大。\n注：运算时注意区分元素成功查找的平均比较次数，平均比较次数（考虑失败的比较）。\n空间复杂度（算法需要空间资源的量） 算法的空间复杂度指算法执行时需要的存储空间。\n程序的空间复杂度指程序运行时所需的内存空间大小和实例特征的函数关系。\n程序空间组成 ####### 1.程序运行时所需空间：\n指令空间（编译程序后指令的存储空间）。\n数据空间（常量和简单变量的所需空间，复合变量比如数组链表树图等所需空间）\n环境栈空间（保存函数返回时恢复运行所需要的信息）\n​\n####### 2.程序p的空间需求量\n组成：常量（与实例无关）+可变部分（与实例有关）\n通常忽略和 实例特征无关的空间需求量，一般复合变量是与实例特征相关性最高）\n####### 3.对递归算法空间复杂度的分析\n计算递归算法的空间复杂度通常涉及到分析递归调用所使用的内存。空间复杂度表示算法在执行过程中所需的额外内存空间，除了输入数据本身。\n在递归调用中，主要的内存使用包括递归调用栈和递归函数自身所使用的内存。\n步骤如下：\n分析递归深度： 首先，确定递归算法的递归深度，也就是递归函数 被连续调用的次数。这将直接影响到递归调用栈的深度。\n分析递归函数的内存消耗： 对于每次递归调用，分析递归函数自身所使用的内存。这包括函数局部变量、参数、返回地址等。递归函数的内存消耗通常与函数的代码和数据结构有关。\n计算总体空间复杂度： 将递归深度乘以每次递归调用的内存消耗，即可得到总体的空间复杂度。\n####### 4.对非递归算法空间复杂度的分析\n分析与实例特征有关的数据结构的大小。\n3.渐进分析法 忽略常数因子和低阶项，关注算法复杂度的增长量级，这种分析叫做渐进分析。\n渐进符号分为三种：\n渐进等于Θ（渐进紧界），渐进大于Ο（渐进上界），渐进小于Ω（渐进下界）。\n以渐进大于为例讲解\n当f(n)=O(g(n))，当且仅当存在正的常数c和n0，使得对于所有的n\u0026gt;=n0，有f(n)\u0026lt;=c*g(n)。\n也就是对于上限比较松散。\neg：f(n)=n^2+6n+10\n那么它的渐进上界Ο可以是n^2, n^3, n^4······\n渐进下界Ω可以是n^2,n,1\n渐进等于Θ可以是n^2.\n4.NP问题，NPhard问题，P问题等 略。\n附： 1.内容参考\n佟鑫宇老师 天津大学智能与计算学部 2023秋 算法设计与分析 ppt\n考试考察重点：时间复杂度，渐近分析法。\n全文同样包含个人的主观理解，如有错误，欢迎访问原文链接指正。\n","date":"2023-05-01T00:00:00Z","permalink":"https://sutdown.github.io/p/%E7%AE%97%E6%B3%95%E4%B8%93%E9%A2%98%E7%BB%AA%E8%AE%BA-%E7%AE%97%E6%B3%95%E5%88%86%E6%9E%90/","title":"算法专题：绪论+算法分析"},{"content":"一：贪心法概述 1）优化问题 优化问题的基本要素：问题的解，约束条件，目标函数。\n很多优化问题均属于NP-hard问题，对其问题的求解大多只能求近似解，贪心法则是求近似解的主要途径。\n注：\n优化问题之所以被认为是NP难问题，是因为在一般情况下，找到一个问题的最优解需要遍历所有可能的解空间，这个解空间的规模通常是指数级别的。因此，对于大规模的优化问题，穷举搜索所有可能的解是不可行的。\n虽然贪心算法是一种高效的求解优化问题的方法，但并不保证能够得到全局最优解。对于某些优化问题，可能需要使用其他算法或方法来求解，例如动态规划、回溯、分支界限等。这些算法通常具有更高的时间复杂度，但能够找到问题的最优解。因此，优化问题通常被认为是NP难问题。\n2）贪心法是什么 贪心法是一种启发式算法，通过局部最优解得到全局最优解。求得最优解需要满足贪心选择性和最优子结构两个特征。\n**贪心选择性质（Greedy Choice Property）**是指在贪心算法中，每一步的选择都是当前看起来最优的选择。也就是说，在每一步中，做出的选择都是局部最优的，希望通过一系列局部最优选择达到全局最优解。\n最优子结构性质（Optimal Substructure）是指问题的最优解包含了子问题的最优解。换句话说，问题的最优解可以通过一系列子问题的最优解逐步构建而得到。这种性质使得我们可以将原问题分解为若干个相互独立的子问题，并通过求解子问题的最优解来得到原问题的最优解。\n贪心算法通过每一步选择当前看起来最优的解决方案，逐步构建全局最优解。它不需要对整个问题空间进行搜索，而是基于局部最优选择策略，通过贪心选择性质和最优子结构性质，得到最终的解。贪心算法的简单性和高效性使得它成为解决某些优化问题的有效方法。\n优点：算法简单，时间和空间复杂性低。\n3）贪心法算法思想 贪心法适用于组合优化问题。 求解过程是多步判断过程，最终的判断序列对应于问题的最优解。 依据某种短视的贪心选择性质判断，性质好坏决定算法的成败。 贪心法必须进行正确性证明（精确解）。 证明贪心法不正确的技巧：举反例。 二：贪心算法的应用实例 1.货船装载问题 若有n个集装箱，集装箱大小一样，第i个集装箱大小为Wi，设船的载重量为c，设计一个装船的方法使得装入的集装箱数目做最多。\n第一直觉：排个序，每次选择最小的。\n这个也可以理解为一个优化问题。\n问题的解为每个集装箱的选择和不选择。约束条件为总重量小于载重量。目标函数为装的集装箱数目最多。\n所以尝试用贪心法求解。寻找贪心策略和是否满足最优子结构特征。\n贪心策略为：轻者优先。且满足最优子结构。·\n是最优解的一种， 不是唯一的最优解。\n2.0/1背包问题 注：NP-hard问题，若有多项式复杂度的算法产生的解可能是近似解。\n背包的容量为c。存在n个物品，每个物品的重量和价值分别为Wi和Pi（1\u0026lt;=i\u0026lt;=n），试给出一种装入物品的方法，使获得的总效益值最大。\n第一眼：每个背包有装和不装两种状态，指数级的时间复杂度，为nph问题。如果使用贪心策略每次选择价值最大的或者重量最小的，都难以满足最优子结构的特点。\n优化问题。问题的解，约束条件，目标函数都可以很容易看出来，不赘述了。\n贪心策略：1）优先选择价值高；2）优先选择体积小；3）优先选择比值高。\n这三种贪心策略都无法百分百得到最优解，不过放一下3）的伪代码。\n1 2 3 4 5 6 7 8 9 10 for i\u0026lt;-1 to n do di=pi/wi #calculate density D\u0026lt;-D U {di} D\u0026lt;-sort(D,P,W) #将物品按密度从大到小排序 for i\u0026lt;-1 to n do if C\u0026gt;=Wi xi=1 #put else xi=0 return X; ## 时间复杂度为O(nlgn) 这里贪心法不能保证一定能得到01背包的最优解，因此产生了一个贪心法和最优解误差的百分比的公式：（|优化值-贪心解值|）/优化值*100%。\n接着产生了\n0/1背包问题：k-优化算法 1.K优化算法是上述3）的算法策略的改进，将误差控制在1/（k+1）的范围内。\n2.具体内容\n对物品密度从大到小排序。\n先将一些物品放入背包，然后其余物品使用贪心法\n​ 预先放入的物品不超过k\n​ 对所有预装物品数不超过k的剩余物品子集执行贪心过程，并从中找到有最大效益值的解作为k优化问题的解。\n注：\n也就是先选择满足大小小于k的子集，再采用密度贪心攻略选择剩下的。最后在先选择的所有子集对应的不同解法中，找到最优解。\n需要测试的子集数目为排列组合问题，从n个中选择k个。\n每个子集贪心法的时间为O(n)，k\u0026gt;0时，总的时间开销为O(n^(k+1))。\n3.连续背包问题 这个与0/1背包的不同在于这里的物品能够拆开，也就是说如果按照上面的密度排序，是能够得到最优解的。\n这是一个简单的思想，但没有学习这门课之前并不能意识到这是贪心法。贪心法确认的关键在于局部最优和全局最优是否等同。\n4.拓扑排序问题 在一个有向无环图内，G=（V，E），找到一个顶点的线性序列，该序列满足以下两个条件：\n1.每个顶点有且只出现一次，如果边包含（u，v），则结点u在拓扑序列中处于v的2前面。\n这是数据结构与算法课程中的例题，不过多讲解，现在直接观察该题的解题思路：\n贪心策略：从当前尚不在拓扑排序序列的顶点中选择一项顶点v，其所有的前驱节点u都在已产生的拓扑序列中，并将v加入到拓扑序列中。（简单来说，将入度为0的顶点加到拓扑排序的顶点中，然后在原图去掉）。\n代码（自然语言）\n1 2 3 4 5 6 7 8 9 10 11 12 /* 计算每个顶点的入度 从第一个开始遍历，将入读为0的首个结点入栈 while（栈不空） { 任取一入度为0的顶点放入拓扑序列中； 将与其相邻的顶点的入读减1； 如有新的入度为0的结点出现，将其放入栈中； } 如有剩余的顶点未被删除，说明该图有环路 */ Kahn算法的时间复杂度为O(n+e); ​\n5.分类二分覆盖问题 在二分图中寻找最小覆盖的问题，等价于集合覆盖问题，是NP难问题。\n二分图是一个无向图，它的n个顶点分为两个不交叉集合A和B，且任一条边的两个顶点不在同一个集合。\nA的一个子集完全覆盖B，当且仅当B中每一个顶点至少与A的子集中的一个顶点相连。\n如果使用贪心问题，贪心策略为每次寻找A中和B相连顶点最多的顶点，那么能够得到一个近似解，但是不一定得到最优解。\n伪代码如下\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 for all i属于A,New[i]=degree[i];//A是A集合中的顶点 for all i属于B,covered[i]=false; A1=空集; while(for some i属于A,New[i]\u0026gt;0){ 选取v为A和A1中New[i]值最大的顶点 A1=A1+{v}; for 所有被v覆盖的B中的结点j{ covered[j]=true; for 所有覆盖结点j中A的顶点k New[k]=New[k]-1 } } if 有B中顶点没被覆盖 return false else 找到一个覆盖 #时间复杂度为：O(A^2+n^2)或者O(A^2+n+e)； n^2为邻接矩阵，n+e为邻接表。 6.最短路径问题 在一个加权有向图G=(V,E)中，它的每一条边都有一个非负的权重，每条路径的长度就是该条边上所有的权重之和。\n从结点u到结点v之间的最短路径就是u到v中权重最小的路径。\n直接搜索的复杂度为n的阶乘，复杂度较高，属于NPhard问题。\n贪心策略：\ndijkstra算法：确实初始节点为最优结点，然后更新最优结点到能到达结点的路径，然后选择初始节点到剩余结点的最优结点这样遍历。\n也就是选择每次到达点路径最小且未标记的点作为一下个结点，这样子选的局部方法就能得到全局的最优解。\n伪代码如下：\n1 2 3 4 5 6 7 8 9 10 11 12 d[s]\u0026lt;-0; #存放初始节点到每个结点的举例 for each v属于V - {s} do d[v]\u0026lt;-∞ S\u0026lt;-空集 #存放路径 Q\u0026lt;-V #Q是维护V-S的优先队列 while Q不等于空集 do u\u0026lt;- extrect-min{Q} S\u0026lt;-S U {u} for each v属于adj[u] do if d[v]\u0026gt;d[u]+w(u,v) then d[v]=d[u]+w(u,v) ## 时间复杂度为O(n^2); 7.最小生成树 给定无向连通带权图G=（V，E），E中每条边的权重w（u，v）。如果G的子图是一颗包含G的所有顶点的树，则称T为图G的生成树。生成树上各边权的总和称为该生成树的耗费，耗费最小的为最小生成树。\nPrim算法：贪心策略：每次在已选边中选择和其它顶点连接最小权重的边的顶点\nPrim伪代码\n1 2 3 4 5 6 7 8 9 10 Q \u0026lt;- V key[v] \u0026lt;- ∞ for all v属于V key[s] \u0026lt;- 0 for some arbitrary s属于V while Q不为空寂 do u \u0026lt;- extract-min(Q) for each v属于adj[u] do if v属于Q and w(u,v)\u0026lt;key[v] then key[v]\u0026lt;- w(u,v) pai[v]\u0026lt;-u // at last,{(v,pai[v])}forms the MST Kruskal算法：贪心策略：每次在整个图中选择权重最小的边，不能构成环。\nkruskal伪代码\n1 2 3 4 5 6 7 8 9 10 //在一个具有n个顶点的网络中找到一颗最小生成树 令T为所选边的集合，初始化T为空 令E为网络中边的集合 while（E不为空\u0026amp;\u0026amp;T不等于n-1）{ 令(u,v)为E中代价最小的边 E=E-{(u,v)} //从E中删除边 if(u,v)加入T中不会产生环路 将(u,v)加入T } if(T==n-1)T是最小耗费生成树 else 网络不是互连的，不能找到生成树 8.哈夫曼编码问题 改变字符二进制的编码方式，进行缩短。\n贪心选择性质：合并出现频率最低的两个字符。\n算法以|C|个叶节点开始，执行|C|-1次合并运算后产生所需最终要求的树T。\n基本流程：\n初始：根据n个字符的频率{w1，w2，···，wn}构成n个根节点的集合F={T1,T2,···，Tn}，其中每颗二叉树Ti中只有一个带权为Wi的根节点，其左右子树均为空（叶子节点）。 在F中选取两颗根结点的权值最小的树作为左右子树构造一棵新的二叉树，且置新的二叉树的根节点的权值为其左右子树结点的根节点的权值之和。 在F中删除这两棵树，同时将新的二叉树加入F当中。 重复第二步和第三步，直到F中只含一棵树为止，称这棵树为最优二叉树。 附： 1.内容参考\n佟鑫宇老师 天津大学智能与计算学部 2023秋 算法设计与分析 ppt\n考试考察重点：优化问题，贪心算法的设计要素，0/1背包问题的连续问题，最短路径问题，最小生成树，哈夫曼编码问题。\n全文同样包含个人的主观理解，如有错误，欢迎访问原文链接指正。\n","date":"2023-04-23T00:00:00Z","permalink":"https://sutdown.github.io/p/%E7%AE%97%E6%B3%95%E4%B8%93%E9%A2%98%E8%B4%AA%E5%BF%83%E6%B3%95/","title":"算法专题：贪心法"},{"content":"注：来源Acwing算法基础课。\n868. 筛质数 - AcWing题库 给定一个正整数 n，请你求出 1∼n中质数的个数。\n首先，暴力做法，对每一个数判断是不是质数，然后res记录个数，由于每一次判断是不是质数需要一次for循环，因此时间复杂度为O（n^2），意料中的超时了。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #include\u0026lt;iostream\u0026gt; using namespace std; bool isPrime(int x) { if (x == 1)return false; for (int i = 2; i \u0026lt;= x / i; i++) { if (x % i == 0)return false; } return true; } int main() { int n; cin \u0026gt;\u0026gt; n; int res=0; for(int i=2;i\u0026lt;=n;i++){ if (isPrime(i))res++; } cout \u0026lt;\u0026lt; res \u0026lt;\u0026lt; endl; return 0; } 所以，对于这道题，我们换一种思路，筛选出来质数并不一定要找出所有质数，将合数排除也可以得到质数。\n所以，从头至尾，记录下所有的质数的个数，同时将所有质数倍数的合数标记为1不计入。由于第i个如果是合数，那么一定由比其小的质数的因数，该数会被标记成1，所以一定不会计入个数，而若第i个是质数，那么s[i]仍然是0，会计入个数，因此遍历所有就能得到结果。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #include\u0026lt;iostream\u0026gt; using namespace std; const int N = 1e6 + 10; int s[N]; int n, cnt; int main() { cin \u0026gt;\u0026gt; n; //1.1既不是质数也不是合数，因此从2开始判断。 //2.合数都可以由一系列质数相乘得到 //3.st中默认为0，因此如果是质数我们将其标记为1； for (int i = 2; i \u0026lt;= n; i++) { if (!s[i])cnt++; for (int j = 2; j \u0026lt;= n / i; j++) { s[j * i] = 1; } } cout \u0026lt;\u0026lt; cnt \u0026lt;\u0026lt; endl; return 0; } 869. 试除法求约数 - AcWing题库 给定 n个正整数 ai，对于每个整数 ai，请你按照从小到大的顺序输出它的所有约数。\n求约数，先给大家看看我的垃圾代码。就是简单的是约数就加入数组中，然后排序。\n1）2乘以1e9这么大的数是怎么用数组申请这么大空间的，一个根本不能，一个没有必要！！！\n答：容器啊容器啊，你忘了vector吗，vector相比普通数组可以动态的先申请部分空间，再如果不够自己会重新申请，这比普通数组不知道好了多少。\n2）vector输出结果时会有重复元素如何解决？ 如果对加入的数进行判断在数组中是否有重复是一件很浪费时间的事情，所以不建议。这时候突然发现了set容器，它有两个特点完美的契合了这道题。一是可以自动去除重复元素；二是可以自动排序，sort都省了！！快乐\n附上两个链接，对vector和set的简单介绍\nVector 简介和优缺点_vector的缺点-CSDN博客\n[深入理解C++ STL库中的set容器-CSDN博客](https://blog.csdn.net/YoyoHuzeyou/article/details/132184083#:~:text=4.set容器的特性 1 唯一性：set容器中的元素是唯一的，不会出现重复的元素。 2,有序性：set容器中的元素按照一定的顺序进行排序，默认是升序。 可以通过自定义比较函数实现自定义排序。 3 动态增删：set容器支持动态地增加和删除元素，同时保持有序性。)\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 #include\u0026lt;iostream\u0026gt; #include\u0026lt;algorithm\u0026gt; using namespace std; const int N = 2 * 1e9 + 10; int cnt = 0; int a[N]; int main() { int n; cin \u0026gt;\u0026gt; n; while (n--) { int x; cin \u0026gt;\u0026gt; x; //1.如何找约数。 //可以从小到大直接找就行 //2.如何从小到大输出。 //排序？会有点慢，不知道能不能过。 for (int i = 1; i \u0026lt;= x / i; i++) if (x % i == 0) { a[cnt++] = i; a[cnt++] = n / i; } sort(a, a + cnt); for (int i = 0; i \u0026lt;= cnt; i++) { cout \u0026lt;\u0026lt; a[i] \u0026lt;\u0026lt; \u0026#39; \u0026#39; ; } cout \u0026lt;\u0026lt; endl; } for (int i = 0; i \u0026lt;= cnt; i++) { cout \u0026lt;\u0026lt; a[i] \u0026lt;\u0026lt; \u0026#39; \u0026#39; ; } return 0; } 来看看接下来的修改版代码，成功AC；\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 #include\u0026lt;iostream\u0026gt; #include\u0026lt;set\u0026gt; using namespace std; int main() { int n; cin \u0026gt;\u0026gt; n; while (n--) { int x; cin \u0026gt;\u0026gt; x; set\u0026lt;int\u0026gt; a; for (int i = 1; i \u0026lt;= x / i; i++) if (x % i == 0) { a.insert(i); a.insert(x / i); } for (auto x : a) cout \u0026lt;\u0026lt; x \u0026lt;\u0026lt; \u0026#39; \u0026#39;; cout \u0026lt;\u0026lt; endl; } return 0; } 870. 约数个数 - AcWing题库 给定 n个正整数 ai，请你输出这些数的乘积的约数个数，答案对 1e9+7 取模。\n第一思路，直接相乘积再求约数个数。不过相乘可能超过int类型，求约数个数也力不从心，所以应该还是逐个分析。\n第二反应，有两个直觉思路，都是用set，自动去重和排序真好用\n1）将所有的约数保存到set容器，再将其两两相乘。\n2）用几个数的乘积去处于所有的约数。\n这两种都是属于有点麻烦的，重新看如何判断约数个数这个问题。约数可以写成几个质数的底数的指数次方 $$ S=a^n*b^m...(个数为（n+1）*（m+1）...) $$ 相乘的形式，每个底数都是不同的，然后对于每个a的n次方，a可以取0-n有n+1种方式，同理后面为m+1，它们各部分相乘的积也一定是S的因数，所以个数就是指数+1的乘积了。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 #include\u0026lt;iostream\u0026gt; #include\u0026lt;unordered_map\u0026gt; using namespace std; const int mod=1e9+7; int main() { int n; cin \u0026gt;\u0026gt; n; long res=1;//用long，防止数据过大越界 unordered_map\u0026lt;int, int\u0026gt; a; while (n--) { int x; cin \u0026gt;\u0026gt; x; for (int i = 2; i \u0026lt;= x / i; i++) while (x % i == 0) {//指数的次数 a[i]++; x = x / i;//对x因数乘积的判断 } if (x \u0026gt; 1)a[x]++; } for (auto i : a)res = res * (1 + i.second) % mod; cout \u0026lt;\u0026lt; res \u0026lt;\u0026lt; endl; return 0; } 874. 筛法求欧拉函数 - AcWing题库 给定一个正整数n，求1∼n中每个数的欧拉函数之和。\n惯例，先看看TLE（Time Limit Exceeded）的算法。注解放在算法中了。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 #include\u0026lt;iostream\u0026gt; using namespace std; const int N = 110; //判断是否是质数，指数的欧拉函数就是其本身-1； bool prime(int n) { for (int i = 2; i \u0026lt;= n / 2; i++) { if (n % i == 0)return false; } return true; } //欧拉函数的判断方法，按照欧拉函数的定义写出来的代码 //链接见：https://zhuanlan.zhihu.com/p/151756874 int oula(int n) { if (n == 1)return 1; if (prime(n))return n - 1; int p[N], cnt = 0, res = n; for (int i = 2; i \u0026lt;= n / i; i++) { if (n % i == 0) { p[cnt++] = i; } while (n % i == 0) { n = n / i; } } if (n \u0026gt; 1)p[cnt++] = n; for (int i = 0; i \u0026lt; cnt; i++) { //要先除再乘，避免溢出 res = res / p[i] * ( p[i] - 1 ); } return res; } int main() { int n; cin \u0026gt;\u0026gt; n; int res = 0; for (int i = 1; i \u0026lt;= n; i++) res += oula(i); cout \u0026lt;\u0026lt; res \u0026lt;\u0026lt; endl; return 0; } orz，看了大佬的题解，这是要多么深厚的数学基础，放个链接AcWing 874. 筛法求欧拉函数 - AcWing\n捋一捋思路，本质上是数学题。\n875. 快速幂 - AcWing题库 给定n组 ai，bi，pi，对于每组数据，求出ai*bimodpi的值。\n第一反应，暴力做法，a的b次幂要怎么求，要怎么取模，如果想直接把a的b次幂表示出来，所花的时间必然会过长导致超时，所以猜测这题的考虑点在如何在求a的b次幂时降低它的时间复杂度。也就暂时不考虑取模。\n求a的b次幂时，本来是要将a乘b次，那么如何降低乘的次数呢，有一个性质，所有的十进制数是可以由二进制数表示的，那么a需要乘的字数自然就降低了。二进制中每位要么是1要么是0，不仅降低了b次的次数，每次的运算量也不大，完美解决。\n这个和01背包也有一点点相似，都是0和1的形式。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 #include\u0026lt;iostream\u0026gt; using namespace std; long long Quickmi(long long a, int b, int p) { long long res = 1; while (b) { if (b \u0026amp; 1) { //这个a指的是b中最后一位为1时a的值 //a第一次就是a，往后就是a的自身的平方 res = res * a % p; } b \u0026gt;\u0026gt;= 1; a = a * a % p; } return res; } int main() { int n; cin \u0026gt;\u0026gt; n; while (n--) { cin.tie(0); ios::sync_with_stdio(false); int a, b, p; cin \u0026gt;\u0026gt; a \u0026gt;\u0026gt; b \u0026gt;\u0026gt; p; cout \u0026lt;\u0026lt; Quickmi(a, b, p) \u0026lt;\u0026lt; endl; } return 0; } 877. 扩展欧几里得算法 - AcWing题库 给定n对正整数 ai,bi，对于每对数，求出一组 xi,yi，使其满足 ai×xi+bi×yi=gcd(ai,bi)。\n","date":"2023-04-10T00:00:00Z","permalink":"https://sutdown.github.io/p/%E7%AE%97%E6%B3%95%E4%B8%93%E9%A2%98%E7%AE%97%E6%B3%95%E5%92%8C%E6%95%B0%E5%AD%A6%E7%9F%A5%E8%AF%86/","title":"算法专题：算法和数学知识"},{"content":"[TOC]\n前言： 解决问题的时候，可以列出所有的候选解，然后依次检查每一个，检查完后就可以得到所需要的解。\n对候选解进行系统检查的常用两种方法是回溯和分支定界。\n推荐文章：回溯法算法复习伪码—自用-CSDN博客\n回溯： 1.定义： 回溯是一种系统的搜索问题解答的方法。\n首先定义一个解空间，下一步组织解空间以便于它能容易的被搜索（树或者图），然后再按照深度优先的方法进行搜索。\n从开始结点开始，深搜，如果某节点无法移动到 一个新节点，该结点就变为死节点，当找到答案或者回溯尽了所有的活结点时，搜索结束。\n回溯通俗来讲指的是一条路走不通了再往回走的意思，应为为traceback。\n注：\n优化问题和存在性问题 排列树和子集树 2.解题思想 在对解空间进行搜索时，盲目搜索的时间和空间是很大的。因此产生两种回溯算法：\n1）使用限界函数阻止不可能获得解答的结点的扩张。\n2）通过不移动到不可能包含比当前最优解还要好的解的右子树。\n3）\n任何搜索算法都可以用建立在解空间上的状态搜索树加以描述。\n状态搜索树是我们尝试选择元组的各个分量时产生的树结构。\n搜索算法并非事先将状态空间树存在计算机内再进行遍历，而是通过展开状态空间树来找所求的解。\n展开的过程中使用启发式的限界方法（减去状态空间树上的某些分支）使搜索算法只展开状态空间树的一部分，从而降低搜索算法的时间和空间复杂度。\n4）有关状态空间树：\n每个搜索算法都在系统的展开状态空间树。\n状态空间树的几种结点的描述：\n活结点：已展开了部分子节点，但所有子节点尚未全部展开的结点。\n死结点：被限界或已展开了所有子节点的结点。\nE-结点：当前正在展开子节点的活结点。\n5）状态空间树的展开方法：\n深度优先展开方法。\n回溯法（加上限界的深度优先展开）。\n分支限界法：结点变成E节点后，展开所有子节点，自己变成死结点。同时需要一个结构维持已展开但是还没成为E结点的那些结点。\n3.应用： *0/1背包问题 这个背包问题比货箱装船的第一艘船的承载量的最大思想严谨多了。\n这里是采用一种对于未来的预测，也就是保存bestcw，同时对未来将要遍历的节点做出判断，假设背包中的物品可划分时会造成的最好最好结果（也就是先将所有密度最大的装入），如果这个结果比bestcw要小，那完全没有继续遍历该子树的必要。\n因此可以得出1）需要将背包中的物品按照密度从大到小排序，便于计算。2）每个节点最重要的要保存的数值是当前获益cp（当前最好容量），未来可能的最大容量bound（剩余容量）从而画出递归树进行判断。\n伪代码如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 按照密度对物品排序 bestp \u0026lt;- -∞ 设x=(x(1),x(1),x(3)......x(k))为当前E节点(bound\u0026gt;bestp成立) 展开左子节点 if cw+w(k) \u0026lt;= c 装入物品k cw \u0026lt;- cw+w(k),cp \u0026lt;- cp+p(k) x(k)=1 else 展开右子节点 if(bound\u0026lt;=bestp)停止产生右子树 else x(k) \u0026lt;- 0,并令(x(1),x(1),x(3)......x(k))为E节点 //类似货船装箱问题 C++代码：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 #include\u0026lt;bits/stdc++.h\u0026gt; using namespace std; struct Item { int w;//重量 int v;//价值 double density;//密度 int index; }; //仿函数，按照密度从大到小排序 bool compare(Item a, Item b) { return a.density \u0026gt; b.density; } vector\u0026lt;int\u0026gt; bestItems; // 用于保存最佳物品组合的向量 int knapsack(vector\u0026lt;Item\u0026gt;\u0026amp; items, int c) { sort(items.begin(), items.end(), compare); int bestv = 0; vector\u0026lt;int\u0026gt; currentItems(items.size(), 0); // 用于保存当前物品组合的向量 function\u0026lt;void(int, int, int)\u0026gt; backtrack = [\u0026amp;](int i, int cw, int cv) { //递归出口 if (i \u0026gt;= (int)items.size()) { if (cv \u0026gt; bestv) { bestv = cv; bestItems = currentItems; // 当找到更好的解决方案时，更新最佳物品组合 } return; } if (cw + items[i].w \u0026lt;= c) { currentItems[i] = items[i].index; // 添加当前物品到当前物品组合 backtrack(i + 1, cw + items[i].w, cv + items[i].v); } if (cv + (c - cw) * items[i].density \u0026gt; bestv) { currentItems.erase(remove(currentItems.begin(), currentItems.end(), items[i].index), currentItems.end()); backtrack(i + 1, cw, cv); } }; backtrack(0, 0, 0); return bestv; } int main() { vector\u0026lt;Item\u0026gt; items; int c = 165; cout \u0026lt;\u0026lt; knapsack(items, c) \u0026lt;\u0026lt; endl; // 打印最佳物品组合 for (int i = 0; i \u0026lt; (int)items.size(); i++)\t{ if (find(bestItems.begin(), bestItems.end(), i) != bestItems.end()) cout \u0026lt;\u0026lt; 1; else cout \u0026lt;\u0026lt; 0; } cout \u0026lt;\u0026lt; endl; return 0; } *货箱装船问题 有n个货箱，w数组中有各个货箱的重量，有两艘船，两艘船的承载量用c1，c2表示。请问是否有一种可以将n个货箱全部装入的方案，若有请找出该方案。\n分析：由于每个货箱有装和不装两种状态，如果采用此种方法则复杂度到了指数级别，所以只能采取其它方案。\n思路一：由于这个问题和01背包很类似，可以先采用动态规划尽可能的装满第一艘船，再判断剩下是否能全部装进第二艘船即可。元组法时间复杂度为O(min{c1,2^n})；\n思路二：当然，这个专题探讨的是回溯，我们来看看回溯思想的解法。\n第一种回溯算法 从上到下遍历解空间，同时阻止现有容量超过了节点的扩张。\n代码如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 template\u0026lt;class T\u0026gt; class Loading { friend MaxLoading(T [], T, int); private: void maxLoading(int i); int n;//货箱数目 T *w;//货箱重量数组 T c, cw, bestcw; //第一艘船的容量，当前容量，目前装载最优的容量。 }; template\u0026lt;class T\u0026gt; void Loading\u0026lt;T\u0026gt;::maxLoading(int i) { //在第i层时 if (i \u0026gt; n) { //处于叶子节点 //bestcw是前面的叶子节点得到的最大值 if (cw \u0026gt; bestcw)bestcw = cw; return; } //检查子树 //当加上这个节点时不会超过最大，那加不加都可以 /****显示算法思想的步骤****/ if (cw + w[i] \u0026lt;= c) { cw += w[i]; //加当前节点时 maxLoading(i + 1); cw -= w[i]; //不加当前节点时 maxLoading(i + 1); }//（感慨，递归真好用） } template\u0026lt;class T\u0026gt; T MaxLoading(T w[], T c, int n) { Loading\u0026lt;T\u0026gt; X; X.w = w, X.c = c, X.n = n, X.cw = 0, X.bestcw = 0; //初始化 X.maxLoading(1); return X.bestcw; } 第二种回溯算法 对第一种进行优化，加上不移动到不包含比当前最优解还好的解的右子树。\n由于是广度搜索，该优化主要发生在i层和i-1层，在i层找到第一个bestcw时，后面的如果解比它差就不需要再进行下去；\n同时设置r为剩余货箱容量，如果当前加上剩余仍然比不上最好，那也没必要遍历该子树，关键部分代码修改如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 template\u0026lt;class T\u0026gt; void Loading\u0026lt;T\u0026gt;::maxLoading(int i) { //在第i层时 if (i \u0026gt; n) { //处于叶子节点 //bestcw是前面的叶子节点得到的最大值 if (cw \u0026gt; bestcw)bestcw = cw; return; } //检查子树 r -= w[i]; //当加上这个节点时不会超过最大，那加不加都可以 if (cw + w[i] \u0026lt;= c) { cw += w[i]; //加当前节点时 maxLoading(i + 1); cw -= w[i]; //不加当前节点时 }//（感慨，递归真好用） //要判断不加当前节点那你以后的最好值能不能大于当前最优解 if (cw + r \u0026gt; bestcw)maxLoading(i + 1); r += w[i]; } 当然还可以再进一步优化添加数组保存可行时的路径，代码就不更新了。\n*子集和数问题 已知n+1个正数，wi（1\u0026lt;=i\u0026lt;=n）和M，要求找出w的所有子集，使子集的元素之和等于M。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 令S=w(1)X(1)+…+w(k-1)X(k-1) r=w(k)+…+w(n),假定S+r≥M(不满足该条件的节点已被限界掉) 如果S=M, 则找到了一个和数为M的子集. 回溯找其它解. 展开左子节点： 如果S+W(k)+W(k+1)\u0026gt;M则停止展开左子节点, r←r-w(k), 并展开右子节点; 否则,X(k)←1 , S←S+w(k), r←r-w(k), k←k+1 (令(x(1),…,x(k))为E节点); 展开右子节点： 如果S+r\u0026lt;M或S+w(k+1)\u0026gt;M则停止展开右子节点并回溯; 否则,X(k)←0, r←r-w(k),k←k+1 (令(x(1),…,x(k))为E节点); 回溯：k ←k-1 (回到父节点). *n皇后问题 n皇后问题的要求在于要求每行每列都要有一个皇后，并且这些皇后不能在同一直线或者同一斜线上。\n我们尝试画一下它的状态空间树，\n要时刻记住，回溯的关键在于减少递归树的分支（无论是阻止扩容还是不移动到无法超过最优解，这些只是主要的思想手段），因此在化状态空间树时，有以下几种策略可以减少递归树的分支：\n~~有n行n列，因此具有对称性，每行不需要遍历所有格子，选取其中的一半即可。\n~~违反题目要求的，比如不在同行同列，不能往下展开。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 //递归回溯 void Queen::Backtrack(int t) { if(t\u0026gt;n) sum++; else for(int i=1;i\u0026lt;=n;i++){ x[t]=i;//第t行放在第i列 if(place(t))Backtrack(t+1); } } bool Queen::place(int k) { for(int j=1;j\u0026lt;k;j++) if(abs(k-j)==abs(x[j]-x[k])||x[j]==x[k])//不能在同一列同斜线 return false; return true; } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 //迭代回溯 void Queen::Backtrack() { x[1]=0; int k=1; while(k\u0026gt;0){ x[k]+=1;//第k行的放到下一列。 while(x[k]\u0026lt;=n\u0026amp;\u0026amp;!place(k))x[k]+=1;//x[k]不能放置，则放到下一列，直到可以放置 if(x[k]\u0026lt;=n)//放在n列范围内 if(k==n)sum++; else k++,x[k]=0; else k--;//第k行无法放置，回溯 } } *旅行商问题 给定一个n 节点的网络, 称一条包含网络中n 个节点的环路为一条周游路线.\n旅行商问题要求找出一条最小成本的周游路线.\n如果先画出它的状态分布树的话，这个树是很奇怪的，如果是4个节点，原本的六条路径在树中被重复了很多次。这个树也是被称为置换树。\n限界条件：\n限界(1) ：剪去不可行。i-1级节点x[i-1]和它的子节点x[i]之间有边相连(!=NoEdge)\n限界(2)：剪去非最优。设bestc为当前得到的最优解的成本值；路径x[1:i]的长度\u0026lt;bestc\n则搜索继续向下进行；否则施行限界。\n因此就可以减少状态分布数的时间和空间复杂度。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 template\u0026lt;class T\u0026gt; T AdjacencyWDigraph\u0026lt;T\u0026gt;::TSP(int V[]) { //用回溯算法解决旅行商问题 //返回最优旅游路径的耗费，最优路径存 入v[1:n] //初始化 x = new int [n + 1]; //x是排列 for (int i = 1; i \u0026lt;= n; i++) x[i] = i; bestc = NoEdge; bestx = v; //使用数组v来存储最优路径 cc = 0; //搜索x[2:n]的各种排列 tSP(2); delete [] x; return bestc; } void AdjacencyWDigraph\u0026lt;T\u0026gt;::tSP(int i) { //旅行商问题的回溯算法 if (i == n) //位于一个叶子的父节点,通过增加两条边来完成旅行 if (a[x[n - 1]][x[n]] = NoEdge \u0026amp;\u0026amp; a[x[n]][1] != NoEdge \u0026amp;\u0026amp; (cc + a[x[n - 1]][x[n]] + a{x[n]][1] \u0026lt; bestc || bestc == NoEdge)) { //找到更优的旅行路径 for (int j = 1; j \u0026lt;= n; j++) bestx[j] = x[j]; bestc = cc + a[x[n - 1]][x[n]] + a[x[n]][1]; } //尝试子树 for (int j = i; j \u0026lt;= n; j++) //能移动到子树x[j]吗？ if (a[x[i - 1]][x[j]] != NoEdge \u0026amp;\u0026amp; (cc + a[x[i - 1]][x[i]] \u0026lt; bestc || bestc == NoEdge)) { //能搜索该子树 Swap(x[i], x[j]); cc += a[x[i - 1]][x[i]]; tSP(i + 1); cc -= a[x[i - 1]][x[i]]; Swap(x[i], x[j]); } } *最大完备子集 完全图：图的每个顶点之间都有边 完全子图的尺寸是图U中顶点的数量 如果一个完全子图U不被包含在G的一个更大的完全子图中，称它是图G的一个集团。 最大集团是指具有最大尺寸的集团，也称最大团。 U是图G的最大团同时也是补图G0的最大独立集 最大团问题属于优化问题，也是子集树。\n解题：\n限界1：**剪去不可行。**检查根节点到状态空间树的某一状态节点的路径上对应的顶点子集是否构成一个完全子图。\n限界2：**剪去非最优。**如果剩余未考虑的顶点数加上团中顶点数不大于当前解的顶点数，则不再展开。\n（好普通的回溯。。。）\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 void AdjacencyGraph::maxClique(int i){ if(i\u0026gt;n){//找到一个完备子集 for(int j=1;j\u0026lt;=n;j++)bestx[j]=x[j]; bestn=cn; return; } int OK=1; for(int j=0;j\u0026lt;i;j++)//看是否与前面所有点都有边 if(x[j]\u0026amp;\u0026amp;a[i][j]==noEdge) Ok=0,break; if(OK){ x[i]=1; cn++; maxClique(i+1); x[i]=0; cn--; }//有边就加上去 if(cn+n-i\u0026gt;bestn){ x[i]=0;//没有边就要考虑是否满足限界2 maxClique(i+1); } } 4.回溯的一般方法（伪代码） 算法说明：（回溯=状态树+限定条件）\n每个解用数组 X(1:n)来表示.\n假定 X(1),…,X(k-1)的值已确定, T(X(1),…,X(k- 1))代表x(k)的所有可能的取值.\n限界函数B(X(1),…, X(k))判断那些X(k)的取值不能导致问题的解,从而停止展开该子节点\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 BACKTRACK: k\u0026lt;---1 while k\u0026gt;0 do { if (X(1),…,X(k-1))还有没展开的子节点 {取X(k)属于T(X(1),…,X(k-1)); if B(X(1),…,X(k))=false {if (X(1),…,X(k))是一答案节点 则输出 (X(1),…,X(k)); k\u0026lt;---k+1}//继续展开子节点// else k\u0026lt;---k-1 //回溯到父节点// } RBACKTRACK(k) //进入该子程序时解X(1: n)的前k-1个分量 X(1),…,X(k-1)已取值// for X(k)属于T(X(1),…,X(k-1)) and B(X(1),…,X(k))=false { if (X(1),…,X(k))是答案节点{ 输出(X(1),…,X(k)); RBACKTRACK(k＋1)} } 分支定界： 1.定义 分支定界同样是一种系统的搜索解空间的办法。\n二者关于搜索上思想的不同可利用数据结构中的广度优先搜索和深度优先搜索区分。回溯遍历结点的时候是到了叶子节点或者不能扩张的结点再往回走，分支定界会将该层所有的点放入队列中，然后依次将当层的点遍历完，这就是广度的来源。\n2.解题思想 为了实现我们分支定界的思想，常有以下两种思路： 1）先进先出（FIFO）。即从活结点表中取出节点的顺序和加入节点的顺序相同，因此活节点表的性质和队列相同。\n2）最大耗费或者最大收益法。在该模式中，每个节点都有一个对应的耗费或者收益，如果查找一个具有最少耗费的解，活结点表可以用最小堆建立，下一个E节点就是具有最小耗费的活结点；如果是搜索一个有最大收益的解，则可用最大堆建立思路同理。（第二种的表述有点抽象，后面再结合具体题目看看。）\nLC-检索\n如果活节点表中每个节点以c(x)为权值,每次从活节点表中取出最小权值节点作为E-节点,则算法能很快找到优化解.\n但在展开x 前不可能知道c(x)的值. 但是有可能从历史信息获得c(x)的某一下界ĉ(x).\n以c(x)的下界估值ĉ(x)做为活节点表中节点的权值, 每次取出有最小ĉ(x)的节点进行展开;\n要求设计的ĉ(x)满足: ĉ(x)=cost(x),当x为可行叶节点时;\nLC-分支-限界算法\n1 2 3 4 5 6 7 8 9 10 11 E=T; U←∞; 置活节点表为空; while(true) { for E 的每个子节点x If x 是叶节点 then U←min{U,cost(x)}; if ĉ(x)\u0026lt;U then {Add(x), parent(x)=E;} If 活节点表空 then 算法结束; delete(E); if ĉ(E)≥U 算法结束; } 基于优先级队列的分枝-限界算法\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 E=T; 置活节点表为空; while(true) { if E是可行叶节点 return; for E 的每个子节点x {Add(x); parent(x)=E} delete(E); } /*算法正确性证明： 当算法结束在第4行时 ĉ(E)=c(E); 对活节点表中每个节点x, 都有: ĉ(x)≥ĉ(E)=c(E) 但c(x)≥ĉ(x)≥c(E), 且c(x)为状态空间树上以x为根的子树的最小成本值. 又因为活节点表覆盖了状态空间树所有叶节点, 所以c(E)是最小成本值, 而E是最优解.*/ 3.应用： （只要理解思想就差不了多少，应用例子就不举这么多了。）\n*0/1背包问题 （子集树）\n限界条件：\n1）选择该物品后的容量不会超过背包容量。\n2）由于如果想确定每个节点收益时，不到叶子节点是无法确定最终收益，不能随便去掉子树。但如果换个角度思考，当我有最大收益时有的损失是最少的，（损失也就是指不选择某个物品时的损失），当损失确定时，如果到达某个节点是不选择的物品过多，损失过大，那么一定无法超过当前的最大收益。因此引入当前最小损失这个变量。\n*最小罚款额作业调度 n个作业,1台处理机,每个作业i对应一个三元组(pi,di,ti)。\npi－罚款额; di－截止期; ti －需要的处理机时间。\n求可行的作业子集J,使得罚款额Σpj最小,其中j为不在J中的作业.\n假设给定4个作业为（5，1，1），（10，3，2），（6，2，1），（3，1，1）。\n该题思路很灵活。\n先用子集树 ： 由于每个作业选择的先后会造成是否有罚款额两种结果，所以我们认为先选的是不产生罚款额的。\n分支限界重要的在于两点：\n1）状态空间树。——子集树\n2）限界条件。\n限界条件一般包括两个，1是题干中给出的，也就是在已经有选择的情况下我再选择这个作业如果不产生罚款额就可以是选择，否则只能是不选择。2是隐形的，由于不选择某项会产生罚款额，因此记录每个节点的罚款额，如果罚款额已经比当前最大罚款额还要大时，就直接出队列。\nConsequently，该题思路为：\n从第一个物品选择与否开始，宽度优先搜索，将符合限定条件1也就是选择可以不产生罚款额的节点和不选择时的节点加入优先队列中，优先队列中节点的排序是按照罚款额由小到大的，按照以上思路持续遍历，当得到一个当前罚款额时，可以经过比较去掉队列中的某些节点，直到所有节点被遍历完，结束。\n排列树： 这个排列并不是列出作业做的先后顺序，而是是否能完成做这个事情并且不产生罚款额，和子集树中限界条件1的理念是一致的。\n限界2就是当前情况的罚款额不能比当前最小罚款额还要大了。\n*旅行商问题 该类问题的实质是在一个带权完全无向图中，找一个权值最小的哈密顿回路。\n贴个链接回溯法和分支限界法 | Xinhecuican\u0026rsquo;s Blog，这篇文章讲的很好理解。\n好的我不写了，各位看链接，链接里面是关于规约矩阵的做法。然后利用规约矩阵得到一个状态空间树。\n规约矩阵： 行规约矩阵就是每行减去每行最小的数，列规约矩阵就是每列减去那一列最小的数。\n当矩阵的每行每列都有0时就得到了原矩阵的规约矩阵。原矩阵每行每列减去的值的绝对值加起来的和就是归约数。\n得到原矩阵的规约矩阵之后，比较出发点到其余各点的距离（由出发点到点的距离+原矩阵变成规约矩阵的规约数+去掉出发点行，到达点列，（到达点，出发点）位置的数变为无穷后的矩阵变成规约矩阵的规约数），得到到达某个点最小的距离。\n然后该点变成出发点，矩阵降一维重复以上的步骤。\n所有重复后从上到下也可以形成一个状态空间树。\n附： 1.内容参考\n佟鑫宇老师 天津大学智能与计算学部 2023秋 算法设计与分析 ppt\n考试考察重点：\n回溯法原理，货箱装船问题，0/1背包问题，旅行商问题\n分支限界法活结点扩充法，旅行商问题。\n全文同样包含个人的主观理解，如有错误，欢迎访问原文链接指正。\n","date":"2023-03-28T00:00:00Z","permalink":"https://sutdown.github.io/p/%E7%AE%97%E6%B3%95%E4%B8%93%E9%A2%98%E5%9B%9E%E6%BA%AF%E5%92%8C%E5%88%86%E6%94%AF%E5%AE%9A%E7%95%8C/","title":"算法专题：回溯和分支定界"},{"content":"[TOC]\n一：分治法概述 分治法思想 分治法的基本思想是将问题分成（divide）多个子问题，再递归（Conquer）的解决每个子问题，再将子问题的解合并（Combine）成原问题的解。\n1）这个思想是不是很熟悉，第一反应能不能想到归并排序和快速排序。后面我们会对两种排序和分治法的思想进行具体分析。\n2）分成多个子问题，有没有想到动态规划和贪心法，它们有什么区别呢？\n看一下这三者的主要思想：\n分治法：将问题分解成多个子问题，并允许不断分解，使规模越来越小，最终可用已知的方法求解足够小的问题。使用要求：（1） 问题能够按照某种方式分解成若干个==规模较小、相互独立且与原问题类型相同的子问题==。 （2） 子问题足够小时可以直接求解。 （3） 能够将子问题的解组合成原问题的解。1 贪心法：总是选择==当前最优解==，并不考虑整体最优。拥有最优子结构特性。当==一个问题的最优解包含其子问题的最优解==时，称此问题具有最优子结构性质。问题的最优子结构性质是该问题可用动态规划算法或贪心算法求解的关键特征。1 动态规划法：将较大问题分解为较小的同类子问题，这一点上它与分治法和贪心法类似。动态规划法解决子问题重叠现象，利用最优子结构，自底向上从子问题的最优解逐步构造出整个问题的最优解。适用动态规划法的要求：==具有最优子结构特性和重叠子问题。==与贪心算法比较：贪心算法局部的最优性依赖于其前面各部分是否最优；且不能保证最终解的最优性。动态规划当前决策的最优性取决于其后续决策系列的是否最优。动态规划方法可以保证问题求解是==全局最优==的。1 （来源：new bing。其中答案点击可达相应链接）\n分治法的基本步骤： 1 2 3 4 5 6 7 8 divide-and-conquer(P) { if ( |P| \u0026lt;= n0) adhoc(P); //解决小规模的问题 divide P into smaller subinstances P1,P2,...,Pk；//分解问题 for (i=1,i\u0026lt;=k,i++) yi=divide-and-conquer(Pi); //递归的解各子问题 return merge(y1,...,yk); //将各子问题的解合并为原问题的解 } 这个步骤为什么是这样子呢？\n这个来源于递归的函数的运行轨迹。\n先解决小规模问题指的是不用进行递归就能解决的极小规模问题，比如求一个数组的最大最小值，数组只有一个和只有两个的情况完全不需要递归就可以直接解决返回。\n然后将问题先分解，进行数次递归分解到最后然后从最后的递归依次往下执行接下来合并子问题的解的代码，回到上一层稍大问题的递归，再执行下面合并子问题的解的代码……直到回到原先要解决的大问题，答案搞定。\n这个过程其实是可以画出一个递归树，因此分治法很好的将n的时间复杂度降低到了logn。\n二：分治法的应用实例 求一组数的最大值和最小值 这个是指找出一组数中最大值和最小值的在数组中的位置。若数组大小为n。\n思路一：（非递归1）分别设立min，max为数组的第一个数，然后一起与后面所有数进行比较，需要进行2*（n-1）次比较。\n1 2 3 4 5 min←a[0];max←a[0];//伪代码 For i←1 to n-1 do if a[i]\u0026lt;min min←a[i] else if a[i]\u0026gt;max max←a[i] 思路二：（递归）将一组数分为（1）1-n/2和（2）n/2+1-n两部分，再将（1）和（2）分别分为两部分，依次往下分解，知道分成单独的一个或两个数，进行比较，设为最大和最小，再依次向上得到最大值。\n1 2 3 4 5 6 7 8 9 Max-min(A[0,n-1],max,min)//伪代码 if n=1 max←min←a[0],return;//解决小规模问题 if n=2 { if a[0]≥a[1] max←a[0],min←a[1] //解决小规模问题 else max←a[1],min←a[0]; } else m←n/2 Max-min(A[0,m-1],max1,min1),Max-min(A[m,n-1],max2,min2),//递归的解决各类子问题 max←max(max1,max2), min←min(min1,min2),//将子问题的解合并为原问题的解 return. 思路三：（非递归2）这个是对非递归的一点优化。\n假如是偶数的话，可以两个一组的进行比较，如果是奇数个，先将第一个当作最大最小值，再先将二三比较依次，将其中较大的与前面较大的比较，较小的同理，最终得到结果。次数降低为[3*n/2]-2次，有了一定的优化效果。\n大整数乘法 大整数乘法是指两个超过计算机整数类型范围的整数相乘。分治法是解决大整数乘法问题的有效方法。对于大整数乘法，我们可以将两个大整数分别拆分成高位和低位两部分，然后使用分治法递归地计算高位和低位的乘积，最后将结果合并起来即可。\n具体来说，我们可以将两个大整数X和Y分别拆分成A、B、C、D四个部分，然后按照以下公式计算它们的乘积： $$ XY = AC * 2^n + [(A-B)*(D-C) + AC + BD] * 2^(n/2) + BD $$ 其中n表示X和Y的位数，AC、BD表示A、C和B、D的乘积，(A-B)*(D-C)表示交叉项的乘积。这样，我们就将一个规模为n的大整数乘法问题转化为了4个规模为n/2的子问题，可以使用递归方法求解。同时进行复杂度分析T(n)=3T(n/2)+O(n);因此该算法的时间复杂度为O(n^log2(3))。在实际应用中，该算法比传统方法更加高效。\n注：这个为什么是3倍的T(n/2)我想了挺久，后来发现是对时间复杂度和主方法的理解模糊导致的。如果 $$ XY = AC * 2^n +(AD + BC) * 2^(n/2) + BD; $$ 的话，T(n)=4T(n/2)+O(n)；时间复杂度就成了O(n^2)，这个三倍的T（n/2)和四倍的主要在于XY中乘法操作的次数，因为在对两个大整数进行相乘时，也是先递归往下分成小的子问题，在四倍时，是分成了求AC，AD，BC，BD四个子问题，而在三倍时，分成了求AC，BD，以及（A-B）*（D-C）的三个子问题。这时候看分成几个子问题采用了基础操作计数法的思想。\n辨：基础操作计数法和渐进时间复杂度\n基本操作计数法和渐进时间复杂度都是用来判断算法时间复杂度，但思路和应用场景不同。\n基本操作计数法是通过计算算法中基本操作执行的次数来估算时间复杂度。基本操作是指算法中执行次数最多的操作，通常是最内层循环的循环体。这种方法适用于顺序结构和循环结构的算法。\n渐进时间复杂度是指当输入规模趋近于无穷大时，算法的时间复杂度的增长速度。通常使用大O记号来表示渐进时间复杂度，例如O(n)、O(nlogn)、O(n^2)等。其中，n表示输入规模，logn表示以2为底的对数。渐进时间复杂度关注算法的增长趋势，不关注具体的执行时间。在实际应用中，我们通常使用渐进时间复杂度来评估算法的效率和优劣。\n基本操作计数法适用于顺序结构和循环结构的算法，而渐进时间复杂度适用于各种类型的算法。在实际应用中，我们可以根据具体情况选择合适的方法来进行分析。\n因此，基本操作计数法评出的时间复杂度和渐进时间复杂度之间没有直接关系。但是，在实际应用中，我们可以通过基本操作计数法评估出一个算法在特定输入规模下的执行效率，并根据其增长趋势来推断其渐进时间复杂度。\n矩阵乘法也和这个大整数乘法问题的思路相似，都是通过更改小问题规模的个数来降低时间复杂度。\n归并排序和快速排序 这两个排序方法都属于分治法，我们来看看这两个经典排序方法是如何运行分治法的，它们的差别在哪里。分治法不断利用递归往下分组的方法建议想成一个递归树。\n归并排序思想：将待排序数组分成两个子数组，将每个子数组再分别分成两个数组，一直往下分直到不能分为止，由于每两个数组都是由一个数组分出来的，所以再利用辅助数组将两个数组重新合并，并且在合并的过程排好序（类似双指针算法）。也就是一个递归树，从最大的数组不断分到底层的子数组，子数组中只有一个数时，两两合并同时排序向上走，走到最上面时就已经排好序了。\n快速排序思想：先选择一个基准元素，将待排序数组划分为左右两个子数组，左边子数组中所有元素都小于基准元素，右边子数组中所有元素都大于基准元素。然后对左右子数组同样选择一个基准玄素，将子数组中小于基准元素放左边数组，大于放右边数组，依次往下，到达递归树的最底层时，顺序已经排好。\n这样子看其实归并排序更接近与我们1中给出的分治法的模板，先分组，在处理。而快速排序是先处理，再分组。\n来看看两种排序的代码：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 //归并排序 void merge_sort(int q[], int l, int r) { if (l \u0026gt;= r) return; int mid = l + r \u0026gt;\u0026gt; 1; merge_sort(q, l, mid), merge_sort(q, mid + 1, r); int k = 0, i = l, j = mid + 1; while (i \u0026lt;= mid \u0026amp;\u0026amp; j \u0026lt;= r) if (q[i] \u0026lt;= q[j]) tmp[k ++ ] = q[i ++ ]; else tmp[k ++ ] = q[j ++ ]; while (i \u0026lt;= mid) tmp[k ++ ] = q[i ++ ]; while (j \u0026lt;= r) tmp[k ++ ] = q[j ++ ]; for (i = l, j = 0; i \u0026lt;= r; i ++, j ++ ) q[i] = tmp[j]; } //快速排序 void quick_sort(int q[], int l, int r) { if (l \u0026gt;= r) return; int i = l - 1, j = r + 1, x = q[l + r \u0026gt;\u0026gt; 1]; while (i \u0026lt; j) { do i ++ ; while (q[i] \u0026lt; x); do j -- ; while (q[j] \u0026gt; x); if (i \u0026lt; j) swap(q[i], q[j]); } quick_sort(q, l, j); quick_sort(q, j + 1, r); } 作者：yxc 链接：https://www.acwing.com/activity/content/code/content/39790/ 来源：AcWing 这两种分治法排序的思想和代码已经实现了，然后就来分析一下时间复杂度。\n对于归并排序，正常分成k份时，T(n)=t(n/k)+t(n-n/k)+cn；分成两份时最好T（n）=2*T（n/2）+cn，渐进时间复杂度为O（nlogn）\n对于快速排序，由于这个时找基准元素，小的在左边，大的在右边，所以基准元素的选择和时间复杂度有着很大的关系，想象一下最差的T(n)=T(1)+T(n-1)+O(n)，我们每次选的基准元素是最小值或者最大值，那就需要O（n^2)的时间复杂度，是一件很糟糕的事情，最好的话当然是选择一个中间数作为基准点，T（n）=2*T（n/2）+O（n），时间复杂度最好就能到O（nlogn）了，这个也是和递归树的高度有关的。\n那为什么其实最常用的是快速排序呢？\n理由如下：\n速度快：快速排序的时间复杂度为O(nlogn)，这意味着它可以在很短的时间内对大量数据进行排序。相比之下，其他排序算法如堆排序和归并排序的时间复杂度也为O(nlogn)，但是快速排序通常比它们更快，因为它可以就地操作，而不需要创建任何辅助数组来保存临时值。 占用资源少：快速排序使用的空间复杂度为O(logn)，这意味着它在内存占用方面比其他排序算法更加高效。 局部性好：快速排序中的分区步骤通常具有很好的局部性，因为它访问前后附近的连续数组元素。这使得计算机硬件可以通过优化访问位置来提高效率。 归并排序和逆序对 逆序：给定自然数 1, ⋯ , n 的一个排列，如果 j \u0026gt; i 但 j 排 在 i 的前面则称 (j, i) 为该排列的一个逆序。\n分为两部分，求内部逆序数，求两个之间的逆序数。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 void MergeSort(int r[], int low, int high) { // 进行归并排序 int mid, r1[1000], i; if (low == high) return; else { mid = (low + high) / 2; // 划分 MergeSort(r, low, mid); // 递归求左子序列中逆序对 MergeSort(r, mid + 1, high); // 递归求右子序列中逆序对 Merge(r, r1, low, mid, high); // 合并 for (i = low; i \u0026lt;= high; i++) r[i] = r1[i]; } } void Merge(int r[], int r1[], int low, int mid, int high) { // 合并子序列 int i = low, j = mid + 1, k = low; while (i \u0026lt;= mid \u0026amp;\u0026amp; j \u0026lt;= high) { if (r[i] \u0026lt;= r[j]) {// 取较小者放入r1[k]中 r1[k++] = r[i++]; } else { count += mid - i + 1; // 若左子序列中的数1大于右子序列的数2，则数1后面的数都大于数2 r1[k++] = r[j++]; } } while (i \u0026lt;= mid) r1[k++] = r[i++]; while (j \u0026lt;= high) r1[k++] = r[j++]; } 二分搜索问题 给定已按升序拍好序的n个元素a[0:n-1]，现要在这n个元素中找到一个特定的元素x。\n遍历直接查找的时间复杂度为O(n)。\n分治可将时间复杂度降低到O(lgn)。\n伪代码如下：\n1 2 3 4 5 6 7 8 9 10 template\u0026lt;class Type\u0026gt; int BinarySearch(Type a[],const Type\u0026amp;x,int l,int x) { while(r\u0026gt;=l) { int m=(l+r)/2; if(x==a[m])return m; if(x\u0026lt;a[m])r=m-1;else l=m+1; } } 选择问题 寻找数组中第k小的元素。\n1）先排序，再可以直接找到第k小的元素。\n2）利用快速排序的方法，筛选出比基准元素小的和比基准元素大的，再根据左右边的数量判断k在哪一边，按照这个思路往下推，直到找到为止。\n对于1）的复杂度分析就略过了，这里重点看看2）\n当左右大小相差不多时，可以得到以下表达式：T（n）\u0026lt;= T（n/2）+ cn，n为2的幂次方的情况下，时间复杂度较好能达到O(n)，当然基准元素最好是要能选到中间的。代码如下。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 T select (T a[], int l, int r, int k) { if (l \u0026gt;= r)return a[l]; int i = l, j = r + 1; T pivot = a[j]; while (true) { do {i++;} while (a[i] \u0026lt; pivot); do {j--;} while (a[j] \u0026gt; pivot); if (i \u0026gt;= j)break; swap(a[i], a[j]); } if (j - l + 1 == k)return pivot; a[l] = a[j]; a[j] = pivot; if (i - l + 1 \u0026lt; k) return select(a, j + 1, r, k - j + l - 1); else return select(a, l, j - 1, k); } 棋盘覆盖问题 在一个2的k次方乘2的k次方个放个组成的棋盘上，恰有一个方格和其它方格不同，称该方格为一特殊方格，且称该棋盘为一特殊棋盘。在棋盘覆盖问题中，要用4种不同形态的L型骨牌覆盖给定的特殊棋盘上的特殊方格以外的所有方格，且任何两个L型骨牌不能重叠覆盖。\n递归式：T(k)=4T(k-1)+O(1);——\u0026gt;T(n)=4T(n/4)+c=O(n)；\n本来只有一个位置是特殊位置，每次切分时，加上一个骨牌，将四个位置都变成特殊位置。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 void chessBoard(int tr, int tc, int dr, int dc, int size) { //dr,dc残缺方块所在行列 //tr,tc棋盘中左上角方格所在行列 if (size == 1) return; int t = tile++, // L型骨牌个数 s = size / 2; // 子棋盘大小 // 覆盖左上角子棋盘 if (dr \u0026lt; tr + s \u0026amp;\u0026amp; dc \u0026lt; tc + s) // 特殊方格在此子棋盘中 chessBoard(tr, tc, dr, dc, s); else {// 此子棋盘中无特殊方格 // 用 L型骨牌覆盖右下角 board[tr + s - 1][tc + s - 1] = t; // 覆盖其余方格 chessBoard(tr, tc, tr + s - 1, tc + s - 1, s); } // 覆盖右上角子棋盘 if (dr \u0026lt; tr + s \u0026amp;\u0026amp; dc \u0026gt;= tc + s) // 特殊方格在此子棋盘中 chessBoard(tr, tc + s, dr, dc, s); else {// 此子棋盘中无特殊方格 // 用L型骨牌覆盖左下角 board[tr + s - 1][tc + s] = t; // 覆盖其余方格 chessBoard(tr, tc + s, tr + s - 1, tc + s, s); } // 覆盖左下角子棋盘 if (dr \u0026gt;= tr + s \u0026amp;\u0026amp; dc \u0026lt; tc + s) // 特殊方格在此子棋盘中 chessBoard(tr + s, tc, dr, dc, s); else {// 用 L型骨牌覆盖右上角 board[tr + s][tc + s - 1] = t; // 覆盖其余方格 chessBoard(tr + s, tc, tr + s, tc + s - 1, s); } // 覆盖右下角子棋盘 if (dr \u0026gt;= tr + s \u0026amp;\u0026amp; dc \u0026gt;= tc + s) // 特殊方格在此子棋盘中 chessBoard(tr + s, tc + s, dr, dc, s); else {// 用 L型骨牌覆盖左上角 board[tr + s][tc + s] = t; // 覆盖其余方格 chessBoard(tr + s, tc + s, tr + s, tc + s, s); } } void OutputBoard(int size) { for (int i = 0; i \u0026lt; size; i++) { for (int j = 0; j \u0026lt; size; j++) cout \u0026lt;\u0026lt; setw (5) \u0026lt;\u0026lt; Board[i][j]; cout \u0026lt;\u0026lt; endl; } } 循环赛日程表 有n=2k个运动员进行网球循环赛，现在要设计一个日程表，满足以下要求：\n1）每个选手必须和其它个选手各比赛一次。\n2）每个选手一天只能赛一次。\n3）循环赛一共进行n-1天。\n按照分治策略，所有的选手分成两半的情况下，n个选手的日程表由外n/2个选手就可以决定。\n代码如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 void gametable(int k) { int a[100][100]; int n, temp, i, j, p, t; n = 2; //两个参赛选手日程 a[1][1] = 1; a[1][2] = 2; a[2][1] = 2; a[2][2] = 1; for (t = 1; t \u0026lt; k; t++) //迭代处理，依次处理2 ^ n....2 ^ k个选手的比赛日程 { temp = n; n = n * 2; //填左下角元素 for (i = temp + 1; i \u0026lt;= n; i++) for (j = 1; j \u0026lt;= temp; j++) a[i][j] = a[i - temp][j] + temp; //左下角和左上角元素的对应关系 for (i = 1; i \u0026lt;= temp; i++) //将左下角元素抄到右上角 for (j = temp + 1; j \u0026lt;= n; j++) a[i][j] = a[i + temp][(j + temp) % n]; for (i = temp + 1; i \u0026lt;= n; i++) //将左上角元素抄到右下角 for (j = temp + 1; j \u0026lt;= n; j++) a[i][j] = a[i - temp][j - temp]; } } 附： 1.内容参考\n佟鑫宇老师 天津大学智能与计算学部 2023秋 算法设计与分析 ppt\n考试考察重点：棋盘覆盖问题，归并排序，快速排序，选择问题。\n","date":"2023-03-20T00:00:00Z","permalink":"https://sutdown.github.io/p/%E7%AE%97%E6%B3%95%E4%B8%93%E9%A2%98%E5%88%86%E6%B2%BB%E6%B3%95divide-and-conquer/","title":"算法专题：分治法（Divide and Conquer）"},{"content":"一：前言 学习基础：数据结构与算法。文章中少部分涉及数据结构的知识不作讲解。\n使用情况： 在动态规划中，一个问题必须拥有重叠子问题和最优子结构，才能用动态规划解决。\n重叠子问题：一个问题可以被分解为若干个子问题，且这些子问题会重复出现。比如求斐波那契数列时，递归（自顶而下Top-down Approach）会产生大量重叠子问题，此时可以采用记忆化搜索记住那些重叠子问题，避免多次计算从而降低时间复杂度。\n最优子结构：如果一个问题的最优解可以由子问题的最优解有效的构造出来，那么称这个问题有最优子结构。比如树塔问题自下而上（Bottom-up Approach）进行递推得到最终结果。\n比如在常见的最短路问题中，可以采取从目标点向前推和起始点向后推两种策略。\n递归和递推： 递归（自顶向下）：\n递归是一种自然而然的思考方式，它将一个大问题分解成一个或多个子问题，并通过递归调用来解决子问题。\n比如斐波那契数列、汉诺塔问题等。此外，递归可以使代码更加清晰，更接近问题的数学描述。\n递推（自底向上）：\n递推是从最小的子问题开始，逐步构建问题的解决方案，直到解决整个问题，通常使用迭代或循环来实现。\n递推通常更有效率，因为它避免了递归调用的开销，并且可以避免潜在的堆栈溢出问题。递推适合处理具有重叠子问题性质的问题，其中子问题的解可以存储并复用。典型的例子包括计算斐波那契数列、求解动态规划问题、计算组合数等。\n因此，可以首先尝试使用递推来解决动态规划问题，因为它更容易优化并且通常具有更好的性能。但是，有些问题可能更容易用递归来思考和表达，因此适用递归方法也是有意义的。有时候，甚至可以将两者结合使用，使用递归来理解问题和定义状态转移方程，然后使用递推来实际计算结果，这种方法称为\u0026quot;记忆化搜索\u0026quot;，具体情况一般具体考虑。\n优缺点： 时间和空间：动态规划中，采用递归或者递推的算法会使时间复杂度递增，所以往往我们会采取迭代的方案以空间记录其中的数据换取时间复杂度的降低。问题规模的大小往往对应该事件的时间复杂度。\n解题步骤： 1.审题观察具有重叠子问题和最优子结构，判断是否为动态规划问题。\n2.找出题中的状态转移方程，即如何把大问题分解为小问题。\n3.找到边界，编写代码具体实现，得出结果。（也可继续分析算法的时间和空间复杂度，对此加强。\n4.回溯找出问题的答案。\n解题思路： 以下介绍中，一般会先根\t据人的本能去思考暴力做法，因为即使是动态规划也会与普通的暴力做法有共通的地方，再去观察这个题目的特点，如何联想到动态规划，再使用寻找状态转移方程写代码求解验证。\n动态规划和静态规划： 动态规划和静态规划都是优化问题，且二者可以相互转换。\n动态规划\n• 优点：DP表使得求解过程清晰，更容易获得最优解。\n• 缺点：设计DP表难度大；状态空间可能呈指数增长。\n静态规划\n静态规划也是一种优化问题，包括线性规划和非线性规划。静态规划是从一个或者多个可行解开始，通过不断迭代对现有的可行解进行优化，最后得到最优解。\n优点：始终保持一个可行解或近优解，思路相对简单。\n缺点：当问题规模较大时，可能面临庞大的计算量。\n二：经典题型 1.线性DP 1.1最大连续子序列和 题目：给定一个数字序列A1，A2，A3…An，求i，j（1\u0026lt;=i\u0026lt;=j\u0026lt;=n)，使Ai+…+Aj最大，输出这个最大和。\n对于该题，首先我们想到的是暴力做法，可以采用前缀和。\n从A1开始，把A1，A1+A2,A1+A2+A3…；然后再从A2开始，A2，A2+A3…;一直到从An开始，可以用双重for实现，时间复杂度O(n^2)，同时采用一个大小为n*（n+1）/2的数组保存其中的所有数据，再求值。但明显，这样的方法过于繁琐。\n再重新观察该题，我们要如何降低O(n^2)的时间复杂度，我们将其拆分为子问题，前1个数的最大连续和，前2个数的最大连续和，前3个数的最大连续和……可知，在求多一个数的连续子序列和时，结合此题，要么就是当前数最大，要么就是加上前几个数的序列，因此我们得到状态转移方程 $$ dp[i]=max(A[i],dp[i-1]+A[i]) $$ 然后我们再比较dp[0],dp[1]…dp[n-1]的最大值即可。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #include\u0026lt;iostream\u0026gt; #include\u0026lt;algorithm\u0026gt; using namespace std; const int maxn = 10010; int A[maxn]; int dp[maxn]; int main() { int n;//输入数组长度和数组中的数 cin \u0026gt;\u0026gt; n; for (int i = 0; i \u0026lt; n; i++)cin \u0026gt;\u0026gt; A[i]; dp[0] = A[0];//状态转移方程 for (int i = 1; i \u0026lt; n; i++) { dp[i] = max(dp[i - 1] + A[i], A[i]); } //标准库函数max_element，它能够返回数组中的最大元素的迭代器，也就是地址，需要*解引用得到该地址的值。 cout \u0026lt;\u0026lt; *max_element(dp, dp + n) \u0026lt;\u0026lt; endl; return 0; } 1.2最长上升子序列 给定一个长度为n的数列，求数值严格单调递增的子序列的长度最长是多少。\n该题和上一题虽然都是线性序列，但是上一题要求的是连续，因此可以从头到尾一个一个来，但是该题的子序列不连续，可以是跳跃状态，因此方法不同。\n先思考暴力做法，序列中的每个元素有取和不取两种状态，如果全部考虑，对应2^n种，明显时间复杂度过高。所以考虑其它思路。\n其实当前问题在于如何找到该问题的子问题，当然还有，每个上升子序列之间如何比较。1.1中是每个子问题中只有一个连续的解答，但是1.2中每个子问题会有多个上升子序列，在不到最后时无法确认最长序列。\n重点来了，解决方案：这个多个上升子序列的解决方案就是开启第二重循环，对前面的每个dp[i]（dp[i]指的是0-i个种的最长子序列）都进行一次比较，然后取得最大上升子序列的个数。状态转移方程为 $$ dp[i]=max(1,dp[j]+1);(j=1,2,3...i-1,A[j]","date":"2023-03-12T00:00:00Z","permalink":"https://sutdown.github.io/p/%E7%AE%97%E6%B3%95%E4%B8%93%E9%A2%98%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92dynamic-programming/","title":"算法专题：动态规划（Dynamic Programming）"}]