绑定完请刷新页面
取消
刷新

分享好友

×
取消 复制
Netconsole实例源代码分析
2020-05-19 09:41:15

Netconsole实例源代码分析

By dremice

dremice.jiang@gmail.com 

1.前言

NetconsoleLinux2.6版内核的一个新的特性。它允许将本机的dmesg系统信息,通过网络的方式传送到另一台主机上。这样,就可以实现远程监控某台机子的kernel panic信息了。使用起来非常方便,也给开发人员调试内核提供了更加便捷的途径。

 

2.实例配置

由于2.6版本的内核本身已经支持Netconsole,并以模块形式编译进了内核。所以,下面的介绍都是基于2.6版内核,且本身在内核编译的时候,已经选择了将Netconsole以模块进行编译的方式。如果内核本身并没有编译Netconsole这个模块,那就需要重新编译内核了(笔者暂时没有找到其他的解决方案)。

2.1 监控主机的配置

监控主机有两种配置方式:使用本身的syslogd和利用netcat这个工具。Syslogd使用了514这个特定的UDP端口,而netcat工具可以指定任意未被使用的UDP端口进行监视。我这里使用的是netcat的方式进行监视的。安装好netcat后,执行shell命令:

# netcat -l -p 30000 –u

这里使用了端口30000来进行接收,这个端口是任意制定的,只要不发生冲突即可。

2.2 被监控主机的配置

在需要被监控的主机上加载运行netconsole模块,由于netconsole模块需要许多其他模块的依赖,以及在加载时必须配置相关的端口,IP地址等信息,所以shell执行命令如下:

#modprobe netconsole netconsole=6666@10.14.0.225/eth7,30000@10.14.0.220/00:0C:29:E2:87:E8

我们来分析一下这段加载命令。

Modprobe 这个模块加载命令是指在加载该模块时,同时加载其依赖的其它模块;

@前的6666是本地端口,这个我们可以在源码中看到,是一个源代码中默认的本地端口;

10.14.0.225/eth7:这里指配本机的IP地址及网卡名称(通常我们可以用ifconfig –a来查看到);

30000@10.14.0.220/00:0C:29:E2:87:E8:三个位段分别是,远端监控端口,远端IP地址及远端机子的MAC地址。也就是我们在2.1节中指定的监控端口30000,以及其IP地址和MAC地址了。

2.3 实例测试

以上配置就算完成了,让我来看一个实例吧。在被监控的主机上运行一个简单的hello world模块,看看我们的printk信息是怎样被发送到监控主机上去的。

模块helloworld源码:

hello.c

  1 /*hello.c*/

  2 #include

  3 #include

  4 #include

  5 MODULE_LICENSE("Dual BSD/GPL");

  6

  7 static int hello_init(void)

  8 {

  9         printk(KERN_ALERT "Hello World\n" );

 10         return 0;

 11 }

 12 static void hello_exit(void)

 13 {

 14         printk(KERN_ALERT "Goodbye World\n" );

 15 }

 16

 17 module_init(hello_init);

 18 module_exit(hello_exit);

 

Makefile:

  1 obj-m:=hello.o

  2 KDIR:=/lib/modules/$(shell uname -r)/build

  3 PWD:=$(shell pwd)

  4

  5 default:

  6         $(MAKE) -C $(KDIR) M=$(PWD) modules

  7

  8 clean:

  9         $(RM) *.o *.mod.c *.ko *.symvers

 

执行:

# make

# insmod hello.ko

在监控机的终端上,我们看到以下信息:

netconsole: network logging started

Hello World

条是我们先前加载netconsole模块时收到的,第二条是我们加载helloworld模块时收到的。

再运行rmmod hello

监控机终端上将看到:

netconsole: network logging started

Hello World

Goodbye World

 

OK,以上测试顺利完成,当然,我们也可以测试kernel panic的情况,例如操作一个内核的非法地址等。下面,我们开始来分析内核源码的netconsole的实现。

3Netconsole内核源码分析

3.1 几个重要的数据结构

