L2 - Niskopoziomowy operacje na plikach i poll

Tutorial 1 - System plików #

Operacje niskopoziomowe na plikach #

Do realizacji odczytu i zapisu plików można użyć też funkcji niskopoziomowych, t.j. takich, których nie definiuje biblioteka standardowa C, a które udostępnia sam system operacyjny. Można przy ich pomocy np. wysyłać pakiety przez sieć, czym zajmiemy się w przyszłym semestrze.

Zamiast wskaźników na struktury FILE funkcje niskopoziomowe pracują na deskryptorach (ang. file descriptor - fd - wartościach całkowitoliczbowych identyfikujących zasoby w obrębie procesu. W tym przypadku będą to pliki, ale w ogólności mogą odnosić się do różnego typu zasobów systemowych. Aby użyć poniższych funkcji konieczne jest dołączenie nagłówków <fcntl.h> i <unistd.h>.

int open(const char *path, int oflag, ...);

Zobacz man 3p open.

  • path - ścieżka otwieranego pliku,
  • oflag - flagi otwarcia pliku połączone operacją bitowego OR |, analogiczne do trybu otwarcia funkcji fopen, ale też precyzujące zachowanie w różnych warunkach brzegowych. Odnieś się do man 3p open dla listy flag. Ważne flagi na laboratorium to:
    • tryb, np. O_RDONLY, O_WRONLY, O_RDWR
    • O_CREAT - tworzy plik, jeżeli nie istnieje
    • O_EXCL - używane zawsze z O_CREAT - tworzy plik jeśli nie istnieje, zwraca błąd gdy plik już istnieje
    • O_APPEND - dopisywanie danych na koniec pliku
    • O_TRUNC - przycięcie pliku do 0 przy otwarciu
    • O_NONBLOCK - otwarcie w trybie nieblokującym - read i write nie będą czekać, tylko od razu zwrócą tyle danych, ile akurat jest dostępne
  • ... - trzeci argument może być potrzebny gdy tworzymy plik - podajemy tam wtedy uprawnienia.

Funkcja zwraca deskryptor pliku fd, którego używać będziemy w kolejnych funkcjach. Deskryptor jest zasobem procesu, dlatego po użyciu należy go zwolnić funkcją close (man 3p close).

Funkcje niskopoziomowe nie korzystają z buforowania. read (man 3p read) dostaje znaki natychmiastowo kiedy są dostępne. Oznacza to, że nie zawsze wczyta tyle znaków ile oczekujemy. Z jednej strony dostajemy dane najszybciej jak się da nie musząc czekać aż system załaduje resztę z dysku. Z drugiej jednak musimy uważać żeby faktycznie wczytać całość danych które chcemy.

ssize_t read(int fildes, void *buf, size_t nbyte);
  • filedes - deskryptor pliku, pozyskany z open,
  • buf - wskaźnik na bufor w którym zapisane będą dane,
  • nbyte - rozmiar danych których ma oczekiwać funkcja.

Funkcja zwraca ilość bajtów które udało się wczytać.

W analogiczny sposób działa funkcja write (man 3p write):

ssize_t write(int fildes, const void *buf, size_t nbyte);
  • filedes - deskryptor pliku, pozyskany z open,
  • buf - wskaźnik na bufor z którego pobierane będą dane,
  • nbyte - rozmiar danych przeznaczonych do wysłania.

Zwrócona wartość oznacza ilość zapisanych bajtów.

Powyższe funkcje odczytują (albo zapisują) dane po kolei. Czasami jednak interesuje potrzebujemy dostępu do danych np. gdzieś w środku pliku. Moglibyśmy oczywiście przeczytać dostatecznie dużo bajtów, byłoby to jednak bardzo nieoptymalne. Funkcja lseek (man 3p lseek) pozwala na bezpośrednie poruszanie kursorem

off_t lseek(int fildes, off_t offset, int whence);

Jako parametr whence możemy podać SEEK_SET (ustawienie kursora na daną pozycję), SEEK_CUR (przesunięcie kursora względem aktualnej pozycji) albo SEEK_END (przesunięcie kursora za koniec pliku). Jako, że lseek zwraca pozycję kursora po wykonaniu operacji, wywołanie go z parametrami 0 i SEEK_CUR pozwala uzyskać aktualną pozycję w pliku.

Przeglądając manuale mogłeś zauważyć istnienie wielu odpowiedników już poznanych funkcji ale z końcówką -at -np. fstatat, openat, symlinkat. Zwykle te funkcją biorą dodatkowy parametr - deskryptor pliku, względem którego będą wyznaczane ścieżki względne. Są więc często miłym ułatwieniem dla programisty, pozwalając pominąć konstruowanie długich ścieżek względnych albo zmienianie katalogu roboczego.

Zadanie #

Napisz prosty program kopiujący pliki. Powinien akceptować jako swoje argumenty dwie ścieżki i skopiować plik z pierwszej na drugą.

Tym razem użyj funkcji niskopoziomowych.

Rozwiązanie zadania #

Co student musi wiedzieć:

  • man 3p open
  • man 3p close
  • man 3p read
  • man 3p write
  • man 3p mknod (tylko stałe opisujące uprawnienia do open)

kod do pliku l2-1.c

#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

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

#define FILE_BUF_LEN 256

void usage(const char *const pname)
{
    fprintf(stderr, "USAGE:%s path_1 path_2\n", pname);
    exit(EXIT_FAILURE);
}

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

    const char *const path_1 = argv[1];
    const char *const path_2 = argv[2];

    const int fd_1 = open(path_1, O_RDONLY);
    if (fd_1 == -1)
        ERR("open");

    const int fd_2 = open(path_2, O_WRONLY | O_CREAT, 0777);
    if (fd_2 == -1)
        ERR("open");

    char file_buf[FILE_BUF_LEN];
    for (;;)
    {
        const ssize_t read_size = read(fd_1, file_buf, FILE_BUF_LEN);
        if (read_size == -1)
            ERR("bulk_read");

        if (read_size == 0)
            break;

        if (write(fd_2, file_buf, read_size) == -1)
            ERR("bulk_write");
    }

    if (close(fd_2) == -1)
        ERR("close");

    if (close(fd_1) == -1)
        ERR("close");

    return EXIT_SUCCESS;
}

