LEKCJA 20 - JEŚLI PROGRAM POWINIEN URUCHOMIĆ INNY PROGRAM... ________________________________________________________________ W trakcie tej lekcji dowiesz się, jak w C++ można programować * procesy potomne * pisać programy rezydujące w pamięci (TSR) ________________________________________________________________ O programach rezydentnych (TSR) i procesach potomnych. Warunek zewnętrznej zgodności z poprzednimi wersjami DOS wyraźnie hamuje ewolucję systemu MS DOS w kierunku "poważnych" systemów operacyjnych umożliwjających pracę wieloprogramową w trybie "multiuser", "multitasking" i "time sharing". Pewną namiastkę pracy wieloprocesowej dają nam już DOS 5/6 i Windows 3.1. Można już otwierać wiele okien programów jednocześnie, można np. drukować "w tle", można wreszcie pisać rezydujące stale w pamięci programy klasy TSR (ang. Terminated and Stay Resident) uaktywniające się "od czasu do czasu". O bloku PSP. System DOS przydziela programom blok - "nagłówek" wstępny nazywany PSP (ang. Program Segment Prefix). Blok ten zawiera informacje o stanie systemu DOS w momencie uruchamiania programu (nazywanego tu inaczej procesem). Znajdują się tam informacje o bieżącym stanie zmiennych otoczenia systemowego (ang. environment variables) i parametrach uruchomieniowych. Blok PSP zajmuje 256 bajtów na początku kodu programu w zakresie adresów: CS:0000 ... CS:0100 (hex) Właściwy kod programu zaczyna się zatem od adresu CS:0100. Interpreter rozkazów systemu DOS ładuje programy do pamięci posługując się funkcją systemową nr 75 (4B hex). Wszystko jest proste dopóki mamy do czynienia z programem "krótkim" typu *.COM. Jeśli jednakże program uruchamiany jest w wersji "długiej" - *.EXE, dowolna może być nie tylko długość pliku, ale także początkowa zawartość rejestrów CS, SS, SP i IP. W plikach typu *.EXE początek bloku PSP wskazują rejestry DS (DS:0000) i ES. W Borland C++ masz do dyspozycji specjalną funkcję getpsp() przy pomocy której możesz uzyskać dostęp do bloku PSP programu. Krótki przykład zastosowania tej funkcji poniżej: /* Przykład zastosowania funkcji getpsp(): */ # include # include main() { static char TAB[128]; char far *ptr; int dlugosc, i; printf("Blok PSP: %u \n", getpsp()); ptr = MK_FP(_psp, 0x80); dlugosc = *ptr; for (i = 0; i < dlugosc; i++) TAB[i] = ptr[i+1]; printf("Parametry uruchomieniowe: %s\n", TAB); } W normalnych warunkach po wykonaniu "swojej roboty" program zostaje usunięty z pamięci operacyjnej (czym zajmuje się funkcja systemowa nr 76 - 4C (hex)). Aby tak się nie stało, program może: * uruchomić swój proces (program) potomny; * wyjść "na chwilę" do systemu DOS - tj. uruchomić jako swój proces potomny interpreter COMMAND.COM; * przekazać sterowanie programowi COMMAND.COM pozostając w pamięci w postaci "uśpionej" oczekując na uaktywninie. Poniżej kilka prostych przykładów uruchamiania jednych procesów przez inne w Borland C++: /* Funkcja execv(): uruchomienie programu "potomnego"*/ # include # include # include void main(int argc, char *argv[]) { int i; printf("Parametry uruchomieniowe:"); for (i=0; i # include void main() { printf("Wyjscie do DOS i wykonanie jednego rozkazu:\n"); system("dir > c:\plik.dir"); } /* Funkcje grupy spawn...() : spawnl() */ # include # include # include void main() { int rezultat; rezultat = spawnl(P_WAIT, "program.exe", NULL); if (rezultat == -1) { perror(" Fiasko !"); exit(1); } } /* Funkcja spawnle() */ # include # include # include void main() { int rezultat; rezultat = spawnle(P_WAIT, "program.exe", NULL, NULL); if (rezultat == -1) { perror("Fiasko !"); exit(1); } } Zagadnienie uruchamiania programów potomnych (ang. child process) przez programy macieżyste (ang. parent process) jest rozpracowane w C++ dość dokładnie i zarazem obszernie. Istnieje wiele gotowych funkcji bibliotecznych, z usług których możesz tu skorzystać. Wszystko to nie jest jednak "prawdziwym" programem TSR. Przyjrzyjmy się zatem dokładniej dopuszcalnym przez system DOS sposobom zakończenia programu nie powodującym usunięcia programu z pamięci. Jeśli program rezydentny jest niewielki (kod < 64 K), możemy zakończyć program posługując się przerywaniem INT 39 (27 hex). Jeśli natomiast zamierzamy posługiwać się dłuższymi programami, mamy do dyspozycji funkcję systemową nr 49 (31 hex). Należy tu zwrócić uwagę, że zakończenie programu w taki sposób (z pozostawieniem w pamięci) nie spowoduje automatycznego zamknięcia plików, a jedynie opróżnienie buforów. Programy rezydentne dzieli się umownie na trzy kategorie: [BP] - background process - procesy działające "w tle"; [SV] - services - programy usługowe - np. PRINT; [PP] - pop up programs - uaktywniane przez określoną kombinację klawiszy; System DOS dysponuje tzw. przerywaniem multipleksowym (naprzemiennym) wykorzystywanym często przez programy rezydentne. Jest to przerywanie nr INT 47 (2F hex). MS DOS załatwia takie problemy funkcjami nr 37 (25 hex) - zapisanie wektora przerywania i 53 (35 hex) - odczytanie wektora przerywania. Z jakich funkcji C++ można skorzystać? W C++ masz do dyspozycji parę funkcji getvect() i setvect() (ang. GET/SET VECTor - pobierz/ustaw wektor przerywania). Poniżej krótkie przykłady zastosowań tych funkcji. /* Opcja: Options | Compiler | Code generation | Test Stack Overflow powinna zostać wyłączona [ ] (off) */ # include "stdio.h" # include "dos.h" # include "conio.h" /* INT 28 (1C hex) - Przerywanie zegarowe */ void interrupt ( *oldhandler)(void); int licznik = 0; void interrupt handler(void) { /* Inkrementacja globalnej zmiennej licznik */ licznik++; /* Wywolujemy stary "handler" zegara */ oldhandler(); } void main() { /* Zapamiętaj poprzedni wektor przerywania 28 */ oldhandler = getvect(28); /* Zainstaluj nową funkcje obslugi przerywania */ setvect(28, handler); /* Inkrementuj licznik */ for (; licznik < 10; ) printf("licznik: %d\n",licznik); //odtworz stara funkcje obslugi przerywania: interrupt handler setvect(28, oldhandler); } # include # include void interrupt nowa_funkcja(); // prototyp funkcji - handlera void interrupt (*oldfunc)(); /* interrupt function pointer */ int warunek = 1; main() { printf("\n [Shift]+[Print Screen] = Quit \n"); printf("Zapamietaj, i nacisnij cosik...."); while(!kbhit()); /* zapamietaj stary wektor */ oldfunc = getvect(5); /* INT 5 to przerywanie Sys Rq, albo Print Screen */ /* zainstaluj nowa funkcje obslugi: interrupt handler */ setvect(5, nowa_funkcja); while (warunek) printf("."); /* Odtworz stary wektor przerywania */ setvect(5, oldfunc); printf("\n Udalo sie... nacisnij cosik..."); while(!kbhit()); } /* Definicja nowego handlera */ void interrupt nowa_funkcja() { warunek = 0; /* jesli warunek == 0, petla zostanie przerwana*/ } Jeśli nasz program zamierza korzystać z przerywania multipleksowego INT 47 (2F hex), należy pamiętać, że przerywanie to wykorzystują także inne programy systemowe. Rozróżniać te programy można przy pomocy identyfikatorów (podaję dziesiętnie): 01 - PRINT.EXE 06 - ASSIGN.COM 16 - SHARE.EXE (10 hex) 26 - ANSI.SYS 67 - HIMEM.SYS 72 - DOSKEY.COM 75 - TASK SWITCHER 173 - KEYB.COM 174 - APPEND.EXE 176 - GRAFTABL.COM 183 - APPEND.EXE Identyfikator programu TSR jest przekazywany za pośrednictwem rejestru AH. System DOS jest na razie systemem w zasadzie jednozadaniowym i jednoużytkownikowym, w którym zasoby są przydzielane procesom kolejno (ang. serially reusable resources). Aby uchronić się przed potencjalnym konfliktem, powinniśmy upewnić się, czy DOS "nic nie robi". Często stosowaną "sztuczką techniczną" jest zastosowanie flag ErrorMode i InDos systemu oraz wykorzystanie mechanizmów przerywań nr 36 i 40 (24 i 28 hex). Przydatną informacją jest także identyfikator programu - PID. Na taką ewntualność Borland C++ dysponuje makrem getpid zdefiniowanym w pliku nagłówkowym : # define getpid() (_psp) Inną przydatną funkcją może okazać się keep() (ang. keep resident - pozostań rezydentny). Oto krótki przykład zastosowania tej funkcji - znów z wykorzystaniem przerywań zegarowych. # include # define INTR 0x1C /* przerywanie INT 28 */ # define ATTR 0x7900 /* ograniczenie wielkości sterty (heap length) i stosu (stack length): */ extern unsigned _heaplen = 1024; extern unsigned _stklen = 512; void interrupt ( *oldhandler)(void); void interrupt handler(void) { unsigned int (far *ekran)[80]; static int licznik; // Adres pamieci dla monitora barwnego: B800:0000. // Dla monitora monochromatycznego: B000:0000. ekran = MK_FP(0xB800,0); // piloksztaltna zmiana licznika w przedziale 0 ... 9 licznik++; licznik %= 10; ekran[0][79] = licznik + '0' + ATTR; // wywołaj stara funkcje obslugi - old interrupt handler: oldhandler(); } void main() { oldhandler = getvect(INTR); // zainstaluj nowa funkcje interrupt handler setvect(INTR, handler); /* _psp - to adres początku programu, SS:SP to adres stosu, czyli koniec programu. Biorac pod uwage przesuniecie SEGMENT/OFFSET o jedna tetrade: SS:SP = SS + SP/16; */ keep(0, (_SS + (_SP/16) - _psp)); } Kilka istotnych drobiazgów technicznych. W Borland C++ masz do dyspozycji predefiniowane struktury BYTEREGS (rejestry jednobajtowe - "połówki") i WORDREGS (rejestry dwubajtowe). Możesz po tych strukturach dziedziczyć i np. taką metodą wbudować je do swoich własnych klas. Nic nie stoi na przeszkodzie, by utworzyć np. klasę class REJESTRY : public WORDREGS { ... }; czy też własną strukturę: struct REJESTRY : WORDREGS { ... }; Definicje tych struktur w Borland C++ wyglądają następująco: struct BYTEREGS { unsigned int al, ah, bl, bh, cl, ch, dl, dh; }; struct WORDREGS { unsigned int ax, bx, cx, dx, si, di, cflag, flags; }; Rejestry segmentowe mają własną strukturę: struct SREGS { unsigned int es, cs, ss, ds; }; Pole WORDREGS::cflag odpowiada stanowi flagi przeniesienia (ang. Carry Flag) rejestru flags mikroprocesora, a pole WORDREGS::flags odpowiada stanowi całości rejestru (w wersji 16 - bitowej). Ponieważ rejestry mogą być widziane alternatywnie jako podzielone na miezależne połówki - lub jako całość, to właśnie "albo - albo" wyraża w C++ unia. W Borland C++ taka predefiniowana unia nazywa się REGS: union REGS { struct WORDREGS x; struct BYTEREGS h; }; Z tych predefiniowanych struktur danych korzystają m. in. funkcje int86() intdosx() i int86x() ("x" pochodzi od eXtended - rozszerzony). Oto krótkie przykłady zastosowania tych funkcji. # include # include # include # define INT_NR 0x10 // 10 hex == 16 (Nr przerywania) VIDEO void UstawKursor(int x, int y) { union REGS regs; regs.h.ah = 2; // ustaw kursor regs.h.dh = y; // Wspolrzedne kursora na ekranie regs.h.dl = x; regs.h.bh = 0; // Aktywna stronica ekranu --> video page 0 int86(INT_NR, ®s, ®s); } void main() { clrscr(); UstawKursor(30, 12); printf("Tekst - Test"); while(!kbhit()); } # include # include # include void main() { char nazwapliku[40]; union REGS inregs, outregs; struct SREGS segregs; printf("\nPodaj nazwe pliku: "); gets(nazwapliku); // gets() == GET String inregs.h.ah = 0x43; inregs.h.al = 0x21; inregs.x.dx = FP_OFF(nazwapliku); segregs.ds = FP_SEG(nazwapliku); int86x(0x21, &inregs, &outregs, &segregs); printf("\n Atrybuty pliku: %X\n", outregs.x.cx); } # include # include int SkasujPlik(char far*) // Prototyp void main() { int error; err = SkasujPlik("PLIK.DAT"); if (!error) printf("\nSkasowalem plik PLIK.DAT"); else printf("\nNie moge skasowac pliku PLIK.DAT"); } int SkasujPlik(char far *nazwapliku) { union REGS regs; struct SREGS sregs; int wynik; regs.h.ah = 0x41; // Funkcja kasowania pliku regs.x.dx = FP_OFF(nazwapliku); sregs.ds = FP_SEG(nazwapliku); wynik = intdosx(®s, ®s, &sregs); return(regs.x.cflag ? wynik : 0); // Jesli CF == 1, nastapilo fiasko operacji } I wreszcie na zakończenie szczegóły techniczne działania funkcji systemowej nr 49 (31 hex) odpowiedzialnej za obsługę programów rezydujących w pamięci (załadowanie procesu z pozostawieniem w pamięci). 1. Wywołanie funkcji: AL = kod powrotu (ang. return code); AH = 0031 (hex) - nr funkcji; DX = długość programu TSR w paragrafach - Size/16 [Bajtów]; 2. Działanie: * funkcja nie zamyka plików, lecz opróżnia bufory; * funkcja odtwarza wektory przerywań nr 34, 35, 36 (hex 21, 22, 23); * proces macieżysty może uzyskać kod powrotu przy pomocy funkcji nr 77 (4D hex). Wykorzystanie struktury SDA (ang. Swappable Data Area - obszar wymiennych danych) nie jest praktyką zalecaną. Tworząc programy rezydentne bądź bardzo ostrożny i pamiętaj o jednej z podstawowych zasad - NIE JESTEŚ (tzn Twój program nie jest) SAM. ________________________________________________________________