L4Re simple client server example

From TUDOS-Wiki
Jump to navigationJump to search

In this tutorial we implement two applications: a simple client and server that send numbers between each other and print them. On our way we focus on the purpose of the involved L4Re objects and methods.

Please have a look at the complete source code, which can be found on github.com.

Overview

Our two tasks are called simple-client and simple-server. The simple-client sends a number to the simple-server, which prints the received number and sends the number multiplied by two back to the client, which then prints the received number.

To do this we have to go through different steps:

  1. Argument marshalling: For sending data through an L4 channel (or: IPC gate) we use a specific memory area that is shared between the thread and the kernel: the User-Level Thread Control Block (UTCB). Arguments that shall be sent to another thread are put into the UTCB. This process is called marshalling.
  2. Send data: we need to send the marshalled data to the server
  3. Receiving data: the server needs to be ready to receive data on the channel used by the client
  4. Argument unmarshalling: the server receives data into its UTCB and needs to read the number from it. This unmarshalling reverses the marshalling process.
  5. Computation and reply: the server computes the doubled number and sends a reply to the client (using the UTCB).
  6. Reply processing: The client receives the result and prints it again.

We will first go through each line in the main function of the client, which puts the number into the utcb, sends it to the server, receives the answer, and prints it.

Afterwards we look at the server's dispatch method, which prints the number and answers with the double of the number.

Finally we see the main function of the server, which prepares the server to be ready for receiving numbers.

ned.lua

L4Re applications are typically launched by Ned using an init-style script. These scripts are written in the Lua programming language.

Let's first look at the configuration file which sets up our example:

require("L4")

local ld = L4.default_loader
local channel = ld:new_channel()

ld:start({
            caps = {
               my_server_side = channel:svr()
            },
         }, "rom/simple-server")
ld:start({
            caps = {
               my_client_side = channel
            },
         }, "rom/simple-client")
  • For this example we start two tasks, simple-server and simple-client. To achieve this, we call the start() method provided by the L4.default_loader object.
  • In order to talk to each other, the applications need to be connected by a communication channel. In L4/Fiasco.OC this channel is represented by an IPC gate kernel object. Luckily, we don't need to know about the tiny details of the kernel interface. Instead, the L4 Lua package provides us a way of creating a channel and connecting the applications to it. This is done by calling ld:new_channel(), which provides us with a Lua channel object.
  • To make the IPC gate accessible to the applications, the init script allows us to specify a set of capabilities that are passed to an application during startup.
    • Capabilities are references to kernel objects and are specified as a list of name = obj_ref pairs in a table named caps which can be passed to every call to ld:start(). Here, obj_ref is a reference to a kernel object, such as the channel we just created. name is the name that the application may at runtime use to obtain this reference.
    • For simple-client, we directly map the name my_client_side to the channel created previously.
    • For simple-server, we map my_server_side to channel:svr() which gives us the server-side end of the channel. The reason for this lies in the kernel interface, which only allows the server-side thread to attach to an IPC gate and receive messages from it, whereas other threads can only send messages through the channel.

client.cc

We now want to look at the straight forward client.

Cap<void> server = Env::env()->get_cap<void>("my_client_side");
if (!server.is_valid())
{
    std::cerr << "no valid cap to 'my_client_side'" << std::endl;
    return 1;
}

Here, we obtain the capability selector, which we declared for the simple-client in its caps table in the ned.lua file. We use the Env::env() object, the Initial Environment of this task set up by Ned. This object provides access to the caps table using the get_cap() function. The returned Cap will be used to refer to the communication channel we are going to send our message through.

Note that get_cap can return an invalid Cap if some error occured. We want to check for errors with the call to is_valid and print a message if it isn't valid.

Ipc::Iostream ios(l4_utcb());

The Ipc::Iostream ios is an abstraction provided by L4Re that handles marshalling and unmarshalling data into the UTCB. Furthermore, it provides methods that wrap the common IPC functionality provided by the Fiasco.OC kernel. As a parameter to constructing ios we provide the UTCB into which the Iostream should write and read the values. With l4_utcb() we get the current UTCB of the running thread.

