Zarządzanie plikami i urządzeniami wejścia-wyjścia

ŁF
<html> <body style="FONT-FAMILY: Verdana; FONT-SIZE: 10pt;"> <style> table {font-size: 10pt} h3 { color: #000000; font-weight: bold; font-size: 18pt; background-color: #e8d5ba; text-align: center; padding: 5; border: double 1pt; width: 100%; margins: 0; paddings: 4 } h4 { color: #000000; font-weight: bold; font-size: 16pt; background-color: #c5dcdc; text-align: center; padding: 5; border: double 1pt; width: 100%; margins: 0; paddings: 4 } h5 { color: #000000; font-weight: normal; font-size: 11pt; background-color: #c5dcc5; text-align: left; padding: 5; border: double 1pt; width: 100%; margins: 0; paddings: 4 } h6 { color: #0000cd; font-weight: bold; font-size: 12pt; font-family: monospace, Courier, "Courier CE"; text-align: left; text-indent: 10pt; word-spacing: 3pt; width: auto } em { color: #0000cd; font-weight: normal; font-size: 12pt; font-family: Courier, "Courier CE"; text-align: left; text-indent: 10pt; word-spacing: 0pt; width: auto } </style>

LINUX - Zarządzanie plikami i urządzeniami wejścia/wyjścia.

1. System plików

Trwałe przechowywanie i udostępnianie danych należy do podstawowych i najbardziej widocznych zadań systemu operacyjnego. Realizując to zadanie, większość systemów posługuje się pojęciem pliku jako logicznej jednostki przechowywania informacji.

W tym artykule opisujemy sposób zarządzania plikami realizowany przez jądro systemu Linux. Przedstawiamy warstwową strukturę systemu plików i omawiamy przeznaczenie poszczególnych warstw. Opisujemy sposób reprezentacji różnych typów plików oraz organizację struktury katalogowej. Omawiamy również interfejs funkcji systemowych umożliwiający operacje na plikach i katalogach.

System plików tworzy mechanizm bezpośredniego przechowywania i dostępu do informacji w systemie operacyjnym. Odwzorowuje logiczną koncepcję pliku na fizyczne urządzenia pamięci masowej.

W systemach Unix i Linux system plików oferuje wspólny interfejs do plików, urządzeń, łączy komunikacyjnych i gniazdek komunikacji sieciowej.

(1.1) Struktura warstwowa systemu plików

System plików Linuxa ma strukturę warstwową przedstawioną na rys. 8.1.

RYS8_1.JPG
Rys. 8.1 Struktura systemu plików w systemie Linux
(1.2) Urządzenia i programy obsługi (sterowniki)

Pliki przechowywane są zazwyczaj na urządzeniach blokowych o dostępie swobodnym, takich jak dyski magnetyczne lub optyczne. Każdy dysk może być podzielony na kilka partycji logicznych, reprezentowanych w systemie jako odrębne urządzenia logiczne. Programy obsługi urządzeń stanowią część składową jądra systemu, która musi być zaimplementowana w specyficzny sposób dla każdej platformy sprzętowej. Każde urządzenie logiczne ma swoją reprezentację w postaci pliku specjalnego w systemie plików Linuxa.

Przykład:
/dev/hda1 - reprezentuje pierwszą partycję na pierwszym dysku IDE w systemie,
/dev/hda2 - reprezentuje drugą partycję tego dysku.

Każda partycja może zawierać odrębny system plików.

(1.3) Podręczna pamięć buforowa

Podręczna pamięć buforowa (ang. buffer cache) pośredniczy w dostępie rzeczywistych systemów plików do blokowych urządzeń pamięci masowej. Bloki danych odczytane z urządzeń umieszczane są w buforach pamięci podręcznej i przechowywane przez pewien czas. Daje to możliwość ponownego ich użycia bez konieczności powtarzania operacji odczytu z urządzenia. Również wszystkie modyfikacje wprowadzane są najpierw w pamięci podręcznej i dopiero w później zapisywane w pamięci masowej w dogodnym dla systemu czasie. Dostęp do pamięci podręcznej realizowany jest znacznie szybciej i w ten sposób system zwiększa szybkość dostępu do danych przechowywanych przez fizyczne urządzenia pamięci.

Podręczna pamięć buforowa jest dzielona pomiędzy wszystkie urządzenia blokowe. W każdej chwili mogą się w niej znajdować bloki pochodzące z różnych urządzeń dyskowych. W celu poprawnej identyfikacji bloków, każdy bufor zaopatrzony jest w nagłówek przechowujący informacje o numerze urządzenia i numerze bloku.

(1.4) Rzeczywiste systemy plików

System Linux może obsługiwać wiele popularnych systemów plików. Stanowi to jego niewątpliwą zaletę, gdyż umożliwia użytkownikom łatwe posługiwanie się plikami z różnych systemów bez koniecznosci dokonywania kłopotliwych konwersji danych.

Tablica 8.1 Wybrane rzeczywiste systemy plików obsługiwane w systemie Linux</span></caption> <font size="+1">Typ systemu plików</span> </td> <font size="+1">Opis</span> </td> </tr> minix</td> Prosty system plików systemu operacyjnego Minix, zaadoptowany jako pierwszy system plików dla systemu Linux.</td> </tr> ext</td> Rozwinięcie systemu minix.</td> </tr> ext2</td> Podstawowy system plików systemu Linux.</td> </tr> msdos</td> System plików systemu MSDOS.</td> </tr> umsdos</td> Rozszerzona wersja systemu MSDOS.</td> </tr> vfat</td> System plików systemu Windows 95, NT.</td> </tr> ntfs</td> System plików systemu Windows NT, 2000.</td> </tr> hpfs</td> System plików systemu OS/2.</td> </tr> ufs</td> System plików systemu Solaris.</td> </tr> iso9660</td> System plików nośników CD-ROM zgodny z normą ISO 9660.</td> </tr> nfs</td> Sieciowy system plików firmy Sun.</td> </tr> smb</td> Sieciowy system plików oparty na protokole SMB.</td> </tr> proc</td> Pseudo-system plików stanowiący interfejs do struktur task_struct procesów w pamięci jądra.</td> </tr> </table>

Konkretny, działający system Linux obsługuje tylko te typy systemu plików, które zostały zarejestrowane w pliku /etc/filesystems. Jego zawartosć może wyglądać następująco:

[apw@venus apw]$ cat /etc/filesystems
ext2
nodev proc
nodev devpts
iso9660
vfat
hfs

(1.5) Wirtualny system plików

System Linux musi zarządzać jednocześnie wieloma typami rzeczywistych systemów plików. System zawiera w tym celu dodatkową warstwę jednolitego interfejsu jądra, która separuje rzeczywiste systemy od reszty systemu operacyjnego. Interfejs ten nosi nazwę wirtualnego systemu plików VFS (ang. Virtual File System).

Zadaniem wirtualnego systemu plików jest połączenie poszczególnych systemów, zrealizowanych na odrębnych urządzeniach logicznych, w jeden wspólny system plików. System VFS organizuje wspólną strukturą katalogową i zapewnia wspólny interfejs funkcji systemowych, niezależny od fizycznej implementacji plików w systemach składowych. W tym celu VFS wykorzystuje specjalne struktury danych, które zapewniają dodatkowy poziom jednolitej reprentacji poszczególnych plików i całych systemów plików.

Każdy plik reprezentowany jest przez i-węzeł VFS, opisujący jego atrybuty oraz wskazujący urządzenie logiczne i zestaw procedur do operacji na rzeczywistym pliku.

Rzeczywisty system plików reprezentuje specjalny blok identyfikacyjny VFS, który zawiera m.in.:

  1. nazwę urządzenia logicznego, zawierającego system pików,
  2. typ systemu plików,
  3. wskazanie na zestaw procedur operujących na bloku identyfikacyjnym rzeczywistego systemu,
  4. informacje specyficzne dla rzeczywistego systemu plików.

Dołączanie nowego systemu plików do wspólnej struktury katalogowej określane jest jako montowanie systemu plików. Operacja wymaga podania punktu montowania, którym powinien być pusty katalog. W przypadku podania niepustego katalogu, jego zawartość zostanie przykryta przez nowy fragment struktury katalogowej.

(1.6) Polecenia systemowe

Tworzenie i nadzorowanie systemu plików należy do najważniejszych zadań administratora systemu. Wykorzystuje przy tym bogaty zestaw nadzędzi programowych oferowanych przez system Linux. Do najważniejszych programów należą:

fdisk - tworzenie i manipulacja tablicą partycji dysku
mkfs - tworzenie systemu plików
fsck - sprawdzanie i naprawianie systemu plików
mount - montowanie systemu plików
umount - odmontowanie systemu plików
df - podaje zajętość przestrzeni dyskowej dla wszystkich zamontowanych systemów plików

2. System plików Ext2

System EXT2 jest podstawowym systemem plików Linuxa. Na rys. 8.2 przedstawiono jego strukturę fizyczną w porównaniu ze strukturą klasycznego systemu plików Unix-a.

RYS8_3.JPG

Rys. 8.2 Struktura fizyczna systemu plików: a) klasyczna, b) systemu EXT2

