classification
Title: Consistency of Unix's shared_memory implementation with windows
Type: behavior Stage: patch review
Components: Library (Lib) Versions: Python 3.9, Python 3.8
process
Status: open Resolution:
Dependencies: Superseder:
Assigned To: Nosy List: davin, eryksun, pitrou, vinay0410
Priority: normal Keywords: patch

Created on 2019-08-04 10:32 by vinay0410, last changed 2019-09-11 16:00 by davin.

Pull Requests
URL Status Linked Edit
PR 15460 closed vinay0410, 2019-08-24 15:03
Messages (12)
msg348980 - (view) Author: Vinay Sharma (vinay0410) * Date: 2019-08-04 10:32
Hi,
I am opening this to discuss about some possible enhancements in the multiprocessing.shared_memory module.

I have recently started using multiprocessing.shared_memory and realised that currently the module provides no functionality to alter the size of the shared memory segment, plus the process has to know beforehand whether to create a segment or open an existing one, unlike shm_open in C, where segment can be automatically created if it doesn't exist.


For an end user perspective I believe that these functionalities would be really helpful, and I would be happy to contribute, if you believe that they are necessary.

I would also like to mention that I agree this might be by design, or because of some challenges, in which case it would be very helpful if I can know them.
msg349902 - (view) Author: Davin Potts (davin) * (Python committer) Date: 2019-08-17 16:47
Attempts to alter the size of a shared memory segment are met with a variety of different, nuanced behaviors on systems we want to support.  I agree that it would be valuable to be able to effectively realloc a shared memory segment, which thankfully the user can do with the current implementation although they become responsible for adjusting for platform-specific behaviors.  The design of the API in multiprocessing.shared_memory strives to be as feature-rich as possible while providing consistent behavior across platforms that can be reasonably supported; it also leaves the door open (so to speak) for users to exploit additional platform-specific capabilities of shared memory segments.

Knowing beforehand whether to create a segment or attach to an existing one is an important feature for a variety of use cases.  I believe this is discussed at some length in issue35813.  If what is discussed there does not help (it did get kind of long sometimes), please say so and we can talk through it more.
msg349987 - (view) Author: Vinay Sharma (vinay0410) * Date: 2019-08-20 04:11
Hi Davin,
Thanks for replying!

As you said I went through the issue, and now understand why segments should not be automatically created if they don't exist.

But, after reading that thread I got to know that shared memory is supposed to exist even if the process exits, and it can only be freed by unlink, which I also believe is an important functionality required by many use cases as you mentioned.

But, this is not the behaviour currently.
As soon as the process exists, all the shared memory created is unlinked.

Also, the documentation currently mentions: "shared memory blocks may outlive the original process that created them", which is not the case at all.

Currently, the resource_tracker, unlinks the shared memory, by calling unlink as specified here:
```
if os.name == 'posix':
    import _multiprocessing
    import _posixshmem

    _CLEANUP_FUNCS.update({
        'semaphore': _multiprocessing.sem_unlink,
        'shared_memory': _posixshmem.shm_unlink,
    })
```

So, is this an expected behaviour, if yes documentation should be updated, and if not the code base should be.

I will be happy to submit a patch in both the cases.

PS: I personally believe from my experience that shared memory segments should outlive the process, unless specified otherwise. Also, a argument persist=True, can be added which can ensure that the shared_memory segment outlives the process, and can be used by processes which are spawned later.
msg350011 - (view) Author: Eryk Sun (eryksun) * (Python triager) Date: 2019-08-20 15:16
> PS: I personally believe from my experience that shared memory 
> segments should outlive the process, unless specified otherwise. 
> Also, a argument persist=True, can be added which can ensure 
> that the shared_memory segment outlives the process, and can be 
> used by processes which are spawned later.

In terms of providing "consistent behavior across platforms that can be reasonably supported", the behavior suggested above could not reasonably be supported in Windows. 

The Section (shared memory) object itself is not a file. It gets created in the object namespace, either globally in "\BaseNamedObjects" or in an interactive session's "\Sessions\<session ID>\BaseNamedObjects". 

By default, kernel objects are temporary. Creating permanent named objects requires SeCreatePermanentPrivilege, which by default is only granted to SYSTEM (sort of like root in Unix). For an object to be accessible across sessions and outlive an interactive session, it needs to be global. Creating global Section objects requires SeCreateGlobalPrivilege, which by default is only granted to administrators and service accounts.

