pstore

1 pstore简介

pstore最初是用于系统发生oops或panic时,自动保存内核log buffer中的日志。不过在当前内核版本中,其已经支持了更多的功能,如保存console日志、ftrace消息和用户空间日志。同时,它还支持将这些消息保存在不同的存储设备中,如内存、块设备或mtd设备。 为了提高灵活性和可扩展性,pstore将以上功能分别抽象为前端和后端,其中像dmesg、console等为pstore提供数据的模块称为前端,而内存设备、块设备等用于存储数据的模块称为后端,pstore core则分别为它们提供相关的注册接口。
  
通常系统内存资源比较紧张,所以我们喜欢往块设备保存更多log分析问题,但出现总线异常导致块设备不能用,或者因调度等问题还没来得及保存log等场景,使用内存设备作为后端可以提供更多的有效信息。

通过模块化的设计,实现了前端和后端的解耦,因此若某些模块需要利用pstore保存信息,就可以方便地向pstore添加新的前端。而若需要将pstore数据保存到新的存储设备上,也可以通过向其添加后端设备的方式完成。
  在这里插入图片描述
  除此之外,pstore还设计了一套pstore文件系统,用于查询和操作上一次重启时已经保存的pstore数据。当该文件系统被挂载时,保存在backend中的数据将被读取到pstore fs中,并以文件的形式显示。

2 代码实现

2.1 pstore初始化流程

pstore初始化主要是为其指定压缩算法,以及初始化pstore文件系统,其流程如下:

若存储空间比较有限,可以将pstore的内容压缩后再保存到backend设备上,相应的压缩算法可以通过配置选项CONFIG_PSTORE_COMPRESS_DEFAULT或模块参数compress指定。当前支持的压缩算法如下:

static const struct pstore_zbackend zbackends[] = {
#if IS_ENABLED(CONFIG_PSTORE_DEFLATE_COMPRESS)
	{
		.zbufsize	= zbufsize_deflate,
		.name		= "deflate",
	},
#endif
#if IS_ENABLED(CONFIG_PSTORE_LZO_COMPRESS)
	{
		.zbufsize	= zbufsize_lzo,
		.name		= "lzo",
	},
#endif
#if IS_ENABLED(CONFIG_PSTORE_LZ4_COMPRESS)
	{
		.zbufsize	= zbufsize_lz4,
		.name		= "lz4",
	},
#endif
#if IS_ENABLED(CONFIG_PSTORE_LZ4HC_COMPRESS)
	{
		.zbufsize	= zbufsize_lz4,
		.name		= "lz4hc",
	},
#endif
#if IS_ENABLED(CONFIG_PSTORE_842_COMPRESS)
	{
		.zbufsize	= zbufsize_842,
		.name		= "842",
	},
#endif
#if IS_ENABLED(CONFIG_PSTORE_ZSTD_COMPRESS)
	{
		.zbufsize	= zbufsize_zstd,
		.name		= "zstd",
	},
#endif
	{ }
}

pstore_init_fs用于初始化pstore文件系统,其代码如下:

int __init pstore_init_fs(void)
{
	…
	err = sysfs_create_mount_point(fs_kobj, "pstore");1)
	…
	err = register_filesystem(&pstore_fs_type);2)
	…
}

(1)在/sys/fs目录下为其创建挂载点pstore

(2)向内核注册pstore文件系统

在pstore文件系统挂载时会调用pstore_fs_type的mount回调,它会执行挂载相关的操作,其流程如下:
在这里插入图片描述
其中pstore文件系统需要实现自身的超级块填充函数pstore_fill_super,它主要为其设置inode和file相关的操作函数,并且将上一次重启时已保存在pstore backend设备中的pstore信息读取出来,并加入文件系统中。其主要实现如下:

void pstore_get_backend_records(struct pstore_info *psi,
				struct dentry *root, int quiet)
{if (psi->open && psi->open(psi))1)
		goto out;

	for (; stop_loop; stop_loop--) {pstore_record_init(record, psi);                   

		record->size = psi->read(record);2)
		…
		decompress_record(record);3)
		rc = pstore_mkfile(root, record);4)
		…
	}
	if (psi->close)
		psi->close(psi);}

