精华内容
下载资源
问答
  • Linux之虚拟网卡驱动
    2021-02-20 19:49:27

    目录

     

    一、网卡驱动程序框架

    二、编写虚拟网卡驱动

    2.1 测试直接注册net_device 

    2.2 提供发包函数 

    2.3 添加统计信息

    2.4 构造假包ping通


    一、网卡驱动程序框架

    网卡驱动程序时网络驱动程序的一部分,并不是网络驱动程序,网络驱动程序分为很多层,在这里只是最底层,最终会操作到硬件,在硬件上,有硬件相关层的驱动程序,即我们要写的网卡驱动

    网卡驱动需要具备收发能力,使数据底层到上层,也能从上层到底层,大多驱动程序都是以面向对象的思想实现,参考真正的网卡驱动程序cs89x0.c,在入口函数中分配了一个net_dev结构体,设置提供发包函数hard_start_xmit函数,在中断中提供收包的功能netif_rx函数,并且注册register_netdev

    int __init init_module(void)
    {
    	struct net_device *dev = alloc_etherdev(sizeof(struct net_local));
    ...
    	ret = cs89x0_probe1(dev, io, 1);
    	if (ret)
    		goto out;
    
    	dev_cs89x0 = dev;
    	return 0;
    out:
    	free_netdev(dev);
    	return ret;
    }
    
    static int __init
    cs89x0_probe1(struct net_device *dev, int ioaddr, int modular)
    {
    	struct net_local *lp = netdev_priv(dev);
    	static unsigned version_printed;
    	int i;
    	int tmp;
    	unsigned rev_type = 0;
    	int eeprom_buff[CHKSUM_LEN];
    	int retval;
    
    ...
    
    	dev->open		= net_open;
    	dev->stop		= net_close;
    	dev->tx_timeout		= net_timeout;
    	dev->watchdog_timeo	= HZ;
    	dev->hard_start_xmit 	= net_send_packet;
    	dev->get_stats		= net_get_stats;
    	dev->set_multicast_list = set_multicast_list;
    	dev->set_mac_address 	= set_mac_address;
    #ifdef CONFIG_NET_POLL_CONTROLLER
    	dev->poll_controller	= net_poll_controller;
    #endif
    
    	printk("\n");
    	if (net_debug)
    		printk("cs89x0_probe1() successful\n");
    
    	retval = register_netdev(dev);
    
    ...
    }
    
    static irqreturn_t net_interrupt(int irq, void *dev_id)
    {
    	struct net_device *dev = dev_id;
    	struct net_local *lp;
    	int ioaddr, status;
    
    ...
    		case ISQ_RECEIVER_EVENT:
    			/* Got a packet(s). */
    			net_rx(dev);
    			break;
    		case ISQ_TRANSMITTER_EVENT:
    			lp->stats.tx_packets++;
    			netif_wake_queue(dev);
    			if ((status & TX_OK) == 0) lp->stats.tx_errors++;
    			if (status & TX_LOST_CRS) lp->stats.tx_carrier_errors++;
    			if (status & TX_SQE_ERROR) lp->stats.tx_heartbeat_errors++;
    			if (status & TX_LATE_COL) lp->stats.tx_window_errors++;
    			if (status & TX_16_COL) lp->stats.tx_aborted_errors++;
    			break;
    ...
    }
    
    static void
    net_rx(struct net_device *dev)
    {
    	struct net_local *lp = netdev_priv(dev);
    	struct sk_buff *skb;
    	int status, length;
    
    ...
    	netif_rx(skb);
    	dev->last_rx = jiffies;
    	lp->stats.rx_packets++;
    	lp->stats.rx_bytes += length;
    }
    

    hard_start_xmit和netif_rx之间传输的sk_buff,总体框架如下

    APP:socket
     

    -----------------------

    -----------------------

    -----------------------

    -----------------------

    若干层网络协议(纯软件)
    发包函数hard_start_xmit

       ||  sk_buff     /\

       \/                  ||

    收包函数netif_rx
    硬件相关的驱动程序(要提供hard_start_xmit,有数据时,用netif_rx上报)
    硬件

    写网卡驱动程序:

        1. 分配一个net_device结构体
        2. 设置:
        2.1 发包函数: hard_start_xmit
        2.2 收到数据时(在中断处理函数里)用netif_rx上报数据
        2.3 其他设置
        3. 注册: register_netdev
    在cs89x0.c中分配net_dev结构体使用如下,若想自己修改名称,直接使用alloc_netdev函数来分配,ether_setup为默认的设置函数,sizeof_priv为私有数据,alloc_netdev可以多分配出一块内存,其中有指针可以指向这块内存

    struct net_device *alloc_etherdev(int sizeof_priv)
    {
        return alloc_netdev(sizeof_priv, "eth%d", ether_setup);
    }

    二、编写虚拟网卡驱动

    2.1 测试直接注册net_device 

    #include <linux/module.h>
    #include <linux/errno.h>
    #include <linux/netdevice.h>
    #include <linux/etherdevice.h>
    #include <linux/kernel.h>
    #include <linux/types.h>
    #include <linux/fcntl.h>
    #include <linux/interrupt.h>
    #include <linux/ioport.h>
    #include <linux/in.h>
    #include <linux/skbuff.h>
    #include <linux/slab.h>
    #include <linux/spinlock.h>
    #include <linux/string.h>
    #include <linux/init.h>
    #include <linux/bitops.h>
    #include <linux/delay.h>
    
    #include <asm/system.h>
    #include <asm/io.h>
    #include <asm/irq.h>
    
    static struct net_device *vnet_dev;
    
    static int virt_net_init(void)
    {
    	/* 1. 分配一个net_device结构体 */
    	vnet_dev = alloc_netdev(0, "vnet%d", ether_setup);;  /* alloc_etherdev */
    
    	/* 2. 设置 */
    
    	/* 3. 注册 */
    	register_netdev(vnet_dev);
    	
    	return 0;
    }
    
    static void virt_net_exit(void)
    {
    	unregister_netdev(vnet_dev);
    	free_netdev(vnet_dev);
    }
    
    module_init(virt_net_init);
    module_exit(virt_net_exit);
    
    MODULE_AUTHOR("xiaoma"); //可以写入作者信息
    MODULE_LICENSE("GPL");

    编译驱动程序,加载到单板上,其中eth0为网卡DM9000,vnet0则是我们注册的网卡驱动,在没有提供发包函数,在这里能够ping得通自己,说明在网络层纯软件中来回,IP是纯软件的概念,跟硬件无关,当我们ping 3.3.3.4后发出去会调用到hard_start_xmit发包函数,这里没有编写,因此会死机,为什么ping 3.3.3.4的时候从网卡vnet0出去,是因为两者之间处于同一网段,所以当PC机有两个网卡时,最好不要处于同一网段,否则ping一个IP或者访问一个主机的时候,操作系统不知道把数据发给哪个网卡

    # insmod virt_net.ko
    # ifconfig vnet0 3.3.3.3
    # ifconfig
    eth0      Link encap:Ethernet  HWaddr 00:60:6E:33:44:55
              inet addr:192.168.0.19  Bcast:192.168.0.255  Mask:255.255.255.0
              UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
              RX packets:1770 errors:0 dropped:0 overruns:0 frame:0
              TX packets:720 errors:0 dropped:0 overruns:0 carrier:0
              collisions:0 txqueuelen:1000
              RX bytes:2170957 (2.0 MiB)  TX bytes:111980 (109.3 KiB)
              Interrupt:51 Base address:0xa000

    lo        Link encap:Local Loopback
              inet addr:127.0.0.1  Mask:255.0.0.0
              UP LOOPBACK RUNNING  MTU:16436  Metric:1
              RX packets:0 errors:0 dropped:0 overruns:0 frame:0
              TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
              collisions:0 txqueuelen:0
              RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)

    vnet0     Link encap:Ethernet  HWaddr 00:00:00:00:00:00
              inet addr:3.3.3.3  Bcast:3.255.255.255  Mask:255.0.0.0
              UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
              RX packets:0 errors:0 dropped:0 overruns:0 frame:0
              TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
              collisions:0 txqueuelen:1000
              RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)

    # ping 3.3.3.3
    PING 3.3.3.3 (3.3.3.3): 56 data bytes
    64 bytes from 3.3.3.3: seq=0 ttl=64 time=1.064 ms
    64 bytes from 3.3.3.3: seq=1 ttl=64 time=0.463 ms
    64 bytes from 3.3.3.3: seq=2 ttl=64 time=0.466 ms
    64 bytes from 3.3.3.3: seq=3 ttl=64 time=0.463 ms
    64 bytes from 3.3.3.3: seq=4 ttl=64 time=0.461 ms
    64 bytes from 3.3.3.3: seq=5 ttl=64 time=0.458 ms
    64 bytes from 3.3.3.3: seq=6 ttl=64 time=0.464 ms

    --- 3.3.3.3 ping statistics ---
    7 packets transmitted, 7 packets received, 0% packet loss
    round-trip min/avg/max = 0.458/0.548/1.064 ms

    # ping 3.3.3.4
    PING 3.3.3.4 (3.3.3.4): 56 data bytes
    Unable to handle kernel NULL pointer dereference at virtual address 00000000
    pgd = c0004000
    [00000000] *pgd=00000000
    Internal error: Oops: 0 [#1]
    Modules linked in: virt_net
    CPU: 0    Not tainted  (2.6.22.6 #1)
    PC is at __init_begin+0x3fff8000/0x30
    LR is at dev_hard_start_xmit+0x1a8/0x240                                   //没有发包函数
    pc : [<00000000>]    lr : [<c0245798>]    psr: 60000013
    sp : c035ddd8  ip : c035ddfc  fp : c035ddf8
    r10: c06cec00  r9 : 04030303  r8 : c03c79b8
    r7 : c06cec00  r6 : c07d6780  r5 : c07d6780  r4 : c06cec00
    r3 : 00000000  r2 : c06e50a0  r1 : c06cec00  r0 : c07d6780
    Flags: nZCv  IRQs on  FIQs on  Mode SVC_32  Segment kernel
    Control: c000717f  Table: 33ecc000  DAC: 00000017
    Process swapper (pid: 0, stack limit = 0xc035c258)
    Stack: (0xc035ddd8 to 0xc035e000)
    ddc0:                                                       c06cec00 00000000
    dde0: c07d6780 c06cec2c 03030303 c035de18 c035ddfc c0252680 c0245600 c06cec00
    de00: c07d6780 00000000 c07189a0 c035de34 c035de1c c0245994 c02525e0 c06f5160
    de20: c07d6140 00000000 c035de44 c035de38 c027ee70 c0245840 c035de64 c035de48
    de40: c027eebc c027ee70 03030303 00000000 c06ced30 00000000 c035dea4 c035de68
    ...

    2.2 提供发包函数 

    对于真实的网卡,virt_net_send_packet会把skb里的数据通过网卡发送出去

    static int virt_net_send_packet(struct sk_buff *skb, struct net_device *dev)
    {
        static int cnt = 0;
        printk("virt_net_send_packet cnt = %d\n", ++cnt);
        return 0;
    }

    vnet_dev->hard_start_xmit = virt_net_send_packet;

    重新加载驱动,ping 3.3.3.4成功,发送了8次,但是重新查看数据信息,TX packets为0

     # ifconfig vnet0 3.3.3.3
    # ping 3.3.3.4
    PING 3.3.3.4 (3.3.3.4): 56 data bytes
    virt_net_send_packet cnt = 1
    virt_net_send_packet cnt = 2
    virt_net_send_packet cnt = 3
    virt_net_send_packet cnt = 4
    virt_net_send_packet cnt = 5
    virt_net_send_packet cnt = 6
    virt_net_send_packet cnt = 7
    virt_net_send_packet cnt = 8

    ...

    # ifconfig
    eth0      Link encap:Ethernet  HWaddr 00:60:6E:33:44:55
              inet addr:192.168.0.19  Bcast:192.168.0.255  Mask:255.255.255.0
              UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
              RX packets:3250 errors:0 dropped:0 overruns:0 frame:0
              TX packets:900 errors:0 dropped:0 overruns:0 carrier:0
              collisions:0 txqueuelen:1000
              RX bytes:2734036 (2.6 MiB)  TX bytes:141182 (137.8 KiB)
              Interrupt:51 Base address:0xa000

    lo        Link encap:Local Loopback
              inet addr:127.0.0.1  Mask:255.0.0.0
              UP LOOPBACK RUNNING  MTU:16436  Metric:1
              RX packets:10 errors:0 dropped:0 overruns:0 frame:0
              TX packets:10 errors:0 dropped:0 overruns:0 carrier:0
              collisions:0 txqueuelen:0
              RX bytes:1120 (1.0 KiB)  TX bytes:1120 (1.0 KiB)

    vnet0     Link encap:Ethernet  HWaddr 00:00:00:00:00:00
              inet addr:3.3.3.3  Bcast:3.255.255.255  Mask:255.0.0.0
              UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
              RX packets:0 errors:0 dropped:0 overruns:0 frame:0
              TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
              collisions:0 txqueuelen:1000
              RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)

    2.3 添加统计信息

    在net_device结构体中有struct net_device_stats stats成员包含了信息

    struct net_device_stats
    {
    	unsigned long	rx_packets;		/* total packets received	*/
    	unsigned long	tx_packets;		/* total packets transmitted	*/
    	unsigned long	rx_bytes;		/* total bytes received 	*/
    	unsigned long	tx_bytes;		/* total bytes transmitted	*/
    	unsigned long	rx_errors;		/* bad packets received		*/
    	unsigned long	tx_errors;		/* packet transmit problems	*/
    	unsigned long	rx_dropped;		/* no space in linux buffers	*/
    	unsigned long	tx_dropped;		/* no space available in linux	*/
    	unsigned long	multicast;		/* multicast packets received	*/
    	unsigned long	collisions;
    
    	/* detailed rx_errors: */
    	unsigned long	rx_length_errors;
    	unsigned long	rx_over_errors;		/* receiver ring buff overflow	*/
    	unsigned long	rx_crc_errors;		/* recved pkt with crc error	*/
    	unsigned long	rx_frame_errors;	/* recv'd frame alignment error */
    	unsigned long	rx_fifo_errors;		/* recv'r fifo overrun		*/
    	unsigned long	rx_missed_errors;	/* receiver missed packet	*/
    
    	/* detailed tx_errors */
    	unsigned long	tx_aborted_errors;
    	unsigned long	tx_carrier_errors;
    	unsigned long	tx_fifo_errors;
    	unsigned long	tx_heartbeat_errors;
    	unsigned long	tx_window_errors;
    	
    	/* for cslip etc */
    	unsigned long	rx_compressed;
    	unsigned long	tx_compressed;
    };

     修改hard_start_xmit函数

    static int virt_net_send_packet(struct sk_buff *skb, struct net_device *dev)
    {
    	static int cnt = 0;
    	printk("virt_net_send_packet cnt = %d\n", ++cnt);
    
    	/* 更新统计信息 */
    	dev->stats.tx_packets++;
    	dev->stats.tx_bytes += skb->len;
    	
    	return 0;
    }

    设置MAC地址,参考cs89x0.c

     vnet_dev = alloc_netdev(0, "vnet%d", ether_setup);
        vnet_dev->dev_addr[0] = 0x08;
        vnet_dev->dev_addr[1] = 0x89;
        vnet_dev->dev_addr[2] = 0x89;
        vnet_dev->dev_addr[3] = 0x89;
        vnet_dev->dev_addr[4] = 0x89;
        vnet_dev->dev_addr[5] = 0x11;

    代码如下:

    #include <linux/module.h>
    #include <linux/errno.h>
    #include <linux/netdevice.h>
    #include <linux/etherdevice.h>
    #include <linux/kernel.h>
    #include <linux/types.h>
    #include <linux/fcntl.h>
    #include <linux/interrupt.h>
    #include <linux/ioport.h>
    #include <linux/in.h>
    #include <linux/skbuff.h>
    #include <linux/slab.h>
    #include <linux/spinlock.h>
    #include <linux/string.h>
    #include <linux/init.h>
    #include <linux/bitops.h>
    #include <linux/delay.h>
    
    #include <asm/system.h>
    #include <asm/io.h>
    #include <asm/irq.h>
    
    static struct net_device *vnet_dev;
    
    static int virt_net_send_packet(struct sk_buff *skb, struct net_device *dev)
    {
    	static int cnt = 0;
    	printk("virt_net_send_packet cnt = %d\n", ++cnt);
    
    	/* 对于真实的网卡, 把skb里的数据通过网卡发送出去 */
    
    	/* 更新统计信息 */
    	dev->stats.tx_packets++;
    	dev->stats.tx_bytes += skb->len;
    	
    	return 0;
    }
    
    
    static int virt_net_init(void)
    {
    	/* 1. 分配一个net_device结构体 */
    	vnet_dev = alloc_netdev(0, "vnet%d", ether_setup);;  /* alloc_etherdev */
    
    	/* 2. 设置 */
    	vnet_dev->hard_start_xmit = virt_net_send_packet;
    
    	/* 设置MAC地址 */
        vnet_dev->dev_addr[0] = 0x08;
        vnet_dev->dev_addr[1] = 0x89;
        vnet_dev->dev_addr[2] = 0x89;
        vnet_dev->dev_addr[3] = 0x89;
        vnet_dev->dev_addr[4] = 0x89;
        vnet_dev->dev_addr[5] = 0x11;
    
    	/* 3. 注册 */
    	register_netdev(vnet_dev);
    	
    	return 0;
    }
    
    static void virt_net_exit(void)
    {
    	unregister_netdev(vnet_dev);
    	free_netdev(vnet_dev);
    }
    
    module_init(virt_net_init);
    module_exit(virt_net_exit);
    
    MODULE_AUTHOR("xiaoma");
    MODULE_LICENSE("GPL");
    

    测试结果如下: 

    # rmmod virt_net
    # insmod virt_net.ko
    # ifconfig vnet0 3.3.3.3
    # ping 3.3.3.4
    PING 3.3.3.4 (3.3.3.4): 56 data bytes
    virt_net_send_packet cnt = 1
    virt_net_send_packet cnt = 2
    virt_net_send_packet cnt = 3
    virt_net_send_packet cnt = 4
    virt_net_send_packet cnt = 5
    virt_net_send_packet cnt = 6

    --- 3.3.3.4 ping statistics ---
    6 packets transmitted, 0 packets received, 100% packet loss
    # ifconfig
    eth0      Link encap:Ethernet  HWaddr 00:60:6E:33:44:55
              inet addr:192.168.0.19  Bcast:192.168.0.255  Mask:255.255.255.0
              UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
              RX packets:4684 errors:0 dropped:0 overruns:0 frame:0
              TX packets:1023 errors:0 dropped:0 overruns:0 carrier:0
              collisions:0 txqueuelen:1000
              RX bytes:3144891 (2.9 MiB)  TX bytes:157598 (153.9 KiB)
              Interrupt:51 Base address:0xa000

    lo        Link encap:Local Loopback
              inet addr:127.0.0.1  Mask:255.0.0.0
              UP LOOPBACK RUNNING  MTU:16436  Metric:1
              RX packets:16 errors:0 dropped:0 overruns:0 frame:0
              TX packets:16 errors:0 dropped:0 overruns:0 carrier:0
              collisions:0 txqueuelen:0
              RX bytes:1792 (1.7 KiB)  TX bytes:1792 (1.7 KiB)

    vnet0     Link encap:Ethernet  HWaddr 08:89:89:89:89:11
              inet addr:3.3.3.3  Bcast:3.255.255.255  Mask:255.0.0.0
              UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
              RX packets:0 errors:0 dropped:0 overruns:0 frame:0
              TX packets:6 errors:0 dropped:0 overruns:0 carrier:0
              collisions:0 txqueuelen:1000
              RX bytes:0 (0.0 B)  TX bytes:252 (252.0 B)

    2.4 构造假包ping通

    假设有IP为3.3.3.4的设备,当我们ping它的时候,会发包(hard_start_xmit)给它,它收到包后会返回数据,有数据时要用(netif_rx)上报,就可以ping通,由于现在是虚拟网卡,并没有真正的网线,也没有3.3.3.4的设备,这里构造假包,直接返回数据,这样我们就可以ping得通

    构造一个假的sk_buff,上报,函数自己实现

    emulator_rx_packet(skb, dev);

    参考cs89x0.c,对于真正的网卡,首先会停止该网卡的队列,把skb的数据写入网卡,释放skb就会有中断产生,数据发送完后会在中断函数唤醒网卡的队列,在这里直接唤醒

        /* 对于真实的网卡, 把skb里的数据通过网卡发送出去 */
        netif_stop_queue(dev); /* 停止该网卡的队列 */
        /* ...... */           /* 把skb的数据写入网卡,这里会操作网卡 */

        /* 构造一个假的sk_buff,上报 */
        emulator_rx_packet(skb, dev);

        dev_kfree_skb (skb);   /* 释放skb */
        netif_wake_queue(dev); /* 数据全部发送出去后,唤醒网卡的队列 */

    编写emulator_rx_packet,参考LDD3,包中有源目的长度三要素,数据放在skb->data中

    static void emulator_rx_packet(struct sk_buff *skb, struct net_device *dev)
    {
    	unsigned char *type;
    	struct iphdr *ih;
    	__be32 *saddr, *daddr, tmp;
    	unsigned char	tmp_dev_addr[ETH_ALEN];
    	struct ethhdr *ethhdr;
    	
    	struct sk_buff *rx_skb;
    		
    	// 从硬件读出/保存数据
    	/* 对调"源/目的"的mac地址 */
    	ethhdr = (struct ethhdr *)skb->data;
    	memcpy(tmp_dev_addr, ethhdr->h_dest, ETH_ALEN);
    	memcpy(ethhdr->h_dest, ethhdr->h_source, ETH_ALEN);
    	memcpy(ethhdr->h_source, tmp_dev_addr, ETH_ALEN);
    
    	/* 对调"源/目的"的ip地址 */    
    	ih = (struct iphdr *)(skb->data + sizeof(struct ethhdr));
    	saddr = &ih->saddr;
    	daddr = &ih->daddr;
    
    	tmp = *saddr;
    	*saddr = *daddr;
    	*daddr = tmp;
    	
    	//((u8 *)saddr)[2] ^= 1; /* change the third octet (class C) */
    	//((u8 *)daddr)[2] ^= 1;
    	type = skb->data + sizeof(struct ethhdr) + sizeof(struct iphdr);
    	//printk("tx package type = %02x\n", *type);
    	// 修改类型, 原来0x8表示ping
    	*type = 0; /* 0表示reply */
    	
    	ih->check = 0;		   /* and rebuild the checksum (ip needs it) */
    	ih->check = ip_fast_csum((unsigned char *)ih,ih->ihl);
    	
    	// 构造一个sk_buff
    	rx_skb = dev_alloc_skb(skb->len + 2);  //保留2个字节
    	skb_reserve(rx_skb, 2); /* align IP on 16B boundary */	
    	memcpy(skb_put(rx_skb, skb->len), skb->data, skb->len);//环型暖冲区
    
    	/* Write metadata, and then pass to the receive level */
    	rx_skb->dev = dev;
    	rx_skb->protocol = eth_type_trans(rx_skb, dev);
    	rx_skb->ip_summed = CHECKSUM_UNNECESSARY; /* don't check it */
    	dev->stats.rx_packets++;  //更新统计信息
    	dev->stats.rx_bytes += skb->len;
    
    	// 提交sk_buff
    	netif_rx(rx_skb);
    }

    对于skb->data,一开始是MAC头,在MAC中有目的MAC,源MAC,还有h_proto,当我们返回数据时,需要把目的和源调换一下位置,MAC头紧接着就是IP头,有源IP和目的IP,同样需要调换,还有check校验码用ip_fast_csum函数来校验,IP头紧接着就是type和数据,type对于0x8原来表示ping包,修改为0表示回复包

    目的MAC源MAC...源IP目的IP...type数据

    struct ethhdr {
        unsigned char    h_dest[ETH_ALEN];    /* destination eth addr    */
        unsigned char    h_source[ETH_ALEN];    /* source ether addr    */
        __be16        h_proto;        /* packet type ID field    */
    } __attribute__((packed)); 

    ethhdr = (struct ethhdr *)skb->data;

    struct iphdr {
    #if defined(__LITTLE_ENDIAN_BITFIELD)
        __u8    ihl:4,
            version:4;
    #elif defined (__BIG_ENDIAN_BITFIELD)
        __u8    version:4,
              ihl:4;
    #else
    #error    "Please fix <asm/byteorder.h>"
    #endif
        __u8    tos;
        __be16    tot_len;
        __be16    id;
        __be16    frag_off;
        __u8    ttl;
        __u8    protocol;
        __sum16    check;
        __be32    saddr;
        __be32    daddr;
        /*The options start here. */
    };

    ih = (struct iphdr *)(skb->data + sizeof(struct ethhdr));

    加上相关标志,才能ping通

        vnet_dev->flags           |= IFF_NOARP;
        vnet_dev->features        |= NETIF_F_NO_CSUM;

    整体代码:

    #include <linux/module.h>
    #include <linux/errno.h>
    #include <linux/netdevice.h>
    #include <linux/etherdevice.h>
    #include <linux/kernel.h>
    #include <linux/types.h>
    #include <linux/fcntl.h>
    #include <linux/interrupt.h>
    #include <linux/ioport.h>
    #include <linux/in.h>
    #include <linux/skbuff.h>
    #include <linux/slab.h>
    #include <linux/spinlock.h>
    #include <linux/string.h>
    #include <linux/init.h>
    #include <linux/bitops.h>
    #include <linux/delay.h>
    #include <linux/ip.h>  // 添加ip头文件
    
    #include <asm/system.h>
    #include <asm/io.h>
    #include <asm/irq.h>
    
    static struct net_device *vnet_dev;
    
    static void emulator_rx_packet(struct sk_buff *skb, struct net_device *dev)
    {
    	/* 参考LDD3 */
    	unsigned char *type;
    	struct iphdr *ih;
    	__be32 *saddr, *daddr, tmp;
    	unsigned char	tmp_dev_addr[ETH_ALEN];
    	struct ethhdr *ethhdr;
    	
    	struct sk_buff *rx_skb;
    		
    	// 从硬件读出/保存数据
    	/* 对调"源/目的"的mac地址 */
    	ethhdr = (struct ethhdr *)skb->data;
    	memcpy(tmp_dev_addr, ethhdr->h_dest, ETH_ALEN);
    	memcpy(ethhdr->h_dest, ethhdr->h_source, ETH_ALEN);
    	memcpy(ethhdr->h_source, tmp_dev_addr, ETH_ALEN);
    
    	/* 对调"源/目的"的ip地址 */    
    	ih = (struct iphdr *)(skb->data + sizeof(struct ethhdr));
    	saddr = &ih->saddr;
    	daddr = &ih->daddr;
    
    	tmp = *saddr;
    	*saddr = *daddr;
    	*daddr = tmp;
    	
    	//((u8 *)saddr)[2] ^= 1; /* change the third octet (class C) */
    	//((u8 *)daddr)[2] ^= 1;
    	type = skb->data + sizeof(struct ethhdr) + sizeof(struct iphdr);
    	//printk("tx package type = %02x\n", *type);
    	// 修改类型, 原来0x8表示ping
    	*type = 0; /* 0表示reply */
    	
    	ih->check = 0;		   /* and rebuild the checksum (ip needs it) */
    	ih->check = ip_fast_csum((unsigned char *)ih,ih->ihl);
    	
    	// 构造一个sk_buff
    	rx_skb = dev_alloc_skb(skb->len + 2);
    	skb_reserve(rx_skb, 2); /* align IP on 16B boundary */	
    	memcpy(skb_put(rx_skb, skb->len), skb->data, skb->len);
    
    	/* Write metadata, and then pass to the receive level */
    	rx_skb->dev = dev;
    	rx_skb->protocol = eth_type_trans(rx_skb, dev);
    	rx_skb->ip_summed = CHECKSUM_UNNECESSARY; /* don't check it */
    	dev->stats.rx_packets++;
    	dev->stats.rx_bytes += skb->len;
    
    	// 提交sk_buff
    	netif_rx(rx_skb);
    }
    
    static int virt_net_send_packet(struct sk_buff *skb, struct net_device *dev)
    {
    	static int cnt = 0;
    	printk("virt_net_send_packet cnt = %d\n", ++cnt);
    
    	/* 对于真实的网卡, 把skb里的数据通过网卡发送出去 */
    	netif_stop_queue(dev); /* 停止该网卡的队列 */
        /* ...... */           /* 把skb的数据写入网卡 */
    
    	/* 构造一个假的sk_buff,上报 */
    	emulator_rx_packet(skb, dev);
    
    	dev_kfree_skb (skb);   /* 释放skb */
    	netif_wake_queue(dev); /* 数据全部发送出去后,唤醒网卡的队列 */
    
    	/* 更新统计信息 */
    	dev->stats.tx_packets++;
    	dev->stats.tx_bytes += skb->len;
    	
    	return 0;
    }
    
    
    static int virt_net_init(void)
    {
    	/* 1. 分配一个net_device结构体 */
    	vnet_dev = alloc_netdev(0, "vnet%d", ether_setup);;  /* alloc_etherdev */
    
    	/* 2. 设置 */
    	vnet_dev->hard_start_xmit = virt_net_send_packet;
    
    	/* 设置MAC地址 */
        vnet_dev->dev_addr[0] = 0x08;
        vnet_dev->dev_addr[1] = 0x89;
        vnet_dev->dev_addr[2] = 0x89;
        vnet_dev->dev_addr[3] = 0x89;
        vnet_dev->dev_addr[4] = 0x89;
        vnet_dev->dev_addr[5] = 0x11;
    
        /* 设置下面两项才能ping通 */
    	vnet_dev->flags           |= IFF_NOARP;
    	vnet_dev->features        |= NETIF_F_NO_CSUM;	
    
    	/* 3. 注册 */
    	register_netdev(vnet_dev);
    	
    	return 0;
    }
    
    static void virt_net_exit(void)
    {
    	unregister_netdev(vnet_dev);
    	free_netdev(vnet_dev);
    }
    
    module_init(virt_net_init);
    module_exit(virt_net_exit);
    
    MODULE_AUTHOR("xiaoma");
    MODULE_LICENSE("GPL");

     测试结果:能ping 3.3.3.4并查看统计信息

    # rmmod virt_net
    # insmod virt_net.ko
    # ifconfig vnet0 3.3.3.3
    # ping 3.3.3.4
    PING 3.3.3.4 (3.3.3.4): 56 data bytes
    virt_net_send_packet cnt = 1
    64 bytes from 3.3.3.4: seq=0 ttl=64 time=0.838 ms
    virt_net_send_packet cnt = 2
    64 bytes from 3.3.3.4: seq=1 ttl=64 time=0.518 ms
    virt_net_send_packet cnt = 3
    64 bytes from 3.3.3.4: seq=2 ttl=64 time=0.504 ms
    virt_net_send_packet cnt = 4
    64 bytes from 3.3.3.4: seq=3 ttl=64 time=0.442 ms
    virt_net_send_packet cnt = 5
    64 bytes from 3.3.3.4: seq=4 ttl=64 time=0.502 ms

    --- 3.3.3.4 ping statistics ---
    5 packets transmitted, 5 packets received, 0% packet loss
    round-trip min/avg/max = 0.442/0.560/0.838 ms

    ifconfig

    ...
    vnet0     Link encap:Ethernet  HWaddr 08:89:89:89:89:11
              inet addr:3.3.3.3  Bcast:3.255.255.255  Mask:255.0.0.0
              UP BROADCAST RUNNING NOARP MULTICAST  MTU:1500  Metric:1
              RX packets:5 errors:0 dropped:0 overruns:0 frame:0
              TX packets:5 errors:0 dropped:0 overruns:0 carrier:0
              collisions:0 txqueuelen:1000
              RX bytes:490 (490.0 B)  TX bytes:490 (490.0 B)

     

    更多相关内容
  • 回环网卡驱动1.回环网卡和普通网卡的区别是他是虚拟的不是实际的物理网卡,它相当于把普通网卡的发送端和接收端短接在一起。2.在内核源代码里的回环网卡程序(drivers/net/loopback.c)不是以一个模块的形式给出,但是...

    回环网卡驱动

    1.回环网卡和普通网卡的区别是他是虚拟的不是实际的物理网卡,它相当于把普通网卡的发送端和接收端短接在一起。

    2.在内核源代码里的回环网卡程序(drivers/net/loopback.c)不是以一个模块的形式给出,但是他的初始化(loopback_net_init)和退出函数(loopback_dev_free)会被内核的其他部分调用到。

    3.参照网卡初始化的流程图进行设计驱动程序,其中分配net_device结构不能用alloc_etherdev函数,因为该函数是分配以太网卡的结构体的,要用alloc_netdev函数来给回环网卡分配结构体。参考内核源代码别人如何用使用这个函数alloc_netdev(0, "lo", loopback_setup);第一个0表示net_device这个结构体的私有成员的大小,一般选择0,第二个表示网卡名字,ifconfig显示的名称,第三个就是具体的网卡结构体的初始化函数指针。

    struct net_device *dev;

    dev = alloc_netdev(0, "lo", loopback_setup);

    最终会调用到loopback_setup函数。(至于在将函数指针作为参数的时候如何传递形参,就要复习C语言了)

    4.回环网卡不需要初始化硬件。所以直接注册回环网卡的结构体到内核(第三步)。最后指定回环网卡注册到网络子系统。

    net->loopback_dev = dev;这一步很关键。

    5.具体的初始化(loopback_setup)(第二步)

    (1)基地址,MAC地址以及中断号都用不着,主要是netdev_ops这个结构体,他包含了这个网卡支持的操作。

    (2)表示回环网卡支持的最大的接收数据的包的大小,除了正式数据,还有相关网络协议的头部分。

    dev->mtu          = (16 * 1024) + 20 + 20 + 12;有效数据一般定义为16KB。

    (3)加上表示回环网卡专有的标志。

    dev->flags          = IFF_LOOPBACK;

    (4)加上构造报头的结构体指针,这个结构体指针指向的结构体成员是众多构造以太网报头的函数指针。

    dev->header_ops          = &eth_header_ops;

    理想查找可看到

    extern const struct header_ops eth_header_ops;

    struct header_ops {

    int    (*create) (struct sk_buff *skb, struct net_device *dev,

    unsigned short type, const void *daddr,

    const void *saddr, unsigned len);

    int    (*parse)(const struct sk_buff *skb, unsigned char *haddr);

    int    (*rebuild)(struct sk_buff *skb);

    #define HAVE_HEADER_CACHE

    int    (*cache)(const struct neighbour *neigh, struct hh_cache *hh);

    void    (*cache_update)(struct hh_cache *hh,

    const struct net_device *dev,

    const unsigned char *haddr);

    };

    6.数据发送

    static int loopback_net_xmit(struct sk_buff *skb,struct net_device *dev)

    {

    skb->protocol = eth_type_trans(skb,dev);

    packets++;

    bytes += skb->len;

    netif_rx(skb);

    return 0;

    }

    }

    (1)第一个参数是协议栈传送给回环网卡的包数据,第二个参数是回环网卡的结构体。

    (2)停止发送队列

    通知上层暂停送数据,好让txd发送已送达的数据,但是不涉及硬件,所以在回环网卡可忽略。相应的,将数据写入寄存器和唤醒再次发送以及释放队列就可忽略。

    (3)信息统计,表明上层送下来的包的协议

    skb->protocol = eth_type_trans(skb, dev);

    (4)统计发送过来的数据大小以及包的个数。

    bytes += skb->len;

    netif_rx(skb);

    7.由于从协议栈来的数据包(skb)存放到txd,而且txd不需要往外发送,txd和rxd“连”在一起,所以直接在发送部分调用普通网卡的接收部分的netif_rx(skb)函数,所以发送的同时就完成了接收。同时我们更清楚地看到在发送的时候不能释放skb,否则没有可接收的数据。

    8.实现获取网卡状态的函数

    static struct net_device_stats *loopback_get_stats(struct net_device *dev)

    {

    struct net_device_stats *stats = &dev->stats;

    stats->rx_packets = packets;

    stats->tx_packets = packets;

    stats->rx_bytes = bytes;

    stats->tx_bytes = bytes;

    return stats;

    }

    从这个结构体可以获取网卡状态信息

    /* The main device statistics structure */

    struct rtnl_link_stats64 {

    __u64    rx_packets;          /* total packets received    */

    __u64    tx_packets;          /* total packets transmitted    */

    __u64    rx_bytes;          /* total bytes received      */

    __u64    tx_bytes;          /* total bytes transmitted    */

    __u64    rx_errors;          /* bad packets received          */

    __u64    tx_errors;          /* packet transmit problems    */

    __u64    rx_dropped;          /* no space in linux buffers    */

    __u64    tx_dropped;          /* no space available in linux    */

    __u64    multicast;          /* multicast packets received    */

    __u64    collisions;

    /* detailed rx_errors: */

    __u64    rx_length_errors;

    __u64    rx_over_errors;          /* receiver ring buff overflow    */

    __u64    rx_crc_errors;          /* recved pkt with crc error    */

    __u64    rx_frame_errors;    /* recv'd frame alignment error */

    __u64    rx_fifo_errors;          /* recv'r fifo overrun          */

    __u64    rx_missed_errors;    /* receiver missed packet    */

    /* detailed tx_errors */

    __u64    tx_aborted_errors;

    __u64    tx_carrier_errors;

    __u64    tx_fifo_errors;

    __u64    tx_heartbeat_errors;

    __u64    tx_window_errors;

    /* for cslip etc */

    __u64    rx_compressed;

    __u64    tx_compressed;

    };

    主要是这四个成员

    stats->rx_packets = packets;

    stats->tx_packets = packets;

    stats->rx_bytes  = bytes;

    stats->tx_bytes  = bytes;

    。实际上自己可以重写这个获取状态的函数,因为struct net_device *dev有一个成员就是struct net_device_stats    stats;所以可以在重写的时候定义一个struct net_device_stats    stats指向形参传递进来的回环网卡结构体的stats成员,同样只需要注意stats成员的上述四个成员即可。

    9.在退出该驱动的时候就是取消注册结构体。达到注销网卡的目的。

    static __net_exit void loopback_net_exit(struct net *net)

    {

    struct net_device *dev = net->loopback_dev;

    unregister_netdev(dev);

    }

    下面为范例代码

    #include

    #include

    #include

    #include

    #include

    #include

    #include

    #include

    #include

    #include

    #include

    #include

    #include

    #include

    #include

    #include

    #include

    #include

    #include

    #include

    #include

    #include

    #include /* For the statistics structure. */

    #include /* For ARPHRD_ETHER */

    #include

    #include

    #include

    #include

    #include

    unsigned long packets = 0;

    unsigned long bytes = 0;

    //struct net_device *dev;

    static __net_exit void loopback_net_exit(struct net *net)

    {

    struct net_device *dev = net->loopback_dev;

    unregister_netdev(dev);

    }

    static int loopback_net_xmit(struct sk_buff *skb,struct net_device *dev)

    {

    skb->protocol = eth_type_trans(skb,dev);

    packets++;

    bytes += skb->len;

    netif_rx(skb);

    return 0;

    }

    static struct net_device_stats *loopback_get_stats(struct net_device *dev)

    {

    struct net_device_stats *stats = &dev->stats;

    stats->rx_packets = packets;

    stats->tx_packets = packets;

    stats->rx_bytes = bytes;

    stats->tx_bytes = bytes;

    return stats;

    }

    static const struct net_device_ops loopback_ops =

    {

    .ndo_start_xmit = loopback_net_xmit,

    .ndo_get_stats = loopback_get_stats,

    };

    /*

    * The loopback device is special. There is only one instance

    * per network namespace.

    */

    static void loopback_setup(struct net_device *dev)

    {

    dev->mtu = (16 * 1024) + 20 + 20 + 12;

    dev->flags = IFF_LOOPBACK;

    dev->header_ops = &eth_header_ops;

    dev->netdev_ops = &loopback_ops;

    }

    /* Setup and register the loopback device. */

    static int loopback_net_init(struct net *net)

    {

    struct net_device *dev;

    //1.

    dev = alloc_netdev(0, "lo", loopback_setup);

    //4.

    register_netdev(dev);

    net->loopback_dev = dev;

    return 0;

    }

    /* Registered in net/core/dev.c */

    struct pernet_operations __net_initdata loopback_net_ops = {

    .init = loopback_net_init,

    .exit = loopback_net_exit,

    };

    0b1331709591d260c1c78e86d0c51c18.png

    展开全文
  • 前言 学习应该是一个先把问题简单化,在把问题复杂化的过程。...一般在使用的Linux网卡驱动代码动辄3000行左右,这个代码量以及它所表达出来的知识量无疑是庞大的,我们有没有办法缩短一下这个代码量,使我们的学习

    前言

    学习应该是一个先把问题简单化,在把问题复杂化的过程。一开始就着手处理复杂的问题,难免让人有心惊胆颤,捉襟见肘的感觉。读Linux网卡驱动也是一样。那长长的源码夹杂着那些我们陌生的变量和符号,望而生畏便是理所当然的了。不要担心,事情总有解决的办法,先把一些我们管不着的代码切割出去,留下必须的部分,把框架掌握了,哪其他的事情自然就水到渠成了,这是笔者的心得。

    一般在使用的Linux网卡驱动代码动辄3000行左右,这个代码量以及它所表达出来的知识量无疑是庞大的,我们有没有办法缩短一下这个代码量,使我们的学习变的简单些呢,经过笔者的不懈努力,在仍然能够使网络设备正常工作的前提下,把它缩减到了600多行,我们把暂时还用不上的功能先割出去。这样一来,事情就简单多了,真的就剩下一个框架了。下面我们就来剖析这个可以执行的框架。

    限于篇幅,以下分析用到的所有涉及到内核中的函数代码,我都不予列出,但给出在哪个具体文件中,请读者自行查阅。

    初始化

    首先,我们来看看设备的初始化。当我们正确编译完我们的程序后,我们就需要把生成的目标文件加载到内核中去,我们会先ifconfig eth0 down和rmmod 8139too来卸载正在使用的网卡驱动,然后insmod 8139too.o把我们的驱动加载进去(其中8139too.o是我们编译生成的目标文件)。就像C程序有主函数main()一样,模块也有第一个执行的函数,即 module_init(rtl8139_init_module);在我们的程序中,rtl8139_init_module()在insmod之后首先执行,它的代码如下:

    static int __init rtl8139_init_module (void)
    {
      return pci_module_init (&rtl8139_pci_driver);
    }
    

    它直接调用了pci_module_init(),这个函数代码在Linux/drivers/net/eepro100.c中,并且把rtl8139_pci_driver(这个结构是在我们的驱动代码里定义的,它是驱动程序和PCI设备联系的纽带)的地址作为参数传给了它。rtl8139_pci_driver定义如下:

    	static struct pci_driver rtl8139_pci_driver = {
      	name: MODNAME,
      	id_table: rtl8139_pci_tbl,
      	probe: rtl8139_init_one,
      	remove: rtl8139_remove_one,
      };
    

    pci_register_driver

    pci_module_init()在驱动代码里没有定义,你一定想到了,它是Linux内核提供给模块是一个标准接口,那么这个接口都干了些什么,笔者跟踪了这个函数。里面调用了pci_register_driver(),这个函数代码在Linux/drivers/pci/pci.c中,pci_register_driver做了三件事情。

    ①是把带过来的参数rtl8139_pci_driver在内核中进行了注册,内核中有一个PCI设备的大的链表,这里负责把这个PCI驱动挂到里面去。

    ②是查看总线上所有PCI设备(网卡设备属于PCI设备的一种)的配置空间如果发现标识信息与rtl8139_pci_driver中的id_table相同即rtl8139_pci_tbl,而它的定义如下:

    	static struct pci_device_id rtl8139_pci_tbl[] __devinitdata = {
      	{0x10ec, 0x8129, PCI_ANY_ID, PCI_ANY_ID, 0, 0, 1},
      	{PCI_ANY_ID, 0x8139, 0x10ec, 0x8139, 0, 0,0 },
      	{0,}
      };
    

    那么就说明这个驱动程序就是用来驱动这个设备的,于是调用rtl8139_pci_driver中的probe函数即rtl8139_init_one,这个函数是在我们的驱动程序中定义了的,它是用来初始化整个设备和做一些准备工作。这里需要注意一下pci_device_id是内核定义的用来辨别不同PCI设备的一个结构,例如在我们这里0x10ec代表的是Realtek公司,我们扫描PCI设备配置空间如果发现有Realtek公司制造的设备时,两者就对上了。当然对上了公司号后还得看其他的设备号什么的,都对上了才说明这个驱动是可以为这个设备服务的。

    ③是把这个rtl8139_pci_driver结构挂在这个设备的数据结构(pci_dev)上,表示这个设备从此就有了自己的驱动了。而驱动也找到了它服务的对象了。

    PCI是一个总线标准,PCI总线上的设备就是PCI设备,这些设备有很多类型,当然也包括网卡设备,每一个PCI设备在内核中抽象为一个数据结构pci_dev,它描述了一个PCI设备的所有的特性,具体请查询相关文档,本文限于篇幅无法详细描述。但是有几个地方和驱动程序的关系特别大,必须予以说明。PCI设备都遵守PCI标准,这个部分所有的PCI设备都是一样的,每个PCI设备都有一段寄存器存储着配置空间,这一部分格式是一样的,比如第一个寄存器总是生产商号码,如Realtek就是10ec,而Intel则是另一个数字,这些都是商家像标准组织申请的,是肯定不同的。我就可以通过配置空间来辨别其生产商,设备号,不论你什么平台,x86也好,ppc也好,他们都是同一的标准格式。当然光有这些PCI配置空间的统一格式还是不够的,比如说人类,都有鼻子和眼睛,但并不是所有人的鼻子和眼睛都长的一样的。网卡设备是PCI设备必须遵守规则,在设备里集成了PCI配置空间,但它是一个网卡就必须同时集成能控制网卡工作的寄存器。而寄存器的访问就成了一个问题。在Linux里面我们是把这些寄存器映射到主存虚拟空间上的,换句话说我们的CPU访存指令就可以访问到这些处于外设中的控制寄存器。总结一下PCI设备主要包括两类空间,一个是配置空间,它是操作系统或BIOS控制外设的统一格式的空间,CPU指令不能访问,访问这个空间要借助BIOS功能,事实上Linux的访问配置空间的函数是通过CPU指令驱使BIOS来完成读写访问的。而另一类是普通的控制寄存器空间,这一部分映射完后CPU可以访问来控制设备工作。

    rtl8139_init_one

    现在我们回到上面pci_register_driver的第二步,如果找到相关设备和我们的pci_device_id结构数组对上号了,说明我们找到服务对象了,则调用rtl8139_init_one,它主要做了七件事:

    ① 建立net_device结构,让它在内核中代表这个网络设备。但是读者可能会问,pci_dev也是代表着这个设备,那么两者有什么区别呢,正如我们上面讨论的,网卡设备既要遵循PCI规范,也要担负起其作为网卡设备的职责,于是就分了两块,pci_dev用来负责网卡的PCI规范,而这里要说的net_device则是负责网卡的网络设备这个职责。

    	dev = init_etherdev (NULL, sizeof (*tp));
      if (dev == NULL) {
      	printk ("unable to alloc new ethernet\n");
      	return -ENOMEM;
      }
      tp = dev->priv;
    

    init_etherdev函数在Linux/drivers/net/net_init.c中,在这个函数中分配了net_device的内存并进行了初步的初始化。这里值得注意的是net_device中的一个成员priv,它代表着不同网卡的私有数据,比如Intel的网卡和Realtek的网卡在内核中都是以net_device来代表。但是他们是有区别的,比如Intel和Realtek实现同一功能的方法不一样,这些都是靠着priv来体现。所以这里把拿出来同net_device相提并论。分配内存时,net_device中除了priv以外的成员都是固定的,而priv的大小是可以任意的,所以分配时要把priv的大小传过去。

    ②开启这个设备(其实是开启了设备的寄存器映射到内存的功能)

    	rc = pci_enable_device (pdev);
    
      if (rc)
      	goto err_out;
    

    pci_enable_device也是一个内核开发出来的接口,代码在drivers/pci/pci.c中,笔者跟踪发现这个函数主要就是把PCI配置空间的Command域的0位和1位置成了1,从而达到了开启设备的目的,因为rtl8139的官方datasheet中,说明了这两位的作用就是开启内存映射和I/O映射,如果不开的话,那我们以上讨论的把控制寄存器空间映射到内存空间的这一功能就被屏蔽了,这对我们是非常不利的,除此之外,pci_enable_device还做了些中断开启工作。

    ③获得各项资源

    	mmio_start = pci_resource_start (pdev, 1);
    	mmio_end = pci_resource_end (pdev, 1);
    	mmio_flags = pci_resource_flags (pdev, 1);
    	mmio_len = pci_resource_len (pdev, 1);
    

    读者也许疑问我们的寄存器被映射到内存中的什么地方是什么时候有谁决定的呢。是这样的,在硬件加电初始化时,BIOS固件统一检查了所有的PCI设备,并统一为他们分配了一个和其他互不冲突的地址,让他们的驱动程序可以向这些地址映射他们的寄存器,这些地址被BIOS写进了各个设备的配置空间,因为这个活动是一个PCI的标准的活动,所以自然写到各个设备的配置空间里而不是他们风格各异的控制寄存器空间里。当然只有BIOS可以访问配置空间。当操作系统初始化时,他为每个PCI设备分配了pci_dev结构,并且把BIOS获得的并写到了配置空间中的地址读出来写到了pci_dev中的resource字段中。这样以后我们在读这些地址就不需要在访问配置空间了,直接跟pci_dev要就可以了,我们这里的四个函数就是直接从pci_dev读出了相关数据,代码在include/linux/pci.h中。定义如下:

    	#define pci_resource_start(dev,bar) ((dev)->resource[(bar)].start)
      #define pci_resource_end(dev,bar) ((dev)->resource[(bar)].end)
    

    这里需要说明一下,每个PCI设备有0-5一共6个地址空间,我们通常只使用前两个,这里我们把参数1传给了bar就是使用内存映射的地址空间。

    ④把得到的地址进行映射

    	ioaddr = ioremap (mmio_start, mmio_len);
    
      if (ioaddr == NULL) {
      	printk ("cannot remap MMIO, aborting\n");
      	rc = -EIO;
      	goto err_out_free_res;
      }
    

    ioremap是内核提供的用来映射外设寄存器到主存的函数,我们要映射的地址已经从pci_dev中读了出来(上一步),这样就水到渠成的成功映射了而不会和其他地址有冲突。映射完了有什么效果呢,我举个例子,比如某个网卡有 100个寄存器,他们都是连在一块的,位置是固定的,加入每个寄存器占4个字节,那么一共400个字节的空间被映射到内存成功后,ioaddr就是这段地址的开头(注意ioaddr是虚拟地址,而mmio_start是物理地址,它是BIOS得到的,肯定是物理地址,而保护模式下CPU不认物理地址,只认虚拟地址),ioaddr+0就是第一个寄存器的地址,ioaddr+4就是第二个寄存器地址(每个寄存器占4个字节),以此类推,我们就能够在内存中访问到所有的寄存器进而操控他们了。

    ⑤重启网卡设备

    重启网卡设备是初始化网卡设备的一个重要部分,它的原理就是向寄存器中写入命令就可以了(注意这里写寄存器,而不是配置空间,因为跟PCI没有什么关系),代码如下:

    	writeb ((readb(ioaddr+ChipCmd) & ChipCmdClear) | CmdReset,ioaddr+ChipCmd);
    

    是我们看到第二参数ioaddr+ChipCmd,ChipCmd是一个位移,使地址刚好对应的就是ChipCmd哪个寄存器,读者可以查阅官方datasheet得到这个位移量,我们在程序中定义的这个值为:ChipCmd = 0x37;与datasheet是吻合的。我们把这个命令寄存器中相应位(RESET)置1就可以完成操作。

    ⑥获得MAC地址,并把它存储到net_device中。

    	for(i = 0; i < 6; i++) { /* Hardware Address */
      	dev->dev_addr[i] = readb(ioaddr+i);
      	dev->broadcast[i] = 0xff;
      }
    

    我们可以看到读的地址是ioaddr+0到ioaddr+5,读者查看官方datasheet会发现寄存器地址空间的开头6个字节正好存的是这个网卡设备的MAC地址,MAC地址是网络中标识网卡的物理地址,这个地址在今后的收发数据包时会用的上。

    ⑦向net_device中登记一些主要的函数

    	dev->open = rtl8139_open;
      dev->hard_start_xmit = rtl8139_start_xmit;
      dev->stop = rtl8139_close;
    

    由于dev(net_device)代表着设备,把这些函数注册完后,rtl8139_open就是用于打开这个设备,rtl8139_start_xmit就是当应用程序要通过这个设备往外面发数据时被调用,具体的其实这个函数是在网络协议层中调用的,这就涉及到Linux网络协议栈的内容,不再我们讨论之列,我们只是负责实现它。rtl8139_close用来关掉这个设备。

    打开设备

    好了,到此我们把rtl8139_init_one函数介绍完了,初始化这个设备之后呢,我们通过ifconfig eth0 up命令来把我们的设备激活。这个命令直接导致了我们刚刚注册的rtl8139_open的调用。这个函数激活了设备。这个函数主要做了三件事。

    ①注册这个设备的中断处理函数。当网卡发送数据完成或者接收到数据时,是用中断的形式来告知的,比如有数据从网线传来,中断也通知了我们,那么必须要有一个处理这个中断的函数来完成数据的接收。关于Linux的中断机制不是我们详细讲解的范畴,有兴趣的可以参考《Linux内核源代码情景分析》,但是有个非常重要的资源我们必须注意,那就是中断号的分配,和内存地址映射一样,中断号也是BIOS在初始化阶段分配并写入设备的配置空间的,然后Linux在建立pci_dev时从配置空间读出这个中断号然后写入pci_dev的irq成员中,所以我们注册中断程序需要中断号就是直接从pci_dev里取就可以了。

    	retval = request_irq (dev->irq, rtl8139_interrupt, SA_SHIRQ, dev->name, dev);
      if (retval) {
      	return retval;
      }
    

    我们注册的中断处理函数是rtl8139_interrupt,也就是说当网卡发生中断(如数据到达)时,中断控制器8259A把中断号发给CPU,CPU根据这个中断号找到处理程序,这里就是rtl8139_interrupt,然后执行。rtl8139_interrupt也是在我们的程序中定义好了的,这是驱动程序的一个重要的义务,也是一个基本的功能。request_irq的代码在arch/i386/kernel/irq.c中。

    ②分配发送和接收的缓存空间

    根据官方文档,发送一个数据包的过程是这样的:先从应用程序中把数据包拷贝到一段连续的内存中(这段内存就是我们这里要分配的缓存),然后把这段内存的地址写进网卡的数据发送地址寄存器(TSAD)中,这个寄存器的偏移量是TxAddr0 = 0x20。在把这个数据包的长度写进另一个寄存器(TSD)中,它的偏移量是TxStatus0 = 0x10。然后就把这段内存的数据发送到网卡内部的发送缓冲中(FIFO),最后由这个发送缓冲区把数据发送到网线上。

    好了现在创建这么一个发送和接收缓冲内存的目的已经很显然了。

    	tp->tx_bufs = pci_alloc_consistent(tp->pci_dev, TX_BUF_TOT_LEN,&tp->tx_bufs_dma);
    	tp->rx_ring = pci_alloc_consistent(tp->pci_dev, RX_BUF_TOT_LEN,&tp->rx_ring_dma);
    

    tp是net_device的priv的指针,tx_bufs是发送缓冲内存的首地址,rx_ring是接收缓存内存的首地址,他们都是虚拟地址,而最后一个参数tx_bufs_dma和rx_ring_dma均是这一段内存的物理地址。为什么同一个事物,既用虚拟地址来表示它还要用物理地址呢,是这样的,CPU执行程序用到这个地址时,用虚拟地址,而网卡设备向这些内存中存取数据时用的是物理地址(因为网卡相对CPU属于头脑比较简单型的)。pci_alloc_consistent的代码在Linux/arch/i386/kernel/pci-dma.c中。

    ③发送和接收缓冲区初始化和网卡开始工作的操作

    RTL8139有4个发送描述符(包括4个发送缓冲区的基地址寄存器(TSAD0-TSAD3)和4个发送状态寄存器(TSD0-TSD3)。也就是说我们分配的缓冲区要分成四个等分并把这四个空间的地址都写到相关寄存器里去,下面这段代码完成了这个操作。

    	for (i = 0; i < NUM_TX_DESC; i++)
      ((struct rtl8139_private*)dev->priv)->tx_buf[i] =
      	&((struct rtl8139_private*)dev->priv)->tx_bufs[i * TX_BUF_SIZE];
    

    上面这段代码负责把发送缓冲区虚拟空间进行了分割。

    	for (i = 0; i < NUM_TX_DESC; i++)
      {
      	writel(tp->tx_bufs_dma+(tp->tx_buf[i]tp->tx_bufs),ioaddr+TxAddr0+(i*4));
      	readl(ioaddr+TxAddr0+(i * 4));
      }
    

    上面这段代码负责把发送缓冲区物理空间进行了分割,并把它写到了相关寄存器中,这样在网卡开始工作后就能够迅速定位和找到这些内存并存取他们的数据。

    	writel(tp->rx_ring_dma,ioaddr+RxBuf);
    

    上面这行代码是把接收缓冲区的物理地址写到了相关寄存器中,这样网卡接收到数据后就能准确的把数据从网卡中搬运到这些内存空间中,等待CPU来领走他们。

    	writeb((readb(ioaddr+ChipCmd) & ChipCmdClear) | CmdRxEnb | CmdTxEnb,ioaddr+ChipCmd);
    

    重新RESET设备后,我们要激活设备的发送和接收的功能,上面这行代码就是向相关寄存器中写入相应值,激活了设备的这些功能。

    	writel ((TX_DMA_BURST << TxDMAShift),ioaddr+TxConfig);
    

    上面这行代码是向网卡的 TxConfig(位移是0x44)寄存器中写入TX_DMA_BURST << TxDMAShift这个值,翻译过来就是6<<8,就是把第8到第10这三位置成110,查阅管法文档发现6就是110代表着一次DMA的数据量为1024字节。

    另外在这个阶段设置了接收数据的模式,和开启中断等等,限于篇幅由读者自行研究。

    数据收发

    rtl8139_start_xmit

    当一个网络应用程序要向网络发送数据时,它要利用Linux的网络协议栈来解决一系列问题,找到网卡设备的代表net_device,由这个结构来找到并控制这个网卡设备来完成数据包的发送,具体是调用net_device的hard_start_xmit成员函数,这是一个函数指针,在我们的驱动程序里它指向的是rtl8139_start_xmit,正是由它来完成我们的发送工作的,下面我们就来剖析这个函数。它一共做了四件事。

    ①检查这个要发送的数据包的长度,如果它达不到以太网帧的长度,必须采取措施进行填充。

    	if( skb->len < ETH_ZLEN ){//if data_len < 60
      	if( (skb->data + ETH_ZLEN) <= skb->end ){
      		memset( skb->data + skb->len, 0x20, (ETH_ZLEN - skb->len) );
      		skb->len = (skb->len >= ETH_ZLEN) ? skb->len : ETH_ZLEN;}
      	else{
      		printk("%s:(skb->data+ETH_ZLEN) > skb->end\n", __FUNCTION__);
      	}
      }
    

    skb->data和skb->end就决定了这个包的内容,如果这个包本身总共的长度(skb->end- skb->data)都达不到要求,那么想填也没地方填,就出错返回了,否则的话就填上。

    ②把包的数据拷贝到我们已经建立好的发送缓存中。

    	memcpy (tp->tx_buf[entry], skb->data, skb->len);
    

    其中skb->data就是数据包数据的地址,而tp->tx_buf[entry]就是我们的发送缓存地址,这样就完成了拷贝,忘记了这些内容的回头看看前面的介绍。

    ③光有了地址和数据还不行,我们要让网卡知道这个包的长度,才能保证数据不多不少精确的从缓存中截取出来搬运到网卡中去,这是靠写发送状态寄存器(TSD)来完成的。

    	writel(tp->tx_flag | (skb->len >= ETH_ZLEN ? skb->len : ETH_ZLEN),ioaddr+TxStatus0+(entry * 4));
    

    我们把这个包的长度和一些控制信息一起写进了状态寄存器,使网卡的工作有了依据。

    ④判断发送缓存是否已经满了,如果满了在发就覆盖数据了,要停发。

    	if ((tp->cur_tx - NUM_TX_DESC) == tp->dirty_tx)
    
      netif_stop_queue (dev);
    

    rtl8139_interrupt

    谈完了发送,我们开始谈接收,当有数据从网线上过来时,网卡产生一个中断,调用的中断服务程序是rtl8139_interrupt,它主要做了三件事。

    ①从网卡的中断状态寄存器中读出状态值进行分析,status = readw(ioaddr+IntrStatus);

    	if ((status &(PCIErr | PCSTimeout | RxUnderrun | RxOverflow |RxFIFOOver | TxErr | TxOK | RxErr | RxOK)) == 0)
      	goto out;
    

    上面代码说明如果上面这9种情况均没有的表示没什么好处理的了,退出。

    if (status & (RxOK | RxUnderrun | RxOverflow | RxFIFOOver))/* Rx interrupt */
    
      rtl8139_rx_interrupt (dev, tp, ioaddr);
    

    如果是以上4种情况,属于接收信号,调用rtl8139_rx_interrupt进行接收处理。

    	if (status & (TxOK | TxErr)) {
    
      spin_lock (&tp->lock);
    
      rtl8139_tx_interrupt (dev, tp, ioaddr);
    
      spin_unlock (&tp->lock);
    
      }
    

    如果是传输完成的信号,就调用rtl8139_tx_interrupt进行发送善后处理。

    rtl8139_rx_interrupt

    下面我们先来看看接收中断处理函数rtl8139_rx_interrupt,在这个函数中主要做了下面四件事

    ①这个函数是一个大循环,循环条件是只要接收缓存不为空就还可以继续读取数据,循环不会停止,读空了之后就跳出。

    	int ring_offset = cur_rx % RX_BUF_LEN;
      rx_status = le32_to_cpu (*(u32 *) (rx_ring + ring_offset));
      rx_size = rx_status >> 16;
    

    上面三行代码是计算出要接收的包的长度。

    ②根据这个长度来分配包的数据结构

    	skb = dev_alloc_skb (pkt_size + 2);
    

    ③如果分配成功就把数据从接收缓存中拷贝到这个包中

    	eth_copy_and_sum (skb, &rx_ring[ring_offset + 4], pkt_size, 0);
    

    这个函数在include/linux/etherdevice.h中,实质还是调用了memcpy()。

    	static inline void eth_copy_and_sum(struct sk_buff*dest, unsigned char *src, int len, int base)
      {
      	memcpy(dest->data, src, len);
      }
    

    现在我们已经熟知,&rx_ring[ring_offset + 4]就是接收缓存,也是源地址,而skb->data就是包的数据地址,也是目的地址,一目了然。

    ④把这个包送到Linux协议栈去进行下一步处理

    	skb->protocol = eth_type_trans (skb, dev);
      netif_rx (skb);
    

    在netif_rx()函数执行完后,这个包的数据就脱离了网卡驱动范畴,而进入了Linux网络协议栈里面,把这些数据包的以太网帧头,IP头,TCP头都脱下来,最后把数据送给了应用程序,不过协议栈不再本文讨论范围内。netif_rx函数在net/core/dev.c,中。

    而rtl8139_remove_one则基本是rtl8139_init_one的逆过程。

    展开全文
  • 26.Linux-网卡驱动介绍以及制作虚拟网卡驱动(详解) - 诺谦 - 博客园 (cnblogs.com) Linux网络设备驱动 _驱动模型 - Abnor - 博客园 (cnblogs.com) 参考书籍是 Linux Kernel Development 3rd ...

        本文是 6.S081 操作系统课程学习最后一个 lab,编写一个 intel 的 e1000 网卡的驱动在 xv6 下。需要复习知识有:操作系统知识,计算机组成原理 DMA 相关,循环缓冲区的概念,e1000 的粗略 spec 和其具备两个环形缓冲区和其引发中断的方式,理论上感觉做这个 lab 只需要 livelock 课程的前面讲网络的基础知识部分记忆和通读 lab 的 handout(包括 hint) 就能很快做出来。下面记录以下具体过程,穿插学习 Linux 下如何编写网卡驱动的 Real World 实现(毕竟 xv6 只是个 toy)。希望写完本文的时候能够具备一些 Linux low level 的驱动知识。

    PCI 标准驱动实现和规格

        Peripheral Component Interconnect 外部互联标准,PCI 总线是一种并行同步系统总线,集中式独立请求仲裁(每个 dev 都有一条请求线和总线使用线),具体仲裁优先级和算法由 PCI 具体实现。同步获取总线是利用REQ#和GNT#两个信号线实现的,前者用于某一个设备占用总线的请求,后者允许某一设备占用总线和应答。

        先看 PCI 接口 DMA 技术网卡的模型:

        其中 Packet  Buffer 就是 RAM 了, 网卡就是右边那块 TX 和 RX 的 MAC 硬件,这里网卡通过 DMA Engine 来把卡上的存储之类的东西的内容一次复制到内存中去。而 PCI 的作用就是负责管理这些外设卡。

    PCI 编址

     PCI 的地址编码来访问不同的设备。

    直接看 xv6 的 pci.c 我们探测 PCI 设备的时候直接遍历 dev 和 func 等。注意这里的 bus 是总线编号,观察一下 window 的设备管理器有惊喜,可以发现核显和主板自带的一些外设件都在 bus 0 下,而涉及主板上的 PCIE 口(笔记本pcie网卡和独立显卡)都接到 PCI bus 1,2去了(当然 PCIE 和 PCI 的机制不一样)如下图:

        我们把 PCI 的 bus 0 叫做 up-stream 总线,bus 1 到以后的(还有到 bus 20 的)涉及一些下级的桥接,他再桥接到 bus 0 上的叫做 down-stream, 具体不究太多了。我们这里认为 Intel 的这个 e1000 是接到 bus0 上的, 实际上我们需要 PCI probe 所有的设备的,这个 lab 我们直接指定了。然后 function 的编号是因为一个 device 可能有多个 function,不过我一开始以为笔记本的 pci或者pcie网卡的 wifi 和 蓝牙 是按这个分 function 走的,结果发现实际是 wlan 走 pcie,蓝牙走 usb(minipcie、ngff 的 pci 接口都自带兼容 usb 接口),实际是两个分开的芯片。所以 function 这方面很难举例了。工业上比如4通道的采样卡就是用 function 实现多通道数据并行传输的。

        直接看代码,我们根据上面的 PFA 来遍历 Bus Device Function 来查找我们要的卡。探测卡的信息就涉及到一个 convention 了,PCI 约定了 pci 上的地址的低位 offset 的一部分地址空间用于登记设备的信息,具体实现就不是 O/S 做的了, 他大概是 PCI 相关的南桥来搞的. (这一点保留意见).

    	// PCI address: 
    	//   |31 enable bit|30:24 Reserved|-
    	//  -|23:16 Bus num|15:11 Dev num|10:8 func num|7:2 off|1:0 0|.
    uint32 off = (bus << 16) | (dev << 11) | (func << 8) | (offset);

    PCI 设备元数据规格

        我们看 Intel 的 dev manual 给的 PCI 的设备信息(针对本 e1000 网卡). 但是头部的 Device ID 等内容是通用的. 所以我们很容易能读到 0h offset 的一行的 ID 信息来判断并装入驱动. 我们应当要记住,PCI 的作用就是完成 register 的 mapping 从而实现能够让 C 程序通过读取内存(vm)来访问设备的寄存器从而实现控制设备,之后的数据传输则是通过操纵那些寄存器来实现的(即控制设备)。一句话就是 PCI 在内存建了一个控制台之后程序就只用操作控制台了。

        下面结合代码来看:

    	void
    	pci_init()
    	{
    	  // we'll place the e1000 registers at this address.
    	  // vm.c maps this range.
    	  uint64 e1000_regs = 0x40000000L; 
    	  // qemu -machine virt puts PCIe config space here.
    	  // vm.c maps this range.
    	  uint32  *ecam = (uint32 *) 0x30000000L;
    	  // look at each possible PCI device on bus 0.
    	  for(int dev = 0; dev < 32; dev++){
    	    int bus = 0;
    	    int func = 0;
    	    int offset = 0;
    	    // PCI address: 
    	    //   |31 enable bit|30:24 Reserved|-
    	    //  -|23:16 Bus num|15:11 Dev num|10:8 func num|7:2 off|1:0 0|.
    	    uint32 off = (bus << 16) | (dev << 11) | (func << 8) | (offset);
    	    volatile uint32 *base = ecam + off;
    	    // PCI address space header:
    	    // Byte Off   |   3   |   2   |   1   |   0   |
    	    //          0h|   Device ID   |   Vendor ID   |
    	    uint32 id = base[0]; // read the first line.
    	    // 10 0e (device id):80 86(vendor id)  is an e1000
    	    if(id == 0x100e8086){
    	      // PCI address space header:
    	      // Byte Off   |   3   |   2    |   1     |   0    |
    	      //         4h |Status register | command register |
    	      // command and status register.
    	      // bit 0 : I/O access enable
    	      // bit 1 : memory access enable
    	      // bit 2 : enable mastering
    	      base[1] = 7;
    	      __sync_synchronize();
    	      for(int i = 0; i < 6; i++){
    	        // Byte Off              |   3   |   2    |   1     |   0    |
    	        // 16b/4b = 4        10h |           Base Address 0          |
    	        //          5        14h |           Base Address 1          |
    	        //          6        18h |           Base Address 2          |
    	        //          7    1ch~24h |          .... 3, 4, 5             |
    	        uint32 old = base[4+i];
    	        // writing all 1's to the BAR causes it to be
    	        // replaced with its size.
    	        base[4+i] = 0xffffffff;
    	        __sync_synchronize();
    	        // if we need a dynamic allocation, we can read the base[4+i] again, remove the low bits
    	        // and calc it's one's complement then plus 1 to get it's BAR size (a dma area).
    	        base[4+i] = old;
    	      }
    	      // tell the e1000 to reveal its registers at
    	      // physical address 0x40000000.
    	      base[4+0] = e1000_regs;
    	      e1000_init((uint32*)e1000_regs);
    	    }
    	  }
    	}
    

        计算的部分和用 uint32 读 bytes 我看代码注释都很清楚了,下面讲解其中几个要点。第一个是 0xffffffff 的意义。网卡内部有 flash 的而且拷贝数据不可能一个 bit 或者 byte 地拷贝效率太慢了,他的编地址机制应该是整数对齐的,所以会有一部分 bit 必须是0,我们写的时候无论你 low bits 填了1还是0,之后再 load 就会发现 low bits 始终 hard-wired to be 0b(b是二进制计数的意思…)(见下面表格的 Description). 这里 base address 的意思是注册一个内存地址给 PCI 设备,让他把 register 和NIC 的 flash 缓存内容往这个内存地址去 map。下面看一下 Intel 的 Manual 里面是怎么说这个 0xffffffff 的意思的。

        这是上面的那个表格的部分详细版,然后看具体的字段意思。

        这就很好理解了这个东西了。顺便摘录一段方便理解:

        The Base Address Registers (or BARs) are used to map the Ethernet con-troller’s register space and flash to system memory space. In PCI-X mode or in PCI mode when the BAR32 bit of the EEPROM is 0b, two registers are used for each of the register space and the flash memory in order to map 64-bit addresses. In PCI mode, if the BAR32 bit in the EEPROM is 1b, one register is used for each to map 32-bit addresses.

        初始化完 PCI 完成了一些 vm 的 mapping,之后就能够通过访问 vm address 来访问 register 了(注意看上面的表格,我们10h offset 即代码中的 base+4是 register 的地方放入 e1000_regs),之后我们转到 e1000_init() 去看怎么完成另一部分的初始化。

    E1000 网卡驱动实现

        e1000_init 做的事情主要有以下亿点:

    • reset 网卡,关闭网卡中断。(记住 e1000 是通过 interrupt 来告知操作系统他的一个 DMA 操作完成了)。
    • lazy allocate 地把 tx_ring 里面的全部状态设置为 done(即可以支持 OS 传来新的 tx 任务)。
    • allocate 所有的 rx_mbuf 以及设置 rx_ring 对应的 addr。
    • 设置网卡的记录 rx_ring 和 tx_ring 的 register,以及登记各种和循环缓冲区有关的 control registers 值。
    • 设置 MAC 地址
    • 在网卡上做一个空的多播表
    • 通过设置控制位来启动网卡 Transmit 部分和 Receive 部分(开机)。
    • 允许接受 Interrupt,即开网卡中断。

        具体的这些到底是怎么样的详细机制我们下文再议,这里网卡的 init 完成之后,就会启动网卡。之后 lab 需要编写的 transmit 和 recv 函数到底在哪被调用呢? 我们上面讲了 e1000 是 DMA 到 buf 之后引发一个中断的,所以对!我们需要回到 trap.c 。

        e1000_intr 要做的也很简单,就是调用 recv 来把 buf 的内容拿走。让 buffer 能够满足一个流动的条件。那么还有一个问题 transmit 是谁调用的? 这就是涉及我们的网络栈的部分了。我们从 lab 提供的 nettests.c 自顶向下来看。

        首先看 ping 函数,该函数通过调用一个 syscall 来创建一个 file descriptor 来读写。

    connect(dst, sport, dport))

        我们进入看他作为 syscall 就是调用了 sockalloc 来创建 socket 接口。然后再 read 和 write 的时候调用 sockread 或者 sockwrite(sysnet.c 下)。sockwrite 将会调用 net_tx_udp 来完成一个 buffer 数据的写入。结论是 xv6 的 write 作用于 socket file 只支持 udp 调用(net.c 只实现了 udp)。我们再来看 net_tx_udp 不过是 encapsulate 一些 udp 头,还是通过 net_tx_ip 封装下层,然后 net_tx_eth 封装 ethernet frame, 进入 net_tx_eth 就看到了 e1000_transmit() 的调用了。

    xv6 用户网络栈与驱动的调用结构

        摘要 xv6 代码结构的图以下方便理解:

    Linux 中的网络驱动     

        这里我要讲一个问题,这里 network stack 里 udp 怎么能直接调用 e1000 的函数呢,这对于计算机多样性(思考支持多种网卡的系统应该使用一种抽象封装的通用函数调用方案)而言是不好的。我们事实上 Linux 的实现必须用一套驱动管理系统。下面就来分析 Linux 的 RealWorld 版本的 network device driver。

        由于这里我不打算 dive deep into the linux kernel,这里我们假定某些 infrastructure 已经给好了。我们需要提供一个驱动文件给 kernel 用。首先是对于 linux 的一些给 driver 用的 api 说明以下。

    Linux 内核模块简介

        首先是内核模块的概念,对于驱动我们是以内核模块的形式加载进入的,每个驱动的程序就编程层一个内核模块。Linux 在运行的时候 start_kernel 时会加载那些内核模块,其通过一个 do_initcalls 函数把一系列的 module_init() / init_module() 函数给调用了(他们两的区别暂且不管,涉及东西太多了,实际就是宏和入口的区别而已)。下面给出一个模块的例子(Linux Kernel Development 3rd):

        这里的 module_init(hello_init) 就是把一个函数注册为模块的入口。当然也可以直接编写一个 init_module() 函数作为入口(这一点对于 main 函数经过 C runtime 包装后作为入口异曲同工)。至于怎么加载内核模块则太 technical 这里不讲了。当然这个 hello module 只有在加载和卸载的时候 print 一些东西。(至于学网卡驱动有什么用考虑虚拟网卡的好处)对于驱动而言,我们需要提供更多注册动作。

    Linux 网卡驱动的层次结构

        我们需要注册 net_device 结构体登记网卡信息,在不同的 Linux 内核版本中,这些结构体的内容多种多样,我选取其中一种来讲解。思想实验可以想到我们规定一个结构体来存储一些网卡信息同时存储一些在模块里的函数指针即可,然后利用订阅机制来给内核添加一个网卡。我写一部分伪代码在这里:

    	struct net{ // in kernel.
    	  struct info some_info;
    	  struct pointer some_pointer;
    	}
    	struct net my_net;
    	void send(){
    	  do_send();
    	}
    	void recv(){
    	  do_recv();
    	}
    	int init_module(){
    	  // PCI api 探测出网卡的地址
    	  my_card = pci_probe(id, vendor);
    	  // 进行上面提到的那些 register 的 vm mapping
    	  map_registers(my_card);
    	  //写入一些信息如 MAC 地址混淆模式,多播广播信息等
    	  set_info(my_net);
    	  // 注册事件处理器(发送和接受)
    	  my_net.some_info.send =  send;
    	  my_net.some_info.recv =  recv;
    	  // 把网卡注册到内核里
    	  register_netdev(my_net);
    	  return 0;
    	}
    	void exit_module(){
    	  unregister_netdev(my_net);
    	}
    

    当然具体还会涉及一些数据结构(如 xv6 的 mbuf),但是这些编程太 dirty 太多 spec 内容(而且不同 linux 版本千差万别,比如你可以把一个 net_device 来存所有的 info 和 function pointers 或者分开来(net_device_ops),对 interrupt recv 的实现可以规定一个默认入口,也可以同样使用 function pointer 等等等等) 了,我们还要做 lab,这部分就不看下去了。讲解 Linux 的具体实现思路是因为 xv6 的过于简陋了思想实验就无法令人接受,也顺带帮助了解一下 Linux kernel module 的知识。

        上文我们说具体的这些到底是怎么样的详细机制我们下文再议,好现在就来做这个 lab 了。本质上还是练习一个 lock 数据结构的访问的编程练习。所以这下我们的重点回到数据结构上。目前对那个循环的 buffer 实际上是有一个模糊的印象而已。我们必须分开来分析和编程。先从 tx 开始吧。

    Ring Buffer 数据结构分析

        lecture 上已经讲过了 network stack 的内容了,我这里也不想再做笔记了。下面给出 circular buffer 的结构以及要用的 register 指针的宏定义(红色字样为相应寄存器在 regs 数组的索引宏别名)。

        我们这里要用到 TDT,因为 TDT 是他发送出去的一个空位置。正常来说全程由我们软件跟踪(因为他负责把包发送出去,所以硬件递增的只有 Head,Tail 只是标记让硬件暂停 transmitting 的一个 flag)所以看到 init 的时候把 TDT 和 TDH 都设置为 0.

        然后我们读这里的操作 HINT 。

    • First ask the E1000 for the TX ring index at which it's expecting the next packet, by reading the E1000_TDT control register.
    • Then check if the the ring is overflowing. If E1000_TXD_STAT_DD is not set in the descriptor indexed by E1000_TDT, the E1000 hasn't finished the corresponding previous transmission request, so return an error.
    • Otherwise, use mbuffree() to free the last mbuf that was transmitted from that descriptor (if there was one).
    • Then fill in the descriptor. m->head points to the packet's content in memory, and m->len is the packet length. Set the necessary cmd flags (look at Section 3.3 in the E1000 manual) and stash away a pointer to the mbuf for later freeing.
    • Finally, update the ring position by adding one to E1000_TDT modulo TX_RING_SIZE.
    • If e1000_transmit() added the mbuf successfully to the ring, return 0. On failure (e.g., there is no descriptor available to transmit the mbuf), return -1 so that the caller knows to free the mbuf.

        解释一下我们的数据结构,这里由一个 status 数组来跟踪我们的 circular buffer,他不负责数据。为了能保持跟踪我们的 mbuf,还要设置一个 mbuf 指针数组,这是回想我们 transmit 的 api 是上层用户提供一个  mbuf 给我们发的,但是我们放到到 ring buffer 的时候只是 local comitting,只有等到他的那个对应的 status 被网卡更新了(remote push,不过 spec 说了你可以指定网卡一 copy 到 flash 就 update status,也可以指定等到 sent 之后再 update)才能 free 掉我们的 mbuf 原件(销毁本地备份)。这个 status 是由硬件写进来的(handout 说的 the E1000 sets the E1000_TXD_STAT_DD bit in the descriptor to indicate this)。所以具体的数据结构如下:

        其中 mbuf 指针数组 tx_mbufs 做的事情不过是做 hint 里要求的 stash away pointers to the mbufs presented in tx_rings 而已。(感觉这部分全部不写好让自己写反而更方便做这个 lab?因为 mbuf 的一些字段好像就没用到,为了理解这个好像有点花时间,不过这样就要涉及更多的读 specification 的工作了)代码如下给出:

    	int 
    	e1000_transmit(struct mbuf* m) 
    	{
    	  //
    	  // Your code here.
    	  //
    	  // the mbuf contains an ethernet frame; program it into
    	  // the TX descriptor ring so that the e1000 sends it. Stash
    	  // a pointer so that it can be freed after sending.
    	  //
    	  acquire(&e1000_lock);
    	  uint32 tail = regs[E1000_TDT];
    	  // overflow
    	  if (tx_ring[tail].status != E1000_TXD_STAT_DD) {
    	    release(&e1000_lock);
    	    return -1;
    	  }
    	  if(tx_mbufs[tail]){
    	    mbuffree(tx_mbufs[tail]);
    	  }
    	  tx_ring[tail].length = (uint16)m->len;
    	  tx_ring[tail].addr = (uint64)m->head;
    	  tx_ring[tail].cmd = 9;
    	  tx_mbufs[tail] = m;
    	  regs[E1000_TDT] = (tail+1)%TX_RING_SIZE;
    	  release(&e1000_lock);
    	  return 0;
    	}
    

        recv 的则类似这里不赘述了,上图,

      Intel Spec 里面的这幅图我也是物语了😓,他画错图了又在下面文字附上HARDWARE OWNS ALL DESCRIPTORS BETWEEN [HEAD AND TAIL]. 浪费我还以为出 bug 了用 python 写 socket 试了一下。这里建议 google 学习一下 python socket 编程然后用来模拟 nettests 里面的内容来测试一下(等于有一个正确的结果的程序),端口号就看 make server 的提示和 makefile 里面显示的以及 handout 说的那一个了。

    区别的是我们要从 tail +1HARDWARE OWNS ALL DESCRIPTORS BETWEEN [HEAD AND TAIL]. 去拿包出来,把 rx_ring 的这个 buffer 空间给 hardware。更重要的是,我们需要把全部阴影部分都拿走。出现多个灰色的原因是我们为了减少 interrupt 的次数(复习前面的 lecturereceive livelock 就是因为 packet 接收速率很快,而每个收到的packet都会生成一个中断,最后,100%CPU时间都被消耗用来处理网卡的输入中断,CPU没有任何时间用来转发 packet 到上层,同时由于每次只从 buffer copyout 一个也容易导致网卡 throw away 快速到达的 packets)。Spec 里说:

    The Receive Timer Interrupt is used to signal most packet reception events (the Small Receive

    Packet Detect interrupt is also used in some cases as described later in this section). In order to

    minimize the interrupts per work accomplished, the Ethernet controller provides two timers to

    control how often interrupts are generated.

    不过我们查看 init 发现 timer 设置为 0. 所以我们实际不需要复制全部的灰色因为已经约定了一个一个地 interrupt,出于学习目的我们还是写一个 while 循环吧。HINT 中说:At some point the total number of packets that have ever arrived will exceed the ring size (16); make sure your code can handle that. 我的理解是我不能理解,可能看 Q&A 不知道会不会讲这个。如果有人知道这个情况会出现什么事情吗可以告诉我。代码如下给出:

    	static void 
    	e1000_recv(void) 
    	{
    	  //
    	  // Your code here.
    	  //
    	  // Check for packets that have arrived from the e1000
    	  // Create and deliver an mbuf for each packet (using net_rx()).
    	  //
    	  int tail = regs[E1000_RDT];
    	  int i = (tail+1)%RX_RING_SIZE; // tail is owned by Hardware!
    	  while (rx_ring[i].status & E1000_RXD_STAT_DD) {
    	    rx_mbufs[i]->len = rx_ring[i].length;
    	    // send mbuf to upper level (the network stack in net.c).
    	    net_rx(rx_mbufs[i]);
    	    // get a new buffer for next recv.
    	    rx_mbufs[i] = mbufalloc(0);
    	    rx_ring[i].addr = (uint64)rx_mbufs[i]->head;
    	    // update status for next recv.
    	    rx_ring[i].status = 0;
    	    i = (i + 1) % RX_RING_SIZE;
    	  }
    	  regs[E1000_RDT] = i - 1; // - 1 for the while loop.
    	}
    

      最后指出一个锁的应用问题:You'll need locks to cope with the possibility that xv6 might use the E1000 from more than one process, or might be using the E1000 in a kernel thread when an interrupt arrives. 就能理解为什么一个要用 spinlock 一个不用 spinlock。

    展开全文
  • 数据接收网卡驱动的数据接收,实际上是一个生产者/消费者模型。核心是输入队列(全局的,或者网卡私有的)。网卡收到数据时,触发中断。在中断执行例程中,把skb挂入输入队列,并出发软中断。稍后的某个时刻,当软中断...
  • 根据接收队列数num_rx_queues建立相应的接收缓冲区结构e1000_rx_ring,在该结构中有描述该区域的指向e1000_rx_desc结构的desc,该缓冲区指向的dma总线地址,用于接收硬件传送来的用e1000_buffer结构描述的缓冲块数组...
  • uboot下网卡驱动的分解,基于飞思卡尔平台,包含linux下的网卡设备抽象以及注册过程
  • rtl8139_interrupt(中断处理函数)当网卡收到数据,发送数据完成,或收发出错都可能发出中断,在中断处理中根据网卡中断状态寄存器的值来判断是什么情况的中断,然后调用相应的处理函数。/* The interrupt handler ...
  • 上篇讲到通过中断,最终网卡调用了b44_rx()来接收报文相关阅读:对这个函数中的一些参数,可以这样理解:bp->rx_cons –处理器处理到的缓冲区号bp->rx_pending –分配的缓冲区个数bp->rx_prod –当前缓冲区...
  • 在执行netif_rx()函数之后,此数据包的数据离开网卡驱动程序并进入Linux网络协议堆栈. 这些数据包的以太网帧头,IP头和TCP头被删除,最后将数据发送到应用程序. netif_rx函数位于net / core / dev.c中. rtl8139_...
  • 在e1000e网卡驱动中有如下代码: BAR0用来映射设备寄存器,即设备有关寄存器都映射到内存空间,我们可以通过操作内存来操作设备寄存器,pci_resource_start(pdev, 0)就是用来获取BAR0的起始地址: mmio_start = pci...
  • Linux e1000网卡驱动流程

    千次阅读 2016-04-20 04:01:34
    本文以e1000驱动为例,试图理清网络驱动层的数据流、逻辑流。 关于网卡接收到以太流的DMA过程: 网卡DMA引擎在主存中为DMA开辟一段连续空间存放Buffer Descriptor(ptr/length/status),一致性映射(dma_alloc_...
  • linux网卡驱动源码分析

    千次阅读 2014-01-10 11:26:08
    网络驱动是一种典型的PCI设备驱动,无论在嵌入式平台还是在PC领域,网络相关的项目开发有着比较广阔的前景,因此,分析当前Linux内核中网络设备的驱动,不但能了解网络相关的基本原理,而且可以借鉴Linux内核的先进...
  • 1,网卡驱动创建rx descriptor ring,将ring的总线地址写入网卡寄存器 2,网卡驱动为每个descriptor分配sk_buff和数据缓存区 数据接收阶段(处理首包和后续包的区别) 3,网卡接收数据包,将数据包写入RX FIFO(如何...
  • 8139 有一个接收缓冲寄存器,用于存放接收缓存的首地址,网卡一边把网线上的发出的数据放到内部FIFO,一边从FIFO 中把数据通过DMA 传送到由接收寄存器指定的内存地址中,接收到的数据依次排放,当长度超过默认的...
  • 很久之前读的网卡驱动源码,很多东西已经忘记了,最近面试被问道了网卡收数据包的全过程,只能答出一个很简单的过程,NAPI这种非常优秀的机制都没有想起来,很惭愧,重新复习了一下收包的过程,顺便把当时注释的代码...
  • linux网卡eth0驱动问题

    2021-05-14 21:32:35
    LLocal Loopbackinet addr:127.0.0.1 Mask:255.0.0.0UP LOOPBAKE RUNNING MTU:16436 Metric:1....# ifconfig eth0eth0:error fecthing interface information:Device not found这个信息就表明没有安装网卡et...
  • POS(packet over sonet)就是为了在传统SDH/SONET网络上传输IP分组。由于历史上电信的传输基本上都是基于...(不过基本上都是“计划”而已,好像只有BT在真的做网卡FIFO buffer大部分的Intel千兆网卡都内置了 FIFO bu...
  • 一、ring buffer在以前的一篇文章--网络编程中接受缓冲的ringbuf的简单实现介绍了一个自己写的ringbuffer,其实原理和linux内核中的队列很相似,思想是一样的,只不过处理的没有内核那么恰当巧妙,这里也可以使用...
  • 网卡 网卡工作在物理层和数据链路层,主要由PHY/MAC芯片、Tx/Rx FIFO、DMA等组成,其中网线通过变压器接PHY芯片、PHY芯片通过MII接MAC芯片、MAC芯片接PCI总线 PHY芯片主要负责:CSMA/CD、模数转换、编解码、串并转换...
  • 本文将介绍Linux系统中,基于RTL8139网卡驱动程序,是如何一步一步将接收到的数据包传送到内核的网络协议栈的。 下图展示了数据包(packet)如何进入内存,并被内核的网络模块开始处理: +-----+ | | Memroy +...
  • 本文讲解得e1000e网卡驱动主要用于intel网卡,以驱动的设计流程,分析整个驱动的接收和发送包过程。 首先介绍4个e1000e基础知识: 1)PCIE的配置空间初始化:PCIE卡都遵循一个标准, x86通过往2个内存地址读写就可以...
  • 1.引言  本分析主要针对e1000网卡,驱动源码为7.3.20-k2。...由于网卡驱动程序与硬件和操作系统都有很紧密的联系,故要把某些问题完全弄清楚,需要很多的经验与相关知识,介于自身的水平有限,且自
  • E100的网卡驱动源码

    2013-04-16 16:02:24
    E100的网卡驱动源码
  • Linux网卡驱动程序详解

    千次阅读 2016-09-27 08:46:40
    本博客转载自:http://blog.csdn.net/luyee2010/article/details/7019975在此仅仅讨论网络设备...1, 驱动模块的加载和卸载 如果网络设备(包括wireless)是PCI规范的,则先是向内核注册该PCI设备(pci_register_drive
  • 原文地址:linux查看网卡型号、驱动版本、队列数 作者:善地可期一、如何查看网卡生产厂家和型号? lspci命令 查看基本信息 lspci, 详细信息lspci -vvv 00:00.0 Host bridge: Intel Corporation 5520 I/O Hub ...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 3,659
精华内容 1,463
关键字:

linux ring 网卡驱动