Skip to content

Commit ac82fc8

Browse files
dcuiLorenzo Pieralisi
authored andcommitted
PCI: hv: Add hibernation support
Add suspend() and resume() functions so that Hyper-V virtual PCI devices are handled properly when the VM hibernates and resumes from hibernation. Note that the suspend() function must make sure there are no pending work items before calling vmbus_close(), since it runs in a process context as a callback in dpm_suspend(). When it starts to run, the channel callback hv_pci_onchannelcallback(), which runs in a tasklet context, can be still running concurrently and scheduling new work items onto hbus->wq in hv_pci_devices_present() and hv_pci_eject_device(), and the work item handlers can access the vmbus channel, which can be being closed by hv_pci_suspend(), e.g. the work item handler pci_devices_present_work() -> new_pcichild_device() writes to the vmbus channel. To eliminate the race, hv_pci_suspend() disables the channel callback tasklet, sets hbus->state to hv_pcibus_removing, and re-enables the tasklet. This way, when hv_pci_suspend() proceeds, it knows that no new work item can be scheduled, and then it flushes hbus->wq and safely closes the vmbus channel. Signed-off-by: Dexuan Cui <[email protected]> Signed-off-by: Lorenzo Pieralisi <[email protected]> Reviewed-by: Michael Kelley <[email protected]>
1 parent a8e3750 commit ac82fc8

File tree

1 file changed

+123
-2
lines changed

1 file changed

+123
-2
lines changed

drivers/pci/controller/pci-hyperv.c

Lines changed: 123 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -455,6 +455,7 @@ enum hv_pcibus_state {
455455
hv_pcibus_init = 0,
456456
hv_pcibus_probed,
457457
hv_pcibus_installed,
458+
hv_pcibus_removing,
458459
hv_pcibus_removed,
459460
hv_pcibus_maximum
460461
};
@@ -1681,6 +1682,23 @@ static void prepopulate_bars(struct hv_pcibus_device *hbus)
16811682

16821683
spin_lock_irqsave(&hbus->device_list_lock, flags);
16831684

