Próbowałem poskładać w całość sposób, w jaki pamięć stosu jest przekazywana wątkom. Nie udało mi się poskładać wszystkiego w całość. Próbowałem przejść do kodu, ale jestem bardziej zdezorientowany, więc proszę o pomoc.

Jakiś czas temu zadałem to pytanie. Załóżmy więc, że ten konkretny program (dlatego wszystkie wątki znajdują się w tym samym procesie). Jeśli napiszę printfs dla każdego początku wskaźnika stosu, a następnie ile jest dla nich przydzielone, otrzymam takie rzeczy jak tabela na końcu tej wiadomości, gdzie pierwsza kolumna to time_t usec , druga nie ma znaczenia, trzecia to numer wątku, czwarta to rozmiar strażnika, potem początek stosu, koniec stosu (posortowane według początku stosu), przedostatni to przydzielony stos (8 MB domyślnie), a ostatnia kolumna jest różnicą między końcem pierwszego przydzielonego stosu a początkiem następnego stosu.

Oznacza to, że (myślę), że jeśli 0, to stosy są ciągłe, jeśli są dodatnie, ponieważ stos zmniejsza się w pamięci, oznacza to, że jest „wolna przestrzeń” o tylu Mb między numerem a następnym (w pamięć). Jeśli jest ujemna, oznacza to, że pamięć jest ponownie używana. Może to więc oznaczać, że ta przestrzeń stosu została zwolniona przed utworzeniem tego wątku.

Mój problem polega na tym: czym dokładnie jest algorytm, który przypisuje przestrzeń stosu do wątków (na wyższym poziomie niż kod) i dlaczego czasami otrzymuję ciągłe stosy, a czasami nie, a czasami otrzymuję wartości takie jak 7,94140625 i 0,0625 w ostatniej kolumnie?

To wszystko jest Linux 2.6, C i pthreads.

To może być pytanie, które będziemy musieli powtarzać, aby je rozwiązać, i za to przepraszam, ale mówię ci to, co wiem teraz. Zapraszam do proszenia o wyjaśnienia.

Dzięki za to. Poniższa tabela.

52815   14  14786   4096    92549120    100941824   8392704 0
52481   14  14784   4096    100941824   109334528   8392704 0
51700   14  14777   4096    109334528   117727232   8392704 0
70747   14  14806   4096    117727232   126119936   8392704 8.00390625
75813   14  14824   4096    117727232   126119936   8392704 0
51464   14  14776   4096    126119936   134512640   8392704 8.00390625
76679   14  14833   4096    126119936   134512640   8392704 -4.51953125
53799   14  14791   4096    139251712   147644416   8392704 -4.90234375
52708   14  14785   4096    152784896   161177600   8392704 0
50912   14  14773   4096    161177600   169570304   8392704 0
51617   14  14775   4096    169570304   177963008   8392704 0
70028   14  14793   4096    177963008   186355712   8392704 0
51048   14  14774   4096    186355712   194748416   8392704 0
50596   14  14771   4096    194748416   203141120   8392704 8.00390625
3
Dervin Thunk 21 lipiec 2011, 04:12
Z ciekawości, dlaczego cię to obchodzi?
 – 
Nemo
21 lipiec 2011, 04:20
Zauważ, że niektóre z twoich wątków wydają się działać na tym samym stosie. Czy to ma sens?
 – 
BjoernD
21 lipiec 2011, 04:35
1
@BjoernD, jasne, o ile jeden zakończy się, zanim drugi zacznie działać
 – 
bdonlan
21 lipiec 2011, 04:36
@Nemo. Krótko mówiąc, prowadzę badania nad przetwarzaniem równoległym. Badałem kod Fibonacciego, z którym połączyłem się w tym pytaniu. To dało mi SIGSEV i pomyślałem (wciąż myślę), że to z powodu stosu. Nagle zdałem sobie sprawę, że nie wiem, jak działa stos w Linuksie, i musiałem wiedzieć... no wiesz... naukowcy, ale im więcej się w to zagłębiałem, tym mniej rozumiałem. Otóż ​​to.
 – 
Dervin Thunk
21 lipiec 2011, 05:51
@BjoernD: bdonlan ma rację. Zakładam, że adres został już zwolniony, gdy wątek powrócił.
 – 
Dervin Thunk
21 lipiec 2011, 05:52

2 odpowiedzi

Najlepsza odpowiedź

Po pierwsze, straciwszy prosty program testowy, który uruchamia pojedynczy wątek, możemy zobaczyć wywołania systemowe, których użył do utworzenia nowego wątku. Oto prosty program testowy:

#include <pthread.h>
#include <stdio.h>

void *test(void *x) { }

int main() {
        pthread_t thr;
        printf("start\n");
        pthread_create(&thr, NULL, test, NULL);
        pthread_join(thr, NULL);
        printf("end\n");
        return 0;
}

I odpowiednia część jego wyjścia strace:

