Pp. 93104 of the Proceedings |
John Wood
Compaq Computer (UK) Ltd
woodjohn@compaq.com
Abstract
Applications that use the Solaris threads application programming interface (API), e.g. thr_create(), mutex_lock(), cond_signal(), etc. [1], are generally non-portable. Thus to port an application that uses Solaris threads to another platform will require some degree of work.
Solaris now supports the POSIX threads API as well as the Solaris threads API. Therefore to make a Solaris threaded application portable, the ideal is to re-code the threaded part of the application to use POSIX threads. However, the Solaris threads API has some unique functionality over the POSIX threads API. This can make the task of converting a Solaris threaded application to use POSIX threads be very time-consuming and hence expensive, sometimes prohibitively so.
This paper outlines
an alternative approach to porting applications that use the Solaris threads
API, which is to use an open-source Solaris-compatible threads library
that layers upon a POSIX threads library. The objective is to allow an
otherwise-portable Solaris threaded application to be ported by simply
rebuilding on the target platform using the Solaris-compatible threads
library and header-files. This reduces the cost of porting the application.
To port an application that uses the Solaris threads API to another UNIX platform typically requires re-working the application to use the POSIX threads API. Depending upon the application, the amount of re-working needed may vary from simply being a matter of a few editor substitutions, to re-engineering the application.
An alternative to requiring
every Solaris threads application to be re-worked for portability is to
provide a portable Solaris-compatible threads library. Once the Solaris-compatible
threads library has been ported to a target platform, threaded Solaris
applications may be ported by just re-compiling [F3].
This paper describes a Solaris-compatible threads library, for which the
acronym STL is used. An ISV porting a threaded Solaris application may
choose not to use STL, but may find the details of the STL implementation
useful to re-work their application for portability.
SCL v1.1 was released in April 2000, and is available for free download. SCL v1.1 provides:
In many cases the Solaris threads API and POSIX threads API are almost identical. For example, consider the kill function that sends a signal to another thread:
Solaris threads API:
int thr_kill(POSIX threads API:
thread_t target_thread_id,
int sig );
int pthread_kill(Both functions take the same parameters of a thread identifier and signal number, although the thread identifier type is different. Both functions return an integer value, which if 0 indicates success.
pthread_t target_thread_id,
int sig );
In essence, to implement thr_kill(), STL first defines the Solaris type thread_t to match the POSIX threads type pthread_t, and then thr_kill() just becomes a jacket routine to pthread_kill().
Mapping the Solaris threads types directly onto their POSIX threads equivalents potentially allows an application to mix Solaris thread API calls with POSIX thread API calls. However, STL does not currently support this. To support mixing would require STL to intercept application calls to some of the POSIX threads functions for thread management, such as:
For most Solaris threads functions, the STL jacket routines have to perform extra work to ensure compatibility. Two examples are given later in this section.
There are a number of areas where Solaris threads provides functionality that is not available within POSIX threads. The main areas are:
For details of STL
functionality and restrictions, see the SCL Users Guide [6].
Specifically, see section 3.2: STL Functionality; Appendix A: Mapping
of Solaris thread types to POSIX thread types by STL, and Appendix
B: Solaris thread functions implemented by STL.
For example, consider the Solaris threads and Tru64 UNIX POSIX threads functions to perform a timed-wait on a condition variable. The APIs are:
Solaris threads API:
int cond_timedwait(Function return values documented on Solaris:
cond_t *cvp,
mutex_t *mp,
timestruc_t *abstime );
int pthread_cond_timedwait(Function return values documented on Tru64 UNIX:
pthread_cond_t *cond,
pthread_mutex_t *mutex,
const struct timespec *abstime );
If the STL implementation of cond_timedwait() receives an ENOMEM return value from calling pthread_cond_timedwait(), then it maps this value to EFAULT, which is one of those documented for the Solaris function. This is because Solaris applications might only check for specific error codes, rather than just testing the status for success. Additionally, STL logs a message to indicate when it is performing a mapping of error statuses (e.g. ENOMEM from pthread_cond_timedwait() mapped to EFAULT from cond_timed_wait() ): these messages may be helpful to understanding the real reason for a particular STL function return value.
Note that a message is only logged when the POSIX threads functionās return value is mapped to a different return value for the Solaris threads routine. For example, with cond_timedwait():
POSIX threads documents and returns EINVAL when uninitialized objects are used as parameters.
There is no portable
way to validate that a POSIX threads synchronization object has been initialized,
so STL does not support the Solaris observed behaviour of implicitly initializing
uninitialized synchronization objects. Attempts to use an uninitialized
object with STL functions results in an error value being returned, and
typically an error-mapping message is logged. But there is an exception
for statically-initialized-to-zero objects: STL tests for these, and will
explicitly initialize them, to conform to the documented Solaris behaviour.
These checks will impact performance, but compatibility is the main objective.
POSIX threads does not support daemon threads. A POSIX threads process terminates when its last thread exits.
Daemon threads could
be used by applications for housekeeping tasks. For example, a daemon thread
may be created that periodically monitors disk space whilst the application
is running.
The daemon-thread implementation sounds simple in outline, but now we look at the implementation in more detail. It requires:
When STL initializes, it creates a thread-specific data key STL.tsd_key, and associates a destructor routine, stl_tsd_key_destructor(), with that key. When a thread that has data associated with STL.tsd_key terminates, it runs the stl_tsd_key_destructor() routine [F8].
When a new thread is created by calling thr_create(), the STL implementation of thr_create() dynamically allocates some memory M for a stl_tsd_t data structure, and fills in its fields. The stl_tsd_t structure has fields for:
When the new thread starts, it first executes stl_thread_start_rtn( M ). Within this routine the thread makes the memory M into thread-specific data for this thread by calling pthread_setspecific( STL.tsd_key, M ). The new thread also determines from the thread attribute flags in M if it is a daemon thread, and if not then the count of non-daemon threads is incremented.
The new thread then calls the user-specified start-routine with the user-specified argument, which are both extracted from M.
When the new thread
terminates, it automatically executes the stl_tsd_key_destructor()
routine. Ultimately, this frees up the memory M,
but first it extracts the thread attribute-flags from M,
and determines from the flags whether this thread is a daemon thread or
not. If the thread is not a daemon, then the count of non-daemon threads
is decremented; and if this count is now zero, then process is terminated
by calling exit().
For STL to support
the mixing of Solaris threads and POSIX threads calls, which Solaris allows,
it would need to intercept calls to pthread_create()to
increment the non-daemon thread count.
The list of current threads is currently implemented as an unsorted linked list. The STL thread-specific data-structure stl_tsd_t is extended to make it be an element of the linked-list by adding pointers to the next and previous entries in the list. These two pointers make it quicker for a thread to unlink itself from the list. The stl_tsd_t structure is also extended to include the thread identifier tid of that thread, and a mutex for that thread. If one thread wants to get or set the attributes of another thread, then it must lock the mutex of the target thread.
A global pointer, STL.lists.current_threads_list_head, points to the head of the list of current threads. New thread-list entries are added to the head of the list. The list will never be empty, except when the last thread of a process is terminating.
Access to the list is coordinated by a read/write lock, STL.lists.current_threads_list_lock. The read/write lock gives better concurrency than a mutex, because write-access to the list is only required when a thread is being created (added to the list) or terminating (removed from the list).
Figure 1 illustrates
the current-threads list.
Only the thread itself
can add itself to the list of current threads, or remove itself from the
list of current threads. This has implications for the locking assumptions.
A new thread adds itself to the current-threads list by executing code
within the stl_thread_start_rtn()
routine. A thread removes itself from the list when it terminates by executing
code within the stl_tsd_key_destructor()
routine.
The STL implementation currently uses a linear search to locate the stl_tsd_t entry of a specific thread. This may have poor-performance implications when the number of current-threads is large, but this is considered acceptable based on the premise that it is not that often that one thread modifies the stl_tsd_t entry of another thread. Usually a thread will modify its own stl_tsd_t entry, which it finds quickly by calling pthread_getspecific(). Thus to implement a function like thr_setprio( tid, pri ) the sequence of events is:
Compare the Solaris threads and POSIX threads APIs for joining a thread:
Solaris threads API:
int thr_join(POSIX threads API:
thread_t tid,
thread_t *ret_tid,
void **ret_val );
int pthread_join(
pthread_t tid,
void **ret_val );
Both the Solaris
threads and POSIX threads APIs let you join with a specific thread identified
by tid.
In addition, if you specify a thread identifier tid
of 0 on Solaris, thr_join() will
join with any terminated non-detached thread that has not yet been joined,
and will return the identifier of the joined thread in ret_tid.
If there are no non-detached terminated threads waiting to be joined, then
thr_join()
with a tid
of 0 will wait for the first such thread to terminate, and will join with
it. This unique Solaris functionality is called join-any-thread.
Note that when a joinable
thread terminates, there may be zero, one or many threads waiting to join
that specific thread, as well as other threads waiting to join any thread.
Solaris does not define the behaviour for this situation, but STL gives
preference to the thread(s) waiting to join the specific thread over the
threads waiting to join an unspecified thread.
int thr_suspend( thread_t tid );Threads can also be created in a suspended state by setting the flags parameter to thr_create().int thr_continue( thread_t tid );
POSIX threads does
not support the suspend and continue operations on a thread.
It is worth noting that in threaded programs, a signal handler is process wide (i.e. the same for all threads), and the signal mask of blocked signals is thread-specific (i.e. can vary per thread).
In the book Programming with POSIX Threads [7], there is an example implementation of the thread-suspend and thread-continue routines, which uses two signals for suspend and continue respectively. This forms the basis of the STL implementation of thr_suspend() and thr_continue(), with a few modifications.
In essence, STL implements thr_suspend() by calling pthread_kill() to send a suspend signal to the target thread. The signal handler for the suspend signal then calls sigwait() to block until it receives a continue signal. sigwait() is one of the few functions that can be safely called from within a signal handler [F11].
A suspended thread is resumed by another thread calling thr_continue(). This function is also implemented by calling pthread_kill(), but sends a continue signal to the target thread. The signal handler for the continue signal is a null routine. Upon receipt of the continue signal the target thread returns from both the continue and suspend signal handlers to resume whatever it was doing.
By default on Tru64 UNIX, STL uses SIGUSR1 as the suspend signal, and SIGUSR2 as the continue signal. However, the signal numbers used by STL can be changed by setting environment variables: see the SCL Users Guide [6].
Solaris documents that
thr_suspend()
does not return until the target thread is suspended. In other words, the
thread executing thr_suspend()
needs to know that the target thread has received the suspend signal. (A
thread may have temporarily blocked a set of signals, in which case the
suspend signal is pending until the thread unblocks that signal). Thus
within the suspend signal handler routine, the thread being suspended needs
to indicate to the caller of thr_suspend()
that it is now suspended. A global semaphore is used for this purpose.
The suspended thread calls sem_post(),
which the thread executing thr_suspend()
waits upon by calling sem_wait().
sem_post()
is another function that can safely be called from within a signal-handler.
It is hoped that the
Linux version of STL will be complete and available by the time of the
USENIX Annual Technical Conference in June 2001.
The GNU C compiler is being used on Linux, with the -ansi and -Wall switches. This has flagged several warnings, which is to be expected given that STL had never been ported before. The code has been changed and is now more portable.
When considering the Linux port of STL there was concern about LinuxThreads [8], the POSIX threads library on Linux. LinuxThreads implements POSIX threads by creating separate processes with the clone() system call. But the concern seems unfounded: the only real problem encountered so far with LinuxThreads is that threads within the same (logical) process actually have different process identifiers.
Initially it was thought that LinuxThreads did not have read/write lock extensions to POSIX threads. The reasons for thinking this were:
_XOPEN_SOURCE=500before including the <pthread.h> header-file, in addition to defining:
_POSIX_C_SOURCE=199506LThese explicit definitions are not necessary on Tru64 UNIX.
Two other problems have been encountered during the Linux port. The first problem was trying to get the shared object library to run an initialization routine. This is achieved by specifying the
__attribute__ (( __constructor__ ))directive, but you must use cc as the link driver, rather than ld, for this directive to be recognized. The other problem was with message catalogs, and the gencat utility in particular. On Linux gencat is white-space sensitive, in accordance with SUSv2. Thus with non-conformant input (that happened to work on Tru64 UNIX), gencat on Linux produced blank messages. The solution was to edit the input to gencat to be conformant.
Table 1 shows how STL
affects the performance in a couple of simple tests, where a test was coded
in both Solaris threads and POSIX threads. The tests were performed using
STL v1.1 on a Tru64 UNIX v5.1 system (a Compaq Alphaserver ES40 with 4
CPUs, but with the number of CPUs active varied from 1 through to 4).
Table 1. Relative
performance of POSIX threads and STL threads for simple tests.
|
|
|
|
|
|
For example, for the mutex-locking test loop, just a single thread is executing, so that there is no contention for the mutex. Consequently the fastest path is taken through the POSIX mutex-locking code. In real applications there will probably be some contention for the mutex, which may result in a locking-thread having to do extra work to block on an already-locked mutex, and the unlocking thread also having to do extra work to wake up the blocked thread. This extra work will make the overhead of STL be less apparent.
For the thread-create-and-join loop using STL, a considerable amount of CPU time was observed being spent in system-mode, compared to using native POSIX threads. This indicates a high number of system calls, the most probable cause being the calls to pthread_sigmask() to block and restore signals when the thread locks an STL resource.
The way an application uses threads is also a big factor on STL performance. For example, consider how a threaded program might be coded to find the first 10,000 prime numbers, using a 4-CPU system. One approach might be to create a new worker-thread to test if one specific number N is prime (for N = 1, 2, 3, 4, etc.), and to have three of these worker threads concurrently active (the main thread makes four threads; i.e. one per CPU). But a better approach would be to create three permanent worker-threads that loop repeatedly, testing successive numbers for prime. The latter approach requires more synchronization between the threads to determine which number to test next, (whereas in the former case the main thread can tell each new thread which number to test via the user-argument to the thread-create routine), but it avoids the overhead of repeatedly creating threads. Both programs require synchronization when a prime is found, to increment a global counter and to store the new prime number in a global array.
Table 2 shows the comparative
performance of two prime-number programs coded to each approach, using
both the native POSIX threads and STL. Values in the table give the number
of primes found per second, using the 4-CPU ES40 again. Bear in mind that
for half the numbers tested for prime, the test will complete within a
very few instructions, because even numbers are not prime.
Table 2. Performance
of prime-number programs.
Results in primes-per-second:
higher is better.
|
|
|
|
|
|
|
|
|
|
|
|
The results confirm
that the overhead of STL is substantial for the areas of thread creation,
thread termination, and joining a thread. The results also show that the
overhead of STL for synchronization object manipulation is low. It is envisaged
that threaded applications will use the synchronization routines much more
frequently than the thread routines, and hence the general overhead of
STL should be low.
This paper describes the major functionality of STL, and how it is implemented.
STL is freely available
in open-source format as part of SCL, and a binary library is available
for Tru64 UNIX.
2. Dave Butenhof. Threaded Programming Standards. http://csa.compaq.com/CTJ_Article_29.html February 2000.
3. Bryan O'Sullivan. 1997. Answers to frequently asked questions for comp.programming.threads. http://www.serpentine.com/~bos/threads-faq/
4. Sun Solaris to Compaq Tru64 UNIX Porting Guide. http://www.unix.digital.com/faqs/publications/pub_page/porting.html
5. Solaris Compatibility Libraries (SCL) for Compaq Tru64 UNIX. http://www.tru64unix.compaq.com/complibs/
6. Solaris Compatibility Librariesā Users Guide for Tru64 UNIX. http://www.tru64unix.compaq.com/complibs/documentation/html/scl_ug.html
7. David R. Butenhof. Programming with POSIX Threads. Addison-Wesley, 1997. ISBN 0-201-63392-2.
8. Leroy Xavier. The LinuxThreads Library. http://pauillac.inria.fr/~xleroy/linuxthreads/
9.
comp.programming.threads Internet newsgroup for discussing
threads programming.
ISO90: International Standardization Organization (ISO): ISO/IEC 9899:1990, Programming languages - C.
POS90: ISO/IEC standard 9945-1:1990: [IEEE Std 1003.1-1990]. Information technology - Portable Operating System Interface (POSIX®) - Part 1: System Application Program Interface (API) [C Language].
POS96: ISO/IEC standard 9945-1:1996 [IEEE/ANSI Std 1003.1, 1996 Edition]. Information Technology-Portable Operating System Interface (POSIX®) - Part 1: System Application: Program Interface (API) [C Language].
UX98:
The Open Group. Single UNIX Specification Version 2. 1998. http://www.unix-systems.org/
[F2] POSIX threads refers to the thread-specific part of the formal standard ISO/IEC 9945-1:1996 [POS96] that is commonly known as POSIX 1003.1-1996. This standard integrates the original POSIX 1003.1-1990 [POS90] standard (base operating system API) with the amendments 1003.1b-1993 (real-time extensions) and 1003.1c-1995 (threads). See Threaded Programming Standards [2] and the comp.programming.threads FAQ [3] for further information.
[F3] But see also the section Non-objectives for a description of other potential porting issues.
[F4] Actually, semaphores were defined by POSIX 1003.1b-1993 (real-time extensions) rather than POSIX 1003.1c-1995 (threads).
[F5] Specifically from the X/Open CAE Specification, System Interfaces and Headers, Issue 5 (also known as XSH5) part of SUSv2.
[F6] See the Error Logging chapter of the SCL Users Guide [6]. Requires setting the SCL_LOG_FILE environment variable.
[F7] A threaded process will also terminate if any thread calls exit(), either explicitly, or, for the main thread only, implicitly if the main thread finishes without calling pthread_exit() (POSIX threads) or thr_exit() (Solaris threads).
[F8] An important feature of the thread-specific data destructor routine is that this routine always gets called, regardless of how the thread terminates.
[F9] Solaris provides functions to set and get the priority of a thread. Since the POSIX threads API has no direct equivalent to explicitly get or set the priority of a thread, STL only stores the priority of a thread so that thr_getprio() returns the value set by thr_setprio().
[F10] In UNIX, a zombie process is a process that has terminated but has not been reaped by the parent process calling one of the wait() system calls.
[F11] POSIX 1003.1-1996 defines which functions are re-entrant with respect to POSIX signals.
[F12] When a thread processes its pending signals, there is no guarantee that they are processed in the same order that they were received.
[F13]
Thread suspend and continue were most troublesome in the original STL implementation
on Tru64 UNIX.
This paper was originally published in the
Proceedings of the FREENIX Track: 2001 USENIX Annual Technical Conference,
June 25-30, 2001, Boston, Masssachusetts, USA
Last changed: 21 June 2001 bleu |
|