Linux内核实践 - 如何添加网络协议[二]:实现

内核版本:2.6.34

实现思路:
      报文在网络协议栈中的流动,对于接收来讲,是对报文的脱壳的过程,由于报文是已知的输入,只要逐个解析协议号;对于发送来讲,是各层发送函数的嵌套调用,由于没有已知的输入,只能按事先设计好的协议进行层层构造。但无论报文怎样的流动,核心是报文所在设备(skb->dev)的变化,相当于各层之间传递的交接棒。
      按照上述思路,brcm协议接收的处理作为模块brcm_packet_type加入到ptype_base中就可以了;brcm协议发送的处理则复杂一点,发送的嵌套调用完全是依赖于设备来推动的,因此要有一种新创建的设备X,插入到vlan设备和网卡设备之间。
因此,至少要有brcm_packet_type来加入ptype_base和register_brcm_dev()来向系统注册设备X。进一步考虑,设备X在全局量init_net中有存储,但我们还需要知道设备X与vlan设备以及网卡设备是何种组织关系,所以在这里设计了brcm_group_hash来存储这种关系。为了对设备感兴趣的事件作出响应,添加自己的notifier到netdev_chain中。另外,为了用户空间具有一定控制能力(如创建、删除),还需要添加brcm相关的ioctl调用。为了让它看起来更完整,一种新的设备在proc中也应有对应项,用来调试和查看设备。
 

从最简单开始
      要让网络协议栈能够接收一种新协议是很简单的,由于已经有报文作为输入,我们要做的仅仅是编写好brcm_packet_type,然后在注册模块时只用做一件事:dev_add_pack。

static int __init brcm_proto_init(void)
{
 dev_add_pack(&brcm_packet_type);
}

static struct packet_type brcm_packet_type __read_mostly = {
 .type = cpu_to_be16(ETH_P_BRCM),
 .func = brcm_skb_recv, /* BRCM receive method */
};

int brcm_skb_recv(struct sk_buff *skb, struct net_device *dev,
    struct packet_type *ptype, struct net_device *orig_dev)
{
 struct brcm_hdr *bhdr;
 struct brcm_rx_stats *rx_stats;

 skb = skb_share_check(skb, GFP_ATOMIC);
 if(!skb)
  goto err_free;
 bhdr = (struct brcm_hdr *)skb->data;

 rcu_read_lock();
 skb_pull_rcsum(skb, BRCM_HLEN);
 // set protocol
 skb->protocol = bhdr->brcm_encapsulated_proto;
 // reorder skb
 skb = brcm_check_reorder_header(skb);
 if (!skb) 
  goto err_unlock;
 
 netif_rx(skb);
 rcu_read_unlock();
 return NET_RX_SUCCESS;

err_unlock:
 rcu_read_unlock();

err_free:
 kfree_skb(skb);
 return NET_RX_DROP;
}

      注册这个模块后,协议栈就能正常接收带brcm报头的报文的,代码中ETH_P_BRCM是brcm的协议号,BRCM_HLEN是brcm的报头长度。正是由于有报文作为输入,接收变得十分简单。
      但这仅仅是能接收而已,发送的报文还是不带brcm报头的,而且接收的这段代码也很粗略,没有变更skb的设备,没有记录流量,没有对brcm报头作有意义的处理,下面逐一进行添加。

设备的相关定义
      一种设备就是net_device类型,而每种设备都有自己的私有变量,它存储在net_device末尾,定义如下,其中real_dev指向下层设备,这是最基本属性,其余可以视需要自己设定,brcm_rx_stats则是该设备接收流量统计:

struct brcm_dev_info{
 struct net_device  *real_dev;
 u16 brcm_port;
 unsigned char  real_dev_addr[ETH_ALEN];
 struct proc_dir_entry *dent;
 struct brcm_rx_stats __percpu  *brcm_rx_stats;
};
struct brcm_rx_stats {
 unsigned long rx_packets;
 unsigned long rx_bytes;
 unsigned long multicast;
 unsigned long rx_errors;
};

 设备间的关系问题
      如果brcm仅仅是只有一个设备,则无需数据结构来存储这种关系,一个全局全变的brcm_dev就可以了。这里的设计考虑的是复杂的情况,可以存在多个下层设备,多个brcm设备,之间没有固定的关系。所以需要一种数据结构来存储这种关系- brcm_group_hash。下面是一个简单的图示: 

      各个数据结构定义如下:

