Dlaczego istnieje zasada jądra Linux, która nigdy nie narusza przestrzeni użytkownika?

38

Zacząłem myśleć o tym problemie w kontekście etykiety na liście mailingowej jądra systemu Linux. Jako najbardziej znany i prawdopodobnie najbardziej udany i ważny projekt wolnego oprogramowania na świecie, jądro Linuksa cieszy się dużą popularnością. A założyciel i lider projektu, Linus Torvalds, najwyraźniej nie wymaga tu przedstawiania.

Linus od czasu do czasu wzbudza kontrowersje swoim płomieniem na LKML. Te płomienie często, jak sam przyznaje, mają związek z łamaniem przestrzeni użytkownika. Co prowadzi mnie do mojego pytania.

Czy mogę spojrzeć z historycznego punktu widzenia na to, dlaczego łamanie przestrzeni użytkownika jest tak złe? Jak rozumiem, łamanie przestrzeni użytkownika wymagałoby poprawek na poziomie aplikacji, ale czy to takie złe, jeśli poprawia kod jądra?

Jak rozumiem, Linus stwierdził, że nieprzestrzeganie przestrzeni użytkownika przebija wszystko inne, w tym jakość kodu. Dlaczego jest to tak ważne i jakie są zalety i wady takiej polityki?

(Oczywiście istnieją pewne wady takiej polityki, konsekwentnie stosowanej, ponieważ Linus czasami ma „nieporozumienia” ze swoimi najlepszymi porucznikami na temat LKML w tym temacie. O ile wiem, zawsze wychodzi mu to na dobre.)

Faheem Mitha
źródło
1
Źle napisałeś imię Linusa we wstępie.
Ismael Miguel
To na pewno nie byłem ja, ale zapomniałem głosować za głosowaniem i oddałem swój głos.
Ismael Miguel
1
Powiązane: stackoverflow.com/q/25954270/350713
Faheem Mitha

Odpowiedzi:

38

Powód nie jest historyczny, ale praktyczny. Istnieje wiele, wiele programów, które działają na jądrze Linux; jeśli interfejs jądra zepsuje te programy, wszyscy będą musieli je zaktualizować.

Teraz prawdą jest, że większość programów w rzeczywistości nie zależy bezpośrednio od interfejsów jądra ( wywołania systemowe ), ale tylko od interfejsów standardowej biblioteki C ( otoczki C wokół wywołań systemowych). Och, ale która standardowa biblioteka? Glibc? uClibC? Dietlibc? Bioniczny? Musl? itp.

Istnieje jednak wiele programów, które implementują usługi specyficzne dla systemu operacyjnego i zależą od interfejsów jądra, które nie są ujawniane przez bibliotekę standardową. (W systemie Linux wiele z nich jest oferowanych za pośrednictwem /proci /sys.)

A potem są statycznie skompilowane pliki binarne. Jeśli aktualizacja jądra psuje jedno z nich, jedynym rozwiązaniem byłoby ich ponowne skompilowanie. Jeśli masz źródło: Linux obsługuje również oprogramowanie zastrzeżone.

Nawet gdy źródło jest dostępne, zebranie go może być uciążliwe. Zwłaszcza podczas aktualizacji jądra, aby naprawić błąd w sprzęcie. Ludzie często aktualizują jądro niezależnie od reszty systemu, ponieważ potrzebują wsparcia sprzętowego. W słowach Linusa Torvaldsa :

Łamanie programów użytkownika jest po prostu niedopuszczalne. (…) Wiemy, że ludzie używają starych plików binarnych od wielu lat, a tworzenie nowej wersji nie oznacza, że ​​możesz to po prostu wyrzucić. Możesz nam zaufać.

On wyjaśnia również, że jednym z powodów, aby ta reguła jest silny, aby uniknąć piekło zależności gdzie nie chcesz mieć tylko uaktualnić inny program, aby uzyskać niektóre nowsze jądro do pracy, ale także uaktualnić kolejny program, a kolejna, i kolejna , ponieważ wszystko zależy od określonej wersji wszystkiego.

