Project 1: Modeling the Basic Operating Principle of the SIMD Multiprocessor Architecture Using Threads for Massive

Matrix Multiplication

Presented by:

Patrice LaFlamme - 1954157

Eric Lapointe - 1961773

Warren Layton - 1956280

Presented to:

Klaus Kanneman

In the context of the class:

CEG4393: Computer Structures

Presented on the day of:

Thursday October 17th, 2002

Preamble

The purpose of this project is to model the basic operation of the single instruction, multiple data (SIMD) architecture using threads. In this architecture, there is a control unit, or CU, which synchronizes the operation of one or more processing elements, or PEs. The algorithm executed by each of the PEs is identical; only the data being processed differs between them. Because of this, it can be said that a "SIMD computer exploits spatial parallelism, rather than temporal parallelism as in a pipelined computer."1

The particular problem being solved with this model is the multiplication of two large matrices. Since matrix multiplication can be divided into many smaller operations for multiplying individual rows and columns, the calculations may be easily decomposed and divided between a number of PEs, all of which execute the same algorithm. This problem is therefore ideally suited for such an architecture.

1 Hwang, Kai, "Advanced Computer Architecture: Parallelism, Scalability, Programmability", McGraw-Hill, New York, 1993, page 11

Rationale of the Design

The purpose of this project is to execute massive matrix multiplication on a SIMD architecture, specifically a four processor Sun SPARC system. The SIMD architecture is particularly suited for this kind of application since matrix multiplication involves computing different elements (multiple data) with the same algorithm (single instruction). In order to have an effective design we need to consider two major factors: data proffering by the control unit (CU) and the synchronization between this CU and the processing elements (PE).

In a parallel computing environment, there are many algorithms to proffer the data and implement the synchronization, but not all methods are equally optimal. The first method that was proposed in class was to assign a thread to each of the processing elements and then suspend the threads while the control unit assigned work to each of them. After the data was assigned, the control unit would then wake up each thread sequentially and wait until each thread was finished its workload and suspended once again. Our team didn't particularly like this method because it didn't take full advantage of the parallelism available, as each thread is woken up sequentially. Also, suspending and resuming threads, while not overly costly in terms of computing time, is not optimal.

Our misgivings about the above method lead us to research inter-process signaling and condition variables. Instead of waking up each thread sequentially, we wanted a method that would allow the control unit to send out a single signal that would cause each PE to begin their execution once the work had been assigned. This method is preferable since all processing elements are able to begin as soon as the control unit sends out its signal, as opposed to having each thread wait for their turn to be woken up.

Although our chosen implementation takes advantage of the SIMD architecture, its performance could be dramatically increased. Since we are simulating 64 PEs on a system where only 4 processors are available, overhead is introduced because the operating system has to constantly schedule the next running PE. To minimize this overhead, we use system threads as opposed to processes.

Also, this implementation introduces certain complexities into the program. For example, locking with mutexes and condition variables is critical for avoiding deadlocks and assure an orderly program flow. Even more, it is necessary to implement a method to alert the control unit when all the PEs have completed their assigned tasks, which involves incrementing a counter that is protected by a mutex. However, the overhead of this low-level locking and protection is much smaller than that of the design proposed in class, where the threads were constantly switch between the suspended and running states.

Applied Design Principles

Two design principles that are implicitly considered in this project are the partitioning of the computational tasks and multithreading; these concepts go hand-in-hand. Proper partitioning of computational tasks in a SIMD architecture is simple. Each processing element is responsible for the execution of the same instructions; only the data varies between the PEs. In the context of this particular project, we are guaranteed that the number of PEs will be a divisor of the matrix size, implying that the work is divided evenly between all the PEs.

In order to take advantage of the parallel processing of the SIMD architecture, we created a multithreaded application. A thread, sometimes called a lightweight process, doesn't consume as many resources as traditional UNIX processes. We created a thread for each PE in our program and each of them performed the same operations on the data. There is also a main thread created for the CU. When the processing is finished, all of the threads are terminated and the CU returns control to the main function in the program, which then exits.

While the purpose of the program's design is to take advantage of the parallel processing available, there are certain tasks that must be done sequentially. In order to accomplish a proper ordering of events, it is necessary to make use of methods of protection and synchronization.

