Operacijski sustavi - Upute za ostvarivanje ljuske

1. Pregled

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. glibcmusl) osim funkcije system.

Ove upute obrađuju sljedeće funkcionalnosti ljuske:

  1. Osnovno pokretanje programa.
  2. Osnovne ugrađene naredbe.
  3. Pokretanje programa pomoću varijable okoline PATH.
  4. Obrada signala SIGINT.
  5. Pokretanje programa u prednjem planu ili pozadini.
  6. Praćenje stanja poslova i upravljanje poslovima.

2. Općenite napomene

3. Pristupanje dokumentaciji sustavskih poziva i bibliotečnih funkcija

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.

Primjer 1. Pristupanje dokumentaciji bibliotečne funkcije 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);
...

4. Osnovno pokretanje programa

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.

Pseudokod 1. Prijedlog pseudokoda osnovnog rada ljuske.
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;
  }
}
Primjer 2. Primjer ispravnog rada ljuske.
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;

4.1. Predloženi tijek rješavanja

  1. Napisati osnovni kostur ljuske.
  2. Ostvariti funkciju koja ulazni niz znakova razbija na ime naredbe i argumente.
  3. Detaljno proučiti sustavske pozive forkwait, i execve te pomoću njih ostvariti pokretanje programa.
Pitanja.
  1. Hoće li se pozvati perror ako se execve uspješno izvrši u sljedećem isječku koda?
    execve(path, argv, environ);
    perror(path);
    
  2. Kako se promijeni ispis naredbe /usr/bin/printenv ako se kao zadnji argument funkciji execve preda NULL umjesto varijable environ?

5. Osnovne ugrađene naredbe

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.

Primjer 3. Primjer ispisa pri korištenju 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.

Primjer 4. Primjer ispisa pri korištenju naredbe 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

6. Pokretanje programa pomoću varijable okoline 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.

Primjer 5. Primjer sadržaja varijable 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).

6.1. Napomene

6.2. Predloženi tijek rješavanja

  1. Proučiti funkciju getenv.
  2. Ostvariti dohvaćanje i obradu sadržaja varijable okoline PATH pri pokretanju ljuske.
  3. Dodati traženje programa naredbe prije pokretanja.
Pitanja.
  1. Što se dogodi ako se funkciji access preda:
    1. putanja direktorija,
    2. putanja datoteke koja nije program,
    3. putanja koja ne postoji u datotečnom sustavu?

7. Obrada signala 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);
}

7.1. Napomene

7.2. Predloženi tijek rješavanja

  1. Proučiti predloženu funkciju set_signal_handler.
  2. Ostvariti funkciju za obradu signala SIGINT.
Pitanja.
  1. Kakve su vrijednosti članskih varijabli strukture action odmah nakon prve linije?
  2. Što rade sigemptyset i sigaddset?

8. Upravljanje procesnim grupama i prednjim planom

Standard POSIX definira procesnu grupu (engl. process group) kao skupinu procesa sa svojstvima od kojih su neka:

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):

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.

Primjer 6. Pokretanje većeg broja poslova u pozadini i prednjem planu u ljusci UNIX-a.

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:

  1. proces ljuske, u pozadini,
  2. procesi find . -name .mp3 i player, u pozadini,
  3. proces unzip data.zip, u pozadini,
  4. proces ping 8.8.8.8, procesna grupa prednjeg plana.

8.1. Smještanje posla u zasebnu procesnu grupu i prednji plan

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.

Pseudokod 2. Prijedlog pseudokoda pokretanja procesa uz prebacivanje u zasebnu procesnu grupu i prednji plan.
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
}

8.1.1. Napomene

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):

  1. Kod jedne mogućnosti posao koji treba biti u prednjem planu pokreće se u procesnoj grupi ljuske. Posao do kraja izvođenja ostaje u prednjem planu ili pozadini jer se procesna grupa ne može mijenjati nakon pokretanja programa.
  2. Kod druge mogućnosti svi poslovi se pokreću u pozadini. Ljuska ostvaruje samo dio funkcionalnosti izvršavanja posla u prednjem planu: odabranom poslu prosljeđuje signale SIGINT i SIGTSTP. Nedostatak je što nijedan posao nema pristup standardnom ulazu.

8.2. Predloženi tijek rješavanja

  1. Proučiti sustavske pozive setpgid, getpgid, funkcije tcgetpgrp i tcsetpgrp i signal SIGTTOU.
  2. Izmijeniti pokretanje programa tako da se proces dijete smjesti u zasebnu procesnu grupu i ostvariti mijenjanje procesne grupe prednjeg plana.
  3. Ostvariti mogućnost pokretanja programa u pozadini.
Pitanja.
  1. Što radi sustavski poziv kill kad primi negativan prvi argument?
  2. Što predstavljaju argumenti u pozivu setpgid(pid, 0)?
  3. Čemu je jednak PGID ciljnog procesa nakon poziva setpgid(pid, 0)?

9. Praćenje stanja poslova i upravljanje poslovima

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 SIGTSTPSIGCONTSIGCHLD. 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:

Sljedeći primjer prikazuje ispis korištenja ljuske s ispravno ostvarenim upravljanjem poslovima.

Primjer 8. Primjer ispisa kod ispravno ostvarenog upravljanja 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

9.1. Praćenje poslova

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.

9.2. Funkcije za obradu signala

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 WIFEXITEDWIFSIGNALED 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.

Pseudokod 3. Pseudokod obrade signala 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(...);
    }
  }  
}

9.3. Napomene

Č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.

Pseudokod 4. Prijedlog načina čekanja na završetak posla u prednjem planu.
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.

9.4. Predloženi tijek rješavanja

  1. Detaljno proučiti poziv waitpid i funkcije WIF*.
  2. Detaljno proučiti ponašanje signala SIGTSTPSIGCONTSIGCHLDSIGSTOP.
  3. Ostvariti strukture podataka i procedure potrebne za praćenje poslova.
  4. Ostvariti funkciju za obradu signala SIGCHLD.
  5. Ostvariti čekanje na posao u prednjem planu pomoću funkcije za obradu signala SIGCHLD.
  6. Ostvariti naredbu bg i jobs.
  7. Ostvariti naredbu fg.
Pitanja 3.
  1. Što se dogodi s procesima djecom (poslovima) ljuske koji rade u pozadini ako se izađe iz ljuske? (Procese možete pratiti naredbom watch -n 0.5 "ps -u".)
  2. Koja je razlika između blokiranja i ignoriranja signala?
  3. Je li signal SIGCHLD blokiran u procesu djetetu?
  4. Utječe li funkcija reset_signals() na to je li SIGCHLD blokiran poslije njenog izvršavanja?
  5. Čemu služe zastavice WNOHANG | WSTOPPED?
  6. Koji signal (i zašto) ljuska primi odmah nakon pokretanja naredbe bash &? Što taj signal govori o stanju posla?

10. Druge funkcionalnosti

Postoje još neke bitne funkcionalnosti ljuske UNIX-a koje ovdje ne obrađujemo:

11. Dodatne poveznice


Izmjene
  • 2023-03: Bojan Novković izradio dokument.
  • 2024-03: Ivan Grubišić napravio izmjene. Glavne izmjene: drugačija struktura i tekst, dodan kod za 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.

Validate