REAL-TIME TASK MODEL and the SRMS API

Task threads can theoretically be any thread of execution that uses the SRMS API to communicate with the SRMS scheduler.  In order to keep consistency between this implementation and the theory behind it, the design of the task model closely follows the model used in Atlas' KURT Linux implementation. Like that one, this implementation in Windows NT supports tasks that know the execution requirement of their jobs and unknowledgeable tasks that do not. In this project, the real-time tasks used for testing are single-threaded user processes programmed using the Win32 API.  The major assumption made here is that all real-time task threads maintain their own local message queue separate from the application's predefined message queue.  Most Windows applications have a main thread that monitors the message queue and dispatches messages to an appropriate handler.  Instead, this implementation assumes that another sibling thread in the task’s process space is handling that job. Therefore, when receiving messages, all SRMS API functions refer to the task’s local thread message queue for simpler and faster communication. 

SRMS API:

The SRMS API is an application programming interface that hides the internals of the SRMS service from the real-time programmer.  Ideally, the SRMS API would seem even more transparent in a shared dynamic link library, but for now, the taskapi.c and taskapi.h files must be included in the compilation of the task executable. The following paragraphs details how a task should use the functions in the SRMS API and also what goes on inside them the API.  For a more information about the real-time task executable file and related compilation settings, refer to the section on File Descriptions and Compilation Settings.

HOW A REAL-TIME TASK USES THE SRMS API:

The first step to becoming a real-time thread is to register with the SRMS service.   To do this, the task needs to pass to the service its real-time parameters.   It calls initialize_rtparams() which is an SRMS API call that gets a lock on a special shared memory buffer for passing real-time parameters. This function returns a pointer to that special memory mapped region. The task then writes all of its real-time information to the buffer (real-time information in the rtparams struct is completely defined in rt_task.h). Once finished, the task calls set_rtparams() and passes to the function a pointer to the shared memory buffer. set_rtparams() sends the SRMS message handler a WM_RegistrationMsg and then waits for a reply. In the mean time, the SRMS service copies the information from the buffer and registers the task. When done, a reply is sent back to the task notifying it that registration is successful set_rtparams() returns.

Next, the task must attempt QoS admission via one of two methods: either the non-blocking call request_QoS_admission() or the blocking call await_QoS_admission(). The non-blocking call returns regardless of whether or not its desired QoS value is met, while the blocking call does not return until its QoS is satisfied. Internally, both functions pass a message to the SRMS message handler indicating that the task wants to attempt QoS admission. Since the service already has all of the task's real time information in memory, it begins QoS negotiation. When done, it posts a WM_QoS_Admission_SucceededMsg message back to the task thread's message queue along with the task's QoS value as the LPARAM argument of the message. If the task used the non-blocking call and this reply is WM_QoS_Admission_FailedMsg, then that means that the SRMS service could not guarantee its QoS and the request_QoS_admission() function returns false. The only other time that either function would return false is if the service sent it a WM_UnregisterRTConfirmedMsg message, which is simply a notice to the task that it has been unregistered, probably due the SRMS service exiting.

Once the task completes and passes QoS negotiation, it must establish mechanisms for communicating with the SRMS scheduler. The initialize_SRMS_communication() function prepares communication by creating the four task-specific event object handles (as described in the Communication and Synchronization Mechanisms section), a small shared memory buffer for transferring job execution times, and a universally defined admit/reject message and job failed message. Next in the setup process, the task must await another message from the SRMS service which is a WM_SystemStartTimeMsg message that has the tick count timestamp of when the system started as the LPARAM value of the message. This is done for the task thread by the await_system_start() API call.  The task only uses this start value for debugging purposes so that it can tag events with a timestamp relative to the SRMS service's time.

Finally, the task is ready to start executing jobs, so it enters its periodic loop. First it waits for a new job to be released by calling the special API function  await_new_job_release(). This function waits for the SRMS scheduler to signal the hJobReleased handle. Once this occurs, the task wakes from its waiting state and checks to see if there are any SRMS-posted messages in its message queue. Typical messages that it may find are one indicating that it must cleanup the last job or one telling it that it has been unexpectedly unregistered. If the former is detected, then that message is removed and ignored since the task is already at the top of its periodic loop. However, an unregister message cannot be avoided and the function returns a negative value. If neither of these messages is found, then the task flushes its message queue to get rid of any unwanted messages. Otherwise, if no message is posted, the function returns the task's current job number.

Now the task needs to communicate its next job's execution requirement. It gets its budget using the get_RTbudget() API call and estimates its job's execution time so long as it has enough time to calculate it. Once it has a value, it passes it to the admit_RTjob() API call who then writes the value to the shared memory buffer, signals the SRMS scheduler, and then waits for a response. It then waits for either an accept or reject response and returns the response to the task (it may also receive an unregister message, in which case the function returns the special SRMS_UNREGISTER_TASK flag and the real time task thread ends).

Next, the task calls await_scheduling() to wait to be scheduled. That function waits for one of two signals to occur: hSchedEvent or hJobReleased. If the first is signaled, then the task proceeds normally to perform its periodic work. But if the second handle is signaled, that means that this job was not scheduled during the period and that a new job has been released. In this case, await_scheduling() will return SRMS_JOB_FAILED to indicate to the task that it needs to clean itself up and reset itself to the beginning of its periodic loop. And again, the function also checks for any SRMS-posted messages and returns those if necessary.

Assuming that hSchedEvent is signaled and there are no cleanup messages in the task's queue, then the task finally has control of the processor.  It immediately calls the function do_work() with pointer to a task-defined worker function. The worker function has a prototype:

BOOL my_rt_work( VOID );