Also, the Windows API has no capability to create permanent objects, so this would require the NT API functions NtMakePermanentObject (undocumented, added in NT 5.1) and NtMakeTemporaryObject.
msg350156 - (view) Author: Vinay Sharma (vinay0410) * Date: 2019-08-22 05:17
> In terms of providing "consistent behavior across platforms that can be
> reasonably supported", the behavior suggested above could not
> reasonably be supported in Windows.

I understand that persistence of a shared memory segment after all the processes using it exit, can be very difficult on Windows.

But, after testing shared_memory on Windows, the behavior on Windows and Unix is not consistent at the moment.

For instance:
Let's say a three processes P1, P2 and P3 are trying to communicate using shared memory.
 --> P1 creates the shared memory block, and waits for P2 and P3 to access it.
 --> P2 starts and attaches this shared memory segment, writes some data to it and exits.
 --> Now in case of Unix, shm_unlink is called as soon as P2 exits.
 --> Now, P3 starts and tries to attach the shared memory segment.
 --> P3 will not be able to attach the shared memory segment in Unix, because shm_unlink has been called on that segment.
 --> Whereas, P3 will be able to attach to the shared memory segment in Windows

One possible solution can be, to register the shared the shared_memory only when it's created and not when it's attached.

I think that might make Unix's implementation more consistent with windows.

Any thoughts on the same will be very helpful.
msg350161 - (view) Author: Eryk Sun (eryksun) * (Python triager) Date: 2019-08-22 06:06
> register the shared the shared_memory only when it's created and 
> not when it's attached.

In Windows, the section object is reference counted. I haven't looked into the Unix implementation, but maybe it could use advisory locking. After opening shared memory via shm_open, a process would try to acquire a shared lock on the fd. If it can't acquire the lock, close the fd and fail the open. When exiting, a process would remove its shared lock and try to acquire an exclusive lock. If it acquires an exclusive lock, it should call shm_unlink because it's the last reference.
msg350379 - (view) Author: Vinay Sharma (vinay0410) * Date: 2019-08-24 17:12
Hi,
I just opened a PR implementing a fix very similar to your suggestions. I am using advisory locking using fcntl.flock.
And I am locking on file descriptors.
If you see my PR, in resource tracker I am opening a file "/dev/shm/<shm_name>", and trying to acquire exclusive lock on the same.
And it's working great on Linux.
Since, resource_tracker is spawned as a different process, I can't directly use file descriptors.

But macOS doesn't have any memory mapped files created by shm_open in /dev/shm. In fact, it doesn't store any reference to memory mapped files in the filesystem.

Therefore it get's difficult to get the file descriptor in resource tracker.
Also, is there a good way to pass file descriptors between processes.

Any ideas on the above issue will be much appreciated.
msg350381 - (view) Author: Vinay Sharma (vinay0410) * Date: 2019-08-24 17:24
Also, shm_open returns an integer file descriptor.
And when this file descriptor is passed too fcntl.flock (in macOS) it throws the following error:
OSError: [Errno 45] Operation not supported

Whereas, the same code works fine on linux.

Therefore, I have doubts whether Macos flock implementation support locking shared_memory files.
msg350529 - (view) Author: Vinay Sharma (vinay0410) * Date: 2019-08-26 12:42
Since, advisory locking doesn't work on integer file descriptors which are returned by shm_open on macos, I was thinking of an alternative way of fixing this.

I was thinking of using a shared semaphore, which will store the reference count of the processes using the shared memory segment.
resource_tracker will unlink the shared_memory and the shared semaphore, when the count stored by shared semaphore becomes 0.
This will ensure that neither the shared memory segment nor the shared semaphore leaks.

Does this sound good ?
Any suggestions would be very helpful.
msg351432 - (view) Author: Davin Potts (davin) * (Python committer) Date: 2019-09-09 11:31
A shared semaphore approach for the resource tracker sounds appealing as a way to make the behavior on Windows and posix systems more consistent.  However this might get implemented, we should not artificially prevent users from having some option to persist beyond the last Python process's exit.

I like the point that @eryksun makes that we could instead consider using NtMakePermanentObject on Windows to permit more posix-like behavior instead, but I do not think we want to head down a path of using undocumented NT APIs.

