root/usr/src/uts/sun4v/os/hsvc.c
/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License (the "License").
 * You may not use this file except in compliance with the License.
 *
 * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
 * or http://www.opensolaris.org/os/licensing.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at usr/src/OPENSOLARIS.LICENSE.
 * If applicable, add the following below this CDDL HEADER, with the
 * fields enclosed by brackets "[]" replaced with your own identifying
 * information: Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 */

/*
 * Copyright 2009 Sun Microsystems, Inc.  All rights reserved.
 * Use is subject to license terms.
 */

#include <sys/types.h>
#include <sys/dditypes.h>
#include <sys/machsystm.h>
#include <sys/archsystm.h>
#include <sys/prom_plat.h>
#include <sys/promif.h>
#include <sys/kmem.h>
#include <sys/hypervisor_api.h>
#include <sys/hsvc.h>

#ifdef DEBUG

int     hsvc_debug = 0;                         /* HSVC debug flags */

/*
 * Flags to control HSVC debugging
 */
#define DBG_HSVC_REGISTER       0x0001
#define DBG_HSVC_UNREGISTER     0x0002
#define DBG_HSVC_OBP_CIF        0x0004
#define DBG_HSVC_ALLOC          0x0008
#define DBG_HSVC_VERSION        0x0010
#define DBG_HSVC_REFCNT         0x0020
#define DBG_HSVC_SETUP          0x0040

#define HSVC_CHK_REFCNT(hsvcp)  \
        if (hsvc_debug & DBG_HSVC_REFCNT) hsvc_chk_refcnt(hsvcp)

#define HSVC_DEBUG(flag, ARGS)  \
        if (hsvc_debug & flag)  prom_printf ARGS

#define HSVC_DUMP()     \
        if (hsvc_debug & DBG_HSVC_SETUP) hsvc_dump()

#else /* DEBUG */

#define HSVC_CHK_REFCNT(hsvcp)
#define HSVC_DEBUG(flag, args)
#define HSVC_DUMP()

#endif /* DEBUG */

/*
 * Each hypervisor API group negotiation is tracked via a
 * hsvc structure. This structure contains the API group,
 * currently negotiated major/minor number, a singly linked
 * list of clients currently registered and a reference count.
 *
 * Since the number of API groups is fairly small, negotiated
 * API groups are maintained via a singly linked list. Also,
 * sufficient free space is reserved to allow for API group
 * registration before kmem_xxx interface can be used to
 * allocate memory dynamically.
 *
 * Note that all access to the API group lookup and negotiation
 * is serialized to support strict HV API interface.
 */

typedef struct hsvc {
        struct hsvc     *next;                  /* next group/free entry */
        uint64_t        group;                  /* hypervisor service group */
        uint64_t        major;                  /* major number */
        uint64_t        minor;                  /* minor number */
        uint64_t        refcnt;                 /* reference count */
        hsvc_info_t     *clients;               /* linked list of clients */
} hsvc_t;


/*
 * Global variables
 */
hsvc_t          *hsvc_groups;           /* linked list of API groups in use */
hsvc_t          *hsvc_avail;            /* free reserved buffers */
kmutex_t        hsvc_lock;              /* protects linked list and globals */

/*
 * Preallocate some space for boot requirements (before kmem_xxx can be
 * used)
 */
#define HSVC_RESV_BUFS_MAX              16
hsvc_t  hsvc_resv_bufs[HSVC_RESV_BUFS_MAX];

/*
 * Pre-versioning groups (not negotiated by Ontario/Erie FCS release)
 */
static uint64_t hsvc_pre_versioning_groups[] = {
        HSVC_GROUP_SUN4V,
        HSVC_GROUP_CORE,
        HSVC_GROUP_VPCI,
        HSVC_GROUP_VSC,
        HSVC_GROUP_NIAGARA_CPU,
        HSVC_GROUP_NCS,
        HSVC_GROUP_DIAG
};