Uwagi i pytania #

  • Dlaczego w powyższym programie używane są funkcje bulk_read i bulk_write? Czy nie wystarczyłoby po prostu użyć read i write

    Odpowiedź:Zgodnie ze specyfikacją funkcje read i write mogą zwrócić zanim ilość danych której zażądał użytkownik zostanie odczytana/zapisana. Więcej o tym zachowaniu dowiesz się w tutorialu do kolejnego laboratorium. Teoretycznie w tym zadaniu nie ma to znaczenia (ponieważ nie używamy sygnałów), ale dobrze się do tego przyzwyczaić już teraz.

  • Czy powyższy program mógłby być zaimplementowany funkcjami bibliotecznymi z C zamiast niskopoziomowym IO? (fopen, fprintf, …)

    Odpowiedź:Tak, w tym programie nie ma niczego co nie pozwala użyć wcześniej pokazanych funkcji.

  • Czy do deskryptora zwróconego z open można zapisać dane przez fprintf?

    Odpowiedź:Nie! Funkcje fprintf, fgets, fscanf itd. przyjmują jako argument zmienną typu FILE*, deskryptor jest natomiast pojedynczą liczbą int używaną przez system operacyjny do identyfikacji otwartego pliku.

Operacje wektorowe na plikach #

Funkcja writev (man 3p writev) oferuje wygodne rozwiązanie w przypadku gdy dane które chcemy zapisać nie znajdują się w jednym ciągłym fragmencie pamięci. Pozwala ona zebrać dane z wielu miejsc i zapisać je w pliku za pomocą jednego wywołania funkcji. Znajduje się w nagłówku <sys/uio.h>.

ssize_t writev(int fildes, const struct iovec *iov, int iovcnt);
  • filedes - deskryptor do którego zapisywane są dane,
  • iov - tablica struktur opisujących bufory z których funkcja zbiera dane - struct iovec (man 0p sys_uio.h), które mają następujące pola:
void   *iov_base -> wskaźnik na obszar pamięci
size_t  iov_len  -> długość obszaru pamięci
  • iovcnt - rozmiar tablicy iov.

Funkcja ta zapisuje do filedes:
iov[0].iov_len bajtów zaczynając od iov[0].iov_base, następnie
iov[1].iov_len bajtów zaczynając od iov[1].iov_base, … aż do
iov[iovcnt-1].iov_len bajtów zaczynając od iov[iovcnt-1].iov_base.

