Почему сигнал SIGTSTP, не обработанный родителем, перемещает всю группу в фоновый режим (вопреки написанному в TTY demystified)?

Вопрос или проблема

Я начал изучать tty(s) и сигналы в Linux и столкнулся с некоторыми проблемами.

Я читаю и использую The TTY demystified в качестве справочного материала.

Я написал две простые программы на golang.

Родитель:

package main

import (
    "fmt"
    "syscall"
    "time"
)

func main() {
    attr := &syscall.ProcAttr{
        Files: []uintptr{0, 1, 2},
    }

    _, err := syscall.ForkExec("./child/child", []string{"child"}, attr)
    if err != nil {
        fmt.Println("Error:", err)
        return
    }

    for {
        fmt.Println("hi from parent")
        time.Sleep(time.Second * 10)
    }
}

Ребенок:

package main

import (
    "fmt"
    "os/signal"
    "syscall"
    "time"
)

func main() {
    signal.Ignore(syscall.SIGTSTP) // golang's way to handle (ignore) signal

    for {
        fmt.Println("hi from child")
        time.Sleep(time.Second * 5)
    }
}

Они довольно простые. Оба просто выводят сообщение на tty каждые 5/10 секунд. Единственная разница в том, что ребенок игнорирует сигнал SIGTSTP (ctrl-z). Поэтому, когда я нажимаю ctrl-z, родитель приостанавливается, но не ребенок. Это именно то, чего я ожидал. Однако, что я не ожидал, так это того, что вся группа перемещается из переднего плана в фоновый режим. Это контрастирует с The TTY demystified. В частности:

Когда все процессы в группе переднего плана были приостановлены, лидирующий процесс сеанса читает текущую конфигурацию с устройства TTY и сохраняет ее для последующего извлечения. Лидирующий процесс сеанса устанавливает себя в качестве текущей группы процессов переднего плана для TTY с помощью вызова ioctl. Затем он выводит что-то вроде “[1]+ Stopped”, чтобы сообщить пользователю, что задача только что была приостановлена.

Говорится, что только когда все процессы в группе переднего плана были приостановлены, лидирующий процесс сеанса (оболочка/bash) перемещает группу в фоновую задачу…

Результат команды ps l -t /dev/pts/0 до и после ctrl-z:

yakog@yakog-computer:~/goprojects/parent$ ps l -t /dev/pts/0
F   UID     PID    PPID PRI  NI    VSZ   RSS WCHAN  STAT TTY        TIME COMMAND
0  1000 1747467 1747441  20   0  14288  5632 do_wai Ss   pts/0      0:00 bash
0  1000 1747496 1747467  20   0 1225432 1792 ep_pol Sl+  pts/0      0:00 ./parent
0  1000 1747501 1747496  20   0 1225424 1664 ep_pol Sl+  pts/0      0:00 child
yakog@yakog-computer:~/goprojects/parent$ ps l -t /dev/pts/0
F   UID     PID    PPID PRI  NI    VSZ   RSS WCHAN  STAT TTY        TIME COMMAND
0  1000 1747467 1747441  20   0  14288  5632 do_sel Ss+  pts/0      0:00 bash
0  1000 1747496 1747467  20   0 1225432 1792 do_sig Tl   pts/0      0:00 ./parent
0  1000 1747501 1747496  20   0 1225680 1792 ep_pol Sl   pts/0      0:00 child

Если я перемещаю игнорирование (signal.Ignore(syscall.SIGTSTP)) из ребенка к родителю, то все работает как надо (по моему мнению). Ребенок приостанавливается (T), родитель возобновляется нормально (R/S), но группа все еще остается задачей переднего плана.

yakog@yakog-computer:~/goprojects/parent$ ps l -t /dev/pts/0
F   UID     PID    PPID PRI  NI    VSZ   RSS WCHAN  STAT TTY        TIME COMMAND
0  1000 1749437 1749410  20   0  14420  5632 do_wai Ss   pts/0      0:00 bash
0  1000 1749957 1749437  20   0 1225448 1920 ep_pol Sl+  pts/0      0:00 ./parent
0  1000 1749962 1749957  20   0 1225412 1664 ep_pol Sl+  pts/0      0:00 child
yakog@yakog-computer:~/goprojects/parent$ ps l -t /dev/pts/0
F   UID     PID    PPID PRI  NI    VSZ   RSS WCHAN  STAT TTY        TIME COMMAND
0  1000 1749437 1749410  20   0  14420  5632 do_wai Ss   pts/0      0:00 bash
0  1000 1749957 1749437  20   0 1225448 1920 ep_pol Sl+  pts/0      0:00 ./parent
0  1000 1749962 1749957  20   0 1225668 1792 do_sig Tl+  pts/0      0:00 child

