I had trouble sleeping last night so I spent a couple of hours writing up another draft of the Trindle kernel system call interface. I’ve managed to knock the complexity down a bit further without losing any functionality. Still has some issues to noodle over, but they’re growing increasingly minor and I think it’s at the point now where I could build it and it might actually work.
Every kernel-managed entity visible in user space is an object. Every object has a globally unique address. This value is only useful within a process which has access permission for that object.
typedef void *object_t;
What is the current process allowed to do with the object at this address? The result will be a bitmask of the relevant access rights from the enum.
enum ACCESS
{
ACCESS_READ = 1, // can read from this segment
ACCESS_WRITE = 2, // can write to this segment
ACCESS_EXECUTE = 4, // can execute code inside this segment
ACCESS_SEND = 8, // can transfer messages into this pipe
ACCESS_RECEIVE = 16, // can receive messages from this pipe
};
int access(object_t);
How large is this object? For a memory segment, this is its size in bytes; for a pipe, this is a lower bound on the number of objects in its queue.
size_t measure(object_t);
A segment is a contiguous block of memory with a common access right. The object address is a pointer to the first byte in the block. Create a new segment by concatenating some arbitrary number of source buffers together. The kernel may zerofill the buffer up to a more convenient size. A source buffer with an address of NULL represents zerofill, not an actual copy. A new segment will have ACCESS_READ|ACCESS_WRITE
.
typedef object_t segment_t;
struct buffer_t
{
size_t bytes;
uint8_t *address;
};
segment_t allocate(size_t, const buffer_t[]);
Processes send and receive messages through fixed-length queues called pipes. Any number of processes may send messages to a single pipe, but only one process may read from it at a time. A pipe is an abstract object, not a memory segment. A new pipe will have ACCESS_SEND|ACCESS_RECEIVE
.
typedef object_t pipe_t;
pipe_t pipe(size_t queue_items);
A process communicates with the rest of the world by sending and receiving messages. A message describes a state change involving an object and/or a communication pipe.
struct message_t
{
pipe_t address;
object_t content;
};
For efficiency, messages are exchanged in batches, sending and receiving as many at a time as possible. A batch of messages is called a mailbox.
struct mailbox_t
{
size_t count;
message_t *address;
};
An outgoing message can accomplish three different jobs, depending on which fields you populate with non-NULL values.
- both populated: share the content object by sending it through the pipe
- address only, content NULL: receive messages from the specified pipe
- content only, address NULL: release access to the specified object
Prepare a list of outgoing messages: the outbox. Fill out an array of message_t, then provide the address of the array base and the item count. Allocate a second array of message_t for incoming messages: the inbox. Provide the address of this array and the maximum number of messages the array can hold. Then call sync to let the system transfer as many messages as it can manage.
void sync(mailbox_t *out, mailbox_t *in);
On return, the outbox will have been sorted, grouping all of the failed messages at the beginning of the buffer, updating out->count
with the number of messages which could not be sent (hopefully zero).
When a send fails, it is either because the recipient pipe has closed or because its queue was temporarily full. You can determine which it was by checking to see whether you still have ACCESS_SEND
for the pipe specified in the failed message’s address.
On return, the inbox may also have been populated with incoming messages, and in->count
will have been changed to reflect the number of messages that were received. The content of the remaining array items is undefined.
An incoming message can communicate several different changes of state depending on which fields are populated with non-NULL values.
- Both address and content: we received a message from an input pipe.
- content only: we now have exclusive ownership of this object.
- address only: the receiver has released this pipe and it is now closed.
What does it mean to have exclusive access to an object, and why would you
want to release it?
A segment can only be safely modified when there is exactly one process with access to its contents. If one process shares a segment object with another, the sender will lose ACCESS_WRITE
and the receiver will gain only ACCESS_READ
.
Should the sender later release its access to the segment, however, such that there remained exactly one process with access, the one remaining process would then gain ACCESS_WRITE
for that segment, whether or not it
had anything to do with the segment’s original creation.
A process can therefore transfer read/write access to a segment in one sync by sending the segment through a pipe and then by releasing its own access. When the last process releases the resource, so nobody has access to it any longer, the kernel will delete it.
Pipes work differently: any number of processes can have ACCESS_SEND
, but only the creating process can ever have ACCESS_RECEIVE
. When the creating process releases its access to the pipe, the pipe goes dead and all the other processes will instantly lose ACCESS_SEND
.
Every process has ACCESS_EXECUTE
to the segment which contains its machine code. ACCESS_EXECUTE
and ACCESS_WRITE
are mutually exclusive, so code segments are read-only whether they are owned by one process or many.
Each new process starts up with an input queue providing access to whatever resources the launching process has chosen to share with it.
typedef void (*entrypoint_t)(pipe_t input);
The entrypoint function cannot return since it lives at the base of the thread stack. When it’s done with its work it should call exit, passing in whichever object represents its output. If something goes horribly wrong, it can bail out on the error path instead.
void exit(object_t);
void abort(object_t);
If you have loaded or generated some code and you want to execute it, you can acquire execute permission for a segment. Once executable, a segment cannot be made writable again; you must release and recreate it if you want to change it.
void extend(segment_t);
An existing process may launch a new one, specifying an entrypoint and a pair of completion pipes that will be notified when the process terminates. The entrypoint MUST be located inside a segment with ACCESS_READ
. The launch function returns the ACCESS_SEND
end of a pipe representing the new process’ main input queue. The out
pipe will receive the process’ final output object when it exits; if it aborts, the err
pipe will get the report instead.
pipe_t launch(entrypoint_t, size_t queue_count, pipe_t out, pipe_t err);