background image

 

 

Programowanie współbieżne 

i rozproszone

• Współbieżność - składające się na nią zjawiska, czynności 

lub działania odbywają się równocześnie. 

• Proces  -  program  (procedura)  w  trakcie  wykonywania,  który 

może być skończony lub nieskończony.

 
• Program  współbieżny  -  zbiór  programów  sekwencyjnych 

wykonywanych  równolegle,  gdzie  nie  jest  wymagane,  aby 
każdy  proces  był  wykonywany  przez  fizycznie  odrębny 
procesor. 

• Geneza – systemy operacyjne wielozadaniowe.

 

background image

 

 

       

Termin  programowanie  współbieżne  używa  się  do  określenia 

technik  i  notacji  programistycznych  służących  do  wyrażania 
równoległości oraz rozwiązywania zagadnień związanych z powstałymi 
przy tym problemami synchronizacji i komunikacji. 

      Programowanie współbieżne jest bardzo ważnym zagadnieniem, gdyż 

pozwala zajmować się równoległością bez wdawania się w szczegóły 
implementacyjne
.  Możliwość  takiego  abstrahowania  okazała  się  na 
tyle  użyteczna  przy  pisaniu  programów,  że  nowoczesne  języki 
programowania  wyposażono  już  w  mechanizmy  programowania 
współbieżnego.

            Podstawowym  problemem  w  programowaniu  współbieżnym  jest 

wyłączność procesów do wspólnych zasobów.

background image

 

 

Programowanie sekwencyjne a 

współbieżne

   programowanie rozwiązań sekwencyjnych 
        jest prostsze

   prostsza analiza poprawności programu
 
   duża  liczba  problemów  analizowanych  i  rozwiązywanych  za  pomocą 

programu    

       sekwencyjnego będzie działała wolniej ,

 

Programowanie sekwencyjne :

background image

 

 

Programowanie współbieżne:

 

 możliwość 

tworzenia 

systemów 

komputerowych 

(programów 

współbieżnych) w sposób jak najbardziej efektywny

 programowania 

współbieżne 

jest 

znacznie 

trudniejsze 

niż 

programowania sekwencyjne, 

 trudniejsze  jest  dowodzenie  poprawności  programów  współbieżnych 

(skomplikowana analiza poprawności programu), 

 współbieżność wymaga uwzględnienia trudnych do opisania zależności 

czasowych występujących pomiędzy poszczególnymi procesami,

 brakuje metod testowych umożliwiających wykrywanie błędów 

synchronizacji,

 konieczność  specyfikacji  (określenia)  instrukcji,  które  mogą  być 

wykonywane  jednocześnie  (problem  wzajemnego  wykluczania, 

sortowanie współbieżne tablic),

background image

 

 

Problemy w programowaniu 

współbieżnym

 problemy synchronizacji i komunikacji 

 problemy związane z przydziałem procesora (priorytetowanie 

procesów)

background image

 

 

Przykład:

 

Złożoność  obliczeniowa  sekwencyjnego  i  równoległego  algorytmu 
sortowania tablic.

Rozmiar 

tablicy

Sortowanie 

przez 

zamianę

Algorytm 

równoległy

Algorytm 

równoległy ze 

współbieżnym 

sortowaniem

N

n

2

/2

(n

2

/4)+n

(n

2

/8)+n

40

800

440

140

100

5000

2600

1350

1000

500 000

251 000

126 000

background image

 

 

    

Sortowanie sekwencyjne n – elementowej tablicy (przez zamianę):

dla wewnętrznej pętli sortowania – 
(n-1)+ (n-2)+...+1= n(n-1)/2= n

2

/2

Sortowanie równoległe 

     

podział tablicy na dwie tablice po n/2 elementów 

     

liczba operacji dla jednej tablicy (n/2)

2

/2 = n

2

/8

     

liczba operacji potrzebnych do posortowania 2 tablic n

2

/4

operacji połączenia dwóch tablic 

background image

 

 

    

Program sekwencyjny jest poprawny, jeśli zatrzymuje się 

oraz drukuje poprawną wartość (wartości). To twierdzenie jest 
też  słuszne  dla  pewnego  rodzaju  programów  współbieżnych 
takich jak sortowanie. 

   
          Cechą  wielu  programów  współbieżnych  (systemów 

operacyjnych, systemów czasu rzeczywistego) jest to, że nigdy 
się  nie  zatrzymują).  Wówczas  w  abstrakcji  współbieżności 
wyróżnimy dwa typy własności dotyczących poprawności:

 

Poprawności programów 

współbieżnych

background image

 

 

     

Własność  bezpieczeństwa  –  program  współbieżny  jest  bezpieczny, 

jeśli nigdy nie doprowadza do niepożądanego stanu.

   Np.:

  problem wzajemnego wykluczania, 

  problem producentów i konsumentów -

      

      każda porcja zostanie skonsumowana w

                       kolejności ich produkowania.

 

   

Własność Żywotności – program współbieżny jest żywotny, jeśli 

zapewnia, że każde pożądane zdarzenie w końcu zajdzie.

background image

 

 

Przejawy braku żywotności

 

   

Brak żywotności globalnej - blokada (zastój, zakleszczenie, martwy 

punkt)  –  występuje  wtedy,  gdy  każdy  proces  z  danego  zbioru  procesów 
jest  wstrzymany  w  oczekiwaniu  na  zdarzenie,  które  może  być 
spowodowane  tylko  przez  jakiś  inny  proces  z  tego  zbioru.  Zjawisko 
blokady  może  być  również  traktowane  jako  przejaw  braku 
bezpieczeństwa
 programu, jest bowiem stan niepożądany. 

     
            Brak  żywotności  lokalnej  -  zagłodzenie
  –  występuje  wtedy,  gdy 

proces  nie  zostaje  wznowiony,  mimo  że  zdarzenie,  na  które  czeka, 
występuje  dowolną  liczbę  razy  i  za  każdym  razem,  gdy  proces  ten 
mógłby  być  wznowiony,  jest  wybierany  jakiś  inny  czekający  proces. 
Pomiędzy  całkowitym  brakiem  pojęcia  czasu  w  koncepcji  żywotności,  a 
wprowadzeniem czasu dokładnego znajduje się pojęcie uczciwości.

background image

 

 

Cztery rodzaje 

uczciwości :

 uczciwość słaba oznacza, że w przypadku nieprzerwanego zgłaszania 

żądania dostępu do zasobu przez proces, zostanie ono kiedyś obsłużone,

 uczciwość mocna oznacza, że gdy proces zgłasza żądanie dostępu 

nieskończenie wiele razy, zostanie ono kiedyś obsłużone,

 oczekiwanie liniowe oznacza, że jeśli proces zgłosi żądanie, zostanie 

ono obsłużone zanim dowolny inny proces zostanie obsłużony więcej niż 
jeden raz,

 kolejka FIFO (ang. First In, First Out) oznacza, że jeśli proces zgłosi 

żądanie, zostanie ono obsłużone przed dowolnym innym żądanie 
zgłoszonym później. 

            Własność  uczciwości  -  program  współbieżny  jest  uczciwy 
(sprawiedliwy), jeśli podczas przydzielania zasobu dzielonego, w taki sam 
sposób  traktuje  wszystkie  procesy.  Własność  ta  jest  szczególnym 
przykładem własności żywotności.

background image

 

 

Poprawności programów 

współbieżnych

   

PrzeplotAby wykazać, że program współbieżny nie jest poprawny, 

wystarczy  wskazać  ciąg  akcji  poszczególnych  procesów,  które 
doprowadzają do stanu niepożądanego.

                              

 System scentralizowany i rozproszony

            Programowanie  współbieżne  może  dotyczyć  systemów  opartych  na 

wspólnej  pamięci  (systemy  scentralizowane),  lub  architektury 
wieloprocesorowej 

połączonej 

siecią 

komputerową 

(systemy 

rozproszone), gdzie każdy procesor dysponuje swoją pamięcią.

background image

 

 

System scentralizowany

KANAŁ KOMUNIKACYJNY

KANAŁ KOMUNIKACYJNY

PROCESOR

PROCESOR

PROCESOR

PROCESOR

WSPÓLNA PAMIĘĆ

WSPÓLNA PAMIĘĆ

background image

 

 

System rozproszony

SIEĆ LOKALNA

SIEĆ LOKALNA

PROCESOR

PROCESOR

PROCESOR

PROCESOR

PAMIĘĆ

PAMIĘĆ

PAMIĘĆ

PAMIĘĆ

background image

 

 

Problem wzajemnego 

wykluczania

 

            Zasób  dzielony  -  wspólny  obiekt,  z  którego  może  korzystać  w 

sposób wyłączny wiele procesów. 

      Zasób własny obiekt, z którego korzysta (do którego ma 

dostęp w dowolnym czasie) tylko jeden proces.

 
      Sekcja krytyczna - fragment procesu (ciąg instrukcji), w którym 

proces korzysta z zasobu dzielonego.

 
      Sekcja lokalna - fragment procesu (ciąg instrukcji), w którym 

proces korzysta z zasobu własnego.

 

background image

 

 

    

Problem wzajemnego wykluczania polega na zsynchronizowaniu 

N  procesów,  z  których  każdy  w  nieskończonej  pętli  na 
przemian  zajmuje  się
  własnymi  sprawami  (  sekcja  lokalna  )  
wykonuje
  sekcję  krytyczną  ,  w  taki  sposób,  aby  wykonywanie 
sekcji  krytycznych  jakichkolwiek
  dwóch  lub  więcej  procesów 
nie pokrywało się w czasie.
 

      Rozwiązanie problemu sprowadza się do specyfikacji zbioru reguł 

(warunków) spełnienie, których jest konieczne do tego, aby dany 
proces mógł wykorzystać zasób dzielony (unikając zagłodzenia). 
Oznacza to, że należy dokonać specyfikacji i implementacji 
następujących protokołów: protokołu wstępnego i protokołu 
końcowego
.

background image

 

 

