LINUX
Komunikacja między procesami (IPC)
Komunikacja IPC
Wprowadzenie
Aby dwa procesy komunikowały się ze sobą, muszą obydwa się na to zgodzić, a system operracyjny musi dostraczyć narzędzi przeznaczonych do komunikacji między procesami (ang. Interprocess communication, IPC).
Komunikacja między procesami nie dotyczy jedynie wymiany informacji pomiędzy procesami w sieci, ale przede wszystkim procesów wykonywanych w jednym systemie w jednym systemie komputerowym (patrz rys.1).
Rys.1 Komunikacja między dwoma procesami w jednym systemie
Widzimy, że komunikacja między dwoma procesami odbywa się za pośrednictwem jądra. Jest to sytuacja typowa, ale nie jest to wymóg.
Komunikacja między procesami w tym samym systemie może być realizowana na kilka róznych sposobów:
Pół-dupleksowe łącza komunikacyjne (ang. half-duplex UNIX pipes),
Kolejki FIFO (łącza nazwane, ang. named pipes),
Kolejki komunikatów (ang.SYS V style message queues),
Zbiory semaforów (ang.SYS V style semaphore sets),
Pamięć współdzielona (ang.SYS V shared memory segments),
Pełno-dupleksowe łacza komunikacyjne (amg. Full-duplex pipes, STREAMS pipes).
Komunikacja między procesami wykonywanymi w różnych systemach przy użyciu jakiejś sieci łączącej systemy może wygldać tak jak na rys.2.
Rys.2 Komunikacja między dwoma procesami w różnych systemach
Komunikacja między procesami znajdującymi się w różnych systemach realizowana jest za pośrednictwem gniazd (ang networking socets, Berkley style).
Łącza komunikacyjne (ang. pipes)
Łącze komunikacyjne jest metodą, która umożliwia połączenie standardowego wyjścia jednego z procesów do standardowego wejścia innego lub tego samego procesu. Łącze komunikacyjne umożliwia przepływ danych tylko w jednym kierunku (stąd nazwa pół-duplex).
Rys.3 Łącze komunikacyjne w jednym procesie
Schemat zastosowania łącza komunikacyjnego w jednym i tym samym procesie pokazano na rys.3. Zasady czytania danych z łącza, w którym nie ma żadnych danych oraz pisanie do łącza wówczas, gdy jest zapełnione podamy dalej, przy okazji omawiania łączy nazwanych.
Rys.4 Łącze komunikacyjne w jednym procesie bezposrednio po wywołaniu funkcji fork
Możliwość wymiany przez proces informacji tylko z sobą jest mało interesująca, chociaż czasami może być przydatna, np. w razie konieczności kolejkowania informacji.
Typowym zsatosowaniem łączy komunikacyjnych jest komunikowanie się dwóch róznych procesów w następujący sposób. Najpierw proces tworzy łacze komunikacyjne, następnie zaś wywołuje funkcje systemową fork, aby utworzyć swoją kopię (patrz rys.4).
Rys.5 Łącze komunikacyjne między dwoma procesami
Następnie proces macierzysty zamyka np. koniec łącza służący do czytania, a proces potomny zamyka koniec łącza służący do pisania. Powstaje w ten sposób jednokierunkowy przepływ informacji między dwoma procesami (rys.5).
Gdy użykownik wprowadzi na przykład z poziomu shell'a następujące polecenie:
who | sort | lpr
Wówczas shell utworzy po kolei trzy procesy i dwa łącza pomiędzy nimi. Utworzony w ten sposób tzw. potok (ang. pipeline) przedstawiony jest na rys.6.
Rys.6 Łącza komunikacyjne między dwoma procesami tworza potok
Wszystkie omawiane łącza były jednokierunkowe, a więc umożliwiały przepływ danych tylko w jedną stronę. Jeśli chcemy uzyskać przepływ danych w obie strony, to musimy stworzyć dwa łącza komunikacyjne skierowane przeciwnie. Trzeba w tym celu wykonać następujące kroki:
Utwórz łacze 1, utwórz łacze 2,
Wywołaj funkcję systemową fork,
Przodek zamyka łącze 1 do czytania,
Przodek zamyka łącze 2 do pisania,
Potomek zamyka łącze 1 do pisania,
Potomek zamyka łącze 2 do czytania.
Schemat konstrukcji przedstawiono na rys.7. Od tego momentu oba procesy posiadają pseudo pełno-duplexowe łącze komunikacyjne.
Rys.7 Dwa łącza komunikacyjne umożliwiają dwukierunkowy przepływ informacji
Tworzenie łaczy komunikacyjnych w języku C
Standardowo łącze komunikacyjne na poziomie języka C tworzy się za pomocą funkcji systemowej pipe. Funkcja pobiera pojedyńczy parametr, bedący wektorm dwóch liczb całkowitych, i zwraca (jeśli wywołanie kończy się pomyślnie) w nich dwa nowe deskryptory wykorzystywane przy konstrukcji potoku.
Wywołanie systemowe: pipe();
Prototyp: int pipe( int fd[2] );
RETURNS: 0 on success
-1 on error: errno = EMFILE (no free descriptors)
EMFILE (system file table is full)
EFAULT (fd array is not valid)
Uwaga: fd[0] jest deskryptorem czytania, fd[1] - deskryptorem pisania
Szkic programu wywołującego funkcje pipe może mieć postać:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
main()
{
int fd[2];
pipe(fd);
.
.
}
Ustanowiwszy łącze komunikacyjne możemy utworzyć nowy proces:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
main()
{
int fd[2];
pid_t childpid;
pipe(fd);
if((childpid = fork()) == -1)
{
perror("fork");
exit(1);
}
.
.
}
Jeśli przodek chce otrzymywać dane od potomka powinien zamknąć deskryptor fd[1], zaś potomek powinien zamknąć fd[0]. Jeśłi z kolei przodek chce przesyłać dane do potomka, wtedy musi zamknąć fd[0], zas potomek powinien zamknąć fd[1]. Jest to istotne z praktycznego punktu widzenia, ponieważ EOF nie będzie nigdy zwrócony jeśli zbędne końce łącza nie zostaną jawnie zamknięte.
/* "Linux Programmer's Guide - Chapter 6" */
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main(void)
{
int fd[2], nbytes;
pid_t childpid;
char string[] = "Hello, world!\n";
char readbuffer[80];
pipe(fd);
if((childpid = fork()) == -1)
{
perror("fork");
exit(1);
}
if(childpid == 0)
{
/* Proces potomny zamyka wejściową stronę łącza */
close(fd[0]);
/* Wysyła "string" poprzez wejście do łącza */
write(fd[1], string, strlen(string));
exit(0);
}
else
{
/* Proces przodka zamyka wyjąsiową strone łącza */
close(fd[1]);
/* Czyta `string' z łącza */
nbytes = read(fd[0], readbuffer,
sizeof(readbuffer));
printf("Odebrano łańcuch: %s", readbuffer);
}
return 0;
}
Często deskryptory potomka są duplikowane po to, aby wskazywały na standardowe wejście lub wyjście:
Wywołanie systemowe: dup();
Prototyp: int dup( int olffd);
RETURNS: 0 on success
-1 on error: errno = EBADF (oldfd is not a valid descriptor)
EBADF (newfd is out of range)
EMFILE (too many descriptors for the
process)
Uwaga: stary deskryptor nie jest zamknięty; oba mogą być używane zamiennie
Zwykle po to duplikujemy deskryptory, aby zamknąć najpierw standardowy strumień (wej/wyj). Dzieje się tak dzięki temu, że wywołanie systemowe dup() przydzielając nowy despryptor wykorzystuje nieużywany (wolny) deskryptor o najniższym numerze. Rozważmy następujący fragment:
childpid = fork();
if(childpid == 0)
{
/* Zamknij standardowe wejście procesu potomnego */
close(0);
/* Duplikuj wejściową stronę łącza i przydziel ją do
stdin */
dup(fd[0]);
execlp("sort", "sort", NULL);
.
}
Procedura execlp uruchamia nowy proces sort (standardowe polecenie shell'a). Ponieważ nowo uruchomiony proces dziedziczy standardowy strumień od swego stwórcy, stąd w naszym przypadku odziedziczy wejście do łącza jako swoje standardowe wejście. Od tego momentu każda informacja wysyłąna przez przodka do łącza będzie przekazywana do procesu sortującego.
Istnieje także inna odmiana procedury dup(), występująca pod nazwą dup2().
Wywołanie systemowe: dup2();
Prototyp: int dup( int olffd, int newfd);
RETURNS: new descriptor on success
-1 on error: errno = EBADF (oldfd is not a valid descriptor)
EBADF (newfd is out of range)
EMFILE (too many descriptors for the
process)
Uwaga: stary deskryptor jest zamykany przez dup2()
Funkcja dup2() jest niepodzielna, tzn. że proces duplikacji oraz zamykania deskryptora nie może być przerwany przez napływające sygnały. W przypadku użycia dup() zachodzi konieczność użycia nastepnie close(). Pomiędzy tymi dwoma wywołaniami może minąć troche czasu i jeśli w tym czasie nadejdzie sygnał proces duplikowania może zakończyć się błędem (deskryptor 0 może zająć inny proces).
childpid = fork();
if(childpid == 0)
{
/* Zamknij stdin, duplikuj wejściową strone łacza
i przydziel ją do stdin */
dup2(0, fd[0]);
execlp("sort", "sort", NULL);
.
}
Prostszy sposób tworzenia łączy komunikacyjnych w języku C
W standardowej bibliotece wejści/wyjścia systemu Linux istnieje funkcja, która tworzy łącze komunikacyjne oraz iniciuje wykonywanie drugiego procesu po to, aby albo czytał z łącza albo do niego pisał. Ta funkcja tworzy pół-dupleksowy potok poprzez wewnętrzne wywołanie funkcji pipe().
Funkcja biblioteczna: popen();
Prototyp: FILE *popen ( char *command,
char *type);
RETURNS: new file stream on success, NULL on
unsuccessful fork() or pipe() call
Uwaga: tworzy łacze oraz wykonuje fork/exec wykorzystując „command”
Argument command oznacza wiersz polecenia. Funkcja ta wywoływana jet przez shell, stąd używa się zmiennej środowiskowej PATH do zlokalizowania polecenia.
Kierunek przepływu danych określony jest przez parametr type. Jeśli wartością tego argumentu jest r, to proces wywołujący funkcję będzie czytać z wejścia standardowego dla polecenia określonego przez argument command. Jeśłi zaś wartością jest w, to proces wywołujący funkcję będzei pisać do wyjścia standardowego dla polecenia command.
Łącze otwarte przy pomocy popen() musi być zamknięte przy pomocy funkcji pclose().
Funkcja biblioteczna: pclose();
Prototyp: int pclose(FILE *stream);
RETURNS: exit status of wait4() call
-1 if "stream" is not valid, or if wait4()
fails
Uwaga: czeka aż proces potokowy zakończy się i nastepnie zamyka strumień.
Rozważmy przykład:
#include <stdio.h>
#define MAXSTRS 5
int main(void)
{
int cntr;
FILE *pipe_fp;
char *strings[MAXSTRS] = { "echo",
"bravo", "alpha", "charlie", "delta"};
/* Utwórz jednokierunkowy potok, wywołując popen() */
if (( pipe_fp = popen("sort", "w")) == NULL)
{
perror("popen");
exit(1);
}
/* Przetwarzaj w pętli */
for(cntr=0; cntr<MAXSTRS; cntr++)
{
fputs(strings[cntr], pipe_fp);
fputc('\n', pipe_fp);
}
/* Zamknij łącze */
pclose(pipe_fp);
return(0);
}
Ponieważ popen() korzsta z shella, stąd dostepne sa wszystkie wszystkie wykorzystywane w nim znaki specjalne, przekierunkowywanie, potoki, itp. Oznacza to, że możłiwe sa następujące wywołania funkcji popen():
popen("ls ˜scottb", "r");
popen("sort > /tmp/foo", "w");
popen("sort | uniq | more", "w");
Kolejny przykład otwiera dwa łącza (jedeno związane z poleceniem ls, drugie z poleceniem sort), listuje zawartość katalogu bieżącego i przekazuje na wejście sort:
#include <stdio.h>
int main(void)
{
FILE *pipein_fp, *pipeout_fp;
char readbuf[80];
/* Utwórz jednokierunkowy potok, wywołując popen() */
if (( pipein_fp = popen("ls", "r")) == NULL)
{
perror("popen");
exit(1);
}
/* Utwórz jednokierunkowy potok, wywołując popen() */
if (( pipeout_fp = popen("sort", "w")) == NULL)
{
perror("popen");
exit(1);
}
/* Przetwarzaj w pętli */
while(fgets(readbuf, 80, pipein_fp))
fputs(readbuf, pipeout_fp);
/* Zamknij łącze */
pclose(pipein_fp);
pclose(pipeout_fp);
return(0);
}
Poniższy przykład szkicuje program, który otwiera potok pomiędzy przekazywanym poleceniem, a plikiem:
/* Plik tPopen.c */
#include <stdio.h>
int main(int argc, char *argv[])
{
FILE *pipe_fp, *infile;
char readbuf[80];
if( argc != 3)
{
fprintf(stderr, "USAGE: popen3 [command]
[filename]\n");
exit(1);
}
/* Otwórz plik wejściowy */
if (( infile = fopen(argv[2], "rt")) == NULL)
{
perror("fopen");
exit(1);
}
/* Utwórz jednokierunkowy potok, wywołując popen() */
if (( pipe_fp = popen(argv[1], "w")) == NULL)
{
perror("popen");
exit(1);
}
/* Przetwarzaj w pętli */
do
{
fgets(readbuf, 80, infile);
if(feof(infile)) break;
fputs(readbuf, pipe_fp);
} while(!feof(infile));
fclose(infile);
pclose(pipe_fp);
return(0);
}
Załóżmy, że po skompilowaniu i zlinkowaniu uzyskalismy plik wykonywalny o nazwie tPopen. Można go urochmiac wówczas np. tak:
tPopen sort tPopen.c
tPopen cat tPpopen.c
tPopen more tPopen.c
tPopen cat tPopen.c | grep main
Łącza nazwane (FIFO)
Łącza nazwane procuja podobnie jak zwykłe łacza, ale można zauważyć kilka istotnych różnic:
Łącze nazwane istnieje w systemie plikowym jako specjalny plik urządzenia,
Procesy o różnym poziomie pokrewieństwa mogą wymieniać dane poprzez łącze,
Jeśli wszystkie operacje I/O realizowane są przez współpracujące procesy, wtedy łącze nazwane pozostaje w systemie plikowym i może być wykorzystywane póżniej.
Istnieje kilka sposobów utworzenia łącza nazwanego. Dwa pierwsze mogą być zrealizowane na poziomie poleceń shell'a.
mknod MYFIFO p
mkfifo a=rw MYFIFO
Powyższe polecenia działaja podobnie, z jedną różnicą: mkfifo umożliwia natychmiasowe określenie upoważnień do łącza (np., a = rw), w przypadku mknod należy uczynić to przy pomocy polecenia chmod.
Aby sprawdzić, że łącze nazwane jest rzeczywiście plikiem posłuzmy się poleceniem ls:
$ ls -l MYFIFO
prw-rw-rw- 1 root root 0 Dec 07 10:20 MYFIFO|
Aby utworzyć łącze FIFO z pozimu języka C należy skorzystać z funkcji mknod():
Funkcja biblioteczna: popen();
Prototyp: int mknod( char *pathname,
mode_t mode, dev_t dev);
RETURNS: 0 on success,
-1 on error: errno = EFAULT (pathname invalid)
EACCES (permission denied)
ENAMETOOLONG (pathname too long)
ENOENT (invalid pathname)
ENOTDIR (invalid pathname)
(see man page for mknod for others)
Uwaga: tworzy węzeł systemu plikowego (file, device file, or FIFO)
Argument pathname oanacza normalna nazwę ścieżki i będzie nazwą kolejki, mode - określa tryb dostępu do pliku, które będzie zsumowane logicznie ze znacznikiem S_IFIFO z pliku <sys/stat.h> wskazującym, że ma być utworzona kolejka FIFO. W przypadku tworzenia łącza FIFO pomija się argument dev (urządzenie).
mknod("/tmp/MYFIFO", S_IFIFO|0666, 0);
Należy pamiętać, że wynikowe prawa dostępu do łacza nazwanego są wynikiem logicznego złożenia żądanych praw dostępu oraz maski wynikającej z praw procesu, który wywołuje operację mknod():
final_umask = requested_permissions & ˜original_umask
Powszechnie stosowany trick:
umask(0);
mknod("/tmp/MYFIFO", S_IFIFO|0666, 0);
Funkcja umask() ustawia tzw. maskę trybu dostępu do pliku.
Gdy powsatnie już kolejka FIFO, wówczas trzeba ja otworzyć do czytania lub pisania przy użyciu funkcji systemowej open(), lub jednej z funkcji standardowego wejścia-wyjścia - fopen() lub freopen(). Dalej przy obsłudze łącza FIFO będziemy otwierać je przy pomocy funkcji fopen(), a zamykać przy pomocy fclose(). Zauważmy, że open i close są odwołanaimi systemowymi (funkcjami systemowymi), podczas gdy fopen i fclose - funkcjami bibliotecznymi.
Rozważmy proces, który jest serwerem:
/* fifoserver.c */
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <unistd.h>
#include <linux/stat.h>
#define FIFO_FILE "MYFIFO"
int main(void)
{
FILE *fp;
char readbuf[80];
/* Utwórz FIFO jeśli nie istnieje */
umask(0);
mknod(FIFO_FILE, S_IFIFO|0666, 0);
while(1)
{
fp = fopen(FIFO_FILE, "r");
fgets(readbuf, 80, fp);
printf("Otrzymany łańcuch: %s\n", readbuf);
fclose(fp);
}
return(0);
}
Ponieważ łącza FIFO są zawsze domyślnie blokowane (patrz dalej), stąd po skom\pilowaniu możemy napisac polecenie:
$ fifoserver&
Dla naszego prostego serwera można teraz dopisać klienta:
/* fifoclient.c */
#include <stdio.h>
#include <stdlib.h>
#define FIFO_FILE "MYFIFO"
int main(int argc, char *argv[])
{
FILE *fp;
if ( argc != 2 )
{
printf("Stosuj: fifoclient [string]\n");
exit(1);
}
if((fp = fopen(FIFO_FILE, "w")) == NULL)
{
perror("fopen");
exit(1);
}
fputs(argv[1], fp);
fclose(fp);
return(0);
}
Łącza nazwane (FIFO) - blokowanie działań
Blokowanie jest standardowym stanem łącza FIFO. Oznacza to, że jeśli FIFO jest otwarte do czytania, to proces który to zrobił jes blokowany dotąd dopóki jakiś inny proces nie otworzy tego łącza do pisania (i vice-versa). Jeśl tego typu zachowanie jest zbędne, wtedy w funkcji open lub fopen należy użyć flagi O_NONBLOCK.
Czytanie danych złączy komunikacyjnych i kolejk FIFO oraz zapisywanie w nich danych przebiega wg następujących reguł:
Jeśli proces żąda przeczytania mniejszej porcji danych niż wynosi bieżąca zawartość łącza komunikacyjnego lub kolejki FIFO, to będzie pobranych dokładnie tyle danych ile zażądano.
Jeśli proces żąda przeczytania większej porcji danych niż wynosi bieżąca zawartość łącza komunikacyjnego lub kolejki FIFO, to funkcja read przekaże tylko tyle danych ile ich znajduje się w łączu.
Jeśli w łączu komunikacyjnym lub kolejce FIFO nie ma danych, a żaden proces nie otworzył ich do pisania, to wartością funkcji systemowej read będzie zero, oznaczjące koniec pliku. Jeśli proces, który wywołuje funkcję read ustawił flagę O_NONBLOCK, to nie wiadomo, czy przekazane przez funkcję zero oznacza, że nie ma danych, czy też, że nie ma żadnego procesu, który mógłby je pisać.
Jeśli proces zapisze mniejszą porcję danych niż wynosi pojemność łącza komunikacyjnego lub kolejki FIFO, to jest zagwarantowane, że pisanie jest operacją niepodzielną. Jeśli porcja danych jest z kolei większa, wówczas nie można zagwarantować, że oparcja będzie niepodzielna.
Jeśłi porces wywołuje funkcję write, aby zapisać dane do łącza komunikacyjnego lub kolejki FIFO, ale istnieje proces, który otworzył je do czytania, to do procesu bdzie wysłany sygnał SIGPIPE, a funkcja write przekaże w wyniku zero oraz w zmiennej globalnej errno umieści wartość stałej EPIPE.
Komunikacja między procesami w Systemie V
Przypomnijmy, że w Systemie V występują trzy rodzaje komunikacji między procesowej:
Kolejki komunikatów (ang.SYS V style message queues),
Zbiory semaforów (ang.SYS V style semaphore sets),
Pamięć współdzielona (ang.SYS V shared memory segments),
Każdy obiekt IPC ma przypisany unikalny identyfiaktor IPC (IP ID). Unikalność ta zagwarantowana jest w ramach jądra. Umożliwia to proste odwoływanie się do IPC obiektów, np. pamieci współdzielonej.
W celu utworzenia unikalnego ID należy użyć klucza. Klucz musi być wzajemnie uzgodniony zarówno przez klienta, jak i serwer.
Klucz może być przez cały czas taki sam, np. wbudowany na stałe w aplikację. Nie jest to zbyt korzysten, ponieważ istnieje zawsze możliwość, że jest on już w uzyciu. Wygodniej jest posłużyć się funkcją ftok(), która generuje wartość klucza dla serwera i klienta:
Funkcja biblioteczna: ftok();
Prototyp: key_t ftok(char *pathname, char proj );
RETURNS: new IPC key value if successful,
-1 if unsuccessful, errno set to return of stat() call
Zwracana wartość klucza jest kombinacją numeru i-węzła i numeru urządzenia, związanych z plikiem podanym jako argument pathname oraz parametru projektowego (np. nazwy aplikacji). Rozwiązanie takie nie gwarantuje unikalniości, ale alikacja może wykryć kolizje i operacje powtórzyć.
Przykłady generowania klucza:
key_t mykey;
mykey = ftok("/tmp/myapp", 'a');
lub
key_t mykey;
mykey = ftok(".", 'a');
Polecenie ipcs
Polecenie ipcs umożliwia określenie statusu wszystkich obiektów IPC (System V). Jego składnia ma postać:
ipcs -q: Show only message queues
ipcs -s: Show only semaphores
ipcs -m: Show only shared memory
ipcs --help: Additional arguments
Domyślnie pokazywane są wszystkie trzy kategorie obiektów. Popatrzmy na następujący wynika dziaąłnia polecenia ipcs:
------ Shared Memory Segments --------
shmid owner perms bytes nattch status
------ Semaphore Arrays --------
semid owner perms nsems status
------ Message Queues --------
msqid owner perms used-bytes messages
0 root 660 5 1
Widać, że istnieje jedna kolejka komuniktów o identyfikatorze 0. Jej właścicielem jest root i posiada prawa dostępu 660 lub -rw-rw---. W kolejce znajduje się jedna wiadomość o długości 5 bajtów.
Polecenie ipcrm
Polecenie ipcrm umożliwia usunięcie obiektu IPC (System V) z jądra. Chociaż IPC obiekty można usuwać przy pomocy funkcji systemowych, to często wygodnie jest usunąć je także z poziomu poleceń shella (ręcznie). Polecenie ma postać:
ipcrm <msg | sem | shm> <IPC ID>
W poleceniu należy określić typ usuwanego obiektu: msg - kolejka komunikatów, sem - semafor lub shm - segment pamięci współdzielonej oraz IPC ID (można go otrzymać przy pomocy polecenia ipcs). UWAGA! NALEŻY ZAWSZE PODAWAĆ TYP OBIEKTU, PONIEWAŻ JĄDRO GWARANTUJE UNIKALNOŚĆ IPC ID TYLKO W RAMACH DANEGO TYPU OBIEKTU.
Kolejki komunikatów
Koljka komunikatów jest wewnętrznie powiązaną listą zarządzana przez jądro. Wiadomość można przesłać do kolejki, jak również można ją stamtąd pobrać.
Aby dobrze rozumieć koleki komunikatów należy dokładnie zapoznać się z wewnętrzymi strukturami stosowanymi do ich opisu.
Bufor wiadomości. Struktura ta o nazwie msgbuf jest wzorcem danych wiadomości i określona jest w pliku linux/msg.h:
struct msgbuf
{
long mtype; /* typ wiadmości */
char mtext[1]; /* tekst wiadomości */
};
gdzie:
type - typ komunikatu (musi być liczba dodatnią),
mtext - wiadomość
Uwaga. Wzorzec msgbuf można redefiniować, np. w taki sposób:
struct my_msgbuf
{
long mtype; /* typ wiadmości */
long request_id; /* identyfikator żądania */
struct client_info; /* tekst wiadomości */
};
Struktura (jądra) msg. Jądro przechowuje każdą wiadomość w kolejce wewnątrz struktury msg (patrz plik linux/msg.h):
/* jedna struktura msg na każda wiadomość */
struct msg
{
struct msg *msg_next; /* następna wiadomość w kolejce */
long msg_type;
char *msg_spot; /* adres tekstu wiadomości */
short msg_ts; /* rozmiar tekstu wiadomości */
};
gdzie:
msg next - wskaźnik na następną wiadomość w kolejce jednokierunkowej w obszarze adresowym jądra,
msg_type - typ wiadomości, określony w strukturze użytkownika msgbuf,
msg_spot - wskaźnik na początek głównej części wiadomości,
msg_ts - długość tekstu lub głównej części wiadomości.
Struktura jądra msqid_ds. Każdy z trzech typów IPC obiektów posiada wewnętrzna strukturę danych zarządzaną przez jądro. W przypadku kolejki wiadomości nazywa się ona msqid_ds (patrz linux/msg.h):
/* jedna struktura msqid na każdą wiadomość w systemie*/
struct msqid_ds
{
struct ipc_perm msg_perm;
struct msg *msg_first; /*pierwsza wiadom. w kolejce*/
struct msg *msg_last; /* ostatnia wiadomość */
time_t msg_stime; /* czas ostat. wysłania msgsnd */
time_t msg_rtime; /* czas ostatniego otrzym. msgrcv */
time_t msg_ctime; /* czas ostatniej zmiany */
struct wait_queue *wwait;
struct wait_queue *rwait;
ushort msg_cbytes;
ushort msg_qnum;
ushort msg_qbytes; /* max liczba bajtów w kolejce */
ushort msg_lspid; /* pid ostatniej msgsnd */
ushort msg_lrpid; /* pid ostatnio odebranej msg */
};
gdzie:
msg perm - egzemplarz struktury ipc_perm, zdefiniowanej w linux/ipc.h; przechowuje informację o uprawnieniach do kolejki, włączając prawa dostępu oraz twórcy kolejki (uid, etc.),
msg_first - wskaźnik na pierwszą wiadomość w kolejce (głowa listy),
msg_last - wskaźnik na ostatnią wiadomość w kolejce (ogon listy),
msg_stime - czas ostatnio umieszczonej wiadomości w kolejce,
msg_rtime - czas ostatnio pobranej wiadomości z kolejki,
wwait i rwait - wskaźniki do systemowej kolejki wait (oczekiwania); są one wykorzystywane wtedy, gdy operacja wykonywana na kolejce wiadomości uważa, że proces powinien przejść w statn uśpienia (tj. kolejka jest pełna i proces oczekuje na otwarcie),
msg_cbytes - całkowita liczba bajtów w kolejce,
msg_qnum - liczba wiadomości w kolejce,
msg_qbytws - maksymalna liczba bajtów w kolejce,
msg_lspid - PID procesu, który ostatnio przesłał wiadomość do kolejki,
msg_lrpid - PID procesu, który ostatnio odebrał wiadomość z kolejki
Struktura jądra ipc_perm. W tej strukturze jądro pzrechowuje informacje o uprawnieniach obiektów IPC (patrz linux/msg.h):
struct ipc_perm
{
key_t key; /* klucz */
ushort uid; /* euid oraz egid właściciela */
ushort gid;
ushort cuid; /* euid oraz egid twórcy */
ushort cgid;
ushort mode; /* tryby dostępu */
ushort seq; /* numer kolejny */
};
Wywołanie systemowe msgget(). Funkcji msgget() używa się albo do utworzenia nowej kolejki albo dostępu do kolejki istniejącej.
Wywołąnie systemowe: msgget();
Prototyp: int msgget ( key_t key, int msgflg );
RETURNS: message queue identifier on success,
-1 on error: errno = EACCESS (permission denied)
EEXIST (Queue exists, cannot create)
EIDRM (Queue is marked for deletion)
ENOENT (Queue does not exist)
ENOMEM (Not enough memory to create queue)
ENOSPC (Maximum queue limit exceeded)
Pierwszy argument msgget() jest wartościa klucza (zwracaną przez ftok()). Wartość tego klucza jest następnie porównywana z istniejącymi wartościami klucza, które znajdują się w wewnątrz jądra dla innych kolejek wiadomości. Wynik operacji otwarcia lub dostępu do kolejki zależy od zawartości argumentu msgflag, którego 9 najmniej znaczących bitów określa tryb dostępu do kanału komunikacji między procesowej (w tym przypadku do kolejki komunikatów):
IPC_CREAT - tworzy kolejkę, jeśli nie istnieje w jądrze; jeśłi kolejak istnieje zwróci jej identyfikator
IPC_EXCL - jeśli użyta razem z IPC_CREAT zwraca błąd w przypadku, gdy kolejka już istnieje (użycie samego IPC_EXCL nie wywołuje żadnych działań); jeśli kolejka nie istnieje - zostanie utworzona.
Zawsze wtedy, gdy tworzony jest nowy kanał komunikacyjny, wywołując funkcję systemową msgget (także semget lub shget - patrz dalej) z ustawionym znacznikiem IPC_CREAT, wtedy 9 najmniej znaczących bitów argumentu msgflag inicjuje słowo trybu dostępu, czyli pole mode w strukturze ipc_perm. Ponadto w polach cuid i cgid tej są umieszczane odpowiednio obowiązujące identyfikatory użytkownika i grupy dla procesu wywołującego funkcję. Podobnie jest w przypadku w pól uid i gid struktury ipc_perm.
Te dwa ostatnie identyfikatory nazywane są identyfikatorami właściciela kanału, zaś dwa pierwsze - identyfikatorami twórcy kanału. Identyfikatory twórcy nigdy nie zmieniaja się, podczas gdy proces może zmienić ID właściciela, wywołując funkcje systemową (w przypadku kolejek wiadomości jest to funkcja msgctl()).
Przykład funkcji otwierającej lub tworzącej kolejkę wiadomości (zwróć uwagę na jawnie określone prawa dostępu 0660:
int open_queue(key_t keyval)
{
int qid;
if((qid = msgget( keyval, IPC_CREAT | 0660 )) == -1)
{
return(-1);
}
return(qid);
}
Wywołanie systemowe msgsnd(). Funkcji msgsnd() używa się do umieszczenia wiadomości w kolejce
Wywołanie systemowe: msgsnd();
Prototyp: int msgsnd ( int msqid,
struct msgbuf *msgp, int msgsz, int msgflg );
RETURNS: 0 on success
-1 on error: errno =
EAGAIN (queue is full, and IPC_NOWAIT was asserted)
EACCES (permission denied, no write permission)
EFAULT (msgp address isn't accessable - invalid)
EIDRM (The message queue has been removed)
EINTR (Received a signal while waiting to write)
EINVAL (Invalid message queue identifier, nonpositive
message type, or invalid message size)
ENOMEM (Not enough memory to copy message buffer)
Argument msqid) jest identyfikatorem kolejki, zwrócony przez msgget(), msgp - wskaźnik do redeklarowanego i załadowanego bufora wiadomości, msgsz - określa rozmiar wiadomości w bajtach, z pominięciem długości typu wiadomości (4 bajty). Argument msgflag ustawiany jest na 0 (ignorowany) lub:
IPC_NOWAIT - jeśli kolejka jest pełna, wtedy wiadomość nie jest zapisywana do kolejki i sterowanie zwracane jest procesowi wołającemu; jeśli parametr ten nie występuje, wtedy proces wołający zawisa (jest blkowany) dotąd, aż będzie mógł pisać.
Przykład funkcji wysyłającej wiadomość:
int send_message( int qid, struct mymsgbuf *qbuf )
{
int result, length;
/*Długość jest rozmiarem struktury minus sizeof(mtype)
length = sizeof(struct mymsgbuf) - sizeof(long);
if((result = msgsnd( qid, qbuf, length, 0)) == -1)
{
return(-1);
}
return(result);
}
Przykład użycia funkcji obsługi kolejki wiadomości:
#include <stdio.h>
#include <stdlib.h>
#include <linux/ipc.h>
#include <linux/msg.h>
main()
{
int qid;
key_t msgkey;
struct mymsgbuf
{
long mtype; /* typ wiadomości */
int request; /* roboczy numer żądania */
double salary; /* pensja pracownika */
} msg;
/* Utwórz wartość klucza IPC */
msgkey = ftok(".", 'm');
/* Open/create kolejkę */
if(( qid = open_queue( msgkey)) == -1)
{
perror("open_queue");
exit(1);
}
/* Wypełnij wiadomość dowolnymi danymi testowymi */
msg.mtype = 1; /* wiadomość musi mieć dodatni typ! */
msg.request = 1; /* #1 element danych */
msg.salary = 1000.00; /* #2 element danych */
/* Wyślij! */
if((send_message( qid, &msg )) == -1)
{
perror("send_message");
exit(1);
}
}
Po skompilowaniu programu uruchom go, a następnie użyj polecenie ipcs, aby obejrzeć status naszej kolejki.
Wywołanie systemowe msgrcv(). Funkcji msgrcv() używa się do odbioru wiadomości z kolejki.
Wywołanie systemowe: msgrcv();
Prototyp: int msgrcv ( int msqid, struct msgbuf
*msgp, int msgsz, long mtype, int mflag);
RETURNS: Number of bytes copied into message buffer
-1 on error: errno =
E2BIG (Message length is greater than msgsz)
EACCES (No read permission)
EFAULT (Address pointed to by msgp is invalid)
EIDRM (Queue was removed during retrieval)
EINTR (Interrupted by arriving signal)
EINVAL (msgqid invalid, or msgsz less than 0)
ENOMSG (IPC_NOWAIT asserted, and no message exists
in the queue to satisfy the request)
Argument msqid jest identyfikatorem kolejki, zwrócony przez msgget(), msgp - wskaźnik do bufora, w którym będzie przechowywana pobrana wiadomość, msgsz - określa rozmiar bufora wiadomości w bajtach, z pominięciem długości typu wiadomości (4 bajty), mtype - określa typ pobieranej z kolejki wiadomości (jądro poszukuje najstarszej wiadomości o wskazanym typie; jeśli przekazana wartość mtype jest równa 0, wówczas z kolejki pobierana jest najstarsza wiadomość niezależnie od jej typu).
Jeśli jako ostatni argument mflag przekazywana jest wartość IPC_NOWAIT i wiadomość jest niedostępna, wtedy zwracany jest błąd ENOMSG. W przeciwnym przypadku proces jest blokowany, aż do momentu otrzymania wiadomości. Jeśli podczas oczekiwania na wiadomość kolejka zostanie usunięta, wówczas zwracany jest błąd EIDRM.
Przykład funkcji pobierającej wiadomość:
int read_message( int qid, long type,
struct mymsgbuf *qbuf )
{
int result, length;
/*Długość jest rozmiarem struktury minus sizeof(mtype)
length = sizeof(struct mymsgbuf) - sizeof(long);
if((result = msgrcv(qid, qbuf, length, type, 0))== -1)
{
return(-1);
}
return(result);
}
Po pobraniu wiadomości z kolejki jest ona z niej fizycznie usuwana.
Ustawienie bitu MSG NOERROR w argumencie mtype oznacza, że jeśli porcja danych otrzymanych w komunikacie jest większa niż msgsz, to nadmiar jest pomijany; jeśli bit ten nie jest ustawiony, wówczas sygnalizowany jest błąd E2BIG. Ta cech umożliwia na napisanie funkcji, która powoli nam na zaglądanie do środka kolejki i sprawdzić, czy nadeszła już wiadomość, która nas interesuje:
int peek_message( int qid, long type )
{
int result, length;
if((result = msgrcv(qid,NULL,0,type,IPC_NOWAIT))== -1)
{
if(errno == E2BIG)
return(TRUE);
}
return(FALSE);
}
Wywołanie systemowe msgctl(). Funkcji msgctl() umożliwia sterowanie operacjami wykonywanymi na kolejce wiadomości.
Wywołanie systemowe: msgctl();
Prototyp: int msgctl (int msgqid, int cmd,
struct msqid_ds *buf);
RETURNS: 0 on success
-1 on error: errno =
EACCES (No read permission and cmd is IPC_STAT)
EFAULT (Address pointed to by buf is invalid
IPC_STAT commands)
EIDRM (Queue was removed during retrieval)
EINVAL (msgqid invalid, or msgsz less than 0)
EPERM (IPC_SET or IPC_RMID command was issued,
calling process does not have write (alter)
access to the queue)
Poza parametrem cmd pozostałe są takie samie, jak już omawiane.
Argument cmd umożliwia wykonywanie poleceń na kolejce wiadomości:
IPC_STAT - pobiera strukturę msqid_ds z kolejki i umieszcza ja pod adresem określonym przez buf.
IPC_SET - ustawia wartość ipc_perm w strukturze msqid_ds; wartość tą pobiera z buf.
IPC_RMID - usuwa kolejkę z jądra.
Używając IPC_STAT można odczytać egzemplarz struktury msqid_ds (taki egzemplarz istnieje dla każdej kolejki, która istnieje w systemie). Przykład:
int get_queue_ds( int qid, struct msgqid_ds *qbuf )
{
if( msgctl( qid, IPC_STAT, qbuf) == -1)
{
return(-1);
}
return(0);
}
Mając kopię wewnętrznej struktury msqid_ds., możemy zmodyfikować uprawnienia do niej:
int change_queue_mode( int qid, char *mode )
{
struct msqid_ds tmpbuf;
/* uzyskaj kopię wewnętrznej struktury danych */
get_queue_ds( qid, &tmpbuf);
/* zmień uprawnienia */
sscanf(mode, "%ho", &tmpbuf.msg_perm.mode);
/* uaktualnij structure wewnętrzną */
if( msgctl(qid, IPC_SET, &tmpbuf) == -1)
{
return(-1);
}
return(0);
}
Po każdym pomyślnym pobraniu wiadomości z kolejki, wiadomość jest z niej usuwana. Obiekty IPC pozostają jednak w systemie (także kolejki wiadomości) dopóki nie zostaną jawnie usunięte lub system zostanie zrestartowany. Kolejkę wiadomości jawnie usuwa się korzystając z flagi IPC_RMID:
int remove_queue( int qid )
{
if( msgctl( qid, IPC_RMID, 0) == -1)
return(-1);
return(0);
}
Przykład: interaktywny manipulator kolejką wiadomości
Przedstawiony poniżej kod interpretera pozwala na interakcyjne operacje na kolejkach wiadomości z poziomu shella. Zakładamy, że po kompilacji program ma nazwę msgtool.
Składnia poleceń:
Nadawanie wiadomości: msgtool s (type) "text"
Pobieranie wiadomości: msgtool r (type)
Zmiana uprawnień: msgtool m (mode)
Usuwanie kolejki: msgtool d
Przykłady wywołań:
msgtool s 1 test
msgtool s 5 test
msgtool s 1 "This is a test"
msgtool r 1
msgtool d
msgtool m 660
Kod programu:
#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#define MAX_SEND_SIZE 80
struct mymsgbuf {
long mtype;
char mtext[MAX_SEND_SIZE];
};
void send_message(int qid,
struct mymsgbuf *qbuf, long type, char *text);
void read_message(int qid,
struct mymsgbuf *qbuf, long type);
void remove_queue(int qid);
void change_queue_mode(int qid, char *mode);
void usage(void);
int main(int argc, char *argv[])
{
key_t key;
int msgqueue_id;
struct mymsgbuf qbuf;
if(argc == 1)
usage();
/* Utwórz unikalny klucz, wołając ftok() */
key = ftok(".", 'm');
/* Otwórz/utwórz kolejkę */
if((msgqueue_id = msgget(key, IPC_CREAT|0660)) == -1)
{
perror("msgget");
exit(1);
}
switch(tolower(argv[1][0]))
{
case 's':
send_message(msgqueue_id,
(struct mymsgbuf *)&qbuf, atol(argv[2]), argv[3]);
break;
case 'r':
read_message(msgqueue_id, &qbuf, atol(argv[2]));
break;
case 'd': remove_queue(msgqueue_id);
break;
case 'm': change_queue_mode(msgqueue_id, argv[2]);
break;
default: usage();
}
return(0);
}
void send_message(int qid, struct mymsgbuf *qbuf,
long type, char *text)
{
/* Wyślij wiadmość do kolejki */
printf("Wysyłanie wiadomości ...\n");
qbuf->mtype = type;
strcpy(qbuf->mtext, text);
if((msgsnd(qid, (struct msgbuf *)qbuf,
strlen(qbuf->mtext)+1, 0)) ==-1)
perror("msgsnd"); exit(1);
}
void read_message(int qid, struct mymsgbuf *qbuf,
long type)
{
/* Czytaj wiadomość z kolejki */
printf("Czytanie wiadomości ...\n");
qbuf->mtype = type;
msgrcv(qid,(struct msgbuf*)qbuf,MAX_SEND_SIZE,type,0);
printf("Typ: %ld Tekst: %s\n",qbuf->mtype, qbuf->mtext);
}
void remove_queue(int qid)
{
/* Usuń kolejke */
msgctl(qid, IPC_RMID, 0);
}
void change_queue_mode(int qid, char *mode)
{
struct msqid_ds myqueue_ds;
/* Pobierz aktualna informację */
msgctl(qid, IPC_STAT, &myqueue_ds);
/* Zmień i załaduj uprawnienia */
sscanf(mode, "%ho", &myqueue_ds.msg_perm.mode);
/* Uaktualnij uprawnienia */
msgctl(qid, IPC_SET, &myqueue_ds);
}
void usage(void)
{
fprintf(stderr, "msgtool - Narzędzie do `majstrownia'
przy kolejkach wiadomości\n");
fprintf(stderr, "\nSkładnia: msgtool (s)end <typ>
<tekst wiadomości>\n");
fprintf(stderr, " (r)ecv <typ>\n");
fprintf(stderr, " (d)elete\n");
frintf(stderr, " (m)ode <tryb oktalny>\n");
exit(1);
}
Semafory
Semafor będziemy rozważać jako zmienną całkowitą, będącą licznikiem zasobów. Wartość zmiennej w dowolnej chwili określa liczbę dostępnych egzemplarzy zasobów.
Ponieważ używamy semaforów do synchronizowania różnych procesów, dlatego bieżąca wartość semafora musi być pamiętana w jądrze systemu (patrz rys.7).
Rys.7 Wartość semafora pamiętana w jądrze systemu
Przyjrzyjmy się pokrótce wewnętrznym strukturom stosowanymi do opisu operacji na semaforach i zarządzanych przez jądro.
Struktura (jądra) semid_ds. Podobnie jak w przypadku kolejki wiadomości, jądro zarządza specjalna wewnętrzną strukturą oddzielna dla każdego zbioru semaforów (patrz plik linux/msg.h):
/* jedna struktura semid na każdy zbiór semaforów
w systemie*/
struct semid_ds
{
struct ipc_perm sem_perm; /* uprawnienia */
time_t sem_otime; /* czas ostatniej operacji na sem */
time_t sem_ctime; /* czas ostatniej zmiany */
struct sem *sem_base; /* wskażnik na pierwszy semafor
w tablicy */
struct wait_queue *eventn;
struct wait_queue *eventz;
struct sem_undo *undo; /* żądanie anulowania do tej
tej tablicy */
ushort sem_nsems; /* liczba semaforów w tablicy */
};
przy czym znaczenie niektórych pól jest nastepujące:
sem_perm - egzemplarz struktury ipc_perm, zdefiniowanej w linux/ipc.h; przechowuje informację o uprawnieniach do zbioru semaforów, włączając prawa dostępu oraz twórcy zbioru semaforów (uid, etc.),
sem_otime - czas ostatniej operacji semop() (patrz dalej),
sem_ctime - czas ostatniej zmiany w tej strukturze,
sem_base - wskażnik do pierwszego semafora w tablicy (patrz następna struktura),
sem_undo - liczba żądań undo w tej tablicy (patrz dalej),
sem_nsems - liczba semaforów w zbiorze semaforów (w tablicy).
Struktura (jądra) sem. W strukturze semid_ds.znajduje się odwołanie (wskaźnik) do tablicy semaforów. Każdy semafor opisany jest z kolei przy pomocy struktury sem (patrz plik linux/msg.h):
/* jedna struktura na każdy semafor w systemie*/
struct sem
{
short sempid; /* pid ostatniej operacji */
ushort semval; /* bieżąca wartość */
ushort semncnt; /* liczba procesów oczekujących na
zwiekszenie semval
ushort semzcnt; /* liczba procesów oczekujących
wartości semval = 0 */
};
gdzie:
sem_pid - PID procesu, który ostatnio dokonał operacji na semaforze,
sem_semval - aktualna wartość semafora,
sem_semncnt - liczba procesów oczekujących na zwolnienie zasobu,
sem_semzcnt - liczba procesów oczekujących na 100% wykorzystanie zasobu.
Wywołanie systemowe semget(). Funkcji semget() tworzy nowy zbiór semaforów lub udostęnia zbiór już istniejący.
Wywołanie systemowe: semget();
Prototyp: int semget ( key_t key, int nsems, int semflg );
RETURNS: semaphore set IPC identifier on success
-1 on error: errno =
EACCESS (permission denied)
EEXIST (set exists, cannot create (IPC_EXCL))
EIDRM (set is marked for deletion)
ENOENT (set does not exist, no IPC_CREAT was
ENOMEM (Not enough memory to create new set)
ENOSPC (Maximum set limit exceeded)
Pierwszy argument msgget() jest wartościa klucza (zwracaną przez ftok()). Wartość tego klucza jest następnie porównywana z istniejącymi wartościami klucza, które znajdują się w wewnątrz jądra dla innych zbiorów semaforów. Wynik operacji otwarcia lub dostępu do kolejki zależy od zawartości argumentu semflag, którego 9 najmniej znaczących bitów określa tryb dostępu do kanału komunikacji między procesowej (w tym przypadku do zbioru semaforów):
IPC_CREAT - tworzy zbiór semaforów, jeśli nie istnieje w jądrze; jeśli zbiór istnieje, to zwróci jej identyfikator
IPC_EXCL - jeśli użyta razem z IPC_CREAT zwraca błąd w przypadku, gdy kolejka już istnieje (użycie samego IPC_EXCL nie wywołuje żadnych działań); jeśli kolejka nie istnieje - zostanie utworzona.
Zawsze wtedy, gdy tworzony jest nowy zbiór semaforów, wywołując funkcję systemową semget (także msgget lub shget) z ustawionym znacznikiem IPC_CREAT, wtedy 9 najmniej znaczących bitów argumentu semflag inicjuje słowo trybu dostępu, czyli pole mode w strukturze ipc_perm. Ponadto w polach cuid i cgid tej są umieszczane odpowiednio obowiązujące identyfikatory użytkownika i grupy dla procesu wywołującego funkcję. Podobnie jest w przypadku w pól uid i gid struktury ipc_perm.
Przykład funkcji otwierającej lub tworzącej zbiór semaforów (zwróć uwagę na jawnie określone prawa dostępu 0660:
int open_semaphore_set( key_t keyval, int numsems )
{
int sid;
if ( ! numsems )
return(-1);
if((sid = semget(mykey,numsems,IPC_CREAT|0660))==-1)
{
return(-1);
}
return(sid);
}
Wywołanie systemowe semoop(). Funkcji semop() umożliwia wykonywanie operacji na zbiorze semaforów:
Wywołanie systemowe: semop();
Prototyp: int semop ( int semid, struct sembuf *sops, unsigned nsops);
RETURNS: 0 on success (all operations performed)
-1 on error: errno =
E2BIG (nsops greater than max number of ops allowed
EACCESS (permission denied)
EAGAIN (IPC_NOWAIT asserted, operation could not
EFAULT (invalid address pointed to by sops argument)
EIDRM (semaphore set was removed)
EINTR (Signal received while sleeping)
EINVAL (set doesn't exist, or semid is invalid)
ENOMEM (SEM_UNDO asserted, not enough memory to
undo structure necessary)
ERANGE (semaphore value out of range)
Argument semid jest identyfikatorem zbioru semaforów, zwrócony przez semget(), zaś nops określa liczbę elementów tablicy struktur sembuf, na którą wskazuje argument sops.
Struktura sembuf jest zadeklarowana w pliku linux/semh i ma postać:
struct sembuf
{
ushort sem_num; /* indeks semafora w tablicy */
short sem_op; /* operacja semaforowa */
short sem_flg; /* znaczniki operacyjny */
};
gdzie:
sem_num - numer semafora, na którym chcemy wykonać operację,
sem_op - operacja na semaforze (dodatnia, ujemna lub zero),
sem_flg - znaczniki operacyjne.
Jeśli sem_op jest ujemna, wtedy proces wywołujący funkcję semop() chce czekać, aż wartość semafora stanie się większa niż (lub taka sama jak) wartość bezwzględna tego pola. Następnie wartość bezwzględną tego opla odejmuje się od bieżącej wartości semafora (odpowiada to przydzieleniu zasobu).
Jeśli sem_op jest dodatnia, to będzie ona dodana do bieżącej wartości semafora. Odpowiada to operacji uwolnienia zasobów, chronionych przez semafor.
Jeśli sem_op jest zerem, wtedy proces wywołujący funkcję semop() chce czekać, aż wartością semafora stanie się zero.
Funkcja semop() może używać różnych znaczników, operacji (patrz struktura sembuf). Jeśli np. znacznikowi temu przypiszemy wartość IPC_NOWAIT, wtedy oznacza to, iż nie chcemy, aby proces czekał na zakończenie operacji.
Przykład. Załóżmy, że dana jest jedna drukarka i wiele procesów. Semafor powinien zapewnić wzajemne wykluczanie w realizacji dostępu do drukarki.. Strukturę sembuf należy zainicjować następująco:
struct sembuf sem_lock = { 0, -1, IPC_NOWAIT };
Zapis ten oznacza, że wartość “-1” zostanie dodana do semafora numer 0. Użycie IPC_NOWAIT oznacza powrót z błdem, jeśli już jakiś proces korzysta z drukarki. Fragment kodu, który korzytsa z tej informacji ma postać:
if((semop(sid, &sem_lock, 1) == -1)
perror("semop");
Jeśli proces zakończy korzystanie z drukarki, powinien wykonać operację odrotną:
struct sembuf sem_unlock = { 0, 1, IPC_NOWAIT };
if((semop(sid, &sem_lock, 1) == -1)
perror("semop");
Wywołanie systemowe semctl(). Funkcji msgctl() umożliwia sterowanie operacjami wykonywanymi na kolejce wiadomości.
Wywołanie systemowe: semctl();
Prototyp: int semctl (int semid, int semnum, int cmd, union semun arg );
RETURNS: positive integer on success
-1 on error: errno =
EACCESS (permission denied)
EFAULT (invalid address pointed to by arg argument)
EIDRM (semaphore set was removed)
EINVAL (set doesn't exist, or semid is invalid)
EPERM (EUID has no privileges for cmd in arg)
ERANGE (semaphore value out of range)
NOTES: Performs control operations on a semaphore set
Argument semid jest identyfikatorem zbioru semaforów, zwrócony przez semget(), zaś semnum numerem semafora, na którym chcemy wykonać operację.
Argument cmd umożliwia wykonywanie poleceń na zbiorze semaforów:
IPC_STAT - pobiera strukturę semid_ds z kolejki i umieszcza ja pod adresem bufora buf określonego w unii semun.
IPC_SET - ustawia wartość ipc_perm w strukturze semid_ds; wartość tą pobiera z bufora buf określonego w unii semun.
IPC_RMID - usuwa kolejkę z jądra,
GETVAL - pobiera wartość semafora,
GETALL - pobiera wartości wszystkich semaforów w zbiorze,
SETVAL - nadaje semaforowi wartość określona przez pole val unii,
SETALL - nadaje wartości wszystkim semaforom w zbiorze, określonym przez pole val unii.
Argument arg jest zmienną typu semun, określoną w pliku linux/sem.h i ma postać:
union semun
{
int val; /* wartość dla SETVAL */
struct semid_ds *buf; /* bufor dla IPC_STAT&IPC_SET */
ushort *array; /* tablica dla GETALL & SETALL */
struct seminfo *__buf; /* bufor dla IPC_INFO */
void *__pad;
};
gdzie:
val - używana wtedy, gdy wykonane jest polecenie SETVAL. Określa wartość przypisywaną semaforowi.
buf - stosowany w przypadku polecenia IPC_STAT/IPC_SET. Reprezentuje kopię wewnętrznej struktury danych semafora używanej przez jądro.
array - wskażnik stosowany w przypadku polecenia GETALL/SETALL. Powinna wskazywać na tablicę wartości całkowitych, które mają być ustawione lub pobrane.
Używając GETVAL można odczytać wartość semafora. Przykład:
int get_sem_val( int sid, int semnum )
{
return( semctl(sid, semnum, GETVAL, 0));
}
Przykład. Aby określić status np. pięciu drukarek można użyć następującego kodu:
#define MAX_PRINTERS 5
printer_usage()
{
int x;
for(x=0; x<MAX_PRINTERS; x++)
printf("Printer %d: %d\n\r", x, get_sem_val(sid,x));
}
Przykład. Aby zainicjować nową wartość semafora:
void init_semaphore( int sid, int semnum, int initval)
{
union semun semopts;
semopts.val = initval;
semctl( sid, semnum, SETVAL, semopts);
}
Przykład. Aby zmienić uprawnienia, można to zrobić np. tak:
void changemode(int sid, char *mode)
{
int rc;
struct semid_ds mysemds;
/* Pobierz bieżące wartości - wskaż najpierw na
lokalną kopię struktury wewnetrznej */
semopts.buf = &mysemds;
/* Spróbuj to zrobic jeszcze raz */
if((rc = semctl(sid, 0, IPC_STAT, semopts)) == -1)
{
perror("semctl");
exit(1);
}
printf("Poprzednie uprawnienia %o\n",
semopts.buf->sem_perm.mode);
/* Zmień uprawnienia do semafora */
sscanf(mode, "%o", &semopts.buf->sem_perm.mode);
/* Uaktualnij wewnetrzna strukturę */
semctl(sid, 0, IPC_SET, semopts);
printf("Uaktualnianie...\n");
}
Przykład: interaktywny manipulator semaforem
Przedstawiony poniżej kod interpretera pozwala na interakcyjne operacje na zbiorze semaforów. Zakładamy, że po kompilacji program ma nazwę semtool.
Składnia poleceń:
Utworzenie zbioru semaforów:
semtool c (liczba semaforów w zbiorze)
Zajmowanie semafora:
semtool l (numer zajmowanego semafora)
Zwalnianie semafora:
semtool u (numer zwalnianego semafora)
Zmiana uprawnień:
semtool m (tryb)
Usuwanie kolejki:
semtool d
Przykłady wywołań:
semtool c 5
semtool l
semtool u
semtool m 660
semtool d
Kod programu:
#include <stdio.h>
#include <ctype.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
/* Początkowe wartości wszystkich */
#define SEM_RESOURCE_MAX 1
void opensem(int *sid, key_t key);
void createsem(int *sid, key_t key, int members);
void locksem(int sid, int member);
void unlocksem(int sid, int member);
void removesem(int sid);
unsigned short get_member_count(int sid);
int getval(int sid, int member);
void dispval(int sid, int member);
void changemode(int sid, char *mode);
void usage(void);
int main(int argc, char *argv[])
{
key_t key;
int semset_id;
if(argc == 1) usage();
/* Utwórz unikalny klucz wywołując ftok() */
key = ftok(".", 's');
switch(tolower(argv[1][0]))
{
case 'c': if(argc != 3)
usage();
createsem(&semset_id, key, atoi(argv[2]));
break;
case 'l': if(argc != 3)
usage();
opensem(&semset_id, key);
locksem(semset_id, atoi(argv[2]));
break;
case 'u': if(argc != 3)
usage();
opensem(&semset_id, key);
unlocksem(semset_id, atoi(argv[2]));
break;
case 'd': opensem(&semset_id, key);
removesem(semset_id);
break;
case 'm': opensem(&semset_id, key);
changemode(semset_id, argv[2]);
break;
default: usage();
}
return(0);
}
void opensem(int *sid, key_t key)
{
/* Otwórz zbiór semaforów - nie twórz go! */
if((*sid = semget(key, 0, 0666)) == -1)
{
printf("Zbiór semaforów nie istnieje!\n");
exit(1);
}
}
void createsem(int *sid, key_t key, int members)
{
int cntr;
union semun semopts;
if(members > SEMMSL)
{
printf("Przekroczono max.liczbę semaforów w
zbiorze\n",SEMMSL);
exit(1);
}
printf("Próba utworzenia nowego zbioru semaforów o
%d elementach\n",members);
if((*sid=semget(key,members,
IPC_CREAT|IPC_EXCL|0666)) == -1)
{
fprintf(stderr, "Zbiór semaforów już istnieje!\n");
exit(1);
}
semopts.val = SEM_RESOURCE_MAX;
/* Inicjuj wszystkie elementy zbioru (można także
zrobić przy pomocy polecenia SETALL) */
for(cntr=0; cntr<members; cntr++)
semctl(*sid, cntr, SETVAL, semopts);
}
void locksem(int sid, int member)
{
struct sembuf sem_lock={ 0, -1, IPC_NOWAIT};
if( member<0 || member>(get_member_count(sid)-1))
{
fprintf(stderr, "element semafora %d spoza
zakrsu\n", member);
return;
}
/* Próba zajęcia zbioru semaforów */
if(!getval(sid, member))
{
fprintf(stderr, "Zasoby semafora wyczerpane (brak
możliwości zajęcia zasobu)!\n");
exit(1);
}
sem_lock.sem_num = member;
if((semop(sid, &sem_lock, 1)) == -1)
{
fprintf(stderr, "Błąd zajęcia\n"); exit(1);
}
else
printf("Zasoby semafora zmniejszone o jeden\n");
dispval(sid, member);
}
void unlocksem(int sid, int member)
{
struct sembuf sem_unlock={ member, 1, IPC_NOWAIT};
int semval;
if( member<0 || member>(get_member_count(sid)-1))
{
fprintf(stderr, "element semafora %d spoza
zakrsu\n", member);
return;
}
/* Czy zbiór semaforów jest zajety? */
semval = getval(sid, member);
if(semval == SEM_RESOURCE_MAX)
{
fprintf(stderr, "Semafor nie zajęty!\n");
exit(1);
}
sem_unlock.sem_num = member;
/* Próba zwolnienia zbioru semaforów */
if((semop(sid, &sem_unlock, 1)) == -1)
{
fprintf(stderr, "Błąd zwolnienia\n");
exit(1);
}
else
printf("Zasoby semafora zwiększone o jeden\n");
dispval(sid, member);
}
void removesem(int sid)
{
semctl(sid, 0, IPC_RMID, 0);
printf("Semafor usunięty\n");
}
unsigned short get_member_count(int sid)
{
union semun semopts;
struct semid_ds mysemds;
semopts.buf = &mysemds;
/* Zwróć luczbe elementów w zbiorze semaforów */
return(semopts.buf->sem_nsems);
}
int getval(int sid, int member)
{
int semval;
semval = semctl(sid, member, GETVAL, 0);
return(semval);
}
void changemode(int sid, char *mode)
{
int rc;
union semun semopts;
struct semid_ds mysemds;
/* Pobierz aktualną wartość struktury wewnętrznej */
semopts.buf = &mysemds;
rc = semctl(sid, 0, IPC_STAT, semopts);
if (rc == -1)
{
perror("semctl");
exit(1);
}
printf("Poprzednie uprawnienia %o\n",
semopts.buf->sem_perm.mode);
/* Zmień uprawnienia do semafora */
sscanf(mode, "%ho", &semopts.buf->sem_perm.mode);
/* Uaktualnij wewnętrzną strukturę danych */
semctl(sid, 0, IPC_SET, semopts);
printf("Uaktualnianie...\n");
}
void dispval(int sid, int member)
{
int semval;
semval = semctl(sid, member, GETVAL, 0);
printf("semval dla elementu %d is %d\n",
member, semval);
}
void usage(void)
{
fprintf(stderr, "semtool - Narzędzie do `majstrownia'
przy semaforach\n");
fprintf(stderr, "\nUżycie: semtool (c)reate
<semcount>\n");
fprintf(stderr, " (l)ock <sem #>\n");
fprintf(stderr, " (u)nlock <sem #>\n");
fprintf(stderr, " (d)elete\n");
fprintf(stderr, " (m)ode <mode>\n");
exit(1);
}
Przykład: semstat - program towarzyszący programowi semtool
Program semstat wyświetla wartości każdego z semaforów utworzonych przy pomocy semtool.
Kod programu:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int get_sem_count(int sid);
void show_sem_usage(int sid);
int get_sem_count(int sid);
void dispval(int sid);
int main(int argc, char *argv[])
{
key_t key;
int semset_id;
/* Utwórz unikalny klucz wywołując ftok() */
key = ftok(".", 's');
/* Otwórz zbiór semaforów - nie twórz go! */
if((semset_id = semget(key, 1, 0666)) == -1)
{
printf("Zbiór semaforów nie istnieje\n");
exit(1);
}
show_sem_usage(semset_id);
return(0);
}
void show_sem_usage(int sid)
{
int cntr=0, maxsems, semval;
maxsems = get_sem_count(sid);
while(cntr < maxsems)
{
semval = semctl(sid, cntr, GETVAL, 0);
printf("Semafor #%d: --> %d\n", cntr, semval);
cntr++;
}
}
int get_sem_count(int sid)
{
int rc;
struct semid_ds mysemds;
union semun semopts;
/* Pobierz aktualną wartość struktury wewnętrznej */
semopts.buf = &mysemds;
if((rc = semctl(sid, 0, IPC_STAT, semopts)) == -1)
{
perror("semctl");
exit(1);
}
/* Zwróć liczbę semaforów w zbiorze */
return(semopts.buf->sem_nsems);
}
void dispval(int sid)
{
int semval;
semval = semctl(sid, 0, GETVAL, 0);
printf("Wartość semval wynosi %d\n", semval);
}
Literatura:
Sven Goldt, Sven van der Meer, Scott Burkett, Matt Welsh The Linux Programmer's Guide, ©1995 by Sven Goldt
W.Richard Stevens Programowanie zastosowań sieciowych w systemie Unix, WNT, Warszawa 1995
Jerzy Pejaś: Systemy operacyjne - Linux
- 44 -