| iMatix home page
| << | < | > | >>
SMT Logo SMT
Version 2.81

The SMT Kernel

The SMT (Simple Multi-Threading kernel) from iMatix is an add-on for the Libero programming tool that lets you write portable high-performance multithreaded programs based on Libero's finite-state machine (FSM) design method.

You may want to go straight to the Table of Contents.

You can use the SMT kernel for:

The SMT kernel's main features are:

The SMT kernel currently supports C/C++, and is written in portable ANSI C. It uses the iMatix SFL (standard function library) as a technical platform, and the Libero tool as development method. Libero, the SFL, and the SMT kernel are provided as free software, copyrighted by iMatix. You may freely incorporate the SFL and SMT packages in your applications with a minimum of conditions. Read the SMT license agreement for details.

To use the SMT kernel you should have used Libero and understand the concepts involved. SMT applications are essentially written using Libero. We assume that the reader has some familiarity with Libero.

The SMT kernel has been ported to MS-DOS, Windows, UNIX systems (Linux, IBM AIX, SunOS, HP/UX) and should be portable to Digital VMS. It comes with complete sources and documentation in HTML. It includes XITAMI, a portable multithreaded web server.

Send comments about the SMT kernel, bug reports, and submissions to smt@imatix.com.  

Table of Contents

  • Introduction
  • Origins
  • Classic Multithreading Environments
  • The History Of Libero Multithreading
  • Differences with Classic Multithreading
  • Why Use Multithreading?
  • What You Should Know
  • The SMT Kernel Architecture
  • Structure Of An SMT Application
  • Floating Event Queues
  • Routers
  • Internal and External Events
  • Event Management
  • Priorities and Scheduling
  • Native Programs Vs. Foreign Programs
  • The SMT Kernel Meta-State-Machine
  • Using The SMT Kernel API
  • SMT Kernel Functions
  • Initialisation and Termination
  • Creating A Thread
  • Sending and Receiving Events
  • Sending An Initial Event To A Thread
  • Event Lifespan and Acknowledgment
  • Using Priorities
  • Writing A Native SMT Program
  • Initialising An agent
  • The Thread Context Block
  • Choosing Event Names
  • Mechanics Of Event Delivery
  • Waiting For An External Event
  • Sending Structured Event Data
  • Sending Events Within An Agent
  • Ignoring External Events
  • Non-Blocking File Access
  • Real-time Programming
  • Using Semaphores
  • Replacing The Standard Agents
  • Using The SMT API In A Foreign Program
  • Writing A Stub Program
  • Managing A Floating Event Queue
  • Executing The Application
  • Standard SMT Agents
  • The Logging Agent - SMTLOG
  • The Operator Console Agent - SMTOPER
  • The Timer Agent - SMTTIME
  • The Time Slot Agent - SMTSLOT
  • The Socket I/O Agent - SMTSOCK
  • The Transfer Agent - SMTTRAN
  • The TCP ECHO Agent - SMTECHO
  • The HTTP Agent - SMTHTTP
  • The Network Delay Simulation Agent - SMTSIMU
  • SMT API Quick Reference
  • The Unattended Process Monitor (UPM) Tool
  • Overview
  • Technical Details
  • Using The UPM Tool
  • UPM Trouble Shooting
  • Installing The SMT Kernel
  • Availability and Distribution
  • Installation for UNIX Systems
  • Installation for Digital VMS Systems
  • Installation for Windows
  • System-Specific Concerns
  • Windows
  • The SMT License Agreement
  • SMT Kernel API functions
  • smt_init
  • smt_term
  • smt_exec_full
  • smt_exec_step
  • smt_active
  • smt_set_console
  • smt_set_timer
  • smt_atexit
  • smt_shutdown
  • agent_declare
  • agent_lookup
  • agent_destroy
  • method_declare
  • method_lookup
  • method_destroy
  • queue_create
  • queue_lookup
  • queue_destroy
  • queue_deliver
  • queue_flush
  • event_send
  • event_accept
  • event_reject
  • event_expire
  • event_discard
  • event_iterate
  • event_destroy
  • event_wait
  • thread_create
  • thread_lookup
  • thread_destroy
  • semaph_create
  • semaph_lookup
  • semaph_destroy
  • semaph_wait
  • semaph_signal
  • lazy_creat
  • lazy_creat_text
  • lazy_open
  • lazy_open_text
  • lazy_read
  • lazy_write
  • lazy_close
  • senderr
  • sendfmt
  • raise_exception
  • recycle_module
  • smt_set_step
  • smt_crash_report
  • The Standard SMT Agents
  • smtauth_init
  • smtecho_init
  • smthttp_init
  • smtftpc_init
  • smtftpd_init
  • smtrdns_init
  • smtlog_init
  • smtoper_init
  • smtslot_init
  • smtsock_init
  • smtsock_trace
  • smttime_init
  • smttran_init
  • The Network Delay Simulator
  • smtsimu_init
  • smtsimu_sock_init
  • smtsimu_smtsock_init
  • smtsimu_smtsock_trace
  • smtsimu_write_UDP
  • The Unattended Process Monitor
  • smtupmc_init
  • Introduction

    Origins

    SMT 2.0 is an evolution of SMT version 1.x of the iMatix SMT kernel. SMT 1.x was specifically designed for TCP/IP server development. SMT 2.x is a more generalised approach that makes multithreaded FSMs suitable for a variety of purposes. SMT 2.0 is not backwards compatible with SMT 1.x, but it is quite straight-forward to convert applications.

    The main objectives of SMT 1.0 were:

    1. Mulithreading - a cleaner and more efficient approach for certain types of problems than alternatives such as multiprocessing (forking) or iteration.
    2. Simplicity - using the Libero method to simplify an otherwise complex multithreaded model.
    3. Portability - so that a SMT application runs on UNIX, Windows 95, Windows NT, and Digital VMS with similar functionality.

    SMT 2.0 is meant to answer a wider set of issues. Specifically, our goal was to provide multithreading capability in these domains:

    In SMT 1.x, external events were received and processed by a kernel built-in to each SMT program. The SMT 1.x kernel was specifically designed to be driven by TCP/IP events. An SMT 1.x program is invoked, does its work, and finally terminates. We call this type of program a 'batch' program. This is suitable for servers ('daemons'), but not for real-time programs that must be integrated into an large-scale event-driven architecture.

    SMT 2.x is designed as an event-passing kernel. The application consists of a number of agent programs, each running one or more threads. Each thread has an event queue. Threads send each other events, which are queued, and delivered by the kernel to the state machine that controls each thread. The SMT kernel API lets you send and receive events, create threads, etc. There are various ways to construct an agent program (single- threaded, multithreaded), and different ways to handle event queues (one thread per queue, or several threads per queue). Agents and events can have priorities, which changes the order of execution and delivery.

    SMT provides a number of standard agents that are easily reused in applications. For example, in an Internet application, the socket i/o agent collects events from the Internet sockets used by the application. When a thread wants to read data from a socket, it sends an event to the socket i/o agent, telling it which port, and how much data. The socket i/o agent reads the data and returns that as a event. Other standard agents are: a logging agent to write log file data; an operator console agent to handle error and warning events; a timer agent to generate alarm events.

    SMT also includes a number of protocol agents for use in SMT applications: echo, HTTP, FTP.

    In SMT 1.x, external events such as TCP/IP events were collected by the kernel, in SMT 2.x such events are collected by an agent program. Thus it is possible to add support to any external event source.

    Classic Multithreading Environments

    Multithreaded programming is often perceived as complicated. When we look at multithreading facilities provided by existing operating systems, we tend to agree. The most common type of multithreading is pre-emptive multithreading. This is typically seen on UNIX and Windows NT systems. The characteristics of this approach are:

    A less common type of multithreading is cooperative multithreading. One example is the chained multithreading method used on Digital VMS systems. The characteristics of this approach are:

    If we compare these methods, we can see advantages and disadvantages in each:

    Both these methods are expensive to program, and can produce code that is hard to maintain, error-prone, and therefore very expensive to make robust enough for real applications.

    The History Of Libero Multithreading

    In 1990, Leif Svalgaard wrote a tiny multitasking monitor for MS-DOS, to demonstrate that multitasking did not require megabytes of memory. This monitor was based on an event-passing kernel. It worked well and could multitask several DOS sessions simply and efficiently. This project was remarkable because it took a very short time to write (one long weekend) and because it required so little memory to run (several kilobytes). This monitor was based on Leif Svalgaard's earlier work in operating systems design, and defined the core principles of the SMT 2.x kernel.

    In 1993, Pieter Hintjens developed a complex multithreaded application using the Digital VMS chained multithreading method. Under severe time constraints, he was obliged to take a radical alternative to the normal approach. He used Libero to abstract the 'chain' of multithreaded logic. This reduced the development cost by an estimated 80%, and resulted in a very stable and efficient application. This experience showed that the Libero state-machine abstraction - already useful for writing normal procedural code - was also good in multithreaded applications.

    In 1993, Christian Rozet and Stephen Bidoul of ACSE built a version of Libero that generated a C++ 'asynchronous finite-state machine' to handle events coming from a GUI (MS-Windows). The resulting applications were in effect multithreaded applications, with the multithreading handled by Libero (actually the code that Libero generated).

    In 1995, Pieter Hintjens and Pascal Antonnaux built SMT version 1.0, and a set of demonstration programs. The smthttpd web server ran on UNIX and Windows 95, showing that portable multitasking was a realistic objective.

    SMT 2.0 is a fresh approach that combines the experience of these projects:

    Differences with Classic Multithreading

    The main differences between SMT and 'classic' multithreading are:

    We note some other points of interest:

    Why Use Multithreading?

    We will consider two specific problems. Firstly, construction of an industrial-scale Internet server. Secondly, construction of a multi-level finite-state machine application.

    There are many different ways to design an Internet server. The main problem is to handle multiple connections at once ('concurrency'). The classic way to get concurrency is to use the operating system multitasking functions. This is straight-forward enough. For instance, under UNIX, the server process uses the fork() system call to create a 'clone' of itself. At any moment there are multiple copies of the server process, each handling one connection. The operating system switches rapidly between these processes, so giving concurrency.

    The problems with this design become apparent when you try to use it for large-scale work. Firstly, it is not portable - the fork() system call does not work on all operating systems. Secondly, each fork() call duplicates the server program in memory. This duplication takes a certain time, as does the eventual removal of the server process. A protocol like HTTP creates a large number of short-term connections. Lastly, each additional instance of the server process consumes system resources so that a typical system cannot handle more than a few hundred connections.

    There are variations on this design that eliminate some of the problems. For instance, you can create a fixed number of instances of the server beforehand, then allow that number of connections. This eliminates the cost of creating and removing server processes, but does not raise the ceiling on the maximum number of connections.

    A more sophisticated approach is to handle multiple connections within a single process. This is relatively simple to arrange, using the BSD socket select() function. This lets a program wait for activity on a set of open sockets. The logic of such a program is: wait for activity on a socket; handle the activity; repeat. This approach works when the logic of 'handle the activity' is simple. In realistic applications, however, this logic becomes complex, and involves activity such as reading or writing to files, or manipulating several sockets at once.

    The SMT kernel uses the last approach, but provides a level of abstraction that makes the approach practical for large-scale problems. You can create one or several 'threads'. Each thread executes a copy of the finite state machine. The basic unit of logic in a thread is the code module.

    The number of threads is limited only by the memory available to the process. Creating or removing a thread is fast (so a new connection can be established faster than using a fork() call), and as far as the operating system is concerned, there is just one process (so the cost to the operating system is lower).

    Let's consider the design of an application that consists of several interworking state machines. This is the kind of design one finds in telecommunications and other specialised domains. The approach can be used in many areas. Typically, such a state machines processes an event queue; one state machine can send events to another.

    In this type of design we need to save the 'state' of each state machine in some way so that it can process events in a meaningful manner. The 'state' consists of the actual state, the last event, and context information that the state machine program needs to remember between events. We can define this 'state' as a thread: the requirements are very close to that of the Internet server described above. If we only want a single thread in any state machine, we can consider this a special case of the general case, which is a full multithreaded approach.

    Our conclusion from these two chains of argument is that a state-machine approach to multithreading is useful and valuable in real applications. Since Libero already provides a state-machine abstraction that converts a state-machine diagram into generated code, it is reasonable to use this mechanism to implement a generic and portable type of multithreading.

    The SMT 2.x kernel works with a specific Libero code generation schema, smtschm.c, to provide this generic multithreading.

    What You Should Know

    If you intend to write Internet servers, you should have a basic understanding of the concepts behind IP, TCP/IP, and UDP/IP. While the SMT kernel does a good job of packaging and abstracting the Internet programming model, it is no substitute for a solid understanding of the issues involved. We recommend that you be familiar with these aspects at least:

    The SFL socket functions provide the main abstraction layer; for instance you will not need to consider system-specific issues such as the WINSOCK interface.

    Before designing or writing an SMT application you should understand the Libero method of program design. The main components of an SMT application - the agents - are designed and written using Libero.

    Before writing SMT applications you should be familiar with the standard function library (SFL), since many SFL functions are used in a typical SMT application.

    The SMT Kernel Architecture

    Structure Of An SMT Application

    An SMT application generally consists of a number of agents, connected to the SMT kernel:

                         .------------.
                         : SMT kernel |
                         `============"
                                |
         - -+------------+------+-----+------------+- -
            |            |            |            |
        .-------.    .-------.    .-------.    .-------.
        : Agent |    : Agent |    : Agent |    : Agent |
        `======="    `======="    `======="    `======="
    
    Figure: Regular SMT application
    

    We may also use an event driver to make an 'embedded application'. This is necessary when external events cannot be collected by a normal SMT agent program such as the socket i/o agent. An example of such an application is one designed for MS Windows.

                         .--------------.
                         : Event Driver |
                         `=============="
                                |
                         .------------.
                         : SMT kernel |
                         `============"
                                |
         - -+------------+------+-----+------------+- -
            |            |            |            |
        .-------.    .-------.    .-------.    .-------.
        : Agent |    : Agent |    : Agent |    : Agent |
        `======="    `======="    `======="    `======="
    
    Figure: Embedded SMT application
    

    There are many other types of SMT application. The SMT kernel defines a number of objects, and operations on those objects. These objects are arranged in a hierarchy: this is structural, not morphological (they do not inherit attributes from each other):

             Agent
               :            :  Method
             Queue
            /          Thread     Event
    
    Figure: Main SMT Objects
    

    The SMT holds these objects in a set of linked lists. Each thread, for instance, contains the reference of its parent queue; a queue refers to its parent agent.

                          .------------.
                          : SMT kernel |
                          `============"
                                 |
           -+-------------+------+------+-------------+-
            |             |             |             |
                      .-------.
                      : Agent |
                      `======="
                          |
            +-------------+-------------+
            |             |             |
        .--------.    .--------.    .--------.
        : Queue  |    : Queue  |    : Queue  |
        `========"    `========"    `========"
            |             |             |
        .--------.    .--------.    .--------.
        : Thread |    : Thread |    : Thread |
        `========"    `========"    `========"
    
    Figure: Agent Program With 3 Threads
    

    Floating Event Queues

    In normal cases, events are sent to a queue that is held by an agent program. Sometimes this is a limitation: we can see cases where non-agent programs also need to receive events. Any program can send an event: this is just a call to the SMT kernel API.

    We therefore define the concept of a floating event queue; that is, an event queue that is managed by some program that is not constructed as an agent:

            Program
               :
             Queue
               :
             Event
    
    Figure: Floating Event Queue
    

    The designer of the application must also find a way to service the event queue, since the SMT kernel cannot schedule and execute such a program.

    Routers

    In most cases, one queue is served by one thread. Thus, when you send an event to a queue, you are effectively sending it to a specific thread. In some cases, it is better to share one queue among several threads. Imagine an agent that executes database requests coming from a set of clients. A thread takes an event, processes it, and returns a reply. Each event is independent, and may take some time to process. It makes sense to start several threads, so that several events can be processed in parallel. It would be wrong to allocate one queue per thread, however, for two reasons. Firstly, this implies that there are N queues which the clients must know about. Secondly, it implies that someone (client or agent) chooses a queue for each event. Consequently, events may be waiting in the queue for one thread while another thread is idle. By allocating just one queue, shared between all the agent threads, events are processed at full speed, in arrival order.

    An agent is either defined as a router, or not. When an agent is a router, it is legal to create multiple threads with the same name. All threads in a router agent share a single event queue.

    Internal and External Events

    SMT agents are built as event-driven Libero programs. We define two kinds of event: an internal event (supplied by the program itself) and an external event (taken from the event queue). If we consider a normal Libero program, all events are internal. The SMT kernel adds the concept of external events.

    When you design the program dialog, you do not distinguish internal and external events. Indeed, a name like "Ok" can be used for both types of event.

    The SMT kernel delivers an external event when the program did not supply an internal event itself. This occurs at a state transition, i.e. after executing the action modules for an event. When an external event is delivered, it is converted into an appropriate internal event.

    To put it another way: if none of the action modules put a value into the_next_event, then the SMT kernel will pull an event from the event queue, translate it into a suitable value, and put that into the_next_event instead.

    The methods define the translation from external event to internal event. External events that do not match declared methods are rejected. This is usually a programming error, so an error message is sent to the console.

    Event Management

    The SMT kernel provides an level of event management. Firstly, it will destroy undelivered events after a specified expiry period. Secondly, it will automatically send return events to acknowledge receipt or non-receipt of an event. This works as follows: when you send an event, you may specify reply events for delivery, rejection, and expiry.

    Priorities and Scheduling

    The SMT kernel has a simple priority and scheduling mechanism. Agents run with a certain priority level (a number from 1 to 255). When the SMT kernel delivers events to threads, it schedules the threads for execution, by placing them in a queue (the active list). Threads with higher priorities are placed at the start of the active list. No account is taken of the time spent in the active list, so it is quite possible for a set of high-priority threads to take-up all execution time. This normally does not happen, since such threads will eventually wait for events from somewhere else, so become passive.

    Events have a similar priority level, defined by the accepting program, in the event method. When the SMT kernel delivers an event, it chooses the event with the highest priority. One example of a high-priority event is shutdown, which is sent to all threads when the SMT kernel receives a 'kill' signal (i.e. when someone decides to stop the running program).

    You can change agent and event priorities on the fly, although this may get a little complex to manage. You can also process the events in an event queue directly, without waiting for the SMT kernel to deliver each one.

    In general we recommend that you leave agents and events with unspecified priority - i.e. the normal priority level - unless there are good reasons for doing otherwise.

    Native Programs Vs. Foreign Programs

    A native program is designed as a finite-state machine (FSM) and built using Libero and the smtschm.c schema. A native program can be multithreaded. It is tightly integrated with the SMT kernel: the kernel supplies events to the program, schedules and executes threads, etc.

    A native program is also called a agent. The two terms are equivalent.

    A foreign program is not based on the smtschm.c schema. It is always single threaded, and while it may call SMT kernel functions, it is not integrated with the kernel. Typically a foreign program calls the SMT kernel to send and receive events and manage floating event queues.

    The SMT Kernel Meta-State-Machine

    We can consider the application (an ensemble of agents, queues, threads, and events) as a meta-state-machine (i.e. a state machine that defines a set of state machines). The application has three states:

    We do not usually define this as a Libero dialog, though it is possible to do so. What we must do is to write a foreign program, a stub, that implements the application meta-state- machine. This is the stub for an Internet ECHO daemon, showing the three states:

    #include "sfl.h"                   /*  SFL library header file  */
    #include "smtlib.h"                /*  SMT kernel functions     */
    
    int main (int argc, char *argv [])
    {
        /*  Application is latent - initialise it                   */
        smt init ();                   /*  Initialise SMT kernel    */
        smtecho init (NULL);           /*  Initialise ECHO agent    */
    
        /*  Application is active - execute it                      */
        smt exec full ();              /*  Run until completed      */
    
        /*  Application is halted - terminate it                    */
        smt term ();                   /*  Shut-down SMT kernel     */
        return (0);
    }
    

    This main code is the glue that joins the various pieces of the application.

    Using The SMT Kernel API

    The SMT kernel API is aimed mainly at native programs. However, you can also use the event-passing facilities in foreign programs. This can be necessary when interfacing SMT applications to other event-based systems.

    SMT Kernel Functions

    This is the complete set of functions supported by the SMT kernel:

    smt init()
    Initialise the SMT kernel.
    smt term()
    Shut-down the SMT kernel.
    smt exec full()
    Execute the SMT application until halted.
    smt exec step()
    Execute just next scheduled thread.
    smt active()
    Check if application has halted.
    smt set console()
    Specify an agent to act as console.
    smt set timer()
    Specify an agent to act as timer.
    smt atexit()
    Define a termination function.
    smt shutdown()
    Halt the application prematurely.
    agent declare()
    Define a new agent.
    agent lookup()
    Check if a specific agent is defined.
    agent destroy()
    Remove an agent from the SMT kernel tables.
    method declare()
    Define a method for an agent.
    method lookup()
    Check if a specific method is defined.
    method destroy()
    Remove a method from the SMT kernel tables.
    queue create()
    Define an event queue for an agent, or a floating queue.
    queue lookup()
    Check if an event specific queue is defined.
    queue destroy()
    Remove an event queue from the SMT kernel tables.
    queue flush()
    Expire out-of-date events in the event queue.
    event send()
    Send an event to some event queue.
    event accept()
    Accept the next event from an event queue.
    event reject()
    Reject the next event from an event queue.
    event expire()
    Expire the next event from an event queue.
    event discard()
    Discard the next event from an event queue.
    event iterate()
    Find the next event in an event queue.
    event destroy()
    Destroy a specific event.
    event wait()
    Wait explicitly for an external event.
    thread create()
    Define a thread for an agent, maybe create a queue.
    thread lookup()
    Check if a specific thread is defined.
    thread destroy()
    Remove a thread from the SMT kernel tables.
    semaph create()
    Create a new semaphore.
    semaph lookup()
    Check if a specific semaphore is defined.
    semaph destroy()
    Remove a semaphore from the SMT kernel tables.
    semaph wait()
    When the semaphore value is > 0, decrement it.
    semaph signal()
    Add 1 to the semaphore value.
    lazy creat()
    Create a file, without blocking.
    lazy open()
    Open a file, without blocking.
    lazy read()
    Read from a file, without blocking.
    lazy write()
    Write to a file, without blocking.
    lazy close()
    Close a file, without blocking.
    senderr()
    Send the current strerror (errno) to some queue.
    sendfmt()
    Format a text using printf conventions and send to some queue.
    raise exception()
    Raise an exception (for dialog programs only).
    recycle module()
    Repeat execution of the current dialog module.

    The SMT kernel API works with a number of objects - agents, threads, queues - which are defined as C structures. The fields in these structures are the properties of the object. The SMT objects contain private fields, which you should never change or refer to, and public fields, which you are free to change and use. We do not discuss the private fields, except to note that these contain information that is internal to the SMT kernel, or reflect particular implementations that may change.

    These are the SMT objects and their public fields:

    AGENT                      /*  Agent descriptor                 */
        AGENT  *next, *prev    /*    Doubly-linked list             */
        NODE    methods        /*    Methods accepted by agent      */
        NODE    queues         /*    Queues defined for agent       */
        char   *name           /*    Agent's name                   */
        Bool    router         /*    Default = FALSE                */
        int     priority       /*    Default = SMT_PRIORITY_NORMAL  */
        long    max_threads    /*    Default = 0 (no limit)         */
        long    cur_threads    /*    Current number of threads      */
        long    top_threads    /*    Max. number threads we had     */
        long    thread_tally   /*    How many threads created       */
        long    switch_tally   /*    How many context switches      */
    

    An agent defines one program written using the Libero SMT schema. When the generated code initialises, it automatically creates an agent object. You may change the router, priority, and max_threads fields, but not the other fields.

    METHOD                     /*  Method descriptor                */
        METHOD *next, *prev    /*    Doubly-linked list             */
        AGENT  *agent          /*    Parent agent descriptor        */
        char   *name           /*    Name of method                 */
        int     priority       /*    Default = SMT_PRIORITY_NORMAL  */
        int     event_number   /*    Internal event number          */
    

    The start-up code for an agent (in Initialise-The-Program) creates a method for each external event it wants to handle. You may change the priority and event_number fields, but not the other fields.

    THREAD                     /*  Thread descriptor                */
        THREAD  *next, *prev   /*    Doubly-linked list             */
        QUEUE   *queue         /*    Parent queue descriptor        */
        long     thread_id     /*    Thread identifier number       */
        char    *name          /*    Name of thread                 */
        Bool     animate       /*    Animate this thread            */
        void    *tcb           /*    Thread context block (TCB)     */
        EVENT   *event         /*    Last-received event            */
    

    Any part of the application can create a thread in an agent, if it knows the name of the agent. You may change the animate field, but not the other fields.

    QUEUE                      /*  Event queue descriptor           */
        QUEUE  *next, *prev    /*    Doubly-linked list             */
        AGENT  *agent          /*    Parent agent descriptor        */
        NODE    events         /*    Events in queue                */
        NODE    threads        /*    Threads for queue              */
        QID     qid            /*    Queue ID descriptor            */
        int     max_events     /*    Maximum allowed events         */
        int     cur_events     /*    Current number of events       */
    

    The SMT kernel automatically creates event queues as needed. Generally it will create one event queue per thread. When an agent is defined as a router, however, it only creates an event queue for the first thread. You can also create floating event queues. You may change the max_events fields, but not the other fields. Note that each queue has a unique QID; this is the identifier for the queue, and the information you need to send an event to a queue.

    EVENT                      /*  Event in queue                   */
        EVENT  *next, *prev    /*    Doubly-linked list             */
        QUEUE  *queue          /*    Parent queue descriptor        */
        QID     sender         /*    Replies come back here         */
        char   *name           /*    Name of event                  */
        size_t  body_size      /*    Size of event body in bytes    */
        char   *body           /*    Event body                     */
        char   *accept_event   /*    Reply if we accept event       */
        char   *reject_event   /*    Reply if we reject event       */
        char   *expire_event   /*    Reply if we expire event       */
        time_t  timeout        /*    Expires at this time (or 0)    */
    

    Any part of the application can send an event to an event queue using the event_send() function. The queue may be served by a thread, or not (if it is a floating event queue). You should not change any of the fields in the event object: the sender, body, and body_size fields are generally interesting.

    SEMAPH                     /*  Semaphore definition             */
        SEMAPH  *next, *prev;  /*    Doubly-linked list             */
        char    *name;         /*    Name of semaphore              */
    

    Semaphores are for synchronising agents, for instance for exclusive access to some resource. We implement general semaphores, i.e. they can take any value of 0 or greater (the value is defined as an integer).

    Initialisation and Termination

    An SMT application must explicitly initialise and shut-down the SMT kernel. This lets the kernel create global objects (such as the symbol table) and then destroy them when finished.

    The smt init() function initialises the SMT kernel. The smt term() function shuts-down the SMT kernel. These are generally the first and last SMT functions that the application calls. Usually, we'll put these calls in the stub (main) program.

    Any part of the application can call smt atexit() to register a shut-down function; these shut-down functions are called by smt term(), in the order that you declare them.

    Creating A Thread

    Any program can create a thread in any agent in the application using the thread_create() function. To create a thread you must know the name of the agent and the name of the thread you want to create.

    A thread's name is local within the agent, and lets an outside program look-up the event queue for that thread. There are various ways to name threads. In single-threaded agents, it is useful to leave the thread name empty (specified as an empty string - ""). In multithreaded agents, the thread name can correspond to some resource name. For instance, in the echo agent, the thread uses the connection socket number as its name. In a router agent, several threads can have the same name; since the threads in such an agent share the same event queue, this allows a program to unambigouously find the event queue from the thread name.

    This is how a single-threaded agent (e.g. the operator console) creates a thread:

        /*  Create initial, unnamed thread                          */
        thread create (AGENT_NAME, "");
    

    This is how the echo agent creates a thread to handle a new connection (tcb-> handle contains the handle of the echo master socket):

        SOCKET
            slave_socket;           /*  Connected socket            */
        static char
            thread_name [12];       /*  Socket number as string     */
    
        slave_socket = accept_socket (tcb-> handle);
        if (slave_socket != INVALID_SOCKET)
          {
            sprintf (thread_name, "%d", slave_socket);
            thread create (AGENT_NAME, thread_name);
          }
    

    Sending and Receiving Events

    These are the API functions that you can use to send an event:

    event send()
    Send an event to some event queue.
    senderr()
    Send the current strerror (errno) to some queue.
    sendfmt()
    Format a text using printf conventions and send to some queue.

    In all cases, the target queue is specified as a QID. The QID is a location-independent queue identifier that the SMT kernel creates for each queue. We use a QID instead of the address of the queue object so that events can be sent between processes running in different address spaces. (Although this is not yet implemented.)

    When you send an event, you specify an event name. An agent program must have declared a method for each event it can accept. There is no such restriction for other programs that manage event queues themselves.

    An event has a body, which is a block of text or binary data that is copied to the receiving event queue. You should always ensure that event bodies are portable, since the receiving event queue could in principle be on a different system. Furthermore, an event body cannot include the address of an object or variable: events can cross address spaces (i.e. be sent to other processes) so that such addresses are not meaningful.

    Sending An Initial Event To A Thread

    You can send an event to a thread as soon as it has been created. This can be useful if you need to pass arguments to a child thread. The event will be delivered after the initialise_the_thread module, if you do not specify a value for the_next_event. For an example, see the echo agent SMTECHO.

    Event Lifespan and Acknowledgment

    The event send() call lets a program specify these optional arguments:

    When a program wants to inspect the events in a queue, it uses event iterate(), which walks through the queue, event by event. To take an event off the queue, a program calls event accept(). This automatically sends an accept event, if specified. If queue is empty it simply returns with an 'not found' feedback. Some programs implement event priorities by combining event iterate() with event accept(). A program may manipulate several event queues.

    To remove an event without using it, a program calls event destroy(). The event destroy() call automatically sends a reject event, if specified.

    The SMT kernel handles event expiry automatically for native programs. Programs that handle floating event queues must expire old events explicitly by calling queue flush() before they start to process waiting events.

    Using Priorities

    The SMT kernel provides support for event priorities and for thread priorities. These work as follows:

    In practice we use priorities rarely, and for specific cases only. In the current version of the SMT kernel we use a high priority for shutdown events, and a low priority for the socket agent. All other events and threads have normal (equal) priority.

    Writing A Native SMT Program

    A native program is a agent. To build an agent you:

    When you design an agent you must decide whether the it is single-threaded or multithreaded. What's the difference?

    By convention, a single-threaded agent creates an unnamed thread when it initialises. A multithreaded agent, by contrast, typically create a new thread for each new connection, and uses a different name for each thread.

    An example of a single-threaded agent is the operator console. This is a program that accepts error messages or warnings from other parts of the application, then does something useful with them. (The current implementation writes them to stderr.) The operator console has no need for multiple threads.

    An example of a multithreaded agent is the logging agent. This is a program that manages log files on behalf of other application programs. It does this at a low priority, and without blocking, so that log file data is written without disturbing ongoing work. The logging agent can write to several log files in parallel: it does this by having one thread for each log file.

    To specify that an agent is single-threaded, define SINGLE_THREADED as TRUE near the start of the program. For instance, this code comes from the SMTOPER agent:

    /*- Definitions ------------------------------------------------*/
    
    #define AGENT_NAME      SMT_OPERATOR  /*  Our public name       */
    #define SINGLE_THREADED TRUE          /*  Single-threaded agent */
    

    Initialising An agent

    An agent program must be 'initialised' before it can do any useful work. For instance, to initialise the logging agent, the application must call the function smtlog init(). Generally, agents are initialised by the stub program, or by other agents. We generally recommend that an agent always try to initialise every agent it requires. It is safe to call initialisation function for an agent several times; only the first call has any effect.

    The initialisation function is the only public function for a agent. Once an agent is initialised, it communicates with other programs only via events.

    An agent program is based on a Libero dialog, and is 'driven' by a chunk of code generated by Libero. This code (defined as an #include file) handles the initialisation of the agent. The code looks something like this (we explain each part):

    if (agent lookup (AGENT_NAME))
        return (0);                 /*  Agent already declared     */
    

    The agent lookup() function returns NULL if an agent object with the specified name already exists. Otherwise it returns a pointer to the agent object. Here we check that the agent has not already been declared. The generated code assumes that AGENT_NAME has been defined to hold the name of the agent. AGENT_NAME can be a variable or a pre-processor macro (the generated skeleton program defines it as a macro).

    if ((agent = agent declare (AGENT_NAME)) == NULL)
        return (-1);                /*  Could not declare agent    */
    

    The agent declare() function returns a pointer to the newly-created agent object. If there was an error (e.g. not enough memory), it returns NULL.

    #if (defined (SINGLE_THREADED))
    agent-> tcb_size    = 0;    /*  No context block            */
    agent-> max_threads = 1;    /*    and max. 1 thread         */
    #else
    agent-> tcb_size    = sizeof (TCB);
    #endif
    

    Once the agent has been created, the generated code sets the thread context block (TCB) size depending on whether the agent is single threaded or multithreaded. A single-threaded agent does not need TCBs, so the size is zero. The code assumes that SINGLE_THREADED has been defined as a macro if required. The generated code then sets a variety of fields in the agent block. This allows the SMT kernel to 'drive' the agent program correctly.

    This is how a typical multithreaded agent program initialises (this code is taken from smtlog.c):

    #define AGENT_NAME  SMT_LOGGING    /*  Our public name         */
    

    The SMT_LOGGING symbol is defined in the standard SMT header file, smtlib.h, since this agent is part of the standard package. For your own agents you would define AGENT_NAME as a string literal. Note that agent names must be unique within the application.

    int smtlog_init (void)
    {
        AGENT   *agent;             /*  Handle for our agent        */
        THREAD  *thread;            /*  Handle to console thread    */
    #   include "smtlog.i"          /*  Include dialog interpreter  */
    

    The initialisation code in smtlog.i assumes that a variable 'agent' is defined. Then, you can refer to the agent in following code using this 'handle'. The first thing we do is to declare each method:

    /*                           Method     Event value  Priority  */
        method declare (agent, "SHUTDOWN", shutdown_event,
                                                 SMT_PRIORITY_MAX);
        method declare (agent, "OPEN",     open_event,         0);
        method declare (agent, "PUT",      put_event,          0);
        method declare (agent, "CLOSE",    close_event,        0);
    

    Methods names are not case-sensitive, but by convention we specify them in uppercase. Every agent must support the SHUTDOWN method; this is sent to each agent when the SMT kernel terminates (for instance when interrupted). SHUTDOWN gets a high priority, so that an agent will handle shutdown events before any other waiting events. The other events get a normal priority (0 means 'default').

    You can define several methods for the same event. The SMT kernel uses the set of methods to translate an incoming external event into an internal dialog event.

        /*  Ensure that operator console is running, else start it  */
        smtoper init ();
        if ((thread = thread lookup (SMT_OPERATOR, "")) != NULL)
            console = thread-> queue-> qid;
        else
            return (-1);
    

    This agent sends error messages to the operator console agent, which is generally a good idea. It initialises the agent (with no effect if the agent is already initialised) and then gets the console queue id, so it can send events to the operator console. Note how we do a thread lookup() with an empty thread name. The operator console is single threaded, and that single thread has no name.

        /*  Signal okay to caller that we initialised okay          */
        return (0);
    }
    

    Finally, if everything went as expected, we return 0 to signal that to the calling program. This is a convention, although you can write the initialisation function any way you like, accepting any arguments and returning any value that is appropriate.

    The Thread Context Block

    Threads can share data: any global data in the agent program is de-facto shared by all threads. Since threads also need 'private' data, each thread owns a block of memory called the Thread Context Block, or TCB. The SMT kernel allocates such a block when a thread is created.

    The TCB is a structure that contains arbitrary fields. You define this structure at the start of your program. All code modules in the program receive a pointer to this structure: they use the pointer to reference private data. For instance, this is how the smtlog.c agent declares its TCB:

    typedef struct                  /*  Thread context block:       */
    {
        int handle;                 /*    Handle for i/o            */
    } TCB;
    
    This is how the smtlog.c agent opens a file:
    /*******************   OPEN THREAD LOGFILE   ********************/
    
    MODULE open_thread_logfile (THREAD *thread)
    {
        char
            *logfile_name;
    
        tcb = thread-> tcb;         /*  Point to thread's context   */
    
        /*  Event body or thread name supplies name for log file    */
        logfile_name = (strused (thread-> event-> body)?
                        thread-> event-> body:
                        thread-> name);
        tcb-> handle = lazy creat (logfile_name, S_IREAD | S_IWRITE);
        if (io_completed)
          {
            if (tcb-> handle < 0)   /*  If open failed, send error  */
              {                     /*    to console, and terminate */
                sendfmt (&console, "ERROR",
                         "Could not open %s for output",
                          logfile_name);
                senderr (&console);
                raise exception (exception_event);
              }
          }
    }
    

    Choosing Event Names

    We generally use the same name for the method as for the event. E.g. CLOSE and close_event. This is not obligatory, and in some cases not appropriate, but it does make the program easier to understand when dialog event names correspond to methods.

    Mechanics Of Event Delivery

    The agent kernel delivers events to threads when required. This happens at a precise moment: when the thread moves to a new dialog state -- after executing the action module list -- and no internal event was provided.

    When a thread moves into a state, the set of possible events is those events defined in that state, plus the events defined in the Defaults state, if any.

    The SMT kernel takes the following event (or the event with the highest priority) and tries to match it to a method name. If the event does not match a method, the event is rejected. Otherwise it is accepted and translated into an internal event number. If the internal event is illegal at that moment in the dialog, this causes a fatal dialog error (the thread rejects the event).

    Waiting For An External Event

    Normally the SMT kernel delivers an external event when the dialog moves to a new state, and no event was specified. In some cases this can make a dialog rather large, since you need to break each step up into states. The event wait() function causes the dialog to halt until an event can be taken from the queue. When several threads are executable, this function also switches execution to the next thread.

    The event wait() call sets the variable "the_external_event". This should be the last statement in a dialog module. When used in the last module in a list, it has strictly no effect.

    Sending Structured Event Data

    Events can optionally have a body to carry additional information. When you send textual data - for instance a string - the event body can be transferred between programs without any type of conversion. (We ignore problems of character-set conversion at present.)

    When you need to send several items in one event body, we speak of sending structured data. Structured data consists of a mixture of data items of these types:

    An example of events with structured data are those accepted by the socket i/o agent SMTSOCK.

    In C, we can group the data items in a structure, hence the term. We cannot, however, simply copy the structure into the message and send that. We cannot copy the address of the structure. Both these methods will work today, but an event may (in the future) be sent to an agent in a different process, perhaps running on a different machine.

    Our solution is to take each data field in turn, and pack the structure into a machine-independent stream. We transmit this stream, then do the reverse unpacking in the target program.

    To do the conversion we use the SFL functions exdr_read() and exdr_write().

    This is more work than just sending the complete structure, but is the only way to ensure that data can safely be sent between two programs that may be running on separate systems.

    Sending Events Within An Agent

    Within one agent, you do not need to use the EXDR functions. It is quite acceptable to pass data in a structure. To do this,

    Ignoring External Events

    In some cases, you may want to ignore reply events sent by an agent. This can be useful to simplify a dialog. This is how we declare a method to ignore some specific event:

        method declare (agent, "SOME_EVENT", SMT_NULL_EVENT, 0);
    

    Non-Blocking File Access

    The SMT kernel provides a minimum file access layer that is safe to use in multithreaded programs. To understand what this means, first understand what is 'unsafe'.

    On some systems, like UNIX and Digital VMS, file access may need resources that are not always available - like memory for buffers. If you ask to read some data from a file, and there is a problem, the operating system may loop a few times - waiting and then trying again - before finally returning to the calling program. In the meantime your program and all threads are blocked.

    The general solution is to request non-blocking file access. Then, in the case of a resource problem, the operating will not loop, but will return at once with an error code that means 'try again'. The SMT kernel integrates this solution with its thread management, so that a thread waiting for file access will loop slowly, allowing other threads to continue to run.

    To make this work, you should not call the open(), creat(), read(), or write() functions directly in your program. Instead, call the SMT kernel functions lazy open(), lazy creat(), lazy read(), lazy write(), and lazy close(). Furthermore, construct your code like this:

    rc = lazy write (tcb-> handle, formatted, fmtsize);
    if (io_completed && rc < 0)     /*  If write failed send error  */
      {                             /*    to console and terminate  */
        sendfmt (&console, "ERROR",
                 "Could not write to %s", thread-> name);
        senderr (&console);
        raise exception (exception_event);
      }
    

    If io_completed is FALSE, then the code module should do no further work. In this case the SMT kernel automatically re- executes the entire code module.

    This is simple and sufficient for sequential file access. If you need heavy database access, where one SELECT statement may take a long time to complete, you'll find that your program responds slowly. A better architecture in such cases is to handle database requests in a separate process, which talks to your application program using sockets. The requests will take the same time to complete, but in the meantime other threads - e.g. handling new connections - can run normally.

    While we generally recommend you use the non-blocking i/o functions, there are instances where this is not really necessary. Most obviously, when an application is initialising (e.g. reading configuration files) or terminating (dumping data to a log file), there is no need to avoid blocking i/o. In such cases, you can access sequential files directly.

    Real-time Programming

    When you call a function like lazy read() and it detects a 'busy' condition, it sets io_completed to FALSE, and automatically re-executes the current code module. You may want to manage this yourself, however. It can also be useful to have a similar looping when you access a socket and receive the EAGAIN error code.

    The recycle module() function lets you control this looping explicitly. For example, we can decide to abort a file access after more than RETRY_MAX retries:

    typedef struct                  /*  Thread context block:       */
    {
        int handle;                 /*    Handle for i/o            */
        int retries;                /*    Number of retries so far  */
    } TCB;
    
    Let's assume that 'retries' has been set to zero, either during thread initialisation (okay), or by a specific dialog module (better). We can then try to open a file like this, calling recycle module() with a FALSE argument to prevent looping when we have retried too often:
    /********************   OPEN THREAD LOGFILE   *******************/
    
    MODULE open_thread_logfile (THREAD *thread)
    {
        tcb = thread-> tcb;         /*  Point to thread's context   */
    
        /*  Our thread's name is the name for the log file          */
        tcb-> handle = lazy creat
                          (thread-> name, S_IREAD | S_IWRITE);
        if (io_completed)
          {
            /*  If open failed, send error to console, and end      */
            if (tcb-> handle < 0)
              {
                sendfmt (&console, "ERROR",
                         "Could not open %s for output", thread-> name);
                senderr (&console);
                raise exception (exception_event);
              }
          }
        else
          {
            if (++tcb-> retries == RETRY_MAX)
              {
                sendfmt (&console, "ERROR",
                         "Could not open %s for output", thread-> name);
                sendfmt (&console, "ERROR", "Too many retries.")
                raise exception (exception_event);
                recycle module (FALSE);
              }
          }
    }
    

    Using Semaphores

    A semaphore is an object that you can use to synchronise threads, lock resources, etc. Semaphores are widely- used in multithreaded and parallel computing, and the SMT kernel implements semaphores in a conventional manner. Semaphores have these characteristics:

    Replacing The Standard Agents

    In realistic projects you will want to replace the operator console agent and perhaps the logging agent with your own, customised versions. One way to do this is to modify the code of the programs we supply. However, this just causes maintenance problems. A better way is to use the existing code as a basis for new agents that use the same agent name (but written as a different source file). Initialise the replacement agent from your main() function. Then, the standard agent will never be initialised. All events normally sent to the standard agent will be sent to your agent instead. Do not try to change the form or meaning of events sent to the standard agents.

    Using The SMT API In A Foreign Program

    In an SMT application there will always be some foreign programs. Some examples are:

    It may make sense in some applications to write all programs as foreign. For instance, you could use the SMT kernel simply to send events between programs without using the multithreading facilities.

    Writing A Stub Program

    When you build a new SMT application you must also write a STUB program (the main() function). The stub program usually does this:

    When you compile and link the stub, you create the executable program for the application.

    Managing A Floating Event Queue

    An interface between a native program (or a thread) and a foreign program consists of at least an event queue in either direction. The SMT kernel automatically handles the event queue for the native program. You must handle the other event queue yourself:

           Foreign                               Native
       .--------------.                     .--------------.
       : event_send() :-----------> ()()()():              |
       :              |()()()() <-----:-----: event_send() |
       `=============="    :          :     `=============="
                           :        normal event queue
                           `----- floating event queue
    
    Figure: Managing a floating event queue
    

    The program that sends an event does not know whether the event queue is floating or not, although it can find this out if it has to, by examining the queue object. The difference between a floating event queue and a normal event queue is that you must get events off a floating event queue yourself. You can use the API function calls:

    event accept()
    Accept the next event from an event queue.
    event reject()
    Reject the next event from an event queue.
    event expire()
    Expire the next event from an event queue.
    event discard()
    Discard the next event from an event queue.
    event iterate()
    Find the next event in an event queue.
    event destroy()
    Destroy a specific event.

    You can use methods if you need to. This can be a convenient way of translating event names into internal numbers of some sort. For instance, if you design the foreign program using the standard Libero C schema, you can use methods to translate event names into internal numbers, much as the SMT kernel does for a native program. This is (more or less) the SMT kernel code that does this translation:

        EVENT   *event;             /*  Event information block     */
        EVENT   *deliver_event;     /*  Event to deliver            */
        METHOD  *method;            /*  Method information block    */
        THREAD  *active;            /*  Thread in active list       */
        int     top_priority;       /*  Highest event priority      */
        int     event_number;       /*  Number for event to deliver */
    
        /*  Get event to deliver - find event with highest priority */
        top_priority  = -1;
        deliver_event = NULL;
        event = queue-> events.next;
        while ((NODE *) event != &queue-> events)
          {
            /*  Lookup method; if method not declared, reject event */
            method = method lookup (agent, event-> name);
            if (method == NULL)
              {
                /*  Reject this event, but keep place in queue...   */
                sendfmt (&console, "ERROR",
                         "Event %s not declared by %s",
                          event-> name, agent-> name);
                event = event-> next;
                event reject (queue, event-> prev);
                continue;           /*  Process this new event      */
              }
            if (method-> priority > top_priority)
              {
                deliver_event = event;
                event_number  = method-> event_number;
                top_priority  = method-> priority;
              }
            event = event-> next;
          }
        if (deliver_event)
          {
            /*  Deliver event, move thread to active queue          */
            thread-> event          = event accept (queue,
                                      deliver_event);
            thread-> the_next_event = event_number;
            thread-> active         = TRUE;
          }
    

    Executing The Application

    The main() function in an executable is a foreign program. Since a foreign program is external to the multithreading process of the SMT kernel, control must pass back and forwards between the SMT kernel and the foreign program. Typically this is a loop like this:

    To do the second step, the foreign program calls smt exec full(). This function runs all the agent threads until there are no more events left to deliver, or active threads.

    Sometimes it is better to call smt exec step() repeatedly. This function delivers events, but only executes the first active thread. This function provides a finer-grain control. For instance, if the application loops because two threads send each other events in a never-ending loop (usually a programming error), the smt exec full() call will never return, while the smt exec step() call will return each time that the kernel switches between the threads.

    Standard SMT Agents

    The SMT architecture lets one build an application out of existing and new agents. We provide a number of standard agents. All these agents correctly support the shutdown method, which the SMT kernel broadcasts when it needs to end the application.

    These are the standard SMT agents:

    These agents are in development, or planned:

     

    The Logging Agent - SMTLOG

    Creates log files, and writes data to the log files. Can handle multiple log files in parallel; each open log file is managed by one thread. Sends errors to the operator console agent.

    To use, call smtlog init(). This does not create any threads. Create an unnamed thread using the thread_create() function. Send an OPEN or APPEND event to create the log file and PUT events to write to the log file. Finally, send a CLOSE event to close the log file and terminate the log file thread. SMTLOG does not reply - errors are sent to the console, not the requesting program. It supports these methods:

    Example of initialising SMTLOG:

    /*  Static data                                                 */
    static QID
        logq;                       /*  Logging agent event queue   */
    
        /*  In agent initialisation code                            */
        THREAD  *thread;            /*  Handle to various threads   */
        /*  Ensure that logging agent is running, else start it     */
        smtlog init ();
        if ((thread = thread create (SMT_LOGGING, "")) != NULL)
            logq = thread-> queue-> qid;   /*  Get logging queue id */
        else
            return (-1);
    

    The CYCLE Method

    If the specified log file already exists, renames it to a file whose name consists of the first letter of the log file name, followed by the date in 7 positions (YYYYDDD). If that file name already exists, generates a unique filename. Takes the event body to supply the log file name; if the event body is empty, uses the thread name instead. If the log file name is empty ("") or the 4-letter string "NULL", nothing is done.

    The OPEN Method

    Creates a new, empty log file. Takes the event body to supply the log file name; if the event body is empty, uses the thread name instead. If the log file name is "" or "NULL", no file is created, and SMTLOG discards all output.

    The APPEND Method

    Opens an existing log file for additional output. If the log file does not already exist, acts like OPEN. Takes the event body to supply the log file name; if the event body is empty, uses the thread name instead.

    The PUT Method

    Takes the event body as a string, prefixes the date and time, and writes it to the log file.

    The PLAIN Method

    Disables time-stamping of the logged data. Use this when you do not want the default SMTLOG time-stamping.

    The STAMP Method

    Enables time-stamping of the logged data.

    The CLOSE Method

    Closes the log file and destroys the thread used to manage it. You normally send this event when you end your program.

    Example Of Use

    The SMTECHO agent shows an example of using the Logging agent.

    SMTLOG Implementation

    The hypertext view of SMTLOG's dialog and source code may help to understand how SMTLOG works.  

    The Operator Console Agent - SMTOPER

    Accepts error messages, warnings, and information messages, and does something useful (but unspecified) with them. The current implementation writes all received messages to the standard error device.

    To use SMTOPER, call smtoper init(). This creates a single unnamed thread automatically the first time it is called, and has no effect thereafter. You can then send messages to this thread. SMTOPER does not reply. It supports these methods:

    Example of initialising SMTOPER:

    /*  Static data                                                 */
    static QID
        console;                    /*  Operator console queue      */
    
        /*  In agent initialisation code                            */
        THREAD  *thread;            /*  Handle to console thread    */
        /*  Ensure that operator console is running, else start it  */
        smtoper_init ();
        if ((thread = thread lookup (SMT_OPERATOR, "")) != NULL)
            console = thread-> queue-> qid;
        else
            return (-1);
    

    The ERROR Method

    The event body contains a string. This string is handled as a serious error message. For example:

        sendfmt (&console, "ERROR", "Could not open %s", filename);
        senderr (&console);
        raise exception (exception_event);
    

    The WARNING Method

    The event body contains a string. This string is handled as a non-fatal warning message. For example:

        sendfmt (&console, "WARNING", "Exceeded connection quota");
    

    The INFO Method

    The event body contains a string. This string is handled as a information message. For example:

        sendfmt (&console, "INFO", "Connection from %s", system);
    

    The LOG Method

    The event body contains a string that specifies the name of a SMTLOG thread. All operator output is sent to this thread.

    SMTOPER Implementation

    The hypertext view of SMTOPER's dialog and source code may help to understand how SMTOPER works.  

    The Timer Agent - SMTTIME

    Generates timing events. You can request one timing event after a specific delay, or repeated timing events at regular intervals. The timer is accurate to 1/100th of a second. You specify delays as a number of days and a number of centiseconds.

    To use SMTTIME, call smttime init(). This creates a single unnamed thread automatically the first time it is called, and has no effect thereafter. You can then send messages to this thread. It supports these methods:

    Example of initialising SMTTIME:

    /*  Static data                                                 */
    static QID
        timeq;                      /*  Timer agent event queue     */
    
        /*  In agent initialisation code                            */
        THREAD  *thread;            /*  Handle to various threads   */
    
        method declare (agent, "TIME_ALARM", alarm_event, 0);
        method declare (agent, "TIME_ERROR", error_event, 0);
    
        /*  Ensure that timer agent is running, else start it       */
        smttime init ();
        if ((thread = thread lookup (SMT_TIMER, "")) != NULL)
            timeq = thread-> queue-> qid;
        else
            return (-1);
    

    The ALARM Method

    Sends a single alarm event after some specified delay. Build the event body using exdr_write() and the message definition SMT_TIME_ALARM. The event body consists of these fields (see exdr_write() for the field types):

    SMTTIME replies to an ALARM event with one of these events:

    The ALARM method implicitly does a FLUSH before proceeding.

    The WAKEUP Method

    Sends a single alarm event at some specified day and time. Build the event body using exdr_write() and the message definition SMT_TIME_ALARM. The event body consists of these fields (see exdr_write() for the field types):

    SMTTIME replies to a WAKEUP event with one of these events:

    The WAKEUP method implicitly does a FLUSH before proceeding.

    The CLOCK Method

    Sends a repeated alarm event after some specified delay, for ever, or a specific number of times. Build the event body using exdr_write() and the message definition SMT_TIME_CLOCK. The event body consists of these fields (see exdr_write() for the field types):

    SMTTIME replies to a CLOCK event with one of these events:

    The CLOCK method implicitly does a FLUSH before proceeding.

    The FLUSH Method

    Removes any requests sent by a particular client thread. Use this to cancel a CLOCK method, or an unexpired ALARM or WAKEUP request. You do not need to provide an event body, but you must specify your thread's QID correctly when you use event_send(). SMTTIME does not reply to a FLUSH method.

    Example Of Use

    The SMTTST1 test agent shows an example of using the timer agent.

    SMTTIME Implementation

    The hypertext view of SMTTIME's dialog and source code may help to understand how SMTTIME works.  

    The Time Slot Agent - SMTSLOT

    Manages 'time slots', a mechanism to allow long-running programs to 'switch on' and 'switch off' at specific times during the day. The time slot agent simulates a wall timer, i.e. a timer that switches a device like a lamp or electric heater on and off during the day.

    To use SMTSLOT, call smtslot init(). This does not create any threads. Create a named thread, then send SPECIFY events to define the various time slots for your application. Then send an ON or OFF event to initialise the timer. The time slot agent then sends a SWITCH_ON event when the timer move to an 'ON' state, and a SWITCH_OFF event when the timer moves to an 'OFF' state. Errors are sent to the console, not the requesting program. SMTSLOT supports these methods:

    Example of initialising SMTSLOT:

    /*  Static data                                                 */
    static QID
        slotq;                       /*  Time slot event queue      */
    
        /*  In agent initialisation code                            */
        THREAD  *thread;            /*  Handle to various threads   */
        /*  Ensure that time slot agent is running, else start it   */
        smtslot init ();
        if ((thread = thread create (SMT_SLOT, "myprog")) != NULL)
            slotq = thread-> queue-> qid;
        else
            return (-1);
    

    The SPECIFY Method

    Defines one or more time slots. A slot specification is a string, in the format: "name value ...". The name field is a day name ("mon"-"sun"), a date in MD order ("12/31") or a date in YMD order ("95/12/31"). The value is a list of times in 24 hour HH:MM[-HH:MM] format ("7:30-12:30 13:30-17:30 17:35"). A value "off" clears all time slots for that day. The time slot accuracy is SLOT_TICK csecs. Any day that does not have specified values is switched 'off'. Build the event body using exdr_write() and the message definition SMT_SLOT_SPEC. The event body consists of these fields (see exdr_write() for the field types):

    SMTTRAN replies to a SPECIFY event with a SLOT_ERROR event if there was an error, else it does not reply.

    The ON Method

    Sets the timer 'ON'. The event does not have a body. The timer will respond with a SWITCH_OFF event when it moves into an 'OFF' state.

    The OFF Method

    Sets the timer 'OFF'. The event does not have a body. The timer will respond with a SWITCH_ON event when it moves into an 'ON' state.

    Example Of Use

    The SMTTST2 agent shows an example of using the time slot agent.

    SMTSLOT Implementation

    The hypertext view of SMTSLOT's dialog and source code may help to understand how SMTSLOT works.  

    The Socket I/O Agent - SMTSOCK

    Handles input and output to TCP and UDP sockets. You should use this agent for all access to TCP or UDP sockets, although you can also access sockets directly using the SFL socket access functions. Socket i/o is both central to most Internet servers, and reasonably delicate, making it a task that is well done by a specific agent.

    SMTSOCK has two main functions: it acts as the central 'heartbeat' for an Internet server, and it perform input and output on sockets. The heartbeat function works as follows: SMTSOCK uses the select() function to monitor all open sockets. Each socket is owned by a thread, somewhere. When a socket shows signs of life, SMTSOCK sends an event to the appropriate thread. The thread can then decide to read or write data as required. In a typical Internet application -- such as the XITAMI web server -- the socket agent is the main source of the events that drive the application. By contrast, in non-Internet applications the 'heartbeat' role could be played by the timer agent SMTTIME.

    The second task for SMTSOCK is input and output on sockets. For instance, you can ask SMTSOCK to read data from a socket, or to write a block of data to a socket. Both these tasks can require multiple cycles, waiting until the socket is ready, then reading/writing as much data as possible, until all the data has been read/written. SMTSOCK handles this automatically.

    To use SMTSOCK, call smtsock init(). This creates a single unnamed thread automatically the first time it is called, and has no effect thereafter. You can then send messages to this thread. SMTSOCK replies to most events. It supports these methods:

    Example of initialising SMTSOCK:

    /*  Static data                                                 */
    static QID
        sockq;                      /*  Socket agent event queue    */
    
        /*  In agent initialisation code                            */
        THREAD  *thread;            /*  Handle to various threads   */
    
        /*  Reply events from socket agent                          */
        method declare (agent, "SOCK_INPUT_OK",  ok_event,       0);
        method declare (agent, "SOCK_OUTPUT_OK", ok_event,       0);
        method declare (agent, "SOCK_READ_OK",   read_ok_event,  0);
        method declare (agent, "SOCK_WRITE_OK",  write_ok_event, 0);
        method declare (agent, "SOCK_CLOSED",    closed_event,   0);
        method declare (agent, "SOCK_ERROR",     error_event,    0);
        method declare (agent, "SOCK_TIMEOUT",   error_event,    0);
    
        /*  Ensure that socket agent is running, else start it      */
        smtsock init ();
        if ((thread = thread lookup (SMT_SOCKET, "")) != NULL)
            sockq = thread-> queue-> qid;
        else
            return (-1);
    

    The READ Method

    Waits for, and reads data from a socket. TCP/IP breaks a stream of data into chunks of arbitrary size, and each low-level read operation will read one such chunk. Thus, to read a specific amount of data, you may need to make several low-level read calls. SMTSOCK packages this so that one READ event can read as much data as required. You can alternatively ask SMTSOCK to read just the next chunk sent by TCP/IP. Build the event body using exdr_write() and the message definition SMT_SOCK_READ. The event body consists of these fields (see exdr_write() for the field types):

    SMTSOCK replies to a READ event with one of these events:

    The READR Method

    Works in the same way as the READ method, but works repeatedly until a FLUSH is sent for the socket. The READR method is useful for servers that have to loop on reading a socket; it saves the need for sending fresh READ events.

    The WRITE Method

    Writes a block of data to a socket. If you call the low-level TCP/IP write function directly, you must handle various error and retry conditions. It is easier and safer to use SMTSOCK to do this. Build the event body using exdr_write() and the message definition SMT_SOCK_WRITE. The event body consists of these fields:

    SMTSOCK replies to a WRITE event with one of these events:

    The INPUT Method

    Waits for input to arrive on a socket. This can be data, or a connection request. Build the event body using exdr_write() and the message definition SMT_SOCK_INPUT. The event body consists of these fields:

    SMTSOCK replies to an INPUT event with one of these events:

    The INPUTR Method

    Works in the same way as the INPUT method, but works repeatedly until a FLUSH is sent for the socket. The INPUTR method is useful for servers that have to loop on waiting for a socket; it saves the need for sending fresh INPUT events.

    The OUTPUT Method

    Waits for a socket to be ready for output. If you use the low- level TCP/IP write functions, you must be sure that the socket is ready for output, or your thread will block the entire application if it has to wait. Build the event body using exdr_write() and the message definition SMT_SOCK_OUTPUT. The event body consists of these fields:

    SMTSOCK replies to an OUTPUT event with one of these events:

    The CONNECT Method

    Establishes a TCP or UDP connection to some specified host and service (or port). Build the event body using exdr_write() and the message definition SMT_SOCK_CONNECT. The event body consists of these fields:

    SMTSOCK replies to a CONNECT event with one of these events:

    The FLUSH Method

    Removes any requests for a socket. Since events are delivered in a straight first-in first-out basis (ignoring the high priority SHUTDOWN event) it is safe to send first a FLUSH event, followed by another event, with no intervening wait. Build the event body using exdr_write() and the message definition SMT_SOCK_FLUSH. The event body consists of these fields:

    SMTSOCK does not reply to a FLUSH event.

    Example Of Use

    The SMTECHO agent provides a good basic example of using SMTSOCK. Study this program, and use it as a basis for your own socket-based agents.

    Notes and Comments

    The SOCK_CLOSED and SOCK_TIMEOUT return events can come from various requests; to make processing of this possible, they are always formatted as SOCK_READ_OK events.

    SMTSOCK Implementation

    The hypertext view of SMTSOCK's dialog and source code may help to understand how SMTSOCK works.  

    The Transfer Agent - SMTTRAN

    Transfers blocks of data or files to connected sockets. You can use this agent to simplify certain types of communication. The transfer agent uses the socket agent for actual reading and writing to sockets.

    To use SMTTRAN, call smttran init(). This creates a single unnamed thread automatically the first time it is called, and has no effect thereafter. You can then send messages to this thread. It supports these methods:

    Example of initialising SMTTRAN:

    /*  Static data                                                 */
    static QID
        tranq;                      /*  Transfer agent queue        */
    
        /*  In agent initialisation code                            */
        THREAD  *thread;            /*  Handle to console thread    */
        /*  Ensure that transfer agent is running, else start it    */
        smttran init ();
        if ((thread = thread lookup (SMT_TRANSFER, "")) != NULL)
            tranq = thread-> queue-> qid;
        else
            return (-1);
    

    The PUT_BLOCK Method

    Writes a length-specified block to a socket: first writes a two-byte length specifier in network format, then writes the block data. Build the event body using exdr_write() and the message definition SMT_TRAN_PUTB. The event body consists of these fields (see exdr_write() for the field types):

    SMTTRAN replies to a PUT_BLOCK event with one of these events:

    The GET_BLOCK Method

    Reads a length-specified block from a socket: first reads a two-byte length specifier in network format, then reads that many bytes of block data. Build the event body using exdr_write() and the message definition SMT_TRAN_GETB. The event body consists of these fields (see exdr_write() for the field types):

    SMTTRAN replies to a GET_BLOCK event with one of these events:

    The PUT_FILE Method

    Writes a file to a socket: reads the file in pieces of unspecified size, and writes these to the output socket. Build the event body using exdr_write() and the message definition SMT_TRAN_PUTF. The event body consists of these fields (see exdr_write() for the field types):

    SMTTRAN replies to a PUT_FILE event with one of these events:

    The GET_FILE Method

    Reads a file from a socket and saves it with the specified name. Build the event body using exdr_write() and the message definition SMT_TRAN_GETF. The event body consists of these fields (see exdr_write() for the field types):

    SMTTRAN replies to a GET_FILE event with one of these events:

    The COMMIT Method

    Waits until all put and get requests are finished, then replies with a TRAN_CLOSED event. This event does not take any arguments. SMTTRAN replies to a GET_FILE event with one of these events:

    Example Of Use

    The SMTHTTP agent uses the transfer agent to send files.

    SMTTRAN Implementation

    The hypertext view of SMTTRAN's dialog and source code may help to understand how SMTTRAN works.  

    The TCP ECHO Agent - SMTECHO

    Provides the TCP echo service on port 7.

    To use SMTECHO, call smtecho init(). This initialises the echo service on TCP port 7. If you set the global variable ip_portbase to some value before initialising SMTECHO, the echo port is moved by that value. For instance, if you set ip_portbase to 8000, the echo port is 8007. This lets you run the service in 'user space': on most systems you need root access to use ports below 1000.

    SMTECHO Implementation

    The hypertext view of SMTECHO's dialog and source code may help to understand how SMTECHO works.  

    The HTTP Agent - SMTHTTP

    Provides the HTTP service on port 80.

    To use SMTHTTP, call smthttp init(). This initialises the HTTP service on TCP port 80. If you set the global variable ip_portbase to some value before initialising SMTHTTP, the HTTP port is moved by that value. For instance, if you set ip_portbase to 8000, the HTTP port is 8080. This lets you run the service in 'user space': on most systems you need root access to use ports below 1000.

    To connect to a webserver running on some system at port 8080, use this URL: http://hostname:8080/.

    The current implementation of SMTHTTP is quite sophisticated. It will look for default.htm, default.html, index.htm, index.html in that order if no filename is specified. The file smthttp.aut defines directories that are protected by username/password definitions. You can find the full documentation for the web server in the Xitami package.

    The program xitami.c is a fairly complete web server based on the SMTHTTP agent. You can use this program as-is, or as a basis for your own work.

    SMTHTTP Implementation

    The hypertext view of SMTHTTP's dialog and source code may help to understand how SMTHTTP works.  

    The Network Delay Simulation Agent - SMTSIMU

    Simulates network delays. You can use SMTSIMU when you want to test that an application will handle network delays in a robust manner. This can be important for applications that use the UDP protocol, which does not provide the same traffic-control features as TCP.

    SMTSIMU will insert itself invisibly between your application agents and the socket agent when you include the file smtsimu.h at the start of your program, after the other include files (specifically, after smtlib.h).

    SMTSIMU Implementation

    The hypertext view of SMTSIMU's dialog and source code may help to understand how SMTSIMU works.

    SMT API Quick Reference

    SMT Quick Reference                                   Revised: 1996/12/14 PH
    
    ------------------------------------------------------------------------------
    Global variables
    
        int     smt_errno                       Set when API detects an error
        char    *smt_errlist []                 Corresponding error messages
        event_t _the_next_event                 May be set by thread code
        event_t _the_external_event             Set by event_wait()
        event_t _the_exception_event            May be set by thread code
        Bool    _exception_raised               May be set by thread code
        Bool    _io_completed                   Last lazy I/O completed
        Bool    _repeat_module                  Repeat current action module
        Bool    signal_raised                   True after interrupt
        Bool    shutdown_pending                When kill signal in progress
        int     signal_value                    Value of signal
    
    ------------------------------------------------------------------------------
    AGENT
        AGENT  *next, *prev;                    Doubly-linked list
        NODE    methods;                        Methods accepted by agent
        NODE    queues;                         Queues defined for agent
        char   *name;                           Agent's name
        Bool    router;                         True if multi-thread/queue
        int     priority;                       50=Low, 100=Normal, 200=High
        long    max_threads;                    Max. permitted threads, or 0
        long    cur_threads;                    Current number of threads
        long    top_threads;                    Max. number threads we had
        long    thread_tally;                   How many threads created
        long    switch_tally;                   How many context switches
    
    ------------------------------------------------------------------------------
    METHOD
        METHOD *next, *prev;                    Doubly-linked list
        AGENT  *agent;                          Parent agent descriptor
        char   *name;                           Name of method
        int     priority;                       50=Low, 100=Normal, 200=High
        int     event_number;                   Internal event number
    
    ------------------------------------------------------------------------------
    QID
        long node;                              Location of queue (zero)
        long ident;                             Queue ID number (1..n)
    
    ------------------------------------------------------------------------------
    QUEUE
        QUEUE  *next, *prev;                    Doubly-linked list
        AGENT  *agent;                          Parent agent descriptor
        NODE    events;                         Events in queue
        NODE    threads;                        Threads for queue
        QID     qid;                            Queue ID descriptor
        int     max_events;                     Maximum allowed events
        int     cur_events;                     Current number of events
    
    ------------------------------------------------------------------------------
    EVENT
        EVENT  *next, *prev;                    Doubly-linked list
        QUEUE  *queue;                          Parent queue descriptor
        QID     sender;                         Replies come back here
        char   *name;                           Name of event
        size_t  body_size;                      Size of event body in bytes
        byte   *body;                           Event body
        char   *accept_event;                   Reply if we accept event
        char   *reject_event;                   Reply if we reject event
        char   *expire_event;                   Reply if we expire event
        time_t  timeout;                        Expires at this time (or 0)
    
    ------------------------------------------------------------------------------
    THREAD Properties
        THREAD  *next, *prev;                   Doubly-linked list
        QUEUE   *queue;                         Parent queue descriptor
        long     thread_id;                     Thread identifier number
        char    *name;                          Name of thread
        Bool     animate;                       Animate this thread
        void    *tcb;                           Thread context block (TCB)
        EVENT   *event;                         Last-received event
    
    ------------------------------------------------------------------------------
    SEMAPH Properties
        SEMAPH  *next, *prev;                   Doubly-linked list
        char    *name;                          Name of semaphore
    
    ------------------------------------------------------------------------------
    Function Prototypes
    
    int      smt_init           (void);
    int      smt_term           (void);
    int      smt_exec_full      (void);
    Bool     smt_exec_step      (void);
    Bool     smt_active         (void);
    void     smt_set_console    (QID *qid);
    void     smt_set_timer      (QID *qid);
    int      smt_atexit         (function exitfct);
    void     smt_shutdown       (void);
    AGENT   *agent_declare      (char *agent);
    AGENT   *agent_lookup       (char *agent);
    int      agent_destroy      (AGENT *agent);
    METHOD  *method_declare     (AGENT *agent, char *method, int nbr, int priority);
    METHOD  *method_lookup