Linux内核--修复ext4日志(jbd2)bug

生产上报来了内核bug:mysql在做reset master时内核整个panic了。

DBA同学非常热心的帮忙找到了重新步骤:就是一个地雷一样的文件,只要open它,再fdatasync,kernel就panic。

从panic的代码位置看,就是 jbd2_journal_commit_transaction() 里的

J_ASSERT(journal->j_running_transaction != NULL);

判断失败触发panic

但是,为什么jbd2在没有running_transaction的时候也会提交事务?那就只能把所有唤醒kjournald2内核线程(里面调用了jbd2_journal_commit_transaction)的地方——即wake_up(&journal->j_wait_commit)处都加上trace,由于重现步骤是现成的,很快就定位到了原因:open一个文件再直接fdatasync的时候,会调用ext4_sync_file ,里面调用jbd2_log_start_commit开始提交jbd2的日志,jbd2_log_start_commit里会加锁然后调用__jbd2_log_start_commit,代码如下:

int __jbd2_log_start_commit(journal_t *journal, tid_t target)                    
{     
        /* 
         * Are we already doing a recent enough commit?                          
         */
        if (!tid_geq(journal->j_commit_request, target)) {
                /*
                 * We want a new commit: OK, mark the request and wakup the      
                 * commit thread.  We do _not_ do the commit ourselves.          
                 */
                journal->j_commit_request = target;
                jbd_debug(1, "JBD: requesting commit %d/%d\n",                   
                          journal->j_commit_request,
                          journal->j_commit_sequence);
                wake_up(&journal->j_wait_commit);
                return 1;
        }     
        return 0;
}

从trace的结果看,journal->j_commit_request的值为2177452108,而target的值为0,看上去j_commit_request显然比target小,应该不会走到if判断里面去,但是实际上是走了的,因为tid_geq的实现是:

static inline int tid_geq(tid_t x, tid_t y)
{
        int difference = (x - y);
        return (difference >= 0);
}

unsigned int型2177452108减去0然后转为int型,猜猜结果是多少?等于 -2117515188 !看上去好像tid_geq的实现又罗嗦又奇怪,于是翻了一下注释,才发现,jbd2给每个transaction一个tid,这个tid是不断增长的,而它又是个unsigned int型,所以容易溢出,于是弄出来这么一个tid_geq,把0看成是比2177452108更“晚”的tid,当commit_request为2177452108而target为0时,意思是:编号2177452108的tid已经提交了,0比2177452108更“晚”,所以有必要把0号transaction给commit一下,于是唤醒kjournald2(那句wake_up)。而这一唤醒,就发现没有running_transaction,于是悲剧了。

从trace看,大部分传入__jbd2_log_start_commit的target值都不是0,看来这个0来得蹊跷,翻了一下upstream的代码,找到了Ted在去年3月份提的一个patch:

commit 688f869ce3bdc892daa993534dc6df18c95df931
Author: Theodore Ts'o 
Date:   Wed Mar 16 17:16:31 2011 -0400
    ext4: Initialize fsync transaction ids in ext4_new_inode()
    When allocating a new inode, we need to make sure i_sync_tid and
    i_datasync_tid are initialized.  Otherwise, one or both of these two
    values could be left initialized to zero, which could potentially
    result in BUG_ON in jbd2_journal_commit_transaction.
    (This could happen by having journal->commit_request getting set to
    zero, which could wake up the kjournald process even though there is
    no running transaction, which then causes a BUG_ON via the
    J_ASSERT(j_ruinning_transaction != NULL) statement.
    Signed-off-by: "Theodore Ts'o" 
diff --git a/fs/ext4/ialloc.c b/fs/ext4/ialloc.c
index 2fd3b0e..a679a48 100644
--- a/fs/ext4/ialloc.c
+++ b/fs/ext4/ialloc.c
@@ -1054,6 +1054,11 @@ got:
                }
        }