Process P1( )     Process P2( ) ......................... Process Pn()
{ while (TRUE)       { while (TRUE)                     {     while (TRUE )
  {       sekcja_lokalna;         {   sekcja_lokalna;                {    sekcja_lokalna;
                protokół_wstępny; 

                  protokół_wstępny;                     

protokół_wstępny;

         sekcja_krytyczna;                                    sekcja_krytyczna;
          sekcja_krytyczna;                            

  protokół_końcowy;                                     protokół_końcowy;
  protokół_końcowy;

   }                                              }                                          }

}       }                                           {

 

background image

 

 

Schemat procesu

Sekcja

 

lokalna

Protokół

 

wstępny

Sekcja

 

krytyczna

Protokół

 

końcowy

background image

 

 

Problem wzajemnego 

wykluczania

      

Dany jest zbiór N współbieżnych procesów cyklicznych, N = {P

1

P

2

, ... , P

n

}. Każdy z procesów w nieskończonej pętli, na przemian, 

wykonuje pracę: na zasobie własnym i zasobie dzielonym lub tylko 
na zasobach dzielonych. Ograniczenie w pracy systemu polega na 
tym,  że  w  tym  samym  czasie  tylko  zbiór  m  procesów  (m    ||M||, 

gdzie M jest podzbiorem zbioru N; M  N oraz moc ||M|| jest równa 

pojemności  zasobu  dzielonego)  może  wykorzystywać  zasób 
dzielony.

 

background image

 

 

Warunki poprawnego rozwiązania 

problemu wzajemnego wykluczania:

 

 procesy muszą być traktowane jako równoważne, nie mogą mieć 

przypisanych im statycznych priorytetów (sprawiedliwe decyzje)

 żaden proces nie może wykonywać swej sekcji krytycznej 

nieskończenie długo, nie może ulec awarii podczas wykonywania sekcji 
krytycznej). Taki sam warunek dotyczy protokołów wstępnego i 
końcowego

 jeżeli  więcej  niż  jeden  proces  chce  wejść  do  sekcji  krytycznej,  to 

decyzja  o  tym,  który  proces  zostanie  wybrany  musi  być  podjęta  w 
skończonym  czasie.  Natomiast,  jeżeli  żaden  z  procesów  nie  wykonuje 
sekcji  krytycznej,  to  przy  zgłoszeniu  się  dowolnego  procesu  musi  być 
mu umożliwione wejście do sekcji krytycznej 

background image

 

 

 zachowanie  się  procesów  poza  sekcją  krytyczną  nie  powinno  być  w 

żaden  sposób  ograniczone (luźne  powiązanie procesów).  Zatrzymanie 
się  jakiegoś  procesu  w  poza  sekcją  krytyczną  nie  może  prowadzić  do 
blokowania innych procesów

 

 Procesy mogą się wykonywać z różnymi prędkościami. W rozwiązaniu 

nie  wolno  czynić  żadnych  założeń  dotyczących  względnej  szybkości 
procesów.

 Żywotność globalna i lokalna programu - brak blokady i zagłodzenia.

 Bezpieczeństwo programu.

background image

 

 

      

Modele komunikacji między procesami:

model pamięci współdzielonej - systemy scentralizowanych
model przesyłania komunikatów - systemy rozproszone 

            W  modelu  pamięci  współdzielonej  rolę  łącza  komunikacyjnego  pomiędzy 

procesami spełnia pamięć dzielona:

              wymiana  informacji  poprzez  zapisywanie  i  odczytywanie  wartości 

zmiennej

         dzielonej
      udogodnienie sprzętowe - arbiter pamięci, zapewnia wyłączność odczytu
         lub zapisu informacji z danej komórki pamięci

       W modelu przesyłania komunikatów rolę łącza spełnia kanał komunikacyjny:

       wymiana informacji jest realizowana poprzez wysyłanie komunikatów –
          nadaj( komunikat ) i odbierz( komunikat ). 

     

background image

 

 

       

Problem  wzajemnego  wykluczanie  w  środowisku 

scentralizowanym  poniższy  algorytm  nadaje  się  do 
wykorzystania  na  każdym  komputerze  niezależnie  od 
zainstalowanego  na  nim  systemu  operacyjnego.  Używa  on 
wyłącznie  instrukcji  języka  maszynowego  udostępnionych 
przez  komputer  bez  korzystania  z  mechanizmów  wysokiego 
poziomu, takich jak semafory czy monitory.

background image

 

 

Program 1 
 
int czyja_kolej=1;
 
proces_P1 ( )
{
while (1)

{
   while (czyja_kolej =2);         // aktywne oczekiwanie                     

 sekcja_krytyczna_P1;
                
czyja_kolej =1;                // protokół końcowy        
sekcja_lokalna_P1;
           }
}
//////////////////////////////
proces_P2 ( )
{
while (1)

{
   while (czyja_kolej =1);         // aktywne oczekiwanie                     

 sekcja_krytyczna_P2;
                
czyja_kolej =2;                // protokół końcowy        
sekcja_lokalna_P2;
           }

background image

 

 

Własności programu

 program jest bezpieczny, 

 brak zagłodzenia, 

 procesy nie są luźno powiązane, pozwolenie jest przekazywane 

wprost od procesu do procesu, stąd: 

 awaria procesu w sekcji lokalnej -> blokada systemu 

 procesy wykorzystują zasób naprzemiennie

 

background image

 

 

Program 2 
 
int k1=1, k2 =1;
 
proces_P1 ( )
{
while (1)

{
   while (k2 =0);         // aktywne oczekiwanie                     

                  k1=0;
 sekcja_krytyczna_P1;
                
  k1=1;                // protokół końcowy        
sekcja_lokalna_P1;
           }
}
//////////////////////////////
proces_P2 ( )
{
while (1)

{
   while (k1 =0);         // aktywne oczekiwanie                     

                  k2=0;
 sekcja_krytyczna_P2;
                
  k2=1;                // protokół końcowy        
sekcja_lokalna_P2;
           }
}

background image

 

 

Własności programu

 program nie jest bezpieczny, 
 brak zagłodzenia, 

 procesy są luźno powiązane, pozwolenie jest przekazywane 

bezpośrednio od procesu do procesu, stąd:

 
 awaria procesu w sekcji lokalnej umożliwia realizacje  drugiemu 

procesowi 

 procesy nie wykorzystują zasobu naprzemiennie

 

background image

 

 

Program 3 
 
int k1=1, k2 =1;
 
proces_P1 ( )
{
while (1)

             k1=0;

   while (k2 =0);         // aktywne oczekiwanie                     

                  sekcja_krytyczna_P1;
                
  k1=1;                // protokół końcowy        
sekcja_lokalna_P1;
           }
}
//////////////////////////////
proces_P2 ( )
{
while (1)

{

            k2=0;
  

   while (k1 =0);         // aktywne oczekiwanie                     

                 sekcja_krytyczna_P2;
                
  k2=1;                // protokół końcowy        
sekcja_lokalna_P2;
           }

background image

 

 

Własności programu

   

  

program jest bezpieczny, 

  

brak zagłodzenia,
możliwość wystąpienia blokady

background image

 

 

Program 4 
 
int k1=1, k2 =1;
 
proces_P1 ( )
{
while (1)

             k1=0;

   while (k2 =0) 

                  {  
                      k1=1;
                          delay(n);  //czasowa rezygnacja z wejścia do sekcji 
                                         //aby P2 mógł wejść 
                      k1=0 
                  }
                  sekcja_krytyczna_P1;
                
  k1=1;                // protokół końcowy        
sekcja_lokalna_P1;
           }
}

background image

 

 

//////////////////////////////
proces_P2 ( )
{
while (1)

             k2=0;

   while (k1 =0) 

                  {  
                      k2=1;
                          delay(n);  //czasowa rezygnacja z wejścia do sekcji 
                                         //aby P1 mógł wejść 
                      k2=0 
                  }
                  sekcja_krytyczna_P2;
                
  k2=1;                // protokół końcowy        
sekcja_lokalna_P2;
           }

background image

 

 

Własności programu

 program jest bezpieczny,
 
 możliwość wystąpienia ciągłego zagłodzenia dwóch procesów, co 

prowadzi do blokady

            Rozwiązanie  poprawne  -  algorytm  Dekkera  -  rozwiązuje  problem 

wzajemnego  wykluczania  dla  dwóch  procesów  konkurujących  ze  sobą 
o dostęp do zasobu dzielonego przy użyciu trzech zmiennych.

background image

 

 

Własności algorytmu 

Dekkera

 

 własność bezpieczeństwa: wzajemnego wykluczania, 
 nie występuje w nim blokada, 
 żaden proces w nim nie zostanie zagłodzony 
 przy braku współzawodnictwa proces może natychmiast wejść do 

swojej sekcji krytycznej

 posiada możliwość zastosowania na każdym komputerze 

      WADA: wymaga on aktywnego oczekiwania, czyli pracy procesu w 

pętli w oczekiwaniu na zmianę lokacji w pamięci, co jest bardzo 
niepożądane ze względu na marnotrawstwo czasu procesora. 

background image

 

 

          W  algorytmie  tym  prawo  do  nalegania  na  wejście  do  sekcji  krytycznej 

jest jawnie przekazywane między procesami za pomocą zmiennych K1 i 
K2.  Zmienne  te  zapewniają  już  wzajemne  wykluczanie,  ale  po  wykryciu 
współzawodnictwa  proces  np.  P1  sprawdza  w  dodatkowej  zmiennej 
globalnej  czyja_kolej  ,  czy  teraz  jest  jego  kolej  na  wejście  do  sekcji 
krytycznej.  Jeśli  nie  to  przywraca  początkową  wartość  zmiennej  K1 
ustępując  w  ten  sposób  procesowi  P2  i  przechodzi  w  pętlę  oczekiwania 
na  swoją  kolej.  Gdy  P2  kończy  swoją  sekcję  krytyczną  zmienia  zmienną 
czyja_kolej  na  1,  dopuszczając  proces  P1  do  jego  sekcji  krytycznej. 
Nawet,  gdy  proces  P2  natychmiast  zgłosi  swoje  żądanie  na  ponowne 
wejście  do  sekcji  krytycznej,  zostanie  on  powstrzymany  przez  zmienną 
czyja_kolej,  gdy  tylko  P1  ponownie  zgłosi  swoje  żądanie.  Jeżeli  jednak 
proces  P2  pierwszy  opuści  swoją  sekcję  lokalną  przed  procesem  P1 
(proces  P1  jeszcze  nie  nalega  na  wejście  do  sekcji  krytycznej,  tzn.  jest 
jeszcze  w  swojej  sekcji  lokalnej  -  nie  wykonał  jeszcze  instrukcji  K1=0;)   
może  on  wejść  do  swojej  sekcji  krytycznej,  pomimo  że  nie  była  to  jego 
kolej.

background image

 

 

Wysokopoziomowe mechanizmy 

synchronizacji

Semafory 

Semafor - abstrakcyjny typ danych zdefiniowany przez Dijkstrę. 
Semafor  s  jest  zmienną  przyjmującą  (w  zależności  od  rodzaju)  wartości 

całkowite

 nieujemne lub wartości logiczne.
nadanie wartości początkowej zmiennej całkowitej (semaforowej), 
podnoszenie semafora 
opuszczanie semafora 

Wszystkie operacje semafora są atomowe, co oznacza, iż nie mogą być
 wykonywane w tym samym czasie przez więcej, niż jeden proces. 

background image

 

 

procesu

wykonanie

zawie

ś

s

s

zmniejsz

s

s

Czekaj

 

 

0

 

0

)

(

Oznaczenie operacji: 
              - Nadanie wartości początkowej(s) :   sem s=1 //{0,1}
              - Czekaj(s)                                      :   wait (s),   P(s)
              - Sygnalizuj(s)                                 :   signal(s), V(s)
 
Operacja  Czekaj(s)  (zwana  również  z  ang.  Wait(s))  określona  jest 

następująco:

Znaczenie  operacji  “zmniejsz  s”  określone  jest  w  zależności  od  rodzaju 

semafora. 

background image

 

 

Operacja Sygnalizuj(s):  (Signal(s)): 

 jeżeli istnieją procesy zawieszone przez semafor (na skutek wykonania 

operacji Czekaj(s)), to wznów jeden z nich 

 w przeciwnym przypadku zwiększ wartość s. 

Definicja nie określa, który proces zostanie wznowiony. Znaczenie operacji 

“zwiększ

 s” określone jest w zależności od rodzaju semafora. 

background image

 

 

Ogólny schemat działania semafora

Istnieją procesy zawieszone

 przez semafor

Wznów jeden z 

zawieszonych 

procesów

Wygeneruj błąd

Zwiększ S

S po zwiększeniu przekraczałoby

maksymalną dopuszczalną

wartość

background image

 

 

Semafor binarny

Przyjmuje tylko wartości logiczne: prawda oraz fałsz.

procesu

wykonanie

zawieś

fałsz

s

fałsz

s

prawda

s

s

Czekaj )

