Principy fungování v operačních systémech typu UNIX na příkladu Linuxu. Vytvoření vlastního lehkého procesu

Jeden z nejotravnějších momentů při přechodu z prostředí do Na bázi Windows k použití příkazový řádek– ztráta snadného multitaskingu. I v Linuxu, pokud používáte systém X Window, můžete jednoduše kliknout myší nový program a otevřete jej. Na příkazovém řádku jste ale do značné míry uvízli u monotaskingu. V tomto článku vám to ukážeme jak multitasking v Linuxu pomocí příkazového řádku.

Zázemí a řízení prioritních procesů

V Linuxu však stále existují způsoby multitaskingu a některé z nich jsou komplexnější než jiné. Jedna vestavěná metoda, která nevyžaduje žádné další software, jednoduše přesouvá procesy na pozadí a v popředí. Mluvíme o tom. Má však některé nevýhody.

Nechráněný

Za prvé Chcete-li odeslat proces na pozadí, musíte jej nejprve pozastavit. Již běžící program není možné odeslat na pozadí a zároveň jej udržovat.

Za druhé, musíte přerušit pracovní postup, abyste mohli spustit nový příkaz. Musíte se dostat z toho, co právě děláte, a zadat do shellu další příkazy. Funguje to, ale je to nepohodlné.

Třetí, měli byste sledovat výstupy z procesů na pozadí. Jakýkoli výstup z nich se objeví na příkazovém řádku a bude zasahovat do toho, co právě děláte. Úlohy na pozadí tedy musí svůj výstup buď přesměrovat samostatný soubor nebo je třeba je úplně deaktivovat.

Kvůli těmto nedostatkům existují obrovské problémy při správě procesu na pozadí a popředí. Nejlepší rozhodnutí– použijte obslužný program příkazového řádku „screen“, jak je znázorněno níže.

Ale nejprve - otevřete novou relaci SSH

Nezapomeňte, že právě otevíráte novou relaci SSH.

Může být nepohodlné neustále otevírat nové relace. A to je, když potřebujete „obrazovku“

Utility obrazovka umožňuje vytvářet více pracovních postupů otevřených současně - nejbližší analogie k „oknům“. Ve výchozím nastavení je k dispozici v běžných linuxových repozitářích. Nainstalujte jej na CentOS/RHEL pomocí následujícího příkazu:

Instalační obrazovka sudo yum

Otevření nové obrazovky

Nyní začněte relaci zadáním „screen“.

Tím se vytvoří prázdné okno v rámci existující relace SSH a přidělte jí číslo, které se zobrazí v záhlaví takto:

Naše obrazovka zde má číslo „0“, jak je znázorněno. Na tomto snímku obrazovky používáme fiktivní příkaz „read“ k zablokování terminálu a čekání na vstup. Nyní řekněme, že chceme udělat něco jiného, ​​zatímco budeme čekat.

Otevřít nová obrazovka a udělat něco jiného, ​​vytiskneme:

Ctrl+a c

„ctrl+a“ je výchozí kombinace kláves pro ovládání obrazovek v programu obrazovky. Co napíšete po něm, určuje akci. Například:

  • ctrl+a c – C aktivuje novou obrazovku
  • ctrl+a [číslo]– skok na konkrétní číslo obrazovky
  • ctrl+a k – K vypne aktuální obrazovku
  • ctrl+a n – Přejít na obrazovku n
  • ctrl+a “- zobrazí všechny aktivní obrazovky v relaci

Pokud stiskneme „ctrl+a c“, dostaneme novou obrazovku s novým číslem.

Pomocí kurzorových kláves můžete procházet seznamem a přejít na požadovanou obrazovku.
Obrazovky jsou nejblíže k „oknům“, jako systém v příkazu Linuxový řetězec. Samozřejmě to není tak jednoduché jako klikání myší, ale grafický subsystém je velmi náročný na zdroje. S obrazovkami můžete získat téměř stejnou funkčnost a umožnit plný multitasking!

Procesy v UNIXu

V UNIXu je hlavním prostředkem organizace a jednotkou multitaskingu proces. Operační systém manipuluje s obrazem procesu, který představuje programový kód, a také s úseky dat procesu, které definují prostředí provádění.

Během provádění nebo při čekání „v křídlech“ jsou procesy obsaženy ve virtuální paměti s organizací stránek. Část této virtuální paměti je mapována na fyzickou paměť. Část fyzické paměti je vyhrazena pro jádro operačního systému. Uživatelé mají přístup pouze k paměti zbývající pro procesy. V případě potřeby jsou stránky paměti procesu přemístěny z fyzické paměti na disk, do odkládací oblasti. Při přístupu na stránku ve virtuální paměti, pokud není ve fyzické paměti, dojde k jejímu odložení z disku.

Virtuální paměť je implementována a automaticky udržována jádrem UNIX.

Typy procesů

V operačních systémech UNIX existují tři typy procesů: systémové, procesy démonů A aplikační procesy.

Systémové procesy jsou součástí jádra a jsou vždy umístěny v paměť s náhodným přístupem. Systémové procesy nemají odpovídající programy ve formě spustitelných souborů a spouštějí se zvláštním způsobem při inicializaci jádra systému. Prováděcí instrukce a data těchto procesů jsou uložena v jádře systému, takže mohou volat funkce a přistupovat k datům, ke kterým jiné procesy nemají přístup.

