I've recently been working on some changes to the spufs code, and thought I'd write-up some of the details.
At present, the spu_run
syscall (used to run a SPU context)
blocks until the SPU program has exited (or some other event has happened,
such as a non-serviceable fault). This means that to take advantage of the
SPUs, you really need to start a new thread for each SPU context that you
create, otherwise your application will be sitting around waiting for each SPU
context to complete.
In fact, we have an invariant in the spufs code at the moment that only
contexts that are currently being spu_run
will ever be runnable
(and, at the moment, schedulable).
Ben H and I have been chatting about some ideas about asynchronous spu
contexts. This means that the userspace app can start the context, then later
retrieve the status of the SPU context (to see if it has stopped, faulted, or
whatever). We can then use standard POSIX semantics like poll()
to
see if a context is still running or has generated any "events", then handle
these events when they become available.
In effect, this is similar to spu_run
: currently, the
spu_run
syscall runs the SPU, then blocks until an event happens,
which is then returned to userpsace as the return value of
spu_run
. The main difference is that we don't block in the kernel
while the SPU is running.
So, I've been coding up an experimental change to spufs. Firstly, we have
to explicitly tell the kernel that we want a context to operate in asynchronous
mode, so I've added a new flag to the spu_create
syscall:
SPU_CREATE_ASYNC
.
I've opted for a file-based interface to these asynchronous contexts -
SPU events are retrieved by reading from a file. Contexts that are created with
the SPU_CREATE_ASYNC
flag have an extra file present (called
something like "event
") in their context directory in the
spufs mount. Reading from this file allows applications to retreive events
that the SPU program has raised.
We need to define a format for the data read from this events file, so here's something to get started with:
struct spu_event { uint32_t event; uint32_t status; uint32_t npc; };
- where the event
member specifies which event happened - a
stop-and-signal for example.
The status and npc members return the status of the SPU and the next program counter register, respectively. While not strictly necessary (this information is available from other files in spufs), it's very likely that the application will need these values in order to handle the event.
So, users of this interface may look something like this:
uint32_t npc = 0; struct context { int fd; int event_fd; } context; /* create the context */ context.fd = spu_create("/spu/ctx", NULL, SPU_CREATE_ASYNC); /* open the events file */ context.event_fd = openat(context.fd, "event", O_RDWR); /* start the context running. unlike the spu_run syscall, * this function does not block for the duration of the * spu program */ run_context(&context, npc); for (;;) { struct spu_event event; /* get the next event caused by the SPU */ read(context.event_fd, &event, sizeof(event)); if (event.event == SPU_EVENT_STOP) break; /* handle other event ... */ }
Note that the userspace examples here are not what we'd present to Cell application developers. They're more low-level examples of how the new asynchronous kernel interface works. In fact, the changes could be completely transparent to applications which use the libSPE interface.
This isn't far from the API provided by the current spu_run
syscall, except that we're not waiting in the kernel while the SPU is
running.
Also, we're going to need to control the SPU somehow - for example, we need
to implement the run_context
function in the pseudocode above.
Rather than overloading the spu_run
syscall, I've opted to use the
same event file - writes to this file will allow userspace to control the SPU.
I'm still working out the exact format of these writes, but the way I've
implemented it at the moment is that the application can write structures of
this layout to the file:
struct spu_control { uint32_t op; char data[]; };
The contents of the data
member depends on the operation
requested (specified by the op
member). For example, a 'start spu'
operation would have four extra bytes - a uint32_t
containing the
NPC to start the SPU execution from. A 'stop spu' operation doesn't require any
extra parameters, so the data
member would be 0 bytes long.
This would allow us to implement the run_context
function
as follows:
void run_context(struct context *context, uint32_t npc) { uint32_t buf[2]; buf[0] = SPU_CONTROL_START_SPU; buf[1] = npc; write(context.event_fd, buf, sizeof(buf)); }
There are plenty of other issues to deal with (like signals, and debugging), but I have a basic prototype working at the moment. More to come!