| /* |
| * Copyright (c) 2009, Microsoft Corporation. |
| * |
| * This program is free software; you can redistribute it and/or modify it |
| * under the terms and conditions of the GNU General Public License, |
| * version 2, as published by the Free Software Foundation. |
| * |
| * This program is distributed in the hope it will be useful, but WITHOUT |
| * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or |
| * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for |
| * more details. |
| * |
| * You should have received a copy of the GNU General Public License along with |
| * this program; if not, write to the Free Software Foundation, Inc., 59 Temple |
| * Place - Suite 330, Boston, MA 02111-1307 USA. |
| * |
| * Authors: |
| * Haiyang Zhang <haiyangz@microsoft.com> |
| * Hank Janssen <hjanssen@microsoft.com> |
| * K. Y. Srinivasan <kys@microsoft.com> |
| * |
| */ |
| #define pr_fmt(fmt) KBUILD_MODNAME ": " fmt |
| |
| #include <linux/init.h> |
| #include <linux/module.h> |
| #include <linux/device.h> |
| #include <linux/interrupt.h> |
| #include <linux/sysctl.h> |
| #include <linux/slab.h> |
| #include <linux/acpi.h> |
| #include <linux/completion.h> |
| #include <linux/hyperv.h> |
| #include <linux/kernel_stat.h> |
| #include <linux/clockchips.h> |
| #include <linux/cpu.h> |
| #include <asm/hyperv.h> |
| #include <asm/hypervisor.h> |
| #include <asm/mshyperv.h> |
| #include <linux/notifier.h> |
| #include <linux/ptrace.h> |
| #include <linux/screen_info.h> |
| #include <linux/kdebug.h> |
| #include <linux/efi.h> |
| #include <linux/random.h> |
| #include "hyperv_vmbus.h" |
| |
| static struct acpi_device *hv_acpi_dev; |
| |
| static struct completion probe_event; |
| |
| |
| static void hyperv_report_panic(struct pt_regs *regs) |
| { |
| static bool panic_reported; |
| |
| /* |
| * We prefer to report panic on 'die' chain as we have proper |
| * registers to report, but if we miss it (e.g. on BUG()) we need |
| * to report it on 'panic'. |
| */ |
| if (panic_reported) |
| return; |
| panic_reported = true; |
| |
| wrmsrl(HV_X64_MSR_CRASH_P0, regs->ip); |
| wrmsrl(HV_X64_MSR_CRASH_P1, regs->ax); |
| wrmsrl(HV_X64_MSR_CRASH_P2, regs->bx); |
| wrmsrl(HV_X64_MSR_CRASH_P3, regs->cx); |
| wrmsrl(HV_X64_MSR_CRASH_P4, regs->dx); |
| |
| /* |
| * Let Hyper-V know there is crash data available |
| */ |
| wrmsrl(HV_X64_MSR_CRASH_CTL, HV_CRASH_CTL_CRASH_NOTIFY); |
| } |
| |
| static int hyperv_panic_event(struct notifier_block *nb, unsigned long val, |
| void *args) |
| { |
| struct pt_regs *regs; |
| |
| regs = current_pt_regs(); |
| |
| hyperv_report_panic(regs); |
| return NOTIFY_DONE; |
| } |
| |
| static int hyperv_die_event(struct notifier_block *nb, unsigned long val, |
| void *args) |
| { |
| struct die_args *die = (struct die_args *)args; |
| struct pt_regs *regs = die->regs; |
| |
| hyperv_report_panic(regs); |
| return NOTIFY_DONE; |
| } |
| |
| static struct notifier_block hyperv_die_block = { |
| .notifier_call = hyperv_die_event, |
| }; |
| static struct notifier_block hyperv_panic_block = { |
| .notifier_call = hyperv_panic_event, |
| }; |
| |
| static const char *fb_mmio_name = "fb_range"; |
| static struct resource *fb_mmio; |
| static struct resource *hyperv_mmio; |
| static DEFINE_SEMAPHORE(hyperv_mmio_lock); |
| |
| static int vmbus_exists(void) |
| { |
| if (hv_acpi_dev == NULL) |
| return -ENODEV; |
| |
| return 0; |
| } |
| |
| #define VMBUS_ALIAS_LEN ((sizeof((struct hv_vmbus_device_id *)0)->guid) * 2) |
| static void print_alias_name(struct hv_device *hv_dev, char *alias_name) |
| { |
| int i; |
| for (i = 0; i < VMBUS_ALIAS_LEN; i += 2) |
| sprintf(&alias_name[i], "%02x", hv_dev->dev_type.b[i/2]); |
| } |
| |
| static u8 channel_monitor_group(struct vmbus_channel *channel) |
| { |
| return (u8)channel->offermsg.monitorid / 32; |
| } |
| |
| static u8 channel_monitor_offset(struct vmbus_channel *channel) |
| { |
| return (u8)channel->offermsg.monitorid % 32; |
| } |
| |
| static u32 channel_pending(struct vmbus_channel *channel, |
| struct hv_monitor_page *monitor_page) |
| { |
| u8 monitor_group = channel_monitor_group(channel); |
| return monitor_page->trigger_group[monitor_group].pending; |
| } |
| |
| static u32 channel_latency(struct vmbus_channel *channel, |
| struct hv_monitor_page *monitor_page) |
| { |
| u8 monitor_group = channel_monitor_group(channel); |
| u8 monitor_offset = channel_monitor_offset(channel); |
| return monitor_page->latency[monitor_group][monitor_offset]; |
| } |
| |
| static u32 channel_conn_id(struct vmbus_channel *channel, |
| struct hv_monitor_page *monitor_page) |
| { |
| u8 monitor_group = channel_monitor_group(channel); |
| u8 monitor_offset = channel_monitor_offset(channel); |
| return monitor_page->parameter[monitor_group][monitor_offset].connectionid.u.id; |
| } |
| |
| static ssize_t id_show(struct device *dev, struct device_attribute *dev_attr, |
| char *buf) |
| { |
| struct hv_device *hv_dev = device_to_hv_device(dev); |
| |
| if (!hv_dev->channel) |
| return -ENODEV; |
| return sprintf(buf, "%d\n", hv_dev->channel->offermsg.child_relid); |
| } |
| static DEVICE_ATTR_RO(id); |
| |
| static ssize_t state_show(struct device *dev, struct device_attribute *dev_attr, |
| char *buf) |
| { |
| struct hv_device *hv_dev = device_to_hv_device(dev); |
| |
| if (!hv_dev->channel) |
| return -ENODEV; |
| return sprintf(buf, "%d\n", hv_dev->channel->state); |
| } |
| static DEVICE_ATTR_RO(state); |
| |
| static ssize_t monitor_id_show(struct device *dev, |
| struct device_attribute *dev_attr, char *buf) |
| { |
| struct hv_device *hv_dev = device_to_hv_device(dev); |
| |
| if (!hv_dev->channel) |
| return -ENODEV; |
| return sprintf(buf, "%d\n", hv_dev->channel->offermsg.monitorid); |
| } |
| static DEVICE_ATTR_RO(monitor_id); |
| |
| static ssize_t class_id_show(struct device *dev, |
| struct device_attribute *dev_attr, char *buf) |
| { |
| struct hv_device *hv_dev = device_to_hv_device(dev); |
| |
| if (!hv_dev->channel) |
| return -ENODEV; |
| return sprintf(buf, "{%pUl}\n", |
| hv_dev->channel->offermsg.offer.if_type.b); |
| } |
| static DEVICE_ATTR_RO(class_id); |
| |
| static ssize_t device_id_show(struct device *dev, |
| struct device_attribute *dev_attr, char *buf) |
| { |
| struct hv_device *hv_dev = device_to_hv_device(dev); |
| |
| if (!hv_dev->channel) |
| return -ENODEV; |
| return sprintf(buf, "{%pUl}\n", |
| hv_dev->channel->offermsg.offer.if_instance.b); |
| } |
| static DEVICE_ATTR_RO(device_id); |
| |
| static ssize_t modalias_show(struct device *dev, |
| struct device_attribute *dev_attr, char *buf) |
| { |
| struct hv_device *hv_dev = device_to_hv_device(dev); |
| char alias_name[VMBUS_ALIAS_LEN + 1]; |
| |
| print_alias_name(hv_dev, alias_name); |
| return sprintf(buf, "vmbus:%s\n", alias_name); |
| } |
| static DEVICE_ATTR_RO(modalias); |
| |
| static ssize_t server_monitor_pending_show(struct device *dev, |
| struct device_attribute *dev_attr, |
| char *buf) |
| { |
| struct hv_device *hv_dev = device_to_hv_device(dev); |
| |
| if (!hv_dev->channel) |
| return -ENODEV; |
| return sprintf(buf, "%d\n", |
| channel_pending(hv_dev->channel, |
| vmbus_connection.monitor_pages[1])); |
| } |
| static DEVICE_ATTR_RO(server_monitor_pending); |
| |
| static ssize_t client_monitor_pending_show(struct device *dev, |
| struct device_attribute *dev_attr, |
| char *buf) |
| { |
| struct hv_device *hv_dev = device_to_hv_device(dev); |
| |
| if (!hv_dev->channel) |
| return -ENODEV; |
| return sprintf(buf, "%d\n", |
| channel_pending(hv_dev->channel, |
| vmbus_connection.monitor_pages[1])); |
| } |
| static DEVICE_ATTR_RO(client_monitor_pending); |
| |
| static ssize_t server_monitor_latency_show(struct device *dev, |
| struct device_attribute *dev_attr, |
| char *buf) |
| { |
| struct hv_device *hv_dev = device_to_hv_device(dev); |
| |
| if (!hv_dev->channel) |
| return -ENODEV; |
| return sprintf(buf, "%d\n", |
| channel_latency(hv_dev->channel, |
| vmbus_connection.monitor_pages[0])); |
| } |
| static DEVICE_ATTR_RO(server_monitor_latency); |
| |
| static ssize_t client_monitor_latency_show(struct device *dev, |
| struct device_attribute *dev_attr, |
| char *buf) |
| { |
| struct hv_device *hv_dev = device_to_hv_device(dev); |
| |
| if (!hv_dev->channel) |
| return -ENODEV; |
| return sprintf(buf, "%d\n", |
| channel_latency(hv_dev->channel, |
| vmbus_connection.monitor_pages[1])); |
| } |
| static DEVICE_ATTR_RO(client_monitor_latency); |
| |
| static ssize_t server_monitor_conn_id_show(struct device *dev, |
| struct device_attribute *dev_attr, |
| char *buf) |
| { |
| struct hv_device *hv_dev = device_to_hv_device(dev); |
| |
| if (!hv_dev->channel) |
| return -ENODEV; |
| return sprintf(buf, "%d\n", |
| channel_conn_id(hv_dev->channel, |
| vmbus_connection.monitor_pages[0])); |
| } |
| static DEVICE_ATTR_RO(server_monitor_conn_id); |
| |
| static ssize_t client_monitor_conn_id_show(struct device *dev, |
| struct device_attribute *dev_attr, |
| char *buf) |
| { |
| struct hv_device *hv_dev = device_to_hv_device(dev); |
| |
| if (!hv_dev->channel) |
| return -ENODEV; |
| return sprintf(buf, "%d\n", |
| channel_conn_id(hv_dev->channel, |
| vmbus_connection.monitor_pages[1])); |
| } |
| static DEVICE_ATTR_RO(client_monitor_conn_id); |
| |
| static ssize_t out_intr_mask_show(struct device *dev, |
| struct device_attribute *dev_attr, char *buf) |
| { |
| struct hv_device *hv_dev = device_to_hv_device(dev); |
| struct hv_ring_buffer_debug_info outbound; |
| |
| if (!hv_dev->channel) |
| return -ENODEV; |
| if (hv_dev->channel->state != CHANNEL_OPENED_STATE) |
| return -EINVAL; |
| hv_ringbuffer_get_debuginfo(&hv_dev->channel->outbound, &outbound); |
| return sprintf(buf, "%d\n", outbound.current_interrupt_mask); |
| } |
| static DEVICE_ATTR_RO(out_intr_mask); |
| |
| static ssize_t out_read_index_show(struct device *dev, |
| struct device_attribute *dev_attr, char *buf) |
| { |
| struct hv_device *hv_dev = device_to_hv_device(dev); |
| struct hv_ring_buffer_debug_info outbound; |
| |
| if (!hv_dev->channel) |
| return -ENODEV; |
| if (hv_dev->channel->state != CHANNEL_OPENED_STATE) |
| return -EINVAL; |
| hv_ringbuffer_get_debuginfo(&hv_dev->channel->outbound, &outbound); |
| return sprintf(buf, "%d\n", outbound.current_read_index); |
| } |
| static DEVICE_ATTR_RO(out_read_index); |
| |
| static ssize_t out_write_index_show(struct device *dev, |
| struct device_attribute *dev_attr, |
| char *buf) |
| { |
| struct hv_device *hv_dev = device_to_hv_device(dev); |
| struct hv_ring_buffer_debug_info outbound; |
| |
| if (!hv_dev->channel) |
| return -ENODEV; |
| if (hv_dev->channel->state != CHANNEL_OPENED_STATE) |
| return -EINVAL; |
| hv_ringbuffer_get_debuginfo(&hv_dev->channel->outbound, &outbound); |
| return sprintf(buf, "%d\n", outbound.current_write_index); |
| } |
| static DEVICE_ATTR_RO(out_write_index); |
| |
| static ssize_t out_read_bytes_avail_show(struct device *dev, |
| struct device_attribute *dev_attr, |
| char *buf) |
| { |
| struct hv_device *hv_dev = device_to_hv_device(dev); |
| struct hv_ring_buffer_debug_info outbound; |
| |
| if (!hv_dev->channel) |
| return -ENODEV; |
| if (hv_dev->channel->state != CHANNEL_OPENED_STATE) |
| return -EINVAL; |
| hv_ringbuffer_get_debuginfo(&hv_dev->channel->outbound, &outbound); |
| return sprintf(buf, "%d\n", outbound.bytes_avail_toread); |
| } |
| static DEVICE_ATTR_RO(out_read_bytes_avail); |
| |
| static ssize_t out_write_bytes_avail_show(struct device *dev, |
| struct device_attribute *dev_attr, |
| char *buf) |
| { |
| struct hv_device *hv_dev = device_to_hv_device(dev); |
| struct hv_ring_buffer_debug_info outbound; |
| |
| if (!hv_dev->channel) |
| return -ENODEV; |
| if (hv_dev->channel->state != CHANNEL_OPENED_STATE) |
| return -EINVAL; |
| hv_ringbuffer_get_debuginfo(&hv_dev->channel->outbound, &outbound); |
| return sprintf(buf, "%d\n", outbound.bytes_avail_towrite); |
| } |
| static DEVICE_ATTR_RO(out_write_bytes_avail); |
| |
| static ssize_t in_intr_mask_show(struct device *dev, |
| struct device_attribute *dev_attr, char *buf) |
| { |
| struct hv_device *hv_dev = device_to_hv_device(dev); |
| struct hv_ring_buffer_debug_info inbound; |
| |
| if (!hv_dev->channel) |
| return -ENODEV; |
| if (hv_dev->channel->state != CHANNEL_OPENED_STATE) |
| return -EINVAL; |
| hv_ringbuffer_get_debuginfo(&hv_dev->channel->inbound, &inbound); |
| return sprintf(buf, "%d\n", inbound.current_interrupt_mask); |
| } |
| static DEVICE_ATTR_RO(in_intr_mask); |
| |
| static ssize_t in_read_index_show(struct device *dev, |
| struct device_attribute *dev_attr, char *buf) |
| { |
| struct hv_device *hv_dev = device_to_hv_device(dev); |
| struct hv_ring_buffer_debug_info inbound; |
| |
| if (!hv_dev->channel) |
| return -ENODEV; |
| if (hv_dev->channel->state != CHANNEL_OPENED_STATE) |
| return -EINVAL; |
| hv_ringbuffer_get_debuginfo(&hv_dev->channel->inbound, &inbound); |
| return sprintf(buf, "%d\n", inbound.current_read_index); |
| } |
| static DEVICE_ATTR_RO(in_read_index); |
| |
| static ssize_t in_write_index_show(struct device *dev, |
| struct device_attribute *dev_attr, char *buf) |
| { |
| struct hv_device *hv_dev = device_to_hv_device(dev); |
| struct hv_ring_buffer_debug_info inbound; |
| |
| if (!hv_dev->channel) |
| return -ENODEV; |
| if (hv_dev->channel->state != CHANNEL_OPENED_STATE) |
| return -EINVAL; |
| hv_ringbuffer_get_debuginfo(&hv_dev->channel->inbound, &inbound); |
| return sprintf(buf, "%d\n", inbound.current_write_index); |
| } |
| static DEVICE_ATTR_RO(in_write_index); |
| |
| static ssize_t in_read_bytes_avail_show(struct device *dev, |
| struct device_attribute *dev_attr, |
| char *buf) |
| { |
| struct hv_device *hv_dev = device_to_hv_device(dev); |
| struct hv_ring_buffer_debug_info inbound; |
| |
| if (!hv_dev->channel) |
| return -ENODEV; |
| if (hv_dev->channel->state != CHANNEL_OPENED_STATE) |
| return -EINVAL; |
| hv_ringbuffer_get_debuginfo(&hv_dev->channel->inbound, &inbound); |
| return sprintf(buf, "%d\n", inbound.bytes_avail_toread); |
| } |
| static DEVICE_ATTR_RO(in_read_bytes_avail); |
| |
| static ssize_t in_write_bytes_avail_show(struct device *dev, |
| struct device_attribute *dev_attr, |
| char *buf) |
| { |
| struct hv_device *hv_dev = device_to_hv_device(dev); |
| struct hv_ring_buffer_debug_info inbound; |
| |
| if (!hv_dev->channel) |
| return -ENODEV; |
| if (hv_dev->channel->state != CHANNEL_OPENED_STATE) |
| return -EINVAL; |
| hv_ringbuffer_get_debuginfo(&hv_dev->channel->inbound, &inbound); |
| return sprintf(buf, "%d\n", inbound.bytes_avail_towrite); |
| } |
| static DEVICE_ATTR_RO(in_write_bytes_avail); |
| |
| static ssize_t channel_vp_mapping_show(struct device *dev, |
| struct device_attribute *dev_attr, |
| char *buf) |
| { |
| struct hv_device *hv_dev = device_to_hv_device(dev); |
| struct vmbus_channel *channel = hv_dev->channel, *cur_sc; |
| unsigned long flags; |
| int buf_size = PAGE_SIZE, n_written, tot_written; |
| struct list_head *cur; |
| |
| if (!channel) |
| return -ENODEV; |
| |
| tot_written = snprintf(buf, buf_size, "%u:%u\n", |
| channel->offermsg.child_relid, channel->target_cpu); |
| |
| spin_lock_irqsave(&channel->lock, flags); |
| |
| list_for_each(cur, &channel->sc_list) { |
| if (tot_written >= buf_size - 1) |
| break; |
| |
| cur_sc = list_entry(cur, struct vmbus_channel, sc_list); |
| n_written = scnprintf(buf + tot_written, |
| buf_size - tot_written, |
| "%u:%u\n", |
| cur_sc->offermsg.child_relid, |
| cur_sc->target_cpu); |
| tot_written += n_written; |
| } |
| |
| spin_unlock_irqrestore(&channel->lock, flags); |
| |
| return tot_written; |
| } |
| static DEVICE_ATTR_RO(channel_vp_mapping); |
| |
| static ssize_t vendor_show(struct device *dev, |
| struct device_attribute *dev_attr, |
| char *buf) |
| { |
| struct hv_device *hv_dev = device_to_hv_device(dev); |
| return sprintf(buf, "0x%x\n", hv_dev->vendor_id); |
| } |
| static DEVICE_ATTR_RO(vendor); |
| |
| static ssize_t device_show(struct device *dev, |
| struct device_attribute *dev_attr, |
| char *buf) |
| { |
| struct hv_device *hv_dev = device_to_hv_device(dev); |
| return sprintf(buf, "0x%x\n", hv_dev->device_id); |
| } |
| static DEVICE_ATTR_RO(device); |
| |
| /* Set up per device attributes in /sys/bus/vmbus/devices/<bus device> */ |
| static struct attribute *vmbus_attrs[] = { |
| &dev_attr_id.attr, |
| &dev_attr_state.attr, |
| &dev_attr_monitor_id.attr, |
| &dev_attr_class_id.attr, |
| &dev_attr_device_id.attr, |
| &dev_attr_modalias.attr, |
| &dev_attr_server_monitor_pending.attr, |
| &dev_attr_client_monitor_pending.attr, |
| &dev_attr_server_monitor_latency.attr, |
| &dev_attr_client_monitor_latency.attr, |
| &dev_attr_server_monitor_conn_id.attr, |
| &dev_attr_client_monitor_conn_id.attr, |
| &dev_attr_out_intr_mask.attr, |
| &dev_attr_out_read_index.attr, |
| &dev_attr_out_write_index.attr, |
| &dev_attr_out_read_bytes_avail.attr, |
| &dev_attr_out_write_bytes_avail.attr, |
| &dev_attr_in_intr_mask.attr, |
| &dev_attr_in_read_index.attr, |
| &dev_attr_in_write_index.attr, |
| &dev_attr_in_read_bytes_avail.attr, |
| &dev_attr_in_write_bytes_avail.attr, |
| &dev_attr_channel_vp_mapping.attr, |
| &dev_attr_vendor.attr, |
| &dev_attr_device.attr, |
| NULL, |
| }; |
| ATTRIBUTE_GROUPS(vmbus); |
| |
| /* |
| * vmbus_uevent - add uevent for our device |
| * |
| * This routine is invoked when a device is added or removed on the vmbus to |
| * generate a uevent to udev in the userspace. The udev will then look at its |
| * rule and the uevent generated here to load the appropriate driver |
| * |
| * The alias string will be of the form vmbus:guid where guid is the string |
| * representation of the device guid (each byte of the guid will be |
| * represented with two hex characters. |
| */ |
| static int vmbus_uevent(struct device *device, struct kobj_uevent_env *env) |
| { |
| struct hv_device *dev = device_to_hv_device(device); |
| int ret; |
| char alias_name[VMBUS_ALIAS_LEN + 1]; |
| |
| print_alias_name(dev, alias_name); |
| ret = add_uevent_var(env, "MODALIAS=vmbus:%s", alias_name); |
| return ret; |
| } |
| |
| static const uuid_le null_guid; |
| |
| static inline bool is_null_guid(const uuid_le *guid) |
| { |
| if (uuid_le_cmp(*guid, null_guid)) |
| return false; |
| return true; |
| } |
| |
| /* |
| * Return a matching hv_vmbus_device_id pointer. |
| * If there is no match, return NULL. |
| */ |
| static const struct hv_vmbus_device_id *hv_vmbus_get_id( |
| const struct hv_vmbus_device_id *id, |
| const uuid_le *guid) |
| { |
| for (; !is_null_guid(&id->guid); id++) |
| if (!uuid_le_cmp(id->guid, *guid)) |
| return id; |
| |
| return NULL; |
| } |
| |
| |
| |
| /* |
| * vmbus_match - Attempt to match the specified device to the specified driver |
| */ |
| static int vmbus_match(struct device *device, struct device_driver *driver) |
| { |
| struct hv_driver *drv = drv_to_hv_drv(driver); |
| struct hv_device *hv_dev = device_to_hv_device(device); |
| |
| /* The hv_sock driver handles all hv_sock offers. */ |
| if (is_hvsock_channel(hv_dev->channel)) |
| return drv->hvsock; |
| |
| if (hv_vmbus_get_id(drv->id_table, &hv_dev->dev_type)) |
| return 1; |
| |
| return 0; |
| } |
| |
| /* |
| * vmbus_probe - Add the new vmbus's child device |
| */ |
| static int vmbus_probe(struct device *child_device) |
| { |
| int ret = 0; |
| struct hv_driver *drv = |
| drv_to_hv_drv(child_device->driver); |
| struct hv_device *dev = device_to_hv_device(child_device); |
| const struct hv_vmbus_device_id *dev_id; |
| |
| dev_id = hv_vmbus_get_id(drv->id_table, &dev->dev_type); |
| if (drv->probe) { |
| ret = drv->probe(dev, dev_id); |
| if (ret != 0) |
| pr_err("probe failed for device %s (%d)\n", |
| dev_name(child_device), ret); |
| |
| } else { |
| pr_err("probe not set for driver %s\n", |
| dev_name(child_device)); |
| ret = -ENODEV; |
| } |
| return ret; |
| } |
| |
| /* |
| * vmbus_remove - Remove a vmbus device |
| */ |
| static int vmbus_remove(struct device *child_device) |
| { |
| struct hv_driver *drv; |
| struct hv_device *dev = device_to_hv_device(child_device); |
| |
| if (child_device->driver) { |
| drv = drv_to_hv_drv(child_device->driver); |
| if (drv->remove) |
| drv->remove(dev); |
| } |
| |
| return 0; |
| } |
| |
| |
| /* |
| * vmbus_shutdown - Shutdown a vmbus device |
| */ |
| static void vmbus_shutdown(struct device *child_device) |
| { |
| struct hv_driver *drv; |
| struct hv_device *dev = device_to_hv_device(child_device); |
| |
| |
| /* The device may not be attached yet */ |
| if (!child_device->driver) |
| return; |
| |
| drv = drv_to_hv_drv(child_device->driver); |
| |
| if (drv->shutdown) |
| drv->shutdown(dev); |
| |
| return; |
| } |
| |
| |
| /* |
| * vmbus_device_release - Final callback release of the vmbus child device |
| */ |
| static void vmbus_device_release(struct device *device) |
| { |
| struct hv_device *hv_dev = device_to_hv_device(device); |
| struct vmbus_channel *channel = hv_dev->channel; |
| |
| hv_process_channel_removal(channel, |
| channel->offermsg.child_relid); |
| kfree(hv_dev); |
| |
| } |
| |
| /* The one and only one */ |
| static struct bus_type hv_bus = { |
| .name = "vmbus", |
| .match = vmbus_match, |
| .shutdown = vmbus_shutdown, |
| .remove = vmbus_remove, |
| .probe = vmbus_probe, |
| .uevent = vmbus_uevent, |
| .dev_groups = vmbus_groups, |
| }; |
| |
| struct onmessage_work_context { |
| struct work_struct work; |
| struct hv_message msg; |
| }; |
| |
| static void vmbus_onmessage_work(struct work_struct *work) |
| { |
| struct onmessage_work_context *ctx; |
| |
| /* Do not process messages if we're in DISCONNECTED state */ |
| if (vmbus_connection.conn_state == DISCONNECTED) |
| return; |
| |
| ctx = container_of(work, struct onmessage_work_context, |
| work); |
| vmbus_onmessage(&ctx->msg); |
| kfree(ctx); |
| } |
| |
| static void hv_process_timer_expiration(struct hv_message *msg, int cpu) |
| { |
| struct clock_event_device *dev = hv_context.clk_evt[cpu]; |
| |
| if (dev->event_handler) |
| dev->event_handler(dev); |
| |
| vmbus_signal_eom(msg, HVMSG_TIMER_EXPIRED); |
| } |
| |
| void vmbus_on_msg_dpc(unsigned long data) |
| { |
| int cpu = smp_processor_id(); |
| void *page_addr = hv_context.synic_message_page[cpu]; |
| struct hv_message *msg = (struct hv_message *)page_addr + |
| VMBUS_MESSAGE_SINT; |
| struct vmbus_channel_message_header *hdr; |
| struct vmbus_channel_message_table_entry *entry; |
| struct onmessage_work_context *ctx; |
| u32 message_type = msg->header.message_type; |
| |
| if (message_type == HVMSG_NONE) |
| /* no msg */ |
| return; |
| |
| hdr = (struct vmbus_channel_message_header *)msg->u.payload; |
| |
| if (hdr->msgtype >= CHANNELMSG_COUNT) { |
| WARN_ONCE(1, "unknown msgtype=%d\n", hdr->msgtype); |
| goto msg_handled; |
| } |
| |
| entry = &channel_message_table[hdr->msgtype]; |
| if (entry->handler_type == VMHT_BLOCKING) { |
| ctx = kmalloc(sizeof(*ctx), GFP_ATOMIC); |
| if (ctx == NULL) |
| return; |
| |
| INIT_WORK(&ctx->work, vmbus_onmessage_work); |
| memcpy(&ctx->msg, msg, sizeof(*msg)); |
| |
| queue_work(vmbus_connection.work_queue, &ctx->work); |
| } else |
| entry->message_handler(hdr); |
| |
| msg_handled: |
| vmbus_signal_eom(msg, message_type); |
| } |
| |
| static void vmbus_isr(void) |
| { |
| int cpu = smp_processor_id(); |
| void *page_addr; |
| struct hv_message *msg; |
| union hv_synic_event_flags *event; |
| bool handled = false; |
| |
| page_addr = hv_context.synic_event_page[cpu]; |
| if (page_addr == NULL) |
| return; |
| |
| event = (union hv_synic_event_flags *)page_addr + |
| VMBUS_MESSAGE_SINT; |
| /* |
| * Check for events before checking for messages. This is the order |
| * in which events and messages are checked in Windows guests on |
| * Hyper-V, and the Windows team suggested we do the same. |
| */ |
| |
| if ((vmbus_proto_version == VERSION_WS2008) || |
| (vmbus_proto_version == VERSION_WIN7)) { |
| |
| /* Since we are a child, we only need to check bit 0 */ |
| if (sync_test_and_clear_bit(0, |
| (unsigned long *) &event->flags32[0])) { |
| handled = true; |
| } |
| } else { |
| /* |
| * Our host is win8 or above. The signaling mechanism |
| * has changed and we can directly look at the event page. |
| * If bit n is set then we have an interrup on the channel |
| * whose id is n. |
| */ |
| handled = true; |
| } |
| |
| if (handled) |
| tasklet_schedule(hv_context.event_dpc[cpu]); |
| |
| |
| page_addr = hv_context.synic_message_page[cpu]; |
| msg = (struct hv_message *)page_addr + VMBUS_MESSAGE_SINT; |
| |
| /* Check if there are actual msgs to be processed */ |
| if (msg->header.message_type != HVMSG_NONE) { |
| if (msg->header.message_type == HVMSG_TIMER_EXPIRED) |
| hv_process_timer_expiration(msg, cpu); |
| else |
| tasklet_schedule(hv_context.msg_dpc[cpu]); |
| } |
| |
| add_interrupt_randomness(HYPERVISOR_CALLBACK_VECTOR, 0); |
| } |
| |
| |
| /* |
| * vmbus_bus_init -Main vmbus driver initialization routine. |
| * |
| * Here, we |
| * - initialize the vmbus driver context |
| * - invoke the vmbus hv main init routine |
| * - retrieve the channel offers |
| */ |
| static int vmbus_bus_init(void) |
| { |
| int ret; |
| |
| /* Hypervisor initialization...setup hypercall page..etc */ |
| ret = hv_init(); |
| if (ret != 0) { |
| pr_err("Unable to initialize the hypervisor - 0x%x\n", ret); |
| return ret; |
| } |
| |
| ret = bus_register(&hv_bus); |
| if (ret) |
| goto err_cleanup; |
| |
| hv_setup_vmbus_irq(vmbus_isr); |
| |
| ret = hv_synic_alloc(); |
| if (ret) |
| goto err_alloc; |
| /* |
| * Initialize the per-cpu interrupt state and |
| * connect to the host. |
| */ |
| on_each_cpu(hv_synic_init, NULL, 1); |
| ret = vmbus_connect(); |
| if (ret) |
| goto err_connect; |
| |
| if (vmbus_proto_version > VERSION_WIN7) |
| cpu_hotplug_disable(); |
| |
| /* |
| * Only register if the crash MSRs are available |
| */ |
| if (ms_hyperv.misc_features & HV_FEATURE_GUEST_CRASH_MSR_AVAILABLE) { |
| register_die_notifier(&hyperv_die_block); |
| atomic_notifier_chain_register(&panic_notifier_list, |
| &hyperv_panic_block); |
| } |
| |
| vmbus_request_offers(); |
| |
| return 0; |
| |
| err_connect: |
| on_each_cpu(hv_synic_cleanup, NULL, 1); |
| err_alloc: |
| hv_synic_free(); |
| hv_remove_vmbus_irq(); |
| |
| bus_unregister(&hv_bus); |
| |
| err_cleanup: |
| hv_cleanup(false); |
| |
| return ret; |
| } |
| |
| /** |
| * __vmbus_child_driver_register() - Register a vmbus's driver |
| * @hv_driver: Pointer to driver structure you want to register |
| * @owner: owner module of the drv |
| * @mod_name: module name string |
| * |
| * Registers the given driver with Linux through the 'driver_register()' call |
| * and sets up the hyper-v vmbus handling for this driver. |
| * It will return the state of the 'driver_register()' call. |
| * |
| */ |
| int __vmbus_driver_register(struct hv_driver *hv_driver, struct module *owner, const char *mod_name) |
| { |
| int ret; |
| |
| pr_info("registering driver %s\n", hv_driver->name); |
| |
| ret = vmbus_exists(); |
| if (ret < 0) |
| return ret; |
| |
| hv_driver->driver.name = hv_driver->name; |
| hv_driver->driver.owner = owner; |
| hv_driver->driver.mod_name = mod_name; |
| hv_driver->driver.bus = &hv_bus; |
| |
| ret = driver_register(&hv_driver->driver); |
| |
| return ret; |
| } |
| EXPORT_SYMBOL_GPL(__vmbus_driver_register); |
| |
| /** |
| * vmbus_driver_unregister() - Unregister a vmbus's driver |
| * @hv_driver: Pointer to driver structure you want to |
| * un-register |
| * |
| * Un-register the given driver that was previous registered with a call to |
| * vmbus_driver_register() |
| */ |
| void vmbus_driver_unregister(struct hv_driver *hv_driver) |
| { |
| pr_info("unregistering driver %s\n", hv_driver->name); |
| |
| if (!vmbus_exists()) |
| driver_unregister(&hv_driver->driver); |
| } |
| EXPORT_SYMBOL_GPL(vmbus_driver_unregister); |
| |
| /* |
| * vmbus_device_create - Creates and registers a new child device |
| * on the vmbus. |
| */ |
| struct hv_device *vmbus_device_create(const uuid_le *type, |
| const uuid_le *instance, |
| struct vmbus_channel *channel) |
| { |
| struct hv_device *child_device_obj; |
| |
| child_device_obj = kzalloc(sizeof(struct hv_device), GFP_KERNEL); |
| if (!child_device_obj) { |
| pr_err("Unable to allocate device object for child device\n"); |
| return NULL; |
| } |
| |
| child_device_obj->channel = channel; |
| memcpy(&child_device_obj->dev_type, type, sizeof(uuid_le)); |
| memcpy(&child_device_obj->dev_instance, instance, |
| sizeof(uuid_le)); |
| child_device_obj->vendor_id = 0x1414; /* MSFT vendor ID */ |
| |
| |
| return child_device_obj; |
| } |
| |
| /* |
| * vmbus_device_register - Register the child device |
| */ |
| int vmbus_device_register(struct hv_device *child_device_obj) |
| { |
| int ret = 0; |
| |
| dev_set_name(&child_device_obj->device, "%pUl", |
| child_device_obj->channel->offermsg.offer.if_instance.b); |
| |
| child_device_obj->device.bus = &hv_bus; |
| child_device_obj->device.parent = &hv_acpi_dev->dev; |
| child_device_obj->device.release = vmbus_device_release; |
| |
| /* |
| * Register with the LDM. This will kick off the driver/device |
| * binding...which will eventually call vmbus_match() and vmbus_probe() |
| */ |
| ret = device_register(&child_device_obj->device); |
| |
| if (ret) |
| pr_err("Unable to register child device\n"); |
| else |
| pr_debug("child device %s registered\n", |
| dev_name(&child_device_obj->device)); |
| |
| return ret; |
| } |
| |
| /* |
| * vmbus_device_unregister - Remove the specified child device |
| * from the vmbus. |
| */ |
| void vmbus_device_unregister(struct hv_device *device_obj) |
| { |
| pr_debug("child device %s unregistered\n", |
| dev_name(&device_obj->device)); |
| |
| /* |
| * Kick off the process of unregistering the device. |
| * This will call vmbus_remove() and eventually vmbus_device_release() |
| */ |
| device_unregister(&device_obj->device); |
| } |
| |
| |
| /* |
| * VMBUS is an acpi enumerated device. Get the information we |
| * need from DSDT. |
| */ |
| #define VTPM_BASE_ADDRESS 0xfed40000 |
| static acpi_status vmbus_walk_resources(struct acpi_resource *res, void *ctx) |
| { |
| resource_size_t start = 0; |
| resource_size_t end = 0; |
| struct resource *new_res; |
| struct resource **old_res = &hyperv_mmio; |
| struct resource **prev_res = NULL; |
| |
| switch (res->type) { |
| |
| /* |
| * "Address" descriptors are for bus windows. Ignore |
| * "memory" descriptors, which are for registers on |
| * devices. |
| */ |
| case ACPI_RESOURCE_TYPE_ADDRESS32: |
| start = res->data.address32.address.minimum; |
| end = res->data.address32.address.maximum; |
| break; |
| |
| case ACPI_RESOURCE_TYPE_ADDRESS64: |
| start = res->data.address64.address.minimum; |
| end = res->data.address64.address.maximum; |
| break; |
| |
| default: |
| /* Unused resource type */ |
| return AE_OK; |
| |
| } |
| /* |
| * Ignore ranges that are below 1MB, as they're not |
| * necessary or useful here. |
| */ |
| if (end < 0x100000) |
| return AE_OK; |
| |
| new_res = kzalloc(sizeof(*new_res), GFP_ATOMIC); |
| if (!new_res) |
| return AE_NO_MEMORY; |
| |
| /* If this range overlaps the virtual TPM, truncate it. */ |
| if (end > VTPM_BASE_ADDRESS && start < VTPM_BASE_ADDRESS) |
| end = VTPM_BASE_ADDRESS; |
| |
| new_res->name = "hyperv mmio"; |
| new_res->flags = IORESOURCE_MEM; |
| new_res->start = start; |
| new_res->end = end; |
| |
| /* |
| * If two ranges are adjacent, merge them. |
| */ |
| do { |
| if (!*old_res) { |
| *old_res = new_res; |
| break; |
| } |
| |
| if (((*old_res)->end + 1) == new_res->start) { |
| (*old_res)->end = new_res->end; |
| kfree(new_res); |
| break; |
| } |
| |
| if ((*old_res)->start == new_res->end + 1) { |
| (*old_res)->start = new_res->start; |
| kfree(new_res); |
| break; |
| } |
| |
| if ((*old_res)->start > new_res->end) { |
| new_res->sibling = *old_res; |
| if (prev_res) |
| (*prev_res)->sibling = new_res; |
| *old_res = new_res; |
| break; |
| } |
| |
| prev_res = old_res; |
| old_res = &(*old_res)->sibling; |
| |
| } while (1); |
| |
| return AE_OK; |
| } |
| |
| static int vmbus_acpi_remove(struct acpi_device *device) |
| { |
| struct resource *cur_res; |
| struct resource *next_res; |
| |
| if (hyperv_mmio) { |
| if (fb_mmio) { |
| __release_region(hyperv_mmio, fb_mmio->start, |
| resource_size(fb_mmio)); |
| fb_mmio = NULL; |
| } |
| |
| for (cur_res = hyperv_mmio; cur_res; cur_res = next_res) { |
| next_res = cur_res->sibling; |
| kfree(cur_res); |
| } |
| } |
| |
| return 0; |
| } |
| |
| static void vmbus_reserve_fb(void) |
| { |
| int size; |
| /* |
| * Make a claim for the frame buffer in the resource tree under the |
| * first node, which will be the one below 4GB. The length seems to |
| * be underreported, particularly in a Generation 1 VM. So start out |
| * reserving a larger area and make it smaller until it succeeds. |
| */ |
| |
| if (screen_info.lfb_base) { |
| if (efi_enabled(EFI_BOOT)) |
| size = max_t(__u32, screen_info.lfb_size, 0x800000); |
| else |
| size = max_t(__u32, screen_info.lfb_size, 0x4000000); |
| |
| for (; !fb_mmio && (size >= 0x100000); size >>= 1) { |
| fb_mmio = __request_region(hyperv_mmio, |
| screen_info.lfb_base, size, |
| fb_mmio_name, 0); |
| } |
| } |
| } |
| |
| /** |
| * vmbus_allocate_mmio() - Pick a memory-mapped I/O range. |
| * @new: If successful, supplied a pointer to the |
| * allocated MMIO space. |
| * @device_obj: Identifies the caller |
| * @min: Minimum guest physical address of the |
| * allocation |
| * @max: Maximum guest physical address |
| * @size: Size of the range to be allocated |
| * @align: Alignment of the range to be allocated |
| * @fb_overlap_ok: Whether this allocation can be allowed |
| * to overlap the video frame buffer. |
| * |
| * This function walks the resources granted to VMBus by the |
| * _CRS object in the ACPI namespace underneath the parent |
| * "bridge" whether that's a root PCI bus in the Generation 1 |
| * case or a Module Device in the Generation 2 case. It then |
| * attempts to allocate from the global MMIO pool in a way that |
| * matches the constraints supplied in these parameters and by |
| * that _CRS. |
| * |
| * Return: 0 on success, -errno on failure |
| */ |
| int vmbus_allocate_mmio(struct resource **new, struct hv_device *device_obj, |
| resource_size_t min, resource_size_t max, |
| resource_size_t size, resource_size_t align, |
| bool fb_overlap_ok) |
| { |
| struct resource *iter, *shadow; |
| resource_size_t range_min, range_max, start; |
| const char *dev_n = dev_name(&device_obj->device); |
| int retval; |
| |
| retval = -ENXIO; |
| down(&hyperv_mmio_lock); |
| |
| /* |
| * If overlaps with frame buffers are allowed, then first attempt to |
| * make the allocation from within the reserved region. Because it |
| * is already reserved, no shadow allocation is necessary. |
| */ |
| if (fb_overlap_ok && fb_mmio && !(min > fb_mmio->end) && |
| !(max < fb_mmio->start)) { |
| |
| range_min = fb_mmio->start; |
| range_max = fb_mmio->end; |
| start = (range_min + align - 1) & ~(align - 1); |
| for (; start + size - 1 <= range_max; start += align) { |
| *new = request_mem_region_exclusive(start, size, dev_n); |
| if (*new) { |
| retval = 0; |
| goto exit; |
| } |
| } |
| } |
| |
| for (iter = hyperv_mmio; iter; iter = iter->sibling) { |
| if ((iter->start >= max) || (iter->end <= min)) |
| continue; |
| |
| range_min = iter->start; |
| range_max = iter->end; |
| start = (range_min + align - 1) & ~(align - 1); |
| for (; start + size - 1 <= range_max; start += align) { |
| shadow = __request_region(iter, start, size, NULL, |
| IORESOURCE_BUSY); |
| if (!shadow) |
| continue; |
| |
| *new = request_mem_region_exclusive(start, size, dev_n); |
| if (*new) { |
| shadow->name = (char *)*new; |
| retval = 0; |
| goto exit; |
| } |
| |
| __release_region(iter, start, size); |
| } |
| } |
| |
| exit: |
| up(&hyperv_mmio_lock); |
| return retval; |
| } |
| EXPORT_SYMBOL_GPL(vmbus_allocate_mmio); |
| |
| /** |
| * vmbus_free_mmio() - Free a memory-mapped I/O range. |
| * @start: Base address of region to release. |
| * @size: Size of the range to be allocated |
| * |
| * This function releases anything requested by |
| * vmbus_mmio_allocate(). |
| */ |
| void vmbus_free_mmio(resource_size_t start, resource_size_t size) |
| { |
| struct resource *iter; |
| |
| down(&hyperv_mmio_lock); |
| for (iter = hyperv_mmio; iter; iter = iter->sibling) { |
| if ((iter->start >= start + size) || (iter->end <= start)) |
| continue; |
| |
| __release_region(iter, start, size); |
| } |
| release_mem_region(start, size); |
| up(&hyperv_mmio_lock); |
| |
| } |
| EXPORT_SYMBOL_GPL(vmbus_free_mmio); |
| |
| /** |
| * vmbus_cpu_number_to_vp_number() - Map CPU to VP. |
| * @cpu_number: CPU number in Linux terms |
| * |
| * This function returns the mapping between the Linux processor |
| * number and the hypervisor's virtual processor number, useful |
| * in making hypercalls and such that talk about specific |
| * processors. |
| * |
| * Return: Virtual processor number in Hyper-V terms |
| */ |
| int vmbus_cpu_number_to_vp_number(int cpu_number) |
| { |
| return hv_context.vp_index[cpu_number]; |
| } |
| EXPORT_SYMBOL_GPL(vmbus_cpu_number_to_vp_number); |
| |
| static int vmbus_acpi_add(struct acpi_device *device) |
| { |
| acpi_status result; |
| int ret_val = -ENODEV; |
| struct acpi_device *ancestor; |
| |
| hv_acpi_dev = device; |
| |
| result = acpi_walk_resources(device->handle, METHOD_NAME__CRS, |
| vmbus_walk_resources, NULL); |
| |
| if (ACPI_FAILURE(result)) |
| goto acpi_walk_err; |
| /* |
| * Some ancestor of the vmbus acpi device (Gen1 or Gen2 |
| * firmware) is the VMOD that has the mmio ranges. Get that. |
| */ |
| for (ancestor = device->parent; ancestor; ancestor = ancestor->parent) { |
| result = acpi_walk_resources(ancestor->handle, METHOD_NAME__CRS, |
| vmbus_walk_resources, NULL); |
| |
| if (ACPI_FAILURE(result)) |
| continue; |
| if (hyperv_mmio) { |
| vmbus_reserve_fb(); |
| break; |
| } |
| } |
| ret_val = 0; |
| |
| acpi_walk_err: |
| complete(&probe_event); |
| if (ret_val) |
| vmbus_acpi_remove(device); |
| return ret_val; |
| } |
| |
| static const struct acpi_device_id vmbus_acpi_device_ids[] = { |
| {"VMBUS", 0}, |
| {"VMBus", 0}, |
| {"", 0}, |
| }; |
| MODULE_DEVICE_TABLE(acpi, vmbus_acpi_device_ids); |
| |
| static struct acpi_driver vmbus_acpi_driver = { |
| .name = "vmbus", |
| .ids = vmbus_acpi_device_ids, |
| .ops = { |
| .add = vmbus_acpi_add, |
| .remove = vmbus_acpi_remove, |
| }, |
| }; |
| |
| static void hv_kexec_handler(void) |
| { |
| int cpu; |
| |
| hv_synic_clockevents_cleanup(); |
| vmbus_initiate_unload(false); |
| for_each_online_cpu(cpu) |
| smp_call_function_single(cpu, hv_synic_cleanup, NULL, 1); |
| hv_cleanup(false); |
| }; |
| |
| static void hv_crash_handler(struct pt_regs *regs) |
| { |
| vmbus_initiate_unload(true); |
| /* |
| * In crash handler we can't schedule synic cleanup for all CPUs, |
| * doing the cleanup for current CPU only. This should be sufficient |
| * for kdump. |
| */ |
| hv_synic_cleanup(NULL); |
| hv_cleanup(true); |
| }; |
| |
| static int __init hv_acpi_init(void) |
| { |
| int ret, t; |
| |
| if (x86_hyper != &x86_hyper_ms_hyperv) |
| return -ENODEV; |
| |
| init_completion(&probe_event); |
| |
| /* |
| * Get ACPI resources first. |
| */ |
| ret = acpi_bus_register_driver(&vmbus_acpi_driver); |
| |
| if (ret) |
| return ret; |
| |
| t = wait_for_completion_timeout(&probe_event, 5*HZ); |
| if (t == 0) { |
| ret = -ETIMEDOUT; |
| goto cleanup; |
| } |
| |
| ret = vmbus_bus_init(); |
| if (ret) |
| goto cleanup; |
| |
| hv_setup_kexec_handler(hv_kexec_handler); |
| hv_setup_crash_handler(hv_crash_handler); |
| |
| return 0; |
| |
| cleanup: |
| acpi_bus_unregister_driver(&vmbus_acpi_driver); |
| hv_acpi_dev = NULL; |
| return ret; |
| } |
| |
| static void __exit vmbus_exit(void) |
| { |
| int cpu; |
| |
| hv_remove_kexec_handler(); |
| hv_remove_crash_handler(); |
| vmbus_connection.conn_state = DISCONNECTED; |
| hv_synic_clockevents_cleanup(); |
| vmbus_disconnect(); |
| hv_remove_vmbus_irq(); |
| for_each_online_cpu(cpu) |
| tasklet_kill(hv_context.msg_dpc[cpu]); |
| vmbus_free_channels(); |
| if (ms_hyperv.misc_features & HV_FEATURE_GUEST_CRASH_MSR_AVAILABLE) { |
| unregister_die_notifier(&hyperv_die_block); |
| atomic_notifier_chain_unregister(&panic_notifier_list, |
| &hyperv_panic_block); |
| } |
| bus_unregister(&hv_bus); |
| hv_cleanup(false); |
| for_each_online_cpu(cpu) { |
| tasklet_kill(hv_context.event_dpc[cpu]); |
| smp_call_function_single(cpu, hv_synic_cleanup, NULL, 1); |
| } |
| hv_synic_free(); |
| acpi_bus_unregister_driver(&vmbus_acpi_driver); |
| if (vmbus_proto_version > VERSION_WIN7) |
| cpu_hotplug_enable(); |
| } |
| |
| |
| MODULE_LICENSE("GPL"); |
| |
| subsys_initcall(hv_acpi_init); |
| module_exit(vmbus_exit); |