Linux串口网卡(一)——通用虚拟网卡的实现

自从接触了Linux以太网驱动以来,我就一直很想能够自己实现一个网卡驱动。我很早就知道,Linux上,驱动开发者只需要实现物理层就好了,上面的其他层都直接使用内核现成的即可。想想当年给IBM做嵌入式,一开始使用GPRS做网络传输,但是GPRS本身不保证网络可靠,于是我用Java模拟TCP/IP写了一套流量控制、超时重发的逻辑,最终还bug百出,以失败告终。当时要是就会网卡驱动的开发,那么会多么方便呀!

后来,我一直想在两线串口上,比如USB-TTL串口上,实现TCP/IP传输。想想很简单,就是把以太帧通过串口发送出去、接收回来。这个想法最近终于实现了!而且我这套实现的通用性很强,几乎与硬件平台无关。我的思路是,整个系统驱动分两部分,一部分是运行在内核态的一个虚拟网卡驱动,另一部分是运行在用户态的转发进程。简单来说,就是当内核需要我的网卡发送某个帧时,我的内核态程序就把这个帧交给用户态程序,用户态程序调用串口发送出去。而当用户态程序通过串口收到一个帧时,则把这个帧交给内核态程序,由内核态程序交给内核。这样的好处是在于,一方面,处于内核态的虚拟网卡与硬件无关;另一方面,处于用户态的转发程序不需要在意串口驱动的实现,可以直接使用现成的串口驱动。于是乎,整个驱动就硬件无关了。

今天这篇要写的就是项目中处于内核态的部分。这一部分重点就是要解决一件事,就是如何搭建用户态进程与内核的桥梁。方法很多,我用的是/proc文件系统,算是我觉得最简单的了。大致流程是这样的:用户态进程读/proc/eth_uart/uio这个虚拟文件,得到一个需要发送出去的帧;用户态进程写/proc/eth_uart/uio,告知驱动程序收到一个帧。每次的交互单位就是一个帧。用户态可以使用阻塞式read(),直到有帧后返回,也可以使用非阻塞式IO轮询,也可以使用poll机制等待可读。

关于/proc的内容可以查看《第一个Linux驱动程序(四)——aMsg使用/proc文件系统》,关于poll机制的内容可以查看《第一个Linux驱动程序(三)——aMsg的非阻塞式IO之select/poll》,关于网卡驱动的内容可以查看《第一个Linux网络设备驱动——最简虚拟网卡virnet》。同时,里面还用到了信号量和等待队列,可以参见《第一个Linux驱动程序(二)——aMsg的阻塞式IO(互斥与同步)》。代码的几乎每一行都有注释,比大白话明白多了。

eth_uart.c

#include <linux/poll.h>
#include <linux/errno.h>
#include <linux/module.h>
#include <linux/proc_fs.h>
#include <linux/semaphore.h>
#include <linux/netdevice.h>
#include <linux/etherdevice.h>

MODULE_LICENSE("GPL");

// 网络设备对象
static struct net_device* sg_dev = 0;
// 与用户态转发程序对接的/proc/eth_uart目录
static struct proc_dir_entry* sg_proc = 0;
// 待发送的帧
static struct sk_buff* sg_frame = 0;
// 信号量,指示有待发送的帧(初始化为0)
static struct semaphore sg_sem_has_frame;
// 用于通知poll机制的等待队列
static wait_queue_head_t sg_poll_queue;

// 有数据帧要发送时,kernel会调用该函数
static int eth_uart_send_packet(struct sk_buff* skb,struct net_device *dev)
{
    // 告诉kernel不要传入更多的帧
    netif_stop_queue(sg_dev);
    // 统计已发送的数据包
    sg_dev->stats.tx_packets++;
    // 统计已发送的字节
    sg_dev->stats.tx_bytes += skb->len;
    // 复制帧
    sg_frame = skb;
    // 通知有待发送的帧
    up(&sg_sem_has_frame);
    // 唤醒阻塞的poll调用
    wake_up(&sg_poll_queue);
    return 0;
}

