Jak Linux radzi sobie ze skryptami powłoki?

22

W przypadku tego pytania rozważmy skrypt powłoki bash, choć to pytanie musi dotyczyć wszystkich typów skryptów powłoki.

Kiedy ktoś wykonuje skrypt powłoki, czy Linux ładuje cały skrypt naraz (może do pamięci), czy też czyta polecenia skryptowe jeden po drugim (wiersz po wierszu)?

Innymi słowy, jeśli wykonam skrypt powłoki i usunę go przed zakończeniem wykonywania, czy wykonanie zostanie zakończone, czy będzie kontynuowane w takim stanie?

zarejestrowany użytkownik
źródło
3
Spróbuj. (Będzie trwało.)
devnull
1
@devnull jest tutaj interesujące pytanie. To prawda, czy test będzie kontynuowany, czy nie, testowanie jest trywialne, ale istnieją różnice między plikami binarnymi (które są ładowane do pamięci) a skryptami z linią shebang lub skryptami bez linii shebang.
terdon
1
Możesz być zainteresowany tą odpowiedzią
terdon
23
W celu rzeczywistego zamiaru usunięcia skryptu powłoki podczas jego wykonywania nie ma znaczenia, czy jest on wczytywany jednocześnie, czy wiersz po wierszu. W Unixie i-węzeł nie jest usuwany (nawet jeśli nie ma linków do niego z żadnego katalogu), dopóki ostatni otwarty plik nie zostanie zamknięty. Innymi słowy, nawet jeśli powłoka podczas wykonywania odczytuje wiersz powłoki wiersz po wierszu, nadal można go bezpiecznie usunąć. Jedynym wyjątkiem jest sytuacja, gdy twoja powłoka zamyka i ponownie otwiera skrypt powłoki za każdym razem, ale jeśli tak, masz znacznie większe problemy (związane z bezpieczeństwem).
Chris Jester-Young

Odpowiedzi:

33

Jeśli używasz strace, możesz zobaczyć, w jaki sposób skrypt powłoki jest wykonywany podczas jego uruchamiania.

Przykład

Powiedz, że mam ten skrypt powłoki.

$ cat hello_ul.bash 
#!/bin/bash

echo "Hello Unix & Linux!"

Uruchamianie za pomocą strace:

$ strace -s 2000 -o strace.log ./hello_ul.bash
Hello Unix & Linux!
$

Przejrzenie strace.logpliku ujawnia następujące informacje.

...
open("./hello_ul.bash", O_RDONLY)       = 3
ioctl(3, SNDCTL_TMR_TIMEBASE or SNDRV_TIMER_IOCTL_NEXT_DEVICE or TCGETS, 0x7fff0b6e3330) = -1 ENOTTY (Inappropriate ioctl for device)
lseek(3, 0, SEEK_CUR)                   = 0
read(3, "#!/bin/bash\n\necho \"Hello Unix & Linux!\"\n", 80) = 40
lseek(3, 0, SEEK_SET)                   = 0
getrlimit(RLIMIT_NOFILE, {rlim_cur=1024, rlim_max=4*1024}) = 0
fcntl(255, F_GETFD)                     = -1 EBADF (Bad file descriptor)
dup2(3, 255)                            = 255
close(3)     
...

Po odczytaniu plik jest następnie wykonywany:

...
read(255, "#!/bin/bash\n\necho \"Hello Unix & Linux!\"\n", 40) = 40
rt_sigprocmask(SIG_BLOCK, NULL, [], 8)  = 0
rt_sigprocmask(SIG_BLOCK, NULL, [], 8)  = 0
fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 3), ...}) = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fc0b38ba000
write(1, "Hello Unix & Linux!\n", 20)   = 20
rt_sigprocmask(SIG_BLOCK, NULL, [], 8)  = 0
read(255, "", 40)                       = 0
exit_group(0)                           = ?

Powyżej widać wyraźnie, że cały skrypt wydaje się być wczytywany jako pojedyncza jednostka, a następnie wykonywany po nim. Tak więc „pojawi się” przynajmniej w przypadku Basha, że ​​wczyta plik, a następnie go wykona. Myślisz, że możesz edytować skrypt podczas jego działania?

UWAGA: Nie rób tego! Czytaj dalej, aby zrozumieć, dlaczego nie powinieneś zadzierać z uruchomionym plikiem skryptu.

Co z innymi tłumaczami?

Ale twoje pytanie jest nieco wyłączone. To nie Linux ładuje zawartość pliku, to interpreter ładuje zawartość, więc tak naprawdę to zależy od tego, jak interpreter zaimplementuje to, czy ładuje plik w całości, czy w blokach lub liniach na raz.

Dlaczego więc nie możemy edytować pliku?

Jeśli używasz znacznie większego skryptu, zauważysz jednak, że powyższy test jest nieco mylący. W rzeczywistości większość tłumaczy ładuje swoje pliki w blokach. Jest to dość standardowe w przypadku wielu narzędzi uniksowych, w których ładują bloki pliku, przetwarzają go, a następnie ładują kolejny blok. Możesz zobaczyć to zachowanie z tymi pytaniami i pytaniami U&L, które napisałem przed chwilą grep, zatytułowane: Ile tekstu zużywa grep / egrep za każdym razem? .

Przykład

Załóżmy, że wykonujemy następujący skrypt powłoki.

$ ( 
    echo '#!/bin/bash'; 
    for i in {1..100000}; do printf "%s\n" "echo \"$i\""; done 
  ) > ascript.bash;
$ chmod +x ascript.bash

Wynikające z tego pliku:

$ ll ascript.bash 
-rwxrwxr-x. 1 saml saml 1288907 Mar 23 18:59 ascript.bash

Który zawiera następujący typ treści:

