Skip to content

Python and SysV shared memory

Monday, 7 October 2024 | Adriaan de Groot

At work-work the system uses, for historical reasons, a lot of SystemV shared memory. The SysV shared memory API has C functions like shmat(2). There is also a different shared memory API, POSIX shared memory, which has functions like shm_open(3). For reasons, on some work-work systems we’re constrained to Python 3.7 and no additional libraries. I wanted to mess with the shared memory on such a system, from Python for convenience, so I wrote some very simple wrappers. Here’s a recap.

As usual, corrections are welcome, or tips (by email). I write these notes as much for future me as anyone else.

Here is the core of the story (I have also added this to my personal GitHub repository, which I won’t link because it’s not future-proof storage).

import ctypes

lib = ctypes.cdll.LoadLibrary(None)

shmget = lib.shmget
shmget.argtypes = [ctypes.c_int, ctypes.c_size_t, ctypes.c_int]
shmget.restype = ctypes.c_int
shmget.__doc__ = """See shmget(2)"""

This works on FreeBSD, where SysV shmem is in the core libraries. On Linux, I think you need to call LoadLibrary("librt"). Anyway, wrapping the library-loading to be safe isn’t the point here.

Once ctypes has loaded a library, you can extract function pointers from the library. By adding annotations, you can give the Python function the same prototype as the C manpage for shmget.

Note that the manpage points to some special flag values. For those, you need to dig into the C headers. On FreeBSD, the special value IPC_PRIVATE is equal to 0, so that’s easy enough to write in Python. The following snippet is then sufficient to create a shared memory segment (one that is 1024 bytes large and world-readable) and print out its ID. The returned value is -1 on error.

print(shmget(0, 1024, 0o644))

The ID can be cross-checked with command ipcs -m (it’s installed by default on FreeBSD and in my KDE Neon machine, so seems like a common tool). To get rid of the segment, ipcrm -m <id> does the trick.

Similar wrappers are there for shmat, shmdt and shmctl – but those wrangle void * in C, and how does that work with Python?

The void pointer

CTypes has a c_void_p type, which can be created from None (a null pointer, seems reasonable) and returned from C functions. It can cast to-and-fro (in classic C style, the thing in memory is what I say is in memory) to other pointer types, and without a typed-pointer type at the machine level that just works (but don’t ask me how).

So the C function int shmctl(int shmid, int cmd, struct shmid_ds *buf) gets these types in Python: shmctl.argtypes = [ctypes.c_int, ctypes.c_int, ctypes.c_void_p], which presents the struct-pointer as a void-pointer.

The function void *shmat(int shmid, const void *addr, int flag) works similarly. When calling it, unless you have specific address needs, parameter addr can be nullptr (er .. ok, this is C, so NULL and in Python None). The pointer it returns is where the shared memory is attached.

Actually doing something with a void * takes work in C, it also takes work in Python with ctypes. You can cast to an int * for instance, with iaddr = ctypes.cast(addr, ctypes.POINTER(ctypes.c_int)) (a cast to char * is also readily available).

A special case is when you need to provide a void * to some C function. Where do they come from? In C you would just declare a (character) buffer of some size and pass it in. In Python, ctypes.create_string_buffer() does the job. Give it a size and get a memory-managed buffer.

Wrangling shared-memory segment destruction

There’s shmget() to create a segment, shmat() to attach (map it into memory of a process) to it, shmdt() to detach from a segment, but destroying a shared-memory segment does not have a simple C call to do it. There is shmctl() which does special-control-actions on a shared-memory segement, and destruction is one of them.

I ended up writing this little wrapper.

def shmrm(shmid : int) -> int:
    return shmctl(shmid, 0, None)

Sending messages

As an experiment, I wrote a program that can create, read, write and destroy a shared-memory segment. By writing (from one invocation) and then reading (from another invocation) I can “send” messages from the past! Or to the future! It is nearly as convenient as writing the messages to a file.

Here’s the write function. It attaches the shared-memory segment and then writes a Pascal-style string to that memory (Pascal-style in the sense of “starts with a length, followed by the actual data, no NUL-termination”). For bloggy purposes I have removed error-handling.

def write(shmid : int, v : str):
    addr = shmat(shmid, ctypes.c_void_p(None), 0)
    iaddr = ctypes.cast(addr, ctypes.POINTER(ctypes.c_int))
    caddr = ctypes.cast(addr, ctypes.POINTER(ctypes.c_char))

    ustring = v.encode("utf-8")
    iaddr[0] = len(ustring)
    for i in range(len(ustring)):
        caddr[4+i] = ustring[i]

    return shmdt(addr)

Here, shmat() returns a void * and I cast that to two typed pointers to the segment. I haven’t figured out how to do pointer arithmetic, so on the assumption there are 32-bit integers, the integer goes first and then the message goes starting at byte (char) number 4.

Takeaways

CTypes is really cool! It makes wrangling C APIs in Python .. well, let’s call it “acceptable”.

Starting with Python 3.8, everything I’ve written above is unnecessary because there is a good shared-memory abstraction in the standard Python library, but for my work-work purposes in a very restricted environment, this particular tool has turned out to be really useful.