Opis zadania

Zadaniem będzie uruchomienie programu w języku ANSI C, który pracując w pętli nieskończonej będzie odczytywał jednoliterowe polecenia użytkownika wpisywane z klawiatury, i wykonywał opisane poniżej operacje.

Realizacja każdego polecenia musi nastąpić w podprocesie. Proces główny zajmuje się tylko odczytem polecenia użytkownika (funkcja fgets), uruchomieniem podprocesu do obsługi tego polecenia (funkcja fork), oraz powrotem do odczytu kolejnych poleceń.

Rozróżniamy dwa rodzaje poleceń: terminalowe, które korzystają z terminala do komunikacji z użytkownikiem, a więc proces główny musi czekać na zakończenie procesu potomka przed powrotem do realizacji kolejnych poleceń, oraz polecenia okienkowe, które otwierają własne okienko i nie korzystają z terminala, zatem proces główny nie czeka na ich zakończenie, tylko natychmiast wraca do realizacji kolejnych poleceń użytkownika.

Program powinien po wczytaniu poniższych jednoznakowych poleceń realizować następujące funkcje:
(a) 'd' - uruchom program date do wyświetlenia aktualnej daty i czasu na terminalu,
(b) 's' - uruchomi interpreter poleceń /bin/sh, w którym użytkownik będzie mógł wykonywać dowolne polecenia, po czym będzie musiał wpisać 'exit' żeby zakończyć proces interpretera,
(c) 'x' - uruchomi zegar okienkowy xclock z sekundnikiem (opcja -update 1)
(d) 'e' - wykorzystując program okienkowy zenity z opcją --file-selection pozwoli użytkownikowi wybrać plik, a następnie uruchomi edytor okienkowy xedit z wybranym plikiem
(e) 'q' - zakończenie pracy programu.

Wykonanie ćwiczenia

Wykorzystując poniższy szkielet programu stwórz na jego podstawie kompletny program, który będzie wczytywał polecenia użytkownika, i początkowo tylko potwierdzał przyjęcie ich do wykonania. Skompiluj i uruchom program. Kompilator ANSI C uruchamia się na systemach uniksowych poleceniem cc z nazwą pliku programu źródłowego jako argumentem. W przypadku poprawnej kompilacji wynikowy program binarny tworzony jest w pliku a.out.

main ()
{
    while (1) {
        printf("Podaj polecenie do wykonania [d,s,x,e,q]:\n");
        fgets(buf, sizeof(buf), stdin);
        sscanf(buf, "%s", polecenie);
        if (fork()==0) {
            printf("Tu potomek pid=%d\n", getpid());
            /* ... wykonanie polecenia potomka ... */
            switch (polecenie[0]) {
            case 'd': /* ... */;
            }
            exit(0); /* obowiazkowe zakonczenie potomka */
        }
        printf("Tu rodzic po utworzeniu potomka.\n");
        /* ... czekanie na potomka terminalowego ... */
        /* ... lub sprzatanie zombie okienkowego ... */
        switch (polecenie[0]) {
        case 'd': /* ... */;
        }
    } /* nieskonczona petla rodzica */
}

Należy zacząć od uruchomienia programu, który będzie tylko odczytywał polecenia użytkownika, a następnie uruchamiał procesy potomków, które będą tylko potwierdzały przyjęcie polecenia do wykonania.

Następnie należy uruchamiać po kolei funkcje programu, od najprostszych.

Początkowo najłatwiej będzie uruchamiać wymagane programy w procesie potomka za pomocą funkcji system. Tworzy ona interpreter poleceń, który wykona dane polecenie. Jednak w końcowej wersji programu funkcję system należy zastąpić wywołaniem jednej z funkcji exec która jedynie wywoła właściwy program w już utworzonym procesie potomka, bez interpretera poleceń.

W przypadku opcji 'e' wymagane jest kolejne wywołanie dwóch programów: najpierw programu zenity do spowodowania wyboru pliku przez użytkownika, a następnie programu xedit do uruchomienia edycji tego pliku. W przypadku realizacji tej operacji przez funkcję system można to zrobić jednym wywołaniem, z zagnieżdżonym poleceniem shella wywołującym zenity w celu uzyskania nazwy pliku. Jednak w przypadku docelowej realizacji przez exec nie jest to możliwe. Najprostszym i polecanym rozwiązaniem jest uruchomienie programu zenity za pomocą funkcji popen, a po jej zakończeniu wywołanie właściwego programu xedit za pomocą funkcji exec. Alternatywnym dla popen sposobem byłoby utworzenie przez proces potomka jeszcze jednego procesu wykonującego następnie zenity za pomocą exec, z wcześniejszym przekierowaniem jego strumienia stdout aby przeczytać wybraną przez ten program nazwę pliku.