System plików zrealizowany na urządzeniu blokowym składa się z ciągu bloków dyskowych o rozmiarze ustalonym w granicach od 512 B do 8 kB, będącym wielokrotnością 512 bajtów. Rozmiar bloku musi być ustalony podczas tworzenia systemu plików.

Niektóre spośród tych bloków mają szczególne przeznaczenie:

blok systemowy lub inicjujący
(ang. boot block)
- może zawierać podstawowy program ładujący jądro systemu operacyjnego do pamięci operacyjnej w czasie STARTowania systemu,
blok identyfikacyjny
(ang. superblock)
- zawiera informacje w pełni opisujące aktualny stan systemu plików.

Pozostałe bloki przechowują struktury i-węzłów reprezentujących pliki oraz dane plików.

System EXT2 wyróżnia grupy bloków. Każda grupa zawiera:

  • kopię bloku identyfikacyjnego systemu,
  • bloki zawierające informacje o pozostałych grupach (deskryptory grup),
  • bloki przechowujące mapy bitowe przydziału i-węzłów i bloków w grupie
  • bloki zawierające tablicę i-węzłów plików
  • bloki danych.
Dzięki zwielokrotnieniu informacji o całym systemie plików i o grupach bloków, zwiększa się bezpieczeństwo przechowywania danych. Ponadto, podział bloków na grupy ułatwia przechowywanie bloków danych pliku w pobliżu jego i-węzła oraz przechowywanie i-węzłów plików blisko i-węzła katalogu, do którego są dowiązane. Taka organizacja przespiesza dostęp do danych.

Obecnie większość systemów uniksowych stosuje strukturę systemu plików zbliżoną do systemu EXT2.

3. Pliki

(3.1) Reprezentacja pliku w systemie EXT2
</p>

Każdy plik w systemie reprezentowany jest przez strukturę inode, nazywaną i-węzłem pliku (rys.8.3). W strukturze tej zapisane są wszystkie atrybuty pliku m. in.:

  • tryb pliku,
  • rozmiar pliku w bajtach,
  • identyfikatory właściciela UID,
  • identyfikator grupy GID,
  • daty i czasy:
    • utworzenia pliku,
    • ostatniej modyfikacji zawartości,
    • ostatniej zmiany atrybutów,
  • liczba dowiązań,
  • adresy bloków danych:
    • 12 adresów bezpośrednich, wskazujących bloki danych,
    • 1 adres pośredni, wskazujący blok dyskowy zawierający właściwe adresy bloków danych,
    • 1 adres powójnie pośredni, zawierający dwa poziomy wskazań pośrednich bloków z adresami,
    • 1 adres potrójnie pośredni, zawierający trzy poziomy wskazań pośrednich bloków z adresami.

