L7 - Pamięć dzielona i mmap

Tutorial 7 - Pamięc dzielona i mmap #

Jest to pierwszy z serii tutoriali w “nowej formule”. Zawiera więcej wyjaśnień działania użytych funkcji oraz ich parametrów. Mamy nadzieję, że w ten sposób będzie wam łatwiej “wejść w temat”. Jest to jednak wciąż zbiór jedynie poglądowy najważniejszych informacji - należy koniecznie przeczytać wskazane strony manuala aby dobrze poznać i zrozumieć wszystkie szczegóły.

mmap #

Na poprzednich zajęciach omówiliśmy już kilka sposobów synchronizacji oraz współdzielenia danych między procesami. Wszystkie jednak wydają się być zawsze bardziej skomplikowane, niż jest to w przypadku wątków - gdzie możemy po prostu mieć współdzielone zmienne. Gdy jednak używamy funkcji fork() procesy potomne otrzymują własną kopią wszystkich danych procesu rodzica. Tak więc, jeśli np. w procesie dziecka zmodyfikujemy tablicę stworzoną przed wywołaniem fork(), zmiany te zajdą jedynie w obrębie procesu dziecka.

Możliwe jest jednak zadeklarowanie tak zwanego “mapowania” między pamięcią programu a tak zwanym “obiektem pamięci” (ang. “memory object”). Wszelkie zmiany zrobione w tak zmapowanym obszarze zostaną przeniesione na wspomniany “obiekt pamięci”, który jest zarządzany przez system operacyjny. Do stworzenia takiego mapowaniu służy polecenie mmap (man 3p mmap, man 2 mmap).

Funkcja ta ma z naszego punktu widzenia kilka głównych zastosowań:

  • Pozwala stworzyć mapowanie między fragmentem pamięci a plikiem - dzięki temu możemy w wygodny sposób modyfikować zawartość pliku, tak jak zwykłą tablicę.
  • Stworzyć obszar pamięci współdzielony z procesami potomnymi.
  • Zmapować obszar pamięci dzielonej (o tym w drugiej części tego tutoriala).

Przyjrzyjmy się sygnaturze i parametrom tej funkcji:

void *mmap(void *addr, size_t len, int prot, int flags,
           int fildes, off_t off);
  • addr - ten parametr pozwala poinformować system operacyjny, jaki obszar pamięci chcemy zmapować (po stronie systemu). W naszych zastosowaniach zwykle przekazujemy tutaj po prostu NULL żeby system sam o tym zdecydował.
  • len - oznacza oczywiście wielkość pamięci
  • prot - oznacza operacje (odczyt/zapis/wykonanie - patrz manual) jakie będziemy wykonywać na pamięci - trochę jak w przypadku plików.
  • flags - ten parametr determinuje rodzaj mapowania stworzonego przez mmap. Poniżej opisane zostały najważniejsze flagi, koniecznie przeczytaj man 2 mmap żeby zapoznać się z pełną listą oraz szczegółowymi informacjami.
    • MAP_ANONYMOUS - oznacza, że nie mapujemy pliku a tworzymy nowy, anonimowy obszar pamięci. W takiej sytuacji parametr filedes powinien być ustawiony na -1. W przypadku, gdy nie użyjemy tej flagi funkcja działa domyślnie w trybie mapowania pliku.
    • MAP_SHARED - oznacza, że mapowanie jest współdzielone z innymi procesami. W naszym przypadku prawie zawsze chcemy użyć tej flagi.
    • MAP_PRIVATE - przeciwieństwo powyższej flagi - mapowanie nie jest współdzielone, pamięć zostanie skopiowana w procesach potomnych. Mało przydatne do naszych zastosowań, ale również ważne. Czy nie zastanawiało cię kiedyś jak działa malloc?
  • filedes - deskryptor pliku, który chcemy zmapować.
  • off - offset w pliku, który chcemy zmapować. Przy MAP_ANONYMOUS powinien zawsze być ustawiony na 0.

