SOP322/ćw - temat H

Win32.

Win32 to nazwa API, czyli zbioru funkcji systemowych WinNT i pokrewnych systemów operacyjnych ...

Dokumentacja Win32 znajduje się w pliku "win32.hlp", dostarczanym z "Borland C++", "Delphi" itp. Jest to nie tylko opis funkcji, ale także obszerny idei!

Do kompilowania przykładów użyj "Borland C++ Builder-a"; należy utworzyć nowy projekt oraz wybrać aplikację konsolową :

File | New | ikona Console Wizard; Window type = console, Execution type = exe
(tak przynajmniej jest w C++Builder 4.0 !)

należy zapisać projekt w osobnym katalogu; podane niżej przykłady [np np01.cpp] najlepiej uruchamiać tak: zmienić nazwę funkcji main() w np01.cpp na main2() i uruchamiać ją z funkcji main() w Project1.cpp; oczywiście w Project1.cpp należy umieścić "extern void main2();".
 

Obiekty jądra.

W jądrze NT występują obiekty następuących typów: "thread", "process", "file", "file-mapping", "event", "pipe", "semaphore", "mailslot", ...

Nowe obiekty tworzy się przy pomocy funkcji z rodziny Create*(). Obiekty często posiadają nazwę; istniejące obiekty posiadające nazwę otwiera się przy pomocy funkcji Open*().

Po utworzeniu lub otwarciu obiektu otrzymujemy uchwyt do obiektu, który jest zasadniczo ważny tylko w jednym procesie. (Funkcja DuplicateHandle() pozwala "dać" uchwyt niespokrewnionemu procesowi.)

Do zamykania obiektów służy CloseHandle(). Dopóki licznik odwołań do obiektu nie spadnie do 0, obiekt nie zostanie  zniszczony.

Procesy potomne mogą dziedziczyć uchwyty do obiektów po procesie macierzystym (jeśli ten sobie tego życzy). Jest to uogólnienie idei znanej z Unix-a, w którym procesy potomne dziedziczyły otwarte deskryptory!

Obiekty mogą posiadać security descriptor, określający którzy użytkownicy mają dostęp (i jaki) do obiektu.
 

Procesy.

W Win32 każdy proces posiada 4GB wirtualną przestrzeń adresową, z której może używać pierwszych 2GB. Jeśli proces używa biblioteki DLL, to jej kod i dane zostają odwzorowane w wirtualną przestrzeń adresową procesu.

Oprócz przestrzeni adresowej proces posiada "zasoby": kod, dane, uchwyty obiektów, zmienne środowiska, bazowy priorytet, jeden lub więcej wątków, min/max zbiór roboczy. Gdy proces się kończy, to jego zasoby są niszczone.

Istnieją 2 typy aplikacji, w Win32:

  1. GUI = Graphical User Interface
  2. CUI = Console User Interface (przypominają aplikacje DOS-owe, ale nimi nie są!)
Do tworzenia/niszczenia procesów służą funkcje: CreateProcess(), ExitProcess(), TerminateProcess(). (NIE MA odpowiednika uniksowego "fork()" ???)

Przykład tworzenia procesu potomnego ("np01.cpp"):

#include <stdio.h>
#include <windows.h>

void main( VOID )
{
    STARTUPINFO si;
    PROCESS_INFORMATION pi;

    ZeroMemory( &si, sizeof(si) );
    si.cb = sizeof(si);

    // Start the child process.
    if( !CreateProcess( NULL, // No module name (use command line).
      "notepad", // Command line.
      NULL,      // Process handle not inheritable.
      NULL,      // Thread handle not inheritable.
      FALSE,     // Set handle inheritance to FALSE.
      0,         // No creation flags.
      NULL,      // Use parent's environment block.
      NULL,      // Use parent's starting directory.
      &si,       // Pointer to STARTUPINFO structure.
      &pi )      // Pointer to PROCESS_INFORMATION structure.
    )
    ExitProcess(1);

    // Wait until child process exits.
    WaitForSingleObject( pi.hProcess, INFINITE );
       /* 
          każdy obiekt moze byc w stanie "signaled" lub "unsignaled";
          funkcja "WaitForSingleObject()" czeka, aż obiekt
          przejdzie do stanu "signaled" ...
       */

    // Close process and thread handles.
    CloseHandle( pi.hProcess );
    CloseHandle( pi.hThread );

    ExitProcess(0);
}

