Hlavní navigace

Co před námi tají /proc (10)

16. 8. 2001
Doba čtení: 9 minut

Sdílet

Jsme u písmene L. L jako loadavg nebo locks. Taky máme malé výročí - desátý díl seriálu. Když uvážím, že jsem to celé plánoval tak na čtyři až pět dílů, vychází mi z toho dvě možnosti. Buď mám hodně špatnej odhad, nebo jsem moc ukecanej. Nechám na vás, co si vyberete.

Jak velí tradice, nejprve výpis základního adresáře /proc. Tečka před názvem znamená, že už to máme za sebou. Pravda, nepřibývají zrovna moc rychle.

  .840     .devices     .kcore    modules     stat     ide/
  .842     .dma         .kmsg     mounts      swaps    irq/
  .847     .execdomains .ksyms    mtrr        uptime   net/
  .848     .filesystems  loadavg  partitions  version  nv/
  .apm     .interrupts   locks    pci         bus/     sys/
  .cmdline .iomem        meminfo  self@       driver/  sysvipc/
  .cpuinfo .ioports      misc     slabinfo    fs/      tty/
loadavg

Zase jeden ze souborů, který možná normálnímu uživateli moc neřekne. Těm znalejším jsou asi jasná první tři čísla. Ale co zbytek?

0.00 0.00 0.00 1/37 844

Jak vidíte, psaní článků, jako je tento, není příliš náročnou úlohou pro počítače dnešních dnů. Ještě že máme počítačové hry a SETI :).

Kolem hodnot loadavg se před časem vedla poměrně rozsáhlá debata na konferenci linux@linux.cz (cz.comp.linux). Číslo by mělo znamenat průměrnou zátěž systému za poslední minutu (první hodnota), pět minut (druhá) a patnáct minut (třetí hodnota). Dle manuálové stránky k programu top jde o průměrné množství procesů ve stavu READY za posledních x minut. Podle tohoto jednoduchého výkladu by to znamenalo, že ideální stav je, když jde o čísla kolem (těsně pod) 1.00. Jestliže je hodnota menší, je systém v prostoji, je-li větší, je systém naopak zahlcen a nestíhá vyřizovat požadavky jednotlivých programů na systémové zdroje. Schválně jsem předchozí tvrzení formuloval pokud možno obecně, protože se liší názory na to, co vše by mělo být a co ve skutečnosti je zahrnuto v této hodnotě. Příkladem budiž proces čekající na dokončení diskové operace. Pokud budeme uvažovat pouze zátěž procesoru (CPU), je tento proces neaktivní a čeká. Pokud se budeme na systém dívat jako na celek, proces využívá jeho část a je tedy aktivní. Na druhou stranu, pokud proces čeká na data čtená z disku, lze předpokládat (pokud vyloučíme chyby nebo výpadky hardwaru), že čekání v blízké a předem (shora) ohraničitelné době skončí. Pokud ale čekáme na data třeba ze síťové karty, nelze předem říct, jak dlouho bude trvat, než je dostaneme. Pokud druhá strana ještě nezačala vysílat, jsou celé síťové rozhraní OS i vlastní HW v klidu a nepřispívají tedy k zatížení systému.

Nyní se tedy pokusíme ze zdrojového textu určit přesný (a aktuální) způsob určování těchto hodnot. Po dlouhé době se odpovídající funkce pro čtení souboru z adresáře proc neodvolává na další obsluhu, ale řeší vše briskně sama. Z funkce loadavg_read_proc() zjistíme rychle význam posledních třech čísel. Úplně na konci je poslední přidělené PID, číslo posledního vytvořeného procesu. V našem případě jde o proces, který provedl výpis tohoto souboru (cat /proc/loadavg). Dvojice čísel oddělených lomítkem udává počet právě běžících procesů (těch, které jsou ve stavu READY nebo RUN). Jestliže je na ukázkovém počítači na tomto místě číslo 2, znamená to, že dva procesy se střídají na procesoru, a zatímco jeden aktivně běží, druhý čeká ve frontě, až na něj přijde při příštím přeplánování řada. Za lomítkem je celkový počet procesů v systému. Jedná se o aktuální počet prvků tabulky procesů a nemusí jít vždy o uživatelské procesy. Jsou zde zahrnuty i „procesy“ jako bdflush, kapm-idle nebo kswapd, které slouží k řešení systémových záležitostí, běží zcela v režimu jádra a jako procesy jsou implementovány jen kvůli zjednodušení. Je tak totiž jednodušší třeba počítat časy běhu jednotlivých procesů, poměr mezi systémovým a uživatelským časem a určitě by se našla i hromada dalších věci.

