Principer för drift i UNIX-liknande operativsystem med Linux som exempel. Skapa din egen lätta process

Ett av de mest irriterande ögonblicken när man flyttar från miljö till Windows baserad för användning kommandorad– förlust av enkel multitasking. Även på Linux, om du använder X Window-systemet, kan du använda musen för att helt enkelt klicka på nytt program och öppna den. På kommandoraden har du dock ganska mycket fastnat för monotasking. I den här artikeln kommer vi att visa dig hur man multitaskar i Linux med kommandoraden.

Bakgrund och prioriterad processledning

Det finns dock fortfarande sätt att multitaska i Linux, och vissa av dem är mer omfattande än andra. En inbyggd metod som inte kräver något extra programvara, är helt enkelt att flytta processer i bakgrunden och i förgrunden. Vi pratar om det här. Det har dock vissa nackdelar.

Oskyddad

för det första För att skicka en process i bakgrunden måste du först avbryta den. Det är inte möjligt att skicka ett redan kört program i bakgrunden och underhålla det samtidigt.

För det andra måste du bryta arbetsflödet för att starta ett nytt kommando. Du måste ta dig ur det du gör just nu och skriva in fler kommandon i skalet. Det fungerar, men det är obekvämt.

Tredje, bör du övervaka utdata från bakgrundsprocesser. Alla utdata från dem kommer att visas på kommandoraden och kommer att störa vad du för närvarande gör. Så bakgrundsuppgifter måste antingen omdirigera sina utdata till separat fil, eller så måste de vara helt inaktiverade.

På grund av dessa brister finns det enorma problem med att hantera bakgrunden och förgrundsprocessen. Det bästa beslutet– använd kommandoradsverktygets "skärm" som visas nedan.

Men först - du kommer att öppna en ny SSH-session

Glöm inte att du precis öppnar en ny SSH-session.

Det kan vara obekvämt att öppna nya sessioner hela tiden. Och det är då du behöver "skärm"

Verktyg skärm låter dig skapa flera arbetsflöden öppna samtidigt - den närmast analogen till "fönster". Som standard är den tillgänglig i vanliga Linux-förråd. Installera det på CentOS/RHEL med följande kommando:

Sudo yum installationsskärm

Öppnar en ny skärm

Starta nu din session genom att skriva "skärm".

Detta kommer att skapa tomt fönster inom en befintlig SSH-session och ge den ett nummer som visas i rubriken så här:

Vår skärm här är numrerad "0" som visas. I den här skärmdumpen använder vi ett dummy "läs"-kommando för att blockera terminalen och få den att vänta på input. Låt oss nu säga att vi vill göra något annat medan vi väntar.

Att öppna ny skärm och gör något annat, vi skriver ut:

Ctrl+a c

"ctrl+a" är standardtangentkombinationen för att kontrollera skärmar i skärmprogrammet. Vad du skriver efter det avgör åtgärden. Till exempel:

  • ctrl+a c – C aktiverar en ny skärm
  • ctrl+a [siffra]– hoppa till ett specifikt skärmnummer
  • ctrl+a k – K stänger av den aktuella skärmen
  • ctrl+a n – Gå till skärmen n
  • ctrl+a "- visar alla aktiva skärmar i sessionen

Om vi ​​trycker på "ctrl+a c" får vi en ny skärm med ett nytt nummer.

Du kan använda markörknapparna för att navigera genom listan och gå till den skärm du vill ha.
Skärmar är det närmaste du kommer "windows", som ett system i ett kommando Linux-sträng. Naturligtvis är det inte så enkelt som att klicka med en mus, men det grafiska delsystemet är mycket resurskrävande. Med skärmar kan du få nästan samma funktionalitet och möjliggöra full multitasking!

Processer i UNIX

I UNIX är det huvudsakliga sättet att organisera och enheten för multitasking processen. Operativsystemet manipulerar processbilden, som representerar programkoden, såväl som processdatasektionerna som definierar exekveringsmiljön.

Under exekvering eller medan man väntar "i kulisserna" finns processer i virtuellt minne med en sidorganisation. En del av detta virtuella minne mappas till fysiskt minne. En del av det fysiska minnet är reserverat för operativsystemets kärna. Användare kan bara komma åt det minne som återstår för processer. Vid behov byts processminnessidor från fysiskt minne till disk, till växlingsområdet. Vid åtkomst till en sida i virtuellt minne, om den inte finns i fysiskt minne, byts den från disk.

Virtuellt minne implementeras och underhålls automatiskt av UNIX-kärnan.

Processtyper

Det finns tre typer av processer i UNIX-operativsystem: systemisk, demonprocesser Och ansökningsprocesser.

Systemprocesserär en del av kärnan och finns alltid i random access minne. Systemprocesser har inte motsvarande program i form av körbara filer och startas på ett speciellt sätt när systemkärnan initieras. De exekverande instruktionerna och data för dessa processer finns i systemets kärna, så de kan anropa funktioner och komma åt data som andra processer inte kan komma åt.