Przejrzyj rozdział "win32.hlp" pod tytułem "Processes and Threads". Chodzi m.in. o to aby uzyskać ogólną orientację o układzie win32.hlp !.

Zadanie 100
Wypróbuj mechanizm "przekazywanie parametrów do procesu potomnego". W tym celu napisz dwa programy PM i PP; PM będzie uruchamiał proces potomny z programem PP; PP powinien wypisywać swoje parametry; po uruchomieniu PM powinniśmy zobaczyć napisy wyświetlane przez PM i PP.
Wskazówka: Jak się przekazuje parametry do procesu potomnego? Patrz 2 parametr!

if( !CreateProcess( 
  NULL, // No module name (use command line).
  "prog_PP 1 2 3 \"1 2 3\"", // Command line.
  NULL, // Process handle not inheritable.
  NULL, // Thread handle not inheritable.
  FALSE, // Set handle inheritance to FALSE.
  0, // No creation flags.
  NULL, // Use parent's environment block.
  NULL, // Use parent's starting directory.
  &si, // Pointer to STARTUPINFO structure.
  &pi ) // Pointer to PROCESS_INFORMATION structure.
) ExitProcess(1);

 

Proces potomny może odziedziczyć niektóre uchwyty po procesie macierzystym; aby to osiągnąć należy:

  1. podczas tworzenia/otwierania/duplikowania obiektu, gdy otrzymujemy uchwyt, należy zaznaczyć, że zezwalamy aby uchwyt był dziedziczony (w przypadku funkcji CreateFile() tę informację umieszcza się w polu bInheritHandle struktury SECURITY_ATTRIBUTES, przekazywanej przez parametr lpSecurityAttributes;
    Uwaga: CreateFile() wbrew nazwie służy też do otwierania plików, nie tylko do ich tworzenia ...).
  2. podczas wywoływania CreateProcess(), należy zażądać dziedziczenia uchwytów (którym na to pozwolono) przy pomocy parametru bInheritHandles.
  3. oprócz tego, że uchwyty są dziedziczone, należy jeszcze przekazać do procesu potomnego konkretne wartości uchwytów, używając dowolnego mechnizmu (np przez parametry lub środowisko).

Zadanie 101 (**) [dla chętnych !]
Napisz przykładowy program ilustrujący dziedziczenie uchwytu do pliku, przez proces potomny. Proces potomny powinien wykonywać operacje na pliku za pośrednictwem odziedziczonego uchwytu. Proces macierzysty powinien pokazywać zmiany w zawartości pliku, co jedną sekundę. Użyj funkcji: CreateFile(), ReadFile(), WriteFile(), Sleep().
 

 

Wątki.

Proces może stworzyć pewną liczbę wątków przy pomocy funkcji CreateThread().  Wątki kończymy wywołując ExitThread(), TerminateThread().

Przypomnijmy główne cechy wątków:

Przykład programu wielowątkowego ("np02.cpp"):

#include <stdio.h>
#include <windows.h>