ios << n;

Ipc::Iostreams implement C++ stream operators to access the UTCB. This call writes the number into the UTCB at the current location as managed by ios.

l4_msgtag_t tag = ios.call(server.cap());
if (l4_error(tag))
{
    std::cerr << "calling server: " << l4_error(tag) << std::endl;
    return 1;
}

Most of this code is error checking and reporting. The important method call is ios.call(server.cap()). call will send data and block waiting for a reply. First, it sends the data stored in the UTCB to the IPC channel we obtained a capability for. The kernel delivers this message to whatever thread is blocked waiting on the other side. The second part of call blocks the current thread and waits for a reply from the thread behind the server capability. (server.cap() simply casts the L4::Cap object into an l4_cap_idx_t, which is expected by the lower-level system call wrappers.) The thread will be unblocked if an IPC arrives, in this case the answer of the simple-server containing a number.

ios >> n;

After the answer from server has arrived, we read the result out of it.

std::cout << n << std::endl;

And finally we print this number. Done.

server.cc

The more complex server contains a very important pattern. It defines a Server_object which does the actual work of answering requests and a Registry_server which dispatches IPCs to different Server_objects. Server_objects allow servers to implement different services and provide them through arbitrary IPC channels. The registry keeps track of all established server objects. Upon arrival of a message it figures out, which server object is responsible for handling the request and relays the request to this object by calling its dispatch() method.

Server object

struct SimpleServer : Server_object

We define our SimpleServer as a Server_object and need to implement its very simple interface.

int dispatch(l4_umword_t, Ipc::Iostream &ios)

dispatch is the only method needed to implement the interface of Server_object. We discard the first argument, cause we don't need it. The second argument is more interesting: It is an Ipc::Iostream that wraps the receiving thread's UTCB and has already been created for us by the communication framework.

ios >> n;

We read a number from the Ipc::Iostream. Particularly sizeof(n) bytes are read from the UTCB to n from the current position and the current position is forward by this number of bytes. No we can print the number and compute the answer.

  • Note, that no one forces the server to read the same amount and type of data the client sent on the other side. This protocol information is left to the implementor of client and server.
ios << n * 2;
return L4_EOK;

The answer is written to the UTCB and with L4_EOK we tell the communication framework that all handling was correct. The framework will thereafter send the reply to the client.

Server setup

In our main function we set up our server and the Server_object we implemented and let the server loop forever for accepting incoming IPCs and answering them.

Util::Registry_server<> server;

We instantiate a Registry_server which will be the heart of our server task. It maintains a registry, where objects of type Server_object are registered and implements the loop() method, which waits for incoming IPC and dispatches incoming messages between the registered Server_objects.

Cap<void> cap = server.registry()->register_obj(&simple_server, "my_server_side");
if (!cap.is_valid())
{
    std::cerr << "invalid cap on register_obj 'my_server_side'" << std::endl;
    return 1;
}

This call registers our instance of SimpleServer in the registry() of the Registry_server. As we already saw in the ned.lua file, there is the name "my_server_side" and we use it here to say that whenever an IPC is sent to the IPC gate named "my_server_side" the IPC is forwarded to the dispatch method of simple_server.

Notice that also register_obj can return an invalid Cap and we want to check and handle this case.

Now we have pretty much set up everything we need to let interaction of a client and a server happen.

server.loop();

This invokes the loop() function implemented by the communication framework. It loops forever and deals with IPCs by forwarding them to our own Server_object and sends answers back.

Conclusion

We have seen a simple setup for a client and a server task in L4Re.

  • On the client side, you can read and write to an UTCB through an Ipc::Iostream object. Then, you use methods like call to initiate an IPC.
  • On the server side, you implement Server_objects and register them in a Registry_server. Underlying communication framework magic waits for incoming messages and calls your server object's dispatch() method, which then handles the request and prepares an answer.