RYS8_4.JPG

Rys. 8.3 Reprezentacja pliku

</span>

Taki sposób reprezentacji pliku ilustruje rysunek 8.3. Dzięki zastosowaniu pośredniego adresowania bloków danych, możliwe jest przechowywanie bardzo dużych plików przy zachowaniu stosunkowo niewielkich rozmiarów i-węzła, który zawiera tylko15 adresów. Małe pliki mieszczą się w blokach adresowanych bezpośrednio. Większe pliki korzystają również z adresów pośrednich. Ceną za taką elastyczność jest wydłużanie czasu dostępu do danych wraz ze wzrostem rozmiaru pliku. Dostęp do początkowych danych pliku, mieszczących się w blokach adresowanych bezpośrednio, wymaga tylko jednej operacji dyskowej w celu odczytania zawartości bloku. Dane umieszczone w dalszych blokach adresowane są w sposób wielostopniowy. Z tego względu wymagają jednej, dwóch lub nawet trzech operacji dostępu do dysku w celu pobrania adresów i jednej operacji odczytania bloku danych. We wszystkich przypadkach dochodzi jeszcze konieczność odczytania z dysku i-węzła pliku (jeśli nie ma go w pamięci podręcznej).

(3.2) Tryb pliku
</p>

Tryb pliku jest liczbą 16-bitową opisującą trzy atrybuty pliku:

  • typ pliku (4 bity),
  • modyfikatory praw dostępu (3 bity),
  • prawa dostępu (9 bitów).
Przeznaczenie poszczególnych bitów pokazuje rys. 8.4.

RYS8_2.JPG

Rys. 8.4 Tryb pliku

</span>

Pierwsze 4 bity kodują typ pliku zgodnie z opisem w tablicy 8.2.

<font size="+1">Tablica 8.2 Kodowanie typu pliku</span> </caption> Stała symboliczna </td> Wartość w kodzie ósemkowym </td> Znaczenie </td> </tr> S_IFMT</td>
0170000
</td> maska bitowa typu pliku</td> </tr> S_IFSOCK</td>
0140000
</td> gniazdo</td> </tr> S_IFLNK</td>
0120000
</td> dowiązanie symboliczne</td> </tr> S_IFREG</td>
0100000
</td> plik zwykły</td> </tr> S_IFBLK</td>
0060000
</td> plik specjalny blokowy</td> </tr> S_IFDIR</td>
0040000
</td> katalog</td> </tr> S_IFCHR</td>
0020000
</td> plik specjalny znakowy</td> </tr> S_IFIFO</td>
0010000
</td> kolejka FIFO</td> </tr> </table>

Wymienione stałe mogą być używane do rozpoznawania typu pliku na podstawie wartości trybu. Stała S_IFMT definiuje maskę wycinającą bity określające typ pliku z 16-bitowej liczby trybu. Wynik porównuje się następnie z jedną z pozostałych stałych w celu rozpoznania typu. Na przykład poniższe wyrażenie sprawdza, czy plik jest katalogiem:

(tryb & S_IFMT) == S_IFDIR

Kolejne 3 bity trybu opisują modyfikatory praw dostępu do pliku. Znaczenie poszczególnych bitów opisano w tablicy 8.3.

<font size="+1">Tablica 8.3 Modyfikatory praw dostępu</span> </caption> Nazwa bitu </td> Stała </td> Wartość </td> Znaczenie bitu </td> </tr> bit ustanowienia użytkownika
(ang. setuid bit)</td> S_ISUID</td> 0004000</td> Bit ma znaczenie tylko dla plików wykonywalnych. Powoduje, że proces wykonuje program z identyfikatorem obowiązującym użytkownika ustawionym na identyfikator własciciela pliku.</td> </tr> bit ustanowienia grupy
(ang. setgid bit)</td> S_ISGID</td> 0002000</td> Bit ma znaczenie tylko dla plików wykonywalnych. Powoduje, że proces wykonuje program z identyfikatorem obowiązującym grupy ustawionym na identyfikator grupy pliku.</td> </tr> bit lepkosci
(ang. sticky bit)</td> S_ISVTX</td> 0001000</td> Dla pliku zwykłego: obecnie bit jest ignorowany. Dawniej wymuszał pozostawienie kodu programu w pamięci po zakończeniu jego wykonywania.
Dla katalogu: pliki mogą być usuwane tylko przez własciciela lub administratoraniezależnie od posiadania prawa pisania w katalogu.</td> </tr> </table>

Ostatnie 9 bitów trybu opisuje prawa dostepu do pliku dla właściciela, grupy i pozostałych użytkowników. Znaczenie tych praw zostało opisane we wcześniejszym artykule. Istnieje również zestaw stałych symbolicznych dla różnych ustawień praw dostępu, jednak powszechnie stosuje wartości w kodzie ósemkowym.

(3.3) Katalogi i dowiązania
</p>

Katalog przechowuje listę pozycji wiążących nazwy plików z numerami ich i-węzłów. Każda pozycja w katalogu, nazywana dowiązaniem lub dowiązaniem twardym, reprezentowana jest przez strukturę dirent opisaną poniżej:

`struct dirent {`
`long d_ino;` - numer i-węzła
`off_t d_off;` - przesunięcie, wskazujące na nastepną pozycję w katalogu
(początek nastepnej struktury dirent)
`unsigned short d_reclen;` - długość nazwy pliku
`char d_name [NAME_MAX+1];` - nazwa pliku
`}`

Plik może być dowiązany do wielu katalogów pod różnymi nazwami i wszystkie dowiązania są równorzędne. Utworzenie każdego nowego dowiązania pliku do katalogu powoduje zwiększenie wartości licznika dowiązań przechowywanego w i-węźle pliku. Usunięcie dowiązania powoduje zmniejszenie wartości tego licznika i nie ma wpływu na pozostałe dowiązania do pliku. Po usunięciu ostatniego dowiązania i wyzerowaniu licznika, jądro systemu zwalnia i-węzeł i bloki danych pliku.