1685+
/*
1686+
* Clear the memory enable bit, in case it's already set. This occurs
1687+
* in the suspend path of hibernation, where the device is suspended,
1688+
* resumed and suspended again: see hibernation_snapshot() and
1689+
* hibernation_platform_enter().
1690+
*
1691+
* If the memory enable bit is already set, Hyper-V sliently ignores
1692+
* the below BAR updates, and the related PCI device driver can not
1693+
* work, because reading from the device register(s) always returns
1694+
* 0xFFFFFFFF.
1695+
*/
1696+
list_for_each_entry(hpdev, &hbus->children, list_entry) {
1697+
_hv_pcifront_read_config(hpdev, PCI_COMMAND, 2, &command);
1698+
command &= ~PCI_COMMAND_MEMORY;
1699+
_hv_pcifront_write_config(hpdev, PCI_COMMAND, 2, command);
1700+
}
1701+
16841702
/* Pick addresses for the BARs. */
16851703
do {
16861704
list_for_each_entry(hpdev, &hbus->children, list_entry) {
@@ -2107,6 +2125,12 @@ static void hv_pci_devices_present(struct hv_pcibus_device *hbus,
21072125
unsigned long flags;
21082126
bool pending_dr;
21092127

2128+
if (hbus->state == hv_pcibus_removing) {
2129+
dev_info(&hbus->hdev->device,
2130+
"PCI VMBus BUS_RELATIONS: ignored\n");
2131+
return;
2132+
}
2133+
21102134
dr_wrk = kzalloc(sizeof(*dr_wrk), GFP_NOWAIT);
21112135
if (!dr_wrk)
21122136
return;
@@ -2223,11 +2247,19 @@ static void hv_eject_device_work(struct work_struct *work)
22232247
*/
22242248
static void hv_pci_eject_device(struct hv_pci_dev *hpdev)
22252249
{
2250+
struct hv_pcibus_device *hbus = hpdev->hbus;
2251+
struct hv_device *hdev = hbus->hdev;
2252+
2253+
if (hbus->state == hv_pcibus_removing) {
2254+
dev_info(&hdev->device, "PCI VMBus EJECT: ignored\n");
2255+
return;
2256+
}
2257+
22262258
hpdev->state = hv_pcichild_ejecting;
22272259
get_pcichild(hpdev);
22282260
INIT_WORK(&hpdev->wrk, hv_eject_device_work);
2229-
get_hvpcibus(hpdev->hbus);
2230-
queue_work(hpdev->hbus->wq, &hpdev->wrk);
2261+
get_hvpcibus(hbus);
2262+
queue_work(hbus->wq, &hpdev->wrk);
22312263
}
22322264

22332265
/**
@@ -3107,6 +3139,93 @@ static int hv_pci_remove(struct hv_device *hdev)
31073139
return ret;
31083140
}
31093141

3142+
static int hv_pci_suspend(struct hv_device *hdev)
3143+
{
3144+
struct hv_pcibus_device *hbus = hv_get_drvdata(hdev);
3145+
enum hv_pcibus_state old_state;
3146+
int ret;
3147+
3148+
/*
3149+
* hv_pci_suspend() must make sure there are no pending work items
3150+
* before calling vmbus_close(), since it runs in a process context
3151+
* as a callback in dpm_suspend(). When it starts to run, the channel
3152+
* callback hv_pci_onchannelcallback(), which runs in a tasklet
3153+
* context, can be still running concurrently and scheduling new work
3154+
* items onto hbus->wq in hv_pci_devices_present() and
3155+
* hv_pci_eject_device(), and the work item handlers can access the
3156+
* vmbus channel, which can be being closed by hv_pci_suspend(), e.g.
3157+
* the work item handler pci_devices_present_work() ->
3158+
* new_pcichild_device() writes to the vmbus channel.
3159+
*
3160+
* To eliminate the race, hv_pci_suspend() disables the channel
3161+
* callback tasklet, sets hbus->state to hv_pcibus_removing, and
3162+
* re-enables the tasklet. This way, when hv_pci_suspend() proceeds,
3163+
* it knows that no new work item can be scheduled, and then it flushes
3164+
* hbus->wq and safely closes the vmbus channel.
3165+
*/
3166+
tasklet_disable(&hdev->channel->callback_event);
3167+
3168+
/* Change the hbus state to prevent new work items. */
3169+
old_state = hbus->state;
3170+
if (hbus->state == hv_pcibus_installed)
3171+
hbus->state = hv_pcibus_removing;
3172+
3173+
tasklet_enable(&hdev->channel->callback_event);
3174+
3175+
if (old_state != hv_pcibus_installed)
3176+
return -EINVAL;
3177+
3178+
flush_workqueue(hbus->wq);
3179+
3180+
ret = hv_pci_bus_exit(hdev, true);
3181+
if (ret)
3182+
return ret;
3183+
3184+
vmbus_close(hdev->channel);
3185+
3186+
return 0;
3187+
}
3188+
3189+
static int hv_pci_resume(struct hv_device *hdev)
3190+
{
3191+
struct hv_pcibus_device *hbus = hv_get_drvdata(hdev);
3192+
enum pci_protocol_version_t version[1];
3193+
int ret;
3194+
3195+
hbus->state = hv_pcibus_init;
3196+
3197+
ret = vmbus_open(hdev->channel, pci_ring_size, pci_ring_size, NULL, 0,
3198+
hv_pci_onchannelcallback, hbus);
3199+
if (ret)
3200+
return ret;
3201+
3202+
/* Only use the version that was in use before hibernation. */
3203+
version[0] = pci_protocol_version;
3204+
ret = hv_pci_protocol_negotiation(hdev, version, 1);
3205+
if (ret)
3206+
goto out;
3207+
3208+
ret = hv_pci_query_relations(hdev);
3209+
if (ret)
3210+
goto out;
3211+
3212+
ret = hv_pci_enter_d0(hdev);
3213+
if (ret)
3214+
goto out;
3215+
3216+
ret = hv_send_resources_allocated(hdev);
3217+
if (ret)
3218+
goto out;
3219+
3220+
prepopulate_bars(hbus);
3221+
3222+
hbus->state = hv_pcibus_installed;
3223+
return 0;
3224+
out:
3225+
vmbus_close(hdev->channel);
3226+
return ret;
3227+
}
3228+
31103229
static const struct hv_vmbus_device_id hv_pci_id_table[] = {
31113230
/* PCI Pass-through Class ID */
31123231
/* 44C4F61D-4444-4400-9D52-802E27EDE19F */
@@ -3121,6 +3240,8 @@ static struct hv_driver hv_pci_drv = {
31213240
.id_table = hv_pci_id_table,
31223241
.probe = hv_pci_probe,
31233242
.remove = hv_pci_remove,
3243+
.suspend = hv_pci_suspend,
3244+
.resume = hv_pci_resume,
31243245
};
31253246

31263247
static void __exit exit_hv_pci_drv(void)

0 commit comments

Comments
 (0)