pdf - Środowisko programisty

Transkrypt

pdf - Środowisko programisty
Środowisko programisty
Zestaw 8
Semestr zimowy 2016/2017
Kraków, 26-27 listopad 2015
Procesy w systemie UNIX
Procesy działające w czasie uruchomienia systemu UNIX można zorganizować w drzewo.
W korzeniu mamy proces (init). Wszystkie procesy oprócz (init) mają proces nadrzędny
(ang. parent process) po którym dziedziczy się pewne zasoby, ustawiania wartości zmiennych
itd. itp. Ważne jest aby pamiętać, że w systemie UNIX proces podrzędny (ang. child
process) może być zmieniany tylko przez rodzica, albo przez debugger.
Na przykład, aby zmienić zawartość zmiennej PATH tak, aby pamiętała ją każda nowo
otwarta konsola należy zmienić odpowiednie ustawienia w pliku konfiguracyjnym, wylogować
się i zalogować od początku. Dopiero nowa instancja procesu (login) będzie znała nową
zawartość zmiennej PATH i przekaże ją nowo otwrtym konsolom (swoim procesom podrzędnym).
Model tworzenia procesów w systemie UNIX jest zorganizowany, w dużym przybliżeniu,
wokół dwóch wywołań systemowych: fork() oraz exec(). fork() tworzy proces podrzędny,
który jest duplikatem (z drobnymi różnicami) swojego procesu nadrzędnego i przydziela
mu odpowiednie zasoby, takie jak pamięć czy czas procesora. Zwyczajna sekwencja zdarzeń
wygląda następująco. Program woła fork() i sprawdza zwróconą wartość. Jeśli jest
ona większa od zera, to jesteśmy w procesie nadrzędnym. Wtedy zwrócona wartość to
numer identyfikacyjny – PID (ang. process identifier ) systemu podrzędnego. Jeśli nie
chcemu aby proces podrzedny działał w tle, proces nadrzędny wywołuje wait i czeka
na zakończenie pracy przez proces podrzędny. Jeśli zwrócone jest 0, wtedy jesteśmy w
procesie podrzędnym. Wywoływany jest exec() i wykonuje się treść procesu podrzędnego.
W czasie wykonywania się procesu podrzędnego mogą być zamykane deskryptory plików,
ustawiane zmienne środowiskowe itd. itp. Żadna z tych zmian jednak nie zmienia procesu
nadrzędnego.
Na przykład proces shell może wywołać następujący proces podrzędny.
echo hello world 1>&2
Zmiany przeprowadzony przez ten proces na dyskryptorach plików nie będą miały wpływu
na shella.
Poniżej podajemy przykładowy proces, który powinien trwać na tyle długo, aby miało sens
mierzenie mu czasu komendą time – próbuje przeglądać zawartość katalogu głównego
i rekurencyjnie zawartość jego podkatalogów, ale nie będzie nic wypisywał na ekran
konsoli (symbol &> oznacza jednoczesne przekierowanie standardowego wyjścia danych
> i standardowego wyjścia błędów 2> do podanego pliku – w tym wypadku specjalnego
pliku, który ignoruje wszystkie dane zapisane do niego):
time ls -R / &> /dev/null
−→
−→real
0m2.594s
−→user
0m1.234s
Strona 1/10
Środowisko programisty
Zestaw 8
Semestr zimowy 2016/2017
Kraków, 26-27 listopad 2015
−→sys
0m1.336s
Czas real to faktyczny czas wykonania programu, czas user to czas poświęcony na
wykonywaniu samych instrukcji programu bez ingerencji w polecenia systemu operacyjnego,
natomiast czas sys, to czas poświęcony na wykonywanie instrukcji w jądrze systemu
operacyjnego.
Trochę zmodyfikujemy nasze ostatnie polecenie, aby zapisywało swój rezultat do normalnego
pliku: ls -R / &> ~/katalogi
Z poziomu konsoli możemy przerwać działanie tego programu przy pomocy kombinacji
Ctrl-C. Po wciśnięciu klawiszy wyemitowany zostanie sygnał SIGINT, który powinien
spowodować wymienione przerwanie, o ile nie zostanie wcześniej przechwycony. Za inną
kombinacją, Ctrl-Z, kryje się sygnał SIGTSTP, który wstrzymuje działanie programu.
Program wstrzymany można ponownie uruchomić od momentu, w którym nastąpiło
wstrzymanie. Jeśli użyliśmy tej ostatniej kombinacji, to zobaczymy status zadania –
zatrzymane.
Przy pomocy polecenia jobs możemy zobaczyć stan wszystkich zadań w aktualnej sesji.
Możemy uruchomić i zatrzymać w podany sposób kolejny proces: sleep 1000 (“nic nie
rób przez 1000 sekund”), co pozwoli nam zobaczyć dwa numerowane wpisy na liście
zadań (pytanie: jaki będzie rezultat wykonania time sleep 5?). Jeśli chcemy zakończyć
pierwszy z tych procesów, to możemy zrobić to przy pomocy polecenia kill %1 odwołującego
się do numeru na liście zadań poprzedzonego znakiem %.
Alternatywnie, jako argument polecenia kill możemy użyć numeru PID. Uzyskamy go
dzięki poleceniu ps, które wypisuje listę naszych procesów związanych z konsolą. Inne
przykłady wywołań polecenia ps:
ps x
wszystkie nasze procesy, także niezwiązane z konsolą;
ps ux
jak wyżej, wraz dodatkowymi informacjami,
np. o właścicielu procesu i czasie startu;
ps ax
wszystkie procesy wszystkich osób na serwerze;
ps axo pid,user,tty,command to co wyżej, ale w czterech następujących kolumnach:
PID, nazwa użytkownika, konsola, treść komendy.
Teraz możemy np. usunąć proces o numerze PID 4493 poprzez komendę kill 4493.
Polecenie kill standardowo wysyła sygnał SIGTERM, który jednak, podobnie jak SIGINT,
może zostać przechwycony przez program. W przypadkach krytycznych należy zatem
używać opcji -9 (np. kill -9 4493), powodującej wysłanie sygnału SIGKILL, który działa
jak SIGTERM, ale nie da się go przechwycić. Jeżeli naszym celem jest zakończenie od
razu wszystkich procesów związanych z konkretną komendą, to możemy użyć polecenia
killall, np. killall sleep.
# include < stdio .h >
# include < signal .h >
void sighandler ( int sig ) {
printf ( " Caught signal % d . Avoiding ^ C .\ n " , sig ) ;
}
Strona 2/10
Środowisko programisty
Zestaw 8
Semestr zimowy 2016/2017
Kraków, 26-27 listopad 2015
int main () {
signal ( SIGINT , & sighandler ) ;
while (1) ;
return 0;
}
Istniejące zadanie (np. o numerze 2 na liście jobs) można zmusić do pracy w tle przy
pomocy polecenia bg %2. Jeśli jednak od razu chcemy wykonać dane zadanie w tle, to
wystarczy na jego końcu wstawić znak &. Uwaga: procesy działające w tle będą działały
nadal po zakończeniu sesji! Przykład polecenia:
ls -l / &> ~/katalogi2 &
−→[3] 5988
Od razu otrzymaliśmy w ten sposób numer zadania i numer PID. Przyjmijmy, że po
wywołaniu komendy jobs otrzymamy:
−→[1]+ Stopped
ls -R / >&katalogi
−→[2]
Running
sleep 1000 &
−→[3]- Done
ls -l / >&katalogi2
Zadanie o numerze 3 zakończyło się, więc przy kolejnym wywołaniu jobs już nie pojawi
się na liście. Drugie zadanie działa w tle, natomiast pierwsze wciąż jest zatrzymane –
możemy je przenieść do pracy na pierwszym planie przy pomocy polecenia fg %1.
Nazwane potoki
Użytecznym narzędziem w programowaniu w bashu są tzw. nazwane potoki (ang. named
pipes). Potok jest również czasem nazywany kolejką FIFO. W następujący sposób tworzymy
nowe potoki.
cd /tmp
mkfifo pipe1
Używając polecenia ls można teraz zobaczyć, że pipe1 jest widoczny w katalogu /temp.
Nie jest to jednak zwykły plik! Nic on nie przechowuje na dysku. Gdy spróbujemy coś do
niego zapisać, proces zostanie wstrzymany aż do momentu gdy ktoś inny spróbuje coś z
niego odczytać. I odwrotnie: proces czytający będzie czekać aż ktoś spróbuje coś do pliku
potoku zapisać.
Gdy już mamy dwa programy, gdzie jeden czyta a drugi pisze – to dane zostają przekazane
bezpośrednio z jednego programu do drugiego. W ten łatwy sposób można uzyskać sprawną
komunikację między różnymi procesami, która fizycznie nie używa dysku twardego. Niemniej,
z punktu widzenia każdego z programów, mogą one używać pliku potoku tak jak każdego
innego pliku.
Strona 3/10
Środowisko programisty
Zestaw 8
Semestr zimowy 2016/2017
Kraków, 26-27 listopad 2015
Tak więc na przykład:
$ echo "hello" > pipe1 &
[1] 44298
Proces „echo” został uruchomiony, ale jego działanie zostało wstrzymane.
$ cat /tmp/pipe1
hello
[1]+ Done
echo "hello" > pipe
Próbując odczytać zawartość potoku komendą cat, komenda echo została odblokowana
i mogła się szczęśliwie zakończyć.
Z potokiem pipe1 można skojarzyć deskryptor pliku.
exec 3<>pipe1
echo "goodbye" >& 3
Poniżej widzimy ciekawy przykład komunikacji pomiędzy dwoma plikami: serwerem i
klientem używając potoku.
server.sh:
#!/ bin / bash
#Tworzymy nazwany potok: pipe
pipe = $ 1
mkfifo $ pipe
while sleep 1
do
echo " server : sending GO to client "
#Następująca komenda wymusi na procesie czekanie aż inny proces
#(w domyśle klient) przeczyta z pipe
echo GO > $ pipe
#Klient przeczytał stringa. Teraz serwer czeka na odpowiedź.
#Komenda "read" tymczasem będzie czekać aż klient coś wpisze do pipe
read answer < $ pipe
#Klient odpowiedział
echo " server : got answer : $ answer "
done
Strona 4/10
Środowisko programisty
Zestaw 8
Semestr zimowy 2016/2017
Kraków, 26-27 listopad 2015
client.sh:
#!/ bin / bash
pipe = $ 1
#Czekamy aż serwer stworzy pipe’a...
until [ -p $ pipe ]
do
sleep 1;
done
#Zaczyna się komunikacja
while sleep 1
do
echo " client : waiting for data "
#Czekamy aż serwer prześle informację:
read data < $ pipe
#Informacja została odebrana
echo " client : read <$ data > , answering "
#Potwierdzamy odebranie wiadomości.
#Podobnie jak wyżej serwer zostanie zablokowany aż serwer przeczyta
echo ACK > $ pipe
done
Uruchomienie serwera i klienta będzie miało następujący efekt:
$ chmod +x server client
$ ./server.sh pipe & ./client.sh pipe &
server: sending GO to client
client: waiting for data
client: read <GO>, answering
server: got answer: ACK
server: sending GO to client
client: waiting for data
client: read <GO>, answering
server: got answer: ACK
server: sending GO to client
client: waiting for data
[...]
Pisanie prostych skryptów
Operatora & można używać jako separatora komend w bashu
Strona 5/10
Środowisko programisty
Zestaw 8
Semestr zimowy 2016/2017
Kraków, 26-27 listopad 2015
command one & command two & command three &
albo
for i in one two three; do command "$i" & done
Istnieje specjalny parameter $!, który pozwala wyłuskać PID ostatnio uruchomionego
procesu w tle.
myjob &
jobpid=$!
Pisząc kill -0 $PID można sprawdzić, czy proces o PIDzie $PID nadal istnieje. Znanego
nam już polecenia wait można użyć, aby poczekać na zakończenie procesu. Polecenia wait
można jednak użyć tylko w przypadku, gdy proces nadrzędny czeka na wywołany przez
siebie proces podrzędny. Jeśli potrzebujemy czekać na zakończenie procesu, który nie jest
naszym procesem podrzędnym można użyć następującego rozwiązania.
while kill -0 $pid
do
sleep 1
done
W poprzednich notatkach wyjaśniliśmy w jaki sposób uruchomić podpowłoki równolegle.
W tej chwili przyjrzymy się innym sposobom uruchamiania procesów równolegle. Możemy
np. użyć dobrze nam znanego polecenia xargs. Po fladze -P podajemy liczbę zadań
uruchamianych na raz. Domyślnie uruchamiane jest jedno zadanie. Jeśli podamy 0, to
xargs uruchomi tak wiele procesów jak będzie to możliwe.
find . -print0 | xargs -0 -n 1 -P 5 command
Kolejny skrypt uruchamia maksymalnie pięć instancji funkcji my_job. Uruchomienie wait
z flagą -n pozwala czekać na to, aż którykolwiek z uruchomionych procesów się zakończy.
Wtedy na jego miejsce uruchamiamy następny. Zmienna IFS przechowuje ciąg znaków,
które są traktowane jako separatory wejścia. Używa się ich aby dzielić linię wejścia na
osługiwane odzielnie słowa/pola. Domyślnie zmienna IFS przechowuje stringa zawierającego
spację, znak tabulacji oraz znak nowej linii.
#!/bin/bash
# Liczba procesów wykonywanych równolegle
Strona 6/10
Środowisko programisty
Zestaw 8
Semestr zimowy 2016/2017
Kraków, 26-27 listopad 2015
num_procs=5
# funkcja, która przetwarza jedno zadanie
my_job() {
echo "Start $1"
sleep "$(( RANDOM % 5 + 1 ))"
echo "End $1"
}
i=0
while IFS= read -r line; do
if (( i++ >= num_procs )); then
wait -n
# czekamy na zakończenie jakiegokolwiek procesu. od basha 4.3
fi
my_job "$line" &
done < inputlist
wait # czekamy na ostatnie procesy
Podobny efekt jak powyżej możemy uzyskać stosując nazwane potoki.
#!/bin/bash
# FD 3 zostanie skojarzony z nazwanym potokiem pipe.
mkfifo pipe; exec 3<>pipe
# Pojedynczy job do wykonania.
s() {
printf ’Sleeping %s\n’ "$1"
sleep "$1"
}
#
#
{
{
{
Zacztnamy z trzema instancjami.
Za każdym razem, kiedy proces kończy działanie, wpisuje informację do pipe’a.
s 5; printf ’\n’ >&3; } &
s 7; printf ’\n’ >&3; } &
s 8; printf ’\n’ >&3; } &
# Kiedy otrzymuje informacją z pipe’a, uruchamiamy kolejnego job’a.
while read; do
{ s "$(( RANDOM % 5 + 7 ))"; printf ’\n’ >&3; } &
done <&3
Strona 7/10
Środowisko programisty
Zestaw 8
Semestr zimowy 2016/2017
Kraków, 26-27 listopad 2015
Na koniec tej sekcji pokażemy jak można usunąć wykorzystywane w skrypcie nazwane
potoki.
#!/bin/bash
unset kids
fifo=/tmp/foo$$
trap ’kill "${kids[@]}"; rm -f "$fifo"’ EXIT
./server $fifo & kids+=($!)
./client $fifo & kids+=($!)
sleep 10
W skrypcie powyżej uruchamiamy wcześniej zdefiniowane skrypty server.sh i client.sh,
które wykorzystują nazwany potok /tmp/fifo$$ do komunikacji. Wartość $$ jest PIDem
procesu nadrzędnego. Dzięki temu wielokrotne uruchomienie tego skryptu nie spowoduje
konfliktów w komunikacji wielu serwerów i klientów.
Gdy upłynie 10 sekund, skrypt nadrzędny kończy działanie. W normalnych warunkach
skrypty podrzędne serwera i klienta działałyby dalej. Jednakże, przy wyjściu z pliku
wysyłany jest sygnał EXIT. Sygnał ten jest przechwycony komendą trap, o następującej
składni:
trap <command> <signals>
gdzie signals jest listą sygnałów, których wywołanie ma powodować wykonanie komendy
<command>.
W naszym przypadku jest to komenda złożona z dwóch poleceń:
∗ Zakończ wszystkie procesy zapisane w tablicy kids – czyli wartości PID serwera i
klienta
∗ Usuń nazwany potok $fifo – dzięki czemu sprzątamy po sobie
Polecenia top and htop
Zarządzać procesami można przy użyciu poleceń top lub htop. Aby uzyskać pomoc w
trakcie działania któregoś z tych programów, wciskamy jeden z klawiszy h lub ?. Wyjście
odbywa się przy pomocy q. htop jest bardziej przejrzysty i wygodny w obsłudze niż
top, ale ten drugi z kolei oferuje możliwość zapisu raportu do pliku. Poniższe akapity są
poświęcone programowi top.
W celu zobaczenia procesów należących wyłącznie do użytkownika user1 możemy użyć
komendy: top -u user1. Alternatywnie, w trakcie działania programu top wciskamy
Strona 8/10
Środowisko programisty
Zestaw 8
Semestr zimowy 2016/2017
Kraków, 26-27 listopad 2015
klawisz u, wpisujemy user1 i akceptujemy. Jeżeli nie napiszemy żadnej nazwy, to zobaczymy
procesy wszystkich użytkowników. Kończymy działanie danego procesu wciskając k i
podając jego PID. Przy pomocy polecenia top -p pid1, pid2, ... (np. top -p 1, 3)
możemy śledzić wyłącznie procesy o podanych PIDach (w przykładzie: 1 i 3). Klawisze z
i b wpływają na oznaczenie aktywnych procesów.
Procesy możemy sortować względem wybranej kolumny. Domyślnie posortowane są po
procentowym zużyciu procesora (klawisz P). Jeśli chcemy ułożyć je po zużyciu pamięci,
to używamy klawisza M. W przypadku innego sortowania używamy O lub F, natomiast R
odwraca sortowanie. Możemy także decydować o wyborze kolumn do wyświetlenia (f) i ich
kolejności (o). Klawisz c (jak i opcja -c przy uruchomieniu) sprawia, że możemy przełączać
się między mniej i bardziej szczegółowym widokiem komend związanych z poszczególnymi
procesami. Przy pomocy l, t i m można włączać lub wyłączać podsumowania nad listą
procesów.
Domyślny czas, po którym odświeżana jest lista procesów wynosi 3 sekundy. Aby go
zmodyfikować, używamy albo komendy top -d liczba_sekund, albo klawiszy d lub s
w trakcie działania polecenia top. Jeżeli chcemy natychmiast poznać aktualny stan listy
procesów, to używamy enter bądź spacji.
W celu zapisania aktualnego stanu procesów do pliku używamy polecenia:
top -bn 1 > plik
Wykonywać różne zadania w tym samym czasie z przełączaniem się na pierwszy plan i tło
można także przy użyciu polecenia screen. W praktyce ten program tworzy w osobnych
procesach wirtualne terminale, między którymi można się łatwo przemieszczać. Podobnie
jak w przypadku procesów działających w tle, wirtualne terminale będą istnieć także po
zakończeniu sesji.
Taki wirtualny terminal można zainicjalizować, wpisując screen komenda lub po prostu
screen, a potem wpisując komendę. W tym pierwszym przypadku jednak wirtualny
terminal zniknie wraz z zakończenie procesu. Przykładowo:
screen sleep 2000
Aby wrócić do głównego terminala, należy użyć kombinacji Ctrl-a d. W ten sposób
można opuścić wirtualny terminal, jednocześnie nie usuwając go. Przekonamy się o tym,
wpisując komendę screen -ls, która wylistuje aktualne sesje screena. Wcześniej jednak
wywołamy screen -S nazwa, która utworzy nam wirtualny terminal o ustalonej nazwie
i opuścimy go. Zatem:
screen -ls
−→There are screens on:
−→
5506.pts-0.tcs (Detached)
−→
8367.nazwa
(Detached)
−→2 Sockets in /tmp/uscreens/S-kosinski.
Poszczególne screeny wypisane są w postaci: PID.nazwa_terminala[.host]. Jeżeli chcemy
Strona 9/10
Środowisko programisty
Zestaw 8
Semestr zimowy 2016/2017
Kraków, 26-27 listopad 2015
wejść do jednego z tych wirtualnych terminali (powiedzmy: 5506.pts-0.tcs), to wpisujemy
jedno z następujących poleceń (ostatnie dwa z nich zadziałają, o ile nazwa wraz z ewentualnym
hostem jest unikalna):
screen -r 5506.pts-0.tcs
screen -r 5506
screen -r pts-0
screen -r pts-0.tcs
Ciekawostka: możemy opuścić aktualny wirtualny terminal z innego. . . “fizycznego” terminala,
wpisując screen -d identyfikator, gdzie identyfikator jest taki, jak w przypadku
opcji -r.
Wewnątrz screena tworzymy kolejne wirtualne podterminale przy pomocy kombinacji
Ctrl-a c. Możemy się między nimi przemieszczać przy pomocy kombinacji:
Ctrl-a n
następny podterminal,
Ctrl-a p
poprzedni podterminal,
Ctrl-a 0-9
skok do podterminala o podanym numerze (od 0 do 9),
Ctrl-a ’
skok do podterminala o numerze podanym po ’,
Ctrl-a "
przejście do menu wyboru podterminala,
Ctrl-a Ctrl-a przemieszczenie się między 2 ostatnio używanymi podterminalami.
Ogólnie wewnątrz screena używamy poleceń, które zaczynają się od Ctrl-a (patrz pomoc
– Ctrl-a ?). W celu zamknięcia podterminala używamy Ctrl-a K, ewentualnie komendy
exit. Po zamknięciu wszystkich podterminali w danym screenie, ten ostatni kończy
działanie.
Strona 10/10

Podobne dokumenty