Protection is needed to avoid deadlocks and to assure that certain commands can be grouped together as an atomic (indivisible) operation. In the program, the processing elements and the control unit must all access global data in order to synchronize the bursts of computation. We made use of the mutexes provided by the Solaris libraries to create critical sections in our code. A critical section of code is a segment that needs to be executed by one thread (or process) at a time and that may not be interrupted once it has been entered. Mutexes provide a way to lock out any thread from a critical section while another thread is currently executing the protected statements. Mutexes are also used to prevent deadlocks for SIMD computers. Even such simple resources as the standard output on UNIX systems can cause a deadlock if two threads attempt to direct their output to the screen at the same time (a problem that we had to address during this project while testing our program on the four-processor "site0" Sun Sparc UltraAX-MP computer). Most of the critical sections that were added to the program addressed protection violations that were non-existent on a uniprocessor computer, but had severe consequences on a multiprocessor system.

Synchronization was the most important issue to consider while designing the program. The synchronization of the threads is needed in two opposite directions: from the control unit to the processing elements to signal the start of a burst, and from the processing elements to the control unit to signal the end of a burst.

In order to signal the start of a burst simultaneously to all of the processing elements, we used condition variables and the functions cond_broadcast and cond_wait. The PEs stall on the cond_wait function while the control unit sets the matrix indexes to be used. Once the matrix indexes are ready, the control unit issues a call to cond_broadcast, which allows all of the threads stuck in a cond_wait call to proceed and to use the new index values. Before discovering the cond_wait and cond_broadcast calls, we considered using the standard UNIX signaling system to begin the bursts. However, using condition variables and functions to manipulate them provided a much more elegant solution while maintaining the parallel start to the bursts that we desired.

In the other direction, we used a mutex-protected counter that is incremented each time a processing element finishes its burst. When the counter reaches the number of processing elements, it unlocks the control unit who then proffers the next elements to compute.

Methods and Tools

Method:

Team Organization

Our team always worked together during the scheduled lab periods. This was mainly because it was easier for us to create a linear SCCS history instead of constantly branching and merging different changes made by each team member on their own. In addition, this allowed each team member to observe and understand the changes to the program as they happened. It also improved communication between team members, since all of us were present when we held discussions about issues relating to our program's development.

We did divide the work between all three team members for such tasks as adding the command-line argument parsing, the reading of the matrix dimensions and elements from files and the addition of macros and error checking.

Program Organization

We used certain conventions when writing our program in order to maintain the legibility of the code. A relatively small change that we made to further this cause was to introduce macros into our code for all error handling. For all functions that result in system calls (from the user-space program to the kernel), we check the return value to determine whether an error was made, and then exit gracefully if needed. Macros were also used for the display function, which contains code to assure the mutual exclusion of the output to the screen. All of these macros were included in the header file, mproc.h.

In addition to the macros, all constants were defined in the header file. This convention kept the C source file smaller while not making it any more difficult to understand. It also follows suggested C programming convention.

Since we are working with matrices, we used two-dimensional float arrays to store the data to be processed. Then another array was needed to store the results of the computation. Because the system resources permitted it, the three matrices can be held in main memory at the same time. This simplifies the program in the way that the whole data can be buffered at the beginning of the program and then no other file handling is needed.

Program Algorithm

First the computational data had to be equally divided between the PEs, in such a way that PEs would not access the same matrix element at the same time for the whole duration of the program. To resolve this problem, we used the pass-by-burst approach. A pass is defined has the processing of a diagonal grouping of elements in the result matrix (see figure 1).

Figure 1: Colour-coded representation of each pass though the matrix containing the result of the calculations. The red areas represent the first pass, after which the diagonals are cycled through from left to right until all of the elements in the black diagonal bar have been calculated.

Bursts are synchronization barriers. Within a burst we assure that the PEs accesses only its assigned row and column to assure a collision free matrix computation. The number of passes depends only on the matrix size as implied above. The number of bursts will vary in function of the matrices size and the number of PEs involved. If the number of elements in a row is equal to the number of PEs, then only one burst per pass is required. If this is not the case, we need to divide size of row/column by the number of PEs to obtain the number of passes (see figure 2 and figure 3).

Example: Matrix size: 64x64 #PEs: 8

From the definition stated above, there would be 64 passes of 64/8 = 8 bursts each.

Figure 2: The passes necessary to multiply two (2) 4x4 matrices with 4 processing elements . Note that since the PEs run concurrently (and in parallel on a multiprocessor machine) they may not necessarily start or finish in the order shown for each pass.

Figure 3: The first four passes of the multiplication of two 8x8 matrices with four (4) PEs. Note that since the PEs run concurrently (and in parallel on a multiprocessor machine) they may not necessarily start or finish in the order shown for each pass.

