Chociaż pytanie Przepełnienie stosu wydawało się na początku wystarczające, rozumiem z twoich komentarzy, dlaczego wciąż możesz mieć co do tego wątpliwości. Dla mnie jest to dokładnie taka sytuacja krytyczna, gdy komunikują się dwa podsystemy UNIX (procesy i pliki).
Jak zapewne wiesz, systemy UNIX są zwykle podzielone na dwa podsystemy: podsystem plików i podsystem procesów. Teraz, o ile nie zostanie wydane inne polecenie przez wywołanie systemowe, jądro nie powinno mieć interakcji między tymi dwoma podsystemami. Jest jednak jeden wyjątek: ładowanie pliku wykonywalnego do regionów tekstowych procesu . Oczywiście można argumentować, że ta operacja jest również wywoływana przez wywołanie systemowe ( execve
), ale zwykle wiadomo, że jest to jedyny przypadek, w którym podsystem procesu wysyła niejawne żądanie do podsystemu plików.
Ponieważ podsystem procesu naturalnie nie ma możliwości obsługi plików (w przeciwnym razie nie byłoby sensu dzielenia całej rzeczy na dwie części), musi korzystać z wszystkiego, co zapewnia podsystem plików, aby uzyskać dostęp do plików. Oznacza to również, że podsystem procesu jest poddawany wszelkim pomiarom, jakie podsystem plików podejmuje w odniesieniu do edycji / usuwania pliku. W tym miejscu poleciłbym przeczytanie odpowiedzi Gillesa na to pytanie dotyczące U&L . Reszta mojej odpowiedzi oparta jest na bardziej ogólnej odpowiedzi Gillesa.
Pierwszą rzeczą, na którą należy zwrócić uwagę jest to, że wewnętrznie pliki są dostępne tylko za pośrednictwem i- węzłów . Jeśli jądro otrzymuje ścieżkę, jego pierwszym krokiem będzie przełożenie go na i-węzeł, który będzie używany do wszystkich innych operacji. Kiedy proces ładuje plik wykonywalny do pamięci, robi to przez swój i-węzeł, który został dostarczony przez podsystem plików po przetłumaczeniu ścieżki. I-węzły mogą być powiązane z kilkoma ścieżkami (linkami), a programy mogą usuwać tylko linki. Aby usunąć plik i jego i-węzeł, użytkownik musi usunąć wszystkie istniejące łącza do tego i-węzła i upewnić się, że jest całkowicie nieużywany. Gdy te warunki zostaną spełnione, jądro automatycznie usunie plik z dysku.
Jeśli spojrzysz na część Gilles dotyczącą zastępowania plików wykonywalnych , zobaczysz, że w zależności od tego, jak edytujesz / usuwasz plik, jądro będzie reagować / dostosowywać się inaczej, zawsze poprzez mechanizm zaimplementowany w podsystemie plików.
- Jeśli spróbujesz zastosować strategię pierwszą ( open / truncate do zero / write lub open / write / truncate to new size ), zobaczysz, że jądro nie będzie kłopotać się obsługą twojego żądania. Pojawi się błąd 26: Plik tekstowy zajęty (
ETXTBSY
). Bez konsekwencji.
- Jeśli spróbujesz zastosować strategię drugą, pierwszym krokiem jest usunięcie pliku wykonywalnego. Ponieważ jednak jest używany przez proces, podsystem plików uruchomi się i zapobiegnie prawdziwemu usunięciu pliku (i jego i-węzła) z dysku. Od tego momentu jedynym sposobem na uzyskanie dostępu do zawartości starego pliku jest zrobienie tego za pomocą jego i-węzła, co robi podsystem procesu za każdym razem, gdy musi załadować nowe dane do sekcji tekstowych (wewnętrznie nie ma sensu używać ścieżek, z wyjątkiem podczas tłumaczenia ich na i-węzły). Nawet jeśli rozłączyłeś sięplik (usunął wszystkie ścieżki), proces może nadal go używać, jakbyś nic nie zrobił. Utworzenie nowego pliku ze starą ścieżką niczego nie zmienia: nowy plik otrzyma zupełnie nowy i-węzeł, o którym uruchomiony proces nie ma wiedzy.
Strategie 2 i 3 są również bezpieczne dla plików wykonywalnych: chociaż uruchamianie plików wykonywalnych (i bibliotek ładowanych dynamicznie) nie jest plikami otwartymi w sensie posiadania deskryptora plików, zachowują się w bardzo podobny sposób. Tak długo, jak jakiś program uruchamia kod, plik pozostaje na dysku, nawet bez wpisu katalogu.
- Strategia trzecia jest dość podobna, ponieważ
mv
operacja jest atomowa. Prawdopodobnie będzie to wymagało użycia rename
wywołania systemowego, a ponieważ procesów nie można przerwać w trybie jądra, nic nie może zakłócać tej operacji, dopóki się nie zakończy (pomyślnie lub nie). Ponownie, nie ma zmian w i-węźle starego pliku: tworzony jest nowy i już działające procesy nie będą o nim wiedziały, nawet jeśli są powiązane z jednym z łączy starego i-węzła.
W strategii 3 krok przeniesienia nowego pliku do istniejącej nazwy usuwa pozycję katalogu prowadzącą do starej treści i tworzy pozycję katalogu prowadzącą do nowej treści. Odbywa się to w jednej operacji atomowej, więc ta strategia ma główną zaletę: jeśli proces otworzy plik w dowolnym momencie, zobaczy albo starą lub nową zawartość - nie ma ryzyka, że zawartość zostanie zmieszana lub plik nie będzie istniejący.
Ponownagcc
kompilacja pliku : podczas używania (a zachowanie jest prawdopodobnie podobne w przypadku wielu innych kompilatorów), używasz strategii 2. Możesz to zobaczyć, uruchamiając jeden strace
z procesów kompilatora:
stat("a.out", {st_mode=S_IFREG|0750, st_size=8511, ...}) = 0
unlink("a.out") = 0
open("a.out", O_RDWR|O_CREAT|O_TRUNC, 0666) = 3
chmod("a.out", 0750) = 0
- Kompilator wykrywa, że plik już istnieje za pośrednictwem wywołań systemowych
stat
i lstat
.
- Plik jest rozłączony . Tutaj, mimo że nie jest już dostępny poprzez nazwę
a.out
, jego i-węzeł i zawartość pozostają na dysku, dopóki są używane przez już uruchomione procesy.
- Nowy plik jest tworzony i wykonywalny pod nazwą
a.out
. Jest to zupełnie nowy i-węzeł i zupełnie nowe treści, na których już nie działają uruchomione procesy.
Teraz, jeśli chodzi o biblioteki współdzielone, zastosowanie będzie miało to samo zachowanie. Dopóki obiekt biblioteki jest używany przez proces, nie zostanie on usunięty z dysku, bez względu na to, jak zmienisz jego łącza. Ilekroć coś musi zostać załadowane do pamięci, jądro zrobi to przez i-węzeł pliku, a zatem zignoruje zmiany, które wprowadziłeś w linkach (takie jak powiązanie ich z nowymi plikami).