3.1.1 struct console

Include/linux/console.h

/*

 * The interface for a console, or any other device that wants to capture

 * console messages (printer driver?)

 *

 * If a console driver is marked CON_BOOT then it will be auto-unregistered

 * when the first real console is registered.  This is for early-printk drivers.

 */

 

#define CON_PRINTBUFFER      (1)

#define CON_CONSDEV      (2) /* Last on the command line */

#define CON_ENABLED       (4)

#define CON_BOOT      (8)

#define CON_ANYTIME      (16) /* Safe to call when cpu is offline */

 

struct console

{

       char name[8];

       void (*write)(struct console *, const char *, unsigned);

       int    (*read)(struct console *, char *, unsigned);

       struct tty_driver *(*device)(struct console *, int *);

       void (*unblank)(void);

       int    (*setup)(struct console *, char *);

       short       flags;

       short       index;

       int    cflag;

       void *data;

       struct      console *next;

};

从上面的注释,我们看到,这个结构,是提供给那些需要捕捉console终端信息的设备的。我们捕获的dmesg的信息,实际上是用prink输出的,因此,我们必须构造这么一个结构,并实现其信息捕捉函数,即write函数。

3.1.2 struct netpoll

Include/linux/netpoll.h

struct netpoll {

       struct net_device *dev; //指向实际的网卡

       char dev_name[16], *name; //名字

       void (*rx_hook)(struct netpoll *, int, char *, int); //钩子函数,这里并没有用到

       void (*drop)(struct sk_buff *skb); //信息发送函数

       u32 local_ip, remote_ip; //本地ip地址和远端ip地址

       u16 local_port, remote_port;//本地端口和远端端口

       unsigned char local_mac[6], remote_mac[6];//本地mac地址和远端mac地址

};

这是netconsole实现的一个关键数据结构。net_device成员指向了实际的网卡,我们正是通过网络进行监视获取dmesg信息,因此就必须通过实际的网卡来发送数据。实际上,这个数据结构包含了一个网络包发送的所有关键字段(除了有效数据)。

3.1.3 struct net_device

Include/linux/netdevice.h

struct net_device

{

 

       /*

        * This is the first field of the "visible" part of this structure

        * (i.e. as seen by users in the "Space.c" file).  It is the name

        * the interface.

        */

       char               name[IFNAMSIZ];

       /* device name hash chain */

       struct hlist_node     name_hlist;

……

#ifdef CONFIG_NETPOLL

       struct netpoll_info       *npinfo;

#endif

#ifdef CONFIG_NET_POLL_CONTROLLER

       void                    (*poll_controller)(struct net_device *dev);

#endif

 

       /* bridge stuff */

       struct net_bridge_port    *br_port;

 

#ifdef CONFIG_NET_DIVERT

       /* this will get initialized at each interface type init routine */

       struct divert_blk     *divert;

#endif /* CONFIG_NET_DIVERT */

 

       /* class/net/name entry */

       struct class_device class_dev;

       /* space for optional statistics and wireless sysfs groups */

       struct attribute_group  *sysfs_groups[3];

};

net_device结构包含了一个网络设备相关的所有信息,这里并没有全部列出其成员,其中用粉红色标记出来的,正是和netpoll相关的结构。从这里我们可以看到,如果要支持netconsole,那么就必须编译netpoll,而编译netpoll,就必须配置CONFIG_NETPOLLCONFIG_NET_POLL_CONTROLLER这两个选项。

结构中,npinfo包含了与netpoll相关的一些重要信息。

3.1.4 struct netpoll_info

Include/linux/netpoll.h

struct netpoll_info {

       spinlock_t poll_lock; //spinlock防止并发访问

       int poll_owner; //所有者

       int tries;//如果发送失败,指定了发送信息的次数

       int rx_flags;

       spinlock_t rx_lock;

       struct netpoll *rx_np; /* netpoll that registered an rx_hook */

       struct sk_buff_head arp_tx; /* list of arp requests to reply to */

};

