上一篇写到vmbus 怎么发现 vmbus上的设备,这一篇讲vmbus的网络设备怎么probe.
当有新的vmbus设备注册 进来时,vmbus 会调用注册在上面的驱动的match函数去匹配是否可以用这个驱动来驱起这个设备,如果能驱起来则会调用probe函数来处理。
在vmbus 的网络设备上会调用 netvsc_probe函数来处理。 这里面的其他操作跟普通的网卡驱动没有什么区别,有意思的是他调用rndis_filter_device_add这个函数,这个函数里面有两个地方比较有意思 第一个是调用了netvsc_device_add函数。该函数如下。
struct netvsc_device *netvsc_device_add(struct hv_device *device,
>------->------->------->-------const struct netvsc_device_info *device_info)
{
>-------int i, ret = 0;
>-------struct netvsc_device *net_device;
>-------struct net_device *ndev = hv_get_drvdata(device);
>-------struct net_device_context *net_device_ctx = netdev_priv(ndev);
>-------net_device = alloc_net_device();
>-------if (!net_device)
>------->-------return ERR_PTR(-ENOMEM);
>-------for (i = 0; i < VRSS_SEND_TAB_SIZE; i++)
>------->-------net_device_ctx->tx_table[i] = 0;
>-------/* Because the device uses NAPI, all the interrupt batching and
>------- * control is done via Net softirq, not the channel handling
>------- */
>-------set_channel_read_mode(device->channel, HV_CALL_ISR);
>-------/* If we're reopening the device we may have multiple queues, fill the
>------- * chn_table with the default channel to use it before subchannels are
>------- * opened.
>------- * Initialize the channel state before we open;
>------- * we can be interrupted as soon as we open the channel.
>------- */
>-------for (i = 0; i < VRSS_CHANNEL_MAX; i++) {
>------->-------struct netvsc_channel *nvchan = &net_device->chan_table[i];
>------->-------nvchan->channel = device->channel;
>------->-------nvchan->net_device = net_device;
>------->-------u64_stats_init(&nvchan->tx_stats.syncp);
>------->-------u64_stats_init(&nvchan->rx_stats.syncp);
>-------}
>-------/* Enable NAPI handler before init callbacks */
>-------netif_napi_add(ndev, &net_device->chan_table[0].napi,
>------->------- netvsc_poll, NAPI_POLL_WEIGHT);
>-------/* Open the channel */
>-------ret = vmbus_open(device->channel, netvsc_ring_bytes,
>------->------->------- netvsc_ring_bytes, NULL, 0,
>------->------->------- netvsc_channel_cb, net_device->chan_table);
>-------if (ret != 0) {
>------->-------netdev_err(ndev, "unable to open channel: %d\n", ret);
>------->-------goto cleanup;
>-------}
>-------/* Channel is opened */
>-------netdev_dbg(ndev, "hv_netvsc channel opened successfully\n");
>-------napi_enable(&net_device->chan_table[0].napi);
>-------/* Connect with the NetVsp */
>-------ret = netvsc_connect_vsp(device, net_device, device_info);
>-------if (ret != 0) {
>------->-------netdev_err(ndev,
>------->------->-------"unable to connect to NetVSP - %d\n", ret);
>------->-------goto close;
>-------}
>-------/* Writing nvdev pointer unlocks netvsc_send(), make sure chn_table is
>------- * populated.
>------- */
>-------rcu_assign_pointer(net_device_ctx->nvdev, net_device);
>-------return net_device;
close:
>-------RCU_INIT_POINTER(net_device_ctx->nvdev, NULL);
>-------napi_disable(&net_device->chan_table[0].napi);
>-------/* Now, we can close the channel safely */
>-------vmbus_close(device->channel);
cleanup:
>-------netif_napi_del(&net_device->chan_table[0].napi);
>-------free_netvsc_device(&net_device->rcu);
>-------return ERR_PTR(ret);
}
这个函数里面调用了 vmbus_open ,vmbus_open里面调用了两个函数 vmbus_alloc_ring和__vmbus_open函数,第一个函数是用来创建接受数据的缓冲区,第二个是设置 channel的onchannel_callback函数的, 而onchannel_callback函数会在 vmbus_on_event函数中调用,或者在vmbus_channel_isr函数中调用 或者在vmbus_on_event函数中调用,vmbus_on_event是一个tasklet函数,这个tasklet 是channel->callback_event。 vmbus_on_event代码如下
void vmbus_on_event(unsigned long data)
{
>-------struct vmbus_channel *channel = (void *) data;
>-------unsigned long time_limit = jiffies + 2;
>-------trace_vmbus_on_event(channel);
>-------do {
>------->-------void (*callback_fn)(void *);
>------->-------/* A channel once created is persistent even when
>------->------- * there is no driver handling the device. An
>------->------- * unloading driver sets the onchannel_callback to NULL.
>------->------- */
>------->-------callback_fn = READ_ONCE(channel->onchannel_callback);
>------->-------if (unlikely(callback_fn == NULL))
>------->------->-------return;
>------->-------(*callback_fn)(channel->channel_callback_context);
>------->-------if (channel->callback_mode != HV_CALL_BATCHED)
>------->------->-------return;
>------->-------if (likely(hv_end_read(&channel->inbound) == 0))
>------->------->-------return;
>------->-------hv_begin_read(&channel->inbound);
>-------} while (likely(time_before(jiffies, time_limit)));
>-------/* The time limit (2 jiffies) has been reached */
>-------tasklet_schedule(&channel->callback_event);
}
static void vmbus_channel_isr(struct vmbus_channel *channel)
{
>-------void (*callback_fn)(void *);
>-------callback_fn = READ_ONCE(channel->onchannel_callback);
>-------if (likely(callback_fn != NULL))
>------->-------(*callback_fn)(channel->channel_callback_context);
}
到这里我们跟上一篇的 vmbus_isr函数对上, vmbus_isr函数会调用vmbus_chan_sched 函数.vmbus_chan_sched 会根据 callback_mode的类型选择是挂起tasklet或者直接调用onchannel_callback函数,很不巧网卡刚刚好是HV_CALL_ISR类型,因此会直接调用onchannel_callback函数。调用的函数netvsc_channel_cb 。 netvsc_channel_cb函数如下。这个函数只是简单的挂起这个channel的napi函数,最后在napi中收包。
void netvsc_channel_cb(void *context)
{
>-------struct netvsc_channel *nvchan = context;
>-------struct vmbus_channel *channel = nvchan->channel;
>-------struct hv_ring_buffer_info *rbi = &channel->inbound;
>-------/* preload first vmpacket descriptor */
>-------prefetch(hv_get_ring_buffer(rbi) + rbi->priv_read_index);
>-------if (napi_schedule_prep(&nvchan->napi)) {
>------->-------/* disable interrupts from host */
>------->-------hv_begin_read(rbi);
>------->-------__napi_schedule_irqoff(&nvchan->napi);
>-------}
}
如果网卡只有一个队列,对着vmbus的设备来说只有一个channel,到这里已经完事, 但是如果网卡有多个队列,也就是多个channel,这个时候 netvsc_sc_open函数会在下一个channel被探测到的时候被执行,主要是因为在rndis_filter_device_add函数中执行 vmbus_set_sc_create_callback函数,该函数如下,看到这个函数我们就想到上一篇说 sub channel 被探测到以后,不会在vmbus中注册但是会执行primary channel的sc_creation_callback函数。这边会执行的就是netvsc_sc_open函数。
void vmbus_set_sc_create_callback(struct vmbus_channel *primary_channel,
>------->------->------->-------void (*sc_cr_cb)(struct vmbus_channel *new_sc))
{
>-------primary_channel->sc_creation_callback = sc_cr_cb;
}
netvsc_sc_open函数如下。
static void netvsc_sc_open(struct vmbus_channel *new_sc)
{
>-------struct net_device *ndev =
>------->-------hv_get_drvdata(new_sc->primary_channel->device_obj);
>-------struct net_device_context *ndev_ctx = netdev_priv(ndev);
>-------struct netvsc_device *nvscdev;
>-------u16 chn_index = new_sc->offermsg.offer.sub_channel_index;
>-------struct netvsc_channel *nvchan;
>-------int ret;
>-------/* This is safe because this callback only happens when
>------- * new device is being setup and waiting on the channel_init_wait.
>------- */
>-------nvscdev = rcu_dereference_raw(ndev_ctx->nvdev);
>-------if (!nvscdev || chn_index >= nvscdev->num_chn)
>------->-------return;
>-------nvchan = nvscdev->chan_table + chn_index;
>-------/* Because the device uses NAPI, all the interrupt batching and
>------- * control is done via Net softirq, not the channel handling
>------- */
>-------set_channel_read_mode(new_sc, HV_CALL_ISR);
>-------/* Set the channel before opening.*/
>-------nvchan->channel = new_sc;
>-------ret = vmbus_open(new_sc, netvsc_ring_bytes,
>------->------->------- netvsc_ring_bytes, NULL, 0,
>------->------->------- netvsc_channel_cb, nvchan);
>-------if (ret == 0)
>------->-------napi_enable(&nvchan->napi);
>-------else
>------->-------netdev_notice(ndev, "sub channel open failed: %d\n", ret);
>-------if (atomic_inc_return(&nvscdev->open_chn) == nvscdev->num_chn)
>------->-------wake_up(&nvscdev->subchan_open);
}
在netvsc_sc_open 我们看到网卡设备打开了更多的队列,当队列数量等于channel数量后,网卡的其他设置功能可用。
vmbus 这块我觉得比较有创意的地方在于,把0xf3 这个 irq当作了一个percpu的irq,然后讲 所有的channel都共享irq。减少了irq的数量。 这个没有pci那么多 枚举的过程。把 外设当作一个主体,这种思想非常有借鉴意义。