Co faktycznie robi otwarcie pliku?

266

We wszystkich językach programowania (których przynajmniej używam) musisz otworzyć plik, zanim będziesz mógł go odczytać lub napisać.

Ale co właściwie robi ta otwarta operacja?

Strony podręcznika dla typowych funkcji tak naprawdę nie mówią nic poza tym, że „otwiera plik do odczytu / zapisu”:

http://www.cplusplus.com/reference/cstdio/fopen/

https://docs.python.org/3/library/functions.html#open

Oczywiście poprzez użycie funkcji można powiedzieć, że wiąże się to z utworzeniem pewnego rodzaju obiektu, który ułatwia dostęp do pliku.

Innym sposobem umieszczenia tego byłoby, gdybym zaimplementował openfunkcję, co musiałby zrobić w systemie Linux?

jramm
źródło
13
Edycja tego pytania, aby skupić się Ci Linux; ponieważ to, czym różnią się Linux i Windows. W przeciwnym razie jest nieco za szeroki. Ponadto każdy język wyższego poziomu wywoła albo C API dla systemu, albo skompiluje do C w celu wykonania, więc pozostawienie na poziomie „C” stawia go na poziomie najmniej wspólnego mianownika.
George Stocker
1
Nie wspominając o tym, że nie wszystkie języki programowania mają tę funkcję, lub jest to funkcja, która jest wysoce zależna od środowiska. Oczywiście, co prawda obecnie rzadko, ale do dziś obsługa plików jest całkowicie opcjonalną częścią ANSI Forth i nie była nawet obecna w niektórych implementacjach w przeszłości.

Odpowiedzi:

184

W prawie każdym języku wysokiego poziomu funkcja otwierająca plik jest otoczona odpowiednim wywołaniem systemowym jądra. Może także robić inne wymyślne rzeczy, ale we współczesnych systemach operacyjnych otwieranie pliku musi zawsze przechodzić przez jądro.

Dlatego argumenty fopenfunkcji bibliotecznej lub Python są openbardzo podobne do argumentów open(2)wywołania systemowego.

Oprócz otwierania pliku funkcje te zwykle ustawiają bufor, który będzie konsekwentnie używany z operacjami odczytu / zapisu. Celem tego bufora jest zapewnienie, że ilekroć chcesz odczytać N bajtów, odpowiednie wywołanie biblioteki zwróci N bajtów, niezależnie od tego, czy wywołania bazowych wywołań systemowych zwracają mniej.

W rzeczywistości nie jestem zainteresowany wdrażaniem własnej funkcji; po prostu rozumiejąc, co się do cholery dzieje… „jeśli nie chcesz”.

W systemach operacyjnych typu Unix udane wywołanie openzwraca „deskryptor pliku”, który jest jedynie liczbą całkowitą w kontekście procesu użytkownika. W konsekwencji ten deskryptor jest przekazywany do każdego wywołania, które wchodzi w interakcję z otwartym plikiem, a po wywołaniu closedeskryptor traci ważność.

Należy zauważyć, że wezwanie do opendziałania działa jak punkt sprawdzania poprawności, w którym przeprowadzane są różne kontrole. Jeśli nie wszystkie warunki są spełnione, wywołanie kończy się niepowodzeniem przez powrót -1zamiast deskryptora, a rodzaj błędu jest wskazany w errno. Niezbędne kontrole to:

  • Czy plik istnieje;
  • Określa, czy proces wywołujący ma uprawnienia do otwierania tego pliku w określonym trybie. Jest to określane poprzez dopasowanie uprawnień do pliku, identyfikatora właściciela i identyfikatora grupy do odpowiednich identyfikatorów procesu wywołującego.

W kontekście jądra musi istnieć pewnego rodzaju mapowanie między deskryptorami plików procesu a fizycznie otwieranymi plikami. Wewnętrzna struktura danych odwzorowana na deskryptor może zawierać jeszcze inny bufor, który obsługuje urządzenia oparte na blokach, lub wewnętrzny wskaźnik wskazujący bieżącą pozycję odczytu / zapisu.