在后面的实现函数分析中,将看到这个结构的详细用处。

3.2 实现分析

Drivers/net/netconsole.h

 

static char config[256]; //定义模块加载时指定参数选项的buf

module_param_string(netconsole, config, 256, 0); //定义模块参数

 

//模块的描述,说明了模块加载时,指定参数及选项的格式

MODULE_PARM_DESC(netconsole, " netconsole=[src-port]@[src-ip]/[dev],[tgt-port]@/[tgt-macaddr]\n");

 

3.2.1 Netconsole初始化

前面提到,如果要对console的信息进行捕获,必须要实现一个struct console。因此,我们首先来看一看如何注册一个console结构。(看源码的时候,很多地方我使用的是英文注释)

/* netconsole module initialization */

static int init_netconsole(void)

{

       /* The config is the module parameter, which contains the capture options */

    //这个config就是上面的模块参数,在加载模块的时候它已经保存了必要的配置信息

       if(strlen(config))

              option_setup(config); //解析模块参数

 

       if(!configured) { //用户加载时,参数不对,配置失败

              printk("netconsole: not configured, aborting\n");

              return 0;

       }

 

       /* Setup the netpoll capture operations */

    if(netpoll_setup(&np)) //初始化建立netpoll的配置,np是一个struct netpoll结构全局变量,//指定了netpoll的相关信息

              return -EINVAL;

 

       /* register netconsole that can get the printk info */

       register_console(&netconsole); //注册netconsole

       printk(KERN_INFO "netconsole: network logging started\n");

       return 0;

}

 

我们首先看一看结构netconsole的定义,去解析对终端信息捕捉的具体操作:

/* Initialize the console capture... */

static struct console netconsole = {

       .name = "netcon",

       .flags = CON_ENABLED | CON_PRINTBUFFER,

       .write = write_msg //这就是对终端信息的捕捉函数

};

来看一下option_setup:

static int configured = 0;

static int option_setup(char *opt)

{

       /* parse the console capture options */

       configured = !netpoll_parse_options(&np, opt);

       return 1;

}

netpoll_parse_options()实现对模块参数的解析,并提取信息,保存到np结构中。这里不再详细解析该函数的实现,有兴趣可以阅读源代码。

 

下面分析netpoll_setup()函数的实现,在分析之前,先看一下np结构这个全局变量的定义:

static struct netpoll np = {

       .name = "netconsole", //名字定义

       .dev_name = "eth0", //默认的网络接口卡名称

       .local_port = 6665, //默认的本地端口定义

       .remote_port = 6666, //默认的远程监控端口定义