(

Sygnalizuj(s):
   jeżeli  istnieją  procesy  zawieszone  przez  semafor,  to  wznów  jeden  z 

nich, w przeciwnym przypadku nadaj s wartość prawda

background image

 

 

Semafor ogólny
Wartość  początkowa  jest  liczbą  nieujemną.  Operacje  elementarne 

przyjmują postać:

Sygnalizuj(S): 
 jeżeli istnieją procesy zawieszone przez semafor, to wznów jeden z 

nich, w przeciwnym przypadku zwiększ wartość s o jeden (s = s + 1)

Sposoby realizacji wznawiania procesów zawieszonych na semaforze. 

procesu

wykonanie

zawieś

s

s

s

s

s

Czekaj

0

1

0

)

(

background image

 

 

       

Semafory ze zbiorem oczekujących

            Procesy  zawieszone  umieszczane  są  w  zbiorze  zawieszonych 

procesów.  Wznowienie  procesu  nie  określa,  który  z  nich  ma 
kontynuować  działanie.  Semafor  tego  typu  łatwo  może  więc 
doprowadzić  do  zagłodzenia  przy  obecności  co  najmniej  trzech 
procesów.  Zaletą  tego  semafora  jest  prostota  realizacji  i  nieznacznie 
większa szybkość w porównaniu z semaforem z kolejką oczekujących.

      Semafory z kolejką oczekujących
            Procesy  zawieszone  umieszczane  są  w  kolejce  FIFO.  O  kolejności 

wznawiania  decyduje  pozycja  w  tej  kolejce  –  najpóźniej  zawieszony 
proces  zostaje  wznowiony.  Zaletą  tego  semafora  jest  brak  możliwości 
zagłodzenia jakiegokolwiek procesu.

      Zastosowanie semafora
      Semafor znajduje zastosowanie do wzajemnego wykluczania procesów 

przy próbie wejścia do sekcji krytycznej, umożliwiając jej wykonanie w 
danym czasie tylko określonej (przez wartość początkową) liczbie 
procesów. Jest on również wykorzystywany w celu wymiany informacji 
między procesami.

background image

 

 

Zasadę działania operacji semaforowych można przedstawić na rysunku:

Proces A

Proces B

P(S)

V(S)

S:=S-1

S:=S-1

S=0

Ktoś czeka

Zasada działania operacji semaforowych

background image

 

 

Strukturalne mechanizmy synchronizacji

Rejony krytyczne

Rejony  krytyczne  (ang.  critical  regions)  zaproponowali  niezależnie 
Hoare i Brinch Hansen. Stanowią one czyste przemodelowanie operacji 
semaforowych Dijkstry P i V w kierunku strukturalizacji.
Punktem  wyjścia  do  ustalenia  odpowiedniej  notacji  językowej  było 
stworzenie  instrukcji,  którą  obejmowałaby  sekcję  krytyczną, 
zapewniając  dla  niej  wykluczenie  wzajemne.  Sekcja  krytyczna 
najczęściej służy do wykonania operacji na pewnym zasobie, a mówiąc 
językiem  „programowania"  —  na  pewnej  zmiennej  określonego  typu. 
Co więcej zazwyczaj jest niedopuszczalne operowanie na tym obiekcie 
poza sekcją krytyczną. 

background image

 

 

Struktura rejonu krytycznego

Obiekt  typu  T,  na  którym  są  wykonywane  operacje  wewnątrz  sekcji 

krytycznej,

nazywamy zmienną dzieloną (ang. shared variable). Jej deklaracja w postaci

var v: shared T;

oznacza  zadeklarowanie  zmiennej  v  typu  T,  która  może  być  używana 

wyłącznie w 

określonych sekcjach krytycznych. Instrukcja strukturalna, tworząca sekcję 
krytyczną dla instrukcji I

1

,...,I

N

 związuje ją jednocześnie ze zmienną dzieloną v

region v do I

1

;...; I

N

 end

Instrukcję tę nazywamy instrukcją rejonu krytycznego (lub krócej rejonem 
krytycznym).

background image

 

 

 

Założenia dotyczące rejonu krytycznego są określone 

trzema 

warunkami:

(1)  Wykonywanie  instrukcji  wewnątrz  rejonów  związanych  z  tą  samą 

zmienną dzieloną w tym samym czasie przez dwa lub więcej procesów 
jest  wykluczające  się  (inaczej  mówiąc  wewnątrz  związanych  ze  sobą 
rejonów krytycznych może pracować co najwyżej jeden proces).

(2) Proces może przebywać wewnątrz rejonu krytycznego w skończonym 

czasie,  czyli  instrukcje  operujące  na  zmiennej  dzielonej  muszą  mieć 
możliwość kończenia się.

(3)  Wejście  do  rejonu  krytycznego  musi  być  umożliwione  dowolnemu 

procesowi w skończonym czasie.

Warto  zauważyć,  że  powyższa  notacja  pozwala  kompilatorowi  języka,  w 

którym  zostałaby  zastosowana,  na  sprawdzenie,  czy  odwołania  do 
zmiennych dzielonych są dokonywane wyłącznie z instrukcji zawartych 
wewnątrz odpowiednich rejonów krytycznych.

 

background image

 

 

Rejony  krytyczne  mogą  być  wewnątrz  siebie  zagłębiane  na  podobnych 

zasadach 

jak instrukcje cyklu w większości języków programowania.

var z1, z2; shared resource;
procedure p;
begin

region z1 do

              

  . . . 

                          region z2 do
                        . . . 
                          end; (*z2*) 
                 end (*z1*)

end; (*p*) 

background image

 

 

Zastosowanie rejonów krytycznych

Przeznaczeniem  rejonów  krytycznych  jest  zapewnienie  wykluczenia 

wzajemnego 

procesów, które korzystają z tego samego zasobu.

PRZYKŁAD  

Synchronizacja dostępu do zasobów 

 Należy zsynchronizować dostęp N procesów współbieżnych do zasobu R.

var R: shared resource;
procedure pp;
begin 

       

cycle

    przetwarzanie-A;

                 region R do
                           request
 R;
                           hold(t);
                          

release                 

                          

end;

                przetwarzanie-B
          

    end

 

end;
process 
1..N) pp  end.

 

background image

 

 

PRZYKŁAD  Producent—konsument 
Dla    uproszczenia  przyjmiemy,  że  operowanie  na  buforze  dla  wszystkich 

procesów

producenta i konsumenta powinno odbywać się wewnątrz sekcji krytycznej.

 
  

const N = MAX

 

var R  shared record
buf: array [1..N] of buffer;

            

lp, lkinteger :=1,1 

      end;

pełnypusty: semaphore :=0, N;
procedure producent;

       begin 
               cycle
          

(* przygotowywanie wiadomości*) 

 

    

hold(t1);

          

(*wysłanie wiadomości*) 

          

P (pusty); 

background image

 

 

region R do

        

    fill buf [lp];

        

    hold(t2); 

        

    release buf [Ip]:                             

           

    lp := lp mod N+

 

end;

             V (pełny)
      end 
   end;
 (*producent*)
 procedure konsument;
 

begin 

             cycle

(* odebranie wiadomości*) 
P (pełny);
region R do

 

      

    quit buf [lk];

      

    hold(t3);

      

    release buf  [lk];

      

    lk := lk mod N+

end;
    V (pusty);

              (*przetwarzanie *) 
 

      hold(t4) 

    end 
  end;
 (*konsument*)
  process (1 .. P) producent;
                 (1 .. K) konsument 
end. 

background image

 

 

Warunkowe rejony krytyczne

Niekompletność  rejonów  krytycznych,  polegająca  na  braku 
prostej  komunikacji  między  procesami,  spowodowała  rozwój 
prac  nad  ich  uzupełnieniem  o  odpowiednie  mechanizmy 
komunikacyjne.  Pierwsze  postulaty  rozszerzeń  zaproponował 
Hoare  ,  natomiast  szerokiego  ich  rozwinięcia  i  wzbogacenia  o 
nowe elementy dokonał Brinch Hansen. Powstałe w ten sposób 
konstrukcje,  jakkolwiek  mające  często  różny  zapis,  otrzymały 
nazwę  warunkowych  rejonów    krytycznych  (ang.  conditional 
critical regions).

background image

 

 

Struktura warunkowych rejonów 

krytycznych

 

Ogólna  postać  warunkowego  rejonu  krytycznego  dopuszcza 

wystąpienie między instrukcjami umieszczonymi wewnątrz tego rejonu 
(dowolną liczbę razy i w dowolnych miejscach) instrukcji synchronizacji 
await

 

var R:shared T;

           . . .
          

region R do

         

   I

1

I

2

;...; await W

1

;

         

   . . .

       

   I

i

, I

i+1

;...; await W

j

;

        

   . . .

                   I

N-1

I

N

                end      

background image

 

 

Działanie instrukcji await, gdzie W

j

 jest wyrażeniem logicznym 

(najczęściej  zależnym  od  zmiennej  R),  polega  na  tym,  że  w 
czasie jej 
wykonywania  jest  sprawdzany  warunek  W

j

.  Jeżeli  jest  on 

spełniony, to 
proces  przechodzi  do  wykonania  następnej  instrukcji, 
natomiast w 
przypadku przeciwnym zostaje zawieszony do czasu spełnienia 
warunku  z  jednoczesnym  zwolnieniem  dostępu  do  sekcji 
krytycznej.

 

background image

 

 

Przyjmując  zasadę,  że  rejony  krytyczne  powinny  być  krótkie, 
służyć  sprawdzaniu  i  zmienianiu  warunków,  natomiast  inne 
operacje (Jak np. wpisywanie do buforu) powinny odbywać się 
poza  nimi  (oczywiście  jednak  z  zapewnieniem  wszelkiej 
ochrony).  Stosując  się  do  tych  postulatów  przed  operacjami 
związanymi z buforem, należy za pomocą warunkowego rejonu 
krytycznego  sprawdzić,  czy  proces  może  wykonać  dane 
operacje  i  czy  nie  ma  żadnego  innego  procesu  już  je 
wykonującego.  Po  zakończeniu  działania  na  buforze  Procesy 
dokonują  wewnątrz  rejonu  krytycznego  zmiany  odpowiednich 
warunków.

background image

 

 

const N = MAX;
var R: shared record
np: integer := 0; (*liczba producentów*) 
nk: integer := 0; (*liczba konsumentów*) 
m:  integer := 0; (*liczba porcji nieodebranych*)
 end;
buf: array [1..N] of  buffer;
lp, lk: integer := 1,1;
procedure producent;                              
begin 
     cycle

( * przygotowanie wiadomości *)
hold(t1);

  

(*wysłanie wiadomości*) 

 

region R do

                   await np = 0 and m < N;
                   np := np +
               end;

background image

 

 

    

fill buf [lp];

                     hold(t2);
                     release buf[lp];
                  lp := lp mod N+1;
                  region R do
                           
