System operacyjny macOS

Opowieść o obsłudze komendami tekstowymi z minimalną ilością żargonu technicznego

Budowa systemu

System operacyjny macOS komputerów firmy Apple należy do podrodziny BSD rodziny Unix. Składa się z warstw:


Wiersz poleceń

Czasami rozwiązanie problemu na komputerze wymaga pracy w wierszu poleceń gdyż w podsystemie Aqua nie ma odpowiednich narzędzi. Taka sytuacja zachodzi również gdy chcemy zautomatyzować ciąg zadań albo pracować z systemem zdalnie poprzez sieć. Po zalogowaniu się lokalnie na konto użytkownika w systemie macOS pojawi się graficzny pulpit kontrolowany przez podsystem (powłokę graficzną) Aqua. Naciskając jednocześnie klawisze command i spację pojawi się okno Spotlight Search. Po wpisaniu w miejscu migającego kursora słowa Terminal i naciśnięciu klawisza Enter ukaże się okno wirtualnego dalekopisu. To wirtualne urządzenie pozwala powłoce jądra lokalnego lub zdalnego systemu na przyjmowanie napisanych po znaku zachęty (przeważnie znaku dolara $) poleceń. Powłoka jądra (domyślnie Bash) to język, który pozwala łączyć polecenia w algorytmiczne instrukcje i tłumaczy je w locie na wywołaia systemowe jądra. Plik z możliwością wykonania zapisanymi w nim instrukcjami powłoki nazywa się skryptem.
Aby dowiedzieć się w jakiej powłoce pracujemy napiszmy komendę:
$ echo $SHELL
/bin/bash

Plik, katalog, komenda

Wpisując słowo tty i naciskając Enter:

$ tty
/dev/ttys000
otrzymamy komunikat zwrotny a jego sens to:

Terminal z którego właśnie korzystasz jest plikem o nazwie ttys000. Znajduje się on w katalogu urządzeń dev a ten zaś w katalogu korzeniu /.

Każde urządzenie czy to rzeczywiste czy wirtualne jest przedstawiane użytkownikowi przez system jako plik znajdujący się w jakimś katalogu. Każdy katalog zawierający swoje pliki, tak naprawdę równieź jest plikiem przechwującym jedynie informacje jak te pliki znaleźć i co ci wolno z nimi zrobić. Plik zwykły to ciąg znaków zakończonych specjalną sekwencją oznaczoną symbolicznie EOF.

Zapytajmy o typ pliku ttys000:

$ file /dev/ttys000
/dev/ttys000: character special (16/0)
Polecenie file poinformowało, że plik, który reprezentuje terminal jest specjalnym plikiem znakowym. Istnieją też specjalne pliki blokowe urządzeń (np. pamięci masowe HDD lub SSD zanim prześlą dane najpierw buforują je w bloki)
Samo polecenie tty też jest plikiem ale wykonywalnym i zwykłym, czyli programem. Znajdźmy jego połżenie poleceniem which:
$ which tty
/usr/bin/tty
Ateraz sprawdźmy jego typ:
$ file /usr/bin/tty
/usr/bin/tty: Mach-O 64-bit executable x86_64
I rzeczywiście tty ma format 64-bitowego pliku wykonywalnego na maszynach o architekturze procesora x86_64 pod kontrolą jądra Mach. Sprawdźmy czy rzeczywiście takim komputerem dysponujemy:
$ machine
x86_64h
A czy odpowiednim systemem operacyjnum?:
$ uname
Darwin

Każde polecenie ma swoją systemową stronę pomocy wywoływaną poleceniem man np.:

$ man file
Przewijamy tekst strzałkami w górę i w dół. Przyciśnięnie klawisza q zamyka program.


Wielozadaniowość, wielodostępność, wieloprocesorowość

Jeden komputer z zainstalowanym systemem operacyjnym Unix pozwala wielu użytkownikom posiadającym na nim konta i zalogowanych np. zdalnie, na jednoczesne uruchomienie przez każdego z nich wielu programów, które stają się wtedy procesami. Procesy stoją w kolejkach o różnych priorytetach. Jądro kontroluje z której kolejki procesor ma na chwilę wykonać proces, by później się przełączyć do następnego co sprawi że każdy użytkownik ma wrażenie że jego procesy pracują jednoczeście. Jądro pilnuje też aby procesy nawzajem się nie podglądały jeśli to zabronione.
Spytajmy ile aktualnie jest procesów w systemie:
$ ps aux | wc -l
    362
Ta liczba ciągle się zmienia. Lepszym poleceniem do obserwacji na bieżąco procesów (a nawet obciążenia nimi procesorów logicznych, pamięci i partcji wymiany) jest polecenie:
$ htop -t
Dalej opiszę jak go zainstalować bo nie jest dostępne domyślnie w systemie.

Prawa

Każdy użytkownik ma do danego pliku przydzielone albo odebrane niezależnie trzy kolejne prawa:

