Wykład 9: Pomiar czasu i synchronizacja względem czasu w Linuksie

Transkrypt

Wykład 9: Pomiar czasu i synchronizacja względem czasu w Linuksie
Systemy Operacyjne
semestr drugi
Wykład dziewiąty
Pomiar czasu i synchronizacja względem czasu w Linuksie
1.
Generowanie taktów i pomiar upływu czasu
Wywołania wielu funkcji jądra są uzależnione nie wystąpieniem określonych zdarzeń lecz upływem czasu, dlatego jądro systemu musi mieć
mechanizm pozwalający na pomiar upływu czasu. Ten pomiar jest również potrzebny aplikacjom przestrzeni użytkownika. Te ostatnie mogą
wymagać informacji na temat bieżącej daty i czasu, jak również mogą dokonywać pomiaru czasu wykonania pewnych czynności.
Głównym mechanizmem wyznaczającym dla jądra upływ kolejnych chwil jest zegar systemowy. Mechanizm ten generuje w określonych
odstępach czasu przerwanie zwane przerwaniem zegarowym, które jest obsługiwane za pomocą specjalnej procedury obsługi przerwania. Zegar
systemowy pracuje na podstawie impulsów zegarowych generowanych przez generator impulsów taktujących procesor lub przez inne
urządzenia tego typu. Częstotliwość generowania kolejnych przerwań przez zegar systemowy jest określona stałą HZ. Znając częstotliwość
można wyznaczyć długość okresu z jakim działa zegar systemowy. W większości współczesnych architektur stała HZ jest równa 100. Wyjątek
stanowi między innymi architektura i368, dla której ta wartość wynosi 1000. Została ona zwiększona 10 krotnie podczas implementacji jądra w
wersji 2.4. Stała ta ma oczywiście wpływ na szybkość działania systemu. Zwiększenie jej ma wiele zalet, ale również może powodować
problemy. Do zalet należy zaliczyć zwiększenie rozdzielczości przerwania zegarowego i zwiększenie dokładności sterowania czasem, co
pociąga za sobą lepszą obsługę wszystkich zdarzeń zależnych od czasu. Niestety nie można tej stałej w dowolny sposób modyfikować. Należy
również uwzględnić fakt, że zwiększenie tej stałej pociąga za sobą zwiększenie częstotliwości występowania przerwania zegarowego i
częstszego wywoływania jego procedury obsługi. Liczbę taktów zegara od momentu uruchomienia systemu przechowuje się w zmiennej jiffies,
której wartość jest zwiększana o jeden podczas każdego taktu zegara. Czas pracy systemu (ang. uptime) mierzony jest jako iloraz jiffies/HZ.
Zmienna jiffies przechowuje wartości naturalne, 64 bitowe. We wcześniejszych wersjach jądra zmienna ta, w architekturach i386 była 32
bitowa. Przy HZ=100 przepełnienie jej następowało co 497 dni. Kiedy od jądra 2.4 zaczęto stosować stałą HZ=1000, to okres ten skrócił się do
49,7 dnia, dlatego też twórcy jądra zastosowali rozwiązanie polegające na „nałożeniu” 32 bitowej zmiennej jiffies na zmienną jiffies_64,
która ma rozmiar 64 bitów. Odwołanie się więc do zmiennej jiffies w architekturach 32 bitowych daje wartość jej młodszego słowa, w
architekturach 64 bitowych jej pełną wartość. Dostęp do pełnej wartości tej zmiennej jest możliwy w obu przypadkach poprzez funkcję
get_jiffies_64(). Aby uniknąć problemów jakie może spowodować przepełnienie tej zmiennej programiści jądra wprowadzili makrodefinicje,
uwzględniające to zjawisko przy pomiarze czasu. Są to time_after, time_before, time_after_eq i time_before_eq. Do wszystkich z nich
przekazywane są dwa parametry: wartość zmiennej jiffies i wartość z którą jest ona porównywana. Pierwsza makrodefinicja zwraca wartość
różną od zera, jeśli pierwszy parametr reprezentuje moment występujący przed momentem reprezentowanym przez parametr drugi. Druga
makrodefinicja działa odwrotnie. Dwie pozostałe dodatkowo uwzględniają przypadek, kiedy oba przekazane makrodefinicjom parametry są
równe.
Aby aplikacje użytkownika, które były napisane z myślą o wcześniejszych niż wersja 2.4 jądrach mogły działać bez przeszkód na jądrach 2.4 i
późniejszych, wprowadzono stałą USER_HZ, która pełni rolę stałej HZ dla przestrzeni użytkownika. Ma ona zastosowanie głównie podczas
skalowania wartości jiffies dla przestrzeni użytkownika. Tej konwersji dokonuje makrodefinicja jiffies_to_clock(). Jądro obsługuje również
mechanizm zegara czasu rzeczywistego (RTC) z którego pracy korzystają głównie aplikacje użytkowników. Zegar ten przechowuje i
aktualizuje informacje o bieżącej dacie i godzinie. Jego zawartość jest odczytywana i umieszczana w zmiennej xtime podczas rozruchu
systemu. Jądro nie odczytuje już więcej zawartości zegara czasu rzeczywistego, ale samo aktualizuje zawartość tej zmiennej i ewentualnie
może aktualizować zawartość samego RTC.
Procedura obsługi przerwania zegarowego podzielona jest na część, która zależna jest od architektury sprzętowej i część, która jest niezależna.
W pierwszej realizowane są cztery czynności:
założenie blokady xtime_lock celem zabezpieczenia dostępu do zmiennej jiffies_64 i zmiennej xtime,
potwierdzenie obsługi przerwania bądź wyzerowanie licznika dekrementacyjnego,
okresowy zapis bieżącej wartości zmiennej xtime do RTC,
wywołanie funkcji do_timer()
Funkcja do_timer() realizuje następujące operacje:
zwiększa o jeden wartość zmiennej jiffies_64,
aktualizuje statystyki wykorzystania zasobów procesu użytkownika (głownie czas spędzony w przestrzeni użytkownika i przestrzeni
systemu), dokonuje tego za pośrednictwem funkcji update_process_times()1,
uruchamia procedur obsługi liczników dynamicznych, które zakończyły odliczanie,
wywołuje funkcje scheduler_tick(),
aktualizuje czas w zmiennej xtime,
oblicza obciążenie systemu.
Bieżący czas systemowy przechowywany jest w zmiennej xtime, której typ jest strukturą posiadającą dwa pola. Pierwsze z nich przechowuje
ilość sekund, które upłynęły od 1 stycznia 1970 roku2, a drugie liczbę nanosekund, które upłynęły od początku bieżącej sekundy. Zapis i
odczyt tej zmiennej wymagają założenia blokady sekwencyjnej xtime_lock. Dla aplikacji użytkownika ta zmienna jest dostępna poprzez
wywołanie gettimeofday().
2.
Liczniki
Liczniki, zwane licznikami dynamicznymi lub licznikami jądra pozwalają opóźnić wykonanie określonych czynnośći o ustaloną ilość czasu
1
2
To uaktualnienie nie jest dokładne, ale nie ma potrzeby zmieniać je na dokładniejsze
Tę datę przyjmuje się za początek „epoki Uniksa”.
1
Systemy Operacyjne
semestr drugi
począwszy od chwili bieżącej. Nie jest to mechanizm precyzyjny i mogą zdarzać się niedokładności rzędu rozmiaru okresu, więc nie powinny
one być stosowane do zadań czasu rzeczywistego. W większości przypadków są one odpowiednie dla większości zastosowań. Należy również
pamiętać, że liczniki te nie są cykliczne, tzn. po upływie określonego czasu są zwalniane i nie jest wznawiana ich praca. Liczniki dynamiczne
są reprezentowane strukturą timer_list. Aby skorzystać z licznika należy zadeklarować zmienną tego typu, zainicjalizować ją przy pomocy
init_timer(), wypełnić bezpośrednio pola expires, data i function, oraz uaktywnić przy pomocy add_timer(). Pierwsze z wymienionych pól
określa czas po którym ma zostać wywołana związana z licznikiem funkcja, drugie zawiera wartość parametru dla tej funkcji, a trzecie jej
adres. Wymieniona wcześniej funkcja ma mieć następujący prototyp:
void my_timer_function(unsigned long data);
Ponieważ funkcja ta może być związana równocześnie z kilkoma licznikami, to wartość parametru pozwala jej ustalić na rzecz którego została
wywołana. Jeśli zaistnieje konieczność modyfikacji momentu wyzwolenia licznika można użyć w tym celu funkcji mod_timer(). Funkcja ta
może zostać użyta zarówno dla aktywnych, jak i nieaktywnych liczników, przy czym te ostatnie aktywuje. Jeśli chcemy usunąć aktywny
licznik możemy to uczynić funkcja del_timer(). W systemach wieloprocesorowych, celem uniknięcia sytuacji hazardowych należy użyć
del_timer_sync(). W każdych systemach należy unikać innej modyfikacji liczników niż przez funkcje mod_timer(), oraz należy pamiętać, aby
zabezpieczyć zasoby do których dostęp współbieżny mają funkcje wywoływane przez liczniki oraz inne części kodu. Liczniki są powiązane w
listę. Funkcje przez nich wywoływane są uruchamiane w kontekście dolnej połówki, z poziomu przerwania programowego, po zakończeniu
obsługi przerwania zegarowego. Aby uniknąć sortowania listy liczników i kosztownego przeglądania są one podzielone na pięć grup, pod
względem czasu po jakim trzeba będzie wywołać ich funkcje.
3.
Opóźnianie wykonania
Najprostszym sposobem opóźnienia wykonania kodu jest umieszczenie w nim pętli aktywnego oczekiwania. W Linuksie można to uczynić
korzystając z funkcji time_before(), celem określenia warunku zakończenia takiej pętli. Wymaga to oczywiście odczytu zmiennej jiffies, co
może rodzić obawy, czy podczas kompilacji nie zostanie ta operacja w niepożądany sposób zoptymalizowana. Aby zapobiec takim sytuacjom
programiści jądra zadeklarowali ją jako volatile, co skutecznie powstrzymuje kompilator od optymalizowania operacji na tej zmiennej.
Zalecanym jest, aby w takiej pętli wywołać cond_reached(), która powoduje przeszeregowanie zadań jeśli zachodzi taka potrzeba. Jeśli czas
opóźnienia ma być krótki, możemy posłużyć się funkcjami udelay() i mdelay(). Pierwsza opóźnia wykonanie na jedną mikrosekundę
wykonując określoną ilość razy pętlę złożoną z określonych instrukcji. Ilość iteracji tej pętli jest określona zmienną BogoMIPS inicjalizowaną
przy uruchamianiu jądra. Funkcja mdelay() korzysta do odliczania czasu z ... funkcji udelay(). Ponieważ aktywne oczekiwania jest
nieefektywne i niewskazane wykonanie procesów można opóźniać używając do tego funkcji schedule_timeout() i ustawiając stan procesu na
TASK_INTERRUPTIBLE lub TASK_UNINTERRUPTIBLE.
2

Podobne dokumenty