Introduction

If you are going to make your application portable it is necessary to isolate system dependent code in few modules. In this case migration to new platform/operating system will require only changing these system dependent modules and not affecting other application code. This library provides abstraction of operating system services to application, so the application can use the same calls under Unix and Windows systems for example.

Current version of SAL library provides abstraction of three subsystems: multitasking, file IO and socket IO. Implementations of this interfaces for Windows 95/98/NT and various versions of Unix are currently available. This library was primary developed for GOODS project (multiplatform distributed Generic Object Orient Database management System) and proved to be effective solution for development of portable application.

Depending on requirements to the subsystem, three different approaches are used for for encapsulation of system dependent implementation for these interfaces:

SubsystemApproach
MultitaskingConditional inheritance
File IODifferent method implementations
Socket IOAbstract class with several implementation classes

Multitasking

Modern applications are used to have several threads of control - do some work in parallel. Multithreaded model of application proved to be the most efficient and convenient way for writing concurrent applications (comparing with old schemes, like signals, ASTs, interprocess communication). Multithreaded model is especially good for client/server model where one server has to serve several clients at the same time.

Abstraction of thread is represented in SAL by class task (name thread was not used to avoid name conflict). Except task class, multitasking subsystem provides set of synchronization classes, which make possible to synchronize execution of concurrent tasks.

Multitasking interface requires high effective inline implementations for critical sections (since this operations are very frequently used in multithreaded applications). That is why conditional inheritance was used for the classes representing synchronization primitives. Each such class XXX is derived from correspondent XXX_internals class, which methods are invoked by inline methods of interface class XXX. Implementation of internal classes depends on the system and is taken from one of the available header files. Currently three different implementations of multitasking subsystem are provided:

  1. Portable cooperative multitasking implementation based on C setjmp()/longjmp() functions
  2. Multitasking implementation based on Posix threads (available in most modern Unix systems)
  3. Implementation for Windows family based on Win32 interface
Portable implementation provides cooperative model of multitasking (rescheduling of tasks should be done explicitly). Multitasking model of two other implementations depends on type of multitasking supported by concrete operating system for system threads. In most cases it is preemptive model of multitasking - processor can be rescheduled at any moment of time. Portable implementation provides asynchronous operations with sockets, but not with files (so all read/write file operations are synchronous and blocks all tasks within process). But as far as all control of multitasking in portable version is done at user library level and requires no system calls, at single processor platform it provides the highest performance comparing with implementation using kernel threads. Most of implementations of Posix threads use two level model combining user and kernel threads. In this case arbitrary number of user threads can be implemented on base of pool of several kernel threads. Kernel is responsible for scheduling of kernel threads, while pthread library performs scheduling of user threads between these kernel threads. But, for example at Digital Unix, experiments show that with single CPU, portable multitasking implementation provides significant benefit in performance, comparing with pthreads implementation.

task


static task* create(fptr f, void* arg = NULL, priority pri = pri_normal, 
 	            size_t stack_size = normal_stack); 
Create new task. Pointer to task object, returned by this function, can be used only for task identification.
Parameters
f - pointer to function to be executed in new task.
arg - argument which should be passed to the function
priority - one of the following task priorities: pri_background, pri_low, pri_normal, pri_high, pri_realtime.
stack_size - space reserved for task stack. You can specify any value not smaller than min_stack. Also the following symbolic links are provided:

NameValue
min_stack 8 Kb
small_stack 16 Kb
normal_stack 64 Kb
big_stack 256 Kb
huge_stack 1024 Kb

Returns
Task identifier, which can be used to compare with identifier of current task returned by task::current() method

static void initialize(size_t main_stack_size = normal_stack);
Initialize multitasking library and create initial task. Invocation of this method should be the first statement of main() procedure. Using multitasking library without initialization can cause unpredictable behavior.
Parameters
main_stack_size - specify stack size reserved for main task. This parameter is used only by portable multitasking implementation.

static void reschedule();
Force task rescheduling. It is necessary to call this method only with cooperative multitasking scheme (portable implementation). But even with cooperative multitasking there are only few situations when programmer has to call this function.

static void sleep(time_t sec);
Suspend execution of current task for specified period of time.
Parameters
sec - value of timeout in seconds

static void exit();
Exit current task. Task is terminated when either exit() method is called or return from task function is done.

static void current();
Get identifier of the current task.
Returns
Identifier of the current task.

mutex

