Wykład 4 - Instytut Informatyki Teoretycznej i Stosowanej

Transkrypt

Wykład 4 - Instytut Informatyki Teoretycznej i Stosowanej
Oprogramowanie systemów
równoległych i rozproszonych
Wykład 4
Dr inż. Tomasz Olas
[email protected]
Instytut Informatyki Teoretycznej i Stosowanej
Politechnika Cz˛estochowska
Wykład 4 – p. 1/7
Proces i pamie˛ ć systemowa
W systemie Unix procesem staje sie˛ program wykonywalny,
który zostaje wczytany do pamieci
˛ systemowej przez jadro,
˛
a
nastepnie
˛
uruchomiony.
Pamieć
˛ systemowa może zostać podzielona na:
obszar użytkowy - w którym uruchomione sa˛ procesy
użytkowników,
obszar jadra
˛
- pozostaje do dyspozycji jadra
˛
i jego
procesów.
Procesy użytkowników moga˛ uzyskiwać dostep
˛ do jadra
˛
wyłacznie
˛
za pośrednictwem wywołań systemowych.
Wykład 4 – p. 2/7
Reprezentacja w systemie operacyjnym
W systemie operacyjnym proces reprezentowany jest poprzez
strukture˛ Proces Control Block (Blok kontrolny procesu).
kod programu w pamieci
˛ (obszar programu i danych),
otwarte pliki (identyfikowane przez deskryptory),
środowisko (zestaw zmiennych i przypisanych im wartości),
funkcja main() wywołana przez procedure˛ startowa˛ z
podanymi argumentami (argv[]) i zmiennymi
środowiskowymi (getenv(), putenv(), extern char **environ)
zakończenie procesu unixowego może być:
normalne, funkcje: return, exit(atexit), _exit
anormalne, funkcja abort lub otrzymanie odpowiedniego
sygnału
Wykład 4 – p. 3/7
Przestrzeń adresowa procesu
Pamieć
˛ procesu jest podzielona na kilka bloków:
code segment (segment kodu) Obszar w którym znajduja˛ sie
˛
wykonywalne instrukcje programu
data segment (segment danych)
initialized data (dane zainicjowane) Dane które sa˛
przechowywane w pliku wykonywalnym w sekcji „Data”
BSS (block started by symbol) Obszar danych domyślnie
inicjowanych wartościa˛ 0 (nie sa˛ przechowywane w pliku
wykonywalnym)
heap (sterta) Pamieć
˛ zarezerwowana np. przez malloc,
calloc, new
stack segment (stos) Lokalne zmienne, parametry funkcji.
Wykład 4 – p. 4/7
Parametry procesu
W bloku kontrolnym procesu zapisane sa˛ miedzy
˛
innymi
nastepuj
˛ ace
˛ parametry charakteryzujace
˛ proces:
PID
Process ID
Identyfikator Procesu
UID
User ID
Identyfikator Użytkownika Wywołujacego
˛
Proces
GID
Group ID
Identyfikator Grupy do jakiej należy Proces
SID
Session ID
Identyfikator Sesji (zwiazany
˛
z terminalem sterujacym)
˛
Wykład 4 – p. 5/7
Środowisko przetwarzania
getpid - umożliwia uzyskanie identyfikatora procesu,
std::cout << "moj PID: " << getpid() << std::endl;
identyfikator procesu macierzystego - getppid():
std::cout << "identyfikator przodka: " << getppid() << std::endl;
identyfikator użytkownika - getuid(),
efektywny identyfikator użytkownika - geteuid(),
identyfikator grupy - getgid(),
efektywny identyfikator grupy - getegid().
Wykład 4 – p. 6/7
Stany procesu
wykonywalny (runnable) - proces w kolejce procesów
gotowych do wykonania,
uśpiony (sleeping) - proces czeka na wystapienie
˛
określonego
„zdarzenia” (np na dane do przeczytania, na sygnał, na dostep
˛
do zasobu lub dobrowolnie „śpi” na określony okres),
zatrzymany (stopped) - proces wykonywany lecz zatrzymany
na skutek otrzymania sygnału SIGSTOP lub SIGTSTP,
wymieciony (swapped out - proces usuniety
˛ okresowo z kolejki
procesów gotowych do wykonywania wskutek działania
algorytmu obsługi pamieci
˛ wirtualnej,
zombie - proces czeka na zakończenie.
Wykład 4 – p. 7/7
Zmienne środowiskowe
Każdy program otrzymuje dostep
˛ do zmiennych
środowiskowych poprzez tablice˛ wskaźników
extern char **environ
Przykładowe funkcje:
char *getenv(const char *name);
pobiera wartość zmiennej
int setenv(const char *name, const char *value, int rewrite);
ustawia wartość zmiennej, rewrite określa czy funkcja ma
nadpisać istniejac
˛ a˛ zmienna˛
Wykład 4 – p. 8/7
Obsługa błedów
˛
W wiekszości
˛
przypadków funkcje systemowe, które kończa˛
sie˛ błedem
˛
zwracaja˛ wartość -1 i przypisuja˛ zmiennej errno
wartość wskazujac
˛ a˛ rodzaj błedu.
˛
Kody błedów
˛
- plik nagłówkowy <sys/errno.h>
Odpowiedni komunikat można uzyskać przy pomocy funkcji
perror:
#include <stdio.h>
void perror(const char *s);
const char *sys_errlist[];
int sys_nerr;
int errno;
lub poprzez wywołanie funkcji:
char *strerror(int errnum);
int strerror_r(int errnum, char *buf, size_t n);
Wykład 4 – p. 9/7
Obsługa błedów
˛
- przykład
#include <iostream>
#include <errno.h>
int main(int argc, char** argv)
{
char buffer[255];
int nchar = read(158, buffer, 255);
if (nchar == -1) {
std::cerr << "Wystapil blad - [" << strerror(errno)
<< "] o numerze " << errno << std::endl;
perror(argv[0]);
exit(1);
}
}
Wynik:
Wystapil blad - [Bad file descriptor] o numerze 9
Numer bledu: 9
./error: Bad file descriptor
Wykład 4 – p. 10/7
Argumenty wywołania programu
Do obsługi argumentów podawanych przy wywołaniu
programu została utworzona funkcja getopt:
#include <stdlib.h>
int getopt(int argc, char** argv, char* optstring);
extern char* optarg;
extern int optind, opterr;
argc - liczba argumentów (liczba łańcuchów wystepuj
˛ aca
˛ w tablicy optstring),
argv - tablica z przekazanymi argumentami,
optstring - opis opcji (przełaczników
˛
programu), jeżeli opcja posiada
dodatkowy argument, to za litera˛ powinien pojawić sie˛ znak dwukropka.
Wykład 4 – p. 11/7
getopt - wynik działania
Funkcja getopt zwraca jedna˛ z trzech wartości całkowitych:
Liczbe˛ -1, która oznacza, że wszystkie opcje zostały już
odczytane albo że natrafiono na pierwszy argument nie
bed
˛ acy
˛ opcja.
˛
Ekwiwalent znaku „?”, który oznacza, że została odczytana
litera opcji nie majaca
˛ odpowiednika w zmiennej
optstring,
Kolejna˛ litere˛ opcji z tablicy argv, która odpowiada literze
ze zmiennej optstring. Jeżeli po literze dopasowanej do
optstring znajduje sie˛ dwukropek, to wtedy zewnetrzny
˛
wskaźnik na znak optrarg bedzie
˛
odsyłał do wartości
argumentu.
Wykład 4 – p. 12/7
getopt - przykład (I)
void PrintUsage(std::ostream &os) {
os << "opt:\n"
<< " Uzycie: opt [-o output_file] [-hv] input_file\n" << " OPCJE\n"
<< " -o output_file: zapisuje wyniki do pliku o nazwie output_file\
<< " -v: wypisuje dodatkowe informacje o wykonywaniu programu\n"
<< " -h: wypisuje powyzsza informacje\n"
<< " input_file: plik wejściowy\n" << std::flush;
}
int main(int argc, char** argv) {
bool verbose = false; // dodatkowe informacje o wykonywaniu programu
bool usage = false; // informacje o opcjach programu
std::string outFileName;
extern
extern
extern
static
opterr
int c;
char* optarg;
int optind;
int opterr;
char optstring[] = "vho:";
= 0;
Wykład 4 – p. 13/7
getopt - przykład (II)
while ((c = getopt(argc, argv, optstring)) != -1)
{
switch (c) {
case ’v’:
verbose = true;
break;
case ’h’:
usage = true;
PrintUsage(std::cerr);
return 0;
case ’o’:
outFileName = std::string(optarg);
break;
case ’?’:
std::cerr << "Bledna opcja" << std::endl;
exit(1);
break;
}
}
Wykład 4 – p. 14/7
getopt - przykład (III)
if (verbose)
std::cout << "output file name: " << outFileName << std::endl;
std::string inputFileName = argv[optind];
std::cout << "input file name: " << inputFileName << std::endl;
return 0;
}
Wykład 4 – p. 15/7
Tworzenie procesu
Do klonowania procesów służy funkcja fork:
#include <sys/types.h>
#include <unistd.h>
pid_t
fork(void);
W wyniku działania funkcji fork jest utworzenie procesu
potomka, który jest prawie dokładna˛ kopia˛ rodzica (zasoby
pamieci
˛ sa˛ duplikowane, różni sie˛ od procesu macierzystego
identyfikatorem procesu).
Proces macierzysty, który wywołał funkcje fork dostaje
identyfikator dziecka.
Proces potomny dostaje 0.
Jeżeli działanie funkcji zakończyło sie˛ błedem
˛
zwracana jest
wartość -1.
Wykład 4 – p. 16/7
Kończenie pracy procesu
W chwili śmierci procesu potomnego jego rodzic otrzymuje
sygnał SIGCLD (domyślnie ignorowany).
Standardowo proces rodzica powinien odczytać kod
zakończenia potomka wywołujac
˛ funkcje˛ systemowa˛ wait():
pid_t wait(int *status);
pid_t waitpid(pid_t pid, int *status, int options);
Gdy proces nadrz˛edny żyje w chwili zakończenia pracy
potomka, i nie wywołuje funkcji wait(), to potomek zostaje w
stanie zombie.
Jeżeli proces ustawi sygnał SIGCHLD na SIG_IGN oznacza to,
proces macierzysty nie jest „zainteresowany” stanem
końcowym swoich procesów potomnych. W takim wypadku
jadro
˛
bedzie
˛
wykonywać funkcje wait() dla wszystkich jego
procesów potomnych.
Wykład 4 – p. 17/7
fork - przykład (I)
#include <sys/types.h>
#include <unistd.h>
#include <iostream>
int main() {
std::cout << "* Kod wykonywany przez proces macierzysty\n"
<< "* przed wykonaniem funkcji fork" << std::endl;
pid_t pid = fork();
if (pid != 0) {
std::cout << "* Kod wykonywany przez proces macierzysty po\n"
<< "* wykonaniu funkcji fork" << std::endl
<< "* PID: " << getpid() << std::endl
<< "* PID procesu potomka: " << pid << std::endl;
int status;
wait(&status);
} else
std::cout << "# Kod wykonywany przez proces potomka po\n"
<< "# wykonaniu funkcji fork" << std::endl
<< "# PID: " << getpid() << std::endl;
return 0; }
Wykład 4 – p. 18/7
fork - przykład (II)
Przykładowy rezultat działania programu:
*
*
*
*
*
*
#
#
#
Kod wykonywany przez proces macierzysty
przed wykonaniem funkcji fork
Kod wykonywany przez proces macierzysty po
wykonaniu funkcji fork
PID: 1375
PID procesu potomka: 1376
Kod wykonywany przez proces potomka po
wykonaniu funkcji fork
PID: 1376
Wykład 4 – p. 19/7
Funkcje exec
Funkcja exec umożliwia uruchomienie innego programu.
Kiedy proces uruchamia funkcje exec, zostaje on przykryty
nowym kodem programu. Segmenty tekstu, danych i stosu
zapełniane sa˛ nowymi danymi.
#include <unistd.h>
extern char **environ;
int execl(const char *path, const char *arg0, ... /*, (char *)0 */);
int execv(const char *path, char *const argv[]);
int execle(const char *path, const char *arg0, ... /*,
(char *)0, char *const envp[]*/);
int execve(const char *path, char *const argv[], char *const envp[]);
int execlp(const char *file, const char *arg0, ... /*, (char *)0 */);
int execvp(const char *file, char *const argv[]);
Wykład 4 – p. 20/7
exec - Przykład
#include <qapplication.h>
#include "getfile.h"
int main(int argc, char** argv)
{
QApplication app(argc, argv);
GetFile getFile;
getFile.resize(210, 70);
app.setMainWidget(&getFile);
getFile.show();
app.exec();
if (getFile.IsOk())
execlp("acroread", "acroread", "/tmp/slides.pdf", NULL);
return 0;
}
Wykład 4 – p. 21/7
Komunikacja miedzyprocesowa
˛
W systemie UNIX można wyróżnić nastepuj
˛ ace
˛ mechanizmy
komunikacji pomiedzy
˛
procesami:
pliki,
sygnały,
łacza
˛
komunikacyjne
mechanizmy synchronizacji IPC Systemu V:
semafory,
kolejki komunikatów,
pamieć
˛ dzielona,
gniazda.
Wykład 4 – p. 22/7
Procesy - współdzielenie informacji
Wykład 4 – p. 23/7
Pliki blokujace
˛
Pliki blokujace
˛ sa˛ jednym z najprostszych sposobów
komunikacji pomiedzy
˛
procesami.
Stosujac
˛ uzgodniona,
˛ wspólna˛ konwencje˛ nazywania plików,
określony proces sprawdza istnienie w danym miejscu pliku
blokujacego.
˛
Jeżeli plik jest obecny proce wykonuje określone działanie, w
przeciwnym wypadku jest wykonywana inna czynność.
Tego typu rozwiazanie
˛
wiaże
˛ sie˛ jednak z wystepowaniem
˛
wielu problemów.
Rozwiazaniem
˛
cz˛eści problemów mechanizmu plików
blokujacych
˛
może być zrealizowane poprzez wykorzystaniu
mechanizmu blokowania plików.
Wykład 4 – p. 24/7
Blokady typu POSIX
Funkcja fcntl umożliwia wykonanie wielu operacji na
deskryptorze otwartego pliku, w tym miedzy
˛
innymi operacji
dostepu
˛
do flag pliku i zarzadzaniem
˛
blokadami typu POSIX:
int fcntl(unsigned int fd, unsigned int cmd, unsigned long arg);
fd - deskryptor pliku,
cmd - komenda (w przypadku blokad plików sa˛ to: F_GETLK - pobiera blokady
założone na plik, F_SETLK - ustawia blokade˛ na plik, F_SETLKW - ustawia
blokade˛ na plik jeśli to możliwe),
arg - argument zależny od wykonywanej komendy. W przypadku blokad pliku
argument arg jest traktowany jako wskaźnik na strukture˛ typu struct flock:
struct
short
short
off_t
off_t
pid_t
flock {
l_type;
l_whence;
l_start;
l_len;
l_pid;
/*
/*
/*
/*
/*
typ blokady */
tryb obliczania przesuniecia w rekordzie */
poczatek obszaru */
przesuniecie w bajtach */
wlasciciel blokady zwracany przez F_GETLK */}
Typ blokady może przyjmować wartości: F_RDLCK - blokada odczytu, F_WRLCK blokada zapisu, F_UNLCK - usuwanie blokady.
Wykład 4 – p. 25/7
Blokady typu flock
Funkcja flock() udostepnia
˛
programiście dostep
˛ do
drugiego rodzaju blokady pliku:
int flock(unsigned int fd, unsigned int cmd);
fd - deskryptor pliku,
cmd - komenda:
LOCK_SH - blokada dzielona,
LOCK_EX - blokada wyłaczna,
˛
LOCK_NB - nie usypiaj w czasie blokowania,
LOCK_UN - usuwanie blokady.
Na plik nie można jednocześnie zakładać blokad typu
dzielonego i wyłacznego.
˛
Blokada wyłaczna
˛
może być tylko jedna na jednym pliku,
natomiast blokad dzielonych może być wiecej.
˛
Wywołanie funkcji flock może blokować proces w przypadku,
gdy blokada została już założona przez inny proces. W celu
wywołania nieblokujacego
˛
należy wywołać funkcje flock z
ustawiona˛ flaga˛ LOCK_NB
Wykład 4 – p. 26/7
Funkcje dup i dup2
Funkcje dup i dup2 służa˛ do powielania istniejacych
˛
deskryptorów plików:
int dup(int oldfd);
int dup2(int oldfd, int newfd);
funkcja dup tworzy nowy deskryptor, który jest kopia˛ deskryptora pliku oldfd
podanego jako argumentem wywołania funkcji,
funkcja dup2 tworzy deskryptor pliku newfd jako kopie˛ deskryptora oldfd. W
przypadku, gdy deskryptor newfd jest otwarty, to zostanie on zamkniety.
˛
Wykład 4 – p. 27/7
Sygnały
Sygnały służa˛ do powiadamiania procesów o wystepuj
˛ acych
˛
zdarzeniach.
Sygnały pojawiaja˛ sie˛ asynchronicznie (w nieustalonych
chwilach i bez przewidywanej kolejności).
Moga˛ być wysyłane przez procesy i przez jadro.
˛
Sygnał jest generowany w chwili wystapienia
˛
zdarzenia i
uznawany za dostarczony w chwili, gdy proces podejmie w
odpowiedzi na nie stosowne działanie.
Wykład 4 – p. 28/7
Ignorowanie i przechwytywanie sygnałów
W przypadku otrzymania sygnału proces może zareagować na
trzy sposoby:
wykonać operacje˛ domyślna:
˛
Exit
Core
Stop
Ignore
zignorować sygnał,
przechwycić sygnał.
Wykład 4 – p. 29/7
Przykładowe sygnały
Nazwa symboliczna
Wartość
Sygnalizowane zdarzenie
SIGABRT
6
Przerwanie (abort)
SIGALRM
14
Budzik (alarm clock)
SIGHUP
1
Zawieszenie (hangup)
SIGILL
4
Nielegalna instrukcja
SIGINT
2
Przerwanie (interrupt)
SIGKILL
9
Zabicie (kill)
SIGQUIT
3
Koniec (quit)
SIGUSR1
16
Sygnał użytkownika nr 1
SIGUSR2
17
Sygnał użytkownika nr 2
Wykład 4 – p. 30/7
Generowanie sygnału
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
pid - oznacza proces lub grupe˛ procesów do których wysłany zostanie sygnał,
Wartość pid
Procesy, które odbieraja˛ sygnał
>0
Procesy o identyfikatorach równych pid
0
Procesy należace
˛ do tej samej grupy co proces wysyłajacy
˛ sygnał
-1
W przypadku procesu superużytkownika - wszystkie procesy oprócz specjalnych, dla pozostałych procesów - wszystkie procesy, których rzeczywisty identyfikator jest równy efektywnemu identyfikatorowi procesu wysyłajacego
˛
sygnał
< -1
Procesy, których grupa ma identyfikator -pid.
Wykład 4 – p. 31/7
Funkcje obsługujace
˛ sygnały
Ignorowanie i przechwytywanie sygnału wymagaja˛ skojarzenia
sygnału z funkcja˛ przechwytujac
˛ a.
˛
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
signum - sygnał, który zostanie skojarzony z nowa˛ funkcja,
˛ SIG_ING lub SIG_DFL,
handler - funkcja przechwytujaca
˛ sygnał,
w przypadku niepowodzenia funkcja zwraca SIG_ERR.
Wykład 4 – p. 32/7
Sygnały - przykład
#include <iostream>
#include <signal.h>
void obsluga_sygnalu(int _signal)
{
std::cout << "otrzymano sygnal: " << _signal << std::endl;
}
int main()
{
if (signal(SIGINT, obsluga_sygnalu) == SIG_ERR)
{
perror("Problem z ustawieniem sygnalu SIGINT");
exit(SIGINT);
}
for (int i = 0; ; i++) {
std::cout << "." << std::flush;
sleep(1);
}
}
Wykład 4 – p. 33/7
Maska sygnałów
Do manipulacja˛ maska˛ sygnałów można wykorzystać funkcje
Systemu V:
#include <signal.h>
int
int
int
int
sighold(int sig);
sigignore(int sig);
sigpause(int sig);
sigrelse(int sig);
Wykład 4 – p. 34/7
POSIX - maska sygnałów
#include <signal.h>
int sigaction(int signum, const struct sigaction *act,
struct sigaction *oldact);
Wykład 4 – p. 35/7
Łacza
˛
Łacza
˛
(potoki) umożliwiaja˛ komunikacje˛ pomiedzy
˛
procesami
zgodnie z zasada˛ kolejki FIFO (First In First Out), jak również
synchronizacje˛ procesów.
Komunikacja może odbywać sie˛ również wtedy, gdy procesy
nie znaja˛ procesów znajdujacych
˛
sie˛ po drugiej stronie łacza.
˛
Dzieli sie˛ je na dwa rodzaje:
łacza
˛
nienazwane,
łacza
˛
nazwane,
Różnia˛ sie˛ sposobem w jaki procesy zaczynaja˛ z nich
korzystać, później do obsługi używa sie˛ tych samych funkcji.
Ponadto komunikacja pomiedzy
˛
łaczami
˛
nienazwanymi może
odbywać sie˛ tylko pomiedzy
˛
procesem potomnym, a procesem
macierzystym.
Wykład 4 – p. 36/7
Łacza
˛
nienazwane
Do otwarcia nienazwanego łacza
˛
służy funkcja systemowa
pipe
#include <unistd.h>
int pipe(int filedes[2]);
W przypadku bezbłednego
˛
działania funkcja zwraca dwa deskryptory plików
filedes[0] i filedes[1], które odsyłaja˛ do dwóch strumieni danych,
filedes[1] służy do zapisu,
filedes[1] wykorzystywany jest do odczytu.
Wykład 4 – p. 37/7
read/write
ssize_t read(int fd, void *buf, size_t count)
ssize_t write(int fd, const void *buf, size_t count)
fd - deskryptor
buff - adres danych do odczytania/zapisania,
length - rozmiar danych do odczytania/zapisania,
wynik - liczba odczytanych/zapisanych danych.
Wykład 4 – p. 38/7
Zamykanie deskryptorów łacza
˛
W przypadku komunikacji jednostronnej każdy z procesów
powinien zamknać
˛ nieużywany deskryptor łacza
˛
za pomoca˛
funkcji close:
int close(int fd);
Zamykanie nieużywanego pliku nie jest konieczne, ale wśród
programistów jest uważane za dobry zwyczaj programistyczny.
Wykład 4 – p. 39/7
Łacza
˛
nienazwane - przykład I
#include
#include
#include
#include
<unistd.h>
<iostream>
<sys/types.h>
<sys/wait.h>
const int BUFFSIZE = 255;
int main(int argc, char** argv)
{
int pipes[2];
char buffer[255];
int result = pipe(pipes);
if (result == -1)
{
perror("pipe");
exit(1);
}
Wykład 4 – p. 40/7
Łacza
˛
nienazwane - przykład II
switch (fork()) {
case -1:
perror("fork");
exit(2);
case 0:
// proces potomka
close(pipes[1]);
if (read(pipes[0], buffer, BUFSIZE) != -1)
std::cout << "otrzymano: " << buffer << std::endl;
else {
perror("read"); exit(3);
}
break;
default:
close(pipes[0]);
if (write(pipes[1], argv[1], strlen(argv[1])) != -1)
sts::cout << "wyslano: " << argv[1] << std::endl;
else { perror("write"); exit(4); }
int status;
wait(&status);
}
Wykład 4 – p. 41/7
Łacza
˛
nazwane
Łacza
˛
nazwane sa˛ podobne koncepcyjnie do potoków
nienazwanych.
Potoki nazwane tworzone sa˛ w postaci fizycznych plików.
Moga˛ być tworzone z poziomu powłoki (polecenie mknod) lub z
poziomu programu:
int mknod(const char* path, mode_t mode, dev_t dev);
int mkfifo(const char* path, mode_t mode);
Stała symboliczna
Plik
S_IFIFO
potok FIFO
S_IFCHR
specjalny znakowy
S_IFDIR
katalog
S_IFBLK
specjalny blokowy
S_IFREG
zwykły plik
Wykład 4 – p. 42/7
Łacza
˛
nazwane - przykład I
#include
#include
#include
#include
#include
<iostream>
<stdlib.h>
<sys/stat.h>
<unistd.h>
<linux/stat.h>
int main()
{
FILE* file;
char buffer[255];
mknod("my_fifo", S_IFIFO | 0666, 0);
while (true)
{
file = fopen("my_fifo", "r");
fgets(buffer, 255, file);
std::cout << "odebrano: " << buffer << std::endl;
fclose(file);
}
return 0;
}
Wykład 4 – p. 43/7
Łacza
˛
nazwane - przykład II
#include <iostream>
#include <stdlib.h>
int main(int argc, char** argv)
{
if (argc < 2) {
std::cerr << "Program wymaga argumentu" << std::endl;
exit(1);
}
FILE* file;
file = fopen("my_fifo", "w");
if (file == NULL) {
perror("fopen");
exit(2);
}
fputs(argv[1], file);
fclose(file);
return(0);
}
Wykład 4 – p. 44/7
Mechanizmy IPC Systemu V
Na mechanizmy IPC (interprocess communications) składaja˛
sie:
˛
kolejki komunikatów,
semafory,
pamieć
˛ wspólna (dzielona).
Wykład 4 – p. 45/7
Cechy IPC
Nie sa˛ deskryptorami plików i nie wykonuje sie˛ na nich operacji I/O standardowymi
funkcjami read/write, lecz każde urzadzenie
˛
ma swój specyficzny zbiór operacji I/O.
Po utworzeniu danego urzadzenia
˛
jest ono od razu gotowe do pracy, nie jest konieczne
jego otwieranie przez każdy proces pragnacy
˛ sie˛ komunikować. (Z wyjatkiem
˛
obszarów pamieci
˛ wspólnej, które każdy proces musi jeszcze odwzorować na swoja˛
przestrzeń adresowa.)
˛
Identyfikatory urzadze
˛
ń System V IPC (typu int) sa˛ globalne w systemie, odmiennie
niż deskryptory plików (także nie sa˛ kolejno generowanymi małymi liczbami). Oznacza
to, że jeden proces może użyć identyfikatora utworzonego przez inny proces.
Wynika stad
˛ możliwość, celowego lub nie, wkleszczania sie˛ procesu w komunikacje˛
prowadzona˛ przez inne procesy.
Konieczne jest jawne kasowanie urzadze
˛
ń funkcjami kontrolnymi. Urzadzenia
˛
komunikacyjne System V IPC istnieja˛ trwale w pamieci
˛ systemu, ale nie sa˛
przechowywane na dysku. Oznacza to, że istnieja˛ nadal po zakończeniu procesu,
który je utworzył, ale znikaja˛ bezpowrotnie przy restarcie systemu.
Wykład 4 – p. 46/7
Obiekty IPC
Każdy obiekt IPC musi zostać utworzony zanim bedzie
˛
można
z niego korzystać.
Obiekty IPC posiadaja˛ twórce,
˛ właściciela oraz przypisane
prawa dostepu,
˛
które można modyfikować.
Na poziomie systemowym dane znajdujace
˛ sie˛ w obiekcie
pobiera sie˛ za pomoca˛ programu ipcs:
------ Shared Memory Segments -------key
shmid
owner
perms
0x00000000 18219008
olas
600
bytes
393216
------ Semaphore Arrays -------key
semid
owner
perms
nsems
------ Message Queues
key
msqid
0x4204bd0b 327680
0x4204bd0d 360449
used-bytes
0
0
-------owner
olas
olas
perms
700
700
nattch
2
status
dest
messages
0
0
Do usuniecia
˛
obiektu IPC można użyć programu ipcrm
(potrzebne sa˛ do tego odpowiednie uprawnienia - właściciel,
root):
ipcrm [ -M key | -m id | -Q key | -q id | -S key | -s id ] ...
Wykład 4 – p. 47/7
Zestawienie funkcji IPC
Działanie
Kolejka komunikatów
Semafory
Pamieć
˛ wspólna
<sys/msg.h>
<sys/sem.h>
<sys/shm.h>
Rezerwacja obiektu IPC,
uzyskanie do niego dostepu
˛
msgget
semget
shmget
Sterowanie obiektem IPC
msgctl
semctl
shmctl
Operacje specyficzne
msgsnd
semop
shmmat
Plik nagłówkowy
msgrcv
shmdt
Wykład 4 – p. 48/7
Klucze key_t i funkcja ftok
Komunikacja miedzyprocesowa
˛
Systemu V używa jako swoich
nazw wartości typu key_t.
Cz˛esto wykorzystywanym sposobem przypisywania wartości
tego typu danym jest wykorzystanie funkcji ftok:
# include <sys/types.h>
# include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);
Funkcja łaczy
˛
otrzymane argumenty, tj. nazwe˛ ścieżki
(argument pathname) i 8-bitowy identyfikator
całkowitoliczbowy (argument proj_id) i tworzy klucz IPC w
postaci liczby całkowitej.
Wykład 4 – p. 49/7
ftok - przykład
#include <iostream>
# include <sys/types.h>
# include <sys/ipc.h>
int main(int argc, char** argv)
{
if (argc != 2)
std::cerr << "Usage: ftok pathname" << std::endl;
std::cout << "wartosc klucza key_t: " << ftok(argv[1], 1) << std::endl;
}
Przykładowy wynik działania:
$ ./ftok .
wartosc klucza key_t: 17078610
$ ./ftok ..
wartosc klucza key_t: 17042617
$ ./ftok /tmp
wartosc klucza key_t: 16982853
Wykład 4 – p. 50/7
Struktura IPC_PERM
Jadro
˛
systemu przechowuje na temat każdego obiektu IPC
strukture˛ informacyjna˛ o postaci podobnej do przechowywanej
informacji o plikach:
struct ipc_perm
{
key_t key;
// klucz
uid_t uid;
// identyfikator użytkownika właściciela
gid_t gid;
// identyfikator grupy właściciela
uid_t cuid;
// identyfikator użytkownika twórcy
gid_t cgid;
// identyfikator grupy twórcy
mode_t mode;
// tryby dost˛
epu
unsigned short seq; // numer kolejny
};
Wykład 4 – p. 51/7
Kolejki komunikatów
Kolejki komunikatów (Message Queue) to kolejka, na która˛
można odkładać komunikaty.
Kolejki moga˛ być publiczne - każdy proces może odłożyć
komunikat na kolejce i prywatne - tylko proces rodzica i
ewentualnie dziecko może odłożyć komunikat.
Komunikaty moga˛ być odczytywane i zapisywane w tym
samym czasie - nie ma ograniczenia jak w przypadku łacz,
˛ że
sa˛ typu FIFO.
Wykład 4 – p. 52/7
Tworzenie kolejki
Utworzenie nowej kolejki komunikatów lub otwarcie dostepu
˛
do
już istniejacej
˛ umożliwia funkcja systemowa msgget():
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgget (key_t key, int msgflg);
gdzie:
key - klucz,
msgflg - flagi:
IPC_CREAT - zostanie utworzona kolejka (o ile nie istnieje), uprawnienia do
kolejki określaja˛ 9 najmniej znaczacych
˛
bitów parametru msgflag,
IPC_EXCL - powoduje, że zwracany jest bład
˛ w przypadku, gdy kolejka już
istnieje.
Wykład 4 – p. 53/7
Bufor komunikatu
Operacje przesyłania komunikatów wymagaja˛ posłużenia sie˛
buforem komunikatu zdefiniowanym w nastepuj
˛ acy
˛ sposób:
struct msgbuf
{
long mtype; # typ komunikatu (wartość > 0)
char mtext[1]; # tekst komunikatu
};
Pole mtype określa typ komunikatu w postaci dodatniej liczby
całkowitej. Tablica mtext[] przechowuje treść komunikatu,
która˛ moga˛ stanowić dowolne dane. Rozmiar tablicy podany w
definicji nie stanowi rzeczywistego ograniczenia, ponieważ
bufor komunikatu można dowolnie przedefiniować w programie
pod warunkiem zachowania typu na poczatku.
˛
Wykład 4 – p. 54/7
Przesyłanie komunikatu
Funkcja msgsnd() umożliwia przesłanie komunikatu do kolejki:
int msgsnd(int msqid, struct msgbuf *msgp, int msgsz, int msgflg);
gdzie:
msqid - identyfikator kolejki komunikatów,
msgp - wskaźnik do bufora zawierajacego
˛
komunikat do wysłania,
msgsz - rozmiar bufora komunikatu z wyłaczeniem
˛
typu (rozmiar treści
komunikatu),
msgflg - flagi.
Wykład 4 – p. 55/7
Pobranie komunikatu
Funkcja msgrcv() pobiera z kolejki jeden komunikat
wskazanego typu. Pobrany komunikat jest usuwaney z kolejki.
int msgrcv(int msqid, struct msgbuf *msgp,
int msgsz, long msgtype, int msgflg);
gdzie:
msqid - identyfikator kolejki komunikatów,
msgp - wskaźnik do bufora, do którego ma być zapisany komunikat, pobrany z
kolejki,
msgsz - rozmiar bufora komunikatu z wyłaczeniem
˛
typu (rozmiar treści
komunikatu),
msgtype - typ komunikatu do pobrania z kolejki:
msgtype > 0 - pobiera najstarszy komunikat danego typu,
msgtype == 0 - pobiera najstarszy komunikat w kolejce.
msgflg - flagi.
Wykład 4 – p. 56/7
Kolejki - przykład I
struct my_msg {
long mtype;
char mtext[255];
};
int main(int argc, char** argv)
{
if (argc != 2)
std::cerr << "Uzycie: sender komunikat" << std::endl;
key_t klucz = ftok("sender.cpp", 1);
int msgid = msgget(klucz, IPC_CREAT | 0666);
std::cout << "msgid: " << msgid << std::endl;
my_msg buf;
buf.mtype = 1;
strcpy(buf.mtext, argv[1]);
if (msgsnd(msgid, (struct msgbuf *)&buf, sizeof(buf), 0) == -1)
perror("msgsnd");
}
Wykład 4 – p. 57/7
Kojeki - przykład II
struct my_msg {
long mtype;
char mtext[255];
};
int main(int argc, char** argv)
{
key_t klucz = ftok("sender.cpp", 1);
int msgid = msgget(klucz, IPC_CREAT | 0666);
my_msg buf;
if (msgrcv(msgid, (struct msgbuf *)&buf, sizeof(buf), 1, 0) == -1)
perror("msgsnd");
std::cout << "id: " << buf.mtype << "\ntext: " << buf.mtext << std::end
if (msgctl(msgid, IPC_RMID, NULL) == -1)
perror("msgctl");
}
Wykład 4 – p. 58/7
Kolejki komunikatów - zarzadzanie
˛
Do wykonywania operacji kontrolnych na kolejce komunikatów
służy funkcja msgctl:
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
msgid - identyfikator kolejki,
cmd - rodzaj operacji wykonywanych na kolejce:
IPC_RMID - usuniecie
˛
kolejki,
IPC_STAT - pobranie struktury msqid_ds i zapisanie jej w
parametrze buf (zawiera prawa dostepu
˛
do kolejki,
informacje dotyczace
˛ ostatniego wysłania i odebrania
komunikatu, aktualna˛ liczbe˛ danych znajdujacych
˛
sie˛ w
kolejce, jej pojemność),
IPC_SET - zmiana praw dostepu
˛
i pojemności kolejki
poprzez przekazana˛ strukture˛ msqid_ds w parametrze
buf.
Wykład 4 – p. 59/7
Semafory I
W teorii semafor jest nieujemna˛ zmienna˛ (sem), domyślnie kontrolujac
˛ a˛ przydział
pewnego zasobu. Wartość zmiennej sem oznacza liczbe˛ dostepnych
˛
jednostek
zasobu. Określone sa˛ nastepuj
˛ ace
˛ operacje na semaforze:
P(sem) - oznacza zajecie
˛
zasobu sygnalizowane zmniejszeniem wartości
semafora o 1, a jeżeli jego aktualna wartość jest 0 to oczekiwanie na jej
zwiekszenie,
˛
V(sem) - oznacza zwolnienie zasobu sygnalizowane zwiekszeniem
˛
wartości
semafora o 1, a jeżeli istnieje(a)
˛ proces(y) oczekujacy(e)
˛
na semaforze to,
zamiast zwiekszać
˛
wartość semafora, wznawiany jest jeden z tych procesów.
Istotna jest niepodzielna realizacja każdej z tych operacji, tzn. każda z operacji P, V
może albo zostać wykonana w całości, albo w ogóle nie zostać wykonana. Z tego
powodu niemożliwa jest prywatna implementacja operacji semaforowych przy użyciu
zmiennej globalnej przez proces.
Wykład 4 – p. 60/7
Semafory II
Możemy wyróżnić:
semafor binarny : semafor, którego wartościa˛ jest 0 lub 1.
semafor zliczajacy
˛ : semafor, którego wartość leży w
zakresie od 0 do określonego limitu.
W Systemie V wprowadzono zbiór semaforów zliczajacych,
˛
tj.
zbiór składajacy
˛ sie˛ z co najmniej jednego semafora, a każdy
semafor jest zliczajacy.
˛
Wykład 4 – p. 61/7
Utworzenie semafora
Utworzenie nowego zestawu semaforów lub dostep
˛ do już
istniejacej
˛ zapewnia funkcja systemowa semget():
int semget(key_t key, int nsems, int semflg);
gdzie:
key - klucz,
nsems - liczba semaforów w zbiorze,
semflg - flagi.
Wykład 4 – p. 62/7
Operacje na semaforze (I)
Operacje na semaforach umożliwia funkcja semop():
int semop(int semid, struct sembuf *sops, unsigned nsops);
gdzie:
semid - identyfikator zbioru semaforów,
sops - wskaźnik do tablicy, której elementami jest struktura danych:
struct sembuf
{
short sem_num;
short sem_op;
short sem_flg;
};
// indeks semafora w tablicy: 0, 1, 2, ..., nse
// operacja na semaforze
// sygnalizatory operacji: 0, IPC_NOWAIT, SEM_U
Każdy element tej tablicy określa operacje˛ wykonywana˛ na jednym konkretnym
semaforze w zbiorze.
nsops - liczba elementów struktury sembuf
Wykład 4 – p. 63/7
Operacje na semaforze (II)
Dla sem_op < 0 podana wartość bezwzgledna
˛
zostanie
odjeta
˛ od wartości semafora. Ponieważ wartość semafora nie
może być ujemna, to proces może zostać zablokowany
(uśpiony) do momentu uzyskania przez semafor odpowiedniej
wartości, która umożliwi wykonanie operacji. Odpowiada to
zajeciu
˛
zasobu.
Dla sem_op > 0 podana wartość zostanie dodana do
wartości semafora. T˛e operacje˛ można zawsze wykonać.
Odpowiada to zwolnieniu zasobu.
Przy sem_op = 0 proces zostanie uśpiony do momentu, gdy
semafor osiagnie
˛
wartość zerowa.
˛
Wykład 4 – p. 64/7
Pamie˛ ć współdzielona
Wszystkie dotychczasowe metody komunikacji powstały m.in.
po to, aby współdzielić dane. Jedyna ich niedogodność to
sekwencyjność.
Pamieć
˛ współdzielona rozwiazuje
˛
problem sekwencyjności dostep
˛ do zasobów jest swobodny.
Z pomoca˛ przychodzi Unixowy sposób przydzielania każdemu
z procesów wirtualnej przestrzeni adresowej, co implikuje duża˛
stabilność.
Wykład 4 – p. 65/7
Pamie˛ ć współdzielona - idea
Deklarujemy sekcje˛ w pamieci,
˛ z której procesy bed
˛ a˛
korzystały jednocześnie.
Oznacza to, iż dane w tej sekcji pamieci
˛ (segmencie) bed
˛ a˛
widziane przez różne procesy.
Wiaże
˛ sie˛ z tym możliwość jednoczesnej modyfikacji zmiennej,
co wymusza użycie narz˛edzi synchronizujacych.
˛
Wykład 4 – p. 66/7
Tworzenie segmentu pamieci
˛
Utworzenie nowego segmentu pamieci
˛ dzielonej lub uzyskanie
dostepu
˛
do już istniejacego
˛
umożliwia funkcja systemowa
shmget().
int shmget(key_t key, int size, int shmflg);
gdzie:
key - klucz,
size - rozmiar segmentu pamieci,
˛
shmflg - flagi. Funkcja zwraca identyfikator segmentu pamieci
˛ zwiazanego
˛
z
podana˛ wartościa˛ klucza key.
Wykład 4 – p. 67/7
Przyłaczanie
˛
segmentu pamieci
˛
W wynika wywołania funkcji shmget() proces uzyskuje
identyfikator segmentu pamieci
˛ dzielonej. Aby można było z
niego korzystać, segment musi zostać jeszcze przyłaczony
˛
do
wirtualnej przestrzeni adresowej procesu za pomoca˛ funkcji
shmat():
void *shmat(int shmid, const void *shmaddr, int shmflg);
gdzie:
shmid - identyfikator segmentu pamieci
˛ dzielonej,
shmaddr - adres w przestrzeni adresowej procesu, od którego ma być dołaczony
˛
segment,
shmflg - flagi.
Funkcja zwraca adres poczatkowy
˛
dołaczonego
˛
segmentu w wirtualnej przestrzeni
adresowej procesu. Adres ten może być wyspecyfikowany w argumencie shmaddr.
Jadro
˛
systemu próbuje wtedy dołaczyć
˛
segment od podanego adresu pod warunkiem,
że jest on wielokrotnościa˛ rozmiaru strony pamieci.
˛ Zaokraglanie
˛
adresu w dół do
granicy strony może być dokonane przez jadro,
˛
jeżeli ustawiona jest flaga SHM_RND.
Zalecane jest jednak ustawienie shmaddr = 0 w wywołaniu funkcji, aby pozwolić na
wybór adresu przez jadro.
˛
Wykład 4 – p. 68/7
Odłaczanie
˛
segmentu pamieci
˛
Po zakończeniu korzystania z segmentu pamieci
˛ dzielonej,
proces powinien odłaczyć
˛
go za pomoca˛ funkcji systemowej
shmdt().
int shmdt(const void *shmaddr);
gdzie:
shmaddr - adres poczatkowy
˛
segmentu w przestrzeni adresowej procesu.
Wykład 4 – p. 69/7
Usuniecie
˛
segmentu pamieci
˛
Odłaczenie
˛
segmentu nie oznacza automatycznie usuniecia
˛
z
jadra
˛
systemu. Segment pozostaje w pamieci
˛ i może być
ponownie dołaczany
˛
przez procesy. W celu usuniecia
˛
segmentu trzeba posłużyć sie˛ funkcja˛ systemowa˛ shmctl().
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
gdzie:
shmid - identyfikator segmentu,
cmd - operacja sterujaca,
˛
buf - wskaźnik do bufora przeznaczonego na strukture˛ shmid_ds segmentu.
Wykład 4 – p. 70/7