Skoči na vsebino

Uporaba lupine

Glavna naloga lupine je zaganjanje ukazov. Na primer:

$ date
Mon Sep 13 15:54:46 CEST 2021
$ hostname
nsc-login1.ijs.si
date
hostname

Prvo besedo v vrstici lupina običajno razume kot ime programa, preostanek pa kot argumente temu programu. V ukazu

$ echo foo bar "1  2  3"
foo bar 1  2  3
echo foo bar "1  2  3"

smo programu echo podali tri argumente – besede v narekovajih lupina obravnava kot en argument. Pomen argumenta je odvisen od ukaza, kateremu mu je podan. V naslednjemu ukazu programu srun podamo ime programa, ki naj ga zažene na računskem vozlišču:

$ srun hostname
srun: job 1121261 queued and waiting for resources
srun: job 1121261 has been allocated resources
nsc-msv003.ijs.si
srun hostname

Tokovi

Zadnji ukaz je izpisal tri vrstice. Prvi dve sta diagnostični sporočili, zadnja pa je dejanski izpis programa hostname z računskega vozlišča. Čeprav ni razvidno iz samega izpisa ukaza, se „normalna“ sporočila izpisujejo na standardni izhodni tok stdout (angl. standard output), diagnostična pa na standardni tok za napake stderr (angl. standard error). Privzeto tok stdout uporablja medpomnjenje (vrstično/bločno), tok stderr pa ne, torej je njegov izpis takojšen.

Izpis programa lahko preusmerimo v datoteko z operatorjem >. Če datoteka že obstaja, lupina njeno vsebino najprej izbriše.

$ srun hostname > izpis.txt
srun: job 1121281 queued and waiting for resources
srun: job 1121281 has been allocated resources
srun hostname > izpis.txt

Zdaj ukaz na zaslon izpiše le diagnostična sporočila. Vsebino datoteke z izpisom si ogledamo s programom cat:

$ cat izpis.txt
nsc-msv003.ijs.si
cat izpis.txt

Pogosto želimo prezreti diagnostična sporočila. To lahko dosežemo tako, da z operatorjem 2> preusmerimo napake v navidezno datoteko /dev/null, ki se obnaša kot črna luknja:

$ srun hostname 2> /dev/null
nsc-msv003.ijs.si
srun hostname 2> /dev/null

Namesto preusmeritve bi lahko za srun uporabili argument --quiet, s katerim ne izpisuje diagnostičnih sporočil. Mnogo programov razume podoben argument; prednost preusmeritve je, da dela za vse programe.

Poglejmo si še primer preusmeritve obeh izhodnih tokov. Pri tem lahko preusmeritev standardnega izhoda stdout pišemo tudi kot 1>:

$ srun hostname 1> izpis.txt 2> /dev/null
srun hostname 1> izpis.txt 2> /dev/null

Poleg izhodnih tokov za sporočila in diagnostiko ima vsak proces še standardni vhod (angl. standard input, krajše stdin), skozi katerega dobi vhodne podatke. Za primer si oglejmo wc (angl. word count), ki na vhodu prebere besedilo in izpiše število vrstic, besed in znakov v njem. Ko poženemo ukaz, vtipkamo ali prilepimo še vhodno besedilo, ki ga zaključimo s Ctrl+D (včasih ga moramo pritisniti dvakrat):

$ wc
vpišemo eno
ali več kratkih vrstic
znakov in besed
      3       9      53
wc
vpišemo eno
ali več kratkih vrstic
znakov in besed

Standardni vhod lahko z operatorjem < preberemo tudi iz datoteke. Preštejmo znake v datoteki izpis.txt od prej:

$ wc < izpis.txt
 1  1 18
wc < izpis.txt

Namesto preusmeritve zna wc tudi sam prebrati vsebino datotek, ki jih podamo v ukazni vrstici. Prednost preusmeritve je spet v tem, da dela isto za vse programe. Poleg tega nam standardni tokovi omogočijo delo z enim najmočnejših orodij v lupini, cevovodi.

Cevovodi

Pogosto želimo izpis nekega ukaza še popraviti, razvrstiti ali presejati. Spomnimo se, da si s programom squeue ogledamo posle Slurm v izvajanju oziroma čakalni vrsti:

$ squeue
   JOBID PARTITION     NAME     USER ST       TIME  NODES NODELIST(REASON)
 1121314  gridlong crpropa_   gen015 PD       0:00      1 (Resources)
 1121315  gridlong crpropa_   gen015 PD       0:00      1 (Priority)
 […]
 1119265  gridlong mc15_13T prdatlas  R    8:25:00      1 nsc-gsv003
 1119378  gridlong mc15_13T prdatlas  R    8:23:24      1 nsc-msv003
