Module: OsCtld::CGroup

Includes:
OsCtl::Lib::Utils::Log
Defined in:
lib/osctld/cgroup.rb,
lib/osctld/cgroup/param.rb

Defined Under Namespace

Classes: ContainerParams, Param, Params

Constant Summary collapse

DEFAULT_FS =
'/sys/fs/cgroup'.freeze
RUNSTATE_FS =
'/run/osctl/cgroup'.freeze
FS =

Compatibility for callers referring to the public hierarchy. Daemon control-plane operations must use CGroup.fs, not this fixed path.

DEFAULT_FS
ROOT_GROUP =
'osctl'.freeze
DELEGATE_FILES =
File.read('/sys/kernel/cgroup/delegate').strip.split
MUTEX =
Mutex.new

Class Method Summary collapse

Class Method Details

.abs_cgroup_path(subsys, *path) ⇒ String

Return an absolute path to a cgroup

Parameters:

  • subsys (String) —

    subsystem

  • path (String)

Returns:

  • (String)


134
135
136
137
138
139
140
# File 'lib/osctld/cgroup.rb', line 134

def self.abs_cgroup_path(subsys, *path)
  if v1?
    File.join(fs, real_subsystem(subsys), *path)
  else
    File.join(fs, *path)
  end
end

.abs_cgroup_path_exist?(subsys, *path) ⇒ Boolean

Check if cgroup exists

Parameters:

  • subsys (String) —

    subsystem

  • path (String)

Returns:

  • (Boolean)


146
147
148
# File 'lib/osctld/cgroup.rb', line 146

def self.abs_cgroup_path_exist?(subsys, *path)
  exist?(abs_cgroup_path(subsys, *path))
end

.attach_to(type, path, pid: nil, debug: false) ⇒ Object

Attach process to a cgroup

Parameters:

  • type (String) —

    subsystem

  • path (Array<String>) —

    paths to create

  • pid (Integer, nil) (defaults to: nil) —

    pid to attach, default to the current process

  • debug (Boolean) (defaults to: false) —

    enable extra logging



296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
# File 'lib/osctld/cgroup.rb', line 296

def self.attach_to(type, path, pid: nil, debug: false)
  cgroup = File.join(abs_cgroup_path(type), *path)

  attached = false
  attach_pid = pid || Process.pid

  ['cgroup.procs', 'tasks'].each do |tasks|
    begin
      File.open(File.join(cgroup, tasks), 'w') do |f|
        f.puts(attach_pid)
      end
    rescue Errno::ENOENT
      next
    end

    if debug
      log(:debug, :cgroup, "Attached PID #{attach_pid} to cgroup #{cgroup}")
    end

    attached = true
    break
  end

  unless attached
    raise "unable to attach pid #{attach_pid} to cgroup #{cgroup.inspect}"
  end

  nil
end

.attach_to_all(path, pid: nil) ⇒ Object

Attach process to a cgroup in all subsystems

Parameters:

  • path (Array<String>) —

    paths to create

  • pid (Integer, nil) (defaults to: nil) —

    pid to attach, default to the current process



329
330
331
332
333
# File 'lib/osctld/cgroup.rb', line 329

def self.attach_to_all(path, pid: nil)
  subsystems.each do |subsys|
    attach_to(subsys, path, pid:)
  end
end

.available_controllers(cgroup) ⇒ Array<String>

Parameters:

  • cgroup (String) —

    absolute path of the cgroup

Returns:

  • (Array<String>)


391
392
393
# File 'lib/osctld/cgroup.rb', line 391

def self.available_controllers(cgroup)
  File.read(File.join(cgroup, 'cgroup.controllers')).strip.split
end

.chown_delegated(cgroup, uid:, gid:, unified: false) ⇒ Object

Give a user ownership of the cgroup files that the kernel designates for delegation. Files from /sys/kernel/cgroup/delegate are optional because their availability depends on the hierarchy version and kernel config.

