news 2026/4/15 18:59:59

Linux CFS 的 block_avg:阻塞任务的平均等待时间

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Linux CFS 的 block_avg:阻塞任务的平均等待时间

一、简介

在Linux内核的CFS(Completely Fair Scheduler)调度器中,任务的状态转换和等待时间统计是理解系统性能瓶颈的关键。block_avg作为调度实体(sched_entity)统计信息中的核心指标,记录了任务因I/O操作、锁竞争等资源依赖而进入不可中断睡眠状态(TASK_UNINTERRUPTIBLE)的平均等待时间。

在实际生产环境中,我们经常会遇到这样的情况:应用层代码逻辑看似没有问题,但系统整体吞吐量却始终上不去;或者某些关键任务的延迟波动异常剧烈,却找不到明显的CPU占用高峰。这些问题往往与任务的阻塞行为密切相关——一个频繁进行磁盘I/O的数据库进程,或者大量等待网络响应的微服务,它们的阻塞等待时间往往比实际CPU执行时间更能反映性能瓶颈的本质。

掌握block_avg的统计机制和读取方法,对于系统性能调优、容量规划以及学术论文中的实验数据分析都具有重要价值。本文将从内核源码层面深入剖析阻塞任务等待时间的统计逻辑,并提供可直接复现的实战案例。

二、核心概念

2.1 调度实体与统计结构

在CFS调度器中,每个任务或任务组都对应一个sched_entity结构体。当内核编译时启用了CONFIG_SCHEDSTATS选项,该结构会包含sched_statistics成员,用于记录任务的各种等待时间统计信息:

#ifdef CONFIG_SCHEDSTATS struct sched_statistics { u64 wait_start; u64 wait_max; u64 wait_count; u64 wait_sum; u64 sleep_start; u64 sleep_max; u64 sum_sleep_runtime; u64 block_start; // 阻塞开始时间戳 u64 block_max; // 单次阻塞最长时间 u64 exec_max; u64 slice_max; u64 nr_migrations; u64 nr_wakeups; // ... 其他统计字段 }; #endif

其中,block_startblock_max是我们关注的重点。block_start记录任务进入阻塞状态的时间点,而block_max则保存了该任务历史上单次阻塞的最长时间。

2.2 PELT算法与负载计算

CFS使用PELT(Per Entity Load Tracking)算法来跟踪每个调度实体的负载情况。sched_avg结构体是PELT算法的核心数据结构:

struct sched_avg { u64 last_update_time; // 最后更新时间 u64 load_sum; // 负载总和(包含runnable和blocked) u64 runnable_load_sum; // 可运行任务的负载 u32 util_sum; // running任务的负载 u32 period_contrib; unsigned long load_avg; // 平均负载(包含blocked) unsigned long runnable_load_avg; // 可运行任务的平均负载 unsigned long util_avg; // 平均利用率 };

对于cfs_rq(CFS运行队列)而言,load_avg包含了所有处于runnableblocked状态的调度实体的负载聚合。这意味着即使一个任务当前正在等待I/O完成,它对系统负载的贡献仍然被计入,这反映了任务对资源的持续占用需求。

2.3 阻塞状态的定义与区分

在Linux调度器中,"阻塞"(blocked)特指任务处于TASK_UNINTERRUPTIBLE状态,通常发生在以下场景:

  • 等待磁盘I/O完成(如文件系统读写、swap操作)

  • 等待页错误处理(page fault时的磁盘读取)

  • 某些内核锁的竞争等待

这与"睡眠"(sleep)状态(TASK_INTERRUPTIBLE)有所区别——后者通常是任务主动调用sleep或等待用户态可中断的事件。通过enqueue_sleeper函数,内核在任务被唤醒时会根据之前记录的时间戳计算阻塞或睡眠时长,并更新相应的统计信息。

三、环境准备

3.1 软硬件环境要求

  • 操作系统:Linux内核版本4.6+(推荐5.x或6.x版本以获得完整的调度统计功能)

  • 架构支持:x86_64、ARM64、RISC-V等主流架构

  • 内核配置:确保内核启用了以下配置选项:

