Celem zadania jest zbudowanie prostego komunikatora o nazwie babbler
pozwalającego komunikować się w ramach grupy laboratoryjnej przy użyciu
segmentu pamięci współdzielonej. Zadaniem jest napisanie zgodnie z
podanymi niżej wymaganiami własnej wersji komunikatora, który podłączy się
do istniejącego segmentu pamięci, wyświetli znajdujące się już tam
komunikaty, a następnie pozwoli użytkownikowi wysyłać własne komunikaty,
jednocześnie śledząc pojawiające się komunikaty innych użytkowników.
Pamięć wpólna wykorzystywana przez komunikator powinna mieć następującą strukturę:
#define BABBLE_NAME "/Y01-42e"
#define BABBLE_MODE 0777
#define BABBLE_LIMIT 10
#define BABBLE_LENGTH 80
struct babblespace {
pthread_mutex_t babble_mutex;
pthread_cond_t babble_cond;
int babble_first, babble_total;
char babbles[BABBLE_LIMIT][BABBLE_LENGTH];
};
BABBLE_NAME
jest nazwą tego segmentu (tu podana przykładowa nazwa dla
jednej z grup laboratoryjnych; inne grupy powinny wykorzystywać nazwy
segmentów podane przez prowadzącego grupę). Stałe BABBLE_LIMIT
i
BABBLE_LENGTH
oznaczają odpowiednio liczbę komunikatów w tablicy i
maksymalny rozmiar komunikatu uwzględniający również końcowy znak NUL (o
wartości 0) standardowo wykorzystywany w ANSI C do oznaczenia końca napisów
tekstowych.
Komunikaty zapisywane są w segmencie współdzielonym do tablicy babbles
o
organizacji bufora kołowego, to znaczy po zapełnieniu tablicy jest ona
nadpisywana od początku. W zmiennej babble_first
zapisany jest numer
(indeks) pierwszego chronologicznie komunikatu (w pustej tablicy będzie to
0), a babble_total
jest liczbą wszystkich komunikatów (też początkowo 0).
Po zapełnieniu tablicy indeks babble_first
zawija się do zera, natomiast
liczba komunikatów babble_total
pozostaje stała równa BABBLE_LIMIT
.
Napisz program, który:
shm_open
uzyska dostęp do istniejącego obszaru
pamięci wspólnej o nazwie podanej przez prowadzącego (domyślnie
będzie to nazwa grupy laboratoryjnej z JSOS zapisana dużymi literami
dodatkowo rozpoczynająca się slashem /
),mmap
wykona mapowanie otrzymanego obszaru pamięci
do przestrzeni adresowej procesu z rzutowaniem na wskaźnik do podanej
struktury; mapowanie powinno docelowo być wykonane w taki sposób, aby
zapisy do pamięci były widoczne w pliku,babbles
); w tablicy jest zawsze babble_total
komunikatów.Uzupełnij program o uzyskanie blokady muteksu zawartego we współdzielonym
obszarze pamięci przed rozpoczęciem wyświetlania komunikatów babbles
,
zwalniając mutex natychmiast po wyświetleniu wszystkich komunikatów.
Zmodyfikuj wyświetlanie komunikatów w ten sposób, aby były one wyświetlane
w kolejności chronologicznej z zachowaniem dyscypliny bufora kołowego, to
znaczy pierwszy powinien być wyświetlony komunikat babble_first
i
wyświetlonych było dokładnie babble_total
komunikatów.
Uzupełnij program o funkcję wpisania przez użytkownika jego własnego
komunikatu do tablicy babbles
na właściwej pozycji
((ptr->babble_first+ptr->babble_total) % BABBLE_LIMIT
), z właściwą
aktualizacją parametrów babble_first
i babble_total
(inną w przypadku
gdy tablica nie była jeszcze zapełniona i gdy już była).
Pobranie komunikatu od użytkownika może odbyć się w dowolny sposób, ale na
czas wpisywania już pobranego komunikatu do tablicy babbles
(i
aktualizacji jej parametrów) program musi ponownie uzyskać blokadę pamięci
wspólnej muteksem.
Program może pozwolić użytkownikowi na wpisanie pojedynczego komunikatu i wyjść, bądź pozwolić na wpisywanie dowolnej liczby komunikatów w pętli.
Uwaga: należy zadbać o to, by do tablicy babbles
wpisywać komunikaty o
długości nieprzekraczającej BABBLE_LENGTH
znaków, nawet jeśli użytkownik
wpisze komunikat dłuższy (w takim przypadku program może go skrócić -
łatwiejsza opcja, albo przenieść nadmiar do kilku kolejnych pozycji w
tablicy babbles
- trudniejsze, bez premii).
Najlepiej gdyby komunikaty wpisywane do tablicy babbles
były oznaczone
jakimś nickiem użytkownika (np. inicjałami), aby inni użytkownicy mogli
zorientować się kto co wpisał.
Uzupełnij program o funkcję interakcyjnego wpisywania komunikatów (jak w
punkcie Zad.4) z jednoczesnym śledzeniem komunikatów wpisanych przez innych
użytkowników. W tym celu utwórz drugi proces funkcją fork
i wykorzystaj
zmienną warunkową babble_cond
. Proces główny (rodzic) powinien
sygnalizować na tej zmiennej własne wpisywane komunikaty, oraz czekać na
zmiennej na sygnały o komunikatach wpisanych przez innych (drugi proces,
potomek). W oczywisty sposób, proces dostanie informacje tylko o
komunikatach wpisanych z wykorzystaniem sygnalizacji przez zmienną
warunkową. Jeśli program innego użytkownika wpisując swój komunikat tylko
zapewni blokadę muteksem, ale nie zasygnalizuje tego przez zmienną
warunkową, Twój program nie zauważy nowego komunikatu.
WAŻNE: ponieważ może istnieć wiele procesów oczekujących na sygnał zmiennej
warunkowej, i wszystkie one chcą przeczytać każdy nowo dodany komunikat,
proces dodający nowy komunikat babble
powinien wysłać sygnał funkcją
pthread_cond_broadcast
zamiast pthread_cond_signal
.
Najprostszym sposobem przetestowania własnego babbler
a będzie
uruchomienie dwóch instancji programu w dwóch różnych oknach, po czym
sprawdzenie czy każdy z nich prawidłowo zauważy i wyświetli komunikaty
wpisywane w tym drugim.
Uwaga 1: wywołania wszystkich funkcji systemowych powinny być obudowane
sprawdzaniem poprawności, czyli sprawdzaniem czy wartość zwrócona przez
funkcję świadczy o poprawnym zakończeniu czy błędzie. W przypadku błędu
wykonania którejkolwiek z funkcji najlepiej od razu zatrzymać program
(funkcją exit
) z wcześniejszym wyświetleniem komunikatu informującego o
miejscu i rodzaju błędu. Najłatwiej taki komunikat wyświetlić funkcją
perror
.
Ominięcie tego kroku (sprawdzanie poprawności + wyświetlanie komunikatu o błędach) prowadzi do bardzo frustrującego procesu uruchamiania i debuggowania programów, gdzie konsekwencje błędu wcześniejszego prowadzą do zatrzymania procesu przez system kilka kroków później, i poszukiwania błędu w zupełnie innym miejscu.
Uwaga 2: bardzo dobrą praktyką jest doprowadzenie programu do kompilacji
bez żadnych ostrzeżeń. Zalecane jest uruchamianie kompilatora gcc
z
opcjami -Wall -pedantic
a kompilatora cc
na Solarisie z opcją -Xc
.
Zwykle wyeliminowanie wszystkich ostrzeżeń zajmuje trochę więcej czasu,
zwłaszcza na początkowym etapie pracy nad programem, ale bardzo często
premią za to jest uniknięcie części błędów na dalszych etapach.
W przypadku gdyby ktoś chciał samodzielnie utworzyć i zainicjalizować
segment pamięci współdzielonej, należy wziąć pod uwagę, że mechanizmy
synchronizacji biblioteki Pthread
działają zasadniczo między wątkami.
Aby zapewnić ich poprawną pracę między procesami, potrzebna jest specjalna
inicjalizacja tych mechanizmów, przedstawiona poniżej.
shm_unlink(BABBLE_NAME);
umask(0);
int babble_fd = shm_open(BABBLE_NAME, O_RDWR|O_CREAT, BABBLE_MODE));
ftruncate(babble_fd, sizeof(struct babblespace));
struct babblespace *babble_ptr = (struct babblespace *)
mmap(NULL, sizeof(struct babblespace),
PROT_READ|PROT_WRITE, MAP_SHARED,
babble_fd, 0));
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_setpshared(&attr, PTHREAD_PROCESS_SHARED);
pthread_mutex_init(&babble_ptr->babble_mutex, &attr);
pthread_condattr_t attr2;
pthread_condattr_init(&attr2);
pthread_condattr_setpshared(&attr2, PTHREAD_PROCESS_SHARED);
pthread_cond_init(&babble_ptr->babble_cond, &attr2);
Poniższe prezentacje demonstrują implementację i posługiwanie się buforem kołowym. Proszę zwrócić uwagę, że w ogólnym przypadku bufor kołowy jest wykorzystywany zarówno do zapełniania go kolejnymi elementami, jak i opróżniania z elementów istniejących. Natomiast w tym zadaniu wykorzystanie bufora jest trochę inne, prostsze, ale i specyficzne. Kolejne elementy są dodawane, a w przypadku zapełnienia bufora nadpisują najstarsze elementu. Natomiast żaden element nie jest nigdy usuwany z bufora.
https://embedjournal.com/implementing-circular-buffer-embedded-c/
https://towardsdatascience.com/circular-queue-or-ring-buffer-92c7b0193326
https://medium.com/@charlesdobson/how-to-implement-a-simple-circular-buffer-in-c-34b7e945d30e