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 będące 64 bitowymi liczbami naturalnymi. 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ózniejszych,
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ądz 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 procedury 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 liczbę 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óznić wykonanie określonych czynności o ustaloną ilość czasu 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,
niemniej w większości przypadków nadają się one do zastosowania. 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
1 To uaktualnienie nie jest dokładne, ale nie ma potrzeby zmieniać je na dokładniejsze.
2 Tę datę przyjmuje się za początek epoki Uniksa .
1
Systemy Operacyjne semestr drugi
z wymienionych pól określa czas po którym ma zostać wywołana związana z licznikiem funkcja, drugie jest parametrem wywołania tej funkcji, a trzecie zawiera jej
adres. Wymieniona wcześniej funkcja musi 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 data 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 są przez nią aktywowane. Jeśli chcemy usunąć aktywny licznik możemy to uczynić funkcją del_timer().
W systemach wieloprocesorowych, celem uniknięcia sytuacji hazardowych należy użyć del_timer_sync(). We wszystkich systemach należy unikać innej modyfikacji
liczników niż przez funkcję 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 nie wywoływane są uruchamiane w kontekście dolnej połówki, z poziomu przerwania programowego
(czyli w kontekście przerwania), po zakończeniu obsługi przerwania zegarowego. Aby uniknąć sortowania listy liczników i kosztownego przeglądania jest ona
podzielona na pięć grup, pod względem czasu po jakim trzeba będzie wywołać funkcje zgromadzonych na niej liczników.
3. Opóznianie wykonania
Najprostszym sposobem opóznienia wykonania kodu jest umieszczenie w nim pętli aktywnego oczekiwania. W Linuksie do określenia warunku zakończenia takiej pętli
można skorzystać z makrodefinicji time_before(). Wymaga to oczywiście odczytu zmiennej jiffies, co może rodzić obawy, czy podczas kompilacji nie zostanie ta operacja
zoptymalizowana w niepożądany sposób. 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óznienia ma być krótki, możemy posłużyć się funkcjami udelay() i mdelay(). Pierwsza opóznia wykonanie na jedną mikrosekundę wykonując
określoną liczbę razy pętlę złożoną z odpowiednich instrukcji. Liczba 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 oczekiwanie jest nieefektywne i niewskazane wykonanie procesów (wątków
wykonania) można opózniać używając do tego funkcji schedule_timeout() i ustawiając stan procesu na TASK_INTERRUPTIBLE lub TASK_UNINTERRUPTIBLE.
Zmiany:
- 2.6.10 funkcja add_timer_on() służąca do aktywacji liczników na określonych procesorach (rdzeniach) przestaje być dostępna dla modułów,
- 2.6.13 dodano funkcję try_to_del_timer_sync() do usuwania liczników, którą można wywoływać w kontekście przerwania, stałą HZ uczyniono konfigurowalną na etapie
kompilacji jądra,
- 2.6.14 dodano dwie nowe funkcje: signed long schedule_timeout_interruptible(signed long timeout) i signed long schedule_timeout_uniterruptible(signed long timeout),
które zastępują równoczesne użycie schedule_timeout() i set_current_state(),
-2.6.17 wprowadzono nowy podsystem czasu rzeczywistego z nowym API,
-2.6.20 wprowadzono funkcję round_jiffies(), która zaokrągla odczytaną wartość zmiennej jiffies do następnej pełnej sekundy,
-2.6.21 wprowadzono poprawkę "clockevents and dyntic ,pozwalającą obsługiwać inne niż standardowe urządzenia generujące impulsy czasowe; ta poprawka pozwala
także wyłączyć budzenie procesora przez czasomierz, jeśli w najbliższym okresie czasu nie ma żadnego oczekującego licznika,
-2.6.22 wprowadzono mechanizm opóznianych liczników (ang. defferable timers), które nie są uaktywniane jeśli system nie jest zajęty,
-2.6.30 dodano funkcję int mod_timer_pending(struct timer_list *timer, unsigned long expires), która działa jak mod_timer() z tym, że nie reaktywuje liczników, które
już zadziałały.
2
Wyszukiwarka
Podobne podstrony:
SO2 wykladSO2 wyklad Warstwa operacji blokowychSO2 wyklad 1SO2 wyklad Przestrzeń adresowa procesówSO2 wykladSO2 wyklad 4 Wywołania systemoweSO2 wyklad 8SO2 wyklad Obsługa sieciSO2 wykladSO2 wyklad 7SO2 wyklad 3SO2 wykladSO2 wyklad 5SO2 wyklad 2SO2 wyklad 6SO2 wyklad 2 Zarządzanie procesamiSO2 wykladSO2 wyklad 4więcej podobnych podstron