    CONFIG_SCHEDSTATS=y # 调度统计信息 CONFIG_DEBUG_KERNEL=y # 调试接口 CONFIG_PROC_FS=y # proc文件系统支持 CONFIG_BPF=y # eBPF支持(用于高级跟踪) CONFIG_BPF_SYSCALL=y

3.2 工具安装

# Debian/Ubuntu系统 sudo apt-get update sudo apt-get install -y linux-headers-$(uname -r) \ bpftrace bcc-tools sysstat procps # RHEL/CentOS系统 sudo yum install -y kernel-headers-$(uname -r) \ bpftrace bcc-tools sysstat # 验证调度统计是否启用 cat /proc/sys/kernel/sched_schedstats # 输出应为1,若为0则执行: sudo sysctl -w kernel.sched_schedstats=1

3.3 内核源码获取

建议下载与当前运行内核版本匹配的源码,用于对照分析:

# 查看当前内核版本 uname -r # 下载对应版本源码(以Ubuntu为例) apt-get source linux-image-$(uname -r) # 或从kernel.org下载 wget https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.6.tar.xz

四、应用场景

在分布式存储系统的性能调优项目中,我曾遇到一个典型场景:某Ceph OSD节点的磁盘I/O延迟看似正常(iostat显示的await在10ms以内),但客户端却频繁报告超时。通过分析/proc/PID/sched中的block_avg相关统计,我们发现OSD进程的block_max高达数秒——这意味着虽然平均I/O延迟不高,但存在间歇性的极端阻塞事件。

进一步定位发现,这是由于SSD的GC(垃圾回收)操作与内核的I/O调度策略冲突所致。通过调整mq-deadline调度器的read_expire参数并优化Ceph的osd_op_thread_timeout配置,我们将P99延迟从原来的2.3秒降低到了150ms以内。这个案例充分说明,平均I/O延迟指标往往掩盖了尾延迟问题,而block_avg相关的统计能够帮助我们发现这些隐蔽的性能瓶颈

类似的应用场景还包括:数据库事务延迟分析(识别锁等待vs I/O等待)、微服务调用链中的阻塞点定位、以及容器化环境中多租户I/O争用的量化评估。

五、实际案例与步骤

5.1 读取任务的阻塞统计信息

每个进程的调度统计信息可以通过/proc/<PID>/sched文件获取。以下脚本演示如何提取阻塞相关的关键指标:

#!/bin/bash # 文件名:analyze_block_stats.sh # 功能:分析指定进程的阻塞等待时间统计 PID=${1:-$$} # 默认分析当前shell进程 if [ ! -f "/proc/$PID/sched" ]; then echo "错误:无法访问PID $PID的调度信息" echo "请检查进程是否存在或权限是否足够" exit 1 fi echo "=== 进程 $PID 的调度统计信息 ===" echo "进程名: $(cat /proc/$PID/comm)" echo "" # 提取关键统计字段 echo "--- 时间统计(单位:纳秒)---" grep -E "se\.statistics\.(block_start|block_max|sleep_start|sleep_max|wait_sum)" /proc/$PID/sched echo "" echo "--- 执行统计 ---" grep -E "se\.sum_exec_runtime|nr_switches" /proc/$PID/sched echo "" echo "--- 负载统计 ---" grep -E "se\.avg\.(load_avg|runnable_load_avg|util_avg)" /proc/$PID/sched

执行示例输出:

=== 进程 1234 的调度统计信息 === 进程名: mysqld --- 时间统计(单位:纳秒)--- se.statistics.block_start : 0.000000 se.statistics.block_max : 152345678901.234567 se.statistics.sleep_start : 9876543210.123456 se.statistics.sleep_max : 8765432109.876543 se.statistics.wait_sum : 123456789012.345678 --- 执行统计 --- se.sum_exec_runtime : 3600000000000.000000 nr_switches : 89234 --- 负载统计 --- se.avg.load_avg : 1024 se.avg.runnable_load_avg : 0 se.avg.util_avg : 234

字段说明

  • block_max:该进程历史上单次阻塞的最长时间(纳秒)。如果数值很大,说明进程曾经历过长时间的I/O等待或锁竞争。

  • block_start:当前阻塞开始的时间戳,为0表示当前未处于阻塞状态。

  • sleep_max:单次睡眠(可中断等待)的最长时间。

