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