Odebranie prawa to zastąpienie odpowiedniej literki znakiem minus. Można utworzyć osiem różnych kombinacji praw:
---, --x, -w-, -wx, r--, r-x, rw-, rwx
Zastąpmy literki cyfrą 1 a minus cyfrą 0, otrzymamy:
000, 001, 010, 011, 100, 101, 110, 111
Powstałe napisy możemy zinterpretować jako kolejne liczby w systemie dwójkowym, ponumerujmy je:
  0,   1,   2,   3,   4,   5,   6,   7
Otrzymaliśmy osiem cyfr, które dalej będziemy interpretować jako liczby w systemie ósemkowym. Każda taka jednocyfrowa liczba ósemkowa określa jednoznacznie zetaw praw użytkownika do pliku.

Użytkownik tworząc plik staje się jego właścicielem i ustala nad nim prawa, które można wyrazić trzycyfrową liczbą ósemkową:

Np. plik z pełnymi prawami dla wszystkich reprezentuje napis rwxrwxrwx lub równoważnie liczba 777.

Każdy użytkownik należy do co najmniej jednej grupy i dokładnie jedna z nich jest dla niego podstawowa. Użytkownik jest rozpoznawany w systemie przez liczbowy identyfikator uid skojarzony z loginem, podobnie jego grupa podstawowa identyfikatorem gid a pozostałe grupy są na liście identyfikatora groups:

$ id
uid=501(adal) gid=20(staff) groups=20(staff),12(everyone),61(localaccounts),79(_appserverusr),80(admin),81(_appserveradm),
98(_lpadmin),501(access_bpf),701(com.apple.sharepoint.group.1),33(_appstore),100(_lpoperator),204(_developer),
250(_analyticsusers),395(com.apple.access_ftp),398(com.apple.access_screensharing),399(com.apple.access_ssh)

Stwórzmy pusty plik:

$ > myfile
Właśnie przsłaliśmy pustkę do pliku a ponieważ plik nie istniał został stworzony bez zawartości.
Każdy plik ma swój unikatowy węzeł (i-node) przechowujący informacje o pliku. Przejrzyjmy zawartość węzła informacyjnego dla pliku myfile:
$ stat -x myfile
File: "myfile"
Size: 0            FileType: Regular File
Mode: (0644/-rw-r--r--)         Uid: (  501/    adal)  Gid: (   20/   staff)
Device: 1,4   Inode: 10975291    Links: 1
Access: Tue Jun 11 10:18:38 2019
Modify: Tue Jun 11 10:18:38 2019
Change: Tue Jun 11 10:18:38 2019
Otrzymane informacje opatrzone są etykietami:

Gdybyśmy chcieli otrzymać pełne uprawnienia do wszystkich plików musielibyśmy się zalogować jako administrator systemu poleceniem:

$ su -l root
Password:
Po wpisaniu prawidłowego hasła (nie widać go gdy go wpisujemy) system przenosi nas do katalogu /var/root a powłoka jądra zgłasza gotowość wykonania poleceń znakiem hash #. Na tym koncie nieświadomie można uszkodzić system - lepej się wylogujmy:
# exit
$

Polecenie su prównuje przekształcenie podanego hasła z napisem umieszczonym w pliku, którego jako użytkownik zwykły nie możemy czytać. Prawo setuid (s zamiast pierwszego x) pozwala wykonać polecenie su jak gdyby wykonywał go administrator a przecież stajemy się nim dopiero po wykonaniu tego polecenia

$ ls -l `which su`
-rwsr-xr-x  1 root  wheel  25488 May  4 09:03 /usr/bin/su

Polecenie write ma uprawnienia dla grupy tty my jednak do niej nie należymy, ale ponieważ plik ma ustawione prawo setgid (s zamiast drugiego x) to jesteśmy do niej przydzieleni tylko na czas wykonania tego polecenia.

$ ls -l `which write`
-r-xr-sr-x  1 root  tty  23936 May  4 09:02 /usr/bin/write

Bit lepki sticky bit (t zamiast ostatniego x) ustawiany dla katalogu czyni go współdzielonym aby użytkownicy mogli tworzyć w nim katalogi i pliki ale aby nie mogli sobie wzajemnie ich usuwać. Taką własność ma katalog /var/tmp:

$ ls -ld /var/tmp
drwxrwxrwt  5 root  wheel  160 Jun 13 10:00 /var/tmp


Czym innym jest wielkość pliku a czym innym wielkość obszaru zajmowanego przez ten plik na nośniku danych.

$ ls -sl myfile
0 -rw-r--r--  1 adal  staff  0 Jun 15 23:23 myfile
Pierwsze zero oznacza zerową ilość zaalokowanych bloków pamięci na nośniku, a drugie zerową ilość danych w pliku. Zapiszmy jeden bajt do pliku:
$ echo -n a > myfile
Teraz plik ma pojemność jednego bajta ale zajmuje 8 bloków:
$ ls -sl myfile
8 -rw-r--r--  1 adal  staff  1 Jun 15 23:30 myfile
Blok ma pojemność 512 bajtów. System plików APFS alokuje obszary po 8 bloków czyli 4096 bajtów. Dopiszmy do pliku 4095 bajtów:
$ for _ in {1..4095}; do echo -n a >> myfile ; done
$ ls -sl myfile
8 -rw-r--r--  1 adal  staff  4096 Jun 15 23:53 myfile
Ilość bloków się nie zmieniła. Dopiszmy jeszcze jeden bajt:
$ echo -n a >> myfile
$ ls -sl myfile
16 -rw-r--r--  1 adal  staff  4097 Jun 15 23:57 myfile
Mimo dopisania jednego bajta obszar na nośniku pamięci powiększył się o kolejne 8 bloków. Dodatkowo ilość plików możliwa do przechowania na nośniku pamięci ograniczona jest przez ilość i-węzłów zdeterminowana przez system plików. Raport wykorzystanych bloków i i-węzłów otrzymamy komendą df.


