Naszą opowieść o kolejkach procesów w Linuksie warto rozpocząć od omówienia pliku list.h. Zawiera on definicję kolejki oraz implementację podstawowych operacji na niej. Struktura kolejki jest bardzo ogólna i umożliwia używanie jej w bardzo wielu sytuacjach. Praktycznsie wszędzie, gdzie w Linuksie 2.4.7 potrzebne jest szeregowanie czy grupowanie obiektów wykorzystywane są właśnie te kolejki, potocznie zwane strukturą list.h.
Kolejki są implementowane jako dwukierunkowe listy cykliczne. Węzeł na takiej liście zawiera wskaźniki do następnego i poprzedniego elementu listy:
struct list_head {
struct list_head *next, *prev;
};
Za pustą uznaje się listę składającą się z jednego elementu (atrapy, wskaźnika na listę), dla którego następnik (a także poprzednik) wskazują na niego samego.
Mamy do dyspozycji makra (których można używać w różnych miejscach kodu) inicjujące pustą kolejkę - to znaczy taką, w której obydwa wskaźniki wskazują jej jedyny element.
Najbardziej przydatne jest makro
LIST_HEAD(nazwa)
które tworzy strukturę typu list_head
o podanej nazwie i inicjuje ją w podany wyżej sposób tworząc pustą kolejkę
oraz makro:
INIT_LIST_HEAD(wskaznik_na_head_list)
które dla danego wskaźnika do list_head
tworzy z niego wskaźnik na pustą listę.
Funkcje wewnętrzne zaczynają się od dwóch podkreśleń i zakładają rzeczywistą znajomość wskaźników do następnego i poprzedniego elementu w stosunku do tego, którym manipulujemy.
void __list_add(struct list_head *nowy, poprzedni, następny)
void __list_del(struct list_head * poprzedni, następny)
Powyższe funkcje wykonują oczywiste manipulacje na wskaźnikach. Są to praktycznie jedyne operacje na kolejkach, które jawnie zmieniają wskaźniki ze struktury list_head
. Oczywiście są od tej zasady wyjątki, o których wspomnimy później.
Przy pomocy funkcji wewnętrznych definiowane są podstawowe operacje na kolejce dostępne dla innych części jądra:
Dodawanie nowego elementu na początku kolejki:
void list_add(struct list_head *nowy_element, lista)
Dodawanie nowego elementu na koniec kolejki:
void list_add_tail(struct list_head *nowy_element, lista)
Usuwanie elementu z kolejki w której jest umieszczony:
void list_del(struct list_head *element)
Uwaga: Po usunięciu elementu z kolejki przy pomocy tej funkcji nie staje się on pustą kolejką, lecz jest w stanie nieokreślonym. Aby był pustą kolejką, należy użyć:
void list_del_init(struct list_head *element)
Sprawdzenie, czy kolejka jest pusta:
int list_empty(struct list_head *lista)
Dodanie całej dodatkowej kolejki na początek starej kolejki:
void list_splice(struct list_head *dodatkowa_lista, *stara_lista)
Bardzo przydatne jest też makro - iterator po wszystkich elementach kolejki:
list_for_each(element, początek_listy) {rób coś z elementem}
Wykonanie tego makra polega na przejrzeniu całej kolejki o podanym początku. Pętla kończy się, gdy przejrzeliśmy wszystkie elementy.
list_entry
Skoro jednak istnieje osobna struktura kolejki, to czy możemy dostać się do samego obiektu, który stoi w kolejce? Przecież "wędrując" po kolejce napotykamy tylko elementy typu head_list
, które same w sobie nic nie znaczą...
Jednak kluczowym jest spostrzeżenie, że skoro list_head
jest polem w jakiejś strukturze, to możemy łatwo wyśledzić, gdzie w pamięci mieści się owa struktura, jeśli tylko mamy o niej parę informacji. Język C umożliwia takie manipulacje, dlatego też dostępne jest makro (jego treść jest oparta na licznych rzutowaniach):
list_entry(wskaznik_na_liste, typ, pole)
Zwraca ono obiekt typu typ
, którego pole
o podanej nazwie jest typu list_head i zawiera element listy wskazywany przez wskaźnik_na_listę
.
Przykład użycia:
struct list_head *jakas_lista;
proces = list_entry(jakas_lista, struct task_struct, run_list);
Wcześniej, jeśli jakiś obiekt wymagał szeregowania, to jego struktura zawierała wskaźniki na poprzedni i następny obiekt tego samego typu. Operacje na liście wymagały szeregu zmian wartości wskaźników. Treści procedur np. dodających do listy powtarzały się, ale ich ujednolicenie było trudne ze względu na odmienną budowę struktur różnych obiektów.
Teraz taki obiekt ma pole typu list_head i przy użyciu prostych procedur z list.h może dokonywać operacji na listach. Skraca to kod i czyni go mniej podatnym na błędy, jednak najważniejsze jest chyba, że lista jest całkiem niezależna od obiektów które zawiera.
Taka implementacja kolejek jest bardzo szeroko używana, między innymi:
Jeszcze raz podkreślimy różnice w stosunku do poprzedniej wersji: