mirror of
https://github.com/xen-project/xen.git
synced 2026-10-28 18:21:39 +01:00
This blocks the main thread indefinitely if there is no PCI backend present in xenstore. Even in the case where there are passthrough devices configured libxl creates the stubdom and waits for it to startup _before_ adding the backend. Since the stub domains main thread is blocked before it can write the "running" state to xenstore the toolstack eventually times out and kills everything. There is already a separate pcifront thread which waits for the backend to appear and calls init_pcifront at the appropriate time should a backend ever appear. Unfortunately I don't have any free test boxes with VT-d so I haven't been able to test the cases where PCI deivces are passed through but I obviously have tested that I can now start an HVM domain with stub qemu without PCI devices passed through which I couldn't do before so this is an improvement. This stuff is a bit like pushing the lump around the carpet :-/ Signed-off-by: Ian Campbell <ian.campbell@citrix.com> Acked-by: Ian Jackson <ian.jackson.citrix.com> Committed-by: Ian Jackson <ian.jackson.citrix.com> Acked-by: Ian Jackson <ian.jackson@eu.citrix.com>
605 lines
17 KiB
C
605 lines
17 KiB
C
/* Minimal PCI driver for Mini-OS.
|
|
* Copyright (c) 2007-2008 Samuel Thibault.
|
|
* Based on blkfront.c.
|
|
*/
|
|
|
|
#include <string.h>
|
|
#include <mini-os/os.h>
|
|
#include <mini-os/lib.h>
|
|
#include <mini-os/xenbus.h>
|
|
#include <mini-os/events.h>
|
|
#include <errno.h>
|
|
#include <mini-os/gnttab.h>
|
|
#include <mini-os/xmalloc.h>
|
|
#include <mini-os/wait.h>
|
|
#include <mini-os/pcifront.h>
|
|
#include <mini-os/sched.h>
|
|
|
|
#define PCI_DEVFN(slot, func) ((((slot) & 0x1f) << 3) | ((func) & 0x07))
|
|
|
|
DECLARE_WAIT_QUEUE_HEAD(pcifront_queue);
|
|
static struct pcifront_dev *pcidev;
|
|
|
|
struct pcifront_dev {
|
|
domid_t dom;
|
|
|
|
struct xen_pci_sharedinfo *info;
|
|
grant_ref_t info_ref;
|
|
evtchn_port_t evtchn;
|
|
|
|
char *nodename;
|
|
char *backend;
|
|
|
|
xenbus_event_queue events;
|
|
};
|
|
|
|
void pcifront_handler(evtchn_port_t port, struct pt_regs *regs, void *data)
|
|
{
|
|
wake_up(&pcifront_queue);
|
|
}
|
|
|
|
static void free_pcifront(struct pcifront_dev *dev)
|
|
{
|
|
if (!dev)
|
|
dev = pcidev;
|
|
|
|
mask_evtchn(dev->evtchn);
|
|
|
|
gnttab_end_access(dev->info_ref);
|
|
free_page(dev->info);
|
|
|
|
unbind_evtchn(dev->evtchn);
|
|
|
|
free(dev->backend);
|
|
free(dev->nodename);
|
|
free(dev);
|
|
}
|
|
|
|
void pcifront_watches(void *opaque)
|
|
{
|
|
XenbusState state;
|
|
char *err = NULL, *msg = NULL;
|
|
char *be_path, *be_state;
|
|
char* nodename = opaque ? opaque : "device/pci/0";
|
|
char path[strlen(nodename) + 9];
|
|
char fe_state[strlen(nodename) + 7];
|
|
xenbus_event_queue events = NULL;
|
|
|
|
snprintf(path, sizeof(path), "%s/backend", nodename);
|
|
snprintf(fe_state, sizeof(fe_state), "%s/state", nodename);
|
|
|
|
while (1) {
|
|
printk("pcifront_watches: waiting for backend path to appear %s\n", path);
|
|
xenbus_watch_path_token(XBT_NIL, path, path, &events);
|
|
while ((err = xenbus_read(XBT_NIL, path, &be_path)) != NULL) {
|
|
free(err);
|
|
xenbus_wait_for_watch(&events);
|
|
}
|
|
xenbus_unwatch_path_token(XBT_NIL, path, path);
|
|
printk("pcifront_watches: waiting for backend to get into the right state %s\n", be_path);
|
|
be_state = (char *) malloc(strlen(be_path) + 7);
|
|
snprintf(be_state, strlen(be_path) + 7, "%s/state", be_path);
|
|
xenbus_watch_path_token(XBT_NIL, be_state, be_state, &events);
|
|
while ((err = xenbus_read(XBT_NIL, be_state, &msg)) != NULL || msg[0] > '4') {
|
|
free(msg);
|
|
free(err);
|
|
xenbus_wait_for_watch(&events);
|
|
}
|
|
xenbus_unwatch_path_token(XBT_NIL, be_state, be_state);
|
|
if (init_pcifront(NULL) == NULL) {
|
|
free(be_state);
|
|
free(be_path);
|
|
continue;
|
|
}
|
|
xenbus_watch_path_token(XBT_NIL, be_state, be_state, &events);
|
|
state = XenbusStateConnected;
|
|
printk("pcifront_watches: waiting for backend events %s\n", be_state);
|
|
while ((err = xenbus_wait_for_state_change(be_state, &state, &events)) == NULL &&
|
|
(err = xenbus_read(XBT_NIL, pcidev->backend, &msg)) == NULL) {
|
|
free(msg);
|
|
printk("pcifront_watches: backend state changed: %s %d\n", be_state, state);
|
|
if (state == XenbusStateReconfiguring) {
|
|
printk("pcifront_watches: writing %s %d\n", fe_state, XenbusStateReconfiguring);
|
|
if ((err = xenbus_switch_state(XBT_NIL, fe_state, XenbusStateReconfiguring)) != NULL) {
|
|
printk("pcifront_watches: error changing state to %d: %s\n",
|
|
XenbusStateReconfiguring, err);
|
|
if (!strcmp(err, "ENOENT")) {
|
|
xenbus_write(XBT_NIL, fe_state, "7");
|
|
free(err);
|
|
}
|
|
}
|
|
} else if (state == XenbusStateReconfigured) {
|
|
printk("pcifront_watches: writing %s %d\n", fe_state, XenbusStateConnected);
|
|
printk("pcifront_watches: changing state to %d\n", XenbusStateConnected);
|
|
if ((err = xenbus_switch_state(XBT_NIL, fe_state, XenbusStateConnected)) != NULL) {
|
|
printk("pcifront_watches: error changing state to %d: %s\n",
|
|
XenbusStateConnected, err);
|
|
if (!strcmp(err, "ENOENT")) {
|
|
xenbus_write(XBT_NIL, fe_state, "4");
|
|
free(err);
|
|
}
|
|
}
|
|
} else if (state == XenbusStateClosing)
|
|
break;
|
|
}
|
|
if (err)
|
|
printk("pcifront_watches: done waiting err=%s\n", err);
|
|
else
|
|
printk("pcifront_watches: done waiting\n");
|
|
xenbus_unwatch_path_token(XBT_NIL, be_state, be_state);
|
|
shutdown_pcifront(pcidev);
|
|
free(be_state);
|
|
free(be_path);
|
|
free(err);
|
|
pcidev = NULL;
|
|
}
|
|
|
|
xenbus_unwatch_path_token(XBT_NIL, path, path);
|
|
}
|
|
|
|
struct pcifront_dev *init_pcifront(char *_nodename)
|
|
{
|
|
xenbus_transaction_t xbt;
|
|
char* err;
|
|
char* message=NULL;
|
|
int retry=0;
|
|
char* msg;
|
|
char* nodename = _nodename ? _nodename : "device/pci/0";
|
|
int dom;
|
|
|
|
struct pcifront_dev *dev;
|
|
|
|
char path[strlen(nodename) + 1 + 10 + 1];
|
|
|
|
if (!_nodename && pcidev)
|
|
return pcidev;
|
|
|
|
printk("******************* PCIFRONT for %s **********\n\n\n", nodename);
|
|
|
|
snprintf(path, sizeof(path), "%s/backend-id", nodename);
|
|
dom = xenbus_read_integer(path);
|
|
if (dom == -1) {
|
|
printk("no backend\n");
|
|
return NULL;
|
|
}
|
|
|
|
dev = malloc(sizeof(*dev));
|
|
memset(dev, 0, sizeof(*dev));
|
|
dev->nodename = strdup(nodename);
|
|
dev->dom = dom;
|
|
|
|
evtchn_alloc_unbound(dev->dom, pcifront_handler, dev, &dev->evtchn);
|
|
|
|
dev->info = (struct xen_pci_sharedinfo*) alloc_page();
|
|
memset(dev->info,0,PAGE_SIZE);
|
|
|
|
dev->info_ref = gnttab_grant_access(dev->dom,virt_to_mfn(dev->info),0);
|
|
|
|
dev->events = NULL;
|
|
|
|
again:
|
|
err = xenbus_transaction_start(&xbt);
|
|
if (err) {
|
|
printk("starting transaction\n");
|
|
free(err);
|
|
}
|
|
|
|
err = xenbus_printf(xbt, nodename, "pci-op-ref","%u",
|
|
dev->info_ref);
|
|
if (err) {
|
|
message = "writing pci-op-ref";
|
|
goto abort_transaction;
|
|
}
|
|
err = xenbus_printf(xbt, nodename,
|
|
"event-channel", "%u", dev->evtchn);
|
|
if (err) {
|
|
message = "writing event-channel";
|
|
goto abort_transaction;
|
|
}
|
|
err = xenbus_printf(xbt, nodename,
|
|
"magic", XEN_PCI_MAGIC);
|
|
if (err) {
|
|
message = "writing magic";
|
|
goto abort_transaction;
|
|
}
|
|
|
|
snprintf(path, sizeof(path), "%s/state", nodename);
|
|
err = xenbus_switch_state(xbt, path, XenbusStateInitialised);
|
|
if (err) {
|
|
message = "switching state";
|
|
goto abort_transaction;
|
|
}
|
|
|
|
err = xenbus_transaction_end(xbt, 0, &retry);
|
|
if (err) free(err);
|
|
if (retry) {
|
|
goto again;
|
|
printk("completing transaction\n");
|
|
}
|
|
|
|
goto done;
|
|
|
|
abort_transaction:
|
|
free(err);
|
|
err = xenbus_transaction_end(xbt, 1, &retry);
|
|
goto error;
|
|
|
|
done:
|
|
|
|
snprintf(path, sizeof(path), "%s/backend", nodename);
|
|
msg = xenbus_read(XBT_NIL, path, &dev->backend);
|
|
if (msg) {
|
|
printk("Error %s when reading the backend path %s\n", msg, path);
|
|
goto error;
|
|
}
|
|
|
|
printk("backend at %s\n", dev->backend);
|
|
|
|
{
|
|
char path[strlen(dev->backend) + 1 + 5 + 1];
|
|
char frontpath[strlen(nodename) + 1 + 5 + 1];
|
|
XenbusState state;
|
|
snprintf(path, sizeof(path), "%s/state", dev->backend);
|
|
|
|
xenbus_watch_path_token(XBT_NIL, path, path, &dev->events);
|
|
|
|
err = NULL;
|
|
state = xenbus_read_integer(path);
|
|
while (err == NULL && state < XenbusStateConnected)
|
|
err = xenbus_wait_for_state_change(path, &state, &dev->events);
|
|
if (state != XenbusStateConnected) {
|
|
printk("backend not avalable, state=%d\n", state);
|
|
xenbus_unwatch_path_token(XBT_NIL, path, path);
|
|
goto error;
|
|
}
|
|
|
|
snprintf(frontpath, sizeof(frontpath), "%s/state", nodename);
|
|
if ((err = xenbus_switch_state(XBT_NIL, frontpath, XenbusStateConnected))
|
|
!= NULL) {
|
|
printk("error switching state %s\n", err);
|
|
xenbus_unwatch_path_token(XBT_NIL, path, path);
|
|
goto error;
|
|
}
|
|
}
|
|
unmask_evtchn(dev->evtchn);
|
|
|
|
printk("**************************\n");
|
|
|
|
if (!_nodename)
|
|
pcidev = dev;
|
|
|
|
return dev;
|
|
|
|
error:
|
|
free(err);
|
|
free_pcifront(dev);
|
|
return NULL;
|
|
}
|
|
|
|
void pcifront_scan(struct pcifront_dev *dev, void (*func)(unsigned int domain, unsigned int bus, unsigned slot, unsigned int fun))
|
|
{
|
|
char *path;
|
|
int i, n, len;
|
|
char *s, *msg = NULL;
|
|
unsigned int domain, bus, slot, fun;
|
|
|
|
if (!dev)
|
|
dev = pcidev;
|
|
if (!dev) {
|
|
printk("pcifront_scan: device or bus\n");
|
|
return;
|
|
}
|
|
|
|
len = strlen(dev->backend) + 1 + 5 + 10 + 1;
|
|
path = (char *) malloc(len);
|
|
snprintf(path, len, "%s/num_devs", dev->backend);
|
|
n = xenbus_read_integer(path);
|
|
|
|
for (i = 0; i < n; i++) {
|
|
snprintf(path, len, "%s/dev-%d", dev->backend, i);
|
|
msg = xenbus_read(XBT_NIL, path, &s);
|
|
if (msg) {
|
|
printk("Error %s when reading the PCI root name at %s\n", msg, path);
|
|
continue;
|
|
}
|
|
|
|
if (sscanf(s, "%x:%x:%x.%x", &domain, &bus, &slot, &fun) != 4) {
|
|
printk("\"%s\" does not look like a PCI device address\n", s);
|
|
free(s);
|
|
continue;
|
|
}
|
|
free(s);
|
|
|
|
if (func)
|
|
func(domain, bus, slot, fun);
|
|
}
|
|
free(path);
|
|
}
|
|
|
|
void shutdown_pcifront(struct pcifront_dev *dev)
|
|
{
|
|
char* err = NULL;
|
|
XenbusState state;
|
|
|
|
char path[strlen(dev->backend) + 1 + 5 + 1];
|
|
char nodename[strlen(dev->nodename) + 1 + 5 + 1];
|
|
|
|
printk("close pci: backend at %s\n",dev->backend);
|
|
|
|
snprintf(path, sizeof(path), "%s/state", dev->backend);
|
|
snprintf(nodename, sizeof(nodename), "%s/state", dev->nodename);
|
|
if ((err = xenbus_switch_state(XBT_NIL, nodename, XenbusStateClosing)) != NULL) {
|
|
printk("shutdown_pcifront: error changing state to %d: %s\n",
|
|
XenbusStateClosing, err);
|
|
goto close_pcifront;
|
|
}
|
|
state = xenbus_read_integer(path);
|
|
while (err == NULL && state < XenbusStateClosing)
|
|
err = xenbus_wait_for_state_change(path, &state, &dev->events);
|
|
if (err) free(err);
|
|
|
|
if ((err = xenbus_switch_state(XBT_NIL, nodename, XenbusStateClosed)) != NULL) {
|
|
printk("shutdown_pcifront: error changing state to %d: %s\n",
|
|
XenbusStateClosed, err);
|
|
goto close_pcifront;
|
|
}
|
|
state = xenbus_read_integer(path);
|
|
while (state < XenbusStateClosed) {
|
|
err = xenbus_wait_for_state_change(path, &state, &dev->events);
|
|
free(err);
|
|
}
|
|
|
|
if ((err = xenbus_switch_state(XBT_NIL, nodename, XenbusStateInitialising)) != NULL) {
|
|
printk("shutdown_pcifront: error changing state to %d: %s\n",
|
|
XenbusStateInitialising, err);
|
|
goto close_pcifront;
|
|
}
|
|
err = NULL;
|
|
state = xenbus_read_integer(path);
|
|
while (err == NULL && (state < XenbusStateInitWait || state >= XenbusStateClosed))
|
|
err = xenbus_wait_for_state_change(path, &state, &dev->events);
|
|
|
|
close_pcifront:
|
|
if (err) free(err);
|
|
xenbus_unwatch_path_token(XBT_NIL, path, path);
|
|
|
|
snprintf(path, sizeof(path), "%s/info-ref", nodename);
|
|
xenbus_rm(XBT_NIL, path);
|
|
snprintf(path, sizeof(path), "%s/event-channel", nodename);
|
|
xenbus_rm(XBT_NIL, path);
|
|
|
|
if (!err)
|
|
free_pcifront(dev);
|
|
}
|
|
|
|
int pcifront_physical_to_virtual (struct pcifront_dev *dev,
|
|
unsigned int *dom,
|
|
unsigned int *bus,
|
|
unsigned int *slot,
|
|
unsigned long *fun)
|
|
{
|
|
char path[strlen(dev->backend) + 1 + 5 + 10 + 1];
|
|
int i, n;
|
|
char *s, *msg = NULL;
|
|
unsigned int dom1, bus1, slot1, fun1;
|
|
|
|
if (!dev)
|
|
dev = pcidev;
|
|
|
|
snprintf(path, sizeof(path), "%s/num_devs", dev->backend);
|
|
n = xenbus_read_integer(path);
|
|
|
|
for (i = 0; i < n; i++) {
|
|
snprintf(path, sizeof(path), "%s/dev-%d", dev->backend, i);
|
|
msg = xenbus_read(XBT_NIL, path, &s);
|
|
if (msg) {
|
|
printk("Error %s when reading the PCI root name at %s\n", msg, path);
|
|
continue;
|
|
}
|
|
|
|
if (sscanf(s, "%x:%x:%x.%x", &dom1, &bus1, &slot1, &fun1) != 4) {
|
|
printk("\"%s\" does not look like a PCI device address\n", s);
|
|
free(s);
|
|
continue;
|
|
}
|
|
free(s);
|
|
|
|
if (dom1 == *dom && bus1 == *bus && slot1 == *slot && fun1 == *fun) {
|
|
snprintf(path, sizeof(path), "%s/vdev-%d", dev->backend, i);
|
|
msg = xenbus_read(XBT_NIL, path, &s);
|
|
if (msg) {
|
|
printk("Error %s when reading the PCI root name at %s\n", msg, path);
|
|
continue;
|
|
}
|
|
|
|
if (sscanf(s, "%x:%x:%x.%x", dom, bus, slot, fun) != 4) {
|
|
printk("\"%s\" does not look like a PCI device address\n", s);
|
|
free(s);
|
|
continue;
|
|
}
|
|
free(s);
|
|
|
|
return 0;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
void pcifront_op(struct pcifront_dev *dev, struct xen_pci_op *op)
|
|
{
|
|
if (!dev)
|
|
dev = pcidev;
|
|
dev->info->op = *op;
|
|
/* Make sure info is written before the flag */
|
|
wmb();
|
|
set_bit(_XEN_PCIF_active, (void*) &dev->info->flags);
|
|
notify_remote_via_evtchn(dev->evtchn);
|
|
|
|
wait_event(pcifront_queue, !test_bit(_XEN_PCIF_active, (void*) &dev->info->flags));
|
|
|
|
/* Make sure flag is read before info */
|
|
rmb();
|
|
*op = dev->info->op;
|
|
}
|
|
|
|
int pcifront_conf_read(struct pcifront_dev *dev,
|
|
unsigned int dom,
|
|
unsigned int bus, unsigned int slot, unsigned long fun,
|
|
unsigned int off, unsigned int size, unsigned int *val)
|
|
{
|
|
struct xen_pci_op op;
|
|
|
|
if (!dev)
|
|
dev = pcidev;
|
|
if (pcifront_physical_to_virtual(dev, &dom, &bus, &slot, &fun) < 0)
|
|
return XEN_PCI_ERR_dev_not_found;
|
|
memset(&op, 0, sizeof(op));
|
|
|
|
op.cmd = XEN_PCI_OP_conf_read;
|
|
op.domain = dom;
|
|
op.bus = bus;
|
|
op.devfn = PCI_DEVFN(slot, fun);
|
|
op.offset = off;
|
|
op.size = size;
|
|
|
|
pcifront_op(dev, &op);
|
|
|
|
if (op.err)
|
|
return op.err;
|
|
|
|
*val = op.value;
|
|
|
|
return 0;
|
|
}
|
|
|
|
int pcifront_conf_write(struct pcifront_dev *dev,
|
|
unsigned int dom,
|
|
unsigned int bus, unsigned int slot, unsigned long fun,
|
|
unsigned int off, unsigned int size, unsigned int val)
|
|
{
|
|
struct xen_pci_op op;
|
|
|
|
if (!dev)
|
|
dev = pcidev;
|
|
if (pcifront_physical_to_virtual(dev, &dom, &bus, &slot, &fun) < 0)
|
|
return XEN_PCI_ERR_dev_not_found;
|
|
memset(&op, 0, sizeof(op));
|
|
|
|
op.cmd = XEN_PCI_OP_conf_write;
|
|
op.domain = dom;
|
|
op.bus = bus;
|
|
op.devfn = PCI_DEVFN(slot, fun);
|
|
op.offset = off;
|
|
op.size = size;
|
|
|
|
op.value = val;
|
|
|
|
pcifront_op(dev, &op);
|
|
|
|
return op.err;
|
|
}
|
|
|
|
int pcifront_enable_msi(struct pcifront_dev *dev,
|
|
unsigned int dom,
|
|
unsigned int bus, unsigned int slot, unsigned long fun)
|
|
{
|
|
struct xen_pci_op op;
|
|
|
|
if (!dev)
|
|
dev = pcidev;
|
|
if (pcifront_physical_to_virtual(dev, &dom, &bus, &slot, &fun) < 0)
|
|
return XEN_PCI_ERR_dev_not_found;
|
|
memset(&op, 0, sizeof(op));
|
|
|
|
op.cmd = XEN_PCI_OP_enable_msi;
|
|
op.domain = dom;
|
|
op.bus = bus;
|
|
op.devfn = PCI_DEVFN(slot, fun);
|
|
|
|
pcifront_op(dev, &op);
|
|
|
|
if (op.err)
|
|
return op.err;
|
|
else
|
|
return op.value;
|
|
}
|
|
|
|
int pcifront_disable_msi(struct pcifront_dev *dev,
|
|
unsigned int dom,
|
|
unsigned int bus, unsigned int slot, unsigned long fun)
|
|
{
|
|
struct xen_pci_op op;
|
|
|
|
if (!dev)
|
|
dev = pcidev;
|
|
if (pcifront_physical_to_virtual(dev, &dom, &bus, &slot, &fun) < 0)
|
|
return XEN_PCI_ERR_dev_not_found;
|
|
memset(&op, 0, sizeof(op));
|
|
|
|
op.cmd = XEN_PCI_OP_disable_msi;
|
|
op.domain = dom;
|
|
op.bus = bus;
|
|
op.devfn = PCI_DEVFN(slot, fun);
|
|
|
|
pcifront_op(dev, &op);
|
|
|
|
return op.err;
|
|
}
|
|
|
|
int pcifront_enable_msix(struct pcifront_dev *dev,
|
|
unsigned int dom,
|
|
unsigned int bus, unsigned int slot, unsigned long fun,
|
|
struct xen_msix_entry *entries, int n)
|
|
{
|
|
struct xen_pci_op op;
|
|
|
|
if (!dev)
|
|
dev = pcidev;
|
|
if (pcifront_physical_to_virtual(dev, &dom, &bus, &slot, &fun) < 0)
|
|
return XEN_PCI_ERR_dev_not_found;
|
|
if (n > SH_INFO_MAX_VEC)
|
|
return XEN_PCI_ERR_op_failed;
|
|
|
|
memset(&op, 0, sizeof(op));
|
|
|
|
op.cmd = XEN_PCI_OP_enable_msix;
|
|
op.domain = dom;
|
|
op.bus = bus;
|
|
op.devfn = PCI_DEVFN(slot, fun);
|
|
op.value = n;
|
|
|
|
memcpy(op.msix_entries, entries, n * sizeof(*entries));
|
|
|
|
pcifront_op(dev, &op);
|
|
|
|
if (op.err)
|
|
return op.err;
|
|
|
|
memcpy(entries, op.msix_entries, n * sizeof(*entries));
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
int pcifront_disable_msix(struct pcifront_dev *dev,
|
|
unsigned int dom,
|
|
unsigned int bus, unsigned int slot, unsigned long fun)
|
|
{
|
|
struct xen_pci_op op;
|
|
|
|
if (!dev)
|
|
dev = pcidev;
|
|
if (pcifront_physical_to_virtual(dev, &dom, &bus, &slot, &fun) < 0)
|
|
return XEN_PCI_ERR_dev_not_found;
|
|
memset(&op, 0, sizeof(op));
|
|
|
|
op.cmd = XEN_PCI_OP_disable_msix;
|
|
op.domain = dom;
|
|
op.bus = bus;
|
|
op.devfn = PCI_DEVFN(slot, fun);
|
|
|
|
pcifront_op(dev, &op);
|
|
|
|
return op.err;
|
|
}
|