Objavljeno: 10.9.2005 12:34 | Avtor: Dalibor Kranjčič | Monitor Februar 2004

Programiranje v operacijskem sistemu Linux

Pisanje zahtevnejših namenskih programov praviloma zahteva razumevanje principov delovanja operacijskega sistema. Linux obsega orodja in notranje mehanizme, ki omogočajo dostop do pomembnih sistemskih virov in funkcij. V nadaljevanju bomo spoznali nekaj ključnih besed operacijskega sistema Linux in podali zglede v programskem jeziku C.

Jedro (kernel)

Jedro je središče operacijskega sistema in omogoča servis vseh njegovih delov. Tipično jedro Linuxa zaobsega: servis prekinitvenih zahtev vhodno/izhodnih operacij (interrupt handling), razvrščevalnik časa in vrstnega reda izvajanja procesov (scheduler), upravnik pomilniškega prostora (memory manager).

Sistemski klic (system call)

Je vmesnik, ki omogoča uporabniku aplikaciji ter drugim delom operacijskega sistema dostop do jedra.

Uporabniški prostor (user space)

Je prostor, v katerem poteka delovanje uporabniških aplikacij in programov. Gre za dejansko ločevanje uporabniškega prostora od prostora jedra, kar pomeni tudi različne načina preslikovanja in naslavljanja pomnilnika.

Procesi

Definicija pravi, da je računalniški proces aktivno delujoči izvod programa, torej vsak program, ki ste ga morebiti pravkar pognali. Terminal je primer procesa, prav tako tudi lupina shell, aktivirana v terminalskem oknu. Vsak ukaz operacijskega sistema Linux, ki ga poženete v lupini shell, je nov proces.

Značilno za procese je, da imajo svoj pomnilniški prostor in unikatno procesno številko PID (Process ID). Procesi so znani po svoji družabnosti, medsebojni interakciji in socialnih vlogah znotraj operacijskega sistema. Tako ima vsak proces svojega starša oziroma ima proces, ki ga je ustvaril (parent process), hkrati pa je ta lahko tudi sam starš. Izjema je proces init - glavni izhodiščni proces - iz katerega nastanejo vsi drugi procesi.

Številko ID procesa starša imenujemo PPID (Parent Process ID).

Vsakemu procesu je priključena ustrezna številka uporabnika (user ID - UID) in grupe (group ID - GID). Grupa obsega enega ali več uporabnikov. Posamezni uporabnik je lahko član večjega števila grup, a grupe ne morejo vključevati drugih grup, temveč le uporabnike.

Dejansko ima vsak proces dve številki ID uporabnika: efektivno številko ID uporabnika (effective user ID) in realno številko ID uporabnika (real user ID). Večino časa operira jedro samo z efektivno številko ID uporabnika. Tako npr. pri odpiranju datoteke preverja jedro samo efektivno številko ID. Uporaba realne številke ID uporabnika pride do izraza pri prijavi uporabnikov na root aplikacije, ki zahtevajo ime uporabnika in geslo. Po opravljenem postopku preverjanja prijavnih podatkov spremeni program efektivno in realno številko ID v številko uporabnika, ki se prijavi v sistem.

Večina funkcij, namenjenih delu s procesi, je deklarirana v datoteki <unistd.h>. Funkcija oziroma sistemski klic, ki omogoča poizvedbo številke ID delujočega procesa v programskem jeziku C, je getpid(). Če želimo izvedeti številko ID procesa starša, uporabimo sistemski klic getppid(). Funkciji vrneta podatkovni tip pid_t, definiran v datoteki <sys/types.h>.

Spodnji programček bo izpisal številko ID delujočega procesa in njegovega starša:

// Datoteka print_pid.c

#include <stdio.h>

#include <unistd.h>

int main ()

{

printf ("ID procesa je %d \n", (int) getpid());

printf ("ID starša je %d \n", (int) getppid ());

return (0);

}

Program prevedete in poženete z naslednjo kombinacijo ukazov v lupini:

#gcc print_pid.c -o print_pid.

#./print_pid.

Ukaz ps omogoča pregled aktivnih procesov v vašem sistemu. Klic ukaza ps brez argumentov bo podal pregled procesov pod trenutnim nadzorom terminala.

Na primer:

# ps

PID TTY TIME CMD

21693 pts/8 00:00:00 bash

21694 pts/8 00:00:00 ps

Ukaz vrne dva procesa. Prvi je lupina bash, ki jo uporablja vaš terminal, drugi je pravkar izveden ukaz ps.

Oglejmo si še nekaj zgledov rabe ukaza ps:

# ps U user  - Pregled vseh procesov za uporabnika user.

# ps - e   - Pregled vseh procesov v sistemu.

# ps - f   - Popolni pregled (full listing).

# ps - l   - Dolgi pregled (long listing).

# ps - a   - Pregled vseh procesov, razen tistih, ki niso

povezani s terminalom (zajame procese vseh

drugih uporabnikov).

# ps - u   - Poda dodatne informacije o uporabniku.

(Prikaže procese, katerih efektivna številka ID

uporabnika je podana na listi uidlist.)        