Blagovest Buyukliev
źródło
2
Warto zauważyć, że w systemach uniksopodobnych deskryptory plików struktury jądra są odwzorowywane, nazywane są „otwartym opisem pliku”. Tak więc FD procesu są mapowane na OFD jądra. Ważne jest, aby zrozumieć dokumentację. Na przykład zobacz man dup2i sprawdź subtelność między otwartym deskryptorem pliku (to jest FD, który jest otwarty) a otwartym opisem pliku (OFD).
rodrigo,
1
Tak, uprawnienia są sprawdzane w czasie otwartym. Możesz przeczytać i przeczytać źródło „otwartej” implementacji jądra : lxr.free-electrons.com/source/fs/open.c, chociaż większość pracy przekazuje do konkretnego sterownika systemu plików.
pjc50,
1
(w systemach ext2 będzie to wymagało odczytania pozycji katalogu w celu ustalenia, w którym i-węźle znajdują się metadane, a następnie załadowania tego i-węzła do pamięci podręcznej i-węzłów. Uwaga: mogą istnieć systemy pseudofile, takie jak „/ proc” i „/ sys”, które mogą wykonywać dowolne czynności po otwarciu pliku)
pjc50
1
Zauważ, że otwarte są kontrole pliku - że plik istnieje, że masz uprawnienia - w praktyce są niewystarczające. Plik może zniknąć lub jego uprawnienia mogą ulec zmianie pod stopami. Niektóre systemy plików próbują temu zapobiec, ale dopóki twój system operacyjny obsługuje pamięć sieciową, nie da się temu zapobiec (system operacyjny może „panikować”, jeśli lokalny system plików źle zachowuje się i jest rozsądny: taki, który robi to, gdy udział sieciowy nie jest realny system operacyjny). Te kontrole są również przeprowadzane przy otwartym pliku, ale muszą (skutecznie) być wykonywane przy każdym dostępie do innych plików.
Jak - Adam Nevraumont,
2
Nie zapomnij o ocenie i / lub tworzeniu zamków. Mogą być udostępniane lub wyłączne i mogą wpływać na cały plik lub tylko jego część.
Thinkeye,
83

Proponuję rzucić okiem na ten przewodnik po uproszczonej wersji open()wywołania systemowego . Używa następującego fragmentu kodu, który reprezentuje to, co dzieje się za scenami po otwarciu pliku.

0  int sys_open(const char *filename, int flags, int mode) {
1      char *tmp = getname(filename);
2      int fd = get_unused_fd();
3      struct file *f = filp_open(tmp, flags, mode);
4      fd_install(fd, f);
5      putname(tmp);
6      return fd;
7  }

W skrócie, oto, co robi ten kod, wiersz po wierszu:

  1. Przydziel blok pamięci kontrolowanej przez jądro i skopiuj do niej nazwę pliku z pamięci kontrolowanej przez użytkownika.
  2. Wybierz nieużywany deskryptor pliku, który możesz traktować jako indeks liczb całkowitych, do rozwijalnej listy aktualnie otwartych plików. Każdy proces ma swoją własną listę, chociaż jest obsługiwany przez jądro; twój kod nie może uzyskać do niego bezpośredniego dostępu. Wpis na liście zawiera wszelkie informacje, których użyje system plików do pobrania bajtów z dysku, takie jak numer i-węzła, uprawnienia do przetwarzania, otwarte flagi i tak dalej.
  3. filp_openFunkcja ma realizację

    struct file *filp_open(const char *filename, int flags, int mode) {
            struct nameidata nd;
            open_namei(filename, flags, mode, &nd);
            return dentry_open(nd.dentry, nd.mnt, flags);
    }
    

    która robi dwie rzeczy:

    1. Użyj systemu plików, aby wyszukać i-węzeł (lub bardziej ogólnie, niezależnie od rodzaju wewnętrznego identyfikatora używanego przez system plików) odpowiadającego podanej nazwie pliku lub ścieżce.
    2. Utwórz struct filez podstawowymi informacjami na temat i-węzła i zwróć go. Ta struktura staje się wpisem na liście otwartych plików, o której wspomniałem wcześniej.
  4. Przechowuj („instaluj”) zwróconą strukturę na liście otwartych plików procesu.

  5. Zwolnij przydzielony blok pamięci kontrolowanej przez jądro.
  6. Zwraca deskryptor pliku, które następnie mogą być przekazywane do funkcji obsługi plików jak read(), write()i close(). Każdy z nich przekaże kontrolę jądra, które może użyć deskryptora pliku, aby wyszukać odpowiedni wskaźnik pliku na liście procesu i użyć informacji w tym wskaźniku pliku, aby faktycznie wykonać odczyt, zapis lub zamknięcie.

