pg_filenode.map文件作用
global和base下每个db都会带一个pg_filenode.map文件,用来保存基础系统表 【oid→文件名】的映射关系。因为普通表可以通过pg_class的relfilenode找到表文件,但是启动时pg_class显然无法通过查找自己来找到表文件。还有一些共享系统表,例如pg_authid,如果在一个库中执行了vacuum full,auth表的relfilenode变了,就需要更新所有库中的pg_class,这里需要跨库并且还要在一个事务内,现在实现做不到这一点,所以基础表的 【oid→文件名】映射关系,是用文件来保存的,并且会写WAL日志保证持久化和一致性。

relmapper.c 给出了解释:
For most tables, the physical file underlying the table is specified by
pg_class.relfilenode. However, that obviously won't work for pg_class
itself, nor for the other "nailed" catalogs for which we have to be able
to set up working Relation entries without access to pg_class. It also
does not work for shared catalogs, since there is no practical way to
update other databases' pg_class entries when relocating a shared catalog.
Therefore, for these special catalogs (henceforth referred to as "mapped
catalogs") we rely on a separately maintained file that shows the mapping
from catalog OIDs to filenumbers.
查看哪些表是mapped的
SELECT relname, relfilenode FROM pg_class WHERE relfilenode = 0 ORDER BY relname;
pg_filenode.map文件读取
文件结构比较简单,可以用shell做查询:
#define RELMAPPER_FILEMAGIC 0x592717
#define MAX_MAPPINGS 64
typedef struct RelMapping {
Oid mapoid; /* 4 bytes: 系统表 OID */
RelFileNumber mapfilenumber; /* 4 bytes: 对应的 filenumber */
} RelMapping; /* 每条 8 bytes */
typedef struct RelMapFile {
int32 magic; /* 4 bytes: 固定值 0x592717 */
int32 num_mappings; /* 4 bytes: 有效映射条数 */
RelMapping mappings[64]; /* 64 × 8 = 512 bytes (固定大小) */
pg_crc32c crc; /* 4 bytes: CRC32C 校验 */
} RelMapFile; /* 总计: 4+4+512+4 = 524 bytes */
脚本
MAP_FILE="${1:-$PGDATA/global/pg_filenode.map}"
# ========== 读 magic number (前4字节, little-endian)
# -A n : Address radix = none,不显示左侧的地址偏移量
# -t x4 : Type = hex, 4 bytes 一组,即以 4 字节十六进制整数输出
# -N 4 : 只读取 4 个字节
# -j 0 : Jump,跳过前 0 个字节(即从文件开头开始读)
# tr -d ' ' : 删除输出中的空格
magic=$(od -An -tx4 -N4 -j0 "$MAP_FILE" | tr -d ' ')
echo "Magic: 0x$magic"
# ========== 读 num_mappings (第5-8字节)
# -t d4 : Type = decimal, 4 bytes 一组,即以 4 字节有符号十进制整数输出
# -N 4 : 只读取 4 个字节
# -j 4 : Jump 4,跳过前 4 个字节(跳过 magic),从第 5 字节开始读
num=$(od -An -td4 -N4 -j4 "$MAP_FILE" | tr -d ' ')
echo "Mappings count: $num"
echo "-----------------------------------"
# printf 格式化输出:%-12s 表示左对齐、占 12 字符宽度的字符串
printf "%-12s -> %s\n" "OID" "FileNumber"
echo "-----------------------------------"
# ========== 逐条读取 mappings ===
# 每条 mapping = 8 bytes (oid 4字节 + filenumber 4字节)
# 数据从文件的第 8 字节(offset=8)开始,前面是 magic(4) + num(4)
for ((i=0; i<num; i++)); do
# 计算当前第 i 条 mapping 的文件偏移量
# offset = 8 (header大小) + i * 8 (每条8字节)
offset=$((8 + i * 8))
# 读 OID:从 offset 处读 4 字节,输出为十进制
# -j$offset : 跳到当前 mapping 的起始位置
oid=$(od -An -td4 -N4 -j$offset "$MAP_FILE" | tr -d ' ')
# 读 FileNumber:从 offset+4 处读 4 字节(OID后面紧跟的4字节)
fnum=$(od -An -td4 -N4 -j$((offset+4)) "$MAP_FILE" | tr -d ' ')
# printf 格式化输出:%-12d 表示左对齐、占12字符宽的十进制整数
printf "%-12d -> %d\n" "$oid" "$fnum"
done

redo流程
关键数据结构
c
/* src/include/utils/relmapper.h */
/*
* xl_relmap_update: 关系映射更新的 WAL 记录数据
* 包含目标数据库信息和完整的新映射文件内容(整体替换)
*/
typedef struct xl_relmap_update
{
Oid dbid; /* 数据库 ID; 0 表示共享映射(shared catalog) */
Oid tsid; /* 数据库所在的表空间; 或 pg_global */
int32 nbytes; /* 映射数据的字节数(= sizeof(RelMapFile)) */
char data[FLEXIBLE_ARRAY_MEMBER]; /* 变长: 完整的 RelMapFile 内容 */
} xl_relmap_update;
#define MinSizeOfRelmapUpdate offsetof(xl_relmap_update, data)
代码
c
/* src/backend/utils/cache/relmapper.c:1096-1142 */
/*
* RELMAP 资源管理器的 redo 入口函数
*/
void
relmap_redo(XLogReaderState *record)
{
uint8 info = XLogRecGetInfo(record) & ~XLR_INFO_MASK;
/* relmap 记录不使用 backup block */
Assert(!XLogRecHasAnyBlockRefs(record));
if (info == XLOG_RELMAP_UPDATE)
{
/*
* ===== 处理 RELMAP_UPDATE: 重写映射文件 =====
*/
xl_relmap_update *xlrec = (xl_relmap_update *) XLogRecGetData(record);
RelMapFile newmap;
char *dbpath;
/*
* 安全性检查: WAL 中的 nbytes 必须等于 sizeof(RelMapFile)
* 如果不匹配, 说明 WAL 记录损坏, 必须 PANIC
*/
if (xlrec->nbytes != sizeof(RelMapFile))
elog(PANIC, "relmap_redo: wrong size %u in relmap update record",
xlrec->nbytes);
/* 从 WAL 记录的 data 区域复制出完整的新映射文件内容 */
memcpy(&newmap, xlrec->data, sizeof(newmap));
/*
* 构建目标数据库的路径
* 例如: base/12345 (对于普通数据库)
* global (对于共享系统表, dbid=0)
*/
dbpath = GetDatabasePath(xlrec->dbid, xlrec->tsid);
/*
* 将新映射写入磁盘并发送 sinval 消息:
* - 第2参数 false: 不写新 WAL(因为我们正在回放 WAL)
* - 第3参数 true: 发送 sinval 失效消息(通知其他后端刷新缓存)
* - 第4参数 false: 不保留文件(非事务上下文)
*
* 注意: 对于 CREATE DATABASE 场景, 发送 sinval 消息是不必要但无害的。
* 如果想避免, 可以在 WAL 记录中加一个标志位区分两种场景,
* 但当前实现选择了简洁性。
*/
LWLockAcquire(RelationMappingLock, LW_EXCLUSIVE);
write_relmap_file(&newmap, false, true, false,
xlrec->dbid, xlrec->tsid, dbpath);
LWLockRelease(RelationMappingLock);
pfree(dbpath);
}
else
elog(PANIC, "relmap_redo: unknown op code %u", info);
}
gdb分析redo
主库执行vacuum full pg_authid;,oid是1260。
备库redo时把完整的relmap更新到文件中了:
