userfaultfd 1.0.0
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.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +142 -0
- data/docs/gvl-and-page-faults.md +58 -0
- data/examples/dirty_tracking.rb +30 -0
- data/examples/lazy_mmap.rb +19 -0
- data/examples/oom_canary.rb +26 -0
- data/examples/post_copy_migration.rb +29 -0
- data/ext/userfaultfd/compat.h +237 -0
- data/ext/userfaultfd/constants.c +102 -0
- data/ext/userfaultfd/extconf.rb +12 -0
- data/ext/userfaultfd/userfaultfd.c +2010 -0
- data/lib/userfaultfd/handler.rb +76 -0
- data/lib/userfaultfd/version.rb +5 -0
- data/lib/userfaultfd.rb +58 -0
- data/sig/userfaultfd.rbs +93 -0
- metadata +59 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class UserfaultFD
|
|
4
|
+
# Owns either a Ruby callback thread or a GVL-free native handler thread.
|
|
5
|
+
class Handler
|
|
6
|
+
attr_reader :error
|
|
7
|
+
|
|
8
|
+
def initialize(uffd, mode: nil, io: nil, source: nil, &block)
|
|
9
|
+
raise ArgumentError, "choose a block or mode, not both" if mode && block
|
|
10
|
+
raise ArgumentError, "a block or mode is required" unless mode || block
|
|
11
|
+
|
|
12
|
+
@uffd = uffd
|
|
13
|
+
if block
|
|
14
|
+
raise DeadlockError, "strict mode cannot verify raw pointer faults" if UserfaultFD.strict
|
|
15
|
+
|
|
16
|
+
warn "userfaultfd: Ruby handlers require Region#read/write or a separate faulting process"
|
|
17
|
+
start_ruby_handler(block)
|
|
18
|
+
else
|
|
19
|
+
native_source = mode == :backing_file ? io : source
|
|
20
|
+
@native = uffd.__send__(:start_native_handler, mode, native_source)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def stop
|
|
25
|
+
if @native
|
|
26
|
+
@native.stop
|
|
27
|
+
return self
|
|
28
|
+
end
|
|
29
|
+
return self unless @thread
|
|
30
|
+
raise Error, "handler cannot stop itself" if Thread.current == @thread
|
|
31
|
+
|
|
32
|
+
@stop_writer.write_nonblock(".") rescue nil
|
|
33
|
+
@thread.join
|
|
34
|
+
@thread = nil
|
|
35
|
+
raise @error if @error
|
|
36
|
+
|
|
37
|
+
self
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def running?
|
|
41
|
+
@native ? @native.running? : !!@thread&.alive?
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def start_ruby_handler(block)
|
|
47
|
+
@stop_reader, @stop_writer = IO.pipe
|
|
48
|
+
@event_reader = @uffd.__send__(:start_event_reader)
|
|
49
|
+
@event_io = IO.for_fd(@event_reader.fileno, autoclose: false)
|
|
50
|
+
@children = []
|
|
51
|
+
@thread = Thread.new do
|
|
52
|
+
loop do
|
|
53
|
+
ready = IO.select([@event_io, @stop_reader])&.first
|
|
54
|
+
break if ready&.include?(@stop_reader)
|
|
55
|
+
|
|
56
|
+
@event_reader.read_events.each do |event|
|
|
57
|
+
@children << event.child_uffd if event.is_a?(ForkEvent)
|
|
58
|
+
block.call(event)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
rescue StandardError => e
|
|
62
|
+
@error = e
|
|
63
|
+
ensure
|
|
64
|
+
begin
|
|
65
|
+
@event_reader.stop
|
|
66
|
+
rescue StandardError => e
|
|
67
|
+
@error ||= e
|
|
68
|
+
end
|
|
69
|
+
@children.each(&:close)
|
|
70
|
+
@stop_reader.close
|
|
71
|
+
@stop_writer.close
|
|
72
|
+
end
|
|
73
|
+
@thread.report_on_exception = false
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
data/lib/userfaultfd.rb
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "userfaultfd/version"
|
|
4
|
+
|
|
5
|
+
# Linux userfaultfd descriptor and capability API.
|
|
6
|
+
#
|
|
7
|
+
# @!method self.supported?(user_mode_only: true)
|
|
8
|
+
# @param user_mode_only [Boolean] restrict faults to userspace; set false only
|
|
9
|
+
# on Linux before 5.11 when the caller has the required privilege
|
|
10
|
+
# @return [Boolean] whether a permitted userfaultfd can be initialized
|
|
11
|
+
# @!method self.features(user_mode_only: true)
|
|
12
|
+
# @return [Array<Symbol>] features available from the running kernel
|
|
13
|
+
# @!method initialize(features: nil, user_mode_only: true)
|
|
14
|
+
# @param features [Array<Symbol>, nil] features to require; by default fork
|
|
15
|
+
# events are attempted and omitted when the kernel denies permission
|
|
16
|
+
# @param user_mode_only [Boolean] restrict faults to userspace
|
|
17
|
+
# @!method register(region, mode:)
|
|
18
|
+
# @param region [UserfaultFD::Region]
|
|
19
|
+
# @param mode [Symbol, Array<Symbol>] `:missing`, `:wp`, or `:minor`
|
|
20
|
+
# @return [Integer] ioctl bitmask supported for the range
|
|
21
|
+
# @!method unregister(region)
|
|
22
|
+
# @return [nil]
|
|
23
|
+
# @!method enabled_features
|
|
24
|
+
# @return [Array<Symbol>] features enabled for this descriptor
|
|
25
|
+
class UserfaultFD
|
|
26
|
+
class Error < StandardError; end
|
|
27
|
+
class UnsupportedError < Error; end
|
|
28
|
+
class DeadlockError < Error; end
|
|
29
|
+
|
|
30
|
+
class << self
|
|
31
|
+
attr_accessor :strict
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
self.strict = false
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
require "userfaultfd/userfaultfd"
|
|
38
|
+
require_relative "userfaultfd/handler"
|
|
39
|
+
|
|
40
|
+
class UserfaultFD
|
|
41
|
+
def to_io
|
|
42
|
+
IO.for_fd(fileno, autoclose: false)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def start_handler(mode: nil, io: nil, source: nil, &block)
|
|
46
|
+
Handler.new(self, mode: mode, io: io, source: source, &block)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# A page-fault event. Resolution methods wake the faulting thread by default.
|
|
51
|
+
class UserfaultFD::Fault
|
|
52
|
+
def write? = flags.include?(:write)
|
|
53
|
+
def wp? = flags.include?(:wp)
|
|
54
|
+
def minor? = flags.include?(:minor)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Compatibility with the constant generated by the original gem skeleton.
|
|
58
|
+
Userfaultfd = UserfaultFD
|
data/sig/userfaultfd.rbs
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
class UserfaultFD
|
|
2
|
+
VERSION: String
|
|
3
|
+
|
|
4
|
+
class Error < StandardError
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
class UnsupportedError < Error
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
class DeadlockError < Error
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
type feature = :pagefault_flag_wp | :event_fork | :event_remap | :event_remove | :event_unmap | :missing_hugetlbfs | :missing_shmem | :sigbus | :thread_id | :minor_hugetlbfs | :minor_shmem | :exact_address | :wp_hugetlbfs_shmem | :wp_unpopulated | :wp_async | :poison | :move
|
|
14
|
+
type register_mode = :missing | :wp | :minor
|
|
15
|
+
type native_mode = :zero_fill | :backing_file | :prefilled
|
|
16
|
+
type event = Fault | ForkEvent | RemapEvent | RemoveEvent | UnmapEvent
|
|
17
|
+
|
|
18
|
+
def self.supported?: (?user_mode_only: bool) -> bool
|
|
19
|
+
def self.features: (?user_mode_only: bool) -> Array[feature]
|
|
20
|
+
def self.strict: () -> bool
|
|
21
|
+
def self.strict=: (bool) -> bool
|
|
22
|
+
|
|
23
|
+
def initialize: (?features: Array[feature]?, ?user_mode_only: bool) -> void
|
|
24
|
+
def features: () -> Array[feature]
|
|
25
|
+
def enabled_features: () -> Array[feature]
|
|
26
|
+
def backend: () -> (:device | :syscall)
|
|
27
|
+
def fileno: () -> Integer
|
|
28
|
+
def to_io: () -> IO
|
|
29
|
+
def close: () -> nil
|
|
30
|
+
def closed?: () -> bool
|
|
31
|
+
def register: (Region, mode: register_mode | Array[register_mode]) -> Integer
|
|
32
|
+
def unregister: (Region) -> nil
|
|
33
|
+
def writeprotect: (Region, enabled: bool) -> nil
|
|
34
|
+
def read_events: (?Integer max) -> Array[event]
|
|
35
|
+
def start_handler: (mode: native_mode, ?io: IO?, ?source: Region?) -> Handler
|
|
36
|
+
| () { (event) -> void } -> Handler
|
|
37
|
+
|
|
38
|
+
class Region
|
|
39
|
+
def initialize: (size: Integer, ?shared: bool, ?huge: bool) -> void
|
|
40
|
+
def address: () -> Integer
|
|
41
|
+
def to_ptr: () -> Integer
|
|
42
|
+
def size: () -> Integer
|
|
43
|
+
def page_size: () -> Integer
|
|
44
|
+
def read: (Integer offset, Integer length) -> String
|
|
45
|
+
def write: (Integer offset, String data) -> Integer
|
|
46
|
+
def madvise: (:dontneed | :remove) -> nil
|
|
47
|
+
def unmap: () -> nil
|
|
48
|
+
def unmapped?: () -> bool
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
class Fault
|
|
52
|
+
attr_reader address: Integer
|
|
53
|
+
attr_reader flags: Array[:write | :wp | :minor]
|
|
54
|
+
attr_reader thread_id: Integer?
|
|
55
|
+
def write?: () -> bool
|
|
56
|
+
def wp?: () -> bool
|
|
57
|
+
def minor?: () -> bool
|
|
58
|
+
def copy: (String, ?wake: bool) -> Integer
|
|
59
|
+
def zero: (?wake: bool) -> Integer
|
|
60
|
+
def continue: (?wake: bool) -> Integer
|
|
61
|
+
def poison: (?wake: bool) -> Integer
|
|
62
|
+
def move: (Region source, ?offset: Integer, ?wake: bool) -> Integer
|
|
63
|
+
def wake: () -> nil
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
class ForkEvent
|
|
67
|
+
attr_reader child_uffd: UserfaultFD
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
class RemapEvent
|
|
71
|
+
attr_reader from: Integer
|
|
72
|
+
attr_reader to: Integer
|
|
73
|
+
attr_reader length: Integer
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
class RemoveEvent
|
|
77
|
+
attr_reader start: Integer
|
|
78
|
+
attr_reader end: Integer
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
class UnmapEvent
|
|
82
|
+
attr_reader start: Integer
|
|
83
|
+
attr_reader end: Integer
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
class Handler
|
|
87
|
+
attr_reader error: StandardError?
|
|
88
|
+
def stop: () -> self
|
|
89
|
+
def running?: () -> bool
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
Userfaultfd: singleton(UserfaultFD)
|
metadata
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: userfaultfd
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Yudai Takada
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: Handle Linux user-space page faults safely without blocking Ruby's GVL.
|
|
13
|
+
email:
|
|
14
|
+
- t.yudai92@gmail.com
|
|
15
|
+
executables: []
|
|
16
|
+
extensions:
|
|
17
|
+
- ext/userfaultfd/extconf.rb
|
|
18
|
+
extra_rdoc_files: []
|
|
19
|
+
files:
|
|
20
|
+
- LICENSE.txt
|
|
21
|
+
- README.md
|
|
22
|
+
- docs/gvl-and-page-faults.md
|
|
23
|
+
- examples/dirty_tracking.rb
|
|
24
|
+
- examples/lazy_mmap.rb
|
|
25
|
+
- examples/oom_canary.rb
|
|
26
|
+
- examples/post_copy_migration.rb
|
|
27
|
+
- ext/userfaultfd/compat.h
|
|
28
|
+
- ext/userfaultfd/constants.c
|
|
29
|
+
- ext/userfaultfd/extconf.rb
|
|
30
|
+
- ext/userfaultfd/userfaultfd.c
|
|
31
|
+
- lib/userfaultfd.rb
|
|
32
|
+
- lib/userfaultfd/handler.rb
|
|
33
|
+
- lib/userfaultfd/version.rb
|
|
34
|
+
- sig/userfaultfd.rbs
|
|
35
|
+
homepage: https://github.com/ydah/userfaultfd
|
|
36
|
+
licenses:
|
|
37
|
+
- MIT
|
|
38
|
+
metadata:
|
|
39
|
+
allowed_push_host: https://rubygems.org
|
|
40
|
+
homepage_uri: https://github.com/ydah/userfaultfd
|
|
41
|
+
rubygems_mfa_required: 'true'
|
|
42
|
+
rdoc_options: []
|
|
43
|
+
require_paths:
|
|
44
|
+
- lib
|
|
45
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
46
|
+
requirements:
|
|
47
|
+
- - ">="
|
|
48
|
+
- !ruby/object:Gem::Version
|
|
49
|
+
version: 3.2.0
|
|
50
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - ">="
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '0'
|
|
55
|
+
requirements: []
|
|
56
|
+
rubygems_version: 4.0.19
|
|
57
|
+
specification_version: 4
|
|
58
|
+
summary: Ruby bindings for Linux userfaultfd
|
|
59
|
+
test_files: []
|