summaryrefslogtreecommitdiffhomepage
path: root/runsc/boot/loader.go
diff options
context:
space:
mode:
authorNicolas Lacasse <nlacasse@google.com>2018-10-17 12:27:58 -0700
committerShentubot <shentubot@google.com>2018-10-17 12:29:05 -0700
commit4e6f0892c96c374b1abcf5c39b75ba52d98c97f8 (patch)
treecb21538ad26a50ff61086d55c1bef36d5026e4c0 /runsc/boot/loader.go
parent578fe5a50dcf8e104b6bce3802987b0f8c069ade (diff)
runsc: Support job control signals for the root container.
Now containers run with "docker run -it" support control characters like ^C and ^Z. This required refactoring our signal handling a bit. Signals delivered to the "runsc boot" process are turned into loader.Signal calls with the appropriate delivery mode. Previously they were always sent directly to PID 1. PiperOrigin-RevId: 217566770 Change-Id: I5b7220d9a0f2b591a56335479454a200c6de8732
Diffstat (limited to 'runsc/boot/loader.go')
-rw-r--r--runsc/boot/loader.go221
1 files changed, 114 insertions, 107 deletions
diff --git a/runsc/boot/loader.go b/runsc/boot/loader.go
index fa169d090..c79b95bde 100644
--- a/runsc/boot/loader.go
+++ b/runsc/boot/loader.go
@@ -19,7 +19,6 @@ import (
"fmt"
mrand "math/rand"
"os"
- "os/signal"
"runtime"
"sync"
"sync/atomic"
@@ -110,7 +109,7 @@ type Loader struct {
// mu guards processes.
mu sync.Mutex
- // processes maps containers root process and invocation of exec. Root
+ // processes maps containers init process and invocation of exec. Root
// processes are keyed with container ID and pid=0, while exec invocations
// have the corresponding pid set.
//
@@ -291,28 +290,9 @@ func New(args Args) (*Loader, error) {
return nil, fmt.Errorf("error creating control server: %v", err)
}
- // We don't care about child signals; some platforms can generate a
- // tremendous number of useless ones (I'm looking at you, ptrace).
- if err := sighandling.IgnoreChildStop(); err != nil {
- return nil, fmt.Errorf("failed to ignore child stop signals: %v", err)
- }
- // Ensure that signals received are forwarded to the emulated kernel.
- ps := syscall.Signal(args.Conf.PanicSignal)
- startSignalForwarding := sighandling.PrepareForwarding(k, ps)
- if args.Conf.PanicSignal != -1 {
- // Panics if the sentry receives 'Config.PanicSignal'.
- panicChan := make(chan os.Signal, 1)
- signal.Notify(panicChan, ps)
- go func() { // S/R-SAFE: causes sentry panic.
- <-panicChan
- panic("Signal-induced panic")
- }()
- log.Infof("Panic signal set to %v(%d)", ps, args.Conf.PanicSignal)
- }
-
procArgs, err := newProcess(args.ID, args.Spec, creds, k)
if err != nil {
- return nil, fmt.Errorf("failed to create root process: %v", err)
+ return nil, fmt.Errorf("failed to create init process for root container: %v", err)
}
if err := initCompatLogs(args.UserLogFD); err != nil {
@@ -320,19 +300,47 @@ func New(args Args) (*Loader, error) {
}
l := &Loader{
- k: k,
- ctrl: ctrl,
- conf: args.Conf,
- console: args.Console,
- watchdog: watchdog,
- spec: args.Spec,
- goferFDs: args.GoferFDs,
- stdioFDs: args.StdioFDs,
- startSignalForwarding: startSignalForwarding,
- rootProcArgs: procArgs,
- sandboxID: args.ID,
- processes: make(map[execID]*execProcess),
+ k: k,
+ ctrl: ctrl,
+ conf: args.Conf,
+ console: args.Console,
+ watchdog: watchdog,
+ spec: args.Spec,
+ goferFDs: args.GoferFDs,
+ stdioFDs: args.StdioFDs,
+ rootProcArgs: procArgs,
+ sandboxID: args.ID,
+ processes: make(map[execID]*execProcess),
}
+
+ // We don't care about child signals; some platforms can generate a
+ // tremendous number of useless ones (I'm looking at you, ptrace).
+ if err := sighandling.IgnoreChildStop(); err != nil {
+ return nil, fmt.Errorf("failed to ignore child stop signals: %v", err)
+ }
+
+ // Handle signals by forwarding them to the root container process
+ // (except for panic signal, which should cause a panic).
+ l.startSignalForwarding = sighandling.PrepareHandler(func(sig linux.Signal) {
+ // Panic signal should cause a panic.
+ if args.Conf.PanicSignal != -1 && sig == linux.Signal(args.Conf.PanicSignal) {
+ panic("Signal-induced panic")
+ }
+
+ // Otherwise forward to root container.
+ deliveryMode := DeliverToProcess
+ if args.Console {
+ // Since we are running with a console, we should
+ // forward the signal to the foreground process group
+ // so that job control signals like ^C can be handled
+ // properly.
+ deliveryMode = DeliverToForegroundProcessGroup
+ }
+ if err := l.signal(args.ID, 0, int32(sig), deliveryMode); err != nil {
+ log.Warningf("error sending signal %v to container %q: %v", sig, args.ID, err)
+ }
+ })
+
ctrl.manager.l = l
return l, nil
}
@@ -467,9 +475,15 @@ func (l *Loader) run() error {
l.rootProcArgs.FDMap.DecRef()
}
- l.mu.Lock()
eid := execID{cid: l.sandboxID}
- l.processes[eid] = &execProcess{tg: l.k.GlobalInit()}
+ ep := execProcess{tg: l.k.GlobalInit()}
+ if l.console {
+ ttyFile := l.rootProcArgs.FDMap.GetFile(0)
+ defer ttyFile.DecRef()
+ ep.tty = ttyFile.FileOperations.(*host.TTYFileOperations)
+ }
+ l.mu.Lock()
+ l.processes[eid] = &ep
l.mu.Unlock()
// Start signal forwarding only after an init process is created.
@@ -572,7 +586,7 @@ func (l *Loader) startContainer(k *kernel.Kernel, spec *specs.Spec, conf *Config
// filesystem.
func (l *Loader) destroyContainer(cid string) error {
// First kill and wait for all processes in the container.
- if err := l.signalContainer(cid, int32(linux.SIGKILL), true /*all*/); err != nil {
+ if err := l.signal(cid, 0, int32(linux.SIGKILL), DeliverToAllProcesses); err != nil {
return fmt.Errorf("failed to SIGKILL all container processes: %v", err)
}
@@ -634,7 +648,7 @@ func (l *Loader) executeAsync(args *control.ExecArgs) (kernel.ThreadID, error) {
return tgid, nil
}
-// waitContainer waits for the root process of a container to exit.
+// waitContainer waits for the init process of a container to exit.
func (l *Loader) waitContainer(cid string, waitStatus *uint32) error {
// Don't defer unlock, as doing so would make it impossible for
// multiple clients to wait on the same container.
@@ -740,11 +754,12 @@ func newEmptyNetworkStack(conf *Config, clock tcpip.Clock) (inet.Stack, error) {
}
}
-// signalProcess sends a signal to the process with the given PID. If
-// sendToFGProcess is true, then the signal will be sent to the foreground
-// process group in the same session that PID belongs to.
-func (l *Loader) signalProcess(cid string, pid, signo int32, sendToFGProcess bool) error {
- if pid <= 0 {
+// signal sends a signal to one or more processes in a container. If PID is 0,
+// then the container init process is used. Depending on the SignalDeliveryMode
+// option, the signal may be sent directly to the indicated process, to all
+// processes in the container, or to the foreground process group.
+func (l *Loader) signal(cid string, pid, signo int32, mode SignalDeliveryMode) error {
+ if pid < 0 {
return fmt.Errorf("failed to signal container %q PID %d: PID must be positive", cid, pid)
}
@@ -756,10 +771,16 @@ func (l *Loader) signalProcess(cid string, pid, signo int32, sendToFGProcess boo
ep, ok := l.processes[eid]
l.mu.Unlock()
- // The caller may be signaling a process not started directly via exec.
- // In this case, find the process in the container's PID namespace and
- // signal it.
- if !ok {
+ switch mode {
+ case DeliverToProcess:
+ if ok {
+ // Send signal directly to the identified process.
+ return ep.tg.SendSignal(&arch.SignalInfo{Signo: signo})
+ }
+
+ // The caller may be signaling a process not started directly via exec.
+ // In this case, find the process in the container's PID namespace and
+ // signal it.
ep, ok := l.processes[execID{cid: cid}]
if !ok {
return fmt.Errorf("no container with ID: %q", cid)
@@ -772,74 +793,60 @@ func (l *Loader) signalProcess(cid string, pid, signo int32, sendToFGProcess boo
return fmt.Errorf("process %d is part of a different container: %q", pid, tg.Leader().ContainerID())
}
return tg.SendSignal(&arch.SignalInfo{Signo: signo})
- }
-
- if !sendToFGProcess {
- // Send signal directly to exec process.
- return ep.tg.SendSignal(&arch.SignalInfo{Signo: signo})
- }
- // Lookup foreground process group from the TTY for the given process,
- // and send the signal to it.
- if ep.tty == nil {
- return fmt.Errorf("failed to signal foreground process group in container %q PID %d: no TTY attached", cid, pid)
- }
- pg := ep.tty.ForegroundProcessGroup()
- if pg == nil {
- // No foreground process group has been set. Signal the
- // original thread group.
- log.Warningf("No foreground process group for container %q and PID %d. Sending signal directly to PID %d.", cid, pid, pid)
- return ep.tg.SendSignal(&arch.SignalInfo{Signo: signo})
- }
+ case DeliverToForegroundProcessGroup:
+ if !ok {
+ return fmt.Errorf("failed to signal foreground process group for container %q PID %d: no such PID", cid, pid)
+ }
- // Send the signal to all processes in the process group.
- var lastErr error
- for _, tg := range l.k.TaskSet().Root.ThreadGroups() {
- if tg.ProcessGroup() != pg {
- continue
+ // Lookup foreground process group from the TTY for the given process,
+ // and send the signal to it.
+ if ep.tty == nil {
+ return fmt.Errorf("failed to signal foreground process group in container %q PID %d: no TTY attached", cid, pid)
}
- if err := tg.SendSignal(&arch.SignalInfo{Signo: signo}); err != nil {
- lastErr = err
+ pg := ep.tty.ForegroundProcessGroup()
+ if pg == nil {
+ // No foreground process group has been set. Signal the
+ // original thread group.
+ log.Warningf("No foreground process group for container %q and PID %d. Sending signal directly to PID %d.", cid, pid, pid)
+ return ep.tg.SendSignal(&arch.SignalInfo{Signo: signo})
+ }
+ // Send the signal to all processes in the process group.
+ var lastErr error
+ for _, tg := range l.k.TaskSet().Root.ThreadGroups() {
+ if tg.ProcessGroup() != pg {
+ continue
+ }
+ if err := tg.SendSignal(&arch.SignalInfo{Signo: signo}); err != nil {
+ lastErr = err
+ }
+ }
+ return lastErr
+ case DeliverToAllProcesses:
+ if !ok {
+ return fmt.Errorf("failed to signal all processes in container %q PID %d: no such PID", cid, pid)
}
- }
- return lastErr
-}
-
-// signalContainer sends a signal to the root container process, or to all
-// processes in the container if all is true.
-func (l *Loader) signalContainer(cid string, signo int32, all bool) error {
- si := arch.SignalInfo{Signo: signo}
-
- l.mu.Lock()
- defer l.mu.Unlock()
-
- eid := execID{cid: cid}
- ep, ok := l.processes[eid]
- if !ok {
- return fmt.Errorf("failed to signal container %q: no such container", cid)
- }
-
- if !all {
- return ep.tg.SendSignal(&si)
- }
- // Pause the kernel to prevent new processes from being created while
- // the signal is delivered. This prevents process leaks when SIGKILL is
- // sent to the entire container.
- l.k.Pause()
- if err := l.k.SendContainerSignal(cid, &si); err != nil {
+ // Pause the kernel to prevent new processes from being created while
+ // the signal is delivered. This prevents process leaks when SIGKILL is
+ // sent to the entire container.
+ l.k.Pause()
+ if err := l.k.SendContainerSignal(cid, &arch.SignalInfo{Signo: signo}); err != nil {
+ l.k.Unpause()
+ return err
+ }
l.k.Unpause()
- return err
- }
- l.k.Unpause()
- // If killing all processes, wait for them to exit.
- if all && linux.Signal(signo) == linux.SIGKILL {
- for _, t := range l.k.TaskSet().Root.Tasks() {
- if t.ContainerID() == cid {
- t.ThreadGroup().WaitExited()
+ // If SIGKILLing all processes, wait for them to exit.
+ if linux.Signal(signo) == linux.SIGKILL {
+ for _, t := range l.k.TaskSet().Root.Tasks() {
+ if t.ContainerID() == cid {
+ t.ThreadGroup().WaitExited()
+ }
}
}
+ return nil
+ default:
+ panic(fmt.Sprintf("unknown signal signal delivery mode %v", mode))
}
- return nil
}