Programowanie w środowisku systemu UNIX
 8320426693 [PDF]

  • 0 0 0
  • Gefällt Ihnen dieses papier und der download? Sie können Ihre eigene PDF-Datei in wenigen Minuten kostenlos online veröffentlichen! Anmelden
Datei wird geladen, bitte warten...
Zitiervorschau

Wydawnictwa NaukowoTechniczne

\

W. Richard Stevens

Programowanie w środowisku @ systemu UNIX

Wydawnictwa NaukowoTechniczne Warszawa

CLASILA W. Richard Stevens

W skład serii „Klasyka Informatyki" wchodzą dzieła najwybitniejszych uczonych świata w dziedzinie informatyki - książki o nieprzemijającej wartości, stanowiące bazę solidnego, klasycznego wykształcenia każdego profesjonalnego informatyka. Wydawnictwa Naukowo-Techniczne przygotowały tę serię ze szczególną pieczołowitością, powierzając tłumaczenie poszczególnych tomów znakomitym specjalistom. Wyboru książek dokonano w ścisłej współpracy z polskim środowiskiem akademickim, dedykując serię głównie studentom informatyki i młodym pracownikom naukowym.

Programowanie w środowisku @ systemu UNIX Z angielskiego przełożyła

i

Maja Górecka-Wolniewicz ;

0 Autorze: W. Richard Stevens był uznanym w świecie autorytetem w dziedzinie oprogramowania sieciowego i autorem bestsellerów informatycznych. Niemal każda napisana przez niego książka natychmiast zdobywała uznanie czytelników. A to z kilku powodów. Po pierwsze, był znakomitym fachowcem, doskonale znającym się na swojej dziedzinie. Po drugie, miał wspaniałe wyczucie potrzeb czytelników; pisał o tym, co było ważne 1 co rzeczywiście musieli wiedzieć. Po trzecie, pisał jasno i zwięźle; przekazywane przez niego informacje były rzetelne i na dobrym poziomie szczegółowości. Jego książki zawierają mnóstwo przykładowych programów, które mogą stanowić bibliotekę gotowych rozwiązań. Urodził się w 1951 roku w Luanshyi, w Zambii (dawnej Rodezji Północnej). W 1968 roku ukończył Fishburne Military School, a w 1973 roku studia w University of Michigan ze stopniem bakałarza techniki lotniczej i astronautycznej. W latach 1973-1975 był programistą (w językach asemblerowych) w firmie Singer's M&M w Santa Ana, w Kalifornii. W latach 1975-1982 pracował na pełnym etacie w Kitt Peak Observatory w Tucson, w Arizonie (jednocześnie robił magisterium i doktorat z inżynierii systemów w University of Arizona). Zajmował się wtedy pisaniem programów do pozyskiwania danych w czasie rzeczywistym i do przetwarzania obrazów.

Przez następnych osiem lat (1982-1990) był wiceprezesem Health Systems International i tworzył oprogramowanie do systemu ochrony zdrowia, wprowadzonego w stanie Connecticut. Od 1990 roku mieszkał w Tucson. Nie miał stałego zatrudnienia. Cały czas poświęcał na pisanie książek i prowadzenie wykładów na ich podstawie. Zmarł 1 września 1999 roku.

Systemowi Michigan Terminal System, czyli MTS, i komputerowi 360/67

ine o oryginale r

. RlCHARD STEVENS

dvanced Programming in the UNIX® Environment ipyright © 1993 by Addison Wesley Longman, Inc. th Printing July 1999 iblished by arrangement with Addison Wesley Longman, Inc. Rights Reserved

Spis treści

owadzenie serii Elżbieta Beuermann ;daktor Zuzanna Grzejszczak cładkę i strony tytułowe projektował Paweł G. Rubaszewski :daktor techniczny Anna Szeląg rekta Zespół zygotowanie do druku Marianna Zadrożna

JIX jest zarejestrowanym znakiem towarowym UNIX System Labs, Inc.

Copyright for the Polish edition by ydawnictwa Naukowo-Techniczne arszawa 2002 1 Rights Reserved inted in Poland wór w całości ani we fragmentach nie może być powielany ani rozpowszechniany pomocą urządzeń elektronicznych, mechanicznych, kopiujących, nagrywających nnych, w tym również nie może być umieszczany ani rozpowszechniany w postaci frowej zarówno w Internecie, jak i w sieciach lokalnych bez pisemnej zgody •siadacza praw autorskich.

ires poczty elektronicznej: [email protected] rona WWW: www.wnt.com.pl

5BN 83-204-2669-3

Przedmowa

1'

\ Pojęcia podstawowe

2'.

1.1 1.2 1.3 1.4 1.5 1.6 1.7 1.8 1.9 1.1.0 1.11 1.12

2. 2. 2 2 3 3 3 4 4 4. Ą 4]

Wprowadzenie Logowanie w systemie Pliki i katalogi Wejście i wyjście Programy i procesy Właściwości ANSI C Obsługa błędów Identyfikacja użytkownika Sygnały Czas w systemie Unix Funkcje systemowe i biblioteczne Podsumowanie

i ,

Standaryzacja systemu Unix, różne implementacje 2.1 2.2

Wprowadzenie Standaryzacja systemu Unix 2.2.1 ANSIC 2.2.2 IEEE POSIX 2.2.3 X/Open XPG3 2.2.4 FIPS

.

1

^

4 3 5 1 5

Spis treści 2.3 2.4 2.5

2.6 2.7 2.8 2.9

Implementacje systemu Unix 2.3.1 System V Wydanie 4 2.3.2 System 4.3+BSD Standardy a ich implementacje Ograniczenia 2.5.1 Ograniczenia ANSI C 2.5.2 Ograniczenia POSIX 2.5.3 Ograniczenia XPG3 275.4 Funkcje sysconf, pathconf i fpathconf 2.5.5 Wymagania FIPS 151-1 2.5.6 Podsumowanie ograniczeń 2.5.7 Nieokreślone ograniczenia fazy wykonania Makra sprawdzające parametry Elementarne systemowe typy danych Konflikty między standardami Podsumowanie

53 54 54 55 56 57 58 60 6K 67 67 70 73 75 75 76

J Operacje wejścia-wyjścia dla plików 3.1 3.2 3.3 3.4 3.5 3.6 3.7 3.8 3.9 3.10 3.11 3.12 3.13 3.14 3.15 3.16

Wprowadzenie Deskryptory plików Funkcja open Funkcja c r e a t Funkcja c l o s e Funkcja l s e e k Funkcja read Funkcja w r i t e Wydajność obsługi wejścia-wyjścia Współdzielenie pliku Operacje atomowe Funkcje dup i dup2 Funkcja f c n t l Funkcja i o c t l Katalog /dev/fd Podsumowanie

77 77 77 78 81 81 82 85 86 87 89 92 94 96 101 102 104

_4 Pliki i katalogi

106

4.1 4.2 4.3

106 106 107

Wprowadzenie Funkcje s t a t , f s t a t oraz l s t a t Typy plików

Spis treści 4.4 4.5 4.6 4.7 4.8 4.9 4.10 4.11 4.12 4.13 4.14 4.15 4.16 4.17 4.18 4.19 4.20 4.21 4.22 4.23 4.24 4.25 4.26

! Bity ustanowienia identyfikatora użytkownika oraz ustanowienia identyfikatora grupy Prawa dostępu do pliku Prawa własności nowych plików i katalogów Funkcja access Funkcja umask Funkcje chmod i f chmod Bit lepki Funkcje chown, fchown oraz lchown Rozmiar pliku Skracanie plików Systemy plików Funkcje l i n k , unlink, remove i rename Dowiązania symboliczne Funkcje symlink i r e a d l i n k Czasy związane z plikiem Funkcja utime Funkcje m k d i r i r m d i r Czytanie katalogów Funkcje c h d i r , f c h d i r oraz getcwd Specjalne pliki urządzeń Funkcje s y n c oraz f s y n c Zestawienie bitów praw dostępu do pliku Podsumowanie

11 ( li; 11; 1 ]< lii 11! 12: \2'. 12: 12' 12' 13 13: 13) 13) 13< 14^ 14^ 14* 15: 15* 15: 15(

5 Standardowa biblioteka wejścia-wyjścia

15S

5.1 5.2 5.3

15? 15?

5.4 5.5 5.6 5.7 5.8 5.9 5.10 5.11 5.12 5.13 5.14 5.15

Wprowadzenie Obiekty strumieni i typ danych F I L E Standardowe wejście, standardowe wyjście i standardowy strumień komunikatów awaryjnych Buforowanie Otwarcie strumienia Czytanie i zapisywanie danych ze strumienia Wejście-wyjście wiersz po wierszu Wydajność standardowego wejścia-wyjścia Binarne wejście-wyjście Pozycjonowanie strumienia Wejście-wyjście formatowane Szczegóły implementacyjne Pliki tymczasowe Techniki zastępujące standardowe wejście-wyjście Podsumowanie

16'. 16f 16^ -6', 51\ 17^ 17--' 17j 17' 1^ 18 i 18* 183

Spis treści

Systemowe pliki danych

187

6.1 6.2 6.3 6.4 6.5 6.6 6.7 6.8 6.9 6.10

Wprowadzenie Plik haseł Maskowanie haseł Plik grup Identyfikatory dodatkowych grup Inne pliki danych Rejestry logowania Dane identyfikujące system Procedury obsługi czasu i daty Podsumowanie

187 187 191 191 193 194 196 196 198 202

Środowisko procesu w systemie Unix

203

7.1 7.2 7.3 7.4 7.5 7.6 7.7 7.8 7.9 7.10 7.11 7.12

203 203 204 208 208 210 211 212 215 219 225 229

_7 Wprowadzenie Funkcja main Zakończenie procesu Argumenty wiersza poleceń Lista zmiennych środowiskowych Program w języku C w pamięci operacyjnej Biblioteki wspólne Alokacja pamięci Zmienne środowiskowe Funkcje setjmp oraz longjmp Funkcje g e t r l i m i t oraz s e t r l i m i t Podsumowanie

JS Sterowanie procesem

231

8.1 8.2 8.3 8.4 8.5 8.6 8.7 8.8 8.9 8.10

231 231 232 238 241 243 249 250 254 261

Wprowadzenie Identyfikatory procesu Funkcja fork Funkcja vf ork Funkcja e x i t Funkcje wait oraz w a i t p i d Funkcje w a i t 3 oraz wait4 Sytuacje wyścigu Funkcje exec Zmiana identyfikatorów użytkownika i identyfikatorów grup . . .

Spis treści 8.11 8.12 8.13 8.14 8.15 8.16

11 Pliki interpretowane Funkcja system Rejestrowanie procesów Identyfikacja użytkownika Czasy procesu Podsumowanie

266 271 276 282 283 286

9 Relacje między procesami

288

9.1 9.2 9.3 9.4 9.5 9.6 9.7 9.8 9.9 9.10 9.11 9.12

288 288 293 295 297 298 300 300 305 310 313 315

Wprowadzenie Logowania terminalowe Logowania sieciowe Grupy procesów Sesje Terminal sterujący Funkcje t c g e t p g r p oraz t c s e t p g r p Sterowanie zadaniami Wykonywanie programów z powłoki Osierocone grupy procesów Implementacja w systemie 4.3+BSD Podsumowanie

10 Sygnały 10.1 10.2 10.3 10.4 10.5 10.6 10.7 10.8 10.9 10.10 10.11 10.12 10.13 10.14 10.15 10.16 10.17

Wprowadzenie Koncepcje sygnałów Funkcja s i g n a l Sygnały niezawodne Przerwane funkcje systemowe Funkcje współużywalne Różne semantyki SIGCLD Terminologia i semantyka sygnałów niezawodnych Funkcje k i l l oraz r a i s e Funkcje a l a r m o r a z p a u s e Zbiory sygnałów Funkcja s i g p r o c m a s k Funkcja s i g p e n d i n g Funkcja sigaction Funkcje s i g s e t jmp oraz s i g l o n g j m p Funkcja s i g s u s p e n d Funkcja a b o r t

317-

317 318 327 331 333 335 j 338 • 341 342 344 i 351 353 , 354 ] 35 7 _' 360, 364 371 ,

Spis treści 10.18 10.19 10.20 10.21 10.22

Funkcja system Funkcja s l e e p Sygnały sterowania zadaniami Inne właściwości Podsumowanie

372 379 382 384 386

\l Terminalowe wejście-wyjście

388

11.1 11.2 11.3 11.4 11.5 11.6 11.7 11.8 11.9 11.10 11.11 11.12 11.13 11.14

388 388 395 401 402 410 410 411 412 416 420 427 428 429

Wprowadzenie Informacje podstawowe Specjalne znaki na wejściu Pobieranie i ustalanie atrybutów terminalu Sygnalizatory opcji terminalu Polecenie s t t y Funkcje związane z szybkością linii Funkcje do kontroli linii Identyfikacja terminalu Tryb kanoniczny Tryb niekanoniczny Rozmiar okna terminalu termcap, terminf o oraz c u r s e s Podsumowanie

_12 Zaawansowane operacje wejścia-wyjścia

431

12.1 12.2 12.3 12.4 12.5

431 431 435 453 467 470 474 477 477 478 479 482 484 490

Wprowadzenie Nieblokujące wejście-wyjście Ryglowanie rekordów Strumienie Zwielokrotnianie wejścia-wyjścia 12.5.1 Funkcja s e l e c t 12.5.2 Funkcja p o l l 12.6 Asynchroniczne wejście-wyjście 12.6.1 System V Wydanie 4 12.6.2 System 4.3+BSD 12.7 Funkcje readv oraz w r i t e v 12.8 Funkcje readn oraz w r i t e n 12.9 Wejście-wyjście oparte na odwzorowaniu w pamięci 12.10 Podsumowanie

Spis treści

13

13 Procesy-demony

492

13.1 13.2 13.3 13.4

492 492 494 496 496 498 502 503

13.5 13.6

Wprowadzenie Charakterystyki demona Zasady programowania demonów Rejestrowanie błędów 13.4.1 Procedura obsługi strumieni log w systemie SVR4 13.4.2 Technika s y s l o g w systemie 4.3+BSD Model klient-serwer Podsumowanie

14 Komunikacja międzyprocesowa

504

14.1 14.2 14.3 14.4 14.5 14.6

Wprowadzenie Łącza komunikacyjne Funkcje popen oraz p c l o s e Koprocesy Kolejki FIFO Metody komunikacji międzyprocesowej Systemu V 14.6.1 Identyfikatory i klucze 14.6.2 Struktura zawierająca prawa dostępu 14.6.3 Ograniczenia konfiguracyjne 14.6.4 Zalety i wady 14.7 Kolejki komunikatów 14.8 Semafory 14.9 Pamięć wspólna 14.10 Zagadnienia dotyczące modelu klient-serwer 14.11 Podsumowanie

504 505 512 519 524 528 528 530 531 531 533 538 545 553 556

15

__

Zaawansowane metody komunikacji międzyprocesowej

559

15.1 15.2 15.3

559559 563 566 568 571 574 581 583 587

15.4 15.5

Wprowadzenie Łącza strumieniowe Przekazywanie deskryptorów pliku 15.3.1 System V Wydanie 4 15.3.2 System 4.3BSD 15.3.3 System 4.3+BSD Serwer otwierający pliki, wersja 1 Funkcje obsługujące połączenia klient-serwer 15.5.1 System V Wydanie 4 15.5.2 System 4.3+BSD

Spis treści 15.6 15.7

Serwer otwierający pliki, wersja 2 Podsumowanie

591 600

16 Biblioteka funkcji obsługi bazy danych

601

16.1 16.2 16.3 16.4 16.5 16.6 16.7 16.8 16.9

601 601 602 604 608 610 612 635 640

Wprowadzenie Historia Biblioteka Przegląd implementacji Obsługa scentralizowana lub zdecentralizowana Współbieżność Kod źródłowy Wydajność Podsumowanie

_17 Komunikacja z drukarką postscriptową

642

17.1 17.2 17.3 17.4 17.5

642 642 646 648 671

Wprowadzenie Komunikacja w trybie PostScript Buforowanie wydruków Kod źródłowy Podsumowanie

^8 Program obsługujący modem

672

18.1 18.2 18.3 18.4 18.5 18.6 18.7 18.8 18.9

672 673 674 675 679 680 711 714 727

Wprowadzenie Historia Projekt programu Pliki danych Projekt serwera Kod źródłowy serwera Projekt klienta Kod źródłowy klienta Podsumowanie

\9 Pseudoterminale

729

19.1 19.2

729 729

Wprowadzenie Informacje podstawowe

Spis treści 19.3 19.4 19.5 19.6 19.7 19.8

J_£ Otwieranie urządzeń pseudoterminalowych 19.3.1 System V Wydanie 4 19.3.2 System 4.3+BSD Funkcja pty_f ork Program p t y Zastosowanie programu p t y Dodatkowe możliwości Podsumowanie

736 736 739 741 743 748 756 757

Dodatek A Prototypy funkcji

759

Dodatek B Różne kody źródłowe

780

B. 1 B.2

780 783

Nasze pliki nagłówkowe Procedury obsługi standardowych błędów

Dodatek C Rozwiązania wybranych ćwiczeń

788

Bibliografia

819

Skorowidz

825

Przedmowa

Wprowadzenie W niniejszej książce opisuję interfejs programowania w systemie Unix funkcje systemowe oraz liczne funkcje zawarte w bibliotece standardowej języka C. Książka jest przeznaczona dla osób piszących programy, które mają być uruchamiane w systemach uniksowych. Jak większość systemów operacyjnych Unix dostarcza wiele usług, z których mogą korzystać programy: otwarcie pliku, czytanie pliku, uruchomienie nowego programu, zaalokowanie obszaru pamięci, pobranie bieżącego czasu itp. Są one wspólnie nazywane interfejsem funkcji systemowych. Oprócz tego biblioteka standardowa języka C oferuje różne funkcje, które są używane w programach pisanych w języku C (formatowanie drukowanej wartości zmiennej, porównanie dwóch napisów itp.). Interfejs funkcji systemowych oraz procedury biblioteczne są na ogół opisywane w częściach 2 i 3 uniksowego podręcznika systemowego (man pages). W tej książce nie będą powielane te fragmenty dokumentacji. Nie ma zresztą tam ani przykładów, ani żadnych uzasadnień, które są tu przedstawione.

Standardy systemu Unix Istotny wpływ na rozwój kolejnych wersji systemu Unix w latach osiemdziesiątych miały różne międzynarodowe standardy, których tworzenie rozpoczęto właśnie wtedy. Do najważniejszych należą: standard ANSI dla języka C, rodzina norm IEEE POSIX (ciągle w trakcie rozwoju) oraz zalecenia dotyczące przenośności - X/Open. Omówiłem tu te wszystkie standardy^_ale nie tylko w sposób formalny. Opisałem je również w odniesj#rri§ć^4fcr$?iłi§?ckJich implementacji - wy-

Przedmowa dania 4 Systemu V oraz mającego się ukazać systemu 4.4BSD1. Dzięki temu udało mi się przedstawić to, czego zazwyczaj brakuje w samym standardzie i w wielu książkach o nim.

(Unix Programmer's Manuał), w którym zamierza się pracować, ponieważ często odwołuję się do tego podręcznika, opisując niektóre z bardziej specyficznych i zależnych od implementacji właściwości. Niemal każdą funkcję, zwykłą lub systemową, przedstawiam wraz z małym, ale kompletnym przykładem programu, dzięki czemu Czytelnik może zobaczyć, jak wyglądają argumenty wywołania i przekazywane wartości. Ponieważ jednak niektóre z tych małych programów są wymyślone, podaję też kilka większych (rozdziały 16, 17, 18 i 19), obrazujących rzeczywiste techniki programowania, stosowane na co dzień. Wszystkie przykłady zostały zamieszczone w tekście bezpośrednio z ich plików źródłowych. Można je znaleźć w ogólnodostępnym archiwum FTP na stacji f t p . u u . n e t , w katalogu p u b l i s h e d / b o o k s / s t e v e n s . advprog. t a r . Z. Pobrawszy kody źródłowe programów, można je zmieniać i eksperymentować z nimi w konkretnych systemach operacyjnych.

lizacja książki Książkę podzieliłem na sześć części. 1. Omówienie podstawowych koncepcji programowania uniksowego, wprowadzenie terminologii (rozdz. 1) oraz przedstawienie różnych działań standaryzacyjnych i różnych implementacji systemu Unix (rozdz. 2). 2. Wejście-wyjście - niebuforowane wejście-wyjście (rozdz. 3), własności plików i katalogów (rozdz. 4), standardowa biblioteka wejścia-wyjścia (rozdz. 5) oraz standardowe pliki systemowe (rozdz. 6). 3. Procesy - środowisko procesu uniksowego (rozdz. 7), sterowanie procesami (rozdz. 8), relacje między różnymi procesami (rozdz. 9) oraz sygnały (rozdz. 10). 4. Rozszerzone informacje na temat wejścia-wyjścia - obsługa wejścia-wyjścia terminalu (rozdz. 11), złożone właściwości wejścia-wyjścia (rozdz. 12), procesy-demony (rozdz. 13). 5. Techniki IPC - metody komunikacji międzyprocesowej (rozdz. 14 i 15). 6. Przykłady - biblioteka bazy danych (rozdz. 16), komunikacja z drukarką pracującą zgodnie ze standardem Postscript (rozdz. 17), program obsługi modemu (rozdz. 18) oraz stosowanie pseudoterminali (rozdz. 19). Do zrozumienia materiału zawartego w tej książce przyda się umiejętność programowania w języku C oraz znajomość ogólnych zasad funkcjonowania systemów uniksowych. Nie trzeba natomiast mieć praktyki w programowaniu uniksowym. Jednym słowem, książka ta jest przeznaczona dla programistów obytych z systemem Unix oraz innymi systemami operacyjnymi, którzy chcą poznać szczegóły dotyczące usług dostarczanych przez większość systemów uniksowych.

kłady w tekście W książce jest wiele przykładów w języku C - ok. 10000 wierszy kodu źródłowego - przy czym ten kod jest zgodny z normą ANSI C. Przy czytaniu książki warto mieć pod ręką podręcznik programowania w systemie Unix 1

W czasie pisania książki system 4.4BSD był dopiero zapowiadany (przyp. tłum.).

19

Przedmowa

Systemy stosowane do sprawdzenia przykładów Niestety, wszystkie systemy operacyjne nadal się rozwijają i powstają coraz to nowsze ich wersje. Unix nie jest wyjątkiem. Na poniższym diagramie prezentuję kolejne wersje Systemu V oraz 4.xBSD. 4.3+BSD 4.3BSD

4.3BSD Tahoe

4.3BSD Reno BSD Net 2

BSD Net 1

1986

1987

1988

SVR3.0

SYR3.1

SYR3.2

t

1989

t

XPG3

4.4BSD ?

1990

1991

1992

SVR4 ANSIC

POSIX.l

Pod nazwą 4.xBSD kryją się różne systemy tworzone przez grupę badawczą Computer Systems Research Group at the University of California at Berkeley. Grupa ta udostępnia również wersje systemów BSD Net 1 i BSD Net 2 - publicznie dostępny kod źródłowy systemów 4.xBSD. Nazwa SVRx odnosi się do wydania x Systemu V powstałego w firmie AT&T. XPG3 to wydanie 3 dokumentu X/Open Portability Guide, czyli przewodnika na temat przenośności, a ANSI C to nazwa standardu ANSI języka C. POSIX.l jest standardem IEEE oraz ISO dla interfejsu do systemu o cechach Uniksa. Więcej informacji na temat tych standardów oraz ich kolejnych wersji Czytelnik znajdzie w podrozdziałach 2.2 i 2.3.

Przedmowa Określenie 4.3+BSD, używane w tekście, odnosi się do systemu Unix powstałego w Berkeley, który mieści się między wersjami BSD Net 2 a 4.4BSD. Gdy pisałem tę książkę, wersja 4.4BSD nie była jeszcze opublikowana. Nie mogłem więc używać tego określenia. Ponieważ trudno było obyć się bez krótkiej nazwy identyfikującej ten system, przyjąłem skrót 4.3+BSD.

Większość prezentowanych przykładów była testowana w czterech różnych wersjach systemu Unix: 1) w wersji 2.0 wydania 4.0 Systemu V (zwanej „vanilla SVR4") powstałej w U.H. Corp. (UHC), pracującej na procesorze Intel 80836; 2) w systemie 4.3+BSD (Computer Systems Research Group, Computer Science Division, University of California at Berkeley), pracującym na stacji roboczej Hewlett Packard; 3) w systemie BSD/386 (wywodzącym się z wydania BSD Net 2), utworzonym przez Berkeley Software Design, Inc., działającym na procesorach Intel 80386; system ten jest niemal identyczny z systemem 4.3+BSD; 4) w systemach SunOS 4.1.1 i 4.1.2 (mających silne korzenie berkelejowskie, a jednocześnie wiele właściwości Systemu V) firmy Sun Microsystems, pracujących na stacji SPARCstation SLC. W tekście można znaleźć wiele odwołań do licznych testowych pomiarów czasu pracy; są również podane systemy zastosowane do ich przeprowadzenia.

wiekowania Jeszcze raz stałem się dłużnikiem mojej rodziny, która okazywała mi miłość oraz wsparcie i z cierpliwością znosiła, że przez większość weekendów przez ostatnie półtora roku nie miałem dla niej czasu. Pisanie książki jest pod różnym względem sprawą rodzinną. Dziękuję Warn, Sally, Bili, Ellen i David. Szczególnie wdzięczny jestem Brianowi Kernighanowi za okazaną mi pomoc. Wielokrotnie czytał cały rękopis i z dużą dozą taktu sterował mną w kierunku ulepszenia tekstu, czego efektem jest końcowa postać książki. Wielu cennych uwag dostarczył mi Steve Rago, który nie tylko przeglądał rękopis, ale również odpowiadał na wiele pytań dotyczących szczegółów i historii Systemu V. Składam też podziękowania opiniodawcom współpracującym z wydawnictwem Addison-Wesley, z których wielu cennych uwag skorzystałem. Należeli do nich: Maury Bach, Mark Ellis, Jeff Gitlin, Peter Honeyman, John Linderman, Doug Mcllroy, Evi Nemeth, Craig Partridge, Dave Presotto, Gary Wilson i Gary Wrigte. Dzięki Keith Bostic i Kirkowi McKusickowi uzyskałem konto na uniwersytecie w Berkeley, z którego korzystałem do sprawdzenia przykładów

Przedmowa

21

w ostatniej wersji systemu BSD. (Wiele podziękowań również dla Petera Salusa). Sam Nataros i Joachim Sacksen z UHC dostarczyli mi kopię systemu SVR4, której używałem do testowania przedstawionych przykładów. Z kolei Trent Hein pomógł mi w otrzymaniu kopii wersji alfa i beta systemu BSD/386. Wielu kolegów wspierało mnie w ciągu ostatniego roku w drobnych, ale bardzo istotnych sprawach. Byli to: Paul Lucchina, Joe Godsil, Jim Hogue, Ed Tankus i Gary Wright. Mój wydawca z Addison-Wesley, John Wait, był moim wielkim przyjacielem w tym czasie. Nigdy nie narzekał, gdy nie dotrzymałem terminów, ani gdy objętość książki ciągle rosła. Specjalne podziękowania należą się National Optical Astronomy Observatories (NOAO), szczególnie Sidneyowi Wolffowi, Richardowi Wolffowi oraz Steve'owi Grandiemu za udostępnienie zasobów komputerowych. Prawdziwe książki na temat Uniksa powstają za pomocą programu t r o f f . Z tą było tak samo, a więc tradycji stało się zadość. Sam przygotowałem książkę w postaci camera-ready, używając pakietu grof f, napisanego przez Jamesa Clarka. Bardzo jestem wdzięczny Jamesowi Clarkowi za utworzenie tak wspaniałego systemu, a także za szybką pomoc w usuwaniu błędów. Być może kiedyś zrozumiem wszystkie pułapki związane z tworzeniem stopek programu t r o f f . Tucson, Arizona kwiecień 1992

W. Richard Stevens

\

Pojęcia podstawowe

1.1

Wprowadzenie Wszystkie systemy operacyjne oferują różne usługi dla uruchamianych programów. Do typowych usług należą: wykonanie nowego programu, otwarcie pliku, odczyt pliku, zalokowanie obszaru pamięci, pobranie bieżącego czasu itp. W tym rozdziale opiszemy usługi, które są dostarczane przez różne uniksowe systemy operacyjne. Opisywanie Uniksa krok po kroku bez odwoływania się do terminów, które jeszcze nie zostały zdefiniowane, jest prawie niemożliwe (i zapewne byłaby to forma nużąca). W tym rozdziale dokonamy przeglądu systemu Unix z punktu widzenia programisty. Przedstawimy krótkie opisy oraz przykłady obrazujące terminologię i koncepcję, z którą będziemy spotykać się dalej w tekście. W kolejnych rozdziałach będziemy szczegółowo omawiać poszczególne własności. Oprócz tego, programiści rozpoczynający pracę z systemem Unix znajdą w tym rozdziale podstawowe informacje na temat usług oferowanych w tym systemie oraz ich przegląd.

1.2

Logowanie w systemie

Nazwa użytkownika Rozpoczynając pracę w systemie Unix, wprowadzamy nazwę użytkownika, a następnie jego hasło. Podana nazwa użytkownika jest poszukiwana w systemowym pliku haseł, zazwyczaj jest to plik /etc/passwd. Jeżeli przyjrzymy się wpisowi w pliku haseł dotyczącemu określonego użytkownika, to okaże się, że składa się on z siedmiu pól oddzielonych dwukropkami. Są to: nazwa użytkownika, zaszyfrowane hasło, numeryczny identyfikator użytkownika

1. Pojęcia podstawowe

1.3. Pliki i katalogi

(224), numeryczny identyfikator grupy (20), pole komentarza, katalog domowy (/home/stevens) i nazwa programu powłoki (/bin/ksh). W wielu nowszych systemach operacyjnych zaszyfrowane hasła są umieszczane w oddzielnym pliku. W rozdziale 6 zobaczymy takie pliki oraz poznamy funkcje, za pomocą których realizujemy dostęp do nich.

W dalszym tekście będziemy prezentować przykłady stosowania powłoki, aby wykonać program, który będziemy tworzyć. Podczas tej interakcyjnej pracy będziemy korzystać z cech wspólnych dla powłok Bourne'a i KornShella.

1.3

fłoka Niektóre systemy po pomyślnym załogowaniu użytkownika wyświetlają komunikaty informacyjne, a następnie można już wprowadzać polecenia, które będzie obsługiwał program powłoki. Powłoka (shell) jest programem interpretującym wprowadzane polecenia - czyta on dane wprowadzone przez użytkownika i wykonuje odpowiednie polecenia. Użytkownik wprowadza dane najczęściej za pomocą terminalu (powłoka interakcyjna), ale nieraz zdarza się, że polecenia są umieszczone w pliku (ta forma jest nazywana skryptem powłoki). Do powszechnie stosowanych powłok należą: • powłoka Bourne'a,/bin/sh • powłoka C, / b i n / c s h • KornShell, /bin/ksh System wie, jakiej powłoki użyć do obsługi konkretnego użytkownika na podstawie ostatniego pola w pliku haseł. Powłoka Bourne'a jest stosowana począwszy od wersji 7 i zawierają ją prawie wszystkie istniejące systemy Unix. Powłokę C rozwinięto w Berkeley i włączono do wszystkich wydań systemu BSD. Oprócz tego, firma AT&T rozprowadzała powłokę C razem z wydaniem 3.2 Systemu V/386 oraz z wydaniem 4 Systemu V (SVR4). (Więcej na temat różnych wersji Uniksa powiemy w następnych rozdziałach). KornShell jest uważany za następcę powłoki Bourne'a, zawiera go system SVR4. KornShell może być uruchamiany w większości systemów uniksowych, ale przed pojawieniem się systemu SVR4 był zazwyczaj fragmentem dodawanym do systemu za specjalną opłatą, dlatego nie jest tak szeroko rozpowszechniony jak dwa poprzednie powłoki. Autorem powłoki Bourne'a jest Steve Bourne z Bell Labs. Instrukcje sterujące w tym programie pochodzą z Algola 68. Powłokę C opracował Bili Joy z Berkeley. Program ten powstał na podstawie szóstej edycji powłoki (6th Edition Shell, ale nie jest to powłoka Bourne'a). Jego instrukcje sterujące przypominają język C. Udostępnia dodatkowe możliwości, których nie było w powłoce Bourne'a - sterowanie zadaniami, mechanizm utrzymywania historii pracy oraz możliwość edycji wiersza poleceń. KornShell to kolejny produkt Bell Labs, rozwinięty przez Davida Korna. Jest on kompatybilny z powłoką Bourne'a i oprócz tego zawiera właściwości, dzięki którym zyskała popularność powłoka C - sterowanie zadaniami, możliwość edycji wiersza poleceń itd. W dalszym tekście będziemy stosować uwagi takie jak ta, aby wprowadzać komentarze o charakterze historycznym oraz by porównywać różne implementacje Uniksa. Często poznanie historycznych korzeni i motywacji umożliwia zrozumienie koncepcji konkretnej implementacji.

25

Pliki i katalogi

System plików Uniksowy system plików jest hierarchicznym uporządkowaniem katalogów i plików. Wszystko rozpoczyna się w katalogu podstawowym zwanym korzeniem, którego nazwę tworzy pojedynczy znak ukośnika (/). Katalog to plik zawierający wszystkie pozycje katalogu. Z logicznego punktu widzenia pozycja w katalogu składa się z nazwy pliku oraz ze struktury zawierającej informacje na temat atrybutów pliku. Atrybuty pliku to m.in.: typ pliku, jego rozmiar, nazwa właściciela, prawa dostępu (np. czy użytkownik może korzystać z tego pliku), czas ostatniej modyfikacji itp. Funkcje s t a t i f s t a t przekazują strukturę informacyjną zawierającą wszystkie atrybuty pliku. W rozdziale 4 przyjrzymy się szczegółowo po kolei wszystkim atrybutom pliku. Nazwa pliku Nazwy w katalogu to po prostu nazwy plików. Tylko dwa znaki nie mogą wystąpić w nazwie pliku: ukośnik (/) oraz znak pusty. Ukośnik oddziela nazwy plików, które tworzą nazwę ścieżki (opisaną dalej), a znak pusty oznacza zakończenie nazwy ścieżki. Mimo to, dobrą praktyką jest ograniczanie zakresu znaków, które stosujemy w nazwach plików do pewnego podzbioru znaków typowych, nadających się do wydruku. (Warto ograniczyć się do podzbioru, ponieważ jeśli użyjemy w nazwie pliku pewnych specjalnych znaków powłoki, to w celu odwołania do tego pliku musimy zastosować typowy dla tej powłoki mechanizm cytowania znaków). Gdy powstaje nowy katalog, wówczas automatycznie są tworzone dwie nazwy plików: . (dot) oraz . . (dot-dot). Kropka wskazuje bieżący katalog, a dwie kropki oznaczają katalog macierzysty. W przypadku katalogu podstawowego, tj. korzenia katalogów, dwie kropki są tym samym co kropka. W niektórych uniksowych systemach plików maksymalna długość nazwy pliku jest ograniczana do 14 znaków, w wersjach systemu BSD ograniczenie to zwiększono do 156 znaków. Nazwa ścieżki Nazwa ścieżki ma postać sekwencji złożonej z zera lub więcej nazw plików oddzielonych znakami ukośnika i opcjonalnie rozpoczynającej się od znaku

1. Pojęcia podstawowe

ukośnika. Nazwa ścieżki rozpoczynająca się od znaku ukośnika jest nazywana bezwzględną (pełną) nazwą ścieżki, w przeciwnym razie jest to względna nazwa ścieżki.

ykład Wypisanie wszystkich plików w katalogu nie jest rzeczą trudną. Program 1.1 jest bardzo prymitywną implementacją polecenia l s ( l ) .

27

1.3. Pliki i katalogi

przykład, wszystkie funkcje dotyczące standardowego wejścia-wyjścia są klasyfikowane w części 3S, np. f open(3S). Niektóre systemy Unix, w szczególności oparte na Xeniksie, nie stosują notacji numerycznej do oznaczenia części. Zamiast tego używają skrótu C dla poleceń (na ogół część 1), S dla usług (części 2 i 3) itd.

Jeżeli Czytelnik ma dostęp do podręcznika systemowego, to może obejrzeć np. opis polecenia l s za pomocą: man 1 l s

;lude clude clude

W programie 1.1 po prostu drukujemy nazwy wszystkich plików w katalogu. Jeżeli nazwiemy plik źródłowy np. myls . c, to możemy skompilować go w sposób domyślny, tak by moduł wynikowy powstał w pliku a. out:

"ourhdr.h"

i(int argc, char *argv[]) DIR struct dirent

*dp; *dirp;

if (argc != 2) err_quit("a single argument (the directory name) is required" if ( (dp = opendir(argv[l])) == NULL) err_sys("can't open %s", argv[l]); while ( (dirp = readdir(dp)) != NULL) printf("%s\n", dirp->d_name); closedir(dp); exit (0);

Próg. 1.1

Wypisanie nazw wszystkich plików w katalogu

Notacja l s ( l ) jest typowym sposobem odwoływania się do konkretnej pozycji w podręczniku systemu Unix. W tym przypadku chodzi o pozycję w części 1, dotyczącą polecenia l s . Części są normalnie numerowane od 1 do 8, a wszystkie wpisy w ramach części są uporządkowane alfabetycznie. W niniejszej książce zakładamy, że Czytelnik dysponuje kopią systemowego podręcznika Uniksa. Starsze wersje systemów uniksowych scalały wszystkie osiem części podręcznika w jeden dokument, nazywany Unix Programmer 's Manuał. Zgodnie z obecną tendencją, poszczególne części tworzą odrębne podręczniki; przykładowo, jedne są przeznaczone dla użytkowników, inne dla programistów, jeszcze inne dla administratorów systemu. Niektóre systemy Unix dokonują dalszego podziału podręczników w ramach poszczególnych części i oznaczająje za pomocą dużej litery. W systemie firmy AT&T [13], na

cc myls.c

Oto przykładowy wydruk programu: $ a.out /dev

MAKEDEV

console tty mem kmem null

wiele wierszy, których tu nie pokazujemy

printer $ a.out /var/spool/mqueue can't open /var/spool/mqueue: Permission denied $ a.out /dev/tty can't open /dev/tty: Not a directory

Dalej w tekście będziemy w następujący sposób prezentować polecenia, wprowadzane dane wejściowe oraz wydruk uzyskany jako wynik: znaki, które wprowadzamy są pokazane za pomocą t e j c z c i o n k i , a wszystko to, co wypisuje program, jest pokazane w t e n sposób. Jeżeli będziemy chcieli dodać komentarz do tego wydruku, to zaznaczymy go kursywą. Znak dolara, który znajduje się na początku wprowadzanych danych, jest znakiem zachęty, drukowanym przez powłokę. Zawsze będziemy pokazywać znak zachęty powłoki jako znak dolara. Zauważmy, że wykaz plików w katalogu nie jest uporządkowany alfabetycznie. Uporządkowanie, do którego się zapewne już przyzwyczailiśmy, jest dziełem polecenia l s . W tym 20-wierszowym programie musimy zwrócić uwagę na wiele szczegółów.

1. Pojęcia podstawowe

Po pierwsze, dodajemy własny plik nagłówkowy o nazwie ourhdr. h. Robimy to prawie we wszystkich naszych programach w tej książce. W ten sposób włączamy do programu niektóre standardowe pliki nagłówkowe oraz definiujemy stałe i prototypy funkcji, z których korzystamy w przykładach. Zawartość tego pliku przedstawiamy w dodatku B. Deklaracja funkcji main korzysta z nowego stylu, zgodnego ze standardem ANSI C. (Więcej na temat standardu ANSI C powiemy w następnym rozdziale). Pobierany argument wiersza poleceń, a r g v [ l ] jest nazwą katalogu, którego zawartość należy wypisać. W rozdziale 7 zobaczymy, jak jest wywoływana funkcja main oraz w jaki sposób program korzysta z argumentów wiersza poleceń i zmiennych środowiskowych. Ponieważ faktyczny format pozycji w katalogu zależy od rodzaju systemu Unix, więc do obsługi katalogu korzystamy z funkcji opendir, r e a d d i r oraz c l o s e d i r . Wynikiem funkcji opendir jest wskaźnik do struktury typu DIR. Ten wskaźnik przekazujemy funkcji r e a d d i r . Nie interesuje nas, czym w rzeczywistości jest struktura DIR. Następnie wywołujemy funkcję r e a d d i r w pętli, aby odczytać kolejne pozycje katalogu. Jej wartością jest wskaźnik do struktury typu d i r e n t lub wskaźnik pusty, gdy dotrzemy do końca katalogu. W strukturze d i r e n t sprawdzamy tylko pole z nazwą pozycji (d_name). Wartości tego pola używamy w wywołaniu funkcji s t a t (podrozdz. 4.2), aby pobrać wszystkie atrybuty pliku. Wywołujemy dwie własne funkcje: e r r _ s y s oraz e r r _ q u i t . W przedstawionym wydruku widzimy, że funkcja e r r _ s y s wypisuje komunikat informacyjny z opisem błędu, który wystąpił („Permission denied" lub „Not a directory"). Te dwie funkcje obsługi błędów są przedstawione i opisane w dodatku B. Natomiast w podrozdz. 1.7 powiemy nieco więcej o obsłudze błędów. Po zrealizowaniu wszystkich zadań program wywołuje funkcję e x i t z argumentem 0. Funkcja ta kończy wywołanie programu. Na ogół podajemy argument równy 0, by wskazać poprawne zakończenie; argumenty od 1 do 255 oznaczają błąd. W podrozdziale 8.5 pokazujemy, jak każdy program (np. powłoka lub program, który sami napiszemy) może odczytać stan zakończenia uruchomionego przez siebie programu. • italog roboczy Każdy proces ma swój katalog roboczy (często nazywany bieżącym katalogiem roboczym). Jest to katalog, w odniesieniu do którego są interpretowane względne nazwy ścieżek. Proces może zmienić swój katalog roboczy za pomocą funkcji c h d i r .

I

1.4. Wejście i wyjście

29

Na przykład względna nazwa ścieżki doc/memo/joe odwołuje się do pliku (lub katalogu) o nazwie j oe w katalogu memo katalogu doc, który musi być katalogiem w ramach bieżącego katalogu roboczego. Patrząc tylko na tę nazwę ścieżki, wiemy, że dwie nazwy: doc i memo muszą oznaczać katalog, nie jesteśmy jednak w stanie stwierdzić, czy nazwa j oe dotyczy pliku czy katalogu. Nazwa ścieżki / u s r / l i b / l i n t jest bezwzględną nazwą ścieżki, wskazującą plik (lub katalog) l i n t w katalogu l i b katalogu usr, który jest zlokalizowany w katalogu podstawowym (korzeniu). Katalog domowy Po załogowaniu katalogiem roboczym użytkownika jest jego katalog domowy. Nazwa tego katalogu jest pobierana z wpisu danego użytkownika w pliku haseł (przypominamy podrozdz. 1.2).

1.4

Wejście i wyjście

Deskryptory plików Deskryptory plików to niewielkie liczby całkowite używane przez system do identyfikacji plików, z których korzysta konkretny proces. Kiedy system otwiera istniejący plik lub tworzy nowy, otrzymujemy deskryptor pliku, którego następnie używamy do czytania z pliku lub pisania do niego. Standardowy strumień wejścia, standardowy strumień wyjścia i standardowy strumień komunikatów awaryjnych Obowiązuje reguła, że wszystkie powłoki otwierają dla każdego uruchamianego programu następujące trzy deskryptory: standardowego strumienia wejścia, standardowego strumienia wyjścia i standardowego strumienia komunikatów awaryjnych. Jeżeli nic specjalnego się nie dzieje, jak np. w poniższyn i poleceniu: ls

to wszystkie trzy deskryptory są związane z naszym terminalem. Większość' powłok oferuje metody przekierowania dowolnego deskryptora lub wszyst-_ kich trzech do jakiegoś pliku. Na przykład wiersz j ls > f i l e . l i s t

i

spowoduje wykonanie polecenia ls ze strumieniem standardowego wyjścia przekierowanym do pliku f i l e . l i s t .

1. Pojęcia podstawowe

ebuforowane wejście-wyjście Funkcje open, read, w r i t e , l s e e k i c l o s e obsługują niebuforowane wejście-wyjście. Wszystkie korzystają z deskryptorów plików.

zykład Gdy chcemy czytać ze standardowego wejścia i zapisywać na standardowe wyjście, możemy się posłużyć próg. 1.2. nclude

"ourhdr.h"

efine BUFFSIZE

8192

t in(void)

1.4. Wejście i wyjście

31

Wartością funkcji read jest liczba odczytanych bajtów. Dzięki niej wiemy, ile bajtów trzeba zapisać. Funkcja read po dotarciu do końca pliku przekazuje wartość 0 i program kończy działanie. Jeżeli pojawi się błąd, to r e a d przekazuje wartość - 1 . Większość funkcji systemowych przyjmuje wartość - 1 , gdy wystąpi błąd. Jeżeli skompilujemy nasz program, tworząc standardowy moduł wynikowy w pliku a. out, i wykonamy go jako a.out > data

to standardowym wejściem jest terminal, standardowe wyjście jest przekierowane do pliku o nazwie data, a standardowym strumieniem komunikatów awaryjnych jest terminal. Jeśli podany plik wyjściowy nie istnieje, to powłoka domyślnie utworzy go. • W rozdziale 3 opiszemy szczegółowo funkcje do obsługi niebuforowanego wejścia-wyjścia.

int n; char buf[BUFFSIZE]; while ( (n = read(STDIN_FILENO, buf, BUFFSIZE)) > 0) if (write(STDOUT_FILENO, buf, n) != n) err_sys("write error"); if (n < 0) err_sys("read error"); exit (0);

Próg. 1.2 Skopiowanie standardowego wejścia na standardowe wyjście

Plik nagłówkowy < u n i s t d . h > (włączany przez o u r h d r . h ) oraz dwie stałe STDIN_FILENO i STDOUT_FILENO są częścią standardu POSIX (więcej na jego temat powiemy w następnym rozdziale). W tym pliku nagłówkowym są umieszczone prototypy wielu uniksowych usług, jak np. funkcji read i w r i t e , które wywołujemy. Prototypy funkcji są z kolei częścią standardu ANSI C i o tym również będziemy mówić w kolejnym rozdziale. Dwie stałe STDIN_FILENO i STDOUT_FILENO, zdefiniowane w pliku nagłówkowym < u n i s t d . h > , zawierają wartości deskryptorów plików dla standardowego wejścia i standardowego wyjścia. Zazwyczaj przyjmują one wartości odpowiednio 0 i 1, ale by zagwarantować przenośność, będziemy stosować nazwy stałych. W podrozdziale 3.9 zapoznamy się dokładnie ze stałą BUFFSIZE i zobaczymy, jaki wpływ mają różne jej wartości na wydajność programu. Oczywiście nasz program, niezależnie od wartości stałej BUFFSIZE, kopiuje dowolny plik uniksowy.

Standardowe wejście-wyjście Standardowe funkcje obsługi wejścia-wyjścia dostarczają buforowany interfejs dla funkcji niebuforowanego wejścia-wyjścia. Gdy stosujemy standardowe wejście-wyjście, to nie musimy martwić się o optymalny rozmiar buforów, czyli np. o wartość stałej BUFFSIZE z próg. 1.2. Inną zaletą korzystania ze standardowego wejścia-wyjścia jest obsługa danych wejściowych w formacie wierszy (popularna postać w aplikacjach uniksowych). Na przykład funkcja fgets odczytuje cały wiersz. Z kolei funkcja read czyta zadaną liczbę znaków. Najpopularniejszą standardową funkcją wejścia-wyjścia jest p r i n t f . W programach wywołujących ją zawsze włączamy plik nagłówkowy < s t d i o . h > (w naszych przykładach przez włączenie pliku ), ponieważ zawiera on prototypy funkcji wszystkich standardowych funkcji wejścia-wyjścia.

Przykład

Program 1.3, który dokładnie przeanalizujemy w podrozdz. 5.8, jest podobny do poprzedniego, wywołującego funkcje r e a d i w r i t e . Kopiuje on dane ze standardowego wejścia na standardowe wyjście i może przekopiować do-, wolny plik uniksowy. #include int main(void)

"ourhdr.h"

1. Pojęcia podstawowe int

c;

while ( (c = getc(stdin)) != EOF) if (putc(c, stdout) == EOF) err_sys("output error"); if (ferror(stdin)) err sys("input e r r o r " ) ;

I

1.5. Programy i procesy #include

33

"ourhdr.h"

int main(void) p r i n t f ( " h e l l o w o r l d from p r o c e s s I D % d \ n " , exit (0);

getpid());

exit (0); Próg. 1.4 Wypisanie identyfikatora procesu Próg. 1.3 Skopiowanie standardowego wejścia na standardowe wyjście przy użyciu biblioteki standardowego wejścia-wyjścia

Funkcja g e t c czyta jeden znak, a następnie funkcja p u t c zapisuje go. Po przeczytaniu ostatniego bajtu danych wejściowych funkcja g e t c przekazuje wartość równą stałej EOF. Stałe związane ze strumieniami standardowego wejścia i wyjścia, s t d i n oraz stdout, są zdefiniowane w pliku nagłówkowym < s t d i n . h > . •

Jeżeli skompilujemy ten program do postaci domyślnego pliku wykonywalnego a. out i wykonamy go, to otrzymamy: $ a.out hello.world from process ID 851 $ a.out hello.world from process ID 854

W celu uzyskania swojego identyfikatora procesu program wywołuje funkcję getpid. • Sterowanie procesami

i

Programy i procesy

ogram Program jest plikiem wykonywalnym, zlokalizowanym na dysku. Jądro systemu wczytuje program do pamięci i wykonuje go za pomocą jednej z sześciu funkcji serii exec. Omówimy je w podrozdz. 8.9. ocesy i identyfikatory procesów Wykonujący się egzemplarz programu nazywamy procesem. Z tym terminem będziemy się spotykać niemal na każdej stronie naszej książki. W niektórych systemach operacyjnych wykonywany program bywa nazywany zadaniem. Każdy proces uniksowy otrzymuje w systemie niepowtarzalny numeryczny identyfikator, zwany identyfikatorem procesu (process ID). Identyfikator procesu jest zawsze nieujemną liczbą całkowitą.

rzykład Program 1.4 ma drukować swój identyfikator procesu.

Mamy trzy podstawowe funkcje przeznaczone do sterowania procesami: f ork, exec oraz waitpid. (Funkcja exec ma sześć odmian, ale często mówimy o niej ogólnie, używając właśnie nazwy exec).

Przykład Działanie funkcji sterujących procesami możemy zademonstrować za pomocą prostego programu (próg. 1.5), który czyta dane ze standardowego wejścia i wykonuje otrzymane polecenia. Jest to przykład implementacji bardzo ubo-

#include #include #include

"ourhdr.h"

int main(void) { char pid_t int

buf[MAXLINE]; pid; status;

printf("%%

");

/* drukujemy znak zachęty (sekwencja printf drukuje %) */

w funkcji

1. Pojęcia podstawowe while (fgets(buf, MAXLINE, stdin) != NULL) { buf[strlen(buf) - 1] = 0 ; /* zastępujemy znak nowego wiersza pustym znakiem */ if ( (pid = fork()) < 0) err_sys{"fork error"); else if (pid == 0) { /* proces potomny +/ execlp(buf, buf, (char *) 0); err_ret("couldn't execute: %s", buf) ; exit (127);

/* proces macierzysty */ if ( (pid = waitpid(pid, S s t a t u s , 0)) < 0) err_sys("waitpid e r r o r " ) ; printf("%% "); } exit (0);

Próg. 1.5 Odczytanie poleceń ze standardowego wejścia i wykonanie ich

giej powłoki. W naszym 30-wierszowym programie musimy uwzględnić kilka rzeczy: • Do czytania danych wejściowych po jednym wierszu używamy standardowej funkcji wejścia-wyjścia f g e t s . Gdy wprowadzimy jako pierwszy znak wiersza znak końca pliku (zazwyczaj Control-D), wówczas f g e t s przekazuje wskaźnik pusty, pętla jest przerywana i proces kończy się. W rozdziale 11 omówimy wszystkie specjalne znaki terminalu (koniec pliku, kasowanie poprzedniego znaku, kasowanie całego wiersza itp.) i sposoby ich zmiany. • Ponieważ każdy wiersz przekazany przez funkcję f g e t s jest zakończony sekwencją", znak nowego wiersza, znak pusty, więc stosujemy standardową funkcję języka C - s t r l e n , aby obliczyć długość tablicy znaków, a następnie zastępujemy znak nowego wiersza bajtem pustym. Robimy tak, ponieważ funkcja e x e c l p wymaga, by argument kończył się znakiem pustym, a nie znakiem nowego wiersza. • Wywołujemy funkcję fork, by utworzyć nowy proces. Nowy proces jest kopią procesu wywołującego tę funkcję. Mówimy, że wywołujący proces jest procesem macierzystym, a nowo utworzony potomnym. Funkcja fork po powrocie przekazuje do procesu macierzystego nieujemny identyfikator nowego procesu potomnego, a do procesu potomnego - wartość 0. Ponieważ funkcja fork tworzy nowy proces, więc mówimy, że jest wywoływana raz, natomiast powraca dwukrotnie (w procesie macierzystym i potomnym).

1.5. Programy i procesy

35

• W procesie potomnym wywołujemy funkcjęv e xeclp, l aby wykonać polecenie, które odczytaliśmy ze standardowego wejścia. W ten sposób proces potomny zostanie zastąpiony nowym programem. Połączenie funkcji fork i wywołania exec jest typową formą generowania nowych procesów w niektórych systemach operacyjnych. W Uniksie te dwie czynności są odseparowane w oddzielnych funkcjach. Więcej na temat tych funkcji dowiemy się w rozdz. 8 • Ponieważ proces potomny wywołuje funkcję execlp, aby wykonać nowy program, zatem proces macierzysty musi odczekać, aż potomek zakończy pracę. Jest to realizowane za pomocą wywołania funkcji w a i t p i d , która otrzymuje w argumencie informację, na jaki proces ma czekać (argument p i d będący identyfikatorem procesu potomnego). Funkcja w a i t p i d po zakończeniu wywołania przekazuje również stan zakończenia procesu potomnego (zmienna s t a t u s ) , ale w naszym prostym programie nie korzystamy z tej wartości. Moglibyśmy ją sprawdzić, aby dowiedzieć się dokładnie, co spowodowało zakończenie procesu potomnego. • Najbardziej istotnym ograniczeniem tego programu jest brak możliwości przekazania argumentów polecenia. Nie możemy podać np. nazwy katalogu, którego zawartość chcemy wypisać. Możemy tylko wykonać polecenie ls dla katalogu roboczego. Aby dopuścić podanie argumentów polecenia, niezbędne jest analizowanie wiersza poleceń oraz określenie sposobu wydzielenia argumentów (prawdopodobnie za pomocą spacji i tabulatorów). Następnie każdy argument musi być przekazany jako oddzielny argument funkcji execlp. Mimo wszystko nasz program pozostaje użyteczny dla zademonstrowania uniksowych funkcji sterowania procesami. Uruchomienie tego programu daje poniższy wynik. Zauważmy, że nasz program używa innego znaku zachęty (znaku procentu). $ a.out % datę Fri Jun 7 15:50:36 MST 1991 % who stevens console Jun 5 06:01 stevens ttypO Jun 5 06:02 % pwd /home/stevens/doc/apue/proc % ls Makefile a. out shelll.c % ~D wprowadzenie naszego znaku końca pliku $ zostanie wypisany znak zachęty typowy dla danej powłoki



1. Pojęcia podstawowe

1.7. Obsługa błędów

Właściwości AIMSI C

W przypadku kompilatora niezgodnego z ANSI C lub gdybyśmy nie dysponowali pokazanym prototypem funkcji, musielibyśmy napisać:

Wszystkie przykłady w tej książce są napisane w wersji języka C, zwanej ANSI C.

write(fd, (void *) data, sizeof(data));

Takich ogólnych wskaźników o postaci void * używamy również w funkcji malloc (podrozdz. 7.8). Oto prototyp funkcji malloc:

itotypy funkcji

void *malloc(size_t) ;

Plik nagłówkowy < u n i s t d . h > zawiera prototypy wielu funkcji związanych z usługami w systemie Unix, takich jak read, w r i t e i g e t p i d , z których już mieliśmy okazję korzystać. Prototypy tych funkcji są fragmentem standardu ANSI C. Wyglądają one mniej więcej tak:

Dzięki niemu, możemy napisać: int *ptr;

ptr = malloc(1000 * sizeof(int));

ssize_t read(int, void *, size_t); ssize_t write(int, const void *, size_t); pif_t getpid(void);

Ostatni prototyp określa na przykład, że funkcja g e t p i d nie pobiera żadnego argumentu (void), a jej wartość jest typu p i d _ t . Dzięki wprowadzeniu takich prototypów funkcji kompilator może dokonywać dodatkowych sprawdzeń, aby zagwarantować, że funkcje są wywoływane z właściwymi argumentami. W programie 1.4, jeżeli wywołalibyśmy funkcję getpid, podając jakiś argument, to zgodnie z opisem g e t p i d ( l ) w dokumentacji systemowej kompilator ANSI C powinien przekazać komunikat awaryjny o poniższej postaci: line

37

bez jawnego przerzutowańia otrzymanego wskaźnika do typu i n t *. Elementarne systemowe typy danych Zgodnie z pokazanym prototypem funkcji g e t p i d wartość funkcji jest typu p i d _ t . Jest to nowość wprowadzona przez normę POSIX. We wcześniejszych wersjach Uniksa funkcja ta przekazywała wartość całkowitą. Podobnie, obie funkcje r e a d i w r i t e przekazują wartość typu s s i z e _ t i wymagają, by trzeci argument był typu s i z e _ t . Wszystkie typy danych, których nazwy kończą się napisem _ t , są nazywane elementarnymi systemowymi typami danych. Ich definicja jest na ogół zawarta w pliku nagłówkowym < s y s / t y p e s . h> (który musi być włączany przez plik nagłówkowy < u n i s t d . h > ) . Zazwyczaj w definicji jest używana deklaracja typedef, istniejąca w języku C od ponad 15 lat (a więc nie wymagająca zgodności ANSI C). Stosowanie tych typów danych ma uchronić programy przed koniecznością wprowadzania konkretnych typów danych (np. i n t , s h o r t albo long) i pozwala, by każda implementacja mogła wybrać typ danych odpowiedni w danym systemie. Zawsze, gdy chcemy zapamiętać identyfikator procesu, alokujemy zmienną typu p i d _ t . (Zwróćmy uwagę, że robimy tak w próg. 1.5 ze zmienną o nazwie pid). Definicja tego typu danych j może zależeć od implementacji, ale przyjmuje się, że różnice ograniczają się do jednego pliku nagłówkowego. Przeniesienie aplikacji do innego systemu wymaga więc jej ponownej kompilacji w tym systemie.

too many arguments to function "getpid"

Ponieważ kompilator zna typy danych argumentów, więc jeśli jest to możliwe, przerzutowuje argumenty do wymaganej postaci. skaźniki ogólne W pokazanych wcześniej prototypach funkcji widzimy, że drugi argument funkcji r e a d i w r i t e ma przypisany typ void *. Wszystkie wcześniejsze wersje systemów Unix stosowały dla tego wskaźnika typ char *. Zmiana jest spowodowana tym, że norma ANSI C używa wskaźnika ogólnego, a nie typu char *. Połączenie prototypów funkcji oraz wskaźników ogólnych umożliwia usuwanie wszelkich jawnych przerzutowań typów, które były potrzebne w kompilatorach niezgodnych z ANSI C. Jeżeli na przykład założymy, że korzystamy z przedstawionego wcześniej prototypu funkcji w r i t e , możemy napisać: float data[100]; write(fd, data, sizeof(data));

1.7

Obsługa błędów



Jeżeli w jakiejś funkcji uniksowej wystąpi błąd, to zazwyczaj jest zwracana ujemna wartość, a zmienna typu całkowitego e r r n o zawiera dodatkowe in-, formacje na temat błędu. Na przykład funkcja open przekazuje albo nieujemny deskryptor pliku, jeśli wszystko jest w porządku, albo - 1 , jeśli wystą-

1. Pojęcia podstawowe

pił błąd. W przypadku błędu wykonania funkcji open możliwe jest około 15 różnych wartości zmiennej e r r n o (np. plik nie istnieje, problem związany z prawami dostępu itp.). Niektóre funkcje używają do sygnalizowania błędu innej konwencji niż przekazywanie wartości ujemnej. Większość funkcji, których wynikiem jest wskaźnik, przekazuje w tej sytuacji wskaźnik pusty. Plik nagłówkowy definiuje zmienną e r r n o oraz stałe odpowiadające wartościom, które może przyjąć ta zmienna. Nazwa każdej z tych zmiennych zaczyna się od litery E. Na ogół pierwsza strona części 2 podręcznika systemu Unix, nosząca nazwę intro(2), zawiera wykaz wszystkich stałych dla komunikatów awaryjnych. Na przykład, jeśli zmienna errno ma wartość EACCESS, to stwierdzono brak uprawnień (np. nie mamy prawa do otwarcia żądanego pliku). Norma POSIX definiuje zmienną e r r n o jako extern

int

39

1.7. Obsługa błędów

Przykład Zastosowanie przedstawionych dwóch funkcji do obsługi błędów widzimy w próg. 1.6.

#include linclude

"ourhdr.h"

int main(int argc, char *argv[]) { fprintf(stderr, "EACCES: %s\n", strerror(EACCES)); errno = ENOENT; perror (argv[0]);

errno;

Definicja zmiennej e r r n o w normie POSDC.l jest dokładniejsza od definicji w standardowym języku C, gdzie zezwala się, by nazwa e r r n o odnosiła się do makra, na podstawie którego powstaje modyfikowalna wartość (lvalue) wyrażenia typu całkowitego (np. funkcja, która przekazuje wskaźnik do numeru błędu).

Musimy pamiętać, że w odniesieniu do zmiennej e r r n o obowiązują dwie ważne reguły. Po pierwsze nie jest ona zerowana w żadnej procedurze, jeśli nie wystąpił błąd. Dlatego możemy analizować jej wartość tylko wtedy, kiedy przekazana wartość z funkcji wskazuje, że błąd wystąpił. Po drugie żadna funkcja nie umieszcza wartości 0 w zmiennej e r r n o oraz żadna ze stałych zdefiniowanych w pliku nagłówkowym < e r r n o . h> nie jest równa 0. W standardowym języku C są zdefiniowane dwie funkcje wspomagające operację wydruku komunikatu awaryjnego. #include

char *strerror (int errnum) ; Przekazuje- wskaźnik do komunikatu awaryjnego

Ta funkcja na podstawie wartości errnum (która jest zazwyczaj wartością zmiennej e r r n o ) przekazuje wskaźnik do napisu odpowiadającego komunikatowi awaryjnemu. Funkcja p e r r o r wypisuje tekst komunikatu o błędzie na standardowym strumieniu komunikatów awaryjnych (na podstawie bieżącej wartości zmiennej e r r n o ) i powraca. #include void perror(const char *msg);

Funkcja najpierw wypisuje napis wskazany przez argument msg, następnie po dwukropku i odstępie tekst komunikatu awaryjnego związanego z wartością zmiennej e r r n o oraz znak nowego wiersza.

exit (0) ; }

Próg. 1.6 Działanie funkcji s t r e r r o r i p e r r o r

Jeżeli skompilujemy go i utworzymy standardowy plik wykonywalny a. out, to po uruchomieniu go otrzymamy $ a.out EACCESS: Permission denied a.out: No such file or directory

Zauważmy, że do funkcji p e r r o r przekazujemy nazwę naszego programu (argv [ 0 ], w tym przypadku a. out). Jest to standardowa konwencja w systemie Unix. Dzięki temu, jeżeli program jest wykonywany jako fragment strumienia komunikacyjnego, np. progi < inputfile I prog2 | prog3 > outputfile

I możemy dowiedzieć się, który z trzech programów wygenerował konkretny komunikat awaryjny. O Wszystkie prezentowane w tekście programy stosują zamiast bezpośred-i niego wywołania funkcji s t r e r r o r lub p e r r o r funkcje obsługi błędów_ pokazane w dodatku B. Dzięki korzystaniu z udogodnienia ANSI C - listy ar-j gumentów o zmiennym rozmiarze - możemy obsłużyć błąd za pomocą jednej; instrukcj i j ęzyka C.
/bin /dev/printer /vmunix: regular /etc: directory /dev/ttya: character special /dev/sd0a: błock special /var/spool/cron/FIFO: fifo /bin: symbolic link /dev/printer: socket

4. Pliki i katalogi

(Celowo wprowadziliśmy znak \ na końcu pierwszego wiersza polecenia, aby wskazać powłoce, że będziemy kontynuować wprowadzanie polecenia w następnym wierszu. Powłoka w takim przypadku wypisuje w nowym wierszu znak zachęty >). Specjalnie zastosowaliśmy funkcję l s t a t zamiast s t a t , ponieważ chcemy wykrywać dowiązania symboliczne. • We wcześniejszych wersjach Uniksa nie było makr s_ISxxx. Trzeba było samodzielnie wykonać operację koniunkcji logicznej wartości pola st_mode z maską S_IFMT, a następnie porównać wynik z odpowiednimi stałymi o nazwach s_lXxxx. Systemy SVR4 oraz 4.3+BSD definiują maski oraz odpowiednie stałe w pliku nagłówkowym < s y s / s t a t . h>. W tym pliku możemy znaleźć makro s_ISDIR o postaci: # d e f i n e S_ISDIR(modę)

(((modę)

& S^IFMT) == S__IFDIR)

Powiedzieliśmy, że w systemach dominują zwykle pliki, ale bardzo ciekawa jest statystyka pokazująca, jaki procent plików stanowią poszczególne typy plików w konkretnym systemie. W tabeli 4.2 pokazujemy ilość oraz wskaźnik procentowy różnych typów plików o średnim rozmiarze. Dane uzyskaliśmy jako wynik programu, który omówimy w podrozdz. 4.21. Tabela 4.2 Liczniki oraz udziały procentowe różnych typów plików w systemie Typ pliku zwykły plik

Licznik

Udział procentowy

30369

91,7

1901

5,7

dowiązanie symboliczne

416

1,3

specjalny plik znakowy

373

1,1

specjalny plik blokowy

61

0,2

gniazdo

5

0,0

kolejka FIFO

1

0,0

katalog

Bity ustanowienia identyfikatora użytkownika oraz ustanowienia identyfikatora grupy Z każdym procesem wiąże się sześć lub więcej identyfikatorów. Pokazujemy je w tab. 4.3. • Rzeczywisty identyfikator użytkownika oraz rzeczywisty identyfikator grupy wskazują, kim faktycznie jest osoba uruchamiająca program. Oba pola są pobierane podczas logowania w systemie z odpowiedniego wpisu w pliku haseł. Na ogół wartości te nie zmieniają się w czasie sesji, ale proces nadzorcy systemu może je zmodyfikować, powiemy o tym w podrozdz. 8.10.

4 . 4 . B i t y u s t a n o w i e n i a i d e n t y f i k a t o r a u ż y t k o w n i k a o r a z u s t a n o w i e n i a identyfikatora...

111

Tabela 4.3 Identyfikatory użytkownika i grupy skojarzone z każdym procesem rzeczywisty identyfikator użytkownika rzeczywisty identyfikator grupy

poświadczenie tożsamości

obowiązujący identyfikator użytkownika obowiązujący identyfikator grupy

używane do kontroli praw dostępu

dodatkowe identyfikatory grup zachowany identyfikator użytkownika zachowany identyfikator grupy

zachowywane przez wywołania funkcji exec

• Obowiązujący identyfikator użytkownika, obowiązujący identyfikator grupy oraz dodatkowe identyfikatory grupy decydują o naszych uprawnieniach przy dostępie do pliku - omówimy je w następnym podrozdziale. (Definicję dodatkowych identyfikatorów grup wprowadziliśmy w podrozdz. 1.8). • Zachowany identyfikator użytkownika (saved set-user-ID) oraz zachowany identyfikator grupy (saved set-group-ID) zawierają kopie obowiązującego identyfikatora użytkownika i obowiązującego identyfikatora grupy w wykonywanym programie. Rolę tych dwóch wartości opiszemy podczas omawiania funkcji s e t u i d w podrozdz. 8.10. Zachowane identyfikatory są opcjami w normie POSIX.l. Program użytkowy może sprawdzać w czasie kompilacji, czy jest zdefiniowana stała _POSIX_SAVED_IDS lub wywołać w czasie wykonania funkcję sysconf z argumentem _SC_SAVED_IDS, aby dowiedzieć się, czy konkretna implementacja stosuje tę właściwość. Jest ona dostępna w systemie SVR4. W standardzie FIPS 151-1 ta opcja POSK.l jest wymagana.

Typowo obowiązujący identyfikator użytkownika jest równy rzeczywistemu identyfikatorowi użytkownika, a obowiązujący identyfikator grupy równy rzeczywistemu identyfikatorowi grupy. Każdy plik ma swojego właściciela oraz grupę. Właściciel jest podany w polu s t _ u i d struktury s t a t , a grupa w polu st_gid. Gdy uruchamiamy plik programu, wówczas obowiązującym identyfikatorem użytkownika procesu jest zazwyczaj rzeczywisty identyfikator użytkownika, a obowiązującym identyfikatorem grupy rzeczywisty identyfikator grupy. Jest jednak możliwe ustawienie specjalnego sygnalizatora w słowie trybu dostępu do pliku (st_mode), który ma następujące znaczenie: jeżeli plik jest wykonywany, to obowiązujący identyfikator użytkownika procesu musi być taki sam jak identyfikator właściciela pliku (st_uid). Podobnie, można ustawić inny bit w słowie trybu dostępu do pliku, dzięki któremu obowiązujący identyfikator grupy procesu uzyska w czasie wykonania programu wartość grupy właściciela (st_gid). Te dwa bity w słowie trybu dostępu do pliku nazywane są bitami ustanowienia identyfikatora użytkownika oraz ustanowienia identyfikatora grupy.

4. Pliki i katalogi

Jeżeli właścicielem pliku jest np. nadzorca systemu, a jednocześnie jest ustawiony bit ustanowienia identyfikatora użytkownika, to plik programu uruchamiany jako proces dysponuje uprawnieniami nadzorcy. Jest tak niezależnie od rzeczywistego identyfikatora użytkownika tego procesu. Na przykład w Uniksie program passwd(l), który umożliwia każdemu użytkownikowi zmianę swojego hasła, ma ustawiony bit ustanowienia identyfikatora użytkownika. Jest to konieczne, aby nowe hasło zostało zapisane do pliku haseł, zazwyczaj jest to plik /etc/passwd lub /etc/shadow, dla którego prawo zapisu ma tylko nadzorca. Ponieważ proces, który jest uruchomiony z ustanowionym identyfikatorem innego użytkownika, na ogół uzyskuje jakieś dodatkowe przywileje, więc należy takie podejście bardzo wnikliwie przeanalizować. W rozdziale 8 omówimy szczegółowo te rodzaje programów. Odnieśmy to wszystko do wprowadzonej wcześniej funkcji s t a t . Bity ustanowienia identyfikatorów użytkownika oraz grupy są zawarte w polu st_mode dla danego pliku. Ich wartość można sprawdzić za pomocą stałych S ISUIDorazS ISGID.

Prawa dostępu do pliku W wartości pola st_mode są również zakodowane bity praw dostępu do pliku. Gdy używamy określenia plik, mamy na myśli dowolny typ pliku spośród opisanych wcześniej. Wszystkie typy plików (katalogi, specjalne pliki znakowe itp.) mają swoje prawa dostępu, chociaż wiele osób sądzi, że dotyczą one tylko zwykłych plików. Dla każdego pliku istnieje dziewięć bitów uprawnień, które można podzielić na trzy kategorie. Pokazujemy je w tab. 4.4. Określenie „użytkownik" w pierwszych trzech wierszach tab. 4.4 oznacza właściciela pliku. W poleceniu chmod(l), które najczęściej jest stosowane do modyfikacji tych dziewięciu bitów uprawnień, można podać w arTabela 4.4 Dziewięć bitów praw dostępu do plików z pliku nagłówkowego < s y s / s t a t .h> Maska st modę

Znaczenie

S_IRUSR

odczyt przez użytkownika

S_IWUSR

zapis przez użytkownika

S IXUSR

wykonanie przez użytkownika

S IRGRP

odczyt przez grupę

S IWGRP

zapis przez grupę

S IXGRP

wykonanie przez grupę

S IROTH

odczyt przez innych

S_IWOTH

zapis przez innych

S IXOTH

wykonanie przez innych

4.5. Prawa dostępu do pliku

113

gumencie literę u, by wskazać użytkownika (właściciela), literę g dla grupy oraz o dla pozostałych. W niektórych książkach o tych trzech kategoriach mówi się: właściciel, grupa, świat; jest to nieco mylące, gdyż program chmod stosuje literę o do wskazania innych (ołher), nie właściciela (owner). My będziemy używać terminów: użytkownik, grupa i inni, ponieważ chcemy być w zgodzie z poleceniem chmod. Trzy kategorie z tab. 4.4: czytanie, zapis oraz wykonanie - są używane na wiele sposobów przez różne funkcje. Podsumujemy teraz ich znaczenie, potem wrócimy do tego zagadnienia podczas omawiania funkcji. • Pierwsza zasada określa, że zawsze, gdy chcemy otworzyć dowolny typ pliku, podając jego nazwę, musimy mieć prawo wykonania dla każdego katalogu występującego w nazwie, łącznie z katalogiem bieżącym, jeśli ma on związek z nazwą pliku. Dlatego dla katalogu bit prawa wykonania jest często nazywany bitem przeszukania. Na przykład, aby otworzyć plik / u s r / d i c t / w o r d s , musimy mieć prawo wykonania dla katalogów /, /usr, / u s r / d i c t . Jest też potrzebne odpowiednie prawo dostępu dla pliku, a o sposobie jego ustawienia decyduje tryb korzystania z pliku (tylko do odczytu, do odczytu i zapisu itp.). Jeżeli bieżącym katalogiem jest / u s r / d i c t , to aby móc otworzyć plik words, musimy mieć prawo wykonania dla bieżącego katalogu. Jest to typowy przykład, gdy bieżący katalog jest sprawdzany, chociaż nie był jawnie wskazany w nazwie pliku. Wszystko przebiega tak samo, jakbyśmy otwierali plik . /words. Zwróćmy uwagę, że dla katalogu uprawnienia do odczytu oraz wykonania mają zupełnie inne znaczenie. Prawo odczytu pozwala czytać zawartość katalogu, tj. otrzymać wykaz wszystkich plików w tym katalogu. Prawo wykonania pozwala przechodzić przez katalog będący składnikiem nazwy ścieżki pliku, do którego chcemy dotrzeć (tzn. przeszukujemy katalog, aby znaleźć konkretną nazwę pliku). Z innym przykładem pośredniego odwoływania się do katalogu możemy się spotkać, jeśli zmienna środowiskowa PATH (opisana w podrozdz. 8.9) wskazuje katalog, który nie ma prawa wykonania. Powłoka nigdy nie znajdzie wykonywalnych plików w takim katalogu. • Prawo odczytu dla pliku decyduje, czy możemy otworzyć istniejący plik do odczytu (sygnalizatory O_RDONLY i O_RDWR w funkcji open). • Prawo zapisu dla pliku decyduje, czy możemy otworzyć istniejący plik do zapisu (sygnalizatory O_WRONLY i O_RDWR w funkcji open). • Musimy mieć prawo zapisu dla pliku, jeśli wskazujemy sygnalizator O_TRUNC w funkcji open. • Nie możemy utworzyć nowego pliku w katalogu, jeśli nie mamy prawa zapisu i wykonania w danym katalogu.

4. Pliki i katalogi

• Aby usunąć istniejący plik, musimy mieć prawo zapisu i wykonania w katalogu, w którym jest umieszczony ten plik. Nie jest potrzebne prawo odczytu ani zapisu dla tego pliku. • Jeżeli chcemy wykonać plik za pomocą jednej z funkcji exec (podrozdz. 8.9), to musimy mieć prawo wykonania dla tego pliku. Musi to być zwykły plik. Przebieg sprawdzenia praw dostępu do pliku, które system dokonuje za każdym razem, gdy proces otwiera, tworzy lub usuwa plik, zależy od tego, kto jest właścicielem (pola s t _ u i d oraz st_gid), od obowiązujących identyfikatorów procesu (obowiązującego identyfikatora użytkownika i obowiązującego identyfikatora grupy) oraz od dodatkowych identyfikatorów grup dla danego procesu (jeżeli są używane). Dwa identyfikatory właścicieli to atrybuty pliku, a dwa obowiązujące identyfikatory i identyfikatory dodatkowych grup to atrybuty procesu. Oto sprawdzenia realizowane przez jądro systemu: 1. Jeżeli obowiązujący identyfikator użytkownika dla danego procesu jest równy 0 (nadzorca), to dostęp jest dozwolony. Właśnie dzięki temu nadzorca panuje w systemie plików. 2. Jeżeli obowiązujący identyfikator użytkownika dla danego procesu jest równy identyfikatorowi właściciela pliku (czyli proces jest właścicielem pliku), to: (a) gdy są ustawione odpowiednie bity prawa dostępu dla użytkownika, dostęp jest dozwolony, (b)w przeciwnym razie dostęp jest zabroniony. Przez określenie odpowiednie bity prawa dostępu rozumiemy, że gdy użytkownik otwiera plik do odczytu, musi być włączony bit prawa odczytu dla użytkownika. Jeżeli proces otwiera plik do zapisu, to musi być włączony bit prawa zapisu dla użytkownika. Jeżeli proces wykonuje plik, to musi być włączony bit prawa wykonania. 3. Jeżeli obowiązujący identyfikator grupy dla danego procesu lub jeden z dodatkowych identyfikatorów grupy tego procesu jest równy identyfikatorowi grupy pliku, to: (a) gdy są ustawione odpowiednie bity prawa dostępu dla grupy, wówczas dostęp jest dozwolony, (b)w przeciwnym razie dostęp jest zabroniony. 4. Jeżeli są ustawione odpowiednie bity praw dostępu dla innych, to dostęp jest dozwolony, w przeciwnym razie jest zabroniony. Przedstawione cztery kroki są realizowane w wymienionej kolejności. Jeżeli proces jest właścicielem pliku (krok 2), to dostęp jest przydzielany bądź nie, a zależy to wyłącznie od praw dostępu dla użytkownika - prawa dla grupy nie są nigdy sprawdzane. Podobnie, jeżeli proces nie jest właścicielem pliku, ale należy do odpowiedniej grupy, to o przydzieleniu dostępu decydują wyłącznie prawa dostępu dla grupy - inne uprawnienia nie są analizowane.

4.6. Prawa własności nowych plików i katalogów

4.6

115

Prawa własności nowych plików i katalogów Kiedy w rozdziale 3 opisywaliśmy proces tworzenia nowego pliku za pomocą funkcji c r e a t lub open, nie mówiliśmy nic o wartościach przypisywanych identyfikatorowi użytkownika i identyfikatorowi grupy. W podrozdziale 4.20, gdy będziemy opisywać funkcję mkdir, zobaczymy, jak są tworzone nowe katalogi. Zasady określania praw własności dla nowego katalogu są takie same jak zasady ustalania właściciela nowego pliku, które opisujemy w tym podrozdziale. Identyfikator użytkownika dla nowego pliku jest ustawiany na podstawie obowiązującego identyfikatora użytkownika danego procesu. Zgodnie z normą POSDC.l, implementacja może wybrać jedną z poniższych opcji, aby ustalić, jaki jest identyfikator grupy dla nowego pliku. 1. Identyfikator grupy dla nowego pliku może być równy obowiązującemu identyfikatorowi grupy danego procesu. 2. Identyfikator grupy dla nowego pliku może być równy identyfikatorowi grupy katalogu, w którym jest tworzony plik. W systemie SVR4 identyfikator grupy nowego pliku zależy od tego, czy jest włączony plik ustanowienia identyfikatora grupy dla katalogu, w którym powstaje plik. Jeżeli ten bit jest ustawiony, to identyfikator grupy nowego pliku jest taki sam jak identyfikator grupy tego katalogu; w przeciwnym razie, identyfikator grupy nowego pliku jest równy obowiązującemu identyfikatorowi grupy tego procesu. W systemie 4.3+BSD nowy plik zawsze uzyska identyfikator grupy równy identyfikatorowi grupy swojego katalogu. W innych systemach jest możliwy wybór na poziomie systemu plików między tymi dwoma opcjami normy POS1X.1. Służy do tego specjalny sygnalizator w poleceniu mount(1). W standardzie FIPS 151-1 jest wymagane, by identyfikator grupy nowego pliku był identyfikatorem grupy katalogu, w którym plik jest tworzony.

Zastosowanie drugiej opcji standardu POSIX.l (dziedziczenia identyfikatora grupy danego katalogu) gwarantuje, że wszystkie tworzone w tym katalogu pliki ' i katalogi będą miały identyfikator grupy swojego katalogu. Grupowe prawa własności plików i katalogów będą się powielać od tego miejsca w dół hierarchii i katalogów. Z tej własności korzysta m.in. katalog /var/spool.

Jak już wspomnieliśmy, grupowe prawa własności są domyślnie przyjęte w systemie 4.3+BSD. W systemie SVR4 są jednak opcją, a dla jej uaktywnienia musimy ustawić ) bit ustanowienia identyfikatora grupy. Oprócz tego, w systemie SVR4, aby wszystko ! działało poprawnie, funkcja mkdir musi automatycznie propagować bit ustanowienia identyfikatora grupy katalogu (i tak robi, zgodnie z opisem z podrozdz. 4.20). *

4. Pliki i katalogi

4.7. Funkcja access

Funkcja access Jak opisaliśmy wcześniej, gdy wywołujemy funkcję open dla jakiegoś pliku, system realizuje zestaw testów praw dostępu, które uwzględniają obowiązujący identyfikator użytkownika oraz obowiązujący identyfikator grupy. Są przypadki, gdy system chce sprawdzić dostępność na podstawie rzeczywistego identyfikatora użytkownika oraz rzeczywistego identyfikatora grupy. Taka technika ma sens, jeśli proces pracuje pod obcym identyfikatorem, gdyż stosujemy bity ustanowienia identyfikatora użytkownika lub grupy. Nawet gdy proces używa bitu ustanowienia identyfikatora grupy dla nadzorcy systemu, może okazać się konieczne sprawdzenie, czy rzeczywisty użytkownik ma prawo dostępu do danego pliku. Funkcja a c c e s s opiera swoje sprawdzenia na rzeczywistych identyfikatorach użytkownika i grupy. (Przechodzi przez cztery pierwsze kroki opisane w podrozdz. 4.5 i zamienia identyfikator obowiązujący z rzeczywistym). łtinclude < u n i s t d . h > i n t a c c e s s ( c o n s t char *pathname, i n t modę) ; Przekazuje: 0, jeśli wszystko w porządku; - Jeśli wystąpił błąd

Argument modę powstaje jako alternatywa bitowa dowolnych stałych podanych w tab. 4.5. Tabela 4.5 Stałe modę dla funkcji access w pliku nagłówkowym modę

Opis

R OK

sprawdzenie uprawnień do odczytu

W_OK

sprawdzenie uprawnień do zapisu

X_OK

sprawdzenie uprawnień do wykonania

F_OK

sprawdzenie istnienia pliku

117

access error for /etc/uucp/Systems: Permission denied open error for /etc/uucp/Systems: Permission denied uzyskaj uprawnienia nadzorcy $ su podaj hasło nadzorcy Password: zmień właściciela pliku na uucp $ chown uucp a.out ustaw bit ustanowienia $ chmod u+s a.out identyfikatora użytkownika sprawdź właściciela i bit SUID $ ls -1 a.out 105216 Jan 18 08:48 a . o u t -rwsrwxr-x 1 uucp # exit $ a.out /etc/uucp/Systems access error for /etc/uucp/Systems: Permission denied open for reading OK

#include tinclude #include

"ourhdr.h"

int mainfint argc, char *argv[]) { if (argc != 2) err_quit("usage: a.out "); if (access(argv[l], R_OK) < 0) err_ret("access error for %s", argv[l]); else printf("read access 0K\n"); if (open(argv[l], O_RDONLY) < 0) err_ret("open error for %s", argv[l]); else printf("open for reading 0K\n"); exit(0);

Próg. 4.2 Przykład funkcj i a c c e s s

zykład Zastosowanie funkcji a c c e s s widzimy w próg. 4.2. Oto przykładowa sesja użycia tego programu. $ ls -1 a.out -rwxrwxr-x 1 stevens 105216 Jan 18 08:48 a.out $ a.out a.out read access OK open for reading OK $ ls -1 /etc/uucp/Systems 1441 Jul 18 15:05 /etc/uucp/Systems -rw-r 1 uucp

W tym przykładzie program z ustawionym bitem ustanowienia identyfikatora użytkownika może stwierdzić, że rzeczywisty użytkownik nie ma prawa odczytu pliku, chociaż powiodła się funkcja open. • W poprzednim przykładzie oraz w rozdz. 8 od czasu do czasu przełączamy tryb pracy, żeby posłużyć się uprawnieniami nadzorcy systemu w celu zademonstrowania, jak działają niektóre rzeczy. Jeżeli Czytelnik pracuje w systemie wielodostępnym i nie ma uprawnień nadzorcy, nie będzie mógł sprawdzić w całości tych przykładów.

18

.8

4. Pliki i katalogi

Funkcja umask Po opisaniu dziewięciu bitów praw dostępu związanych z każdym plikiem możemy przystąpić do omówienia maski trybu dostępu do pliku, która istnieje w każdym procesie. Funkcja umask ustawia maskę trybu dostępu do pliku w danym procesie i przekazuje jej poprzednią wartość. (Jest to jedna z niewielu funkcji nie mających określonego komunikatu awaryjnego).

4.9. Funkcje chmod i fchmod #include #include #include #include

umask(0); if ( c r e a t ( " f o o " , S_IRUSR I S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH) < 0) e r r _ s y s ( " c r e a t e r r o r for f o o " ) ; umask(S_IRGRP I S_IWGRP I S_IROTH I S_IWOTH); if ( c r e a t ( " b a r " , S_IROSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH) < 0) e r r s y s ( " c r e a t e r r o r for b a r " ) ; exit(0);

modę t umask(modę t cmask); Przekazuje: poprzednią maskę trybu dostępu

Argument cmask powstaje jako alternatywa bitowa dowolnych dziewięciu stałych z tab. 4.4: S_IRUSR, S_IWUSR itd. Maska trybu dostępu do plików jest używana, gdy proces tworzy nowy plik lub katalog. (Przypominamy podrozdz. 3.3 i 3.4, w których opisaliśmy funkcje open i c r e a t . W obu funkcjach podaje się argument modę, aby określić bity praw dostępu do nowego pliku). W podrozdziale 4.20 pokażemy, jak przebiega tworzenie nowego katalogu. Wszystkie bity, które są włączone w masce trybu dostępu do pliku, ^wyłączane w wartości argumentu modę.

Próg. 4.3 Przykład funkcji umask

pliki i chcemy zagwarantować włączenie pewnych bitów praw dostępu do pliku, musimy zmodyfikować wartość maski trybu dostępu w trakcie pracy procesu. Na przykład, aby zapewnić możliwość odczytu pliku przez wszystkich użytkowników, powinniśmy nadać tej masce wartość 0. W przeciwnym razie wartość umask obowiązująca w uruchomionym procesie może spowodować wyłączenie potrzebnych bitów praw dostępu. W poprzednim przykładzie zastosowaliśmy dwukrotnie polecenie umask: do wydruku maski trybu dostępu, zanim uruchomiliśmy program, a potem po jego zakończeniu. Okazało się, że zmiana maski trybu dostępu do pliku w procesie nie ma wpływu na wartość tej maski w procesie macierzystym (najczęściej jest nim powłoka). Wszystkie trzy powłoki mają wbudowane polecenie umask, z którego możemy korzystać do ustawienia lub wydruku bieżącego ustawienia maski trybu dostępu do pliku.

rzykład Program 4.3 tworzy dwa pliki: jeden, gdy umask ma wartość 0; drugi, gdy umask ma wyłączone wszystkie bity dostępu dla grupy i innych. Uruchomienie tego programu drukujemy bieżącą maską trybu dostępu

0 Nov 16 16:23 b a r 0 Nov 16 16:23 foo sprawdzamy, czy zmieniła się maska trybu dostępu

pokazuje, jak zostały ustawione bity praw dostępu.

modę

121

4.9. Funkcje chmod i f chmod

#include #include iinclude

"ourhdr.h"

int main(void) {

struct stat

statbuf;

/* włączenie bitu ustanowienia identyfikatora grupy oraz wyłączenie bitu wykonania przez grupę */ if (stat("foo", sstatbuf) < 0) err_sys("stat error for foo"); if (chmod("foo", (statbuf.st_mode s ~S_IXGRP) I S_ISGID) < 0) err_sys("chmod error for foo"); /* bezwzględne ustawienie trybu dostępu "rw-r—r—" */ if (chmod("bar", S_IRUSR | S_IWUSR I S_IRGRP | S_IROTH) < 0) err_sys("chmod error for bar"); exit (0);

Próg. 4.4 Przykład funkcji chmod

4. Pliki i katalogi

4.11. Funkcje chown, f chown oraz 1 chown

wymiany. (Przez pojęcie tekstu programu rozumiemy jego instrukcje maszynowe). Program taki był znacznie szybciej ładowany do pamięci przy kolejnych jego wywołaniach, gdyż obszar wymiany jest obsługiwany jak ciągły plik, podczas gdy w normalnym systemie plików poszczególne bloki pliku są rozmieszczone losowo. Zazwyczaj bit lepki ustawiano dla popularnie wykonywanych aplikacji, jak edytory tekstu czy poszczególne komponenty kompilatora C. Naturalnie, wprowadzono parametr ograniczający liczbę plików z ustawionym bitem lepkim, które można było umieszczać jednocześnie w obszarze wymiany, a jego przekroczenie powodowało przepełnienie obszaru wymiany. Mimo to technika ta była bardzo użyteczna. Nazwa bit lepki przyjęła się, gdyż tekstowa wersja pliku tkwiła w obszarze wymiany aż do kolejnego załadowania systemu. Późniejsze wersje Uniksa mówiły o bicie zachowanego tekstu (saved text), stąd stała S_ISVTX. W obecnych, nowych wersjach Uniksa, stosujących pamięć wirtualną oraz szybsze systemy plików, przeminęła idea tej techniki.

aby zaznaczyć aktywne obowiązkowe ryglowanie rekordu dla tego pliku. Dla pliku bar ustalamy uprawnienia na podstawie podanej wartości bezwzględnej, a więc nowa wartość nie zależy od bieżącego stanu bitów uprawnień. Zauważmy też, że data i czas podawane przez polecenie l s nie uległy zmianie po uruchomieniu próg. 4.4. W podrozdziale 4.18 zobaczymy, że funkcja chmod aktualizuje tylko czas ostatniej zmiany i-węzła. Domyślnie funkcja ls -1 wypisuje czas odpowiadający ostatniej modyfikacji pliku. D Funkcje serii chmod automatycznie zerują dwa z bitów uprawnień, o ile zajdą wymagane warunki: • Jeżeli próbujemy ustawić bit lepki (sticky bit) ( S _ I S V T X ) dla zwykłego pliku i nie mamy uprawnień nadzorcy, to ten bit jest automatycznie wyłączany w argumencie modę. (Bit lepki opiszemy w kolejnym podrozdziale). Oznacza to, że tylko nadzorca ma prawo ustawić bit lepki dla zwykłego pliku. Takie podejście jest niezbędne, by złośliwi użytkownicy przez ustawienie bitu lepkiego nie mogli przepełnić obszaru wymiany, jeżeli konkretny system ma zaimplementowaną właściwość zachowanego tekstu. • Nowo utworzony plik może być w grupie, do której nie należy nasz proces. Przypomnijmy sobie, że zgodnie z opisem w podrozdz. 4.6 identyfikator grupy nowego pliku może być identyfikatorem grupy katalogu macierzystego. Konkretnie, jeżeli identyfikator grupy nowego pliku nie jest równy ani obowiązującemu identyfikatorowi grupy procesu, ani żadnemu z dodatkowych identyfikatorów grup, a proces nie ma uprawnień nadzorcy, to automatycznie jest wyłączany bit ustanowienia identyfikatora grupy. Dzięki takiej strategii użytkownik nie może utworzyć pliku z ustawionym bitem ustanowienia identyfikatora grupy, jeśli sam nie należy do danej grupy. W systemie 4.3+BSD oraz w innych systemach berkelejowskich dodano inną właściwość, która zabezpiecza przed niepoprawnym użyciem bitów uprawnień. Jeżeli proces nie mający uprawnień nadzorcy zapisuje dane do pliku, to automatycznie są wyłączane bity ustanowienia identyfikatorów użytkownika i grupy. Jeżeli złośliwy użytkownik wyszuka plik z ustawionym bitem ustanowienia identyfikatora użytkownika lub grupy, do którego ma prawo zapisać dane, to uda mu się zmienić zawartość pliku, ale straci specjalne uprawnienia tego pliku.

0

Bit lepki Bit S_ISVTX ma bardzo ciekawą historię. We wcześniejszych wersjach Uniksa nazywano go bitem lepkim. Ustawiano go dla plików wykonywalnych zawierających programy. Gdy program wykonywał się pierwszy raz, to w chwili zakończenia procesu kopia tekstu programu była umieszczana w obszarze

123

Systemy SVR4 i 4.3+BSD zezwalają również na ustawienie bitu lepkiego dla katalogu. Jeżeli zostanie on ustawiony dla katalogu, to użytkownik, który ma możliwość usuwania pliku lub zmiany jego nazwy, musi mieć prawo zapisu w tym katalogu oraz musi spełniać jeden z poniższych warunków: • być właścicielem pliku, • być właścicielem katalogu lub • być nadzorcą systemu. Dobrym przykładem stosowania bitu lepkiego są katalogi /tmp i / v a r / s p o o l / u u c p p u b l i c , ponieważ użytkownicy często tworzą tam swoje pliki. Zazwyczaj te katalogi mają w prawach dostępu włączone bity odczytu, zapisu i wykonania dla wszystkich (użytkownika, grupy i innych). Użytkownicy nie mogą jednak usuwać lub zmieniać nazwy obcego pliku. Standard POSIX.l nie definiuje pojęcia bitu lepkiego. Jest to rozszerzenie zaimplementowane w systemach SVR4 oraz 4.3+BSD.

4.11

Funkcje chown, f chown oraz lchown Funkcje serii chown służą do zmiany identyfikatora użytkownika pliku oraz identyfikatora grupy pliku. #include core.copy $ ls -1 core* - r w - r — r — 1 stevens 8483248 Nov 18 12:18 core -rw-rw-r— 1 stevens 8483248 Nov 18 12:27 core.copy $ du -s core* 272 core 16592 core.copy

Nowy plik zawiera 8 495 104 (512*16 592) bajtów. Różnica między tym rozmiarem a rozmiarem podanym przez polecenie ls jest spowodowana liczbą bajtów zużywanych przez system plików do przechowywania wskaźników do rzeczywistych bloków danych. Czytelnik, którego to zagadnienie interesuje, powinien zapoznać się zpodrozdz. 4.2 książki Bacha, 1986 [15] oraz podrozdz. 7.2 książki Lefflera, 1989 [32], w których podano wiele szczegółów na temat fizycznego rozłożenia plików.

127

4.14. Systemy plików

4.13 Skracanie plików Zdarza się, że chcemy skrócić plik i skasować dane umieszczone na końcu. Specjalnym przypadkiem skracania jest przypisanie plikowi zerowego rozmiaru za pomocą sygnalizatora O_TRUNC W funkcji open. #include łfinclude i n t t r u n c a t e (const char *pathname, off_t length) ; i n t f t r u n c a t e (int filedes, off_t length); Przekazują: 0, jeśli wszystko w porządku; -1, jeśli wystąpił błąd

Obie funkcje skracają plik do rozmiaru równego length bajtów. Jeżeli poprzedni rozmiar pliku był większy niż length bajtów, to dane powyżej bajtu o numerze length stają się niedostępne. Jeżeli poprzedni rozmiar pliku był mniejszy niż length bajtów, to wynik zależy od systemu. Gdy dana implementacja realizuje powiększanie pliku, to dane między starym a obecnym końcem pliku będą odczytywane jako bajty o wartości 0 (czyli w pliku prawdopodobnie powstaje dziura). Obie funkcje są zaimplementowane w systemach SVR4 oraz 4.3+BSD. Nie są częścią standardu POSIX.! ani XPG3. System SVR4 skraca lub rozszerza plik. System 4.3+BSD za pomocą tych funkcji tylko skraca plik - nie możemy ich użyć w celu zwiększenia rozmiaru pliku. Nigdy jednak nie było standardowego sposobu skracania pliku w Uniksie. Aplikacje, w których jest istotna przenośność, powinny stosować technikę kopiowania zadanej liczby znaków z jednego pliku do drugiego. W systemie SVR4 istnieje rozszerzenie funkcji f c n t l (F_FREESP), pozwalające zwolnić dowolny fragment pliku, a nie tylko blok na końcu pliku.

Zastosujemy funkcję f t r u n c a t e w próg. 12.5, kiedy będziemy musieli wyzerować plik po otrzymaniu rygla związanego z nim.

4.14 Systemy plików Aby docenić koncepcję dowiązań do pliku, musimy dobrze poznać strukturę uniksowego systemu plików. Ważne jest również zrozumienie różnicy między i-węzłem oraz pozycją w katalogu, która wskazuje na ten i-węzeł. Możemy się spotkać z różnorodnymi implementacjami uniksowych systemów plików, które są obecnie w użyciu. Na przykład system SVR4 stosuje dwa różne typy dyskowych systemów plików: tradycyjny system plików

128

4. Pliki i katalogi

zgodny z Systemem V (nazywany S5) oraz ujednolicony system plików (Unified File System) (nazywany UFS). O jednej z różnic między tymi dwoma systemami plików dowiedzieliśmy się, analizując tab. 2.6. UFS jest oparty na berkelejowskim szybkim systemie plików. W systemie SVR4 dodatkowo istnieją niedyskowe systemy plików, dwa sieciowe systemy plików oraz system plików wspomagający wstępne ładowanie systemu {bootstrap filesystem), ale architektura żadnego z nich nie ma wpływu na naszą dyskusję. W tym podrozdziale opiszemy tradycyjny uniksowy system plików systemu V. Ten typ systemu plików pochodzi z wersji 7. Dla uproszczenia przyjmijmy, że w napędzie dyskowym jest wydzielona co najmniej jedna partycja. Każda partycja może zawierać system plików, jak pokazano na rys. 4.1. partycja

napęd dysku

partycja

system plików

i-lista

partycja

bloki katalogów oraz bloki danych

blok ( ładowania wstępnego blok nadrzędny i-węzeł

i-węzeł

i-węzeł

Rys. 4.1 Napęd dyskowy, partycje i system plików

I-węzły są pozycjami o ustalonym rozmiarze i gromadzą większość informacji na temat pliku. W wersji 7 i-węzel zajmuje 64 bajtów, a w systemie 4.3+BSD - 128 bajtów. W systemie SVR4 rozmiar i-węzła zależy od typu systemu plików: i-węzel w S5 ma 64 bajtów, a w UFS-ie - 128 bajtów.

Na rysunku 4.2 przedstawiamy szczegółowo informację zawartą w systemie plików, przy czym pominięto na nim blok systemowy oraz blok identyfikujący. Zwróćmy uwagę na poszczególne elementy z rys. 4.2: • Pokazaliśmy dwie pozycje w katalogu, które wskazują na ten sam i-węzeł. Każdy i-węzeł ma licznik dowiązań zawierający liczbę pozycji w katalogu, które wskazują na ten i-węzeł. Plik może zostać usunięty tylko wówczas, gdy licznik dowiązań uzyska wartość 0 (czyli można

12S

4.14. Systemy plików bloki katalogów oraz bloki danych i-lista

i-węzel i-węzeł

Rys. 4.2 Szczegółowy opis systemu plików

zwolnić bloki danych związane z plikiem). To właśnie jest przyczyną że operacja usunięcia dowiązania do pliku nie zawsze oznacza usunięcie bloków danych należących do tego pliku. Dlatego też funkcja usuwającą pozycję w katalogu nazywamy u n l i n k , a nie d e l e t e W strukturze s t a t licznik dowiązań jest zlokalizowany w poh s t _ n l i n k . Jego elementarnym systemowym typem danych jes n l i n k t . Te rodzaje dowiązań są nazywane twardymi. Przypomi namy, że zgodnie z tab. 2.7 standard POSIX.l definiuje stał; LINK_MAX, określającą maksymalną wartość licznika dowiązań dli pliku. Innym rodzajem dowiązań są dowiązania symboliczne. Faktyczną za wartością pliku tego typu (czyli jego bloków danych) jest nazw* pliku, na którą wskazuje to dowiązanie. Na przykład pokazana niże pozycja: lrwxrwxrwx 1 root 7 Sep 25 07:14 lib -> usr/lib

oznacza, że nazwa pliku w katalogu liczy trzy litery, a plik ma rozmia 7 bajtów i jest w nim umieszczona nazwa u s r / l i b . Dzięki temu, 7* przechowywany w i-węźle typ pliku ma wartość stałej S__IFLNK, syi, tem wie, że jest to dowiązanie symboliczne. I-węzeł zawiera wszystkie informacje na temat pliku: jego typ, bit! praw dostępu, rozmiar, wskaźniki do bloków danych itp. Większoś; informacji, która jest przekazywana w strukturze s t a t , jest pobierań! z i-węzła. Tylko dwa elementy są zapamiętane w pozycji katalogi^ nazwa pliku oraz numer i-węzła. Typem danych stosowanym dla nuj meru i-węzła jest i n o _ t . Ponieważ numer i-węzła w pozycji katalogu musi wskazywać na i-wę1 zeł w tym samym systemie plików, więc żadna pozycja w katalogu nic może wskazywać i-węzła w innym systemie plików. Dlatego polecę

4. Pliki i katalogi

4.15. Funkcje l i n k , unlink, remove i rename

nie l n ( l ) (utwórz nową pozycję w katalogu, która wskazuje na istniejący plik) nie może łączyć plików w różnych systemach plików. Funkcję l i n k opiszemy w następnym podrozdziale. Jeżeli zmieniamy nazwę pliku w ten sposób, że plik pozostaje w tym samym systemie plików, to nie jest dokonywane przeniesienie pliku trzeba jedynie utworzyć nową pozycję w katalogu powiązaną z istniejącym i-węzłem i usunąć starą pozycję katalogu. Na przykład, gdy zmieniamy nazwę pliku / u s r / l i b / f o o na /usr/foo, a katalogi / u s r / l i b i /usr leżą w tym samym systemie plików, to zawartość pliku f oo nie musi zostać przeniesiona. Tak właśnie działa polecenie

zwę (którego nie pokazaliśmy na rys. 4.3), katalog kropka oraz katalog dwie kropki naszego katalogu t e s t d i r . Zwróćmy uwagę, że każdy katalog w bieżącym katalogu powoduje zwiększenie licznika dowiązań katalogu bieżącego o 1. Jak powiedzieliśmy, jest to klasyczny format systemu plików, który szczegółowo jest opisany w rozdz. 4 książki Bacha, 1986 [15]. Więcej informacji na temat zmian w porównaniu do szybkiego berkelejowskiego systemu plików można znz\tit w książce Lefflera i in., 1989 [32].

4.15 Mówiliśmy na temat licznika dowiązań dla plików, ale czy taki licznik jest używany dla katalogów? Załóżmy, że tworzymy nowy katalog $ mkdir testdir

Na rysunku 4.3 widzimy wynik operacji. Pokazujemy na nim jawnie pozycje katalogów kropka (.) i dwie kropki (. .). bloki katalogów oraz bloki danych blok j katalogu i

i-lista

Funkcje l i n k , u n l i n k , remove i rename W poprzednim podrozdziale przekonaliśmy się, że każdy plik może być reprezentowany przez wiele pozycji w katalogach, które wskazują na ten sam i-węzeł. Dowiązania do istniejącego pliku tworzymy za pomocą funkcji l i n k . #include i n t link(const char *existingpath, const char * newpath) ;

Przekazuje: 0, jeśli wszystko w porządku;-1, jeśli wystąpił błąd 1 blok | k.italogu

?54£) 1267

131

1267 numer i-węzła 2549

Funkcja tworzy w katalogu nową pozycję o nazwie ścieżki newpath, odwołującą się do istniejącego pliku o nazwie ścieżki existingpath. Jeżeli nazwa newpath już istnieje, to jest przekazywany błąd. Czynności utworzenia nowej pozycji w katalogu oraz zwiększenia licznika dowiązań muszą być realizowane jako jedna operacja atomowa. (Przypominamy dyskusję na temat operacji atomowych z podrozdz. 3.11). Większość implementacji, jak np. systemy SVR4 i 4.3+BSD, wymaga by obie nazwy ścieżek dotyczyły tego samego systemu plików. Norma POSK.l pozwala, by implementacje stosowały dowiązania plików, które są zlokalizowane w różnych systemach plików.

testdir

Rys. 4.3 Przykładowy system plików po utworzeniu katalogu t e s t d i r

I-węzeł o numerze 2549 ma pole typu, które 07x&.z7a. katalog oraz licznik dowiązań równy 2. Każdy końcowy katalog (czyli taki, który nie zawiera innych katalogów) zawsze ma licznik dowiązań równy 2. Wartość 2 uwzględnia pozycję katalogu z nazwą tego katalogu ( t e s t d i r ) oraz pozycję dla katalogu kropka w tym katalogu. I-węzeł o numerze 1267 ma pole typu, które oznacza katalog oraz licznik dowiązań większy lub równy 3. Wartość licznika wynika z tego, że wskazują go co najmniej: katalog zawierający tę na-

Tylko nadzorca może utworzyć nowe dowiązanie, które wskazuje na katalog. Powodem tego ograniczenia jest niebezpieczeństwo powstania pętli' w systemie plików, która może być źródłem kłopotów w programach obsługujących system plików. (W podrozdziale 4.16 zobaczymy przykład pętli spowodowanej dowiązaniem symbolicznym). Aby usunąć istniejącą pozycję w katalogu, wywołujemy funkcję unlink. #include i n t unlink(const char *pathname) ;

Przekazuje: 0, jeśli wszystko w porządku, - 1 , jeśli wystąpił błąd

S2

4. Pliki i katalogi

Ta funkcja usuwa pozycję z katalogu i zmniejsza licznik dowiązań dla pliku wskazywanego nazwą ścieżki pathname. Jeżeli istnieją inne dowiązania do pliku, to dane w tym pliku pozostają dostępne. Jeżeli funkcja przekazuje błąd, to plik na pewno nie uległ zmianie. Wspomnieliśmy wcześniej, że aby usunąć dowiązanie, musimy dysponować prawem zapisu oraz wykonania w katalogu, w którym jest zawarta pozycja odpowiadająca temu dowiązaniu. W podrozdziale 4.10 dodaliśmy, że jeżeli dla tego katalogu jest ustawiony bit lepki, to oprócz prawa zapisu w katalogu trzeba spełnić jeden z poniższych warunków: • być właścicielem pliku, • być właścicielem katalogu, • mieć uprawnienia nadzorcy. Zawartość pliku jest usuwana, tylko gdy licznik dowiązań osiągnie wartość 0. Dodatkowo, plik nie może zostać usunięty, jeżeli jakiś proces utrzymuje go w stanie otwarcia. Gdy proces zamyka plik, jądro systemu najpierw sprawdza licznik wskazujący, w ilu procesach plik pozostaje nadal otwarty. Jeżeli licznik ten osiągnie wartość zerową, to system sprawdza licznik dowiązań i gdy jest on równy zeru, to usuwa zawartość pliku.

rzykład Program 4.5 otwiera plik, a następnie usuwa go za pomocą funkcji u n l i n k . Zakończenie pracy jest opóźnione o 15 sekund (wywołanie funkcji sleep). .nclude .nclude Lnclude mclude

. (Może ktoś zapomniał zdefiniować makra S_ISLNK?). Zaprojektuj sposób zaradzenia temu niedociągnięciu. Pokaż, co należy umieścić w pliku nagłówkowym , aby każdy program potrzebujący makra S_ISLNK mógł go użyć.

Niektóre wersje polecenia f inger(l) wypisują teksty ,JVew maił received ..." i „unread sińce ...", a w miejscu ... są umieszczone odpowiednie daty i czasy. W jaki sposób program może ustalić te dwa czasy i daty?

4.16

Przeanalizuj format archiwizacji stosowany przez polecenia cpio(l) oraz tar(l). (Ich opisy można tradycyjnie znaleźć w części 5 uniksowego podręcznika systemu). Ile z trzech możliwych wartości czasów jest zapamiętywanych dla każdego pliku? Gdy plik jest odtwarzany, to zgodnie z jaką wartością Twoim zdaniem, jest ustawiany czas ostatniego dostępu. Dlaczego?

4.17

Polecenie f i l e ( l ) próbuje określić logiczny typ pliku (program w języku C, program w języku Fortran, skrypt powłoki itp.). Czyta początek pliku, analizuje jego zawartość i na tej postawie podejmuje decyzję. W niektórych systemach uniksowych jest polecenie, które umożliwia wykonywanie innych poleceń i prześledzenie wszystkich funkcji systemowych realizowanych przez to polecenie. (W systemie SVR4 jest to polecenie truss(l). W 4.3+BSD służą do tego polecenia ktrace(l) i kdump(l). Poniższy przykład korzysta z polecenia trace(l) w systemie SunOS). Jeżeli uruchomimy śledzenie funkcji systemowych polecenia f i l e

Ostatnie osiem stałych można również zgrupować, ponieważ:

s

IRWXU = S IRUSR 1 S IWUSR 1 S IXUSR

s IRWXG = S_IRGRP ! S_IWGRP t s_IXGRP s IRWXO = S IROTH I S IWOTH 1 s IXOTH

Podsumowanie

4.3

Co stanie się, gdy maska trybu dostępu ma wartość 777 (ósemkowo)? Potwierdź wyniki, używając polecenia umask w Twojej powłoce.

4.4

Sprawdź, że gdy wyłączysz w jakimś ze swoich plików prawo odczytu dla użytkownika, wówczas stracisz możliwość dostępu do tego pliku.

4.5

Uruchom próg. 4.3 po utworzeniu plików foo i bar. Co się stało?

4.6

W podrozdziale 4.12 powiedzieliśmy, że rozmiar zerowy zwykłego pliku jest poprawny. Powiedzieliśmy również, że katalogi i symboliczne dowiązania mają zdefiniowaną wartość pola s t s i z e . Czy możemy kiedykolwiek zobaczyć rozmiar 0 dla katalogu lub symbolicznego dowiązania?

4.7

Napisz program wzorowany na poleceniu cp ( l ) , który będzie kopiował pliki, zawierające dziury bez zapisywania bajtów zerowych w pliku wynikowym.

4.8

Zwróć uwagę, że w wydruku z polecenia l s w podrozdz. 4.12 pliki core i core. copy mają różne prawa dostępu. Wyjaśnij, skąd wynika różnica, jeżeli nie uległa zmianie maska trybu dostępu między utworzeniem tych dwóch plików.

4.9

W czasie uruchomienia próg. 4.5 sprawdzaliśmy za pomocą polecenia df(l) dostępną przestrzeń dyskową. Dlaczego nie użyliśmy polecenia du(l)?

4.10

W tabeli 4.9 pokazaliśmy, że funkcja unlink modyfikuje czas zmiany stanu samego pliku. Jak to się dzieje?

tracę file a.out

zobaczymy, że były wywoływane następujące funkcje: l s t a t ( " a . o u t " , 0xf7fff650) = 0 open ( " a . o u t " , 0, 0) =3 read (3, " . . , 512) = 512 f s t a t (3, 0xf7fffl60) = 0 w r i t e ( 1 , " a . o u t : demand paged e x e c u " . . . , 44) = 44 a . o u t : demand paged e x e c u t a b l e not s t r i p p e d utime ( " a . o u t , 0xf7ffflb0) = 0

Dlaczego polecenie f i l e wywołuje funkcję utime?

4. Pliki i katalogi 4.18

4.19

5

Czy w Uniksie jest jakieś ograniczenie dotyczące liczby poziomów w drzewie katalogu? Aby znaleźć odpowiedź, przygotuj program, który w pętli tworzy katalog i przechodzi do niego. Zawsze upewnij się, czy długość bezwzględnej nazwy ścieżki katalogu końcowego nie przekracza systemowego ograniczenia PATH MAX. Czy możesz wywołać funkcję getcwd, aby pobrać nazwę ścieżki katalogu? Jak obsługują długie nazwy ścieżek standardowe programy narzędziowe Uniksa? Czy możesz zarchiwizować katalog za pomocą programu t a r lub cpio?

Standardowa biblioteka wejścia-wyjścia

W podrozdziale 3.15 opisaliśmy rolę katalogu /dev/fd. Aby użytkownicy mogli korzystać z takich plików, muszą one mieć ustalone prawa dostępu o postaci rw-rw-rw-. Niektóre programy, które wyprowadzają wyniki do pliku, najpierw usuwają wcześniej istniejący plik (ignorując kod powrotu). unlink(path); if ( (fd = c r e a t ( p a t h , FILE__MODE) ) < 0) err_sys(...); Co stanie się, jeśli w argumencie path podamy /dev/fd/l?

5.1

Wprowadzenie W tym rozdziale opiszemy standardową bibliotekę wejścia-wyjścia. Jej definicja jest zawarta w standardzie ANSI C, gdyż była implementowana w wielu systemach operacyjnych, również tych, które nie mają związku z Uniksem. Biblioteka ta obsługuje takie szczegóły jak alokowanie buforów oraz optymalne wykonywanie wejścia-wyjścia. Dzięki niej programista nie musi troszczyć się o dobór poprawnej wielkości bufora (jak np. w podrozdz. 3.9). Korzystanie z biblioteki nie jest trudne, chociaż nieraz wprowadza ona dodatkowe problemy, głównie spowodowane tym, że nie jesteśmy świadomi, co w istocie się dzieje. Standardową bibliotekę wejścia-wyjścia napisał Dennis Ritchie około 1975 roku. Powstała ona w wyniku znaczącej modyfikacji biblioteki (Portable I/O Library) autorstwa Mike'a Leska. Zdumiewające, że bardzo niewiele rzeczy uległo w niej zmianie przez ponad 15 lat.

5.2

Obiekty strumieni i typ danych F I L E Wszystkie procedury wejścia-wyjścia, które pokazaliśmy w rozdz. 3, korzystały z deskryptorów plików. Po otwarciu pliku otrzymujemy deskryptor pliku, którego następnie używamy do wykonywania operacji wejścia-wyjścia. Dyskusja na temat biblioteki wejścia-wyjścia skupia się wokół strumieni. (Nie można utożsamiać strumienia, standardowego terminu wejścia-wyjścia, z techniką STREAMS I/O zdefiniowaną w Systemie V). Po utworzeniu lub otwarciu pliku za pomocą standardowej biblioteki wejścia-wyjścia otrzymujemy strumień związany z danym plikiem.

5. Standardowa biblioteka wejścia-wyjścia

Kiedy otwieramy strumień, wtedy standardowa funkcja wejścia-wyjścia o nazwie f open przekazuje wskaźnik do obiektu typu FILE. Obiekt ten jest na ogół strukturą danych zawierającą wszystkie informacje wymagane przez standardową bibliotekę wejścia-wyjścia do zarządzania strumieniem, czyli: deskryptor pliku, rozmiar bufora, licznik znaków w buforze, sygnalizator błędu itp. W zasadzie w oprogramowaniu użytkowym nigdy nie ma potrzeby analizowania obiektu FILE. Wskaźnik do obiektu typu FILE jest jednak argumentem wszystkich funkcji standardowego wejścia-wyjścia. Dalej w tekście będziemy odwoływać się do wskaźnika obiektu FILE, czyli typu FILE * za pomocą określenia wskaźnik pliku (file pointer). W bieżącym rozdziale opisujemy standardową bibliotekę wejścia-wyjścia w odniesieniu do systemów uniksowych. Jak wspomnieliśmy, ta biblioteka jest dostosowana do różnego rodzaju systemów operacyjnych, również niezgodnych z systemem Unix. Jednak, aby pokazać ją od wnętrza, posługujemy się jej konkretną implementacją w systemie Unix.

Standardowe wejście, standardowe wyjście i standardowy strumień komunikatów awaryjnych W każdym procesie są wstępnie zdefiniowane i automatycznie dostępne trzy strumienie: standardowe wejście, standardowe wyjście i standardowy strumień komunikatów awaryjnych. Odnoszą się one do tych samych plików, które wskazują deskryptory plików STDIN_FILENO, STDOUT_FILENO, STDERR_FILENO, wspomniane w podrozdz. 3.2. Do tych trzech standardowych strumieni wejścia-wyjścia odwołujemy się za pomocą wskaźników plików s t d i n , s t d o u t oraz s t d e r r , zdefiniowanych w pliku nagłówkowym < s t d i o . h>.

Buforowanie Celem buforowania, stosowanego przez standardową bibliotekę wejścia-wyjścia jest minimalizacja liczby wywołań read oraz w r i t e . (Przypominamy tab. 3.1, w której zebraliśmy wyniki pomiarów czasu procesora potrzebnego do wykonania operacji wejścia-wyjścia przy różnych rozmiarach buforów). Oprócz tego, aby odciążyć programy użytkowe, biblioteka próbuje sama automatycznie wprowadzać buforowanie dla każdego strumienia wejścia-wyjścia. Niestety, buforowanie jest tym aspektem biblioteki wejścia-wyjścia, który wprowadza najwięcej nieporozumień. Istniejątrzy rodzaje buforowania: 1. Pełne buforowanie. Dla tego przypadku faktyczna operacja wejścia-wyjścia odbywa się, gdy bufor standardowego wejścia-wyjścia będzie całkowicie wypełniony. Standardowa biblioteka wejścia-wyjścia stosuje za-

5.4. Buforowanie

161

zwyczaj pełne buforowanie w odniesieniu do plików umieszczonych na dysku. Typowo funkcja biblioteki wejścia-wyjścia uzyskuje potrzebny bufor przez wywołanie funkcji malloc (podrozdz. 7.8) przy pierwszej operacji wykonywanej na tym strumieniu. Termin opróżnianie bufora (flush) oznacza wypisanie zawartości standardowego bufora wejścia-wyjścia. Bufor może zostać opróżniony automatycznie przez procedury wejścia-wyjścia (np. gdy zostanie całkowicie wypełniony) lub po wywołaniu funkcji f flush. Tak się składa, że w środowisku uniksowym słowo opróżnić może określać dwie rzeczy. W terminologii standardowej biblioteki wejścia-wyjścia oznacza wypisanie zawartości bufora (który może być tylko częściowo wypełniony). W terminologii procedur obsługi terminalu (np. w przypadku funkcji t c f lush z rozdz. 11) oznacza skasowanie danych zapamiętanych wcześniej w buforze. 2. Buforowanie wierszy. W tym przypadku standardowa biblioteka wejścia-wyjścia wykonuje operację po napotkaniu znaku nowego wiersza w danych wejściowych lub wyjściowych. Możemy więc wyprowadzać pojedyncze znaki (za pomocą standardowej funkcji fputc) i mamy pewność, że rzeczywista operacja wejścia-wyjścia odbędzie się, gdy zakończymy zapisywanie jednego wiersza. Buforowanie wierszy jest typowo używane dla strumieni, które odnoszą się do terminalu (np. standardowe wejście i standardowe wyjście). Gdy korzystamy z buforowania wierszy, zawsze musimy pamiętać o dwóch zagrożeniach. Po pierwsze, ponieważ bufor używany przez standardową bibliotekę wejścia-wyjścia do gromadzenia danych w wierszu ma ustalony rozmiar, w rzeczywistości operacja wejścia-wyjścia może odbyć się w sytuacji wypełnienia zawartości bufora przed pojawieniem się znaku nowego wiersza. Oprócz tego obowiązuje reguła, że za każdym razem, gdy program zażąda wprowadzenia danych przy użyciu standardowej biblioteki wejścia-wyjścia albo (a) ze strumienia niebuforowanego, albo (b) ze strumienia z buforowaniem wierszy (co wymaga pobrania danych z jądra systemu), najpierw jest realizowane opróżnienie wszystkich strumieni stosujących buforowanie wierszy. Przyczyną wprowadzenia tego warunku dla sytuacji (b) jest potencjalna możliwość, że żądane dane są już w buforze, a więc zbędne będzie ich czytanie z jądra systemu. Oczywiście każde wprowadzenie danych ze strumienia niebuforowanego, czyli sytuacja (a), wiąże się z pobraniem danych z jądra. 3. Bez buforowania. W tym przypadku standardowa biblioteka wejścia-wyjścia nie buforuje znaków. Jeżeli np. zapiszemy 15 znaków za pomocą standardowej funkcji wejścia-wyjścia fputs, to oczekujemy, że znaki zostaną wyprowadzone jak najszybciej (prawdopodobnie za pomocą funkcji w r i t e opisanej w podrozdz. 3.8). Przykładowo, standardowy strumień komunikatów jest na ogół niebuforowany, aby wszelkie komunikaty o błędach pojawiały się w miarc

5. Standardowa biblioteka wejścia-wyjścia

163

5.4. Buforowanie

możliwości natychmiast, niezależnie od tego, czy są one zakończone znakiem nowego wiersza, czy nie.

W funkcji setvbuf określamy precyzyjnie, jaki typ buforowania chcemy wprowadzić. Służy do tego argument buf. IOFBF IOLBF IONBF

Standard ANSI C narzuca następującą charakterystykę buforowania: 1. Strumienie standardowego wejścia i wyjścia stosują pełne buforowanie, ale wyłącznie wówczas, gdy nie są związane z urządzeniami interakcyjnymi. 2. Standardowy strumień komunikatów awaryjnych nigdy nie stosuje pełnego buforowania.

Jeżeli określamy strumień jako niebuforowany, to argumenty buf oraz size są ignorowane. Jeżeli definiujemy strumień w pełni buforowany lub buforowany wierszami, to argumenty buf\ size mogą opcjonalnie wskazywać bufor o zadanym rozmiarze. Gdy strumień jest buforowany i argument buf jest równy NULL, wówczas standardowa biblioteka wejścia-wyjścia automatycznie przydzieli temu strumieniowi własny bufor o odpowiednim rozmiarze. Przez pojęcie odpowiedni rozmiar rozumiemy wartość określoną w polu st b l k s i z e struktury s t a t opisanej w podrozdz. 4.2. Jeżeli system nie jest w stanie ustalić tej wartości dla danego strumienia (np. gdy strumień odnosi się do urządzenia lub łącza komunikacyjnego), to zaalokowany bufor ma rozmiar BUFSIZ.

Te wymagania nie precyzują jednak pozostałych zasad. Nie wiemy, czy jeżeli strumienie standardowego wejścia i standardowego wyjścia są związane z urządzeniem interakcyjnym, to mają być niebuforowane lub buforowane wierszami; czy standardowy strumień komunikatów awaryjnych powinien być niebuforowany, czy buforowany wierszami. W systemach SVR4 oraz 4.3+BSD domyślnie są stosowane następujące rodzaje buforowania:

Używanie wartości s t _ b l k s i z e jako rozmiaru bufora wywodzi się z systemów berkelejowskich. We wcześniejszych wersjach Systemu V używano stałej standardowego wejścia-wyjścia BUFSIZ (typowo wartość 1024). Nawet w systemie 4.3+BSD jest zdefiniowana wartość stałej BUFSIZ równa 1024, chociaż do ustalenia optymalnego rozmiaru bufora operacji wejścia-wyjściajest stosowana wartość pola s t _ b l k s i z e .

• Standardowy strumień komunikatów awaryjnych jest niebuforowany. • Wszystkie inne strumienie są buforowane wierszami, jeżeli dotyczą urządzenia terminalu; w przeciwnym razie są w pełni buforowane. Jeżeli te domyślne ustawienia nie odpowiadają nam dla danego strumienia, to możemy zmienić typ buforowania, wywołując jedną z pokazanych poniżej funkcji. #include

W tabeli 5.1 zamieszczamy wyniki wykonania poszczególnych funkcji oraz podajemy różne opcje tych funkcji. Tabela 5.1 Zestawienie funkcji setbuf i setvbuf Funkcja

modę

void setbuf(FILE *fp, char *buf) ; int setvbuf(FILE *fp, char *buf, int modę, size_t size) ;

buf

Bufor i jego rozmiar

Typ buforowania

niepusty

bufor użytkowy buf o rozmiarze BUFSIZ

pełne buforowanie lub buforowanie wierszami

NULL

(brak bufora)

bez buforowania

niepusty

bufor użytkowy buf o rozmiarze size

NULL

bufor systemowy o odpowiednim rozmiarze

setbuf

Przekazuje: 0, jeśli wszystko w porządku; wartość niezerową, jeśli wystąpił błąd

Funkcje te wywołujemy zawsze po otwarciu strumienia (jest to naturalne, ponieważ każda z nich wymaga podania w pierwszym argumencie istniejącego wskaźnika pliku), ale zanim wykonamy jakąkolwiek operację na danym strumieniu. Za pomocą funkcji s e t b u f możemy włączyć lub wyłączyć buforowanie. Aby włączyć buforowanie, argument buf musi wskazywać na bufor o rozmiarze BUFSIZ (stała zdefiniowana w pliku < s t d i o . h > ) . Typowo strumień jest po takiej operacji w pełni buforowany, ale w niektórych systemach może zostać ustawione buforowanie wierszy, jeżeli strumień jest związany z terminalem. Aby wyłączyć buforowanie, podajemy w argumencie buf wartość NULL.

pełne buforowanie buforowanie wierszy brak buforowania

_IOFBF

setvbuf

niepusty

bufor użytkowy buf o rozmiarze size

_IOLBF

NULL

bufor systemowy o odpowiednim rozmiarze

_IONBF

(ignorowany) (brak bufora)

pełne buforowanie

buforowanie wierszami bez buforowania

Należy być świadomym tego, że jeśli w jakiejś funkcji alokujemy standardowy bufor wejścia-wyjścia jako zmienną automatyczną, to przed powrotem z niej musimy zamknąć strumień. (Więcej na ten temat powiemy w podrozdz. 7.8). Dodatkowo, system SVR4 używa części bufora dla własnych

5. Standardowa biblioteka wejścia-wyjścia

potrzeb, więc rzeczywista liczba bajtów danych, które możemy wpisać do bufora, jest mniejsza niż size. W zasadzie powinniśmy zrzucić na system dobór rozmiaru bufora i jego automatyczną alokację. Wówczas standardowa biblioteka wejścia-wyjścia również automatycznie zwolni bufor w czasie zamykania strumienia. W dowolnej chwili możemy wymusić opróżnienie bufora.

3. Funkcja fdopen pobiera istniejący deskryptor pliku (możemy wcześniej otrzymać go z funkcji open, dup, dup2, f c n t l lub pipę) i wiąże z nim standardowy strumień wejścia-wyjścia. Tej funkcji używamy często, gdy dysponujemy deskryptorami uzyskanymi z funkcji tworzących łącza komunikacyjne lub kanały komunikacji sieciowej. Ponieważ specjalne typy plików nie mogą zostać otwarte za pomocą standardowej funkcji wejścia-wyjścia fopen, więc musimy najpierw wywołać funkcję specyficzną dla danego urządzenia, aby otrzymać deskryptor pliku, a następnie powiązać ten deskryptor ze standardowym strumieniem wejścia-wyjścia za pomocą funkcji fdopen.

tinclude int fflush(FILE *fp) ; Przekazuje: 0, jeśli wszystko w porządku, znacznik końca pliku (EOF), jeśli wystąpił błąd

Funkcje fopen oraz freopen są częścią standardu ANSI C. Funkcja fdopen jest zdefiniowana w normie POSIX.l, ponieważ ANSI C nie zajmuje się deskryptorami plików.

Funkcja ta powoduje, że wszystkie niezapisane dane dla danego strumienia są przekazywane do jądra systemu. W szczególnym przypadku, jeśli argumenty jest równy NULL, następuje opróżnienie wszystkich strumieni wyjściowych. Możliwość przekazania pustego wskaźnika w celu wymuszenia opróżnienia wszystkich strumieni wyjściowych jest nowością w standardzie ANSI C. Biblioteki niezgodne z ANSI C (np. wcześniejsze wersje Systemu V oraz 4.3BSD) nie mają tej właściwości.

165

5.5. Otwarcie strumienia

Standard ANSI C określa 15 różnych wartości argumentu type. Pokazujemy je wtab. 5.2. Tabela 5.2

Argument type przy otwarciu standardowego strumienia wejścia-wyjścia

type

Opis

r lub r b

otwarcie do odczytu

w lub wb

skrócenie długości do zera lub utworzenie do zapisu

Otwarcie strumienia

a lub ab

dołączanie danych; otwarcie w celu zapisywania na końcu pliku lub utworzenie do zapisu

Poniższe trzy funkcje otwierają standardowy strumień wejścia-wyjścia.

r+ lub r+b lub r b +

otwarcie do odczytu i zapisu

w+ lub w+b lub wb+

skrócenie długości do zera lub utworzenie do odczytu i zapisu

a+ lub a+b lub a b +

otwarcie lub utworzenie do odczytu lub zapisu na końcu pliku

#include

FILE *fopen(const char *pathname, const char *type) ; FILE *freopen (const char *pathname, const char *type, FILE *fp) ; FILE *fdopen (int filedes, const char *type) ; Przekazują: wskaźnik pliku, jeśli wszystko w porządku; NULL, jeśli wystąpił błąd

Oto różnice między tymi trzema funkcjami: 1. Funkcja f open otwiera podany plik. 2. Funkcja freopen otwiera podany plik, który będzie związany ze wskazanym w argumencie strumieniem. Jeśli strumień ten był wcześniej otwarty, to jest najpierw zamykany. Typowe zastosowanie tej funkcji to otwarcie pliku na jednym z predefiniowanych strumieni: standardowym wejściu, standardowym wyjściu lub standardowym strumieniu komunikatów awaryjnych.

Podanie znaku b w argumencie type umożliwia rozróżnienie przez standardowy system wejścia-wyjścia plików tekstowych i binarnych. Ponieważ jądro systemu Unix nie widzi różnicy między tymi dwoma typami plików, więc w tym przypadku znak b w argumencie type nie ma żadnego znaczenia. Dla funkcji fdopen znaczenie argumentu type jest nieco odmienne. Ponieważ deskryptor pliku już istnieje, więc otwarcie pliku nie może wyzerować jego rozmiaru. (Jeżeli np. deskryptor powstał na skutek utworzenia pliku przez funkcję open, a plik istniał wcześniej, to sygnalizator O_TRUNC zdecydował, czy plik został skrócony, czy nie. Funkcja fdopen nie może skrócić żadnego pliku, który jest przez nią otwierany do zapisu). Oprócz tego, jeśli używamy standardowego wejścia-wyjścia w trybie dołączania, to nie możemy utworzyć pliku (ponieważ plik musi istnieć skoro istnieje deskryptor z nim związany). Gdy plik jest otwarty w trybie dołączania danych, wówczas każdy zapis rozpoczyna się od bieżącego końca pliku. Jeśli wiele procesów otworzy ten sam plik za pomocą standardowej biblioteki wejścia-wyjścia w trybie dołączania, to dane z każdego procesu zostaną poprawnie zapisane do pliku.

5. Standardowa biblioteka wejścia-wyjścia

5.6. Czytanie i zapisywanie danych ze strumienia

Wersje funkcji fopen z systemów berkelejowskich poprzedzających 4.3+BSD oraz prosta implementacja podana na s. 233 książki Kernighana i Ritchiego [28] nie obsługują poprawnie trybu dołączania. Wersje te, gdy strumień jest już otwarty, wykonują funkcję l s e e k w celu przejścia na koniec pliku. Aby właściwie realizować tryb dołączania, gdy mamy do czynienia z wieloma procesami, plik musi zostać otwarty z sygnalizatorem O_APPEND, opisanym w podrozdz. 3.3. Wykonanie funkcji lseek przed każdym zapisem nie jest wystarczające, zgodnie z tym co omówiliśmy w podrozdz. 3.11.

#include int fclose (FILE *fp) ; Przekazuje: 0, jeśli wszystko w porządku; EOF, jeśli wystąpił błąd

Strumień buforowany jest opróżniany zanim zostanie zamknięty. Wszystkie dane oczekujące na wczytanie są kasowane. Jeżeli standardowa biblioteka wejścia-wyjścia zaalokowała bufor dla strumienia, to jest on zwalniany. Gdy proces kończy się normalnie, czyli wywołuje wprost funkcję e x i t lub powraca z funkcji main, to wszystkie standardowe strumienie wejścia-wyjścia, które mają zbuforowane jeszcze niewypisane dane, są opróżniane, a następnie strumienie te są zamykane.

Gdy plik jest otwarty do odczytu i zapisu (znak plus w argumencie type), to obowiązują następujące ograniczenia: • Bezpośrednio po wyprowadzaniu danych nie można realizować wprowadzania danych, dopóki nie wywołamy jednej z funkcji: f f lush, fseek, f s e t p o s lub rewind. • Bezpośrednio po wprowadzaniu danych nie można wyprowadzać danych, dopóki nie wywołamy funkcji fseek, f s e t p o s albo rewind, albo jeśli operacja odczytu nie przekaże znacznika końca pliku. W tabeli 5.3 zamieszczamy podsumowanie sześciu różnych sposobów otwarcia strumienia, wymienionych w tab. 5.2. Tabela 5.3

Sześć różnych sposobów otwarcia standardowego strumienia wejścia-wyjścia

Ograniczenia

r

plik musi wcześniej istnieć



można czytać ze strumienia można tylko dopisywać na końcu strumienia

a

r+

w+

a+

• •

skasowana poprzednia zawartość pliku można zapisać do strumienia

w



• •



















Zauważmy, że nowy plik powstaje, jeśli w argumencie type podamy albo w, albo a, ale nie ma możliwości wskazania bitów praw dostępu do pliku (na co pozwalały funkcje open i c r e a t z rozdz. 3). Standard POSIX.l wymaga, by tworzony plik otrzymał następujące prawa dostępu

5.6

Czytanie i zapisywanie danych ze strumienia Po otwarciu strumienia mamy do dyspozycji trzy różne rodzaje operacji nieformatowanego wejścia-wyjścia. (Funkcje formatowanego wejścia-wyjścia, takie jak p r i n t f i scanf, opiszemy w podrozdz. 5.11). 1. Wejście-wyjście znak po znaku. Możemy czytać lub wypisać po jednym znaku, a standardowe funkcje wejścia-wyjścia obsługują kwestie buforowania (jeżeli strumień jest buforowany). 2. Wejście-wyjście wiersz po wierszu. Jeżeli chcemy czytać lub pisać po jednym wierszu, to korzystamy z funkcji f g e t s oraz f p u t s . Każdy wiersz jest zakończony znakiem nowego wiersza. Jeżeli używamy funkcji fgets, to musimy podać maksymalną długość wiersza, którą jesteśmy w stanie obsłużyć. Obie funkcje opiszemy w podrozdz. 5.7. 3. Bezpośrednie wejście-wyjście. Ten typ wejścia-wyjścia jest wspierany przez funkcje fread, f w r i t e . W każdej operacji czytamy lub zapisujemy wskazaną liczbę obiektów o zadanym rozmiarze. Te funkcje są często stosowane do obsługi plików binarnych, gdy odczytujemy lub zapisujemy jakąś strukturę. Opiszemy je w podrozdz. 5.9. Termin bezpośrednie wejście-wyjście pochodzi ze standardu ANSI C. Jest znany pod różnymi nazwami: binarne wejście-wyjście, obiektowe wejście-wyjście, wejście-wyjście nastawione na obsługę rekordów czy struktur.

Funkcje wprowadzania danych S IRUSR | S IWUSR | S IRGRP I S IWGRP | S IROTH | S IWOTH

Domyślnie otwierany strumień jest w pełni buforowany, o ile nie odnosi się do urządzenia terminalowego, wówczas jest buforowany wierszami. Gdy strumień jest już otwarty, wówczas przed wykonaniem pierwszej operacji na nim możemy zmienić typ buforowania za pomocą funkcji setbuf lub setvbuf opisanych w poprzednim podrozdziale. Otwarty strumień zamykamy, wywołując funkcję f c l o s e .

167

Trzy funkcje umożliwiają czytanie po jednym znaku. #include i n t getc(FILE *fp) ; i n t fgetc (FILE *fp) ; i n t getchar(void) ; Przekazują: następny znak, jeśli wszystko w porządku; EOF, jeśli koniec pliku lub błąd

5. Standardowa biblioteka wejścia-wyjścia Funkcja g e t c h a r jest równoważna funkcji g e t c ( s t d i n ) . Różnica między pierwszymi dwiema funkcjami polega na tym, że funkcja g e t c może być zaimplementowana jako makro, natomiast funkcja f g e t c nie może. Wynikają z tego trzy rzeczy: 1. Argument funkcji g e t c nie powinien być wyrażeniem wprowadzającym jakieś efekty uboczne. 2. Ponieważ mamy gwarancję, że f g e t c jest funkcją, więc jest dostępny jej adres. Dzięki temu możemy przekazać adres funkcji f g e t c jako argument innej funkcji. 3. Wywołania f g e t c najprawdopodobniej trwają dłużej niż wywołania g e t c , ponieważ na ogół wywołanie funkcji jest bardziej czasochłonne niż wywołanie makra. Rzeczywiście, analiza pliku nagłówkowego < s t d i o . h > w różnych implementacjach pokazuje, że g e t c jest makrem przygotowanym w celu poprawy wydajności. Wszystkie trzy funkcje przekazują kolejny znak ze strumienia wejściowego jako wartość typu unsigned char skonwertowaną do typu i n t . Wprowadzenie typu bezznakowego zapobiega uzyskaniu wartości ujemnej, gdy jest ustawiony najwyższy bit znaku. Przyczyną wprowadzenia całkowitoliczbowej wartości powrotu jest umożliwienie przekazania wszystkich dopuszczalnych znaków, a także informacji, która świadczy o wystąpieniu błędu lub o napotkaniu znacznika końca pliku. Stała EOF, umieszczona w pliku < s t d i o . h>, musi mieć wartość ujemną. Najczęściej jest równa - 1 . Taka reprezentacja oznacza również, że nie możemy zapamiętać wartości z tych trzech funkcji w zmiennej typu znakowego, by następnie porównywać je ze stałą EOF. Zwróćmy uwagę, że pokazane funkcje przekazują taką samą wartość w przypadku błędu oraz gdy dojdziemy do końca pliku. Aby rozróżnić te dwie informacje, musimy wywołać jedną z funkcji f e r r o r lub feof. #include int ferror (FILE *fp) ; int feof (FILE *fp) ; Przekazują: wartość niezerową (prawda), jeśli warunek jest spełniony; w przeciwnym razie 0 (fałsz) void clearerr(FILE *fp) ;

W większości implementacji dla każdego strumienia w obiekcie FILE są utrzymywane dwa sygnalizatory: • sygnalizator błędu, • sygnalizator końca pliku. Oba sygnalizatory są zerowane za pomocą wywołania funkcji c l e a r e r r .

5.6. Czytanie i zapisywanie danych ze strumienia

169

Po przeczytaniu danych ze strumienia możemy wycofać znaki, wywołując funkcję ungetc. #include int ungetc(int c, FILE *fp) ; Przekazuje: zmiennąc, jeśli wszystko w porządku; EOF, jeśli wystąpił błąd

Znaki, które wycofamy, będą przekazane w następnej operacji odczytu ze strumienia w odwrotnym porządku niż były zwracane. Pamiętajmy jednak, że wprawdzie standard ANSI C dopuszcza zwrócenie dowolnej liczby znaków, ale implementacje są w stanie wycofać tylko jeden znak, nie możemy niestety liczyć na nic więcej. Znaki, które zwrócimy, nie muszą być takie same jak znaki odczytane. Nie możemy zwrócić znaku EOF, ale gdy dojdziemy do końca pliku, możemy zwrócić dowolny znak. W kolejnym odczycie uzyskamy właśnie ten znak, a dopiero następny odczyt przekaże znak EOF. Takie działanie wynika z faktu, że poprawne wywołanie funkcji ungetc usuwa wskazanie końca pliku na strumieniu. Zwracanie znaków jest często stosowane, gdy czytamy strumień wejściowy i dokonujemy podziału danych na słowa lub elementy o zadanej postaci. Nieraz chcemy tylko sprawdzić, jaki jest kolejny znak, aby ocenić, jak obsłużyć bieżący. Łatwo jest wówczas wycofać pobrany znak, aby został następnie przekazany w kolejnym wywołaniu getc. Gdyby standardowa biblioteka wejścia-wyjścia nie miała możliwości wycofania znaku, to musielibyśmy zapamiętać znak we własnej zmiennej, a jakiś dodatkowy sygnalizator wskazywałby, że gdy sięgamy po kolejne dane, mamy użyć tego znaku, zamiast wywoływać g e t c . Funkcje wyprowadzania danych Dla każdej opisanej funkcji wyprowadzania danych możemy znaleźć jej odpowiednik w zestawie funkcji do obsługi danych wyjściowych. #include int

putc (int c, FILE *fp);

int

fputc(int c, FILE *fp);

int

putchar(int c) ; Przekazują: zmienną c, jeśli wszystko w porządku; EOF, jeśli wystąpił błąd

Podobnie jak w funkcjach służących do wprowadzania danych, funkcja p u t c h a r (c) jest tym samym co p u t c ( s t d o u t ) , a p u t c może być zaimplementowanajako makro, podczas gdy fputc musi być funkcją.

5. Standardowa biblioteka wejścia-wyjścia

5.8. Wydajność standardowego wejścia-wyjścia

Wejściewyjście wiersz po wierszu

Funkcja fputs wypisuje na zadanym strumieniu tablicę znaków zakończoną pustym znakiem. Kończący pusty znak nie jest wypisywany. Zauważmy, że funkcja ta nie wymaga, by wyprowadzanie odbywało się wiersz po wierszu, ponieważ tablica znaków nie musi zawierać znaku nowego wiersza jako ostatniego niepustego znaku. Zazwyczaj tak rzeczywiście jest, znak nowego wiersza poprzedza pusty znak, ale nie jest to wymagane. Funkcja p u t s wypisuje na standardowym wyjściu tablicę znaków zakończoną pustym znakiem (bez dołączania pustego znaku), a następnie umieszcza w tym strumieniu znak nowego wiersza. W przeciwieństwie do funkcji g e t s funkcja p u t s nie jest niebezpieczna. Mimo wszystko będziemy unikać stosowania jej, aby nie martwić się, czy znak nowego wiersza został dołączony, czy nie. Decydujemy, że zawsze będziemy używać funkcji fgets oraz fputs, dzięki czemu łatwiej będzie nam pamiętać, by obsługiwać znak nowego wiersza na końcu każdego wiersza.

Poniższe dwie funkcje umożliwiają wprowadzanie danych wierszami. fłinclude char *fgets(char *buf, i n t n, char *gets(char

FILE *fp) ;

*buf) ;

Przekazują: buf, jeśli wszystko w porządku; NULL, jeśli koniec pliku lub błąd

W obu funkcjach jest podany adres bufora, do którego będzie wczytany wiersz. Funkcja g e t s czyta ze standardowego wejścia, podczas gdy f g e t s czyta ze wskazanego strumienia. W funkcji f g e t s musimy podać w argumencie n rozmiar bufora. Funkcja ta czyta dane do znaku nowego wiersza włącznie, ale nie może pobrać do bufora więcej niż n-\ znaków. Dane w buforze są zakończone znakiem pustym. Jeżeli wiersz razem ze znakiem nowego wiersza ma więcej niż n~\ znaków, to jest przekazywana tylko część wiersza, ale bufor jest zawsze zakończony znakiem pustym. Kolejne wywołanie funkcji f g e t s odczyta pozostałą część wiersza. Funkcja g e t s jest wycofywana z użycia. Wywołujący nie może w niej wskazać rozmiaru bufora i stąd wynika problem. Jeżeli wiersz jest dłuższy niż rozmiar bufora, to dochodzi do przepełnienia bufora i zostają zamazane dane umieszczone w pamięci bezpośrednio za buforem. W wydaniu Communication ofthe ACM z czerwca 1989 r. można przeczytać o tym, jakie zagrożenie powstało w Internecie w 1988 r. na skutek tej własności funkcji g e t s . Oprócz tego funkcja g e t s nie zapamiętuje znaku nowego wiersza na końcu bufora, natomiast f g e t s umieszcza ten znak. Różnice między obsługą znaku nowego wiersza przez te dwie funkcje wynikają z ewolucji Uniksa. Nawet podręcznik wersji 7 (1979 r.) stwierdzał, że „gets usuwa znak nowego wiersza, fgets zachowuje go, wszystko dla zachowania zgodności wstecz".

Mimo że standard ANSI C wymaga implementacji g e t s , to funkcja ta nie powinna być stosowana. Wyprowadzanie wiersz po wierszu odbywa się za pomocą funkcji fputs oraz p u t s . łfinclude

i n t fputs (const char *str, FILE *fp) ; i n t p u t s (const char *str) ; Przekazują: wartość nieujcmną, jeśli wszystko w porządku; EOF. jeśli wystąpił błąd

171

5.8

Wydajność standardowego wejścia-wyjścia Oszacujemy teraz wydajność systemu standardowego wejścia-wyjścia, gdy stosujemy funkcje zaprezentowane w poprzednim podrozdziale. Program 5.1 jest podobny do próg. 3.3: kopiuje dane ze standardowego wejścia na standardowe wyjście za pomocą funkcji g e t c oraz putc. Obie procedury mogą być zaimplementowane jako makra.

ttinclude

"ourhdr.h"

int main(void) ( int while ( (c = getc(stdin)) != EOF) if (putc(c, stdout) == EOF) err_sys("output error"); if (ferror(stdin)) err_sys("input e r r o r " ) ; exit(0);

Próg. 5.1 Kopiowanie standardowego wejścia na standardowe wyjście przy użyciu funkcji g e t c i p u t c

Możemy przygotować inną wersję tego programu, korzystającą z funkcji f g e t c oraz f p u t c , które nie mogą być makrami. (Nie pokazujemy tej prostej zmiany w kodzie źródłowym).

5. Standardowa biblioteka wejścia-wyjścia

Mamy również wersję, która czyta i zapisuje wiersze; jest to próg. 5.2. clude

"ourhdr.h"

n (void) char

buf[MAXLINE];

while (fgets(buf, MAXLINE, stdin) != NULL) if (fputs(buf, stdout) == EOF) err__sys ("output error"); if (ferror(stdin)) err_sys("input error"); e x i t (0);

Próg. 5.2

Kopiowanie standardowego wejścia na standardowe wyjście przy użyciu funkcji f g e t s i f p u t s

Zwróćmy uwagę, że ani w próg. 5.1, ani w próg. 5.2 nie zamykamy jawnie standardowego strumienia wejścia-wyjścia. Jesteśmy świadomi, że funkcja e x i t opróżni wszystkie niezapisane dane i dopiero wtedy zamknie otwarte strumienie. (Przedyskutujemy to zagadnienie w podrozdz. 8.5). Interesujące jest porównanie wyników pomiaru czasu w tych programach oraz wyników zamieszczonych w tab. 3.1. W tabeli 5.4 pokazujemy dane uzyskane dla tego samego pliku (1,5 MB, 30 000 wierszy). Tabela 5.4

Wyniki pomiaru czasu przy stosowaniu procedur standardowego wejścia-wyjścia Czas użytkownika procesu (sekundy)

keja

Czas systemowy procesu (sekundy)

Czas zegarowy (sekundy)

Liczba bajtów w tekście programu

epszy wynik z tab. 3.1

0,0

0,3

0,3

itS, f p u t s

2,2

0,3

2,6

184

:c, p u t c

4,3

0,3

4,8

384

itc, f p u t c

4,6

0,3

5,0

152

23,8

397,9

423,4

5 dla pojedynczego bajtu b. 3.1

Dla każdej z trzech wersji standardowego wejścia-wyjścia czas użytkownika procesu jest większy niż najlepsza wersja oparta na funkcji r e a d z tab. 3.1, ponieważ w tym przypadku znakowe wejście-wyjście używa zawsze pętli wykonującej się 30000 razy. W wersji z funkcją read pętla jest realizowana

5.8. Wydajność standardowego wejścia-wyjścia

173

tylko 180 razy (dla bufora o rozmiarze 8192). Wyniki dotyczące czasu użytkownika procesu uwzględniają różnice czasów zegarowych, gdyż systemowe czasy procesu są zawsze takie same. Systemowy czas procesu jest taki sam jak przedtem, ponieważ jądro systemu wykonuje taką samą liczbę zleceń. Widzimy, że dzięki stosowaniu standardowej biblioteki wejścia-wyjścia nie musimy się zajmować alokowaniem bufora oraz doborem optymalnego rozmiaru bufora. Musimy wprawdzie oszacować maksymalny rozmiar wiersza w wersji stosującej funkcję fgets, ale jest to znacznie prostsze niż dobór wielkości bufora dla operacji wejścia-wyjścia. W ostatniej kolumnie tab. 5.4 zamieściliśmy dla każdej funkcji main liczbę bajtów w przestrzeni tekstu programu (tj. rozmiar instrukcji maszynowych wygenerowanych przez kompilator języka C). Możemy zobaczyć, że wersja korzystająca z makra g e t c umieszcza instrukcje makr g e t c i p u t c wewnątrz kodu, a więc uzyskujemy większy rozmiar tekstu programu niż w przypadku wywołań funkcji f g e t c i fputc. Porównanie czasów użytkownika procesu dla wersji z g e t c oraz z f g e t c pokazuje, że w naszym teście nie ma większej różnicy między zastosowaniem makra w programie a wywołaniem w nim funkcji. Wersja stosująca wejście-wyjście wiersz po wierszu jest około dwa razy szybsza niż wersja znak po znaku (zarówno pod względem czasu użytkownika procesu, jak i czasu zegarowego). Jeżeli funkcje f g e t s oraz fputs są zaimplementowane za pomocą g e t c oraz p u t c (zob. podrozdz. 7.7 książki Kernighana i Ritchiego [28]), to spodziewamy się, że wyniki pomiaru czasu będą podobne jak dla wersji z g e t c . Wprawdzie moglibyśmy oczekiwać, że wersja obsługująca wiersze zużyje więcej czasu, ponieważ do istniejących 60 000 wywołań funkcji dodajemy wówczas 3 miliony wywołań makr. W tym przykładzie jednak funkcje do obsługi wiersz po wierszu są zaimplementowane przy użyciu funkcji memccpy(3). Często funkcja memccpy jest napisana dla poprawy wydajności w assemblerze. Interesujące jest, że wyniki dla wersji z funkcją f g e t c są o wiele lepsze od rezultatów dla B U F F S I Z E = 1 W tab. 3.1. Obie wersje testów wykonują taką samą liczbę wywołań funkcji (około 3 miliony), ale wersja z f g e t c zużywa ponad 5 razy mniej czasu użytkownika procesu i około 100 razy mniej czasu zegarowego. Różnica polega na tym, że wersja z wywołaniem funkcji readwykonuje około 3 miliony wywołań funkcji, które z kolei wykonują 3 miliony funkcji systemowych. W przypadku wersji z fgetc wywołujemy funkcje 3 miliony razy, ale oznacza to tylko 360 wywołań funkcji systemowych. Funkcje systemowe są zazwyczaj dużo bardziej kosztowne niż zwykłe wywołania funkcji. Musimy zdawać sobie sprawę z tego, że pokazane rezultaty pomiarów czasu są prawdziwe tylko dla jednego, konkretnego systemu, w którym wykonaliśmy testy. Wyniki zależą od wielu własności implementacji, które nie są takie same w różnych systemach uniksowych. Mimo wszystko, na podsta-

5. Standardowa biblioteka wejścia-wyjścia

wie otrzymanych wyników możemy próbować wyjaśniać różnice między poszczególnymi wersjami, co pomaga w lepszym zrozumieniu systemu. Na podstawie tego podrozdziału oraz podrozdz. 3.9 możemy bez obaw wyciągnąć wniosek, że standardowa biblioteka wejścia-wyjścia nie jest dużo wolniejsza od bezpośrednich wywołań funkcji read oraz w r i t e . Oszacowany na podstawie uzyskanych wyników koszt wynosi 3 sekundy czasu procesora w przypadku kopiowania jednego megabajta danych za pomocą g e t c i putc. Dla wielu bardziej zaawansowanych aplikacji zdecydowana część czasu procesora jest używana przez aplikację, a nie przez standardowe procedury wejścia-wyjścia.

Wejście-wyjście binarne Funkcje, które pokazaliśmy w podrozdz. 5.6 wykonywały operacje znak po znaku lub wiersz po wierszu. Gdy realizujemy wejście-wyjście binarne, to na ogół chcemy od razu przeczytać lub zapisać całą strukturę. Jeśli użyjemy do tego funkcji g e t c lub putc, to musimy powtarzać operacje tyle razy, ile wynosi rozmiar struktury, ponieważ za każdym razem czytamy lub piszemy jeden bajt. Nie możemy zastosować funkcji do obsługi wiersz po wierszu, gdyż funkcja f p u t s kończy zapisywanie, gdy natrafi na pusty bajt, a taką wartość mogą mieć bajty wewnątrz struktury. Podobnie funkcja f g e t s nie będzie poprawnie obsługiwała danych wejściowych, jeśli napotka bajt nowego wiersza lub bajt pusty. Dlatego mamy do dyspozycji dwie poniższe funkcje realizujące binarne operacje wejścia-wyjścia. łłinclude size_t fread(void *ptr, size_t size, size_t nobj, FILE *fp) ;

size_t fwrite(const void *ptr, size_t size, size_t nobj, FILE *fp) ;

Przekazują: liczbę odczytanych lub zapisanych obiektów

Oto dwa najpopularniejsze zastosowania tych funkcji. 1. Odczyt lub zapis tablicy binarnej. Na przykład, aby zapisać elementy od 2 do 5 tablicy liczb zmiennopozycyjnych, możemy napisać: float

data [10];

if (fwrite(Sdata[2], sizeof(float), 4, fp) err_sys("fwrite error");

!= 4)

W argumencie size podaliśmy rozmiar każdego elementu tablicy, a w argumencie nobj liczbę elementów.

5.9. Wejście-wyjście binarne

175

2. Odczyt lub zapis struktury. Możemy na przykład napisać: struct { short count; long total; char name[NAMESIZE] ; } itera; if (fwrite(Sitem, sizeof(item), 1, fp) != 1) err_sys("fwrite error");

W argumencie size podaliśmy rozmiar struktury, a wartość 1 argumentu nobj oznacza liczbę zapisywanych obiektów. Naturalnym uogólnieniem tych dwóch przypadków jest odczyt i zapis tablicy struktur. Argument size powinien być wówczas rozmiarem struktury, a nobj liczbą elementów w tablicy. Obie funkcje, fread i fwrite, przekazują liczbę odczytanych lub zapisanych obiektów. W przypadku odczytu otrzymana wartość jest mniejsza niż argument nobj, jeśli wystąpił błąd lub dotarliśmy do końca pliku. Musimy wówczas wywołać funkcję f e r r o r lub feof. W przypadku zapisu wartość funkcji mniejsza niż wartość podana w argumencie nobj oznacza błąd. Podstawowym problemem w binarnych operacjach wejścia-wyjścia jest wymaganie, by odczytywane dane były zapisane w tym samym systemie. Wiele lat temu to ograniczenie nie stwarzało kłopotu (gdyż wszystkie systemy Unix pracowały na komputerach PDP-11), natomiast obecnie regułą są systemy wieloplatformowe, heterogeniczne, połączone między sobą siecią komputerową. W tej sytuacji należy unikać tych funkcji, gdyż:

1. Pozycja konkretnego pola w strukturze może być różna w zależności od użytego kompilatora oraz systemu (z powodu różnych wymagań dotyczących wyrównania bajtów). W rzeczywistości niektóre kompilatory mają opcję, która pozwala ciasno upakować dane w strukturze (aby zaoszczędzić przestrzeń kosztem spadku wydajności) lub precyzyjnie wyrównać dane, by zoptymalizować dostęp do poszczególnych pól w czasie pracy programu. i 2. Formaty binarne używane do zapamiętywania wielobajtowych liczb całkowitych oraz wartości zmiennopozycyjnych zależą od architek' tury maszyny.

Dobrym rozwiązaniem, stosowanym do wymiany danych binarnych przez różne systemy, jest korzystanie z protokołu wyższego poziomu. Odsyłamy do , podrozdz. 18.2 książki Stevensa, 1995 [44], gdzie jest umieszczony opis nie- j których technik używanych przez różne protokoły sieciowe. Do funkcji f read powrócimy jeszcze w podrozdz. 8.13, kiedy będziemy ; z niej korzystać do odczytania struktury binarnej, zawierającej rekordy służące do rejestrowania zużycia zasobów przez procesy w Uniksie.

5. Standardowa biblioteka wejścia-wyjścia

10

177

5.11. Wejście-wyjście formatowane

Pozycjonowanie strumienia

cję w pliku tekstowym, argument whence musi być równy SEEKSET, a argument offset musi przyjąć jedną z dwóch wartości: 0 (co oznacza przewinięcie pliku do początku) lub wartość uzyskaną z funkcji f t e l l dla tego pliku. Oprócz tego, funkcja rewind może ustawić strumień na początku pliku. Jak już powiedzieliśmy, poniższe dwie funkcje są nowe w standardzie ANSI C.

Są dwie metody, które umożliwiają ustalanie pozycji w standardowym strumieniu wejścia-wyjścia. 1. Funkcje f t e l l i f seek. Funkcje te były już dostępne w wersji 7. Zakładają, że pozycja w pliku jest zapamiętana w długiej liczbie całkowitej . 2. Funkcje fgetpos i f s e t p o s . Są to nowe funkcje w standardzie ANSI C. Wprowadzają nowy abstrakcyjny typ danych, fpos_t, który służy do zapamiętywania pozycji w pliku. W systemach nieuniksowych ten typ danych może mieć dowolny rozmiar odpowiadający konkretnym wymaganiom.

ttinclude int fgetpos (FILE *fp, fpos_t *pos) ; int fsetpos {FILE *fp, const fpos_t *pos) ; Przekazują: 0, jeśli wszystko w porządku; wartość niezerową, jeśli wystąpił błąd

Funkcja fgetpos zapamiętuje bieżącą wartość wskaźnika pozycji w pliku w obiekcie wskazywanym przez argument pos. Ta wartość może zostać później użyta w wywołaniu funkcji f s e t p o s , aby powrócić do poprzedniej pozycji w pliku.

Przenośne aplikacje, które mogą być uruchamiane w systemach nieuniksowych, powinny korzystać z funkcji fgetpos oraz f s e t p o s . #include long ftell (FILE *fp) ; Przekazuje: wskaźnik bieżącej pozycji, jeśli wszystko w porządku; -1L, jeśli wystąpił błąd i n t fseek(FILE *fp,

long offset,

i n t whence);

5.11

Wejście-wyjście formatowane

Wyjście formatowane Do sformatowanego wyprowadzania danych służą trzy funkcje.

Przekazuje: 0, jeśli wszystko w porządku; wartość niezerową, jeśli wystąpił błąd v o i d rewind(FILE *fp) ;

Dla plików binarnych wskaźnik bieżącej pozycji jest wyznaczany jako liczba bajtów od początku pliku. Wartość przekazywana przez funkcję f t e l l dla pliku binarnego jest pozycją bajtową. Aby ustalić pozycję w pliku za pomocą funkcji f seek, musimy podać bajtową wartość argumentu offset oraz sposób interpretacji wskaźnika pozycji. Wartości whence są takie same jak w funkcji l s e e k , pokazanej w podrozdz. 3.6: SEEK_SET oznacza od początku pliku, SEEK_CUR - od bieżącej pozycji, a SEEK_END - od końca pliku. Standard ANSI C nie wymaga, by implementacje stosowały dla plików binarnych specyfikację SEEK_END, ponieważ w niektórych systemach pliki binarne są uzupełniane na końcu bajtami zerowymi, by rozmiar pliku był wielokrotnością jakiejś magicznej liczby. W Uniksie argument SEEKEND jest jednak wspierany dla plików binarnych. W przypadku plików tekstowych wskaźnik bieżącej pozycji nie zawsze może być określany jako prosty wskaźnik bajtowy. Taka sytuacja, tak jak poprzednio, może się przede wszystkim zdarzyć w systemach nieuniksowych, które zapamiętują pliki tekstowe w specyficznym formacie. Aby ustalić pozy-

#include int

printf(const char *format,

int

fprintf(FILE

. . .) ;

*fp, c o n s t c h a r * format, . . .) ;

Przekazują: iczbę wyprowadzonych znaków, jeśli wszystko w porządku; wartość ujemną, jeśli wystąpił błąd int

sprintf(char

*buf, c o n s t c h a r * format, . . .) ; Przekazuje: liczbę znaków zapamiętanych w tablicy

Funkcja p r i n t f pisze na standardowe wyjście, f p r i n t f - do wskazanego strumienia, a s p r i n t f umieszcza dane sformatowane w tablicy buf. Funkcja sprintf automatycznie dołącza na końcu tablicy pusty znak, który nie jest jed- < nak uwzględniany w otrzymanej wartości powrotu z funkcji. W systemie 4.3BSD funkcja s p r i n t f nie ma wartości całkowitoliczbowej, lecz war- i tość typu char *, odpowiadającą pierwszemu argumentowi (wskaźnikowi bufora). i

Zwróćmy uwagę, że funkcja s p r i n t f może spowodować przepełnienie bufora wska- , zywanego przez argument buf. Wywołujący tę funkcję jest odpowiedzialny za zapewnienie właściwego rozmiaru bufora.

5. Standardowa biblioteka wejścia-wyjścia

Nie będziemy tutaj wnikać w żadne szczegóły związane z różnymi możliwymi konwersjami danych w formatach, które oferują te trzy funkcje. Zainteresowanych odsyłamy do systemowego podręcznika Uniksa oraz dodatku B w książce Kernighana i Ritchiego [28]. Poniższe trzy warianty funkcji p r i n t f są bardzo podobne do poprzednich, ale zmienna lista argumentów (...) jest w nich zastąpiona argumentem arg.

179

5.12. Szczegóły implementacyjne

5.12 Szczegóły implementacyjne Wspominaliśmy już, że w systemie Unix korzystanie ze standardowej biblioteki wejścia-wyjścia prowadzi do wywołania procedur obsługi wejścia-wyjścia, które opisaliśmy w rozdz. 3. Każdy standardowy strumień wejścia-wyjścia ma związany ze sobą deskryptor pliku, możemy go otrzymać, wywołując funkcję f ileno.

#include #include

#include

int vprintf (const char *format, va_list arg) ;

int

int vf printf (FILE *fp, const char *format, va_list arg);

Przekazuje deskryptor pliku związanego ze strumieniem

Przekazują: liczbę wyprowadzonych znaków, jeśli wszystko w porządku; wartość ujemną, jeśli wystąpił błąd int vsprintf (char *buf, const char *format, va_list arg) ; Przekazuje: liczbę znaków zapamiętanych w tablicy

Właśnie funkcji v s p r i n t f używamy do obsługi błędów w naszych procedurach, które prezentujemy w dodatku B. Szczegóły na temat obsługi listy argumentów o zmiennym rozmiarze w standardzie ANSI C można znaleźć w podrozdz. 7.3 wspomnianej książki [28]. Zwróćmy uwagę, że procedury obsługi listy argumentów o zmiennym rozmiarze w standardzie ANSI C (plik nagłówkowy < s t d a r g . h > oraz związane z nim funkcje) są inne niż procedury zdefiniowane w pliku nagłówkowym , udostępniane przez systemy SVR3 (i wcześniejsze) oraz 4.3BSD. matowane wprowadzanie danych Do obsługi formatowanego wprowadzania danych służą funkcje serii scanf. #include int

scanf (const

char

i n t f s c a n f ( F I L E *fp,

* format,

...);

c o n s t c h a r * format,

i n t s s c a n f ( c h a r *buf, c o n s t c h a r *format,

...); . . .) ;

Przekazują: liczbę przypisanych danych wejściowych, jeśli wszystko w porządku; EOF, jeśli wystąpił błąd wprowadzania lub wykryto znak końca pliku przed jakąkolwiek konwersją

Tak jak dla funkcji serii p r i n t f , opis wszystkich szczegółów oraz różnych opcji formatowania jest dostępny w podręczniku systemowym.

fileno(FILE *fp) '

Ta funkcja jest potrzebna, m.in. gdy chcemy wywołać funkcje dup lub fcntl.

Gdy chcemy przyjrzeć się implementacji standardowej biblioteki wejścia-wyjścia w konkretnym systemie, zawsze zaczynajmy od pliku nagłówkowego < s t d i o . h > . Tam znajdziemy definicje obiektu FILE, sygnalizatorów stosowanych dla strumieni oraz sprawdzimy, które procedury wejścia-wyjścia są makrami (np. getc). W podrozdziale 8.5 książki Kernighana i Ritchiego [28] zamieszczono przykładową implementację, którą można uznać za wzorcową dla wielu systemów. W rozdziale 12 książki Plaugera, 1992 [38] jest zawarty kompletny kod źródłowy implementacji standardowej biblioteki wejścia-wyjścia. Również implementacja standardowej biblioteki wejścia-wyjścia w systemie 4.3+BSD (autorstwa Chrisa Toreka) jest ogólnodostępna.

Przykład Program 5.3 drukuje typy buforowania dla trzech standardowych strumieni oraz dla strumienia związanego ze zwykłym plikiem. Zwróćmy uwagę, że najpierw wykonujemy operację wejścia-wyjścia na każdym strumieniu, dopiero potem drukujemy stan buforowania, ponieważ na ogół dopiero pierwsza operacja powoduje przypisanie bufora do strumienia. Pola struktury o nazwach _ f l a g i _bufsiz oraz stałe _IONBF i _IOLBF były zdefiniowane w systemie używanym przez autora. Pamiętajmy jednak, że inne systemy" uniksowe mogą korzystać z odmiennej implementacji standardowej biblioteki wejścia-wyjścia. ttinclude "ourhdr.h" void pr_stdio(const char *, FILE int main(void) { FILE *fp;

5. Standardowa biblioteka wejścia-wyjścia fputs("enter any character\n", stdout); if (getcharO == EOF) err_sys("getchar error"); fputs("one linę to standard error\n", stderr) pr_stdio("stdin", stdin); pr_stdio("stdout", stdout); pr_stdio("stderr", stderr); if ( (fp = fopen("/etc/motd" err sys("fopen error"); if (getc(fp) == EOF) err_sys("getc error"); pr_stdio("/etc/motd", fp) ; exit (0);

"r")) == NULL)

stdio(const char *name, FILE *fp) printf("stream = %s, ", name); /* poniższe podejście nie jest przenośne */ if (fp->_flag & _IONBF) printf("unbuffered"); else if (fp->_flag & _IOLBF) printf("linę buffered"); else /* jeśli nie został spełniony żaden z poprzednich warunków */ printf("fully buffered"); printf(", buffer size = %d\n", fp->_bufsiz);

Próg. 5.3 Buforowanie wydruków dla różnych strumieni standardowego wejścia-wyjścia

Jeżeli uruchomimy dwukrotnie próg. 5.3, raz, gdy trzy standardowe strumienie są związane z terminalem, i drugi raz, gdy trzy standardowe strumienie są przekierowane do plików, to otrzymamy następujący wynik: $ a.out stdin, stdout i stderr są połączone z terminalem enter any character piszemy znak nowego wiersza one linę to standard error stream = stdin, linę buffered, buffer size = 128 stream = stdout, linę buffered, buffer size = 128 stream = stderr, unbuffered, buffer size = 8 stream = /etc/motd, fully buffered, buffer size = 8192 $ a.out < /etc/termcap > std.out 2> std.err uruchamiamy program ponownie, przekierowujemy trzy strumienie $ cat std.err one linę to standard error $ cat std.out enter any character stream = stdin, fully buffered, buffer size = 8192 stream = stdout, fully buffered, buffer size = 8192 stream = stderr, unbuffered, buffer size = 8 stream = /etc/motd, fully buffered, buffer size = 8192

181

5.13. Pliki tymczasowe

Widzimy, że w tym systemie domyślnie standardowe wejście i standardowe wyjście są buforowane wierszami, gdy są powiązane z terminalem. Bufor wierszy ma rozmiar 128 bajtów. Nie oznacza to, że musimy zawsze wprowadzać i wyprowadzać wiersze krótsze niż 128 bajtów, a jedynie, że taki jest rozmiar bufora. Zapisanie 512-bajtowego wiersza wymaga czterech wywołań funkcji systemowych w r i t e . Gdy przekierujemy oba strumienie standardowe do zwykłych plików, to stają się one w pełni buforowane, a rozmiar bufora jest równy zalecanej wielkości bloku w operacjach wejścia-wyjścia (pole s t b l k s i z e w strukturze s t a t ) dla danego systemu plików. Jak pokazują wyniki, standardowy strumień komunikatów jest zawsze niebuforowany (tak właśnie powinno być), a dla zwykłych plików domyślnie obowiązuje pełne buforowanie. •

5.13 Pliki tymczasowe W standardowej bibliotece wejścia/wyjścia znajdziemy dwie funkcje, które są pomocne przy tworzeniu plików tymczasowych. #include char *tmpnam(char *ptr) ; Przekazuje: wskaźnik do unikatowej nazwy ścieżki FILE * t m p f i l e ( v o i d ) ; Przekazuje: wskaźnik pliku, jeśli wszystko w porządku; NULL, jeśli wystąpił błąd

Funkcja tmpnam generuje poprawną nazwę ścieżki, która nie odpowiada żadnemu istniejącemu plikowi. Jej kolejne wywołania, do TMP_MAX razy, tworzą za każdym razem inną nazwę ścieżki. Stała TMP_MAX jest zdefiniowana w pliku nagłówkowym < s t d i o . h>. Mimo że stała TMP_MAX jest zdefiniowana w ANSI C, standardowe języki C wymagają jedynie, by jej wartość była nie mniejsza niż 25. Za to w standardzie XPG3 stała ta nie może być mniejsza niż 10000. Chociaż te wartości pozwalają implementacjom stosować wyłącznie cyfry do tworzenia nazw (od 0000 do 9999), to jednak większość systemów Unix używa do tego celu małych i wielkich liter.

Jeżeli argument ptr jest równy NULL, to wygenerowana nazwa ścieżki jest zapamiętywana w statycznym obszarze, a funkcja przekazuje wskaźnik do tego miejsca w pamięci. Kolejne wywołania funkcji tmpnam mogą zamazać zawartość statycznej pamięci. (Oznacza to, że jeśli wywołamy funkcję tmpnam więcej niż jeden raz, to musimy zachować kopię nazwy ścieżki, a nie kopię wskaźnika do miejsca pamięci). Gdy argument ptr nie jest równy NULL, to zakłada się, że wskazuje na tablicę liczącą co najmiej Ltmpnam znaków. (Stała L_tmpnam jest zdefiniowana w pliku < s t d i o . h>). Wygenerowana nazwa ścieżki jest umieszczana w tej tablicy, a p t r jest wartością tej funkcji.

5. Standardowa biblioteka wejścia-wyjścia

Funkcja tempnam jest odmianą funkcji tmpnam, w której wywołujący określa zarówno katalog, jak i przedrostek tworzonej nazwy ścieżki.

Funkcja tmpf i l e tworzy binarny plik tymczasowy (typ wb+), który jest automatycznie usuwany podczas zamykania go lub gdy skończy się program. To, że plik jest binarny, nie ma żadnego znaczenia w Uniksie.

#include char *tempnam (const char *directory, const char *prefix) ;

rkład

Przekazuje: unikatową nazwę ścieżki

Zastosowanie obu funkcji widzimy w próg. 5.4. Wykonując go, otrzymujemy

Aby wskazać nazwę katalogu, mamy do wyboru cztery możliwości, przy czym obowiązuje pierwszy spełniony warunek.

$ a.out /usr/tmp/aaaa004 70 /usr/tmp/baaa004 70 one linę of output

1. Jeżeli jest zdefiniowana zmienna środowiskowa TMPDIR, to jej wartość jest nazwą katalogu. (Zmienne środowiskowe opiszemy w podrozdz. 7.9). 2. Jeżeli argument directory jest różny od NULL, to jest on nazwą katalogu. 3. Nazwą katalogu jest napis zdefiniowany jako wartość stałej P_tmpdir w pliku < s t d i o . h>. 4. Lokalny katalog, zazwyczaj /tmp jest używany jako katalog, w którym będzie tworzony tymczasowy plik.

Pięciocyfrowy przyrostek dodany do każdej nazwy pliku tymczasowego jest identyfikatorem procesu. Dzięki temu wygenerowane nazwy ścieżek są unikatowe w każdym procesie, który może wywołać funkcję tmpnam. • ;lude

"ourhdr.h"

Jeżeli argument prefu nie jest równy NULL, to powinien być najwyżej 5-znakowym napisem, stosowanym jako przedrostek nazwy pliku. Funkcja ta wywołuje funkcję malloc, aby przydzielić dynamiczną pamięć dla konstruowanej nazwy ścieżki. Możemy zwolnić tę pamięć (free), gdy nazwa ścieżki nie będzie już nam potrzebna. (W podrozdziale 7.8 opiszemy funkcje malloc oraz free).

i (void) char FILE

183

5.13. Pliki tymczasowe

name[L_tmpnam], linę[MAXLINE]; *fp;

printf("%s\n", tmpnam(NULL));

/* pierwsza nazwa pliku tymczasowego */

tmpnam(name); printf("%s\n", name);

/* drugi plik tymczasowy */

if ( (fp = tmpfileO) == NULL) /* tworzymy plik tymczasowy */ err_sys("tmpfile error"); fputsfone linę of output \n", fp) ; /* zapisujemy do pliku tymczasowego */ rewind(fp); /* ponowne odczytanie */ if (fgetsdine, sizeof (linę) , fp) == NULL) err_sys("fgets error"); fputs(line, stdout); /* drukujemy wcześniej zapisany wiersz */ exit(0);

Funkcja tempnam nie jest zdefiniowana w standardach POSIX.l i ANSI C, ale jest częścią normy XPG3. Implementacje, które opisaliśmy, są zgodne z systemami SVR4 oraz 4.3+BSD. Wersja XPG3 jest podobna, ale nie jest w niej zdefiniowana zmienna środowiskowa TMPDIR.

Przykład Zastosowanie funkcji tempnam widzimy w próg. 5.5. tfinclude

"ourhdr.h"

int

Próg. 5.4 Działanie funkcji tmpnam i tmpf i l e

Standardową techniką, często stosowaną w Uniksie przez funkcję tmpf i l e , jest utworzenie unikatowej nazwy ścieżki przez wywołanie tmpnam, a następnie utworzenie pliku i zaraz potem wywołanie funkcji u n l i n k dla tej nazwy. Przypominamy, że zgodnie z podrozdz. 4.15 wykonanie funkcji u n l i n k nie usuwa pliku, dopóki nie zostanie on zamknięty. Dzięki tej metodzie, gdy zamykamy plik albo jawnie, albo w efekcie zakończenia procesu, następuje usunięcie jego zawartości.

main(int argc,

char *argv[])

{

if (argc != 3) err_quit("usage:

a.out ");

p r i n t f ( " % s \ n " , tempnam( argv[l][0] != argv[2][0] != e x i t (0) ;

? argv[l] : NULL, ? argv[2] : NULL) );

Próg. 5.5 Działanie funkcji tempnam

5. Standardowa biblioteka wejścia-wyjścia

5.15. Podsumowanie

Pamiętajmy, że jeśli jeden z argumentów wiersza poleceń (katalog lub przedrostek) zaczyna się od pustego znaku, to do funkcji przekazujemy pusty wskaźnik. Pokażemy teraz różne sposoby wywołania naszego programu. $ a.out /home/stevens TEMP /home/stevens/TEMPAAAa00571 $ a.out " " PFX /usr/tmp/PFXAAAa0057 2 $ TMPDIR=/tmp a.out /usr/tmp " "

godzić ten problem, stosując funkcję, która czyta wiersz i przekazuje wskaźnik do niego, zamiast kopiować ten wiersz do dodatkowego bufora. Raport przedstawiony w artykule Hume'a, 1988 [25] stwierdza trzykrotny wzrost szybkości programu pomocniczego grep(l), uzyskany dzięki wprowadzeniu takiej zmiany. W pracy Korna i Vo, 1991 [30] opisano inną technikę zastępującą standardową bibliotekę wejścia-wyjścia: pakiet sfw. Ten pakiet daje podobne wyniki jak biblioteka fio i są one na ogół lepsze od rezultatów uzyskiwanych, gdy stosujemy standardową bibliotekę wejścia-wyjścia. Pakiet sfw ma też nowe właściwości, których nie było w innych rozwiązaniach: strumienie wejścia-wyjścia zostały uogólnione i mogą reprezentować zarówno pliki, jak i obszary pamięci, można przygotować moduły przetwarzające nadbudowane na typowych strumieniach wejścia-wyjścia, które zmieniają standardowe operacje na tych strumieniach oraz lepiej obsługuje wszelkie sytuacje wyjątkowe. Krieger, Stumm i Unrau, 1992 [31] opisali kolejną alternatywę standardowej biblioteki wejścia-wyjścia, stosującą odwzorowanie plików - funkcję mmap, którą opiszemy w podrozdz. 12.9. Nowy pakiet nazwano ASI {Alloc Stream Interface). Interfejs programowania przypomina funkcje alokowania pamięci (malloc, r e a l l o c oraz free, opisane w podrozdz. 7.8). Tak samo jak pakiet sflo, pakiet ASI korzysta ze wskaźników, by minimalizować liczbę operacji kopiowania.

podajemy katalog i przedrostek używamy domyślnego katalogu: P_tmpdir

używamy zmiennej środowiskowej; nie podajemy przedrostka I tmp /AAAa 0057 3 zmienna środowiskowa ma wyższy priorytet niż podany katalog $ TMPDIR=/no/such/dir a.out /tmp QQQQ /tmp/QQQQAAAa0057 4 niepoprawna nazwa katalogu jest ignorowana $ TMPDIR=/no/such/file a.out /etc/uucp MMMMM / u s r /tmp/MMMMMAAAa0057 5 niepoprawna zmienna środowiskowa; niepoprawny katalog; obie wartości są ignorowane Jak pokazują wyniki, funkcja, aby określić nazwę katalogu, realizuje w ustalonej kolejności wymienione wcześniej cztery kroki, a jednocześnie sprawdza, czy nazwa katalogu ma sens. Jeżeli wybrany katalog nie istnieje (przykład z katalogiem / n o / s u c h / d i r ) lub gdy nie mamy prawa zapisu dla tego katalogu (przykład z katalogiem /etc/uucp), to dany krok jest omijany i jest analizowany kolejny warunek. Zaprezentowany przykład pokazuje, jaka jest rola identyfikatora procesu przy tworzeniu nazwy ścieżki, widzimy też, że w tej konkretnej implementacji stała P_tmpdir ma wartość /usr/tmp. Technika, której użyliśmy do ustawienia zmiennej środowiskowej, polegająca na podaniu formuły TMPDIR= przed nazwą programu, jest stosowana w powłoce Bourne'a oraz KornShellu. •

Techniki zastępujące standardowe wejście-wyjście Standardowa biblioteka wejścia-wyjścia nie jest idealna. W pracy Korna i Vo, 1991 [30] wymieniono wiele jej wad - niektóre dotyczą podstawowej koncepcji, ale większość jest związana z rozmaitymi implementacjami. Istotną cechą nieefektywną, mającą wpływ na wydajność, jest liczba operacji kopiowania danych. Gdy korzystamy z funkcji f g e t s oraz fputs do obsługi wejścia-wyjścia wiersz po wierszu, dane są zazwyczaj kopiowane dwukrotnie: raz między jądrem systemu a buforem standardowego wejścia-wyjścia (gdy wywołamy funkcję read lub write), drugi raz między buforem standardowego wejścia-wyjścia a naszym buforem wiersza. Biblioteka o nazwie Fast I/O (fio(3) w dokumentacji AT&T), 1990a [9] próbuje zała-

185

5.15

Podsumowanie Ze standardowej biblioteki wejścia-wyjścia korzysta większość aplikacji w Uniksie. Przyjrzeliśmy się wszystkim funkcjom dostarczanym przez tę bibliotekę, przeanalizowaliśmy pewne szczegóły implementacyjne oraz kwestie wydajnościowe. Zapamiętajmy, że biblioteka ta realizuje buforowanie, gdyż to właśnie jest źródłem licznych problemów i pomyłek.

Ćwiczenia 5.1

Zaimplementuj funkcję setbuf na podstawie funkcji setvbuf.

5.2

Przygotuj program, który kopiuje plik za pomocą operacji wejścia-wyjścia wiersz po wierszu (fgets i fputs), opisanych w podrozdz. 5.8, ale zastosuj wartość MAXLINE równą 4. Co się dzieje, gdy kopiujesz wiersze o długości większej niż 4 znaki. Wyjaśnij to.

5.3

Co oznacza przekazanie wartości 0 przez funkcję p r i n t f ?

5.4

Poniższy kod działa poprawnie na niektórych komputerach, a na innych daje błędne wyniki. Gdzie leży problem?

5. Standardowa biblioteka wejścia-wyjścia

6

#include int main(void) { char c; while ( (c = getcharl putchar(c);

Systemowe pliki danych

! = EOF )

5.5

Dlaczego w funkcji tempnam przedrostek może mieć najwyżej pięć znaków?

5.6

Jak można zastosować funkcję f sync (podrozdz. 4.24) ze standardowym strumieniem wejścia-wyjścia?

5.7

W programach 1.5 i 1.8 drukowana sekwencja zachęty nie zawiera znaku nowego wiersza, a także nie wywołujemy w nich funkcji f f lush. Dlaczego mimo to sekwencja ta jest drukowana?

6.1

Wprowadzenie Do poprawnej pracy systemu są wymagane liczne pliki danych. Przykładowo pliki haseł (/etc/passwd) oraz grup (/etc/group) są bardzo często używane przez różne programy. Plik haseł jest używany przy każdym załogowaniu użytkownika w systemie uniksowym, a także za każdym razem, gdy jest wykonywane polecenie ls - 1 . Początkowo były to pliki tekstowe, zakodowane w standardzie ASCII, a do ich odczytu stosowano standardową bibliotekę wejścia-wyjścia. Niestety, w przypadku dużych plików sekwencyjne przeglądanie pliku haseł jest bardzo czasochłonne. Przydatna byłaby możliwość zapamiętywania takich plików danych w formacie różnym od kodowania ASCII i korzystania z nich w programach użytkowych za pomocą interfejsu, który będzie działał z każdym rodzajem plików. Takie przenośne interfejsy do plików danych są tematem tego rozdziału. Omówimy też funkcje służące do identyfikowania systemu oraz funkcje do obsługi czasu i daty.

6.2

Plik haseł Uniksowy plik haseł, nazwany w normie POSIX.l bazą danych użytkowników, zawiera pola pokazane w tab. 6.1. Pola te są również składowymi struktury passwd zdefiniowanej w pliku nagłówkowym . Zwróćmy uwagę, że POSIX.l określa jedynie pięć z siedmiu pól struktury p a s s w d . Pozostałe dwa występująjednak w systemach SVR4 i 4.3+BSD. Tradycyjnie plik haseł był identyfikowany nazwą ścieżki /etc/passwd i stosował kodowanie ASCII. Każdy wiersz zawiera siedem pól opisanych

6. Systemowe pliki danych Tabela 6.1 Pola w pliku/etc/passwd Opis

pole struct passwd

nazwa użytkownika

char * pw

name passwd

POSDC.l



zaszyfrowane hasło

char * pw

numeryczny identyfikator użytkownika

gid t

pw uid



numeryczny identyfikator grupy

uid_t

pw gid



pole komentarza

char * pw

gecos

startowy katalog roboczy

char * pw

dir



startowa powłoka

char * pw

shell



wtab. 6.1, które są oddzielone dwukropkami. Oto przykładowe trzy wiersze z takiego pliku: root:jheVopR58x9Fx:0:1:The superuser:/:/bin/sh nobody:*:65534:65534::/: stevens:3hKVD8R58r9Fx:224:20:Richard Stevens:/home/stevens:/bin/ksh

Zwróćmy uwagę na parę szczegółów charakterystycznych dla pokazanych wpisów. • Zazwyczaj w pliku haseł jest wpis dla użytkownika o nazwie r o o t . Ma on identyfikator użytkownika równy 0 (nadzorca). • Zaszyfrowane pole hasła zawiera kopię hasła użytkownika, wygenerowaną przez jednokierunkowy algorytm szyfrowania. Ponieważ jest to algorytm jednokierunkowy, więc nie możemy odgadnąć na podstawie wersji zaszyfrowanej, jakie jest oryginalne hasło. Obecnie używany algorytm (patrz Morris i Thompson, 1979 [35]) zawsze generuje 13 drukowalnych znaków, należących do 64-znakowego zbioru [a-zA-ZO-9 . / ] . Wpis dla użytkownika nobody zawiera jeden znak, więc zaszyfrowane hasło nigdy nie zostanie dopasowane do tego wzorca. Taka nazwa użytkownika bywa używana przez serwery sieciowe, które wprawdzie umożliwiają nam logowanie w systemie, ale uzyskujemy identyfikator użytkownika i identyfikator grupy nie dający nam żadnych uprawnień. Możemy więc korzystać wyłącznie z tych plików w systemie, które mają ustalone prawo odczytu lub zapisu dla wszystkich. (Zakładamy, że nie ma w systemie plików, których właściciel ma identyfikator użytkownika równy 65534 i identyfikator grupy równy 65534 - zazwyczaj tak właśnie jest). Dalej w tym rozdziale omówimy ostatnio wprowadzone zmiany w pliku haseł (czyli maskowanie haseł). • Niektóre pola w pliku haseł mogą być puste. Puste pole zaszyfrowanego hasła na ogół oznacza, że użytkownik nie ma hasła. (Nie zalecamy tego). Wpis dla użytkownika nobody ma dwa puste pola: komentarza oraz powłoki startowej. Brak komentarza nie ma znaczenia. Gdy pole powłoki jest puste, to jest przyjmowana wartość domyślna, czyli /bin/sh.

189

6.2. Plik haseł

• Niektóre systemy Unix udostępniające polecenie f i n g e r ( l ) umożliwiają wprowadzanie w polu komentarza dodatkowych informacji. Poszczególne składowe, oddzielane przecinkami to przykładowo: imię i nazwisko użytkownika, nazwa firmy, telefon służbowy i domowy. Dodatkowo, jeśli w polu komentarza pojawi się znak ampersanda (&), to niektóre programy pomocnicze zastąpią go nazwą użytkownika (pisaną wielką literą). Możemy np. umieścić wpis: stevens:3hKVD8R58r9Fx:224:20:Richard S, B232, 555-1111, 555-2222 :/home/stevens:/bin/ksh

Nawet gdy w konkretnym systemie nie ma polecenia f inger, możemy umieszczać w pliku haseł te dodatkowe informacje, ponieważ są one zawarte w polu komentarza, a programy systemowe go nie interpretują. POSIX.l definiuje tylko dwie funkcje pobierające wpisy z pliku haseł. Umożliwiają one wyszukiwanie wpisu na podstawie zadanej nazwy użytkownika lub zadanego numerycznego identyfikatora użytkownika. łfinclude łfinclude s t r u c t passwd *getpwuid(uid_t uid); s t r u c t passwd *getpwnam(const char *name); Przekazują: wskaźnik, jeśli wszystko w porządku; NULL, jeśli wystąpił błąd

Funkcja getpwuid jest stosowana przez program l s ( l ) do odwzorowania numerycznego identyfikatora użytkownika, zawartego w i-węźle na nazwę użytkownika w systemie. Funkcja getpwnam jest używana przez program l o g i n ( l ) , gdy wprowadzamy do systemu swoją nazwę. Obie funkcje przekazują wskaźnik do wypełnionej przez siebie struktury passwd. Struktura ta jest na ogół zmienną z modyfikatorem s t a t i c , a więc jej zawartość jest zamazywana, gdy kolejny raz wywołujemy jedną z tych funkcji. Pokazane dwie funkcje zgodne z normą POSIX.l wystarczają, gdy chodzi nam o znalezienie wpisu na podstawie podanej nazwy użytkownika lub jego identyfikatora. Zdarza się jednak, że chcemy przejrzeć cały plik haseł. Służą do tego trzy funkcje. #include #include struct passwd *getpwent(void) ; Przekazuje: wskaźnik, jeśli wszystko w porządku; NULL, jeśli wystąpił błąd lub koniec pliku void setpwent(void); void endpwent(void);

6. Systemowe pliki danych Mimo że nie są częścią POSDC1, te trzy funkcje występują w systemach SVR4 i 4.3+BSD.

6.3

Aby pobrać kolejny wpis z pliku haseł, wywołujemy funkcję getpwent. Podobnie jak dwie funkcje zgodne z normą POSDC.l, getpwent przekazuje wskaźnik do wypełnionej przez siebie struktury passwd. Struktura ta jest zazwyczaj wypełniana od nowa przy każdym wywołaniu funkcji. Gdy pierwszy raz wywołujemy funkcję getpwent, wówczas są otwierane wszystkie potrzebne pliki. Nie obowiązuje żadne narzucone przez tę funkcję uporządkowanie pobieranych wpisów - ukazują się one w dowolnej kolejności. (Wynika to z faktu, że niektóre systemy stosują wersję pliku /etc/passwd poddaną działaniu algorytmu skrótu). Funkcja setpwent przewija wszystkie używane pliki do początku, a funkcja endpwent zamyka te pliki. Gdy korzystamy z funkcji getpwent, powinniśmy zawsze po zakończeniu działań upewnić się za pomocą funkcji endpwent, czy zamknęliśmy używane pliki. Funkcja getpwent wprawdzie wie, kiedy musi otworzyć pliki (gdy jest wywoływana po raz pierwszy), ale nie jest w stanie zgadnąć, że zakończyliśmy działania.

Aby było trudniej uzyskać dane w postaci zaszyfrowanych haseł, niektóre systemy operacyjne zapamiętują zaszyfrowane hasła w innym pliku, nazywanym plikiem maskowanych haseł (shadow password file). Taki plik zawiera co najmniej nazwę użytkownika i zaszyfrowane hasło. Można też w nim umieścić inne dane związane z hasłem. Przykładowo, w niektórych systemach stosujących maskowanie haseł wymaga się, by użytkownik zmieniał hasło co pewien czas. Nazywa się to przedawnianiem haseł, a czas między kolejnymi zmianami hasła jest zapisany właśnie w pliku maskowanych haseł.

Program 6.1 jest implementacją funkcji getpwnam.



zt passwd * wnam(const char *name)

W systemie SVR4 plik maskowanych haseł jest nazywany /etc/shadow. System 4.3+BSD przechowuje zaszyfrowane hasła w pliku / e t c / m a s t e r .passwd.

struct passwd *ptr;

Plik maskowanych haseł nie może mieć prawa odczytu przez wszystkich. Tylko nielicznym programom jest potrzebny dostęp do zaszyfrowanych haseł, przykładami są polecenia l o g i n ( l ) oraz passwd(l). Zazwyczaj programy te używają bitu ustanowienia identyfikatora użytkownika dla nadzorcy systemu ( r o o t ) . Jeżeli mamy plik zamaskowanych haseł, to wszyscy mogą mieć prawo odczytu pliku /etc/passwd.

setpwent(); while ( (ptr = getpwent()) != NULL) { if (strcmp(name, ptr->pw_name) == 0)

break;

endpwent(); return(ptr);

Maskowanie haseł W poprzednim podrozdziale wspomnieliśmy, że stosowany powszechnie w hasłach uniksowych algorytm szyfrowania jest jednokierunkowy. Mając zaszyfrowane hasło nie możemy zastosować żadnego algorytmu, który wygeneruje na jego podstawie hasło w postaci tekstowej. (Tekstowa postać hasła to po prostu ciąg znaków, jaki podajemy w odpowiedzi na sekwencję zachęty Password:). Możemy jednak próbować odgadnąć hasło, poddać je działaniu algorytmu jednokierunkowego szyfrowania, a następnie porównać wynik z hasłem zaszyfrowanym. Jeżeli użytkownicy korzystaliby z losowo wygenerowanych haseł, to taki brutalny atak nie dawałby żadnych szans. Jednak zazwyczaj stosują nielosowe hasła (imię współmałżonka, nazwa ulicy, zdrobnienie imienia itp.). Często zdarzają się ataki polegające na pobraniu kopii pliku haseł i podjęciu prób odgadnięcia haseł. (W rozdziale 2 książki Garfinkela i Spafforda, 1991 [21] Czytelnik znajdzie więcej szczegółów oraz informacje na temat historii haseł w Uniksie i schematu ich szyfrowania).

kład

lude ludę lude lude

191

6.4. Plik grup

/* nazwa została dopasowana */

/* jeśli nie udało się dopasować nazwy, to zmienna ptr jest równa NULL */

Próg. 6.1 Funkcja getpwnam

Dla pewności wywołujemy na początku funkcję setpwent, by zagwarantować, że zaczynamy sprawdzanie od początku pliku niezależnie od tego, czy wcześniej wywołano funkcję getpwent. Na koniec wywołujemy funkcję endpwent, gdyż funkcje getpwnam i g e t p u i d nie powinny pozostawiać otwartych plików. •

6.4

Plik grup Uniksowy plik grup, nazywany w normie POSIX.l bazą danych grup, zawiera pola, które pokazujemy w tab. 6.2. Pola te są również składowymi struktury typu group, zdefiniowanej w pliku nagłówkowym . Standard POSDC.l definiuje tylko trzy z czterech pól. Pole o nazwie gr_passwd jest jednak stosowane przez systemy SVR4 oraz 4.3+BSD.

6. Systemowe pliki danych

6.5

Tabela 6.2 Pola w pliku /etc/group pole typu danych

Opis

struct group

POSDC.l •

nazwa grupy

char

* gr name

zaszyfrowane hasło

char

* gr passwd

numeryczny identyfikator grupy

int

gr gid



tablica wskaźników do kolejnych nazw użytkowników

char

* *gr mem



Pole gr_mem jest tablicą wskaźników do nazw tych użytkowników, którzy należą do tej grupy. Tablica jest zakończona pustym wskaźnikiem. Stosując poniższe dwie funkcje, zdefiniowane w normie POSIX.l, możemy przeszukiwać ten plik na podstawie zadanej nazwy grupy lub jej numerycznego identyfikatora. #include #include struct group *getgrgid(gid_t gid); struct group *getgrnam(const char *name); Przekazują: wskaźnik, jeśli wszystko w porządku; NULL, jeśli wystąpił błąd

Tak jak w przypadku funkcji obsługujących plik haseł, obie te funkcje przekazują wskaźniki do zmiennej statycznej ( s t a t i c ) , która na ogół jest zamazywana przy każdym wywołaniu funkcji. Jeżeli chcemy przeszukiwać cały plik grup, to potrzebujemy innych funkcji. Kolejne trzy, pokazane niżej są odpowiednikami funkcji do obsługi plików. #include #include struct group *getgrent(void); Przekazuje : wskaźnik, jeśli wszystko w porządku; NULL, jeśli wystąpił błąd lub koniec pliku

193

6.5. Identyfikatory dodatkowych grup

Identyfikatory dodatkowych grup Z biegiem lat zmieniało się korzystanie z grup w Uniksie. W wersji 7 każdy użytkownik należał w danej chwili do jednej grupy. W czasie logowania w systemie użytkownik otrzymywał rzeczywisty identyfikator grupy, zgodny z numerycznym identyfikatorem grupy, umieszczonym we wpisie tego użytkownika w pliku haseł. Można było zmienić przynależność do grupy, wykonując polecenie newgrp(l). Jeśli polecenie newgrp powiodło się (odsyłamy do opisu reguł dotyczących praw dostępu w podręczniku systemowym), to rzeczywisty identyfikator użytkownika uzyskiwał wartość identyfikatora nowej grupy i z takiego ustawienia korzystały wszystkie kolejne sprawdzenia praw dostępu do plików. Oczywiście, zawsze mogliśmy powrócić do oryginalnej wartości identyfikatora grupy, wywołując polecenie newgrp bez żadnych argumentów. Taka postać członkostwa w grupie obowiązywała, dopóki nie zaszły zmiany w wersji 4.2BSD (około 1983 r.). Wprowadzono wówczas koncepcję identyfikatorów dodatkowych grup. Użytkownik poza przynależnością do grupy, która jest wskazana w jego wpisie w pliku haseł, może być członkiem 16 grup dodatkowych. Zmodyfikowano sposób sprawdzenia praw dostępu do plików i obecnie z identyfikatorem grupy pliku jest porównywany nie tylko obowiązujący identyfikator grupy, lecz również wszystkie identyfikatory dodatkowych grup. Identyfikatory dodatkowych grup są właściwością opcjonalną w normie POSDC.l. Stała NGROUPS_MAX (tab. 2.7) określa liczbę możliwych identyfikatorów dodatkowych grup. Najczęściej ma ona wartość 16. Jeżeli dana implementacja nie wspiera identyfikatorów dodatkowych grup, to ta stała przyjmuje wartość 0. Identyfikatory dodatkowych grup są stosowane w systemach SVR4 oraz 4.3+BSD. Standard FIPS 151-1 wymaga, by były zaimplementowane identyfikatory dodatkowych grup oraz by stała NGROUPS_MAX była nie mniejsza niż 8.

Dzięki stosowaniu identyfikatorów dodatkowych grup nie musimy jawnie zmieniać grup. Przynależność do wielu grup (czyli jednoczesne uczestnictwo w wielu projektach) nie jest sytuacją rzadką. Do pobierania identyfikatorów dodatkowych grup służą trzy funkcje.

void setgrent (void); void endgrent (void); Wszystkie trzy funkcje występują w systemach SVR4 oraz 4.3+BSD. Nie są natomiast częścią normy POSIX. 1.

Funkcja s e t g r e n t otwiera plik grup (jeśli nie jest jeszcze otwarty) i przewija go do początku. Funkcja g e t g r e n t czyta następny wpis z pliku grup, otwierając ten plik, jeśli jeszcze nie jest otwarty. Funkcja endgrent zamyka plik grup.

#include #include i n t getgroups (int gidsetsize, gid_t grouplist{]) ; Przekazuje: liczbę identyfikatorów dodatkowych grup, jeśli wszystko w porządku; - 1 , jeśli wystąpił błąd i n t s e t g r o u p s ( i n t ngroups, c o n s t g i d _ t grouplist[] ) ; int

i n i t g r o u p s ( c o n s t c h a r *username,

g i d _ t basegid) ;

Przekazują: 0, jeśli wszystko w porządku; -1 jeśli wystąpił błąd

6. Systemowe pliki danych Z tych trzech funkcji, tylko getgroups jest zdefiniowana w normie POSDC.l. Funkcje s e t g r o u p s oraz i n i t g r o u p s wykonują operacje uprzywilejowane, więc nie są częścią normy POSIX. 1. Systemy SVR4 oraz 4.3+BSD implementują jednak wszystkie trzy funkcje.

Funkcja getgroups wypełnia tablicę grouplist identyfikatorami dodatkowych grup. W tablicy można wpisać do gidsetsize elementów. Funkcja przekazuje liczbę zapisanych w tablicy identyfikatorów dodatkowych grup. Jeżeli w danym systemie stała NGROUPS_MAX ma wartość 0, to funkcja przekazuje 0, a nie błąd. W szczególnym przypadku, gdy argument gidsetsize jest równy 0, funkcja przekazuje tylko liczbę identyfikatorów dodatkowych grup. Tablica grouplist nie jest modyfikowana. (W ten sposób wywołujący może dowiedzieć się, jaki powinien zaalokować obszar dla tablicy grouplist). Funkcję s e t g r o u p s wywołuje nadzorca systemu, by ustalić listę identyfikatorów dodatkowych grup dla wywołującego tę funkcję procesu. Tablica wskazana argumentem grouplist zawiera identyfikatory grup, a argument ngroups określa liczbę elementów w tablicy. Praktycznie funkcja s e t g r o u p s jest zazwyczaj stosowana jedynie w funkcji i n i t g r o u p s , która czyta cały plik grup (za pomocą funkcji g e t g r e n t , s e t g r e n t oraz endgrent, które opisaliśmy wcześniej) i sprawdza, do jakich grup należy użytkownik o nazwie username. Następnie wywołuje funkcję s e t g r o u p s , aby zainicjować listę identyfikatorów dodatkowych grup dla tego użytkownika. Tylko nadzorca może wywołać funkcję i n i t g r o u p s , ponieważ wywołuje ona funkcję s e t g r o u p s . Funkcja i n i t g r o u p s nie tylko wyszukuje wszystkie grupy, do których należy użytkownik wskazany w argumencie username i umieszcza je na liście identyfikatorów dodatkowych grup, lecz również, na podstawie pliku haseł, podaje w argumencie basegid wartość identyfikatora grupy dla zadanego argumentu username. Tylko nieliczne programy wywołują funkcję i n i t g r o u p s - przykładem jest program l o g i n ( l ) , uruchamiany, gdy logujemy się w systemie.

Inne pliki danych Dotychczas omówiliśmy tylko dwa systemowe pliki danych - pliki haseł i grup. Typowe, wykonywane na co dzień operacje w systemie Unix korzystają z wielu innych plików. Na przykład w sieciowym oprogramowaniu BSD ważne są takie pliki jak: plik zawierający usługi dostarczane przez różne serwery sieciowe ( / e t c / s e r v i c e s ) , pliki do obsługi protokołów ( / e t c / p r o t o c o l s ) i sieci (/etc/networks). Na szczęście interfejsy do tych rozmaitych plików są bardzo podobne do opisanych dla plików haseł i grup. Ogólną zasadą jest istnienie co najmniej trzech funkcji dla każdego systemowego pliku danych:

195

6.6. Inne pliki danych

1. Funkcja serii get czyta kolejny rekord, otwiera plik, jeśli jest to potrzebne, i zazwyczaj przekazuje wskaźnik do struktury. Jeśli otrzymamy pusty wskaźnik, to oznacza, że dotarliśmy do końca pliku. Większość funkcji serii get przekazuje wskaźnik do struktury statycznej, a więc zawsze gdy chcemy przechować odpowiedź, musimy zapamiętać zawartość tej struktury. 2. Funkcja serii s e t otwiera plik (jeśli nie jest jeszcze otwarty) i przewija go do początku. Funkcję tę stosujemy, gdy wiemy, że chcemy wznowić działania od początku pliku. 3. Funkcja serii end zamyka plik danych. Jak wspomnieliśmy wcześniej, powinniśmy zawsze ją wywołać po zakończeniu obsługi określonego pliku, by zamknąć wszystkie otwarte pliki. Dodatkowo, jeżeli plik danych może być przeszukiwany na podstawie zadanych kluczy, to są dostarczane procedury, które szukają rekordu o podanym kluczu. Mamy na przykład dwie funkcje do przeszukiwania pliku haseł na podstawie kluczy: funkcja getpwnam szuka rekordu zawierającego podaną nazwę użytkownika, a funkcja getpwuid szuka rekordu z zadanym numerycznym identyfikatorem użytkownika. W tabeli 6.3 pokazujemy niektóre z procedur, które są dostępne w systemach SVR4 i 4.3+BSD. Uwzględniliśmy w niej nie tylko funkcje do obsługi pliku haseł i pliku grup, o których mówiliśmy wcześniej w tym rozdziale, ale również pewne funkcje sieciowe. W tabeli widzimy funkcje serii get, s e t oraz end dla wszystkich rodzajów plików danych. Tabela 6.3 Podobne procedury umożliwiające dostęp do systemowych plików danych Opis

Plik danych

Plik nagłówkowy

Struktura

Dodatkowe funkcje wyszukujące na podstawie klucza

passwd

getpwnam, getpwuid

hasła

/etc/passwd

grupy

/etc/group

group

getgrnam, getgrgid

stacje

/etc/hosts

hostent

gethostbyname, gethostbyaddr

sieci

/etc/networks

netent

getnetbyname, getnetbyaddr

protokoty /etc/protocols

protoent

getprotobyname, getprotobynumber

usługi

servent

getservbyname, getservbyport

/etc/services

W systemie SVR4 ostatnie cztery pliki danych z tab. 6.3 są dowiązaniami symbolicznymi do plików o takich samych nazwach w katalogu / e t c / i n e t . Oba systemy, SVR4 oraz 4.3+BSD, mają dodatkowe, bardzo podobne funkcje służące do zarządzania plikami związanymi z administrowaniem systemem, ale są one specyficzne dla każdej implementacji.

6. Systemowe pliki danych

Rejestry logowania Większość systemów w Uniksie korzysta z dwóch plików danych: pliku utmp, który gromadzi informacje na temat wszystkich aktualnie załogo wanych użytkowników, oraz pliku wtmp, który rejestruje wszystkie logowania i wylogowania. W wersji 7 do obu plików zapisywano taką samą postać rekordu: struct utmp { char ut_line[8]; char ut_name[8]; long ut_time;

linia terminalu: "ttyhO", "ttydO", "ttypO", ... */ nazwa użytkownika */ sekundy od początku "epoki" */

W czasie logowania program l o g i n wypełnia jedną taką strukturę i zapisuje ją do pliku utmp, a następnie tę samą strukturę dołącza do pliku wtmp. W czasie wylogowania proces i n i t wymazuje odpowiedni wpis z pliku utmp (wypełnia go bajtami zerowymi) oraz dołącza nowy wpis do pliku wtmp. Wpis w pliku wtmp dotyczący wylogowania ma pole ut_name wypełnione zerami. Do pliku wtmp są dołączane specjalne wpisy, które wskazują, kiedy system był ładowany, oraz zaznaczają zmianę czasu systemowego (wpis przed i po zmianie czasu). Program who(l) czyta plik utmp i drukuje jego zawartość w czytelnej postaci. W nowszych wersjach Uniksa pojawiło się polecenie l a s t ( l ) , które czyta plik wtmp i drukuje wybrane wpisy. W większości obecnych wersji Uniksa nadal istnieją oba pliki, utmp oraz wtmp. Jak moglibyśmy się spodziewać, ilość informacji przechowywanych w tych plikach wzrosła. Wersja 7 zapisywała strukturę o rozmiarze 20 bajtów, w systemie SVR2 było to już 36 bajtów, a rozszerzona struktura utmp w systemie SVR4 zajmuje ponad 350 bajtów! Szczegółowy format tych rekordów w systemie SVR4 jest opisany na stronach utrap(4) oraz utmpx(4) podręcznika systemowego. W systemie SVR4 oba pliki są umieszczone w katalogu /var/adm. SVR4 dostarcza wielu funkcji opisanych na stronach podręcznika getut(3) oraz getutx(3) służących do odczytu i zapisu tych plików. Strona utmp(5) w podręczniku systemowym systemu 4.3+BSD przedstawia format rekordów w tej wersji. Pliki rejestrów mają nazwy ścieżek /var/run/utmp oraz /var/log/wtmp.

Dane identyfikujące system W normie POSIX.l jest zdefiniowana funkcja uname, która przekazuje informacje o lokalnej stacji oraz systemie operacyjnym. #include i n t uname ( s t r u c t utsname *name); Przekazuje wartość nieujemną, jeśli wszystko w porządku - 1 , jeśli wystąpił błąd

197

6.8. Dane identyfikujące system

Podajemy adres struktury utsname, a funkcja wypełniają. Norma POSDC.l definiuje tylko minimalny zakres pól w strukturze (wszystkie pola są tablicami typu znakowego), a każda implementacja musi sama ustalać rozmiar poszczególnych tablic. Pewne implementacje wprowadzają dodatkowe pola w strukturze. System V alokował dla każdego elementu po 8 bajtów razem z miejscem dla pustego bajtu na końcu. struct char char char char char

utsname { sysname[9]; nodename[9] ; release[9]; version[9]; machinę[9];

/* /* /* /* /*

nazwa systemu operacyjnego */ nazwa węzła (stacji) */ bieżące wydanie systemu operacyjnego */ bieżąca wersja wydania */ nazwa typu sprzętu */

Polecenie uname(l) służy do wyprowadzania informacji ze struktury utsname. Norma POSDC.l ostrzega, że element nodename nie zawsze może być stosowany do odwoływania się do stacji w sieci komputerowej. Funkcja ta wywodzi się z Systemu V, a dawniej pole nodename umożliwiało odwoływanie się do stacji w sieci UUCP. Zauważmy, że informacja w tej strukturze nie zawiera żadnych danych na temat wersji standardu POSIX.l. Możemy się o tym dowiedzieć za pomocą stałej _POSIX_VERSION, zgodnie z opisem w p. 2.5.2. Wreszcie, funkcja ta daje nam jedynie metodę pobrania informacji ze struktury, nie ma natomiast możliwości zainicjowania danych, norma POSIX.l nie określa reguł nadawania wartości początkowej. W większości wersji Systemu V informacja ta zostaje wkompilowana w jądro systemu podczas jego tworzenia.

Systemy berkelejowskie stosują funkcję gethostname, która przekazuje nazwę stacji. Nazwa ta na ogół odpowiada nazwie w sieci TCP/IP. #include

i n t gethostname(char * name, i n t namelen) ; Przekazuje: 0, jeśli wszystko w porządku; -•1, jeśli wystąpił błąd

Nazwa, którą otrzymujemy w zmiennej name, jest zakończona pustym znakiem, o ile dostarczono wystarczająco dużo miejsca na dane. Stała MAXHOSTNAMELEN z pliku nagłówkowego określa maksymalną długość nazwy (zazwyczaj 64 bajty). Jeżeli stacja jest podłączona do sieci TCP/IP, to nazwa stacji jest typowo pełną kwalifikowaną nazwą domenową. Mamy również polecenie hostname(l), które może pobrać nazwę stacji. (Nazwa stacji jest ustalana przez nadzorcę za pomocą podobnej funkcji o nazwie sethostname). Nazwa stacji jest określana w czasie wstępnego ła-

6. Systemowe pliki danych

199

6.9. Procedury obsługi czasu i daty

dowania systemu na podstawie plików startowych, wywoływanych z pliku /etc/rc. Mimo że ta funkcja ma korzenie berkelejowskie, w systemie SVR4, w ramach pakietu zgodności z BSD, znajdziemy funkcje gethostname i sethostname oraz polecenie hostname. System SVR4 zwiększył wartość stałej MAXHOSTNAMELEN do 256 bajtów.

ctime\

struct tm (czas przetworzony)

Procedury obsługi czasu i daty Podstawowa usługa czasu, dostarczana wraz z jądrem systemu Unix, zlicza sekundy, które upłynęły od początku „epoki", czyli od 1 stycznia 1979 r., godz. 00:00:00, zgodnie z czasem uniwersalnym. W podrozdziale 1.10 powiedzieliśmy, że ten czas, nazywany czasem kalendarzowym, jest reprezentowany w strukturze typu t i m e _ t . Zawiera on zarówno czas, jak i datę. System Unix zawsze różnił się od innych systemów operacyjnych, gdyż (a) utrzymuje czas uniwersalny (UTC), a nie lokalny, (b) automatycznie obsługuje zamianę czasu letniego na zimowy i odwrotnie oraz (c) przechowuje jako jeden element datę i czas. Funkcja time przekazuje bieżący czas i datę. #include time t time(time t *calptr) ; Przekazuje: wartość, jeśli wszystko w porządku - 1 , jeśli wystąpił błąd

Wartością funkcji jest zawsze czas. Jeżeli argument jest niepusty, to czas jest również umieszczany w lokalizacji wskazanej przez calptr. W wielu systemach berkelejowskich time(3) jest funkcją, która tylko wykonuje funkcję systemową gettimeofday(2). Nie powiedzieliśmy, w jaki sposób jest inicjowany czas w ramach jądra systemu. W systemie SVR4 służy do tego funkcja stime(2), a w systemach berkelejowskich funkcja settimeofday(2). Funkcje gettimeofday oraz settimeofday, wzorowane na systemie BSD, stosują większą rozdzielczość (podają czas z dokładnością do mikrosekundy) niż funkcje time i stime. Jest to ważne dla niektórych aplikacji.

Gdy już otrzymamy dużą wartość całkowitoliczbową, która zlicza sekundy od początku „epoki", to na ogół wywołujemy inne funkcje obsługi czasu, które konwertują czas do czytelnej postaci czasu i daty. Na rysunku 6.1 są zobrazowane relacje między różnymi funkcjami do obsługi czasu. (Wszystkie cztery funkcje, których nazwy wymieniamy nad przerywanymi liniami, l o c a l t i m e , mktime, ctime i s t r f t i m e zależą od wartości zmiennej środowiskowej TZ, którą opiszemy jeszcze w tym podrozdziale).

t i m e _ t I (czas kalendarzowy)

jądro systemu Rys. 6.1 Zależności między różnymi funkcjami obsługi czasu

Dwie funkcje l o c a l t i m e oraz gmtime przekształcają czas kalendarzowy do postaci czasu zapisanego w strukturze tm. struct i ni: int int int int int int int int

czas przetworzony */ tm 1 /* c tm sec; /* sekundy po minutach: fO, 61] tm min; /* minuty po godzinach: [O, 59] tm hour; /* godziny po północy: [O, 23] ^ / tm mday; /* dzień miesiąca: [1, 31] */ tm_ mon; /* miesiąc danego roku: [O, 12] tm_ year; /* rok od 1900 */ tm wday; /* kolejny dzień tygodnia od n i e d z i e l i : [0, 6] */ tm yday; /* kolejny dzień roku od 1. stycznia: [0, 365] */ tm isdst ; / * s y g n a l i z a t o r zmiany czas l e t n i - c z a s zimowy: * 0 */

Pole sekund może mieć wartość większą niż 59, ponieważ są w nim umieszczane również sekundy przestępne (leap seconds). Zauważmy, że wszystkie pola oprócz dnia miesiąca zaczynają się od zera. Sygnalizator zmiany czas letni-czas zimowy jest dodatni, jeśli obowiązuje zmiana czasów, a równy, zeru, gdy nie ma być realizowana zmiana czasów; wartość ujemna świadczy o tym, że informacja nie jest dostępna. łfinclude struct tm *gmtime (const time t

i1

calptr) ;

struct tm *localtime(const time t *calptr) ; Przekazują: wskaźnik do czasu w postaci struktury

6. Systemowe pliki danych

6.9. Procedury obsługi czasu i daty

Funkcje l o c a l t i m e i gmtime różnią się między sobą tym, że pierwsza przekształca czas kalendarzowy do postaci lokalnej (z uwzględnieniem lokalnej strefy czasowej oraz sygnalizatora zmiany czas letni-czas zimowy), a druga przekształca czas kalendarzowy do postaci danych w strukturze prezentowanych jako czas uniwersalny. Funkcja mktime pobiera czas umieszczony w strukturze tm (traktowany jako czas lokalny) i przekształca go do postaci wartości t i m e t .

Argument format steruje formatowaniem wartości czasu. Podobnie jak w funkcjach serii p r i n t f , specyfikatory konwersji są podawane jako sekwencja: znak procenta oraz znak specjalny. Wszystkie pozostałe znaki napisu format są kopiowane wprost na strumień wyjściowy. Sekwencja dwóch znaków procentu generuje na wyjściu jeden taki znak (%). W przeciwieństwie do funkcji serii p r i n t f każda konwersja tworzy napis o stałym rozmiarze, nie ma możliwości podania szerokości pola w argumencie format. W tabeli 6.4 zebraliśmy 21 różnych specyfikatorów konwersji w standardzie ANSI.

iinclude

Tabela 6.4 Specyfikatory konwersji dla funkcji s t r f t i m e

time t mktime (struct tm *tmptr) ; Przekazuje: czas kalendarzowy, jeśli wszystko w porządku; -

201

Jeśli wystąpił błąd

Format

Opis

Przykład

%a

skrócona nazwa dnia tygodnia

Tue

%A

pełna nazwa dnia tygodnia

Tuesday

%b

skrócona nazwa miesiąca

Jan

%B

pełna nazwa miesiąca

January

%c

data i czas

Tue Jan 14 19:40:30 1992

%d

dzień miesiąca: [01, 31]

14

łfinclude

%H

godzina dnia 24-godzinnego: [00, 23]

19

char *asctime (const struct tm *tmptr) ;

%I

godzina dnia 12-godzinnego: [01, 12]

07

char *ctime(const time_t *calptr) ;

%j

dzień roku: [001,366]

014

%m

miesiąc: [01, 12]

01

%M

minuta: [00, 59]

40

%p

AM/PM (przed południem/po południu)

PM

%S

sekunda: [00, 61]

30

Funkcje a s c t i m e oraz ctime generują popularny napis 26-znakowy, podobny do standardowego wydruku polecenia date(l): Tue Jan 14 17:49:03 1992\n\0

Przekazują: wskaźnik do napisu zakończonego pustym znakiem

Argumentem funkcji asctime jest wskaźnik do przetworzonego napisu umieszczonego w strukturze, a argumentem funkcji ctime jest wskaźnik do zmiennej reprezentującej czas kalendarzowy. Ostatnia funkcja służąca do obsługi czasu jest najbardziej skomplikowana. Funkcja s t r f t i m e jest wzorowana na funkcji p r i n t f , ale pozwala wypisywać wartości czasu. #include size t strftime (const char *buf, si ze_t maxsize,

%u

numer tygodnia, licząc od niedzieli [00, 53]

02

%w

dzień tygodnia: [0=niedziela, 6]

2

%W

numer tygodnia, licząc od poniedziałku [00, 53]

02

%x

data

01/14/92

%X

czas

19:40:30

%y

dwie ostatnie cyfry roku: [00, 99]

92

%Y

cztery cyfry roku

1992

%Z

nazwa strefy czasowej

MST

const char *format. const s t r u c t tm *tmptr) ; Przekazuje: liczbę znaków zapamiętanych w tab icy, jeżeli starczyło miejsca; w przeciwnym razie 0

Ostatni argument jest wartością czasu, którą chcemy sformatować, podaną jako wskaźnik do struktury typu tm. Sformatowany wynik jest umieszczany w tablicy buf, której rozmiar wynosi mcocsize znaków. Jeżeli rozmiar wyniku razem z kończącym pustym znakiem nie przekracza rozmiaru bufora, to funkcja przekazuje liczbę znaków umieszczonych w buf (bez kończącego pustego znaku). W przeciwnym razie funkcja przekazuje wartość 0.

Trzecia kolumna tej tabeli pokazuje napis, reprezentujący datę i czas, wyprowadzany przez funkcję s t r f t i m e w systemie SVR4: T u e J a n 14 1 9 : 4 0 : 3 0 MST 1992

Tylko dwa specyfikatory mają nazwy nie sugerujące odpowiadającego im znaczenia; są to % u oraz %w. Pierwszy odpowiada numerowi tygodnia w roku, przy czym pierwszym tygodniem roku jest tydzień zawierający pierwszą niedzielę. Drugi, %w, odpowiada numerowi tygodnia w roku, przy czym pierwszym tygodniem roku jest tydzień zawierający pierwszy poniedziałek.

6. Systemowe pliki danych

7

Systemy SVR4 oraz 4.3+BSD stosują również dodatkowe rozszerzenia napisu format w funkcji s t r f time. Wspomnieliśmy, że na cztery funkcje z rys. 6.1, wskazane tam przerywaną linią, ma wpływ zmienna środowiskowa TZ; są to: l o c a l t i m e , mktime, ctime oraz s t r f t i m e . Jeżeli zmienna TZ jest zdefiniowana, to funkcje używają jej zamiast domyślnej wartości strefy czasowej. Jeżeli zmienna jest pustym znakiem (np. TZ=), to na ogół jest stosowany czas uniwersalny. Typową wartością zmiennej TZ jest np. TZ=EST5EDT, ale norma POSIX.l umożliwia dużo bardziej precyzyjną specyfikację. Zainteresowanych szczegółami dotyczącymi zmiennej TZ odsyłamy do p. 8.1.1 standardu POSIX.l [IEEE 1990], strony environ(5) w podręczniku systemu SVR4 [13] lub opisu funkcji ctime(3).

Środowisko procesu w systemie Unix

Wszystkie opisane w tym podrozdziale funkcje obsługi daty i czasu są zdefiniowane w standardzie ANSI. W normie POSIX.l dodano zmienną środowiskową TZ. Pięć z siedmiu funkcji z rys. 6.1 pochodzi z wersji 7 (lub jeszcze wcześniejszych wersji Uniksa): time, localtime, gmtime oraz ctime. Ostatnie uaktualnienia tych funkcji dotyczyły głównie stref czasowych poza Stanami Zjednoczonymi oraz nowych reguł zmiany czasu letniego i zimowego.

7.1

W następnym rozdziale przystąpimy do omawiania narzędzi służących do sterowania procesami, jednak przedtem musimy zaznajomić się ze środowiskiem pracy pojedynczego procesu. Zobaczymy, jak jest wywoływana funkcja main, gdy wykonujemy program, jak są przekazywane do nowego programu argumenty wiersza poleceń, jak wygląda typowe rozłożenie programu w pamięci, jak jest alokowana dodatkowa pamięć, jak proces może korzystać ze zmiennych środowiskowych i w jaki sposób może zakończyć pracę. Przyjrzymy się też funkcjom longjmp oraz s e t jmp i prześledzimy ich współpracę ze stosem procesu. Na koniec dokonamy przeglądu ograniczeń dotyczących zasobów procesu.

Podsumowanie Wszystkie systemy uniksowe korzystają z plików haseł i grup. Dokonaliśmy przeglądu różnych funkcji służących do odczytu tych plików. Mówiliśmy o maskowaniu haseł, które znacznie poprawia bezpieczeństwo systemu. Identyfikatory dodatkowych grup stały się popularne w nowszych wersjach Uniksa - umożliwiają jednoczesne uczestnictwo w wielu grupach. Przyjrzeliśmy się też podobnym funkcjom stosowanym w większości systemów do ułatwienia korzystania z różnych systemowych plików danych. Zakończyliśmy rozdział przeglądem funkcji do obsługi czasu i daty, które są dostarczane w standardach ANSI C oraz POSIX.l.

Ćwiczenia 6.1

Co należy zrobić, jeżeli chcemy pobrać zaszyfrowane hasło, a system używa pliku maskowanych haseł?

6.2

Wykonaj poprzednie ćwiczenie przy założeniu, że masz uprawnienia nadzorcy i Twój system stosuje maskowanie haseł.

6.3

Napisz program, który wywołuje funkcję uname i drukuje wszystkie pola struktury utsname. Porównaj wynik z rezultatem polecenia uname(l).

6.4

Napisz program, który pobiera bieżący czas i drukuje go za pomocą funkcji strftime, tak że wynik wygląda jak dane wyprowadzone przez polecenie date(l). Zmień ustawienie zmiennej środowiskowej TZ i sprawdź, jaki jest efekt.

Wprowadzenie

7.2

Funkcja main Program napisany w języku C zaczyna pracę od wywołania funkcji main. Oto prototyp funkcji main: int main(int argc, char *argv[]) ; argc wskazuje liczbę argumentów wiersza poleceń, a argv jest tablicą wskaźników do tych argumentów. Opiszemy to szczegółowo w podrozdz. 7.4. Gdy system uruchamia program w języku C (za pomocą jednej z funkcji serii exec, które pokażemy w podrozdz. 8.9), wówczas najpierw jest wywoływana specjalna procedura startowa, a następnie rozpoczyna się wykonanie funkcji main. Plik z programem wykonywalnym wskazuje jako adres początkowy programu właśnie procedurę startową - wszystko ustala program łą-

7. Środowisko procesu w systemie Unix

czący wywoływany przez kompilator C (zazwyczaj nosi nazwę cc). Procedura startowa pobiera pewne wartości z jądra systemu (argumenty wiersza poleceń oraz zmienne środowiskowe) i ustala wszystko, co jest potrzebne, by wywołać funkcję main.

Zakończenie procesu Jest pięć sposobów zakończenia procesu. 1. Zakończenie normalne: (a) powrót z funkcji main, (b) wywołanie funkcji e x i t , (c) wywołanie funkcji _ e x i t . 2. Zakończenie awaryjne: (a) wywołanie funkcji a b o r t (rozdz. 10), (b) zakończenie w wyniku pojawienia się sygnału (rozdz. 10). Procedura startowa, o której mówiliśmy w poprzednim podrozdziale, jest tak przygotowana, że po powrocie z funkcji main wywołuje funkcję e x i t . Gdyby procedura startowa była napisana w języku C (na ogół jest napisana w asemblerze), to wywołanie funkcji main miałoby postać: exit( main(atgc, argv) ) ;

:jeexit oraz_exit Dwie funkcje umożliwiają poprawne zakończenie programu: _ e x i t , która natychmiast powraca do jądra systemu, oraz e x i t , która najpierw wykonuje pewne działania porządkujące, a następnie powraca do jądra systemu. #include void e x i t ( i n t status); fłinclude void exit (int status) ;

W podrozdziale 8.5 omówimy wpływ obu funkcji na inne procesy, np. procesy potomne oraz proces macierzysty kończącego się procesu. Funkcje te korzystają z innych plików nagłówkowych, ponieważ funkcja e x i t jest określona w standardzie ANSI C, a _ e x i t w normie POSK.l.

Tradycyjnie funkcja e x i t realizowała czyste zakończenie pracy standardowej biblioteki wejścia-wyjścia: najpierw wywołanie funkcji f c l o s e zamy-

7.3. Zakończenie procesu

205

kało wszystkie otwarte strumienie. Przypominamy z podrozdz. 5.5, że oznaczało to opróżnienie wszystkich buforów z danymi wyjściowymi (czyli zapisanie zaległych danych do plików). Obie funkcje e x i t oraz _ e x i t wymagają jednego argumentu całkowitoliczbowego, nazywanego stanem zakończenia (exit status). Większość powłok uniksowych umożliwia analizowanie stanu zakończenia procesu. Stan zakończenia jest nieokreślony w następujących przypadkach: (1) gdy wywołamy jedną z tych funkcji, nie podając stanu zakończenia, (2) gdy funkcja main realizuje powrót bez żadnej wartości, (3) gdy nie powiedzie się funkcja main (powrót pośredni). Oznacza to, że klasyczny przykład: #include

main() { printf("hello, world\n");

jest niekompletny, ponieważ zakończenie awaryjne spowoduje powrót do procedury startowej bez przekazania wartości (stanu zakończenia). Powinniśmy więc dodać return(0);

lub exit (0);

aby dostarczyć zerowy stan zakończenia do procesu wykonującego ten program (na ogół jest to powłoka). Również deklaracja funkcji main powinna mieć postać int main(void);

W następnym rozdziale zobaczymy, jak proces rozpoczyna wykonanie programu oraz jak oczekuje na zakończenie procesu i pobiera stan zakończenia. Gdy deklarujemy, że funkcja main ma wartość calkowitoliczbową i jednocześnie używamy funkcji e x i t (zamiast return), wówczas niektóre kompilatory oraz programy l i n k ( l ) w Uniksie generują zbędne ostrzeżenia. Wynika to z faktu, że kompilatory te nie wiedzą, że wywołanie funkcji e x i t z funkcji main jest tym samym co wywołanie r e - •• turn. Komunikat ostrzegający ma mniej więcej taką postać: control reaches end of nomoid function (sterowanie dotarło do końca funkcji, która ma wartość niepustą). Jednym ze sposobów uniknięcia takich ostrzeżeń (które szybko mogą zacząć nas irytować) jest stosowanie w funkcji głównej instrukcji r e t u r n zamiast e x i t . Jednak takie podejście uniemożliwi nam korzystanie z programu pomocniczego grep do wyszukiwania wszystkich wywołań e x i t w programie. Innym rozwiązaniem jest zadeklarowanie, że funkcja main przekazuje wartość typu void, a nie i n t , i pozostawienie wywołania e x i t . Uwolnimy się wówczas od ostrzeżeń kompilatora, ale kod programu nie wygląda poprawnie (szczególnie w tekście na temat programowania). W naszej książce funkcje main zawsze mają wartość i n t , ponieważ taka jest definicja w standardach ANSI C oraz POSIX. 1. Będziemy więc ignorować liczne ostrzeżenia przekazywane przez kompilator.

7. Środowisko procesu w systemie Unix cja a t e x i t

Zgodnie ze standardem ANSI C proces może zarejestrować do 32 funkcji, które są wywoływane automatycznie przez funkcję e x i t . Są one nazywane procedurami obsługi zakończenia (exit handlers), a rejestrujemy je za pomocą funkcji atexit. #include

Przekazuje 0, jeśli wszystko w porządku; wartość niezerową, jeśli wystąpi! błąd

Taka deklaracja oznacza, że w argumencie funkcji a t e x i t przekazujemy adres funkcji. Wywołanie tej funkcji odbywa się bez żadnego argumentu i nie spodziewamy się otrzymania wartości zwracanej. Funkcja e x i t wywołuje procedury obsługi zakończenia w kolejności odwrotnej niż odbywało się ich rejestrowanie. Każda funkcja jest wywoływana tyle razy, ile razy była rejestrowana. Procedury obsługi zakończenia pojawiły się dopiero w standardzie ANSI C. Stosująje systemy SVR4 oraz 4.3+BSD. Wczesne wydania Systemu V oraz 4.3BSD nie miały możliwości rejestrowania procedury obsługi zakończenia.

W standardach ANSI C oraz POSIX.l funkcja e x i t najpierw wywołuje procedury obsługi zakończenia, a następnie funkcję f c l o s e w celu zamknięcia wszystkich otwartych strumieni. Na rysunku 7.1 przedstawiamy, jak program w języku C startuje oraz w jaki sposób może się zakończyć. exit procedura obsługi zakończenia procedura obsługi zakończenia

proces użytkowy

stand. procedury porządk. we-wy

jądro systemu Rys. 7.1

Zwróćmy uwagę, że jedynym sposobem wykonania programu przez system jest wywołanie jednej z funkcji exec. Jedynym sposobem dobrowolnego zakończenia pracy przez proces jest wywołanie funkcji _ e x i t albo bezpośrednio, albo pośrednio (za pomocą funkcji e x i t ) . Proces może również zostać zakończony nie z własnej woli, lecz w wyniku przechwycenia sygnału (nie pokazujemy tego na rys. 7.1).

Przykład

int atexit (void (*func) (void));

procedury startowe języka C

207

7.3. Zakończenie procesu

Sposób startu programu w języku C oraz jego zakończenie

W programie 7.1 zastosowano funkcję a t e x i t . Uruchomienie programu daje poniższy wynik. $ a.out main is done first exit handler first exit handler second exit handler

Zauważmy, że nie wywołujemy funkcji e x i t , zamiast tego powracamy (za pomocą instrukcji r e t u r n ) z funkcji głównej. • łfinclude

"ourhdr.h"

static void my_exitl(void), my_exit2(void), int main(void) { if (atexit(my_exit2) != 0) err_sys("can't register my_exit2") if (atexit(my_exitl) != 0) err_sys("can't register my_exitl") if (atexit(my_exitl) != 0) err_sys("can't register my_exitl") printf("main is done\n"); return(0); } static void my_exitl(void) { printf("first exit handler\n"); } static void my_exit2(void) { printf("second exit handler\n");

Próg. 7.1

Przykład procedur obsługi wyjścia

7. Środowisko procesu w systemie Unix

4

Argumenty wiersza poleceń Gdy chcemy wykonać program, wówczas wywołujący funkcję exec może przekazać do nowego programu argumenty wiersza poleceń. Jest to typowa operacja w powłokach uniksowych. Widzieliśmy to już wiele razy w naszych przykładach z poprzednich rozdziałów.

pustym znakiem. Adres tablicy wskaźników jest umieszczony w globalnej zmiennej o nazwie environ. extern char **environ;

Jeśli, na przykład, środowisko składa się z pięciu napisów, to wygląda to tak:

zykład

wskaźnik środowiska

Program 7.2 powiela wszystkie argumenty wiersza poleceń na standardowe wyjście. (Polecenie echo(l) z systemu Unix nie przepisuje argumentu zerowego). Jeżeli skompilujemy go i nazwiemy moduł wykonywalny echoarg, to podczas uruchomienia otrzymujemy: $ ./echoarg argl TEST foo argv[0]: ./echoarg argv[l]: argl argv[2]: TEST argv[3]: foo

*- HOME=/home/stevens\0

erwiron:

• PATH=:/bin:/usr/bin\O -*• SHELL=/bin/sh\O -• USER=stevens\O

Rys. 7.2 Środowisko składające się z pięciu napisów znakowych języka C

"ourhdr.h"

Pokazujemy jawnie puste bajty na końcu każdego napisu. Zmienną environ nazywamy wskaźnikiem środowiska, tablicę wskaźników listą środowiska, a napisy, na które wskazuje, napisami środowiska. Środowisko jest opisywane za pomocą widocznych na rys. 7.2 napisów przypisujących nazwie zmiennej wartość name=value Nazwy są typowo pisane w całości wielkimi literami, ale jest to tylko ogólnie przyjęta reguła. Dawniej systemy Unix przekazywały funkcji main trzeci argument będący adresem listy środowiska:

Ln(int argc, char *argv[]) i;

for (i = 0 ; i < argc; i++) printf("argv[%d]: exit(0) ;

napisy środowiska

NULL

for (i = 0; argv[i] != NULL;

int

lista środowiska

-*• LOGNAME=stevens\0

Standardy ANSI C oraz POSIX.l gwarantują, że a r g v [ a r g c ] jest pustym wskaźnikiem. Do przetwarzania argumentów możemy więc alternatywnie zastosować następującą pętlę:

iclude

209

7.5. Lista zmiennych środowiskowych

%s\n",

/* echo wszystkich argumentów wiersza * poleceń */ i,

i n t main(int argc, char *argv[], char *envp[]) ;

argv[i]);

Próg. 7.2 Powtórzenie wszystkich argumentów wiersza poleceń na standardowym wyjściu

D

Lista zmiennych środowiskowych Każdy program otrzymuje również listę zmiennych środowiskowych. Tak jak lista argumentów, lista zmiennych środowiskowych jest tablicą wskaźników, a każdy wskaźnik zawiera adres typowego napisu w języku C, zakończonego

Ponieważ standard ANSI C definiuje dwuargumentową funkcję main, a stosowanie trzeciego argumentu nie daje żadnych korzyści w porównaniu z użyciem zmiennej globalnej erwiron, norma POSIX.l określa, że zamiast trzeciego argumentu (opcjonalnego) należy korzystać ze zmiennej environ. Dostęp do konkretnych zmiennych środowiskowych uzyskujemy na ogół za pomocą funkcji getenv i s e t e n v (opisanych w podrozdz. 7.9), a nie przez bezpośrednie operacje na zmiennej environ. Jednak, gdy chcemy przejrzeć wszystkie zmienne środowiska, najwygodniej jest skorzystać ze wskaźnika environ.

7. Środowisko procesu w systemie Unix

6

211

7.7. Biblioteki wspólne

Program w języku C w pamięci operacyjnej

najwyższy adres

Tradycyjnie program w języku C zawiera następujące części:

stos

• Segment tekstu. Są to instrukcje maszynowe, wykonywane przez procesor. Zazwyczaj segment tekstu jest współdzielony, aby jedna kopia wystarczyła, jeśli programy są często uruchamiane (w przypadku edytorów tekstu, kompilatorów C, powłok itp.). Na ogół segment tekstu może być tylko czytany, aby program nie zamazał przypadkowo swoich instrukcji. • Segment danych zainicjowanych. Często jest nazywany po prostu segmentem danych i zawiera dane, które są w sposób jawny inicjowane w programie. Na przykład deklaracja języka C

" " " \

t

sterta dane niezainicjowane (bss) dane zainicjowane

i n t maxcount = 9 9;

najniższy adres

umieszczona poza jakąkolwiek funkcją, powoduje, że zmienna maxcount razem ze swoją wartością początkową jest umieszczana w segmencie danych zainicjowanych. • Segment danych niezainicjowanych. Często w skrócie nazywany segmentem „bss", po dawnym operatorze języka asembler, który oznaczał blok zaczynający się symbolem {błock started by symbol). System przed uruchomieniem programu nadaje wszystkim danym w tym segmencie wartości początkowe równe arytmetycznemu zeru lub wskaźnikowi pustemu. Jeśli deklaracja w języku C

Na rysunku 7.3 widzimy typowe rozłożenie poszczególnych segmentów w pamięci. Jest to prezentacja logiczna programu w pamięci - nie ma żadnych wymagań, by konkretna implementacja miała w ten sposób zorganizowaną pamięć. Mimo wszystko ten poglądowy rysunek pokazuje nam typową sytuację.

tekst

I zerowa wartość początkowa j po wywołaniu funkcji e x e c

}

przeczytane z pliku programu przez funkcję e x e c

Rys. 7.3 Typowa organizacja pamięci

W systemie 4.3+BSD na komputerach VAX segment tekstu zaczyna się od adresu 0, a stos rozciąga się od adresu 0x7 f f f f f f f w dół. W tych komputerach nieużywana wirtualna przestrzeń adresowa między szczytem sterty a dnem stosu jest bardzo duża. Zauważmy, że zgodnie z rys. 7.3 zawartość segmentu danych niezainicjowanych nie jest zapamiętana w pliku programu na dysku. System ustala zerową wartość danych w tym obszarze przed uruchomieniem programu. Plik programu na dysku zawiera tylko segment tekstu oraz dane zainicjowane. Polecenie s i z e ( l ) podaje w bajtach rozmiar segmentów tekstu, danych oraz segmentu bss. Na przykład

long sum[1000];

nie należy do żadnej funkcji, to zmienna jest umieszczana w segmencie danych niezainicjowanych. • Stos. W tym obszarze są zapamiętywane zmienne automatyczne oraz informacje związane z każdym wywołaniem funkcji. Za każdym razem, gdy wołamy jakąś funkcję, na stosie jest umieszczany adres wskazujący dokąd wywoływana funkcja ma powrócić, a także są tam przechowywane pewne dodatkowe informacje o środowisku wywołującego funkcję (np. niektóre rejestry maszynowe). Następnie nowo wywołana funkcja alokuje na stosie obszar dla swoich zmiennych automatycznych oraz tymczasowych. Dzięki takiej organizacji stosu możemy używać w języku C rekurencyjnych funkcji. • Sterta. Dynamiczna alokacja pamięci zazwyczaj korzysta z obszaru sterty. Tradycyjnie sterta była lokalizowana między początkiem niezainicjowanego segmentu danych a końcem stosu.

argumenty wiersza poleceń i zmienne środowiskowe

$ size /bin/cc /bin/sh text data bss dec 81920 16384 664 98968 90112 16384 0 106496

hex 18298 laOOO

/bin/cc /bin/sh

W czwartej i piątej kolumnie otrzymujemy rozmiary podane dziesiętnie oraz szesnastkowo.

7.7

Biblioteki wspólne Obecnie wiele systemów uniksowych stosuje biblioteki wspólne. Arnold w swojej książce [7] opisuje jedną z pierwszych implementacji w Systemie V, a Gingell i in. w pracy [22] przedstawiają różne implementacje w systemach SunOS. Wspólne biblioteki umożliwiają usunięcie popularnych procedur bibliotecznych z pliku wykonywalnego, zamiast tego system przechowuje w ogólnodostępnej pamięci pojedynczą kopię procedur bibliotecznych. Dzię-

7. Środowisko procesu w systemie Unix ki temu maleje rozmiar każdego pliku wykonywalnego, ale dochodzi nieco dodatkowej pracy w czasie uruchomienia programu albo przy pierwszym wywołaniu programu przez funkcję exec, albo przy pierwszym wywołaniu funkcji pochodzącej z tej biblioteki wspólnej. Zaletą wspólnych bibliotek jest automatyczne wprowadzenie nowych wersji procedur, bez potrzeby powtórnej realizacji fazy łączenia dla każdego programu korzystającego z biblioteki. (Oczywiście przy założeniu, że liczba i typy argumentów nie ulegają zmianie). Rozmaite systemy stosują różne sposoby powiadamiania programu, czy ma korzystać ze wspólnych bibliotek, czy nie. Opcje dla programów cc(l) oraz l d ( l ) są typowe. Abyśmy mogli zaobserwować różnice w rozmiarze pliku, poniższy plik wykonywalny (klasyczny program h e l l o . c ) powstał najpierw bez użycia bibliotek wspólnych. $ l s -1 a. out -rwxrwxr -x 1 stevens $ size a .out data text 49152 49152

bss 0

104859 Aug dec

hex

98304

18000

2 14:25 a . o u t

Jeśli następnie skompilujemy ten program, by używał wspólnych bibliotek, to otrzymane rozmiary dla pliku wykonywalnego znacznie zmaleją. $ l s -1 a . o u t -rwxrwxr-x 1 stevens $ size a. out

text 8192

data 8192

bss 0

24576 Aug 2 14:26 a.out dec

hex

16384

4000

Alokacja pamięci Standard ANSI C określa trzy funkcje służące do alokacji pamięci. 1. malloc. Alokuje w pamięci wskazaną liczbę bajtów. Wartość początkowa pamięci nie jest określona. 2. c a l l o c . Alokuje przestrzeń dla określonej liczby obiektów o zadanym rozmiarze. Cały zaalokowany obszar jest wypełniony bitami zerowymi. 3. r e a l l o c . Zmienia rozmiar poprzednio zaalokowanego obszaru (zwiększa go lub zmniejsza). Jeśli rozmiar rośnie, może to oznaczać przesunięcie wcześniej zaalokowanego obszaru w inne miejsce, aby dodać wolną przestrzeń na jego końcu. W takiej sytuacji nie jest określona wartość początkowa fragmentu pamięci między końcem starego a końcem nowego obszaru.

213

7.8. Alokacja pamięci #include

void *malloc(size t size) ; void *calloc(size t nobj, size t size) ; void *realloc(void

k

ptr, size t newsize) ;

Przekazują : niepuste wskaźniki, jeśli wszystko w porządku; NOLL, jeśli wystąpił błąd

void f ree (void *ptr)

Mamy pewność, że wszystkie trzy funkcje przekazują wskaźnik odpowiednio wyrównany, by nadawał się do umieszczenia dowolnego obiektu danych. Jeśli na przykład w danym systemie najbardziej restrykcyjne wymaganie dotyczy typu double, który musi zaczynać się od lokalizacji o adresie będącym wielokrotnością liczby 8, to wszystkie wskaźniki przekazywane przez trzy funkcje do alokacji będą tak wyrównane. Przypominamy naszą dyskusję z podrozdz. 1.6 na temat ogólnego wskaźnika void * oraz prototypów funkcji. Ponieważ trzy funkcje serii a l l o c przekazują wskaźniki ogólne, więc jeśli umieścimy dyrektywę #include < s t d l i b . h > (by dołączyć prototypy funkcji), to nie musimy jawnie przerzutowywać otrzymanego z tych funkcji wskaźnika, gdy przypisujemy go wskaźnikowi innego typu. Funkcja f r e e zwalnia pamięć wskazaną przez ptr. Zwolniona pamięć jest na ogół oddawana do puli dostępnej pamięci i może zostać zaalokowana przy kolejnych wywołaniach funkcji a l l o c . Funkcja r e a l l o c umożliwia zwiększenie lub zmniejszenie rozmiaru wcześniej zaalokowanego obszaru. (Najbardziej popularnym zastosowaniem jest zwiększenie obszaru). Na przykład, jeśli zaalokujemy obszar dla tablicy liczącej 512 elementów, a następnie tablica ta w czasie pracy programu zostanie w całości wypełniona i stwierdzimy, że są nam potrzebne dodatkowe puste elementy, to możemy wywołać funkcję r e a l l o c . Gdy jest wolna przestrzeń o wymaganym rozmiarze tuż za istniejącym obszarem, to funkcja r e a l l o c nie musi przesuwać wszystkich danych, tylko alokuje dodatkowy obszar na końcu istniejącego i jako wynik przekazuje ten sam wskaźnik, który otrzymała w argumencie. Jeżeli natomiast nie ma miejsca na końcu istnieją-' cego obszaru, to r e a l l o c alokuje nowy, wystarczająco duży obszar pamięci, kopiuje do niego wypełnioną wcześniej tablicę 512-elementową, a przed przekazaniem wskaźnika do nowego obszaru zwalnia stary obszar. Ponieważ obszary zaalokowane dynamicznie mogą być przesuwane w pamięci, nie powinniśmy umieszczać w nich wskaźników. Ćwiczenie 4.18 pokazuje zastosowanie funkcji r e a l l o c razem z funkcją getcwd do obsługi nazwy ścieżki o dowolnym rozmiarze. Z kolei próg. 15.27 prezentuje przykład użycia funkcji r e a l l o c , gdy chcemy uniknąć korzystania z tablic o rozmiarach ustalonych w czasie kompilacji.

7. Środowisko procesu w systemie Unix

7.9. Zmienne środowiskowe

z wersji jest dołączana do systemu 4.3+BSD), a ich kompilacja z podaniem specjalnych sygnalizatorów uruchamia dodatkowe sprawdzenia w fazie wykonania. Działanie systemu alokującego pamięć często decyduje o wydajności pewnych aplikacji, dlatego pewne systemy oferują dodatkowe możliwości. Na przykład w systemie SVR4 jest funkcja o nazwie m a l l o p t , która powoduje, że proces ustala wartości pewnych zmiennych kontrolujących działanie mechanizmu alokacji pamięci. Funkcja o nazwie m a l l i n f o dostarcza nam charakterystyki mechanizmu alokacji. Zalecamy sprawdzenie na stronie malloc(3) podręcznika systemowego, czy w konkretnym systemie możliwości te są dostępne.

Zwróćmy uwagę, że ostatnim argumentem funkcji r e a l l o c jest nowy rozmiar obszaru, newsize, a nie różnica między starym a nowym rozmiarem. W szczególnym przypadku, gdy ptr jest pustym wskaźnikiem, funkcja r e a l l o c zachowuje się jak malloc i po prostu alokuje obszar o rozmiarze newsize. Właściwość ta jest nowością w ANSI C. Starsze wersje funkcji r e a l l o c nie potrafiły obsłużyć pustego wskaźnika. Starsze wersje funkcji r e a l l o c umożliwiały ponowne alokowanie obszaru, który zdążyliśmy już zwolnić (wykonaliśmy funkcję free) po ostatnim wywołaniu jednej z funkcji malloc, r e a l l o c lub c a l l o c . Ten trik, pochodzący z wersji 7, korzysta ze stosowanej przez funkcję malloc strategii wyszukiwania w celu zapewnienia maksymalnej zwartości pamięci. System 4.3+BSD nadal stosuje taką zasadę, ale SVR4 nie. Możliwość ta jest wycofywana i nie powinna być stosowana.

Procedury alokujące są na ogół implementowane za pomocą funkcji systemowej sbrk(2). Wywołanie to rozszerza (lub zacieśnia) stertę procesu. (Odsyłamy do rys. 7.3). Prosta implementacja funkcji malloc i f r e e jest pokazana w podrozdz. 8.7 książki Kernighana i Ritchiego [28]. Mimo że wywołanie funkcji sbrk może rozszerzyć lub zawęzić pamięć procesu, to jednak większość wersji funkcji malloc i f r e e nigdy nie zmniejsza swojego rozmiaru pamięci. Pamięć, którą zwalniamy, staje się dostępna dla kolejnych alokacji, ale nie powraca do jądra systemu -jest utrzymywana w puli, którą dysponuje funkcja malloc. Musimy zwrócić uwagę na fakt, że większość implementacji alokuje nieco więcej przestrzeni niż jest to wymagane, a dodatkowy obszar jest używany do przechowywania specjalnych danych wspomagających śledzenie - rozmiaru alokowanych bloków, wskaźnika do kolejnego bloku do alokacji itp. Oznacza to, że zapisanie danych za końcem zalokowanego obszaru może wymazać takie specjalne informacje dotyczące następnego bloku. Jeśli przesuniemy wskaźnik do poprzedniego bloku, to jest możliwe zamazanie tego typu danych w bieżącym bloku. Inne, często brzemienne w skutkach, błędy powstają, gdy zwolnimy blok, który już wcześniej został zwolniony, lub wywołamy funkcję free ze wskaźnikiem, którego nie otrzymaliśmy wcześniej z jednej z funkcji służących do alokacji obszaru pamięci. Jeżeli proces wielokrotnie wywołuje funkcję malloc i twierdzi, że wykonuje również funkcję free, ale jego pamięć w sposób ciągły rośnie, to taki efekt nazywamy wyciekaniem pamięci (leakage). Najczęstszą przyczyną są jednak niepoprawne wywołania funkcji free, które teoretycznie miały zwracać nieużywane obszary pamięci. Ponieważ błędy związane z alokowaniem pamięci są bardzo trudne do prześledzenia, niektóre systemy udostępniają specjalne wersje tych funkcji alokacji, które za każdym razem, gdy jest wywoływana jedna z funkcji a l l o c lub funkcja f r e e , realizują dodatkowe sprawdzenia, czy nie wystąpił błąd. Takie wersje są często wskazywane przez dodanie specjalnej biblioteki w fazie łączenia. Źródła odpowiednich programów są ogólnie dostępne (np. jedna

215

Funkcja a i l o c a Na koniec powiemy parę słów o funkcji a i l o c a . Sekwencja wywołania funkcji a i l o c a jest taka sama jak funkcji malloc, ale zamiast alokowania przestrzeni na stercie, otrzymujemy pamięć w ramach obszaru stosu dla bieżącej funkcji. Dzięki takiemu podejściu staje się zbędne zwalnianie pamięci, gdyż obszar taki jest automatycznie oddawany przy powrocie z funkcji. Funkcja a i l o c a zwiększa rozmiar stosu. Niestety, niektóre systemy nie mogą stosować funkcji a i l o c a , gdyż nie jest możliwe zwiększanie rozmiaru stosu po wywołaniu funkcji. Mimo wszystko wiele pakietów programowych korzysta z tej funkcji i istniejąjej implementacje dla rozmaitych systemów.

7.9

Zmienne środowiskowe Powiedzieliśmy już wcześniej, że napisy środowiska mają zazwyczaj postać name—value Jądro systemu Unix nigdy nie analizuje tych napisów - ich interpretacją zajmują się różne aplikacje. Powłoki korzystają na przykład z licznych zmiennych środowiskowych. Niektóre z nich są ustalane w chwili logowania w systemie (np. HOME, USER itp.), a wartości innych zależą od nas samych. Typowo, zmiennym środowiskowym nadajemy wartość za pomocą pliku startowego dla powłoki i w ten sposób kontrolujemy dalsze działanie powłoki. Jeśli np. przypiszemy wartość zmiennej środowiskowej MAILPATH, to powłoka Bourne'a lub KornShell wie, gdzie ma szukać listów przekazanych pocztą elektroniczną. Standard ANSI C definiuje funkcję, której możemy używać do pobrania wartości zmiennych środowiskowych, ale ten sam standard stwierdza, że zawartość środowiska jest określona w implementacji.

216

7. Środowisko procesu w systemie Unix #include

char * g e t e n v ( c o n s t char *name); Przekazuje: wskaźnik do wartości zmiennej o podanej nazwie, jeśli wszystko w porządku; NULL, jeśli nie znaleziono definicji zmiennej

Zauważmy, że funkcja przekazuje wskaźnik do wartości (yalue) w napisie name=value. Powinniśmy zawsze używać funkcji geterw do pobrania konkretnej wartości środowiska, a nie korzystać wprost ze zmiennej environ. Niektóre zmienne środowiskowe są zdefiniowane przez POSIX.l oraz XPG3. W tabeli 7.1 zebraliśmy te zmienne, które określają oba standardy oraz mają systemy SVR4 i 4.3+BSD. Jest wiele dodatkowych, zależnych od implementacji zmiennych środowiskowych, których używają systemy SVR4 i 4.3+BSD. Standard ANSI C nie definiuje żadnych zmiennych środowiskowych. Tabela 7.1 Zmienne środowiskowe Standardy Zmienna

POSK.l •

HOME

7.9. Zmienne środowiskowe

217

możemy zmodyfikować ustawienia tylko w bieżącym procesie oraz w każdym z procesów potomnych. Nie mamy natomiast wpływu na środowisko procesu macierzystego, którym jest najczęściej powłoka. Mimo to metoda aktualizacji listy środowiska bywa bardzo przydatna). Niestety, nie wszystkie systemy wspierają taką możliwość. W tabeli 7.2 pokazujemy funkcje, które sa stosowane w różnych standardach i implementacjach. Tabela 7.2 Różne funkcje obsługujące środowisko Funkcja getenv

Standardy

Implementacje

ANSIC

POSK.l



putemr

XPG3

SVR4

4.3+BSD









(być może)







unseterw



clearenv

(być może)

1

Implementacje

XPG3

SVR4 4.3+BSD •

nazwa kategorii locale nazwa kategorii locale

LANG





LC ALL







LC COLLATE







wpis





katalog domowy

nazwa kategorii locale do sortowania znaków

W uzasadnieniach (Rationale) standardu POSK.l stwierdzono, że rozważa się dodanie funkcji putenv oraz clearenv.

Prototypy trzech funkcji, wymienionych w środkowych wierszach tab. 7.2 mają następującą postać:

LC CTYPE







nazwa kategorii locale do klasyfikacji znaków





#include < s t d l i b . h >

LCJ40NETARY



nazwa kategorii locale do edycji systemu pieniężnego

i n t putenv {const char *str) ;

LC NUMERIC







nazwa kategorii locale do edycji liczb

LC TIME







nazwa kategorii locale do formatowania czasu/daty



LOGNAME



^JLSPATH



setenv





nazwa logowania sekwencja wzorców dla katalogów z komunikatami

PATH









lista przedrostków nazw ścieżek do poszukiwania plików wykonywalnych

TERM









typ terminalu

rz









informacja o strefie czasowej

Standard FIPS 151-1 wymaga, by powłoka logowania definiowała zmienne środowiskowe HOME oraz LOGNAME.

Oprócz pobierania wartości zmiennej środowiskowej, chcielibyśmy móc ustalać jej nową wartość. Możemy zmienić wartość istniejącej zmiennej lub dodać nową zmienną do środowiska. (W kolejnym rozdziale zobaczymy, że

i n t setenv (const char *name, const char *value, i n t rewrite) ; Przekazują: 0, jeśli wszystko w porządku; wartość niezerową, jeśli wystąpił błąd void unsetenv(const char *name);

Działanie tych trzech funkcji jest następujące: • Funkcja putenv pobiera napis o postaci name=value i umieszcza go na liście środowiska. Jeśli nazwa zmiennej name już istnieje, to jest usuwana stara definicja. • Funkcja s e t e n v ustala wartość name równą yalue. Jeśli name już istnieje, to: (a) gdy argument rewrite jest niezerowy, wówczas jest usuwana istniejąca definicja nazwy name; (b) gdy argument rewrite jest równy 0, istniejąca definicja dla nazwy name nie jest usuwana {name nie otrzymuje nowej wartości value oraz nie pojawia się błąd). • Funkcja unsetenv usuwa definicję nazwy name. Jeśli taka definicja nie istnieje, to nie otrzymamy błędu.

7. Środowisko procesu w systemie Unix

Ciekawe wyniki daje sprawdzenie, jak działają te funkcje, gdy modyfikujemy listę środowiska. Powróćmy do rys. 7.3, z którego wynika, że lista środowiska (tablica wskaźników do napisów o postaci name=value) oraz napisy środowiska są typowo zapamiętane na początku przestrzeni adresowej procesu (powyżej stosu). Usunięcie napisu jest proste - znajdujemy odpowiedni wskaźnik w liście środowiska i przesuwamy wszystkie następne wskaźniki o jeden w dół. Niestety, dodanie nowego napisu albo zmodyfikowanie istniejącego jest trudniejsze. Obszar na szczycie stosu nie może zostać rozszerzony, ponieważ jest on często umieszczany na początku przestrzeni adresowej procesu. Taka lokalizacja uniemożliwia rozszerzenie go ani w górę, ani w dół, ponieważ nie można przesuwać elementów stosu. 1. Jeśli modyfikujemy istniejącą nazwę name: (a) Gdy rozmiar nowej wartości (yalue) nie jest większy niż rozmiar już istniejącej, wówczas możemy po prostu skopiować nowy napis na miejsce starego. (b) Gdy rozmiar nowej wartości (value) jest większy od starej, najpierw używamy funkcji malloc, by otrzymać przestrzeń dla nowego napisu, a następnie kopiujemy nowy napis do tego obszaru i zamiast starego wskaźnika dla nazwy name w liście środowiska umieszczamy wskaźnik do nowo zaalokowanego obszaru. 2. Jeśli dodajemy nową nazwę name, to sytuacja jest bardziej skomplikowana. Najpierw wywołujemy funkcję malloc, by zaalokować obszar dla napisu name=value, a następnie kopiujemy ten napis do tego miejsca. (a) Gdy po raz pierwszy dodajemy nowy napis do środowiska, musimy wywołać funkcję malloc, by otrzymać miejsce na nową listę wskaźników. Kopiujemy starą listę środowiska do tego nowego obszaru, a na końcu listy zapamiętujemy wskaźnik do napisu name=value. Naturalnie, na końcu tej listy umieszczamy również pusty wskaźnik. Na koniec zmieniamy wartość zmiennej erwiron, by wskazywała nową listę wskaźników. Zauważmy z rys. 7.3, że jeśli oryginalna lista środowiska była umieszczona powyżej szczytu stosu (tak na ogół jest), to teraz musimy przesunąć tę listę wskaźników do sterty. Jednak większość wskaźników z tej listy to nadal adresy napisów name=value, umieszczonych powyżej szczytu stosu. (b) Gdy kolejny raz dodajemy nowy napis do listy środowiska, wiemy, że mamy już zaalokowany obszar dla tej listy na stercie, więc wywołujemy tylko funkcję r e a l l o c , aby zaalokować przestrzeń dla kolejnego wskaźnika. Wskaźnik do nowego napisu name=value jest umieszczany na końcu listy (w miejscu przewidywanego pustego wskaźnika), a po nim jest wpisywany pusty wskaźnik.

7.10. Funkcje s e t jmp oraz longjmp

7.10

219

Funkcje s e t jmp oraz longjmp W języku C nie możemy użyć instrukcji goto z etykietą umieszczoną w innej funkcji. Aby zrealizować tego typu skok musimy zastosować funkcje s e t jmp i longjmp. Jak zobaczymy, obie funkcje są bardzo przydatne do obsługi błędów, które mogą się pojawić w programach, stosujących wielokrotnie zagnieżdżone wywołania funkcji. Przyjrzyjmy się szkieletowi próg. 7.3. W pętli głównej program czyta kolejny wiersz ze standardowego wejścia i wywołuje funkcję d o _ l i n e w celu przetworzenia danych. Zakładamy, że pierwszym elementem wiersza jest ustalona postać polecenia, a instrukcja switch wybiera odpowiedni fragment kodu obsługi. Pokazaliśmy tu tylko przykład jednego polecenia, dla którego jest wywoływana funkcja cmd_add.

iinclude

"ourhdr.h"

#define TOK_ADD 5 void void int

do_line(char * ) ; cmd_add(void) ; get_token(void);

int main(void) { char line[MAXLINE]; while (fgets(line, MAXLINE, stdin) != NULL) do_line(linę); exit(0); } char

*tok_ptr;

/* globalny wskaźnik dla funkcji get_token() */

void do_line(char *ptr) /* przetworzenie jednego wiersza danych wejściowych */ { int cmd; tok_ptr = ptr; while ( (cmd = get_token()) > 0) { switch (cmd) { /* dla każdego polecenia jedna instrukcja case */ case TOK_ADD: cmd_add(); break;

7. Środowisko procesu w systemie Unix add(void) int

token;

token = get_token(); /* pozostałe czynności związane z obsługą polecenia */

token(void) /* pobranie z wiersza następnego elementu, na który wskazuje tok_ptr */

221

7.10. Funkcje s e t jmp oraz longjmp

dzonego wiersza i powrócić do funkcji main w celu pobrania kolejnego wiersza. Jeśli jednak znajdujemy się wiele poziomów poniżej funkcji main, to w języku C nie jest łatwo taką czynność zrealizować. (W naszym przykładzie funkcja cmd_add leży tylko dwa poziomy poniżej funkcji main, ale nierzadko się zdarza, że chcemy cofnąć się od razu o pięć lub więcej poziomów w górę). Gdybyśmy musieli w każdej funkcji uwzględnić specjalny powrót z wartością wskazującą, że mamy dotrzeć do poziomu pierwszego, to nasz program byłby nienaturalnie skomplikowany. Idealnym rozwiązaniem jest użycie techniki nielokalnego skoku - funkcji setjmp oraz longjmp. Określenie „nielokalny" oznacza fakt, że nie realizujemy zwykłej instrukcji goto w ramach jednej funkcji, lecz przechodzimy wstecz do funkcji będącej w ścieżce wywołań, które doprowadziły do wywołania bieżącej funkcji.

Próg. 7.3 Typowy szkielet programu przetwarzającego polecenia #include

Program 7.3 obrazuje typowe podejście, polegające na odczycie polecenia, jego identyfikacji i wywołaniu odpowiedniej funkcji obsługi. Na rysunku 7.4 pokazujemy przybliżoną postać stosu po wywołaniu funkcji cmd a d d . szczyt stosu

Przekazuje: 0. jeśli była wywołana bezpośrednio; wartość niezerową, jeśli była wywołana przez funkcję longjmp v o i d longjmp (jmp_buf env,

ramka stosu funkcji main ramka stosu funkcji do l i n ę

kierunek wzrostu stosu

int setjmp{jmp_buf env) ;

ramka stosu funkcji cmd add

Rys. 7.4 Ramki stosu po wywołaniu funkcji cmd_add

Zmienne automatyczne są umieszczone na stosie w ramce odpowiadającej danej funkcji. Tablica l i n ę jest zlokalizowana w ramce stosu dla funkcji main, wartość całkowitoliczbowa cmd leży w ramce stosu funkcji d o _ l i n e , a zmienna całkowita token należy do ramki stosu funkcji cmd_add. Jak już raz powiedzieliśmy, taka organizacja stosu jest typowa, ale nie wymagana. Stosy nie muszą koniecznie rosnąć w kierunku niższych adresów pamięci. W systemach, które nie mają wbudowanej obsługi stosu, implementacje w języku C mogą do przechowywania poszczególnych ramek stosu używać list powiązanych. Często powtarzającym się problemem w programach podobnych do próg. 7.3 jest sposób obsługi niekrytycznych błędów. Na przykład funkcja c m d a d d po wykryciu błędnej sytuacji, w której np. podano niewłaściwą liczbę, może wydrukować komunikat o błędzie, zignorować resztę wprowa-

i n t vał) ;

Funkcję setjmp wywołujemy z miejsca, do którego chcemy powrócić. W naszym przykładzie jest to funkcja main. Funkcja setjmp przekazuje w tym przypadku wartość 0, ponieważ wywołaliśmy ją bezpośrednio. Argument env w wywołaniu setjmp ma specjalny typ danych, jmp_buf. Jest on pewną formą tablicy, w której są przechowywane wszystkie informacje potrzebne do odtworzenia stanu stosu, gdy wywołamy funkcję longjmp. Zazwyczaj zmienna env jest zmienną globalną, gdyż chcemy odwoływać się do niej z innej funkcji. Gdy wykryjemy błąd, np. w funkcji cmd_add, to wywołujemy funkcję longjmp z dwoma argumentami. Pierwszym argumentem jest zmienna env, której użyliśmy w wywołaniu setjmp, a drugim val o wartości niezerowej ten argument stanie się wartością zwracaną z funkcji setjmp. Dodanie drugiego argumentu do funkcji longjmp umożliwia wprowadzenie więcej niż jednego wywołania longjmp dla każdej funkcji setjmp. Możemy na przykład w funkcji cmd_add wykonać longjmp z wartością val równą 1, a z funkcji from_token z wartością val równą 2. Funkcja setjmp może więc przekazać z funkcji głównej albo 1, albo 2, a sprawdzając otrzymaną wartość dowiadujemy się (jeśli chcemy), skąd była wywołana funkcja l o n g jmp, z funkcji cmd_add czy z f rom_token. Powróćmy do naszego przykładu. Program 7.4 zawiera funkcje main oraz cmd_add. (Pozostałe dwie funkcje, d o _ l i n e oraz get_token, nie uległy zmianie).

7. Środowisko procesu w systemie Unix nclude nclude

dające ich stanom z poprzedniego wywołania setjmp (czyli czy zostaną odtworzone), czy pozostają takie, jakie były podczas wywołania funkcji do l i n ę (która wywołała funkcję cmd_add, a ta z kolei doprowadziła do wywołania longjmp). Niestety, musimy odpowiedzieć „to zależy". Większość implementacji nie próbuje odtwarzać zmiennych automatycznych oraz rejestrowych, a standardy mówią jedynie, że wartości tych zmiennych są nieokreślone. Jeśli więc mamy zmienną automatyczną, a nie chcemy, by była odtwarzana, to musimy zdefiniować ją z atrybutem v o l a t i l e . Zmienne zadeklarowane jako globalne lub statyczne pozostają w trakcie wywołania longjmp bez zmian.

"ourhdr.h"

efine TOK_ADD

5

p buf jmpbuffer; t in(void) char linę[MAXLINE]; (setjmp(jmpbuffer) != 0) printf("error"); while ( f g e t s ( l i n e , MAXLINE, stdin) do_line(linę); exit (0) ;

223

7.10. Funkcje setjmp oraz longjmp

if

!= NULL)

id i add(void) int

Przykład W programie 7.5 możemy zaobserwować, że w wyniku wywołania funkcji longjmp mogą się pojawić różne wartości zmiennych automatycznych, rejestrowych oraz ulotnych. Jeśli skompilujemy i uruchomimy nasz program, raz stosując optymalizację kompilatora, a raz bez optymalizacji, to otrzymamy różne wyniki: $ cc t e s t jmp. c

token;

token = get_token() ; if (token < 0) /* pojawił s i ę błąd */ longjmp(jmpbuffer, 1); /* pozostałe czynności związane z obsługą polecenia */

kompilacja bez żadnej optymalizacji

$ a.out in f 1 ( ) : count = 97, val = 98, sum = 99 a f t e r longjmp: count = 97, val = 98, sum = 99 $ cc -O t e s t jmp. c $ a.out in f l ( ) : c o u n t = 97,

kompilacja z pełną optymalizacją v a l = 98,

sum = 99

after longjmp: count = 2, val = 3, sum = 99

Próg. 7.4 Przykłady funkcji s e t jmp i longjmp

Gdy wykonujemy funkcję main, wywołanie setjmp rejestruje w zmiennej jmpbuffer wszystkie informacje, które są potrzebne do powrotu w to miejsce i przekazuje wartość 0. Następnie wywołujemy funkcję d o _ l i n e , która z kolei woła funkcję cmd_add. Załóżmy, że pojawia się jakiś błąd. Na rysunku 7.4 pokazujemy, jak wygląda stos przed wywołaniem funkcji longjmp w funkcji cmd add. Funkcja longjmp powoduje, że stos zostaje odtworzony do postaci odpowiadającej wykonaniu funkcji main, zostają usunięte ramki stosu dotyczące funkcji cmd_add oraz d o _ l i n e . W wyniku wywołania funkcji longjmp w funkcji main powraca funkcja setjmp, ale tym razem przekazuje wartość 1 (równąwartości drugiego argumentu funkcji longjmp). ienne automatyczne, rejestrowe oraz ulotne Pojawia się kolejne pytanie: jakie są wartości zmiennych automatycznych oraz rejestrowych w funkcji main? Jeśli do funkcji main wracamy za pomocą wywołania longjmp, to ciekawi nas, czy te zmienne mają wartości odpowia-

Zauważmy, że zmienna zadeklarowana z atrybutem v o l a t i l e (sum) nie zależy od optymalizacji, jej wartość po wywołaniu funkcji longjmp odpowiada wartości ostatnio przypisanej tej zmiennej. W jednym z systemów na stronie setjmp(3) dowiadujemy się, że wszystkie zmienne zapamiętane w pamięci będą miały wartości, jakie mają w chwili ostatniego wywołania funkcji longjmp, natomiast zmienne zlokalizowane w procesorze oraz rejestry | zmiennopozycyjne zostaną odtworzone i uzyskują wartości odzwierciedlające ich stan w chwili wywołania setjmp. Dokładnie to widzimy, gdy uruchamiamy próg. 7.5. Bez optymalizacji wszystkie trzy zmienne są umieszczone w pamięci (czyli jest ignorowany atrybut r e g i s t e r dla zmiennej val). Gdy włączymy optymalizację, wówczas zmienne count i v a l są umieszczane w rejestrach (mimo że ta ostatnia zmienna nie miała atrybutu r e g i s t e r ) , • a zmienna z atrybutem v o l a t i l e pozostaje w pamięci. Jako wniosek z tego i przykładu zapamiętajmy, że musimy stosować atrybut v o l a t i l e , gdy przy- . gotowujemy przenośny kod używający nielokalnych skoków. Każda inna deklaracja może w różnych systemach spowodować niespodziewane zmiany wartości. •

7. Środowisko procesu w systemie Unix iclude iclude

7.11. Funkcje getrlimit oraz setrlimit

ływać po powrocie z funkcji deklarującej tę zmienną. W podręcznikach Uniksa jest wiele ostrzeżeń na ten temat. Program 7.6 jest funkcją o nazwie o p e n d a t a , która otwiera standardowy strumień wejścia-wyjścia i ustala sposób buforowania dla strumienia.

"ourhdr.h"

itic void f l ( i n t , i n t , itic void f2(void); itic jmp_buf

int)

jmpbuffer;

#include

#define DATAFILE

n (void)

"datafile"

FILE * open_data(void)

int count; register int val; volatile int sum; count = 2; val = 3; sum = 4; if (setjmp(jrapbuffer) != 0) { printf("after longjmp: count count, val, sum); exit(0);

{ FILE *fp; char databuf [BUFSIZ]; /* po wywołaniu setvbuf bufor standardowego we-wy */

= %d, val = %d, sum = %d\n"

count = 97; val = 98; sum = 99; /* zmieniane po setjmp, przed longjmp */ fl(count, val, s u m ) ; /* nigdy nie powraca */

if ( (fp = fopen(DATAFILE, "r")) == NULL) return(NULL); if (setvbuf(fp, databuf, _IOLBF, BUFSIZ) != 0) return(NULL); return(fp);

/* błąd */

Próg. 7.6

tic void int i, int j , int k)

c void roid) longjmp(jmpbuffer, 1 ) ;

Wpływ funkcji l o n g j m p na zmienne automatyczne, rejestrowe oraz zmienne z kwalifikatorem v o l a t i l e

Do tych dwóch funkcji, setjmp oraz longjmp, powrócimy jeszcze w rozdz. 10, gdy będziemy omawiać obsługę sygnałów i, przy okazji, wersje tych funkcji dla sygnałów s i g s e t jmp oraz siglongjmp. ncjalny problem ze zmiennymi automatycznymi Gdy wiemy już, w jaki sposób są obsługiwane ramki na stosie, warto przyjrzeć się potencjalnym błędom w obsłudze zmiennych automatycznych. Podstawowa zasada mówi, że do zmiennej automatycznej nie możemy się odwo-

Niepoprawne zastosowanie zmiennej automatycznej

Problem, który ujawnia się w tym programie, jest spowodowany obowiązującą zasadą sprawiającą, że gdy funkcja open_data powraca, przestrzeń, z której korzystała na stosie, jest ponownie używana jako ramka stosu kolejnej wywołanej funkcji. Jednocześnie standardowa biblioteka wejścia-wyjścia nadal będzie używać tego fragmentu pamięci jako bufora dla strumienia. Powstaje trudny do opanowania bałagan. Aby nie dopuścić do takiej sytuacji, tablica databuf musi być zaalokowana z puli pamięci globalnej albo statycznej ( s t a t i c lub extern), albo dynamicznej (za pomocą jednej z funkcji serii a l l o c ) .

printf ("in f 1 () : count = %d, va,l = %d, sum = id\n", i, j, k) f 2 () ;

Próg. 7.5

225

7.11

Funkcje getrlimit oraz s e t r l i m i t Każdy proces ma ustalone ograniczenia dotyczące zasobów systemowych. • Pewne wartości ograniczeń możemy pobrać oraz zmienić za pomocą funkcji getrlimit i setrlimit. #include ttinclude int getrlimit (int resource, struct rlimit *rlptr) ; int setrlimit (int resource, const struct rlimit *rlptr) ; Przekazują: 0, jeśli wszystko w porządku; wartość niezerową, jeśli wystąpił błąd

7. Środowisko procesu w systemie Unix

Każde wywołanie tych funkcji przekazuje jako argument pojedynczy zasób (resource) oraz wskaźnik do następującej struktury: struct rlimit { rlim_t rlim_cur; rlim t rlim max;

7.11. Funkcje getrlimit oraz setrlimit

RLIMIT_MEMLOCK RLIMIT_NOFILE

bieżące ograniczenie */ sztywne ograniczenie: maksymalna wartość dla rlim cur */

RLIMIT NPROC

Obie funkcje nie są częścią normy POSIX.l, ale są zaimplementowane w systemach SVR4 oraz 4.3+BSD. Dla pól w pokazanej strukturze system SVR4 używa elementarnego typu danych r l i m _ t . W innych systemach oba pola są deklarowane jako i n t lub long i n t . Ograniczenia zasobów dla procesu są zazwyczaj określane przez proces o identyfikatorze 0 podczas inicjowania systemu, a następnie dziedziczą je kolejne procesy w systemie. W systemie SVR4 wartości domyślne możemy odczytać z pliku /etc/conf/cf. d/mtune. W systemie 4.3+BSD wartości domyślne są rozrzucone między różne pliki nagłówkowe.

Zmianą ograniczeń zasobów rządzą trzy reguły: 1. Każdy proces może zmodyfikować bieżące ograniczenie, pod warunkiem, że nowa wartość będzie nie większa niż sztywne ograniczenie. 2. Każdy proces może zmniejszyć wartość sztywnego ograniczenia do wartości nie mniejszej niż bieżące ograniczenie. Zmniejszenie sztywnego ograniczenia jest dla zwykłego użytkownika nieodwracalne. 3. Tylko nadzorca może zwiększyć sztywne ograniczenie. Nieokreślona wartość ograniczenia jest wskazywana za pomocą stałej RLIM_INFINITY.

Argument resource przyjmuje jedną z poniższych wartości. Zwróćmy uwagę, że nie wszystkie zasoby są zaimplementowane w obu systemach, SVR4 oraz 4.3+BSD. RLIMIT CORE

RLIMIT CPU

RLIMIT DATA

RLIMIT FSIZE

(SVR4 i 4.3+BSD) Maksymalny rozmiar pliku będącego obrazem systemu (core) w bajtach. Zerowa wartość ograniczenia zabrania tworzenia pliku core. (SVR4 i 4.3+BSD) Maksymalny czas procesora w sekundach. Gdy zostanie przekroczone bieżące ograniczenie, wówczas proces odbierze sygnał SIGXCPU. (SVR4 i 4.3+BSD) Maksymalny rozmiar segmentu danych w bajtach. Jest to suma rozmiaru danych zainicjowanych, niezainicjowanych oraz sterty z rys. 7.3. (SVR4 i 4.3+BSD) Maksymalny rozmiar w bajtach pliku, który może utworzyć proces. Gdy zostanie przekroczone bieżące ograniczenie, wówczas proces odbierze sygnał SIGXFSZ.

RLIMIT OFILE RLIMIT RSS

RLIMIT_STACK RLIMIT VMEM

227

(tylko 4.3+BSD) Adres zaryglowanej przestrzeni pamięci (opcja jeszcze niezaimplementowana). (tylko SVR4) Maksymalna liczba otwartych plików w jednym procesie. Zmiana tego ograniczenia ma wpływ na wartość uzyskiwaną z funkcji sysconf, gdy podajemy argument _SC_OPEN_MAX (p. 2.5.4). Patrz próg. 2.3. (tylko 4.3+BSD) Maksymalna liczba procesów potomnych uruchomionych z jednym rzeczywistym identyfikatorem użytkownika. Zmiana tego ograniczenia ma wpływ na wartość uzyskiwaną z funkcji sysconf, gdy podajemy argument _SC_CHILD_MAX (p. 2.5.4). (4.3+BSD) To samo co ograniczenie R L I M I T _ N O F I L E w systemie SVR4. (tylko 4.3+BSD) Maksymalny rozmiar w bajtach obszaru rezydentnego (RSS). Jeżeli brakuje pamięci fizycznej, jądro pobiera pamięć od procesów, które przekraczają swój rozmiar RSS. (SVR4 i 4.3+BSD) Maksymalny rozmiar stosu w bajtach. Patrz rys. 7.3. (tylko SVR4) Maksymalny rozmiar przestrzeni odwzorowania adresów w bajtach. Ta wartość ma wpływ na funkcję mmap (podrozdz. 12.9).

Zmiana ograniczeń zasobów jest realizowana w procesie wywołującym funkcję, a nowe wartości są dziedziczone przez wszystkie procesy potomne. Oznacza to, że w rzeczywistości ustalenie nowych wartości ograniczeń powinno być wbudowane w powłoki, by wartości te były uwzględniane we wszystkich przyszłych procesach. W istocie, powłoka Bourne'a oraz KornShell mają wbudowane polecenie u l i m i t , a powłoka C ma polecenie l i m i t . (Funkcje umask oraz c h d i r również mogą być obsługiwane jako polecenia wbudowane). Starsze powłoki Bourne'a, jak np. powłoka dostarczana w systemie z Berkeley, nie mają polecenia u l i m i t . Nowsze wersje KornShella mają nieudokumentowane opcje -H oraz -S w poleceniu u l i m i t , służące odpowiednio do sprawdzania i modyfikacji ograniczeń bieżących.

Przykład Program 7.7 drukuje dla każdego ograniczenia zasobów w systemie dwie wartości: bieżące ograniczenie zmienne oraz sztywne ograniczenie. Abyśmy mogli uruchomić ten program w obu systemach: SVR4 oraz 4.3+BSD, niektóre nazwy zasobów sąwkompilowane warunkowo.

;8

7. Środowisko procesu w systemie Unix

nclude nclude nclude



nclude

"ourhdr.h"

efine doit(name) pr_limits (ttname, name) atic void pr_limits(char *, i n t ) ;

I

229

7.12. Podsumowanie

W makrze d o i t zastosowaliśmy nowy w standardzie ANSI C operator do tworzenia napisów (#), by dla każdej nazwy zasobu wygenerować wartość napisu. Gdy napiszemy: doit(RLIMIT_CORE);

to preprocesor C utworzy instrukcję: pr_limits("RLIMIT_CORE", RLIMIT_CORE);

t in(void)

Oto wynik uruchomienia tego programu w systemie SVR4.

doit(RLIMIT_ CORE); doit(RLIMIT__CPU) ; doit(RLIMIT_ DATA); doit(RLIMIT_ FSIZE); fdef RLIMIT_MEMLOCK doit (RLIMIT_ MEMLOCK); ndif fdef RLIMIT NOFILE /* nazwa SVR4 * doit(RLIMIT_ NOFILE); ndif fdef RLIMITJDFILE /* nazwa 4.3+BSD doit(RLIMIT_JDFILE) ; ndif fdef RLIMIT_NPROC doit(RLIMIT__NPROC) ; ndif fdef RLIMIT_RSS doit(RLIMIT__RSS) ; ndif doit(RLIMIT STACK); fdef RLIMIT_VMEM doit(RLIMIT VMEM); ndif exit (0); atic void limits(char 'name, int resource) struct rlimit limit; if (getrlimit(resource, Slimit) < 0) err_sys("getrlimit error for % s " , name) printf("%-14s ", name); if (limit.rlim_cur == RLIM_INFINITY) printf("(infinite) "); else printf("%101d ", limit.rlim_cur); if (limit.rlim_max == RLIM_INFINITY) printf("(infinite)\n"); else printf("%101d\n", limit.rlim_max); Próg. 7.7 Wydruk bieżących ograniczeń zasobów

$ a.out RLIMIT_CORE RLIMIT_CPO RLIMIT_DATA RLIMIT_FSIZE RLIMIT NOFILE RLIMIT_STACK RLIMIT_VMEM

1048576 (infinite) 16777216 2097152 64 16777216 16777216

1048576 (infinite) 16777216 2097152 1024 16777216 16777216

'stemie 4.3+BSD otrzymujemy: $ a.out RLIMIT_CORE RLIMIT CPU RLIMIT_DATA RLIMIT FSIZE RLIMIT MEMLOCK RLIMITJDFILE RLIMIT_NPROC RLIMIT RSS RLIMIT STACK

(infinite) (infinite) 8388608 (infinite) (infinite) 64 40 27070464 524288

(infinite) (infinite) 16777216 (infinite) (infinite) (infinite) (infinite) 27070464 16777216



Po omówieniu sygnałów jako dalszy ciąg dyskusji o ograniczeniach zasobów wykonamy ćw. 10.11.

7.12 Podsumowanie Dobre zrozumienie środowiska programów w języku C pracujących w Uniksie jest niezbędne, gdy chcemy zgłębić zagadnienie sterowania procesami w systemie Unix. W tym rozdziale prześledziliśmy, jak startuje proces, jak się kończy i jak są mu przekazywane listy argumentów oraz środowisko. Mimo że obie listy nie są interpretowane przez jądro systemu, to właśnie jądro pośredniczy w przekazaniu tych danych między procesem wywołującym funkcję exec a nowym procesem. Przyjrzeliśmy się również typowemu rozłożeniu w pamięci gotowego do wykonania programu przygotowanego w języku C i pokazaliśmy, jak proces może dynamicznie alokować pamięć, a następnie ją zwalniać. Cennym doświadczeniem była szczegółowa analiza dostępnych funkcji umożliwiających manipu-

7. Środowisko procesu w systemie Unix

8

lowanie środowiskiem, gdyż wiązało się to z alokacją pamięci. Zaprezentowaliśmy funkcje setjmp oraz longjmp, pokazując sposób wykonania nielokalnych skoków w programie. Zakończyliśmy rozdział opisem ograniczeń dotyczących zasobów procesu, które obowiązują w systemach SVR4 oraz 4.3+BSD.

Sterowanie procesem

Ćwiczenia 7.1

Gdy w systemach SVR4 oraz 4.3+BSD, pracujących na procesorze 80386, wykonamy program, który wypisuje komunikat „hello, worid" i przed zakończeniem nie wywołuje ani exit, ani return, wówczas stan zakończenia programu (który możemy sprawdzić w powłoce) jest równy 13. Dlaczego? 7.2 Kiedy odbywa się rzeczywiste wypisanie danych, które są wyprowadzane przez funkcje printf w próg. 7.1? 7.3 Czy funkcja, która została wywołana z funkcji main, może w jakiś sposób przeanalizować argumenty wiersza poleceń, jeśli (a) nie przekażemy argc i argv jako argumentów dostarczanych z funkcji main do nowej funkcji ani (b) nie wykonaliśmy w funkcji main kopii argc oraz argv do zmiennych globalnych? 7.4 Niektóre implementacje Uniksa celowo zabraniają korzystania w wywoływanych programach z lokalizacji o adresie 0 w segmencie danych. Dlaczego? 7.5 Użyj dyrektywy języka C typedef do zdefiniowania nowego typu danych Exitfunc dla procedury obsługi zakończenia. Zmień prototyp funkcji atexit, by korzystał z tego typu danych. 7.6 Jeżeli za pomocą wywołania caiioc zaalokujemy tablicę liczb typu long, to czy elementy tej tablicy mają wartości początkowe równe 0? Gdy używając funkcji caiioc zaalokujemy tablicę wskaźników, wówczas czy poszczególne jej elementy są pustymi wskaźnikami? 7.7 Dlaczego w wydruku uzyskanym z polecenia size, który pokazaliśmy na końcu podrozdz. 7.6, nie są podane żadne wartości dla stosu i sterty? 7.8 Rozmiary dwóch plików podane w podrozdz. 7.7 (104859 i 24576) nie są równe sumom rozmiarów ich segmentów danych oraz tekstu. Dlaczego? 7.9 Dlaczego w przykładzie z podrozdz. 7.7 otrzymaliśmy w tak prostym programie znaczną różnicę w rozmiarze pliku wykonywalnego, gdy używamy biblioteki wspólnej? 7.10 Na końcu podrozdz. 7.10 pokazaliśmy, że funkcja nie może przekazywać wskaźnika w zmiennej automatycznej. Czy poniższy kod jest poprawny? int

fl(int val) int if (val int val ptr

*ptr; == 0) { val; = 5; = &val;

return( *ptr + 1);

8.1

Wprowadzenie Teraz zajmiemy się sposobami sterowania procesami oferowanymi przez system Unix. Zagadnienie to obejmuje tworzenie nowych procesów, wykonywanie programów oraz zakończenie procesu. Przyjrzymy się też różnym identyfikatorom, które są własnością procesu (identyfikatory: rzeczywisty, obowiązujący i zapamiętany; użytkownika i grupy) i dowiemy się, jak wpływają na nie różne narzędzia służące do sterowania procesami. Omówimy pliki interpretowane (skrypty) oraz funkcję system. Zakończymy ten rozdział przeglądem technik rejestrowania przebiegu procesów, które są dostarczane przez większość systemów uniksowych. Dzięki temu będziemy mogli spojrzeć z innej perspektywy na funkcje sterujące procesami.

8.2

Identyfikatory procesu Każdy proces ma unikatowy identyfikator procesu, będący nieujemną liczbą całkowitą. Ponieważ identyfikator procesu jest jedyną ogólnie znaną nazwą procesu, więc właśnie on jest często używany jako fragment innych identyfi-' katorów, aby zagwarantować jednoznaczność. Funkcja tmpnam z podrozdz. 5.13 tworzyła niepowtarzalne nazwy ścieżek, włączając do nazwy identyfikator procesu. W systemie są pewne specjalne procesy. Proces o identyfikatorze 0 to zazwyczaj zarządca, nazywany często demonem wymiany (swapper). Proces ten nie ma swojego programu na dysku, jest on częścią jądra systemu, dlatego bywa nazywany procesem systemowym. Identyfikator 1 jest zazwyczaj przypisany procesowi inicjującemu (init process), wywoływanemu przez jądro na koniec procedury wstępnego ładowania systemu (bootstrap). W starszych

8. Sterowanie procesem wersjach Uniksa plik z programem tego procesu miał nazwę ścieżki / e t c / i n i t , w nowszych - / s b i n / i n i t . Proces ten jest odpowiedzialny za uruchomienie systemu po jego wstępnym załadowaniu. Proces i n i t zazwyczaj czyta zależne od systemu pliki inicjujące (pliki / e t c / r c * ) i wprowadza system na określony poziom pracy (np. do trybu wieloużytkowego). Proces i n i t nigdy nie ginie. Jest normalnym procesem użytkowym (nie procesem systemowym w jądrze, jak demon wymiany), ale pracuje z uprawnieniami nadzorcy. Dalej w tym rozdziale zobaczymy, że proces i n i t staje się procesem macierzystym wszystkich osieroconych procesów potomnych. W niektórych implementacjach systemu Unix z pamięcią wirtualną proces o identyfikatorze 2 jest demonem stronicowania (pagedaemoń). Proces ten zapewnia stronicowanie w systemie korzystającym z pamięci wirtualnej. Podobnie jak demon wymiany, demon stronicowania jest procesem w jądrze systemu. Oprócz identyfikatora procesu każdy proces dysponuje innymi identyfikatorami. Do ich pobrania służą następujące funkcje. t t i n c l u d e < s y s / t y p e s . h> fłinclude < u n i s t d . h > pid_t getpid(void); pid_t getppid(void); uid t g e t u i d ( v o i d ) ;

Przekazuje: identyfikator procesu wywołującego funkcję Przekazuje: identyfikator procesu macierzystego procesu wywołującego funkcję Przekazuje: rzeczywisty identyfikator użytkownika procesu wywołującego funkcję

uid_t geteuid(void);

Przekazuje: obowiązujący identyfikator użytkownika procesu wywołującego funkcję

gid_t g e t g i d ( v o i d ) ;

Przekazuje: rzeczywisty identyfikator grupy procesu wywołującego funkcję

gid_t getegid(void);

Przekazuje: obowiązujący identyfikator grupy procesu wywołującego funkcję

Zwróćmy uwagę, że żadna z tych funkcji nie przekazuje komunikatów o błędzie. O identyfikatorze procesu macierzystego będziemy jeszcze mówić w następnym podrozdziale podczas prezentowania funkcji fork. Identyfikatory rzeczywisty i obowiązujący użytkownika oraz grupy omówiliśmy w podrozdz. 4.4.

Funkcja f o r k Jądro systemu Unix ma tylko jeden sposób, by utworzyć nowy proces: istniejący proces musi wywołać funkcję fork. (Nie dotyczy to procesów specjalnych, o których mówiliśmy w poprzednim podrozdziale, czyli demona wymiany, procesu i n i t oraz demona stronicowania. Te procesy są tworzone przez jądro w trakcie wstępnego ładowania systemu).

8.3. Funkcja fork

233

#include tinclude pid_t fork(void); Przekazuje: 0 do procesu potomnego, identyfikator procesu potomnego do procesu macierzystego; - 1 , jeśli wystąpił błąd

Nowo utworzony przez funkcję fork proces jest nazywany procesem potomnym. Funkcja ta jest wywoływana raz, ale powraca dwukrotnie. Jedyną różnicą w obu tych powrotach jest przekazywana wartość: proces potomny otrzymuje wartość 0, a proces macierzysty odbiera identyfikator nowego procesu potomnego. Proces macierzysty musi odebrać identyfikator procesu potomnego, ponieważ zdarza się, że ma więcej niż jeden proces potomny, a brakuje funkcji, które umożliwiałyby pobranie identyfikatorów wszystkich procesów potomnych danego procesu. Funkcja fork przekazuje 0 do procesu potomnego, ponieważ proces potomny ma jeden proces macierzysty i zawsze może wywołać funkcję getppid, by pobrać identyfikator procesu macierzystego. (Proces o identyfikatorze 0 jest zawsze używany przez demona wymiany, a więc żaden inny proces nie może mieć zerowego identyfikatora). Oba procesy, macierzysty i potomny, kontynuują realizację zgodnie z instrukcjami, które są umieszczone po wywołaniu fork. Proces potomny jest kopią macierzystego. Proces potomny otrzymuje z procesu macierzystego kopię obszaru danych, sterty i stosu. Pamiętajmy, że proces potomny korzysta z kopii, czyli nie ma mowy o współużytkowaniu tych obszarów. Często natomiast procesy macierzysty i potomny traktują jako wspólny segment tekstu (podrozdz. 7.6), gdyż jest on używany w trybie tylko do odczytu. Wiele obecnych implementacji nie wykonuje pełnej kopii danych procesu macierzystego, stosu oraz sterty, ponieważ bardzo często natychmiast po wywołaniu fork jest wykonywana funkcja exec. Zamiast tego jest używana technika kopiowania przy zapisie (copy-on-write, CO W). W tej sytuacji wymienione obszary są wspólne dla procesów macierzystego i potomnego, a jądro systemu ustala dla nich tryb tylko do odczytu. Jeżeli jakiś proces próbuje modyfikować dane w jednym z tych obszarów, to dopiero wówczas jądro wykonuje kopię, ale nie pełną, lecz aktualizowanego fragmentu pamięci - zazwyczaj jest kopiowana tylko jednostka zwana stroną w systemie pamięci wirtualnej. • W podrozdziale 9.2 książki Bacha, 1986 [15] oraz w podrozdz. 5.7 książki Lefflera i in., 1989 [32] można znaleźć więcej szczegółów na ten temat.

Przykład Działanie funkcji fork widzimy w próg. 8.1. Wykonując go, otrzymamy: $ a.out a write to stdout before fork

8. Sterowanie procesem pid = 430, glob = 7, pid = 429, glob = 6, $ a.out > temp.out $ cat temp.out a write to stdout before fork pid = 432, glob = 7, before fork pid = 431, glob = 6,

var = 89 var = 88

zmienne potomka uległy zmianie kopia w procesie macierzystym bez zmian

var = 89 var = 88

W zasadzie nigdy nie wiemy, czy proces potomny zaczyna pracę przed macierzystym, czy odwrotnie. Wszystko zależy od algorytmu planowania stosowanego przez jądro systemu. Jeśli wymaga się, by procesy macierzysty i potomny pracowały w sposób zsynchronizowany, to jest niezbędna jakaś forma komunikacji międzyprocesowej. W programie 8.1 proces macierzysty po prostu przysypia na 2 sekundy, aby w tym czasie potomek wykonał swoje zadania. Oczywiście nie mamy gwarancji, że jest to poprawne podejście. W podrozdziale 8.8 będziemy jeszcze mówić o tym przykładzie oraz o innych rodzajach synchronizacji, gdy zajmiemy się sytuacją wyścigu. W podrozdziale 10.16 pokażemy, jak po wywołaniu fork można synchronizować procesy macierzysty i potomny za pomocą sygnałów. iclude iclude

"ourhdr.h"

; glob = 6; /* zewnętrzna zmienna w danych zainicjowanych */ ir buf[] = "a write to stdout\n"; • .n (void) int var; /* zmienna automatyczna na stosie */ pid_t pid; var = 88; if (write(STDOUT_FILENO, buf, sizeof(buf)-1) != sizeof(buf)-1) err_sys("write error"); printf("before fork\n"); /* nie opróżniamy strumienia stdout */ if ( (pid = fork()) < 0) err_sys("fork error") else if (pid == 0) { /* potomek */ glob++; /* modyfikacja zmiennych var++; } else sleep(2); /* proces macierzysty */ printf("pid = %d, glob = %d, var = %d\n", getpid() , glob, var); exit(0);

Próg. 8.1 Przykład użycia funkcji f o r k

8.3. Funkcja fork

235

Zwróćmy uwagę na interakcję funkcji fork z funkcjami obsługi wejścia-wyjścia w próg. 8.1. Przypominamy, że zgodnie z rozdz. 3 funkcja w r i t e jest niebuforowana. Ponieważ wywołaliśmy funkcję w r i t e przed funkcją fork, dane ukazały się raz na standardowym wyjściu. Standardowa biblioteka wejścia-wyjścia stosuje natomiast buforowanie. W podrozdziale 5.12 powiedzieliśmy, że standardowe wyjście jest buforowane wierszami, jeśli jest powiązane z urządzeniem terminala, w przeciwnym razie jest w pełni buforowane. Gdy uruchomimy ten program interakcyjnie, wówczas otrzymamy na strumieniu wyjściowym kopię jednego wiersza wyprowadzonego przez funkcję p r i n t f , ponieważ bufor został opróżniony, gdy wpisano do niego znak nowego wiersza. Jeśli przekierujemy standardowe wyjście do pliku, to otrzymamy dwie kopie wiersza wyprowadzonego przez p r i n t f . Łatwo można wyjaśnić tę sytuację. Funkcja p r i n t f była wprawdzie wywołana przed wywołaniem fork tylko raz, ale w trakcie wywołania fork wiersz był w buforze. Ponieważ przestrzeń danych procesu macierzystego jest kopiowana do procesu potomnego, więc bufor biblioteki wejścia-wyjścia został również skopiowany. W efekcie, oba procesy, macierzysty i potomny są, właścicielami bufora zawierającego wiersz danych. Drugie wywołanie funkcji p r i n t f , tuż przed wywołaniem e x i t , tylko dołącza dane do istniejącego bufora. Dopiero podczas zakończenia pracy każdego procesu są opróżniane wszystkie bufory. • Współdzielenie plików Jak wynika z próg. 8.1, gdy przekierujemy standardowe wyjście procesu macierzystego, wówczas również jest przekierowane standardowe wyjście potomka. Rzeczywiście, jedna z własności funkcji fork mówi, że wszystkie deskryptory otwarte w procesie macierzystym są powielane do procesu potomnego. Celowo mówimy „powielone", ponieważ wygląda to dokładnie tak, jakby wywołano funkcję dup dla każdego deskryptora. Odpowiednie deskryptory procesów macierzystego i potomnego odwołują się do jednej pozycji w tablicy plików (przypominamy rys. 3.3). Rozważmy proces, który ma otwarte trzy różne pliki jako standardowe wejście, standardowe wyjście oraz standardowy strumień komunikatów awaryjnych. Na rysunku 8.1 widzimy wzajemne przyporządkowania ustalone w chwili powrotu z funkcji fork. Ważne jest, że proces macierzysty i jego potomek używają wspólnej bieżącej pozycji w pliku. Przyjmijmy, że proces realizuje fork i tworzy swojego potomka, a następnie oczekuje (wait) na zakończenie pracy potomka. Za- łóżmy, że oba procesy w wyniku przetwarzania danych zapisują dane na stan-1 dardowym wyjściu. Jeśli proces macierzysty przekierował swoje standardowe wyjście (np. za pomocą powłoki), to jest bardzo istotny fakt, że wskaźnik bie-' żącej pozycji w pliku procesu macierzystego jest aktualizowany przez potomka, gdy ten zapisuje dane na standardowe wyjście. W tym przypadku po-

8. Sterowanie procesem wpis w tabl. dot. procesu macierzystego

tablica plików sygnał, stanu pliku bież. poz. w pliku

fd 0: fd 1fd2:

fd wskaźnik

wskaźnik do v-węzła >—,

••

tablica v-wę_złów dane dot. v-wczła dane dot. i-wązła bieżący rozmiar pliku

sygnał, stanu pliku bież. poz. w pliku wskaźnik do v-węzła

wpis w tabl. dot. procesu potomnego

sygnał, stanu pliku

dane dot. v-węzła dane dot. i-węzła bieżący rozmiar pliku

bież. poz. w pliku fd(V fdl:

fd wskaźnik i

ta 2: ••



wskaźnik do v-węzła

dane dot. v-węzła dane dot. i-węzła bieżący rozmiar pliku

Rys. 8.1 Współdzielenie otwartych plików przez procesy macierzysty i potomny po wywołaniu funkcji f o r k

tomek może pisać na standardowe wyjście, podczas gdy proces macierzysty oczekuje na zakończenie pracy potomka, a gdy to nastąpi, proces macierzysty może kontynuować pisanie na standardowe wyjście, wiedząc, że dane będą dołączone do komunikatu potomka. Gdyby procesy macierzysty i potomny nie współdzieliły wskaźnika bieżącej pozycji w pliku, wówczas dużo trudniej byłoby zaaranżować ten typ interakcji i prawdopodobnie byłaby niezbędna jawna akcja ze strony procesu potomnego. Gdy oba procesy, macierzysty i potomny, piszą korzystając z tego samego deskryptora, wówczas (oczywiście przy założeniu, że deskryptor był otwarty przed wywołaniem f ork) możemy wymieszać wydruki tych procesów bez żadnej dodatkowej formy synchronizacji (czyli np. bez oczekiwania przez proces macierzysty na zakończenie pracy potomka). Chociaż jest to możliwe (widzieliśmy w próg. 8.1), jednak trudno uznać takie działanie za typowe. Po wywołaniu f ork istnieją dwie typowe sytuacje, które wiążą się z obsługą deskryptorów.

8.3. Funkcja f ork

237

1. Proces macierzysty czeka na zakończenie pracy potomka. W tym przypadku proces macierzysty nie musi nic robić ze swoimi deskryptorami. Po zakończeniu procesu potomnego wszystkie wspólne deskryptory, których potomek używał do czytania lub pisania, mają odpowiednio zaktualizowane wskaźniki bieżących pozycji w pliku. 2. Procesy macierzysty i potomny realizują swoje własne zadania. Wówczas, po wywołaniu fork, proces macierzysty zamyka wszystkie deskryptory, których nie potrzebuje, i to samo robi potomek. Dzięki temu procesy nie będą kolidować ze sobą przez otwarte deskryptory. Taki scenariusz jest bardzo popularny w serwerach sieciowych. Oprócz otwartych plików, potomek dziedziczy z procesu potomnego wiele innych własności: • rzeczywisty identyfikator użytkownika, rzeczywisty identyfikator grupy, obowiązujący identyfikator użytkownika, obowiązujący identyfikator grupy, • identyfikatory dodatkowych grup, identyfikator sesji, terminal sterujący, sygnalizator ustanowienia identyfikatora użytkownika oraz sygnalizator ustanowienia identyfikatora grupy, bieżący katalog roboczy, katalog główny, maskę tworzenia plików, • maskę sygnałów oraz dyspozycje obsługi sygnałów, • sygnalizator zamykania przy wywołaniu funkcji exec (close-on-exec) dla wszystkich otwartych deskryptorów plików, • środowisko, • przyłączone segmenty pamięci wspólnej, • ograniczenia zasobów systemowych. Sąjednak pewne różnice między procesem macierzystym a potomnym: • wartość powrotu z funkcji f ork, • różne identyfikatory procesów, • inne identyfikatory procesów macierzystych - w procesie potomnym jest to identyfikator procesu macierzystego; w procesie macierzystym identyfikator procesu macierzystego nie zmienia się, • w procesie potomnym wartości tms_utime, tms_cutime i tms_ustime są równe 0, • potomek nie dziedziczy rygli plików, ustalonych w procesie macierzystym, • w procesie potomnym są zerowane wszystkie zaległe alarmy, • w procesie potomnym jest zerowany zbiór zaległych sygnałów.

8. Sterowanie procesem

O wielu tych własnościach jeszcze nie mówiliśmy, przedyskutujemy je w następnych rozdziałach. Możliwe są dwie przyczyny niepoprawnego zakończenia funkcji fork: (a) jeśli w systemie jest już za dużo procesów (co na ogół oznacza, że dzieje się coś złego) lub (b) jeśli całkowita liczba procesów dla danego rzeczywistego identyfikatora użytkowników jest większa od ustalonego ograniczenia systemowego. Przypominamy, że zgodnie z tab. 2.7 stała CHILD_MAX określa maksymalną liczbę jednocześnie działających procesów o jednym rzeczywistym identyfikatorze użytkownika. Są dwa zastosowania funkcji fork: 1. Gdy proces tworzy swoją kopię, by następnie oba procesy, macierzysty oraz potomny, jednocześnie wykonywały różne fragmenty kodu. Jest to bardzo popularne rozwiązanie w serwerach sieciowych, gdy proces macierzysty czeka na żądanie klienta. Po otrzymaniu żądania, proces macierzysty wywołuje funkcję fork i potomek obsługuje żądanie. Proces macierzysty powraca do oczekiwania na następne żądanie. 2. Gdy proces chce wykonać inny program. Takie podejście jest z kolei popularne w powłokach. Potomek zaraz po powrocie z funkcji fork wywołuje funkcję exec (odpowiednie funkcje opiszemy w podrozdz. 8.9). Niektóre systemy operacyjne łączą dwie operacje, składające się na krok 2 (fork, a następnie exec), w jedną operację, nazywaną rozmnożeniem (spawn). W Uniksie te dwie czynności są oddzielone, ponieważ istnieją liczne zastosowania funkcji fork, w których nie jest wywoływana funkcja exec. Dzięki odseparowaniu potomek może zmodyfikować atrybuty procesu między wywołaniami fork a exec, jak np. przekierować wejście-wyjście, zmienić identyfikator użytkownika czy dyspozycję sygnału itp. W rozdziale 14 zobaczymy wiele przykładów.

Funkcja vf o r k Funkcja vfork ma taką samą sekwencję wywołania jak funkcja fork i takie same wartości powrotu. Różne jest jednak znaczenie obu funkcji. Funkcja vfork pochodzi z pierwszych wydań systemu 4BSD, które stosowały pamięć wirtualną. W podrozdziale 5.7 książki Lefflera i in. [32] stwierdzono, że wprawdzie funkcja vfork pozwala osiągnąć wysoką wydajność, ale ma nietypową semantykę i jest ogólnie uznawana za architektonicznie błędną. Mimo wszystko systemy SVR4 oraz 4.3+BSD dostarczają tę funkcję. W niektórych systemach istnieje plik nagłówkowy , który należy włączać do programu wywołującego funkcję vfork.

8.4. Funkcja vf ork

239

Zadaniem funkcji vf ork jest utworzenie nowego procesu, który ma następnie wykonać za pomocą wywołania exec nowy program (krok 2 na końcu poprzedniego podrozdziału). Nasza prosta powłoka z próg. 1.5 jest również przykładem tego typu programu. Funkcja vfork tworzy nowy proces, tak samo jak funkcja fork, ale nie wykonuje pełnej kopii przestrzeni adresowej procesu macierzystego w procesie potomnym, ponieważ potomek nie będzie odwoływał się do tego obszaru - zaraz po vfork wywoła funkcję exec (lub e x i t ) . Potomek w trakcie swojej pracy, do chwili wywołania funkcji exec lub e x i t , korzysta z przestrzeni adresowej swojego procesu macierzystego. Taka optymalizacja daje korzyści wydajnościowe w niektórych systemach Unix stosujących pamięć wirtualną. (Jak wspomnieliśmy w poprzednim podrozdziale, niektóre implementacje używają techniki kopiowania przy zapisie, aby poprawić wydajność sekwencji wywołań: fork i exec). Kolejna różnica między tymi dwiema funkcjami polega na tym, że używając vfork mamy gwarancję, że najpierw rozpoczyna pracę potomek i wykonuje wszystko, co poprzedza wywołanie exec lub e x i t . Gdy potomek wywoła jedną z tych funkcji, wówczas wznowi pracę proces macierzysty. (Może to doprowadzić do zakleszczenia, jeśli działanie potomka przed wywołaniem jednej z tych funkcji zależy od dalszych akcji procesu macierzystego).

Przykład Spróbujmy w próg. 8.1 zastąpić wywołanie fork wywołaniem vfork. Usuwamy też wywołanie funkcji w r i t e , która wypisywała dane na strumień wyjściowy. Nie potrzebujemy teraz usypiać procesu macierzystego, ponieważ jądro systemu gwarantuje, że proces ten będzie wstrzymany do chwili, gdy potomek wywoła funkcję exec lub e x i t . Uruchomienie tego programu daje poniższy wynik: $ a.out before vfork pid = 607, glob = 7, var = 89

Widzimy, że zwiększenie przez potomka wartości zmiennych zmodyfikowało te wartości w procesie macierzystym. Nie dziwi nas to, gdyż potomek korzysta z przestrzeni adresowej swojego procesu macierzystego. Pod tym wzglę- ; dem funkcja vfork zachowuje się inaczej niż fork. Zwróćmy uwagę, że w próg. 8.2 wywołujemy funkcję _ e x i t , a nie e x i t . W podrozdziale 8.5 powiemy, że funkcja _ e x i t nie realizuje opróż- j nienia buforów wejścia-wyjścia. Jeżeli zamiast tego wywołamy e x i t , to wynik będzie wyglądał inaczej. ' $ a.out before vfork

8. Sterowanie procesem

40

"ourhdr.h"

include include nt

glob = 6;

/* zewnętrzna zmienna w danych zainicjowanych */

,nt |iain (void) int var; pid_t pid;

/* zmienna automatyczna na stosie */

var = 88; printf("before vfork\n");

/* nie opróżniamy strumienia stdio */

if ( (pid = vfork()) < 0) err_sys("vfork error"); else if (pid == 0) { /* potomek */ glob++; /* modyfikacja zmiennych procesu macierzystego */ var++; _exit(0); /* potomek kończy pracę */

/* proces macierzysty */ printf{"pid = %d, glob = %d, var = %d\n", getpid(), glob, var) exit (0);

Próg. 8.2 Przykład użycia funkcji vf o r k

Nie pojawił się wydruk pochodzący z funkcji p r i n t f w procesie macierzystym. Przyczyna tkwi w tym, że potomek wywołał funkcję e x i t , która opróżniła i zamknęła wszystkie strumienie wejścia-wyjścia. Oczywiście dotyczyło to również standardowego wyjścia. Chociaż wszystko zdarzyło się w procesie potomnym, efekt jest widoczny również w przestrzeni adresowej procesu macierzystego, czyli wszystkie obiekty wejścia-wyjścia typu FILE są takie same w tym procesie jak u potomka. Gdy później proces macierzysty wywołuje funkcję p r i n t f , to okazuje się, że standardowe wyjście jest zamknięte, a funkcja p r i n t f przekazuje - 1 . • W podrozdziale 5.7 książki Lefflera i in. [32] można znaleźć więcej informacji o zagadnieniu implementacji funkcji fork oraz vfork. Ćwiczenia 8.1 oraz 8.2 są kontynuacją dyskusji na temat funkcji vfork.

I

241

8.5. Funkcja exit

8.5

Funkcja e x i t

Już w podrozdziale 7.3 stwierdziliśmy, że są trzy sposoby normalnego zakończenia procesu oraz dwa sposoby zakończenia awaryjnego.

1. Zakończenie normalne. (a) Wykonanie instrukcji r e t u r n w funkcji main. Zgodnie z podrozdz. 7.3 jest to równoważne wywołaniu funkcji e x i t . (b) Wywołanie funkcji e x i t . Ta funkcja, zdefiniowana w standardzie ANSI C, wywołuje procedury obsługi zakończenia, zarejestrowane wcześniej za pomocą funkcji a t e x i t , oraz zamyka wszystkie standardowe strumienie wejścia-wyjścia. Ponieważ standard ANSI C nie zajmuje się deskryptorami plików, wielokrotnymi procesami (procesami macierzystymi i potomnymi) oraz sterowaniem zadaniami, więc definicja tej funkcji jest niekompletna w odniesieniu do systemu Unix. (c) Wywołanie funkcji _ e x i t . Funkcja ta jest wywoływana przez funkcję e x i t i służy do obsługi szczegółów specyficznych dla systemu Unix. Funkcja _ e x i t jest określona w normie POSIX. 1. W większości implementacji systemów Unix exit(3) jest funkcją standardowej biblioteki języka C, natomiast _exit(2) jest funkcją systemową.

2. Zakończenie awaryjne. (a) Wywołanie funkcji a b o r t . Wkrótce okaże się, że jest to specjalny przypadek następnego punktu, gdyż jest wówczas generowany sygnał SIGABRT. (b) Gdy proces odbiera jakieś sygnały. (Sygnały opiszemy szczegółowo w rozdz. 10). Sygnał może być wygenerowany przez ten sam proces (np. za pomocą wywołania funkcji abort), inny proces lub jądro systemu. Przykładowo, jądro systemu może wygenerować sygnał, gdy proces odwołuje się do lokalizacji w pamięci, leżącej poza jego przestrzenią adresową, lub gdy proces wykonuje dzielenie przez 0. i

Niezależnie od tego, jak kończy się proces, system wykonuje zawsze ten sam kod programowy: zamyka wszystkie otwarte deskryptory w procesie, zwalnia używaną pamięć itp. W każdym z pokazanych przypadków chcemy, by kończący się proces • mógł powiadomić swój proces macierzysty, z jakiej przyczyny zakończył pra-; cę. Gdy używamy funkcji e x i t oraz _ e x i t , wówczas następuje to przez j przekazanie w argumencie tych funkcji stanu wyjścia. W przypadku zakończenia awaryjnego jądro systemu (a nie proces) generuje stan zakończenia,' aby wskazać przyczynę niepowodzenia. W każdej sytuacji proces macierzysty może otrzymać status zakończenia za pomocą jednej z funkcji: wait lub w a i t p i d (opiszemy je w kolejnym podrozdziale).

8. Sterowanie procesem

8.6. Funkcje wait oraz w a i t p i d

Celowo dokonaliśmy tu rozróżnienia między „stanem wyjścia" (który jest argumentem funkcji e x i t i _ e x i t albo wartością przekazywaną przez funkcję główną) oraz „stanem zakończenia". Stan wyjścia jest przekształcony przez jądro do postaci stanu zakończenia, gdy jest wywoływana funkcja e x i t (przypominamy rys. 7.1). Tabela 8.1 pokazuje sposoby, jakich może użyć proces macierzysty, by sprawdzić stan zakończenia procesu potomnego. Jeśli potomek zakończył pracę normalnie, to proces macierzysty może otrzymać stan zakończenia od potomka. Tabela 8.1

aby pobrać osiem mniej znaczących bitów argumentu przekazanego przez potomka do funkcji e x i t lub e x i t .

Nie możemy również zapomnieć o sytuacji odwrotnej, gdy proces potomny kończy się przed macierzystym. Gdyby potomek po prostu znikł, to proces macierzysty nie mógłby otrzymać jego stanu zakończenia wtedy, kiedy postanowiłby sprawdzić, czy potomek jeszcze pracuje (jeśli w ogóle go to zainteresuje). Dlatego system musi utrzymywać pewną pulę danych na temat każdego procesu, który się zakończył. Informacja ta będzie dzięki temu dostępna, gdy proces macierzysty zakończonego procesu wywoła funkcję wait lub w a i t p i d . Minimalny zestaw informacji składa się z identyfikatora procesu, stanu zakończenia oraz czasu procesora zużytego przez proces. System może skasować pamięć, z której korzystał proces, oraz zamknąć wszystkie otwarte w nim pliki. W terminologii systemu Unix proces, który kończy się, ajego proces macierzysty nie oczekuje na niego, jest nazywany procesem wstanie zombie. Polecenie ps(l) drukuje w polu stanu takiego procesu literę z. Jeśli napiszemy program, który pracuje bardzo długo i tworzy wiele procesów potomnych, ale proces macierzysty nie czeka na swoich potomków i nie musi dowiadywać się o ich stanie zakończenia, to potomkowie stają się procesami w stanie zombie.

Wartość prawda, jeśli przekazany stan dotyczył potomka, który zakończył pracę z błędem (po odebraniu sygnału, którego nie przechwycił). W tym przypadku możemy wykonać:

W podrozdziale 10.7 pokażemy, że w Systemie V istnieje niestandardowy sposób na uniknięcie procesów w stanie zombie.

Makra stosowane do analizy stanu zakończenia przekazywanego przez funkcje wait i w a i t p i d

akro

Opis

IFEXITED {Status)

Wartość prawda, jeśli przekazany stan dotyczył potomka, który zakończył pracę w sposób normalny. W tym przypadku możemy wykonać: WEXITSTATUS {status)

IFSIGNALED (status)

WTERMSIG (Status)

Nie wiemy jeszcze, co dzieje się, gdy proces, który został odziedziczony przez proces i n i t , zakończy się. Czy będzie procesem w stanie zombie? Nie, gdyż proces i n i t jest tak przygotowany, że gdy kończy się jeden z jego procesów potomnych, wówczas wywołuje funkcję rodziny wait, by otrzymać stan zakończenia. W ten sposób i n i t zapobiega powstawaniu bardzo dużej liczby procesów w stanie zombie. Gdy mówimy Jeden z potomków procesu i n i t " , to myślimy albo o procesie, który i n i t sam wygenerował (np. proces g e t t y , o którym powiemy w podrozdz. 9.2), albo o procesie, którego proces macierzysty zakończył się i proces i n i t odziedziczył osieroconego potomka.

aby pobrać numer sygnału, który spowodował zakończenie. Dodatkowo w systemach SVR4 i 4.3+BSD (ale nie w normie POSIX.l) jest zdefiniowane makro WCOREDUMP (Status) przekazujące wartość prawda, jeśli kończący się proces utworzył obraz pamięci w pliku c o r e . IFSTOPPED {status)

243

Wartość prawda, jeśli przekazany stan dotyczył potomka, który jest obecnie zatrzymany. W tym przypadku możemy wykonać: WSTOPSIG (status) aby pobrać numer sygnału, który spowodował zatrzymanie potomka.

Gdy opisywaliśmy funkcję fork, było dla nas oczywiste, że po jej wywołaniu istnieje proces macierzysty procesu potomnego. Teraz analizujemy przekazywanie stanu zakończenia. Co stanie się, jeśli proces macierzysty zakończył pracę przed swoim potomkiem? Okazuje się, że proces i n i t staje się procesem macierzystym wszystkich tych procesów, których procesy macierzyste zakończyły się. Mówimy, że proces i n i t dziedziczy inne procesy. Na ogół odbywa się to tak, że gdy jakiś proces kończy się, wówczas system przegląda wszystkie aktywne procesy, aby sprawdzić, czy kończący się proces nie jest procesem macierzystym istniejącego procesu. Jeśli znajdzie taki proces, to zmienia jego identyfikator procesu macierzystego na wartość 1 (identyfikator procesu i n i t ) . Dzięki temu mamy pewność, że każdy proces ma swój proces macierzysty.

8.6

Funkcje wait oraz waitpid Gdy proces kończy się albo normalnie, albo awaryjnie, wówczas proces macierzysty jest o tym powiadamiany za pomocą sygnału SIGCHLD, który przesyła do niego jądro systemu. Ponieważ zakończenie pracy przez potomka jest zdarzeniem asynchronicznym (zdarza się w dowolnym czasie pracy procesu macierzystego), więc sygnał ten jest asynchronicznym powiadomieniem procesu przez jądro. Proces macierzysty może albo ignorować taki sygnał, albo zarejestrować jego obsługę przez specjalną funkcję (procedura obsługi sygnału). Domyślną akcją dla tego sygnału jest ignorowanie. Szczegółowo opiszemy dostępne opcje w rozdz. 10. Na razie musimy tylko być świadomi, że proces, który wywołuje funkcję wait lub funkcję waitpid, może:

8. Sterowanie procesem

8.6. Funkcje w a i t oraz w a i t p i d

• ulec zablokowaniu (jeśli wszystkie procesy potomne ciągle pracują) lub • natychmiast powrócić ze stanem zakończenia potomka (jeśli potomek zakończył pracę i oczekuje na pobranie jego stanu zakończenia), lub • natychmiast powrócić z komunikatem awaryjnym (jeśli nie ma żadnych procesów potomnych). Jeśli proces wywołuje funkcję wait, ponieważ otrzymał sygnał SIGCHLD, to spodziewamy się, że funkcja wait powróci natychmiast. Jeśli jednak wywołujemy tę funkcję losowo, w dowolnej chwili, to możemy wprowadzić proces w stan zablokowania.

Standard POSIX.l ustala, że stan zakończenia należy sprawdzać za pomocą różnych makr, zdefiniowanych w pliku nagłówkowym < s y s / w a i t . h>. Są tam trzy, wzajemnie wykluczające się makra, które określają, w jaki sposób proces zakończył pracę. Nazwy tych makr zaczynają się od napisu WIF. Opierając się na tym, które z makr przekaże wartość prawda, korzystamy z kolejnych makr, by pobrać stan wyjścia, numer sygnału itp. Wszystko pokazuje tab. 8.1. W podrozdziale 9.8, gdy będziemy mówić o sterowaniu zadaniami, opiszemy, jak można zatrzymać proces.

Przykład Funkcja p r _ e x i t z próg. 8.3 używa makr z tab. 8.1, by wydrukować opis stanu zakończenia. Wywołujemy tę funkcję z wielu programów w tej książce. Zwróćmy uwagę, że funkcja korzysta z makra WCOREDUMP, jeśli jest ono zdefiniowane w konkretnym systemie.

tinclude #include pid_t wait (int *statloc) ; p i d _ t w a i t p i d ( p i d _ t pid,

245

i n t *statloc, i n t options) ;

Obie przekazują: identyfikator procesu, jeśli wszystko w porządku; 0 (patrz dalej) lub —1, jeśli wystąpił błąd

Oto różnice między tymi dwoma funkcjami: • wait może zablokować wywołującego tę funkcję do czasu zakończenia procesu potomnego, a w a i t p i d ma opcję, która zapobiega blokowaniu. • w a i t p i d nie musi czekać do zakończenia pracy przez dowolnego potomka, funkcja ta ma wiele opcji kontrolujących, na którego potomka oczekuje. Jeśli potomek już zakończył pracę i jest procesem w stanie zombie, to wait powraca natychmiast ze stanem procesu potomnego. W przeciwnym razie funkcja wait blokuje wywołującego, dopóki potomek nie zakończy pracy. Jeśli proces wywołujący funkcję uległ zablokowaniu, a ma wielu potomków, to powrót nastąpi, gdy jeden z nich zakończy się. Zawsze możemy powiedzieć, jaki potomek zakończył pracę, gdyż funkcja przekazuje identyfikator zakończonego procesu. W obu funkcjach argument statloc jest wskaźnikiem do liczby całkowitej. Jeśli wskaźnik ten nie jest pusty, to pod podanym adresem zostanie umieszczony stan zakończenia procesu. Gdy nie interesuje nas ten stan, przekazujemy w argumencie statloc pusty wskaźnik. Tradycyjnie całkowitoliczbowa postać stanu zakończenia, przekazywana przez obie funkcje, jest definiowana w konkretnych implementacjach. Pewne bity wskazują stan wyjścia (dla normalnego powrotu), inne numer sygnału (dla powrotu awaryjnego), jeden bit wskazuje, czy powstał plik core itp.

#include #include ttinclude

"ourhdr.h"

void pr_exit(int status) if

(WIFEXITED(status)) printf("normal termination, exit status = %d\n", WEXITSTATUS(status) ) ; else if (WIFSIGNALED(status)) printf("abnormal termination, signal number = %d%s\n", WTERMSIG(status), #ifdef WCOREDUMP WCOREDUMP(status) ? " (core file generated)" : " " ) ; #else #endif else if (WIFSTOPPED(status)) printf("child stopped, signal number = %d\n", WSTOPSIG(status));

Próg. 8.3 Wydruk opisu stanu otrzymanego z funkcji e x i t

Program 8.4 wywołuje funkcję p r _ e x i t i pokazuje różne wartości stanów zakończenia. Uruchomienie próg. 8.4 daje następujący wynik: $ a.out normal termination, exit status = 7 abnormal termination, signal number = 6 (core file generated) abnormal termination, signal number = 8 (core file generated)

8. Sterowanie procesem nclude nclude nclude

"ourhdr.h"

in(void) pid_t int

pid; status;

if ( (pid = fork()) < 0) err sys("fork error"); else if (pid == 0) /* potomek */ exit (7); if (wait(&status) != pid) /* oczekiwanie na potomka */ err_sys("wait error"); pr_exit(status); /* i wydruk jego stanu */ if ( (pid = fork()) < 0) err_sys("fork error"); else if (pid == 0) abort();

8.6. Funkcje wait oraz waitpid

247

my identyfikator procesu, na który chcemy czekać)? W starszych wersjach Uniksa musieliśmy wówczas wywołać funkcję wait i sprawdzić, czy pokazany przez nią identyfikator procesu jest tym, na który czekamy. Jeśli zakończony proces nas nie interesował, to było konieczne zapamiętanie odebranego identyfikatora procesu oraz stanu zakończenia, po czym było powtarzane wywołanie wait. Kontynuowaliśmy te działania, dopóki nie zakończył si^ właściwy proces. Gdy następny raz chcieliśmy oczekiwać na konkretny proces, wówczas najpierw przeglądaliśmy listę wcześniej zakończonych procesów, by sprawdzić, czy już nie odebraliśmy jego stanu zakończenia; jeśli nie, to wywoływaliśmy znowu funkcję wait. Przydatna byłaby funkcja, która oczekuje na konkretny proces. Tą funkcjonalnością, a również innymi dodatkowymi własnościami charakteryzuje się funkcja waitpid, zdefiniowana w normie POSDC.l. Funkcja waitpid jest nowością w normie POSK.l. Jest zaimplementowana w systemach SVR4 oraz 4.3+BSD. Wczesne wersje Systemu V i 4.3BSD jednak jej nie stosowały.

Interpretacja argumentu pid w funkcji w a i t p i d zależy od jego wartości: /* potomek */ /* wygenerowanie sygnału SIGABRT */

if (wait(sstatus) != pid) /* oczekiwanie na potomka */ err_sys("wait error"); pr_exit(status); /* i wydruk jego stanu */ if ( (pid = fork()) < 0) err_sys("fork error"); else if (pid == 0) /* potomek */ status /= 0; /* dzielenie przez 0 generuje sygnał SIGFPE */ if (wait(sstatus) != pid) /* oczekiwanie na potomka */ err_sys("wait error"); pr_exit(status); /* i wydruk jego stanu */ exit (0);

Próg. 8.4 Różne stany przekazywane przez funkcję e x i t

Niestety, nie ma przenośnego sposobu odwzorowania numerów sygnałów otrzymanych w makrze WTERMSIG na ich nazwy. (W podrozdziale 10.21 podajemy jedną metodę). Musimy zajrzeć do pliku nagłówkowego < s i g n a l .h>, aby sprawdzić, że sygnał SIGABRT ma wartość 6, a S I G F P E wartość 8. • Jak wspomnieliśmy, gdy mamy więcej potomków, funkcja wait powraca w chwili zakończenia pracy przez dowolnego potomka. Co zrobić, gdy chcemy oczekiwać na zakończenie konkretnego procesu (zakładając, że zna-

pid==-\ Oczekiwanie na dowolny proces potomny. W tej sytuacji funkcja w a i t p i d jest równoważna funkcji wait. pid > 0 Oczekiwanie na proces o identyfikatorze równym pid. pid == 0 Oczekiwanie na każdego potomka, którego identyfikator grupy procesów jest równy identyfikatorowi grupy procesów w procesie wywołującym tę funkcję. pid < -1 Oczekiwanie na każdego potomka, którego identyfikator grupy procesów jest równy wartości absolutnej argumentu pid. (W podrozdziale 9.4 opiszemy grupy procesów). Funkcja w a i t p i d przekazuje identyfikator zakończonego procesu potomnego, a zawartość argumentu statloc mówi o stanie zakończenia. W przypadku funkcji wait możemy odebrać błąd tylko wówczas, gdy proces nie- ma potomków. (Możliwy jest jeszcze inny komunikat awaryjny, gdy funkcja zostanie przerwana na skutek pojawienia się sygnału. Pokażemy to w rozdz. 10). Funkcja w a i t p i d możt| przekazać błąd, jeśli nie istnieje podany proces czy grupa procesów lub wywołujący ją proces nie ma potomków. ' : Argument option umożliwia dokładniejsze kontrolowanie operacji funkcji w a i t p i d . Argument ten jest albo równy 0, albo tworzymy go przez bitową^ alternatywę stałych zamieszczonych w tab. 8.2. W systemie SVR4 istnieją dwie dodatkowe, niestandardowe stałe, używane w argu-j mencie option. Stała WNOWAIT oznacza, że system ma zachować w stanie oczekiwania wszystkie przekazywane przez funkcję waitpid stany zakończenia, aby można było* powtórnie na nie oczekiwać. Stała WCONTINUED powoduje, że dla każdego potomka1 wskazanego za pomocą argumentu pid jest dostarczana informacja o kontynuacji pracy, jeśli dotychczas nie przekazano jego informacji o stanie.

8. Sterowanie procesem Tabela 8.2

8.7. Funkcje wait3 oraz wait4

Stałe options w funkcji w a i t p i d

a

Opis

)HANG

Funkcja w a i t p i d nie ulegnie zablokowaniu, jeśli potomek określony przez argument pid nie jest natychmiast dostępny. W tym przypadku wartością zwracaną jest 0.

/* * * * *

JTRACED

Gdy implementacja ma wbudowane sterowanie zadaniami, to jest przekazywany stan każdego zatrzymanego potomka wskazanego w argumencie pid, jeśli jego stan nie był raportowany od czasu zatrzymania. Makro WIFSTOPPED ustala, czy wartość zwracana odpowiada zatrzymanemu procesowi potomka.

sleep(2); printf("second child, parent pid = %d\n", getppid()); exit (0);

Funkcja w a i t p i d ma trzy własności, których brakuje funkcji wait.

/* Proces macierzysty (oryginalny proces); kontynuuje realizację; * wie, że nie jest procesem macierzystym drugiego potomka. */ exit (0);

Próg. 8.5 Uniknięcie procesów typu zombie dzięki dwukrotnemu wywołaniu funkcji f o r k

TRACED).

W drugim procesie potomnym wywołujemy funkcję s l e e p , by zagwarantować, że pierwszy potomek zakończy pracę przed wydrukiem identyfikatora procesu macierzystego. Po wywołaniu fork kontynuuje wykonanie albo proces macierzysty, albo potomek, gdyż nigdy nie wiemy, który z tych dwóch procesów zacznie działać pierwszy. Gdybyśmy nie uśpili drugiego potomka i zdarzyłoby się, że on zacząłby pracę przed procesem macierzystym, to wydrukowany przez niego identyfikator procesu byłby taki sam jak identyfikator jego procesu macierzystego, czyli nie byłby równy 1. Wykonanie próg. 8.5 daje poniższy wynik:

ykład Przypomnijmy sobie podrozdz. 8.5, w którym mówiliśmy o procesach w stanie zombie. Jeśli przygotowujemy program, który tworzy procesy potomne i nie ma oczekiwać na zakończenie pracy przez potomków, a jednocześnie nie odpowiada nam, by zakończone procesy potomne stały się procesami zombie do czasu, gdy proces macierzysty zakończy pracę, to dobrą metodą jest dwukrotne wywołanie funkcji f ork. Tak dzieje się w próg. 8.5.

$ a.out $ second child, parent pid = 1

"ourhdr.h"

Zwróćmy uwagę, że powłoka wypisała swoją sekwencję zachęty zaraz po zakończeniu oryginalnego procesu, zanim drugi potomek wypisał identyfikator swojego procesu macierzystego. •

n(void) pid_t

Proces drugiego potomka; natychmiast po wywołaniu przez rzeczywisty proces macierzysty funkcji exit() w powyższej instrukcji procesem macierzystym staje się init. Od tego miejsca będzie kontynuowane wykonanie, gdy proces ten zakończy pracę; init przechwyci stan. */

if (waitpid (pid, NULL, 0) != pid) /* oczekujemy na pierwszego potomka */ err_sys("waitpid error");

1. w a i t p i d umożliwia oczekiwanie na konkretny proces (podczas gdy wait przekazuje stan dowolnego zakończonego procesu potomnego). Powrócimy jeszcze do tej własności, gdy będziemy mówić o funkcji popen. 2. w a i t p i d daje nam nieblokującą wersję funkcji wait. Zdarza się, że chcemy przechwycić stan potomka, ale nie chcemy zostać zablokowani. 3. w a i t p i d umożliwia sterowanie zadaniami (jeśli użyjemy opcji WUN-

clude clude clude

249

pid;

if ( (pid = fork()) < 0) err__sys ("fork error"); /* pierwszy potomek */ else if (pid == 0) ( if ( (pid = fork()) < 0) err__sys ( "fork error" ) ; else if (pid > 0) exit(0); /* proces macierzysty z drugiego wywołania fork to pierwszy potomek */

8.7

Funkcje w a i t 3 oraz wait4 W systemie 4.3+BSD są dwie dodatkowe funkcje, wait3 oraz wait4. Mają one tylko jedną własność różniącą je od posiksowych funkcji wait i w a i t p i d -jest to dodatkowy argument, dzięki któremu system przekazuje podsumowanie zużycia zasobów przez kończący się proces oraz wszystkie jego procesy potomne.

iO

8. Sterowanie procesem #include #include #include #include pid_t w a i t 3 ( i n t *statloc, i n t options, s t r u c t rusage *rusage) ; pid_t wait4 (pid_t pid, i n t *statloc, i n t options, s t r u c t rusage *rusage) ; Przekazują: identyfikator procesu, jeśli wszystko w porządku; 0 lub —1, jeśli wystąpił błąd System SVR4 udostępnia również funkcję w a i t 3 w bibliotece zgodności z systemem BSD.

Informacja na temat zasobów obejmuje czas procesora, liczbę błędów stronicowania, liczbę odebranych sygnałów itp. Odsyłamy do dokumentacji systemowej getrusage(2), gdzie można znaleźć więcej szczegółów. Informacja o zasobach jest dostępna tylko dla zakończonych procesów potomnych, a nie dla procesów zatrzymanych. (Postać informacji o zasobach różni się od opisanych w podrozdz. 7.11 ograniczeń zasobów). W tabeli 8.3 pokazujemy różne argumenty w omawianych wersjach funkcji serii wait w rozmaitych implementacj ach. Tabela 8.3 Argumenty stosowane przez różne funkcje w a i t w różnych systemach unkcja

pid

options





rusage

ait aitpid ait3 ait4

.8











P0SK.1

SVR4

4.3+BSD















• •

Sytuacje wyścigu Sytuacja wyścigu powstaje, gdy wiele procesów próbuje wykonywać jakieś operacje na wspólnych danych i końcowy wynik zależy od kolejności realizacji procesów. Stosunkowo często mogą powstawać sytuacje wyścigu w funkcji fork, jeśli przebieg pracy programu po wywołaniu funkcji fork pośrednio lub bezpośrednio zależy od kolejności, w jakiej rozpoczynają pracę procesy macierzysty i potomny. W zasadzie nie możemy przewidzieć, który proces najpierw będzie wykonywany. Nawet jeśli wiemy, który z procesów powinien zacząć pracę pierwszy, to nie wiadomo, co stanie się zaraz po wystartowaniu tego procesu, gdyż wszystko zależy od obciążenia systemu oraz stosowanego przez jądro systemu algorytmu planowania zadań. Widzieliśmy już potencjalne zagrożenie sytuacją wyścigu w próg. 8.5, gdy drugi potomek drukował identyfikator swojego procesu macierzystego. Jeśli drugi potomek rozpoczyna swoją pracę przed pierwszym procesem potomnym, to jego procesem macierzystym jest proces i n i t . Nawet wywołanie

8.8. Sytuacje wyścigu

251

funkcji s l e e p , jak tam zrobiliśmy, nie daje gwarancji. Jeśli system jest mocno obciążony, to drugi potomek może zacząć pracę po powrocie z wywołania s l e e p , zanim pierwszy proces potomny będzie miał szansę wystartowania. Problemy tego typu mogą być bardzo trudne do prześledzenia, ponieważ istotnie zależą od układów czasowych. Gdy proces postanawia czekać do zakończenia pracy przez swojego potomka, to musi wywołać jedną z funkcji wait. Jeśli proces chce czekać na zakończenie pracy przez swój proces macierzysty, jak było w próg. 8.5, to zapewni to poniższy zestaw instrukcji: while (getppid() != 1) sleep(1);

Następstwem tego typu pętli, nazywanej odpytywaniem, (polling) jest znacząca strata czasu procesora, ponieważ proces wywołujący jest budzony co sekundę w celu przetestowania bieżących warunków. Aby uniknąć sytuacji wyścigu oraz odpytywania, jest potrzebna jakaś forma sygnalizacji między wieloma procesami. Można do tego celu użyć sygnałów, w podrozdz. 10.16 pokażemy jedną z takich metod. Alternatywą są różne mechanizmy komunikacji międzyprocesowej, o których będziemy mówić w rozdz. 14 i 15. W przypadku relacji między procesami macierzystym i potomnym bardzo często mamy następujący scenariusz. Po wywołaniu fork oba procesy, macierzysty i potomny, mają na ogół coś do zrobienia. Proces macierzysty aktualizuje na przykład rekord w kronice systemowej, wpisując identyfikator procesu potomnego, a potomek tworzy plik dla swojego procesu macierzystego. W takiej sytuacji wymagamy, by procesy wzajemnie powiadomiły się o zakończeniu wykonania zestawu operacji inicjującego pracę i również, by wzajemnie oczekiwały na zakończenie tego fragmentu działań, zanim przejdą do własnych zadań. Oto przykładowy scenariusz:

#include "ourhdr.h" TELL_WAIT() /* ustalenie wszystkiego dla TELL_xxx & WAIT_xxx */ if ( (pid = fork()) < 0) err_sys("fork error"); else if (pid == 0) { /* potomek */ , /* potomek wykonuje wszystko, co potrzebuje ... */ TELL_PARENT(getppidf)); /* potomek powiadamia proces macierzysty o zakończeniu */ WAIT_PARENT(); /* i oczekuje na proces macierzysty */ /* następnie potomek kontynuuje własne zadania ... */ exit(0);

;

j

/* /•* proces macierzysty realizuje wszystkie potrzebne operacje ... */ j TELL_CHILD(pid); /* proces macierzysty powiadamia potomka o zakończeniu tego fragmentu pracy */ '* WAIT_CHILD(); /* i oczekuje na potomka */ /* proces macierzysty kontynuuje swoje zadania... */ exit (0);

252

8. Sterowanie procesem

Strumień wyjściowy pracuje bez buforowania, a więc każde wypisanie znaku generuje jedno wywołanie w r i t e . Celem tego przykładu jest wymuszenie częstego przełączania procesów przez jądro, co pozwoli zademonstrować sytuację wyścigu. (Jeśli nie zastosowalibyśmy takiego scenariusza, to być może nigdy nie pojawiłyby się wyniki, które wkrótce pokażemy. Z faktu, że nie widzimy błędnych wyników, nie można wyciągnąć wniosku, że program nie wprowadza sytuacji wyścigu, oznacza tylko, że nie możemy ich zaobserwować w tym jednym, konkretnym systemie). Poniższy wydruk pokazuje, jak mogą zmieniać się wyniki.

Założyliśmy, że w pliku nagłówkowym ourhdr. h są zdefiniowane wszystkie wymagane zmienne. Pięć procedur TELL_WAIT, TELL_PARENT, TELL_CHILD, WAIT_PARENT i WAIT_CHiLD to albo makra, albo funkcje. Jeszcze w tym podrozdziale zobaczymy różne sposoby implementacji procedur serii TELL oraz WAIT. W podrozdziale 10.16 pokażemy implementację stosującą sygnały. Implementację używającą strumieniowych łączy komunikacyjnych zaprezentujemy w próg. 14.3. Przyjrzyjmy się teraz przykładowi, który tylko korzysta z tych pięciu procedur.

Przykład

$ a.out output from child output from parent $ a.out oouuttppuutt ffrroomra t $ a.out oouuttppuutt ffrroomm

Program 8.6 wypisuje dwa napisy: jeden pochodzi z procesu potomnego, drugi z procesu macierzystego. Grozi mu sytuacja wyścigu, ponieważ wynik zależy od kolejności, w jakiej system uruchomi te dwa procesy oraz od czasu pracy każdego procesu. include include

"ourhdr.h"

pcahrielndt

Musimy zmodyfikować próg. 8.6, by używał funkcji serii WAIT oraz TELL. Zmiany pokazuje próg. 8.7. Nowe wiersze zaczynają się od znaku +.

nt ain(void) pid;

if ( (pid = fork()) < 0) err_sys("fork error"); else if (pid == 0) ( charatatime("output from child\n"); } else { charatatime("output from parent\n") } exit (0) ;

#include łłinclude

int main(void) pid_t +

*ptr; c;

setbuf(stdout, NULL); /* bez buforowania */ for (ptr = str; c = *ptr++; ) putc (c, stdout);

Sytuacja wyścigu w programie

pid;

TELL_WAIT(); if ( (pid = fork()) < 0) err_sys("fork error"); else if (pid == 0) { WAIT_PARENT(); /* proces macierzysty zaczyna pracę */ charatatime("output from child\n"); } else { charatatime("output from parent\n"); TELL_CHILD(pid); exit(0);

Próg. 8.6

60

8. Sterowanie procesem

która otrzymuje nazwę pliku i przekazuje do nowego programu bieżące środowisko wywołujące tę funkcję. Funkcja e x e c l p działa poprawnie tylko dlatego, że katalog /home/stevens/bin jest umieszczony na liście przedrostków ścieżek (zmienna PATH). Zwróćmy uwagę, że pierwszy argument, a r g v [ 0 ] , zawiera tylko jeden składnik nazwy ścieżki, tj. nazwę pliku z nowym programem. W niektórych powłokach argument ten przyjmuje wartość nazwy ścieżki. W programie 8.8 dwukrotnie wykonujemy program e c h o a l l (próg. 8.9). Jest to bardzo prosty program, drukujący wszystkie argumenty wiersza poleceń oraz całą listę środowiska. :

"ourhdr.h"

include

26

Zwróćmy uwagę, że znak zachęty powłoki pojawił się między wydrukami ar gurhentów argv[0] i a r g v [ l ] w drugim wywołaniu exec. Stało się tak ponieważ proces macierzysty nie wywołał funkcji wait, by oczekiwać na za kończenie pracy przez swojego potomka. z

8.10 Zmiana identyfikatorów użytkownika i identyfikatorów grup Za pomocą funkcji s e t u i d możemy przypisać nowy rzeczywisty lub obo wiązujący identyfikator użytkownika. Podobnie, używając funkcji s e t g i d możemy nadać nową wartość rzeczywistemu lub obowiązującemu identyfi katorowi grupy. #include #include

nt iain(int argc, char *argv[];

int setuid (uid_t uid) ;

int i; char **ptr; extern char **environ;

int setgid (gid_t gid) ;

for (i = 0 ; i < argc; i++)

/* echo wszystkich argumentów wiersza poleceń */ printf("argv[%d]: %s\n", i, argv[i]);

for (ptr = erwiron; *ptr != 0; ptr++)

/* i wszystkich napisów środowiska */

printf("%s\n", *ptr); exit (0);

Próg. 8.9 Powtórzenie na standardowym wyjściu wszystkich argumentów wiersza poleceń oraz wszystkich napisów środowiska

Wykonanie próg. 8.8 daje poniższy wynik: $ a.out argv[0]: echoall argv[1]: myargl argv[2]: MY ARG2 USER=unknown PATH=/tmp argv[0]: echoall $ argv[l]: only 1 arg USER=stevens HOME=/home/stevens LOGNAME=stevens EDITOR=/usr/ucb/vi

8 10. Zmiana identyfikatorów użytkownika i identyfikatorów grup

Przekazują: 0 jeśli wszystko w porządku; - 1 , jeśli wystąpił błąd

Istnieje zestaw reguł, które ustalają, kto może zmieniać identyfikatory. Roz ważmy najpierw identyfikator użytkownika. (Wszystko, co powiemy na tema identyfikatora użytkownika, odnosi się również do identyfikatora grupy).

1. Gdy proces ma uprawnienia nadzorcy, wówczas funkcja s e t u i d nadaje wartość uid rzeczywistemu identyfikatorowi użytkownika, obo wiązującemu identyfikatorowi użytkownika oraz zachowanemu iden tyfikatorowi użytkownika. 2. Jeśli proces nie ma uprawnień nadzorcy, ale argument uid jest równ) albo rzeczywistemu identyfikatorowi użytkownika, albo zachowanemu identyfikatorowi użytkownika, to funkcja s e t u i d nadaje war tość uid tylko obowiązującemu identyfikatorowi użytkownika. Nie s«j zmieniane ani rzeczywisty identyfikator użytkownika, ani zachowań; identyfikator użytkownika. 3. Gdy żaden z powyższych warunków nie jest spełniony, wówcza:' zmienna e r r n o przyjmuje wartość EPERM i otrzymujemy błąd. [

Założyliśmy przy tym, że jest zdefiniowana nazwa _POSIX_SAVED_IDS. Je' śli nie jest to realizowane w konkretnym systemie, to należy usunąć wszystkie odwołania do zachowanego identyfikatora użytkownika. W standardzie FIPS 151-1 wymaga się, by było to zaimplementowane. nie pokazujemy pozostałych 31 wierszy wydruku

W systemie SVR4 jest to zaimplementowane.

8. Sterowanie procesem

Możemy więc zebrać kilka zasad dotyczących trzech identyfikatorów użytkownika, które utrzymuje jądro systemu. 1. Tylko proces nadzorcy może zmienić rzeczywisty identyfikator użytkownika. Normalnie, rzeczywisty identyfikator użytkownika jest ustalany przez program l o g i n ( l ) w czasie logowania w systemie i nigdy się nie zmienia. Ponieważ program l o g i n jest procesem nadzorcy, więc wywołanie funkcji s e t u i d ustala wartości wszystkich trzech identyfikatorów użytkownika. 2. Obowiązujący identyfikator użytkownika jest ustalany przez funkcje exec tylko wówczas, gdy w pliku programu jest ustawiony bit ustanowienia identyfikatora użytkownika. Jeżeli nie jest, to funkcja exec nie zmienia bieżącej wartości obowiązującego identyfikatora użytkownika. W każdej chwili możemy wywołać funkcję setuid, aby nadać nową wartość obowiązującemu identyfikatorowi użytkownika równą albo rzeczywistemu identyfikatorowi użytkownika, albo zachowanemu identyfikatorowi użytkownika. Naturalnie, nie możemy nadać obowiązującemu identyfikatorowi użytkownika jakiejś losowej wartości. 3. Funkcja exec kopiuje do zachowanego identyfikatora użytkownika obowiązujący identyfikator użytkownika ustalony w poprzednim kroku. Kopia ta istnieje po ustaleniu przez funkcję exec nowego obowiązującego identyfikatora użytkownika na podstawie identyfikatora użytkownika pliku (jeśli jest ustawiony bit ustanowienia identyfikatora użytkownika). Tabela 8.5 jest podsumowaniem różnych metod zmiany poszczególnych identyfikatorów użytkownika. Tabela 8.5

Różne sposoby zmiany trzech identyfikatorów użytkownika s e t u i d (uid)

exec wyłączony bit ustanowienia identyfikatora użytkownika

włączony bit ustanowienia identyfikatora

zywisty

bez zmiany

bez zmiany

na podstawie wartości uid

bez zmiany

wiązujący

bez zmiany

ustawiany na podstawie identyfikatora użytkownika pliku z programem

na podstawie wartości uid

na podstawie wartości uid

lowany

skopiowany z obowiązującego identyfikatora użytkownika

skopiowany z obowiązującego identyfikatora użytkownika

na podstawie wartości uid

bez zmiany

ntyfikator tkownika

nadzorca

użytkownik nieuprzywilejowany

8.10. Zmiana identyfikatorów użytkownika i identyfikatorów grup

263

Zwróćmy uwagę, że funkcje g e t u i d i g e t e u i d z podrozdz. 8.2 służą do pobrania bieżących wartości rzeczywistego identyfikatora użytkownika oraz obowiązującego identyfikatora użytkownika. Nie możemy natomiast otrzymać wartości zachowanego identyfikatora użytkownika.

Przykład Aby zobrazować przydatność zachowanego identyfikatora użytkownika, przyjrzyjmy się działaniu programu, który korzysta z niego. Przeanalizujemy berkelejowski program t i p ( l ) . (Podobny do niego jest program cu(l) w Systemie V). Oba programy łączą się ze zdalnym systemem albo przez bezpośrednie połączenie, albo przez modem. Program t i p korzysta z modemu i musi być w czasie pracy jego wyłącznym właścicielem, dlatego rygluje pewien plik. Program UUCP używa również tego pliku, ponieważ może ubiegać się o modem w tym samym czasie. Oto kolejno realizowane kroki w prezentowanym scenariuszu. 1. Właścicielem pliku z programem t i p jest użytkownik o nazwie uucp. Plik ma ustawiony bit ustanowienia identyfikatora użytkownika. Gdy wykonujemy ten program za pomocą funkcji exec, to rzeczywisty identyfikator użytkownika = nasz identyfikator użytkownika, obowiązujący identyfikator użytkownika =

uucp,

zapamiętany identyfikator użytkownika = uucp.

2. Program t i p korzysta z plików rygla. Te pliki są własnością użytkownika uucp, ale ponieważ obowiązujący identyfikator użytkownika jest równy uucp, dostęp do plików jest możliwy. 3. Program t i p wywołuje funkcję s e t u i d ( g e t u i d () ). Ponieważ nasz proces nie ma uprawnień nadzorcy, więc jest zmieniany tylko obowiązujący identyfikator użytkownika. Otrzymujemy rzeczywisty identyfikator użytkownika = nasz identyfikator użytkownika (bez zmiany), obowiązujący identyfikator użytkownika =

nasz identyfikator użytkownika,

zachowany identyfikator użytkownika = u u c p (bez zmiany).

Teraz program t i p pracuje z naszym identyfikatorem użytkownika jako obowiązującym identyfikatorem użytkownika. Oznacza to, że mamy typowe prawa dostępu do tych plików. Nie dysponujemy żadnymi dodatkowymi uprawnieniami. 4. Gdy kończymy pracę, program t i p wywołuje funkcję s e t u i d (uucpuid), gdzie argument uucpuid jest numerycznym identyfikatorem użytkownika o nazwie uucp. (Program t i p prawdopodobnie zapamiętał tę wartość, wywołując funkcję g e t e u i d podczas startu. Przy takim założeniu nie musi przeszukiwać pliku haseł, by

8. Sterowanie procesem odpowiedni numeryczny identyfikator). Takie wywołanie jest możliwe, ponieważ argument funkcji s e t u i d jest równy zachowanemu identyfikatorowi użytkownika. Teraz mamy rzeczywisty identyfikator użytkownika = nasz identyfikator użytkownika (bez zmiany), obowiązujący identyfikator użytkownika =

uucp,

zachowany identyfikator użytkownika = u u c p (bez zmiany).

5. Ponieważ obowiązujący identyfikator użytkownika w programie t i p odpowiada użytkownikowi uucp, więc program może teraz wykonać operacje na plikach ryglowania, tym razem chodzi tylko o zwolnienie rygli. Używając w ten sposób zachowanego identyfikatora użytkownika, możemy na początku i na końcu pracy procesu skorzystać z dodatkowych uprawnień, które uzyskujemy dzięki ustawionemu bitowi ustanowienia identyfikatora użytkownika. Jednak przez większość czasu działania proces ma typowe uprawnienia. Gdyby nie było możliwe odtworzenie na końcu wartości identyfikatora na podstawie zachowanego identyfikatora użytkownika, to moglibyśmy zdecydować się utrzymywać dodatkowe uprawnienia przez cały czas działania procesu (a w ten sposób sami prosilibyśmy się o kłopoty). Popatrzmy, co się stanie, gdy t i p utworzy program powłoki (wywołując kolejno funkcje fork oraz exec), aby obsłużyć nasze żądanie. Ponieważ zarówno rzeczywisty identyfikator użytkownika, jak i obowiązujący identyfikator użytkownika są po prostu naszymi normalnymi identyfikatorami (zob. krok 3 powyżej), więc nowy proces powłoki nie otrzymuje żadnych dodatkowych uprawnień. Utworzona w czasie pracy programu t i p nowa powłoka nie ma dostępu do zachowanego identyfikatora użytkownika, który wskazuje użytkownika o nazwie uucp, ponieważ funkcja exec kopiuje do zachowanego identyfikatora użytkownika w powłoce wartość obowiązującego identyfikatora użytkownika. W efekcie w procesie potomnym, który realizuje funkcję exec, wszystkie trzy identyfikatory użytkownika są normalnymi identyfikatorami użytkownika. Nasz opis użycia funkcji s e t u i d przez program t i p jest poprawny tylko wówczas, gdy program ten ma ustawiony bit ustanowienia identyfikatora użytkownika dla nadzorcy. Jest to konieczne, gdyż aby wywołanie funkcji s e t u i d mogło ustalić wartości wszystkich trzech identyfikatorów użytkownika, wołający ją musi mieć uprawnienia nadzorcy. W naszym przykładzie, by wszystko działo się zgodnie z opisem, funkcja s e t u i d musi ustawiać tylko obowiązujący identyfikator użytkownika. D [inkcje setreuid i setregid W systemie 4.3+BSD funkcja s e t r e u i d pozwala zamienić rzeczywisty identyfikator użytkownika na obowiązujący.

8.10. Zmiana identyfikatorów użytkownika i identyfikatorów grup

265

#include #include i n t s e t r e u i d (uid_t ruid, uid_t euid) ; i n t s e t r e g i d (gid_t rgid, gid_t egid); Przekazują: 0, jeśli wszystko w porządku; -1, jeśli wystąpił błąd

Zasada jest prosta: każdy użytkownik nie posiadający dodatkowych uprawnień może zawsze wymienić rzeczywisty identyfikator użytkownika na obowiązujący. W ten sposób programy z ustawionym bitem ustanowienia identyfikatora użytkownika mogą przełączać się z trybu pracy z typowymi przywilejami użytkownika do trybu pracy odpowiadającego ustawionemu bitowi ustanowionego identyfikatora użytkownika. Od czasu, gdy w normie POSDC.l wprowadzono koncepcję zachowanego identyfikatora użytkownika, rozszerzono tę funkcję, by było możliwe ustawienie obowiązującego identyfikatora użytkownika zgodnie z wartością zachowanego identyfikatora użytkownika. W systemie SVR4 te dwie funkcje są dostarczane w bibliotece zgodności z systemem BSD. System 4.3BSD nie miał zaimplementowanej opisanej wcześniej możliwości zachowania identyfikatora użytkownika. Zamiast tego używał funkcji s e t r e u i d oraz s e t r e gid. Umożliwiały one zamianę wartości tych identyfikatorów - program t i p , przygotowany w systemie BSD, korzystał właśnie z tej możliwości. Bądźmy jednak świadomi tego, że gdy t i p rozmnaża program powłoki, to przed wywołaniem funkcji exec musi nadać rzeczywistemu identyfikatorowi użytkownika normalną wartość identyfikatora użytkownika. Jeśli nie zrobiłby tego, to rzeczywisty identyfikator użytkownika mógłby wskazywać użytkownika uucp (na podstawie wymiany identyfikatorów dokonanej za pomocą funkcji s e t r e u i d ) , a proces powłoki mógłby wywołać funkcję s e t r e u i d i wymienić oba identyfikatory i uzyskać uprawnienia użytkownika uucp. Program t i p , stosując technikę programowania defensywnego, nadaje w procesie potomnym obu identyfikatorom użytkownika, rzeczywistemu oraz obowiązującemu, wartość odpowiadającą normalnemu identyfikatorowi użytkownika.

Funkcje seteuid i setegid Proponowana zmiana w standardzie POSIX.l dodaje dwie funkcje: s e t e u i d oraz s e t e g i d . Mają one służyć do zmiany obowiązującego identyfikatora użytkownika lub grupy. #include #include int seteuid (uid_t uid) ; i n t s e t e g i d ( g i d _ t gid) ; Przekazują: 0, jeśli wszystko w porządku; - 1 , jeśli wystąpił błąd

8. Sterowanie procesem

:66

Użytkownik nie posiadający dodatkowych uprawnień może zmienić swój obowiązujący identyfikator użytkownika i nadać mu wartość albo jego rzeczywistego identyfikatora użytkownika, albo zachowanego identyfikatora użytkownika. Gdy użytkownik ma uprawnienia nadzorcy, wówczas tylko obowiązujący identyfikator użytkownika uzyskuje wartość uid. (Jest więc inaczej niż w funkcji s e t u i d , która zmienia wszystkie trzy identyfikatory użytkownika). Ta proponowana zmiana w normie POSDC.l wymaga, by był zawsze zaimplementowany zachowany identyfikator użytkownika. Obie funkcje są dostępne w systemach SVR4 oraz 4.3+BSD. Na rysunku 8.3 podsumowujemy wszystkie opisane w tym podrozdziale funkcje, które modyfikują trzy różne identyfikatory użytkownika. nadzorca s e t r e u i d {ruid, euid)

nadzorca s e t e u i d (uid)

nadzorca s e t u i d (uid)

ruid

8.11. Pliki interpretowane

Odstęp między znakiem wykrzyknika a nazwą ścieżki {pathname) nie jes wymagany. Najczęściej w wierszu tym piszemy # ! /bin/sh

Nazwa pathname to bezwzględna nazwa ścieżki, ponieważ nie jest ona pod dawana żadnym dodatkowym operacjom (czyli system nie korzysta ze zmień nej PATH, by zlokalizować plik). Rozpoznaniem plików interpretowanycr zajmuje się jądro systemu w czasie realizacji funkcji systemowej exec. Rzeczywistym plikiem programu, który wywołuje funkcja exec, nie jest sam plil> interpretowany, lecz plik wskazany w pierwszym wierszu za pomocą nazwj ścieżki pathname. Musimy zawsze umieć odróżnić plik interpretowany (plit tekstowy, zaczynający się od wiersza # ! ) od samego interpretera (wskazanego za pomocą_pathname w pierwszym wierszu pliku interpretowanego).

Pamiętajmy, że niektóre systemy ograniczają rozmiar pierwszego wiersza w pliku interpretowanym do 32 znaków. Licznik znaków uwzględnia sekwencję # !, nazwę ścieżki, opcjonalny argument oraz odstępy.

Przykład

rzeczywisty bez uprawnień nadzorcy obowiązujący bez uprawnień nadzorcy zapamiętany ID setreuid ID _ setreuid ID wywołanie exec dla ustalonego ID bez uprawnień nadzorcy s e t u i d lub s e t e u i d

bez uprawnień nadzorcy s e t u i d lub s e t e u i d

Rys. 8.3 Zestawienie wszystkich funkcji ustawiających różne identyfikatory użytkownika (ID)

Przeanalizujmy teraz przykład, aby przekonać się, co jądro systemu robi z argumentami przekazywanymi przez funkcję exec, gdy plik do wykonania jest plikiem interpretowanym, oraz do czego służy argument podawany w pierwszym wierszu pliku interpretowanego. Program 8.10 wykonuje plik interpretowany. #include #include #include

Identyfikatory grupy Wszystko, co powiedzieliśmy dotychczas, można zastosować do opisu operacji manipulujących identyfikatorami grup. Funkcja s e t g i d nie ma żadnego wpływu na identyfikatory dodatkowych grup.

Pliki interpretowane Pliki interpretowane są używane w systemach SVR4 oraz 4.3+BSD. Są to pliki tekstowe zaczynające się od wiersza o postaci # ! pathname [opcjonalne-argumenty]

"ourhdr.h"

int main(void) pid_t

fB.11

26'

pid;

if ( (pid = fork()) < 0) err_sys("fork error"); else if (pid == 0) { /* potomek */ if (execl("/home/stevens/bin/testinterp", "testinterp", "myargl", "MY ARG2 err_sys("execl error"); if (waitpid(pid, NULL, 0) < 0) err_sys("waitpid error"); exit(0);

(char *) 0) < 0)

/* proces macierzysty */

Próg. 8.10 Program, który wykonuje (exec) plik interpretowany

8. Sterowanie procesem

168

Poniżej pokazujemy jednowierszowy plik interpretowany, który jest wykonywany przez funkcję exec, oraz wynik uruchomienia próg. 8.10: $ cat /home/stevens/bin/testinterp #!/home/stevens/bin/echoarg foo $ a.out argv[0]: /home/stevens/bin/echoarg argv[1]: foo argv[2]: /home/stevens/bin/testinterp argv[3]: rayargl argv[4]: MY ARG2

Program e c h o a r g (interpreter) jedynie wypisuje każdy argument wiersza poleceń. (Jest to próg. 7.2). Zwróćmy uwagę, że gdy system wywołuje z funkcji exec program interpretera (/home/stevens/bin/echoarg), wówczas a r g v [ 0 ] jest nazwą ścieżki programu interpretera, a r g v [ l ] opcjonalnym argumentem podawanym w pierwszym wierszu pliku interpretowanego, a pozostałe argumenty to nazwa ścieżki wołanego programu ( / h o m e / s t e v e n s / b i n / t e s t i n t e r p ) oraz drugi i trzeci argument wywołania funkcji e x e c l w próg. 8.10 (myargl oraz MY ARG2). Argumenty a r g v [ l ] oraz argv[2] w wywołaniu e x e c l zostały więc przesunięte w prawo o dwie pozycje. Zauważmy, że system pobiera pathname z wywołania funkcji e x e c l , a nie z pierwszego argumentu ( t e s t i n t e r p ) , zakładając, że nazwa pathname może zawierać więcej informacji niż pierwszy argument. •

'rzykład Często zdarza się, że w miejscu argumentów opcjonalnych w pierwszym wierszu pliku interpretowanego pojawia się opcja -f (oczywiście dotyczy to programów, które akceptują taką opcję). Przykładowo, możemy tak wywołać program awk(l): awk -f myfile

co oznacza, że program ma przeczytać i wykonać plik myfile. W wielu systemach istnieją dwie wersje języka programowania awk. Nazwa awk jest często kojarzona ze starym programem awk odpowiadającym oryginalnemu wydaniu rozprowadzanemu w wersji 7. Program nawk (nowy awk) zawiera wiele rozszerzeń i jest oparty na języku opisanym w [4]. Ta nowsza wersja umożliwia dostęp do argumentów wiersza poleceń, co będzie nam potrzebne w prezentowanych przykładach. W systemie SVR4 znajdujemy programy awk i oawk, które są identyczne, oraz uwagę, że w następnych wydaniach program awk będzie zgodny z programem nawk. W standardzie POSIX.2 nowa wersja języka jest określana nazwą awk i z tego właśnie programu korzystamy w tekście tej książki.

Używając opcji -f, możemy napisać w pliku interpretowanym: #!/bin/awk -f (dalej w pliku interpretowanym jest umieszczony program w języku awk)

269

8.11. Pliki interpretowane

Na przykład w próg. 8.11 podano zawartość pliku interpretowanego o nazwie /usr/local/bin/awkexample. # ! / b i n / a w k -f BEGIN { for (i = 0 ; i < ARGC; i++) p r i n t f "ARGV[%d] = %s\n" exit

i , ARGV[i]

Próg. 8.11 Program awk jako interpreter

Jeśli jednym z przedrostków ścieżek (w zmiennej PATH) jest nazwa / u s r / l o c a l / b i n , to możemy w ten sposób wykonać próg. 8.11 (zakładając, że jest ustawiony bit wykonania dla tego pliku): $ awkexample filel FILENAME2 f3 ARGV[0] = /bin/awk ARGV[1] = filel ARGV[2] = FILENAME2 ARGV[2] = f3

Wykonywany program awk ma następujące argumenty wiersza poleceń: /bin/awk -f /usr/local/bin/awkexample filel FILENAME2 f3

Interpreter otrzymuje nazwę ścieżki wskazującą plik interpretowany /usr/local/bin/awkexample. Nie wystarczy tylko nazwa pliku (czyli to, co napisaliśmy, wywołując program z powłoki), musimy podać nazwę ścieżki, ponieważ interpreter (w tym przypadku program /bin/awk) w celu lokalizacji pliku nie korzysta ze zmiennej środowiskowej PATH. Program awk, czytając plik interpretowany, ignoruje pierwszy wiersz, ponieważ znak # oznacza dla niego wiersz komentarza. Za pomocą poniższych poleceń możemy sprawdzić, jakie są argumenty wiersza poleceń: $ su uzyskujemy uprawnienia nadzorcy Password: wprowadzamy hasło nadzorcy # mv /bin/awk /bin/awk.save zachowujemy oryginalny program # cp /home/stevens/bin/echoarg /bin/awk i czasowo zamieniamy program awk # suspend zawieszamy powłokę nadzorcy za pomocą mechanizmu kontrolowania zadań' [1] + Stopped su $ awkexample f i l e l FILENAME2 f3 argv[0] /bin/awk argv[l] -f argv[2] /usr/local/bin/awkexample argv[3] filel argv[4] FILENAME2 argv[5] f3 $ fg wznawiamy powłoką nadzorcy za pomocą mechanizmu kontrolowania zadań su # mv /bin/awk.save /bin/awk odtwarzamy oryginalny program # exit opuszczamy powlokę nadzorcy

8. Sterowanie procesem

plikiem wykonywalnym, ale nie maszynowym modułem wykonywalnym, więc jest przekazywany błąd, co dla funkcji e x e c l p oznacza, że plik jest skryptem powłoki (jest to prawdą). Następnie funkcja exec wywołuje program /bin/sh, podając w argumencie nazwę ścieżki skryptu powłoki. Powłoka poprawnie przetwarza nasz skrypt, ale by uruchomić program awk, wywołuje kolejno funkcje fork, exec i wait. Gdy zastąpimy skrypt interpretera skryptem powłoki, dochodzi więc wiele nadmiarowych działań. 3. Skrypty interpretera umożliwiają napisanie skryptów powłoki za pomocą innych programów niż /bin/sh. Gdy funkcja execlp znajdzie plik wykonywalny nie będący maszynowym modułem wykonywalnym, musi zdecydować, jaką powłokę ma wywołać. Zawsze w takiej sytuacji używa programu /bin/sh. Używając skryptu interpretera, możemy jednak napisać

W tym przykładzie jest wymagane przekazanie opcji -f interpreterowi. Jak powiedzieliśmy, w ten sposób program awk dowiaduje się, gdzie znajduje się plik z programem. Gdybyśmy usunęli tę opcję z pliku interpretowanego, wówczas uzyskalibyśmy poniższy wynik: $ awkexample filel FILENAME2 f3 /bin/awk: syntax error at source linę 1 context is >>> /usr/local do przekierowania wejścia i wyjścia. Jeśli nie stosujemy powłoki do wykonywania poleceń, lecz próbujemy po prostu wykonać samo polecenie, to wszystko się nieco komplikuje. Po pierw-

273

8.12. Funkcja system

sze powinniśmy wówczas wywołać funkcję execlp, a nie e x e c l , aby użyć zmiennej środowiskowej PATH, podobnie jak to robi powłoka. Oprócz tego musimy sami podzielić zakończony pustym znakiem napis na poszczególne argumenty, które potem przekażemy funkcji execlp. Wreszcie nie możemy w tej sytuacji użyć typowych dla powłoki znaków specjalnych (metaznaków). #include #include #include linclude



int systemfconst char *cmdstring) { pid_t pid; int status;

/* wersja bez obsługi sygnałów */

if (cmdstring == NULL) return(l); /* taką samą wartość przekazuje procesor poleceń w Uniksie */ if ( (pid = fork()) < 0) { status = -1; /* prawdopodobnie brakuje procesów */ } else if (pid == 0) { /* potomek */ execl("/bin/sh", "sh", "-c", cmdstring, (char *) 0); _exit(127); /* błąd execl */ } else { /* proces macierzysty */ while (waitpid(pid, Łstatus, 0) < 0) if (errno != EINTR) { status = -1; /* funkcja waitpid() przekazała błąd różny od EINTR */ break;

return(status);

Próg. 8.12 Funkcja s y s t e m (bez obsługi sygnałów)

Zwróćmy uwagę, że wywołujemy funkcję _ e x i t , a nie e x i t . W ten sposób zapobiegamy opróżnieniu buforów standardowego wejścia-wyjścia (które w wyniku wywołania fork mogły zostać skopiowane z procesu macierzystego do potomnego). Program 8.13 służy do przetestowania tej wersji funkcji system. (Funkcję p r _ e x i t określiliśmy w próg. 8.3). Jego uruchomienie daje poniższy wynik:

74

8. Sterowanie procesem $ a.out Thu Aug 29 14:24:19 MST 1991 normal termination, exit status = 0 sh: nosuchcommand: not found normal termination, exit status = 1

8.12. Funkcja system

czy nie zakończył się proces potomny wygenerowany przez funkcję system. a gdy zdarzy się, że jakiś inny potomek zakończy pracę przed procesem identyfikowanym za pomocą wartości pid, to wówczas ten identyfikator procesu oraz stan jego zakończenia zostaną skasowane. W istocie, właśnie brak możliwości oczekiwania na konkretnego potomka był, zgodnie z uzasadnieniami normy POSDC.l, motywem dodania funkcji w a i t p i d . W podrozdziale 14.3 zobaczymy, że ten sam problem występuje, gdy używamy funkcji popen oraz p c l o s e , i nie możemy skorzystać z funkcji w a i t p i d .

dotyczy polecenia d a t ę dotyczy polecenia nosuchcommand

stevens console Aug 25 11:49 stevens ttypO Aug 25 11:49 stevens t t y p l Aug 29 05:56 stevens ttyp2 Aug 29 05:56 normal termination, exit s t a t u s = 44 dotyczy polecenia exit .nclude Lnclude include

. Ma on następującą postać: typedef u_short_t comp_t;

/* 3-bitowy wykładnik o podstawie 8 * oraz 13-bitowy ułamek */

struct acct char ac_flag; char ac stat;

uid_t ac_uid; gid__t ac_gid; dev_t ac_tty; time_t ac_btime; comp_t ac_utime; comp_t ac_stime; comp_t ac_etime; comp_t ac_mem; comp_t ac_io; comp_t ac_rw; char ac coiran[8] ;

sygnalizator (patrz tab. 8.6) */ stan zakończenia (tylko sygnalizatory sygnału i pliku core) */ (nie istnieje w systemach BSD) */ rzeczywisty identyfikator użytkownika */ rzeczywisty identyfikator grupy */ terminal sterujący */ kalendarzowy czas startu */ czas użytkownika procesora (liczba taktów zegara) */ systemowy czas procesora (liczba taktów zegara) */ czas zużyty (liczba taktów zegara) */ średnie zużycie pamięci */ liczba przesłanych bajtów (odczytanych i zapisanych) */ liczba odczytanych i zapisanych bloków */ nazwa polecenia: [8] w systemie SVR4, [10] w systemie 4.3+BSD */

W systemach berkelejowskich łącznie z 4.3+BSD nie ma pola ac s t a t . W czasie wykonania procesu w polu ac_f lag są umieszczane informacje o pewnych zdarzeniach. Wymieniamy je w tab. 8.6.

8. Sterowanie procesem Tabela 8.6

Wartości pola ac_f l a g w rekordzie rejestrującym

ac_flag

Opis

AFORK

proces powstał w wyniku wywołania f ork, ale nigdy nie wywołał exec

AS U

proces używał uprawnień nadzorcy

ACOMPAT

proces używał trybu zgodności (tylko w komputerach VAX)

ACORE

proces utworzył obraz pamięci (nie w systemie SVR4)

AXSIG

proces został skasowany przez sygnał (nie w systemie SVR4)

Dane potrzebne do utworzenia rekordu rejestracji (czasy procesora, liczba przesłanych znaków itp.) pochodzą z utrzymywanej przez jądro systemu tablicy procesu. Tablica ta jest inicjowana podczas tworzenia nowego procesu (np. w procesie potomnym po funkcji fork). Rekord rejestrujący powstaje, gdy proces się kończy. Oznacza to, że uporządkowanie rekordów w pliku będącym rejestrem jest zgodne z kolejnością zakończenia procesów, a nie z kolejnością ich rozpoczęcia. W celu uporządkowania procesów zgodnie z czasem startu musimy przejrzeć rejestr i posortować rekordy na podstawie pola zawierającego kalendarzowy czas rozpoczęcia. Nie jest to jednak idealne rozwiązanie, gdyż czas kalendarzowy jest podawany z dokładnością do sekundy (podrozdz. 1.10), a w ciągu jednej sekundy może wystartować wiele procesów. Zamiast tego możemy skorzystać z pola zawierającego czas zużyty podawany jako liczba taktów zegara (zazwyczaj na jedną sekundę przypada od 50 do 100 taktów zegara). Nie mamy jednak informacji o czasie zakończenia procesu, wiemy tylko, kiedy wystartował, oraz znamy uporządkowanie czasów zakończenia pracy. Oznacza to, że chociaż czas zużyty jest dokładniejszy od czasu startu, to nie możemy nadal na podstawie danych rejestrujących odtworzyć kolejności rozpoczynania pracy przez procesy. Rekordy rejestrujące są związane z procesami, a nie programami. Jądro systemu inicjuje rekord rejestru dla nowego procesu potomnego po wykonaniu funkcji fork, a nie w czasie wywołania funkcji exec. Chociaż funkcja exec nie tworzy nowego rekordu rejestrującego, to zmienia się wówczas nazwa polecenia oraz jest zerowany sygnalizator AFORK. Oznacza to, że jeśli wykonujemy sekwencję trzech programów (program A wykonuje program B, B wykonuje C, a C wywołuje funkcję e x i t ) , to w rejestrze jest zapisywany jeden rekord. Nazwa polecenia w takim rekordzie odpowiada nazwie programu C, ale np. czasy procesora są sumą czasów w programach A, B i C .

rzykład Aby dysponować jakimiś danymi rejestrującymi do przykładowej analizy, uruchomimy próg. 8.16, który czterokrotnie wywołuje funkcję fork. Każdy proces potomny robi coś konkretnego, a następnie kończy pracę. Na rysunku 8.4 zobrazowaliśmy działanie tego programu.

8.13. Rejestrowanie procesów #include #include

279

"ourhdr.h"

int main(void) pid_t

pid;

if ( (pid = fork()) < 0) err_sys("fork error") else if (pid != 0) { sleep(2) ; exit (2);

/* proces macierzysty */ /* kończymy ze stanem wyjścia 2 */

/* pierwszy potomek */ if ( (pid = fork () ) < 0) err_sys("fork error"); else if (pid != 0) { sleep(4); abort(); /* kończymy, tworząc obraz pamięci */ /* drugi potomek */ if ( (pid = fork()) < 0) err_sys("fork error"); else if (pid != 0) { execl("/usr/bin/dd", "dd", "if=/boot", "of=/dev/null", NULL); exit(7); /* nie powinniśmy tu dotrzeć */ /* trzeci potomek */ if ( (pid = fork () ) < 0) err_sys("fork error"); else if (pid != 0) { sleep(8); exit (0); /* normalne wyjście */ } sleep(6); kill(getpidO, SIGKILL); exit(6);

/* czwarty potomek */ /+ kończymy sygnałem bez tworzenia obrazu */ /* n i e powinniśmy tu dotrzeć */

Próg. 8.16 Program generujący dane rejestrujące

Program 8.17 drukuje na podstawie rekordów rejestrujących wybrane pola. Następnie wykonujemy poniższe kroki:

'

• > i

1. Przechodzimy do trybu nadzorcy i wywołujemy polecenie aceton, by < uaktywnić rejestrowanie. Pamiętajmy, że podczas zakończenia obsługi ' tego polecenia rejestrowanie będzie już włączone, a więc pierwszy rekord w rejestrze powinien dotyczyć wykonywanego polecenia.

:80

8. Sterowanie procesem

proces macierzysty

„ sleep(2) \ ^ - ^ e x i t (2)

pierwszy potomek

,c s l e e p (4) ^*-\> 13) & 7; while (exp-- > 0) val *= 8; returri (val) ;

Próg. 8.17

/* ułamek 13-bitowy */ /* wykładnik 3-bitowy (0-7) */

Wydruk wybranych pól z systemowego pliku rejestrującego

8. Sterowanie procesem Pamiętajmy, że pole a c s t a t nie jest prawdziwym stanem zakończenia procesu. Zawiera ono tylko część informacji typowego stanu zakończenia opisanego w podrozdz. 8.6. Jeżeli proces zakończył się awaryjnie, to w bajcie a c f l a g jest umieszczony bit wskazujący, czy powstał plik core (jest to na ogół najstarszy bit), oraz numer sygnału (zajmujący 7 najniższych bitów). Gdy proces skończył pracę normalnie, wówczas nie jesteśmy w stanie na podstawie pliku rejestru dowiedzieć się o stanie wyjścia. W naszym przykładzie dla pierwszego potomka ta wartość wyniosła 128+6. Wartość 128 odpowiada bitowi pliku core, a 6 wskazuje w tym systemie pojawienie się sygnału SIGABRT (ten sygnał jest generowany przez wywołanie funkcji a b o r t ) . Wartość 9 dla czwartego potomka oznacza, że przechwycono sygnał S I G K I L L . Nie możemy powiedzieć na podstawie rejestru, że proces macierzysty wywołał funkcję e x i t z argumentem 2, a trzeci proces potomny zakończył pracę ze stanem wyjścia równym 0. Proces dd realizowany przez drugiego potomka skopiował plik /boot o rozmiarze 110 888 bajtów. Liczba przesłanych znaków w czasie operacji wejścia-wyjścia jest jednak ponad dwa razy większa. Jest tak, ponieważ 110 888 bajtów przeczytano, a następnie tyle samo bajtów zapisano. Nawet gdy wyjście jest skierowane do pustego urządzenia, wypisywane znaki są zliczane. Wartości pola a c _ f l a g są zgodne z oczekiwaniami. Sygnalizator Fjest ustawiony we wszystkich procesach potomnych oprócz drugiego potomka, który nie wywołał funkcji e x e c l . Sygnalizator ten nie jest ustawiony w procesie macierzystym, ponieważ interakcyjna powłoka, która wykonywała proces macierzysty, wywołała kolejno funkcje f ork oraz exec z plikiem a. out. Sygnalizator zrzucenia obrazu pamięci do pliku core (D) jest włączony w pierwszym procesie potomnym, gdyż wywołał on funkcję a b o r t . Funkcja a b o r t generuje sygnał SIGABRT, który tworzy obraz pamięci w pliku. Włączony sygnalizator X w tym procesie wskazuje zakończenie przez sygnał. Sygnalizator X jest ustawiony też w czwartym procesie, ale sygnał S I G K I L L nie tworzy pliku core, jedynie kończy proces. Zwróćmy na koniec jeszcze uwagę, że chociaż pierwszy potomek utworzył plik core, to liczba znaków przesłanych w operacjach wejścia-wyjścia jest w tym procesie równa 0. Oznacza to, że wejście-wyjście związane z zapisywaniem pliku core nie obciąża procesu. •

i

Identyfikacja użytkownika Każdy proces może poznać swoje rzeczywiste i obowiązujące identyfikatory użytkownika oraz grupy. Nieraz jednak chcemy się dowiedzieć, jaka jest nazwa użytkownika, który uruchamia program. Możemy w tym celu wywołać funkcję getpwuid ( g e t u i d () ), ale co stanie się, gdy jeden użytkownik ma wiele nazw, a każda nazwa ten sam identyfikator użytkownika? (Jedna osoba

283

8.15. Czasy procesu

może mieć kilka wpisów w pliku haseł, które mają np. taki sam numeryczny identyfikator użytkownika, ale różnią się powłoką logowania). System typowo rejestruje nazwę, z jaką się logujemy (podrozdz. 6.7), a funkcja g e t l o g i n umożliwia jej przechwycenie. #include char *getlogin(void) ; Przekazuje: wskaźnik do napisu będącego nazwą logowania, jeśli wszystko w porządku; NULL, jeśli wystąpił błąd

Jeśli proces nie jest dołączony do terminalu sterującego, z którego użytkownik wylogował się, to wywołanie tej funkcji może się nie udać. Takie procesy nazywamy demonami. Będziemy o nich mówić w rozdz. 13. Mając nazwę użytkownika, możemy następnie za pomocą funkcji getpwnam wyszukać ją w pliku haseł (by np. określić powłokę logowania). W celu poznania nazwy logowania systemy uniksowe dawniej wywoływały funkcję ttyname (podrozdz. 11.9), a następnie próbowały odnaleźć odpowiedni wpis w pliku utmp (podrozdz. 6.7). System 4.3+BSD zapamiętuje nazwę logowania w pozycji tablicy procesów i dostarcza funkcje systemowe, które pobierają oraz zapisują tę nazwę. W Systemie V istnieje funkcja cuserid, która przekazuje nazwę logowania. Wywołuje ona z kolei funkcję getlogin, a gdy ta nie powiedzie się, wykonuje getpwuid (getuid () ). Wprawdzie dokument IEEE Std. 1003.1-1988 specyfikuje funkcję cuserid, ale ma ona służyć do obsługi obowiązującego identyfikatora użytkownika, a nie rzeczywistego. W wersji POSK.l z 1990 roku usunięto funkcję cuserid. Standard F I P S 151-1 wymaga, by powłoka logowania definiowała zmienną środowiskową LOGNAME, która ma zawierać nazwę podaną w czasie logowania. W systemie 4.3+BSD tę zmienną ustawia program login, a program powłoki dziedziczy ją. Jednak użytkownik może zmodyfikować każdą zmienną środowiska, zatem nie powinniśmy używać zmiennej LOGNAME, aby potwierdzać tożsamość użytkownika. Zamiast tego należy korzystać z funkcji g e t l o g i n .

8.15

Czasy procesu

'

W podrozdziale 1.10 opisaliśmy trzy czasy, które możemy zmierzyć: bieżący ; czas zegarowy, czas użytkownika procesu i czas systemowy procesu. Każdy proces może wywołać funkcję times i pobrać wartości czasów, które dotyczą , jego procesu oraz każdego zakończonego procesu potomnego. #include clock_t times(struct tms *buf); Przekazuje: zużyty czas zegarowy liczony w taktach zegara, jeśli wszystko w porządku; - 1 , jeśli wystąpił błąd

8. Sterowanie procesem

Funkcja ta wypełnia strukturę tms wskazywaną przez argument buf. struct tms { clock_t tms_utime; clock_t tms_stime; clock_t tms_cutime; clock t tms cstime;

Systemy berkelejowskie, łącznie z 4.3BSD, odziedziczyły funkcję t i m e s z wersji 7, która nie przekazywała czasu zegarowego. W starszych wersjach w przypadku poprawnego zakończenia otrzymywaliśmy wartość 0, a wartość -1 oznaczała błąd. System 4.3+BSD stosuje posiksową wersję tej funkcji. Systemy 4.3+BSD oraz SVR4 (w ramach biblioteki zgodności z systemem BSD) mają funkcję g e t r u s a g e ( 2 ) . Przekazuje ona czasy procesora oraz 14 innych wartości, określających zużycie zasobów.

ładu Program 8.18 pobiera poszczególne argumenty wiersza poleceń i zakładając, że są to polecenia powłoki, wykonuje je, pobiera czasy obsługi poleceń i drukuje wartości ze struktury tms. Oto wynik uruchomienia programu: $ a.out "sleep 5" "datę' command: s l e e p 5

real: 5 25 user: 0 00 sys: 0 00 child user: 0.02 child sys: 0.13 normal termination, exit

command: datę Sun Aug 18 09:25:38 real: 0.27 user: 0.00 sys: 0.00 child user: child sys: normal termination,

/* czas użytkownika procesu */ /* czas systemowy procesu */ /* czas użytkownika procesu, dotyczy zakończonych procesów potomnych */ /* czas systemowy procesu, dotyczy zakończonych procesów potomnych */

Widzimy, że w strukturze tms nie jest zapamiętywany żaden pomiar dotyczący bieżącego czasu zegarowego; jest on natomiast wartością funkcji. Czas ten nie jest liczony począwszy od pewnego ustalonego momentu w przeszłości, więc nie możemy używać jego wartości bezwzględnej - zawsze traktujemy go w sposób względny. Możemy na przykład wywołać funkcję times i zapamiętać odebraną wartość. Następnie wywołujemy ponownie tę samą funkcję i odejmujemy od nowej wartości zwracanej zachowaną poprzednią wartość. Różnica reprezentuje czas zegarowy. (W przypadku długo pracującego procesu czas zegarowy może przekroczyć ustaloną wartość - zob. ćw. 1.6). Dwa pola w strukturze są przeznaczone dla procesów potomnych i zawierają wartości czasów dla tych procesów, na które proces macierzysty oczekiwał za pomocą funkcji wait. Wszystkie wartości typu clock_t, które przekazuje funkcja times, są przetwarzane na sekundy przy użyciu liczby taktów zegara na sekundę, a wartość stałej _SC_CLK_TCK otrzymujemy, wywołując funkcję sysconf (p. 2.5.4).

285

8.15. Czasy procesu

MST 1991

0.05 0.10 exit status = 0

W obu przykładach wszystkie czasy dotyczą pracy procesu potomnego; właśnie w tym procesie pracuje powłoka oraz są w nim wykonywane polecenia. iinclude iinclude

"ourhdr.h"

static void pr_times(clock_t, struct tms *, struct tms * ) ; static void do cmdfchar * ) ; int mainfint argc, char *argv[]) { int i; for (i = 1 ; i < argc; i++) do cmd(argv[i]); /* jedno wywołanie dla jednego argumentu wiersza poleceń */ exit (0); } static void do_cmd(char *cmd) /* wykonujemy polecenie ze zmiennej cmd i mierzymy czas */ { struct tms tmsstart, tmsend; clock_t start, end; int status; fprintf(stderr, "\ncommand: %s\n", cmd); if ( (start = times(Stmsstart)) == -1) err_sys("times error");

/* wartości początkowe */

if ( (status = system(cmd)) < 0) err_sys("system() error");

/* wykonanie polecenia */

if ( (end = times(stmsend)) == -1) err_sys("times error");

/* wartości końcowe */

pr_times(end-start, Łtmsstart, stmsend); pr_exit(status); static void pr times(clock_t real, struct tms *tmsstart, struct tms *tmsend)

8. Sterowanie procesem s t a t i c long if

clktck = 0;

(clktck == 0)

/* za pierwszym razem pobieramy liczbę taktów zegara na sekundę */ if ( (clktck = sysconf(_SC_CLK_TCK)) < 0) err_sys("sysconf e r r o r " ) ; f p r i n t f ( s t d e r r , " r e a l : %7.2f\n", r e a l / (double) c l k t c k ) ; f p r i n t f ( s t d e r r , " u s e r : %7.2f\n", (tmsend->tms_utime - tmsstart->tms_utime) / (double) clktck) ; f p r i n t f ( s t d e r r , " sys: %7.2f\n", (tmsend->tms_stime - tmsstart->tms_stime) / (double) c l k t c k ) ; f p r i n t f ( s t d e r r , " c h i l d user: %7.2f\n", (tmsend->tms_cutime - tmsstart->tms_cutime) / (double) clktck) , f p r i n t f ( s t d e r r , " c h i l d sys: %7.2f\n", (tmsend->tms_cstime - tmsstart->tms cstime) / (double) clktck) ,

8.16. Podsumowanie

Zakładając, że Czytelnik opanował już tematykę pojedynczych procesów oraz ich procesów potomnych, w następnym rozdziale zajmiemy się relacjami między procesami - sesjami oraz sterowaniem zadań. Zakończymy omawianie procesów w rozdz. 10, w którym opiszemy obsługę sygnałów.

Ćwiczenia 8.1

W programie 8.2 powiedzieliśmy, że zamiana wywołania _ e x i t funkcją e x i t powoduje zamknięcie strumienia wyjściowego. Zmodyfikuj program, by przekonać się, że funkcja p r i n t f rzeczywiście przekaże wówczas wartość - 1 .

8.2

Przypomnij sobie typowy układ w pamięci pokazany na rys. 7.3. Wiadomo, że ramki stosu, związane z poszczególnymi funkcjami, są zwykle przechowywane na stosie, a jednocześnie po wywołaniu funkcji vfork proces potomny pracuje, korzystając z przestrzeni adresowej procesu macierzystego. Co się więc stanie, jeśli wywołamy funkcję vf ork z innej funkcji niż main, a potomek nie wykona powrotu ze swojej funkcji po wywołaniu vfork? Napisz program, który to sprawdzi, oraz zobrazuj na rysunku, co się dzieje.

8.3

Jeśli wykonamy próg. 8.7 jeden raz,

Próg. 8.18 Pomiar czasu wykonania wszystkich poleceń przekazanych w wierszu poleceń

Powtórzmy jeszcze przykład z podrozdz. 1.10 $ a . o u t "cd / u s r / i n c l u d e ;

g r e p _POSIX_SOURCE * / * . h > / d e v / n u l l "

command: real: user:

g r e p _POSIX_SOURCE * / * . h > / d e v / n u l l

sys:

cd / u s r / i n c l u d e ; 18.67 0.00

$ a.out to wynik jest poprawny. Gdy uruchomimy ten program wielokrotnie $ a.out ; a.out ; a.out output from parent ooutput from parent ouotuptut from child put from parent output from child utput from child

0.02

child user: 0.43 child sys: 4.13 normal termination, exit status = 0

Zgodnie z naszymi oczekiwaniami, wszystkie trzy wartości (czas rzeczywisty oraz czasy w procesach potomnych) są podobne do otrzymanych w podrozdz. 1.10. •

6

Podsumowanie Pisanie zaawansowanych programów sieciowych wymaga dogłębnego poznania zagadnienia sterowania procesami w Uniksie. Na szczęście trzeba opanować niewiele funkcji: fork, rodzinę exec, _ e x i t , wait oraz w a i t p i d . Są one używane w wielu aplikacjach. Przy okazji omawiania funkcji fork zapoznaliśmy się z sytuacjami wyścigów. Dokładne przeanalizowanie funkcji system oraz sposobów rejestrowania procesów pozwoliło nam inaczej spojrzeć na funkcje sterujące procesami. Przyjrzeliśmy się też nietypowej odmianie funkcji exec, poznaliśmy sposób funkcjonowania plików interpretowanych. Opisaliśmy różnice między rozmaitymi identyfikatorami użytkownika i grupy (rzeczywiste, obowiązujące i zachowane), których funkcjonowanie jest nadzwyczaj ważne, gdy przygotowujemy bezpieczne programy z ustawionym bitem ustanowionego identyfikatora użytkownika.

287

to wynik jest zły. Co się stało? Jak można to poprawić? Czy taki problem zdarzy się, gdy pozwolimy, by potomek wypisał komunikat pierwszy? 8.4

W programie 8.10 wywołujemy funkcję execl, podając jako pathname nazwę intepretowanego pliku. Gdybyśmy zamiast tego wywołali funkcję execip, podając ]dkofilename nazwę t e s t i n t e r p i gdyby katalog /home/stevens/bin był jednym z przedrostków ścieżki, wówczas co zostałoby wypisane w czasie urucho- | mienia programu jako argument argv [ 2 ] ?

8.5

W jaki sposób proces może otrzymać swój zachowany identyfikator użytkownika?

8.6

Napisz program, który utworzy procesy typu zombie, a następnie wywoła funkcję \ system, by wykonać polecenie ps(l) i sprawdzić, że powstał tego typu proces.

8.7

W podrozdziale 8.9 wspomnieliśmy, że norma POSEK. 1 wymaga, aby wszystkie j otwarte strumienie katalogowe zostały zamknięte w wyniku wywołania exec. Sprawdź to w następujący sposób: wywołaj funkcję opendir dla katalogu główne- ^ go, obejrzyj strukturę DIR W implementacji systemu, z którego korzystasz, : i wydrukuj sygnalizator zamknięcia podczas wywołania programu. Następnie otwórz ten sam katalog do odczytu i wydrukuj wartość tego sygnalizatora.

9

Relacje między procesami

Wprowadzenie W poprzednim rozdziale dowiedzieliśmy się, że między procesami istnieją pewne relacje. Każdy proces ma swój proces macierzysty. Proces macierzysty otrzymuje informację o zakończeniu pracy przez potomka i może uzyskać stan końcowy procesu potomnego. Podczas opisywania funkcji w a i t p i d (podrozdz. 8.6) wspomnieliśmy o grupach procesów, powiedzieliśmy też, jak możemy oczekiwać na zakończenie dowolnego procesu należącego do danej grupy. W tym rozdziale przyjrzymy się szczegółowo grupom procesów oraz nowej koncepcji sesji wprowadzonej w standardzie POSIX.l. Przeanalizujemy też relacje między powłoką wywoływaną w czasie logowania w systemie a wszystkimi procesami, które są następnie inicjowane z powłoki. Nie da się opisać tych zależności bez omówienia sygnałów. Z kolei, aby zaprezentować sygnały, konieczna jest znajomość wielu koncepcji przedstawianych w tym rozdziale. Czytelnicy, którym teoria sygnałów jest zupełnie obca, powinni najpierw, chociaż pobieżnie, zapoznać się z rozdz. 10.

Logowania terminalowe Zacznijmy od przeglądu programów wykonywanych podczas logowania w systemie Unix. Dawniej w takich systemach jak wersja 7 użytkownik logował się, korzystając z terminali „nieinteligentnych", podłączonych do linii szeregowej typu RS-232. Terminale były albo lokalne (dołączone bezpośrednio), albo zdalne (dołączone przez modem). W każdym przypadku logowanie musiało przejść przez systemowy program obsługi urządzenia terminalowego. W systemach PDP-11 najpopularniejsze były urządzenia DH-11 oraz DZ-11.

289

9.2. Logowania terminalowe

Każda stacja ma ustaloną liczbę urządzeń terminalowych, dlatego obowiązywało ograniczenie liczby osób pracujących równolegle. Procedura, którą teraz opiszemy, jest używana do logowania w systemie Unix za pomocą terminalu RS-232. Logowanie terminalowe w systemie 4.3+BSD Procedura logowania nie zmieniła się wiele od ostatnich 15 lat. Administrator systemu tworzy plik, typową jego nazwą jest / e t c / t t y s , w którym każdy wiersz opisuje jedno urządzenie terminalowe. W każdym wierszu jest podawana nazwa urządzenia oraz inne parametry, które są przekazywane do programu g e t t y . Przykładowym parametrem jest szybkość linii terminalowej, podawana w bodach. W czasie wstępnego ładowania systemu jądro tworzy proces i n i t o identyfikatorze 1, który doprowadza system do trybu wieloużytkowego. Proces i n i t czyta plik / e t c / g e t t y i dla każdego terminalu dopuszczającego logowanie wywołuje najpierw funkcję fork, a następnie funkcję exec w celu uruchomienia programu g e t t y . Scenariusz wydarzeń obrazuje rys. 9.1. identyfikator procesu 1 init , fc o r k

init

i jedno wywołanie f o r k ? ,, , f • , dla każdego terminalu

J

1^ każdy potomek wykonuj e (e x e c) j program g e t t y getty Rys. 9.1 Procesy wywoływane przez i n i t w celu umożliwienia logowań terminalowych

Oba identyfikatory użytkownika, rzeczywisty oraz obowiązujący, są we wszystkich procesach pokazanych na rys. 9.1 równe 0 (czyli wszystkie te procesy mają uprawnienia nadzorcy). Gdy proces i n i t wywołuje funkcję exec, by wykonać program g e t t y , to jego lista środowiska jest pusta. Program g e t t y otwiera urządzenie terminalowe do odczytu i do zapisu, Jeśli urządzenie to jest modemem, funkcja open może zostać opóźniona w programie obsługi urządzenia do czasu, gdy modem wybierze numer i otrzyma odpowiedź od swojego partnera. Po otwarciu urządzenia są już z nim związane deskryptory plików o numerach 0, 1 oraz 2. Program g e t t y wypisuje komunikat, np. l o g i n : , i oczekuje na wprowadzenie nazwy przez

9. Relacje między procesami

użytkownika. Jeśli terminal ma możliwość pracy z różnymi szybościami, to program g e t t y potrafi wykryć specjalne znaki wskazujące, że trzeba zmienić szybkość terminalu (podawaną w bodach). W podręczniku dla konkretnego systemu Unix można znaleźć więcej informacji na temat programu g e t t y oraz plików danych (np. g e t t y t a b ) sterujących jego pracą. Program g e t t y kończy działanie, gdy wprowadzimy naszą nazwę logowania. Wywołuje wówczas program l o g i n , np. w taki sposób execle("/usr/bin/login", "login", "-p", username, (char *) 0, envp);

(W pliku g e t t y t a b mogą być podane opcje, które spowodują, że będą wywoływane inne programy, ale domyślnie jest uruchamiany program login). Gdy proces i n i t wywołuje program g e t t y , wówczas jego środowisko jest puste. Program g e t t y tworzy środowisko dla programu l o g i n (argument envp); zostaje określona nazwa terminalu (np. TERM=f oo, typ terminalu pochodzi z pliku g e t t y t a b ) i wartości innych zmiennych środowiskowych, zgodnie z plikiem g e t t y t a b . Opcja -p programu l o g i n zleca zachowanie przekazanego środowiska oraz dodanie go do środowiska tego programu, a nie zastąpienie. Na rysunku 9.2 pokazujemy stan wszystkich procesów tuż po wywołaniu programu l o g i n . identyfikator procesu 1 init

fork

> odczyt / e t c / t t y s ; nedno wywołanie f ork dla każdego terminalu; utworzenie pustego środowiska

otwarcie urządzenia terminalowego "\ (deskryptory 0, 1,2); J odczytanie nazwy użytkownika; zainicjowanie środowiska

login Rys. 9.2 Stany procesów po wywołaniu programu l o g i n

Wszystkie procesy z rys. 9.2 dziedziczą z procesu i n i t uprawnienia nadzorcy. Identyfikatory wszystkich trzech procesów, umieszczonych na rys. 9.2 poniżej procesu i n i t , są takie same, ponieważ w wyniku wywołania exec nie ulega zmianie identyfikator procesu. Procesy macierzyste wszystkich trzech procesów (oprócz procesu i n i t ) mają identyfikatory równe 1. Program l o g i n realizuje wiele czynności. Zna już nazwę użytkownika, więc za pomocą funkcji getpwnam pobiera odpowiedni wpis z pliku haseł.

291

Następnie wywołuje funkcję getpass(3), aby wyświetlić sekwencję zachęty Password:, i odczytuje podane hasło (oczywiście nie pokazując czytanych znaków na ekranie). Potem wywołuje funkcję crypt(3), aby zaszyfrować wprowadzone przez użytkownika hasło i porównuje otrzymany wynik z wartością pola pw_passwd w odpowiednim wpisie pliku haseł. Jeśli próba załogowania nie powiedzie się z powodu podania niewłaściwego hasła (dopuszcza się kilka kolejnych prób podania nazwy oraz hasła), to program l o g i n wywołuje funkcję e x i t z argumentem równym 1. O takim zakończeniu zostanie powiadomiony proces macierzysty ( i n i t ) , który wywołuje wówczas ponownie funkcję fork, a potem exec dla programu g e t t y i cała procedura obsługi terminalu jest powtarzana. Jeśli logowanie w systemie zakończy się pomyślnie, to program l o g i n ustala katalog domowy (funkcja c h d i r ) . Wlogowany użytkownik staje się właścicielem urządzenia, na którym pracuje, oraz właścicielem jego grupy (funkcja chown). Jednocześnie program modyfikuje prawa dostępu do urządzenia, ustawia bity odczytu i zapisu przez użytkownika oraz zapisu przez grupę. Za pomocą funkcji s e t g i d oraz i n i t g r o u p s ustala identyfikatory grupy odpowiadające wlogowanemu użytkownikowi. Następnie na podstawie posiadanej informacji inicjuje środowisko; ustawia wartości zmiennych zawierających: nazwę katalogu domowego (HOME), powłokę (SHELL), nazwę użytkownika (USER i LOGNAME) oraz domyślną ścieżkę (PATH). Na koniec program l o g i n ustala identyfikator użytkownika równy identyfikatorowi osoby wlogowanej (funkcja s e t u i d ) i wywołuje odpowiednią powłokę logowania, np. execl("/bin/sh",

init

getty

9.2. Logowania terminalowe

"-sh",

(char *) 0);

Znak minusa umieszczony na pierwszej pozycji argumentu argv [0] sygnalizuje, że wywołanie odbywa się z programu logowania. Powłoki mogą analizować ten znak i dodawać odpowiednie czynności startowe. Program l o g i n w rzeczywistości robi jeszcze więcej niż tu opisaliśmy. Może na przykład wypisywać z pliku komunikat dnia, sprawdzać, czy nadeszła nowa poczta itp. Dla nas istotne są tylko te własności, które omówiliśmy. Przypomnijmy sobie naszą dyskusję w podrozdz. 8.10 na temat funkcji s e t u i d , gdzie powiedzieliśmy, że jeśli wywołuje ją nadzorca, to zmieniają się wszystkie trzy identyfikatory użytkownika: rzeczywisty, obowiązujący oraz zachowany. Wywołanie funkcji s e t g i d w programie l o g i n miało taki sam wpływ na wszystkie trzy identyfikatory grupy. Po zrealizowaniu wyżej opisanych kroków działa już powloką użytkownika. Jego proces macierzysty ma identyfikator procesu i n i t ( l ) i właśnie ten proces będzie powiadamiany o zakończeniu pracy powłoki logowania (otrzyma sygnał SIGCHLD). Proces i n i t powtórzy wówczas opisaną procedurę dla danego terminalu. Deskryptory plików o numerach 0, 1 i 2 w powłoce logowania są związane z urządzeniem terminalowym. Na rysunku 9.3 zobrazowaliśmy te zależności.

9. Relacje między procesami identyfikator procesu 1

się uda, wywołuje powłokę logowania użytkownika i w ten sposób dochodzimy do miejsca docelowego, pokazanego na rys. 9.3. Jest jednak jedna różnica - procesem macierzystym powłoki użytkownika jest program ttymor., a w przypadku programu g e t t y był nim proces i n i t .

init

r powłoka logowania i

i

\ getty oraz login /

deskryptory 0, 1,2

program obsługi terminalu

9.3

Logowania sieciowe

Logowania sieciowe w systemie 4.3 + BSD 1 połączenie RS-232

użytkownik przy terminalu^ Rys. 9.3

293

9.3. Logowania sieciowe

Organizacja procesów po zakończeniu przygotowań do logowania terminalowego

Powłoka logowania użytkownika odczytuje wszystkie pliki startowe ( . p r o f i l e dla shella Bourne'a i KornShella, . c s h r c i . l o g i n dla powłoki C). Pliki startowe zazwyczaj zmieniają ustawienia pewnych zmiennych środowiska oraz dodają wiele nowych zmiennych. Na przykład większość użytkowników ustawia swoją zmienną PATH czy sekwencję zachęty używaną przez bieżący terminal (TERM). Gdy powłoka obsłuży pliki startowe, wówczas na terminalu użytkownika pojawia się sekwencja zachęty powłoki i można rozpocząć wprowadzanie poleceń. gowanie terminalowe w systemie SVR4 System SVR4 stosuje dwie formy logowania terminalowego: (a) wzorowane na programie g e t t y , opisanym wcześniej dla systemu 4.3+BSD, oraz (b) logowanie za pomocą programu ttymon, będące nową możliwością w systemie SVR4. Zazwyczaj stosuje się program g e t t y dla konsoli, a ttymon dla innych logowań terminalowych. Program ttymon jest fragmentem obszernego programu pomocniczego o nazwie SAF (Sernice Access Facility). Dla naszych potrzeb możemy uznać, że wszystko przebiega bardzo podobnie do tego, co pokazuje rys. 9.3, ale między programem i n i t oraz powłoką logowania pojawia się dodatkowy zestaw kroków. Proces i n i t jest procesem macierzystym procesu o nazwie sac (program sprawdzający prawa dostępu do usługi, seryice access controller), który z kolei w chwili wejścia systemu w tryb wieloużytkowy wywołuje funkcje f ork oraz exec dla programu ttymon. Program ttymon monitoruje pracę wszystkich portów terminalowych wymienionych w pliku konfiguracyjnym tego programu i wywołuje funkcję fork, gdy użytkownik podaje swoją nazwę. Proces potomny ttymon za pomocą funkcji exec wywołuje program l o g i n , a ten z kolei pyta użytkownika o hasło. Potem, gdy wszystko

W przypadku logowania terminalowego, opisanego w poprzednim podrozdziale, proces i n i t wie, które urządzenia terminalowe dopuszczają logowanie, i tworzy dla każdego z nich proces o nazwie g e t t y . Logowania sieciowe są obsługiwane inaczej, wszystkie przechodzą przez procedury obsługi interfejsu sieciowego w jądrze systemu (np. procedury obsługi interfejsu typu Ethernet) i nie jesteśmy w stanie przewidzieć, ile takich logowań nastąpi. W tym przypadku nie powstaje po jednym procesie oczekującym na każde potencjalne logowanie, w zamian system sam oczekuje na połączenia sieciowe. W systemie 4.3+BSD istnieje jeden proces oczekujący na większość połączeń sieciowych, jest to proces i n e t d , nazywany nieraz procesem nadzorcy internetowego. W tym podrozdziale przyjrzymy się sekwencji procesów wywoływanych w systemie 4.3+BSD w celu załogowania sieciowego. Nie interesują nas w tym momencie szczegóły programowania sieciowego tych procesów - informacje o tym można znaleźć w mojej książce [44]. W ramach startowych procedur systemowych proces i n i t wywołuje powłokę, która wykonuje skrypt powłoki / e t c / r c . Jeden z uruchamianych przez ten skrypt demonów to proces i n e t d . Po zakończeniu skryptu powłoki procesem macierzystym i n e t d staje się i n i t . Proces i n e t d czeka na żądania połączeń TCP/IP nadchodzące do tej stacji i gdy pojawią się, wykonuje najpierw funkcję fork, a potem funkcję exec, by wykonać właściwy program. Załóżmy, że nadchodzi żądanie połączenia TCP do serwera usługi TELNET. TELNET to aplikacja umożliwiająca zdalne logowanie, używa- ; jąca protokołu TCP. Użytkownik na innej stacji (połączony ze stacją ser- i wera za pomocą dowolnej postaci sieci komputerowej) lub na tej samej sta- i cji inicjuje logowanie, uruchamiając program klienta usługi TELNET: \ telnet

hostname

,

Klient otwiera połączenie TCP ze stacją o nazwie hostname, a program wcześniej , uruchomiony na tej stacji jest nazywany serwerem usługi TELNET. Następnie | klient i serwer wymieniają dane przez połączenie sieciowe TCP, używając proto-' kołu TELNET. Użytkownik, który uruchomił program kliencki, jest zalogowany^ na stacji serwera. (Oczywiście zakładamy, że użytkownik ma ważne konto na; stacji serwera). Na rysunku 9.4 pokazujemy sekwencję procesów, których działanie wiąże się z serwerem usługi TELNET, nazywanym t e l n e t d . \

H

9. Relacje między procesami

9.4. Grupy procesów

identyfikator procesu 1

init

żądanie połączenia TCP od klienta TELNET

inetd

i grupy oraz początkowe środowisko. Na koniec program l o g i n zostaje zastąpiony, dzięki wywołaniu exec, powłoką logowania. Na rysunku 9.5 prezentujemy organizację procesów, gdy dochodzimy do tej fazy pracy. Oczywiście między procedurą obsługi urządzenia a rzeczywistym użytkownikiem pracującym przy terminalu dzieje się wiele dodatkowych rzeczy. Dopiero w rozdz. 19, gdy będziemy szczegółowo opisywać pseudoterminale, pokażemy wszystkie procesy uwikłane w opisywany ciąg wydarzeń. Warto zapamiętać, że niezależnie czy logujemy się ze zwykłego terminalu (rys. 9.3), czy przez sieć (rys. 9.5), w powłoce logowania strumienie standardowego wejścia, standardowego wyjścia oraz standardowego strumienia komunikatów są związane z urządzeniem terminalu lub pseudoterminalu. W kolejnych rozdziałach zobaczymy, że powłoka logowania jest punktem startowym sesji POSIX.l, a terminal lub pseudoterminal jest terminalem sterującym w tej sesji.

-s f ork i exec programu /bin/sh w celu I wykonania skryptu powłoki / e t c / r c , J gdy system wchodzi w tryb pracy wieloużytkowej g d yo d

f ork 1 Miente TELNET J nadchodzi żądanie połączenia inetd exec

telnetd Rys. 9.4 Sekwencja procesów związanych z wykonaniem serwera TELNET

Proces t e l n e t d otwiera urządzenie pseudoterminalowe i, wywołując funkcję fork, rozdziela się na dwa procesy. (W rozdziale 19 omówimy szczegółowo pseudoterminale). Proces macierzysty obsługuje komunikację przez sieć komputerową, a proces potomny za pomocą funkcji exec wykonuje program l o g i n . Procesy macierzysty i potomny są połączone przez pseudoterminal. Przed wykonaniem funkcji exec proces potomny wiąże z pseudoterminalem deskryptory 0, 1 i 2. Jeśli próba załogowania powiedzie się, to program l o g i n realizuje takie same kroki jak w naszym opisie w podrozdz. 9.2 - ustala katalog domowy, ustawia identyfikatory użytkownika identyfikator procesu 1

init 1 wywołania: i n e t d , J t e l n e t d oraz login powłoka logowania deskryptory ,0,1,2 program obsługi pseudoterminalu połączenie sieciowe za pomocą serwera t e l n e t d i klienta t e l n e t

Rys. 9.5 Organizacja procesów po zakończeniu przygotowań do logowania sieciowego

295

Logowanie sieciowe w systemie SVR4 Scenariusz logowania sieciowego w systemie SVR4 składa się z niemal takich samych kroków jak w systemie 4.3+BSD. Serwer i n e t d obsługuje pracę sieciową, ale jego procesem macierzystym nie jest i n i t , gdyż w systemie SVR4 program i n e t d jest wywoływany jako usługa przez podsystem sac kontrolujący dostęp do usług. Mimo to końcową sytuację obrazuje ten sam rysunek (rys. 9.5).

9.4

Grupy procesów

Każdy proces ma unikatowy identyfikator procesu, a oprócz tego należy do jakiejś grupy procesów. Z zagadnieniem grup procesów spotkamy się ponownie w rozdz. 10, podczas omawiania sygnałów. Grupa procesów jest zbiorem złożonym z co najmniej jednego procesu. Każda grupa procesów ma swój unikatowy identyfikator grupy. Identyfikatory grup procesów są bardzo podobne do identyfikatorów procesów - są te | dodatnie liczby całkowite, przechowywane w typie danych p i d t . Funkcja g e t p g r p przekazuje identyfikator grupy procesów, do której należy wywra-, łujący ją proces. #include #include pid_t getpgrp(void); Przekazuje: identyfikator grupy procesów wywołującego procesu W wielu systemach berkelejowskich, łącznie z 4.3+BSD, funkcja ta wymaga podania argumentu pid i przekazuje grupę procesów tego procesu. Powyższy prototyp pokazuje posiksową wersję tej funkcji.

9. Relacje między procesami

Każda grupa procesów może mieć lidera. Lidera wyróżnia identyfikator grupy procesów, ponieważ jest on równy identyfikatorowi jego procesu. Lider grupy procesów może utworzyć grupę procesów oraz wygenerować procesy w tej grupie, a następnie zakończyć pracę. Grupa procesów istnieje, dopóki jest w niej co najmniej jeden proces, i nie ma znaczenia czy proces lidera skończył się, czy nie. Okres czasu, który rozpoczyna się od utworzenia grupy i kończy, gdy ostatni proces opuszcza tę grupę, jest nazywany czasem życia grupy procesów. Ostatni proces w grupie procesów może albo typowo zakończyć pracę, albo przejść do innej grupy procesów. Proces dołącza do istniejącej grupy procesów lub tworzy nową grupę procesów, wywołując funkcję s e t p g i d . (W kolejnym podrozdziale zobaczymy, że również funkcja s e t s i d tworzy nową grupę procesów). tinclude #include int setpgid (pid_t pid, pid_t pgid) ;

9.5. Sesje

9.5

29:

Sesje

Sesję tworzy co najmniej jedna grupa procesów. Możemy na przykład mieć układ pokazany na rys. 9.6. W zobrazowanej sytuacji w jednej sesji są tro grupy. Procesy wchodzące w skład jednej grupy procesów są typowo łączom w jedną grupę przez kanał komunikacyjny powłoki. Na przykład ukłac z rys. 9.6 mógłby zostać wygenerowany przez polecenia powłoki o postaci: procl | proc2 S proc3 | proc4 | proc5

Proces ustanawia nową sesję, wywołując funkcję s e t s i d .

1 1

' 1

powłoka logowania

i

grupa procesów

procl

proc2

proc4

grupa procesów

Przekazuje: 0, jeśli wszystko w porządku; - 1 , jeśli wystąpił błąd

W ten sposób grupa procesów otrzymuje identyfikator równy identyfikatorowi procesu pid. Jeśli dwa argumenty są takie same, to proces wskazywany przez argument pid staje się liderem grupy procesów. Proces może ustawić swój identyfikator grupy procesów lub identyfikatory swoich potomków. Oprócz tego może zmienić identyfikator grupy procesów swojego potomka po wywołaniu przez niego funkcji exec. Jeśli argument pid jest równy 0, to jest używany identyfikator procesu wywołującego funkcję. Gdy argument pgid jest zerowy, to jako identyfikator grupy procesów jest używany identyfikator procesu wskazany przezpid. Jeśli system nie realizuje sterowania zadaniami (na ten temat będziemy mówić w podrozdz. 9.8), czyli nie jest zdefiniowana stała _POSIX_JOB_CONTROL, to funkcja przekazuje błąd i ustala wartość errno równą ENOSYS. W większości powłok mających wbudowane sterowanie zadaniami ta funkcja jest wywoływana po funkcji fork, aby proces macierzysty ustalił identyfikator grupy procesu potomnego i by potomek ustalił swój identyfikator grupy procesów. Jedno z tych wywołań jest zbędne, ale wykonując oba gwarantujemy, że potomek staje się członkiem własnej grupy procesów, zanim inny proces przyjmie, że tak jest. Jeśli nie zrobilibyśmy tak, to groziłaby nam sytuacja wyścigu, ponieważ wszystko zależałoby od kolejności, w której byłyby wykonywane procesy. Gdy będziemy opisywać sygnały, zobaczymy, jak można wysłać sygnał do pojedynczego procesu (wskazanego identyfikatorem procesu) lub do grupy procesów (wskazanych identyfikatorem grupy procesów). Podobnie jest z funkcją w a i t p i d opisaną w podrozdz. 8.6, która umożliwia oczekiwanie na pojedynczy proces lub jeden z procesów z określonej grupy.

proc3

proc5 grupa procesów sesja Rys. 9.6 Organizacja procesów w grupy procesów i sesje

#include #include pid_t setsid(void); Przekazuje: identyfikator grupy procesów, jeśli wszystko w porządku; - 1 , jeśli wystąpił błąd

Jeśli proces wywołujący nie jest liderem grupy procesów, to funkcja tworzy nową sesję. Zachodzą trzy zdarzenia: !

1. Proces staje się liderem tej nowej sesji. (Lider sesji to proces, któryś utworzył tę sesję). Bieżący proces jest jedynym procesem tej sesji. 2. Proces staje się liderem nowej grupy procesów. Nowy identyfikator i grupy procesów jest identyfikatorem procesu wywołującego tę funkcję. ' 3. Proces nie ma terminalu sterującego. (Na temat terminali sterujących^ mówimy w następnym podrozdziale). Jeśli proces miał terminal ste-1 rujący przed wywołaniem funkcji s e t s i d , to takie powiązanie jest usuwane. * Ta funkcja przekazuje błąd, jeśli wywołujący ją jest już liderem grupy procesów. Aby zagwarantować, że nie grozi taka sytuacja, często stosowaną prak-

298

9. Relacje między procesami tyką jest wywołanie funkcji fork, zakończenie procesu macierzystego i kontynuowanie procesu potomnego. Mamy wówczas pewność, że potomek nie jest liderem grupy procesów, gdyż identyfikator grupy procesów jest dziedziczony przez proces potomny, a proces ten otrzymuje nowy identyfikator. Dlatego nie jest możliwe, by proces potomny miał identyfikator równy odziedziczonemu identyfikatorowi grupy procesów. Norma Posix.1 mówi tylko o ..liderze sesji". Nie istnieje „identyfikator sesji" na wzór identyfikatora procesu lub identyfikatora grupy procesów. Oczywiście liderem sesji jest pojedynczy proces o unikatowym identyfikatorze sesji, więc możemy traktować identyfikator procesu lidera sesji jako identyfikator sesji. Tak właśnie jest w systemie SVR4 i podręczniki systemowe SVID i SVR4 w opisie funkcji s e t s i d ( 2 ) w ten sposób definiują identyfikator sesji. Jest to szczegół implementacyjny, nie będący częścią normy POSIX.l i nie stosowany przez system 4.3+BSD.

2
gdy inne różnice między powłokami nie mają większego znaczenia w opisy- i wanym zagadnieniu. Jeśli uruchamiamy zadanie w tle, to powłoka przypisuje mu identyfikator , i drukuje jeden lub kilka identyfikatorów procesu. Poniższy skrypt pokazuje, jak obsługuje to KornShell.

9. Relacje między procesami $ make a l l > Make.out & [1] 1475 $ p r *.c | lpr & [2] 1490 naciskamy klawisz RETURN $ pr * . c I l p r s [2] + Done make all > Make.out & [1] + Done

Program make jest zadaniem o numerze 1, a proces, który wystartował, ma identyfikator 1475. Kolejne zadanie będące potokiem poleceń otrzymało numer 2, a identyfikator pierwszego procesu jest równy 1490. Gdy po zakończeniu zadania naciśniemy klawisz RETURN, powłoka przekazuje nam informację o zakończeniu zadań. Musimy nacisnąć klawisz RETURN, ponieważ dopiero wówczas powłoka drukuje swój znak zachęty. Powłoka nie wypisuje informacji o zmianie stanu swoich zadań pracujących w tle w dowolnym czasie, lecz tuż przed wyprowadzeniem znaku zachęty, zezwalającego na podawanie kolejnych poleceń. W każdym innym rozwiązaniu powłoka mogłaby wyprowadzać dane, kiedy wprowadzamy wiersz danych wejściowych. Istnieje jeden specjalny znak, którego wprowadzenie ma wpływ na zadanie pracujące w tle - chodzi o klawisz zawieszenia zadania (zazwyczaj sekwencja Control-Z). Z tego powodu właśnie pojawia się zagadnienie współpracy między procedurą obsługi terminalu a powłoką. Wprowadzenie tego znaku powoduje, że procedura obsługi terminalu wysyła sygnał SIGTSTP do wszystkich procesów należących do pierwszoplanowej grupy procesów. Nie ma on natomiast wpływu na zadania we wszystkich drugoplanowych grupach procesów. Program obsługi terminalu sprawdza, czy nie pojawia się jeden z trzech znaków specjalnych, które generują sygnały przekazywane do pierwszoplanowej grupy procesów: • znak przerwania (typowo DELETE lub Control-C) generuje sygnał SIGINT • znak zakończenia (typowo Control-Y) generuje sygnał SIGQUIT • znak zawieszenia (typowo Control-Z) generuje sygnał S I G T S T P W rozdziale 11 zobaczymy, jak można zmienić te trzy znaki na inne wybrane przez siebie oraz jak zakazać procedurze obsługi terminalu przetwarzania tych specjalnych znaków. Jest jeszcze inna sytuacja związana ze sterowaniem zadaniami, na którą musi być przygotowana procedura obsługi terminalu. Jeżeli mamy jedno zadanie pierwszoplanowe i jedno lub więcej zadań drugoplanowych, to skąd wiadomo, które z nich otrzymuje znaki przekazywane za pomocą terminalowego strumienia wejściowego? Nie jest błędem, gdy zadanie w tle próbuje czytać dane z terminalu; tę sytuację wykrywa procedura obsługi terminalu i wysyła do zadania specjalny sygnał, SIGTTIN. Typowo, pojawienie się takiego sygnału zatrzymuje zadanie w tle, o czym powiadamia nas powłoka,

9.8. Sterowanie zadaniami

303

więc możemy wówczas przesunąć zadanie do pierwszego planu, by mogło odczytać dane z terminalu. Poniżej pokazujemy taką sytuację. $ c a t > temp.foo &

start zadania w tle, ale będzie ono czytało dane ze standardowego wejścia

[1] 1681 $ naciskamy klawisz RETURN [1] + S t o p p e d ( t t y i n p u t ) cat > temp.foo & $ fg %1 przenosimy zadanie o numerze I do pierwszego planu cat > temp.foo hello, world wprowadzamy jeden wiersz danych A D wpisujemy znak końca pliku $ cat temp. f oo sprawdzamy, że w pliku jest wprowadzony wiersz hello, world

Powłoka uruchamia w tle proces cat, lecz gdy cat próbuje przeczytać dane ze swojego standardowego wejścia (terminal sterujący), to procedura obsługi terminalu, wiedząc, że współpracuje z zadaniem drugoplanowym, wysyła do niego sygnał SIGTTIN. Powłoka wykrywa zmianę stanu swojego potomka (przypominamy naszą dyskusję w podrozdz. 8.6 na temat funkcji wait oraz waitpid) i przekazuje nam informację o zatrzymaniu zadania. Następnie, używając polecenia fg, przenosimy zatrzymane zadanie do pierwszego planu. (Czytelników zainteresowanych poleceniami związanymi ze sterowaniem zadaniami w konkretnej powłoce odsyłamy do odpowiedniego opisu w podręczniku systemowym). W efekcie tych działań powłoka umieszcza zadanie w pierwszoplanowej grupie procesów (funkcja t c s e t p g r p ) i wysyła do tej grupy procesów sygnał kontynuacji (SIGCONT). Zadanie należy już do pierwszoplanowej grupy procesów, a więc może odczytywać dane z terminalu sterującego. Co się stanie, gdy zadanie pracujące w tle wypisze dane na terminal sterujący? Na efekt ma wpływ opcja, którą możemy uaktywnić bądź wyłączyć. Na ogół, aby zmienić stan tej opcji używamy polecenia s t t y ( l ) . (W rozdziale 11 zobaczymy, jak zmienić ją w przygotowywanym programie). Poniżej pokazujemy, jak przebiega wykonanie. $ cat temp.foo & L1] 1719 $ hello, world [1]

+

Done

$ stty tostop

wykonujemy zadanie w tle

po znaku zachęty widzimy dane wyprowadzone przez zadanie pracujące w tle, naciskamy klawisz RETURN cat temp.foo s wyłączamy możliwość wyprowadzania danych na terminal sterujący przez zadanie pracujące w tle wykonujemy zadanie w tle

$ c a t temp. f oo & [1] 1721 $ naciskamy klawisz RETURN i dowiadujemy się, że zadanie jest zatrzymane [1] + S t o p p e d ( t t y o u t p u t ) c a t temp.foo & $fg%1 wznawiamy w pierwszym planie zatrzymane zadanie cat temp.foo powłoka przekazuje nam informację, które zadanie pracuje teraz w pierwszym planie hello, world a oto jego wydruk

9. Relacje między procesami i n i t lub inetd

I

nałowi SIGTTOU oznacza, że ukazanie się na terminalu danych wyprowadzanych przez proces pracujący w drugoplanowej grupie procesów jest opcjonalne. Czy sterowanie zadaniami jest niezbędne, czy tylko przydatne? Jest to temat budzący wiele kontrowersji wśród użytkowników. Sterowanie zadaniami zaprojektowano, zanim stała się popularna technika pracy za pomocą terminali okienkowych. Niektórzy twierdzą, że sterowanie zadaniami jest zbędne, jeśli mamy dobrze zaprojektowany system okienkowy. Inni z kolei narzekają, że implementacja sterowania zadaniami, która wymaga wsparcia w jądrze systemu, procedurze obsługi terminalu oraz konkretnych aplikacjach jest zawiła i nienaturalna. Są też użytkownicy korzystający zarówno ze sterowania zadaniami, jak i z systemu okienkowego, którzy twierdzą, że oba komponenty są potrzebne. Niezależnie od tych opinii, sterowanie zadaniami jest częścią standardów POSIX.l orazFIPS 151-1 i obecnie jest stosowane.



getty lub telnetd exec po wywołaniu setsid, następnie ustanowienie terminalu sterującego ] login

powłoka logowania

grupa (grupy) procesów drugoplanowych

grupa procesów pierwszoplanowych

305

9.9. Wykonywanie programów z powłoki

9.9

Wykonywanie programów z powłoki Przyjrzymy się teraz dokładnie, jak powłoki wykonują programy i jakie jest powiązanie tych działań z koncepcjami grup procesów, terminali sterujących oraz sesji. Do naszej analizy skorzystamy ponownie z polecenia ps. Najpierw użyjemy powłoki nie pozwalającej na sterowanie zadaniami klasycznej powłoki Bourne'a. Po wykonaniu polecenia ps -xj

otrzymujemy poniższy wynik:

program obsługi terminalu

PPID 1 163

użytkownik przy terminalu, --—_——^ sesja Rys. 9.8 Podsumowanie możliwości sterowania zadaniami pierwszoplanowymi i drugoplanowymi w odniesieniu do programu obsługi terminalu

Na rysunku 9.8 podsumowaliśmy pewne, wcześniej opisane możliwości sterowania zadaniami. Ciągłe linie, przechodzące przez prostokąt reprezentujący procedurę obsługi terminalu, oznaczają, że wejście-wyjście terminalu oraz wygenerowane przez terminal sygnały znajdują się zawsze między pierwszoplanową grupą procesów a rzeczywistym terminalem. Przerywana linia odpowiadająca syg-

PID 163 163

PGID 163 163

SID TPGID 163 163 163 163

COMMAND -Sh ps

(Usunęliśmy w wydruku kolumny, które nas nie interesują, np. nazwę terminalu, identyfikator użytkownika, czas procesora). Powłoka oraz polecenie ps pracują w tej samej sesji oraz tej samej pierwszoplanowej grupie procesów (163). Ponieważ liczba 163 jest identyfikatorem grupy procesów, wskazanym, w kolumnie TPGID, więc możemy stwierdzić, że jest to pierwszoplanowa grupa procesów. Zgodnie z naszymi oczekiwaniami, procesem macierzystym polecenia ps jest powłoka. Zwróćmy uwagę, że w wywołaniu powłoki logowania przez program l o g i n pierwszym znakiem sekwencji wywołania jest łącznik.

; ' , j

Niestety, postać wydruków produkowanych przez polecenie ps zależy od wersji sys- *' temu Unix. W systemie SVR4 podobne pola uzyskamy, gdy wykonamy polecenie ps - j i , a nowsze wersje tego systemu nigdy nie wyprowadzają pola TPGID. W systemie 4.3+BSD taki sam wydruk otrzymamy jako wynik polecenia ps -xj -otpgid.

9. Relacje między procesami Zauważmy, że wiązanie procesu z identyfikatorem terminalowej grupy procesów (kolumna TPGID) jest błędem w nazewnictwie. Proces nie ma terminalowej grupy procesów. Proces należy do jednej z grup procesów oraz do sesji. Sesja może mieć swój terminal sterujący, ale nie musi. Jeśli ma, to urządzenie terminalowe zna identyfikator grupy procesów tego procesu, który pracuje w pierwszym planie. W procedurze obsługi terminalu wartość tego identyfikatora można ustalić za pomocą funkcji t c s e t p g r p , jak pokazuje rys. 9.8. Identyfikator pierwszoplanowej grupy procesów jest atrybutem terminalu, a nie procesu. Polecenie ps pokazuje w kolumnie TPGID jego wartość, pobieraną z procedury obsługi urządzenia terminalowego. Jeśli program ps stwierdzi, że sesja nie ma terminalu sterującego, to wypisuje w tym polu wartość - 1 . Wykonanie w tle poniższego polecenia: p s -xj &

zmienia w wydruku tylko jedną wartość - identyfikator procesu związanego z wykonaniem polecenia. PPID 1 163

PID 163 169

PGID 163 163

SID TPGID 163 163 163 163

COMMAND -sh ps

Ponieważ używana powłoka nie potrafi sterować zadaniami, więc zadanie pracujące w tle nie jest umieszczane we własnej grupie procesów i nie jest mu odbierany terminal sterujący. Sprawdzimy, jak powłoka Bourne'a obsługuje potoki. Jeśli wykonamy

9.9. Wykonywanie programów z powłoki

307

Jeśli uruchomimy potok poleceń w tle ps

-xj

catl &

to zmieniają się wyłącznie identyfikatory procesów. Ponieważ nasza powłoka nie obsługuje sterowania zadaniami, identyfikatorem grupy procesów związanej z procesami pracującymi w tle pozostaje 163 i taki sam jest identyfikator terminalowej grupy procesów. Co stanie się, gdy w tej sytuacji proces pracujący w tle próbuje czytać ze swojego terminalu sterującego? Wykonajmy polecenie cat > temp.foo &

Gdyby sterowanie zadaniami było aktywne, to zadanie w tle należałoby do drugoplanowej grupy procesów, a wygenerowany sygnał SIGTTIN powiadomiłby, że zadanie w tle próbuje czytać z terminalu sterującego. Jeśli nie ma możliwości sterowania zadaniami, powłoka automatycznie przekierowuje standardowe wejście procesu działającego w tle do urządzenia / d e v / n u l l , jeśli nie zrobi tego sam proces. Odczyt z urządzenia / d e v / n u l l przekazuje znacznik końca pliku. Oznacza to, że proces c a t pracujący w tle od razu przeczyta znacznik końca pliku i zakończy się. Poprzednio omówiliśmy przypadek, gdy proces pracujący w tle próbuje uzyskać dostęp do terminalu sterującego za pomocą standardowego strumienia wejściowego. Co stanie się, jeśli proces działający w tle jawnie otworzy urządzenie / d e v / t t y i będzie czytał z terminalu sterującego? Musimy odpowiedzieć „to zależy", chociaż oczywiście lepiej byłoby, gdyby sprawa była jasna. Na przykład polecenie crypt < salaries I lpr &

ps -x;I | catl

to uzyskamy wydruk PPID 1 163 200

PID 163 200 201

PGID 163 163 163

SID TPGID 163 163 163 163 163 163

COMMAND -sh catl ps

(Program c a t l jest kopią programu c a t , ma tylko inną nazwę. Mamy również inną kopię tego programu o nazwie c a t 2 , z której będziemy korzystać później w tym podrozdziale. Jeśli dysponujemy dwiema kopiami programu c a t i łączymy je w potok, to różne nazwy programów umożliwiają odróżnienie tych dwóch programów). Widzimy, że ostatni proces w potoku jest procesem potomnym powłoki, a pierwszy proces potoku jest potomkiem ostatniego procesu. Okazuje się, że powłoka tworzy za pomocą wywołania f ork swoją kopię, a następnie ta kopia wywołuje kolejny raz funkcję f ork, aby wykonać każdy z procesów w potoku.

jest przykładem takiego potoku. Uruchamiamy je w tle, ale program crypt otwiera urządzenie /dev/tty, zmienia charakterystykę terminalu (aby wyłączyć echo), czyta z urządzenia i na koniec odtwarza oryginalną charakterystykę terminalu. Kiedy wykonujemy taki potok poleceń w tle, na terminalu pojawia się wyprowadzana przez program crypt sekwencja zachęty Password:, ale dane wprowadzane przez nas (hasło szyfrowania) są czytane przez powłokę, która następnie próbuje wykonać polecenie o podanej nazwie,. Następny wprowadzony do powłoki wiersz jest traktowany jako hasło szyfrowania. W efekcie plik nie zostanie poprawnie zaszyfrowany, a do drukarki ; będzie przesłany nieokreślony zestaw danych. Mamy dwa procesy, które pró- ' bująjednocześnie czytać z tego samego urządzenia, i efekt zależy od systemu. , Jak opisaliśmy wcześniej, sterowanie zadaniami obsługuje poprawnie współ- j ne używanie pojedynczego terminalu przez wiele procesów. Powróćmy do naszego przykładu w powłoce Bourne'a. Jeśli wykonamy * w potoku trzy polecenia ps -xj

I c a t l I cat2

9. Relacje między procesami

to możemy sprawdzić, jak dana powłoka obsługuje kontrolę procesów. PPID 1 163 202 202

PID 163 202 203 204

PGID 163 163 163 163

SID TPGID 163 163 163 163 163 163 163 163

COMMAND -sh cat2 ps catl

potok

Xx

1

cat2 (202) Rys. 9.9 Procesy w potoku " p s -xj

|

sh (204)

exec

T

catl (204)

"" " "potok catl

I

c a t 2 " wywołane z powłoki Bourne'a

Ponieważ ostatni proces w potoku jest procesem potomnym powłoki, więc gdy ten proces (cat2) kończy się, powłoka jest o tym powiadamiana. Przeanalizujemy teraz te same przykłady dla powłoki sterującej zadaniami. Zobaczymy, w jaki sposób takie powłoki obsługują zadania w tle. Skorzystamy z KornShella, ale rezultaty dla powłoki C byłyby niemal identyczne. Polecenie ps

-xj

daje poniższy wynik PPID 1 700

PID 700 708

PGID 700 708

zostaje pierwszoplanową grupą procesów, jeśli ma terminal sterujący. Powłoka logowania w czasie wykonywania polecenia ps należy do drugoplanowej grupy procesów. Widzimy jednak, że obie grupy procesów, 700 oraz 708, są członkami tej samej sesji. Wkrótce zobaczymy, że sesja nie zmienia się w kolejnych przykładach prezentowanych w tym podrozdziale. Wykonanie tego procesu w tle ps -xj &

Ponownie, ostatni proces w potoku jest potomkiem powłoki, a wszystkie poprzednie procesy w potoku są potomkami ostatniego procesu. Przebieg wydarzeń widzimy na rys. 9.9.

exec

daje wynik PPID 1 700

COMMAND -ksh ps

(Począwszy od tego przykładu pokazujemy pierwszoplanową grupę procesów za pomocą c z c i o n k i pogrubionej). Od razu widzimy różnicę w stosunku do przykładu z powłoką Bourne'a. KornShell umieszcza zadanie pierwszoplanowe (ps) w swojej grupie procesów (708). Polecenie ps jest liderem grupy procesów oraz jedynym procesem w tej grupie. Ta grupa procesów po-

PID 700 709

PGID 700 709

SID TPGID 700 700 700 700

COMMAND -ksh ps

Również tutaj polecenie ps tworzy własną grupę procesów, ale tym razem jej identyfikator (709) nie jest numerem pierwszoplanowej grupy procesów. Grupa ta jest drugoplanowa. W kolumnie TPGID widzimy liczbę 700 wskazującą, że pierwszoplanową grupą procesów jest nasza powłoka logowania. Wykonując w potoku dwa procesy catl

ps -

otrzymujemy PPID 1 700 700

PID 700 710 711

PGID 700 710 710

SID TPGID 700 710 700 710 700 710

COMMAND -ksh ps catl

Oba procesy, ps oraz c a t l , są umieszczane w nowej grupie procesów (710), która staje się pierwszoplanową grupą procesów. Możemy tutaj zaobserwować kolejną różnicę między tym przykładem a sytuacją przy użyciu powłoki Bourne'a. Powłoka Bourne'a tworzyła najpierw ostatni proces w potoku i ostatni proces stawał się procesem macierzystym pierwszego procesu. Teraz KornShell jest procesem macierzystym obu procesów. Jeśli jednak wykonamy ten potok poleceń w tle ps -

SID TPGID 700 708 700 708

309

9.9. Wykonywanie programów z powłoki

catl &

to wynik świadczy o tym, że KornShell generuje procesy tak samo jak powłoka Bourne'a. PPID 1 700 712

PID 700 712 713

PGID 700 712 712

SID TPGID 700 710 700 700 700 700

COMMAND -ksh catl ps

Procesy o identyfikatorach 712 i 713 zostały umieszczone w drugoplanowej grupie procesów o numerze 712.

10

9. Relacje między procesami

,10 Osierocone grupy procesów Powiedzieliśmy wcześniej, że proces, którego proces macierzysty zakończył się, staje się sierotą i jest dziedziczony przez proces i n i t . Opiszemy teraz, w jaki sposób norma POSDC.l obsługuje sytuację, w której zostają osierocone całe grupy procesów.

rzykład Rozważmy proces, który, wywołując funkcję fork, tworzy swojego potomka, a następnie kończy pracę. Nie jest to sytuacja awaryjna (dzieje się tak bardzo często), ale zastanówmy się, co się stanie, jeśli proces potomny zostanie zatrzymany (za pomocą techniki sterowania zadaniami), gdy proces macierzysty kończy swoje działanie. Czy proces potomny kiedykolwiek wznowi pracę, a jeśli, to w jaki sposób, i czy dowie się, że został osierocony? Konkretny przykład widzimy w próg. 9.1. Wprowadzamy w nim kilka nowych właściwości, które omówimy poniżej. Na rysunku 9.10 pokazujemy stan po wystartowaniu próg. 9.1 i wywołaniu w nim funkcji f ork w celu utworzenia potomka. include include include include include

0) { sleep(5); exit (0);

/* proces macierzysty */ /* usypia, by potomek mógł sam wstrzymać pracę */ /* potem proces macierzysty kończy pracę */

/* proces potomny */ else { pr_ids("child"); signal(SIGHOP, sig_hup); /* rejestruje procedurę obsługi sygnału * k i l l ( g e t p i d O , SIGTSTP); /* zatrzymuje pracę */ pr_ids("child"); /* ten wydruk pojawi się tylko, gdy uda się wznowić pracę */ if (read(0, &c, 1) != 1) printf("read error from control terminal, errno = %d\n", errno); exit(0);

Rys. 9.10 Przykład grupy procesów, która za chwilę zostanie osierocona

W naszej analizie zakładamy, że powłoka realizuje sterowanie zada-' niami. Przypominamy z poprzedniego podrozdziału, że powłoka umieszcza' proces pierwszoplanowy we własnej grupie procesów (w naszym przykładzie 512), a sama powłoka pozostaje w swojej grupie procesów (442). Pcj wywołaniu funkcji fork potomek dziedziczy grupę procesów swojego procesu macierzystego (512). 1 Proces macierzysty wchodzi w stan uśpienia na 5 sekund. W ten spo sób pozwalamy, by potomek wykonał się przed zakończeniem pracy procesu macierzystego (nie jest to rozwiązanie doskonałe). Potomek ustanawia procedurę obsługi sygnału zawieszenia (SIGHUP), Dzięki temu zobaczymy, że proces potomny otrzymał sygnał SIGHUP. (O procedurach obsługi sygnałów będziemy mówić w rozdz. 10).

9. Relacje między procesami • Potomek wysyła do siebie za pomocą funkcji k i l l sygnał zatrzymania (SIGSTOP). Proces potomny zostaje zatrzymany, tak samo jak w powłoce zatrzymywaliśmy zadanie pracujące w pierwszym planie, wprowadzając specjalny znak zatrzymania terminalu (Control-Z). • Gdy proces macierzysty kończy pracę, potomek staje się sierotą i od tego czasu jego procesem macierzystym jest proces i n i t o identyfikatorze 1. • Od tej chwili proces potomny jest członkiem osieroconej grupy procesów. Według normy POSIX.l osierocona grupa procesów to taka grupa, w której proces macierzysty każdego członka jest albo sam członkiem tej grupy, albo nie jest członkiem sesji tej grupy. Mówiąc inaczej, grupa procesów nie jest osierocona, dopóki jest w niej przynajmniej jeden proces, którego proces macierzysty jest w innej grupie procesów należącej do tej samej sesji. Jeśli grupa procesów nie jest osierocona, to jeden z procesów macierzystych należących do innej grupy procesów tej samej sesji może wznowić pracę zatrzymanego procesu w takiej grupie. W naszym przypadku proces macierzysty każdego procesu w grupie (np. proces o identyfikatorze 1 jest procesem macierzystym procesu 513) należy do innej sesji. • Grupa procesów zostaje osierocona, gdy proces macierzysty zakończy pracę. Norma POSIX.l wymaga, by każdy zatrzymany proces, należący do nowo osieroconej grupy procesów (np. nasz proces potomny), otrzymał sygnał zawieszenia (SIGHUP), a następnie sygnał kontynuacji (SIGCONT). • W ten sposób proces potomny kontynuuje pracę po przetworzeniu sygnału zawieszenia. Domyślną akcją, podejmowaną po odbiorze sygnału zawieszenia, jest zakończenie procesu, dlatego właśnie dodaliśmy własną procedurę obsługi sygnału i chcemy przejąć ten sygnał. Oczekujemy, że wywołanie funkcji p r i n t f w funkcji sig_hup nastąpi przed wywołaniem p r i n t f w funkcji p r _ i d s . Oto dane wyprowadzone przez próg. 9.1. $ a.out parent: pid = 512, ppid = 442, pgrp = 512 child: pid = 513, ppid = 512, pgrp = 512 $ SIGHUP received, pid = 513 child: pid = 513, ppid = 1, pgrp = 512 read error from control terminal, errno = 5

Widzimy, że znak zachęty powłoki pojawił się razem z danymi wyprowadzanymi przez proces potomny, ponieważ dwa procesy, powłoka logowania i potomek, wysyłają jednocześnie dane do terminalu. Zgodnie z oczekiwaniami identyfikator procesu macierzystego potomka przyjął wartość 1.

9.11. Implementacja w systemie 4.3+BSD

313

Zauważmy, że po wywołaniu funkcji p r _ i d s w procesie potomnym program próbuje czytać ze standardowego wejścia. Widzieliśmy wcześniej w tym rozdziale, że gdy drugoplanowa grupa procesów próbuje odczytywać dane z terminalu sterującego, jest generowany sygnał S I G T T I N kierowany do wszystkich procesów grupy drugoplanowej. W tym przypadku grupa procesów jest osierocona, więc jeśli jądro systemu zatrzymałoby ją za pomocą tego sygnału, to najprawdopodobniej procesy tej grupy nigdy nie mogłyby wznowić pracy. Norma POSDC.l określa zatem, że w tej sytuacji funkcja read musi przekazać błąd, a zmienna e r r n o przyjmuje wartość EIO (równą w tym systemie 5). Na koniec zwróćmy uwagę, że proces potomny po zakończeniu procesu macierzystego przechodzi do drugoplanowej grupy procesów, gdyż proces macierzysty był wykonywany przez powłokę jako zadanie pierwszoplanowe. • W podrozdziale 19.5 w programie p t y zobaczymy inny przykład osieroconych grup procesów.

9.11

Implementacja w systemie 4.3+BSD Powiedzieliśmy już o wielu atrybutach procesu: grupie procesów, sesji oraz terminalu sterującym. Warto teraz przyjrzeć się, jak są one implementowane. Przyjrzymy się implementacji używanej przez system 4.3+BSD. Niektóre szczegóły związane z implementacją tych właściwości w systemie SVR4 można znaleźć w pracy Williamsa, 1989 [49]. Na rysunku 9.11 pokazujemy różne struktury danych, z których korzysta system 4.3+BSD. Przeanalizujemy wszystkie pola, które są wymienione na rysunku. Zaczniemy od struktury s e s s i o n . Dla każdej sesji jest alokowana jedna taka struktura (np. przy każdym wywołaniu funkcji s e t s i d ) . • Pole s_count jest liczbą grup procesów w sesji. Gdy licznik ten przyjmie wartość 0, wówczas strukturę można zwolnić. • Pole s _ l e a d e r jest wskaźnikiem do struktury proc lidera sesji. Jak wcześniej wspomnieliśmy, system 4.3+BSD nie obsługuje pola identyfikatora sesji, natomiast SVR4 obsługuje. • Pole s_ttyvp jest wskaźnikiem do struktury ynode terminalu sterującego. • Pole s _ t t y p jest wskaźnikiem do struktury t t y terminalu sterującego. Gdy jest wywoływana funkcja s e t s i d , następuje alokacja nowej struktury s e s s i o n w jądrze systemu. Pole s_count uzyskuje wartość 1, pole s _ l e a d e r wskazuje strukturę p r o c procesu wywołującego tę funkcję, a pola s _ t t y v p oraz s _ t t y p są pustymi wskaźnikami, ponieważ nowa sesja nie ma terminalu sterującego.

9. Relacje między procesami struktura t t y

struktura s e s s i o n

struktura v n o d e

rzeczywisty i-węzeł urządzenia

9.12. Podsumowanie

315

Aby znaleźć pierwszoplanową grupę procesów konkretnej sesji, jądro systemu zaczyna poszukiwania od struktury sesji, potem na podstawie pola s t t y p pobiera strukturę t t y terminalu sterującego, a następnie, korzystając ze wskaźnika t_pgrp, pobiera strukturę pgrp pierwszoplanowej grupy procesów. Struktura pgrp zawiera informacje dotyczące konkretnej grupy procesów.

• Pole pg_id jest identyfikatorem grupy procesów. • Pole pg_session wskazuje na strukturę s e s s i o n sesji, do której ten proces należy. • Pole pg_mem jest wskaźnikiem do struktury proc pierwszego procesu będącego członkiem tej grupy procesów. Pole p_pgrpnxt w strukturze proc wskazuje następny proces w grupie itd., aż do napotkania w tym polu struktury proc pustego wskaźnika oznaczającego, że bieżąca struktura dotyczy ostatniego procesu tej grupy. Struktura p r o c zawiera wszystkie informacje o pojedynczym procesie.

• Pole p_pid zawiera identyfikator sesji. • Pole p _ p p t r jest wskaźnikiem do struktury proc procesu macierzystego. • Pole p_pgrp wskazuje na strukturę pgrp grupy procesów, do której należy ten proces. • Pole p_pgrpnxt jest wskaźnikiem do następnego procesu w grupie procesów, jak opisaliśmy wcześniej. Rys. 9.11 Implementacja sesji i grup procesów w systemie 4.3+BSD

Przejdźmy do omówienia struktury t t y . W jądrze systemu jest po jednej takiej strukturze dla każdego urządzenia terminalowego oraz pseudoterminalowego. (Na temat pseudoterminali będziemy mówić w rozdz. 19). • Pole t _ s e s s i o n wskazuje na strukturę s e s s i o n , dla której ten terminal jest terminalem sterującym. (Zwróćmy uwagę, że struktura t t y wskazuje na strukturę s e s s i o n i odwrotnie). Terminal używa tego wskaźnika do wysyłania sygnału zawieszenia do lidera sesji, jeśli terminal utraci częstotliwość nośną (rys. 9.7). • Pole t _ t e r m i o s jest strukturą zawierającą wszystkie znaki specjalne oraz inne informacje związane z terminalem (np. szybkość w bodach, informację, czy włączone jest echo itp.). Powrócimy do tej struktury w rozdz. 11. • Pole t _ w i n s i z e jest strukturą w i n s i z e zawierającą bieżący rozmiar okna terminalowego. Gdy zmienia się rozmiar okna terminalu, do pierwszoplanowej grupy procesów jest przesyłany sygnał SIGWINCH. W podrozdziale 11.2 pokażemy, w jaki sposób można ustalić oraz pobrać bieżący rozmiar okna.

Ostatnią strukturą jest vnode. Jest ona alokowana w czasie otwierania terminalu sterującego. Wszystkie odesłania do urządzenia / d e v / t t y w procesie przechodzą przez strukturę vnode. Zaznaczamy, że rzeczywisty i-węzeł jest częścią v-węzła. W podrozdziale 3.10 powiedzieliśmy, że taka jest implementacja używana przez system 4.3+BSD, podczas gdy system SVR4 przechowuje w i-węźle v-węzeł.

9.12 Podsumowanie

;

W tym rozdziale opisaliśmy zależności między grupami procesów. MówHi1 śmy o sesjach, które składają się z grup procesów. Sterowanie zadaniami jest właściwością obecnie zaimplementowaną w wielu systemach uniksowych. Pokazaliśmy, jak wygląda w powłokach realizujących sterowanie zadaniami j W prezentowane zależności między procesami jest również uwikłany terminali sterujący procesu,/dev/tty. Wielokrotnie powoływaliśmy się na sygnały, które są stosowane do. współpracy procesów. Następny rozdział jest kontynuacją tej tematyki, przeanalizujemy w nim szczegółowo wszystkie sygnały w systemie Unix.

9. Relacje między procesami

Ćwiczenia 9.1

9.2

Powróć do naszej dyskusji na temat plików utmp oraz wtmp w podrozdz. 6.7. Dlaczego rekordy związane z wylogowaniem są zapisywane w systemie 4.3+BSD przez proces i n i t ? Czy tak samo przebiega obsługa logowania sieciowego? Napisz mały program, który wywołuje funkcję fork, a następnie w procesie potomnym tworzy nową sesję. Sprawdź, że staje się liderem grupy procesów oraz że potomek przestaje być właścicielem terminalu sterującego.

10

Sygnały

10.1 Wprowadzenie Sygnały są przerwaniami programowymi. Większość bardziej zaawansowanych programów użytkowych ma do czynienia z sygnałami. Sygnały służą do obsługi asynchronicznych zdarzeń, które występują, np. gdy użytkownik uruchomi za pomocą terminalu klawisz przerwania, by zatrzymać program lub gdy kolejny program w potoku zakończy się przedwcześnie. Sygnały wprowadzono już we wczesnych wersjach systemów uniksowych, jednak model sygnałów, z którego korzystają takie systemy jak np. System V nie jest niezawodny. Sygnały mogą tam zaginąć, a wyłączenie w procesie wybranych sygnałów w czasie wykonywania instrukcji w obszarze krytycznym nie jest proste. W systemach 4.3+BSD oraz SVR3 zmieniono model sygnałów, dodano tzw. sygnały niezawodne (reliable signals). Jednak zmiany dokonane w systemach berkelejowskich oraz systemach firmy AT&T nie były w pełni kompatybilne. Na szczęście norma POSIX. 1 standaryzuje procedury dotyczące sygnałów niezawodnych i właśnie temu zagadnieniu poświęcamy bieżący rozdział. Rozdział zaczniemy od dokonania ogólnego przeglądu i opisu, do czego są używane poszczególne sygnały. Następnie przyjrzymy się problemom, które występowały w starszych implementacjach. Przeanalizowanie niedociągnięć pewnych implementacji często okazuje się bardzo cenne i pozwala nauczyć się, jak należy poprawnie obsługiwać pewne szczegóły. W bieżącym rozdziale dołączamy wiele przykładów; nie wszystkie są w pełni poprawne, będziemy omawiać ich wady.

10. Sygnały

!

Koncepcje sygnałów Po pierwsze, każdy sygnał ma nazwę. Nazwy sygnałów zaczynają się od napisu SIG. Na przykład SIGABRT jest sygnałem awarii generowanym, gdy proces wywoła funkcję a b o r t . Nazwa SIGALRM oznacza sygnał alarmu generowany, gdy licznik zegarowy ustawiony przez funkcję alarm, przekroczy termin. Wersja 7 miała 15 różnych sygnałów; systemy SVR4 oraz 4.3+BSD obsługują 31 sygnałów. Wszystkie nazwy sygnałów są zdefiniowane w pliku nagłówkowym < s i g n a l . h > za pomocą dodatnich stałych całkowitoliczbowych (numerów sygnałów). Żaden sygnał nie ma numeru 0. W podrozdziale 10.9 zobaczymy, że funkcja k i l l używa sygnału o numerze 0 w specjalnej sytuacji. Norma POSIX.l nazywa tę wartość sygnałem pustym. Sygnały mogą zostać wygenerowane w wyniku różnych sytuacji. • Sygnały generowane przez terminal pojawiają się po naciśnięciu przez użytkownika niektórych klawiszy terminalu. Na przykład naciśnięcie klawisza DELETE powoduje wysłanie sygnału przerwania (SIGINT). W ten sposób można zatrzymać program, którego działanie nie satysfakcjonuje nas. (W rozdziale 11 zobaczymy, jak można odwzorować ten sygnał na dowolny inny znak wprowadzany z terminalu). • Sygnały mogą zostać wygenerowane w sytuacjach wyjątkowych związanych z pracą sprzętu: przy próbie dzielenia przez 0, niepoprawnym odesłaniu w pamięci itp. Okoliczności takie są na ogół wykrywane sprzętowo. Jądro systemu otrzymuje wówczas informację o zdarzeniu i generuje odpowiedni sygnał kierowany do procesu związanego z tym zdarzeniem. Sygnał SIGSEGV jest na przykład wysyłany do procesu, który próbuje sięgnąć do niewłaściwego adresu pamięci. • Funkcja k i l 1(2) służy do wysyłania przez proces dowolnego sygnału albo do innego procesu, albo do grupy procesów. Istnieją oczywiście pewne ograniczenia: proces wysyłający sygnał musi być właścicielem procesu, do którego wysyła sygnał lub nadzorcą systemu. • Polecenie k i l 1(1) umożliwia wysłanie sygnału do innych procesów. Ten program jest interfejsem do funkcji k i l l . Polecenie k i l l jest często używane do zakończenia procesów pracujących w tle. • Również pewne warunki programowe mogą generować sygnały, aby powiadomić proces o ich wystąpieniu. Nie są to sytuacje spowodowane stanem urządzeń (jak np. przy próbie dzielenia przez 0), lecz okoliczności czysto programowe. Przykładami są sygnały: SIGURG (generowany, gdy na połączeniu sieciowym nadchodzą dane wysokopriorytetowe), SIGPIPE (generowany, jeśli proces zapisuje dane do łącza komunikacyjnego, a strona odczytująca dane z tego łącza zakończyła pracę) oraz SIGALRM (generowany przy przekroczeniu terminu licznika zegarowego ustawionego w procesie).

10.2. Koncepcje sygnałów

319

Sygnały są klasycznym przykładem zdarzeń asynchronicznych. Pojawiają się w procesie w dowolnym czasie, którego nie można przewidzieć. Proces nie może więc po prostu sprawdzać jakiejś zmiennej (np. errno), by dowiedzieć się, czy sygnał pojawił się, lecz musi powiadomić jądro systemu, że jeśli sygnał wystąpi, to trzeba wykonać określone czynności. Możemy ustalić, by jądro systemu w chwili pojawienia się sygnału wykonywało jedną z trzech następujących czynności. Takie ustalenie jest nazywane dyspozycją dla sygnału lub akcją związaną z sygnałem. 1. Zignorowanie sygnału. Taka możliwość dotyczy wszystkich sygnałów z wyjątkiem dwóch, które nigdy nie mogą zostać zignorowane: SIGKILL oraz SIGSTOP. Sygnałów tych nie wolno zignorować, gdyż nadzorca musi mieć sposób zabicia bądź zakończenia dowolnego procesu. Również, jeśli ignorujemy jeden z sygnałów generowanych w sprzętowych sytuacjach wyjątkowych (jak np. przy niepoprawnym odwołaniu w pamięci lub przy próbie dzielenia przez 0), to zachowanie procesu nie jest określone. 2. Przechwycenie sygnału. W tym celu ustalamy, że jądro systemu wywołuje w chwili odbioru sygnału pewną dostarczoną mu funkcję. W naszej funkcji możemy zrobić cokolwiek chcemy, aby obsłużyć bieżącą sytuację. Jeśli np. przygotowujemy program, pełniący funkcję interpretera poleceń, to naciśnięcie przez użytkownika klawisza przerwania powinno zapewne powodować zakończenie polecenia wykonywanego dla tego użytkownika i powrót do pętli głównej programu. Gdy przechwycimy sygnał SIGCHLD, wówczas wiemy, że proces potomny zakończył pracę, a więc funkcja obsługi tego sygnału może wywołać funkcję waitpid, by pobrać identyfikator procesu potomnego oraz stan zakończenia. Innym przykładem jest przygotowanie procedury obsługi sygnału SIGTERM (sygnał zakończenia będący domyślnym sygnałem wysyłanym przez polecenie k i l l ) , która usuwa pliki tymczasowe, tworzone podczas pracy programu. 3. Zgoda na wykonanie akcji domyślnej. Każdy sygnał ma swoją akcję , domyślną, pokazujemy to w tab. 10.1. Zwróćmy uwagę, że dla wiek- j szóści sygnałów domyślną akcjąjest zakończenie procesu. i W tabeli 10.1 zamieszczamy nazwy wszystkich sygnałów. Dla każdego sygnału podajemy, w jakich systemach jest on zaimplementowany oraz jaka jest jego domyślna akcja. Kolumna o nagłówku POSIX.l zawiera znak pogrubionej kropki, gdy sygnał jest wymagany, lub słowo „zadanie", jeśli sygnał jest związany ze sterowaniem zadaniami (co jest wymagane, gdy system realizuje sterowanie zadaniami). Termin „ c o r e " w kolumnie domyślnej akcji oznacza, że jest tworzony obraz pamięci procesu w pliku core w bieżącym katalogu roboczym. (Nazwa tego pliku, core, świadczy o tym, jak długo ta właściwość istnieje w systemie

i j

1

i j , ,

10. Sygnały

Tabela 10.1 (cd.)

Tabela 10.1 Sygnały w Uniksie jwa

Opis

;ABRT

niepoprawne zakończenie

ANSIC POSIX.l SVR4 4.3+BSD Domyślna akcja •

Nazwa

Opis







zakończenie, c o r e

SIGTTOU

zapis na terminal sterujący przez proces drugoplanowy







zakończenie

SIGURG





zakończenie, c o r e

zdarzenie wysokopriorytetowe

(abort) GALRM

przekroczenie czasu (alarm)

321

10.2. Koncepcje sygnałów

ANSIC POSDC.l SVR4 4.3+BSD Domyślna akcja zadanie





zatrzymanie procesu





zignorowanie

SBUS

błąd sprzętowy

GCHLD

zmiana stanu procesu potomnego

zadanie





zignorowanie

SIGUSR1

sygnał zdefiniowany przez użytkownika







zakończenie

GCONT

kontynuacja zatrzymanego procesu

zadanie





kontynuacja/zignor.

SIGUSR2

sygnał zdefiniowany przez użytkownika







zakończenie

GEMT

błąd sprzętowy





zakończenie, c o r e

SIGVTALRM





zakończenie

GFPE

błąd operacji arytmetycznej







zakończenie, c o r e

alarm czasu wirtualnego (setitimer)

GHUP

zawieszenie







zakończenie

SIGWINCH

zmiana rozmiaru okna terminalu





zignorowanie

GILL

nielegalna instrukcja







zakończenie, c o r e

SIGXCPU





zakończenie, c o r e

żądanie podania statusu wysłane z klawiatury

zignorowanie

przekroczone ograniczenie czasu pracy procesora



GINFO

przekroczone ograniczenie rozmiaru pliku ( s e t r l i m i t )





zakończenie, c o r e

• •









zakończenie

GINT

terminalowy znak przerwania

GIO

asynchroniczne wejście-wyjście





zakończenie/zignor.

GIOT

błąd sprzętowy





zakończenie, c o r e

GKILL

zakończenie







zakończenie

GPIPE

zapisanie danych do łącza, gdy nie ma odczytu







zakończenie

GPOLL

zdarzenie, o które można odpytywać ( p o l l )



GPROF

ustawienie zegara (setitimer)



GPWR

awaria zasilania / restart

:GQUIT

terminalowy znak zakończenia

:GSEGV

niepoprawne wskazanie pamięci

:GSTOP

zatrzymanie

GSYS

niepoprawna funkcja systemowa

:GTERM

zakończenie





zakończenie •

zakończenie, c o r e



zignorowanie







zakończenie, c o r e







zakończenie, c o r e

zadanie





zatrzymanie procesu





zakończenie, c o r e





zakończenie





:GTRAP

błąd sprzętowy



zakończenie, c o r e

:GTSTP

terminalowy znak zatrzymania

zadanie





zatrzymanie procesu

[GTTIN

odczyt z terminalu sterującego przez proces drugoplanowy

zadanie





zatrzymanie procesu

(setrlimit) SIGXFSZ

Unix). Z tego pliku korzysta większość programów analizujących wykonanie programu (debugger), w celu sprawdzenia stanu procesu w chwili zakończenia. Plik taki nie będzie generowany, gdy (a) proces miał ustawiony bit ustanowienia identyfikatora użytkownika, a bieżący użytkownik nie jest właścicielem pliku z programem, lub (b) proces miał ustawiony bit ustanowienia identyfikatora grupy, a bieżący użytkownik nie jest właścicielem grupowym tego pliku, lub (c) użytkownik nie ma uprawnień, by zapisać dane w bieżącym katalogu roboczym, lub (d) plik jest za duży (przypominamy ograniczenie RLIMIT_CORE, o którym mówiliśmy w podrozdz. 7.11). Plik core uzyskuje zazwyczaj prawa dostępu (zakładając, że dotychczas nie istniał) gwarantujące: odczyt przez użytkownika, zapis przez użytkownika, odczyt przez grupę oraz odczyt przez innych. Tworzenie pliku c o r e jest właściwością implementacyjną w większości systemów ,.] Unix. Zagadnienie to nie jest określone w normie POSIX. 1. Wersja 6 systemu Unix nie sprawdzała warunków (a) oraz (b), a kod źródłowy zawierał : komentarz o takiej treści: „Jeśli szukasz sposobu złamania ochrony, to wiele możliwo: ' ści uzyskasz, gdy znajdziesz polecenie z ustawionym bitem ustanowienia identyfikatora ' użytkownika". System 4.3+BSD generuje obecnie plik o nazwie core .próg, przyrostek próg powstaje ) na podstawie pierwszych 16 znaków nazwy wykonywanego programu. Jest to bardzo i przydatna właściwość, gdyż można dzięki niej zidentyfikować plik z obrazem pamięci.

Sygnały zawierające w opisie określenie „błąd sprzętowy" są powiązane ze zdefiniowanymi w konkretnych implementacjach błędami sprzętowymi.

10. Sygnały

10.2. Koncepcje sygnałów

Tabela 10.1 Sygnały w Uniksie *Iazwa

Opis

3IGABRT

niepoprawne zakończenie (abort)

3IGALRM

przekroczenie czasu (alarm)

ANSIC POSK.l SVR4 4.3+BSD Domyślna akcja •

Nazwa

Opis







zakończenie, c o r e

SIGTTOU

zapis na terminal sterujący przez proces drugoplanowy







zakończenie

SIGURG





zakończenie, c o r e

zdarzenie wysokopriorytetowe

32S Tabela 10.1 (cd.) ANSIC POSK.l SVR4 4.3+BSD Domyślna akcja zadanie





zatrzymanie procesu





zignorowanie

5IGBUS

błąd sprzętowy

3IGCHLD

zmiana stanu procesu potomnego

zadanie





zignorowanie

SIGUSR1

sygnał zdefiniowany przez użytkownika







zakończenie

3IGCONT

kontynuacja zatrzymanego procesu

zadanie





kontynuacja/zignor.

SIGUSR2

sygnał zdefiniowany przez użytkownika







zakończenie

5IGEMT

błąd sprzętowy





zakończenie, c o r e





zakończenie

5IGFPE

błąd operacji arytmetycznej







zakończenie, c o r e

SIGVTALRM alarm czasu wirtualnego (setitimer)

3IGHUP

zawieszenie







zakończenie





zignorowanie

3IGILL

nielegalna instrukcja







zakończenie, c o r e







zignorowanie

zakończenie, c o r e

3IGINFO

żądanie podania statusu wysłane z klawiatury

3IGINT

terminalowy znak przerwania





zakończenie





zakończenie, c o r e



zakończenie/zignor.

• •





3IGIO

asynchroniczne wejście-wyjście



3IGIOT

błąd sprzętowy





zakończenie, c o r e

3IGKILL

zakończenie







zakończenie

3IGPIPE

zapisanie danych do łącza, gdy nie ma odczytu







zakończenie

3IGPOLL

zdarzenie, o które można odpytywać (po 11)



3IGPROF

ustawienie zegara (setitimer)





zakończenie, c o r e

3IGPWR

awaria zasilania / restart



zignorowanie







zakończenie, c o r e







zakończenie, c o r e

zadanie





zatrzymanie procesu





zakończenie, c o r e





zakończenie

3IGQUIT

terminalowy znak zakończenia

3IGSEGV

niepoprawne wskazanie pamięci

3IGSTOP

zatrzymanie

3IGSYS

niepoprawna funkcja systemowa

3IGTERM

zakończenie

3IGTRAP

błąd sprzętowy

3IGTSTP 3IGTTIN

• terminalowy znak zatrzymania odczyt z terminalu sterującego przez proces drugoplanowy







zakończenie





zakończenie, c o r e

zadanie





zatrzymanie procesu

zadanie





zatrzymanie procesu

SIGWINCH SIGXCPU

SIGXFSZ

zmiana rozmiaru okna terminalu przekroczone ograniczenie czasu pracy procesora (setrlimit) przekroczone ograniczenie rozmiaru pliku ( s e t r l i m i t )

Unix). Z tego pliku korzysta większość programów analizujących wykonanie programu (debugger), w celu sprawdzenia stanu procesu w chwili zakończenia. Plik taki nie będzie generowany, gdy (a) proces miał ustawiony bit ustanowienia identyfikatora użytkownika, a bieżący użytkownik nie jest właścicielem pliku z programem, lub (b) proces miał ustawiony bit ustanowienia identyfikatora grupy, a bieżący użytkownik nie jest właścicielem grupowym tego pliku, lub (c) użytkownik nie ma uprawnień, by zapisać dane w bieżącym katalogu roboczym, lub (d) plik jest za duży (przypominamy ograniczenie RLIMIT_CORE, o którym mówiliśmy w podrozdz. 7.11). Plik core uzyskuje zazwyczaj prawa dostępu (zakładając, że dotychczas nie istniał) gwarantujące: odczyt przez użytkownika, zapis przez użytkownika, odczyt przez grupę oraz odczyt przez innych. ; Tworzenie pliku c o r e jest właściwością implementacyjną w większości systemów, Unix. Zagadnienie to nie jest określone w normie POSIX. 1. ' Wersja 6 systemu Unix nie sprawdzała warunków (a) oraz (b), a kod źródłowy zawierali komentarz o takiej treści: „Jeśli szukasz sposobu złamania ochrony, to wiele możliwo-j ści uzyskasz, gdy znajdziesz polecenie z ustawionym bitem ustanowienia identyfikatora-* użytkownika". -j

i

System 4.3+BSD generuje obecnie plik o nazwie core .próg, przyrostek próg powstajd na podstawie pierwszych 16 znaków nazwy wykonywanego programu. Jest to bardzo! przydatna właściwość, gdyż można dzięki niej zidentyfikować plik z obrazem pamięci, ą

I Sygnały zawierające w opisie określenie „błąd sprzętowy" są powiązane ze zdefiniowanymi w konkretnych implementacjach błędami sprzętowymi, i

10. Sygnały

10.2. Koncepcje sygnałów

Wiele z tych nazw wywodzi się z oryginalnej implementacji Uniksa na komputerze PDP-11. Zawsze w podręczniku systemowym można znaleźć informację, z jakimi błędami wiążą się poszczególne sygnały. Opiszemy teraz szczegółowo poszczególne sygnały. SIGABRT SIGALRM

SIGBUS SIGCHLD

SIGCONT

SIGEMT

wskazywanego w polu s l e a d e r struktury s e s s i o n . Sygn SIGHUP jest generowany w opisanej sytuacji, tylko gdy termin nie ma ustawionego sygnalizatora CLOCAL. (Sygnalizator CLOC2 dla terminalu jest ustawiony, jeśli dołączony terminal jest lokaln Jego włączenie wskazuje procedurze obsługi terminalu, że nalei ignorować wszystkie dane dotyczące stanu modemu. W rozdzi; le 11 powiemy, jak ustawiać ten sygnalizator). Zwróćmy uwagę,; lider sesji, który otrzymuje sygnał SIGHUP, może pracować w tl zobacz np. rys. 9.7. Nie było tak w przypadku normalnych sygn; łów wygenerowanych za pomocą terminalu (przerwanie, zakoi czenie czy zawieszenie), które są zawsze dostarczane do pierwszt planowej grupy procesów.

Jest generowany przez funkcję a b o r t (podrozdz. 10.17). Proces kończy się awaryjnie. Jest generowany, gdy przekroczy termin licznik zegarowy ustawiony przez funkcję alarm. Więcej szczegółów można znaleźć w podrozdz. 10.10. Taki sam sygnał pojawia się, gdy licznik zegarowy interwałów czasowych, ustawiony przez funkcję s e t i t i m e r ( 2 ) , przekroczy termin. Wskazuje błąd sprzętowy zdefiniowany w konkretnej implementacjiZa każdym razem, gdy proces kończy się lub zatrzymuje, do procesu macierzystego jest wysyłany sygnał SIGCHLD. Domyślnie sygnał ten jest ignorowany, a więc proces macierzysty musi go przechwycić, jeśli chce być powiadamiany o każdej zmianie stanu potomka. Typową akcją w funkcji przechwytującej sygnał jest wywołanie jednej z funkcji serii wait i pobranie identyfikatora procesu potomnego oraz stanu zakończenia. Starsze wersje Systemu V miały podobny sygnał o nazwie SIGCLD (bez litery H w nazwie). Ten sygnał miał niestandardowe znaczenie i począwszy od wersji SVR2 podręczniki systemowe nie zalecały stosowania go w nowych programach. Aplikacje powinny używać sygnału SIGCHLD. W podrozdziale 10.7 będziemy omawiać oba sygnały. Jest sygnałem techniki sterowania zadaniami, wysłanym w celu wznowienia zatrzymanego wcześniej procesu. Jeśli proces był wcześniej zatrzymany, to domyślną akcją jest jego kontynuacja, w przeciwnym razie sygnał jest ignorowany. Na przykład edytor vi przechwytuje ten sygnał i odświeża ekran terminalu. W podrozdziale 10.20 podajemy więcej szczegółów na ten temat. Wskazuje błąd sprzętowy zdefiniowany w konkretnej implementacji-

Sygnał SIGHUP jest również generowany, gdy zakończy pracę 1 der sesji. System przekazuje go do wszystkich procesów w pierv szoplanowej grupie procesów.

SIGILL

System 4.3BSD generował taki sygnał za pomocą funkcji a b o r t . Obecn funkcja ta służy do wysyłania sygnału SIGABRT.

SIGINFO

SIGINT

Nazwa EMT pochodzi od instrukcji „emulator trap" na komputerze PDP-11.

SIGFPE SIGHUP

Oznacza pojawienie się wyjątku matematycznego, np. dzielenie przez 0, przepełnienie liczby zmiennopozycyjnej itp. Jest wysyłany do procesu sterującego (lidera sesji) związanego z terminalem sesji, jeśli na interfejsie terminalu wykryto rozłączenie. Zgodnie z rys. 9.11, sygnał taki jest wysyłany do procesu

Sygnał ten jest powszechnie stosowany do powiadamiania proc< sów demonów (rozdz. 13), że mają powtórnie wczytać swoje pl ki konfiguracyjne. Wybrano do tego celu właśnie ten sygnał, pc nieważ demony nie powinny używać terminalu sterującego, wic typowo nie korzystają z tego sygnału. Wskazuje, że proces wykonał nielegalną instrukcję sprzętową.

SIGIO

Jest generowany w systemie 4.3+BSD przez procedurę obsłuj terminalu za pomocą klawisza stanu (zazwyczaj służy do tego sc kwencja Control-T). Zostaje wysyłany do wszystkich procesów pracujących w pierwszoplanowej grupie procesów (zob. rys. 9.8 Typowo sygnał ten sprawia, że na terminalu jest wyświetlana ir formacja na temat procesów pierwszoplanowej grupy procesów. Jest generowany przez procedurę obsługi terminalu po nacis nięciu klawisza przerwania (najczęściej za pomocą klawisz DELETE lub sekwencji Control-C). Zostaje wysyłany do wszysi kich procesów pracujących w pierwszoplanowej grupie procesói (zob. rys. 9.8). Sygnał ten jest typowo używany do zatrzymanj programu, który np. wyprowadza na ekran zbyt dużą ilość niepc1 trzebnych danych. i

Oznacza asynchroniczne zdarzenie wejścia-wyjścia. Będzien?| mówić na ten temat w p. 12.6.2.

W tabeli 10.1 w polu domyślnej akcji dla sygnału S I G I O wymieniliśmy dwi możliwości: zakończenie lub zignorowanie. Niestety, wartość domyślna ZE leży od systemu. W systemie SVR4 sygnały S I G I O i SIGPOLL są identyczn
pw_name, "stevens") != 0) printf("return value corrupted!, pw_name = %s\n ptr->pw_name);

10.7. Różne semantyki SIGCLD

tradycję (czyli wprowadza pewne ograniczenia w celu uzyskania zgodności), jeśli ustalimy dyspozycję dla tego sygnału za pomocą funkcji s i g n a l lub s i g s e t (starsze funkcje, zgodne z systemem SVR3, służące do definiowania dyspozycji sygnału). Taka obsługa sygnału SIGCLD wiązała się z poniższymi czynnościami:

tic void alarm(int signo)

1. Jeśli proces ustalił dyspozycję S I G I G N dla sygnału, to żaden potomek procesu wywołującego nie będzie pozostawał w systemie jako proces typu zombie. Zwróćmy uwagę, że taka dyspozycja nie oznacza akcji domyślnej (SIG_DFL), którą na podstawie tab. 10.1 jest właśnie ignorowanie. Zamiast tego, w chwili zakończenia jest usuwana informacja o stanie procesów potomnych. Jeśli następnie proces wywołujący wywoła jednąz funkcji wait, to pozostanie zablokowany, dopóki wszystkie jego procesy potomne nie zakończą pracy, wówczas funkcja wait przekaże wartość -1 oraz ustali wartość zmiennej e r r no równą ECHILD. (Domyślną dyspozycją obsługi tego sygnału jest zignorowanie go, ale ta akcja nie wiąże się z działaniami opisanymi powyżej. Aby uzyskać pokazany efekt, musimy koniecznie jawnie ustalić dyspozycję S I G _ I G N ) .

struct passwd *rootptr; printf("in signal handler\n"); if ( (rootptr = getpwnam("root")) == NULL) err_sys("getpwnam(root) error"); alarm(l) ; return;

Próg. 10.2 Wywołanie niewspółużywalnej funkcji z procedury obsługi sygnału

Uruchomienie programu daje losowe wyniki. Typowo program kończy się sygnałem SIGSEGV, gdy procedura obsługi sygnału powraca pierwszy raz. Sprawdzenie zawartości pliku core pokazuje, że funkcja main wywołała funkcję getpwnam, ale jakiś wewnętrzny wskaźnik został zniszczony, gdy procedura obsługi sygnału wywołała tę funkcję ponownie. Zdarzało się, że nasz program pracował kilka sekund zanim zakończył się awaryjnie z błędem SIGSEGV. Gdy funkcja main nie działała poprawnie po wystąpieniu sygnału, wówczas nie zawsze wiązało się to z niepoprawną wartością powrotu z funkcji getpwnam, zdarzyło się, że wartość ta była właściwa. Raz wywołanie funkcji getpwnam z procedury obsługi sygnału przekazało błąd EBADF (niewłaściwy deskryptor pliku). Jak pokazuje ten przykład, jeśli wywołujemy niewspółużywalną funkcję z procedury obsługi sygnału, to nie można przewidzieć wyniku. •

Norma POSDC.l nie określa, co stanie się, jeśli zignorujemy sygnał SIGCHLD, dlatego taka implementacja jest dozwolona. System 4.3+BSD zawsze tworzy procesy typu zombie, gdy jest ignorowany sygnał SIGCHLD. Jeśli chcemy uniknąć powstawania takich procesów, musimy konsekwentnie wywoływać funkcję w a i t dla każdego potomka. W systemie SVR4, gdy za pomocą funkcji s i g n a l lub s i g s e t ustalamy ignorowanie jako dyspozycję dla sygnału SIGCHLD, to procesy zombie nigdy nie powstaną. Unikniemy takich procesów, stosując w zamian funkcję s i g a c t i o n w SVR4 z ustawionym sygnalizatorem SA_NOCLDWAIT (tab. 10.5).

2. Gdy jest ustalana nowa dyspozycja dla SIGCHLD oznaczająca przechwytywanie sygnału, wówczas jądro systemu natychmiast sprawdza, czy są jakieś procesy potomne, dla których od razu można wywołać funkcję wait; jeśli tak, to natychmiast jest wywoływana procedura obsługi sygnału.

7 Różne semantyki S I G C L D Dwa sygnały, SIGCLD oraz SIGCHLD, są powodem pewnego zamieszania. Po pierwsze, sygnał SIGCLD (bez litery H w nazwie) jest nazwą w Systemie V i ma on inną semantykę niż sygnał BSD o nazwie SIGCHLD. Sygnał normy POSIX.l ma nazwę SIGCHLD. W systemach berkelejowskich znaczenie sygnału SIGCHLD jest typowe, aje^ł zachowanie takie samo jak innych sygnałów. Po pojawieniu się takiego sygnału, oznaczającego zmianę stanu procesu potomnego, musimy wywołać jednąz funkcji wait, by dowiedzieć się szczegółowo, co się wydarzyło. Jednak w Systemie V sygnał SIGCLD był tradycyjnie obsługiwany inaczej niż inne sygnały. System SRV4 kontynuuje tę budzącą sporo wątpliwości

339

Punkt drugi powoduje, że musimy zmienić sposób przygotowywania procedury obsługi sygnału dla tego typu sygnału.

Przykład Zgodnie z tym, co stwierdziliśmy w podrozdz. 10.4, pierwszą czynnością w procedurze obsługi sygnału powinno być powtórne wywołanie funkcji s i g n a l , aby ponownie zarejestrować procedurę. (W ten sposób uzyskujemy minimalny przedział czasu, podczas którego powracamy do domyślnej dyspo-

10. Sygnały

10.8. Terminologia i semantyka sygnałów niezawodnych

zycji dla sygnału, a właśnie wówczas może zaginąć sygnał). Pokazujemy to w próg. 10.3. Ten program nie działa. Jeśli skompilujemy go i uruchomimy w systemie SVR2, to jako wynik uzyskamy niekończącą się sekwencję wierszy o postaci SIGCLD received. Taki proces może również zużyć dostępną pamięć na stosie i zakończyć się awaryjnie. iclude iclude iclude

tomny potrzebujący wywołania funkcji wait (w tym przypadku jest taki proces, ponieważ przetwarzamy sygnał SIGCLD); jeśli jest, to generuje kolejne wywołanie procedury obsługi sygnału. Procedura ta wywołuje funkcję s i g n a l i cała sekwencja powtarza się. Aby usunąć błąd w naszym programie, musimy przenieść wywołanie s i g n a l za wywołanie funkcji wait. W ten sposób funkcja s i g n a l wykonuje się po pobraniu stanu zakończenia procesu potomnego, a system wygeneruje kolejny sygnał, dopiero gdy zakończy się inny proces potomny.



Norma POSIX.l stwierdza, że jeśli ustalamy procedurę obsługi sygnału SIGCHLD, a w tym czasie istnieje potomek, który zakończył pracę, i nikt na niego dotychczas nie oczekiwał, to nie ma pewności, czy zostanie wygenerowany sygnał, czy nie. Dlatego jest możliwe opisane poprzednio zachowanie. Ponieważ jednak, zgodnie z normą POSDCl, wystąpienie sygnału nie powoduje powrotu do domyślnej dyspozycji jego obsługi (zakładając, że korzystamy z posiksowej funkcji s i g a c t i o n dla ustalenia dyspozycji), więc nie ma potrzeby, by ponownie ustalać dyspozycję dla sygnału SIGCHLD w procedurze obsługi sygnału. •

atic void sig_cld(); t

pid_t

pid;

if (signal(SIGCLD, sig_cld) == -1) perror("signal error") ;

Zawsze pamiętajmy o semantyce, jaką konkretna implementacja wiąże z sygnałem SIGCHLD. Bądźmy też przygotowani na to, że w niektórych systemach istnieje deklaracja zamieszczona w odpowiednich plikach nagłówkowych, która ustala, że sygnał #def i n e SIGCHLD jest równoważny sygnałowi SIGCHLD lub odwrotnie. Taka zmiana nazwy może wprawdzie umożliwić kompilację programu napisanego dla innego systemu, ale jeśli program taki opiera się na innej semantyce niż obowiązująca w tym systemie, to może nie działać poprawnie.

if ( (pid = forkO ) < 0) perror("fork error"); else if (pid == 0) { /* potomek */ sleep(2); exit (0); pause(); exit (0);

/* proces macierzysty */

atic void g_cld() pid_t int

10.8 pid; status;

printf("SIGCLD received\n"); if (signal(SIGCLD, sig_cld) == -1) perror("signal error");

341

/* ustanawiamy procedurę obsługi */

if ( (pid = wait(Sstatus)) < 0) /* pobieramy stan procesu potomnego */ perror("wait error"); printf("pid = %d\n", pid); return; /* przerywamy realizację funkcji pause() */

?' Próg. 10.3 Działająca niepoprawnie procedura obsługi sygnału w Systemie V

Problem w tym przykładzie polega na tym, że wywołanie s i g n a l na początku procedury obsługi sygnału wiąże się z uruchomieniem działań przedstawionych powyżej w punkcie 2 - jądro sprawdza, czy jest jakiś proces po-

Terminologia i semantyka sygnałów niezawodnych W naszej dyskusji na temat sygnałów przewijają się wielokrotnie terminy, które w końcu musimy zdefiniować. Po pierwsze, sygnał jest generowany dla określonego procesu (lub po prostu wysyłany do procesu), gdy zachodzi zdarzenie, którego następstwem jest pojawienie się sygnału. Zdarzeniem takim , może być wyjątek sprzętowy (np. próba dzielenia przez 0), okoliczności pro- ,,| gramowe (np. przekroczenie terminu przez licznik zegarowy) lub wywołanie | funkcji k i l l . Jądro systemu, generując sygnał, zazwyczaj ustawia jednoczę1 , śnie pewien sygnalizator w tablicy procesów. Mówimy, że sygnał jest dostarczany do procesu, gdy proces podejmuje ' akcję obsługi sygnału. W czasie między wygenerowaniem sygnału a jego dostarczeniem sygnał oczekuje na dostarczenie. i Proces ma możliwość zablokowania dostarczania sygnału. Jeśli został J wygenerowany sygnał dla procesu, który zablokował dany sygnał, a dyspozy- _ cją dla tego sygnału jest albo akcja domyślna, albo przechwycenie, to taki sy- . gnał pozostaje w oczekiwaniu, dopóki proces albo (a) odblokuje sygnał, albo (b) zmieni dyspozycję na ignorowanie. System decyduje, co zrobić z zablo- .

10. Sygnały kowanym sygnałem, gdy go dostarcza, a nie gdy syggnał jest generowany. Dzięki temu proces może zmienić akcję dotyczącą sygmału przed jego dostarczeniem. Proces może wywołać funkcję sigpending ((podrozdz. 10.13), aby stwierdzić, jakie sygnały są zablokowane lub jakie oczekkująna dostarczenie. Co stanie się, jeśli zablokowany sygnał zostanie vwygenerowany więcej niż jeden raz, zanim proces odblokuje ten sygnał? Norrma POSDC.l pozwala systemowi dostarczać określony sygnał więcej niż jedenn raz. Jeśli system dostarczy sygnał więcej niż jeden raz, to mówimy o zakoblejkowaniu sygnałów. Jednak większość systemów nie kolejkuje sygnałów. Ją^dro systemu Unix dostarcza sygnał tylko jeden raz.

10.9. Funkcje k i l l oraz r a i s e #include #include int kill (pid_t pid, int signo) ; int raise (int signo); Przekazują: 0, jeśli wszystko w porządku; - 1 , jeśli wystąpił błąd

Argument/?/£/ funkcji k i l l spełnia jeden z czterech warunków. pid > 0 pid==0

Wprawdzie podręcznik systemowy wczesnych wersji Systenmu V stwierdzał, że sygnał SIGCLD jest kolejkowany, jednak w rzeczywistości wcale taak nie było. Sygnał ten był po prostu powtórnie generowany przez jądro, zgodnie z opiseem w podrozdz. 10.7. W opisie funkcji sigaction(2) w dokumentacji [13] stwieprdza się, że dzięki ustawieniu sygnalizatora SA_SIGINFO (tab. 10.5) sygnały są kołeejkowane w sposób niezawodny. Nie jest to prawdą. Taka właściwość istnieje w jądrzee systemu, ale nie jest uaktywniona w systemie SVR4.

Co stanie się, jeśli mamy gotowych kilka sygnałów do dostarczenia do procesu? Norma POSIX.l nie określa uporządkowania, zzgodnie z którym takie sygnały będą dostarczane do procesu. W dodatku Rationoale for POSLK.1 zaleca się, by sygnały związane z bieżącym stanem procesu, jzak np. SIGSEGV, były dostarczane przed innymi sygnałami. Każdy proces ma maską sygnałów, definiującą zesttaw sygnałów, których dostarczanie do procesu jest aktualnie zablokowane. Mlożemy wyobrazić sobie, że w takiej masce jeden bit odpowiada jednemu poteencjalnemu sygnałowi w systemie. Jeśli bit dla danego sygnału jest ustawiony,, to sygnał jest aktualnie zablokowany. Proces może sprawdzić i zmienić bieżżącą maskę sygnałów, wywołując funkcję sigprocmask, którą opiszemy w poDdrozdz. 10.12. Ponieważ typ i n t ogranicza liczbę dostępnych s^ygnałów, które mogą być reprezentowane w takiej masce, więc norma POSDK.l definiuje nowy typ danych, s i g s e t _ t , służący do przechowywania zbiorui sygnałów. Maska sygnałów jest umieszczana jako jeden zbiór sygnałów. W podrozdziale 10.11 opiszemy pięć funkcji, które operują na zbiorach sygnałćów.

Funkcje k i l l oraz r a i s e FuK&cja k i l l wysyła sygnał do procesu lub do grupy prrocesów. Dzięki funkcji r a i s e proces może wysłać sygnał do siebie. Funkcję r a i s e definiuje standard ANSI C, a nie POSDC.l. Ponieważ ANSI C nie uwzględnia wielu procesów w systemie, więc nie może deifiniować takiej funkcji jak k i l l , której argumentem jest identyfikator procesu.

343

pid < 0

pid == -1

Sygnał jest wysyłany do procesu o identyfikatorze pid. Sygnał jest wysyłany do wszystkich procesów, których identyfikator grupy procesów jest równy identyfikatorowi grupy procesów procesu wysyłającego sygnał, a oprócz tego proces wysyłający musi mieć uprawnienia do przekazywania sygnału do tych procesów. Termin „wszystkie procesy" wyłącza zdefiniowany w implementacji zbiór procesów systemowych. W większości systemów Unix w zbiorze procesów systemowych znajdują się proces wymiany (identyfikator 0), proces i n i t (identyfikator 1) oraz demon stronicowania (identyfikator 2). Sygnał jest wysyłany do wszystkich procesów, których identyfikator grupy procesów jest równy wartości absolutnej argumentu pid i proces wysyłający ma uprawnienia pozwalające mu przekazać sygnał do tych procesów. Jak poprzednio, zbiór „wszystkich procesów" wyłącza pewne procesy systemowe, które wymieniliśmy wcześniej. Norma POSIX. 1 pozostawia ten warunek nie określony. Systemy SVR4 i 4.3+BSD używają tej opcji do wysyłania tzw. sygnałów rozpowszechnianych (broadcast signals). Tego typu sygnały nie są nigdy przesyłane do zbiorów procesów opisanych powyżej. System 4.3+BSD nie przekazuje nigdy sygnału rozpowszechnianego do procesu wysyłającego ten sygnał. Jeśli taką funkcję wywołuje nadzorca systemu, to sygnał jest przekazywany do wszystkich procesów. Jeśli wywołujący nie jest nadzorcą, to sygnał jest wysyłany do wszystkich procesów, których rzeczywisty lub zachowany identyfikator użytkownika jest równy rzeczywistemu lub obowiązującemu identyfikatorowi użytkownika procesu wywołującego tę funkcję. Sygnały rozgłoszeniowe po-, winny być stosowane tylko do celów administracyjnych (np. gdy jakiś proces nadzorcy przygotowuje się do zamknięcia systemu).

Wspomnieliśmy już, że proces musi mieć pewne uprawnienia, by mógł wysłać sygnał do jakiegoś innego procesu. Nadzorca może wysłać sygnał do dowolnego procesu. Dla innych użytkowników podstawową zasadą jest zgodność rzeczywistego lub obowiązującego identyfikatora użytkownika nadawcy sygnału z rzeczywistym lub obowiązującym identyfikatorem użytkownika procesu odbierającego sygnał. Jeśli dana implementacja korzysta z właściwości definiowanej za pomocą stałej _POSIX_SAVED_IDS (np. system SVR4),

10. Sygnały

to w procesie odbiorcy jest sprawdzany zachowany identyfikator użytkownika, a nie obowiązujący identyfikator użytkownika. Gdy kontrolujemy prawa dostępu, to musimy wziąć pod uwagę jeden specjalny przypadek: jeśli wysyłanym sygnałem jest SIGCONT, to proces może go przekazać do każdego procesu, który jest członkiem tej samej sesji. W normie POSIX.l zdefiniowano sygnał pusty o numerze 0. Jeśli argument signo jest równy 0, to funkcja k i l l wykonuje rutynowe czynności, ale nie wysyła żadnego sygnału. Służy to często do sprawdzenia, czy dany proces nadal istnieje. Jeśli wysyłamy pusty sygnał do nie istniejącego procesu, to funkcja k i l l przekazuje wartość -1 oraz nadaje zmiennej e r r n o wartość ESRCH. Pamiętajmy jednak, że system Unix po pewnym czasie powtórnie używa zwolnionych identyfikatorów procesów, a więc istnienie procesu o danym identyfikatorze nie musi oznaczać, że w systemie jest proces, który nas interesuje. Jeśli funkcja k i l l spowoduje, że zostanie wygenerowany sygnał skierowany do procesu wywołującego ją i gdy sygnał taki nie jest zablokowany, to przed powrotem z funkcji k i l l jest dostarczany albo sygnał signo, albo inne oczekujące na dostarczenie, niezablokowane sygnały.

10 Funkcje a l a r m oraz p a u s e Funkcja alarm umożliwia ustawienie licznika czasu, który w zadanym czasie w przyszłości przekroczony termin. Zostanie wówczas wygenerowany sygnał SIGALRM. Jeśli ignorujemy lub nie przychwytujemy tego sygnału, to domyślną akcjąjest zakończenie procesu. #include unsigned i n t alarm(unsigned i n t seconds) ; Przekazuje: 0 lub liczbę sekund do przewidzianego ustalonego alarmu

Wartość argumentu seconds określa, po ilu sekundach ma zostać wygenerowany sygnał. Gdy licznik przekroczy termin, jądro systemu wygeneruje sygnał, ale z powodu opóźnień związanych z zarządzaniem pracą procesora może minąć trochę czasu zanim proces, do którego sygnał był skierowany, przejmie kontrolę, by go obsłużyć. Starsze wersje Uniksa ostrzegały również, że sygnał może zostać wysłany sekundę wcześniej. Norma POSIX.l nie pozwala na to.

^ W procesie może istnieć tylko jeden licznik zegarowy związany z alarmem. Jeżeli w chwili wywołania funkcji alarm istnieje poprzednio zarejestrowany licznik zegarowy alarmu dla tego procesu i nie przekroczył on jeszcze terminu, to jako wynik wywołania otrzymujemy liczbę sekund pozostających do tamtego alarmu. Nowa wartość, przekazana w argumencie seconds, zastępuje poprzednio zarejestrowany licznik alarmu.

345

10.10. Funkcje a l a r m oraz pause

Jeśli natomiast w chwili wywołania funkcji alarm z argumentem seconds równym 0 istnieje wcześniej zarejestrowany licznik alarmu, który nie zdążył przekroczyć terminu, to poprzedni licznik jest kasowany. Podobnie jak w poprzedniej sytuacji, wynikiem funkcji jest liczba sekund pozostających do przekroczenia terminu przez poprzedni licznik alarmu. Wprawdzie domyślną akcją dla sygnału SIGALRM jest zakończenie procesu, lecz większość procesów używających licznika alarmu przechwytuje ten sygnał. Nawet gdy taki proces chce zakończyć pracę, najpierw powinien dokonać pewnych działań porządkujących środowisko pracy. Funkcja pause zawiesza wywołujący ją proces do czasu przechwycenia sygnału. #include int pause (void); Przekazuje: -1 oraz zmienną errno równą EINTR

Funkcja pause może powrócić, tylko gdy wykonała się procedura obsługi sygnału, która zakończyła się powrotem. W tym przypadku funkcja pause przekazuje wartość - 1 , a zmienna e r r n o jest równa EINTR.

Przykład Używając funkcji alarm oraz pause można wprowadzić proces w stan uśpienia na określony czas. Tak właśnie działa funkcja s l e e p l z próg. 10.4. #include #include



static void sig_alrm(int signo) { return; /* nic nie musimy robić, tylko powracamy, by zakończyć funkcję pause() */ unsigned int sleepl(unsigned int nsecs) { if (signal(SIGALRM, sig_alrm) == SIG_ERR) return(nsecs); alarm(nsecs); /i start licznika czasu */ pause () ; /•> obudzi nas następny przechwycony sygnał */ r e t u r n ( alarm(O) ); / J wyłączamy licznik czasu, przekazujemy czas do wygaśnięcia licznika */

Próg. 10.4 Prosta, niekompletna implementacja funkcji s l e e p

10. Sygnały

Nasza funkcja jest wzorowana na zwykłej funkcji s l e e p , którą opisujemy w podrozdz. 10.19. Ta prosta implementacja sprawia pewne problemy. 1. Jeśli wywołujący ją proces wcześniej zarejestrował licznik alarmu, to licznik taki jest zamazywany przez pierwsze wywołanie funkcji alarm. Możemy poprawić to, sprawdzając wartość powrotu po wywołaniu funkcji alarm. Jeśli liczba sekund pozostających do przekroczenia terminu przez poprzednio ustawiony licznik alarmu jest mniejsza niż wartość przekazanego argumentu, to powinniśmy czekać tylko do czasu, gdy poprzedni licznik przekroczy termin. Jeśli poprzednio ustawiony licznik alarmu miałby przekroczyć termin po naszym liczniku alarmu, to przed powrotem z funkcji obsługi sygnału powinniśmy spowodować, by kolejny sygnał alarmu pojawił się w zaplanowanym wcześniej czasie. 2. Zmodyfikowaliśmy dyspozycję dla sygnału SIGALRM. Jeśli przygotowujemy funkcję, która będzie wywoływana w nieznanej sytuacji, to powinniśmy zapamiętać poprzednią dyspozycję dla tego sygnału i odtworzyć japo zakończeniu określonego fragmentu programu. Możemy poprawić to w naszym programie, przypisując wartość powrotu z funkcji s i g n a l jakiejś zmiennej, a następnie odtworzyć dyspozycję przed powrotem z obsługi. 3. Między wywołaniem funkcji alarm a wywołaniem funkcji pause istnieje niebezpieczeństwo pojawienia się sytuacji wyścigu. W bardzo obciążonych systemach może się zdarzyć, że zanim będzie wywołana funkcja pause licznik alarmu przekroczy termin i sygnał alarmu zostanie obsłużony. Jeśli tak się stanie, wywołujący może na zawsze zablokować się w funkcji pause (przy założeniu, że nie będzie przechwycony kolejny sygnał). Wcześniejsze wersje funkcji s l e e p były wzorowane na kodzie pokazanym w naszej funkcji, ale miały poprawione niedociągnięcia omówione w punktach 1 i 2. Są dwa sposoby, by zaradzić problemowi z punktu 3. Pierwszy korzysta z funkcji setjmp - omówimy go wkrótce. Drugi używa funkcji sigprocmask oraz sigsuspend; tę wersję pokażemy w podrozdz. 10.19.

D

ykład Implementacja funkcji s l e e p w systemie SVR2 używała funkcji setjmp oraz longjmp (podrozdz. 7.10), by uniknąć opisanej w punkcie 3 sytuacji wyścigu. Prostą wersję takiej funkcji, nazwaną sleep2, pokazuje próg. 10.5. (Aby zmniejszyć rozmiar kodu przykładowego programu, nie obsługujemy tu problemów przedstawionych w punktach 1 i 2).

347

10.10. Funkcje alarm oraz pause #include łfinclude #include



static jmp_buf env_alrm; static void sig alrm(int signo) { longjmp(env_alrm, 1); unsigned int sleep2(unsigned int nsecs) { if (signal(SIGALRM, sig_alrm) == SIG_ERR) return(nsecs); if (setjmp(env_alrm) == 0) { alarm(nsecs); /* start licznika czasu */ pause(); /* obudzi nas następny przechwycony sygnał */ return( alarm(O) );

/* wyłączamy licznik czasu, przekazujemy czas do wygaśnięcia licznika */

Próg. 10.5 Inna (niedoskonała) implementacja funkcji s l e e p

W tym programie nie pojawi się sytuacja wyścigu, która groziła w próg. 10.4. Funkcja s l e e p 2 powraca, nawet gdy funkcja pause nie zostanie wykonana po wystąpieniu sygnału. W tej funkcji jest inny subtelny problem, wiążący się z kolizją z innymi sygnałami. Jeśli sygnał SIGALRM przerywa inną procedurę obsługi sygnału, to wywołanie funkcji longjmp kończy awaryjnie tę procedurę. Program 10.6 pokazuje ten scenariusz. Pętla obsługi sygnału SIGINT została tak przygotowana, że wykonuje się w systemie, z którego korzystał autor dłużej niż 5 sekund. Celowo chcemy, by jej wykonanie przekroczyło czas podany w argumencie funkcji sleep2. Zadeklarowaliśmy też zmienną j z kwalifikatorem v o l a t i l e , aby nie dopuścić do optymalizacji kompilatora, która mogłaby usunąć naszą pętlę. Wykonanie próg. 10.6 daje poniższy wynik $ a.out A,

wprowadzamy znak przerwania

sig_int starting sleep2 returned:

Widzimy, że wywołanie funkcji longjmp w funkcji s l e e p 2 zakończyło awaryjnie inną procedurę obsługi sygnału, s i g i n t , chociaż nie wykonała ona jeszcze wszystkich swoich czynności. Taka sytuacja zdarza, się, gdy użyjemy funkcji s l e e p zgodnej z systemem SVR2 razem z obsługą innych sygnałów. Zobacz ćw. 10.3. •

10. Sygnały ludę ludę gned int ic void

"ourhdr.h" sleep2(unsigned int sig int(int);

(void) unsigned int

unslept;

if (signal(SIGINT, sig_int) == SIG_ERR) err_sys("signal(SIGINT) error"); unslept = sleep2(5); printf("sleep2 returned: %u\n", unslept), sxit(0);

10.10. Funkcje a l a r m oraz pause #include #include

349

"ourhdr.h"

static void sig alrm(int) ; int main(void) n; linę[MAXLINE];

int char

if (signal(SIGALRM, sig_alrm) == SIG_ERR) err_sys("signal(SIGALRM) error"); alarm(lO); if ( (n = read(STDIN_FILENO, linę, MAXLINE)) < 0) err_sys("read error"); alarm(0); write(STDOUT_FILENO, linę, n ) ;

Lc void

exit (0);

Lnt(int signo) .nt rolatile int

i; j;

>rintf("\nsig_int starting\n" :or (i = 0; i < 2000000; i++) j += i * i ; irintf("sig_int finished\n"); •eturn;

Próg. 10.6 Wywołanie ftinkcji s l e e p 2 z programu przechwytującego inne sygnały

Celem dwóch przykładów, funkcji s l e e p l oraz sleep2, było zaprezentowanie następstw nieświadomej obsługi sygnałów. Kolejny podrozdział pokaże, jak omijać tego rodzaju trudności i jak niezawodnie obsługiwać sygnały, niezależnie od tego, co dzieje się w innych fragmentach kodu w programie. [ład Popularnym zastosowaniem funkcji alarm, oprócz implementacji funkcji s l e e p , jest ustalanie górnego limitu czasu wykonywania operacji, które mogą uleu zablokowaniu. Na przykład, jeśli mamy operację odczytu z urządzenia bjfclcującego pracę (urządzenie „powolne", jak opisaliśmy w podrozdz. 10.5), to przydatne może być ustalenie czasu oczekiwania, po którym operacja zakończy się, nawet gdy nie nadeszły żadne dane. Program 10.7 realizuje taki model pracy, czyta po jednym wierszu ze standardowego wejścia i zapisuje pobrane dane na standardowe wyjście.

static void sig_alrm(int signo) return;

/* nic nie robimy, tylko powracamy do przerwanego odczytu */

Próg. 10.7 Wywołanie r e a d z ustalonym czasem oczekiwania

Taką sekwencję kodu możemy spotkać w wielu aplikacjach uniksowych, ale niestety w pokazanym programie mamy kilka problemów. 1. Program 10.7 działa podobnie jak opisany wcześniej próg. 10.4, a więc między wywołaniami alarm a r e a d istnieje groźba wystąpienia sytuacji wyścigu. Jeżeli jądro systemu zablokuje nasz proces w tym przedziale czasu na dłużej niż trwa okres do wystąpienia alarmu, to funkcja r e a d może się zablokować na zawsze. Większość tego typu operacji nadaje licznikowi alarmu wystarczająco dużą wartość, np. minutę lub dłużej, by zmniejszyć niebezpieczeństwo wyścigu, ale mimo to zagrożenie nadal istnieje. 2. Jeśli funkcje systemowe są automatycznie wznawiane, to sygnał SIGALRM nie przerwie funkcji read, gdy powraca procedura obsługi sygnału. W tej sytuacji ustalony czas oczekiwania nie ma żadnego znaczenia. W tym przypadku chcemy, by powolne funkcje systemowe były przerywane. Norma POSIX.l nie daje jednak przenośnej metody rozwiązania tego problemu, n

348

10. Sygnały

#include #include unsigned int static void

"ourhdr.h" sleep2(unsigned int); sig_int(int);

int main(void) unsigned int

unslept;

unslept = sleep2(5); printf("sleep2 returned: %u\n", unslept) exit (0);

"ourhdr.h"

static void sig alrm(int); int main(void) n; line[MAXLINE] ;

int char

if (signal(SIGALRM, sig_alrm) == SIG_ERR) err_sys("signal(SIGALRM) error"); alarm(10); if ( (n = read(STDIN_FILENO, linę, MAXLINE)) < 0) err_sys("read error"); alarm(O); write(STDOUT_FILENO, linę, n ) ;

static void sig int(int signo) int volatile int

#include #include

i

if (signal(SIGINT, sig_int) == SIG_ERR) err_sys("signal(SIGINT) error");

i i

10.10. Funkcje alarm oraz pause

exit (0);

i; j;

printf(" \nsig int starting\n"); for (i = 0; i < 2000000; i++) j += i * i; printf(" sig int finished\n"); return;

Próg. 10.6 Wywołanie funkcji s l e e p 2 z programu przechwytującego inne sygnały

Celem dwóch przykładów, funkcji s l e e p l oraz sleep2, było zaprezentowanie następstw nieświadomej obsługi sygnałów. Kolejny podrozdział pokaże, jak omijać tego rodzaju trudności i jak niezawodnie obsługiwać sygnały, niezależnie od tego, co dzieje się w innych fragmentach kodu w programie.

Przykład Popularnym zastosowaniem funkcji alarm, oprócz implementacji funkcji s l e e p , jest ustalanie górnego limitu czasu wykonywania operacji, które mogą ulec zablokowaniu. Na przykład, jeśli mamy operację odczytu z urządzenia blokującego pracę (urządzenie „powolne", jak opisaliśmy w podrozdz. 10.5), to przydatne może być ustalenie czasu oczekiwania, po którym operacja zakończy się, nawet gdy nie nadeszły żadne dane. Program 10.7 realizuje taki model pracy, czyta po jednym wierszu ze standardowego wejścia i zapisuje pobrane dane na standardowe wyjście.

static void sig_alrm(int signo) return;

/* nic nie robimy, tylko powracamy do przerwanego odczytu

Próg. 10.7 Wywołanie r e a d z ustalonym czasem oczekiwania

Taką sekwencję kodu możemy spotkać w wielu aplikacjach uniksowyc niestety w pokazanym programie mamy kilka problemów.

1. Program 10.7 działa podobnie jak opisany wcześniej próg. a więc między wywołaniami alarm a r e a d istnieje groźba wys nia sytuacji wyścigu. Jeżeli jądro systemu zablokuje nasz p w tym przedziale czasu na dłużej niż trwa okres do wystąpienia mu, to funkcja r e a d może się zablokować na zawsze. Większoś>j typu operacji nadaje licznikowi alarmu wystarczająco dużą wa| np. minutę lub dłużej, by zmniejszyć niebezpieczeństwo wyścig mimo to zagrożenie nadal istnieje. ' 2. Jeśli funkcje systemowe są automatycznie wznawiane, to Ą SIGALRM nie przerwie funkcji read, gdy powraca procedur, sługi sygnału. W tej sytuacji ustalony czas oczekiwania nie m nego znaczenia.

W tym przypadku chcemy, by powolne funkcje systemowe były przery^ Norma POSIX.l nie daje jednak przenośnej metody rozwiązania tegd blemu. :

iO

10. Sygnały

zykład

10.11 Zbiory sygnałów

Zmienimy teraz poprzedni przykład, używający funkcji longjmp (próg. 10.8). Dzięki tej modyfikacji nie musimy troszczyć się o to, czy wolne przerwanie systemowe jest przerywane, czy nie. nclude nclude nclude

351

10.11. Zbiory sygnałów

"ourhdr.h"

atic void atic jmp_buf

sig_alrm(int) env_alrm;

in (void) int

n;

char

line[MAXLINE];

if (signal(SIGALRM, sig_alrm) == SIG_ERR) err_sys("signal(SIGALRM) error"); if (setjmp(env_alrm) != 0) err_quit("read timeout"); alarm(10); if ( (n = read(STDIN_FILENO, linę, MAXLINE)) < 0) err_sys("read error"); alarm(0);

Jest nam potrzebny typ danych, który mógłby reprezentować wiele sygnałów - zbiór sygnałów. Będziemy z niego korzystać w takich funkcjach, jak np. sigprocmask (w następnym podrozdziale), aby przekazać do jądra systemu informację, że nie ma dostarczać do procesu sygnałów, które są wskazane w zbiorze. Jak wcześniej zauważyliśmy, liczba różnych sygnałów może przekroczyć ilość bitów w liczbie całkowitej. Dlatego w ogólnym przypadku nie możemy używać typu i n t do reprezentowania zbioru wszystkich sygnałów w systemie, przy założeniu, że jeden bit odpowiada jednemu rodzajowi sygnału. Norma POSDC.l definiuje typ danych o nazwie s i g s e t _ t , zawierający zbiór sygnałów oraz poniższe pięć funkcji służących do manipulowania zbiorami sygnałów. #include int sigemptyset(sigset t *set) ; int sigfillset(sigset t *set) ; int sigaddset(sigset t *set, i n t signo) ; int sigdelset(sigset t *set, i n t signo) ;

Wszystkie cztery przekazują: 0, jeśli wszystko w porządku; - 1 , jeśli wystąpił błąd int sigismember(sigset t *set, i n t signo) ;

Przekazuje: 1, jeśli prawda, 0, jeśli nieprawda

write(STDOUT_FILENO, linę, n ) ; exit (0); atic void g_alrm(int signo) longjmp(env_alrm, 1);

Próg. 10.8 Wywołanie r e a d z ustalonym czasem oczekiwania przy użyciu l o n g j m p

Ta wersja działa zgodnie z oczekiwaniami, niezależnie od tego, czy system wznawia przerwane funkcje systemowe, czy nie. Nadal jednak mamy problem interakcji z innymi procedurami obsługi sygnału, podobnie jak w próg. 10.5. • jfidy chcemy ustalić limit wykonywania operacji wejścia-wyjścia, wówczas'musimy użyć funkcji longjmp, jak pokazaliśmy poprzednio, uwzględniając jednocześnie, że mogą pojawić się problemy współpracy z innymi procedurami obsługi sygnału. Alternatywnym rozwiązaniem jest zastosowanie funkcji s e l e c t lub p o l l , opisanych w punktach 12.5.1 oraz 12.5.2.

Wówczas funkcja sigemptyset inicjuje wskazywany przez argument set zbiór sygnałów, wyłączając w nim wszystkie sygnały. Funkcja s i g f i l l s e t inicjuje wskazywany przez argument set zbiór sygnałów, włączając w nim wszystkie sygnały. Wszystkie aplikacje przed użyciem każdego zbioru sygnałów muszą wywołać funkcję sigemptyset lub s i g f i l l s e t dla tego zbioru. Jest to wymagane, ponieważ nie można założyć, że zainicjowanie zmiennych zewnętrznych i statycznych w języku C (ustalenie wartości zerowej) jest zgodne z implementacją zbioru sygnałów w danym systemie operacyjnym. i Po ustaleniu wartości początkowej zbioru sygnałów możemy dodawać do , niego sygnały oraz usuwać konkretne sygnały. Funkcja s i g a d d s e t dodaje, pojedynczy sygnał do istniejącego zbioru sygnałów, a s i g d e l s e t usuwa po- J jedynczy sygnał ze zbioru. Wkrótce zobaczymy, że we wszystkich funkcjach, i które wymagają jako argumentu zbioru sygnałów, podajemy adres takiego i zbioru. I

I

Implementacja

Jeśli konkretna implementacja ma mniej sygnałów niż jest bitów w liczbie całkowitej, to zbiór sygnałów może być implementowany przy użyciu jed- ;

10. Sygnały

52

nego bitu na sygnał. Większość implementacji systemów 4.3+BSD używa 31 sygnałów oraz 32-bitowego typu i n t . Funkcja s i g e m p t y s e t zeruje liczbę całkowitą, a s i g f i l l s e t ustawia wszystkie bity w liczbie całkowitej. Te dwie funkcje mogą być implementowane jako makra w pliku nagłówkowym :

Może nam się wydawać, że najwygodniejsza jest implementacja tych trzech funkcji jako jednowierszowego makra w pliku nagłówkowym < s i g n a l . h>, ale norma POSIX.l wymaga, by zawsze była sprawdzana poprawność argumentu będącego numerem sygnału oraz by funkcja ustawiała wartość zmiennej e r r n o . Te czynności jest dużo łatwiej wykonać w funkcji niż w makrze.

(ptr) = 0 ) • (ptr) = "(sigset_t)0, 0

#define sigemptyset(ptr) #define sigfillset(ptr)

Zwróćmy uwagę na to, że funkcja s i g f i l l s e t musi przekazać wartość zero oraz ustawić wszystkie bity w zbiorze sygnałów, używamy więc przecinka jako operatora języka C - wartość po przecinku staje się wartością całego wyrażenia. W tej implementacji funkcja s i g a d d s e t ustawia jeden bit, a funkcja s i g d e l s e t zeruje pojedynczy bit. Funkcja sigismember sprawdza określony bit. Ponieważ nigdy nie będzie istniał w zbiorze sygnał o numerze 0, więc w celu otrzymania numeru bitu, który mamy obsłużyć, odejmujemy 1 od numeru sygnału. Program 10.9 zawiera te funkcje.

10.12 Funkcja sigprocmask W podrozdziale 10.8 powiedzieliśmy, że maska sygnałów procesu jest zbiorem sygnałów, dla których jest zablokowana możliwość dostarczania do procesu. Wywołując poniższą funkcję, proces może sprawdzać lub zmieniać (albo wykonywać jednocześnie obie czynności) swoją maskę sygnałów. tinclude i n t sigprocmask (int how, const s i g s e t _ t *set, s i g s e t _ t *oset) ;



include include

353

10.12. Funkcja sigprocmask

Przekazuje: 0, jeśli wszystko w porządku; - 1 , jeśli wystąpił błąd

^define SIGBAD(signo) ( (signo) = NSIG) '* zazwyczaj definiuje s t a ł ą NSIG łącznie z sygnałem o numerze 0 */

Po pierwsze, jeśli argument oset jest wskaźnikiem niepustym, to pod tym adresem funkcja przekazuje bieżące ustawienie maski w procesie. Po drugie, jeśli argument set jest wskaźnikiem niepustym, to argument how wskazuje, w jaki sposób ma być zmodyfikowana bieżąca maska. W tabeli 10.4 pokazujemy różne możliwe wartości argumentu how. S I G B L O C K jest operacją alternatywy, a SIG_SETMASK przypisaniem.

.nt iigaddset(sigset_t *set, int signo) if (SIGBAD(signo)) { errno = EINVAL; return(-l); } *set |= 1 « (signo - 1); return(0) ;

/* ustawiamy bit */

Tabela 10.4 Sposoby zmiany bieżącej maski sygnałów za pomocą funkcji s i g p r o c m a s k lint igdelset(sigset_t *set, int signo)

how

Opis

SIG BŁOCK

Nowa maska sygnałów procesu jest połączeniem bieżącej maski sygnałów tego procesu i zbioru sygnałów wskazanego przez set. Oznacza to, że zbiór set zawiera dodatkowe sygnały, które chcemy zablokować.

SIG UNBLOCK

Nowa maska sygnałów procesu jest częścią wspólną bieżącej maski sygnałów tego, procesu i uzupełnienia zbioru sygnałów wskazanego przez set. Oznacza to, że zbiór set zawiera sygnały, które chcemy odblokować.

SIG SETMASK

Nowa maska sygnałów procesu jest wartością wskazywaną przez set.

if (SIGBAD(signo)) { errno = EINVAL; return(-l); } *set &= ~(1 « (signo - 1)); return(0);

/* zerujemy bit */

int sigismember(const sigset_t *set, int signo)

I

if (SIGBAD(signo)) returh(

(*set s

( e r r n o = EINVAL; r e t u r n ( - l ) ;

(1 «

(signo - 1 ) ) )

}

!= 0

Próg. 10.9 Implementacja funkcji s i g a d d s e t , s i g d e l s e t i sigismember

Jeśli argument set jest wskaźnikiem pustym, to maska sygnałów procesuj nie ulega zmianie, a wartość argumentu how nie ma znaczenia. Gdy po wywołaniu funkcji sigprocmask istnieją jakieś sygnały, które, są odblokowane i oczekują na dostarczenie, wówczas przed powrotem z tej funkcji do procesu jest przekazany co najmniej jeden taki sygnał.

54

10. Sygnały

rzykład W programie 10.10 pokazujemy funkcję drukującą nazwy sygnałów umieszczonych w masce sygnałów wywołującego ją procesu. Wywołujemy ją z programów 10.14 oraz 10.15. Aby kod programu był krótszy, nie sprawdzamy, czy w masce znajduje się każdy sygnał wymieniony w tab. 10.1. (Zobacz ćw. 10.9). • include include include

"ourhdr.h"

"ourhdr.h"

#include #include

int main(void)

r mask(const char *str)

int

kę sygnałów (by odtworzyć ją później), a następnie usypia na 5 sekund. Każde wystąpienie sygnału zakończenia w tym czasie jest blokowane i nie będzie dostarczone, dopóki sygnał nie zostanie odblokowany. Po zakończeniu 5-sekundowej drzemki sprawdzamy, czy są sygnały oczekujące na dostarczenie, i odblokowujemy sygnał.

static void sig_quit(int);

oid

sigset t

355

10.13. Funkcja s i g p e n d i n g

sigset; errno save;

errno_save = errno; /* może być wywołany z procedur obsługi sygnału */ if (sigprocmask(0, NULL, Ssigset) < 0) err_sys("sigprocmask error"); printf("%s", str); if (sigismember(Ssigset SIGINT)) printf("SIGINT ") ; if (sigismember(Ssigset SIGQUIT)) printf("SIGQUIT "i if (sigismember(ssigset SIGOSRl)) printf("SIGUSR1 " ; if (sigismember(Ssigset SIGALRM)) printf("SIGALRM "; /* tu można umieścić pozostałe sygnały rintf ("\n") ; :rno = errno save;

Prog. 10.10 Wydruk maski sygnałów procesu

0.13 Funkcja sigpending Funkcja s i g p e n d i n g przekazuje zbiór sygnałów zawierający te wszystkie sygnały, które oczekują na dostarczenie do procesu, ponieważ zostały wcześniej zablokowane. Zbiór sygnałów odbieramy przez argument set. #include int sigpending(sigset _t *set) ; Przekazuje: 0, jeśli wszystko w porządku; - Jeśli wystąpił błąd

zykład W programie 10.11 widzimy wiele tych własności sygnałów, które dotychczas opisaliśmy. Proces blokuje sygnał SIGQUIT, zapamiętując aktualną mas-

sigset_t

newmask, oldmask, pendmask;

if (signal(SIGQUIT, sig_quit) == SIG_ERR) err_sys("can't catch SIGQUIT"); sigemptyset(snewmask); sigaddset(Snewmask, SIGQOIT); /* blokujemy SIGQUIT i zapamiętujemy bieżącą maskę sygnałów */ if (sigprocmask(SIG_BLOCK, snewmask, soldmask) < 0) err_sys("SIG_BLOCK error"); sleep(5);

/* przechwycony SIGQUIT pozostaje nie załatwiony */

if (sigpending(spendmask) < 0) err_sys("sigpending error"); if (sigismember (Spendmask, SIGO.UIT) ) printf("\nSIGQUIT pending\n"); /* odtwarzamy maskę sygnałów, co odblokowuje SIGQUIT */ if (sigprocmask(SIG_SETMASK, Soldmask, NULL) < 0) err_sys("SIG_SETMASK error") ; printf("SIGQUIT unblocked\n"); sleep(5); /* przechwycony SIGQUIT zakończy pracę, tworząc obraz pamięci */ exit(0);

static void sig_quit(int signo) { printf("caught SIGQUIT\n"); if (signal (SIGO.UIT, SIG_DFL) == SIG_ERR) err_sys("can't reset SIGQUIT"); return;

Prog. 10.11 Przykład zbiorów sygnałów i użycia funkcji s i g p r o c m a s k

pcja

POSDC.l

SVR4

4.3+BSD Opis

A NOCLDSTOP

Jeżeli argument signo jest równy SIGCHLD, to taki sygnał nie jest generowany, gdy proces potomny zatrzymuje się (sterowanie zadaniami). Ten sygnał jest nadal generowany, gdy potomek kończy pracę (zob. poniżej opcję SA_NOCLDWAIT w systemie SVR4).

A RESTART

Wywołania systemowe przerwane przez ten sygnał są automatycznie powtarzane. (Podrozdz. 10.5).

A ONSTACK

A NOCLDWAIT

>A NODEFER

iA RESETHAND

SIGINFO

Jeżeli zadeklarowano dodatkowy stos za pomocą funkcji s i g a l t s t a c k ( 2 ) , to ten sygnał jest dostarczany do procesu za pośrednictwem dodatkowego stosu. Jeżeli argument signo jest równy SIGCHLD, to ta opcja sprawia, że nie są tworzone procesy zombie, gdy kończą się procesy potomne wywołującego procesu. Jeśli proces wywołuje następnie funkcję wait, to ulega zablokowaniu do czasu, gdy wszystkie jego procesy potomne zakończą pracę, a następnie powraca z wartością-1 i zmienną e r r n o równą ECHILD. (Podrozdz. 10.7). Taki sygnał po przechwyceniu nie zostanie automatycznie zablokowany przez system w czasie wykonania procedury obsługi sygnału. Ten typ działania jest zgodny z funkcjonowaniem wcześniejszych, zawodnych sygnałów. Przy wejściu do procedury obsługi sygnału jest przywracana domyślna (SIG_DFL) dyspozycja dla tego sygnału. Ten typ działania jest zgodny z funkcjonowaniem wcześniejszych, zawodnych sygnałów. Ta opcja umożliwia dostarczanie dodatkowych informacji do procedury obsługi sygnału. Więcej szczegółów można znaleźć w podrozdz. 10.21.

359

10.14. Funkcja s i g a c t i o n

Przykład - funkcja s i g n a l Użyjemy teraz funkcji s i g a c t i o n do zaimplementowania funkcji s i g n a l . Takie podejście stosuje system 4.3+BSD (zgodnie z uwagą zamieszczoną w uzasadnieniach normy POSDC.l, taka była intencja tego standardu). System SVR4 stosuje funkcję s i g n a l , która korzysta ze starej semantyki zawodnych sygnałów. Dlatego w tym systemie, jeśli nie jest nam potrzebny stary model sygnałów (do uzyskania zgodności wstecz), powinniśmy używać poniższej implementacji funkcji s i g n a l lub wywoływać bezpośrednio funkcję s i g a c t i o n . (Jak możemy się domyślać, implementacja tradycyjnej funkcji s i g n a l o starej semantyce w systemie SVR4 wywołuje funkcję s i g a c t i o n z sygnalizatorami SA_RESETHAND oraz SA_NODEFER). Wszystkie przykłady w tej książce, które wywołują funkcję s i g n a l , w rzeczywistości wywołują funkcję z próg. 10.12. /* Niezawodna wersja funkcji signalO, używająca sigaction() z normy POSIX. */ #include #include

"ourhdr.h"

Sigfunc * signal(int signo, Sigfunc *func) { struct sigaction act, oact; act.sa_handler = func; sigemptyset(Sact.sa_mask) ; act.sa_flags = 0; if (signo == SIGALRM) { #ifdef SA_INTERRUPT a c t . s a _ f l a g s |= SA_INTERRUPT; #endif } else { #ifdef SA_RESTART a c t . s a _ f l a g s |= SA_RESTART; #endif

/* SunOS */

/* SVR4, 4.3+BSD */

if ( s i g a c t i o n ( s i g n o , &act, &oact) < 0) return(SIG_ERR); return(oact.sa handler);

Próg. 10.12 Implementacja funkcji s i g n a l przy użyciu funkcji s i g a c t i o n

Zwróćmy uwagę, że aby zainicjować pole sa mask, wywołaliśmy funkii cję s i g e m p t y s e t . Nie ma pewności, że instrukcja ^ act.sa_mask = 0;

zrobi to samo.

:

10. Sygnały Celowo próbujemy ustawić sygnalizator SARESTART dla wszystkich sygnałów różnych od SIGALRM, by każde przerwane wywołanie systemowe, spowodowane tymi sygnałami, było automatycznie powtarzane. Nie chcemy tak obsługiwać sygnału SIGALRM, ponieważ służy on do ustalenia czasu oczekiwania na zakończenie operacji wejścia-wyjścia. (Przypominamy naszą dyskusję o próg. 10.7). Pewne systemy (jak np. SunOS) definiują sygnalizator SA_INTERRUPT. Takie systemy domyślnie powodują restart przerwanych wywołań systemowych, a wskazanie tego sygnalizatora umożliwia przerwanie wywołania systemowego bez jego powtórzenia. •

ykład - funkcja s i g n a l _ i n t r

10.15. Funkcje s i g s e t jmp oraz siglongjmp

programu zamiast zwykłego powrotu po zakończeniu obsługi. Faktycznie, standard ANSI C stwierdza, że procedura obsługi sygnału może albo powrócić ( r e t u r n ) , albo wywołać takie funkcje jak: a b o r t , e x i t czy longjmp. Widzieliśmy to w programach 10.5 i 10.8. Niestety, podczas wywołania funkcji longjmp pojawia się problem. Gdy zostanie przechwycony sygnał, sterowanie programu przechodzi do instrukcji funkcji obsługującej ten sygnał, ale przed wykonaniem pierwszej instrukcji do maski sygnałów procesu jest automatycznie dodawany bieżący sygnał. Dzięki temu mamy pewność, że kolejne wystąpienie takiego samego sygnału nie przerwie procedury obsługi. Jeśli wywołamy funkcję longjmp, by opuścić procedurę obsługi sygnału, to co stanie się z maską sygnałów naszego procesu? W systemie 4.3+BSD funkcje setjmp i longjmp zapamiętują i odtwarzają maskę sygnałów. Jednak w systemie SVR4 nie dzieje się tak. W systemie 4.3+BSD są specjalne funkcje _setjmp i _longjmp, które nie zapamiętują i nie odtwarzają maski sygnałów.

Program 10.13 jest kolejną wersją funkcji s i g n a l , która zabrania wznawiania przerwanych wywołań systemowych. clude clude

"ourhdr.h"

Eunc * ial__intr (int signo, Sigfunc *func) struct sigaction

Aby dopuścić każdą formę działania, norma POSDC.l nie definiuje wpływu funkcji setjmp i longjmp na maski sygnałów. Zamiast tego definiuje dwie nowe funkcje, s i g s e t jmp oraz siglongjmp, używane, gdy zachodzi potrzeba wykonania skoku z procedury obsługi sygnału.

act, oact;

act.sa_handler = func; sigemptyset(Sact.sa_mask); act.sa_flags = 0; ief SA_INTERRUPT /* SunOS */ a c t . s a _ f l a g s 1= SA_INTERRUPT; iif if (sigaction(signo, sact, Soact) < 0) return(SIG_ERR); return(oact.sa handler);

Próg. 10.13 Funkcja signal__intr

Aby nie dopuścić do wznowienia przerwanego wywołania systemowego, używamy sygnalizatora SA_INTERRUPT, jeśli jest on zdefiniowany w danym systemie. •

15 Funkcje sjjgsetjmp oraz siglongjmp

f

361

W podrozdziale 7.10 omówiliśmy funkcje setjmp oraz longjmp, które są używane do wykonania nielokalnego skoku. Funkcja longjmp jest często wywoływana z procedury obsługi sygnału, w celu powrotu do głównej pętli

#include i n t s i g s e t jmp (sigjmp_buf env, i n t savemask) ;

Przekazuje: 0, jeśli wywołanie bezpośrednie; wartość niezerową, jeśli powrót z funkcji siglongjmp

void siglongjmp (sigjmp_buf env, i n t val) ;

Jedyną różnicą między tymi funkcjami a funkcjami setjmp i longjmp jest dodatkowy argument w funkcji s i g s e t jmp. Jeśli argument scnemask jest niezerowy, to funkcja s i g s e t j m p zapamiętuje dodatkowo w zmiennej env bieżącą maskę sygnałów procesu. Gdy jest wywoływana funkcja s i g l o n g jmp, a argument env został zapamiętany przez wywołanie funkcji s i g s e t j m p . z niezerowym argumentem savemask, to funkcja siglongjmp odtwarza ma-, skę sygnałów.

Przykład W programie 10.14 widzimy, jak jest realizowane dołączenie do maski sygnałów przechwyconego sygnału, jeśli maska została ustalona automatycznie po wywołaniu procedury sygnału. Przykład ten ilustruje też użycie funkcji s i g s e t j m p oraz siglongjmp.

10. Sygnały :lude :lude :lude :lude



"ourhdr.h"

:ic void :ic sigjmp_buf ;ic volatile sig_atomic_t

sig_usrl(int), sig_alrm(int), jmpbuf; canjump;

i(void) if (signal(SIGOSR1, sig_usrl) == SIG_ERR) err_sys("signal(SIGUSR1) error"); if (signal(SIGALRM, sig_alnn) == SIG_ERR) err_sys("signal(SIGALRM) error"); pr_mask("starting main: " ) ; /* {Program 10.10} */ if (sigsetjmp(jmpbuf, 1)) { pr mask("ending main: " ) ; exit(0); canjump = 1;

/* teraz jest już po wywołaniu sigsetjmpO */

for ( ; ; ) pause(); :ic void _usrl(int signo) time t starttime; if (canjump == 0) return;

/* nieoczekiwany sygnał, ignorujemy */

pr_mask("starting sig_usrl: " ) ; alarm(3);

/* SIGALRM za 3 sekundy */

starttime = time(NULL); for ( ; ; ) /* aktywne oczekiwanie przez 5 sekund */ if (time(NULL) > starttime + 5) break; pr_mask("finishing sig_usrl: " ) ; canjump = 0; siglongjmp(jmpbuf, 1);

/* powrót do funkcji main, a nie zwykła instrukcja return */

:ic void _alrm(int signo) pr__mask("in aj?g_alrm: " ) ; return; f Próg. 10.14

Przykład masek sygnałów, funkcji s i g s e t jmp i s i g l o n g j m p

363

10.15. Funkcje s i g s e t jmp oraz siglongjmp

W programie widzimy kolejną technikę, która powinna być używana przy każdym wywołaniu funkcji siglongjmp z procedury obsługi sygnału. Zmienna canjump uzyskuje wartość niezerową, tylko gdy wywołaliśmy już funkcję s i g s e t jmp. Tę zmienną sprawdza procedura obsługi sygnału i wywołuje funkcję siglongjmp pod warunkiem, że jej wartość jest niezerową. W ten sposób uzyskujemy zabezpieczenie przed próbą wykonania procedury obsługi przed lub po wyznaczonym czasie zaznaczonym zainicjowaniem bufora skoku przez funkcję s i g s e t jmp. (W naszym bardzo prostym przykładzie kończymy pracę krótko po wywołaniu funkcji siglongjmp, ale w bardziej zawiłych programach procedura obsługi sygnału może obowiązywać jeszcze długo po wywołaniu funkcji siglongjmp). Dodanie tego typu ochrony zazwyczaj nie jest potrzebne, gdy używamy funkcji longjmp w zwykłym kodzie języka C (a nie w procedurze obsługi sygnału). Ponieważ sygnał może wystąpić w dowolnym czasie, zatem jest potrzebne dodatkowe zabezpieczenie. W tym programie używamy typu danych o nazwie sig_atomic_t, który jest zdefiniowany w standardzie ANSI C jako typ zmiennej, którą można aktualizować bez niebezpieczeństwa przerwania operacji. Oznacza to, że taka zmienna w systemie używającym pamięci wirtualnej nie może rozciągać się między granicami strony, a dostęp do niej jest zrealizowany za pomocą jednej instrukcji maszynowej. Zawsze do deklaracji zmiennej tego typu dołączamy kwalifikator v o l a t i l e , ponieważ korzystają z niej jednocześnie dwa wątki sterowania - funkcja main oraz wykonywana asynchronicznie procedura obsługi sygnału. Na rysunku 10.1 pokazujemy wykres naszego programu na osi czasu. Możemy ten rysunek podzielić na trzy części: lewą (związaną z funkcją main), środkową (odpowiadającą funkcji s i g _ u s r l ) oraz prawą (odpowiadającą funkcji sig_alrm). Gdy proces wykonuje instrukcję części lewej, wówczas maska sygnałów jest równa 0 (żaden sygnał nie jest zablokowany). W czasie realizacji części środkowej maska sygnałów zawiera sygnał SIGUSR1. Podczas wykonania części prawej maska sygnałów ma wartość SIGUSRl | SIGALRM. Przyjrzyjmy się wydrukowi uzyskanemu po uruchomieniu próg. 10.14. $ a.out & starting main: [1] 531

start procesu w tle powłoka realizująca sterowanie zadaniami drukuje identyfikator procesu wysianie do procesu sygnału SIGUSRl

$ k i l l -USR1 531 s t a r t i n g sig_usrl: SIGUSRl $ in sig_alrm: SIGUSRl SIGALRM finishing sig_usrl: SIGUSRl ending main: nacikamy klawisz RETURN [1 ] + Done a.out S

10. Sygnały

10.16. Funkcja sigsuspend

s i s e t _ t newmask, oldmask; sigemptyset(snewmask); sigaddset(snewmask, SIGINT); /* zablokowanie sygnału SIGINT oraz zapamiętanie starej maski */ if (sigprocmask(SIG_BLOCK, snewmask, soldmask) < 0) err_sys("SIG_BLOCK e r r o r " ) ;

main signal() signal() pr_mask() sigsetjmpO pause () dostarczony sygnał SIGUSR1

sig_usrl

/* obszar krytyczny kodu */

pr mask() alarm() time () time () time ()

/* przywrócenie s t a r e j maski sygnałów - odblokowanie sygnału SIGINT */ if (sigprocmask(SIG_SETMASK, Soldmask, NULL) < 0) err_sys("SIG_SETMASK e r r o r " ) ;

dostarczony sygnał -Jsig_alrm SIGALRM pr mask() powrót z procedury return() obsługi sygnału sigsetjmp () pr mask() exit()

pause ( ) ;

/* oczekiwanie na sygnał */

/* kontynuacja przetwarzania */

pr mask() -siglbngjmp()

Rys. 10.1 Wykres czasowy dla przykładu programu obsługującego dwa sygnały Jest on zgodny z oczekiwaniami: podczas wywoływania procedury obsługi sygnału przechwycony sygnał jest dodawany do bieżącej maski sygnałów procesu. Oryginalna maska sygnałów jest odtwarzana podczas powrotu z procedury obsługi. Również funkcja siglongjmp odtwarza maskę sygnałów zapamiętaną wcześniej przez funkcję sigsetjmp. Gdy zmienimy próg. 10.14, tak by zamiast funkcji s i g s e t j m p i s i g longjmp wywoływał funkcje _setjmp i _longjmp, wówczas w systemie 4.3+BSD ostatni wiersz wydruku będzie miał postać ending main: SIGUSR1 Oznacza to, że po wywołaniu funkcji _setjmp funkcja main wykonywała się z zablokowanym sygnałem SIGUSR1. Nie było to prawdobodobnie naszym zamierzeniem.

365



16 Funkcja sigsuspend Zobaczyliśmy już, jak można zmienić maskę sygnałów procesu, aby zablokować lub odblokować wybrane sygnały. Taka technika pozwala nam chronić w kodzie Jfogramu obszary krytyczne, których wykonanie nie powinno zostać przeifwane przez sygnał. Co zrobić, gdy chcemy odblokować sygnał, anastępnfe wywołać funkcję pause w celu oczekiwania na pojawienie się wcześniej zablokowanego sygnału? Załóżmy, że chodzi o sygnał SIGINT. Najpierw pokazujemy niepoprawne podejście.

W tym fragmencie kodu pojawia się problem, jeśli sygnał wystąpi między odblokowaniem a wywołaniem funkcji pause. Każde wystąpienie sygnału w tym przedziale czasu zaniknie. Takie niebezpieczeństwo dotyczy starszych zawodnych sygnałów. Aby zaradzić opisanemu problemowi, musimy mieć możliwość odtworzenia wartości maski sygnałów oraz wprowadzenia procesu w stan uśpienia w jednej operacji atomowej. Taką właściwość ma funkcja sigsuspend. łtinclude int sigsuspend (const sigset_t *sigmask) ; Przekazuje: -1 i zmienną errno równą EINTR Maska sygnałów procesu jest ustawiana zgodnie z wartością wskazywaną przez argument sigmask. Jednocześnie jest wstrzymywana realizacja procesu do czasu pojawienia się danego sygnału albo zakończenia całego procesu przez inny sygnał. Jeśli proces przechwyci sygnał, a następnie procedura obsługi sygnału powróci po zakończeniu przetwarzania, to dopiero wówczas skończy się wywołanie funkcji sigsuspend, a maska sygnałów procesu powróci do wartości sprzed wywołania tej funkcji. Zwróćmy uwagę, że nie ma poprawnego powrotu z tej funkcji. Jeśli funkcja powraca, to wywołujący zawsze otrzymuje wartość - 1 , a zmienna e r r n o jest równa EINTR (tj. oznacza przerwaną funkcję systemową).

Przykład W programie 10.15 pokazujemy poprawny sposób ochrony krytycznego obszaru kodu przed konkretnym sygnałem. Zauważmy, że funkcja s i g s u s p e n d po powrocie ustala maskę sygnałów zgodnie z jej wartością sprzed wywołania. W tym przypadku blokujemy sygnał SIGINT. Następnie maska sygnałów uzyskuje poprzednio zapamiętaną wartość (oldmask).

566

10. Sygnały

finclude łinclude

"ourhdr.h"

static void sig_int(int); Lnt nain(void)

Przykład

Innym zastosowaniem funkcji sigsuspend jest oczekiwanie, by procedur; obsługi sygnału ustawiła pewną zmienną globalną. W programie 10.16 prze#include #include

sigset_t

newmask, oldmask, zeromask;

if (signal(SIGINT, sig_int) == SIG_ERR) err_sys("signal(SIGINT) error") ; sigemptyset(szeromask); sigemptyset(snewmask); sigaddset(snewmask, SIGINT); /* blokujemy SIGINT i zapamiętujemy bieżącą maskę sygnałów */ if (sigprocmask(SIG_BLOCK, snewmask, soldmask) < 0) err_sys("SIG_BLOCK error"); /* krytyczny obszar kodu */ pr_mask("in critical region: " ) ; /* zezwalalmy na wszystkie sygnały i robimy przerwę */ if (sigsuspendt&zeromask) != -1) err_sys("sigsuspend error"); pr_mask("after return from sigsuspend: " ) ; /* odtwarzamy maskę sygnałów, co odblokowuje SIGINT */ if (sigprocmask(SIG_SETMASK, Soldmask, NULL) < 0) err_sys("SIG_SETMASK error"); /* i kontynuujemy przetwarzanie exit (0); static void sig_int(int signo) pr_mask("\nin s i g _ i n t : " ) ; return;

Próg. 10.15 Ochrona obszaru krytycznego przed sygnałem

Widzimy, że w czasie powrotu z funkcji sigsuspend jest odtwarzana wartość maski sygnałów sprzed wywołania. •

"ourhdr.h"

volatile sig_atomic_t ąuitflag;

/* otrzymuje wartość niezerową w procedurze obsługi sygnału */

int main(void) void sigset_t

sig_int(int); newmask, oldmask, zeromask;

if (signal(SIGINT, sig_int) == SIG_ERR) err_sys("signal(SIGINT) error"); if (signal(SIGQOIT, sig_int) == SIG_ERR) err_sys("signal(SIGQUIT) error") ; sigemptyset(Szeromask);

sigemptyset(Snewmask) ; sigaddset(Snewmask, SIGQUIT); /* blokujemy SIGQUIT i zapamiętujemy bieżącą maskę sygnałów */ if (sigprocmask(SIG_BLOCK, snewmask, soldmask) < 0) err_sys("SIG_BLOCK error"); while (ąuitflag == 0) sigsuspend(Szeromask) ; /* przechwycono SIGQUIT, który jest teraz zablokowany */ ąuitflag = 0; /* odtwarzamy maskę sygnałów, co odblokowuje SIGQOIT */ if (sigprocmask(SIG_SETMASK, soldmask, NULL) < 0) err_sys("SIG_SETMASK error"); exit(0); void sig_int(int signo)

Uruchomienie próg. 10.15 daje poniższy wydruk. $ a.out in c r i t i c a l region: SIGINT A ?« wprowadzenie znaku przerwania iiijfsig_int: SIGINT ajrter return from sigsuspend: SIGINT

36'

10.16. Funkcja s i g s u s p e n d

. _ ! /* jedna procedura obsługi sygnału dla SIGINT oraz . i SIGQUIT */

if (signo == SIGINT) printf("\ninterrupt\n") ; i else if (signo == SIGQUIT) j ąuitflag = 1; /* ustawiamy sygnalizator używany w pętli głównej */ ' return; ^

Próg. 10.16

Użycie funkcji s i g s u s p e n d do oczekiwania na ustawienie zmiennej globalnej

10. Sygnały chwytujemy dwa sygnały, przerwania oraz zakończenia, ale budzimy oczekującą pętlę główną, tylko gdy przejmiemy sygnał zakończenia. Oto przykładowy wydruk uzyskany z tego programu. $ a.out ^? interrupt A

ponowne wprowadzenie znaku przerwania

interrupt

i następne

A ip

interrupt -\ $

zakończenie za pomocą specjalnego znaku zakończenia



W celu uzyskania przenośności między systemami nie stosującymi normy POSIX, ale zgodnymi z ANSI C, a systemami posiksowymi wystarczy w procedurze obsługi sygnału przypisać wartość zmiennej typu sig_atomic_t. Norma POSDC.l idzie dalej i podaje listę funkcji, których wywoływanie z procedury obsługi sygnału jest w pełni bezpieczne (tab. 10.3), ale jeśli skorzystamy z tej możliwości, to nasz program może nie działać poprawnie w systemach niezgodnych ze standardem POSIX.

jykład W kolejnym przykładzie obrazującym zastosowanie sygnałów pokazujemy, jak można używać sygnałów do synchronizowania procesów macierzystego i potomnego. Program 10.17 implementuje pięć procedur: TELL_WAIT, TELL_PARENT, TELL_CHILD, WAIT_PARENT oraz WAIT_CHILD, które StOsowaliśmy w podrozdz. 8.8. Używamy dwóch sygnałów definiowanych przez użytkownika: proces macierzysty wysyła do swojego potomka sygnał SIGUSR1, a proces potomny do procesu macierzystego - sygnał SIGUSR2. W programie 14.3 pokażemy kolejną implementację tych pięciu funkcji, korzystającą z łączy komunikacyjnych. • nclude iclude

"ourhdr.h"

itic volatile sig_atomic__t sigflag; /* otrzymuje wartość niezerową w procedurze obsługi sygnału */ atic sigset_t newmask, oldmask, zeromask; atic void g_usr(int signo)

sigflag = 1; return; m

id L WAIT(void)

/* jedna procedura obsługi sygnału dla SIGUSR1 oraz SIGUSR2 */

369

if (signal(SIGUSR1, sig_usr) err_sys("signal(SIGUSR1) if (signal(SIGUSR2, sig_usr) err_sys("signal(SIGUSR2)

== SIG_ERR) error"); == SIG_ERR) error");

sigemptyset(szeromask);

wprowadzenie znaku przerwania

rozdziałach przygotujemy dwa programy demonstrujące działanie terminalowego wejścia-wyjścia: jeden komunikuje się z drukarką postscriptową (rozdz. 17), drugi umożliwia kontaktowanie się z modemem oraz logowanie na zdalnej stacji (rozdz. 18).

Program obsługi terminalu przekazuje w odpowiedzi na jedno żądanie odczytu najwyżej jeden wiersz danych. 2. Przetwarzanie danych wejściowych w trybie niekanonicznym. Wprowadzane znaki nie tworzą wierszy. Jeśli nie robimy nic specjalnego, to wystarcza domyślny tryb kanoniczny. Na przykład, jeżeli powłoka przekierowuje standardowe wejście na terminal i za pomocą funkcji read i w r i t e kopiujemy standardowe wejście na standardowe wyjście, to terminal pracuje w trybie kanonicznym i każda funkcja read przekazuje najwyżej jeden wiersz danych. Programy, które manipulują całym ekranem, jak np. edytor vi, używają trybu niekanonicznego, ponieważ polecenia mogą składać się z jednego znaku i nie są zakończone znakiem nowego wiersza. Oprócz tego, w przypadku tego edytora nie ma sensu, by system przetwarzał znaki specjalne, gdyż niektóre z nich mogą pokrywać się z poleceniami edytora. Na przykład znak Control-D zazwyczaj oznacza przy obsłudze terminalu koniec pliku, a dla edytora vi jest poleceniem przewinięcia ekranu w dół o pół ekranu. W programach obsługi terminalu w wersji 7 oraz systemach berkelejowskich są możliwe trzy tryby wprowadzania danych z terminalu: (a) tryb przetworzony (dane wejściowe są gromadzone w wiersze, znaki specjalne są interpretowane), (b) tryb surowy (dane wejściowe są gromadzone w wiersze, znaki specjalne nie są interpretowane) i (c) tryb cbreak (dane wejściowe nie są gromadzone w wiersze, ale pewne znaki specjalne są interpretowane). Program 11.10 zawiera posiksową funkcję, która ustala dla terminalu tryb surowy lub cbreak.

POSDC.l definiuje 11 specjalnych znaków wejściowych, z czego 9 może być modyfikowanych. Z niektórych z nich korzystaliśmy już w tekście książki: np. ze znaku końca pliku (na ogół Control-D), znaku zawieszenia (na ogół Control-Z). W podrozdziale 11.3 opiszemy wszystkie te znaki. Urządzenie terminalowe jest kontrolowane przez program obsługi terminalu zlokalizowany w jądrze. Każde urządzenie terminalowe ma kolejkę wejściową oraz kolejkę wyjściową, pokazuje to rys. 11.1.

kolejka wyjściowa

.2 Informacje podstawowe Są dwa raźne tryby terminalowego wejścia-wyjścia: 1. Przetwarzanie danych wejściowych w trybie kanonicznym. W tym trybie dane wprowadzane z terminalu są przetwarzane wierszami.

następny znak odczytany przez proces,,

następny znak zapisany przez proces

jeśli włączone echo

kolejka wejściowa

-MAX następny znak transmitowany do urządzenia Rys. 11.1

INPUT-

następny znak czytany z urządzenia

Logiczna reprezentacja kolejek wejściowej i wyjściowej urządzenia terminalowego

'

11. Terminalowe wejście-wyjście

Musimy zwrócić uwagę na parę elementów tego rysunku. • Jeśli jest włączona opcja echa (potwierdzania na ekranie znaków wprowadzonych z klawiatury), to między kolejkami wejściową a wyjściową może istnieć połączenie. • Rozmiar kolejki wejściowej, MAX_INPUT (zob. tab. 2.5), może być ograniczony. Reakcja systemu na przepełnienie kolejki wejściowej konkretnego urządzenia zależy od implementacji. W większości systemów jest generowany sygnał dźwiękowy (beli). • Istnieje dodatkowe ograniczenie, którego nie pokazaliśmy na rysunku, stała MAX_CANON. W trybie kanonicznym jest to maksymalna liczba bajtów w jednym wierszu wejściowym. • Chociaż kolejka wyjściowa ma zazwyczaj skończony rozmiar, nie ma żadnych stałych definiujących rozmiar takiej kolejki w programie. Jest tak dlatego, że po przepełnieniu kolejki system wymusza uśpienie procesu do czasu, gdy będzie dostępna wolna przestrzeń. • Zobaczymy, w jaki sposób funkcja t c f l u s h opróżnia kolejkę wejściową lub wyjściową. Podobnie, gdy będziemy omawiać funkcję t c s e t a t t r , dowiemy się, jak powiadomić system, by zmienił atrybuty terminalu, tylko gdy kolejka wyjściowa jest pusta. (Chcemy by tak się stało, gdy np. zmieniamy atrybuty wyjściowe). Możemy również wymusić, by system, zmieniając atrybuty terminalu, skasował wszystko, co znajduje się w kolejce wejściowej. (Chcemy, by tak się stało, gdy zmieniamy atrybuty wejściowe lub przechodzimy z trybu kanonicznego do niekanonicznego i obawiamy się, że wcześniej wprowadzone znaki zostaną błędnie zinterpretowane). Większość systemów uniksowych implementuje wszelkie przetwarzanie kanoniczne w module nazywanym dyscypliną linii terminalu {terminal linę discipline). Możemy wyobrazić sobie, że moduł taki jest umieszczony między jądrem systemu, w którym są obsługiwane funkcje read i w r i t e , a rzeczywistym programem obsługi urządzenia. Pokazuje to rys. 11.2. Powrócimy do tego rysunku w podrozdz. 12.4 podczas omawiania systemu strumieni wejścia-wyjścia oraz w rozdz. 19, gdy będziemy prezentować pseudoterminale. Cała charakterystyka urządzenia terminalowego, którą możemy analizować lub modyfikować jest zawarta w strukturze t e r m i o s . Jest ona zdefiniowana w pliku nagłówkowym , z którego ciągle korzystamy w tym rozdziale. struclą termios { tcfjag_t c_iflag; t c | i a g _ t c_oflag; t c f l a g _ t c_cflag; tcflag_t c_lflag; cc t c cctNCCS] ,

sygnalizatory danych wejściowych */ sygnalizatory danych wyjściowych */ sygnalizatory sterowania */ sygnalizatory lokalne */ znaki kontrolne */

391

11.2. Informacje podstawowe proces użytkowy

---1--funkcje odczytu i zapisu

dyscyplina linii terminalu

jądro

program obsługi urządzenia terminalowego

urządzenie rzeczywiste Rys. 11.2 Dyscyplina linii terminalu

Mówiąc ogólnie, sygnalizatory danych wejściowych kontrolują sposób wprowadzania znaków przez program obsługi terminalu (usunięcie ósmego bitu, włączenie kontroli parzystości itp.), sygnalizatory danych wyjściowych kontrolują sposób wyprowadzania znaków przez program obsługi terminalu (przetwarzanie danych wyjściowych, odwzorowanie znaku nowego wiersza na sekwencję znak powrotu karetki oraz znak nowego wiersza itp.), sygnalizatory sterowania mają wpływ na obsługę linii szeregowych RS-232 (ignorowanie linii stanu modemu, jeden lub dwa bity stopu na jeden znak itp.), a sygnalizatory lokalne decydują o interfejsie między programem obsługi a użytkownikiem (włączenie lub wyłączenie potwierdzania wprowadzonych znaków, obsługa znaków kasowania, włączenie sygnałów generowanych przez terminal, stosowanie przez sterowanie zadaniami sygnału zatrzymania do wypisywania w tle itp.). Typ t c f l a g _ t jest wystarczająco duży, by mógł przechować każdą wartość sygnalizatora. Często jest definiowany jako unsigned long. Tablicą c c c zawiera wszystkie znaki specjalne, które możemy zmieniać. Stała NCC$ jest liczbą elementów w tej tablicy i zazwyczaj ma wartość między 11 a 18 (ponieważ większość systemów uniksowych stosuje więcej znaków specjalnych niż jedenaście zdefiniowanych w normie POSIX.l). Typ cc_t jest wystarczająco duży, by przechowywał każdy specjalny znak i typowo definiuje się go jako unsigned char. Starsze wersje Systemu V miały plik nagłówkowy o nazwie oraz strukturę termio. Norma POSIX.l dodała literę s do obu nazw w celu odróżnienia od poprzedników.

11. Terminalowe wejście-wyjście W tabeli 11.1 są umieszczone wszystkie sygnalizatory terminalu, które możemy zmieniać, by zmodyfikować charakterystykę urządzenia terminalowego. Zwróćmy uwagę, że chociaż POSIX.l definiuje wspólny podzbiór sygnalizatorów dla systemów SVR4 oraz 4.3+BSD, to obie implementacje mają swoje własne rozszerzenia. Te dodatkowe ustalenia wynikają z historycznych różnic między obydwoma systemami. W podrozdziale 11.5 omawiamy po kolei każdą wartość sygnalizatora. Tabela 11.1 Sygnalizator

Opis

c iflag

BRKINT ICRNL IGNBRK

generuje SIGINT w odpowiedzi na BREAK



odwzorowuje na wejściu znak CR na NL ignoruje warunek BREAK

• •

IGNCR IGNPAR

ignoruje CR ignoruje znaki z błędami parzystości

• •

IMAXBEL

generuje sygnał akustyczny po przepełnieniu kolejki odwzorowuje na wejściu znak NL na CR

ISTRIP IUCLC IXANY IXOFF IXON PARMRK c__oflag

aktywuje kontrolowanie parzystości na wejściu obcina ósmy bit znaków wejściowych odwzorowuje na wejściu znaki pisane wielką literą na małe sprawia, że każdy znak wznawia wyprowadzanie danych włącza start/zatrzymanie sterowania przepływem na wejściu włącza start/zatrzymanie sterowania przepływem na wyjściu zaznacza błędy parzystości

POSDC.l

SVR4



ONOEOT OPOST OXTABS



4.3+BSD

c cflag



c_lflag •

• • •

FFDLY NLDLY OCRNL OFDEL

maska opóźnienia znaku wysuwu strony maska opóźnienia znaku nowego wiersza odwzorowuje na wyjściu znak CR na NL

• •

znakiem wypełnienia jest DEL, w przeciwnym razie NULL używanie znaku wypełnienia do opóźniania odwzorowuje na wejściu znaki pisane małą literą na wielkie odwzorowuje NL na sekwencję CR-NL (podobnie do CRMOD) NL pełni rolę CR bez wyprowadzania znaku CR w zerowej kolumnie

TABDLY VTDLY



maska opóźnienia znaku CR

ONLRET ONOCR

c oflag





ONJCR

Sygnalizator

• •

maska opóźnienia znaku wycofania

OLCUC

Pole

Rozszerzenie

BSDLY CRDLY

OFILL

Tabela 11.1 (cd.)

Sygnalizatory terminalu

Pole

INLCR INPCK

11.2. Informacje podstawowe

• •

>

• • • • • •



Opis

POSIX.l

kasowanie znaków EOT ( D) na wyjściu realizowanie przetwarzania danych wyjściowych wygenerowanie odstępów w miejscu znaków tabulacji maska opóźnienia znaku poziomej tabulacji maska opóźnienia znaku pionowej tabulacji

kanoniczna prezentacja małych/wielkich liter

4.3+BS! •

A

CCTS_OFLOW sterowanie przepływem CTS na wyjściu CIGNORE ignorowanie sygnalizatorów sterowania CLOCAL ignorowanie modemowych linii stanu CREAD aktywizacja odbioru CRTS_IFLOW sterowanie przepływem RTS na wejściu CSIZE maska rozmiaru znaku CSTOPB wysyłanie dwóch bitów zatrzymania, domyślnie jest wysyłany jeden bit HUPCL zawieszenie po ostatnim zamknięciu MDMBUF sterowanie przepływem za pomocą nośnej PARENB włączenie parzystości PARODD kontrola nieparzystości, domyślnie parzystości ALTWERASE stosowanie alternatywnego algorytmu WERASE ECHO włączenie echa ECHOCTL echo znaków sterujących w postaciA (znak) ECHOE wizualne usuwanie znaków ECHOK echo znaku zabicia ECHOKE wizualne usuwanie znaków zabicia ECHONL echo znaku NL ECHOPRT tryb wizualnego usuwania przy wydruku FLUSHO opróżnienie strumienia wyjściowego ICANON tryb kanoniczny na wejściu IEXTEN włączenie rozszerzonego przetwarzania znaków wejściowych ISIG włączenie sygnałów generowanych przez terminal NOFLSH wyłączenie opróżniania po przerwaniu lub zakończeniu NOKERNINFO bez wyprowadzania danych przez jądro systemu w odpowiedzi na STATUS PENDIN powtórne wprowadzenie zaległych danych wejściowych TOSTOP przesyłanie sygnału SIGTTOU przy wyprowadzaniu danych w tle XCASE

Rozszerzenie SVR4

• • • • • • • • • • • • • • • * • •







• •

• •

• • •

• * * *

i

* •

11. Terminalowe wejście-wyjście Tabela 11.2 Zestawienie terminalowych funkcji wejścia-wyjścia w normie POSDC.l Funkcja

Opis

tcgetattr

pobranie atrybutów (struktura t e r m i o s )

tcsetattr

ustawienie atrybutów (struktura t e r m i o s )

cfgetispeed

pobranie szybkości wejściowej

cfgetospeed

pobranie szybkości wyjściowej

cfsetispeed

ustalenie szybkości wejściowej

cfsetospeed

ustalenie szybkości wyjściowej

tcdrain

oczekiwanie na przetransmitowanie wszystkich danych

tcfIow

zawieszenie transmisji lub odbioru

tcflush

opróżnienie zaległego wejścia lub/i wyjścia

tcsendbreak

przesłanie znaku BREAK

tcgetpgrp

pobranie identyfikatora pierwszoplanowej grupy procesów

tcsetpgrp

ustalenie identyfikatora pierwszoplanowej grupy procesów

i

T)

a) a, m

a w

•o '

1

0)

rzystości na wejściu" to dwa odrębne zagadnienia. O generowaniu i wykryciu parzystości decyduje sygnalizator PARENB. Ustawienie tego sygnalizatora zazwyczaj powoduje, że program obsługi interfejsu szeregowego generuje bit parzystości dla wyprowadzanych znaków oraz sprawdza bit parzystości w danych wejściowych. Sygnalizator PARODD określa, czy kontrolujemy parzystość, czy nieparzystość. Jeśli nadchodzi znak wejściowy o złej parzystości, to jest sprawdzany stan sygnalizatora INPCK. Jeśli jest on włączony, to jest sprawdzany stan sygnalizatora IGNPAR (aby dowiedzieć się, czy

11.5. Sygnalizatory opcji terminalu

ISIG

ISTRIP

IUCLC IXANY IXOFF

IXON

MDMBUF

407

bajt wejściowy o niepoprawnej parzystości ma być zignorowany); jeśli bajt nie ma być ignorowany, to stan sygnalizatora PARMRK decyduje, czy znak będzie przekazany do procesu czytającego dane. ( c _ l f l a g , POSIX.l) Jego ustawienie powoduje, że jest sprawdzany każdy znak wejściowy i jeśli dany znak jest znakiem specjalnym, generującym sygnał terminalowy (INTR, QUIT, SUSP lub DSUSP), to jest generowany odpowiedni sygnał. ( c _ i f l a g , POSIX.l) Jego ustawienie powoduje, że poprawne bajty wejściowe są obcinane do 7 bitów. Gdy ten sygnalizator nie jest ustawiony, to jest przetwarzane osiem bitów. ( c _ i f l a g , SVR4) Odwzorowanie wielkich liter na wejściu na małe. ( c _ i f l a g , SVR4 i 4.3+BSD) Każdy znak może wznowić wyprowadzanie danych. ( c _ i f l a g , POSIX.l) Jeśli jest ustawiony, to obowiązuje kontrolowanie wprowadzania danych za pomocą znaków START i STOP. Program obsługi terminalu po stwierdzeniu, że kolejka wejściowa przepełnia się, wysyła znak STOP. Urządzenie wysyłające dane powinno rozpoznać ten znak i zatrzymać pracę urządzenia. Później, po przetworzeniu znaków z bufora wejściowego podsystem obsługi terminalu wyprowadza znak START. Po jego odbiorze urządzenie powinno wznowić pracę. ( c _ i f l a g , POSIX.l) Jeśli jest ustawiony, to obowiązuje kontrolowanie wprowadzania danych za pomocą znaków START i STOP. Program obsługi terminalu po odbiorze znaku STOP zatrzymuje wyprowadzanie danych. Odbiór znaku START wznawia wyprowadzanie danych. Jeśli ten sygnalizator nie jest ustawiony, proces odczytuje znaki START i STOP jak zwykłe znaki.

( c _ c f l a g , 4.3+BSD) Kontrola sterowania wyjścia zgodna z sygnalizatorem nośnej modemu. NLDLY ( c _ o f l a g , SVR4) Maska opóźnienia znaku nowego wiersza,] Wartości maski to NLO oraz NL1. j NOFLSH ( c _ l f l a g , POSDC.l) Domyślnie, gdy podsystem obsługi ter-, minalu generuje sygnały S I G I N T i SIGQUIT, wówczas kolej-ki: wejściowa i wyjściowa są opróżniane. G d y generuje s y g n a ł SIGSUSP, to jest opróżniana kolejka wejściowa. Jeśli jest ustawiony sygnalizator N O F L S H , to nie jest realizowane typowej opróżniane kolejek po odbiorze sygnałów. I NOKERNINFO ( c _ l f l a g , 4.3+BSD) Jego ustawienie powoduje, że znak' S T A T U S nie wymusza wydruku informacji na temat pierw-. szoplanowej grupy procesów. Niezależnie od ustawienia tego sygnalizatora, wynikiem wprowadzenia znaku S T A T U S jest,

11. Terminalowe wejście-wyjście

OCRNL OFDEL

OFILL

OLCUC ONLCR

ONLRET ONOCR

ONOEOT

OPOST

OXTABS

PARENB

PARMRK

wygenerowanie sygnału SIGINFO wysyłanego do pierwszoplanowej grupy procesów. ( c _ o f l a g , SVR4) Jeśli jest ustawiony, to znak CR jest odwzorowywany na znak NL. ( c _ o f l a g , SVR4) Jeśli jest ustawiony, to znakiem wypełnienia na wyjściu jest ASCII DEL, w przeciwnym razie ASCII NUL. Odsyłamy do opisu sygnalizatora OFILL. (c_of lag, SVR4) Jeśli jest ustawiony, to opóźnienie jest symulowane przez transmisję znaków wypełnienia (albo ASCII DEL, albo ASCII NUL, zob. opis sygnalizatora OFDEL) zamiast użycia zegarowego opóźnienia. Odsyłamy do opisu sześciu sygnalizatorów opóźnienia: BSDLY, CRDLY, FFDLY, NLDLY, TABDLY i VTDLY. ( c _ o f l a g , SVR4) Jeśli jest ustawiony, to na wyjściu małe litery są odwzorowywane na wielkie. ( c _ o f l a g , SVR4 i 4.3+BSD) Jeśli jest ustawiony, to na wyjściu znak NL jest odwzorowywany na sekwencję znaków CR-NL. (c__of l a g , SVR4) Jeśli jest ustawiony, to znak NL na wyjściu realizuje akcję powrotu karetki. ( c _ o f l a g , SVR4) Jeśli jest ustawiony, to w kolumnie 0 nie jest wyprowadzany znak CR. ( c _ o f l a g , 4.3+BSD) Jeśli jest ustawiony, to na wyjściu są usuwane znaki EOT ( A D). Bywa to niezbędne w niektórych terminalach, które traktują znak Control-D jako wezwanie do zawieszenia. ( c _ o f l a g , POSDC.l) Jeśli jest ustawiony, to przetwarzanie odbywa się zgodnie z regułami zależnymi od implementacji. Odsyłamy do tab. 11.1, w której podajemy różne sygnalizatory zależne od implementacji, które można umieścić w polu c_of l a g . (c_of l a g , 4.3+BSD) Jeśli jest ustawiony, to na wyjściu znak tabulacji jest reprezentowany przez odpowiednią sekwencję odstępów. Efekt jest taki, jak ustawienie sygnalizatora opóźnienia tabulacji poziomej (TABDLY) za pomocą wartości XTABS lub TAB 3. (c_cf l a g , POSIX.l) Jeśli jest ustawiony, to jest generowany bit parzystości dla wyprowadzanych znaków, a dla znaków wejściowych jest sprawdzana parzystość. Jeśli jest ustawiony sygnalizator PARODD, to jest sprawdzana nieparzystość bitów, . Jeśli ptr jest wskaźnikiem pustym, to funkcja sama alokuje obszar dla takiej tablicy (na ogół jako zmienną statyczną). Tak samo jak poprzednio, nazwa terminalu sterującego procesu jest umieszczana w tej tablicy. W obu sytuacjach wartością funkcji jest adres początku tablicy. Ponieważ większość systemów uniksowych używa jako nazwy terminalu sterującego / d e v / t t y , celem tej funkcji jest zapewnienie przenośności, gdy korzystamy z innych systemów operacyjnych.

Przykład - funkcja ctermid Program 11.3 jest implementacjąposiksowej funkcji ctermid. #include #include



static char ctermid_name[L_ctermid]; char * ctermid(char *str) { if (str == NOLL) str = ctermid_name; return(strcpy(str, "/dev/tty"));

/* strcpy() przekazuje str */

Próg. 11.3 Implementacja funkcji c t e r m i d zgodna z normąPOSIX.l

W systemie Unix dużo bardziej interesujące są dwie inne funkcje: i s a t t y , która przekazuje wartość odpowiadającą zdarzeniu „prawda", gdy deskryptor pliku dotyczy urządzenia terminalowego, oraz ttyname, która , przekazuje nazwę ścieżki otwartego urządzenia terminalowego powiązanego i ! z podanym deskryptorem pliku. #include i n t i s a t t y ( i n t filedes) ; Przekazuje: 1 (prawda), jeśli urządzenie terminalowe; w przeciwnym razie 0 (fałsz) char *ttyname (int filedes);

char *ctermid(char *ptr) ; Przekazuje: (patrz poniżej)



Przekazuje: wskaźnik do nazwy ścieżki terminalu, jeśli wszystko w porządku; NULL, jeśli wystąpił błąd

"1 i

11. Terminalowe wejście-wyjście

ykład - funkcja i s a t t y Implementacja funkcji i s a t t y jest bardzo prosta, pokazuje to próg. 11.4. Wywołujemy jedną ze specyficznych dla terminalu funkcji (która nic nie zmienia, jeśli jej wykonanie się powiedzie) i kontrolujemy wartość powrotu. clude

tty(int

Wersja funkcji g e t p a s s w systemie 4.3+BSD czyta ze standardowego wejścia i wypisuje informacje na standardowy strumień komunikatów, gdy nie może otworzyć terminalu sterującego do odczytu i zapisu. Wersja w systemie SVR4 zawsze wyprowadza dane na standardowy strumień komunikatów, ale czyta wyłącznie z terminalu sterującego. • Blokujemy dwa sygnały: SIGINT i SIGTSTP. Gdybyśmy tego nie zrobili, to wprowadzenie znaku INTR mogłoby przerwać wykonanie programu i pozostawić terminal z wyłączonym echem wprowadzanych znaków. Podobnie, efektem wprowadzenia znaku SUSP byłoby

11.10. Tryb kanoniczny

419

zatrzymanie programu i powrót do powłoki z wyłączonym echem wprowadzanych znaków. Decydujemy się na blokowanie sygnałów w czasie czytania hasła. Jeśli te sygnały pojawią się w tym czasie, to zostaną wstrzymane aż do powrotu z odczytu. Są inne metody obsługi tych sygnałów. Niektóre wersje funkcji g e t p a s s po prostu ignorują SIGINT (po zapamiętaniu poprzedniej dyspozycji) i przed powrotem odtwarzają poprzednią dyspozycję dla sygnału. Każde wystąpienie aktualnie ignorowanego sygnału jest tracone. Inne wersje tej funkcji przechwytują sygnał SIGINT (po zapamiętaniu poprzedniej dyspozycji), a po jego odbiorze zerują stan terminalu, realizują konkretną akcję obsługi i wysyłają do siebie sygnał za pomocą funkcji k i l l . Żadna z wersji funkcji g e t p a s s ani nie przechwytuje, ani nie ignoruje, ani nie blokuje sygnału SIGQUIT, a więc wprowadzenie znaku QUIT przerywa program i prawdopodobnie pozostawia terminal z wyłączonym echem wczytywanych znaków. • Musimy pamiętać, że niektóre powłoki, w szczególności KornShell, włączają echo, gdy realizują interakcyjne wczytywanie danych. Są to powłoki, które mają możliwość edytowania wiersza poleceń, a więc manipulują stanem terminalu za każdym razem, gdy wprowadzamy polecenia. Jeśli wywołamy ten program z jednej z takich powłok i następnie przerwiemy go znakiem QUIT, to być może powłoka włączy echo. Inne powłoki, bez wbudowanej edycji poleceń, jak np. powłoka Bourne'a czy powłoka C, przerwą program i pozostawią terminal z wyłączonym echem znaków. Jeśli stanie się tak z naszym terminalem, to za pomocą polecenia s t t y możemy włączyć echo. • Aby czytać dane z terminalu sterującego i zapisywać je na terminal sterujący, korzystamy ze standardowego wejścia-wyjścia. Celowo ustalamy, że strumień jest niebuforowany, inaczej mogłyby zachodzić pewne zależności między zapisywaniem a odczytywaniem ze strumienia (trzeba wówczas dodać wywołania fflush). Moglibyśmy również użyć niebuforowanego wejścia-wyjścia (rozdz. 3), ale chcemy móc symulować funkcję getc za pomocą wywołań read. • Zapamiętujemy tylko do ośmiu znaków hasła. Wszystkie wprowa-„j dzone dodatkowe znaki są ignorowane. > Program 11.9 wywołuje funkcję g e t p a s s i wypisuje wprowadzone da-' ne. Robimy tak, tylko by potwierdzić, że poprawnie działają znaki ERASE ' i KILL (tak powinno być w trybie kanonicznym). Program, który wywołując funkcję g e t p a s s odczytał wpisane jawnym j tekstem hasło, powinien po zakończeniu działań na tym haśle dla bezpieczeń- J stwa wyzerować swoją pamięć związaną z tymi danymi. Jeśli taki program^ może wygenerować plik core z prawem odczytu przez innych (przypomi- namy, że zgodnie z podrozdz. 10.2 domyślne uprawnienia dla pliku core pozwalają na jego odczyt przez każdego) lub gdy inny proces może w jakiś spo- .

11. Terminalowe wejście-wyjście :lude

11.11. Tryb niekanoniczny

421

Rozwiązaniem jest zlecenie systemowi, by zakończył odczyt po odebraniu określonej liczby danych lub gdy upłynie zadany czas oczekiwania. Technika ta używa dwóch zmiennych w tablicy c c c struktury t e r m i o s : MIN i TIME. Te dwa elementy tablicy są indeksowane za pomocą nazw VMIN oraz

"ourhdr.h"

*getpass(const char * ) ;

VTIME.

i (void) char *ptr; if ( (ptr = getpass("Enter password:")) == NULL) err_sys("getpass error"); printf("password: %s\n", ptr); /* teraz można użyć hasła (prawdopodobnie trzeba je zaszyfrować) ... */ while (*ptr != 0) *ptr++ = 0; /* zerujemy zawartość pamięci, gdy zakończyliśmy korzystanie z hasła */ exit(0);

Próg. 11.9

Wywołanie funkcji g e t p a s s

sób odczytać pamięć zawierającą dane naszego programu, to grozi nam przechwycenie hasła. (Przez pojęcie „hasło wprowadzone jawnym tekstem" rozumiemy, że wprowadzono hasło w odpowiedzi na sekwencję zachęty funkcji g e t p a s s . Większość programów uniksowych modyfikuje następnie takie hasło, doprowadzając je do postaci zaszyfrowanej. Pole pw_passwd w pliku haseł zawiera hasło zaszyfrowane, a nie pisane jawnym tekstem). D

11 Tryb niekanoniczny Tryb niekanoniczny definiujemy, wyłączając sygnalizator ICANON w polu e l f l a g struktury t e r m i o s . W trybie niekanonicznym wprowadzane dane nie są gromadzone w wiersze. Nie są przetwarzane następujące znaki specjalne (podrozdz. 11.3): ERASE, KILL, EOF, NL, EOL, EOL2, CR, REPRINT, STATUS i WERASE. Powiedzieliśmy wcześniej, że obsługa trybu kanonicznego jest prosta system przekazuje za każdym razem najwyżej jeden wiersz. Skąd w trybie niekanonicznym system wie, kiedy przekazać nam dane? Przekazywanie po jednym znaku nie byłoby efektywne. (Przypominamy tab. 3.1, która pokazywała, jak rrtjto wydajne jest zapisywanie po jednym bajcie. Każde podwojenie rozmiaru przekazywanych danych zmniejsza o połowę liczbę dodatkowych wywołań/funkcji systemowych). System nie może jednak zawsze przekazywać od razu wielu bajtów danych, gdyż zdarza się, że nie wiemy, ile danych trzeba odczytać, zanim nie zaczniemy czytać.

MIN określa minimalną liczbę odczytanych bajtów, zanim funkcja r e a d powróci. TIME określa, ile dziesiętnych części sekundy należy oczekiwać na dane. Są cztery możliwości: Przypadek A: MIN > 0, TIME > 0 TIME określa licznik czasu oczekiwania między odebraniem kolejnych dwóch bajtów i jest inicjowany po odbiorze pierwszego bajta. Jeżeli przed przedawnieniem licznika system odbierze MIN bajtów, to r e a d przekazuje taką liczbę bajtów. Jeśli licznik czasu oczekiwania przedawni się przed odczytem MIN bajtów, to r e a d przekazuje tylko odebrane bajty. (Jeżeli licznik przekroczył termin, to musiał zostać zainicjowany, a więc odebrano co najmniej jeden bajt). W tym przypadku wywołujący ulega zablokowaniu, dopóki nie odbierze pierwszego bajta. Jeżeli dane są już dostępne, gdy wywołano funkcję read, to efekt jest taki, jak gdyby dane odebrano natychmiast po wywołaniu funkcji read. Przypadek B: MIN > 0, TIME == 0 Funkcja r e a d nie powróci, dopóki nie odbierze MIN bajtów. Efektem takiej sytuacji może być zablokowanie w funkcji r e a d na zawsze.

Przypadek C: MIN == 0, TIME > 0 TIME określa licznik czasu oczekiwania na odczyt i jest inicjowany podczas wywołania funkcji read. (Zauważmy, że w przypadku A niezerowa wartość TIME oznacza licznik czasu oczekiwania między kolejnymi bajtami, który nie jest inicjowany, dopóki nie zostanie odebrany pierwszy bajt). Funkcja read powraca po odbiorze pojedyn-"] czego bajta lub po przedawnieniu licznika czasu oczekiwania. Jeśli licznik uległ przedawnieniu, to funkcja read ma wartość 0. i

Przypadek D: MIN == 0, TIME == 0 • Jeżeli są dostępne jakieś dane, to read przekazuje wymaganą liczbę bajtów. W przeciwnym razie, read przekazuje natychmiast wartość 0. j

Zwróćmy uwagę na to, że we wszystkich tych przypadkach MIN jest warto- = 0) tty_reset(ttysavefd);

62

63

60

172 •

Btruct termios * tty_termios(void)

/* wywołujący może zobaczyć oryginalny stan terminalu */

return(&save_termios);

Próg. 11.10 Ustalenie trybu terminalu: surowego lub cbreak

Podstawową zasadą przy pisaniu programu, który zmienia tryb pracy jferminalu, powinno być przechwytywanie większości sygnałów. 'Dzięki temu możemy przywrócić oryginalny tryb pracy terminalu przed zakończeniem programu. Wyłączenie potwierdzania wprowadzonych znaków na ekranie terminalu.

wprowadzamy znak DELETE Enter cbreak modę characters, terminate with SIGINT 1 wprowadzamy Control-A 10 wprowadzamy znak cofnięcia s i g n a l caught wprowadzamy znak przerwania

\ •' ) '• ,

W trybie surowym wprowadziliśmy znak Control-D (04), a następnie nacis-, nęliśmy klawisz specjalny F7. Na tym konkretnym terminalu klawisz funk-s cyjny wygenerował sześć znaków: ESC (033), [ (0133), 2 (062), 3 (063),; 0 (060) oraz z (0172). Widzimy, że gdy jest wyłączone przetwarzanie danychj 1 na wyjściu w trybie surowym (~OPOST), to po kolejnych znakach nie poją- ; wiają się znaki powrotu karetki. Ponadto w trybie cbreak jest wyłączone prze^ twarzanie specjalnych znaków (a więc Control-D, czyli znak końca pliku ani! znak cofnięcia nie są traktowane specjalnie), ale nadal są obsługiwane sygnały generowane za pomocą terminalu. D'

11. Terminalowe wejście-wyjście

clude clude

11.12 Rozmiar okna terminalu

"ourhdr.h"

Systemy SVR4 oraz systemy berkelejowskie potrafią śledzić bieżący rozmiar okna terminalu, a jądro systemu powiadamia pierwszoplanową grupę procesów o każdej zmianie rozmiaru. System dla każdego terminalu i pseudoterminalu przechowuje strukturę winsize.

t i c void sig_catch(int); n(void) int char

struct winsize { unsigned short unsigned short unsigned short unsigned short

i;

c;

if (signal (SIGINT, sig_catch) == SIG_ERR) /* przechwytywanie sygnałów */ err_sys("signal(SIGINT) error"); if (signal (SIGCjUIT, sig_catch) == SIG_ERR) err_sys("signal(SIGQUIT) error"); if (signal(SIGTERM, sig_catch) == SIG_ERR) err_sys("signal(SIGTERM) error");

tty_reset(STDIN_FILENO); if (i liaoq programy. SysWz^S blokujące wejśoz[3w aapjplold

Przykład Przyjrzyjmy się przyH^siq aig Program 12.1 czyta doob BJ^SO I.£f oiBigoi'! zapisać je na standard»biBbriB}g BH w tryb pracy bez bloldold sad

w

a wynik każdego wyw^w ogabśfijf >Iin^{w B dowy strumień komurno>f ńairnuite ^wob s e t f l , którą pokaza£SB>Ioq ^ióJ>I ;I3:__;t9a den lub kilka bitów sy^a wóJid BiIIi>ł duł nab łfinclude łfinclude tfinclude tfinclude char



"ourhdr.h"

9buxonx#



9buion±# sbuloniłł 9buloni#

"rf.abriTuo" ; [00000i]lud

int main(void)

inńo

Jni (bxov) nis/n i

}

i

int char

ntowrite, nwritti^wn *ptr;

li^woJn

Jni

\ iJa*

lerlo

ntowrite = read(STDIN_H_MiaTB)b&ei = f p r i n t f ( s t d e r r , "read ¥ set_fl(STDOUT_FILENO, O for

(ptr = buf; ntowritrittworfn ;xud = lią) io1 errno = 0; -,0 = o m i e nwrite = write (STDOCIT2): f p r i n t f ( s t d e r r , "nwn' if (nwrite > 0) { } (0 < g j i w n ) p t r += nwrite; •, 9 i i w n =+ i Ją ntowrite -= nwnwn =-

Clr_f1(STDOUT_FILENO, O ,OH3JIr3_TUOaT8) exit (0);

Próg. 12.1 ZapisygiąBS ISl .goił

I

12. Zaawansowane operacje wejścia-wyjścia

12.3. Ryglowanie rekordów

Standardowe wyjście jest zwykłym plikiem; oczekujemy, że funkcja w r i t e wykona się jeden raz.

miaru funkcja w r i t e zaczęła przekazywać błąd EAGAIN. (Plik wejściowy n został nigdy wypisany na terminal, a program generował ciągły strumień kc munikatów o błędzie). Takie zachowanie jest spowodowane faktem, że w systemie SVR4 pn gram obsługi terminalu jest powiązany z naszym programem za pomocą sy temu strumieni wejścia-wyjścia. (W podrozdziale 12.4 opisujemy szczegc łowo strumienie). System strumieni ma swoje własne bufory i dlatego moi zaakceptować jednorazowo więcej danych z programu. Zachowanie w syst< mie SVR4 zależy również od typu terminalu, czyli od tego, czy jest to term nal fizyczny, konsola czy pseudoterminal. i

$ l s l s -1 /etc/termcap sprawdzamy rozmiar pliku -rw-rw-r— 1 root 133439 Oct 11 1990 /etc/termcap $ a . o u t < /etc/termcap > temp. f i l e najpierw próbujemy użyć zwykłego pliku read 100000 bytes nwrite = 100000, errno = 0 pojedynczy zapis $ l s -1 temp. f i l e sprawdzamy rozmiar pliku wyjściowego -rw-rw-r— 1 stevens 100000 Nov 21 16:27 t e m p . f i l e

Gdy standardowym wyjściem jest terminal, to oczekujemy, że funkcja w r i t e przekaże raz część danych, a następnym razem błąd. To właśnie widzimy na wydruku.

W tym przykładzie program wykonuje tysiące wywołań funkcji writ< podczas gdy 20 wywołań wystarcza do wyprowadzenia danych. Pozosta tylko przekazują błąd. Taki rodzaj pętli, zwany odpytywaniem, jest w sy; temie wieloużytkowym istotną stratą czasu procesora. W podrozdziale 12 zobaczymy, że bardziej wydajną metodą realizacji tego typu zadania je zwielokrotnianie wejścia-wyjścia dla nieblokujących deskryptorów. Z nieblokującym wejściem-wyjściem spotkamy się jeszcze w rozdz. 1' gdy podczas wyprowadzania danych na urządzenie terminalowe (druka kę postscriptową) będzie nam zależało, by nie zablokować się w funkc write.

$ a.out < /etc/termcap 2>stderr.out wyprowadzanie danych na terminal ciąg danych wyprowadzonych na terminal... $ cat stderr.out read 100000 bytes nwrite = 8192, errno = 0 nwrite = 8192, errno = 0 nwrite = - 1 , errno = 11 211 takich błędów nwrite = 4096, errno = 0 nwrite = - 1 , errno = 11

658 takich błędów

nwrite = 4096, errno = 0 nwrite = - 1 , errno = 11

604 takich błędów

nwrite = 4096, errno = 0 nwrite = - 1 , errno = 11

1047 takich błędów

nwrite = - 1 , errno = 11

1046 takich błędów

nwrite = 4096, errno = 0

i tak dalej...

W systemie, w którym otrzymaliśmy powyższe rezultaty, wartość 11 zmiennej e r r n o oznacza EAGAIN. Program obsługi terminalu akceptował za każdym razem 4096 lub 8192 bajtów. W innym systemie pierwsze trzy wywołania funkcji w r i t e przekazały odpowiednio wartości 2005, 1822 oraz 1811, a potem 96 błędów, następnie zostało zapisanych 1846 bajtów itd. Ilość danych akceptowanych przez poszczególne operacje w r i t e zależy od systemu. Zachowanie tego programu w systemie SVR4 jest zupełnie odmienne od pokazanego wyżej - do wyprowadzenia danych na terminal wystarcza jedno wywojinie funkcji w r i t e , które wypisuje cały plik wejściowy. Widzimy, że tryb Mez blokowania nie wprowadza w tym systemie żadnych różnic. Utworzyliśmy większy plik i zwiększyliśmy rozmiar bufora w programie. Program zachowywał się tak samo (jedno wywołanie funkcji w r i t e dla całego pliku), dopóki rozmiar pliku był mniejszy niż 700 tysięcy bajtów. Powyżej tego roz-

43

12.3 Ryglowanie rekordów

Co się stanie, gdy jednocześnie dwóch użytkowników przystąpi do modyf kowania tego samego pliku? W większości systemów uniksowych ost; teczny stan pliku jest zgodny z tym, co zapisał do niego ostatni proces. ls nieją jednak programy użytkowe, jak np. systemy bazy danych, w któryc proces musi mieć pewność, że sam zapisuje dane do pliku. Z tego powód nowsze systemy realizują ryglowanie rekordów. (W rozdziale 16 przygoti jemy bibliotekę do obsługi bazy danych, która będzie korzystała z ryglowi nia rekordów). , Ryglowanie rekordów to termin stosowany do wskazania, że jeden ppj ces, który odczytuje lub aktualizuje fragment pliku, może zapobiec modyflk! cji jakiegoś obszaru pliku przez inny proces. W systemie Unix nazwa ,„rj kord" jest myląca, gdyż jądro nie stosuje pojęcia rekordu w pliku. Lepszy] terminem byłby „zakres ryglowania", ponieważ ryglowaniu podlega pewis zakres pliku (nieraz nawet cały plik). , Historia

•3 W tabeli 12.1 pokazujemy różne formy ryglowania rekordów stosowaf w systemach uniksowych.

12. Zaawansowane operacje wejścia-wyjścia Tabela 12.1 Formy ryglowania rekordów stosowane przez różne systemy uniksowe Obowiązujące

Zalecane

System

fcntl

lockf

POS1X.1





XPG3





SVR2







SVR3, SVR4







4.3BSD



4.3BSD Reno





flock

pid_t l_ •





Ryglowanie rekordów dodał w 1980 roku do wersji 7 John Bass. Do jądra systemu wprowadzono funkcję systemową o nazwie locking. Ta funkcja, realizująca obowiązkowe ryglowanie rekordów, pojawiła się w różnych wersjach Systemu III. Przejęły ją również systemy Xenix, a system SVR4 nadal jej używa w swojej bibliotece zgodności z systemem Xenix. System SVR2 z 1984 roku był pierwszym wydaniem Systemu V, które stosowało styl ryglowania rekordów za pomocą funkcji f c n t l .

yglowanie rekordów za pomocą funkcji f c n t l Powtórzmy prototyp funkcji f c n t l , który pokazaliśmy już w podrozdz. 3.13. #include #include łtinclude f c n t l ( i n t filedes,

struct flock { short l_type; off_t l_start; short l whence; off_t l

Dalej w tym podrozdziale opisujemy różnice między ryglowaniem zalecanym oraz obowiązkowym. Jak pokazuje ta tabela, norma POSIX.l wybrała styl ryglowania rekordów Systemu V oparty na funkcji f c n t l . Takiej samej metody używa wersja systemu 4.3BSD o nazwie Reno. Starsze wersje berkelejowskie stosowały wyłącznie funkcję flock z systemu BSD. Funkcja flock zawsze rygluje cały plik, nie potrafi wykonać tej operacji dla wskazanego obszaru pliku. Funkcja f c n t l normy POSIX.l może zaryglować dowolny fragment pliku, a nawet pojedynczy bajt w tym pliku. My opisujemy wyłącznie ryglowanie posiksowe korzystające z funkcji f c n t l . Funkcja lockf w Systemie V to tylko interfejs do funkcji f c n t l .

int

12.3. Ryglowanie rekordów

int

cmd,

...I*

struct

flock

*flockptr

*/);

Przekazuje: wartości zależne od argumentu cmd (patrz poniżej), jeśli wszystko j w porządku; - 1 , jeśli wystąpił błąd

W przypadku ryglowania rekordów argument cmd przyjmuje wartości: F GETLK, F_SETLK lub F_SETLKW. Trzeci argument (który nazwaliśmy tu flockptr) jest wskaźnikiem do struktury flock.

437 /* F_RDLCK, F_WRLCK lub F_UNLCK */ /* pozycja liczona w bajtach, relatywnie do l_whence */ /* SEEK_SET, SEEK_CUR lub SEEKJEND */ /* rozmiar w bajtach; 0 oznacza ryglowanie do końca pliku */ /* identyfikator procesu przekazywany przez operację F_GETLK */

Struktura ta zawiera: • typ żądanego ryglowania: FRDLCK (wspólny rygiel odczytu), F_WRLCK (wyłączny rygiel zapisu) lub FJJNLCK (odryglowanie obszaru), • wskaźnik początku obszaru, który jest ryglowany lub zwalniany ( l _ s t a r t i l_whence) i • rozmiar obszaru ( l l e n ) . Istnieje wiele zasad dotyczących sposobu określania obszaru, który ma zostać zaryglowany lub odryglowany. • Dwa elementy, które definiują wskaźnik początku obszaru, są podobne do dwóch argumentów funkcji l s e e k (podrozdz. 3.6). Rzeczywiście, pole l w h e n c e może przyjąć jedną z trzech wartości SEEK_SET,SEEK_CURlub SEEK_END.

• Rygle mogą zaczynać się i rozciągać powyżej bieżącego końca pliku, ale nie mogą rozpoczynać się i rozciągać przed początkiem pliku. • Jeśli pole 1 1 en ma wartość 0, to rygiel rozciąga się aż do największego możliwego wskaźnika pozycji w tym pliku. W ten sposób możemy zaryglować obszar rozpoczynający się w dowolnym miejscu w pliku i rozciągający się do ostatniej pozycji w pliku, łącznie z wszystkimi danymi ostatnio dołączonymi do niego. (Nie musimy więc zgadywać, ile bajtów może zostać dodanych do pliku). • Aby zaryglować cały plik, ustawiamy pola l _ s t a r t oraz l_whence, tak by wskazywały początek pliku i podajemy rozmiar ( l l e n ) rów- j ny 0. (Jest kilka sposobów określenia początku pliku, ale większość> programów użytkowych podaje w polu l s t a r t wartość 0, a w polu ( l_whence wartość SEEK_SET). - '

Wymieniliśmy dwa typy rygli: wspólny rygiel odczytu (pole l _ t y p e \ równe F_RDLCK) oraz wyłączny rygiel zapisu (F_WRLCK). Podstawowa za- i sada głosi, że dowolna liczba procesów może mieć wspólny rygiel odczytu I danego bajtu, ale tylko jeden proces może mieć wyłączny rygiel zapisu da- \ nego bajtu. Oprócz tego, jeśli dany bajt jest zaryglowany wielokrotnie do od-^ czytu, to nie jest możliwe otrzymanie żadnego rygla dla zapisu tego bajtu, i a gdy dany bajt jest zaryglowany do zapisu, to nikt nie otrzyma rygla do odczytu tego bajtu. Tę regułę zgodności przedstawiamy w tab. 12.2. |

12. Zaawansowane operacje wejścia-wyjścia Tabela 12.2

żacy stan obszaru

Zgodności różnych rodzajów ryglowania Żądanie rygiel odczytu

rygiel zapisu

bez rygli

w porządku

w porządku

co najmniej jeden rygiel odczytu

w porządku

zabronione

jeden rygiel zapisu

zabronione

zabronione

Gdy chcemy uzyskać rygiel do odczytu, wówczas deskryptor musi być otwarty do odczytu, a uzyskanie rygla zapisu jest możliwe, gdy deskryptor został otwarty do zapisu. Po tym wstępie możemy już opisać trzy różne operacje w funkcji f c n t l . F GETLK

FSETLK

Sprawdzenie, czy rygiel opisany w strukturze wskazywanej przez argument flockptr jest zajęty przez jakiś inny rygiel. Jeżeli istnieje taki rygiel, to informacja o nim zamazuje dane wskazywane przez argument flockptr. Jeżeli nie istnieje żaden rygiel, który uniemożliwia utworzenie naszego, to zawartość struktury wskazywanej przez flockptr pozostaje bez zmian, tylko pole I t y p e przyjmuje wartość F_UNLCK. Zaryglowanie zgodne z opisem w argumencie flockptr. Jeżeli próbujemy otrzymać rygiel do odczytu (pole l_type równe F_RDLCK) lub do zapisu (pole l_type równe F_WRLCK), a reguła zgodności nie zezwala, by system przekazał taki rygiel (tab. 12.2), to funkcja f c n t l natychmiast powraca z wartością zmiennej errno równą EACCES lub EAGAIN. System SVR2 przekazuje obecnie błąd EACCES, ale podręcznik systemowy ostrzega, że w przyszłości będzie przekazywany błąd EAGAIN. Jednak system SVR4 kontynuuje tę tradycję (przekazując EACCES z takim samym ostrzeżeniem na przyszłość). System 4.3+BSD przekazuje EAGAIN. POSIX. 1 dopuszcza każdy z tych typów błędów.

Ta operacja jest również stosowana do usunięcia rygla wskazanego w argumencie, flockptr (pole l_type równe F_UNLCK). F_SETLKW Blokująca wersja F_SETLK. (Litera W w nazwie polecenia oznacza oczekiwanie, wait). Jeżeli wymagany rygiel odczytu lub zapisu nie > może zostać przydzielony, ponieważ inny proces aktualnie ma zaryglowaną jakąś część danego obszaru, to proces wywołujący jest wprowadzany w stan uśpienia. Jego drzemkę przerwie odbiór jakiegoś sygnału. Pamiętajmy że sprawdzenie rygla przez operację F_GETLK, a następnie podjęcie próbyjftrzymania tego rygla przez operację F_SETLK lub F_SETLKW nie jest operacją atomową. Nie mamy gwarancji, że między dwoma wywołaniami funkcji f c n t l nie pojawi się jakiś inny proces, który zażąda tego samego rygla. Jeśli nie chcemy zablokować się w oczekiwaniu na określony rygiel, to musimy obsłużyć ewentualny błąd przekazany przez polecenie FSETLK.

12.3. Ryglowanie rekordów

439

Gdy ustalamy lub zwalniamy rygiel dla danego pliku, wówczas system w miarę potrzeby odpowiednio łączy lub dzieli obszary. Na przykład, jeżeli ryglujemy bajty od pozycji 100 do pozycji 199, a następnie zwalniamy rygiel bajtu 150, to jądro nadal utrzymuje rygle bajtów od 100 do 149 i od 151 do 199.

Przykład - pobieranie i zwalnianie rygla Aby uniknąć konieczności każdorazowego alokowania struktury f lock i wypełniania wszystkich jej elementów, funkcja lock_reg z próg. 12.2 obsługuje te szczegóły. #include #include #include

"ourhdr.h"

int lock_reg(int fd, int cmd, int type, off_t offset, int whence, off t len) struct flock lock; lock.l_type = type; lock.l_start = offset; lock.l_whence = whence; lock.l len = len;

/* /* /* /*

F_RDLCK, F_WRLCK, FJJNLCK */ pozycja bajtu, w odniesieniu do l_whence */ SEEK_SET, SEEK_CUR, SEEK_END */ liczba bajtów (0 oznacza do znaku EOF) */

return( fcntl(fd, cmd, slock)

Próg. 12.2

Funkcja ryglująca lub odryglowująca obszar pliku

Ponieważ większość funkcji ryglujących służy do zaryglowania lub zwolnienia obszaru (polecenie F_GETLK jest rzadko stosowane), typowo używamy jednego z poniższych makr umieszczonych w pliku ourhdr. h (dodatek B). #define read_lock (fd, offset, whence, len) \ lock_reg(fd, F_SETLK, F_RDLCK, offset, #define readw_lock(fd, offset, whence, len) \ lock_reg(fd, F_SETLKW, F_RDLCK, offset, #define write_lock(fd, offset, whence, len) \ lock_reg(fd, F_SETLK, F_WRLCK, offset, #define writew_lock(fd, offset, whence, len) \ lock_reg(fd, F_SETLKW, F_WRLCK, offset, #define un__lock(fd, offset, whence, len) \ lock_reg(fd, F_SETLK, FJJNLCK, offset,

whence, len),

.j

whence, len) , whence, len) whence,

len) j

whence, len) ^

Celowo zdefiniowaliśmy trzy pierwsze argumenty tych makr zgodnie z uporządkowaniem argumentów funkcji lseek. •

40

12. Zaawansowane operacje wejścia-wyjścia

rzykład - sprawdzanie rygla W programie 12.3 jest zdefiniowana funkcja l o c k _ t e s t , której używamy do sprawdzania rygla. Lnclude Lnclude Lnclude



"ourhdr.h" int, off t ) ;

int main(void)

Ld__t

int pid_t

>ck test(int fd, int type, off_t offset, int whence, off_t len)

/* F_RDLCK lub F_WRLCK */

lock.l_start = offset; /* pozycja bajtu, w odniesieniu do l_whence */ lock.l_whence = whence; /* SEEK_SET, SEEK_CUR, SEEK_END */ lock.l_len = len; /* liczba bajtów (0 oznacza do znaku EOF) */ if (fcntlffd, F_GETLK, Slock) < 0) err_sys("fcntl error");

TELL_WAIT(); if ( (pid = fork()) < 0) err_sys("fork error");

if (lock.l_type == F_UNLCK) return(O); /* fałsz, obszar nie jest zaryglowany przez inny proces */ return(lock.l_pid); /* prawda, przekazujemy identyfikator procesu właściciela rygla */

Funkcja sprawdzająca warunki ryglowania

Jeśli istnieje rygiel, który mógłby zablokować żądanie określone w argumentach, to nasza funkcja przekazuje identyfikator procesu utrzymującego ten rygiel. W przeciwnym razie przekazuje 0 (fałsz). Zazwyczaj wywołujemy tę funkcję z jednego z poniższych dwóch makr (zdefiniowanych w pliku ourhdr. h). #define is_read_lockable(fd, offset, whence, len) \ lock_test(fd, F_RDLCK, offset, whence, len) #define is_write_lockable(fd, offset, whence, len) \ lock_test(fd, F_WRLCK, offset, whence, len)

fd; pid;

/* tworzymy plik i zapisujemy do niego dwa bajty */ if ( (fd = creat("templock", FILE_M0DE)) < 0) err_sys("creat error"); if (write(fd, "ab", 2) != 2) err_sys("write error");

struct flock lock;

Próg. 12.3

#include #include #include fłinclude

static void lockabyte(const char

"ourhdr.h"

lock.l_type = type;

441

12.3. Ryglowanie rekordów

*



zykład - zakleszczenie Do zakleszczenia dochodzi, gdy dwa procesy oczekują na element danych, który wcześniej zaryglował ktoś inny. Potencjalne zakleszczenie pojawia się, jeśli proces mający zaryglowany jakiś obszar zostanie uśpiony, gdy próbuje zaryglować kolejny obszar będący pod kontrolą innego procesu.

else if (pid == 0) { /* potomek */ lockabyte("child", fd, 0) ; TELL_PARENT(getppid()); WAIT_PARENT(); lockabyte("child", fd, 1 ) ; /* proces macierzysty */ } else { lockabyte("parent", fd, 1 ) ; TELL_CHILD(pid); WAIT_CHILD(); lockabyte("parent", fd, 0) ; exit(0);

static void lockabyte(const char *name, int fd, off_t offset) { if (writew_lock(fd, offset, SEEK_SET, 1) < 0) err_sys("%s: writew_lock error", name); printf("%s: got the lock, byte %d\n", name, offset);

Próg. 12.4

Przykład wykrywania zakleszczenia

Przykładowe zakleszczenie widzimy w próg. 12.4. Potomek rygluje bajt 0, a proces macierzysty bajt numer 1. Następnie każdy z procesów próbuje otrzymać rygiel bajtu przed chwilą zaryglowanego przez drugi proces. Korzystamy z procedur synchronizowania pracy procesu macierzystego i potomka

12. Zaawansowane operacje wejścia-wyjścia zpodrozdz. 8.8 (TELL_xxx i WAIT_XXX), dzięki którym procesy mogą wzajemnie oczekiwać na siebie, by otrzymać rygiel. Uruchomienie próg. 12.4 daje następujący wynik: $ a.out child: got the lock, byte 0 parent: got the lock, byte 1 child: writew lock error: Deadlock situation detected/avoided parent: got the lock, byte 0

Po wykryciu zakleszczenia jądro musi wybrać jeden proces, który odbierze błąd. W tym przypadku odebrał błąd proces potomny, ale jest to tylko szczegół implementacyjny. Testowe uruchomienia tego programu w innym systemie pokazały, że szansę obu procesów na otrzymanie błędu są równe. •

ilikowane dziedziczenie i zwalnianie rygli Automatycznym dziedziczeniem i zwalnianiem rygli rekordów rządzą trzy reguły. 1. Rygle są powiązane z procesem i plikiem. Ma to dwa następstwa. Pierwsze jest naturalne: gdy proces kończy się, wówczas jego rygle są zwalniane. Drugie nie jest zbyt oczywiste: gdy jest zamykany deskryptor, wówczas wszystkie rygle w danym procesie, odnoszące się do pliku wskazywanego tym deskryptorem, są zwalniane. Oznacza to, że jeśli realizujemy poniższe cztery kroki: fdl = open(pathname, . . . ) ; read_lock(fdl, ...); fd2 = dup(fdl); close(fd2);

to po wywołaniu funkcji c l o s e (fd2) jest zwalniany rygiel, który otrzymaliśmy dla deskryptora fdl. Tak samo stanie się, gdy zastąpimy funkcję dup funkcją open, np. tak fdl = open(pathname, . . . ) ; read_lock(fdl, . . . ) ; fd2 = open(pathname, . . . ) ; close(fd2);

by otworzyć ten sam plik i otrzymać dla niego inny deskryptor. 2. Rygle nie są nigdy dziedziczone przez proces potomny po wywołaniu funkcji fork. Oznacza to, że gdy proces uzyskuje rygiel i wywołuje funkcję fork, to względem rygla otrzymanego przez proces macie-

12.3. Ryglowanie rekordów

443

rzysty potomek jest traktowany jak „obcy proces". Potomek musi wywołać funkcję f c n t l , aby otrzymać własne rygle dla deskryptorów, które odziedziczył w wyniku funkcji fork. Jest to logiczne, gdyż rygle mają zapobiegać jednoczesnemu pisaniu do tego samego pliku przez wiele procesów. Jeżeli potomek dziedziczyłby rygle po wywołaniu fork, to procesy macierzysty i potomny mogłyby pisać jednocześnie do tego samego pliku. 3. Rygle mogą być dziedziczone przez nowy program wywołany za pomocą funkcji exec. Musimy powiedzieć mogą, gdyż norma POSIX. 1 nie stawia takiego wymagania. W systemach SVR4 i 4.3+BSD rygle są dziedziczone w wyniku wywołania funkcji

Implementacja 4.3+BSD Przyjrzymy się teraz strukturom danych, z których korzysta implementacja w systemie 4.3+BSD. Z pewnością ułatwi nam to zrozumienie zasady 1, stwierdzającej, że rygle są powiązane z procesem i plikiem. Załóżmy, że jakiś proces wykonuje poniższe instrukcje (ignorując przekazywane przez nie błędy): fdl = open(pathname, . . . ) ; write_lock(fdl, 0, SEEK_SET, 1);

/* proces macierzysty rygluje do zapisu bajt 0 */ /* proces macierzysty */

if (fork() > 0) { fd2 = dup(fdl); fd3 = open(pathname, . . . ) ; pause(); } else { read_lock(fdl, 1, SEEK_SET, 1); /* potomek rygluje do odczytu bajt 1 */ pause();

„i

Na rysunku 12.1 pokazujemy stan struktur danych po wykonaniu przez i oba procesy, macierzysty i potomny, operacji pause. i Wcześniej pokazaliśmy wszystkie struktury danych, na które mają, wpływ wywołania funkcji open, fork i dup (rys. 3.3 i 8.1). Tutaj mamy do-"i datkowo struktury f lock, które są powiązane ze sobą za pomocą struktury •, natomiast przy operatorze » nastąpiło zablokowanie do czasu usunięcia obowiązkowego rygla. (Różnice w obsłudze operatora dołączenia możemy wytłumaczyć tym, że w KornShellu w otwarciu pliku są wskazywane sygnalizatory O C R E A T i O_APPEND, a zgodnie z tym, co powiedzieliśmy, O_CREAT generuje błąd. Powłoka Bourne'a nie używa sygnalizatora OCREAT, jeśli, plik już istnieje, a więc funkcja open może się powieść, ale operacja w r i t e ulega zablokowaniu). Ostatnia sytuacja pokazuje, że musimy być czujni, gdy stosujemy ryglowanie obowiązkowe rekordów. Z przykładu z edytorem ed wynika, że niektóre programy potrafią obejść ograniczenia ryglowania. Zdarza się, że złośliwi użytkownicy, stosują obowiązkowe ryglowanie rekordów, by utrzymywać rygiel pliku przeznaczonego do odczytu przez wszystkich. W efekcie nikt nie ma prawa zapisywać do takiego pliku. (Oczywiście, aby było to możliwe, plik musi mieć włączone obowiązkowe ryglo-

45]

wanie rekordów, co na ogół wymaga, by użytkownik mógł zmienić bit} uprawnień dostępu dla tego pliku). Rozważmy bazę danych, która jest czytani przez wszystkich i ma włączone obowiązkowe ryglowanie rekordów. Gdyb} złośliwy użytkownik utrzymywał przez dłuższy czas rygiel odczytu dla całegc pliku, wówczas do takiego pliku nie mógłby pisać żaden inny proces.

Przykfad

Program 12.7 sprawdza, czy system stosuje obowiązkowe ryglowanie rekordów. #include #include #include #include #include łfincłude



"ourhdr.h"

int main(void) r

1

fd; int pid_t pid; char buff[5]; struct :stat statbuf; if ( (fd = open("templock", O RDWR | O CREAT | O TRUNC, FILE MODĘ)) < 0) err _sys("open error"); if (write(fd, "abcdef", 6) 6) err sys("write error"); /* włączamy bit ustanowienia identyfikatora grupy i wyłączamy bit wykonania przez grupę */ if (fstatffd, Sstatbuf) < 0) err_sys("fstat error"); if (fchmod(fd, (statbuf.st_mode & ~S_IXGRP) | S_ISGID) < 0) err_sys("fchmod error"); TELL_WAIT(); if ( (pid = fork()) < 0) { err_sys("fork error"); } else if (pid > 0) { /* proces macierzysty */ /* rygiel zapisu całego pliku */ if (write_lock(fd, 0, SEEK_SET, 0) < 0) err_sys("write_lock error"); TELL_CHILD(pid); if (waitpid(pid, NULL, 0) < 0) err_sys("waitpid error");

12. Zaawansowane operacje wejścia-wyjścia } else { WAIT PARENT(

/* proces potomny */ /* czekamy, by proces macierzysty ustalił rygiel */

set_f1(fd, O_NONBLOCK); /* gdy obszar jest zaryglowany, najpierw sprawdzamy rodzaj błędu */ if (read_lock(fd, 0, SEEK_SET, 0) != -1) /* bez oczekiwania */ err_sys("child: read_lock succeeded"); printf("read_lock of already-locked region returns %d\n", errno); /* następnie próbujemy odczytać plik zaryglowany obowiązkowo */ if (lseek(fd, 0, SEEK_SET) == -1) err_sys("lseek error"); if (read(fd, buff, 2) < 0) err_ret("read failed (mandatory locking works)"); else printf("read OK (no mandatory locking), buff = %2.2s\n", buff); exit(0); Próg. 12.7 Sprawdzenie, czy system stosuje obowiązkowe ryglowanie

Tworzy plik i włącza dla niego obowiązkowe ryglowanie. Następnie generuje proces potomny. Proces macierzysty uzyskuje rygiel całego pliku. Potomek najpierw ustala, że jego deskryptor jest nieblokujący, a następnie próbuje otrzymać rygiel odczytu dla tego pliku, oczekując, że odbierze błąd. W ten sposób sprawdzamy, czy system przekazuje błąd EACCES, czy EAGAIN. Następnie potomek przewija plik do początku i próbuje wykonać operację read. Jeśli system stosuje ryglowanie obowiązkowe, to funkcja read powinna przekazać błąd EACCES lub EAGAIN (ponieważ deskryptor jest nieblokujący). W przeciwnym razie, funkcja read przekazuje odczytane dane. Uruchomienie tego programu w systemie SVR4 (który realizuje obowiązkowe ryglowanie) daje poniższy wynik $ a.out read_lock of already-locked region returns 13 read failed (mandatory locking works): No morę processes

Zgodnie z systemowymi plikami nagłówkowymi oraz dokumentacją intro(2), wartość 13 zmiennej errno oznacza błąd EACCES. Na podstawie tego przykładu widzimy też, że błędowi przekazanemu przez read (EAGAIN) towarzyszy komunikat No morę processes (brak procesów w systemie). Typowo taki błąd przekazuje funkcja f ork, gdy system nie może utworzyć kolejnego procesu. W systemie 4.3+BSD otrzymujemy $ a. out read_lock of already-locked region returns 35 read OK (no mandatory locking), buff = ab

Zmienna e r r n o o wartości 35 oznacza EAGAIN. Ryglowanie obowiązkowe nie jest zaimplementowane w tym systemie. •

12.4. Strumienie

453

Przykład

Powróćmy do naszego pierwszego pytania w tym podrozdziale: co się stanic, gdy dwie osoby edytująjednocześnie ten sam plik? Zwykłe uniksowe edytory tekstowe nie stosują ryglowania rekordów, a więc nadal odpowiedź nie jest jednoznaczna, gdyż ostateczny wynik zależy od tego, który proces jako ostatni zapisał dane do pliku. (W systemie 4.3+BSD edytor vi używa opcji kompilacji, która włącza zalecane ryglowanie rekordów, ale domyślnie ta opcja nie jest aktywna). Nawet jeżeli nałożymy wymóg zalecanego ryglowania w jakimś edytorze, np. vi, to nadal inne edytory w systemie nie stosują tego typu ryglowania. Jeżeli system implementuje obowiązkowe ryglowanie rekordów, to możemy dostosować używany przez siebie edytor (jeśli mamy jego kod źródłowy), by używał tego ryglowania. Nie mając kodu źródłowego edytora, możemy spróbować zastosować poniższe podejście. Przygotowujemy własny program, który pełni funkcję programu opakowującego polecenie vi. Nasz program natychmiast wywołuje funkcję fork, zadaniem procesu macierzystego jest tylko oczekiwanie na zakończenie pracy przez potomka. Potomek otwiera plik wskazany w wierszu poleceń, włącza ryglowanie obowiązkowe, uzyskuje rygiel do zapisu, a następnie za pomocą funkcji exec wywołuje vi. Gdy działa edytor vi, wówczas plik jest zaryglowany do zapisu, a więc inny użytkownik nie zmodyfikuje go w tym czasie. Gdy vi zakończy pracę, funkcja wait w procesie macierzystym powróci i zakończy się również nasz program opakowujący. W tym przykładzie zakładamy, że rygle są dziedziczone po wywołaniu exec; jak powiedzieliśmy wcześniej, jest tak w systemie SVR4 (jedyny opisany przez nas system, który realizuje ryglowanie obowiązkowe). Okazuje się, że taki mały program opakowujący niestety nie działa zgodnie z oczekiwaniami. Problem polega na tym, że większość edytorów (przynajmniej vi i ed) odczytuje plik wejściowy i zaraz potem zamyka go. Zamknięcie deskryptora związanego z tym plikiem jest jednoznaczne ze zwolnieniem jego rygla. Oznacza to, że gdy edytor zamyka plik po przeczytaniu jego zawar- , tości, ginie rygiel pliku. Nie ma metody, by temu zapobiec. • (j

W rozdziale 16 korzystamy z ryglowania rekordów w naszej bibliotece ' obsługi bazy danych, aby zagwarantować współbieżny dostęp do zasobów j przez wiele procesów. W tym rozdziale dokonujemy również pomiaru czasu, • H aby sprawdzić, jak ryglowanie rekordów wpływa na proces.

12.4 Strumienie Strumienie dają w Systemie V uniwersalny sposób komunikacji między procedurami obsługi urządzeń w jądrze systemu. Musimy omówić strumienie, aby zrozumieć (a) interfejs terminalu w Systemie V, (b) użycie funkcji p o l l do

1

12. Zaawansowane operacje wejścia-wyjścia

12.4. Strumienie

455

zwielokrotniania wejścia-wyjścia (p. 12.5.2), (c) implementację łączy strumieniowych oraz nazwanych łączy strumieniowych (podrozdz. 15.2 oraz 15.5). Strumienie zaprojektował Dennis Ritchie ([41]) jako sposób uporządkowania tradycyjnego znakowego wejścia-wyjścia (listy znakowe, clist) oraz dostosowania protokołów sieciowych. Następnie dodano strumienie do systemu SVR3. Pełne wsparcie dla strumieni (czyli system terminali oparty na strumieniach) dostarczono w systemie SVR4. Dokumentacja [12] opisuje implementację w systemie SVR4, w którym ta technika jest nazywana STREAMS.

proces użytkowy t

w dół strumienia czoło strumienia i

moduł przetwarzający

Nie należy mylić tego zastosowania słowa „strumienie" z poprzednim, z którym spotkaliśmy się przy omawianiu standardowej biblioteki wejścia-wyjścia (podrozdz. 5.2).

Strumień daje nam dwukierunkową ścieżkę między procesem użytkowym a procedurą obsługi urządzenia. Nie ma wymagania, by strumień kontaktował się z rzeczywistym urządzeniem sprzętowym. Możemy używać strumieni w procedurach obsługi pseudourządzeń. Na rysunku 12.5 przedstawiamy to, co nazywamy prostym strumieniem. proces użytkowy

r---

czoło strumienia (interfejs funkcji systemowej) •t

jądro

program obsługi urządzenia (lub pseudourządzenia)

Rys. 12.5 Prosty strumień

Poniżej czoła strumienia możemy wkładać moduły przetwarzające dla danego strumienia. Służy do tego funkcja i o c t l . Na rysunku 12.6 widzimy strumień z pojedynczym modułem przetwarzającym. Za pomocą dwóch strzałek pokazujemy połączenia między poszczególnymi elementami, aby podkreślić dwukierunkową naturę strumieni. Do jednego strumienia można wprowadzić wiele modułów przetwarzających. Nowy moduł jest zawsze wkładany bezpośrednio za czołem strumienia i przesuwa poprzednio wprowadzone moduły w dół. (Jest to metoda wzorowana na stosie stosującym strategię „ostatni na wejściu, pierwszy na wyjściu"). Na rysunku 12.6 umieściliśmy etykiety wskazujące kierunek w dół i w górę strumienia. Dane zapisywane do czoła strumienia są wysyłane w dół strumienia. Dane czytane przez procedurę obsługi są wysyłane w górę strumienia.

jądro

i

i

program obsługi urządzenia w górę strumienia Rys. 12.6

Strumień z modułem przetwarzającym

Moduły przetwarzania strumieniowego są podobne do procedur obsługi urządzenia, gdyż stanowią część zadań jądra systemu i typowo są dodawane podczas tworzenia jądra w procesie łączenia. Większość systemów nie pozwala na wprowadzenie do strumienia dowolnego modułu strumieniowego, jeśli nie został on wcześniej dołączony do jądra systemu. Na rysunku 11.2 pokazaliśmy typowy system terminalowy oparty na strumieniach. Element tego rysunku nazwany „funkcje odczytu i zapisu" stanowi czoło strumienia, a element „dyscyplina linii terminalu" jest modułem przetwarzania strumieniowego. W rzeczywistości moduł przetwarzania ma nazwę ldterm. (W części 7 dokumentacji AT&T, 1990d [12] oraz części 7 AT&T, 1991 [14] można znaleźć opisy różnego typu strumieni). Ze strumieni korzystamy za pomocą funkcji przedstawionych w rozdz. 3: open, c l o s e , read, w r i t e oraz i o c t l . Dodatkowo w jądrze systemu SVR3 udostępniono kolejne trzy funkcje do współpracy ze strumieniami (getmsg, putmsg i p o l l ) , a następne dwie dodano w systemie SVR4 do obsługi komunikatów o różnych pasmach priorytetu w strumieniu (getpmsg i p u t pmsg). Dalej w tym podrozdziale opiszemy te pięć nowych funkcji. Nazwa ścieżki pathname, którą podajemy w funkcji open, dla strumieni zazwyczaj' wiąże się z katalogiem /dev. Polecenie l s -1 z nazwą strumienia nie poka-' żuje nam, czy mamy do czynienia z urządzeniem strumieniowym. Wszystkie urządzenia strumieniowe są specjalnymi plikami znakowymi. Chociaż pewne dokumentacje na temat strumieni sugerują, że możemy przygotować moduły przetwarzające i po prostu wprowadzić je do strumienia, to jednak napisanie tych modułów wymaga umiejętności i uwagi, podobnie jak przygotowanie procedury obsługi urządzenia. W zasadzie wyłącznie specjalizowane programy użytkowe lub funkcje potrafią wkładać i zdejmować moduły przetwarzania strumieniowego.

12. Zaawansowane operacje wejścia-wyjścia Zanim pojawiły się strumienie, terminale były obsługiwane za pomocą mechanizmu list znakowych. (W punkcie 10.3.1 książki Bacha, 1984 [15] oraz w podrozdz. 9.6 książki Lefflera i in., 1989 [32] jest opisana implementacja tej techniki w systemach SVR2 i 4.3BSD). Dodanie do jądra nowych urządzeń znakowych zazwyczaj wiąże się z przygotowaniem procedury obsługi implementującej wszystkie szczegóły. Dostęp do takiego urządzenia jest na ogół realizowany w trybie surowym, co oznacza, że każda funkcja read i write wykonuje się bezpośrednio w procedurze obsługi. Mechanizm strumieni uporządkowuje ten sposób interakcji, umożliwiając przekazywanie danych przez komunikaty strumieniowe krążące między czołem strumienia a procedurą obsługi strumienia. Dodatkowo, jest możliwe pośrednie przetwarzanie danych przez wiele modułów.

nunikaty strumieniowe Wszelkie wprowadzanie i wyprowadzanie danych za pomocą strumieni jest realizowane przez komunikaty. Czoło strumienia oraz proces użytkowy wymieniają komunikaty, używając funkcji read, w r i t e , i o c t l , getmsg, g e t pmsg, putmsg i putpmsg. Komunikaty wędrują również w górę i w dół strumienia między czołem strumienia, modułami przetwarzającymi i procedurą obsługi. Komunikat przekazywany między czołem strumienia a procesem użytkowym składa się z (a) typu komunikatu, (b) opcjonalnej informacji sterującej i (c) opcjonalnych danych. W tabeli 12.4 pokazujemy, jak różne argumenty funkcji w r i t e , putmsg i putpmsg wpływają na typy komunikatów. Informacja sterująca oraz dane są umieszczane w strukturach s t r b u f .

nkcja ite

Tabela 12.4 Typy komunikatów strumieniowych generowanych przez funkcje w r i t e , putmsg i putpmsg Tryb generowanego komunikatu band flag Sterowanie? Dane? M_DATA (zwykły) nie stos. nie stos. tak nie stos.

itmsg

nie

nie

nie stos.

0

nie jest wysyłany komunikat, przekazuje 0

ltmsg

nie

tak

nie stos.

0

MJDATA (zwykły)

itmsg

tak

tak lub nie

nie stos.

0

M_PROTO (zwykły)

itmsg

tak

tak lub nie

nie stos.

RS_HIPRI

MPCPROTO (wysokopriorytetowy)

itmsg

nie

tak lub nie

nie stos.

RS HIPRI

błąd, EINVAL

0-255

0

błąd, EINVAL

ltpmsg

tak lub nie tak lub nie

ltpmsg

nie

nie

0-255

MSG_BAND

nie jest wysyłany komunikat, przekazuje 0

atpmsg

nie

tak

0

MSG_BAND

M_DATA (zwykły)

utpmsg

nie

tak

1-255

MSG_BAND

M DATA (pasmo priorytetowe)

utpmsg

tak

tak lub nie

0

MSG_BAND

M_PROTO (zwykły)

utpmsg

tak

tak lub nie

1-255

MSG BAND

M_PROTO (pasmo priorytetowe)

utpmsg

tak

tak lub nie

0

MSG_HIPRI M_PCPROTO (wysokopriorytetowy)

0

MSG_HIPRI błąd, EINVAL

utpmsg utpmsg

tak lub nie nie tak lub nie tak lub nie niezerowy MSG_HIPRI błąd, EINVAL

12.4. Strumienie struct strbuf in maxlen; int len; char *buf;

457 /* rozmiar bufora */ /* bieżąca liczba bajtów w buforze */ /* wskaźnik do bufora */

Gdy przesyłamy komunikat, używając funkcji putmsg lub putpmsg, wówczas pole l e n określa liczbę bajtów danych w buforze. Gdy odbieramy komunikat, używając funkcji getmsg lub getpmsg, maxlen określa rozmiar bufora (by jądro nie przepełniło go), a w polu len jądro systemu wpisuje informację, ile bajtów zapamiętano w buforze. Zobaczymy, że zerowy rozmiar komunikatu jest poprawny, a wartość -1 w polu l e n może oznaczać, że nie ma informacji sterujących lub zwykłych danych. Dlaczego musimy przekazać zarówno informacje sterujące, jak i dane? Dostarczenie tych dwóch typów danych umożliwia implementowanie interfejsów usługowych między procesem użytkowym a strumieniem. Olander, McGrath i Israel w swojej pracy [37] opisują oryginalną implementację interfejsów usługowych w Systemie V. Rozdział 5 w dokumentacji AT&T, 1990d [12] prezentuje szczegółowo interfejsy usługowe i podaje prosty przykład. Wszystko wskazuje na to, że najpopularniejszym interfejsem usługowym jest interfejs programów użytkowych TLI {Transport Layer Interface), opisany w rozdz. 7 książki Stevensa, 1990 [44]; jest on fragmentem systemu sieciowego. Innym przykładem informacji sterującej jest wysyłanie komunikatu sieciowego w trybie bezpołączeniowym (datagramu). Aby wysłać komunikat, musimy określić jego zawartość (dane) oraz adres docelowy (informacja sterująca). Jeżeli nie możemy przesłać razem informacji sterującej oraz danych, to są potrzebne pewne dodatkowe ustalenia. Za pomocą funkcji i o c t l możemy na przykład określić adres, a następnie wywołać funkcję write, by zapisać dane. W innej technice adres zajmuje pierwsze N bajtów danych zapisywanych przez funkcję w r i t e . Oddzielenie informacji sterującej od zwykłych danych i dostarczenie funkcji do obsługi tych komponentów (putmsg oraz getmsg) jest najlepszą metodą obsługi takiego problemu. Istnieje ponad 25 typów komunikatów, ale tylko kilka jest stosowanych między procesem użytkowym a czołem strumienia. Pozostałe są przekazywane w jądrze systemu w górę i w dół strumienia. (Zainteresują one zapewne tych, którzy przygotowują moduły przetwarzania strumieniowego, ale osoby' piszące programy na poziomie użytkowym mogą spokojnie to zagadnienie' pominąć). W naszych przykładach korzystamy z typowych funkcji (read, w r i t e , i o c t l , getmsg, getpmsg, putmsg i putpmsg), więc przyjrzymy się tylko trzem typom komunikatów: • M D ATA (dane użytkowe dla operacji wejścia-wyjścia), • MPROTO (informacje sterujące protokołu) i • MPCPROTO (wysokopriorytetowe informacje sterujące protokołu).

12. Zaawansowane operacje wejścia-wyjścia Każdy komunikat w strumieniu ma priorytet kolejkowania. Rozróżniamy: • komunikaty wysokopriorytetowe (najwyższy priorytet), • komunikaty o określonym paśmie priorytetu, • zwykłe komunikaty (najniższy priorytet). Zwykłe komunikaty są komunikatami o paśmie priorytetu równym 0. Komunikaty określonego pasma priorytetu mają numery pasma od 1 do 255; im wyższy numer pasma, tym wyższy priorytet. Każdy moduł strumieniowy ma dwie kolejki wejściowe. Jedna odbiera komunikaty z modułu nadrzędnego (komunikaty przekazywane w dół strumienia od czoła strumienia w kierunku procedury obsługi), a druga komunikaty z modułu podrzędnego (komunikaty przekazywane w górę strumienia od procedury obsługi do czoła strumienia). Komunikaty w kolejce wejściowej są uporządkowane według priorytetu. W tabeli 12.4 widzimy, jak różne argumenty funkcji w r i t e , putmsg i putpmsg zmieniają priorytet generowanych komunikatów. Istnieją inne typy komunikatów, których tutaj nie omawiamy. Jeśli czoło strumienia odbierze z dołu komunikat typu M_SIG, to generuje sygnał. W ten sposób moduł dyscypliny linii terminalu przesyła sygnały generowane przez terminal do pierwszoplanowej grupy procesów związanej z terminalem sterującym.

nkcje putmsg i putpmsg Do zapisywania komunikatu strumieniowego (informacji sterujących lub danych, lub obu komponentów naraz) służy jedna z funkcji: putmsg lub putpmsg. Różnica między tymi funkcjami polega na tym, że ostatnia umożliwia określanie pasma priorytetu komunikatu. #include i n t putmsg (int filedes, const s t r u c t strbuf *ctlptr, const s t r u c t strbuf *dataptr, i n t flag) ; i n t putpmsg (int filedes, const s t r u c t strbuf * ctlptr, const s t r u c t strbuf *dataptr, i n t band, i n t flag) ; Przekazują: 0, jeśli wszystko w porządku; -1 jeśli wystąpił błąd Możemy również wywołać funkcję w r i t e dla strumienia i jest to równoważne z wywołaniem funkcji putmsg bez podania żadnych informacji sterujących i z argumentem flag równym 0. Te dwie funkcje mogą wygenerować komunikaty o trzech różnych priorytetach: zwykłe, określonego pasma priorytetu oraz wysokopriorytetowe.

12.4. Strumienie

459

W tabeli 12.4 pokazujemy, jakie typy komunikatów są generowane przez różne kombinacje argumentów w tych dwóch funkcjach. Notacja „nie stos." oznacza, że danego elementu nie stosuje się. Notacja „nie" dla części sterującej komunikatu oznacza, że albo argument ctlptr jest pusty, albo pole ctlptr->len jest równe - 1 . Notacja „tak" dla części sterującej komunikatu oznacza, że argument ctlptr jest niepusty, a pole ctlptr->len jest większe lub równe 0. Zwykłe dane w komunikacie są obsługiwane tak samo (przy użyciu argumentu dataptr zamiast ctlptr). Operacje strumieniowe realizowane przez funkcję i o c t l W podrozdziale 3.14 powiedzieliśmy, że jeśli nie możemy wykonać jakiejś operacji wejścia-wyjścia, używając typowych funkcji, to funkcja i o c t l z pewnością poradzi sobie z tym problemem. To samo odnosi się do strumieni. W systemie SVR4 funkcja i o c t l może wykonać 29 różnych poleceń na strumieniu. Są one opisane w części streamio(7) podręcznika systemowego (fragment [12]). Jeśli kod źródłowy w języku C wykonuje takie operacje, to musimy włączyć plik nagłówkowy < s t r o p t s . h > . Drugi argument funkcji i o c t l , reąuest, określa, jaką chcemy wykonać operację spośród 29 możliwych. Wszystkie wartości argumentu reąuest są nazwami stałych zaczynającymi się od napisu i_. Trzeci argument zależy od wartości argumentu reąuest. Nieraz trzeci argument jest wartością całkowitoliczbową, innym razem wskaźnikiem do zmiennej typu i n t lub do struktury.

Przykład - funkcja isastream Zdarza się, że musimy określić, czy deskryptor jest związany ze strumieniem, czy nie. Jest to podobne do przypadku, gdy wywołujemy funkcję i s a t t y , aby dowiedzieć się, czy deskryptor dotyczy urządzenia terminalowego (podrozdz. 11.9). W systemie SVR4 służy do tego funkcja i s a s t r e a m . i n t isastream (int filedes); Przekazuje: 1 (prawda), jeśli urządzenie strumieniowe; 0 (fałsz) w przeciwnym przypadku

(Z jakiegoś powodu projektanci systemu SVR4 zapomnieli umieścić prototypu tej funkcji w jednym z plików nagłówkowych, nie możemy więc pokazać dyrektywy # i n c l u d e dla tej funkcji1). Jest to bardzo prosta funkcja, podobnie jak i s a t t y . Wywołuje funkcję i o c t l , a pozytywny wynik wywołania gwarantuje, że mamy do czynienia z urządzeniem strumieniowym. Program 12.8 jest jedną z możliwych impleW systemie Solaris począwszy od wersji 2.6 prototyp funkcji isastream jest umieszczony w pliku nagłówkowym (przyp. tłum.).

; i ; J ; ^

12. Zaawansowane operacje wejścia-wyjścia

mentacji tej funkcji. Używamy funkcji i o c t l z żądaniem ICANPUT, które sprawdza, czy można pisać do pasma wskazanego w trzecim argumencie (w tym przykładzie pasmo ma wartość 0). Jeżeli wywołanie funkcji i o c t l zakończy się powodzeniem, to strumień nie ulegnie zmianie. ldude iclude



istream(int

fd)

r e t u r n ( i o c t l ( f d , I_CANPUT, 0) != -1) ;

12.4. Strumienie

Jak mogliśmy się spodziewać, urządzenie / d e v / t t y jest urządzeniem strumieniowym w systemie SVR4. /dev/vidadm nie jest urządzeniem strumieniowym, lecz specjalnym plikiem znakowym, dla którego mogą być realizowane inne żądania i o c t l . Takie urządzenia przekazują błąd EINVAL, gdy zlecenie i o c t l jest nieznane. Urządzenie / d e v / n u l l jest specjalnym plikiem znakowym, dla którego nie można stosować żadnych żądań i o c t l , a więc otrzymujemy błąd ENODEV. Wreszcie, nazwa /etc/motd odnosi się do zwykłego pliku, a nie specjalnego pliku znakowego i funkcja i o c t l przekazuje klasyczny błąd ENOTTY. Nigdy nie odbierzemy błędu, który wydaje się najbardziej naturalny: ENOSTR (Device is not a stream, urządzenie nie jest strumieniem). „Nie maszyna do pisania" jest historycznym artefaktem, ponieważ jądro systemu Unix przekazuje ENOTTY, kiedy funkcja i o c t l jest sprawdzana na deskryptorze, który nie odnosi się do specjalnego urządzenia znakowego. •

Próg. 12.8 Sprawdzenie, czy deskryptor jest urządzeniem strumieniowym

Możemy zastosować próg. 12.9 do testowania tej funkcji. .clude .clude .clude

"ourhdr.h"

n(int argc, char *argv[]) int

i, fd;

for (i = 1; i < argc; i++) { printf("%s: ", argv[i]); if ( (fd = open(argv[i], O_RDONLY)) < 0) { err_ret("%s: can't open", argv[i]); continue; if (isastream(fd) == 0) err_ret("%s: not a stream", argv[i]); else err_msg("%s: streams device", argv[i]); } exit (0);

461

Przykład Jeżeli argument reąuest w funkcji i o c t l jest równy I_LIST, to system przekazuje nazwy wszystkich modułów tego strumienia, czyli tych modułów, które zostały włożone do strumienia, oraz procedury obsługi urządzenia zlokalizowanej na najwyższym poziomie. (Mówimy „procedura najwyższego poziomu", gdyż w przypadku zwielokrotniającej obsługi może istnieć więcej niż jedna procedura obsługi. Szczegóły na temat zwielokrotniających procedur obsługi opisano w rozdz. 10 dokumentacji AT&T, 1990d [12]). Trzeci argument musi być wskaźnikiem do struktury s t r l i s t . struct str_list { int sl_nmods; / J liczba pozycji w tablicy */ struct strjnlist *sl modlist; / J wskaźnik do pierwszego elementu tablicy */

Wartość pola s l _ m o d l i s t musi wskazywać pierwszy element tablicy struk- | tur typu s t r _ m l i s t , a pole sl_nmods musi zawierać liczbę pozycji' . w tablicy. '•_

Próg. 12.9 Sprawdzenie funkcji i s a s t r e a m

Uruchomienie tego programu pokazuje różne typy błędów przekazywanych przez funkcję i o c t l . $ a.out /dev/tty /dev/vidadm /dev/null /etc/motd /dev/tty: /dev/tty: streams device /dev/vidadm: /dev/vidadm: not a stream: Invalid argument /dev/null: /dev/null: not a stream: Not such device /etc/motd: /etc/motd: not a stream: Not a typewriter

struct str_mlist { char l_name[FMNAMESZ+1] ;

nazwa modułu zakończona znakiem pustym */

Stała FMNAMESZ jest zdefiniowana w pliku nagłówkowym i ma zazwyczaj wartość 8. Dodatkowy bajt w tablicy l n a m e jest przeznaczony na pusty znak.

12. Zaawansowane operacje wejścia-wyjścia

Jeżeli trzeci argument funkcji i o c t l jest równy 0, to zamiast nazw modułów jest przekazywana liczba modułów (jako wartość funkcji i o c t l ) . Użyjemy tej cechy, aby najpierw dowiedzieć się, ile modułów ma strumień, w celu zaalokowania odpowiedniej ilości struktur s t r m l i s t . W programie 12.10 widzimy zastosowanie operacji l_LIST. W przekazywanej liście modułów nie możemy rozróżnić, który element jest modułem, a który procedurą obsługi, ale drukując po kolei nazwy modułów, wiemy, że ostatnia pozycja listy jest z pewnością procedurą obsługi, gdyż właśnie ona jest umieszczona na dnie strumienia. nclude nclude nclude nclude nclude



"ourhdr.h"

it

,in(int argc, char *argv[]) int struct str list if

fd, i, list;

(argc != 2) err_quit("usage:

nmods;

(fd = o p e n ( a r g v [ l ] , O_RDONLY)) < 0) e r r s y s ( " c a n ' t open %s", a r g v [ l ] ) ; (isastream(fd) == 0) err_quit("%s is not a stream", a r g v [ l ] ) ;

(nmods = i o c t l ( f d , I_LIST, (void *) 0)) < 0) err_sys("I_LIST e r r o r for nmods"); printf("#modules = %d\n", nmods); /* alokujemy obszar dla wszystkich nazw modułów */ l i s t . s l _ m o d l i s t = calloc(nmods, s i z e o f ( s t r u c t s t r _ m l i s t ) ) ; if ( l i s t . s l j n o d l i s t == NULL) err sys("calloc e r r o r " ) ; l i s t . s l _ n m o d s = nmods; /* i pobieramy nazwy modułów */ ( i o c t l ( f d , I_LIST, s l i s t ) < 0) err_sys("I_LIST e r r o r for l i s t " ) ; /* drukujemy nazwy modułów */ for (i = 1; i swojego terminalu), to musi on powiadomić potomka, by ten również zakończył pracę. Moglibyśmy użyć jakiegoś sygnału (np. SIGUSR1), ale takie rozwiązanie komplikuje nieco program. Możemy zastosować w jednym programie nieblokujące wejście-wyjście. Aby to zrealizować, deklarujemy oba deskryptory jako nieblokujące i wywołujemy funkcję r e a d dla pierwszego z deskryptorów. Jeśli dane są gotowe, to czytamy je i przetwarzamy. Jeśli nie ma danych, to funkcja natychmiast powraca. Następnie wykonujemy te same czynności dla drugiego deskryptora. Potem odczekujemy pewien czas (powiedzmy kilka sekund) i ponawiamy próbę odczytu z pierwszego deskryptora. Ten rodzaj pętli jest nazywany od-

12.5. Zwielokrotnianie wejścia-wyjścia

469

pytywaniem. Jego wadąjest strata czasu procesora. Przez większość czasu nie ma żadnych danych do odczytu, a więc tracimy czas wywołując funkcję read. Musimy również zgadywać, ile czasu należy oczekiwać przed powtórzeniem przedstawionej sekwencji operacji. Mimo że odpytywanie działa we wszystkich systemach implementujących nieblokujące wejście-wyjście, to należy tej metody unikać w systemach wielozadaniowych. Inna technika jest nazywana asynchronicznym wejściem-wyjściem. Aby ją zrealizować, zlecamy jądru systemu, by powiadamiało nas sygnałem, gdy deskryptor jest gotowy do odczytu. Wiążą się z tym dwa problemy. Po pierwsze, nie wszystkie systemy wspierają tę właściwość (nie jest ona częścią normy POSIX, ale prawdopodobnie będzie w przyszłości). System SVR4 stosuje w tej technice sygnał SIGPOLL, ale działa on, tylko gdy deskryptor odnosi się do urządzenia strumieniowego. System 4.3+BSD używa sygnału SIGIO mającego podobne ograniczenie - działa wyłącznie z deskryptorami odnoszącymi się do urządzeń terminalowych lub sieciowych. Drugi problem wiąże się z tym, że dla każdego procesu w systemie istnieje tylko jeden z takich sygnałów (SIGPOLL lub SIGIO). Jeżeli uaktywnimy taki sygnał dla dwóch deskryptorów (w omawianym przykładzie dla obu deskryptorów odczytu), to na podstawie odebranego sygnału nie dowiemy się, który deskryptor jest gotowy. Aby stwierdzić to, musimy nadal stosować tę samą technikę co poprzednio: ustawić tryb bez blokowania dla deskryptorów i próbować odczytać dane po kolei z każdego z nich aż do skutku. Asynchroniczne wejście-wyjście opiszemy krótko w podrozdz. 12.6. Lepszą techniką jest zwielokrotnianie wejścia-wyjścia. W tym celu tworzymy listę deskryptorów, które nas interesują (zazwyczaj więcej niż jeden deskryptor) i wywołujemy funkcję, która nie powróci, dopóki jeden z deskryptorów nie będzie gotowy do wykonania operacji wejścia-wyjścia. Po powrocie z funkcji wiemy, które deskryptory są gotowe. Zwielokrotnianie wejścia-wyjścia nie jest jeszcze zdefiniowane w normie POSIX. Funkcja s e l e c t jest dostarczana w systemach SVR4 oraz 4.3+BSD i służy do realizacji zwielokrotnionego wejścia-wyjścia. Funkcja p o l l jest dostępna tylko w systemie SVR4. W istocie SVR4 implementuje funkcję s e l e c t za pomocą funkcję p o l l . Zwielokrotnianie wejścia-wyjścia pojawiło się w systemie 4.2BSD razem z funkcją s e l e c t . Ta funkcja zawsze współpracowała z deskryptorami dowolnego typu, alę główne jej zastosowanie dotyczyło wejścia-wyjścia terminalowego oraz sieciowego. W systemie SVR3 razem ze strumieniami dodano funkcję p o l l . Dopóki nie pojawił się system SVR4, funkcja p o l l obsługiwała wyłącznie urządzenia strumieniowe. W systemie SVR4 możemy stosować funkcję p o l l dla dowolnego deskryptora.

Możliwość przerwania funkcji s e l e c t i p o l l W systemie 4.2BSD wprowadzono automatyczne wznawianie przerwanych funkcji systemowych (podrozdz. 10.5), ale funkcja s e l e c t nigdy nie podlegała tej procedurze. Tak samo jest w systemie 4.3+BSD (oraz większości sys-

12. Zaawansowane operacje wejścia-wyjścia temów o korzeniach berkelejowskich), chociaż zdefiniowano tam opcję SA_RESTART. Jednak w systemie SVR4, jeżeli podamy sygnalizator SARESTART, to nawet funkcje p o l l i s e l e c t są automatycznie powtarzane. Aby temu zapobiec, w naszych programach uruchamianych w systemie SVR4 będziemy używać funkcji s i g n a l _ i n t r (próg. 10.13), zawsze gdy sygnał może przerwać wywołania tych funkcji.

12.5.1 Funkcja s e l e c t Funkcja s e l e c t służy do zwielokrotniania wejścia-wyjścia w systemach SVR4 oraz 4.3+BSD. Argumenty, które przekazujemy funkcji s e l e c t , zawierają pewne informacje dla jądra systemu: 1. Jakie deskryptory nas interesują. 2. Jakie warunki nas interesują dla poszczególnych deskryptorów. (Czy chcemy czytać z danego deskryptora? Czy chcemy zapisywać na dany deskryptor? Czy interesują nas warunki wyjątkowe dla danego deskryptora?). 3. Jak długo chcemy oczekiwać. (Możemy czekać aż do skutku, przez określony czas lub wcale nie czekać). Po powrocie z funkcji s e l e c t dowiadujemy się 1. Ile deskryptorów jest gotowych. 2. Jakie deskryptory są gotowe dla poszczególnych warunków (odczyt, zapis lub warunek wyjątkowy). Dysponując tymi informacjami, możemy wywołać odpowiednią funkcję wejścia-wyjścia (na ogół r e a d lub w r i t e ) , która z pewnością nie zablokuje się. łtinclude #include #include

/* typ danych fd_set */ /* struct timeval */ /* prototyp funkcji może być w tym pliku */

int select (int maxfdpl, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, s t r u c t t i m e v a l *tvptr) ;

Przekazuje: liczbę gotowych deskryptorów, jeśli wszystko w porządku; 0, jeśli jest przekroczony czas oczekiwania; - 1 , jeśli wystąpił błąd

Przyjrzyjmy się najpierw ostatniemu argumentowi. Określa on, jak długo chcemy oczekiwać.

471

12.5. Zwielokrotnianie wejścia-wyjścia struct timeval long tv_sec; long tv usec;

/* sekundy */ /* i mikrosekundy */

Mamy trzy sytuacje. tvptr == NULL Nieskończone oczekiwanie. Takie oczekiwanie może zostać przerwane przez przechwycenie sygnału. Funkcja powraca, jeśli jest gotowy jeden ze wskazanych deskryptorów lub jeśli proces przechwycił sygnał. Jeżeli proces przechwycił sygnał, to funkcja s e l e c t przekazuje wartość - 1 , a zmienna e r r n o jest równa EINTR. tvptr—>tv_sec == 0 && tvptr—>tv_usec == 0 Bez żadnego oczekiwania. Wszystkie wskazane deskryptory są sprawdzane, po czym funkcja natychmiast powraca. Jest to sposób odpytywania systemu, aby dowiedzieć się o stan wielu deskryptorów, bez zablokowania w funkcji s e l e c t . tvptr—>tv_sec ! = 0 | | tvptr->tv_usec ! = 0 Oczekiwanie przez określoną liczbę sekund i mikrosekund. Funkcja powraca, jeśli jest gotowy jeden ze wskazanych deskryptorów lub jeśli zostanie przekroczony czas oczekiwania. Jeżeli czas oczekiwania minie, zanim będzie gotowy jakiś deskryptor, to funkcja uzyskuje wartość 0. (Jeżeli system nie stosuje mikrosekundy jako jednostki rozdzielczości czasu, to wartość tvptr-łtv_usec jest zaokrąglana do najbliższej stosowanej jednostki czasu). Tak samo jak dla pierwszej sytuacji, to oczekiwanie może zostać przerwane przez sygnał.

Środkowe trzy argumenty, readfds, writefds i exceptfds, są wskaźnikami do zbioru deskryptorów. Te trzy zbiory określają, które deskryptory nas interesują dla poszczególnych warunków (odczyt, zapis, warunek wyjątkowy). Do przechowywania zbioru deskryptorów służy typ danych f d s e t . W konkretnych implementacjach ten typ danych jest tak dopasowany, by przechowywał po jednym bicie dla każdego możliwego deskryptora. Możemy trakto-i wać go poglądowo jak dużą tablicę (rys. 12.9). i Działania na danych typu f d s e t ograniczają się do (a) alokowania: zmiennej tego typu, (b) przypisywania zmiennej tego typu wartości innej zmiennej tego samego typu lub (c) zastosowania do zmiennej tego typu jed i nego z poniższych czterech makr: FD ZERO(fd set * fdset) ; /* wyzerowanie wszystkich bitów w fdset *' j FD_SET(int fd, fd_set * fdset) ; /* ustawienie b i t u J odpowiadającego fd w fdset */ > FD_CLR(int fd, fd_set * fdset) ; /* wyzerowanie b i t u ^ odpowiadającego fd w fdset */' FD_ISSET (int fd, fd_set * fdset) ; /* sprawdzenie b i t u odpowiadającego fd w fdset */1

12. Zaawansowane operacje wejścia-wyjścia tdO fdl readfds

0

fd2

0

0 po jednym bicie dla każdego możliwego deskryptora

writefds

0

0

0 —

exceptfds

0

0

I

0

Rys. 12.9 Określenie deskryptorów odczytu, zapisu i warunków wyjątkowych dla funkcji s e l e c t

Po zadeklarowaniu deskryptora, np. fd_set int

rset; fd;

musimy wyzerować zbiór za pomocą makra FD_ZERO.

< s y s / t y p e s . h > , która określa maksymalną liczbę deskryptorów (na ogół 256 lub 1024), ale dla większości programów użytkowych ta wartość jest zbyt duża. W rzeczywistości większość aplikacji stosuje deskryptory o numerach od 3 do 10. (Istnieją aplikacje, które potrzebują większej liczby deskryptorów, ale nie są to typowe programy uniksowe). Dzięki określeniu największego deskryptora, który nas interesuje, jądro systemu nie musi obsługiwać setek niepotrzebnych bitów w zbiorze deskryptorów podczas poszukiwania włączonych bitów. Na przykład na rys. 12.10 widzimy istniejące dwa zbiory deskryptorów po wykonaniu poniższego kodu fd_set readset, writeset; FD_ZERO(Sreadset); FD ZEROf&writeset); FD_SET(0, FD_SET(3, FD_SET(1, FD SET(2,

Sreadset); sreadset); Swriteset); swriteset);

select (4, sreadset, Swriteset, NOLL, NULL);

FD_ZERO(&rset);

Następnie ustawiamy w zbiorze bity dla poszczególnych deskryptorów, które nas interesują:

Musieliśmy dodać do maksymalnego deskryptora wartość 1, ponieważ deskryptory zaczynają się od 0, a pierwszy argument jest w rzeczywistości liczbą numerów deskryptorów do sprawdzenia (począwszy od deskryptora 0).

FD_SET(fd, &rset); FD_SET(STDIN_FILENO, Srset);

Po powrocie z funkcji s e l e c t możemy użyć makra FD_ISSET, by sprawdzić, czy jest ustawiony konkretny bit w zbiorze: if (FD ISSET(fd, srset))

473

12.5. Zwielokrotnianie wejścia-wyjścia

{

Dowolny (lub wszystkie) środkowe argumenty funkcji s e l e c t (wskaźniki do zbioru deskryptorów) mogą być pustymi wskaźnikami, jeśli dany warunek nas nie interesuje. Gdy wszystkie trzy wskaźniki są puste, to funkcja s e l e c t działa jak licznik o wyższej dokładności niż oferowana przez funkcję s l e e p . (Przypominamy, że zgodnie z opisem w podrozdz. 10.19 funkcja s l e e p oczekuje przez zadaną całkowitą liczbę sekund. Stosując funkcję s e l e c t , możemy oczekiwać z dokładnością większą niż jedna sekunda; rzeczywista rozdzielczość zależy od zegara systemowego). Ćwiczenie 12.6 pokazuje taką funkcję. Pierwszy argument funkcji s e l e c t , mcafdpl, oznacza maksymalną liczbę deskryptorów plus 1. Otrzymujemy go, podając największy z deskryptorów, który nas interesuje w dowolnym z trzech zbiorów i dodając do tej wartości 1. Możemy po prostu nadać pierwszemu argumentowi wartość FDSETSIZE, czyli wartość stałej zdefiniowanej w pliku nagłówkowym

fdO fdl fd2 fd3 readset:

1

0

0 I—*• żaden z tych bitów nie jest przeglądany

writeset:

maxfdpl =4 Rys. 12.10 Przykład zbioru deskryptorów dla funkcji s e l e c t

Funkcja s e l e c t ma jedną z trzech wartości powrotu. 1. Wartość powrotu równa -1 oznacza błąd. Taka sytuacja zdarza się, gdy proces przechwyci sygnał, zanim jakiś z określonych deskryptorów będzie gotowy. 2. Wartość powrotu równa 0 oznacza, że żaden deskryptor nie jest gotowy. Zdarza się tak, gdy czas oczekiwania minie, zanim jakiś z określonych deskryptorów będzie gotowy. 3. Dodatnia wartość powrotu przekazuje liczbę deskryptorów, które są gotowe. W tym przypadku wszystkie ustawione bity w trzech zbiorach deskryptorów wskazują deskryptory, które są gotowe.

12. Zaawansowane operacje wejścia-wyjścia Zbiory deskryptorów powinny być sprawdzane, tylko jeśli wartość powrotu jest większa niż zero. Stan zbiorów deskryptorów po powrocie z wywołania funkcji, gdy pojawił się sygnał lub przedawnił się licznik czasu, zależy od implementacji. Rzeczywiście, jeśli minie czas oczekiwania w systemie 4.3+BSD, to zbiory deskryptorów nie zmieniają się, natomiast w systemie SVR4 zbiory te są wówczas zerowane. Istnieje jeszcze inna rozbieżność między implementacjami funkcji s e l e c t w systemach SVR4 i BSD. Systemy BSD zawsze przekazują sumę liczby gotowych deskryptorów w poszczególnych zbiorach. Jeżeli ten sam deskryptor jest gotowy w dwóch zbiorach (np. w zbiorze odczytu i zbiorze zapisu), to jest on liczony podwójnie. W systemie SVR4 niestety uległo to zmianie i jeśli ten sam deskryptor jest gotowy w wielu zbiorach, to jest liczony raz. Ponownie więc widzimy problemy, z którymi się musimy borykać, dopóki takie funkcje jak s e l e c t nie zostaną zestandaryzowane w normie POSIX.

Musimy precyzyjnie określić, co oznacza „gotowy". 1. Deskryptor w zbiorze odczytu (readfds) jest gotowy, jeżeli wywołanie funkcji r e a d dla tego deskryptora nie zablokuje się. 2. Deskryptor w zbiorze zapisu (writefds) jest gotowy, jeżeli wywołanie funkcji w r i t e dla tego deskryptora nie zablokuje się. 3. Deskryptor w zbiorze warunków wyjątkowych (exceptfds) jest gotowy, jeżeli istnieje jakiś warunek wyjątkowy dla tego deskryptora. Obecnie warunek wyjątkowy wiąże się z (a) nadejściem danych pozapasmowych na połączeniu sieciowym lub (b) pewnymi zdarzeniami dotyczącymi pseudoterminalu pracującego w trybie pakietowym. (Podrozdział 15.10 w książce Stevensa [44] opisuje tę ostatnią sytuację). Zawsze pamiętajmy, że na działanie funkcji s e l e c t nie ma wpływu to, czy dla danego deskryptora obowiązuje blokowanie, czy nie. Oznacza to, że jeśli mamy nieblokujący deskryptor, z którego chcemy czytać dane, i wywołujemy funkcję s e l e c t z wartością czasu oczekiwania równą 5 sekund, to funkcja s e l e c t zablokuje się maksymalnie na 5 sekund. Podobnie, jeśli podamy nieskończony czas oczekiwania, funkcja s e l e c t zablokuje się do czasu, gdy będą gotowe dane lub do przechwycenia sygnału przez proces. Gdy natrafimy na znacznik końca pliku na jakimś deskryptorze, wówczas jest on traktowany przez funkcję s e l e c t jako deskryptor gotowy do odczytu. Następne wywołanie funkcji r e a d przekazuje wartość 0, co jest typową metodą informowania o końcu pliku w systemie Unix. (Wiele osób błędnie zakłada, że funkcja s e l e c t po napotkaniu znacznika końca pliku wskaże warunek wyjątkowy na deskryptorze).

12.5.2

Funkcja p o l l

Funkcja p o l l w systemie SVR4 jest podobna do funkcji s e l e c t , chociaż ma ona zupełnie inny interfejs programisty. Jak zobaczymy, funkcja p o l l jest powiązana z systemem strumieni, mimo że w systemie SVR4 możemy jej używać z deskryptorem dowolnego typu.

12.5. Zwielokrotnianie wejścia-wyjścia

475

#include < s t r o p t s . h > #include i n t p o l l ( s t r u c t pollfd fdarray[ ] , unsigned long nfds, i n t timeout) ; Przekazuje: liczbę gotowych deskryptorów; 0, jeśli jest przekroczony czas oczekiwania; - 1 , jeśli wystąpił błąd

Zamiast tworzyć zbiory deskryptorów dla poszczególnych warunków (odczyt, zapis, warunek wyjątkowy), jak było w funkcji s e l e c t , w funkcji p o l l tworzymy tablicę struktur typu p o l l f d , a każdy element tej tablicy określa numer deskryptora oraz warunek, który nas interesuje dla tego deskryptora. struct pollfd { int fd;

/* deskryptor pliku do sprawdzenia lub wartość ujemna, by pominąć */ short events; /* interesujące nas zdarzenia związane z fd */ short revents; /* zdarzenia, które zaszły dla fd */

Argument nfds określa liczbę elementów w tablicy fdarray. Z nieznanego powodu w systemie SVR3 liczba elementów w tablicy jest określana przez typ danych unsigned long, co wydaje się przesadne. W podręczniku SVR4 [12], w prototypie funkcji p o l l drugi argument ma typ s i z e _ t . (Przypominamy elementarne systemowe typy danych, tab. 2.8). Jednak prototyp w pliku nagłówkowym nadal deklaruje drugi argument jako unsigned long. W dokumentacji SVID dla systemu SVR4 AT&T, 1989 [8] pierwszym argumentem funkcji p o l l jest s t r u c t p o l l f d fdarray [ ], natomiast podręcznik SVR4 [12] podaje, że argument ten ma postać s t r u c t p o l l f d * fdarray. W języku C obie deklaracje są równoważne. Używamy w naszych przykładach pierwszej deklaracji, by wzmocnić fakt, że fdarray wskazuje tablicę struktur, a nie jest wskaźnikiem do pojedynczej struktury.

Dla każdego elementu tablicy musimy ustalić wartość pola e v e n t s , używając jednej lub kilku wartości z tab. 12.5. W ten sposób powiadamiamy,] jądro systemu, jakie zdarzenia nas interesują dla danego deskryptora. Po po- \ wrocie z funkcji pole r e v e n t s zawiera zapisane przez jądro zdarzenia, któfe j zaszły dla danego deskryptora. (Zwróćmy uwagę, że funkcja p o l l nie zmie- ' nia wartości pola events, jest więc inaczej niż w funkcji s e l e c t , która mo-^ dyfikowała swoje argumenty, aby wskazać gotowe deskryptory). Pierwsze cztery wiersze tab. 12.5 dotyczą sprawdzenia, czy są dane doi odczytu, kolejne trzy sprawdzenia, czy można pisać dane, a ostatnie trzy po- \ zwalają dowiedzieć się o warunki wyjątkowe. ^ Ostatnie trzy wartości z tab. 12.5 są ustawiane przez jądro systemu pod- \ czas powrotu z funkcji. Są one przekazywane w polu r e v e n t s , jeżeli zajdzie zdarzenie, nawet gdy nie zostały wskazane w polu events.

12. Zaawansowane operacje wejścia-wyjścia

12.6. Asynchroniczne wejście-wyjście

Tabela 12.5 Sygnalizatory events i revents w funkcji p o l l wa

Dane wejściowe dla e v e n t s

Wynik w revents

LLIN





Wszystkie dane oprócz wysokopriorytetowych mogą być czytane bez blokowania.

LLRDNORM





Normalne dane (pasmo 0) mogą być czytane bez blokowania.

LLRDBAND





Dane z niezerowego pasma priorytetu mogą być czytane bez blokowania.

LLPRI





Dane wysokopriorytetowe mogą być czytane bez blokowania.

LLOUT





Normalne dane mogą być zapisywane bez blokowania.

LLWRNORM





Tak samo jak POLLOUT.

LLWRBAND





Dane do niezerowego pasma priorytetu mogą być zapisywane bez blokowania.

LLERR



Wystąpił błąd.

LLHUP



Wystąpiło zawieszenie.

LLNVAL



Deskryptor nie odnosi się do otwartego pliku.

J

lony przez timeout minie, zanim będzie gotowy jakiś z interesujących • nas deskryptorów, to wartość powrotu jest równa 0. (Jeżeli system nie stosuje rozdzielczości milisekundowej, to wartość timeout jest zaokrąglana w górę do najbliższej stosowanej jednostki czasu).

Opis

Ważne jest odróżnianie znacznika końca pliku od zawieszenia. Jeżeli wprowadzając dane z terminalu wpiszemy znak końca pliku, to zostanie włączony sygnalizator POLLIN, więc możemy za pomocą funkcji r e a d odczytać znacznik końca pliku (read przekaże 0). W polu r e v e n t s nie jest ustawiany sygnalizator POLLHUP. Natomiast jeżeli podczas czytania danych z modemu zawiesi się linia telefoniczna, to otrzymamy powiadomienie POLLHUP. Tak jak dla funkcji s e l e c t , to czy deskryptor jest obsługiwany jako blokujący, czy nieblokujący nie ma wpływu na blokowanie funkcji p o l l .

12.6

Asynchroniczne wejście-wyjście Używając funkcji s e l e c t oraz p o l l , które omówiliśmy w poprzednim podrozdziale, realizujemy synchroniczną formę powiadamiania. System nie przekazuje nam żadnej informacji, dopóki nie otrzyma od nas odpowiedniego zapytania (wywołanie funkcji s e l e c t lub p o l l ) . Jak widzieliśmy w rozdz. 10, sygnały są asynchroniczną formą powiadamiania o istotnych zdarzeniach. Systemy SVR4 oraz 4.3+BSD stosują asynchroniczne wejście-wyjście, używając sygnału (SIGPOLL w SVR4 i SIGIO w 4.3+BSD), aby poinformować proces, że zaszło jakieś zdarzenie na deskryptorze, którego stanem jesteśmy zainteresowani.

Jeżeli deskryptor jest w stanie zawieszenia (POLLHUP), to nie możemy już do niego pisać danych. Nie oznacza to jednak, że nie można z niego czytać danych. Ostatni argument funkcji p o l l określa, jak długo chcemy oczekiwać. Tak samo jak w funkcji s e l e c t , mamy trzy różne przypadki. timeout == INFTIM

Widzieliśmy, że w systemie SVR4 funkcje s e l e c t i p o l l obsługują dowolne deskryptory. W systemie 4BSD funkcja s e l e c t zawsze działała ze wszystkimi deskryptorami. Jednak w przypadku asynchronicznego wejścia-wyjścia wprowadzono pewne restrykcje. W systemie SVR4 asynchroniczne wejście-wyjście można realizować tylko dla urządzeń strumieniowych. Natomiast w systemie 4.3+BSD ta technika jest zaimplementowana tylko dla terminali i urządzeń sieciowych.

Nieskończone oczekiwanie. Stała INFTIM jest zdefiniowana w pliku nagłówkowym < s t r o p t s . h>, a jej wartością jest najczęściej - 1 . W tym przypadku powrót z funkcji nastąpi, gdy jest gotowy jeden z określonych deskryptorów lub gdy proces przechwyci sygnał. Po przechwyceniu sygnału funkcja p o l l przekazuje - 1 , a zmienna e r r n o ma wartość EINTR.

Jednym z ograniczeń asynchronicznego wejścia-wyjścia, np. w implementacjach w systemach SVR4 i 4.3+BSD, jest istnienie jednego konkretnego sygnału w jednym procesie. Jeżeli uaktywnimy więcej niż jeden deskryptor, by obsługiwał asynchroniczne wejście-wyjście, to po odebraniu sygnału nie możemy dowiedzieć się, z jakim deskryptorem był związany sygnał.

timeout == 0 Bez żadnego oczekiwania. Wszystkie wskazane deskryptory są sprawdzane, po czym funkcja natychmiast powraca. Jest to sposób odpytywania systemu, aby dowiedzieć się o stan wielu deskryptorów, bez zablokowania w funkcji p o l l .

; 0 Oczekiwanie przez określoną w argumencie timeout liczbę milisekund. Powrót z funkcji nastąpi, gdy jest gotowy jeden z określonych deskryptorów lub gdy licznik timeout przekroczy termin. Jeżeli czas usta-

477

J

System V Wydanie 4

j

Asynchroniczne wejście-wyjście w systemie SVR4 jest fragmentem podsys- ' temu strumieni. Można je stosować wyłącznie w odniesieniu do urządzeń , strumieniowych. Sygnałem do obsługi asynchronicznego wejścia-wyjścia jest w tym systemie SIGPOLL. ,

12. Zaawansowane operacje wejścia-wyjścia

Aby włączyć asynchroniczne wejście-wyjście dla danego urządzenia strumieniowego, musimy wywołać funkcję i o c t l , podając w drugim argumencie {reąuest) wartość ISETSIG. Trzeci argument jest wartością całkowitą utworzoną za pomocą jednej lub kilku stałych z tab. 12.6. Stałe te są zdefiniowane w pliku nagłówkowym < s t r o p t s . h>. Tabela 12.6 Warunki do wygenerowania sygnału SIGPOLL Stała

Opis

S__INPUT

Nadszedł komunikat nie będący komunikatem wysokopriorytetowym.

S RDNORM

Nadszedł zwykły komunikat.

S_RDBAND

Nadszedł komunikat w paśmie niezerowym.

S BANDURG

Jeśli podano tę stałą razem z S RDBAND, to po nadejściu komunikatu w paśmie niezerowym jest generowany sygnał SIGURG zamiast SIGPOLL.

S

Nadszedł komunikat wysokopriorytetowy.

HIPRI

S OUTPUT

Kolejka zapisu nie jest już pełna.

S WRNORM

Tak jak S_OUTPUT.

S WRBAND

Można przesłać komunikat do pasma niezerowego.

S MSG

Strumień sygnalizuje nadejście komunikatu zawierającego sygnał SIGPOLL.

S_ERROR

Nadszedł komunikat M ERROR.

S HANGUP

Nadszedł komunikat M HANGUP.

W opisach stałych w tab. 12.6 używamy określenia „nadszedł", by powiedzieć, że komunikat znalazł się w czole strumienia kolejki odczytu. Oprócz wywołania funkcji i o c t l , aby ustalić, jakie warunki mają generować sygnał SIGPOLL, musimy również zarejestrować procedurę obsługi sygnału. Przypominamy, że zgodnie z tab. 10.1, domyślną reakcją na sygnał SIGPOLL jest zakończenie procesu, musimy więc ustalić procedurę obsługi sygnału przed wywołaniem funkcji i o c t l .

12.6.2 System 4.3+BSD Asynchroniczne wejście-wyjście w systemie 4.3+BSD używa dwóch różnych sygnałów: SIGIO oraz SIGURG. Pierwszy jest ogólnym sygnałem asynchronicznego wejścia-wyjścia, drugi jest stosowany do powiadamiania procesu o nadejściu danych pozapasmowych na połączeniu sieciowym. Aby odbierać sygnał SIGIO, musimy wykonać parę kroków. 1. Ustalić procedurę obsługi sygnału, wywołując funkcję s i g n a l lub sigaction. 2. Ustalić, który identyfikator procesu lub identyfikator grupy procesów ma odbierać sygnały dla tego deskryptora. Służy do tego wywołanie f c n t l z poleceniem F_SETOWN (podrozdz. 3.13).

12.7. Funkcje readv oraz writev

479

3. Włączyć asynchroniczne wejście-wyjście dla danego deskryptora, czyli wywołując funkcję f c n t l z poleceniem F_SETFL, uaktywnić sygnalizator stanu pliku O_ASYNC (tab. 3.2). Krok 3 można realizować tylko dla deskryptorów odnoszących się do terminali lub urządzeń sieciowych - jest to fundamentalne ograniczenie techniki asynchronicznego wejścia-wyjścia w systemie 4.3+BSD. Dla sygnału SIGURG musimy wykonać tylko kroki 1 i 2. Ten sygnał jest generowany tylko dla deskryptorów odnoszących się do połączeń sieciowych obsługujących dane pozapasmowe.

12.7 Funkcje readv oraz w r i t e v Funkcje readv oraz w r i t e v realizują w jednej funkcji systemowej odczyt i zapis z wielu nieciągłych buforów. Są często nazywane rozproszonym odczytem i zbierającym zapisem. #include #include ssize_t readv(int filedes, const s t r u c t iovec *iov[] , i n t iovcnt) ; ssize_t w r i t e v ( i n t filedes, const s t r u c t iovec *iov[] , i n t iovcnt) ; Obie przekazują: liczbę bajtów odczytanych lub zapisanych; - 1 , jeśli wystąpił błąd

Drugi argument obu funkcji jest wskaźnikiem do tablicy struktur iovec: struct iovec { void *iov_base; int iov_len;

/* adres początku bufora */ /* rozmiar bufora */

Argument iovcnt określa liczbę elementów w tablicy iov.

I i

Obie funkcje pochodzą z systemu 4.2BSD. Są obecnie implementowane w systemie SVR4? .

Ich prototypy oraz używana przez nie struktura iovec ponownie pokazują różnice, ] z którymi mamy do czynienia, gdy funkcje nie zostały zestandaryzowane przez takie i normy jak POSIX czy XPG3. Możemy sprawdzić, że każda z definicji tej funkcji, w dokumentacjach SVR4 Programmer's Manuał AT&T, 1990e [13] i SVID for SVR4 j AT&T, 1989 [8] oraz w plikach nagłówkowych w systemach SVR4^ i 4.3+BSD, jest inna! Częściowo wiąże się to z faktem, że definicja SVID oraz SVR4 ' Programmer's Manuał opierają się na wersji normy POSDC.l z 1988 roku, a nie wersji"* z roku 1990. Pokazane powyżej, prototyp oraz definicja struktury, są zgodne z defini- • cjami funkcji read i w r i t e w normie POSIX.l: adresy buforów są typu void *, rozmiary buforów typu s i z e _ t , a wartość powrotu typu s s i z e t. '

12. Zaawansowane operacje wejścia-wyjścia

12.7. Funkcje readv oraz writev

2. Zaalokowanie własnego bufora, wystarczająco dużego, by pomieścił oba bufory, i skopiowanie danych z obu buforów do nowego obszaru pamięci. Następnie wywołanie jeden raz funkcji w r i t e w celu wypisania danych z nowego bufora. 3. Wywołanie funkcji w r i t e v w celu wypisania obu buforów.

Zwróćmy uwagę na to, że drugi argument funkcji readv ma kwalifikator const. Pojawia się on w prototypie funkcji w systemie 4.3+BSD, nie ma go natomiast w systemie SVR4. Kwalifikator const dotyczy tylko funkcji readv, gdyż pola struktury iovec nie są przez nią modyfikowane; funkcja ta zmienia jedynie zawartość lokalizacji pamięci wskazywanej przez pola iov_base. W systemach SVR4 oraz 4.3BSD maksymalną wartością licznika iovcnt jest 16. System 4.3+BSD definiuje stałą UIO_MAXIOV, która obecnie ma wartość 1024. Zgodnie z definicją SVID stalą IOV_MAX stanowi podobne ograniczenie w Systemie V, jednak nie jest ona zdefiniowana w żadnym pliku nagłówkowym.

Na rysunku 12.11 pokazujemy związki argumentów obu funkcji ze strukturą iovec. Funkcja w r i t e v tworzy dane wyjściowe na podstawie buforów w kolejności: iov[0], iov[l] aż do iov[iovcnt-l]. Funkcja w r i t e v przekazuje całkowitą liczbę wyprowadzonych bajtów, zazwyczaj jest to suma rozmiarów wszystkich buforów. iov [ 0 ] . iov_base iov[0] . iov_len rozmiarO iov[l] . iov_base iov [ 1 ] . iov len rozmiar 1

buforO

H .

rozmiar 1 *•

leni

H

buforl

H — rozmiar 1 — H ».

iov[iovcnt-l]. iov_base iov[iovcnt—l]. iov_len

•>

buforl •

Rys. 12.11 Struktura iovec dla funkcji readv oraz w r i t e v

Funkcja readv wczytuje dane po kolei do wielu buforów. Zawsze wypełnia całkowicie jeden bufor, zanim przejdzie do wprowadzania danych do następnego bufora. Funkcja readv przekazuje całkowitą liczbę odczytanych bajtów. Jeżeli nie ma więcej danych i odczytano znacznik końca pliku, funkcja przekazuje wartość 0.

zykład W podrozdziale 16.7 w funkcji _ d b _ w r i t e i d x musimy wypisać do pliku po kolei zawartości dwóch buforów. Drugi wyprowadzany bufor jest argumentem przekazywanym przez wywołującego, a pierwszy, przez nas tworzony, zawiera rozmiar drugiego bufora oraz wskaźnik pozycji w pliku, gdzie są umieszczone inne informacje. Jest parę sposobów realizacji takiego zadania. 1. Dwukrotne wywołanie funkcji w r i t e , jeden raz dla każdego bufora.

481

W naszym rozwiązaniu w podrozdz. 16.7 korzystamy z funkcji w r i t e v , ale bardzo pouczające jest porównanie wyników z dwoma innymi podejściami. W tabeli 12.7 widzimy rezultaty uzyskane przy użyciu opisanych przed chwilą metod. Tabela 12.7 Wyniki pomiaru czasu w celu porównania wydajności funkcji w r i t e v z innymi technikami SPARC Operacja

80386

Użytkownik

System

Zegar

Użytkownik

System

Zegar

dwa wywołania w r i t e

0,2

7,2

17,2

0,5

13,1

13,7

kopiowanie bufora, potem jedno wywołanie w r i t e

0,5

4,4

17,2

0,7

7,3

8,1

jedno wywołanie w r i t e v

0,3

4,6

17,1

0,3

7,8

8,2

Nasz program testujący wypisywał 100 bajtów nagłówka, a następnie 200 bajtów danych. Powtórzyliśmy te operacje 10 000 razy, tworząc plik o rozmiarze 3 miliony bajtów. Przygotowaliśmy trzy wersje programu, a każdy program mierzył trzy czasy: czas użytkownika procesu, systemowy czas procesu oraz czas zegarowy. Wszystkie czasy mierzyliśmy w sekundach. Zgodnie z naszymi oczekiwaniami, gdy wywołujemy funkcję w r i t e dwa razy, czas systemowy zwiększa się niemal dwukrotnie w porównaniu zjednokrotnym wywołaniem w r i t e lub writev. Jest to spójne z wynikami zaprezentowanymi w tab. 3.1. Zauważmy również, że zawsze suma czasów procesora (w trybach użytkownika i systemowym) jest prawie taka sama, niezależnie czy najpierw kopiujemy bufor, a następnie wywołujemy funkcję write, czy realizujemy pojedyncze, wywołanie writev. Jest tak, ponieważ różnie rozkłada się czas użytkownika procesu (większy, gdy kopiujemy bufor) oraz systemowy czas procesu (większy przy wywołaniu writev). Otrzymaliśmy sumaryczny czas procesora równy 4,9 sekund na komputerze SPARC oraz około 8 sekund na platformie 80386. Z tabeli 12.7 wynika jeszcze jedna ważna uwaga, która nie wiąże się z naszą dyskusją na temat funkcji r e a d v i w r i t e v . Czas zegarowy w używanym w tym przykładzie systemie SPARC bardzo mocno zależał od szybkości dysku (czas zegarowy jest dwukrotnie większy niż czas procesora, a testy były wykonywane na nieobciążonym systemie), natomiast w systemie 80386 na czas zegarowy ma przede wszystkim wpływ szybkość procesora (czas zegarowy jest niemal taki sam jak czas procesora). •

i

12. Zaawansowane operacje wejścia-wyjścia Podsumowując, zawsze możemy używać funkcji readv i w r i t e v zamiast wielokrotnie wywoływać read i w r i t e . Rezultaty pomiaru czasu pokazują, że skopiowanie bufora i zapisanie go (write) zazwyczaj zabiera taką samą ilość czasu procesora jak pojedyncze wywołanie writev, ale na ogół zaalokowanie nowego obszaru tymczasowej pamięci i skopiowanie do niego danych jest bardziej skomplikowane niż wywołanie w r i t e v.

Funkcje readn oraz writen Niektóre urządzenia, głównie terminale, urządzenia sieciowe oraz urządzenia strumieniowe w systemie SVR4 mają następujące dwie własności. 1. Funkcja read może przekazać mniej danych, niż określono w jednym z argumentów, mimo że nie natrafiliśmy na znacznik końca pliku. Nie jest to błędem i powinniśmy kontynuować odczyt z urządzenia. 2. Funkcja w r i t e może również wypisać mniej danych, niż sobie życzyliśmy. Powodem są np. pewne ograniczenia związane z przepływem sterowania narzucane przez moduły przetwarzające. Nie jest to błędem i powinniśmy kontynuować zapisywanie pozostałych danych. (Typowo funkcja w r i t e przekazuje wartość mniejszą od żądanej liczby bajtów do zapisu, tylko jeśli deskryptor jest nieblokujący lub jeśli proces przechwycił sygnał). Nigdy nie zobaczymy takiej sytuacji przy odczycie lub zapisie pliku dyskowego. W rozdziale 18 pokażemy przykład, w którym podczas zapisywania danych do łącza strumieniowego (które jest zaimplementowane za pomocą strumieni systemu SVR4 lub berkelejowskich gniazd dziedziny Unix) będą dla nas ważne powyższe charakterystyki. Można wówczas zastosować dwie poniżej opisane funkcje, które odczytują lub zapisująN bajtów danych, obsługując odpowiednio wartości powrotu, gdy są one mniejsze od wymaganych. Te dwie funkcje po prostu wywołują odpowiednią ilość razy funkcję read lub w r i t e , aby odczytać lub zapisać zleconą liczbę N bajtów danych. #include "ourhdr.h" s s i z e _ t readn (int filedes, void *buff, size_t nbytes) ; s s i z e _ t writen (int filedes, void *buff, size_t nbytes); Obie przekazują: liczbę bajtów przeczytanych lub zapisanych; - 1 , jeśli wystąpił błąd

W naszych przykładach wywołujemy zawsze funkcję w r i t e n , gdy zapisujemy dane do urządzenia jednego z wymienionych wcześniej typów. Funkcję readn wywołujemy tylko, gdy wiemy zawczasu, że otrzymamy określoną

12.8. Funkcje readn oraz w r i t e n

483

porcję danych. (Na ogół wywołujemy read, by odczytać z urządzenia wszystko, co jest dostępne). Program 12.12 jest implementacją funkcji writen, z której korzystamy w dalszych przykładach, a próg. 12.13 implementacją funkcji readn. iinclude

"ourhdr.h"

ssize_t writen(int fd, { size_t ssize_t const char

/* zapisujemy do deskryptora "n" bajtów */ const void *vptr, size t n)

ptr = vptr;

nleft; nwritten; *ptr; /* nie można realizować arytmetyki wskaźników dla typu void* */

nleft = n; while (nleft > 0) { if ( (nwritten = write(fd, ptr, nleft)) 0) { if ( (nread = read(fd, ptr, nleft)) < 0) return(nread); /* błąd; przekazujemy wartość < 0 */ else if (nread == 0) break; /* EOF */ nleft -= nread; ptr += nread; return(n - nleft);

/* przekazujemy wartość >= 0 */ Próg. 12.13 Funkcja readn

12. Zaawansowane operacje wejścia-wyjścia

Wejście-wyjście oparte na odwzorowaniu w pamięci Wejść ie-wyjście oparte na odwzorowaniu w pamięci umożliwia umieszczenie w buforze w pamięci pliku dyskowego w ten sposób, że pobieranie bajtu z bufora oznacza odczyt odpowiedniego bajtu z pliku. Tak samo, gdy zapamiętujemy dane w buforze, odpowiednie bajty są automatycznie zapisywane do pliku. Dzięki tej technice możemy realizować operacje wejścia-wyjścia bez stosowania wywołań read i w r i t e . Aby używać tej właściwości, musimy zlecić systemowi odwzorowanie danego pliku w obszarze pamięci. Służy do tego funkcja mmap.

najwyższy adres stos

len

it

sterta dane niezainicjowane (bss) dane zainicjowane

mmap(caddr t i 'addr, s i z e t len, i n t prot, i n t flag, i n t f i l e d e s , off_t off) ;

Przekazuje: adres początku obszaru odwzorowania, jeśli wszystko w porządku; - 1 , jeśli wystąpił błąd Wejście-wyjście oparte na odwzorowaniu w pamięci było stosowane przez wiele lat w systemach korzystających z pamięci wirtualnej. System 4.1BSD (1981) wprowadził funkcje vread oraz vwrite, które realizują inną formę wejścia-wyjścia opartego na odwzorowaniu pliku w pamięci. W wersji 4.2BSD te dwie funkcje usunięto, gdyż miała je zastąpić funkcja mmap. Jednak nie została ona włączona do systemu 4.2BSD (powody przedstawiono w podrozdz. 2.5 pracy Lefflera i in. [32]). Gingell, Moran i Shannon w pracy [23] opisują implementację funkcji mmap. Funkcja mmap jest obecnie zaimplementowana w systemach SVR4 oraz 4.3+BSD.

Typ danych c a d d r _ t jest często definiowany jako char *. Argument addr pozwala nam określić adres początku obszaru odwzorowania w pamięci. Typowo podajemy w nim wartość 0, by system wybrał odpowiedni adres obszaru. Wartością tej funkcji jest adres początku obszaru odwzorowania. Argument filedes jest deskryptorem pliku określającym plik, który ma zostać odwzorowany w pamięci. Zanim przystąpimy do odwzorowania pliku w pamięci, musimy ten plik otworzyć. Argument len określa liczbę odwzorowywanych w pamięci bajtów, a o^fjest wskaźnikiem pierwszego bajtu pliku przeznaczonego do odwzorowania. (Później omówimy pewne ograniczenia obowiązujące dla wartości off). Zanim przyjrzymy się kolejnym argumentom, prześledźmy, co dzieje się w trakcie odwzorowywania pliku. Na rysunku 12.12 pokazujemy plik odwzorowany w pamięci. (Porównaj z organizacją pamięci w typowym procesie z rys. 7.3). Na tym rysunku „adres początku" jest wartością otrzymaną z funkcji mmap. Wprawdzie pokazujemy, że odwzorowany obszar jest zlokalizowany między stertą a stosem, ale jest to szczegół implementacyjny i konkretne rozwiązania mogą być różne.

część pliku do odwz. w pamięci

adres początkowy -

#include #include caddr t

485

12.9. Wejście-wyjście oparte na odwzorowaniu w pamięci

najniższy adres

tekst

plik:

część pliku do odwz. w pamięci

•w+U-

len

Rys. 12.12 Przykład pliku odwzorowanego w pamięci

Argument pro? określa sposób ochrony odwzorowanego obszaru (tab. 12.8). Ochrona określona dla danego obszaru musi być zgodna z trybem dostępu wskazanym w funkcji open. Nie możemy na przykład podać stałej PROT_WRITE, jeśli otworzyliśmy plik w trybie tylko do odczytu. Tabela 12 8 Ochrona obszaru odwzorowanego w pamięci prot

Opis

PROT_READ

obszar może być czytany

PROT_WRITE

obszar może być zapisywany

PROT EXEC

obszar może być wykonywany

PROT NONE

nie ma dostępu do obszaru (nie w systemie 4.3+BSD)

Argumenty/ag ma wpływ na różne atrybuty odwzorowanego obszaru. •, ,

Wartość powrotu musi być równa argumentowi addr. Nie zaleca, się stosowania tego sygnalizatora, gdyż utrudnia przenośność. \ Jeżeli nie podamy tego sygnalizatora, a argument addr jest niezerowy, to jądro systemu używa wartości addr jako wska- j zówki, gdzie umieścić obszar odwzorowania. ; Najlepszą przenośność uzyskujemy, gdy w argumencie addr% podamy wartość 0. • MAPSHARED Ten sygnalizator opisuje dyspozycję dotyczącą operacji zapamiętywania danych w obszarze odwzorowania przez dany ' MAP_FIXED

12. Zaawansowane operacje wejścia-wyjścia

12.9. Wejście-wyjście oparte na odwzorowaniu w pamięci

487

proces. Powoduje, że każda operacja zapamiętania danych modyfikuje odwzorowany w tym obszarze plik, czyli zapamiętanie danych jest równoważne z wywołaniem funkcji w r i t e , zapisującej dane do tego pliku. Musimy uaktywnić albo ten sygnalizator, albo opisany poniżej (MAP_PRIVATE). MAP_PRIVATE Ten sygnalizator sprawia, że operacja zapamiętania danych w obszarze odwzorowania wiąże się z utworzeniem kopii odwzorowanego pliku. Każde kolejne odwołania do obszaru odwzorowania dotyczą kopii pliku. (Jednym z zastosowań tego sygnalizatora jest program umożliwiający diagnozowanie procesu w trakcie jego uruchomienia, który odwzorowuje część tekstową pliku z programem, ale pozwala na modyfikowanie przez użytkownika instrukcji. Każda taka modyfikacja dotyczy kopii, a nie oryginalnego pliku programu).

Obszar odwzorowania w pamięci jest dziedziczony przez potomka po wywołaniu fork (ponieważ jest on fragmentem przestrzeni adresowej procesu macierzystego), ale z tego samego powodu nie jest dziedziczony przez nowy program wykonywany przez funkcję exec. Obszar odwzorowania w pamięci jest automatycznie kasowany, gdy kończy się proces; to samo dzieje się w wyniku bezpośredniego wywołania funkcji munmap. Zamknięcie deskryptora pliku filedes nie oznacza usunięcia odwzorowania pliku w pamięci.

System 4.3+BSD ma dodatkowe wartości sygnalizatorów (MAP_XXX), specyficzne dla implementacji. Szczegóły można znaleźć na stronie mmap(2) w systemie 4.3+BSD. Wartości argumentów off oraz addr (jeśli jest włączony sygnalizator MAP_FIXED) typowo powinny być wielokrotnością rozmiaru strony pamięci wirtualnej systemu. W systemie SVR4 tę wartość otrzymujemy, wywołując funkcję sysconf (p. 2.5.4) z argumentem _SC_PAGESIZE. W systemie 4.3+BSD rozmiar strony definiuje stała NBPG, umieszczona w pliku nagłówkowym . Na ogół off i addr mają wartość 0, więc to wymaganie nie stwarza dodatkowych problemów. Ponieważ wskaźnik początku odwzorowywanego pliku zostaje powiązany z rozmiarem strony w systemowej pamięci wirtualnej, co stanie się, jeżeli rozmiar odwzorowywanego obszaru nie jest wielokrotnością rozmiaru strony? Załóżmy, że plik ma rozmiar 12 bajtów, a rozmiar strony w systemie wynosi 512 bajtów. W takim przypadku system typowo przydziela obszar odwzorowania wielkości 512 bajtów, wyzerowując końcowe 500 bajtów obszaru. Możemy wprawdzie modyfikować ostatnie 500 bajtów pliku, ale żadne zmiany nie są odzwierciedlane w pliku. Przy obsłudze obszarów odwzorowania są używane zazwyczaj dwa sygnały. SIGSEGV to sygnał stosowany do wskazania próby korzystania z pamięci, która nie jest dla nas dostępna. Taki sygnał może zostać wygenerowany również, gdy chcemy zapisać dane do pamięci odwzorowania, dla której określiliśmy w funkcji nunap uprawnienia tylko do odczytu. Sygnał SIGBUS jest generowany, gdy chcemy użyć fragmentu obszaru odwzorowania, a w tym czasie korzystanie z niego nie ma sensu. Załóżmy na przykład, że odwzorowujemy plik na podstawie jego rozmiaru, ale przed naszym odwołaniem do obszaru odwzorowania inny proces skrócił dany plik. Gdy następnie próbujemy użyć obszaru odwzorowania aktualnie umieszczonego za bieżącym końcem pliku, wówczas odbierzemy sygnał SIGBUS.

Funkcja munmap nie ma wpływu na stan obiektu, który był odwzorowany, co oznacza, że wywołanie munmap nie wiąże się z zapisaniem zawartości obszaru odwzorowania do pliku dyskowego. Aktualizacja pliku dyskowego, gdy dla obszaru odwzorowania obowiązuje sygnalizator MAP_SHARED, jest realizowana automatycznie przez jądro systemu, kiedy zapamiętujemy dane w obszarze odwzorowania, zgodnie z algorytmem obsługi pamięci wirtualnej.

łfinclude #include void *munmap(caddr t * addr, size t len) ; Przekazuje: 0, jeśli wszystko w porządku - 1 , jeśli wystąpił błąd

Pewne systemy mają funkcję msync, która jest podobna do f sync (podrozdz. 4.24), ale obsługuje obszary odwzorowania w pamięci.

Przykład Program 12.14 kopiuje plik (tak jak polecenie cp(l)), używając wejścia-wyjścia opartego na odwzorowaniu w pamięci. Najpierw otwieramy oba pliki, następnie wywołujemy funkcję f s t a t , aby otrzymać rozmiar pliku wejściowego. Rozmiar jest nam potrzebny do wywołania funkcji mmap dla pliku wejściowego, a również służy do ustalenia rozmiaru pliku wyjściowego. Wywołujemy funkcję lseek, a potem funkcję w r i t e , zapisującą jeden bajt, aby ustalić rozmiar pliku wyjściowego. Jeżeli nie zrobilibyśmy tego, to wprawdzie wywołanie funkcji mmap dla pliku wyjściowego powiodłoby się, ale pierwsze odwołanie do obszaru odwzorowania związanego z tym plikiem zakończyłoby się wygenerowaniem sygnału SIGBUS. Wydawałoby się, że najprościej byłoby użyć funkcji f t r u n c a t e , by ustalić rozmiar pliku wyjściowego, ale nie wszystkie systemy pozwalają za pomocą tej funkcji zwiększać rozmiar pliku. (Zobacz podrozdz. 4.13). Następnie wywołujemy funkcję mmap dla każdego pliku, by odwzorować te pliki w pamięci, i na koniec używamy funkcji memcpy do skopiowania danych z bufora wejściowego do bufora wyjściowego. Gdy pobieramy kolejne bajty danych z bufora wejściowego (src), wówczas system automatycznie czyta dane z pliku wejściowego; gdy zapamiętujemy dane w buforze wyjściowym (dst), dane są automatycznie zapisywane do pliku wyjściowego.

12. Zaawansowane operacje wejścia-wyjścia

clude clude clude clude .clude



"ourhdr.h"

Porównajmy wyniki pomiaru czasu kopiowania pliku przy użyciu odwzorowania w pamięci z typowym kopiowaniem realizowanym przez funkcje read i w r i t e (dla bufora o rozmiarze 8192). W tabeli 12.9 pokazujemy uzyskane rezultaty.

/* mmap() */

Tabela 12.9

ndef MAP FILE

/* 4.3+BSD definiuje tę stałą i wymaga jej, gdy używa się mmap */ •fine MAP FILE 0 /* w celu kompilacji w systemach innych niż 4.3+BSD */ idif .n(int argc, char *argv[])

if (argc != 3) err quit("usage: a.out "); if ( (fdin = open(argv[l], O_RDONLY)) < 0) err_sys("can't open %s for reading", argv[l]); if ( (fdout = open(argv[2], O_RDWR I O_CREAT | O_TRUNC, FILE_MODE)) < 0) err sys("can't creat %s for writing", argv[2]); if (fstat(fdin, sstatbuf) < 0) /* potrzebujemy rozmiar pliku wejściowego */ err_sys("fstat error"); /* ustalamy rozmiar pliku wynikowego */ if (lseek(fdout, statbuf.st_size - 1, SEEK_SET) == -1) err sys("lseek error"); if (write(fdout, "", 1) != D err sys("write error"); ( s r c = mmap(0, s t a t b u f . s t _ s i z e , PROT_READ, MAP_FILE | MAP_SHARED, f d i n , 0)) == (caddr_t) -1) err_sys("mmap e r r o r for i n p u t " ) ;

if ( (dst = mmap{0, s t a t b u f . s t _ s i z e , PROT_READ I PROT_WRITE, MAP FILE | MAP_SHARED, fdout, 0)) == (caddr_t) e r r sys("mmap e r r o r for o u t p u t " ) ; memcpy(dst,

src,

statbuf.st_size);

Wyniki pomiaru czasu w celu porównania wydajności funkcji read i w r i t e z funkcjami mmap i memcpy SPARC

Operacja

80386

Użytkownik

System

Zegar

Użytkownik

read/write

0,0

2,6

11,0

0,0

5,3

11,2

mmap / memcpy

0,9

1,7

3,7

0,3

2,7

5,7

System

Zegar

Czasy podajemy w sekundach. Kopiowany plik liczył około 3 miliony bajtów. Dla platformy SPARC całkowity czas procesora (w trybach użytkownika i systemowym) jest taki sam dla obu typów kopiowania: 2,6 s. (Tak samo było w przypadku pomiarów czasu dla funkcji w r i t e v , pokazanych w tab. 12.7). Dla platformy 386 całkowity czas procesora zmniejsza się niemal o połowę, gdy używamy wywołań mmap oraz memcpy. Czasy systemowe maleją, gdy używamy funkcji mmap na obu platformach, SPARC i 386, gdyż jądro realizuje wejście-wyjście bezpośrednio z i do buforów obszaru odwzorowania. Podczas wywołań funkcji read i w r i t e jądro systemu najpierw musi kopiować dane między naszymi buforami a własnymi i dopiero potem realizuje operacje wejścia-wyjścia, używając swoich buforów. Ostatnia uwaga dotyczy czasu zegarowego - przy użyciu mmap i memcpy czas ten maleje dwukrotnie. D

int fdin, fdout; char *src, *dst; _, struct stat statbuf ;

if (

489

12.9. Wejście-wyjście oparte na odwzorowaniu w pamięci

1)

/* kopiujemy p l i k */

exit (0); Próg. 12.14 Kopiowanie pliku przy użyciu wejścia-wyjścia z odwzorowaniem w pamięci

Wejście-wyjście oparte na odwzorowaniu w pamięci jest techniką szybszą, gdy kopiujemy zwykłe pliki. Sąjednak pewne ograniczenia. Nie możemy stosować tej metody między pewnymi urządzeniami (takimi jak urządzenie sieciowe lub terminalowe). Musimy też być bardzo czujni, gdyż po wywołaniu mmap może zmieniać się rozmiar pliku podlegającego odwzorowaniu. Mimo to w pewnych programach użytkowych zastosowanie wejścia-wyjścia opartego na odwzorowaniu w pamięci może być bardzo korzystne, gdyż operowanie na pamięci zamiast czytania z pliku i zapisywania do pliku często istotnie upraszcza algorytmy. Jednym z przykładów zastosowania, w którym wprowadzenie techniki wejścia-wyjścia opartego na odwzorowaniu w pamięci przynosi znaczne korzyści, jest obsługa urządzenia bufora ramki zawierającego odwzorowanie bitowe obrazu ekranowego. Krieger, Stumm i Unrau [31] opisują alternatywną bibliotekę standardowego wejścia-wyjścia (rozdz. 5), używającą wejścia-wyjścia opartego na odwzorowaniu w pamięci. Powrócimy do tematu wejścia-wyjścia opartego na odwzorowaniu w pamięci w podrozdz. 14.9 - pokażemy przykład użycia tej techniki w systemach SVR4 i 4.3+BSD, gdy będziemy implementować pamięć wspólną, z której korzystają spokrewnione procesy.

12. Zaawansowane operacje wejścia-wyjścia

10 Podsumowanie W tym rozdziale opisaliśmy wiele zaawansowanych funkcji wejścia-wyjścia, z większości z nich będziemy korzystać w przykładach w kolejnych rozdziałach: •

• •

• • •

wejście-wyjście bez blokowania - wykonywanie operacji bez możliwości zablokowania (będzie nam to potrzebne w procedurze obsługi drukarki postscriptowej w rozdz. 17); ryglowanie rekordów (więcej szczegółów zobaczymy w przykładzie implementacji biblioteki do obsługi bazy danych w rozdz. 16); strumienie Systemu V (będą nam potrzebne w rozdz. 15, by zrozumieć łącza strumieniowe systemu SVR4, przekazywanie deskryptorów pliku oraz połączenia klient-serwer w systemie SVR4); zwielokrotnianie wejścia-wyjścia - funkcje s e l e c t i p o l l (będziemy z nich korzystać w wielu przykładach); funkcje r e a d v oraz w r i t e v (również używane w wielu przykładach); wejście-wyjście oparte na odwzorowaniu w pamięci (mmap).

Ćwiczenia 12.1

Usuń w pętli for w próg. 12.6 drugie wywołanie funkcji write. Co się stało? Dlaczego?

12.2

Przyjrzyj się zawartości pliku nagłówkowego i przeanalizuj implementację funkcji s e l e c t oraz czterech makr serii FD_.

12.3

W pliku nagłówkowym jest na ogół ustalone ograniczenie dotyczące maksymalnej liczby deskryptorów, jaką może obsłużyć typ danych fd_set. Załóżmy, że musimy zwiększyć to ograniczenie do 2048 deskryptorów. Jak możemy to zrobić?

12.4

Porównaj różne funkcje obsługujące zbiory sygnałów (podrozdz. 10.11) oraz zbiory deskryptorów typu fd_set. Porównaj też ich implementację w Twoim systemie.

12.5

Ile różnych typów informacji może przekazać funkcja getmsg?

12.6

Zaimplementuj funkcję sleep_us podobną do funkcji sleep, ale oczekującą przez określoną liczbę mikrosekund. Zastosuj funkcję s e l e c t lub poll. Porównaj tę funkcję z berkelejowską funkcją usieep.

12.7

Czy potrafisz zaimplementować funkcje TELL_WAIT, TELL_PARENT, TELL_CHILD,

WAIT_PARENT i WAIT_CHILD z próg. 10.17 za pomocą zalecanego ryglowania rekordów zamiast sygnałów? Jeśli tak, to przygotuj kod źródłowy i przetestuj swoją implementację.

12.10. Podsumowanie

491

12.8 Określ pojemność łącza komunikacyjnego, używając funkcji s e l e c t lub poll. Porównaj wartość z wartością stałej PIPE_BUF Z rozdz. 2. 12.9

Uruchom próg. 12.4, by skopiować plik i sprawdź, czy został zmodyfikowany czas ostatniego dostępu do pliku.

12.10 W programie 12.14 po wywołaniu funkcji mmap wywołaj funkcję ciose dla pliku wejściowego, by potwierdzić teorię, że zamknięcie deskryptora nie ma wpływu na dalsze używanie wejścia-wyjścia opartego na odwzorowaniu w pamięci.

493

13.2. Charakterystyki demona

13

Procesy-demony

|1 Wprowadzenie Demony to procesy żyjące bardzo długo. Często są uruchamiane podczas wstępnego ładowania systemu i kończą się, gdy system jest zamykany. Mówimy, że pracują w tle, ponieważ nie mają swojego terminalu sterującego. W systemach uniksowych jest wiele demonów, które realizują typowe czynności systemowe. W tym rozdziale przyjrzymy się strukturze procesów-demonów, nauczymy się je programować. Ponieważ demon nie ma terminalu sterującego, musimy wiedzieć, w jaki sposób może raportować błędy, gdy dzieje się coś złego.

2

Charakterystyki demona Przyjrzyjmy się popularnym demonom systemowym, zwracając uwagę na to, jaki mają związek z opisanymi w rozdz. 9 koncepcjami grup procesów, terminali sterujących i sesji. Polecenie ps(l) wypisuje stan różnych procesów w systemie. Ma ono ogromną liczbę opcji - opisuje je podręcznik systemowy. Wykonajmy polecenie ps

-axj

w systemie 4.3+BSD lub SunOS, aby prześledzić informacje, o których będziemy mówić. Opcja -a pokazuje stan procesów, których właścicielami są inni użytkownicy, a -x pokazuje te procesy, które nie mają terminalu sterującego. Opcja -j wyświetla informacje związane ze sterowaniem zadaniami: identyfikator sesji, identyfikator grupy procesów, terminal sterujący i identyfikator terminalowej grupy procesów. W systemie SVR4 podobną rolę od-

grywa polecenie ps -ef j c. (W niektórych systemach Unix, które są zgodne z zaleceniami dotyczącymi bezpieczeństwa ustalonymi przez Department of Defense, nie można użyć polecenia ps, by otrzymać informację o obcym procesie). Polecenie ps drukuje informację o poniższej postaci PPID 0 0 0 1 1 1 1 1 1

PID 0 1 2 80 88 105 108 114 117

PGID 0 0 0 80 88 37 108 114 117

SID 0 0 0 80 88 37 108 114 117

TT ? ? ? ? ? ? ? ? ?

TPGID -1 -1 -1 -1 -1 -1 —1 —1 _ 1

UID 0 0 0 0 0 0 0 0 0

COMMAND swapper /sbin/init pagedaemon syslogd /usr/lib/sendmail -bd -qlh update cron inetd /usr/lib/lpd

Opuściliśmy parę kolumn, które nas nie interesują, np. całkowity czas procesora. Nagłówki kolumn to kolejno: identyfikator procesu macierzystego, identyfikator samego procesu, identyfikator grupy procesów, identyfikator sesji, nazwa terminalu, identyfikator terminalowej grupy procesów (pierwszoplanowa grupa procesów związana z terminalem sterującym), identyfikator użytkownika oraz napis polecenia. System, na którym uruchomiliśmy polecenie ps (SunOS) używa pojęcia identyfikatora sesji, o którym wspomnieliśmy, omawiając funkcję s e t s i d w podrozdz. 9.5. Jest to po prostu identyfikator procesu lidera sesji. System 4.3+BSD wypisałby w tym miejscu adres struktury session, odpowiadającej grupie procesów, do której należy dany proces (podrozdz. 9.11).

Procesy o identyfikatorach 0, 1 i 2 opisaliśmy już w podrozdz. 8.2. Są to procesy specjalne i istnieją przez cały czas, gdy działa system. Nie mają identyfikatorów procesu macierzystego, grupy procesów ani sesji. Z demona s y s l o g d korzysta wiele programów, by zapisywać w kronice systemowej komunikaty dla operatora. Komunikaty mogą być wypisywane na konsoli lub dodawane do pliku. (W punkcie 13.4.2 opiszemy program syslogd). Program sendmail jest standardowym demonem do obsługi poczty elektronicznej. Program update służy do zapisywania co pewien czas (na ogół co 30 sekund) zawartości buforów pamięci podręcznej jądra systemu do plików dyskowych. Aby to zrealizować, po prostu co 30 sekund wywołuje funkcję sync(2). (W podrozdziale 4.24 opisaliśmy funkcję sync). Demon cron wykonuje polecenia według ustalonego terminu. Wiele zadań administracyjnych w systemie jest obsługiwanych przez regularne wykonywanie określonych programów przez demona cron. W podrozdziale 9.3 mówiliśmy o demonie i n e t d . Oczekuje on na różnych interfejsach sieciowych na żądania związane z usługami sieciowymi. Ostatni demon, lpd, obsługuje w systemie żądania wydruku.

13. Procesy-demony

13.3. Zasady programowania demonów

Niektóre demony mogą jednak zmienić bieżący katalog roboczysoodc dowolny inny, ustalony katalog związany z ich pracą. Na przyU^C > demony kolejkujące żądania drukowania często używają, jako k>l OJIBJ logu roboczego, katalogu, w którym są umieszczane zadania. 4. Nadajemy wartość zerową masce tworzenia plików. Odziedzicsoisba maska trybu dostępu do plików mogła blokować pewne uprawnieainwB" Jeżeli proces demona ma mieć możliwość tworzenia plików, to nn oJ tv mieć ustalone konkretne uprawnienia. Na przykład, jeżeli demon tt nont; rzy pliki z ustawionymi bitami odczytu i zapisu przez grupę, to maem oJ , trybu dostępu do plików, wyłączająca jakieś z tych uprawnień, ULI crt9ii możliwi mu takie operacje. 5. Powinniśmy zamknąć wszystkie niepotrzebne deskryptory. Dzięki teł blais demon nie utrzymuje otwartych wszystkich deskryptorów odziedzioisbais nych z procesu macierzystego (którym może być powłoka lub jakiś ii Z\^B{ proces). To, które deskryptory trzeba zamknąć, zależy od demona, t£norr tego nie pokazujemy tego kroku w naszym przykładzie. Można użyo^śu fii tego funkcji open_max (próg. 2.3), by sprawdzić, jaki jest najwięW?iw(Bi deskryptor, i zamknąć wszystkie deskryptory do tej wartości.

Zwróćmy uwagę na to, że wszystkie demony pracują z uprawnieniami nadzorcy systemu (identyfikator użytkownika równy 0). Żaden z demonów nie ma terminalu sterującego - w kolumnie odpowiadającej nazwie terminalu widzimy same znaki zapytania, a pierwszoplanową terminalową grupą procesów jest - 1 . Brak terminalu sterującego wiąże się prawdopodobnie z tym, że demon wywołuje funkcję s e t s i d . Wszystkie demony oprócz update są liderami grupy procesów oraz liderami sesji, a także jedynymi procesami w swojej grupie procesów i w sesji. Proces update jest jedynym członkiem swojej grupy procesów (37) oraz sesji (37), ale lider tej grupy procesów (będący zapewne też liderem sesji) istniał już wcześniej. Na koniec zwróćmy uwagę, że procesem macierzystym wszystkich tych demonów jest proces i n i t .

Zasady programowania demonów Istnieją pewne podstawowe zasady przygotowywania kodu źródłowego demonów, które pozwalają zapobiec niechcianym interakcjom między procesami. Podamy te reguły, a następnie pokażemy funkcję daemon_init, która je implementuje. 1. Pierwszą rzeczą, jaką należy zrobić, jest wywołanie f ork i zakończenie pracy procesu macierzystego (exit). Jeśli demon został uruchomiony za pomocą polecenia powłoki, to zakończenie procesu macierzystego z punktu widzenia powłoki oznacza, że całe polecenie zakończyło się. Poza tym potomek dziedziczy identyfikator grupy procesów procesu macierzystego, ale otrzymuje nowy identyfikator procesu, a więc mamy gwarancję, że nie stanie się liderem grupy procesów. Jest to wymagane przez funkcję s e t s i d , którą zaraz wywołamy. 2. Wywołujemy funkcję s e t s i d , aby utworzyć nową sesję. Są realizowane trzy kroki opisane w podrozdz. 9.5. Proces (1) staje się liderem nowej sesji, (2) staje się liderem nowej grupy procesów i (3) nie jest właścicielem terminalu sterującego. W systemie SVR4 niektórzy zalecają w tym miejscu ponowne wywołanie funkcji fork i zakończenie procesu macierzystego. Drugi potomek kontynuuje wówczas pracę jako demon. Gwarantuje to, że demon nie jest liderem sesji, a więc nie może, według reguł systemu SVR4 (podrozdz. 9.6), otrzymać terminalu sterującego. Zamiast tego, aby proces ten nie uzyskał terminalu sterującego, możemy przy otwarciu terminalu podać sygnalizator O_NOCTTY.

3. Zmieniamy bieżący katalog roboczy na katalog główny. Bieżący katalog roboczy odziedziczony z procesu potomnego może być umieszczony na zamontowanym systemie plików. Demony zazwyczaj pracują aż do ponownego załadowania systemu, a więc dopóki żyje demon, nie można odmontować systemu plików.

Przykład

Program 13.1 zawiera funkcję wywoływaną z programu, który chce przygną 3 tować się do roli demona. #include #include #include #include



"ourhdr.h"

int daemon init (void) i

pid t

pid;

if ( (pid = fork () ) < 0) return(-1); else if (pid != 0) exit(0); /* proces macierzysty kończy pracę */ /* proces potomny kontynuuje pracę */ setsidO; /* staje się liderem sesji */ chdir("/");

/* zmienia katalog roboczy */

urna s k (0) ;

/* zeruje maskę tworzenia plików */

return(0);

Próg. 13.1 Inicjowanie procesu demona

13. Procesy-demony

Jeżeli funkcja d a e m o n i n i t jest wywoływana z programu głównego, który następnie usypia na pewien czas, to możemy sprawdzić stan demona za pomocą polecenia ps.

$ a.out $ ps -axj

PPID PID 1 735

PGID 735

SID TT TPGID -1 735

UID 224

COMMAND a.out

Widzimy, że nasz demon został poprawnie zainicjowany.

4



Rejestrowanie błędów Jednym z problemów w demonie jest sposób obsługi komunikatów awaryjnych. Nie może on po prostu wypisać ich na standardowy strumień komunikatów, ponieważ nie ma terminalu sterującego. Nie chcemy, by wszystkie demony wyprowadzały komunikaty na konsolę, gdyż na wielu stacjach roboczych konsola pracuje w systemie okienkowym. Nie chcemy również, by każdy demon zapisywał swoje komunikaty o błędach w oddzielnym pliku. Administratorowi systemu trudno byłoby panować nad tym, jaki demon zapisuje komunikaty do jakiego pliku, oraz niełatwe byłoby regularne przeglądanie kronik systemowych. Dlatego potrzebny jest centralny demon obsługujący zapisywanie do kronik systemowych. Idea syslog powstała w Berkeley i była stosowana począwszy od wersji 4.2BSD. Większość systemów wywodzących się z 4.xBSD stosuje syslog. Opiszemy tę technikę w p. 13.4.2. W Systemie V nie było nigdy centralnego demona do rejestrowania błędów. SVR4 stosuje berkelejowski program syslog. Demon i n e t d w systemie SVR4 używa syslog. Podstawą programu syslog w systemie SVR4 jest procedura obsługi urządzenia strumieniowego /dev/log, którą opiszemy w następnym podrozdziale.

13.4.1

Procedura obsługi strumieni l o g w systemie SVR4

W systemie SVR4 istnieje procedura obsługi strumieni opisana na stronie log(7) w dokumentacji AT&A, 1990d [12], mająca interfejs do strumieniowego rejestrowania błędów, strumieniowego śledzenia zdarzeń oraz rejestrowania na konsoli. Na rysunku 13.1 widzimy szczegółową strukturę tej techniki. Każdy komunikat log może być przekazany do jednej z trzech kronik: błędów, śledzenia lub konsoli. Pokazujemy trzy sposoby generowania komunikatów typu log oraz trzy sposoby odczytywania ich.

497

13.4. Rejestrowanie błędów dane zapisywane do pliku lub wysyłane do /var/adm/streams/error.mm-itó zalogowanego użytkownika bądź innej stacji stdout

t

strerr

stracę

rejestr, błędów

rejestr, śledzenia

getmsg

/dev/log

getmsg

/dev/log

t

syslogd rejestr, na konsoli getmsg

/dev/log

proces użytkowy putmsg

/dev/log

proces użytkowy write

/dev/conslog

moduł strumieniowy procedura obsługi . strlogO lub strumieni l o g procedura obsługi jądro Rys. 13.1 Technika l o g w systemie SVR4

Generowanie komunikatów typu log. 1. Procedury w jądrze systemu mogą wywołać funkcję s t r l o g , aby wygenerować rejestrowane komunikaty. Ta metoda jest na ogół stosowana dla komunikatów o błędach oraz komunikatów śledzenia tworzonych przez moduły strumieniowe oraz procedury obsługi urządzeń strumieniowych. (Komunikaty śledzenia są często używane przy diagnozowaniu pracy nowych modułów strumieniowych lub procedur obsługi). Nie będziemy omawiać tego typu komunikatów, gdyż nie interesuje nas przygotowywanie kodu procedur jądra systemu. 2. Proces użytkowy (np. demon) może wywołać funkcję putmsg dla urządzenia /dev/log. Komunikat można przesłać do dowolnego z trzech rejestrów. 3. Proces użytkowy (np. demon) może wywołać funkcję w r i t e dla urządzenia /dev/conslog. Komunikat jest przesyłany tylko do kroniki konsoli. Odczytywanie komunikatów typu log. 4. Do rejestrowania błędów służy program s t r e r r ( l M ) . Program ten dołącza komunikaty do pliku w katalogu /var/adm/stream. Plik ma nazwę e r r o r .mm-dd, gdzie mm jest miesiącem, a dd dniem

13. Procesy-demony

13.4. Rejestrowanie błędów

499 dane zapisywane do pliku lub wysyłane do zalogowanego użytkownika bądź innej stacji

miesiąca. Ten program sam jest demonem i na ogół pracuje w tle, dołączając komunikaty rejestrowane do pliku. 5. Do rejestrowania śledzenia służy program s t r a c e ( l M ) . Może on selektywnie wypisywać zestaw komunikatów śledzenia na standardowe wyjście. 6. Do rejestrowania na konsoli służy program berkelejowski syslog, który opiszemy w kolejnym podrozdziale. Ten program jest demonem, który czyta plik konfiguracyjny i zapisuje komunikaty do konkretnego pliku (konsola jest również plikiem) lub wysyła je do zalogowanego użytkownika albo do demona s y s l o g pracującego na innej stacji. Nie wymieniliśmy na tej liście jednej możliwości. Proces użytkowy może zamienić standardowe demony dostarczone z systemem; możemy używać własnego programu rejestrującego błędy, komunikaty śledzenia oraz konsoli. Każdy komunikat typu log zawiera oprócz treści dodatkowe informacje. Na przykład komunikaty wysyłane w górę strumienia przez procedurę obsługi log zawierają informację o tym, kto wygenerował ten komunikat (jeśli został wygenerowany przez moduł strumieniowy w jądrze systemu), jaki jest jego poziom, priorytet, jakie sygnalizatory mu towarzyszą oraz kiedy został wygenerowany. Szczegóły można znaleźć w podręczniku log(7). Jeżeli generujemy komunikat log, używając funkcji putmsg, możemy sami ustawić pewne z tych pól. Jeżeli wywołujemy funkcję w r i t e , by wysłać komunikat do kroniki konsoli (przez urządzenie /dev/conslog), to możemy przesłać jedynie napis komunikatu. Na rysunku 13.1 nie widzimy, że demon systemu SVR4 może wywołać berkelejowska funkcję syslog(3). W ten sposób komunikat jest wysyłany do programu rejestrującego na konsoli, podobnie jak przy wywołaniu putmsg dla urządzenia /dev/log. Za pomocą funkcji s y s l o g można ustalić priorytet komunikatu. Opiszemy tę funkcję w kolejnym podrozdziale. Niestety, w systemie SVR4 używanie komunikatów typu log nie jest popularne. Niektóre demony systemowe korzystają z tej możliwości, ale większość ma wbudowane w kod programu zapisywanie komunikatów bezpośrednio na konsolę. Wprawdzie funkcja syslog(3) oraz demon syslogd(lM) są opisane w dokumentacji BSD Compatibility Library AT&T, 1990c [11], ale nie są częścią tej biblioteki - są umieszczone w standardowej bibliotece języka C, dostępnej dla wszystkich procesów użytkowych (demonów).

13.4.2

Technika syslog w systemie 4.3+BSD

Berkelejowska technika s y s l o g jest powszechnie stosowana począwszy od systemu 4.2BSD. Korzysta z niej większość demonów. Na rysunku 13.2 widzimy szczegóły związane z jej organizacją.

gniazdo datagramowe dziedziny Unix

gniazdo datagramowe dziedziny Internet

log procedury w jądrze systemu

|jądro sieć TCP/IP Rys. 13.2 Technika s y s l o g w systemie 4.3+BSD

Są dwa sposoby generowania komunikatów rejestrowanych: 1. Procedury jądra mogą wywołać funkcję log. Komunikaty mogą być czytane przez każdy proces użytkowy, który wywołuje funkcje open i read da urządzenia /dev/klog. Nie opisujemy dokładnie tej funkcji, ponieważ nie interesuje nas przygotowywanie procedur jądra systemu. 2. Większość procesów użytkowych (demonów), aby wygenerować komunikat -ejestrowany, wywołuje funkcję syslog(3). Sekwencję wywołania pokażemy później. W efekcie tego wywołania komunikat jest wysyłany do gniazda datagramowego dziedziny Unix, /dev/log. 3. Proces użytkowy na tej samej stacji lub proces na jakiejś innej stacji, połączony siecią TCP/IP, może wysyłać komunikaty rejestrowane do portu UDP o numerze 514. Zwróćmy uwagę, że funkcja s y s l o g nigdy nie generuje datagramów UDP, gdyż wymagają one jawnego pro-' gramowaiia sieciowego w procesie generującym komunikaty rejestrowane. W książce Stevensa [44], można znaleźć szczegółowe omówienie gniazd dziedziny Unix oraz gniazd UDP. Normalnie dsmon syslogd czyta wszystkie trzy rodzaje rejestrowanych komunikatów. W czasie startu czyta plik konfiguracyjny, najczęściej jest to / e t c / s y s l o g d . c o n f , który określa, gdzie są przesyłane różne typy komu-

13.Procesy-demony

nikatów. Na przykład pilne komunikaty mogą być przekazywane do administratora systemu (jeśli jest zalogowany) oraz wypisywane na konsolę, a ostrzeżenia mogą być zapisywane do pliku. Interfejsem tej techniki jest funkcja syslog. #include void openlog(char *ident, i n t

option, i n t facility) ;

void syslog (int priority, char *format,

...);

vcid closelog(void);

Wywołanie funkcji openlog jest opcjonalne. Jeśli z niego zrezygnujemy, to przy pierwszym wywołaniu funkcji syslog, funkcja openlog zostanie wywołana automatycznie. Wywołanie funkcji c l o s e l o g jest również opcjonalne - służy jedynie do zamknięcia deskryptora używanego do komunikacji z demonem syslogd. Dzięki wywołaniu openlog można określić wartość argumentu ident, dodawaną do każdego komunikatu rejestrowanego. Jest to na ogół nazwa programu (np. cron, i n e t d itp.). W tabeli 13.1 opisujemy wszystkie możliwe wartości argumentu option. Tabela 13.1 Argument option w funkcji o p e n l o g Opis CONS

Jeżeli komunikat typu l o g nie może zostać wysiany do serwera s y s l o g d w postaci datagramu dziedziny Unix, to zamiast tego jest wypisywany na konsoli.

NDELAY

Natychmiastowe otwarcie gniazda datagramowego dziedziny Unix w celu uzyskania połączenia z demonem s y s l o g d , bez oczekiwania na pojawienie się pierwszego komunikatu rejestrującego. Normalnie gniazdo jest otwierane, dopiero gdy pojawia się pierwszy komunikat typu log.

PERROR

PID

Komunikat typu l o g jest nie tylko przesyłany do serwera syslogd, lecz również na standardowy strumień komunikatów awaryjnych. Opcję tę stosuje system 4.3+BSD Reno oraz późniejsze wersje. Razem z każdym komunikatem jest rejestrowany identyfikator procesu. Ta właściwość dotyczy demonów, które tworzą procesy potomne do obsługi różnych żądań (w przeciwieństwie do takich demonów jak syslogd, które nigdy nie wywołują funkcji fork).

Wartości argumentu facility w funkcji openlog pokazujemy w tab. 13.2. Ten argument umożliwia, by w pliku konfiguracyjnym była określona różna obsługa komunikatów w zależności od ich rodzaju. Jeżeli nie wywołamy funkcji openlog lub wywołamy ją, podając w argumencie facility wartość 0, to nadal możemy określić rodzaj komunikatu jako część argumentu priority w funkcji syslog.

501

13.4. Rejestrowanie błędów Tabela 13.2

Argument facility w funkcji openlog

facility

Opis

LOG_AUTH

programy do uwierzytelniania tożsamości: l o g i n , su, getty,...

LOG_CRON

c r o n i at

LOG_DAEMON

systemowe demony: f t p d , r o u t e d

LOG_KERN

komunikaty generowane przez jądro systemu

LOG_LOCAL0

zarezerwowany do użycia lokalnego

LOG LOCAL1

zarezerwowany do użycia lokalnego

LOG_LOCAL2

zarezerwowany do użycia lokalnego

LOG_LOCAL3

zarezerwowany do użycia lokalnego

LOG LOCAL4

zarezerwowany do użycia lokalnego

LOG_LOCAL5

zarezerwowany do użycia lokalnego

LOG_LOCAL6

zarezerwowany do użycia lokalnego

LOG_LOCAL7

zarezerwowany do użycia lokalnego

LOG LPR

podsystem wydruków: lpd, l p c

LOG_MAIL

podsystem obsługi poczty elektronicznej

LOG_NEWS

podsystem news sieci Usenet

LOG SYSLOG

sam demon s y s l o g d

LOGJJSER

komunikaty z procesów użytkowych (wartość domyślna)

LOG UUCP

podsystem UUCP

Aby wygenerować komunikat rejestrowany, wywołujemy funkcję s y s log. Argument priority jest połączeniem wartości argumentu facility, pokazanego w tab. 13.2, oraz argumentu level, opisanego w tab. 13.3. Wartości level są uporządkowane zgodnie z priorytetem, począwszy od najwyższego do najniższego. Tabela 13.3 Argument level w technice s y s l o g level

Opis

LOG EMERG

awaria, nie można korzystać z systemu (najwyższy priorytet)

LOG_ALERT

sytuacje, którym należy natychmiast zaradzić

LOG_CRIT

warunek krytyczny (np. błąd na dysku twardym)

LOG_ERR

błąd

LOG WARNING

ostrzeżenie

LOG_NOTICE

typowa, ale istotna sytuacja

LOG_INFO

komunikat informacyjny

LOG DEBUG

komunikat zawierający informację diagnostyczną

Argument format oraz wszystkie pozostałe argumenty są przekazywane do funkcji v s p r i n t f jako napis formatowania. Wszystkie wystąpienia znaków %m w argumencie format są najpierw zamieniane na napis z treścią komunikatu ( s t r e r r o r ) , odpowiadający wartości zmiennej e r r n o .

13. Procesy-demony W systemach SVR4 oraz 4.3+BSD jest dostępny program logger(l), służący do przesyłania komunikatów rejestrowanych do demona syslogd. Opcjonalnymi argumentami tego programu są: facility, level oraz ident. Jest on przeznaczony dla skryptów uruchamianych nieinterakcyjnie, które muszą generować komunikaty rejestrowane. Postać polecenia logger jest ustalona w normie POSDC.2.

ykład W demonie obsługującym drukarkę postscriptową w rozdz. 17 zobaczymy poniższą sekwencję openlog("lprps", LOG_PID, LOG_LPR); syslog(LOG_ERR, "open error for %s: %m"

filename);

Pierwsze wywołanie nadaje argumentowi ident wartość odpowiadającą nazwie programu, a także wskazuje, że zawsze ma być drukowany identyfikator procesu, oraz ustala domyślną wartość argumentując////^1 odpowiadającą systemowi obsługi drukarki. Wywołanie funkcji syslog określa typ błędu i napis komunikatu. Jeżeli nie wywołalibyśmy najpierw funkcji openlog, to aby otrzymać ten sam efekt, drugie wywołanie musiałoby mieć postać

13.6. Podsumowanie

13.6

Podsumowanie Procesy-demony pracują nieprzerwanie w systemie uniksowym. Aby zainicjować własny proces, który ma pracować jako demon, trzeba spełnić pewne reguły i dobrze rozumieć relacje między procesami, które opisaliśmy w rozdz. 9. W tym rozdziale pokazaliśmy funkcję, którą może wywołać proces-demona, by poprawnie przygotować się do swojej roli. Opisaliśmy też, jak demon może rejestrować komunikaty o błędach; nie jest to łatwe, ponieważ demon zazwyczaj nie ma terminalu sterującego. W systemie SVR4 jest dostępna strumieniowa procedura obsługi log, a w systemie 4.3+BSD służy do tego technika syslog. Technika syslog jest również stosowana w systemie SVR4, dlatego, gdy w kolejnych rozdziałach będziemy chcieli zarejestrować w demonie komunikaty o błędach, użyjemy funkcji syslog. Będzie nam to potrzebne w rozdz. 17, w którym pokazujemy demona obsługującego drukarkę postscriptową.

Ćwiczenia 13.1

Jak można domyślać się na podstawie rys. 13.2, gdy jest inicjowana technika syslog, albo przez bezpośrednie wywołanie funkcji openlog, albo przy pierwszym wywołaniu funkcji syslog, musi zostać otwarte specjalne urządzenie związane z gniazdem datagramowym dziedziny Unix, /dev/log. Co stanie się, jeśli proces użytkowy (demon) wywoła funkcję chroot przed wywołaniem openlog?

13.2

Przedstaw wykaz wszystkich aktywnych demonów w Twoim systemie i określ rolę każdego z nich.

syslog(LOG_ERR | LOG_LPR, "open error for %s: %m", filename); W tej sytuacji trzeba połączyć w argumencie priority wartość argumentów level i facility. •

Model klient-serwer Demony bardzo często są stosowane jako procesy serwera. Na rysunku 13.2 możemy nazwać proces syslogd procesem serwera, do którego procesy użytkowe (klienci) wysyłają komunikaty przez gniazdo datagramowe dziedziny Unix. Ogólnie mówiąc, serwer to proces oczekujący na kontaktującego się z nim klienta, który żąda od niego wykonania pewnej usługi. Serwer syslogd z rys. 13.2 oferuje usługę rejestrowania komunikatów o błędach. Komunikacja między klientem a serwerem na rys. 13.2 jest jednostronna. Klient tylko wysyła do serwera żądanie wykonania usługi, a serwer nie przesyła odpowiedzi do klienta. W następnym rozdziale na temat komunikacji międzyprocesowej zobaczymy różne przykłady komunikacji dwustronnej między klientem a serwerem. Zazwyczaj klient wysyła żądanie do serwera, a serwer przekazuje odpowiedź do klienta.

503

13.3 Przygotuj program, który wywołuje funkcję daemon_init z próg. 13.1. Po wywołaniu tej funkcji wywołaj funkcję getlogin (podrozdz. 8.14), aby sprawdzić, czy proces zna nazwę użytkownika, gdy stał się już demonem. Wypisz nazwę użytkownika do deskryptora pliku o numerze 3 i w czasie uruchomienia programu przekieruj ten deskryptor do tymczasowego pliku za pomocą notacji 3>/tmp/namel (w powłoce Bourne'a lub KornShellu). Uruchom ponownie program, w którym deskryptory 0, 1 i 2 są zamykane po " wywołaniu funkcji daemon_init, przed wywołaniem getlogin. Czy widzisz jakąś różnicę? , ]

13.4 Napisz demona w systemie SVR4, który staje się programem rejestrującym na j konsoli. Szczegóły znajdziesz w podręczniku iog(7) w [AT&T 1990d]. Po każdym odebraniu komunikatu drukuj odpowiednią informację. Napisz rów- . nież program testowy sprawdzający pracę demona, który wysyła komunikaty j rejestrowane do urządzenia /dev/iog.

13.5 Zmodyfikuj próg. 13.1 zgodnie z zasadą2 w podrozdz. 13.3; wywołaj drugi raz , funkcję fork, aby nie było możliwe otrzymanie terminalu w systemie SVR4. Przetestuj swojąfunkcję, by potwierdzić, że demon nie jest już liderem sesji.

14.2. Łącza komunikacyjne

14

505

między procesami pracującymi na tej samej stacji. Ostatnie dwa rzędy dotyczą gniazd i strumieni, będących jedynymi technikami, służącymi do komunikacji procesów pracujących na różnych stacjach. (W książce Stevensa [44] można znaleźć szczegółowy opis sieciowych technik komunikacji międzyprocesowej). Mimo że dla trzech form IPC, wymienionych w środkowych wierszach tabeli (kolejki komunikatów, semafory i pamięć wspólna), zaznaczamy, że są stosowane tylko w Systemie V, to jednak ich implementacja jest dodana w większości sprzedawanych systemów uniksowych o korzeniach berkelejowskich (np. SunOS i Ultrix).

Komunikacja międzyprocesowa

Różne grupy pracują aktualnie nad specyfikacją IPC w normie POSIX, ale ostateczny wynik nadal nie jest znany. Wydaje się, że końcowe rozwiązania nie powstaną wcześniej niż w 1994 roku.

Dyskusję na temat IPC podzieliliśmy na dwa rozdziały. W bieżącym pokazujemy klasyczne metody komunikacji międzyprocesowej: łącza komunikacyjne, kolejki FIFO, kolejki komunikatów, semafory i pamięć wspólną. W następnym rozdziale przyjrzymy się bardziej zaawansowanym charakterystykom IPC, wspieranym przez systemy SVR4 i 4.3+BSD: łączom strumieniowym, nazwanym łączom strumieniowym oraz omówimy sposoby obsługi tych form komunikacji międzyprocesowej.

Wprowadzenie W rozdziale 8 opisaliśmy procedury służące do sterowania procesem i zobaczyliśmy, w jaki sposób powstaje wiele procesów. Jednak dotychczas pokazaliśmy, że procesy mogą wymieniać między sobą informację jedynie przekazując otwarte pliki w wywołaniach fork i exec lub przez system plików. Teraz przedstawimy inne techniki umożliwiające komunikowanie się procesów - techniki IPC (Interprocess Communication), czyli komunikację międzyprocesowa. Uniksowe techniki IPC były i są do dnia dzisiejszego mieszaniną różnych podejść, a niewiele z nich daje się przenosić między różnymi implementacjami. Tabela 14.1

Zestawienie uniksowych metod komunikacji międzyprocesowej

dzaj IPC

POSK.l XPG3 V7 SVR2 SVR3.2 SVR4 4.3BSD 4.3+BSD

ia (jednokierunkowe) ejki FIFO (nazwane łącza)

* m

»

*

4

#

»

rz. strumieniowe (dwukierunkowe)

4

ejki komunikatów

4

nieć wspólna





4

4

4

< 4

azda imienie



*

4

wane łącza strumieniowe lafory

*

t

*

*

Jak pokazuje tabela 14.1, jedyną formą IPC, na którą możemy liczyć niezależnie od implementacji systemu Unix, są jednokierunkowe łącza komunikacyjne. Pierwsze siedem rodzajów IPC w tej tabeli można używać wyłącznie

14.2

Łącza komunikacyjne Łącza komunikacyjne są najstarszą formą uniksowej komunikacji międzyprocesowej, dlatego są implementowane we wszystkich systemach Unix. Mają dwa ograniczenia: 1. Są półdupleksowe. Dane są przekazywane tylko w jednym kierunku. 2. Mogą być stosowane tylko przez procesy mające wspólnego przodka. Zazwyczaj łącze powstaje w procesie, który wywołuje następnie funkcję f ork, i używa łącza między procesem macierzystym a procesem potomnym. ", stdout); fflush(stdout); if (fgets(line, MAXLINE, fpin) == NULL) break; if (fputs(linę, stdout) == EOF) err_sys("fputs error to pipę"); } if (pclose(fpin) == -1) err_sys("pclose error"); putchar('\n'); exit(0);

/* czytamy z łącza */

"ourhdr.h" Próg. 14.7 Zastosowanie filtra przekształcającego wielkie litery na małe przy odczycie poleceń

nt ain(void) int

519

14.4. Koprocesy

c;

while ( (c = getcharf)) != EOF) { if (isupper(c)) c = tolower(c); if (putchar(c) == EOF) err sys("output error"); if (c == '\n') fflush(stdout); exit(0)

Próg. 14.6 Filtr przekształcający wielkie litery na małe

Kompilujemy program-filtr, tworząc plik wykonywalny o nazwie myuclc, który następnie wywołujemy w próg. 14.7 z funkcji popen. i Po wypisaniu sekwencji zachęty musimy wywołać funkcję f flush, po-, nieważ typowo standardowe wyjście jest buforowane wierszami, a sekwencja, zachęty nie zawiera znaku nowego wiersza. -•-

14.4 Koprocesy

I

Filtr uniksowy jest programem, który czyta dane ze standardowego wejścia' i zapisuje je na standardowe wyjście. Filtry są na ogół połączone liniowo^ i tworzą potok powłoki. Filtr staje się koprocesem, gdy ten sam program generuje jego dane wejściowe i czyta jego dane wyjściowe. ;

14. Komunikacja międzyprocesowa

521

14.4. Koprocesy

W KornShellu są zaimplementowane koprocesy (Bolsky i Korn, 1989 [16]). Powłoki Bourne'a i C nie dostarczają sposobu połączenia procesów, by tworzyły koprocesy. Koproces jest zazwyczaj uruchamiany przez powłokę w tle, a jego standardowe wejście i standardowe wyjście są połączone z innym programem za pomocą łącza komunikacyjnego. Wprawdzie składnia powłoki wymagana do zainicjowania koprocesu i połączenia jego wejścia i wyjścia z innymi procesami jest nieco zawiła (więcej szczegółów można znaleźć na stronach 65-66 książki powyższych autorów [16]), ale koprocesy są również bardzo przydatne w programach C. Funkcja popen daje nam łącze jednokierunkowe do standardowego wejścia lub standardowego wyjścia innego procesu, ale używając koprocesów mamy dwa jednokierunkowe łącza komunikacyjne związane z innym procesem jedno z jego standardowym wejściem, drugie z jego standardowym wyjściem. Zamierzamy zapisywać dane do standardowego wejścia koprocesu, który przekształci te dane, a następnie odczytamy je ze standardowego wyjścia.

#include

"ourhdr.h"

int

main(void) ;1 int char

n, i n t l , int2; linę[MAXLINE1;

while ( (n = read(STDIN_FILENO, linę, MAXLINE)) > 0) { linę [n] = 0; /* kończymy pustym znakiem */ if (sscanf(linę, "%d%d", sintl, sint2) == 2) { sprintf(linę, "%d\n", intl + int2); n = strlen(linę); if (write(STDOUT_FILENO, linę, n) != n) err_sys("write error"); } else { if (write(STDOUT_FILENO, "invalid args\n", 13) != 13) err_sys("write error"); }

ykład

exit (0) ;

Przyjrzyjmy się przykładowi użycia koprocesów. Proces tworzy dwa łącza komunikacyjne: jedno jest standardowym wejściem koprocesu, drugie jego standardowym wyjściem. Zobrazowaliśmy to na rys. 14.8. proces macierzysty fdl[l] fd2[0]

łącze1 łącze2

Próg. 14.8 Prosty filtr dodający dwie liczby

)roces potomny (koproces)

#include #include

stdin

static void sig_pipe(int);

stdout

int main(void) { int pid_t char

Rys. 14.8 Sterowanie koprocesem przez zapisywanie jego standardowego wejścia i odczytywanie standardowego wyjścia

Program 14.8 jest prostym koprocesem, który czyta ze standardowego wejścia dwie liczby, oblicza ich sumę i zapisuje ją do standardowego wyjścia. Kompilujemy ten program, umieszczając moduł wykonywalny w pliku

"ourhdr.h" /* nasza procedura obsługi sygnału */

n, f d l [ 2 ] , fd2[2]; pid; linę[MAXLINE];

if (signal(SIGPIPE, sig_pipe) == SIG_ERR) err_sys("signal error"); if (pipę(fdl) < 0 || pipe(fd2) < 0) err_sys("pipę error");

add2.

Program 14.9 po przeczytaniu dwóch liczb ze standardowego wejścia wywołuje koproces add2. Wartość otrzymana z koprocesu jest wypisywana na standardowe wyjście. Tworzymy tu dwa łącza komunikacyjne, a następnie procesy macierzysty i potomny zamykają niepotrzebne im końce. Musimy użyć dwóch łączy: jednego dla standardowego wejścia koprocesu, drugiego dla jego standardowego wyjścia. Potomek wywołuje funkcję dup2, aby przed wywołaniem e x e c l powiązać deskryptory łącza komunikacyjnego ze standardowym wejściem i standardowym wyjściem.

if ( (pid = fork()) < 0) err_sys("fork error"); else if (pid > 0) { /* proces macierzysty */ close(fdl[0]); close(fd2[l]); while (fgetsdine, MAXLINE, stdin) != NULL) { n = strlen(linę) ; if (write(fdl[l], linę, n) != n) err_sys("write error to pipę");

U

14. Komunikacja międzyprocesowa if ( (n = read(fd2[0], linę, MAXLINE)) < 0) err_sys("read error from pipę"); if (n == 0) { err_msg("child closed pipę"); break; } line[n] = 0; /* kończymy pustym znakiem */ if (fputsfline, stdout) == EOF) err_sys("fputs error"); } if (ferror(stdin)) err_sys("fgets error on stdin"); exit(0) ; } else { /* proces potomny */ close(fdl[l]); close(fd2[0]); if (fdl[0] != STDIN_FILENO) { if (dup2(fdl[0], STDIN_FILENO) != STDIN_FILENO) err_sys("dup2 error to stdin"); close(fdl[0]); } if (fd2[l] != STDOUT_FILENO) { if (dup2(fd2[1], STDOUT_FILENO) != STDOUT_FILENO) err_sys("dup2 error to stdout"); close(fd2[l]); } if (execl("./add2", "add2", (char *) 0) < 0) err_sys("execl error");

tic void _pipe(int signo) printf("SIGPIPE c a u g h t W exit (1);

Próg. 14.9 Program korzystający z filtra add2

Kompilujemy i uruchamiamy próg. 14.9 - okazuje się, że działa tak, jak się spodziewaliśmy. Jeżeli usuniemy koproces add2 (za pomocą polecenia k i l l ) , kiedy próg. 14.9 oczekuje na swoje dane wejściowe, a następnie wprowadzimy dwie liczby, wtedy podczas zapisu danych do łącza, które nie jest już gotowe do odczytu danych, zostanie wywołana procedura obsługi sygnału. (Zobacz ćw. 14.4). W programie 15.1 pokażemy inną wersję tego przykładu, korzystającą z pojedynczego łącza dwukierunkowego zamiast dwóch łączy jednokierunkowych. •

523

14.4. Koprocesy

Przykład W koprocesie add2 (próg. 14.8) celowo użyliśmy uniksowego wejścia-wyjścia, funkcji read i w r i t e . Co się stanie, gdy zmienimy program koprocesu, by używał standardowego wejścia-wyjścia? Program 14.10 jest nową wersją koprocesu. #include

ourhdr.h

int main(void) (

intl, int2; line[MAXLINE];

int char

while (fgets(linę, MAXLINE, stdin) !=NULL) { if (sscanf(linę, "%d%d", Sintl, &int2) == 2) { if (printf("%d\n", intl + int2) == EOF) err_sys("printf error"); } else { if (printf("invalid args\n") == EOF) err_sys("printf error"); } exit(0);

Próg. 14.10 Filtr dodający dwie liczby, używający standardowego wejścia-wyjścia

Jeżeli wywołamy ten nowy koproces z próg. 14.9, to okaże się, że program nie działa poprawnie. Problem wiąże się z domyślnym buforowaniem standardowego wejścia-wyjścia. Gdy wywołujemy próg. 14.10, wówczas pierwsze wywołanie funkcji fgets dla standardowego wejścia powoduje, że biblioteka standardowego wejścia-wyjścia alokuje bufor i wybiera odpowiedni typ buforowania. Ponieważ standardowe wejście jest łączem komunikacyjnym, więc funkcja i s a t t y przekazuje wartość „fałsz" i biblioteka standar-j dowego wejścia-wyjścia ustawia domyślne buforowanie pełne. To samo dzie-', je się ze standardowym wyjściem. Kiedy program add2 jest zablokowany i w operacji odczytu ze standardowego wejścia, próg. 14.9 jest zablokowany) w odczycie z łącza. Powstało zakleszczenie. , j W tym przypadku mamy jednak kontrolę nad koprocesem, który jest wy-1* konywany. Możemy zmienić próg. 14.10, dodając przed pętlą while poniż-\ sze cztery wiersze. J if (setvbuf(stdin, NULL, _IOLBF, 0) != 0) err_sys("setvbuf error"); if (setvbuf(stdout, NULL, _IOLBF, 0) != 0) err_sys("setvbuf error");

'* ;


0) if (writen(clifd, msg, n) != n) return(-1);

i n t send_err(int spipefd, i n t status, const char *errmsg) ; Przekazują: 0, jeśli wszystko w porządku; -1, jeśli wystąpił błąd int recv_fd(int spipefd, ssize_t (*userfunc) (int, const void *, size_t) ) ; Przekazuje: deskryptor pliku, jeśli wszystko w porządku; wartość ujemną, jeśli wystąpił błąd

Gdy proces (na ogół serwer) chce przekazać deskryptor do innego procesu, wówczas wywołuje tylko funkcję s e n d f d lub s e n d e r r . Proces oczekujący na odbiór deskryptora (klient) wywołuje funkcję recv_f d.

n;

if (errcode >= 0) errcode = -1;

/* wysyłamy komunikat o błędzie */ i

/* zmienna musi być ujemna */

if (send_fd(clifd, errcode) < 0) return(-1); return(0);

Próg. 15.4 Funkcja send e r r

15. Zaawansowane metody komunikacji międzyprocesowej Kolejne trzy podrozdziały prezentują istniejące w systemach SVR4 i 4.3+BSD implementacje dwóch funkcji: s e n d f d oraz recv_f d.

15.3.1

System V Wydanie 4

W systemie SVR4 deskryptory plików są wymieniane między procesami za pomocą funkcji i o c t l oraz jej operacji I_SENDFD i I_RECVFD. Aby wysłać deskryptor, w trzecim argumencie funkcji i o c t l podajemy przekazywany deskryptor. Pokazuje to próg. 15.5. dudę .clude .clude

"ourhdr.h"

Funkcja przekazuje deskryptor do innego procesu. jjeśli fd= 0) if (ioctl(clifd, I_SENDFD, fd) < 0) return(-1); return(0); Próg. 15.5 Funkcja s e n d _ f d w systemie SVR4

Gdy odbieramy deskryptor, to trzeci argument funkcji i o c t l jest wskaźnikiem do struktury s t r r e c v f d . struct strrecvfd { int fd; /* nowy deskryptor */ uid t uid; /* obowiązujący identyfikator użytkownika nadawcy */ gid_t gid; /* obowiązujący identyfikator grupy nadawcy */ char f i l l [ 8 ] ;

15.3. Przekazywanie deskryptorów pliku #include #include #include /* * * *

567

"ourhdr.h"

Funkcja odbiera deskryptor pliku z innego procesu (serwera). Dodatkowo, wszystkie dane odebrane z serwera są przekazywane w wywołaniu (*userfunc)(STDERR_FILENO, buf, nbytes). Do odbioru fd z funkcji send_fd() jest używany protokół 2-bajtowy. */

int recv_fd(int servfd, ssize_t (*userfunc)(int, const void *, size_t)) int char struct strbuf struct strrecvfd

newfd, nread, flag, status; *ptr, buf[MAXLINE]; dat; recvfd;

status = - 1 ; for ( ; ; ) { dat.buf = buf; dat.maxlen = MAXLINE; flag = 0; if (getmsg(servfd, NULL, Sdat, sflag) < 0) err_sys("getmsg error"); nread = dat.len; if (nread == 0) { err_ret("connection closed by server"); return(-1); } /* Sprawdzamy, czy są to ostatnie dane zawierające pusty * znak i stan. Pusty znak musi być przedostatnim bajtem * w buforze, a stan ostatnim. Stan równy 0 oznacza, że * należy odebrać deskryptor pliku. */ for (ptr = buf; ptr < sbuf[nread]; ) { if (*ptr++ == 0) { if (ptr != sbuf[nread-1] ) err_dump("message format error"); status = *ptr & 255; if (status == 0) { if (ioctl(servfd, I_RECVFD, &recvfd) < 0) return (-1); newfd = recvfd.fd; /* nowy deskryptor */ } else newfd = -status; nread -= 2; } } if (nread > 0) if ((*userfunc)(STDERR_FILENO, buf, nread) != nread) return(-1); if (status >= 0) /* nadeszły ostatnie dane */ return(newfd); /* deskryptor lub -status */

Próg. 15.6 Funkcja r e c v _ f d w systemie SVR4

15. Zaawansowane metody komunikacji międzyprocesowej Funkcja recv_fd czyta dane z łącza strumieniowego, dopóki nie przeczyta pierwszego bajtu naszego dwubajtowego protokołu (pusty bajt). Gdy wywołujemy funkcję i o c t l z operacją I_RECVFD, wówczas następny komunikat do odczytu w czole strumienia musi być deskryptorem przekazanym przez operację I_SENDFD, W przeciwnym razie odbierzemy błąd. Pokazuje to próg. 15.6.

15.3. Przekazywanie deskryptorów pliku #include #include #include (finclude #include #include



"ourhdr.h"

569

/* struct msghdr */ /* struct iovec */

/* Funkcja przekazuje deskryptor do innego procesu. Jeśli fd i w systemie 4.3BSD wygląda następująco: struct msghdr { caddr_t msg_name; /* opcjonalny adres */ int msg_namelen; /* rozmiar adresu */ struct iovec *msg_iov; /* tablica do wysłania/odebrania */ int msg_iovlen; /* liczba elementów w tablicy msg_iov */ caddr_t insg_accrights; /* prawa dostępu do wysłania/odbioru */ int msg_accrightslen; /* rozmiar bufora z prawami dostępu */

Pierwsze dwa pola są typowo używane do przesyłania datagramów przez połączenie sieciowe i służą do wskazania adresu docelowego każdego datagramu. Następne dwa pola umożliwiają zdefiniowanie tablicy buforów (rozproszony odczyt lub zbierający zapis) według opisu funkcji readv oraz w r i t e v (podrozdz. 12.7). Ostatnie dwa pola są związane z przekazywaniem i odbiorem praw dostępu. Obecnie jest możliwe przekazywanie wyłącznie praw dostępu do deskryptorów plików. Prawa dostępu mogą być przekazywane tylko przez gniazdo dziedziny Unix (czyli za pomocą techniki implementującej łącza strumieniowe w systemie 4.3BSD). Aby przesyłać lub odbierać deskryptor pliku, musimy ustalić wartości pól: pole m s g a c c r i g h t s wskazuje deskryptor całkowitoliczbowy, a pole m s g _ a c c r i g h t s l e n ma być równe rozmiarowi deskryptora (czyli rozmiarowi liczby całkowitej). Rozmiar deskryptora musi być niezerowy. W programie 15.7 pokazujemy funkcję send_fd w systemie 4.3BSD. W wywołaniu sendmsg przesyłamy zarówno dwa bajty danych protokołu (^bajt pusty i bajt stanu), jak i deskryptor.

int send_fd(int clifd, int fd) struct iovec struct msghdr c h a r

iov[l]; msg; buf[2];

/* 2-bajtowy protokół send_fd()/recv_fd()

iov[0].iov_base = buf; iov[0].iov_len = 2; msg.msg_iov = iov; msg.msg_iovlen = 1; msg.msg_name = NULL; msg.msg_namelen = 0; if (fd < 0) { msg.msg_accrights = NULL; msg.msg_accrightslen = 0; buf[1] = -fd; /* niezerowy stan oznacza błąd */ if (buf[l] == 0) buf[1] = 1; /* -256 itp. może zaszkodzić protokołowi */ } else { msg.msg_accrights = (caddr_t) sfd; /* adres deskryptora */ msg.msg_accrightslen = sizeof(int); /* przekazujemy jeden deskryptor */ buf[1] = 0; /* stan zero oznacza, że wszystko jest w porządku buf[0] = 0;

/* zerowy bajt sygnalizatora dla recv_fd() */

if (sendmsg(clifd, &msg, 0) != 2) return (-1); return(O);

Prog. 15.7 Funkcja send_f d w systemie 4.3BSD

W celu odbioru deskryptora pliku czytamy z łącza strumieniowego, dopóki nie przeczytamy pustego bajtu, poprzedzającego ostatni bajt stanu. Wszystko po tym pustym bajcie jest komunikatem awaryjnym od nadawcy. Pokazujemy to w prog. 15.8.

15. Zaawansowane metody komunikacji międzyprocesowej ludę ludę ludę ludę ludę

/* struct msghdr */

/* struct iovec */

"ourhdr.h"

unkcja odbiera z innego procesu (serwera) deskryptor pliku. Dodatkowo, szystkie dane odebrane z serwera są przekazywane w wywołaniu *userfunc)(STDERR_FILENO, buf, nbytes). Do odbioru fd z funkcji end f d O j e s t używany protokół 2-bajtowy. */

15.3. Przekazywanie deskryptorów pliku

571

else newfd = -status; nread -= 2;

if (nread > 0) if {(*userfunc)(STDERR_FILENO, buf, nread) != nread) return(-1); if (status >= 0) return(newfd);

/* nadeszły ostatnie dane */ /* deskryptor lub -status */

fdfint servfd, ssize_t (*userfunc)(int, const void *, size_t)) int char struct iovec struct msghdr

newfd, nread, status; *ptr, buf[MAXLINE]; iov[l]; msg;

status = - 1 ; for ( ; ; ) { iov[0].iov_base = buf; iov[0].iov_len = sizeof(buf); msg.msg_iov = iov; msg.msg_iovlen = 1; msg.msg_name = NOLL; msg.msg_namelen = 0; msg.msg_accrights = (caddr_t) &newfd; /* adres deskryptora */ msg.msg_accrightslen = sizeof(int); /* odbieramy jeden deskryptor */ if ( (nread = recvmsg(servfd, smsg, 0)) < 0) err_sys("recvmsg error"); else if (nread == 0) { err_ret("connection closed by server"); return(-1);

/* Sprawdzamy, czy są to ostatnie dane zawierające pusty * znak i stan. Pusty znak musi być przedostatnim bajtem * w buforze, a stan ostatnim. Stan zero oznacza, że należy * odebrać deskryptor pliku. */ for (ptr = buf; ptr < sbuf[nread]; ) { if (*ptr++ == 0) { if (ptr != Sbuf[nread-1]) err_dump("message format error"); status = *ptr & 255; if (status == 0) { if (msg.msg_accrightslen != sizeof(int)[ err dump("status = 0 but no f d " ) ; /* newfd = nowy deskryptor */

Próg. 15.8 Funkcja r e c v _ f d w systemie 4.3BSD

Zwróćmy uwagę, że zawsze jesteśmy przygotowani na odbiór deskryptora (ustalamy wartości pól msg_accrights i m s g _ a c c r i g h t s l e n przed każdym wywołaniem funkcji recvmsg), a o rzeczywistym odbiorze deskryptora świadczy niezerowa wartość pola m s g _ a c c r i g h t s l e n po powrocie z wywołania.

15.3.3 System 4.3+BSD Począwszy od systemu 4.3BSD Reno zmieniła się definicja struktury msghdr. Ostatnie dwa pola, które nazywaliśmy w poprzednich wersjach „prawami dostępu", stały się „danymi dodatkowymi". Oprócz tego, na końcu struktury dodano nowe pole, msg_f l a g s . struct msghdr { caddr_t msg_name; /* opcjonalny adres */ int msg_namelen /* rozmiar adresu */ struct iovec *msg_iov; /* tablica do wysłania/odebrania */ int msg_iovlen; /* liczba elementów w tablicy msg_iov */ caddr_t msg_control /* prawa dostępu do wysłania/odbioru */ u_int msg_controllen; /* rozmiar bufora z prawami dostępu */ int msg_flags; /* sygnalizatory w odbieranym komunikacie */

Pole msg_control wskazuje teraz strukturę cmsghdr (nagłówek komunikatu sterującego). struct cmsghdr { u_int cmsg_len; /* licznik bajtów danych, łącznie z nagłówkiem */ int cmsg_level; /* protokół */ int cmsg_type; /* typ zależny od protokołu */ /* poniżej są umieszczone rzeczywiste dane komunikatu sterującego */

15. Zaawansowane metody komunikacji międzyprocesowej

ludę ludę :lude ludę :lude :lude

cms g_t ype = SCM_RIGHTS; cmptr->cmsg_len = CONTROLLEN; msg.msg_control = (caddr_t) cmptr; msg.msg_controllen = CONTROLLEN; *(int *)CMSG_DATA(cmptr) = fd; /* fd do przekazania */ buf[1] = 0; /* stan zero oznacza, że wszystko jest w porządku */ buf[0]

0;

/* zerowy bajt sygnalizatora dla recv_fd () */

if (sendmsg(clifd, &msg, 0) != 2) return(-1); return (0); Próg. 15.9 Funkcja s e n d _ f d w systemie 4.3BSD Reno

573

15.3. Przekazywanie deskryptorów pliku

#include #include #include #include #include

= 0) /* nadeszły ostatnie dane */ return(newfd); /* deskryptor lub -status */

1. Klient wysyła do serwera przez łącze strumieniowe żądanie o postaci o p e n \0

jest wartością numeryczną, podaną dziesiętnie i odpowiadającą drugiemu argumentowi funkcji open. Napis żądania jest zakończony pustym bajtem. 2. Serwer po wywołaniu funkcji send_f d lub s e n d _ e r r odsyła otwarty deskryptor pliku lub błąd.

Próg. 15.10 Funkcja recv_f d w systemie 4.3BSD Reno

|4 Serwer otwierający pliki, wersja 1 Używając techniki przekazywania deskryptorów, zaimplementujemy teraz serwer otwierający pliki: program wykonywany (exec) przez proces, który chce otworzyć plik lub pliki. Zamiast serwera, który przekazuje w odpowiedzi do klienta zawartość pliku, nasz program będzie przesyłał otwarty deskryptor pliku. Dzięki temu serwer będzie mógł obsługiwać dowolnego typu pliki (również linie modemowe lub połączenia sieciowe), a nie tylko pliki zwykłe. Oznacza to również, że przez kanał komunikacji międzyprocesowej będzie przekazywana minimalna ilość informacji - od klienta do serwera tylko nazwa pliku oraz tryb otwarcia, a od serwera do klienta deskryptor pliku. W takim rozwiązaniu zawartość pliku nie jest przekazywana przy użyciu komunikacji międzyprocesowej. Zaprojektowanie serwera jako oddzielnego programu wykonywalnego (albo takiego, który jest wykonywany przez klienta, jak pokażemy w tym podrozdziale, albo serwera typu demon, jak pokażemy w podrozdz. 15.6) ma wiele zalet.

575

Zajmiemy się teraz przykładem procesu wysyłającego otwarty deskryptor do swojego procesu macierzystego. W podrozdziale 15.6 zmodyfikujemy ten przykład, aby korzystał z serwera typu demon przesyłającego deskryptor do zupełnie niezależnego procesu. Najpierw pokazujemy plik nagłówkowy open. h (próg. 15.11), który włącza standardowe systemowe pliki nagłówkowe i definiuje prototypy funkcji. łtinclude #include #include

0) if (write(STDOUT_FILENO, buf, n) != n) err_sys("write e r r o r " ) ; if (n < 0) err_sys("read e r r o r " ) ; close(fd);

exit (0);

Próg. 15.12 Funkcja main Funkcja csopen (próg. 15.13) po utworzeniu łącza strumieniowego wywołuje kolejno f ork i exec, by wykonać program serwera. Potomek zamyka jeden koniec łącza, a proces macierzysty zamyka koniec przeciwny. Potomek powiela swój koniec łącza na standardowe wejście i wyjście, aby mógł poprawnie wykonać program serwera. (W innym rozwiązaniu możemy przekazać deskryptor f d [ 1 ] reprezentowany w kodzie ASCII jako argument wywołania programu serwera). Proces macierzysty przesyła do serwera żądanie zawierające nazwę ścieżki oraz tryb otwarcia. Na koniec proces macierzysty wywołuje funkcję recv_fd, aby przekazać albo deskryptor, albo błąd. Jeśli serwer przekazuje błąd, to jest wywoływana funkcja w r i t e , która wypisuje komunikat na standardowym strumieniu komunikatów awaryjnych.

-1, -1 };

/* za pierwszym razem fork/exec naszego otwierającego serwera */ if (s_pipe(fd) < 0) err_sys("s_pipe error"); if ( (pid = fork()) < 0) err_sys("fork error"); else if (pid == 0) { /* proces potomny */ close(fd[0]); if (fd[l] != STDIN_FILENO) { if (dup2(fd[l], STDIN_FILENO) != STDIN_FILENO) err_sys("dup2 error to stdin"); } if (fd[l] != STDOUT_FILENO) { if . (dup2(fd[l], STDOUT_FILENO) != STDOUT_FILENO) err_sys("dup2 error to stdout"); } if (execl("./opend", "opend", NULL) < 0) err_sys("execl error"); } close(fd[l] /* proces macierzysty */

sprintf(buf, " %d", oflag); /* wartość oflag w postaci ASCII */ iov[0].iov_base = CL_OPEN " "; iov[0].iov_len = strlen(CL_OPEN) + 1; iov[l].iov_base = name; iov[l].iov_len = strlen(name); iov[2].iov_base = buf; iov[2].iov_len = strlen(buf) + 1; /* +1, by zmieścił się pusty znak na końcu buf */ len = iov[0].iov_len + iov[1].iov_len + iov[2].iov len; if (writev(fd[0], siov[0], 3) != len) ~ err_sys("writev error"); /* czytamy deskryptor, przekazane błędy obsługuje write() */ return( recv_fd(fd[0], write) );

Próg. 15.13 Funkcja csopen

15. Zaawansowane metody komunikacji międzyprocesowej

15.4. Serwer otwierający pliki, wersja 1

Przyjrzyjmy się teraz serwerowi otwierającemu plik. Jest to program opend wykonywany przez klienta w próg. 15.13. Najpierw pokazujemy plik nagłówkowy opend.h (próg. 15.14), który włącza systemowe pliki nagłówkowe i deklaruje zmienne globalne oraz prototypy funkcji. clude clude clude

/* żądanie klienta do serwera */

/* deklaracja zmiennych globalnych */ errmsg[]; /* napis komunikatu o błędzie przekazywany do klienta */ ern int oflag; / * s y g n a l i z a t o r y o p e n ( ) : O_xxx ...*/ e r n char *pathname; / * nazwa p l i k u otwieranego d l a k l i e n t a * / ern char

/* prototypy funkcji */ c l i _ a r g s ( i n t , char * * ) ; reąuest(char *, int, i n t ) ; Próg. 15.14 Plik nagłówkowy o p e n d . h

Funkcja main (próg. 15.15) czyta z łącza strumieniowego (standardowe wejście) żądanie od klienta i wywołuje funkcję r e ą u e s t . "opend.h"

clude

/* definicja zmiennych globalnych */ errmsg[MAXLINE]; oflag; *pathname;

r

n (void) int char

Funkcja r e ą u e s t z próg. 15.16 realizuje wszystkie potrzebne działania. Wywołuje funkcję b u f a r g s , aby podzielić żądanie klienta na standardową listę argumentów (na wzór tablicy argv) i wywołuje funkcję c l i _ a r g s , by przetworzyć argumenty przekazane przez klienta. Jeśli nie ma żadnych problemów z argumentami, to dla danego pliku jest wywoływana funkcja open, a potem funkcja send_fd, by przez łącze strumieniowe (standardowe wyjście) odesłać deskryptor do klienta. Jeśli pojawił się błąd, to jest wywoływana funkcja send_err, aby odesłać komunikat o błędzie, używając protokołu, który opisaliśmy wcześniej.

"ourhdr.h"

fine CL OPEN "open"

nread; buf[MAXLINE] ;

for ( ; ; ) { /* czytamy bufor od k l i e n t a , przetwarzamy żądanie */ if ( (nread = read(STDIN^FILENO, buf, MAXLINE)) < 0) err_sys("read e r r o r on stream p i p ę " ) ; else if (nread == 0) breąk; /* k l i e n t zamknął łącze strumieniowe */ reąuest(buf, nread, STDIN_FILENO); }

exit (0);

Próg. 15.15 Funkcja main

579

#include #include

"opend.h"

void reąuest(char *buf, int nread, int fd)

int

newfd;

if (buf[nread-1] != 0) { sprintf(errmsg, "reąuest not null terminated: %*.*s\n", nread, nread, buf) ; send_err(fd, - 1 , errmsg); return;

/* analizujemy argumenty, ustalamy opcje */ if (buf_args(buf, cli_args) < 0) send_err(fd, - 1 , errmsg); return;

if ( (newfd = open(pathname, oflag)) < 0) { sprintf(errmsg, "can't open %s: %s\n", pathname, strerror(errno) send_err(fd, - 1 , errmsg); return;

/* wysyłamy deskryptor * if (send_fd(fd, newfd) < 0) err_sys("send_fd error") close(newfd); /* żako

ńczona obsługa deskryptora */

Próg. 15.16 Funkcja reąuest Żądanie klienta jest napisem zakończonym pustym bajtem, a kolejne argumenty są oddzielone odstępami lub znakami tabulacji. Funkcja buf_args z próg. 15.17 przekształca ten napis do postaci listy argumentów w stylu tablicy argv i wywołuje specjalną funkcję przetwarzającą te argumenty. Bę-

15. Zaawansowane metody komunikacji międzyprocesowej dziemy jeszcze korzystać z tej funkcji w bieżącym rozdziale oraz w rozdziale 18. Używamy funkcji ANSI C s t r t o k do wyodrębnienia elementów w napisie, by utworzyć listę argumentów. Ludę

"ourhdr.h"

Lne MAXARGC 50 /* maksymalna liczba argumentów w buforze buf */ ine WHITE " \t\n" /* znaki odstępów służące do wydzielenia argumentów */ uf[] zawiera argumenty oddzielone znakami odstępów. Konwertujemy buf o postaci tablicy wskaźników argv[] i wywołujemy funkcję *optfunc) (), do przetwarzania tablicy argv[]. rzekazujemy wywołującemu funkcję wartość -1, jeśli pojawił się problem rzy analizie bufora; w przeciwnym razie przekazujemy wartość otrzymaną funkcji optfuncO. Zauważmy, że tablica buf[] jest modyfikowana (po ażdym elemencie jest w niej umieszczany pusty znak). */

15.5. Funkcje obsługujące połączenia klient-serwer #include

581

"opend.h"

/* Tę funkcję wywołuje buf_args(), którą z kolei wywołuje funkcja * reąuest () . Funkcja buf_args() utworzyła z bufora klienta tablicę * w stylu argv[], którą teraz przetwarzamy. */ int cli_args(int argc, {

char **argv)

if (argc != 3 || strcmp(argv[0], CL_OPEN) != 0) { strcpy(errmsg, "usage: \n"); return(-1); }

pathname = a r g v [ l ] ; /* zapamiętujemy wskaźnik do otwieranej nazwy ścieżki */ oflag = a t o i ( a r g v [ 2 ] ) ; return(0);

args(char *buf, int (*optfunc)(int, char **)) char int

Próg. 15.18

*ptr, *argv[MAXARGC]; argc;

if (strtok(buf, WHITE) == NULL) return(-1); argv[argc = 0] = buf;

argv[++argc] = NULL; return(

(*optfunc)(argc, argv) ); /* Wskaźniki argv[] wskazują dane w buforze buf[], więc funkcja użytkownika może je przekopiować i nie jest i s t o t n e , że tablica argv[] znika przy powrocie. */

Próg. 15.17

Po omówieniu w kolejnym podrozdziale połączeń klient-serwer, w podrozdz. 15.6 przerobimy ten przykład serwera, by używał jednego serwera typu demon, z którym łączą się wszyscy klienci.

/* argv[0] jest wymagany */

while ( (ptr = strtok(NULL, WHITE)) != NULL) { if (++argc >= MAXARGC-1) /* -1, by na końcu było miejsce dla NULL */ return(-1); argv[argc] = ptr;

Funkcja buf _args

Funkcja buf_args wywołuje inną funkcję serwera, c l i _ a r g s (próg. 15.18). Jej zadaniem jest sprawdzenie, czy klient przesłał poprawną liczbę argumentów, oraz zapamiętanie w zmiennych globalnych nazwy ścieżki i trybu otwarcia. W ten sposób otrzymujemy serwer otwierający pliki, który jest wywoływany z programu klienta przez funkcje fork i exec. Przed wywołaniem fork powstaje pojedyncze łącze strumieniowe służące do komunikacji między klientem a serwerem. W takim scenariuszu jeden serwer obsługuje jednego klienta.

Funkcja c l i _ a r g s

15.5

Funkcje obsługujące połączenia klient-serwer

Łącza strumieniowe są przydatne, gdy korzystamy z komunikacji międzyprocesowej w procesach pokrewnych, np. macierzystym i potomnym. Serwer otwierający pliki, pokazany w poprzednim podrozdziale, mógł przekazywać deskryptory plików przez nienazwane łącze strumieniowe. Jeżeli jednak obsługujemy komunikację niezależnych procesów (na przykład, gdy serwer jest demonem), to jest nam potrzebne nazwane łącze strumieniowe. Możemy utworzyć nienazwane łącze strumieniowe (za pomocą funkcji s p i p e ) i przypisać każdemu końcowi łącza nazwę ścieżki w systemie pli- j ków. Serwer typu demon może utworzyć łącze strumieniowe i korzystać tylko z jego jednego końca, któremu jest przypisywana nazwa. W ten sposób nie» zależne od siebie procesy klienckie mogą nawiązać kontakt z demonem i wysyłać komunikaty do końca łącza przydzielonego serwerowi. Jest to podobne < rozwiązanie do pokazanego na rys. 14.11, gdzie do wysłania żądań klienckich używaliśmy ogólnie znanej kolejki FIFO klienta. • Zapewne lepszym rozwiązaniem jest użycie techniki, w której serwer j tworzy jeden koniec łącza strumieniowego o ogólnie znanej nazwie, a klient nawiązuje połączenie z tym końcem. Dodatkowo, za każdym razem, gdy no- , wy klient nawiązuje połączenie z nazwanym łączem strumieniowym serwera, powstaje nowe łącze strumieniowe między klientem a serwerem. W ten spo-

15. Zaawansowane metody komunikacji międzyprocesowej sób serwer jest powiadamiany o każdym dołączeniu się nowego klienta oraz o zakończeniu pracy przez klienta. Taką formę IPC wspierają systemy SVR4 oraz 4.3+BSD. W tym podrozdziale przygotujemy trzy funkcje, używane w modelu klient-serwer do ustanowienia połączeń klienckich. #include "ourhdr.h" int serv_listen(const char *name); Przekazuje: deskryptor pliku, przeznaczony do nasłuchu, jeśli wszystko w porządku; wartość ujemną, jeśli wystąpił błąd

Najpierw serwer musi zgłosić chęć nasłuchu na nadchodzące połączenia klienckie na deskryptorze związanym z ogólnie znaną nazwą (nazwą ścieżki w systemie plików); służy do tego funkcja s e r v _ l i s t e n . Argument name jest ogólnie znaną nazwą serwera. Klienci będą używać tej nazwy, gdy będą chcieli połączyć się z tym serwerem. Wynikiem funkcji s e r v _ l i s t e n jest deskryptor pliku związany z końcem nazwanego łącza strumieniowego przeznaczonym dla serwera. Serwer po wywołaniu funkcji s e r v _ l i s t e n wywołuje funkcję serv_ accept, aby rozpocząć oczekiwanie na połączenia klienta. łtinclude "ourhdr.h" int serv_accept (int listenfd, uid_t *uidptr) ; Przekazuje: nowy deskryptor pliku, jeśli wszystko w porządku; wartość ujemną, jeśli wystąpił błąd

Argument listenfd jest deskryptorem otrzymanym z funkcji s e r v _ l i s t e n . Ta funkcja nie powróci, dopóki do ogólnie znanej nazwy serwera nie dołączy się jakiś klient. Gdy klient nawiąże połączenie z serwerem, wówczas automatycznie powstaje nowe nazwane łącze strumieniowe, a wynikiem funkcji jest nowy deskryptor. Dodatkowo w miejscu pamięci wskazanym argumentem uidptr jest zapamiętywany obowiązujący identyfikator procesu klienta. Klient, aby połączyć się z serwerem, wywołuje funkcję c l i _ c o n n . #include "ourhdr.h"

15.5. Funkcje obsługujące połączenia klient-serwer

Używając tych trzech funkcji, możemy pisać OBZią ^maśom jjpjfnui nów służące do obsługi dowolnej liczby klientówółnailjl ^dsoil janlowob i§ jest liczba deskryptorów dostępnych w jednym rn^nbaL w rio^ną^aob we wymaga po jednym deskryptorze dla każdego połłoą ogabśBj! Bib asioJą^Ma; używają zwykłych deskryptorów plików, więc servi3a oaiw twć»lilq wóiotq\£i>l cję s e l e c t lub p o l l do obsługi żądań wszystkjłJz^saw nEb^ś igutedo ob zwielokrotnienie wejścia-wyjścia. Ponieważ wszy^sew śBwsinocI .Biozjxw-Bi: wer są łączami strumieniowymi, więc można prze3siq Bnxorn oaiw jrn^woin; deskryptory. W kolejnych dwóch punktach przeanalizujemymajusilBriBas^ rb£j>Inuq dc funkcji w systemach SVR4 oraz 4.3+BSD. Następą^teBU .CI2a+£> SBIO J\HV robimy nasz serwer otwierający pliki z podrożsoiboq s i>lilq dynczego serwera typu demon, działającego przy \[xiq og30BJBteixb Będziemy również z nich korzystać w rozdziale 1SI alfiisbsoi w aste^sioM rloi serwera akceptującego dowolne połączenia. .Bin3so^łoq anlowob

15.5.1

9inBbyVl

System V Wydanie 4

W systemie SVR4 są dostępne montowane strummin:te snBwoJnorn rzania strumieni o nazwie connld. Tych kompone3noqmo>l rfo^T .blnnoo aiv implementować nazwane łącza strumieniowe, twoiowJ t3woin3irmn:fe BSOBJ sn z serwerem. Presotto i Ritchie zaprojektowali montowane strumiesirntnlz srrawolnom i temu Research Unix [39]. Następnie pomysł ten zastosołZBs nal fz^moą ai

Najpierw serwer tworzy nienazwane łącze : aso^t anBWSKnain jednego z końców łącza moduł przetwarzania stnte BinBsiBwJasiq iubom waliśmy to na rys. 15.5. proces użytkowy fd[0]

fd[l]

czoło strumienia

czoło strumienia

[0]b:

int cli_conn(const char *name); Przekazuje: deskryptor pliku, jeśli wszystko w porządku; wartość ujemną, jeśli wystąpił błąd

Podawany przez klienta argument name musi być tą samą nazwą, której użyto w wywołaniu funkcji s e r v _ l i s t e n w serwerze. Odebrany deskryptor odnosi się do łącza strumieniowego, powiązanego z serwerem.

connld

blnnoo

Rys. 15.5 Łącze w systemie SVR4 po włożeniu modułuufuborn uinaśołw oq

15. Zaawansowane metody komunikacji międzyprocesowej Następnie przypisujemy nazwę ścieżki końcowi łącza, do którego był włożony moduł connld. W systemie SVR4 służy do tego funkcja f a t t a c h . Każdy proces, który otwiera (open) tę nazwę ścieżki (np. klient), odwołuje się do nazwanego końca łącza. Program 15.19 zawiera kod potrzebny do zaimplementowania funkcji serv_listen. iclude .clude iclude .clude

585

klient fd

clifdl

czoło strumienia

czoło strumienia

fd[0]

fd[l]

y/tmp/sevl

czoło strumienia



"ourhdr.h"

ifine FIFO_MODE

czoło strumienia jądro systemu connld

(S_IRUSRIS_IWUSRIS_IRGRP|S_IWGRPIS_IROTHIS_IWOTH) /* użytkownik rw, grupa rw, inni rw */

;

/* przekazujemy fd, j e ś l i wszystko w porządku; wartość ujemną, j e ś l i wystąpił błąd */ :v_listen(const char *name) int

15.5. Funkcje obsługujące połączenia klient-serwer

tempfd, fd[2], len;

/* tworzymy plik: punkt montowania dla fattach() */ unlink(name); if ( (tempfd = creat(name, FIFO_MODE)) < 0) return(-1); if (close(tempfd) < 0) return(-2); if (pipe(fd) < 0) return(-3) ; /* kładziemy do fd[l] connld, potem wywołujemy fattach() */ if (ioctl(fd[l], I_PUSH, "connld") < 0) return(-4); if (fattach(fd[l], name) < 0) return(-5); return(fd[0]); /* na deskryptorze fd[0] nadchodzi klienckie żądanie połączenia */ Próg. 15.19 Funkcja s e r v _ l i s t e n dla systemu SVR4

Gdy inny proces wywoła funkcje open dla nazwanego końca łącza (koniec z włożonym modułem connld), wówczas zachodzą poniższe zdarzenia: 1. Powstaje nowe łącze. 2. Jeden z deskryptorów nowego łącza jest przekazywany dó klienta jako wartość funkcji open. 3. Drugi deskryptor jest przekazywany do serwera przez jego drugi koniec łącza (czyli koniec, do którego nie włożono modułu connld). Serwer odbiera nowy deskryptor, używając funkcji i o c t l i operacji I RECVFD.

Rys. 15.6 Połączenie klient-serwer przez nazwane łącze strumieniowe

Przyjmijmy, że serwer przypisuje ( f a t t a c h ) swojemu łączu ogólnie znaną nazwę /tmp/servl. Na rysunku 15.6 pokazujemy uzyskane połączenia, gdy klient powrócił już z wywołania fd = open("/tmp/servl", O_RDWR);

Wywołując funkcję open, tworzymy łącze między klientem a serwerem otwierana nazwa ścieżki jest nazwanym strumieniem, do którego został włożony moduł connld. Funkcja open w programie klienta przekazuje deskryptor pliku (fd). Serwer otrzymuje nowy deskryptor pliku ( c l i f d l ) , wywołując dla deskryptora fd[0] funkcję i o c t l z operacją I_RECVFD. Serwer po włożeniu modułu connld do deskryptora f d [ l ] oraz przypisaniu nazwy temu deskryptorowi nigdy więcej nie używa jawnie f d [ 1 ]. Serwer czeka na połączenie klienta, wywołując funkcję s e r v _ a c c e p t pokazaną w próg. 15.20. Porównując wywołanie funkcji serv_accept z rys. 15.6, pierwszy argument tej funkcji jest deskryptorem pliku f d [ 0 ] , a wartość funkcji odpo- ,| wiada deskryptorowi c l i f d l . Klient inicjuje połączenie do serwera, wywołując funkcję c l i _ c o n n , w próg. 15.21. ' ; Dwukrotnie sprawdzamy, że przekazany deskryptor odnosi się do urzą- -.' dzenia strumieniowego, na wypadek gdyby serwer jeszcze nie wystartował, a dana nazwa ścieżki istniałaby w systemie plików. (W systemie SVR4 wy- • daje się mało sensowne wywołanie funkcji c l i _ c o n n zamiast wywołania J wprost funkcji open. W następnym punkcie zobaczymy, że w systemie BSD _ funkcja c l i c o n n jest bardziej skomplikowana). .

15. Zaawansowane metody komunikacji międzyprocesowej ;lude ;lude :lude ;lude

15.5. Funkcje obsługujące połączenia klient-serwer

15.5.2 System 4.3+BSD



"ourhdr.h"

W systemie 4.3+BSD połączenie klienta i serwera za pomocą gniazd dziedziny Unix wymaga innego zestawu operacji. Nie będziemy opisywać tu szczegółów dotyczących funkcji socket, bind, l i s t e n , a c c e p t i conn e c t , z których korzystamy na ogół w protokołach sieciowych. Zainteresowanych odsyłamy do książki Stevensa, 1990 [44].

"unkcja czeka na nadchodzące połączenia klienta i akceptuje je. )trzymuje identyfikator użytkownika klienta. */ /* przekazuje nowy deskr. fd, jeśli wszystko w porządku; wartość ujemną, jeśli wystąpił błąd */

Ponieważ system SVR4 również implementuje gniazda dziedziny Unix, więc kod zaprezentowany w tym podrozdziale działa też w tym systemie.

r_accept(int listenfd, uid_t *uidptr)

W programie 15.22 pokazujemy funkcję s e r v _ l i s t e n . Jest ona wywoływana przez serwer jako pierwsza.

struct strrecvfd recvfd; if (ioctl(listenfd, I_RECVFD, &recvfd) < 0) return(-l); /* być może EINTR, jeśli przechwycono sygnał */ if (uidptr != NULL) *uidptr = recvfd.uid;

return(recvfd.fd);

/* obowiązujący identyfikator użytkownika wywołującego funkcję */

/* przekazujemy nowy deskryptor */

Próg. 15.20 Funkcja serv_accept dla systemu SVR4 :lude :lude :lude ;lude



"ourhdr.h"

^unkcja tworzy kliencki punkt końcowy oraz połączenie z serwerem. */ /* przekazuje fd, jeśli wszystko w porządku; wartość ujemną, jeśli wystąpił błąd */ conn(const char *nąme) int

587

fd;

/* otwieramy zamontowany strumień */ if ( (fd = open(name, O_RDWR)) < 0) return(-1); if (isastream(fd) == 0) return (-2);

#include tinclude #include iinclude

id ient del(int fd) int

łtinclude #include int char int char Client int int

main(int argc, char *argv[]) c;

log_open("open.serv", LOG_PID, LOGJJSER); opterr = 0; /* funkcja getoptO nie ma wypisywać komunikatów na stderr */ while ( (c = getopt(argc, argv, "d")) != EOF) { switch (c) { case 'd': /* z diagnostyką */ debug = 1; break; case '?': err_quit("unrecognized option: -%c", optopt);

if (debug == 0) daemon_init( loop ();

i;

for (i = 0 ; i < client_size; i if (client[i].fd == fd) { client[i].fd = -1; return; log_quit("can't find client entry for fd %d", fd) ; Próg. 15.27 Funkcje do obsługi tablicy c l i e n t

"opend.h"

/* definicja zmiennych globalnych */ debug; errmsg[MAXLINE]; oflag; *pathname; *client = NULL; client_size;

int

if (client == NULL) /* gdy jest wywoływana pierwszy raz */ client_alloc(); ain: for (i = 0 ; i < client_size; i++) { if (client[i].fd == -1) { /* znajdujemy dostępną pozycję */ client[i].fd = fd; client[i].uid = uid; return(i); /* przekazujemy pozycję w tablicy client [] */ } } /* brak miejsca w tablicy klienta, pora by zaalokować kolejne pozycje */ client_alloc(); goto again; /* i przeszukujemy ponownie tablicę (teraz się uda) */

595

Jeżeli pojawi się błąd, to powyższe funkcje wywołują funkcje serii log_ (dodatek B), ponieważ zakładamy, że serwer jest demonem. Funkcja main (próg. 15.28) definiuje zmienne globalne, przetwarza opcje z wiersza poleceń i wywołuje funkcję loop. Jeżeli wywołamy nasz serwer z opcją -d, to nie będzie on demonem, lecz programem interakcyjnym. Taka możliwość jest stosowana przy testowaniu serwera.

"opend.h"

efine NALLOC 10

int

15.6. Serwer otwierający pliki, wersja 2

/* nigdy nie powraca */

Próg. 15.28 Funkcja main



\

Funkcja loop jest nieskończoną pętlą, wykonywaną przez serwer. Pokażemy jej dwie wersje. Program 15.29 zawiera pierwszą, używającą funkcji s e l e c t (rozwiązanie to działa w systemach 4.3+BSD oraz SVR4), potem omówimy wersję korzystającą z funkcji p o l l (tylko dla systemu SVR4).

15. Zaawansowane metody komunikacji międzyprocesowej

15.6. Serwer otwierający pliki, wersja 2

Funkcja loop wywołuje s e r v _ l i s t e n , by utworzyć po stronie serwera element końcowy przyjmujący polecenia klienckie. Reszta kodu tej funkcji jest pętlą rozpoczynającą się wywołaniem s e l e c t . Po powrocie z tego wywołania może być spełniony jeden z poniższych warunków.

"opend.h"

nclude nclude id op(void)

int i, n, maxfd, maxi, listenfd, clifd, nread; char buf[MAXLINE]; uid_t uid; fd set rset, allset; FD_ZERO(sallset); /* deskryptor fd do nasłuchu na żądania klienta if ( (listenfd = serv_listen(CS_OPEN) ) < 0) log_sys("serv_listen error"); FD_SET(listenfd, sallset); maxfd = listenfd; maxi = -1;

1. Może być gotowy do odczytu deskryptor l i s t e n f d , co oznacza, że nowy klient wywołał funkcję cli_conn. Aby to obsłużyć, wywołujemy funkcję serv_accept, a następnie modyfikujemy tablicę c l i e n t oraz wszystkie informacje rejestrujące nowego klienta. (Zawsze kontrolujemy, jaki jest największy używany numer deskryptora, by poprawnie podać pierwszy argument funkcji s e l e c t . Sprawdzamy również, jaki jest największy używany indeks w tablicy c l i e n t ) . 2. Może być gotowe do odczytu istniejące połączenie klienta. Oznacza to jedną z dwóch sytuacji: (a) klient zakończył pracę lub (b) klient przesłał nowe żądanie. O zakończeniu pracy klienta świadczy powrót z funkcji read z wartością 0 (koniec pliku). Jeżeli funkcja read przekazuje wartość dodatnią, to mamy nowe żądanie do przetworzenia. Wywołujemy wówczas funkcję r e ą u e s t , by obsłużyć żądanie klienta.

*/

for ( ; ; ) { rset = allset;

/* zmienna rset jest modyfikowana przy każdym okrążeniu */ if ( (n = select(maxfd + 1, &rset, NOLL, NULL, NULL)) < 0) log_sys("select error"); if (FD__ISSET (listenfd, srset)) { /* akceptujemy nowe żądanie klienta */ if ( (clifd = serv_accept(listenfd, suid)) < 0) log sys("serv accept error: %d", clifd); i = client_add(clifd, uid); FD_SET(clifd, sallset); if (clifd > maxfd) maxfd = clifd; /* maksymalna wartość fd dla selectO */ if (i > maxi) maxi = i; /* maksymalny wskaźnik pozycji w tablicy client[] */ log_msg("new connection: uid %d, fd %d", uid, clifd); continue; for (i (i = 0; i maxi) maxi = i; log_msg("new connection: uid %d, fd %d", uid, clifd) for (i = 1; i cnt_delok++; } else { rc = -1; /* nie znaleziono rekordu */ db->cnt_delerr++; } if (un_lock(db->idxfd, db->chainoff, SEEK_SET, 1) < 0) err_dump("un_lock error"); return(rc);

Próg. 16.13 Funkcja db_delete nclude

"db.h"

Funkcja usuwa bieżący rekord wskazany w strukturze DB. Jest wywoływana z funkcji db_delete() oraz db_store() po znalezieniu rekordu przez funkcję _db_find(). */ t b_dodelete(DB *db) int char off_t

i; *ptr; freeptr, saveptr;

/* wypełniamy bufor danych znakami odstępu */ for (ptr = db->datbuf, i = 0; i < db->datlen - 1; i++) *ptr++ = ' '; *ptr = 0; /* kończymy pustym znakiem, bo jest to potrzebne funkcji _db_writedat() */ /* czyścimy klucz */ ptr = db->idxbuf; while (*ptr) *ptr++ = ' '; /* musimy zaryglować listę wolnych rekordów */ if (writew_lock(db->idxfd, FREEJ3FF, SEEK_SET, 1) < 0) err_dump("writew_lock error"); /* zapisujemy rekord danych wypełniony znakami odstępów */ db writedat(db, db->datbuf, db->datoff, SEEK_SET);

625

16.7. Kod źródłowy

/* Czytamy wskaźnik listy wolnych rekordów. Ta wartość staje się polem wskaźnika łańcucha usuniętego rekordu indeksu. Oznacza to, że usunięty rekord jest umieszczany na początku listy wolnych rekordów. */ freeptr = _db_readptr(db, FREE_OFF); /* zapamiętujemy zawartość wskaźnika łańcucha rekordu indeksu zanim zostanie zmieniona przez funkcję _db_writeidx() */ saveptr = db->ptrval; /* Zmieniamy rekord indeksu. W ten sposób są również modyfikowane: rozmiar rekordu indeksu, pozycja danych oraz rozmiar danych, chociaż żadna z tych wielkości nie była zmieniana - jest to poprawne. */ _db_writeidx(db, db->idxbuf, db->idxoff, SEEK_SET, freeptr); /* zapisujemy nowy wskaźnik listy wolnych rekordów */ ^db^writeptr(db, FREE_OFF, db->idxoff); /* Zmieniamy wskaźnik łańcucha, który wskazywał na ten usuwany rekord. Przypominamy, że w funkcji _db_find() pole db->ptroff uzyskuje wartość wskazującą na ten wskaźnik łańcucha. Nadajemy temu wskaźnikowi łańcucha wartość wskaźnika łańcucha usuwanego rekordu, tj. saveptr - ta wartość jest albo równa 0, albo niezerowa. */ _db_writeptr(db, db->ptroff, saveptr); if (un_lock(db->idxfd, FREE_OFF, SEEK_SET, 1) < 0) err_dump("un_lock error"); return(0);

Próg. 16.14 Funkcja db_dodelete

Funkcja _db_dodelete rygluje do zapisu listę wolnych rekordów. W ten sposób nie dopuszczamy do żadnych interakcji między dwoma procesami usuwającymi w tym samym czasie rekordy dla dwóch różnych łańcu- j chów haszowania. Dodajemy usunięty rekord do listy wolnych rekordów, co powoduje zmianę wskaźnika listy wolnych rekordów, dlatego równocześnie tylko jeden proces może realizować takie działania. Funkcja _db_dodelete wywołuje funkcję _db_writedat (próg. 16.15), .< aby zapisać pusty rekord danych. Zauważmy, że funkcja d b w r i t e d a t nie " rygluje pliku danych. Funkcja d b d e l e t e otrzymała rygiel zapisu dla łańcucha = haszowania tego rekordu, wiemy więc, że żaden inny proces nie odczytuje rów- j nocześnie tego konkretnego rekordu danych ani nie zapisuje do niego. Gdy będziemy omawiać dalej w tym podrozdziale funkcję db_store, zobaczymy sy- ; tuację, w której funkcja _db_writedat, dopisując do pliku danych, musi go zaryglować.

16. Biblioteka funkcji obsługi bazy danych ludę

"db.h"

unkcja usuwa wskazany rekord. */ elete(DB *db, const char *key) int

rc;

if (_db_find(db, key, 1) == 0) { rc = _db_dodelete(db); /* znaleziono rekord */ db->cnt_delok++; } else { rc = -1; /* nie znaleziono rekordu */ db->cnt_delerr++; if (un_lock(db->idxfd, db->chainoff, SEEK__SET, 1) < 0) err_dump("un_lock error"); return(rc);

Próg. 16.13 Funkcja db_delete "db.h" Funkcja usuwa bieżący rekord wskazany w strukturze DB. Jest wywoływana z funkcji db_delete() oraz db_store() po znalezieniu rekordu przez funkcję _db_find(). */ _dodelete(DB *db) int char off_t

i; *ptr; freeptr, saveptr;

/* wypełniamy bufor danych znakami odstępu */ for (ptr = db->datbuf, i = 0; i < db->datlen - 1; i++) *ptr++ = ' '; *ptr = 0; /* kończymy pustym znakiem, bo jest to potrzebne funkcji _db_writedat() */ /* czyścimy klucz */ ptr = db->idxbuf; while (*ptr) *ptr++ = ' '; /* musimy zaryglować listę wolnych rekordów */ if (writew_lock(db->idxfd, FREE_OFF, SEEK_SET, 1) < 0) err_dump ("writew__lock error") ; /* zapisujemy rekord danych wypełniony znakami odstępów */ db writedat(db, db->datbuf, db->datoff, SEEK SET);

625

16.7. Kod źródłowy

/* Czytamy wskaźnik listy wolnych rekordów. Ta wartość staje się polem wskaźnika łańcucha usuniętego rekordu indeksu. Oznacza to, że usunięty rekord jest umieszczany na początku listy wolnych rekordów. */ freeptr = _db_readptr(db, FREE_OFF); /* zapamiętujemy zawartość wskaźnika łańcucha rekordu indeksu zanim zostanie zmieniona przez funkcję _db_writeidx() */ saveptr = db->ptrval; /* Zmieniamy rekord indeksu. W ten sposób są również modyfikowane: rozmiar rekordu indeksu, pozycja danych oraz rozmiar danych, chociaż żadna z tych wielkości nie była zmieniana - jesz to poprawne. */ __db_writeidx (db, db->idxbuf, db->idxoff, SEEK_SET, freeptr); /* zapisujemy nowy wskaźnik listy wolnych rekordów */ __db_writeptr (db, FREE_OFF, db->idxoff); /* Zmieniamy wskaźnik łańcucha, który wskazywał na ten usuwany rekord. Przypominamy, że w funkcji _db_find() pole db->ptroff uzyskuje wartość wskazującą na ten wskaźnik łańcucha. Nadajemy temu wskaźnikowi łańcucha wartość wskaźnika łańcucha usuwanego rekordu, tj. saveptr - ta wartość jest albo równa 0, albo niezerowa. */ _db_writeptr(db, db->ptroff, saveptr); if (un_lock(db->idxfd, FREE_OFF, SEEK_SET, err_dump("un_lock error");

0)

return(0);

Próg. 16.14 Funkcja db dodelete

Funkcja _db_dodelete rygluje do zapisu listę wolnych rekordów. W ten sposób nie dopuszczamy do żadnych interakcji między dwoma procesami usuwającymi w tym samym czasie rekordy dla dwóch różnych łańcuchów haszowania. Dodajemy usunięty rekord do listy wolnych rekordów, co powoduje zmianę wskaźnika listy wolnych rekordów, dlatego równocześnie tylko jeden proces może realizować takie działania. Funkcja _db_dodelete wywołuje funkcję _db_writedat (próg. 16.15), aby zapisać pusty rekord danych. Zauważmy, że funkcja d b w r i t e d a t nie rygluje pliku danych. Funkcja d b d e l e t e otrzymała rygiel zapisu dla łańcucha haszowania tego rekordu, wiemy więc, że żaden inny proces nie odczytuje równocześnie tego konkretnego rekordu danych ani nie zapisuje do niego. Gdy będziemy omawiać dalej w tym podrozdziale funkcję db s t o r ę , zobaczymy sytuację, w której funkcja _db_writedat, dopisując do pliku danych, musi go zaryglować.

16

16. Biblioteka funkcji obsługi bazy danych

Lnclude Lnclude

"db.h" datfd, 0, SEEK_SET, 0) < 0) err_dump("writew_lock e r r o r " ) ; if (

(db->datoff = lseek(db->datfd, offset, whence)) == -1) err_dump("lseek e r r o r " ) ; db->datlen = strlen(data) + 1; /* datlen zawiera znak nowego wiersza */ iov[0].iov_base = (char *) data; iov[0].iov_len = db->datlen - 1; iov[l].iov_base = Snewline; iov[l].iov_len = 1; if (writev(db->datfd, &iov[0], 2) != db->datlen) err_dump("writev error of data record"); if (whence == SEEK_END) if (un_lock(db->datfd, 0, SEEK_SET, 0) < 0) err_dump("un lock e r r o r " ) ;

Próg. 16.15 Funkcja _db_writedat

Funkcja _db_writedat wywohije writev, aby zapisać rekord danych i znak nowego wiersza. Nie mamy gwarancji, że w buforze wywołującego jest na końcu miejsce potrzebne do wpisania znaku nowego wiersza. Przypominamy podrozdz. 12.7, w którym stwierdziliśmy, że pojedyncze wywołanie w r i t e v jest szybsze niż dwa wywołania w r i t e . Po zmianie wskaźnika łańcucha funkcja d b d o d e l e t e modyfikuje zawartość rekordu indeksu, by wskazywał pierwszy rekord listy wolnych rekordów (jeżeli lista wolnych rekordów była pusta, to nowy wskaźnik łańcucha jest równy 0), następnie zmienia wskaźnik listy wolnych rekordów, by pokazywał zapisany przed chwilą rekord indeksu (usunięty rekord). Oznacza to, że lista wolnych rekordów jest obsługiwana według zasady pierwszy na wejściu, pierwszy na wyjściu - usunięte rekordy są dodawane na początku listy wolnych rekordów.

627

16.7. Kod źródłowy

#include #include /* * * *

"db.h"

/* struct iovec */

Funkcja zapisuje rekord indeksu. Przed nią jest wywoływaną funkcja _db_writedat(), by nadać wartości polom datoff i datlen w strukturze DB, gdyż są one potrzebne do zapisania rekordu indeksu. */

void _db_writeidx(DB *db, const char *key, off_t offset, int whence, off_t ptrval) struct iovec char int

iov[2]; asciiptrlen[PTR_SZ + IDXLEN_SZ + 1 ] ; len;

if ( (db->ptrval = ptrval) < 0 i I ptrval > PTR_MAX) err_quit("invalid ptr: %d", ptrval); sprintf(db->idxbuf, "%s%c%d%c%d\n", key, SEP, db->datoff, SEP, db->datlen); if ( (len = strlen(db->idxbuf)) < IDXLEN_MIN || len > IDXLEN_MAX) err_dump("invalid length"); sprintf(asciiptrlen, "%*d%*d", PTR_SZ, ptrval, IDXLEN_SZ, len);

1 I

/* Jeśli dołączamy rekord, to musimy zaryglować plik zanim wykonamy , lseek() oraz write(), by dokonywały się atomowo. Jeśli zapisujemy i w miejscu istniejącego rekordu, rygiel nie jest potrzebny. */ J

i if (whence == SEEK_END) /* dołączamy rekord */ if (writew_lock(db->idxfd, ((db->nhash+l)*PTR_SZ)+1, SEEK_SET, 0) < 0) err_dump("writew_lock error");

: !

/* ustalamy pozycję w pliku indeksowym i rejestrujemy ją */ if ( (db->idxoff = lseek(db->idxfd, offset, whence)) == -1) err_dump("lseek error");

] •
idxbuf; iov[l].iov_len = len; if (writev{db->idxfd, &iov[0], 2) != PTR_SZ + IDXLEN_SZ + len) err_dump("writev error of index record"); if (whence == SEEK_END) if (un_lock(db->idxfd, ((db->nhash+l)*PTR_SZ)+1, SEEK_SET, 0) < 0) err dump ("un__lock error");

Próg. 16.16 Funkcja _db w r i t e i d x

Ostatnią funkcją, wywoływaną z funkcji _db_dodelete, jest d b _ w r i t e p t r (próg. 16.17). Jest ona wywoływana dwukrotnie: raz, by zmienić wskaźnik listy wolnych rekordów, i drugi raz, by zmienić wskaźnik do łańcucha haszowania (prowadzący do usuniętego rekordu). nclude

"db.h"

Funkcja wypełnia w pliku indeksowym pola wskaźnika łańcucha: w liście wolnych rekordów, tablicy z haszowaniem lub rekordzie indeksu. */ lb_writeptr(DB *db, off_t offset, off_t ptrval) char asciiptr[PTR_SZ + 1]; if (ptrval < 0 || ptrval > PTR_MAX) err_quit("invalid ptr: %d", ptrval); sprintf(asciiptr, "%*d", PTR_SZ, ptrval); if (lseek(db->idxfd, offset, SEEK_SET) == -1) err_dump("lseek error to ptr f i e l d " ) ; if (write(db->idxfd, a s c i i p t r , PTR_SZ) != PTR_SZ) err_dump("write error of p t r f i e l d " ) ;

Próg. 16.17 Funkcja _db_writeptr

W programie 16.18 pokazujemy najbardziej rozbudowaną funkcję obsługi bazy danych, d b s t o r e . Rozpoczyna się ona od wywołania funkcji _db_find, która sprawdza, czy rekord już istnieje. Jeśli istnieje, to używamy stałej DB_REPLACE, w przeciwnym razie stałej DB_INSERT. Gdy zastępujemy istniejący rekord, wówczas jest wymagane, by klucze były identyczne, ale rekordy danych będą prawdopodobnie inne. Ostatni argument funkcji _db_f ind określa, że łańcuch haszowania musi zostać zaryglowany do zapisu, ponieważ zapewne będziemy go modyfikować.

16.7. Kod źródłowy #include

629

"db.h"

/* Funkcja zapamiętuje rekord w bazie danych. * Przekazuje 0, jeśli wszystko w porządku; 1, jeśli rekord istnieje * i podano DB_INSERT; -1, jeśli rekord nie istnieje i podano DB_REPLACE. */ int db_store(DB *db, const char *key, const char *data, int flag) int off_t

rc, keylen, datlen; ptrval;

keylen = strlen(key); datlen = strlen(data) + 1;

/* +1, by zmieścił się na końcu znak nowego wiersza */ if (datlen < DATLEN_MIN I I datlen > DATLEN_MAX) err_dump("invalid data length"); /* db_find() wylicza, do której tablicy z haszowaniem trafia nowy rekord w (db->chainoff) niezależnie od tego, czy rekord ten istnieje, czy nie. Poniższe wywołanie _db_writeptr() zmienia wpis tablicy z haszowaniem dla tego łańcucha, by wskazywał nowy rekord. Oznacza to, że nowy rekord jest dodawany na początku łańcucha haszowania. */

if (_db_find(db, key, 1) < 0) { /* nie znalezionio rekordu */ if (flag & DB_REPLACE) { rc = -1; db->cnt_storerr++; goto doreturn; /* błąd, rekord nie istnieje */ /* Funkcja db_find() zaryglowała już łańcuch haszowania; czytamy wskaźnik łańcucha do pierwszego rekordu indeksu tego łańcucha haszowania. */ ptrval = _db_readptr(db, db->chainoff);

if (_db_findfree(db, keylen, datlen) < 0) { /* Nie znaleziono pustego rekordu o dobrym rozmiarze. Musimy,] dołączyć nowy rekord na końcu plików indeksów i danych. * .i _db_writedat(db, data, 0, SEEK_END); _db_writeidx(db, key, 0, SEEK_END, ptrval); , j /* Pole db->idxoff zostało ustalone przez _db_writeidx(). Nowyrekord jest umieszczany na początku łańcucha haszowania. */^ _db_writeptr(db, db->chainoff, db->idxoff); , db->cnt_storl++; i } else { | /* Możemy użyć ponownie tego samego rekordu. • Funkcja _db_findfree() usuwa go z listy wolnych 4 i ustala wartości pól db->datoff i db->idxoff. */ '•• _db_writedat(db, data, db->datoff, SEEK_SET); db_writeidx(db, key, db->idxoff, SEEK_SET, ptrval); j

iO

16. Biblioteka funkcji obsługi bazy danych /* ponownie użyty rekord jest umieszczany na początku łańcucha haszowania */ _db_writeptr(db, db->chainoff, db->idxoff); db->cnt stor2++; } else { if (flag & DB_INSERT) { rc = 1; db->cnt_storerr++; goto doreturn;

631

16.7. Kod źródłowy #include

"db.h"

/* Funkcja próbuje znaleźć wolny rekord indeksu oraz odpowiadający mu * rekord danych o właściwym rozmiarze. Jest wywoływana tylko * z funkcji db_store(). */

/* znaleziono rekord */

int

/* błąd, rekord już istnieje w bazie */

_db_findfree(DB *db, int keylen, int datlen) { int rc; off_t offset, nextoffset, saveoffset;

/* Zastępujemy istniejący rekord. Wiemy, że nowy klucz jest taki sam jak istniejący, ale musimy sprawdzić, czy rekordy danych mają takie same rozmiary. */ if (datlen != db->datlen) { _db_dodelete(db); /* usuwamy istniejący rekord */ /* czytamy ponownie wskaźnik łańcucha w tablicy z haszowaniem (mógł się zmienić w trakcie usuwania) */ ptrval = _db_readptr(db, db->chainoff); /* dołączamy na końcu plików nowe rekordy indeksu i danych */ _db_writedat(db, data, 0, SEEK_END); _db_writeidx(db, key, 0, SEEK_END, ptrval); /* nowy rekord na początku łańcucha haszowania */ _db_writeptr(db, db->chainoff, db->idxoff); db->cnt_stor3++; } else { /* ten sam rozmiar danych, tylko wpisujemy nowe dane */ _db_writedat(db, data, db->datoff, SEEK_SET); db->cnt_stor4++; rc = 0; /* w porządku */ return: /* odryglowujemy łańcuch haszowania zaryglowany przez _db_find() */ if (un_lock(db->idxfd, db->chainoff, SEEK_SET, 1) < 0) err_dump("un_lock error"); return(rc);

Próg. 16.18 Funkcja d b _ s t o r e

Jeżeli dodajemy nowy rekord do bazy danych, to wywołujemy funkcję _db_findfree (próg. 16.19), aby wyszukać na liście wolnych rekordów usunięty rekord o tym samym rozmiarze klucza i tym samym rozmiarze danych. W pętli while funkcji _db_findfree przeglądamy listę wolnych rekordów, poszukując rekordu o takich samych rozmiarach klucza i danych. W tej prostej implementacji używamy ponownie wcześniej usuniętych rekor-

/* ryglujemy listę wolnych rekordów */ if (writew_lock(db->idxfd, FREE_OFF, SEEK_SET, 1) < 0) err_dump("writew_lock error") ; /* czytamy wskaźnik listy wolnych rekordów */ saveoffset = FREE_OFF; offset = _db_readptr(db, saveoffset); while (offset != 0) { nextoffset = _db_readidx(db, offset); if (strlen(db->idxbuf) == keylen && db->datlen == datlen) break; /* znalezione dopasowanie */ saveoffset = offset; offset = nextoffset; if (offset == 0) rc = -1; /* brak dopasowania */ else { /* Znaleźliśmy wolny rekord o odpowiednim rozmiarze. Rekord indeksu został już odczytany powyżej przez funkcję _ db_readidx(),która ustaliła wartość db->ptrval. Zmienna saveoffset wskazuje na wskaźnik łańcucha, który kieruje nas do tego pustego rekordu w liście wolnych rekordów. Nadajemy temu wskaźnikowi łańcucha wartość db->ptrval i dzięki temu usuwamy ten pusty rekord z listy wolnych rekordów. */ _db_writeptr(db, saveoffset, db->ptrval); rc = 0;

'< |

/* Zauważmy, że _db_readidx() ustala wartości pól db->idxoff . ! oraz db->datoff. Korzysta z tego wywołujący funkcję db_store () , -, by zapisać nowy rekord indeksu i rekord danych. */ \ /* odryglowujemy listę wolnych */ if (un_lock(db->idxfd, FREEJDFF, SEEK_SET, 1) < 0) err_dump("un_lock error"); return(rc);

Próg. 16.19

Funkcja_db_findfree

16. Biblioteka funkcji obsługi bazy danych dów, ale tylko wtedy, kiedy występują takie same rozmiary klucza i danych w nowym rekordzie i w usuniętym. Jest wiele lepszych metod ponownego używania usuniętych rekordów, ale wymagają one bardziej skomplikowanego kodu. Funkcja _db_f indf r e e musi dysponować ryglem zapisu dla listy wolnych rekordów, aby nie doszło do interakcji z innymi procesami, które właśnie korzystają z tej listy. Po usunięciu rekordu z listy wolnych rekordów można zwolnić rygiel zapisu. Przypominamy, że funkcja _db_dodelete modyfikowała również listę wolnych rekordów. Powracamy do funkcji db_store. Po wywołaniu funkcji _db_find widzimy, że kod obsługuje cztery przypadki. 1. Dodanie nowego rekordu, ale funkcja _db_findfree nie znalazła pustego rekordu o odpowiednich rozmiarach. Oznacza to, że musimy dołączyć nowy rekord do końca pliku indeksowego i pliku danych. Wywołanie funkcji _ d b _ w r i t e p t r dodaje nowy rekord na początku łańcucha haszowania. 2. Dodanie nowego rekordu i funkcja _db_findfree znalazła pusty rekord o odpowiednich rozmiarach. Wywołanie funkcji _db_findfree usuwa pusty rekord z listy wolnych rekordów i zamienia rekord danych oraz rekord indeksu. Wywołanie funkcji d b _ w r i t e p t r dodaje nowy rekord na początku łańcucha haszowania. 3. Zastąpienie istniejącego rekordu, ale rozmiar nowego rekordu danych różni się od rozmiaru istniejącego rekordu. Wywołujemy funkcję _db_dodelete, aby usunąć istniejący rekord, a następnie dopisujemy nowy rekord na końcu pliku indeksowego i pliku danych. (Istnieją inne sposoby obsługi tego przypadku. Możemy próbować znaleźć usunięty rekord o odpowiednim rozmiarze danych). Wywołanie funkcji _ d b _ w r i t e p t r dodaje nowy rekord na początku łańcucha haszowania. 4. Zastąpienie istniejącego rekordu; rozmiar nowego rekordu danych jest taki sam jak rozmiar istniejącego rekordu. Jest to najprostszy przypadek - musimy tylko zmienić zawartość rekordu danych. Opiszemy teraz, jak przebiega ryglowanie, gdy dołączamy na końcu pliku nowe rekordy indeksu lub rekordy danych. (Przypominamy problemy omówione przy okazji analizy próg. 12.6, związane z ryglowaniem, gdy punktem odniesienia jest koniec pliku). W przypadkach 1 i 3 funkcja d b _ s t o r e wywołuje funkcje _db_writeidx i _db_writedat z trzecim argumentem równym 0 oraz czwartym argumentem równym stałej SEEK_END. Ten czwarty argument jest sygnalizatorem oznaczającym w tych funkcjach dołączenie do końca pliku. Funkcja _db_writeidx rygluje do zapisu obszar pliku indeksowego od miejsca bezpośrednio za łańcuchem haszowania do końca pliku. Taka technika nie ma żadnego wpływu na inne działania związane z odczytem lub zapisem bazy danych (ponieważ korzystają tylko z rygla łańcucha haszowania) i zapobiega próbom dołączenia danych w tym samym czasie

632

16.7. Kod źródłowy

przez inne wywołania funkcji d b _ s t o r e . Funkcja _db_writedat rygluje dc zapisu cały plik danych. Dzięki temu jej działanie nie ma żadnego wpływu m inne operacje odczytu i zapisu bazy danych (ponieważ nie będą one nawę próbować uzyskać rygla pliku danych) i zapobiega próbom dołączenia danyd w tym samym czasie przez inne wywołania funkcji d b _ s t o r e . (Zobac; ćw. 16.3). Kończymy analizę kodu źródłowego opisem dwóch funkcji db nextrec oraz db_rewind, których używamy do odczytu wszystkich rekordów w bazis danych. Na ogół korzystamy z nich w pętli o postaci db_rewind(db); while ( (ptr = db_nextrec(db, key)) != NULL) { /* przetwarzanie rekordu */ }

Wcześniej ostrzegaliśmy już, że nie można przewidzieć kolejności przekazy wanych rekordów, gdyż nie są one uporządkowane według kluczy. Funkcja db_rewind (próg. 16.20) ustala bieżącą pozycję w pliku indek sowym odpowiadającą pierwszemu rekordowi indeksu (bezpośrednio za ta blicąz haszowaniem). tinclude

"db.h"

/* Funkcja przewija plik indeksu; jest to potrzebne w funkcji db_nextrec() * Jest wywoływana automatycznie z funkcji db open(). * Musi zostać wywołana przed pierwszym wywołaniem db_nextrec(). */ void db rewind(DB *db) off_t

offset;

offset = (db->nhash + 1)

PTR_SZ;

/* +1, by zmieścił się wskaźnik * listy wolnych rekordów */

/* Tylko ustawiamy bieżący wskaźnik pozycji w pliku w tym procesie na początek rekordów indeksowych, nie ma więc potrzeby ryglowania Poniżej dodajemy 1, aby na końcu tablicy z haszowaniem zmieścił ', się znak nowego wiersza. */ '< if ( (db->idxoff = lseek(db->idxfd, offset+1, SEEK_SET)) == -1) err_dump("lseek error");

Próg. 16.20 Funkcja db_rewind

. ' j

i

Następnie funkcja db_nextrec tylko czyta sekwencyjnie wszystkie re kordy indeksu. Jak widzimy w próg. 16.21, funkcja db_nextrec nie korzys^ z łańcuchów haszowania. Ponieważ czyta wszystkie usunięte rekordy, rów nież rekordy w łańcuchu haszowania, musi więc sprawdzać, czy dany rekorc był usunięty (świadczy o tym pusty klucz) i ignorować takie rekordy.

16. Biblioteka funkcji obsługi bazy danych "db.h"

iclude

Funkcja przekazuje kolejny rekord. Przechodzi kolejno przez plik indeksowy, ignorując usunięte rekordy. Przed jej pierwszym wywołaniem trzeba wywołać funkcję db rewind().

nextrec(DB *db, char *key) char c, *ptr; /* ryglujemy do odczytu listę wolnych rekordów, dzięki czemu nie odczytamy rekordu, który jest w trakcie usuwania */ if (readw_lock(db->idxfd, FREE_OFF, SEEK_SET, 1) < 0) err_dump("readw_lock error"); do { /* czytamy następny rekord indeksu */ if (_db_readidx(db, 0) < 0) { ptr = NULL; /* koniec pliku indeksu, EOF */ goto doreturn; } /* sprawdzamy, czy klucz składa się z samych znaków odstępu (pusty rekord) */ ptr = db->idxbuf; while ( (c = *ptr++) != 0 && c == ' ') /* omijamy, dopóki bajt nie jest pusty i jest odstępem */ } while (c == 0); /* pętla, dopóki nie znajdziemy klucza złożonego ze znaków różnych niż odstęp */ if (key != NULL) strcpy(key, db->idxbuf); /* przekazujemy klucz */ p t r = _db_readdat(db); /* przekazujemy wskaźnik do bufora danych */ db->cnt_nextrec++; return: if (un_lock(db->idxfd, FREE_OFF, SEEK_SET, 1) < 0) err_dump("un_lock e r r o r " ) ; return(ptr); Próg. 16.21 Funkcja db_nextrec

Jeżeli w trakcie kolejnych wywołań funkcji db_nextrec baza danych zostanie zmodyfikowana, to otrzymane rekordy należy traktować jako obraz zmieniającej się zawartości bazy danych w określonym punkcie czasu. Funkcja db_nextrec zawsze przekazuje „poprawny" rekord odzwierciedlający stan w czasie jej wywołania; oznacza to, że nigdy nie przekaże rekordu usuniętego wcześniej. Jest natomiast możliwe, że bezpośrednio po powrocie

16.8. Wydajność

635

z wywołania db_nextrec właśnie przekazany rekord zostanie usunięty. Podobnie, zaraz po tym jak funkcja db_nextrec ominie usunięty rekord, rekord ten może zostać ponownie użyty, czego nie zobaczymy, dopóki nie powrócimy do początku bazy danych. Jeżeli jest dla nas ważne, by obraz zawartości bazy danych uzyskany za pomocą sekwencji wywołań db_nextrec dokładnie odzwierciedlał stan zasobów, musimy zagwarantować, że w czasie realizacji pętli nie będzie żadnych operacji wstawienia lub usunięcia. Przyjrzyjmy się sposobowi ryglowania stosowanemu w funkcji d b _ n e x t r e c . Nie przeglądamy żadnego łańcucha haszowania, ani nie jesteśmy w stanie stwierdzić, do jakiego łańcucha haszowania należy dany rekord. Dlatego jest możliwe, że gdy funkcja d b _ n e x t r e c czyta dany rekord, to jest on właśnie przetwarzany w celu usunięcia. Aby temu zapobiec, funkcja d b _ n e x t r e c rygluje do odczytu listę wolnych rekordów i w ten sposób unika interakcji z funkcjami _db_dodelete oraz db f i n d f r e e .

16.8 Wydajność Napisaliśmy specjalny program do przetestowania biblioteki obsługującej bazę danych i do pomiaru pewnych zależności czasowych. Program pobiera z wiersza poleceń dwa argumenty: liczbę tworzonych procesów potomnych oraz liczbę rekordów bazy danych (nrec) zapisywanych przez każdego potomka. Tworzy pustą bazę danych (wywołując funkcję db_open), generuje procesy potomne (fork) i oczekuje na zakończenie pracy przez wszystkich potomków. Każdy potomek wykonuje następujące kroki:

• Zapisuje nrec rekordów do bazy danych. • Odczytuje nrec rekordów z bazy danych na podstawie wartości klucza. • Realizuje poniższą pętlę nrec x 5 razy. - Odczytuje losowy rekord. - Raz na 37 okrążeń pętli usuwa wybrany losowo rekord. - Raz na 11 okrążeń pętli dodaje nowy rekord i odczytuje go. - Raz na 17 okrążeń pętli zastępuje wybrany losowo rekord nowym ' rekordem. Każdy nowy rekord ma albo taki sam rozmiar danych, ; albo jego rozmiar danych jest większy. , ! • Usuwa wszystkie rekordy zapisane przez siebie. Przy każdym usunieciu rekordu przegląda 10 wybranych losowo rekordów. \

Rzeczywista liczba operacji zrealizowanych na bazie danych jest przechowy- . wana w polach cnt_xxx struktury DB. Wartości te są odpowiednio zwięk-j szane w funkcjach. Liczba operacji w procesach potomnych zmienia się, gdyż^ generator liczb losowych stosowany do wybierania rekordów jest inicjowany ' w każdym procesie potomnym zgodnie z identyfikatorem procesu potomnego, j W tabeli 16.1 pokazujemy typową liczbę operacji zrealizowanych w każdym procesie potomnym, gdy parametr n r e c przyjmuje wartość 500. \

16. Biblioteka funkcji obsługi bazy danych

16.8. Wydajność

Tabela 16.1 Typowa liczba operacji wykonywanych przez każdy proces potomny, gdy nrec wynosi 500 Operacja

JTabela 16.2 Jeden proces potomny, zmienna liczba nrec, różne techniki ryglowania

675

db s t o r ę , DB INSERT, powtórne użycie pustego rekordu

170

db s t o r ę , DB_REPLACE, różna długość danych, dołączenie

100

db s t o r ę , DB REPLACE, ta sama długość danych

100

db f e t ch, znaleziony rekord

Ryglowanie zalecane

Liczba

db s t o r ę , DB INSERT, brak pustego rekordu, dołączenie

db s t o r ę , nie znaleziony rekord

637

20 8300

db f e t c h , nie znaleziony rekord

750

db d e l e t e , znaleziony rekord

840

db d e l e t e , nie znaleziony rekord

100

Operacji pobrania danych z bazy było około 10 razy więcej niż operacji zapamiętania lub usunięcia. Taki wynik jest zapewne typowy dla wielu aplikacji baz danych. Każdy potomek realizuje te operacje (pobranie, zapamiętanie i usunięcie) tylko dla rekordów, które sam zapisał. W przykładzie uwzględniono wszystkie aspekty kontroli współbieżności, ponieważ procesy potomne wykonywały działania na tej samej bazie danych (wprawdzie na różnych rekordach tej samej bazy danych). Całkowita liczba rekordów w bazie wzrosła proporcjonalnie do liczby procesów potomnych. (Jeden potomek zapisał do bazy danych nrec rekordów. Dwa procesy potomne zapisały 2 x nrec itd.). Aby sprawdzić osiągany poziom współbieżności przy ryglowaniu zgrubnym w porównaniu z ryglowaniem precyzyjnym i by porównać trzy różne typy ryglowania (brak ryglowania, ryglowanie zalecane i ryglowanie obowiązkowe), uruchamiamy trzy wersje programu. Pierwsza wersja stosuje kod źródłowy pokazany w podrozdz. 16.7; tę technikę nazwaliśmy ryglowaniem precyzyjnym. W drugiej wersji zmieniamy funkcje ryglowania, by implementować ryglowanie zgrubne zgodnie z opisem w podrozdz. 16.6. W trzeciej wersji usuwamy wszystkie funkcje ryglowania, dzięki czemu możemy zmierzyć, jaki koszt wnosi ryglowanie. Wersję pierwszą i drugą (ryglowanie precyzyjne i zgrubne) możemy uruchomić, używając albo ryglowania zalecanego, albo obowiązkowego; w tym celu zmieniamy bity praw dostępu dla plików bazy danych. (We wszystkich wynikach testów pokazywanych w tym podrozdziale mierzyliśmy czasy dla ryglowania obowiązkowego, używając tylko implementacji ryglowania precyzyjnego).

jfniki dla pojedynczego procesu W tabeli 16.2 widzimy wyniki, gdy pracuje tylko jeden proces potomny, a parametr nrec przyjmuje wartości 500, 1000 i 2000.

Ryglowanie obowiązkowe

Bez ryglowania zgrubne nrec

Użytkownik

System

500

15

68

1000

60

2000

157

Użytkownik

System

84

16

78

340

402

63

906

1068

158

Zegar

precyzyjne

precyzyjne

Użytkownik

System

94

15

79

94

16

92

109

360

425

63

366

430

71

412

488

936

1096

158

934

1097

159

1081

1253

Zegar

Zegar

Użytkownik

System

Zegar

Ostatnie 12 kolumn zawiera czasy podawane w sekundach. We wszystkich przypadkach suma czasu użytkownika procesora oraz czasu systemowego procesora jest mniej więcej równa czasowi zegarowemu. Ten zbiór testów był ograniczony czasem procesora, a nie szybkością dysku. Wartości w środkowych sześciu kolumnach (ryglowanie zalecane, zgrubne i precyzyjne) są w każdym wierszu zbliżone. Wynik jest logiczny - gdy mamy jeden proces, to nie ma różnicy między ryglowaniem zgrubnym a precyzyjnym. Porównując wyniki przy braku ryglowania z ryglowaniem zalecanym, widzimy, że dołożenie funkcji ryglujących powoduje wzrost systemowego czasu procesora od 3 do 15%. Nawet gdy rygle nie są nigdy używane (ponieważ pracuje tylko jeden proces), wywołania funkcji f c n t l wiążą się z dodatkowym zużyciem czasu. Zwróćmy uwagę, że dla wszystkich czterech wersji ryglowania czasy użytkownika procesora są prawie takie same. To również jest logiczne, ponieważ kod użytkownika jest zawsze niemal taki sam (oprócz liczby wywołań funkcji f c n t l ) . Ostami wniosek wynikający z tabeli 16.2 dotyczy ryglowania obowiązkowego, w którym dochodzi około 15% systemowego czasu procesora w porównaniu z ryglowaniem zalecanym. Ponieważ liczba wywołań funkcji ryglujących jest taka sama dla zalecanego ryglowania precyzyjnego i obowiązkowego ryglowania precyzyjnego, więc dodatkowe wywołania systemowe wiążą się z operacjami read i w r i t e . W ostatnim teście wypróbowaliśmy program bez ryglowania dla wielu pro- , cesów potomnych. Otrzymaliśmy, zgodnie z oczekiwaniami, różne losowe błędy,,j Zazwyczaj rekordy, które zostały dodane do bazy danych, nie mogły być potem ; odnalezione i program kończył się awaryjnie. Przy każdym uruchomieniu pro- , gramu testowego błędy były inne. Jest to klasyczna sytuacja wyścigu - wiele procesów aktualizuje ten sam plik bez używania jakiejś formy ryglowania. .i Wyniki dla wielu procesów

i

Kolejny zestaw pomiarów dotyczy głównie różnic między ryglowaniem zgrubnym a precyzyjnym. Jak wcześniej powiedzieliśmy, intuicyjnie oczeku--^ jemy, że ryglowanie precyzyjne zwiększy poziom współbieżności, ponieważ ' poszczególne fragmenty bazy danych są przez krótszy czas zaryglowane i niedostępne z innych procesów. W tabeli 16.3 pokazujemy uzyskane rezul- '

16. Biblioteka funkcji obsługi bazy danych Tabela 16.3 Porównanie różnych technik ryglowania, nrec = 500 Ryglowanie obowiązkowe

Ryglowanie zalecane zgrubne Użytczba cesów kownik

System

A

precyzyjne Zegar

Użytkownik

System

Zegar

A

precyzyjne

Zegar

Użytkownik

System

Zegar

Procent

1

16

79

96

16

83

99

3

16

96

112

16

2

42

230

273

43

237

281

8

43

271

315

14 18

3

79

454

536

81

464

547

11

78

545

626

4

128

753

884

132

757

892

8

123

888

1015

17

5

185

1123

1315

196

1173

1376

61

189

1366

1560

16

6

262

1601

1870

270

1611

1888

18

264

1931

2205

20

7

351

2164

2526

354

2174

2537

11

341

2527

2877

16

8

451

2801

3264

454

2766

3230

-34

438

3298

3750

19

9

565

3513

4092

569

3483

4067

-25

548

4148

4712

19

10

684

4293

5000

688

4215

4925

-75

658

5048

5732

20

11

812

5151

5987

811

5043

5876

-111

797

6198

7020

23

12

958

6057

7058

960

5992

6980

-78

937

7298

8265

22

taty dla parametru nrec równego 500 oraz liczby potomków zmieniającej się od wartości 1 do 12. Wszystkie czasy użytkownika, systemowe oraz zegarowe, podajemy w sekundach. Są to czasy całkowite dla procesu macierzystego i wszystkich jego potomków. Na podstawie tych danych możemy rozważyć wiele zagadnień. Ósma kolumna, zatytułowana „A zegar", jest podaną w sekundach różnicą między czasami zegarowymi dla zalecanego ryglowania zgrubnego i zalecanego ryglowania precyzyjnego. Miara ta pokazuje, ile zyskujemy, przechodząc od ryglowania zgrubnego do precyzyjnego. W systemach używanych do tych testów ryglowanie zgrubne jest szybsze, dopóki nie mamy więcej niż siedem procesów. Nawet gdy liczba procesów jest większa niż siedem, zmniejszenie czasu zegarowego przy użyciu ryglowania precyzyjnego nie jest duże (około 1%). Wynik ten może nieco dziwić, warto zastanowić się, czy w ogóle opłaci się dla takiego efektu dodawać kod implementujący ryglowanie precyzyjne. Twierdziliśmy, że czas zegarowy zmaleje przy zastosowaniu ryglowania precyzyjnego zamiast zgrubnego i tak faktycznie jest, ale równocześnie oczekujemy, że czas systemowy pozostanie wyższy przy ryglowaniu precyzyjnym dla dowolnej liczby procesów. Powodem takiego założenia jest większa liczba wywołań funkcji f c n t l w przypadku ryglowania precyzyjnego niż ryglowania zgrubnego. Jeżeli podsumujemy liczbę wywołań funkcji f c n t l z tab. 16.1 dla ryglowania zgrubnego oraz precyzyjnego, to otrzymujemy średnio 22 110 wywołań dla ryglowania zgrubnego i 25 680 dla ryglowania precyzyjnego. (Aby uzyskać te liczby, musimy wziąć pod uwagę, że każda operacja pokazana w tab. 16.1 potrzebuje dwóch wywołań f c n t l dla ryglowania zgrubnego, a każde z pierwszych trzech wywołań d b s t o r e , związane z usunię-

639

16.8. Wydajność

ciem rekordu po jego znalezieniu, potrzebuje czterech wywołań funkcji f c n t l dla ryglowania precyzyjnego). Spodziewamy się, że wzrost liczby wywołań funkcji f c n t l o 16% pociągnie za sobą zwiększenie czasu systemowego dla ryglowania precyzyjnego. Wynik budzi duże zdziwienie, gdyż dla ryglowania precyzyjnego uzyskaliśmy niewielki spadek czasu systemowego, dopiero gdy mamy ponad siedem procesów. Ostatnia kolumna, „A procent" pokazuje procentowy przyrost systemowego czasu procesora dla zalecanego ryglowania precyzyjnego i obowiązkowego ryglowania precyzyjnego. Te wartości procentowe potwierdzają rezultaty z tab. 16.2, pokazujące, że ryglowanie obowiązkowe zwiększa czas systemowy o około 15-20%. Ponieważ kod użytkowy we wszystkich testach jest prawie taki sam (dla ryglowania precyzyjnego, zarówno zalecanego, jaki i obowiązkowego dochodzą dodatkowe wywołania funkcji f c n t l ) , więc spodziewamy się, że czasy procesora w trybie użytkownika nie będą się zmieniać w danym wierszu. Jednak przy przejściu od zalecanego ryglowania zgrubnego do zalecanego ryglowania precyzyjnego czas użytkownika procesora zawsze wzrasta o 1-3%. Nie ma dobrego wytłumaczenia tych różnic. Wartości w pierwszym wierszu tab. 16.3 są podobne do wyników dla parametru nrec równego 500 w tab. 16.2. Spodziewaliśmy się tego. Na rysunku 16.4 widzimy graficzną reprezentacją danych z tab. 16.3 dla zalecanego ryglowania precyzyjnego. Pokazujemy czas zegarowy, gdy liczba

4000-

- 400

350030002500-

- 300

czas pracy procesora w trybie systemowym na liczbę procesów.

2000-

- 200

1500 — 1000-

- 100

5000-

czas pracy procesora w trybie użytkownika na liczbę procesów

1 4

1

5

T

6

T

8

liczba procesów

Rys. 16.4 Wartości z tab. 16.3 dotyczące precyzyjnego ryglowania zalecanego

16. Biblioteka funkcji obsługi bazy danych procesów wzrasta od 1 do 9. (Nie pokazujemy wartości dla 10, 11 i 12 procesów, by uniknąć rozciągania wykresu w pionie). Przedstawiamy również czas użytkownika procesora podzielony przez liczbę procesów oraz czas systemowy procesora podzielony przez liczbę procesów. Zwróćmy uwagę, że oba wykresy czasów procesora podzielonych przez liczbę procesów są liniowe, ale wykres czasu zegarowego jest nieliniowy. Jeśli podsumujemy czasy procesora w trybach użytkownika i systemowym ztab. 16.3 i porównamy wynik z czasem zegarowym w danym wierszu, to okaże się, że różnica między tymi wartościami wzrasta, gdy zwiększa się liczba procesów. Jest to prawdopodobnie spowodowane dodatkowym czasem procesora, zużywanym przez system operacyjny, gdy rośnie liczba procesów. Te nadmiarowe działania systemu operacyjnego będą odwzorowane w większym czasie zegarowym, ale nie powinny mieć wpływu na czasy procesora indywidualnych procesów. Powodem wzrostu czasu procesora w trybie użytkownika przy zwiększaniu liczby procesów jest rosnąca liczba rekordów w bazie danych. Wzrasta rozmiar każdego z łańcuchów haszowania, a więc funkcja _db_f i n d działa dłużej, by znaleźć dany rekord.

6.9 Podsumowanie W tym rozdziale przeanalizowaliśmy projekt i implementację biblioteki do obsługi bazy danych. Wprawdzie dla potrzeb prezentacji staraliśmy się, by biblioteka nie była zbyt duża, ale mimo to stosujemy w niej ryglowanie rekordów wymagane do umożliwienia współbieżnego dostępu przez wiele procesów. Oceniliśmy wydajność tej biblioteki dla różnej liczby procesów i różnych typów ryglowania: braku ryglowania, zalecanego ryglowania (precyzyjnego oraz zgrubnego) i ryglowania obowiązkowego. Przekonaliśmy się, że ryglowanie zalecane zwiększa o około 10% czas zegarowy w stosunku do braku ryglowania, a ryglowanie obowiązkowe dodaje kolejne 10% w stosunku do ryglowania zalecanego.

Ćwiczenia 16.1

Ryglowanie w funkcji _db_dodelete jest dosyć ostrożne. Moglibyśmy zwiększyć współbieżność, rezygnując z ryglowania do zapisu listy wolnych rekordów do czasu, gdy rygiel ten będzie nam faktycznie potrzebny. Oznacza to, że wywołanie writew_iock można by przenieść między wywołania _db_writedat oraz __db_readptr. Co się stanie, jeśli tak zrobimy?

16.2

Załóżmy, że funkcja _db_nextrec nie zarygluje do odczytu listy wolnych rekordów, a rekord, który czytaliśmy, był właśnie w trakcie usuwania. Opisz,

16.9. Podsumowanie

641

w jaki sposób funkcja _db_nextrec może przekazać poprawny klucz i pusty rekord danych (a więc wynik niepoprawny). (Wskazówka: przeanalizuj funkcję _db_dodelete).

16.3

Po dyskusji na temat funkcji d b s t o r e opisaliśmy ryglowanie realizowane przez funkcje _db_writeidx i _db_writedat. Powiedzieliśmy, że ryglowanie to nie ma żadnego wpływu na inne procesy czytające i zapisujące, oprócz tych, które właśnie wywołują funkcję d b s t o r e . Czy jest to prawdą, jeżeli używamy obowiązkowego ryglowania?

16.4 Jak dodałbyś wywołanie f sync do biblioteki obsługi bazy danych? 16.5

Utwórz nową bazę danych i zapisz do niej pewną liczbę rekordów. Napisz program, który wywołuje funkcję db_nextrec, aby przeczytać poszczególne rekordy bazy danych i wywołaj funkcję _db_hash, by obliczyć wartość funkcji haszującej dla każdego rekordu. Wydrukuj zestawienie obrazujące liczbę rekordów w każdym łańcuchu haszowania. Czy funkcja haszująca z próg. 16.9 jest właściwa?

16.6

Zmodyfikuj funkcje obsługi bazy danych, aby móc podawać w czasie tworzenia bazy danych liczbę łańcuchów haszowania w pliku indeksowym.

16.7

Jeżeli Twój system implementuje sieciowy system plików, np. „Network File System" (NFS) firmy Sun lub „Remote File Sharing" (RFS) firmy AT&T, to porównaj wydajność funkcji obsługi bazy danych, gdy baza danych jest zlokalizowana: (a) na tej samej stacji co program testujący i (b) na innej stacji. Czy ryglowanie rekordów stosowane przez bibliotekę obsługi bazy danych nadal działa poprawnie?

17

Komunikacja z drukarką postscriptową

17.2. Komunikacja w trybie PostScript

64:

resowanych tym zagadnieniem odsyłamy do dokumentacji Adobe Systems [1 i [2]. Nas interesuje wyłączne komunikacja z drukarką postscriptową).

/Times-Roman findfont 15 scalefont rozmiar czcionki w punktach 15 setfont ustalenie obowiązującej czcionki 300 350 moveto x=300, y=350 (lokalizacja na stronie) (hello, world) show wydruk napisu na bieżącej stronie showpage % i wyprowadzenie strony na urządzenie wyjściowe

Jeżeli w tym programie zmienimy słowo s e t f o n t na s s e t f o n t i prześlem> taką wersję do drukarki, to nie otrzymamy żadnego wydruku. Zamiast tegc odbierzemy z drukarki poniższy komunikat %%[ Error: undefined; OffendingCommand: ssetfont ] % % %%[ Flushing: rest of job (to end-of-file) will be ignored ] % %

[7.1. Wprowadzenie Przygotujemy teraz program, który będzie komunikował się z drukarką postscriptową. Obecnie drukarki postscriptowe są bardzo popularne i normalnie mają połączenie z konkretną stacją przez łącze szeregowe RS-232. Potem będziemy mogli zastosować zaimplementowane funkcje do obsługi terminalowego wejścia-wyjścia, które omówiliśmy w rozdziale 11. Komunikacja z drukarką postscriptową odbywa się w trybie w pełni dupleksowym, co oznacza, że w czasie, gdy wysyłamy dane do drukarki, musimy być przygotowani do odczytu informacji o stanie, którą może przekazać drukarka. Dlatego będziemy również mieli okazję, by zastosować funkcje zwielokrotniania wejścia-wyjścia z podrozdz. 12.5: s e l e c t oraz p o l l . Program, który omawiamy, jest oparty na oprogramowaniu l p r p s napisanym przez Jamesa Clarka. Ten program i inne programy tworzące pakiet l p r p s zostały udostępnione w grupie Usenet o nazwie comp. sources .misc, wtórnie 21 (lipiec 1991).

7.2

Komunikacja w trybie PostScript Pierwszą ważną rzeczą, o której musimy wiedzieć, chcąc drukować na drukarce postscriptowej, jest to, że do drukarki jest wysyłany nie drukowany plik, lecz program postscriptowy, który drukarka ma wykonać. Drukarka jest typowo wyposażona w interpreter PostScriptu, który wykonuje program i generuje jedną lub więcej stron wydruku. Jeśli program postscriptowy ma błędy, to drukarka (a właściwie interpreter PostScriptu) przekazuje komunikat awaryjny i może w tej sytuacji nie utworzyć wydruku. Poniższy program w PostScripcie drukuje popularny napis „ h e l l o , world". (Nie opisujemy w tej książce programowania w PostScripcie, zainte-

Właśnie komunikaty awaryjne, które mogą nadejść z drukarki w dowolnym czasie, są elementem komplikującym obsługę drukarki postscriptowej. Nie możemy po prostu wysłać całego programu w PostScripcie do drukarki i zapomnieć o tym - musimy inteligentnie obsłużyć wszystkie możliwe komunikaty o błędach. (W tym rozdziale będziemy zazwyczaj mówić „drukarka", nawet gdy technicznie chodzi o interpreter PostScriptu). Drukarki postscriptowe są na ogół dołączone do komputera za pomocą łącza szeregowego RS-232. Z punktu widzenia stacji jest to zwykłe połączenie terminalowe. Wszystko, co powiedzieliśmy w rozdziale 11 o terminalowym wejściu-wyjściu, ma tutaj zastosowanie. (Istnieją inne sposoby dołączenia drukarek postscriptowych do stacji, coraz bardziej popularne stają się interfejsy sieciowe. Jednak metodą dominującą obecnie jest interfejs szeregowy). Na rysunku 17.1 pokazujemy typowe zależności. Program postscriptowy może generować dwie postaci wydruków: operator showpage tworzy wydruk na kolejnej stronie, a operator p r i n t wyprowadza dane na standardowe wyjście drukarki (w tym przypadku połączenie szeregowe do stacji). Interpreter PostScriptu wysyła i odbiera znaki w 7-bitowym kodziwj ASCII. Program w PostScripcie składa się wyłącznie ze znaków ASCII, które! są znakami rozpoznawanymi na wydruku. Niektóre ze znaków ASCII nie ma-j ją postaci nadającej się do wydruku, a ich znaczenie jest specjalne; wymię-' niamyje w tab. 17.1. ^ Znak końca pliku (EOF) w PostScripcie (Control-D) jest stosowany do synchronizowania pracy drukarki i stacji. Wysyłamy do drukarki progranii w PostScripcie i zaraz potem znak EOF. Drukarka po zakończeniu realizacji programu w PostScripcie wysyła z powrotem znak EOF. * W czasie, gdy interpreter wykonuje program postscriptowy, możemy: przesłać znak przerwania (Control-C). Typowo powoduje to zakończenie programu realizowanego przez drukarkę.

17. Komunikacja z drukarką postscriptową

17.2. Komunikacja w trybie PostScript %%[ Error: undefined; OffendingCoiratiand: ssetfont ] % %

proces użytkowy

%%[ Flushing: rest of job (to end-of-file) will be ignored ] % %

Komunikat o stanie ma postać %%[ s t a t u s : i d l e ]%%

funkcje r e a d oraz w r i t e

Oprócz stanu i d l e (nie jest przetwarzane żadne zadanie) możemy otrzymać inne stany: busy (w trakcie realizacji programu postscriptowego), p r i n t i n c (w trakcie wydruku), i n i t i a l i z i n g (w trakcie inicjowania pracy) i p r i n t i n g t e s t page (w trakcie wydruku strony testowej). Przeanalizujemy te raz komunikaty, które są generowane przez interpreter PostScriptu. Widzieli śmy już komunikat o postaci

jądro systemu

dyscyplina linii terminalu

%%[ E r r o r : error; OffendingCommand: operator ] %%

program obsługi terminalu

stan drukarki, wynik polecenia print

polecenie drukowania, program postscriptowy Rys. 17.1

interpreter postscriptu

Możliwe jest pojawienie się około 25 różnych typów błędów {error). Popu larne błędy to: d i c t s t a c k u n d e r f l o w , i n v a l i d a c c e s s , typechec> i undefined. Pole operator zawiera operator PostScriptu, który wygenerował ten błąd. O błędzie drukarki świadczy komunikat o postaci

showpage mechanizm drukarki

drukarka postscriptowa

%%[

Komunikacja z drukarką postscriptowa przez połączenie szeregowe

Tabela 17.1

ontrol-C

003

reason

]%%

gdzie pole reason to najczęściej Out of Paper, Cover Open lub Miscell a n e o u s Error. Po wystąpieniu błędu interpreter PostScriptu często przekazuje kolejn) komunikat

Specjalne znaki wysyłane z komputera do drukarki postscriptowej

Wartość Opis ósemkowa

nak

PrinterError:

%%[ Flushing: rest of job (to end-of-file) will be ignored ] % %

Przerwanie. Sprawia, że jest realizowany operator postscriptowy i n t e r r u p t . Typowo przerywa to interpretację aktualnie wykonywanego programu postscriptowego.

umieszczane między sekwencjami znaków %%[ oraz ]%%. Program postscriptowy może również wygenerować wydruk za pomocą operatora p r i n t

Aby obsłużyć takie komunikaty, musimy analizować ich napisy, które s£

lontrol-D

004

Koniec pliku.

^ysuw 'iersza

012

Koniec wiersza, postscriptowy znak nowego wiersza. Jeśli kolejno odebrano

Takie dane wyjściowe powinny zostać przekazane użytkownikowi, który wy-

znak powrotu i znak nowego wiersza, to do interpretera jest przekazywany

słał program do drukarki - program drukujący nie musi j u ż interpretować tegc

owrót

pojedynczy znak nowego wiersza. 015

wydruku.

Koniec wiersza. Przekształcony na postscriptowy znak nowego wiersza.

!ontrol-Q

021

Rozpoczęcie wyprowadzania danych (sterowanie przepływem XON).

lontrol-S

023

Zatrzymanie wyprowadzania danych (sterowanie przepływem XOFF).

:ontrol-T

024

Zapytanie o stan. Interpreter Postscriptu odpowiada jedno wierszowym komunikatem o stanie.

W odpowiedzi na zapytanie o stan (Control-T) drukarka odsyła komunikat o stanie. Wszystkie komunikaty odbierane z drukarki mają poniższy format: %%[ k e y : v a l ] % %

W komunikacie może się pojawić dowolna liczba par key. val; są one oddzielane znakiem średnika. Przypominamy komunikat, który został przekazany w poprzednim przykładzie:

W tabeli 17.2 wymieniamy specjalne znaki, które są wysyłane przez in terpreter PostScriptu do stacji komputerowej. Tabela 17.2 Znak

''

Specjalne znaki wysyłane z komputera do drukarki postscriptowej

Wartość Opis ósemkowa

Control-D

004

Koniec pliku.

Wysuw wiersza

012

Koniec wiersza. Gdy znak nowego wiersza zostanie zapisany na standardowe; wyjście interpretera, to jest on przekształcony do postaci znaku powrotu oraz znaku nowego wiersza. "3

Control-Q

021

Rozpoczęcie wyprowadzania danych (sterowanie przepływem XON).

Control-S

023

Zatrzymanie wyprowadzania danych (sterowanie przepływem XOFF).

17. Komunikacja z drukarką postscriptową

7.3 Buforowanie wydruków Program, który przygotowujemy w tym rozdziale, przesyła do drukarki postscriptowej program w Postscripcie albo w trybie autonomicznym (stand-alone), albo przez berkelejowski system buforowania wydruków. Typowo stosuje się system buforowania, ale warto również poznać tryb autonomiczny (diagnostyczny), przydatny przy testowaniu. Również system SVR4 ma swój system buforowania wydruków, ale jest on bardziej skomplikowany niż berkelejowski. O szczegółach można dowiedzieć się z podręcznika systemowego, począwszy od strony na temat polecenia l p w części 1 dokumentacji AT&T [14]. W rozdziale 13 książki Stevensa [44] są opisane szczegółowo systemy buforowania w systemach BSD oraz Systemie V poprzedzającym SVR4. Nas interesuje w tym rozdziale komunikacja z drukarką postscriptową, a nie detale dotyczące systemów buforowania. W berkelejowskim systemie buforowania drukujemy plik za pomocą polecenia o postaci lpr -pps main.c

Polecenie to przesyła na drukarkę o nazwie ps plik main.c. Jeśli nie byłoby napisu -pps, to wydruk zostałby wysłany do drukarki wskazanej przez zmienną środowiskową PRINTER lub do drukarki domyślnej l p . Drukarka jest poszukiwana w pliku / e t c / p r i n t c a p . Na rysunku 17.2 pokazujemy wpis dotyczący naszej drukarki postscriptowej. lplps:\ :br#19200:lp=/dev/ttyb:\ :sf:sh:rw:\ :fc#0000374:fs#0000003:xc#0:xs#0040040:\ :af=/var/adm/psacct:lf=/var/adm/pslog:sd=/var/spool/pslpd:\ :if=/usr/local/lib/psif: Rys. 17.2 Wpis dotyczący drukarki postscriptowej w pliku p r i n t c a p

W pierwszym wierszu są podane dwie nazwy tego wpisu, które mogą być używane zamiennie: ps i lp. Wartość br określa współczynnik szybkości w bodach, równy tu 19200. lp podaje nazwę ścieżki specjalnego urządzenia związanego z tą drukarką; sf oznacza zniesienie znaku zakończenia strony. Napis sh usuwa wydruk nagłówka strony na początku każdego zadania, rw określa, że urządzenie jest otwierane do odczytu i zapisu, zgodnie z opisem w podrozdz. 17.2, drukarka postscriptową wymaga tego. Kolejne cztery pola podają bity, które mają zostać włączone lub wyłączone w starej berkelejowskiej strukturze s g t t y . (Opisujemy je tutaj, gdyż większość systemów BSD używających pliku p r i n t c a p o tej postaci wspiera ten starszy styl ustalania parametrów terminalu. W kodzie źródłowym omawianym w tym rozdziale pokażemy, jak ustawia się wszystkie parametry ter-

17.3. Buforowanie wydruków

64'

minalu za pomocą funkcji normy POSIX.l z rozdziału 11). Najpierw maski f c zeruje w elemencie sg_f l a g s następujące bity: EVENP i ODDP (wyłączę nie kontroli parzystości oraz generowania bitu parzystości), RAW (wyłączeni trybu surowego), CRMOD (wyłączenie odwzorowania CR/LF na wejściu i wyj ściu), ECHO (wyłączenie echa) i LCASE (wyłączenie odwzorowania małe li tery/wielkie litery na wejściu i wyjściu). Następnie maska f s włącza nastę pujące bity: CBREAK (wprowadzanie danych po jednym znaku) i TANDEM (sta cja generuje znaki Control-S i Control-Q w celu kontroli przepływu). Wartoś xc zeruje bity w słowie trybu lokalnego. W tym przykładzie wartość 0 nic ni oznacza. Na koniec, wartość xs ustawia bity w słowie trybu lokalnegc LDECCTQ (tylko znak Control-Q wznawia wyprowadzanie danych zatrzymań sekwencją Control-S) oraz LLITOUT (zniesienie translacji danych wyjście wych). Napisy w polach af oraz l f określają odpowiednio plik ewidencjonc wania wydruków oraz plik rejestru; sd definiuje nazwę katalogu przeznaczę nego do buforowania wydruków, a if nazwę filtra wejściowego. Filtr wejściowy jest wywoływany przez każdy plik, który ma być drukc wany. Wywołanie ma postać filter -n loginname -h hostname acctfde

Mogą się również w nim pojawić inne, opcjonalne argumenty, które nie maj znaczenia w przypadku drukarki postscriptowej. Plik do wydruku jest przek; zywany przez standardowe wejście, a jako standardowe wyjście jest otwierar urządzenie drukarki (na podstawie wpisu lp w pliku p r i n t c a p ) . Standa dowe wejście może być łączem komunikacyjnym. W przypadku drukarki postscriptowej filtr wejściowy powinien sprav dzać pierwsze dwa bajty pliku wejściowego, aby stwierdzić, czy plik jest tel stem ASCII, czy programem w Postscripcie. Typową konwencją jest umies: czanie dwubajtowej sekwencji % ! na początku pliku, by wskazać, że jest program w PostScripcie. Jeżeli plik jest programem postscriptowym, to pr< gram l p r p s (opisany dalej w tym rozdziale) może przesłać go bezpośredn do drukarki. Jeżeli plik jest tekstem ASCII, to jest potrzebny dodatkowy pq gram, który skonwertuje jego zawartość do postaci programu w PostScript drukującego ten tekst. > Filtr p s i f , który podajemy w pliku p r i n t c a p , jest dostarczany w pi kiecie l p r p s . Program t e x t p s z tego pakietu na podstawie pliku z teksty ASCII tworzy odpowiedni program postscriptowy drukujący ten plik. Na t sunku 17.3 prezentujemy rolę tych programów. I Nie pokazaliśmy na rys. 17.3 jednego z programów, psrev, który Ą wraca strony produkowane przez program w PostScripcie. Można z niego lv rzystać, gdy drukarka postscriptową generuje wydruki „do góry nogami". * Po opisaniu wszystkich założeń możemy przystąpić do szczegółów analizy projektu oraz kodu źródłowego programu l p r p s . ;

17. Komunikacja z drukarką postscriptową

64

17.4. Kod źródłowy ftinclude #include łłinclude #include #include #include



"ourhdr.h"

/* ponieważ nasz program jest demonem */

#define EXIT_SUCCESS 0 /* definiowany w systemie kolejkowania wydruków BSD #define EXIT_REPRINT 1 #define EXIT_THROW_AWAY 2 lprps "$@"

textps | lprps "$@"

Rys. 17.3 System lprps

Zacznijmy od przeglądu funkcji, które są wywoływane z funkcji main, i przyjrzyjmy się ich współpracy z drukarką. W tabeli 17.3 są zawarte informacje o tych zależnościach. Druga kolumna „Przerwanie" wskazuje, czy funkcja może zostać przerwana przez sygnał SIGINT. Trzecia kolumna określa czas oczekiwania (w sekundach), który jest ustawiany przez funkcję. Zwróćmy uwagę, że gdy przesyłamy do drukarki program w Postscripcie, to nie obowiązuje żaden licznik czasu oczekiwania. Jest tak, ponieważ wykonanie takiego programu może trwać dowolnie długo. „Nasz program postscriptowy" w opisie funkcji get_page jest odwołaniem do niewielkiego programu postscriptowego w próg. 17.9, który pobiera licznik bieżącej strony. Tabela 17.3 Funkcje wywoływane przez funkcję main Przerwanie?

Czas Wysyłane do drukarki oczekiwania?

get status

nie

5

get page

nie

30

send file

tak

żaden

get_page

nie

"/dev/ttyb"

#define DEF_BAUD

B19200

#define MAILCMD

7.4 Kod źródłowy

Funkcja

#define DEF_DEVICE

30

Odbierane z drukarki

Control-T

%%[

nasz program postscriptowy EOF

EOF

użytkowy program postscriptowy EOF nasz program postscriptowy EOF

status:

]%%

%%[ p a g e c o u n t : n] %%

EOF

%%[

pagecount:

extern extern extern extern extern

idle

«]%%

EOF

W programie 17.1 widzimy plik nagłówkowy l p r p s . h . Włączają go wszystkie pliki źródłowe. W tym pliku są dołączane wszystkie potrzebne w większości programów systemowe pliki nagłówkowe, definiowane pewne stałe i deklarowane zmienne globalne i prototypy funkcji globalnych.

/* modyfikujemy odpowiednio poniższe wartości */ "maił -s \"printer job\" %s@%s < %s"

tdefine OBSIZE 1024 #define IBSIZE 1024 #define MBSIZE 1024

extern extern extern extern extern extern

/* domyślna wartość dla trybu diagnostycznego */

/* bufor wyjściowy */ /* bufor wejściowy */ /* bufor komunikatu */

/* deklaracja zmiennych globalnych */ char *loginname; char *hostname; char *acct_file; char eofc; /* postscriptowy koniec pliku (004) */ int debug; /* prawda, jeśli praca interakcyjna (nie demon) int in_job; /* prawda, jeśli trwa przesyłanie zadania PS do drukarki */ int psfd; /* deskryptor pliku drukarki postscriptowej */ int start_page; /* nr strony początkowej */ int end_page; /* nr strony końcowej */ volatile sig_atomic_t intr_flag; /* ustawiona, gdy przechwycono SIGINT */ volatile sig_atomic_t alrm_flag; /* ustawiona, gdy pojawił się SIGALRM */

extern enum status { /* stan drukarki */ INVALID, UNKNOWN, IDLE, BUSY, WAITING } status;

void

/* prototypy funkcji globalnych */ do acct(void); /* acct.c */

void void void

clear_alrm(void); handle_alrm(void); set_alrm(unsigned int);

/* alarm.c */

void

get_status(void);

/* getstatus.c */

void

init_input(int);

/* input.c */

50 >id >id lid

17. Komunikacja z drukarką postscriptową

/* interrupt.c */

/* mail.c */

iid

close_mailfp(void); mail_char(int);

iid

mail_line(const char *, const char * ) ;

iid

/* message.c */

iid

msg_init(void); msg_char(int); proc_msg(void);

id

out_char(int);

/* output.c */

id

get_page(int

/* pagecount.c */

id

send_file(void);

/* sendfile.c */

id id id id id

block_write(const char *, int); tty_flush(void); set_block(void); set_nonblock(void); tty_open(void);

/* tty.c */

lid

*);

Plik v a r s . c (próg. 17.2) definiuje zmienne globalne. nclude ar ar ar ar

*loginname; *hostname; *acct file; eofc = '\004';

t t

psfd = STDOUT FILENO, start page = -1; end page = -1; debug; in job;

status

"lprps.h"

static void usage(void); int

int

c;

log_open("lprps", LOG_PID, LOG_LPR); opterr = 0; /* funkcja getopt() nie ma wypisywać komunikatów na stderr */ while ( (c = getopt(argc, argv, "cdh:i:1:n:x:y:w:")) != EOF) { switch (c) ( /* znaki sterujące do przekazania */ case 'c' /* rozmiar strony w poziomie */ case 'x' /* rozmiar strony w pionie/ case 'y' /* szerokość */ case 'w' /* długość */ case '1' /* wcięcie */ case 'i' break; /* reszta nie jest tu istotna */ case 'd': /* diagnostyka (praca interakcyjna) */ debug = 1; break; case 'n': /* nazwa zalogowanego użytkownika */ loginname = optarg; break;

"lprps.h"

Latile sig_atomic_t Latile sig_atomic_t

#include

main(int argc, char *argv[])

Próg. 17.1 Plik nagłówkowy l p r p s . h

t t

Realizacja programu zaczyna się od funkcji main pokazanej w próg. 17.3. Funkcja main wywołuje funkcję log_open (przedstawioną w dodatku B), ponieważ program na ogół pracuje jako demon. Nie możemy wypisywać informacji o błędach na standardowy strumień komunikatów awaryjnych, zamiast tego korzystamy z techniki syslog opisanej w p. 13.4.2.

proc_input_char(int) ; proc_some_input(void); proc_upto_eof(int); clear_intr(void); handle_intr(void); set_intr(void);

651

17.4. Kod źródłowy

/* Cc

intr_flag; alrm_flag;

status = INVALID; Próg. 17.2 Deklaracja zmiennych globalnych

case 'h': /* nazwa stacji użytkownika */ hostname = optarg; break; case ' ?': log_msg("unrecognized option: -%c", optopt); usage();

if (hostname == NULL I| loginname == NULL) usage(); /* wymagamy podania nazwy stacji i nazwy logowania */ if (optind < argc) acct_file = argv[optind];

/* ostatni argument jest nazwą pliku rejestru * /

17. Komunikacja z drukarką postscriptową if

(debug) tty_open();

17.4. Kod źródłowy #include

"lprps.h"

get_status();

/* * * *

get_page(&start_page);

void do_acct(void)

if (atexit(close_mailfp) < 0) /* rejestrujemy funkcję obsługi exit () */ log_sys("main: atexit error");

send_file();

/* wysyłamy do drukarki dane ze strumienia stdin */

653

Funkcja zapisuje do pliku rejestru liczbę stron, nazwę stacji i nazwę logowania. Jest wywoływana na końcu funkcji main() (jeśli wszystko było w porządku w funkcjach printer_flushing() i handle_intr(), gdy odebrano przerwanie). */

FILE

*fp;

if (end_page > start_page && acct_file != NULL SS (fp = fopen(acct_file, "a")) != NULL) { fprintf(fp, "%7.2f %s:%s\n", (double)(end_page - start_page) hostname, loginname); if (fclose(fp) == EOF) log_sys("do_acct: fclose error"); }

get_page(&end_page); do_acct(); exit(EXIT SUCCESS);

S

tic void ge(void) log_msg("lprps: invalid arguments" exit(EXIT_THROW_AWAY);

Próg. 17.3 Funkcja main

Następnie są przetwarzane argumenty wiersza poleceń, w przypadku drukarki postscriptowej większość z nich można zignorować. Używamy opcji -d, aby wskazać, że program ma pracować interakcyjnie, a nie jako demon. Jeśli ten sygnalizator jest ustawiony, to musimy zainicjować tryb terminalowy (tty_open). Później opiszemy funkcję close_mailfp, którą rejestrujemy jako procedurę obsługi zakończenia. Wywołujemy potem kolejno funkcje wymienione w tab. 17.3, aby: pobrać stan drukarki i upewnić się, że jest ona gotowa ( g e t _ s t a t u s ) , pobrać początkowy licznik stron (get_page), wysłać plik (program postscriptowy) do drukarki (send_file), pobrać końcowy licznik stron (get_page), zapisać rekord ewidencjonujący (do_acct) i zakończyć pracę. Plik a c c t . h definiuje funkcję do_acct (próg. 17.4). Jest ona wywoływana na końcu funkcji main, aby zapisać rekord ewidencjonujący wydruki. Nazwa pliku ewidencjonującego jest pobierana z odpowiedniego wpisu w pliku p r i n t c a p (rys. 17.2) i przekazywana jako ostatni argument wiersza poleceń. Tradycyjnie wszystkie filtry berkelejowskie zapisują wydrukowaną liczbę stron do pliku ewidencjonującego wydruki za pomocą formatu %7.2f w funkcji p r i n t f . Dzięki temu urządzenia rastrowe mogą raportować ilość wyprowadzanych danych w stopach (oraz ich ułamkach), a nie tylko w stronach.

Próg. 17.4 Funkcja do_acct

Kolejny plik, t t y . c (próg. 17.5), zawiera wszystkie potrzebne funkcje do obsługi terminalowego wejścia-wyjścia. Wywołują one funkcje opisane w rozdz. 3 ( f c n t l , w r i t e oraz open) oraz terminalowe funkcje normy POSDC.l przedstawione w rozdz. 11 ( t c f l u s h , t c g e t a t t r , t c s e t a t t r , c f s e t i s p e e d oraz cfsetospeed). Zdarza się, że zablokowanie się funkcji w r i t e nie jest istotne, w tych przypadkach będziemy wywoływać funkcję b l o c k w r i t e . Jeżeli zależy nam, by nie ulec zablokowaniu, to wywołujemy najpierw funkcję set_nonblock, a dopiero potem funkcję read lub w r i t e . Drukarka postscriptową jest urządzeniem w pełni dupleksowym, więc nie chcemy zablokować się w wywołaniu funkcji w r i t e , ponieważ drukarka może przesłać do nas dane (na przykład komunikat o błędzie). Jeśli drukarka wysyła komunikat awaryjny, gdy jesteśmy zablokowani, próbując przesłać dane, to może dojść do zakleszczenia. Jądro systemu typowo buforuje terminalowe wejście i wyjście, dlatego gdy pojawi się błąd, wywołujemy funkcję t t y f l u s h , aby opróżnić kolejki wejściową i wyjściową. Funkcja t t y o p e n jest wywoływana z funkcji main, gdy program pracuje interakcyjnie (a nie jako demon). Musimy wejść w niekanoniczny tryb terminalowy, określić szybkość w bodach oraz ustalić kilka innych sygnalizatorów terminalu. Warto pamiętać, że te ustawienia nie są takie same dla wszystkich drukarek postscriptowych. Należy najpierw sprawdzić je w opisie konkretnej drukarki. (Najczęściej drukarki różnią się ustawieniami liczby bitów danych, 7 bitów lub 8 bitów, liczby bitów startu i stopu oraz parzystości).

17. Komunikacja z drukarką postscriptową nclude nclude nclude

"lprps.h"

atic int

block_flag = 1;

void tty_open(void) { struct termios term; /* domyślnie blokowane wejście-wyjście */

id t błock(void) /* wyłączenie sygnalizatora braku blokowania */ /* funkcja wywoływana tylko z poniższej funkcji block_write() */ int val; if (block_flag == 0) { if { (val = fcntl(psfd, F_GETFL, 0)) < 0) log_sys("set_block: fcntl F_GETFL error"); val &= ~O_NONBLOCK; if (fcntl(psfd, F_SETFL, val) < 0) log_sys("set_block: fcntl F_SETFL error"); block_flag = 1;

id t nonblock(void) int

/* ustalenie braku blokowania dla deskryptora */

val;

if (block_flag) { if ( (val = fcntl(psfd, F_GETFL, 0)) < 0) log_sys("set_nonblock: fcntl F_GETFL error"); val 1= O_NONBLOCK; if (fcntl(psfd, F_SETFL, val) < 0) log_sys("set_nonblock: fcntl F_SETFL error"); block_flag = 0;

id ock_write(const char *buf, int n) set_block(); if (write(psfd, buf, n) != n) log_sys("block_write: write error" jid jy_flush(void)

655

17.4. Kod źródłowy

/* opróżnienie wejścia terminalu i kolejek wyjściowych */

if (tcflush(psfd, TCIOFLUSH) < 0) log_sys("tty_flush: tcflush error");

if ( (psfd = open(DEF_DEVICE, O_RDWR)) < 0) log_sys("tty_open: open error"); if (tcgetattr(psfd, Sterm) < 0) /* pobieramy atrybuty log_sys("tty_open: tcgetattr error") ; / * dane 8-bitowe */ term.c_cflag = CS8 | J CREAD | /* włączamy odbiór /* ignorujemy l i n i e stanu modemu CLOCAL;

/* bez parzystości, 1 b i t stopu •>

term.c_oflag &= -OPOST; / * wyłączamy przetwarzanie końcowe */ term.c_iflag = IXON I IXOFF | /* sterowanie przepływem Xon/Xoff */ /* ignorujemy przerwania */ IGNBRK I /* obcinamy dane wejściowe do 7 bitów */ ISTRIP I /* ignorujemy odebrany znak CR */ IGNCR; term.c_lflag

= 0;

/* wyłączamy wszystko w lokalnym sygnalizatorze: zabroniony tryb kanoniczny, zabronione generowanie sygnału, wyłączone echo */ /* po jednym bajcie, bez licznika czasu */

term.c_cc[VMIN] = 1; term.c_cc[VTIME] = 0; cfsetispeed(sterm, DEF_BAUD) , cfsetospeed(&term, DEF_BAUD) if ( t c s e t a t t r ( p s f d , TCSANOW, sterm) < 0) log_sys("tty_open: t c s e t a t t r error")

/ * ustalamy atrybuty */

Próg. 17.5 Funkcje terminalowe

Program obsługuje dwa sygnały: S I G I N T oraz SIGALRM. Każdy filtr wywoływany przez berkelejowski system buforowania wydruków musi obsługiwać sygnał S I G I N T . Sygnał ten jest przesyłany do filtru, jeżeli polecenie lprm(l) usunie zadanie wydruku. Sygnału SIGALRM używamy do ustalania liczników czasów oczekiwania. Oba sygnały obsługujemy podobnie:, nasza funkcja set_xxx służy do ustanowienia procedury obsługi sygnału, a funkcja clear_xxx wyłącza tę procedurę. Gdy system dostarcza sygnał do procesu, to procedura obsługi sygnału tylko ustawia globalny sygnalizator, i n t r _ f l a g lub alrm_flag, i powraca. W innej części programu sprawdzamy w odpowiednim czasie te sygnalizatory, aby dowiedzieć się, czy sygnał został przechwycony. Jednym z możliwych przypadków jest powrót z funkcji wejścia-wyjścia z błędem EINTR. Program wywołuje wówczas funkcje h a n d l e _ i n t r lub handle_alrm, aby obsłużyć to zdarzenie. Wywołujemy funkcję s i g n a l _ i n t r (próg. 10.13), by każdy sygnał przerywał wolne wywołanie systemowe. Program 17.6 zawiera plik i n t e r r u p t . c obsługujący sygnał SIGINT.

17. Komunikacja z drukarką postscriptową nclude

"lprps.h"

atic void g int(int signo)

/* procedura obsługi SIGINT */

intr_flag = 1; return; Funkcja jest wywoływana po dostarczeniu sygnału SIGINT, który został rozpoznany w pętli głównej. (Nie jest procedurą obsługi sygnału - jest nią powyższa funkcją set_intr()). */

indle_intr(void) char

c;

Gdy wystąpi przerwanie, wówczas musimy wysłać do drukarki postscriptowy znak przerwania (Control-C), a potem znak końca pliku (EOF). Interpreter Postscriptu po odbiorze takiej sekwencji znaków na ogół przerywa program, który właśnie przetwarza. Następnie oczekujemy, by drukarka odesłała znak EOF. (Funkcję p r o c u p t o e o f opiszemy później). Kończąc pracę odczytujemy końcowy stan licznika stron i zapisujemy rekord ewidencjonujący wydruki. W tabeli 17.3 zaznaczyliśmy, które z funkcji limitują czasy oczekiwania. Ustalamy czas oczekiwania, tylko gdy zlecamy pobranie stanu drukarki ( g e t _ s t a t u s ) , odczytujemy licznik stron ( g e t p a g e ) lub gdy przerywamy pracę drukarki ( h a n d l e i n t r ) . Jeśli następuje przekroczenie czasu oczekiwania, to rejestrujemy błąd, odczekujemy chwilę i kończymy pracę. Program 17.7 zawiera plik alarm. c. #include

intr_flag = 0; clear_intr(); /* wyłączamy sygnał */ set_alrm(30);

/* czekamy 30 sekund na przerwanie pracy drukarki */

tty flush(); /* kasujemy zakolejkowane dane wyjściowe */ c =~~'\003'; block_write(&c, 1); /* Control-C przerywa zadanie PS */ block_write(&eofc, 1); /* po nim piszemy EOF */ proc_upto_eof(1); /* czytamy i ignorujemy wszystko do znaku EOF */

get_page(&end_page); do_acct(); exit(EXIT SOCCESS);

static void sig_alrm(int signo)

/* procedura obsługi sygnału SIGALRM */

alrm_flag = 1; return; void handle_alrm(void)

exit(EXIT_REPRINT); /* udało się, bo użytkownik wykonał lprm dla tego zadania */

/* aktywacja procedury obsługi sygnału */

if (signal_intr(SIGINT, sig_int) == SIG_ERR) log_sys("set_intr: signal_intr error");

oid lear_intr(void)

"lprps.h"

log_ret("printer not responding"); sleep{60); /* co najmniej tak długo potrwa nagrzanie drukarki */

clear alrm();

oid et_intr(void)

657

17.4. Kod źródłowy

/* ignorowanie sygnału */

if (signal(SIGINT, SIG_IGN) == SIG_ERR) log_sys("clear_intr: signal e r r o r " ) ;

Próg. 17.6 Plik i n t e r r u p t . c do obsługi sygnałów przerwania

void /* rejestracja procedury obsługi sygnału oraz ustalenie alarmu */ set_alrm(unsigned int nsec) alrm_flag = 0; if (signal_intr(SIGALRM, sig_alrm) == SIG_ERR) log_sys("set_alrm: signal_intr error"); alarm(nsec); void clear_alrm(void) { alarm(0); if (signal(SIGALRM, SIG_IGN) == SIG_ERR) log_sys("clear_alrm: signal error"); alrm flag = 0;

Próg. 17.7 Plik a l a r m , c do obsługi liczników czasu

58

17. Komunikacja z drukarką postscriptową

include * * * *

Jeżeli otrzymamy komunikat

"lprps.h"

Funkcja jest wywoływana z funkcji main() przed wydrukowaniem zadania. Wysyła do drukarki znak Control-T, aby pobrać jej stan. Jeśli licznik czasu oczekiwania przekroczy termin, zanim będzie odebrany stan, to coś jest nie w porządku. */

oid et status(void) char

65S

17.4. Kod źródłowy

c;

set_alrm(5);

/* 5 sekund oczekiwania na pobranie stanu */

tty_flush(); c = '\024' ; błock write(&c, 1) ;

/* przesyłamy do drukarki Control-T */

%%[ status: waiting ] % %

to drukarka oczekuje na dalsze dane od nas związane z zadaniem, które aktualnie drukuje. Oznacza to, że z poprzednim zadaniem zdarzyło się coś dziwnego. Ab} wyzerować ten stan, wysyłamy do drukarki znak EOF i kończymy pracę. Drukarka postscriptową przechowuje licznik stron. Jest on zwiększany za każdym razem, gdy jest drukowana kolejna strona, a jego zawartość nie ginie, nawet gdy wyłączymy zasilanie drukarki. Aby przeczytać ten licznik, musimv przesłać do drukarki program postscriptowy. Plik p a g e c o u n t . c (prog. 17.9] zawiera mały program postscriptowy (około dwanaście operacji PostScriptu) oraz funkcję g e t p a g e wysyłającą odpowiedni program do drukarki. #include

init_input(0); while (status == INVALID) proc_some_input(); /* czekamy na odpowiedź */

"lprps.h"

/* Program w Postscripcie do pobrania licznika stron drukarki. * Napis przekazywany przez drukarkę: * %%[ pagecount: N ] % % * będzie analizowany przez funkcję proc_msg(). */ static char pagecount_string[] = "(%%[ pagecount: ) print "

switch (status) { case IDLE: clear_alrm(); return;

/* tego właśnie się spodziewaliśmy ... */

case WAITING: /* drukarka wskazuje, że jest w trakcie obsługi zadania */ block_write(seofc, 1); /* przesyłamy do drukarki EOF */ sleep(5); exit(EXIT_REPRINT); case BUSY: case UNKNOWN: sleep(15); e x i t ( E X I T REPRINT);

Prog. 17.8 Funkcja g e t _ s t a t u ś

Program 17.8 zawiera funkcję g e t _ s t a t u ś wywoływaną z funkcji main. Wysyła ona do drukarki sekwencję Control-T, by pobrać stan drukarki. Drukarka powinna odpowiedzieć jednowierszowym komunikatem. Komunikat, którego oczekujemy, ma postać: %%[ s t a t u s : i d l e ]%%

i oznacza, że drukarka może przyjąć nowe zadanie. W funkcji proc_some_input, którą dokładnie przeanalizujemy później, czytamy i interpretujemy komunikat.

/

print przesyła dane do bieżącego pliku wyjściowego */ "statusdict begin pagecount end /* wkłada licznik stron na stos */ "20 string " /* tworzy napis o rozmiarze 20 */ "cvs " /* konwertuje do postaci napisu */ "print " • /* pisze dane do bieżącego pliku wyjściowego */ "( ]%%) print "flush\n"; /* opróżnia bieżący plik wyjściowy */

/* Funkcja czyta z drukarki początkowy i końcowy licznik stron. Argumentem * jest albo &start_page, albo &end_page. */ void get_page(int *ptrcount) set alrm(30);

/* czekamy 30 sekund na odczyt licznika stron */

tty_flush() ; block_write(pagecount_string, sizeof(pagecount_string) - 1); /* wysłanie do drukarki zapytania */ init_input(0); •ptrcount = -1; while (*ptrcount < 0) proc_some_input(); /* czytamy z drukarki wynik */ błock write(Seofc, l); proc upto eof(0);

/ /

wysyłamy do drukarki znak EOF */ oczekujemy na znak EOF z drukarki

clear alrm(); Prog. 17.9 Plik p a g e c o u n t . c - pobieranie licznika stron drukarki

17. Komunikacja z drukarką postscriptową

Tablica p a g e c o u n t s t r i n g zawiera program w PostScripcie. Mimo że możemy pobrać licznik stron i wydrukować go używając polecenia statusdict begin pagecount end = flush

celowo formatujemy dane wyjściowe, by miały postać komunikatu o stanie przekazywanego z drukarki:

17.4. Kod źródłowy

Scripcie, który wykona się i wyśle wynik do stacji komputerowej. PostScript nie jest językiem, w którym chętnie programujemy. Mimo to zdarza się, że chcemy wysłać do drukarki program w PostScripcie i odebrać dane wyjściowe na stacji komputerowej, a nie jako kolejne strony wydruku. Jednym z przykładów może być program postscriptowy, który codziennie pobiera licznik stron, aby śledzić użytkowanie drukarki. statusdict begin pagecount end =

%% t p a g e c o u n t : N ]%%

Dzięki temu funkcja proc_some_input obsługuje ten komunikat podobnie jak dowolny inny komunikat o stanie drukarki. Funkcja send_f i l e w próg. 17.10 jest wywoływana z funkcji main w celu wysłania do drukarki programu postscriptowego użytkownika.

Interpreter PostScriptu ma przekazywać dane wyjściowe tego programu, chociaż nie są skierowanym do stacji komputerowej komunikatem o stanie, by mogły one zostać następnie wysłane jako komunikat poczty elektronicznej. Plik m a i ł . c z próg. 17.11 realizuje odpowiednią obsługę. #include

.nclude

"lprps.h"

.id :nd_file(void)

/* wywoływana z main() w celu skopiowania danych ze strumienia stdin do drukarki */

int c; init_input(1) ; set i n t r ( ) ;

/* przechwytujemy SIGINT */

while ( (c = getchar()) out_char(c); out_char(EOF);

!= EOF) /* główna p ę t l a programu */ /* wypisujemy każdy znak */ /* wypisujemy końcowy bufor */

block_write(seofc, 1); /* wysyłamy do drukarki znak EOF */ proc_upto_eof(0); /* czekamy na odesłanie przez drukarkę znaku EOF */

Próg. 17.10

661

Funkcja send_f i l e

Ta funkcja jest zwykłą pętlą while, która czyta dane ze standardowego wejścia ( g e t c h a r ) i wywołuje funkcję out_char, by wypisać kolejny znak na drukarce. Po napotkaniu znaku końca pliku w standardowym strumieniu wejściowym wysyłamy do drukarki znak EOF (wskazujący zakończenie zadania) i oczekujemy na odbiór w odpowiedzi znaku końca pliku z drukarki (proc_upto_eof).

Przypominamy, że zgodnie z rys. 17.1 dane wyjściowe generowane przez interpreter PostScriptu i pojawiające się na porcie szeregowym mogą być albo komunikatem o stanie drukarki, albo wydrukiem wykonanym przez operator p r i n t . Jest więc możliwe, że wynikiem nie będzie żadna wyprowadzona strona wydruku. Taki efekt może powstać, gdy plik jest programem w Post-

"lprps.h"

static FILE *mailfp; static char temp_file[L_tmpnam]; static void open_mailfp(void); /* Funkcja jest wywoływana z proc input_char() po znalezieniu znaków nie * należących do komunikatu. Takie znaki są przesyłane z powrotem do * użytkownika. */ void mail_char(int c) { static int done_intro = 0; if (in_job && (done_intro II c != '\n')) { open_mailfp(); if (done_intro == 0) { fputs("Your Postscript printer job " "produced the foilowing output:\n", mailfp); done_intro = 1; putc(c, mailfp);

/* Funkcja jest wywoływana z proc_msg(), gdy interpreter Postscriptu * przekazuje klucz "Error" or "OffendingCommand". Wysyła do użytkownika * klucz i wartość val. */ void mail_line(const char *msg, const char *val) { if (in_job) { open_mailfp(); fprintf(mailfp, msg, val);

17. Komunikacja z drukarką postscriptową

i otrzymujemy w odpowiedzi komunikat e-mail o postaci

Funkcja tworzy i otwiera plik tymczasowy, jeśli jeszcze nie jest on otwarty. Jest wywoływana z powyższych funkcji mail_char() i mail_line(). */

Your Postscript printer job produced the following output: 11185

tatic void Den_mailfp(void) if (mailfp == NULL) { if ( (mailfp = fopen(tmpnam(temp_file), "w")) == NULL) log_sys("open_mailfp: fopen error");

Funkcja zamyka tymczasowy plik z komunikatem i wysyła go do użytkownika. Dzięki wywołaniu atexit() w funkcji main() jest zarejestrowana jako funkcja realizowana przy wywołaniu exit(). */ oid ose_mailfp(void)

663

17.4. Kod źródłowy

Plik o u t p u t . c (próg. 17.12) zawiera funkcję out_char, która była wywoływana z funkcji send_f i l e , aby wypisać kolejne znaki na drukarce. #include

"lprps.h"

static char outbuf[OBSIZE]; static int outcnt = OBSIZE; /* pozostała liczba bajtów */ static char *outptr = outbuf; static void out_buf(void); /* Funkcja wypisuje pojedynczy znak. * Jest wywoływana w pętli głównej funkcji send_file(). */

char command[1024]; if (mailfp != NULL) { if (fclose(mailfp) == EOF) log_sys{"close_mailfp: fclose e r r o r " ) ; sprintf(command, MAILCMD, loginname, hostname, temp_file); system(command); unlink(temp_file);

void out_char(int c) { if (c == EOF) { out_buf(); /* sygnalizator, że obsługa skończona */ return; if (outcnt 0) ( FD_SET(psfd, swfds); FD_SET(psfd, srfds); if (intr_flag) handle_intr(); while (select(psfd + 1, srfds, Łwfds, NULL, NULL) < 0) { if (errno == EINTR) { if (intr_flag) handle_intr{) ; /* bez powrotu */ } else log_sys("out_buf: select error" if

(FD_ISSET(psfd, srfds)) { /* można czytać z drukarki */ if ( (nread = read(psfd, ibuf, IBSIZE)) < 0) log_sys("outjsuf: read e r r o r " ) ; r p t r = ibuf; while (--nread >= 0) proc_input_char(*rptr++) ;

}

if (FD_ISSET(psfd, swfds)) { /* można przesyłać do drukarki */ if ( (nwritten = write(psfd, wptr, went)) < 0) log_sys("out_buf: write e r r o r " ) ; went -= nwritten; wptr += nwritten;

outptr = outbuf;

/* odtwarzamy pierwotną wartość wskaźnika bufora i licznika */

outcnt = OBSIZE;

665

17.4. Kod źródłowy

wymi interpretera, które mają być przesłane jako komunikat e-mail do użytkownika. Podczas zapisywania danych do drukarki musimy odpowiednio zareagować, jeśli wartość powrotu funkcji w r i t e będzie mniejsza od żądanej liczby znaków do zapisania. Przypominamy ponownie przykład z próg. 12.1, w którym przekonaliśmy się, że urządzenie terminalowe może akceptować w każdej operacji w r i t e dowolną liczbę danych. Plik i n p u t . c pokazany w próg. 17.13 definiuje funkcje, które obsługują dane wejściowe nadchodzące z drukarki. Może to być albo komunikat o stanie drukarki, albo przeznaczone dla użytkownika dane wyjściowe interpretera PostScriptu. #include

"lprps.h"

static int eof_count; static int ignore_input; static enum parse_state { /* stan analizy danych wejściowych z drukarki */ NORMAL, HAD_ONE_PERCENT, HAD_TWO_PERCENT, IN_MESSAGE, HAD_RIGHT_BRACKET, HAD_RIGHT_BRACKET_AND_PERCENT } parse_state; /* Funkcja inicjuje obsługę wejścia. */ void init_input(int job)

Próg. 17.12 Plik o u t p u t . c

Argument funkcji out_char będący znakiem EOF oznacza, że doszliśmy do końca danych wejściowych i należy wysłać ostateczną postać bufora wyjściowego do drukarki. Funkcja o u t c h a r umieszcza kolejny znak w buforze wyjściowym, a gdy bufor jest już pełny, wywołuje funkcję o u t b u f . Musimy być ostrożni, zapisując dane do bufora out_buf: możliwe jest jednoczesne wysyłanie danych do drukarki oraz przesyłanie do nas danych przez drukarkę. Aby uniknąć zablokowania w funkcji w r i t e , musimy ustalić dla deskryptora tryb pracy bez blokowania. (Przypominamy przykład w próg. 12.1). Do multipleksowania dwóch kierunków: wejścia i wyjścia używamy funkcji s e l e c t . Ustawiamy ten sam deskryptor w zbiorze odczytu i zapisu. Istnieje możliwość, że funkcja s e l e c t zostanie przerwana przez przechwycony sygnał (SIGINT), musimy więc sprawdzać, czy nie wystąpił ten błąd. Po asynchronicznym odbiorze danych wejściowych z drukarki wywołujemy funkcję p r o c i n p u t c h a r , by przetworzyć kolejne znaki. Dane wejściowe mogą być albo komunikatem o stanie drukarki, albo danymi wyjścio-

in_job = job; /* prawda, tylko gdy tę funkcję wywołano z send_file() */ parse_state = NORMAL; ignore_input = 0; /* Funkcja czyta dane z drukarki do napotkania znaku EOF. Od wartości * "ignore" zależy, czy dane wejściowe są przetwarzane, czy nie. */ void proc_upto_eof(int ignore) { int ec; ignore_input = ignore; ec = eof count; /* proc_input_char() while (ec == eof_count) proc_some_input();

\

zwiększa eof_count */

/* Funkcja oczekuje na dane, następnie odczytuje je. * Dla każdego przeczytanego znaku wywołuje funkcję proc_input_char(). */

, j i

17. Komunikacja z drukarką postscriptową id oc_some_input(void) char ibuf[IBSIZE] ; char *ptr; int nread; fd_set rfds; FD_ZERO(&rfds); FD_SET(psfd, Srfds); set_nonblock(); if (intr_flag) handle_intr(); if (alrm_flag) handle_alrm(); while (select(psfd + 1, srfds, NULL, NULL, NULL) < 0) { if (errno == EINTR) { if (alrm_flag) handle_alrm(); /* nie powraca */ else if (intr_flag) handle_intr(); /* nie powraca */ } else log_sys("proc_some_input: select error"); } if ( (nread = read(psfd, ibuf, IBSIZE)) < 0) log sys("proc_some_input: read e r r o r " ) ; else if (nread == 0) log_sys("proc_some_input: read returned 0"); p t r = ibuf; while (--nread >= 0) proc_input_char(*ptr++);

/* przetwarzamy każdy znak */

Funkcja jest wywoływana z funkcji proc_some_input() po odczytaniu danych wejściowych. Jest wywoływana również z out_buf(), gdy pojawiają się asynchroniczne dane wejściowe. */ 'id ~ o c _ i n p u t _ c h a r ( i n t c) if (c == ' \ 0 0 4 ' ) { eof_count++; /* tylko do zliczania znaków EOF */ return; } else if (ignore_input) return; /* ignorujemy wszystko oprócz znaków EOF */ switch (parse_state) { /* analizujemy dane wejściowe */ case NORMAL: if (c == '%') parse_state = HAD_ONE_PERCENT; else mail_char(c); break;

17.4. Kod źródłowy

667

case HAD_ONE_PERCENT: if (c == '%') parse_state = HAD_TWO_PERCENT; else { mail_char('%'); mail_char(c); parse_state = NORMAL; } break; case HAD_TWO_PERCENT: if (c == ' [') { msg_init(); /* początek komunikatu; inicjujemy bufor */ parse_state = IN_MESSAGE; } else { mail_char(c); mail_char('%'); mail_char('% parse_state = NORMAL; break; case IN_MESSAGE: if (c == ']') parse_state = HAD_RIGHT_BRACKET; else msg_char(c); break; case HAD_RIGHT_BRACKET: if (c == '%') parse_State = HAD_RIGHT_BRACKET_AND_PERCENT; else { msg_char(']'); msg char(c); parse_state = IN_MESSAGE; } break; case HAD_RIGHT_BRACKET_AND_PERCENT: if (c =='%') { parse_state = NORMAL; proc_msg(); /* mamy komunikat; przetwarzamy go */ } else { msg_char(']'); msg_char('%'); msg_char(c); parse_state = IN_MESSAGE; } break; default: abort();

Próg. 17.13 Plik i n p u t . c - czytanie i przetwarzanie danych wejściowych drukarki

, |

Zawsze gdy oczekujemy na nadejście znaku EOF z drukarki, wywołujemy 4 funkcj ę p r o c_up t o_e o f. Funkcja proc_some__input czyta dane z portu szeregowego. Zwróćmy uwagę, że wywołujemy funkcję s e l e c t , aby określić, kiedy deskryptor jest;

17. Komunikacja z drukarką postscriptową

gotowy do odczytu. Robimy tak, gdyż funkcję s e l e c t może przerywać przechwycenie sygnału i po takim zdarzeniu nie jest ona na ogół automatycznie wznawiana. Funkcja s e l e c t może zostać przerwana przez sygnał SIGALRM lub S I G I N T , a nie chcemy, by jej wykonanie było wówczas wznawiane. Przypominamy naszą dyskusję w podrozdz. 10.5, w której mówiliśmy, że można ustawić sygnalizator SA_RESTART, by wymusić automatyczne wznowienie funkcji wejścia-wyjścia po przechwyceniu sygnału, ale zawsze istnieje również sygnalizator o przeciwnym działaniu, który ustala, że przerwane funkcje wejścia-wyjścia nie mają być wznawiane. Jeżeli nie ustawimy jawnie sygnalizatora SA_RESTART, to zdajemy się na przyjęte ustalenia domyślne w systemie, które być może oznaczają, że przerwane funkcje wejścia-wyjścia są automatycznie powtarzane. Kiedy nadchodzą dane wejściowe z drukarki, odczytujemy je w trybie bez blokowania i pobieramy wszystko, co drukarka nam przekazuje. Wywołując funkcję p r o c _ i n p u t _ c h a r , przetwarzamy kolejny znak. Cała skomplikowana praca związana z przetwarzaniem komunikatu wysyłanego do nas z drukarki jest realizowana w funkcji p r o c _ i n p u t _ c h a r . Musimy sprawdzać po kolei każdy znak i zawsze pamiętać bieżący stan. Utrzymuje go zmienna p a r s e s t a t e . Wszystkie znaki po sekwencji %%[ umieszczamy w buforze komunikatu wywołując funkcję m s g c h a r . Po napotkaniu sekwencji ] %% wywołujemy funkcję proc_msg, aby obsłużyć komunikat. Zakładamy, że wszystkie znaki różne od początkowej sekwencji %%[, sekwencji końcowej ] %% oraz komunikatu między nimi są danymi wyjściowymi dla użytkownika, które trzeba do niego przesłać w postaci komunikatu e-mail (wywołując funkcję mail_char). Przyjrzymy się teraz funkcjom, które przetwarzają komunikat zapamiętany przez opisane powyżej funkcje wejściowe. Program 17.14 zawiera plik message.c. clude clude

"lprps.h"

I

ic char msgbuf[MBSIZE]; xc int msgcnt; ic void printer_flushing(void) ;

17.4. Kod źródłowy void msg_char(int c) { if (c != '\0' && msgcnt < MBSIZE - 1) msgbuf[msgcnt++] = c; /* * * * *

Ta funkcja jest wywoływana z funkcji proc_input_char(), tylko gdy napotkano ostatni znak procentu w sekwencji "%%[ ] % % " . Funkcja analizuje , który składa się z co najmniej jednej pary "key: val". Jeśli jest wiele par, to są one oddzielone znakiem średnika. */

void proc_msg(void) { char *ptr, *key, *val; int n; msgbuf[msgcnt] = 0 ; /* kończymy komunikat pustym znakiem */ for (ptr = strtok(msgbuf, " ; " ) ; ptr != NULL; ptr = strtok(NULL, ";")) { while (isspace(*ptr)) ptr++; /* omijamy w kluczu początkowe odstępy */ key = ptr; if ( (ptr = strchr(ptr, ':')) == NULL) continue; /* brakuje dwukropka; błąd, ignorujemy */ *ptr++ = '\0'; /* kończymy klucz pustym znakiem (zamieniamy dwukropek) */ while (isspace(*ptr)) ptr++; /* omijamy w wartości początkowe odstępy */ val = ptr; /* usuwamy w wartości końcowe odstępy */ ptr = strchr(val, '\0'); while (ptr > val && isspace(ptr[-1])) —ptr; *ptr = '\0'; if (strcmp(key, "Flushing") == 0) { printer_flushing(); /* nigdy nie powraca */

'unkcja jest wywoływana z proc_input_char() po napotkaniu znaków "H |ctóre rozpoczynają komunikat. */

} else if (strcmp(key, "PrinterError") == 0) { log_msg("proc_msg: printer error: %s", val);

_init (void)

} else if (strcmp(key, "Error") == 0) { mail_line("Your PostScript printer job " "produced the error '%s'.\n", val);

msgcnt = 0;

/* licznik znaków w buforze komunikatu */

'unkcja proc_some_input() umieszcza w buforze komunikatu wszystkie :naki między sekwencjami %%[ oraz ]%%. Poniższa funkcja >roc_msg() przeanalizuje ten komunikat. */

} else if (strcmp(key, "status") == 0) { if (strcmp(val, "idle") == 0) status = IDLE; else if (strcmp(val, "busy") == 0) status = BUSY;

669

17. Komunikacja z drukarką postscriptową else if (strcmp(val, "waiting") == 0) status = WAITING; else status = UNKNOWN; /* "printing", "PrinterError", "initializing" lub "printing test page" */ else if (strcmp(key, "OffendingCommand") == 0) { mail_line("The offending command was '%s'.\n", val); else if (strcmp(key, "pagecount") == 0) { if (sscanf(val, "%d", &n) == 1 && n >= 0) { if (start_page < 0) start_page = n; else end_page = n;

Funkcja jest wywoływana tylko z funkcji proc_msg() , gdy z drukarki odebrano komunikat "Flushing". Koniec pracy. */ ttic void .nter_flushing(void) clear_intr();

17.5. Podsumowanie

671

Jeżeli odbierzemy komunikat o postaci %% [ P r i n t e r E r r o r : reason ] %%

to jest wywoływana funkcja logjnsg, która rejestruje błąd. Inne błędy, oznaczane wartością key Error, są wysyłane do użytkownika jako komunikat e-mail. Zazwyczaj wskazują one jakiś błąd programu postscriptowego. Jeżeli otrzymamy komunikat o stanie wyróżniany wartością key równą s t a t u s , to prawdopodobnie funkcja g e t _ s t a t u s przesłała do drukarki zlecenie pobrania stanu (Control-T). Sprawdzamy wówczas wartość val i odpowiednio ustawiamy zmienną s t a t u s . Wartość Of fendingCommand w polu key nadchodzi na ogół razem z innymi parami key. val, na przykład: %%[ Error: stackunderfIow; OffendingCommand: pop ]%%

Dodajemy wówczas kolejny wiersz do komunikatu e-mail przesyłanego w odpowiedzi do użytkownika. Wartość pagecount w polu key jest generowana przez program postscriptowy w funkcji get_page (próg. 17.9). Wywołujemy funkcję sscanf, aby skonwertować val do postaci binarnej i ustalamy wartość początkową lub końcową licznika stron. W pętli while w funkcji get_page oczekujemy, by ta zmienna była nieujemna.

/* nie przechwytujemy SIGINT */

tty_flush(); /* czyścimy wejście terminalu oraz kolejki wyjściowe */ block_write(seofc, 1); /* wysyłamy do drukarki znak EOF */ proc_upto_eof(1);

/* to wywołanie nie będzie rekurencyjne, ponieważ wskazaliśmy ignorowanie danych wejściowych */ get_page(&end_page); do_acct(); exit(EXIT SUCCESS);

17.5 Podsumowanie W tym rozdziale przeanalizowaliśmy szczegółowo cały program, który przez połączenie szeregowe RS-232 wysyła do drukarki postscriptawej program w PostScripcie. Mieliśmy okazję zobaczyć zastosowanie w rzeczywistym programie wielu funkcji, które opisaliśmy w poprzednich rozdziałach: funkcji zwielokrotniania wejścia-wyjścia, wejścia-wyjścia bez blokowania, terminalowego wejścia-wyjścia oraz funkcji do obsługi sygnałów.

Próg. 17.14 Plik m e s s a g e . c - przetwarzanie komunikatów przekazywanych przez drukarkę

Funkcja msg_init jest wywoływana po znalezieniu sekwencji %% [, by zainicjować licznik bufora. Następnie jest wywoływana funkcja msg_char, która obsługuje kolejne znaki komunikatu. Funkcja proc_msg dzieli komunikat na pary key. val i analizuje każdą wartość key. Korzystamy z funkcji s t r t o k języka ANSI C, by podzielić komunikat na elementy key. val, które są odseparowane średnikiem. Po pojawieniu się komunikatu o postaci %%[ Flushing: rest of job (to end-of-file) will be ignored ]%%

jest wywoływana funkcja p r i n t e r _ f l u s h i n g , która opróżnia bufory terminalu i wysyła do drukarki znak EOF, a następnie oczekuje na odbiór znaku EOF z drukarki.

Ćwiczenia 17.1

Powiedzieliśmy, że plik drukowany przez program lprps znajduje się ' w standardowym strumieniu wejściowym, który może być łączem komunikacyjnym. Jak napisałbyś program psif (rys. 17.3), by obsługiwał tę sytuacji ; jeżeli musi on sprawdzać dwa pierwsze bajty pliku?

17.2

Zaimplementuj filtr psif, obsługując sytuację omówioną w poprzednim ćwi- ,

17.3

czemu. Przeczytaj w Adobe Systems [3] podrozdz. 12.5 tekst na temat obsługi zleceń j dotyczących czcionki w programie PostScript. Zmodyfikuj program lprps ] z tego rozdziału, by obsługiwał takie żądania. 4

18

Program obsługujący modem

.1 Wprowadzenie Od wielu lat programy obsługujące modemy musiały umieć uporać się z obsługą różnorodnych modemów. W większości systemów uniksowych istnieją dwa programy obsługujące modemy. Pierwszy to program zdalnego logowania, który umożliwia wybranie innego komputera, załogowanie się i pracę na danej stacji. W Systemie V nosi on nazwę cu, a systemy berkelejowskie nazywają go t i p . Oba programy realizują te same czynności i znają różne typy modemów. Innym programem używającym modemu jest uucico będący fragmentem pakietu UUCP. Działanie tych programów opiera się na informacjach, które są zazwyczaj wbudowane w kod źródłowy, więc jeśli przygotowujemy kolejny program obsługujący modem, to musimy wykonywać sami podobne działania implementacyjne. Podobnie, gdy chcemy zmienić istniejące programy, aby stosowały jakąś inną formę komunikacji zamiast modemowej (np. połączenia sieciowe), wówczas są konieczne bardzo znaczące modyfikacje. W tym rozdziale przygotujemy oddzielny program, który obsługuje szczegóły związane z modemem. Zgromadzimy w nim wszystkie detale, zamiast rozpraszać je między wiele modułów. (Motywem do zastosowania takiego podejścia przy tworzeniu tego programu był serwer połączeń, który opisali Presotto i Ritchie w pracy [39]). Planujemy, że nasz program po wywołaniu ma przekazać deskryptor pliku, tak jak to opisaliśmy w podrozdz. 15.3. Następnie zastosujemy go przy przygotowywaniu programu zdalnego logowania (wzorowanego na poleceniach cu oraz t i p ) .

18.2. Historia

673

18.2 Historia Polecenie cu(l) (skrót od cali Unix, wywołaj Unix) pojawiło się po raz pierwszy w Wersji 7. Jednak obsługiwało ono tylko jedno konkretne urządzenie automatycznego wywoływania (ACU, automatic cali unii). Bili Shannon z Berkeley zmodyfikował program cu i ta nowa wersja została dołączona do systemu 4.2BSD jako program t i p ( l ) . Dużą nadzieją było wprowadzenie pliku tekstowego (/etc/remote), zawierającego wszystkie potrzebne informacje dla różnych systemów (numer telefonu, zalecany numer wybierany za pomocą modemu, współczynnik szybkości w bodach, parzystość, sposób kontroli przepływu itp.). Ta wersja programu t i p obsługiwała około sześciu rodzajów jednostek dzwoniących i modemów, ale aby dodać obsługę kolejnego typu urządzenia, niezbędne były zmiany w kodzie źródłowym. Z modemów i jednostek dzwoniących korzystał, oprócz programów cu i t i p , podsystem UUCP. UUCP zarządzał ryglowaniem różnych modemów, aby jednocześnie mogło pracować kilka egzemplarzy programu UUCP. Programy t i p i cu musiały akceptować reguły protokołu ryglowania obowiązujące w podsystemie UUCP, aby uniknąć kolizji z tym podsystemem. Systemy BSD rozwinęły własny zestaw funkcji obsługujących modemy. Te funkcje były dołączane do modułów wykonywalnych UUCP, co oznaczało, że dodanie nowego modemu wymagało zmian w kodzie źródłowym. W systemie SVR2 pojawiła się funkcja dial(3), której celem było zgromadzenie w jednej funkcji bibliotecznej unikatowych właściwości związanych z wywoływaniem modemu. Była ona używana przez program cu, ale nie przez podsystem UUCP. Ponieważ należała do standardowej biblioteki języka C, więc mogły z niej korzystać wszystkie programy. W podsystemie UUCP o nazwie „Honey DanBer" [Redman 1989] z kodu źródłowego w języku C wydzielono polecenia modemowe i umieszczono je w pliku D i a l e r s . Dzięki temu dodawanie nowego typu modemów mogło odbywać się bez modyfikacji kodu źródłowego. Jednak funkcje używane przez polecenie cu oraz podsystem UUCP w celu korzystania z pliku D i a l e r s nie były ogólnie dostępne. Oznaczało to, że inne programy niż cu lub podsystem UUCP nie mogły uzyskać dostępu do pliku D i a l e r s bez powtórnego przygotowania kodu źródłowego realizującego przetwarzanie informacji z tego pliku. We wszystkich wersjach programów cu, t i p i podsystemu UUCP do zapewnienia, że tylko jeden program w określonym czasie korzysta z danego urządzenia, było potrzebne ryglowanie. Ponieważ wszystkie te programy działały w różnych systemach operacyjnych, a starsze wersje systemów nie wspierały ryglowania rekordów, więc używano bardzo prostego ryglowania plików. W efekcie rygle mogły pozostawać aktywne po awaryjnym zakonczeniu programu i, aby temu zapobiec, stosowano zaimprowizowane na goraco techniki. (Nie możemy używać ryglowania rekordów dla plików będących specjalnymi urządzeniami, a więc ryglowanie rekordów nie może być wszechstronnym rozwiązaniem).

18. Program obsługujący modem

$.3 Projekt programu Chcemy, aby nasz program obsługujący modem miał następujące właściwości. 1. Musi być możliwe dodanie nowych typów modemu bez zmian w kodzie źródłowym. Aby spełnić to wymaganie, będziemy używać pliku D i a l e r s z systemu „Hony DanBer". Kod źródłowy, który korzysta z tego pliku do wybierania numeru modemu, umieścimy w programie będącym serwerem typu demon, dzięki czemu każdy program będzie mógł z niego korzystać za pomocą funkcji komunikacji klient-serwer z podrozdz. 15.5. 2. Musi być stosowana jakaś forma ryglowania, która w przypadku awaryjnego zakończenia programu utrzymującego rygle wymusi ich zwolnienie. Techniki doraźne, np. takie, które są nadal używane przez większość wersji programów cu oraz UUCP, powinny zostać wreszcie wycofane, gdyż istniejąjuż dużo lepsze metody. Decydujemy się, by wszystkie operacje związane z ryglowaniem urządzeń realizował demon serwera. Ponieważ funkcje klient-serwer z podrozdz. 15.5 automatycznie powiadamiają serwer o zakończeniu pracy klienta, więc demon może zwolnić wszystkie rygle, których właścicielem jest kończący się proces. 3. Nowo tworzone programy muszą móc korzystać ze wszystkich właściwości, które tu przygotowujemy. Kolejny program obsługujący modem nie powinien realizować ponownie takiego samego kodu. Wybranie numeru za pomocą konkretnego modemu powinno być tak proste, jak wywołanie funkcji. W tym celu decydujemy się, by centralny demon serwera wykonywał działania polegające na wybraniu numeru za pomocą modemu i przekazaniu deskryptora pliku. 4. Programy klienckie, na przykład cu i t i p , nie powinny wymagać żadnych dodatkowych uprawnień. Nie powinny też być programami z ustawionym bitem ustanowienia identyfikatora grupy. Specjalne uprawnienia przyznajemy demonowi serwera, dzięki czemu jego klienci mogą dysponować typowymi uprawnieniami. Na rysunku 18.1 pokazujemy zależności między klientem a serwerem. W celu ustanowienia połączenia ze zdalnym systemem są realizowane poniższe kroki: 0. Startuje serwer. 1. Startuje klient i, używając funkcji c l i _ c o n n (podrozdz. 15.5), otwiera połączenie do serwera. Klient wysyła żądanie, by serwer w jego imieniu wywołał zdalny system.

18.4. Pliki danych

pliki Systems, Devices oraz Dialers

675 (2)

(1) żądanie = wywołanie zdalnego systemu

(3) wybranie numeru przez modem

(4) odpowiedź = deskryptor

Rys. 18.1 Klient i serwer

2. Serwer czyta pliki Systems, Devices i D i a l e r s , aby dowiedzieć się, jak wywołać zdalny system. (W następnym podrozdziale opiszemy te pliki). Jeżeli jest używany modem, to w pliku D i a l e r s są umieszczone polecenia potrzebne do wybrania numeru za pomocą konkretnego modemu. 3. Serwer otwiera (open) urządzenie modemowe i wybiera numer za pomocą modemu. Może to chwilę potrwać (zazwyczaj około 15-30 sekund). Serwer obsługuje wszystkie kwestie związane z ryglowaniem tego urządzenia, aby uniknąć kolizji z jego innymi użytkownikami. 4. Jeżeli wybranie numeru powiodło się, to serwer przekazuje klientowi deskryptor pliku związanego z urządzeniem modemu. Nasze funkcje z podrozdz. 15.3 wysyłają i odbierają ten deskryptor. 5. Klient komunikuje się bezpośrednio z modemem. Serwer nie uczestniczy w tej wymianie danych, klient czyta (read) i zapisuje (write), używając deskryptora pliku, który otrzymał w kroku 4. Klient i serwer (kroki 1 i 4) komunikują się za pomocą łącza strumieniowego. Klient zamyka to łącze strumieniowe, gdy zakończy współpracę ze zdalnym systemem (typowo dzieje się to, gdy kończy się program klienta). Serwer zauważa to zamknięcie i zwalnia rygiel urządzenia modemowego.

18.4 Pliki danych W tym podrozdziale opiszemy trzy pliki, które są używane przez podsystejn: „Honey DanBer" UUCP: Systems, Devices i D i a l e r s . Jest w nich wiele; 1 pól, które są używane przez system UUCP. Nie będziemy ich tutaj szczegółowo opisywać (ani systemu UUCP). Zainteresowanych tymi zagadnieniami, odsyłamy do pracy Redmana, 1989 [40]. j W tabeli 18.1 pokazujemy sześć pól pliku Systems. Prezentujemy je w kolejnych kolumnach. '4 Pole name jest nazwą zdalnego systemu. Używamy jej w poleceniach, np. cu h o s t l . Zauważmy, że możemy mieć wiele wpisów dla tego samego

18. Program obsługujący modem

677

18.4. Pliki danych

zdalnego systemu. Wpisy są wybierane w kolejności ich umieszczenia. Wpisy nazwane modem i l a s e r służą do bezpośredniego połączenia z modemem i drukarką laserową. Nie musimy wybierać numeru za pomocą modemu, aby połączyć się z tymi urządzeniami, ale musimy otworzyć odpowiednią linię terminalową oraz obsłużyć potrzebne rygle.

Ostatnie pole, dialer, służy do znalezienia odpowiedniego wpisu w pliku D i a l e r s . Wpisy dla urządzeń bezpośrednio dołączonych mają w tym polu napis d i r e c t . W tabeli 18.3 pokazujemy format pliku D i a l e r s . Jest to plik zawierający wszystkie polecenia związane z wybieraniem numeru za pomocą modemu.

Tabela 18.1 Plik Systems

Tabela 18.3 Plik D i a l e r s

name

time

type

class

phone

login

hostl

dowolny

ACU

19200

5551234

(nie używany)

hostl

dowolny

hostl

dowolny

ACU

9600

5552345

(nie używany)

ACU

2400

5556789

(nie używany)

modem

dowolny

modem

19200

-

(nie używany)

laser

dowolny

laser

19200

-

(nie używany)

Pole time określa czas oraz dzień tygodnia, kiedy dana stacja ma zostać wywołana. Jest to pole typowe dla podsystemu UUCP. Pole type decyduje o tym, którego wpisu z pliku Devices należy użyć dla nazwy name. Pole class określa szybkość linii (w bodach). Pole phone ustala numer telefonu dla wpisów o wartości ACU w polu type. Dla pozostałych wpisów pole phone T&wiera znak łącznika. Resztę wiersza zajmuje ostatnie pole, login. Jest to sekwencja napisów, używanych przez podsystem UUCP do załogowania się w systemie zdalnym. Nam nie jest ono potrzebne. Plik Devices zawiera informację o modemach oraz dołączonych bezpośrednio stacjach komputerowych. W tabeli 18.2 pokazujemy pola tego pliku. Pole type jest dopasowywane do odpowiedniego wpisu w plikach Systems oraz Devices. Pole class musi również być takie samo jak odpowiednie pole w pliku Systems. Zazwyczaj określa ono szybkość linii. Tabela 18.2 Plik Devices type

linę

Hne2

class

dialer

ACU

cuaO

-

19200

tbfast

ACU

cuaO

-

9600

tb9600

ACU

cuaO

-

2400

tb2400

ACU

cuaO

-

1200

tbl200

modem

ttya

-

19200

direct

laser

ttyb

-

19200

direct

Właściwą nazwę urządzenia otrzymujemy, poprzedzając napis z pola linę sekwencją znaków /dev/. W podanym przykładzie rzeczywiste nazwy urządzeń są następujące: /dev/cua0, / d e v / t t y a oraz / d e v / t t y b . Następne pole, Une2, nie jest używane.

dialer

sub

handshake

tb9600

=w-,

"" \dA\pA\pA\PTQ0S2=255S12=255s50=6s58=2s68=255\r\c OK\r \EATDT\T\r\c CONNECT\s9600 \r\c ""

tbfast

=w-,

"" \dA\pA\pA\pTQ0S2=255S12=255s50=255s58=2s68=255sll0=lslll=30\r\c OK\r \EATDT\T\r\c CONNECT\sFAST

Pokazujemy tylko dwa wpisy w tym pliku, nie przedstawiamy wpisów dla t b l 2 0 0 oraz tb2400, do których odwołujemy się w pliku Devices. Pole handshake jest umieszczone w jednym wierszu, złamaliśmy je tylko po to, by zmieściło się na stronie. Pole dialer jest używane do znalezienia odpowiedniego wpisu w pliku Devices. Pole sub określa, jak należy zastąpić znak równości oraz znak minusa w numerze telefonu. W dwóch wpisach z tab. 18.3 pole to ustala, że znak równości ma zostać zastąpiony znakiem w, a znak minusa znakiem przecinka. Dzięki temu numery telefonów w pliku Systems mogą zawierać znak równości (oznaczający „oczekiwanie na sygnał ciągły") oraz znak minusa (oznaczający „pauzę"). Sposób translacji tych dwóch znaków dla potrzeb konkretnego modemu określa plik D i a l e r s . Ostatnie pole, handshake, zawiera instrukcje służące do wybierania numeru przez modem. Jest to sekwencja napisów oddzielonych znakami odstępu lub tabulacji, którą nazywamy napisami oczekiwanymi i wysyłanymi. Oczekujemy (czyli czytamy, dopóki nie uzyskamy zgodności) pierwszego napisu, a następnie wysyłamy (czyli zapisujemy) kolejny napis. Przyjrzyjmy się wpisowi t b f a s t . Dotyczy on modemu Telebit Trailblazer, pracującego w trybie PEP (packetized ensemble protocol).

1. Pierwszy napis, na który oczekujemy, jest pusty, co oznacza „nie czekaj na nic". Dowolna przeczytana sekwencja jest zgodna z pustym napisem. 2. Wysyłamy następny napis. Specjalne sekwencje są poprzedzone zna-; 1 kiem \. Sekwencja \d wprowadza opóźnienie o 2 sekundy. Następnie wysyłamy A. Robimy przerwę na pół sekundy (\p) i wysyłamy ko-. lejny znak A, pauzujemy, wysyłamy kolejny znak A i ponownie pau-j żujemy. Następnie wysyłamy pozostałe znaki napisu, począwszy od litery T. Te polecenia ustalają parametry modemu. Sekwencja \ri[ przesyła znak powrotu karetki, a ostatnia sekwencja, \c, mówi, że nie należy zapisywać znaku nowego wiersza na końcu wysłanego napisu.

18. Program obsługujący modem

3. Czytamy dane (read) z modemu, dopóki nie odbierzemy napisu OK\r. (Sekwencja \r oznacza znak powrotu karetki). 4. Kolejny napis zaczyna się od sekwencji \E. Służy do włączenia echa wprowadzonych znaków na ekranie: po wysłaniu każdego znaku do modemu czekamy, aż nie przeczytamy jego echa. Następnie wysyłamy cztery znaki ATDT. Kolejny znak specjalny, \T, zostaje zastąpiony numerem telefonu. Po nim jest przesyłany znak powrotu karetki, a typowy znak nowego wiersza, znajdujący się na końcu napisu, nie jest przesyłany. 5. Ostatnim napisem, na który czekamy, jest przekazywany przez modem komunikat CONNECT FAST. (Sekwencja \s oznacza pojedynczy odstęp). Po otrzymaniu ostatniego napisu proces wybierający numer za pomocą modemu zostaje zakończony. (Istnieje wiele innych sekwencji, które mogą pojawić się w napisie handshake, ale nie omawiamy ich tutaj). Podsumujmy działania, jakie musimy wykonać w odniesieniu do trzech wymienionych na początku plików. 1. Pobranie nazwy zdalnego systemu, wyszukanie w pliku Systems pierwszego wpisu o zadanej nazwie (name). 2. Wyszukanie w pliku Devices wpisu, który w polach type oraz class ma te same wartości co wybrany wpis w pliku Systems. 3. Wyszukanie w pliku D i a l e r s wpisu, który ma taką samą wartość pola dialer co wybrany wpis w pliku Devices. 4. Wybranie numeru za pomocą modemu. Są dwie potencjalne przyczyny niepowodzenia: (1) urządzenie wskazane w pliku linę jest już używane przez kogoś innego lub (2) wybranie numeru nie uda się (telefon w zdalnym systemie jest zajęty lub zdalny system nie działa i nie odpowiada na telefony). Drugi przypadek jest często wykrywany przez licznik czasu oczekiwania, który jest ustawiany, gdy czytamy dane z modemu i sprawdzamy ich zgodność z określonym napisem oczekiwania (zob. ćw. 18.10). W każdej sytuacji, jeśli nie powiedzie się wybranie modemu, chcemy powrócić do kroku numer 1 i poszukać kolejnego wpisu, który dotyczy tego samego zdalnego systemu. Jak wynikało z tab. 18.1 jeden zdalny system może mieć kilka wpisów, każdy z innym numerem telefonu (każdy numer telefonu może odpowiadać innemu urządzeniu). W systemie Honay DanBer istnieją inne pliki, których nie używamy w przykładzie w tym rozdziale. Plik Dialcodes określa kody stosowane w numerach telefonów umieszczonych w pliku Systems. Plik S y s f i l e s umożliwia określenie alternatywnych kopii plików Systems, Devices oraz Dialers.

679

18.5. Projekt serwera

18.5 Projekt serwera Zacznijmy od opisu serwera. Na jego projekt mają wpływ dwa czynniki: 1. Wybranie numeru zajmuje trochę czasu (15-30 sekund), więc serwer musi wywołać funkcję fork, by utworzyć proces potomny, który zajmie się procesem wybrania. 2. Serwer typu demon (proces macierzysty) musi być jedynym procesem, który obsługuje wszystkie rygle. Na rysunku 18.2 pokazujemy zależności między procesami.

pliki Systems, Devices oraz Dialers

proces macierzysty

potomek fork serwer

serwer

exit

polecenia wybrania numeru przez modem

modem

żądanie

klient

Rys. 18.2 Organizacja procesów podczas wybierania numeru za pomocą modemu

Serwer wykonuje następujące kroki: 1. Proces macierzysty odbiera żądanie od klienta za pośrednictwem ogólnie znanej nazwy serwera. Jak opisaliśmy w podrozdz. 15.5, powstaje unikatowy strumień komunikacyjny między serwerem a klientem. Proces macierzysty musi obsługiwać jednocześnie wielu klientów, tak jak serwer otwierający pliki z podrozdz. 15.6. 2. Dysponując nazwą zdalnego systemu, z którym klient chce się skontaktować, proces macierzysty przegląda pliki Systems oraz Devices, aby dopasować pola. Ponieważ proces macierzysty utrzymuje również ta ' blicę ryglowań, w której jest informacja o aktualnie używanych urządzeniach, więc może pominąć wpisy w pliku Devices odpowiadające; zajętym urządzeniom. ; 3. Proces macierzysty po wyszukaniu odpowiednich wpisów generują nowy proces (fork), który zajmie się wybraniem numeru. (Od tej; chwili proces macierzysty może obsługiwać kolejnych klientów). Gd>j wszystko się uda, wówczas proces potomny odsyła klientowi deskryptor związany z modemem, używając łącza strumieniowego przy-ą dzielonego temu klientowi (które zostało powielone w wyniku wywołania fork) i wywołuje funkcję exit(0). Jeśli pojawi się błąd (zajęta

18. Program obsługujący modem linia telefoniczna, brak odpowiedzi itp.), to potomek wywołuje funkcję e x i t ( l ) . 4. Proces macierzysty po odbiorze sygnału SIGCHLD, powiadamiającego o zakończeniu pracy przez klienta, pobiera stan zakończenia (waitpid). Jeśli potomek pomyślnie zakończył pracę, to proces macierzysty nie ma nic więcej do zrobienia. Rygiel musi być utrzymywany, dopóki klient nie zakończy korzystania z urządzenia modemowego. Łącze strumieniowe między klientem a jego procesem macierzystym, ustanowione specjalnie dla tego klienta, pozostaje otwarte. Dzięki temu, jeżeli klient zakończy pracę, proces macierzysty zostanie o tym powiadomiony i wówczas zwolni rygiel. Jeśli potomek zakończył pracę awaryjnie, to proces macierzysty powraca do pliku Systems, do miejsca, które opuścił w celu obsługi klienta i próbuje znaleźć kolejne pasujące wartości. Jeśli znajdzie inny wpis dla tego zdalnego systemu, to powraca do kroku 3 i generuje nowy proces potomny, który ma wybrać kolejny numer. Gdy nie ma więcej wpisów dla tego zdalnego systemu, wówczas wywołuje funkcję send e r r (próg. 15.4) i zamyka łącze strumieniowe tego klienta. Dysponowanie unikatowym połączeniem z każdym klientem pozwala na wysłanie im danych diagnostycznych, jeśli jest to potrzebne. Często klient chce śledzić, jak przebiega wybieranie numeru za pomocą modemu, szczególnie gdy pojawiają się jakieś problemy. Chociaż proces wybierania jest realizowany przez potomka niespokrewnionego serwera, unikatowe połączenie umożliwia przesłanie danych wyjściowych bezpośrednio do klienta.

68]

18.6. Kod źródłowy serwera Tabela 18.4 Pliki źródłowe serwera Plik źródłowy

Proces macierzysty/potomny Funkcje P

childdial.c

child dial

cliargs.c

M

cli args

client.c

M

client alloc, client add, client del, client sigchld

ctlstr.c

P

ctl str

debug.c

P

DEBUG, DEBUG NONL

devfile.c

M

dev next, dev rew, dev find

dialfile.c expectstr.c lock.c

M

P

dial next, dial rew, dial find

P

expect_str, expt_read, sig alrm find linę, lock set, lock rei, is locked

loop.c

. M

main.c

M

main

reąuest.c

M

reąuest

loop, cli done, child done

P

sendstr.c

send str

sigchld.c

M

sig chld

sysfile.c

M

sys next, sys rew, sys posn

ttydial.c

P

tty dial

ttyopen.c

P

tty open

Kod źródłowy serwera Nasz serwer składa się z siedemnastu plików źródłowych. W tabeli 18.4 podajemy szczegółowe informacje o tym, jakie funkcje są umieszczone w poszczególnych plikach oraz kto z nich korzysta - proces macierzysty czy potomny. Na rysunku 18.3 pokazujemy sposób wywołania poszczególnych funkcji. Program 18.1 zawiera plik nagłówkowy c a l l d . h , który jest włączany przez wszystkie pliki źródłowe. Dołącza on standardowe pliki nagłówkowe, definiuje podstawowe stałe i deklaruje zmienne globalne. Definiujemy strukturę C l i e n t , która zawiera wszystkie informacje o klientach. Jest to rozszerzenie podobnej struktury pokazanej w próg. 15.26. W czasie między wywołaniem funkcji fork, by utworzyć potomka, który zajmie się wybraniem numeru w imieniu klienta, a zakończeniem pracy tego potomka możemy obsłużyć dowolną liczbę innych klientów. Struktura C l i e n t zawiera wszystkie informacje, które są nam potrzebne, aby wyszukać kolejny wpis w pliku Systems dla danego klienta i powtórzyć próbę wybrania modemu.

loop reguest

fork

child_dial

i I

dial_find sig chld

tty_open tty_dial

"I

send_fd, exit(0) lub writen, exit(1) Rys. 18.3 Wywołania funkcji w serwerze

18. Program obsługujący modem Unclude Unclude Unclude Unclude fdefine fdefine tdefine Wefine tdefine łdefine tdefine tdefine tdefine

(dodatek B) są wysyłane na standardowy strumień komunikatów awaryjnyc W przeciwnym razie są zapisywane do kroniki systemowej za pomocą tec niki syslog. Funkcja loop jest główną pętlą serwera (próg. 18.3). Obsługuje wie deskryptorów, używając funkcji s e l e c t . #include łfinclude łtinclude

"calld.h"

static void cli_done(int); static void child_done(int);

static fd_set allset; /* 1 bit na 1 połączenie klienta i 1 dla listenfd *, /* modyfikowane przez loop() i cli_done() */ void loop(void) { int char Client uid_t fd_set

i, n, maxfd, maxi, buf[MAXLINE]; *cliptr; uid; rset;

listenfd,

nread;

if (signal_intr(SIGCHLD, sig_chld) == SIG_ERR) log_sys("signal e r r o r " ) ; if (

j

"

/* otrzymujemy deskryptor oczekiwania na żądania klienta j (listenfd = serv_listen(CS_CALL)) < 0) i log_sys ( "serv_listen e r r o r " ) ; -4

FD_ZERO(sallset); FD_SET(listenfd, s a l l s e t ) ;

18. Program obsługujący modem

/* Nadeszły dane od klienta.

maxfd = l i s t e n f d ; maxi = - 1 ;

child_done(raaxi) ; rset = allset; /* zmienna rset jest modyfikowana przy każdym okrążeniu */ if { (n = select(maxfd + 1, Srset, NULL, NULL, NULL)) < 0) { if (errno == EINTR) { /* przechwycony sygnał SIGCHLD, znaleziony wpis z ustawionym sygnalizatorem childdone */ child_done(maxi); continue; /* ponowne wywołanie select */ } else log_sys("select error"); }

log_msg("starting: %s, from uid %d", buf, uid); /* Analizujemy argumenty, ustalamy opcje. Ponieważ możemy potem chcieć ponowić próbę wywołania dla tego klienta, więc zapamiętujemy opcje w tablicy clientf]. */ if (buf_args(buf, cli_args) < 0) log_quit("command linę error: %s", buf); cliptr->Debug = Debug; cliptr->parity = parity; strcpy(cliptr->sysname, sysname); strcpy(cliptr->speed, (speed == NULL) ? "" : speed); cliptr->childdone = 0; cliptr->sysftell = 0; cliptr->foundone = 0;

if (FD_ISSET{listenfd, srset)) { /* akceptujemy nowe żądanie klienta */ if ( (clifd = serv_accept(listenfd, suid)) < 0) log_sys("serv_accept error: %d", clifd); i = client_add(clifd, uid); FD_SET(clifd, sallset); if (clifd > maxfd) maxfd = clifd; /* maksymalna wartość fd dla funkcji select() */ if (i > maxi) maxi = i; /* najwyższa pozycja w tablicy client[] */ log_msg("new connection: uid %d, fd %d", uid, clifd); continue;

if (reąuest(cliptr) < 0) { / nie znaleziono systemu lub nie jest możliwe połączenie */ if (send_err(cliptr->fd, -1, errmsg) < 0) log_sys("send_err error"); cli_done(clifd); continue;

}

/* W tym miejscu funkcja reąuest() wywołała już fork i istnieje potomek, który próbuje wybrać zdalny system. Gdy potomek zakończy pracę, będzie znany jego stan. */

/* Przeglądamy dane w tablicy client[]. Czytamy wszystkie nadchodzące dane od klienta */ for (cliptr = &client[0]; cliptr fd) < 0) continue; if (FD_ISSET(clifd, srset)) ( /* Czytamy bufor argumentów od klienta */ if ( (nread = read(clifd, buf, MAXLINE)) < 0) log_sys("read error on fd %d", clifd); else if (nread == 0) { /* Klient zakończył pracę lub zamknął łącze strumieniowe. Zwalniamy rygiel urządzenia. */ clifd);

Przetwarzamy żądanie klienta. */

if (buf[nread-1] != 0) { log_quit("reąuest from uid %d not null terminated:" " %*.*s", uid, nread, nread, buf); cli_done(clifd); continue;

for ( ; ; ) { if (chld_flag)

log_msg("closed: uid %d, fd %d", cliptr->uid, lock_rel(cliptr->pid); cli_done(clifd); continue;

687

18.6. Kod źródłowy serwera

/* * * *

Analizujemy tablicę client[], szukając klientów, których procesy potomne zakończyły wybieranie numeru. Ta funkcja jest wywoływana przez loopO, gdy sygnalizator chld_flag ma wartość niezerową (ustawianą prze'z procedurę obsługi SIGCHLD). */ '

static void child_done(int maxi)

.

< " Client

j *cliptr;

agam: chld_flag = 0;

; by kontrolować, kiedy skończymy pętlę dla większej liczby sygnałów SIGCHLD */

18. Program obsługujący modem for (cliptr = &client[0]; cliptr fd) < 0) continue; if (cliptr->childdone) { log_msg("child done: pid %d, status %d", cliptr->pid, cliptr->childdone-l);

18.6. Kod źródłowy serwera

dury obsługi. Potem funkcja loop wywołuje funkcje s e r v _ l i s t e n (programy 15.19 i 15.22). Pozostała część funkcji jest nieskończoną pętlą z wywołaniem funkcji s e l e c t , która sprawdza następujące dwa warunki: 1. Jeśli nadeszło nowe połączenie klienckie, to wywołujemy funkcję s e r v _ a c c e p t (programy 15.20 i 15.24). Funkcja c l i e n t _ a d d tworzy nową pozycję w tablicy c l i e n t związaną z nowym klientem. 2. Następnie przeglądamy tablicę c l i e n t , aby sprawdzić, czy (a) jakiś klient zakończył pracę lub (b) nadeszły nowe żądania od klienta. Gdy klient kończy pracę (z własnej woli lub nie), wówczas łącze strumieniowe tego klienta jest zamykane, co powoduje odczytanie znaku końca pliku z łącza. Możemy wówczas zwolnić rygle urządzenia, których właścicielem jest ten klient, i usunąć odpowiednią pozycję w tablicy c l i e n t . Kiedy nadchodzi żądanie od klienta, po wstępnym przygotowaniu wywołujemy funkcję r e ą u e s t . (Funkcję buf_args pokazaliśmy w próg. 15.17). Jeśli nazwa zdalnego systemu jest poprawna i znajdziemy wpis opisujący obecnie dostępne urządzenie, to funkcja r e ą u e s t generuje proces potomny i powraca.

/* Jeśli potomek zakończył poprawnie pracę (exit(0)), tylko zerujemy sygnalizator. Gdy klient skończy pracę, odczytamy z łącza strumieniowego znak EOF i zwolnimy rygiel urządzenia. */ if (cliptr->childdone == 1) { /* potomek wykonał exit(0) */ cliptr->childdone = 0; continue; /* Nie udało się: potomek wykonał exit(l). Zwalniamy rygiel urządzenia i próbujemy ponownie od miejsca, które opuściliśmy. */ cliptr->childdone = 0; lock_rel(cliptr->pid); /* odryglowanie wejścia urządzenia */ if (reąuest(cliptr) < 0) { /* nadal nie można, pora się poddać */ if (send_err(cliptr->fd, -1, errmsg) < 0) log_sys("send_err error"); cli_done(clifd); continue; }

/* reąuest() wygenerowała kolejnego potomka dla tego klienta */

if (chld_flag) /* przechwycono dodatkowe sygnały SIGCHLD */ goto again; /* musimy sprawdzić ponownie wszystkie sygnalizatory childdone */

689

W czasie wykonywania tej funkcji może nastąpić zakończenie procesu potomnego. Jeśli jesteśmy wówczas zablokowani w funkcji s e l e c t , to powrócimy z błędem EINTR. Ponieważ sygnał może nadejść w dowolnym miejscu funkcji loop, więc przed każdym wywołaniem funkcji s e l e c t w naszej pętli sprawdzamy sygnalizator c h l d f lag. Jeżeli sygnał pojawił się, to wywołujemy funkcję child_done, aby obsłużyć zakończenie. Ta funkcja przegląda całą tablicę c l i e n t , sprawdzając dla każdej pozycji sygnalizator childdone. Jeśli potomek pomyślnie zakończył pracę, nie musimy nic więcej robić. Jeśli zakończył pracę ze stanem 1, to wywołujemy funkcję r e ą u e s t , aby znaleźć inny wpis w pliku Systems dla tego klienta.

Porządki po zakończeniu obsługi klienta. */ a t i c void i_done(int

#include clifd)

client_del(clifd); /* usuwamy wpis w tablicy c l i e n t [ ] */ FD_CLR(clifd, S a l l s e t ) ; /* wyłączamy bit w zbiorze dla select () */ close(clifd); /* zamykamy nasz koniec łącza strumieniowego */

Próg. 18.3 Plik l o o p . c

Funkcja inicjuje tablicę c l i e n t i ustanawia procedurę obsługi sygnału SIGCHLD. Wywołujemy funkcję s i g n a l _ i n t r zamiast s i g n a l , dzięki temu każda powolna funkcja systemowa zostanie przerwana po powrocie z proce-

"calld.h"

/* Tę funkcję wywołuje buf_args() wywoływana z funkcji loop(). * buf_args() utworzyła na podstawie bufora klienta tablicę wzorowaną na * tablicy argv[], którą teraz przetwarzamy. */ int cli_args(int argc, char **argv) { int c; if (argc < 2 || strcmp(argv[0], CL_CALL) != 0) { strcpy(errmsg, "usage: cali "); return(-1);

18. Program obsługujący modem Debug = 0 ; /* opcja domyślna */ parity = NONE; speed = NULL; opterr = 0; /* funkcja getopt() nie ma wypisać komunikatów na stderr */ optind = 1; /* ponieważ wywołujemy getopt() wielokrotnie */ while ( (c = getopt(argc, argv, "des:o")) != EOF) { switch (c) { case 'd': Debug = 1; /* klient chce, by była wyprowadzana diagnostyka */ break; case 'e': /* parzystość */ parity = EVEN; break; case 'o 1 : /* nieparzystość */ parity = ODD; break; case 's 1 : /* szybkość */ speed = optarg; break; case '?': sprintf(errmsg, "unrecognized option: -%c\n", optopt); return(-1);

if (optind < argc) sysname = argv[optind]; /* nazwa wywoływanej stacji */ else { sprintf(errmsg, "missing to call\n"); return(-1); } return(0);

Próg. 18.4 Funkcja cli_args

Program 18.4 zawiera funkcję c l i _ a r g s wywoływaną przez buf_args w funkcji loop, gdy nadchodzi żądanie od klienta. Funkcja ta przetwarza argumenty wiersza poleceń otrzymane od klienta. Zwróćmy uwagę, że ustawia ona zmienne globalne na podstawie argumentów wiersza polecenia, funkcja loop kopiuje je następnie do odpowiedniego wpisu w tablicy a r r a y , ponieważ ta opcja ma wpływ tylko na jedno żądanie klienta. Program 18.5 zawiera plik c l i e n t . c definiujący funkcje manipulujące tablicą c l i e n t . Jedyną różnicą między programami 18.5 a 15.27 jest sprawdzanie wpisów na podstawie identyfikatora procesu (funkcja client

sigchld).

18.6. Kod źródłowy serwera #include

"calld.h"

s t a t i c void client_alloc(void) int

/* alokacja większej liczby pozycji w tablicy client[] */

i;

if (client == NULL) client = malloc(NALLOC * sizeof(Client)); else c l i e n t = r e a l l o c ( c l i e n t , (client_size + NALLOC) * sizeof(Client)); if (client == NULL) err_sys("can 1 t alloc for c l i e n t array"); /* musimy zainicjować nowe pozycje */ for (i = client_size; i < client_size + NALLOC; i++) client[i].fd = -1; /* gdy fd równa się - 1 , to pozycja j e s t niedostępna */ client size += NALLOC;

/* Wywoływana z funkcji loop(), gdy nadchodzi połączenie od nowego * klienta. */ int client_add(int fd, uid_t uid) { int i; if (client == NULL) /* gdy pierwsze wywołanie */ client_alloc(); again: for (i = 0 ; i < client_size; i++) { if (client[i].fd == -1) { /* znaleziona dostępna pozycja */ client[i].fd = fd; client[i].uid = uid; return(i); /* przekazujemy indeks w tablicy client[] */

/* pełna tablica client, pora wykonać realloc */ client_alloc(); goto again; /* i przeszukujemy ponownie (teraz się uda) */

/* Wywoływana z funkcji loop(), gdy kończymy obsługę klienta. */ void client_del(int fd) int

i;

18. Program obsługujący modem for

static Lock * find line(char *line)

(i = 0 ; i < client_size; if ( c l i e n t [ i ] . f d == fd) client[i].fd = -1; return;

int Lock

log_quit("can't find client entry for fd %d",

fd) ;

Wyszukanie pozycji klienta odpowiadającego identyfikatorowi procesu. Ta funkcja jest wywoływana przez sig_chld(), tj. procedurę obsługi sygnału, tylko gdy potomek zakończył pracę. */ id ient_sigchld(pid_t pid, int stat) int

i;

for (i = 0 ; i < client_size; i++) { if (client[i].pid == pid) { client[i].childdone = stat;

692

18.6. Kod źródłowy serwera

/* stan potomka z exit() powiększony o 1 */

i;

*lptr;

for (i = 0; i < nlocks; i++) { if (strcmp(linę, lock[i].linę) == 0) return(&lock[i]); /* znaleziona pozycja dla tego urządzenia */ } /* Nie znaleziono odpowiedniej pozycji. To urządzenie nie było dotychczas ryglowane. Dodajemy nowe pozycję do tablicy lock[]. */ if (nlocks >= lock_size) { /* brak miejsca w tablicy lock[] */ if (lock == NULL) /* pierwsze okrążenie */ lock = malloc(NALLOC * sizeof(Lock)); else lock = realloc(lock, (lock_size + NALLOC) * sizeof(Lock)); if (lock ==.NULL) err_sys("can1t alloc for lock array"); lock size += NALLOC;

return; log_quit("can't find client entry for pid %d",

pid);

Próg. 18.5 Plik c l i e n t . c

Program 18.6 jest plikiem l o c k . c . Te funkcje służą procesowi potomnemu do zarządzania tablicą lock. Tak samo jak w funkcji c l i e n t , wywołujemy funkcję r e a l l o c , aby dynamicznie przydzielać pamięć dla tablicy lock i w ten sposób uniknąć ograniczeń wprowadzanych w czasie kompilacji. nclude

"calld.h"

pedef struct { char *line; /* Did_t pid;

wskazuje obszar uzyskany z mallocO */ /* ryglujemy, korzystając z nazwy linii (nazwa urządzenia ) */ /* odryglowujemy na podstawie identyfikatora procesu */ /* identyfikator procesu równy 0 oznacza dostępność */

Lock; atic Lock "lock = NULL; /* tablica z malloc/realloc */ atic int lock_size; /* liczba pozycji w lock[] */ atic int nlocks; /* liczba pozycji aktualnie używanych w lock[] Wyszukanie w tablicy lock[] konkretnego urządzenia (linę). Jeśli go nie znajdziemy, to tworzymy na końcu tablicy lock[] nową pozycję dla tego urządzenia. W ten sposób są stopniowo dodawane do tablicy lock[] wszystkie możliwe urządzenia. */

lptr = &lock[nlocks++]; if ( (lptr->line = malloc(strlen(linę) +1)) == NULL) log_sys("malloc error"); strcpy(lptr->line, linę); /* kopiujemy nazwę linii w procesie wywołującym */ lptr->pid = 0; return(lptr); void lock_set(char *line, pid_t pid) Lock

*lptr;

log_msg("locking %s for pid lptr = find_line(linę) ; lptr->pid = pid;

", linę, pid)

void lock_rel(pid_t pid) Lock

*lptr;

for (lptr = &lock[0]; lptr < slock[nlocks]; lptr++) { if (lptr->pid == pid) { log_msg("unlocking %s for pid %d", lptr->line, pid) lptr->pid = 0; return;

18. Program obsługujący modem

if (sysptr->name[0] == '#') goto again; /* ignorujemy wiersz komentarza */

log_msg("can't find lock for pid = %d", pid); d_t lockedfchar

if ( (sysptr->time = strtok(NULL, WHITE)) == NULL) log_quit("missing 'time' in Systems file, linę %d", syslineno);

*line)

return( find_line(linę)->pid );

/* niezerowy ident. procesu oznacza zaryglowanie */

if ( (sysptr->type = strtok(NULL, WHITE)) == NULL) log_quit("missing 'type' in Systems file, linę %d", syslineno); if ( (sysptr->class = strtok(NULL, WHITE)) == NULL) log_quit{"missing 'class' in Systems file, linę %d", syslineno);

Próg. 18.6 Funkcje zarządzające ryglami urządzenia klienta

Każda pozycja w tablicy lock jest związana z pojedynczą linią (drugie pole, linę, w pliku Devices). Ponieważ funkcje ryglujące nie znają wszystkich pozostałych wartości pola linę w tym pliku danych, więc nowe pozycje w tablicy lock są tworzone za każdym razem, gdy jest ryglowana nowa linia. Realizuje to funkcja f i n d _ l i n e . Kolejne trzy pliki źródłowe obsługujątrzy pliki danych Systems, Devices oraz D i a l e r s . Każdy plik ma swoją funkcję XXX_next, która czyta następny wiersz pliku i dzieli go na pola. Aby podzielić wiersz na pola, wywołujemy funkcję standardu ANSI C o nazwie s t r t o k . Program 18.7 służy do obsługi pliku Systems. nclude

695

18.6. Kod źródłowy serwera

"calld.h"

atic FILE *fpsys = NULL; atic int syslineno; /* dla komunikatów o błędzie */ atic char sysline[MAXLINE]; nie może być automatycznie; sys_next() przekazuje wskaźnik do tego miejsca */ Funkcja czyta z pliku Systems jeden wiersz i dzieli go na elementy. ng

/* przekazuje wartość dodatnią, jeśli wszystko w porządku; -1, jeśli EOF */ s_next(Systems *sysptr) /* wypełnienie struktury wskaźnikami */ if (fpsys == NULL) { if ( (fpsys = fopen(SYSTEMS, "r")) == NULL) log_sys("can't open %s", SYSTEMS); syslineno = 0;

if (fgets(sysline, MAXLINE, fpsys) == NULL) return(-1); /* EOF */ syslineno++; if ( (sysptr->name = strtok(sysline, WHITE)) == NULL) { if (sysline[0] == '\n') goto again; /* ignorujemy pusty wiersz */ log_quit("missing 'name' in Systems file, linę %d", syslineno);

if ( (sysptr->phone = strtok(NULL, WHITE)) == NULL) log_quit("missing 'phone' in Systems file, linę %d", syslineno); if ( (sysptr->login = strtok(NULL, "\n")) == NULL) log_quit("missing 'login' in Systems file, linę %d", syslineno); return(ftell(fpsys));

/* przekazujemy pozycję w pliku Systems */

void sys_rew(void) { if (fpsys != NULL) rewind(fpsys); syslineno = 0;

void

sys_posn(long posn) /* pozycja w pliku Systems */ { if (posn == 0) sys_rew(); else if (fseek(fpsys, posn, SEEK_SET) != 0) log_sys("fseek error");

Próg. 18.7 Funkcje czytające plik S y s t e m s

Z funkcji r e ą u e s t wywołujemy funkcję sys_next, aby odczytać kolejny wpis w pliku. , : Obsługując każdego klienta, musimy zapamiętać naszą pozycję w tym; pliku (pole s y s f t e l l w strukturze C l i e n t ) . Jest to nam niezbędne, ponie-1 waż, gdy procesowi potomnemu nie uda się wybrać zdalnego systemu, chce-, my powrócić do miejsca, które opuściliśmy w pliku Systems (dla tego klien-j ta) i próbować znaleźć kolejny wpis dla tego samego systemu zdalnego. Bieżącą pozycję pobieramy, wywołując funkcję standardowego wejścia-wyj-"4 ścia, f t e l l , a ustalamy ją za pomocą funkcji fseek. Program 18.8 zawiera funkcje służące do czytania pliku Devices.

18. Program obsługujący modem Itinclude

itatic FILE *fpdev = NULL; >tatic int devlineno; /* dla komunikatów o błędzie */ itatic char devline[MAXLINE]; /* nie może być automatycznie; dev_next() przekazuje wskaźnik do tego miejsca */ i* Funkcja czyta wiersz z pliku Devices i wydziela w nim elementy. */ /* wypełnienie wskaźników w strukturze */

if (fpdev == NULL) { if ( (fpdev = fopen(DEVICES, "r")) == NULL) log_sys("can't open %s", DEVICES); devlineno = 0;

if ( (devptr->type = strtok(devline, WHITE)) == NULL) { if (devline[0] == '\n') goto again; /* ignorujemy pusty wiersz */ log_quit("missing 'type' in Devices file, linę %d", devlineno); if (devptr->type[0] == '#') goto again; /* ignorujemy wiersz komentarza

*/

if ( (devptr->line = strtok(NULL, WHITE)) == NULL) log_quit("missing 'linę' in Devices file, linę %d", devlineno); if ( (devptr->line2 = strtok(NULL, WHITE)) == NULL) log_quit("missing 'Iine2' in Devices file, linę %d", devlineno); if ( (devptr->class = strtok(NULL, WHITE)) == NULL) log_quit("missing 'class1 in Devices file, linę %d", devlineno); if ( (devptr->dialer = strtok(NULL, WHITE)) == NULL) log_quit("missing 'dialer' in Devices file, linę %d", devlineno);

sv_rew(void) if

(fpdev != NULL) rewind(fpdev); devlineno = 0;

int dev_find(Devices *devptr, const Systems *sysptr) { dev_rew(); while (dev_next(devptr) >= 0) { if (strcmp(sysptr->type, devptr->type) == 0 ss strcmp(sysptr->class, devptr->class) == 0) return (0); /* znaleziono urządzenie dopasowane */ } s p r i n t f ( e r r m s g , "device '%s'/'%s' not found\n", sysptr->type, s y s p t r - > c l a s s ) ; return(-1); Próg. 18.8 Funkcje czytające plik Devices

gain: if (fgets(devline, MAXLINE, fpdev) == NULL) return(-1); /* EOF */ devlineno++;

return(0);

69

/* Funkcja wyszukuje dopasowane wartości type i class */

"calld.h"

.nt lev_next(Devices *devptr)

18.6. Kod źródłowy serwera

Zobaczymy, że funkcja r e ą u e s t wywohije funkcję dev_f ind, aby zlokali zować wpis o polach type oraz class zgodnych z tymi samymi polami w plik Systems. Program 18.9 zawiera funkcje do odczytu pliku D i a l e r s . #include

"calld.h"

static FILE *fpdial = NULL; static int diallineno; /* dla komunikatów o błędzie */ static char dialline[MAXLINE]; /* nie może być automatycznie; sys_next() przekazuje wskaźnik do tego miejsca */ /* Funkcja czyta wiersz z pliku Dialers i wydziela w nim elementy. */ int dial_next(Dialers *dialptr) /* wypełnienie wskaźników w strukturze */ { if (fpdial == NULL) ( if ( (fpdial = fopen(DIALERS, "r")) == NULL) log_sys("can't open %s", DIALERS); diallineno = 0;

again: j if (fgets(dialline, MAXLINE, fpdial) == NULL) return (-1); /* EOF */ diallineno++; i if ( (dialptr->dialer = strtok(dialline, WHITE)) == NULL) { \ if (dialline [0] == '\n') -A goto again; /* ignorujemy pusty wiersz */ ; log_quit("missing 'dialer' in Dialers file, linę %d", diallineno);

18. Program obsługujący modem

18.6. Kod źródłowy serwera

if (dialptr->dialer[O] == '#•) goto again; /* ignorujemy wiersz komentarza */

#include

if ( (dialptr->sub = strtok(NULL, WHITE)) == NULL) log_quit("missing 'sub' in Dialers file, linę %d", diallineno);

int

if ( (dialptr->expsend = strtok(NULL, "\n")) == NULL) log_quit("missing 'expsend' in Dialers file, linę %d", diallineno); return(0); Ld il_rew (void) if (fpdial != NULL) rewind(fpdial); diallineno = 0; Funkcja znajduje odpowiednią wartość dialer. */ il_find(Dialers *dialptr, const Devices *devptr) dial_rew(); while (dial_next(dialptr) >= 0) { if (strcmp(dialptr->dialer, devptr->dialer) == 0) return(O); /* znaleziono dopasowanie dialer */ }

s p r i n t f ( e r r m s g , " d i a l e r '%s' not found\n", d i a l p t r - > d i a l e r ) ; return(-1);

Próg. 18.9 Funkcje czytające plik D i a l e r s

Jak się okaże, funkcja c h i l d _ d i a l wywołuje funkcję d i a l _ f ind, aby wyszukać wpis o konkretnym polu dialer odpowiadającym wartości dla danego urządzenia. Zauważmy, że zgodnie z tab. 18.4 pliki Systems oraz Devices są obsługiwane przez proces macierzysty, a plik D i a l e r s przez proces potomny. Był to jeden z naszych celów projektowych - proces macierzysty wyszukuje wolne urządzenie i generuje proces potomny, który ma wybrać to urządzenie. Przyjrzyjmy się funkcji r e ą u e s t w próg. 18.10. Była ona wywoływana przez funkcję loop w celu znalezienia wolnego urządzenia związanego z określonym systemem zdalnym. Funkcja ta analizuje pliki Systems oraz Devices. Po dopasowaniu wartości generuje proces potomny. Klient oprócz nazwy zdalnego systemu może określić szybkość linii. Na przykład dla pliku Systems pokazanego w tab. 18.1, po odebraniu żądania klienta o postaci cali -s 9600 hostl

zignorujemy pozostałe dwa wpisy dla nazwy h o s t l (tab. 18.1).

69
sysftell); while ( (cliptr->sysftell = sys_next(ssystems)) >= 0) { if (strcmp(cliptr->sysname, systems.name) == 0) { /* dopasowana wartość system */ /* jeśli klient podał szybkość, to i ta wartość musi być dopasowana */ if (cliptr->speed[0] != 0 && strcmp(cliptr->speed, systems.class) != 0) continue; /* nie można dopasować szybkości */ DEBUG("trying sys: %s, %s, %s, %s", systems.name, systems.type, systems.class, systems.phone); cliptr->foundone++; if (dev_find(sdevices, ssystems) < 0) break; DEBUG("trying dev: %s, %s, %s, %s", devices.type, devices.linę, devices.class, devices.dialer); if ( (pid = is_locked(devices.linę)) != 0) { sprintf(errmsg, "device '%s' already locked by pid %d\n", devices.linę, pid); continue; /* szukamy innego zapisu w pliku Systems */ /* Znaleźliśmy urządzenie, które nie jest zaryglowane. Wywołujemy fork() , by utworzyć potomka, który wybierze numer za pomocą modemu. */ TELL_WAIT(); if ( (cliptr->pid = fork()) < 0) log_sys("fork error"); else if (cliptr->pid == 0) { /* proces potomny */ WAIT_PARENT(); /* proces macierzysty ustanowi rygiel */ child_dial(cliptr); /* nigdy nie powraca */ } /* proces macierzysty */ lock_set(devices.line, cliptr->pid); /* potomek może wznowić pracę, gdy jest już ustalony rygiel */ TELL_CHILD(cliptr->pid); return(0); /* rozpoczęliśmy pracę potomka */

18. Program obsługujący modem /* doszliśmy do znaku EOF w pliku Systems */ if (cliptr->foundone == 0) sprintf(errmsg, "system '%s' not found\n", cliptr->sysname); else if (errmsg[0] == 0) sprintf(errmsg, "unable to connect to system '%s'\n", cliptr->sysname); return(-l); /* również pole c l i p t r - > s y s f t e l l j e s t równe -1 */ Próg. 18.10 Funkcja r e ą u e s t Zwróćmy uwagę, że nie możemy zarejestrować rygla urządzenia za pomocą funkcji l o c k _ s e t , dopóki nie poznamy identyfikatora procesu klienta (czyli możemy to zrobić dopiero po wywołaniu funkcji fork), ale jednocześnie musimy sprawdzić, czy urządzenie nie jest zaryglowane przed wywołaniem fork. Ponieważ nie chcemy, by potomek rozpoczął pracę, zanim ustanowimy rygiel, więc stosujemy wywołania funkcji TELL_WAIT (próg. 10.17), aby zsynchronizować pracę procesów macierzystego i potomnego. Warto zauważyć, że chociaż sprawdzenie zmiennej i s _ l o c k e d oraz faktyczne zaryglowanie przez funkcję s e t l o c k to dwie oddzielne operacje (czyli niejedna nclude nclude

"calld.h"

Procedura obsługi sygnału SIGCHLD; wywoływana, gdy potomek kończy pracę. */

id g_chld(int int pid_t

signo) stat, pid;

errno_save;

errno_save = errno; /* log_msg() może zmienić wartość errno */ chld_flag = 1; if ( (pid = waitpid(-l, s s t a t , 0)) fd (by odesłać klientowi dane wyjściowe typu DEBUG() oraz fd), cliptr->Debug (by wyprowadzić dane typu DEBUG()), cldptr->parity, systems, devices, dialers. */ iild_dial(Client * cliptr) int fd, n; Debug = cliptr -> Debug; DEBUG("child, pid %d", getpid()); if (strcmp(devices.dialer, "direct") == 0) {

/* bezpośrednia linia terminalowa */ fd = tty_open(systems.class, devices.linę, cliptr->parity, 0); if (fd < 0) goto die; } else { /* w przeciwnym razie zakładamy, że trzeba wybrać numer za pomocą modemu */ if (dial_find(sdialers, &devices) < 0) goto die; fd = tty_open(systems.class, devices.linę, cliptr->parity, 1); if (fd < 0) goto die; if (tty_dial(fd, systems.phone, dialers.dialer, dialers.sub, dialers.expsend) < 0) goto die;

DEBUG("done"); /* odsyłamy klientowi otwarty deskryptor */ if (send_fd(cliptr->fd, fd) < 0) log_sys("send_fd error"); exit(0); /* dowie się o tym proces macierzysty */ /* Potomek nie może wywołać send_err(), gdyż w ten sposób zostałby wysłany do klienta końcowy 2-bajtowy protokół. Odsyłamy do klienta tylko nasz komunikat o błędzie. Jeśli proces macierzysty ostatecznie zrezygnuje z wybierania numeru, to wywoła funkcję send_err(). */ n = strlen(errmsg); if (writen(cliptr->fd, errmsg, n) != n) /* wysyłamy do klienta błąd */ log_sys("writen error"); exit(l); /* proces macierzysty dowie się o tym, zwolni rygiel i ponowi próbę */

Próg. 18.12 Funkcja child_dial

70:

18.6. Kod źródłowy serwera

niku wywołania f ork, a więc potomek może przez nie przesyłać bezpośred nio do klienta deskryptor pliku lub komunikat awaryjny. Klient może przesłać serwerowi w swoim poleceniu opcję -d, któr; ustawia dla konkretnego klienta zmienną Debug. Ten sygnalizator jest uży wany w próg. 18.13 przez dwie funkcje DEBUG oraz DEBUG_NONL, aby odsy łać klientowi dane uzyskane w czasie diagnozowania pracy. Takie informacji są przydatne, gdy pojawiają się problemy przy wybieraniu numeru w kon kretnym systemie. Te dwie funkcje są wywoływane głównie przez potomka ale proces macierzysty może ich używać w funkcji r e ą u e s t (próg. 18.10). #include #include

"calld.h"

/* Wszystkie wyprowadzane dane diagnostyczne są przekazywane do klienta. * void DEBUG(char *fmt,

/* diagnostyczne dane wyjściowe, znak nowego wiersza na końcu */

va_list args; char linę[MAXLINE]; int n; if (Debug == 0) return; va_start (args, fint) ; vsprintf (linę, fint, args); strcatfline, "\n"); va_end(args); n = strlen(linę); if (writen(clifd, linę, n) != n) log_sys("writen error"); void DEBUG NONL(char *fmt,

/* diagnostyczne dane wyjściowe, bez znaku nowego wiersza na końcu */

va_list args; char line[MAXLINE]; int n; if (Debug == 0) return; va_start (args, fint); vsprintf (linę, fint, args); va_end(args); n = strlen(linę); if (writen(clifd, linę, n) != n) log_sys("writen error");

Próg. 18.13 Funkcje diagnozujące

04

18. Program obsługujący modem Program 18.14 zawiera funkcję t t y o p e n . Jest ona wywoływana dla modemów oraz urządzeń dołączonych bezpośrednio, by otworzyć terminal i ustawić tryby jego pracy. Pole class w plikach Systems oraz Devices podaje szybkość linii, a klient może określić sposób kontroli parzystości. Otwieramy urządzenie terminalowe z sygnalizatorem braku blokowania, gdyż zdarza się, że otwarcie terminalu połączonego z modemem nie powraca, dopóki nie pojawi się sygnał nośnej modemu. Ponieważ dzwonimy na zewnątrz, a nie do siebie, nie chcemy czekać. Na końcu funkcji wywołujemy funkcję c l r fl, aby wyzerować tryb bez blokowania. Jedyną różnicą w funkcji t t y o p e n między modemem a bezpośrednim połączeniem jest ustawienie bitu CLOCAL w przypadku linii bezpośredniej.

include include include

"calld.h"

* Funkcja otwiera linię terminalu */ nt ty_open(char *class, char *line, enum parity parity, int modem) int fd, baud; char devname[100]; struct termios term; /* pierwsze otwarcie urządzenia */ strcpy(devname, "/dev/"); strcat(devname, linę); if ( (fd = open(devname, O_RDWR | O_NONBLOCK)) < 0) { sprintf(errmsg, "can't open %s: %s\n", devname, strerror(errno)); return(-1); if (isatty(fd) == 0) { sprintf(errmsg, "%s is not a tty\n", devname); return(-1); /* pobranie i ustalenie stanu terminalu modemowego */ if (tcgetattr(fd, sterm) < 0) log_sys("tcgetattr error"); if (parity == NONE) term.c_cflag = CS8; else if (parity == EVEN) term.c_cflag = CS7 | PARENB; else if (parity == ODD) term.c_cflag = CS7 | PARENB I PARODD; else log_quit("unknown p a r i t y " ) ;

18.6. Kod źródłowy serwera

70

term.c_cflag |= CREAD |

/* zezwalamy odbiór */

HUPCL; /* zerujemy linie modemu przy ostatnim zamknięciu •/ /* 1 bit stopu (bo CSTOPB wyłączony) */

if (modem == 0) term.c_cflag |= CLOCAL; term.c_oflag

/* ignorujemy linie stanu modemu */

= 0;

/* wyłączamy jakiekolwiek przetwarzanie danych wyjściowych */ term.c_iflag = IXON | IXOFF | /* sterowanie przepływem Xon/Xoff (domyślne) */ IGNBRK /* ignorujemy przerwania */ ISTRIP I* obcinamy dane wejściowe do 7 bitów */ IGNPAR; I* ignorujemy błędy parzystości danych wejściowych */ term.c_lflag = 0; wyłączamy wszystko w lokalnym sygnalizatorze: zabroniony tryb kanoniczny, zabronione generowanie sygnału, wyłączone echo */ term.c_cc[VMINJ = 1; /* po jednym bajcie, bez licznika czasu */ term.c_cc[VTIME] = 0; /* zob. tab. 18.5 */ if (strcmp(class, "38400") == 0) baud B38400; else if (strcmp(class, "19200") == 0) baud B192 00; else if (strcmp(class, "9600") == 0) baud B9600; else if (strcmp(class, "4800") == 0) baud B4800; else if (strcmp(class, "2400") == 0) baud B2400; else if (strcmp(class, "1800") == 0) baud B1800; else if (strcmp(class, "1200") == 0) baud B1200; else if (strcmp(class, "600") == 0) baud B600; else if (strcmp(class, "300") == 0) baud B300; else if (strcmp(class, "200") == 0) baud B200; else if (strcmp(class, "150") == 0) baud B150; else if (strcmp(class, "134") == 0) baud B134; baud else if (strcmp(class, "110") == 0) B110; baud •= 0 ) else if (strcmp(class, "75' B75; (strcmp(class, "50") == 0) baud else if (strcmp(class, B50; else { sprintf(errmsg, "invalid baud ratę: %s\n" , class); return(-1); cfsetispeedUterm, baud) ; cfsetospeedf&term, baud); if (tcsetattr(fd, TCSANOW, sterm) < 0) log_sys("tcsetattr error"); DEBUG("tty open"); clr_fl(fd, O_NONBLOCK); return(fd);

/* ustalamy atrybuty */

/* wyłączamy brak blokowania */

Próg. 18.14 Funkcja tty open

18. Program obsługujący modem Szczegóły związane z wybieraniem numeru za pomocą modemu są zawarte w funkcji t t y d i a l (próg. 18.15). Wywołujemy ją tylko dla linii modemowych. #include

"calld.h"

int tty_dial(int fd, char *phone, char *dialer, char *sub, char *expsend) char

*ptr;

ptr = strtok(expsend, WHITE); /* najpierw oczekujemy na napis */ for ( ; ; ) { DEBUG_NONL("expect = %s\nread: ", ptr); if (expect_str(fd, ptr) < 0) return(-1); if ( (ptr = strtok(NULL, WHITE)) == NULL) return(O); /* koniec oczekiwania/wysyłania */ DEBUG_NONL("send = %s\nwrite: ", ptr); if (send_str(fd, ptr, phone, 0) < 0) return(-1); if ( (ptr = strtok(NULL, WHITE)) == NULL) return(O); /* koniec oczekiwania/wysyłania */

Próg. 18.15 Funkcj a 11 y_d i a 1

Ta funkcja wywołuje jedną funkcję, aby obsłużyć oczekiwany napis, oraz drugą, by obsłużyć wysyłany napis. Gdy nie ma więcej napisów do wysłania i do oczekiwania, wówczas wszystko jest już gotowe. (Zauważmy, że nie zajmujemy się napisem sub z tab. 18.3). Program 18.16 zawiera funkcję s e n d s t r , która wyprowadza wysyłane napisy. Aby nasz przykład nie był zbyt duży, nie zaimplementowaliśmy wszystkich sekwencji specjalnych, lecz tylko te, które wystarczają do korzystania z programu dla pliku D i a l e r s z tab. 18.3. Funkcja s e n d s t r wywołuje funkcję c t l _ s t r , aby przekonwertować znaki sterujące ASCII do postaci nadającej się do wydruku. Funkcję c t l _ s t r zawiera próg. 18.17. Najtrudniejszym fragmentem wybierania numeru za pomocą modemu jest rozpoznawanie oczekiwanych napisów. Realizuje to funkcja e x p e c t _ s t r z próg. 18.18. (Tak samo jak przy wysyłaniu napisów zaimplementowaliśmy tylko wybrany podzbiór wszystkich możliwych właściwości dostarczanych przez plik Dialers).

18.6. Kod źródłowy serwera iinclude "calld.h" int send_str(int fd, char *ptr, char *phone, int echocheck) { char c, tempc;

/* analizujemy przesłany napis, konwertujemy na bieżąco sekwencj specjalne */ while ( (c = *ptr++) != 0) { if (c == '\\') { if (*ptr == 0) { sprintf(errmsg, "backslash at end of send string\n"); return(-1); } c = *ptr++; /* kolejny znak po znaku backslash */ switch (c) { case 'c': /* bez CR, jeśli koniec napisu */ if (*ptr == 0) goto returnok; continue; /* ignorujemy, jeśli nie koniec napisu */ case 'd 1 : /* opóźnienie 2 sekundy */ DEBUG_NONL(""); sleep(2); continue; case 'p 1 : /* pauza 0.25 sekundy */ DEBUG_NONL(""); sleep_us(250000); /* ćwiczenie 12.6 */ continue; case 'e': DEBUG_NONL(""); echocheck = 0; continue; case 'E': DEBUG_NONL(""); echocheck = 1; continue;

case 'T': /* wyprowadzany numer telefonu */ send_str(fd, phone, phone, echocheck); /* rekurencyjne * continue; I case 'r' : c = '\r'; break; case ' s ' : ~* c = ' '; -\ break; /* miejsce na kolejne warunki case ... */ default: sprintf("errmsg, unknown send escape char: \\%s\n", ctl str(c)); return(-1);

1

!

18. Program obsługujący modem DEBUG_NONL("% s", Ctl_st r(c) ) ; if (write(fd, &c, 1) != 1) log_sys("write error"); if (echocheck) { /* czekamy na echo znaku */ do { if (readffd, stempc, 1) != 1) log_sys("read error"); DEBUG_NONL("{%s}", ctl_str(tempc)); } while (tempc != c ) ;

18.6. Kod źródłowy serwera linclude

"calld.h"

#define EXPALRM static int static void

/* gdy na końcu napisu nie ma znaku \c, wówczas dopisujemy CR */ DEBUG_NONL("%s", ctl_str(c)); if (write(fd, &c, 1) != 1) log_sys("write error"); eturnok: DEBUG(""); return(O);

Próg. 18.16 Funkcja s e n d _ s t r

include

"calld.h"

* Funkcja tworzy napis nadający się do wydruku ze znaku umieszczonego * w zmiennej c, który może być znakiem sterującym. Działa tylko dla kodu * ASCII. */ har * tl_str(char c) static char tempstr[6];

/* największy to "\177" + pusty znak */

c &= 255; if (c == 0) /* nie powinniśmy widzieć pustego znaku */ return("\\0"); else if (c < 040) sprintf (tempstr, %c", c + 'A' - 1); else if (c == 0177) return("DEL"); else if (c > 0177) sprintf(tempstr, "\\%03o", c) ; else sprintf(tempstr, "%c", c) ; return(tempstr);

Próg. 18.17 Funkcja ctl_str

/* czas alarmowy na odczyt oczekiwanego napisu */

expalarm = EXPALRM; sig_alrm(int);

static volatile sig_atomic_t caught_alrm; static size_t

c = '\r';

45

exp_read(int, char * ) ;

int /* przekazuje 0, jeśli otrzymano napis; -1, jeśli nie */ expect_str(int fd, char *ptr) { char expstr[MAXLINE], inbuf[MAXLINE]; char c, * s r c , *dst, * i n p t r , *cmpptr; int i, matchlen; if (strcmp(ptr, "\"\"") == 0) goto returnok; /* specjalny przypadek "" (oczekuj na pusty napis) *, /* tworzymy kopię oczekiwanego napisu, konwertujemy sekwencje specjalne */ for (src = ptr, dst = expstr; (c = *src++) != 0; ) { if (c == • W ) { if (*src == 0) { sprintf(errmsg, "invalid expect string: %s\n", ptr); return(-1); } c = *src++; /* kolejny znak po znaku backslash */ switch (c) { case 'r': c = '\r'; break; '; break; case 's': c = /* miejsce dla kolejnych instrukcji case .. default: sprintf(errmsg, "unknown expect escape char: \\%s\n", ctl_str(c)); return(-1); } } *dst++ = c; } *dst = 0 ; matchlen = strlen(expstr); ~* if (signal(SIGALRM, sig_alrm) == SIG_ERR) log_quit("signal error"); caught_alrm = 0 ; alarm(expalarm); do { if (exp_read(fd, &c) < 0) return(-1);

18. Program obsługujący modem } while (c != e x p s t r [ 0 J ) ;

/* omijamy aż do pierwszego takiego samego znaku */

cmpptr = i n p t r = inbuf; *inptr = c; for (i = 1; i < matchlen; i++) inptr++; if (exp_read(fd, return(-1);

{

/* czytamy matchlen znaków */

inptr) < 0)

for ( ; ; ) { /* czytamy, dopóki nie dopasujemy całego napisu */ if (strncmp(cmpptr, expstr, matchlen) == 0) break; /* dopasowany */ inptr++; if (exp_read(fd, inptr) < 0) return(-1); cmpptr++; urnok: alarm(O); DEBUG("\nexpect: got i t " ) ; return(0);

et

/* czytamy jeden bajt, obsługujemy błąd czasu oczekiwania i realizujemy DEBUG */ _read(int fd, char *buf) if (caught_alrm) {

/* sprawdzamy sygnalizator, zanim zablokujemy się w odczycie */ DEBUG("\nread timeout"); return(-1);

} if (read(fd, buf, 1) = = 1 ) { DEBUG_NONL("%s", ctl_str(*buf)); return(1); } if (errno == EINTR && caught_alrm) { DEBUG("\nread timeout"); return(-1); } log_sys("read error"); tic void _alrm(int signo) caught_alrm = 1; return;

Próg. 18.18 Funkcje czytające i interpretujące oczekiwany napis

18.7. Projekt klienta

711

Najpierw tworzymy kopię oczekiwanego napisu, konwertując znaki specjalne. Nasza technika dopasowywania polega na odczytywaniu znaków z modemu, dopóki nie otrzymamy takiego samego znaku jak pierwszy znak oczekiwanego napisu. Następnie czytamy tyle znaków, ile liczy oczekiwany napis. Od tej chwili czytamy znaki z modemu do bufora, porównując jego zawartość z oczekiwanym napisem, dopóki nie znajdziemy zgodności lub dopóki nie pojawi się alarm. (Istnieją lepsze algorytmy dopasowywania oczekiwanego napisu - wybraliśmy taki, aby uprościć kod źródłowy. Liczba znaków przekazywanych przez modem, która jest porównywana z oczekiwanym napisem, jest zazwyczaj rzędu 50, a oczekiwany napis ma na ogół od 10 do 20 znaków). Zauważmy, że za każdym razem, gdy dopasowujemy oczekiwany napis, musimy ustawiać alarm, ponieważ tylko w ten sposób jesteśmy w stanie dowiedzieć się, że nie odebraliśmy tego, na co oczekujemy. Kończy to opis demona serwera. Jego działania polegają na otwarciu urządzenia terminalowego i wybraniu modemu. Od klienta zależy, co stanie się z urządzeniem terminalowym po jego otwarciu. Zajmiemy się teraz klientem, który oferuje interfejs podobny do programów cu i t i p i daje możliwość wybrania zdalnego systemu i załogowania się w nim.

18.7 Projekt klienta Interfejs między klientem a serwerem ma kilkanaście wierszy kodu. Klient tworzy polecenie, przesyła je do serwera i odbiera w odpowiedzi albo deskryptor pliku, albo komunikat awaryjny. Reszta projektu klienta zależy od tego, co klient chce zrobić z otrzymanym deskryptorem. W tym podrozdziale opiszemy projekt klienta o nazwie c a l i , który działa podobnie jak znane programy cu oraz t i p . Używając tego programu, możemy wywołać zdalny system i załogować się w nim. Nie musi to być koniecznie system uniksowy, nasz program umożliwia komunikację z dowolnym systemem lub urządzeniem połączonym ze stacją komputerową linią szeregową RS-232. Dyscyplina linii terminalowej

{

Na rysunkach 12.7 i 12.8 pokazaliśmy zasady działania programu obsługującego wybieranie numeru za pomocą modemu. Rysunek 18.4 jest rozszerzeniem rysunku 12.7, przy uwzględnieniu faltu, że linia terminalowa między użytkownikiem a modemem może pracować w dwóch trybach, oraz założę-" niu, że używamy programu do wybierania numeru zdalnej uniksowej stacji, komputerowej. (Przypominamy, że zgodnie z wydrukiem z próg. 12.10 dlaj systemów terminalowych opartych na podejściu strumieniowym rys. 18.4 jest uproszczeniem. Może istnieć wiele strumieniowych modułów związanych z dyscypliną linii oraz wiele modułów tworzących procedurę obsługi urządzenia. Nie pokazujemy też jawnie czoła strumienia).

18. Program obsługujący modem proces c a l i

cierzysty i potomny, jak pokazuje rys. 12.8. Na rysunku 18.5 przedstawiamy tylko te dwa procesy oraz umieszczone poniżej moduły dyscypliny linii. Tradycyjnie programy cu oraz t i p używały zawsze dwóch procesów, tak jak widzimy na rys. 18.5. Było tak, ponieważ starsze systemy uniksowe nie stosowały zwielokrotniania wejścia-wyjścia.

powłoka i

ut, stderr

' stdin

dyscyplina linii terminalu (tryb kanoniczny)

i dyscyplina linii ' dyscyplina linii | terminalu i terminalu (tryb niekanoniczny) i (tryb niekanoniczny) j jądro systemu

i

r

713

18.7. Projekt klienta

proces macierzysty cali

jądro systemu

proces potomny cali

program obsługi terminalu

i program obsługi i ' terminalu ! i i i

lokalny system modem

zdalny system modem

Rys. 18.4 Proces wybierania numeru za pomocą modemu w celu załogowania na zdalnej stacji uniksowej

Dwa elementy umieszczone w lokalnym systemie powyżej modemu i zaznaczone na rys. 18.4 prostokątem o przerywanych liniach zostały ustanowione przez wywołanie w programie serwera funkcji tty_open (próg. 18.14). Ta funkcja sprawia, że zaznaczony linią przerywaną moduł dyscypliny linii pracuje w trybie niekanonicznym (czyli surowym). W funkcji t t y d i a l serwera został wybrany numer za pomocą modemu w lokalnym systemie (próg. 18.15). Dwie strzałki między prostokątem narysowanym przerywaną linią a procesem c a l i wiążą się z deskryptorem pliku przekazywanym przez serwer. (Pokazujemy pojedynczy deskryptor jako dwie strzałki, aby podkreślić, że jest to deskryptor dwukierunkowy). Proces logowania w systemie ustala, że element związany z dyscypliną linii umieszczony poniżej pracuje w trybie kanonicznym. Chcemy, by po wybraniu numeru za pomocą modemu moduł dyscypliny linii zdalnej stacji rozpoznawał specjalne terminalowe znaki wejściowe (koniec pliku, skasowanie wiersza itp., opisane w podrozdz. 11.3). Oznacza to, że musimy ustalić niekanoniczny tryb pracy modułu umieszczonego powyżej terminalu (standardowe wejście, standardowe wyjście oraz standardowy strumień komunikatów awaryjnych procesu c a l i ) .

den proces czy dwa? Na rysunku 18.4 pokazaliśmy c a l i jako jeden proces. W tej sytuacji system musi dostarczać funkcje do obsługi zwielokrotnionego wejścia-wyjścia, jak np. s e l e c t czy p o l l , ponieważ oba deskryptory mogą być odczytywane i zapisywane. Możemy również zaprojektować klienta jako dwa procesy, ma-

dyscyplina linii terminalu (tryb niekanoniczny) Rys. 18.5

Klient c a l i działający jako dwa procesy

Poniżej podane motywy sprawiły, że zdecydowaliśmy się na używanie jednego procesu. 1. Dysponowanie dwoma procesami komplikuje zakończenie pracy klienta. Potomek rozpoznaje sekwencję ~. (znak tyldy, a następnie kropka) na początku wiersza, która służy do zerwania połączenia i zakończenia pracy. Proces macierzysty musi wówczas przechwycić sygnał SIGCHLD, aby również zakończyć pracę. Jeśli połączenie zostanie zamknięte albo przez zdalny system, albo z powodu zerwania łączności na tej linii, to proces macierzysty dowie się o tym, odczytując znak końca pliku z deskryptora związanego z modemem. Następnie proces macierzysty musi powiadomić swojego potomka, aby ten również zakończył pracę. Używając pojedynczego procesu nie musimy realizować wzajemnego powiadamiania o zakończeniu pracy. 2. Zamierzamy zaimplementować po stronie klienta funkcję obsługująca przesyłanie pliku, wzorowaną na poleceniach put i t a k e w progran mach cu i t i p . Wprowadzamy polecenia za pomocą standardowego wejścia w wierszu rozpoczynającym się znakiem tyldy (domyśln> znak poprzedzający znaki specjalne). Jeżeli używamy dwóch proce^ sów, to te polecenia są rozpoznawane przez potomka (rys. 18.5). Jednak plik odbierany przez klienta w odpowiedzi na polecenie t a k e jesJ przekazywany przez deskryptor modemu, z którego czyta proces ma' cierzysty. Oznacza to, że w celu zaimplementowania polecenia pa* brania danych potomek musi powiadomić swój proces macierzysty: by ten wstrzymał odczytywanie danych z modemu. Proces macierzy-

18. Program obsługujący modem

sty jest wówczas prawdopodobnie zablokowany w funkcji r e a d z tego deskryptora, a więc jest potrzebny sygnał, który przerwie jej realizację. Gdy potomek zakończy swoje czynności, wówczas jest potrzebne kolejne powiadomienie, aby proces macierzysty mógł wznowić odczyt z modemu. Mimo że taki scenariusz jest możliwy, szybko doprowadza do bardzo zagmatwanych zależności. Pojedynczy proces ułatwia implementację klienta. Jednak decydując się na takie podejście, tracimy możliwość zatrzymania tylko procesu potomnego przez podsystem sterowania zadaniami. Berkelejowski program t i p ma taką właściwość. Dzięki niej można zatrzymać potomka, podczas gdy proces macierzysty kontynuuje pracę. Oznacza to, że wszystkie wprowadzane z terminalu dane są kierowane do naszej powłoki, a nie do potomka, a więc możemy pracować w lokalnym systemie, chociaż widzimy dane wyprowadzane na nasz terminal przez zdalny system. Jest to wygodne rozwiązanie, gdy w zdalnym systemie uruchamiamy zadanie trwające bardzo długo i chcemy widzieć dane wyjściowe generowane przez zdalny system, a jednocześnie pracować w lokalnym systemie. Teraz przyjrzymy się kodowi źródłowemu, implementującemu klienta.

715

18.8. Kod źródłowy klienta #include iinclude #include #include #include



"ourhdr.h"

#define CS_CALL "/home/stevens/calld" /* dobrze znana nazwa serwera */ #define CL_CALL "cali" /* polecenie dla serwera */ /* deklaracja zmiennych globalnych */ extern char escapec; /* znak tyldy wskazuje lokalne polecenia */ extern char *src; /* dla poleceń take i put */ extern char *dst; /* dla poleceń take i put */ /* prototypy funkcji */ int calllconst char * ) ; int doescape(int); void loop(int); int prompt_read(char *, int (*)(int, char * * ) ) ; void put(int); void take(int); int take_put_args(int, char * * ) ; Próg. 18.19 Plik nagłówkowy cali. h

.8 Kod źródłowy klienta Program klienta jest dużo krótszy od programu serwera, ponieważ klient nie obsługuje wszystkich szczegółów związanych z połączeniem ze zdalnym systemem - wszystko to robi serwer, pokazany w podrozdz. 18.6. Ponad połowa kodu źródłowego klienta służy do obsługi poleceń takich jak pobranie czy przekazanie. Program 18.19 zawiera plik nagłówkowy c a l l . h , który jest dołączany do wszystkich plików źródłowych. Polecenia serwera oraz ogólnie znana nazwa serwera muszą odpowiadać wartościom podanym w próg. 18.1. Program 18.20 zawiera funkcję main. Funkcja ta przetwarza argumenty wiersza poleceń i zapamiętuje je w tablicy args, która jest przesyłana do serwera. Funkcja c a l i łączy się z serwerem i przekazuje deskryptor pliku do systemu zdalnego. Funkcja tty_raw (próg. 11.10) ustanawia niekanoniczny tryb pracy modułu dyscypliny linii umieszczonego powyżej terminalu (rys. 18.4). Aby przywrócić standardowe parametry terminalu po zakończeniu działań, rejestrujemy funkcję t t y _ a t e x i t jako procedurę obsługi wyjścia. Wywołujemy funkcję loop, aby kopiować wszystko, co wprowadzimy z klawiatury terminalu do modemu oraz wszystkie dane z modemu na ekran terminalu.

#include char char char

"call.h"

/* definicja zmiennych globalnych */ escapec = '~'; *src; *dst;

static void usage(char * ) ; int main(int argc, char *argv[]) { int c, remfd, debug; char args[MAXLINE]; args[0] = 0; /* utworzenie listy argumentów dla serwera połączeń */ opterr = 0; /* funkcja getoptO nie ma zapisywać komunikatów na stderr */ while ( (c = getopt(argc, argv, "des:o")) != EOF) { switch (c) { case 'd': /* diagnostyka */ debug = 1; strcat (args, "-d " ) ; break;

18. Program obsługujący modem case 'e 1 : /* parzystość */ strcat(args, "-e " ) ; break;

iinclude iinclude

case 'o 1 : /* nieparzystość */ strcat(args, "-o " ) ; break; 1

case 's : /* szybkość */ strcat(args, "-s " ) ; strcat(args, optarg); strcat(args, " " ) ; break;

if (optind < argc) strcat(args, argv[optind]); /* nazwa wywoływanej stacji */ else usage("missing to cali"); if ( (remfd = cali(args)) < 0) /* wywołanie */ exit(l); /* cali() drukuje przyczynę niepowodzenia */ printf("Connected\n") ; if (tty_raw(STDIN_FILENO) < 0) err_sys("tty_raw error"); if (atexit(tty_atexit) < 0)

"call.h"

/* tryb surowy terminalu użytkownika */

int call(const char *args) csfd, len; iov[2];

/* tworzymy połączenie do serwera */ if ( (csfd = cli_conn(CS_CALL)) < 0) err_sys("cli_conn e r r o r " ) ; iov[0].iov_base iov[0].iov_len iov[l].iov_base iov[l].iov_len

= CL_CALL " "; = strlen(CL_CALL) + 1; = (char *) args; = strlen(args) + 1; /* zawsze przesyłamy pusty znak na końcu args */ len = iov[0].iov_len + iov[l].iov_len; if (writev(csfd, &iov[0], 2) != len) err_sys("writev e r r o r " ) ; /* czytamy przekazany deskryptor */ /* błędy obsługuje funkcja writef) */ return( recv fd(csfd, write) );

/* odtworzenie stanu terminalu przy wyjściu */

Próg. 18.21 Funkcja c a l i

err_sys("atexit error"); loop(remfd);

/ * i realizacja * /

printf("Disconnected\n\r"); exit (0);

Funkcja loop obsługuje zwielokrotnianie wejścia-wyjścia między dwoma strumieniami wejściowymi i dwoma strumieniami wyjściowymi. Możemy użyć albo funkcji s e l e c t , albo p o l l , w zależności od możliwości lokalnego systemu. Program 18.22 jest implementacją stosującą funkcję p o l l . #include #include #include

itic void ige(char *msg) err_quit("%s\nusage: c a l i -d -e -o -s ", msg);

Próg. 18.20 Funkcja main

Funkcja c a l i w próg. 18.21 kontaktuje się z serwerem, by otrzymać deskryptor pliku związany z modemem. Jak powiedzieliśmy wcześniej, kod źródłowy potrzebny do połączenia się z serwerem i do odbioru deskryptora pliku zajmuje kilkanaście wierszy.

/* struct iovec */

/* Funkcja realizuje wywołanie: wysyła do serwera wartość "args" * i odczytuje przekazany deskryptor. */

int struct iovec

usage("unrecognized option")

717

18.8. Kod źródłowy klienta

"call.h"

/* Funkcja kopiuje wszystko ze strumienia stdin do deskryptora "remfd" * i wszystko z deskryptora "remfd" do strumienia stdout. */ idefine BUFFSIZE 512 void loop (int remfd) int char struct pollfd

boi, n, nread; c, buff[BUFFSIZE]; fds[2];

18. Program obsługujący modem setbuf(stdout,

NULL);

/* ustalamy brak buforowania dla strumienia stdout * /

/* (potrzebne instrukcje printf w funkcjach take() i put()) */ fds[0].fd = STDIN_FILENO; /* dane wejściowe terminalu */ fds[0].events = POLLIN; fds[l].fd = remfd; /* dane wejściowe ze zdalnego urządzenia (modemu) */ fds[l].events = POLLIN; for ( ; ; ) { if (polKfds, 2, INFTIM) gotowe */ if (fds[1].revents S POLLIN) { /* dane do odczytu ze zdalnego urządzenia */ if ( (nread = read(remfd, buff, BUFFSIZE)) gotowe */

Próg. 18.22 Funkcja loop używająca funkcji poll

18.8. K o d źródłowy klienta

719

Podstawowa pętla tej funkcji służy do oczekiwania na dane nadchodzące albo z terminalu, albo z modemu. Dane odczytywane z terminalu są tylko kopiowane do modemu i odwrotnie. Jedyną komplikacją jest konieczność rozpoznawania znaku specjalnego (tylda) na początku wiersza. Zwróćmy uwagę, że czytamy z terminalu (standardowe wejście) po jednym znaku, natomiast z modemu czytamy, dopóki nie wypełnimy całego bufora. Czytamy dane z terminalu po jednym znaku, aby móc sprawdzać każdy znak i dowiedzieć się, kiedy zaczyna się nowy wiersz wskazujący, że trzeba interpretować polecenia. Operacje wejścia-wyjścia realizowane po jednym znaku wpływają wprawdzie niekorzystnie na potrzebny czas dla procesora (przypominamy tab. 3.1), ale zazwyczaj danych wprowadzanych z terminalu jest znacznie mniej niż danych pochodzących ze zdalnego systemu. (W mierzonych przez autora sesjach zdalnego logowania używających tego programu było około 100 razy więcej znaków, wyprowadzanych przez zdalny system niż znaków wprowadzanych). Po natrafieniu na znak specjalny wywołujemy funkcję doescape, która przetwarza polecenie (próg. 18.23). Implementujemy tylko pięć poleceń. Proste polecenia są realizowane od razu w tej funkcji, a bardziej skomplikowane, jak polecenia pobrania i przekazania, są wykonywane przez oddzielne funkcje (take oraz put).

• Znak kropki kończy pracę klienta. Dla niektórych urządzeń, np. drukarki laserowej, jest to jedyny sposób zakończenia pracy klienta. Gdy jesteśmy zalogowani w zdalnym systemie w sposób zobrazowany na rys. 18.4, to wylogowanie na ogół powoduje zerwanie połączenia telefonicznego przez zdalny modem, w wyniku czego z deskryptora modemu odczytujemy w funkcji loop znak zawieszenia. • Jeśli system ma wbudowane sterowanie zadaniami, to rozpoznajemy znak zawieszenia zadania i wstrzymujemy pracę klienta. Zauważmy, że samodzielne rozpoznanie tego znaku i zatrzymanie pracy klienta jest dużo łatwiejsze niż realizowanie tego przez moduł dyscypliny linii, który po rozpoznaniu znaku zawieszenia musiałby wygenerować sygnał SIGSTOP (zob. próg. 10.22). Przywracamy oryginalny tryb pracy terminalu, zanim wstrzymamy pracę, i ponownie go odtwarzamy przed wznowieniem pracy. • Znak # generuje warunek BREAK dla deskryptora modemu. Używamy do tego posiksowej funkcjT t c s e n d b r e a k (podrozdz. 11.8); Warunek BREAK powoduje przełączenie szybkości linii przez pro-1 gram g e t t y lub ttymon (podrozdz. 9.2). . • Polecenia pobrania i przekazania wymagają wywołań oddzielnych funkcji. Sposobem zapamiętania, jaką rolę odgrywają te dwa polecę nia, jest uświadomienie sobie, jakie operacje realizuje klient w syste^ mie lokalnym: pobiera plik ze zdalnego systemu lub przekazuje plik do zdalnego systemu.

r

20

18. Program obsługujący modem

Unclude Unclude '* * * * *

c;

if (read(STDIN_FILENO, &c, 1) != 1) e r r _ s y s ( " r e a d e r r o r from s t d i n " ) ; if (c == escapec) return(escapec);

/* kolejny znak wejściowy */

/* dwa po sobie -> przetwarzamy jeden */

e l s e if (c = = ' . ' ) { /* kończymy */ write(STDOUT_FILENO, " ~ . \ n \ r " , 4); return(-1); #ifdef VSUSP } else if (c == tty_termios()->c_cc[VSUSP]) { /* zawieszenie klienta */ tty_reset(STDIN_FILENO) ; /* odtwarzamy tryb terminalu */ kill(getpidO, SIGTSTP); /* zawieszamy pracę */

#endif

tty raw(STDIN_FILENO); return(0);

/* i przywracamy tryb surowy terminalu */

} else if (c = = ' # ' ) { /* generujemy przerwanie */ tcsendbreak(remfd, 0 ) ; return(0); } else if (c == 't') { take(remfd); return(0);

/* pobieramy plik ze zdalnej stacji */

} else if (c == 'p': put(remfd); return(0);

/* przekazujemy plik do zdalnej stacji */

return(c);

#include

/* nie jest to znak specjalny */

"call.h"

#define CTRLA 001 static static static static

int doescape(int remfd) char

Program 18.24 zawiera kod źródłowy potrzebny do obsługi poleceń pobrania. Funkcja t a k e najpierw wywołuje funkcję p r o m p t r e a d (pok zaną w próg. 18.25), która wypisuje sekwencję ~[take] w odpowiedzi i odebrane polecenie ~t.

"call.h"

Funkcja jest wywoływana, gdy pierwszym znakiem wiersza jest znak specjalny (tylda). Czyta następny znak i przetwarza go. Przekazuje -1, jeśli następnym znakiem jest znak zakończenia; 0, jeśli następnym znakiem jest poprawny znak polecenia (który został przetworzony) lub zwykły znak danych (nie ma specjalnego znaczenia). */

72

18.8. Kod źródłowy klienta

int char char int

/* wskaźnik końca pliku (eof) dla polecenia take */

rem_read(int); rem_buf[MAXLINE]; rem_ptr; r e m c n t = 0;

/* Funkcja kopiuje p l i k ze zdalnej

l o k a l i z a c j i do l o k a l n e j .

*/

void take (int remfd) int char FILE

n, linecnt; c, cmd[MAXLINE]; * fpout;

if (prompt_read("~[take] ", take_put_args) < 0) { p r i n t f ( " u s a g e : [take] \n\r" return;

/* otwieramy lokalny plik wyjściowy */ if ( (fpout = fopen(dst, "w")) == NULL) { err_ret("can't open %s for writing", dst); putc('\r', stderr); fflush(stderr); return; /* przesyłamy do zdalnej stacji polecenie cat/echo */ sprintf(cmd, "cat %s; echo %c\r", src, CTRLA); n = strlen(cmd); if (write(remfd, cmd, n) != n) err_sys("write error"); /* czytamy ze zdalnej stacji echo poleceń cat/echo */ rem_cnt = 0 ; /* inicjujemy rem_read() */ for ( ; ; ) { if ( (c = rem_read(remfd)) == 0) return; /* rozłączenie linii */ if (c == '\n') break; /* koniec potwierdzanego wiersza */

Próg. 18.23 Funkcja escape /* czytamy ze zdalnej stacji plik */

18. Program obsługujący modem

Zdalna stacja po odebraniu tego polecenia wykonuje program cat, a następnie realizuje echo znaku Control-A w kodzie ASCII. W sekwencji znaków przekazywanych ze zdalnej stacji wyszukujemy znak Control-A, który powiadamia nas o zakończeniu przesyłania pliku. Zwróćmy uwagę, że musimy również przeczytać polecenie przekazane do tej stacji jako potwierdzenie jego odbioru przez zdalną stację. Dopiero po otrzymaniu potwierdzenia odbioru polecenia możemy przystąpić do odbierania danych wyjściowych polecenia cat. Czytając zdalny plik, wyszukujemy znaki nowego wiersza i zliczamy wiersze. Wyświetlamy liczbę na lewym marginesie, zastępując ciągle aktualną wartością (ponieważ napis wyprowadzany za pomocą funkcji p r i n t f kończymy tylko znakiem powrotu karetki, a nie znakiem nowego wiersza). W ten sposób możemy obserwować na terminalu postęp przesyłania pliku, a po zakończeniu dysponujemy licznikiem wierszy. Ten plik źródłowy zawiera również funkcje rem_read, która jest wywoływana w celu odczytania znaków ze zdalnej stacji. Czytamy całymi buforami, ale wysyłamy dane pojedynczymi znakami. Oryginalne polecenie pobrania czytało po jednym znaku, podobnie jak programy cu oraz t i p . Dziesięć lat temu, gdy modemy o szybkości 1200 bodów uważano za szybkie, takie podejście nie stanowiło problemu. Obecne modemy są znacznie szybsze, dostarczają znaki do procedury obsługi urządzenia z szybkością co najmniej 9600 bodów. Znaki mogą w tej sytuacji zaginąć, nawet gdy używamy szybkich procesorów. Autor stwierdził, że jest tak w programach cu i t i p , gdy zastosował modem Telebit T2500 w trybie PEP, nawet gdy obie strony, stacje lokalna i zdalna, stosowały kontrolę przepływu. Podczas przesyłania dużego pliku tekstowego (około 75 000 bajtów) stracono blisko połowę znaków, co wymagało powtórzenia operacji. Rozwiązaniem było wprowadzenie w funkcji rem_read odczytu polegającego na wypełnieniu całego bufora. Takie podejście zmniejsza czas procesora około trzykrotnie (z 16 sekund do 5 sekund, przy przesyłaniu 75 000 bajtów) i zawsze daje niezawodną operację przesłania. W funkcji rem_read dodano tymczasowy licznik, aby sprawdzać, ile znaków przekazuje pojedyncze wywołanie read. W tabeli 18.5 pokazujemy wyniki.

l i n e c n t = 0; for ( ; ; ) { if ( (c = rem_read(remfd)) == 0) break; / * rozłączenie l i n i i * / if (c == CTRLA) break; /* wszystko gotowe */ if

(c ==

'\r')

continue; /* ignorujemy powrót */ if (c == '\n') /* ale zapisujemy do pliku znaki nowych wierszy */ printf("\r%d", ++linecnt); if (putc(c, fpout) == EOF) break; /* błąd wyjścia */ } if (ferror(fpout) II fclose(fpout) == EOF) { err_msg("output error to local file"); putc('\r', stderr); fflush(stderr) ; } c = '\n'; write(remfd, &c, 1); Funkcja czyta dane ze zdalnej stacji. Odbiera MAXLINE bajtów, ale wysyła po jednym znaku. */

n_read(int remfd) if (rem_cnt %s; stty echo\r", dst); n = strlen(cmd); if (write(remfd, cmd, n) != n) err sys("write error"); tcdrain(remfd); /* oczekujemy na przesłanie naszych danych wyjściowych */ sleep(4); /* i dajemy szansę, by zadziałało polecenie stty */ /* przesyłamy plik do zdalnej stacji */ linecnt = 0; for ( ; ; ) { if ( (i = getc(fpin)) == EOF) break; /* wszystko gotowe */ c = i; if (write(remfd, &c, 1) != 1) break; /* zapewne zerwana linia */ if (C == '\n') /* ale znaki nowego wiersza są zapisywane do pliku */ printf("\r%d", ++linecnt); } /* przesyłamy do zdalnej stacji znak EOF, by zakończyć cat */ c = tty_termios()->c_cc[VEOF]; write(remfd, sc, 1); tcdrain(remfd); /* oczekujemy na przesłanie naszych danych wyjściowych */ sleep(2); tcflush(remfd, TCIOFLUSH); /* opróżniamy potwierdzenia poleceń stty/cat/stty */ c = '\n'; write(remfd, &c, 1); if

(ferror(fpin)) { err_msg("read error of local putc('\r', stderr); fflush(stderr);

file");

}

fclose(fpin);

Próg. 18.26 Funkcja put Ostatnią funkcją kliencką jest put (próg. 18.26). Jest ona wywoływana, aby skopiować lokalny plik do zdalnej stacji. Tak samo jak dla pobrania wysyłamy polecenie do zdalnego systemu. Tym razem ma ono postać:

18.9. Podsumowanie

18.9 Podsumowanie W tym rozdziale przeanalizowaliśmy dwa programy: serwer typu demon, który wybiera numer za pomocą modemu, oraz program zdalnego logowania, który korzysta z serwera, aby kontaktować się ze zdalnym systemem dołączonym za pomocą portu terminalowego. Serwera mogą używać również inne programy, chcące kontaktować się ze zdalnymi systemami lub urządzeniami dołączonymi przez asynchroniczne porty terminalowe. Projekt serwera był podobny do serwera otwierającego pliki z podrozdz. 15.6 i wymagał zastosowania łączy strumieniowych, unikatowego połączenia klienta z serwerem oraz przekazywania deskryptorów plików. Zaawansowane metody komunikacji międzyprocesowej umożliwiają tworzenie aplikacji klient-serwer mających wszystkie potrzebne właściwości, zgodnie z opisem w podrozdz. 18.3. Klient jest podobny do programów cu i t i p dostarczanych w wielu systemach uniksowych, ale w naszym przykładzie nie musimy zajmować się obsługiwaniem modemu, kolizjami między plikami ryglowania w podsystemie UUCP, ustalaniem charakterystyk modułu dyscypliny linii itp. Wszystkie te detale obsługuje serwer. Możemy się skoncentrować na konkretnych zadaniach klienta, takich jak dostarczenie niezawodnego mechanizmu przesyłania plików.

Ćwiczenia 18.1

Jak w podrozdz. 18.3 możemy uniknąć kroku 0 (uruchamiając serwer „ręcznie")?

18.2 Co się stanie, jeśli w próg. 18.4 nie nadamy zmiennej optind wartości 1? 18.3

Co się stanie, gdy ktoś zmieni plik Systems między wywołaniem funkcji f ork w funkcji reąuest (próg. 18.10) a zakończeniem przez potomka pracy ze stanem 1?

18.4

W podrozdziale 7.8 powiedzieliśmy, że musimy być bardzo ostrożni, gdy używamy wskaźników w obszarze, który bywa powtórnie alokowany (realloc) j ponieważ obszar ten może zostać przemieszczony w pamięci w wyniku wywołania funkcji realloc. Dlaczego możemy używać w próg. 18.3 wskaźnika c l i p t r , chociaż tablica c l i e n t jest obsługiwana przez funkcję realloc?

18.5

Co stanie się, jeśli jakiś z argumentów nazwy ścieżki w poleceniu pobrania lub; przekazania będzie zawierał średnik?

18.6

Zmodyfikuj program serwera, aby od razu czytał po starcie wszystkie trzy pliki i danych i zapamiętywał je w pamięci. Jak serwer powinien obsługiwać zdarzenie zmiany zawartości tych plików? ^

18.7

Dlaczego w próg. 18.21 przerzutowujemy argument args w czasie wypełniania struktury dla funkcji writev?

s t t y -echo; cat > destfde ; s t t y echo

Musimy wyłączyć echo, w przeciwnym razie otrzymalibyśmy z powrotem cały przesyłany plik. Aby zakończyć polecenie cat, wysyłamy znak końca pliku (na ogół Control-D). Nakłada to dodatkowe wymaganie, by systemy lokalny i zdalny używały tego samego znaku końca pliku. Oprócz tego, plik nie może zawierać znaków ERASE oraz KILL stosowanych przez zdalny system.

727

18. Program obsługujący modem 18.8

Zaimplementuj próg. 18.22, używając funkcji select zamiast funkcji poll.

18.9 Jak możesz upewnić się, że plik wysyłany w wyniku polecenia przekazania nie zawiera znaków, które mogłyby zostać zinterpretowane przez dyscyplinę linii w zdalnym systemie? 18.10 Im wcześniej funkcja wybierająca stwierdzi, że wybranie nie udało się, tym szybciej może przystąpić do przetwarzania następnego wpisu w pliku Systems. Jeśli na przykład przed przekroczeniem terminu ustalonego w zmiennej expect_str stwierdzimy, że zdalny telefon jest zajęty, to możemy zaoszczędzić 15 lub 20 sekund. Aby obsłużyć tego typu błędy, berkelejowski podsystem UUCP pozwala na umieszczanie bezpośrednio przed oczekiwanym napisem napisu ABORT. Po znalezieniu tak zdefiniowanego oczekiwanego napisu obsługujący proces wybierania zostanie przerwany. Na przykład tuż przed ostatnim oczekiwanym napisem CONNECT\SFAST wtab. 18.3 można dodać

19

Pseudoterminale

ABORT BUSY

Zaimplementuj taką właściwość.

19.1 Wprowadzenie W rozdziale 9 zobaczyliśmy, że logowania terminalowe przechodzą przez urządzenie terminalowe, które automatycznie narzuca sposób obsługi. Istnieje ustalona dyscyplina linii (rys. 11.2) między terminalem a uruchamianymi programami, dzięki której możemy określić specjalne znaki terminalowe (wycofanie, skasowanie wiersza, przerwanie itp.). Jednak gdy na połączeniu sieciowym nadchodzi zlecenie załogowania, między połączeniem sieciowym a powłoką logowania nie jest automatycznie ustalana dyscyplina linii terminalu. Z rysunku 9.5 wynikało, że o charakterystyce terminalu decyduje procedura obsługi urządzenia zwanego pseudoterminalem. W tym rozdziale powiemy nie tylko o logowaniu sieciowym, lecz również o innych zastosowaniach pseudoterminali. Zaczniemy od pokazania funkcji, które tworzą pseudoterminale w systemach SVR4 oraz 4.3+BSD, a następnie użyjemy ich do napisania programu, który nazwiemy pty. Pokażemy różne zastosowania tego programu: do rejestrowania wszystkich wprowadzanych i wyprowadzanych znaków na terminalu (berkelejowski program s c r i p t ) i do uruchamiania koprocesów w celu uniknięcia problemów buforowania, o których dyskutowaliśmy przy okazji omawiania próg. 14.10.

19.2 Informacje podstawowe

i Termin pseudoterminal sugeruje, że jednostka ta przypomina terminal związany z programem użytkowym, ale nie jest rzeczywistym terminalem. Na ry*s sunku 19.1 pokazujemy typowe zależności między procesami realizowanymi, gdy używamy pseudoterminalu. Oto kluczowe elementy tego rysunku.

19. Pseudoterminale proces użytkowy

funkcje obsługi czytania i zapisywania

fork exec

proces użytkowy ' ' stdin, stdout, stderr

funkcje obsługi czytania i zapisywania dyscyplina linii terminalu

pseudoterminal nadrzędny

jądro systemu

pseudoterminal podległy

73

19.2. Informacje podstawowe

Na rysunku 19.1 widzimy terminal w systemie BSD. W punkcie 19.3.2 poka żemy, jak otwiera się takie urządzenia. W systemie SVR4 pseudoterminal jest tworzony za pomocą podsystem strumieni (podrozdz. 12.4). Na rysunku 19.2 prezentujemy zależności miedz pseudoterminalowymi modułami strumieniowymi w systemie SVR4. Dw moduły strumieniowe obramowane tu przerywaną linią są opcjonalna Zwróćmy uwagę, że trzy moduły strumieniowe powyżej pseudoterminal podległego są takie, jak otrzymane w przypadku logowania sieciowego n wydruku z programu 12.10. W punkcie 19.3.1 pokażemy, jak tworzy się talukład modułów strumieniowych. Począwszy od tego miejsca możemy już uprościć nasze rysunki; nie b^ dziemy pokazywać „funkcji obsługi czytania i zapisywania" z rys. 19.1, ar „czoła strumienia" z rys. 19.2. Będziemy również stosować skrót „pty" n oznaczenie pseudoterminalu i scalimy wszystkie moduły strumieniowe powj żej podporządkowanego pseudoterminalu na rys. 19.2 w jeden element n; zwany „dyscypliną linii terminalowej", tak samojaknarys. 19.1. Teraz przeanalizujemy niektóre typowe zastosowania pseudoterminali.

Rys. 19.1 Typowa organizacja procesów używających pseudoterminalu 1. Na ogół proces otwiera nadrzędny pseudoterminal i wywołuje funkcję fork. Proces potomny ustanawia nową sesję, otwiera odpowiedni pseudoterminal podległy, powiela jego deskryptor na deskryptory standardowego wejścia, standardowego wyjścia oraz standardowych komunikatów awaryjnych, a następnie wywołuje funkcję exec. Podrzędny pseudoterminal staje się terminalem sterującym w procesie potomnym. 2. Proces użytkowy umieszczony powyżej terminalu podległego uważa, że jego standardowe wejście, standardowe wyjście i standardowy strumień komunikatów awaryjnych są związane z urządzeniem terminalowym. Może on wykonywać dla tych deskryptorów wszystkie funkcje terminalowego wejścia-wyjścia, opisane w rozdz. 11. Ponieważ jednak nie istnieje żadne rzeczywiste urządzenie terminalowe powiązane z terminalem podległym, funkcje nie mające w tym kontekście sensu (zmiana szybkości linii, wysłanie znaku przerwania, ustalenie trybu kontroli nieparzystości bitów itp.) są po prostu ignorowane. 3. Wszystkie dane zapisywane do terminalu nadrzędnego pojawiają się jako dane wejściowe terminalu podległego i odwrotnie. Rzeczywiście, wszystkie dane wejściowe otrzymywane przez terminal podległy pochodzą z procesu użytkowego zlokalizowanego powyżej pseudoterminalu nadrzędnego. Przypomina to łącze strumieniowe (rys. 15.3), ale ponieważ dysponujemy modułem dyscypliny linii umieszczonym powyżej pseudoterminalu podległego, mamy większe możliwości niż przy stosowaniu łączy.

proces użytkowy

fork

proces użytkowy '' stdin, stdout, stderr

czoło strumienia

czoło strumienia

znz

moduł ! strumieniowy i ttcompat •

"TT"'

moduł strumieniowy ldterm

moduł strumieniowy pckt

moduł strumieniowy ~* ptem

pseudoterminal nadrzędny

pseudoterminal podległy

"TT"

jądro systemu

Rys. 19.2 Organizacja pseudoterminali w systemie SVR4

19. Pseudoterminale

plik ze skryptem

wery logowania sieciowego Pseudoterminale są wbudowywane w serwery umożliwiające logowanie sieciowe. Typowymi przykładami są serwery t e l n e t d oraz r l o g i n d . W rozdziale 15 w książce Stevensa [44] są opisane szczegółowo kroki związane z realizacją usługi r l o g i n . Na rysunku 19.3 widzimy sytuację, w której na zdalnej stacji pracuje już powłoka logowania. Podobne zależności powstają, gdy zastosujemy usługę t e l n e t d . serwer rlogind

proces s c r i p t

fork

powłoka

exec

stdout stderr

dyscyplina linii terminalu

stdin

dyscyplina linii terminalu

'' stdin program obsługi terminalu

nadrzędny pty

podległy pty

jądro systemu

dyscyplina linii terminalu

protokoły TCP/IP

program obsługi urządzenia sieciowego

powłoka fork logowania exec (stan uśpienia)

powłoka fork exec, exec logowania stdout stderr

733

19.2. Informacje podstawowe

nadrzędny pty

podległy pty

jądro systemu

użytkownik przy terminalu.

Rys. 19.4 Program s c r i p t sieć Rys. 19.3 Organizacja procesów serwera r l o g i n d

Pokazujemy dwa wywołania funkcji exec między serwerem r l o g i n d a powłoką logowania, gdyż program l o g i n używany do uwiarygodnienia użytkownika jest na ogół umieszczony między nimi. Kluczowym zagadnieniem tego rysunku jest to, że proces zarządzający pseudoterminalem nadrzędnym typowo jednocześnie czyta i zapisuje inny strumień wejścia-wyjścia. W tym przykładzie inny strumień wejścia-wyjścia reprezentuje element opisany jako „protokoły TCP/IP". Oznacza to, że proces musi używać jakiejś formy zwielokrotniania obsługi wejścia-wyjścia (podrozdz. 12.5), na przykład funkcji s e l e c t lub p o l l , lub musi być podzielony na dwa procesy. Przypominamy naszą dyskusję z podrozdz. 18.7 na temat zalet i wad podejścia stosującego jeden lub dwa procesy. ram s c r i p t Program s c r i p t ( l ) , dostarczany przez systemy SVR4 oraz 4.3+BSD, tworzy w pliku kopię wszystkiego, co jest wprowadzane i wyprowadzane w danej sesji terminalowej. Zostaje umieszczony między terminalem a wywołaniem powłoki logowania. Szczegółowe zależności związane z dodaniem programu

s c r i p t pokazuje rys. 19.4. Wynika z niego, że typowo program s c r i p t jest uruchamiany z powłoki logowania, która następnie oczekuje na zakończenie pracy przez ten program. Gdy działa program s c r i p t , wówczas wszystkie dane wyprowadzane przez dyscyplinę linii umieszczoną powyżej pseudoterminalu podległego są kopiowane do pliku (na ogół nazywanego t y p e s c r i p t ) . Ponieważ moduł dyscypliny linii typowo potwierdza dane wprowadzane z klawiatury na ekranie (echo), więc plik ten zawiera również nasze dane wejściowe. Nie zawiera natomiast żadnych haseł, które wprowadzamy, gdyż nigdy nie jest generowane echo haseł. Wszystkie przykłady w tej książce, które polegają na uruchomieniu programu i wydrukowaniu jego danych wyjściowych, zostały wygenerowane przez program s c r i p t i Dzięki temu można uniknąć błędów przy przepisywaniu, z którymi często borykamy się przy ręcznym wprowadzaniu wyniku.

Po przygotowaniu podstawowego pępgramu o nazwie p t y w pod-, rozdz. 19.5 zobaczymy, że bardzo prosty skrypt powłoki przekształca go, w wersję programu s c r i p t .

Program expect Pseudoterminali możemy używać do zarządzania interakcyjnymi programami pracującymi w trybie nieinterakcyjnym. Wiele programów z powodu wbudowanego w nie kodu wymaga przy uruchomieniu istnienia terminalu. Proces

19. Pseudoterminale c a l i w podrozdz. 18.7 jest tego przykładem. Zakłada on, że standardowe wejście jest terminalem i, startując, ustala dla niego tryb surowy (próg. 18.20). Ten program nie może być uruchamiany ze skryptu powłoki w celu automatycznego wybrania zdalnego systemu, załogowania się w nim, pobrania jakieś informacji i wylogowania. Zamiast modyfikować wszystkie interakcyjne programy, by mogły działać w trybie wsadowym, lepszym rozwiązaniem jest dostarczenie metody uruchamiania dowolnego interakcyjnego programu w skrypcie. Sposób taki oferuje program expect (Libes [33], [34]). Korzysta on z pseudoterminali, by uruchamiać inne programy, podobnie jak program p t y z podrozdz. 19.5. Program expect dostarcza również języka programowania umożliwiającego przeglądanie danych wyjściowych uruchamianego programu przed podjęciem decyzji, jakie przekazać dane wejściowe. Gdy uruchamiamy interakcyjny program ze skryptu, nie możemy po prostu kopiować wszystkiego ze skryptu do programu i odwrotnie. Musimy wysłać programowi jakieś dane wejściowe, przeanalizować otrzymane dane wyjściowe i zadecydować, co dalej przesyłać. chamianie koprocesów W programie 14.10 nie mogliśmy wywołać koprocesu, który używa standardowej biblioteki wejścia-wyjścia do obsługi wejścia i wyjścia. Gdy do komunikowania się z koprocesem służy łącze, wówczas standardowa biblioteka wejścia-wyjścia stosuje tryb pełnego buforowania wejścia i wyjścia, co prowadzi do zakleszczenia. Jeśli koproces jest gotowym programem i nie dysponujemy jego wersją źródłową, to nie możemy zaradzić temu, dodając wywołania f f l u s h . Na rysunku 14.8 pokazaliśmy proces, który współpracował z koprocesem. Okazuje się, że dobrą radą na te problemy jest umieszczenie pseudoterminalu między tymi dwoma procesami, jak widzimy na rys. 19.5.

program uruchamiający

735

gie podejście polega na wykonaniu programu p t y (podrozdz. 19.5), którego argumentem jest koproces. Przyjrzymy się obu tym rozwiązaniom po zaprezentowaniu programu pty. Śledzenie danych wyjściowych długo pracujących programów Gdy mamy program, który pracuje stosunkowo długo, to używając dowolnej standardowej powłoki możemy w prosty sposób uruchomić go w tle. Jeśli jednak przekierujemy standardowe wyjście do pliku, a nasz program generuje rozsądną ilość danych wyjściowych, które chcielibyśmy obserwować na bieżąco, to nie możemy widzieć postępów pracy, gdyż standardowa biblioteka wejścia-wyjścia będzie w pełni buforować standardowe wyjście. Widzimy tylko, ze standardowa biblioteka wejścia-wyjścia zapisuje bloki danych do pliku wyjściowego, prawdopodobnie poszczególne fragmenty danych mają po 8192 bajtów. Jeśli mamy kod źródłowy, to możemy wstawić wywołanie f f l u s h . Zamiast tego, możemy uruchomić ten program z programu pty, dzięki czemu standardowa biblioteka wejścia-wyjścia przyjmie, że jej standardowe wyjście jest terminalem. Na rysunku 19.6 pokazujemy układ po wywołaniu programu slowout. Strzałka z napisem fork/exec od powłoki logowania do procesu p t y jest zaznaczona linią przerywaną, aby podkreślić fakt, że proces p t y jest zadaniem pracującym w tle. plik wyjściowy

powłoka _fork logowania exec

proces pty

koproces

łącze1 pseudoterminal łącze2

19.2. Informacje podstawowe

stdin stdout

Rys. 19.5 Uruchamianie koprocesu za pomocą pseudoterminalu

Teraz standardowe wejście i standardowe wyjście koprocesu wyglądają jak zwykłe urządzenie terminalowe, dzięki czemu biblioteka wejścia-wyjścia ustali dla tych dwóch strumieni buforowanie wierszami. Proces macierzysty może otrzymać pseudoterminal zlokalizowany między nim a koprocesem na dwa sposoby. (W tym przypadku procesem macierzystym może być albo próg. 14.9, który korzysta z dwóch łączy do komunikowania się z koprocesem, albo próg. 15.1 stosujący pojedyncze łącze strumieniowe). Jednym ze sposobów jest bezpośrednie wywołanie przez proces macierzysty funkcji p t y f o r k (podrozdz. 19.4) zamiast funkcji fork. Dru-

dyscyplina linii terminalu

dyscyplina linii terminalu

program obsługi terminalu

fork slowout exec stdout stdin stderr

nadrzędny pty

podległy pty

jądro systemu

użytkownik nrzy terminalu, Rys. 19.6

Użycie pseudoterminalu przy uruchomieniu programu powolnie tworzącego wydruki

19. Pseudoterminale

.3 Otwieranie urządzeń pseudoterminalowych Otwieranie urządzenia pseudoterminalowego odbywa się inaczej w systemach SVR4 oraz 4.3+BSD. Przygotowujemy dwie funkcje, które obsługują wszystkie szczegóły: ptym_open, która otwiera następne dostępne nadrzędne urządzenie pseudoterminalowe, oraz p t y s o p e n otwierającą odpowiednie urządzenie podległe. łtinclude " o u r h d r . h " i n t ptym_open (char *pts_name) ; Przekazuje: deskryptor pliku pseudoterminalu nadrzędnego, jeśli wszystko w porządku; - 1 , jeśli wystąpił błąd i n t ptys_open ( i n t fdm, char *pts_name) ; Przekazuje: deskryptor pliku pseudoterminalu podległego, jeśli wszystko w porządku; - 1 , jeśli wystąpił błąd

Typowo nie wywołujemy tych dwóch funkcji wprost, funkcja pty_fork (podrozdz. 19.4) wywołuje jedną z nich oraz funkcję fork, by wygenerować proces potomka. Funkcja p t y m o p e n dowiaduje się, jaki jest kolejny dostępny pseudoterminal nadrzędny i otwiera to urządzenie. Wywołujący musi zaalokować tablicę, w której są przechowywane nazwy pseudoterminali nadrzędnych i podległych. Po pomyślnym wywołaniu w argumencie ptsjiame otrzymujemy nazwę odpowiadającego pseudoterminalu podległego. Ta nazwa oraz deskryptor pliku, odebrany z funkcji ptym_open, są przekazywane do funkcji ptys_open, która otwiera urządzenie podległe. Gdy pokażemy implementację funkcji pty_fork, wówczas stanie się oczywista przyczyna wprowadzenia dwóch funkcji do otwarcia dwóch urządzeń. Typowo proces wywołuje funkcję ptym_open, aby otworzyć pseudoterminal nadrzędny i otrzymać nazwę pseudoterminalu podległego. Następnie proces wywołuje funkcję fork i proces potomny wywołuje funkcję p t y s o p e n , aby po ustanowieniu nowej sesji w wyniku wywołania funkcji s e t s i d otworzyć pseudoterminal podległy. W ten właśnie sposób pseudoterminal podległy staje się terminalem sterującym procesu potomnego.

19.3. Otwieranie urządzeń pseudoterminalowych

Urządzeniem związanym z pseudoterminalem nadrzędnym jest /dev/ P tmx. Jest to strumieniowe urządzenie klonowane (clone device). Oznacza to, że gdy otwieramy je, to jego procedury automatycznie wyszukują pierwsze nieużywane urządzenie pseudoterminalu nadrzędnego i otwierają je. (W następnym podrozdziale zobaczymy, że w systemach berkelejowskich musimy znaleźć sami pierwsze nieużywane urządzenie pseudoterminalu nadrzędnego). Najpierw wywołujemy funkcję open, by otworzyć urządzenie klonowane /dev/ptmx i otrzymać deskryptor pliku dla pseudoterminalu nadrzędnego (próg. 19.1). Automatyczne otwieranie urządzenia nadrzędnego rygluje odpowiadające mu urządzenie podległe. #include łtinclude #include #include iinclude #include



"ourhdr.h"

extern char *ptsname(int);

/* prototyp, którego nie ma w systemowym pliku nagłówkowym */

int pt ym_ope n(char * pts_name) char *ptr; int fdm; strcpy(pts_name, "/dev/ptmx"); /* gdy nie uda się otwarcie */ if ( (fdm = open(pts_name, O_RDWR)) < 0) return(-l); if (grantpt(fdm) < 0) { close(fdm); return(-2);

/* prawo dostępu dla terminalu podległego */

if (unlockpt(fdm) < 0) { /* zerujemy sygnalizator ryglowania terminalu podległego */ close(fdm); return(-3); if ( (ptr = ptsname(fdm)) == NULL) {

19.3.1

System V Wydanie 4

Wszystkie szczegóły związane z implementacją pseudoterminali za pomocą strumieni są omówione w rozdziale 12 dokumentacji AT&T [12]. Funkcje te są również opisane w podręcznikach systemowych, na stronach: grantpt(3), unlockpt(3) i ptsname(3).

737

/* pobieramy nazwę terminalu podległego */

close(fdm); return(-4);

strcpy(pts_name, ptr); /* przekazujemy nazwę terminalu podległego */ return(fdm); /* przekazujemy deskryptor terminalu nadrzędnego */

19. Pseudoterminale

s_open(int fdm, int

char *pts_name)

fds;

/* poniżej alokujemy terminal sterujący */ if ( (fds = open(pts_name, O_RDWR)) < 0) { close(fdm); return(-5); }

if (ioctKfds, I_PUSH, "ptem") < 0) { close(fdm); close(fds); return(-6); } if (ioctKfds, I_PUSH, "ldterm") < 0) { close(fdm); close(fds); return(-7); } if (ioctKfds, I_PUSH, "ttcompat") < 0) { close(fdm); close(fds); return(-8); return(fds);

Próg. 19.1 Funkcje otwierające pseudoterminal w systemie SVR4

Wywołujemy funkcję g r a n t p t , by zmienić uprawnienia urządzenia podległego. Efekt jest następujący: (a) zmiana właściciela pseudoterminalu podległego na obowiązujący identyfikator użytkownika, (b) zmiana właściciela grupy na t t y i (c) zmiana uprawnień, by były włączone wyłącznie prawo odczytu, zapisu przez użytkownika oraz zapisu przez grupę. Przypisanie identyfikatora t t y jako właściciela grupy identyfikatora oraz włączenie prawa zapisu przez grupę wynika z tego, że programy w a l l ( l ) oraz w r i t e ( l ) mają ustawiony bit ustanowienia identyfikatora grupy dla t t y . W funkcji g r a n t p t jest wykonywany program /usr/lib/pt_chmod. Jest to program z ustawionym bitem ustanowienia identyfikatora użytkownika dla nadzorcy systemu (root), dzięki czemu może modyfikować właściciela oraz uprawnienia związane z pseudoterminalem podległym. Wywołanie funkcji unlockpt usuwa wszystkie wewnętrzne rygle związane z urządzeniem podległym. Musimy to zrobić, zanim wywołamy funkcję open, by otworzyć pseudoterminal podległy. Oprócz tego musimy wywołać funkcję ptsname, aby otrzymać nazwę urządzenia podległego. Ta nazwa ma

postać /dev/pts/NNN.

19.3. Otwieranie urządzeń pseudoterminalowych

739

Następną funkcją jest p t y ś open, która otwiera urządzenie podrzędne. W systemie SVR4, jeśli wywołujący jest liderem sesji nie mającym jeszcze terminalu sterującego, to funkcja open alokuje pseudoterminal podległy jako terminal sterujący. Jeśli nie chcemy, by tak się stało, możemy w wywołaniu open podać sygnalizator ONOCTTY. Po otwarciu urządzenia podległego wprowadzamy trzy moduły strumieniowe do strumienia pseudoterminalu podległego. Nazwa ptem oznacza moduł emulacji pseudoterminalu (pseudoterminal emulation module), a l d t e r m jest modułem dyscypliny linii. Te moduły tworzą rzeczywisty terminal. Moduł ttcompat dostarcza cech zgodnych z dawnymi wywołaniami i o c t l w systemach V7, 4BSD oraz Xenix. Jest to moduł opcjonalny, ale wprowadzamy go do strumienia pseudoterminalu podległego, gdyż jest automatycznie umieszczany w przypadku logowania z konsoli oraz logowania sieciowego (zob. wynik próg. 12.10). W wyniku wywołania tych dwóch funkcji otrzymujemy deskryptory pliku dla pseudoterminali nadrzędnego oraz podległego.

19.3.2 System 4.3+BSD W systemie 4.3+BSD musimy najpierw sami wyszukać pierwsze dostępne nadrzędne urządzenie pseudoterminalowe. W tym celu zaczynamy od /dev/ptyp0 i próbujemy, aż nie uda nam się otworzyć pseudoterminalu nadrzędnego lub kończymy pracę, gdy okaże się, że nie ma już żadnych urządzeń. Z funkcji open możemy otrzymać dwa różne błędy: EIO oznaczający, że urządzenie jest już używane, lub ENOENT oznaczający, że urządzenie nie istnieje. W ostatnim przypadku możemy zakończyć poszukiwanie, gdyż wszystkie pseudoterminale są zajęte. Gdy uda nam się otworzyć pseudoterminal nadrzędny, powiedzmy /dev/ptyMV, to ma on nazwę /dev/ttyMV. Funkcja p t y s o p e n z próg. 19.2 otwiera urządzenie podległe. Wywołujemy funkcje chown oraz chmod, ale musimy zdawać sobie sprawę, że nie będą one działać, jeśli proces wywołujący je nie ma uprawnień nadzorcy. Jeżeli zależy nam na zmianie właściciela oraz praw dostępu, to musimy umieścić wywołania tych dwóch funkcji w pliku wykonywalnym należącym do użytkownika r o o t i mający ustawiony bit ustanowienia identyfikatora użytkownika, na wzór funkcji g r a n t p t w systemie SVR4. finclude #include #include #include #include #include

s. 351 Przekazuje: 1, jeśli prawda, 0, jeśli fałsz

void

siglongjmp (sigjmp_buf env, i n t val) ;

Ta funkcja nigdy nie powraca

void

( * s i g n a l ( i n t signo, v o i d (*func) ( i n t ) ) ) ( i n t ) ;

s. 327 Przekazuje: poprzednią dyspozycję sygnału; SIG_ERR, jeśli wystąpił błąd

s. 361

int

sigpending(sigset_t *set) ;

s. 354 Przekazuje: 0, jeśli wszystko w porządku; -1, jeśli wystąpił błąd

int

sigprocmask (int how, const sigset_t *set, sigset_t *oset) ;

s. 353 how: SIG_BLOCK, SIG_UNBLOCK, SIG_SETMASK Przekazuje: 0, jeśli wszystko w porządku; - 1 , jeśli wystąpił błąd

int

sigsetjmp(sigjmp buf env, i n t savemask) ;

s. 361 Przekazuje: 0, jeśli wywołana wprost; wartość niezerową, jeśli powrót z wywołania siglongjmp

int

sigsuspend(const s i g s e t _ t *sigmask) ;

s. 365 Przekazuje: -1 i zmiennąerrno równąEINTR

unsigned int sleep (unsigned i n t seconds) ;

s. 379 Przekazuje: 0 lub liczbę sekund do zakończenia przerwanej drzemki -^ int

s p r i n t f (char *buf, c o n s t c h a r *format, . . . ) ;

s. 177 Przekazuje: liczbę znaków umieszczonych w tablicy

int

s s c a n f ( c o n s t c h a r *buf, c o n s t c h a r * format, . . . ) ;

s. 178 Przekazuje: liczbę przypisanych elementów wejściowych; EOF, jeśli wystąpił błąd wprowadzania danych, lub EOF przed jakąkolwiek konwersją

A. Prototypy funkcji int

s t a t (const char *pathname, s t r u c t s t a t *buf) ;

s. 106

Przekazuje: 0, jeśli wszystko w porządku; - 1 , jeśli wystąpił błąd

char

*strerror(int errnum) ;

s. 38 Przekazuje: wskaźnik do napisu komunikatu

size_t

s t r f t i m e ( c h a r *buf, size_t maxsize, const char *format, const s t r u c t tm *tmptr) ;

s. 200 Przekazuje: liczbę znaków umieszczonych w tablicy, jeśli starczyło miejsca; 0, w przeciwnym razie

int

symlink (const char *actualpath, const char *sympath) ;

s. 138 Przekazuje: 0, jeśli wszystko w porządku; - 1 , jeśli wystąpił błąd

void

sync(void);

long

sysconf(int

s. 154

name);

s. 61 name: _SC_ARG_MAX, _SC_CHILD_MAX, _SC_CLK_TCK, _SC_NGROUPS_MAX, _SC_OPEN_MAX, _SC_PASS_MAX,

pid_t

void

syslog

( i n t priority,

char

* format,

. . . ) ;

s. 500

int

system (const char *cmdstring) ;

Przekazuje: stan zakończenia powłoki

int

t c d r a i n ( i n t filedes) ;

s. 411 Przekazuje: 0, jeśli wszystko w porządku; - 1 , jeśli wystąpił błąd

int

tcflow (int filedes, i n t action) ;

s. 411 action: TCOOFF, TCOON, TCIOFF, TCION Przekazuje: 0, jeśli wszystko w porządku; - 1 , jeśli wystąpił błąd

int

t c f l u s h ( i n t filedes, i n t queue) ;

s. 411 ąueue: TCIFLUSH, TCOFLUSH, TCIOFLUSH Przekazuje: 0, jeśli wszystko w porządku; - 1 , jeśli wystąpił błąd

int

t c g a t a t t r ( i n t filedes, s t r u c t termios *termptr) ;

s. 401 Przekazuje: 0, jeśli wszystko w porządku; - 1 , jeśli wystąpił błąd

t c g e t p g r p ( i n t filedes) ;

s. 300

Przekazuje: identyfikator grupy procesów pierwszoplanowej grupy procesów, jeśli wszystko w porządku; -1, jeśli wystąpił błąd

int

tcsendbreak (int filedes, int duration) ;

s. 411 Przekazuje: 0, jeśli wszystko w porządku; —1, jeśli wystąpił błąd

int

t c s e t a t t r (int filedes, int opt, const struct termios *termptr) ;

s. 401 opt:

TCSANOW,

TCSADRAIN,

TCSAFLUSH

Przekazuje: 0, jeśli wszystko w porządku; - 1 , jeśli wystąpił błąd int

tcsetpgrp ( i n t filedes, p i d _ t pgrpid) ;

s. 300

Przekazuje: 0, jeśli wszystko w porządku; - 1 , jeśli wystąpił błąd

char

*tempnam(const char *directory, const char *prefix) ;

s. 183 Przekazuje: wskaźnik do unikatowej nazwy ścieżki

time_t

time (time_t *calptr) ;

s. 198 Przekazuje: wartość czasu, jeśli wszystko w porządku; -1, jeśli wystąpił błąd

_SC_STREAM_MAX, _SC_TZNAME_MAX, _SC_JOB_CONTROL, _SC_SAVED_IDS, _SC_VERSION,

_SC_XOPEN_VERSION Przekazuje: odpowiednią wartość, jeśli wszystko w porządku; - 1 , jeśli wystąpił błąd

777

A. Prototypy funkcji

clock_t times(struct tms *buf) ;

s. 283 Przekazuje: zużyty czas zegarowy, liczony w taktach zegara; - 1 , jeśli wystąpił błąd FILE

*tmpfile(void);

s. 181 Przekazuje: wskaźnik pliku, jeśli wszystko w porządku; NULL, jeśli wystąpił błąd

char

*tmpnam(char *ptr) ;

s. 181 Przekazuje: wskaźnik do unikatowej nazwy ścieżki

int

t r u n c a t e (const char *pathname, off_t length) ;

s. 127

Przekazuje: 0, jeśli wszystko w porządku; - 1 , jeśli wystąpił błąd

s. 272

char

mode_t

*ttyname (int filedes) ;
foo $ datę > bar $ chmod a-r foo bar $ l s -1 foo bar —w—w 1 stevens --w--w 1 stevens $ a.out $ l s -1 foo bar --w--w 1 stevens --w--w 1 stevens

4.6.

Katalog nigdy nie ma rozmiaru 0, ponieważ są w nim zawsze pozycje dla katalogów „."oraz „. .". Rozmiar dowiązania symbolicznego jest liczbą znaków w nazwie ścieżki, która jest umieszczona w dowiązaniu symbolicznym, a taka nazwa ścieżki musi zawierać co najmniej jeden znak.

Do pliku o u r h d r . h możemy dodać następujące wiersze

4.8.

Jądro systemu, tworząc nowy plik core, może korzystać z domyślnego ustawienia bitów praw dostępu dla pliku. W tym przykładzie miało ono postać r w - r — r — . Domyślna wartość może, ale nie musi, zostać zmodyfikowana przez wartość umask. Powłoka korzysta też z domyślnego ustawienia bitów praw dostępu do pliku, gdy tworzy nowy plik w celu przekierowania. W tym przykładzie miały one wartość rw-rw-rw-; ta wartość jest zawsze modyfikowana przez bieżącą wartość umask. W tym przykładzie wartość umask była równa 02.

4.9.

Nie możemy użyć polecenia du, ponieważ wymaga ono albo nazwy ścieżki, np.

Wszystkie prawa dostępu są wyłączone. $ umask. 777 $ datę > temp.foo $ l s -1 temp.foo 1 stevens

du tempfile

albo nazwy katalogu, np.

29 Jan 14 06:39 temp.foo

du .

Poniższa sekwencja poleceń pokazuje, co dzieje się, gdy jest wyłączone prawo odczytu przez użytkownika. $ datę > foo wytaczamy prawo odczytu przez użytkownika $ chmod u-r foo sprawdzamy prawa dostępu $ l s -1 foo 29 J u l 31 09:00 foo —w-rw-r— 1 stevens i próbujemy odczytać plik $ cat foo cat: foo: Permission denied

4.5.

wyłączamy wszystkie uprawnienia odczytu sprawdzamy prawa dostępu 29 Jul 31 10:47 bar 29 Jul 31 10:47 foo uruchamiamy próg. 4.3 sprawdzamy prawa dostępu i rozmiar 0 Jul 31 10:47 bar 0 Jul 31 10:47 foo

Zwróćmy uwagę, że prawa dostępu nie uległy zmianie, ale pliki zostały skrócone.

Ten przykład pokazuje, jak nasze pliki nagłówkowe mogą maskować pewne różnice systemowe.

4.4.

usuwamy pliki, jeśli istnieją tworzymy je, zapisując jakieś dane

Wywołanie funkcji s t a t zawsze próbuje podążać za dowiązaniami symbolicznymi (tab. 4.7), a więc program nigdy nie wypisze informacji „symbolic link" (dowiązanie symboliczne). W przykładzie pokazanym w tekście /bin jest dowiązaniem symbolicznym do katalogu / u s r / b i n ; funkcja s t a t stwierdzi wówczas, że / b i n jest katalogiem, a nie dowiązaniem symbolicznym. Jeśli dowiązanie symboliczne wskazuje nieistniejący plik, funkcja s t a t przekazuje błąd.

# i f defined(S_IFLNK) && !defined(S_ISLNK) #define S_ISLNK(mode) (((modę) & S_IFMT) == S_IFLNK) #endif

4.3.

793

Jeśli próbujemy za pomocą funkcji open lub c r e a t utworzyć plik, który już istnieje, to prawa dostępu dla tego pliku nie ulegną zmianie. Możemy to potwierdzić, uruchamiając próg. 4.3.

Jednak po powrocie z funkcji u n l i n k , pozycja w katalogu dotycząca ' pliku t e m p f i l e ginie. Pokazane polecenie du . nie uwzględni przestrzeni, która jest nadal zajmowana przez plik t e m p f i l e . Musimy więc użyć w tym przypadku polecenia df, aby zobaczyć rzeczywistą ilość wolnej przestrzeni w systemie plików. 4.10.

;

Jeśli usuwane dowiązanie nie jest ostatnim dowiązaniem do tego pliku, to plik nie jest usuwany. W tym przypadku jest aktualizowany J czas zmiany stanu pliku. Jeśli jednak usuwane dowiązanie jest ostatnim dowiązaniem do tego pliku, to nie ma sensu aktualizacja tego czasu, gdyż wszystkie informacje o pliku (jego i-węzeł) zostaną usunięte razem z tym plikiem.

C. Rozwiązania wybranych ćwiczeń

4.11.

4.13.

Po otwarciu katalogu za pomocą funkcji opendir wywołujemy rekurencyjnie naszą funkcję dopath. Jeśli założymy, że funkcja opend i r korzysta z jednego deskryptora pliku, to za każdym razem, gdy schodzimy o jeden poziom niżej, pobieramy kolejny deskryptor. (Zakładamy, że deskryptor ten nie jest zamykany, dopóki nie skończymy naszych działań z tym katalogiem i nie wywołamy c l o s e d i r ) . Ogranicza to głębokość przeglądanego drzewa katalogów w systemie plików do maksymalnej liczby otwartych deskryptorów w jednym procesie. Zauważmy, że funkcja ftw umożliwia podanie przez wywołującego liczby używanych deskryptorów, co sprawia, że ta implementacja może zamykać i ponownie używać deskryptorów.

Funkcja c h r o o t może być wywoływana tylko przez nadzorcę systemu, a po zmianie katalogu głównego w danym procesie, ani ten proces, ani wszystkie jego procesy pochodne nie mogą powrócić do oryginalnego ustawienia katalogu głównego. 4.14.

Najpierw wywołaj funkcję s t a t , aby pobrać trzy czasy dotyczące tego pliku, a następnie wywołaj funkcję utime, aby ustawić wymaganą wartość. Jeśli wywołanie utime nie ma zmienić jednej z wartości czasu, to powinna być ona równa wartości uzyskanej z wywołania funkcji s t a t .

4.15.

Polecenie f i n g e r ( l ) wywołuje funkcję s t a t dla pliku zawierającego skrzynkę pocztową. Czas ostatniej modyfikacji jest zgodny z czasem otrzymania ostatniego komunikatu e-mail, a czas ostatniego dostępu jest czasem ostatniego odczytu komunikatu ze skrzynki. Polecenia cpio oraz t a r zapamiętują jedynie czas modyfikacji ( s t m t i m e ) archiwum. Czas dostępu nie jest zapamiętywany -jego wartość odpowiada czasowi utworzenia archiwum, ponieważ plik musi zostać przeczytany, aby go zarchiwować. Opcja -a w poleceniu cpio sprawia, że po odczytaniu wszystkich archiwowanych plików

795

wejściowych jest odtwarzana oryginalna wartość czasu dostępu do tych plików. Dzięki temu utworzenie archiwum nie zmienia czasu dostępu. (Przywrócenie czasu dostępu modyfikuje jednak czas ostatniej zmiany stanu). Czas ostatniej zmiany stanu