Do spisu tresci tematu 7
Podprogram obslugi terminali
Spis tresci
Wprowadzenie
Urzadzenia terminalowe (dalej bedziemy je nazywac terminalami) sa szczegolnym przypadkiem urzadzen znakowych, przeznaczonymi do "kontaktu" systemu z ludzmi. Ich osobliwosc polega glownie na tym, ze jako urzadzenia przeznaczone do komunikacji z czlowiekiem maja nieco zmieniana semantyke funkcji systemowej read. Dlaczego ? Otoz jesli wywolujemy read(..) z zadaniem wczytania pewnej ilosci bajtow, to oczekujemy ze jej wykonanie zakonczy sie dopiero wtedy gdy te bajty zostana wczytane lub zostanie osiagniety koniec pliku (To jest nie do konca prawdza w przypadku laczy i temu podobnych). W przypadku terminali takie podejscie jest nieprzydatne, gdyz ludzie, gdy np.wprowadzaja polecenia, to oczekuja reakcji programu po nacisnieciu klawisza ENTER, a nie gdy wprowadza np. 10 (lub 100) znakow. Oczywiscie ktos moglby powiedziec, ze program moglby za kazdym razem zadac odczytu tylko jednego bajtu i nastepnie sobie skladac cale polecenie po kolei. To podejscie jednak ma powazne wady:
- Koszt - wczytanie 100 znakowego polecenia to 100 (co najmniej ) wywolan fukcji systemowej (A wywolanie funkcji systemowej to przejscie do trybu jadra,czesto zmiana kontekstu itd itp)
- Czesto chcielibysmy aby wiekszosc znakow byla traktowana jednakowo we wszystkich programach (np TAB lub BACKSPACE) - trudno to zachowac gdy kazdy program sam decyduje co ma robic z kazdym wczytanym znakiem.
Jeszcze jednym powodem specjalnego znaczenia terminali jest pojecie sesji.
Po prostu gdy uzytkownik zasiadzie przy klawiaturze, wejdzie do systemu i,
za pomoca polecen systemowych zacznie tworzyc procesy, to beda one (powinny
byc) w pewien sposob zwiazane z tym urzadzeniem, po to, aby w momencie
zakonczenia pracy mogly zostac powiadomione o tym fakcie(a nawet
zakonczone.)
- Pojecia
- Terminal sterujacy - terminal opisywany przez strukture tty_struct wskazywana przez pole tty w task_struct. Z tego terminala proces dostaje sygnaly SIGHUP,SIGINT,SIGSTP itp. Proces zwykle dziedziczy po swoim przodku ten terminal lub moze uzyskac go za pomoca wywolania funkcji ioctl(...).
- Grupa terminalowa - Zbior procesow majacych ten sam terminal sterujacy. W Systemie V procesy nalezace do jednej grupy terminalowej, w przypadku odlaczenia terminala(ang. hangup) dostawaly sygnal SIGHUP. W Linux'ie jest mozliwe zarzadzanie przypisaniami terminali do procesow tak jak jest to robione w sytemie BSD (a nie w Systemie V) tzn. gdy wywolujemy funkcje setpgrp(...) to wtedy stajemy sie w pewnym sensie odrebna grupa terminalowa i sygnal SIGHUP jaki odbierze przywodca grupy terminalowej, do nas nie zostanie wyslany. (W szczegolnosci oznacza to, ze jesli wywolamy jakies polecenie w tle (w tzw. interpretatorze sterujacym pracami (Wg terminologi Stevensa) - takim jest np. bash) i sie wylogujemy to proces uruchomiony w tle nie dostanie sygnalu SIGHUP.)
- Przywodca grupy terminalowej - proces ktorego identyfikator sesji (pole session w task_struct) jest rowny jego identyfikatorowi.
- Numeracja urzadzen - w Linux'ie zastosowano bardzo ciekawy sposob numeracji urzadzen.Otoz system ten zapewnia obsluge 63 konsol, 64 terminali podlaczanych do portow szeregowych, 64 pseudoterminali nadrzednych, 64 pseudoterminali podrzednych. Kazde uzadzenie terminalowe w Linux'ie ma numer glowny 4. Podzial numerow drugorzednych jest nastepujacy
- Konsole maja numery 1-63
- Terminale podlaczone do linii szeregowych maja numery 64-127
- Pseudoterminale nadrzedne maja numeru 128-191
- Pseudoterminale podrzedne maja numery 191-255,przy czym pseudoterminal podrzedny o numerze n odpowiada pseudoterminalowi nadrzednemu o numerze n-64.
Natomiast urzadzenie o numerze gl. 4 i drugorzednym 0 jest aktywna (biezaca konsola), ta na ktorej wlasnie pracujemy; dobrym okresleniem na nia byloby:"ta do ktorej kierowane sa znaki z klawiatury". Nie nalezy tego mylic urzadzeniem "/dev/tty", ktore jest terminalem sterujacym konkretnego procesu.
- Struktury
- Termio i termios
Jesli ktos czytal podrecznik programowania w systemie Unix'owym to byc moze spotkal sie ze struktura termio opisujaca podstawowe parametry pracy terminala( Jak traktowac tabulacje, czy wyswietlac na ekranie echo wprowadzanych znakow - wazne przy np. wprowadzaniu hasla , czy ENTER oznacza przejscie do nowej linii , szybkosc transmisjii itp) W systemie Linux sa zdefiniowane 2 takie struktury:
- termio (zdefiniowana w /include/asm/termios.h ) - dla zachowania zgodnosci z wczesniej napisanymi programami
- termios (zdefiniowana w include/asm/termbits.h) - uzywana wszedzie w jadrze i udostepniana rowniez (oprocz termio) przez funkcjie ioctl(...)
Roznica miedzy tymi 2 strukturami jest niewielka: po prostu pewne pola w termio sa typu "unsigned short" a odpowiadajace im pola w termios maja typ "unsigned int" (bo tctflag_t to unsigned int przynajmnniej w konfiguracji dla procesorow 80x86). Prawdopodbnie chodzi o przyspieszenie dostepu do 32-bitowych liczb.
- Pole termios_locked - w strukturze tty_struct (podobnie jak w tty_driver) wystepuja pola nazwane termios_locked. Ich znaczenie jest dosc ciekawe: otoz jesli pewien bit na tym polu jest ustawiony,to wywolania funkcji ioctl(..) nie sa w stanie zmienic wartosci bitu o tym samym numerze w polu termios. Tak wiec pole to zabezpiecza przed "lekkomyslnym" zmienianiem flag sterujacych terminalem.(Aby moc zmienic takie bity musimy najpierw skasowac bity w polu termios_locked, a dopiero pozniej zmieniac bity w polu termios)
- tty_struct - podstawowa struktura opisujaca terminal (jest ona zdefiniowana w /include/linux/tty.h)
- tty_ldisc - (/include/linux/tty_ldisc.h) struktura zawierajaca wskazniki do kolejnych funkcji udostepnianych przez dyscypline linii(dyscyplina linii bedzie omowiona dalej), cos podobnego do file_operations + pare liczb nieistotnych z naszego punktu widzenia.
- tty_driver - struktura opisujaca sterownik urzadzenia (zdefiniowana w /include/linux/tty_driver.h) Zawiera ona rowniez deklaracje funkcji stanowiacych interfejs do konkretnego urzadzenia (tym razem urzadzenie nalezy rozumiec jako "sprzet" - na bardzo niskim poziomie)
- Listy znakowe - sa opisywane w ksiazce M.Bacha - w Linux'ie ich nie ma, sa statyczne bufory.
- Algorytmy
- tty_open
- Wykonaj funkcje init_dev(..) (Bedzie opisana pozniej)
- Jesli sterownik zwiazany z urzadzeniem terminalowym ma procedure otwarcia (czyli driver->open !=NULL) to ja wykonaj wpp. zwroc blad ENODEV.
- tty_read(..)
- Sprwadz czy nie zaszla sytuacja niemozliwa (np.struktura opisujaca urzadzenie terminalowe jest pusta)
- Wywolaj funkcje tty->ldisc->read
- Ustaw i-node.i-atime na currenttime
- tty_write(..) praktycznie tak samo jak tty_read(...)
- init_dev(...) Zadaniem tej funkcji jest zaalokowanie pamieci i zainicjowanie struktury opisujacej urzadzenie terminalowe. Jej algortytm jest nastepujacy:
- Znajdz strukture opisujaca sterownik terminala (get_tty_driver())
- Ze struktury opisujacej sterownik urzadzenia pobierz wskazniki do struktur opisujacych nasze urzadznie:tty,termios,termios_locked (byc moze beda one mialy wartosc NULL)
- Etykietka repeat
- Dla kazdej ze struktur wykonaj: Jesli wskaznik do struktury byl NULL to zaalokuj pamiec(algorytm get_free_page()) i wroc do punktu repeat wpp. skocz do punktu end_init
- Jesli otwierane urzadzenie bylo urzadzeniem PTY (pseudoterminalem) to powtorz poprzedni krok aby otworzyc drugiego czlonka pary.
- Etykieta end init
Ten algorytm zostal napisany w ten sposob, gdyz moze wystapic "wyscig procesow". Chodzi mniej wiecej o to, ze musimy tutaj 3-krotnie zaalokowac strone pamieci i moze sie zdazyc, ze np. proces A zaalokowal 1 strone, zostal wywlaszczony przez proces B,otwierajacy ten sam terminal, ktoremu sie udalo wykonac init_dev() w calosci. Wtedy proces A powinien oddac ta jedna strone ktora zaalokowal i skorzystac ze strony zaalokowanej przez proces B.
- do_tty_hangup(...) - jest to charakterystyczna dla terminali funkcja. Jej zadaniem jest poinformowanie procesow podlaczonych do danego terminala o tym, ze nastapilo zerwanie komunikacji np. uzytkownik sie wylogowal lub nastapilo przerwanie transmisji przez lacze szeregowe. Algorytm tej funkcji jest nastepujacy:
- Przegladaj wszystkie i-wezly plikow spejalnych i dla tych, ktore sa zwiazane ze zrywanym wlasnie terminalem wykonaj funkcje tty_fasync(..) (Musimy to robic, gdyz dwa pliki specjalne moga wskazywac na jedno urzadzenie, patrz M.Bach "Budowa syst. oper. Unix")
- Ustaw operajce dostepne na pliku na operacje otrzymane jako parametr (z tego co zauwazylem, zawsze dostaje zestaw fops_tty_hung_up, w ktorym wiekszosc wskaznikow ma wartosc NULL, badz wskazuja na funkcje zwracajace 0 lub 1)
- Oproznij bufor dyscypliny linii.(ldisc->flush_buffer())
- Oproznij bufor sterownika(driver->flush_buffer())
- Zamknij aktualna dyscypline linii i zainicjiuj dyscypline N_TTY
- Dla kazdego procesu, ktory spelnia warunki: Nalezy do sesji zwiazanej z danym terminalem (p->session==tty->session) jest przywodca grupy terminalowej (p->leader==1) wyslij sygnaly SIGHUP,SIGCNT i jego terminal sterujacy ustaw na NULL.
- Wywolaj funkcje zerwania dla sterownika (driver->hangup())
- tty_ioctl(..) Jak wiadomo funkcje ioctl() jedynymi funkcjami, ktore maja prawo zachowywac sie roznie w zaleznosci od urzadzenia. Algorytm tej funkcji jest taki:
- Wykonaj dluga instrukcje switch()
- Jesli parametr "cmd" nie zostal znaleziony to sprobuj wykonac instrukcje driver->ioctl(),jesli zwroci ona blad EIOCTLCMD, to sprobuj wykonca ldisc->ioctl()
Przez dyscypline linii rozumiemy wstepne przetwarzanie znakow wejsciowych (czasmi tez wyjsciowych) na drodze terminal -> proces. Jest wprowadzone dlatego, zeby miec pewien standardowy sposob obslugi pewnych kombinacji klawiszy, np. Ctl-Z lub Ctl-C, aby mozna bylo systemowi zlecic takie rzeczy jak wyswietlanie (lub nie) echa wprowadzonych znakow itp. Mozna sie oczywiscie sprzeczac czy taki fragment kodu powinien byc w jadrze systemu operacyjnego, ale zwykle rzadko kiedy chcemy (chcielismy ?) czytac surowe wejscie-wyjscie, a implementacja takiej obrobki znakow w programach bylaby zapewne tylko dublowaniem kodu.
Interfejs do dyscypliny linii jest zdefiniowany w /include/linux/tty_ldisc.h. Tu mozna jedynie dodac ze w Linux'ie sa zdefiniowane nastepujace dyscypliny linii:
- N_TTY
- N_MOUSE
- N_SLIP
- N_PPP
- N_STRIP
Trzy ostatnie dyscypliny linii, to w rzeczywistosci implementacje protokolow sieciowch, natomiast N_MOUSE jestu uzywana na komputerach SUN ale nie wglebialem
sie co ona wlasciwie robi.
Teraz omowie najwazniesza (chyba) dyscypline nazywajaca sie N_TTY. Jest to ta dyscyplina ktorej efekty widza wszyscy pracujacy pod Linux'em; to ona wysyla SIGINT po nacisnieciu Ctl-C albo SIGSTP po nacisnieciu Ctl-Z,dzieki niej mamy na ekranie echo naciskanych klawiszy.A oto jej
2 algorytmy algorytmy:(W nawiasie nazwa w interfejsie)
- write_chan(...) (write)
- Sprawdz,czy terminal nie jest zastopowany, jesli tak to koniec
- W pp.wstaw sie na kolejke procesow czekajacych na zapis do urzadzenia i w petli wykonuj
- Sprawdzaj czy np. polaczenie nie jest zerwane
- Jesli jest ustawiona flaga "koncowego przetwarzania wejscia"(O_POST) to: w petli pobieraj pojedyncze znaki, przetwarzaj je az uzbiera sie pelny bufor sterownika,potem wywolaj flush_chars() o ile istnieje
- Jesli flaga O_POST nie byla ustawiona to po prostu wywolaj driver->write()
- wykonaj schedule()
- przenies sie z kolejki procesow czekajacych na zapis do kolejki procesow gotowych.
- read_chan() (read)
- Sprawdz, ile jest znakow do przeczytania. Jesli w buforze jest za malo to wstaw sie na kolejke procesow czekajacych na odczyt( o ile nie zostalismy wywolani z flaga O_NDELAY) i wykonaj schedule()
- Sprawdz czy np. nie zostalo zerwane polaczenie
- Skopiuj wszystkie znaki do przestrzeni uzytkownika poza znakami __DISABLED_CHAR
- Jesli zwolnila sie odpowiednia ilosc miejsca w buforze wejsciowym to wykonaj funkcje driver->unthrottle(...) (Bedzie omowiona dalej)
- Zmniejsz wartosc opisujaca ile chcemy wczytac znakow i przejdz do p-tu 1.
- n_tty_receive_buf()
- jest wolana z dolu przez sterownik zajmujacy sie odbiorem znakow np. z klawiatury. Algorytm tej funkcji jest prosty.
- Jesli terminal pracuje w trybie surowym (real_raw) to po prostu przekopiuj znaki z bufora "flip" do bufora uzytkownika
- W przeciwnym wypadku wywolaj funkcje zalezna od flagi znaku. W przypadku konsoli nie mozna sie raczej spodziewac czegokolwiek innego niz TTY_NORMAL lub TTY_BREAK (pozostale flagi sa zwiazane z liniami szeregowymi).
- Jesli flaga byla TTY_NORMAL, to jest wolana funkcja n_tty_receive_char(...), ktora analizuje otrzymane znaki (w przypadku innych flag odpowiednie funkcje po prostu wstawiaja do bufora pewne ustalone znaczki lub przekazuja blad )
- n_tty_receive_room() - ta funkcja zwraca ilosc wolnego miejsca w buforze wejsciowym tzn. tym do ktorego pisze sterownik (lub , w przypadku pseudoterminali, drugi czlonek pary).
- n_tty_receive_char() Ta funkcja dostaje pojedynczy wczytany znak i, interpretujac go, realizuje efekty o ktorych mowilem tzn. zminia stany terminala,wyswietla echo,generuje sygnaly itp.
Jak powiedzialem wczesniej, w systemie moga byc rozne rodzaje urzadzen terminalowych. Tym, co je rozni jest wlasnie to jaki maja sterownik, tzn. modul dokonujacego koncowego "wyrzucenia" znakow i jednoczesnie odczytujacego znaczki z zewnatrzi i za pomoca ldisc->receive_buf() przekazujacym je do dyscypliny linii. Obecnie w Linux'ie jest zdefiniowane 5 rodzajow sterownikow:
- TTY_DRVER_TYPE_SYSTEM
- TTY_DRIVER_TYPE_CONSOLE czyli konsole.
- TTY_DRIVER_TYPE_SERIAL czyl terminale podlaczane do linii szeregowych
- TTY_DRIVER_TYPE_PTY - pseudoterminale, dziela sie one na 2 podtypy:
- PTY_TYPE_MASTER - nadrzedny
- PTY_SLAVE - podrzedny
- TTY_DRIVER_TYPE_SCC - jesli dobrze zrozumialem sterownik podlaczony do radiostacji
Jednak w systemie sa uzywane tylko 3 z nich tzn. sterowniki:konsoli, pseudoterminali i lacza szeregowego. (Choc zapewne pozostale tez daloby sie skonfigurowac.)
Dokladna postac interfejsu sterownika urzadzen jest podana w /include/linux/tty_driver.h. Tu opowiem tylko o 2 funkcjach, ktore moga sie wydac nieoczywiste: throttle(...) i unthrottle(...) Jak napisalem wczesniej, przy opisie struktury dyscypliny linii sterownik, po odebraniu znakow ze swiata zewnetrznego, wywoluje funkcje dyscypliny linii
receive_buf(...). Wtedy dyscyplina linii, analizujac to, co otrzymala, moze dojsc do wniosku, ze nalezaloby przyhamowac nieco sterownik i w tym celu wywoluje funkcje driver->throttle(..). Natomiast gdy sytuacja wroci do bezpiecznego stanu,dyscyplina linii moze poinformowac o tym sterownik wywolujac funkcje driver->unthrottle()
Z jakich plikow korzystalem:
- /drivers/char/tty_io.c podstawowe funkcje zdefiniowane dla terminali, tzn. tty_open(...), tty_close(...), itp, ponadto funkcje wywolywane gdy nastapilo zerwanie polaczenia , a jakis proces chce czytac lub pisac do danego terminala (bo sie uodpornil na SIGHUP)
- /drivers/char/n_tty.c - podstawowe funkcje zdefiniowane dla dyscypliny linii N_TTY (jest to taka normalna, znana wszystkim,dyscyplina, powodujaca np. wyslanie sgnalu SIGINT po nacisnieciu Ctrl-C, lub SIGSTP po Ctrl-Z)
- /drivers/char/tty_ioctl.c - tutaj jest zdefiniowana funkcja n_tty_ioctl() i nic poza tym.
- /drivers/char/keyboard.c - definicja paru funkcji obslugjacych klawiature.
- /drivers/char/pty.c - definicje funkcji dla pseudoterminali
- /include /linux/tty.h - definicja struktury tty_struct
- /include/linux/tty_ldisc.h - definicja struktury i jednoczesnie interfejsu dyscypliny linii
- /include/linux/tty_driver.h - definicja interfejsu do sterownika urzadzenia terminalowego.
- Ponadto pliki naglowkowe dotyczace zarzadzania procesami i obslugi przerwan.
Ograniczenia:
- Moze byc maks. 64 urzadzenia terminalowe jednego rodzaju
- Moze byc w sytemie zdefiniowanych 16 roznych dyscyplin linii.
Ciekawe rozwiazania programistyczne: Mi osobiscie najbardziej podobala obrobka klawiszy za pomoca tablicy odwzorowan. (Patrz nastepny plik).Prawde mowiac jest to jedyny powod dla ktorego napisalem cos o konsoli.
Najwazniejsze struktury zostaly opisane wyzej (wraz z linkami do plikow)
Autor Kamil Jonca