As stated in the project specifications, the main section of the program creates the threads and then calls the CU, implemented as a procedure call. It also gracefully exits by returning a “0” exit code when the CU terminates successfully. Then, the control unit is only responsible for the calculation of the new matrix index and the broadcasting of the signal that starts the processing elements. The control unit is also responsible for determining when the last burst is about to occur and to alert the PEs of this fact.

In turn, the processing elements are only responsible for calculating the matrix element assigned to them and then waiting for the control unit to assign a new index. Since each PE is assigned a unique integer identification number when they are started, the CU only needs to increment the matrix index by the total number of processing elements after each burst, modulo the matrix size. When the burst starts, each PE reads the new index value and then adds its own unique identification number to that value. That way, each processing element is guaranteed to work on a different row of the A matrix, and a different column of the B matrix. When the index value for the row loops back to zero (due to the modulo operation), the index value for the column is incremented by one to make sure that each diagonal in the matrix is calculated. This method works because, from the project's description, we are assured that the number of processing elements must be a divisor of the matrix size.

Program Synchronization Barrier

This part describes own the synchronization barrier has been implemented. As we explained in the rationale of our design and the applied design principles, the CU is responsible for proffering data to the PEs and keep them synchronized. As described above, synchronization occurs at the beginning of each burst. During a burst, all the PEs work independently as they don’t need to communicate between each other to accomplish their task. When all the PEs are done with a burst they must wait for a signal from the CU before starting the next burst. Also, the CU must wait for a signal from the PEs to know when to change the matrix indexes. The PEs use a function named signalCU() to accomplish this task.

The signalCU() function uses a mutex (named lock in our code) to assure that only one PE can execute it at any one time. Once a PE acquires the mutex, it increments a static counter by one, then if the counter is equal to the number of PEs, it posts a semaphore (named CUSynchControl) which will alert the CU that all the PEs have completed the burst and it also resets the counter to 0.

The PEs block on a conditional variable (cond_wait(&burstSynchControl)) which is controlled by the CU. When the CU receives the signal from the PEs, it changes the matrix indexes and sends a signal (cond_broadcast(&burstSynchControl)). This sounds fairly simple but because of time-slicing that is performed by the operating system, we had to carefully implement the sequence of operations to obtain the desired result. There were three different situations that had to be handled with care.

First we consider the first burst of the first pass. Our main program creates the threads which start immediately. Then the CU sets the first matrix indexes and sends the signal to start. Here we need to be certain that all the PEs have blocked on the conditional variable. If only one PE doesn’t receive the CU’s signal, it will block on the conditional variable forever. This in turn implies that the CU will forever wait for a signal from the PEs, creating a deadlock situation. To fix this problem, the CU waits for a signal from the PEs before starting the first pass. This implies that each PE make a function call to signalCU() before blocking on the condition variable.

Secondly, for any burst, when the last PE executes the signalCU() function, it has to reach the cond_wait() function before the CU executes the cond_broadcast() function. If it doesn’t, we end up having the same problem as stated above. To resolve this problem we had to understand how the cond_wait() function works and cleverly use mutexes. Before calling the signalCU() function, the PE acquires a mutex (mutex_lock(burstSynchMutex)). Now the trick is the following, the CU has to acquire this same mutex before issuing the cond_broadcast(). Then when the PE returns from the signalCU() call, it does a cond_wait(&burstSynchControl, &burstSynchMutex). This atomically blocks on the condition variable and releases the mutex. Now that the mutex has been released, the CU can safely signal the start of a new burst.

The last special condition occurs when the CU signals the last burst of the last pass. Since the PEs block on the condition variable just before the computing statements, we have to notify them that this signal will be the last from CU and that they must not block on the conditional variable once they finish the next burst. The CU controls a flag (name work) which is the condition of the while loop of the PEs. When all the PEs are blocked on the conditional variable, we change this flag’s value to signal the last burst. The PEs will compute the last burst and when they test the flag, will exit the loop and then do a thr_exit(). By doing this they are not signaling the CU as before, this means that the CU must not block on the semaphore posted in the signalCU() function. Instead the CU blocks with the thr_join() that will wait for all the threads exit.

Tools:

This section briefly describes the development tools we used to create a productive work environment.

Text Editor