To nieco ok, aby mieć dobrze określoną zależność jednokierunkową. To smutne, ale czasem nieuniknione. (…) NIE jest w porządku, aby mieć dwukierunkową zależność. Jeśli kod HAL przestrzeni użytkownika zależy od nowego jądra, to dobrze, chociaż podejrzewam, że użytkownicy mieliby nadzieję, że nie będzie to „jądro tygodnia”, a bardziej „jądro z ostatnich kilku miesięcy”.

Ale jeśli masz zależność TWO-WAY, to jesteś zepsuty. Oznacza to, że musisz dokonać aktualizacji krok po kroku, i to po prostu NIE JEST AKCEPTOWALNE. Jest to okropne dla użytkownika, ale co ważniejsze, jest okropne dla programistów, ponieważ oznacza to, że nie można powiedzieć „wystąpił błąd” i robić rzeczy, takie jak próba zawężenia go przez podział na segmenty lub podobne.

W przestrzeni użytkownika te wzajemne zależności są zwykle rozwiązywane przez utrzymywanie różnych wersji bibliotek; ale możesz uruchomić tylko jedno jądro, więc musi obsługiwać wszystko, co ludzie mogą z nim zrobić.

oficjalnie ,

wsteczna kompatybilność dla [wywołań systemowych uznanych za stabilne] będzie gwarantowana przez co najmniej 2 lata.

W praktyce jednak

Oczekuje się, że większość interfejsów (takich jak syscall) nigdy się nie zmieni i zawsze będzie dostępna.

To, co zmienia się częściej, to interfejsy, które mają być używane tylko przez programy związane ze sprzętem, w /sys. ( /procz drugiej strony, która od czasu wprowadzenia /syszostała zarezerwowana dla usług niezwiązanych ze sprzętem, prawie nigdy nie psuje się w sposób niezgodny).

W podsumowaniu,

podział przestrzeni użytkownika wymagałby poprawek na poziomie aplikacji

i to źle, ponieważ istnieje tylko jedno jądro, które ludzie chcą aktualizować niezależnie od reszty swojego systemu, ale istnieje wiele aplikacji o złożonych współzależnościach. Łatwiej jest utrzymać stabilne jądro niż aktualizować tysiące aplikacji w milionach różnych konfiguracji.

Gilles „SO- przestań być zły”
źródło
1
Dziękuję za Twoją odpowiedź. Czy interfejsy, które zostały zadeklarowane jako stabilne, stanowią nadzbiór wywołań systemowych POSIX? Moje pytanie o historię dotyczy tego, jak ewoluowała ta praktyka. Prawdopodobnie pierwotne wersje jądra Linuksa nie martwiły się o uszkodzenie przestrzeni użytkownika, przynajmniej początkowo.
Faheem Mitha,
3
@FaheemMitha Tak, zrobili to od 1991 roku . Nie sądzę, aby podejście Linusa ewoluowało, zawsze było to, że „interfejsy dla normalnych aplikacji się nie zmieniają, interfejsy dla oprogramowania, które jest bardzo silnie powiązane ze zmianami jądra bardzo rzadko”.
Gilles „SO- przestań być zły”
24

W każdym systemie współzależnym istnieją zasadniczo dwie możliwości. Abstrakcja i integracja. (Celowo nie używam terminów technicznych). W przypadku Abstraction mówisz, że kiedy wywołujesz interfejs API, który, choć kod stojący za interfejsem API może ulec zmianie, wynik będzie zawsze taki sam. Na przykład, gdy dzwonimy fs.open(), nie obchodzi nas, czy jest to dysk sieciowy, dysk SSD czy dysk twardy, zawsze otrzymamy otwarty deskryptor pliku, z którym możemy coś zrobić. Celem „integracji” jest zapewnienie „najlepszego” sposobu na zrobienie czegoś, nawet jeśli sposób się zmieni. Na przykład otwarcie pliku może być inne w przypadku udziału sieciowego niż pliku na dysku. Oba sposoby są dość szeroko stosowane we współczesnym komputerze z systemem Linux.