To tak naprawdę tyle - wskaźnik na pamięć otrzymany z wywołania mmap z flagą MAP_SHARED będzie mógł być użyty w dowolnym procesie potomnym a zmiany będą widoczne we wszystkich pozostałych procesach. Ważnym zagadnieniem jest jednak zadbanie o synchronizację albo o to, żeby każdy proces działał tylko w ściśle wyznaczonym dla siebie obszarze wspólnej pamięci.

Z mmap wiążą się jeszcze dwie funkcje:

  • msync - przydatna przy mapowaniu plików, pozwala wymusić na systemie operacyjnym wykonanie modyfikacji mapowanego pliku. Ze względu na wydajność (modyfikacje plików na dysku są wolne) w momencie gdy zmodyfikujemy fragment pamięci zmapowanej do pliku, system od razu nie odzwierciedli tych zmian. Inaczej tworzyłoby to bardzo wiele bardzo małych modyfikacji, co bardzo spowolniłoby działanie systemu. Jeśli chcemy mieć pewność, że zmiany zostaną odwzorowane należy użyć funkcji msync. Można o tym myśleć, jako o funkcji analogicznej do fflush dla strumieni. Patrz man 3p msync.
  • munmap - zmapowaną pamięć trzeba kiedyś “odmapować” - służy do tego to polecenie - patrz man 3p munmap. Warto wspomnieć, że przy zmapowanych plikach munmap nie gwarantuje synchronizacji - konieczne jest wywołanie wcześniej msync.

Zadanie #

Napisz program obliczający liczbę PI metodą Monte Carlo. Program ma jeden argument - 0 < N < 30 - liczbę procesów obliczeniowych. Każdy proces obliczeniowy wykonuje 100 000 iteracji Monte Carlo.

Proces główny mapuje dwa obszary pamięci. Pierwszy obszar służy do współdzielenia wyników obliczeń procesów potomnych. Ma rozmiar N*4 bajtów. Każdy proces potomny zapisuje wynik swoich obliczeń do jednej 4-bajtowej komórki pamięci jako float. Drugi obszar pamięci jest mapowanie pliku log.txt. Proces główny ustawia rozmiar tego pliku na N*8. Następnie procesy dzieci zapisują tam swoje wyniki końcowe w postaci tekstowej - każdy w jendej linii o szerokości 7 (+ósmy znak to \n).

Rozwiązanie zadania #

Nowe strony z manuala:

man 3p mmap
man 2 mmap
man 3p munmap
man 3p msync
man 0p sys_mman.h
man 3p ftruncate

rozwiązanie l7-1.c:

#define _GNU_SOURCE

#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/wait.h>
#include <unistd.h>

#define MONTE_CARLO_ITERS 100000
#define LOG_LEN 8

#define ERR(source) \
    (fprintf(stderr, "%s:%d\n", __FILE__, __LINE__), perror(source), kill(0, SIGKILL), exit(EXIT_FAILURE))

void child_work(int n, float* out, char* log)
{
    int sample = 0;
    srand(getpid());
    int iters = MONTE_CARLO_ITERS;
    while (iters-- > 0)
    {
        double x = ((double)rand()) / RAND_MAX, y = ((double)rand()) / RAND_MAX;
        if (x * x + y * y <= 1.0)
            sample++;
    }
    out[n] = ((float)sample) / MONTE_CARLO_ITERS;
    char buf[LOG_LEN + 1];

    snprintf(buf, LOG_LEN + 1, "%7.5f\n", out[n] * 4.0f);
    memcpy(log + n * LOG_LEN, buf, LOG_LEN);
}

void parent_work(int n, float* data)
{
    pid_t pid;
    double sum = 0.0;
    for (;;)
    {
        pid = wait(NULL);
        if (pid <= 0)
        {
            if (errno == ECHILD)
                break;
            ERR("waitpid");
        }
    }
    for (int i = 0; i < n; i++)
        sum += data[i];
    sum = sum / n;

    printf("Pi is approximately %f\n", sum * 4);
}