DWORD WINAPI ThreadFunc( LPVOID lpParam )
{
   MessageBox( NULL, (char*)lpParam, "Thread created.", MB_OK );
   printf("watek %s sie konczy!!!\n", (char*)lpParam);
   ExitThread(0);
}
main()
{
   DWORD dwThreadId[10];
   char chThreadParam[10][40];
   HANDLE hThread[10];

   for(int i=0; i<10; i++) {
     sprintf(chThreadParam[i], "Watek nr %i", i);

     hThread[i] = CreateThread(
        NULL,              // no security attributes
        0,                 // use default stack size
        ThreadFunc,        // thread function
        chThreadParam[i],  // argument to thread function
        0,                 // use default creation flags
        &dwThreadId[i]);   // returns the thread identifier

     // Check the return value for success.
     if (hThread[i] == NULL) ExitProcess(1);
   }

   for(int i=0; i<10; i++)
   CloseHandle( hThread[i] );

   printf("22222222222222222\n");
   printf("dowolny znak + enter "); getchar();

   ExitProcess(0);
}
Ćwiczenie 102
Wypróbuj przykład "np02.cpp"; sprawdź co się dzieje gdy proces zakończy się prędzej niż wszystkie wątki (w przykładzie np02.cpp wątki wyświetlają okienko i kończą się po naciśnięciu na OK; proces - a właściwie wątek główny - kończy się po naciśnięciu enter na konsoli).

.........................................................

Czym się różni "uchwyt obiektu wątkowego", od "identyfikatora obiektu wątkowego"? Odp: identyfikator jest poprawny w całym systemie, natomiast uchwyt (zasadniczo) tylko w procesie, w którym uzyskano ten uchwyt.

Wątek może zostać tymczasowo "uśpiony" przy pomocy funkcji SuspendThread(), Sleep(). ResumeThread() wznawia działanie wątku. Wątek może być także uruchomiony, od razu w stanie "suspended": patrz flaga CREATE_SUSPENDED, parametru dwCreationFlags, w funkcji CreateThread().

Przykład procesu wielowątkowego ("np03.cpp"), w którym wątki wyświetlają napisy na wspólnej konsoli. Wątek macierzysty czeka na zakończenie wątków potomnych przy pomocy funkcji WaitForSingleObject():

#include <stdio.h>
#include <windows.h>

DWORD WINAPI ThreadFunc( LPVOID lpParam )
{
    for(long i=0; i<300; i++) {
      printf("[%s] %li\n", (char*)lpParam, i);
    }
    ExitThread(0);
}
main()
{
   DWORD dwThreadId[10];
   char chThreadParam[10][40];
   HANDLE hThread[10];

   for(int i=0; i<10; i++) {
     sprintf(chThreadParam[i], "Watek nr %i", i);
     hThread[i] = CreateThread(
        NULL,              // no security attributes
        0,                 // use default stack size
        ThreadFunc,        // thread function
        chThreadParam[i], // argument to thread function
        0,                      // use default creation flags
        &dwThreadId[i]);   // returns the thread identifier

     // Check the return value for success.
     if (hThread[i] == NULL) ExitProcess(1);
   }

   // czeka na zakonczenie wszystkich watkow
   for(int i=0; i<10; i++)
   WaitForSingleObject( hThread[i], INFINITE );

   for(int i=0; i<10; i++)
   CloseHandle( hThread[i] );

   ExitProcess(0);
}
Ćwiczenie 103
Wypróbuj powyższy przykład "np03.cpp".

 

Synchronizacja wątków.

W Win32 jest wiele narzędzi do synchronizacji wątków, jak np semafory czy muteksy.

Przeczytaj rozdział win32.hlp: "Synchronization Objects".

Każdy "obiekt synchronizacyjny" (i w ogóle każdy obiekt) może być w stanie :
  • signaled
  • unsignaled

Funkcja "WaitForSingleObject()" wstrzymuje działania tak długo, aż obiekt znajdzie się w stanie signaled.


Co oznacza stan signaled dla różnych obiektów:

Zadanie 104
Przykład "np05b.cpp" zawiera program ze zmiennymi konto1 i konto2 i z wątkami dokonującymi przelewów. Przelewy są chronione przy pomocy semafora. Dodaj do tego przykładu wątek kontrolny pokazujący wartość sumy konto1+konto2 co 1 sekundę. Zbadaj co się dzieje gdy wyłączymy ochronę sekcji krytycznej (przelewów).