squeue

Posle lahko preštejemo s pomočjo wc. Z operatorjem | ustvarimo cevovod med programoma squeue in wc, ki standardni izhod prvega preusmeri na standardni vhod drugega. Argument --lines poskrbi, da wc izpiše samo število vrstic, ne pa tudi znakov in besed. Znak | zapišemo s pritiskom kombinacije AltGr+W.

$ squeue | wc --lines
650
squeue | wc --lines

V resnici je število poslov za ena manjše, saj je v prvi vrstici glava z imeni polj. Popravimo naš ukaz, da bo program wc dobil samo vrstice od druge naprej:

$ squeue | tail -n +2 | wc --lines
649
squeue | tail -n +2 | wc --lines

V cevovod smo vrinili še tail, ki z danimi argumenti izpisuje le vrstice od druge naprej. Če želimo izpisati le posle v čakanju (stanje PD), izpis še presejemo z grep:

$ squeue | tail -n +2 | grep PD | wc --lines
406
squeue | tail -n +2 | grep PD | wc --lines

Program grep ohrani le vrstice, ki vsebujejo podan vzorec. Seveda bi namesto grep lahko za squeue uporabili argument --states PD, ki že v začetku izpiše le take vrstice. To je bolj zanesljivo, saj se ne zmoti, če se niz PD pojavi še na kakšnem drugem mestu. Je pa filtriranje z grep precej priročno, sploh za interaktivno delo.

Izpišimo še vse posle in jih razvrstimo po porabljenem procesorskem času. Uporabimo sacct, ki mu naročimo, naj izpiše samo informacije o celih poslih (-X) vseh uporabnikov (-a), brez glave v prvi vrstici (-n). Izpisana polja določimo z argumentom -o.

$ sacct -Xan -o JobID,JobName,User,CPUTimeRaw | sort -n -k 4
1191775      crpropa_n+    gen015          0 
[…]
649281       mc15_14Te+ prdatlas+  265193856 
649226       N6801227b+ prdatlas+  265239504 
sacct -Xan -o JobID,JobName,User,CPUTimeRaw | sort -n -k 4

Izpis smo preusmerili na vhod programa sort. Argument -n uporabi številsko razvrščanje namesto slovarskega, z -k določimo polje, po katerem razvrščamo. Pri tem so polja ločena s poljubno dolgim zaporedjem presledkov.

Naštejmo še imena stotih poslov z največ porabljenega procesorskega časa. Spodaj s tr -s ' ' vsako zaporedje presledkov zamenjamo z enim samim presledkom, s cut -d ' ' -f 2 pa nato v vsaki vrstici ohranimo le (-f) drugo polje, pri čemer so polja ločena (-d) s presledki. Program uniq nato odstrani ponovljene vrstice v (urejenem) izpisu.

$ sacct -Xan -o JobID,JobName,User,CPUTimeRaw |
>     sort -n -k 4 |
>     tail -n 100 |
>     tr -s ' ' |
>     cut -d ' ' -f 2 |
>     sort |
>     uniq
crpropa_n+
data15_13+
mc15_13Te+
mc15_14Te+
mc15_vali+
mc16_13Te+
Mete
N6801227b+
run-senti+
sacct -Xan -o JobID,JobName,User,CPUTimeRaw |
    sort -n -k 4 |
    tail -n 100 |
    tr -s ' ' |
    cut -d ' ' -f 2 |
    sort |
    uniq

Za vajo odstranimo enega ali več podukazov iz cevovoda in si ogledamo delne transformacije izpisa, ki jih tako dobimo.

Skripte

Če zaporedje ukazov zapišemo v datoteko, dobimo skripto – program, ki ga poženemo z lupino. Napišimo program, ki izpiše datum in uro ter ime sistema, kjer ga poganjamo:

echo "Trenutni čas in lokacija:"
date
hostname

Skripto shranimo v datoteko skripta.sh in poženemo z

$ sh skripta.sh
Trenutni čas in lokacija:
Mon May 29 13:46:12 CEST 2023
nsc-login1.ijs.si
sh skripta.sh

Na začetek skripte ponavadi dodamo vrstico s potjo do lupine #!/bin/sh:

#!/bin/sh
# Znak # v splošnem označuje komentar; lupina ignorira vse, kar v vrstici
# sledi temu znaku. Komentar #! na začetku datoteke je poseben – operacijskemu
# sistemu naroči, naj za zagon skripte uporabi podani program.

echo "Trenutni čas in lokacija:" # še en komentar, ki si deli vrstico z ukazom
date
hostname