In the current code, the resource tracker inappropriately triggers _posixshmem.shm_unlink; we need to fix this in the immediate short term (before 3.8 is released) as it breaks the expected behavior @vinay0410 describes.
msg351445 - (view) Author: Vinay Sharma (vinay0410) * Date: 2019-09-09 12:46
Hi @davin,
I researched on lots of approaches to solve this problem, and I have listed down some of the best ones.

1. As Eryk Sun suggested initially to use advisory locking to implement a reference count type of mechanism. I implemented this in the current Pull Request already open. And it works great on most platforms, but fails on MacOS. This is because MacOS makes no file-system entry like Linux in /dev/shm. In fact it makes no filesystem entry for the created shared memory segment. Therefore this won't work.



2. I thought of creating a manual file entry for the created shared memory segment in /tmp in MacOS. This will work just fine unless the user manually changes the permissions of /tmp directory on MacOS. And we will have to rely on the fact that /tmp is writable if we use this approach.



3. Shared Semaphores: This is a very interesting approach to implement reference count, where a semaphore is created corresponding to every shared memory segment, which keeps reference count of the shared memory.
Resource Tracker will clear the shared memory segment and the shared semaphore as soon as the value of the shared semaphore becomes 0. 

The only problem with this approach is to decide the name of shared semaphore. We will have to define a standardised way to get extract shared semaphore's name from shared segment's name.

For instance, shared_semaphore_name = 'psem' + shared_segment_name.
This can cause problems if a semaphore with shared_semaphore_name is already present.

This could be solved by taking any available name and storing it inside shared memory segment upon creation, and then extracting this name to open the shared semaphore.



4. Another way could be to initialize a semaphore by sem_init() inside the shared memory segment. For example the first 4 bytes can be reserved for semaphore. But, since MacOS has deprecated sem_init(), it wouldn't be a good practice.



5. Atomic Calls: I think this is the most simple and best way to solve the above problem. We can reserve first 4 bytes for an integer which is nothing but the reference count of shared memory, and to prevent data races we could use atomic calls to update it.
gcc has inbuilt support for some atomic operations. I have tested them using processes by updating them inside shared memory. And they work great.

Following are some atomic calls:

type __sync_add_and_fetch (type *ptr, type value, ...)
type __sync_sub_and_fetch (type *ptr, type value, ...)
type __sync_or_and_fetch (type *ptr, type value, ...)

Documentation of these can be found at below links:
https://gcc.gnu.org/onlinedocs/gcc/_005f_005fsync-Builtins.html#g_t_005f_005fsync-Builtins
https://gcc.gnu.org/onlinedocs/gcc/_005f_005fatomic-Builtins.html#g_t_005f_005fatomic-Builtins
msg351962 - (view) Author: Davin Potts (davin) * (Python committer) Date: 2019-09-11 16:00
I have created issue38119 to track a fix to the inappropriate use of resource tracker with shared memory segments, but this does not replace or supersede what is discussed here.
History
Date User Action Args
2019-09-11 16:00:52davinsetmessages: + msg351962
2019-09-09 12:46:46vinay0410setmessages: + msg351445
2019-09-09 11:31:19davinsetmessages: + msg351432
2019-08-26 12:42:21vinay0410setmessages: + msg350529
2019-08-24 17:24:33vinay0410setmessages: + msg350381
2019-08-24 17:12:47vinay0410setmessages: + msg350379
title: Persistence of Shared Memory Segment after process exits -> Consistency of Unix's shared_memory implementation with windows
2019-08-24 15:03:42vinay0410setkeywords: + patch
stage: patch review
pull_requests: + pull_request15154
2019-08-24 06:01:20vinay0410setversions: + Python 3.9
2019-08-22 06:06:42eryksunsetmessages: + msg350161
2019-08-22 05:17:38vinay0410setmessages: + msg350156
2019-08-20 15:16:26eryksunsetnosy: + eryksun
messages: + msg350011
2019-08-20 12:40:07vinay0410settype: enhancement -> behavior
2019-08-20 04:11:18vinay0410setmessages: + msg349987
2019-08-20 03:51:22vinay0410settitle: alter size of segment using multiprocessing.shared_memory -> Persistence of Shared Memory Segment after process exits
2019-08-17 16:47:51davinsetmessages: + msg349902
2019-08-04 10:32:38vinay0410create