diff options
author | Nicolas Lacasse <nlacasse@google.com> | 2018-10-17 12:27:58 -0700 |
---|---|---|
committer | Shentubot <shentubot@google.com> | 2018-10-17 12:29:05 -0700 |
commit | 4e6f0892c96c374b1abcf5c39b75ba52d98c97f8 (patch) | |
tree | cb21538ad26a50ff61086d55c1bef36d5026e4c0 /runsc/boot | |
parent | 578fe5a50dcf8e104b6bce3802987b0f8c069ade (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')
-rw-r--r-- | runsc/boot/controller.go | 58 | ||||
-rw-r--r-- | runsc/boot/fds.go | 45 | ||||
-rw-r--r-- | runsc/boot/loader.go | 221 |
3 files changed, 182 insertions, 142 deletions
diff --git a/runsc/boot/controller.go b/runsc/boot/controller.go index eaeb9e2d8..bee82f344 100644 --- a/runsc/boot/controller.go +++ b/runsc/boot/controller.go @@ -425,6 +425,26 @@ func (cm *containerManager) WaitPID(args *WaitPIDArgs, waitStatus *uint32) error return cm.l.waitPID(kernel.ThreadID(args.PID), args.CID, args.ClearStatus, waitStatus) } +// SignalDeliveryMode enumerates different signal delivery modes. +type SignalDeliveryMode int + +const ( + // DeliverToProcess delivers the signal to the container process with + // the specified PID. If PID is 0, then the container init process is + // signaled. + DeliverToProcess SignalDeliveryMode = iota + + // DeliverToAllProcesses delivers the signal to all processes in the + // container. PID must be 0. + DeliverToAllProcesses + + // DeliverToForegroundProcessGroup delivers the signal to the + // foreground process group in the same TTY session as the specified + // process. If PID is 0, then the signal is delivered to the foreground + // process group for the TTY for the init process. + DeliverToForegroundProcessGroup +) + // SignalArgs are arguments to the Signal method. type SignalArgs struct { // CID is the container ID. @@ -433,36 +453,20 @@ type SignalArgs struct { // Signo is the signal to send to the process. Signo int32 - // All is set when signal should be sent to all processes in the container. - // When false, the signal is sent to the root container process only. - All bool -} - -// Signal sends a signal to the root process of the container. -func (cm *containerManager) Signal(args *SignalArgs, _ *struct{}) error { - log.Debugf("containerManager.Signal %q %d, all: %t", args.CID, args.Signo, args.All) - return cm.l.signalContainer(args.CID, args.Signo, args.All) -} - -// SignalProcessArgs are arguments to the Signal method. -type SignalProcessArgs struct { - // CID is the container ID. - CID string - // PID is the process ID in the given container that will be signaled. + // If 0, the root container will be signalled. PID int32 - // Signo is the signal to send to the process. - Signo int32 - - // SendToForegroundProcess indicates that the signal should be sent to - // the foreground process group in the session that PID belongs to. - // This is only valid if the process is attached to a host TTY. - SendToForegroundProcess bool + // Mode is the signal delivery mode. + Mode SignalDeliveryMode } -// SignalProcess sends a signal to a particular process in the container. -func (cm *containerManager) SignalProcess(args *SignalProcessArgs, _ *struct{}) error { - log.Debugf("containerManager.Signal: %+v", args) - return cm.l.signalProcess(args.CID, args.PID, args.Signo, args.SendToForegroundProcess) +// Signal sends a signal to one or more processes in a container. If args.PID +// is 0, then the container init process is used. Depending on the +// args.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 (cm *containerManager) Signal(args *SignalArgs, _ *struct{}) error { + log.Debugf("containerManager.Signal %+v", args) + return cm.l.signal(args.CID, args.PID, args.Signo, args.Mode) } diff --git a/runsc/boot/fds.go b/runsc/boot/fds.go index a5a6ba8af..9416e3a5c 100644 --- a/runsc/boot/fds.go +++ b/runsc/boot/fds.go @@ -35,6 +35,7 @@ func createFDMap(ctx context.Context, k *kernel.Kernel, l *limits.LimitSet, cons fdm := k.NewFDMap() defer fdm.DecRef() + mounter := fs.FileOwnerFromContext(ctx) // Maps sandbox FD to host FD. fdMap := map[int]int{ @@ -42,16 +43,44 @@ func createFDMap(ctx context.Context, k *kernel.Kernel, l *limits.LimitSet, cons 1: stdioFDs[1], 2: stdioFDs[2], } - mounter := fs.FileOwnerFromContext(ctx) - for sfd, hfd := range fdMap { - file, err := host.ImportFile(ctx, hfd, mounter, console /* isTTY */) - if err != nil { - return nil, fmt.Errorf("failed to import fd %d: %v", hfd, err) + var ttyFile *fs.File + for appFD, hostFD := range fdMap { + var appFile *fs.File + + if console && appFD < 3 { + // Import the file as a host TTY file. + if ttyFile == nil { + var err error + appFile, err = host.ImportFile(ctx, hostFD, mounter, true /* isTTY */) + if err != nil { + return nil, err + } + defer appFile.DecRef() + + // Remember this in the TTY file, as we will + // use it for the other stdio FDs. + ttyFile = appFile + } else { + // Re-use the existing TTY file, as all three + // stdio FDs must point to the same fs.File in + // order to share TTY state, specifically the + // foreground process group id. + appFile = ttyFile + } + } else { + // Import the file as a regular host file. + var err error + appFile, err = host.ImportFile(ctx, hostFD, mounter, false /* isTTY */) + if err != nil { + return nil, err + } + defer appFile.DecRef() } - defer file.DecRef() - if err := fdm.NewFDAt(kdefs.FD(sfd), file, kernel.FDFlags{}, l); err != nil { - return nil, fmt.Errorf("failed to add imported fd %d to FDMap: %v", hfd, err) + + // Add the file to the FD map. + if err := fdm.NewFDAt(kdefs.FD(appFD), appFile, kernel.FDFlags{}, l); err != nil { + return nil, err } } 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 } |