10.4 Proces init
Spis tresci
Przedostatnia instrukcja w funkcji "start_kernel()" uruchamianej
przy inicjalizacji systemu jest powolanie nowego procesu, procesu o identyfikatorze
1, zwanym powszechnie procesem init. Proces ten bedac caly czas w trybie
jadra uruchamia dwa demony. Pierwszy do obslugi swap'a, drugi do opoznionego
zapisu buforow na dysk. Nastepnie wywoluje funkcje setup(), ktora inicjalizuje
urzadzenia i podmontowywuje do korzenia glowny system plikow. Otwiera terminal
i ustawia na niego swoje stdin, stdout i stderr. Nastepnie wykonuje po
kolei exec("/etc/init"); exec("/bin/init");exec("/sbin/init").
Jezeli zaden sie nie udal, to proces init przechodzi do nieskonczonej petli,
w ktorej: 1. uruchamia shell 2. czeka na jego zakonczenie.
Przypadek, w ktorym nie udalo sie uruchomic zadnego inita jest malo ciekawy,
dlatego zajmiemy sie teraz przypadkiem, w ktorym nam sie to udalo.
Wersji "initow" jest dosyc duzo, wiecej niz wersji jadra. Zwiazane
jest to z tym, ze program init nie nalezy juz do kodu jadra, jest on zwyklym
programem, dzialajacym w trybie uzytkownika, tyle ze z identyfikatorem
root'a. Kazdy moze latwo go sobie zmienic na inny bez zmieniania jadra,
wystarczy dotychczasowy program init, ktory jest w jednym z katalogow /etc,
/bin lub /sbin, zamienic na nowy o takiej samej nazwie.
Ostatnio standardem staje sie init Systemu V, dlatego dzialanie procesu
init opisze na jego przykladzie.
Prace procesu init mozemy podzielic na dwie czesci. Pierwsza, ktora jest wykonywana podczas ladowania systemu, polega na wczytaniu pliku /etc/inittab i uruchomieniu procesow zwiazanych z inicjacja systemu, wczytaniu poziomu dzialania (ang. runlevel) i uruchomieniu procesow zwiazanych z tym poziomem. Druga czesc to nadzorowanie pracy systemu. Praca ta polega na wznawianiu procesow, ktorych akcja zdefiniowana w inittabie bylo "respawn" , na przechwytywaniu sygnalow o zakloceniach zasilania i na uruchamianiu zwiazanych z nimi procesow; oraz na przejsciu na nowy poziom dzialania na zyczenie administratora. Przejscie to wiaze sie z zabiciem procesow, ktore nie sa powiazane z nowym poziomem, i uruchomieniem tych, ktore sa powiazane.
algorytm init
wejscie: brak
wyjscie: brak
{
Ustaw obsluge sygnalow;
runlevel="sysinit"; /* runlevel jest zmienna globalna,
na ktorej mamy aktualny poziom dzialania */
Przeczytaj plik /etc/inittab;
Sprawdz, czy sa dzieci, ktore nalezaloby uruchomic;
/* w praktyce sprowadza sie to do uruchomienia procesow o akcji "sysinit"
*/
for ever /*glowna petla*/
{
Sprawdz, czy nie nalezy zmienic poziomu dzialania;
/* Poziom jest zmieniany tylko podczas inicjacji systemu, oraz po zakonczeniu
wszystkich procesow-dzieci w poziomie dla jednego uzytkownika */
if (sa procesy, na ktorych zakonczenie czekamy i nie ma nieobsluzonych
sygnalow) pause();
/* pause() powoduje uspienie procesu do czasu otrzymania jakiegokolwiek
sygnalu */
Sprawdz, czy mozna juz uruchomic dzieci, ktorych nie udalo
sie wznowic wczesniej;
Przetworz sygnaly;
Sprawdz, czy sa dzieci, ktore nalezaloby uruchomic;
sync(); /* zapisuje bufory na dysk */
}
} / * init */
Kazda linia w tym pliku ma format: id:runlevels:action:process
id to niepowtarzalny dwuliterowy identyfikator linii, runlevels to poziomy,
przy ktorych ma byc wykonana dana akcja, action okresla wlasnie te akcje,
a process jest linia komendy, jaka ma byc wykonana.
Mamy zdefiniowane nastepujace akcje:
Pole runlevels dla akcji boot, bootwait, sysinit, initdefault jest ignorowane.
Jezeli chodzi o znaczenie poziomow dzialania, standartowo 's' oznacza poziom
dla jednego uzytkownika, 1,2,3,4,5,6 poziomy wieloztkownikowe, przy czym
6 to poziom dla X-windows. Poziom 0 to zamkniecie systemu. Podkereslam,
ze jest to standartowe ustawinie, tak na prawde to znaczenie ich definiuja
procesy jakie sa przy nich uruchamiane. Standartowy plik /etc/inittab jest
przedstawiony tutaj. Ale kazady
moze zamienic go sobie jak chce i wetedy te poziomy moga miec zupelnie
inne znaczenie.
Krotko o kolejnosci wykonywania procesow podczas inicjalizowania systemu:
Pierwszym poziomem dzialania po rozpoczeciu pracy inita jest sysinit. Potem
uruchamiamy procesy, ktorych akcja jest wlasnie "sysinit" i czekamy
na ich zakonczenie .Nastepnie sprawdzamy, jaki ma byc docelowy poziom;
sprawdzenia tego dokonuje funkcja "GetInitDefault".
Jesli tym poziomem jest tryb dla jednego uzytkownika (ang. single user
mode), to po zakonczeniu poziomu sysinit od razu do niego przechodzimy,
wpp po sysinit, a przed poziomem docelowym mamy jeszcze poziom "boot".
W tym poziomie odpalamy procesy, ktorych akcja jest "boot" lub
"bootwait". Po zakonczeniu procesow o akcji "bootwait"
przechodzimy juz do docelowego poziomu. Zmiana poziomow zajmuje sie funkcja
"BootTransitions " .
Procesy o akcji "boot" to jedyne procesy, ktore moga dzialac
we wszystkich poziomach, reszta moze dzialac tylko w poziomach okreslonych
przez "runlevels".
algorytm: GetInitDefault;
wejscie: brak;
wyjscie: brak;
{
Sprawdz, czy w pliku inittab byla linijka o akcji "initdefault";
If (byla taka linijka) return( poziom, jaki ona definiowala);
Spytaj administratora, jaki ma byc poziom dzialania;
return ( odpowiedz administratora );
} /* GetInitDefalut */
Najwazniejsza struktura danych w programie init jest CHILD; Opisuje
ona kazde polecenie wczytane z inittaba oraz ewentualny proces z nim zwiazany.
struct CHILD {
int flags ; ---------/* informacje o statucie dzialania procesu */
int extsta; --------/* kod wyjscia procesu */
int pid; -----------/* identyfikator procesu, lub zero jesli nie jest uruchomiony
*/
time_t tm; --------/* czas ostatniego wznowienia */
int count; ---------/* liczba wznowien w ostatnich
TESTTIME sekundach */
char id[8]; ---------/* pole id z inittaba tego polecenia */
char[12];----------/* poziomy, przy ktorych moze dzialac ten proces */
int action; ---------/* akcja */
char process[128]; ---/* linia komendy, jaka nalezy wykonac */
CHILD * new; ------/* pole to jest wykorzystywane przy ponownym czytaniu
inittaba do zaznaczenia procesow, ktore sie powtarzaja */
CHILD * next; -----/* wskaznik na nastepny element listy dzieci */
} /* CHILD */
Mamy zdefiniowane jeszcze trzy stale zwiazane z wykonywaniem procesow:
define MAXSPAWN /* okresla maksymalna liczbe wznowien procesu w przeciagu
... */
define TESTTIME /* tylu sekund */
define SLEEPTIME /* w przypadku przekroczenia liczby
wznowien, czas, jaki musi minac, aby ponownie init probowal uruchomic ten
proces */
dygresja: W inicie, ktory opisuje, powyzsze stale sa zdefiniowane nastepujaco (10, 120, 300). Wystarczy wiec w przeciagu dwoch minut zalogowac sie i wylogowac, zeby zablokowac terminal na 5 minut.
Jezeli chodzi o flagi w strukturze CHILD to mamy:
Za dziecko uwazam zatem strukture, ktora powyzej przedstawilem, a nieraz
takze proces z nia zwiazany ( jedynym wyjatkiem jest obsluga sygnalu SIGCHLD;
tam pod pojeciem dziecko kryje sie proces, ktorego identyfikatorem ojca
jest liczba 1( czyli dziecko w ujeciu Unixa)). Wiem, ze nie jest to najlepsze
rozwiazanie, ale niestety nie wpadlem na lepsze, poza tym w kodzie tez
bylo to tak nazwane (za przyklad daje nazwe struktury), tych, ktorych przyjecie
takiej terminologii wprowadzilo w blad, z gory przepraszam.Pod pojeciem
"proces" kryje sie zwykle "proces zwiazany z danym dzieckiem".
Zadanie to wykonuje funkcja ReadItab(). Jest ona wywolywana przy rozpoczeciu pracy przez inita, oraz przy przejsciu do innego poziomu dzialania.
algorytm: ReadItab
wejscie: brak;
wyjscie: brak;
{
while(nie przeczytales calego pliku){
Przeczytaj linie i sprawdz, czy jest poprawna;
if (jest poprawna){
Przydziel pamiec dla struktury CHILD i zainicjuj
ja;
Dodaj to dziecko do listy dzieci;
/* procesy zwiazane z obsluga problemow z napieciem, umieszcza sie na poczatku
listy, natomiast pozostale na koncu */
};
};
if (bylo to ponowne czytanie inittaba){ /* oznacza to, ze przechodzimy
na nowy poziom dzialania na polecenie administratora */
Zabij dzieci, ktore nie powinny dzialac na nowym poziomie.
/*Zabijamy wszystkie procesy, ktorych nie ma w nowo przeczytanym inittabie,
tzn. nie ma linijki z identyfikatorem, ktory odpowiadalby polu "id"
w strukturze CHILD danego procesu. Zabijamy takze procesy, ktore sa w nowoprzeczytanym
inittabie, ale pole "rlevel" ich struktury CHILD nie zawiera
poziomu, na ktory sie przelaczamy, wyjatkiem sa procesy o akcji "boot",
one moga dzialac zawsze. */
}; /* ReadItab */
for( kazde dziecko ) if ( dziecko dziala i jego akcja nie jest boot
) return; /* nie mozemy zmienic poziomu*/
if ( aktualny poziom to "sysinit"){
log_level=GetInitDefault(); /* zmienna
"log_level" jest statyczna */
if (log_level=='s') /* od razu przechodzimy do decelowego poziomu */ {
runlevel = 's'
return:
}
else /* najpierw poziom boot */{
runlevel = 'boot';
renturn;
}
} /* poziomom sysinit */;
if ( aktualnym poziomem jest boot ) runlevel=log_level; /* wartosc wczytana
poprzednio przez "GetInitDefault"
*/
if ( aktualnym poziomem jest 's') {
runlevel=GetInitDefault();
Odwies flagi (RUNNIGN, XECUTED, WAITING) dzieciom zwiazanym z poziomem
's'; /* tzn. tym, ktorych pole rlevel ich struktury CHILD
zawiera 's' */
}
} /* BootTransitions */
Proces init obsluguje sygnaly dwuetapowo. Pierwszy etap polega na zaznaczeniu na zmiennej globalnej "got_signlas" sygnalu, ktory przyszedl. Etap ten jest obslugiwany przez funkcje "signal_handler", ktora przechwytuje prawie wszystkie sygnaly. Wyjatkiem jest sygnal SIGCHLD (sygnal ten jest wysylany w przypadku smierci jakiegos naszego dziecka), ktory jest obslugiwany przez "chld_handler". Funkcja ta oprocz ustawienia odpowiedniego bitu w "got_signals" sprawdza ktore to bylo dziecko, i jesli byl to proces z inittaba to ustawia w jego strukturze CHILD flage ZOMBIE. Drugim, zasadniczym etapem przetwarzania sygnalu jest wykonanie zadan odpowiadajacych danemu sygnalowi. Etapem tym zajmuje sie funkcja "ProcessSignals".
algortym Ustawienie przechwytywania sygnalow;
wejscie: brak;
wyjscie: brak:
{
Powiadom jadro, ze chcemy otrzymac sygnal SIGINT, jesli zostalo nacisniete
ctrl+alt+del;
Ustaw przechwycenie sygnalu SIGCHLD przez funkcje "chld_handler";
Ustaw przechwycenie reszty sygnalow przez funkcje "signal_handlers";
};
Przetwarzaniem sygnalow zajmuje sie funkcja "ProcessSignals". Funkcja ta jest wywolywana przy kazdym obrocie petli glownej.
algorytm: ProcessSignals;
wejscie: brak;
wyjscie: brak:
{
if( przechwycilismy SIGPWR){ /*mamy jakies problemy z zasilaniem*/
Przeczytaj z pliku /etc/powerstatus co sie dzieje;
Odwies flagi XECUTED tym procesom, ktore sa zwiazane z tym zdarzeniem;
/* Odwieszenie tej flagi spowoduje, ze przy najblizszym wywolaniu funkcji
"StartEmIfNeeded" procesy te zostana uruchomione*/
Usun sygnal SIGPWR ze zmiennej "got_signlas";
};
if (przechwycilismy SIGINT) { /* nacisnieto ctr+alt+del*/
Odwies flage XECUTED procesom, ktorych akcja jest ctrlaltdel; /* skutek,
patrz wyzej */
Usun sygnal SIGINT ze zmiennej "got_signlas";
};
if (przechwycilismy SIGCHLD) { /* wlasnie umarlo jakies nasze dziecko*/
Odwies flagi RUNNING, WAIT, ZOMBIE , procesom, ktore maja ustawiona
flage ZOMBIE.
/* flage te ustawiala funkcja "chld_handler" */
/* odwieszenie tych flag bedzie potem wykorzystywane przez funkcje "StartEmIfNeeded"*/
Usun sygnal SIGCHLD ze zmiennej "got_signlas";
};
if ( przechwycilismy SIGHUP) { /*zmiana poziomu dzialania*/
Przeczytaj z pliku /etc/initrunlevel, jaki ma byc nowy poziom;
Przeczytaj plik /etc/inittab; /* dziala inaczej niz za pierwszym czytaniem
*/
Usun sygnal SIGHUP ze zmiennej "got_signlas";
};
};/ * ProcessSignals*/
Sprawdzeniem, czy sa procesy, ktore nalezaloby
uruchomic, zajmuje sie funkcja "StartEmIfNeeded".
algorytm: StartEmIfNeeded;
wejscie: brak;
wyjscie: brak;
{
for(kazde dziecko){
if ( czekamy na zakonczenie tego procesu) break;
if ( ten proces jest uruchomiony) continue;
if ( ten proces moze dzialac w aktualnym poziomie dzialania) StartUp(dziecko);
};
}; /* StartEmIfNeeded */
Funkcja StartUp sprawdza czy dane dziecko nalezy uruchomic, jesli tak to wywoluje funkce Spawn.
algorytm: StartUp;
wejscie: dziecko, ktore nalezy sprawdzic, czy nie nalezy go uruchomic;
wyjscie: brak;
{
if (to dziecko przekroczylo maksymalna liczbe wznowien w ostatnim czasie)
return;
switch(akcja dziecka){
case SYSINIT: case BOOTWAIT: case WAIT: case POWERWAIT:
case CTRALTDEL: case POWEROKWAIT:
if ( jeszcz nie bylo wznawiane)/* uruchamiamy je teraz */ flags=flags|WAITING;
case BOOT: case POWERFAIL: case ONCE:
if (juz bylo uruchomione) return;
case RESPAWN:
Spawn(dziecko); /* uruchamiamy to dziecko */
};
}; /* StartUp */
algorytm: Spawn; /* Uruchom proces */
wejscie: dziecko, ktore nalezy uruchomic;
wyjscie: brak;
{
if (akcja tego dziecka jest RESPAWN) /* musimy sprawdzic, czy tym uruchomieniem nie przekroczym maksymalnej liczyby wznowien*/
if ( wznawiamy go za czesto) { alarm(SLEEPTIME); return};
flags|=(RUNNING|XECUTED);
Utworzy tablice argv dla tego dziecka; /* jezeli linia komend tego dziecka
zawiera jakies dziwne znaczki typu: ! $ | / < > ?, oznacza to, ze
musimy uruchomic jakiegos shella i przekazac mu linie komend jako argument.
Natomiast jesli nie zawiera, to kazdy wyraz z tej linii wpisujemy kolejno
do tablicy argv */
if ((pid =fork())==0){ /* jestesmy w procesie dziecka */
setsid() /* otwiera sesje dla tego procesu */
execvp(argv[0], argv);
}
else { /* Jestesmy w procesie ojca */
CHILD-> pid=pid;
return;
};
}; /* Spawn */
Sprawdzeniem, czy mozna juz uruchamiac procesy, ktore
przekroczyly maksymalna liczbe wznowien, zajmuje sie funkcja "FailCheck()".
Jest ona wywolywana przy kazdym obrocie glownej
petli.
algorytm: FailCheck;
wejscie: brak;
wyjscie: brak;
{
for(kazde dziecko){
if( to dziecko padlo){
if ( od ostatniego wzonwienia minelo juz SLEEPTIME sekund ) {
CHILD->flags&=~FAILING; /* odwieszenie tej flagi spowoduje ze
przy najblizszym wywolaniu funkcji "StartEmIfNeeded"
dziecko to zostanie uruchomione */
CHILD->count=0;
CHILD->tm=aktualny_czas();
}
else alrm(CHILD->tm+SLEEPTIME-aktualny_czas()) /* sprawdzimy go potem
*/
} /* to dziecko padlo */
} /* for */
}/ * FailCheck */
/*Poziom do jakiego mamy przejsc po inicjalizacji systemu. */
id:5:initdefault:
/*Uruchamiamy ten skrypt przy inicjalizacji systemu (standartowo wykonuje
on polecenie '/sbin/fsck -a -A', ktore sprawdza czy wyszystkie systemy
plikow aktualnie podlaczone sa poprawne, automatycznie usuwajac napotkane
bledy ; skrypt ten podmontowywuje takze systemy plikow opisane z pliku
/etc/fstab)*/
si::sysinit:/etc/rc.d/rc.S
/* Skrypt ten uruchamiamy kiedy przechodzimy do poziomu dla jednego
uzytkownika. Standardowo odlacza on wszystkie systemy plikow i wylacza
swap'a. */
su:S:wait:/etc/rc.d/rc.K
/* Skrypt ten uruchamiamy kiedy przechodzimy do poziomu wielouzytkownikowego.
Standardowo inicjalizuje on siec, uruchamia on demona poczty, sprawdza
CD i jesli jest to podlacza je do katalogu /cdrom. */
rc:123456:wait:/etc/rc.d/rc.M
/* Trzech kroli powoduje wylaczenie komputera. */
ca::ctrlaltdel:/sbin/shutdown -t3 -rf now
/* Co robic kiedy zanika napiecie. */
pf::powerfail:/sbin/shutdown -f +5 "THE POWER IS FAILING"
/* Jesli napiecie wraca to: */
pg:0123456:powerokwait:/sbin/shutdown -c "THE POWER IS BACK"
/* Uruchamianie odpowiedniej ilosci terminali: */
c1:12345:respawn:/sbin/agetty 38400 tty1
c2:12345:respawn:/sbin/agetty 38400 tty2
c3:45:respawn:/sbin/agetty 38400 tty3
c4:45:respawn:/sbin/agetty 38400 tty4
c5:45:respawn:/sbin/agetty 38400 tty5
c6:456:respawn:/sbin/agetty 38400 tty6
/* Poziom 6 to standardowo uruchomienie X-windows. Skrypt rc.6 wlasnie
je uruchamia */
x1:6:wait:/etc/rc.d/rc.6
/* koniec pliku /etc/inittab */
Prawda o programie init.
Tak naprawde to program init dziala troche inaczej. Sa to w zasadzie dwa programy w jednym. Jeden jest wykonywany jezeli identyfikatorem procesu jest liczba 1, natomiast drugi wpp. Pierwszy program to wlasnie ten, ktory opisalem na tej stronie, natomiast drugi sluzy do powiadomienia "prawdziwego inita", ze chcemy zmienic poziom dzialania.
algorytm: Prawdziwy init;
wejsie: Poziom, na ktory chcemy przejsc, lub brak jesli to ma byc proces
o id=1.
wyjsie: brak;
{
Pobierz identyfikator twojego procesu;
if (jest on rowny jeden) wykonaj algorytm
init
else
if (id uzytkownika jest rozny od zera) return; /* zmiany poziomu moze
dokonac tylko root */
else { /* zmieniamy poziom */
Zapisz w pliku /etc/initrunlevel jaki ma byc nowy poziom;
Wyslij do "prawdziwego inita" sygnal SIGHUP; /* Patrz obsluge
tego sygnalu przez init, tutaj */
} /* zmieniamy poziom*/
} /* else */
} /* Prawdziwy init */
Pozwolilem sobie na takie male oszustwo, zeby opis dzialania inita bylo zgodny z opisem przedstawianym w wielu ksiazkach np Bacha, Stevensa. Mam nadzieje, ze zostanie mi to wybaczone.