Systemprocesser inkluderar processen för initial initiering, i det, som är stamfadern till alla andra processer. Fastän i det inte är en del av kärnan, och dess körning sker från en körbar fil, är dess funktion avgörande för att hela systemet ska fungera som helhet.

Demoner- dessa är icke-interaktiva processer som startas på vanligt sätt - genom att ladda sina motsvarande program i minnet, och exekveras i bakgrunden. Vanligtvis startas demoner under systeminitiering, men efter att kärnan har initierats, och säkerställer driften av olika UNIX-undersystem: terminalåtkomstsystem, utskriftssystem, nätverkstjänster etc. Demoner är inte associerade med någon användare. För det mesta väntar demoner på att en eller annan process ska begära specifik tjänst.



TILL ansökningsprocesser inkluderar alla andra processer som körs på systemet. Vanligtvis är dessa processer som skapas inom en användarsession. Den viktigaste användarprocessen är initial kommandotolk, som tillhandahåller exekvering av användarkommandon på ett UNIX-system.

Användarprocesser kan köras i både interaktiva (förebyggande) och bakgrundslägen. Interaktiva processer har exklusivt ägande av terminalen, och tills en sådan process slutförs har användaren ingen tillgång till kommandoraden.

Processattribut

En UNIX-process har ett antal attribut som gör det möjligt operativ system sköta sitt arbete. Huvudattribut:

· Process ID (PID), vilket gör att systemkärnan kan skilja mellan processer. När den skapas ny process, tilldelar kärnan den nästa lediga (d.v.s. inte associerad med någon process) identifierare. Tilldelningen av en identifierare sker vanligtvis i stigande ordning, d.v.s. ID:t för den nya processen är större än ID:t för processen som skapades före den. Om ID når det maximala värdet (vanligtvis 65737), kommer nästa process att ta emot den lägsta lediga PID och cykeln upprepas. När en process avslutas släpper kärnan identifieraren som den använde.

· Parent Process ID (PPID)– identifierare för processen som ledde till denna process. Alla processer i systemet utom systemprocesser och process i det, som är stamfadern till de återstående processerna, genereras av en av de befintliga eller tidigare existerande processerna.

· Prioritetskorrigering (NI)– processens relativa prioritet, beaktad av schemaläggaren när startordningen fastställs. Den faktiska fördelningen av processorresurser bestäms av exekveringsprioriteten (attribut PRI), beroende på flera faktorer, särskilt på den givna relativa prioritet. Den relativa prioriteten ändras inte av systemet under hela processen, även om den kan ändras av användaren eller administratören när processen startar med kommandot trevlig. Intervallet för prioritetsstegringsvärden på de flesta system är -20 till 20. Om inget inkrement anges används standardvärdet 10. Ett positivt inkrement innebär en minskning av den aktuella prioriteten. Vanliga användare kan bara ställa in ett positivt inkrement och därmed bara minska prioriteten. Användare rot kan ställa in ett negativt inkrement, vilket ökar prioritet för processen och därmed bidrar till dess högre snabbt arbete. Till skillnad från relativ prioritet ändras exekveringsprioriteten för en process dynamiskt av schemaläggaren.

· Terminal Line (TTY)– en terminal eller pseudoterminal associerad med en process. Denna terminal är associerad med standard strömmar: inmatning, ledig dag Och meddelandeflöde om fel. Strömmar ( programkanaler) är standardmedel interprocesskommunikation i UNIX OS. Demonprocesser är inte associerade med en terminal.

· Verkliga (UID) och effektiva (EUID) användaridentifierare. Det verkliga användar-ID:t för en given process är ID:t för användaren som startade processen. Den effektiva identifieraren används för att bestämma processens åtkomsträttigheter till systemresurser (i första hand resurser filsystem). Vanligtvis är de verkliga och effektiva identifierarna desamma, dvs. processen har samma rättigheter i systemet som användaren som startade den. Det är dock möjligt att ge en process fler rättigheter än användaren genom att ställa in SUID bit när den effektiva identifieraren är inställd på identifieraren för ägaren av den körbara filen (till exempel användaren rot).

· Verkliga (GID) och effektiva (EGID) gruppidentifierare. Det verkliga grupp-ID:t är lika med det primära eller nuvarande grupp-ID:t för användaren som startade processen. Den effektiva identifieraren används för att bestämma åtkomsträttigheter till systemresurser på uppdrag av en grupp. Vanligtvis är det effektiva grupp-ID:t detsamma som det riktiga. Men om den körbara filen är inställd på SGID-bit, körs en sådan fil med det effektiva ägargrupps-ID.

LABORATORIEARBETE Nr 3

MULTI-UPPGIFTER PROGRAMMERING ILINUX

1. Målet med arbetet: Bekanta dig med gcc-kompilatorn, programfelsökningstekniker och funktioner för att arbeta med processer.

2. Kort teoretisk information.

Den minsta uppsättningen av gcc-kompilatoromkopplare är - Wall (visa alla fel och varningar) och -o (utgångsfil):

gcc - Wall - o print_pid print_pid. c

Teamet kommer att skapa körbar fil print_pid.