Parameters:

  • cgroup (String) —

    absolute path of the delegated cgroup

  • uid (Integer) —

    owner uid

  • gid (Integer) —

    owner gid

  • unified (Boolean) (defaults to: false) —

    treat a named v1 unified hierarchy like v2



278
279
280
281
282
283
284
285
286
287
288
289
# File 'lib/osctld/cgroup.rb', line 278

def self.chown_delegated(cgroup, uid:, gid:, unified: false)
  File.chown(uid, gid, cgroup)
  File.chown(uid, gid, File.join(cgroup, 'cgroup.procs'))

  return unless v2? || unified

  (DELEGATE_FILES - ['cgroup.procs']).uniq.each do |file|
    File.chown(uid, gid, File.join(cgroup, file))
  rescue Errno::ENOENT
    # A kernel can omit delegation files that do not apply to its setup.
  end
end

.create(cgroup, delegate:, type:, base:) ⇒ Boolean

Create cgroup

Parameters:

  • cgroup (String) —

    absolute cgroup path

  • delegate (Boolean) —

    delegate controllers on cgroupv2

  • type (String) —

    subsystem

  • base (String) —

    absolute path to subsystem root

Returns:

  • (Boolean) —

    true if created, false if already existed



249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
# File 'lib/osctld/cgroup.rb', line 249

def self.create(cgroup, delegate:, type:, base:)
  created = false

  sync do
    begin
      Dir.mkdir(cgroup)
      created = true
    rescue Errno::EEXIST
      created = false
    end

    if created && v2? && delegate
      CGroup.delegate_available_controllers(cgroup)
    end

    init_cgroup(type, base, cgroup) if created
  end

  created
end

.delegate_available_controllers(cgroup) ⇒ Object

Enable all available controllers on cgroup. If a block is given, call it only when a controller write is needed and immediately before that write.

Parameters:

  • cgroup (String) —

    absolute path of the cgroup



377
378
379
380
381
382
383
384
385
386
387
# File 'lib/osctld/cgroup.rb', line 377

def self.delegate_available_controllers(cgroup)
  delegated = subtree_control(cgroup)
  cmd = (available_controllers(cgroup) - delegated).map do |controller|
    "+#{controller}"
  end.join(' ')

  return if cmd.empty?

  yield if block_given?
  File.write(File.join(cgroup, 'cgroup.subtree_control'), cmd)
end

.exist?(abs_path) ⇒ Boolean

Check if cgroup at path exists

Parameters:

  • abs_path (String) —

    absolute cgroup path, including /sys/fs/cgroup

Returns:

  • (Boolean)


153
154
155
# File 'lib/osctld/cgroup.rb', line 153

def self.exist?(abs_path)
  Dir.exist?(abs_path) && File.exist?(File.join(abs_path, 'cgroup.procs'))
end

.freeze_tree(path) ⇒ Object

Freeze cgroup at path

Parameters:

  • path (String)


537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
# File 'lib/osctld/cgroup.rb', line 537

def self.freeze_tree(path)
  abs_path = abs_cgroup_path('freezer', path)

  if v1?
    state = File.join(abs_path, 'freezer.state')

    begin
      File.write(state, 'FROZEN')
    rescue SystemCallError => e
      log(:warn, "Unable to freeze #{abs_path}: #{e.message} (#{e.class})")
    end

  else
    state = File.join(abs_path, 'cgroup.freeze')

    begin
      File.write(state, '1')
    rescue SystemCallError => e
      log(:warn, "Unable to freeze #{abs_path}: #{e.message} (#{e.class})")
    end
  end
end

.fs ⇒ Object

Absolute path to the cgroup hierarchy used by osctld.

vpsAdminOS keeps a private bind mount in /run/osctl/cgroup so daemon helpers do not depend on /sys visibility after kernfs filtering.



104
105
106
# File 'lib/osctld/cgroup.rb', line 104

def self.fs
  @fs ||= detect_fs(@version)
end

.get_cgroup_pids(subsys, path) ⇒ Array<Integer>

Get a list of processes in a cgroup

