SOP322/ćw - temat B
Funkcje systemowe Unixa
Pojęcia Unix-owe, które powinniśmy rozumieć:
- proces macierzysty i potomny, uruchamianie programów:
- tworzenie procesu potomnego, fun. sys. fork()
- oczekiwanie na zakończenie procesu potomnego, fun. sys. wait()
- uruchamianie programów w procesie, fun. sys. exec*()
- deskryptory do plików, przeadresowywanie, fun. sys. dup2()
- łącza nienazwane; deskryptory do końcówek łączy, prawa rządzące
łączami
- parametry i środowisko (zmienne środowiska) procesu/programu
Stare materiały o funkcjach systemowych
Unixa (bardziej szczegółowe niż obecne!).
1. Literatura
Stevens "Programowanie sieciowe w Unix-ie", rozdział 2; jest tam
krótki opis najważniejszych funkcji systemowych Unix-a.
Grey "Komunikacja między procesami w Unix-ie", rozdział 10 o
gniazdkach, 11 o wątkach.
Gabassi, Dupouy "Przetwarzanie rozproszone w systemie Unix",
rozdział 4 o gniazdkach.
Rago "Unix System V Network Programming".
The Linux Programmer's Guide (po
polsku)
2. Tworzenie procesów i uruchamianie programów
Tworzenie procesów ...
int fork ( void );
proces, który wywołał fork() "rozdwaja się" - mamy teraz proces macierzysty
i proces potomny, który początkowo jest dokładną kopią procesu
macierzystego
- w procesie potomnym fork() zwraca 0
- w procesie macierzystym fork() zwraca PID procesu potomnego
proces potomny ma taką samą tablicę deskryptorów jak proces
macierzysty (czyli pliki otwarte w procesie macierzystym pozostają
otwarte w procesie potomnym !);
proces potomny tak samo obsługuje sygnały, jak proces macierzysty;
proces potomny ma takie same zmienne środowiska jak proces
macierzysty;
void exit(int kod_zakończenia)
funkcja "exit()" - powoduje zakończenie procesu potomnego,
przekazany parametr może być odczytany przez proces macierzysty.
gdy proces się kończy, to niektóre(?) zasoby zarezerwowane przez
proces zostają zwolnione; np wszytkie otwarte pliki zostaną zamknięte
int wait ( int *status );
wait() powoduje, że proces macierzysty czeka na zakończenie
(dowolnego) procesu potomnego
wartością zwracaną przez wait() jest PID procesu potomnego, który
się zakończył
w zmiennej "status" jest zwracana informacja o sposobie
zakończenia
działania potomka;
niech: status == HHLL (4 cyfry hex)
- potomek zakończył się przez wywołanie "exit(y)"; wtedy HH=y, LL=0
- potomek zakończył się z powodu sygnału; wtedy HH=0, 7-my bit LL
zawiera 1 jeśli wygenerowano plik "core", bity 6-0 LL zawierają nr
sygnału
Teraz mały przykład (fork01.cc).
Ten sam przykład bez komentarzy dotyczących języka "C" (fork01a.cc).
Uruchamianie programów w procesach ...
int exec*(...)
int execl (
const char *path,
const char *arg,
... );
int execv (
const char *path,
char * const argv[ ] );
int execle (
const char *path,
const char *arg,
...
char * const envp[ ] );
int execve (
const char *path,
char * const argv[ ],
char * const envp[ ] );
int execlp (
const char *file,
const char *arg,
... );
int execvp (
const char *file,
char * const argv[ ] );
funkcja "exec()" zastępuje kod bieżącego procesu nowym
kodem
nowy program dziedziczy po procesie,
który
wywołał "exec" wiele rzeczy (np może dziedziczyć otwarte deskryptory
plików)
pierwszy parametr funkcji "exec*()" identyfikuje plik z nowym
programem
istnieją różne sposoby przekazywania "parametrów"
i "środowiska" do nowego programu; literki
w nazwie funkcji "exec*()" maja następujące znaczenie :
"v" oznacza, ze parametry są przekazywanie przez tablice wskazań
char *v[]={"ls", "-l", NULL};
execv("/bin/ls", v);
"l" oznacza, ze parametry są przekazywanie przez listę
execl("/bin/ls", "ls", "-l", NULL);
"p" nie potrzeba podawać pełnej ścieżki do pliku z programem;
będzie wykorzystywana zmienna PATH
execlp("ls", "ls", "-l", NULL);
"e" oznacza, ze podajemy nowe środowisko za pomocą tablicy wskazań
do napisów "zmienna=wartosc".
char *v[]={"zmienna1=wart1", "zmienna2=wart2", NULL};
execle("./prog", "prog", "par1", "par2", NULL, v);
UWAGA: jeśli nie ma literki "e" to środowisko pozostaje takie
jakie było przed wykonaniem exec*() !!!
Jest tu mowa o "parametrach" i "środowisku" które są dostępne w programie
(napisanym w języku "C") przy pomocy parametrów "argv" i "env" w fun.
main() :
main(int argc, char *argv[], char *env[])
{
int i; char **c;
for(i=0; i<argc; i++) printf("%s\n", argv[i]);
// wyswietlanie argumentow
c=env; while(*c) printf("%s\n", *c++);
// wyświetlanie środowiska
c=environ; while(*c) printf("%s\n", *c++);
// wyświetlanie środowiska innym sposobem
}
Środowisko jest dostępne także przy pomocy zmiennej globalnej
"environ". Ponadto proces może odczytywać i modyfikować swoje
środowisko przy pomocy funkcji putenv() i getenv().
UWAGA: putenv() i getenv() NIE SĄ funkcjami
systemowymi.
Parametry i zmienne środowiska służą do
przekazywania danych z procesu macierzystego do procesu potomnego w momencie tworzenia procesu potomnego. |
Teraz mały przykład (fork02.cc). W
przykładzie tym tworzona jest zmienna środowiska o nazwie QWERTY i
wartości 123456; następnie proces tworzy proces potomny który
dziedziczy
m.in. zmienne środowiska, a więc i zmienną QWERTY. Potem proces potomny
uruchamia program "env" wyświetlający zmienne środowiska. Po wywołaniu
execl() proces posiada te same zmienne środowiska co przed wywołaniem
execl(), dlatego zmienna QWERTY także zostanie wyświetlona.
Zadanie
40
Napisz program podobny do "fork01.cc", który stworzy "drzewko
procesów", takie jak na następującym rysunku; obok procesów podane jest
ile czasu mają "spać"; spanie realizujemy uruchamiając program sleep (a
nie przy pomocy fun.sys/bibl sleep() !!!); procesy macierzyste czekają
na procesy potomne; wszystkie procesy powinny wyświetlać napisy
informujące o swoich poczynaniach podobnie jak to było w przykładzie
"fork01.cc"; użyj komentarzy "// pm", "// pp1", "// pp2" itp aby
ułatwić
sobie zadanie !).
3. Łącza
Łącze to kanał komunikacyjny pozwalający procesom komunikować się
(wysyłać ciąg bajtów). Każde łącze posiada dwie "końcówki": jedną
do zapisu, drugą do odczytu.
Końcówki są dostępne poprzez deskryptory; zapis i odczyt wykonuje
się przy pomocy funkcji read() i write(), zupełnie tak samo jak w
przypadku zwykłych plików.
Łącze posiada bufor; jeśli bufor jest pusty/pełny to proces chcący
czytać/pisać musi zostac "powstrzymany" (wtedy funkcje read() lub
write() będą blokować).
3.1. Łącza nienazwane
Służą one do komunikacji między spokrewnionymi procesami.
Do tworzenia "łącz nienazwanych" służy fun. sys. pipe() :
int pipe ( int filedes[2] );
Funkcja ta tworzy łącze i przydziela dwa deskryptory, dające dostęp
do jego końcówek, które umieszcza w podanej przez parametr tablicy 2
integer-ów:
- filedes[0] - końcówka do czytania
- filedes[1] - końcówka do pisania
--> jesli czytamy funkcją "read()" z
łącza, ktorego NIKT nie ma otwartego do zapisu, to "read()" zwraca 0
(zupelnie tak, jakby plik się skonczył !).
--> jeśli ktoś ma łącze otwarte do zapisu, lecz jest ono PUSTE -
to fun. "read()" blokuje, tak długo aż nie pojawią się jakies znaki.
Jeśli w pewnym momencie okaże się, ze nie ma procesow, ktore maja to
łącze otwarte do zapisu to fun. "read()" przestanie blokować i zwroci
0.
--> jeśli piszemy funkcją "write()" do łącza, ktorego NIKT nie ma
otwartego do czytania, to jest wysylany sygnal SIGPIPE do procesu
wywołującego write(), a sama funkcja "write()" kończy dzialanie z
błędem.
--> jesli ktoś ma łącze otwarte do
czytania, lecz uległo ono PRZEPEŁNIENIU, to fun. "write()" bedzie
blokować, o ile liczba zapisywanych danych jest <= od stałej
PIPE_BUF
(jeśli jest większa od PIPE_BUF to write() może zapisać mniej niż
zażądaliśmy ! [wg książki Bacha, str 127]). Jesli w
pewnym
momencie okaże się, że nie ma procesów, ktore maja to łącze otwarte do
czytania to fun. "write()" zakonczy się błędem i do procesu
wywołującego
write() będzie wyslany SIGPIPE.
Przykład użycia pipe() w programie, w języku C:
struct {int do_czytania,do_pisania;} L;
// struktura "L" pozwala na bardziej czytelny dostęp do końcówek łącza
// niż tablica 2 integer-ów !
pipe((int *)&L);
write(L.do_pisania, "ABC", 3);
// zapis do łącza
char buf[10]; int i;
i=read(L.do_czytania, buf, 10);
// odczyt z łącza
Zadanie
41
Napisz program uruchamiający "cat" w procesie potomnym. Proces
macierzysty ma się z nim komunikować przy pomocy 2 łączy. Proces
macierzysty wysyła napisy do procesu potomnego i otrzymuje od niego
"echo" (które wyświetla na terminalu). Gdy proces macierzysty zamknie
łącze do zapisu to proces potomny powinien się automatycznie zakończyć.
Przypominam że program "cat" przepisuje stdin na stdout.
3.2. Potoki
--> co to jest potok ?
Przez potok rozumiemy kilka procesów, np p1, p2, p3, p4, komunikujących
się przez łącza
miedzy parami procesow: p1->-p2, p2->-p3, p3->-p4. Informacja
płynie więc w jedną
stronę jak woda w potoku.
Przyklad "potok.cc" (+unix.h).
Jest to program
tworzący potok z komend podanych jako parametry, w którym NIE MOŻNA
podawać parametrów komend. Sposób użycia:
potok ls cat cat cat cat
Przyklad "potok2.cc". Jest to program tworzący
potok z komend podanych jako parametry, w którym MOŻNA podawać
parametry
komend. Sposób użycia:
potok2 ls -l \| cat \| cat \| grep txt \| cat
Zadanie 42
Odpowiedz na pytania:
- jak wygląda "pokrewieństwo" procesów
tworzonych przez "potok2.cc" ?
- jaką własność powinien mieć program
uruchamiany "w środku" potoku przy pomocy jednego z powyższych
programów aby wszystko działało tak jak się tego spodziewamy ? (ma to
związek z stdin i stdout czyli deskryptorami 0 i 1)
- jeśli wydamy polecenie "potok cat cat cat
cat" to wprowadzane linie tekstu będą przepisywane na terminal; jeśli
naciśniemy Ctrl-D to 4 procesy "cat" będą się prawidłowo i po kolei
kończyć; dlaczego tak się dzieje ? - wyjaśnij to dokładnie
- co by się stało i dlaczego gdyby
funkcja ZamknijLacza() była "pusta",
po uruchomieniu
potok2 cat \| cat \|
cat
i po wpisaniu kilku linii tekstu oraz
naciśnięciu Ctrl-D ?
"Ctrl-D" powoduje że terminal zachowuje
się jak plik który się skończył;
bardziej precyzyjnie: czytanie z
terminala na którym naciśnięto Ctrl-D jest identyczne jak czytanie
(sekwencyjne) z pliku zwykłego gdy bieżąca pozycja przekroczy koniec
pliku; funkcja read() zwraca wtedy wartość 0 |
Dlatego w punkcie 3 i 4 powyższego ćwiczenia
pierwszy "cat" który czyta dane z terminala na pewno zakończy swoje
działanie po naciśnięciu Ctrl-D.
UWAGA: Ctrl-D nie powoduje wysłania sygnału jak
to
czyni Ctrl-C czy też Ctrl-Z !.
Zadanie
43
Do mini-powłoki z przykładu mini03.cc (+unix.h) dodaj
możliwość uruchamiania potoków:
ls -l -a | cat | grep txt | sort | more
Propozycja rozwiązania znajduje się w pliku mini03.cc, należy tylko
wyciągnąć z komentarza co trzeba. W mini03.cc wykorzystuje się
procedurę
z programu "potok2.cc".
Odpowiedz na pytanie: jakie procesy istnieją po wydaniu polecenia "cat
| cat | cat" i jakie jest między nimi pokrewieństwo ?
Do sprawozdania wstaw tylko wyniki eksperymentów:
cat | cat | cat
# wpisz z klawiatury jakiś tekst +enter
# i wszystkie wydruki generowane przez powłokę umieść w sprawozdaniu
Ponadto pokaż wszystkie procesy utworzone przez mini-powłokę po uruchomieniu
powyższego potoku (polecenie "ps -t terminal" wydane z innego terminala).
Zadanie
44
Do naszej mini-powłoki dodaj komendę wbudowaną "export" służącego do
tworzenia zmiennych środowiska w następujący sposób:
export zmienna = wartość
# dla uproszczenia zakładamy że wartość jest jednym słowem !
Wskazówka: Środowisko uruchamianego programu ma
zawierać zmienne środowiska zdefiniowane w mini-powłoce poleceniem
"export" ORAZ niektóre przydatne zmienne np HOME i PATH. Należy
zadeklarować zmienną zewnętrzną:
extern char **environ;
oraz wykorzystać funkcję systemową execvp(), która używa
tablicy
environ aby utworzyć środowisko potomnego procesu. Element tablicy
environ znajdujący się za ostatnim niepustym elementem powinien mieć
przypisany NULL. Nie musimy nadmiernie dbać o wykrywanie błędów we
wprowadzanych komendach. Wypróbuj działanie mini-powłoki uruchamiając
program wyświetlający zmienne środowiska, np env. Do sprawozdania wstaw
kod mini-powłoki oraz wyniki eksperymentów dowodzące że komenda export
rzeczywiście działa.
Zadanie 45
"CYKL PROCESÓW"
Napisać program "cykl", podobny do programów z serii "potok*",
pozwalający uruchamiać programy "w cyklu". Sposób uruchomienia :
cykl ile_proc prog -par1 -par2
Parametr "ile_proc" oznacza, że ten sam program "prog", ma działać
w "ile_proc" procesach. Parametry "-par1" i "-par2" są przekazywane do
wszystkich kopii "prog".
Programy mają się komunikować poprzez deskryptory "3" i "4"
przeadresowane do odpowiednich łączy ("3" oznacza poprzedni proces, "4"
oznacza następny proces w cyklu); patrz obrazek.
UWAGA: jedyny
dopuszczalny sposób komunikacji między procesami to
wysyłanie/odbieranie
komunikatów do/od sąsiada (zabrania się używania semaforów itp
...), jest to tzw "model obliczeń z przesyłaniem komunikatów"
który jest przeciwieństwem "modelu obliczeń ze wspólną pamięcią".
Zakładamy, że połączenia w cyklu są jednokierunkowe (komunikaty
mogą przebiegać tylko w jedną stronę).
Jeśli jeden z procesów "prog" się zakończy to wszystkie pozostałe
procesy też powinny się zakończyć (należy wykorzystać mechanizmy
automatycznego kończenia się potoków).
Zauważ, że program "cykl" tworzy jedynie infrastrukturę - cykl
procesów, które mogą się komunikować (z bezpośrednimi sąsiadami)
poprzez
łącza. Procesy będą wykonywać program podany jako drugi parametr
polecenia "cykl".
Jako przyklad zastosowania zrealizuj algorytm "Leader
Election in Rings", opisany w pliku leaderel.ps
(który należy czytać przegladarka PS), w punkcie 2.3.1 na stronie
15. Realizacja tego algorytmu powinna być programem o nazwie
"leaderel", który będzie można przetestować następująco:
cykl 20 leaderel
Każdy proces "leaderel" powinien tuż przed swoim zakończeniem
wyświetlać PID oraz informacje czy został liderem czy nie.
Wskazówki: przy przeadresowywaniu końcówki łącza na 3 lub
4,
weź pod uwagę że końcówka innego, wcześniej utworzonego łącza może mieć
deskryptor 3 lub 4; ten problem trzeba jakoś rozwiązać !!!
przemyśl sposób w jaki program "cykl" kończy działanie; czy na
pewno wszystkie tworzone przez niego procesy się kończą ?
komunikaty algorytmu wyboru lidera zawieraja binarna reprezentację
liczb (identyfikatorow); zazwyczaj typ int jest reprezentowany
na 4 bajtach; można przyjąć że takie komunikaty są "niepodzielne" tj
jeśli do łącza zapisano 4 bajty wywołując raz funkcję write()
to
te 4 bajty zostaną odczytane w całości pojedynczym wywołaniem
funkcji read() - (?)
komunikaty diagnostyczne wyświetlane przy pomocy printf() kończ
zawsze przez "\n", np printf("ok\n"); chodzi o wypróżnienie bufora
tworzonego przez język C
dobrym zwyczajem jest sprawdzanie czy funkcje systemowe nie kończą
się błędem (zwracają (-1) i ustawiają odpowiednio zmienną errno, opis
błędu można wyświetlić funkcją perror())
Różne wnioski:
Jak mogliśmy się przekonać, program czytający dane przy pomocy
deskryptora "d1" i wyświetlający wyniki działania przy pomocy
deskryptora "d2", może bez żadnych zmian wspólpracować z terminalami,
z łączami czy też z plikami zwykłymi (dzięki możliwości
"przeadresowywania" tych deskryptorów); jest to bardzo eleganckie
rozwiązanie, oszczędzające pracę programistów ...
Warto pamiętać że "prawa" rządzące łączami rządzą także innymi
narzędziami do przesyłania strumienia bajtów (takimi jak np gniazda
BSD).
Zauważmy że łącze to gotowe rozwiązanie jednej z wersji
problemu "producenta & konsumenta" (jednak tutaj jest możliwość
produkcji i konsumpcji więcej niż jednego elementu !).
Zadanie
46
Napisz program "testcyklu", który może być uruchamiany w przy pomocy
programu "cykl" z powyższego zadania, a którego celem jest zbadanie
czasu przebiegu komunikatu przez cykl. Program "testcyklu"
powinien zawierac procedurę JestemLiderem() zwracająca 1 w
procesie który został wybrany jako lider, a 0 w pozostałych procesach.
Program "testcyklu" powinien najpierw wywołać powyższą procedurę, a
następnie lider powinien 50-krotnie wysłać komunikat kontrolny do 4 i
odebrac go z 3 [sprawdzając czy nie wystąpił jakiś błąd]. Czas
powyższej
operacji powinien być mierzony w następujący sposób:
#include <time.h>
timeval czas0, czas1;
gettimeofday(&czas0,0);
//
// operacje których czas mierzymy
//
gettimeofday(&czas1,0);
RoznicaCzasu(czas0, czas1);
void RoznicaCzasu(timeval &c0, timeval &c1)
{
if( c0.tv_sec==c1.tv_sec)
printf("czas=%li\n", (long)(c1.tv_usec-c0.tv_usec));
else
printf("czas=...\n");
}
Program należy przetestować następująco:
cykl 10 testcyklu
cykl 20 testcyklu
cykl 40 testcyklu
Czy czas przebiegu komunikatu zależy liniowo od długości cyklu ?