W analogiczny sposób działa funkcja readv (man 3p readv):

ssize_t readv(int fildes, const struct iovec *iov, int iovcnt);
  • filedes - deskryptor z którego czytane są dane,
  • iov - tablica struktur struct iovec opisujących bufory do których funkcja rozprasza dane.
  • iovcnt - rozmiar tablicy iov.

W pozostałych aspektach funkcje te zachowują się jak ich niewektorowe odpowiedniki write i read.

Operacje blokujące, nieblokujące i fcntl #

Przy opisie funkcji open wspomniana była flaga O_NONBLOCK pozwalająca otworzyć plik w trybie nieblokującym. Domyślnie pliki (warto pamiętać, że pracujemy w systemach POSIX, gdzie ,,wszystko jest plikiem’’) otwierane są w trybie blokującym. Oznacza to, że wywołanie read albo write na takim deskryptorze będzie czekać tak długo, aż dane będą dostępne do odczytu/zapisu. Przykładem takiego zachowania jest terminal i funkcje takiej jak scanf czy getchar. Gdy je wywołamy wykonanie programu jest zatrzymywane tak długo, aż nie podamy jakiś danych na terminal. Gdybyśmy jednak przełączyli stdin w tryb nieblokujący takie funkcje kończyłyby się od razu - rozważ poniższy przykład:

#define _POSIX_C_SOURCE 200809L

#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

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

void set_nonblocking(int fd)
{
    int flags = fcntl(fd, F_GETFL);
    if (flags == -1)
        ERR("fcntl");
    flags |= O_NONBLOCK;
    if (fcntl(fd, F_SETFL, flags) == -1)
        ERR("fcntl");
}

int main(void)
{
    char buf[128];
    ssize_t n;

    puts("Testing blocking scanf - type something and press enter:");
    scanf("%100s", buf);
    printf("You typed: %s\n", buf);

    puts("Testing blocking read - type something and press enter:");
    n = read(STDIN_FILENO, buf, sizeof(buf) - 1);
    if (n < 0)
        ERR("read");
    buf[n] = 0;
    printf("Got %lu bytes: \"%s\"\n", n, buf);
    if (buf[n - 1] == '\n')
        puts("(your last character was \\n)");

    set_nonblocking(STDIN_FILENO);
    printf("Nonblocking read for ~10s, type something and press enter\n");

    for (int i = 0; i < 10; i++)
    {
        printf("Try %d/10...\n", i + 1);
        n = read(STDIN_FILENO, buf, sizeof(buf) - 1);
        if (n > 0)
        {
            buf[n] = '\0';
            printf("Got %zd bytes: %s", n, buf);
        }
        else if (n == 0)
        {
            printf("EOF\n");
            break;
        }
        else if (errno == EAGAIN || errno == EWOULDBLOCK)
        {
            printf("No data yet (EAGAIN)...\n");
        }
        else
        {
            ERR("read");
        }
        sleep(1);
    }
    return 0;
}

Skompiluj i przetestuj powyższy program. W pierwszej, blokującej części, zarówno scanf jak i read w nieskończoność czeka aż wpiszemy dane (i enter - domyślnie terminal nie wysyła danych dopóki nie zatwierdzi się ich enterem). Po zmianie deskryptora na nieblokujące read zaczyna zachowywać się inaczej - po prostu zwraca aktualnie dostępne dane, nawet jeżeli nic nie ma. Widać też tutaj dobrze, że nie zawsze zwrócenie przez funkcję błędu (-1 w tym wypadku) wymaga od razu zakończenia programu. W przypadku nieblokującego read jest to wręcz oczekiwane - sprawdzamy errno i dowiadujemy się, że przyczyną jest po prostu brak danych (EAGAIN, EWOULDBLOCK). Dopiero gdy błąd jest inny wiemy, że coś poszło nie tak.

Zwróć również uwagę na funkcję fcntl (man 3p fcntl). Pozwala ona sprawdzać oraz zmieniać właściwości deskryptora (flagi) po jego otwarciu.

int fcntl(int fildes, int cmd, ...);

