PROCESY

autor: Ewa Łyszczek

1. STRUKTURY DANYCH I FUNKCJE DO OBSŁUGI PROCESÓW

2. ZMIANY STANÓW PROCESÓW


1. STRUKTURY DANYCH I FUNKCJE DO OBSŁUGI PROCESÓW


Deskryptor procesu - task_struct

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.


Przechowywanie deskryptora procesu - task_union

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];
};


Makro current

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.


Wydajne alokowanie obszarów pamięci dla task_union

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.


Znajdowanie deskryptora procesu na podstawie jego numeru PID

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.




2. ZMIANY STANÓW PROCESÓW


Stany procesów

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ł.

Dobry diagram przejść między stanami znajduje się tu.

Zasypianie

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)
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)
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 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


Budzenie

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.