static struct hlist_head brcm_group_hash[BRCM_GRP_HASH_SIZE];
struct brcm_group {
 struct hlist_node hlist;
 struct net_device *real_dev;
 int nr_ports;
 int killall;
 struct net_device *brcm_devices_array[BRCM_GROUP_ARRAY_LEN];
 struct rcu_head  rcu;
};

      brcm_group_hash作为全局变量存在,以hash表形式组织,brcm_group被插入到brcm_group_hash中,brcm_group存储了它与下层设备的关系(eth与brcm),real_dev指向e下层设备,而brcm设备则存储在brcm_devices_array数组中。
     下面完成由下层设备转换成brcm设备的函数,brcm_port是报头中的值,可以自己设定它的含义,这里设定它表示报文来自于哪个端口。

struct net_device *find_brcm_dev(struct net_device *real_dev, u16 brcm_port)
{
 struct brcm_group *grp = brcm_find_group(real_dev);
 if (grp) 
  brcm_dev = grp->brcm_devices_array[brcm_port];
 return NULL;
}

     因为在接收报文时,报文到达brcm层开始处理时,skb->dev指向的仍是下层设备,这时通过skb->dev查到brcm_group->real_dev相匹配的hash项,然后通过报文brcm报头的信息,确定brcm_group->brcm_devices_array中哪个brcm设备作为skb的新设备;
     而在发送报文时,报文到达brcm层开始处理时,skb->dev指向的是brcm设备,为了继续向下传递,需要变更为它的下层设备,在设备数据net_device的私有数据部分,一般会存储一个指针,指向它的下层设备,因此skb->dev只要变更为brcm_dev_info(dev)->real_dev。
 

流量统计
      在数据结构中,brcm设备的私有数据brcm_dev_info中brcm_rx_stats记录接收的流量信息;而dev->_tx[index]则会记录发送的流量信息。
      在接收函数brcm_skb_rcv()中对于成功接收的报文会增加流量统计:

rx_stats = per_cpu_ptr(brcm_dev_info(skb->dev)->brcm_rx_stats,
  smp_processor_id());
rx_stats->rx_packets++;
rx_stats->rx_bytes += skb->len;

      在发送函数brcm_dev_hard_start_xmit()中对于发送的报文会增加相应流量统计:

if (likely(ret == NET_XMIT_SUCCESS)) {
txq->tx_packets++;
txq->tx_bytes += len;
} else
 txq->tx_dropped++;

      而brcm_netdev_ops->ndo_get_stats()即brcm_dev_get_stats()函数,则会将brcm网卡设备中记录的发送和接收流量信息汇总成通用的格式net_device_stats,像ifconfig等命令使用的就是net_device_stats转换后的结果。 

完整收发函数
      有了这些后接收函数brcm_skb_recv()就可以完整了,其中关于报头brcm_hdr的处理可以略过,由于是空想的协议,含义是可以自己设定的:

int brcm_skb_recv(struct sk_buff *skb, struct net_device *dev,
    struct packet_type *ptype, struct net_device *orig_dev)
{
 struct brcm_hdr *bhdr;
 struct brcm_rx_stats *rx_stats;
 int op, brcm_port;

 skb = skb_share_check(skb, GFP_ATOMIC);
 if(!skb)
  goto err_free;
 bhdr = (struct brcm_hdr *)skb->data;
 op = bhdr->brcm_tag.brcm_53242_op;
 brcm_port = bhdr->brcm_tag.brcm_53242_src_portid- 23;

 rcu_read_lock();

 // drop wrong brcm tag packet
 if (op != BRCM_RCV_OP || brcm_port < 1 
  || brcm_port > 27) 
  goto err_unlock;

 skb->dev = find_brcm_dev(dev, brcm_port);
 if (!skb->dev) {
  goto err_unlock;
 }

 rx_stats = per_cpu_ptr(brcm_dev_info(skb->dev)->brcm_rx_stats,
          smp_processor_id());
 rx_stats->rx_packets++;
 rx_stats->rx_bytes += skb->len;
 skb_pull_rcsum(skb, BRCM_HLEN);

 switch (skb->pkt_type) {
 case PACKET_BROADCAST: /* Yeah, stats collect these together.. */
  /* stats->broadcast ++; // no such counter :-( */
  break;

 case PACKET_MULTICAST:
  rx_stats->multicast++;
  break;

 case PACKET_OTHERHOST:
  /* Our lower layer thinks this is not local, let's make sure.
   * This allows the VLAN to have a different MAC than the
   * underlying device, and still route correctly.
   */
  if (!compare_ether_addr(eth_hdr(skb)->h_dest,
     skb->dev->dev_addr))
   skb->pkt_type = PACKET_HOST;
  break;
 default:
  break;
 }
 