# ps - x   - Razširjena lista procesov.

Zelo koristen je ukaz top. Ta nam poda pregled najbolj delujočih nalog (tasks) oziroma procesov v sistemu. Možen je ogled nalog in procesov ter razvrščanje po izkoriščenosti procesorja, pomnilnika in času delovanja. Podana je tudi globalna stopnja izkoriščenosti virov.

Procese uničujemo z ukazom kill. Ukaz deluje tako, da pošlje procesu signal SIGTERM. O signalih več v nadaljevanju.

Zgledi rabe ukaza kill v lupini:

# kill     -s ime signala     številka PID procesa

# kill     -številka signala    številka PID procesa

Seznam signalov:

# kill -l

Funkcija system omogoča preprosto izvajanje ukazov lupine v programski kodi. Funkcija ustvari podproces standardne lupine shell (/bin/sh) in mu hkrati poda ukaz, namenjen izvajanju. Spodnji program izvaja ukaz lupine ls na isti način, kot če bi ga sami vtipkali v ukazni vrstici lupine.

// Datoteka system.c

#include <stdlib.h>

int main()

{

int nVrnjena_vred = system ("ls -l /");

return nVrnjena_vred;

}

Program prevedete in poženete z naslednjo kombinacijo ukazov v lupini:

# gcc system.c -o system

# ./system

Funkcija system vrača izhodni status ukaza lupine. Če lupine ni mogoče aktivirati, funkcija vrne vrednost 127; če pride do druge napake, funkcija vrne vrednost -1.

Funkcija fork omogoča rojstvo novega procesa. Pri klicu te funkcije program ustvari dvojnik procesa, imenovan otrok (child). Glavni program nadaljuje izvajanje programa s točke zadnjega klica funkcije fork. Novo nastali proces otroka prav tako izvaja isti program z iste točke. Proces starša in proces otroka se razlikujeta po številki ID procesa.

Eden od načinov razločevanja dveh procesov (starša in otroka) je uporaba ukaza getpid(), vendar je še druga možnost. Ob svojem klicu funkcija fork() vrača različne vrednosti glede na to, ali je bila uporabljena v procesu starša ali otroka. Vrnjena vrednost v procesu starša je številka PID otroka. Vrnjena vrednost v procesu otroka je 0.

Naslednji program ustvari proces otroka s funkcijo fork() in izpiše svojo identiteto (starš ali otrok) glede na vrnjeno vrednost funkcije:

// Datoteka fork.c

#include <stdio.h>

#include <sys/types.h>

#include <unistd.h>

int main ()

{

pid_t otrok_pid;

printf ("številka ID glavnega programa je %d \n", (int) getpid ());

// Funkcija ustvari otroka in vrne njegovo številko PID

otrok_pid = fork ();

// Preverjamo, ali smo v procesu otroka ali starša

if (otrok_pid != 0) {

printf ("To je proces starša s številko ID = %d \n", (int) getpid());

printf ("ID otroka je %d \n\n", (int) otrok_pid);

} else

printf ("To je proces otroka s številko ID %d \n", (int) getpid());

return 0;

}

Program prevedemo in poženemo z naslednjimi ukazi:

# gcc fork.c -o fork

# ./fork

Družina funkcij exec omogoča zaganjanje programov (procesov) iz trenutno aktivnega programa. Izvajanje programa, iz katerega je opravljen klic ene od funkcij družine exec, se nadomesti z novim programom, podanim kot argument k tej funkciji. Funkcije družine exec se razlikujejo predvsem po načinu podajanja argumentov.

V naslednjem zgledu bomo spoznali funkcijo execvp, ki bo podobno kot pri prejšnjem zgledu rabe funkcije system klicala ukaz lupine ls, a tokrat brez posredovanja lupine shell.

// Datoteka fork_exec.c

#include <stdio.h>

#include <stdlib.h>

#include <sys/types.h>

#include <unistd.h>

/* Funkcija ustvari otroka, iz katerega izvaja klic

ukaza ls s pripadajočimi argumenti */

int ustvari (char* program, char** lista_argumentov)

{

pid_t otrok_pid;

// Podvoji proces

otrok_pid = fork();

if (otrok_pid != 0)

// To je proces starša

return otrok_pid;

else {

// Poišči PROGRAM na podlagi podane poti in ga poženi

execvp (program, lista_argumentov);  

// Izpiši napako, če funkcija execvp ni bila uspešno izvedena

fprintf (stderr, "Prišlo je do napake pri klicu ukaza execvp. \n");

abort (); // Povzroči konec programa

}

}

int main ()

{

/* Seznam argumentov ukaza "ls" */

char* lista_argumentov[] = {

"ls", /* argv[0], ime programa. */

"-l",

"/",

NULL /* Seznam argumentov končujemo z NULL. */

};

// Uporabimo prej implementirano metodo in poženemo ukaz "ls".

ustvari ("ls", lista_argumentov);

printf ("Konec glavnega programa. \n");

return (0);

}

Program prevedemo in poženemo z naslednjimi ukazi:

# gcc fork_exec.c -o fork_exec

# ./fork_exec