Tak a teď vlastní hodnoty loadavg. V tomto zvláštním případě budu co nejpřesněji popisovat aktuální stav ze zdrojových kódu, abych co nejvíce zamezil nepřesnostem.

V souboru include/linux/sched­.h je popsána definice formátu čísla, který se používá pro uložení hodnot loadavg do 32bitového čísla (v tomto případě int). Jedná se o formát fixed-point. Ten je definován tak, že bity 0 až FSHIFT-1 (současná hodnota 11) jsou určeny pro uložení reálné části (za desetinou tečkou), bity FSHIFT až 31 uchovávají informaci o celé části čísla. Pomyslná desetinná tečka (čárka, jste-li příznivci LOCALES) je na pozici mezi bitem FSHIFT a bitem FSHIFT-1. Jednotlivé řády představuje následující schéma. Bity v čísle typu int označme takto:

2^31 2^30 ... 2^FSHIFT , 2^FSHIFT-1 ... 2^1 2^0

Jim potom na stejných pozicích odpovídají významově bity formátu fixed-point:

2^20 2^19 ... 2^0 , 2^-1 2^-2 .... 2^-11

Tímto způsobem jsou tedy uložena čísla pro loadavg. Při tisku jejich hodnot se nejprve ke každému přičte 0.005, čímž se po odřezání dosáhne zaokrouhlení na dvě platné číslice za desetinou tečkou. Před tiskem jsou pak použita makra pro ořezání nepotřebných částí – reálné části pro celé číslo a zbytku reálné části pro dvě cifry za desetinou tečkou. Trošku s podivem je, že pole pro uložení těchto tří hodnot avenrun[] obsahuje dle definice hodnoty typu unsigned long. Nicméně pro dnešní 32bitové počítače i při ořezání na int dostaneme maximální load kolem milionu procesů. To asi běžnému uživateli stačí (alespoň mně většinou jo :).

Tím jsme vyřešili, co se vlastně tiskne, teď už „zbývá“ jen zjistit, co se do patřičných částí vlastně ukládá…

… (po dvou hodinách procházení zdrojových kódů). Už v tom mám jasno. Držte si kloboučky, jedeme z kopce. Na začátek jenom důležitá poznámka. Uvedený postup a princip je nový pro jádro verze 2.4.7 (možná je to už i v 2.4.6, ale není to v 2.4.5). Tak, celkem jednoduché bylo zjistit, kdo nastavuje prvky pole avenrun. Je to funkce calc_load() v souboru kernel/timer.c. Ta pracuje s frekvencí LOAD_FREQ (aktuálně je to 5*HZ. HZ je počet tiků časovače za sekundu definovaný v souboru include/asm/pa­ram.h s hodnotou 100 pro x86, 1024 pro Alphy nebo 1000 pro rtLinux – Real Time Linux). Jednou za pět sekund se spočítají aktivní úlohy. Jednoduše se projde tabulka všech úloh v systému, a pokud je hodnota položky state v tabulce procesu TASK_RUNNING nebo TASK_UNINTERRUP­TIBLE, zvýší se počet „běžících“ úloh o jedničku. Zajímavé je, že dle přiřazení hodnot (v include/li­nux/sched.h) se vylučují pouze stavy TASK_RUNNING a TASK_INTERRUPTIBLE. Jestliže máme spočítáno množství „bežících“ (lepší by bylo označení aktivních – v případě, že termín „bežící“ včetně uvozovek nepřenesete přes srdce, proveďte si s/„běžící“/ak­tivní) procesů, můžeme spočítat hodnotu zatížení systému pomocí makra CALC_LOAD ze souboru include/linux/sched­.h, kde se provádí následující výpočet:

load = (load * e + N * (FIXED_1 - e)) >> FSHIFT

Exponent je číslo ve formátu fixed-point o hodnotě 1/(exp^(5sec/[1mi­n|5min|15min])), N je počet aktivních procesů, funkce exp() představuje mocninu přes Eulerovo číslo.

Tím jsme objasnili, jak číslo vznikne, teď ještě kdy se funkce calc_load vlastně volá. Je to jednou za určitý počet tiků (tedy ne nutně po každém). Volání je provedeno z funkce update_times, která je volána z handleru dolní poloviny přerušení pro časovač. Handler se jmenuje time_bh. Když už jsme v tom, tak ještě vysvětlení, jak momentálně fungují dolní handlery přerušení a proč může být počet tiků mezi voláními více než jedna.

Vlastní handler přerušení od časovače je funkce timer_interrup­t(). Ta je nastavena ve funkci time_init() volané při startu systému ze start_kernel(). Handler přerušení provede funkci do_timer_inte­rrupt(), a ta konečně zavolá funkci do_timer(). Trošku složité? Bude hůř. Pokud vás to nebaví, raději to přeskočte, než abyste se nechali znechutit. Vypisovat jména funkcí je možná nuda, ale až to někdo z vás bude hledat, mohlo by mu to usnadnit život.

