网络驱动程序:使用虚拟设备模拟网络通信

weirme 2022-01-18 10:33:20

在Linux系统中,有字符设备、块设备和网络设备三种标准设备类型。其中字符设备和块设备通常作为/dev目录下的一个特殊文件而存在,系统使用通用的文件系统接口对这些设备进行操作。而网络设备与前者不同,不能对其进行常用的由file_operations定义的读写操作,而是有一系列不同的处理方法。除此之外,网络设备还有一个显著的不同,即不只是响应来自内核的请求,还可能异步的接收来自外部世界的数据包,这也增加了网络驱动程序接口的复杂性。网络驱动程序需要支持大量的管理任务,如设置地址、修改传输参数、以及维护流量和错误统计等。网络驱动程序的API反映了这一需求,这也使得它看起来与字符设备、块设备的驱动接口有所不同。

为了学习网络驱动程序的基本工作方式和流程,我们创建两个虚拟的网络设备,并分配属于两个不同C类网段的IP地址。在网络驱动的数据收发中,修改数据报内容,来实现这两个设备的跨网络通信。

内核模块

内核模块是Linux内核向外部提供的一个插口,其全称为动态可加载内核模块,我们简称为模块。Linux内核之所以提供模块机制,是因为它本身是一个单内核。单内核的最大优点是效率高,因为所有的内容都集成在一起,但其缺点是可扩展性和可维护性相对较差,模块机制就是为了弥补这一缺陷。

Linux的所有设备驱动程序几乎都是通过模块的形式来完成的,一部分驱动与内核一起编译并装载,另一些可选的模块则由用户自己编译添加。对于内核模块的编写需要非常谨慎,因为一旦在内核空间出错,就可能直接引起系统的崩溃。

在编写模块时,必须实现模块的初始化函数和注销函数,通过module_init()module_exit()函数进行绑定。此外,还需要在模块文件中声明模块的作者、描述、版本、遵循的许可证等必要信息。在完成模块编写后,进行编译得到.ko文件,接着使用insmod指令即可完成模块的装载。

基本数据结构

sk_buff

sk_buff结构体在内核中处于网络子系统的核心地位,其用来抽象的表示一个套接字缓冲区。从其中可以找到处理该套接字缓冲区的设备,以及指向数据包各个层的报文头的指针。通过这个结构体,可以方便的对要发送或接收的数据进行操作。

net_deivce

net_device是网络设备驱动编写中最核心的一个数据结构,它代表了一个网络设备在操作系统中的抽象,在其中定义了设备名称、设备状态等基本信息,以及网络接口地址、中断号等硬件信息,同时也通过函数指针的方式包含了网络设备的打开、数据发送等基本操作的方法。

net_device->priv

privnet_device结构体的一个成员,该成员的作用和字符设备驱动中的private_data指针类似,保存了属于该设备的私有数据,并由驱动程序进行定义和维护。我们的priv结构体定义如下:

struct snull_priv {
  /* 网络设备状态 */
    struct net_device_stats stats; 
    int status;
  /* 维护的空闲数据包列表,每次发送时都从中取出一个数据包 */
    struct snull_packet *ppool; 
    struct snull_packet *rx_queue; /* 接收到的数据包队列 */
    int rx_int_enabled; /* 接收中断使能信号 */
    int tx_packetlen; /* 发送数据包长度 */
    u8 *tx_packetdata; /* 发送数据包数据 */
    struct sk_buff *skb;    /* 当前正在处理的套接字对象 */
    spinlock_t lock; /* 自旋锁 */
};

模块的初始化和注销

在我们的网络驱动中,用到了两个虚拟设备,因此需要分配两个net_deivce结构体,函数代码如下:

snull_devs[0] = alloc_netdev(sizeof(struct snull_priv), "sn%d", snull_init);
snull_devs[1] = alloc_netdev(sizeof(struct snull_priv), "sn%d", snull_init);

其中snull_init函数是我们自己编写的设备初始化函数。在该函数中对dev对象进行赋值,将其中的函数指针绑定到具体函数,初始化私有数据成员priv,并控制硬件打开接收数据包的中断(在我们的网络驱动中通过软件来模拟这个过程)。