#define HSVC_PRE_VERSIONING_GROUP_CNT   \
        (sizeof (hsvc_pre_versioning_groups) / sizeof (uint64_t))

static boolean_t
pre_versioning_group(uint64_t api_group)
{
        int     i;

        for (i = 0; i < HSVC_PRE_VERSIONING_GROUP_CNT; i++)
                if (hsvc_pre_versioning_groups[i] == api_group)
                        return (B_TRUE);
        return (B_FALSE);
}

static hsvc_t *
hsvc_lookup(hsvc_info_t *hsvcinfop)
{
        hsvc_t          *hsvcp;
        hsvc_info_t     *p;

        for (hsvcp = hsvc_groups; hsvcp != NULL; hsvcp = hsvcp->next) {
                for (p = hsvcp->clients; p != NULL;
                    p = (hsvc_info_t *)p->hsvc_private)
                        if (p == hsvcinfop)
                                break;
                if (p)
                        break;
        }

        return (hsvcp);
}

#ifdef DEBUG

/*
 * Check client reference count
 */
static void
hsvc_chk_refcnt(hsvc_t *hsvcp)
{
        int             refcnt;
        hsvc_info_t     *p;

        for (refcnt = 0, p = hsvcp->clients; p != NULL;
            p = (hsvc_info_t *)p->hsvc_private)
                refcnt++;

        ASSERT(hsvcp->refcnt == refcnt);
}

/*
 * Dump registered clients information
 */
static void
hsvc_dump(void)
{
        hsvc_t          *hsvcp;
        hsvc_info_t     *p;

        mutex_enter(&hsvc_lock);

        prom_printf("hsvc_dump: hsvc_groups: %p  hsvc_avail: %p\n",
            (void *)hsvc_groups, (void *)hsvc_avail);

        for (hsvcp = hsvc_groups; hsvcp != NULL; hsvcp = hsvcp->next) {
                prom_printf(" hsvcp: %p (0x%lx 0x%lx 0x%lx) ref: %ld clients: "
                    "%p\n", (void *)hsvcp, hsvcp->group, hsvcp->major,
                    hsvcp->minor, hsvcp->refcnt, (void *)hsvcp->clients);

                for (p = hsvcp->clients; p != NULL;
                    p = (hsvc_info_t *)p->hsvc_private) {
                        prom_printf("  client %p (0x%lx 0x%lx 0x%lx '%s') "
                            "private: %p\n", (void *)p, p->hsvc_group,
                            p->hsvc_major, p->hsvc_minor, p->hsvc_modname,
                            p->hsvc_private);
                }
        }

        mutex_exit(&hsvc_lock);
}

#endif /* DEBUG */

/*
 * Allocate a buffer to cache API group information. Note that we
 * allocate a buffer from reserved pool early on, before kmem_xxx
 * interface becomes available.
 */
static hsvc_t *
hsvc_alloc(void)
{
        hsvc_t  *hsvcp;

        ASSERT(MUTEX_HELD(&hsvc_lock));

        if (hsvc_avail != NULL) {
                hsvcp = hsvc_avail;
                hsvc_avail = hsvcp->next;
        } else if (kmem_ready) {
                hsvcp = kmem_zalloc(sizeof (hsvc_t), KM_SLEEP);
                HSVC_DEBUG(DBG_HSVC_ALLOC,
                    ("hsvc_alloc: hsvc_avail: %p  kmem_zalloc hsvcp: %p\n",
                    (void *)hsvc_avail, (void *)hsvcp));
        } else
                hsvcp = NULL;
        return (hsvcp);
}