void create_children(int n, float* data, char* log)
{
    while (n-- > 0)
    {
        switch (fork())
        {
            case 0:
                child_work(n, data, log);
                exit(EXIT_SUCCESS);
            case -1:
                perror("Fork:");
                exit(EXIT_FAILURE);
        }
    }
}

void usage(char* name)
{
    fprintf(stderr, "USAGE: %s n\n", name);
    fprintf(stderr, "10000 >= n > 0 - number of children\n");
    exit(EXIT_FAILURE);
}

int main(int argc, char** argv)
{
    int n;
    if (argc != 2)
        usage(argv[0]);
    n = atoi(argv[1]);
    if (n <= 0 || n > 30)
        usage(argv[0]);

    int log_fd;
    if ((log_fd = open("./log.txt", O_CREAT | O_RDWR | O_TRUNC, -1)) == -1)
        ERR("open");
    if (ftruncate(log_fd, n * LOG_LEN))
        ERR("ftruncate");
    char* log;
    if ((log = (char*)mmap(NULL, n * LOG_LEN, PROT_WRITE | PROT_READ, MAP_SHARED, log_fd, 0)) == MAP_FAILED)
        ERR("mmap");
    if (close(log_fd))
        ERR("close");
    float* data;
    if ((data = (float*)mmap(NULL, n * sizeof(float), PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0)) ==
        MAP_FAILED)
        ERR("mmap");

    create_children(n, data, log);
    parent_work(n, data);

    if (munmap(data, n * sizeof(float)))
        ERR("munmap");
    if (msync(log, n * LOG_LEN, MS_SYNC))
        ERR("msync");
    if (munmap(log, n * LOG_LEN))
        ERR("munmap");

    return EXIT_SUCCESS;
}

Uwagi i pytania #

  • Czemu w zadaniu nie występuje żadna synchronizacja?

    Odpowiedź:Każdy z procesów potomnych piszę do własnego wydzielonego fragmentu pamięci dzielonej a dopiero rodzic na końcu łączy wyniki. Z tego powodu nie występuje  żaden konflikt wymagający synchronizacji.

  • Do czego konieczne jest wywołanie ftruncate?

    Odpowiedź:Bez tego wywołania otwarty plik log.txt miałby rozmiar 0, ponieważ jest otwierany z flagą O_TRUNC, która powoduje skasowanie jego zawartości. Z kolei bez tej flagi rozmiar zależałby od poprzedniej zawartości pliku, byłoby więc to nieprzewidywalne.

Pamięć dzielona i robust mutex #

shm_* #

W przykładzie wyżej dzięki mmap tworzyliśmy obszar pamięci, który współdzieliła ze sobą grupa procesów stworzona przez fork(). Co jednak, gdy procesy są od siebie niezależne? W takiej sytuacji możemy stworzyć nazwany obiekt pamięci dzielonej, która każdy proces może zmapować po unikalnej nazwie. Jest więc to rzecz bardzo podobna i analogiczna w różnicach co pipe i FIFO omawiane na L5.

Do tworzenia nazwanego obiektu pamięci dzielonej służy funkcja shm_open (patrz man 3p shm_open). Jej użycie jest bardzo proste, a parametry analogiczne do zwykłego open albo mq_open, tak więc po prostu przeczytaj stronę manuala.

shm_open zwróci deskryptor, zachowujący się bardzo podobnie co deskryptor pliku. Na początku stworzona pamięć ma zerowy rozmiar, dlatego należy ją powiększyć używająć np. ftruncate. Następnie wystarczy użyć funkcji mmap.

Gdy wszystkie procesy skończą działać z pamięcią dzieloną należy ją usunąć funkcją shm_unlink, co również jest bardzo proste, więc po prostu sprawdź man 3p shm_unlink.

Współdzielenie obiektów synchronizacji i robust mutex #

Mając współdzieloną pamięć możemy synchronizować procesom dostęp do niej (albo wykonanie innych operacji) przy pomocy poznanych już pipe-ów, fifo i kolejek komunikatów. Możliwe jest jednak również umieszczenie semaforów oraz mutexów w pamięci dzielonej tak, aby wszystkie procesy miały do nich dostęp.

