13. ZARZĄDZANIE PROCESAMI W SYSTEMIE OPERACYJNYM UNIX
Z punktu widzenia organizacji systemu Unix procesy są obiektami logicznymi
posiadającymi swoje
atrybuty (zapisane w odpowiednich systemowych strukturach danych), na
których można wykonywać
operacje przy użyciu odpowiednich funkcji systemowych.
Najważniejsze atrybuty procesów:
- identyfikator procesu PID
(Process ID)
- identyfikator procesu rodzicielskiego PPID
(Parent Process ID)
- rzeczywisty identyfikator użytkownika UID
(User ID)
- efektywny identyfikator użytkownika EUID
(Effective User ID)
- rzeczywisty identyfikator grupy użytkowników GID
(Group ID)
- efektywny identyfikator grupy użytkowników EGID
(Effective Group ID)
- identyfikator grupy procesów PGRP
(Process Group)
Proces może mieć swój terminal sterujący, z którym może komunikować się,
pobierając z niego
dane (na przykład polecenia użytkownika) i wyświetlając wyniki. Jednym z
atrybutów procesu jest
wskaźnik do struktury terminala - systemowej struktury danych związanej z
danym terminalem.
Struktura terminala zawiera między innymi bieżący identyfikator grupy
terminala
(Current
Terminal Process Group)
- nie ma ustalonego skrótu, na potrzeby wykładu
przyjmijmy CTPGRP.
Jeśli proces nie ma swojego terminala sterującego, jego wskaźnik ma wartość
NULL.
W większości przypadków procesy użytkowników mają swój terminal sterujący
(ten, z którego
zostały uruchomione), natomiast procesy systemowe nie mają swojego
terminala (wykonują one
swoją pracę przez cały czas, niezależnie od tego, czy nadzorca systemu jest
aktualnie zalogowany).
Procesy systemowe, mające za zadanie dostarczanie określonych usług na rzecz
innych procesów,
nazywane są często serwerami tych usług lub demonami
(daemon)
.
Zwykły użytkownik systemu również ma możliwość uruchamiania procesów
„pozbawionych
terminala sterującego”, które mogą być wykonywane nawet po zakończeniu
przez użytkownika sesji
pracy i wyłączeniu terminala (na przykład mogą wykonywać przez całą noc
skomplikowane
obliczenia i zapisywać wyniki do pliku).
Proces interpretatora komend, który zostaje uruchomiony w wyniku zalogowania
się do systemu,
nazywany jest shellem zgłoszonym. Na początku pracy jest on procesem
sterującym terminala,
z którego nastąpiło zalogowanie, czyli przywódcą grupy terminala (zachodzi dla
niego
PID = PGRP = CTPGRP).
Proces nazywamy pierwszoplanowym
(foreground process)
, jeżeli jego PGRP jest
równy CTPGRP
wskazywanego przez niego terminala. W przeciwnym przypadku proces nazywamy
drugoplanowym,
lub wykonującym się w tle
(background process)
. Procesy pierwszoplanowe mogą
odczytywać znaki
z terminala i zapisywać znaki do terminala (to jest pliku specjalnego
odpowiadającego terminalowi).
Procesy drugoplanowe nie mogą odczytywać znaków z terminala (są zawieszane
przy próbie czytania),
natomiast zazwyczaj mogą zapisywać znaki do terminala (jest to zależne od
systemu i aktualnych
ustawień w strukturze terminala).
Uwaga.
Zazwyczaj w każdej chwili istnieje dokładnie jeden proces pierwszoplanowy
terminala.
Pojedynczą komendę lub pojedynczy potok komend uruchomiony z linii poleceń
shella nazywamy
pracą
(job)
. Shell ma własność sterowania pracami
(job control)
, jeżeli posiada
możliwość zmiany
CTPGRP terminala, z którego został uruchomiony.
Polecenia:
komenda - uruchamia proces pierwszoplanowy;
komenda & - uruchamia proces drugoplanowy
(komenda &) - uruchamia proces drugoplanowy korzystając z
podshella
Ctrl-Z - zawiesza proces pierwszoplanowy
Ctrl-Y - zawiesza proces pierwszoplanowy w chwili
rozpoczęcia czytania
bg praca (lub praca &) - uruchamia zawieszony proces w tle
fg praca (lub praca ) - uruchamia zawieszony proces na pierwszym planie
kill -9 PID - powoduje zakończenie procesu (działającego lub
zawieszonego)
Uwaga. Można uruchomić kilka procesów jednocześnie z jednej linii poleceń
(pisząc na przykład
komenda_1 & komenda_2 & ).
Do uzyskiwania informacji o aktualnie wykonywanych procesach (numerach,
właścicielach, stanach
procesów itp.) służy polecenie zewnętrzne ps. W zależności od podanych opcji
może wyświetlać
jedynie informacje o aktywnych procesach lub o wszystkich (też zawieszonych),
informacje jedynie
o procesach użytkownika wydającego polecenie, lub o wszystkich procesach w
systemie (ta ostatnia
opcja zwykle nie jest udostępniana dla użytkowników nieuprzywilejowanych).
Uwaga
Użytkownik może badać stan swoich procesów związanych z bieżącą sesją przy
użyciu interpretera
komend bieżącej sesji (w takim przypadku proces interpretera musi być
pierwszoplanowy, a wszystkie
inne procesy drugoplanowe lub zawieszone), lub też otwierając nową sesję (i
uruchamiając w ten
sposób nowy proces interpretera) i z niej sprawdzając stan procesów pierwszej
sesji. Jest to możliwe,
bo użytkownik jest właścicielem wszystkich procesów zarówno pierwszej, jak i
drugiej sesji.
Drugi sposób jest lepszy, jeśli z jakichś powodów nie jest wskazane czasowe
przerywanie pracy
procesu pierwszoplanowego pierwszej sesji.
Pełny diagram stanów procesu (według [ M. Bach ] ):
Wykonywany w trybie użytkownika
wywołanie
funkcji systemowej, powrót do trybu
przerwanie przerwanie powrót
użytkownika
i powrót wywłaszczenie
Wykonywany w trybie jądra
wyjście
Wywłaszczony
Zombie ponowne
zasypia uszeregowanie
Gotowy do wykonania w
pamięci
Uśpiony budzi się
w pamięci dość pamięci
usunięcie usunięcie sprowadzenie
Utworzony
z pamięci z pamięci do pamięci
fork
Uśpiony budzi się za mało pamięci
poza pamięcią Gotowy do wykonania
poza pamięcią
Uwaga. Powyższy diagram nie uwzględnia mechanizmu stronicowania pamięci.
Jądro utrzymuje globalną strukturę nazywaną tablicą procesów, której zajęte
pozycje odpowiadają
identyfikatorom procesów aktualnie istniejących w systemie (typowa wielkość
tablicy procesów -
32768 pozycje). Każda zajęta pozycja zawiera między innymi:
- pole stanu procesu;
- identyfikator właściciela procesu (UID);
- wskaźnik do tablicy segmentów procesu;
- wskaźnik do u-obszaru
(u-area, user area)
procesu. Tablica
segmentów
U-obszar
Tablica segmentów
procesu
Tablica procesów Pamięć
operacyjna
Tablica procesów jest strukturą globalną, do której dostęp ma jedynie jądro
systemu. Z każdym
wykonywanym procesem związany jest jego u-obszar, do którego dostęp ma
zarówno jądro, jak
i sam proces (gdy wykonywany jest w trybie jądra). U-obszar zawiera między
innymi:
- wskaźnik do odpowiadającej mu pozycji w tablicy procesów;
- deskryptory plików otwartych przez dany proces;
- nazwę bieżącego katalogu;
- argumenty aktualnego wywołania funkcji systemowej, zwracane wartości i kod
błędu.
Z każdym wykonywanym procesem związana jest też tablica segmentów
procesu, która zawiera
opisy segmentów przydzielonych procesowi (kodu, danych, stosu, pamięci
wspólnej) wraz ze
wskaźnikami do odpowiednich pozycji w globalnej tablicy segmentów. Tablica
segmentów,
utrzymywana przez jądro segmentów, zawiera z kolei deskryptory wszystkich
istniejących segmentów
w pamięci operacyjnej. Taki dwustopniowy system adresowania segmentów w
pamięci umożliwia
współdzielenie segmentów przez wiele procesów, przy jednoczesnym zachowaniu
możliwości ich
ochrony (na przykład jeden proces może mieć prawo do zapisu w danym
segmencie, a pozostałe tylko
do odczytu).
Każdy proces, poza procesem rozpoczynającym pracę systemu, powstaje w wyniku
utworzenia przez
inny proces. Proces uruchamiający jądro systemu jest tworzony „bezpośrednio” i
otrzymuje numer 0.
Jest on procesem wykonywanym w trybie jądra. Po uruchomieniu systemu
przyjmuje on rolę procesu
wymiany. Tworzy on proces init o numerze 1, który jest procesem wykonywanym
w trybie użytkow-
nika, i który zajmuje się dalej tworzeniem procesów potomnych. Init jest zatem
przodkiem wszystkich
procesów w systemie poza procesem 0.
Każdy proces ma prawo tworzyć swoje procesy potomne (ich liczba jest
ograniczona pojemnością
tablicy procesów oraz, ewentualnie, maksymalną liczbą procesów należących do
jednego użytkownika).
W klasycznych systemach uniksowych proces potomny powstawał zawsze wskutek
wywołania przez
proces rodzicielski funkcji systemowej fork (obecnie takich funkcji jest więcej, co
jest między innymi
związane z implementacją wątków, czyli procesów lekkich). Utworzony proces
potomny jest wpisywany
do tablicy procesów na kolejnej wolnej pozycji (modulo rozmiar tablicy), której
numer staje się jego
identyfikatorem. Każdy proces „pamięta” identyfikator swojego procesu
rodzicielskiego, natomiast
spośród identyfikatorów procesów potomnych pamięta jedynie ostatni (najpóżniej
przydzielony).
Każdy nowo utworzony proces dziedziczy identyfikator grupy procesów (PGRP)
po swoim procesie
rodzicielskim. Może w tej grupie pozostać lub wywołać funkcję systemową
setpgrp, która powoduje
zmianę jego dotychczasowego PGRP na wartość jego identyfikatora (PID).
Proces, który wywołał
setpgrp, staje się przywódcą grupy procesów.
Proces potomny dziedziczy po swoim procesie rodzicielskim większość jego
atrybutów - UID, GID,
deskryptory przydzielonych zasobów systemowych (w szczególności deskryptory
otwartych plików)
i zbiór zmiennych środowiska. Dziedziczy również segment kodu programu oraz
otrzymuje kopie
segmentu danych i stosu. Ponieważ zazwyczaj celem utworzenia nowego
procesu jest powierzenie
mu wykonywania innego programu, niż wykonuje jego proces rodzicielski,
proces potomny powinien
wywołać funkcję systemową exec, której argumentem jest nazwa pliku z innym
programem.
Proces potomny, kończąc działanie (wywołując funkcję systemową exit) przesyła
informację o tym
do swojego procesu rodzicielskiego, przekazując mu jednocześnie swój kod
zakończenia
(exit code)
,
będący niedużą liczbą całkowitą (0 w przypadku traktowanym jako pomyślny).
Jeśli proces rodziciel-
ski zakończył działanie przed zakończeniem wszystkich jego procesów
potomnych, automatycznie
„przybranym rodzicem” dla niezakończonych procesów potomnych staje się
proces init.
Priorytet procesu, decydujący o częstości wybierania go do wykonywania przez
procesor, zależny jest
od kilku wielkości, między innymi od współczynnika nice (nazywanego też
poziomem uprzejmości
procesu) umieszczonego w odpowiadającej danemu procesowi pozycji tablicy
procesów.
Współczynnik ten może mieć wartość z zakresu 0 ... 39 , przy czym niższa
wartość współczynnika
oznacza podwyższenie priorytetu.
Standardowa wartość nice wynosi 20 - wartość tę dziedziczą między innymi
wszystkie shelle
zgłoszone pośrednio od procesu init (przez domniemanie każdy proces dziedziczy
wartość nice po
swoim procesie rodzicielskim). Użytkownicy nieuprzywilejowani mogą
uruchamiać procesy ze
współczynnikiem nice równym 20 lub więcej (do jego zwiększania służy polecenie
systemowe nice).
Tylko nadzorca systemu może uruchamiać procesy z dowolnym współczynnikiem
nice.
Uwaga
W dawniejszych systemach uniksowych wartość nice mogła być zmieniana
jedynie przez proces,
którego ta wartość dotyczyła i nawet nadzorca systemu nie mógł wpływać na
priorytety procesów
użytkowników. Obecnie możliwość taka istnieje (polecenie renice).
Procesy mogą korzystać z pomiaru czasu prowadzonego przez system i ustalać
pory wykonywania
różnych czynności (na przykład tworzenia nowych procesów, wysyłania
komunikatów itd.) poprzez
zapisywanie ich w tablicach crontab (związanych z poszczególnymi
użytkownikami systemu).
Tablice te są sukcesywnie przeglądane przez proces demona zegarowego cron,
który w podanych
porach inicjuje wykonywanie podanych komend.
Do operowania na zawartości tablicy crontab służy polecenie o takiej samej
nazwie. Innymi
poleceniami związanymi z pomiarem czasu i wykonywaniem pewnych czynności
w ustalonych
porach są polecenia at oraz leave.
Polecenia związane z pomiarem czasu ogólnie są uważane za polecenia
niebezpieczne z punktu
widzenia systemu operacyjnego (nie są starannie opracowane pod kątem
zabezpieczenia przed
włamaniami). W systemach uniksowych są zazwyczaj przechowywane listy
użytkowników (ustalane
przez administratora), którzy albo a) mają prawo do wykonywania konkretnych
poleceń związanych
z pomiarem czasu, albo b) mają zakaz wykonywania konkretnych poleceń.