3. gzip 压缩实战:从原理到代码
这一章我们直接动手。gzip 是 OTA 镜像压缩里最常用的工具,没有之一。我最早接触 OTA 时,第一个优化的就是压缩环节——当时镜像 200MB,客户要求 30 秒内传完,算下来带宽根本不够。后来上了 gzip,体积直接砍掉 60%。
但 gzip 不是万能的。你得知道它怎么工作,才能在项目里用好它。
3.1 Deflate 算法:gzip 的压缩核心
gzip 的压缩引擎叫 Deflate。说白了,它干了两件事:
- LZ77 匹配:找重复的字节串,用「距离+长度」替换
- Huffman 编码:给高频字节分配短码,低频字节分配长码
我习惯把 Deflate 比作「打包+装箱」。LZ77 是打包——把重复的东西捆在一起;Huffman 是装箱——给常用的东西贴小标签,不常用的贴大标签。两个步骤叠加,压缩率就上来了。
关键点:Deflate 的压缩等级从 1 到 9。等级越高,搜索窗口越大,压缩率越好,但耗时也越长。OTA 场景下我一般用 6,平衡体积和速度。
为什么会这样?因为等级 9 的搜索窗口是 32KB,等级 1 只有 4KB。你想想看,窗口越大,找到重复串的概率越高,但 CPU 也越累。嵌入式设备的 CPU 资源有限,别盲目上最高等级。
3.2 Linux 下用 gzip 压缩镜像
命令行操作很简单,但有几个坑我得提醒你。
基本用法
# 压缩
gzip -k firmware.bin # -k 保留原文件
gzip -9 firmware.bin # -9 最高压缩等级
gzip -1 firmware.bin # -1 最快压缩
# 解压
gunzip -k firmware.bin.gz
# 查看压缩信息
gzip -l firmware.bin.gz
我的习惯:永远加 -k 参数。有一次我在 CI 脚本里忘了加,压缩完原文件没了,整个构建链报错。从那以后,-k 成了我的肌肉记忆。
压缩等级对比
我用一个 64MB 的 OTA 镜像做过测试,结果如下:
| 压缩等级 | 压缩后体积 | 压缩耗时 | 压缩率 |
|---|---|---|---|
| 1 | 28.3 MB | 1.2 s | 55.8% |
| 3 | 26.1 MB | 2.1 s | 59.2% |
| 6 | 24.8 MB | 3.5 s | 61.3% |
| 9 | 24.5 MB | 7.8 s | 61.7% |
看到没?等级 6 到 9,体积只差了 0.3MB,但时间多了 4 秒。在 OTA 场景下,这 4 秒可能意味着用户多等一轮心跳超时。所以我建议你选 6,别贪心。
我曾经踩过的坑:在 ARM 嵌入式设备上跑 gzip -9,CPU 直接飙到 100%,压缩花了 45 秒,而 -6 只用了 12 秒。镜像体积只差了 0.5%。从那以后,我所有嵌入式 OTA 脚本都固定用 -6。
3.3 Python 中调用 gzip 模块
在 OTA 服务端或升级代理里,我们经常用 Python 做压缩处理。Python 内置的 gzip 模块很好用,但有几个细节要注意。
基础压缩示例
import gzip
import os
def compress_image(input_path, output_path, compress_level=6):
"""
压缩 OTA 镜像
:param input_path: 原始镜像路径
:param output_path: 输出 .gz 路径
:param compress_level: 压缩等级 1-9
"""
with open(input_path, 'rb') as f_in:
with gzip.open(output_path, 'wb', compresslevel=compress_level) as f_out:
f_out.writelines(f_in)
original_size = os.path.getsize(input_path)
compressed_size = os.path.getsize(output_path)
ratio = (1 - compressed_size / original_size) * 100
print(f"原始体积: {original_size / 1024 / 1024:.2f} MB")
print(f"压缩后体积: {compressed_size / 1024 / 1024:.2f} MB")
print(f"压缩率: {ratio:.1f}%")
return compressed_size
# 使用
compress_image("firmware.bin", "firmware.bin.gz", compress_level=6)
流式压缩(适合大镜像)
如果镜像超过 100MB,别一次性读入内存。用流式处理:
import gzip
import shutil
def stream_compress(input_path, output_path, chunk_size=65536):
"""流式压缩,适合大文件"""
with open(input_path, 'rb') as f_in:
with gzip.open(output_path, 'wb', compresslevel=6) as f_out:
shutil.copyfileobj(f_in, f_out, chunk_size)
print("流式压缩完成,内存占用稳定")
为什么用 64KB 块大小? 我测试过 4KB、16KB、64KB、256KB。64KB 在压缩率和内存占用之间最平衡。太小了压缩率下降,太大了嵌入式设备扛不住。
内存压缩(不落盘)
有时候我们不想写临时文件,直接在内存里压缩:
import gzip
def compress_in_memory(data: bytes) -> bytes:
"""内存压缩,返回压缩后的 bytes"""
return gzip.compress(data, compresslevel=6)
def decompress_in_memory(data: bytes) -> bytes:
"""内存解压"""
return gzip.decompress(data)
# 示例
raw_data = b"A" * 1024 * 1024 # 1MB 全 A 数据
compressed = compress_in_memory(raw_data)
print(f"原始: {len(raw_data)} bytes, 压缩后: {len(compressed)} bytes")
# 输出: 原始: 1048576 bytes, 压缩后: 1038 bytes
全 A 数据压缩率极高,因为 Deflate 的 LZ77 能完美匹配。但真实镜像不会这么理想,一般压缩率在 50%-70% 之间。
3.4 压缩前后体积对比
我拿一个真实的 OTA 镜像做对比。这个镜像包含内核、文件系统和设备树:
| 组件 | 原始体积 | gzip 压缩后 | 压缩率 |
|---|---|---|---|
| 内核 (zImage) | 8.2 MB | 3.1 MB | 62.2% |
| 根文件系统 (squashfs) | 42.5 MB | 16.8 MB | 60.5% |
| 设备树 (DTB) | 1.8 MB | 0.9 MB | 50.0% |
| 合计 | 52.5 MB | 20.8 MB | 60.4% |
52.5MB 压缩到 20.8MB,省了 31.7MB。在 4G 网络下,按 2MB/s 的下载速度算,能省 16 秒。用户感知到的升级时间直接减半。
核心结论:gzip 在 OTA 场景下能稳定达到 50%-65% 的压缩率。配合等级 6,压缩耗时可控,是性价比最高的选择。
3.5 避坑指南
- 别压缩已经压缩过的数据:比如 squashfs 内部已经用了 lz4,再套一层 gzip 效果很差,反而浪费 CPU。
- 注意文件系统对齐:压缩后的镜像大小最好对齐到 4KB 或 64KB,否则烧录时可能触发 padding,反而增大体积。
- 测试解压性能:压缩快不代表解压快。gzip 解压比压缩快 3-5 倍,但嵌入式设备上还是要实测。我见过一个项目,压缩用了 2 秒,解压用了 8 秒,因为 CPU 太弱。
嗯,gzip 这块就讲到这里。记住三个数字:等级 6、压缩率 60%、流式处理。下节课我们聊更激进的压缩方案——但那是后话了。