Signali

so mehanizmi za komunikacijo in upravljanje procesov v Linuxu. Signal je posebno sporočilo, poslano procesu. Signali so asinhroni; proces obdela signal, brž ko ga sprejme, ne glede na nalogo, ki jo trenutno izvaja. Poznamo več različnih signalov z različnimi pomeni. Signali so definirani v datoteki /usr/include/bits/signum.h, ki jo praviloma ne vključujemo v svoje programe. V ta namen uporabimo datoteko <signal.h>.

Ob sprejetju signala se proces lahko odzove na več načinov. Za vsak signal je na voljo privzeto nagnjenje (default disposition), ki določa odziv programa, če ni podano drugo ustrezno nagnjenje. Za večino signalov ima program možnost reakcije - bodisi sprejeti signal ignorira ali pa kliče namensko funkcijo za servis signala, poimenovano upravljalnik signala (signal handler).

Linux pošilja signale k procesom kot posledico posebnih stanj sistema. Na primer: SIGBUS (bus error - napaka na vodilu) ali SIGFPE (floating point exception - napaka plavajoče vejice).

Proces lahko pošilja signale tudi drugim procesom. Značilna raba takega mehanizma je zaključevanje drugih procesov s pošiljanjem signalov SIGTERM in SIGKILL. Oba sicer zaključujeta procese, a je razlika v načinu. Pri signalu SIGTERM ima proces možnost ignoriranja zahteve. SIGKILL konča proces takoj.

Signala SIGUSR1 in SIGUSR2 sta rezervirana v uporabniške namene.