  • wait_sum:在运行队列中等待的总时间(反映调度延迟)。

5.2 使用bpftrace实时跟踪阻塞事件

为了捕获实时的阻塞事件并计算平均等待时间,可以使用bpftrace编写eBPF程序:

#!/usr/bin/env bpftrace /* * 文件名:block_avg_tracker.bt * 功能:跟踪任务的阻塞等待时间,计算平均阻塞时长 * 用法:sudo ./block_avg_tracker.bt -p <PID> */ #include <linux/sched.h> BEGIN { printf("开始跟踪阻塞事件... 目标PID: %d\n", $1); printf("格式: TIME PID COMM BLOCK_DURATION_NS(本次阻塞时长) AVG_BLOCK_NS(平均阻塞时长)\n"); printf("=================================================================\n"); @block_start[$1] = 0; @block_total[$1] = 0; @block_count[$1] = 0; } // 跟踪任务进入不可中断睡眠状态 kprobe:__set_task_state / ((struct task_struct *)arg0)->pid == $1 / { $task = (struct task_struct *)arg0; $state = arg1; // TASK_UNINTERRUPTIBLE = 2 if ($state == 2 && $task->in_iowait) { @block_start[$1] = nsecs; } } // 跟踪任务唤醒(从阻塞状态恢复) kprobe:wake_up_state / ((struct task_struct *)arg0)->pid == $1 / { $task = (struct task_struct *)arg0; $pid = $task->pid; if (@block_start[$pid] != 0) { $duration = nsecs - @block_start[$pid]; @block_total[$pid] += $duration; @block_count[$pid]++; $avg = @block_total[$pid] / @block_count[$pid]; printf("%llu %d %s %llu %llu\n", nsecs, $pid, $task->comm, $duration, $avg); @block_start[$pid] = 0; } } END { printf("\n=================================================================\n"); printf("统计摘要:\n"); printf("总阻塞次数: %llu\n", @block_count[$1]); printf("总阻塞时间: %llu ns (%.2f ms)\n", @block_total[$1], @block_total[$1] / 1000000.0); if (@block_count[$1] > 0) { printf("平均阻塞时间: %llu ns (%.2f ms)\n", @block_total[$1] / @block_count[$1], (@block_total[$1] / @block_count[$1]) / 1000000.0); } clear(@block_start); clear(@block_total); clear(@block_count); }

使用说明

  1. 保存上述代码为block_avg_tracker.bt

  2. 赋予执行权限:chmod +x block_avg_tracker.bt

