The netshm
library implements a number of practical schemes to manage
shared memory. Here we give a tutorial how to use that functions.
In general, shared memory is a memory region that is mapped into several address spaces. The O'Caml language already supports one kind of shared memory out of the box, the so-called multi-threading. Here, several execution threads access the same memory, and thus can access the same data. There are a lot of accompanying techniques allowing one to manage shared memory (mutexes, condition variables, event channels, etc.).
Of course, the netshm
library does not use this kind of shared
memory (it is effectively even incompatible with multi-threading to
some degree). Here, memory is shared by independent processes. This
means that any process on the computer can access a shared memory
object provided
In the Unix world, there are (at least) two common APIs to access global shared memory objects:
netshm
library has an extensible interface that can support
several system APIs. Currently, however, there is only an
implementation for POSIX shared memory.
In addition to that, it is also possible to access file-backed memory.
Note that not all OS support POSIX shared memory. To check, look at
the value of Netsys.have_posix_shm()
.
Global names
The module Netshm
defines the possible global names:
type shm_name =
[ `POSIX of string
| `File of string
]
For POSIX shared memory you must take a `POSIX
global name, for
file-backed memory a `File
name. A `POSIX
global name has
a string as argument that looks like a strange file name. It must
begin with a slash, followed by the name that must not contain
further slashes, e.g. `POSIX "/sample"
.
Actually, the OS transforms this name to a real filename in a
system-dependent way. For example, Linux would map `POSIX "/sample"
to the real file /dev/shm/sample
. There is, however, no way to
find something out about this transformation from a portable
program.
The `File
names are real file names.
There is the function Netshm.open_shm
that works much like
Unix.openfile
. For example,
let sd =
Netshm.open_shm (`POSIX "/sample") [Unix.O_CREAT; Unix.O_RDWR] 0o666
opens /sample
for read-write access, and if the object does not
exist yet, it is created with mode 0o666. The returned result
is a so-called shared memory descriptor.
In order to create unique new objects, you can also use
Netshm.create_unique_shm
. This function takes a pattern for the
global name, and creates a new object with a unique name based on the
pattern. This is done by replacing all 'X'
letters in the string
argument by random characters until a new name has been found.
For example:
let sd =
Netshm.create_unique_shm (`POSIX "/myprogram.XXXXX") 0o666
The actual name is returned by Netshm.name_of_shm
.
Like files, shared memory objects must be closed after usage:
Netshm.close_shm sd
Of course, the object continues to exist after the descriptor
has been closed. There is a special function Netshm.unlink_shm
to delete objects.
It is discouraged to open shared memory objects several times in parallel from the same process, as locking methods (see below) are confused by this.
If you create several processes by calling Unix.fork
it is required
that every process opens the shared memory object anew. It is not
sufficient to open the object in the master process, and to use
it in the child processes after forking. This will cause subtle
malfunctions.
Data structures
The Netshm
module contains only the algorithms for the primitve
data structure, a hash table from int32
to bigarrays of
int32
elements. Netshm_hashtbl
and Netshm_array
have
user-friendlier data structures:
Netshm_hashtbl
is a hash table from type s to t, where
both s and t can be any so-called manageable type (see below).Netshm_array
is an array of elements, again of one of
the mentioned manageable types. Arrays can be sparse.Netshm_data
. In principle, this can be any
type for which you can write a data manager.
Netshm_data
contains only data managers for these types:
int
int32
int64
nativeint
string
int
and a string
, one can do
let dm =
Netshm_data.pair_manager
Netshm_data.int_manager
Netshm_data.string_manager
dm
has then type (int * string) data_manager
.
In order to view a shared memory object as hash table or array,
it is necessary to manage
it:
let sd =
Netshm.open_shm (`POSIX "/sample") [Unix.O_RDWR] 0 in
let tab =
Netshm_hashtbl.manage
Netshm_data.string_manager
dm
`No_locking
sd
The manage
function for hash tables takes the data managers for
keys and values of the table, a thing called `No_locking
, and
the shared memory descriptor sd
as input. It returns the
abstract value that represents the hash table. What `No_locking
means will become clearer below.
After being managed, you can access tab
like a hash table:
Netshm_hashtbl.add tab "First Key" (1, "First value")
let (n, s) = Netshm_hashtbl.find tab "First Key"
Note that neither input nor output values of managed objects are
placed in shared memory. They are normal O'Caml values for which no
restrictions apply. The shared memory is totally hidden from user code
(actually, there are two functions in Netshm
that exhibit values
that reside in this memory, but they should only be used by experts).
The manage
function for arrays is slightly different. In particular,
only one data manager must be given.
Concurrent Access
If `No_locking
is effective, nothing is done by netshm to make
concurrent accesses by several processes to the same object safe. The
implications depend on what you do. If you only read, everything is
ok. If there is a writer, it is possible that netshm cannot access
the object properly in certain situations, and it is even possible
that the internal structures of the object are destroyed.
In order to avoid such problems, you can specify a locking method
other than `No_locking
. Currently there is only `Record_locking
.
It is sufficient to pass `Record_locking
to the manage
functions - the rest works automatically:
let tab =
Netshm_hashtbl.manage
Netshm_data.string_manager
dm
`Record_locking
sd
Record locking bases on the Unix.lockf
function. It has been chosen
as primary locking method because it is available everywhere - although
it is not the fastest method.
The effect of locking is that every read access places read locks on the relevant parts of the object, and that every write access acquires the needed write locks. The locking scheme is rather fine-grained and allows true parallel accesses when a reader and a writer manipulate the same key of the hash table. Two writers, however, lock each other out.
Grouping
Consider this piece of code:
let v = Netshm_hashtbl.find tab k in
let v' = compute_something v in
Netshm_hashtbl.replace tab k v'
This is a so-called read-modify-write cycle. If several processes do this in parallel, the execution of the cycles can overlap. For example, it is possible that process P writes the modified value v' while process Q checks for the value of this key. The outcome of such overlapping cycles is quite random: In the best case, computations are repeated, in the worst case, results of computations are lost.
Netshm
has the function Netshm.group
to avoid such effects:
Netshm.group
(Netshm_hashtbl.shm_table tab)
(fun () ->
let v = Netshm_hashtbl.find tab k in
let v' = compute_something v in
Netshm_hashtbl.replace tab k v'
)
()
Note that you need Netshm_hashtbl.shm_table
to get the underlying
primitive table in order to use group
.
The effect of group
is that conflicting accesses to the same parts
of the memory object are deferred, so that they do not overlap anymore
(technically, the release of the locks is deferred). However, another
phenomenon is now possible: Deadlocks.
Deadlock situations usually occur between the locking requirements
of a reader and a function that removes a value (remove
or replace
).
Imagine what happens when processes P and Q execute the same
grouped read-modify-write cycle almost at the same time:
Fortunately, record locking includes deadlock detection. One process
is notified that there is a deadlock. The netshm user gets the
Netshm.Deadlock
exception.
In a simple read-modifiy-write cycle, this exception is quite harmless:
The replace
call is interrupted before it got the chance to modify
the table. The suggested strategy to cope
with that is to sleep for a moment to let the other process do its
job and to restart the cycle from the beginning.
In a more complicated cycle, it can happen that a value is already written, and the second trial to write a value triggers the exception. Well, written is written, and there is no mechanism to "undo" the chaos. Best strategy: avoid such cycles, or use another way to store values in a shared storage (e.g. database system).
Applications
I hope it has become clear that netshm is only useful for simple applications as there is no support for transactions. Another fundamental limitation is that the file format is non-portable (netshm files written on platform A may not be readable on platform B).
Ideas:
The maximum size of a memory object depends on the pagesize
and init
parameters of the manage
call. Roughly, the
maximum size is
pagesize * init * 32768
which is about 8 G for the defaults pagesize
= 256 and
init
= 1000. Usually, other limits are hit first.
The hash table and array elements use 32 bit numbers for
their bookkeeping, e.g. strings can have at most a length
of 2 ^ 31 - 1 bytes.