C-standardbiblioteket (libc, implementerat i Linux i glibc) drar fördel av multitasking-funktionerna i Unix System V (nedan kallat SysV). I libc är typen pid_t definierad som ett heltal som kan innehålla en pid. Funktionen som rapporterar pid för den aktuella processen har en prototyp av pid_t getpid(void) och definieras tillsammans med pid_t i unistd. h och sys/typer. h).

För att skapa en ny process, använd gaffelfunktionen:

pid_t fork(void)

Genom att infoga en fördröjning av slumpmässig längd med sömn- och randfunktionerna kan du se effekten av multitasking tydligare:

detta kommer att få programmet att "sova" i ett slumpmässigt antal sekunder: från 0 till 3.

För att anropa en funktion som en underordnad process, anropa den bara efter förgrening:

// om en underordnad process körs, anropa funktionen

pid=process(arg);

// avsluta processen

Ofta är det nödvändigt att köra ett annat program som en underordnad process. För att göra detta, använd exec-familjens funktioner:

// om en underordnad process körs, anropa programmet


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

printf("FEL vid startprocessen\n");

else printf("processen startade (pid=%d)\n", pid);

// avsluta processen

Ofta behöver en föräldraprocess utbyta information med sina barn, eller åtminstone synkronisera med dem, för att kunna utföra operationer vid rätt tidpunkt. Ett sätt att synkronisera processer är med funktionerna wait och waitpid:

#omfatta

#omfatta

pid_t wait(int *status) - avbryter exekveringen av den aktuella processen tills någon av dess underordnade processer avslutas.

pid_t waitpid (pid_t pid, int *status, int optioner) - avbryter exekveringen av den aktuella processen tills den specificerade processen slutförs eller kontrollerar att den specificerade processen har slutförts.

Om du behöver ta reda på statusen för den underordnade processen när den avslutas och värdet den returnerar, använd sedan WEXITSTATUS-makrot och skicka den statusen för den underordnade processen som en parameter.

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

if (pid == status) (

printf("PID: %d, Resultat = %d\n", pid, WEXITSTATUS(status)); )

För att ändra prioriteterna för skapade processer, används inställningsprioriteten och funktionerna. Prioriteter sätts i intervallet från -20 (högst) till 20 (lägst), normalvärdet är 0. Observera att endast en superanvändare kan öka prioriteten över det normala!

#omfatta

#omfatta