Mutex is basic synchronization primitive providing mutual exclusion of several concurrent tasks. Only one task can own the mutex each moment of time. So mutexes can be used to guard critical section from concurrent access.


void enter();
Make the current task an owner of the mutex. If some other task owns the mutex at this moment, calling task will wait until first task release the mutex. The task can invoke enter method several times and mutex will be released only after correspondent number of leave invocations.

void leave();
Release the mutex if number of enter() invocations becomes equal to the number of leave() invocations.

critical_section

This class provides convenient way of protecting block of code (critical section) from concurrent access. If automatic object (local variable) of this class is created on stack as first statement of the block, constructor of this object will lock attached mutex object and destructor of the object will release it after exiting from the block (normal or as a result of exception).


critical_section(mutex& guard);
Lock specified mutex object.
Parameters
guard - mutex used to synchronize access to this critical section

semaphore

This class provides implementation of classical Dijkstra semaphore with wait and signal operations. Semaphore usually are used to manage access to some fixed amount of resource.


void wait();
Block current task until semaphore counter becomes non-zero. Then conunter is decremented by 1 and execution of the task will continue.

boolean wait_with_timeout(time_t sec) { 
Block current task until semaphore counter becomes non-zero or specified timeout is expired.
Parameters
sec - value of timeout in seconds. If zero timeout parameter is specified the method will return immediately.
Returns
True if value of counter is non-zero, False if timeout is expired before semaphore is signaled.

void signal();
This method increments by one semaphore counter. If there are one or more tasks waiting for this semaphore, exactly one of them (task waiting the longest time) will be awaken.

semaphorex

If process in critical section (owning some mutex) has to wait for semaphore, it should first release the mutex, wait until semaphore will be signaled and then reestablish ownership on the mutex. The problem is that in the interval after releasing mutex and sleeping at semaphore or after task awakening and locking mutex, another process can enter critical section and use the resource, which was indented for this task. To avoid such situation, special kind of semaphore was proposed: semaphore guarded by mutex. All operations with such semaphore should be done with the associated mutex locked. When a task sleeps at semaphore, it unlocks guarded mutex, and when the task is awaken by signaling semaphore - it reestablish ownership of the guard mutex. These operation are done atomically: it means that no other task can lock this mutex between the moment of task awakening and reentering the critical section.

This class has the same methods as semaphore class. Result of executing method of this class with unlocked guard mutex or with mutex locked more than once (nested locks) is unpredictable. Portable multitasking library is able to catch such errors by assert statement, but the same is not true for implementation based on OS threads. This class provides the same model of accessing conditional variables as used in Posix threads.


semaphores(mutex& guard);
Associate mutex with semaphore object.
Parameters
guard - mutex to be used as semaphore guard.

event

Event is simple synchronization object similar with conditional variable or event flag. It can be set in signaled state and will be left in this state until it is explicitly reset (in Win32 such object is called event with manual reset).


void wait();
If event is not in signaled state, then block current task until some other task will signal the event.

boolean wait_with_timeout(time_t sec) { 
Wait within specified period of time until event will be signaled,
Parameters
sec - value of timeout in seconds. If zero timeout parameter is specified the method will return immediately.
Returns
True if event is signaled, False if timeout is expired before event is set to signaled state.

void signal();
Set event to the signaled state, All tasks waiting for this event are awaken. The event will remain in signal state until reset() method will be called.

void reset();
Reset event to non-signaled state.

eventex

Event guarded by mutex. Method wait() atomically releases mutex and blocks calling task until event will be signaled. After return the mutex has been locked and is owned by the current task. This class has the same methods as event class. Result of executing method of this class with unlocked guard mutex or with mutex locked more than once (nested locks) is unpredictable.


eventex(mutex& guard);
Associate mutex with event object.
Parameters
guard - mutex to be used as event guard.

fifo_queue

Template class fifo_queue can be used as communication buffer between two or more tasks. Such models of intertask communications as producer/consumers and channels can be implemented by means of this class. This class uses cyclic buffer which size is determined at the moment of object creation.


fifo_queue(size_t size);
Construct queue with specified size of cyclic buffer.
Parameters
size - cyclic buffer size. When size elements are put into the queue, it become full and any other attempt to add element to the queue will block the calling task.

boolean is_empty() const;
Checks if there are available elements in queue.
Returns
True if there are no elements in queue; False otherwise.

boolean is_full() const;
Checks if more elements can be placed in queue.
Returns
True if cyclic buffer is full (no more elements can be placed in the queue until some elements were taken from it); False otherwise.

fifo_queue& operator << (T const& elem);
Put new element is queue. This method will block current task if queue is full (if there is no free space in cyclic buffer).
Parameters
elem - element to be inserted in queue.
Returns
Reference to this object to make it possible to use chain of << operations.

fifo_queue& operator >> (T& elem);
Get element from the queue. This method will block current task if queue is empty (if there are no elements in queue).
Parameters
elem - reference to location where extracted element should be placed.
Returns
Reference to this object to make it possible to use chain of >> operations.

barrier

A barrier class allows a set of tasks to sync up at some point in their code. It is initialized to the number of tasks to be using it, then it blocks all tasks calling it until it reaches zero, at which point it unblocks them all. The idea is that you can now arrange for a set of tasks to stop when they get to some predefined point in their computation and wait for all others to catch up. If you have eight tasks, you initialize the barrier to eight. Then, as each task reaches that point, it decrements the barrier, and hence goes to sleep. When the last task arrives, it decrement the barrier to zero, and they all unblock and proceed.


void reset(int n);
Initialize the barrier object.
Parameters
n - number of tasks using this barrier for synchronization.

void reach();
Decrement barrier value. If barrier value is positive after decrement operation, then block calling task otherwise unblock all tasks reached the barrier.

Debugging multitasking applications

Debugging of multitasking applications is very difficult task because non-deterministic program behavior and presence of several concurrent threads of control. A number of things can go wrong when you try to coordinate the interactions of concurrent tasks. The most popular bugs are race condition when you forget to synchronize access to common variables, and deadlocks when two or more tasks mutually lock each other.

Most of the systems supporting multithreaded model have debugger which is able to deal with threads (suspend/resume threads, switch between threads, show thread context). As far as cooperative multitasking provided by SAL is not visible for the system and debugger, a number of special functions were implemented to make it possible to debug such applications using standard debugger (debugger should support evaluation of user functions). The following section describes these functions and should be read only if you are going to use portable multitasking implementation. My experiments with this library shows that is more convenient to start debugging of the application with cooperative multitasking and then switch to preemptive multitasking.

To enable debugging of application using cooperative multitasking provided by SAL, you should compile SAL sources with CTASK_DEBUGGING_SUPPORT macro defined. As far as defining this name adds very small runtime overhead, it is defined by default. If this macro is defined three functions will be available in SAL library, which can be used for analyzing state of multitasking program (unfortunately it is not possible to continue execution after such analysis).


void debug_get_number_of_tasks();
Get number of tasks in application.
Returns
The number of tasks in application (active, waiting or sleeping).

void debug_catch_task_activation();
You should set breakpoint to this functions to see context of the task. Idea of debugging multitasking applications by standard C debugger is very simple: function debug_select_task(int) is used to switch context to specified task, and breakpoint in debug_catch_task_activation() make it possible to programmer to see this context.

void debug_select_task(int i);
Switch context to specified task. Total number of active tasks in process can be obtained by debug_get_number_of_tasks() function. Index of task passed as parameter to this function should be positive number less than value returned by debug_get_number_of_tasks(). Task with index 0 refers to the task, which was running before debugger stops the application, and it can not be activated with this function (so always investigate current context before switching to other tasks). After activation of the specified task, control is passed to the function debug_catch_task_activation() and then execution of the task continues.
Parameter
i - index of the task, should be in range [1..number-of-tasks)
Returns
-1 if specified number is greater or equal to the number of tasks in the application or less than 1. Otherwise control from this function will not return (longjmp).

Files

SAL library provides three different abstractions of files:

  1. Abstraction of normal file
  2. Abstraction of mapped on memory file
  3. Abstraction of file consisting of several physical segments

Because of presence of this abstract class hierarchy (all this classes are derived from abstract class file), it is not convenient to use inheritance to provide system dependent implementations. Instead of this several system dependent implementations of file class methods are provided and they are placed in two system dependent modules unifile.cxx and winfile.cxx. This is possible because structure of the file class is almost the same for all systems, we need only to describe system dependent handle type.

os_file

Class os_file provides standard set of methods for accessing operating system file.


os_file(const char* name);
Create file object with specified name.
Parameters
name - name of the file

iop_status read(void* buf, size_t size);
Read specified number of bytes from the current position in the file. File pointer is then incremented by number of really read bytes.
Parameter
buf - buffer to hold read data. The buffer size should be not less than size.
size - number of bytes to read
Returns
file::ok if operation successfully completed;
end_of_file if there are less than size bytes between current position and end of file;
any other system dependent code if operation failed.

iop_status read(fposi_t pos, void* buf, size_t size);
Read specified number of bytes from the specified position in the file. Position of file pointer is not defined after this operation.
Parameter
pos - position in the file
buf - buffer to hold read data. The buffer size should be not less than size.
size - number of bytes to read
Returns
file::ok if operation successfully completed;
end_of_file if there are less than size bytes between specified position and end of file;
any other system dependent code if operation failed.

iop_status write(void const* buf, size_t size);
Write specified number of bytes to the current position in the file. File pointer is then incremented by number of really written bytes,
Parameter
buf - buffer with data to be written.
size - number of bytes to write
Returns
file::ok if operation successfully completed;
end_of_file if number of bytes really written is less than size bytes;
any other system dependent code if operation failed.

iop_status write(fposi_t pos, void const* buf, size_t size);
Write specified number of bytes to the specified position in the file. Position of file pointer is not defined after this operation.
Parameter
buf - buffer with data to be written.
size - number of bytes to write
Returns
file::ok if operation successfully completed;
end_of_file if number of bytes really written is less than size bytes;
any other system dependent code if operation failed.

iop_status set_position(fposi_t pos);
Set current position in the file.
Parameter
pos - position in the file
Returns
file::ok if operation successfully completed;
any other system dependent code if operation failed.

iop_status get_position(fposi_t& pos);
Get current position in the file.
Parameter
pos - reference to variable to hold current position in the file
Returns
file::ok if operation successfully completed;
any other system dependent code if operation failed.

iop_status flush();
Flush all modified file data on disk. After successful completion of this method file data in system cache is guaranteed to be synchronized with contents of the disk.
Returns
file::ok if operation successfully completed;
any other system dependent code if operation failed.

iop_status open(access_mode mode, int flags);
Open the file in specified mode.
Parameter
mode - one of fa_read, fa_write, fa_readwrite
flags - combination of 0 or more flags:

FlagDescription
fo_truncatereset length of file to 0
fo_createcreate file if not existed
fo_sync wait completion of write operations
fo_randomoptimize file for random access
fo_exclusiveprevent file from opening by another process
fo_sharedprevent concurrent write access to the file

Returns
file::ok if operation successfully completed;
file::lock_error if fo_exclusive or fo_shared flags are specified and file was opened by some other process in incompatible mode;
any other system dependent code if operation failed.

iop_status close();
Close the file.
Returns
file::ok if operation successfully completed;
any other system dependent code if operation failed.

char const* get_name();
Get file name.
Returns
Name of the file.

iop_status set_name(char const* new_name);
Rename the file.
Parameters
new_name - new name for the file
Returns
file::ok if operation successfully completed;
any other system dependent code if operation failed.

iop_status  get_size(fsize_t& size);
Get file size.
Parameters
size - reference of variable to hold file size.
Returns
file::ok if operation successfully completed;
any other system dependent code if operation failed.

iop_status set_size(fsize_t new_size);
Change file size.
Parameters
new_size - new file size
Returns
file::ok if operation successfully completed;
any other system dependent code if operation failed.

void get_error_text(iop_status code, char* buf, size_t buf_size)
Get error message text for specified error code.
Parameters
code - error code returned by some other file operation.
buf - buffer to receive text of the error message
buf_size - size of buffer, no more than buf_size bytes will be placed in the buffer

mmap_file

Mapped on memory file provides direct access to the file data using virtual memory mechanism. That is the most efficient way to access file, because it requires no context switching and data copying. This class provides all the methods from os_file class and has one additional method get_mmap_addr().


mmap_file(const char* name, size_t init_size);
Create mapped on memory file object with specified name.
Parameters
name - name of the file
init_size - initial size of the file. This method will reserve at least init_size bytes of virtual memory and map file on it. If the file size will exceed this value, then file will be reallocated. If the file size is greater than init_size, then this parameter is ignored.

char* get_mmap_addr() const;
Provide information about file mapping.
Returns
Starting address of virtual memory section where file is mapped,

multifile

Class multifile can be used to overcome system limitation for maximal file size or to distribute file space between several disk partitiones. This class implements all the methods described in os_file section.


multifile(int n_segments, segment* segments);
Create file object consisting of several physical segments. Each segment is described by segment structure declared locally in multifile class and containing name and size fields. Only file described in the last segment can be extended.
Parameters
n_segments - number of segments in multifile
segments - pointer to the array of segment descriptions. This array should have n_segments elements.

Sockets

Abstract class socket has several derived classes providing system dependent implementations of socket mechanism. To resolve name conflict this abstract socket class was called socket_t. Concrete object implementing socket is create by static create, accept or connect methods. Access to sockets should be synchronized by mutexes or some other synchronization primitive. Concurrent execution of two read or two write operations can cause unpredictable behavior. But concurrent execution of read and write operation is possible.

Implementations of sockets are mostly based on socket library provided by operating system, but as far as local domain sockets are supported only in Unix, SAL provides very efficient implementation of local sockets for Win32. These local sockets are implemented by Win32 shared memory and semaphore objects and can be used to perform fast communication between processes within one computer. My experiments shows that this implementation of local sockets is about 10 times faster than original socket library provided by Microsoft.


boolean read(void* buf, size_t size);
Read data from socket.
Parameters
buf - buffer to hold received data
buf_size - number of bytes to receive
Returns
True if operation successfully completed, False otherwise.

boolean write(void const* buf, size_t size);
Write data to socket.
Parameters
buf - buffer containing data to send
buf_size - number of bytes to send
Returns
True if operation successfully completed, False otherwise.

socket_t* accept();
Accept new socket. This method is called by server to establish connection with new client. When the client execute connect method and access server's accept port, accept method will create new socket, which can be used for communication with the client. Accept method will block current task until some connection will be established.
Returns
Pointer to new socket or NULL if operation failed.

boolean cancel_accept();
Cancel accept operation. Task blocked in accept call be be awaken and continue execution.
Returns
True if socket was successfully closed, False otherwise.

boolean shutdown();
Shutdown the socket. This function prohibits write and read operation on the socket. All future attempts to read or write data from/to the socket will be refused. But all previously initiated operations are guaranteed to be completed.
Returns
True if operation successfully completed, False otherwise.

boolean close();
Close connection.
Returns
True if operation successfully completed, False otherwise.

static socket_t*  connect(char const* address, 
      		          socket_domain domain = sock_any_domain, 
			  int max_attempts = DEFAULT_CONNECT_MAX_ATTEMPTS,
			  time_t timeout = DEFAULT_RECONNECT_TIMEOUT);
Establish connection with server. This method will do at most max_attempts attempts to connect server, with timeout interval between attempts.
Parameters
address - address of server socket in format "hostname:port"
domain - type of connection. The following values of this parameter are recognized:

DomainDescription
sock_any_domaindomain is chosen automatically
sock_local_domainlocal domain (connection with one host)
sock_global_domaininternet domain

If sock_any_domain is specified, local connection is chosen when either port was omitted in specification of the address or hostname is "localhost", and global connection is used in all other cases.

max_attempts - maximal number of attempts to connect to server
timeout - timeout in seconds between attempts to connect the server
Returns
This method always create new socket object and returns pointer to it. If connection with server was not established, this socket contains error code describing reason of failure. So returned socket should be first checked by is_ok() method.

static socket_t* create_local(char const* address,
			      int listen_queue_size = 
				  DEFAULT_LISTEN_QUEUE_SIZE);

Create and open socket in local domain at the server site.
Parameters
address - address to be assigned to the socket
listen_queue_size - size of listen queue
Returns
This method always create new socket object and returns pointer to it. If socket can not be opened, error code field of returned socket describes the reason of failure. So returned socket should be first checked by is_ok() method.

static socket_t* create_global(char const* address,
			      int listen_queue_size = 
				  DEFAULT_LISTEN_QUEUE_SIZE);

Create and open socket in global (internet) domain at the server site.
Parameters
address - address to be assigned to the socket
listen_queue_size - size of listen queue
Returns
This method always create new socket object and returns pointer to it. If socket can not be opened, error code field of returned socket describes the reason of failure. So returned socket should be first checked by is_ok() method.

boolean is_ok();
Check the status of the last operation with socket.
Returns
True if the last operation completed successfully, False otherwise

void get_error_text(char* buf, size_t buf_size)
Get error message text for the last operation.
Parameters
buf - buffer to receive text of the error message
buf_size - size of buffer, no more than buf_size bytes will be placed in the buffer

Distribution

This product is freeware in is distributed in hope to be useful. I will do my best to fix all reported bugs and extend SAL functionality. Also e-mail support is guaranteed.


Look for new version at my homepage | E-Mail me about bugs and problems