The purpose of this paper is to learn how to use Message Passing Interprocess Communication (IPC) classes with Kylix. In this paper, you will learn what message passing IPCs are available in Linux, what purpose each is best used for, and how to create reusable classes.
In order to avoid the phrase “multiple processes and/or threads”, I will simplify the phrase to “multiple threads”. Unless I specifically say that a certain topic applies only to multiple threads, you can assume that it also applies to multiple processes. I will point out differences whenever they are applicable.
Listed below are several key concepts that need to be understood before diving into the topic at hand. Since these concepts are woven into this entire paper, it is essential that these concepts be clearly understood.
Synchronization allows multiple threads to coordinate the usage of shared resources. It prevents multiple threads from accessing the same resource at the same time. Linux provides the low-level APIs, and Kylix provides a method for creating easy to use classes. Once these classes have encapsulated the IPC functions, more time can be spent on coding the actual solution, rather than the underlying plumbing that is needed to make it all work.
A task is a unit of work that is scheduled by the Linux scheduler. It includes both processes and threads.
Processes have existed from the beginning of the programming era. A process is an executable program that is currently loaded. It can either be running, stopped or blocked. Stopped processes are those are currently being debugged or have received a SIGSTOP, SIGSTP, SIGTTIN, or a SIGTTOU signal. Blocked processes are those that are waiting to be executed. Each process has it’s own instruction pointer, address space, memory space, and other various CPU registers.
The instruction pointer is used to keep track of what the current instruction the program is running. Address space (also known as the text segment) refers to the memory location of where the process’ program code is loaded. Memory space refers to the heap, where memory is dynamically allocated, the stack where local variables are located and the global process data.
Without using an IPC of some kind, a process cannot communicate with another process. The good news is a process cannot directly read or write another process’ memory. This is good news because a process cannot inadvertently corrupt another process. Imagine if a program could begin to write data in a valid working process. The results could be disastrous. At the same time, the bad news is a process cannot directly read or write another process’ memory. When processes need to communicate, they need to use an IPC like pipes or shared memory. Coordinating and using the chosen IPC mechanism requires diligent design and adds additional overhead.
New processes are created by using one of the exec APIs: execl, execlp, execle, execv, and execvp. The definition of these APIs can be found in Libc.pas.
A thread is path of execution in a process. In a normal process, there is only one path of execution – a single threaded program. Each thread has its own instruction pointer and various CPU registers. It shares the same memory space with the process that created the thread. All of the threads in the process share the same global memory, but each thread is allocated a chuck of memory for its stack space.
Threads are sometimes referred to as “Lightweight Processes”. In Linux, they are scheduled in the kernel. At the kernel level, threads are viewed as processes that share the same global memory. This makes it much easier for the kernel to schedule tasks. However, the kernel does not know that a process is a collection of threads.
Linux does not address multi-threaded applications in the same way as other environments. It does not recognize a particular application boundary for a collection of related threads; rather it treats threads and processes similarly. For most applications this myopia is fine. When dealing with multi-processor computers, however, the scheduling module that is currently implemented does not allow for applications to take advantage of the additional processors. The reason for this is that the kernel has no association between two tasks of the same process.
The system call that is usually used to create threads is the pthread_create function. Another API that can be used is the clone function, but it is more complex to use and is not as portable as the pthread_create function. In fact, pthread_create uses the kernel clone function to create a thread. Both functions are located in Libc.pas. Kylix has made it even easier to use by developing the abstract TThread class located in Classes.pas. In addition, applications that descend from the TThread class are cross-platform, as long as they do not use platform specific code.
In Linux, threads can be attached or detached. Use attached threads when they need to be controlled. Valid reasons for using attached threads are: waiting for the thread to finish, the need to get a return value from the thread or to cancel a thread. When a thread is independent, use detached threads. In the current implementation of TThread object, threads are created as attached and are detached, using the pthread_detach function, just before the thread is finished.
Message passing is sending information, or messages between multiple threads. In Linux, messages can be sent by using either Pipes, FIFOs (a UNIX term for named pipes) or System V Message Queues. Other versions of UNIX can also using POSIX Message Queues, however, they are not available at the time of this writing.
Linux has three message passing IPCs available. They are Pipes, FIFOs (aka Named Pipes) and System V Message Queues.
A pipe is a one way communication channel. For bi-directional communication, an additional pipe is needed. Since pipes are not named, they can only be used by processes that are related. They are frequently using in processes that create child processes. (A child process is one that is created from another process). Shell programs like bash or csh create a pipe when they are specified on the command line with the vertical bar symbol (|). The shell creates a pipe and forks a child process. In the parent process, it redirects the standard output to the write side of the pipe and in the child, it redirects standard input to the read side of the pipe.
Note: Two-way pipes do exist in other versions of UNIX like SVR4.
The following table lists the functions available for manipulating pipes:
Creates a one way pipe. Returns two file descriptors for each end of the pipe.
Reads from a pipe or any file descriptor
Writes to a pipe or any file descriptor.
Closes a pipe or any file descriptor
Duplicates a file descriptor, closing the new file descriptor if necessary.
Creates a pipe and starts another process that can read from or write to the pipe.
fgets (and more)
Reads from a pipe or any file stream.
fputs (and more)
Writes to a pipe or any file stream.
Closes a file stream and waits for the process spawned by popen to terminate.
FIFOs (First In, First Out) remove a major limitation of a pipe: it is identified by a name, allowing for unrelated proceses to communicate. The name given to the FIFO needs to be a valid Linux filename and also has file permissions associated with it. Like pipes, FIFOs are a one way communication device.
The following table lists the functions available for creating, opening, reading, writing and deleting FIFOs:
Creates a FIFO
Opens a FIFO as well as normal files.
Reads from a pipe or any file descriptor
Writes to a pipe or any file descriptor.
Closes a pipe or any file descriptor
Deletes a FIFO
A System V Message Queue is a one way communication device with the most flexibility. Unrelated processes can add and remove messages as long as they have the proper permissions and know the name of the message queue. Furthermore, messages can be prioritized, allowing for the removal of messages by priority, not a first in, first out basis. The messages that are sent and received are defined by the applications and not by the functions that manipulate them.
The following table lists the functions available for creating, accessing and deleting System V Message Queues:
System V Message Queue Functions
Creates an IPC key from a filename and integer
Creates or opens a message queue
Puts a message on the queue
Removes a message from the queue
Removes the message queue from the system and other message queue control operations.
The basic operations that are needed for creating a Pipe class are: creating, reading, writing and closing. Two additional methods have been added: PipeWritesToStdOut redirects write side of the pipe to standard ouptut and PipeReadsFromStdIn redirects standard input to the read side of the pipe. In the example, the usage of these additional methods will be shown.
Note: All of the classes are descendants of a class called TIPCBase. This class contains debugging and helper methods for reporting errors.
TPipeBuffer = array of char; TPipeSide = (psRead, psWrite); TPipe = class (TIPCBase) private FHandles : array[0..1] of Integer; public constructor Create; destructor Destroy; override; function Read(var buffer : TPipeBuffer; bufSize : Integer) : boolean; function Write(var buffer : TPipeBuffer; bufSize : Integer) : boolean; function Close(what : TPipeSide) : boolean; function WriteToStdOut : boolean; function ReadFromStdIn : boolean; end;
In the constructor, the pipe is created, and the handles are saved in an array. The read side of the pipe is position zero and the write side is position one.
constructor TPipe.Create; var pptr : PInteger; ret : integer; begin inherited; pptr := @FHandles; ret := pipe(pptr); if ret <> LIBC_SUCCESS then ErrorMessage('pipe'); end;
The destructor closes both handles of the pipe.
destructor TPipe.Destroy; begin __close(FHandles[IDX_READ_PIPE]); __close(FHandles[IDX_WRITE_PIPE]); inherited; end;
Reading and writing to the pipe are accomplished by using the Libc functions __read and __write. These functions retrieve and send information to and from the pipe. After writing the data, the method flushes the buffer so that the data is sent out over the pipe.
function TPipe.Read(var buffer : TPipeBuffer; bufSize : Integer) : boolean; var ret : integer; begin // IDX_READ_PIPE = 0 ret := __read(FHandles[IDX_READ_PIPE],buffer,bufSize); Result := (ret = LIBC_SUCCESS); end; function TPipe.Write(var buffer : TPipeBuffer; bufSize : Integer) : boolean; var ret : integer; begin // IDX_WRITE_PIPE = 1 ret := __write(FHandles[IDX_WRITE_PIPE],buffer,bufSize); // flush the write down the pipe.. fsync(FHandles[IDX_WRITE_PIPE]); Result := (ret = LIBC_SUCCESS); end;
When developing command line tools, it is very common to use what is called a pipeline. A pipeline takes the output of one program and sends it to the input of another program, creating a “pipeline” between the two programs. ReadFromStdIn first closes standard input, then it makes standard input point to the read side of the pipe, thereby redirecting standard input to the pipe.
function TPipe.ReadFromStdIn : boolean; var ret : integer; begin // IDX_READ_PIPE = 0 ret := dup2(FHandles[IDX_READ_PIPE], STDIN_FILENO); if (ret = LIBC_FAILURE) then ErrorMessage('ReadFromStdIn:dup2'); Result := (ret = LIBC_SUCCESS); end;
Similarly, WriteToStdOut first closes standard output, then it makes a copy of the write side of the pipe point to standard output, thereby redirecting the output of the pipe to standard output.
function TPipe.WriteToStdOut : boolean; var ret : integer; begin // IDX_WRITE_PIPE = 1 ret := dup2(FHandles[IDX_WRITE_PIPE], STDOUT_FILENO); if (ret = LIBC_FAILURE) then ErrorMessage('WriteToStdOut:dup2'); Result := (ret = LIBC_SUCCESS); end;
When a specific side of the pipe needs to be closed, use the close method.
function TPipe.Close(what : TPipeSide) : boolean; begin __close(FHandles[ord(what)]); Result := true; end;
Like a pipe, a FIFO has the same basic operations as the Pipe class, namely reading and writing.
TPipeSide = (psRead, psWrite); TNamedPipe = class(TIPCBase) private FOpen : boolean; FFileDesc : integer; FPipeName : string; FPipeSide : TPipeSide; function PipeSideToBits(pipeSide : TPipeSide) : integer; public constructor Create(name : string; pipeSide : TPipeSide; perm : TIPCFilePermissions); destructor Destroy; override; function Read(var buffer : TPipeBuffer; bufSize : Integer) : boolean; function Write(var buffer : TPipeBuffer; bufSize : Integer) : boolean; function Close : boolean; end;
The constructor creates the named pipe with the appropriate permissions, based on which side of the pipe that is opened. The read-side only needs read only permissions, while the write side needs write permissions. After creating the FIFO, it is opened. Any errors raise exceptions.
constructor TNamedPipe.Create(name : string; pipeSide : TPipeSide; perm : TIPCFilePermissions); var ret : integer; begin inherited Create; FOpen := false; // create the pipe ret := mkfifo(PChar(name), FilePermissionsToBits(perm)); // It's okay if the pipe already exists.. if (ret < 0) and (GetLastError <> EEXIST) then ErrorMessage('mkfifo'); FPipeName := name; FPipeSide := pipeSide; // open the pipe FFileDesc := open(PChar(name), PipeSideToBits( FPipeSide ), 0); if FFileDesc <> LIBC_SUCCESS then ErrorMessage('NamedPipe.__open'); FOpen := true; end;
In the destructor, the pipe is closed.
destructor TNamedPipe.Destroy; begin Close; inherited; end;
PipeSideToBits is a private method used in the constructor. Its purpose is to map the side of the pipe with the proper permissions. Reading requires read only permissions, writing requrires write only permissions.
function TNamedPipe.PipeSideToBits(pipeSide : TPipeSide) : integer; begin Result := 0; case pipeSide of psRead : Result := O_RDONLY; psWrite : Result := O_WRONLY; else ErrorMessage('PipeSideToBits: unknown argument'); end; end;
Reading information from the pipe is accomplished by using the Read method while writing data to the pipe is accomplished by using the write method. Both methods take a buffer and the size of the buffer. They return a boolean to indicate success or failure.
function TNamedPipe.Read(var buffer : TPipeBuffer; bufSize : Integer) : boolean; var ret : integer; begin ret := __read(FFileDesc,buffer,bufSize); Result := (ret = LIBC_SUCCESS); end; function TNamedPipe.Write(var buffer : TPipeBuffer; bufSize : Integer) : boolean; var ret : integer; begin ret := __write(FFileDesc,buffer,bufSize); // flush the write down the pipe.. fsync(FFileDesc); Result := (ret = LIBC_SUCCESS); end;
When finished using a FIFO, close it with the Close method. It closes the file descriptor then it attempts to delete the pipe name. It is important to note that the last call to the Close method will actually delete the pipe, since another process or thread still has it opened.
function TNamedPipe.Close : boolean; begin if FOpen then begin __close(FFileDesc); FOpen := false; // we skip error checking because it will fail if another // process still has the file open. When the last process // ends, the pipe will be deleted. DeleteFile(FPipeName); end; Result := true; end;
One of the advantages for using a System V Message Queue is that the messages are programmer defined. Whatever type of information that is needed to be queued can be packaged and be placed it. In this implementation of a System V Message Queue class, blocking can be specified for both reading and writing. This allows a process or thread to either wait for the read or write to finish or continue to do something else and try the read or write again at another time.
Note: A useful Linux command for dealing with System V Message Queues, System V Semaphores and shared memory is ipcs. This command provides IPC information that the system knows about. Another useful command is ipcrm which allows the deletion of the IPC resources listed above. Also, there is a kernel option to either enable and disable System V IPCs.
TMQExampleMsg is only an example of what a message might look like. The only requirement is that the first field must be an integer that indicates the message type. It must be greater than zero. Anything after the first field can be whatever is needed and is not limited to character data types.
TMQExampleMsg = packed record MessageType : integer; // required, > 0 MessageText : array[0..1] of char; // whatever you require end; TMQFilePermission = (fpOwnerRead, fpOwnerWrite, fpGroupRead, fpGroupWrite, fpOtherRead, fpOtherWrite); TMQFilePermissions = set of TMQFilePermission; TSystemVMQ = class(TIPCBase) private FMsgKey : key_t; // key needed for Message Queue FMsgID : integer; // refers to the queue created by msgget FOpened : boolean; FPrivateKey : boolean; FPathName : string; FIPCKey : integer; FPermissions : TMQFilePermissions; FOpenExist : boolean; FBlockRead : boolean; FBlockWrite : boolean; procedure SetIPCKey(const value : integer); function FilePermissionsToBits : integer; protected procedure CheckProperties; public constructor Create; destructor Destroy; override; procedure Open; function Write(buffer : Pointer; MessageLength : integer) : boolean; function Read(buffer : Pointer; buflen : integer; MessageType : integer; var NumRead : integer) : boolean; procedure Close; property PrivateKey : boolean read FPrivateKey write FPrivateKey; property PathName : string read FPathName write FPathName; property IPCKey : integer read FIPCKey write SetIPCKey; property Permissions : TMQFilePermissions read FPermissions write FPermissions; property OpenExisting : boolean read FOpenExist write FOpenExist; property BlockOnRead : boolean read FBlockRead write FBlockRead; property BlockOnWrite : boolean read FBlockWrite write FBlockWrite; end;
The constructor initializes the properties to their default values. Notice that by default, blocking will occur on the reads and writes.
constructor TSystemVMQ.Create; begin inherited; FPrivateKey := false; FOpened := false; FPathName := ''; FIPCKey := 0; FPermissions := ; FOpenExist := false; // default it to create mq if it does not exist. FBlockRead := true; // default is to block FBlockWrite := true; // default is to block FMsgID := 0; end;
FilePermissionsToBits maps the file permissions to the bit values needed for System V Message Queue permissions. Unfortunately, Linux does not define the XXX_MSG_R and XXX_MSG_W bits. They have been defined in SysVMsgQueueConstants unit.
function TSystemVMQ.FilePermissionsToBits : integer; begin Result := 0; if fpOwnerRead in FPermissions then Result := Result or USR_MSG_R; if fpOwnerWrite in FPermissions then Result := Result or USR_MSG_W; if fpGroupRead in FPermissions then Result := Result or GRP_MSG_R; if fpGroupWrite in FPermissions then Result := Result or GRP_MSG_W; if fpOtherRead in FPermissions then Result := Result or WRLD_MSG_R; if fpOtherWrite in FPermissions then Result := Result or WRLD_MSG_W; end;
There are two ways of opening a Message Queue. The first way is to specify a filename and IPC key. Using the ftok function, Linux generates a key that is then used when communicating with the message queue. Another option is to use a private key, which is guaranteed to be unique. Typically, unrelated processes use a filename and IPC key where related processes or threads use a private key. When specifying a filename and IPC key, the filename must exist, so the Open method creates an empty file, if needed.
In the Close method, the message queue is closed and the file is deleted if is no longer in use.
procedure TSystemVMQ.Open; var ret,oflag : integer; tmpFile : file; begin ValidateProperties; if FPrivateKey then FMsgKey := IPC_PRIVATE else begin // make sure the file exists.. if not FileExists(FPathName) then begin // create a file if we can.. Assign(tmpFile,FPathName); Rewrite(tmpFile); CloseFile(tmpFile); end; FMsgKey := ftok(PChar(FPathName),FIPCKey); if FMsgKey = LIBC_FAILURE then ErrorMessage('Unable to ftok message queue'); end; oflag := IPC_CREAT; // if we want to only open it, then mask in the IPC_EXCL bit(s) if FOpenExist then oflag := oflag or IPC_EXCL; // now get the permission bits and add them to oflag oflag := oflag or FilePermissionsToBits; // do the open ret := msgget(FMsgKey, oflag); if (ret = LIBC_FAILURE) then ErrorMessage('msgget failed'); FOpened := true; end; procedure TSystemVMQ.Close; var ret : integer; begin if FOpened then begin ret := msgctl(FMsgID, IPC_RMID, nil); if ret = LIBC_FAILURE then ErrorMessage('Error closing msq queue (msgctl)'); FOpened := false; // clean up the file if we can.. DeleteFile(FPathName); end; end;
Reading and writing to the message queue is more complex then pipes and FIFOs. Reading allows a message to be removed from the queue in order (first in, first out), or by priority. Specifying a message type of zero indicates that the first message (or the oldest) in queue be returned. A message type that is positive will return the first message that matches the specified type, and a negative message type indicates that the first message with the lowest type that is less than or equal to the absolute value of the specified, negative, message type. The NumRead parameter returns the number of bytes read excluding the initial field of message type.
To write a message that is placed on the queue, a buffer and the message length is passed to the Write method. MessageLength parameter is the size of the MESSAGE and not the entire record. Using the TMQExampleMsg for instance, would give us a message length of sizeof(TMQExampleMsg) – sizeof(integer).
function TSystemVMQ.Read(buffer : Pointer; buflen : integer; MessageType : integer; var NumRead : integer) : boolean; var ret,flag : integer; begin if not FOpened then Result := false else begin flag := 0; if not FBlockRead then flag := IPC_NOWAIT; Result := true; ret := msgrcv(FMsgID, buffer, buflen, MessageType, flag); if ret = LIBC_FAILURE then Result := false; end; end; function TSystemVMQ.Write(buffer : Pointer; MessageLength : integer) : boolean; var ret,flag : integer; begin if not FOpened then Result := false; else begin flag := 0; if not FBlockWrite then flag := IPC_NOWAIT; Result := true; ret := msgsnd(FMsgID, buffer, MessageLength, flag); if (ret = LIBC_FAILURE) then Result := false; end; end;
The PipeTest program below demonstrates how to use a pipe to send and receive messages.
program PipeTest; uses SysUtils,Pipes; const PIPE_NAME = '/tmp/fifo.1'; var pipe : TPipe; tmpbuf : string; sendBuf : TPipeBuffer; recvBuf : TPipeBuffer; begin pipe := TPipe.Create; tmpbuf := 'Hello Pipe world!'; SetLength(sendBuf,length(tmpbuf)+1); StrPCopy(PChar(sendBuf),tmpbuf); // write something on one end and see // if we get the same thing on the read side if not pipe.Write(sendBuf, length(sendBuf)) then writeln('Error writing buffer!'); SetLength(recvBuf,length(tmpbuf)+1); // now read something on the other end if not pipe.Read(recvBuf, length(recvBuf)) then writeln('Error reading buffer!'); writeln('Hey I got the following from the pipe ->',StrPas(PChar(recvBuf)),'<--'); pipe.Free; end.
For the FIFO example, there is a server program and a client program. The server side creates a pipe and blocks while waiting for the client to send a message. In the client, the pipe is opened and the message is sent to the server. Then, the server retrieves the message and displays whatever the client sent to it.
// The Fifo Server program program fifoserver; uses SysUtils, Pipes; const PIPE_NAME = '/tmp/fifotest'; var fifo : TNamedPipe; recvBuf : TPipeBuffer; begin writeln('Starting fifo server...'); fifo := TNamedPipe.Create( PIPE_NAME, psRead, [fpOwnerRead, fpOwnerWrite, fpGroupRead, fpGroupWrite]); SetLength(recvBuf,1024); if not fifo.Read(recvBuf, length(recvBuf)) then writeln('Server: Error reading from fifo!') else writeln('Received ->',StrPas(PChar(recvBuf)),'<- from a client'); fifo.Free; writeln('Server is done.'); end. // The Fifo Client program program fifoclient; uses SysUtils, Pipes; const PIPE_NAME = '/tmp/fifotest'; var fifo : TNamedPipe; tmpstr : string; sendBuf : TPipeBuffer; begin writeln('Starting fifo Client..'); fifo := TNamedPipe.Create( PIPE_NAME, psWrite, [fpOwnerRead, fpOwnerWrite, fpGroupRead, fpGroupWrite]); tmpstr := 'Hello from a fifo client!'; SetLength(sendBuf,length(tmpstr)); StrPCopy(PChar(sendBuf),tmpstr); if not fifo.Write(sendBuf, length(sendBuf)) then writeln('Client: error writing to fifo!') else writeln('Message successfully sent!'); fifo.Free; writeln('Client is done.'); end.
To show how to use the System V Message Queue class, the examples below shows two programs. One adds a message to the queue and the other retrieves a message from the queue. In the addmessage program, it places the message on the queue and exits. The getmessage program, removes an item from the queue and displays the contents to the console window. Notice that neither program calls the Close message on the queue, so that the queue is not deleted from the system.
// Adds a message to the queue program addmessage; uses SysUtils, SysVMsgQueue; const MQ_PATH_NAME = '/tmp/msg1'; MAX_CHARS = 80; type TMyMsgQueueRecord = packed record MessageType : integer; MyInteger : integer; MyFloat : double; MyCharStr : array[0..MAX_CHARS] of char; // notice that this is not a string! end; var msg : TSystemVMQ; buf : TMyMsgQueueRecord; begin writeln('Adding a message'); msg := TSystemVMQ.Create; writeln('Setting properties..'); msg.Debug := true; msg.PrivateKey := false; msg.PathName := MQ_PATH_NAME; msg.Permissions := [fpOwnerRead, fpOwnerWrite, fpGroupRead, fpGroupWrite]; msg.OpenExisting := false; // create it if necessary msg.BlockOnWrite := true; writeln('Opening the msg queue..'); msg.Open; // build the "message" buf.MessageType := 1; buf.MyInteger := 7; buf.MyFloat := 1734.58; StrPCopy(buf.MyCharStr,'This is my message to say' + ' hello from addmessage'); writeln('Sending the message..'); if not msg.Write(@buf,sizeof(TMyMsgQueueRecord)-sizeof(integer)) then writeln('error writing message!') else writeln('message sent!'); msg.Free; end. // Retrieves a message from the Queue program getmessage; uses SysUtils, SysVMsgQueue; const MQ_PATH_NAME = '/tmp/msg1'; MAX_CHARS = 80; type TMyMsgQueueRecord = packed record MessageType : integer; MyInteger : integer; MyFloat : double; MyCharStr : array[0..MAX_CHARS] of char; end; var msg : TSystemVMQ; buf : TMyMsgQueueRecord; NumRead : integer; begin writeln('Retrieving a message'); msg := TSystemVMQ.Create; writeln('Setting properties..'); msg.Debug := true; msg.PrivateKey := false; msg.PathName := MQ_PATH_NAME; msg.Permissions := [fpOwnerRead, fpOwnerWrite, fpGroupRead, fpGroupWrite]; msg.OpenExisting := false; // create it if necessary msg.BlockOnRead := true; writeln('Opening the msg queue..'); msg.Open; // clear the message buf.MessageType := 0; buf.MyInteger := 0; buf.MyFloat := 0; StrPLCopy(buf.MyCharStr,'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',MAX_CHARS); writeln('receiving the message..'); if not msg.Read(@buf, sizeof(TMyMsgQueueRecord)-sizeof(integer),1,NumRead) then writeln('error reading message!') else begin writeln('message received! read ',NumRead,' messages'); writeln('contents of message'); writeln('MessageType = ',buf.MessageType,' MyInteger = ', buf.MyInteger,' MyFloat = ',buf.MyFloat, ' MyCharStr = ',buf.MyCharStr); writeln; end; msg.Free; end.
Kylix 2 Development by Eric Whipple and Rick Ross, published by Wordware, ISBN # 1556227744
Understanding POSIX Threads: Programming with POSIX Threads by David R. Butenhof, published by Addison-Wesley, ISBN # 0-201-63392-2.
Interprocess Communication: UNIX Network Programming Interprocess Communications Volume 2, Second Edition by W. Richard Stevens, published by Prentice Hall, ISBN 0-13-081081-9
LINUX Core Kernel Commentary by Scott Maxwell, published by Coriolis, ISBN 1-57610-469-9
Interesting notes on the Linux Kernel and Threads: http://boudicca.tux.org/hypermail/linux-kernel/1998/1998week50/0701.html
POSIX and Unix Threads:: http://www-users.cs.umn.edu/~seetala/ResourceCenter/systems-programming.html
POSIX Threads Explained: http://www-106.ibm.com/developerworks/library/posix1.html?dwzone=linux
Additional online resources can be found at http://rick-ross.com