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 funkcjifopen, ale też precyzujące zachowanie w różnych warunkach brzegowych. Odnieś się doman 3p opendla listy flag. Ważne flagi na laboratorium to:- tryb, np.
O_RDONLY,O_WRONLY,O_RDWR O_CREAT- tworzy plik, jeżeli nie istniejeO_EXCL- używane zawsze zO_CREAT- tworzy plik jeśli nie istnieje, zwraca błąd gdy plik już istniejeO_APPEND- dopisywanie danych na koniec plikuO_TRUNC- przycięcie pliku do 0 przy otwarciuO_NONBLOCK- otwarcie w trybie nieblokującym -readiwritenie będą czekać, tylko od razu zwrócą tyle danych, ile akurat jest dostępne
- tryb, np.
...- 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 zopen,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 zopen,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_readibulk_write? Czy nie wystarczyłoby po prostu użyćreadiwriteOdpowiedź:
Zgodnie ze specyfikacją funkcjereadiwritemogą 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
openmożna zapisać dane przezfprintf?Odpowiedź:
Nie! Funkcjefprintf,fgets,fscanfitd. przyjmują jako argument zmienną typuFILE*, deskryptor jest natomiast pojedynczą liczbąintuż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 tablicyiov.
Funkcja ta zapisuje do filedes:iov[0].iov_len bajtów zaczynając od iov[0].iov_base, następnieiov[1].iov_len bajtów zaczynając od iov[1].iov_base, … aż doiov[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 strukturstruct iovecopisujących bufory do których funkcja rozprasza dane.iovcnt- rozmiar tablicyiov.
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 wszystkimPOLLINczyli oczekiwanie aż deksyptor będzie miał gotowe do odczytu dane, orazPOLLOUT- gdy deskryptor będzie gotowy do zapisu.short revents- od return events - w tej bitmascepollustawi flagi wskazujące, co było przyczyną jego zakończenia - może to być przede wszystkim to, co ustawiliśmy wevents(POLLINlubPOLLOUT), ale także wartości takie jakPOLLERR,POLLHUPlubPOLLNVALoznaczają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
Czemu otworzyliśmy pliki w trybie nieblokującym?
Czy program działałby z deskryptorami w trybie blokującym?Answer
Czy możemy użyć poll-a do czytania ze zwykłych plików?Answer
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.