Nowe dowiązanie pliku nie może przekroczyć granicy systemu plików. Oznacza to, że katalog zawierający dowiązanie i wskazywany przez nie plik muszą znajdować się na tej samej partycji logicznej.

Dowiązanie symboliczne jest plikiem specjalnego typu, który wskazuje położenie innego pliku w strukturze katalogowej. Reprezentowane jest przez i-węzeł i jeden blok danych zawierający nazwę ścieżkową wskazywanego pliku. Jako plik, dowiązanie symboliczne musi być też dowiązane do jakiegoś katalogu. Stworzenie dowiązania symbolicznego oznacza więc konieczność utworzenia nowego pliku i nowego dowiązania twardego w katalogu.

Dowiązanie symboliczne może wskazywać dowolny plik, przechowywany na dowolnej partycji logicznej. w ramach jednej struktury katalogowej. Dowiązanie symboliczne może przekraczać granicę systemu plików w ramach jednej struktury katalogowej. Może zatem wskazywać dowolny plik, przechowywany na dowolnej partycji logicznej, ponieważ przechowuje tylko jego nazwę ścieżkową w strukturze katalogowej.

(3.4) Pliki specjalne
</p>

Wszystkie urządzenia są w systemie Linux reprezentowane przez pliki specjalne. System rozróżnia dwa typy urządzeń:

urządzenia blokowe o dostępie swobodnym - reprezentowane przez pliki specjalne blokowe,
urządzenia znakowe o dostępie sekwencyjnym - reprezentowane przez pliki specjalne znakowe.

Reprezentacja plików specjalnych różni się trochę od reprezentacji innych plików. Plik specjalny nie korzysta z bloków danych, a w jego i-węźle zamiast adresów przechowywane są dwa numery programu obsługi urządzenia: główny i drugorzędny.

Do tworzenia plików specjalnych służy funkcja systemowa mknod().

int mknod(const char *pathname, mode_t mode, dev_t dev);
gdzie:
pathname - nazwa scieżkowa tworzonego pliku,
mode - tryb pliku, definiujący typ i prawa dostępu do pliku,
dev - numery urządzenia, główny i drugorzędny.

4. Operacje na plikach


(4.1) Podstawowe operacje na plikach zwykłych

Utworzenie nowego pliku zwykłego lub otwarcie istniejącego umożliwiają dwie funkcje systemowe:

int open(const char *pathname, int flags, mode_t mode);
int creat(const char *pathname, mode_t mode);
gdzie:
pathname - nazwa scieżkowa pliku,
flags - flagi,
mode - prawa dostępu do pliku.

Funkcja open() otwiera plik o podanej nazwie, przydziela mu najmniejszy z dostępnych w procesie deskryptorów i zwraca jego wartość. Jeżeli podany plik nie istnieje, to funkcja zwraca błąd albo tworzy ten plik, gdy ustawione są odpowiednie flagi.

Argument flags musi zawierać znacznik określający typ dostępu do pliku:

O_RDONLY - otwarty tylko do czytania,
O_WRONLY - otwarty tylko do pisania,
O_RDWR - otwarty do czytania i pisania.

Dodatkowo można dołączyć w postaci sumy bitowej inne flagi modyfikujące sposób działania funkcji oraz sposób korzystania z otwartego pliku. W tablicy 8.4 opisano przeznaczenie kilku najczęściej używanych flag.

<font size="+1">Tablica 8.4 Flagi modyfikujące sposób dostępu do pliku</span> </caption> <font size="+1"> Flaga</span> </td> <font size="+1"> Znaczenie</span> </td> </tr> O_CREAT</td> Funkcja tworzy podany plik lub otwiera go, jeśli plik już istnieje.</td> </tr> O_EXCL</td> Powinna być używana wyłącznie z O_CREAT.
Funkcja tworzy podany plik lub zwraca błąd, jeśli plik już istnieje.</td> </tr> O_TRUNC</td> Jeżeli plik już istnieje, to funkcja kasuje jego zawartość i ustawia rozmiar 0.</td> </tr> O_APPEND</td> Funkcja otwiera plik w trybie dopisywania. Wskaźnik bieżącej pozycji ustawiany jest na koniec pliku przed każda operacją zapisu.</td> </tr> O_NONBLOCK
O_NDELAY</td> Funkcja otwiera plik w trybie operacji bez blokowania. Taki tryb można ustawić jedynie dla kolejek FIFO i gniazd. Operacje na zwykłych plikach są zawsze blokujące.</td> </tr> O_SYNC</td> Funkcja otwiera plik w trybie synchronicznych operacji zapisu. Każda operacja zapisu do pliku powoduje zablokowanie procesu do momentu fizycznego zapisania danych na dysku. W zwykłym trybie zapisane dane podlegają buforowaniu w pamięci i są zapisywane na dysku w dogodnym dla systemu momencie.</td> </tr> </table>

Wywołanie funkcji creat() jest równoważne wywołaniu funkcji open() z flagami: O_CREAT | O_WRONLY | O_TRUNC.

Do zamykania otwartych plików służy funkcja close().

int close(int fd);
Funkcja zamyka podany deskryptor pliku. Jeśli jest to ostania kopia deskryptora wskazującego otwarty plik, to struktura file tego pliku jest usuwana z pamięci. Przed zamknięciem funkcja zapisuje na dysku wszystkie zbuforowane dane. Jeśli ta operacja nie powiedzie się, to funkcja zwróci błąd.
ssize_t read(int fd, void *buf, size_t count);
gdzie:
fd - deskryptor otwartego pliku,
buf - wskaźnik do bufora, w którym zostaną zapisane dane odczytane z pliku,
count - liczba bajtów danych do odczytania.

