This chapter explains the role of actors in a ChorusOS operating system application. It contains the following sections:
"Actor Definition" defines the term actor, and explains how an actor is named and used.
"Loading Actors" explains how to load an actor.
"Execution Environment of Actors" explains how the execution environment of an actor is defined.
"Spawning an Actor" explain the ways in which an actor can be run.
An actor is the unit of loading for an application. It serves also as the encapsulation unit to associate all system resources used by the application and the threads running within the actor. Threads, memory regions and communication end-points are some examples of these resources. They will be covered in more detail throughout this chapter. All system resources used by an actor are freed upon actor termination.
Some resources, known as anonymous resources, are not bound to a given actor. They must be freed explicitly when they are no longer required. Examples of anonymous resources are physical memory, reserved ranges of virtual memory, and interrupt vectors.
The ChorusOS operating system is dedicated to the development and execution of applications in a host-target environment where applications are developed, compiled, linked and stored on a host system and then executed on a target machine where the ChorusOS operating system is running. When properly configured, the ChorusOS operating system offers convenient support for writing and running distributed applications.
Within the ChorusOS operating system environment, an application is a program or a set of programs usually written in C or C++. In order to run, an application must be loaded on the ChorusOS runtime system. The normal unit of loading is called an actor and is loaded from a binary file located on the host machine. As with any program written in C or C++, an actor has a standard entry point:
int main() { /* A rather familiar starting point, isn't it? */ }
The code of this type of application will be executed by a main thread which is automatically created at load time by the system. The ChorusOS operating system provides means to dynamically create and run more than one thread in an actor. It also offers services which enable these actors, whether single-threaded or multithreaded, to cooperate, synchronize, exchange data either locally or remotely, or get control of hardware events, for example. These topics will be covered step by step throughout this chapter.
An actor may be of two types: it may be either a supervisor actor or a user actor. This information defines the nature of the actor address space. User actors have separate and protected address spaces so that they cannot overwrite each other's address spaces. Supervisor actors use a common but partitioned address space. Depending on the underlying hardware, a supervisor actor can execute privileged hardware instructions (such as initiating an I/O), while a user actor cannot.
In flat memory, supervisor and user actors share the same address space and there is no address protection mechanism.
Binary files from which actors are loaded may also be of two kinds: either absolute or relocatable. An absolute binary is a binary where all addresses have been resolved and computed from a well-known and fixed basis which may not be changed. A relocatable file is a binary which may be loaded or relocated at any address.
Both user and supervisor actors can be loaded either from absolute or relocatable binary files. However, common practice is to load them from relocatable files to avoid a static partitioning of the common supervisor address space, and to allow the loading of user actors into this space in the flat memory model. This is covered in more detail in "User and Supervisor Actors".
Every actor, whether it is a boot actor or a dynamically loaded actor, is uniquely identified by an actor capability. When several ChorusOS operating systems are cooperating together over a network in a distributed system, these capabilities are always unique through space and time. An actor may identify itself with a predefined capability:
K_MYACTOR
.
In addition, an actor created from the POSIX personality is identified by a local actor identifier. This actor identifier is displayed on the console as the result of the arun command. It may be used from the console as a parameter of the akill command.
% rsh target arun hello Started aid = 13 % |
In this example, target is the name of your target.
There are two main kinds of actors which may be run within the ChorusOS operating system environment: user actors and supervisor actors. A user actor runs in its own private address space so that if it attempts to reference a memory address which is not valid in its address space, it will encounter a fault and, by default, will be automatically deleted by the ChorusOS operating system.
Supervisor actors do not have their own fully contained private address space. Instead, they share a common supervisor address space, which means that an ill-behaved supervisor actor can access, and potentially corrupt, memory belonging to another supervisor actor. The common supervisor address space is partitioned between the ChorusOS operating system components and all supervisor actors.
As supervisor actors reside in the same address space, there is no memory context switch to perform when execution switches from one supervisor actor to another. Thus, supervisor actors provide a trade-off between protection and performance. Moreover, they allow execution of privileged hardware instructions and so enable device drivers, for example, to be loaded and run as supervisor actors.
On most platforms, the address space is split into two ranges: one reserved for user actors and one for supervisor actors (see Figure 5-1). As user actor address spaces are independent and overlap each other, the address where these actors run is usually the same, even if the actors are loaded from relocatable binaries. On the other hand, available address ranges in supervisor address space may vary depending on how many and which supervisor actors are currently running. Since the ChorusOS operating system is able to find a slot dynamically within the supervisor address space to load the actor, the user does not need to be aware of the partitioning of the supervisor address space: using relocatable binary files will suffice.
The ChorusOS operating system offers a way to determine dynamically whether a program is currently running as a user or a supervisor actor:
#include <chorus.h> int actorPrivilege(KnCap* actorCap, KnActorPrivilege* old, KnActorPrivilege* new);
If actorCap
is set to the name of an actor,
you can use this API to obtain the privilege of the named
actor. If actorCap
is set to the predefined value K_MYACTOR, you can obtain the privilege of the current actor. This
call may also be used to dynamically change the privilege of an actor from
user to supervisor or vice versa.
The following example illustrates a usage of the actorPrivilege() service. It is a small program that retrieves its privilege, without trying to modify it. It prints one message if the actor is running as a user actor, and another if it is running as a supervisor actor.
#include <stdio.h> #include <chorus.h> int main(int argc, char** argv, char** envp) { KnActorPrivilege actorP; int res; /* Get actor's privilege */ res = actorPrivilege(K_MYACTOR, &actorP, NULL); if (res != K_OK) { printf("Cannot get the privilege of the actor, error %d\n", res); exit(1); } if (actorP == K_SUPACTOR) { printf("This actor is running as a supervisor actor\n"); } else { printf("This actor is running as a user actor\n"); } exit(0); }
KnActorPrivilege is the type defined by the ChorusOS operating system to handle the type of an actor. The defined values for the actor type are:
K_SUPACTOR
: a supervisor actor running
in the supervisor address space which can access all privileged kernel calls.
K_SYSTEMACTOR
: a trusted
user actor, launched at boot time by the kernel and running in its own user
space address, which can access certain privileged kernel calls.
K_USERACTOR
: a user actor
running in its user space. It has fewer privileges than K_SYSTEMACTOR
.
Actors may be loaded in two different ways: either at system boot time or dynamically.
The ChorusOS operating system is started from a bootable file, called the system image, which is loaded in memory either by a hardware boot or a primary boot, depending on the hardware. This bootable file contains the image of the system to be run on the target machine.
The ChorusOS operating system environment provides tools to configure this system image with user provided actors, which may be user or supervisor actors. Once the system has performed its own initialization, it starts these actors automatically, creating a main thread in each of them. These actors are often referred to as boot actors.
In order to be able to dynamically load an application on a ChorusOS
operating system, the system must have been configured with the ACTOR_EXTENDED_MNGT
feature. In this type of configuration, the ChorusOS
operating system is able to dynamically load binary files from the host system
acting as an NFS server, from a local disk, or from the
system image (/image/sys_bank). This host-target environment
enables the user to load supervisor and user actors using a simple remote
shell mechanism. To execute an application called hello
on the target host moon, use the arun
command, as follows:
% rsh moon arun hello |
The ChorusOS operating system uses the .r suffix to denote relocatable binary files.
A relocatable actor is executed as follows:
% rsh moon arun mySupAppl.r |
In this example, the .r suffix could be omitted, because the ChorusOS operating system looks first for the name as specified, mySupAppl, and then, if it does not find a file of that name, automatically looks for a file of that name with the suffix .r, mySupAppl.r.
The execution environment of actors varies slightly depending on whether the actors have been loaded dynamically or at boot time.
An actor loaded at boot time does not have any arguments or environment.
If it is linked with the embedded library, it may perform very simple input
or output operations such as printing traces on the system console using the printf() C library routine. It may also read characters typed in
from the keyboard of the system console through the scanf()
C library routine. If it is linked with the libcx.a library,
it must first open /dev/console three times in order
to activate stdin, stdout, or stderr and allow printf() or scanf() operations on the system console. The main thread of an actor
loaded at boot time will belong to the SCHED_FIFO
scheduling policy, with an arbitrary priority depending on the rank of the
actor within the system image. The size of the stack provided to the main
thread of this type of actor is defined by a system-wide tunable parameter.
A dynamically loaded actor is started as a regular C program with arguments and environments:
int main(int argc, char** argv, char** envp) { /* Main routine of a dynamically loaded actor */ /* regardless of whether the actor is a user */ /* or supervisor actor. */ }
The standard input, output and error files of an extended actor may be redirected so that the I/O operations performed by this actor occur either on the system console, on a regular file (accessed through NFS) or on a terminal window of the host system. The main thread of a dynamically loaded actor has its scheduling policy, priority and stack size set according to system-wide tunable parameters.
The precise context of an actor depends on how the system is configured. An extended actor has a file context similar to the file context of a UNIX process: it has a root directory as well as a current directory. It may also create, open, close, read and write files or sockets.
An extended actor runs on behalf of a user who is identified by means of a credentials structure. The actor credentials include: the identifier of the user, the identifier of the group of the user as well as a possibly empty list of identifiers of supplementary groups. Readers familiar with the concept of credentials in UNIX should note that the ChorusOS operating system concept of credentials is simpler than the UNIX one. ChorusOS 4.0 does not differentiate between real or effective user/group identification as it is not supported.
These actor credentials are used for file access. They are also used when the ChorusOS operating system runs in secured mode to check the validity of an operation. For example, in secured mode only the superuser, whose user identifier is 0, may load supervisor actors.
An extended actor may take advantage of the entire C library for dealing
with I/O. In addition to the I/O interface provided by the C library, an extended
actor may also use POSIX I/O services such as open
, read
, or write
, as well as POSIX socket
services such as socket
, bind
,
and connect
.
The following program may be run as an actor, and illustrates the way in which the C library might be used from an actor.
#include <stdio.h #include <stdlib.h> #include <chorus/stat.h> #define BUF_SIZE 80 struct stat st; int main(int argc, char** argv, char** envp) { FILE* file; FILE* filew; char* buf; int res; if (argc != 2 && argc != 3) { fprintf(stderr, "Usage: %s filename\n", argv[0]); exit(1); } file = fopen(argv[1], "r"); if (file == NULL) { fprintf(stderr, "Cannot open file %s\n", argv[1]); exit(1); } res = stat(argv[1], &st); if (res < 0) { fprintf(stderr, "Cannot stat file\n"); exit(1); } printf("File size is %d mode 0x%x\n", st.st_size, st.st_mode); buf = (char*) malloc(BUF_SIZE); if (buf == NULL) { fprintf(stderr, "Cannot allocate buffer\n"); exit(1); } bzero(buf, BUF_SIZE); res = read(fileno(file), buf, 80); if (res == -1) { fprintf(stderr, "Cannot read file\n"); exit(1); } printf("%s\n", buf); if (argv[2] != NULL) { filew = fopen(argv[2], "w"); if (filew == NULL) { fprintf(stderr, "Cannot open file %s\n", argv[2]); exit(1); } printf("Type any input you like: \n"); do { scanf("%80s", buf); printf("buf=%s\n", buf); fprintf(filew, "%s", buf); printf("buf=%s\n", buf); } while (buf[0] != 'Q'); } exit(0); }
This example assumes that argv[0] is valid, and the actor is linked with the ~lib/classix/libcx.a library, since the library is common to both user and supervisor actors. Referencing argv[0] without checking if argc is greater than zero can cause the actor to make an exception and be deleted.
In any C program, memory can be dynamically allocated by means of the malloc() C library routine within actors, whether loaded at boot time or dynamically, and whether running as user or supervisor actors.
Example 5-2 shows a usage of the malloc() routine.
As shown in the previous example, an actor may terminate by invoking the exit() routine, as with any typical C program. Invoking exit() ensures that all resources used by the actor are freed: I/O buffers will be flushed, all open files are closed, and all other system resources provided by features configured within the system are released automatically.
So far, two ways of loading and running actors have been described: either the inclusion of the actor as part of the system image, or the usage of the arun mechanism. The ChorusOS operating system also enables an actor to dynamically spawn another actor from a binary file. This spawned actor may be either a supervisor or a user actor. This service is similar to the exec() UNIX system call:
#include <am/afexec.h> int afexecve (const char* path, KnCap* actorCap, const AcParam* param, char* const* argv, char* const* envp);
This service creates a new actor whose capability is returned by the system at the location pointed to by the actorCap argument. The actor created will execute the binary file stored in the file named path. The main thread of this actor will run the main routine of the program. This thread will have the same scheduling attributes and stack size as an actor loaded using the arun mechanism.
argv and envp are pointers to the array of arguments and environments that will be received by the newly created actor.
The afexec() service comes in several variants, similar to the UNIX exec() call variants. When successful, all afexec() routines return the actor identifier of the newly created actor. Otherwise, they return -1, and the error code is returned in the errno variable.
Most of the time, application writers will not need to use the afexec() service. They will use either the arun facility or include the actor as part of the system image. However, for convenience, some examples within this document do use this service.
Below is an example of use of the afexecve() service call.
A user actor loaded by arun spawns another user actor, running the same executable file.
They both print a trace and terminate.
The first actor is distinguished from the one it spawns by the number of arguments: the first actor has no argument, but spawns the second actor passing it a string as its first argument. This string is printed by the spawned actor.
The first actor prints the actor identifier of the spawned actor.
#include <stdio.h> #include <errno.h> #include <am/afexec.h> AcParam param; char* spawnedArgs[3]; char* tagPtr = "Welcome newly created actor!"; main(int argc, char** argv, char**envp) { KnCap spawnedCap; int res; if (argc == 1) { /* * This is the first actor (or spawning actor): * Binary file used to load this actor is passed * by "arun" as argv[0], * * Set an argument in order to enable the second * actor to know it is the second one. */ param.acFlags = AFX_USER_SPACE; spawnedArgs[0] = argv[0]; spawnedArgs[1] = tagPtr; spawnedArgs[2] = NULL; /* * Other fields are implicitly set to NULL, as * param is allocated within the bss segment of * the program. */ res = afexecve(argv[0], &spawnedCap, ¶m , spawnedArgs, envp); if (res == -1) { printf("Cannot spawn second actor, error %d\n", errno); exit(1); } printf("I succeeded creating actor whose aid is %d\n", res); } else { /* * This is the spawned actor: * Check the number of args, * Print args, * Exit */ if ((argc == 2) && (strcmp(tagPtr, argv[1]) == 0)) { /* * This is really the spawned actor. */ printf("My spawning actor passed me this argument: %s\n", argv[1]); } else { printf("You ran %s with an argument, you should not!\n", argv[0]); exit(1); } } exit(0); }
This example assumes that argv[0] is valid, and the actor is linked with the ~lib/classix/libcx.a library, since the library is common to both user and supervisor actors. Referencing argv[0] without checking if argc is greater than zero can cause the actor to make an exception and be deleted.
A null AcParam argument instructs the system to use default values for afexec() calls.
The acFlags field indicates, among other possibilities, whether the actor should be created as a user actor (when the flag is set to AFX_USER_SPACE), or as a supervisor actor (when the flag is set to AFX_SUPERVISOR_SPACE). These values are mutually exclusive: one and only one of the two values may be set. In addition, the user must make sure that the value of the flag is consistent with the binary file used to load the actor. Trying to create a supervisor actor with a binary file prepared for a user actor, by linking with the user libraries, will result in an error.
The AFX_ANY_SPACE option can be passed to instruct the operating system to retrieve the privilege of the binary file and create an actor with the same privilege.
Unused fields of the AcParam argument must be set to 0.