Our entire team used the vi editor as their text editor and working environment. It is possible to quickly invoke command-line programs from within vi, not to mention compiling and executing the program. When the makefile is in the current working directory, typing :!make <target> can do any task that is included in the project's makefile. Also, the use of macros (see .exrc file on disk) improves and considerably accelerate the speed at which we can produce effective code.

Make

In addition to the vi editor, a properly constructed makefile is an effective tool that can reduce time spent compiling and testing programs. Our makefile included instructions to perform the SCCS get operations (including a useful unget target, which allowed us to back out of undesirable changes), remove unnecessary files from the current directory and to test and verify our program by executing tools such as cscope.

scp

We made use of scp (secure cp) to copy the SCCS master files between team members when a new revision was created. It was easier to send the file in this fashion from one user to another than to adjust the permissions in each user's home directory (in many cases, the permissions that would allow team members to access the directories would necessarily allow any student to access the directories, too). Another reason behind this choice was the fact that we always worked together during the lab periods, so there was no need for complex merging or branching of the code base.

cscope

cscope is used to browse through C source files and find any identifier or code element. All the files in the current directory are search and the whole lines containing the requested element are displayed. We can then easily start editing the file at the line of the requested element using vi by simply selected the associated line in cscope. Although cscope was properly configured and used, since our project consisted of only two files (only one .c file) most of the elements were easy to find using vi’s own search capabilities.

SCCS

SCCS stands for Source Code Control System. In order to maintain the credibility of our work the use of such a tool is essential. By preserving all the versions of our source code, we give the possibility of following the complete evolution of our development. In additon, it simplifies the backup of the code and also provides an easy way to revert back to an older version. Returning to an older version can be useful in the event where inserting new code causes the newer version to malfunction and where the bugs can’t be found.

cc

The C compiler is probably the most important tool we used for this project. Without it, we could never create executable code and accomplish the tasks described in this document. cc interfaces to the ANSI/ISO C compilation system. This system is composed of a preprocessor (see cp below) and the code generator that actually creates the executable code. It also performs tasks such as assembling, optimizing and linking the code.

cpp

This is the C preprocessor which, in fact, is a macro processor. In our project, we used it to create clearer code and to automate repetitive tasks such as error checking. Constants that are used throughout the code, can easily be modified by using the cpp. We define identifier and associate a value with them. The cpp is also very useful for debugging purposes. Identifiers can be used in conjunction with the #ifdef/#ifndef preprocessor commands to eliminate or include code in function of the value associated with the defined identifier.

Flow Charts

Figure 4: Flow chart for the main function in the mproc program.

Figure 5: Flow chart for the CU function in the mproc program. Upon exit, it returns to the main function (see figure 4, above).

Figure 6: Flow chart for the PE function in the mproc program.

Source Code Listing

/*----------------------------------------------

Module: mproc.c

Version: 4.1

Qualifier: mproc

Type: ANSI-C

Delta Date: 10/21/02

Retrieval Date: 10/21/02

Masterfile:

/a/csie/h/users/u1954157/CEG4393/group23_projI_MMII/s.mproc.c

----------------------------------------------*/

#include "mproc.h"

void *ePEn (void *);

void *eCU (void);

void *signalCU();

extern int errno;

cond_t burstSynchControl;

mutex_t burstSynchMutex,output_stream;

sema_t CUSynchControl;

int a_row=0, b_col=0,msiz,burst,npe,work=1;

float mat_a[MAXSIZ][MAXSIZ],mat_b[MAXSIZ][MAXSIZ],mat_c[MAXSIZ][MAXSIZ];

thread_t tid[MAXPE]; /* max out to MAXPE PEs */

int main(int argc, char **argv) {

int i;

char hostname[1024];

time_t current_time;

/*Initialize (streams unbuffered)*/

setbuf(stdout,NULL);

setbuf(stderr,NULL);

/* Display information */

PGMINFO()

/* Process command-line options */

if(argc > 3) {

display(stdout,"Invalid invocation: Usage: mproc <npe> <msize>\n",NULL,NULL,NULL);

exit(1);

}

if(argc > 1) {

npe = atoi(argv[1]);

if(npe > MAXPE || npe < 1) {

display(stdout,"Invalid invocation: Number of PEs must be between 1 and %d\n",

MAXPE,NULL,NULL)

exit(1);

}

} else {

npe = N_THREADS;

}

if(argc == 3) {

msiz = atoi(argv[2]);

if(msiz > MAXSIZ || msiz < MINSIZ) {

display(stdout,"Invalid invocation: Matrix size must between %d and %d\n",

MINSIZ,MAXSIZ,NULL);

exit(1);

} else if(msiz % npe !=0) {

display(stdout,"Invalid ratio: Number of PEs must be a factor of the matrix size\n",

NULL,NULL,NULL);

exit(1);

}

} else {

msiz = 0;

}

/*Create PE threads*/

for(i=0; i<npe; i++) {

/* This creates thread that will contend for system-wide

ressources.*/

CATCHSYSERROR(thr_create(NULL,NULL,ePEn,(void*)i,THR_BOUND,&(tid[i])),"no thread");

}

/*initialize conditional variable and semaphore*/

CATCHSYSERROR(cond_init(&burstSynchControl, USYNC_THREAD, NULL),"Unable to initialize condition variable");

CATCHSYSERROR(sema_init(&CUSynchControl,0,USYNC_THREAD,NULL),"Unable to initialize semaphore");

/* Call our CU */

eCU();

return 0;

}