Zarówno cmd jak i opcjonalny trzeci argument może przyjmować bardzo dużo parametrów. Dla na istotne są teraz komendy F_GETFD and F_SETFD. Podanie F_GETFD sprawia, że funkcja zwraca aktualnie ustawione flagi jako maskę bitową zapisaną w zmiennej typu int. Z kolei użycie F_SETFD wymaga podania maski definiującej flagi, jakie chcemy ustawić. Dlatego zwykle proces ustawiania flag deskryptora wygląda jak w funkcji set_nonblocking() w powyższym kodzie. Najpierw pobieramy aktualne flagi, ustawiamy te pola, które nas interesują, a następnie zapisujemy. Próba ustawienia flag poprzez wywołanie np. fcntl(fd, F_SETFL, O_NONBLOCK) jest błędem, ponieważ w ten sposób przy okazji resetujemy wszystko poza flagą O_NONBLOCK.

Czasami potrzebujemy żeby nasz plik, zachowywał się w różny sposób zależnie od kontekstu - np. chcemy raz czytać blokująca a raz nieblokująco. W takiej sytuacji zmiana flag co chwilę byłaby nieoptymalna. Zamiast tego możemy użyć funkcji dup() żeby zduplikować deskryptor. Oba deskryptory wskazują wtedy na ten sam plik, ale mogą mieć ustawione inne flagi.

Multipleksowanie IO #

Jak można zauważyć w przykładzie wyżej nieblokujące IO jest niewygodne w użyciu, jeżeli czekamy na pojawienie się danych. Musimy wtedy periodycznie sprawdzać czy dane są już dostępne - jest to tak zwany busy waiting albo busy polling. Jeżeli, czekając na dane, napiszemy pętle, która w kółko będzie sprawdzać czy dane są dostępne zajmiemy w pełni cały rdzeń procesora - proszę sobie wyobrazić, że każdy otwarty terminal działałby w taki sposób - komputer zupełnie nie nadawałby się do użytku. Z drugiej strony możemy wprowadzić przerwy w sprawdzaniu - tak jak w przykładzie powyżej funkcją sleep. Obciążenie systemu spada, ale w zamian za to rośnie opóźnienie w działaniu programu. Warto dodać, że na laboratorium używanie busy waitingu jest niedopuszczalne, chyba że zadanie wprost na to pozwala.

Jak widać blokujące IO wydaje się być idealnym rozwiązaniem powyższego problemu. Nasz program zostaje wstrzymany i nie zużywa zasobów, a gdy dane są dostępne i może działać dalej system sam go wybudza. Zauważmy jednak podstawowy problem - możemy w ten sposób obsługiwać tylko jeden deskryptor na raz. Dotąd nie stanowiło to problemu, bo znaliśmy tylko stdin (zaraz poznamy kilka innych przykładów), jednak możemy sobie wyobrazić, że mamy teraz kilka deskryptorów, które zachowują się jak stdin i na których czekamy na dane. Wydaje się, że jedynym rozwiązaniem jest używanie nieblokującego IO i robienie busy waitingu - ma to jednak duże wady, jak to zostało opisane wyżej.

Aby rozwiązać powyższe problemy i umożliwić responsywną obsługę wielu deskryptorów systemy operacyjne wprowadzają tzw. multipleksowanie IO (ang. IO multiplexing). Mamy kilka funkcji zapewniające ten mechanizm - select, poll (POSIX), epoll (linux). Poniżej opiszemy działanie funkcji poll. W materiałach dodatkowych możesz znaleźć opis działania funkcji epoll. Należy ona do rozszerzeń wprowadzonych przez linuxa (nie jest częścią standardu POSIX) i jest bardzo zoptymalizowana na tym systemie - nie będziemy jej jednak wymagać na laboratorium.

Poll #

Zacznijmy od przyjrzenia się sygnaturze funkcji poll (man 3p poll):

int poll(struct pollfd fds[], nfds_t nfds, int timeout);

fds to tablica struktur z desktryptorami, na które będziemy czekać - zaraz przyjrzymy się jej dokładniej. nfds to oczywiście rozmiar tej tablicy. poll zakończy się gdy któryś z dekskryptorów będzie gotowy do wykonania operacji albo osiągnięty zostanie timeout w milisekundach - możemy też podać tam wartość -1 żeby czekać w nieskończoność.