dev对象被初始化后,接着就能将该结构传递给register_netdev()函数进行设备注册,注册完成后,就可以调用驱动程序操作设备了。

对于模块的注销,只要逆序的执行初始化过程的步骤即可,先取消设备的注册,再释放net_device结构体。

设备的打开和关闭

在接口能够传送数据包之前,必须由内核打开接口并赋予其地址。在Linux系统中,内核可以在响应ifconfig命令时打开或关闭一个接口。对于我们这里使用的虚拟设备来说,在打开或关闭时执行的操作都很简单。在打开时,我们对设备的硬件MAC地址赋值,并开启数据包传输队列。

memcpy(dev->dev_addr, "\0SNUL0", ETH_ALEN);
if (dev == snull_devs[1])
    dev->dev_addr[ETH_ALEN-1]++; /* \0SNUL1 */
netif_start_queue(dev); /* 开启传输队列 */

而在关闭设备时,我们只需要简单的关闭传输队列即可。

数据发送

数据发送对应net_device结构体中的一个函数指针成员hard_start_xmit。该函数将在内核发送数据时调用,输入的参数有两个,一个是从上层传递来的套接字缓冲sk_buff结构体指针,另一个是用来发送该数据的设备net_device结构体指针。

在数据发送处理中,我们首先通过下面的代码从套接字缓冲中获取数据的信息:

/* 从套接字缓冲中获取数据 */
data = skb->data;
len = skb->len;

/* 复制数据 */
if (len < ETH_ZLEN) {
    memset(shortpkt, 0, ETH_ZLEN);
    memcpy(shortpkt, skb->data, skb->len);
    len = ETH_ZLEN;
    data = shortpkt;
}

/* 将套接字缓冲保存到私有数据中,直到该数据被发送完后释放 */
priv->skb = skb;

为了方便后续的测试,在这里我们插入一段用于输出数据包内容的打印代码,可以直观的查看到数据包的网络层头部、传输层头部和数据。

int i;
for (i=14 ; i<34; i++)
  printk(" %02x",buf[i]&0xff);
printk("\n");
for (i=34 ; i<54; i++)
  printk(" %02x",buf[i]&0xff);
printk("\n");
for (i=54 ; i<len; i++)
  printk(" %02x",buf[i]&0xff);
printk("\n");

按照网络数据发送的流程,接下来需要操控硬件来完成。但由于我们使用的是虚拟设备,这里采用代码的方式来模拟硬件发送过程。在发送数据之前,为了实现我们的网络驱动的功能,需要直接对数据报内容进行修改。根据我们的网络驱动功能,核心的修改代码如下:

/* 获取源地址和目标地址 */
ih = (struct iphdr *)(buf+sizeof(struct ethhdr));
saddr = &ih->saddr;
daddr = &ih->daddr;

/* 第三个字节进行取反 */
((u8 *)saddr)[2] ^= 1; 
((u8 *)daddr)[2] ^= 1;

/* 重新计算校验和 */
ih->check = 0;         
ih->check = ip_fast_csum((unsigned char *)ih,ih->ihl);

对数据报的处理完成后,接下来构造通过网卡发出的报文。

/* 从设备的空缓冲区链表里取出一个缓冲区 */
tx_buffer = snull_get_tx_buffer(dev);
tx_buffer->datalen = len;
memcpy(tx_buffer->data, buf, len); /* 装入数据 */

在实际的网络驱动中,之后的工作应该交给硬件来完成,但由于我们这里使用的是虚拟设备,无法进行硬件操作,故通过代码来模拟这个过程。我们通过数据报内容找到目的IP对应的设备dest,将这个待发出的报文直接加入到该设备的接收队列里,并通过代码来产生一个中断(这个中断本应由网卡硬件通过中断控制器产生)。

snull_enqueue_buf(dest, tx_buffer);
if (priv->rx_int_enabled) {
    priv->status |= SNULL_RX_INTR;
    snull_interrupt(0, dest, NULL);
}

进一步的,我们通过代码来模拟一个发送端的发送完成中断。

priv = netdev_priv(dev);
priv->tx_packetlen = len;
priv->tx_packetdata = buf;
priv->status |= SNULL_TX_INTR;
snull_interrupt(0, dev, NULL);

数据接收