np := np -l;
                           m := m+1 
                  end
            end 
       end;
 (*producent*)
      procedure konsument;
      begin 
          cycle
              (* odebranie wiadomości*) 
              region R do
                  await
 nk = 0 and m > 0;
                  nk:= nk+1 
               end;

background image

 

 

       

quit buf[lk];

               hold(t3);
               release buf [lk];
               lk:= lk mod N+1;
               region R do
                   
nk := nk l;
                   m := m1
               end;
                  (* przetwarzanie *
                   hold{t4)
     end                                      
end;
 (*konsument*)
 process (1..P) producent;
                    (1..K) konsument
        end. 

background image

 

 

PRZYKŁAD

 

Czytający-piszący

W  przykładzie  podamy  rozwiązanie  drugiej  wersji  tego  problemu. 
Opierać się ona będzie na sprawdzeniu relacji między liczbą procesów 
czytających,  które  wykonują  operację  czytania  (lc)  i  liczbą  procesów 
piszących,  które  chciałyby  pisać  (lp).  Warto  zauważyć,  jak  przez 
zmianę  kolejności  operacji  wewnątrz  rejonu  krytycznego  można 
pewnej  grupie  procesów  zapewnić  priorytet  w  stosunku    do  procesów 
drugiej grupy.

var R : shared record
lc, lp: integer := 0,0                         

 

end;

 

Z: resource; 
W: shared boolean := true; 

background image

 

 

procedure czytanie;
begin 
      cycle
           region R do
            
   await lp = 0;
               lc:=lc+1 
           
end;
        request
 Z;
              (* czytanie*) 
        release Z;
        region R do
            lc:=lc-1
       end                                                             
   end

end; (*czytanie*)

 

background image

 

 

 

procedure pisanie;

begin
      cycle
           region 
R do 
              
lp:=lp+1;
               await lc = 0 
            end;
            region W do  
                   
request Z;
                   (*pisanie*) 
                   release Z 
            end;
            region
 R do
                lp:=lp-1
            end 
      end
end;
( *pisanie*)
 process (1.. N) czytanie;

  (1..M)  pisanie

end.

Łatwo  zauważyć,  że  rejon  krytyczny  związany  ze  zmienną  dzieloną  W  służy  do 

wykluczenia między sobą procesów piszących w czasie pisania. 

background image

 

 

Implementacja warunkowych rejonów 

krytycznych

Warunkowe  rejony  krytyczne  są  naturalnym  mechanizmem  komunikacji 

między procesami, dostosowanym do potrzeb języków wysokiego poziomu. 

Pozwalają  one  na  jawne  przedstawienie  faktu  oczekiwania  przez  dany 

proces  na  spełnienie  pewnego  warunku  dotyczącego  zmiennej  dzielonej. 

Ceną  za  niewątpliwe  zalety  tej  metody  synchronizacji  są  znaczne 

utrudnienia realizacyjne.
        Pierwszą  propozycję  implementacji  warunkowych  rejonów  krytycznych 

podał  Brinch  Hansen  .  Proces  wywołując  instrukcję  rejonu  krytycznego  w 

zależności  od  tego,  czy  sekcja  objęta  tym  rejonem  jest  wolna  czy  nie, 

przechodzi  do  wykonania  instrukcji  rejonu  lub  jest  zawieszany  w  kolejce 

wejściowej związanej z daną zmienną dzieloną (RYS). Wykonując instrukcję 

await  przy  niespełnionym  warunku,  proces  opuszcza  czasowo  sekcję 

krytyczną i zostaje umieszczony w kolejce warunku Q

E

, również związanej z 

daną  zmienną  dzieloną.  W  tym  czasie  inne  procesy  z  kolejki  Q

V

  mogą  po 

kolei wchodzić do sekcji krytycznej, natomiast procesy z kolejki Q

E

 powinny 

okresowo  sprawdzać,  czy  nie  został  spełniony  warunek  zawieszenia.  W 

chwili, gdy sekcja krytyczna zostaje zwolniona, a obie kolejki są niepuste, 

występuje  konflikt  dwóch  procesów,  które  powinny  wejść  do  sekcji 

krytycznej.  W  takiej  sytuacji  musi  być  podjęta  jednoznaczna  decyzja  i 

jednocześnie  programista  musi  być  jej  świadomy.  Propozycją  Brinch 

Hansena jest, aby wyższy priorytet nadać procesom z kolejki warunku. 

background image

 

 

RYS. Schemat wykonywania rejonu krytycznego (a) i warunkowego 
rejonu   
          krytycznego (b)

...

SEKCJA

KRYTYCZNA

a)

Q

V

...

SEKCJA

KRYTYCZNA

...

b)

Q

V

Q

E

await

background image

 

 

Z  powyższego  schematycznego  algorytmu  implementacji 
widać  od  razu  kolejną  jego  wadę.  Przy  każdorazowym 
zwolnieniu  sekcji  krytycznej  należy  sprawdzać,  czy  nie  został 
spełniony 

warunek 

dla 

któregokolwiek 

procesów 

oczekujących  w  kolejce  warunku,  chociaż  do  rejonu 
krytycznego będzie mógł wejść tylko jeden proces, a pozostałe 
ponownie  zostaną  zawieszone.  Jest  to  oczywiście  wysoce 
nieefektywne,  zwłaszcza  w  przypadku,  gdy  wiele  procesów 
może być zawieszonych w tym samym czasie.

background image

 

 

PRZYKŁAD  Implementacja warunkowych rejonów krytycznych

 

Celem naszym jest pokazanie realizacji rejonów krytycznych w postaci:
var v: shared T;
. . .
region
 do ... await W ... end;
W czasie translacji instrukcji rejonów krytycznych dokonywane jest ich 
przekształcenie  w  instrukcje  języka  sekwencyjnego,  uzupełnione 
procedurami realizującymi algorytm działania rejonów. Przekształcenie 
to przedstawimy kolejno dla poszczególnych elementów struktury. 
(1) Uzupełnienie deklaracji zmiennej dzielonej deklaracją:

var vv: record 

                         v: T;
                        r: rejon
                        
end;

gdzie pole r typu rejon jest przeznaczone dla potrzeb implementacji. 

background image

 

 

(2) Zastąpienie instrukcji wejścia do rejonu (region...) instrukcją:
      with vv.v do wejscie(vv.r);
 
(3) Zastąpienie instrukcji await instrukcją:                               

       

while not do oczekiwanie(vv.r) end;

(4) Poprzedzenie instrukcji wyjścia z rejonu (end) instrukcją:

wyjście (vv.r) end (* zamyka instrukcję with vv ... *);

 

W pierwszej wersji implementacji procedur wykorzystamy znane już 
operacje kolejkach i procesach. Struktura danych typu rejon ma postać:

 

type rejon = record

stan: Boolean := false; (*dostępność rejonu*) 
Qv, Qe: queue := nil, nil; (*kolejka wejściowa i warunku*) 
lqe: integer := 0; (*licznik procesów zawieszonych w Qe*)

 

pr: proc

           end;

background image

 

 

Monitory

Monitor  jest  składnikiem  programu,  złożonym  z  deklaracji  zmiennych 

wspólnych,  dzielonych  przez  kooperujące  ze  sobą  procesy,  oraz  ze 

zbioru wszystkich procedur działających na tych zmiennych. Zazwyczaj 

monitor  deklaruje  się  jako  zmienną  specjalnego  typu  monitorowego. 

Schematyczna postać deklaracji monitora wygląda następująco:

var identyfikator-monitora: monitor 

      

      deklaracje-zmiennych;
      procedury;
      lista-udostępnionych-na-zewnątrz-nazw-procedur 
      end.

Na zmiennych monitora mogą być wykonywane operacje wyłącznie w 

procedurach  monitora,  przy  czym  niektóre  z  tych  ostatnich  (lub 

wszystkie)  mogą  być  wywołane  ze  środowiska  zewnętrznego  (np.  z 

procesów  lub  innych  monitorów).  Wywołując  procedurę  monitora, 

należy  podać  jego  identyfikator,  nazwę  procedury  i  listę  jej 

parametrów aktualnych.

identyfikator-monitora.nazwa-procedury(parametry aktualne)

background image

 

 

var M: monitor
var 
R: resource;

                 procedure A;

   begin
        . . .
       (* operacja A na zasobie R*)
        . . .
    end;
 (*A*) 

                 procedure Z;

    begin
         . . .
       (* operacja Z na zasobie R*)
         . . .
     end;(*Z*)

 

     export A,..., Z 

        end;(*M*)
        . . .
        
(*proces 1*)
         . . .
        M.A;
         . . .
       
(*proces N*)
        . . .
        M.Z;
        . . .

background image

 

 

 

Z kolejką są związane jeszcze dwie pomocnicze operacje:

           clear(kol) — procedura przywracająca pierwotny stan kolejki — pusty 

(przy powstawaniu monitora kolejki są zawsze puste), 

           empty(kol) — funkcja sprawdzająca czy kolejka jest
                

 pusta (wartość true), czy nie (wartość false).

Zastosowanie monitorów

PRZYKŁAD  Producent-konsument

var bufor: monitor

 

const N = MAX;

      var buf: array [1..N] of buffer;

pełny, pusty: queue;
lp, lk: integer := 1, 1;
m: integer := 0; (*liczba nieodebranych zapełnionych pól buforowych*)

background image

 

 

procedure wpisz;
begin

if  m = N then delay (pełny) end;
fill
 buf[lp]; 
  

hold(t2);

  

release buf[lp];

  

lp := lp mod N+1;

  

m := m+1;

  

continue (pusty) 

  end; (*wpisz*) 
  procedure pobierz;
  begin

if  m = 0 then delay (pusty) end;
quit
 buf[lk];
hold(t3);
release buf [lk];
lk := lk mod N+1;
m := m—1;
continue (pełny) 

end; (*pobierz*)

background image

 

 

export wpisz, pobierz 
end; (*bufor*) 
procedure producent;
begin

            cycle
     

(* przygotowanie wiadomości*) 

      

hold(t1) 

      

(* wysłanie wiadomości*)

    

bufor.wpisz 

             end
      end;
 (*producent*) 

procedure konsument 
begin

             cycle
       

(* odebranie wiadomości *)

       

bufor. pobierz;

       

(*przetwarzanie*)

       

hold(t4) 

            end
      end;
 (*konsument*)
      process (1 . .P) producent;
                     (1 . .K) konsument
end.

background image

 

 

Wyrażenia ścieżkowe

Wyrażenia  ścieżkowe  (ang.  path  expressions),  będące  koncepcyjnie 
odmiennym  mechanizmem  synchronizacji  procesów  współbieżnych, 
zostały wprowadzone przez Campbella i Habermanna. Zaproponowana 
przez  nich  metoda  opisuje  synchronizację  na  poziomie  procedur. 
Oznacza  to,  że  każda  akcja,  która  ma  podlegać  synchronizacji,  musi 
wystąpić w programie w postaci oddzielnej procedury.
Wzajemne  powiązania  między  tymi  procedurami  są  opisywane  przy 
użyciu  specjalnych  operatorów,  ustalając  warunki,  jakie  muszą  być 
spełnione, aby po wywołaniu dana procedura mogła być wykonana.

Zapis wyrażeń ścieżkowych

 

Podstawowymi 

powiązaniami 

występującymi 

wyrażeniach 