write(1, "start\n", 6start
)                  = 6
mmap2(NULL, 8392704, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_STACK, -1, 0) = 0xf6e32000
brk(0)                                  = 0x8915000
brk(0x8936000)                          = 0x8936000
mprotect(0xf6e32000, 4096, PROT_NONE)   = 0
clone(child_stack=0xf7632494, flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID, parent_tidptr=0xf7632bd8, {entry_number:12, base_addr:0xf7632b70, limit:1048575, seg_32bit:1, contents:0, read_exec_only:0, limit_in_pages:1, seg_not_present:0, useable:1}, child_tidptr=0xf7632bd8) = 9181
futex(0xf7632bd8, FUTEX_WAIT, 9181, NULL) = -1 EAGAIN (Resource temporarily unavailable)
write(1, "end\n", 4end
)                    = 4
exit_group(0)                           = ?

Widzimy, że uzyskuje stos z mmap z ochroną PROT_READ|PROT_WRITE i flagami MAP_PRIVATE|MAP_ANONYMOUS|MAP_STACK. Następnie chroni pierwszą (tj. najniższą) stronę stosu, aby wykryć przepełnienia stosu. Pozostałe rozmowy nie są tak naprawdę istotne dla prowadzonej dyskusji.

Jak więc mmap alokuje stos? Cóż, zacznijmy od mmap_pgoff w jądro Linuksa; punkt wejścia dla nowoczesnego wywołania systemowego mmap2. Deleguje do do_mmap_pgoff po zrobieniu kilku zamki. To następnie wywołuje get_unmapped_area, aby znaleźć odpowiedni zakres stron niezmapowanych.

Niestety, to następnie wywołuje wskaźnik funkcji zdefiniowany w vma - prawdopodobnie dzieje się tak, że procesy 32-bitowe i 64-bitowe mogą mieć różne pomysły na to, które adresy mogą być mapowane. W przypadku x86 jest to zdefiniowane w arch_pick_mmap_layout, który przełącza się w zależności od tego, czy w tym procesie jest używana architektura 32-bitowa, czy 64-bitowa.

Spójrzmy więc na implementację arch_get_unmapped_area w takim razie. Najpierw otrzymuje kilka rozsądnych wartości domyślnych wyszukiwania z find_start_end, a następnie testuje, czy przekazana wskazówka adresu jest prawidłowa (w przypadku stosów wątków żadna wskazówka nie jest przekazywana). Następnie rozpoczyna skanowanie wirtualnej mapy pamięci, zaczynając od adresu z pamięci podręcznej, aż do znalezienia dziury. Zapisuje koniec dziury do użycia w następnym wyszukiwaniu, a następnie zwraca lokalizację tej dziury. Jeśli dojdzie do końca przestrzeni adresowej, zaczyna ponownie od początku, co daje mu jeszcze jedną szansę na znalezienie otwartego obszaru.

Jak widać, zwykle przypisuje stosy w sposób rosnący (dla x86; x86-64 używa arch_get_unmapped_area_topdown i prawdopodobnie przypisze je malejące). Jednak przechowuje również pamięć podręczną, od której należy rozpocząć wyszukiwanie, więc może pozostawić luki w zależności od tego, kiedy obszary zostaną zwolnione. W szczególności, gdy obszar zmapowany jest zwolniony, może zaktualizować pamięć podręczną wyszukiwania wolnych adresów, więc możesz zobaczyć tam również alokacje poza kolejnością.

To powiedziawszy, to wszystko jest szczegółem implementacji. Nie polegaj na tym w swoim programie. Weź tylko rozdane adresy mmap i bądź szczęśliwy :)

8
bdonlan 21 lipiec 2011, 04:33
1
Myślę, że między nami zidentyfikowaliśmy cały odpowiedni kod źródłowy. Zgadza się, to 90% Ty :-)
 – 
Nemo
21 lipiec 2011, 04:36
@Nemo, ja też chciałem poszukać źródła pthreads, ale jakoś znalazłem się w linuxthreads i postanowiłem po prostu prześledzić to :)
 – 
bdonlan
21 lipiec 2011, 04:37
Przy okazji, w przypadku odniesień do źródeł jądra, możesz połączyć się bezpośrednio z Drzewo git Linusa. Co może, ale nie musi być tym, czego chcesz.
 – 
Nemo
21 lipiec 2011, 05:00
@Nemo, uważam, że LXR jest ładniejszy, ponieważ zawiera odniesienia dla ciebie :)
 – 
bdonlan
21 lipiec 2011, 05:10
1
+1 świetna odpowiedź z 3 powodów: (1) polecanie strace jako sposobu na samodzielne znalezienie odpowiedzi, (2) szczegółowe informacje na temat wewnętrznych elementów jądra, (3) rada, aby nie polegać na żadnym z nich :-)
 – 
R.. GitHub STOP HELPING ICE
21 lipiec 2011, 10:53

Glibc obsługuje to w nptl/allocatetestack.c.

Kluczowa linia to:

mem = mmap (NULL, size, prot,
            MAP_PRIVATE | MAP_ANONYMOUS | MAP_STACK, -1, 0);

Więc po prostu prosi jądro o jakąś anonimową pamięć, podobnie jak malloc robi dla dużych bloków. To, który blok faktycznie otrzyma, zależy od jądra...

4
Nemo 21 lipiec 2011, 05:04