Prinsipper for drift i UNIX-lignende operativsystemer med Linux som eksempel. Lag din egen lettvektsprosess

Et av de mest irriterende øyeblikkene når man flytter fra miljø til Windows basert for bruk kommandolinje– tap av enkel multitasking. Selv på Linux, hvis du bruker X Window-systemet, kan du bruke musen til å klikke på nytt program og åpne den. På kommandolinjen, derimot, sitter du ganske mye fast med monotasking. I denne artikkelen vil vi vise deg hvordan multitaske i Linux ved hjelp av kommandolinjen.

Bakgrunn og prioritert prosessledelse

Imidlertid er det fortsatt måter å multitaske på i Linux, og noen av dem er mer omfattende enn andre. En innebygd metode som ikke krever noe ekstra programvare, er ganske enkelt å flytte prosesser i bakgrunnen og i forgrunnen. Vi snakker om dette. Det har imidlertid noen ulemper.

Ubeskyttet

for det første For å sende en prosess i bakgrunnen, må du først stoppe den. Det er ikke mulig å sende et allerede kjørende program i bakgrunnen og vedlikeholde det samtidig.

for det andre, må du bryte arbeidsflyten for å starte en ny kommando. Du må komme deg ut av det du gjør for øyeblikket og skrive inn flere kommandoer i skallet. Det fungerer, men det er upraktisk.

Tredje, bør du overvåke utdata fra bakgrunnsprosesser. Eventuelle utdata fra dem vil vises på kommandolinjen og vil forstyrre det du gjør for øyeblikket. Så bakgrunnsoppgaver må enten omdirigere produksjonen til egen fil, eller de må være fullstendig deaktivert.

På grunn av disse manglene er det store problemer med å håndtere bakgrunns- og forgrunnsprosessen. Den beste avgjørelsen– bruk kommandolinjeverktøyets "skjerm" som vist nedenfor.

Men først - du vil åpne en ny SSH-økt

Ikke glem at du bare åpner en ny SSH-økt.

Det kan være upraktisk å åpne nye økter hele tiden. Og det er da du trenger "skjerm"

Nytte skjerm lar deg lage flere arbeidsflyter åpne samtidig - den nærmeste analogen til "vinduer". Som standard er den tilgjengelig i vanlige Linux-depoter. Installer den på CentOS/RHEL ved å bruke følgende kommando:

Sudo yum installasjonsskjerm

Åpner en ny skjerm

Start nå økten ved å skrive "skjerm".

Dette vil skape tomt vindu i en eksisterende SSH-økt og gi den et tall som vises i overskriften slik:

Skjermen vår her er nummerert "0" som vist. I dette skjermbildet bruker vi en dummy "les"-kommando for å blokkere terminalen og få den til å vente på input. La oss nå si at vi vil gjøre noe annet mens vi venter.

Å åpne ny skjerm og gjør noe annet, skriver vi ut:

Ctrl+a c

"ctrl+a" er standard tastekombinasjon for å kontrollere skjermer i skjermprogrammet. Hva du skriver etter det avgjør handlingen. For eksempel:

  • ctrl+a c – C aktiverer en ny skjerm
  • ctrl+a [Antall]– hopp til et bestemt skjermnummer
  • ctrl+a k – K slår av gjeldende skjerm
  • ctrl+a n – Gå til skjermen n
  • ctrl+a "- viser alle aktive skjermer i økten

Hvis vi trykker “ctrl+a c” får vi en ny skjerm med et nytt nummer.

Du kan bruke markørtastene til å navigere gjennom listen og gå til skjermen du ønsker.
Skjermer er det nærmeste du kommer "windows", som et system i en kommando Linux-streng. Det er selvfølgelig ikke så enkelt som å klikke med en mus, men det grafiske undersystemet er veldig ressurskrevende. Med skjermer kan du få nesten samme funksjonalitet og muliggjøre full multitasking!

Prosesser i UNIX

I UNIX er den viktigste organiseringen og enheten for multitasking prosessen. Operativsystemet manipulerer prosessbildet, som representerer programkoden, samt prosessdataseksjonene som definerer utførelsesmiljøet.

Under utførelse eller mens du venter "i vingene", er prosesser inneholdt i virtuelt minne med en sideorganisering. Noe av dette virtuelle minnet er tilordnet fysisk minne. En del av det fysiske minnet er reservert for operativsystemkjernen. Brukere kan bare få tilgang til minnet som er igjen for prosesser. Om nødvendig byttes prosessminnesider fra fysisk minne til disk, til bytteområdet. Når du får tilgang til en side i virtuelt minne, hvis den ikke er i fysisk minne, byttes den fra disk.

Virtuelt minne implementeres og vedlikeholdes automatisk av UNIX-kjernen.

Prosesstyper

Det er tre typer prosesser i UNIX-operativsystemer: systematisk, daemon-prosesser Og søknadsprosesser.

Systemprosesser er en del av kjernen og er alltid lokalisert i tilfeldig tilgangsminne. Systemprosesser har ikke tilsvarende programmer i form av kjørbare filer og startes på en spesiell måte når systemkjernen initialiseres. Utføringsinstruksjonene og dataene til disse prosessene ligger i kjernen til systemet, slik at de kan kalle opp funksjoner og få tilgang til data som andre prosesser ikke har tilgang til.