int process(int i) (

setpriority(PRIO_PROCESS, getpid(),i);

printf("Bearbeta %d tråd-ID: %d arbetar med prioritet %d\n",i, getpid(),getpriority(PRIO_PROCESS, getpid()));

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

För att döda en process, använd kill-funktionen:

#omfatta

#omfatta

int kill(pid_t pid, int sig);

Om pid > 0, så specificerar den PID för processen till vilken signalen skickas. Om pid = 0, så skickas signalen till alla processer i gruppen som den aktuella processen tillhör.

sig - signaltyp. Några typer av signaler i Linux:

SIGKILL Denna signal gör att processen avslutas omedelbart. Processen kan inte ignorera denna signal.

SIGTERM Denna signal är en begäran om att avsluta processen.

SIGCHLD Systemet skickar denna signal till en process när en av dess underordnade processer avslutas. Exempel:

if (pid[i] == status) (

printf("Tråd-ID: %d klar med status %d\n", pid[i], WEXITSTATUS(status));

else kill(pid[i],SIGKILL);

3. Metodiska instruktioner.

3.1. För att bekanta dig med gcc-kompilatorns alternativ och beskrivningar av C-språkfunktioner, använd man och info-instruktionerna.

3.2. För att felsöka program är det bekvämt att använda den inbyggda redigeraren i filhanteraren Midnattschef(MC), som framhäver olika språkkonstruktioner i färg och indikerar markörens position i filen (rad, kolumn) på skärmens översta rad.

3.3. Midnight Commander-filhanteraren har en kommandobuffert som kan anropas med en kortkommando - H, som kan flyttas med markörpilarna (upp och ner). För att infoga ett kommando från bufferten i kommandoraden, använd tangenten , för att redigera ett kommando från buffert-tangenterna<- и ->, Och .


3.4. Kom ihåg att den aktuella katalogen inte finns i sökvägen, så du måste köra programmet som "./print_pid" från kommandoraden. I MC håller du bara muspekaren över filen och klickar .

3.5. Använd kortkommandot för att se resultatet av programkörningen - O. De fungerar också i filredigeringsläge.

3.6. För att logga resultaten av programkörningen är det lämpligt att använda omdirigering av utdata från konsolen till en fil: ./test > resultat. Text

3.7. För att komma åt filer skapade på Linux-server, använd ftp-protokollet, vars klientprogram är tillgängligt i Windows 2000 och är inbyggt i filhanterare LÅNGT. Vart i konto och lösenordet är detsamma som när du ansluter via ssh.

4.1. Bekanta dig med gcc-kompilatorns alternativ och metoder för att felsöka program.

4.2. För varianter av uppgifter från laboratoriearbete nr 1, skriv och felsök ett program som implementerar den genererade processen.

4.3. För uppgiftsalternativ från laboratoriearbete Nr 1 skriv och felsök ett program som implementerar en överordnad process som anropar och övervakar tillståndet för underordnade processer - program (väntar på att de ska slutföra eller förstör dem, beroende på alternativet).

4.4. För varianter av uppgifter från laboratoriearbete nr 1, skriv och felsök ett program som implementerar en överordnad process som anropar och övervakar tillståndet för underordnade processer - funktioner (väntar på att de slutförs eller förstör dem, beroende på variant).

5. Alternativ för uppgifter. Se alternativ för uppgifter från laborationer nr 1

6. Rapportens innehåll.

6.1. Målet med arbetet.

6.2. Uppgiftsalternativ.

6.3. Programlistor.

6.4. Protokoll för programexekvering.

7. Kontrollfrågor.

7.1. Funktioner för att kompilera och köra C-program på Linux.

7.2. Vad är pid, hur bestämmer man det i operativsystemet och programmet?

7.3. Gaffelfunktion - syfte, tillämpning, returvärde.

7.4. Hur kör man en funktion i en skapad process? Program?

7.5. Sätt att synkronisera föräldra- och barnprocesser.

7.6. Hur tar man reda på tillståndet för den skapade processen när den avslutas och värdet den returnerar?

7.7. Hur hanterar man processprioriteringar?

7.8. Hur dödar man en process i operativsystemet och programmet?

Vi fortsätter ämnet multithreading i Linux-kärnan. Förra gången pratade jag om avbrott, deras bearbetning och tasklets, och eftersom det ursprungligen var meningen att detta skulle vara en artikel, kommer jag i min berättelse om workqueue att referera till tasklets, förutsatt att läsaren redan är bekant med dem.
Som förra gången ska jag försöka göra min berättelse så detaljerad och detaljerad som möjligt.

Artiklar i serien:

  1. Multitasking i Linux-kärnan: arbetskö

Arbetskö

Arbetskö- Dessa är mer komplexa och tunga enheter än tasklets. Jag kommer inte ens försöka beskriva alla krångligheterna med implementeringen här, men jag hoppas att jag kommer att analysera de viktigaste sakerna mer eller mindre i detalj.
Arbetsköer, som uppgiftsläsar, tjänar till fördröjd avbrottsbearbetning (även om de kan användas för andra ändamål), men till skillnad från uppgiftsläsar, exekveras de i sammanhanget av en kärnprocess; följaktligen behöver de inte vara atomära och kan använda sömn () funktion, olika synkroniseringsverktyg etc.

Låt oss först förstå hur arbetsköbehandlingsprocessen är organiserad i allmänhet. Bilden visar det väldigt ungefärligt och förenklat, hur allt faktiskt går till beskrivs i detalj nedan.

Flera enheter är involverade i denna mörka materia.
För det första, arbets objekt(bara arbeta för kort) är en struktur som beskriver funktionen (till exempel en avbrottshanterare) som vi vill schemalägga. Det kan ses som en analog till tasklet-strukturen. Vid schemaläggning lades uppgifter till i köer som var dolda för användaren, men nu måste vi använda en speciell kö - arbetskö.
Uppgifter rakas av schemaläggarfunktionen och arbetskön bearbetas av speciella trådar som kallas arbetare.
Arbetstagare's ger asynkron exekvering av arbeten från arbetskön. Även om de kallar arbete i rotationsordning, är det i det allmänna fallet inte fråga om strikt, sekventiell avrättning: här sker trots allt förköp, sömn, väntan etc.

I allmänhet är arbetare kärntrådar, det vill säga de styrs av Linux-kärnschemaläggaren. Men arbetare ingriper delvis i planeringen av ytterligare organisering av parallellt utförande av arbete. Detta kommer att diskuteras mer i detalj nedan.

För att beskriva de huvudsakliga funktionerna i arbetskömekanismen föreslår jag att du utforskar API:et.

Om kön och dess tillkomst

alloc_workqueue(fmt, flaggor, max_active, args...)
Parametrarna fmt och args är printf-formatet för namnet och argumenten till det. Parametern max_activate är ansvarig för det maximala antalet arbeten som från denna kö kan exekveras parallellt på en CPU.
En kö kan skapas med följande flaggor:
  • WQ_HIGHPRI
  • WQ_UNBOUND
  • WQ_CPU_INTENSIVE
  • WQ_FREEZABLE
  • WQ_MEM_RECLAIM
Särskild uppmärksamhet bör ägnas flaggan WQ_UNBOUND. Baserat på närvaron av denna flagga delas köerna in i bundna och obundna.
I länkade köer När de läggs till är arbeten bundna till den aktuella CPU:n, det vill säga i sådana köer exekveras arbeten på kärnan som schemalägger det. I detta avseende liknar bundna köer tasklets.
I obundna köer arbete kan utföras på vilken kärna som helst.

En viktig funktion i arbetsköimplementeringen i Linux-kärnan är den extra organisationen av parallellkörning som finns i bundna köer. Det skrivs mer i detalj nedan, men nu kommer jag att säga att det är utfört på ett sådant sätt att så lite minne som möjligt används, och så att processorn inte står i viloläge. Allt detta implementeras med antagandet att ett verk inte använder för många processorcykler.
Detta är inte fallet för obundna köer. I grund och botten ger sådana köer helt enkelt sammanhang till arbetarna och startar dem så tidigt som möjligt.
Således bör obundna köer användas om CPU-intensiv arbetsbelastning förväntas, eftersom schemaläggaren i detta fall kommer att ta hand om parallell exekvering på flera kärnor.

I analogi med tasklets kan verk tilldelas exekveringsprioritet, normal eller hög. Prioriteten är gemensam för hela kön. Som standard har kön normal prioritet, och om du ställer in flaggan WQ_HIGHPRI, då, följaktligen, hög.

Flagga WQ_CPU_INTENSIVEär bara vettigt för bundna köer. Denna flagga är en vägran att delta i en ytterligare organisation av parallellt utförande. Denna flagga bör användas när arbetet förväntas ta mycket CPU-tid, i vilket fall det är bättre att flytta ansvaret till schemaläggaren. Detta beskrivs mer i detalj nedan.

Flaggor WQ_FREEZABLE Och WQ_MEM_RECLAIMär specifika och utanför ämnets omfattning, så vi kommer inte att uppehålla oss i detalj.

Ibland är det vettigt att inte skapa sina egna köer, utan att använda vanliga. De viktigaste:

  • system_wq - bunden kö för snabbt arbete
  • system_long_wq - en bunden kö för arbeten som förväntas ta lång tid att utföra
  • system_unbound_wq - obunden kö

Om arbetet och deras planering

Låt oss nu ta itu med verken. Låt oss först titta på initierings-, deklarations- och förberedelsemakron:
DECLARE(_DELAYED)_WORK(namn, void (*funktion)(struct work_struct *work)); /* vid kompileringstid */ INIT(_DELAYED)_WORK(_work, _func); /* under körning */ PREPARE(_DELAYED)_WORK(_work, _func); /* för att ändra funktionen som körs */
Verk läggs till i kön med hjälp av funktionerna:
bool queue_work(struct workqueue_struct *wq, struct work_struct *work); bool queue_delayed_work(struct workqueue_struct *wq, struct delayed_work *dwork, osignerad lång fördröjning); /* arbete kommer att läggas till i kön först efter att förseningen har gått ut */
Detta är värt att uppehålla sig mer i detalj. Även om vi anger en kö som en parameter, placeras faktiskt inte arbeten i själva arbetskön, som det kan tyckas, utan i en helt annan enhet - i kölistan för worker_pool-strukturen. Strukturera worker_pool, i själva verket är den viktigaste enheten för att organisera arbetskömekanismen, även om den för användaren förblir bakom kulisserna. Det är med dem som arbetare arbetar, och det är i dem som all grundläggande information finns.

Låt oss nu se vilka pooler som finns i systemet.
Till att börja med pooler för bundna köer (på bilden). För varje CPU är två arbetarpooler statiskt allokerade: en för högprioriterat arbete, den andra för arbete med normal prioritet. Det vill säga om vi har fyra kärnor så blir det bara åtta bundna pooler, trots att det kan finnas hur många arbetsköer som helst.
När vi skapar en arbetskö har den en tjänst tilldelad för varje CPU pool_workqueue(pwq). Varje sådan pool_workqueue är associerad med en arbetarpool, som är allokerad på samma CPU och som i prioritet motsvarar kötypen. Genom dem samverkar arbetskön med arbetarpoolen.
Arbetare utför arbete från arbetarpoolen urskillningslöst, utan att urskilja vilken arbetskö de ursprungligen tillhörde.

För obundna köer allokeras arbetarpooler dynamiskt. Alla köer kan delas in i ekvivalensklasser enligt deras parametrar, och för varje sådan klass skapas en egen arbetarpool. De nås med hjälp av en speciell hashtabell, där nyckeln är en uppsättning parametrar, respektive värdet är arbetarpoolen.
Faktum är att för obundna köer är allt lite mer komplicerat: om för bundna köer pwq och köer skapades för varje CPU, skapas de här för varje NUMA-nod, men detta är en ytterligare optimering som vi inte kommer att överväga i detalj.

Alla möjliga småsaker

Jag kommer också att ge några funktioner från API:et för att komplettera bilden, men jag kommer inte att prata om dem i detalj:
/* Tvinga slutförande */ bool flush_work(struct work_struct *work); bool flush_delayed_work(struct delayed_work *dwork); /* Avbryt körning av arbete */ 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); /* Ta bort en kö */ void destroy_workqueue(struct workqueue_struct *wq);

Hur arbetare gör sitt jobb

Nu när vi är bekanta med API:t, låt oss försöka förstå mer i detalj hur det hela fungerar och hanteras.
Varje pool har en uppsättning arbetare som hanterar uppgifter. Dessutom förändras antalet arbetstagare dynamiskt och anpassar sig till den aktuella situationen.
Som vi redan har upptäckt är arbetare trådar som utför arbete i kärnan. Arbetaren hämtar dem i ordning, en efter en, från den arbetarpool som är kopplad till den, och arbetarna kan, som vi redan vet, tillhöra olika källköer.

Arbetare kan villkorligt vara i tre logiska tillstånd: de kan vara inaktiva, köra eller hantera.
Arbetare kan stå sysslolös och gör ingenting. Det är till exempel när allt arbete redan är på gång. När en arbetare går in i detta tillstånd, går den i vila och kommer följaktligen inte att utföra förrän den har väckts;
Om poolhantering inte krävs och listan över schemalagda arbeten inte är tom, börjar arbetaren utföra dem. Vi brukar kalla sådana arbetare löpning.
Vid behov tar arbetaren rollen chef slå samman. En pool kan ha antingen bara en arbetsledare eller ingen arbetare alls. Dess uppgift är att upprätthålla det optimala antalet arbetare per pool. Hur gör han det? För det första raderas arbetare som har varit lediga länge. För det andra skapas nya arbetare om tre villkor är uppfyllda samtidigt:

  • det finns fortfarande uppgifter att slutföra (fungerar i poolen)
  • inga sysslolösa arbetare
  • det finns inga arbetande arbetare (det vill säga aktiva och inte sova)
Det sista villkoret har dock sina egna nyanser. Om poolköerna är obundna tas inte hänsyn till löpande arbetare, för dem är detta villkor alltid sant. Detsamma gäller i fallet med en arbetare som utför en uppgift från en länkad, men med flaggan WQ_CPU_INTENSIVE, köer. Dessutom, när det gäller knutna köer, eftersom arbetare arbetar med arbeten från den gemensamma poolen (som är en av två för varje kärna på bilden ovan), visar det sig att en del av dem räknas som arbetande och andra inte. Därav följer också att utföra arbete fr.o.m WQ_CPU_INTENSIVE köer kanske inte startar omedelbart, men de själva stör inte utförandet av annat arbete. Nu borde det stå klart varför den här flaggan kallas så, och varför den används när vi förväntar oss att arbetet kommer att ta lång tid att slutföra.

Redovisning för arbetande arbetare utförs direkt från Linux-kärnschemaläggaren. Denna kontrollmekanism säkerställer en optimal samtidighetsnivå, förhindrar att arbetskön skapar för många arbetare, men gör inte heller att arbetet väntar i onödan för länge.

De som är intresserade kan titta på arbetarfunktionen i kärnan, den heter worker_thread().

Alla beskrivna funktioner och strukturer finns mer detaljerat i filerna include/linux/workqueue.h, kernel/workqueue.c Och kernel/workqueue_internal.h. Det finns även dokumentation om arbetskö in Documentation/workqueue.txt.

Det är också värt att notera att arbetskömekanismen används i kärnan inte bara för uppskjuten avbrottsbehandling (även om detta är ett ganska vanligt scenario).

Således tittade vi på mekanismerna för uppskjuten avbrottshantering i Linux-kärnan - tasklet och workqueue, som är en speciell form av multitasking. Du kan läsa om avbrott, tasklets och arbetsköer i boken "Linux Device Drivers" av Jonathan Corbet, Greg Kroah-Hartman, Alessandro Rubini, även om informationen där ibland är föråldrad.

Vi fortsätter ämnet multithreading i Linux-kärnan. Förra gången pratade jag om avbrott, deras bearbetning och tasklets, och eftersom det ursprungligen var meningen att detta skulle vara en artikel, kommer jag i min berättelse om workqueue att referera till tasklets, förutsatt att läsaren redan är bekant med dem.
Som förra gången ska jag försöka göra min berättelse så detaljerad och detaljerad som möjligt.

Artiklar i serien:

  1. Multitasking i Linux-kärnan: arbetskö

Arbetskö

Arbetskö- Dessa är mer komplexa och tunga enheter än tasklets. Jag kommer inte ens försöka beskriva alla krångligheterna med implementeringen här, men jag hoppas att jag kommer att analysera de viktigaste sakerna mer eller mindre i detalj.
Arbetsköer, som uppgiftsläsar, tjänar till fördröjd avbrottsbearbetning (även om de kan användas för andra ändamål), men till skillnad från uppgiftsläsar, exekveras de i sammanhanget av en kärnprocess; följaktligen behöver de inte vara atomära och kan använda sömn () funktion, olika synkroniseringsverktyg etc.

Låt oss först förstå hur arbetsköbehandlingsprocessen är organiserad i allmänhet. Bilden visar det väldigt ungefärligt och förenklat, hur allt faktiskt går till beskrivs i detalj nedan.

Flera enheter är involverade i denna mörka materia.
För det första, arbets objekt(bara arbeta för kort) är en struktur som beskriver funktionen (till exempel en avbrottshanterare) som vi vill schemalägga. Det kan ses som en analog till tasklet-strukturen. Vid schemaläggning lades uppgifter till i köer som var dolda för användaren, men nu måste vi använda en speciell kö - arbetskö.
Uppgifter rakas av schemaläggarfunktionen och arbetskön bearbetas av speciella trådar som kallas arbetare.
Arbetstagare's ger asynkron exekvering av arbeten från arbetskön. Även om de kallar arbete i rotationsordning, är det i det allmänna fallet inte fråga om strikt, sekventiell avrättning: här sker trots allt förköp, sömn, väntan etc.

I allmänhet är arbetare kärntrådar, det vill säga de styrs av Linux-kärnschemaläggaren. Men arbetare ingriper delvis i planeringen av ytterligare organisering av parallellt utförande av arbete. Detta kommer att diskuteras mer i detalj nedan.

För att beskriva de huvudsakliga funktionerna i arbetskömekanismen föreslår jag att du utforskar API:et.

Om kön och dess tillkomst

alloc_workqueue(fmt, flaggor, max_active, args...)
Parametrarna fmt och args är printf-formatet för namnet och argumenten till det. Parametern max_activate är ansvarig för det maximala antalet arbeten som från denna kö kan exekveras parallellt på en CPU.
En kö kan skapas med följande flaggor:
  • WQ_HIGHPRI
  • WQ_UNBOUND
  • WQ_CPU_INTENSIVE
  • WQ_FREEZABLE
  • WQ_MEM_RECLAIM
Särskild uppmärksamhet bör ägnas flaggan WQ_UNBOUND. Baserat på närvaron av denna flagga delas köerna in i bundna och obundna.
I länkade köer När de läggs till är arbeten bundna till den aktuella CPU:n, det vill säga i sådana köer exekveras arbeten på kärnan som schemalägger det. I detta avseende liknar bundna köer tasklets.
I obundna köer arbete kan utföras på vilken kärna som helst.

En viktig funktion i arbetsköimplementeringen i Linux-kärnan är den extra organisationen av parallellkörning som finns i bundna köer. Det skrivs mer i detalj nedan, men nu kommer jag att säga att det är utfört på ett sådant sätt att så lite minne som möjligt används, och så att processorn inte står i viloläge. Allt detta implementeras med antagandet att ett verk inte använder för många processorcykler.
Detta är inte fallet för obundna köer. I grund och botten ger sådana köer helt enkelt sammanhang till arbetarna och startar dem så tidigt som möjligt.
Således bör obundna köer användas om CPU-intensiv arbetsbelastning förväntas, eftersom schemaläggaren i detta fall kommer att ta hand om parallell exekvering på flera kärnor.

I analogi med tasklets kan verk tilldelas exekveringsprioritet, normal eller hög. Prioriteten är gemensam för hela kön. Som standard har kön normal prioritet, och om du ställer in flaggan WQ_HIGHPRI, då, följaktligen, hög.

Flagga WQ_CPU_INTENSIVEär bara vettigt för bundna köer. Denna flagga är en vägran att delta i en ytterligare organisation av parallellt utförande. Denna flagga bör användas när arbetet förväntas ta mycket CPU-tid, i vilket fall det är bättre att flytta ansvaret till schemaläggaren. Detta beskrivs mer i detalj nedan.

Flaggor WQ_FREEZABLE Och WQ_MEM_RECLAIMär specifika och utanför ämnets omfattning, så vi kommer inte att uppehålla oss i detalj.

Ibland är det vettigt att inte skapa sina egna köer, utan att använda vanliga. De viktigaste:

  • system_wq - bunden kö för snabbt arbete
  • system_long_wq - en bunden kö för arbeten som förväntas ta lång tid att utföra
  • system_unbound_wq - obunden kö

Om arbetet och deras planering

Låt oss nu ta itu med verken. Låt oss först titta på initierings-, deklarations- och förberedelsemakron:
DECLARE(_DELAYED)_WORK(namn, void (*funktion)(struct work_struct *work)); /* vid kompileringstid */ INIT(_DELAYED)_WORK(_work, _func); /* under körning */ PREPARE(_DELAYED)_WORK(_work, _func); /* för att ändra funktionen som körs */
Verk läggs till i kön med hjälp av funktionerna:
bool queue_work(struct workqueue_struct *wq, struct work_struct *work); bool queue_delayed_work(struct workqueue_struct *wq, struct delayed_work *dwork, osignerad lång fördröjning); /* arbete kommer att läggas till i kön först efter att förseningen har gått ut */
Detta är värt att uppehålla sig mer i detalj. Även om vi anger en kö som en parameter, placeras faktiskt inte arbeten i själva arbetskön, som det kan tyckas, utan i en helt annan enhet - i kölistan för worker_pool-strukturen. Strukturera worker_pool, i själva verket är den viktigaste enheten för att organisera arbetskömekanismen, även om den för användaren förblir bakom kulisserna. Det är med dem som arbetare arbetar, och det är i dem som all grundläggande information finns.

Låt oss nu se vilka pooler som finns i systemet.
Till att börja med pooler för bundna köer (på bilden). För varje CPU är två arbetarpooler statiskt allokerade: en för högprioriterat arbete, den andra för arbete med normal prioritet. Det vill säga om vi har fyra kärnor så blir det bara åtta bundna pooler, trots att det kan finnas hur många arbetsköer som helst.
När vi skapar en arbetskö har den en tjänst tilldelad för varje CPU pool_workqueue(pwq). Varje sådan pool_workqueue är associerad med en arbetarpool, som är allokerad på samma CPU och som i prioritet motsvarar kötypen. Genom dem samverkar arbetskön med arbetarpoolen.
Arbetare utför arbete från arbetarpoolen urskillningslöst, utan att urskilja vilken arbetskö de ursprungligen tillhörde.

För obundna köer allokeras arbetarpooler dynamiskt. Alla köer kan delas in i ekvivalensklasser enligt deras parametrar, och för varje sådan klass skapas en egen arbetarpool. De nås med hjälp av en speciell hashtabell, där nyckeln är en uppsättning parametrar, respektive värdet är arbetarpoolen.
Faktum är att för obundna köer är allt lite mer komplicerat: om för bundna köer pwq och köer skapades för varje CPU, skapas de här för varje NUMA-nod, men detta är en ytterligare optimering som vi inte kommer att överväga i detalj.

Alla möjliga småsaker

Jag kommer också att ge några funktioner från API:et för att komplettera bilden, men jag kommer inte att prata om dem i detalj:
/* Tvinga slutförande */ bool flush_work(struct work_struct *work); bool flush_delayed_work(struct delayed_work *dwork); /* Avbryt körning av arbete */ 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); /* Ta bort en kö */ void destroy_workqueue(struct workqueue_struct *wq);

