platform: Improve OOM killer for iOS

This commit is contained in:
世界
2026-02-27 13:35:58 +08:00
parent 21a1512e6c
commit 65150f5cc3
8 changed files with 341 additions and 25 deletions

View File

@@ -409,7 +409,7 @@ func (s *StartedService) SubscribeStatus(request *SubscribeStatusRequest, server
func (s *StartedService) readStatus() *Status {
var status Status
status.Memory = memory.Inuse()
status.Memory = memory.Total()
status.Goroutines = int32(runtime.NumGoroutine())
s.serviceAccess.RLock()
nowService := s.instance

2
go.mod
View File

@@ -33,7 +33,7 @@ require (
github.com/sagernet/gomobile v0.1.11
github.com/sagernet/gvisor v0.0.0-20250811.0-sing-box-mod.1
github.com/sagernet/quic-go v0.59.0-sing-box-mod.4
github.com/sagernet/sing v0.8.0-beta.16
github.com/sagernet/sing v0.8.0-beta.16.0.20260227013657-e419e9875a07
github.com/sagernet/sing-mux v0.3.4
github.com/sagernet/sing-quic v0.6.0-beta.13
github.com/sagernet/sing-shadowsocks v0.2.8

4
go.sum
View File

@@ -226,8 +226,8 @@ github.com/sagernet/nftables v0.3.0-beta.4 h1:kbULlAwAC3jvdGAC1P5Fa3GSxVwQJibNen
github.com/sagernet/nftables v0.3.0-beta.4/go.mod h1:OQXAjvjNGGFxaTgVCSTRIhYB5/llyVDeapVoENYBDS8=
github.com/sagernet/quic-go v0.59.0-sing-box-mod.4 h1:6qvrUW79S+CrPwWz6cMePXohgjHoKxLo3c+MDhNwc3o=
github.com/sagernet/quic-go v0.59.0-sing-box-mod.4/go.mod h1:OqILvS182CyOol5zNNo6bguvOGgXzV459+chpRaUC+4=
github.com/sagernet/sing v0.8.0-beta.16 h1:Fe+6E9VHYky9Mx4cf0ugbZPWDcXRflpAu7JQ5bWXvaA=
github.com/sagernet/sing v0.8.0-beta.16/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
github.com/sagernet/sing v0.8.0-beta.16.0.20260227013657-e419e9875a07 h1:LQqb+xtR5uqF6bePmJQ3sAToF/kMCjxSnz17HnboXA8=
github.com/sagernet/sing v0.8.0-beta.16.0.20260227013657-e419e9875a07/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
github.com/sagernet/sing-mux v0.3.4 h1:ZQplKl8MNXutjzbMVtWvWG31fohhgOfCuUZR4dVQ8+s=
github.com/sagernet/sing-mux v0.3.4/go.mod h1:QvlKMyNBNrQoyX4x+gq028uPbLM2XeRpWtDsWBJbFSk=
github.com/sagernet/sing-quic v0.6.0-beta.13 h1:umDr6GC5fVbOIoTvqV4544wY61zEN+ObQwVGNP8sX1M=

View File

@@ -1,3 +1,14 @@
package option
type OOMKillerServiceOptions struct{}
import (
"github.com/sagernet/sing/common/byteformats"
"github.com/sagernet/sing/common/json/badoption"
)
type OOMKillerServiceOptions struct {
MemoryLimit *byteformats.MemoryBytes `json:"memory_limit,omitempty"`
SafetyMargin *byteformats.MemoryBytes `json:"safety_margin,omitempty"`
MinInterval badoption.Duration `json:"min_interval,omitempty"`
MaxInterval badoption.Duration `json:"max_interval,omitempty"`
ChecksBeforeLimit int `json:"checks_before_limit,omitempty"`
}

View File

@@ -0,0 +1,51 @@
package oomkiller
import (
"time"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
)
func buildTimerConfig(options option.OOMKillerServiceOptions, memoryLimit uint64, useAvailable bool) (timerConfig, error) {
safetyMargin := uint64(defaultSafetyMargin)
if options.SafetyMargin != nil && options.SafetyMargin.Value() > 0 {
safetyMargin = options.SafetyMargin.Value()
}
minInterval := defaultMinInterval
if options.MinInterval != 0 {
minInterval = time.Duration(options.MinInterval.Build())
if minInterval <= 0 {
return timerConfig{}, E.New("min_interval must be greater than 0")
}
}
maxInterval := defaultMaxInterval
if options.MaxInterval != 0 {
maxInterval = time.Duration(options.MaxInterval.Build())
if maxInterval <= 0 {
return timerConfig{}, E.New("max_interval must be greater than 0")
}
}
if maxInterval < minInterval {
return timerConfig{}, E.New("max_interval must be greater than or equal to min_interval")
}
checksBeforeLimit := defaultChecksBeforeLimit
if options.ChecksBeforeLimit != 0 {
checksBeforeLimit = options.ChecksBeforeLimit
if checksBeforeLimit <= 0 {
return timerConfig{}, E.New("checks_before_limit must be greater than 0")
}
}
return timerConfig{
memoryLimit: memoryLimit,
safetyMargin: safetyMargin,
minInterval: minInterval,
maxInterval: maxInterval,
checksBeforeLimit: checksBeforeLimit,
useAvailable: useAvailable,
}, nil
}

View File

@@ -57,37 +57,72 @@ var (
type Service struct {
boxService.Adapter
logger log.ContextLogger
router adapter.Router
logger log.ContextLogger
router adapter.Router
memoryLimit uint64
hasTimerMode bool
useAvailable bool
timerConfig timerConfig
adaptiveTimer *adaptiveTimer
}
func NewService(ctx context.Context, logger log.ContextLogger, tag string, options option.OOMKillerServiceOptions) (adapter.Service, error) {
return &Service{
s := &Service{
Adapter: boxService.NewAdapter(boxConstant.TypeOOMKiller, tag),
logger: logger,
router: service.FromContext[adapter.Router](ctx),
}, nil
}
if options.MemoryLimit != nil {
s.memoryLimit = options.MemoryLimit.Value()
if s.memoryLimit > 0 {
s.hasTimerMode = true
}
}
config, err := buildTimerConfig(options, s.memoryLimit, s.useAvailable)
if err != nil {
return nil, err
}
s.timerConfig = config
return s, nil
}
func (s *Service) Start(stage adapter.StartStage) error {
if stage != adapter.StartStateStart {
return nil
}
if s.hasTimerMode {
s.adaptiveTimer = newAdaptiveTimer(s.logger, s.router, s.timerConfig)
if s.memoryLimit > 0 {
s.logger.Info("started memory monitor with limit: ", s.memoryLimit/(1024*1024), " MiB")
} else {
s.logger.Info("started memory monitor with available memory detection")
}
} else {
s.logger.Info("started memory pressure monitor")
}
globalAccess.Lock()
isFirst := len(globalServices) == 0
globalServices = append(globalServices, s)
globalAccess.Unlock()
if isFirst {
C.startMemoryPressureMonitor()
}
s.logger.Info("started memory pressure monitor")
return nil
}
func (s *Service) Close() error {
if s.adaptiveTimer != nil {
s.adaptiveTimer.stop()
}
globalAccess.Lock()
for i, service := range globalServices {
if service == s {
for i, svc := range globalServices {
if svc == s {
globalServices = append(globalServices[:i], globalServices[i+1:]...)
break
}
@@ -122,17 +157,36 @@ func goMemoryPressureCallback(status C.ulong) {
default:
level = "normal"
}
var freeOSMemory bool
for _, s := range services {
if isCritical {
s.logger.Error("memory pressure: ", level, ", usage: ", memory.Total()/(1024*1024), " MiB, resetting network")
s.router.ResetNetwork()
} else if isWarning {
s.logger.Warn("memory pressure: ", level, ", usage: ", memory.Total()/(1024*1024), " MiB")
usage := memory.Total()
if s.hasTimerMode {
if isCritical {
s.logger.Warn("memory pressure: ", level, ", usage: ", usage/(1024*1024), " MiB")
if s.adaptiveTimer != nil {
s.adaptiveTimer.startNow()
}
} else if isWarning {
s.logger.Warn("memory pressure: ", level, ", usage: ", usage/(1024*1024), " MiB")
} else {
s.logger.Debug("memory pressure: ", level, ", usage: ", usage/(1024*1024), " MiB")
if s.adaptiveTimer != nil {
s.adaptiveTimer.stop()
}
}
} else {
s.logger.Debug("memory pressure: ", level, ", usage: ", memory.Total()/(1024*1024), " MiB")
if isCritical {
s.logger.Error("memory pressure: ", level, ", usage: ", usage/(1024*1024), " MiB, resetting network")
s.router.ResetNetwork()
freeOSMemory = true
} else if isWarning {
s.logger.Warn("memory pressure: ", level, ", usage: ", usage/(1024*1024), " MiB")
} else {
s.logger.Debug("memory pressure: ", level, ", usage: ", usage/(1024*1024), " MiB")
}
}
}
if isCritical {
if freeOSMemory {
runtimeDebug.FreeOSMemory()
}
}

View File

@@ -7,33 +7,75 @@ import (
"github.com/sagernet/sing-box/adapter"
boxService "github.com/sagernet/sing-box/adapter/service"
C "github.com/sagernet/sing-box/constant"
boxConstant "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/memory"
"github.com/sagernet/sing/service"
)
func RegisterService(registry *boxService.Registry) {
boxService.Register[option.OOMKillerServiceOptions](registry, C.TypeOOMKiller, NewService)
boxService.Register[option.OOMKillerServiceOptions](registry, boxConstant.TypeOOMKiller, NewService)
}
type Service struct {
boxService.Adapter
logger log.ContextLogger
router adapter.Router
adaptiveTimer *adaptiveTimer
timerConfig timerConfig
hasTimerMode bool
useAvailable bool
memoryLimit uint64
}
func NewService(ctx context.Context, logger log.ContextLogger, tag string, options option.OOMKillerServiceOptions) (adapter.Service, error) {
return &Service{
Adapter: boxService.NewAdapter(C.TypeOOMKiller, tag),
}, nil
s := &Service{
Adapter: boxService.NewAdapter(boxConstant.TypeOOMKiller, tag),
logger: logger,
router: service.FromContext[adapter.Router](ctx),
}
if options.MemoryLimit != nil {
s.memoryLimit = options.MemoryLimit.Value()
}
if s.memoryLimit > 0 {
s.hasTimerMode = true
} else if memory.AvailableSupported() {
s.useAvailable = true
s.hasTimerMode = true
}
config, err := buildTimerConfig(options, s.memoryLimit, s.useAvailable)
if err != nil {
return nil, err
}
s.timerConfig = config
return s, nil
}
func (s *Service) Start(stage adapter.StartStage) error {
if stage != adapter.StartStateStart {
return nil
}
return E.New("memory pressure monitoring is not available on this platform")
if !s.hasTimerMode {
return E.New("memory pressure monitoring is not available on this platform without memory_limit")
}
s.adaptiveTimer = newAdaptiveTimer(s.logger, s.router, s.timerConfig)
s.adaptiveTimer.start(0)
if s.useAvailable {
s.logger.Info("started memory monitor with available memory detection")
} else {
s.logger.Info("started memory monitor with limit: ", s.memoryLimit/(1024*1024), " MiB")
}
return nil
}
func (s *Service) Close() error {
if s.adaptiveTimer != nil {
s.adaptiveTimer.stop()
}
return nil
}

View File

@@ -0,0 +1,158 @@
package oomkiller
import (
runtimeDebug "runtime/debug"
"sync"
"time"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing/common/memory"
)
const (
defaultChecksBeforeLimit = 4
defaultMinInterval = 500 * time.Millisecond
defaultMaxInterval = 10 * time.Second
defaultSafetyMargin = 5 * 1024 * 1024
)
type adaptiveTimer struct {
logger log.ContextLogger
router adapter.Router
memoryLimit uint64
safetyMargin uint64
minInterval time.Duration
maxInterval time.Duration
checksBeforeLimit int
useAvailable bool
access sync.Mutex
timer *time.Timer
previousUsage uint64
lastInterval time.Duration
}
type timerConfig struct {
memoryLimit uint64
safetyMargin uint64
minInterval time.Duration
maxInterval time.Duration
checksBeforeLimit int
useAvailable bool
}
func newAdaptiveTimer(logger log.ContextLogger, router adapter.Router, config timerConfig) *adaptiveTimer {
return &adaptiveTimer{
logger: logger,
router: router,
memoryLimit: config.memoryLimit,
safetyMargin: config.safetyMargin,
minInterval: config.minInterval,
maxInterval: config.maxInterval,
checksBeforeLimit: config.checksBeforeLimit,
useAvailable: config.useAvailable,
}
}
func (t *adaptiveTimer) start(_ uint64) {
t.access.Lock()
defer t.access.Unlock()
t.startLocked()
}
func (t *adaptiveTimer) startNow() {
t.access.Lock()
t.startLocked()
t.access.Unlock()
t.poll()
}
func (t *adaptiveTimer) startLocked() {
if t.timer != nil {
return
}
t.previousUsage = memory.Total()
t.lastInterval = t.minInterval
t.timer = time.AfterFunc(t.minInterval, t.poll)
}
func (t *adaptiveTimer) stop() {
t.access.Lock()
defer t.access.Unlock()
t.stopLocked()
}
func (t *adaptiveTimer) stopLocked() {
if t.timer != nil {
t.timer.Stop()
t.timer = nil
}
}
func (t *adaptiveTimer) running() bool {
t.access.Lock()
defer t.access.Unlock()
return t.timer != nil
}
func (t *adaptiveTimer) poll() {
t.access.Lock()
defer t.access.Unlock()
if t.timer == nil {
return
}
usage := memory.Total()
delta := int64(usage) - int64(t.previousUsage)
t.previousUsage = usage
var remaining uint64
var triggered bool
if t.memoryLimit > 0 {
if usage >= t.memoryLimit {
remaining = 0
triggered = true
} else {
remaining = t.memoryLimit - usage
}
} else if t.useAvailable {
available := memory.Available()
if available <= t.safetyMargin {
remaining = 0
triggered = true
} else {
remaining = available - t.safetyMargin
}
} else {
remaining = 0
}
if triggered {
t.logger.Error("memory threshold reached, usage: ", usage/(1024*1024), " MiB, resetting network")
t.router.ResetNetwork()
runtimeDebug.FreeOSMemory()
}
var interval time.Duration
if triggered {
interval = t.maxInterval
} else if delta <= 0 {
interval = t.maxInterval
} else if t.checksBeforeLimit <= 0 {
interval = t.maxInterval
} else {
timeToLimit := time.Duration(float64(remaining) / float64(delta) * float64(t.lastInterval))
interval = timeToLimit / time.Duration(t.checksBeforeLimit)
if interval < t.minInterval {
interval = t.minInterval
}
if interval > t.maxInterval {
interval = t.maxInterval
}
}
t.lastInterval = interval
t.timer.Reset(interval)
}