Nieoczekiwane działanie wielokrotnego potokowania programu cat we wzorcowej powłoce

Nieoczekiwane działanie wielokrotnego potokowania programu cat we wzorcowej powłoce

Napisane przez: Maciej Malicki ()
Liczba odpowiedzi: 2

Dla ustalenia uwagi wybierzmy polecenie

cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat

Liczba potoków jest przypadkowa, nie sprawdzałem innych. Rozumiem oczywiście, że nie jest to najbardziej typowy przypadek użycia powłoki, ale może warto mu się przyjrzeć?

Powoduje on średnio odtwarzalne skutki:

1. Przy moim pierwszym uruchomieniu polecenia wywołanie Setpgid nie powiodło się, co pociągnęło za sobą zakończenie cat:

# cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat
Setpgid error: Permission denied
cat: -: Input/output error
[tekst zachęty mojej powłoki]

Dostarczona powłoka również zamknęła się, zwracając sterowanie do powłoki, w której była uruchomiona.

2. Inny przykład uruchomienia tego polecenia: jeśli nie widać żadnych niepomyślnych wywołań systemowych, polecenie zdaje się wcale nie wykonywać – nie odpowiada na wpisywany tekst i nie jest to bynajmniej kwestia powolności mojego systemu, bo w innych powłokach to polecenie wykonuje się poprawnie. Ctrl-d nie zwraca sterowania powłoce. Ctrl-c powoduje za to wypisanie wiadomości o przeniesieniu zadania do tła (istotnie można sprawdzić np. poleceniem ps, że co najmniej część programów cat pozostała uruchomiona), a późniejsze przywrócenie go na pierwszy plan sprawia, że zadanie od razu się zakańcza.

# cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat
aaa
^C[1] suspended 'cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat'
# fg
[1] continue 'cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat'

#

Powyżej miał być jeden blok zamiast dwóch, ale forum chyba uznaje, że pustą linią chciałem oddzielić paragrafy/akapity.

Z czego może wynikać takie zachowanie dostarczonej wzorcowej powłoki?

W odpowiedzi na Maciej Malicki

Odp: Nieoczekiwane działanie wielokrotnego potokowania programu cat we wzorcowej powłoce

Napisane przez: Patrycja Balik ()

Pierwszy błąd łatwiej reprodukować z absolutnie obrzydliwym wrapperem forka, który sprawia, że rodzic wykonuje setpgid znacznie później niż dziecko.

#define _GNU_SOURCE
#include <dlfcn.h>
#include <unistd.h>

pid_t fork() {
  static pid_t (*fork_orig)() = NULL;
  fork_orig = dlsym(RTLD_NEXT, "fork");
  pid_t pid;
  if ((pid = fork_orig())) {
    sleep(5);
  }
  return pid;
}
 

Wrappera można używać z wzorcówką za pomocą LD_PRELOAD.

gcc -o wrap.so -fPIC -shared wrap.c -ldl
LD_PRELOAD=./wrap.so ./shell
 

Błąd wynika z tego, że jeśli dziecko już wykonało execve, setpgid w rodzicu rzuci błędem EACCER, więc nie można w rodzicu używać dostarczonego wrappera Setpgid. strace u mnie potwierdza to przypuszczenie:

setpgid(1770, 1770)                     = -1 EACCES (Permission denied)
 
W odpowiedzi na Patrycja Balik

Odp: Nieoczekiwane działanie wielokrotnego potokowania programu cat we wzorcowej powłoce

Napisane przez: Patrycja Balik ()
Można dodać wrapper setpgida, który brutalnie ignoruje EACCES, żeby łatwiej przyglądać się drugiemu przypadkowi.
int setpgid(pid_t pid, pid_t pgid) {
  static pid_t (*setpgid_orig)(pid_t pid, pid_t pgid) = NULL;
  setpgid_orig = dlsym(RTLD_NEXT, "setpgid");
  int rt;
  if ((rt = setpgid_orig(pid, pgid) < 0) && errno == EACCES) {
    return 0;
  }
  return rt;
}
Wydaje się, że występuje wyścig, i proces dostaje SIGTTIN zanim nastąpi tcsetpgrp w rodzicu, bo rodzic wisi na sigsuspend. Dokumentacja GNU libc o kontroli zadań mówi:
If the job is being launched as a foreground job, the new process group also needs to be put into the foreground on the controlling terminal using tcsetpgrp. Again, this should be done by the shell as well as by each of its child processes, to avoid race conditions.
Co może być lub nie być użyteczne.