void *eCU () {

int i, asiz, bsiz, j,k,nburst;

float next;

void *status; /* return status of the PEs */

FILE *file_a, *file_b;

/* Read the matrices from the files */

file_a = fopen("A.dat", "r");

CATCHFOPENERROR(file_a, "Unable to open A.dat");

file_b = fopen("B.dat", "r");

CATCHFOPENERROR(file_b, "Unable to open B.dat");

/* first line: size of matrices */

CATCHEOF(fscanf(file_a, "%d", &asiz), "A.dat has incorrect file format\n");

CATCHEOF(fscanf(file_b, "%d", &bsiz), "B.dat has incorrect file format\n");

if (msiz == 0) {

if (asiz != bsiz) {

display(stderr, "Both matrices must have same size in A.dat and B.dat\n", NULL, NULL, NULL);

exit(1);

}

if (asiz > MAXSIZ || asiz < MINSIZ) {

display(stderr, "Matrix in A.dat must be between size %d and size %d\n", MINSIZ, MAXSIZ, NULL);

exit(1);

}

if (bsiz > MAXSIZ || bsiz < MINSIZ) {

display(stderr, "Matrix in B.dat must be between size %d and size %d\n", MINSIZ, MAXSIZ, NULL);

exit(1);

}

msiz = asiz;

} else {

if (msiz > asiz || msiz > bsiz) {

display(stderr, "Invalid invocation: msiz must be bounded by the size in A.dat and B.dat\n", NULL, NULL, NULL);

exit(1);

}

}

if ( (msiz % npe) != 0) {

display(stderr, "Invalid ratio: Number of PEs must be a factor of the matrix size\n", NULL,NULL,NULL);

exit(1);

}

display(stdout, "Matrix size: %d x %d\n", msiz, msiz, NULL);

/* now read the data */

for (j = 0; j < msiz; j++) {

for (k = 0; k < msiz; k++) {

CATCHEOF(fscanf(file_a, "%f", &next), "A.dat has incorrect format\n");

mat_a[j][k] = next;

CATCHEOF(fscanf(file_b, "%f", &next), "B.dat has incorrect format\n");

mat_b[j][k] = next;

}

if (msiz < asiz || msiz < bsiz) {

while (getc(file_a) != '\n');

while (getc(file_b) != '\n');

}

}

CATCHSYSERROR(fclose(file_a), "Unable to close A.dat");

CATCHSYSERROR(fclose(file_b), "Unable to close B.dat");

/* end of file reading */

#ifdef DISPLAY

display(stdout, "Ready to begin, press [enter] to begin, or [ctrl-c] to quit\n", NULL, NULL, NULL);

getchar();

#endif

a_row -= npe;/* set initial indexes */

b_col -= npe;

nburst=msiz/npe;/* speed improvement */

sema_wait(&CUSynchControl);/* wait for initial PE ready signal */

for (i=0;i<msiz;i++) {

display(stdout,"CU: *************************************Pass %d\n",i+1,NULL,NULL);

for (burst=0;burst<nburst;burst++) {

a_row = (a_row + npe) % msiz;

b_col = (b_col + npe) % msiz;

if(a_row == 0)

++b_col;

display(stdout,"CU: <--------->Burst %d\n",burst+1,NULL,NULL);

mutex_lock(&burstSynchMutex);

if(i==msiz-1 && burst==msiz/npe-1)

work = 0; /* STOP instruction */

cond_broadcast(&burstSynchControl);

mutex_unlock(&burstSynchMutex);

/*Synch barrier, which will be skipped when

work = 0 */

if (work)

sema_wait(&CUSynchControl);

}

}

/* wait for the threads to terminate and return

their status. */

while(thr_join(NULL,NULL,&status) == 0) {

display(stdout,"CU: *** PE %d has terminated ***\n", (int)status, NULL , NULL);

}

}

