Programowanie procesów - komunikacja przez pamięć wspólną

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.

Specyfikacja i wymagania

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.

Zadania do wykonania

Zad.1. (2 punkty - na zajęciach)

Napisz program, który:

Zad.2. (1 punkt - na zajęciach)

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.

Zad.3. (2 punkty - na zajęciach, lub 1 punkt - w domu)

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.

Zad.4. (3 punkty - na zajęciach, lub 2 punkty - w domu)

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ł.

Zad.5. (4 punkty - w domu)

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 babblera 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.

Uwagi

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.

Dodatek - tworzenie i inicjalizacja segmentu pamięci współdzielonej

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);

Materiały źródłowe

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