This chapter defines the concepts of actors and processes as they relate to ChorusOS systems. It explains how actors relate to POSIX processes, and demonstrates the use of the application programming interfaces that enable applications to handle actors and processes.
Although actors and processes have similar functionality, they are used in different contexts and different rules apply to them. Once you have completed this chapter, you will understand how actors and processes differ, what kinds of applications you are creating, and what rules apply to your applications.
Within the ChorusOS operating system environment, an application is a program or a set of programs usually written in C or C++. An application must be loaded on the ChorusOS runtime system to run. The normal unit of loading is called an actor or a process. It is loaded from a binary file located on the host machine or on a ramdisk or physical disk on the target.
Applications are loaded from binary files, which can be either absolute or relocatable. In an absolute binary, all addresses have been resolved and computed from a well-known, fixed basis that cannot be changed. A relocatable binary can be loaded or relocated at any address. Absolute binaries are executed in place in the system image and are built from standard relocatable binaries when building the system image. Absolute binaries cannot use dynamic or shared libraries.
Both user and supervisor applications can be loaded from either absolute or relocatable binary files. Common practice is to load them from relocatable files to avoid a static partitioning of the common supervisor address space, and to enable the loading of user applications into the address space in the flat memory model.
Whether an application is defined as an actor or a process is determined by the set of APIs to which the application has access. The ChorusOS APIs are discussed in detail in "ChorusOS APIs". For now, it is sufficient to know that there are two main sets of APIs:
POSIX APIs
Microkernel APIs
Applications that have access to the POSIX API are called processes while applications that have access to the Microkernel API are called actors. This distinction is illustrated in Figure 3-1.
The POSIX subsystem provides a complete set of POSIX APIs that enable you to create POSIX-compatible applications. These applications are called processes. A process is the unit of encapsulation of the POSIX subsystem.
A process is identified by a local process identifier (PID). This identifier is displayed when the process is executed. The PID can be passed as a parameter of the akill command to kill a process.
A process can be defined as a ChorusOS application that uses the POSIX APIs. The C_OS, shown in Figure 3-1, manages I/O operations and provides the POSIX interface to applications. The C_OS also manages file systems, networking and shared memory. Processes have access to the complete set of POSIX APIs and extensions. Microkernel APIs must not be used by processes (except for a small number of explicit services). For more information on the APIs that can be used with processes, refer to "ChorusOS APIs".
Processes are dynamic and cannot be loaded alongside the C_OS and drivers at boot time. However, it is possible to include processes as part of the sysadm.ini file, which is used to specify system initialization commands. For more information, see "System Administration in the Extended Profile" in ChorusOS 5.0 System Administrator's Guide
An actor is the unit of encapsulation of resources with regard to the ChorusOS microkernel.
Actors are linked only with the microkernel libraries and are completely unknown to the C_OS. A process can load or run a microkernel actor dynamically using the POSIX-like afexec() microkernel service.
For more information on loading actors, see "Spawning an Actor".
An actor can be embedded, which means that it becomes part of the system image. However, actors are not necessarily embedded and can be located on a separate disk or file system. Embedded actors can be bootable, which means that they are executed as the ChorusOS system is booted. Embedded actors that are not boot actors can be loaded by other embedded actors using the afexec() system call.
Actors cannot access POSIX services. If microkernel services and POSIX services are used together in the same application, the behaviour of the application is unpredictable and can even cause the system to crash.
An actor is represented by its capability (KnCap
),
which is allocated by the microkernel.
Typically, drivers are embedded actors, however drivers do not require the services of the POSIX subsystem and can be started dynamically through the afexec() system call.
Processes and actors are similar in many ways. In most instances, the same functionality is available to both actors and processes. However, different system calls are required to obtain this functionality.
A process can be defined as an actor that uses the services of the POSIX API. Therefore, all processes are actors and have a capability KnCap. The reverse is not true, that is, not all actors are necessarily processes.
Processes are linked to the os/lib/libc.a library. Actors such as drivers should never be linked to the os/lib/libc.a library and therefore cannot be processes.
The rsh targetname aps command enables a list of all processes running on a target to be displayed.
To the microkernel, all entities running on the system (both actors and processes) are actors.
Every actor, whether it is a boot actor or a dynamically
loaded actor, is uniquely identified by an actor capability (KnCap
). When several ChorusOS operating systems are functioning over
a network in a distributed system, these capabilities are always unique through
space and time. An actor may identify itself with the predefined capability:
K_MYACTOR
The microkernel attributes a capability (KnCap
) to
each actor. A process is also identified by a local process identifier (PID).
This identifier is displayed when the process is executed manually.
Applications are classified as either supervisor or user applications. This classification is called the privilege of the application. User applications have separate and protected address spaces so that they cannot overwrite each other's address spaces. Supervisor applications use a common (but partitioned) address space. Depending on the underlying hardware, a supervisor application can execute privileged hardware instructions (such as initiating I/O), while a user application cannot.
The ChorusOS operating system offers a method to determine dynamically whether an application is currently running as a user or supervisor process or actor. The example below demonstrates how to find the privilege of an actor. The same call can be used to find the privilege of a process.
#include <chorus.h> int actorPrivilege ( KnCap* actorCap, KnActorPrivilege* old, KnActorPrivilege* new);
In the previous example, 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. If the actor is a privileged (supervisor or system) actor, this call can also be used to change the privilege of an actor dynamically from user to system or vice versa.
The following example illustrates the use of the actorPrivilege(2K) service. It is a small program that retrieves its privilege without trying to modify it. The program prints one message if the actor is running as a user actor, and another if it is running as a supervisor actor.
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 that can access all privileged microkernel calls.
K_SYSTEMACTOR: a trusted user actor, launched at boot time by the microkernel and running in its own user address space. This actor can access certain privileged microkernel calls. A system actor can also be launched using rsh with the -T option (where -T indicates trusted).
K_USERACTOR: a user actor running in its own user space. It has fewer privileges than K_SYSTEMACTOR.
#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/system actor\n"); } exit(0); }
Actors have a different execution environment depending on whether they are loaded dynamically or at boot time. Processes have the same execution environment because they are always loaded dynamically.
An actor loaded at boot time has no arguments or environment. Consequently, argc is set to 0, and argv and argp are null pointers. If an actor loaded at boot time is linked with the embedded library, it can perform basic I/O operations such as printing traces on the system console using the printf() C library routine. It can also read characters entered at the keyboard through scanf() and similar C library routines. If the actor is linked with the libc.a library, it must open /dev/console three times to activate stdin, stdout, or stderr (to enable the printf() and scanf() operations).
The main thread of an actor loaded at boot time belongs to the SCHED_FIFO scheduling policy (see "Setting Scheduling Attributes") . The main thread has 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.
Start a dynamically loaded actor or processes as a regular C application with arguments and an environment:
int main(int argc, char** argv, char** envp) { /* Main routine of a dynamically loaded application */ /* regardless of whether the application is a user */ /* or supervisor process/actor. */ }
The standard I/O and error files of a dynamically loaded actor or process may be redirected so that the I/O operations performed by this application occur either on the system console, a regular file (accessed through NFS), or a terminal window of the host system. The main thread of a dynamically loaded application has its scheduling policy, priority, and stack size set according to system-wide tunable parameters.
The context of an application depends on how the system is configured. A process 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 can also create, open, close, read and write files or sockets.
A process runs on behalf of a user who is identified by a credentials structure. The process credentials include:
the identifier of the user (uid
)
the identifier of the user group (gid
)
a list (possibly empty) of identifiers of supplementary groups
The process credentials are specified in the data structure cx_cred_t, which includes the following members:
uid_t cr_uid; /* process's user ID */ gid_t cr_gid; /* process's group ID */ unsigned short cr_ngroups; /* number of groups in cr_groups */ gid_t cr_groups[]; /* supplementary group list */
The ChorusOS operating system concept of credentials is simpler than the UNIX one. The ChorusOS operating system does not differentiate between real or effective user and group identification because it is not supported.
These process credentials are used for file access and also when the ChorusOS operating system runs in secure mode to check the validity of an operation. For example, in secure mode only the superuser, whose user identifier is 0, can load supervisor actors.
A process can take advantage of the entire C library for dealing with I/O. In addition to the I/O interface provided by the C library, a process 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 can be run as a process and illustrates the way in which the C library can be used from a process.
#include <stdio.h> #include <stdlib.h> #include <chorus.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); }
Referencing argv[0] without checking if argc is greater than zero can cause the process to incur an exception and be deleted.
In any C program, memory can be dynamically allocated by means of the malloc() C library routine within actors and processes. It makes no difference whether the actors or process are loaded at boot time or dynamically, or whether they are running as user or supervisor applications.
You can terminate an application by invoking the exit() routine, as with any typical C program. Invoking exit() ensures that all resources used by the application are freed. I/O buffers are flushed, all open files are closed, and other system resources provided by features configured within the system are automatically released.
Processes can be spawned using the posix_spawn system call. Actors can also be spawned using the afexec system call.
Processes can also be launched using the fork() and exec() system calls. For more information, see the fork(2POSIX) and exec(3POSIX) man pages.
A process can spawn another process dynamically using the posix_spawn() or posix_spawnp() function. This spawned process can be either a supervisor or a user process.
Example 3-3 creates a new process. The spawned process executes a binary file stored in the file named path. Note that the example uses posix_spawnp() and not posix_spawn(). The posix_spawnp() function searches for the binary file in all directories specified by the PATH environment variable. If you use posix_spawn() instead, you must specify the full path to the binary file.
The main thread of this process runs the main routine of the program. This thread has the same scheduling attributes and stack size as an embedded actor loaded manually.
argv and spawnedEnv are pointers to the array of arguments and environments that are received by the newly created process.
If successful, all posix_spawn() and posix_spawnp() routines return 0. Otherwise, they return an error code.
The value of errno is unspecified.
In the example:
A user process spawns another user process, running the same executable file.
Both print a trace and terminate.
The parent process is distinguished from the one it spawns by the number of arguments; the parent process has no argument but spawns the child process and passes it a string as its first argument. This string is printed by the child process.
The parent process prints the process identifier (PID) of the child process.
#include <stdlib.h> #include <stdio.h> #include <errno.h> #include <span.h> #include <string.h> char* spawnedArgs[3]; char* spawnedEnv[1]; char* tagPtr = "Welcome newly created process!"; int main(int argc, char* argv[]) { int res; pid_t pid; if (argc == 1) { /* * This is the first process (or spawning process): * Binary file used to load this process is passed * as argv[0], * * Set an argument in order to enable the second * process to know it is the second one. */ spawnedArgs[0] = argv[0]; spawnedArgs[1] = tagPtr; spawnedArgs[2] = NULL; spawnedEnv[0] = NULL; res = posix_spawnp(&pid, spawnedArgs[0], NULL, NULL, spawnedArgs, spawnedEnv); if (res != 0) { printf("Cannot spawn second process, error %d\n", res); exit(1); } printf("I successfully created a process whose pid is %d\n", pid); } else { /* * This is the spawned process: * Check the number of args, * Print args, * Exit */ if ((argc == 2) && (strcmp(tagPtr, argv[1]) == 0)) { /* * This is really the spawned process. */ printf("My spawning process passed me this argument: %s\n", argv[1]); } else { printf("You ran %s with an argument. Do not!\n", argv[0]); exit(1); } } exit(0); }
The ChorusOS operating system enables an actor to spawn another actor dynamically 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 <cx/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 executes the binary file stored in the file named path. If the file named path does not exist, afexec() will attempt to find path.gz. The main thread of this actor runs the main routine of the program. This thread has the same scheduling attributes and stack size as an actor loaded using the arun command.
argv and envp are pointers to the array of arguments and environments that will be received by the newly created actor.
There are several variants of the afexec(2K) service that are similar to the UNIX exec() call variants. If successful, all afexec(2K) routines return the actor identifier of the newly created actor. Otherwise, they return -1, and the error code is returned in the errno variable.
In most instances, you do not need to use the afexec(2K) service. Instead, simply execute the actor manually, or include the actor as part of the system image. However, for convenience, some examples within this chapter do use this service.
The following example illustrates the use of the afexecve(2K) service call. In this example:
A user actor spawns another user actor, running the same executable file.
They both print a trace and terminate.
The parent actor is distinguished from the one it spawns by the number of arguments; the parent actor has no argument but spawns the child actor, passing it a string as its first argument. This string is printed by the child actor.
The parent actor prints the actor identifier of the child actor.
This example assumes that argv[0] is valid, and that the actor is linked with the os/lib/libc.a library because 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 create an exception and be deleted.
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> #include <cx/afexec.h> AcParam param; char* spawnedArgs[3]; char* tagPtr = "Welcome newly created actor!"; int 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_ANY_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 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 pid 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); } } return 0; }
A null AcParam argument instructs the system to use default values for afexec(2K) 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 - only one of the two values can be set. In addition, ensure that the value of the flag is consistent with the binary file used to load the actor. Attempting 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 binary file privilege and create an actor that has the same privilege.
Unused fields of the AcParam argument must be set to 0.
An actor that does not require the services of the C_OS, and created with actorCreate(), can be deleted using the actorDelete() call, (provided you have the actor capability).
void main() { KnActorPrivilege actPriv; KnCap actCap; int err; actPriv = K_SUPACTOR; err = actorCreate(K_MYACTOR, &actCap;, actPriv, K_STOPPED); if (err != K_EOK) { printf("actorCreate %d\n", err); } err = actorDelete(&actCap;); if (err != K_EOK) { printf("actorDelete %d\n", err); } }
Processes can be deleted using the standard POSIX call kill(). See the kill(2POSIX) man page for more information.
The following examples demonstrate the basic commands you can use with either processes or actors:
An embedded actor is launched at boot time. A standard actor or process can be loaded manually from the host, using rsh.
To launch a process or actor with specific options, use the arun command:
% rsh targetname arun -k processname |
To launch a process or actor without runtime options, enter the application name at the command line.
% rsh targetname processname |
or
% rsh targetname actorname |
Use the aps command to list all processes running on a target. This command displays the following information for each process:
User ID (UID)
Process ID (PID)
Process name (NAME)
Debugging status (DBG)
Process status (STAT)
Group ID (GROUP)
% rsh targetname aps UID PID NAME DBG STAT GROUP 0 12 MY_PROG 0 SRUN 0 0 3 HR_CTRL 0 SRUN 0 0 2 C_INIT 0 SRUN 0 0 1 init 0 SRUN 0 0 0 unused 0 SRUN 0 |
For more information, refer to the aps(1M) man page.
Use the akill command to kill a process, passing the PID as a parameter:
% rsh targetname akill pid |
The previous example showed a process called MY_PROG with a pid of 12 running on the target. To kill this process, you would type the following:
% rsh targetname akill 12 |
For more information, refer to the akill(1M) man page.
The ChorusOS operating system does not provide a command to terminate an actor. Actors cannot be terminated using the akill command. They can be terminated using the akill(2K) or the actorDelete(2K) system calls.