mirror of
https://github.com/SagerNet/sing-box.git
synced 2026-04-11 17:47:20 +10:00
platform: Improve OOM killer for iOS
This commit is contained in:
@@ -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
2
go.mod
@@ -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
4
go.sum
@@ -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=
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
51
service/oomkiller/config.go
Normal file
51
service/oomkiller/config.go
Normal 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
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
158
service/oomkiller/service_timer.go
Normal file
158
service/oomkiller/service_timer.go
Normal 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)
|
||||
}
|
||||
Reference in New Issue
Block a user