(1)打开pstore backend设备

(2)从backend设备中逐条读取pstore record信息

(3)若使用了压缩算法,则需要将数据解压缩

(4)根据recored的内容,为其在pstore文件系统中创建一个文件。此后,用户就可以通过文件查询相关消息了

2.2 pstore backend注册流程

pstore core通过pstore_info结构体来描述一个pstore后端,该结构体主要包含了后端设备相关的信息,以及相关的操作函数,其定义如下:

struct pstore_info {
	struct module	*owner;
	const char	*name;1)

	struct semaphore buf_lock;
	char		*buf;2)
	size_t   bufsize;

	struct mutex	read_mutex;

	int		flags;3int		max_reason;4void		*data;5int		(*open)(struct pstore_info *psi);6int		(*close)(struct pstore_info *psi);
	ssize_t		(*read)(struct pstore_record *record);
	int		(*write)(struct pstore_record *record);
int		(*write_user)(struct pstore_record *record,
				      const char __user *buf);
int		(*erase)(struct pstore_record *record);
}

(1)后端设备的名字

(2)buf用于保存实际的消息

(3)flag用于表示消息的类型,其定义如下:

#define PSTORE_FLAGS_DMESG	BIT(0)
#define PSTORE_FLAGS_CONSOLE	BIT(1)
#define PSTORE_FLAGS_FTRACE	BIT(2)
#define PSTORE_FLAGS_PMSG	BIT(3)

(4)该参数只有前端为DMESG时才有效,用于控制哪些消息允许被dump到pstore中。只有dump reason小于等于该值的消息才会被dump,其reason取值如下:

enum kmsg_dump_reason {
	KMSG_DUMP_UNDEF,
	KMSG_DUMP_PANIC,
	KMSG_DUMP_OOPS,
	KMSG_DUMP_EMERG,
	KMSG_DUMP_SHUTDOWN,
	KMSG_DUMP_MAX
}

(5)后端设备的私有数据,可由回调函数传回

(6)后端设备相关的回调函数,如open和close用于打开和关闭设备,read、write用于读写pstore recored数据。write_user也用于向后端写入recored,但是其buffer为用户态地址,erase用于删除一个指定的recored

后端设备首先需要构造一个pstore_info结构体,然后调用pstore_register函数将其注册到pstore core中。以ram后端为例,其注册流程如下:
  在这里插入图片描述
  其中后端的配置信息可通过dts配置,如ramoops使用的内存地址、长度、recored长度等,ramoops_parse_dt用于解析dts中的相关信息。由于pstore可以包含多个前端,因此后端设备需要根据前端类型进行分区,ramoops_init_przs和ramoops_init_prz就是用于执行相关的分区操作。

pstore_register用于执行实际的注册工作,它的主要工作为校验输入参数是否合法,以及根据配置参数,注册相应的前端,其主要代码如下(去除了参数校验等部分):

int pstore_register(struct pstore_info *psi)
{if (psi->flags & PSTORE_FLAGS_DMESG)
		allocate_buf_for_compression();1pstore_get_records(0);2if (psi->flags & PSTORE_FLAGS_DMESG) {
		pstore_dumper.max_reason = psinfo->max_reason;
		pstore_register_kmsg();3}
	if (psi->flags & PSTORE_FLAGS_CONSOLE)
		pstore_register_console();
	if (psi->flags & PSTORE_FLAGS_FTRACE)
		pstore_register_ftrace();
	if (psi->flags & PSTORE_FLAGS_PMSG)
		pstore_register_pmsg();}

(1)若为pstore设置了压缩功能,则为其分配压缩时使用的内存

(2)从backend中读取上次重启时已保存的pstore recored,并为其建立相应的文件

(3)根据flags的值,注册相应的pstore前端

2.3 pstore frontend注册流程

pstore前端主要工作包含两部分,何时触发pstore写操作,以及需要向pstore写入什么内容,下面以dmesg前端为例,简要介绍一下其实现。

printk实现了一个kmsg_dump函数,用于方便其它模块dump内核的log buffer,当内核发生oops、panic或重启等事件时,都会调用该函数dump log信息。其代码实现如下