static void
hsvc_free(hsvc_t *hsvcp)
{
        ASSERT(hsvcp != NULL);
        ASSERT(MUTEX_HELD(&hsvc_lock));

        if (hsvcp >= hsvc_resv_bufs &&
            hsvcp < &hsvc_resv_bufs[HSVC_RESV_BUFS_MAX]) {
                hsvcp->next = hsvc_avail;
                hsvc_avail =  hsvcp;
        } else {
                HSVC_DEBUG(DBG_HSVC_ALLOC,
                    ("hsvc_free: hsvc_avail: %p  kmem_free hsvcp: %p\n",
                    (void *)hsvc_avail, (void *)hsvcp));
                (void) kmem_free(hsvcp, sizeof (hsvc_t));
        }
}

/*
 * Link client on the specified hsvc's client list and
 * bump the reference count.
 */
static void
hsvc_link_client(hsvc_t *hsvcp, hsvc_info_t *hsvcinfop)
{
        ASSERT(MUTEX_HELD(&hsvc_lock));
        HSVC_CHK_REFCNT(hsvcp);

        hsvcinfop->hsvc_private = hsvcp->clients;
        hsvcp->clients = hsvcinfop;
        hsvcp->refcnt++;
}

/*
 * Unlink a client from the specified hsvc's client list and
 * decrement the reference count, if found.
 *
 * Return 0 if client unlinked. Otherwise return -1.
 */
static int
hsvc_unlink_client(hsvc_t *hsvcp, hsvc_info_t *hsvcinfop)
{
        hsvc_info_t     *p, **pp;
        int     status = 0;

        ASSERT(MUTEX_HELD(&hsvc_lock));
        HSVC_CHK_REFCNT(hsvcp);

        for (pp = &hsvcp->clients; (p = *pp) != NULL;
            pp = (hsvc_info_t **)&p->hsvc_private) {
                if (p != hsvcinfop)
                        continue;

                ASSERT(hsvcp->refcnt > 0);
                hsvcp->refcnt--;
                *pp = (hsvc_info_t *)p->hsvc_private;
                p->hsvc_private = NULL;
                break;
        }

        if (p == NULL)
                status = -1;

        return (status);
}

/*
 * Negotiate/register an API group usage
 */
