系统panic后主动广播最后内核dmesg信息-一个几乎可以使用的方案

本文介绍了一种在Linux内核panic时自动发送dmesg日志到网络的方法,通过编写特定模块捕获日志并在系统崩溃时广播出去。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在《 远程触发SYSRQ获取最新的dmesg信息-一个几乎没有什么用的方案》中,我认为远程触发SYSRQ并没有什么实际的用处,系统没有挂起时,用SSH等标准方式会好很多,系统挂起时,远程触发在多数情况下均无法得到响应。那么有什么方法在系统panic的时候通知外部呢?
       当然采用crash kexec kernel的方式会是一个好方法,但是那毕竟是为了debug而生的,有的时候只是想知道dmesg的最后几行就好,要知道此时系统已经完全没有机会记录日志了,用户态的任何进程均已经全军覆没。但是别忘了还有一个口子可以将最后的信息送出去,那就是网络。为了使得信息更加可能被接收,我觉得采用广播会比较好。具体的方式我在《 远程触发SYSRQ获取最新的dmesg信息-一个几乎没有什么用的方案》的最后已经给出了。本文主要给出一个切实可以运行的代码。这个代码和平时的代码有所不同,那就是这个代码是在系统已经panic了以后被触发的,此时的系统很可能全然混乱,因此此时最好的做法就是:
1.减少内存操作;
2.不处理异常流;
3.尽可能简化,不再计算校验值;
4.避免ARP交互,直接广播。
代码的逻辑如下:
1.加载模块:预先分配skb和get device,为了不在panic以后处理内存;注册通知链。
2.panic之后:调用通知链的通知操作,用最简化的方式广播没有校验码的数据包。

代码如下:


#include <linux/module.h>
#include <linux/skbuff.h>
#include <net/ip.h>

struct sk_buff * skb = NULL;
struct net_device * dev = NULL;

u8 ethhdr [ETH_HLEN] = {
        0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
        0x00, 0x0c, 0x29, 0xdd, 0xaa, 0xfd,
        0x00, 0x00
};

u8 iphdr [20] = {
        0x45,
        0x00,
        0x00, 0x00,  //total length
        0x00, 0x00,  //ID
        0x40, 0x00,  //Don't fragment
        0x40,        //TTL
        0xff,        // UDP?
        0x00, 0x00,  //checksum
        0x02, 0x02, 0x02, 0x02,
        0xff, 0xff, 0xff, 0xff
};

u8 data[1024] = {0};

void do_nothing (struct sk_buff *skb)
{
    /* 不再进行任何内存操作 */
    //dev_put(skb->dev);
}

static int send_last_msg(struct notifier_block *self,
            unsigned long event, void *unused)
{
    int ret;
    struct ethhdr *eth;
    struct iphdr *ip;
    char *p;

    /* 获取内核缓冲区中的数据 */
    kernel_log_buffer(data, sizeof(data));
 
    skb->dev = dev;
    skb->pkt_type = PACKET_OTHERHOST;
    skb->protocol = htons(ETH_P_IP);
    skb->ip_summed = CHECKSUM_NONE;
    skb->destructor = do_nothing;
    skb->priority = 0;

    /* 保留skb区域 */
    skb_reserve (skb, 2 + ETH_HLEN + sizeof(struct iphdr) + sizeof(data));

    /* 构造数据区(使用UDP会比较好,但是懒的封装了) */
    p = skb_push(skb, sizeof(data));
    memcpy(p, &data[0], sizeof(data));
    skb_reset_transport_header(skb);

    /* 构造IP头 */
    p = skb_push(skb, sizeof(struct iphdr));
    memcpy(p, &iphdr, sizeof(struct iphdr));
    ip = (struct iphdr*)p;
    ip->tot_len = htons(sizeof(data) + sizeof(struct iphdr));
    skb_reset_network_header(skb);

    /* 构造以太头 */
    p = skb_push(skb, sizeof(struct ethhdr));
    eth = (struct ethhdr*)p;
    eth->h_proto = htons(ETH_P_IP);
    memcpy(p, ðhdr, sizeof(struct ethhdr));
    skb_reset_mac_header(skb);

    /* 发射 */
    ret = dev_queue_xmit(skb);
    if (ret < 0) {
        /* 由于panic了,不再处理内存,不处理异常流 */
        //kfree_skb (skb);
        //dev_put(dev);
        goto out;
    }
out:
    return ret;
}