Parameters:

  • subsys (String)
  • path (String) —

    cgroup path relative to the subsystem

Returns:

  • (Array<Integer>)


522
523
524
525
526
527
528
529
530
531
532
533
# File 'lib/osctld/cgroup.rb', line 522

def self.get_cgroup_pids(subsys, path)
  pids = []

  File.open(File.join(abs_cgroup_path(subsys, path), 'cgroup.procs')) do |f|
    f.each_line do |line|
      pid = line.strip.to_i
      pids << pid if pid > 0
    end
  end

  pids
end

.get_process_cgroups(pid) ⇒ Hash<String, String>

Get process cgroups, v1-only

Parameters:

  • pid (Integer)

Returns:

  • (Hash<String, String>) —

    subsystem => cgroup path, relative to the mountpoint



472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
# File 'lib/osctld/cgroup.rb', line 472

def self.get_process_cgroups(pid)
  raise 'CGroup.get_process_cgroups is for cgroup v1 only' unless CGroup.v1?

  ret = {}

  File.open(File.join('/proc', pid.to_s, 'cgroup')) do |f|
    f.each_line do |line|
      s = line.strip

      # Hierarchy ID
      colon = s.index(':')
      next if colon.nil?

      s = s[(colon + 1)..]

      # Controllers
      colon = s.index(':')
      next if colon.nil?

      subsystems = if colon == 0
                     'unified'
                   else
                     s[0..(colon - 1)].split(',').map do |subsys|
                       # Remove name= from named controllers
                       if (eq = subsys.index('='))
                         subsys[(eq + 1)..]
                       else
                         subsys
                       end
                     end.join(',')
                   end

      s = s[(colon + 1)..]

      # Path
      next if s.nil?

      path = s

      ret[subsystems] = path
    end
  end

  ret
end

.inherit_param(base, cgroup, param) ⇒ Object

Inherit cgroup parameter from the parent cgroup

The parameter is considered to be set if it isn't empty. If the parent cgroup does not have the parameter set, it is inherited from its own parent and so on, all the way to the root cgroup defined by base. All parents will inherit the parameter as well.

Parameters:

  • base (String) —

    absolute path to the root cgroup

  • cgroup (String) —

    absolute path of the created cgroup

  • param (String) —

    parameter name



364
365
366
367
368
369
370
371
372
# File 'lib/osctld/cgroup.rb', line 364

def self.inherit_param(base, cgroup, param)
  v = File.read(File.join(cgroup, param)).strip
  return v unless v.empty?
  raise "parameter #{param} not set in root cgroup #{base}" if base == cgroup

  v = inherit_param(base, File.dirname(cgroup), param)
  set_param(File.join(cgroup, param), [v])
  v
end

.init(setup_host_mount: false) ⇒ Object



20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# File 'lib/osctld/cgroup.rb', line 20

def self.init(setup_host_mount: false)
  begin
    @version = File.read(RunState::CGROUP_VERSION).strip.to_i
  rescue Errno::ENOENT
    @version = 1
  end

  @version = 1 unless [1, 2].include?(@version)

  self.setup_host_mount if setup_host_mount
  @fs = detect_fs(@version)

  @subsystems =
    if @version == 1
      Dir.entries(fs) - ['.', '..']
    else
      ['']
    end
end

.init_cgroup(type, base, cgroup) ⇒ Object

Initialize cgroup after it was created.

This is used to ensure that cpuset cgroups have parameters cpuset.cpus and cpuset.mems set.

Parameters:

  • type (String) —

    cgroup subsystem

  • base (String) —

    absolute path to the root cgroup

  • cgroup (String) —

    absolute path of the created cgroup



343
344
345
346
347
348
349
350
351
352
# File 'lib/osctld/cgroup.rb', line 343

def self.init_cgroup(type, base, cgroup)
  case type
  when 'cpuset'
    if v1?
      inherit_param(base, cgroup, 'cpuset.cpus')
      inherit_param(base, cgroup, 'cpuset.mems')
      set_param(File.join(cgroup, 'cgroup.clone_children'), ['1'])
    end
  end