Jeśli masz ambicję, możesz porównać ten uproszczony przykład do implementacji open()wywołania systemowego w jądrze Linuksa, funkcji o nazwie do_sys_open(). Nie powinieneś mieć problemów ze znalezieniem podobieństw.


Oczywiście jest to tylko „górna warstwa” tego, co dzieje się podczas wywoływania open()- a ściślej, jest to najwyższy poziom kodu jądra, który jest wywoływany podczas otwierania pliku. Język programowania wysokiego poziomu może dodać do tego dodatkowe warstwy. Na niższych poziomach dzieje się wiele. (Podziękowania dla Ruslana i pjc50 za wyjaśnienie.) Z grubsza, od góry do dołu:

  • open_namei()i dentry_open()wywołać kod systemu plików, który jest również częścią jądra, aby uzyskać dostęp do metadanych i zawartości plików i katalogów. System plików odczytuje surowe bajty z dysku i interpretuje te wzorce bajtów jako drzewo plików i katalogów.
  • System plików używa warstwy urządzenia blokowego , ponownie części jądra, aby uzyskać te nieprzetworzone bajty z dysku. (Ciekawostka: Linux umożliwia dostęp do surowych danych z warstwy urządzeń blokowych przy użyciu /dev/sdaitp.)
  • Warstwa urządzenia blokowego wywołuje sterownik urządzenia pamięci masowej, który jest również kodem jądra, w celu przetłumaczenia instrukcji średniego poziomu, takich jak „odczyt sektora X” na indywidualne instrukcje wejścia / wyjścia w kodzie maszynowym. Istnieje kilka rodzajów sterowników urządzeń pamięci masowej, w tym IDE , (S) ATA , SCSI , Firewire itd., Odpowiadające różnym standardom komunikacji, z których może korzystać dysk. (Pamiętaj, że nazywanie to bałagan).
  • Instrukcje We / Wy wykorzystują wbudowane możliwości układu procesora i kontrolera płyty głównej do wysyłania i odbierania sygnałów elektrycznych na przewodzie prowadzącym do napędu fizycznego. To jest sprzęt, a nie oprogramowanie.
  • Na drugim końcu drutu oprogramowanie układowe dysku (wbudowany kod sterujący) interpretuje sygnały elektryczne w celu obracania talerzy i poruszania głowicami (HDD), odczytu komórki Flash ROM (SSD) lub innych elementów niezbędnych do uzyskania dostępu do danych na tego rodzaju urządzenie pamięci masowej.

Może to być również nieco niepoprawne z powodu buforowania . :-P Poważnie, jednak pominęłem wiele szczegółów - osoba (nie ja) mogłaby napisać wiele książek opisujących, jak działa cały ten proces. Ale to powinno dać ci pomysł.

David Z
źródło
67

Każdy system plików lub system operacyjny, o którym chcesz rozmawiać, jest dla mnie w porządku. Miły!


W przypadku ZX Spectrum zainicjowanie LOADpolecenia wprowadzi system w ścisłą pętlę, odczytując linię Audio In.