数据接收是网络设备驱动和字符设备、块设备之间的一个不同之处,对于网络设备来说,接收的数据是异步的从外部主机发送来的。在模块初始化,我们需要向系统的中断处理函数表中注册我们的设备处理函数,在该函数中实现我们的数据接收代码。当网卡设备接收到数据时,就触发中断,调用我们的处理函数。对于我们这里使用的虚拟设备,我们是通过代码的方式来模拟中断,因此也不需要向系统进行注册,只要在中断处理函数中实现相应的逻辑即可。

另外要注意的是,在中断处理函数里,我们需要对接收到数据和数据已发送完这两种不同的子中断进行处理,我们本应通过硬件读取到具体的子中断名,而这里我们是通过设备私有数据中的一个status成员来记录中断名。处理的代码如下:

statusword = priv->status; /* 读取状态 */
priv->status = 0; /* 清除中断标志 */

/* 处理接收中断 */
if (statusword & SNULL_RX_INTR) {
    pkt = priv->rx_queue; /* 从接收队列获取数据包 */
    if (pkt) {
        priv->rx_queue = pkt->next;
        snull_rx(dev, pkt); /* 调用接收处理函数 */
    }
}

/* 处理发送完成中断 */
if (statusword & SNULL_TX_INTR) {
  /* 修改设备的状态记录,可以通过ifconfig查看到 */
    priv->stats.tx_packets++;
    priv->stats.tx_bytes += priv->tx_packetlen;
  /* 释放以处理完成的套接字缓冲 */
    dev_kfree_skb(priv->skb);
}

在真正的接收处理函数中,我们需要把网卡的数据转换成sk_buff类型,交给上层进行处理。在这个函数中的主要工作就是申请一个套接字缓冲区,并对其初始化赋值,将数据拷贝到其中。

/* 申请一个套接字缓冲区,并向其中拷贝数据 */
skb = dev_alloc_skb(pkt->datalen + 2);
skb_reserve(skb, 2);
memcpy(skb_put(skb, pkt->datalen), pkt->data, pkt->datalen);

/* 对sk_buff结构中的成员变量进行赋值 */
skb->dev = dev;
skb->protocol = eth_type_trans(skb, dev);
skb->ip_summed = CHECKSUM_UNNECESSARY;

/* 修改设备的状态记录,可以通过ifconfig查看到 */
struct snull_priv *priv = netdev_priv(dev);
priv->stats.rx_packets++;
priv->stats.rx_bytes += pkt->datalen;

/* 移交给上层处理 */
netif_rx(skb);

网络驱动的装载和测试

1. 编译模块并装载

make
insmod snull.ko

完成后系统中会新增sn0sn1两个网络接口。

2. 给网络接口绑定IP

ifconfig sn0 local0
ifconfig sn1 local1

绑定完成后,可以通过ifconfig命令查看接口信息,输出结果如下图所示。

img

3. 测试连通性

ping -c 1 remote0

这里我们通过常用的ping命令来测试接口是否连通,当访问remote0时,数据从local0发出,并通过我们驱动程序中的处理送入到local1。命令执行的结果如下图所示,可见网络驱动程序能够正常工作。

img

接着通过dmesg命令查看内核的输出,可以在其中找到执行ping命令时的往返数据包,如下图所示。

img

图中两个数据包各自第一行为IP数据报的首部,在行末可以找到源和目的IP地址,第一条报文从local0发往remote0,而第二条的回复报文则是从local1发往remote1,可见在网络驱动中实际上发生了IP地址的转换。

最后,再次使用ifconfig命令查看网络接口的状态,可以发现这时sn0sn1两个接口的发送数据包和接收数据包个数都增加了1,正好与ping命令的2次数据传输过程一致。

img

参考

《Linux设备驱动程序第三版》

作者:NP265

...全文
233 回复 打赏 收藏 转发到动态 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复

571

社区成员

发帖
与我相关
我的任务
社区描述
软件工程教学新范式,强化专项技能训练+基于项目的学习PBL。Git仓库:https://gitee.com/mengning997/se
软件工程 高校
社区管理员
  • 码农孟宁
加入社区
  • 近7日
  • 近30日
  • 至今

试试用AI创作助手写篇文章吧