Systemprosesser inkluderer prosessen med initialisering, i det, som er stamfaderen til alle andre prosesser. Selv om i det er ikke en del av kjernen, og dens kjøring skjer fra en kjørbar fil, er operasjonen avgjørende for at hele systemet skal fungere som helhet.

Demoner- dette er ikke-interaktive prosesser som startes på vanlig måte - ved å laste inn tilhørende programmer i minnet, og kjøres i bakgrunnen. Vanligvis lanseres demoner under systeminitialisering, men etter at kjernen er initialisert, og sikrer driften av forskjellige UNIX-undersystemer: terminaltilgangssystemer, utskriftssystemer, nettverkstjenester etc. Demoner er ikke assosiert med noen bruker. Mesteparten av tiden venter demoner på at en eller annen prosess ber om spesifikk tjeneste.



TIL søknadsprosesser inkluderer alle andre prosesser som kjører på systemet. Vanligvis er dette prosesser som genereres i en brukerøkt. Den viktigste brukerprosessen er innledende kommandotolk, som gir kjøring av brukerkommandoer på et UNIX-system.

Brukerprosesser kan kjøres i både interaktive (preemptive) og bakgrunnsmoduser. Interaktive prosesser har eksklusivt eierskap til terminalen, og inntil en slik prosess fullfører sin utførelse, har ikke brukeren tilgang til kommandolinjen.

Prosessattributter

En UNIX-prosess har en rekke attributter som gjør det mulig operativsystem administrere sitt arbeid. Hovedattributter:

· Prosess-ID (PID), slik at systemkjernen kan skille mellom prosesser. Når opprettet ny prosess, tildeler kjernen den neste ledige (dvs. ikke assosiert med noen prosess) identifikator. Tildelingen av en identifikator skjer vanligvis i stigende rekkefølge, dvs. ID-en til den nye prosessen er større enn ID-en til prosessen som ble opprettet før den. Hvis ID-en når maksimumsverdien (vanligvis 65737), vil neste prosess motta minimum ledig PID og syklusen gjentas. Når en prosess avsluttes, frigir kjernen identifikatoren den brukte.

· ID for overordnet prosess (PPID)– identifikator for prosessen som skapte denne prosessen. Alle prosesser i systemet unntatt systemprosesser og prosess i det, som er stamfaderen til de gjenværende prosessene, genereres av en av de eksisterende eller tidligere eksisterende prosessene.

· Prioritetskorreksjon (NI)– prosessens relative prioritet, tatt i betraktning av planleggeren når oppstartsrekkefølgen bestemmes. Den faktiske fordelingen av prosessorressurser bestemmes av utførelsesprioriteten (attributt PRI), avhengig av flere faktorer, spesielt den gitte relative prioritet. Den relative prioriteten endres ikke av systemet gjennom hele prosessens levetid, selv om den kan endres av brukeren eller administratoren når prosessen starter med kommandoen hyggelig. Området for prioriterte økningsverdier på de fleste systemer er -20 til 20. Hvis ingen inkrement er spesifisert, brukes standardverdien på 10. En positiv økning betyr en reduksjon i gjeldende prioritet. Vanlige brukere kan bare angi en positiv økning og dermed bare redusere prioriteten. Bruker rot kan sette et negativt inkrement, som øker prioriteringen av prosessen og dermed bidrar til dens høyere raskt arbeid. I motsetning til relativ prioritet, endres utførelsesprioriteten til en prosess dynamisk av planleggeren.

· Terminal Line (TTY)– en terminal eller pseudoterminal knyttet til en prosess. Denne terminalen er tilknyttet standard bekker: input, fridag Og meldingsflyt om feil. Strømmer ( programkanaler) er standard betyr kommunikasjon mellom prosesser i UNIX OS. Daemon-prosesser er ikke knyttet til en terminal.

· Ekte (UID) og effektive (EUID) brukeridentifikatorer. Den virkelige bruker-ID-en til en gitt prosess er ID-en til brukeren som startet prosessen. Den effektive identifikatoren brukes til å bestemme prosessens tilgangsrettigheter til systemressurser (primært ressurser filsystem). Vanligvis er de virkelige og effektive identifikatorene de samme, dvs. prosessen har samme rettigheter i systemet som brukeren som startet den. Det er imidlertid mulig å gi en prosess flere rettigheter enn brukeren ved å sette SUID bit når den effektive identifikatoren er satt til identifikatoren til eieren av den kjørbare filen (for eksempel brukeren rot).

· Ekte (GID) og effektive (EGID) gruppeidentifikatorer. Den virkelige gruppe-ID-en er lik den primære eller gjeldende gruppe-ID-en til brukeren som startet prosessen. Den effektive identifikatoren brukes til å bestemme tilgangsrettigheter til systemressurser på vegne av en gruppe. Vanligvis er den effektive gruppe-IDen den samme som den virkelige. Men hvis den kjørbare filen er satt til SGID bit, kjøres en slik fil med den effektive eiergruppe-IDen.

LABORATORIEARBEID nr. 3

MULTI-TASKING PROGRAMMERING ILINUX

1. Målet med arbeidet: Gjør deg kjent med gcc-kompilatoren, programfeilsøkingsteknikker og funksjoner for arbeid med prosesser.

2. Kort teoretisk informasjon.

