MOP2 pipes are a lot like UNIX pipes - they’re a bidirectional stream of data, but there’s slight difference in
the interface. Let’s take a look at what ulib defines:
Definitions for ipc_pipeXXX() calls
int32_t ipc_piperead(PID_t pid, uint64_t pipenum, uint8_t *const buffer, size_t len);
int32_t ipc_pipewrite(PID_t pid, uint64_t pipenum, const uint8_t *buffer, size_t len);
int32_t ipc_pipemake(uint64_t pipenum);
int32_t ipc_pipedelete(uint64_t pipenum);
int32_t ipc_pipeconnect(PID_t pid1, uint64_t pipenum1, PID_t pid2, uint64_t pipenum2);
In UNIX you have 2 processes working with a single pipe, but in MOP2, a pipe is exposed to the outside world and
anyone can read and write to it, which explains why these calls require a PID to be provided (indicates the
owner of the pipe).
Example of ipc_piperead() - reading your applications own input stream
#include <stddef.h>
#include <stdint.h>
#include <ulib.h>
void main(void) {
PID_t pid = proc_getpid();
#define INPUT_LINE_MAX 1024
for (;;) {
char buffer[INPUT_LINE_MAX];
string_memset(buffer, 0, sizeof(buffer));
int32_t nrd = ipc_piperead(pid, 1, (uint8_t *const)buffer, sizeof(buffer) - 1);
if (nrd > 0) {
uprintf("Got something: %s\n", buffer);
}
}
}
ipc_pipewrite() is a little boring, so let’s not go over it. Creating, deleting and connecting pipes is where
things get interesting.
A common issue, I’ve encountered, while programming in userspace for MOP2 is that I’d want to spawn some external
application and collect it’s output, for eg. into an ulib StringBuffer or some other akin structure. The
obvious thing to do would be to (since everything is polling-based) spawn an application, poll it’s state (not
PROC_DEAD) and while polling, read it’s out pipe (0) and save it into a stringbuffer. The code to do this would
look something like this:
Pipe lifetime problem illustration
#include <stddef.h>
#include <stdint.h>
#include <ulib.h>
void main(void) {
StringBuffer outsbuf;
stringbuffer_init(&outsbuf);
char *appargs = { "-saystring", "hello world" };
int32_t myapp = proc_spawn("base:/bin/myapp", appargs, ARRLEN(appargs));
proc_run(myapp);
// 4 == PROC_DEAD
while (proc_pollstate(myapp) != 4) {
int32_t r;
char buf[100];
string_memset(buf, 0, sizeof(buf));
r = ipc_piperead(myapp, 0, (uint8_t *const)buf, sizeof(buf) - 1);
if (r > 0) {
stringbuffer_appendcstr(&outsbuf, buf);
}
}
// print entire output
uprintf("%.*s\n", (int)outsbuf.count, outsbuf.data);
stringbuffer_free(&outsbuf);
}
Can you spot the BIG BUG? What if the application dies before we manage to read data from the pipe, taking the pipe
down with itself? We’re then stuck in this weird state of having incomplete data and the app being reported as
dead by proc_pollstate.
This can be easily solved by changing the lifetime of the pipe we’re working with. The parent process shall
allocate a pipe, connect it to it’s child process and make it so that a child is writing into a pipe managed by
it’s parent.
Pipe lifetime problem - the solution
#include <stddef.h>
#include <stdint.h>
#include <ulib.h>
void main(void) {
PID_t pid = proc_getpid();
StringBuffer outsbuf;
stringbuffer_init(&outsbuf);
char *appargs = { "-saystring", "hello world" };
int32_t myapp = proc_spawn("base:/bin/myapp", appargs, ARRLEN(appargs));
// take a free pipe slot. 0 and 1 are already taken by default
ipc_pipemake(10);
// connect pipes
// myapp's out (0) pipe --> pid's 10th pipe
ipc_pipeconnect(myapp, 0, pid, 10);
proc_run(myapp);
// 4 == PROC_DEAD
while (proc_pollstate(myapp) != 4) {
int32_t r;
char buf[100];
string_memset(buf, 0, sizeof(buf));
r = ipc_piperead(myapp, 0, (uint8_t *const)buf, sizeof(buf) - 1);
if (r > 0) {
stringbuffer_appendcstr(&outsbuf, buf);
}
}
// print entire output
uprintf("%.*s\n", (int)outsbuf.count, outsbuf.data);
ipc_pipedelete(10);
stringbuffer_free(&outsbuf);
}
Now, since the parent is managing the pipe and it outlives the child, everything is safe.