"Gdyby budowlani budowali domy w taki sam sposób, w jaki programiści piszą programy, to jeden dzięcioł zniszczyłby całą naszą cywilizację."
Drugie prawo Weinberga
Pytanie typu "jak działa komputer" jest zmorą, dręczącą po nocach ludzi mogących być na nie narażonymi. Jest nie tyle trudne ile bałamutne. Zadają je ludzie którzy albo nie za bardzo wiedzą co to w ogóle jest, albo chcą żeby ten "mądry" zaczął się jąkać i plątać w zeznaniach. Albo oba motywy na raz - z mojego doświadczenia wynika że to sytuacja najczęstsza.
Przyczyna jąkania jest dosyć trywialna: prosto i łatwo się nie da. Zarówno wyjaśnianie, jak i słuchanie objaśnień wymaga czasu, uwagi i systematyczności. W przeciwnym wypadku powstanie typowa pomroczność jasna, pospolita wśród polityków, w technice niepożądana.
Nie żeby tam jakaś wielka mądrość siedziała, lecz dlatego że wewnętrzne życie maszyn cyfrowych jest trochę "nieludzkie". Komputer to szybkie urządzenie elektroniczne które może wykonywać za nas męczące zadania, ale tylko takie które można przełożyć na algorytm, czyli dokładny opis metody rozwiązywania. Algorytm koduje się w postaci programu, czyli ciągu elementarnych i jednoznacznych instrukcji, zrozumiałych dla maszyny.
Maszyna wykonuje program na dostarczonych jej danych. Dane mogą być przetwarzane (dodawane, odejmowane, sortowane, porównywane itd). Mogą też zmieniać bieg programu, ale tylko wtedy, gdy taka możliwość jest w nim zawarta. Jak dotąd, komputery nie potrafią same modyfikować działania programu - nie mają zdolności twórczych. To, co "robi" komputer, tworzą ludzie. Ludzie konstruują sprzęt komputerowy, układają programy, dostarczają danych. Maszyna tylko pomaga ubrać zamysł w ciało. Bez oprogramowania potrafi jedynie zżerać energię elektryczną.
Innymi słowy: komputer może obierać ziemniaki, ale w tym celu musi zostać wyposażony w cyfrowo sterowaną obieraczkę i odpowiednie oprogramowanie. Dla człowieka nic naturalniejszego: trzeba obrać kartofel, to go łapiemy, w drugą łapkę nożyk, obieramy - plums - i gotowe. Nikt "normalny" nie zastanawia się przy tym jak sam działa. Po prostu obiera ten przebrzydły kartofel.
Działanie komputera zasadniczo zawiera się w trzech jego blokach: procesorze, czyli mózgu, pamięci, pełniącej rolę kartki papieru do notowania, i układom wejścia i wyjścia, przez które komunikujemy się ze światem zewnętrznym. Poprzez porty wejściowe nasz komputer dostaje program. Ot, na przykład taki:
1) pobierz z wejścia liczbę a,
2) pobierz z wejścia liczbę b,
3) dodaj liczby a i b,
4) pokaż operatorowi wynik.
Komputer wszystko to notuje skrzętnie w kajeciku, po czym czeka na polecenie rozpoczęcia pracy. Kiedy operator da sygnał, maszynka grzecznie odczytuje program i wykonuje krok po kroku. W trakcie wykonywania pierwszego i drugiego rozkazu przerywa, i patrzy zachęcająco na swego dzielnego operatora - czeka na dane. Otrzymane dane (niech będzie a=2 i b=3.14) zapisuje, czyli umieszcza w pamięci.
W poleceniu trzecim zaczynają się schody. Trzeba zerknąć na zapisane dane i ruszyć głową. Wynik tego ruszania zostaje także zanotowany na kartce - głowa ma być pusta i gotowa do ewentualnych dalszych działań.
Pozostaje zakomunikować wynik operatorowi. Jako, że nie dano innej dyspozycji (np. odśpiewania), nasza supernowoczesna maszyna zapisze tę piątkę z groszami na rogu swej kartki, o którym wie, że jest widoczny dla operatora pomiędzy kubkiem kawy a popielniczką. Inaczej mówiąc, wyświetli "5.14" na monitorze ekranowym.
Wypadałoby powiedzieć jak ta maszyneria to robi. Zastrzegam sobie od razu, że opisany poniżej komputer nie istnieje - to zaledwie prymitywny model do zilustrowania zasady.
Prawdziwą pamięć komputera można wyobrazić sobie jako elektroniczną matrycę z elementów, mogących zapamiętywać cyfry dwójkowe - zera i jedynki. Poszczególne wiersze matrycy są komórkami, zawierającymi wartości binarne, czyli słowa. Słowa mogą być danymi (liczbami, kodami znaków alfabetu, zbiorami parametrów punktów obrazu graficznego, itp), poszczególnymi rozkazami programu, albo kodami sterującymi wewnętrznymi funkcjami komputera. Każda komórka pamięci ma swój kolejny numer, czyli adres.
Z grubsza pamięć można podzielić na szybką pamięć operacyjną i pamięć zewnętrzną (tzw. masową). W pamięci operacyjnej komputer trzyma aktualnie wykonywane programy i dane.
Pamięć zewnętrzna zwykle pełni rolę długoterminowego magazynu. Jest znacznie wolniejsza, ale utrzymuje zapisane informacje kiedy komputer jest wyłączony lub ulegnie awarii. Informacje są w niej zawarte w postaci plików - zbiorów stanowiących zamkniętą całość. Pliki mogą być wykonywalne; są to programy. Inne są plikami danych: binarnymi, tekstowymi, graficznymi, itp.
Nazywanie procesora mózgiem jest sporą przesadą; mówi się tak chyba tylko po to, żeby na siłę zachować jakąś analogię do czynności ludzkich organów.
Podstawową częścią procesora jest jednostka arytmetyczno - logiczna (ALU - Arithmetical - Logical Unit), przeznaczona do wykonywania operacji arytmetycznych i logicznych. Oprócz ALU w obrębie procesora są układy dekodujące rozkazy i sterujące przepływem danych, zegar taktujący, oraz pewna ilość tak zwanych rejestrów. Rejestry są przeznaczone między innymi do przechowywania danych, potrzebnych procesorowi w czasie wykonywania operacji.
Wszystkie części składowe komputera są połączone centralnym ciągiem komunikacyjnym - magistralami, czyli szynami. Kolejowa symbolika jest jak najbardziej na miejscu, ponieważ magistrala to nie tylko przewody; obejmuje ona między innymi układy komutacyjne - swoiste zwrotnice, umożliwiające procesorowi dostęp do konkretnych bloków funkcjonalnych. Urządzenia te mogą same żądać dostępu do procesora. Na przykład wtedy gdy w porcie klawiatury pojawi się informacja, co oznacza że operator się obudził i czegoś chce. Urządzenia w których coś sie dzieje wysyłają wtedy sygnał żądania obsługi, a procesor utworzy połączenie na magistrali, po czym rozpocznie pobieranie danych i dekodowanie ich.
![]() | Właściwie są dwie główne szyny systemowe: szyna danych i szyna adresowa. W komputerze wszystko ma swój "adres zamieszkania". Na szynie adresowej ustawia się adres komórki pamięci, urządzenia lub portu, z którym ma zostać połączona szyna danych. Dla porządku trzeba wspomnieć o trzeciej, szynie sterowań, służącej do wymiany sygnałów sterujących i kontrolnych. |
Połączone szynami wewnętrzne bloki komputera tworzą jednostkę centralną (CPU - Central Processing Unit). Do CPU nie należą urządzenia zewnętrzne i pamięć masowa.
Ostatnio przyjęło się użycie określenia CPU dla mikroprocesora. No cóż, architektura dzisiejszych mikrokomputerów jest tak zagmatwana że ma to pewne podstawy.
Zgodnie z wcześniejszym zastrzeżeniem, najstaranniej zbudowany komputer bez oprogramowania jest martwy. Przyjmijmy, że operator wprowadził do pamięci program dodawania dwóch liczb. Program zajmuje w pamięci pewną ilość komórek, po jednej instrukcji na komórkę, począwszy od pierwszego adresu.
Tyle, że rzeczywisty program dodawania wygląda zupełnie inaczej, niż ten "poglądowy" na początku artykułu. Jako się rzekło, musi on precyzyjnie opisywać działanie bloków funkcjonalnych komputera. Na początku trzeba pobrać z portu wejściowego (np. portu klawiatury) kolejno dwie liczby, oznaczone a i b, i umieścić je w obszarze pamięci, przeznaczonym na dane. W następnym kroku procesor pobiera z pamięci pierwszą z nich i umieszcza ją w rejestrze, zwanym akumulatorem. Następnie pobiera drugą i dodaje ją do zawartości akumulatora. Wynik zostaje umieszczony z powrotem w akumulatorze. Pozostaje przesłać zawartość akumulatora do pamięci adaptera monitora ekranowego.
Z każdym z tych kroków zawartość specjalnego rejestru procesora, zwanego licznikiem rozkazów, jest zwiększana o jeden. Zawartość licznika rozkazów wskazuje wtedy adres kolejnego polecenia programu.
Nasuwa się pytanie, skąd komputer wie, skąd ma pobrać program i gdzie w pamięci ma go umieścić, gdzie zapisać dane, jak rozpocząć wykonywanie programu, jak wyświetlić albo wydrukować wynik. No i co zrobić w przypadku błędu. Otóż w pierwszych komputerach wszystkie te sprawy były sztywno ustalone konstrukcją maszyny. Programista, operator i użytkownik, często z konieczności w jednej osobie, musieli pamiętać lokalizację rozmaitych rzeczy w pamięci, adresy używanych portów, itp. Obecnie taka metoda jest stosowana tylko w bardzo wyspecjalizowanych konstrukcjach, na przykład w sterownikach mikroprocesorowych. Pracują one z jednym programem i ustalonymi raz na zawsze urządzeniami wejścia/wyjścia oraz rodzajami danych.
Typowe komputery pracują pod kontrolą systemu operacyjnego. System operacyjny jest specjalnym programem, który steruje pracą komputera, pośrednicząc pomiędzy użytkownikiem a maszyną. Operator, zlecając komputerowi wykonanie polecenia (pobranie pliku z pamięci masowej, uruchomienie programu, itd) komunikuje się z systemem operacyjnym, który zamienia jego działania na sekwencje rozkazów maszynowych i dobierze parametry w zależności od rozporządzalnych zasobów. W drugą stronę, system operacyjny zapewni operatorowi informację o zdarzeniach w trakcie wykonywania programu i zapisze wyniki do właściwego urządzenia wyjściowego.
System operacyjny zajmuje się także współpracą z pamięciami zewnętrznymi. Na przykład, użytkownik (lub program) zleca zapisanie pliku na dysku magnetycznym. System zajmie się przydzieleniem obszaru na dysku i przesłanie informacji z pamięci na dysk. Oprócz tego zajmuje się udzielaniem informacji o plikach na dysku i organizacją hierarchicznej organizacji plików (katalogi i podkatalogi). Śmiało można powiedzieć że użytkownik widzi swój komputer poprzez otoczkę systemu operacyjnego.
Systemy operacyjne można podzielić z grubsza na jednozadaniowe i wielozadaniowe.
Systemy jednozadaniowe są zdolne wykonywać jeden program na raz. Są to zwykle systemy dla prostych mikrokomputerów: DOS (Disk Operating System) dla IBM PC i pochodnych, lub CP/M (Control Program for Microcomputer). Oprócz nich było całe mrowie systemików dla ośmiobitowych komputerków domowych.
W wielozadaniowych systemach operacyjnych kilka zadań (programów) może być wykonywanych w tym samym czasie. Użytkownik może uruchomić kilka programów i będzie to wyglądało tak, jakby programy te działały jednocześnie. Oznacza to na przykład, że użytkownik może pracować nad edycją tekstu, mając na podorędziu aktywny arkusz kalkulacyjny, a na dodatek inny plik w trakcie drukowania.
W rzeczywistości CPU może wykonywać tylko jedno zadanie w danej chwili, ale system operacyjny ma zdolność dzielenia czasu jednostki centralnej między wiele procesów. Przez jakiś plasterek czasu, niedostrzegalny dla użytkownika, wykonywany jest jakiś program, po czym jego stan jest zapamiętywany. Na następny plasterek czasu system reaktywuje inny program, i tak dalej. (Termin "plasterek czasu" jest drętwym tłumaczeniem angielskiego time slice, ale niezwykle mi się podoba.) Z punktu widzenia użytkownika wygląda to tak, jakby wszystkie programy były wykonywane równocześnie.
Wielozadaniowe systemy operacyjne z reguły są wyposażone w rozbudowane graficzne interfejsy użytkownika, prezentujące działające programy w postaci okien na ekranie monitora. Przykładami mogą być UNIX i pochodne (m.in.LINUX) z nakładką X-Windows, MOS (Mac Operating System) dla komputerów Apple oraz popularne MS Windows dla komputerów typu PC.
Wśród wielozadaniowych systemów operacyjnych istnieje spora grupa systemów wielodostępnych. Wielodostępność oznacza, że wielu użytkowników może użytkować system w tym samym czasie. Wiele terminali (monitorów ekranowych z klawiaturami, myszkami, i czym tam chcecie) może być dołączonych do tego samego komputera. Rolę terminali mogą z powodzeniem spełniać mikrokomputery, połączone z komputerem centralnym i ze sobą nawzajem przy pomocy sieci komputerowej. Każdy z użytkowników przy terminalach może korzystać przy tym z wielozadaniowości. Dużą zaletą takiej architektury jest możliwość dostępu wielu członków grupy roboczej do tych samych danych w tym samym czasie.
Do systemów wielodostępnych należą wszystkie systemy dla superkomputerów, dużych maszyn typu mainframe (OS370 itp.) i minikomputerów. Bardzo rozpowszechnione są też sieciowe wersje systemów mikrokomputerowych (MS Windows NT i 2000). Najszerzej obecnie wykorzystuje się uniwersalny system UNIX i pokrewne: IRIX dla maszyn Silicon Graphics, SunOS (Sun) oraz popularny LINUX.
Na koniec chciałbym wrócić do naszego programu użytkowego (pożal się Boże). W tych sześciu krokach, zapisanych w postaci zrozumiałych dla maszyny kodów binarnych, zawierałby się program dodawania. Taki bezpośredni zapis programu nazywany językiem maszynowym. Polecenia języka maszynowego pokrywają się z listą rozkazów danego procesora. Można je podzielić na cztery grupy według funkcji:
- Polecenia przesłania, dotyczące pobierania i zapisywania informacji.
- Polecenia arytmetyczne, dotyczące wykonywania operacji arytmetycznych przez procesor.
- Polecenia logiczne, umożliwiające porównywanie i sortowanie danych, oraz operacje na ciągach znaków. Pozwalają także na konstruowanie programów, zawierających alternatywne moduły, wykonywane w zależności od zadanych parametrów.
- Polecenia sterujące, dające możliwość zarządzania pracą poszczególnych bloków funkcjonalnych.
Programowanie w języku maszynowym jest niezwykle trudne i pracochłonne; słowa maszynowe wprowadza się operując rozkazami i parametrami w postaci liczb dwójkowych. Dla ułatwienia tego procesu wprowadzono zapis kodu programu, nazwany assemblerem (czyli monterem - każdy konstruktor wie że warto mieć dobrego technika do pomocy).
Polecenia assemblera odpowiadają pojedyńczym rozkazom kodu maszynowego, ale zapisywanym "po ludzku", w postaci tzw. kodów mnemonicznych. Program tłumaczący przekłada kod assemblera na kod maszynowy.
Pisanie programów w assemblerze wymaga doskonałej znajomości budowy komputera. Poza tym kod maszynowy jest właściwy dla konkretnego typu maszyny; jeśli programista chciałby uruchomić swój program na innym komputerze, musiałby go dostosować do specyfiki nowej maszyny; praktycznie napisać od nowa.
W celu ułatwienia programowania stworzono języki programowania wyższego rzędu. Języki programowania operują rozkazami realizującymi typowe czynności. Każdy rozkaz odpowiada sekwencji kodów maszynowych.
Nasz program, zapisany w języku BASIC (Beginners All Purpose Symbolic Instruction Code) wyglądałby następująco:
input "podaj liczbę a" a
input "podaj liczbę b" b
print a+b
end
Jak widać, polecenia są po prostu krótkimi angielskimi słowami, opisującymi daną czynność: input wyświetla na ekranie napis "podaj liczbę a" (albo b) i czeka na wprowadzenie liczby z klawiatury. Kiedy liczby a i b są już znane, zostanie wykonane polecenie print a+b, które wyświetli sumę a i b.
Tu należy zwrócić uwagę na użyte zmienne a i b. Ludzie mówią "dodaj dwa do dwóch", uznając domyślnie że chodzi o liczby. A przecież zmienna może być wartością liczbową albo łańcuchem znaków. Komputerowi który ma dodać dwa do dwóch trzeba zasygnalizować typ danych; zmienne liczbowe zostaną potraktowane arytmetycznie i wyjdzie cztery. Jeżeli będą to dane znakowe, komputer wykona sumę logiczną - po prostu sklei łańcuchy znaków i wyjdzie 22.
Zmienna znakowa może być cyfrą, literą lub wyrazem - czym chcemy, natomiast jeżeli zmiennej liczbowej zechcemy nadać wartość "dwa" zostanie to uznane za błąd.
Tu, w Basicu, typ zmiennej liczbowej zadeklarowaliśmy używając nazwy złożonej z liter alfabetu. Nazwa zmiennej znakowej musi mieć na końcu znak dolara ($). Inne języki programowania wymagają wcześniejszego zadeklarowania typów używanych zmiennych.
Idąc dalej, jeżeli chcemy żeby nasz program albo jego część działała "w kółko" możemy zastosować instrukcję skoku. Na przykład zależy nam na programie który będzie do stałej liczby a dodawał zmienne b:
input "podaj liczbę a" a
zmień:
input "podaj liczbę b" b
print a+b
goto zmień:
W pewnym miejscu programu postawiliśmy etykietkę "zmień". Po wykonaniu dodawania maszyna poda wynik, po czym zapyta nas o następną wartość b. Zastosowaliśmy tu tak zwany skok bezwarunkowy.
Jako się rzekło na wstępie wartość danych może zmieniać przebieg wykonywania programu. Nazywa się to rozgałęzieniem.
input "podaj liczbę a" a
input "podaj liczbę b" b
if a+b>20 then print "za dużo dla mnie!" else print a+b
Tu uzależniliśmy wyświetlenie wyniku od jego wartości. Maszyna ma policzyć sobie w pamięci, a jeżeli wynik będzie większy od 20 ma oświadczyć (then) że tyle to już za wiele. Jeżeli warunek ten nie jest spełniony (else) ma grzecznie wyświetlić sumę a i b.
Powyższe przykłady zostały napisane w niezbyt popularnym Turbo Basicu, ale konstrukcja takich podstawowych działań jest zbliżona we wszystkich językach programowania.
Czasem spotykam się z pytaniem: "Dlaczego te wszystkie polecenia są po angielsku"? Otóż z dwóch przyczyn. Po pierwsze angielski jest dość powszechnie używany na świecie. A według wielkiego Woltera jest to świat najlepszy ze światów możliwych, ponieważ w odróżnieniu od nich istnieje realnie.
A po drugie nie każdemu pięknemu językowi właściwa jest zwięzłość i precyzja w jednym. Pamiętam jak dawno temu w jakimś spolszczonym LOGO (też taki język programowania, podobno dla dzieci - miały się po nim jakoś cholernie rozwijać) zamiast DELALL (delete all) trzeba było wstukać WYRWSZ (niby że wyrzuć wszystko). Rozwinęliśmy się z bratem do rozpuku. Oczywiście uprzednio zwięźle i precyzyjnie określiwszy takie patriotyczne wodotryski.
No, ale do rzeczy. Wielką zaletą języków wyższego rzędu jest możliwość przenoszenia programów na inne typy komputerów. Musi tylko istnieć program tłumaczący z danego języka programowania na kod maszynowy komputera docelowego. Jeżeli składnia wersji języka w którym pisaliśmy program różni się nieco od tej, dostępnej dla komputera docelowego, dość łatwo to poprawić.
Co do tego tłumaczenia na kod maszynowy, to może się ono odbywać na dwa sposoby: języki programowania mogą być językami kompilowanymi lub interpretowanymi.
Większość języków jest językami kompilowanymi. Program, zapisany jako zwykły tekst, poddaje się działaniu kompilatora, czyli programu tłumaczącego, którego produktem jest plik z kodem wykonywalnym. Zaletą skompilowanych programów jest mała objętość i spora szybkość działania.
Językami kompilowanymi są FORTRAN (Formulae Translator), Pascal, niektóre wersje BASICa, i niezwykle popularny język C.
Program napisany w języku interpretowanym zaczyna być tłumaczony na instrukcje maszynowe dopiero w momencie uruchomienia. Potrzeba do tego działania innego programu, zwanego interpreterem. Interpreter tłumaczy na bieżąco instrukcję po instrukcji. Dlatego programy interpretowane często nazywa się skryptami.
Skrypty wykonują się dość wolno, a interpretacja wymaga sporej ilości pamięci. Ale języki interpretowane mają też pewne zalety. Doskonale nadają się do szybkiego tworzenia programów potrzebnych doraźnie, do kilkakrotnego zaledwie zastosowania. Poza tym taki program daje się szybko zmodyfikować.
Innym, dość świeżym zastosowaniem są programy wysyłane poprzez sieć komputerową do komputera odbiorcy z pominięciem jego pamięci masowej. Programy takie są automatycznie uruchamiane, a po zakończeniu działania wymazywane z pamięci operacyjnej. Tak działa język HTML (Hypertext Markup Language), którego działanie właśnie widzicie. Jego interpreter jest zawarty w przeglądarce internetowej.
Językami interpretowanymi są PERL i klasyczny BASIC. Miejsce pośrednie między językami kompilowanymi a interpretowanymi zajmuje niezwykle obecnie modna Java, o możliwościach porównywalnych z językiem C. Programy napisane w języku Java są kompilowane do tak zwanego kodu pośredniego. Zoptymalizowany i skompresowany kod pośredni zajmuje mniej miejsca i może być wykonywany szybciej niż "goły" program źródłowy. Poza tym daje się przesyłać jak zwykły tekst i interpretować na miejscu. Czysto interpretowana wersja Javy nazywa się Java Script. Małe programy (tzw. applety) można wplatać w tekst stron internetowych - interpreter Javy i Java Scriptu zawierają wszystkie nowsze wersje przeglądarek.
Tak na marginesie, to ten brak zdolności twórczych u komputerów jest też niezupełnie taki oczywisty. Marzenie o inteligentnych maszynach jest stare jak same maszyny - lenistwo jest przecież motorem postępu. Toteż od dawna prowadzi się prace nad sztuczną inteligencją (tzw. AI - artificial intelligence), czyli sprzętem i oprogramowaniem zdolnym do gromadzenia doświadczeń, tworzenia powiązań i używania ich do modyfikowania programów działania.
Ogólnie rzecz biorąc celem prac nad AI jest stworzenie systemów zdolnych do całkowicie autonomicznego wykonywania zadań w dynamicznie zmieniających się warunkach. "Inteligentna" maszyna musi być zdolna do poradzenia sobie z nieznanymi jej wcześniej zjawiskami i sytuacjami - musi umieć zastosować wiedzę, tę "daną" i tę zdobytą, do rozwiązania problemu.
Już naprawdę na koniec powtórzę po raz kolejny: to nieprawda że komputer zmienia świat. To my go zmieniamy, posługując się między innymi komputerami. Jak każde narzędzie, komputer może służyć do celów dobrych i do złych. Niekiedy trzeba by mądrości równej Bożej wszechwiedzy żeby odróżnić jedne od drugich. Mam cichą nadzieję że coraz częściej będziemy komputerów używać do robienia rzeczy jednoznacznie dobrych. Chociażby do odkręcania wcześniej narobionych głupstw. Komputer tego za nas nie zrobi.