XNU IPC - Introduction to OOL data
Contents
This is the third article in the series covering my learnings and experiments with the Interprocess Communication mechanisms in XNU. This time we’ll continue exploring APIs related to complex messages and see how to use them to transfer out-of-line data.
Previous parts of the series are:
Unless you’re already familiar with these concepts, I’d recommend reading the previous articles first.
More type descriptors
Part #2 of the series covered bidirectional communication. One of the ways to achieve this was exchanging port rights over Mach messages, using port descriptors. Mach messages with more than just some inline data are referred to as complex, and sharing port rights isn’t their only use case. Another common descriptor type is a one to transfer out-of-line data, an OOL type descriptor.
The power of OOL data, and its advantage over inline data, comes from integration with the virtual memory system. A sender can share entire memory pages with the receiver without manually copying the data into temporary buffers. It is especially beneficial when transferring a large amount of data. The kernel can directly operate on virtual memory mappings to make the transfer as fast as possible and minimize memory usage. The sender can even choose to deallocate the memory regions from its address space during sending, allowing for even more optimizations.
Given an understanding of the general Mach message concepts and message descriptors, transferring OOL data is relatively straightforward in terms of the Mach APIs. More complexity comes with more control over how to transfer the memory using different data exchange options.
This article focuses on the OOL descriptor format and exchanging OOL data between two processes. The virtual memory system integration details and different memory transfer options will be for another time.
OOL descriptor
When sharing data inline, all that’s required is adding extra fields to the message structure. As the name suggests, data transferred using the out-of-line descriptors comes from outside of the message structure. Let’s take a look at the descriptor structure:
|
|
The fields of the OOL descriptor are:
- address of the out-of-line data
- size of the data
- deallocate, when true the memory page at the address will be removed from the sender’s address space once the message’s been sent
- copy defines the way of copying the memory
- type of the message descriptor, for the OOL descriptor, it’s
MACH_MSG_OOL_DESCRIPTOR
There are two copy types:
|
|
For the scope of this article, you’re only going to need the MACH_MSG_VIRTUAL_COPY
copy type.
Note that sending out-of-line data requires merely the data address and size, where both can be dynamic. This is unlike inline data, where indirection isn’t possible, and all data must be copied into the message buffer.
OOL messages example
Now that the theory around the out-of-line message descriptors’s been covered, it’s time to write some more code. We will extend the client/server programs from the previous part, from the first example of bidirectional communication using the reply port. This time, the server program will be the one sending data upon the client’s request, and we’re going to make the client request it.
Let’s start with the client implementation.
Client
To retrieve the OOL data, we’ll need a new message structure type, one that includes the OOL descriptor. The client also has to explicitly request the data from the server, so it needs a new message id to distinguish it from the default messages.
#1 OOL message format
Similar to the port descriptor example, we need a a complex Mach message. The following is the new message structure:
|
|
OOLMessage
consists of the message header, the number of type descriptors,
a single OOL descriptor, and it has no inline data.
This time it’s the client that’s on the receiving side, so we also already need a structure with the message trailer:
|
|
Let’s also already define the new message id. The client can use it to request OOL data, and the server will use it in the OOL response.
|
|
#2 Message receive routine
The client program already has a basic message retrieval routine. Now we need to add one for the OOL message. It’s not really different from other message types. We could use the similar trick with the union type from the previous article, but this time let’s keep it simple and add a dedicated routine:
|
|
#3 Sending request
To request the OOL data from the server, we can use the basic Message
structure, with the MSG_ID_COPY_MEM
id. You must not forget to specify the
msgh_local_port
field and message bits. Otherwise, the server won’t be able to
answer.
|
|
#4 Receiving OOL data
After sending the request, the client expects to receive a response with OOL data.
Now it’s time to use the OOL message retrieval routine. Upon successfull
read, you can see the data contents - assuming here it’s a string, full data size,
and how the data was transferred based on the copy
field:
|
|
Server
On the server side, we need to handle the out-of-line data request. But first, we need to have some data that we can share using the OOL descriptor. Only then can we send the message.
#1 OOL message buffer
To send the OOL data, we need some memory data to share with the client.
You could use any dummy, valid memory chunk or allocate one using standard
APIs such as malloc
. However, since the goal is to learn more about XNU,
let’s use its proprietary API - vm_allocate
.
vm_allocate
allows directly allocating virtual memory regions without going
through malloc
or other higher-level APIs.
Here we allocate a new memory region of a page size - vm_page_size
, at a
random location and with read/write permissions.
|
|
Once the region is ready, we can fill in some dummy data to share with the clients:
|
|
#2 Messsage send routine
Now it’s time to send the data. This is also very similar to the other types of messages, so let’s first focus on the OOL type descriptor fields:
|
|
And the entire sending routine is:
|
|
#3 Server loop
The data to send is now in place, and so is the routine to send it. The only remaining piece is to handle the client’s request and send the OOL response. So far, the server was only sending back the default message given the incoming message had a reply port:
|
|
The client program is going to use the dedicated MSG_ID_COPY_MEM
message id
to request the OOL data. So the server can use the message id field to distinguish
the type of a request and send OOL data when needed:
|
|
Result
All the pieces are now in place. When you start the server program and run the client, you should see those messages in the client’s output:
|
|
The client has sent two messages, and the server has responded to both,
with inline and out-of-line data.
Hello, OOL data!
is the out-of-line data we wanted to transfer, so that
worked out. Copy option 1
means MACH_MSG_VIRTUAL_COPY
, and thus it looks like
a virtual memory copy.
Summary
In this part, we’ve built upon the previously introduced concepts of complex
messages and type descriptors to learn how to transfer arbitrary,
out-of-line data using Mach messages. We also had a peek into the virtual
memory APIs in XNU with the vm_allocate
function.
Next time, we will dive into the integration of OOL data in Mach messages with the virtual memory system.
Full implementation of the example can be found at GitHub.
Thanks for reading! If you’ve got any questions, comments, or general feedback, you can find all my social links at the bottom of the page.