Funkcja wczytuje count bajtów z pliku o deskryptorze fd zaczynając od bieżącej pozycji wskaźnika w pliku i umieszcza odczytane dane w buforze buf. Funkcja zwraca liczbę wczytanych bajtów, która może być mniejsza od wartości count. Domyślnie proces jest blokowany do momentu zakończenia operacji. Każda operacja odczytu powoduje przesunięcie wskaźnika bieżącej pozycji o liczbę odczytanych bajtów.

ssize_t write(int fd, const void *buf, size_t count);
gdzie:
fd - deskryptor otwartego pliku,
buf - wskaźnik do bufora, z którego zostaną pobrane dane przeznaczone do zapisania w pliku,
count - liczba bajtów danych do zapisania.

Funkcja zapisuje count bajtów z bufora buf w pliku o deskryptorze fd zaczynając od bieżącej pozycji wskaźnika w pliku. Funkcja zwraca liczbę zapisanych bajtów, która może być mniejsza od wartości count. Domyślnie proces jest blokowany do momentu zakończenia operacji. Każda operacja zapisu powoduje przesunięcie wskaźnika bieżącej pozycji o liczbę zapisanych bajtów. Dzięki temu kolejne dane zostaną zapisane za poprzednimi, najczęściej na końcu pliku.

Istnieje możliwość ustawienia wskaźnika na dowolną pozycję w pliku przy pomocy funkcji lseek().

off_t lseek(int fd, off_t offset, int whence);
gdzie:
fd - deskryptor otwartego pliku,
offset - przesunięcie wskaźnika,
whence - miejsce w pliku, względem którego następuje przesunięcie.

Funkcja przesuwa wskaźnik bieżącej pozycji w pliku o offset bajtów względem miejsca określonego przez argument whence:

SEEK_SET - przesunięcie względem początku pliku,
SEEK_CUR - przesunięcie względem bieżącej pozycji w pliku,
SEEK_END - przesunięcie względem końca pliku.
Dodatnia wartość offset powoduje przesunięcie w kierunku końca pliku. Wartość ujemna może wystąpić tylko dla SEEK_CUR i SEEK_END i powoduje przesunięcie w kierunku początku pliku.

Wskaźnik może być nawet przesunięty poza aktualny koniec pliku.

Funkcja truncate() umożliwia skrócenie pliku do podanej długości poprzez ustawienie nowej wartości atrybutu rozmiar w i-węźle. Dane położone poza nowym rozmiarem są tracone.

int truncate(const char *path, off_t length);
gdzie:
path - nazwa pliku,
length - długość pliku w bajtach.

Przykład
Przedstawiamy kod programu, który odwraca kolejność znaków w pliku podanym jako argument wywołania (zapisuje ten sam plik od końca). Program wykorzystuje funkcje lseek() do przesuwania wskaźnika oraz funkcje read() i write() do zamiany znaków.

#include <stdio.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>

#define MAXNAME 100
#define PERM 0666

main(int argc, char *argv[])
{
        int dfile, dtmp;
        off_t ptr;
        char tmpfile[MAXNAME]="\0", c;

        if (argc != 2)
        {
                printf("Skladnia: %s plik\n", argv[0]);
                exit(1);
        }
        if ((dfile = open(argv[1], O_RDONLY)) == -1)
        {
                printf("Blad otwarcia pliku: %s\n", argv[1]);
                exit(2);
        }
        sprintf(tmpfile, "tmp%d", getpid());
        if ((dtmp = creat(tmpfile, PERM)) == -1)
        {
                printf("Blad utworzenia pliku: %s\n", tmpfile);
                exit(2);
        }
        if ((ptr = lseek(dfile, -1L, 2)) == -1)
        {
                exit(3);
        }
        while (read(dfile, &c, 1) == 1)
        {
                write(dtmp, &c, 1);
                if (ptr == 0)
                        break;
                if ((ptr = lseek(dfile, -2L, 1)) == -1)
                {
                        exit(3);
                }
        }
        if (close(dfile) == -1)
        {
                printf("Blad zamkniecia pliku: %s\n", argv[1]);
                exit(4);
        }
        if (close(dtmp) == -1)
        {
                printf("Blad zamkniecia pliku: %s\n", tmpfile);
                exit(4);
        }
        unlink(argv[1]);
        link(tmpfile, argv[1]);
        unlink(tmpfile);
}
 

(4.2) Pobieranie i modyfikacja atrybutów pliku
</p>

Atrybuty pliku zapisane w i-węźle można pobrać jedną z trzech funkcji systemowych:

int stat(const char *file_name, struct stat *buf);
int lstat(const char *file_name, struct stat *buf);
int fstat(int filedes, struct stat *buf);
gdzie:
file_name - nazwa ścieżkowa pliku,
filedes - deskryptor pliku,
buf - wskaźnik do struktury stat, w której zostaną zapisane atrybuty pliku.

Funkcje odczytują zawartość i-węzła wskazanego pliku i zapisują w buforze. Plik może być wskazany przez nazwę w funkcjach stat() i lstat() lub przez numer deskryptora otwartego pliku w funkcji fstat(). Proces nie musi posiadać żadnych uprawnień do pliku, ale musi posiadać prawo przeglądania wszystkich katalogów podanych w nazwie ścieżkowej pliku.

Funkcja lstat() pozwala odczytać atrybuty dowiązań symbolicznych, podczas gdy stat() podąża za dowiązaniem i operuje na pliku wskazywanym.

Funkcje zapisują informacje o pliku w strukturze stat zdefiniowanej następująco:

`struct stat {`
`dev_t         st_dev;` - nazwa urządzenia, na którym plik jest zapisany
`ino_t         st_ino;` - numer i-węzła
`mode_t        st_mode;` - tryb pliku
`nlink_t       st_nlink;` - liczba dowiązań
`uid_t         st_uid;` - identyfikator właściciela UID
`gid_t         st_gid;` - identyfikator grupy GID
`dev_t         st_rdev;` - typ urządzenia dla plików specjalnych
`off_t         st_size;` - rozmiar pliku w bajtach
`unsigned long st_blksize;` - zalecany rozmiar bloku dla operacji wejścia/wyjścia
`unsigned long st_blocks;` - liczba zajmowanych bloków dyskowych
`time_t        st_atime;` - czas ostatniego dostępu do pliku
`time_t        st_mtime;` - czas ostatniej modyfikacji zawartości
`time_t        st_ctime;` - czas ostatniej zmiany atrybutów
`}`