Почему это происходит, что я упускаю из виду?

Когда все процессы в группе переднего плана были приостановлены, лидирующий процесс сеанса…

Ошибочно с двух сторон:

  • Управление задачами выполняется (активно) оболочками, а не (автоматически) лидирующими процессами сеанса. Оболочка не обязана быть лидирующим процессом сеанса, чтобы управлять задачами.

    Когда вы запускаете xterm, xterm запускает вашу оболочку (по умолчанию) в новом процессе, который он запускает в новом сеансе и который будет управлять устройством псевдотерминала. Затем эта оболочка станет лидирующим процессом сеанса. Но если вы запускаете другую интерактивную оболочку из этой оболочки, она не будет лидирующим процессом сеанса, но возьмет на себя управление задачами.

  • Когда оболочка форкает процесс оболочки и выполняет в нем команду, она не имеет видимости процессов, которые этот процесс сам порождает. Когда задача выполняется в переднем плане, оболочка ждет процесс(ы), которые она запустила сама¹.

    Если этот верхний процесс приостанавливается, wait*() вернется, или оболочка получит сигнал SIGCHLD², который оболочка интерпретирует как приостановку задачи (независимо от того, продолжают ли выполняться процессы в этой задаче, о которых она не может знать) и сообщает драйверу устройства tty, что он больше не должен находиться в переднем плане (ставя свою собственную группу процессов в передний план). Но если это ребенок этого процесса, который приостанавливается, SIGCHLD отправляется его родителю, а не оболочке, оболочка не получает сигнал SIGCHLD, и wait*(), который она делает для процесса, о котором знает, не возвращается.


¹ в некоторых оболочках даже не во всех, например в cmdA | cmdB, некоторые оболочки ждут только процесс, выполняющий cmdB, в то время как другие ждут процессы, выполняющие и cmdA, и cmdB, так что в первом типе (как в bosh на основе оболочки Bourne), если вы нажмете Ctrl+z на (trap '' TSTP; sleep 100) | sleep 42, вы обнаружите, что sleep 42 приостановлен, а sleep 100 не приостановлен, но вы все равно вернетесь к приглашению с sleep 100, который теперь работает в фоновом режиме (и его окончательная смерть не обработана, пока вы не выполните fg) и sleep 42 приостановлен.

² Некоторые оболочки используют одну из системных вызовов wait*(), некоторые используют обработчики на SIGCHLD с использованием нового API sigaction(), где обработчики получают полную информацию о состоянии процессов, YMMV, но конечный результат один и тот же.

.

Ответ или решение

В мире управления процессами в операционных системах Linux и Unix управление сигналами и группами процессов является ключевым аспектом. Теория: сигнал SIGTSTP (вызываемый через Ctrl+Z) предназначен для временной приостановки процесса. Когда он отправляется процессу в группе процессов на переднем плане, ожидается, что вся группа будет перемещена в фоновый режим только в том случае, если все процессы в ней приостановлены. Однако порядок обработки сигналов внутри родительских и дочерних процессов может вызывать сложности, что и наблюдается в вашем примере.

Пример: в приведенных вами Go-программах родительский процесс создает дочерний с помощью системного вызова ForkExec. В дочернем процессе сигнал SIGTSTP игнорируется с помощью signal.Ignore(syscall.SIGTSTP), тогда как родительский процесс не обрабатывает этот сигнал явно. Когда вы нажимаете Ctrl+Z, родительский процесс приостанавливается, в то время как дочерний продолжает выполнение. Несмотря на это, вся группа процессов перемещается в фоновый режим. Такое поведение может показаться неожиданным в свете материала из “The TTY Demystified”, где утверждается, что перемещение группы происходит только после приостановки всех процессов.

Применение: поведение, которое вы наблюдаете, связано с тем, как оболочка обрабатывает сигналы и управляет рабочими процессами. Когда родительский процесс получает SIGTSTP и приостанавливается, оболочка, не дожидаясь приостановки дочерних процессов, уже считает задание “приостановленным” и переносит его в фоновый режим. Это объясняется тем, что оболочка ожидает только верхний уровень процесса (родительский процесс) и не имеет явного контроля над дочерними процессами, которые этот процесс мог создать. Другими словами, оболочка считает задание завершённым или приостановленным, как только родительский процесс это показывает. Таким образом, данный пример подчеркивает различие между теоретическими ожиданиями и практической реализацией управления сигналами в системах Unix.

Для корректного управления такими ситуациями может понадобиться явная обработка сигналов на стороне родительского процесса или использование механизмов межпроцессного взаимодействия для координации состояния процесса.

Оцените материал
Добавить комментарий

Капча загружается...