static struct notifier_block on_panic_send = {
        .notifier_call = send_last_msg,
};

static int __init
panic_sendmsg_init(void)
{
    int ret = -1;
    dev = dev_get_by_name (&init_net, "eth2");
    if (!dev) {
        printk("Can't get device\n");
        goto out;
    }
    /* 不在通知链中分配,因为那时已经panic,故预先分配 */
    skb = alloc_skb(1500, GFP_ATOMIC);
    if (!skb) {
        dev_put(dev);
        printk("Alloc skb failed\n");
        goto out;
    }
    /* 注册 */
    ret = atomic_notifier_chain_register(&panic_notifier_list, &on_panic_send);
    if (ret) {
        dev_put(dev);
        kfree_skb (skb);
        printk("Register notifier chain failed\n");
        goto out;
    }

    return 0;
out:
    return ret;
}

static void __exit
panic_sendmsg_exit(void)
{
    atomic_notifier_chain_unregister(&panic_notifier_list, &on_panic_send);
    if (dev) {
        dev_put(dev);
    }
    if (skb) {
        kfree_skb (skb);
    }
}

module_init(panic_sendmsg_init);
module_exit(panic_sendmsg_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("marywangran <marywangran@126.com>");

需要指出的是kernel_log_buffer接口,这个接口是我自己添加进内核并EXPORT出来的。我认为这是必要的,printk将信息放在了内核的一个缓冲区中,这个缓冲区可以被用户态进程通过syslog系统调用读取。我们知道,常规的办法是内核信息被syslog守护进程读取,然后发送到网络或者保存成文件。但是panic之后呢?既然用户态已经完全不再运行,那么就不存在通过什么守护进程存日志的希望了,当然在内核panic通知链钩子中直接操作磁盘日志文件是可以的,但是那太不可靠,因为磁盘的IO逻辑同样存在于内核态代码或者驱动代码中,此时内核已经panic了...虽然从网络发送数据也同样要用到已经panic的内核执行流,但是毕竟协议栈操作要比磁盘操作的调用深度要浅很多(数据的最终解释,排序,保存操作都在对端进行,磁盘IO则不然)。

       不管怎么样,不管是磁盘操作还是网络操作,都需要将内核log缓冲区中的内存dump下来才好,然而内核log缓冲区本身并没有被EXPORT,它只能通过不多的几个系统调用接口来获取,而这些接口很难在内核里面调用,因此我自己添加了一个:


int kernel_log_buffer(char *buf, int max_len);

其中,buf为内核log保存到的缓冲区,max_len为buf的长度,返回值为实际返回信息的长度。这个接口完全类似于read系统调用。
<think>嗯,用户问的是EXPORT_SYMBOL在内核模块中导出的函数是否可以被内核调用。我需要先确认自己对EXPORT_SYMBOL的理解。记得EXPORT_SYMBOL宏的作用是将符号(函数或变量)导出到内核的符号表,这样其他模块或者内核本身就可以访问这些符号。那问题就是,内核本身能否调用这个导出的函数呢? 首先,用户可能是在开发内核模块,需要让某个函数被其他模块或者内核的其他部分使用。EXPORT_SYMBOL确实用于这个目的。但内核本身是否可以调用,可能涉及到内核的导出符号机制。我需要回忆一下内核模块的运作方式。 内核模块在加载时,会连接到内核的符号表。当模块使用EXPORT_SYMBOL导出一个函数时,这个函数会被添加到全局符号表中。这样,其他模块在加载时就可以使用这些符号。那内核的核心部分(比如不是模块的部分,比如编译进内核的代码)是否可以使用这个导出的函数呢? 这里可能需要分情况讨论。如果内核的核心代码在编译时已经存在对这些符号的引用,那么可能需要这些符号在编译时就可用。但如果是动态加载的模块,导出的符号是在模块加载后才存在的,内核核心代码可能无法在运行时动态调用这些函数,除非有相应的机制,比如函数指针或者动态查找符号的方法。 不过,通常内核的核心部分(比如vmlinux)在编译时已经确定了所调用的函数,如果这些函数是来自其他模块,那么可能需要将这些模块编译进内核(built-in)而不是作为可加载模块。否则,当核心代码调用一个模块导出的函数时,该模块可能还没有加载,导致符号不存在。 但如果是另一个模块调用了这个导出的函数,那当然是可以的。比如模块A导出函数func,模块B在加载时依赖于模块A,那么模块B可以调用func。但如果是内核核心代码(非模块部分)调用模块导出的函数,这可能会有问题,因为核心代码在编译时无法解析该符号,除非该符号在编译时已经存在。 不过,用户的问题可能更倾向于,当模块加载后,内核的其他部分(比如其他子系统或者核心代码)能否调用该函数。这个时候,可能需要明确内核是否有机制在运行时查找并调用这些符号。比如,内核中有可能通过查找符号表来获取函数地址,然后调用。例如,使用kallsyms_lookup_name找到符号地址,然后进行调用。这种情况下,理论上是可以的,但需要注意模块是否已加载,以及函数是否确实被导出。 另外,用户可能没有考虑到模块的生命周期问题。如果内核的核心代码调用了一个模块导出的函数,而该模块被卸载了,那么会导致悬空指针,调用时引发崩溃。因此,通常不建议内核核心代码直接调用模块导出的函数,除非有严格的模块管理机制,比如引用计数,确保模块在使用期间不会被卸载。 总结来说,EXPORT_SYMBOL导出的函数可以被其他模块调用,也可以被内核中动态查找符号并调用的部分使用,但需要满足模块已加载且符号存在的条件。而内核核心代码在编译时若未包含对该符号的引用,则无法直接调用,除非使用动态查找的方式。同时,需要注意模块的生命周期管理,避免卸载后调用导致的错误。</think>EXPORT_SYMBOL在内核模块中的作用及调用规则可以通过以下步骤分析: 1. **基本原理** - `EXPORT_SYMBOL`宏的作用是将函数或变量添加到**内核符号表**中 - 被导出的符号会出现在`/proc/kallsyms`中(需root权限查看) - 符号的可见性由两个层面控制: * 编译层面:通过头文件声明 * 链接层面:通过符号表导出 2. **调用可行性** - ✅ **允许调用**的情况: * 内核其他模块通过`extern`声明后直接调用 * 内核核心代码(非模块代码)通过动态查找符号地址调用 - ❌ **禁止调用**的情况: * 未导出符号的模块内部函数 * 模块卸载后继续尝试调用(会导致kernel panic) 3. **生命周期管理** ```c // 模块加载时增加引用计数 try_module_get(THIS_MODULE); // 调用导出函数 exported_function(); // 使用完成后减少引用计数 module_put(THIS_MODULE); ``` 需要特别注意: - 模块卸载前必须确保没有未完成的引用 - 建议通过`/sys/module/[module_name]/refcnt`监控引用计数 4. **典型应用场景** - 设备驱动与内核系统交互 - 文件系统实现扩展 - 网络协议栈扩展开发 - 内核性能监控工具开发 5. **验证方法** - 使用`cat /proc/kallsyms | grep <函数名>`确认符号已导出 - 通过`dmesg`观察系统日志: ``` [ 256.478925] Module test_module loaded, exported_symbol at ffffffffc0a01234 ``` 6. **安全注意事项** - 导出的函数必须进行严格的参数校验 - 需要实现互斥锁机制防止竞态条件 - 必须处理所有可能的错误返回路径 - 建议遵循`GPL`兼容协议 **总结**:通过`EXPORT_SYMBOL`导出的内核模块函数可以被内核调用,但必须遵守以下条件: 1. 调用方在编译时能访问函数声明 2. 被调用模块已正确加载且保持加载状态 3. 遵守内核模块的引用计数规则 4. 满足内核版本兼容性要求 实际开发中推荐使用`EXPORT_SYMBOL_GPL`加强代码安全性,同时建议通过`MODULE_LICENSE("Dual BSD/GPL")`声明模块许可证。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值