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:
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:
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:
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:
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.
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".
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.
int vtični_deskriptor
struct sockaddr_in *naslov_strežnika
int velikost_naslova_strežnika.
Vloge argumentov so podobne kot pri prejšnjih funkcijah.
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.
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).
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