ścieżkowych  są    sekwencja  i  selekcja  akcji  (przez  termin  akcja 
będziemy  w  tym  rozdziale  rozumieli    wykonanie  przez  proces 
procedury,  które,  zgodnie  z  uprzednią  definicją  pojęcia  akcji,  będzie 
traktowane jako czynność jednostkowa, niepodzielna).

 

background image

 

 

Omówione  podstawowe  elementy  wyrażeń  ścieżkowych  mogą  być 
łączone, tworząc bardziej skomplikowane ścieżki. Na przykład ścieżka

p;(q, r);s

 

synchronizuje  cztery  akcje.  Pierwszą  z  nich  musi  być  wykonanie 
procedury  p,  następnie  jednej  z  procedur  q  lub  r  ,  po  czym  kolejną 
akcją może być tylko wykonanie procedury s. A więc powyższa ścieżka 
określa dwie możliwe serie wykonań wymienionych procedur: p-q-s lub 
p-r-s (RYS.). W podanym przykładzie zostały  użyte dodatkowe nawiasy 
okrągłe;  ich  znaczenie  jest  takie  samo,  jak  w  wyrażeniach 
arytmetycznych.  Pozwalają  one  na  sprecyzowanie  kolejności 
analizowania  operatorów  w  ramach  ścieżki  (w  pracy  Campbella  i 
Habermanna  założono  jednakowy  priorytet  wszystkich  operatorów 
występujących w wyrażeniach ścieżkowych). 

background image

 

 

RYS.  Graf pierwszeństwa dla ścieżki p; (q, r); s

q

p

r

s

background image

 

 

Kolejnym  elementem  sterowania  wykonaniem  procedur  jest  ich 
równoczesność.  Pozwala  ona  na  równoległe  wykonywanie  danej 
procedury lub fragmentu wyrażenia ścieżkowego przez kilka procesów. 
Operatorami  równoczesności  są  nawiasy  kątowe  <i>  (oryginalnie 
zaproponowano  nawiasy  {i},  które  jednak  zazwyczaj  nie  występują 
wśród 

znaków 

urządzeniach 

peryferyjnych). 

Przykładem 

zastosowania równoczesności może być ścieżka

<q; s>

Przykładem bardziej skomplikowanej ścieżki może być wyrażenie

 

path n,(p; <q ; s>) end 

background image

 

 

Zastosowanie wyrażeń ścieżkowych

Wyrażenia  ścieżkowe  są  jasną  i  zwięzłą  metodą  opisu  synchronizacji 

dla  szerokiej  klasy  problemów.  Zasady  stosowania  wyrażeń 

ścieżkowych pokażemy na podstawie kilku wersji przykładu współpracy 

procesów czytających i piszących.

PRZYKŁAD Czytający-piszący  
Najprostszą synchronizację obu grup procesów można przedstawić 

następująco: 

var

    

  R: resource;

 

procedure czytanie;
begin
     reguest R;
     (* czy tanie*)
      release R

         

end; (*czytanie*) 

background image

 

 

 

procedure pisanie;

   begin

     request R;
     (*pisanie*)

        release R
      
end; (*pisanie*)
  
procedure czyt;
      begin
          cycle
              czytanie
          end

       end;(*czyt*)

 

background image

 

 

procedure pisz;
begin

     cycle 

            pisanie
           end
end;
 (*pisz*)
 
path czytanie, pisanie end;
process (1..N) czyt;
              (1..M) pisz
 
end.

background image

 

 

Zastosowane  wyrażenie  ścieżkowe  synchronizuje  wykonywanie 
procedur czytanie i pisanie w taki sposób, że w każdej chwili może być 
realizowana  tylko  jedna  z  nich  przez  dokładnie  jeden  proces. 
Wyrażenie  to  nie  określa  również  kolejności  wykonywania  obu  tych 
procedur.
Wymiana użytej ścieżki przez

path <czytanie>, pisanie end;

powoduje, że procedura czytanie może być realizowana równocześnie 
przez  kilka  procesów. Jeżeli  jakiś  proces rozpoczął czytanie,  to dostęp 
do  tej  procedury  będzie  natychmiastowy  dla  innych  procesów  tale 
długo  jak  długo  procedura  ta  będzie  wykonywana  przez  co  najmniej 
jeden proces (priorytet czytania jest wyższy niż pisania). 

background image

 

 

W celu wyrównania priorytetów obu operacji czytania i pisania należy 
poprzednią ścieżkę zastąpić wyrażeniem

path <czytanie>, <pisanie> end;
path
 pisanie end

 

Pierwsza  ścieżka  zapewnia  równość  priorytetów  obu  operacji, 
natomiast druga wykluczenie wzajemne wykonania procedury pisanie. 
Analiza  tego  rozwiązania  pokazuje,  że  jego  wadą  jest  blokowanie 
dostępu do zasobu — jeżeli jakiś proces zacznie czytać (lub pisać), to 
ze względu na równoczesność przez pewien czas może być blokowany 
dostęp dla zapisu (odczytu).
Zlikwidowanie blokowania jest możliwe przez dodanie do poprzedniego 
rozwiązania procedury pustej zgłoszenie-czytania

procedure zgłoszenie-czytania,     
begin end;

background image

 

 

zmienienie procedury czyt;

procedure czyt;
begin 
cycle
zgloszenie-czytania;
czytanie
   
end
 end;
 (*czytanie*)
oraz wymianę wyrażenia ścieżkowego na
path zgłoszenie-czytania, pisanie end;
path
 <zgłoszenie-czytania; czytanie>, pisanie end;
 
 

background image

 

 

Proces  czytający  musi  obecnie  wykonać  sekwencję  procedur 
zgłoszenie-czytania  i  czytanie.  Pierwsza  z  nich  jest  procedurą 
pomocniczą, 

której 

jedyną 

funkcją 

jest 

zapewnienie 

pojedynczego  przyjmowania  żądań  czytania  lub  pisania  w 
czasie  wykonywania  tych  operacji.  Tak  więc  pierwsza  ścieżka 
powoduje,  że  szanse  startu  operacji  czytania  i  pisania  są 
równe. Jeżeli została wykonana procedura zgłoszenie-czytania, 
to  druga  ścieżka  powoduje,  że  pisanie  będzie  mogło  być 
realizowane  dopiero  po  wykonaniu  procedury  czytanie. 
Równoczesność  w  drugiej  ścieżce  pozwala  na  równoległe 
czytanie przez wiele procesów czytających pod warunkiem, że 
nie nadeszły żadne żądania zapisu informacji.

background image

 

 

    Powyższe  rozwiązanie  odpowiadało  pierwszemu  problemowi 

piszących  i  czytających.  Rozwiązanie  drugiego  problemu,  w 
którym  procesy  piszące  mają  wyższy  priorytet,  wymaga 
pewnej modyfikacji wyrażenia ścieżkowego do postaci

 path zgłoszenie-czytania end;

path zgłoszenie-czytania, <pisanie> end;
path
 <zgłoszenie-czytania; czytanie>, pisanie end; 

background image

 

 

Mechanizmy komunikacji i synchronizacji w 

systemie Unix

      Współbieżność procesów w systemie Unix

Unix  jest  systemem  wielozadaniowym  i  wielodostępnym,  co  oznacza, 
że  w  jednym  momencie  z  jednego  komputera  może  korzystać  wielu 
użytkowników,  oraz  że  każdy  użytkownik  może  uruchomić  w  danej 
chwili  więcej  niż  jeden  proces.  Proces  jest  to  środowisko  wykonania 
programu,  które  składa  się  z  segmentu  instrukcji,  segmentu  danych 
użytkowych  oraz  segmentu  danych  systemowych,  podczas  gdy 
program jest to plik zawierający instrukcje i dane służące do inicjacji 
segmentu instrukcji oraz segmentu danych użytkowych procesu.
Pojęcie  jednoczesnego  (współbieżnego)  wykonywania  wielu  procesów 
na  maszynie  jednoprocesorowej  jest  w  znacznym  stopniu  umowne  i 
oznacza ono podział czasu procesora pomiędzy wykonywane procesy.

background image

 

 

       

            Jednym  z  dobrodziejstw  systemu  operacyjnego  Unix  jest  możliwość 

rozwidlania 

procesów. 

Rozwidlający 

proces 

zwany 

procesem 

macierzystym tworzy procesy potomne, podczas gdy każdy z procesów 

potomnych  ma  możliwość  tworzenia  następnych  procesów.  Pozwala  to 

nam  na  współbieżne  wykonywanie  np.  procedur  obliczeniowych,  co 

zwłaszcza  przy maszynach wieloprocesorowych  lub  podziale  obowiązków 

na kilka komputerów korzystnie może wpłynąć na czas trwania obliczeń.

            Mówiąc  o  rozwidlaniu  procesów  nie  można  nie  wspomnieć  o  metodach 

komunikacji  i  synchronizacji  międzyprocesowej.  O  tym  jak  ważne  jest  to 

zagadnienie  niech  świadczy  poniższy  przykład.  Niech  zagadnieniem 

procesu  P1  będzie  odjęcie  z  zasobu  dzielonego  wartości  5  (student 

podejmuje  pieniądze  na  książki  z  konta  bankowego).  W  tym  celu 

odczytuje aktualną wartość konta np. 5,00 , odejmuje od niej wartość 5 i 

otrzymaną różnicę (0,00) właśnie chce zapisać jako nowy stan kąta. Lecz 

w tym momencie proces P2 (dział ds. stypendiów naukowych ) przelewa 

na konto stypendium w wysokości 20. W tym celu odczytuje wartość 5,00 

(ciągle  jest  tam  niezmieniona  wartość)  i  dodaje  do  niej  20.  Cały  przelew 

trwał    „ułamek  sekundy”  ,  nowa  wartość  to  25,00.  Proces  P1  dokańcza 

operację zapisu nowej wartości (tej początkowej pomniejszonej o 5) i stan 

kąta  wynosi  0,00.  W  wyniku  tych  operacji  konto  studenta  posiada 

nieprawidłową wartość.

background image

 

 

            Jednak  system  operacyjny  Unix  dostarcza  nam  całą  gamę 

przeróżnych 

mechanizmów 

komunikacji 

synchronizacji 

międzyprocesowej.  W  systemach  unixowych,  które  poprzedzały 
System III, procesy mogły komunikować się między sobą za pomocą 
dzielonych  wskaźników  do  plików,  sygnałów,  śledzenia  procesów, 
plików  oraz  łączy  komunikacyjnych  –  potoków.  W  Systemie  III 
zastosowano  kolejki  proste  FIFO  (łącza  nazwane).  W  Systemie  V 
pojawiły  się  semafory,  komunikaty  oraz  pamięć  dzielona,  potem 
zdalne wywołanie procedur w systemie Unix BSD.

Dzielone  wskaźniki    do  pliku  są  rzadko  używane  do  komunikacji. 
Teoretycznie, jeden proces może ustawić wskaźnik do pliku (na pewne 
fikcyjne  miejsce  na  pliku),  a  drugi  może  sprawdzić,  co  ten  wskaźnik 
wskazuje. To miejsce w pliku stanowiłoby obszar komunikacji.

Sygnały  są  używane,  gdy  proces  potrzebuje  tylko  dać  znak  innemu 
procesowi.  Za  ich  pomocą  nie  można  jednak  przekazać  tylu 
informacji,  aby  mogły  być  one  przydatne  w  większości  zastosowań. 
Wadą  sygnałów  jest  to,  że  powoduje  on  przerwanie  wykonywania 
procesu,  do  którego  jest  on  skierowany.  Sygnały  służą  głównie  do 
kończenia wykonywania procesów. 