Wartości atrybutów pliku ulegają zmianie w wyniku działania różnych funkcji systemowych, np. dopisanie danych do pliku powoduje zmianę rozmiaru i daty modyfikacji. Niektóre atrybuty można zmieniać wprost posługując się specjalnymi funkcjami systemowymi. Dotyczy to w szczególności identyfikatorów użytkowników i praw dostępu do pliku.

Zmianę identyfikatorów umożliwia jedna z poniższych funkcji:

int chown(const char *path, uid_t owner, gid_t group);
int lchown(const char *path, uid_t owner, gid_t group);
int fchown(int fd, uid_t owner, gid_t group);
gdzie:
path - nazwa ścieżkowa pliku,
fd - deskryptor otwrtego pliku,
owner - identyfikator właściciela,
group - identyfikator grupy.

Wyłącznie administrator systemu (użytkownik root) może zmienić dowolnie obydwa identyfikatory. Właściciel pliku może jedynie ustawić numer jednej z grup, do których sam należy. Różnice w działaniu poszczególnych funkcji są analogiczne jak w przypadku funkcji stat().

Proces chcący określić swoje uprawnienia do pliku musi zinterpretować wartości identyfikatorów i trybu pliku, odczytane funkcją stat(). Jest to operacja dość kłopotliwa i czasochłonna dla twórcy programu. System udostępnia więc funkcję access() dającą możliwość prostego sprawdzania uprawnień do pliku.

int access(const char *pathname, int mode);
gdzie:
pathname - nazwa ścieżkowa pliku,
mode - maska uprawnień, które mają być sprawdzone.

Argument mode może przyjąć jedną lub kilka z następujących wartości:

F_OK - sprawdza, czy plik istnieje,
R_OK - sprawdza, czy plik istnieje oraz czy proces posiada prawo do czytania z pliku,
W_OK - sprawdza, czy plik istnieje oraz czy proces posiada prawo do pisania do pliku,
X_OK - sprawdza, czy plik istnieje oraz czy proces posiada prawo do wykonywania pliku.

Funkcja zwraca 0, jeśli wyspecyfikowane uprawnienia są nadane.

Prawa dostępu oraz ich modyfikatory można zmieniać funkcją systemową chmod().

int chmod(const char *path, mode_t mode);
int fchmod(int fildes, mode_t mode);
gdzie:
pathname - nazwa ścieżkowa pliku,
fildes - deskryptor otwartego pliku,
mode - tryb dostępu do pliku.

Tryb podaje się w kodzie ósemkowym lub w postaci symbolicznej, wykorzystując zestaw zdefiniowanych stałych symbolicznych. Pełny zestaw stałych można odnależć w dokumentacji elektronicznej. Zmian może dokonać proces z identyfikatorem obowiązującym właściciela pliku lub administratora.

Pliki tworzone przez proces mają ustawiany tryb dostępu na podstawie atrybutu umask procesu. Atrybut definiuje maskę praw dostępu wskazującą, które bity praw dostępu mają być wyłączone. Proces może zmienić swoją maskę wywołując funkcję systemową:

mode_t umask(mode_t mask);
gdzie:
mask - maska praw dostępu w kodzie ósemkowym.

Funkcja zwraca starą wartość maski i ustawia nową.

(4.3) Operacje na deskryptorach

Większość funkcji systemowych operujących na plikach odnosi się do ich fizycznej reprezentacji w postaci i-węzła i bloków danych. Funkcje te mogą odczytywać lub modyfikować zawartość pliku albo jego atrybuty zapisane w i-węźle. Wszystkie zmiany stają się widoczne dla innych procesów korzystających z pliku.

Niektóre funkcje dają procesom możliwość modyfikacji tylko tych własności, które nie są na trwałe związane z plikiem, a jedynie określają sposób dostępu procesu do otwartego pliku. Funkcje te operują na strukturach file otwartych plików wskazywanych przez deskryptory. Powszechnie przyjęło się określać w skrócie, że funkcje wykonują operacje na deskryptorach plików.

Funkcja sterująca fcntl() umożliwia wykonywanie różnorodnych operacji na deskryptorach, które mogą dotyczyć między innymi:

  • odczytania lub ustawienia flag (znaczników), określających np. trybu dostępu do otwartego pliku,
  • powielania deskryptorów,
  • blokowania rekordów w pliku,
  • asynchronicznych operacji wejścia - wyjścia sterowanych sygnałem SIGIO.
Omówione tu zostaną tylko dwa pierwsze z wymienionych zagadnień. Pozostałe zagadnienia wykraczają poza tematykę tego podręcznika a ich omówienie można znaleźć w [ ].
int fcntl(int fd, int cmd);
int fcntl(int fd, int cmd, long arg);
gdzie:
fd - deskryptor otwartego pliku,
cmd - operacja na deskryptorze,
arg - argument wymagany przez niektóre operacje.

Argument cmd może przyjąć jedną z wielu wartości zdefiniowanych w postaci stałych, takich jak:

F_GETFL - odczytuje wartości wszystkich flag otwartego pliku,
F_SETFL - ustawia wartości flag na podstawie argumentu arg,
F_DUPFD - powiela deskryptor otwartego pliku.

Flagi określające tryb dostępu ustalane są podczas otwierania pliku funkcją open(). Specyfikuje się wtedy, czy plik ma być otwarty do czytania, pisania lub dopisywania. Domyślnie ustawiany jest też tryb operacji blokujących proces do momentu zakończenia operacji. Tylko niektóre flagi mogą być ustawiane funkcją fcntl() już po otwarciu pliku:

