1. STRUKTURY DANYCH I FUNKCJE DO OBSŁUGI PROCESÓW
Każdy proces jest opisany przez deskryptor procesu , który jest typu task_struct
.
(Struktura ta jest zdefiniowana w pliku /usr/src/linux/includ/linux/sched.h.)
Struktura ta ma m.in. następujące pola (tu tylko wymienione, a szczegółowo omówione przy poszczególnych
tematach oraz
tu ):
Stan procesu:
volatile long state;
Do szeregowania procesów:
long counter;
unsigned long policy;
/* SCHED_RR
SCHED_FIFO
SCHED_OTHER */
long nice;
unsigned long rt_priority;
volatile long need_resched;
int exit_code, exit_signal;
Identyfikatory procesu:
pid_t pid;
pid_t pgrp;
pid_t sssion;
int leader;
Identyfikatory użytkownika:
uid_t uid, euid, suid, fsuid;
gid_t gid, egid, sgid, fsgid;
struct user_struct *user;
Dowiązania do innych procesów:
struct task_struct *next_task, *prev_task;
struct task_struct *next_run, *prev_run;
struct task_struct *p_opptr, *p_pptr, *p_cptr, *p_ysptr, *p_osptr;
struct task_struct *pidhash_next, **pidhash_pprev;
wait_queue_had_t wait_childexit;
struct list_head run_list;
Pamięć:
struct mm_struct *mm;
int swappable : 1;
Pliki:
struct tty_struct *tty;
struct fs_struct *fs;
struct files_struct *files;
Uwaga: W Linuksie 2.4 nie
ma już globalnej tablicy task
przechowującej wskaźniki na deskryptory procesów aktualnie istniejących w systemie.
Dla każdego procesu jego deskryptor przechowywany jest wraz ze stosem trybu jądra tego procesu. Deskryptor i stos trzymane są w pojedynczym, dynamicznie alokowanym obszarze pamięci o wielkości 8 KB.
Deskryptor zaczyna się na początku tego obszaru pamięci, a stos na końcu i rośnie "w dół".
Zawartość tego obszaru pamięci jest zdefiniowana przez konstrukcję (z pliku sched.h):
union task_union {
struct task_struct task;
unsigned long stack[2048];
};
Wskaźnik stosu, określający położenie wierzchołka stosu, trzymany jest w rejestrze esp. W szczególności, kiedy pewien proces wykonuje się w trybie jądra, rejestr esp wskazuje na wierzchołek stosu trybu jądra tego właśnie procesu. Skoro deskryptor procesu i stos trybu jądra tego procesu są trzymane w 8 KB ( = 213 bitowym) obszarze pamięci, a deskryptor zaczyna się na początku tego obszaru, więc wystarczy zamaskować 13 najmniej znaczących bitów w rejestrze esp, by dostać wskaźnik na deskryptor aktualnie wykonywanego procesu (o ile proces znajduje się w trybie jądra).
Realizuje to makro current
, zawierające kilka tylko instrukcji asemblera
(plik usr/src/linux/include/asm-i386/current.h).
Użycie:
Makro current zazwyczaj pojawia się w kodzie jądra jako przedrostek pól deskryptora procesu. Np. current -> pid
zwraca pid aktualnie wykonującego się procesu.
Zalety:
Efektywność - wyznaczanie wskaźnika na deskryptor aktualnie wykonywanego procesu jest bardzo szybkie.
Nie trzeba przechowywać globalnej zmiennej current
czy (w systemach wieloprocesorowych) całej tablicy current
.
Wady:
Te 8 KB przechowywane jest na dwóch stronach pamięci, znajdujących się obok siebie, a pierwsza strona wyrównywana jest do wielokrotności 8 KB. Może to okazać się problemem, gdy jest niewiele dostępnej pamięci dynamicznej.
Gdy jakiś proces jest usuwany, a za chwilę tworzony jest inny, potrzeba zwolnić, a za moment zaalokować 8 KB pamięci na task_union
tych procesów. Aby unikać częstego wywoływania alokatora pamięci jądra, używana jest mała pamięć podręczna (ciekawy mechanizm). Pamięć podręczna składa się z EXTRA_TASK_STRUCT
obszarów pamięci (makro to ma zazwyczaj wartość 16).
Tablica task_stract_stack
zawiera wskaźniki do wolnych obszarów task_union
(czy - co na jedno wychodzi - deskryptorów procesów), znajdujących się w pamięci podręcznej.
Do obsługi tej pamięci podręcznej służą dwie funkcje (makra z pliku usr/src/linux/arch/i386/kernel/process.c). Nazwa task_stract_stack
pochodzi stąd, że funkcje te
wstawiają i pobierają wolne obszary z tej tablicy, jak ze stosu:
free_task_struct()
Jeśli pamięć podręczna nie jest pełna, to funkcja ta wstawia zwalniany 8 KB obszar pamięci task_union
do pamięci podręcznej (czyli wstawia do tablicy task_stack_struct
wskaźnik do tego obszaru). Jeśli pamięć podręczna jest pełna, to funkcja ta po prostu zwalnia obszar na task_union
.
alloc_task_struct()
Funkcja ta alokuje 8 KB obszary pamięci na task_union
, chyba że może je pobrać z pamięci podręcznej, bez wywoływania alokatora. A może je pobrać z pamięci podręcznej, gdy pamięć (tablica task_struct_stack
) jest przynajmniej w połowie pełna lub nie można znaleźć dwóch kolejnych wolnych stron pamięci na task_union
.
Czasem jądro potrzebuje pobrać wskaźnik na deskryptor procesu mając tylko numer PID procesu. (Jest to potrzebne np. w funkcji systemowej kill()
). Przeszukiwanie listy wszystkich procesów i sprawdzanie pól pid
w deskryptorach procesów byłoby nieefektywne.
Zastosowano rozwiązanie: tablica haszująca pidhash
. Zawiera ona PIDHASH_SZ
elementów, będących wskaźnikami do deskryptorów procesów. (Makro PIDHASH_SZ
ma zazwyczaj wartość 1024).
Funkcja haszująca (w praktyce makro) pid_hashfn()
tłumaczy podany PID na indeks tablicy pidhash
:
#define pid_hashfn (x) \
((((x) >> 8) ^ (x)) & (PIDHASH_SZ - 1))
Każda pozycja tablicy jest dwukierunkową listą deskryptorów procesów, dla których funkcja haszująca zwraca tę samą wartość. Listy te są zrealizowane za pomocą pól pidhash_next
i pidhash_pprev
w deskryptorze procesu.
Do obsługi tablicy haszującej służą funkcje:
hash_pid(struct task_struct *p)
Wstawia do tablicy pidhash
proces o deskryptorze *p.
unhash_pid(struct task_struct *p)
Usuwa z tablicy pidhash
proces o deskryptorze *p.
struct task_struct *find_task_by_pid(int pid)
Zwraca wskaźnik na deskryptor procesu o numerze PID równym pid
.
Pole state
w deskryptorze procesu opisuje stan procesu w danym momencie. Pole to może przyjmować jedną z następujących wartości (makra te są zdefiniowane w pliku sched.h) :
TASK_RUNNING Proces jest aktualnie wykonywany albo czeka na wykonanie. Innymi słowy jest w kolejce procesów gotowych do wykonania.
TASK_INTERRUPTIBLE Proces jest uśpiony w pewnej kolejce oczekiwania i czeka na zajście jakiegoś zdarzenia, np. na zwolnienie określonego zasobu. Proces może zostać obudzony (tzn. przywrócony do stanu TASK_RUNNING) np. przez przerwania sprzętowe lub nadejście sygnału.
TASK_UNINTERRUPTIBLE Jak stan TAK_INTERRUPTIBLE, z tym że proces nie reaguje na sygnały. Ten stan jest używany np. wtedy, gdy proces oczekuje na zajście jakiegoś warunku związanego ze sprzętem i nie powinien zostać przerwany przez sygnał.
TASK_ZOMBIE Proces zakończył się, ale jego deskryptor (task_struct
) nie został usunięty. Deskryptor zostanie usunięty dopiero po wywołaniu funkcji systemowej z rodziny wait()
przez rodzica procesu (lub przez proces init
, który "adoptuje" proces, jeśli jego rodzic się zakończył jeszcze przed zakończeniem danego procesu).
TASK_STOPPED Proces został wstrzymany. Proces może zostać wstrzymany po otrzymaniu sygnału SIGSTOP, SIGTSTP, SIGTTIN, SIGTTOU
. Ponadto kiedy proces jest monitorowany przez inny proces (np. debugger, który wywołuje funkcję systemową ptrace()
), to może
zostać przerwany przez dowolny sygnał.
Wywołanie którejś z poniższych funkcji powoduje zaśnięcie procesu w danej kolejce procesów oczekujących na zdarzenie (funkcje te są zdefiniowane w pliku usr/scr/linux/kernel/sched.c) :
void sleep_on(wait_queue_head_t *q)
Dwie ostatnie funkcje pozwalają na określenie czasu, po upływie którego proces zostanie zbudzony przez jądro, tzn. po prosu wstawiony do kolejki procesów gotowych i przywrócony do stanu
void interruptible_sleep_on(wait_queue_head_t *q)
long sleep_on_timeout(wait_queue_head_t *q, long timeout)
long interruptible_sleep_on_timeout(wait_queue_head_t *q, long timeout) TASK_RUN
.
Algorytm tych funkcji:
1: zmiana stanu bieżącego procesu na
TASK_UNINTERRUPTIBLE
{lub TASK_INTERRUPTIBLE dla interruptible_ }
2: zapamiętanie poziomu pracy procesora i zablokowanie przerwań, zablokowanie kolejki q do pisania
3: wstawienie procesu do kolejki q
4: odblokowanie kolejki q
5: wywołanie schedule()
{lub schedule_timeout() dla _timeout}
(tu funkcja schedule() usuwa proces z kolejki procesów gotowych)
Po obudzeniu procesu:
6: zablokowanie kolejki q do pisania
7: usunięcie procesu z kolejki q
8: odblokowanie kolejki q, przywrócenie początkowego poziomu pracy procesora
wake_up_proces
Poniższa funkcja budzi proces p ze stanu TASK_INTERRUPTIBLE
lub UNINTERRUPTIBLE
:
int wake_up_proces (struct task_struct *p)
Algorytm tej funkcji:
1: zapamiętanie poziomu pracy procesora i zablokowanie przerwań
2: zmiana stanu procesu p na TASK_RUNNING
3: jeśli procesu p nie ma w kolejce procesów gotowych, to:
dodanie procesu p do kolejki procesów gotowych,
i jeśli obecnie wykonywanym procesem jest idle
to wywołanie funkcji ustawiającej need_resched
4: przywrócenie początkowego poziomu pracy procesora
Uwaga: funkcja wake_up_proces
nie usuwa procesu z kolejki procesów oczekujących. Gdy proces dostanie procesor zrobi to funkcja sleep_on
, w której proces zasnął.
wake_up
Poniższe funkcje budzą z kolejki q
procesy znajdujące się w stanie TASK_INTERRUPTIBLE
lub UNINTERRUPTIBLE
:
void wake_up(wait_queue_head_t *q)
Budzi tylko jeden proces.
void wake_up_nr(wait_queue_head_t *q, int nr)
Budzi nr
procesów.
void wake_up_all(wait_queue_head_t *q)
Budzi wszystkie procesy z kolejki q
.
Analogiczne wake_up_interruptible
, wake_up_interruptible_nr
i wake_up_interruptible_all
budzą tylko procesy znajdujące się w stanie TASK_INTERRUPTIBLE
.
Algorytm powyższych funkcji:
1: zapamiętanie poziomu pracy procesora i zablokowanie przerwań,
zablokowanie kolejki q do czytania
2: wywołanie wake_up_process dla odpowiedniej ilości procesów z kolejki q
3: odblokowanie kolejki q, przywrócenie początkowego poziomu pracy procesora
Uwaga: Tu przedstawione są tylko algorytmy najwaniejszych funkcji, a nie rzeczywista implementacja i sposoby wywoływania poszczególnych funkcji pomocniczych.
Uwaga: W Linuksie 2.2 nie było możliwości budzenia tylko jednego procesu z danej kolejki. Obecne rozwiązanie znacznie poprawia wydajność semaforów.