W przypadku semaforów sprawa jest prosta. Zobacz man 3p sem_init - jeśli drugi parametr, pshared, zostanie ustawiony na wartość inną niż zero, semafor może być dzielony między procesami. Tak więc, wystarczy wybrać pewien obszar pamięci dzielonej o rozmiarze sizeof(sem_t) i zainicjalizować tam semafor w ten sposób a wszystkie procesy będą mogły z niego korzystać.

Nieco bardziej skomplikowane jest współdzielenie mutexów. Ustawianie specjalnych właściwości mutexa również odbywa się przy jego inicjalizacji (pthread_mutex_init, ale patrz man 3p pthread_mutex_destroy) - jako drugi parametr możemy podać specjalne atrybuty - pthread_mutexattr_t. Tak więc, żeby mutex mógł być współdzielony między procesami należy stworzyć obiekt atrybutów mutexa (pthread_mutexattr_init) a następnie wywołać na tak zainicjalizowanych atrybutach pthread_mutexattr_setpshared (patrz man 3p pthread_mutexattr_getpshared). Następnie wystarczy przekazać te atrybuty do pthread_mutex_init.

Stworzony w ten sposób mutex będzie mógł być poprawnie współdzielony między procesami, to jednak nie koniec problemów. Wciąż bowiem obowiązuje nas podstawowe ograniczenie - mutex musi zostać odblokowany w tym samym wątku, który go zablokował. Stwarza to problem, gdy w wyniku błędu albo awarii proces zablokuje mutex i zakończy się przed jego odblokowaniem. Mutex znajdzie się wtedy w niepoprawnym stanie. Aby zaradzić takim sytuacjom należy stworzyć tzw. “niezawodny mutex” (ang. “robust mutex”).

Tworzenie “robust mutexa” polega na dodaniu mu odpowiedniego atrybytu przy użyciu pthread_mutexattr_setrobust (patrz man 3p pthread_mutexattr_getrobust) - podobnie jak to było ze współdzieleniem. Gdy spróbujemy zablokować mutex (pthread_mutex_lock), który jest w niepoprawnym stanie (został zablokowany przez inny proces, który zakończył się i go nie odblokował) otrzymamy błąd EOWNERDEAD (patrz man 3p pthread_mutex_lock) - ale mutex zostanie mimo wszystko zablokowany. Żeby naprawić stan mutexu należy dodatkowo wywołać funkcję pthread_mutex_consistent (man 3p pthread_mutex_consistent).

Podobnie jak mutex inicjalizowane są równie zmienne warunkowe i bariery. One również mogą być współdzielone między procesami, dzięki ustawieniu odpowiednich atrybutów. Zobacz man 3p pthread_condattr_getpshared i man 3p pthread_barrierattr_getpshared. W przypadku tych obiektów na szczęście nie występują dodatkowe problemy, które trzeba rozwiązać, tak jak przy mutexie.

Zobacz, jak współdzielona pamięć oraz robust mutex zostały wykorzystane w poniższym zadaniu.

Zadanie #

Napisz dwa programy - klienta i serwera. Proces serwera ma jeden parametr - 3 < N <= 20. Po uruchomieniu najpierw wypisuje komunikat My PID is: <pid>. Następnie tworzy segment pamięci dzielonej o nazwie <pid>-board i rozmiarze 1024 bajtów. W pamięci dzielonej umieszcza mutex, N oraz planszę - tablicę bajtów rozmiaru NxN wypełnioną losowymi liczbami z zakresu [1,9]. Następnie co trzy sekundy wypisuje stan planszy. Po otrzymaniu SIGINT wypisuje stan planszy po raz ostatni i się kończy.