void *ePEn (void *param) {

int c=(u_int)param;

int i,mat_row, mat_col;

while(work) {

mutex_lock(&burstSynchMutex);

signalCU();

cond_wait(&burstSynchControl,&burstSynchMutex);/*wait for broadcast from CU*/

mutex_unlock(&burstSynchMutex);

mat_row = (a_row + c) % msiz;

mat_col = (b_col + c) % msiz;

for (i = 0; i < msiz; ++i) {

mat_c[mat_row][mat_col] += mat_a[mat_row][i]* mat_b[i][mat_col];

}

display_result(stdout,"PE %d finished: C[%d][%d] = %f\n", param, mat_row, mat_col,

mat_c[mat_row][mat_col]);

}

/* All done, exit and return the PE's id */

thr_exit(param);

}

void *signalCU() {

static mutex_t lock;/*make sure only one PE signals at a time*/

static int counter=0;

mutex_lock(&lock);

counter++;

if (counter==npe) {

counter=0;

sema_post(&CUSynchControl);/*green light for CU*/

}

mutex_unlock(&lock);

}

/*----------------------------------------------

Module: mproc.h

Version: 4.1

Qualifier: mproc

Type: ANSI-C

Delta Date: 10/21/02

Retrieval Date: 10/21/02

Masterfile:

/a/csie/h/users/u1954157/CEG4393/group23_projI_MMII/s.mproc.h

----------------------------------------------*/

#include <sys/sysinfo.h>

#include <time.h>

#include <stdio.h>

#include <sched.h>

#include <thread.h>

#include <errno.h>

#include <synch.h>

extern int errno;

#define N_THREADS 4

#define MAXSIZ 1024

#define MINSIZ 4

#define MAXPE 64

#ifdef DISPLAY

#define display(stream,fstring,arg1,arg2,arg3)\

mutex_lock(&output_stream);\

fprintf(stream,fstring,arg1,arg2,arg3);\

mutex_unlock(&output_stream);

#define display_result(stream, fstring, arg1, arg2, arg3, arg4)\

mutex_lock(&output_stream);\

fprintf(stream, fstring, arg1, arg2, arg3, arg4);\

mutex_unlock(&output_stream);

#else

#define display(stream,fstring,arg1,arg2,arg3)

#define display_result(stream, fstring, arg1, arg2, arg3, arg4)

#endif

#define CATCHSYSERROR(fct,errmsg)\

if(fct != 0) {\

perror(errmsg);\

exit(errno);\

}

#define CATCHEOF(fct, errmsg) \

if (fct == EOF) { \

display(stderr, errmsg, NULL, NULL, NULL); \

exit(1); \

}

#define CATCHFOPENERROR(file_ptr, errmsg) \

if (file_ptr == NULL) { \

perror(errmsg); \

exit(errno);\

}

#define PGMINFO() \

display(stdout, "Version: 4.1\n", NULL, NULL, NULL); \

display(stderr, "Version: 4.1\n", NULL, NULL, NULL); \

display(stdout, "Last compiled on: %s %s\n", __DATE__, __TIME__, NULL); \

display(stderr, "Last compiled on: %s %s\n", __DATE__, __TIME__, NULL); \

current_time = time(NULL); \

display(stdout, "Current date/time is: %s", asctime(localtime(&current_time)), NULL, NULL); \

display(stderr, "Current date/time is: %s", asctime(localtime(&current_time)), NULL, NULL); \

display(stdout, "Invoking file is %s\n", argv[0], NULL, NULL); \

display(stderr, "Invoking file is %s\n", argv[0], NULL, NULL); \

CATCHSYSERROR(gethostname(hostname, sizeof(hostname)), "Could not get hostname"); \

display(stdout, "Hostname: %s\n", hostname, NULL, NULL); \

display(stderr, "Hostname: %s\n", hostname, NULL, NULL); \

display(stdout, "|-------------------------------|\n\n", NULL,NULL,NULL); \

display(stderr, "|-------------------------------|\n\n", NULL,NULL,NULL);

References

Hwang, Kai, "Advances Computer Architecture: Parallelism, Scalability, Programmability", McGraw-Hill, New York, 1993