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

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

Я начал изучать linux tty и сигналы и столкнулся с некоторыми проблемами.

Я читаю и использую 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("Ошибка:", err)
        return
    }

    for {
        fmt.Println("Привет от родителя")
        time.Sleep(time.Second * 10)
    }
}

Дочерняя программа:

package main

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

func main() {
    signal.Ignore(syscall.SIGTSTP)

    for {
        fmt.Println("Привет от дочерней программы")
        time.Sleep(time.Second * 5)
    }
}

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

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

Говорится, что только когда все процессы в выполнении в переднем плане были приостановлены, лидер сеанса (шелл/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

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

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

Ваша проблема связана с тем, как сигнал SIGTSTP обрабатывается и как это влияет на группы процессов в Unix-подобных системах. Давайте разберем это подробнее, рассматривая теоретическую основу, пример вашего кода и практическое применение.

Теория

В Unix/Linux системах терминал ассоциирован с сессией, в рамках которой процессы образуют группы. Когда вы выполняете команду с терминала, она запускается в группе процессов, которая связана с управляющим терминалом (TTY). SIGTSTP — это сигнал, который обычно используется для приостановки процесса. Он высылается процессам в текущей группе терминала, которая находится на переднем плане (foreground process group), когда пользователь нажимает Ctrl-Z.

Согласно управлению сессиями и группами процессов, когда все процессы в передней группе приостановлены или завершены, оболочка (например, Bash) может передвигать фокус терминала на заднюю группу процессов. Оболочка контролирует, какие процессы имеют прямой доступ к вводу-выводу через терминал, и переключает эти привилегии между заранее объявленными состояниями передней и задней частей.

Пример

Ваши Golang программы демонстрируют следующее поведение:

  1. Родительский процесс создает дочерний процесс и работает в цикле, выводя сообщения. Оба процесса находятся в одной процессной группе, связанной с текущим терминалом.
  2. В дочернем процессе игнорируется сигнал SIGTSTP, поэтому при нажатии Ctrl-Z, только родительский процесс приостанавливается.
  3. Несмотря на то что дочерний процесс продолжает выполнение, оболочка сообщает терминалу, что передняя группа была перемещена на задний план.

Ваше наблюдение верно: перемещение группы на фон происходит из-за приостановки процесса, от которого ожидается взаимодействие с терминалом, то есть родительского процесса, а не обязательно всех процессов в группе.

Применение

Ваше решение использовать signal.Ignore(syscall.SIGTSTP) в родительском процессе и ожидать, что дочерний процесс будет приостановлен, действительно изменяет поведение. Это связано с тем, что при игнорировании SIGTSTP родительским процессом, он не будет приостановлен, что позволяет ему оставаться в передней группе терминала, поскольку основная цель — сохранить интерактивную сессию в том виде, в котором она активно управляет I/O через терминал.

Заключение

Основным фактором, способствующим этому поведению, является организация управления процессами и сессиями в Unix-подобных системах, которая гарантирует, что только одна процессная группа может иметь прямой доступ к терминалу в любой момент времени. При этом, приостановка процессов данной группы является триггером для перемещения группы на задний план. Понимание и правильное манипулирование этим поведением может помочь лучше управлять задачами и процессами в сценариях, связанных с управлением терминалами и сигналами.

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

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