Przerwanie wywołań systemowych w momencie przechwycenia sygnału


29

Z czytania stron podręcznika read()i write()wywołań wynika, że ​​połączenia te są przerywane sygnałami, niezależnie od tego, czy muszą być blokowane, czy nie.

W szczególności załóżmy

  • proces ustanawia moduł obsługi dla niektórych sygnałów.
  • urządzenie jest otwarte (powiedzmy terminal) z O_NONBLOCK nie ustawionym (tzn. działa w trybie blokowania)
  • następnie proces wykonuje read()wywołanie systemowe w celu odczytu z urządzenia, w wyniku czego wykonuje ścieżkę kontroli jądra w przestrzeni jądra.
  • podczas gdy preces wykonuje swoją pracę read()w przestrzeni jądra, sygnał, dla którego wcześniej zainstalowano moduł obsługi, jest dostarczany do tego procesu i wywoływany jest moduł obsługi sygnału.

Czytając strony podręcznika i odpowiednie sekcje w woluminie interfejsów systemowych (XSH) SUSv3 , okazuje się, że:

ja. Jeśli a read()zostanie przerwane przez sygnał przed odczytaniem jakichkolwiek danych (tj. Musiał zablokować, ponieważ żadne dane nie były dostępne), zwraca -1 z errnoustawionym na [EINTR].

ii. Jeśli a read()zostanie przerwane przez sygnał po pomyślnym odczytaniu niektórych danych (tj. Możliwe było natychmiastowe rozpoczęcie obsługi żądania), zwraca liczbę odczytanych bajtów.

Pytanie A): Czy słusznie zakładam, że w obu przypadkach (blok / brak bloku) dostarczenie i obsługa sygnału nie jest całkowicie przejrzysta dla read()?

Przypadek I wydaje się zrozumiałe, ponieważ blokowanie read()zwykle TASK_INTERRUPTIBLEustawia proces w takim stanie, że po dostarczeniu sygnału jądro ustawia proces w TASK_RUNNINGstan.

Jednak gdy read()nie trzeba blokować (przypadek ii.) I przetwarza żądanie w przestrzeni jądra, pomyślałbym, że nadejście sygnału i jego obsługa byłaby przejrzysta, podobnie jak nadejście i poprawna obsługa HW przerwanie byłoby. W szczególności założyłbym, że po dostarczeniu sygnału proces zostałby tymczasowo wprowadzony w tryb użytkownika w celu wykonania jego procedury obsługi sygnału, z której w końcu powróciłby w celu zakończenia przetwarzania przerwanego read()(w przestrzeni jądra), aby read()działał kurs do zakończenia, po którym proces wraca do punktu tuż po wywołaniu read()(w przestrzeni użytkownika), w wyniku czego odczytane są wszystkie dostępne bajty.

Ale ii. wydaje się sugerować, że read()jest przerwane, ponieważ dane są dostępne natychmiast, ale zwraca zwraca tylko niektóre dane (zamiast wszystkich).

To prowadzi mnie do mojego drugiego (i ostatniego) pytania:

Pytanie B): Jeśli moje założenie w punkcie A) jest prawidłowe, dlaczego read()przerwanie zostaje przerwane, nawet jeśli nie trzeba go blokować, ponieważ dostępne są dane, aby natychmiast zaspokoić żądanie? Innymi słowy, dlaczego nie jest read()ono wznawiane po wykonaniu procedury obsługi sygnału, co ostatecznie powoduje zwrócenie wszystkich dostępnych danych (które mimo wszystko były dostępne)?

Odpowiedzi:


29

Podsumowanie: masz rację, że odbiór sygnału nie jest przezroczysty, ani w przypadku i (przerwany bez przeczytania czegokolwiek), ani w przypadku ii (przerwany po częściowym odczytaniu). W przeciwnym razie wymagałoby to dokonania fundamentalnych zmian zarówno w architekturze systemu operacyjnego, jak i architekturze aplikacji.

Widok implementacji systemu operacyjnego

Zastanów się, co się stanie, jeśli wywołanie systemowe zostanie przerwane przez sygnał. Procedura obsługi sygnału wykona kod trybu użytkownika. Ale moduł obsługi syscall jest kodem jądra i nie ufa żadnemu kodowi trybu użytkownika. Przeanalizujmy więc opcje obsługi programu syscall:

  • Zakończ połączenie systemowe; zgłoś, ile zrobiono kodowi użytkownika. W razie potrzeby kod aplikacji może w jakiś sposób zrestartować wywołanie systemowe. Tak działa Unix.
  • Zapisz stan wywołania systemowego i pozwól, aby kod użytkownika wznowił połączenie. Jest to problematyczne z kilku powodów:
    • Podczas działania kodu użytkownika może się zdarzyć, że nastąpi unieważnienie zapisanego stanu. Na przykład, jeśli czyta z pliku, plik może zostać obcięty. Tak więc kod jądra wymagałby dużo logiki do obsługi tych przypadków.
    • Nie można pozwolić, aby zapisany stan utrzymywał jakąkolwiek blokadę, ponieważ nie ma gwarancji, że kod użytkownika kiedykolwiek wznowi połączenie systemowe, a wtedy blokada zostanie utrzymana na zawsze.
    • Jądro musi udostępniać nowe interfejsy, aby wznowić lub anulować trwające wywołania systemowe, oprócz zwykłego interfejsu, aby rozpocząć wywołanie systemowe. Jest to bardzo skomplikowane w rzadkich przypadkach.
    • Stan zapisany musiałby wykorzystywać zasoby (przynajmniej pamięć); zasoby te musiałyby zostać przydzielone i zatrzymane przez jądro, ale wliczone do przydziału procesu. To nie jest nie do pokonania, ale jest to komplikacja.
      • Zauważ, że procedura obsługi sygnału może wywoływać wywołania systemowe, które same zostają przerwane; więc nie możesz mieć tylko statycznego przydziału zasobów, który obejmuje wszystkie możliwe połączenia systemowe.
      • A jeśli nie można przydzielić zasobów? Wtedy syscall i tak musiałby zawieść. Co oznacza, że ​​aplikacja musiałaby mieć kod do obsługi tego przypadku, więc ten projekt nie uprościłby kodu aplikacji.
  • Pozostań w toku (ale zawieszony), utwórz nowy wątek dla procedury obsługi sygnału. To znowu jest problematyczne:
    • Wczesne implementacje unixowe miały jeden wątek na proces.
    • Osoba obsługująca sygnał ryzykowałaby przekroczenie butów syscall. To i tak jest problem, ale w obecnym projekcie uniksowym jest zawarty.
    • Konieczne byłoby przydzielenie zasobów dla nowego wątku; patrz wyżej.