Struktura pollfd jest opisana w man 0p poll.h. Musi definiować przynajmniej następujące pola:

  • int fd - deskryptor na który będziemy czekać
  • short events - bitmaska w której zapisujemy wydarzenia (events) na które chcemy czekać. Dla na istotne jest przede wszystkim POLLIN czyli oczekiwanie aż deksyptor będzie miał gotowe do odczytu dane, oraz POLLOUT - gdy deskryptor będzie gotowy do zapisu.
  • short revents - od return events - w tej bitmasce poll ustawi flagi wskazujące, co było przyczyną jego zakończenia - może to być przede wszystkim to, co ustawiliśmy w events (POLLIN lub POLLOUT), ale także wartości takie jak POLLERR, POLLHUP lub POLLNVAL oznaczające, że wystąpił błąd lub problem z deskyptorem - szczegóły sprawdź w manualu.

Użycie funkcji poll polega więc na stworzeniu tablicy fds i wpisanie tam deskryptorów oraz wydarzeń na które będziemy czekać. Na przykład chcąc czekać aż jakieś użytkownik wpisze jakieś dane na standardowe wyjście ustawimy .fd na 1 lub STDIN_FILENO a .events na POLLIN. revents będzie ustawiany przez samego poll-a, dla bezpieczeństwa najlepiej go wyzerować.

W poniższym przykładzie próbujemy odczytań informacje z plików /dev/input/event* - w systemie linux odpowiadają one za przekazywanie danych z klawiatur, myszy i podobnych urządzeń wejściowych. Aktywacja urządzenia wejściowego, np. naciśnięcie klawisza na klawiaturze, powoduje ,,dopisanie’’ do tych plików nowej struktury input_event zdefiniowanej w nagłówku linux/input.h. Więcej informacji nt. działania podsystemu urządzeń wejściowych w linuxie można znaleźć pod adresem https://www.kernel.org/doc/html/latest/input/input.html. Nagłówek linux/input-event-codes.h definiuje wartości ze struktury input_event - np. klawisz F odpowiada wartości 33. Nie musisz się uczyć tych rzeczy na pamięć, nie będą one wymagane na laboratorium - podajemy ja jako ciekawostkę i interesujący przykład.

#define _POSIX_C_SOURCE 200809L

#include <ctype.h>
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <poll.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include <linux/input.h>

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

#define MAX_DEVS 32

int is_event_name(const char *name)
{
    if (strncmp(name, "event", 5) != 0)
        return 0;
    for (int i = 5; name[i] != '\0'; i++)
    {
        if (!isdigit(name[i]))
            return 0;
    }
    return 1;
}

int get_descriptors(struct pollfd fds[MAX_DEVS], char paths[MAX_DEVS][256])
{
    int n = 0;
    DIR *dir = opendir("/dev/input");
    if (!dir)
    {
        ERR("opendir");
    }

    struct dirent *de;
    while ((de = readdir(dir)) != NULL)
    {
        if (!is_event_name(de->d_name))
            continue;
        if (n >= MAX_DEVS)
            break;

        snprintf(paths[n], sizeof(paths[n]), "/dev/input/%s", de->d_name);

        int fd = open(paths[n], O_RDONLY | O_NONBLOCK);
        if (fd < 0)
            ERR("open");

        fds[n].fd = fd;
        fds[n].events = POLLIN;
        fds[n].revents = 0;
        n++;
    }
    closedir(dir);
    return n;
}

int main(void)
{
    struct pollfd fds[MAX_DEVS];
    char paths[MAX_DEVS][256];
    int n = get_descriptors(fds, paths);

    if (n == 0)
    {
        fprintf(stderr, "No readable event devices (check permissions)\n");
        return 1;
    }

    for (int i = 0; i < 30; i++)
    {
        int ret = poll(fds, n, -1);
        if (ret < 0)
            ERR("poll");

        for (int i = 0; i < n; i++)
        {
            if (fds[i].fd < 0)
                continue;

            if (fds[i].revents & (POLLERR | POLLHUP | POLLNVAL))
            {
                close(fds[i].fd);
                fds[i].fd = -1;
                continue;
            }

            if (!(fds[i].revents & POLLIN))
                continue;

            struct input_event ev;
            ssize_t r;
            while ((r = read(fds[i].fd, &ev, sizeof(ev))) == sizeof(ev))
            {
                printf("%s: %ld.%06ld type=%u code=%u value=%d\n", paths[i], (long)ev.input_event_sec,
                       (long)ev.input_event_usec, ev.type, ev.code, ev.value);
            }
        }
    }

    for (int i = 0; i < n; i++)
        if (fds[i].fd >= 0)
            close(fds[i].fd);

    return 0;
}

