A rendszerhívás sikeres visszatérése nem feltétlenül jelenti a teljes
alkalmazásszintű művelet befejezését. Egy read() kevesebbet
olvashat a kértnél, egy write() csak a buffer elejét
továbbíthatja, a close() pedig akkor is jelezhet hibát,
amikor a descriptor már nem használható. A robusztus I/O ezért nem
egyetlen hívás, hanem dokumentált állapotátmenetek sorozata.
Az errno kezelésénél megismert szabály
változatlan: az errno értékét csak hibát jelző visszatérés
után értelmezzük, és cleanup előtt elmentjük. A bufferekhez a byte view és memóriatartományok szerződését
használjuk. A standard descriptorok kapcsolatát a programindulással a main, argumentumok és környezet tárgyalta.
POSIX: a file descriptor, röviden
FD, egy folyamat descriptortáblájának nemnegatív egész
indexe. Nem maga a fájl, és nem C-pointer. Az FD egy megnyitott
fájlleírásra (open file description) hivatkozik; ehhez
tartozhat például az aktuális fájlpozíció és az O_NONBLOCK
állapot.
Az FD-t tároló int objektum élettartama és az
OS-erőforrás élettartama különbözik. Az egész érték lemásolása nem hoz
létre új descriptort.
| Művelet | Tulajdonlási hatás | Érvénytelenítés, hibakimenet |
|---|---|---|
open() |
Siker esetén új FD tulajdonosa a hívó | Hiba: -1, nincs új erőforrás |
read(), write() |
Csak kölcsönzi az FD-t és a buffert | Nem zárja le az FD-t; hiba: -1 és
errno |
dup() |
Új, külön lezárandó FD-t ad | Hiba: -1; az eredeti változatlan |
dup2() |
A cél-FD kötését lecserélheti | A régi célhoz tartozó kölcsönzések szemantikailag elavulnak |
close() |
Lezárási kísérlet; a pontos hibapolicy platformfüggő | A szám később más erőforráshoz kiosztható |
fileno() |
A FILE által kezelt FD kölcsönzése |
fclose() után nem használható |
A dup() eredménye ugyanarra a megnyitott fájlleírásra
hivatkozik: a fájlpozíció és a fájlállapotjelzők közösek. A két FD
azonban külön lezárandó. A dup2(oldfd, newfd) az ellenőrzés
és csere közötti versenyablak nélkül helyettesíti a célkötést; nem
azonos egy kézzel írt close(newfd); dup(oldfd); sorozattal.
Érvényes, azonos argumentumoknál nem hoz létre új FD-t. A lecserélt cél
lezárásának esetleges hibáját nem jelenti külön.
POSIX, Linuxon is: az O_CLOEXEC az új
FD-hez close-on-exec jelzőt állít be: sikeres exec esetén
az FD bezáródik. Ez nem helyettesíti a normál cleanupot, viszont
elkerüli a megnyitás és egy későbbi jelzőbeállítás közötti öröklési
versenyablakot. A dup() és az eltérő FD-k közötti
dup2() nem viszi tovább ezt a jelzőt az új FD-re; azonos
argumentumú dup2() változatlanul hagyja.
A read() és write() eredménye
ssize_t, mert nemnegatív darabszám mellett a
-1 hibajelzést is reprezentálnia kell. Csak a hibavizsgálat
után alakítsuk size_t típusra.
POSIX, pozitív kérési méret mellett:
| Eredmény | read() |
write() |
|---|---|---|
| Pozitív érték | Ennyi C bájt érvényes a bufferben | Ennyi C bájtnyi prefixet elfogadott |
0 |
Szokásos fájlnál vagy byte-stream végpontnál EOF | A helpernek előrehaladás nélküli esetet kell kezelnie |
-1, EINTR |
Megszakítás; a hívás nem közölt átvitt adatot | Megszakítás; a hívás nem közölt átvitt adatot |
-1, EAGAIN vagy
EWOULDBLOCK |
Most nem végezhető el blokkolás nélkül | Most nem végezhető el blokkolás nélkül |
| Más hiba | Hibapolicy szükséges | Hibapolicy szükséges |
A rövid művelet (partial I/O) pozitív, de a kértnél kisebb eredmény. Önmagában nem hiba és nem EOF. Ha megszakítás előtt történt adatátvitel, a hívás pozitív darabszámot is visszaadhat.
Az EOF sem feltétlenül végleges tulajdonság: egy később megnövelt fájlból újra lehet adatot olvasni. Pipe esetén az üres buffer és az összes íróvég lezárása együtt vezet EOF-hoz.
A következő helper szerződése normál fájlokra, pipe-okra és byte-stream végpontokra vonatkozik, nem speciális eszközök minden üzemmódjára. Egyes terminálbeállításoknál például a nulla olvasási eredmény más jelentésű lehet.
A blocking mód nem garantál teljes átvitelt. A
nonblocking mód pedig azt követeli meg az
alkalmazástól, hogy EAGAIN után várjon egy megfelelő
eseményre, ne foglalja a processzort azonnali újrapróbálkozással.
Linuxon az O_NONBLOCK normál fájlokra nem jelent általános
garanciát arra, hogy az I/O soha nem vár háttértárra.
A szerződés külön kezeli az előrehaladást, a leállás okát és a hibakódot:
read_some() legfeljebb egy olvasási kísérletet végez;
rövid olvasás megengedett.write_all() addig ír, amíg minden adat elfogy, vagy
jelentendő állapot következik be.IO_INTR esetén a hívó dönt az újrapróbálásról vagy
megszakításról.IO_OK, nem EOF.Példaprofil: ISO C17, POSIX API, Linux x86-64 System V, LP64, 8 bites C bájt. Választott laborfordítók: GCC 14.2.0 és Clang 19.1.7; libc: glibc 2.39. A kódhoz itt nem tartozik fordítási vagy futtatási jegyzőkönyv.
Az alábbi fd_io.c önállóan fordítható modul:
#define _POSIX_C_SOURCE 200809L
#include <errno.h>
#include <limits.h>
#include <stddef.h>
#include <unistd.h>
typedef enum {
IO_OK, IO_EOF, IO_WAIT, IO_INTR, IO_ERROR
} io_state;
typedef struct {
size_t done;
io_state state;
int error;
} io_result;
static size_t request_size(size_t remaining)
{
const size_t limit = (size_t)SSIZE_MAX;
return remaining < limit ? remaining : limit;
}
static io_result stopped(size_t done, int error)
{
io_state state = IO_ERROR;
if (error == EINTR)
state = IO_INTR;
else if (error == EAGAIN || error == EWOULDBLOCK)
state = IO_WAIT;
return (io_result){done, state, error};
}
io_result read_some(int fd, void *buffer, size_t capacity)
{
if (capacity == 0)
return (io_result){0, IO_OK, 0};
ssize_t n = read(fd, buffer, request_size(capacity));
if (n > 0)
return (io_result){(size_t)n, IO_OK, 0};
if (n == 0)
return (io_result){0, IO_EOF, 0};
int saved = errno;
return stopped(0, saved);
}
io_result write_all(int fd, const void *buffer, size_t length)
{
const unsigned char *bytes = buffer;
size_t done = 0;
while (done < length) {
ssize_t n = write(fd, bytes + done,
request_size(length - done));
if (n > 0) {
done += (size_t)n;
continue;
}
if (n == 0)
return (io_result){done, IO_ERROR, EIO};
int saved = errno;
return stopped(done, saved);
}
return (io_result){done, IO_OK, 0};
}
/* Kifejezetten Linux-policy: a tulajdonjogot elfogyasztja. */
int close_owned_linux(int *owner)
{
int fd = *owner;
*owner = -1;
if (fd == -1)
return 0;
if (close(fd) == 0)
return 0;
return errno;
}Fordítás, linkelés és futtatás nélkül:
gcc -std=c17 -Wall -Wextra -Wpedantic -Wconversion -O2 \
-c fd_io.c -o fd_io.gcc.o
clang -std=c17 -Wall -Wextra -Wpedantic -Wconversion -O2 \
-c fd_io.c -o fd_io.clang.oA buffer tulajdonosa a hívó. Pozitív hossz esetén a teljes megadott
tartomány legyen élő és megfelelően hozzáférhető: olvasásnál írható,
írásnál olvasható. Felszabadítás, áthelyező realloc vagy az
objektum élettartamának vége érvényteleníti a kölcsönzést. A buffer és
az FD a hívás közben nem szüntethető meg más végrehajtási szálból.
A példa nem támogat külön igazítási szerződést igénylő
O_DIRECT I/O-t. Pipe vagy socket írásánál a hívónak a
SIGPIPE policy-t is rendeznie kell: különben a folyamat
megszűnhet, mielőtt az EPIPE eredményt feldolgozná.
Bizonyításvázlat a méretszámítás helyességére. Legyen a teljes hossz, a már továbbított mennyiség. A ciklusinvariáns:
A következő kérés:
Pozitív eredménynél , ezért:
Így sem a kivonás, sem az összeadás nem csordul túl. A pointert csak
mellett képezzük; nulla hosszhoz null pointer is adható. Az
SSIZE_MAX korlát elkerüli az ennél nagyobb kérés
implementációfüggő kezelésére támaszkodást.
A nulla eredményű íráshoz rendelt EIO a helper saját
hibapolicy-ja: megakadályozza az előrehaladás nélküli végtelen
ciklust.
Tegyük fel, hogy read_some() egy 4096 C bájtos bufferbe
1200 C bájtot olvas. Ez siker, nem EOF. A write_all()
először 700-at továbbít, majd EINTR miatt visszatér:
/* Szemléltető eredmény, nem futtatási mérés. */
io_result result = {700, IO_INTR, EINTR};A következő próbálkozás csak a maradék 500 C bájtot kaphatja:
/* Részlet: buffer él, out_fd kölcsönzött, újrapróbálás engedélyezett. */
io_result next = write_all(out_fd, buffer + 700, 1200 - 700);A teljes 1200 ismételt elküldése adatduplikáció lenne. A buffer csak
az összes hátralévő adat továbbítása vagy tudatos eldobása után
használható új olvasáshoz. IO_WAIT esetén ugyanezt a
maradékot kell megőrizni a későbbi folytatáshoz.
A megnyitás szokásos mintája, POSIX-függvényen belüli részletként:
/* Szükséges headerek: <fcntl.h>, <errno.h>. */
int fd = open(path, O_RDONLY | O_CLOEXEC);
if (fd == -1)
return errno;
/* fd tulajdonosa ez a függvény; I/O közben csak kölcsönadja. */A hibás ellenőrzés, fd <= 0, elutasítaná a teljesen
érvényes nulla descriptort is.
Linux-policy: a fenti
close_owned_linux() egyszer próbál lezárni, és a
tulajdonosi változót -1 értékre állítja. A függvény
kizárólag érvényes, saját tulajdonú FD-t vagy -1 értéket
fogadhat. Az owner pointer egy írható, élő int
objektum kölcsönzése.
Linuxon a lezárás hibája — az eleve érvénytelen FD-t jelző
EBADF esetétől eltekintve — nem jogosít fel ugyanazon szám
újabb lezárására. A szám addigra más erőforráshoz tartozhat. Ez az
EINTR esetre is vonatkozik.
Platform- és verzióhatár: ez nem univerzális
POSIX-policy. Más rendszereken az EINTR utáni FD-állapot
eltérhet; a célzott POSIX-kiadás és a tényleges libc/OS szerződését
együtt kell ellenőrizni. Egy POSIX.1-2024 követelmény megnevezése
önmagában nem bizonyítja a telepített Linux/libc viselkedését. Az
EINTR kezelését ezért műveletenként, nem általános
retry-makróval tervezzük.
A lezárási hibát nem szabad elhallgatni: korábbi írás késleltetett
hibáját is jelezheti. A korábbi I/O-hibát azonban előbb el kell menteni;
a cleanup hibája másodlagos diagnosztika legyen. A sikeres
write() és close() sem általános tartóssági
garancia; ezt a fájlrendszer és tartósság
tárgyalja.
A FILE saját felhasználói pufferrel dolgozik. Emiatt a
fprintf() után közvetlenül kiadott
write(fileno(stream), ...) nem feltétlenül a kívánt
sorrendben jelenik meg.
POSIX: az ugyanazon megnyitott fájlleírást használó
streamek és descriptorok között az aktív kezelőre (active
handle) vonatkozó szinkronizálási szabályokat kell követni.
Kimeneti streamről descriptorra váltás előtt tipikusan ellenőrzött
fflush() szükséges. Seekelhető bemenetnél a read-ahead
puffer miatt a logikai streampozíció és a kernelpozíció eltérhet;
például egy ellenőrzött fseek(stream, 0, SEEK_CUR)
rendezheti a pozíciót a váltás előtt.
Nem seekelhető bemenetnél az előre beolvasott adat általában nem
adható vissza a pipe-nak. Ilyenkor célszerű egyetlen I/O-rétegnél
maradni. Az fflush(stdin) nem hordozható ISO
C-megoldás.
A fileno() nem ad tulajdonjogot. A sikeres
fdopen() után a lezárási felelősséget a streamhez kell
rendelni: a fclose() mellett külön close()
dupla lezárási hiba.
Magyarázó: egy 8192 C bájtos olvasási kérés 1500-at ad vissza. Mi bizonyított, és mi nem?
Kódolási: írj blocking
write_all_blocking() wrappert, amely kizárólag
IO_INTR után próbál újra, megőrzi az összesített
előrehaladást, és nem pörög IO_WAIT esetén.
Diagnosztikai: Linuxon a program addig ismétli a
close(fd) hívást, amíg az EINTR eltűnik.
Közben másik szál fájlokat nyit. Mi a hiba, és hogyan
javítanád?
A buffer első 1500 C bájtja érvényes új adat. Nem bizonyított az EOF, és nem bizonyított alkalmazásszintű üzenethatár sem. Ellenőrzési feltétel: a feldolgozó pontosan ezt az 1500-at fogyassza el; a buffer többi részét ne tekintse frissen olvasottnak.
Az előző modulhoz hozzáadható részlet:
io_result write_all_blocking(int fd, const void *buffer, size_t length)
{
const unsigned char *bytes = buffer;
size_t total = 0;
while (total < length) {
io_result r = write_all(fd, bytes + total, length - total);
total += r.done;
if (r.state == IO_OK)
return (io_result){total, IO_OK, 0};
if (r.state != IO_INTR)
return (io_result){total, r.state, r.error};
}
return (io_result){total, IO_OK, 0};
}Az előfeltételek változatlanok; a hívó engedélyezi a megszakítás
utáni újrapróbálást. Ez nem megfelelő policy, ha a signal leállítási
kérés. Ellenőrzési feltétel: részleges írás, majd EINTR
után minden adat pontosan egyszer szerepeljen a továbbított sorozatban;
IO_WAIT azonnal térjen vissza.
Az első hívás már felszabadíthatta az FD számát. Az ismételt hívás
így a másik szál új fájlját zárhatja le. Javítás: egyszeri
Linux-lezárás, a tulajdonosi változó érvénytelenítése, a hibakód
rögzítése, újrapróbálás nélkül. Ellenőrzési feltétel: egy tulajdonlási
életciklusból legfeljebb egy close() kísérlet induljon.
A robusztus I/O három külön adatot őriz: az előrehaladást, a megállás okát és az erőforrás tulajdonosát. A rövid művelet siker; az EOF, az átmeneti várakozás és a megszakítás külön állapot. Folytatáskor csak a maradék tartomány használható, lezáráskor pedig platformhoz kötött policy szükséges — nem általános retry-szabály.