Lock-free: stos
Ostatnio bawię się w rozwijanie własnego systemu operacyjnego. Wspaniała sprawa jeśli ktoś myśli, że wszystko wie ;)
W trakcie wyszło na jaw, że muszę zaimplementować synchronizowany zbiór (dokładniej rzecz biorąc - nieużywanych stron). Ale czekanie na mutexie nie wydaje się optymalną strategią. Poszukałem więc algorytmów lock-free. Na początek przyszedł stos.
Operacje
Podstawowy zdejmowania jest taki:
loop
saved_head = head
if saved_head == null
return null
end
if cmp_and_swap(head, saved_head, saved_head.next)
return saved_head.data
end
end
A wkładania taki:
node = malloc();
do
saved_head = head
node.next = saved_head
while(!cmp_and_swap(head, saved_head, node))
Implementacja w assemblerze x86
Assembler x86 ma przydatną instrukcję. Dostajemy nie tyle wiadomość czy operacja się udała tylko ustawioną flagę zero jeśli się udała oraz nową wartość w eax.
W poniższym kodzie zakładam, że wskaźnik na następny element jest pierwszym polem węzła.
- ; edx - adres wskaźnika na pierwszy element
- mov eax, [edx]
- spin:
- test eax, eax
- jz end ; Jeśli stos pusty kończymy
- mov ecx, [eax] ; Pobieramy nowy początek
- lock ; Atomicznie
- cmpxchg [edx], ecx ; Jeśli się nie zmieniło to zamieniamy
- jnz spin ; A jeśli się zmieniło to do w eax jest nowy początek
- end:
Na koniec mamy w akumulatorze zdjęty właśnie węzeł.
- ; ecx - węzeł, który wkładamy
- ; edx - adres wskaźnika na pierwszy element
- mov eax, [edx]
- spin:
- mov [ecx], eax
- lock
- cmpxchg [edx], ecx
- jnz spin
Implementacja w C
Implementacja w C zamiast w inline assembler ma mało zalet. IMHO kod jest mniej czytelny a i tak jest mniej przenośny. Na dodatek gcc nie potrafi kodu dobrze zoptymalizować (nie wie o zmienionych flagach etc.).
- struct stack_node {
- struct stack_node *next;
- void *data;
- };
- struct stack {
- volatile struct stack_node *head;
- };
- struct stack_node *
- pop(struct stack *stack)
- {
- struct stack_node *head;
- do
- {
- head = stack->head;
- if (head == NULL)
- break;
- }
- while(!cmp_and_swap(&stack->head, head, head->next));
- return head;
- }
- void
- push (struct stack *stack, struct stack_node *head)
- {
- do
- {
- head->next = stack->head;
- }
- while(!cmp_and_swap(&stack->head, head->next, head));
- }
Na co trzeba zwrócić uwagę
Jeśli nie mamy GC to jest problem co zrobić z wskaźnikiem. Nie można go po prostu zwolnić bo nie wiemy czy właśnie ktoś go nie próbuje użyć. Pozostawienie go 'bez opieki' to wyciek pamięci. Rozwiązanie które znalazłem jest proste - należy mieć osobny stos (no dobrze - nie musi być stos ale to już mamy zaimplementowane) na którym odkładamy wolne węzły.
Źródła
Wzorowałem się na fragmentach kodu (a właściwie na postach o tych fragmentach) z OmniThreadLibrary - bibliotece na licencji BSD.
Na potrzeby projektu stworzyłem nagłówek. Podobne rozwiązanie znalazłem w którymś kernelu *BSD:
- #define STACK(name, data...) \
- struct name ##_node \
- { \
- struct name ## _node *next; \
- data; \
- }; \
- struct name \
- { \
- volatile struct name ## _node *head; \
- };
- #define STACK_POP(stack, node) \
- __asm__ __volatile__ ("\n" \
- "1:\n" \
- "\ttest %%eax, %%eax\n" \
- "\tjz 1f\n" \
- "\tmov (%%eax), %%ecx\n" \
- "\tlock cmpxchg %%ecx, %0\n" \
- "\tjnz 1b\n" \
- "1:" \
- : "=m"(stack.head), "=a" (node) \
- : "m"(stack.head), "a" (stack.head) \
- : "%ecx" ); \
- #define STACK_PUSH(stack, node) \
- __asm__ __volatile__ ("\n" \
- "1:\n" \
- "\tmov %%eax, (%%ebx)\n" \
- "\tlock cmpxchg %%ebx, %0\n" \
- "\tjnz 1b\t" \
- : "=m"(stack.head), "=m"(node->next) \
- : "m" (stack.head), "b"(node), \
- "a" (stack.head)); \
PS. Będę szukał informacji na temat innych struktur LF. Czy jest zainteresowanie?
PPS. Spróbuje upchnąć wpis na techblogu. Może się uda...
Otagowano: lock-free,
Komentarze do wpisu
Możesz śledzić odpowiedzi poprzez kanał RSS. Możesz dodać komentarz lub zostawić ślad (trackback) ze swojego bloga.
Avilar
Fajna sprawa, jakbyś w przystępny sposób opisał jak stworzyć kawałek kodu który się sam zabootuje byłoby super. Pamiętam że gdzieś coś takiego czytałem ale nie bardzo pamiętam gdzie. Poza tam bardzo mało osób pisze tu o systemach od tej strony.
Tak trzymać.
27 grudnia 2008, 22:41:58
Paweł Dziepak
@Avilar: Pewnie chodziło Ci o ten tekst: http://binboy.sphere.pl/index.php?show=43
@Użytkownik: Pierwszy fragment w assemblerze x86 -> zgubiłeś ‘p’ w cmpxchg.
Osobiście jeszcze nie zajmowałem się zagadnieniami związanymi z synchronizacją, ale muszę przyznać, że zaimponowały mi możliwości jakie skrywa cmpxchg. Słyszałem, że ta instrukcja jest często wykorzystywana do takich zadań jednak nie pomyślałem, że cały mechanizm jest aż taki piękny w swej prostocie :P
No i prawie sakramentalne pytanie jeżeli chodzi o każdy amatorski kernel: monolit czy mikrojądro?
27 grudnia 2008, 23:30:15
Uzytkownik
@Avilar: Mogę napisać cosik o NIOS. Ale sam się uczę. Na razie próbuje rozwiązać inicjalizację podsystemu pamięci (pierwszy podsystem).
Jeśli chodzi o większą ilość informacji to jest OSDev i kilka innych źródeł. Ale zazwyczaj są albo niekompletne (jak zrobić tak by się bootowały) albo strasznie oschłe i nieprzystępne.
@Pawel Dziepak: Kod zajmuje prawieże mniej niż w C i jest w dodatku.. czystszy.
Jądro z założenia ma być mikrojądrem. Dlatego 2 pierwszymi podystemami mają być pamięć i wielozadaniowość (scheduling i IPC). A postaram się, żeby np. video było modułem w userspace ładowanym przez gruba.
27 grudnia 2008, 23:53:10
Paweł Dziepak
Tu już nawet nie chodzi o to czy implementacja w C czy w assemblerze x86 (czy jakimkolwiek innym), bo niezależnie od tego i tak instrukcja cmpxchg zostanie użyta.
Inna sprawa, że pewne niskopoziomowe rzeczy lepiej jest zrobić w assemblerze. Sam niedawno robiłem takie porządki w moim kodzie, szczególnie w miejscach gdzie wykorzystałem wcześniej inline assembler.
Z tego co mówisz to będzie to takie bardziej „prawdziwe” mikrojądro czyli coś w stylu L4.
Jeszcze co do serwerów/modułów, wszystkie chcesz ładować przez gruba, czy tylko te które będą potrzebne kernelowi do samodzielnego załadowania kolejnych?
28 grudnia 2008, 00:05:05
Uzytkownik
Hmm. Z implementacji w C niewiele da się wykroić poza zwykłe C&S. Generalnie mam niechęć do asm ale tutaj pokazał swoje kły.
Na razie wszystko przez GRUBa jako biblioteki ELF czy binarki ELF czy cokolwiek bądź. Potem (a właściwie jeśli ;), po zaimplementowaniu obsługi dysku i FS to ładowanie będzie z dysku/sieci/cokolwiek podsystem VFS wymyśli.
Pytanie tylko czy skorzystać z implementacji ELF czy odpowiednika w sposób bezpośredni czy bardziej potraktować to jako biblioteki (jądro wywołuje odpowiednie funkcje działające w RINGu 3 w swojej przestrzeni adresowej).
28 grudnia 2008, 00:21:06
Uzytkownik
PS. Na sensowniejsze wypowiedzi trzeba poczekać do jutra. Dobranoc ;)
28 grudnia 2008, 00:21:40
Dodaj komentarz