process_shared 0.0.3 → 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/README.rdoc +25 -2
- data/ext/libpsem/psem_posix.c +34 -4
- data/lib/process_shared/abstract_semaphore.rb +22 -2
- data/lib/process_shared/binary_semaphore.rb +48 -0
- data/lib/process_shared/bounded_semaphore.rb +5 -5
- data/lib/process_shared/condition_variable.rb +30 -4
- data/lib/process_shared/libpsem.so +0 -0
- data/lib/process_shared/mutex.rb +15 -15
- data/lib/process_shared/posix_call.rb +3 -3
- data/lib/process_shared/psem.rb +7 -3
- data/lib/process_shared/semaphore.rb +23 -5
- data/lib/process_shared/shared_array.rb +69 -0
- data/lib/process_shared/shared_memory.rb +80 -12
- data/lib/process_shared/shared_memory_io.rb +321 -0
- data/lib/process_shared/with_self.rb +2 -2
- data/spec/process_shared/binary_semaphore_spec.rb +13 -0
- data/spec/process_shared/condition_variable_spec.rb +67 -0
- data/spec/process_shared/semaphore_spec.rb +41 -0
- data/spec/process_shared/shared_array_spec.rb +26 -0
- data/spec/process_shared/shared_memory_spec.rb +25 -0
- data/spec/spec_helper.rb +11 -2
- metadata +70 -82
data/README.rdoc
CHANGED
@@ -59,12 +59,35 @@ Install the gem with:
|
|
59
59
|
|
60
60
|
puts "value should be zero: #{mem.get_int(0)}"
|
61
61
|
|
62
|
+
== Transfer Objects Across Processes
|
63
|
+
|
64
|
+
# allocate a sufficient memory block
|
65
|
+
mem = ProcessShared::SharedMemory.new(1024)
|
66
|
+
|
67
|
+
# sub process can write (serialize) object to memory (with bounds checking)
|
68
|
+
pid = fork do
|
69
|
+
mem.write_object(['a', 'b'])
|
70
|
+
end
|
71
|
+
|
72
|
+
Process.wait(pid)
|
73
|
+
|
74
|
+
# parent process can read the object back (synchronizing access
|
75
|
+
# with a Mutex left as an excercie to reader)
|
76
|
+
|
77
|
+
mem.read_object.must_equal ['a', 'b']
|
78
|
+
|
62
79
|
== Todo
|
63
80
|
|
64
|
-
*
|
81
|
+
* Test ConditionVariable
|
65
82
|
* Implement optional override of core Thread/Mutex classes
|
66
83
|
* Extend libpsem to win32? (See Python's processing library)
|
67
84
|
* Break out tests that use PSem.getvalue() (which isn't supported on Mac OS X)
|
68
85
|
so that the test suite will pass
|
69
86
|
* Add finalizer to Mutex? (finalizer on Semaphore objects may be enough) or a method to
|
70
|
-
explicitly close and release resources?
|
87
|
+
explicitly close and release resources?
|
88
|
+
* Test semantics of crashing processes who still hold locks, etc.
|
89
|
+
* Is SharedArray with Enumerable mixing sufficient Array-like interface?
|
90
|
+
* Remove bsem from libpsem as it is of little use and doesn't work on Mac OS X
|
91
|
+
* Possibly implement BoundedSemaphore with arbitrary bound (in Ruby
|
92
|
+
rather than relying on sem_getvalue()), but this is of little
|
93
|
+
utility beyond extra error checking..
|
data/ext/libpsem/psem_posix.c
CHANGED
@@ -68,7 +68,6 @@ psem_free(psem_t *psem) {
|
|
68
68
|
error_new((err), E_SOURCE_SYSTEM, errno); \
|
69
69
|
return ERROR; \
|
70
70
|
} \
|
71
|
-
return OK; \
|
72
71
|
} while (0)
|
73
72
|
|
74
73
|
#define errcheck(expr, err) errcheck_val((expr), -1, (err))
|
@@ -79,52 +78,83 @@ psem_open(psem_t *psem, const char *name, unsigned int value, error_t **err)
|
|
79
78
|
errcheck_val(psem->sem = sem_open(name, O_CREAT | O_EXCL, 0600, value),
|
80
79
|
SEM_FAILED,
|
81
80
|
err);
|
81
|
+
return OK;
|
82
82
|
}
|
83
83
|
|
84
84
|
int
|
85
85
|
psem_close(psem_t *psem, error_t **err)
|
86
86
|
{
|
87
87
|
errcheck(sem_close(psem->sem), err);
|
88
|
+
return OK;
|
88
89
|
}
|
89
90
|
|
90
91
|
int
|
91
92
|
psem_unlink(const char *name, error_t **err)
|
92
93
|
{
|
93
94
|
errcheck(sem_unlink(name), err);
|
95
|
+
return OK;
|
94
96
|
}
|
95
97
|
|
96
98
|
int
|
97
99
|
psem_post(psem_t *psem, error_t **err)
|
98
100
|
{
|
99
101
|
errcheck(sem_post(psem->sem), err);
|
102
|
+
return OK;
|
100
103
|
}
|
101
104
|
|
102
105
|
int
|
103
106
|
psem_wait(psem_t *psem, error_t **err)
|
104
107
|
{
|
105
108
|
errcheck(sem_wait(psem->sem), err);
|
109
|
+
return OK;
|
106
110
|
}
|
107
111
|
|
108
112
|
int
|
109
113
|
psem_trywait(psem_t *psem, error_t **err)
|
110
114
|
{
|
111
115
|
errcheck(sem_trywait(psem->sem), err);
|
116
|
+
return OK;
|
112
117
|
}
|
113
118
|
|
119
|
+
#define NS_PER_S (1000 * 1000 * 1000)
|
120
|
+
#define US_PER_NS (1000)
|
121
|
+
#define TV_NSEC_MAX (NS_PER_S - 1)
|
122
|
+
|
114
123
|
int
|
115
124
|
psem_timedwait(psem_t *psem, float timeout_s, error_t **err)
|
116
125
|
{
|
126
|
+
struct timeval now;
|
117
127
|
struct timespec abs_timeout;
|
118
128
|
|
119
|
-
|
120
|
-
abs_timeout.
|
121
|
-
|
129
|
+
errcheck(gettimeofday(&now, NULL), err);
|
130
|
+
abs_timeout.tv_sec = now.tv_sec;
|
131
|
+
abs_timeout.tv_nsec = now.tv_usec * US_PER_NS;
|
132
|
+
|
133
|
+
/* Fun with rounding: careful adding reltive timeout to abs time */
|
134
|
+
{
|
135
|
+
time_t sec; /* relative timeout */
|
136
|
+
long nsec;
|
137
|
+
|
138
|
+
sec = floorf(timeout_s);
|
139
|
+
nsec = floorf((timeout_s - floorf(timeout_s)) * NS_PER_S);
|
140
|
+
|
141
|
+
abs_timeout.tv_sec += sec;
|
142
|
+
abs_timeout.tv_nsec += nsec;
|
143
|
+
|
144
|
+
while (abs_timeout.tv_nsec > TV_NSEC_MAX) {
|
145
|
+
abs_timeout.tv_sec += 1;
|
146
|
+
abs_timeout.tv_nsec -= NS_PER_S;
|
147
|
+
}
|
148
|
+
}
|
122
149
|
|
123
150
|
errcheck(sem_timedwait(psem->sem, &abs_timeout), err);
|
151
|
+
return OK;
|
124
152
|
}
|
125
153
|
|
126
154
|
int
|
127
155
|
psem_getvalue(psem_t *psem, int *sval, error_t **err)
|
128
156
|
{
|
129
157
|
errcheck(sem_getvalue(psem->sem, sval), err);
|
158
|
+
return OK;
|
130
159
|
}
|
160
|
+
|
@@ -8,23 +8,43 @@ module ProcessShared
|
|
8
8
|
include ProcessShared::PSem
|
9
9
|
public
|
10
10
|
|
11
|
-
# Generate a name for a semaphore.
|
11
|
+
# Generate a name for a semaphore. If +name+ is given, it is used
|
12
|
+
# as the name (and so a semaphore could be shared by arbitrary
|
13
|
+
# processes not forked from one another). Otherwise, a name is
|
14
|
+
# generated containing +middle+ and the process id.
|
15
|
+
#
|
16
|
+
# @param [String] middle arbitrary string used in the middle
|
17
|
+
# @param [String] name if given, used as the name
|
18
|
+
# @return [String] name, or the generated name
|
12
19
|
def self.gen_name(middle, name = nil)
|
13
20
|
if name
|
14
21
|
name
|
15
22
|
else
|
16
23
|
@count ||= 0
|
17
24
|
@count += 1
|
18
|
-
"ps-#{middle}-#{Process.pid}-#{@count}"
|
25
|
+
"ps-#{middle}-#{::Process.pid}-#{@count}"
|
19
26
|
end
|
20
27
|
end
|
21
28
|
|
29
|
+
# Make a Proc suitable for use as a finalizer that will call
|
30
|
+
# +psem_unlink+ on +name+ and ignore system errors.
|
31
|
+
#
|
32
|
+
# @return [Proc] a finalizer
|
22
33
|
def self.make_finalizer(name)
|
23
34
|
proc { ProcessShared::PSem.psem_unlink(name, nil) }
|
24
35
|
end
|
25
36
|
|
26
37
|
# private_class_method :new
|
27
38
|
|
39
|
+
def synchronize
|
40
|
+
wait
|
41
|
+
begin
|
42
|
+
yield
|
43
|
+
ensure
|
44
|
+
post
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
28
48
|
protected
|
29
49
|
|
30
50
|
attr_reader :sem, :err
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'process_shared/psem'
|
2
|
+
require 'process_shared/semaphore'
|
3
|
+
require 'process_shared/process_error'
|
4
|
+
|
5
|
+
module ProcessShared
|
6
|
+
# BinarySemaphore is identical to Semaphore except that its value is
|
7
|
+
# not permitted to rise above one (it may be either zero or one).
|
8
|
+
# When the value is at the maximum, calls to #post will raise an
|
9
|
+
# exception.
|
10
|
+
#
|
11
|
+
# This is identical to a Semaphore but with extra error checking.
|
12
|
+
class BinarySemaphore < Semaphore
|
13
|
+
# Create a new semaphore with initial value +value+. After
|
14
|
+
# {Kernel#fork}, the semaphore will be shared across two (or more)
|
15
|
+
# processes. The semaphore must be closed with {#close} in each
|
16
|
+
# process that no longer needs the semaphore.
|
17
|
+
#
|
18
|
+
# (An object finalizer is registered that will close the semaphore
|
19
|
+
# to avoid memory leaks, but this should be considered a last
|
20
|
+
# resort).
|
21
|
+
#
|
22
|
+
# @param [Integer] value the initial semaphore value
|
23
|
+
# @param [String] name not currently supported
|
24
|
+
def initialize(value = 1, name = nil)
|
25
|
+
raise ArgumentErrror 'value must be 0 or 1' if (value < 0 or value > 1)
|
26
|
+
super(value, name)
|
27
|
+
end
|
28
|
+
|
29
|
+
# Increment from zero to one.
|
30
|
+
#
|
31
|
+
# First, attempt to decrement. If this fails with EAGAIN, the
|
32
|
+
# semaphore was at zero, so continue with the post. If this
|
33
|
+
# succeeds, the semaphore was not at zero, so increment back to
|
34
|
+
# one and raise {ProcesError} (multiple workers may have acquired
|
35
|
+
# the semaphore at this point).
|
36
|
+
def post
|
37
|
+
begin
|
38
|
+
try_wait
|
39
|
+
# oops, value was not zero...
|
40
|
+
psem_post(sem, err)
|
41
|
+
raise ProcessError, 'post would raise value over bound'
|
42
|
+
rescue Errno::EAGAIN
|
43
|
+
# ok, value was zero
|
44
|
+
psem_post(sem, err)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -8,9 +8,9 @@ module ProcessShared
|
|
8
8
|
class BoundedSemaphore < Semaphore
|
9
9
|
# With no associated block, open is a synonym for
|
10
10
|
# Semaphore.new. If the optional code block is given, it will be
|
11
|
-
# passed
|
11
|
+
# passed +sem+ as an argument, and the Semaphore object will
|
12
12
|
# automatically be closed when the block terminates. In this
|
13
|
-
# instance,
|
13
|
+
# instance, BoundedSemaphore.open returns the value of the block.
|
14
14
|
#
|
15
15
|
# @param [Integer] value the initial semaphore value
|
16
16
|
# @param [String] name not currently supported
|
@@ -18,9 +18,9 @@ module ProcessShared
|
|
18
18
|
new(maxvalue, value, name).with_self(&block)
|
19
19
|
end
|
20
20
|
|
21
|
-
# Create a new semaphore with initial value
|
22
|
-
# Kernel#fork, the semaphore will be shared across two (or more)
|
23
|
-
# processes. The semaphore must be closed with #close in each
|
21
|
+
# Create a new semaphore with initial value +value+. After
|
22
|
+
# {Kernel#fork}, the semaphore will be shared across two (or more)
|
23
|
+
# processes. The semaphore must be closed with {#close} in each
|
24
24
|
# process that no longer needs the semaphore.
|
25
25
|
#
|
26
26
|
# (An object finalizer is registered that will close the semaphore
|
@@ -1,14 +1,18 @@
|
|
1
1
|
require 'process_shared/semaphore'
|
2
2
|
|
3
3
|
module ProcessShared
|
4
|
-
# TODO: implement this
|
5
4
|
class ConditionVariable
|
6
5
|
def initialize
|
7
|
-
@
|
6
|
+
@internal = Semaphore.new(1)
|
7
|
+
@waiting = SharedMemory.new(:int)
|
8
|
+
@waiting.write_int(0)
|
9
|
+
@sem = Semaphore.new(0)
|
8
10
|
end
|
9
11
|
|
10
12
|
def broadcast
|
11
|
-
@
|
13
|
+
@internal.synchronize do
|
14
|
+
@waiting.read_int.times { @sem.post }
|
15
|
+
end
|
12
16
|
end
|
13
17
|
|
14
18
|
def signal
|
@@ -18,10 +22,32 @@ module ProcessShared
|
|
18
22
|
def wait(mutex, timeout = nil)
|
19
23
|
mutex.unlock
|
20
24
|
begin
|
21
|
-
|
25
|
+
inc_waiting
|
26
|
+
if timeout
|
27
|
+
begin
|
28
|
+
@sem.try_wait(timeout)
|
29
|
+
rescue Errno::EAGAIN, Errno::ETIMEDOUT
|
30
|
+
# success!
|
31
|
+
end
|
32
|
+
else
|
33
|
+
@sem.wait
|
34
|
+
end
|
35
|
+
dec_waiting
|
22
36
|
ensure
|
23
37
|
mutex.lock
|
24
38
|
end
|
25
39
|
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def inc_waiting(val = 1)
|
44
|
+
@internal.synchronize do
|
45
|
+
@waiting.write_int(@waiting.read_int + val)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def dec_waiting
|
50
|
+
inc_waiting(-1)
|
51
|
+
end
|
26
52
|
end
|
27
53
|
end
|
Binary file
|
data/lib/process_shared/mutex.rb
CHANGED
@@ -1,24 +1,24 @@
|
|
1
|
-
require 'process_shared/
|
1
|
+
require 'process_shared/semaphore'
|
2
2
|
require 'process_shared/with_self'
|
3
3
|
require 'process_shared/shared_memory'
|
4
4
|
require 'process_shared/process_error'
|
5
5
|
|
6
6
|
module ProcessShared
|
7
|
-
# This Mutex class is implemented as a
|
8
|
-
#
|
9
|
-
#
|
10
|
-
# process different from the locking process, or if #lock is
|
11
|
-
# while the process already holds the lock (i.e. the mutex is
|
12
|
-
# re-entrant). This tracking is not without performance cost,
|
13
|
-
# course (current implementation uses
|
14
|
-
# and SharedMemory segment).
|
7
|
+
# This Mutex class is implemented as a Semaphore with a second
|
8
|
+
# internal Semaphore used to track the locking process is tracked.
|
9
|
+
# {ProcessError} is raised if either {#unlock} is called by a
|
10
|
+
# process different from the locking process, or if {#lock} is
|
11
|
+
# called while the process already holds the lock (i.e. the mutex is
|
12
|
+
# not re-entrant). This tracking is not without performance cost,
|
13
|
+
# of course (current implementation uses the additional {Semaphore}
|
14
|
+
# and {SharedMemory} segment).
|
15
15
|
#
|
16
|
-
# The API is intended to be identical to the ::Mutex in the core
|
16
|
+
# The API is intended to be identical to the {::Mutex} in the core
|
17
17
|
# Ruby library.
|
18
18
|
#
|
19
19
|
# TODO: the core Ruby api has no #close method, but this Mutex must
|
20
|
-
# release its
|
21
|
-
#
|
20
|
+
# release its {Semaphore} and {SharedMemory} resources. For now,
|
21
|
+
# rely on the object finalizers of those objects...
|
22
22
|
class Mutex
|
23
23
|
# include WithSelf
|
24
24
|
|
@@ -27,10 +27,10 @@ module ProcessShared
|
|
27
27
|
# end
|
28
28
|
|
29
29
|
def initialize
|
30
|
-
@internal_sem =
|
30
|
+
@internal_sem = Semaphore.new
|
31
31
|
@locked_by = SharedMemory.new(:int)
|
32
32
|
|
33
|
-
@sem =
|
33
|
+
@sem = Semaphore.new
|
34
34
|
end
|
35
35
|
|
36
36
|
# @return [Mutex]
|
@@ -68,7 +68,7 @@ module ProcessShared
|
|
68
68
|
if @locked_by.get_int(0) > 0
|
69
69
|
false # was locked
|
70
70
|
else
|
71
|
-
@sem.wait
|
71
|
+
@sem.wait # should return immediately
|
72
72
|
self.locked_by = ::Process.pid
|
73
73
|
true
|
74
74
|
end
|
@@ -2,9 +2,9 @@
|
|
2
2
|
|
3
3
|
module ProcessShared
|
4
4
|
module PosixCall
|
5
|
-
# Replace methods in
|
6
|
-
# invoke the original method and raise a SystemCallError with
|
7
|
-
# current errno if the return value is an error.
|
5
|
+
# Replace methods in +syms+ with error checking wrappers that
|
6
|
+
# invoke the original method and raise a {SystemCallError} with
|
7
|
+
# the current errno if the return value is an error.
|
8
8
|
#
|
9
9
|
# Errors are detected if the block returns true when called with
|
10
10
|
# the original method's return value.
|
data/lib/process_shared/psem.rb
CHANGED
@@ -23,6 +23,9 @@ module ProcessShared
|
|
23
23
|
# Replace methods in `syms` with error checking wrappers that
|
24
24
|
# invoke the original psem method and raise an appropriate
|
25
25
|
# error.
|
26
|
+
#
|
27
|
+
# The last argument is assumed to be a pointer to a pointer
|
28
|
+
# where either a psem error or NULL will be stored.
|
26
29
|
def psem_error_check(*syms)
|
27
30
|
syms.each do |sym|
|
28
31
|
method = self.method(sym)
|
@@ -32,7 +35,8 @@ module ProcessShared
|
|
32
35
|
errp = args[-1]
|
33
36
|
unless errp.nil?
|
34
37
|
begin
|
35
|
-
err = Error.new(errp.
|
38
|
+
err = Error.new(errp.read_pointer)
|
39
|
+
errp.write_pointer(nil)
|
36
40
|
if err[:source] == PSem.e_source_system
|
37
41
|
raise SystemCallError.new("error in #{sym}", err[:errno])
|
38
42
|
else
|
@@ -86,7 +90,7 @@ module ProcessShared
|
|
86
90
|
attach_function :psem_post, [:pointer, :pointer], :int
|
87
91
|
attach_function :psem_wait, [:pointer, :pointer], :int
|
88
92
|
attach_function :psem_trywait, [:pointer, :pointer], :int
|
89
|
-
attach_function :psem_timedwait, [:pointer, :
|
93
|
+
attach_function :psem_timedwait, [:pointer, :float, :pointer], :int
|
90
94
|
attach_function :psem_getvalue, [:pointer, :pointer, :pointer], :int
|
91
95
|
|
92
96
|
psem_error_check(:psem_open, :psem_close, :psem_unlink, :psem_post,
|
@@ -100,7 +104,7 @@ module ProcessShared
|
|
100
104
|
attach_function :bsem_post, [:pointer, :pointer], :int
|
101
105
|
attach_function :bsem_wait, [:pointer, :pointer], :int
|
102
106
|
attach_function :bsem_trywait, [:pointer, :pointer], :int
|
103
|
-
attach_function :bsem_timedwait, [:pointer, :
|
107
|
+
attach_function :bsem_timedwait, [:pointer, :float, :pointer], :int
|
104
108
|
attach_function :bsem_getvalue, [:pointer, :pointer, :pointer], :int
|
105
109
|
|
106
110
|
psem_error_check(:bsem_open, :bsem_close, :bsem_unlink, :bsem_post,
|
@@ -5,7 +5,7 @@ module ProcessShared
|
|
5
5
|
class Semaphore < AbstractSemaphore
|
6
6
|
# With no associated block, open is a synonym for
|
7
7
|
# Semaphore.new. If the optional code block is given, it will be
|
8
|
-
# passed
|
8
|
+
# passed +sem+ as an argument, and the Semaphore object will
|
9
9
|
# automatically be closed when the block terminates. In this
|
10
10
|
# instance, Semaphore.open returns the value of the block.
|
11
11
|
#
|
@@ -15,7 +15,7 @@ module ProcessShared
|
|
15
15
|
new(value, name).with_self(&block)
|
16
16
|
end
|
17
17
|
|
18
|
-
# Create a new semaphore with initial value
|
18
|
+
# Create a new semaphore with initial value +value+. After
|
19
19
|
# Kernel#fork, the semaphore will be shared across two (or more)
|
20
20
|
# processes. The semaphore must be closed with #close in each
|
21
21
|
# process that no longer needs the semaphore.
|
@@ -33,18 +33,36 @@ module ProcessShared
|
|
33
33
|
end
|
34
34
|
|
35
35
|
# Decrement the value of the semaphore. If the value is zero,
|
36
|
-
# wait until another process increments via #post.
|
36
|
+
# wait until another process increments via {#post}.
|
37
37
|
def wait
|
38
38
|
psem_wait(sem, err)
|
39
39
|
end
|
40
40
|
|
41
|
+
# Decrement the value of the semaphore if it can be done
|
42
|
+
# immediately (i.e. if it was non-zero). Otherwise, wait up to
|
43
|
+
# +timeout+ seconds until another process increments via {#post}.
|
44
|
+
#
|
45
|
+
# @param timeout [Numeric] the maximum seconds to wait, or nil to not wait
|
46
|
+
#
|
47
|
+
# @return If +timeout+ is nil and the semaphore cannot be
|
48
|
+
# decremented immediately, raise Errno::EAGAIN. If +timeout+
|
49
|
+
# passed before the semaphore could be decremented, raise
|
50
|
+
# Errno::ETIMEDOUT.
|
51
|
+
def try_wait(timeout = nil)
|
52
|
+
if timeout
|
53
|
+
psem_timedwait(sem, timeout, err)
|
54
|
+
else
|
55
|
+
psem_trywait(sem, err)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
41
59
|
# Increment the value of the semaphore. If other processes are
|
42
60
|
# waiting on this semaphore, one will be woken.
|
43
61
|
def post
|
44
62
|
psem_post(sem, err)
|
45
63
|
end
|
46
64
|
|
47
|
-
# Get the current value of the semaphore. Raises Errno::NOTSUP on
|
65
|
+
# Get the current value of the semaphore. Raises {Errno::NOTSUP} on
|
48
66
|
# platforms that don't support this (e.g. Mac OS X).
|
49
67
|
#
|
50
68
|
# @return [Integer] the current value of the semaphore.
|
@@ -55,7 +73,7 @@ module ProcessShared
|
|
55
73
|
end
|
56
74
|
|
57
75
|
# Release the resources associated with this semaphore. Calls to
|
58
|
-
# other methods are undefined after #close has been called.
|
76
|
+
# other methods are undefined after {#close} has been called.
|
59
77
|
#
|
60
78
|
# Close must be called when the semaphore is no longer needed. An
|
61
79
|
# object finalizer will close the semaphore as a last resort.
|
@@ -0,0 +1,69 @@
|
|
1
|
+
module ProcessShared
|
2
|
+
class SharedArray < SharedMemory
|
3
|
+
include Enumerable
|
4
|
+
|
5
|
+
# A fixed-size array in shared memory. Processes forked from this
|
6
|
+
# one will be able to read and write shared data to the array.
|
7
|
+
# Access should be synchronized using a {Mutex}, {Semaphore}, or
|
8
|
+
# other means.
|
9
|
+
#
|
10
|
+
# Note that {Enumerable} methods such as {#map}, {#sort},
|
11
|
+
# etc. return new {Array} objects rather than modifying the shared
|
12
|
+
# array.
|
13
|
+
#
|
14
|
+
# @param [Symbol] type_or_count the data type as a symbol
|
15
|
+
# understood by FFI (e.g. :int, :double)
|
16
|
+
#
|
17
|
+
# @param [Integer] count number of array elements
|
18
|
+
def initialize(type_or_count = 1, count = 1)
|
19
|
+
super(type_or_count, count)
|
20
|
+
|
21
|
+
# See https://github.com/ffi/ffi/issues/118
|
22
|
+
ffi_type = FFI.find_type(self.type)
|
23
|
+
|
24
|
+
name = if ffi_type.inspect =~ /FFI::Type::Builtin:(\w+)*/
|
25
|
+
# name will be something like int32
|
26
|
+
$1.downcase
|
27
|
+
end
|
28
|
+
|
29
|
+
unless name
|
30
|
+
raise ArgumentError, "could not find FFI::Type for #{self.type}"
|
31
|
+
end
|
32
|
+
|
33
|
+
getter = "get_#{name}"
|
34
|
+
setter = "put_#{name}"
|
35
|
+
|
36
|
+
# singleton class
|
37
|
+
sclass = class << self; self; end
|
38
|
+
|
39
|
+
unless sclass.method_defined?(getter)
|
40
|
+
raise ArgumentError, "no element getter for #{self.type} (#{getter})"
|
41
|
+
end
|
42
|
+
|
43
|
+
unless sclass.method_defined?(setter)
|
44
|
+
raise ArgumentError, "no element setter for #{self.type} (#{setter})"
|
45
|
+
end
|
46
|
+
|
47
|
+
sclass.send(:alias_method, :get_type, getter)
|
48
|
+
sclass.send(:alias_method, :put_type, setter)
|
49
|
+
end
|
50
|
+
|
51
|
+
def each
|
52
|
+
# NOTE: using @count because Enumerable defines its own count
|
53
|
+
# method...
|
54
|
+
@count.times { |i| yield self[i] }
|
55
|
+
end
|
56
|
+
|
57
|
+
def each_with_index
|
58
|
+
@count.times { |i| yield self[i], i }
|
59
|
+
end
|
60
|
+
|
61
|
+
def [](i)
|
62
|
+
get_type(i * self.type_size)
|
63
|
+
end
|
64
|
+
|
65
|
+
def []=(i, val)
|
66
|
+
put_type(i * self.type_size, val)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -1,25 +1,37 @@
|
|
1
1
|
require 'process_shared/rt'
|
2
2
|
require 'process_shared/libc'
|
3
3
|
require 'process_shared/with_self'
|
4
|
+
require 'process_shared/shared_memory_io'
|
4
5
|
|
5
6
|
module ProcessShared
|
6
|
-
# Memory block shared across processes.
|
7
|
+
# Memory block shared across processes.
|
7
8
|
class SharedMemory < FFI::Pointer
|
8
9
|
include WithSelf
|
9
10
|
|
10
|
-
attr_reader :size, :fd
|
11
|
+
attr_reader :size, :type, :type_size, :count, :fd
|
11
12
|
|
12
13
|
def self.open(size, &block)
|
13
14
|
new(size).with_self(&block)
|
14
15
|
end
|
15
16
|
|
16
|
-
def
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
17
|
+
def self.make_finalizer(addr, size, fd)
|
18
|
+
proc do
|
19
|
+
pointer = FFI::Pointer.new(addr)
|
20
|
+
LibC.munmap(pointer, size)
|
21
|
+
LibC.close(fd)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def initialize(type_or_count = 1, count = 1)
|
26
|
+
@type, @count = case type_or_count
|
27
|
+
when Symbol
|
28
|
+
[type_or_count, count]
|
29
|
+
else
|
30
|
+
[:uchar, type_or_count]
|
31
|
+
end
|
32
|
+
|
33
|
+
@type_size = FFI.type_size(@type)
|
34
|
+
@size = @type_size * @count
|
23
35
|
|
24
36
|
name = "/ps-shm#{rand(10000)}"
|
25
37
|
@fd = RT.shm_open(name,
|
@@ -33,13 +45,69 @@ module ProcessShared
|
|
33
45
|
LibC::PROT_READ | LibC::PROT_WRITE,
|
34
46
|
LibC::MAP_SHARED,
|
35
47
|
@fd,
|
36
|
-
0)
|
48
|
+
0).
|
49
|
+
slice(0, size) # slice to get FFI::Pointer that knows its size
|
50
|
+
# (and thus does bounds checking)
|
51
|
+
|
52
|
+
@finalize = self.class.make_finalizer(@pointer.address, @size, @fd)
|
53
|
+
ObjectSpace.define_finalizer(self, @finalize)
|
54
|
+
|
37
55
|
super(@pointer)
|
38
56
|
end
|
39
57
|
|
58
|
+
# Write the serialization of +obj+ (using Marshal.dump) to this
|
59
|
+
# shared memory object at +offset+ (in bytes).
|
60
|
+
#
|
61
|
+
# Raises IndexError if there is insufficient space.
|
62
|
+
def put_object(offset, obj)
|
63
|
+
# FIXME: This is a workaround to an issue I'm seeing in
|
64
|
+
# 1.8.7-p352 (not tested in other 1.8's). If I used the code
|
65
|
+
# below that works in 1.9, then inside SharedMemoryIO#write, the
|
66
|
+
# passed string object is 'terminated' (garbage collected?) and
|
67
|
+
# won't respond to any methods... This way is less efficient
|
68
|
+
# since it involves the creation of an intermediate string, but
|
69
|
+
# it works in 1.8.7-p352.
|
70
|
+
if RUBY_VERSION =~ /^1.8/
|
71
|
+
str = Marshal.dump(obj)
|
72
|
+
return put_bytes(offset, str, 0, str.size)
|
73
|
+
end
|
74
|
+
|
75
|
+
io = SharedMemoryIO.new(self)
|
76
|
+
io.seek(offset)
|
77
|
+
Marshal.dump(obj, io)
|
78
|
+
end
|
79
|
+
|
80
|
+
# Read the serialized object at +offset+ (in bytes) using
|
81
|
+
# Marshal.load.
|
82
|
+
#
|
83
|
+
# @return [Object]
|
84
|
+
def get_object(offset)
|
85
|
+
io = to_shm_io
|
86
|
+
io.seek(offset)
|
87
|
+
Marshal.load(io)
|
88
|
+
end
|
89
|
+
|
90
|
+
# Equivalent to {#put_object(0, obj)}
|
91
|
+
def write_object(obj)
|
92
|
+
put_object(0, obj)
|
93
|
+
end
|
94
|
+
|
95
|
+
# Equivalent to {#read_object(0, obj)}
|
96
|
+
#
|
97
|
+
# @return [Object]
|
98
|
+
def read_object
|
99
|
+
Marshal.load(to_shm_io)
|
100
|
+
end
|
101
|
+
|
40
102
|
def close
|
41
|
-
|
42
|
-
|
103
|
+
ObjectSpace.undefine_finalizer(self)
|
104
|
+
@finalize.call
|
105
|
+
end
|
106
|
+
|
107
|
+
private
|
108
|
+
|
109
|
+
def to_shm_io
|
110
|
+
SharedMemoryIO.new(self)
|
43
111
|
end
|
44
112
|
end
|
45
113
|
end
|