Zadania rekrutacyjne i ćwiczeniowe
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
chodak166 2633f92c75 Added cpu-tracker doc directory with task content and README.md file 2 years ago
..
README.md Added cpu-tracker doc directory with task content and README.md file 2 years ago
tieto_zadanie_rekrutacyjne_cut_v2.pdf Added cpu-tracker doc directory with task content and README.md file 2 years ago

README.md

Treść

Treść zadania w pliku tieto_zadanie_rekrutacyjne_cut_v2.pdf

Kompilacja

System budowania to CMake, kompilacja w następujących krokach:

mkdir build && cd build
cmake ..
cmake --build .

Pliki wynikowe będą w katalogu bin. Plik aplikacji to cpu-tracker, reszta to testy. Wszystkie testy można uruchomić pojedynczo lub zbiorczo przez CTest, uruchamiając polecenie ctest w katalogu build lub polecenie ctest -V, aby zobaczyć wyniki pośrednie i wartości zmiennych w przypadku niezaliczonego testu.

Projekt

Rozwiązanie w tym repozytorium to mocny over-engineering, ale przyjąłem, że takie były oczekiwania zważywszy na polecenie odczytu danych w pięciu wątkach. W rzeczywistym przypadku zrealizowałbym to z jedną kolejką zadań pracującą w głównym wątku, plus drugi wątek (worker) w tle, który wykonuje odczyt i przerzuca kopię danych na kolejkę w wątku głównym.

Struktura projektu

W projekcie wydzieliłem dwie biblioteki, które mogłyby mieć duży re-use w kolejnych projektach. Są to log do logowania z podaniem formatu (jak w printf) i thread przykrywający pthread i dodający funkcje i typy pomocnicze (kolejka, bloker, watchdog itp.). Cała reszta, czyli główna aplikacja jest w katalogu app.

Każda część (moduł, biblioteka, aplikacja) zazwyczaj jest podzielona tak samo, chyba że jest bardzo prosta. Podział wychodzi głównie z koncepcji Hexagonal Architecture i Clean Architecture (i może lekko DDD - Domain Driven Design, linki poniżej). Ten podział to:

core (lub np. domain, czasami entities) - czysty język programowania do zapisu idei związanych z dziedziną problemu, logiką biznesową itp. Wszystko, co można omówić w ekspertem i zapisać w pseudokodzie. Jeśli potrzebny jest np. zapis do pliku, odczyt godziny, pobranie danych itp. używana jest abstrakcja w postaci np. wywołania funkcji spod wskaźnika (który zostanie dostarczony podczas inicjalizacji aplikacji). Jeśli aplikacja jest duża i dobrze opisana, dodaję tu katalog use_cases lub commands, gdzie są obiekty lub funkcje realizujące wszystkie przypadki użycia danej aplikacji.

application - logika aplikacji, czyli np. obsługa zdarzeń i warunkowe sterowanie przepływem, odpalanie funkcji z innych warstw itp. Jeśli jest tego mało, pomijam i rzucam do warstwy main (jak w przypadku tego projektu). Ten kod może zależeć tylko od core.

interface (lub czasami controllers, adapters, gateways lub wszystko to jako podkatalogi w interface) - jeśli aplikacja ma dużo interfejsów wejścia (np. CLI, HTTP api, GUI, MQTT itd.), to ten katalog zawiera "wejścia", czyli zestawy funkcji dostępowych do domeny oraz "prezentery" tłumaczące dane na format prezentacji, np. json, xml, html itd. Tutaj był tylko prosty printer z jedną funkcją, więc darowałem tę część i wrzuciłem go do infrastructure. Ten kod może zależeć od mniejszych bibliotek w części implementacji (raczej nie w publicznych interfejsach), ale raczej nie od frameworków i konkretnych technologii (np. bazy danych).

infrastructure - kod zależny od technologii, systemu operacyjnego, podatny na częste zmiany techniczne, np. po cotygodniowych spotkaniach zespołu. Tutaj wchodzi implementacja zapisu/odczytu danych, połączeń z innymi urządzeniami, komponentami sprzętowymi itp. Jeśli core musi użyć np. dostępu do stałych danych czy odczytać czas systemowy, tutaj jest to implementowane, a wskaźnik zostanie wstrzyknięty do core w warstwie main.

main - kod składający wszystko w całość, tworzący obiekty z innych warstw, uruchamiający wątki, zwalniający pamięć po zamknięciu itp. To "brudny" kod, który może zależeć od wszystkiego, ale nie powinien zawierać za dużo logiki aplikacji, i definitywnie żadnej logiki biznesowej.

tests - katalog z testami.

tests/unit - testy jednostkowe, zazwyczaj nie linkują całych bibliotek, testują pojedyncze funkcje lub powiązane zestawy funkcji. Implementują mocki/stuby/fake'i i wstrzykują do testowanych jednostek aby zrealizować scenariusz testowy.

tests/integration - testy sprawdzające działanie więcej niż jednego komponentu lub pojedynczych komponentów, ale z wykorzystaniem np. systemu operacyjnego (czas, pliki, wątki itd.).

tests/functional - testy uruchamiane przez zewnętrzny framework weryfikujący wyniki danego zakresu funkcjonalnego.

Dodatkowo w katalogu głównym projektu może znaleźć się katalog extern z bibliotekami zewnętrznymi. W tym projekcie korzystałem z greatest do testów, ale zastąpiłem go własnym makrem CHECK.

Testy