// 用户态转发程序通过读/proc/eth_uart/uio取走待发送的帧
static ssize_t eth_uart_uio_read(struct file* file, char* buf, size_t count, loff_t* offset)
{
    // 如果要求非阻塞操作
    if(file->f_flags & O_NONBLOCK)
    {
        // 查看是否有待发送的帧,如果没有则立即返回,否则锁定
        if(down_trylock(&sg_sem_has_frame) != 0)
            return -EAGAIN;
    }
    // 如果要求阻塞操作
    else
    {
        // 等待,直到有待发送的帧,若被中断则立即返回,否则锁定
        if(down_interruptible(&sg_sem_has_frame) != 0)
        {
            printk("<eth_uart.ko> down() interrupted...\n");
            return -EINTR;
        }
    }
    // 之所以复制一份sg_len,是为了避免netif_wake_queue()之后sg_frame可能被修改
    int len = sg_frame->len;
    // 空间不够
    if(count < len)
    {
        up(&sg_sem_has_frame);
        printk("<eth_uart.ko> no enough buffer to read the frame...\n");
        return -EFBIG;
    }
    // 把帧复制到用户态缓冲区
    copy_to_user(buf, sg_frame->data, len);
    // 释放数据帧
    dev_kfree_skb(sg_frame);
    sg_frame = 0;
    // 告诉内核可以传入更多帧了
    netif_wake_queue(sg_dev);
    return len;
}

// 用户可以对/proc/eth_uart/uio执行poll操作
static uint eth_uart_uio_poll(struct file* file, poll_table* queue)
{
    // 添加等待队列
    poll_wait(file, &sg_poll_queue, queue);
    // 不管如何,都是可写的
    uint mask = POLLOUT | POLLWRNORM;
    // 如果有帧,则设置状态码为可读
    if(sg_frame != 0)
        mask |= POLLIN | POLLRDNORM;
    return mask;
}

// 用户态转发程序从物理上收到一个帧,通过写/proc/eth_uart/uio告知驱动程序
static ssize_t eth_uart_uio_write(struct file* file, const char* buf, size_t count, loff_t* offset)
{
    // 分配count + 2字节的空间
    struct sk_buff* skb = dev_alloc_skb(count + 2);
    if(skb == 0)
    {
        printk("<eth_uart.ko> dev_alloc_skb() failed!\n");
        return -ENOMEM;
    }
    // 开头的2字节预留,这样14字节的以太头就能对其到16字节
    skb_reserve(skb, 2);
    // 把接下来的count字节复制进来
    copy_from_user(skb_put(skb, count), buf, count);
    skb->dev = sg_dev;
    // 得到协议号
    skb->protocol = eth_type_trans(skb, sg_dev);
    // 底层没有校验,交给内核计算
    skb->ip_summed = CHECKSUM_NONE;
    // 统计已接收的数据包
    sg_dev->stats.rx_packets++;
    // 统计已发收的字节
    sg_dev->stats.rx_bytes += skb->len;
    // 通知kernel收到一个数据包
    netif_rx(skb);
    return count;
}

// 驱动程序支持的操作
static struct net_device_ops sg_ops =
{
    // 发送数据帧
    .ndo_start_xmit = eth_uart_send_packet,
};

// /proc/eth_uart/uio支持的操作
static struct file_operations sg_uio_ops =
{
    .owner = THIS_MODULE,
    // 读,即用户态转发程序取走待发送的帧
    .read = eth_uart_uio_read,
    // poll, 即用户态程序等待有帧可取(但不一定真的能取走)
    .poll = eth_uart_uio_poll,
    // 写,即用户态转发程序从物理线路上收到一个帧
    .write = eth_uart_uio_write,
};

// 驱动程序初始化
static int eth_uart_init(void)
{
    int ret = 0;
    // 创建一个网络设备,名为“eth_uart"
    sg_dev = alloc_netdev(0, "eth_uart", ether_setup);
    // kernel 4+上需要四个参数如下
    // sg_dev=alloc_netdev(0,"eth_uart", NET_NAME_UNKNOWN, ether_setup);
    // 该网络设备的操作集
    if(sg_dev == 0)
    {
        printk("<eth_uart.ko> alloc_netdev() failed!\n");
        ret = -EEXIST;
        goto err_1;
    }
    // 该网络设备的操作集
    sg_dev->netdev_ops = &sg_ops;
    // MAC地址前3字节固定为EC-A8-6B
    memcpy(sg_dev->dev_addr, "\xEC\xA8\x6B", 3);
    // MAC地址后3字节随机产生
    get_random_bytes((char*)sg_dev->dev_addr + 3, 3);
    // 注册网络设备
    ret = register_netdev(sg_dev);
    if(ret != 0)
    {
        printk("<eth_uart.ko> register_netdev() failed!\n");
        goto err_2;
    }
    // 创建/proc/eth_uart目录
    sg_proc = proc_mkdir("eth_uart", 0);
    if(sg_proc == 0)
    {
        printk("<eth_uart.ko> proc_mkdir() failed!\n");
        ret = -EEXIST;
        goto err_3;
    }
    // 创建/proc/eth_uart/uio文件
    struct proc_dir_entry* t_proc_uio = proc_create("uio", 0666, sg_proc, &sg_uio_ops);
    if(t_proc_uio == 0)
    {
        printk("<eth_uart.ko> proc_create() failed!\n");
        ret = -EEXIST;
        goto err_4;
    }
    // 初始化信号量
    sema_init(&sg_sem_has_frame, 0);
    // 初始化poll队列
    init_waitqueue_head(&sg_poll_queue);
    return 0;
    err_4:
        // 删除/proc/eth_uart目录
        remove_proc_entry("eth_uart", 0);
    err_3:
        // 注销网络设备
        unregister_netdev(sg_dev);
    err_2:
        // 释放网络设备对象
        free_netdev(sg_dev);
    err_1:
        ;
    return ret;
}

