Debugowanie deadlocków z gdb

 
W programowaniu wielowątkowym zdarza się bardzo wiele problemów, wynikających z tego, że program wykonuje się współbieżnie w wielu wątkach naraz i niestety istnieje potrzeba dostępu do współdzielonych zasobów oraz sygnalizacji pewnych zdarzeń. Deadlockami straszą rozmaite podręczniki o programowaniu a za nimi wykładowcy zajęć z programowania wielowątkowego czy systemów operacyjnych. Zupełnie niepotrzebnie, bo w programowaniu wielowątkowym dużo poważniejsze problemy powodują rozmaite wyścigi, jeśli programista zapomniał o umieszczeniu gdzieś sekcji krytycznej, albo tzw. lockouty, wynikające często z braku (lub błędu) sygnalizacji, a na deser pozostają problemy powstałe przez nieodpowiedzialne pląsanie po stercie procesu, współdzielonej przecież przez wszystkie wątki, czyli heap corruption. Deadlock w katalogu wszystkich bugów wydaje się najmniejszym zmartwieniem. Tutaj z reguły wystarczającym rozwiązaniem jest uruchomienie debuggera i skorzystanie z oczu.

Kod źródłowy

Tak, kod źródłowy tym razem na początku. Prosty main(), uruchamiający 5 wątków. Przy czym jeden, ten ostatni, zapomina zwolnić mutexa (jest tą przysłowiową czarną owcą). W bibliotece pthread mutex domyślnie jest nierekurencyjny, czyli jeden wątek nie może zająć tego samego mutexa wielokrotnie bez uprzedniego zwolnienia, chociaż można, przez ustawienie flagi PTHREAD_MUTEX_RECURSIVE wymusić takie zachowanie . W windowsie z tego co pamiętam mutexy są rekurencyjne i basta (trzeba w zamian użyć semafora z ograniczeniem do 1).

No i katastrofa gotowa. Program wykonuje się następująco:

 Weźmisz gdb

Tak, gdb, jeśli ktoś z was jeszcze nie korzystał, to jest to dobry moment, żeby zacząć.

Teraz czekamy na deadlock.

Już, to teraz trzeba przerwać wykonanie (Ctrl+C) i przyjrzeć się, co się dzieje z naszym programem, a raczej co się nie dzieje.

info threads wyświetli nam listę wątków, które obecnie istnieją w aplikacji wraz z ich systemowym ID (pthread_t) oraz czubek stosu wywołań, czyli z grubsza to, czym wątek się zajmuje. Wątek główny robi sleep(1000), stąd pojawia się nanosleep(), pozostałe wątki próbują posiąść mutex, stąd __lll_lock_wait(), czekają na zwolnienie mutexa i niestety się nie doczekają. Zatem zajrzyjmy, o który konkretnie mutex chodzi. W tak trywialnym przypadku z góry wiemy, bo jest tylko jeden mutex na całą aplikację, ale “życiowych” przypadkach, gdy mamy wiele, często zbyt wiele mutexów i elementów synchronizacyjnych, nie będziemy z góry wiedzieć, o który konkretnie lock się rozchodzi.

No to od razu z grubej rury, przejrzyjmy stosy *wszystkich* wątków w aplikacji, czasem mogą ich być nawet setki:

Czasem wygodnie jest zrzucić powyższy log do pliku, by móc go swobodnie analizować, na przykład wtedy, gdy przyłapiemy na deadlocku produkcyjną aplikację, która musi być szybko uruchomiona na nowo.
Interesuje nas znajomo wyglądająca ramka stosu:

Finisz

Tutaj od razu widać, że chodzi o linię 24 z pliku main.cpp, ale niech nam to pokaże gdb. W tym celu trzeba przeskoczyć do obserwowanego wątku, wybrać ramkę i wylistować kod wokół linii 24.

Widać, że chodzi o mutex o nazwie mutex, czyli nasz jedyny mutex, w dodatku z celowym deadlockiem, jednak w “życiowym” przypadku odnajdywanie deadlocków ma szansę być równie proste. Niestety czasem największą przeskodą jest niemożliwość zreprodukowania problemu, ale gdyby się udało, zawsze można z pomocą gdb “podłączyć” się do działającego-niedziałającego procesu przez (gdb) attach <pid> .

Teraz możemy sobie poszperać po przeróżnych zmiennych, wypisać ich wartości, przeglądnąć pamięć, czyli wszystko to, do czego gdb jest niezrównany. Polecam zapoznać się z możliwościami gdb i ściągnąć popularną, jednokartkową, obsutronną ściągawkę do gdb, którą znajdziemy tutaj.

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *