hv_netvsc: rework link status change handling
There are several issues in hv_netvsc driver with regards to link status change handling: - RNDIS_STATUS_NETWORK_CHANGE results in calling userspace helper doing '/etc/init.d/network restart' and this is inappropriate and broken for many reasons. - link_watch infrastructure only sends one notification per second and in case of e.g. paired disconnect/connect events we get only one notification with last status. This makes it impossible to handle such situations in userspace. Redo link status changes handling in the following way: - Create a list of reconfig events in network device context. - On a reconfig event add it to the list of events and schedule netvsc_link_change(). - In netvsc_link_change() ensure 2-second delay between link status changes. - Handle RNDIS_STATUS_NETWORK_CHANGE as a paired disconnect/connect event. Signed-off-by: Vitaly Kuznetsov <vkuznets@redhat.com> Signed-off-by: David S. Miller <davem@davemloft.net>
This commit is contained in:
committed by
David S. Miller
parent
77b75f4d8c
commit
27a70af3f4
@@ -177,7 +177,6 @@ struct rndis_device {
|
|||||||
|
|
||||||
enum rndis_device_state state;
|
enum rndis_device_state state;
|
||||||
bool link_state;
|
bool link_state;
|
||||||
bool link_change;
|
|
||||||
atomic_t new_req_id;
|
atomic_t new_req_id;
|
||||||
|
|
||||||
spinlock_t request_lock;
|
spinlock_t request_lock;
|
||||||
@@ -644,11 +643,24 @@ struct netvsc_stats {
|
|||||||
struct u64_stats_sync syncp;
|
struct u64_stats_sync syncp;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct netvsc_reconfig {
|
||||||
|
struct list_head list;
|
||||||
|
u32 event;
|
||||||
|
};
|
||||||
|
|
||||||
/* The context of the netvsc device */
|
/* The context of the netvsc device */
|
||||||
struct net_device_context {
|
struct net_device_context {
|
||||||
/* point back to our device context */
|
/* point back to our device context */
|
||||||
struct hv_device *device_ctx;
|
struct hv_device *device_ctx;
|
||||||
|
/* reconfigure work */
|
||||||
struct delayed_work dwork;
|
struct delayed_work dwork;
|
||||||
|
/* last reconfig time */
|
||||||
|
unsigned long last_reconfig;
|
||||||
|
/* reconfig events */
|
||||||
|
struct list_head reconfig_events;
|
||||||
|
/* list protection */
|
||||||
|
spinlock_t lock;
|
||||||
|
|
||||||
struct work_struct work;
|
struct work_struct work;
|
||||||
u32 msg_enable; /* debug level */
|
u32 msg_enable; /* debug level */
|
||||||
|
|
||||||
|
|||||||
@@ -42,6 +42,7 @@
|
|||||||
|
|
||||||
|
|
||||||
#define RING_SIZE_MIN 64
|
#define RING_SIZE_MIN 64
|
||||||
|
#define LINKCHANGE_INT (2 * HZ)
|
||||||
static int ring_size = 128;
|
static int ring_size = 128;
|
||||||
module_param(ring_size, int, S_IRUGO);
|
module_param(ring_size, int, S_IRUGO);
|
||||||
MODULE_PARM_DESC(ring_size, "Ring buffer size (# of pages)");
|
MODULE_PARM_DESC(ring_size, "Ring buffer size (# of pages)");
|
||||||
@@ -647,37 +648,33 @@ void netvsc_linkstatus_callback(struct hv_device *device_obj,
|
|||||||
struct net_device *net;
|
struct net_device *net;
|
||||||
struct net_device_context *ndev_ctx;
|
struct net_device_context *ndev_ctx;
|
||||||
struct netvsc_device *net_device;
|
struct netvsc_device *net_device;
|
||||||
struct rndis_device *rdev;
|
struct netvsc_reconfig *event;
|
||||||
|
unsigned long flags;
|
||||||
|
|
||||||
|
/* Handle link change statuses only */
|
||||||
|
if (indicate->status != RNDIS_STATUS_NETWORK_CHANGE &&
|
||||||
|
indicate->status != RNDIS_STATUS_MEDIA_CONNECT &&
|
||||||
|
indicate->status != RNDIS_STATUS_MEDIA_DISCONNECT)
|
||||||
|
return;
|
||||||
|
|
||||||
net_device = hv_get_drvdata(device_obj);
|
net_device = hv_get_drvdata(device_obj);
|
||||||
rdev = net_device->extension;
|
|
||||||
|
|
||||||
switch (indicate->status) {
|
|
||||||
case RNDIS_STATUS_MEDIA_CONNECT:
|
|
||||||
rdev->link_state = false;
|
|
||||||
break;
|
|
||||||
case RNDIS_STATUS_MEDIA_DISCONNECT:
|
|
||||||
rdev->link_state = true;
|
|
||||||
break;
|
|
||||||
case RNDIS_STATUS_NETWORK_CHANGE:
|
|
||||||
rdev->link_change = true;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
net = net_device->ndev;
|
net = net_device->ndev;
|
||||||
|
|
||||||
if (!net || net->reg_state != NETREG_REGISTERED)
|
if (!net || net->reg_state != NETREG_REGISTERED)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
ndev_ctx = netdev_priv(net);
|
ndev_ctx = netdev_priv(net);
|
||||||
if (!rdev->link_state) {
|
|
||||||
|
event = kzalloc(sizeof(*event), GFP_ATOMIC);
|
||||||
|
if (!event)
|
||||||
|
return;
|
||||||
|
event->event = indicate->status;
|
||||||
|
|
||||||
|
spin_lock_irqsave(&ndev_ctx->lock, flags);
|
||||||
|
list_add_tail(&event->list, &ndev_ctx->reconfig_events);
|
||||||
|
spin_unlock_irqrestore(&ndev_ctx->lock, flags);
|
||||||
|
|
||||||
schedule_delayed_work(&ndev_ctx->dwork, 0);
|
schedule_delayed_work(&ndev_ctx->dwork, 0);
|
||||||
schedule_delayed_work(&ndev_ctx->dwork, msecs_to_jiffies(20));
|
|
||||||
} else {
|
|
||||||
schedule_delayed_work(&ndev_ctx->dwork, 0);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -1009,12 +1006,9 @@ static const struct net_device_ops device_ops = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Send GARP packet to network peers after migrations.
|
* Handle link status changes. For RNDIS_STATUS_NETWORK_CHANGE emulate link
|
||||||
* After Quick Migration, the network is not immediately operational in the
|
* down/up sequence. In case of RNDIS_STATUS_MEDIA_CONNECT when carrier is
|
||||||
* current context when receiving RNDIS_STATUS_MEDIA_CONNECT event. So, add
|
* present send GARP packet to network peers with netif_notify_peers().
|
||||||
* another netif_notify_peers() into a delayed work, otherwise GARP packet
|
|
||||||
* will not be sent after quick migration, and cause network disconnection.
|
|
||||||
* Also, we update the carrier status here.
|
|
||||||
*/
|
*/
|
||||||
static void netvsc_link_change(struct work_struct *w)
|
static void netvsc_link_change(struct work_struct *w)
|
||||||
{
|
{
|
||||||
@@ -1022,36 +1016,89 @@ static void netvsc_link_change(struct work_struct *w)
|
|||||||
struct net_device *net;
|
struct net_device *net;
|
||||||
struct netvsc_device *net_device;
|
struct netvsc_device *net_device;
|
||||||
struct rndis_device *rdev;
|
struct rndis_device *rdev;
|
||||||
bool notify, refresh = false;
|
struct netvsc_reconfig *event = NULL;
|
||||||
char *argv[] = { "/etc/init.d/network", "restart", NULL };
|
bool notify = false, reschedule = false;
|
||||||
char *envp[] = { "HOME=/", "PATH=/sbin:/usr/sbin:/bin:/usr/bin", NULL };
|
unsigned long flags, next_reconfig, delay;
|
||||||
|
|
||||||
rtnl_lock();
|
|
||||||
|
|
||||||
ndev_ctx = container_of(w, struct net_device_context, dwork.work);
|
ndev_ctx = container_of(w, struct net_device_context, dwork.work);
|
||||||
net_device = hv_get_drvdata(ndev_ctx->device_ctx);
|
net_device = hv_get_drvdata(ndev_ctx->device_ctx);
|
||||||
rdev = net_device->extension;
|
rdev = net_device->extension;
|
||||||
net = net_device->ndev;
|
net = net_device->ndev;
|
||||||
|
|
||||||
if (rdev->link_state) {
|
next_reconfig = ndev_ctx->last_reconfig + LINKCHANGE_INT;
|
||||||
netif_carrier_off(net);
|
if (time_is_after_jiffies(next_reconfig)) {
|
||||||
notify = false;
|
/* link_watch only sends one notification with current state
|
||||||
} else {
|
* per second, avoid doing reconfig more frequently. Handle
|
||||||
netif_carrier_on(net);
|
* wrap around.
|
||||||
notify = true;
|
*/
|
||||||
if (rdev->link_change) {
|
delay = next_reconfig - jiffies;
|
||||||
rdev->link_change = false;
|
delay = delay < LINKCHANGE_INT ? delay : LINKCHANGE_INT;
|
||||||
refresh = true;
|
schedule_delayed_work(&ndev_ctx->dwork, delay);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
ndev_ctx->last_reconfig = jiffies;
|
||||||
|
|
||||||
|
spin_lock_irqsave(&ndev_ctx->lock, flags);
|
||||||
|
if (!list_empty(&ndev_ctx->reconfig_events)) {
|
||||||
|
event = list_first_entry(&ndev_ctx->reconfig_events,
|
||||||
|
struct netvsc_reconfig, list);
|
||||||
|
list_del(&event->list);
|
||||||
|
reschedule = !list_empty(&ndev_ctx->reconfig_events);
|
||||||
|
}
|
||||||
|
spin_unlock_irqrestore(&ndev_ctx->lock, flags);
|
||||||
|
|
||||||
|
if (!event)
|
||||||
|
return;
|
||||||
|
|
||||||
|
rtnl_lock();
|
||||||
|
|
||||||
|
switch (event->event) {
|
||||||
|
/* Only the following events are possible due to the check in
|
||||||
|
* netvsc_linkstatus_callback()
|
||||||
|
*/
|
||||||
|
case RNDIS_STATUS_MEDIA_CONNECT:
|
||||||
|
if (rdev->link_state) {
|
||||||
|
rdev->link_state = false;
|
||||||
|
netif_carrier_on(net);
|
||||||
|
netif_tx_wake_all_queues(net);
|
||||||
|
} else {
|
||||||
|
notify = true;
|
||||||
|
}
|
||||||
|
kfree(event);
|
||||||
|
break;
|
||||||
|
case RNDIS_STATUS_MEDIA_DISCONNECT:
|
||||||
|
if (!rdev->link_state) {
|
||||||
|
rdev->link_state = true;
|
||||||
|
netif_carrier_off(net);
|
||||||
|
netif_tx_stop_all_queues(net);
|
||||||
|
}
|
||||||
|
kfree(event);
|
||||||
|
break;
|
||||||
|
case RNDIS_STATUS_NETWORK_CHANGE:
|
||||||
|
/* Only makes sense if carrier is present */
|
||||||
|
if (!rdev->link_state) {
|
||||||
|
rdev->link_state = true;
|
||||||
|
netif_carrier_off(net);
|
||||||
|
netif_tx_stop_all_queues(net);
|
||||||
|
event->event = RNDIS_STATUS_MEDIA_CONNECT;
|
||||||
|
spin_lock_irqsave(&ndev_ctx->lock, flags);
|
||||||
|
list_add_tail(&event->list, &ndev_ctx->reconfig_events);
|
||||||
|
spin_unlock_irqrestore(&ndev_ctx->lock, flags);
|
||||||
|
reschedule = true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
rtnl_unlock();
|
rtnl_unlock();
|
||||||
|
|
||||||
if (refresh)
|
|
||||||
call_usermodehelper(argv[0], argv, envp, UMH_WAIT_EXEC);
|
|
||||||
|
|
||||||
if (notify)
|
if (notify)
|
||||||
netdev_notify_peers(net);
|
netdev_notify_peers(net);
|
||||||
|
|
||||||
|
/* link_watch only sends one notification with current state per
|
||||||
|
* second, handle next reconfig event in 2 seconds.
|
||||||
|
*/
|
||||||
|
if (reschedule)
|
||||||
|
schedule_delayed_work(&ndev_ctx->dwork, LINKCHANGE_INT);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void netvsc_free_netdev(struct net_device *netdev)
|
static void netvsc_free_netdev(struct net_device *netdev)
|
||||||
@@ -1106,6 +1153,9 @@ static int netvsc_probe(struct hv_device *dev,
|
|||||||
INIT_DELAYED_WORK(&net_device_ctx->dwork, netvsc_link_change);
|
INIT_DELAYED_WORK(&net_device_ctx->dwork, netvsc_link_change);
|
||||||
INIT_WORK(&net_device_ctx->work, do_set_multicast);
|
INIT_WORK(&net_device_ctx->work, do_set_multicast);
|
||||||
|
|
||||||
|
spin_lock_init(&net_device_ctx->lock);
|
||||||
|
INIT_LIST_HEAD(&net_device_ctx->reconfig_events);
|
||||||
|
|
||||||
net->netdev_ops = &device_ops;
|
net->netdev_ops = &device_ops;
|
||||||
|
|
||||||
net->hw_features = NETIF_F_RXCSUM | NETIF_F_SG | NETIF_F_IP_CSUM |
|
net->hw_features = NETIF_F_RXCSUM | NETIF_F_SG | NETIF_F_IP_CSUM |
|
||||||
@@ -1145,8 +1195,6 @@ static int netvsc_probe(struct hv_device *dev,
|
|||||||
pr_err("Unable to register netdev.\n");
|
pr_err("Unable to register netdev.\n");
|
||||||
rndis_filter_device_remove(dev);
|
rndis_filter_device_remove(dev);
|
||||||
netvsc_free_netdev(net);
|
netvsc_free_netdev(net);
|
||||||
} else {
|
|
||||||
schedule_delayed_work(&net_device_ctx->dwork, 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return ret;
|
return ret;
|
||||||
|
|||||||
Reference in New Issue
Block a user