 // set protocol
 skb->protocol = bhdr->brcm_encapsulated_proto;

 // reorder skb
 skb = brcm_check_reorder_header(skb);
 if (!skb) {
  rx_stats->rx_errors++;
  goto err_unlock;
 }
 
 netif_rx(skb);
 rcu_read_unlock();
 return NET_RX_SUCCESS;

err_unlock:
 rcu_read_unlock();

err_free:
 kfree_skb(skb);
 return NET_RX_DROP;
}

      同时,发送函数brcm_dev_hard_start_xmit()可以完整了,同样,其中关于brcm_hdr的处理可以略过:

static netdev_tx_t brcm_dev_hard_start_xmit(struct sk_buff *skb,
         struct net_device *dev)
{
 int i = skb_get_queue_mapping(skb);
 struct netdev_queue *txq = netdev_get_tx_queue(dev, i);
 struct brcm_ethhdr *beth = (struct brcm_ethhdr *)(skb->data);
 unsigned int len;
 u16 brcm_port;
 int ret;

 /* Handle non-VLAN frames if they are sent to us, for example by DHCP.
  *
  * NOTE: THIS ASSUMES DIX ETHERNET, SPECIFICALLY NOT SUPPORTING
  * OTHER THINGS LIKE FDDI/TokenRing/802.3 SNAPs...
  */
 if (beth->h_brcm_proto != htons(ETH_P_BRCM)){
  //unsigned int orig_headroom = skb_headroom(skb);
  brcm_t brcm_tag;
  brcm_port = brcm_dev_info(dev)->brcm_port;
  if (brcm_port == BRCM_ANY_PORT) {
   brcm_tag.brcm_op_53242 = 0;
   brcm_tag.brcm_tq_53242 = 0;
   brcm_tag.brcm_te_53242 = 0;
   brcm_tag.brcm_dst_53242 = 0;
  }else {
   brcm_tag.brcm_op_53242 = BRCM_SND_OP;
   brcm_tag.brcm_tq_53242 = 0;
   brcm_tag.brcm_te_53242 = 0;
   brcm_tag.brcm_dst_53242 = brcm_port + 23;
  }

  skb = brcm_put_tag(skb, *(u32 *)(&brcm_tag));
  if (!skb) {
   txq->tx_dropped++;
   return NETDEV_TX_OK;
  }
 }

 skb_set_dev(skb, brcm_dev_info(dev)->real_dev);
 len = skb->len;
 ret = dev_queue_xmit(skb);

 if (likely(ret == NET_XMIT_SUCCESS)) {
  txq->tx_packets++;
  txq->tx_bytes += len;
 } else
  txq->tx_dropped++;

 return ret;
}

注册设备
      接收通过dev_add_pack(),就可以融入协议栈了,前面几篇的分析已经讲过通过ptype_base对报文进行脱壳。现在要融入的发送,函数已经完成了,既然发送是一种嵌套的调用,并且是由dev来推过的,那么发送函数的融入一定在设备进行注册时,作为设备的一种发送方法。
      创建一种设备时,一定会有设备的XXX_setup()初始化,大部分设备都会用ether_setup()来作初始化,再进行适当更改。下面是brcm_setup():

void brcm_setup(struct net_device *dev)
{
 ether_setup(dev);

 dev->priv_flags  |= IFF_BRCM_TAG;
 dev->priv_flags  &= ~IFF_XMIT_DST_RELEASE;
 dev->tx_queue_len = 0;

 dev->netdev_ops  = &brcm_netdev_ops;
 dev->destructor  = free_netdev;
 dev->ethtool_ops = &brcm_ethtool_ops;

 memset(dev->broadcast, 0, ETH_ALEN);
}

      其中发送函数就在brcm_netdev_ops中,每层设备都会这样调用:dev->netdev_ops->ndo_start_xmit()。     