Obsługa tworzonych podprocesów

Podprocesy tworzone funkcją fork powinny być poprawnie obsłużone przez program główny, co polega na odczytaniu statusu po ich zakończeniu. W przypadku poleceń 'd' i 's' jest to proste, ponieważ program główny powinien czekać na zakończenie uruchomionego podprocesu. Czekanie na zakończenie i odczytanie statusu podprocesu realizowane jest funkcją waitpid.

Jednak poprocesy tworzone funkcjami 'x' i 'e' są okienkowe i proces główny nie powinien czekać na ich zakończenie, zatem w chwili zakończenia będą stawały się procesami zombie. Ponieważ program główny nie czeka na te podprocesy, zatem nie może w prosty sposób wywołać wait (ani waitpid) i zakończyć zombie. Początkowo program może ignorować to zjawisko, jednak w końcowej wersji należy uzupełnić program o niezbędne wywołania funkcji waitpid odczytujące status zakończonych potomków. Przykładowe sposoby rozwiązania tego problemu przedstawione są w PDF-ie do wykładu o procesach, od strony 31.

Subtelności

Pewne elementy wykonania tego zadania mają niewielki wpływ na działanie programu, ale uważny programista powinien zwracać uwagę na takie subtelności jak:

Typowe błędy w realizacji zadania

Typową trudnością w zadaniu jest rozróżnienie kodu rodzica i potomka. Często skutkiem nieprawidłowego rozdzielenia ich funkcji jest: (a) niewspółbieżna praca rodzica i potomka okienkowego, (b) współbieżna praca rodzica i potomka terminalowego (co skutkuje konkurencją w dostępie do terminala, i zastopowaniem procesu rodzica przez system), a także czasami (c) kompletna porażka polegająca na wejściu procesu potomka do kodu rodzica (brak return lub exit w kodzie potomka), co powoduje konkurencję o dostęp do terminala i/lub nieograniczone rozmnażanie się potomków !!

Bardzo częsta jest nieprawidłowa obsługa zombie.

Praca pod Windowsem

W przypadku zdalnej pracy z Windowsa, dla uruchamiania uniksowych programów okienkowych konieczne będzie zainstalowanie i uruchomienie na lokalnym systemie programu zwanego serwerem X Window.

Krótka instrukcja: http://diablo.kcir.pwr.edu.pl/xwindow/

W razie napotkania problemów proszę zwracać się o pomoc do prowadzącego na zajęciach, konsultacjach, lub emailem, lub do techników/administratorów (szczegółowe informacje na stronach WWW diablo i panaminta).

Praca na zajęciach - I termin

Minimalny wynik: uzupełniony i uruchomiony program szkieletowy, odczyt poleceń użytkownika, tworzenie podprocesów, zakończenie programu 'q' (2 punkty).

Pożądany wynik: uruchomione co najmniej dwie z funkcji 'd', 's', 'x' przynajmniej w wersji przez funkcję system (+2 punkty).

Praca na zajęciach - II termin

Minimalny wynik: wszystkie cztery funkcje programu uruchomione z wykorzystaniem jednej z funkcji exec* (2 punkty, lub 1 punt w przypadku niepełnej realizacji, braku współbieżności, lub innych błędów).

Pożądany wynik: poprawna obsługa podprocesów przez program główny, poprawne odczytywanie statusu podprocesów (+1 punkt - na zajęciach).

Raport

Raport z wykonania zadania, wysłany jednorazowo po jego zakończeniu całego zadania powinien zawierać potwierdzenie i opis sposobu wykonania wszystkich elementów zadania (system/popen/exec) i/lub uwagi na temat ewentualnych problemów.

Dodatkowo, proszę podać w raporcie czy i w jaki sposób program rozwiązuje problem powstających procesów zombie.

W przypadku uruchomienia i przetestowania programu na obydwóch systemach (diablo/Solaris, panamint/Linux), proszę również zaznaczyć to w raporcie.

Uwaga: w ocenie zadania jeden punkt będzie przyznawany za brak ostrzeżeń kompilacji programu z opcją -Wall (gcc na panamincie) oraz -Xc (cc na diablo).