Za določanje nagnjenja signala uporabljamo funkcijo sigaction. Prvi parameter funkcije sigaction predstavlja številko signala. Naslednja dva parametra sta kazalca na strukturo sigaction; prvi od teh dveh parametrov vsebuje privzeto nagnjenje za podano številko signala, drugi pa sprejema prejšnje nagnjenje. Najpomembnejši član strukture sigaction je sa_handler, ki lahko zavzame tri vrednosti:

  • SIG_DFL - privzeto nagnjenje za podani signal;
  • SIG_IGN - ignoriranje signala;
  • Kazalec na funkcijo upravljalnik signala (signal handler). Funkcija vzame kot vhodni parameter številko signala, vrača pa tip void.
  • Podali bomo zgled rabe signala SIGUSR1, ki ga bo program pošiljal sebi samemu. Število poslanih signalov je določeno s konstanto ST_POSL_SIG. Ob sprejetju signala se aktivira funkcija ServisSignala. Ta ob vsakem prispelem signalu poveča števec sprejetih signalov za 1. Na koncu programa izpišemo vrednost števca prispelih signalov.

    Za pošiljanje signalov uporabimo funkcijo kill. Prvi parameter funkcije je številka procesa PID, drugi parameter pa ime signala, ki ga pošiljamo.

    Najbrž ste opazili, da je globalna spremenljivka števec signalov definirana z uporabo posebnega podatkovnega tipa sig_atomic_t. Ta poskrbi, da se spremenljivki dodeli vrednost v enem ciklu strojnega ukaza in tako preprečuje napake, ki bi nastale ob morebitnem sprejemanju signala sredi ciklov dodeljevanja.

    // Datoteka sigusr.c

    #include <signal.h>

    #include <stdio.h>

    #include <string.h>

    #include <sys/types.h>

    #include <unistd.h>

    // Število poslanih signalov

    #define ST_POSL_SIG 10000

    // Atomska spremenljivka - izvede se v enem ciklu strojnega ukaza

    sig_atomic_t sigusr1_stevec = 0;

    // Rutina za servis signala

    void ServisSignala (int nStevSig)

    {

    // Povečaj števec za 1

    ++sigusr1_stevec;

    }

    int main ()

    {

    // Inicializiraj števec while zanke

    int nStevPoslanihSig = ST_POSL_SIG;

    struct sigaction sa;

    memset (&sa, 0, sizeof (sa));

    sa.sa_handler = &ServisSignala;

    sigaction (SIGUSR1, &sa, NULL);

    // Pošiljaj signal SIGUSR1 samemu sebi

    while (nStevPoslanihSig--)

    kill (getpid(), SIGUSR1);

    // Izpiši vrednost števca prispelih signalov v servisno rutino

    printf ("SIGUSR1 je poslan %d krat. \n", sigusr1_stevec);

    return (0);

    }

    Program prevedemo in poženemo z naslednjimi ukazi:

    # gcc sigusr.c -o sigusr

    # ./sigusr

    Medprocesna komunikacija (IPC - Internal Process Communication)

    Linux pozna več načinov komunikacije med procesi:

  • imenovane cevi - FIFO mehanizmi;
  • neimenovane cevi (pipes);
  • sporočilne vrste (message queues);
  • skupni pomnilnik/preslikani pomnilnik (Shared memory/Mapped memory);
  • vtičnice (Sockets).
  • Osredotočili se bomo predvsem na mehanizme FIFO, preslikani pomnilnik (poenostavljeno različico skupnega pomnilnika) in vtičnice.

    Imenovane cevi - FIFO (first in first out - prvi noter prvi ven)

    Imenovane cevi oziroma mehanizme FIFO uporabljamo za komunikacijo dveh procesov, ki uporabljata isti datotečni sistem (file system). Vsak proces odpira in zapira svoj konec cevi za branje, pisanje ali pa oboje.

    Uporabnost mehanizma FIFO primerno ponazorimo z zgledom v lupini. FIFO ustvarimo z uporabo ukaza mkfifo. Če želimo ustvariti, denimo, FIFO v mapi /tmp/fifo, napišemo naslednje ukaze:

    # mkfifo /tmp/fifo

    # ls -l /tmp/fifo

    prw-rw-rw- 1 dalibor dalibor 0 Dec 1 14:04 /tmp/fifo

    Kot vidimo, je prva črka izhodnega rezultata ukaza ls črka p (pipe). Ta nam pove, da je to datoteka FIFO.

    Odpremo novo okno in preberemo podatke iz mehanizma FIFO na naslednji način:

    # cat < /tmp/fifo

    V nadaljevanju odpremo drugo okno in pišemo v FIFO:

    # cat > /tmp/fifo

    Nato vtipkamo poljubno besedilo. Spremembe, narejene v drugem oknu, se bodo prikazale v prvem oknu.

    Prejšnji zgled ponazarja ustvarjanje mehanizma FIFO v lupini z ukazom mkfifo. Programski jezik C pozna istoimensko funkcijo. Funkcija potrebuje dva argumenta. Prvi argument predstavlja pot do mape, v kateri ustvarimo FIFO. Drugi argument je številski opis pravic za branje, pisanje in izvajanje datoteke FIFO v datotečnem sistemu. Če datoteke FIFO ni mogoče ustvariti, funkcija vrne vrednost -1.

    Do cevi FIFO imamo dostop na isti način kot do navadne datoteke. Če želimo vpisati v FIFO podatek poljubnega tipa, to naredimo z naslednjo kombinacijo funkcij:

    // Najprej odpremo FIFO

    // fd je datotečni deskriptor (file descriptor).

    // Od zdaj naprej bo deskriptor označeval FIFO ter bo klican v nadaljevanju

    int fd = open (pot_do_fifo, O_WRONLY);

    // Pišemo na FIFO

    write (fd, podatek_poljubnega_tipa, velikost_ podatka);

    // Zapiramo FIFO

    close(fd);

    Sledi konkretni zgled komunikacije dveh procesov (programov) ob pomoči mehanizma FIFO. Program read_fifo najprej ustvari cev FIFO, poimenovano fifo, in pasivno čaka na podatek, poslan iz programa write_fifo. Podatek, ki ga pošiljamo, je sestavljenega tipa, definiran v datoteki ipc.h. Datoteka Makefile poskrbi za ustrezno prevajanje programov in povezovanje.

    // Datoteka read_fifo.c

    /* Prebere sestavljeni podatkovni tip iz

    FIFO-ta poslan iz procesa write_fifo */

    #include <fcntl.h>

    #include <unistd.h>

    #include <stdlib.h>

    #include <stdio.h>

    #include <sys/types.h>

    #include <sys/stat.h>

    /* Definicija sestavljenega podatkovnega

    tipa, ki ga posiljamo skozi FIFO */

    #include "ipc.h"

    int main() {

    int fd; // datotečni deskriptor (fd -file descriptor)

    MSG SestavljeniTip;

    // Ustvarimo FIFO ter nastavimo dostopne pravice

    mkfifo ("./fifo", O_RDWR);

    chmod("fifo", S_IFIFO | 0666);

    // Odpremo FIFO za branje

    if ((fd = open("fifo", O_RDONLY)) < 0) {

    fprintf(stderr, "Napaka pri odpiranju.\n");

    exit(1);

    }

    // Branje prispelega podatka

    read(fd, &SestavljeniTip, sizeof(SestavljeniTip));

    printf ("Branje prispelega sestavljenega tipa MSG: \n \n");

    printf ("Podatek tipa int = %d \n", SestavljeniTip.nPodatekTipa_int);

    printf ("Podatek tipa string = %s \n\n", SestavljeniTip.szPodatekTipa_char);

    // Zapiramo FIFO

    close(fd);

    }

    // Datoteka write_fifo.c

    // Pošlje sestavljeni podatkovni tip v FIFO

    #include <fcntl.h>

    #include <unistd.h>

    #include <stdlib.h>

    #include <stdio.h>

    #include <iostream.h>

    /* Definicija sestavljenega podatkovnega tipa,

    ki ga pošiljamo v FIFO */

    #include "ipc.h"

    int main()

    {    

    int fd; // Datotečni deskriptor (fd - file descriptor)

    MSG SestavljeniTip;

    /* Preberemo vhodne podatke, ki ji bomo pošiljali

    na drugi proces */

    printf ("Vpiši poljubno številko: \n");

    cin " SestavljeniTip.nPodatekTipa_int;

    printf ("Vpiši poljubni niz (max. 10): \n");

    cin " SestavljeniTip.szPodatekTipa_char;

    // Odpiramo FIFO za pisanje

    if ((fd = open("fifo", O_WRONLY)) < 0) {

    fprintf(stderr, "Napaka pri odpiranju. \n");

    exit(1);

    }

    // Pišemo na FIFO     

    if ((write(fd, &SestavljeniTip, sizeof(SestavljeniTip))) < 0) {

    fprintf(stderr, "Napaka pri pisanju na FIFO. \n");

    exit(1);

    }   

    return (0);

    }

    // Datoteka ipc.h

    #ifndef IPC_H

    #define IPC_H

    #endif

    /* Definiramo sestavljeni podatkovni tip MSG

    , ki ga bomo pošiljali skozi FIFO */

    typedef struct msg {

    int nPodatekTipa_int; // Podatek tipa int

    char szPodatekTipa_char[10]; // Niz znakov dolžine 10

    } MSG;

    # Datoteka Makefile:

    all:  read_fifo write_fifo

    read_fifo:  read_fifo.c ipc.h

    g++ -o read_fifo read_fifo.c

    write_fifo:  write_fifo.c ipc.h

    g++ -o write_fifo write_fifo.c

    clean:

    rm -f read_fifo write_fifo

    Pred prevajanjem nastavite dostopne pravice za datoteko Makefile:

    # chmod 755 Makefile

    Program prevedite s klicem ukaza make:

    # ./make

    Poženite programa read_fifo in write_fifo:

    # ./read_fifo

    # ./write_fifo

    Preslikani pomnilnik (mapped memory)

    Omogoča medsebojno komunikacijo procesov s souporabniško datoteko. Preslikani pomnilnik ustvari zvezo med datoteko in pomnilniškim procesom. Datoteka se porazdeli na kose, ki se kopirajo v virtualni pomnilnik, ter so na razpolago v naslovnem prostoru procesa. To procesu omogoča branje datotečnih podatkov z dostopom do pomnilnika.

    Za preslikavo datoteke v pomnilniški prostor procesa uporabljamo sistemski klic mmap.

    Prvi argument ukaza je naslov procesa v pomnilniku, kamor preslikamo datoteko. Če za prvi argument izberemo NULL, bo ustrezni naslov za nas izbral Linux. Drugi argument predstavlja dolžino mape v bajtih. Tretji argument opisuje način zaščite preslikanega naslovnega prostora. Možno je določiti zaščito z uporabo pravic za pisanje, branje in izvajanje (PROT_WRITE, PROT_READ, PROT_EXEC).

    Četrti argument je vrednost zastavice, ki določa posebne dodatne možnosti:

  • MAP_FIXED - pove Linuxu, naj uporabi podani naslov za preslikavo. Če naslov ni na voljo ali je morebiti že v uporabi, se preslikava ne bo izvedla.
  • MAP_PRIVATE - pomeni, da je dostop do preslikanega pomnilniškega področja zaseben in spremembe v njem ne bodo dostopne drugim procesom. Ta možnost izključuje možnost MAP_SHARED.
  • MAP_SHARED - spremembe v preslikanem pomnilniškem prostoru so vidne v preslikani datoteki in dostopne vsem procesom, ki preslikujejo isto datoteko.
  • Peti argument predstavlja datotečni deskriptor za preslikano datoteko. Zadnji argument je odmik od začetka datoteke, s katero začenjamo preslikavo.

    Podali bomo zgled uporabe preslikanega pomnilnika v komunikaciji dveh procesov, podobno kot pri prejšnjemu zgledu FIFO. Program mapiran_rd čaka na spremembo vrednosti podatka v preslikanem pomnilniku, na katero kaže kazalec pch_map_kaz. Začetna vrednost podatka tipa char je 0. Naslednji program mapiran_wr na isto preslikano pomnilniško lokacijo vpisuje naključno številko med 1 in 100. Ob spremembi oziroma vpisu nove vrednosti bo program mapiran_rd prebral to novo naključno vrednost in jo izpisal.

    // Datoteka mapiran_rd.c

    /* Program registrira (prebere) spremembo vrednosti

    v preslikanem pomnilniku, ki jo izvede program mapiran_wr */

    // Opomba: program poženite kot root uporabnik

    #include <fcntl.h>

    #include <unistd.h>

    #include <stdlib.h>

    #include <stdio.h>

    #include <time.h>

    #include <sys/times.h>

    #include <fcntl.h>

    #include <sys/mman.h>

    #include <sys/stat.h>

    #include <time.h>

    /* Indeks prvega podatka, na katerega

    kaže kazalec na preslikani pomnilnik */

    #define INDEX 0

    int main()

    {    

    int fd; // Datotečni deskriptor

    char *pch_map_kaz; // kazalec na preslikani pomnilnik

    /* Odpri datoteko preslikanega pomnilnika */

    if ((fd = open("/dev/mem", O_RDWR | O_CREAT)) < 0) {

    printf("Napaka pri odpiranju datoteke /dev/mem.\n");

    exit(1);

    }

    // Vzpostavi kazalec na preslikani pomnilnik

    pch_map_kaz = (char *)mmap(0, 8192, PROT_READ | PROT_WRITE,

    MAP_FILE | MAP_SHARED, fd, 0);

    // Določi začetno vrednost (char) prvega podatka, kamor kaže kazalec

    pch_map_kaz[INDEX] = 0;

    /* Tu čakamo na spremembo vrednosti.

    Ta bo spremenjena iz procesa mapiran_wr */

    while (1) {    

    if (pch_map_kaz[INDEX] != 0) {

    printf("Dobili smo podatek od drugega procesa. \n");

    printf("Vrednost spremenljivke = %d \n", pch_map_kaz[INDEX]);

    exit(1);

    }

    }

    return (0);

    }

    / Datoteka mapiran_wr.c

    /* Program spremeni vrednost v preslikanem pomnilniku,

    ki jo potem zazna program mapiran_rd */

    // Opomba: program poženite kot root uporabnik

    #include <fcntl.h>

    #include <unistd.h>

    #include <stdlib.h>

    #include <stdio.h>

    #include <time.h>

    #include <sys/times.h>

    #include <fcntl.h>

    #include <sys/mman.h>

    #include <sys/stat.h>

    #include <time.h>

    /* Indeks prvega podatka, na katerega

    kaže kazalec na preslikani pomnilnik */

    #define INDEX 0

    int main()

    {        

    int fd; // Datotečni deskriptor

    int nRand; // Naključna spremenljivka

    char *pch_map_kaz; // Kazalec na preslikani pomnilnik

    // Inicializacija semena (srand) za generator naključnega št.

    srand((int)time(NULL));

    nRand = rand() % (100)+1; // Generiraj naključno število med 1 in 100

    /* Odpri datoteko preslikanega pomnilnika */

    if ((fd=open("/dev/mem", O_RDWR | O_CREAT)) < 0) {

    printf("Napaka pri odpiranju datoteke /dev/mem. \n");

    exit(1);

    }

    // Inicializiraj kazalec na preslikani pomnilnik

    pch_map_kaz = (char *)mmap(0, 8192, PROT_READ | PROT_WRITE,

    MAP_FILE | MAP_SHARED, fd, 0);

    /* Spremeni vrednost, ki jo kaže kazalec na preslikani

    pomnilnik, na vrednost, različno od 0. To spremembo bo

    zaznal program mapiran_rd */

    pch_map_kaz[INDEX] = nRand;

    printf ("Vrednost na začetnem naslovu, kamor kaže kazalec, je %d \n", nRand);

    return (0);

    }

    Programe prevedemo kot root uporabnik:

    # su root

    # gcc mapiran_rd.c -o mapiran_rd

    # gcc mapiran_wr.c -o mapiran_wr

    in poženemo:

    # ./mapiran_rd

    # ./mapiran_wr

    Vtičnice (sockets)

    Vtičnica je dvosmerni komunikacijski kanal, ki ga uporabljamo za komunikacijo procesov v istem računalniku ali različnih med seboj oddaljenih računalnikih. Sistemski klici, povezani z vtičnico, so:

  • socket - namen sistemskega klica oziroma funkcije socket je ustvarjanje vtičnice. Funkcija jemlje naslednje argumente:
  • int domena_vtičnice

    int komunikacijski_slog_vtičnice       

    int protokol

    Argument domena vtičnice določa domeno, znotraj katere poteka komunikacija. Poznamo lokalno domeno (local domain) in internetno domeno (Internet domain). Podobno delimo vtičnice na lokalne vtičnice in vtičnice internetne domene. Pri lokalnih vtičnicah komunikacija poteka v istem računalniku, medtem ko se vtičnice internetne domene uporabljajo pri omrežni komunikaciji različnih oddaljenih računalnikov. Naslov vtičnice pri lokalni domeni predstavlja navadno datoteko. Pri internetni domeni je naslov vtičnice sestavljen iz internetnega naslova IP in številke vrat (port number). Številka vrat ločuje različne vtičnice na istem računalniku. Poznamo različne tipe domen za vtičnice, definirane v datoteki /usr/include/sys/socket.h. Izbor domene je povezan z izborom družine protokolov. Poznamo naslednje družine protokolov:

    AF_UNIX - UNIX interni protokoli (lokalna komunikacija);

    AF_INET - ARPA internetni protokoli;

    AF_ISO - protokoli mednarodne standardne organizacije;

    AF_NS - Xerox omrežni protokoli.

    Komunikacijski slog vtičnice opisuje način prenosa informacijskih paketov. Poznamo povezavne in nepovezavne načine prenosa podatkov.

    Povezavni način omogoča zanesljiv prenos vseh paketov po pravilnem vrstnem redu. Če se paket pri prenosu izgubi, sprejemnik zahteva vnovično pošiljanje. Naslova sprejemnika in pošiljatelja podatkov sta fiksno določena v začetni fazi komunikacije, podobno kot pri telefonskem klicu. Protokol TCP je zgled povezavnega načina prenosa podatkov.

    Nepovezavni prenos ne zagotavlja pravilnega vrstnega reda dostave paketov. Paketi (datagrami) se sicer pošiljajo, vendar ni zagotovila o njihovi dostavi in mehanizmov, ki bi ugotavljali morebitne nepravilnosti ter omogočali ponovljivost paketov. Protokol UDP je zgled nepovezavnega načina prenosa podatkov.

    Poznamo naslednje komunikacijske sloge vtičnic:

    SOCK_STREAM - omogoča zanesljivo, sekvenčno, dvosmerno komunikacijo (povezavni prenos);

    SOCK_DGRAM - hiter in nezanesljiv način prenosa podatkov;

    (nepovezavni prenos);

    SOCK_RAW - uporablja se pri internih omrežnih protokolih;

    SOCK_SEQPACKET- uporablja se samo pri protokolu AF_NS;

    SOCK_RDM - ni izvedbe.

    Protokol določa način pošiljanja podatkov. Navadno je samo en protokol za podporo določenega tipa vtičnice, vendar je mogoč tudi obstoj številnih protokolov. V tem primeru posebne protokole navajamo z uporabo tretjega argumenta funkcije socket.

  • close - zapre vtičnico.
  • bind - povezuje proces z vtičnico. Navadno se uporablja v procesu strežnika za konfiguracijo vtičnice za prihajajoče povezave odjemalca (client). Funkcija bind jemlje naslednje argumente:
  • int vtični_deskriptor

    struct sockaddr_in *moj_naslov

    int velikost_strukture_sockaddr_in

    Prvi argument funkcije je vrednost deskriptorja vtičnice, vrnjena iz predhodnega klica funkcije socket. Drugi argument funkcije je naslov strukture sockaddr_in. Zgled strežnika, ki bo opisan v nadaljevanju, inicializira elemente strukture sockaddr_in na naslednji način:

    sin.sin_family = AF_INET;

    sin.sin_addr.s_addr = INADDR_ANY;

    sin.sin_port = htons(nPort);

    Vrednost AF_INET določa, da je uporabljen internetni naslov. Element sin_addr.s_addr shranjuje internetni naslov želenega računalnika kot 32-bitno številko IP tipa int. V našem primeru je vrednost naslova nastavljena na konstanto INADDR_ANY, kar pomeni sprejemanje vseh prihajajočih naslovov IP. Tretji element strukture sin.sin_port predstavlja številko vrat, ki označuje povezavo na točno določeni vtičnici. Postopek uporabe funkcije bind imenujemo tudi "podajanje imena vtičnici".

  • listen - po izvedbi funkcij socket in bind, proces strežnika posluša prihajajoče povezave na vtičnici. Argumenti tega sistemskega klica so:
  • int vtični_deskriptor

    int velikost_vhodne_vrste

    Prvi argument funkcije je vrednost deskriptorja vtičnice, vrnjena iz predhodnega klica funkcije socket. Drugi argument določa velikost vhodne podatkovne vrste.

  • connect - sistemski klic connect uporabljamo za povezovanje na oddaljeni strežnik. Argumenti so naslednji:
  • int vtični_deskriptor

    struct sockaddr_in *naslov_strežnika

    int velikost_naslova_strežnika.

    Vloge argumentov so podobne kot pri prejšnjih funkcijah.

  • accept - sprejema povezavo in hkrati ustvari novo vtičnico za povezavo.
  • Proces sprejemanja novih povezav še vedno poteka na stari vtičnici. Vsaka nova povezava ima svojo vtičnico za prenos podatkov. Argumenti funkcije accept so:

    int vtični_descriptor

    struct sockaddr_in *sprejeti_naslov_računalnika

    int *velikost_sprejetega_naslova_računalnika

    Večina argumentov je že znanih, razen tretjega, ki predstavlja vrnjeno vrednost velikosti sprejetega naslova računalnika.

  • recv - uporabljamo za sprejemanje sporočil na vtičnici. Argumenti funkcije recv so:
  • int vtični_descriptor

    void *podatki

    int dolžina_podatkov

    unsigned int zastavice

    19

    Zastavice (flags) določajo način sprejemanja vhodnih podatkov. Poznamo tri zastavice: MSG_OOB (za sporočila visoke prioritete), MSG_PEEK (za bežni pregled sporočil brez branja) in MSG_WAITALL (počaka, da bo sprejeti medpomnilnik poln pred vračanjem funkcije).

  • send - uporabljamo za pošiljanje sporočil na vtičnico. Argumenti funkcije send so:
  • int vtični_deskriptor

    const void * podatki

    int dolžina_podatkov

    unsigned int zastavice

    Funkcija send pozna dve zastavici: MSG_OOB in MSG_DONTROUTE (ne uporabljaj usmerjanja - routing).

    Podali bomo zgled komunikacije dveh procesov ob pomoči vtičnice. Komunikacija poteka po krajevnem naslovu IP računalnika (127.0.0.1), kar omogoča preizkus povezovanja procesov v istem računalniku. Če boste izvajali dejansko povezavo dveh oddaljenih računalnikov, ustrezno spremenite naslove IP v programski kodi. Program strežnik ustvari vtičnico, posluša in sprejema povezave ter izpisuje sprejeti niz podatkov, ki ga pošlje program odjemalec.

    // Datoteka streznik.c

    #include <stdio.h>

    #include <sys/socket.h>

    #include <netinet/in.h>

    #include <arpa/inet.h>

    #include <netdb.h>

    // Stevilka porta

    int n_port = 8000;

    int main() {

    struct sockaddr_in moj_naslov ;

    struct sockaddr_in sprejeti_naslov;

    int n_vticni_deskriptor;

    int n_novi_vticni_deskriptor;

    int n_velikost_sprejetega_naslova;

    char sz_medpomnilnik[100]; // Buffer

    memset(sz_medpomnilnik, 0, 100);

    // Ustavarjanje vtičnice

    n_vticni_deskriptor = socket(AF_INET, SOCK_STREAM, 0);

    // Preverjanje morebitne napake

    if (n_vticni_deskriptor == -1) {

    perror("Napaka pri klicu funkcije socket. \n");

    exit(1);

    }

    // Inicializacija strukture sockaddr_in

    bzero(&moj_naslov, sizeof(moj_naslov));

    moj_naslov.sin_family = AF_INET; // Internet naslov

    moj_naslov.sin_addr.s_addr = INADDR_ANY;// Sprejema vse IP naslove

    moj_naslov.sin_port = htons(n_port); // Port vticnice

    // Povezuje proces streznika z vtičnico

    if (bind(n_vticni_deskriptor, (struct sockaddr *)&moj_naslov,

    sizeof(moj_naslov)) == -1) {

    perror("Napaka pri klicu funkcije bind.");

    exit(1);

    }

    // Posluša

    if (listen(n_vticni_deskriptor, 20) == -1) {

    perror("Napaka pri klicu funkcije listen.");

    exit(1);

    }

    printf("Sprejemam povezave ...\n");

    // Neskončno dolgo čakam na povezave

    while(1) {

    n_novi_vticni_deskriptor =

    accept(n_vticni_deskriptor, (struct sockaddr *)&sprejeti_naslov,

    &n_velikost_sprejetega_naslova);

    if (n_novi_vticni_deskriptor == -1) {

    perror("Napaka pri klicu funkcije accept");

    exit(1);

    }

    // Branje podatkov v medpomnilnik (buffer)

    if (recv(n_novi_vticni_deskriptor, sz_medpomnilnik, 100, 0) == -1) {

    perror("Napaka pri klicu funkcije recv.");

    exit(1);

    }

    printf("Sprejeto od odjemalca: %s\n", sz_medpomnilnik);

    // Zapiranje vtičnice

    close(n_novi_vticni_deskriptor);

    }

    }

    // Datoteka odjemalec.c

    #include <stdio.h>

    #include <sys/socket.h>

    #include <netinet/in.h>

    #include <arpa/inet.h>

    #include <netdb.h>

    #include <string.h>

    char* psz_naslov_streznika = "127.0.0.1"; // Lokalni naslov

    int n_port = 8000; // Številka porta

    int main() {

    int n_vticni_deskriptor;

    struct sockaddr_in naslov_streznika;

    struct hostent *ime_gostitelja;

    char* psz_niz = "POSLANI TEKSTOVNI NIZ";

    // Pretvorba IP številke v ime

    if ((ime_gostitelja = gethostbyname(psz_naslov_streznika)) == 0) {

    perror("Napaka pri reševanju imena streznika.\n");

    exit(1);

    }

    // Inicializacija strukture sockaddr_in

    bzero(&naslov_streznika, sizeof(naslov_streznika));

    naslov_streznika.sin_family = AF_INET;

    naslov_streznika.sin_addr.s_addr = htonl(INADDR_ANY);

    naslov_streznika.sin_addr.s_addr=((struct in_addr *)(ime_gostitelja->h_addr))->s_addr;

    naslov_streznika.sin_port = htons(n_port);

    // Ustvarjanje vtičnice

    if ((n_vticni_deskriptor = socket(AF_INET, SOCK_STREAM, 0)) == -1) {

    perror("Napaka pri odpiranju vtičnice.\n");

    exit(1);

    }

    // Povezovanje na strežnik

    if (connect(n_vticni_deskriptor, (void *)&naslov_streznika,

    sizeof(naslov_streznika)) == -1) {

    perror("Napaka pri prikljucitvi na vtičnico. \n");

    exit(1);

    }

    printf("Pošiljamo sporocilo %s na strežnik...\n", psz_niz);

    // Pošiljanje na vticnico

    if (send(n_vticni_deskriptor, psz_niz, strlen(psz_niz), 0) == -1) {

    perror("Napaka pri posiljanju na streznik.\n");

    exit(1);

    }

    // Zapiranje vtičnice

    close(n_vticni_deskriptor);

    return (0);

    }

    Programe prevedemo na naslednji način:

    # gcc streznik.c -o streznik

    # gcc odjemalec.c -o odjemalec

    in poženemo:

    # ./streznik

    # ./odjemalec

    Naroči se na redna tedenska ali mesečna obvestila o novih prispevkih na naši spletni strani!

    Komentirajo lahko le prijavljeni uporabniki

     
    • Polja označena z * je potrebno obvezno izpolniti
    • Pošlji