Zadanie 105
Zmodyfikuj rozwiązanie poprzedniego zadania - zamiast semaforów użyj muteksów. Przeczytaj opis funkcji CreateMutex() w "win32.hlp".

Zadanie 106 (*)
Napisz program rozwiązujący problem producenta/konsumenta, ze skończonym buforem, z dowolną liczbą producentów i konsumentów, przy pomocy semaforów. Program powinien produkować wydruki po których można poznać że wszystko działa prawidłowo (producent powinien produkować kolejne liczby, 5 konsumentów powinno wypisywać na ekranie co konsumują).

Zadanie 107 (**) [dla chętnych]
Przeczytaj opis funkcji:

i spróbuj skonstruować monitor Bufor, rozwiązujący problem producenta/konsumenta, ze skończonym buforem.  Monitor powinien być klasą C++, powinien używać muteksów i obiektów event (?).  Obiekty event powinny (?) pełnić rolę zmiennych condition "prawdziwych" monitorów.
 

IPC w Win32.

Przejrzyj pobieżnie rozdział win32.hlp "Interprocess Communications".

Przeczytaj rozdział win32.hlp "Named Pipes".

Zadanie 108
Napisz program, w którym proces macierzysty tworzy proces potomny i komunikuje się z nim przy pomocy łączy, tworzonych przy pomocy CreateNamedPipe().  Proces potomny powinien zamieniać małe litery na duże. Do pisania i czytania użyj funkcji ReadFile() i WriteFile().  Zauważ, że prawa rządzące tymi funkcjami w odniesieniu do łączy są podobne do tego co było pod Unixem !.  Zwróć uwagę, że w Win32 nie można w prosty sposób przekazać wartości uchwytów (dlatego używamy Named Pipes a nie Anonymous)
 

Szeregowanie wątków.

Szeregowanie wątków (czyli wybór jednego, spośród wątków gotowych do działania, w celu przydzielenia mu procesora) jest wykonywane przy pomocy priorytetów. Każdy wątek posiada priorytet. Priorytety mogą się zmieniać w czasie.

Wątki można podzielić na grupy, zawierające wątki z tym samym priorytetem. Zawsze wybiera się grupę wątków (gotowych do wykonania) z najwyższym priorytetem. Taka grupa jest obsługiwana przy pomocy metody "Round Robin" (każdy wątek otrzymuje kwant czasu).

W WinNT priorytety przyjmują wartości od 0 do 31 (duża liczba oznacza duży priorytet). Priorytety są podzielone na 2 klasy:

  1. real-time priority class; wartości od 16 do 31; priorytety się nie zmieniają
  2. variable priority class; wartości od 0 do 15; priorytety się zmieniają; początkowy priorytet jest określony poprzez 2 liczby: "proces base priority" - przedział [0,15]; "thread base priority" - przedział [-2,2]; początkowy priorytet to suma tych 2 liczb.
Do odczytywania/ustawiania "thread base priority" służą funkcje GetThreadPriority(), SetThreadPriority(). Do odczytywania/ustawiania "process base priority" służą funkcje GetPriorityClass(), SetPriorityClass().

Przeczytaj rozdział win32.hlp "Scheduling Priorities".

Zadanie 109
Zademonstruj, że priorytety wątków mają wpływ na ich działanie przy pomocy programu tworzącego 2 wątki, które powinny mieć różne priorytety. Wątki te zwiekszają o 1 wartości zmiennych zmA i zmB. Co 1 sekumdę 3 wątek (kontrolny), powinien wyświetlać wartości tych zmiennych.
Wskazówka: priorytety ustawiamy następującymi funkcjami:

SetThreadPriority(hThread, THREAD_PRIORITY_NORMAL);
SetThreadPriority(hThread, THREAD_PRIORITY_ABOVE_NORMAL);