Če datoteki s skripto dodamo še dovoljenje za izvajanje s

$ chmod u+x skripta.sh
chmod u+x skripta.sh

jo lahko zaženemo tudi neposredno, brez sh:

$ ./skripta.sh
Trenutni čas in lokacija:
Mon May 29 13:46:59 CEST 2023
nsc-login1.ijs.si
./skripta.sh

Skripto, ki se začne z vrstico #!, lahko v sistemu Slurm pošljemo v izvajanje z ukazom sbatch. Prvi vrstici v skripti lahko sledijo še poljubni argumenti za sbatch, ki jih prav tako zapišemo kot posebne komentarje. Preusmerimo izpis skripte v datoteko skripta.out in napake v datoteko skripta.err ter omejimo čas izvajanja na dve minuti:

#!/bin/sh
#SBATCH --output=skripta.out
#SBATCH --error=skripta.err
#SBATCH --time=2:00

echo "Trenutni čas in lokacija:"
date
hostname

Poženemo posel in izpišemo rezultat:

$ sbatch skripta.sh
Submitted batch job 1130529
$ # Počakamo, da se posel konča (komentar lahko zapišemo tudi v ukazni vrstici).
$ cat skripta.out 
Trenutni čas in lokacija:
Tue Sep 14 10:38:07 CEST 2021
nsc-fp003.ijs.si
sbatch skripta.sh
cat skripta.out

Spremenljivke

Poljubno vrednost lahko shranimo v spremenljivko in jo uporabimo kasneje. Vrednost spremenljivke x nastavimo z x=… in uporabimo z $x:

$ x=42
$ echo "Vrednost spremenljivke x je $x"
Vrednost spremenljivke x je 42
x=42
echo "Vrednost spremenljivke x je $x"

Opomba

V x=y okoli simbola = ne sme biti presledkov, sicer bo lupina poskusila pognati program x z argumentoma = in y. Za dostop do vrednosti pa je predvsem v skriptah namesto $spremenljivka bolje pisati "${spremenljivka}". Oblika z narekovaji se namreč obnaša pravilno tudi, kadar vrednost vsebuje presledke.

Izpis nekega ukaza lahko z operatorjem $(…) uporabimo v drugem ukazu. Tako si lahko recimo zapomnimo številko posla, ki smo ga poslali v izvajanje z ukazom sbatch (argument --parsable poskrbi, da sbatch izpiše samo številko, brez spremnega besedila):

$ job_id=$(sbatch --parsable skripta.sh)
$ echo "Številka posla je $job_id"
Številka posla je 1177884
$ seff $job_id
Job ID: 1177884
Cluster: nsc
User/Group: user/user
State: COMPLETED (exit code 0)
Cores: 1
CPU Utilized: 00:00:00
CPU Efficiency: 0.00% of 00:00:00 core-walltime
Job Wall-clock time: 00:00:01
Memory Utilized: 0.00 MB (estimated maximum)
Memory Efficiency: 0.00% of 3.91 GB (1.95 GB/core)
job_id=$(sbatch --parsable skripta.sh)
echo "Številka posla je $job_id"
seff $job_id

Poleg spremenljivk, ki jih nastavimo sami, ponavadi obstaja še vrsta okoljskih spremenljivk, ki jih nastavi sistem. Med njimi so informativne spremenljivke, kot sta $HOME in $PWD z domačim in trenutnim imenikom. Druge spremenljivke vplivajo na delovanje posameznih programov. Ena najpomembnejših je $PATH s seznamom imenikov, v katerih lupina išče programe. Trenutne vrednosti okoljskih spremenljivk izpišemo s printenv:

$ echo $HOME
/ceph/grid/home/user
$ printenv
[…]
PATH=/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin
[…]
echo $HOME
printenv

V skriptah lahko uporabimo še nekaj posebnih spremenljivk, ki jih za nas nastavi lupina. Shranimo skripto parametri.sh, ki prikaže najpomembnejše med njimi:

1

Skripto zaženemo:

$ chmod +x parametri.sh
$ ./parametri.sh "ena   dva" pet   sedem   devet
Ime skripte: ./parametri.sh
Št. argumentov: 4
Prvi argument: ena dva
Vsi argumenti: ena dva pet sedem devet
Prvi argument z narekovaji: ena   dva
Vsi argumenti z narekovaji: ena   dva pet sedem devet
chmod +x parametri.sh
./parametri.sh "ena   dva" pet   sedem   devet

Lupina shrani rezultat (angl. exit status) vsakega ukaza v spremenljivko $?. Po dogovoru vrednost 0 pomeni uspešno izvajanje, ostale vrednosti pa označujejo razne napake.