Zatím jsme se dostali do nejnižší funkce obsluhy přerušení, která nás zajímá. Zde je obsluha ukončena, když je předtím provedeno volání mark_bh(TIMER_BH). To zajistí provedení dolní poloviny handleru přerušení, kterou tvoří funkce timer_bh() zmiňovaná v předchozím textu.

Pro ty nejnáročnější ještě třešnička na závěr. Jak jsou aktivovány dolní poloviny handlerů přerušení? Existuje pole struktur jménem bh_task_vec. Další je pole ukazatelů na funkce bh_base. Obě mají momentálně 32 položek. Při inicializaci plánovače ve funkci sched_init() je položka pole base_bh s indexem TIMER_BH (aktuálně 0) inicializována na funkci timer_bh(). Dále existuje funkce bh_action(), která po provedení různých kontrol a zajištění jedinečnosti spuštěné instance zavolá funkci napojenou na požadovanou položku v poli bh_base. V jiné inicializační funkci jsou položky pole bh_task_vec nastaveny tak, aby volaly právě funkci bh_action s potřebným parametrem.

Tímto se dostáváme k pojmu softirq. Dal by se jednoduše vysvětlit jako softwarové přerušení. Ovšem jen kdyby neexistovala ještě jedna softwarová přerušení. Standardně se pod tímto pojmem rozumí část klasického 256ti prvkového vektoru přerušení, která není alokována ani pro systémové výjimky, ani pro tzv. hardwarové přerušení označované jako IRQ. Malou exkurzi už jsme měli v dílu číslo 8. To, o čem teď budu psát, budu nazývat softirq, a jak uvidíte, nemá to s přerušeními v klasickém slova smyslu nic společného. V Linuxu existuje taková šikovná věc, jmenuje se fronta úloh. Jedná se o seznam úloh naplánovaných k provedení, něco jako „Seznam věcí, které mám dnes udělat“. S tím rozdílem, že je možno v libovolném okamžiku zavolat funkci, která frontu probere a jednotlivé úkoly provede. Programátor si může definovat vlastní fronty a hospodařit s nimi dle libosti, většinou však vystačí s frontami, které má předdefinované jádro.

Teď si vzpomeňte na naši funkci mark_bh(). Ta udělá to, že do fronty úloh nazvané tasklet_hi_vec přidá patřičnou úlohu z pole bh_task_vec. Jak jsme si řekli, jedna z nich je i obsluha časovače funkcí timer_bh, dále se volá funkce pro vyvolání softirq jménem HI_SOFTIRQ. Vyvolání je překvapivě jednoduché. Již dříve jsme mluvili o statistikách pro jednotlivá přerušení v jádře. Existuje také speciální statistika jménem softirq_pending a vyvolání přerušení číslo N se provede nastavením N-tého bitu právě v čítači této statistiky. Ten se jako běžný čítač vůbec nepoužívá. Dobrým příkladem této techniky je implementace signálů.

Dobrá, máme zařazenu naši obsluhu do fronty úloh a „generovali“ jsme „přerušení“. Teď už zbývá jen někdo, kdo frontu provede a vyprázdní. Ten někdo je proces ksoftirqd. Ten existuje pro každý přítomný procesor a vzhledem k existenci stejného počtu front jménem tasklet_hi_vec (a jiných) je možno provádět obsluhu paralelně. Zpět k našemu démonu. Ten nedělá nic jiného než opakované testování právě statistiky přerušení softirq – a pokud se nějaké takové přerušení vyskytne, zavolá funkci pro jeho obsluhu do_softirq(). Jedním z „přerušení“ je i HI_SOFTIRQ a jeho obsluhou je funkce tasklet_hi_ac­tion(). Ta už pouze vyprázdní frontu úloh tasklet_hi_vec a jednotlivé úlohy provede. Jak si jistě ještě vzpomínáte, mezi úlohami byla i naše dolní polovina obsluhy přerušení časovače.

Na úplný závěr přece jen ještě jedna otázka. I když byla nejkritičtější obsluha časovače provedena v horní polovině, stále zbývá pro dolní polovinu dost akcí, které nejde odkládat moc dlouho. Jak je tedy možné svěřit jejich provedení procesu, který je plánován stejným algoritmem jako všechny ostatní procesy? Odpovědí může být třeba to, že po vyvolání přerušení a jeho obsluze horní polovinou je možné procesy přeplánovat a v takovém případě velmi pravděpodobně dostane šanci proces ksoftirqd, který běží s prioritou (nice) nastavenou na 19 (což je v našich krajích poměrně dost :).

Byl pro vás článek přínosný?

Autor článku