Z punktu widzenia programistów jest to pytanie „działa z dowolną wersją” lub „działa z określoną wersją”. Doskonałym tego przykładem jest OpenGL. Większość gier jest skonfigurowana do pracy z określoną wersją OpenGL. Nie ma znaczenia, czy kompilujesz ze źródła. Jeśli gra została napisana z myślą o wykorzystaniu OpenGL 1.1 i próbujesz uruchomić ją na 3.x, nie będziesz się dobrze bawić. Na drugim końcu spektrum niektóre połączenia powinny działać bez względu na wszystko. Na przykład chcę zadzwonić fs.open()Nie chcę się przejmować wersją jądra, na której jestem. Chcę tylko deskryptora pliku.

Są zalety na każdy sposób. Integracja zapewnia „nowsze” funkcje kosztem kompatybilności wstecznej. Podczas gdy abstrakcja zapewnia stabilność w przypadku „nowszych” połączeń. Chociaż należy zauważyć, że jest to kwestia priorytetu, a nie możliwości.

Z punktu widzenia społeczności, bez naprawdę dobrego powodu, abstrakcja jest zawsze lepsza w złożonym systemie. Wyobraź sobie na przykład, że fs.open()działało inaczej w zależności od wersji jądra. Wówczas prosta biblioteka interakcji systemu plików wymagałaby utrzymania kilkuset różnych metod „otwartego pliku” (lub prawdopodobnie bloków). Kiedy pojawi się nowa wersja jądra, nie będziesz w stanie „uaktualnić”, będziesz musiał przetestować każdy używany program. Jądro 6.2.2 (fałszywe) może po prostu zepsuć edytor tekstu.

W niektórych przykładach ze świata rzeczywistego OSX nie przejmuje się niszczeniem przestrzeni użytkownika. Częściej dążą do „integracji” zamiast „abstrakcji”. I przy każdej dużej aktualizacji systemu operacyjnego rzeczy się psują. To nie znaczy, że jeden sposób jest lepszy od drugiego. To decyzja dotycząca wyboru i projektu.

Co najważniejsze, ekosystem Linux jest wypełniony niesamowitymi projektami typu open source, w których ludzie lub grupy pracują nad projektem w wolnym czasie lub dlatego, że narzędzie jest przydatne. Mając to na uwadze, w momencie, gdy przestaje być zabawą i zaczyna być PIA, programiści pójdą gdzie indziej.

Na przykład przesłałem łatkę do BuildNotify.py. Nie dlatego, że jestem altruistą, ale dlatego, że używam tego narzędzia i chciałem funkcji. To było łatwe, więc tutaj, mam łatkę. Gdyby to było skomplikowane lub uciążliwe, nie użyłbym tego BuildNotify.pyi znalazłbym coś innego. Jeśli za każdym razem, gdy pojawiała się aktualizacja jądra, mój edytor tekstowy pękał, używałbym innego systemu operacyjnego. Mój wkład w społeczność (bez względu na to, jak niewielki) nie byłby kontynuowany lub istniał, i tak dalej.

Podjęto więc decyzję projektową dotyczącą abstrakcyjnych wywołań systemowych, więc kiedy to robię fs.open(), po prostu działa. Oznacza to utrzymanie fs.opendługo po fs.open2()zdobyciu popularności.

Historycznie taki jest cel systemów POSIX w ogóle. „Oto zestaw połączeń i oczekiwane wartości zwrotne, wymyślisz środek”. Ponownie ze względów przenośności. Dlaczego Linus decyduje się na zastosowanie tej metodologii, jest wewnętrzną cechą jego mózgu i trzeba by go zapytać, aby dokładnie wiedział dlaczego. Gdybym to był ja, wybrałbym abstrakcję zamiast integracji w złożonym systemie.