Hur arbetare gör sitt jobb

Nu när vi är bekanta med API:t, låt oss försöka förstå mer i detalj hur det hela fungerar och hanteras.
Varje pool har en uppsättning arbetare som hanterar uppgifter. Dessutom förändras antalet arbetstagare dynamiskt och anpassar sig till den aktuella situationen.
Som vi redan har upptäckt är arbetare trådar som utför arbete i kärnan. Arbetaren hämtar dem i ordning, en efter en, från den arbetarpool som är kopplad till den, och arbetarna kan, som vi redan vet, tillhöra olika källköer.

Arbetare kan villkorligt vara i tre logiska tillstånd: de kan vara inaktiva, köra eller hantera.
Arbetare kan stå sysslolös och gör ingenting. Det är till exempel när allt arbete redan är på gång. När en arbetare går in i detta tillstånd, går den i vila och kommer följaktligen inte att utföra förrän den har väckts;
Om poolhantering inte krävs och listan över schemalagda arbeten inte är tom, börjar arbetaren utföra dem. Vi brukar kalla sådana arbetare löpning.
Vid behov tar arbetaren rollen chef slå samman. En pool kan ha antingen bara en arbetsledare eller ingen arbetare alls. Dess uppgift är att upprätthålla det optimala antalet arbetare per pool. Hur gör han det? För det första raderas arbetare som har varit lediga länge. För det andra skapas nya arbetare om tre villkor är uppfyllda samtidigt:

  • det finns fortfarande uppgifter att slutföra (fungerar i poolen)
  • inga sysslolösa arbetare
  • det finns inga arbetande arbetare (det vill säga aktiva och inte sova)