end

.mkpath(type, path, chown: nil, attach: false, leaf: true, pid: nil, delegate_existing: true, debug: false) ⇒ Boolean

Create CGroup a path, optionally chowning the last CGroup or attaching the current process into it.

For example, path ['osctl', 'subgroup', 'subsubgroup'] will create osctl/subgroup/subsubgroup in the chosen subsystem. If chown or attach is set, it has an effect on the last group, i.e. subsubgroup,

Parameters:

  • type (String) —

    subsystem

  • path (Array<String>) —

    paths to create

  • chown (Integer) (defaults to: nil) —

    chown the last group to chown:chown

  • attach (Boolean) (defaults to: false) —

    attach the current process to the last group

  • leaf (Boolean) (defaults to: true) —

    do not delegate controllers to the last cgroup

  • pid (Integer, nil) (defaults to: nil) —

    pid to attach, default to the current process

  • delegate_existing (Boolean) (defaults to: true) —

    delegate controllers on already-existing non-leaf cgroups

  • debug (Boolean) (defaults to: false) —

    enable extra logging

Returns:

  • (Boolean) —

    true if the last component was created, false if it already existed



175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
# File 'lib/osctld/cgroup.rb', line 175

def self.mkpath(
  type,
  path,
  chown: nil,
  attach: false,
  leaf: true,
  pid: nil,
  delegate_existing: true,
  debug: false
)
  base = abs_cgroup_path(type)
  tmp = []
  created = false

  path.each_with_index do |name, i|
    tmp << name
    cgroup = File.join(base, *tmp)
    delegate = i + 1 < path.length || (!leaf && !attach)

    created = create(
      cgroup,
      delegate:,
      type:,
      base:
    )

    next unless delegate_existing &&
                !created &&
                v2? &&
                delegate &&
                File.expand_path(cgroup) != File.expand_path(base)

    CGroup.delegate_available_controllers(cgroup)
  end

  if chown
    cgroup = File.join(base, *path)
    chown_delegated(cgroup, uid: chown, gid: chown, unified: type == 'unified')
  end

  attach_to(type, path, pid:, debug:) if attach

  created
end

.mkpath_all(path, chown: nil, attach: false, leaf: true, pid: nil, delegate_existing: true, debug: false) ⇒ Object

Create cgroup path in all subsystems, see selfself.mkpath

Parameters:

  • path (Array<String>) —

    paths to create

  • chown (Integer) (defaults to: nil) —

    chown the last group to chown:chown

  • attach (Boolean) (defaults to: false) —

    attach the current process to the last group

  • leaf (Boolean) (defaults to: true) —

    do not delegate controllers to the last cgroup

  • pid (Integer, nil) (defaults to: nil) —

    pid to attach, default to the current process

  • delegate_existing (Boolean) (defaults to: true) —

    delegate controllers on already-existing non-leaf cgroups

  • debug (Boolean) (defaults to: false) —

    enable extra logging



229
230
231
232
233
234
235
236
237
238
239
240
241
# File 'lib/osctld/cgroup.rb', line 229

def self.mkpath_all(
  path,
  chown: nil,
  attach: false,
  leaf: true,
  pid: nil,
  delegate_existing: true,
  debug: false
)
  subsystems.each do |subsys|
    mkpath(subsys, path, chown:, attach:, leaf:, pid:, delegate_existing:, debug:)
  end
end

.real_subsystem(subsys) ⇒ Object

Convert a single subsystem name to the mountpoint name, because some CGroup subsystems are mounted in a shared mountpoint.



110
111
112
113
114
115
# File 'lib/osctld/cgroup.rb', line 110

def self.real_subsystem(subsys)
  return 'cpu,cpuacct' if %w[cpu cpuacct].include?(subsys)
  return 'net_cls,net_prio' if %w[net_cls net_prio].include?(subsys)

  subsys
end

.reset_subtree_control(cgroup) ⇒ Object

Remove controller delegation from a cgroup