       .remote_mac = {0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, //默认的远程监控机MAC地址,实际上是一个广播地址

       .drop = netpoll_queue, //定义了netpoll在处理console信息的发送函数

};

 

int netpoll_setup(struct netpoll *np)

{

       struct net_device *ndev = NULL;

       struct in_device *in_dev;

       struct netpoll_info *npinfo;

       unsigned long flags;

   

    //检查是否指定了网络接口卡,如果指定了,并查找系统中是否存在这样一块网卡

       if (np->dev_name)

              /* find the network interface card */

              ndev = dev_get_by_name(np->dev_name);

       if (!ndev) {

              printk(KERN_ERR "%%s: %%s doesn't exist, aborting.\n",

                     np->name, np->dev_name);

              return -1;

       }

 

       np->dev = ndev; //np结构指定网络接口卡

       if (!ndev->npinfo) { //这里就是准备初始化前面说到的net_device结构这个重要的npinfo成员了

              npinfo = kmalloc(sizeof(*npinfo), GFP_KERNEL);

              if (!npinfo)

                     goto release;

 

              npinfo->rx_flags = 0;

              npinfo->rx_np = NULL; //不指定hook函数

              spin_lock_init(&npinfo->poll_lock); //初始化自旋锁

              npinfo->poll_owner = -1; //暂时无引用

              npinfo->tries = MAX_RETRIES; //设定如果发送失败的话,重复的大次数

              spin_lock_init(&npinfo->rx_lock);

              skb_queue_head_init(&npinfo->arp_tx); //arp相关的

       } else

              npinfo = ndev->npinfo;

 

       if (!ndev->poll_controller) { //检查netdev是否设定了poll_controller,否则将不能支持netconsole。它保证了在不使能中断的情况下,就可以发送skb,而且它并不是在执行终端例程的情况下被执行的。

              printk(KERN_ERR "%%s: %%s doesn't support polling, aborting.\n",

                     np->name, np->dev_name);

              goto release;

       }

 

       if (!netif_running(ndev)) { //检查网卡是否启动

              unsigned long atmost, atleast;

 

              printk(KERN_INFO "%%s: device %%s not up yet, forcing it\n",

                     np->name, np->dev_name);

 

              rtnl_lock();

              if (dev_change_flags(ndev, ndev->flags | IFF_UP) < 0) {

                     printk(KERN_ERR "%%s: failed to open %%s\n",

                            np->name, np->dev_name);

                     rtnl_unlock();

                     goto release;

              }

              rtnl_unlock();

 

              atleast = jiffies + HZ/10;

             atmost = jiffies + 4*HZ;

              while (!netif_carrier_ok(ndev)) {

                     if (time_after(jiffies, atmost)) {

                            printk(KERN_NOTICE

                                   "%%s: timeout waiting for carrier\n",

                                   np->name);

                            break;

                     }

                     cond_resched();

              }

 

              /* If carrier appears to come up instantly, we don't

               * trust it and pause so that we don't pump all our

               * queued console messages into the bitbucket.

               */

 

              if (time_before(jiffies, atleast)) {

                     printk(KERN_NOTICE "%%s: carrier detect appears"

                            " untrustworthy, waiting 4 seconds\n",

                            np->name);

                     msleep(4000);

              }

       }

 

       /* initialize the local mac, that why we need not set the local mac for the options */

   //初始化本地mac地址,在加载模块的时候,我们只是指定了监控机的mac地址,所以这里要进行本地mac地址的初始化。

       if (is_zero_ether_addr(np->local_mac) && ndev->dev_addr)

              memcpy(np->local_mac, ndev->dev_addr, 6);

 

       if (!np->local_ip) {//检查是否指定了本地ip,如果没有的话,主动去获取

              rcu_read_lock();

              in_dev = __in_dev_get_rcu(ndev);

 

              if (!in_dev || !in_dev->ifa_list) {

                     rcu_read_unlock();

                     printk(KERN_ERR "%%s: no IP address for %%s, aborting\n",

                            np->name, np->dev_name);

                     goto release;

              }

 

              np->local_ip = ntohl(in_dev->ifa_list->ifa_local);

              rcu_read_unlock();

              printk(KERN_INFO "%%s: local IP %%d.%%d.%%d.%%d\n",

                     np->name, HIPQUAD(np->local_ip));

       }

 

       if (np->rx_hook) {

              spin_lock_irqsave(&npinfo->rx_lock, flags);

              npinfo->rx_flags |= NETPOLL_RX_ENABLED;

              npinfo->rx_np = np;

              spin_unlock_irqrestore(&npinfo->rx_lock, flags);

       }

 

       /* fill up the skb queue */

       refill_skbs();

 

       /* last thing to do is link it to the net device structure */

       ndev->npinfo = npinfo;

 

       /* avoid racing with NAPI reading npinfo */

       synchronize_rcu();

 

       return 0;

 

 release:

       if (!ndev->npinfo)

              kfree(npinfo);

       np->dev = NULL;

       dev_put(ndev);

       return -1;

}

以上,初始化完成。

3.2.2 具体运行实现

在上一节开始就提到了netconsole这个结构的定义,它里面的一个重要的write函数的初始化,正是这个函数实现了对终端信息的捕捉。下面,我们就来分析一下write_msg()的具体实现:

#define MAX_PRINT_CHUNK 1000

 

static void write_msg(struct console *con, const char *msg, unsigned int len)

{

       int frag, left;

       unsigned long flags;

 

       if (!np.dev)

              return;

 

       local_irq_save(flags);

   

    //处理msg的发送,由于发送的包大小不能操作长度MAX_PRINT_CHUNK,所以可能分多次发送这些信息

       for(left = len; left; ) {

              /* the transport  msg should NOT larger than MAX_PRINT_CHUNK*/

              frag = min(left, MAX_PRINT_CHUNK);

              netpoll_send_udp(&np, msg, frag); /* send the msg */

              msg += frag;

              left -= frag;

       }

 

       local_irq_restore(flags);

}

 

我们看到,真正发送信息,实际上调用的是netpoll_send_udp()函数:

void netpoll_send_udp(struct netpoll *np, const char *msg, int len)

{

       int total_len, eth_len, ip_len, udp_len;

       struct sk_buff *skb;

       struct udphdr *udph;

       struct iphdr *iph;

       struct ethhdr *eth;

 

       udp_len = len + sizeof(*udph);

       ip_len = eth_len = udp_len + sizeof(*iph);

       total_len = eth_len + ETH_HLEN + NET_IP_ALIGN;

 

       skb = find_skb(np, total_len, total_len - len);

       if (!skb)

              return;

 

       memcpy(skb->data, msg, len);

       skb->len += len;

 

       udph = (struct udphdr *) skb_push(skb, sizeof(*udph));

       udph->source = htons(np->local_port);

       udph->dest = htons(np->remote_port);

       udph->len = htons(udp_len);

       udph->check = 0;

 

       iph = (struct iphdr *)skb_push(skb, sizeof(*iph));

 

       /* iph->version = 4; iph->ihl = 5; */

       put_unaligned(0x45, (unsigned char *)iph);

       iph->tos      = 0;

       put_unaligned(htons(ip_len), &(iph->tot_len));

       iph->id       = 0;

       iph->frag_off = 0;

       iph->ttl      = 64;

       iph->protocol = IPPROTO_UDP;

       iph->check    = 0;

       put_unaligned(htonl(np->local_ip), &(iph->saddr));

       put_unaligned(htonl(np->remote_ip), &(iph->daddr));

       iph->check    = ip_fast_csum((unsigned char *)iph, iph->ihl);

 

       eth = (struct ethhdr *) skb_push(skb, ETH_HLEN);

 

       eth->h_proto = htons(ETH_P_IP);

       memcpy(eth->h_source, np->local_mac, 6);

       memcpy(eth->h_dest, np->remote_mac, 6);

 

       skb->dev = np->dev;

 

       netpoll_send_skb(np, skb);

}

这个函数看起来并不复杂,主要是对一些控制信息的设置,后调用了netpoll_send_skp()来发送数据。接下来看看netpoll_send_skp()是怎样实现的:

static void netpoll_send_skb(struct netpoll *np, struct sk_buff *skb)

{

       int status;

       struct netpoll_info *npinfo;

 

    //相关检查

       if (!np || !np->dev || !netif_running(np->dev)) {

              __kfree_skb(skb);

              return;

       }

 

       npinfo = np->dev->npinfo;

 

       /* avoid recursion */

    //检查是不是本地cpu在处理发送信息

       if (npinfo->poll_owner == smp_processor_id() ||

           np->dev->xmit_lock_owner == smp_processor_id()) {

           /* for our own cpu */

        //本地cpu的情况,检查netpoll结构是否指定了drop函数,回到3.2.1节的初始化,我们为该结构指定的drop函数是netpoll_queue(在后面分析该函数的实现)

              if (np->drop) /* invoke the netpoll_queue of np to send the msg, the work_queue */

                     np->drop(skb);

              else

                     __kfree_skb(skb);

              return;

       }

 

    //非本地cpu以及drop函数并没有实现的情况

       do {

              /* the times of try to send the msg if it fails */

              npinfo->tries--;

              netif_tx_lock(np->dev);

 

              /*

               * network drivers do not expect to be called if the queue is

               * stopped.

               */

              status = NETDEV_TX_BUSY;

              if (!netif_queue_stopped(np->dev))

                     /* send the package directly */

            //直接调用了底层的skb数据包发送函数

                     status = np->dev->hard_start_xmit(skb, np->dev);

 

              netif_tx_unlock(np->dev);

 

              /* success */

              if(!status) {

                     npinfo->tries = MAX_RETRIES; /* reset */

                     return;

              }

 

              /* transmit busy */

        //发送繁忙的情况,进行poll操作,并尝试重新发包

              netpoll_poll(np);

              udelay(50);

       } while (npinfo->tries > 0);

}

 

后,剩下重要的netpoll_queue()函数的实现分析了。它的实现具有一定的神奇之处,慢慢来剖析吧。

void netpoll_queue(struct sk_buff *skb)

{

       unsigned long flags;

 

       if (queue_depth == MAX_QUEUE_DEPTH) {

              __kfree_skb(skb);

              return;

       }

 

       spin_lock_irqsave(&queue_lock, flags);

       if (!queue_head)

              queue_head = skb;

       else

              queue_tail->next = skb;

       queue_tail = skb;

       queue_depth++;

       spin_unlock_irqrestore(&queue_lock, flags);

 

       schedule_work(&send_queue);

}

从代码已开始,就看到了工作队列,先不说这个函数的实现,补习一下工作队列的相关知识。工作队列是和tasklet一类的东西,但本质上存在极大的差异。表现在:

1、  工作队列可以睡眠;

2、  工作队列可以运行在多cpu上(默认是同一处理器上);

3、  工作队类不必以原子化执行,它还可以延迟执行;

相比于tasklet,工作队列在实时性上就显得不足了。Tasklet可以在很短的时间内很快执行,并且以原子模式执行。关于工作队列更详细的描述,请参考LDD3p204206)。