Program klienta przyjmuje jeden parametr - PID serwera. Otwiera obszar pamięci stworzone przez serwer. Następnie wykonuje następującą procedurę:

  1. Blokuje mutex
  2. Losuje liczbę z zakresu [1,10]. W przypadku wylosowani 1 wypisuje Ops... i się kończy.
  3. Losuje dwie liczby x i y z zakresu od 0 do N-1 i wypisuje trying to search field (x,y)
  4. Sprawdza jaka liczba znajduje się na planszy na polu o współrzędnych (x,y)
  5. Jeśli nie jest to zero program dodaje tę liczbę do sumy swoich punktów, wypisuje found <P> points, zeruje to pole, odblokowywuje mutex, czeka sekundę i wraca do kroku 1.
  6. Jeśli jest to zero program odblokowywuje mutex, wypisuje GAME OVER: score <X> (gdzie X to zdobyta suma punktów) i się kończy.

Rozwiązanie zadania #

Nowe strony z manuala:

man 3p shm_open
man 3p shm_unlink
man 3p pthread_mutexattr_destroy
man 3p pthread_mutexattr_setpshared
man 3p pthread_mutexattr_setrobust
man 3p pthread_mutex_consistent

rozwiązanie l7-2_server.c:

#define _GNU_SOURCE

#include <errno.h>
#include <fcntl.h>
#include <pthread.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>

#define ERR(source) \
    (fprintf(stderr, "%s:%d\n", __FILE__, __LINE__), perror(source), kill(0, SIGKILL), exit(EXIT_FAILURE))

#define SHM_SIZE 1024

void usage(char* name)
{
    fprintf(stderr, "USAGE: %s N\n", name);
    fprintf(stderr, "3 < N <= 30 - board size\n");
    exit(EXIT_FAILURE);
}

typedef struct
{
    int running;
    pthread_mutex_t mutex;
    sigset_t old_mask, new_mask;
} sighandling_args_t;

void* sighandling(void* args)
{
    sighandling_args_t* sighandling_args = (sighandling_args_t*)args;
    int signo;
    if (sigwait(&sighandling_args->new_mask, &signo))
        ERR("sigwait failed.");
    if (signo != SIGINT)
    {
        ERR("unexpected signal");
    }

    pthread_mutex_lock(&sighandling_args->mutex);
    sighandling_args->running = 0;
    pthread_mutex_unlock(&sighandling_args->mutex);
    return NULL;
}

int main(int argc, char* argv[])
{
    if (argc != 2)
        usage(argv[0]);

    const int N = atoi(argv[1]);
    if (N < 3 || N >= 100)
        usage(argv[0]);

    const pid_t pid = getpid();
    srand(pid);

    printf("My PID is %d\n", pid);
    int shm_fd;
    char shm_name[32];
    sprintf(shm_name, "/%d-board", pid);

    if ((shm_fd = shm_open(shm_name, O_CREAT | O_EXCL | O_RDWR, 0666)) == -1)
        ERR("shm_open");
    if (ftruncate(shm_fd, SHM_SIZE) == -1)
        ERR("ftruncate");

    char* shm_ptr;
    if ((shm_ptr = (char*)mmap(NULL, SHM_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, shm_fd, 0)) == MAP_FAILED)
        ERR("mmap");

    pthread_mutex_t* mutex = (pthread_mutex_t*)shm_ptr;
    char* N_shared = (shm_ptr + sizeof(pthread_mutex_t));
    char* board = (shm_ptr + sizeof(pthread_mutex_t)) + 1;
    N_shared[0] = N;

    for (int i = 0; i < N; i++)
    {
        for (int j = 0; j < N; j++)
        {
            board[i * N + j] = 1 + rand() % 9;
        }
    }

    pthread_mutexattr_t mutex_attr;
    pthread_mutexattr_init(&mutex_attr);
    pthread_mutexattr_setpshared(&mutex_attr, PTHREAD_PROCESS_SHARED);
    pthread_mutexattr_setrobust(&mutex_attr, PTHREAD_MUTEX_ROBUST);
    pthread_mutex_init(mutex, &mutex_attr);

    sighandling_args_t sighandling_args = {1, PTHREAD_MUTEX_INITIALIZER};

    sigemptyset(&sighandling_args.new_mask);
    sigaddset(&sighandling_args.new_mask, SIGINT);
    if (pthread_sigmask(SIG_BLOCK, &sighandling_args.new_mask, &sighandling_args.old_mask))
        ERR("SIG_BLOCK error");

    pthread_t sighandling_thread;
    pthread_create(&sighandling_thread, NULL, sighandling, &sighandling_args);

    while (1)
    {
        pthread_mutex_lock(&sighandling_args.mutex);
        if (!sighandling_args.running)
            break;

        pthread_mutex_unlock(&sighandling_args.mutex);

        int error;
        if ((error = pthread_mutex_lock(mutex)) != 0)
        {
            if (error == EOWNERDEAD)
            {
                pthread_mutex_consistent(mutex);
            }
            else
            {
                ERR("pthread_mutex_lock");
            }
        }

        for (int i = 0; i < N; i++)
        {
            for (int j = 0; j < N; j++)
            {
                printf("%d", board[i * N + j]);
            }
            putchar('\n');
        }
        putchar('\n');
        pthread_mutex_unlock(mutex);
        struct timespec t = {3, 0};
        nanosleep(&t, &t);
    }

    pthread_join(sighandling_thread, NULL);

    pthread_mutexattr_destroy(&mutex_attr);
    pthread_mutex_destroy(mutex);

    munmap(shm_ptr, SHM_SIZE);
    shm_unlink(shm_name);

    return EXIT_SUCCESS;
}