Parameters:

  • cgroup (String) —

    absolute path of the cgroup



397
398
399
400
401
402
403
404
405
# File 'lib/osctld/cgroup.rb', line 397

def self.reset_subtree_control(cgroup)
  changes = subtree_control(cgroup).map do |controller|
    "-#{controller}"
  end

  return if changes.empty?

  File.write(File.join(cgroup, 'cgroup.subtree_control'), changes.join(' '))
end

.rmpath(subsystem, path) ⇒ Object

Remove cgroup path

Parameters:

  • subsystem (String)
  • path (String) —

    path to remove, relative to the subsystem



441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
# File 'lib/osctld/cgroup.rb', line 441

def self.rmpath(subsystem, path)
  abs_path = abs_cgroup_path(subsystem, path)

  # Remove subdirectories recursively
  Dir.entries(abs_path).each do |dir|
    next if ['.', '..'].include?(dir)
    next unless Dir.exist?(File.join(abs_path, dir))

    rmpath(subsystem, File.join(path, dir))
  end

  # Remove directory
  Dir.rmdir(abs_path)

  if CGroup.v2?
    # Remove pinned links for the cgroup
    Devices::V2::BpfProgramCache.prune_cgroup_links(abs_path)
  end
rescue Errno::ENOENT
  # pass
end

.rmpath_all(path) ⇒ Object

Remove cgroup path in all subsystems

Parameters:

  • path (String) —

    path to remove, relative to subsystem



465
466
467
# File 'lib/osctld/cgroup.rb', line 465

def self.rmpath_all(path)
  subsystems.each { |subsys| rmpath(subsys, path) }
end

.set_param(path, value) ⇒ Boolean

Returns:

  • (Boolean)

Raises:



415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
# File 'lib/osctld/cgroup.rb', line 415

def self.set_param(path, value)
  raise CGroupFileNotFound.new(path, value) unless File.exist?(path)

  ret = true

  value.each do |v|
    log(:info, :cgroup, "Set #{path}=#{v}")

    begin
      File.write(path, v.to_s)
    rescue StandardError => e
      log(
        :warn,
        :cgroup,
        "Unable to set #{path}=#{v}: #{e.message}"
      )
      ret = false
    end
  end

  ret
end

.setup_host_mount ⇒ Object

Live activation does not run stage1. Adopt the existing hierarchy before launching LXC, which always uses this private root. Never create a fresh hierarchy or replace a mount; running processes and controllers stay put.



43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
# File 'lib/osctld/cgroup.rb', line 43

def self.setup_host_mount
  mounts = File.readlines('/proc/self/mountinfo').map(&:split)
  source = mounts.find { |parts| parts[4] == DEFAULT_FS }
  target = mounts.find { |parts| parts[4] == RUNSTATE_FS }

  # The private view is authoritative once established. Kernfs filtering
  # can hide the public mount from a restarted daemon on a running host.
  if target
    unless root_hierarchy_mount?(target, mounts)
      raise "#{RUNSTATE_FS} is not the existing cgroup hierarchy"
    end

    if source && !(source[2..3] == target[2..3] && root_hierarchy_mount?(source, mounts) &&
                   File.identical?(DEFAULT_FS, RUNSTATE_FS))
      raise "#{RUNSTATE_FS} is not the existing cgroup hierarchy"
    end

    return
  end

  raise "#{DEFAULT_FS} is not mounted" unless source
  unless root_hierarchy_mount?(source, mounts)
    raise "#{DEFAULT_FS} is not the existing cgroup hierarchy"
  end

  FileUtils.mkdir_p(RUNSTATE_FS)
  sys = OsCtl::Lib::Sys.new
  sys.rbind_mount(DEFAULT_FS, RUNSTATE_FS)
  sys.make_rprivate(RUNSTATE_FS)
end

.subsystem_paths ⇒ Hash<Symbol, String>

Returns:

  • (Hash<Symbol, String>)


124
125
126
127
128
# File 'lib/osctld/cgroup.rb', line 124