As solely a matter of design choice, the function takes no parameters, but it does have a boolean return value indicating whether there is still more real-time work to complete. It is suggested that the worker function operate in a tight loop that frequently checks the message queue for a WM_RTJobFailedMsg message from the SRMS scheduler. By catching this message and returning from the worker function as quickly as possible, the task thread will stay synchronized with the scheduler. When the worker function returns to do_work(), the message queue is again checked for any cleanup or unregister messages, and then it returns to the task the value returned by the worker function.

This procedure repeats itself indefinitely until either the SRMS service unregisters the task or the do_work() function returns false, meaning that there is no more real-time work to be done. In that case, the task calls the unregisterRT() API function with its real-time ID. The service does some work on its end to unregister the task with the SRMS system, and the task thread closes all communication objects that it opened during setup. Lastly, the task thread returns and the real-time task exits.

Note: A potential problem exists with multi-threaded processes that designate one thread to become a real-time task thread. In order to be able to schedule the task threads, the SRMS scheduler needs to boost the priority level of the real-time task thread to be equal to that of all other threads in the real-time task set. If the task thread's process is not in the same NT priority class as the other threads in the task set, then changing one thread's priority class will also unintentionally change the priority class of all its sibling threads in its process to the new priority class. This poses a serious potential problem, especially since task sets are usually scheduled in the REAL-TIME priority range.  If one or more sibling threads of the real-time task are scheduled at a higher thread priority level than the SRMS scheduler's, then the boost to the REAL-TIME class may cause those threads to preempt the SRMS scheduler thread, causing unpredictable results.


Example of the Real-Time task model used in this implementation:

#include <windows.h>
#include <mmsystem.h>
#include "taskapi.h"

BOOL my_rt_work( VOID );
LONG execTime = SRMS_EXEC_UNSURE;
   
int main(int argc, char *argv[])
{
    BOOL haveWork = TRUE;                     // true if there's work to do, else false
    BOOL accepted;                            // true if QoS admission is successful, false if not
    LONG jobnum = 0,                          // this job's numeric identifier
        budget = 0,                           // time remaining in budget (from srms)
        admitted = 0,                         // holds return value from admit_RTjob()
        result = 0;                           // holds return value from await_scheduling()
    DWORD rt_id;                              // holds this task's NT thread id
    DOUBLE myQos;                             // QoS expected to be delivered by SRMS for this task
    STARTUPINFO si;                           // startup info structure for this task

    rt_id = GetCurrentThreadId();
    my_rtparams = initialize_rtparams();
   
    // fill in the rt_params structure:
    my_rtparams->rt_id = rt_id;                             // NT assigned thread ID
    my_rtparams->rt_pid = GetCurrentProcessId();            // NT assigned process ID        
    my_rtparams->rt_importance = 0;                         // scheduler assign default importance
    my_rtparams->period = MY_PERIOD;                        // period in milliseconds
    read_sample_exec_file(argv[1],
                        my_rtparams->sample_execs);         // sample execution times array
    my_rtparams->num_samples = NUM_OF_SAMPLE_EXECS;         // number of samples in the array
    my_rtparams->desiredQoS = MY_DESIRED_QOS;
    my_rtparams->minimumQoS = MY_MINIMUM_QOS;
   
    GetStartupInfo( &si );
    lstrcpy(my_rtparams->lpszTitle, si.lpTitle);
   
    // duplicate the this thread's handle with all access privileges to give it to the scheduler
    DuplicateHandle(GetCurrentProcess(), GetCurrentThread(),
                    GetCurrentProcess(), &(my_rtparams->hThread),
                    THREAD_ALL_ACCESS, FALSE, 0);
   
    set_rtparams( my_rtparams );

    accepted = request_QoS_admission ( rt_id, &myQos );

    // check if Qos is sufficient for this task's minimum qos
    if ( !accepted || myQos < MY_MINIMUM_QOS )
        return(0);

    initialize_SRMS_communication( rt_id );

    await_system_start();

    while(haveWork)
    {
        jobnum = await_new_job_release();

        if (jobnum < 0)
            return (0);

        // ------------------------------------------------------------------------------
        // This next section supports tasks which have accurate knowledge of their jobs'
        // execution times and of the time it will take to compute those execution times.
        // ------------------------------------------------------------------------------

        // get the budget that was returned by the OS.

        budget = get_RTbudget();
        admitted = admit_RTjob( myExecTime );

        if ( admitted == SRMS_JOB_FAILED )
            continue;
        else
            if (admitted == SRMS_UNREGISTER_TASK )
                return (0);

        // -------------------------------------------------------------------------------
        // Waiting to be scheduled.
        // Wait for either a schedule event signal or a clean-up job signal from the OS

        result = await_scheduling();

        if ( result == SRMS_JOB_FAILED )
            continue;
        else
            if ( result == SRMS_UNREGISTER_TASK )
                return (0);

        haveWork = do_work(my_rt_work);             // Task is all set to execute

        if ( haveWork == SRMS_UNREGISTER_TASK )
            return(0);
    }

    // Unregister this task as a real-time task
    unregisterRT( rt_id );

    return (0);
}

BOOL my_rt_work( VOID )
{
    MSG msg;
    DWORD start;
    extern UINT WM_RTJobFailedMsg;

    start = timeGetTime();

    while ( (timeGetTime() < start + myExecTime) && (msg.message != WM_RTJobFailedMsg))
    {
        if (PeekMessage ( &msg, NULL, 0, 0, PM_NOREMOVE))
            TranslateMessage( &msg );

        SwitchToThread();
    }
    return TRUE;
}

< Back to the SRMS Service Home Page >