Po uruchomieniu za każdym razem jak naciśniesz klawisz na klawiaturze, poruszysz myszką etc. powyższy program będzie wypisywał przychodzące wydarzenia wejściowe. W przypadku gdy program nie zadziała z powodu niewystarczających uprawnień sprawdź, czy możesz czytać pliki w /dev/input - może być konieczne przynależenie do grupy input. W ostateczności można odpalić program z sudo.

Kiedy program się kończy?

Answer
Program kończy się po tym, jak `poll` zwróci 30 razy. Nie jest to równoważne z otrzymaniem 30 eventów. Za jednym razem możemy odczytać zdarzenie z kilku urządzeń, a każde urządzenie może mieć zakolejkowane kilka zdarzeń.

Czemu otworzyliśmy pliki w trybie nieblokującym? Czy program działałby z deskryptorami w trybie blokującym?

Answer
Należy uważać z używaniem blokującego IO i multipleksingu. Zauważmy, że `poll` informuje nas tylko, że jakieś dane są dostępne, jednak nie wiemy jak dużo. Przy nieblokującym deskryptorze możemy po prostu czytać do skutku - co w istocie robimy. Gdyby deskryptory były w trybie blokującym program nigdy nie wyszedłby z pętli w linii 98. Moglibyśmy skasować tę pętlę i czytać tylko jedną wiadomość na raz - gdyby było ich kilka, to po prostu `poll` nam od razu to zwróci i odczytamy kolejne. Należy jednak z tym uważać. W tym wypadku mamy gwarancję, że dane mają rozmiar struktury `input_event`. Gdybyśmy jednak tego nie wiedzieli musielibyśmy czytać po jednym bajcie. Jak widać dużo lepiej jest tutaj używać nieblokującego IO, blokujące nie ma żadnych zalet a stwarza ryzyko zablokowania programu.

Czy możemy użyć poll-a do czytania ze zwykłych plików?

Answer
Możemy ale nie ma to za dużo sensu. Zwykłe pliki są zawsze gotowe do odczytu, `poll` zawsze od razu to zwróci.