O_APPEND - określająca tryb dopisywania do pliku,
O_NONBLOCK - określająca tryb operacji bez blokowania,
O_ASYNC - określająca tryb operacji asynchronicznych.

Proces ma możliwość uzyskania nowego deskryptora do otwartego pliku poprzez powielenie aktualnego deskryptora. Taką możliwość zapewnia funkcja fcntl() oraz dwie inne funkcje systemowe dup() i dup2(). Każda z funkcji zwraca nowy deskryptor wskazujący ten sam otwarty plik, co stary deskryptor. W tablicy fd[] otwartych plików procesu, funkcja tworzy kopię wskaźnika do struktury file pliku określonego przez stary deskryptor. Nowy deskryptor jest indeksem nowego wskaźnika w tablicy. Obydwa deskryptory mogą być odtąd używane zamiennie w dostępie do pliku.

Funkcja fcntl() powinna być wywołana z poleceniem F_DUPFD i argumentem określającym minimalny numer nowego deskryptora np.:

fcntl(oldfd, F_DUPFD, newfd);

Proces może również wykorzystać jedną z pozostał funkcji do powielania deskryptorów:

int dup(int oldfd);
int dup2(int oldfd, int newfd);
gdzie:
oldfd - stary deskryptor związany z otwartym plikiem,
newfd - nowy deskryptor pliku.

Funkcja dup() przydziela najmniejszy deskryptor aktualnie destępny w procesie i zwraca jego wartość. Nie daje więc możliwosci wyboru konkretnego numeru nowego deskryptora pliku. Często jednak proces chce uzyskać konkretny deskryptor, na przykład deskryptor 1 w celu przekierowania do pliku standardowego strumienia wyjściowego (stdout). Powinien w tym celu zamknąć plik związany z tym deskryptorem, a następnie użyć funkcji systemowej dup2(), która pozwala wskazać nowy deskryptor pliku newfd. Jeśli wskazany deskryptor jest już używany, to zostanie najpierw zamknięty.

(4.4) Operacje na dowiązaniach

Operacje na dowiązaniach (dowiązaniach twardych) są w istocie operacjami na zawartości katalogu. Sprowadzają się do dodania lub usunięcia pozycji w katalogu albo na zmianie nazwy istniejącej pozycji. Z tego względu nie są wymagane żadne uprawnienia do plików wskazywanych przez dowiązania, a jedynie do katalogów, których dane zmieniamy.

Funkcja systemowa link() umożliwia tworzenie nowych dowiązań do istniejącego pliku.

int link(const char *oldpath, const char *newpath);
gdzie:
oldpath - nazwa ścieżkowa istniejącego dowiązania do pliku,
newpath - nazwa ścieżkowa nowego dowiązania do pliku.

Nowa nazwa newpath musi dotyczyć tego samego systemu plików (tej samej partycji logicznej). Jeśli istnieje już dowiązanie o podanej nazwie, to nie zostanie zmienione. W wyniku pomyślnego zakończenia funkcji plik będzie widoczny w strukturze katalogowej pod dwiema (lub więcej) nazwami.

Funkcja systemowa rename() zmienia nazwę w istniejącym dowiązaniu albo usuwa dowiązanie w jednym katalogu i tworzy w innym nowe dowiązanie do tego samego pliku.

int rename(const char *oldpath, const char *newpath);
Funkcja zmienia tylko jedno dowiązanie do pliku, wyspecyfikowane w wywołaniu. Pozostałe dowiązania, jeśli istnieją, nie ulegają zmianie.

Każde dowiązanie można usunąć funkcją systemową unlink().

int unlink(const char *pathname);
Operacja powoduje zmniejszenie wartości licznika dowiązań w i-węźle pliku i nie ma wpływu na pozostałe dowiązania do pliku. Po usunięciu ostatniego dowiązania i wyzerowaniu licznika plik również zostaje usunięty przez jądro systemu.

Funkcja symlink() umożliwia stworzenie dowiązania symbolicznego do istniejącego pliku.

int symlink(const char *oldpath, const char *newpath);
gdzie:
oldpath - nazwa ścieżkowa istniejącego dowiązania do pliku,
newpath - nazwa ścieżkowa nowego dowiązania symbolicznego do pliku.

W wyniku działania funkcji powstaje nowy plik typu dowiązanie symboliczne wskazujący nazwę ścieżkową innego pliku dokładnie w takiej postaci, w jakiej została podana w argumencie oldpath. Powstaje również zwykłe dowiązanie do nowego pliku w katalogu wynikającym z nazwy ścieżkowej newpath (w katalogu bieżącym, jeśli newpath zawiera tylko nazwę pliku). Nowa nazwa może dotyczyć innego systemu plików (innej partycji), ponieważ dowiązanie symboliczne wskazuje na nazwę a nie na i-węzeł.


(4.5) Zwielokrotnianie wejścia/wyjścia

Procesy często korzystają z wielu źródeł danych i w związku z tym są zmuszone obsługiwać wiele deskryptorów otwartych plików. Źródłem danych mogą być pliki urządzeń, np. plik terminala, łącza komunikacyjne lub gniazda. Jeżeli nowe dane nadchodzą w nieustalonej kolejności, to pojawia się problem właściwej obsługi wszystkich otwartych plików. Istnieją dwie podstawowe metody rozwiązania tego problemu:

  • aktywne czekanie, polegające na sekwencyjnym sprawdzaniu kolejno wszystkich otwartych plików,
  • wstrzymanie działania procesu w oczekiwaniu na pojawienie się nowych danych w którymkolwiek z otwartych plików.
Metoda aktywnego czekania wymaga użycia operacji nieblokujących, aby wykluczyć możliwość wstrzymania procesu przy próbie odczytania nowych danych z pliku. Istotną wadą tej metody jest duże obciążenie systemu przez stale działający proces, który wciąż usiłuje odczytywać dane.

