前面一篇文章介绍了kexec和kdump的思想,本文着重讲它们的另一个方面,就是kdump到底是如何转储垮掉内核的内存映像的。首先定义一个链表,它很重要。
static LIST_HEAD(vmcore_list);
unsigned long long elfcorehdr_addr = ELFCORE_ADDR_MAX; //一个重要变量记录内核装载的位置,用以判断是正常启动还是kdump启动。
struct vmcore {
struct list_head list; //链表组织
unsigned long long paddr; //物理地址
unsigned long long size; //本内存片断的大小
loff_t offset;
};
以上的结构体主要接收挂掉内核的内存片断,所有的vmcore组成一个链表,这个链表就是内存按照从小到大的顺序,其实之所以用此结构体组成链表来装载挂掉内核的内存是因为内核本身就是elf的,elf中就是将所有的内存看作一系列的节,所有的节组成了整个linux内核映射空间,linux的内核vmlinuz就是映射在这个空间的,在模块初始化的时候会调用vmcore_init
static int __init vmcore_init(void)
{
int rc = 0;
if (!(elfcorehdr_addr
return rc;
rc = parse_crash_elf_headers(); //本质上内核文件是一个elf映像,这个函数要做的就是将内核映像(注意是已经crash的内核)的elf诸多内存节字段设置到内核专门为kdump准备的一个list中,即上述的链表vmcore_list。
...//出错处理
if (proc_vmcore)
proc_vmcore->size = vmcore_size;
return 0;
}
如果系统崩溃,那么在kdump的机制kexec中起来新的内核后,会将垮掉内核的内存映像放入到/proc/vmcore中,其实并不是把几百兆的数据存入了文件,procfs文件系统本身就不是用来在硬盘上存放数据的,它只是提供了一个存取接口,通过这个存取接口用户可以得到一些内核的信息,比如对于/proc/vmcore,它的读取回调函数就是read_vmcore,具体过程就是在新内核启动的过程中,也就是还没有到用户空间的时候,内核就把数据设置好了,其实不是设置,而是组织好了,等到启动到用户空间的时候会通过复制/proc/vmcore到一个磁盘文件系统的某个地方来真的将垮掉内核的内存映像转储到磁盘,既然是复制那肯定就用到了/proc/vmcore的读取函数,这时,内核组织好的数据就会一一流入到磁盘中,再次重申一下这里组织数据的意义,就是不需要事先把需要的数据全部放到那里等待拷贝,而是将数据组织成一个的格式,等到真正读取的时候再现场将数据形成,这也是linux内核中很多文件系统的方式,只是提供一个存取接口,接口具体怎么实现,文件系统负责,其实也就是file_operations负责,细化一点,数据怎么形成,也是这个结构里面的回调函数负责。还是先看看read_vmcore吧:
static ssize_t read_vmcore(struct file *file, char __user *buffer, size_t buflen, loff_t *fpos)
{
ssize_t acc = 0, tmp;
size_t tsz;
u64 start, nr_bytes;
struct vmcore *curr_m = NULL;
if (buflen == 0 || *fpos >= vmcore_size)
return 0;
if (buflen > vmcore_size - *fpos)
buflen = vmcore_size - *fpos;
if (*fpos
tsz = elfcorebuf_sz - *fpos;
if (buflen
tsz = buflen;
if (copy_to_user(buffer, elfcorebuf + *fpos, tsz))//头部拷贝给用户
return -EFAULT;
buflen -= tsz;
*fpos += tsz;
buffer += tsz;
acc += tsz;
if (buflen == 0)
return acc;
}
start = map_offset_to_paddr(*fpos, &vmcore_list, &curr_m);//为了下面拷贝物理内存映像,这里必须设置好物理内存的地址参数,具体见下面的map_offset_to_paddr函数分析,这个函数本质上就是一个查找,linux内核中惯用的链表查找。
...//出错处理以及数据大小初始化和偏移更新等代码。
while (buflen) {
tmp = read_from_oldmem(buffer, tsz, &start, 1); //实际读取物理内存,注意这里读取的是整个物理内存,还记得吗?新启动的捕捉内核只是占用了一小部分内存,这部分内存一直保留着,在grub参数上可以设置其大小和位置。
if (tmp
return tmp;
buflen -= tsz;
*fpos += tsz;
buffer += tsz;
acc += tsz; //这里更新读取数量和偏移
if (start >= (curr_m->paddr + curr_m->size)) {
if (curr_m->list.next == &vmcore_list)
return acc;
curr_m = list_entry(curr_m->list.next, struct vmcore, list); //在vmcore_list取出下一个的vmcore结构体,继续读取并且送给用户空间,然后用户空间程序将之写入磁盘文件。
start = curr_m->paddr;
}
if ((tsz = (PAGE_SIZE - (start & ~PAGE_MASK))) > buflen)
tsz = buflen;
nr_bytes = (curr_m->size - (start - curr_m->paddr));
if (tsz > nr_bytes)
tsz = nr_bytes;
}
return acc;
}
以下这个函数就是读取物理内存了,没有什么重要的,在进入这个函数之前,参数已经可以说明一切了,buf就是要拷入的缓存地址,couont是需要的数量,ppos是偏移,最后一个参数说明是否向用户缓存拷贝数据。可以看出,也正是到了这里,实际读取的时候,垮掉内核的内存映像数据才最终形成可以拷贝的用户的形式,其实就是在copy_oldmem_page形成并拷贝的,本质上就是先将页面映射到捕捉内核的虚存,再拷贝数据,再解除映射。
static ssize_t read_from_oldmem(char *buf, size_t count, u64 *ppos, int userbuf)
{
unsigned long pfn, offset;
size_t nr_bytes;
ssize_t read = 0, tmp;
if (!count)
return 0;
offset = (unsigned long)(*ppos % PAGE_SIZE); //页面内部的偏移
pfn = (unsigned long)(*ppos / PAGE_SIZE); //页面的索引号
if (pfn > saved_max_pfn)
return -EINVAL;
do {
if (count > (PAGE_SIZE - offset))
nr_bytes = PAGE_SIZE - offset;
else
nr_bytes = count;
tmp = copy_oldmem_page(pfn, buf, nr_bytes, offset, userbuf); //循环将页面的数据拷贝给buf,注意这些页面根本不属于新启动的捕捉内核,这些页面还是垮掉内核的页面,并且还保留了一个现场。
...//惯例,调整偏移和数量以及出错处理。
offset = 0;
} while (count);
return read;
}
以下这个函数的本质很简单,就是查找:
static u64 map_offset_to_paddr(loff_t offset, struct list_head *vc_list, struct vmcore **m_ptr)
{
struct vmcore *m;
u64 paddr;
list_for_each_entry(m, vc_list, list) {
u64 start, end;
start = m->offset;
end = m->offset + m->size - 1;
if (offset >= start && offset
paddr = m->paddr + offset - start;
*m_ptr = m;
return paddr;
}
}
*m_ptr = NULL;
return 0;
}
最后,elfcorehdr_addr很重要,它记录了垮掉内核的映像地址,那么这个地址是怎么知道的呢?往往在kdump服务启动的时候会将这个参数传入给内核,当此内核垮掉的时候就会以此参数加上别的参数作为新的启动参数启动新的捕捉内核,正如下面的代码所示:
static void __init parse_cmdline_early (char ** cmdline_p)
{
...
else if (!memcmp(from, "elfcorehdr=", 11)) //这个elfcorehdr一般由kdump服务传输过来
elfcorehdr_addr = memparse(from+11, &from);
...
}
等到新的捕捉内核启动后,/proc/vmcore已经有了组织好的在捕捉内核启动过程中植入的链表,此时捕捉内核已经进入了用户空间,那么用户空间的kdump服务进程就会将/proc/vmcore读出,在读取的当时形成内存映像数据(读取前仅仅是一个vmcore_list链表),然后kdump服务进程将之写入一个磁盘文件,写完后系统不再继续把控制权交给用户而是直接重新启动,这是因为kdump用的是kexec机制,它启动一个捕捉内核就是用来捕获垮掉内核的内存映像的不是干别的的,因此没有必要继续启动下去,把/proc/vmcore拷贝到磁盘便于以后调试就完成了这次更换内核并重启的使命。捕捉内核启动到用户空间后,用户空间的程序是怎么知道该做什么了呢?这还是linux的老本行,机制和策略分离的益处,在/etc下有kdump的配置文件,你可以配置转储的内存映像存在什么位置以及用何格式存储,而且kdump是一个服务,在红帽子系列下可以用service start命令启动,服务就是守护进程,完全从配置文件中得到策略,这又是一个分层的问题,kexec是机制,kdump是kexec的一个策略性应用;kdump的内核内存转储是机制,而kdump的用户空间守护进程是其一个策略性应用;kdump守护进程是机制,而如何配置它的行为是一个策略性的行为。
最后注意一下转储调试时vmalloc分配的内核模块的问题。