void kmsg_dump(enum kmsg_dump_reason reason)
{list_for_each_entry_rcu(dumper, &dump_list, list) {1enum kmsg_dump_reason max_reason = dumper->max_reason;

		if (max_reason == KMSG_DUMP_UNDEF) {                     
			max_reason = always_kmsg_dump ? KMSG_DUMP_MAX :
							KMSG_DUMP_OOPS;
		}
		if (reason > max_reason)2continue;

		dumper->dump(dumper, reason);3}
	rcu_read_unlock();
}

(1)遍历dump_list链表中的所有的dumper,并对它们分别执行如下log dump操作

(2)根据dumper的max_reason值,确定是否需要向其dump log

(3)若需要dump,则调用该dumper的dump回调,执行实际的dump操作

因此pstore前端只需将自身注册到dump_list链表中即可,该操作由以下函数实现:

int kmsg_dump_register(struct kmsg_dumper *dumper)
{if (!dumper->registered) {
		dumper->registered = 1;
		list_add_tail_rcu(&dumper->list, &dump_list);
		err = 0;
	}}

其中dmesg前端的主要工作就是为其实现一个dump函数,该函数将从log buffer中读取lou信息,然后将其封装为recored之后写入对应的后端设备,其主要定义如下:

static struct kmsg_dumper pstore_dumper = {
	.dump = pstore_dump,
}

static void pstore_dump(struct kmsg_dumper *dumper,
			enum kmsg_dump_reason reason)
{while (total < kmsg_bytes) {pstore_record_init(&record, psinfo);1)
		…
		header_size = snprintf(dst, dst_size, "%s#%d Part%u\n", why,
				 oopscount, part);2)
		dst_size -= header_size;

		if (!kmsg_dump_get_buffer(&iter, true, dst + header_size,
					  dst_size, &dump_size))3break;

		if (big_oops_buf) {
			zipped_len = pstore_compress(dst, psinfo->buf,
						header_size + dump_size,
						psinfo->bufsize);4)
			…
		} else {
			record.size = header_size + dump_size;
		}

		ret = psinfo->write(&record);5)
		…
	}}

(1)初始化一个record结构体

(2)先向其写入pstore头信息,如dmesg reason、oops发生次数等

(3)从log buffer中读取一行log信息

(4)若需要压缩信息,则执行压缩操作

(5)调用后端的写函数,将record写入相应的后端设备

3 pstore配置和使用

pstore使用主要包含以下四个步骤:
(1)通过配置内核参数,使能pstore功能。由于dmesg前端是默认使能的,因此若以ramoops为后端,且使能所有前端为例,则其相应的配置参数如下:

CONFIG_PSTORE=y
CONFIG_PSTORE_RAM=y
CONFIG_PSTORE_PMSG=y
CONFIG_PSTORE_FTRACE=y
CONFIG_PSTORE_CONSOLE=y

(2)配置devicetree,为后端设备设置相关参数。以ramoops为例,pstore使用的内存需要被添加到reserved内存中,以下为其中的一个示例:

reserved-memory {
#address-cells = <2>;
    #size-cells = <2>;
    ranges;

    ramoops@21f00000 {
         compatible = "ramoops";
         reg = <0x0 0x21f00000 0x0 0x00100000>;
         record-size     = <0x00020000>;
         console-size    = <0x00020000>;
         ftrace-size     = <0x00020000>;
     };
};

(3)挂载pstore文件系统,由于其挂载点为/sys/fs/pstore,因此相应的挂载操作如下:

mount -t pstore pstore /sys/fs/pstore

(4)挂载完成后,若后端中已经保存了先前的pstore消息,则可在/sys/fs/pstore目录下查看到对应的文件。用户可对该文件执行读取,删除等操作

4 课后作业

为巩固所学,给pstore加点功能,加深体会。
我们知道kdump是解决稳定性问题的利器,但crash kernel也是一个内核,它也有挂的时候,分析异常需要log,所以我们打算给crash kernel也开启pstore功能。
系统如何支持保存两个内核的pstore文件呢,这是我们下面要解决的主要问题。

参考: 内核崩溃日志抓取pstore

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值