Zastosowanie drugiej metody umożliwia funkcja select(). Wstrzymuje ona wykonywanie procesu w oczekiwaniu na zmianę statusu podanych deskryptorów otwartych plików.

int select(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
gdzie:
n - liczba deskryptorów do sprawdzenia, obliczana jako najwyższy numer użytego deskryptora plus 1,
readfds - zbiór deskryptorów sprawdzanych pod kątem możliwości odczytania danych,
writefds - zbiór deskryptorów sprawdzanych pod kątem możliwości zapisania danych,
exceptfds - zbiór deskryptorów sprawdzanych pod kątem nieobsłużonych sytuacji wyjątkowych,
timeout - struktura opisująca maksymalny czas oczekiwania funkcji select() na zajście jakiegoś zdarzenia.

Deskryptory pogrupowane są w trzy zbiory typu fd_set w zależności od rodzaju zdarzenia, na które proces chce oczekiwać.

Operacje na zbiorze deskryptorów ułatwia zestaw następujących makrodefinicji:

FD_ZERO(fd_set *set); - zeruje zbiór deskryptorów,
FD_SET(int fd, fd_set *set); - ustawia podany deskryptor w zbiorze,
FD_CLR(int fd, fd_set *set); - usuwa podany deskryptor ze zbioru,
FD_ISSET(int fd, fd_set *set); - sprawdza, czy podany deskryptor jest ustawiony.

5. Operacje na katalogach

(5.1) Zmiana katalogu głównego i katalogu bieżacego procesu

W każdym procesie zdefiniowane są dwa atrybuty okrelające położenie w strukturze katalogowej systemu:

  • katalog główny procesu,
  • katalog bieżący procesu.
W systemie plików istnieje jeden katalog główny /. Proces może jednak wybrać inny katalog, do którego będzie się odnosił przez nazwę / i traktował jako katalog główny systemu. Wszystkie odwołania procesu poprzez ścieżkę bezwzględną będą interpretowane względem nowego katalogu głównego, przez co proces uzyska dostęp ograniczony tylko do określonego poddrzewa struktury katalogowej.

Zdefiniowanie nowego katalogu głównego w procesie umożliwia funkcja systemowa:

int chroot(const char *path);
Funkcja ustala nowy katalog główny ale nie zmienia katalogu bieżącego procesu. Może więc dojść do sytuacji, gdy katalog bieżący procesu nie leży wewnątrz jego katalogu głównego. Proces powinien wtedy zmienić bieżący katalog roboczy przy pomocy funkcji systemowej chdir().
int chdir(const char *path);
Nazwę bieżącego katalogu można uzyskać funkcją biblioteczną:
char *getcwd(char *buf, size_t size);
gdzie:
buf - wskaźnik do bufora przeznaczonego na nazwę katalogu,
size - rozmiar bufora.

Funkcja zapisuje nazwę katalogu zakończoną znakiem '\0' do bufora i zwraca wskaźnik do tego bufora. Jeżeli długość nazwy przekracza rozmiar size - 1, to funkcja zwraca błąd. Dopuszczalne jest użycie argumentu NULL zamiast wskaźnika do bufora, aby zezwolić funkcji na dynamiczne zarezerwowanie bufora odpowiednich rozmiarów.


(5.2) Tworzenie i usuwanie katalogów

Funkcja mkdir() umożliwia utworzenie katalogu z podanymi prawami dostępu zmodyfikowanymi przez maskę procesu.

int mkdir(const char *pathname, mode_t mode);
gdzie:
pathname - nazwa ścieżkowa katalogu,
mode - prawa dostępu do katalogu.

Do usuwania pustych katalogów służy funkcja rmdir().

int rmdir(const char *pathname);

Argumentem funkcji nie mogą być pozycje . i .. ani niepusty katalog.

(5.3) Przeglądanie zawartości katalogów
</p>

Katalogi są reprezentowane tak samo, jak zwykłe pliki. Inna powinna być tylko interpretacja zawartosci ich bloków danych. W zasadzie można więc używać funkcji systemowych open(), read() i close() do odczytania zawartości katalogu i właściwie zinterpretować odczytane dane. Zaleca się jednak korzystanie z zestawu funkcji zdefiniowanych w bibliotece języka C.

Funkcja opendir() otwiera katalog wyłącznie do czytania i zwraca wskaźnik do struktury DIR, reprezentującej otwarty katalog. Nie ma możliwości otwarcia katalogu do pisania.

DIR *opendir(const char *name);
Zwrócony wskaźnik identyfikuje otwarty katalog w wywołaniach pozostałych funkcji bibliotecznych.

Zawartość katalogu odczytywana jest sekwencyjnie pozycja po pozycji przy pomocy funkcji readdir().

struct dirent *readdir(DIR *dir);
Funkcja odczytuje jedną pozycję z katalogu i zapisuje w strukturze dirent opisanej powyżej.. Jednocześnie ustawia wskaźnik na następną pozycję w katalogu, która zostanie odczytana w następnym wywołaniu funkcji.

Aktualne położenie wskaźnika można uzyskać funkcją telldir() a zmianę tego położenia umożliwia funkcja seekdir().

off_t telldir(DIR *dir);
void seekdir(DIR *dir, off_t offset);
Argument offset określa przesunięcie względem bieżącej pozycji wskaźnika i powinno stanowić wielokrotnosć rozmiaru struktury dirent.

Funkcja rewinddir() przestawia wskaźnik na początek katalogu umożliwiając ponowne odczytanie pierwszej pozycji.

void rewinddir(DIR *dir);
Po zakończeniu czytania proces wywołuje funkcję closedir() w celu zamknięcia katalogu.
int closedir(DIR *dir);

Autorami powyższego artykułu są mgr inż. A. Wielgus i dr Z. Jaworski. Drobnych zmian i poprawek dokonał Ł. Fronczyk
W dziale źródła/C++ znajdują się przykładowe programy do tego i pozostałych artykułów z tego cyklu.
Wszelkie pytania proszę kierować na adres [email protected]

0 komentarzy