Systémové procesy zahrnují proces počáteční inicializace, init, který je předchůdcem všech ostatních procesů. Ačkoli init není součástí jádra a jeho spouštění probíhá ze spustitelného souboru, jeho činnost je životně důležitá pro fungování celého systému jako celku.

Démoni- jedná se o neinteraktivní procesy, které se spouštějí obvyklým způsobem - načtením příslušných programů do paměti a jsou prováděny na pozadí. Obvykle se démoni spouštějí během inicializace systému, ale po inicializaci jádra a zajišťují provoz různých podsystémů UNIX: systémy terminálového přístupu, tiskové systémy, síťové služby atd. Démoni nejsou spojeni s žádným uživatelem. Většinu času démoni čekají na žádost jednoho nebo druhého procesu konkrétní službu.



NA aplikační procesy zahrnuje všechny ostatní procesy běžící v systému. Obvykle se jedná o procesy vytvořené v rámci uživatelské relace. Nejdůležitější uživatelský proces je počáteční příkazový interpret, který zajišťuje provádění uživatelských příkazů v systému UNIX.

Uživatelské procesy mohou běžet jak v interaktivním (preemptivním), tak v režimy na pozadí. Interaktivní procesy mají výhradní vlastnictví terminálu a dokud takový proces nedokončí své provedení, uživatel nemá přístup k příkazovému řádku.

Atributy procesu

Proces UNIX má řadu atributů, které mu to umožňují operační systémřídit jeho práci. Hlavní atributy:

· ID procesu (PID), což umožňuje jádru systému rozlišovat mezi procesy. Při vytvoření nový proces, jádro mu přiřadí další volný (tj. nesdružený s žádným procesem) identifikátor. K přidělení identifikátoru dochází zpravidla vzestupně, tzn. ID nového procesu je větší než ID procesu vytvořeného před ním. Pokud ID dosáhne maximální hodnoty (obvykle 65737), další proces obdrží minimální volné PID a cyklus se opakuje. Když proces skončí, jádro uvolní identifikátor, který používalo.

· ID rodičovského procesu (PPID)– identifikátor procesu, který tento proces vyvolal. Všechny procesy v systému kromě systémové procesy a proces init, který je předchůdcem zbývajících procesů, jsou generovány jedním ze stávajících nebo dříve existujících procesů.

· Prioritní oprava (NI)– relativní prioritu procesu, kterou bere v úvahu plánovač při určování pořadí spouštění. Skutečná distribuce prostředků procesoru je určena prioritou provádění (atribut PRI), v závislosti na několika faktorech, zejména na dané relativní prioritě. Relativní prioritu nemění systém po celou dobu životnosti procesu, i když ji může změnit uživatel nebo administrátor při spuštění procesu pomocí příkazu pěkný. Rozsah hodnot přírůstku priority na většině systémů je -20 až 20. Pokud není zadán žádný přírůstek, použije se výchozí hodnota 10. Kladný přírůstek znamená snížení aktuální priority. Běžní uživatelé mohou nastavit pouze kladný přírůstek a tím pouze snížit prioritu. Uživatel vykořenit lze nastavit záporný přírůstek, který zvyšuje prioritu procesu a tím přispívá k její vyšší rychlá práce. Na rozdíl od relativní priority je priorita provádění procesu dynamicky měněna plánovačem.

· Terminálová linka (TTY)– terminál nebo pseudoterminál spojený s procesem. Tento terminál je spojen se standardem proudy: vstup, volno A tok zpráv o chybách. Streamy ( programové kanály) jsou standardní prostředky meziprocesová komunikace v OS UNIX. Procesy démona nejsou spojeny s terminálem.

· Skutečné (UID) a efektivní (EUID) identifikátory uživatele. Skutečné ID uživatele daného procesu je ID uživatele, který proces spustil. Efektivní identifikátor se používá k určení přístupových práv procesu k systémovým prostředkům (především prostředkům souborový systém). Obvykle jsou skutečné a efektivní identifikátory stejné, tzn. proces má v systému stejná práva jako uživatel, který jej spustil. Nastavením je však možné dát procesu více práv než uživatel SUID bit když je efektivní identifikátor nastaven na identifikátor vlastníka spustitelného souboru (např. vykořenit).

· Skutečné (GID) a efektivní (EGID) skupinové identifikátory. Skutečné ID skupiny se rovná primárnímu nebo aktuálnímu ID skupiny uživatele, který spustil proces. Efektivní identifikátor se používá k určení přístupových práv k systémovým prostředkům jménem skupiny. Obvykle je efektivní ID skupiny stejné jako skutečné. Ale pokud je spustitelný soubor nastaven na bit SGID, je takový soubor spuštěn s ID skutečné skupiny vlastníků.

LABORATORNÍ PRÁCE č. 3

MULTI-TASKING PROGRAMOVÁNÍ VLINUX

1. Cíl práce: Seznamte se s kompilátorem gcc, technikami ladění programů a funkcemi pro práci s procesy.

2. Stručné teoretické informace.

Minimální sada přepínačů kompilátoru gcc je - Wall (zobrazí všechny chyby a varování) a - o (výstupní soubor):

gcc - Zeď - o print_pid print_pid. C

Tým vytvoří spustitelný soubor print_pid.