int
hsvc_register(hsvc_info_t *hsvcinfop, uint64_t *supported_minor)
{
        hsvc_t *hsvcp;
        uint64_t api_group = hsvcinfop->hsvc_group;
        uint64_t major = hsvcinfop->hsvc_major;
        uint64_t minor = hsvcinfop->hsvc_minor;
        int status = 0;

        HSVC_DEBUG(DBG_HSVC_REGISTER,
            ("hsvc_register %p (0x%lx 0x%lx 0x%lx ID %s)\n", (void *)hsvcinfop,
            api_group, major, minor, hsvcinfop->hsvc_modname));

        if (hsvcinfop->hsvc_rev != HSVC_REV_1)
                return (EINVAL);

        mutex_enter(&hsvc_lock);

        /*
         * Make sure that the hsvcinfop is new (i.e. not already registered).
         */
        if (hsvc_lookup(hsvcinfop) != NULL) {
                mutex_exit(&hsvc_lock);
                return (EINVAL);
        }

        /*
         * Search for the specified api_group
         */
        for (hsvcp = hsvc_groups; hsvcp != NULL; hsvcp = hsvcp->next)
                if (hsvcp->group == api_group)
                        break;

        if (hsvcp) {
                /*
                 * If major number mismatch, then return ENOTSUP.
                 * Otherwise return currently negotiated minor
                 * and the following status:
                 *      ENOTSUP         requested minor < current minor
                 *      OK              requested minor >= current minor
                 */

                if (hsvcp->major != major) {
                        status = ENOTSUP;
                } else if (hsvcp->minor > minor) {
                        /*
                         * Client requested a lower minor number than
                         * currently in use.
                         */
                        status = ENOTSUP;
                        *supported_minor = hsvcp->minor;
                } else {
                        /*
                         * Client requested a minor number same or higher
                         * than the one in use.  Set supported minor number
                         * and link the client on hsvc client linked list.
                         */
                        *supported_minor = hsvcp->minor;
                        hsvc_link_client(hsvcp, hsvcinfop);
                }
        } else {
                /*
                 * This service group has not been negotiated yet.
                 * Call OBP CIF interface to negotiate a major/minor
                 * number.
                 *
                 * If not enough memory to cache this information, then
                 * return EAGAIN so that the caller can try again later.
                 * Otherwise, process OBP CIF results as follows:
                 *
                 *      H_BADTRAP       OBP CIF interface is not supported.
                 *                      If not a pre-versioning group, then
                 *                      return EINVAL, indicating unsupported
                 *                      API group. Otherwise, mimic default
                 *                      behavior (i.e. support only major=1).
                 *
                 *      H_EOK           Negotiation was successful. Cache
                 *                      and return supported major/minor,
                 *                      limiting the minor number to the
                 *                      requested value.
                 *
                 *      H_EINVAL        Invalid group. Return EINVAL
                 *
                 *      H_ENOTSUPPORTED Unsupported major number. Return
                 *                      ENOTSUP.
                 *
                 *      H_EBUSY         Return EAGAIN.
                 *
                 *      H_EWOULDBLOCK   Return EAGAIN.
                 */
                hsvcp = hsvc_alloc();
                if (hsvcp == NULL) {
                        status = EAGAIN;
                } else {
                        uint64_t hvstat;

                        hvstat = prom_set_sun4v_api_version(api_group,
                            major, minor, supported_minor);

                        HSVC_DEBUG(DBG_HSVC_OBP_CIF,
                            ("prom_set_sun4v_api_ver: 0x%lx 0x%lx, 0x%lx "
                            " hvstat: 0x%lx sup_minor: 0x%lx\n", api_group,
                            major, minor, hvstat, *supported_minor));

                        switch (hvstat) {
                        case H_EBADTRAP:
                                /*
                                 * Older firmware does not support OBP CIF
                                 * interface. If it's a pre-versioning group,
                                 * then assume that the firmware supports
                                 * only major=1 and minor=0.
                                 */
                                if (!pre_versioning_group(api_group)) {
                                        status = EINVAL;
                                        break;
                                } else if (major != 1) {
                                        status = ENOTSUP;
                                        break;
                                }

                                /*
                                 * It's a preversioning group. Default minor
                                 * value to 0.
                                 */
                                *supported_minor = 0;

                                /*FALLTHROUGH*/
                        case H_EOK:
                                /*
                                 * Limit supported minor number to the
                                 * requested value and cache the new
                                 * API group information.
                                 */
                                if (*supported_minor > minor)
                                        *supported_minor = minor;
                                hsvcp->group = api_group;
                                hsvcp->major = major;
                                hsvcp->minor = *supported_minor;
                                hsvcp->refcnt = 0;
                                hsvcp->clients = NULL;
                                hsvcp->next = hsvc_groups;
                                hsvc_groups = hsvcp;

                                /*
                                 * Link the caller on the client linked list.
                                 */
                                hsvc_link_client(hsvcp, hsvcinfop);
                                break;

                        case H_ENOTSUPPORTED:
                                status = ENOTSUP;
                                break;

                        case H_EBUSY:
                        case H_EWOULDBLOCK:
                                status = EAGAIN;
                                break;

                        case H_EINVAL:
                        default:
                                status = EINVAL;
                                break;
                        }
                }
                /*
                 * Deallocate entry if not used
                 */
                if (status != 0)
                        hsvc_free(hsvcp);
        }
        mutex_exit(&hsvc_lock);

        HSVC_DEBUG(DBG_HSVC_REGISTER,
            ("hsvc_register(%p) status; %d sup_minor: 0x%lx\n",
            (void *)hsvcinfop, status, *supported_minor));

        return (status);
}

/*
 * Unregister an API group usage
 */
