.phpc (PHP Compiled) 是 OpKit 扩展使用的二进制预编译文件格式。它将 PHP 脚本的编译结果(Opcode、常量、类、函数等)持久化为二进制文件,供后续快速加载执行,无需重新解析和编译源码。
┌─────────────────────────────────────────────────────────────┐
│ File Header (112 bytes on 64-bit) │
├─────────────────────────────────────────────────────────────┤
│ Offset │ Size │ Field │
├──────────┼─────────┼───────────────────────────────────────────┤
│ 0 │ 8 │ magic[8] - 文件标识 "PHPC\0\0\0\0" │
│ 8 │ 32 │ system_id[32] - PHP 系统标识 │
│ 40 │ 8 │ mem_size - 内存数据区大小 │
│ 48 │ 8 │ str_size - 字符串池大小 │
│ 56 │ 8 │ script_offset - 脚本结构偏移量 │
│ 64 │ 8 │ timestamp - 文件编译时间戳 │
│ 72 │ 4 │ checksum - Adler32 校验和 │
│ 76 │ 4 │ (padding) - 对齐填充 │
│ 80 │ 8 │ metadata_size - Metadata 分区大小 │
│ 88 │ 8 │ code_size - Code 分区大小 │
│ 96 │ 8 │ data_size - Data 分区大小 │
│ 104 │ 8 │ misc_size - Misc 分区大小 │
└─────────────────────────────────────────────────────────────┘
char magic[8] = "PHPC\0\0\0\0";文件标识符,用于快速识别 .phpc 文件格式。
char system_id[32];PHP 系统的唯一标识,基于 PHP 版本、编译选项、扩展等生成。用于确保 .phpc 文件与当前 PHP 环境兼容,防止不兼容的二进制文件被加载。
size_t mem_size; // 主内存数据区大小
size_t str_size; // 字符串池大小mem_size: 序列化后的zend_persistent_script结构及其关联数据的总大小str_size: 所有 interned 字符串的序列化后总大小
size_t script_offset;zend_persistent_script 结构在内存数据区中的偏移量。通常为 0,表示脚本结构从数据区起始位置开始。
time_t timestamp;文件编译时的时间戳,用于增量编译时比较源文件修改时间。
uint32_t checksum;使用 Adler32 算法计算内存数据区和字符串池的校验和,用于验证文件完整性。
checksum = zend_adler32(ADLER32_INIT, mem_data, mem_size);
checksum = zend_adler32(checksum, string_pool, str_size);size_t metadata_size; // Metadata 分区大小
size_t code_size; // Code 分区大小
size_t data_size; // Data 分区大小
size_t misc_size; // Misc 分区大小用于统计和调试,显示各逻辑分区的内存占用情况。
内存数据区存储序列化后的 zend_persistent_script 结构,包含以下主要部分:
typedef struct _zend_persistent_script {
zend_script script; // 脚本基本信息
zend_long compiler_halt_offset;// __HALT_COMPILER 位置
int ping_auto_globals_mask;
accel_time_t timestamp; // 编译时间戳
bool corrupted; // 是否损坏
bool is_phar; // 是否为 phar 包
bool empty; // 是否为空
uint32_t num_warnings; // 警告数量
uint32_t num_early_bindings; // 早期绑定数量
zend_error_info **warnings; // 编译警告
zend_early_binding *early_bindings; // 早期绑定信息
void *mem; // 共享内存指针
size_t size; // 内存大小
// ... 动态成员
} zend_persistent_script;typedef struct _zend_script {
zend_string *filename; // 原始文件名
HashTable class_table; // 类定义表
HashTable function_table; // 函数定义表
zend_op_array main_op_array; // 主 op_array(顶层代码)
} zend_script;typedef struct _opkit_persistent_script {
zend_persistent_script script;
HashTable constants_table; // 用户定义常量表
} opkit_persistent_script;所有内存指针在序列化时被转换为相对于 script->mem 起始地址的偏移量:
#define SERIALIZE_PTR(ptr) do { \
if (ptr) { \
ptr = (void*)((char*)ptr - (char*)script->mem); \
} \
} while (0)反序列化时恢复为绝对地址:
#define UNSERIALIZE_PTR(ptr) do { \
if (ptr) { \
ptr = (void*)((char*)buf + (size_t)ptr); \
} \
} while (0)字符串分为两类处理:
- 普通字符串:转换为相对于
script->mem的偏移量 - Interned 字符串:存储在独立的字符串池中,偏移量低 1 位标记为 1
#define IS_SERIALIZED_INTERNED(ptr) ((size_t)(ptr) & Z_UL(1))
// Interned 字符串偏移格式
void *serialized = (void*)(pool_offset | Z_UL(1));使用 xlat 表(转换表)跟踪已序列化的对象,避免重复序列化:
// 序列化前检查
if (zend_shared_alloc_get_xlat_entry(source)) {
return; // 已序列化,跳过
}
zend_shared_alloc_register_xlat_entry(source, offset);存储脚本元数据和结构信息:
zend_persistent_script结构本身zend_script结构- 类表、函数表的哈希表结构
- 运行时缓存 (
op_array->cache_size)
存储编译后的 PHP 字节码:
zend_op指令数组 (op_array->opcodes)- 每个指令包含操作码、操作数、类型等信息
存储运行时数据:
- 字面量 (
op_array->literals) - zval 数组 - 变量名 (
op_array->vars) - 参数信息 (
zend_arg_info) - 字符串值
存储辅助数据结构:
- AST 节点(用于常量表达式)
- 属性 (attributes)
- Try-catch 块 (
zend_try_catch_element) - Live ranges
- 静态变量表
编译 PHP 源码
↓
创建 zend_persistent_script
↓
计算内存需求 (zend_accel_script_persist_calc)
- 遍历所有结构计算 size
- 分别累加到四个分区
↓
分配内存缓冲区
↓
序列化到缓冲区 (zend_file_cache_serialize)
- 序列化 script 结构
- 序列化 class_table
- 序列化 function_table
- 序列化 main_op_array
- 序列化常量表
- 序列化警告和早期绑定
↓
写入文件
- 写入 Header (96 bytes)
- 写入内存数据区
- 写入字符串池
↓
计算并写入 checksum
读取文件
↓
验证 Header
- 检查 magic
- 检查 system_id 匹配
- 验证 checksum
↓
分配内存并读取数据
├── 堆内存路径: mem = emalloc(total_size)
└── SHM 路径: mem = opkit_shared_alloc(total_size)
(当 opkit.shm_size > 0 且空间充足时)
↓
反序列化 (zend_file_cache_unserialize)
- 恢复所有指针偏移量
- 重建哈希表
- 恢复 interned 字符串
- 初始化运行时缓存指针
↓
类链接 (opkit_link_classes)
- 解析继承关系
- 链接父类、接口
↓
解析类常量
- 计算常量表达式
↓
执行全局代码 (define, const 等)
当 opkit.shm_size 大于 0 时,反序列化后的脚本数据会存储在 mmap(MAP_SHARED) 映射的共享内存中。这意味着:
- 文件数据仍需要从磁盘读取到临时缓冲区
- 持久化阶段将数据从临时缓冲区复制到共享内存(通过
zend_accel_script_persist) - 子进程继承:
fork()后子进程继承父进程的共享内存映射,无需重新读取文件 - 内存标记:
opkit_script_node->in_shm标记内存来源,RSHUTDOWN 时 SHM 内存不释放
增量编译通过以下字段判断是否重新编译:
- System ID: PHP 环境发生变化时,system_id 会改变
- Magic: 文件格式版本变更时会更新
- Timestamp: 与源文件修改时间比较
if (system_id != current_system_id ||
magic != FILE_CACHE_MAGIC ||
file_mtime > phpc_mtime) {
// 需要重新编译
}- System ID 验证: 防止在不同 PHP 版本或配置间混用 .phpc 文件
- Checksum 验证: 检测文件损坏或篡改
- 大小限制: 通过
max_file_size限制单个文件大小 - 路径隔离: Phar 模式下的路径修复,防止路径泄露
使用 phpc -i <file>.phpc 可以查看文件的元信息和分区统计:
File: example.phpc
Magic: PHPC
System ID: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Memory Size: 12345 bytes
String Pool: 6789 bytes
Checksum: 0xAABBCCDD
Partition Sizes:
Metadata: 4096 bytes
Code: 4096 bytes
Data: 2048 bytes
Misc: 2105 bytes
Classes: 5
Functions: 12
Constants: 3