Wymagany był przynajmniej jeden test, tutaj zaliczam do niego głównie testy jednostkowe kolejki zadań w bibliotece thread. Można dopisać tam jeszcze kilka przypadków opisujących specyfikację, np. przypadek sprawdzający, czy po zatrzymaniu kolejki zostaną wykonane zaległe zadania czy może kolejka je usuwa.

W testach integracyjnych często nie robiłem asercji, służyły mi do sprawdzania wyników w trakcie pisania, a CTest i tak weryfikuje test na podstawie kodu na wyjściu (0 zaliczony, inaczej niezaliczony).

Linki

Hex:

https://www.happycoders.eu/software-craftsmanship/hexagonal-architecture/

https://medium.com/ssense-tech/hexagonal-architecture-there-are-always-two-sides-to-every-story-bc0780ed7d9c

Clean:

https://betterprogramming.pub/the-clean-architecture-beginners-guide-e4b7058c1165

https://codilime.com/blog/clean-architecture/

SOLID + wzorce:

https://www.renaissancesoftware.net/files/articles/ESC-204Paper_Grenning-v1r0.pdf

https://media.readthedocs.org/pdf/eswp3/v1.0/eswp3.pdf

Dobry e-book o wzorcach w C dla systemów embedded:

https://repositorio.uci.cu/jspui/bitstream/123456789/10139/1/Design%20Patterns%20for%20Embedded%20Systems%20in%20C_%20An%20Embedded%20Software%20Engineering%20Toolkit%20%28%20PDFDrive%20%29.pdf

Realizacja

W najkrótszym wariancie można zrealizować przerzucanie danych między wątkami z mutexami i zmiennymi warunkowymi (np. pthread_cond_t). Tutaj trudno byłoby jednocześnie spełnić dydaktyczne wymogi zadania, więc całość działa na kolejkach zadań (opisane m.in. w e-booku linkowanym powyżej).

Kolejka zadań zawiera dynamiczną kolejkę FIFO wskaźników na funkcje void(void*). Po wystartowaniu działa w pętli, sprawdzając, czy w kolejce czeka coś do wykonania. Jeśli tak, uruchamia funkcję spod wskaźnika i zdejmuje wskaźnik z kolejki.

Każdy wątek ma swoją kolejkę (TaskQueue). W standardowym przypadku zadania, które mogą powodować równoczesny dostęp do zasobu są kolejkowane w jednym wątku. Tutaj np. dzięki kolejce z zadaniami wyświetlania komunikatów albo logowania do pliku, nie będzie śmieci w postaci pomieszanych linii czy wyrazów, kiedy wiele wątków próbowałoby pisać jednocześnie.

Task to po prostu wskaźnik na funkcję void(void*). Parametrem void* może być wszystko, np. liczba, struktura lub kolejna funkcja do wykonania.

Jednostka Thread przykrywa pthread, zgodnie z techniką/idiomem pimpl. Taki wrapper uwalnia użytkowników biblioteki (tutaj główną aplikację) od wiedzy o użytej technologii/frameworku/bibliotece i nie wprowadza bezpośredniej zależności (np. nie trzeba aktualizować plików nagłówkowych, jeśli zmieni się implementacja).

Jednostka QueuedThread ułatwia tworzenie wątków z wbudowaną kolejką zadań, aby nie trzeba było składać wszystkiego każdorazowo dla nowego wątku.

Blocker to wrapper zmiennej warunkowej (pthread_cond_t). Pozwala na czasowe blokowanie wątku do momentu odblokowania przez inny wątek. W najprostszej wersji można użyć sleep, ale wtedy: a) trzeba doczekać, aż sleep skończy czekać aby przejść dalej i np. zdjąć zadania z kolejki lub wyłączyć program; b) trudno pisać testy jednostkowe bez możliwości wstrzykiwania funkcji czekającej do testowanej jednostki.

Watchdog posiada listę wątków, którymi się "opiekuje". Bardziej zgodne z regułą SRP byłoby tworzenie wątków poza watchdogiem i późniejsze rejestrowanie ich w nim. Tutaj ze względu na mały kod, umieściłem tworzenie i usuwanie wątków w watchdogu.

Dzięki zastosowaniu kolejek, implementacja watchdoga jest prosta i nie wymaga "zatruwania" wątków jakimiś mechanizmami powiadamiania go o tym, że jeszcze żyją. Skoro każdy wątek i tak ma kolejkę, watchdog po prostu wrzuca im zadanie do wykonania, one nawet nie wiedzą, co wykonują. Zadanie to zaktualizowanie aktualnej godziny w watchdogu. Watchdog sprawdza okresowo, czy wszystkie wpisy dla wątków są aktualne, jeśli któryś wątek nie wykonał w wyznaczonym czasie aktualizacji, watchdog uznaje, że się zawiesił.

UWAGA DO WYNIKÓW: odczyt i wyliczanie użycia CPU to implementacja z pierwszego lepszego artykułu. Empirycznie wydaje się działać dobrze, ale zauważyłem, że po przełączeniu komputera w stan uśpienia i wybudzeniu go po kilku godzinach, wartości dla wszystkich rdzeni mają 99% i maleją z każdą minutą. Nie debugowałem tego, bo to skrajny przypadek i nie jest to faktycznym celem zadania.

UWAGA DO WYDAJNOŚCI: w systemach embedded lub systemach czasu rzeczywistego sporo z tego byłoby do skrócenia. Wtedy kosztem abstrakcji minimalizowałbym liczbę dereferencji wskaźników lub zamiast wstrzykiwania zależności używałbym importu funkcji przez extern w plikach .c.