SOP121/ćw - temat D.
Unix: funkcje systemowe.
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.cc -o prog01
gdzie "prog01.cc" to nazwa pliku z programem w języku "C++" (rozszerzenie
*.cc --> program w języku "C++", rozszerzenie *.c --> program w języku
"C" ), natomiast "prog01" to nazwa binarnego pliku wykonywalnego
(tj z prawem "x"). 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").
Tworzenie procesow, uruchamianie programow;
fork(), wait(), exec*(), exit()
UWAGA: w opisach funcji systemowych odwołujemy się
niekiedy do pojęć, które zostaną zdefiniowane później ! (np "deskryptor",
"sygnał").
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
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).
Zadanie 50
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 51
Napisz program tworzący "mumię", która istnieje 60s. Program
powinien wyświetlać m.in. następujące napisy:
mumia - początek
mumia - koniec
Mumię tę należy zaobserwować przy pomocy polecenia "ps".
Definicja mumii: Proces potomny staje się mumią jeśli
się zakończył, jeśli istnieje jego proces macierzysty, oraz jeśli proces
macierzysty nie "czeka" na zakończenie tego procesu potomnego (przy pomocy
funkcji wait()).
UWAGA: to ćwiczenie może się nie udać jeśli polecenie "ps" nie pokazuje
mumii.
Zadanie 52 (**)
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 ! kazda klamre {} koncz przez exit()
aby miec pewnosc ktory fragment kodu wykonuje sie w ktorym procesie).
Teraz pokażemy, jak w procesie potomnym uruchomić
inny program;
służą do tego celu funkcje z rodziny exec*() ...
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 atrybutów
(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 wykonaneim 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 2 (patrz niżej)
}
Środowisko jest dostępne także przy pomocy zmiennej globalnej "environ".
Proces może odczytywać i modyfikować swoje środowisko przy pomocy funkcji
putenv() i getenv().
UWAGA: putenv() i getenv() NIE SĄ funkcjami systemowymi.
Teraz mały przykład (fork02.cc):
Zadanie 53
Uruchom przykład "fork02.cc" oraz wyjaśnij, dlaczego program "env"
wyświetla zmienną środowiska "QWERTY". Potem zamień program "env"
na "pr01" wyświetlający parametry i środowisko (uruchom go z parametrami
"par1" "par2" "par3").
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);
Zadanie 54
Wypróbuj powyższy przykład (musisz napisać kompletny program w języku
"C"). Zwróć uwagę od czego zależy wartość zmiennej "i" !
Zadanie 55
Napisz mini-powloke, która czyta linie z terminala, np takie:
ls -l kat1
ps -O ppid,user
traktuje je jak "komendy" i wykonuje (oczywiście, w procesie potomnym).
Powłoka czeka na zakończenie działania każdego uruchomionego programu.
Szkielet programu znajduje się w pliku (mini01.cc),
wystarczy wyciągnąć odpowiednie fragmenty z komentarza. Wypróbuj
działanie mini-powłoki; sprawdź czy po uruchomieniu "sleep 20", gdy powłoka
wyświetli kolejny "$" proces wykonujący sleep już nie istnieje.
Operacje na plikach, przeadresowywanie;
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"
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
);
funkcja "dup()" zwraca nowy deskryptor, dotyczący pliku dostępnego poprzez
podany (otwarty) deskryptor; będzie to pierwszy wolny deskryptor w tablicy
deskryptorów procesu; ten sam plik będzie dostępny poprzez dwa różne deskryptory;
będą one używać tej samej pozycji w tablicy plików, co oznacza że będą
miały tę samą bieżącą pozycję
funkcja "dup2()" zapewnia, że 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",
// 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 56
Przerób przykład fork02.cc tak, aby program
"env" zamiast na terminal pisał do pliku "plik.txt" (w wyniku przeadresowania).
Zadanie 57
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).
Zadanie 58
Napisz mini-powłokę z możliwością "przeadresowywania", np:
ls -l kat1 >plik.txt
cat <plik_we.txt >plik_wy.txt
Możesz wykorzystać plik mini02.cc (+unix.h),
należy tylko usunąć z kodu odpowiednie komentarze; ("mini02.cc" zawiera
także implementację "&").
Sygnały;
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 59 (*)
Do mini-powloki (mini02.cc) dodaj prawidłowe
działanie Ctrl-C. Przypominam, że 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 !.
Łą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ć).
Łą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 60
Uruchom przykład znajdujący się w pliku lacze01.cc+(unix.h)
oraz wyjaśnij kolejność napisów wyświetlanych przez ten program. Następnie
uruchom przykład lacze02.cc i wyjaśnij co
się w nim dzieje.
Zadanie 61 (*)
Napisz program uruchamiający "cat" w procesie potomnym. Proces macierzysty
ma się z nim komunikować przy pomocy 2 łączy. Proces macierzysty wysyła
kilka napisów 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ć.
Poniżej przedstawiam programy pozwalające uruchamiać 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 62
Odpowiedz na pytania:
-
jak wygląda "pokrewieństwo" procesów tworzonych przez
"potok.cc" ?
-
jaką własność powinien mieć program uruchamiany "w
środku" potoku przy pomocy jednego z powyższych programów aby dane
"przepływały" przez potok ? (ma to związek z stdin i stdout)
-
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 zdefiniowana jako {},
po uruchomieniu
potok 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ł |
... 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 63 (**)
Do naszej mini-powłoki dodaj możliwość uruchamiania potoków, np:
ls -l -a | cat | grep txt | sort | more
Propozycja rozwiązania znajduje się w pliku mini03.cc,
należy tylko wyciągnąc z komentarza co trzeba. Wykorzystuje się w nim procedurę
z programu "potok2.cc". Dobrze byłoby, gdyby ta wersja mini-powłoki zawierała
wszystko, co było zrobione w poprzednich wersjach.
Jakie procesy istnieją po wydaniu polecenia "cat | cat | cat" i jakie
jest między nimi pokrewieństwo ?