rozwiązanie l7-2_client.c:

#define _GNU_SOURCE

#include <errno.h>
#include <fcntl.h>
#include <pthread.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>

#define ERR(source) \
    (fprintf(stderr, "%s:%d\n", __FILE__, __LINE__), perror(source), kill(0, SIGKILL), exit(EXIT_FAILURE))

#define SHM_SIZE 1024

void usage(char* name)
{
    fprintf(stderr, "USAGE: %s server_pid\n", name);
    exit(EXIT_FAILURE);
}

int main(int argc, char* argv[])
{
    if (argc != 2)
        usage(argv[0]);

    const int server_pid = atoi(argv[1]);
    if (server_pid == 0)
        usage(argv[0]);

    srand(getpid());

    int shm_fd;
    char shm_name[32];
    sprintf(shm_name, "/%d-board", server_pid);

    if ((shm_fd = shm_open(shm_name, O_RDWR, 0666)) == -1)
        ERR("shm_open");

    char* shm_ptr;
    if ((shm_ptr = (char*)mmap(NULL, SHM_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, shm_fd, 0)) == MAP_FAILED)
        ERR("mmap");

    pthread_mutex_t* mutex = (pthread_mutex_t*)shm_ptr;
    char* N_shared = (shm_ptr + sizeof(pthread_mutex_t));
    char* board = (shm_ptr + sizeof(pthread_mutex_t)) + 1;
    const int N = N_shared[0];

    int score = 0;
    while (1)
    {
        int error;
        if ((error = pthread_mutex_lock(mutex)) != 0)
        {
            if (error == EOWNERDEAD)
            {
                pthread_mutex_consistent(mutex);
            }
            else
            {
                ERR("pthread_mutex_lock");
            }
        }

        const int D = 1 + rand() % 9;
        if (D == 1)
        {
            printf("Ops...\n");
            exit(EXIT_SUCCESS);
        }

        int x = rand() % N, y = rand() % N;
        printf("trying to search field (%d,%d)\n", x, y);
        const int p = board[N * y + x];
        if (p == 0)
        {
            printf("GAME OVER: score %d\n", score);
            pthread_mutex_unlock(mutex);
            break;
        }
        else
        {
            printf("found %d points\n", p);
            score += p;
            board[N * y + x] = 0;
        }

        pthread_mutex_unlock(mutex);
        struct timespec t = {1, 0};
        nanosleep(&t, &t);
    }

    munmap(shm_ptr, SHM_SIZE);

    return EXIT_SUCCESS;
}