int
hsvc_unregister(hsvc_info_t *hsvcinfop)
{
        hsvc_t          **hsvcpp, *hsvcp;
        uint64_t        api_group;
        uint64_t        major, supported_minor;
        int             status = 0;

        if (hsvcinfop->hsvc_rev != HSVC_REV_1)
                return (EINVAL);

        major = hsvcinfop->hsvc_major;
        api_group = hsvcinfop->hsvc_group;

        HSVC_DEBUG(DBG_HSVC_UNREGISTER,
            ("hsvc_unregister %p (0x%lx 0x%lx 0x%lx ID %s)\n",
            (void *)hsvcinfop, api_group, major, hsvcinfop->hsvc_minor,
            hsvcinfop->hsvc_modname));

        /*
         * Search for the matching entry and return EINVAL if no match found.
         * Otherwise, remove it from our list and unregister the API
         * group if this was the last reference to that API group.
         */
        mutex_enter(&hsvc_lock);

        for (hsvcpp = &hsvc_groups; (hsvcp = *hsvcpp) != NULL;
            hsvcpp = &hsvcp->next) {
                if (hsvcp->group != api_group || hsvcp->major != major)
                        continue;

                /*
                 * Search client list for a matching hsvcinfop entry
                 * and unlink it and decrement refcnt, if found.
                 */
                if (hsvc_unlink_client(hsvcp, hsvcinfop) < 0) {
                        /* client not registered */
                        status = EINVAL;
                        break;
                }

                /*
                 * Client has been unlinked. If this was the last
                 * reference, unregister API group via OBP CIF
                 * interface.
                 */
                if (hsvcp->refcnt == 0) {
                        uint64_t        hvstat;

                        ASSERT(hsvcp->clients == NULL);
                        hvstat = prom_set_sun4v_api_version(api_group, 0, 0,
                            &supported_minor);

                        HSVC_DEBUG(DBG_HSVC_OBP_CIF,
                            (" prom unreg group: 0x%lx hvstat: 0x%lx\n",
                            api_group, hvstat));

                        /*
                         * Note that the call to unnegotiate an API group
                         * may fail if anyone, including OBP, is using
                         * those services. However, the caller is done
                         * with this API group and should be allowed to
                         * unregister regardless of the outcome.
                         */
                        *hsvcpp = hsvcp->next;
                        hsvc_free(hsvcp);
                }
                break;
        }

        if (hsvcp == NULL)
                status = EINVAL;

        mutex_exit(&hsvc_lock);

        HSVC_DEBUG(DBG_HSVC_UNREGISTER,
            ("hsvc_unregister %p status: %d\n", (void *)hsvcinfop, status));

        return (status);
}


/*
 * Get negotiated major/minor version number for an API group
 */
int
hsvc_version(uint64_t api_group, uint64_t *majorp, uint64_t *minorp)
{
        int status = 0;
        uint64_t hvstat;
        hsvc_t  *hsvcp;

        /*
         * Check if the specified api_group is already in use.
         * If so, return currently negotiated major/minor number.
         * Otherwise, call OBP CIF interface to get the currently
         * negotiated major/minor number.
         */
        mutex_enter(&hsvc_lock);
        for (hsvcp = hsvc_groups; hsvcp != NULL; hsvcp = hsvcp->next)
                if (hsvcp->group == api_group)
                        break;

        if (hsvcp) {
                *majorp = hsvcp->major;
                *minorp = hsvcp->minor;
        } else {
                hvstat = prom_get_sun4v_api_version(api_group, majorp, minorp);

                switch (hvstat) {
                case H_EBADTRAP:
                        /*
                         * Older firmware does not support OBP CIF
                         * interface. If it's a pre-versioning group,
                         * then return default major/minor (i.e. 1/0).
                         * Otherwise, return EINVAL.
                         */
                        if (pre_versioning_group(api_group)) {
                                *majorp = 1;
                                *minorp = 0;
                        } else
                                status = EINVAL;
                        break;

                case H_EINVAL:
                default:
                        status = EINVAL;
                        break;

                }
        }
        mutex_exit(&hsvc_lock);

        HSVC_DEBUG(DBG_HSVC_VERSION,
            ("hsvc_version(0x%lx) status: %d major: 0x%lx minor: 0x%lx\n",
            api_group, status, *majorp, *minorp));

        return (status);
}

