Wywołania systemowe Uniksa
W systemach uniksowych program jest całkowicie odizolowany od sprzętu, dlatego zawsze musi się odwoływać do odpowiednich funkcji jądra.
Z punktu widzenia programu odwołania te są ukryte w bibliotece libc – program nie wie, czy dana funkcja dostarczana jest bezpośrednio przez jądro, czy też implementuje ją libc korzystając z innych mechanizmów jądra (np. w GNU/Linuksie fork
zaimplementowany jest za pomocą clone
).
Na x86 i innych systemach o podobnej architekturze libc
(lub też czasem program bezpośrednio) komunikuje się z jądrem za pośrednictwem przerwań systemowych. W Linuksie funkcje systemowe są dostępne przez przerwanie 0x80
, argumenty są przekazywane w rejestrach w następującej kolejności: eax
, ebx
, ecx
, edx
, edi
, esi
, ebp
. Numer funkcji systemowej jest przekazywany w eax
, natomiast pozostałe argumenty zależą od rodzaju funkcji (nie wszystkie muszą być wykorzystane). Status operacji zwracany jest w rejestrze eax
. Gdy operacja wykona się bezbłędnie, jego wartość jest równa 0, w przeciwnym razie jest to (ujemna) stała z pliku asm/errno.h
[1]. Pozostałe rejestry nie są zmieniane.
W przypadku innych procesorów wywołania systemowe są wykonywane przez specjalizowane instrukcje procesora – np. Pentium 4 posiada instrukcję sysenter
(ang. system enter).
Śledzenie wywołań
[edytuj | edytuj kod]Wywołania systemowe można śledzić za pomocą programu truss
(większość uniksów) lub strace
(Linux).
Oto przykład działania strace
dla trywialnego programu true
:
execve("/bin/true", ["true"], [/* 35 vars */]) = 0 uname({sys="Linux", node="myhost", ...}) = 0 brk(0) = 0x804a308 access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory) open("/etc/ld.so.preload", O_RDONLY) = -1 ENOENT (No such file or directory) open("/home/taw/local/lib/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory) stat64("/home/taw/local/lib", {st_mode=S_IFDIR|0755, st_size=72, ...}) = 0 open("/etc/ld.so.cache", O_RDONLY) = 3 fstat64(3, {st_mode=S_IFREG|0644, st_size=85050, ...}) = 0 old_mmap(NULL, 85050, PROT_READ, MAP_PRIVATE, 3, 0) = 0x40012000 close(3) = 0 open("/lib/libc.so.6", O_RDONLY) = 3 read(3, "\\177ELF\\1\\1\\1\\0\\0\\0\\0\\0\\0\\0\\0\\0\\3\\0\\3\\0\\1\\0\\0\\0@Z\\1\\000"..., 1024) = 1024 fstat64(3, {st_mode=S_IFREG|0755, st_size=1109900, ...}) = 0 old_mmap(NULL, 1122692, PROT_READ|PROT_EXEC, MAP_PRIVATE, 3, 0) = 0x40027000 mprotect(0x40130000, 37252, PROT_NONE) = 0 old_mmap(0x40130000, 20480, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED, 3, 0x109000) = 0x40130000 old_mmap(0x40135000, 16772, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x40135000 close(3) = 0 old_mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x4013a000 munmap(0x40012000, 85050) = 0 brk(0) = 0x804a308 brk(0x804b308) = 0x804b308 brk(0x804c000) = 0x804c000 _exit(0)
Uwaga: szczegóły dotyczą jądra Linux 2.4, ale różnice nie są aż tak duże.
Otwieranie i zamykanie plików – open
, create
i close
[edytuj | edytuj kod]Pliki otwiera się za pomocą trzyargumentowego open
, którego definicja znajduje się w fcntl.h
[2]:
int open(const char *pathname, int flags, mode_t mode);
Pierwszy argument pathname
oznacza ścieżkę do pliku.
Drugi flags
– opcje otwarcia, z czego ważniejsze to:
O_RDONLY
– otwórz tylko do odczytuO_WRONLY
– otwórz tylko do zapisuO_RDWR
– otwórz do zapisu i odczytuO_CREAT
– utwórz plik jeśli nie istniejeO_EXCL
– używane razem zO_CREAT
– zwróć błąd jeśli plik już istniejeO_TRUNC
– jeśli w pliku są już jakieś dane, skasuj jeO_APPEND
– plik jest otwierany w trybie dopisywania do końcaO_NONBLOCK
– otwórz plik w trybie nieblokującym
Opcjonalny trzeci – uprawnienia dla nowo utworzonych plików.
open
należy do nielicznych wywołań systemowych dopuszczających pomijanie argumentu:
int open(const char *pathname, int flags);
Istnieje też specjalna postać open
:
int creat(const char *pathname, mode_t mode);
równoważna open(pathname, O_CREAT|O_WRONLY|O_TRUNC, mode)
W przypadku powodzenia open
i creat
zwracają numer otwartego deskryptora pliku.
W przypadku błędu zwracają -1
a errno
jest ustawiana na kod błędu.
int close(int fd);
zamyka otwarty deskryptor pliku. W dawnych czasach close
nie zwracało kodu błędu,
więc nikt go nie sprawdzał. Współcześnie zwraca kod błędu, co z punktu widzenia architektury systemu
jest kompletnym nieporozumieniem – nikt tak naprawdę nie zdefiniował co konkretnie ma znaczyć
błąd przy zamykaniu pliku i co program ma z tym zrobić.
Kernel może zwracać błędy EBADF
(deskryptor jest zły), EINTR
i dość ogólny EIO
(błąd wejścia/wyjścia).
Tworzenie plików specjalnych – mkdir
, mkfifo
, mknod
[edytuj | edytuj kod]open
i creat
potrafią tworzyć tylko zwykłe pliki.
Do tworzenia innych plików stworzono osobne wywołania systemowe.
Katalogi tworzy się za pomocą:
int mkdir(const char *pathname, mode_t mode);
gdzie pathname
i mode
mają znaczenie podobne jak w creat
.
Pliki urządzeń tworzy się za pomocą:
int mknod(const char *pathname, mode_t mode, dev_t dev);
gdzie pathname
i mode
mają to samo znaczenie a dev
to informacje o typie urządzenia.
Zakończenie pracy – _exit
[edytuj | edytuj kod]void _exit(int status);
służy do zakończenia pracy programu.
status
zostanie zwrócony jako kod wyjścia.
Zarządzanie pamięcią – brk
[edytuj | edytuj kod]int brk(void *end_data_segment);
Wywołanie brk
jest bardzo ważne i widać je często w wynikach strace
,
ale praktycznie nigdy nie jest używane bezpośrednio. brk
zmienia wielkość sterty programu.
Do alokacji pamięci (szczególnie pamięci dzielonej między procesami) można używać też mmap
i innych
wywołań systemowych.
Pisanie i czytanie
[edytuj | edytuj kod]Otwarty deskryptor plików służy głównie do zapisu i odczytu danych.
Podstawowe wywołania systemowe to read
i write
, jednak ze względu na kwestie wydajności
powstały też inne takie jak readv
, writev
i sendfile
.
Innym mechanizmem jest mmap
.
read
[edytuj | edytuj kod]read
jest zdefiniowany w unistd.h
[] jako:
ssize_t read(int fd, void *buf, size_t count);
Pierwszym argumentem jest otwarty deskryptor piku, drugim bufor, do którego mają się dostać zapisywane dane,
trzecim zaś liczba danych, którą co najwyżej chcemy odczytać. read
zwraca liczbę bajtów, która była
w rzeczywistości odczytana.
Liczba ta może być mniejsza od żądanej z wielu przyczyn – np. jeśli akurat w danej chwili ilość danych dostępnych na połączeniu sieciowym jest mniejsza od żądanej, lub też jeśli zanim odczytano wszystkie dane nastąpiło przerwanie.
I o ile wartości od 1
do count
są poprawne, wartość 0
może oznaczać tylko jedno – koniec pliku. Jeśli przerwanie nastąpiło przed odczytaniem danych, kernel zwraca kod błędu (errno) EINTR
,
jeśli zaś nie było aktualnie żadnych danych, a połączenie było otwarte w trybie nieblokującym – EAGAIN
.
Inne możliwe błędy to:
EBADF
– błędny deskryptorEINVAL
– deskryptor nie do odczytu (np. otwarty jako tylko do zapisu)EIO
– błąd wejścia wyjściaEISDIR
– deskryptor wskazuje na katalog. Na niektórych systemach katalogi można czytać za pomocąread
, jednak służyło to wyłącznie implementacji odpowiednich procedur libc. Na innych jest to niedozwolone, a libc radzi sobie w inny sposób.EFAULT
– błędny adres bufora, poza przestrzenią adresową procesu
write
[edytuj | edytuj kod]write
jest zdefiniowany w unistd.h
jako:
ssize_t write(int fd, const void *buf, size_t count);
Argumenty mają takie samo znaczenie jak w read
– write
pisze do deskryptora fd
co najwyżej count
bajtów z bufora buf
i zwraca liczbę zapisanych bajtów. W przypadku write
liczba 0
jest jednak równie poprawna jak pozostałe i można próbować dalej.
readv
i writev
[edytuj | edytuj kod]Ze względu na każdorazową zmianę kontekstu pracy procesora, wywołanie systemowe jest bardzo kosztowne - jeśli, co często ma miejsce, zapisywane dane składają się z dużej części stałej i małej zmiennej (np. zmienne nagłówki HTTP i plik zawarty w cache'u serwera), możliwe są dwie nieoptymalne strategie:
- wywołać
write
kilkakrotnie (przynajmniej dwa razy) - przepiąć dane tak, żeby były ciągłe w pamięci, po czym wywołać
write
tylko jeden raz
Nic nie stoi jednak na przeszkodzie, żeby kernel sam zajął się tą operacją – służą temu zdefiniowane w sys/uio.h
wywołania:
int readv(int filedes, const struct iovec *vector, size_t count);
int writev(int filedes, const struct iovec *vector, size_t count);
Pierwszy argument to tradycyjnie otwarty deskryptor pliku, drugi to wskaźnik na tablicę wektorów, trzeci zaś to ilość elementów tej tablicy. Element ma postać:
struct iovec {
void *iov_base;
size_t iov_len;
};
gdzie iov_base
to adres a iov_len
rozmiar bufora.
Procedura naszego serwera miałaby wówczas postać:
struct iovec io[2];
io[0].iov_base = http_headers;
io[0].iov_len = http_headers_size;
io[1].iov_base = file_headers;
io[1].iov_len = file_headers_size;
writev (fd, io, 2);
readv
i writev
pojawiły się po raz pierwszy w systemie 4.2BSD.
readv
nie jest aż tak istotne jak writev
.
sendfile
[edytuj | edytuj kod]Kolejny często występujący problem wydajności przedstawia następujący fragment kodu:
bytes_read = read (fd1, buf, buf_size);
write (fd2, buf, bytes_read);
Często występuje konieczność przerzucenia ogromnej ilości danych z jednego deskryptora do drugiego.
Jednym problemem jest podwojona liczba wywołań systemowych, ale jeszcze poważniejse jest zupełnie
bezużyteczne kopiowanie danych. Przeciwdziałać temu ma zdefiniowany w nagłówku sys/sendfile.h
:
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
out_fd
to deskryptor wyjściowy, in_fd
– wejściowy, offset
to wskaźnik na
zmienną przechowującą offset w pliku wejściowym, od którego ma zacząć dane wywołanie, a count
– ilość danych do przetransferowania. Wywołanie zwraca ilość rzeczywiście zapisanych danych i poprawia offset
na nową wartość.
sendfile
nie stosuje zwyczajnych metod przesuwania offsetu dla pliku wejściowego,
co umożliwia używanie jednego deskryptora do wielu takich operacji jednocześnie.
Np. serwer HTTP może wysyłać ten sam plik przez kilka połączeń naraz i dzięki temu rozwiązaniu
nie musi wielokrotnie duplikować deskryptora, a po ich rozłączeniu wielokrotnie go zamykać.
Offset pliku wyjściowego jest poprawiany normalnie – wysyłanie jednocześnie kilku plików na ten sam deskryptor nie miałoby większego sensu (sekwencyjnemu wysyłaniu oczywiście to nie przeszkadza).
sendfile
taki jak tu pokazany pojawił się w Linuksie 2.2, jednak wywołania o podobnym działaniu
istnieją również w innych systemach.
Przykład działania:
#include <sys/sendfile.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
int main(int argc, char **argv)
{
off_t ofs=0;
int in, out;
if (argc < 3)
{
fprintf (stderr, "Usage: %s <infile> <outfile>\n", argv[0]);
return 1;
}
in = open (argv[1], O_RDONLY);
if (in == -1)
{
fprintf (stderr, "Can't open %s\n", argv[1]);
return 1;
}
out = open (argv[2], O_WRONLY|O_CREAT, 0666);
if (out == -1)
{
fprintf (stderr, "Can't open %s\n", argv[2]);
return 1;
}
while (sendfile(out, in, &ofs, 64*1024*1024) > 0);
return 0;
}
Ponieważ dane nie wędrują przez pamięć procesu, można podawać „absurdalne” wartości typu (jak wyżej) 64 megabajty
i kernel dobrze sobie z nimi radzi. Powyższy program kopiuje plik 32219641-bajtowy (linux-2.4.19.tar.gz
) prawie dwukrotnie szybciej niż cat
(który robi to 4-kilobajtowymi odwołaniami read
i write
),
czy standardowy cp
(niektóre nowsze wersje używają mmap
lub sendfile
).
Ciekawa część wyników strace
to:
open("/home/username/linux-2.4.19.tar.gz", O_RDONLY) = 3 open("/home2/username/linux-kopia", O_WRONLY|O_CREAT, 0666) = 5 sendfile(5, 3, [0], 67108864) = 32219641 sendfile(5, 3, [32219641], 67108864) = 0 _exit(0) = ?
Sprawdzanie uprawnień
[edytuj | edytuj kod]Zdefiniowane w unistd.h
wywołanie:
int access(const char *pathname, int mode);
służy do sprawdzenia praw do pliku pathname
.
Tryb to maska złożona z:
R_OK
– plik można czytaćW_OK
– do pliku można pisaćX_OK
– plik jest wykonywalnyF_OK
– plik istnieje
Semantyka wywołania access
nie jest jednak prosta.
access
patrzy się jedynie na uprawnienia, nie na rzeczywiste możliwości, tak więc:
- jeśli system jest zamontowany read only,
access
pokażeW_OK
zależnie od uprawnień, choć nie można na nim pisać - znaczenie praw
R_OK
,W_OK
,X_OK
dla katalogów jest inne niż dla plików - prawa
X_OK
często mają pliki które nie nadają się do wykonywania, np. w systemach plików ISO 9660 czy FAT. - itd.
access
zwraca tylko prawa przysługujące uprawnieniom real, nie zaś effective – tak więc
ma pewne zastosowanie w programach używających praw setuid czy też setgid.
Naiwne stosowanie – sprawdzenie za pomocą wywołania access
, po czym otwarcie pliku za pomocą open
– stwarza jednak lukę czasową, w trakcie której plik może zostać podmieniony.
Przykład działania:
#include <unistd.h>
#include <stdio.h>
char *str[] = {
"but isn't readable, writable or executable",
"and is executable but is not readable or writable",
"and is writable but is not readable or executable",
"and is writable and executable but not readable",
"and is readable but is not writable or executable",
"and is readable and executable but not writable",
"and is readable and writable but not executable",
"and is readable, writable and executable"
};
int main(int argc, char *argv[])
{
int i=1;
int f,r,w,x;
for (;i<argc;++i)
{
f = !access (argv[i], F_OK);
if (!f) {
printf ("File %s doesn't exist\n", argv[i]);
continue;
}
r = !access (argv[i], R_OK);
w = !access (argv[i], W_OK);
x = !access (argv[i], X_OK);
printf ("File %s exists %s\n", argv[i], str[(r<<2)+(w<<1)+x]);
}
return 0;
}
Przechwytywanie sygnałów
[edytuj | edytuj kod]Kontroler sygnałów instaluje się za pomocą zdefiniowanej w signal.h
funkcji:
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
Na przykład jeśli nie chcemy pozwolić na Control-C (SIGINT, 2) w trakcie wpisywania danych, możemy przechwycić sygnał:
#include <stdio.h>
#include <signal.h>
void catchsig (int arg)
{
printf ("We've just got signal %i\n", arg);
}
int main ()
{
float f;
signal (SIGINT, catchsig);
scanf ("%f\n", &f);
return 0;
}