Ports
Décision du plan 11 step 57A (2026-10-08). Elle remplace la décision du step 53 (pas de ports, processus) : les programmes disposent des ports tels qu'OTP les définit, et les E/S externes passent par eux. Les steps 57B–57F l'implémentent ; chaque section indique son step. Ce contrat fixe la représentation, le modèle de pilotes et le thread d'E/S avant qu'une source puisse ouvrir un port.
Implémenté : les identités, la table des ports, les builtins et messages de
ports, les liens, moniteurs, noms et signaux de sortie des ports, et les ports
fd en sortie seule (step 57B, runtime/src/scheduler/ports.cpp,
runtime/src/builtins/ports.cpp, runtime/src/ports/ ; golden OTP
executables_port_identities) ; le thread d'E/S et l'entrée des ports fd
avec découpage en flux, en paquets et en lignes (step 57C,
runtime/src/ports/io*.cpp ; golden OTP executables_port_input) ; les ports
de sous-processus, os:type/0, os:getenv/1 et os:cmd/1 (step 57D,
runtime/src/ports/spawn*.cpp, library/stdlib/os.erl ; golden OTP
executables_port_spawn) ; le pilote de fichiers, le sous-ensemble file de
la bibliothèque et l'entrée standard via io:get_line/io:get_chars
(step 57E, runtime/src/ports/file.cpp, library/stdlib/{file,io}.erl ;
golden OTP executables_file_io) ; les sockets (step 57F, golden OTP
executables_sockets) ; un thread d'E/S unique piloté par les événements
pour tous les types de ports (step 57G1, runtime/src/ports/reactor.cpp ;
golden OTP executables_many_ports, test du runtime runtime_port_io) ; les
tâches de ports sur les workers de l'ordonnanceur (step 57G2,
runtime/src/scheduler/ports.cpp ; golden OTP executables_port_fairness) ;
les ports occupés et l'entrée bornée (step 57G3 ; goldens OTP
executables_busy_ports, executables_slow_owner).
Identité
- Un port est un mot immédiat : étiquette
0x7(TermKind2::portsous l'étiquette primairesee_termkind2, à côté du0x3des pids), charge utile un numéro de port issu d'une séquence unique à l'échelle du processus qui n'est jamais réutilisée, comme les numéros de pid (pids). Un runtime n'admet que les numéros qu'il a émis ; les mots de port forgés ou étrangers sont rejetés. - Affichage :
#Port<0.N>;port_to_list/1donne ce texte etlist_to_port/1l'analyse (badarg pour toute autre chose). - Ordre : nombres < atomes < références < funs < ports < pids < tuples, comme dans OTP ; les ports se comparent par numéro.
- L'identité d'un port fermé reste valide :
is_port/1reste vrai,port_info/1,2répondentundefined, les envois vers lui sont abandonnés. - Les ports sont des immédiats, donc la copie, la collecte et les messages ne nécessitent rien de nouveau.
Table des ports et propriété
- Chaque runtime garde une table des ports (numéro → port) dans son exécuteur, protégée par le mutex de l'exécuteur comme les liens et les noms des processus (workers).
- Un port enregistre son pilote, son processus connecté (celui qui l'a ouvert,
changé par
port_connect/2ou{Pid, {connect, New}}), ses liens, les moniteurs posés sur lui, un nom enregistré optionnel, ses options et ses compteurs d'octets d'entrée/sortie. open_port/2lie le nouveau port au processus qui l'ouvre. Lorsque le processus connecté se termine, son port se ferme ; un port qui se ferme envoie des signaux de sortie à ses liens et des messages'DOWN'à ses moniteurs avec sa raison (normalpour une fermeture qui n'est pas une erreur, un atome POSIX tel queepipesinon).- Un signal de sortie qui atteint un port le ferme avec sa raison (
killdeexit/2devientkilled), saufnormalvia un lien depuis un processus autre que le processus connecté, qui ne fait que supprimer le lien.exit(Port, normal)ferme le port. Un processus connecté qui s'est délié laisse son port ouvert lorsqu'il se termine. - Un message de requête provenant d'un processus autre que le processus
connecté, ou un message mal formé, envoie au processus connecté un signal
de sortie
badsigdepuis le port ; le port reste ouvert. - Les signaux de sortie et les messages d'un port vers un processus qui
s'exécute sur un autre worker attendent la fin de la tranche de temps de ce
processus (ils ne contiennent que des atomes, des pids et des ports) ; un
exit(Port, Reason)avec un terme de raison dans le tas de l'expéditeur fait réexécuter le builtin jusqu'à ce qu'aucun processus lié ou surveillant ne s'exécute ailleurs, comme pour les signaux de processus (workers). - Les ports acceptent les noms enregistrés (
register/2) etmonitor(port, Port);link/1etunlink/1les acceptent.
Builtins et messages de ports
| Builtin ou message | Step |
|---|---|
is_port/1 vrai pour les ports, port_to_list/1, list_to_port/1, ports/0 | 57B |
port_info/1,2 (name, links, id, connected, input, output, os_pid, monitors, monitored_by, registered_name) | 57B |
port_close/1, port_connect/2, port_command/2,3 | 57B |
Port ! {Pid, {command, Data}}, {Pid, close}, {Pid, {connect, New}} | 57B |
link/1, unlink/1, monitor(port, P), exit/2 sur des ports ; register/2 d'un port | 57B |
port_control/3, port_call/3 | 57B (badarg pour les pilotes sans contrôle) ; utilisés par les pilotes de 57E/57F |
open_port({fd, In, Out}, Opts) | 57B pour la sortie ; entrée à partir de 57C |
open_port({spawn, Command} | {spawn_executable, File}, Opts) | 57D |
Pilotes internes de la bibliothèque du projet (file, sockets) | 57E, 57F |
Les builtins remplacent les diagnostics [ports] notimpl du step 53 ; la
fonctionnalité ports devient implémentée en 57B. Les erreurs suivent OTP :
badarg pour un port fermé ou invalide et pour de mauvais arguments, la
raison POSIX (enoent, eacces) comme error pour une ouverture qui échoue.
Les messages d'un port vont à son processus connecté :
{Port, {data, Data}}, {Port, eof} (option eof),
{Port, {exit_status, Status}} (option exit_status), {Port, closed}
(après {Pid, close}), {Port, connected} (à l'ancien propriétaire après une
connexion).
Modes de données et options
| Option | Effet |
|---|---|
stream (par défaut), {packet, N} (N = 1, 2, 4) | Octets tels qu'ils arrivent, ou messages délimités par une longueur big-endian de N octets que la sortie reçoit aussi |
{line, L} | {eol, Line} par ligne, {noeol, Part} pour les parties plus longues que L ou une fin non terminée |
binary | Données sous forme de binaries au lieu de listes d'octets |
eof | {Port, eof} en fin d'entrée ; le port reste ouvert jusqu'à sa fermeture |
exit_status | {Port, {exit_status, S}} lorsque le programme se termine (ports spawn) |
use_stdio (par défaut), nouse_stdio, stderr_to_stdout, in, out, hide | Comme dans OTP (hide n'a aucun effet) |
{args, List}, {arg0, A}, {env, Env}, {cd, Dir} | Ports spawn (57D) |
{busy_limits_port, {Low, High} | disabled} | Octets de sortie en file qui rendent le port occupé (ports occupés, 57G3) |
Sans eof, la fin de l'entrée ferme le port avec la raison normal, après le
message exit_status lorsqu'il a été demandé. Les options inconnues donnent
badarg.
Pilotes
Un pilote (driver) est un objet C++ derrière un port (runtime/src/ports/) :
il ouvre la ressource, accepte la sortie (port_command), répond à
port_control/3 lorsqu'il prend en charge le contrôle, signale l'entrée et
les erreurs sous forme d'événements et se ferme. Pilotes :
fd: descripteurs existants, sortie écrite de façon synchrone (57B), entrée via le thread d'E/S (57C).spawn: un programme enfant dont stdin et stdout sont des tubes (57D).file: un fichier ouvert du modulefilede la bibliothèque du projet ; ses opérations sont des appels synchrones àport_control/3qui peuvent bloquer le worker exécutant l'appelant pendant la durée des E/S disque, comme le font les ordonnanceurs dirty I/O d'OTP (57E).tcp_inet,udp_inet: sockets degen_tcp,gen_udpetinetde la bibliothèque du projet, sur Boost.Asio ; les connexions, acceptations et réceptions sont asynchrones (57F, sockets).
Les pilotes internes de file et des sockets sont ouverts avec
{spawn_driver, Name} sous des noms propres à Clause, et leurs opérations
port_control/3 forment un protocole de Clause : les programmes utilisent les
modules de la bibliothèque, pas les protocoles prim_inet/efile d'OTP.
Thread d'E/S
Un thread d'E/S par runtime (detail::Reactor,
runtime/src/ports/reactor.hpp, step 57G1) exécute un io_context de
Boost.Asio : un port de complétion d'E/S sous Windows, epoll sous Linux,
kqueue sous macOS. Le premier port qui en a besoin le démarre ; il sert tous
les types de ports, de sorte qu'un port ne coûte aucun thread propre :
- Tubes des programmes lancés : tubes nommés en mode overlapped sous Windows
(
windows::stream_handle; les tubes anonymes ne peuvent pas être en mode overlapped), tubes ordinaires ailleurs (posix::stream_descriptor). La sortie est mise en file et écrite dans l'ordre par des écritures asynchrones. - Entrée
fdsous Linux et macOS : le thread attend que le descripteur soit lisible, puis un seulread()prend ce qui s'y trouve, de sorte que les descripteurs propres du programme conservent leur mode bloquant ; un fichier ordinaire, qu'on ne peut pas attendre, est lu immédiatement. - Fins de programmes : sous Windows, le pool de threads d'attente du système
(
RegisterWaitForSingleObject, comme l'utiliseobject_handled'Asio) attend le handle du processus et la fin est signalée sur le thread d'E/S ; sous Linux et macOS, un gestionnaire deSIGCHLD(signal_set) etwaitpid(WNOHANG)récupèrent chaque programme surveillé, y compris après la fermeture de son port. - Sockets (sockets).
- Exception : un handle d'entrée
fdsous Windows (une console ou un tube anonyme hérité) ne peut pas être en mode overlapped, il est donc lu par un thread bloquant qui lui est propre, comme le font libuv et ERTS ; fermer le port annule la lecture (CancelSynchronousIo) et libère le thread.
runtime/src/ports/io*.cpp contiennent les E/S des ports
(detail::IoService) : ses méthodes ne font que poster du travail vers le
thread d'E/S, où réside tout l'état des E/S.
- Le thread d'E/S transmet l'entrée telle qu'elle a été lue : octets bruts, fin d'entrée, erreur de lecture, code de sortie d'un programme. Les événements de sockets (messages, connexions acceptées) sont transmis de la même façon.
Tâches de ports
Step 57G2. Un port est ordonnancé comme un processus : ce que le thread d'E/S transmet attend dans le port, et le port attend dans la file des ports de l'exécuteur jusqu'à ce qu'un worker de l'ordonnanceur exécute sa tâche.
- Les workers prennent tour à tour une tâche de port et une tranche de temps de processus tant que les deux files ont du travail, de sorte qu'un port inondé d'entrées ne peut pas affamer les processus, et que les processus ne peuvent pas affamer les ports ; un worker inactif prend ce qui est en file.
- Une tâche s'exécute sous le mutex de l'exécuteur pendant
PORT_TASK_REDUCTIONS(les 4 000 d'une tranche de temps) : chaque message qu'elle délivre coûte 100 réductions plus une par tranche de 64 octets transportés. Un port auquel il reste du travail est remis en fin de file. - La tâche découpe l'entrée (
InputDecoder) : l'entrée en flux arrive par les morceaux que renvoient les lectures ;{packet, N}retient les octets jusqu'à l'arrivée d'un paquet entier (un paquet incomplet en fin d'entrée est abandonné, comme dans OTP) ;{line, L}coupe à\n, envoie une ligne plus longue queLen morceaux{noeol, Part}et une fin non terminée en{noeol, Rest}.port_info(P, input)compte chaque octet lu, sauts de ligne et en-têtes de paquets compris, au moment où il est lu. - La fin de l'entrée envoie
{Port, eof}avec l'optioneof, sinon ferme le port avec la raisonnormal; une erreur de lecture le ferme aveceio. - Un message devient immédiatement un message pour son processus lorsque ce processus ne s'exécute pas sur un worker (en le réveillant comme tout message), sinon à la fin de sa tranche de temps, de sorte que le tas d'un processus en cours d'exécution n'est jamais touché par un autre thread.
- Les commandes, fermetures, connexions et signaux de sortie envoyés à un port
agissent immédiatement sur le worker de l'expéditeur, comme le fait ERTS
pour un port qui n'est pas occupé : un port
fdécrit sa sortie immédiatement sur le worker de l'appelant ; les pilotes de tubes et de sockets mettent la sortie en file et laissent le thread d'E/S l'écrire (57D, 57F, 57G1) ; un port occupé suspend l'expéditeur (ports occupés).
Le prototype tests/prototypes/poller/ (run.py --wsl) montre le réveil sous
Windows (port de complétion) et sous Linux WSL (poll()) : un thread
d'ordonnanceur inactif se réveille 9 à 91 µs après l'entrée, et l'arrêt
stoppe le thread d'E/S sans entrée.
À la fin du programme, chaque port est fermé (les programmes enfants voient la
fin de l'entrée ; ils ne sont pas tués, comme dans OTP) et le thread d'E/S
s'arrête : il est joint en dehors du mutex de l'exécuteur, car une livraison
en cours prend ce mutex ; un lecteur fd Windows bloqué dans une lecture qui
ne peut pas être annulée est détaché et ne délivre plus rien.
Ports occupés
Step 57G3. La sortie qu'un pilote met en file pour le thread d'E/S (tubes des
programmes lancés) est comptée par rapport aux limites d'occupation du port,
le busy_limits_port d'OTP (par défaut : haute 8 192 octets, basse 4 096 ;
{busy_limits_port, {Low, High}} ou disabled comme option de
open_port/2, limites d'au moins 1, une limite basse supérieure à la haute
étant abaissée à celle-ci) :
- Une écriture qui porte la sortie en file à la limite haute ou au-delà part quand même et rend le port occupé ; il reste occupé jusqu'à ce que le thread d'E/S en ait écrit assez pour que moins que la limite basse soit en file.
port_command/2etPort ! {Pid, {command, Data}}vers un port occupé suspendent l'expéditeur, qui n'écrit rien ; une fois que le port n'est plus occupé, ou qu'il se ferme, l'expéditeur réexécute son builtin (un port fermé donne alorsbadarg, comme dans OTP). Un processus suspendu reçoit toujours les signaux de sortie.port_command/3avecnosuspendrenvoiefalsepour un port occupé et n'écrit rien ;forcelèvenotsup, car les pilotes spawn etfdd'OTP ne le permettent pas.port_info(P, queue_size)est la sortie en file non encore écrite.- Les ports
fdécrivent immédiatement et la sortie des sockets passe parport_control/3, donc aucun des deux n'est jamais occupé.
Un port borne aussi l'entrée qu'il retient :
- Lorsque plus de 64 Kio d'entrée brute attendent la tâche du port, le thread d'E/S cesse de lire le port (un programme qui y écrit se bloque alors, comme sur un tube plein) jusqu'à ce que la tâche l'ait ramenée sous 32 Kio.
- Tant que le processus connecté peut s'exécuter (il est en cours
d'exécution ou en file, ni en attente dans un
receive, ni suspendu, ni bloqué) et détient déjà 1 024 messages (ou a autant de signaux de port en attente de la fin de sa tranche de temps), la tâche du port ne délivre plus rien jusqu'à la fin de la tranche de ce processus. Un processus en attente dans unreceivereçoit toujours l'entrée, de sorte qu'une réception d'un message ultérieur du port ne peut pas attendre indéfiniment.
Sous-processus
Step 57D. open_port({spawn, Command}, Options) et
open_port({spawn_executable, File}, Options) démarrent un programme dont
stdin et stdout sont des tubes du port :
{spawn, Command}: sous Linux et macOS,/bin/sh -c Command; sous Windows,CreateProcessWavecCommandcomme ligne de commande (il trouve le programme dans le chemin de recherche).{spawn_executable, File}exécuteFilesans shell, avec argv[0]{arg0, A}(par défautFile) et{args, List}; sous Windows, les arguments sont mis entre guillemets selon les règles deCommandLineToArgvW.{env, [{Name, Value | false}]}définit ou supprime des variables de l'enfant,{cd, Dir}son répertoire,stderr_to_stdoutfusionne stderr dans le port ; sinon l'enfant partage le stderr du programme.inn'ouvre aucun tube stdin etoutaucun tube stdout (le périphérique nul à la place).hideetoverlapped_ion'ont aucun effet.- Un programme qui ne peut pas être démarré lève
error:Reasonavec la raison POSIX (enoent,eacces,enoexec) ; les noms et options incorrects lèventbadarg. - La sortie est mise en file et écrite par le thread d'E/S, avec les limites d'occupation du port (ports occupés) ; fermer le port le laisse terminer ce qui est en file, puis ferme le stdin du programme. Le programme n'est jamais tué ; il se termine généralement en fin d'entrée.
- L'option
exit_statusenvoie{Port, {exit_status, S}}une fois le programme terminé (son code de sortie ; sous Linux et macOS, 128 plus le signal pour un programme terminé par un signal) et avant{Port, eof}ou la fermeture en fin d'entrée ; un port qui ne lit rien le signale lorsque le programme se termine, puis se ferme. port_info(P, os_pid)est l'identifiant de processus du programme ;nameest la commande ou le fichier.os:cmd/1de la bibliothèque exécuteCommandavecCOMSPEC /csous Windows (cmds'il n'est pas défini) ou/bin/sh -c, collecte stdout et stderr jusqu'à ce que le programme les ferme et renvoie les octets sous forme de liste ;os:type/0vaut{win32, nt},{unix, linux}ou{unix, darwin};os:getenv/1renvoie une chaîne oufalse.
E/S standard et fichiers
Step 57E.
io:format,io:put_charseterlang:displaycontinuent d'écrire directement sur la sortie standard (io). L'entrée standard est lue par un unique processus serveur de la bibliothèque, enregistré sousclause_stdinet démarré à la première utilisation, qui possède un port{fd, 0, 1}:io:get_line/1,2écrit l'invite, puis renvoie la ligne suivante avec son saut de ligne, le reste de l'entrée sans saut de ligne à la fin, puiseof;io:get_chars/2,3renvoie jusqu'àNcaractères, puiseof. Les requêtes de plusieurs processus sont servies dans l'ordre d'arrivée. L'entrée est renvoyée sous forme d'octets (un élément de liste par octet).- Le module
filede la bibliothèque pilote le pilote de fichiers du runtime,{spawn_driver, "clause_file"}, avecport_control/3. Ses opérations (ports/file.cpp: open, read, write, position, read_line, close, et les opérations sur les chemins read_file, write_file, delete, rename, list_dir, make_dir, del_dir) sont des appels système synchrones sur le worker de l'appelant, en dehors du mutex de l'exécuteur ; leurs réponses commencent par un octet de statut (0 ok, 1 erreur suivie de sa raison POSIX, 2 fin de fichier). Le protocole est propre à Clause. file:open/2renvoie le pid d'un serveur d'E/S qui possède un port de fichier et est lié au processus qui l'ouvre (modesread,write,append,exclusive,binary; les autres modes sont ignorés, comme OTP ignore les options qu'il n'utilise pas).read/2,read_line/1,write/2,position/2etio:get_line/2,io:get_chars/3sur ce pid sont des requêtes à ce serveur ; aprèsclose/1, les requêtes renvoient{error, terminated}etclose/1resteok.- Les erreurs suivent OTP :
{error, enoent},eexist,eisdir(un répertoire ouvert ou lu comme un fichier),einval(une position négative),ebadf(lecture d'un fichier en écriture seule ou écriture d'un fichier en lecture seule),badargpour les noms et données incorrects. Les noms de fichiers sont des chaînes, des binaries ou des atomes, encodés en UTF-8. - Un module référencé par le programme qui est un builtin du catalogue
(
io:format) n'entraîne plus le module de bibliothèque de ce nom dans le programme ; seul un appel à l'une de ses fonctions Erlang le fait.
Sockets (57F)
Une socket est un port, comme avec le backend inet_drv d'OTP :
is_port(Socket) est vrai et les messages du mode actif sont
{tcp, Socket, Data}, {tcp_closed, Socket},
{tcp_error, Socket, Reason}, {udp, Socket, Address, Port, Data}. Le
sous-ensemble : gen_tcp:connect/3,4, listen/2, accept/1,2, send/2,
recv/2,3, close/1, controlling_process/2, shutdown/2 ;
gen_udp:open/1,2, send/4, recv/2,3, close/1 ; inet:setopts/2,
inet:port/1, inet:peername/1, inet:sockname/1 ; modes
{active, true | false | once}, binary/list,
{packet, 0 | 1 | 2 | 4 | raw}, {reuseaddr, Bool}, {backlog, N},
{ip, Address}/{ifaddr, Address}, inet/inet6 ; adresses IPv4 et IPv6
sous forme de tuples, noms d'hôtes sous forme de chaînes ou d'atomes
(loopback compris). Les options de réglage nodelay, keepalive,
send_timeout, send_timeout_close, delay_send et exit_on_close sont
acceptées et non appliquées ; toute autre option donne exit(badarg), comme
pour une option invalide dans OTP.
Implémentation (runtime/src/ports/sockets.cpp) :
- Le thread d'E/S du runtime (thread d'E/S) sert les sockets ;
l'état de chaque socket réside sur ce thread. La bibliothèque ouvre un port
avec
{spawn_driver, "tcp_inet" | "udp_inet"}et le pilote avecport_control/3; le worker appelant poste l'opération vers le thread d'E/S et attend sa réponse synchrone (octet de statut 0 et un résultat, ou 1 et une raison POSIX). - Les opérations qui attendent (connect, accept, recv) répondent plus tard par
un message
{clause_socket, Socket, Reply}à leur appelant, de sorte que l'appelant se bloque dans unreceiveordinaire et que les autres processus continuent de s'exécuter. Un délai d'attente annule la requête ; l'annulation elle-même répondcancelledaprès tout ce que la socket a envoyé auparavant, de sorte qu'une réponse arrivée la première est renvoyée au lieu d'être perdue. - Les messages de sockets sont décrits hors tas (
ports/value.hpp) et construits dans le tas du destinataire lors de leur livraison, selon les règles de l'exécuteur décrites dans thread d'E/S. Une connexion acceptée devient un nouveau port appartenant à l'appelant d'acceptet lié à lui, avec le mode de la socket d'écoute. controlling_process/2arrête la livraison active, déplace vers le nouveau propriétaire les messages de la socket déjà envoyés à l'ancien, puis reconnecte le port, comme le faitinetd'OTP ; seul le propriétaire peut l'appeler ({error, not_owner}).- La sortie est mise en file sans plafond et écrite dans l'ordre sur le
thread d'E/S. Fermer un port envoie ce qui est en file, puis ferme la
connexion proprement (FIN) ; un port fermé répond
{error, closed}à chaque appelant en attente. La résolution de noms s'exécute sur le worker de l'appelant, car elle est bloquante. - Les ports de sockets sont ordonnancés comme les autres ports (tâches de ports) : les messages d'une socket attendent dans son port jusqu'à ce que la tâche du port les délivre.
Non fourni
Les pilotes liés (linked-in drivers) et les NIF,
erlang:open_port({spawn_driver, Name}) pour les noms de pilotes d'OTP, les
ports de distribution, port_call/3 sur les pilotes fournis en dehors du
protocole propre à la bibliothèque, les ports de sockets occupés, et les
options overlapped_io, parallelism et busy_limits_msgq.
Clause