background image

 

 

            Śledzenie  procesów  służy  do  kontrolowania  przez  proces  swoich 

potomków. Proces macierzysty może czytać oraz pisać w obszarze danych 
swego potomka, co pozwala im na wspólną komunikację.

      Pliki są najbardziej powszechną metodą komunikacji międzyprocesowej, 

nie są jednak odpowiednie dla procesów wykonywanych współbieżnie.

            Mechanizm  łączy  komunikacyjnych  skutecznie  rozwiązuje  problemy 

synchronizacji między procesami. Pomimo, że łącze, podobnie jak plik, ma 
swój i-ty węzeł to dowiązania do niego nie istnieją. Jeżeli proces czytający 
wyprzedzi  proces  piszący,  to  musi  on  czekać  na  następne  dane.  Jeżeli 
proces piszący wyprzedzi proces czytający, to zostanie on wstrzymany, aż 
dogoniony zostanie przez proces czytający.

            Kolejka  FIFO  jest  plikiem  specjalnym,  który  może  zostać  otwarty  do 

czytania lub pisania przez każdy proces mający odpowiednie uprawnienia. 
Zagwarantowana  jest  również  niepodzielność  wykonywanych  operacji. 
Bajty  danych  zapisywane  lub  odczytywane  podczas  jednego  wywołania 
funkcji systemowej tworzą zawsze spójny blok.

            Komunikat  jest  to  niewielka  liczba  danych,  którą  można  przesłać  do 

kolejki komunikatów. Komunikaty mogą być różnych typów. Proces mający 
odpowiednie uprawnienia, może pobierać komunikaty z kolejki. 

background image

 

 

Przy rozwiązywaniu problemów programowania współbieżnego w tej  

pracy 

wykorzystane zostały następujące mechanizmy:
 
- mechanizmy IPC Systemu V : 
- semafory, 
- pamięć dzielona,
- zdalne wywołanie procedur (RPC- Remote Procedurę Cali, wersja Sun 

RPC).

Aby rozwidlić proces w systemie Unix należy w kodzie programu   
wywołać funkcję
                                                   int   fork() 

background image

 

 

      Funkcja fork() tworzy kopię procesu, który tę funkcję wywołał. 

Proces  wywołujący  funkcję  fork()  nazywa  się  procesem 
macierzystym  lub  przodkiem  (rodzicem)  nowego  procesu, 
nowy  zaś  proces  -  procesem  potomnym  lub  potomkiem 
(dzieckiem).  Funkcja  systemowa  fork()  wywołana  raz  (przez 
proces 

macierzysty) 

przekazuje 

wartość 

dwukrotnie 

(przodkowi  i  potomkowi).  Te  wartości  różnią  się  tym,  że  w 
przypadku 

procesu 

macierzystego 

jest 

to 

numer 

identyfikacyjny  nowo  utworzonego  procesu  potomnego,  a 
wartością przekazywaną procesowi potomnemu jest zero. Gdy 
funkcji  for  k  ()  nie  uda  się  wykonać  pomyślnie,  wówczas  jej 
wynikiem  jest  -1.  Jeśli  proces  potomny  chce  uzyskać  numer 
identyfikacyjny  swojego  przodka,  to  może  wywołać  funkcję 
systemową getppid () . 

       

background image

 

 

Przykład:
 

main ()