回到这个程序,我们在看一下在这个函数之前的一些声明和定义:

static void queue_process(void *p)

{

       unsigned long flags;

       struct sk_buff *skb;

 

       while (queue_head) {

              spin_lock_irqsave(&queue_lock, flags);

 

              skb = queue_head;

              queue_head = skb->next;

              if (skb == queue_tail)

                     queue_head = NULL;

 

              queue_depth--;

 

              spin_unlock_irqrestore(&queue_lock, flags);

 

              dev_queue_xmit(skb);

       }

}

 

static DECLARE_WORK(send_queue, queue_process, NULL);

我们看到,在这里定义了work queuesend_queue,并指定了调用函数queue_process,没有传递参数。

可以看到,queue_process()函数,终调用函数dev_queue_xmit实现了数据包的发送。

重新回到函数netpoll_queue(),它实际上只是做了一些wrok queue深度的检查,并终调用函数schedule_work(&send_queue);实现对工作队列的调度,也就是终能够调度到queue_process()来完成数据包的发送任务。

4.总结

Netconsole提供了一种通过网络监控调试信息的便捷的方法,配置也十分简单。其源代码实现主要也精炼,由于使用了工作队列的机制,它可以安全的工作在中断上下文中。

但是,netconsole的使用仍有一些限制,正如kernelnetconsole.txt文档说的:“only IP networks, UDP packets and ethernet devices are supported”,它只能工作在IP网络中,并且使用不可靠的UDP连接,目前只支持以太网络设备。

期待在新的内核版本中,netconsole会有新的发展。


文章来源CU社区:Netconsole实例源代码分析


分享好友

分享这个小栈给你的朋友们,一起进步吧。

内核源码
创建时间:2020-05-18 13:36:55
内核源码精华帖内容汇总
展开
订阅须知

• 所有用户可根据关注领域订阅专区或所有专区

• 付费订阅:虚拟交易,一经交易不退款;若特殊情况,可3日内客服咨询

• 专区发布评论属默认订阅所评论专区(除付费小栈外)

技术专家

查看更多
  • 飘絮絮絮丶
    专家
戳我,来吐槽~