// 驱动程序销毁
static void eth_uart_exit(void)
{
    // 删除/proc/eth_uart/uio文件
    remove_proc_entry("uio", sg_proc);
    // 删除/proc/eth_uart目录
    remove_proc_entry("eth_uart", 0);
    // 注销网络设备
    unregister_netdev(sg_dev);
    // 释放对象
    free_netdev(sg_dev);
    // 释放数据帧
    if(sg_frame != 0)
        dev_kfree_skb(sg_frame);
}

module_init(eth_uart_init);
module_exit(eth_uart_exit);

代码里用红色标记的地方都是值得玩味的。

netif_stop_queue(struct net_device* dev);

这个函数是告知内核,网卡设备dev不能再处理更多的帧了。内核为每一个网卡都维护了一个队列,试图通过该网卡发送出去的帧都会被加入该队列。内核会串行地调用用户注册的函数,即struct net_device_ops sg_ops的.ndo_start_xmit字段指向的函数,将帧传入。由于我的驱动程序需要等待用户态从/proc/eth_uart/uio把帧取走,所以在取走之前不允许内核传入更多帧。对应地,当用户态取走了帧时,则告知内核可以传入更多帧:

netif_wake_queue(struct net_device* dev);

信号量sg_sem_has_frame的计数与帧的有无是严格对应的,其计数初始化为0,表示没有帧。当内核传入一个帧时,计数加一,即up();当需要取走一帧时,计数减一,即down_*()。这样充分利用了信号量的语义,保证了并发情形下不会出错。

当内核把一个帧,即数据结构struct sk_buff传给驱动程序时,内核并不知道驱动程序会如何处理该帧,因此驱动程序有责任在不再需要该帧时释放之,即:

dev_kfree_skb(struct sk_buff*);

当用户态把一帧通过/proc/eth_uart/uio传给驱动程序时,驱动程序只需要直接把它交给内核处理即可,而所有与并发有关的处理也由内核管理了,因此write()没有使用任何全局变量,是可重入的,在逻辑上简单地多。驱动通过:

netif_rx(struct sk_buff*);

告知内核收到了一帧。那么如何组建一个struct sk_buff呢?首先是分配内存空间,要使用专用的函数:

struct sk_buff* dev_alloc_skb(size_t);

代码中很奇怪,传入的size是帧长度count+2。为什么要多两字节呢?《Linux内核源码剖析:TCP/IP实现》第3.4.4节“数据预留和对齐”给出了答案(感谢51CTO.com):

最重要的原因还是,让14字节的以太网头部变成16字节,使得后面的负载对齐到16字节。

最后一个比较有趣的是,如何在内核中产生随机数。内核中可以使用函数:

void get_random_bytes(void *buf, int nbytes);

以产生指定长度的随机字节序列。MAC地址前3字节是厂商编号,这个最好不要随机。我固定为EC-A8-6B,这个厂商的信息如下:

后三字节则随机产生,这样基本避免了MAC地址冲突。

那么这个代码怎么用呢?首先是千年不变的Makefile:

obj-m := eth_uart.o
KERNEL_DIR := /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)

all: eth_uart.c
	make -C $(KERNEL_DIR) SUBDIRS=$(PWD) modules

clean:
	rm -f *.o *.ko *.mod.c *.symvers *.order

.PHONY:clean

接着运行:

make
insmod eth_uart.ko

此时使用

ifconfig -a

就能看到一张叫做eth_uart的以太网卡了:

同时,也多出来一个/proc/eth_uart/uio的虚拟文件:

至于用户态转发程序,就在接下来的博客中讲解了~