{
       int   pid_potomka;
       if   ((pid_potomka=fork())==-1)
perror(„Błąd   fork" ) ; 
else
             if(pid_potomka==0) 
            / /proces potomny
            printf(„Potomek: pid potomka=%d, pid przodka=

%d\n",getpid(),  

            getppid()); 
              else
            / /proces macierzysty
            printf(„Przodek:pid potomka=%d, pid przodka=%d\n",pid 

potomka,  

            getpid()); 

background image

 

 

     

Aby zakończyć wykonywanie procesu należy wywołać  funkcję 

      systemową exit()
                       
void  exit (int   stan) 
     Argument stan jest wartością stanu końcowego procesu wywołującego 

funkcję exit().

      Powrót z tej funkcji nigdy nie następuje. Przyjmuje się, że zerowy stan 

oznacza poprawne zakończenie procesu, niezerowy zaś (najczęściej -1) 
oznacza wystąpienie błędu.

Proces macierzysty procesu kończącego działanie otrzymuje jego kod   
stanu przy pomocy funkcji systemowej wait().
                                              
                                          int   wait(int   *stan)

background image

 

 

           

Argument  *stan  jest  miejscem,  pod  które  wstawiony  zostanie  kod 

wyjścia  procesu  potomnego  (argument  funkcji  exit  ()  ).  Funkcja 
zwraca identyfikator procesu lub -1 w przypadku błędu.

Jeżeli istnieją procesy potomne, to funkcja systemowa wait() czeka na 
zakończenie  jednego  z  nich.  Nie  można  zlecić  czekania  na  określony 
proces  potomny.  Potomek,  który  pierwszy  zakończy  działanie, 
powoduje  „odwieszenie"  rodzica,  który  wywołał  wait().Jeśli  wskaźnik 
stan ma wartość NULL, stan zakończenia procesu potomnego nie jest 
nigdzie zapamiętany.

Semafory
W  systemie  UNIX  operacje  semaforowe  (chociaż  w  dalszym  ciągu 
najważniejsze  to  nadanie  wartości  początkowej,  zwiększenie  i 
zmniejszenie) nie wyglądają już tak prosto.
Operacji      P(S)        (opuszczenie)      odpowiada      wywołanie      funkcji     
semop()   z następującymi argumentami: 

struct   sembuf   sem_lock={0,-1,0}; 
semop(sid,&sem_lock,1);

background image

 

 

Aby  używać  semaforów  w  programach  pisanych  w  języku  C 

należy  do  ich  treści  dołączyć  za  pomocą  dyrektywy  #include 

następujące pliki:

      #include <sys/types.h>
      #include <sys/ipc.h>
      #include <sys/sem.h>

Oto  funkcja  zablokuj()  odpowiadająca  operacji  P(S).  Argument 

sid  jest  numerem  identyfikacyjnym  zestawu  semaforów 

zwróconym  przez  funkcję  semget(),  zaś  numer_semafora  jest 

kolejnym numerem semafora w zestawie licząc od zera.

background image

 

 

void zablokuj (int sid, int numer_semafora)
{

int wartosc;
struct sembuf sem_lock={numer_semafora,-1,0};
wartosc=semctl(sid,numer_semafora,GETVAL,0);
if(!wartosc)

         {
fprintf(stderr,"PID:%d-Proces wstrzymany na semaforze !\n",getpid());

fflush(stdout);

   }

     i f ( ( semop (sid, &sem_lock, 1) ) ==-1) 

   {

  fprintf (stderr, "PID: %d-Blokowanie nie powiodlo sie!\n", getpid ());

     f flush (stdout) ; 

           exit (EXIT_FAILURE) ;

   }

     else
printf ( "PID : %d-Semafor opuszczony do wartosci %d\n", 
getpid ( ) , wartosc) ;
}

background image

 

 

      Wywołanie wartosc=semctl (sid,numer_semafora,GETVAL, 0)   

    nadaje  zmiennej  wartość  aktualną  wartość  semafora  o 
numerze podanym jako drugi argument.

      Natomiast operacji V(S) - podniesienie ,odpowiada wywołanie 

funkcji semop() z następującymi argumentami:

      struct sembuf sem_lock={0,1,0}; 
      semop(sid,&sem lock,1);

      Oto funkcja odblokuj() odpowiadająca operacji V(S).

background image

 

 

void odblokuj (int sid, int numer_semafora) 
{

struct sembuf sem_unlock={numer_semafora,1,0};
int wartosc;

i f ( (semop (sid, &sem_unlock, 1) ) ==-1)

{

                   fprintf (stderr, "PID: %d-0dblokowanie nie powiodlo sie! 

     \n",getpid() ) ;
     fflush (stdout) ; 
     exit (EXIT_FAILURE) ; 

else
}

             wartosc=getval (sid, numer_semafora) ;

   printf ( "PID: %d-Semafor opuszczony do wartosci %d\n", 
   getpid(), wartosc ); 
}

}

background image

 

 

      System UNIX daje nam możliwość wykonywania jednoczesnych 

operacji  semaforowych,  przy  czym  każda  z  nich  może  być  inna. 
Wykonanie  operacji  jednoczesnej  wtedy  kończy  się  sukcesem, 
gdy zostaną wykonane wszystkie jej operacje składowe.

              Poniższy  kod  wykonuje  zablokowanie  semafora  o  numerze 

SEMAFOR_PELNE (element sem_num struktury sembuf ustawiony 
na  -1)  z  jednoczesnym  odblokowaniem  semafora  o  numerze 
SEMAFOR_CK (element ten ustawiony na 1) z zestawu semaforów 
identyfikowanego  przez  zmienną  sid  (wartość  zwrócona  przez 
semget()).  Operacja  ta  będzie  blokująca,  tzn.  w  przypadku 
zajętych  zasobów  proces  będzie  tak  długo  czekał  z  wywołaniem 
funkcji  semop()  dopóki  jakiś  inny  proces  ich  nie  zwolni  (element 
sem_flg struktury sembuf nie jest ustawiony na IPC_NOWAIT).

 

background image

 

 

struct sembuf sops[2];

sops[0].sem_num=SEMAFOR_PELNE;
sops[0].sem_op=-1;
sops[0].sem_flg=0;
sops[1].sem_num=SEMAFOR_CK;
sops[1].sem_op=1;
sops[1].sem_flg=0;

if((semop(sid,sops,2)==-1)

 

{

fprintf(stderr,"PID:%d-Blokowanie nie powiodlo sie!\n",getpid());

    exit(EXIT_FAILURE); 

    } 

background image

 

 

Funkcje systemowe Unixa dotyczące semaforów:

1.

key_t   ftok   (char   *ścieżka,   char projekt)

      Funkcja ftok ()   przekształca nazwę ścieżki i identyfikator projektu na 

klucz, używany w komunikacji międzyprocesowej. Klucze służą do 
identyfikowania kolejek komunikatów, semaforów i pamięci dzielonej. 
Typ danych key_t znajduje się w pliku <sys/types . h> .

            Procesy,  które  korzystają  z  jednej  lub  kilku  metod  komunikacji 

międzyprocesowej  powinny  mieć  uzgodnioną  nazwę  ścieżki  oraz 
identyfikator projektu aby mieć pewność, że procesy korzystają z tego 
samego  kanału  komunikacyjnego  /tego  samego  zestawu  semaforów 
/tego samego obszaru pamięci dzielonej.

      Funkcja zwraca nową wartość klucza na podstawie kombinacji numeru 

i-węzła  i  podrzędnego  numeru  urządzenia  z  pliku  podanego  w 
argumencie  ścieżka  wraz  z  identyfikatorem  projektu  podanym  jako 
drugi  argument,  lub  -1  w  przypadku  błędu.  Może  to  nastąpić  np.  gdy 
nazwa ścieżki  określona argumentem  ścieżka  nie istnieje, lub gdy nie 
jest dostępna dla procesu wywołującego funkcję ftok () .

background image

 

 

Przykład:
key_t   klucz; klucz=ftok(".",'S')
printf("Wartość   klucza   wygenerowanego   przy   pomocy   ftok() :%x\n"   

,klucz);

Efekt działania programu:
[root@localhost /root]# Wartość klucza wygenerowanego przy pomocy 

ftok():53454d55

2.
        int semget(key_t klucz, int nsems, int semflg)

Wywołanie   systemowe   semget ()    tworzy   nowy   zestaw   semaforów   lub 
uzyskuje dostęp do już istniejącego. Zwraca identyfikator IPC zestawu semaforów 
przy pomyślnym wykonaniu lub -1 w przypadku błędu.

 Argument klucz jest wartością zwróconą przez funkcję f tok () .
Argument nsems określa liczbę  semaforów ,które zostaną utworzone dla nowego 
zestawu.  W  przypadku  otwarcia  już  istniejącego  zestawu  semaforów  argument 

ten 

jest ignorowany, możemy nadać temu argumentowi wartość 0.

background image

 

 

      Argument semflg mówi o tym, czy ma być utworzony nowy zestaw, 

czy też będzie używany już istniejący.

      Gdy semflg jest równe:

IPC_CREAT - zostanie utworzony nowy zestaw semaforów jeżeli do tej 
pory  nie  istniał.  Semget  ()  zwraca  identyfikator  zestawu,  który  został 
utworzony  lub  identyfikator  już  istniejącego,  który  posiada  podaną 
wartość  klucza.  IPC_CREAT|IPC_EXCL  -  zostanie  utworzony  nowy 
zestaw  semaforów  lub  semget  ()  zwróci  błąd.  Gwarantuje  to,  że  nie 
będzie używany już istniejący zestaw.

      Przykład:

semid=semget(klucz,l,0666|IPC_CREAT|IPC_EXCL)

      Wynikiem takiego wywołania będzie nadanie zmiennej semid numeru 

identyfikacyjnego  nowoutworzonego  zestawu  semaforów  złożonego  z 
jednego  semafora  (drugi  argument  funkcji)  lub  -1  w  przypadku,  gdy 
zestaw  semaforów  o  wartości  klucza  równej  parametrowi  klucz  już 
istnieje.  Nowoutworzony  zestaw  będzie  posiadał  prawa  czytania  i 
pisania dla właściciela, grupy oraz innych użytkowników (prawa 0666).

background image

 

 

3.

int semop(int semid, struct sembuf  *sops, unsigned nsops)
Wywołanie      systemowe        semop  ()        umożliwia      wykonywanie     
operacji      na  semaforach,  zwraca  0  gdy  sukces,  -1  w  przeciwnym 
wypadku.
Argument  semid  jest  numerem  identyfikacyjnym  zestawu  semaforów 
zwróconym przez semget().
Argument sops jest wskaźnikiem do tablicy operacji, które mają zostać 
przeprowadzone  na  grupie  semaforów.  Wskazuje  to  na  tablicę 
następujących struktur zdefiniowanych w <sys/sem.h>:

struct   sembuf
     {
       ushort   sem_num;   //numer   semafora

       short   sem_op;

 //operacja (dodatnia, ujemna lub 

zero)

       short   sem_flg; 

//flagi   operacji 

     }

background image

 

 

            Gdy  sem_op  jest  ujemny  jego  wartość  zostaje  odjęta  od 

wartości  semafora.  Odpowiada  to  zajęciu  (zablokowaniu) 
zasobów.  Jeżeli  nie  ustawiono  semjlg  na  IPC_NOWAIT  proces 
wywołujący  funkcję    semop()  śpi    do    czasu    aż  wymagana 
liczba będzie dostępna.

            Gdy  sem__op  jest  dodatni  jego  wartość  jest  dodawana  do 

semafora. Odpowiada to zwolnieniu (odblokowaniu) zasobów.

          Gdy  sem_op  jest  równy  zero  to  proces  wywołujący  funkcję 

semop() będzie czekał, aż wartością semafora będzie zero.

            Argument  nsops  jest  liczbą  operacji  w  tablicy  (liczba 

elementów tablicy  struktur). 

background image

 

 

Przykład:
void locksem(int sid)
{
struct sembuf sem_lock={0,-1, 0 }; 
if((semop(sid,&sem_lock,1))==-!)

{
    fprintf(stderr,"PID:%d-Blokowanie semafora nie powiodło sie!\n", 
getpid());    

         fflush(stdout); 

    exit(EXIT FAILURE);
}

}
Funkcja ta powoduje opuszczenie (zablokowanie) semafora o numerze 0 z 

zestawu  identyfikowanego  przez  sid  (parametr  zwrócony  przez  funkcję 
semget ())

4.

int   semctl(int   semid,   int   semnum,   int   cmd,   imion   semun   arg)

background image

 

 

Wywołanie  systemowe  semctl()  używane  jest  do  kontrolowania  zestawu 

semaforów, 

zwraca 0 gdy sukces, -1 w przeciwnym wypadku.
Argument semid jest numerem identyfikacyjnym zestawu semaforów zwróconym 
przez semget().
Argument semnum jest numerem semafora w zestawie (liczonym od zera). 
Argument cmd jest komendą, która ma zostać wykonana:

IPC_STAT - pobiera strukturę semid_ds i zachowuje ją pod adresem  buf 
argumentu semun.

 

IPC_SET - ustawia wartość ipc_perm struktury semid_ds. Wartość pobiera z buf 

 

unii semun.
IPC_RMID - usuwa zestaw z jądra.
GETALL - Używany do pobierania wartości wszystkich semaforów z zestawu.

Wartości całkowite przechowywane są w tablicy unsigned short int wskazanej przez 
array - członka unii semun.

background image

 

 

  

GETNCNT - zwraca ilość procesów oczekujących na zasoby.

  GETPID - zwraca PID procesu, który jako ostatni wywołał semop().
  GETYAL - zwraca wartość pojedynczego semafora z zestawu.
  SETALL - ustawia wartości wszystkich semaforów wartościami z            
  tablicy array  zawartej w unii semun.
    SETY  AL  -  ustawia  wartość  jednego  semafora  z  zestawu.  Wartość   
            pobiera z val zawartego w unii semun.

Argument  arg  jest    elementem    typu    semun.Typ  ten  jest  unią 

zadeklarowaną w <sys/sem.h> 

union   semun   {

int   val;

          /*   wartość   dla   SETVAL   */

struct   semid_ds   *buf;               /*  bufor  dla   IPC_STAT   i   IPC_SET   */
unsigned   short   int   *array;       /*   tablica   dla   GETALL   i   SETALL   
*/
struct   seminfo   * _buf;               /*  bufor  dla   IPC   INFO   */

}

background image

 

 

Przykład:

semval = semctl(sid,0,GETVAL, 0) ;

Funkcja  zwróci  wartość  semafora  o  numerze  0  z  zestawu 

identyfikowanego przez  sid (parametr zwrócony przez funkcję semget 
() ). 

union semun unia_sem;

if (semctl(semid, 0,IPC_RMID,unia_sem)==-1)
fprintf(stderr, "Nie udało sie usunac semafora\n"); 
else

printf("Semafor został usuniety\n");

Funkcja  semctl  ()  w  tym  przypadku  usunie  zestaw  semaforów 
identyfikowany  przez  semid  (parametr  zwrócony  przez  funkcję 
semget()), złożony z jednego semafora ( drugi argument).

background image

 

 

Dla  każdego  zestawu  semaforów  w  systemie  przydzielona  jest  jedna 

struktura semid_ds.
struct semid_ds  {
struct ipc_perm sem_perm; 
time_t sem_otime; 
time_t sem_ctime; 
struct sem *sem_base; 
struct wait_queue *eventn; 
struct wait_queue *eventz; 
struct sem_undo *undo; 
ushort sem nsems;

};
Najważniejsze z elementów struktury to:
sem_perm -jest to struktura typu ipc_perm, który jest zdefiniowany w sys/ipc 

. h.

Przechowuje prawa dostępu, oraz informacje o twórcy i właścicielu zestawu;
sem_otime - czas ostatniej operacji semop ();
sem_ctime - czas ostatniej zmiany struktury. Np. zmiana praw;
sem nsems - liczba semaforów w zestawie.

background image

 

 

Aby  pobrać  strukturę  semid_ds  należy  wywołać  funkcję  semctl()  

z  parametrem IPC_STAT:

union semun semopts;
struct semid_ds moja_ds;

semopts.buf=&moja_ds;      //pobranie     aktualnej     wartości 

struktury

semctl(sid,0,IPC_STAT,semopts); 
printf(„Stare prawa dostępu :%o\n", semopts.guf->sem_perm.mode);

Aby zapisać zmiany w strukturze należy wywołać funkcję semctl() z 

parametrem IPC_SET:

sscanf(nowe_prawa,"%ho",&semopts.buf->sem_perm.mode); 
semctl(sid,O,IPC SET,semopts);

background image

 

 

Pamięć dzielona.

Pamięć  dzielona  jest  najszybszym  z  mechanizmów  IPC,  ponieważ  nie 
istnieje żaden pośrednik. Dane w niej umieszczone przez proces piszący 
stają  się  natychmiast  dostępne  dla  innych  procesów.  Pamięć  dzielona 
pozwala  na  dostęp  do  tej  samej,  logicznej  pamięci  kilku  nie 
spokrewnionym  procesom.  Jest  bardzo  wydajną  metodą  przekazywania 
danych.
Pamięć  dzielona  jest  specjalnym  zakresem  adresów,  tworzonych  prze 
IPC  na  użytek  jednego  procesu  i  pojawiającym  się  w  jego  przestrzeni 
adresowej.  Inne  procesy  mogą  następnie  dołączyć  segment  pamięci 
dzielonej  do  swojej  przestrzeni  adresowej.  Segment  pamięci  dzielonej 
może  zostać  utworzony  przez  jeden  proces,  a  korzystać  z  niego  może 
dowolna  liczba  procesów.  Wszystkie  procesy  mogą  używać  pamięci 
dzielonej  tak  samo,  jakby  została  ona  przydzielona  funkcją  malloc(). 
Dokonanie  zapisu  do  pamięci  dzielonej  prze  jeden  z  procesów 
spowoduje,  że  będą  one  widoczne  również  dla  innych  procesów 
mających  do  niej  dostęp.  Może  istnieć  kilka  segmentów  pamięci 
dzielonej,  z  których  każdy  jest  dzielony  przez  określony  podzbiór 
aktywnych  procesów.  Każdy  proces  może  mieć  dostęp  do  kilku 
segmentów pamięci dzielonej.

background image

 

 

      Pamięć dzielona nie udostępnia żadnych mechanizmów synchronizacji 

procesów.  Nie  ma  możliwości,  aby  automatycznie  zapobiegać 
odczytywaniu  pamięci  przez  jeden  proces,  zanim  inny  nie  zakończy  jej 
zapisywać. W tym celu należy samemu zapobiec takiej sytuacji, np. tak 
jak to zostało zrobione w tej pracy, używając semaforów.

Funkcje systemowe Unixa dotyczące pamięci dzielonej.
1.
int   shmget(key   t   klucz,   int   size,   int   shmflg)

            Wywołanie  systemowe  shmget  ()  tworzy  nowy  segment  pamięci 

dzielonej  lub  uzyskuje  dostęp  do  już  istniejącego.  Zwraca  identyfikator 
IPC  segmentu  pamięci  przy  pomyślnym  wykonaniu  lub  -1  w  przypadku 
błędu.

Argument klucz jest wartością zwróconą przez funkcję f tok () .
Argument  size  określa  rozmiar  nowotworzonego  obszaru  pamięci 
dzielonej.
Argument  shmflg  mówi  o  tym,  czy  ma  być  utworzony  nowy  segment, 
czy też będzie używany już istniejący. 

background image

 

 

Gdy shmflg jest równe:
IPC CREAT - zostanie utworzony nowy segment pamięci dzielonej jeżeli 
do tej pory  nie  istniał.   Shmget ()    zwraca  identyfikator  segmentu,  
  który    został  utworzony  lub  identyfikator  już  istniejącego,  który 
posiada podaną wartość klucza.
IPC_CREAT|IPC_EXCL  -  zostanie  utworzony nowy  segment pamięci  
lub shmget ()   zwróci błąd. Gwarantuje to, że nie będzie używany już 
istniejący obszar.

Przykład:

shmid=shmget(klucz,100,0666 IPC CREAT IPC EXCL)

Wynikiem takiego wywołania będzie nadanie zmiennej shmid numeru 
identyfikacyjnego  nowoutworzonego  obszaru  pamięci  dzielonej  o 
wielkości 100 bajtów (drugi argument funkcji) lub -l w przypadku, gdy 
obszar pamięci o wartości klucza równej parametrowi klucz już istnieje. 
Nowoutworzony segment będzie posiadał prawa czytania i pisania dla 
właściciela, grupy oraz innych użytkowników (prawa 0666).

background image

 

 

2.

int shmat(int shmid, char *shmaddr, int shmflg)

Wywołanie  systemowe  shmat  ()  służy  do  podłączenia  (mapowania) 
segmentu pamięci dzielonej w obszar adresowy. Shmat () zwraca adres 
pod którym segment został przyłączony do procesu lub -1 w przypadku 
błędu.
Argument   shmid  jest   numerem   identyfikacyjnym   segmentu   
pamięci   dzielonej zwróconym przez shmget () .

Argument  shmadrr  powinien  być  ustawiony  na  0.  W  takim  przypadku 
jądro  wybiera  adres  dla  procesu  wywołującego.  W  innym  przypadku 
musimy  zrobić  to  sami.  Jeżeli  nie  chcemy  wymuszać  stronicowego 
wyrównania  adresu  (zaokrąglenia  w  dół  do  najbliższego  rozmiaru 
strony)  ani  nie  chcemy  aby  segment  został  wmapowany  jako 
"tylko_do_odczytu"  (SHM_RDONLY)  argument  shmflg  ustawiamy  na 
zero. 

background image

 

 

Przykład:

if((segptr=shmat(shmid,0,0))==-1)

{
   perror(”shmat”);
   return(1);
}

3.

int shmctl(int shmid, int cmd, struct shmid ds *buf)

Wywołanie  systemowe  shmctl  ()    używane  jest  do  kontrolowania 
segmentu  pamięci  dzielonej,  zwraca  0  gdy  sukces,  -l  w  przeciwnym 
wypadku.  Argument  shmid  jest  numerem  identyfikacyjnym  obszaru 
pamięci  zwróconym  przez  shmget().  Argument  cmd  jest  komendą, 
która ma zostać wykonana:
IPC_STAT - pobiera strukturę shmid_ds i zachowuje ją pod adresem buf.

background image

 

 

IPC_SET - ustawia wartość ipc_perm struktury shmid_ds. Wartość pobiera z 
argumentu buf.
IPC_RMID - zaznacza segment do usunięcia.
Argument buf jest wskaźnikiem do elementu typu shmid_ds. Typ ten jest 

strukturą 

zadeklarowaną w <sys/shm.h>
Podanie komendy IPC_RMID nie usuwa segmentu, lecz go tylko zaznacza do 
usunięcia. Ostateczne usunięcie segmentu następuje po odłączeniu segmentu 
przez ostatni proces. Przykład:
Aby   zaznaczyć   do   usunięcia   segment   należy   wywołać   funkcję   

shmctl()    z 

parametrem IPC_RMID

shmctl(shmid,IPC_RMID,0);

background image

 

 

Aby pobrać strukturę semid_ds należy wywołać funkcję shmctl ()   z 
parametrem IPC STAT:
char *prawa;
struct shmid_ds struktura_ds;
shmctl(shmid,IPC_STAT,&struktura_ds); 
printf("Stare prawa dostępu: %o\n" , struktura__ds . shm_perm.mode) ;

Aby zapisać zmiany w strukturze należy wywołać funkcję shmctl() z 
parametrem IPC_SET:

printf("Podaj nowe prawa    :"); 
scanf("%o",prawa);
struktura__ds . shm_perm.mode=*prawa; 
if((shmctl(shmid,IPC_SET,&struktura_ds))==-1)
printf("Błąd shmctl\n"}; 
else
    printf("Zaktualizowano prawa dostępu..\n",struktura ds.shm 
perm.mode ) ;

background image

 

 

4.

int shmdt(char *shmaddr)

Wywołanie  systemowe  shmdt  ()  odłącza  segment  pamięci  dzielonej, 
zwraca 0 gdy sukces, -1 w przeciwnym wypadku.

Argument  shmaddr  jest  wskaźnikiem  do  obszaru  pamięci  dzielonej 
(wartość zwrócona przez shmat () ).

Aby  zobaczyć  aktualnie  istniejące  mechanizmy  IPC  należy  z  linii 
komend wydać polecenie ipcs. Uzyskamy w ten sposób informacje na 
temat  istniejącego  klucza,  numeru  identyfikacyjnego  utworzonych 
mechanizmów  IPC,  właściciela  mechanizmu,  prawach  dostępu  do 
niego,  liczby  bajtów  segmentu  oraz  liczby  procesów  z  niego 
korzystających  w  przypadku  pamięci  dzielonej,  liczby  semaforów  w 
zestawie. 

background image

 

 

Przykładowy wynik wykonania polecenia ipcs na ekranie 

terminala: 

—— Shared Memory Segments ——— 

key       shmid     owner     perms     bytes     nattch    status 
0x53454d55 1         root      666       50        0

—— Semaphore Arrays ——

  key       semid     owner     perms     nsems     status
  0x53454d550         root         666      6

 —— Message Queues ———
 key       msqid     owner     perms     used-bytes messages

background image

 

 

Rozwiązywanie problemów wzajemnego 

wykluczania za pomocą semaforów

Dzięki semaforom w prosty sposób można zrealizować wzajemne 

wykluczanie. Wystarczy do tego jeden semafor binarny.

binarny_semafor S=1;  // wartość początkowa semafora ustawiona na 1
process P
{

while (TRUE)
{
sekcja_lokalna;
P(S);                           //  opuszczenie ( zablokowanie ) semafora
sekcja_krytyczna; //  korzystanie z zasobu dzielonego
V(S); 

        //  podniesienie ( odblokowanie ) semafora

}

}
 

background image

 

 

Ponieważ  wartość  początkowa  semafora  jest  równa  1  tylko 
jeden  proces  będzie  mógł  wykonać  P(S)  natychmiast. 
Pozostałe  procesy  będą  musiały  poczekać,  aż  proces  ten 
wykona operację V(S), inaczej mówiąc pozostałe procesy będą 
wstrzymane na semaforze S.

background image

 

 

Wzajemne wykluczanie
Funkcja proces(czas_kryt, 
czas_lok)

Początkowa wartość 
semafora:
SEMAFOR_WW=1;

Protokół 
wstępny

Zablokuj(SEMAFOR_WW)

Wyświetl(znak)

Czekaj(czas_kryt)

Wyświetl(znak)

odblokuj(SEMAFOR_WW)

Czekaj(czas_lok)

Sekcja 
krytyczna

Protokół 
końcowy

Sekcja 
lokalna

background image

 

 

Rozwiązywanie problemu wzajemnego 

wykluczania z tablicą czasów trwania sekcji 

za pomocą semaforów

Aby  do  rozwiązania  problemu  wzajemnego  wykluczania  za  pomocą 

semaforów  wprowadzić  możliwość  podawania  z  klawiatury  czasów 
przebywania  procesów  w  ich  sekcji  lokalnej  i  krytycznej,  należało  te 
procesy w jednoznaczny sposób zidentyfikować. 
Każdy proces w systemie Unix otrzymuje od niego numer nazywany 
numerem identyfikacyjnym procesu (PID). Daje to programiście 
możliwość kontroli nad rozwidlonymi procesami. Aby uruchomić 
funkcję proces() z parametrami czas_krytyczny i czas_lokalny należało 
najpierw (po rozwidleniu procesu macierzystego na liczbę procesów 
podanych przez użytkownika) gdzieś zapisać PID-y każdego z 
procesów. Posłużyła do tego pamięć dzielona. Przez ograniczenie liczby 
procesów do 10 otrzymano rozmiar pamięci dzielonej 10*sizeof(int) . 

background image

 

 

Numery  procesów  zostały  zapisane  w  kolejności  zgłoszenia  się.  Do 
zapewnienia  wzajemnego  wykluczania  procesów  chcących  zapisać 
swój PID do obszaru pamięci dzielonej należało zastosować tradycyjnie 
jeden semafor (w programie został on nazwany SEMAFOR_CZASY ). Do 
indeksacji  miejsca  wstawiania  PIDu  użyto  także  semafora 
(  SEMAFOR_INDEX  ).  (patrz  rys.  Funkcja  zapisz_pid(  )  )    Następnie 
każdemu  z  procesów  przyporządkowany  został  zestaw  danych 
składający  się  z  dwóch  liczb  zmiennopozycyjnych  odpowiadających 
kolejno  czasowi  przebywania  procesu  w  sekcji  krytycznej  i  w  sekcji 
lokalnej.  Mając  już  zestaw  potrzebnych  parametrów  uruchomiono 
funkcję 

proces(czas_kryt, 

czas_lok) 

(patrz 

rys. 

Funkcja 

proces(czas_kryt, czas_lok) ). 

background image

 

 

‘p

‘u’

START

Generowanie 

klucza

klucz=ftok()

Menu()

Zainicjuj_zestaw_semaforów_i_pamięć_dzieloną(rozmiar_m
agazynu,klucz)

Usuń_semafor_i_p

amięć_dzieloną()

Otwórz_zestaw_semaforów_i_

pamięć_dzieloną(klucz)

Otwórz_zestaw_semaforów_i_

pamięć_dzieloną(klucz)

Liczba 
procesów ?

Wczytaj tablicę 

czasów

Wybierz czasy kryt. i lokal. z tablicy 
odpowiadające indeksowi PID-u w 
pamięci dzielonej

Rozwidl (liczba_procesów)

Zapisz PID do pamięci 

dzielonej

Uruchom proces(czas_kryt, 

czas_lok)

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

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

‘i

EXIT

Wybierz czasy kryt. i lokal. z tablicy 
odpowiadające indeksowi PID-u w 
pamięci dzielonej

Zapisz PID do pamięci 

dzielonej

Uruchom proces(czas_kryt, 

czas_lok)

Rozwiązanie problemu wzajemnego 
wykluczania z tablicą czasów trwania sekcji 
za pomocą semaforów

 


Document Outline