分类
未分类

vmbus 分析二

上一篇写到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那么多 枚举的过程。把 外设当作一个主体,这种思想非常有借鉴意义。

发表评论

电子邮件地址不会被公开。 必填项已用*标注