<?xml version="1.0" encoding="UTF-8" ?>
<rss version="2.0"
xmlns:content="http://purl.org/rss/1.0/modules/content/"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:atom="http://www.w3.org/2005/Atom"
xmlns:slash="http://purl.org/rss/1.0/modules/slash/">
<channel>
<title>NothAmor - Little Stone</title>
<link>https://nothamor.com/</link>
<description>Just So So ...</description>
<atom:link href="https://nothamor.com/rss.xml" rel="self" type="application/rss+xml" />
<lastBuildDate>Thu, 16 Apr 2026 04:00:27 +0800</lastBuildDate>
<item>
<title>UGREEN AX900 (AIC8800) USB WiFi 网卡 Linux 6.x 折腾全记录</title>
<link>https://nothamor.com/archives/AIC8800.html</link>
<guid isPermaLink="false">https://nothamor.com/archives/AIC8800.html</guid>
<pubDate>Sun, 05 Apr 2026 03:27:00 +0800</pubDate>
<dc:creator>NothAmor</dc:creator>
<description><![CDATA[> 记录从开箱到开箱即用的完整折腾路径，包括内核兼容性修复和开机自动配置。
目录

背景
问题一：驱动编译失败
问题二：每次开机都要手动加载驱动
问题三：为什么要"拔掉再插上"
完整解决方案
总结


背景
硬件：UGREEN AX900 USB WiFi...]]></description>
<content:encoded><![CDATA[
<blockquote>
<p>记录从开箱到开箱即用的完整折腾路径，包括内核兼容性修复和开机自动配置。</p>
</blockquote>
<h2>目录</h2>
<ul>
<li><a href="#背景">背景</a></li>
<li><a href="#问题一驱动编译失败">问题一：驱动编译失败</a></li>
<li><a href="#问题二每次开机都要手动加载驱动">问题二：每次开机都要手动加载驱动</a></li>
<li><a href="#问题三为什么要拔掉再插上">问题三：为什么要"拔掉再插上"</a></li>
<li><a href="#完整解决方案">完整解决方案</a></li>
<li><a href="#总结">总结</a></li>
</ul>
<hr />
<h2>背景</h2>
<p><strong>硬件</strong>：UGREEN AX900 USB WiFi 6 网卡<br />
<strong>芯片</strong>：AICSemi AIC8800DC<br />
<strong>系统</strong>：Ubuntu 24.04 LTS<br />
<strong>内核</strong>：6.17.0-20-generic  </p>
<p>刚装完系统，插入网卡，<strong>没有任何反应</strong>。</p>
<pre><code class="language-bash">$ lsusb | grep -i aic
# 空，什么都没有

$ ip link
# 只有 lo 和有线网卡，没有无线网卡</code></pre>
<p>AIC8800 官方驱动只支持到内核 3.10+，在 Linux 6.x 上编译直接报错。于是开始了折腾之路。</p>
<hr />
<h2>问题一：驱动编译失败</h2>
<h3>现象</h3>
<p>从绿联官网或 AICSemi 下载的驱动源码，直接 <code>make</code> 报错：</p>
<pre><code class="language-bash">$ make
...
error: implicit declaration of function 'del_timer'
error: too few arguments to function 'cfg80211_rx_spurious_frame'
error: initialization of 'int (*)(struct wiphy *, int, u32)' from incompatible pointer type
...</code></pre>
<h3>原因分析</h3>
<p>Linux 6.x 内核有几处 API 变更：</p>
<table>
<thead>
<tr>
<th>API 变更</th>
<th>旧版本</th>
<th>Linux 6.x</th>
</tr>
</thead>
<tbody>
<tr>
<td>定时器</td>
<td><code>del_timer()</code></td>
<td><code>timer_delete()</code></td>
</tr>
<tr>
<td>cfg80211</td>
<td><code>set_wiphy_params(wiphy, changed)</code></td>
<td>增加 <code>radio_idx</code> 参数</td>
</tr>
<tr>
<td>唤醒源</td>
<td><code>wakeup_source_create()</code> + <code>wakeup_source_add()</code></td>
<td><code>wakeup_source_register()</code></td>
</tr>
</tbody>
</table>
<h3>修复方案</h3>
<h4>1. 修复 <code>del_timer</code> API</h4>
<p>在 <code>rwnx_main.c</code>、<code>rwnx_rx.c</code>、<code>aicwf_tcp_ack.c</code> 中添加：</p>
<pre><code class="language-c">#include &lt;linux/version.h&gt;
#include &lt;linux/timer.h&gt;

#if LINUX_VERSION_CODE &gt;= KERNEL_VERSION(6, 0, 0)
#define del_timer(timer) timer_delete(timer)
#define del_timer_sync(timer) timer_delete_sync(timer)
#endif</code></pre>
<h4>2. 修复 <code>cfg80211</code> API</h4>
<p>在 <code>rwnx_main.c</code> 中修改两个函数签名：</p>
<pre><code class="language-c">// set_wiphy_params 增加 radio_idx 参数
static int rwnx_cfg80211_set_wiphy_params(struct wiphy *wiphy,
#if LINUX_VERSION_CODE &gt;= KERNEL_VERSION(6, 0, 0)
                                          int radio_idx,
#endif
                                          u32 changed)

// set_tx_power 增加 radio_idx 参数
static int rwnx_cfg80211_set_tx_power(struct wiphy *wiphy,
#if LINUX_VERSION_CODE &gt;= KERNEL_VERSION(3, 8, 0)
    struct wireless_dev *wdev,
#endif
#if LINUX_VERSION_CODE &gt;= KERNEL_VERSION(6, 0, 0)
    int radio_idx,
#endif
    enum nl80211_tx_power_setting type, int mbm)</code></pre>
<h4>3. 修复 <code>wakeup_source</code> API</h4>
<p>在 <code>rwnx_wakelock.c</code> 中：</p>
<pre><code class="language-c">struct wakeup_source *rwnx_wakeup_init(const char *name)
{
#if LINUX_VERSION_CODE &gt;= KERNEL_VERSION(6, 0, 0)
    return wakeup_source_register(NULL, name);
#else
    struct wakeup_source *ws;
    ws = wakeup_source_create(name);
    wakeup_source_add(ws);
    return ws;
#endif
}</code></pre>
<h4>4. 修复 <code>sprintf</code> 源目标重叠</h4>
<p>在 <code>aicwf_compat_8800d80.c</code> 等文件中：</p>
<pre><code class="language-c">// 错误的写法（GCC 会警告）
sprintf(aic_fw_path, "%s/%s", aic_fw_path, "aic8800DC");

// 正确的写法
{
    char tmp_path[200];
    strncpy(tmp_path, aic_fw_path, sizeof(tmp_path) - 1);
    tmp_path[sizeof(tmp_path) - 1] = '\0';
    snprintf(aic_fw_path, sizeof(aic_fw_path), "%s/%s", tmp_path, "aic8800DC");
}</code></pre>
<h4>5. 修复 <code>from_timer</code> 宏</h4>
<p>在需要使用的文件中添加：</p>
<pre><code class="language-c">#ifndef from_timer
#define from_timer(var, callback_timer, timer_fieldname) \
    container_of(callback_timer, typeof(*var), timer_fieldname)
#endif</code></pre>
<h3>编译安装</h3>
<pre><code class="language-bash">cd aic8800_linux_driver/drivers/aic8800
make clean
make
sudo make install</code></pre>
<p>成功后会生成两个 <code>.ko</code> 文件：</p>
<ul>
<li><code>aic_load_fw/aic_load_fw.ko</code> - 固件加载器</li>
<li><code>aic8800_fdrv/aic8800_fdrv.ko</code> - WiFi 驱动</li>
</ul>
<hr />
<h2>问题二：每次开机都要手动加载驱动</h2>
<h3>现象</h3>
<p>每次重启后：</p>
<pre><code class="language-bash">$ lsmod | grep aic
# 空的，驱动没加载

$ ip link
# 没有无线网卡</code></pre>
<p>必须手动执行：</p>
<pre><code class="language-bash">sudo modprobe cfg80211
sudo modprobe aic_load_fw
sudo modprobe aic8800_fdrv</code></pre>
<h3>原因</h3>
<p>驱动没有配置为开机自动加载。</p>
<h3>解决方案</h3>
<p><strong>方案 1：配置 modules-load.d（推荐）</strong></p>
<pre><code class="language-bash">echo -e "cfg80211\naic_load_fw\naic8800_fdrv" | sudo tee /etc/modules-load.d/aic8800.conf</code></pre>
<p><strong>方案 2：使用 systemd 服务</strong></p>
<p>创建 <code>/etc/systemd/system/aic8800-autoload.service</code>：</p>
<pre><code class="language-ini">[Unit]
Description=AIC8800 USB WiFi Driver
After=systemd-udev-settle.service

[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/sbin/modprobe aic8800_fdrv

[Install]
WantedBy=multi-user.target</code></pre>
<p>启用服务：</p>
<pre><code class="language-bash">sudo systemctl daemon-reload
sudo systemctl enable aic8800-autoload.service</code></pre>
<hr />
<h2>问题三：为什么要"拔掉再插上"</h2>
<h3>现象</h3>
<p>按照网上教程，加载驱动后要：</p>
<ol>
<li><code>lsmod | grep aic</code> 确认驱动加载成功</li>
<li><strong>拔掉</strong>网卡</li>
<li><strong>重新插入</strong>网卡</li>
<li>网卡才能工作</li>
</ol>
<h3>根本原因：USB 网卡的双模式设计</h3>
<p>AIC8800 芯片的 USB WiFi 网卡采用<strong>双模式设计</strong>：</p>
<table>
<thead>
<tr>
<th>模式</th>
<th>VID:PID</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>存储模式</strong></td>
<td><code>a69c:5721</code></td>
<td>插入时默认模式，系统识别为 U 盘（用于 Windows 自动安装驱动）</td>
</tr>
<tr>
<td><strong>WiFi 模式</strong></td>
<td><code>a69c:8800</code></td>
<td>实际工作的无线网卡模式</td>
</tr>
</tbody>
</table>
<h3>工作流程</h3>
<pre><code>插入网卡
    ↓
系统识别为 USB 存储设备 (a69c:5721)
    ↓
执行 eject /dev/aicudisk（弹出存储设备）
    ↓
网卡自动重新枚举为 WiFi 设备 (a69c:8800)
    ↓
驱动识别并初始化设备
    ↓
网卡正常工作</code></pre>
<h3>为什么必须先加载驱动再插网卡？</h3>
<p><strong>驱动加载顺序问题</strong>：</p>
<ul>
<li>
<p><strong>如果先插网卡，后加载驱动</strong>：</p>
<ul>
<li>网卡处于存储模式 (<code>a69c:5721</code>)</li>
<li>驱动加载时寻找 WiFi 设备 (<code>a69c:8800</code>)，找不到</li>
<li>驱动初始化失败或无法绑定设备</li>
</ul>
</li>
<li>
<p><strong>正确的顺序</strong>：</p>
<ol>
<li>先加载驱动（注册 USB 设备监听）</li>
<li>插入网卡 → 触发 udev 规则 → 自动执行 <code>eject</code> 切换模式</li>
<li>网卡切换到 WiFi 模式 → 驱动识别并绑定</li>
</ol>
</li>
</ul>
<h3>手动操作流程</h3>
<pre><code class="language-bash"># 1. 加载驱动（等待设备）
sudo modprobe cfg80211
sudo modprobe aic_load_fw
sudo modprobe aic8800_fdrv

# 2. 确认驱动已加载
lsmod | grep aic
# aic8800_fdrv          696320  0
# aic_load_fw            94208  1 aic8800_fdrv

# 3. 插入网卡（udev 自动处理模式切换）
# 或者手动切换：sudo eject /dev/aicudisk

# 4. 查看网卡
ip link
# wlan0: &lt;BROADCAST,MULTICAST&gt; mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000</code></pre>
<hr />
<h2>完整解决方案</h2>
<h3>一键配置开机自动加载</h3>
<p>我已经创建了完整的自动配置脚本：</p>
<pre><code class="language-bash">cd aic8800_linux_driver/tools/autostart
sudo ./setup-autostart.sh</code></pre>
<p>这个脚本会自动完成：</p>
<ol>
<li><strong>安装固件</strong>到 <code>/lib/firmware/aic8800D80/</code></li>
<li><strong>配置 udev 规则</strong>：<ul>
<li>检测到存储模式 (<code>a69c:5721</code>) → 自动 <code>eject</code> 切换模式</li>
<li>检测到 WiFi 模式 (<code>a69c:8800</code>) → 自动加载驱动</li>
</ul>
</li>
<li><strong>配置模块开机加载</strong>：<code>cfg80211</code>、<code>aic_load_fw</code>、<code>aic8800_fdrv</code></li>
<li><strong>创建 systemd 服务</strong>：处理开机时网卡已插入的情况</li>
</ol>
<p>然后<strong>重启电脑</strong>，网卡就能自动识别了。</p>
<h3>卸载自动配置</h3>
<p>如果需要恢复手动操作：</p>
<pre><code class="language-bash">sudo ./remove-autostart.sh</code></pre>
<hr />
<h2>验证</h2>
<h3>检查网卡是否识别</h3>
<pre><code class="language-bash">$ lsusb | grep a69c
Bus 001 Device 007: ID a69c:8800 AICSemi AIC8800DC
# 注意 VID:PID 是 8800（WiFi 模式），不是 5721（存储模式）</code></pre>
<h3>检查驱动是否加载</h3>
<pre><code class="language-bash">$ lsmod | grep aic
aic8800_fdrv          696320  0
aic_load_fw            94208  1 aic8800_fdrv
cfg80211             1462272  3 b43,mac80211,aic8800_fdrv</code></pre>
<h3>检查网络接口</h3>
<pre><code class="language-bash">$ ip link
3: wlan0: &lt;BROADCAST,MULTICAST,UP,LOWER_UP&gt; mtu 1500 qdisc mq state UP mode DORMANT group default qlen 1000
    link/ether xx:xx:xx:xx:xx:xx brd ff:ff:ff:ff:ff:ff</code></pre>
<h3>连接 WiFi</h3>
<pre><code class="language-bash">nmcli device wifi list
nmcli device wifi connect "SSID" password "password"</code></pre>
<hr />
<h2>总结</h2>
<table>
<thead>
<tr>
<th>问题</th>
<th>原因</th>
<th>解决方案</th>
</tr>
</thead>
<tbody>
<tr>
<td>编译失败</td>
<td>Linux 6.x API 变更</td>
<td>修改 9 个文件，添加兼容性宏</td>
</tr>
<tr>
<td>开机不自动加载</td>
<td>未配置模块自动加载</td>
<td>配置 <code>modules-load.d</code> 和 systemd 服务</td>
</tr>
<tr>
<td>需要拔插</td>
<td>USB 双模式设计，需要切换模式</td>
<td>udev 规则自动处理模式切换</td>
</tr>
</tbody>
</table>
<p><strong>最终效果</strong>：</p>
<ul>
<li>开机自动识别网卡，无需手动操作</li>
<li>热插拔支持，随时插拔都能自动识别</li>
<li>适配 Linux 6.x 内核</li>
</ul>
<hr />
<h2>参考</h2>
<ul>
<li>修复后的驱动源码：<a href="https://github.com/NothAmor/ugreen-ax900-6.x-kernel-fix-ver">ugreen-ax900-6.x-kernel-fix-ver</a></li>
<li>AICSemi 官方驱动（旧版）</li>
<li>Linux 内核 API 变更文档</li>
</ul>
<hr />
<p><em>Ubuntu 24.04 + 内核 6.17.0</em></p>
]]></content:encoded>
<slash:comments>0</slash:comments>
<comments>https://nothamor.com/archives/AIC8800.html#comments</comments>
<enclosure url="https://cdn.nothamor.com/usr/uploads/2026/04/3783647151.jpg" length="0" type="image/jpeg" />
</item>
<item>
<title>深入浅出 Golang Runtime 源码分析</title>
<link>https://nothamor.com/archives/166.html</link>
<guid isPermaLink="false">https://nothamor.com/archives/166.html</guid>
<pubDate>Mon, 02 Mar 2026 13:10:00 +0800</pubDate>
<dc:creator>NothAmor</dc:creator>
<category><![CDATA[技术]]></category>
<description><![CDATA[# Golang Runtime 源码深度解析：从调度器到内存管理
引言
Go 语言以其简洁的语法和强大的并发能力赢得了众多开发者的青睐。然而，Go 的魅力远不止于此——其内置的 runtime 系统才是支撑这一切的核心引擎。本文将深入 Go runtim...]]></description>
<content:encoded><![CDATA[
<h1>Golang Runtime 源码深度解析：从调度器到内存管理</h1>
<h2>引言</h2>
<p>Go 语言以其简洁的语法和强大的并发能力赢得了众多开发者的青睐。然而，Go 的魅力远不止于此——其内置的 runtime 系统才是支撑这一切的核心引擎。本文将深入 Go runtime 的源码，带你一探究竟，理解这个精巧系统如何高效地管理 goroutine、内存分配和垃圾回收。</p>
<h2>1. Go Runtime 架构概览</h2>
<p>Go runtime 是 Go 程序运行时的核心组件，它负责：</p>
<ul>
<li>Goroutine 调度</li>
<li>内存分配与垃圾回收</li>
<li>网络轮询</li>
<li>系统调用封装</li>
<li>栈管理</li>
</ul>
<p>Runtime 的源码主要位于 <code>src/runtime</code> 目录下，其中最关键的几个文件包括：</p>
<ul>
<li><code>proc.go</code> - 调度器实现</li>
<li><code>mheap.go</code> - 内存分配器</li>
<li><code>mgc.go</code> - 垃圾回收器</li>
<li><code>netpoll.go</code> - 网络轮询器</li>
</ul>
<h2>2. Goroutine 调度器：GMP 模型</h2>
<h3>2.1 什么是 GMP？</h3>
<p>Go 的调度器采用 GMP 模型：</p>
<ul>
<li><strong>G (Goroutine)</strong>: 用户级轻量级线程</li>
<li><strong>M (Machine)</strong>: 操作系统线程</li>
<li><strong>P (Processor)</strong>: 逻辑处理器，包含 goroutine 队列</li>
</ul>
<p>这种设计巧妙地解决了传统线程模型的性能瓶颈问题。</p>
<h3>2.2 调度器工作原理</h3>
<p>当一个 goroutine 被创建时，它会被放入某个 P 的本地队列中。M 线程会从关联的 P 中获取 goroutine 来执行。如果本地队列为空，M 会尝试从其他 P 的队列中"偷取"工作（work-stealing）。</p>
<p>关键数据结构：</p>
<pre><code class="language-go">// G 结构体（简化版）
type g struct {
    stack       stack          // goroutine 栈
    sched       gobuf          // 调度信息
    goid        int64          // goroutine ID
    status      uint32         // 状态
}

// M 结构体（简化版）
type m struct {
    g0          *g             // 用于调度的特殊 goroutine
    curg        *g             // 当前正在执行的 goroutine
    p           puintptr       // 关联的 P
}

// P 结构体（简化版）
type p struct {
    runq        [256]guintptr  // 本地运行队列
    runnext     guintptr       // 下一个要运行的 goroutine
}</code></pre>
<h3>2.3 调度时机</h3>
<p>Go 调度器在以下时机进行调度：</p>
<ul>
<li><strong>系统调用</strong>：当 goroutine 进行系统调用时，M 可能会被阻塞，此时 runtime 会将 P 交给其他 M</li>
<li><strong>channel 操作</strong>：当 goroutine 在 channel 上阻塞时</li>
<li><strong>网络 I/O</strong>：通过 netpoller 实现非阻塞 I/O</li>
<li><strong>主动让出</strong>：通过 <code>runtime.Gosched()</code> 主动让出 CPU</li>
</ul>
<h2>3. 内存分配器</h2>
<p>Go 的内存分配器借鉴了 TCMalloc 的设计思想，采用多级缓存策略来减少锁竞争。</p>
<h3>3.1 内存分配层级</h3>
<p>Go 的内存分配分为三个层级：</p>
<ol>
<li><strong>mcache</strong>: 每个 P 对应一个 mcache，用于小对象分配，无锁</li>
<li><strong>mcentral</strong>: 全局中心缓存，按大小类组织，有锁但粒度较小</li>
<li><strong>mheap</strong>: 全局堆，管理大块内存和虚拟地址空间</li>
</ol>
<h3>3.2 分配流程</h3>
<p>当需要分配内存时：</p>
<ol>
<li>首先检查 mcache 中对应大小类的 span 是否有空闲对象</li>
<li>如果没有，从 mcentral 获取一个 span</li>
<li>如果 mcentral 也没有，从 mheap 分配新的 span</li>
</ol>
<p>关键函数 <code>mallocgc</code> 的简化逻辑：</p>
<pre><code class="language-go">func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {
    if size &lt;= maxSmallSize {
        // 小对象分配
        if noscan &amp;&amp; size &lt; maxTinySize {
            // 微小对象分配（&lt;16字节）
            return tinyAlloc(size, needzero)
        }
        // 小对象分配（16字节-32KB）
        return smallAlloc(size, needzero)
    }
    // 大对象分配（&gt;32KB）
    return largeAlloc(size, needzero)
}</code></pre>
<h2>4. 垃圾回收器</h2>
<p>Go 采用三色标记清除算法，实现了低延迟的并发垃圾回收。</p>
<h3>4.1 三色标记法</h3>
<ul>
<li><strong>白色</strong>: 未被访问的对象，垃圾回收后会被回收</li>
<li><strong>灰色</strong>: 已被访问但其引用对象未被扫描的对象</li>
<li><strong>黑色</strong>: 已被访问且其引用对象也已扫描的对象</li>
</ul>
<h3>4.2 GC 流程</h3>
<p>Go 的 GC 分为四个阶段：</p>
<ol>
<li><strong>Sweep Termination</strong>: 清理上一轮 GC 的残留</li>
<li><strong>Mark</strong>: 并发标记阶段，将可达对象标记为黑色</li>
<li><strong>Mark Termination</strong>: 完成标记，重新扫描栈</li>
<li><strong>Sweep</strong>: 并发清理白色对象</li>
</ol>
<h3>4.3 写屏障</h3>
<p>为了保证并发标记的正确性，Go 使用了 Dijkstra 写屏障：</p>
<pre><code class="language-go">// 写屏障伪代码
func writePointer(slot *unsafe.Pointer, ptr unsafe.Pointer) {
    *slot = ptr
    if gcphase == _GCmark || gcphase == _GCmarktermination {
        gcWriteBarrier(ptr)
    }
}</code></pre>
<p>写屏障确保在 GC 过程中，任何新建立的引用都会被正确标记。</p>
<h2>5. 栈管理</h2>
<p>Go 的 goroutine 栈是动态增长的，初始栈大小很小（2KB），根据需要自动扩展。</p>
<h3>5.1 栈扩容</h3>
<p>当函数调用需要更多栈空间时，runtime 会：</p>
<ol>
<li>分配更大的栈</li>
<li>复制原有栈内容到新栈</li>
<li>更新所有指向旧栈的指针</li>
</ol>
<h3>5.2 栈收缩</h3>
<p>在 Go 1.3 之前，栈只会增长不会收缩。从 Go 1.4 开始，引入了栈收缩机制，当栈使用率低于 1/4 时会尝试收缩。</p>
<h2>6. 网络轮询器</h2>
<p>Go 的网络轮询器基于 epoll (Linux)、kqueue (macOS) 或 IOCP (Windows) 实现，将阻塞的网络 I/O 转换为非阻塞操作。</p>
<h3>6.1 netpoller 工作原理</h3>
<ol>
<li>当 goroutine 执行网络读写时，如果数据不可用，会将 goroutine 挂起</li>
<li>netpoller 监控文件描述符的可读/可写状态</li>
<li>当文件描述符就绪时，netpoller 唤醒对应的 goroutine</li>
</ol>
<h3>6.2 关键数据结构</h3>
<pre><code class="language-go">// pollDesc 结构体（简化版）
type pollDesc struct {
    link   *pollDesc     // 链表链接
    fd     uintptr       // 文件描述符
    rg     guintptr      // 读等待者
    wg     guintptr      // 写等待者
}</code></pre>
<h2>7. 性能优化技巧</h2>
<p>基于对 runtime 的理解，我们可以写出更高效的 Go 代码：</p>
<h3>7.1 减少内存分配</h3>
<ul>
<li>使用 sync.Pool 复用对象</li>
<li>预分配切片容量</li>
<li>避免不必要的字符串转换</li>
</ul>
<h3>7.2 优化 goroutine 使用</h3>
<ul>
<li>避免创建过多的 goroutine</li>
<li>使用 worker pool 模式限制并发数</li>
<li>及时关闭不再需要的 goroutine</li>
</ul>
<h3>7.3 GC 调优</h3>
<ul>
<li>通过 <code>GOGC</code> 环境变量调整 GC 频率</li>
<li>使用 <code>debug.SetGCPercent()</code> 动态调整</li>
<li>监控 GC 统计信息：<code>runtime.ReadMemStats()</code></li>
</ul>
<h2>结论</h2>
<p>Go runtime 是一个精巧而复杂的系统，它通过 GMP 调度模型、分层内存分配、并发垃圾回收等机制，为我们提供了高效的并发编程体验。理解 runtime 的工作原理不仅能满足我们的好奇心，更能帮助我们写出更高效、更可靠的 Go 代码。</p>
<p>正如 Rob Pike 所说："Concurrency is not parallelism, it's about composition." Go runtime 正是这种哲学的最佳体现——它让我们能够优雅地组合并发原语，构建复杂的并发系统。</p>
<h2>参考资料</h2>
<ul>
<li>Go 源码: <a href="https://github.com/golang/go/tree/master/src/runtime">https://github.com/golang/go/tree/master/src/runtime</a></li>
<li>"Go 语言学习笔记" - 雨痕</li>
<li>"Go 程序设计语言" - Alan A. A. Donovan &amp; Brian W. Kernighan</li>
<li>Go Blog: <a href="https://blog.golang.org/">https://blog.golang.org/</a></li>
</ul>
]]></content:encoded>
<slash:comments>0</slash:comments>
<comments>https://nothamor.com/archives/166.html#comments</comments>
<enclosure url="https://cdn.nothamor.com/usr/uploads/2026/03/3667465910.jpg" length="0" type="image/jpeg" />
</item>
<item>
<title>oh-my-zsh默认主题显示全路径</title>
<link>https://nothamor.com/archives/156.html</link>
<guid isPermaLink="false">https://nothamor.com/archives/156.html</guid>
<pubDate>Thu, 29 Aug 2024 17:38:00 +0800</pubDate>
<dc:creator>NothAmor</dc:creator>
<category><![CDATA[技术]]></category>
<description><![CDATA[网上查的让我改PWD，但是改PWD只能一直显示一个路径，不会随当前的目录改变而改变
搞了一下分享出来
打开终端，输入如下指令
vi ~/.oh-my-zsh/themes/robbyrussell.zsh-theme
进入后按99dd，然后按i键后，粘贴如...]]></description>
<content:encoded><![CDATA[
<p>网上查的让我改PWD，但是改PWD只能一直显示一个路径，不会随当前的目录改变而改变</p>
<p>搞了一下分享出来</p>
<p>打开终端，输入如下指令</p>
<pre><code>vi ~/.oh-my-zsh/themes/robbyrussell.zsh-theme</code></pre>
<p>进入后按99dd，然后按i键后，粘贴如下内容</p>
<pre><code>PROMPT="%(?:%{$fg_bold[green]%}%1{➜%} :%{$fg_bold[red]%}%1{➜%} ) %{$fg[cyan]%}%~%{$reset_color%}"
PROMPT+=' $(git_prompt_info)'

ZSH_THEME_GIT_PROMPT_PREFIX="%{$fg_bold[blue]%}git:(%{$fg[red]%}"
ZSH_THEME_GIT_PROMPT_SUFFIX="%{$reset_color%} "
ZSH_THEME_GIT_PROMPT_DIRTY="%{$fg[blue]%}) %{$fg[yellow]%}%1{✗%}"
ZSH_THEME_GIT_PROMPT_CLEAN="%{$fg[blue]%})"</code></pre>
]]></content:encoded>
<slash:comments>0</slash:comments>
<comments>https://nothamor.com/archives/156.html#comments</comments>
<enclosure url="https://nothamor-1251700120.cos.ap-shanghai.myqcloud.com/2024/08/29/1724924302.jpg" length="0" type="image/jpeg" />
</item>
<item>
<title>Golang时间轮精简实现</title>
<link>https://nothamor.com/archives/golangTimeWheel.html</link>
<guid isPermaLink="false">https://nothamor.com/archives/golangTimeWheel.html</guid>
<pubDate>Sat, 03 Aug 2024 15:23:00 +0800</pubDate>
<dc:creator>NothAmor</dc:creator>
<category><![CDATA[技术]]></category>
<description><![CDATA[**文章背景**
最近遇到一个业务上的问题，用户进行一个操作，会同时生成两个kafka消息
在一个消费者消费消息的时候依赖另一个消费者生产的数据
被依赖方执行的速度比依赖方的慢，所以希望延迟一点消费这条数据
处理方法
kafka生产消息的时候可以加入一个d...]]></description>
<content:encoded><![CDATA[
<p><strong>文章背景</strong></p>
<p>最近遇到一个业务上的问题，用户进行一个操作，会同时生成两个kafka消息</p>
<p>在一个消费者消费消息的时候依赖另一个消费者生产的数据</p>
<p>被依赖方执行的速度比依赖方的慢，所以希望延迟一点消费这条数据</p>
<p><strong>处理方法</strong></p>
<p>kafka生产消息的时候可以加入一个delay参数，用于控制消息的延迟消费</p>
<p>但是这里的问题是生产者面对非常多的消费者，加入这个参数风险不可控</p>
<p>所以决定在希望延迟消费的消费者这里加入一个时间轮，用于实现延迟消费的功能</p>
<p>所以有了这篇文章</p>
<p><strong>时间轮代码</strong></p>
<pre><code class="language-go">package timewheel

import (
    "container/list"
    "fmt"
    "sync"
    "time"
)

type Timer struct {
    expiration time.Time
    task       func()
}

type TimeWheel struct {
    ticker      *time.Ticker  // 定时器
    slots       []*list.List  // 时间槽
    currentSlot int           // 当前时间槽
    slotCount   int           // 时间槽数量
    duration    time.Duration // 时间槽间隔
    lock        sync.Mutex    // 锁
}

// NewTimeWheel 创建时间轮
func NewTimeWheel(slotCount int, duration time.Duration) *TimeWheel {
    slots := make([]*list.List, slotCount)
    for i := range slots {
        slots[i] = list.New()
    }
    return &amp;TimeWheel{
        ticker:      time.NewTicker(duration),
        slots:       slots,
        slotCount:   slotCount,
        duration:    duration,
        currentSlot: 0,
    }
}

// AddTask 添加一个定时任务到时间轮
func (tw *TimeWheel) AddTask(delay time.Duration, task func()) {
    tw.lock.Lock()
    defer tw.lock.Unlock()

    expiration := time.Now().Add(delay)

    // 计算定时任务在时间轮中的到期时间, 添加到对应的时间槽
    ticks := int(delay / tw.duration)
    slotIndex := (tw.currentSlot + ticks) % tw.slotCount

    timer := &amp;Timer{expiration: expiration, task: task}
    tw.slots[slotIndex].PushBack(timer)
}

// Start 启动时间轮
func (tw *TimeWheel) Start() {
    go func() {
        defer func() {
            if err := recover(); err != nil {
                fmt.Println("timeWheel panic: ", err)
            }
        }()
        for range tw.ticker.C {
            tw.tickHandler()
        }
    }()
}

// Stop 停止时间轮
func (tw *TimeWheel) Stop() {
    tw.ticker.Stop()
}

func (tw *TimeWheel) tickHandler() {
    tw.lock.Lock()
    defer tw.lock.Unlock()

    slot := tw.slots[tw.currentSlot]
    tw.currentSlot = (tw.currentSlot + 1) % tw.slotCount

    for e := slot.Front(); e != nil; {
        next := e.Next()
        timer := e.Value.(*Timer)
        if timer.expiration.Before(time.Now()) || timer.expiration.Equal(time.Now()) {
            go timer.task()
            slot.Remove(e)
        }
        e = next
    }
}


</code></pre>
]]></content:encoded>
<slash:comments>2</slash:comments>
<comments>https://nothamor.com/archives/golangTimeWheel.html#comments</comments>
<enclosure url="https://nothamor-1251700120.cos.ap-shanghai.myqcloud.com/2024/08/03/1722669785.jpg" length="0" type="image/jpeg" />
</item>
<item>
<title>2024可用的docker镜像源</title>
<link>https://nothamor.com/archives/docker2024.html</link>
<guid isPermaLink="false">https://nothamor.com/archives/docker2024.html</guid>
<pubDate>Tue, 02 Jul 2024 10:38:00 +0800</pubDate>
<dc:creator>NothAmor</dc:creator>
<category><![CDATA[技术]]></category>
<description><![CDATA[docker daemon.json配置：
{
    "registry-mirrors": [
        "https://docker.1panel.live",
        "https://hub.rat.dev"
    ]
}...]]></description>
<content:encoded><![CDATA[
<p>docker daemon.json配置：</p>
<pre><code>{
    "registry-mirrors": [
        "https://docker.1panel.live",
        "https://hub.rat.dev"
    ]
}</code></pre>
]]></content:encoded>
<slash:comments>2</slash:comments>
<comments>https://nothamor.com/archives/docker2024.html#comments</comments>
<enclosure url="https://nothamor-1251700120.cos.ap-shanghai.myqcloud.com/2024/07/02/1719887898.jpg" length="0" type="image/jpeg" />
</item>
<item>
<title>Golang八股面试题+大厂面经</title>
<link>https://nothamor.com/archives/144.html</link>
<guid isPermaLink="false">https://nothamor.com/archives/144.html</guid>
<pubDate>Thu, 11 Jan 2024 17:13:00 +0800</pubDate>
<dc:creator>NothAmor</dc:creator>
<category><![CDATA[技术]]></category>
<description><![CDATA[Golang面试题
https://nothamor-1251700120.cos.ap-shanghai.myqcloud.com/Go%E9%9D%A2%E8%AF%95%E9%A2%98.pdf
Golang大厂面经
https://nothamor...]]></description>
<content:encoded><![CDATA[
<p>Golang面试题</p>
<p><a href="https://nothamor-1251700120.cos.ap-shanghai.myqcloud.com/Go%E9%9D%A2%E8%AF%95%E9%A2%98.pdf">https://nothamor-1251700120.cos.ap-shanghai.myqcloud.com/Go%E9%9D%A2%E8%AF%95%E9%A2%98.pdf</a></p>
<p>Golang大厂面经</p>
<p><a href="https://nothamor-1251700120.cos.ap-shanghai.myqcloud.com/Go%20%E5%A4%A7%E5%8E%82%E9%9D%A2%E7%BB%8F.zip">https://nothamor-1251700120.cos.ap-shanghai.myqcloud.com/Go%20%E5%A4%A7%E5%8E%82%E9%9D%A2%E7%BB%8F.zip</a></p>
]]></content:encoded>
<slash:comments>3</slash:comments>
<comments>https://nothamor.com/archives/144.html#comments</comments>
<enclosure url="https://nothamor-1251700120.cos.ap-shanghai.myqcloud.com/2024/01/11/1704964373.jpg" length="0" type="image/jpeg" />
</item>
<item>
<title>Golang Gin框架中实现Server Sent Events(SSE)</title>
<link>https://nothamor.com/archives/golangGinSSE.html</link>
<guid isPermaLink="false">https://nothamor.com/archives/golangGinSSE.html</guid>
<pubDate>Tue, 02 Jan 2024 10:18:00 +0800</pubDate>
<dc:creator>NothAmor</dc:creator>
<category><![CDATA[技术]]></category>
<description><![CDATA[被这个功能困扰了好久，一直没实现，因为网上的Gin框架实现的SSE都非常复杂，写成了一整套复杂的中间件，经过研究，写出了一套精简的实现。
SSE接口代码, 发送信息修改sendEvents内代码即可：
func ChatStreamSSE(ctx cont...]]></description>
<content:encoded><![CDATA[
<p>被这个功能困扰了好久，一直没实现，因为网上的Gin框架实现的SSE都非常复杂，写成了一整套复杂的中间件，经过研究，写出了一套精简的实现。</p>
<p>SSE接口代码, 发送信息修改sendEvents内代码即可：</p>
<pre><code>func ChatStreamSSE(ctx context.Context, c *gin.Context, req vo.ChatCompletionRequest) {
event := Event{
    ConversationId: uuid.New().String(),
    IsFinish:       false,
    IsReasoning:    false,
}

stream, err := openai.NewChatCompletionStream(ctx, req)
if err != nil {
    logger.Ex(ctx, tag, "NewChatCompletionStream:%+v", err)
    event.Content = fmt.Sprintf("new chat completion stream failed. err:[%+v]", err)
    event.IsFinish = true
    c.SSEvent("data", event)
    c.Writer.Flush()
    return
}
defer stream.Close()

for {
    streamData, streamErr := stream.Recv()
    if streamErr != nil &amp;&amp; !errors.Is(streamErr, io.EOF) &amp;&amp; streamErr.Error() != "EOF" {
        logger.Ex(ctx, tag, "stream data error: %v", streamErr)
        event.Content = fmt.Sprintf("stream data error: %v", streamErr)
        event.IsFinish = true
        c.SSEvent("data", event)
        c.Writer.Flush()
    }
    c.SSEvent("data", event)
    c.Writer.Flush()
}

return
}</code></pre>
]]></content:encoded>
<slash:comments>2</slash:comments>
<comments>https://nothamor.com/archives/golangGinSSE.html#comments</comments>
<enclosure url="https://nothamor-1251700120.cos.ap-shanghai.myqcloud.com/2024/01/02/1704162847.png" length="0" type="image/jpeg" />
</item>
<item>
<title>驾校一点通 - 理论考试爬虫</title>
<link>https://nothamor.com/archives/DriverLicenseQuestionsSpider.html</link>
<guid isPermaLink="false">https://nothamor.com/archives/DriverLicenseQuestionsSpider.html</guid>
<pubDate>Mon, 12 Jul 2021 19:04:00 +0800</pubDate>
<dc:creator>NothAmor</dc:creator>
<category><![CDATA[技术]]></category>
<description><![CDATA[[github repo="NothAmor/DriverLicenseQuestionsSpider" /]
因为写过了很多爬虫项目，临近科目四考试，在驾考宝典刷题的时候就在想能不能把这些题目全都爬取下来，百度搜了一下发现了这个驾校一点通，因为有题目接口...]]></description>
<content:encoded><![CDATA[
<p>[github repo="NothAmor/DriverLicenseQuestionsSpider" /]</p>
<p>因为写过了很多爬虫项目，临近科目四考试，在驾考宝典刷题的时候就在想能不能把这些题目全都爬取下来，百度搜了一下发现了这个驾校一点通，因为有题目接口，而且完全没有反爬机制，一晚上就搞定了</p>
<p>直接存储所有题目到mysql数据库里，图片存储到本地文件夹内，如果这个文件夹在网站内，可以直接修改imageurl为网址存储到数据库里</p>
<p>然后写完了之后我也不知道有啥用，水篇博客吧</p>
]]></content:encoded>
<slash:comments>1</slash:comments>
<comments>https://nothamor.com/archives/DriverLicenseQuestionsSpider.html#comments</comments>
<enclosure url="https://nothamor-1251700120.cos.ap-shanghai.myqcloud.com/blog/typecho/E5cm9EPXIAMkyoi.jpg" length="0" type="image/jpeg" />
</item>
<item>
<title>不支持M.2接口的旧主板使用M.2 SSD作为开机启动盘的三种方法</title>
<link>https://nothamor.com/archives/old_masterboard_use_nvme.html</link>
<guid isPermaLink="false">https://nothamor.com/archives/old_masterboard_use_nvme.html</guid>
<pubDate>Fri, 18 Jun 2021 19:34:00 +0800</pubDate>
<dc:creator>NothAmor</dc:creator>
<category><![CDATA[技术]]></category>
<description><![CDATA[## 前言
之所以会有这个文章是因为云计算赛项中获得了国赛二等奖, 发了奖金就给老电脑更新一点配件, 暂时买了一个NVME M.2转换PCI-E的转接卡,  金士顿骇客神条DDR3 8GB 1600 * 2, 希捷2TB 5900转硬盘, 影驰256GB ...]]></description>
<content:encoded><![CDATA[
<h2>前言</h2>
<p>之所以会有这个文章是因为云计算赛项中获得了国赛二等奖, 发了奖金就给老电脑更新一点配件, 暂时买了一个NVME M.2转换PCI-E的转接卡,  金士顿骇客神条DDR3 8GB 1600 * 2, 希捷2TB 5900转硬盘, 影驰256GB NVME M.2固态硬盘</p>
<h2>起因</h2>
<p>最初我以为旧电脑中, 虽然没有NVME M.2的接口, 但是买一个转接卡转给PCI-E作为启动盘, 是没有什么问题的, 但是当我给SSD装好系统, 插上主板之后, 发现BIOS中无法选择PCI-E上的硬盘作为启动设备, 特征为不认盘, 就是在启动菜单中不显示PCI-E槽中的SSD, 经过笔者一整天的思考, 发现了三种方法解决这一个问题, 让没有M.2接口的旧主板作为启动设备</p>
<h2>三种解决方案</h2>
<ol>
<li>使用Clover这一类boot工具, 在进入boot manager后再选择SSD进行启动</li>
<li>手动修改主板BIOS, 导入NVME模块, 使BIOS支持PCI-E硬盘引导</li>
<li>将M.2 SSD转为USB, 开机使用USB作为启动设备</li>
</ol>
<h2>解决方案一: Clover</h2>
<p>这里可以直接参考这个大佬的文章, 写的很详细</p>
<p><a href="https://www.bilibili.com/read/cv5496032/">【教程】使用Clover启动Nvme协议的固态硬盘 - 哔哩哔哩 (bilibili.com)</a></p>
<h2>解决方案二: 更新主板BIOS</h2>
<p><strong>事先提醒, 刷BIOS有风险</strong></p>
<p>参考文章: <a href="https://www.52pojie.cn/thread-1136840-1-1.html">mmtool——BIOS修改工具及添加nvme模块教程 - 『精品软件区』 - 吾爱破解 - LCG - LSG |安卓破解|病毒分析|www.52pojie.cn</a></p>
<p>如果你的主板是B85-PLUS R2.0, 那么可以用我在下面提供的BIOS进行操作, 这个是我亲测没问题的</p>
<p><a href="https://cdn.nothamor.com/B85-PLUS-R2.0-NVME(Z97).zip">https://cdn.nothamor.com/B85-PLUS-R2.0-NVME(Z97).zip</a></p>
<p>这个包的操作方法:</p>
<ol>
<li>
<p>准备一个空U盘, 新建一个BIOS文件夹, 将包中的三个文件放入其中</p>
</li>
<li>
<p>准备一个PE启动U盘/DVD, 进入纯DOS模式(一定要纯DOS模式, 大白菜就有纯DOS)</p>
</li>
<li>
<p>进入纯DOS模式后, 按照以下操作</p>
<p>首先进入C盘, 输入: <code>C:\</code>后回车</p>
<p>输入 <code>dir</code>后回车, 查看C盘下文件, 如果有BIOS文件夹, 则是正确, 如果没有BIOS文件夹, 依次输入 <code>D:\</code>等, 直到你找到BIOS文件夹</p>
<p>输入 <code>cd BIOS</code>回车进入BIOS文件夹下, 再输入 <code>dir</code>查看当前目录下的所有文件, 如果有 <code>AFUDOS.exe, NVME.ROM, START.bat</code>三个文件的话, 则为正确, 直接输入 <code>START.bat</code>回车进行BIOS的刷写, 然后等待之后BIOS就刷写好了</p>
<p>刷写好BIOS后输入 <code>r</code>回车, 进行重新启动, 这时候进入BIOS, 会发现启动设备多了一个Windows Boot Manager, 则为成功</p>
</li>
<li>
<p>这时候工作还没有做完, SSD中的系统必须按照如下要求安装</p>
<p>进入PE系统, 使用Disk Genius分区工具, 将PCI-E槽中的SSD硬盘进行格式化(格式化前备份好资料)</p>
<p>格式化后, 选中SSD硬盘, 选择上方的快速分区, 分区表类型选择 <code>GUID</code></p>
<p>下方的分区数目可以自定义, 想要分几个区, 就选几个区, 一般这个SSD都是用于引导系统, 所以分一个区就够了, 给它全部空间</p>
<p>在下方选择 <code>重建主引导记录(MBR)</code>, 在右侧高级设置中分配分区空间</p>
<p>一定要勾选右下角的 <code>对齐分区到此扇区数的整数倍</code>, 给默认值就好(2048扇区)</p>
<p>然后确定, 这时候就分区好了</p>
<p>只有PCI-E槽上的SSD需要这么分区, 其它硬盘按照正常分区方法进行分区就可以</p>
</li>
<li>
<p>在分区之后, 使用Disk Genius工具清除 <code>除了ESP, MSR分区</code>以外的 <code>所有分区盘符</code>(就是C盘, D盘这些东西, 先给清掉)</p>
</li>
<li>
<p>然后再使用Disk Genius工具, <code>给那些分区重新分配盘符</code>, 但是 <code>一定要给SSD的那个分区C盘</code>, 其它的自己分就好 <code>(ESP, MSR不用管)</code></p>
</li>
<li>
<p>然后使用老毛桃PE中的 <code>WinNTSetUp工具</code>, 进行操作系统的安装, 位置在 <code>开始菜单-&gt;所有程序-&gt;安装维护</code>中, Windows安装源选择官方Windows10镜像, 引导驱动器选择刚才的ESP分区(这个ESP分区一定要在Disk Genius中和文件管理器中确认好), 安装驱动器选择C盘, 然后单击开始安装就好了</p>
</li>
<li>
<p>安装完系统后进行重启, 重启后一定要进入BIOS确认好Windows Boot Manager在第一启动项, 确认好后按F10保存并重启</p>
</li>
<li>
<p>重启后就进行系统的配置了, 这时候就已经结束了所有步骤了, Enjoy it!</p>
</li>
</ol>
<h2>解决方案三: M.2转USB</h2>
<p>事先声明, 这个方案只是笔者想到的, 但是没有实践, 因为我使用解决方案二解决了我的问题, 但是我认为这个解决方案应该是可行的</p>
<p>就是淘宝买一个支持M.2转USB3.0的转接器, 然后插在电脑上, BIOS中选择USB启动, 选择这个USB, 应该没有问题</p>
<p>但是SSD的速度有可能被影响, 这个笔者没有试验过</p>
]]></content:encoded>
<slash:comments>6</slash:comments>
<comments>https://nothamor.com/archives/old_masterboard_use_nvme.html#comments</comments>
<enclosure url="https://nothamor-1251700120.cos.ap-shanghai.myqcloud.com/E3Bs_8WXoAsOwYy.jpg" length="0" type="image/jpeg" />
</item>
<item>
<title>自动播放传智播客课程视频</title>
<link>https://nothamor.com/archives/auto_play_chuanzhi_video.html</link>
<guid isPermaLink="false">https://nothamor.com/archives/auto_play_chuanzhi_video.html</guid>
<pubDate>Mon, 22 Jun 2020 22:56:00 +0800</pubDate>
<dc:creator>NothAmor</dc:creator>
<category><![CDATA[技术]]></category>
<description><![CDATA[天天让看视频做那个作业, 打游戏的时候还要盯着时长, 回来切视频
太麻烦了, 干脆写了个脚本自动帮我切换, 如果有习题就会播放语音提醒
(一点小提示, 可以配合tampermonkey的H5播放器控制来实现16倍速播放, 畅享极致丝滑, 几秒一个视频, 我...]]></description>
<content:encoded><![CDATA[
<p>天天让看视频做那个作业, 打游戏的时候还要盯着时长, 回来切视频
太麻烦了, 干脆写了个脚本自动帮我切换, 如果有习题就会播放语音提醒
(一点小提示, 可以配合tampermonkey的H5播放器控制来实现16倍速播放, 畅享极致丝滑, 几秒一个视频, 我也是听我朋友说的传智不计观看视频时长, 如果计视频观看时长给分数的话就GG了, 酌情使用)</p>
<p><strong>使用方法:</strong>
在传智播客视频播放页按F12, 将下面的代码粘贴到控制台里面, 回车即可运行
(本项目已在GitHub开源, 如果对你有用的话, 顺路给个starrrrrr吧!)
[github repo="NothAmor/auto_chuanzhi" /]</p>
<pre><code>console.log("欢迎使用传智自动播放插件, 作者博客:https://www.nothamor.com");
    setTimeout(function() {
        let url = window.location.href;
        if(url.includes("http://stu.ityxb.com/lookPaper/busywork/")) {
            auto_search();
            console.log("检测到为测试页面, 开始自动查询题目");
        } else if(url.includes("http://stu.ityxb.com/preview/detail/")) {
            auto_play();
            console.log("检测到为视频播放页面, 开始自动播放视频");
        }
    }, 5000);

    function auto_play() {
        const CLASS_LIST = document.getElementsByClassName("point-progress-box");
        const CLASS_NAME = document.getElementsByClassName("point-text ellipsis");
        let question_text = document.getElementsByTagName("pre")[0];
        let player = document.getElementsByTagName("video")[0].id;
        let question_text_value;
        document.getElementById(player).click();
        let counter = 0;
        const TIMER = setInterval(function () {
            let percent = CLASS_LIST[counter].innerHTML.replace(/\ +/g, "").replace(/[\r\n]/g, "");
            let title_name = CLASS_NAME[counter].innerHTML.replace(/\ +/g, "").replace(/[\r\n]/g, "");
            if (percent.includes("100%") &amp;&amp; counter == (CLASS_LIST.length - 1)) {
                clearInterval(TIMER);
                alert("当前页所有视频均已播放完成");
            } else if (percent.includes("100%")) {
                CLASS_LIST[counter + 1].click();
                player = document.getElementsByTagName("video")[0].id;
                document.getElementById(player).click();
                counter++;
            }
            if (title_name.includes("习题")) {
                question_text = document.getElementsByTagName("pre")[0];
                question_text_value = question_text.innerHTML;
                console.log(" ");
                GM_xmlhttpRequest({
                    method: 'GET',
                    url: 'http://jb.s759n.cn/chati.php?w=' + encodeURIComponent(QUESTION[counter].innerHTML),
                    headers: {
                        'Content-type': 'application/x-www-form-urlencoded',
                    },
                    data: 'q=' + encodeURIComponent(QUESTION[counter].innerHTML),
                    onload: function (response) {
                        if (response.status == 200) {
                            let obj = $.parseJSON(response.responseText.replace(/^操作数据失败！/, '')) || {};
                            obj.answer = obj.data;
                            console.log("题目:" + QUESTION[counter].innerHTML + "的答案为:" + obj.answer);
                            if (obj.code) {
                            } else {
                                console.log('服务器繁忙，正在重试...');
                            }
                        } else if (response.status == 403) {
                            console.log('请求过于频繁，建议稍后再试');
                        } else {
                            console.log('服务器异常，正在重试...');
                        }
                    }
                });
            }
        }, 1000);
    }
    function auto_search() {
        const QUESTION = document.getElementsByTagName("pre");
        let counter = 0;
        const SEARCH = setInterval(function() {
            GM_xmlhttpRequest({
                method: 'GET',
                url: 'http://jb.s759n.cn/chati.php?w=' + encodeURIComponent(QUESTION[counter].innerHTML),
                headers: {
                    'Content-type': 'application/x-www-form-urlencoded',
                },
                onload: function (response) {
                    if (response.status == 200) {
                        let obj = $.parseJSON(response.responseText.replace(/^操作数据失败！/, '')) || {};
                        console.log("第" + counter + "题" + "的答案为:" + obj.data);
                        if (obj.code) {
                        } else {
                            console.log('服务器繁忙，正在重试...');
                        }
                    } else if (response.status == 403) {
                        console.log('请求过于频繁，建议稍后再试');
                    } else {
                        console.log('服务器异常，正在重试...');
                    }
                }
            });
            counter++;
            if(counter == (QUESTION.length)) {
                clearInterval(SEARCH);
                console.log("题目搜索完成");
            }
        }, 1000);
    }</code></pre>
<p>当然还有另外一个版本, 这个依赖于浏览器插件tampermonkey, 不用每次都手动去输入脚本内容
可以手动添加, 也可以直接在greasy fork上下载本脚本
greasy fork下载链接:<a href="https://greasyfork.org/zh-CN/scripts/405920-%E4%BC%A0%E6%99%BA%E8%87%AA%E5%8A%A8%E6%92%AD%E6%94%BE%E8%A7%86%E9%A2%91">https://greasyfork.org/zh-CN/scripts/405920-传智自动播放视频</a></p>
<pre><code>// ==UserScript==
// @name         传智自动播放视频
// @namespace    http://tampermonkey.net/
// @version      0.3
// @description  自动播放传智播客课程视频, 开发者博客:http://www.nothamor.cn
// @author       nothamor
// @match        *.ityxb.com/*
// @grant        GM_xmlhttpRequest
// ==/UserScript==

(function() {
    'use strict';

    console.log("欢迎使用传智自动播放插件, 作者博客:https://www.nothamor.cn");
    setTimeout(function() {
        let url = window.location.href;
        if(url.includes("http://stu.ityxb.com/lookPaper/busywork/")) {
            auto_search();
            console.log("检测到为测试页面, 开始自动查询题目");
        } else if(url.includes("http://stu.ityxb.com/preview/detail/")) {
            auto_play();
            console.log("检测到为视频播放页面, 开始自动播放视频");
        }
    }, 5000);

    function auto_play() {
        const CLASS_LIST = document.getElementsByClassName("point-progress-box");
        const CLASS_NAME = document.getElementsByClassName("point-text ellipsis");
        let question_text = document.getElementsByTagName("pre")[0];
        let player = document.getElementsByTagName("video")[0].id;
        let question_text_value;
        document.getElementById(player).click();
        let counter = 0;
        const TIMER = setInterval(function () {
            let percent = CLASS_LIST[counter].innerHTML.replace(/\ +/g, "").replace(/[\r\n]/g, "");
            let title_name = CLASS_NAME[counter].innerHTML.replace(/\ +/g, "").replace(/[\r\n]/g, "");
            if (percent.includes("100%") &amp;&amp; counter == (CLASS_LIST.length - 1)) {
                clearInterval(TIMER);
                alert("当前页所有视频均已播放完成");
            } else if (percent.includes("100%")) {
                CLASS_LIST[counter + 1].click();
                player = document.getElementsByTagName("video")[0].id;
                document.getElementById(player).click();
                counter++;
            }
            if (title_name.includes("习题")) {
                question_text = document.getElementsByTagName("pre")[0];
                question_text_value = question_text.innerHTML;
                console.log(" ");
                GM_xmlhttpRequest({
                    method: 'GET',
                    url: 'http://jb.s759n.cn/chati.php?w=' + encodeURIComponent(QUESTION[counter].innerHTML),
                    headers: {
                        'Content-type': 'application/x-www-form-urlencoded',
                    },
                    data: 'q=' + encodeURIComponent(QUESTION[counter].innerHTML),
                    onload: function (response) {
                        if (response.status == 200) {
                            let obj = $.parseJSON(response.responseText.replace(/^操作数据失败！/, '')) || {};
                            obj.answer = obj.data;
                            console.log("题目:" + QUESTION[counter].innerHTML + "的答案为:" + obj.answer);
                            if (obj.code) {
                            } else {
                                console.log('服务器繁忙，正在重试...');
                            }
                        } else if (response.status == 403) {
                            console.log('请求过于频繁，建议稍后再试');
                        } else {
                            console.log('服务器异常，正在重试...');
                        }
                    }
                });
            }
        }, 1000);
    }
    function auto_search() {
        const QUESTION = document.getElementsByTagName("pre");
        let counter = 0;
        const SEARCH = setInterval(function() {
            GM_xmlhttpRequest({
                method: 'GET',
                url: 'http://jb.s759n.cn/chati.php?w=' + encodeURIComponent(QUESTION[counter].innerHTML),
                headers: {
                    'Content-type': 'application/x-www-form-urlencoded',
                },
                onload: function (response) {
                    if (response.status == 200) {
                        let obj = $.parseJSON(response.responseText.replace(/^操作数据失败！/, '')) || {};
                        console.log("第" + counter + "题" + "的答案为:" + obj.data);
                        if (obj.code) {
                        } else {
                            console.log('服务器繁忙，正在重试...');
                        }
                    } else if (response.status == 403) {
                        console.log('请求过于频繁，建议稍后再试');
                    } else {
                        console.log('服务器异常，正在重试...');
                    }
                }
            });
            counter++;
            if(counter == (QUESTION.length)) {
                clearInterval(SEARCH);
                console.log("题目搜索完成");
            }
        }, 1000);
    }
})();</code></pre>
]]></content:encoded>
<slash:comments>55</slash:comments>
<comments>https://nothamor.com/archives/auto_play_chuanzhi_video.html#comments</comments>
<enclosure url="https://cdn.nothamor.com/v2-d90a4aa914c8570fe7fbd51baaafad3b_r.jpg" length="0" type="image/jpeg" />
</item>
</channel>
</rss>
