Next: Context Implementation Up: Implementation Previous: Host model

Process Implementation

C-with-Ease has two forms of process creation: cooperation and subordination. Subordination is like fork, the process executing the subordinate statement may continue as soon as the process is created (although the actual order of execution is undefined). Cooperation differs in that the creating (parent) process waits for all created (child) processes to complete before returning.

An Ease process creation statement allows processes to be created with any number and type of parameters. Since the created process will execute within any thread on the Ease host a flexible system for passing parameters to the created process is required.

Initial attempts at parameter passing to created processes used the thread creation function's knowledge of how parameters were placed on the stack by a C function call. This required the thread creation function to know the total size of the function's parameters on the stack. While this system worked with one threads package (the Sun lwp library), it was unsuitable for use on a distributed memory machine or with most other threads libraries (some of which only provide for supplying a single parameter to the created thread's function).

The mechanism developed uses a number of generated stub functions for each function that is used in Ease process creation statements. The stub_in function takes all of its subject function's parameters and packs them into a structure. This structure and its size are then passed to the Ease library which can then pass it on to another cell in a message or to a new or existing thread. A generated stub_out function takes the structure as a parameter and call its subject function with the parameters from the structure.

Since the stub function mechanism uses no special knowledge about the host architecture's function calling mechanisms or stack layout it is completely portable. Is has the added advantage over other systems (such as using varargs) of providing compile time checking of function parameter types.

Syntax and code generation

Recall the syntax for process creation is as follows:

The C-with-Ease code @subordinate func1(param); is translated into:

   {
      void *_ec_childs=0;
      _ec_childs = _stub_in_func1(_ec_childs,0,param);
   }
A new block is created to encapsulate the variable _ec_childs. This variable is used in cooperation statements to help generate a list of processes for which the parent must wait to complete. _ec_childs is present in subordination statements to simplify the translator - much of the translation is the same as that for cooperation.

The stub function, _stub_in_func1 in this example, is generated from the process declaration for the corresponding function. This function packs the function's arguments into a structure which can then be passed to the process that is created. The stub functions are described in section 5.2.2.

The subordination with replication statement @subordinate (i @for 10) func1(i); is translated into:

   {
      void *_ec_childs=0;
      int ___REPLIC(i, 10, 0, 1) 
         _ec_childs = _stub_in_func1(_ec_childs,0 ,i);
   }

The macro ___REPLIC is used to simplify the translator. Its arguments are the variable to use, the number of processes, the initial value for the variable and the step value. The macro definition is:

  #define ___REPLIC(var,count,base,step) var; \
    int __step = step; int __base = base; int __count = count;  \
    for (var = __base; var < __base + __count*__step; var += __step)

The variables __step etc are used to ensure that the expressions passed as arguments to the macro are only calculated once.

The cooperation @cooperate { func1(param); func2(param); } is translated into:

   {
      void *_ec_childs=0;
      _ec_childs = _stub_in_func1(_ec_childs,1,param) ;
      _ec_childs = _stub_in_func2(_ec_childs,1,param);
      __ease_wait_for_children(_ec_childs);
   }

Here the _ec_childs variable is used to build up a list of the processes. __ease_wait_for_children() will return once all of the child processes in the list have completed.

Process Function Declarations

 

In order to use a function in an Ease process creation statement appropriate stub functions must be generated to assist in packing function arguments into structures and for unpacking and calling the function in the created process. The @process declaration is used for this.

In C-with-Ease , the declaration @process func2(int i, double d) { /* ... */ } is translated into the following: a structure to hold the arguments, a forward declaration, a _stub_out function, a _stub_in function and finally the function preamble itself.

The structure to hold the arguments is as follows:

  struct _stub_struct_func2
  {
    _ec_special _ec_specials;
    int i;
    double d;
  };

Note that the structure also contains the an _ec_special structure, which contains a pointer to the stub out function and a flag to indicate whether this particular process is part of a cooperation. The Ease library expects to be passed an _ec_special so this component is at the start of the structure.

Following the structure is a prototype for the subject function and the stub out function, as follows:

  void func2(int i, double d);
  void 
  _stub_out_func2 (struct _stub_struct_func2 *p, int len)
  {
    func2(p->i, p->d);
  }

The stub_out function simply unpacks the structure and calls the correct function. The forward declaration for the function is provided to keep C (and C++ ) compilers happy. This function is called in the created process.

The stub_in function is called in response to a process creation statement.

  void *
  _stub_in_func2 (void *_ec_childs, int _ec_parent, int i, double d)
  {
    struct _stub_struct_func2 _stub_var =
    {
      {(void (*)(void *, int)) _stub_out_func2, _ec_parent},
      i, d
    };
    return _ec_create_process (_ec_childs, "func2",
      (_ec_special *) & _stub_var, sizeof (_stub_var));
  }
The _stub_in function packs all the arguments into the appropriate structure and calls _ec_create_process. The packing of parameters into the structure is a binary copy, so using pointer parameters will not always work as the process they are passed to may be in a separate address space. A string with the name of the function is passed for debugging purposes and also for future expansion: currently the system assumes that functions have the same address in all processes, using the name string will allow functions to be at any address provided they have been registered.

Finally the function itself is output:

  void func2 (int i, double d) { /* ... */ }
Note that Ease processes return void since they do not return a value to the caller (except via contexts).

Process Function Declarations in C++ -with-Ease

 

xxx Move this section to C++ -with-Ease implem section

In C++ -with-Ease , the generated code for process declarations is somewhat different. This is due to the use of marshalling (further described in section 3.3). This allows variable sized structures (such as lists or arrays) to be passed if appropriate marshalling functions are supplied.

The code generated for @process func2(int i, double d) { /* ... */ } in C++ -with-Ease does not contain a structure declaration like the C-with-Ease code. It begins with a forward declaration and the stub_out function:

  void func2 (int &i, double &d);
  void _stub_out_func2 (void *_ec_p, int _ec_len)
  {
    umStream _ec_s (_ec_p, _ec_len);
    _ec_special _ec_d;
    _ec_s.remove (&_ec_d, sizeof _ec_d);
    _ec_noinit ();
    int i;
    _ec_s >> i;
    double d;
    _ec_s >> d;
    _ec_allowinit ();
    func2 (i, d);
  }

The function definition is changed so that it uses reference paramaters rather than value parameters. This avoids a copy on calling the actual function from the _stub_out function (which could be substantial for larger structures). An unmarshal stream, umStream, is created and initialized with the supplied data. The parameters are then extracted from the stream. The _ec_noinit()/_ec_allowinit() pair switch off automatic context creation during unmarshalling. Usually a new context is created when a context variable is constructed but in this situation the context will be initialized from the marshal stream immediately after construction. These functions also lock the context creation mutex to ensure that only contexts created by the current thread are affected by the _ec_noinit.

To facilitate extension of the Ease library to handle architectures which may have different addresses for a function on different cells, a static structure is generated to register the stub_out function by name:

  static _ec_Process _ec_proc_func2 (_stub_out_func2, "func2");

The class contains no data - it is only used so that its constructor is called upon process initialization.

Using this facility it is quite feasible to make C++ -with-Ease work over a heterogeneous network. The Ease programmer would have to do some extra work with the marshalling functions to ensure that all architectures could marshal into the same binary stream.

The _stub_in function is similar to that for C-with-Ease , except that it uses the marshal stream to pack data rather than a structure:

  void *
  _stub_in_func2 (void *_ec_childs, int _ec_parent,
    const int &i, const double &d)
  {
    _ec_special _ec_d;
    _ec_d.func = _stub_out_func2;
    _ec_d.parent = _ec_parent;
    mStream _ec_s;
    _ec_s.add (&_ec_d, sizeof _ec_d);
    _ec_s << i << d;
    void *_ec_p;
    int _ec_l;
    _ec_p = _ec_s.get (_ec_l);
    return _ec_create_process (_ec_childs, "func2",
      (_ec_special *) _ec_p, _ec_l);
  }

Finally, the function definition is output:

  void func2 (int &i, double &d) { /* ... */ }

Process Messages

The messages involved in process handling are quite simple. A message of type TYPE_CREATE results in a process being started in the receiving cell. The content of the message is passed directly to the appropriate _stub_out function. A TYPE_CHILD message is sent to the creator of a cooperating process when the process exits.

If a process interacts with a context that is dead (that is, the context's creating process has exited), then that process may be sent a TYPE_KILL message, which will cause the process to abort.

The selection of the cell for a new process is host dependent. Ideally a cell would be selected so as to minimize communication costs while maximizing processor utilization (inter cell communication could be minimized by placing all processes on the same cell but this does not make good use of the processors available).

Currently, no attempt is made to minimize communication, an attempt is made to maximize processor utilization by spreading processes evenly over available cells. Since any process may create a new process facilities must be provided on all cells which select a candidate cell. In the current implementations, two schemes are used: a centralized scheme where a single cell (the "main" cell) keeps track of current allocation and other cells interact with it in order to be allocated a cell, and a global scheme where all cells know the current state of the host and select a new cell using their knowledge. The global scheme is really only useful on hosts which have a cheap broadcast facility, such as the Fujitsu AP1000 which has a special broadcast network.

Next: Context Implementation Up: Implementation Previous: Host model




Tim MacKenzie <tym@cs.monash.edu.au>
Mon Apr 1 00:27:29 EST 1996