Powstaje pytanie, do jakich (sensownych) rodzajów plików możemy jeszcze użyć poll-a. Z poznanych dotąd (na SOPach oraz ,,Podstawach systemu UNIX’’) są to przede wszystkim urządzenia znakowe - należą do nich terminale (/dev/tty, ale również po prostu standardowe wejście i wyjście naszych programów), pokazane wyżej urządzenia /dev/input/* oraz wiele innych plików w /dev. Ponadto łącza (pipe/FIFO), gniazda sieciowe i wiele innych deskryptorów zwracanych przez różne funkcje systemowe (timerfd, signalfd, inotify…).

Timerfd (dodatkowy materiał) #

Prostym i bardzo przydatnym system na którym możemy testować funkcję poll jest timerfd. Należy on do linuxowych rozszerzeń (nie jest częścią standardu POSIX) dlatego nie będziemy go wymagać na laboratorium. Jest to jednak przydatny i ciekawy mechanizm, dlatego warto przeczytać resztę tego podrozdziału.

timerfd pozwala nam tworzyć zegary które następnie odczytujemy przez deskyptor. Pełen opis znajduje się w man 2 timerfd.

Aby stworzyć nowy timer używamy funkcji

int timerfd_create(int clockid, int flags);

clockid to rodzaj zegara systemowego, którego użyjemy - najprościej wybrać CLOCK_REALTIME, ale są też inne opcje, których opis znajdziesz w manualu. Z flag jakie można ustawić interesujące jest dla nas przede wszystkim TFD_NONBLOCK.

Gdy już nasz timer jest stworzony należy go ustawić poleceniem:

int timerfd_settime(int fd, int flags, const struct itimerspec *new_value,
                         struct itimerspec *_Nullable old_value);

Podajemy tu oczywiście nasz deskryptor uzyskany z timerfd_create. Przez new_value przekazujemy na jaki czas chcemy ustawić zegar. Struktura ma ta dwa pola (man 3type itimerspec). Ustawiając it_value wybieramy czas do pierwszego powiadomienia zegara. Ustawiając it_interval możemy dodatkowo ustawić powiadomienia okresowe. Oba pola są typu timespec (man 3type timespec) to znaczy że specyfikują sekundy i nanosekundy. Wiele przydatnych funkcji używa struktury timespec, dlatego warto się z nią zapoznać. Parametr old_value jest opcjonalny, możemy ustawic go na NULL - więcej informacji przeczytaj o nim w manualu.

#define _POSIX_C_SOURCE 200809L

#include <inttypes.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/timerfd.h>
#include <unistd.h>

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

int main(void)
{
    struct itimerspec new_value;
    memset(&new_value, 0, sizeof(new_value));
    new_value.it_value.tv_sec = 2;

    int fd = timerfd_create(CLOCK_REALTIME, 0);
    if (fd == -1)
        ERR("timerfd_create");

    printf("Preparing timer for waiting for 2 seconds\n");
    if (timerfd_settime(fd, 0, &new_value, NULL) == -1)
        ERR("timerfd_settime");

    uint64_t val = 0;
    if (read(fd, &val, sizeof(uint64_t)) < 0)
        ERR("read");
    printf("2 seconds passed, got %lu from timer, exiting now\n", val);

    return 0;
}

Działanie powyższego programu jest chyba jasne. Żeby potwierdzić, że timer działa dobrze możesz uruchomić program przez polecenie time, np. time ./l2-4.

Zauważ, że w tym wypadku nie ustawiliśmy naszego timera w trybie nieblokującym - celem tutaj był jak najprostszy przykład demonstrujący działanie tej funkcjonalności, więc używamy pojedynczego read. Jako ćwiczenie spróbuj zmodyfikować poprzedni program z poll-em w taki sposób, żeby program kończył się po określonym czasie (albo może po określonym czasie nieaktywności? spróbuj paru rzeczy!).

Mmap #

Jeszcze jeden, bardzo wygodny sposób na dostęp do pliku to ,,zmapowanie’’ go do pamięci. Służy do tego funkcja mmap. Jest ona szeroko używana również do wielu innych rzeczy, które omówimy w przyszłości. Na chwilę obecną skupimy się jedynie na mapowaniu plików. Wygląda to zwykle w następujący sposób:

#define _POSIX_C_SOURCE 200809L

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

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

void create_file()
{
    int fd = open("a.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
    if (fd == -1)
        ERR("open");
    char *data = "abcdefghijklmnopqrstuvwxyz\n";
    if (write(fd, data, strlen(data)) < 0)
        ERR("write");
    close(fd);
}

int main(void)
{
    create_file();

    int fd = open("a.txt", O_RDWR);
    if (fd == -1)
        ERR("open");

    struct stat sb;
    if (fstat(fd, &sb) == -1)
        ERR("fstat");
    size_t length = sb.st_size;
    char *buff = mmap(NULL, length, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (buff == MAP_FAILED)
        ERR("mmap");

    printf("a.txt[3] = %c (ASCII: %d)\n", buff[3], buff[3]);
    buff[3] = 'Z';
    msync(buff, length, MS_SYNC);

    munmap(buff, length);
    close(fd);
    return 0;
}

Najważniejszą funkcją jest tutaj oczywiście sam mmap:

char *buff = mmap(NULL, length, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);

W ten sposób prosimy system operacyjny o ,,zmapowanie’’ pliku o deskryptorze fd do pamięci o rozmiarze length tak, żebyśmy mogli go zarówno czytać jak i modyfikować (PROT_READ | PROT_WRITE). Wynikiem jest wskaźnik na zmapowaną pamięć lub wartość MAP_FAILED gdy wystąpi błąd. O pozostałych parametrach i zastosowaniach tej funkcji możesz poczytać w manualu (man 3p mmap, man 2 mmap dla linuxowych rozszerzeń), jednak nie będziemy ich wymagać. Na razie interesuje nas tylko, że, jak widać w powyższym kodzie, mmap pozwala nam bardzo wygodnie modyfikować pliki, tak samo jak zwykłe tablice. Dwie funkcje pomocnicze to msync i munmap. msync wymusza synchronizację zmian na dysk - system operacyjny nie robi tego zawsze od razu, ponieważ zapisy na dysk są wolne. munmap to likwidacja mapowania, czyli sprzątanie.

Ważną uwagą jest, że deskryptor musi mieć odpowiednie uprawnienia. Gdybyśmy do mmap podali PROT_READ | PROT_WRITE oraz deskryptor który otworzyliśmy np. z flagą O_RDONLY funkcja zwróciłaby błąd.

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