/*
 * Initialize framework data structures
 */
void
hsvc_init(void)
{
        int             i;
        hsvc_t          *hsvcp;

        /*
         * Initialize global data structures
         */
        mutex_init(&hsvc_lock, NULL, MUTEX_DEFAULT, NULL);
        hsvc_groups = NULL;
        hsvc_avail = NULL;

        /*
         * Setup initial free list
         */
        mutex_enter(&hsvc_lock);
        for (i = 0, hsvcp = &hsvc_resv_bufs[0];
            i < HSVC_RESV_BUFS_MAX; i++, hsvcp++)
                hsvc_free(hsvcp);
        mutex_exit(&hsvc_lock);
}


/*
 * Hypervisor services to be negotiated at boot time.
 *
 * Note that the kernel needs to negotiate the HSVC_GROUP_SUN4V
 * API group first, before doing any other negotiation. Also, it
 * uses hypervisor services belonging to the HSVC_GROUP_CORE API
 * group only for itself.
 *
 * Note that the HSVC_GROUP_DIAG is negotiated on behalf of
 * any driver/module using DIAG services.
 */
typedef struct hsvc_info_unix_s {
        hsvc_info_t     hsvcinfo;
        int             required;
} hsvc_info_unix_t;

static hsvc_info_unix_t  hsvcinfo_unix[] = {
        {{HSVC_REV_1, NULL,     HSVC_GROUP_SUN4V,       1,      0, NULL}, 1},
        {{HSVC_REV_1, NULL,     HSVC_GROUP_CORE,        1,      2, NULL}, 1},
        {{HSVC_REV_1, NULL,     HSVC_GROUP_DIAG,        1,      0, NULL}, 1},
        {{HSVC_REV_1, NULL,     HSVC_GROUP_INTR,        1,      0, NULL}, 0},
        {{HSVC_REV_1, NULL,     HSVC_GROUP_REBOOT_DATA, 1,      0, NULL}, 0},
};

#define HSVCINFO_UNIX_CNT       (sizeof (hsvcinfo_unix) / sizeof (hsvc_info_t))
static char     *hsvcinfo_unix_modname = "unix";

/*
 * Initialize framework and register hypervisor services to be used
 * by the kernel.
 */
void
hsvc_setup()
{
        int                     i, status;
        uint64_t                sup_minor;
        hsvc_info_unix_t        *hsvcinfop;

        /*
         * Initialize framework
         */
        hsvc_init();

        /*
         * Negotiate versioning for required groups
         */
        for (hsvcinfop = &hsvcinfo_unix[0], i = 0; i < HSVCINFO_UNIX_CNT;
            i++, hsvcinfop++) {
                hsvcinfop->hsvcinfo.hsvc_private = NULL;
                hsvcinfop->hsvcinfo.hsvc_modname = hsvcinfo_unix_modname;
                status = hsvc_register(&(hsvcinfop->hsvcinfo), &sup_minor);

                if ((status != 0) && hsvcinfop->required) {
                        cmn_err(CE_PANIC, "%s: cannot negotiate hypervisor "
                            "services - group: 0x%lx major: 0x%lx minor: 0x%lx"
                            " errno: %d\n", hsvcinfop->hsvcinfo.hsvc_modname,
                            hsvcinfop->hsvcinfo.hsvc_group,
                            hsvcinfop->hsvcinfo.hsvc_major,
                            hsvcinfop->hsvcinfo.hsvc_minor, status);
                }
        }
        HSVC_DUMP();
}