Cilj ove laboratorijske vježbe je praktično proučavanje i upoznavanje sa sustavskim pozivima vezanim uz stvaranje i upravljanje procesima. U okviru vježbe potrebno je ostvariti jednostavnu ljusku, koja se može zvati fsh
(Fer SHell).
Ljusku je potrebno ostvariti u programskom jeziku C ili C++ (bilo koji standard) i u bilo kojem okruženju UNIX-a (npr. GNU/Linux, FreeBSD, itd.). Dozvoljeno je korištenje svih funkcija iz standardne biblioteke C-a (npr. glibc
, musl
) osim funkcije system
.
Ove upute obrađuju sljedeće funkcionalnosti ljuske:
PATH
.SIGINT
.Glavni izvor informacija o naredbama i funkcijama koje pruža operacijski sustav je naredba man
. Dokumentacija sustava grupirana je u odjeljke. Na primjer,
Naredba oblika man <ime>
slijedom pretražuje odjeljke i vraća dokumentaciju prve naredbe ili funkcije koja odgovara imenu.
U slučajevima kad više naredbi ili funkcija dijeli ime, naredbi man
možemo eksplicitno zadati odjeljak kao što je prikazano u sljedećem primjeru.
sleep
.Pretpostavimo da želimo dokumentaciju bibliotečne funkcije sleep
. Naredba man sleep
će nam umjesto toga dati dokumentaciju naredbe sleep
:
$ man sleep
SLEEP(1) User Commands SLEEP(1)
NAME
sleep - delay for a specified amount of time
SYNOPSIS
sleep NUMBER[SUFFIX]...
sleep OPTION
...
Dokumentaciji bibliotečne funkcije sleep
možemo pristupiti ovako:
$ man 3 sleep
sleep(3) Library Functions Manual sleep(3)
NAME
sleep - sleep for a specified number of seconds
LIBRARY
Standard C library (libc, -lc)
SYNOPSIS
#include <unistd.h>
unsigned int sleep(unsigned int seconds);
...
Sljedeći pseudokod ugrubo opisuje rad ljuske. Prije čekanja na upisivanje naredbe, ljuska treba ispisati odzivnik (engl. prompt) fsh>
. Svaka naredba sastoji se od imena i argumenata odvojenih proizvoljnim brojem razmaka.
while (1) {
print "fsh> ";
wait for and read user input;
parse user input;
execute command if built in;
if (command not built in){
execute the command in a child process;
wait until the child has exited;
}
}
fsh> /usr/bin/pwd
/home/student
fsh> /usr/bin/echo "hello"
hello
fsh> /usr/bin/ls
example.txt data documents projects
fsh> /bin/asdf
fsh: /bin/asdf: No such file or directory
Pune putanje do programa dane u primjeru mogu se razlikovati ovisno o sustavu. Putanje do programa možete provjeriti pomoću naredbe whereis
.
U ovoj laboratorijskoj vježbi za pokretanje programa nije dozvoljeno korištenje funkcije system
, koja koristi ljusku sh
, ni funkcija execvp
, execlp
i execvpe
, koje mogu tražiti program ako prvi argument (file
) predstavlja ime programa. Trebate koristiti sustavski poziv execve
, koji kao argumente prima prima putanju do programa, pathname
, niz argumenata naredbe, argv
, i niz varijabli okoline, envp
, kojem se može pristupiti sljedećom deklaracijom:
extern char **environ;
fork
, wait
, i execve
te pomoću njih ostvariti pokretanje programa.perror
ako se execve
uspješno izvrši u sljedećem isječku koda?
execve(path, argv, environ);
perror(path);
/usr/bin/printenv
ako se kao zadnji argument funkciji execve
preda NULL
umjesto varijable environ
?Ugrađena naredba exit
treba omogućiti izlazak iz ljuske.
Ugrađena naredba cd
treba omogućiti navigaciju po datotečnom sustavu. Ona se mož ostvariti pomoću sustavskog poziva chdir
(man 3 chdir
). Slijedi primjer korištenja ispravno ostvarene naredbe.
cd
.fsh> /usr/bin/pwd
/home/student
fsh> cd Documents
fsh> /usr/bin/pwd
/home/student/Documents
fsh> cd /nepostojec
cd: No such file or directory
Obratite pozornost na to da za ispisivanje grešaka (npr. ako direktorij ne postoji) na stderr
može ostvariti pozivom funkcije perror
nakon sustavskog poziva s pogreškom.
Za jednostavnije testiranje naredbe cd
, možete u odzivniku uključiti i putanju trenutnog direktorija, koju može dati funkcija getcwd
, kao u sljedećem primjeru.
cd
uz odzivnik koji zadrži trenutni direktorij.fsh /home/student> cd Documents
fsh /home/student/Documents> cd ..
fsh /home/student> cd fsh.c
cd: Not a directory
PATH
Ljuske obično omogućuju i pokretanje programa na temelju imena traženjem programa u direktorijima koje definira varijabla okoline PATH
. Varijabla PATH
sadrži niz putanja odvojenih znakom ":
", kao u sljedećem primjeru.
PATH
.echo $PATH
/usr/local/bin:/usr/bin:/bin:/usr/local/sbin
Ljuska može dohvatiti sadržaj varijable PATH
korištenjem bibliotečne funkcije getenv (man 3 getenv)
.
access
./
" ili "./
".perror
) ako putanja ne predstavlja program ili se program ne može pokrenuti.getenv
.PATH
pri pokretanju ljuske.access
preda: SIGINT
Ljuska mora korisniku omogućiti prekidanje izvođenja programa slanjem signala SIGINT
kombinacijom tipki Ctrl+C
. Pri tome je bitno da poslani signal ne prekine rad ljuske. Ako korisnik pošalje SIGINT
dok nijedan program nije pokrenut, ljuska treba samo ispisati novi red. Za ostvarenje ove funkcionalnosti predlažemo porodicu funkcija sigaction
(man 3 sigaction
). Postavljanja funkcije za obradu signala može se ostvariti pomoću ove funkcije:
int set_signal_handler(int signal, void (*handler)(int)) {
struct sigaction act;
act.sa_handler = handler;
act.sa_flags = 0;
sigemptyset(&act.sa_mask);
sigaddset(&act.sa_mask, signal);
return sigaction(signal, &act, NULL);
}
Ctrl+C
ljuska ne dobiva niz znakova ^C
. Samo prima signal SIGINT
.getline
čeka na ulaz, getline
kod svakog budućeg poziva izađe s greškom. To se može spriječiti pomoću funkcija ferror
i clearerr
.set_signal_handler
.SIGINT
.action
odmah nakon prve linije?sigemptyset
i sigaddset
?Standard POSIX definira procesnu grupu (engl. process group) kao skupinu procesa sa svojstvima od kojih su neka:
kill(-pgid, sig)
, primaju ga svi procesi unutar procesne grupe s PGID-om pgid
.fork
dodjeljuje se procesna grupa njegovog roditelja.Svakom terminalu pridružena je jedna sjednica (engl. session) – skup procesnih grupa. Samo jedna procesna grupa može biti procesna grupa prednjeg plana (engl. foreground process group):
Ctrl+C
(SIGINT
) i Ctrl+Z
(SIGTSTP
).Ljuske UNIX-a obično podržavaju pokretanje većeg broja procesa djece. Nepovezane procese djecu smještaju u odvojene procesne grupe, za koje kažemo da su poslovi. Odvajanje procesa u procesne grupe omogućuje:
Ako se program pokrene u pozadini, ljuska nastavlja s radom u prednjem planu i omogućuje izvršavanje novih naredbi. Ako se pokrene u prednjem planu, ljuska čeka program i program ima pristup standardnom ulazu i signalima uzrokovanim kombinacijama tipki.
Sljedeći primjer pokazuje rezultat pokretanja 4 procesa u 3 procesne grupe.
Pretpostavimo da ovako pokrećemo procese u nekoj ljusci:
$ find . -name .mp3 | play &
$ unzip data.zip &
$ ping 8.8.8.8
Ako je "&
" na kraju naredbe, ljuska pokreće program u pozadini. Dobivamo sljedeće procesne grupe:
find . -name .mp3
i player
, u pozadini,unzip data.zip
, u pozadini,ping 8.8.8.8
, procesna grupa prednjeg plana.Ljuska treba pokrenuti program u pozadini (i bez čekanja na njega) ako je na kraju naredbe dodan znak "&
".
Poželjno je da ljuska procese djecu pokreće u zasebnim procesnim grupama kako bi se tijekom izvođenja mogli premještati iz prednjeg plana u pozadinu i obrnuto. Proces dijete se pri stvaranju može premjestiti u zasebnu procesnu grupu pozivom setpgid(pid, 0)
(man 3 setpgid
) prije nego što dijete pozove execve
. Nakon poziva funkcije execve
mijenjanje procesne grupe nije moguće. Proces nakon toga više nije u procesnoj grupi prednjeg plana, tj. u pozadini je. Uz uobičajene postavke terminala, ako proces koji je u pozadini pokuša čitati sa standardnog ulaza, bit će zaustavljen.
Ako želimo program pokrenuti u prednjem planu, nakon premještanja djeteta u zasebnu procesnu grupu trebamo procesnu grupu djeteta prebaciti u prednji plan pozivom tcsetpgrp(STDIN_FILENO, pgid)
, gdje je STDIN_FILENO
opisnik datoteke ulaza terminala, a pgid
PGID procesa. To se treba ostvariti prije poziva execve
kako dijete ne bi pokušalo čitati prije nego što dobije prednji plan.
Ako se tcsetpgrp
poziva iz procesa koji nije u prednjem planu, operacijski sustav zaustavi taj proces signalom SIGTTOU
. Takvo zaustavljanje se može spriječiti ignoriranjem signala SIGTTOU
ranije definiranom funkcijom:
set_signal_handler(SIGTTOU, SIG_IGN);
Pokretanje u prednjem planu može se ostvariti na više načina. Sljedeći pseudokod pokazuje jedan način, gdje i dijete i roditelj pozovu setpgid
i tcsetpgrp
, pri čemu treba paziti na signal SIGTTOU
. Još jedan način je da dijete čeka roditelja.
pid_t run_program(path, argv) {
pid_t pid = fork();
if (pid == 0) {
setpgid(0, 0); // PGID == PID
tcsetpgrp(STDIN_FILENO, getpgid(0));
/* alternative: wait for the parent */
reset_signals();
execve(path, argv, NULL);
perror(path);
exit(-1);
} else if (pid == -1) {
...
}
setpgid(pid, 0); // PGID == PID
tcsetpgrp(STDIN_FILENO, pid);
return pid;
}
Kako proces dijete ne bi naslijedio postavke blokiranja (maskiranja) ili ignoriranja signala od roditelja, prije pokretanja programa treba izvršiti kod kao u sljedećoj funkciji:
void reset_signals() {
sigset_t mask_set;
sigemptyset(&mask_set);
sigprocmask(SIG_SETMASK, &mask_set, NULL); // for blocked (masked) signals
set_signal_handler(SIGTTOU, SIG_DFL); // for the ignored signal SIGTTOU
}
Vraćanje ljuske u prednji plan. Kako bi ljuska nakon završetka ili zaustavljanja djeteta mogla ispravno raditi, treba svoju procesnu grupu vratiti u prednji plan.
Mijenjanje postavki terminala. Ovisno o programu koji proces dijete izvršava, ljuska nakon povratka u prednji plan može raditi neispravno jer dijete (npr. python
) izmijeni postavke terminala. To se može spriječiti tako da se prvo spreme postavke terminala funkcijom tcgetattr
(npr. pri inicijalizaciji ljuske):
struct termios shell_term_settings; // #include<termios.h>
tcgetattr(STDIN_FILENO, &shell_term_settings);
Pri povratku u prednji plan ljuska može vratiti spremljene postavke terminala funkcijom tcsetattr
:
tcsetattr(STDIN_FILENO, 0, &shell_term_settings);
Pri testiranju ljuske mogli bi zaostati neki procesi nakon izlaska iz ljuske. Oni se mogu pratiti npr. sljedećom naredbom:
watch -n 0.5 "ps -u $USER --format=user,pid,pgid,tty,sid,state,command"
Za slanje signala većem broju procesa po imenu može se koristiti program pkill
.
Debugger i ponašanje terminala. Program za otklanjanje pogrešaka (debugger) može uzrokovati izmijenjeno ponašanje terminala zbog kojeg ljuska može ponašati neispravno.
Nepotpuno ostvarivanje funkcionalnosti upravljanja prednjim planom. Moguće je jednostavnije, ali nepotpuno ostvarivanje funkcionalnosti upravljanja prednjim planom bez mijenjanja procesne grupe prednjeg plana (gdje je ljuska uvijek u procesnoj grupi prednjeg plana):
SIGINT
i SIGTSTP
. Nedostatak je što nijedan posao nema pristup standardnom ulazu.setpgid
, getpgid
, funkcije tcgetpgrp
i tcsetpgrp
i signal SIGTTOU
.kill
kad primi negativan prvi argument?setpgid(pid, 0)
?setpgid(pid, 0)
?Većina ljuski pruža mogućnost upravljanja stanjima procesa. Proces u tom slučaju može biti u tri stanja:
Za ovu funkcionalnost bitni su signali SIGTSTP
, SIGCONT
i SIGCHLD
. Slanjem signala SIGTSTP
(zbog pritiska kombinacije tipkiCtrl+Z
) jezgra privremeno zaustavlja odredišni proces, dok ga slanjem signala SIGCONT
pokreće. Prilikom završetka, zaustavljanja ili nastavka izvođenja procesa djeteta, operacijski sustav šalje signal SIGCHLD
procesu roditelju.
U okviru ovog podzadatka potrebno je ostvariti sljedeće ugrađene naredbe:
bg <job_id>
– šalje zaustavljen proces povezan s identifikatorom posla job_id
u pozadinsko izvršavanje slanjem signala SIGCONT
.fg <job_id>
– šalje proces povezan s identifikatorom job_id
u prednji plan.jobs
– ispisuje sve trenutne poslove u formatu [<id>] (<pid>) <stanje>, <naredba>
.Sljedeći primjer prikazuje ispis korištenja ljuske s ispravno ostvarenim upravljanjem poslovima.
fsh> sleep 60
^Z # SIGTSTP
fsh: Job 1 has stopped
fsh> sleep 120 & # start in background
fsh> jobs
[1] (1234) Stopped, 'sleep 60'
[2] (1235) Running, 'sleep 120 &'
fsh> bg 1 # continue in background
sleep 60
fsh> fg 1 # move to foreground
Za ostvarenje funkcionalnosti potrebno je ostvariti strukture podataka za praćenje poslova. Radi jednostavnosti predlažemo praćenje pozadinskih poslova pomoću dvostruko povezane liste u čijim se elementima nalaze informacije o poslu (npr. identifikator (ID) posla, PID procesa itd.). Posao koji se izvršava u prednjem planu možete pratiti pomoću zasebne varijable.
Prilikom gašenja ili zaustavljanja procesa djeteta, jezgra procesu roditelju šalje signal SIGCHLD
. Ljuska po primitku tog signala treba pomoću poziva jezgre waitpid
"pokupiti" proces dijete i provjeriti njegovo stanje (status) pomoću funkcija WIFEXITED
, WIFSIGNALED
i WIFSTOPPED
(primjeri korištenja mogu se naći na dnu stranice ovdje i ovdje). Uz to je potrebno ažurirati strukture za praćenje poslova. Sljedeći pseudokod pokazuje nepotpunu funkciju za obradu signala SIGCHLD
.
SIGCHLD
.handle_sigchld() {
while (1) {
int status;
pid_t pid = waitpid(-1, &status, WNOHANG | WSTOPPED);
/* Break the loop if there are no more children to process*/
if(pid == -1)
break;
if (WIFEXITED(status) || WIFSIGNALED(status)) {
...
if (is_in_foreground(pid))
move_shell_to_foreground();
remove_job(pid);
} else if (WIFSTOPPED(status)) {
...
if (is_in_foreground(pid))
move_shell_to_foreground();
update_job_list(...);
}
}
}
Čekanje na završetak posla. Radi jednostavnosti predlažemo da čekanje na završetak procesa u prednjem planu ostvarite petljom s radnim čekanjem i funkcijom sleep
.
Završetak posla prije nego što je dodan u listu. Prilikom stvaranja procesa treba paziti na to da se može dogoditi da dijete završi prije nego što ga ljuska uspije dodati u listu poslova. Zbog toga funkcija za obradu signala SIGCHLD
može pokušati obrisati posao koji još nije dodan. Kako biste to izbjegli, potrebno je blokirati (maskirati) obradu signala SIGCHLD
prije stvaranja procesa i omogućiti ju nakon ažuriranja liste poslova. Sljedeći pseudokod skicira takvo rješenje.
handle_sigchld(...) {
/* Tracks job states and removes jobs */
}
run_program_and_add_job(...) {
block_signal(SIGCHLD);
pid_t pid = run_program(...);
add_job(pid, ...); // Adds new jobs to the list
release_signal(SIGCHLD);
wait_for_foreground_job(...);
}
Funkcije block_signal
i release_signal
mogu se ostvariti pomoću odgovarajućih poziva oblika sigprocmask(SIG_BLOCK, ...)
i sigprocmask(SIG_UNBLOCK, ...)
. Treba paziti i na to da proces dijete nasljeđuje postavke blokiranja i ignoriranja signala od roditelja, što nije poželjno u slučaju ljuske.
waitpid
i funkcije WIF*
.SIGTSTP
, SIGCONT
i SIGCHLD
i SIGSTOP
.SIGCHLD
.SIGCHLD
.bg
i jobs
.fg
.watch -n 0.5 "ps -u"
.)SIGCHLD
blokiran u procesu djetetu?reset_signals()
na to je li SIGCHLD
blokiran poslije njenog izvršavanja?WNOHANG
| WSTOPPED
?bash &
? Što taj signal govori o stanju posla?Postoje još neke bitne funkcionalnosti ljuske UNIX-a koje ovdje ne obrađujemo:
Delete
,set_signal_handler
, izmijenjeni i dodani pseudokodovi, dodane upute u vezi upravljanja procesnim grupama i prednjim planom, dodani primjeri i pitanja. Korišteni su alati Obsidian s proširenjem Copy document as HTML i Visual Studio Code.