| MUTEX_INIT(3C) | Standard C Library Functions | MUTEX_INIT(3C) |
mutex_init,
mutex_lock, mutex_trylock,
mutex_unlock, mutex_enter,
mutex_exit,
mutex_consistent,
mutex_destroy — mutual
exclusion locks
Standard C Library (libc, -lc)
#include
<thread.h>
#include <synch.h>
int
mutex_init(mutex_t *mp,
int type, void *arg);
int
mutex_lock(mutex_t *mp);
int
mutex_trylock(mutex_t *mp);
int
mutex_unlock(mutex_t *mp);
void
mutex_enter(mutex_t *mp);
void
mutex_exit(mutex_t *mp);
int
mutex_consistent(mutex_t
*mp);
int
mutex_destroy(mutex_t *mp);
Mutual exclusion locks (mutexes) prevent multiple threads from
simultaneously executing critical sections of code that access shared data
(that is, mutexes are used to serialize the execution of threads). All
mutexes must be global. A successful call for a mutex lock by way of
mutex_lock()
will cause another thread that is also trying to lock the same mutex to
block until the owner thread unlocks it by way of
mutex_unlock(). Threads within the same process or
within other processes can share mutexes.
Mutexes can synchronize threads within the same process or in other processes. Mutexes can be used to synchronize threads between processes if the mutexes are allocated in writable memory and shared among the cooperating processes (see mmap(2)), and have been initialized for this task.
Mutexes are either intra-process or inter-process, depending upon the argument passed implicitly or explicitly to the initialization of that mutex. A statically allocated mutex does not need to be explicitly initialized; by default, a statically allocated mutex is initialized with all zeros and its scope is set to be within the calling process.
For inter-process synchronization, a mutex needs
to be allocated in memory shared between these processes. Since the memory
for such a mutex must be allocated dynamically, the mutex needs to be
explicitly initialized using
mutex_init().
The
mutex_init()
function initializes the mutex referenced by mp with
the type specified by type. Upon successful
initialization the state of the mutex becomes initialized and unlocked. Only
the attribute type LOCK_PRIO_PROTECT uses
arg. The type argument must be
one of the following:
USYNC_THREADUSYNC_PROCESSThe type argument can be augmented by the bitwise-inclusive-OR of zero or more of the following flags:
LOCK_ROBUSTEOWNERDEAD. The application must always check the
return value from
mutex_lock()
for a mutex of this type. The new owner of this mutex should then attempt
to make the state protected by the mutex consistent, since this state
could have been left inconsistent when the last owner died. If the new
owner is able to make the state consistent, it should call
mutex_consistent()
to restore the state of the mutex and then unlock the mutex. All
subsequent calls to mutex_lock() will then behave
normally. Only the new owner can make the mutex consistent. If for any
reason the new owner is not able to make the state consistent, it should
not call mutex_consistent() but should simply
unlock the mutex. All waiting processes will be awakened and all
subsequent calls to mutex_lock() will fail in
acquiring the mutex with an error value of
ENOTRECOVERABLE. If the thread or process that
acquired the lock with EOWNERDEAD terminates
without unlocking the mutex, the next owner will acquire the lock with an
error value of EOWNERDEAD.
The memory for the object to be initialized
with this attribute must be zeroed before initialization. Any thread or
process interested in the robust lock can call
mutex_init()
to potentially initialize it, provided that all such callers of
mutex_init() specify the same set of attribute
flags. In this situation, if mutex_init() is
called on a previously initialized robust mutex,
mutex_init() will not reinitialize the mutex and
will return the error value EBUSY.
LOCK_RECURSIVELOCK_ERRORCHECKLOCK_RECURSIVE is also set, a thread
attempting to relock this mutex without first unlocking it will return
with an error rather than deadlocking itself. A thread attempting to
unlock this mutex without first owning it will return with an error.LOCK_PRIO_INHERITLOCK_PRIO_INHERIT attribute,
it executes at the higher of its priority or the priority of the highest
priority thread waiting on any of the mutexes owned by this thread and
initialized with this attribute.LOCK_PRIO_PROTECTLOCK_PRIO_PROTECT attribute, it executes at the
higher of its priority or the highest of the priority ceilings of all the
mutexes owned by this thread and initialized with this attribute,
regardless of whether other threads are blocked on any of these mutexes.
When this attribute is specified, arg must point to
an int containing the priority ceiling.See
pthread_mutexattr_getrobust(3C)
for more information about robust mutexes. The
LOCK_ROBUST attribute is the same as the POSIX
PTHREAD_MUTEX_ROBUST attribute.
See
pthread_mutexattr_settype(3C)
for more information on recursive and error checking mutex types. The
combination (LOCK_RECURSIVE |
LOCK_ERRORCHECK) is the same as the POSIX
PTHREAD_MUTEX_RECURSIVE type. By itself,
LOCK_ERRORCHECK is the same as the POSIX
PTHREAD_MUTEX_ERRORCHECK type.
The LOCK_PRIO_INHERIT
attribute is the same as the POSIX
PTHREAD_PRIO_INHERIT attribute. The
LOCK_PRIO_PROTECT attribute is the same as the POSIX
PTHREAD_PRIO_PROTECT attribute. See
pthread_mutexattr_getprotocol(3C),
pthread_mutexattr_getprioceiling(3C),
and
pthread_mutex_getprioceiling(3C)
for a full discussion. The LOCK_PRIO_INHERIT and
LOCK_PRIO_PROTECT attributes are mutually exclusive.
Specifying both of these attributes causes
mutex_init()
to fail with EINVAL.
Initializing mutexes can also be accomplished by allocating in
zeroed memory (default), in which case a type of
USYNC_THREAD is assumed. In general, the following
rules apply to mutex initialization:
These rules do not apply to LOCK_ROBUST
mutexes. See the description for LOCK_ROBUST above.
If default mutex attributes are used, the macro
DEFAULTMUTEX can be used to initialize mutexes that
are statically allocated.
Default mutex initialization (intra-process):
mutex_t mp; mutex_init(&mp, USYNC_THREAD, NULL);
or
mutex_t mp = DEFAULTMUTEX;
Customized mutex initialization (inter-process):
mutex_init(&mp, USYNC_PROCESS, NULL);
Customized mutex initialization (inter-process robust):
mutex_init(&mp, USYNC_PROCESS | LOCK_ROBUST, NULL);
Statically allocated mutexes can also be initialized with macros
specifying LOCK_RECURSIVE and/or
LOCK_ERRORCHECK:
mutex_t mp = RECURSIVEMUTEX;
Same as (USYNC_THREAD |
LOCK_RECURSIVE)
mutex_t mp = ERRORCHECKMUTEX;
Same as (USYNC_THREAD |
LOCK_ERRORCHECK)
mutex_t mp = RECURSIVE_ERRORCHECKMUTEX;
Same as (USYNC_THREAD |
LOCK_RECURSIVE |
LOCK_ERRORCHECK)
When working in an intra-process context, it is
always recommended to use error checking mutexes and use the mutex lock and
unlock variants
mutex_enter()
and mutex_exit(). These simplify the error checking
conditions and will terminate a process on any cases an error would have
been returned, similar to the operation of the kernel's
mutex_enter(9F) and
mutex_exit(9F).
A critical section of code is enclosed by the call to lock the mutex and the call to unlock the mutex to protect it from simultaneous access by multiple threads. Only one thread at a time may possess mutually exclusive access to the critical section of code that is enclosed by the mutex-locking call and the mutex-unlocking call, whether the mutex's scope is intra-process or inter-process. A thread calling to lock the mutex either gets exclusive access to the code starting from the successful locking until its call to unlock the mutex, or it waits until the mutex is unlocked by the thread that locked it.
Mutexes have ownership, unlike semaphores. Although any thread, within the scope of a mutex, can get an unlocked mutex and lock access to the same critical section of code, only the thread that locked a mutex should unlock it.
If a thread waiting for a mutex receives a signal, upon return from the signal handler, the thread resumes waiting for the mutex as if there was no interrupt. A mutex protects code, not data; therefore, strongly bind a mutex with the data by putting both within the same structure, or at least within the same procedure.
A call to
mutex_lock()
locks the mutex object referenced by mp. If the mutex
is already locked, the calling thread blocks until the mutex is freed; this
will return with the mutex object referenced by mp in
the locked state with the calling thread as its owner. If the current owner
of a mutex tries to relock the mutex, it will result in deadlock.
The
mutex_trylock()
function is the same as mutex_lock(), except that if
the mutex object referenced by mp is locked (by any
thread, including the current thread), the call returns immediately with an
error.
The
mutex_unlock()
function is called by the owner of the mutex object referenced by
mp to release it. The mutex must be locked and the
calling thread must be the one that last locked the mutex (the owner). If
there are threads blocked on the mutex object referenced by
mp when mutex_unlock() is
called, the mp is freed, and the scheduling policy
will determine which thread gets the mutex. If the calling thread is not the
owner of the lock, no error status is returned, and the behavior of the
program is undefined.
The
mutex_enter()
function is a variant of the mutex_lock() function
that returns void, meaning that it will perform all error checking and if
any of the traditional mutex error conditions occur then the process will be
terminated as though upanic(2) or
similar was called. All of the conditions of error checking mutexes are
checked including trying to lock an uninitialized mutex, recursively locking
a mutex that is not a recursive type, etc. The
mutex_enter() function is only suitable for
USYNC_THREAD (intra-process) locks and the lock must
have been initialized with the LOCK_ERRORCHECK
attribute (or the ERRORCHECKMUTEX static
initializer). If mp was not initialized as an
error-checking mutex then that is treated as a fatal error when using
mutex_enter().
The
mutex_exit()
function is a variant of the mutex_unlock() function
that returns void, meaning that it will perform all error checking and if
any of the traditional mutex error conditions occur then the process will be
terminated as though upanic(2) or
similar was called. All of the conditions of error checking mutexes are
checked including trying to unlock a mutex that is not currently locked or
where the calling thread is not the owner. The
mutex_exit() function is only suitable for
USYNC_THREAD (intra-process) locks and the lock must
have been initialized with the LOCK_ERRORCHECK
attribute (or the ERRORCHECKMUTEX static
initializer). If mp was not initialized as an
error-checking mutex then that is treated as a fatal error when using
mutex_exit().
The
mutex_enter()
and mutex_exit() functions are modeled after the
kernel's mutex_enter(9F) and
mutex_exit(9F) functions. Using
mutex_enter() and
mutex_exit() is recommended over
mutex_lock() and
mutex_unlock() as most applications do not properly
check or handle error conditions and terminating a process that has
encountered programmer error is generally preferable to just sitting there
deadlocked waiting for an administrator to determine that this has happened
and kill the process. When these conditions are encountered, a core file
will be generated subject to the
coreadm(8) settings.
The
mutex_destroy()
function destroys the mutex object referenced by mp.
The mutex object becomes uninitialized. The space used by the destroyed
mutex variable is not freed. It needs to be explicitly reclaimed.
Upon successful completion, the
mutex_init(), mutex_lock(),
mutex_trylock(),
mutex_unlock(),
mutex_consistent(), and
mutex_destroy() functions return
0. Otherwise, an
error number is returned.
The following example uses one global mutex as a gate-keeper to permit each thread exclusive sequential access to the code within the user-defined function "change_global_data". This type of synchronization will protect the state of shared data, but it also prohibits parallelism.
/* gcc thisfile.c */
#define _REENTRANT
#include <stdio.h>
#include <thread.h>
#define NUM_THREADS 12
void *change_global_data(void *); /* for thr_create() */
main(int argc,char * argv[]) {
int i=0;
for (i=0; i< NUM_THREADS; i++) {
thr_create(NULL, 0, change_global_data, NULL, 0, NULL);
}
while ((thr_join(NULL, NULL, NULL) == 0));
}
void * change_global_data(void *null){
static mutex_t Global_mutex;
static int Global_data = 0;
mutex_lock(&Global_mutex);
Global_data++;
sleep(1);
printf("%d is global data\n",Global_data);
mutex_unlock(&Global_mutex);
return NULL;
}
The previous example, the mutex, the code it owns, and the data it protects was enclosed in one function. The next example uses C++ features to accommodate many functions that use just one mutex to protect one data:
/* g++ thisfile.c use C++ to compile*/
#define _REENTRANT
#include <stdlib.h>
#include <stdio.h>
#include <thread.h>
#include <errno.h>
#include <iostream.h>
#define NUM_THREADS 16
void *change_global_data(void *); /* for thr_create() */
class Mutected {
private:
static mutex_t Global_mutex;
static int Global_data;
public:
static int add_to_global_data(void);
static int subtract_from_global_data(void);
};
int Mutected::Global_data = 0;
mutex_t Mutected::Global_mutex;
int Mutected::add_to_global_data() {
mutex_lock(&Global_mutex);
Global_data++;
mutex_unlock(&Global_mutex);
return Global_data;
}
int Mutected::subtract_from_global_data() {
mutex_lock(&Global_mutex);
Global_data--;
mutex_unlock(&Global_mutex);
return Global_data;
}
void
main(int argc,char * argv[]) {
int i=0;
for (i=0;i< NUM_THREADS;i++) {
thr_create(NULL,0,change_global_data,NULL,0,NULL);
}
while ((thr_join(NULL,NULL,NULL) == 0));
}
void * change_global_data(void *) {
static int switcher = 0;
if ((switcher++ % 3) == 0) /* one-in-three threads subtracts */
cout << Mutected::subtract_from_global_data() << endl;
else
cout << Mutected::add_to_global_data() << endl;
return NULL;
}
A mutex can protect data that is shared among processes. The mutex
would need to be initialized as USYNC_PROCESS. One
process initializes the process-shared mutex and writes it to a file to be
mapped into memory by all cooperating processes (see
mmap(2)). Afterwards, other independent
processes can run the same program (whether concurrently or not) and share
mutex-protected data.
/* gcc thisfile.c */
/* To execute, run the command line "a.out 0 &; a.out 1" */
#define _REENTRANT
#include <sys/types.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <thread.h>
#define INTERPROCESS_FILE "ipc-sharedfile"
#define NUM_ADDTHREADS 12
#define NUM_SUBTRACTTHREADS 10
#define INCREMENT '0'
#define DECREMENT '1'
typedef struct {
mutex_t Interprocess_mutex;
int Interprocess_data;
} buffer_t;
buffer_t *buffer;
void *add_interprocess_data(), *subtract_interprocess_data();
void create_shared_memory(), test_argv();
int zeroed[sizeof(buffer_t)];
int ipc_fd, i=0;
void
main(int argc,char * argv[]){
test_argv(argv[1]);
switch (*argv[1]) {
case INCREMENT:
/* Initializes the process-shared mutex */
/* Should be run prior to running a DECREMENT process */
create_shared_memory();
ipc_fd = open(INTERPROCESS_FILE, O_RDWR);
buffer = (buffer_t *)mmap(NULL, sizeof(buffer_t),
PROT_READ | PROT_WRITE, MAP_SHARED, ipc_fd, 0);
buffer->Interprocess_data = 0;
mutex_init(&buffer->Interprocess_mutex, USYNC_PROCESS,0);
for (i=0; i< NUM_ADDTHREADS; i++)
thr_create(NULL, 0, add_interprocess_data, argv[1],
0, NULL);
break;
case DECREMENT:
/* Should be run after the INCREMENT process has run. */
while(ipc_fd = open(INTERPROCESS_FILE, O_RDWR)) == -1)
sleep(1);
buffer = (buffer_t *)mmap(NULL, sizeof(buffer_t),
PROT_READ | PROT_WRITE, MAP_SHARED, ipc_fd, 0);
for (i=0; i< NUM_SUBTRACTTHREADS; i++)
thr_create(NULL, 0, subtract_interprocess_data, argv[1],
0, NULL);
break;
} /* end switch */
while ((thr_join(NULL,NULL,NULL) == 0));
} /* end main */
void *add_interprocess_data(char argv_1[]){
mutex_lock(&buffer->Interprocess_mutex);
buffer->Interprocess_data++;
sleep(2);
printf("%d is add-interprocess data, and %c is argv1\n",
buffer->Interprocess_data, argv_1[0]);
mutex_unlock(&buffer->Interprocess_mutex);
return NULL;
}
void *subtract_interprocess_data(char argv_1[]) {
mutex_lock(&buffer->Interprocess_mutex);
buffer->Interprocess_data--;
sleep(2);
printf("%d is subtract-interprocess data, and %c is argv1\n",
buffer->Interprocess_data, argv_1[0]);
mutex_unlock(&buffer->Interprocess_mutex);
return NULL;
}
void create_shared_memory(){
int i;
ipc_fd = creat(INTERPROCESS_FILE, O_CREAT | O_RDWR );
for (i=0; i<sizeof(buffer_t); i++){
zeroed[i] = 0;
write(ipc_fd, &zeroed[i],2);
}
close(ipc_fd);
chmod(INTERPROCESS_FILE, S_IRWXU | S_IRWXG | S_IRWXO);
}
void test_argv(char argv1[]) {
if (argv1 == NULL) {
printf("use 0 as arg1 for initial process\n \
or use 1 as arg1 for the second process\n");
exit(NULL);
}
}
A mutex can protect data that is shared among processes robustly.
The mutex would need to be initialized as
USYNC_PROCESS | LOCK_ROBUST.
One process initializes the robust process-shared mutex and writes it to a
file to be mapped into memory by all cooperating processes (see
mmap(2)). Afterwards, other independent
processes can run the same program (whether concurrently or not) and share
mutex-protected data.
The following example shows how to use a
USYNC_PROCESS | LOCK_ROBUST
type mutex.
/* gcc thisfile.c */
/* To execute, run the command line "a.out & a.out 1" */
#include <sys/types.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <stdio.h>
#include <thread.h>
#define INTERPROCESS_FILE "ipc-sharedfile"
typedef struct {
mutex_t Interprocess_mutex;
int Interprocess_data;
} buffer_t;
buffer_t *buffer;
int make_date_consistent();
void create_shared_memory();
int zeroed[sizeof(buffer_t)];
int ipc_fd, i=0;
main(int argc,char * argv[]) {
int rc;
if (argc > 1) {
while((ipc_fd = open(INTERPROCESS_FILE, O_RDWR)) == -1)
sleep(1);
buffer = (buffer_t *)mmap(NULL, sizeof(buffer_t),
PROT_READ | PROT_WRITE, MAP_SHARED, ipc_fd, 0);
mutex_init(&buffer->Interprocess_mutex,
USYNC_PROCESS | LOCK_ROBUST, 0);
} else {
create_shared_memory();
ipc_fd = open(INTERPROCESS_FILE, O_RDWR);
buffer = (buffer_t *)mmap(NULL, sizeof(buffer_t),
PROT_READ | PROT_WRITE, MAP_SHARED, ipc_fd, 0);
buffer->Interprocess_data = 0;
mutex_init(&buffer->Interprocess_mutex,
USYNC_PROCESS | LOCK_ROBUST, 0);
}
for(;;) {
rc = mutex_lock(&buffer->Interprocess_mutex);
switch (rc) {
case EOWNERDEAD:
/*
* The lock is acquired.
* The last owner died holding the lock.
* Try to make the state associated with
* the mutex consistent.
* If successful, make the robust lock consistent.
*/
if (make_data_consistent())
mutex_consistent(&buffer->Interprocess_mutex);
mutex_unlock(&buffer->Interprocess_mutex);
break;
case ENOTRECOVERABLE:
/*
* The lock is not acquired.
* The last owner got the mutex with EOWNERDEAD
* and failed to make the data consistent.
* There is no way to recover, so just exit.
*/
exit(1);
case 0:
/*
* There is no error - data is consistent.
* Do something with data.
*/
mutex_unlock(&buffer->Interprocess_mutex);
break;
}
}
} /* end main */
void create_shared_memory() {
int i;
ipc_fd = creat(INTERPROCESS_FILE, O_CREAT | O_RDWR );
for (i=0; i<sizeof(buffer_t); i++) {
zeroed[i] = 0;
write(ipc_fd, &zeroed[i],2);
}
close(ipc_fd);
chmod(INTERPROCESS_FILE, S_IRWXU | S_IRWXG | S_IRWXO);
}
/* return 1 if able to make data consistent, otherwise 0. */
int make_data_consistent () {
buffer->Interprocess_data = 0;
return (1);
}
The following example allocates and frees memory in which a mutex is embedded.
struct record {
int field1;
int field2;
mutex_t m;
} *r;
r = malloc(sizeof(struct record));
mutex_init(&r->m, USYNC_THREAD, NULL);
/*
* The fields in this record are accessed concurrently
* by acquiring the embedded lock.
*/
The thread execution in this example is as follows:
Thread 1 executes: Thread 2 executes: ... ... mutex_lock(&r->m); mutex_lock(&r->m); r->field1++; localvar = r->field1; mutex_unlock(&r->m); mutex_unlock(&r->m); ... ...
Later, when a thread decides to free the memory pointed to by
r, the thread should call
mutex_destroy() on the mutexes in this memory.
In the following example, the main thread can do a thr_join(3C) on both of the above threads. If there are no other threads using the memory in r, the main thread can now safely free r:
for (i = 0; i < 2; i++)
thr_join(0, 0, 0);
mutex_destroy(&r->m); /* first destroy mutex */
free(r); /* then free memory */
If the mutex is not destroyed, the program could have memory leaks.
The mutex_init() function will fail
if:
EINVALLOCK_PRIO_INHERIT and
LOCK_PRIO_PROTECT attributes are both
specified.The mutex_init() function will fail for
LOCK_ROBUST type mutex if:
EBUSYEINVALThe mutex_trylock() function will fail
if:
EBUSYThe mutex_lock() and
mutex_trylock() functions will fail for a
LOCK_RECURSIVE mutex if:
EAGAINThe mutex_lock() function will fail for a
LOCK_ERRORCHECK and
non-LOCK_RECURSIVE mutex
if:
EDEADLKThe mutex_lock() function may fail for a
non-LOCK_ERRORCHECK and
non-LOCK_RECURSIVE mutex
if:
EDEADLKThe mutex_unlock() function will fail for
a LOCK_ERRORCHECK mutex if:
EPERMThe mutex_lock() or
mutex_trylock() functions will fail for
LOCK_ROBUST type mutex if:
EOWNERDEADmutex_consistent() and unlock the mutex.
Subsequent calls to mutex_lock() will behave
normally, as before. If the caller is not able to clean up the state,
mutex_consistent() should not be called but the
mutex should be unlocked. Subsequent calls to
mutex_lock() will fail to acquire the mutex,
returning with the error value
ENOTRECOVERABLE.
If the owner who acquired the lock with
EOWNERDEAD
dies, the next owner will acquire the lock with
EOWNERDEAD.ENOTRECOVERABLEEOWNERDEAD
and the owner was not able to clean up the state and unlocked the mutex
without calling mutex_consistent().The mutex_consistent() function will fail
if:
EINVALLOCK_ROBUST mutex having an inconsistent state
(EOWNERDEAD).mmap(2), shmop(2), pthread_mutex_getprioceiling(3C), pthread_mutex_init(3C), pthread_mutexattr_getprioceiling(3C), pthread_mutexattr_getprotocol(3C), pthread_mutexattr_getrobust(3C), pthread_mutexattr_gettype(3C), attributes(7), mutex(7), standards(7)
Previous releases of Solaris provided the
USYNC_PROCESS_ROBUST mutex type. This type is now
deprecated but is still supported for source and binary compatibility. When
passed to
mutex_init(),
it is transformed into (USYNC_PROCESS |
LOCK_ROBUST). The former method for restoring a
USYNC_PROCESS_ROBUST mutex to a consistent state was
to reinitialize it by calling mutex_init(). This
method is still supported for source and binary compatibility, but the
proper method is to call mutex_consistent().
The USYNC_PROCESS_ROBUST
type permitted an alternate error value,
ELOCKUNMAPPED, to be returned by
mutex_lock()
if the process containing a locked robust mutex unmapped the memory
containing the mutex or performed one of the
exec(2) functions. The
ELOCKUNMAPPED error value implies all of the
consequences of the EOWNERDEAD error value and as
such is just a synonym for EOWNERDEAD. For full
source and binary compatibility, the ELOCKUNMAPPED
error value is still returned from mutex_lock() in
these circumstances, but only if the mutex was initialized with the
USYNC_PROCESS_ROBUST type. Otherwise,
EOWNERDEAD is returned in these circumstances.
The
mutex_lock(),
mutex_unlock(), and
mutex_trylock() functions do not validate the mutex
type. An uninitialized mutex or a mutex with an invalid type does not return
EINVAL. Interfaces for mutexes with an invalid type
have unspecified behavior.
Uninitialized mutexes that are allocated locally
could contain junk data. Such mutexes need to be initialized using
mutex_init().
By default, if multiple threads are waiting for a mutex, the order of acquisition is undefined.
| April 19, 2026 | OmniOS |