Programy dla macOS

Źródłem programów płatnych i darmowych kontrolowanym przez firmę Apple jest serwis App Store. Innym źródłem darmowych programów przetłumaczonych z wersji linuksowych jest serwis Homebrew. Aby z niego skorzystać należy wydać w konsoli polecenie:
$ /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
Aplikacje tekstowe można przeglądać na stronie https://formulae.brew.sh/formula a graficzne na stronie https://formulae.brew.sh/cask. Aby wyszukać program htop:
$ brew search htop
Wyświetlmy informacje o programie przed jego instalacją:
$ brew info htop
Aby zainstalować:
$ brew install htop
Aby odinstalować program:
$ brew uninstall htop
Aby odświerzyć bazę dostępnych programów i ich uaktualnień:
$ brew udate
Aby uaktualinić już zainstalowane programy:
$ brew upgrade

Języki programowania

Najnowszym językiem do tworzenia programów w systemie macOS i jego odmian jak: iOS, watchOS, tvOS, iPadOS oraz szkieletów do budowania serwisów internetowych jak Kitura (od IBM) lub Vapor jest Swift. Język Swift jest kompilowany: (czyli kod programu napisany przez programistę w całości musi być przetłumaczony na język maszynowy, znim prawidłowo zacznie go wykonywać procesor)

Podstawowym środowiskiem rozwoju oprogramowania dla tego języka jest Xcode pracujący w warstwie Aqua. Inny sposób to kompilacja z linii komend. Poprawnym programem będzie nawet skompilowany z pustego pliku.

$ touch myprogram.swift
$ swiftc -v myprogram.swift
Uruchomienie otrzymanego po kompilacji już niepustego pliku z kodem maszynowym nic nie robi:
$ ./myprogram
$


Przykłady algorytmów w języku Swift

Kilka zadań algorytmicznych, które rozwiązałem w języku Swift (na podstawie dostępnego fragmentu książki).

Rozwiązania można przetestować w serwisie kompilującym Swifta online. Na karcie main.swift wklejamy kod, gdzie w pierwszej linijce dołączamy standardową bibliotekę Swifta import Foundation). Dane wejściowe zapisujemy na karcie STDIN. Wykonanie programu następuje po kliknięciu pola Execute.

Napisz program, który oblicza sumę wszystkich liczb naturalnych podzielnych przez 3 lub 5 aż do podanej wartości granicznej wprowadzonej przez użytkownika:
print("InPut(Int):", terminator: " ")

guard let upNum = Int(readLine()!.trimmingCharacters(in: .whitespacesAndNewlines))
    else { exit(EXIT_FAILURE) }

let sum = (1...upNum).filter {$0 % 3 == 0 || $0 % 5 == 0} .reduce(0,+)
print(sum)

Napisz program, który obliczy i wyświetli największy wspólny dzielnik dwóch dodatnich liczb całkowitych.
Rekurencyjnie:
extension String: Error {}

let p = UInt(readLine()!)!
let q = UInt(readLine()!)!

func gcd(_ a: UInt, _ b: UInt) throws -> UInt {
    if a == 0 && b == 0 { throw "Both numbers are ZERO" }
    return b == 0 ? a : try gcd(b, a % b)
}

do {
    try print(gcd(p,q))
} catch let Err {
    print("ERROR: \(Err)")
}

Nierekurencyjnie za pomocą krotek:
let p = UInt(readLine()!)!
let q = UInt(readLine()!)!

func gcd(_ m: UInt, _ n: UInt) -> UInt {
    var (a, b) = (m, n)
    while b != 0 {
        (a, b) = (b, a % b)
    }
    return a
}

print(gcd(p,q))

Jako ostatni element sekwencji:
let p = UInt(readLine()!)!
let q = UInt(readLine()!)!

let gcd :(UInt, UInt) -> UInt = { sequence(first: ($0, $1), next: {$0.1 != 0 ? ($0.1, $0.0 % $0.1) : nil})
                                    .suffix(1).first!.0
                                }
print(gcd(p,q))

Testowanie pierwszości liczby:
extension Int {
    var isPrime: Bool {
        get {
                guard self > 3 else {return self > 1}
                guard self % 2 != 0 && self % 3 != 0 else {return false}
                for i in stride(from: 5, through: Int(Double(self).squareRoot()) + 1, by: 6) {
                if self % i == 0 || self % (i + 2) == 0 {return false}
            }
            return true
}}}

let n = UInt(readLine()!)!
print(n.isPrime)