Add Ruby/FFI wrapper around POSIX shared memory and semaphore (obviating libpsem).
This commit is contained in:
parent
b58a1a7cda
commit
ae67dc6889
|
@ -0,0 +1,40 @@
|
|||
require 'ffi'
|
||||
|
||||
module ProcessShared
|
||||
module Posix
|
||||
module Errno
|
||||
extend FFI::Library
|
||||
|
||||
ffi_lib FFI::Library::LIBC
|
||||
|
||||
attach_variable :errno, :int
|
||||
|
||||
# Replace methods in +syms+ with error checking wrappers that
|
||||
# invoke the original method and raise a {SystemCallError} with
|
||||
# the current errno if the return value is an error.
|
||||
#
|
||||
# Errors are detected if the block returns true when called with
|
||||
# the original method's return value.
|
||||
def error_check(*syms, &is_err)
|
||||
unless block_given?
|
||||
is_err = lambda { |v| (v == -1) }
|
||||
end
|
||||
|
||||
syms.each do |sym|
|
||||
method = self.method(sym)
|
||||
new_method_body = proc do |*args|
|
||||
ret = method.call(*args)
|
||||
if is_err.call(ret)
|
||||
raise SystemCallError.new("error in #{sym}", Errno.errno)
|
||||
else
|
||||
ret
|
||||
end
|
||||
end
|
||||
|
||||
define_singleton_method(sym, &new_method_body)
|
||||
define_method(sym, &new_method_body)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,78 @@
|
|||
require 'ffi'
|
||||
|
||||
require 'process_shared/posix/errno'
|
||||
require 'process_shared/posix/time_val'
|
||||
|
||||
module ProcessShared
|
||||
module Posix
|
||||
module LibC
|
||||
module Helper
|
||||
extend FFI::Library
|
||||
|
||||
# Workaround FFI dylib/bundle issue. See https://github.com/ffi/ffi/issues/42
|
||||
suffix = if FFI::Platform.mac?
|
||||
'bundle'
|
||||
else
|
||||
FFI::Platform::LIBSUFFIX
|
||||
end
|
||||
|
||||
ffi_lib File.join(File.expand_path(File.dirname(__FILE__)),
|
||||
'helper.' + suffix)
|
||||
|
||||
[:o_rdwr,
|
||||
:o_creat,
|
||||
:o_excl,
|
||||
|
||||
:prot_read,
|
||||
:prot_write,
|
||||
:prot_exec,
|
||||
:prot_none,
|
||||
|
||||
:map_shared,
|
||||
:map_private].each do |sym|
|
||||
attach_variable sym, :int
|
||||
end
|
||||
|
||||
[:sizeof_sem_t].each do |sym|
|
||||
attach_variable sym, :size_t
|
||||
end
|
||||
end
|
||||
|
||||
extend FFI::Library
|
||||
extend Errno
|
||||
|
||||
ffi_lib FFI::Library::LIBC
|
||||
|
||||
MAP_FAILED = FFI::Pointer.new(-1)
|
||||
MAP_SHARED = Helper.map_shared
|
||||
MAP_PRIVATE = Helper.map_private
|
||||
|
||||
PROT_READ = Helper.prot_read
|
||||
PROT_WRITE = Helper.prot_write
|
||||
PROT_EXEC = Helper.prot_exec
|
||||
PROT_NONE = Helper.prot_none
|
||||
|
||||
O_RDWR = Helper.o_rdwr
|
||||
O_CREAT = Helper.o_creat
|
||||
O_EXCL = Helper.o_excl
|
||||
|
||||
def self.type_size(type)
|
||||
case type
|
||||
when :sem_t
|
||||
Helper.sizeof_sem_t
|
||||
else
|
||||
FFI.type_size(type)
|
||||
end
|
||||
end
|
||||
|
||||
attach_function :mmap, [:pointer, :size_t, :int, :int, :int, :off_t], :pointer
|
||||
attach_function :munmap, [:pointer, :size_t], :int
|
||||
attach_function :ftruncate, [:int, :off_t], :int
|
||||
attach_function :close, [:int], :int
|
||||
attach_function :gettimeofday, [TimeVal, :pointer], :int
|
||||
|
||||
error_check(:mmap) { |v| v == MAP_FAILED }
|
||||
error_check(:munmap, :ftruncate, :close, :gettimeofday)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,154 @@
|
|||
require 'process_shared/posix/errno'
|
||||
require 'process_shared/posix/libc'
|
||||
require 'process_shared/posix/time_val'
|
||||
require 'process_shared/posix/time_spec'
|
||||
require 'process_shared/with_self'
|
||||
|
||||
module ProcessShared
|
||||
module Posix
|
||||
class Semaphore
|
||||
module Foreign
|
||||
extend FFI::Library
|
||||
extend Errno
|
||||
|
||||
ffi_lib 'rt' # 'pthread'
|
||||
|
||||
typedef :pointer, :sem_p
|
||||
|
||||
attach_function :sem_open, [:string, :int], :sem_p
|
||||
attach_function :sem_close, [:sem_p], :int
|
||||
attach_function :sem_unlink, [:string], :int
|
||||
|
||||
attach_function :sem_init, [:sem_p, :int, :uint], :int
|
||||
attach_function :sem_destroy, [:sem_p], :int
|
||||
|
||||
attach_function :sem_getvalue, [:sem_p, :pointer], :int
|
||||
attach_function :sem_post, [:sem_p], :int
|
||||
attach_function :sem_wait, [:sem_p], :int
|
||||
attach_function :sem_trywait, [:sem_p], :int
|
||||
attach_function :sem_timedwait, [:sem_p, TimeSpec], :int
|
||||
|
||||
error_check(:sem_close, :sem_unlink, :sem_init, :sem_destroy,
|
||||
:sem_getvalue, :sem_post, :sem_wait, :sem_trywait,
|
||||
:sem_timedwait)
|
||||
end
|
||||
|
||||
include Foreign
|
||||
include ProcessShared::WithSelf
|
||||
|
||||
# Make a Proc suitable for use as a finalizer that will call
|
||||
# +shm_unlink+ on +sem+.
|
||||
#
|
||||
# @return [Proc] a finalizer
|
||||
def self.make_finalizer(sem)
|
||||
proc { LibC.shm_unlink(sem) }
|
||||
end
|
||||
|
||||
# With no associated block, open is a synonym for
|
||||
# Semaphore.new. If the optional code block is given, it will be
|
||||
# passed +sem+ as an argument, and the Semaphore object will
|
||||
# automatically be closed when the block terminates. In this
|
||||
# instance, Semaphore.open returns the value of the block.
|
||||
#
|
||||
# @param [Integer] value the initial semaphore value
|
||||
def self.open(value = 1, &block)
|
||||
new(value).with_self(&block)
|
||||
end
|
||||
|
||||
# Create a new semaphore with initial value +value+. After
|
||||
# Kernel#fork, the semaphore will be shared across two (or more)
|
||||
# processes. The semaphore must be closed with #close in each
|
||||
# process that no longer needs the semaphore.
|
||||
#
|
||||
# (An object finalizer is registered that will close the semaphore
|
||||
# to avoid memory leaks, but this should be considered a last
|
||||
# resort).
|
||||
#
|
||||
# @param [Integer] value the initial semaphore value
|
||||
def initialize(value)
|
||||
@sem = SharedMemory.new(LibC.type_size(:sem_t))
|
||||
sem_init(@sem, 1, value)
|
||||
ObjectSpace.define_finalizer(self, self.class.make_finalizer(@sem))
|
||||
end
|
||||
|
||||
# Get the current value of the semaphore. Raises {Errno::NOTSUP} on
|
||||
# platforms that don't support this (e.g. Mac OS X).
|
||||
#
|
||||
# @return [Integer] the current value of the semaphore.
|
||||
def value
|
||||
int = FFI::MemoryPointer.new(:int)
|
||||
sem_getvalue(@sem, int)
|
||||
int.read_int
|
||||
end
|
||||
|
||||
# Increment the value of the semaphore. If other processes are
|
||||
# waiting on this semaphore, one will be woken.
|
||||
def post
|
||||
sem_post(@sem)
|
||||
end
|
||||
|
||||
# Decrement the value of the semaphore. If the value is zero,
|
||||
# wait until another process increments via {#post}.
|
||||
def wait
|
||||
sem_wait(@sem)
|
||||
end
|
||||
|
||||
NS_PER_S = 1e9
|
||||
US_PER_NS = 1000
|
||||
TV_NSEC_MAX = (NS_PER_S - 1)
|
||||
|
||||
# Decrement the value of the semaphore if it can be done
|
||||
# immediately (i.e. if it was non-zero). Otherwise, wait up to
|
||||
# +timeout+ seconds until another process increments via {#post}.
|
||||
#
|
||||
# @param timeout [Numeric] the maximum seconds to wait, or nil to not wait
|
||||
#
|
||||
# @return If +timeout+ is nil and the semaphore cannot be
|
||||
# decremented immediately, raise Errno::EAGAIN. If +timeout+
|
||||
# passed before the semaphore could be decremented, raise
|
||||
# Errno::ETIMEDOUT.
|
||||
def try_wait(timeout = nil)
|
||||
if timeout
|
||||
now = TimeVal.new
|
||||
abs_timeout = TimeSpec.new
|
||||
|
||||
LibC.gettimeofday(now, nil)
|
||||
|
||||
abs_timeout[:tv_sec] = now[:tv_sec];
|
||||
abs_timeout[:tv_nsec] = now[:tv_usec] * US_PER_NS
|
||||
|
||||
# add timeout in seconds to abs_timeout; careful with rounding
|
||||
sec = timeout.floor
|
||||
nsec = ((timeout - sec) * NS_PER_S).floor
|
||||
|
||||
abs_timeout[:tv_sec] += sec
|
||||
abs_timeout[:tv_nsec] += nsec
|
||||
while abs_timeout[:tv_nsec] > TV_NSEC_MAX
|
||||
abs_timeout[:tv_sec] += 1
|
||||
abs_timeout[:tv_nsec] -= NS_PER_S
|
||||
end
|
||||
|
||||
sem_timedwait(@sem, abs_timeout)
|
||||
else
|
||||
sem_trywait(@sem)
|
||||
end
|
||||
end
|
||||
|
||||
# Close the shared memory block holding the semaphore.
|
||||
#
|
||||
# FIXME: May leak the semaphore memory on some platforms,
|
||||
# according to the Linux man page for sem_destroy(3). (Should not
|
||||
# be destroyed as it may be in use by other processes.)
|
||||
def close
|
||||
# sem_destroy(@sem)
|
||||
|
||||
# Not entirely sure what to do here. sem_destroy() goes with
|
||||
# sem_init() (unnamed semaphroe), but other processes cannot use
|
||||
# a destroyed semaphore.
|
||||
@sem.close
|
||||
@sem = nil
|
||||
ObjectSpace.undefine_finalizer(self)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,137 @@
|
|||
require 'ffi'
|
||||
|
||||
require 'process_shared/posix/errno'
|
||||
require 'process_shared/posix/libc'
|
||||
require 'process_shared/shared_memory_io'
|
||||
require 'process_shared/with_self'
|
||||
|
||||
module ProcessShared
|
||||
module Posix
|
||||
# Memory block shared across processes.
|
||||
class SharedMemory < FFI::Pointer
|
||||
module Foreign
|
||||
extend FFI::Library
|
||||
extend Errno
|
||||
|
||||
# FIXME: mac and linux OK, but what about everything else?
|
||||
if FFI::Platform.mac?
|
||||
ffi_lib 'c'
|
||||
else
|
||||
ffi_lib 'rt'
|
||||
end
|
||||
|
||||
attach_function :shm_open, [:string, :int, :mode_t], :int
|
||||
attach_function :shm_unlink, [:string], :int
|
||||
|
||||
error_check :shm_open, :shm_unlink
|
||||
end
|
||||
|
||||
include SharedMemory::Foreign
|
||||
include LibC
|
||||
|
||||
include ProcessShared::WithSelf
|
||||
|
||||
attr_reader :size, :type, :type_size, :count, :fd
|
||||
|
||||
def self.open(size, &block)
|
||||
new(size).with_self(&block)
|
||||
end
|
||||
|
||||
def self.make_finalizer(addr, size, fd)
|
||||
proc do
|
||||
pointer = FFI::Pointer.new(addr)
|
||||
LibC.munmap(pointer, size)
|
||||
LibC.close(fd)
|
||||
end
|
||||
end
|
||||
|
||||
def initialize(type_or_count = 1, count = 1)
|
||||
@type, @count = case type_or_count
|
||||
when Symbol
|
||||
[type_or_count, count]
|
||||
else
|
||||
[:uchar, type_or_count]
|
||||
end
|
||||
|
||||
@type_size = FFI.type_size(@type)
|
||||
@size = @type_size * @count
|
||||
|
||||
name = "/ps-shm#{rand(10000)}"
|
||||
@fd = shm_open(name,
|
||||
O_CREAT | O_RDWR | O_EXCL,
|
||||
0777)
|
||||
shm_unlink(name)
|
||||
|
||||
ftruncate(@fd, @size)
|
||||
@pointer = mmap(nil,
|
||||
@size,
|
||||
LibC::PROT_READ | LibC::PROT_WRITE,
|
||||
LibC::MAP_SHARED,
|
||||
@fd,
|
||||
0).
|
||||
slice(0, size) # slice to get FFI::Pointer that knows its size
|
||||
# (and thus does bounds checking)
|
||||
|
||||
@finalize = self.class.make_finalizer(@pointer.address, @size, @fd)
|
||||
ObjectSpace.define_finalizer(self, @finalize)
|
||||
|
||||
super(@pointer)
|
||||
end
|
||||
|
||||
# Write the serialization of +obj+ (using Marshal.dump) to this
|
||||
# shared memory object at +offset+ (in bytes).
|
||||
#
|
||||
# Raises IndexError if there is insufficient space.
|
||||
def put_object(offset, obj)
|
||||
# FIXME: This is a workaround to an issue I'm seeing in
|
||||
# 1.8.7-p352 (not tested in other 1.8's). If I used the code
|
||||
# below that works in 1.9, then inside SharedMemoryIO#write, the
|
||||
# passed string object is 'terminated' (garbage collected?) and
|
||||
# won't respond to any methods... This less efficient since it
|
||||
# involves the creation of an intermediate string, but it works
|
||||
# in 1.8.7-p352.
|
||||
if RUBY_VERSION =~ /^1.8/
|
||||
str = Marshal.dump(obj)
|
||||
return put_bytes(offset, str, 0, str.size)
|
||||
end
|
||||
|
||||
io = to_shm_io
|
||||
io.seek(offset)
|
||||
Marshal.dump(obj, io)
|
||||
end
|
||||
|
||||
# Read the serialized object at +offset+ (in bytes) using
|
||||
# Marshal.load.
|
||||
#
|
||||
# @return [Object]
|
||||
def get_object(offset)
|
||||
io = to_shm_io
|
||||
io.seek(offset)
|
||||
Marshal.load(io)
|
||||
end
|
||||
|
||||
# Equivalent to {#put_object(0, obj)}
|
||||
def write_object(obj)
|
||||
put_object(0, obj)
|
||||
end
|
||||
|
||||
# Equivalent to {#read_object(0, obj)}
|
||||
#
|
||||
# @return [Object]
|
||||
def read_object
|
||||
Marshal.load(to_shm_io)
|
||||
end
|
||||
|
||||
def close
|
||||
ObjectSpace.undefine_finalizer(self)
|
||||
@finalize.call
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def to_shm_io
|
||||
ProcessShared::SharedMemoryIO.new(self)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,10 @@
|
|||
require 'ffi'
|
||||
|
||||
module ProcessShared
|
||||
module Posix
|
||||
class TimeSpec < FFI::Struct
|
||||
layout(:tv_sec, :time_t,
|
||||
:tv_nsec, :long)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,10 @@
|
|||
require 'ffi'
|
||||
|
||||
module ProcessShared
|
||||
module Posix
|
||||
class TimeVal < FFI::Struct
|
||||
layout(:tv_sec, :time_t,
|
||||
:tv_usec, :suseconds_t)
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in New Issue