Det sista villkoret har dock sina egna nyanser. Om poolköerna är obundna tas inte hänsyn till löpande arbetare, för dem är detta villkor alltid sant. Detsamma gäller i fallet med en arbetare som utför en uppgift från en länkad, men med flaggan WQ_CPU_INTENSIVE, köer. Dessutom, när det gäller knutna köer, eftersom arbetare arbetar med arbeten från den gemensamma poolen (som är en av två för varje kärna på bilden ovan), visar det sig att en del av dem räknas som arbetande och andra inte. Därav följer också att utföra arbete fr.o.m WQ_CPU_INTENSIVE köer kanske inte startar omedelbart, men de själva stör inte utförandet av annat arbete. Nu borde det stå klart varför den här flaggan kallas så, och varför den används när vi förväntar oss att arbetet kommer att ta lång tid att slutföra.

Redovisning för arbetande arbetare utförs direkt från Linux-kärnschemaläggaren. Denna kontrollmekanism säkerställer en optimal samtidighetsnivå, förhindrar att arbetskön skapar för många arbetare, men gör inte heller att arbetet väntar i onödan för länge.

De som är intresserade kan titta på arbetarfunktionen i kärnan, den heter worker_thread().

Alla beskrivna funktioner och strukturer finns mer detaljerat i filerna include/linux/workqueue.h, kernel/workqueue.c Och kernel/workqueue_internal.h. Det finns även dokumentation om arbetskö in Documentation/workqueue.txt.

Det är också värt att notera att arbetskömekanismen används i kärnan inte bara för uppskjuten avbrottsbehandling (även om detta är ett ganska vanligt scenario).

Således tittade vi på mekanismerna för uppskjuten avbrottshantering i Linux-kärnan - tasklet och workqueue, som är en speciell form av multitasking. Du kan läsa om avbrott, tasklets och arbetsköer i boken "Linux Device Drivers" av Jonathan Corbet, Greg Kroah-Hartman, Alessandro Rubini, även om informationen där ibland är föråldrad.