SOP322/ćw - temat B
Funkcje systemowe Unixa - powtórka?
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
- potoki; jak są zaimplementowane, automatyczne kończenie się
procesów w
potoku
- parametry i środowisko (zmienne środowiska) procesu/programu
1. Wstęp.
Aby proces mógł skorzystać z usług systemu operacyjnego (np odczytać
zawartość
pliku, lub uruchomić inny proces), musi wywołać funkcję systemową.
W systemie operacyjnym UNIX będziemy eksperymentować z funkcjami
systemowymi
przy pomocy języka "C". Wywołanie funkcji systemowej w języku "C"
nie różni się od wywołania funkcji "nie-systemowej", różnica tkwi w
sposobie
implementacji tej funkcji.
Informacje na temat funcji systemowych znajdują się w
rozdziale
drugim manuala UNIX-a:
man 2 fork
man 2 read
Informacje na temat większości funcji "nie-systemowych", np printf()
znajdują się w trzecim rozdziale manuala, po tym - w praktyce - można
rozróżnić,
czy funkcja jest systemowa czy nie.
man 3 printf
Kompilowanie programów pod UNIX-em:
gcc prog01.c -o prog01
// dla jezyka C (programy w C maja rozszerzenie .c)
// w opcji -o podaje sie nazwe pliku wykonywalnego
g++ prog01.cc -o prog01
// dla jezyka C++ (programy w C++ maja rozszerzenie .cc)
Program można potem uruchamiać następująco:
prog01
Jeśli to nie zadziała, za to zadziała "./prog01", to oznacza to, że
brakuje
nam "." w zmiennej PATH; można to naprawić poleceniem:
PATH=$PATH:.
Aby zatrzymać działający program
(pierwszoplanowy)
naciśnij Ctrl-C !
|
Jeśli w trakcie działania funkcji systemowej wystąpi błąd to
zwraca ona wartość (-1), ponadto globalna zmienna errno jest
ustawiana
na odpowiednia wartość (opisującą błąd). Słowne opisy możliwych
wartości zmiennej errno można zobaczyć w pliku
"/usr/include/errno.h". Opis możliwych wartości znajduje się także na
stronach
manuala, dotyczących konkretnej funkcji systemowej. Słowny opis błędu
można
wypisać na ekranie funkcją: perror("tekst pomocniczy").
1.1. Literatura.
Stevens "Programowanie sieciowe w Unix-ie", rozdział 2; jest tam
krótki opis najważniejszych funkcji systemowych Unix-a.
Haviland, Gray, Salema "Unix, programowanie systemowe".
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.
Bach "Budowa systemu operacyjnego UNIX", jest to opis wewnętrznych
algorytmów i struktur danych Unix-a System V Wersja 2.
Rago "Unix System V Network Programming".
The Linux Programmer's Guide
The Linux Programmer's Guide (po polsku)
2. Tworzenie procesów, uruchamianie programów;
funkcje systemowe: fork(), wait(), exec*(), exit().
Opis funkcji systemowych:
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).
Zadanie 35
W przykładzie "fork01.cc" zaobserwuj jakie informacje zwraca
"wait()",
gdy proces potomny kończy się na dwa sposoby:
- gdy kończy się z powodu wywołania "exit()"
- gdy kończy się z powodu otrzymania sygnału SIGINT (czyli nr 2)
(Wskazówki dotyczące punktu "2.")
Zwiększ "czas spania" procesu "pp" do 60 lub 120 sekund, aby zdążyć
to wszystko zrobić !
Jak sprawdzić jakie mamy procesy ?
ps -O ppid
PID PPID CMD
111 222 fork01
333 111 fork01
Jak wysłać sygnał SIGINT do procesu "pp" ?
kill -int 333
Zadanie 36
Napisz program podobny do "fork01.cc", który stworzy "drzewko
procesów",
takie jak na następującym rysunku (obok procesow podane jest ile czasu
mają "spać"; 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 !).
Teraz pokażemy, jak w procesie potomnym
uruchomić
inny program ...
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 37
W przykładzie (fork02.cc) zamień program
"env" na "pr01" wyświetlający parametry i środowisko (uruchamiaj go z
parametrami "par1" "par2" "par3"). Program "pr01" należy napisać
samodzielnie.
.........................................................................................
Nieco wyprzedzając, podamy teraz opisy funkcji służących do
czytania/pisania
z/do otwartego deskryptora udostępniającego plik. Przypomnijmy że
każdy proces ma zaraz po uruchomieniu otwarte deskryptory 0, 1, 2
(stdin,
stdout, stderr) pozwalające czytać/pisać z/do terminala.
int read( int filedes, void *buffer,
int nbytes );
próbuje wczytać podana liczbę bajtów (nbytes) z pliku o podanym
deskryptorze
(filedes) do podanego bufora; bieżąca pozycja w pliku przesuwa się o
tyle,
ile bajtów przeczytano
read() zwraca ilość bajtów naprawdę przeczytanych (zawracana wartość
może
być mniejsza od nbytes !)
gdy "bieżąca pozycja" przekroczy koniec pliku, to read() zwraca 0
int write( int filedes,
void *buffer, int nbytes );
działa podobnie do read(), z ta różnicą, że pisze do pliku zamiast
czytać
A oto mały przykład, w którym odczytujemy z stdin co najwyżej 100
znaków i przepisujemy je na stdout (przepisujemy dokładnie tyle znaków
ile wczytaliśmy):
char buf[100]; // deklaracja tablicy 100 znaków
int i,j;
i=read(0, buf, 100);
j=write(1, buf, i);
printf("i=%i, j=%i\n", i, j);
3. Operacje na plikach, przeadresowywanie;
funkcje systemowe: open(), close(), read(), write(), dup(), dup2().
Otwarcie pliku, to przygotowanie pliku do
przetwarzania.
Po otwarciu pliku otrzymujemy deskryptor, przy pomocy którego
można
się potem odwoływać do pliku - wykonywać operacje zapisu i
odczytu.
Deskryptor to liczba całkowita.
Pamiętajmy, że w UNIX-ie rozmaite urządzenia
(np
terminale) są dostępne poprzez pliki - są to tzw pliki specjalne,
znajdujące
się zwyczajowo w katalogu "/dev". Przykłady: dostęp do
terminala
można uzyskać otwierając plik "/dev/tty??" i pisząc lub czytając
z tego pliku; "surowy" dostęp do dyskietki (traktowanej jako ciąg
bajtów) uzyskujemy otwierając plik "/dev/fd0".
Istnieją deskryptory o specjalnym znaczeniu:
0,
1, 2 - standardowe wejście, wyjście i wyjście błędów. Jeśli
uruchomimy
program z powłoki poleceniem:
prog01
to od samego początku, w programie tym będą
otwarte
deskryptory 0,1,2 i będą one związane z terminalem.
Jeśli uruchomimy program inaczej:
prog01 >plik.txt
to deskryptory 0 i 2 będą związane z terminalem,
natomiast deskryptor 1 będzie związany z plikiem "plik.txt"
(jest
to tzw przeadresowanie stdout do pliku)
Oto pojęcia niezbędne aby posługiwać się
plikami
pod Unix-em:
->
bieżąca pozycja w otwartym pliku
->
tablica
deskryptorów procesu (deskryptory to indeksy elementów tej tablicy)
->
tablica
plików (tu przechowuje się "bieżącą pozycję")
->
tablica
i-węzłów (każdy element tej tablicy identyfikuje plik)
Każdy proces ma własną tablicę deskryptorów.
W danej maszynie istnieje jedna tablica plików i jedna tablica
i-węzłów.
Na poniższym rysunku deskryptory 10,11,12 udostępniają ten sam plik
"plik.txt". Jeśli teraz przesuniemy bieżącą pozycję poprzez desk
10 to ma to wpływ na bieżącą pozycje poprzez desk 11 lecz nie poprzez
desk
12. Gdy otwieramy dwukrotnie ten sam plik funkcją open() -
opisaną poniżej - to otrzymamy różne pozycje w tablicy plików.
Gdy
duplikujemy deskryptor funkcją dup()/dup2() lub wykonujemy fork() to
otrzymamy
tę samą pozycję w tablicy plików.
Opis funkcji systemowych związanych z plikami:
int
open(
const char *path, int oflag [ , mode_t mode ] );
służy do otwierania pliku o nazwie podanej przez "path"; zwraca
deskryptor
bity parametru "oflag" pozwalają określić następujące rzeczy:
- File Acces Flag (tylko jeden bit ustawiony)
O_RDONLY - The file is open for reading only.
O_WRONLY - The file is open for writing only.
O_RDWR - The file is open for reading and writing.
- File Status Flag
--specjalne przetwarzanie przy otwieraniu pliku
O_CREAT - jeśli plik nie istniał to zostanie utworzony
O_EXCL - razem z O_CREAT kończy się błędem, jeśli plik istnieje
O_TRUNC - jeśli plik istnieje to zostanie "skrócony do zera"
--początkowy stan otwartego pliku
O_APPEND - bieżąca pozycja pliku jest ustawiana na końcu
przed każdym zapisem
O_NONBLOCK, O_NDELAY - fun. sys. dotyczące tego pliku
nie będą blokować
jeśli jest tworzony nowy plik (O_CREAT), to używane jest "mode",
zawiera
ono prawa dla nowo tworzonego pliku; parametr "mode" można podawać tak
jak ósemkowy parametr dla polecenia "chmod" (uwaga na "umask")
int close( int filedes );
zamyka plik o deskryptorze "filedes"
int read( int filedes, void
*buffer, int nbytes );
próbuje wczytać podana liczbę bajtów z pliku o podanym deskryptorze
do
podanego bufora; bieżąca pozycja w pliku przesuwa się o tyle, ile
bajtów
przeczytano
read() zwraca ilość bajtów naprawdę przeczytanych (zawracana wartość
może
być mniejsza od nbytes !)
gdy "bieżąca pozycja" przekroczy koniec pliku, to read() zwraca 0
UWAGA:
Jeśli czytamy z terminala i akurat nie ma nic do
przeczytania, to wtedy funkcja read() będzie blokować, tak
długo
aż nie pojawią się jakieś znaki do przeczytania. Podobnie,
funkcja
wait() blokuje proces macierzysty, gdy oczekuje on na zakończenie
swojego
procesu potomnego.
Blokowanie NIE JEST realizowane przez "aktywne
czekanie", lecz przez wprowadzenia procesu w "stan uśpienia"; gdy
pożądane
zdarzenie się zdarzy to proces zostanie "obudzony"; dzięki temu proces
zablokowany podczas wykonywania funkcji systemowej nie zużywa czasu
procesora;
w systemie może istnieć bardzo wiele procesów w stanie uśpienia lecz
nie
ma to wpływu na szybkość działania pozostałych procesów !.
int write( int filedes, void
*buffer,
int nbytes );
działa podobnie do read(), z ta różnicą, że pisze do pliku zamiast
czytać
int dup( int filedes );
int dup2( int filedes, int new
);
funkcje "dup()" i "dup2()" służą do powielania deskryptorów
(otwartych)
funkcja "dup()" zwraca nowy deskryptor, który jest związany z plikiem
dostępnym
poprzez deskryptor "filedes"; będzie to pierwszy wolny deskryptor w
tablicy
deskryptorów procesu; ten sam plik będzie dostępny poprzez dwa różne
deskryptory; uwaga:
będą one używać tej samej pozycji w tablicy plików co oznacza że zawsze
będą miały tę samą bieżącą pozycję !
funkcja "dup2()" zapewnia, że tym nowym deskryptorem będzie "new";
pozwala
ona dokonać tzw przeadresowania w
następujący
sposób:
int desk=open("plik.txt",O_RDWR|O_CREAT|O_TRUNC,0600);
dup2(desk,1);
close(desk);
write(1,"ABC",3);
// to jest zapis na "stdout" (czyli deskryptor 1),
// ale w rzeczywistości wszystko idzie do "plik.txt",
// czyli "stdout" zostało przeadresowane do pliku "plik.txt"
off_t lseek ( int filedes, off_t
offset,
int whence );
ustawianie "bieżącej pozycji" w pliku
parametry:
offset:
o ile przesunąć bieżącą pozycję
whence:
SEEK_SET - względem początku pliku
SEEK_CUR - względem bieżącej pozycji
SEEK_END - względem końca pliku
jedyna różnica w porównaniu z DOS-em: można ustawiać bieżąca pozycje
daleko
poza końcem pliku i pisać, "dziura" zostanie wypełniona zerami; lseek()
"samo w sobie" nigdy nie zwiększa rozmiaru pliku
Zadanie 38
Przerób przykład fork02.cc tak aby program
"env" zamiast na terminal pisał do pliku "plik.txt", czyli dokonaj
odpowiedniego
przeadresowania przed wywołaniem exec(); przypomnij sobie opis
funkcji exec().
UWAGA: Pozostałe napisy wyświetlane przez fork02.cc NIE maja się
znaleźć
w tym pliku !.
Zadanie 39
W przykładzie open01.cc (+unix.h)
pokazuje się, jaka jest różnica między:
a) otwarciem pliku i powieleniem deskryptora przy pomocy funkcji
"dup()"
lub "dup2()";
b) dwukrotnym otwarciem tego samego pliku funkcją "open()"
Wyjaśnij działanie tego programu (dlaczego wyświetla takie a nie inne
napisy).
4. Sygnały;
funkcja systemowa: signal().
Do procesów mogą docierać tzw sygnały. Mogą one być
wysyłane
przez system operacyjny lub przez inne procesy. Sygnały informują
o wystąpieniu jakiejś nieprawidłowości (SIG???), lub służą do
likwidowania
niepotrzebnych procesów (SIGKILL), lub informują o pewnych zdarzeniach
(SIGINT informuje o naciśnięciu Ctrl-C).
Sygnały docierające do procesu mogą być:
- przechwytywane
- ignorowane
- obsługiwane standardowo
Określamy to przy pomocy fun. sys. "signal()" :
void (*signal( int sig, void
(*function)
(int) )) (int);
przechwytywanie sygnału (czyli definiowanie procedury obsługi
sygnału):
// to jest procedura obsługi sygnału
void procObslugi(int nr_sygnalu)
{ printf("obsługa sygnału SIGINT !!!\n");
}
// instalacja proc obsługi
signal(SIGINT,procObslugi);
ignorowanie sygnału:
signal(SIGINT,SIG_IGN);
standardowa obsługa sygnału:
signal(SIGINT,SIG_DFL);
UWAGA:
Funkcja signal() może działać różnie w zależności od wersji Unixa,
np należy sprawdzić w manualu (man 2 signal) czy w procedurze
obsługującej
przerwanie nie powinno się umieścić kodu ponownie instalującego tę
procedurę.
Nowocześniejsza funkcja do definiowania obsługi sygnałów to
sigaction().
Co się dzieje z obsługą sygnałów po wykonaniu "fork()" ?
- wszystko działa bez zmian
Co się dzieje z obsługą sygnałów po wykonaniu "exec*()" ?
- przechwytywane będą obsługiwane standardowo (bo nie ma "kodu
przechwytującego"
!)
- ignorowane będą nadal ignorowane
- obsługiwane standardowo będą nadal obsługiwane standardowo
Zadanie 40
Napisz mini-powłokę zawierającą następujące elementy:
- uruchamianie komend (programów) z parametrami
ls -l -d kat1
- uruchamianie procesów drugoplanowych
sleep 10 &
- przeadresowywanie
ls -l kat1 >plik.txt
cat <plik_we.txt
>plik_wy.txt
- prawidłowe działanie Ctrl+C
Możesz wykorzystać pliki
mini01.cc i mini02.cc
(+unix.h) w których jest większość kodu (należy go
wyciągnąć z komentarza). Powłoka powinna wyświetlać prompt "$" i w
ogóle powinna się zachowywać jak typowa powłoka Unixowa ...
Wskazówki:
ad 1: wprowadzając komendę trzeba między wszystkie słowa
wstawiać spacje; można zakończyć mini-powłokę klawiszami Ctrl+D;
ad 2: mini-powłoka powinna czekać na zakończenie procesu
pierwszoplanowego, a nie drugoplanowego który się wcześniej skończył
(ten element mini-)
ad 4: Ctrl-C powoduje wysłanie sygnału
SIGINT; standardowa obsługa sygnału SIGINT to zakończenie procesu;
Ctrl-C powinno kończyć proces pierwszoplanowy, natomiast nie
powinno kończyć procesów drugoplanowych oraz samej mini-powłoki.
Do sprawozdania wstaw funkcję main() oraz wydruki eksperymentów
stwierdzające poprawność działania mini-powłoki:
ls >plik1.txt
cat <plik1.txt >plik2.txt
cat <plik2.txt
sleep 100
Ctrl+C
ps
sleep 50 &
sleep 100 &
ps
sleep 5
ps
5. Łą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ć).
5.1. Łącza nienazwane;
funkcja systemowa: pipe().
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.
5.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". 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 naszej mini-powłoki 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 mini-powłoki w najprostszej wersji dodaj komendę wbudowaną
"export" służącego do tworzenia zmiennych środowiska :
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 ?