Coteyr
źródło
1
Interfejs API do przestrzeni użytkownika, API „syscall”, jest dobrze zdefiniowany (szczególnie podzbiór POSIX) i stabilny, ponieważ usunięcie jakiejkolwiek jego części spowoduje uszkodzenie oprogramowania, które ludzie mogli zainstalować. To, czego nie ma, to stabilny interfejs API sterownika .
pjc50,
4
@FaheemMitha, jest na odwrót. Programiści jądra mogą dowolnie łamać interfejs API sterownika, o ile chcą naprawić wszystkie wbudowane sterowniki przed następną wersją. Łamie interfejs API przestrzeni użytkownika, a nawet robi rzeczy inne niż API, które mogłyby zniszczyć przestrzeń użytkownika, co wywołuje niesamowite reakcje Linusa.
Mark
4
Na przykład, jeśli ktoś zdecyduje się go zmienić, zwracając w niektórych okolicznościach inny kod błędu niż ioctl (): lkml.org/lkml/2012/12/23/75 (zawiera przekleństwa i osobiste ataki na odpowiedzialnego programistę). Łata została odrzucona, ponieważ spowodowałaby uszkodzenie PulseAudio, a zatem całego dźwięku w systemach GNOME.
pjc50,
1
@ FaheemMitha, w zasadzie, def add (a, b); zwraca a + b; end --- def add (a, b); c = a + b; zwróć c; end --- def add (a, b); c = a + b +10; zwraca c - 10; end - wszystkie są „tą samą” implementacją add. To, co go tak denerwuje, to fakt, że ludzie dodają def (a, b); return (a + b) * -1; koniec Zasadniczo zmiana sposobu, w jaki „wewnętrzne” rzeczy działają w jądrze, jest w porządku. Zmiana tego, co jest zwracane do zdefiniowanego i „publicznego” wywołania API, nie jest możliwa. Istnieją dwa rodzaje wywołań API „prywatny” i „publiczny”. Uważa, że ​​publiczne wywołania API nigdy nie powinny się zmieniać bez uzasadnionego powodu.
coteyr
3
Przykład bez kodu; Idziesz do sklepu, kupujesz 87 oktanowy gaz. Ty, jako konsument, nie dbasz o to, skąd pochodził gaz ani jak go przetwarzano. Po prostu dbasz o benzynę. Jeśli gaz przeszedł inny proces rafinacji, nie obchodzi cię to. Upewnij się, że proces rafinacji może się zmienić. Istnieją nawet różne źródła ropy. Ale zależy ci na otrzymaniu 87 oktanowego gazu. Tak więc jego pozycja to zmiana źródeł, zmiana rafinerii, zmiana wszystkiego, o ile tylko to, co wychodzi z pompy, to 87 oktanowy gaz. Wszystkie rzeczy „za kulisami” nie mają znaczenia. Tak długo, jak jest 87 oktanowy gaz.
coteyr
8

To decyzja projektowa i wybór. Linus chce być w stanie zagwarantować programistom przestrzeni użytkownika, że ​​z wyjątkiem niezwykle rzadkich i wyjątkowych (np. Związanych z bezpieczeństwem) zmian w jądrze nie zepsują swoich aplikacji.

Zaletą jest to, że twórcy przestrzeni użytkownika nie zobaczą, jak ich kod nagle pęka w nowych jądrach z arbitralnych i kapryśnych powodów.

Wadą jest to, że jądro musi utrzymywać stary kod i stare wywołania systemowe itp. Na zawsze (lub przynajmniej dawno minęły daty przydatności).

cas
źródło
Dziękuję za odpowiedź. Czy znasz historię ewolucji tej decyzji? Mam świadomość projektów, które mają nieco inną perspektywę. Na przykład projekt Mercurial nie ma stałego interfejsu API i może i łamać kod, który na nim opiera się.
Faheem Mitha,
Nie, przepraszam, nie pamiętam, jak to się stało. Możesz wysłać do Linusa lub LKMLa e-maila i zapytać go.
cas
2
Mercurial nie jest systemem operacyjnym. Głównym celem systemu operacyjnego jest umożliwienie uruchamiania na nim innego oprogramowania, a łamanie tego innego oprogramowania jest bardzo niepopularne. Dla porównania system Windows zachowywał także zgodność wsteczną od bardzo dawna; 16-bitowy kod systemu Windows został niedawno przestarzały.
pjc50,
@ pjc50 To prawda, że nie jest Mercurial OS, ale niezależnie od tego, tam jest inne oprogramowanie, nawet jeśli tylko skrypty, które zależą od niego. I potencjalnie mogą zostać zepsute przez zmiany.
Faheem Mitha,