process_shared 0.0.3 → 0.0.4
Sign up to get free protection for your applications and to get access to all the features.
- 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
|