Ports
Programme erhalten Ports so, wie OTP sie definiert, und externe I/O läuft über
sie. Die Port-Tabelle, die Exit-Regeln und die Port-Tasks stehen in
runtime/src/scheduler/ports.cpp, die Builtins in
runtime/src/builtins/erlang/ports.cpp, die Treiber und der I/O-Thread in
runtime/src/ports/. OTP-Goldens: executables_port_identities,
executables_port_input, executables_port_spawn, executables_file_io,
executables_sockets, executables_many_ports, executables_port_fairness,
executables_busy_ports, executables_slow_owner; Runtime-Test
runtime_port_io.
Identität
- Ein Port ist ein Immediate-Wort: Tag
0x7(TermKind2::portunter dem primären Tagsee_termkind2, neben0x3der Pids), als Nutzlast eine Port-Nummer aus einer prozessweiten Folge, die nie wiederverwendet wird, wie bei Pid-Nummern (Pids). Eine Runtime akzeptiert nur die Nummern, die sie selbst vergeben hat; gefälschte und fremde Port-Wörter werden abgewiesen. - Ausgabe:
#Port<0.N>;port_to_list/1liefert diesen Text undlist_to_port/1parst ihn (badarg für alles andere). - Ordnung: Zahlen < Atome < Referenzen < Funs < Ports < Pids < Tupel, wie in OTP; Ports werden nach Nummer verglichen.
- Die Identität eines geschlossenen Ports bleibt gültig:
is_port/1bleibt wahr,port_info/1,2antwortenundefined, Sendungen an ihn werden verworfen. - Ports sind Immediates, daher brauchen Kopieren, Speicherbereinigung und Nachrichten nichts Neues.
Port-Tabelle und Besitz
- Jede Runtime führt in ihrem Executor eine Port-Tabelle (Nummer → Port), geschützt durch den Executor-Mutex wie die Links und Namen der Prozesse (Worker).
- Ein Port speichert seinen Treiber, seinen verbundenen Prozess (den Öffner,
geändert durch
port_connect/2oder{Pid, {connect, New}}), seine Links, die auf ihn gehaltenen Monitore, einen optionalen registrierten Namen, seine Optionen und seine Ein-/Ausgabe-Bytezähler. open_port/2verlinkt den neuen Port mit seinem Öffner. Endet der verbundene Prozess, schließt sich sein Port; ein Port, der sich schließt, sendet Exit-Signale an seine Links und'DOWN'-Nachrichten an seine Monitore mit seinem Grund (normalfür ein Schließen, das kein Fehler ist, sonst ein POSIX-Atom wieepipe).- Ein Exit-Signal, das einen Port erreicht, schließt ihn mit seinem Grund
(
killausexit/2alskilled), außernormalüber einen Link von einem anderen als dem verbundenen Prozess, was nur den Link entfernt.exit(Port, normal)schließt den Port. Ein verbundener Prozess, der den Link gelöst hat, lässt seinen Port offen, wenn er endet. - Eine Anfrage-Nachricht von einem anderen als dem verbundenen Prozess oder eine
fehlerhafte Nachricht sendet dem verbundenen Prozess ein Exit-Signal
badsigvom Port; der Port bleibt offen. - Exit-Signale und Nachrichten eines Ports an einen Prozess, der auf einem
anderen Worker läuft, warten, bis dessen Zeitscheibe endet (sie enthalten nur
Atome, Pids und Ports); ein
exit(Port, Reason)mit einem Grund-Term im Heap des Senders lässt das Builtin erneut laufen, bis kein verlinkter oder überwachender Prozess anderswo läuft, wie bei Prozesssignalen (Worker). - Ports nehmen registrierte Namen (
register/2) undmonitor(port, Port)an;link/1undunlink/1akzeptieren sie.
Builtins und Port-Nachrichten
is_port/1(wahr für Ports),port_to_list/1,list_to_port/1,ports/0.port_info/1,2:name,links,id,connected,input,output,os_pid,monitors,monitored_by,registered_name,queue_size(ausgelastete Ports).port_close/1,port_connect/2,port_command/2,3; die NachrichtenPort ! {Pid, {command, Data}},{Pid, close},{Pid, {connect, New}}.link/1,unlink/1,monitor(port, P),exit/2auf Ports;register/2eines Ports.port_control/3,port_call/3:badargfür Treiber ohne Steuerung; die Treiberfileund Socket werden überport_control/3gesteuert.open_port({fd, In, Out}, Opts),open_port({spawn, Command}, Opts),open_port({spawn_executable, File}, Opts)und die internen Treiber der Projektbibliothek (file, Sockets).
Fehler folgen OTP: badarg für einen geschlossenen oder ungültigen Port und
ungültige Argumente, der POSIX-Grund (enoent, eacces) als error für ein
fehlgeschlagenes Öffnen.
Nachrichten eines Ports gehen an seinen verbundenen Prozess:
{Port, {data, Data}}, {Port, eof} (Option eof),
{Port, {exit_status, Status}} (Option exit_status), {Port, closed} (nach
{Pid, close}), {Port, connected} (an den alten Besitzer nach einem
Connect).
Datenmodi und Optionen
| Option | Wirkung |
|---|---|
stream (Standard), {packet, N} (N = 1, 2, 4) | Bytes, wie sie ankommen, oder Nachrichten, gerahmt durch eine N Byte lange Big-Endian-Länge, die auch die Ausgabe erhält |
{line, L} | {eol, Line} pro Zeile, {noeol, Part} für Teile länger als L oder ein nicht abgeschlossenes Ende |
binary | Daten als Binaries statt Byte-Listen |
eof | {Port, eof} am Ende der Eingabe; der Port bleibt offen, bis er geschlossen wird |
exit_status | {Port, {exit_status, S}}, wenn das Programm endet (Spawn-Ports) |
use_stdio (Standard), nouse_stdio, stderr_to_stdout, in, out, hide | Wie in OTP (hide hat keine Wirkung) |
{args, List}, {arg0, A}, {env, Env}, {cd, Dir} | Spawn-Ports (Unterprozesse) |
{busy_limits_port, {Low, High} | disabled} | Gepufferte Ausgabe-Bytes, die den Port auslasten (ausgelastete Ports) |
Ohne eof schließt das Ende der Eingabe den Port mit Grund normal, nach der
Nachricht exit_status, falls diese angefordert wurde. Unbekannte Optionen
ergeben badarg.
Treiber
Ein Treiber ist ein C++-Objekt hinter einem Port (runtime/src/ports/): Er
öffnet die Ressource, nimmt Ausgabe an (port_command), beantwortet
port_control/3, wenn er Steuerung unterstützt, meldet Eingabe und Fehler als
Ereignisse und schließt. Treiber:
fd: vorhandene Deskriptoren, Ausgabe synchron geschrieben, Eingabe über den I/O-Thread.spawn: ein Kindprogramm mit stdin und stdout als Pipes (Unterprozesse).file: eine geöffnete Datei des Modulsfileder Projektbibliothek; seine Operationen sind synchroneport_control/3-Aufrufe, die den Worker, der den Aufrufer ausführt, für die Dauer der Platten-I/O blockieren können, wie es die Dirty-I/O-Scheduler von OTP tun (Dateien).tcp_inet,udp_inet: Sockets vongen_tcp,gen_udpundinetder Projektbibliothek auf Boost.Asio; Verbindungsaufbau, Annehmen und Empfangen sind asynchron (Sockets).clause_registry(Windows): die Registry vonwin32reg(die Registry).
Die internen Treiber von file, den Sockets und der Registry werden mit
{spawn_driver, Name} unter Clause-Namen geöffnet, und ihre
port_control/3-Operationen sind ein Clause-Protokoll: Programme nutzen die
Bibliotheksmodule, nicht die Protokolle prim_inet/efile von OTP.
I/O-Thread
Ein I/O-Thread pro Runtime (detail::Reactor, runtime/src/ports/reactor.hpp)
betreibt einen Boost.Asio-io_context: einen I/O Completion Port unter
Windows, epoll unter Linux, kqueue unter macOS. Der erste Port, der ihn
braucht, startet ihn; er bedient jede Port-Art, sodass ein Port keinen eigenen
Thread kostet:
- Pipes gestarteter Programme: überlappende Named Pipes unter Windows
(
windows::stream_handle; anonyme Pipes können nicht überlappend sein), sonst gewöhnliche Pipes (posix::stream_descriptor). Ausgabe wird gepuffert und durch asynchrone Schreibvorgänge in Reihenfolge geschrieben. fd-Eingabe unter Linux und macOS: Der Thread wartet, bis der Deskriptor lesbar ist, dann nimmt einread(), was vorhanden ist, sodass die eigenen Deskriptoren des Programms ihren blockierenden Modus behalten; eine reguläre Datei, auf die nicht gewartet werden kann, wird sofort gelesen.- Programmenden: Unter Windows wartet der Wait-Thread-Pool des Systems
(
RegisterWaitForSingleObject, wie ihnobject_handlevon Asio nutzt) auf das Prozess-Handle, und das Ende wird auf dem I/O-Thread gemeldet; unter Linux und macOS sammeln einSIGCHLD-Handler (signal_set) undwaitpid(WNOHANG)jedes überwachte Programm ein, auch nachdem sein Port geschlossen wurde. - Sockets (Sockets).
- Ausnahme: Ein
fd-Eingabe-Handle unter Windows (eine Konsole oder eine geerbte anonyme Pipe) kann nicht überlappend sein, daher liest es ein eigener blockierender Thread, wie bei libuv und ERTS; das Schließen des Ports bricht das Lesen ab (CancelSynchronousIo) und gibt den Thread frei.
runtime/src/ports/io*.cpp enthalten die Port-I/O (detail::IoService): Ihre
Methoden übergeben Arbeit nur an den I/O-Thread, wo der gesamte I/O-Zustand
liegt. Der I/O-Thread übergibt Eingabe so, wie sie gelesen wurde: rohe Bytes,
das Ende der Eingabe, einen Lesefehler, den Exit-Status eines Programms.
Socket-Ereignisse (Nachrichten, angenommene Verbindungen) werden auf die gleiche
Weise übergeben.
Der Prototyp tests/prototypes/poller/ (run.py --wsl) zeigt das Aufwecken
unter Windows (Completion Port) und WSL Linux (poll()): Ein untätiger
Scheduler-Thread wacht 9–91 µs nach der Eingabe auf, und das Herunterfahren
stoppt den I/O-Thread ohne Eingabe.
Am Programmende wird jeder Port geschlossen (Kindprogramme sehen das Ende der
Eingabe; sie werden nicht beendet, wie in OTP), und der I/O-Thread stoppt: Er
wird außerhalb des Executor-Mutex gejoint, weil eine laufende Zustellung ihn
nimmt; ein Windows-fd-Leser, der in einem nicht abbrechbaren Lesevorgang
blockiert, wird abgekoppelt und stellt nichts mehr zu.
Port-Tasks
Ein Port wird wie ein Prozess eingeplant: Was der I/O-Thread übergibt, wartet im Port, und der Port wartet in der Port-Warteschlange des Executors, bis ein Scheduler-Worker seinen Task ausführt.
- Worker nehmen abwechselnd einen Port-Task und eine Prozess-Zeitscheibe, solange beide Warteschlangen Arbeit haben, sodass ein Port, der mit Eingabe flutet, Prozesse nicht aushungern kann und Prozesse Ports nicht aushungern können; ein untätiger Worker nimmt, was gerade ansteht.
- Ein Task läuft unter dem Executor-Mutex für
PORT_TASK_REDUCTIONS(die 4.000 einer Zeitscheibe): Jede Nachricht, die er zustellt, kostet 100 Reduktionen plus eine pro 64 Bytes, die sie trägt. Ein Port mit verbleibender Arbeit wird hinten erneut eingereiht. - Der Task rahmt die Eingabe (
InputDecoder): Stream-Eingabe kommt in den Blöcken an, die Lesevorgänge liefern;{packet, N}hält Bytes zurück, bis ein ganzes Paket angekommen ist (ein unvollständiges Paket am Ende der Eingabe wird verworfen, wie in OTP);{line, L}trennt bei\n, sendet eine Zeile, die länger alsList, als{noeol, Part}-Stücke und ein nicht abgeschlossenes Ende als{noeol, Rest}.port_info(P, input)zählt jedes gelesene Byte, Zeilenumbrüche und Paket-Header eingeschlossen, zum Zeitpunkt des Lesens. - Das Ende der Eingabe sendet
{Port, eof}mit Optioneof, sonst schließt es den Port mit Grundnormal; ein Lesefehler schließt ihn miteio. - Eine Nachricht wird sofort zu einer Nachricht an ihren Prozess, wenn dieser Prozess nicht auf einem Worker läuft (sie weckt ihn wie jede Nachricht), sonst wenn seine Zeitscheibe endet, sodass der Heap eines laufenden Prozesses nie von einem anderen Thread berührt wird.
- Commands, Schließen, Connects und Exit-Signale, die an einen Port gesendet
werden, wirken sofort auf dem Worker des Senders, wie ERTS es für einen nicht
ausgelasteten Port tut: Ein
fd-Port schreibt seine Ausgabe sofort auf dem Worker des Aufrufers; die Pipe- und Socket-Treiber puffern Ausgabe und lassen sie vom I/O-Thread schreiben; ein ausgelasteter Port suspendiert den Sender (ausgelastete Ports).
Ausgelastete Ports
Ausgabe, die ein Treiber für den I/O-Thread puffert (Pipes gestarteter
Programme), zählt gegen die Auslastungsgrenzen des Ports, busy_limits_port von
OTP (Standardwerte: hoch 8.192 Bytes, niedrig 4.096;
{busy_limits_port, {Low, High}} oder disabled als Option von
open_port/2, Grenzen mindestens 1, eine niedrige Grenze über der hohen wird
auf diese gesenkt):
- Ein Schreibvorgang, der die gepufferte Ausgabe auf die hohe Grenze oder darüber bringt, geht noch hinaus und lastet den Port aus; er bleibt ausgelastet, bis der I/O-Thread genug geschrieben hat, dass weniger als die niedrige Grenze gepuffert ist.
port_command/2undPort ! {Pid, {command, Data}}an einen ausgelasteten Port suspendieren den Sender, der nichts schreibt; sobald der Port nicht mehr ausgelastet ist oder sich schließt, führt der Sender sein Builtin erneut aus (ein geschlossener Port ergibt dannbadarg, wie in OTP). Ein suspendierter Prozess empfängt weiterhin Exit-Signale.port_command/3mitnosuspendgibt für einen ausgelasteten Portfalsezurück und schreibt nichts;forcelöstnotsupaus, da die Spawn- undfd-Treiber von OTP es nicht erlauben.port_info(P, queue_size)ist die gepufferte, noch nicht geschriebene Ausgabe.fd-Ports schreiben sofort, und Socket-Ausgabe geht überport_control/3, daher ist keiner von beiden je ausgelastet.
Ein Port begrenzt auch die Eingabe, die er hält:
- Wenn mehr als 64 KiB rohe Eingabe auf den Task des Ports warten, hört der I/O-Thread auf, den Port zu lesen (ein Programm, das in ihn schreibt, blockiert dann wie bei einer vollen Pipe), bis der Task sie unter 32 KiB gebracht hat.
- Solange der verbundene Prozess laufen kann (er läuft oder steht in der
Warteschlange, wartet nicht in einem
receive, ist nicht suspendiert oder blockiert) und bereits 1.024 Nachrichten hält (oder so viele Port-Signale hat, die auf das Ende seiner Zeitscheibe warten), stellt der Task des Ports nichts mehr zu, bis die Zeitscheibe dieses Prozesses geendet hat. Ein Prozess, der in einemreceivewartet, erhält die Eingabe immer, sodass ein receive auf eine spätere Nachricht des Ports nicht ewig warten kann.
Unterprozesse
open_port({spawn, Command}, Options) und
open_port({spawn_executable, File}, Options) starten ein Programm, dessen
stdin und stdout Pipes des Ports sind:
{spawn, Command}: unter Linux und macOS/bin/sh -c Command; unter WindowsCreateProcessWmitCommandals Befehlszeile (es findet das Programm im Suchpfad).{spawn_executable, File}führtFileohne Shell aus, mit argv[0]{arg0, A}(StandardFile) und{args, List}; unter Windows werden Argumente nach den Regeln vonCommandLineToArgvWgequotet.{env, [{Name, Value | false}]}setzt oder entfernt Variablen des Kindes,{cd, Dir}sein Verzeichnis,stderr_to_stdoutführt stderr in den Port zusammen; andernfalls teilt das Kind die stderr des Programms.inallein öffnet keine stdin-Pipe undoutallein keine stdout-Pipe (stattdessen das Null-Gerät); mit beiden oder keiner der Optionen hat der Port beide Pipes, wie in OTP.hideundoverlapped_iohaben keine Wirkung.- Ein Programm, das nicht gestartet werden kann, löst
error:Reasonmit dem POSIX-Grund aus (enoent,eacces,enoexec); ungültige Namen und Optionen lösenbadargaus. - Ausgabe wird gepuffert und vom I/O-Thread geschrieben, mit den Auslastungsgrenzen des Ports (ausgelastete Ports); das Schließen des Ports lässt ihn das Gepufferte fertig schreiben und schließt dann die stdin des Programms. Das Programm wird nie beendet; es endet normalerweise am Ende der Eingabe.
- Option
exit_statussendet{Port, {exit_status, S}}, sobald das Programm geendet hat (sein Exit-Code; unter Linux und macOS 128 plus das Signal für ein Programm, das durch ein Signal beendet wurde), und vor{Port, eof}oder dem Schließen am Ende der Eingabe; ein Port, der nichts liest, meldet es, wenn das Programm endet, und schließt sich dann. port_info(P, os_pid)ist die Prozess-ID des Programms;nameist der Befehl oder die Datei.os:cmd/1der Bibliothek führtCommandunter Windows mitCOMSPEC /caus (cmd, wenn nicht gesetzt) oder mit/bin/sh -c, sammelt stdout und stderr, bis das Programm sie schließt, und gibt die Bytes als Liste zurück;os:type/0ist{win32, nt},{unix, linux}oder{unix, darwin};os:getenv/1gibt einen String oderfalsezurück.
Standard-I/O und Dateien
io:format,io:put_charsunderlang:displayschreiben direkt auf die Standardausgabe (io). Die Standardeingabe liest ein einziger Server-Prozess der Bibliothek, registriert alsclause_stdinund bei erster Nutzung gestartet, der einen{fd, 0, 1}-Port besitzt:io:get_line/1,2schreibt den Prompt und gibt dann die nächste Zeile mit ihrem Zeilenumbruch zurück, den Rest der Eingabe ohne einen solchen an seinem Ende, danneof;io:get_chars/2,3gibt bis zuNZeichen zurück, danneof. Anfragen mehrerer Prozesse werden in Eingangsreihenfolge beantwortet. Eingabe wird als UTF-8 in Zeichen dekodiert, wie OTP sein Terminal liest; der Server hält die noshell-Optionen von OTP fürstandard_io(binary,encoding,echo,log;io:getopts/0,1,io:setopts/1,2): Der binary-Modus gibt binaries zurück,echoschreibt verbrauchte Eingabe auf die Standardausgabe,get_passwordergibt ohne Shell{error, enotsup}, wie bei OTP.- Dateiserver beantworten
getoptsundsetoptswie der Datei-I/O-Server von OTP (binary/listundencoding;file:open/2nimmt{encoding, E}an): I/O-Anfragen (io:get_line,get_chars,put_chars,get_until) kodieren und dekodieren die Gerätekodierung, undfile:read/2,read_line/1auf einer kodierten Datei antworten wie latin1-Anfragen anget_chars/get_line(no_translationüber 255, was den Server stoppt). Ausgabe vonio:format/3,io:put_chars/2,io:nl/1an eine beliebige Pid ist eineput_chars-Anfrage im I/O-Protokoll. - Das Modul
fileder Bibliothek steuert den Dateitreiber der Runtime,{spawn_driver, "clause_file"}, mitport_control/3. Seine Operationen (ports/file.cpp: open, read, write, position, read_line, sync, close sowie die Pfadoperationen read_file, write_file, delete, rename, list_dir, make_dir, del_dir und read_file_info,ports/file_info.cpp; get_cwd, set_cwd, read_link, read_link_info, make_link und make_symlink,ports/file_paths.cpp) sind synchrone Systemaufrufe auf dem Worker des Aufrufers, außerhalb des Executor-Mutex; ihre Antworten beginnen mit einem Status-Byte (0 ok, 1 Fehler und sein POSIX-Grund, 2 Dateiende). Das Protokoll ist Clauses eigenes. read_line liest 64 KB im Voraus und gibt Zeilen aus diesem Puffer zurück; jede andere Operation auf der Datei springt zuerst über die noch nicht zurückgegebenen Bytes zurück, sodass Positionen exakt bleiben. - Pfadoperationen führen die Systemaufrufe von OTP aus und keine weiteren: Der
Status eines Pfads wird erst gelesen, wenn ein Öffnen oder Löschen scheitert
(um
eisdiroderepermzurückzugeben); delete entfernt den Link einer Datei und nie ein Verzeichnis, und read_file liest eine Datei bis zu der von ihrem Deskriptor gemeldeten Größe, wie OTP, ohne ihr Ende durch Lesen zu prüfen. port_control/3übergibt ein binary oder[]direkt an den Treiber (andere iodata werden zuerst abgeflacht), und eine Antwort wird ohne Kopie zum Ergebnis-binary: Antworten sindByteBuffers (runtime/include/byte_buffer.hpp), Byte-Vektoren, die ohne Nullen wachsen, sodass ein Lesevorgang die Bytes seiner Antwort einmal füllt.file:open/2gibt die Pid eines I/O-Servers zurück, der einen Datei-Port besitzt und sich schließt, wenn der Öffner endet, wie bei OTP (unter Windows ist die Datei zum Lesen, Schreiben und Löschen freigegeben, wie OTP sie öffnet, sodass eine geöffnete Datei gelöscht oder umbenannt werden kann) (Modiread,write,append,exclusive,binary,raw; andere Modi werden ignoriert, wie OTP Optionen ignoriert, die es nicht nutzt;writekürzt die Datei, sofern nicht auchappendangegeben ist).file:write_file/3öffnet mit seinen Modi plusbinaryundwrite;file:read_file_info/1,2geben das#file_info{}von OTP aus dem Status des Pfads zurück (Links werden verfolgt), mit Zeiten als lokales oder universelles Datum und Uhrzeit oder POSIX-Sekunden ({time, local | universal | posix};rawwird akzeptiert). Unter Windows sindinode,uidundgid0, undmajor_deviceist die Laufwerksnummer, wie OTP sie meldet.read/2,read_line/1,write/2,position/2undio:get_line/2,io:get_chars/3darauf sind Anfragen an diesen Server; nachclose/1geben Anfragen{error, terminated}zurück, undclose/1bleibtok.- Positionierte I/O und Dateiänderungen:
pread/2,3undpwrite/2,3positionieren auf einem Dateiserver und lesen oder schreiben dann (curliest weiter, die Position verschiebt sich), wie der Server von OTP; eineraw-Datei nimmt nur ganzzahlige Offsets an (sonstbadarg) und behält ihre Position.truncate/1kürzt an der Position;advise/4nimmt die Hinweis-Atome von OTP an (posix_fadviseunter Linux, anderswo ignoriert, wie bei OTP);allocate/3reserviert Platz (fallocate, macOSF_PREALLOCATE,{error, enotsup}unter Windows), wobei seine Argumente zuerst geprüft werden (badargfür Nichtzahlen,einvalfür negative Werte und Floats). Wie der Dateiserver von OTP antwortet ein Server, dessen Schreiben, positioniertes Schreiben, sync, truncate oder advice scheitert, mit dem Fehler und endet, sodass spätere Anfragen{error, terminated}erhalten; eine raw-Datei läuft weiter.write_file_info/2,3undchange_mode/2,change_owner/2,3,change_group/2,change_time/2,3ändern Besitzer, Berechtigungen und Zeiten in der Reihenfolge von prim_file (Windows: Besitzer ignoriert, nur das Schreibschutzattribut aus dem Schreibbit des Besitzers, auch die Erstellungszeit wird gesetzt).sendfile/2,5lesen eine raw-Datei in Blöcken (höchstens 1 MiB,chunk_size) und senden sie mitgen_tcp:send/2oder dem angegebenen fun, dem Rückfallpfad von OTP: Clause hat kein natives sendfile.format_error/1liefert die Texte von OTP, POSIX-Texte auserl_posix_msg:message/1. - Zeilen und fehlgeschlagene Lesevorgänge:
read_line/1undio:get_linelesen ein CRLF-Ende als LF (die Position zählt das CR), wie OTP; ein Dateiserver, dessen Lesevorgang scheitert, antwortet mit dem Fehler und endet, und einer, dessen Zeilenlesen scheitert, endet ohne Antwort (der Aufrufer erhält{error, terminated}), wie bei OTP. compressedundcompressed_one: Schreiben läuft durch einen gzip-deflate-Stream (zlib, window bits 31), der beim Schließen abgeschlossen wird, auch wenn der Server stoppt oder sein Öffner endet; Lesen dekomprimiert eine Datei, die mit den Kennbytes von gzip beginnt (compressedliest verkettete Teile als einen,compressed_onestoppt nach dem ersten), und liest jede andere Datei unverändert. Positionen zählen unkomprimierte Bytes: vorwärts durch Lesen oder Schreiben von Nullen, rückwärts nur beim Lesen (erneut vom Anfang an),eofergibteinval. Lesen einer zum Schreiben geöffneten Datei oder Schreiben einer zum Lesen geöffneten ergibtebadf;allocate/3und bei raw-Dateien positionierte I/O ergebenenotsup; ein Dateiserver stoppt nachtruncate,adviseundsync(enotsup), wie bei OTP. Gleichzeitiges Lesen und Schreiben oder Anhängen ergibt{error, einval}.{zstd, Options}: wiecompressed, über einen mitOptionserzeugten zstd-Kontext ({error, badarg}, wenn er sie ablehnt): Jeder Schreibvorgang ist ein eigener Frame, und Schließen fügt einen leeren hinzu, wie die Schicht von OTP schreibt; Lesen dekomprimiert die in 8-KiB-Blöcken gelesenen Frames.ram: Die Datei ist ihr iodata-Inhalt im Speicher, von einem Prozess wie andere Dateien bedient und mit ihrem Öffner geschlossen; nur die Modiread,writeundbinary(read, wenn keiner angegeben ist, alles anderebadarg,rawmitramlöstbadargaus). Lesen, Schreiben, Positionen,pread/pwrite(die Position bleibt erhalten),truncate,sync,adviseundallocateantworten wie der ram-Dateitreiber von OTP: Schreiben hinter dem Ende füllt die Lücke mit Nullen, Offsets und Größen jenseits von 32 Bit mit Vorzeichen ergebeneinval, Schreiben einer nur lesbaren Datei ergibtebadf, ihr Kürzeneacces.- Das I/O-Protokoll von OTP: Der Standardeingabe-Server und der Server jeder
geöffneten Datei beantworten
{io_request, From, ReplyAs, Request}mit{io_reply, ReplyAs, Reply}, wasio:request/1,2undio:requests/1,2senden (standard_iounduserbezeichnen den Standardeingabe-Server, ein anderes Atom einen registrierten Prozess;{error, arguments}, wenn er nicht registriert ist,{error, calling_self}für den Aufrufer selbst). Anfragen:get_until(M:F(Cont, Chars | eof, Extra...)abCont = [], gespeist mit dem Gepufferten oder mit Dateiblöcken von 4 KiB bis{done, Result, Rest};Restwird von der nächsten Anfrage erneut gelesen: im Puffer behalten, oder bei einer Datei wird die Position darüber zurückbewegt),get_line,get_chars,put_chars(Zeichen oderM:F(A)), die alten Formen ohne Kodierung (latin1) und{requests, List}(der Reihe nach, mit Abbruch beim ersten{error, _}). Zeichen sind Bytes, wie oben; eine Datei im Binärmodus gibt ein Listenergebnis vonget_untilals Binary zurück. Andere erhalten{error, {request, Request}}. Eine fehlschlagendeget_until- oderput_chars-Funktion lässt den Server einer Datei{error, Function}antworten und mit dem Fehler enden, wie es der von OTP tut; der Öffner lebt weiter (der Dateiserver von OTP ist nicht mit ihm verlinkt). Eingabeanfragen auf der Standardeingabe schreiben ihren Prompt. - Verzeichnisse, Links und Kopien:
get_cwd/0schreibt das aktuelle Verzeichnis wie OTP (Schrägstriche, ein kleingeschriebener Laufwerksbuchstabe),get_cwd/1nimmt ein Windows-Laufwerk (enotsupanderswo);set_cwd/1ändert es für das ganze Programm;read_link/1,read_link_all/1(einvalfür einen Pfad, der kein Link ist),read_link_info/1,2(der eigene Status eines letzten Links: POSIXlstat, unter Windows die Felder des Ziels mit Typsymlink),make_link/2,make_symlink/2(braucht unter Windows ein Privileg:eperm, wie OTP),list_dir_all/1,del_dir_r/1,copy/2,3(Namen,{Name, Modes}oder geöffnete Dateien;{ok, BytesCopied}),raw_read_file_info/1,read_file/2,delete/2,native_name_encoding/0(utf8).del_dir/1antwortetenoentfür einen fehlenden Pfad undenotdirfür eine Datei. - Das
filevon Clause ist die Dateischicht;filelibundfilenamevon Clause laufen darauf (filelib:fold_files/5nutztre);erl_prim_loaderwird überfilebereitgestellt, mit den bloßenerror-Antworten von OTP und einem Loader-Pfad von["."](set_path/1), seine Lade- und Archivfunktionen werden abgelehnt;prim_fileundprim_bufferfehlen weiterhin, sodass die Aufrufer vonraw_file_io*aus OTP nie in ein Programm gelangen. - Fehler folgen OTP:
{error, enoent},eexist,eisdir(ein Verzeichnis, das als Datei geöffnet oder gelesen wird),einval(eine negative Position;badargfür eine Angabe, die keine Position ist),ebadf(Lesen einer nur schreibbaren oder Schreiben einer nur lesbaren Datei),badargfür ungültige Namen und Daten. Dateinamen sind Strings, Binaries oder Atome, kodiert als UTF-8.
Sockets
Ein Socket ist ein Port, wie beim inet_drv-Backend von OTP:
is_port(Socket) ist wahr, und Nachrichten im aktiven Modus sind
{tcp, Socket, Data}, {tcp_closed, Socket}, {tcp_error, Socket, Reason},
{udp, Socket, Address, Port, Data}. Die Teilmenge:
gen_tcp:connect/2,3,4 (Host und Port oder eine Socket-Adress-map),
listen/2, accept/1,2, send/2, recv/2,3, close/1,
controlling_process/2, shutdown/2; gen_udp:open/1,2, connect/2,3,
send/2,3,4,5 (die Zielformen und Zusatzdaten von OTP), recv/2,3,
close/1; inet:setopts/2, getopts/2, getstat/1,2, info/1, i/0,1,2,
monitor/1, cancel_monitor/1, port/1, peername/1, peernames/1,2,
sockname/1, socknames/1,2; Modi {active, true | false | once},
binary/list ({mode, M}), {packet, 0 | 1 | 2 | 4 | raw}, {backlog, N},
{ip, Address}/{ifaddr, Address}, inet/inet6; IPv4- und IPv6-Adressen
als Tupel, Hostnamen als Strings oder Atome (loopback eingeschlossen).
Optionen jenseits des Datenmodus (ports/socket_options.cpp,
library/kernel/clause_socket_options.erl) sind Treiber-records aus einem
Code und einem Wert, angegeben beim Öffnen eines Sockets (gesetzt vor dem
Binden oder Verbindungsaufbau) oder durch setopts/2; jede Option eines
setopts/2-Aufrufs wird geprüft, bevor eine gesetzt wird. Die Systemoptionen
(reuseaddr, keepalive, dontroute, linger, broadcast, sndbuf,
recbuf, nodelay, tos, tclass, ipv6_v6only, multicast_ttl,
multicast_loop, multicast_if, add_membership, drop_membership,
exclusiveaddruse unter Windows, priority und reuseport, wo das System
sie anbietet) gehen an setsockopt und werden mit getsockopt zurückgelesen;
die Optionen, die inet_drv von OTP selbst hält (buffer, header,
deliver, exit_on_close, send_timeout, Wasserstandsmarken,
packet_size, ...), werden mit den Standardwerten von inet_drv gehalten und
nicht angewendet (Unterschiede). Wie bei inet_drv erhöht ein
Empfangspuffer buffer, bis es gesetzt wird, und getopts/2 lässt Optionen
aus, die einem Socket-Typ fehlen (broadcast bei einem TCP-Socket, nodelay
bei einem UDP-Socket, tclass bei IPv4), und weist unbekannte mit
{error, einval} ab. Eine angenommene Verbindung übernimmt die Optionen ihres
lauschenden Sockets. send/5 mit Zusatzdaten setzt tos, tclass oder ttl
nur für dieses Datagramm; unter Windows ergibt es {error, einval}, wie OTP.
Jeder Socket zählt seinen Datenverkehr für getstat/1,2 wie inet_drv:
Empfangene Pakete sind Lesevorgänge (TCP) oder Datagramme, gesendete Pakete sind
send-Aufrufe (mit ihrem Paket-Header) oder Datagramme, mit den gleitenden
Durchschnittswerten von inet_drv; send_pend ist die gepufferte und noch nicht
geschriebene Ausgabe. getstatus liefert die Status-Flags von inet_drv
(open, connected, connecting, listen, accepting), die info/1 und
i/0,1,2 mit den Namen und der Modulspalte von OTP (inet_tcp, inet6_udp,
...) ausgeben. Abfragen eines geschlossenen Sockets antworten
{error, einval}, wie OTP.
Das Modul socket (library/kernel/socket.erl) nutzt dieselben Treiber-Ports
im passiven binary-Modus. Ein Stream-Socket kann vor dem Lauschen oder
Verbindungsaufbau gebunden werden (Operation 22 öffnet und bindet den
System-Socket, den listen zum Acceptor macht oder connect verbindet); ein
Datagramm-Socket wird durch udp_open gebunden, implizit bei seinem ersten
Senden. Das Ende einer Verbindung beantwortet jedes wartende recv (auch
eines, das mehr Bytes anfordert, als übrig sind). socket hält die Bytes, die
ein recv nicht übernommen hat, in seinem eigenen Puffer, sodass recv/2,3,4
die Antworten {error, {timeout, Data}} und {error, {closed, Data}} von OTP
liefert.
Implementierung (runtime/src/ports/sockets.cpp):
- Der I/O-Thread der Runtime (I/O-Thread) bedient die Sockets;
der Zustand jedes Sockets liegt auf diesem Thread. Die Bibliothek öffnet einen
Port mit
{spawn_driver, "tcp_inet" | "udp_inet"}und steuert ihn mitport_control/3; der aufrufende Worker übergibt die Operation an den I/O-Thread und wartet auf dessen synchrone Antwort (Status-Byte 0 und ein Ergebnis oder 1 und ein POSIX-Grund). - Wartende Operationen (connect, accept, recv) antworten später mit einer
Nachricht
{clause_socket, Socket, Reply}an ihren Aufrufer, sodass der Aufrufer in einem gewöhnlichenreceiveblockiert und andere Prozesse weiterlaufen. Ein Timeout bricht die Anfrage ab; der Abbruch selbst antwortetcancellednach allem, was der Socket zuvor gesendet hat, sodass eine Antwort, die das Rennen gewonnen hat, zurückgegeben statt verloren wird. - Socket-Nachrichten werden außerhalb des Heaps beschrieben
(
ports/value.hpp) und bei der Zustellung im Heap des Empfängers aufgebaut (Port-Tasks). Eine angenommene Verbindung wird zu einem neuen Port, der dem Aufrufer vonacceptgehört und mit ihm verlinkt ist, mit dem Modus des lauschenden Sockets. controlling_process/2stoppt die aktive Zustellung, verschiebt die bereits an den alten Besitzer gesendeten Nachrichten des Sockets zum neuen und verbindet dann den Port neu, wie esinetvon OTP tut; nur der Besitzer darf es aufrufen ({error, not_owner}).- Ausgabe wird ohne Obergrenze gepuffert und auf dem I/O-Thread in Reihenfolge
geschrieben. Das Schließen eines Ports sendet das Gepufferte und schließt dann
die Verbindung geordnet (FIN); ein geschlossener Port antwortet jedem
wartenden Aufrufer
{error, closed}. Die Namensauflösung läuft auf dem Worker des Aufrufers, da sie blockiert. - Socket-Ports werden wie andere Ports eingeplant (Port-Tasks): Die Nachrichten eines Sockets warten in seinem Port, bis der Task des Ports sie zustellt.
- Adressen und Hosts:
inet:parse_address/1,2,parse_strict_address/1,2,parse_ipv4_address/1,parse_ipv4strict_address/1,parse_ipv6_address/1,parse_ipv6strict_address/1(die toleranten Formen von OTP: kurze sowie oktale oder hexadezimale IPv4-Felder,::, IPv4-Enden,%scopevonfe80::undff02::),ntoa/1(die Schreibweise von OTP: kleingeschriebenes Hexadezimal, die erste längste Nullfolge als::),is_ipv4_address/1,is_ipv6_address/1,is_ip_address/1sind Erlang;gethostname/0(eine Socket-Operation),getaddr/2,getaddrs/2(der Resolver des Systems,getaddrinfo) undgethostbyname/1,2(h_nameist der angefragte Name, keine Aliasse) antworten über einen für die Anfrage geöffneten Socket-Port. - Dienste, Rückwärtsauflösung und Schnittstellen (
inet:getservbyname/2,getservbyport/2,gethostbyaddr/1,getifaddrs/0,1) sind mitnetgeteilteclause_net-Builtins (Netzwerknamen). gen_tcp,gen_udp,inetundnetvon Clause sind die Netzwerkschicht; das Modulsocketvon OTP (die Socket-NIF-API) fehlt, ebensoprim_socket,prim_net,prim_inetundsocket_registry.
Die Registry
win32reg öffnet {spawn_driver, "clause_registry"} unter Windows
(runtime/src/ports/registry_windows.cpp). Der Treiber hält den Zustand des
Registry-Treibers von OTP (den angeforderten Zugriff, den aktuellen Schlüssel
und seine Wurzel, anfangs HKEY_CLASSES_ROOT) und beantwortet die neun Befehle
von OTP als synchrone port_control/3-Operationen mit den Antworten, die der
Treiber von OTP als Port-Daten sendet: o, e und der errno-Name
(Registry-Schäden ergeben eio, ein gelöschter Schlüssel einval, andere
Windows-Fehler werden wie von der C-Runtime abgebildet), k und ein
Schlüsselname, v, der Typ, Name und die Daten (Strings ohne ihre abschließende
Null, DWORDs Big-Endian), s und der aktuelle Schlüssel. Auflistungen geben
jede Antwort zurück, jeweils mit ihrer 32-Bit-Größe davor. Namen und Strings
nutzen die ANSI-Codepage, wie beim Treiber von OTP.
Nicht bereitgestellt
Linked-in-Treiber und NIFs, erlang:open_port({spawn_driver, Name}) für
OTP-Treibernamen, Distributions-Ports, port_call/3 auf den bereitgestellten
Treibern außer dem eigenen Protokoll der Bibliothek, ausgelastete Socket-Ports
sowie die Optionen overlapped_io, parallelism und busy_limits_msgq.
Clause