Główną różnicą w przypadku przerwania jest to, że kod przerwania jest zaufany i wysoce ograniczony. Zazwyczaj nie wolno alokować zasobów ani działać wiecznie, ani brać zamków i nie zwalniać ich, ani robić innych paskudnych rzeczy; ponieważ program obsługi przerwań jest napisany przez samego implementatora systemu operacyjnego, wie, że nie zrobi nic złego. Z drugiej strony kod aplikacji może zrobić wszystko.

Widok projektu aplikacji

Czy w przypadku przerwania aplikacji w trakcie wywołania systemowego wywołanie systemowe powinno być kontynuowane? Nie zawsze. Rozważmy na przykład program taki jak powłoka, która odczytuje linię z terminala, a użytkownik naciska Ctrl+C, uruchamiając SIGINT. Odczyt nie może się zakończyć, na tym właśnie polega sygnał. Zauważ, że ten przykład pokazuje, że readsyscall musi być przerywalny, nawet jeśli żaden bajt nie został jeszcze odczytany.

Musi więc istnieć sposób, aby aplikacja poinformowała jądro o anulowaniu wywołania systemowego. W systemie uniksowym dzieje się to automatycznie: sygnał powoduje powrót syscall. Inne projekty wymagałyby od aplikacji wznowienia lub anulowania wywołania systemowego według własnego uznania.

readWywołanie systemowe jest sposób, to dlatego, że jest prymitywny, że ma sens, biorąc pod uwagę ogólny projekt systemu operacyjnego. Oznacza to w przybliżeniu „czytaj tyle, ile możesz, do limitu (rozmiar bufora), ale przestań, jeśli coś innego się wydarzy”. Rzeczywiste odczytanie pełnego bufora wymaga działania readw pętli, dopóki nie zostanie odczytanych jak najwięcej bajtów; Jest to funkcja wyższego poziomu, fread(3). W przeciwieństwie read(2)do wywołania systemowego, freadjest funkcją biblioteki, zaimplementowaną w przestrzeni użytkownika na górze read. Jest odpowiedni dla aplikacji, która czyta plik lub umiera próbując; nie jest odpowiedni dla interpretera wiersza poleceń ani programu sieciowego, który musi dławić połączenia czysto, ani programu sieciowego, który ma równoległe połączenia i nie używa wątków.

Przykład odczytu w pętli znajduje się w Programowaniu systemu Linux Roberta Love'a:

ssize_t ret;
while (len != 0 && (ret = read (fd, buf, len)) != 0) {
  if (ret == -1) {
    if (errno == EINTR)
      continue;
    perror ("read");
    break;
  }
  len -= ret;
  buf += ret;
}

Dba o case ioraz case iii kilka innych.


Bardzo dziękuję Gillesowi za bardzo zwięzłą i jasną odpowiedź, która potwierdza podobne poglądy przedstawione w artykule na temat filozofii projektowania UNIX. Wydaje mi się bardzo przekonujące, że zachowanie przerwa syscall ma do czynienia z filozofią projektowania UNIX zamiast technicznych ograniczeń lub utrudnień
darbehdar

@darbehdar To wszystko trzy: filozofia projektowania unix (tutaj głównie, że procesy są mniej zaufane niż jądro i mogą uruchamiać dowolny kod, a także, że procesy i wątki nie są tworzone pośrednio), ograniczenia techniczne (dotyczące alokacji zasobów) i projektowanie aplikacji (tam to przypadki, w których sygnał musi anulować wywołanie systemowe).
Gilles 'SO - przestań być zły'

2

Aby odpowiedzieć na pytanie A :

Tak, dostarczenie i obsługa sygnału nie jest całkowicie przejrzysta dla read().

Bieg w read()połowie drogi może zajmować pewne zasoby, podczas gdy jest przerywany sygnałem. Program obsługi sygnału może również wywoływać inne read()(lub dowolne inne bezpieczne systemy asynchroniczne ). Tak więc read()przerwany przez sygnał musi zostać najpierw zatrzymany, aby zwolnić zasoby, z których korzysta, w przeciwnym razie read()wywołany z procedury obsługi sygnału uzyska dostęp do tych samych zasobów i spowoduje problemy z ponownym wysłaniem.

Ponieważ wywołania systemowe inne niż read()mogłyby być wywoływane z procedury obsługi sygnału i mogą również zajmować identyczny zestaw zasobów, jak to read()robi. Aby uniknąć powyższych problemów z ponownym wysłaniem, najprostszym i najbezpieczniejszym rozwiązaniem jest zatrzymanie przerwanego działania za read()każdym razem, gdy pojawi się sygnał podczas jego pracy.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.