实现这个虚拟交换机肯定需要内核的支持,linux的模块加载是严格依赖版本号的,因此vmware的linux版本必然要提供源代码并且在vmware安装的时候根据当前运行的内核版本重新编译,因此也就给了我们看一眼虚拟交换机以及虚拟网络是如何实现的机会。以VMware Workstation 5.5.1 Build 19175 for Linux这个版本为例,网络方面内核模块在VMware-workstation-5.5.1-19175/vmware-distrib/lib/modules/source/vmnet-only目录中,其中有几个比较重要且简单的文件:
hub.c:实现了虚拟交换机,即Virtual Switch,上面可以连接很多“网卡”即卡口
bridge.c:实现了一个“网卡”,绑定在一个物理真实网卡上的一个卡口
driver.c:实现了一个接口,用于操作字符设备/dev/vmnetX
netif.c:实现了一个操作系统的虚拟网卡,用ifconfig可以看到,当然它包含一个卡口
userif.c:仅仅实现了一个卡口,操作系统看不到
vnetInt.h:定义了“网卡”的卡口,虚拟交换机端口的卡口等重要数据
下面从最基本的数据结构说起:
VNetJack是一个卡口,它没有mac地址的概念,但是却有peer的概念,peer就相当于“线缆”另一端的卡口,和真实网卡一样,如果一条双绞线插入了一个网卡卡口,另一端没有查在任何设备上,它还是不通的,因此peer的概念很重要,peer很显然是另一个卡口。
struct VNetJack {
VNetJack *peer;
int numPorts;
char name[VNET_MAX_JACK_NAME_LEN];
void *private; // private field for containing object
int index; // private field for containing object
VNetProcEntry *procEntry; // private field for containing object
void (*free)(VNetJack *this);
void (*rcv)(VNetJack *this, struct sk_buff *skb);
Bool (*cycleDetect)(VNetJack *this, int generation);
void (*portsChanged)(VNetJack *this);
int (*isBridged)(VNetJack *this);
};
既然VNetJack只是一个卡口,那么它可以存在于虚拟交换机上,也可以存在于一个“网卡”上。虚拟交换机的端口和“网卡”的区别就在于“网口”虽然也是一个卡口,然而它却拥有mac地址等一系列的属性,因此下面的VNetPort结构体仅仅表征这种有mac地址的“网卡”的卡口,因此它相当于VNetJack的“子类”,扩充了VNetJack。这个结构体主要用于userif这种卡口:
struct VNetPort {
VNetJack jack; // must be first
unsigned id;
uint32 flags;
uint8 paddr[ETH_ALEN];
uint8 ladrf[VNET_LADRF_LEN];
VNetPort *next;
int (*fileOpRead)(VNetPort *this, struct file *filp, char *buf, size_t count);
int (*fileOpWrite)(VNetPort *this, struct file *filp,const char *buf, size_t count);
int (*fileOpIoctl)(VNetPort *this, struct file *filp,unsigned int iocmd, unsigned long ioarg);
int (*fileOpPoll)(VNetPort *this, struct file *filp,poll_table *wait);
};
针对bridge模式的虚拟机网卡而言,还有一个数据结构VNetBridge,它绑定了一个物理真实网卡设备,并且实现了一个“网卡”卡口:
struct VNetBridge {
struct notifier_block notifier; // for device state changes
char name[VNET_NAME_LEN]; // name of net device (e.g., "eth0")
struct net_device *dev; file:///C:/Documents%20and%20Settings/Administrator/Application%20Data/Microsoft/Internet%20Explorer/Quick%20Launch/Mozilla%20Firefox.lnk // 绑定的真实网卡
struct sock *sk; // socket associated with skb's
struct packet_type pt; // used to add packet handler
Bool enabledPromisc; // track if promisc enabled
Bool warnPromisc; // tracks if warning has been logged
struct sk_buff *history[VNET_BRIDGE_HISTORY]; // avoid duplicate packets
spinlock_t historyLock; // protects 'history'
VNetPort port; // connection to virtual hub
Bool wirelessAdapter; // connected to wireless adapter?
struct SMACState *smac; // device structure for wireless
};
既然虚拟交换机的端口也是一种卡口,它也有send和receive例程,并且很简单,在hub.c中,其receive例程如下:
void VNetHubReceive(VNetJack *this, struct sk_buff *skb)
{
VNetHub *hub = (VNetHub*)this->private;
VNetJack *jack;
struct sk_buff *clone;
int i;
hub->stats[this->index].tx++;
for (i=0; i<NUM_JACKS_PER_HUB; i++) { //向所有连接在此交换机上的卡口将数据发送出去
jack = &hub->jack[i];
if (jack->private /* allocated */
&& jack->peer /* and connected */
&& (jack != this)) { /* and not a loop */
clone = skb_clone(skb, GFP_ATOMIC);
if (clone) {
VNetSend(jack, clone);
}
}
}
dev_kfree_skb(skb);
}
对于任何卡口而言,send例程实现应该是一样的,都是将数据发送给peer,因此这个send实现更简单:
void VNetSend(VNetJack *jack, struct sk_buff *skb)
{
read_lock(&vnetPeerLock);
if (jack && jack->peer && jack->peer->rcv) {
jack->peer->rcv(jack->peer, skb); //调用peer的rcv
} else {
dev_kfree_skb(skb);
}
read_unlock(&vnetPeerLock);
}
如果是一个“网卡”这种卡口发送了数据,也就是调用了send例程,由于它的peer肯定是hub,因此就调用hub的receive例程VNetHubReceive,而hub的receive例程将把数据send到它的所有的卡口上。老双绞线卡口对于线缆连接的“同种设备交叉连接,相异设备直连”的要求在这里有所体现,交换机的receive的本质其实还是一个或者多个send,因此相当于在交换机内部实现了线缆的交叉操作,因此主机和交换机之间是直连,唉,扯远了。
对于bridge模式的虚拟机网卡来讲,首先该虚拟机网卡卡口连接到了一个虚拟交换机,而后物理机器的真实网卡卡口也应连接到该虚拟交换机,可是物理网卡卡口的连接是我们左右不了的,因此采用模拟的办法,主要实现将数据发往真实网卡和实现从物理网卡接收数据就可以了,因此bridge.c中的bridge表示为一个VNetBridge,其VNetPort之VNetJack的receive例程有两个作用,如果目的地址是真实机器的物理网卡,则由真实机器接收数据,将数据送往协议栈上层,如果不是本真实机器,那么将数据从真实网卡发送出去:
void VNetBridgeReceiveFromVNet(VNetJack *this, struct sk_buff *skb)
{
VNetBridge *bridge = (VNetBridge*)this->private;
struct net_device *dev = bridge->dev;
uint8 dest[ETH_ALEN];
struct sk_buff *clone;
...
memcpy(dest, SKB_2_DESTMAC(skb), ETH_ALEN); //得到数据包的目的mac地址
...
if (MAC_EQ(dest, dev->dev_addr) ||
skb->len > dev->mtu + dev->hard_header_len) {
dev_unlock_list();
} else {
...
struct sock *sk = bridge->sk;
atomic_add(skb->truesize, &sk->sk_wmem_alloc);
clone->sk = sk;
clone->protocol = ((struct ethhdr *)skb->data)->h_proto; // XXX
if ((dev->flags & IFF_UP) != 0) {
dev_unlock_list();
DEV_QUEUE_XMIT(clone, dev, 0); //如果目的地不是本真实机器,则从绑定的bridge网卡发出
...
}
}
if (VNetPacketMatch(dest, dev->dev_addr, (uint8 *)&AllMultiFilter, dev->flags)) {
clone = skb_clone(skb, GFP_ATOMIC);
...
netif_rx_ni(clone); //如果是本真实机器,则将数据送往本真实机器的协议栈上层。
}
}
...
}
对于从物理网卡卡口接收数据,vmnet使用了packet机制,也就是调用dev_add_pack注册了一个ETH_P_ALL类型的协议处理器,这样任何到达真实机器的真实网卡的数据都会被bridge接收到了:
bridge->pt.func = VNetBridgeReceiveFromDev;
bridge->pt.type = htons(ETH_P_ALL);
bridge->pt.dev = bridge->dev;
...
dev_add_pack(&bridge->pt);
这个bridge的reiceive例程如下:
int VNetBridgeReceiveFromDev(struct sk_buff *skb, // IN: packet to receive
struct net_device *dev, // IN: unused
struct packet_type *pt, // IN: pt (pointer to bridge)
struct net_device *real_dev) // IN: real device, unused
{
VNetBridge *bridge = list_entry(pt, VNetBridge, pt);
int i;
unsigned long flags;
...
spin_unlock_irqrestore(&bridge->historyLock, flags);
...
skb_push(skb, skb->data - skb->mac.raw);
VNetSend(&bridge->port.jack, skb); //数据发往虚拟交换机
return 0;
}
对于nat模式的虚拟机网卡的虚拟网络,虚拟机网卡卡口需要连接在一个虚拟交换机上,由于虚拟机网卡是由用户空间的vmware-vmx进程实现的一个userif代理的,因此就由该userif的卡口连接到虚拟交换机上,另外为了本真实机器和虚拟机互通,本真实机器也要搞一块虚拟网卡出来用于该nat虚拟网卡,使用虚拟网卡的原因在于真实网卡直接和线缆相连,要么将数据bridge出去,要么路由出去,只有使用虚拟网卡才可以实现nat(openvpn使用基于netfilter的iptables的nat表在内核实现而vmware为了不依赖操作系统的机制,在用户态以透明代理的方式实现),该虚拟网卡的卡口也连接于虚拟交换机,它是一个netif卡口,最后用户态的进程vmnet-natd实现nat设备,它实现了一个userif卡口并且拥有mac地址,实际上就是一个模拟的nat网卡,不考虑dhcp的情况下这三个卡口就够了,它们连在一个虚拟交换机上。由于真实机器的虚拟网卡只是为了真实机器和虚拟机器互通,因此虚拟网卡卡口VNetJack的receive例程就是将数据送到本机协议栈的上层:
void VNetNetIfReceive(VNetJack *this, struct sk_buff *skb)
{
VNetNetIF *netIf = (VNetNetIF*)this->private;
uint8 *dest = SKB_2_DESTMAC(skb);
...
if (!VNetPacketMatch(dest, //数据包的目的mac地址
netIf->dev.dev_addr, //本虚拟网卡的mac地址,ifconfig vmnet8可以看到
(uint8 *)AllMultiFilter,
netIf->dev.flags)) {
goto drop_packet;
}
skb->dev = &netIf->dev;
skb->protocol = eth_type_trans(skb, &netIf->dev);
netif_rx_ni(skb); //通过该虚拟网卡比如vmnet8送到本机
netIf->stats.rx_packets++;
...
}
userif这种卡口显然只需要和用户态打交道,至于它的数据如何发往netif或者真实网卡,那就需要虚拟交换机来转接数据了,因此它的receive例程十分简单:
static void VNetUserIfReceive(VNetJack *this, struct sk_buff *skb)
{
VNetUserIF *userIf = (VNetUserIF*)this->private;
uint8 *dest = SKB_2_DESTMAC(skb);
...
if (!VNetPacketMatch(dest, //数据包的目的mac地址
userIf->port.paddr, //userif的VNetPort的mac地址,比如用户态vmnet-natd实现的nat网卡的mac地址,另外还有vmware-vmx代理的虚拟机的网卡的mac地址
userIf->port.ladrf,
userIf->port.flags)) {
userIf->stats.droppedMismatch++;
goto drop_packet;
}
...
skb_queue_tail(&userIf->packetQueue, skb); //排入字符设备队列
...
wake_up(&userIf->waitQueue); //唤醒用户态进程,比如vmware-vmx或者vmnet-natd
return;
...
}
userif这种卡口receive的数据需要发送给/dev/vmnetX这种字符设备,这是由userif的read例程完成的:
static int VNetUserIfRead(VNetPort *port, // IN
struct file *filp, // IN
char *buf, // OUT
size_t count) // IN
{
VNetUserIF *userIf = (VNetUserIF*)port->jack.private;
struct sk_buff *skb;
int ret;
DECLARE_WAITQUEUE(wait, current);
add_wait_queue(&userIf->waitQueue, &wait);
for (;;) {
...
skb = skb_dequeue(&userIf->packetQueue); //从队列退出skb,该skb由userif的receive例程queue进去
...
}
... //拷贝至用户态的vmnet-natd,vmware-vmx等进程的地址空间
count = VNetCopyDatagramToUser(skb, buf, count);
dev_kfree_skb(skb);
return count;
}
而从/dev/vmnetX字符设备发出的数据需要send到虚拟交换机,通过userif的write例程实现:
static int VNetUserIfWrite(VNetPort *port, // IN
struct file *filp, // IN
const char *buf, // IN
size_t count) // IN
{
VNetUserIF *userIf = (VNetUserIF*)port->jack.private;
struct sk_buff *skb;
...
skb = dev_alloc_skb(count + 7);
skb_reserve(skb, 2);
userIf->stats.written++;
copy_from_user(skb_put(skb, count), buf, count);//拷贝用户态数据到内核,转为skb
VNetSend(&userIf->port.jack, skb);
return count;
}
最后在driver.c中,实现了一个总控机制,创建了/dev/vmnetX字符设备,并且实现了open/write/read/ioctl等文件系统的操作:
VNetFileOpOpen()
{
hubNum = minor(inode->i_rdev); //通过/dev/vmnetX中的X识别虚拟交换机,mknod的时候X和其次设备号有对应关系
retval = VNetUserIf_Create(&port); //创建一个卡口
hubJack = VNetHub_AllocVnet(hubNum); //要么分配一个虚拟交换机,要么返回已有的
retval = VNetConnect(&port->jack, hubJack); //将“网卡”卡口插到虚拟交换机上
}
read/write例程就是简单的调用“网卡”卡口VNetPort的fileOpRead/fileOpWrite例程