Minimumssettet med gcc-kompilatorbrytere er - Vegg (vis alle feil og advarsler) og -o (utdatafil):

gcc - Vegg - o print_pid print_pid. c

Teamet vil opprette kjørbar fil print_pid.

C-standardbiblioteket (libc, implementert i Linux i glibc) drar fordel av multitasking-mulighetene til Unix System V (heretter SysV). I libc er pid_t-typen definert som et heltall som kan inneholde en pid. Funksjonen som rapporterer pid for gjeldende prosess har en prototype av pid_t getpid(void) og er definert sammen med pid_t i unistd. h og sys/typer. h).

For å lage en ny prosess, bruk gaffelfunksjonen:

pid_t gaffel (void)

Ved å sette inn en forsinkelse av tilfeldig lengde ved å bruke dvale- og randfunksjonene, kan du se effekten av multitasking tydeligere:

dette vil få programmet til å "sove" i et tilfeldig antall sekunder: fra 0 til 3.

For å kalle en funksjon som en underordnet prosess, bare kall den etter forgrening:

// hvis en underordnet prosess kjører, kall funksjonen

pid=prosess(arg);

// avslutt prosessen

Ofte er det nødvendig å kjøre et annet program som en barneprosess. For å gjøre dette, bruk exec-familiefunksjonene:

// hvis en underordnet prosess kjører, ring programmet


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

printf("FEIL under start av prosessen\n");

else printf("prosess startet (pid=%d)\n", pid);

// avslutt prosessen

Ofte må en overordnet prosess utveksle informasjon med barna sine, eller i det minste synkronisere med dem, for å utføre operasjoner til rett tid. En måte å synkronisere prosesser på er med wait og waitpid-funksjonene:

#inkludere

#inkludere

pid_t wait(int *status) - suspenderer utførelsen av gjeldende prosess til noen av dens underordnede prosesser avsluttes.

pid_t waitpid (pid_t pid, int *status, int alternativer) - suspenderer utførelsen av gjeldende prosess til den spesifiserte prosessen fullføres eller sjekker fullføringen av den spesifiserte prosessen.

Hvis du trenger å finne ut tilstanden til den underordnede prosessen når den avsluttes og verdien den returnerer, bruk WEXITSTATUS-makroen, og gi den statusen til den underordnede prosessen som en parameter.

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

