Plasma GitLab Archive
Projects Blog Knowledge

Netshm_intro

Shared Memory for IPC

The netshm library implements a number of practical schemes to manage shared memory. Here we give a tutorial how to use that functions.

What kind of shared memory we talk about

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

  • the process knows the global name of the object, and
  • the process has the access rights.

It is not necessary that the processes have something else in common (e.g. that one process is created by the other, etc.).

Types of shared memory provided by the OS

In the Unix world, there are (at least) two common APIs to access global shared memory objects:

  • System V shared memory
  • POSIX shared memory

The 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 * 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 two strings as arguments: (file_name,mem_name). The file_name points to a normal file, usually located in the /tmp hierarchy. This file is only used for management and locking purposes. The mem_name is the actual POSIX memory object. This name looks like a file name, but actually lives in a different name space. It must begin with a slash, followed by the name that must not contain further slashes. An exampe: `POSIX("/tmp/sample", "/sample").

Opening shared memory objects

There is the function Netshm.open_shm that works much like Unix.openfile. For example,

let sd = 
  Netshm.open_shm
    (`POSIX("/tmp/sample","/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("/tmp/myprogram.XXXXX","/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.

The element types are restricted to those types that can be managed with 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
  • pairs of two manageable types (pairs of pairs are allowed)

For example, to get a data manager for the pair of an 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("/tmp/sample", "/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:

  • First, both P and Q read-lock the read value
  • Second, one process is a little bit faster (say P), and wants to replace the value, thus needing a write lock. It cannot get the write lock now because Q still has a read lock
  • Third, Q wants to replace the value, needing a write lock. It cannot get the write lock now because P still has a read lock

If nothing else happened, both processes would wait forever.

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).

Persistency of shared memory

Note that POSIX shared memory is not automatically released when the process terminates. Instead, such memory objects persist until explicitly deleted (just like files). If you forget to delete such objects, RAM will be consumed until the system is rebooted!

There is a helper data structure in Netsys_pmanage helping to find and delete memory: If you have an object `POSIX(tmp_name, mem_name), the file tmp_name contains a line pointing to mem_name, e.g.

 R POSIX_SHM "/my_object" 

You can use Netsys_pmanage to read this line and to unlink the object:

let m = Netsys_pmanage.pmanage tmp_name in
m # unlink()

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:

  • Servers using multi-processing where the processes must share some state (e.g. sessions)
  • Scientific computations implemented on a multi-CPU system with multi-processing, where the processes put their results into shared memory
  • Using netshm as fast persistent lookup table

Limits

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.

This web site is published by Informatikbüro Gerd Stolpmann
Powered by Caml