Uwagi i pytania #

  • Zwróć uwagę w jaki sposób w programie obsługiwane są sygnały. Dzięki użyciu dedykowanego wątku problematyczny kod do obsługi sygnałów jest ograniczony do jednej funkcji, nie ma zmiennych globalnych i nie musimy używać wszędzie TEMP_FAILURE_RETRY. Ma to znaczenie zwłaszcza w bardziej złożonych projektach.

  • Czy gdyby nie nasze losowe zamykanie procesu potomnego po wylosowaniu 1 robust mutex byłby potrzebny?

    Odpowiedź:Tak, proces i tak mógłby się zakończyć w tym miejscu, np. z powodu otrzymania sygnału SIGKILL. Generalnie przy współdzieleniu mutexu z innymi procesami, najlepiej żeby zawsze był to robust mutex.

  • Czy robust mutex sprawia, że nie może dojśc do deadlocka z powodu błędu w jednym z procesów?

    Odpowiedź:Niestety nie. Proces wciąż może np. nie zwalniać mutexa albo mieć inny błąd. Należy zwrócić szczególną uwagę na sytuację, w której proces robiłby unmap przed zwolnieniem mutexu. W takiej sytuacji mechanizm “robustness” nie zadziała!

  • Do czego konieczne jest wywołanie ftruncate?

    Odpowiedź:Stworzony obiekt pamięci dzielonej ma na początku rozmiar 0 i musi być powiększony.

Nazwany semafor #

Nazwane semafory były już wspomniane na SOP1, jednak warto je powtórzyć. Nazwany semafor ma się do zwykłego semafora podobnie jak fifo do pipe. Tworzymy go przy pomocy funkcji sem_open (patrz man 3p sem_open) przekazując flagę O_CREAT, uprawnienia oraz początkową wartość, np:

sem_t *semaphore = sem_open("SOP-semaphore", O_CREAT, 0666, 5);

utworzy semafor o nazwie SOP-semaphore i zainicjalizuje go wartością 5. Wykonanie potem tego samego wywołania (albo prostszego, bez O_CREATE: sem_open("SOP-semaphore", 0);) w innym procesie zwróci ten sam, już istniejący semafor. Jak widać jest to zupełnie analogiczne do fifo albo kolejek komunikatów.

Nazwane semafory dzięki swojej prostocie są wygodne w użyciu do zapewniania synchronizacji dla procesów korzystających z pamięci dzielonej. Gdy kilka niezależnych procesów zamierza korzystać z jednego obiektu pamięci dzielonej tylko jeden będzie ją inicjalizował. Istotne jest więc, żeby pozostałe procesy wtedy czekały, aż inicjalizacja zostanie zakończona - co można właśnie prosto zrealizować przy użyciu semafora.

Kody źródłowe z treści tutoriala #

Materiały dodatkowe #

Wykonaj przykładowe zadania. Podczas laboratorium będziesz miał więcej czasu oraz dostępny startowy kod, jeśli jednak wykonasz poniższe zadania w przewidzianym czasie, to znaczy że jesteś dobrze przygotowany do zajęć.

  • Zadanie 1 ~120 minut
  • Zadanie 2 ~120 minut
  • Zadanie 3 ~120 minut
  • Spróbuj rozwiązać przykładowe zadanie z L5 używając pamięci dzielonej. Zamiast wysyłania wiadomości o nowej rundzie synchronizacja może odbywać się przy pomocy semafora (rodzic wywołuje sem_post odpowiednią ilość razy na początku rundy). Przetestuj semafor współdzielony w pamięci dzielonej oraz nazwany semafor otwierany niezależnie przez procesy potomne. Zamiast wysyłania kart przez łącza gracze mogą umieszczać je w odpowiednim obszarze pamięci dzielonej. Do zasygnalizowania serwerowi, że gracze wystawili już swoje karty możesz użyć współdzielonej bariery albo ponownie semafora.
  • http://cs341.cs.illinois.edu/coursebook/Ipc#mmap