$ head -3 ascript.bash ; echo "..."; tail -3 ascript.bash 
#!/bin/bash
echo "1"
echo "2"
...
echo "99998"
echo "99999"
echo "100000"

Teraz, gdy uruchomisz to przy użyciu tej samej techniki powyżej z strace:

$ strace -s 2000 -o strace_ascript.log ./ascript.bash
...    
read(255, "#!/bin/bash\necho \"1\"\necho \"2\"\necho \"3\"\necho \"4\"\necho \"5\"\necho \"6\"\necho \"7\"\necho \"8\"\necho \"9\"\necho \"10\"\necho 
...
...
\"181\"\necho \"182\"\necho \"183\"\necho \"184\"\necho \"185\"\necho \"186\"\necho \"187\"\necho \"188\"\necho \"189\"\necho \"190\"\necho \""..., 8192) = 8192

Zauważysz, że plik jest wczytywany z przyrostem 8 KB, więc Bash i inne powłoki prawdopodobnie nie załadują pliku w całości, a raczej czytają je w blokach.

Referencje

slm
źródło
@terdon - tak, pamiętam, że widziałem wcześniej te pytania.
slm
5
W przypadku 40-bajtowego skryptu jest on odczytywany w jednym bloku. Spróbuj ze skryptem> 8kB.
Gilles „SO- przestań być zły”
Nigdy nie próbowałem, ale myślę, że usuwanie plików nie jest faktycznie wykonywane, dopóki wszystkie procesy nie zamkną deskryptora pliku powiązanego z usuniętym plikiem, więc bash może kontynuować czytanie z usuniętego pliku.
Farid Nouri Neshat
@Gilles - tak, dodałem przykład, dostałem się do tego.
slm
2
To zachowanie zależy od wersji. Testowałem z wydaniem wersji bash 3.2.51 (1) i okazało się, że nie buforuje ona poza bieżącą linię (zobacz odpowiedź na przepełnienie stosu ).
Gordon Davisson,
11

Jest to bardziej zależne od powłoki niż systemu operacyjnego.

W zależności od wersji, kshczytaj skrypt na żądanie w bloku 8k lub 64k bajtów.

bashczytaj skrypt wiersz po wierszu. Jednak biorąc pod uwagę fakt, że linie mogą mieć dowolną długość, odczytuje za każdym razem 8176 bajtów od początku następnego wiersza do analizy.

Dotyczy to prostych konstrukcji, tj. Zestawu prostych poleceń.

Jeśli używane są polecenia strukturalne powłoki ( przypadek, w którym zaakceptowana odpowiedź nie jest brana pod uwagę ), takie jak for/do/donepętla, case/esacprzełącznik, dokument tutaj, podpowłoka ujęta w nawiasy, definicja funkcji itp. Oraz dowolna kombinacja powyższych, interpretatory powłoki odczytują do końca budowy, aby najpierw upewnić się, że nie wystąpił błąd składniowy.

Jest to nieco nieefektywne, ponieważ ten sam kod może być wielokrotnie odczytywany wiele razy, ale jest łagodzony przez fakt, że ta zawartość jest zwykle buforowana.

Niezależnie od interpretera powłoki, modyfikowanie skryptu powłoki podczas wykonywania jest niemądre, ponieważ powłoka może ponownie odczytać dowolną część skryptu, co może prowadzić do nieoczekiwanych błędów składniowych, jeśli nie jest zsynchronizowany.

Zauważ też, że bash może ulec awarii z naruszeniem segmentacji, gdy nie jest w stanie zapisać zbyt dużej konstrukcji skryptu, ksh93 może bezbłędnie czytać.

jlliagre
źródło
7

Zależy to od sposobu działania interpretera uruchamiającego skrypt. Jądro musi jedynie zauważyć, że plik do uruchomienia zaczyna się #!, w zasadzie uruchamia resztę wiersza jako program i nadaje mu plik wykonywalny jako argument. Jeśli wymieniony tam interpreter odczytuje ten plik wiersz po wierszu (tak jak interaktywne powłoki robią z tym, co piszesz), to właśnie otrzymujesz (ale struktury pętli wieloliniowych są odczytywane i przechowywane do powtórzenia); jeśli interpreter zatopi plik do pamięci, przetwarza go (być może kompiluje go do pośredniej reprezentacji, takiej jak Perl i Pyton), plik jest w całości czytany przed wykonaniem.

Jeśli w międzyczasie usuniesz plik, plik nie zostanie usunięty, dopóki interpreter go nie zamknie (jak zawsze pliki znikają po ostatnim odwołaniu, niezależnie od tego, czy jest to wpis do katalogu, czy proces utrzymujący go w pozycji otwartej).

vonbrand
źródło
4

Plik „x”:

cat<<'dog' >xyzzy
LANG=C
T=`tty`
( sleep 2 ; ls -l xyzzy >$T ) &
( sleep 4 ; rm -v xyzzy >$T ) &
( sleep 4 ; ls -l xyzzy >$T ) &
echo alive. ; sleep 1
echo alive. ; sleep 1
echo alive. ; sleep 1
echo alive. ; sleep 1
echo alive. ; sleep 1
echo alive. ; sleep 1
echo alive. ; sleep 1
echo alive. ; sleep 1
dog

sh xyzzy

Bieg:

~/wrk/tmp$ sh x
alive.
alive.
alive.
-rw-r--r-- 1 yeti yeti 287 Mar 23 16:57 xyzzy
alive.
removed `xyzzy'
ls: cannot access xyzzy: No such file or directory
alive.
alive.
alive.
alive.
~/wrk/tmp$ _

IIRC plik nie jest usuwany, dopóki proces utrzymuje go otwarty. Usunięcie po prostu usuwa podany DIRENT.


źródło