LVM元数据分析
LVM数据布局
在每个LVM物理卷(PV)上都包含三个部分:LVM标签、元数据区域、数据区域。
LVM标签
LVM标签占用一个扇区(512字节),可以位于分区开始的四个扇区中的任何一个,默认情况下在第二个扇区,LVM标签主要包含以下部分:
- UUID -- 一个随机定长字符串,可以唯一标示一个PV,且不受系统启动顺序的影响。
- 数据区域位置 -- 以NULL结尾的列表,每一项包含以字节为单位的“偏移”和“大小”。
- 元数据区域位置 -- 以NULL结尾的列表,每一项包含以字节为单位的“偏移”和“大小”。
LVM标签最多可以包含15个区域,默认情况下有3个,1个数据区域加上2个元数据区域。
元数据区域
- 开始有一个元数据头,接着是一个循环缓冲区,使用ASCII格式以追加的方式保存卷组信息。
- 每个物理卷可以包含0、1和2份完全相同的元数据,可以在创建PV时通过参数指定,之后也可以进行修改,默认值为1。如果有2份,则第二份在分区的结尾。
- 位于同一个VG中的所有PV上的元数据完全相同,可以设置只在部分分区上有元数据,部分分区没有元数据。
数据区域
以PE为单位进行数据空间分配。
关键数据结构
LVM标签头结构
$ cat lib/label/label.h
...
/* On disk - 32 bytes */
struct label_header {
int8_t id[8]; /* LABELONE */
uint64_t sector_xl; /* Sector number of this label */
uint32_t crc_xl; /* From next field to end of sector */
uint32_t offset_xl; /* Offset from start of struct to contents */
int8_t type[8]; /* LVM2 001 */
} __attribute__ ((packed));
...
- id - 标签标识字符串,必须是“LABELONE”。
- sector_xl - 标签所处的扇区号,一般都是1。
- crc_xl - 从offset_xl开始到扇区结尾的数据的CRC校验值。
- offset_xl - 标签正文起始位置偏移(从标签开始位置,以字节为单位进行计算,一般是32,也就是是label_header的大小)。
- type - 标签类型,一般都是“LVM2 001”。
接下来就是从offset_xl偏移处开始的是标签正文,包含UUID以NULL结尾的数据区域位置和元数据区域位置列表等。
LVM标签正文结构
PV头结构:
$ cat lib/format_text/layout.h
...
struct pv_header {
int8_t pv_uuid[ID_LEN];
/* This size can be overridden if PV belongs to a VG */
uint64_t device_size_xl; /* Bytes */
/* NULL-terminated list of data areas followed by */
/* NULL-terminated list of metadata area headers */
struct disk_locn disk_areas_xl[0]; /* Two lists */
} __attribute__ ((packed));
...
- pv_uuid - PV的UUID。
- device_size_xl - PV的大小(以字节为单位),如果属于一个VG,则会被覆盖。
- disk_areas_xl - 数据和元数据区域列表。
扩展PV头结构:
$ cat lib/format_text/layout.h
...
struct pv_header_extension {
uint32_t version;
uint32_t flags;
/* NULL-terminated list of bootloader areas */
struct disk_locn bootloader_areas_xl[0];
} __attribute__ ((packed));
...
- version - 版本。
- flags - 标志。
- bootloader_areas_xl - 启动区域列表数组。
区域描述结构:
$ cat lib/format_text/format-text.h
...
/* On disk */
struct disk_locn {
uint64_t offset; /* Offset in bytes to start sector */
uint64_t size; /* Bytes */
} __attribute__ ((packed));
...
- offset - 数据或元数据区域的起始位置的字节偏移,从整个分区的第0个扇区开始计算。
- size - 区域的大小,以字节为单位,0表示剩余所有空间。
元数据区域头结构
$ cat lib/format_text/layout.h
...
/* On disk */
/* Structure size limited to one sector */
struct mda_header {
uint32_t checksum_xl; /* Checksum of rest of mda_header */
int8_t magic[16]; /* To aid scans for metadata */
uint32_t version;
uint64_t start; /* Absolute start byte of mda_header */
uint64_t size; /* Size of metadata area */
struct raw_locn raw_locns[0]; /* NULL-terminated list */
} __attribute__ ((packed));
...
- checksum_xl - 该结构所在扇区除checksum_xl外所有数据的的CRC校验。
- magic - 魔数,必须是"\040\114\126\115\062\040\170\133\065\101\045\162\060\116\052\076"。
- version - 版本号, 必须是1。
- start - 元数据区域的起始位置的字节偏移,从整个分区的第0个扇区开始计算。
- size - 区域的大小,以字节为单位,0表示剩余所有空间。
元数据区域原始位置结构
由于元数据区域是一个用ASCII格式描述VG信息的循环缓冲区,所以又使用raw_locn来定位当前正在使用的VG信息的位置。
$ cat lib/format_text/layout.h
...
/* On disk */
struct raw_locn {
uint64_t offset; /* Offset in bytes to start sector */
uint64_t size; /* Bytes */
uint32_t checksum;
uint32_t flags;
} __attribute__ ((packed));
...
- offset - 从该扇区开始的偏移地址。
- size - 字节大小。
- checksum - CRC校验。
- flags - 标志。
源码分析
LVM命令执行环境初始化
$ tools/lvmcmdline.c
...
int lvm_run_command(struct cmd_context *cmd, int argc, char **argv)
{
...
if (lvmetad_used() && !(cmd->command->flags & NO_LVMETAD_AUTOSCAN)) {
if (cmd->include_foreign_vgs || !lvmetad_token_matches(cmd)) {
if (lvmetad_used() && !lvmetad_pvscan_all_devs(cmd, cmd->include_foreign_vgs ? 1 : 0)) {
log_warn("WARNING: Not using lvmetad because cache update failed.");
lvmetad_make_unused(cmd);
}
}
...
}
...
$ cat lib/cache/lvmetad.c
...
int lvmetad_pvscan_all_devs(struct cmd_context *cmd, int do_wait)
{
...
while ((dev = dev_iter_get(iter))) {
if (sigint_caught()) {
ret = 0;
stack;
break;
}
if (!lvmetad_pvscan_single(cmd, dev, NULL, NULL)) {
ret = 0;
stack;
break;
}
}
...
if (!_token_update(NULL)) {
log_error("Failed to update lvmetad token after device scan.");
return 0;
}
...
}
...
int lvmetad_pvscan_single(struct cmd_context *cmd, struct device *dev,
struct dm_list *found_vgnames,
struct dm_list *changed_vgnames)
{
struct label *label;
struct lvmcache_info *info;
struct _lvmetad_pvscan_baton baton;
/* Create a dummy instance. */
struct format_instance_ctx fic = { .type = 0 };
if (!lvmetad_used()) {
log_error("Cannot proceed since lvmetad is not active.");
return 0;
}
// 读取标签
if (!label_read(dev, &label, 0)) {
log_print_unless_silent("No PV label found on %s.", dev_name(dev));
if (!lvmetad_pv_gone_by_dev(dev))
goto_bad;
return 1;
}
info = (struct lvmcache_info *) label->info;
baton.vg = NULL;
baton.fid = lvmcache_fmt(info)->ops->create_instance(lvmcache_fmt(info), &fic);
if (!baton.fid)
goto_bad;
if (baton.fid->fmt->features & FMT_OBSOLETE) {
lvmcache_fmt(info)->ops->destroy_instance(baton.fid);
log_warn("WARNING: Disabling lvmetad cache which does not support obsolete (lvm1) metadata.");
lvmetad_set_disabled(cmd, LVMETAD_DISABLE_REASON_LVM1);
_found_lvm1_metadata = 1;
/*
* return 1 (success) so that we'll continue to populate lvmetad
* instead of leaving the update incomplete.
*/
return 1;
}
// 遍历每个Metadata Area,并调用_lvmetad_pvscan_single函数进行处理
lvmcache_foreach_mda(info, _lvmetad_pvscan_single, &baton);
if (!baton.vg)
lvmcache_fmt(info)->ops->destroy_instance(baton.fid);
if (!lvmetad_pv_found(cmd,