$ ls ta-datoteka-ne-obstaja.xtx
ls: cannot access 'ta-datoteka-ne-obstaja.xtx': No such file or directory
$ echo $?
2
ls ta-datoteka-ne-obstaja.xtx
echo $?

Pogojni stavek

Če želimo ukaz izvesti le pod določenim pogojem, uporabimo pogojni stavek if. V splošnem ima obliko

if <test> 
then
    <ukazi>   
else
    <ukazi>
fi

Vse ukaze med then in else lupina izvede le, če ukaz CMD vrne rezultat 0, v nasprotnem primeru pa izvede ukaze med else in fi. Primer else lahko tudi izpustimo.

Pogosto vidimo tudi obliko, kjer je beseda then v isti vrstici kot if - v tem primeru moramo sestavne dele pogojnega stavka ločiti s podpičjem. Za celovitost pokažimo še preverjanje dodatnih testov z elif:

if <test> ; then
    <ukazi>
elif <test> ; then
    <ukazi>    
else
    <ukazi>
fi

Tako pa pogojni stavek zapišemo kot enovrstičnico:

if <test> ; then <ukazi> ; elif <test> ; then <ukazi> ; else <ukazi> ; fi

Preverimo z if, če lahko na gruči dobimo vozlišče z 10 TB pomnilnika:

$ if srun --mem 10T hostname ; then
>     echo "Uspešno opravil posel."
> else
>     echo "Posla ni bilo moč zagnati."
> fi
srun: error: Memory specification can not be satisfied
srun: error: Unable to allocate resources: Requested node configuration is not available
Posla ni bilo moč zagnati.
if srun --mem 10T hostname ; then
    echo "Uspešno zaključil posel."
else
    echo "Posla ni bilo moč zagnati."
fi

Opomba

Lupina izpiše tudi sporočila programov, ki jih uporabimo v pogoju. Če tega nočemo, lahko izpis za te programe preusmerimo v »črno luknjo« z > /dev/null 2> /dev/null. Oba izhoda preusmerimo v isto datoteko z 2>&1 > /dev/null; v večini lupin deluje tudi &> /dev/null.

Pogosto želimo preveriti kakšen drug pogoj. Pomaga nam program test, s katerim lahko med drugim primerjamo nize in števila. test ne izpiše ničesar in vrne 0, če je pogoj izpolnjen.

$ x=42
$ test $x -gt 10    # "greater than"
$ echo $?
0
$ test "a" = "b"    # za primerjanje števil uporabimo -eq
$ echo $?
1
x=42
test $x -gt 10    # "greater than"
echo $?
test "a" = "b"    # za primerjanje števil uporabimo -eq
echo $?

V kombinaciji s pogojnim stavkom lahko test uporabimo tako (tu smo cel ukaz zapisali v eni vrstici, zato smo uporabili ;):

$ if test 3 -gt 2 ; then echo "tri je večje od dva" ; fi
tri je večje od dva
if test 3 -gt 2 ; then echo "tri je večje od dva" ; fi

Za lepše branje lahko namesto test … pišemo tudi [ … ]. Pri tem pazimo, da med [ in ] ne izpustimo kakšnega presledka.

$ if [ ! a != a ] ; then echo "ni res, da je a različno od a" ; fi
ni res, da je a različno od a
if [ ! a != a ] ; then echo "ni res, da je a različno od a" ; fi

Zanka

Enega ali več ukazov lahko z uporabo zanke for ponovimo za vsak element danega seznama. V splošnem ima zanka obliko

for VAR in A B C ... ; do
    <ukazi>
done

pri čemer je VAR spremenljivka, ki zaporedoma dobiva vrednosti A, B in C. To spremenljivko lahko (kot $VAR) uporabimo v telesu zanke. Za primer poiščimo prafaktorje števil med ena in deset:

$ for i in $(seq 10) ; do
>     factor $i
> done
1:
2: 2
3: 3
4: 2 2
5: 5
6: 2 3
7: 7
8: 2 2 2
9: 3 3
10: 2 5
for i in $(seq 10) ; do
    factor $i
done

Spomnimo se, da lupina $(…) zamenja z izpisom ukaza med narekovaji. Pogosto želimo telo zanke ponoviti za vse datoteke, ki ustrezajo vzorcu:

$ for ime in *.txt ; do
>     echo V trenutnem imeniku obstaja datoteka $ime
> done
for ime in *.txt ; do
    echo V trenutnem imeniku obstaja datoteka $ime
done

Preveri svoje znanje

Vaja

Vaja 02: na podatkih preizkusi zgornje ukaze.