Standardní knihovna C (libc, implementovaná v Linuxu v glibc) využívá možnosti multitaskingu Unix System V (dále jen SysV). V libc je typ pid_t definován jako celé číslo schopné obsahovat pid. Funkce, která hlásí pid aktuálního procesu, má prototyp pid_t getpid(void) a je definována spolu s pid_t v unistd. h a sys/typy. h).

Chcete-li vytvořit nový proces, použijte funkci vidlice:

pid_t fork(void)

Vložením zpoždění náhodné délky pomocí funkcí spánku a rand můžete jasněji vidět účinek multitaskingu:

to způsobí, že program „usne“ na náhodný počet sekund: od 0 do 3.

Chcete-li volat funkci jako podřízený proces, stačí ji zavolat po větvení:

// pokud běží podřízený proces, zavolejte funkci

pid=process(arg);

// ukončení procesu

Často je nutné spustit jiný program jako podřízený proces. Chcete-li to provést, použijte funkce rodiny exec:

// pokud běží podřízený proces, zavolejte program


if (execl(./file","file",arg, NULL)<0) {

printf("CHYBA při spouštění procesu\n");

else printf("proces zahájen (pid=%d)\n", pid);

// ukončení procesu

Rodičovský proces si často potřebuje vyměňovat informace se svými potomky, nebo se s nimi alespoň synchronizovat, aby mohl provádět operace ve správný čas. Jedním ze způsobů, jak synchronizovat procesy, jsou funkce wait a waitpid:

#zahrnout

#zahrnout

pid_t wait(int *status) - pozastaví provádění aktuálního procesu, dokud některý z jeho podřízených procesů neukončí.

pid_t waitpid (pid_t pid, int *stav, možnosti int) - pozastaví provádění aktuálního procesu, dokud se zadaný proces nedokončí, nebo nezkontroluje dokončení zadaného procesu.

Pokud potřebujete zjistit stav podřízeného procesu při jeho ukončení a hodnotu, kterou vrací, použijte makro WEXITSTATUS a jako parametr mu předejte stav podřízeného procesu.

status=waitpid(pid,&stav, WNOHANG);

if (pid == stav) (

printf("PID: %d, Výsledek = %d\n", pid, WEXITSTATUS(stav)); )

Ke změně priorit spawnovaných procesů se používá setpriorita a funkce. Priority jsou nastaveny v rozsahu od -20 (nejvyšší) do 20 (nejnižší), normální hodnota je 0. Pamatujte, že pouze superuživatel může zvýšit prioritu nad normální!

#zahrnout

#zahrnout

int proces(int i) (

setpriority(PRIO_PROCESS, getpid(),i);

printf("Proces %d ThreadID: %d pracující s prioritou %d\n",i, getpid(),getpriority(PRIO_PROCESS, getpid()));

return(getpriority(PRIO_PROCESS, getpid()));

Chcete-li zabít proces, použijte funkci kill:

#zahrnout

#zahrnout

int kill(pid_t pid, int sig);

Je-li pid > 0, pak udává PID procesu, do kterého je signál posílán. Je-li pid = 0, pak je signál odeslán všem procesům skupiny, do které aktuální proces patří.

sig - typ signálu. Některé typy signálů v Linuxu:

SIGKILL Tento signál způsobí okamžité ukončení procesu. Proces nemůže tento signál ignorovat.

SIGTERM Tento signál je požadavkem na ukončení procesu.

SIGCHLD Systém odešle tento signál procesu, když jeden z jeho podřízených procesů skončí. Příklad:

if (pid[i] == stav) (

printf("ID vlákna: %d ukončeno se stavem %d\n", pid[i], WEXITSTATUS(stav));

else kill(pid[i],SIGKILL);

3. Metodické pokyny.

3.1. Chcete-li se seznámit s možnostmi kompilátoru gcc a popisy funkcí jazyka C, použijte pokyny man a info.

3.2. K ladění programů je vhodné použít vestavěný editor správce souborů Půlnoční velitel(MC), který barevně zvýrazňuje různé jazykové konstrukce a v horním řádku obrazovky označuje pozici kurzoru v souboru (řádek, sloupec).

3.3. Správce souborů Midnight Commander má vyrovnávací paměť příkazů, kterou lze vyvolat klávesovou zkratkou - H, které lze posouvat pomocí kurzorových šipek (nahoru a dolů). Chcete-li vložit příkaz z vyrovnávací paměti do příkazového řádku, použijte klávesu , pro úpravu příkazu z tlačítek bufferu<- и ->, A .


3.4. Pamatujte, že aktuální adresář není obsažen v cestě, takže musíte program spustit jako "./print_pid" z příkazového řádku. V MC stačí najet na soubor a kliknout .

3.5. Chcete-li zobrazit výsledek provádění programu, použijte klávesovou zkratku - O. Pracují také v režimu úprav souborů.

3.6. Pro protokolování výsledků běhu programu je vhodné použít přesměrování výstupu z konzole do souboru: ./test > result. txt

3.7. Pro přístup k souborům vytvořeným na Linuxový server, použijte protokol ftp, jehož klientský program je k dispozici ve Windows 2000 a je vestavěn správce souborů DALEKO. V čem Účet a heslo je stejné jako při připojení přes ssh.

4.1. Seznamte se s možnostmi a metodami kompilátoru gcc pro ladění programů.

4.2. Pro varianty úloh z laboratorní práce č. 1 napište a odlaďte program, který vygenerovaný proces implementuje.

4.3. Pro možnosti úkolu od laboratorní práceČ. 1 píše a ladí program, který implementuje nadřazený proces, který volá a sleduje stav podřízených procesů - programů (čeká na jejich dokončení nebo je zničí, v závislosti na volbě).

4.4. Pro varianty úloh z laboratorní práce č. 1 napište a odlaďte program, který implementuje nadřazený proces, který volá a sleduje stav podřízených procesů - funkcí (čeká na své dokončení nebo je zničí, podle varianty).

5. Možnosti pro úkoly. Viz možnosti úloh z laboratorní práce č.1

6. Obsah zprávy.

6.1. Cíl práce.

6.2. Možnost úkolu.

6.3. Seznamy programů.

6.4. Protokoly provádění programu.

7. Kontrolní otázky.

7.1. Vlastnosti kompilace a spouštění programů C na Linuxu.

7.2. Co je pid, jak jej určit v operačním systému a programu?

7.3. Funkce vidličky - účel, použití, návratová hodnota.

7.4. Jak spustit funkci ve vytvořeném procesu? Program?

7.5. Způsoby synchronizace nadřazených a podřízených procesů.

7.6. Jak zjistit stav spawnovaného procesu při jeho ukončení a hodnotu, kterou vrací?

7.7. Jak řídit procesní priority?

7.8. Jak zabít proces v operačním systému a programu?

Pokračujeme v tématu multithreadingu v linuxovém jádře. Minule jsem mluvil o přerušeních, jejich zpracování a taskletech, a protože bylo původně zamýšleno, že to bude jeden článek, budu ve svém příběhu o workqueue odkazovat na tasklety, předpokládám, že je čtenář již zná.
Stejně jako minule se pokusím udělat svůj příběh co nejpodrobnější a nejpodrobnější.

Články v seriálu:

  1. Multitasking v jádře Linuxu: pracovní fronta

Pracovní fronta

Pracovní fronta- jedná se o složitější a těžší entity než tasklety. Ani se zde nebudu snažit popisovat všechny složitosti implementace, ale doufám, že to nejdůležitější rozeberu více či méně podrobně.
Pracovní fronty, stejně jako tasklety, slouží ke zpracování odloženého přerušení (ačkoli mohou být použity pro jiné účely), ale na rozdíl od taskletů se spouštějí v kontextu procesu jádra; nemusí tedy být atomické a mohou využívat režim spánku. () funkce, různé synchronizační nástroje atd.

Pojďme nejprve pochopit, jak je obecně organizován proces zpracování pracovní fronty. Obrázek to ukazuje velmi přibližně a zjednodušeně, jak se vše vlastně děje je podrobně popsáno níže.

Do této temné hmoty je zapojeno několik entit.
Za prvé, pracovní položka(jen zkráceně práce) je struktura, která popisuje funkci (například obsluhu přerušení), kterou chceme naplánovat. Lze si ji představit jako analogii struktury taskletu. Při plánování byly Tasklety přidány do front skrytých před uživatelem, ale nyní musíme použít speciální frontu - pracovní fronta.
Úkoly jsou rakovány funkcí plánovače a pracovní fronta je zpracovávána speciálními vlákny nazývanými pracovníci.
Pracovník's poskytují asynchronní provádění prací z pracovní fronty. Práci sice nazývají v pořadí rotace, ale v obecném případě nejde o striktní, sekvenční provádění: vždyť se zde odehrává preempce, spánek, čekání atd.

Obecně jsou pracovníci vlákny jádra, to znamená, že jsou řízeni hlavním plánovačem jádra Linuxu. Pracovníci však částečně zasahují do plánování dodatečné organizace paralelního provádění prací. To bude podrobněji probráno níže.

Abych nastínil hlavní možnosti mechanismu pracovní fronty, navrhuji prozkoumat API.

O frontě a jejím vytvoření

alloc_workqueue(fmt, příznaky, max_aktivní, argumenty...)
Parametry fmt a args jsou formát printf pro jméno a jeho argumenty. Parametr max_activate je zodpovědný za maximální počet prací, které lze z této fronty provést paralelně na jednom CPU.
Frontu lze vytvořit s následujícími příznaky:
  • WQ_HIGHPRI
  • WQ_UNBOUND
  • WQ_CPU_INTENSIVE
  • WQ_FREEZABLE
  • WQ_MEM_RECLAIM
Zvláštní pozornost by měla být věnována vlajce WQ_UNBOUND. Na základě přítomnosti tohoto příznaku se fronty dělí na vázané a nepřipojené.
V propojených frontách Po přidání jsou práce vázány na aktuální CPU, to znamená, že v takových frontách jsou práce prováděny na jádře, které ji naplánuje. V tomto ohledu vázané fronty připomínají tasklety.
V nepřipojených frontách práce lze provádět na jakémkoli jádru.

Důležitým rysem implementace pracovní fronty v jádře Linuxu je další organizace paralelního spouštění, která je přítomna ve vázaných frontách. Níže je to napsáno podrobněji, ale nyní řeknu, že je to provedeno tak, aby bylo použito co nejméně paměti a procesor nestál v nečinnosti. To vše je implementováno s předpokladem, že jedna práce nevyužívá příliš mnoho procesorových cyklů.
To neplatí pro nepřipojené fronty. Takové fronty v podstatě jednoduše poskytují pracovníkům kontext a spouštějí je co nejdříve.
Nepřipojené fronty by se tedy měly používat, pokud se očekává zatížení CPU, protože v tomto případě se plánovač postará o paralelní provádění na více jádrech.

Analogicky s tasklety lze pracím přiřadit prioritu provádění, normální nebo vysokou. Priorita je společná pro celou frontu. Ve výchozím nastavení má fronta normální prioritu, a pokud nastavíte příznak WQ_HIGHPRI, tedy podle toho vysoká.

Vlajka WQ_CPU_INTENSIVE má smysl pouze pro vázané fronty. Tento příznak je odmítnutím účasti na dodatečné organizaci paralelního provádění. Tento příznak by se měl použít, když se očekává, že práce bude spotřebovávat hodně času CPU, v takovém případě je lepší přesunout odpovědnost na plánovač. Toto je podrobněji popsáno níže.

Vlajky WQ_FREEZABLE A WQ_MEM_RECLAIM jsou specifické a přesahují rámec tématu, proto se jim nebudeme podrobně věnovat.

Někdy má smysl nevytvářet vlastní fronty, ale používat společné. Ty hlavní:

  • system_wq - vázaná fronta pro rychlou práci
  • system_long_wq - vázaná fronta pro práce, u kterých se očekává, že jejich provedení bude trvat dlouho
  • system_unbound_wq - nevázaná fronta

O práci a jejich plánování

Nyní se pojďme zabývat díly. Nejprve se podívejme na inicializační, deklarační a přípravná makra:
DECLARE(_DELAYED)_WORK(jméno, void (*funkce)(struct work_struct *work)); /* v době kompilace */ INIT(_DELAYED)_WORK(_work, _func); /* během provádění */ PŘIPRAVIT(_ODLOŽENO)_WORK(_work, _func); /* pro změnu vykonávané funkce */
Práce se přidávají do fronty pomocí funkcí:
bool queue_work(struct workqueue_struct *wq, struct work_struct *work); bool queue_delayed_work(struct workqueue_struct *wq, struct delayed_work *dwork, nepodepsané dlouhé zpoždění); /* práce bude přidána do fronty až po uplynutí prodlevy */
Stojí za to se nad tím podrobněji pozastavit. Přestože jako parametr zadáváme frontu, ve skutečnosti se práce neumisťují do samotné pracovní fronty, jak by se mohlo zdát, ale do úplně jiné entity – do seznamu front struktury worker_pool. Struktura worker_pool, je ve skutečnosti nejdůležitější entitou v organizaci mechanismu pracovní fronty, i když pro uživatele zůstává v pozadí. Právě s nimi dělníci pracují a právě v nich jsou obsaženy všechny základní informace.

Nyní se podívejme, jaké bazény jsou v systému.
Pro začátek, fondy pro vázané fronty (na obrázku). Pro každý CPU jsou staticky přiděleny dva fondy pracovníků: jeden pro práci s vysokou prioritou, druhý pro práci s normální prioritou. To znamená, že pokud máme čtyři jádra, bude zde pouze osm vázaných fondů, a to navzdory skutečnosti, že pracovních front může být tolik, kolik chcete.
Když vytvoříme pracovní frontu, má pro každý CPU přidělenou službu pool_workqueue(pwq). Každý takový pool_workqueue je spojen s pracovním fondem, který je alokován na stejném CPU a svou prioritou odpovídá typu fronty. Jejich prostřednictvím interaguje pracovní fronta s fondem pracovníků.
Pracovníci provádějí práci ze skupiny pracovníků bez rozdílu, aniž by rozlišovali, do které pracovní fronty původně patřili.

Pro nepřipojené fronty jsou fondy pracovníků alokovány dynamicky. Všechny fronty lze rozdělit do tříd ekvivalence podle jejich parametrů a pro každou takovou třídu je vytvořen vlastní pracovní fond. Přistupuje se k nim pomocí speciální hashovací tabulky, kde klíčem je sada parametrů a hodnotou je fond pracovníků.
Ve skutečnosti je pro nevázané fronty všechno trochu komplikovanější: pokud pro vázané fronty byly vytvořeny pwq a fronty pro každý CPU, zde jsou vytvořeny pro každý uzel NUMA, ale toto je další optimalizace, kterou nebudeme podrobně zvažovat.

Všemožné maličkosti

Uvedu také několik funkcí z API pro doplnění obrázku, ale nebudu o nich mluvit podrobně:
/* Vynutit dokončení */ bool flush_work(struct work_struct *work); bool flush_delayed_work(struct delayed_work *dwork); /* Zrušit provádění práce */ bool cancel_work_sync(struct work_struct *work); bool cancel_delayed_work(struct delayed_work *dwork); bool cancel_delayed_work_sync(struct delayed_work *dwork); /* Smazat frontu */ void zničit_pracovní fronta(struct workqueue_struct *wq);

Jak dělníci dělají svou práci

Nyní, když jsme se seznámili s API, zkusme podrobněji porozumět tomu, jak to celé funguje a je spravováno.
Každý fond má sadu pracovníků, kteří se zabývají úkoly. Počet pracovníků se navíc dynamicky mění a přizpůsobuje se aktuální situaci.
Jak jsme již zjistili, pracovníci jsou vlákna, která provádějí práci v kontextu jádra. Pracovník je načítá v pořadí, jeden po druhém, z fondu pracovníků, který je s ním spojený, a pracovníci, jak již víme, mohou patřit do různých zdrojových front.

Pracovníci mohou být podmíněně ve třech logických stavech: mohou být nečinní, běžící nebo řídící.
Dělník může nečinně stát a nic nedělat. To je například, když se již provádí veškerá práce. Když pracovník vstoupí do tohoto stavu, přejde do režimu spánku, a proto se neprovede, dokud nebude probuzen;
Pokud není správa fondu vyžadována a seznam naplánovaných prací není prázdný, pracovník je začne provádět. Takové pracovníky budeme konvenčně nazývat běh.
V případě potřeby se role ujímá pracovník manažer bazén. Fond může mít buď pouze jednoho řídícího pracovníka, nebo žádného pracovníka. Jeho úkolem je udržovat optimální počet pracovníků na bazén. Jak to dělá? Nejprve jsou odstraněni pracovníci, kteří byli dlouhou dobu nečinní. Za druhé, noví pracovníci se vytvářejí, pokud jsou současně splněny tři podmínky:

  • stále zbývají úkoly k dokončení (funguje v bazénu)
  • žádné nečinné pracovníky
  • nejsou žádní pracující pracovníci (tj. aktivní a nespící)
Poslední podmínka má však své vlastní nuance. Pokud nejsou fronty fondu připojeny, pak se neberou v úvahu běžící pracovníci, pro ně tato podmínka platí vždy. Totéž platí v případě, že pracovník provádí úkol z propojeného, ​​ale s příznakem WQ_CPU_INTENSIVE, fronty. Navíc v případě vázaných front, protože pracovníci pracují s díly ze společného fondu (který je jedním ze dvou pro každé jádro na obrázku výše), se ukazuje, že některé z nich se počítají jako pracovní a některé ne. Z toho také vyplývá, že provádění práce od WQ_CPU_INTENSIVE fronty se nemusí spustit okamžitě, ale samy nezasahují do provádění jiné práce. Nyní by mělo být jasné, proč se tento příznak tak nazývá a proč se používá, když očekáváme, že dokončení práce bude trvat dlouho.

Účetnictví pro pracující pracovníky se provádí přímo z hlavního plánovače linuxového jádra. Tento kontrolní mechanismus zajišťuje optimální úroveň souběžnosti, zabraňuje tomu, aby pracovní fronta vytvářela příliš mnoho pracovníků, ale také nezpůsobuje, že práce zbytečně dlouho čeká.

Zájemci se mohou podívat na funkci worker v jádře, jmenuje se worker_thread().

Všechny popsané funkce a struktury najdete podrobněji v souborech include/linux/workqueue.h, kernel/workqueue.c A kernel/workqueue_internal.h. K dispozici je také dokumentace o pracovní frontě Documentation/workqueue.txt.

Za zmínku také stojí, že mechanismus pracovní fronty se v jádře nepoužívá pouze pro zpracování odloženého přerušení (i když jde o poměrně běžný scénář).

Podívali jsme se tedy na mechanismy pro obsluhu odložených přerušení v linuxovém jádře – tasklet a workqueue, které jsou speciální formou multitaskingu. O přerušeních, úkolech a pracovních frontách se můžete dočíst v knize „Linux Device Drivers“ od Jonathana Corbeta, Grega Kroaha-Hartmana, Alessandra Rubiniho, i když jsou tam informace někdy zastaralé.

Pokračujeme v tématu multithreadingu v linuxovém jádře. Minule jsem mluvil o přerušeních, jejich zpracování a taskletech, a protože bylo původně zamýšleno, že to bude jeden článek, budu ve svém příběhu o workqueue odkazovat na tasklety, předpokládám, že je čtenář již zná.
Stejně jako minule se pokusím udělat svůj příběh co nejpodrobnější a nejpodrobnější.

Články v seriálu:

  1. Multitasking v jádře Linuxu: pracovní fronta

Pracovní fronta

Pracovní fronta- jedná se o složitější a těžší entity než tasklety. Ani se zde nebudu snažit popisovat všechny složitosti implementace, ale doufám, že to nejdůležitější rozeberu více či méně podrobně.
Pracovní fronty, stejně jako tasklety, slouží ke zpracování odloženého přerušení (ačkoli mohou být použity pro jiné účely), ale na rozdíl od taskletů se spouštějí v kontextu procesu jádra; nemusí tedy být atomické a mohou využívat režim spánku. () funkce, různé synchronizační nástroje atd.

Pojďme nejprve pochopit, jak je obecně organizován proces zpracování pracovní fronty. Obrázek to ukazuje velmi přibližně a zjednodušeně, jak se vše vlastně děje je podrobně popsáno níže.

Do této temné hmoty je zapojeno několik entit.
Za prvé, pracovní položka(jen zkráceně práce) je struktura, která popisuje funkci (například obsluhu přerušení), kterou chceme naplánovat. Lze si ji představit jako analogii struktury taskletu. Při plánování byly Tasklety přidány do front skrytých před uživatelem, ale nyní musíme použít speciální frontu - pracovní fronta.
Úkoly jsou rakovány funkcí plánovače a pracovní fronta je zpracovávána speciálními vlákny nazývanými pracovníci.
Pracovník's poskytují asynchronní provádění prací z pracovní fronty. Práci sice nazývají v pořadí rotace, ale v obecném případě nejde o striktní, sekvenční provádění: vždyť se zde odehrává preempce, spánek, čekání atd.

Obecně jsou pracovníci vlákny jádra, to znamená, že jsou řízeni hlavním plánovačem jádra Linuxu. Pracovníci však částečně zasahují do plánování dodatečné organizace paralelního provádění prací. To bude podrobněji probráno níže.

Abych nastínil hlavní možnosti mechanismu pracovní fronty, navrhuji prozkoumat API.

O frontě a jejím vytvoření

alloc_workqueue(fmt, příznaky, max_aktivní, argumenty...)
Parametry fmt a args jsou formát printf pro jméno a jeho argumenty. Parametr max_activate je zodpovědný za maximální počet prací, které lze z této fronty provést paralelně na jednom CPU.
Frontu lze vytvořit s následujícími příznaky:
  • WQ_HIGHPRI
  • WQ_UNBOUND
  • WQ_CPU_INTENSIVE
  • WQ_FREEZABLE
  • WQ_MEM_RECLAIM
Zvláštní pozornost by měla být věnována vlajce WQ_UNBOUND. Na základě přítomnosti tohoto příznaku se fronty dělí na vázané a nepřipojené.
V propojených frontách Po přidání jsou práce vázány na aktuální CPU, to znamená, že v takových frontách jsou práce prováděny na jádře, které ji naplánuje. V tomto ohledu vázané fronty připomínají tasklety.
V nepřipojených frontách práce lze provádět na jakémkoli jádru.

Důležitým rysem implementace pracovní fronty v jádře Linuxu je další organizace paralelního spouštění, která je přítomna ve vázaných frontách. Níže je to napsáno podrobněji, ale nyní řeknu, že je to provedeno tak, aby bylo použito co nejméně paměti a procesor nestál v nečinnosti. To vše je implementováno s předpokladem, že jedna práce nevyužívá příliš mnoho procesorových cyklů.
To neplatí pro nepřipojené fronty. Takové fronty v podstatě jednoduše poskytují pracovníkům kontext a spouštějí je co nejdříve.
Nepřipojené fronty by se tedy měly používat, pokud se očekává zatížení CPU, protože v tomto případě se plánovač postará o paralelní provádění na více jádrech.

Analogicky s tasklety lze pracím přiřadit prioritu provádění, normální nebo vysokou. Priorita je společná pro celou frontu. Ve výchozím nastavení má fronta normální prioritu, a pokud nastavíte příznak WQ_HIGHPRI, tedy podle toho vysoká.

Vlajka WQ_CPU_INTENSIVE má smysl pouze pro vázané fronty. Tento příznak je odmítnutím účasti na dodatečné organizaci paralelního provádění. Tento příznak by se měl použít, když se očekává, že práce bude spotřebovávat hodně času CPU, v takovém případě je lepší přesunout odpovědnost na plánovač. Toto je podrobněji popsáno níže.

Vlajky WQ_FREEZABLE A WQ_MEM_RECLAIM jsou specifické a přesahují rámec tématu, proto se jim nebudeme podrobně věnovat.

Někdy má smysl nevytvářet vlastní fronty, ale používat společné. Ty hlavní:

  • system_wq - vázaná fronta pro rychlou práci
  • system_long_wq - vázaná fronta pro práce, u kterých se očekává, že jejich provedení bude trvat dlouho
  • system_unbound_wq - nevázaná fronta

O práci a jejich plánování

Nyní se pojďme zabývat díly. Nejprve se podívejme na inicializační, deklarační a přípravná makra:
DECLARE(_DELAYED)_WORK(jméno, void (*funkce)(struct work_struct *work)); /* v době kompilace */ INIT(_DELAYED)_WORK(_work, _func); /* během provádění */ PŘIPRAVIT(_ODLOŽENO)_WORK(_work, _func); /* pro změnu vykonávané funkce */
Práce se přidávají do fronty pomocí funkcí:
bool queue_work(struct workqueue_struct *wq, struct work_struct *work); bool queue_delayed_work(struct workqueue_struct *wq, struct delayed_work *dwork, nepodepsané dlouhé zpoždění); /* práce bude přidána do fronty až po uplynutí prodlevy */
Stojí za to se nad tím podrobněji pozastavit. Přestože jako parametr zadáváme frontu, ve skutečnosti se práce neumisťují do samotné pracovní fronty, jak by se mohlo zdát, ale do úplně jiné entity – do seznamu front struktury worker_pool. Struktura worker_pool, je ve skutečnosti nejdůležitější entitou v organizaci mechanismu pracovní fronty, i když pro uživatele zůstává v pozadí. Právě s nimi dělníci pracují a právě v nich jsou obsaženy všechny základní informace.

Nyní se podívejme, jaké bazény jsou v systému.
Pro začátek, fondy pro vázané fronty (na obrázku). Pro každý CPU jsou staticky přiděleny dva fondy pracovníků: jeden pro práci s vysokou prioritou, druhý pro práci s normální prioritou. To znamená, že pokud máme čtyři jádra, bude zde pouze osm vázaných fondů, a to navzdory skutečnosti, že pracovních front může být tolik, kolik chcete.
Když vytvoříme pracovní frontu, má pro každý CPU přidělenou službu pool_workqueue(pwq). Každý takový pool_workqueue je spojen s pracovním fondem, který je alokován na stejném CPU a svou prioritou odpovídá typu fronty. Jejich prostřednictvím interaguje pracovní fronta s fondem pracovníků.
Pracovníci provádějí práci ze skupiny pracovníků bez rozdílu, aniž by rozlišovali, do které pracovní fronty původně patřili.

Pro nepřipojené fronty jsou fondy pracovníků alokovány dynamicky. Všechny fronty lze rozdělit do tříd ekvivalence podle jejich parametrů a pro každou takovou třídu je vytvořen vlastní pracovní fond. Přistupuje se k nim pomocí speciální hashovací tabulky, kde klíčem je sada parametrů a hodnotou je fond pracovníků.
Ve skutečnosti je pro nevázané fronty všechno trochu komplikovanější: pokud pro vázané fronty byly vytvořeny pwq a fronty pro každý CPU, zde jsou vytvořeny pro každý uzel NUMA, ale toto je další optimalizace, kterou nebudeme podrobně zvažovat.

Všemožné maličkosti

Uvedu také několik funkcí z API pro doplnění obrázku, ale nebudu o nich mluvit podrobně:
/* Vynutit dokončení */ bool flush_work(struct work_struct *work); bool flush_delayed_work(struct delayed_work *dwork); /* Zrušit provádění práce */ bool cancel_work_sync(struct work_struct *work); bool cancel_delayed_work(struct delayed_work *dwork); bool cancel_delayed_work_sync(struct delayed_work *dwork); /* Smazat frontu */ void zničit_pracovní fronta(struct workqueue_struct *wq);

Jak dělníci dělají svou práci

Nyní, když jsme se seznámili s API, zkusme podrobněji porozumět tomu, jak to celé funguje a je spravováno.
Každý fond má sadu pracovníků, kteří se zabývají úkoly. Počet pracovníků se navíc dynamicky mění a přizpůsobuje se aktuální situaci.
Jak jsme již zjistili, pracovníci jsou vlákna, která provádějí práci v kontextu jádra. Pracovník je načítá v pořadí, jeden po druhém, z fondu pracovníků, který je s ním spojený, a pracovníci, jak již víme, mohou patřit do různých zdrojových front.

Pracovníci mohou být podmíněně ve třech logických stavech: mohou být nečinní, běžící nebo řídící.
Dělník může nečinně stát a nic nedělat. To je například, když se již provádí veškerá práce. Když pracovník vstoupí do tohoto stavu, přejde do režimu spánku, a proto se neprovede, dokud nebude probuzen;
Pokud není správa fondu vyžadována a seznam naplánovaných prací není prázdný, pracovník je začne provádět. Takové pracovníky budeme konvenčně nazývat běh.
V případě potřeby se role ujímá pracovník manažer bazén. Fond může mít buď pouze jednoho řídícího pracovníka, nebo žádného pracovníka. Jeho úkolem je udržovat optimální počet pracovníků na bazén. Jak to dělá? Nejprve jsou odstraněni pracovníci, kteří byli dlouhou dobu nečinní. Za druhé, noví pracovníci se vytvářejí, pokud jsou současně splněny tři podmínky:

  • stále zbývají úkoly k dokončení (funguje v bazénu)
  • žádné nečinné pracovníky
  • nejsou žádní pracující pracovníci (tj. aktivní a nespící)
Poslední podmínka má však své vlastní nuance. Pokud nejsou fronty fondu připojeny, pak se neberou v úvahu běžící pracovníci, pro ně tato podmínka platí vždy. Totéž platí v případě, že pracovník provádí úkol z propojeného, ​​ale s příznakem WQ_CPU_INTENSIVE, fronty. Navíc v případě vázaných front, protože pracovníci pracují s díly ze společného fondu (který je jedním ze dvou pro každé jádro na obrázku výše), se ukazuje, že některé z nich se počítají jako pracovní a některé ne. Z toho také vyplývá, že provádění práce od WQ_CPU_INTENSIVE fronty se nemusí spustit okamžitě, ale samy nezasahují do provádění jiné práce. Nyní by mělo být jasné, proč se tento příznak tak nazývá a proč se používá, když očekáváme, že dokončení práce bude trvat dlouho.

Účetnictví pro pracující pracovníky se provádí přímo z hlavního plánovače linuxového jádra. Tento kontrolní mechanismus zajišťuje optimální úroveň souběžnosti, zabraňuje tomu, aby pracovní fronta vytvářela příliš mnoho pracovníků, ale také nezpůsobuje, že práce zbytečně dlouho čeká.

Zájemci se mohou podívat na funkci worker v jádře, jmenuje se worker_thread().

Všechny popsané funkce a struktury najdete podrobněji v souborech include/linux/workqueue.h, kernel/workqueue.c A kernel/workqueue_internal.h. K dispozici je také dokumentace o pracovní frontě Documentation/workqueue.txt.

Za zmínku také stojí, že mechanismus pracovní fronty se v jádře nepoužívá pouze pro zpracování odloženého přerušení (i když jde o poměrně běžný scénář).

Podívali jsme se tedy na mechanismy pro obsluhu odložených přerušení v linuxovém jádře – tasklet a workqueue, které jsou speciální formou multitaskingu. O přerušeních, úkolech a pracovních frontách se můžete dočíst v knize „Linux Device Drivers“ od Jonathana Corbeta, Grega Kroaha-Hartmana, Alessandra Rubiniho, i když jsou tam informace někdy zastaralé.