Ievads operētājsistēmās
Sveiki, Habr! Vēlos jums iepazīstināt ar tulkojumu sēriju no interesantas literatūras — OSTEP. Šajā rakstā tiek detalizēti aplūkota Unix līdzīgu operētājsistēmu darbība, proti — procesu pārvaldība, dažādi plānotāji, atmiņa un tamlīdzīgi komponenti, kas veido mūsdienu operētājsistēmu. Visu materiālu oriģinālus varat apskatīt šeit . Lūdzu, ņemiet vērā, ka tulkojums veikts neprofesionāli (īpaši brīvi), taču ceru, ka esmu saglabājis kopējo nozīmi.
Laboratorijas darbus par šo tēmu var atrast šeit:
— oriģināls:
— oriģināls:
— mana personīgā adaptācija:
Un jūs varat apmeklēt manu kanālu =)
Programmas darbība
Kas notiek, kad darbojas noteikta programma? Palaista programma veic vienu vienkāršu uzdevumu — tā izpilda instrukcijas. Katru sekundi miljardi instrukciju tiek izvilktas procesora no operatīvās atmiņas, un procedūra tās dekodē (piemēram, nosaka, kādam tipam pieder šīs instrukcijas) un izpilda. Tās var būt divu skaitļu saskaitīšana, piekļuve atmiņai, nosacījuma pārbaude, pāreja uz funkciju un tamlīdzīgi. Pabeidzis vienas instrukcijas izpildi, procesors pāriet pie citas. Un tā, instrukcija pēc instrukcijas, tās tiek izpildītas, līdz programma beidzas.
Šis piemērs, protams, ir aplūkots vienkāršoti — patiesībā, lai paātrinātu procesora darbu, mūsdienu aparatūra ļauj izpildīt instrukcijas bez secības, aprēķināt iespējamos rezultātus, izpildīt instrukcijas vienlaikus un veikt tamlīdzīgas viltības.
Fon-Neimaņa skaitļošanas modelis
Mūsu aprakstītā vienkāršotā darba forma ir līdzīga Fon-Neimaņa skaitļošanas modelim. Fon-Neimans ir viens no datoru sistēmu pionieriem, viņš ir arī viens no spēļu teorijas autoriem. Programmas izpildes laikā notiek vēl daudzas citas darbības, darbojas neskaitāmi citi procesi un blakus loģika, kuru galvenais mērķis ir vienkāršot sistēmas palaišanu, darbību un apkalpošanu.
Ir существует programmatūras kopums, kas atbild par programmu palaišanas vienkāršību (vai pat ļauj startēt vairākas programmas vienlaikus), tas ļauj programmām dalīties tajā pašā atmiņā, kā arī mijiedarboties ar dažādām iekārtām. Šādu programmatūras kopumu būtībā sauc par operētājsistēmu, un tās uzdevumos ietilpst uzraudzīt, lai sistēma darbotos pareizi un efektīvi, kā arī nodrošināt šīs sistēmas pārvaldības vienkāršību.
Operētājsistēma
Operētājsistēma, īsāk - OS, ir savstarpēji saistītu programmu komplekss, kas paredzēts datoru resursu pārvaldīšanai un lietotāja mijiedarbības organizēšanai ar datoru..
OS sasniedz savu efektivitāti pirmkārt, izmantojot vissvarīgāko tehniku - tehnoloģiju virtualizāciju. OS mijiedarbojas ar fiziskajiem resursiem (procesoru, atmiņu, disku un tamlīdzīgi) un pārvērš tos par vispārīgāku, ar plašākām iespējām un lietošanai ērtāku formu. Tādēļ vispārējai izpratnei var ļoti rupji salīdzināt operētājsistēmu ar virtuālo mašīnu.
Lai lietotāji varētu dot komandas operētājsistēmai un tādējādi izmantot virtuālās mašīnas iespējas (piemēram: programmas palaišana, atmiņas piešķiršana, piekļuve failam utt.), operētājsistēma nodrošina noteiktu saskarni, ko sauc par API (lietojumprogrammu izstrādes saskarni) uz kuru var veikt izsaukumus (call). Tipiska operētājsistēma nodrošina iespēju veikt simtiem sistēmisko izsaukumu.
Un visbeidzot, ņemot vērā, ka virtualizācija ļauj daudziem programmām darboties (tādējādi kopīgi izmantot CPU), un vienlaikus piekļūt to instrukcijām un datiem (tādējādi daloties ar atmiņu), kā arī iegūt piekļuvi diskam (tādējādi kopīgi izmantojot ievades/izvades ierīces), operētājsistēmu vēl sauc par resursu pārvaldnieku. Katrs procesors, disks un atmiņa ir sistēmas resurss, un tādējādi viens no operētājsistēmas uzdevumiem ir šo resursu efektīva pārvaldība, kā arī godprātīga vai, pretēji, atkarībā no uzdevuma, kuram operētājsistēma ir izstrādāta.
CPU virtualizācija
Apskatīsim nākamo programmu:
(https://www.youtube.com/watch?v=zDwT5fUcki4&feature=youtu.be)

Tā neveic nekādas īpašas darbības, būtībā viss, ko tā dara — izsauc funkciju spin(), kuras uzdevums ir cikliski pārbaudīt laiku un atgriezties pēc tam, kad pagājusi viena sekunde. Tādējādi tā nebeidzami atkārto rindu, kuru lietotājs nodevis kā argumentu.
Palaižam šo programmu un nododam tai argumentā simbolu “A”. Rezultāts iznāk ne īpaši interesants — sistēma vienkārši izpilda programmu, kas periodiski izvada ekrānā simbolu “A”.
Tagad izmēģināsim variantu, kad tiek palaižami daudzi vienas un tās pašas programmas piemēri, bet katrs parāda dažādas burtus, lai būtu skaidrāk. Šajā gadījumā rezultāts iznāk nedaudz citādāks. Neskatoties uz to, ka mums ir viens procesors, programma tiek izpildīta vienlaicīgi. Kāpēc tā notiek? Tas notiek tāpēc, ka operētājsistēma, ne bez aparatūras palīdzības, rada ilūziju. Ilūziju par to, ka sistēmā ir vairāki virtuāli procesori, pārvēršot vienu fizisko procesoru teorētiski nebeidzamā daudzumā un tādējādi ļaujot programmām izskatīties, ka tās tiek izpildītas vienlaicīgi. Šādu ilūziju sauc par CPU virtualizāciju.
Šāda aina rada daudz jautājumu, piemēram, ja vairākas programmas vēlas palaisties vienlaicīgi, tad kura tieši tiks palaista? Par šo jautājumu atbild “politikas” OS. Politikas tiek izmantotas daudzviet operētājsistēmā un atbild uz līdzīgiem jautājumiem, kā arī ir pamata mehānismi, kurus OS īsteno. No tā izriet arī OS loma kā resursu pārvaldnieka.
Atmiņas virtualizācija
Tagad apskatīsim atmiņu. Fiziskais atmiņas modelis mūsdienu sistēmās tiek attēlots kā byte masīvs. Lai lasītu no atmiņas, nepieciešams norādīt šūnas adresi, lai iegūtu piekļuvi. Lai ierakstītu vai atjauninātu datus, nepieciešams norādīt datus un šūnas adresi, kur tos ierakstīt.
Piekļuve atmiņai notiek pastāvīgi programmas darbības procesā. Programma saglabā atmiņā visu tās datu struktūru un piekļūst tai, izpildot dažādas instrukcijas. Starp citu, instrukcijas arī tiek glabātas atmiņā, tāpēc piekļuve tai notiek arī katram pieprasījumam attiecībā uz nākamo instrukciju.
Izsaukums malloc()
Apskatīsim nākamo programmu, kas piešķir atmiņas bloku, izmantojot saucienu malloc() (https://youtu.be/jnlKRnoT1m0):

Programa veic vairākas darbības. Vispirms tā piešķir noteiktu daudzumu atmiņas (rindā 7), tad izvada piešķirtās atmiņas šūnas adresi (rindā 9), ieraksta nulli pirmajā piešķirtās atmiņas slotā. Pēc tam programma ienāk ciklā, kurā inkrementē vērtību, kas ierakstīta atmiņā, adrese atrodas mainīgajā “p”. Tāpat tā izvada procesa identifikatoru pati par sevi. Procesa identifikators ir unikāls katram palaistam procesam. Ja palaidīsim vairākas kopijas, mēs sastapsim interesantu rezultātu: Pirmajā gadījumā, ja neko nedarīsim un vienkārši palaidīsim vairākas kopijas, adreses būs atšķirīgas. Bet tas taču nesakrīt ar mūsu teoriju! Jā, jo mūsdienu distribūcijās pēc noklusējuma ir iekļauta atmiņas nejaušības funkcija. Ja to atspēkosim, iegūsim gaidīto rezultātu — atmiņas adreses divām vienlaikus strādājošām programmām būs vienādas.

Tādējādi iznāk, ka divas neatkarīgas programmas strādā ar savām privātajām adresātu telpām, kuras savukārt operētājsistēma attēlo fiziskajā atmiņā. Tāpēc atmiņas adresu izmantošana vienas programmas ietvaros nekādā veidā neietekmēs citas un katrai programmai šķiet, ka tai ir savs fiziskās atmiņas gabals, kas pilnībā nodots tās rīcībā. Patiesība tomēr ir tāda, ka fiziskā atmiņa ir koplietojams resurss, kura pārvaldību veic operētājsistēma.
Sakritība
Vēl viens no svarīgajiem tematiem operētājsistēmu ietvarā — sakritība. Šis termins tiek izmantots, kad runa ir par problēmām sistēmā, kas var rasties, strādājot ar daudziem objektiem vienlaikus vienas programmas ietvaros. Sakritības problēmas rodas pat pašā operētājsistēmā. Iepriekšējos piemēros ar atmiņas un procesora virtualizāciju mēs sapratām, ka operētājsistēma pārvalda daudzas lietas vienlaikus — sāk pirmo procesu, pēc tam otro un tā tālāk. Izrādās, šāda uzvedība var radīt dažas problēmas. Piemēram, mūsdienu daudz pavedienu programmas saskaras ar šādām grūtībām.
Apskatīsim nākamo programmu:

Programma galvenajā funkcijā izveido divus pavedienus, izmantojot izsaukumu Pthread_create(). Šajā piemērā pavediena var domāt kā funkciju, kas tiek izpildīta vienā atmiņas telpā līdzās citām funkcijām, turklāt vienlaikus tiek palaistas vairāk nekā viena funkcija. Šajā piemērā katrs pavediens sākas un izpilda funkciju worker(), kas savukārt vienkārši palielina mainīgo,.
. Palaižam šo programmu ar argumentu 1000. Kā jau varējāt nojaust, rezultātam jābūt 2000, jo katrs pavediens palielina mainīgo 1000 reizes. Tomēr viss nav tik vienkārši. Mēģināsim palaist programmu ar skaitli, kas ir par kārtu lielāks.

Ievadot skaitli, piemēram, 100000, mēs gaidām redzēt izvadē skaitli 200000. Tomēr, palaižot skaitli 100000 vairākas reizes, mēs ne tikai neredzēsim pareizo atbildi, bet arī saņemsim dažādas nepareizas atbildes. Atbilde slēpjas tajā, ka skaita palielināšanai nepieciešamas trīs operācijas — izvilkšana no atmiņas, palielināšana un pēc tam skaitļa ierakstīšana atpakaļ. Tā kā visas šīs instrukcijas netiek veiktas atomiski (vienlaikus), var notikt šādas dīvainas lietas. Šī problēma programmēšanā tiek dēvēta par race condition — sacensību stāvoklis. Kad nezināmas spēks nezināmā brīdī var ietekmēt kādu no jūsu operācijām.
Avots: habr.com
