You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
2845 lines
83 KiB
2845 lines
83 KiB
/*
|
|
* drivers/vservices/session.c
|
|
*
|
|
* Copyright (c) 2012-2018 General Dynamics
|
|
* Copyright (c) 2014 Open Kernel Labs, Inc.
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License version 2 as
|
|
* published by the Free Software Foundation.
|
|
*
|
|
* This is the generic session-management code for the vServices framework.
|
|
* It creates service and session devices on request from session and
|
|
* transport drivers, respectively; it also queues incoming messages from the
|
|
* transport and distributes them to the session's services.
|
|
*/
|
|
|
|
#include <linux/version.h>
|
|
#include <linux/kernel.h>
|
|
#include <linux/moduleparam.h>
|
|
#include <linux/module.h>
|
|
#include <linux/jiffies.h>
|
|
#include <linux/workqueue.h>
|
|
#include <linux/sched.h>
|
|
#include <linux/wait.h>
|
|
#include <linux/notifier.h>
|
|
#include <linux/spinlock.h>
|
|
#include <linux/device.h>
|
|
#include <linux/mutex.h>
|
|
#include <linux/bitops.h>
|
|
#include <linux/slab.h>
|
|
#include <linux/kdev_t.h>
|
|
#include <linux/err.h>
|
|
|
|
#include <vservices/transport.h>
|
|
#include <vservices/session.h>
|
|
#include <vservices/service.h>
|
|
|
|
#include "session.h"
|
|
#include "transport.h"
|
|
#include "compat.h"
|
|
|
|
/* Minimum required time between resets to avoid throttling */
|
|
#define RESET_THROTTLE_TIME msecs_to_jiffies(1000)
|
|
|
|
/*
|
|
* Minimum/maximum reset throttling time. The reset throttle will start at
|
|
* the minimum and increase to the maximum exponetially.
|
|
*/
|
|
#define RESET_THROTTLE_MIN RESET_THROTTLE_TIME
|
|
#define RESET_THROTTLE_MAX msecs_to_jiffies(8 * 1000)
|
|
|
|
/*
|
|
* If the reset is being throttled and a sane reset (doesn't need throttling)
|
|
* is requested, then if the service's reset delay mutliplied by this value
|
|
* has elapsed throttling is disabled.
|
|
*/
|
|
#define RESET_THROTTLE_COOL_OFF_MULT 2
|
|
|
|
/* IDR of session ids to sessions */
|
|
static DEFINE_IDR(session_idr);
|
|
DEFINE_MUTEX(vs_session_lock);
|
|
EXPORT_SYMBOL_GPL(vs_session_lock);
|
|
|
|
/* Notifier list for vService session events */
|
|
static BLOCKING_NOTIFIER_HEAD(vs_session_notifier_list);
|
|
|
|
static unsigned long default_debug_mask;
|
|
module_param(default_debug_mask, ulong, S_IRUGO | S_IWUSR);
|
|
MODULE_PARM_DESC(default_debug_mask, "Default vServices debug mask");
|
|
|
|
/* vServices root in sysfs at /sys/vservices */
|
|
struct kobject *vservices_root;
|
|
EXPORT_SYMBOL_GPL(vservices_root);
|
|
|
|
/* vServices server root in sysfs at /sys/vservices/server-sessions */
|
|
struct kobject *vservices_server_root;
|
|
EXPORT_SYMBOL_GPL(vservices_server_root);
|
|
|
|
/* vServices client root in sysfs at /sys/vservices/client-sessions */
|
|
struct kobject *vservices_client_root;
|
|
EXPORT_SYMBOL_GPL(vservices_client_root);
|
|
|
|
#ifdef CONFIG_VSERVICES_CHAR_DEV
|
|
struct vs_service_device *vs_service_lookup_by_devt(dev_t dev)
|
|
{
|
|
struct vs_session_device *session;
|
|
struct vs_service_device *service;
|
|
|
|
mutex_lock(&vs_session_lock);
|
|
session = idr_find(&session_idr, MINOR(dev) / VS_MAX_SERVICES);
|
|
get_device(&session->dev);
|
|
mutex_unlock(&vs_session_lock);
|
|
|
|
service = vs_session_get_service(session,
|
|
MINOR(dev) % VS_MAX_SERVICES);
|
|
put_device(&session->dev);
|
|
|
|
return service;
|
|
}
|
|
#endif
|
|
|
|
struct vs_session_for_each_data {
|
|
int (*fn)(struct vs_session_device *session, void *data);
|
|
void *data;
|
|
};
|
|
|
|
int vs_session_for_each_from_idr(int id, void *session, void *_data)
|
|
{
|
|
struct vs_session_for_each_data *data =
|
|
(struct vs_session_for_each_data *)_data;
|
|
return data->fn(session, data->data);
|
|
}
|
|
|
|
/**
|
|
* vs_session_for_each_locked - call a callback function for each session
|
|
* @fn: function to call
|
|
* @data: opaque pointer that is passed through to the function
|
|
*/
|
|
extern int vs_session_for_each_locked(
|
|
int (*fn)(struct vs_session_device *session, void *data),
|
|
void *data)
|
|
{
|
|
struct vs_session_for_each_data priv = { .fn = fn, .data = data };
|
|
|
|
lockdep_assert_held(&vs_session_lock);
|
|
|
|
return idr_for_each(&session_idr, vs_session_for_each_from_idr,
|
|
&priv);
|
|
}
|
|
EXPORT_SYMBOL(vs_session_for_each_locked);
|
|
|
|
/**
|
|
* vs_register_notify - register a notifier callback for vServices events
|
|
* @nb: pointer to the notifier block for the callback events.
|
|
*/
|
|
void vs_session_register_notify(struct notifier_block *nb)
|
|
{
|
|
blocking_notifier_chain_register(&vs_session_notifier_list, nb);
|
|
}
|
|
EXPORT_SYMBOL(vs_session_register_notify);
|
|
|
|
/**
|
|
* vs_unregister_notify - unregister a notifier callback for vServices events
|
|
* @nb: pointer to the notifier block for the callback events.
|
|
*/
|
|
void vs_session_unregister_notify(struct notifier_block *nb)
|
|
{
|
|
blocking_notifier_chain_unregister(&vs_session_notifier_list, nb);
|
|
}
|
|
EXPORT_SYMBOL(vs_session_unregister_notify);
|
|
|
|
/*
|
|
* Helper function for returning how long ago something happened
|
|
* Marked as __maybe_unused since this is only needed when
|
|
* CONFIG_VSERVICES_DEBUG is enabled, but cannot be removed because it
|
|
* will cause compile time errors.
|
|
*/
|
|
static __maybe_unused unsigned msecs_ago(unsigned long jiffy_value)
|
|
{
|
|
return jiffies_to_msecs(jiffies - jiffy_value);
|
|
}
|
|
|
|
static void session_fatal_error_work(struct work_struct *work)
|
|
{
|
|
struct vs_session_device *session = container_of(work,
|
|
struct vs_session_device, fatal_error_work);
|
|
|
|
session->transport->vt->reset(session->transport);
|
|
}
|
|
|
|
static void session_fatal_error(struct vs_session_device *session, gfp_t gfp)
|
|
{
|
|
schedule_work(&session->fatal_error_work);
|
|
}
|
|
|
|
/*
|
|
* Service readiness state machine
|
|
*
|
|
* The states are:
|
|
*
|
|
* INIT: Initial state. Service may not be completely configured yet
|
|
* (typically because the protocol hasn't been set); call vs_service_start
|
|
* once configuration is complete. The disable count must be nonzero, and
|
|
* must never reach zero in this state.
|
|
* DISABLED: Service is not permitted to communicate. Non-core services are
|
|
* in this state whenever the core protocol and/or transport state does not
|
|
* allow them to be active; core services are only in this state transiently.
|
|
* The disable count must be nonzero; when it reaches zero, the service
|
|
* transitions to RESET state.
|
|
* RESET: Service drivers are inactive at both ends, but the core service
|
|
* state allows the service to become active. The session will schedule a
|
|
* future transition to READY state when entering this state, but the
|
|
* transition may be delayed to throttle the rate at which resets occur.
|
|
* READY: All core-service and session-layer policy allows the service to
|
|
* communicate; it will become active as soon as it has a protocol driver.
|
|
* ACTIVE: The driver is present and communicating.
|
|
* LOCAL_RESET: We have initiated a reset at this end, but the remote end has
|
|
* not yet acknowledged it. We will enter the RESET state on receiving
|
|
* acknowledgement, unless the disable count is nonzero in which case we
|
|
* will enter DISABLED state.
|
|
* LOCAL_DELETE: As for LOCAL_RESET, but we will enter the DELETED state
|
|
* instead of RESET or DISABLED.
|
|
* DELETED: The service is no longer present on the session; the service
|
|
* device structure may still exist because something is holding a reference
|
|
* to it.
|
|
*
|
|
* The permitted transitions are:
|
|
*
|
|
* From To Trigger
|
|
* INIT DISABLED vs_service_start
|
|
* DISABLED RESET vs_service_enable (disable_count -> 0)
|
|
* RESET READY End of throttle delay (may be 0)
|
|
* READY ACTIVE Latter of probe() and entering READY
|
|
* {READY, ACTIVE}
|
|
* LOCAL_RESET vs_service_reset
|
|
* {READY, ACTIVE, LOCAL_RESET}
|
|
* RESET vs_service_handle_reset (server)
|
|
* RESET DISABLED vs_service_disable (server)
|
|
* {READY, ACTIVE, LOCAL_RESET}
|
|
* DISABLED vs_service_handle_reset (client)
|
|
* {INIT, RESET, READY, ACTIVE, LOCAL_RESET}
|
|
* DISABLED vs_service_disable_noncore
|
|
* {ACTIVE, LOCAL_RESET}
|
|
* LOCAL_DELETE vs_service_delete
|
|
* {INIT, DISABLED, RESET, READY}
|
|
* DELETED vs_service_delete
|
|
* LOCAL_DELETE DELETED vs_service_handle_reset
|
|
* vs_service_disable_noncore
|
|
*
|
|
* See the documentation for the triggers for details.
|
|
*/
|
|
|
|
enum vs_service_readiness {
|
|
VS_SERVICE_INIT,
|
|
VS_SERVICE_DISABLED,
|
|
VS_SERVICE_RESET,
|
|
VS_SERVICE_READY,
|
|
VS_SERVICE_ACTIVE,
|
|
VS_SERVICE_LOCAL_RESET,
|
|
VS_SERVICE_LOCAL_DELETE,
|
|
VS_SERVICE_DELETED,
|
|
};
|
|
|
|
/* Session activation states. */
|
|
enum {
|
|
VS_SESSION_RESET,
|
|
VS_SESSION_ACTIVATE,
|
|
VS_SESSION_ACTIVE,
|
|
};
|
|
|
|
/**
|
|
* vs_service_start - Start a service by moving it from the init state to the
|
|
* disabled state.
|
|
*
|
|
* @service: The service to start.
|
|
*
|
|
* Returns true if the service was started, or false if it was not.
|
|
*/
|
|
bool vs_service_start(struct vs_service_device *service)
|
|
{
|
|
struct vs_session_device *session = vs_service_get_session(service);
|
|
struct vs_session_driver *session_drv =
|
|
to_vs_session_driver(session->dev.driver);
|
|
|
|
WARN_ON(!service->protocol);
|
|
|
|
mutex_lock_nested(&service->ready_lock, service->lock_subclass);
|
|
|
|
if (service->readiness != VS_SERVICE_INIT) {
|
|
if (service->readiness != VS_SERVICE_DELETED)
|
|
dev_err(&service->dev,
|
|
"start called from invalid state %d\n",
|
|
service->readiness);
|
|
mutex_unlock(&service->ready_lock);
|
|
return false;
|
|
}
|
|
|
|
if (service->id != 0 && session_drv->service_added) {
|
|
int err = session_drv->service_added(session, service);
|
|
if (err < 0) {
|
|
dev_err(&session->dev, "Failed to add service %d: %d\n",
|
|
service->id, err);
|
|
mutex_unlock(&service->ready_lock);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
service->readiness = VS_SERVICE_DISABLED;
|
|
service->disable_count = 1;
|
|
service->last_reset_request = jiffies;
|
|
|
|
mutex_unlock(&service->ready_lock);
|
|
|
|
/* Tell userspace about the service. */
|
|
dev_set_uevent_suppress(&service->dev, false);
|
|
kobject_uevent(&service->dev.kobj, KOBJ_ADD);
|
|
|
|
return true;
|
|
}
|
|
EXPORT_SYMBOL_GPL(vs_service_start);
|
|
|
|
static void cancel_pending_rx(struct vs_service_device *service);
|
|
static void queue_ready_work(struct vs_service_device *service);
|
|
|
|
static void __try_start_service(struct vs_service_device *service)
|
|
{
|
|
struct vs_session_device *session = vs_service_get_session(service);
|
|
struct vs_session_driver *session_drv =
|
|
to_vs_session_driver(session->dev.driver);
|
|
struct vs_transport *transport;
|
|
int err;
|
|
struct vs_service_driver *driver;
|
|
|
|
lockdep_assert_held(&service->ready_lock);
|
|
|
|
/* We can't start if the service is not ready yet. */
|
|
if (service->readiness != VS_SERVICE_READY)
|
|
return;
|
|
|
|
/*
|
|
* There should never be anything in the RX queue at this point.
|
|
* If there is, it can seriously confuse the service drivers for
|
|
* no obvious reason, so we check.
|
|
*/
|
|
if (WARN_ON(!list_empty(&service->rx_queue)))
|
|
cancel_pending_rx(service);
|
|
|
|
if (!service->driver_probed) {
|
|
vs_dev_debug(VS_DEBUG_SESSION, session, &service->dev,
|
|
"ready with no driver\n");
|
|
return;
|
|
}
|
|
|
|
/* Prepare the transport to support the service. */
|
|
transport = session->transport;
|
|
err = transport->vt->service_start(transport, service);
|
|
|
|
if (err < 0) {
|
|
/* fatal error attempting to start; reset and try again */
|
|
service->readiness = VS_SERVICE_RESET;
|
|
service->last_reset_request = jiffies;
|
|
service->last_reset = jiffies;
|
|
queue_ready_work(service);
|
|
|
|
return;
|
|
}
|
|
|
|
service->readiness = VS_SERVICE_ACTIVE;
|
|
|
|
driver = to_vs_service_driver(service->dev.driver);
|
|
if (driver->start)
|
|
driver->start(service);
|
|
|
|
if (service->id && session_drv->service_start) {
|
|
err = session_drv->service_start(session, service);
|
|
if (err < 0) {
|
|
dev_err(&session->dev, "Failed to start service %s (%d): %d\n",
|
|
dev_name(&service->dev),
|
|
service->id, err);
|
|
session_fatal_error(session, GFP_KERNEL);
|
|
}
|
|
}
|
|
}
|
|
|
|
static void try_start_service(struct vs_service_device *service)
|
|
{
|
|
mutex_lock_nested(&service->ready_lock, service->lock_subclass);
|
|
|
|
__try_start_service(service);
|
|
|
|
mutex_unlock(&service->ready_lock);
|
|
}
|
|
|
|
static void service_ready_work(struct work_struct *work)
|
|
{
|
|
struct vs_service_device *service = container_of(work,
|
|
struct vs_service_device, ready_work.work);
|
|
struct vs_session_device *session = vs_service_get_session(service);
|
|
|
|
vs_dev_debug(VS_DEBUG_SESSION, session, &service->dev,
|
|
"ready work - last reset request was %u ms ago\n",
|
|
msecs_ago(service->last_reset_request));
|
|
|
|
/*
|
|
* Make sure there's no reset work pending from an earlier driver
|
|
* failure. We should already be inactive at this point, so it's safe
|
|
* to just cancel it.
|
|
*/
|
|
cancel_work_sync(&service->reset_work);
|
|
|
|
mutex_lock_nested(&service->ready_lock, service->lock_subclass);
|
|
|
|
if (service->readiness != VS_SERVICE_RESET) {
|
|
vs_dev_debug(VS_DEBUG_SESSION, session, &service->dev,
|
|
"ready work found readiness of %d, doing nothing\n",
|
|
service->readiness);
|
|
mutex_unlock(&service->ready_lock);
|
|
return;
|
|
}
|
|
|
|
service->readiness = VS_SERVICE_READY;
|
|
/* Record the time at which this happened, for throttling. */
|
|
service->last_ready = jiffies;
|
|
|
|
/* Tell userspace that the service is ready. */
|
|
kobject_uevent(&service->dev.kobj, KOBJ_ONLINE);
|
|
|
|
/* Start the service, if it has a driver attached. */
|
|
__try_start_service(service);
|
|
|
|
mutex_unlock(&service->ready_lock);
|
|
}
|
|
|
|
static int __enable_service(struct vs_service_device *service);
|
|
|
|
/**
|
|
* __reset_service - make a service inactive, and tell its driver, the
|
|
* transport, and possibly the remote partner
|
|
* @service: The service to reset
|
|
* @notify_remote: If true, the partner is notified of the reset
|
|
*
|
|
* This routine is called to make an active service inactive. If the given
|
|
* service is currently active, it drops any queued messages for the service,
|
|
* and then informs the service driver and the transport layer that the
|
|
* service has reset. It sets the service readiness to VS_SERVICE_LOCAL_RESET
|
|
* to indicate that the driver is no longer active.
|
|
*
|
|
* This routine has no effect on services that are not active.
|
|
*
|
|
* The caller must hold the target service's ready lock.
|
|
*/
|
|
static void __reset_service(struct vs_service_device *service,
|
|
bool notify_remote)
|
|
{
|
|
struct vs_session_device *session = vs_service_get_session(service);
|
|
struct vs_session_driver *session_drv =
|
|
to_vs_session_driver(session->dev.driver);
|
|
struct vs_service_driver *driver = NULL;
|
|
struct vs_transport *transport;
|
|
int err;
|
|
|
|
lockdep_assert_held(&service->ready_lock);
|
|
|
|
/* If we're already inactive, there's nothing to do. */
|
|
if (service->readiness != VS_SERVICE_ACTIVE)
|
|
return;
|
|
|
|
service->last_reset = jiffies;
|
|
service->readiness = VS_SERVICE_LOCAL_RESET;
|
|
|
|
cancel_pending_rx(service);
|
|
|
|
if (!WARN_ON(!service->driver_probed))
|
|
driver = to_vs_service_driver(service->dev.driver);
|
|
|
|
if (driver && driver->reset)
|
|
driver->reset(service);
|
|
|
|
wake_up_all(&service->quota_wq);
|
|
|
|
transport = vs_service_get_session(service)->transport;
|
|
|
|
/*
|
|
* Ask the transport to reset the service. If this returns a positive
|
|
* value, we need to leave the service disabled, and the transport
|
|
* will re-enable it. To avoid allowing the disable count to go
|
|
* negative if that re-enable races with this callback returning, we
|
|
* disable the service beforehand and re-enable it if the callback
|
|
* returns zero.
|
|
*/
|
|
service->disable_count++;
|
|
err = transport->vt->service_reset(transport, service);
|
|
if (err < 0) {
|
|
dev_err(&session->dev, "Failed to reset service %d: %d (transport)\n",
|
|
service->id, err);
|
|
session_fatal_error(session, GFP_KERNEL);
|
|
} else if (!err) {
|
|
err = __enable_service(service);
|
|
}
|
|
|
|
if (notify_remote) {
|
|
if (service->id) {
|
|
err = session_drv->service_local_reset(session,
|
|
service);
|
|
if (err == VS_SERVICE_ALREADY_RESET) {
|
|
service->readiness = VS_SERVICE_RESET;
|
|
service->last_reset = jiffies;
|
|
queue_ready_work(service);
|
|
|
|
} else if (err < 0) {
|
|
dev_err(&session->dev, "Failed to reset service %d: %d (session)\n",
|
|
service->id, err);
|
|
session_fatal_error(session, GFP_KERNEL);
|
|
}
|
|
} else {
|
|
session->transport->vt->reset(session->transport);
|
|
}
|
|
}
|
|
|
|
/* Tell userspace that the service is no longer active. */
|
|
kobject_uevent(&service->dev.kobj, KOBJ_OFFLINE);
|
|
}
|
|
|
|
/**
|
|
* reset_service - reset a service and inform the remote partner
|
|
* @service: The service to reset
|
|
*
|
|
* This routine is called when a reset is locally initiated (other than
|
|
* implicitly by a session / core service reset). It bumps the reset request
|
|
* timestamp, acquires the necessary locks, and calls __reset_service.
|
|
*
|
|
* This routine returns with the service ready lock held, to allow the caller
|
|
* to make any other state changes that must be atomic with the service
|
|
* reset.
|
|
*/
|
|
static void reset_service(struct vs_service_device *service)
|
|
__acquires(service->ready_lock)
|
|
{
|
|
service->last_reset_request = jiffies;
|
|
|
|
mutex_lock_nested(&service->ready_lock, service->lock_subclass);
|
|
|
|
__reset_service(service, true);
|
|
}
|
|
|
|
/**
|
|
* vs_service_reset - initiate a service reset
|
|
* @service: the service that is to be reset
|
|
* @caller: the service that is initiating the reset
|
|
*
|
|
* This routine informs the partner that the given service is being reset,
|
|
* then disables and flushes the service's receive queues and resets its
|
|
* driver. The service will be automatically re-enabled once the partner has
|
|
* acknowledged the reset (see vs_session_handle_service_reset, above).
|
|
*
|
|
* If the given service is the core service, this will perform a transport
|
|
* reset, which implicitly resets (on the server side) or destroys (on
|
|
* the client side) every other service on the session.
|
|
*
|
|
* If the given service is already being reset, this has no effect, other
|
|
* than to delay completion of the reset if it is being throttled.
|
|
*
|
|
* For lock safety reasons, a service can only be directly reset by itself,
|
|
* the core service, or the service that created it (which is typically also
|
|
* the core service).
|
|
*
|
|
* A service that wishes to reset itself must not do so while holding its state
|
|
* lock or while running on its own workqueue. In these circumstances, call
|
|
* vs_service_reset_nosync() instead. Note that returning an error code
|
|
* (any negative number) from a driver callback forces a call to
|
|
* vs_service_reset_nosync() and prints an error message.
|
|
*/
|
|
int vs_service_reset(struct vs_service_device *service,
|
|
struct vs_service_device *caller)
|
|
{
|
|
struct vs_session_device *session = vs_service_get_session(service);
|
|
|
|
if (caller != service && caller != service->owner) {
|
|
struct vs_service_device *core_service = session->core_service;
|
|
|
|
WARN_ON(!core_service);
|
|
if (caller != core_service)
|
|
return -EPERM;
|
|
}
|
|
|
|
reset_service(service);
|
|
/* reset_service returns with ready_lock held, but we don't need it */
|
|
mutex_unlock(&service->ready_lock);
|
|
|
|
return 0;
|
|
}
|
|
EXPORT_SYMBOL_GPL(vs_service_reset);
|
|
|
|
/**
|
|
* vs_service_reset_nosync - asynchronously reset a service.
|
|
* @service: the service that is to be reset
|
|
*
|
|
* This routine triggers a reset for the nominated service. It may be called
|
|
* from any context, including interrupt context. It does not wait for the
|
|
* reset to occur, and provides no synchronisation guarantees when called from
|
|
* outside the target service.
|
|
*
|
|
* This is intended only for service drivers that need to reset themselves
|
|
* from a context that would not normally allow it. In other cases, use
|
|
* vs_service_reset.
|
|
*/
|
|
void vs_service_reset_nosync(struct vs_service_device *service)
|
|
{
|
|
service->pending_reset = true;
|
|
schedule_work(&service->reset_work);
|
|
}
|
|
EXPORT_SYMBOL_GPL(vs_service_reset_nosync);
|
|
|
|
static void
|
|
vs_service_remove_sysfs_entries(struct vs_session_device *session,
|
|
struct vs_service_device *service)
|
|
{
|
|
sysfs_remove_link(session->sysfs_entry, service->sysfs_name);
|
|
sysfs_remove_link(&service->dev.kobj, VS_SESSION_SYMLINK_NAME);
|
|
}
|
|
|
|
static void vs_session_release_service_id(struct vs_service_device *service)
|
|
{
|
|
struct vs_session_device *session = vs_service_get_session(service);
|
|
|
|
mutex_lock(&session->service_idr_lock);
|
|
idr_remove(&session->service_idr, service->id);
|
|
mutex_unlock(&session->service_idr_lock);
|
|
vs_dev_debug(VS_DEBUG_SESSION, session, &service->dev,
|
|
"service id deallocated\n");
|
|
}
|
|
|
|
static void destroy_service(struct vs_service_device *service,
|
|
bool notify_remote)
|
|
{
|
|
struct vs_session_device *session = vs_service_get_session(service);
|
|
struct vs_session_driver *session_drv =
|
|
to_vs_session_driver(session->dev.driver);
|
|
struct vs_service_device *core_service __maybe_unused =
|
|
session->core_service;
|
|
int err;
|
|
|
|
lockdep_assert_held(&service->ready_lock);
|
|
WARN_ON(service->readiness != VS_SERVICE_DELETED);
|
|
|
|
/* Notify the core service and transport that the service is gone */
|
|
session->transport->vt->service_remove(session->transport, service);
|
|
if (notify_remote && service->id && session_drv->service_removed) {
|
|
err = session_drv->service_removed(session, service);
|
|
if (err < 0) {
|
|
dev_err(&session->dev,
|
|
"Failed to remove service %d: %d\n",
|
|
service->id, err);
|
|
session_fatal_error(session, GFP_KERNEL);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* At this point the service is guaranteed to be gone on the client
|
|
* side, so we can safely release the service ID.
|
|
*/
|
|
if (session->is_server)
|
|
vs_session_release_service_id(service);
|
|
|
|
/*
|
|
* This guarantees that any concurrent vs_session_get_service() that
|
|
* found the service before we removed it from the IDR will take a
|
|
* reference before we release ours.
|
|
*
|
|
* This similarly protects for_each_[usable_]service().
|
|
*/
|
|
synchronize_rcu();
|
|
|
|
/* Matches device_initialize() in vs_service_register() */
|
|
put_device(&service->dev);
|
|
}
|
|
|
|
/**
|
|
* disable_service - prevent a service becoming ready
|
|
* @service: the service that is to be disabled
|
|
* @force: true if the service is known to be in reset
|
|
*
|
|
* This routine may be called for any inactive service. Once disabled, the
|
|
* service cannot be made ready by the session, and thus cannot become active,
|
|
* until vs_service_enable() is called for it. If multiple calls are made to
|
|
* this function, they must be balanced by vs_service_enable() calls.
|
|
*
|
|
* If the force option is true, then any pending unacknowledged reset will be
|
|
* presumed to have been acknowledged. This is used when the core service is
|
|
* entering reset.
|
|
*
|
|
* This is used by the core service client to prevent the service restarting
|
|
* until the server is ready (i.e., a server_ready message is received); by
|
|
* the session layer to stop all communication while the core service itself
|
|
* is in reset; and by the transport layer when the transport was unable to
|
|
* complete reset of a service in its reset callback (typically because
|
|
* a service had passed message buffers to another Linux subsystem and could
|
|
* not free them immediately).
|
|
*
|
|
* In any case, there is no need for the operation to be signalled in any
|
|
* way, because the service is already in reset. It simply delays future
|
|
* signalling of service readiness.
|
|
*/
|
|
static void disable_service(struct vs_service_device *service, bool force)
|
|
{
|
|
lockdep_assert_held(&service->ready_lock);
|
|
|
|
switch(service->readiness) {
|
|
case VS_SERVICE_INIT:
|
|
case VS_SERVICE_DELETED:
|
|
case VS_SERVICE_LOCAL_DELETE:
|
|
dev_err(&service->dev, "disabled while uninitialised\n");
|
|
break;
|
|
case VS_SERVICE_ACTIVE:
|
|
dev_err(&service->dev, "disabled while active\n");
|
|
break;
|
|
case VS_SERVICE_LOCAL_RESET:
|
|
/*
|
|
* Will go to DISABLED state when reset completes, unless
|
|
* it's being forced (i.e. we're moving to a core protocol
|
|
* state that implies everything else is reset).
|
|
*/
|
|
if (force)
|
|
service->readiness = VS_SERVICE_DISABLED;
|
|
service->disable_count++;
|
|
break;
|
|
default:
|
|
service->readiness = VS_SERVICE_DISABLED;
|
|
service->disable_count++;
|
|
break;
|
|
}
|
|
|
|
cancel_delayed_work(&service->ready_work);
|
|
}
|
|
|
|
static int service_handle_reset(struct vs_session_device *session,
|
|
struct vs_service_device *target, bool disable)
|
|
{
|
|
struct vs_session_driver *session_drv =
|
|
to_vs_session_driver(session->dev.driver);
|
|
int err = 0;
|
|
|
|
mutex_lock_nested(&target->ready_lock, target->lock_subclass);
|
|
|
|
switch (target->readiness) {
|
|
case VS_SERVICE_LOCAL_DELETE:
|
|
target->readiness = VS_SERVICE_DELETED;
|
|
destroy_service(target, true);
|
|
break;
|
|
case VS_SERVICE_ACTIVE:
|
|
/*
|
|
* Reset the service and send a reset notification.
|
|
*
|
|
* We only send notifications for non-core services. This is
|
|
* because core notifies by sending a transport reset, which
|
|
* is what brought us here in the first place. Note that we
|
|
* must already hold the core service state lock iff the
|
|
* target is non-core.
|
|
*/
|
|
target->last_reset_request = jiffies;
|
|
__reset_service(target, target->id != 0);
|
|
/* fall through */
|
|
case VS_SERVICE_LOCAL_RESET:
|
|
target->readiness = target->disable_count ?
|
|
VS_SERVICE_DISABLED : VS_SERVICE_RESET;
|
|
if (disable)
|
|
disable_service(target, false);
|
|
if (target->readiness != VS_SERVICE_DISABLED)
|
|
queue_ready_work(target);
|
|
break;
|
|
case VS_SERVICE_READY:
|
|
/* Tell userspace that the service is no longer ready. */
|
|
kobject_uevent(&target->dev.kobj, KOBJ_OFFLINE);
|
|
/* fall through */
|
|
case VS_SERVICE_RESET:
|
|
/*
|
|
* This can happen for a non-core service if we get a reset
|
|
* request from the server on the client side, after the
|
|
* client has enabled the service but before it is active.
|
|
* Note that the service is already active on the server side
|
|
* at this point. The client's delay may be due to either
|
|
* reset throttling or the absence of a driver.
|
|
*
|
|
* We bump the reset request timestamp, disable the service
|
|
* again, and send back an acknowledgement.
|
|
*/
|
|
if (disable && target->id) {
|
|
target->last_reset_request = jiffies;
|
|
|
|
err = session_drv->service_local_reset(
|
|
session, target);
|
|
if (err < 0) {
|
|
dev_err(&session->dev,
|
|
"Failed to reset service %d; %d\n",
|
|
target->id, err);
|
|
session_fatal_error(session,
|
|
GFP_KERNEL);
|
|
}
|
|
|
|
disable_service(target, false);
|
|
break;
|
|
}
|
|
/* fall through */
|
|
case VS_SERVICE_DISABLED:
|
|
/*
|
|
* This can happen for the core service if we get a reset
|
|
* before the transport has activated, or before the core
|
|
* service has become ready.
|
|
*
|
|
* We bump the reset request timestamp, and disable the
|
|
* service again if the transport had already activated and
|
|
* enabled it.
|
|
*/
|
|
if (disable && !target->id) {
|
|
target->last_reset_request = jiffies;
|
|
|
|
if (target->readiness != VS_SERVICE_DISABLED)
|
|
disable_service(target, false);
|
|
|
|
break;
|
|
}
|
|
/* fall through */
|
|
default:
|
|
dev_warn(&target->dev, "remote reset while inactive (%d)\n",
|
|
target->readiness);
|
|
err = -EPROTO;
|
|
break;
|
|
}
|
|
|
|
mutex_unlock(&target->ready_lock);
|
|
return err;
|
|
}
|
|
|
|
/**
|
|
* vs_service_handle_reset - handle an incoming notification of a reset
|
|
* @session: the session that owns the service
|
|
* @service_id: the ID of the service that is to be reset
|
|
* @disable: if true, the service will not be automatically re-enabled
|
|
*
|
|
* This routine is called by the core service when the remote end notifies us
|
|
* of a non-core service reset. The service must be in ACTIVE, LOCAL_RESET or
|
|
* LOCAL_DELETED state. It must be called with the core service's state lock
|
|
* held.
|
|
*
|
|
* If the service was in ACTIVE state, the core service is called back to send
|
|
* a notification to the other end. If it was in LOCAL_DELETED state, it is
|
|
* unregistered.
|
|
*/
|
|
int vs_service_handle_reset(struct vs_session_device *session,
|
|
vs_service_id_t service_id, bool disable)
|
|
{
|
|
struct vs_service_device *target;
|
|
int ret;
|
|
|
|
if (!service_id)
|
|
return -EINVAL;
|
|
|
|
target = vs_session_get_service(session, service_id);
|
|
if (!target)
|
|
return -ENODEV;
|
|
|
|
ret = service_handle_reset(session, target, disable);
|
|
vs_put_service(target);
|
|
return ret;
|
|
}
|
|
EXPORT_SYMBOL_GPL(vs_service_handle_reset);
|
|
|
|
static int __enable_service(struct vs_service_device *service)
|
|
{
|
|
if (WARN_ON(!service->disable_count))
|
|
return -EINVAL;
|
|
|
|
if (--service->disable_count > 0)
|
|
return 0;
|
|
|
|
/*
|
|
* If the service is still resetting, it can't become ready until the
|
|
* reset completes. If it has been deleted, it will never become
|
|
* ready. In either case, there's nothing more to do.
|
|
*/
|
|
if ((service->readiness == VS_SERVICE_LOCAL_RESET) ||
|
|
(service->readiness == VS_SERVICE_LOCAL_DELETE) ||
|
|
(service->readiness == VS_SERVICE_DELETED))
|
|
return 0;
|
|
|
|
if (WARN_ON(service->readiness != VS_SERVICE_DISABLED))
|
|
return -EINVAL;
|
|
|
|
service->readiness = VS_SERVICE_RESET;
|
|
service->last_reset = jiffies;
|
|
queue_ready_work(service);
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* vs_service_enable - allow a service to become ready
|
|
* @service: the service that is to be enabled
|
|
*
|
|
* Calling this routine for a service permits the session layer to make the
|
|
* service ready. It will do so as soon as any outstanding reset throttling
|
|
* is complete, and will then start the service once it has a driver attached.
|
|
*
|
|
* Services are disabled, requiring a call to this routine to re-enable them:
|
|
* - when first initialised (after vs_service_start),
|
|
* - when reset on the client side by vs_service_handle_reset,
|
|
* - when the transport has delayed completion of a reset, and
|
|
* - when the server-side core protocol is disconnected or reset by
|
|
* vs_session_disable_noncore.
|
|
*/
|
|
int vs_service_enable(struct vs_service_device *service)
|
|
{
|
|
int ret;
|
|
|
|
mutex_lock_nested(&service->ready_lock, service->lock_subclass);
|
|
|
|
ret = __enable_service(service);
|
|
|
|
mutex_unlock(&service->ready_lock);
|
|
|
|
return ret;
|
|
}
|
|
EXPORT_SYMBOL_GPL(vs_service_enable);
|
|
|
|
/*
|
|
* Service work functions
|
|
*/
|
|
static void queue_rx_work(struct vs_service_device *service)
|
|
{
|
|
bool rx_atomic;
|
|
|
|
rx_atomic = vs_service_has_atomic_rx(service);
|
|
vs_dev_debug(VS_DEBUG_SESSION, vs_service_get_session(service),
|
|
&service->dev, "Queuing rx %s\n",
|
|
rx_atomic ? "tasklet (atomic)" : "work (cansleep)");
|
|
|
|
if (rx_atomic)
|
|
tasklet_schedule(&service->rx_tasklet);
|
|
else
|
|
queue_work(service->work_queue, &service->rx_work);
|
|
}
|
|
|
|
static void cancel_pending_rx(struct vs_service_device *service)
|
|
{
|
|
struct vs_mbuf *mbuf;
|
|
|
|
lockdep_assert_held(&service->ready_lock);
|
|
|
|
cancel_work_sync(&service->rx_work);
|
|
tasklet_kill(&service->rx_tasklet);
|
|
|
|
spin_lock_irq(&service->rx_lock);
|
|
while (!list_empty(&service->rx_queue)) {
|
|
mbuf = list_first_entry(&service->rx_queue,
|
|
struct vs_mbuf, queue);
|
|
list_del_init(&mbuf->queue);
|
|
spin_unlock_irq(&service->rx_lock);
|
|
vs_service_free_mbuf(service, mbuf);
|
|
spin_lock_irq(&service->rx_lock);
|
|
}
|
|
service->tx_ready = false;
|
|
spin_unlock_irq(&service->rx_lock);
|
|
}
|
|
|
|
static bool reset_throttle_cooled_off(struct vs_service_device *service);
|
|
static unsigned long reset_cool_off(struct vs_service_device *service);
|
|
|
|
static void service_cooloff_work(struct work_struct *work)
|
|
{
|
|
struct vs_service_device *service = container_of(work,
|
|
struct vs_service_device, cooloff_work.work);
|
|
struct vs_session_device *session = vs_service_get_session(service);
|
|
unsigned long current_time = jiffies, wake_time;
|
|
|
|
mutex_lock_nested(&service->ready_lock, service->lock_subclass);
|
|
|
|
if (reset_throttle_cooled_off(service)) {
|
|
vs_debug(VS_DEBUG_SESSION, session,
|
|
"Reset thrashing cooled off (delay = %u ms, cool off = %u ms, last reset %u ms ago, last reset request was %u ms ago)\n",
|
|
jiffies_to_msecs(service->reset_delay),
|
|
jiffies_to_msecs(reset_cool_off(service)),
|
|
msecs_ago(service->last_reset),
|
|
msecs_ago(service->last_reset_request));
|
|
|
|
service->reset_delay = 0;
|
|
|
|
/*
|
|
* If the service is already in reset, then queue_ready_work
|
|
* has already run and has deferred queuing of the ready_work
|
|
* until cooloff. Schedule the ready work to run immediately.
|
|
*/
|
|
if (service->readiness == VS_SERVICE_RESET)
|
|
schedule_delayed_work(&service->ready_work, 0);
|
|
} else {
|
|
/*
|
|
* This can happen if last_reset_request has been bumped
|
|
* since the cooloff work was first queued. We need to
|
|
* work out how long it is until the service cools off,
|
|
* then reschedule ourselves.
|
|
*/
|
|
wake_time = reset_cool_off(service) +
|
|
service->last_reset_request;
|
|
|
|
WARN_ON(time_after(current_time, wake_time));
|
|
|
|
schedule_delayed_work(&service->cooloff_work,
|
|
wake_time - current_time);
|
|
}
|
|
|
|
mutex_unlock(&service->ready_lock);
|
|
}
|
|
|
|
static void
|
|
service_reset_work(struct work_struct *work)
|
|
{
|
|
struct vs_service_device *service = container_of(work,
|
|
struct vs_service_device, reset_work);
|
|
|
|
service->pending_reset = false;
|
|
|
|
vs_service_reset(service, service);
|
|
}
|
|
|
|
/* Returns true if there are more messages to handle */
|
|
static bool
|
|
dequeue_and_handle_received_message(struct vs_service_device *service)
|
|
{
|
|
struct vs_service_driver *driver =
|
|
to_vs_service_driver(service->dev.driver);
|
|
struct vs_session_device *session = vs_service_get_session(service);
|
|
const struct vs_transport_vtable *vt = session->transport->vt;
|
|
struct vs_service_stats *stats = &service->stats;
|
|
struct vs_mbuf *mbuf;
|
|
size_t size;
|
|
int ret;
|
|
|
|
/* Don't do rx work unless the service is active */
|
|
if (service->readiness != VS_SERVICE_ACTIVE)
|
|
return false;
|
|
|
|
/* Atomically take an item from the queue */
|
|
spin_lock_irq(&service->rx_lock);
|
|
if (!list_empty(&service->rx_queue)) {
|
|
mbuf = list_first_entry(&service->rx_queue, struct vs_mbuf,
|
|
queue);
|
|
list_del_init(&mbuf->queue);
|
|
spin_unlock_irq(&service->rx_lock);
|
|
size = vt->mbuf_size(mbuf);
|
|
|
|
/*
|
|
* Call the message handler for the service. The service's
|
|
* message handler is responsible for freeing the mbuf when it
|
|
* is done with it.
|
|
*/
|
|
ret = driver->receive(service, mbuf);
|
|
if (ret < 0) {
|
|
atomic_inc(&service->stats.recv_failures);
|
|
dev_err(&service->dev,
|
|
"receive returned %d; resetting service\n",
|
|
ret);
|
|
vs_service_reset_nosync(service);
|
|
return false;
|
|
} else {
|
|
atomic_add(size, &service->stats.recv_bytes);
|
|
atomic_inc(&service->stats.recv_mbufs);
|
|
}
|
|
|
|
} else if (service->tx_ready) {
|
|
service->tx_ready = false;
|
|
spin_unlock_irq(&service->rx_lock);
|
|
|
|
/*
|
|
* Update the tx_ready stats accounting and then call the
|
|
* service's tx_ready handler.
|
|
*/
|
|
atomic_inc(&stats->nr_tx_ready);
|
|
if (atomic_read(&stats->nr_over_quota) > 0) {
|
|
int total;
|
|
|
|
total = atomic_add_return(jiffies_to_msecs(jiffies -
|
|
stats->over_quota_time),
|
|
&stats->over_quota_time_total);
|
|
atomic_set(&stats->over_quota_time_avg, total /
|
|
atomic_read(&stats->nr_over_quota));
|
|
}
|
|
atomic_set(&service->is_over_quota, 0);
|
|
|
|
/*
|
|
* Note that a service's quota may reduce at any point, even
|
|
* during the tx_ready handler. This is important if a service
|
|
* has an ordered list of pending messages to send. If a
|
|
* message fails to send from the tx_ready handler due to
|
|
* over-quota then subsequent messages in the same handler may
|
|
* send successfully. To avoid sending messages in the
|
|
* incorrect order the service's tx_ready handler should
|
|
* return immediately if a message fails to send.
|
|
*/
|
|
ret = driver->tx_ready(service);
|
|
if (ret < 0) {
|
|
dev_err(&service->dev,
|
|
"tx_ready returned %d; resetting service\n",
|
|
ret);
|
|
vs_service_reset_nosync(service);
|
|
return false;
|
|
}
|
|
} else {
|
|
spin_unlock_irq(&service->rx_lock);
|
|
}
|
|
|
|
/*
|
|
* There's no need to lock for this list_empty: if we race
|
|
* with a msg enqueue, we'll be rescheduled by the other side,
|
|
* and if we race with a dequeue, we'll just do nothing when
|
|
* we run (or will be cancelled before we run).
|
|
*/
|
|
return !list_empty(&service->rx_queue) || service->tx_ready;
|
|
}
|
|
|
|
static void service_rx_tasklet(unsigned long data)
|
|
{
|
|
struct vs_service_device *service = (struct vs_service_device *)data;
|
|
bool resched;
|
|
|
|
/*
|
|
* There is no need to acquire the state spinlock or mutex here,
|
|
* because this tasklet is disabled when the lock is held. These
|
|
* are annotations for sparse and lockdep, respectively.
|
|
*
|
|
* We can't annotate the implicit mutex acquire because lockdep gets
|
|
* upset about inconsistent softirq states.
|
|
*/
|
|
__acquire(service);
|
|
spin_acquire(&service->state_spinlock.dep_map, 0, 0, _THIS_IP_);
|
|
|
|
resched = dequeue_and_handle_received_message(service);
|
|
|
|
if (resched)
|
|
tasklet_schedule(&service->rx_tasklet);
|
|
|
|
spin_release(&service->state_spinlock.dep_map, 0, _THIS_IP_);
|
|
__release(service);
|
|
}
|
|
|
|
static void service_rx_work(struct work_struct *work)
|
|
{
|
|
struct vs_service_device *service = container_of(work,
|
|
struct vs_service_device, rx_work);
|
|
bool requeue;
|
|
|
|
/*
|
|
* We must acquire the state mutex here to protect services that
|
|
* are using vs_service_state_lock().
|
|
*
|
|
* There is no need to acquire the spinlock, which is never used in
|
|
* drivers with task context receive handlers.
|
|
*/
|
|
vs_service_state_lock(service);
|
|
|
|
requeue = dequeue_and_handle_received_message(service);
|
|
|
|
vs_service_state_unlock(service);
|
|
|
|
if (requeue)
|
|
queue_work(service->work_queue, work);
|
|
}
|
|
|
|
/*
|
|
* Service sysfs statistics counters. These files are all atomic_t, and
|
|
* read only, so we use a generator macro to avoid code duplication.
|
|
*/
|
|
#define service_stat_attr(__name) \
|
|
static ssize_t __name##_show(struct device *dev, \
|
|
struct device_attribute *attr, char *buf) \
|
|
{ \
|
|
struct vs_service_device *service = \
|
|
to_vs_service_device(dev); \
|
|
\
|
|
return scnprintf(buf, PAGE_SIZE, "%u\n", \
|
|
atomic_read(&service->stats.__name)); \
|
|
} \
|
|
static DEVICE_ATTR_RO(__name)
|
|
|
|
service_stat_attr(sent_mbufs);
|
|
service_stat_attr(sent_bytes);
|
|
service_stat_attr(recv_mbufs);
|
|
service_stat_attr(recv_bytes);
|
|
service_stat_attr(nr_over_quota);
|
|
service_stat_attr(nr_tx_ready);
|
|
service_stat_attr(over_quota_time_total);
|
|
service_stat_attr(over_quota_time_avg);
|
|
|
|
static struct attribute *service_stat_dev_attrs[] = {
|
|
&dev_attr_sent_mbufs.attr,
|
|
&dev_attr_sent_bytes.attr,
|
|
&dev_attr_recv_mbufs.attr,
|
|
&dev_attr_recv_bytes.attr,
|
|
&dev_attr_nr_over_quota.attr,
|
|
&dev_attr_nr_tx_ready.attr,
|
|
&dev_attr_over_quota_time_total.attr,
|
|
&dev_attr_over_quota_time_avg.attr,
|
|
NULL,
|
|
};
|
|
|
|
static const struct attribute_group service_stat_attributes = {
|
|
.name = "stats",
|
|
.attrs = service_stat_dev_attrs,
|
|
};
|
|
|
|
static void delete_service(struct vs_service_device *service)
|
|
{
|
|
struct vs_session_device *session = vs_service_get_session(service);
|
|
bool notify_on_destroy = true;
|
|
|
|
/* FIXME: Jira ticket SDK-3495 - philipd. */
|
|
/* This should be the caller's responsibility */
|
|
vs_get_service(service);
|
|
|
|
mutex_lock_nested(&service->ready_lock, service->lock_subclass);
|
|
|
|
/*
|
|
* If we're on the client side, the service should already have been
|
|
* disabled at this point.
|
|
*/
|
|
WARN_ON(service->id != 0 && !session->is_server &&
|
|
service->readiness != VS_SERVICE_DISABLED &&
|
|
service->readiness != VS_SERVICE_DELETED);
|
|
|
|
/*
|
|
* Make sure the service is not active, and notify the remote end if
|
|
* it needs to be reset. Note that we already hold the core service
|
|
* state lock iff this is a non-core service.
|
|
*/
|
|
__reset_service(service, true);
|
|
|
|
/*
|
|
* If the remote end is aware that the service is inactive, we can
|
|
* delete right away; otherwise we need to wait for a notification
|
|
* that the service has reset.
|
|
*/
|
|
switch (service->readiness) {
|
|
case VS_SERVICE_LOCAL_DELETE:
|
|
case VS_SERVICE_DELETED:
|
|
/* Nothing to do here */
|
|
mutex_unlock(&service->ready_lock);
|
|
vs_put_service(service);
|
|
return;
|
|
case VS_SERVICE_ACTIVE:
|
|
BUG();
|
|
break;
|
|
case VS_SERVICE_LOCAL_RESET:
|
|
service->readiness = VS_SERVICE_LOCAL_DELETE;
|
|
break;
|
|
case VS_SERVICE_INIT:
|
|
notify_on_destroy = false;
|
|
/* Fall through */
|
|
default:
|
|
service->readiness = VS_SERVICE_DELETED;
|
|
destroy_service(service, notify_on_destroy);
|
|
break;
|
|
}
|
|
|
|
mutex_unlock(&service->ready_lock);
|
|
|
|
/*
|
|
* Remove service syslink from
|
|
* sys/vservices/(<server>/<client>)-sessions/ directory
|
|
*/
|
|
vs_service_remove_sysfs_entries(session, service);
|
|
|
|
sysfs_remove_group(&service->dev.kobj, &service_stat_attributes);
|
|
|
|
/*
|
|
* On the client-side we need to release the service id as soon as
|
|
* the service is deleted. Otherwise the server may attempt to create
|
|
* a new service with this id.
|
|
*/
|
|
if (!session->is_server)
|
|
vs_session_release_service_id(service);
|
|
|
|
device_del(&service->dev);
|
|
vs_put_service(service);
|
|
}
|
|
|
|
/**
|
|
* vs_service_delete - deactivate and start removing a service device
|
|
* @service: the service to delete
|
|
* @caller: the service initiating deletion
|
|
*
|
|
* Services may only be deleted by their owner (on the server side), or by the
|
|
* core service. This function must not be called for the core service.
|
|
*/
|
|
int vs_service_delete(struct vs_service_device *service,
|
|
struct vs_service_device *caller)
|
|
{
|
|
struct vs_session_device *session =
|
|
vs_service_get_session(service);
|
|
struct vs_service_device *core_service = session->core_service;
|
|
|
|
if (WARN_ON(!core_service))
|
|
return -ENODEV;
|
|
|
|
if (!service->id)
|
|
return -EINVAL;
|
|
|
|
if (caller != service->owner && caller != core_service)
|
|
return -EPERM;
|
|
|
|
delete_service(service);
|
|
|
|
return 0;
|
|
}
|
|
EXPORT_SYMBOL_GPL(vs_service_delete);
|
|
|
|
/**
|
|
* vs_service_handle_delete - deactivate and start removing a service device
|
|
* @service: the service to delete
|
|
*
|
|
* This is a variant of vs_service_delete which must only be called by the
|
|
* core service. It is used by the core service client when a service_removed
|
|
* message is received.
|
|
*/
|
|
int vs_service_handle_delete(struct vs_service_device *service)
|
|
{
|
|
struct vs_session_device *session __maybe_unused =
|
|
vs_service_get_session(service);
|
|
struct vs_service_device *core_service __maybe_unused =
|
|
session->core_service;
|
|
|
|
lockdep_assert_held(&core_service->state_mutex);
|
|
|
|
delete_service(service);
|
|
|
|
return 0;
|
|
}
|
|
EXPORT_SYMBOL_GPL(vs_service_handle_delete);
|
|
|
|
static void service_cleanup_work(struct work_struct *work)
|
|
{
|
|
struct vs_service_device *service = container_of(work,
|
|
struct vs_service_device, cleanup_work);
|
|
struct vs_session_device *session = vs_service_get_session(service);
|
|
|
|
vs_dev_debug(VS_DEBUG_SESSION, session, &service->dev, "cleanup\n");
|
|
|
|
if (service->owner)
|
|
vs_put_service(service->owner);
|
|
|
|
/* Put our reference to the session */
|
|
if (service->dev.parent)
|
|
put_device(service->dev.parent);
|
|
|
|
tasklet_kill(&service->rx_tasklet);
|
|
cancel_work_sync(&service->rx_work);
|
|
cancel_delayed_work_sync(&service->cooloff_work);
|
|
cancel_delayed_work_sync(&service->ready_work);
|
|
cancel_work_sync(&service->reset_work);
|
|
|
|
if (service->work_queue)
|
|
destroy_workqueue(service->work_queue);
|
|
|
|
kfree(service->sysfs_name);
|
|
kfree(service->name);
|
|
kfree(service->protocol);
|
|
kfree(service);
|
|
}
|
|
|
|
static void vs_service_release(struct device *dev)
|
|
{
|
|
struct vs_service_device *service = to_vs_service_device(dev);
|
|
|
|
vs_dev_debug(VS_DEBUG_SESSION, vs_service_get_session(service),
|
|
&service->dev, "release\n");
|
|
|
|
/*
|
|
* We need to defer cleanup to avoid a circular dependency between the
|
|
* core service's state lock (which can be held at this point, on the
|
|
* client side) and any non-core service's reset work (which we must
|
|
* cancel here, and which acquires the core service state lock).
|
|
*/
|
|
schedule_work(&service->cleanup_work);
|
|
}
|
|
|
|
static int service_add_idr(struct vs_session_device *session,
|
|
struct vs_service_device *service, vs_service_id_t service_id)
|
|
{
|
|
int start, end, id;
|
|
|
|
if (service_id == VS_SERVICE_AUTO_ALLOCATE_ID) {
|
|
start = 1;
|
|
end = VS_MAX_SERVICES;
|
|
} else {
|
|
start = service_id;
|
|
end = service_id + 1;
|
|
}
|
|
|
|
mutex_lock(&session->service_idr_lock);
|
|
id = idr_alloc(&session->service_idr, service, start, end,
|
|
GFP_KERNEL);
|
|
mutex_unlock(&session->service_idr_lock);
|
|
|
|
if (id == -ENOSPC)
|
|
return -EBUSY;
|
|
else if (id < 0)
|
|
return id;
|
|
|
|
service->id = id;
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
vs_service_create_sysfs_entries(struct vs_session_device *session,
|
|
struct vs_service_device *service, vs_service_id_t id)
|
|
{
|
|
int ret;
|
|
char *sysfs_name, *c;
|
|
|
|
/* Add a symlink to session device inside service device sysfs */
|
|
ret = sysfs_create_link(&service->dev.kobj, &session->dev.kobj,
|
|
VS_SESSION_SYMLINK_NAME);
|
|
if (ret) {
|
|
dev_err(&service->dev, "Error %d creating session symlink\n",
|
|
ret);
|
|
goto fail;
|
|
}
|
|
|
|
/* Get the length of the string for sysfs dir */
|
|
sysfs_name = kasprintf(GFP_KERNEL, "%s:%d", service->name, id);
|
|
if (!sysfs_name) {
|
|
ret = -ENOMEM;
|
|
goto fail_session_link;
|
|
}
|
|
|
|
/*
|
|
* We dont want to create symlinks with /'s which could get interpreted
|
|
* as another directory so replace all /'s with !'s
|
|
*/
|
|
while ((c = strchr(sysfs_name, '/')))
|
|
*c = '!';
|
|
ret = sysfs_create_link(session->sysfs_entry, &service->dev.kobj,
|
|
sysfs_name);
|
|
if (ret)
|
|
goto fail_free_sysfs_name;
|
|
|
|
service->sysfs_name = sysfs_name;
|
|
|
|
return 0;
|
|
|
|
fail_free_sysfs_name:
|
|
kfree(sysfs_name);
|
|
fail_session_link:
|
|
sysfs_remove_link(&service->dev.kobj, VS_SESSION_SYMLINK_NAME);
|
|
fail:
|
|
return ret;
|
|
}
|
|
|
|
/**
|
|
* vs_service_register - create and register a new vs_service_device
|
|
* @session: the session device that is the parent of the service
|
|
* @owner: the service responsible for managing the new service
|
|
* @service_id: the ID of the new service
|
|
* @name: the name of the new service
|
|
* @protocol: the protocol for the new service
|
|
* @plat_data: value to be assigned to (struct device *)->platform_data
|
|
*
|
|
* This function should only be called by a session driver that is bound to
|
|
* the given session.
|
|
*
|
|
* The given service_id must not have been passed to a prior successful
|
|
* vs_service_register call, unless the service ID has since been freed by a
|
|
* call to the session driver's service_removed callback.
|
|
*
|
|
* The core service state lock must not be held while calling this function.
|
|
*/
|
|
struct vs_service_device *vs_service_register(struct vs_session_device *session,
|
|
struct vs_service_device *owner, vs_service_id_t service_id,
|
|
const char *protocol, const char *name, const void *plat_data)
|
|
{
|
|
struct vs_service_device *service;
|
|
struct vs_session_driver *session_drv;
|
|
int ret = -EIO;
|
|
char *c;
|
|
|
|
if (service_id && !owner) {
|
|
dev_err(&session->dev, "Non-core service must have an owner\n");
|
|
ret = -EINVAL;
|
|
goto fail;
|
|
} else if (!service_id && owner) {
|
|
dev_err(&session->dev, "Core service must not have an owner\n");
|
|
ret = -EINVAL;
|
|
goto fail;
|
|
}
|
|
|
|
if (!session->dev.driver)
|
|
goto fail;
|
|
|
|
session_drv = to_vs_session_driver(session->dev.driver);
|
|
|
|
service = kzalloc(sizeof(*service), GFP_KERNEL);
|
|
if (!service) {
|
|
ret = -ENOMEM;
|
|
goto fail;
|
|
}
|
|
|
|
INIT_LIST_HEAD(&service->rx_queue);
|
|
INIT_WORK(&service->rx_work, service_rx_work);
|
|
INIT_WORK(&service->reset_work, service_reset_work);
|
|
INIT_DELAYED_WORK(&service->ready_work, service_ready_work);
|
|
INIT_DELAYED_WORK(&service->cooloff_work, service_cooloff_work);
|
|
INIT_WORK(&service->cleanup_work, service_cleanup_work);
|
|
spin_lock_init(&service->rx_lock);
|
|
init_waitqueue_head(&service->quota_wq);
|
|
|
|
service->owner = vs_get_service(owner);
|
|
|
|
service->readiness = VS_SERVICE_INIT;
|
|
mutex_init(&service->ready_lock);
|
|
service->driver_probed = false;
|
|
|
|
/*
|
|
* Service state locks - A service is only allowed to use one of these
|
|
*/
|
|
spin_lock_init(&service->state_spinlock);
|
|
mutex_init(&service->state_mutex);
|
|
#ifdef CONFIG_VSERVICES_LOCK_DEBUG
|
|
service->state_spinlock_used = false;
|
|
service->state_mutex_used = false;
|
|
#endif
|
|
|
|
/* Lock ordering
|
|
*
|
|
* The dependency order for the various service locks is as follows:
|
|
*
|
|
* cooloff_work
|
|
* reset_work
|
|
* ready_work
|
|
* ready_lock/0
|
|
* rx_work/0
|
|
* state_mutex/0
|
|
* ready_lock/1
|
|
* ...
|
|
* state_mutex/n
|
|
* state_spinlock
|
|
*
|
|
* The subclass is the service's rank in the hierarchy of
|
|
* service ownership. This results in core having subclass 0 on
|
|
* server-side and 1 on client-side. Services directly created
|
|
* by the core will have a lock subclass value of 2 for
|
|
* servers, 3 for clients. Services created by non-core
|
|
* services will have a lock subclass value of x + 1, where x
|
|
* is the lock subclass of the creator service. (e.g servers
|
|
* will have even numbered lock subclasses, clients will have
|
|
* odd numbered lock subclasses).
|
|
*
|
|
* If a service driver has any additional locks for protecting
|
|
* internal state, they will generally fit between state_mutex/n and
|
|
* ready_lock/n+1 on this list. For the core service, this applies to
|
|
* the session lock.
|
|
*/
|
|
|
|
if (owner)
|
|
service->lock_subclass = owner->lock_subclass + 2;
|
|
else
|
|
service->lock_subclass = session->is_server ? 0 : 1;
|
|
|
|
#ifdef CONFIG_LOCKDEP
|
|
if (service->lock_subclass >= MAX_LOCKDEP_SUBCLASSES) {
|
|
dev_warn(&session->dev, "Owner hierarchy is too deep, lockdep will fail\n");
|
|
} else {
|
|
/*
|
|
* We need to set the default subclass for the rx work,
|
|
* because the workqueue API doesn't (and can't) provide
|
|
* anything like lock_nested() for it.
|
|
*/
|
|
|
|
struct lock_class_key *key = service->rx_work.lockdep_map.key;
|
|
|
|
/*
|
|
* We can't use the lockdep_set_class() macro because the
|
|
* work's lockdep map is called .lockdep_map instead of
|
|
* .dep_map.
|
|
*/
|
|
lockdep_init_map(&service->rx_work.lockdep_map,
|
|
"&service->rx_work", key,
|
|
service->lock_subclass);
|
|
}
|
|
#endif
|
|
|
|
/*
|
|
* Copy the protocol and name. Remove any leading or trailing
|
|
* whitespace characters (including newlines) since the strings
|
|
* may have been passed via sysfs files.
|
|
*/
|
|
if (protocol) {
|
|
service->protocol = kstrdup(protocol, GFP_KERNEL);
|
|
if (!service->protocol) {
|
|
ret = -ENOMEM;
|
|
goto fail_copy_protocol;
|
|
}
|
|
c = strim(service->protocol);
|
|
if (c != service->protocol)
|
|
memmove(service->protocol, c,
|
|
strlen(service->protocol) + 1);
|
|
}
|
|
|
|
service->name = kstrdup(name, GFP_KERNEL);
|
|
if (!service->name) {
|
|
ret = -ENOMEM;
|
|
goto fail_copy_name;
|
|
}
|
|
c = strim(service->name);
|
|
if (c != service->name)
|
|
memmove(service->name, c, strlen(service->name) + 1);
|
|
|
|
service->is_server = session_drv->is_server;
|
|
|
|
/* Grab a reference to the session we are on */
|
|
service->dev.parent = get_device(&session->dev);
|
|
service->dev.bus = session_drv->service_bus;
|
|
service->dev.release = vs_service_release;
|
|
|
|
service->last_reset = 0;
|
|
service->last_reset_request = 0;
|
|
service->last_ready = 0;
|
|
service->reset_delay = 0;
|
|
|
|
device_initialize(&service->dev);
|
|
service->dev.platform_data = (void *)plat_data;
|
|
|
|
ret = service_add_idr(session, service, service_id);
|
|
if (ret)
|
|
goto fail_add_idr;
|
|
|
|
#ifdef CONFIG_VSERVICES_NAMED_DEVICE
|
|
/* Integrate session and service names in vservice devnodes */
|
|
dev_set_name(&service->dev, "vservice-%s:%s:%s:%d:%d",
|
|
session->is_server ? "server" : "client",
|
|
session->name, service->name,
|
|
session->session_num, service->id);
|
|
#else
|
|
dev_set_name(&service->dev, "%s:%d", dev_name(&session->dev),
|
|
service->id);
|
|
#endif
|
|
|
|
#ifdef CONFIG_VSERVICES_CHAR_DEV
|
|
if (service->id > 0)
|
|
service->dev.devt = MKDEV(vservices_cdev_major,
|
|
(session->session_num * VS_MAX_SERVICES) +
|
|
service->id);
|
|
#endif
|
|
|
|
service->work_queue = vs_create_workqueue(dev_name(&service->dev));
|
|
if (!service->work_queue) {
|
|
ret = -ENOMEM;
|
|
goto fail_create_workqueue;
|
|
}
|
|
|
|
tasklet_init(&service->rx_tasklet, service_rx_tasklet,
|
|
(unsigned long)service);
|
|
|
|
/*
|
|
* If this is the core service, set the core service pointer in the
|
|
* session.
|
|
*/
|
|
if (service->id == 0) {
|
|
mutex_lock(&session->service_idr_lock);
|
|
if (session->core_service) {
|
|
ret = -EEXIST;
|
|
mutex_unlock(&session->service_idr_lock);
|
|
goto fail_become_core;
|
|
}
|
|
|
|
/* Put in vs_session_bus_remove() */
|
|
session->core_service = vs_get_service(service);
|
|
mutex_unlock(&session->service_idr_lock);
|
|
}
|
|
|
|
/* Notify the transport */
|
|
ret = session->transport->vt->service_add(session->transport, service);
|
|
if (ret) {
|
|
dev_err(&session->dev,
|
|
"Failed to add service %d (%s:%s) to transport: %d\n",
|
|
service->id, service->name,
|
|
service->protocol, ret);
|
|
goto fail_transport_add;
|
|
}
|
|
|
|
/* Delay uevent until vs_service_start(). */
|
|
dev_set_uevent_suppress(&service->dev, true);
|
|
|
|
ret = device_add(&service->dev);
|
|
if (ret)
|
|
goto fail_device_add;
|
|
|
|
/* Create the service statistics sysfs group */
|
|
ret = sysfs_create_group(&service->dev.kobj, &service_stat_attributes);
|
|
if (ret)
|
|
goto fail_sysfs_create_group;
|
|
|
|
/* Create additional sysfs files */
|
|
ret = vs_service_create_sysfs_entries(session, service, service->id);
|
|
if (ret)
|
|
goto fail_sysfs_add_entries;
|
|
|
|
return service;
|
|
|
|
fail_sysfs_add_entries:
|
|
sysfs_remove_group(&service->dev.kobj, &service_stat_attributes);
|
|
fail_sysfs_create_group:
|
|
device_del(&service->dev);
|
|
fail_device_add:
|
|
session->transport->vt->service_remove(session->transport, service);
|
|
fail_transport_add:
|
|
if (service->id == 0) {
|
|
session->core_service = NULL;
|
|
vs_put_service(service);
|
|
}
|
|
fail_become_core:
|
|
fail_create_workqueue:
|
|
vs_session_release_service_id(service);
|
|
fail_add_idr:
|
|
/*
|
|
* device_initialize() has been called, so we must call put_device()
|
|
* and let vs_service_release() handle the rest of the cleanup.
|
|
*/
|
|
put_device(&service->dev);
|
|
return ERR_PTR(ret);
|
|
|
|
fail_copy_name:
|
|
if (service->protocol)
|
|
kfree(service->protocol);
|
|
fail_copy_protocol:
|
|
kfree(service);
|
|
fail:
|
|
return ERR_PTR(ret);
|
|
}
|
|
EXPORT_SYMBOL_GPL(vs_service_register);
|
|
|
|
/**
|
|
* vs_session_get_service - Look up a service by ID on a session and get
|
|
* a reference to it. The caller must call vs_put_service when it is finished
|
|
* with the service.
|
|
*
|
|
* @session: The session to search for the service on
|
|
* @service_id: ID of the service to find
|
|
*/
|
|
struct vs_service_device *
|
|
vs_session_get_service(struct vs_session_device *session,
|
|
vs_service_id_t service_id)
|
|
{
|
|
struct vs_service_device *service;
|
|
|
|
if (!session)
|
|
return NULL;
|
|
|
|
rcu_read_lock();
|
|
service = idr_find(&session->service_idr, service_id);
|
|
if (!service) {
|
|
rcu_read_unlock();
|
|
return NULL;
|
|
}
|
|
vs_get_service(service);
|
|
rcu_read_unlock();
|
|
|
|
return service;
|
|
}
|
|
EXPORT_SYMBOL_GPL(vs_session_get_service);
|
|
|
|
/**
|
|
* __for_each_service - Iterate over all non-core services on a session.
|
|
*
|
|
* @session: Session to iterate services on
|
|
* @func: Callback function for each iterated service
|
|
*
|
|
* Iterate over all services on a session, excluding the core service, and
|
|
* call a callback function on each.
|
|
*/
|
|
static void __for_each_service(struct vs_session_device *session,
|
|
void (*func)(struct vs_service_device *))
|
|
{
|
|
struct vs_service_device *service;
|
|
int id;
|
|
|
|
for (id = 1; ; id++) {
|
|
rcu_read_lock();
|
|
service = idr_get_next(&session->service_idr, &id);
|
|
if (!service) {
|
|
rcu_read_unlock();
|
|
break;
|
|
}
|
|
vs_get_service(service);
|
|
rcu_read_unlock();
|
|
|
|
func(service);
|
|
vs_put_service(service);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* vs_session_delete_noncore - immediately delete all non-core services
|
|
* @session: the session whose services are to be deleted
|
|
*
|
|
* This function disables and deletes all non-core services without notifying
|
|
* the core service. It must only be called by the core service, with its state
|
|
* lock held. It is used when the core service client disconnects or
|
|
* resets, and when the core service server has its driver removed.
|
|
*/
|
|
void vs_session_delete_noncore(struct vs_session_device *session)
|
|
{
|
|
struct vs_service_device *core_service __maybe_unused =
|
|
session->core_service;
|
|
|
|
lockdep_assert_held(&core_service->state_mutex);
|
|
|
|
vs_session_disable_noncore(session);
|
|
|
|
__for_each_service(session, delete_service);
|
|
}
|
|
EXPORT_SYMBOL_GPL(vs_session_delete_noncore);
|
|
|
|
/**
|
|
* vs_session_for_each_service - Iterate over all initialised and non-deleted
|
|
* non-core services on a session.
|
|
*
|
|
* @session: Session to iterate services on
|
|
* @func: Callback function for each iterated service
|
|
* @data: Extra data to pass to the callback
|
|
*
|
|
* Iterate over all services on a session, excluding the core service and any
|
|
* service that has been deleted or has not yet had vs_service_start() called,
|
|
* and call a callback function on each. The callback function is called with
|
|
* the service's ready lock held.
|
|
*/
|
|
void vs_session_for_each_service(struct vs_session_device *session,
|
|
void (*func)(struct vs_service_device *, void *), void *data)
|
|
{
|
|
struct vs_service_device *service;
|
|
int id;
|
|
|
|
for (id = 1; ; id++) {
|
|
rcu_read_lock();
|
|
service = idr_get_next(&session->service_idr, &id);
|
|
if (!service) {
|
|
rcu_read_unlock();
|
|
break;
|
|
}
|
|
vs_get_service(service);
|
|
rcu_read_unlock();
|
|
|
|
mutex_lock_nested(&service->ready_lock, service->lock_subclass);
|
|
|
|
if (service->readiness != VS_SERVICE_LOCAL_DELETE &&
|
|
service->readiness != VS_SERVICE_DELETED &&
|
|
service->readiness != VS_SERVICE_INIT)
|
|
func(service, data);
|
|
|
|
mutex_unlock(&service->ready_lock);
|
|
vs_put_service(service);
|
|
}
|
|
}
|
|
|
|
static void force_disable_service(struct vs_service_device *service,
|
|
void *unused)
|
|
{
|
|
lockdep_assert_held(&service->ready_lock);
|
|
|
|
if (service->readiness == VS_SERVICE_ACTIVE)
|
|
__reset_service(service, false);
|
|
|
|
disable_service(service, true);
|
|
}
|
|
|
|
/**
|
|
* vs_session_disable_noncore - immediately disable all non-core services
|
|
* @session: the session whose services are to be disabled
|
|
*
|
|
* This function must be called by the core service driver to disable all
|
|
* services, whenever it resets or is otherwise disconnected. It is called
|
|
* directly by the server-side core service, and by the client-side core
|
|
* service via vs_session_delete_noncore().
|
|
*/
|
|
void vs_session_disable_noncore(struct vs_session_device *session)
|
|
{
|
|
vs_session_for_each_service(session, force_disable_service, NULL);
|
|
}
|
|
EXPORT_SYMBOL_GPL(vs_session_disable_noncore);
|
|
|
|
static void try_enable_service(struct vs_service_device *service, void *unused)
|
|
{
|
|
lockdep_assert_held(&service->ready_lock);
|
|
|
|
__enable_service(service);
|
|
}
|
|
|
|
/**
|
|
* vs_session_enable_noncore - enable all disabled non-core services
|
|
* @session: the session whose services are to be enabled
|
|
*
|
|
* This function is called by the core server driver to enable all services
|
|
* when the core client connects.
|
|
*/
|
|
void vs_session_enable_noncore(struct vs_session_device *session)
|
|
{
|
|
vs_session_for_each_service(session, try_enable_service, NULL);
|
|
}
|
|
EXPORT_SYMBOL_GPL(vs_session_enable_noncore);
|
|
|
|
/**
|
|
* vs_session_handle_message - process an incoming message from a transport
|
|
* @session: the session that is receiving the message
|
|
* @mbuf: a buffer containing the message payload
|
|
* @service_id: the id of the service that the message was addressed to
|
|
*
|
|
* This routine will return 0 if the buffer was accepted, or a negative value
|
|
* otherwise. In the latter case the caller should free the buffer. If the
|
|
* error is fatal, this routine will reset the service.
|
|
*
|
|
* This routine may be called from interrupt context.
|
|
*
|
|
* The caller must always serialise calls to this function relative to
|
|
* vs_session_handle_reset and vs_session_handle_activate. We don't do this
|
|
* internally, to avoid having to disable interrupts when called from task
|
|
* context.
|
|
*/
|
|
int vs_session_handle_message(struct vs_session_device *session,
|
|
struct vs_mbuf *mbuf, vs_service_id_t service_id)
|
|
{
|
|
struct vs_service_device *service;
|
|
struct vs_transport *transport;
|
|
unsigned long flags;
|
|
|
|
transport = session->transport;
|
|
|
|
service = vs_session_get_service(session, service_id);
|
|
if (!service) {
|
|
dev_err(&session->dev, "message for unknown service %d\n",
|
|
service_id);
|
|
session_fatal_error(session, GFP_ATOMIC);
|
|
return -ENOTCONN;
|
|
}
|
|
|
|
/*
|
|
* Take the rx lock before checking service readiness. This guarantees
|
|
* that if __reset_service() has just made the service inactive, we
|
|
* either see it and don't enqueue the message, or else enqueue the
|
|
* message before cancel_pending_rx() runs (and removes it).
|
|
*/
|
|
spin_lock_irqsave(&service->rx_lock, flags);
|
|
|
|
/* If the service is not active, drop the message. */
|
|
if (service->readiness != VS_SERVICE_ACTIVE) {
|
|
spin_unlock_irqrestore(&service->rx_lock, flags);
|
|
vs_put_service(service);
|
|
return -ECONNRESET;
|
|
}
|
|
|
|
list_add_tail(&mbuf->queue, &service->rx_queue);
|
|
spin_unlock_irqrestore(&service->rx_lock, flags);
|
|
|
|
/* Schedule processing of the message by the service's drivers. */
|
|
queue_rx_work(service);
|
|
vs_put_service(service);
|
|
|
|
return 0;
|
|
}
|
|
EXPORT_SYMBOL_GPL(vs_session_handle_message);
|
|
|
|
/**
|
|
* vs_session_quota_available - notify a service that it can transmit
|
|
* @session: the session owning the service that is ready
|
|
* @service_id: the id of the service that is ready
|
|
* @count: the number of buffers that just became ready
|
|
* @call_tx_ready: true if quota has just become nonzero due to a buffer being
|
|
* freed by the remote communication partner
|
|
*
|
|
* This routine is called by the transport driver when a send-direction
|
|
* message buffer becomes free. It wakes up any task that is waiting for
|
|
* send quota to become available.
|
|
*
|
|
* This routine may be called from interrupt context from the transport
|
|
* driver, and as such, it may not sleep.
|
|
*
|
|
* The caller must always serialise calls to this function relative to
|
|
* vs_session_handle_reset and vs_session_handle_activate. We don't do this
|
|
* internally, to avoid having to disable interrupts when called from task
|
|
* context.
|
|
*
|
|
* If the call_tx_ready argument is true, this function also schedules a
|
|
* call to the driver's tx_ready callback. Note that this never has priority
|
|
* over handling incoming messages; it will only be handled once the receive
|
|
* queue is empty. This is to increase batching of outgoing messages, and also
|
|
* to reduce the chance that an outgoing message will be dropped by the partner
|
|
* because an incoming message has already changed the state.
|
|
*
|
|
* In general, task context drivers should use the waitqueue, and softirq
|
|
* context drivers (with tx_atomic set) should use tx_ready.
|
|
*/
|
|
void vs_session_quota_available(struct vs_session_device *session,
|
|
vs_service_id_t service_id, unsigned count,
|
|
bool send_tx_ready)
|
|
{
|
|
struct vs_service_device *service;
|
|
unsigned long flags;
|
|
|
|
service = vs_session_get_service(session, service_id);
|
|
if (!service) {
|
|
dev_err(&session->dev, "tx ready for unknown service %d\n",
|
|
service_id);
|
|
session_fatal_error(session, GFP_ATOMIC);
|
|
return;
|
|
}
|
|
|
|
wake_up_nr(&service->quota_wq, count);
|
|
|
|
if (send_tx_ready) {
|
|
/*
|
|
* Take the rx lock before checking service readiness. This
|
|
* guarantees that if __reset_service() has just made the
|
|
* service inactive, we either see it and don't set the tx_ready
|
|
* flag, or else set the flag before cancel_pending_rx() runs
|
|
* (and clears it).
|
|
*/
|
|
spin_lock_irqsave(&service->rx_lock, flags);
|
|
|
|
/* If the service is not active, drop the tx_ready event */
|
|
if (service->readiness != VS_SERVICE_ACTIVE) {
|
|
spin_unlock_irqrestore(&service->rx_lock, flags);
|
|
vs_put_service(service);
|
|
return;
|
|
}
|
|
|
|
service->tx_ready = true;
|
|
spin_unlock_irqrestore(&service->rx_lock, flags);
|
|
|
|
/* Schedule RX processing by the service driver. */
|
|
queue_rx_work(service);
|
|
}
|
|
|
|
vs_put_service(service);
|
|
}
|
|
EXPORT_SYMBOL_GPL(vs_session_quota_available);
|
|
|
|
/**
|
|
* vs_session_handle_notify - process an incoming notification from a transport
|
|
* @session: the session that is receiving the notification
|
|
* @flags: notification flags
|
|
* @service_id: the id of the service that the notification was addressed to
|
|
*
|
|
* This function may be called from interrupt context from the transport driver,
|
|
* and as such, it may not sleep.
|
|
*/
|
|
void vs_session_handle_notify(struct vs_session_device *session,
|
|
unsigned long bits, vs_service_id_t service_id)
|
|
{
|
|
struct vs_service_device *service;
|
|
struct vs_service_driver *driver;
|
|
unsigned long flags;
|
|
|
|
service = vs_session_get_service(session, service_id);
|
|
if (!service) {
|
|
/* Ignore the notification since the service id doesn't exist */
|
|
dev_err(&session->dev, "notification for unknown service %d\n",
|
|
service_id);
|
|
return;
|
|
}
|
|
|
|
/*
|
|
* Take the rx lock before checking service readiness. This guarantees
|
|
* that if __reset_service() has just made the service inactive, we
|
|
* either see it and don't send the notification, or else send it
|
|
* before cancel_pending_rx() runs (and thus before the driver is
|
|
* deactivated).
|
|
*/
|
|
spin_lock_irqsave(&service->rx_lock, flags);
|
|
|
|
/* If the service is not active, drop the notification. */
|
|
if (service->readiness != VS_SERVICE_ACTIVE) {
|
|
spin_unlock_irqrestore(&service->rx_lock, flags);
|
|
vs_put_service(service);
|
|
return;
|
|
}
|
|
|
|
/* There should be a driver bound on the service */
|
|
if (WARN_ON(!service->dev.driver)) {
|
|
spin_unlock_irqrestore(&service->rx_lock, flags);
|
|
vs_put_service(service);
|
|
return;
|
|
}
|
|
|
|
driver = to_vs_service_driver(service->dev.driver);
|
|
/* Call the driver's notify function */
|
|
driver->notify(service, bits);
|
|
|
|
spin_unlock_irqrestore(&service->rx_lock, flags);
|
|
vs_put_service(service);
|
|
}
|
|
EXPORT_SYMBOL_GPL(vs_session_handle_notify);
|
|
|
|
static unsigned long reset_cool_off(struct vs_service_device *service)
|
|
{
|
|
return service->reset_delay * RESET_THROTTLE_COOL_OFF_MULT;
|
|
}
|
|
|
|
static bool ready_needs_delay(struct vs_service_device *service)
|
|
{
|
|
/*
|
|
* We throttle resets if too little time elapsed between the service
|
|
* last becoming ready, and the service last starting a reset.
|
|
*
|
|
* We do not use the current time here because it includes the time
|
|
* taken by the local service driver to actually process the reset.
|
|
*/
|
|
return service->last_reset && service->last_ready && time_before(
|
|
service->last_reset,
|
|
service->last_ready + RESET_THROTTLE_TIME);
|
|
}
|
|
|
|
static bool reset_throttle_cooled_off(struct vs_service_device *service)
|
|
{
|
|
/*
|
|
* Reset throttling cools off if enough time has elapsed since the
|
|
* last reset request.
|
|
*
|
|
* We check against the last requested reset, not the last serviced
|
|
* reset or ready. If we are throttling, a reset may not have been
|
|
* serviced for some time even though we are still receiving requests.
|
|
*/
|
|
return service->reset_delay && service->last_reset_request &&
|
|
time_after(jiffies, service->last_reset_request +
|
|
reset_cool_off(service));
|
|
}
|
|
|
|
/*
|
|
* Queue up the ready work for a service. If a service is resetting too fast
|
|
* then it will be throttled using an exponentially increasing delay before
|
|
* marking it ready. If the reset speed backs off then the ready throttling
|
|
* will be cleared. If a service reaches the maximum throttling delay then all
|
|
* resets will be ignored until the cool off period has elapsed.
|
|
*
|
|
* The basic logic of the reset throttling is:
|
|
*
|
|
* - If a reset request is processed and the last ready was less than
|
|
* RESET_THROTTLE_TIME ago, then the ready needs to be delayed to
|
|
* throttle resets.
|
|
*
|
|
* - The ready delay increases exponentially on each throttled reset
|
|
* between RESET_THROTTLE_MIN and RESET_THROTTLE_MAX.
|
|
*
|
|
* - If RESET_THROTTLE_MAX is reached then no ready will be sent until the
|
|
* reset requests have cooled off.
|
|
*
|
|
* - Reset requests have cooled off when no reset requests have been
|
|
* received for RESET_THROTTLE_COOL_OFF_MULT * the service's current
|
|
* ready delay. The service's reset throttling is disabled.
|
|
*
|
|
* Note: Be careful when adding print statements, including debugging, to
|
|
* this function. The ready throttling is intended to prevent DOSing of the
|
|
* vServices due to repeated resets (e.g. because of a persistent failure).
|
|
* Adding a printk on each reset for example would reset in syslog spamming
|
|
* which is a DOS attack in itself.
|
|
*
|
|
* The ready lock must be held by the caller.
|
|
*/
|
|
static void queue_ready_work(struct vs_service_device *service)
|
|
{
|
|
struct vs_session_device *session = vs_service_get_session(service);
|
|
unsigned long delay;
|
|
bool wait_for_cooloff = false;
|
|
|
|
lockdep_assert_held(&service->ready_lock);
|
|
|
|
/* This should only be called when the service enters reset. */
|
|
WARN_ON(service->readiness != VS_SERVICE_RESET);
|
|
|
|
if (ready_needs_delay(service)) {
|
|
/* Reset delay increments exponentially */
|
|
if (!service->reset_delay) {
|
|
service->reset_delay = RESET_THROTTLE_MIN;
|
|
} else if (service->reset_delay < RESET_THROTTLE_MAX) {
|
|
service->reset_delay *= 2;
|
|
} else {
|
|
wait_for_cooloff = true;
|
|
}
|
|
|
|
delay = service->reset_delay;
|
|
} else {
|
|
/* The reset request appears to have been be sane. */
|
|
delay = 0;
|
|
|
|
}
|
|
|
|
if (service->reset_delay > 0) {
|
|
/*
|
|
* Schedule cooloff work, to set the reset_delay to 0 if
|
|
* the reset requests stop for long enough.
|
|
*/
|
|
schedule_delayed_work(&service->cooloff_work,
|
|
reset_cool_off(service));
|
|
}
|
|
|
|
if (wait_for_cooloff) {
|
|
/*
|
|
* We need to finish cooling off before we service resets
|
|
* again. Schedule cooloff_work to run after the current
|
|
* cooloff period ends; it may reschedule itself even later
|
|
* if any more requests arrive.
|
|
*/
|
|
dev_err(&session->dev,
|
|
"Service %s is resetting too fast - must cool off for %u ms\n",
|
|
dev_name(&service->dev),
|
|
jiffies_to_msecs(reset_cool_off(service)));
|
|
return;
|
|
}
|
|
|
|
if (delay)
|
|
dev_err(&session->dev,
|
|
"Service %s is resetting too fast - delaying ready by %u ms\n",
|
|
dev_name(&service->dev),
|
|
jiffies_to_msecs(delay));
|
|
|
|
vs_debug(VS_DEBUG_SESSION, session,
|
|
"Service %s will become ready in %u ms\n",
|
|
dev_name(&service->dev),
|
|
jiffies_to_msecs(delay));
|
|
|
|
if (service->last_ready)
|
|
vs_debug(VS_DEBUG_SESSION, session,
|
|
"Last became ready %u ms ago\n",
|
|
msecs_ago(service->last_ready));
|
|
if (service->reset_delay >= RESET_THROTTLE_MAX)
|
|
dev_err(&session->dev, "Service %s hit max reset throttle\n",
|
|
dev_name(&service->dev));
|
|
|
|
schedule_delayed_work(&service->ready_work, delay);
|
|
}
|
|
|
|
static void session_activation_work(struct work_struct *work)
|
|
{
|
|
struct vs_session_device *session = container_of(work,
|
|
struct vs_session_device, activation_work);
|
|
struct vs_service_device *core_service = session->core_service;
|
|
struct vs_session_driver *session_drv =
|
|
to_vs_session_driver(session->dev.driver);
|
|
int activation_state;
|
|
int ret;
|
|
|
|
if (WARN_ON(!core_service))
|
|
return;
|
|
|
|
if (WARN_ON(!session_drv))
|
|
return;
|
|
|
|
/*
|
|
* We use an atomic to prevent duplicate activations if we race with
|
|
* an activate after a reset. This is very unlikely, but possible if
|
|
* this work item is preempted.
|
|
*/
|
|
activation_state = atomic_cmpxchg(&session->activation_state,
|
|
VS_SESSION_ACTIVATE, VS_SESSION_ACTIVE);
|
|
|
|
switch (activation_state) {
|
|
case VS_SESSION_ACTIVATE:
|
|
vs_debug(VS_DEBUG_SESSION, session,
|
|
"core service will be activated\n");
|
|
vs_service_enable(core_service);
|
|
break;
|
|
|
|
case VS_SESSION_RESET:
|
|
vs_debug(VS_DEBUG_SESSION, session,
|
|
"core service will be deactivated\n");
|
|
|
|
/* Handle the core service reset */
|
|
ret = service_handle_reset(session, core_service, true);
|
|
|
|
/* Tell the transport if the reset succeeded */
|
|
if (ret >= 0)
|
|
session->transport->vt->ready(session->transport);
|
|
else
|
|
dev_err(&session->dev, "core service reset unhandled: %d\n",
|
|
ret);
|
|
|
|
break;
|
|
|
|
default:
|
|
vs_debug(VS_DEBUG_SESSION, session,
|
|
"core service already active\n");
|
|
break;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* vs_session_handle_reset - Handle a reset at the session layer.
|
|
* @session: Session to reset
|
|
*
|
|
* This function is called by the transport when it receives a transport-level
|
|
* reset notification.
|
|
*
|
|
* After a session is reset by calling this function, it will reset all of its
|
|
* attached services, and then call the transport's ready callback. The
|
|
* services will remain in reset until the session is re-activated by a call
|
|
* to vs_session_handle_activate().
|
|
*
|
|
* Calling this function on a session that is already reset is permitted, as
|
|
* long as the transport accepts the consequent duplicate ready callbacks.
|
|
*
|
|
* A newly created session is initially in the reset state, and will not call
|
|
* the transport's ready callback. The transport may choose to either act as
|
|
* if the ready callback had been called, or call this function again to
|
|
* trigger a new ready callback.
|
|
*/
|
|
void vs_session_handle_reset(struct vs_session_device *session)
|
|
{
|
|
atomic_set(&session->activation_state, VS_SESSION_RESET);
|
|
|
|
schedule_work(&session->activation_work);
|
|
}
|
|
EXPORT_SYMBOL_GPL(vs_session_handle_reset);
|
|
|
|
/**
|
|
* vs_session_handle_activate - Allow a session to leave the reset state.
|
|
* @session: Session to mark active.
|
|
*
|
|
* This function is called by the transport when a transport-level reset is
|
|
* completed; that is, after the session layer has reset its services and
|
|
* called the ready callback, at *both* ends of the connection.
|
|
*/
|
|
void vs_session_handle_activate(struct vs_session_device *session)
|
|
{
|
|
atomic_set(&session->activation_state, VS_SESSION_ACTIVATE);
|
|
|
|
schedule_work(&session->activation_work);
|
|
}
|
|
EXPORT_SYMBOL_GPL(vs_session_handle_activate);
|
|
|
|
static ssize_t id_show(struct device *dev,
|
|
struct device_attribute *attr, char *buf)
|
|
{
|
|
struct vs_session_device *session = to_vs_session_device(dev);
|
|
|
|
return scnprintf(buf, PAGE_SIZE, "%d\n", session->session_num);
|
|
}
|
|
|
|
static DEVICE_ATTR_RO(id);
|
|
|
|
/*
|
|
* The vServices session device type
|
|
*/
|
|
static ssize_t is_server_show(struct device *dev,
|
|
struct device_attribute *attr, char *buf)
|
|
{
|
|
struct vs_session_device *session = to_vs_session_device(dev);
|
|
|
|
return scnprintf(buf, PAGE_SIZE, "%d\n", session->is_server);
|
|
}
|
|
|
|
static DEVICE_ATTR_RO(is_server);
|
|
|
|
static ssize_t name_show(struct device *dev,
|
|
struct device_attribute *attr, char *buf)
|
|
{
|
|
struct vs_session_device *session = to_vs_session_device(dev);
|
|
|
|
return scnprintf(buf, PAGE_SIZE, "%s\n", session->name);
|
|
}
|
|
|
|
static DEVICE_ATTR_RO(name);
|
|
|
|
#ifdef CONFIG_VSERVICES_DEBUG
|
|
static ssize_t debug_mask_show(struct device *dev,
|
|
struct device_attribute *attr, char *buf)
|
|
{
|
|
struct vs_session_device *session = to_vs_session_device(dev);
|
|
|
|
return scnprintf(buf, PAGE_SIZE, "%.8lx\n", session->debug_mask);
|
|
}
|
|
|
|
static ssize_t debug_mask_store(struct device *dev,
|
|
struct device_attribute *attr, const char *buf, size_t count)
|
|
{
|
|
struct vs_session_device *session = to_vs_session_device(dev);
|
|
int err;
|
|
|
|
err = kstrtoul(buf, 0, &session->debug_mask);
|
|
if (err)
|
|
return err;
|
|
|
|
/* Clear any bits we don't know about */
|
|
session->debug_mask &= VS_DEBUG_ALL;
|
|
|
|
return count;
|
|
}
|
|
|
|
static DEVICE_ATTR_RW(debug_mask);
|
|
|
|
#endif /* CONFIG_VSERVICES_DEBUG */
|
|
|
|
static struct attribute *vservices_session_dev_attrs[] = {
|
|
&dev_attr_id.attr,
|
|
&dev_attr_is_server.attr,
|
|
&dev_attr_name.attr,
|
|
#ifdef CONFIG_VSERVICES_DEBUG
|
|
&dev_attr_debug_mask.attr,
|
|
#endif
|
|
NULL,
|
|
};
|
|
ATTRIBUTE_GROUPS(vservices_session_dev);
|
|
|
|
static int vs_session_free_idr(struct vs_session_device *session)
|
|
{
|
|
mutex_lock(&vs_session_lock);
|
|
idr_remove(&session_idr, session->session_num);
|
|
mutex_unlock(&vs_session_lock);
|
|
return 0;
|
|
}
|
|
|
|
static void vs_session_device_release(struct device *dev)
|
|
{
|
|
struct vs_session_device *session = to_vs_session_device(dev);
|
|
|
|
vs_session_free_idr(session);
|
|
|
|
kfree(session->name);
|
|
kfree(session);
|
|
}
|
|
|
|
/*
|
|
* The vServices session bus
|
|
*/
|
|
static int vs_session_bus_match(struct device *dev,
|
|
struct device_driver *driver)
|
|
{
|
|
struct vs_session_device *session = to_vs_session_device(dev);
|
|
struct vs_session_driver *session_drv = to_vs_session_driver(driver);
|
|
|
|
return (session->is_server == session_drv->is_server);
|
|
}
|
|
|
|
static int vs_session_bus_remove(struct device *dev)
|
|
{
|
|
struct vs_session_device *session = to_vs_session_device(dev);
|
|
struct vs_service_device *core_service = session->core_service;
|
|
|
|
if (!core_service)
|
|
return 0;
|
|
|
|
/*
|
|
* Abort any pending session activation. We rely on the transport to
|
|
* not call vs_session_handle_activate after this point.
|
|
*/
|
|
cancel_work_sync(&session->activation_work);
|
|
|
|
/* Abort any pending fatal error handling, which is redundant now. */
|
|
cancel_work_sync(&session->fatal_error_work);
|
|
|
|
/*
|
|
* Delete the core service. This will implicitly delete everything
|
|
* else (in reset on the client side, and in release on the server
|
|
* side). The session holds a reference, so this won't release the
|
|
* service struct.
|
|
*/
|
|
delete_service(core_service);
|
|
|
|
/* Now clean up the core service. */
|
|
session->core_service = NULL;
|
|
|
|
/* Matches the get in vs_service_register() */
|
|
vs_put_service(core_service);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int vservices_session_uevent(struct device *dev,
|
|
struct kobj_uevent_env *env)
|
|
{
|
|
struct vs_session_device *session = to_vs_session_device(dev);
|
|
|
|
dev_dbg(dev, "uevent\n");
|
|
|
|
if (add_uevent_var(env, "IS_SERVER=%d", session->is_server))
|
|
return -ENOMEM;
|
|
|
|
if (add_uevent_var(env, "SESSION_ID=%d", session->session_num))
|
|
return -ENOMEM;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void vservices_session_shutdown(struct device *dev)
|
|
{
|
|
struct vs_session_device *session = to_vs_session_device(dev);
|
|
|
|
dev_dbg(dev, "shutdown\n");
|
|
|
|
/* Do a transport reset */
|
|
session->transport->vt->reset(session->transport);
|
|
}
|
|
|
|
struct bus_type vs_session_bus_type = {
|
|
.name = "vservices-session",
|
|
.match = vs_session_bus_match,
|
|
.remove = vs_session_bus_remove,
|
|
.dev_groups = vservices_session_dev_groups,
|
|
.uevent = vservices_session_uevent,
|
|
.shutdown = vservices_session_shutdown,
|
|
};
|
|
EXPORT_SYMBOL_GPL(vs_session_bus_type);
|
|
|
|
/*
|
|
* Common code for the vServices client and server buses
|
|
*/
|
|
int vs_service_bus_probe(struct device *dev)
|
|
{
|
|
struct vs_service_device *service = to_vs_service_device(dev);
|
|
struct vs_service_driver *vsdrv = to_vs_service_driver(dev->driver);
|
|
struct vs_session_device *session = vs_service_get_session(service);
|
|
int ret;
|
|
|
|
vs_dev_debug(VS_DEBUG_SESSION, session, &service->dev, "probe\n");
|
|
|
|
/*
|
|
* Increase the reference count on the service driver. We don't allow
|
|
* service driver modules to be removed if there are any device
|
|
* instances present. The devices must be explicitly removed first.
|
|
*/
|
|
if (!try_module_get(vsdrv->driver.owner))
|
|
return -ENODEV;
|
|
|
|
ret = vsdrv->probe(service);
|
|
if (ret) {
|
|
module_put(vsdrv->driver.owner);
|
|
return ret;
|
|
}
|
|
|
|
service->driver_probed = true;
|
|
|
|
try_start_service(service);
|
|
|
|
return 0;
|
|
}
|
|
EXPORT_SYMBOL_GPL(vs_service_bus_probe);
|
|
|
|
int vs_service_bus_remove(struct device *dev)
|
|
{
|
|
struct vs_service_device *service = to_vs_service_device(dev);
|
|
struct vs_service_driver *vsdrv = to_vs_service_driver(dev->driver);
|
|
int err = 0;
|
|
|
|
reset_service(service);
|
|
|
|
/* Prevent reactivation of the driver */
|
|
service->driver_probed = false;
|
|
|
|
/* The driver has now had its reset() callback called; remove it */
|
|
vsdrv->remove(service);
|
|
|
|
/*
|
|
* Take the service's state mutex and spinlock. This ensures that any
|
|
* thread that is calling vs_state_lock_safe[_bh] will either complete
|
|
* now, or see the driver removal and fail, irrespective of which type
|
|
* of lock it is using.
|
|
*/
|
|
mutex_lock_nested(&service->state_mutex, service->lock_subclass);
|
|
spin_lock_bh(&service->state_spinlock);
|
|
|
|
/* Release all the locks. */
|
|
spin_unlock_bh(&service->state_spinlock);
|
|
mutex_unlock(&service->state_mutex);
|
|
mutex_unlock(&service->ready_lock);
|
|
|
|
#ifdef CONFIG_VSERVICES_LOCK_DEBUG
|
|
service->state_spinlock_used = false;
|
|
service->state_mutex_used = false;
|
|
#endif
|
|
|
|
module_put(vsdrv->driver.owner);
|
|
|
|
return err;
|
|
}
|
|
EXPORT_SYMBOL_GPL(vs_service_bus_remove);
|
|
|
|
int vs_service_bus_uevent(struct device *dev, struct kobj_uevent_env *env)
|
|
{
|
|
struct vs_service_device *service = to_vs_service_device(dev);
|
|
struct vs_session_device *session = vs_service_get_session(service);
|
|
|
|
dev_dbg(dev, "uevent\n");
|
|
|
|
if (add_uevent_var(env, "IS_SERVER=%d", service->is_server))
|
|
return -ENOMEM;
|
|
|
|
if (add_uevent_var(env, "SERVICE_ID=%d", service->id))
|
|
return -ENOMEM;
|
|
|
|
if (add_uevent_var(env, "SESSION_ID=%d", session->session_num))
|
|
return -ENOMEM;
|
|
|
|
if (add_uevent_var(env, "SERVICE_NAME=%s", service->name))
|
|
return -ENOMEM;
|
|
|
|
if (add_uevent_var(env, "PROTOCOL=%s", service->protocol ?: ""))
|
|
return -ENOMEM;
|
|
|
|
return 0;
|
|
}
|
|
EXPORT_SYMBOL_GPL(vs_service_bus_uevent);
|
|
|
|
static int vs_session_create_sysfs_entry(struct vs_transport *transport,
|
|
struct vs_session_device *session, bool server,
|
|
const char *transport_name)
|
|
{
|
|
char *sysfs_name;
|
|
struct kobject *sysfs_parent = vservices_client_root;
|
|
|
|
if (!transport_name)
|
|
return -EINVAL;
|
|
|
|
sysfs_name = kasprintf(GFP_KERNEL, "%s:%s", transport->type,
|
|
transport_name);
|
|
if (!sysfs_name)
|
|
return -ENOMEM;
|
|
|
|
if (server)
|
|
sysfs_parent = vservices_server_root;
|
|
|
|
session->sysfs_entry = kobject_create_and_add(sysfs_name, sysfs_parent);
|
|
|
|
kfree(sysfs_name);
|
|
if (!session->sysfs_entry)
|
|
return -ENOMEM;
|
|
return 0;
|
|
}
|
|
|
|
static int vs_session_alloc_idr(struct vs_session_device *session)
|
|
{
|
|
int id;
|
|
|
|
mutex_lock(&vs_session_lock);
|
|
id = idr_alloc(&session_idr, session, 0, VS_MAX_SESSIONS, GFP_KERNEL);
|
|
mutex_unlock(&vs_session_lock);
|
|
|
|
if (id == -ENOSPC)
|
|
return -EBUSY;
|
|
else if (id < 0)
|
|
return id;
|
|
|
|
session->session_num = id;
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* vs_session_register - register a vservices session on a transport
|
|
* @transport: vservices transport that the session will attach to
|
|
* @parent: device that implements the transport (for sysfs)
|
|
* @server: true if the session is server-side
|
|
* @transport_name: name of the transport
|
|
*
|
|
* This function is intended to be called from the probe() function of a
|
|
* transport driver. It sets up a new session device, which then either
|
|
* performs automatic service discovery (for clients) or creates sysfs nodes
|
|
* that allow the user to create services (for servers).
|
|
*
|
|
* Note that the parent is only used by the driver framework; it is not
|
|
* directly accessed by the session drivers. Thus, a single transport device
|
|
* can support multiple sessions, as long as they each have a unique struct
|
|
* vs_transport.
|
|
*
|
|
* Note: This function may sleep, and therefore must not be called from
|
|
* interrupt context.
|
|
*
|
|
* Returns a pointer to the new device, or an error pointer.
|
|
*/
|
|
struct vs_session_device *vs_session_register(struct vs_transport *transport,
|
|
struct device *parent, bool server, const char *transport_name)
|
|
{
|
|
struct device *dev;
|
|
struct vs_session_device *session;
|
|
int ret = -ENOMEM;
|
|
|
|
WARN_ON(!transport);
|
|
|
|
session = kzalloc(sizeof(*session), GFP_KERNEL);
|
|
if (!session)
|
|
goto fail_session_alloc;
|
|
|
|
session->transport = transport;
|
|
session->is_server = server;
|
|
session->name = kstrdup(transport_name, GFP_KERNEL);
|
|
if (!session->name)
|
|
goto fail_free_session;
|
|
|
|
INIT_WORK(&session->activation_work, session_activation_work);
|
|
INIT_WORK(&session->fatal_error_work, session_fatal_error_work);
|
|
|
|
#ifdef CONFIG_VSERVICES_DEBUG
|
|
session->debug_mask = default_debug_mask & VS_DEBUG_ALL;
|
|
#endif
|
|
|
|
idr_init(&session->service_idr);
|
|
mutex_init(&session->service_idr_lock);
|
|
|
|
/*
|
|
* We must create session sysfs entry before device_create
|
|
* so, that sysfs entry is available while registering
|
|
* core service.
|
|
*/
|
|
ret = vs_session_create_sysfs_entry(transport, session, server,
|
|
transport_name);
|
|
if (ret)
|
|
goto fail_free_session;
|
|
|
|
ret = vs_session_alloc_idr(session);
|
|
if (ret)
|
|
goto fail_sysfs_entry;
|
|
|
|
dev = &session->dev;
|
|
dev->parent = parent;
|
|
dev->bus = &vs_session_bus_type;
|
|
dev->release = vs_session_device_release;
|
|
dev_set_name(dev, "vservice:%d", session->session_num);
|
|
|
|
ret = device_register(dev);
|
|
if (ret) {
|
|
goto fail_session_map;
|
|
}
|
|
|
|
/* Add a symlink to transport device inside session device sysfs dir */
|
|
if (parent) {
|
|
ret = sysfs_create_link(&session->dev.kobj,
|
|
&parent->kobj, VS_TRANSPORT_SYMLINK_NAME);
|
|
if (ret) {
|
|
dev_err(&session->dev,
|
|
"Error %d creating transport symlink\n",
|
|
ret);
|
|
goto fail_session_device_unregister;
|
|
}
|
|
}
|
|
|
|
return session;
|
|
|
|
fail_session_device_unregister:
|
|
device_unregister(&session->dev);
|
|
kobject_put(session->sysfs_entry);
|
|
/* Remaining cleanup will be done in vs_session_release */
|
|
return ERR_PTR(ret);
|
|
fail_session_map:
|
|
vs_session_free_idr(session);
|
|
fail_sysfs_entry:
|
|
kobject_put(session->sysfs_entry);
|
|
fail_free_session:
|
|
kfree(session->name);
|
|
kfree(session);
|
|
fail_session_alloc:
|
|
return ERR_PTR(ret);
|
|
}
|
|
EXPORT_SYMBOL(vs_session_register);
|
|
|
|
void vs_session_start(struct vs_session_device *session)
|
|
{
|
|
struct vs_service_device *core_service = session->core_service;
|
|
|
|
if (WARN_ON(!core_service))
|
|
return;
|
|
|
|
blocking_notifier_call_chain(&vs_session_notifier_list,
|
|
VS_SESSION_NOTIFY_ADD, session);
|
|
|
|
vs_service_start(core_service);
|
|
}
|
|
EXPORT_SYMBOL_GPL(vs_session_start);
|
|
|
|
/**
|
|
* vs_session_unregister - unregister a session device
|
|
* @session: the session device to unregister
|
|
*/
|
|
void vs_session_unregister(struct vs_session_device *session)
|
|
{
|
|
if (session->dev.parent)
|
|
sysfs_remove_link(&session->dev.kobj, VS_TRANSPORT_SYMLINK_NAME);
|
|
blocking_notifier_call_chain(&vs_session_notifier_list,
|
|
VS_SESSION_NOTIFY_REMOVE, session);
|
|
|
|
device_unregister(&session->dev);
|
|
|
|
kobject_put(session->sysfs_entry);
|
|
}
|
|
EXPORT_SYMBOL_GPL(vs_session_unregister);
|
|
|
|
struct service_unbind_work_struct {
|
|
struct vs_service_device *service;
|
|
struct work_struct work;
|
|
};
|
|
|
|
static void service_unbind_work(struct work_struct *work)
|
|
{
|
|
struct service_unbind_work_struct *unbind_work = container_of(work,
|
|
struct service_unbind_work_struct, work);
|
|
|
|
device_release_driver(&unbind_work->service->dev);
|
|
|
|
/* Matches vs_get_service() in vs_session_unbind_driver() */
|
|
vs_put_service(unbind_work->service);
|
|
kfree(unbind_work);
|
|
}
|
|
|
|
int vs_session_unbind_driver(struct vs_service_device *service)
|
|
{
|
|
struct service_unbind_work_struct *unbind_work =
|
|
kmalloc(sizeof(*unbind_work), GFP_KERNEL);
|
|
|
|
if (!unbind_work)
|
|
return -ENOMEM;
|
|
|
|
INIT_WORK(&unbind_work->work, service_unbind_work);
|
|
|
|
/* Put in service_unbind_work() */
|
|
unbind_work->service = vs_get_service(service);
|
|
schedule_work(&unbind_work->work);
|
|
|
|
return 0;
|
|
}
|
|
EXPORT_SYMBOL_GPL(vs_session_unbind_driver);
|
|
|
|
static int __init vservices_init(void)
|
|
{
|
|
int r;
|
|
|
|
printk(KERN_INFO "vServices Framework 1.0\n");
|
|
|
|
vservices_root = kobject_create_and_add("vservices", NULL);
|
|
if (!vservices_root) {
|
|
r = -ENOMEM;
|
|
goto fail_create_root;
|
|
}
|
|
|
|
r = bus_register(&vs_session_bus_type);
|
|
if (r < 0)
|
|
goto fail_bus_register;
|
|
|
|
r = vs_devio_init();
|
|
if (r < 0)
|
|
goto fail_devio_init;
|
|
|
|
return 0;
|
|
|
|
fail_devio_init:
|
|
bus_unregister(&vs_session_bus_type);
|
|
fail_bus_register:
|
|
kobject_put(vservices_root);
|
|
fail_create_root:
|
|
return r;
|
|
}
|
|
|
|
static void __exit vservices_exit(void)
|
|
{
|
|
printk(KERN_INFO "vServices Framework exit\n");
|
|
|
|
vs_devio_exit();
|
|
bus_unregister(&vs_session_bus_type);
|
|
kobject_put(vservices_root);
|
|
}
|
|
|
|
subsys_initcall(vservices_init);
|
|
module_exit(vservices_exit);
|
|
|
|
MODULE_DESCRIPTION("OKL4 Virtual Services Session");
|
|
MODULE_AUTHOR("Open Kernel Labs, Inc");
|
|
|