if (pid == status) (

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

For å endre prioriteringene til oppstartede prosesser, brukes settprioritet og funksjoner. Prioritetene settes i området fra -20 (høyest) til 20 (laveste), normalverdien er 0. Merk at kun en superbruker kan øke prioriteten over normalen!

#inkludere

#inkludere

int prosess(int i) (

setpriority(PRIO_PROCESS, getpid(),i);

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

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

For å drepe en prosess, bruk kill-funksjonen:

#inkludere

#inkludere

int kill(pid_t pid, int sig);

Hvis pid > 0, spesifiserer den PID-en til prosessen som signalet sendes til. Hvis pid = 0, sendes signalet til alle prosesser i gruppen som den gjeldende prosessen tilhører.

sig - signaltype. Noen typer signaler i Linux:

SIGKILL Dette signalet fører til at prosessen avsluttes umiddelbart. Prosessen kan ikke ignorere dette signalet.

SIGTERM Dette signalet er en forespørsel om å avslutte prosessen.

SIGCHLD Systemet sender dette signalet til en prosess når en av dens underordnede prosesser avsluttes. Eksempel:

if (pid[i] == status) (

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

else kill(pid[i],SIGKILL);

3. Metodiske instruksjoner.

3.1. For å gjøre deg kjent med gcc-kompilatoralternativene og beskrivelser av C-språkfunksjoner, bruk man og info-instruksjonene.

3.2. For å feilsøke programmer er det praktisk å bruke den innebygde editoren i filbehandleren Midnattssjef(MC), som fremhever ulike språkkonstruksjoner i farger og indikerer posisjonen til markøren i filen (rad, kolonne) i den øverste linjen på skjermen.

3.3. Midnight Commander-filbehandleren har en kommandobuffer som kan kalles opp med en hurtigtast - H, som kan flyttes ved hjelp av markørpilene (opp og ned). For å sette inn en kommando fra bufferen i kommandolinjen, bruk tasten , for å redigere en kommando fra buffer-tastene<- и ->, Og .


3.4. Husk at gjeldende katalog ikke er inneholdt i banen, så du må kjøre programmet som "./print_pid" fra kommandolinjen. I MC, bare hold musepekeren over filen og klikk .

3.5. For å se resultatet av programkjøringen, bruk hurtigtasten - O. De fungerer også i filredigeringsmodus.

3.6. For å logge resultatene av programkjøring, er det tilrådelig å bruke omdirigering av utdata fra konsollen til en fil: ./test > resultat. tekst

3.7. For å få tilgang til filer opprettet på Linux server, bruk ftp-protokollen, klientprogrammet som er tilgjengelig i Windows 2000 og er innebygd i filbehandler LANGT. Hvori Regnskap og passordet er det samme som når du kobler til via ssh.

4.1. Gjør deg kjent med gcc-kompilatoralternativene og metodene for feilsøking av programmer.

4.2. For varianter av oppgaver fra laboratoriearbeid nr. 1, skriv og feilsøk et program som implementerer den genererte prosessen.

4.3. For oppgavealternativer fra laboratoriearbeid Nr. 1 skrive og feilsøke et program som implementerer en overordnet prosess som kaller og overvåker tilstanden til underordnede prosesser - programmer (venter på at de skal fullføre eller ødelegge dem, avhengig av alternativet).

4.4. For varianter av oppgaver fra laboratoriearbeid nr. 1, skriv og feilsøk et program som implementerer en overordnet prosess som kaller og overvåker tilstanden til underordnede prosesser - funksjoner (venter på fullføring eller ødelegger dem, avhengig av varianten).

5. Alternativer for oppgaver. Se alternativer for oppgaver fra laboratoriearbeid nr. 1

6. Innhold i rapporten.

6.1. Målet med arbeidet.

6.2. Oppgavealternativ.

6.3. Programoppføringer.

6.4. Protokoller for programkjøring.

7. Kontrollspørsmål.

7.1. Funksjoner ved å kompilere og kjøre C-programmer på Linux.

7.2. Hva er pid, hvordan bestemmer jeg det i operativsystemet og programmet?

7.3. Gaffelfunksjon - formål, applikasjon, returverdi.

7.4. Hvordan kjører jeg en funksjon i en oppstartet prosess? Program?

7.5. Måter å synkronisere foreldre- og barneprosesser.

7.6. Hvordan finne ut tilstanden til den oppstartede prosessen når den avsluttes og verdien den returnerer?

7.7. Hvordan administrere prosessprioriteringer?

7.8. Hvordan drepe en prosess i operativsystemet og programmet?

Vi fortsetter temaet multithreading i Linux-kjernen. Sist gang jeg snakket om avbrudd, deres behandling og oppgaver, og siden det opprinnelig var meningen at dette skulle være én artikkel, vil jeg i min historie om arbeidskø referere til oppgaver, forutsatt at leseren allerede er kjent med dem.
Som forrige gang skal jeg prøve å gjøre historien min så detaljert og detaljert som mulig.

Artikler i serien:

  1. Multitasking i Linux-kjernen: arbeidskø

Arbeidskø

Arbeidskø- Dette er mer komplekse og tunge enheter enn oppgaver. Jeg vil ikke engang prøve å beskrive alle detaljene ved implementeringen her, men jeg håper jeg vil analysere de viktigste tingene mer eller mindre detaljert.
Arbeidskøer, som oppgaver, tjener til utsatt avbruddsbehandling (selv om de kan brukes til andre formål), men i motsetning til oppgaver utføres de i sammenheng med en kjerneprosess; følgelig trenger de ikke å være atomære og kan bruke søvn () funksjon, ulike synkroniseringsverktøy, etc.

La oss først forstå hvordan arbeidskøbehandlingsprosessen er organisert generelt. Bildet viser det veldig omtrentlig og forenklet, hvordan alt faktisk skjer er beskrevet i detalj nedenfor.

Flere enheter er involvert i denne mørke materien.
For det første, arbeidselement(bare arbeid for kort) er en struktur som beskriver funksjonen (for eksempel en avbruddsbehandler) som vi ønsker å planlegge. Den kan tenkes på som en analog av oppgavestrukturen. Ved planlegging ble oppgaver lagt til køer skjult for brukeren, men nå må vi bruke en spesiell kø - arbeidskø.
Oppgaver rakes av planleggerfunksjonen, og arbeidskøen behandles av spesielle tråder kalt arbeidere.
Arbeider's gir asynkron utførelse av arbeid fra arbeidskø. Selv om de kaller arbeid i rotasjonsrekkefølge, er det i det generelle tilfellet ikke snakk om streng, sekvensiell utførelse: Tross alt foregår forkjøp, søvn, venting osv. her.

Generelt er arbeidere kjernetråder, det vil si at de kontrolleres av Linux-kjerneplanleggeren. Men arbeiderne griper delvis inn i planleggingen for ytterligere organisering av parallell utførelse av arbeid. Dette vil bli diskutert mer detaljert nedenfor.

For å skissere hovedfunksjonene til arbeidskømekanismen foreslår jeg at du utforsker API.

Om køen og dens opprettelse

alloc_workqueue(fmt, flagg, max_active, args...)
Parametrene fmt og args er printf-formatet for navnet og argumentene til det. Parameteren max_activate er ansvarlig for det maksimale antallet verk som fra denne køen kan utføres parallelt på én CPU.
En kø kan opprettes med følgende flagg:
  • WQ_HIGHPRI
  • WQ_UNBOUND
  • WQ_CPU_INTENSIVE
  • WQ_FRYSBAR
  • WQ_MEM_RECLAIM
Spesiell oppmerksomhet bør rettes mot flagget WQ_UNBOUND. Basert på tilstedeværelsen av dette flagget, er køene delt inn i bundet og ubundet.
I koblede køer Når det legges til, er arbeid bundet til gjeldende CPU, det vil si at i slike køer, blir arbeid utført på kjernen som planlegger det. I denne forbindelse ligner bundne køer på oppgaver.
I ubundne køer arbeid kan utføres på hvilken som helst kjerne.

Et viktig trekk ved arbeidskøimplementeringen i Linux-kjernen er den ekstra organiseringen av parallell kjøring som er tilstede i bundne køer. Det er skrevet mer detaljert nedenfor, men nå vil jeg si at det er utført på en slik måte at minst mulig minne brukes, og slik at prosessoren ikke står uvirksom. Alt dette er implementert med antagelsen om at ett verk ikke bruker for mange prosessorsykluser.
Dette er ikke tilfelle for ubundne køer. I hovedsak gir slike køer ganske enkelt kontekst til arbeidere og starter dem så tidlig som mulig.
Derfor bør ukoblede køer brukes hvis CPU-intensiv arbeidsbelastning forventes, siden i dette tilfellet vil planleggeren ta seg av parallell kjøring på flere kjerner.

Analogt med oppgaver kan arbeid tildeles utførelsesprioritet, normal eller høy. Prioriteten er felles for hele køen. Som standard har køen normal prioritet, og hvis du setter flagget WQ_HIGHPRI, og følgelig høy.

Flagg WQ_CPU_INTENSIVE gir bare mening for bundne køer. Dette flagget er et avslag på å delta i en ekstra organisering av parallell utførelse. Dette flagget bør brukes når arbeid forventes å ta mye CPU-tid, i så fall er det bedre å flytte ansvaret til planleggeren. Dette er beskrevet mer detaljert nedenfor.

Flagg WQ_FRYSBAR Og WQ_MEM_RECLAIM er spesifikke og utenfor omfanget av emnet, så vi vil ikke dvele ved dem i detalj.

Noen ganger er det fornuftig å ikke lage dine egne køer, men å bruke vanlige. De viktigste:

  • system_wq - bundet kø for raskt arbeid
  • system_long_wq - en bundet kø for arbeider som forventes å ta lang tid å utføre
  • system_unbound_wq - ubundet kø

Om arbeidet og deres planlegging

La oss nå ta oss av arbeidet. La oss først se på initialiserings-, deklarasjons- og forberedelsesmakroene:
DECLARE(_DELAYED)_WORK(navn, void (*funksjon)(struct work_struct *work)); /* ved kompileringstid */ INIT(_DELAYED)_WORK(_work, _func); /* under utførelse */ PREPARE(_DELAYED)_WORK(_work, _func); /* for å endre funksjonen som utføres */
Verk legges til i køen ved hjelp av funksjonene:
bool køarbeid(struktur arbeidskøstruktur *wq, struktur arbeidsstruktur *arbeid); bool queue_delayed_work(struct workqueue_struct *wq, struct delayed_work *dwork, usignert lang forsinkelse); /* arbeid legges til i køen først etter at forsinkelsen har utløpt */
Dette er verdt å dvele mer ved. Selv om vi spesifiserer en kø som en parameter, plasseres faktisk ikke arbeid i selve arbeidskøen, som det kan virke, men i en helt annen enhet - i kølisten til worker_pool-strukturen. Struktur worker_pool, faktisk, er den viktigste enheten i organiseringen av arbeidskømekanismen, selv om den for brukeren forblir bak kulissene. Det er med dem arbeiderne jobber, og det er i dem all grunnleggende informasjon finnes.

La oss nå se hvilke bassenger som er i systemet.
Til å begynne med bassenger for bundne køer (på bildet). For hver CPU er to arbeidergrupper statisk tildelt: en for høyprioritert arbeid, den andre for arbeid med normal prioritet. Det vil si at hvis vi har fire kjerner, så blir det kun åtte tilknyttede bassenger, til tross for at arbeidskøen kan være så mange som ønskelig.
Når vi oppretter en arbeidskø, har den en tjeneste tildelt for hver CPU pool_workqueue(pwq). Hver slik pool_workqueue er assosiert med en arbeiderpool, som er allokert på samme CPU og tilsvarer prioritet til køtypen. Gjennom dem samhandler arbeidskøen med arbeiderbasen.
Arbeidere utfører arbeid fra arbeiderpoolen tilfeldig, uten å skille hvilken arbeidskø de opprinnelig tilhørte.

For ikke-tilknyttede køer tildeles arbeiderpooler dynamisk. Alle køer kan deles inn i ekvivalensklasser i henhold til deres parametere, og for hver slik klasse opprettes en egen arbeiderpool. De får tilgang til ved hjelp av en spesiell hash-tabell, der nøkkelen er et sett med parametere, og verdien er henholdsvis arbeiderpoolen.
Faktisk, for ubundne køer er alt litt mer komplisert: hvis for bundne køer pwq og køer ble opprettet for hver CPU, her opprettes de for hver NUMA-node, men dette er en ekstra optimalisering som vi ikke vil vurdere i detalj.

Alle slags småting

Jeg vil også gi noen funksjoner fra API for å fullføre bildet, men jeg vil ikke snakke om dem i detalj:
/* Force fullføring */ bool flush_work(struct work_struct *work); bool flush_delayed_work(struct delayed_work *dwork); /* Avbryt utførelse av arbeid */ 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); /* Slett en kø */ void destroy_workqueue(struct workqueue_struct *wq);

Hvordan arbeiderne gjør jobben sin

Nå som vi er kjent med API, la oss prøve å forstå mer detaljert hvordan det hele fungerer og administreres.
Hvert basseng har et sett med arbeidere som håndterer oppgaver. Dessuten endrer antallet arbeidere seg dynamisk, og tilpasser seg dagens situasjon.
Som vi allerede har funnet ut, er arbeidere tråder som utfører arbeid i sammenheng med kjernen. Arbeideren henter dem i rekkefølge, en etter en, fra arbeiderpoolen knyttet til den, og arbeiderne kan, som vi allerede vet, tilhøre forskjellige kildekøer.

Arbeidere kan betinget være i tre logiske tilstander: de kan være inaktive, kjørende eller administrerende.
Arbeider kan stå stille og gjør ingenting. Dette er for eksempel når alt arbeidet allerede er utført. Når en arbeider går inn i denne tilstanden, går den i dvale og vil følgelig ikke utføre før den er vekket;
Hvis bassengadministrasjon ikke er nødvendig og listen over planlagte arbeider ikke er tom, begynner arbeideren å utføre dem. Vi vil konvensjonelt kalle slike arbeidere løping.
Om nødvendig tar arbeideren på seg rollen sjef basseng. En pool kan ha enten bare én administrerende arbeider eller ingen arbeider i det hele tatt. Dens oppgave er å opprettholde det optimale antallet arbeidere per basseng. Hvordan gjør han det? For det første slettes arbeidere som har vært inaktive i lang tid. For det andre opprettes nye arbeidere hvis tre betingelser er oppfylt samtidig:

  • det er fortsatt oppgaver å fullføre (fungerer i bassenget)
  • ingen ledige arbeidere
  • det er ingen arbeidere (det vil si aktive og ikke sovende)
Den siste betingelsen har imidlertid sine egne nyanser. Hvis bassengkøene er ubundne, tas det ikke hensyn til løpende arbeidere; for dem er denne betingelsen alltid sann. Det samme gjelder for en arbeider som utfører en oppgave fra en koblet, men med flagget WQ_CPU_INTENSIVE, køer. Dessuten, i tilfelle av bundne køer, siden arbeidere jobber med arbeider fra felles basseng (som er en av to for hver kjerne på bildet ovenfor), viser det seg at noen av dem regnes som arbeidende, og noen ikke. Det følger også at utføre arbeid fra WQ_CPU_INTENSIVE køer starter kanskje ikke umiddelbart, men de selv forstyrrer ikke utførelsen av annet arbeid. Nå skal det være klart hvorfor dette flagget heter det, og hvorfor det brukes når vi forventer at arbeidet vil ta lang tid å fullføre.

Regnskap for arbeidende arbeidere utføres direkte fra Linux-kjerneplanleggeren. Denne kontrollmekanismen sikrer et optimalt samtidighetsnivå, forhindrer at arbeidskøen skaper for mange arbeidere, men får heller ikke arbeidet til å vente unødvendig for lenge.

De som er interessert kan se på arbeiderfunksjonen i kjernen, den heter worker_thread().

Alle beskrevne funksjoner og strukturer finnes mer detaljert i filene include/linux/workqueue.h, kernel/workqueue.c Og kernel/workqueue_internal.h. Det er også dokumentasjon på arbeidskø inn Documentation/workqueue.txt.

Det er også verdt å merke seg at arbeidskømekanismen brukes i kjernen ikke bare for utsatt avbruddsbehandling (selv om dette er et ganske vanlig scenario).

Dermed så vi på mekanismene for utsatt avbruddshåndtering i Linux-kjernen - tasklet og workqueue, som er en spesiell form for multitasking. Du kan lese om avbrudd, oppgaver og arbeidskøer i boken "Linux Device Drivers" av Jonathan Corbet, Greg Kroah-Hartman, Alessandro Rubini, selv om informasjonen der noen ganger er utdatert.

Vi fortsetter temaet multithreading i Linux-kjernen. Sist gang jeg snakket om avbrudd, deres behandling og oppgaver, og siden det opprinnelig var meningen at dette skulle være én artikkel, vil jeg i min historie om arbeidskø referere til oppgaver, forutsatt at leseren allerede er kjent med dem.
Som forrige gang skal jeg prøve å gjøre historien min så detaljert og detaljert som mulig.

Artikler i serien:

  1. Multitasking i Linux-kjernen: arbeidskø

Arbeidskø

Arbeidskø- Dette er mer komplekse og tunge enheter enn oppgaver. Jeg vil ikke engang prøve å beskrive alle detaljene ved implementeringen her, men jeg håper jeg vil analysere de viktigste tingene mer eller mindre detaljert.
Arbeidskøer, som oppgaver, tjener til utsatt avbruddsbehandling (selv om de kan brukes til andre formål), men i motsetning til oppgaver utføres de i sammenheng med en kjerneprosess; følgelig trenger de ikke å være atomære og kan bruke søvn () funksjon, ulike synkroniseringsverktøy, etc.

La oss først forstå hvordan arbeidskøbehandlingsprosessen er organisert generelt. Bildet viser det veldig omtrentlig og forenklet, hvordan alt faktisk skjer er beskrevet i detalj nedenfor.

Flere enheter er involvert i denne mørke materien.
For det første, arbeidselement(bare arbeid for kort) er en struktur som beskriver funksjonen (for eksempel en avbruddsbehandler) som vi ønsker å planlegge. Den kan tenkes på som en analog av oppgavestrukturen. Ved planlegging ble oppgaver lagt til køer skjult for brukeren, men nå må vi bruke en spesiell kø - arbeidskø.
Oppgaver rakes av planleggerfunksjonen, og arbeidskøen behandles av spesielle tråder kalt arbeidere.
Arbeider's gir asynkron utførelse av arbeid fra arbeidskø. Selv om de kaller arbeid i rotasjonsrekkefølge, er det i det generelle tilfellet ikke snakk om streng, sekvensiell utførelse: Tross alt foregår forkjøp, søvn, venting osv. her.

Generelt er arbeidere kjernetråder, det vil si at de kontrolleres av Linux-kjerneplanleggeren. Men arbeiderne griper delvis inn i planleggingen for ytterligere organisering av parallell utførelse av arbeid. Dette vil bli diskutert mer detaljert nedenfor.

For å skissere hovedfunksjonene til arbeidskømekanismen foreslår jeg at du utforsker API.

Om køen og dens opprettelse

alloc_workqueue(fmt, flagg, max_active, args...)
Parametrene fmt og args er printf-formatet for navnet og argumentene til det. Parameteren max_activate er ansvarlig for det maksimale antallet verk som fra denne køen kan utføres parallelt på én CPU.
En kø kan opprettes med følgende flagg:
  • WQ_HIGHPRI
  • WQ_UNBOUND
  • WQ_CPU_INTENSIVE
  • WQ_FRYSBAR
  • WQ_MEM_RECLAIM
Spesiell oppmerksomhet bør rettes mot flagget WQ_UNBOUND. Basert på tilstedeværelsen av dette flagget, er køene delt inn i bundet og ubundet.
I koblede køer Når det legges til, er arbeid bundet til gjeldende CPU, det vil si at i slike køer, blir arbeid utført på kjernen som planlegger det. I denne forbindelse ligner bundne køer på oppgaver.
I ubundne køer arbeid kan utføres på hvilken som helst kjerne.

Et viktig trekk ved arbeidskøimplementeringen i Linux-kjernen er den ekstra organiseringen av parallell kjøring som er tilstede i bundne køer. Det er skrevet mer detaljert nedenfor, men nå vil jeg si at det er utført på en slik måte at minst mulig minne brukes, og slik at prosessoren ikke står uvirksom. Alt dette er implementert med antagelsen om at ett verk ikke bruker for mange prosessorsykluser.
Dette er ikke tilfelle for ubundne køer. I hovedsak gir slike køer ganske enkelt kontekst til arbeidere og starter dem så tidlig som mulig.
Derfor bør ukoblede køer brukes hvis CPU-intensiv arbeidsbelastning forventes, siden i dette tilfellet vil planleggeren ta seg av parallell kjøring på flere kjerner.

Analogt med oppgaver kan arbeid tildeles utførelsesprioritet, normal eller høy. Prioriteten er felles for hele køen. Som standard har køen normal prioritet, og hvis du setter flagget WQ_HIGHPRI, og følgelig høy.

Flagg WQ_CPU_INTENSIVE gir bare mening for bundne køer. Dette flagget er et avslag på å delta i en ekstra organisering av parallell utførelse. Dette flagget bør brukes når arbeid forventes å ta mye CPU-tid, i så fall er det bedre å flytte ansvaret til planleggeren. Dette er beskrevet mer detaljert nedenfor.

Flagg WQ_FRYSBAR Og WQ_MEM_RECLAIM er spesifikke og utenfor omfanget av emnet, så vi vil ikke dvele ved dem i detalj.

Noen ganger er det fornuftig å ikke lage dine egne køer, men å bruke vanlige. De viktigste:

  • system_wq - bundet kø for raskt arbeid
  • system_long_wq - en bundet kø for arbeider som forventes å ta lang tid å utføre
  • system_unbound_wq - ubundet kø

Om arbeidet og deres planlegging

La oss nå ta oss av arbeidet. La oss først se på initialiserings-, deklarasjons- og forberedelsesmakroene:
DECLARE(_DELAYED)_WORK(navn, void (*funksjon)(struct work_struct *work)); /* ved kompileringstid */ INIT(_DELAYED)_WORK(_work, _func); /* under utførelse */ PREPARE(_DELAYED)_WORK(_work, _func); /* for å endre funksjonen som utføres */
Verk legges til i køen ved hjelp av funksjonene:
bool køarbeid(struktur arbeidskøstruktur *wq, struktur arbeidsstruktur *arbeid); bool queue_delayed_work(struct workqueue_struct *wq, struct delayed_work *dwork, usignert lang forsinkelse); /* arbeid legges til i køen først etter at forsinkelsen har utløpt */
Dette er verdt å dvele mer ved. Selv om vi spesifiserer en kø som en parameter, plasseres faktisk ikke arbeid i selve arbeidskøen, som det kan virke, men i en helt annen enhet - i kølisten til worker_pool-strukturen. Struktur worker_pool, faktisk, er den viktigste enheten i organiseringen av arbeidskømekanismen, selv om den for brukeren forblir bak kulissene. Det er med dem arbeiderne jobber, og det er i dem all grunnleggende informasjon finnes.

La oss nå se hvilke bassenger som er i systemet.
Til å begynne med bassenger for bundne køer (på bildet). For hver CPU er to arbeidergrupper statisk tildelt: en for høyprioritert arbeid, den andre for arbeid med normal prioritet. Det vil si at hvis vi har fire kjerner, så blir det kun åtte tilknyttede bassenger, til tross for at arbeidskøen kan være så mange som ønskelig.
Når vi oppretter en arbeidskø, har den en tjeneste tildelt for hver CPU pool_workqueue(pwq). Hver slik pool_workqueue er assosiert med en arbeiderpool, som er allokert på samme CPU og tilsvarer prioritet til køtypen. Gjennom dem samhandler arbeidskøen med arbeiderbasen.
Arbeidere utfører arbeid fra arbeiderpoolen tilfeldig, uten å skille hvilken arbeidskø de opprinnelig tilhørte.

For ikke-tilknyttede køer tildeles arbeiderpooler dynamisk. Alle køer kan deles inn i ekvivalensklasser i henhold til deres parametere, og for hver slik klasse opprettes en egen arbeiderpool. De får tilgang til ved hjelp av en spesiell hash-tabell, der nøkkelen er et sett med parametere, og verdien er henholdsvis arbeiderpoolen.
Faktisk, for ubundne køer er alt litt mer komplisert: hvis for bundne køer pwq og køer ble opprettet for hver CPU, her opprettes de for hver NUMA-node, men dette er en ekstra optimalisering som vi ikke vil vurdere i detalj.

Alle slags småting

Jeg vil også gi noen funksjoner fra API for å fullføre bildet, men jeg vil ikke snakke om dem i detalj:
/* Force fullføring */ bool flush_work(struct work_struct *work); bool flush_delayed_work(struct delayed_work *dwork); /* Avbryt utførelse av arbeid */ 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); /* Slett en kø */ void destroy_workqueue(struct workqueue_struct *wq);

Hvordan arbeiderne gjør jobben sin

Nå som vi er kjent med API, la oss prøve å forstå mer detaljert hvordan det hele fungerer og administreres.
Hvert basseng har et sett med arbeidere som håndterer oppgaver. Dessuten endrer antallet arbeidere seg dynamisk, og tilpasser seg dagens situasjon.
Som vi allerede har funnet ut, er arbeidere tråder som utfører arbeid i sammenheng med kjernen. Arbeideren henter dem i rekkefølge, en etter en, fra arbeiderpoolen knyttet til den, og arbeiderne kan, som vi allerede vet, tilhøre forskjellige kildekøer.

Arbeidere kan betinget være i tre logiske tilstander: de kan være inaktive, kjørende eller administrerende.
Arbeider kan stå stille og gjør ingenting. Dette er for eksempel når alt arbeidet allerede er utført. Når en arbeider går inn i denne tilstanden, går den i dvale og vil følgelig ikke utføre før den er vekket;
Hvis bassengadministrasjon ikke er nødvendig og listen over planlagte arbeider ikke er tom, begynner arbeideren å utføre dem. Vi vil konvensjonelt kalle slike arbeidere løping.
Om nødvendig tar arbeideren på seg rollen sjef basseng. En pool kan ha enten bare én administrerende arbeider eller ingen arbeider i det hele tatt. Dens oppgave er å opprettholde det optimale antallet arbeidere per basseng. Hvordan gjør han det? For det første slettes arbeidere som har vært inaktive i lang tid. For det andre opprettes nye arbeidere hvis tre betingelser er oppfylt samtidig:

  • det er fortsatt oppgaver å fullføre (fungerer i bassenget)
  • ingen ledige arbeidere
  • det er ingen arbeidere (det vil si aktive og ikke sovende)
Den siste betingelsen har imidlertid sine egne nyanser. Hvis bassengkøene er ubundne, tas det ikke hensyn til løpende arbeidere; for dem er denne betingelsen alltid sann. Det samme gjelder for en arbeider som utfører en oppgave fra en koblet, men med flagget WQ_CPU_INTENSIVE, køer. Dessuten, i tilfelle av bundne køer, siden arbeidere jobber med arbeider fra felles basseng (som er en av to for hver kjerne på bildet ovenfor), viser det seg at noen av dem regnes som arbeidende, og noen ikke. Det følger også at utføre arbeid fra WQ_CPU_INTENSIVE køer starter kanskje ikke umiddelbart, men de selv forstyrrer ikke utførelsen av annet arbeid. Nå skal det være klart hvorfor dette flagget heter det, og hvorfor det brukes når vi forventer at arbeidet vil ta lang tid å fullføre.

Regnskap for arbeidende arbeidere utføres direkte fra Linux-kjerneplanleggeren. Denne kontrollmekanismen sikrer et optimalt samtidighetsnivå, forhindrer at arbeidskøen skaper for mange arbeidere, men får heller ikke arbeidet til å vente unødvendig for lenge.

De som er interessert kan se på arbeiderfunksjonen i kjernen, den heter worker_thread().

Alle beskrevne funksjoner og strukturer finnes mer detaljert i filene include/linux/workqueue.h, kernel/workqueue.c Og kernel/workqueue_internal.h. Det er også dokumentasjon på arbeidskø inn Documentation/workqueue.txt.

Det er også verdt å merke seg at arbeidskømekanismen brukes i kjernen ikke bare for utsatt avbruddsbehandling (selv om dette er et ganske vanlig scenario).

Dermed så vi på mekanismene for utsatt avbruddshåndtering i Linux-kjernen - tasklet og workqueue, som er en spesiell form for multitasking. Du kan lese om avbrudd, oppgaver og arbeidskøer i boken "Linux Device Drivers" av Jonathan Corbet, Greg Kroah-Hartman, Alessandro Rubini, selv om informasjonen der noen ganger er utdatert.