diff --git a/2022/07/15/os-journal-vol-1/index.html b/2022/07/15/os-journal-vol-1/index.html index 3822ddc..6f513ac 100644 --- a/2022/07/15/os-journal-vol-1/index.html +++ b/2022/07/15/os-journal-vol-1/index.html @@ -7,7 +7,7 @@ - + @@ -166,7 +166,7 @@ isHome: false, isHighlightShrink: false, isToc: true, - postUpdate: '2024-11-19 14:52:26' + postUpdate: '2024-11-22 16:30:16' }

操统实验日志 第一章 序章

简述

+

操统实验日志 第一章 序章

简述

在一切开始之前,请允许我先简要地介绍一下关于这个实验的一切

它是关于什么的

@@ -506,7 +506,7 @@ btf.addGlobalFn('pjaxSend', () => {

目前为止,环境已经基本配置完成了
接下来就让我们开始愉快的操作系统实验之旅吧!

-
文章作者: Linloir
文章链接: https://blog.linloir.cn/2022/07/15/os-journal-vol-1/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 時痕
avatar
Linloir
我、技术、生活与值得分享的一切
Follow Me
最新文章

操统实验日志 第二章 万丈高楼平地起

关于本章

+

操统实验日志 第二章 万丈高楼平地起

关于本章

本章的将会首先介绍操作系统是如何运行起来的,并在此基础上介绍实现一个完备的操作系统实验需要实现哪些方面,以及这些部分的先后顺序和依赖关系

由于这份文档我并不打算作为一份完备的教程文档来编写,因此语言方面的介绍会相对简略或是跳过,对应的详细介绍可以参考学校的同步教程

在本章的后半部分,将会介绍MBR和中断的相关知识,记录如何编写MBR、测试使用BIOS启动MBR引导程序并通过中断输出字符串进行测试

@@ -953,7 +953,7 @@ BIOS从0x7C00处开始执行代码,其并不区分内存中存储 下一章中,将会编写bootloader,从mbr中加载bootloader并且启动,最后在bootloader中让CPU进入保护模式
如果准备好了的话,就让我们进入下一章吧!

-
文章作者: Linloir
文章链接: https://blog.linloir.cn/2022/07/15/os-journal-vol-2/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 時痕
avatar
Linloir
我、技术、生活与值得分享的一切
Follow Me
最新文章

操统实验日志 第三章 从实模式到保护模式

关于本章

+

操统实验日志 第三章 从实模式到保护模式

关于本章

在本章的第一部分中,将会介绍读取硬盘的CHS方式、LBA方式以及如何通过inout指令读写硬盘,之后会将上一章输出Hello World!的代码移植到BootLoader中,并且从MBR中加载并跳转到编写的BootLoader执行

第二部分中,会回顾保护模式的概念并介绍进入保护模式的四个步骤,并在开启保护模式之后输出第二个Hello World

第一次跃进:从MBR跳转到BootLoader

@@ -970,7 +970,7 @@ LoopCount &= SectorCount \times 512 \div 2 \\

完成
至此,就完成了本章的全部任务,赶紧使用make clean build run来测试代码的运行情况吧!

-
文章作者: Linloir
文章链接: https://blog.linloir.cn/2022/07/19/os-journal-vol-3/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 時痕
avatar
Linloir
我、技术、生活与值得分享的一切
Follow Me

操统实验日志 第四章 勇者之路

关于本章

+

操统实验日志 第四章 勇者之路

关于本章

在本章节的第一部分中,将会简要介绍在下一章中将要编写的KernelLoader,以及在开始着手进行它的编写之前所需要完成的,包括各种驱动、文件系统接口等在内的诸多准备工作。

在第一部分之后,我决定按照KernelLoader中的函数调用顺序,逐节完成KernelLoader中所需要的所有准备工作,因此在第二部分中,将会首先记录如何在项目中使用C语言和汇编混合编程,包括C语言是如何进行函数调用的,以及内联汇编中NASM向AT&T迁移语法所需要注意的问题。有了这部分基础知识,就可以进行第三部分编写一些常用的驱动,并从我个人的角度讲讲为什么要这么做,它对后续的代码编写能够起到哪些帮助。

在之后的第四部分中,会进行有关文件系统的知识的详述,并且带领大家阅读微软关于FAT文件系统的文档,根据文档完成FAT文件系统接口的设计和实现。

@@ -1845,7 +1845,7 @@ VirtualPageIndex &= VirtualPageAddress >> n \nonumber \\

勇者奖章
恭喜你,读完了所有日志中最长最复杂的一篇,后面的实验之路将会因这一章的努力而愈发平坦。

-
文章作者: Linloir
文章链接: https://blog.linloir.cn/2022/08/20/os-journal-vol-4/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 時痕
avatar
Linloir
我、技术、生活与值得分享的一切
Follow Me

重生

时隔两年,终于借着重新配置家里网络环境的契机,重新搭建了这个博客。

+

重生

时隔两年,终于借着重新配置家里网络环境的契机,重新搭建了这个博客。

原先关于操作系统的文章正在慢慢搬迁,应该很快就能恢复了~

再一次启用关于自己的博客,感觉心里良多感慨。还记得上一次搭博客时的自己,刚来到计算机学院,对着网上的保姆教程在腾讯云的小机器上搭了 git 仓库、配置了宝塔面板、DNS 解析。

那时的自己对 TLS、证书、Git、反代、CDN、Docker 这些东西都还是那么陌生,以至于教程之外的东西完全不敢去碰,哪怕是在宝塔面板上配一个 Let’s Encrypt 的证书都要折腾好久,也没有去研究 hexo deploy 到底 deploy 了什么到服务端,只觉得能跑便是好事,这也就导致了后来的删库跑路事件——本地的博客仓库被主动删除,等到发现服务器上是没有 Markdown 源文件的时候已经太迟,由于没有了源文件,写新的博客势必会导致旧的 html 被覆盖,又因为文章实在太长迟迟没有动手迁移,原先的数万字长文就这样被冻在了旧的博客里长达两年。

@@ -182,7 +182,7 @@ btf.addGlobalFn('pjaxSend', () => {

不夸张地说,看到熟悉的页面再一次出现在浏览器中的时候,内心有许多感慨,大概就像离家的游子多年后重新推开家门时那样吧。拂去把手上的灰尘,推开门回到曾经熟悉的地方,所有的东西都还在原本的地方等着自己,仿佛从来没有离开过那样。博客大概就是我内心无处安放的杂思的归宿吧,我想,如今它们终于又能安家了。

这一次回来,不知道能够持续多久,但我希望,能够长一些、再长一些。至于内容,我也不打算维持早年纯技术的导向了,我更多地想让这个博客成为我存在的痕迹,让多年后的自己看到曾今的文章能够会想起当年的纠结、焦虑、喜悦或是激动,能够从这里,看到我。

总之,欢迎回家。

-
文章作者: Linloir
文章链接: https://blog.linloir.cn/2024/10/11/reborn/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 時痕
avatar
Linloir
我、技术、生活与值得分享的一切
Follow Me

基于 IPv6 公网地址、NAS 和 MacMini 的私有部署博客方案

方案速览

简单来说,方案分为了几个主要的部分:

+

基于 IPv6 公网地址、NAS 和 MacMini 的私有部署博客方案

方案速览

简单来说,方案分为了几个主要的部分:

  1. 根据 在 NAS 上部署自己的 Gitea 服务,无需公网服务器 方案打通外网到家用 NAS / MacMini 的链路
  2. 采用 Git 仓库 main 分支存放源码 + Gitea Actions 编译至 publish 分支实现源码及制品存储
  3. @@ -187,7 +187,7 @@ btf.addGlobalFn('pjaxSend', () => {

    环境准备

    在配博客之前,我是先配好了 Nas 上的 Gitea 服务,可以参考 在 NAS 上部署自己的 Gitea 服务,无需公网服务器 这一篇博客来准备基本的网络环境和 Gitea 服务。

    (也就是说,我是先搭好了 Gitea,然后实在不知道能拿干点什么,才决定把博客迁移回来的。有点为了醋包饺子的感觉哈哈,不过现在博客全部内容都运行在自己本地感觉还是颇有成就感的)

    仓库配置

    待后面补充~

    -
文章作者: Linloir
文章链接: https://blog.linloir.cn/2024/10/12/blog-from-scratch/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 時痕
avatar
Linloir
我、技术、生活与值得分享的一切
Follow Me

短文 - 博客迁移小记

最开始想的迁移方案是使用 skip-render 标记 html,但始终觉得不够优雅,因为导航栏、个人信息、头图之类的内容时常都会变,如果 skip-render 那永远都会是当时那个版本的页面,甚至可能超链接都是失效的,除了能显示原本的博文之外其实体验应该是相当差的——横竖感觉就是很突兀嘛!

+

短文 - 博客迁移小记

最开始想的迁移方案是使用 skip-render 标记 html,但始终觉得不够优雅,因为导航栏、个人信息、头图之类的内容时常都会变,如果 skip-render 那永远都会是当时那个版本的页面,甚至可能超链接都是失效的,除了能显示原本的博文之外其实体验应该是相当差的——横竖感觉就是很突兀嘛!

直到今天突然意识到,hexo 渲染 markdown 为 html 文本肯定会分为三个大部分:

  1. 正文前部的各种元素,例如头图、导航栏、侧边栏等
  2. @@ -192,7 +192,7 @@ btf.addGlobalFn('pjaxSend', () => {

    然后再把原先的图片复制到现在的 img 目录下,批量改一手路径,Done!

    谁能想到这个卡了我两年的问题,竟然能 15 分钟就搞定了!!!

    过于激动,遂特写此文记录一下,真是拍大腿啊!!!

    -
文章作者: Linloir
文章链接: https://blog.linloir.cn/2024/10/13/blog-mig/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 時痕
avatar
Linloir
我、技术、生活与值得分享的一切
Follow Me

在 NAS 上部署自己的 Gitea 服务,无需公网服务器

方案速览

简单来说,方案包含了以下几个主要部分:

+

在 NAS 上部署自己的 Gitea 服务,无需公网服务器

方案速览

简单来说,方案包含了以下几个主要部分:

  1. 公网访问使用光猫桥接路由器拨号,通过路由器同时获取 IPv4 大内网和 IPv6 公网 /64 地址
  2. DNS 解析通过 MacMini 上部署的 ddns-go 实现
  3. @@ -248,7 +248,7 @@ btf.addGlobalFn('pjaxSend', () => {

    不过,由于 Cloudflare 只支持有限的端口转发,并且只支持基于 http/https 协议的流量转发,因此适用面相对不那么广,但是家用部署网站服务还是绰绰有余了。具体允许的端口号见 Cloudflare 文档

    经过测试,中国联通封了 80,443,2096 端口,回源我采用的 2095 端口,这个在 Cloudflare 控制台 - 规则 - Origin Rules 可以创建回源规则针对特定域名指定。

    Gitea 安装与反向代理

    待后续翔实~

    -
文章作者: Linloir
文章链接: https://blog.linloir.cn/2024/10/13/host-git-at-home/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 時痕
avatar
Linloir
我、技术、生活与值得分享的一切
Follow Me

短文 - 关于短博文的碎碎念

感觉如果博客只写长文的话,好像很快就会疲乏,其实很多时候想说的内容就是一两句话,即便硬是写成了长文,又觉得好像啰嗦了。

+

短文 - 关于短博文的碎碎念

感觉如果博客只写长文的话,好像很快就会疲乏,其实很多时候想说的内容就是一两句话,即便硬是写成了长文,又觉得好像啰嗦了。

看到 Hexo - Butterfly 有提供一个 “说说” 的页面可以用 .yml 格式来存一些说说文档,但是仔细一看发现好像不会自动分页,这样一来图片一多感觉加载就会变成彻底的灾难…

不知道为什么在静态编译的时候没有做成本地分页的格式呢… 就像文章那样,其实在编译阶段就可以分散到不同的 index.html 去了,好可惜,也许以后有空会想办法看看能不能改吧…

还有很多云存储的方案,但感觉把自己的内容放在云上,总感觉会比较担心数据安全和以后的迁移成本,纯本地的话哪怕一天发两条十年也不过才不到上万条数据,一个 .yml 就带走了,哎可惜没分页终究还是不打算去用。

想来想去,就把短博客也当作正常的文章一样的显示在主页吧,不过会在标题前面加上 “短文” 的标记和对应的 tag,也方便浏览的时候来做区分好了

-
文章作者: Linloir
文章链接: https://blog.linloir.cn/2024/10/13/micro-posts/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 時痕
avatar
Linloir
我、技术、生活与值得分享的一切
Follow Me

短文 - 转正日

今天收到了转正邮件,正式标志着一个新的人生阶段的开始。

+

短文 - 转正日

今天收到了转正邮件,正式标志着一个新的人生阶段的开始。

对自己的期望就是,不要忘记做技术的初心,在新的阶段能有所成长,有所收获。

Po 一张在鹅厂的第一个关爱里程碑~

first_day

-
文章作者: Linloir
文章链接: https://blog.linloir.cn/2024/10/15/tencent-new-start/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 時痕
avatar
Linloir
我、技术、生活与值得分享的一切
Follow Me

用 adb + app_process 执行 Java 代码 —— 一种无需安装 apk 的脱机代码执行方案

方案速览

本方案本质上是使用了安卓提供的 app_process 命令,在将 Java 代码正确地打包为需要的 .jar 或是 .dex 文件后,通过 app_process 启动对应的入口函数来实现 adb 执行 Java 代码的能力。

+

用 adb + app_process 执行 Java 代码 —— 一种无需安装 apk 的脱机代码执行方案

方案速览

本方案本质上是使用了安卓提供的 app_process 命令,在将 Java 代码正确地打包为需要的 .jar 或是 .dex 文件后,通过 app_process 启动对应的入口函数来实现 adb 执行 Java 代码的能力。

对于目标 .jar 或是 .dex 文件,有两种不同的编译方案:

  1. .dex 文件方式:
      @@ -272,7 +272,7 @@ btf.addGlobalFn('pjaxSend', () => {
    1. 在研究 atx/uiautomator2 的实现的时候,先入为主地就认为作者使用了 uiautomator <jar> 这种方式调用,后面注意力都放在了查找作者针对 u2.jar 的打包方式和相关代码上了,以至于在仓库里搜索 u2.jar 的时候竟然没有注意到在搜索结果中 core.py 赫然有着 command = "CLASSPATH=/data/local/tmp/u2.jar app_process / com.wetest.uia2.Main" 这一启动方式,与答案擦肩而过并且进一步在寻找 uiautomator 1.0 的打包方式上浪费了大半天的时间
    2. 对于一个陌生的仓库,如果想要了解它的源码实现,不一定非要自己去读,除了进行仓库里的关键词搜索,也可以去查找别人的源码解析,走个捷径
    -
文章作者: Linloir
文章链接: https://blog.linloir.cn/2024/10/20/jar-via-adb/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 時痕
avatar
Linloir
我、技术、生活与值得分享的一切
Follow Me

短文 - 面对首次绩效考核的一些随笔

快要到年底绩效考核的时间了,回想过去半年好像一直忙忙碌碌但好像又没有什么很亮眼的成果。期间还有一大部分精力都投在了一个设计难度远大于实现难度的需求。

+

短文 - 面对首次绩效考核的一些随笔

快要到年底绩效考核的时间了,回想过去半年好像一直忙忙碌碌但好像又没有什么很亮眼的成果。期间还有一大部分精力都投在了一个设计难度远大于实现难度的需求。

回过头想想,其实在工作上开始做一件长期的事情之前,还是要去评估可行性、投入以及收效,到底这件事情有多大的优先级,是不是就值得现在立马开始投入人力去做这个。到底做些什么可以被业务感知到,从而去判断,到底哪些需求才是重要的,而不是一味的承接需求,最后反倒抓不到重点了。

当然,除此之外,更重要的一点是,在知道了公司存在强制的 Underperform 比例时,不免会想,也许有一天我也要成为背这个绩效的人?在这种场景下,是否不要把鸡蛋放在一个篮子里更为安全?诚然,上班本质还是一种利益交换,我给公司提供我能够产出的内容,帮助公司节省人力开支或是做原本人力做不了的事情,公司给我反馈大厂背景、经验、人脉和钱,当然,这里面必然伴随着剩余价值的剥削云云,但总的来看,也算是目前相对公平且不错的买卖,在这样的背景下,我还是会在工作的时候专注于需求本身,坚守住技术人应有的底线,但在工作时间之外,确实应该更加关注自身的投资,也许是身体的健康,也许是产品思维,也许真的去做一些能代表自己实力的东西吧。

前几天有看到一个帖子,个人认为说的其实很有道理,大致意思就是,人总要有一些能证明自己能力的东西,在校招的时候,学历就是最大的背书;在工作的时候,便是绩效和公司、项目经历。但是,如果能有足够的其他履历,比如大的开源项目、成功的产品,这些就会替代前者变成能力的证明,毕竟,选择前者只是没有后者的无奈之举。所以,我想,做自己感兴趣的事,并且在这方面出彩,大概才是最有价值的投资吧。

-
文章作者: Linloir
文章链接: https://blog.linloir.cn/2024/11/08/first-grading/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 時痕
avatar
Linloir
我、技术、生活与值得分享的一切
Follow Me

长更 - Linux 常用指令

先挖坑,慢慢填

-
文章作者: Linloir
文章链接: https://blog.linloir.cn/2024/11/08/linux-commands/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 時痕

长更 - Linux 常用指令

先挖坑,慢慢填

+
文章作者: Linloir
文章链接: https://blog.linloir.cn/2024/11/08/linux-commands/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 時痕

Listary 命令分享 - 快捷 clone 仓库并使用 VSCode 打开

背景

日常工作中,经常会需要临时 Clone 某个仓库并且用 VSCode 打开,在 Windows 上我一般都是:

+

Listary 命令分享 - 快捷 clone 仓库并使用 VSCode 打开

背景

日常工作中,经常会需要临时 Clone 某个仓库并且用 VSCode 打开,在 Windows 上我一般都是:

  1. 用文件资源管理器定位到需要 Clone 到的位置然后右键呼出终端
  2. git clone
  3. @@ -198,7 +198,7 @@ btf.addGlobalFn('pjaxSend', () => {

    之后,在文件夹中直接输入 clode <repo url> [dest folder] 即可一键 clone

    [dest folder] 为选填,不填则是默认取 <repo url> 末尾仓库名称作为目标文件夹,同直接执行 git clone <repo url> 行为保持一致

    Settings

    -
文章作者: Linloir
文章链接: https://blog.linloir.cn/2024/11/19/listary-quick-clone-command/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 時痕

问题定位回顾 - Windows 上发起 tcp 连接时提示 Only one usage of each socket address (protocol/network address/port) is normally permitted

问题背景

某天在 Windows 宿主机上执行任务时,发现 wda 指令请求一直失败,查看日志发现唯一有效的错误日志是 Only one usage of each socket address (protocol/network address/port) is normally permitted,回顾宿主机环境在过去一段时间没有进行过变更,并且该问题是第一次出现,此前相同环境并没有出现过这个问题

+

尝试在其他宿主机以及本地开发机上执行相同命令均不能稳定复现该问题,其中 Linux 开发机无法复现该问题

+

error info

+

定位过程

报错分析

由于报错信息只有一行,也只能从这个信息来入手。搜索引擎检索得到如下内容:

+ +

由于此前宿主机并没有暴露出该问题,优先考虑并不是发起请求及接收请求的 client 及 wda 侧的问题,即第一篇文章中提到的可能,而是从宿主机上的 tcp 连接状况入手来进一步分析这个问题,于是主要的关注点转向如下方面:

+
    +
  1. 问题出现时,宿主机上的 tcp 连接情况如何,是否存在异常,异常现象怎样?
  2. +
  3. 如果是 tcp 连接问题,为什么 Linux 机器无法复现该问题
  4. +
+

链路分析

在开始检查 tcp 连接情况前,首先需要确认大致的请求链路,从而确定需要关注的 tcp 连接范围。问题发生在 Windows 上 docker 容器内服务向宿主机发送 wda 请求时,因此从这一条请求链路入手:

+

请求的末端是 usbmuxd 服务,该服务负责和连接的 iPhone 进行通信,所有请求最后都会发送到 usbmuxd 来进行多路复用 (multiplexing),从而达到在一个 usb 链路上同时执行多个请求。

+

usbmuxd 服务在 Linux 和 Windows 上的表现并不相同,在 Linux 上,其监听一个 UNIX socket 套接字来提供服务,而在 Windows 上,其监听 127.0.0.1:27015 端口提供服务,这里应该是处于安全考虑,其只监听了 127.0.0.1:27015 而不是 0.0.0.0:27015

+

由于 Windows 上的 usbmuxd 服务仅监听 127.0.0.1:27015 端口,对于容器内打出来的请求,考虑到其 ip 可能不是 127.0.0.1,遂在宿主机有起一个反代服务,从 0.0.0.0:23333 端口反向代理到 127.0.0.1:27015,这样不是以 127.0.0.1 发起请求的服务可以通过连接到宿主机的 23333 端口来和 usbmuxd 通信。在 Linux 上,同样有这个反代服务,不过它的作用是将请求代理到对应的 UNIX socket 上。

+

之后就是 docker 内的服务了,其通过 host.docker.internal 域名向宿主机的 23333 端口建立连接,并和 usbmuxd 通信来发送 wda 的请求。

+

这样,在宿主机上需要关注的连接就很明了了:

+
    +
  1. 所有直接向 127.0.0.1:27015 服务发起的通信,包括宿主机对 usbmuxd 服务的请求以及反代服务发起的请求
  2. +
  3. 向宿主机 23333 端口发起的通信
  4. +
+

tcp 链接分析

在 Windows 再次复现问题时,使用 netstat -ano | findstr 27015 以及 netstat -ano | findstr 23333,并辅助 | Mesaure-Object 方法来查看连接情况发现:

+
    +
  1. ESTABLISHED 状态连接数正常,差不多在十到百量级,符合当前宿主机负载情况
  2. +
  3. CLOSED 状态几乎没有
  4. +
  5. TIME_WAIT 状态非常多,平均在 8000 左右
  6. +
+

上述现象佐证:

+
    +
  1. 宿主机中应该没有发生连接泄露的情况,连接泄露一般表现为 tcp 打开连接后并不正常关闭导致 ESTABLISHED 数量异常增多,此时 ESTABLISHED 状态数正常,故暂时排除这个原因
  2. +
  3. 存在高并发的短连接请求负载,TIME_WAIT 为请求发起方正确通告服务端关闭 tcp 连接后进入的 2MSL 的等待窗口,该标记意味请求已经正常关闭,但是由于短时间内发起量太大导致大量 TIME_WAIT 状态连接堆积
  4. +
+

联系 报错分析 中的第二篇参考文章,初步猜测可能由于存在进程大量发起请求,请求结束后 tcp 连接进入 TIME_WAIT 状态,由于没有设置 SO_REUSEADDR 此时端口号仍然处于被占用的状态,如果继续建立连接确实可能会出现端口号用尽导致报错。

+

这也就引导关注点到如下问题:

+
    +
  1. 20000 ~ 65535 这么多端口,怎么会这么快就用尽?netstat -ano | findstr 27015 | findstr | findstr TIME_WAIT | Measure-Object 或是 netstat -ano | findstr 23333 | findstr TIME_WAIT | Measure-Object 反映出来的 TIME_WAIT 连接也不到一万,问题是如何触发的?
  2. +
  3. 是哪个进程在大量发起这样的请求?是否符合预期?
  4. +
+

Windows 侧动态端口范围分析

先关注第一个问题,通过检索搜索引擎 windows dynamic port range 可以检索到如下文档:

+

The default dynamic port range for TCP/IP has changed since Windows Vista and in Windows Server 2008:*To comply with Internet Assigned Numbers Authority (IANA) recommendations, Microsoft has increased the dynamic client port range for outgoing connections in Windows Vista and Windows Server 2008. The new default start port is 49152, and the new default end port is 65535.*,也即 Windows 上的默认动态端口范围自 Windows Server 2008 开始,默认从 49152 开始到 65535,一共 16384 个,同时使用 netsh int ipv4 show dynamicport tcp 可以查看实际配置的值

+

Powershell 执行 netsh int ipv4 show dynamicport tcp,确实得到 16384 这个结果:

+
1
2
3
4
协议 tcp 动态端口范围
---------------------------------
启动端口 : 49152
端口数 : 16384
+ +

联系 tcp 链接分析 中的数量,如果与 2701523333 端口建立的连接数量均达到 8000,确实会有端口耗尽的可能。所以下一步的关键就是:究竟是哪个进程在建立这些连接?是否是自己工程逻辑有问题?

+

TIME_WAIT 源头分析

由于 netstat -ano | findstr 27015 | findstr TIME_WAIT 无法展示实际创建连接的 PID,为此我专门写了一个 python 小脚本,这个脚本不断获取非 TIME_WAIT 状态的连接列表来获取连接的 PID,再获取 TIME_WAIT 连接并找到它们原先对应的 PID 输出:

+
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
import subprocess
import time
import signal
import sys

# Dictionary to keep track of dual tcp ip:port pair to pid
tcp_map = {}

def update_tcp_map():
global tcp_map
# Execute the command to get established connections
command = "powershell -Command \"netstat -ano | findstr /V TIME_WAIT\""
result = subprocess.run(command, capture_output=True, text=True, shell=True)

if result.returncode == 0:
lines = result.stdout.strip().split('\n')
for line in lines:
parts = line.split()
if len(parts) >= 5:
local_address = parts[1]
remote_address = parts[2]
pid = parts[4]
tcp_map[(local_address,remote_address)] = pid

def check_time_wait_sockets():
global tcp_map
# Execute the command to get TIME_WAIT sockets
command = "powershell -Command \"netstat -ano | findstr TIME_WAIT | findstr /V :443 | findstr /V :80\""
result = subprocess.run(command, capture_output=True, text=True, shell=True)

if result.returncode == 0:
lines = result.stdout.strip().split('\n')
for line in lines:
parts = line.split()
if len(parts) >= 5:
local_address = parts[1]
remote_address = parts[2]
pid = parts[4]
# Check if the TIME_WAIT socket was previously tracked
if (local_address,remote_address) in tcp_map:
print(f"TIME_WAIT socket: {local_address} -> {remote_address}, previously tracked PID: {tcp_map[(local_address,remote_address)]}")
# Remove the tracked record
del tcp_map[(local_address,remote_address)]

def signal_handler(sig, frame):
print("\nExiting...")
sys.exit(0)

def main():
signal.signal(signal.SIGINT, signal_handler)

print("Monitoring TCP connections. Press Ctrl+C to exit.")
while True:
update_tcp_map()
check_time_wait_sockets()

if __name__ == "__main__":
main()
+ +

脚本写的比较简单,考虑到没有太多干扰连接,直接抓取的所有的 TIME_WAIT 连接的原始 PID,同时过滤了下 44380 这种明显干扰的结果。

+

同时为了分析 报错分析 中的第二个问题,同样也写了一份针对 Linux 机器的分析脚本:

+
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
import subprocess
import time
import signal
import sys

# Dictionary to keep track of dual tcp ip:port pair to pid
tcp_map = {}

def update_tcp_map():
global tcp_map
# Execute the command to get established connections
command = "netstat -anolp | grep -v TIME_WAIT"
result = subprocess.run(command, capture_output=True, text=True, shell=True)

if result.returncode == 0:
lines = result.stdout.strip().split('\n')
for line in lines:
parts = line.split()
if len(parts) >= 7:
local_address = parts[3]
remote_address = parts[4]
pid = parts[6]
tcp_map[(local_address,remote_address)] = pid

def check_time_wait_sockets():
global tcp_map
# Execute the command to get TIME_WAIT sockets
command = "netstat -anolp | grep TIME_WAIT"
result = subprocess.run(command, capture_output=True, text=True, shell=True)

if result.returncode == 0:
lines = result.stdout.strip().split('\n')
for line in lines:
parts = line.split()
if len(parts) >= 7:
local_address = parts[3]
remote_address = parts[4]
# Check if the TIME_WAIT socket was previously tracked
if (local_address,remote_address) in tcp_map:
print(f"TIME_WAIT socket: {local_address} -> {remote_address}, previously tracked PID: {tcp_map[(local_address,remote_address)]}")
# Remove the tracked record
del tcp_map[(local_address,remote_address)]

def signal_handler(sig, frame):
print("\nExiting...")
sys.exit(0)

def main():
signal.signal(signal.SIGINT, signal_handler)

print("Monitoring TCP connections. Press Ctrl+C to exit.")
while True:
update_tcp_map()
check_time_wait_sockets()

if __name__ == "__main__":
main()
+ +

分析脚本的输出,发现:

+
    +
  1. 在 Linux 上,几乎所有的 TIMEWAIT 请求从容器内发出,并与宿主机的反代建立连接。
  2. +
  3. 在 Linux 上单容器任一时刻均会存在约 1000 TIMEWAIT 请求,这一数量与 Windows 上基本保持一致。
  4. +
  5. 在 Windows 上,TIMEWAIT 分为几乎数量完全相同的两个部分:
      +
    1. 127.0.0.1 发起的,与反代监听端口建立的连接,PID 查看发起进程为 com.docker.backend.exe
    2. +
    3. 由反代进程发起的,与 usbmuxd 监听端口建立的连接
    4. +
    +
  6. +
+

其中第一点符合预期,因为在 Linux 上反代通过 UNIX socket 与 usbmuxd 通信,并不经过 tcp 连接。

+

第二点则提示工程确实存在冗余逻辑,1000 请求量在 120s TIMEWAIT 窗口下代表约 8qps 请求负载,这明显存在逻辑错误,自动化再怎么跑也不应该出现如此高的请求负载。

+

第三点令人感到疑惑:为什么从 docker 容器内发出的请求实际发起 ip 为 127.0.0.1,并且是由 docker 进程发起的?

+

由于 Linux 下,请求的发起 ip 均为 docker bridge 网卡下容器的 ip,可以推断 Linux 下容器内发起的请求不会占用宿主机的动态端口。而如果 Windows 会由 docker 代为发起请求,从现象上看起来确实是会占用宿主机的端口范围,这很有可能是 Linux 上无法复现这个问题的,除了 Linux 上与 usbmuxd 通信不使用 tcp 连接之外的另一个关键原因。

+

至此,可以产生一些阶段性的结论:

+
    +
  1. 工程逻辑一定是存在问题需要优化,8qps 请求压力不合理。
  2. +
  3. 问题大概率是由于端口耗尽问题引起,Linux 由于 docker 容器网络与 Windows 原理不同不占用宿主机端口,同时与 usbmuxd 也不占用端口,因此不存在这个问题。
  4. +
+

接下来,更多的是验证上述结论的正确性,目光转向下列方向:

+
    +
  1. 如果确定是端口耗尽导致的问题,手动构造场景是否可以复现这个问题?
  2. +
  3. 工程是否真的有问题?因为也有可能是发起请求的组件存在问题。
  4. +
  5. bridge 模式在 Windows 上发起请求时的行为是否真的与上述一致?是否可以复现?是否有文档支撑?
  6. +
+

问题复现

有了猜想,想要复现问题其实比较容易,Windows 侧动态端口范围分析 中提到,Windows 默认动态端口范围就是 16384 个,为了减少额外操作干扰,就不改动这个范围,直接手动打 16000 个连接看看问题能不能复现即可。

+

遂随便手搓了一对 server-client 脚本,server 甚至还有没办法退出的 bug(不过管他呢,反正也是复现问题用的,能跑就行)

+

client 可以手动通过 open <number>close <number> 指定打开或关闭多少个连接,从而达到精确控制,免得开得不够多或者开得太多,主打一个灵活。

+
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
"""server"""
import socket
import threading

def handle_client(client_socket):
"""Handles a single client connection."""
try:
while True:
# Keep the connection alive
data = client_socket.recv(1024)
if not data:
break
finally:
client_socket.close()

def start_server():
"""Starts the TCP server."""
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(('0.0.0.0', 23335))
server.listen(5)
print("Server listening on port 23335...")
while True:
client_socket, addr = server.accept()
print(f"Accepted connection from {addr}")
client_handler = threading.Thread(target=handle_client, args=(client_socket,))
client_handler.start()

if __name__ == "__main__":
start_server()
+ +
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
"""client"""
import socket
import sys

class ConnectionManager:
def __init__(self):
self.connections = []
def open_connections(self, number):
"""Open a specified number of TCP connections."""
for _ in range(number):
try:
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# client_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # Allow reuse of the address
client_socket.connect(('127.0.0.1', 23335))
self.connections.append(client_socket)
print(f"Opened connection {len(self.connections)}")
except Exception as e:
print(f"Failed to open connection: {e}")
def close_connections(self, number):
"""Close a specified number of TCP connections."""
to_close = min(number, len(self.connections))
for _ in range(to_close):
client_socket = self.connections.pop()
client_socket.close()
print(f"Closed connection, {len(self.connections)} remaining")

def listen_for_input(connection_manager):
"""Listens for user input to open/close connections."""
while True:
user_input = input("Enter command (open <number> / close <number>): ")
parts = user_input.split()
if len(parts) != 2:
print("Invalid command. Use 'open <number>' or 'close <number>'.")
continue
command, number_str = parts
try:
number = int(number_str)
except ValueError:
print("Please enter a valid number.")
continue
if command == "open":
connection_manager.open_connections(number)
elif command == "close":
connection_manager.close_connections(number)
else:
print("Unknown command. Use 'open' or 'close'.")

if __name__ == "__main__":
connection_manager = ConnectionManager()
try:
listen_for_input(connection_manager)
except KeyboardInterrupt:
print("\nExiting client.")
for conn in connection_manager.connections:
conn.close()
sys.exit(0)
+ +

测试发现,当打开了 16000 个端口左右再全部 close 后,问题确实可以复现。

+

为了交叉验证,在等待一段时间 (大概相当于第一个 open 的端口关闭 2 分钟后)问题自行消失并且在不另开新连接时不再复现。

+

同时,还通过设置 SO_REUSEADDR 标记进一步验证是因为 TIME_WAIT 端口无法被复用导致的问题,在设置该标记后,TIME_WAIT 阶段的端口可以被新的连接复用,实际测试反复开关连接超过 16000 次仍让不会导致问题复现。

+

至此,基本可以肯定问题出现的原因就是短连接过多导致 TIME_WAIT 堆积,最后动态端口耗尽无法创建新的连接使得 wda 请求失败。

+

工程问题排查

由于此处与工程相关逻辑强相关,不详细展开。经过代码 review,发现部分环节存在多次反复获取同一信息的问题,即有一些连续执行的逻辑链路 A-B-C-D-E,其中每一步都会请求 get_necessary_data 方法,由于五个方法分开实现,串联五个方法的代码作者与方法编写人又各不相同,没有注意到这里方法的反复多次冗余调用,最终导致了短时间高频调用的逻辑。暂时通过请求缓存优化后得以解除。

+

此处也佐证了高频的 wda 请求确实与工程的逻辑相关,进一步排除了底层工具链存在问题的可能性。

+

Docker 在 Windows 下的网络驱动行为

在搜索引擎检索相关内容,发现并没有太多相关的内容,个人也没有找到官方有关上述发现的行为相关的文档。通过将 问题复现 中的 client 代码搬到 docker 容器内执行并请求 host.docker.internal 地址,可以复现这种行为,也即容器内与 docker 网卡网关地址建立连接,宿主机上由 docker 发起另一段连接,并不直通,而在 Linux 上执行则可以看到不会有这样的行为。

+

在 Windows 上执行 ipconfig 同样也可以看到不会有 Linux 下执行 ip a 所看到的 docker0 或是 vethX 网卡,进一步推测两个平台上 docker 对于 bridge 网络驱动的实现方案不相同。

+

同时,我也对 gpt 进行了相关问题的询问,下面是 gpt 给出的解答,大家参考着看,不具有权威性:

+
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
[Prompt]

I'm using docker on Windows, and I'm aware of a weird behavior:

When I use host.docker.internal to access the host port, say, I try to connect to a tcp server listening on 0.0.0.0:23333 on host machine, I open a socket inside the container with the container ip:port as one end and host.docker.internal:23333 as the other.

However, when I check the open connections on host, it shows that the just opened socket has 127.0.0.1:another port as on end and 127.0.0.1:23333 as the other. It seems that docker act as an intermediate, accept the connection to host.docker.internal and reopen one on host. Is this real and expected? Why is this behavior different than that on linux?

Also, I've checked ipconfig on Windows and ifconfig on WSL, I can't see any additional interfaces like `veth` or `docker0` or `br-xxx` as these interfaces exists on Linux

------
[Answer]

Yes, your observation about Docker's behavior on Windows is correct and expected due to the differences in how Docker is implemented on Windows compared to Linux.

### Docker Networking on Windows vs. Linux

On Linux, Docker uses the host's kernel features directly, such as namespaces and iptables, to manage containers. It creates virtual network interfaces (like `veth` pairs), bridges (`docker0`), and uses the native Linux networking stack. This setup allows containers to communicate with the host and each other in a manner that closely resembles how separate physical machines would interact on a network.

On Windows, Docker operates differently because Windows does not natively support the same kernel-level features as Linux. Docker on Windows has two main modes: using Hyper-V (with Docker Desktop) or using the Windows Subsystem for Linux (WSL2). Each of these modes handles networking in its own way:

1. **Hyper-V**: When using Docker Desktop with Hyper-V, Docker runs containers inside a lightweight virtual machine. All Docker networking goes through this VM. The `host.docker.internal` DNS name resolves to an internal gateway IP that Docker sets up to route traffic from the container to the host. This is why you see connections appearing to come from `127.0.0.1` on different ports; Docker is essentially NATing (Network Address Translation) the traffic between the container and the host.

2. **WSL2**: In this mode, Docker utilizes a utility VM that runs a real Linux kernel, and Docker containers are managed much like they are on a native Linux system. However, the networking is still handled through a translation layer between Windows and the Linux kernel in WSL2, leading to similar behaviors as seen with Hyper-V.

### Why the Difference?

The primary reason for this difference is the underlying OS architecture and the lack of native container support in Windows. Docker must rely on virtualization and network translation to enable similar functionalities, which inherently changes how networking behaves.

### Lack of `veth`, `docker0`, etc., on Windows

As you've noticed, network interfaces like `veth`, `docker0`, or `br-xxx` are not visible on Windows. This is because these interfaces are part of the Linux networking architecture, which Windows does not natively support. Docker on Windows uses other methods (like creating a virtual network inside Hyper-V or leveraging the network stack in WSL2) to handle container networking.

### Conclusion

The behavior you're seeing is expected given the architectural differences in how Docker operates on Windows versus Linux. Docker must adapt to the underlying operating system's capabilities, leading to different networking setups and behaviors. If you need more consistent behavior across different OSes, you might consider using Linux VMs on Windows or deploying your Docker containers on native Linux systems.
+ +

总结

问题的根因是工程逻辑变更后请求次数异常增加导致,直接原因是由于请求次数大量增加后,Windows 上动态端口耗尽触发了该报错,同时,docker 在 Windows 上的网络行为也进一步加剧了请求数量增长的影响(导致请求数量翻倍)。

+

由于 usbmuxd 和 docker 在 Windows 和 Linux 上行为的差异性导致了这个问题在 Linux 机器上无法复现,同时由于同一时间宿主机上任务负载量不同,导致了在同一个宿主机上也不能够稳定复现。通过手动构造极端场景可以做到稳定复现。

+

解决方案

找到了问题的原因,解决方案也就很明了了:

+
    +
  1. 优先级最高的就是直接干掉工程内部的逻辑了,通过优化掉工程的错误调用可以快速解决目前问题
  2. +
  3. 考虑到后面宿主机仍有负载增长空间,代码编写也仍然不一定完美,可以通过调整 Windows 动态端口范围和直接从容器内访问 usbmuxd 服务来将允许的请求并发量提高大约 4 倍。
  4. +
+

此处额外解释一下第二点中为什么又可以直接从容器内访问 usbmuxd 了:前面发现容器内请求 host.docker.internal 会让 docker 以 com.docker.backend.exe 的身份使用 127.0.0.1 发起一个代理的请求,这还正好满足了 usbmuxd 只监听 127.0.0.1:27015 的行为了,这样宿主机的反代服务也就不需要了。

+

对于 Linux 服务,如果需要保持架构上的统一,一个可能的优化方向是将 usbmuxd 监听的 socket 映射到容器内?考虑到 docker daemon 的 socket 可以做到,大概这个方向也可行,但目前还没有做尝试,就留作一些可行的方案放在此处吧~

+
文章作者: Linloir
文章链接: https://blog.linloir.cn/2024/11/22/debug-windows-socket-drain/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 時痕
\ No newline at end of file diff --git a/archives/2022/07/index.html b/archives/2022/07/index.html index 9af18de..58aa5ab 100644 --- a/archives/2022/07/index.html +++ b/archives/2022/07/index.html @@ -163,7 +163,7 @@ isHome: false, isHighlightShrink: false, isToc: false, - postUpdate: '2024-11-19 14:52:36' + postUpdate: '2024-11-22 16:30:27' }
avatar
Linloir
我、技术、生活与值得分享的一切
Follow Me
最新文章
+
avatar
Linloir
我、技术、生活与值得分享的一切
Follow Me
最新文章
@@ -190,7 +190,7 @@ btf.addGlobalFn('pjaxSend', () => {
  • 2024 - 10 + 11
  • @@ -200,7 +200,7 @@ btf.addGlobalFn('pjaxSend', () => { 4 -
    网站信息
    文章数目 :
    14
    本站总字数 :
    70.4k
    本站访客数 :
    本站总浏览量 :
    最后更新时间 :
    全部文章 - 1
    2022
    操统实验日志 第四章 勇者之路
    avatar
    Linloir
    我、技术、生活与值得分享的一切
    Follow Me
    最新文章
    +
    全部文章 - 1
    2022
    操统实验日志 第四章 勇者之路
    avatar
    Linloir
    我、技术、生活与值得分享的一切
    Follow Me
    最新文章
    @@ -190,7 +190,7 @@ btf.addGlobalFn('pjaxSend', () => {
  • 2024 - 10 + 11
  • @@ -200,7 +200,7 @@ btf.addGlobalFn('pjaxSend', () => { 4 -
    网站信息
    文章数目 :
    14
    本站总字数 :
    70.4k
    本站访客数 :
    本站总浏览量 :
    最后更新时间 :
    avatar
    Linloir
    我、技术、生活与值得分享的一切
    Follow Me
    最新文章
    +
    avatar
    Linloir
    我、技术、生活与值得分享的一切
    Follow Me
    最新文章
    @@ -190,7 +190,7 @@ btf.addGlobalFn('pjaxSend', () => {
  • 2024 - 10 + 11
  • @@ -200,7 +200,7 @@ btf.addGlobalFn('pjaxSend', () => { 4 -
    网站信息
    文章数目 :
    14
    本站总字数 :
    70.4k
    本站访客数 :
    本站总浏览量 :
    最后更新时间 :
    avatar
    Linloir
    我、技术、生活与值得分享的一切
    Follow Me
    最新文章
    +
    avatar
    Linloir
    我、技术、生活与值得分享的一切
    Follow Me
    最新文章
    +
    avatar
    Linloir
    我、技术、生活与值得分享的一切
    Follow Me
    最新文章
    +
    avatar
    Linloir
    我、技术、生活与值得分享的一切
    Follow Me
    最新文章
    @@ -190,7 +190,7 @@ btf.addGlobalFn('pjaxSend', () => {
  • 2024 - 10 + 11
  • @@ -200,7 +200,7 @@ btf.addGlobalFn('pjaxSend', () => { 4 -
    网站信息
    文章数目 :
    14
    本站总字数 :
    70.4k
    本站访客数 :
    本站总浏览量 :
    最后更新时间 :
    全部文章 - 11
    2024
    重生
    \ No newline at end of file diff --git a/archives/index.html b/archives/index.html index 2a740ce..aac204b 100644 --- a/archives/index.html +++ b/archives/index.html @@ -163,7 +163,7 @@ isHome: false, isHighlightShrink: false, isToc: false, - postUpdate: '2024-11-19 14:52:36' + postUpdate: '2024-11-22 16:30:27' }
    avatar
    Linloir
    我、技术、生活与值得分享的一切
    Follow Me
    最新文章
    +
    avatar
    Linloir
    我、技术、生活与值得分享的一切
    Follow Me
    最新文章
    @@ -190,7 +190,7 @@ btf.addGlobalFn('pjaxSend', () => {
  • 2024 - 10 + 11
  • @@ -200,7 +200,7 @@ btf.addGlobalFn('pjaxSend', () => { 4 -
    网站信息
    文章数目 :
    14
    本站总字数 :
    70.4k
    本站访客数 :
    本站总浏览量 :
    最后更新时间 :
    avatar
    Linloir
    我、技术、生活与值得分享的一切
    Follow Me
    最新文章
    +
    avatar
    Linloir
    我、技术、生活与值得分享的一切
    Follow Me
    最新文章
    @@ -190,7 +190,7 @@ btf.addGlobalFn('pjaxSend', () => {
  • 2024 - 10 + 11
  • @@ -200,7 +200,7 @@ btf.addGlobalFn('pjaxSend', () => { 4 -
    网站信息
    文章数目 :
    14
    本站总字数 :
    70.4k
    本站访客数 :
    本站总浏览量 :
    最后更新时间 :
    avatar
    Linloir
    我、技术、生活与值得分享的一切
    Follow Me
    最新文章
    +
    avatar
    Linloir
    我、技术、生活与值得分享的一切
    Follow Me
    最新文章
    @@ -192,7 +192,7 @@ btf.addGlobalFn('pjaxSend', () => {
  • 2024 - 10 + 11
  • @@ -202,7 +202,7 @@ btf.addGlobalFn('pjaxSend', () => { 4 -
    网站信息
    文章数目 :
    14
    本站总字数 :
    70.4k
    本站访客数 :
    本站总浏览量 :
    最后更新时间 :
    avatar
    Linloir
    我、技术、生活与值得分享的一切
    Follow Me
    最新文章
    +
    avatar
    Linloir
    我、技术、生活与值得分享的一切
    Follow Me
    最新文章
    @@ -190,7 +190,7 @@ btf.addGlobalFn('pjaxSend', () => {
  • 2024 - 10 + 11
  • @@ -200,7 +200,7 @@ btf.addGlobalFn('pjaxSend', () => { 4 -
    网站信息
    文章数目 :
    14
    本站总字数 :
    70.4k
    本站访客数 :
    本站总浏览量 :
    最后更新时间 :
    分类 - 技术
    2022
    操统实验日志 第一章 序章
    \ No newline at end of file diff --git a/categories/杂思/index.html b/categories/杂思/index.html index b2ce186..0e12917 100644 --- a/categories/杂思/index.html +++ b/categories/杂思/index.html @@ -163,7 +163,7 @@ isHome: false, isHighlightShrink: false, isToc: false, - postUpdate: '2024-11-19 14:52:36' + postUpdate: '2024-11-22 16:30:27' }
    avatar
    Linloir
    我、技术、生活与值得分享的一切
    Follow Me
    最新文章
    +
    avatar
    Linloir
    我、技术、生活与值得分享的一切
    Follow Me
    最新文章
    @@ -190,7 +190,7 @@ btf.addGlobalFn('pjaxSend', () => {
  • 2024 - 10 + 11
  • @@ -200,7 +200,7 @@ btf.addGlobalFn('pjaxSend', () => { 4 -
    网站信息
    文章数目 :
    14
    本站总字数 :
    70.4k
    本站访客数 :
    本站总浏览量 :
    最后更新时间 :
    avatar
    Linloir
    我、技术、生活与值得分享的一切
    Follow Me
    最新文章
    +
    avatar
    Linloir
    我、技术、生活与值得分享的一切
    Follow Me
    最新文章
    @@ -190,7 +190,7 @@ btf.addGlobalFn('pjaxSend', () => {
  • 2024 - 10 + 11
  • @@ -200,7 +200,7 @@ btf.addGlobalFn('pjaxSend', () => { 4 -
    网站信息
    文章数目 :
    14
    本站总字数 :
    70.4k
    本站访客数 :
    本站总浏览量 :
    最后更新时间 :