Comecei a aprender sobre tty(s) e sinais do Linux e tive alguns problemas.
Estou lendo e usando o livro The TTY demystified como referência.
Criei dois programas simples de golang.
Pai:
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)
}
}
Criança:
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)
}
}
Eles são bem simples. Ambos apenas imprimem uma mensagem para tty a cada 5/10 segundos. A única diferença é que o filho ignora o sinal SIGTSTP (ctrl-z). Então, quando eu pressiono ctrl-z, ele suspende o pai, mas não o filho. É exatamente o que eu esperava. No entanto, o que eu não esperava é que todo o grupo fosse movido do primeiro plano para o grupo de segundo plano. Isso contrasta com The TTY demystified . Especificamente:
Quando todos os processos no trabalho em primeiro plano foram suspensos , o líder da sessão lê a configuração atual do dispositivo TTY e a armazena para recuperação posterior. O líder da sessão continua a se instalar como o grupo de processos em primeiro plano atual para o TTY usando uma chamada ioctl. Então, ele imprime algo como "[1]+ Stopped" para informar ao usuário que um trabalho foi suspenso.
Ele diz que somente quando todos os processos no trabalho em primeiro plano forem suspensos , o líder da sessão (shell/bash) moverá o grupo para o trabalho em segundo plano...
Resultado de ps l -t /dev/pts/0
antes e depois de 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
Se eu mover o ignore ( signal.Ignore(syscall.SIGTSTP)
) do filho para o pai, então tudo funciona como deveria (do meu ponto de vista). O filho suspende (T), o pai retoma normalmente (R/S), mas o grupo ainda é o trabalho de primeiro plano.
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
Por que isso está acontecendo? O que estou perdendo?
É impreciso em duas frentes:
O controle de job é feito (ativamente) por shells, não (automaticamente) por líderes de sessão . Um shell não precisa ser um líder de sessão para fazer o controle de job.
Quando você inicia
xterm
,xterm
executa seu shell (por padrão) no novo processo que ele inicia em uma nova sessão e que controlará o dispositivo escravo pseudo-terminal. Então esse shell será o líder da sessão. Mas se você iniciar outro shell interativo a partir desse shell, ele não será o líder da sessão, mas assumirá o controle do job.Quando um shell bifurca um processo shell e executa um comando nele, ele não tem visibilidade sobre os processos que esse processo em si gera. Ao executar um job em primeiro plano, o shell espera pelo(s) processo(s) que ele mesmo iniciou ¹.
Se esse processo de nível superior for suspenso, o
wait*()
retornará, ou o shell receberá um SIGCHLD², que o shell interpretará como se o trabalho tivesse sido suspenso (independentemente de ainda haver processos em execução naquele trabalho dos quais ele não pode saber) e informará ao driver de dispositivo tty que ele não deve mais ficar em primeiro plano (colocando seu próprio grupo de processos em primeiro plano). Mas se for um filho desse processo que está suspenso, o SIGCHLD é enviado para seu pai, não para o shell, o shell não receberá um SIGCHLD e o quewait*()
ele fizer no processo que ele conhece não retornará.¹ em alguns shells, nem mesmo todos, por exemplo em
cmdA | cmdB
, alguns shells esperam apenas o processo em execuçãocmdB
, enquanto alguns esperam tanto o que está em execuçãocmdA
quanto o que está executandocmdB
, então no primeiro tipo (como combosh
base no Bourne shell), se você pressionar Ctrl+ zem(trap '' TSTP; sleep 100) | sleep 42
, você encontrarásleep 42
suspenso esleep 100
não, mas você ainda retornará ao prompt comsleep 100
agora em execução em segundo plano (e sua eventual morte não será tratada até que você executefg
) esleep 42
suspenso.² Alguns shells usam uma das
wait*()
chamadas de sistema, alguns usam manipuladores no SIGCHLD usando asigaction()
API mais recente, onde os manipuladores obtêm informações completas sobre o status dos processos, YMMV, mas o resultado final é o mesmo.