static const struct net_device_ops brcm_netdev_ops = {
	.ndo_change_mtu		= brcm_dev_change_mtu,
	.ndo_init		= brcm_dev_init,
	.ndo_uninit		= brcm_dev_uninit,
	.ndo_open		= brcm_dev_open,
	.ndo_stop		= brcm_dev_stop,
	.ndo_start_xmit =  brcm_dev_hard_start_xmit,
	.ndo_validate_addr	= eth_validate_addr,
	.ndo_set_mac_address	= brcm_dev_set_mac_address,
	.ndo_set_rx_mode	= brcm_dev_set_rx_mode,
	.ndo_set_multicast_list	= brcm_dev_set_rx_mode,
	.ndo_change_rx_flags	= brcm_dev_change_rx_flags,
	//.ndo_do_ioctl		= brcm_dev_ioctl,
	.ndo_neigh_setup	= brcm_dev_neigh_setup,
	.ndo_get_stats		= brcm_dev_get_stats,
};
       而设备的初始化应该发生在创建设备时,也就是向网络注册它时,也就是register_brcm_dev(),注册一个新设备,需要知道它的下层设备real_dev以及唯一标识brcm设备的brcm_port。首先确定该设备没有被创建,然后用alloc_netdev_mq创建新设备new_dev,然后设置相关属性,特别是它的私有属性brcm_dev_info(new_dev),然后添加它到brcm_group_hash中,最后发生真正的注册register_netdevice()。

static int register_brcm_dev(struct net_device *real_dev, u16 brcm_port)
{
 struct net_device *new_dev;
 struct net *net = dev_net(real_dev);
 struct brcm_group *grp;
 char name[IFNAMSIZ];
 int err;

 if(brcm_port >= BRCM_PORT_MASK)
  return -ERANGE;

 // exist yet
 if (find_brcm_dev(real_dev, brcm_port) != NULL)
  return -EEXIST;

 snprintf(name, IFNAMSIZ, "brcm%i", brcm_port);
 new_dev = alloc_netdev_mq(sizeof(struct brcm_dev_info), name,
      brcm_setup, 1);
 if (new_dev == NULL)
  return -ENOBUFS;
 new_dev->real_num_tx_queues = real_dev->real_num_tx_queues;
 dev_net_set(new_dev, net);
 new_dev->mtu = real_dev->mtu;

 brcm_dev_info(new_dev)->brcm_port = brcm_port;
 brcm_dev_info(new_dev)->real_dev = real_dev;
 brcm_dev_info(new_dev)->dent = NULL;
 //new_dev->rtnl_link_ops = &brcm_link_ops;

 grp = brcm_find_group(real_dev);
 if (!grp)
  grp = brcm_group_alloc(real_dev);
 
 err = register_netdevice(new_dev);
 if (err < 0)
  goto out_free_newdev;
 
 /* Account for reference in struct vlan_dev_info */
 dev_hold(real_dev);
 brcm_group_set_device(grp, brcm_port, new_dev);

 return 0;

out_free_newdev:
 free_netdev(new_dev);
 return err;
}

ioctl
      由于brcm设备可以存在多个,并且和下层设备不是固定的对应关系,因此它的创建应该可以人为控制,因此通过ioctl由用户进行创建。这里只为brcm提供了两种操作-添加与删除。一种设备添加一定是与下层设备成关系的,因此添加时需要手动指明这种下层设备,然后通过__dev_get_by_name()从网络空间中找到这种设备,就可以调用register_brcm_dev()来完成注册了。而设备的删除则是直接删除,直接删除unregister_brcm_dev()。
     

static int brcm_ioctl_handler(struct net *net, void __user *arg)
{
	int err;
	struct brcm_ioctl_args args;
	struct net_device *dev = NULL;

	if (copy_from_user(&args, arg, sizeof(struct brcm_ioctl_args)))
		return -EFAULT;

	/* Null terminate this sucker, just in case. */
	args.device1[23] = 0;
	args.u.device2[23] = 0;

	rtnl_lock();

	switch (args.cmd) {
	case ADD_BRCM_CMD:
	case DEL_BRCM_CMD:
		err = -ENODEV;
		dev = __dev_get_by_name(net, args.device1);
		if (!dev)
			goto out;

		err = -EINVAL;
		if (args.cmd != ADD_BRCM_CMD && !is_brcm_dev(dev))
			goto out;
	}

	switch (args.cmd) {
	case ADD_BRCM_CMD:
		err = -EPERM;
		if (!capable(CAP_NET_ADMIN))
			break;
		err = register_brcm_dev(dev, args.u.port);
		break;

	case DEL_BRCM_CMD:
		err = -EPERM;
		if (!capable(CAP_NET_ADMIN))
			break;
		unregister_brcm_dev(dev, NULL);
		err = 0;
		break;
		
	default:
		err = -EOPNOTSUPP;
		break;
	}
out:
	rtnl_unlock();
	return err;
}

        这些是brcm协议模块的主体部分了,当然它还不完整,在下篇中继续完成brcm协议的添加,为它完善一些细节:proc文件系统, notifier机制等等,以及内核Makefile的编写,当然还有协议的测试。相关源码在下篇中打包上传。

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值