  3. 运行:sudo ./block_avg_tracker.bt -p $(pgrep mysqld)

该脚本通过在内核的__set_task_statewake_up_state函数上挂载探针,精确测量任务进入和离开阻塞状态的时间差。in_iowait标志用于区分I/O相关的阻塞与其他类型的不可中断睡眠。

5.3 分析CFS运行队列的阻塞负载

update_blocked_averages函数是CFS调度器中用于更新阻塞任务平均负载的关键函数。以下Python脚本演示如何读取和分析系统级的阻塞负载信息:

#!/usr/bin/env python3 """ 文件名:cfs_block_load_analyzer.py 功能:分析CFS运行队列的阻塞负载统计 """ import os import glob import struct def read_proc_schedstat(): """ 读取/proc/schedstat获取系统级调度统计 格式说明参见内核文档:Documentation/scheduler/sched-stats.txt """ stats = {} try: with open('/proc/schedstat', 'r') as f: lines = f.readlines() cpu_stats = {} current_cpu = None for line in lines: line = line.strip() if line.startswith('cpu'): parts = line.split() cpu_num = int(parts[0][3:]) current_cpu = cpu_num # 格式:cpu<N> <running_time> <waiting_time> <timeslices> ... if len(parts) >= 4: cpu_stats[cpu_num] = { 'running_time': int(parts[1]), 'waiting_time': int(parts[2]), 'timeslices': int(parts[3]) } elif current_cpu is not None and line.startswith('domain'): # 调度域统计信息 pass stats['cpu_stats'] = cpu_stats return stats except Exception as e: print(f"读取schedstat失败: {e}") return None def analyze_task_block_patterns(pid): """ 分析指定进程的阻塞模式 """ sched_file = f'/proc/{pid}/sched' if not os.path.exists(sched_file): return None data = {} with open(sched_file, 'r') as f: for line in f: line = line.strip() if ':' in line: key, value = line.split(':', 1) key = key.strip() value = value.strip() # 提取数值 try: if '.' in value: data[key] = float(value) else: data[key] = int(value) except: data[key] = value return data def calculate_block_ratio(task_data): """ 计算阻塞时间占总时间的比例 """ if not task_data: return None exec_runtime = task_data.get('se.sum_exec_runtime', 0) # 注意:/proc/PID/sched中的block_max是历史最大值,而非累计值 # 这里我们通过nr_switches估算平均阻塞 nr_switches = task_data.get('nr_switches', 1) # 简单的启发式估算:假设每次切换都伴随一定阻塞 # 实际分析应结合bpftrace的实时数据 block_heuristic = task_data.get('se.statistics.block_max', 0) * 0.1 total_time = exec_runtime + block_heuristic if total_time > 0: block_ratio = block_heuristic / total_time else: block_ratio = 0 return { 'exec_runtime_ms': exec_runtime / 1e6, 'block_estimate_ms': block_heuristic / 1e6, 'block_ratio_percent': block_ratio * 100, 'nr_switches': nr_switches } if __name__ == '__main__': import sys print("=== CFS阻塞负载分析工具 ===\n") # 系统级统计 print("1. 系统级调度统计:") schedstat = read_proc_schedstat() if schedstat: for cpu, stat in schedstat['cpu_stats'].items(): avg_wait = stat['waiting_time'] / max(stat['timeslices'], 1) print(f" CPU{cpu}: 平均等待时间={avg_wait/1e6:.2f}ms, " f"时间片数={stat['timeslices']}") # 进程级分析 if len(sys.argv) > 1: pid = int(sys.argv[1]) print(f"\n2. 进程 {pid} 的阻塞分析:") task_data = analyze_task_block_patterns(pid) if task_data: ratio = calculate_block_ratio(task_data) if ratio: print(f" 执行时间: {ratio['exec_runtime_ms']:.2f} ms") print(f" 估算阻塞: {ratio['block_estimate_ms']:.2f} ms") print(f" 阻塞比例: {ratio['block_ratio_percent']:.2f}%") print(f" 上下文切换次数: {ratio['nr_switches']}") print(f"\n 原始统计字段:") for key in ['se.statistics.block_max', 'se.statistics.sleep_max', 'se.statistics.wait_sum', 'se.avg.load_avg', 'se.avg.util_avg']: if key in task_data: print(f" {key}: {task_data[key]}") else: print(f" 无法读取PID {pid}的调度信息") else: print("\n2. 用法:python3 cfs_block_load_analyzer.py <PID>")

5.4 内核视角:enqueue_sleeper的实现逻辑

深入理解block_avg的统计机制,需要分析内核中的enqueue_sleeper函数。当任务从睡眠或阻塞状态被唤醒并入队时,该函数负责计算并更新统计信息:

/* * 内核代码片段(基于kernel/sched/fair.c) * 展示了enqueue_sleeper如何统计阻塞时间 */ static void enqueue_sleeper(struct cfs_rq *cfs_rq, struct sched_entity *se) { #ifdef CONFIG_SCHEDSTATS struct task_struct *tsk = task_of(se); u64 delta; // 处理睡眠状态(TASK_INTERRUPTIBLE) if (se->statistics.sleep_start) { delta = rq_clock(rq_of(cfs_rq)) - se->statistics.sleep_start; if ((s64)delta < 0) delta = 0; if (unlikely(delta > se->statistics.sleep_max)) se->statistics.sleep_max = delta; se->statistics.sleep_start = 0; se->statistics.sum_sleep_runtime += delta; // 触发tracepoint:sched_stat_sleep trace_sched_stat_sleep(tsk, delta); } // 处理阻塞状态(TASK_UNINTERRUPTIBLE)- 这就是我们关注的block统计 if (se->statistics.block_start) { delta = rq_clock(rq_of(cfs_rq)) - se->statistics.block_start; if ((s64)delta < 0) delta = 0; if (unlikely(delta > se->statistics.block_max)) se->statistics.block_max = delta; // 更新最大阻塞时间 // 注意:内核没有直接维护block_sum,但可以通过tracepoint获取 se->statistics.block_start = 0; // 区分I/O等待和其他阻塞 if (tsk->in_iowait) { trace_sched_stat_iowait(tsk, delta); } else { trace_sched_stat_blocked(tsk, delta); } } #endif }

关键逻辑解析

  1. block_start在任务进入TASK_UNINTERRUPTIBLE状态时被设置(通常在__set_task_state调用链中)

  2. 当任务被唤醒并入队时,enqueue_sleeper计算delta = 当前时间 - block_start

  3. 如果delta超过历史最大值,则更新block_max

  4. 通过in_iowait标志区分I/O等待和其他类型的阻塞

六、常见问题与解答

Q1: 为什么我的系统/proc/PID/sched中没有block_max字段?

A: 该字段仅在内核编译时启用了CONFIG_SCHEDSTATS选项时才可用。检查方法:

grep CONFIG_SCHEDSTATS /boot/config-$(uname -r) # 或 zcat /proc/config.gz | grep CONFIG_SCHEDSTATS

如果输出为# CONFIG_SCHEDSTATS is not set,则需要重新编译内核并启用该选项。对于生产环境,也可以考虑使用eBPF通过kprobe跟踪enqueue_sleeper函数来获取类似数据。

Q2:block_max的值看起来异常大(几十秒),这是否正常?

A:block_max记录的是历史上单次的最大阻塞时间。在以下场景中,较大的值是正常的:

  • 系统内存紧张时的swap操作

  • 慢速存储设备(如机械硬盘)上的大文件读取

  • 网络文件系统(NFS/SMB)在连接中断时的重试等待

但如果block_max持续增长而业务并未触发明显的I/O操作,可能表明存在:

  • 内核锁竞争(如inode锁、mmap_sem等)

  • 驱动层面的bug导致的不可中断睡眠

建议结合echo w > /proc/sysrq-trigger查看当前阻塞任务的调用栈(通过dmesg输出)。

Q3: 如何区分I/O阻塞和锁等待?

A: 内核通过task_struct中的in_iowait标志来区分。在bpftrace脚本中,可以通过检查该标志来判断阻塞类型:

kprobe:io_schedule / ((struct task_struct *)arg0)->pid == $1 / { printf("PID %d进入I/O等待\n", $1); } kprobe:mutex_lock / ((struct task_struct *)arg0)->pid == $1 / { printf("PID %d可能进入锁等待\n", $1); }

此外,/proc/PID/stack文件可以显示任务的当前调用栈,是诊断阻塞原因的有力工具。

Q4: 容器环境中的block_avg统计是否准确?

A: 在启用cgroup v2的系统中,CFS支持组调度(group scheduling),此时sched_entity可能代表一个cgroup而非单个任务。block_avg的统计仍然有效,但需要注意:

  • 对于cgroup级别的统计,反映的是该组内所有任务的聚合行为

  • 在嵌套cgroup场景中,需要逐级查看cpu.stat文件中的nr_throttledthrottled_usec来区分调度延迟和资源限制导致的等待

七、实践建议与最佳实践

7.1 性能分析工作流

  1. 初步筛选:使用sar -q 1观察blocked列(当前阻塞等待I/O的任务数),如果持续大于CPU核心数,说明存在I/O瓶颈

  2. 精确定位:通过for pid in $(pgrep <pattern>); do echo "=== $pid ==="; grep block_max /proc/$pid/sched; done快速找出阻塞时间最长的进程

  3. 深入分析:对可疑进程使用bpftrace脚本跟踪实时的阻塞事件,观察block_avg的变化趋势

  4. 根因确认:结合iostat -x 1pidstat -d 1以及/proc/PID/stack确定是设备层问题还是内核锁竞争

7.2 调优建议

  • I/O密集型应用:如果block_avg高但CPU利用率低,考虑使用异步I/O(io_uring)或增加预读缓冲区大小

  • 数据库系统:监控block_max的突发增长,配合vm.dirty_ratiovm.dirty_background_ratio的调整,避免刷盘操作造成的长时间阻塞

  • 实时性要求高的场景:考虑使用SCHED_FIFOSCHED_RR策略将关键任务移出CFS调度类,但这会牺牲公平性

7.3 监控指标采集

建议将以下指标纳入Prometheus/Grafana监控体系:

# 节点级指标:从/proc/schedstat提取 awk '/^cpu/ {print "sched_cpu_running_time{cpu=\""$1"\"} "$2"\nsched_cpu_waiting_time{cpu=\""$1"\"} "$3}' /proc/schedstat # 关键进程指标 for pid in $(pgrep mysqld); do block_max=$(awk '/se.statistics.block_max/{print $2}' /proc/$pid/sched) echo "sched_block_max{pid=\"$pid\",comm=\"$(cat /proc/$pid/comm)\"} $block_max" done

八、总结与应用场景

本文深入剖析了Linux CFS调度器中阻塞任务平均等待时间的统计机制,从sched_entity的数据结构、enqueue_sleeper的统计逻辑,到用户态的读取方法和eBPF跟踪技术,构建了一套完整的分析体系。

在实时Linux系统(PREEMPT_RT)中,虽然调度策略有所不同,但block_avg相关的统计仍然有效,且对于验证系统的确定性延迟行为尤为重要——即使CPU调度延迟被优化到微秒级,磁盘I/O或锁竞争导致的阻塞仍可能成为尾延迟的主要来源。

掌握这些知识后,读者可以:

  • 在学术论文中准确描述实验环境的调度行为特征

  • 在生产环境中快速定位"CPU使用率不高但响应慢"的疑难问题

  • 设计更精细的负载均衡策略,将I/O模式相似的任务聚合以减少对调度公平性的干扰

建议读者结合实际工作负载运行本文提供的脚本,建立对block_avg指标的直观理解,并根据具体场景调整分析粒度。


参考文献

  • Linux Kernel Source:kernel/sched/fair.c,include/linux/sched.h

  • Documentation:Documentation/scheduler/sched-stats.txt

  • PELT算法详解:Linux内核负载跟踪机制

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/15 18:59:53

CAT3D实战:如何用单张照片5分钟生成可交互3D场景(附NeRF对比测试)

CAT3D实战&#xff1a;如何用单张照片5分钟生成可交互3D场景&#xff08;附NeRF对比测试&#xff09; 当游戏开发者需要在48小时内为独立游戏制作一个废弃工厂的3D场景&#xff0c;或是电商设计师要为一款新品多角度展示却只有一张产品照片时&#xff0c;传统3D建模流程往往令人…

作者头像 李华
网站建设 2026/4/15 18:57:55

STM32G474外部中断避坑指南:从CubeMX配置到中断服务函数编写,新手常犯的5个错误

STM32G474外部中断避坑指南&#xff1a;从CubeMX配置到中断服务函数编写 第一次接触STM32G474的外部中断功能时&#xff0c;很多开发者都会遇到各种奇怪的问题——中断不触发、响应异常甚至系统卡死。这些问题往往源于几个容易被忽视的细节配置。本文将深入剖析新手最容易踩的5…

作者头像 李华
网站建设 2026/4/15 18:55:27

别再只用扫码枪了!用LabVIEW+OpenCV打造你的条形码/二维码混合识别系统

工业级视觉识别系统实战&#xff1a;用LabVIEWOpenCV替代传统扫码枪 在自动化产线和智能仓储场景中&#xff0c;扫码设备如同神经末梢般重要。但传统扫码枪的局限性日益凸显——固定安装方式难以适应柔性生产需求&#xff0c;高精度型号动辄上万元的采购成本让中小企业望而却步…

作者头像 李华
网站建设 2026/4/15 18:54:24

从DTU数据集到MVSNet:点云重建精度与完整度的量化评估实战

1. 从零开始理解DTU数据集与MVSNet 第一次接触三维重建时&#xff0c;我被各种专业术语搞得晕头转向。直到亲手用DTU数据集跑通了MVSNet&#xff0c;才真正理解点云重建的奥妙。DTU数据集就像三维世界的"标尺"&#xff0c;而MVSNet则是帮你画图的"智能画笔"…

作者头像 李华
网站建设 2026/4/15 18:54:18

掌握专业Unity资源提取:AssetStudio高效使用与深度配置指南

掌握专业Unity资源提取&#xff1a;AssetStudio高效使用与深度配置指南 【免费下载链接】AssetStudio AssetStudio - Based on the archived Perfares AssetStudio, I continue Perfares work to keep AssetStudio up-to-date, with support for new Unity versions and additi…

作者头像 李华