Początek danych jest sygnalizowany stałym tonem, a następnie następuje sekwencja długich / krótkich impulsów, przy czym krótki impuls jest dla binarnego, 0a dłuższy dla binarnego 1( https://en.wikipedia.org/ wiki / ZX_Spectrum_software ). Ciasna pętla obciążenia zbiera bity, aż wypełni bajt (8 bitów), zapisuje to w pamięci, zwiększa wskaźnik pamięci, a następnie zapętla z powrotem, aby wyszukać więcej bitów.

Zazwyczaj pierwszą rzeczą, którą czytnik powinien przeczytać, jest krótki nagłówek o stałym formacie , wskazujący co najmniej oczekiwaną liczbę bajtów i ewentualnie dodatkowe informacje, takie jak nazwa pliku, typ pliku i adres ładowania. Po przeczytaniu tego krótkiego nagłówka program może zdecydować, czy kontynuować ładowanie głównej części danych, czy zakończyć procedurę ładowania i wyświetlić odpowiedni komunikat dla użytkownika.

Stan końca pliku można rozpoznać po otrzymaniu tylu bajtów, ile się spodziewa (stałej liczby bajtów zapisanych w oprogramowaniu lub zmiennej liczby, takiej jak wskazana w nagłówku). Zgłoszono błąd, jeśli pętla obciążeniowa nie otrzymywała impulsu w oczekiwanym zakresie częstotliwości przez określony czas.


Małe tło do tej odpowiedzi

Opisana procedura ładuje dane ze zwykłej taśmy audio - stąd potrzeba przeskanowania wejścia audio (jest to połączone ze standardową wtyczką do magnetofonu). LOADKomenda jest technicznie taka sama jak openpliku - ale to fizycznie przywiązany do rzeczywiście ładuje plik. Wynika to z faktu, że magnetofon nie jest kontrolowany przez komputer i nie można (pomyślnie) otworzyć pliku, ale nie można go załadować.

Wspomniano o „ciasnej pętli”, ponieważ (1) procesor, Z80-A (jeśli pamięć służy), był naprawdę wolny: 3,5 MHz i (2) spektrum nie miało wewnętrznego zegara! Oznacza to, że musiał dokładnie liczyć stany T (czasy instrukcji) dla każdego. pojedynczy. instrukcja. wewnątrz tej pętli, aby zachować dokładny czas sygnału dźwiękowego.
Na szczęście ta niska prędkość procesora miała tę wyraźną zaletę, że można było obliczyć liczbę cykli na kawałku papieru, a tym samym czas rzeczywisty, jaki by to zajęło.

usr2564301
źródło
10
@BillWoodger: no tak. Ale to uczciwe pytanie (mam na myśli twoje). Głosowałem za zamknięciem jako „zbyt szeroki”, a moja odpowiedź ma zilustrować, jak bardzo szerokie jest to pytanie.
usr2564301
8
Myślę, że trochę za bardzo poszerzasz odpowiedź. ZX Spectrum miał polecenie OTWIERANIA, a to zupełnie różniło się od LOAD. I trudniejsze do zrozumienia.
rodrigo,
3
Nie zgadzam się również na zamknięcie pytania, ale bardzo podoba mi się twoja odpowiedź.
Enzo Ferber
23
Chociaż edytowałem moje pytanie, aby ograniczyć się do systemu operacyjnego Linux / Windows, próbując zachować je otwarte, ta odpowiedź jest całkowicie poprawna i przydatna. Jak stwierdzono w moim pytaniu: nie chcę niczego wdrażać ani nakłaniać innych do wykonywania mojej pracy, chcę się uczyć. Aby się uczyć, musisz zadawać „duże” pytania. Jeśli stale zamykamy pytania dotyczące SO za to, że są „zbyt szerokie”, ryzykujesz, że staniesz się miejscem, w którym po prostu zachęcisz ludzi do napisania twojego kodu bez podawania wyjaśnienia, gdzie, gdzie i dlaczego. Wolę zachować to miejsce, do którego mogę się uczyć.
jramm,
14
Ta odpowiedź wydaje się dowodzić, że twoja interpretacja pytania jest zbyt szeroka, a nie, że samo pytanie jest zbyt szerokie.
jwg
17

To zależy od systemu operacyjnego, co dokładnie dzieje się po otwarciu pliku. Poniżej opisuję, co dzieje się w Linuksie, ponieważ daje wyobrażenie o tym, co dzieje się po otwarciu pliku. Możesz sprawdzić kod źródłowy, jeśli jesteś zainteresowany bardziej szczegółowymi informacjami. Nie obejmuję uprawnień, ponieważ spowodowałoby to zbyt długą odpowiedź.

W systemie Linux każdy plik jest rozpoznawany przez strukturę o nazwie i- węzeł. Każda struktura ma unikalny numer, a każdy plik otrzymuje tylko jeden numer i-węzła. Ta struktura przechowuje metadane pliku, na przykład rozmiar pliku, uprawnienia do pliku, znaczniki czasu i wskaźnik do bloków dysku, ale nie samą nazwę pliku. Każdy plik (i katalog) zawiera wpis nazwy pliku i numer i-węzła do wyszukiwania. Po otwarciu pliku, przy założeniu, że masz odpowiednie uprawnienia, deskryptor pliku jest tworzony przy użyciu unikalnego numeru i-węzła związanego z nazwą pliku. Ponieważ wiele procesów / aplikacji może wskazywać na ten sam plik, i-węzeł ma pole łącza, które utrzymuje łączną liczbę łączy do pliku. Jeśli plik jest obecny w katalogu, jego liczba linków wynosi jeden, jeśli ma link twardy, jego liczba linków będzie wynosić dwa, a jeśli plik zostanie otwarty przez proces, liczba linków zostanie zwiększona o 1.

Alex
źródło
6
Co to ma wspólnego z rzeczywistym pytaniem?
Bill Woodger,
1
Opisuje, co dzieje się na niskim poziomie po otwarciu pliku w systemie Linux. Zgadzam się, że pytanie jest dość ogólne, więc może nie była to odpowiedź, której szukał jramm.
Alex
1
Więc znowu, nie sprawdzam uprawnień?
Bill Woodger,
11

Głównie księgowość. Obejmuje to różne kontrole, takie jak „Czy plik istnieje?” i „Czy mam uprawnienia do otwierania tego pliku do zapisu?”.

Ale to wszystko jądro - chyba że wdrażasz własny zabawkowy system operacyjny, nie ma w czym zagłębiać się (jeśli tak, baw się dobrze - to wspaniałe doświadczenie edukacyjne). Oczywiście nadal powinieneś nauczyć się wszystkich możliwych kodów błędów, które możesz otrzymać podczas otwierania pliku, abyś mógł sobie z nimi właściwie poradzić - ale są to zwykle ładne małe abstrakcje.

Najważniejszą częścią na poziomie kodu jest to, że daje on uchwyt do otwartego pliku, którego używasz do wszystkich innych operacji wykonywanych z plikiem. Nie możesz użyć nazwy pliku zamiast tego dowolnego uchwytu? Cóż, jasne - ale użycie uchwytu daje pewne korzyści:

  • System może śledzić wszystkie otwarte pliki i zapobiegać ich usuwaniu (na przykład).
  • Nowoczesne systemy operacyjne są zbudowane wokół uchwytów - istnieje mnóstwo przydatnych rzeczy, które możesz zrobić z uchwytami, a wszystkie rodzaje uchwytów zachowują się prawie identycznie. Na przykład, gdy asynchroniczna operacja we / wy zakończy się na uchwycie pliku Windows, uchwyt jest sygnalizowany - pozwala to zablokować uchwyt, dopóki nie zostanie zasygnalizowany lub zakończyć operację całkowicie asynchronicznie. Oczekiwanie na uchwyt pliku jest dokładnie takie samo, jak oczekiwanie na uchwyt wątku (sygnalizowany np. Po zakończeniu wątku), uchwyt procesowy (ponownie sygnalizowany, gdy proces się kończy) lub gniazdo (po zakończeniu jakiejś operacji asynchronicznej). Co równie ważne, uchwyty należą do ich odpowiednich procesów, więc gdy proces zostanie nieoczekiwanie zakończony (lub aplikacja jest źle napisana), system operacyjny wie, jakie uchwyty może zwolnić.
  • Większość operacji ma charakter pozycyjny - ty readod ostatniej pozycji w twoim pliku. Używając uchwytu do identyfikacji konkretnego „otwarcia” pliku, możesz mieć wiele współbieżnych uchwytów do tego samego pliku, każdy odczytując z ich własnych miejsc. W pewien sposób uchwyt działa jako ruchome okno do pliku (i sposób wydawania asynchronicznych żądań We / Wy, które są bardzo przydatne).
  • Uchwyty są znacznie mniejsze niż nazwy plików. Uchwyt ma zwykle rozmiar wskaźnika, zwykle 4 lub 8 bajtów. Z drugiej strony nazwy plików mogą mieć setki bajtów.
  • Uchwyty pozwalają systemowi przenieść plik, nawet jeśli aplikacje go otwierają - uchwyt jest nadal ważny i nadal wskazuje ten sam plik, nawet jeśli nazwa pliku uległa zmianie.

Istnieją również inne sztuczki, które możesz zrobić (na przykład współużytkować uchwyty między procesami, aby mieć kanał komunikacyjny bez użycia pliku fizycznego; w systemach uniksowych pliki są również używane dla urządzeń i różnych innych kanałów wirtualnych, więc nie jest to absolutnie konieczne ), ale tak naprawdę nie są one związane z opensamą operacją, więc nie zamierzam się w to zagłębiać.

Luaan
źródło
7

U jej podstaw podczas otwierania się do czytania nic szczególnego nie musi się wydarzyć. Wystarczy sprawdzić, czy plik istnieje, a aplikacja ma wystarczające uprawnienia, aby go odczytać i utworzyć uchwyt, w którym można wydawać polecenia odczytu do pliku.

To na te polecenia zostanie wysłany faktyczny odczyt.

System operacyjny często uzyskuje przewagę w czytaniu, rozpoczynając operację odczytu w celu wypełnienia bufora związanego z uchwytem. Następnie, kiedy faktycznie czytasz, może natychmiast zwrócić zawartość bufora, zamiast czekać na IO dysku.

Aby otworzyć nowy plik do zapisu, system operacyjny będzie musiał dodać pozycję w katalogu dla nowego (obecnie pustego) pliku. I znowu tworzony jest uchwyt, na którym możesz wydawać polecenia zapisu.

maniak zapadkowy
źródło
5

Zasadniczo wywołanie open musi znaleźć plik, a następnie nagrać wszystko, czego potrzebuje, aby później operacje we / wy mogły go ponownie znaleźć. To dość niejasne, ale będzie to prawdą we wszystkich systemach operacyjnych, o których mogę natychmiast pomyśleć. Szczegóły różnią się w zależności od platformy. Wiele odpowiedzi już tutaj mówi o współczesnych komputerowych systemach operacyjnych. Zrobiłem małe programowanie na CP / M, więc zaoferuję swoją wiedzę na temat tego, jak to działa na CP / M (MS-DOS prawdopodobnie działa w ten sam sposób, ale ze względów bezpieczeństwa zwykle nie robi się tak dzisiaj ).

Na CP / M masz coś o nazwie FCB (jak wspomniałeś C, możesz to nazwać struct; to naprawdę jest 35-bajtowy ciągły obszar w pamięci RAM zawierający różne pola). FCB ma pola do zapisania nazwy pliku i (4-bitową) liczbę całkowitą identyfikującą napęd dyskowy. Następnie, wywołując Open File jądra, przekazujesz wskaźnik do tej struktury, umieszczając go w jednym z rejestrów procesora. Jakiś czas później system operacyjny powraca z nieznacznie zmienioną strukturą. Cokolwiek zrobisz we / wy dla tego pliku, przekażesz wskaźnik do tej struktury do wywołania systemowego.

Co CP / M robi z tym FCB? Zastrzega sobie pewne pola na własny użytek i wykorzystuje je do śledzenia pliku, więc lepiej nigdy nie dotykaj ich w programie. Operacja Otwórz plik przeszukuje tabelę na początku dysku w poszukiwaniu pliku o tej samej nazwie, co zawartość FCB (znak wieloznaczny „?” Pasuje do dowolnego znaku). Jeśli znajdzie plik, kopiuje pewne informacje do FCB, w tym fizyczną lokalizację pliku na dysku, tak aby kolejne wywołania We / Wy ostatecznie wywoływały system BIOS, który może przekazać te lokalizacje do sterownika dysku. Na tym poziomie szczegóły są różne.

OmarL
źródło
-7

Mówiąc prościej, po otwarciu pliku w rzeczywistości żądasz od systemu operacyjnego załadowania żądanego pliku (skopiuj zawartość pliku) z pamięci dodatkowej do pamięci RAM w celu przetworzenia. Powodem tego jest (ładowanie pliku), ponieważ nie można przetworzyć pliku bezpośrednio z dysku twardego ze względu na jego bardzo małą prędkość w porównaniu do Ram.

Polecenie open wygeneruje wywołanie systemowe, które z kolei kopiuje zawartość pliku z pamięci dodatkowej (dysku twardego) do pamięci podstawowej (pamięci RAM).

I „Zamykamy” plik, ponieważ zmodyfikowana zawartość pliku musi zostać odzwierciedlona w oryginalnym pliku znajdującym się na dysku twardym. :)

Mam nadzieję, że to pomaga.


źródło