Next | Prev | Up | Top | Contents | Index

Process Synchronization and Share Groups

IRIX provides a variety of features to make it possible to build an application consisting of multiple, lightweight processes. In general, a lightweight process is one that shares the address space of its parent process (see "Process-Level Parallelism"). The parent process and the sibling processes that it creates are a share group. IRIX provides special services to share groups.


Process Communication and Coordination

IRIX supports a wide range of interprocess communication (IPC) facilities. These are discussed in detail in Chapter 2, "Interprocess Communication." They include:

The REACT(TM)/Pro product includes a number of examples of real-time programs that use IRIX IPC features. The REACT Real-Time Programmer's Guide includes the source code of additional examples.


Process Creation

The sproc() and sprocsp() functions create a lightweight process (see the sproc(2) reference page). The difference between the calls is that sproc() allocates a new memory segment to serve as the stack for the new process. You use sprocsp() to specify a stack segment that you have already allocated--for example, a block of memory that you allocate and lock against paging using mpin().

In the traditional fork() call, the new process executes the identical program text as the old one; that is, both processes "return" from fork() and you distinguish them by the return code, which is 0 in the child process and the new process ID in the parent.

The sproc() call differs in that it takes as an argument the address of the function that should be executed by the new process. Often, each child process has a particular role to play, and the function that represents that work.

Another design is possible. The sproc() function has considerable overhead. It is inefficient to continually create and destroy child processes. In some applications, you may have to manage a flow of many, relatively short, activities which should be done in parallel. You do not want to create a new child process for each activity and destroy it afterward. Instead, you can create a pool containing a small number of general-purpose processes. When a piece of work needs to be done, you can dispatch one process to do it. The fragmentary code in Example 3-1 shows the general approach.

Example 3-1 : Partial Code to Manage a Pool of Processes

typedef void (*func)(void *arg) workFunc;
struct oneSproc {
   struct oneSproc *next;        /* -> next oneSproc ready to run */
   workFunc calledFunc;          /* -> function sproc is to call */
   void *callArg;                /* argument to pass to called func */
   usema_t *sprocDone;           /* optional sema to post on completion */
   usema_t *sprocWait;           /* sproc waits for work here */
} sprocList[NUMSPROCS];
usema_t *readySprocs;            /* count represents sprocs ready to work */
uslock_t sprocListLock;          /* mutex control of sprocList head */
struct oneSproc *sprocList;      /* -> first ready oneSproc */
/*
|| Put a oneSproc structure on the ready list and sleep on it.
|| Called by a child process when its work is done.
*/
void sprocSleep(struct oneSproc *theSproc)
{
    ussetlock(sprocListLock);       /* acquire exclusive rights to sprocList */
    theSproc->next = sprocList;  /* put self on the list */
    sprocList = theSproc;
    usunsetlock(sprocListLock);  /* release sprocList */
    usvsema(readySprocs);           /* notify master, at least 1 on the list */
    uspsema(theSproc->sprocWait);/* sleep until master posts me */
}
/*
|| Body of a general-purpose child process. The argument, which must
|| be declared void* to match the sproc() prototype, is the oneSproc
|| structure that represents this process.   The contents of that
|| struct, in particular sprocWait, are initialized by the parent.
*/
void childBody(void *theSprocAsVoid)
{
    struct oneSproc *mySproc = (struct oneSproc *)theSprocAsVoid;
    /* here one could establish signal handlers, etc. */
    for(;;)
    {
        sprocSleep(mySproc);      /* wait for work to do */
        mySproc->calledFunc(mySproc->callArg);  /* do the work */
        if (mySproc->sprocDone)   /* if a completion sema is given, */
            usvsema(mySproc->sprocDone); /* ..post it */
    }
}
/*
|| Acquire a oneSproc structure from the ready list, waiting if necessary. 
|| Called by the master process as part of dispatching a sproc.
*/
struct oneSproc *getSproc()
{
    struct oneSproc *theSproc;
    uspsema(readySprocs);        /* wait until at least 1 sproc is free */
    ussetlock(sprocListLock);       /* acquire exclusive rights to sprocList */
    theSproc = sprocList;        /* get address of first free oneSproc */
    sprocList = theSproc->next;  /* make next in list, the head of list */
    usunsetlock(sprocListLock);  /* release sprocList */
    return theSproc;
}
/*
|| Start a function going asynchronously. Called by master process.
*/
void execFunc(workFunc toCall, void *callWith, usema_t *done)
{
    struct oneSproc *theSproc = getSproc();
    theSproc->calledFunc = toCall;     /* set address of func to exec */
    theSproc->callArg = callWith;      /* set argument to pass */
    theSproc->sprocDone = done;           /* set sema to post on completion */
    usvsema(theSproc->sprocWait);      /* wake up sleeping process */
}

Process Scheduling Features

The IRIX kernel supports special process scheduling rules for share groups. This permits you to increase the efficiency of a parallel program in some cases. The feature is controlled by the schedctl() kernel function (detailed in the schedctl(2) reference page).

When schedctl() is called with the SCHEDMODE argument, it sets one of three scheduling rules for the share group whose member issues the call:

SGS_FREEThe normal situation, in which each process is scheduled individually.
SGS_SINGLEAll but the master process of the share group are blocked. This permits the master process to perform initialization or error recovery without contention from other members of the group.
SGS_GANGAll processes of the group run concurrently, provided there are sufficient CPUs available.

Under gang scheduling, IRIX tries to run all processes of a share group concurrently. When this is possible (in other words, when there are enough available CPUs in the multiprocessor), gang scheduling can greatly reduce lock conflicts between processes.

Without gang scheduling, one member of the share group can acquire a lock and then be suspended. Another member, attempting to acquire the lock, is also suspended until the first process is dispatched again and releases the lock.

With gang scheduling, when a second member attempts to acquire the lock, the first process is almost certainly executing at the same time, and releases the lock while the second member is still spinning.


Process Management Features

The prctl() kernel function provides a variety of process-related management tools (detailed in the prctl(2) reference page). One feature useful for parallel programs is the PR_MAXPPROCS query. This returns the number of different CPUs that the calling process could use for execution. The returned number is 1 when the caller has been assigned to a particular CPU. Otherwise it is the number of unrestricted CPUs in the system. A parent process could use this during initialization to find out the degree of parallelism it can hope to achieve.

The sysmp() kernel function provides information about a multiprocessor (detailed in the sysmp(2) reference page). Some of the queries useful to a parallel program include MP_NPROCS, return number of CPUs in the system, and MP_NAPROCS, return the number of CPUs available for normal process scheduling.


Next | Prev | Up | Top | Contents | Index