def self.subsystem_paths
  %i[cpu cpuacct memory pids].to_h do |subsys|
    [subsys, abs_cgroup_path(real_subsystem(subsys.to_s))]
  end
end

.subsystems ⇒ Array<String>

Returns a list of mounted CGroup subsystems on the system

Returns:

  • (Array<String>)


119
120
121
# File 'lib/osctld/cgroup.rb', line 119

def self.subsystems
  @subsystems
end

.subtree_control(cgroup) ⇒ Array<String>

Return controllers from cgroup.subtree_control

Parameters:

  • cgroup (String) —

    absolute path of the cgroup

Returns:

  • (Array<String>)


410
411
412
# File 'lib/osctld/cgroup.rb', line 410

def self.subtree_control(cgroup)
  File.read(File.join(cgroup, 'cgroup.subtree_control')).strip.split
end

.sync(&block) ⇒ Object



633
634
635
636
637
638
639
# File 'lib/osctld/cgroup.rb', line 633

def self.sync(&block)
  if MUTEX.owned?
    block.call
  else
    MUTEX.synchronize(&block)
  end
end

.thaw_tree(path) ⇒ Object

Thaw all frozen cgroups under path

Parameters:

  • path (String)


590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
# File 'lib/osctld/cgroup.rb', line 590

def self.thaw_tree(path)
  abs_path = abs_cgroup_path('freezer', path)

  begin
    entries = Dir.entries(abs_path)
  rescue Errno::ENOENT
    log(:warn, "Unable to thaw #{abs_path}: directory not found")
    return
  end

  entries.each do |dir|
    next if ['.', '..'].include?(dir)
    next unless Dir.exist?(File.join(abs_path, dir))

    thaw_tree(File.join(path, dir))
  end

  if v1?
    state = File.join(abs_path, 'freezer.state')

    begin
      if %w[FREEZING FROZEN].include?(File.read(state).strip)
        log(:info, "Thawing #{abs_path}")
        File.write(state, 'THAWED')
      end
    rescue SystemCallError => e
      log(:warn, "Unable to thaw #{abs_path}: #{e.message} (#{e.class})")
    end

  else
    state = File.join(abs_path, 'cgroup.freeze')

    begin
      if File.read(state).strip == '1'
        log(:info, "Thawing #{abs_path}")
        File.write(state, '0')
      end
    rescue SystemCallError => e
      log(:warn, "Unable to thaw #{abs_path}: #{e.message} (#{e.class})")
    end
  end
end

.v1? ⇒ Boolean

Returns:

  • (Boolean)


92
93
94
# File 'lib/osctld/cgroup.rb', line 92

def self.v1?
  @version == 1
end

.v2? ⇒ Boolean

Returns:

  • (Boolean)


96
97
98
# File 'lib/osctld/cgroup.rb', line 96

def self.v2?
  @version == 2
end

.version ⇒ 1, 2

Returns cgroup hierarchy version.

Returns:

  • (1, 2) —

    cgroup hierarchy version



88
89
90
# File 'lib/osctld/cgroup.rb', line 88

def self.version
  @version
end

.wait_frozen(path, timeout: 30) ⇒ Object

Wait for an earlier freeze request to stop all tasks in the subtree. A request alone is asynchronous and is not an admission barrier.



562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
# File 'lib/osctld/cgroup.rb', line 562

def self.wait_frozen(path, timeout: 30)
  abs_path = abs_cgroup_path('freezer', path)
  deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout

  loop do
    frozen =
      if v1?
        File.read(File.join(abs_path, 'freezer.state')).strip == 'FROZEN'
      else
        File.read(File.join(abs_path, 'cgroup.events')).lines.any? { |v| v.split == %w[frozen 1] }
      end
    return if frozen

    if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
      raise "Timed out waiting for cgroup #{path} to freeze"
    end

    sleep(0.05)
  end
rescue Errno::ENOENT
  # A removed subtree cannot admit new work.
  raise if Dir.exist?(abs_path)

  nil
end