+       if (ext4_handle_valid(handle)) {
+               ei->i_sync_tid = handle->h_transaction->t_tid;
+               ei->i_datasync_tid = handle->h_transaction->t_tid;
+       }
+
        err = ext4_mark_inode_dirty(handle, inode);
        if (err) {
                ext4_std_error(sb, err);

啊哈,就是它了,由于i_sync_tid和i_datasync_tid都没有正确赋值,所以带上了默认的0值,一路传给ext4_sync_file,而后面的__jbd2_log_start_commit又误认为0是一个要提交的新事务(其实此时还没有把当前事务挂到running_transaction上去),所以错误了。打上这个patch,再走重现步骤kernel也不panic了。

既然这么容易重现为什么其它机器上没有遇到?原因就是这个commit_request必须是一个很大的值,大到转为int型时会变为负数。我试了一下在ext4上不停的创建空文件并fdatasync之,10分钟左右commit_request才变为一百万,如果要让它到二十亿,至少还需要十四天,而线上的io压力毕竟没有人工压力测试那么大,所以几个月后commit_request才到二十亿,才触发了这个bug。

redhat最新的2.6.32-220内核是有这个问题的,大家多小心。

感谢@元云和@希羽两位同学帮忙提供了重现步骤,内核修bug,最难的就是重现,两位却直接把步骤提供出来了,真是太体贴太客气了!

======

本来想用ksplice来不重启升级内核,这样DBA就可以不重启机器修复这个bug,但是研究了一下ksplice,发现它要求加gcc参数 -ffunction-sections -fdata-sections 来编译内核,而这两个参数又和 -pg 参数冲突,而我们的kernel trace需要用到 -pg ,所以....目前无解,还没有办法用ksplice来帮助我们在线升级内核。

原文作者: 斯巴达第二季

原文地址:http://wp.me/p3N0Wv-U(版权归原文作者所有,侵权留言联系删除)

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Linux内核ext4文件系统是一种高性能、可靠性高的文件系统,其代码在内核源码树的/fs/ext4目录下。 代码文件主要包括以下几个部分: 1. ext4.h:定义了ext4文件系统的数据结构和相关常量。 2. super.c:实现了ext4文件系统超级块的读取和写入。 3. inode.c:实现了ext4文件系统inode节点的读取和写入。 4. namei.c:实现了ext4文件系统文件名的查找和创建。 5. dir.c:实现了ext4文件系统目录的读写操作。 6. file.c:实现了ext4文件系统文件的读写操作。 7. extents.c:实现了ext4文件系统的extents分配和管理。 8. inode_table.c:实现了ext4文件系统inode表的管理。 9. resize.c:实现了ext4文件系统的动态扩容和缩容。 10. journal.c:实现了ext4文件系统的日志功能。 下面以super.c文件为例,对其代码进行逐行注释介绍。 ```c /* * linux/fs/ext4/super.c * * Copyright (C) 1995-2006 Theodore Ts'o. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. */ ``` 代码开头是版权和许可证声明。 ```c #include <linux/module.h> #include <linux/fs.h> #include <linux/seq_file.h> #include <linux/parser.h> #include <linux/random.h> #include <linux/string.h> #include <linux/slab.h> #include <linux/time.h> #include <linux/uaccess.h> #include <linux/crc32c.h> #include <linux/buffer_head.h> #include <linux/init.h> #include <linux/magic.h> #include <linux/blkdev.h> #include <linux/backing-dev.h> #include <linux/kdev_t.h> #include <linux/sched.h> #include <linux/quotaops.h> #include <linux/pagemap.h> #include <linux/compat.h> #include <linux/falloc.h> #include <linux/atomic.h> #include <linux/fiemap.h> #include <linux/fscrypt.h> #include "ext4.h" #include "xattr.h" #include "acl.h" #include "ext4_jbd2.h" #include "mballoc.h" #include "extents.h" #include "ext4_extents.h" #include "ext4_inode.h" #include "ext4_raw.h" #include "htree.h" ``` 接下来是一些头文件的引用,包括了一些常用的内核函数和结构体定义。 ```c static void ext4_put_super(struct super_block *sb); static int ext4_sync_fs(struct super_block *sb, int wait); static int ext4_freeze(struct super_block *sb); static int ext4_unfreeze(struct super_block *sb); static int ext4_statfs(struct dentry *dentry, struct kstatfs *buf); static int ext4_remount(struct super_block *sb, int *flags, char *data); static int ext4_show_options(struct seq_file *seq, struct dentry *root); static int ext4_commit_super(struct super_block *sb, int sync); static int ext4_write_super(struct super_block *sb); ``` 这部分是函数的声明。 ```c static int ext4_fill_super(struct super_block *sb, void *data, int silent); ``` 这是ext4文件系统的核心函数,用于读取超级块和初始化文件系统。 ```c static struct dentry *ext4_mount(struct file_system_type *fs_type, int flags, const char *dev_name, void *data); ``` 这是文件系统挂载函数,用于将ext4文件系统挂载到指定设备上。 ```c static struct file_system_type ext4_fs_type = { .owner = THIS_MODULE, .name = "ext4", .mount = ext4_mount, .kill_sb = kill_block_super, .fs_flags = FS_REQUIRES_DEV, }; MODULE_ALIAS_FS("ext4"); MODULE_ALIAS("fs-ext4"); ``` 这部分定义了一个file_system_type结构体,用于注册ext4文件系统类型。其中mount指向ext4_mount函数,kill_sb指向kill_block_super函数。 ```c static int __init init_ext4_fs(void) { int err = init_ext4_fs_once(); if (err) return err; err = register_filesystem(&ext4_fs_type); if (err) goto out1; err = ext4_register_li_request(); if (err) goto out2; return 0; out2: unregister_filesystem(&ext4_fs_type); out1: destroy_ext4_fs(); return err; } module_init(init_ext4_fs); static void __exit exit_ext4_fs(void) { ext4_unregister_li_request(); unregister_filesystem(&ext4_fs_type); destroy_ext4_fs(); } module_exit(exit_ext4_fs); ``` 这部分是初始化和销毁ext4文件系统的函数。 以上就是ext4文件系统的主要代码,对其进行注释可以更好地理解它的实现原理和具体实现方式。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值