Порти
Рішення плану 11, крок 57A (2026-10-08). Воно замінює рішення кроку 53 (без портів, процеси): програми отримують порти в тому вигляді, як їх визначає OTP, і зовнішній ввід-вивід проходить через них. Кроки 57B–57F це реалізують; кожен розділ називає свій крок. Цей контракт визначає представлення, модель драйверів і потік вводу-виводу до того, як будь-яке джерело зможе відкрити порт.
Реалізовано: ідентичності, таблицю портів, вбудовані функції й повідомлення
портів, зв'язки (links), монітори, імена й сигнали виходу портів, а також
порти fd лише для виводу (крок 57B, runtime/src/scheduler/ports.cpp,
runtime/src/builtins/ports.cpp, runtime/src/ports/; еталон OTP
executables_port_identities); потік вводу-виводу і ввід портів fd з
потоковим, пакетним і рядковим фреймуванням (крок 57C,
runtime/src/ports/io*.cpp; еталон OTP executables_port_input); порти
підпроцесів, os:type/0, os:getenv/1 і os:cmd/1 (крок 57D,
runtime/src/ports/spawn*.cpp, library/stdlib/os.erl; еталон OTP
executables_port_spawn); файловий драйвер, підмножину file бібліотеки і
стандартний ввід через io:get_line/io:get_chars (крок 57E,
runtime/src/ports/file.cpp, library/stdlib/{file,io}.erl; еталон OTP
executables_file_io); сокети (крок 57F, еталон OTP executables_sockets);
один подієвий потік вводу-виводу для портів усіх видів (крок 57G1,
runtime/src/ports/reactor.cpp; еталон OTP executables_many_ports, тест
runtime runtime_port_io); завдання портів на робочих потоках планувальника
(крок 57G2, runtime/src/scheduler/ports.cpp; еталон OTP
executables_port_fairness); зайняті порти й обмежений ввід (крок 57G3;
еталони OTP executables_busy_ports, executables_slow_owner).
Ідентичність
- Порт — це безпосереднє (immediate) слово: тег
0x7(TermKind2::portпід первинним тегомsee_termkind2, поряд із0x3для pid), корисне навантаження — номер порту з однієї загальнопроцесної послідовності, що ніколи не використовується повторно, як і номери pid (pid). Runtime приймає лише номери, які сам видав; підроблені й чужі слова портів відхиляються. - Друк:
#Port<0.N>;port_to_list/1дає цей текст, аlist_to_port/1розбирає його (badarg для будь-чого іншого). - Порядок: числа < атоми < посилання < fun < порти < pid < кортежі, як в OTP; порти порівнюються за номером.
- Ідентичність закритого порту лишається дійсною:
is_port/1лишається істинним,port_info/1,2відповідаютьundefined, надіслане йому відкидається. - Порти є безпосередніми значеннями, тож копіювання, збирання сміття й повідомлення не потребують нічого нового.
Таблиця портів і володіння
- Кожен runtime тримає таблицю портів (номер → порт) у своєму виконавці (executor), захищену м'ютексом виконавця, як і зв'язки та імена процесів (робочі потоки).
- Порт зберігає свій драйвер, свій під'єднаний процес (той, що відкрив порт;
змінюється через
port_connect/2або{Pid, {connect, New}}), свої зв'язки, монітори на ньому, необов'язкове зареєстроване ім'я, свої опції та лічильники вхідних і вихідних байтів. open_port/2зв'язує новий порт із процесом, що його відкрив. Коли під'єднаний процес завершується, його порт закривається; порт, що закривається, надсилає сигнали виходу своїм зв'язкам і повідомлення'DOWN'своїм моніторам зі своєю причиною (normalдля закриття, що не є помилкою, інакше атом POSIX, як-отepipe).- Сигнал виходу, що досягає порту, закриває його зі своєю причиною (
killзexit/2якkilled), окрімnormalчерез зв'язок від процесу, відмінного від під'єднаного, що лише видаляє зв'язок.exit(Port, normal)закриває порт. Під'єднаний процес, що розірвав зв'язок, залишає свій порт відкритим, коли завершується. - Повідомлення-запит від процесу, відмінного від під'єднаного, або
некоректне повідомлення надсилає під'єднаному процесу сигнал виходу
badsigвід порту; порт лишається відкритим. - Сигнали виходу й повідомлення порту процесу, що виконується на іншому
робочому потоці, чекають кінця кванту часу цього процесу (вони містять лише
атоми, pid і порти);
exit(Port, Reason)з термом причини в купі відправника змушує вбудовану функцію виконуватися знову, доки жоден зв'язаний чи моніторний процес не виконується деінде, як і для сигналів процесів (робочі потоки). - Порти приймають зареєстровані імена (
register/2) іmonitor(port, Port);link/1іunlink/1приймають їх.
Вбудовані функції й повідомлення портів
| Вбудована функція або повідомлення | Крок |
|---|---|
is_port/1 істинна для портів, 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 для портів; register/2 порту | 57B |
port_control/3, port_call/3 | 57B (badarg для драйверів без керування); використовуються драйверами 57E/57F |
open_port({fd, In, Out}, Opts) | 57B вивід; ввід з 57C |
open_port({spawn, Command} | {spawn_executable, File}, Opts) | 57D |
Внутрішні драйвери бібліотеки проєкту (file, сокети) | 57E, 57F |
Вбудовані функції замінюють діагностику [ports] notimpl кроку 53; функція
(feature) ports стає реалізованою в 57B. Помилки відповідають OTP: badarg
для закритого чи недійсного порту й неправильних аргументів, причина POSIX
(enoent, eacces) як error для невдалого відкриття.
Повідомлення від порту йдуть його під'єднаному процесу:
{Port, {data, Data}}, {Port, eof} (опція eof),
{Port, {exit_status, Status}} (опція exit_status), {Port, closed}
(після {Pid, close}), {Port, connected} (старому власникові після
під'єднання).
Режими даних і опції
| Опція | Ефект |
|---|---|
stream (типово), {packet, N} (N = 1, 2, 4) | Байти в міру надходження або повідомлення, обрамлені N-байтовою довжиною в порядку big-endian, яку отримує й вивід |
{line, L} | {eol, Line} для кожного рядка, {noeol, Part} для частин, довших за L, або для незавершеного кінця |
binary | Дані як binary замість списків байтів |
eof | {Port, eof} наприкінці вводу; порт лишається відкритим, доки його не закриють |
exit_status | {Port, {exit_status, S}}, коли програма завершується (порти spawn) |
use_stdio (типово), nouse_stdio, stderr_to_stdout, in, out, hide | Як в OTP (hide не має ефекту) |
{args, List}, {arg0, A}, {env, Env}, {cd, Dir} | Порти spawn (57D) |
{busy_limits_port, {Low, High} | disabled} | Кількість байтів виводу в черзі, що робить порт зайнятим (зайняті порти, 57G3) |
Без eof кінець вводу закриває порт із причиною normal, після
повідомлення exit_status, якщо його було запитано. Невідомі опції дають
badarg.
Драйвери
Драйвер — це об'єкт C++ за одним портом (runtime/src/ports/): він відкриває
ресурс, приймає вивід (port_command), відповідає на port_control/3, якщо
підтримує керування, повідомляє про ввід і помилки як про події та
закривається. Драйвери:
fd: наявні дескриптори, вивід пишеться синхронно (57B), ввід — через потік вводу-виводу (57C).spawn: дочірня програма, чиї stdin і stdout є каналами (pipes) (57D).file: відкритий файл модуляfileбібліотеки проєкту; його операції — це синхронні викликиport_control/3, що можуть блокувати робочий потік, на якому виконується процес, що викликає, на час дискового вводу-виводу, як це роблять брудні (dirty) планувальники вводу-виводу OTP (57E).tcp_inet,udp_inet: сокети модулівgen_tcp,gen_udpіinetбібліотеки проєкту на Boost.Asio; з'єднання, прийняття з'єднань і отримання асинхронні (57F, сокети).
Внутрішні драйвери file і сокетів відкриваються через
{spawn_driver, Name} під іменами Clause, а їхні операції port_control/3
утворюють протокол Clause: програми використовують модулі бібліотеки, а не
протоколи OTP prim_inet/efile.
Потік вводу-виводу
Один потік вводу-виводу на runtime (detail::Reactor,
runtime/src/ports/reactor.hpp, крок 57G1) виконує io_context Boost.Asio:
порт завершення вводу-виводу (I/O completion port) на Windows, epoll на
Linux, kqueue на macOS. Його запускає перший порт, якому він потрібен; він
обслуговує порти всіх видів, тож порт не коштує окремого потоку:
- Канали запущених програм: перекриті (overlapped) іменовані канали на
Windows (
windows::stream_handle; анонімні канали не можуть бути перекритими), звичайні канали деінде (posix::stream_descriptor). Вивід ставиться в чергу й пишеться по порядку асинхронними записами. - Ввід
fdна Linux і macOS: потік чекає, доки дескриптор стане доступним для читання, потім одинread()забирає наявне, тож власні дескриптори програми зберігають свій блокувальний режим; звичайний файл, на який не можна чекати, читається одразу. - Завершення програм: на Windows системний пул потоків очікування
(
RegisterWaitForSingleObject, як використовуєobject_handleв Asio) чекає на дескриптор процесу, а про завершення повідомляється в потоці вводу-виводу; на Linux і macOS обробникSIGCHLD(signal_set) іwaitpid(WNOHANG)збирають кожну відстежувану програму, також після закриття її порту. - Сокети (сокети).
- Виняток: вхідний дескриптор
fdна Windows (консоль або успадкований анонімний канал) не може бути перекритим, тож його читає окремий блокувальний потік, як у libuv і ERTS; закриття порту скасовує читання (CancelSynchronousIo) і відпускає потік.
runtime/src/ports/io*.cpp містять ввід-вивід портів (detail::IoService):
його методи лише передають роботу потоку вводу-виводу, де живе весь стан
вводу-виводу.
- Потік вводу-виводу передає ввід у тому вигляді, як його прочитано: сирі байти, кінець вводу, помилку читання, код завершення програми. Події сокетів (повідомлення, прийняті з'єднання) передаються так само.
Завдання портів
Крок 57G2. Порт планується як процес: те, що передає потік вводу-виводу, чекає в порту, а порт чекає в черзі портів виконавця, доки робочий потік планувальника не виконає його завдання.
- Робочі потоки по черзі беруть завдання порту й квант часу процесу, поки обидві черги мають роботу, тож порт, що заливає вводом, не може заморити процеси голодом, а процеси не можуть заморити порти; вільний робочий потік бере те, що є в черзі.
- Завдання виконується під м'ютексом виконавця протягом
PORT_TASK_REDUCTIONS(4 000 одного кванту часу): кожне доставлене повідомлення коштує 100 редукцій плюс одну за кожні 64 байти, які воно несе. Порт, у якого лишилася робота, знову стає в кінець черги. - Завдання фреймує ввід (
InputDecoder): потоковий ввід надходить шматками, які повертають читання;{packet, N}утримує байти, доки не надійде цілий пакет (неповний пакет наприкінці вводу відкидається, як в OTP);{line, L}розбиває за\n, надсилає рядок, довший заL, частинами{noeol, Part}, а незавершений кінець — як{noeol, Rest}.port_info(P, input)рахує кожен прочитаний байт, включно з переведеннями рядка й заголовками пакетів, у момент читання. - Кінець вводу надсилає
{Port, eof}з опцієюeof, інакше закриває порт із причиноюnormal; помилка читання закриває його зeio. - Повідомлення стає повідомленням своєму процесу одразу, коли цей процес не виконується на робочому потоці (будячи його, як будь-яке повідомлення), інакше — коли закінчується його квант часу, тож купи процесу, що виконується, ніколи не торкається інший потік.
- Команди, закриття, під'єднання й сигнали виходу, надіслані порту, діють
одразу на робочому потоці відправника, як ERTS робить для незайнятого
порту: порт
fdпише свій вивід одразу на робочому потоці процесу, що викликає; драйвери каналів і сокетів ставлять вивід у чергу й дають потоку вводу-виводу записати його (57D, 57F, 57G1); зайнятий порт призупиняє відправника (зайняті порти).
Прототип tests/prototypes/poller/ (run.py --wsl) показує пробудження на
Windows (порт завершення) і WSL Linux (poll()): вільний потік планувальника
прокидається через 9–91 мкс після вводу, а завершення роботи зупиняє потік
вводу-виводу без вводу.
Наприкінці програми кожен порт закривається (дочірні програми бачать кінець
вводу; їх не вбивають, як і в OTP), а потік вводу-виводу зупиняється: його
приєднують (join) поза м'ютексом виконавця, бо доставка, що триває, бере
цей м'ютекс; читач fd на Windows, заблокований у читанні, яке не можна
скасувати, від'єднується й більше нічого не доставляє.
Зайняті порти
Крок 57G3. Вивід, який драйвер ставить у чергу для потоку вводу-виводу
(канали запущених програм), враховується в межах зайнятості порту,
busy_limits_port в OTP (типово: верхня 8 192 байти, нижня 4 096;
{busy_limits_port, {Low, High}} або disabled як опція open_port/2,
межі щонайменше 1, нижня межа понад верхню знижується до неї):
- Запис, що доводить вивід у черзі до верхньої межі чи вище, все одно виконується й робить порт зайнятим; він лишається зайнятим, доки потік вводу-виводу не запише достатньо, щоб у черзі лишилося менше за нижню межу.
port_command/2іPort ! {Pid, {command, Data}}до зайнятого порту призупиняють відправника, який нічого не пише; щойно порт перестає бути зайнятим або закривається, відправник знову виконує свою вбудовану функцію (закритий порт тоді даєbadarg, як в OTP). Призупинений процес і далі отримує сигнали виходу.port_command/3зnosuspendповертаєfalseдля зайнятого порту й нічого не пише;forceпороджуєnotsup, бо драйвери spawn іfdв OTP цього не дозволяють.port_info(P, queue_size)— це вивід у черзі, ще не записаний.- Порти
fdпишуть одразу, а вивід сокетів іде черезport_control/3, тож жоден із них ніколи не буває зайнятим.
Порт також обмежує ввід, який утримує:
- Коли понад 64 КіБ сирого вводу чекає на завдання порту, потік вводу-виводу перестає читати порт (програма, що пише в нього, тоді блокується, як на заповненому каналі), доки завдання не знизить обсяг нижче 32 КіБ.
- Поки під'єднаний процес може виконуватися (виконується або стоїть у черзі,
а не чекає в
receive, не призупинений і не заблокований) і вже має 1 024 повідомлення (або стільки ж сигналів портів, що чекають кінця його кванту часу), завдання порту більше нічого не доставляє, доки квант цього процесу не закінчиться. Процес, що чекає вreceive, завжди отримує ввід, тож receive на пізніше повідомлення порту не може чекати вічно.
Підпроцеси
Крок 57D. open_port({spawn, Command}, Options) і
open_port({spawn_executable, File}, Options) запускають програму, чиї stdin
і stdout є каналами порту:
{spawn, Command}: на Linux і macOS/bin/sh -c Command; на WindowsCreateProcessWзCommandяк командним рядком (він знаходить програму в шляху пошуку).{spawn_executable, File}запускаєFileбез оболонки, з argv[0]{arg0, A}(типовоFile) і{args, List}; на Windows аргументи беруться в лапки за правиламиCommandLineToArgvW.{env, [{Name, Value | false}]}встановлює чи видаляє змінні дочірнього процесу,{cd, Dir}— його каталог,stderr_to_stdoutзливає stderr у порт; інакше дочірній процес ділить stderr програми.inне відкриває каналу stdin, аout— каналу stdout (натомість нульовий пристрій).hideіoverlapped_ioне мають ефекту.- Програма, яку не вдається запустити, породжує
error:Reasonз причиною POSIX (enoent,eacces,enoexec); неправильні імена й опції породжуютьbadarg. - Вивід ставиться в чергу й пишеться потоком вводу-виводу з урахуванням меж зайнятості порту (зайняті порти); закриття порту дає йому дописати те, що в черзі, а потім закриває stdin програми. Програму ніколи не вбивають; зазвичай вона завершується наприкінці вводу.
- Опція
exit_statusнадсилає{Port, {exit_status, S}}, щойно програма завершилася (її код виходу; на Linux і macOS 128 плюс номер сигналу для програми, яку завершив сигнал), і до{Port, eof}чи закриття наприкінці вводу; порт, що нічого не читає, повідомляє його, коли програма завершується, а потім закривається. port_info(P, os_pid)— це ідентифікатор процесу програми;name— це команда або файл.os:cmd/1бібліотеки запускаєCommandчерезCOMSPEC /cна Windows (cmd, якщо не задано) або/bin/sh -c, збирає stdout і stderr, доки програма їх не закриє, і повертає байти як список;os:type/0— це{win32, nt},{unix, linux}або{unix, darwin};os:getenv/1повертає рядок абоfalse.
Стандартний ввід-вивід і файли
Крок 57E.
io:format,io:put_charsіerlang:displayі далі пишуть у стандартний вивід безпосередньо (io). Стандартний ввід читає один серверний процес бібліотеки, зареєстрований якclause_stdinі запущений при першому використанні, якому належить порт{fd, 0, 1}:io:get_line/1,2пише запрошення, потім повертає наступний рядок із його переведенням рядка, залишок вводу без нього наприкінці, потімeof;io:get_chars/2,3повертає доNсимволів, потімeof. Запити кількох процесів обслуговуються в порядку надходження. Ввід повертається як байти (один елемент списку на байт).- Модуль
fileбібліотеки керує файловим драйвером runtime,{spawn_driver, "clause_file"}, черезport_control/3. Його операції (ports/file.cpp: open, read, write, position, read_line, close, і операції над шляхами read_file, write_file, delete, rename, list_dir, make_dir, del_dir) — це синхронні системні виклики на робочому потоці процесу, що викликає, поза м'ютексом виконавця; їхні відповіді починаються з байта стану (0 — ok, 1 — помилка та її причина POSIX, 2 — кінець файлу). Протокол власний для Clause. file:open/2повертає pid сервера вводу-виводу, якому належить файловий порт і який зв'язаний із процесом, що відкрив файл (режимиread,write,append,exclusive,binary; інші режими ігноруються, як OTP ігнорує опції, яких не використовує).read/2,read_line/1,write/2,position/2таio:get_line/2,io:get_chars/3над ним — це запити до цього сервера; післяclose/1запити повертають{error, terminated}, аclose/1лишаєтьсяok.- Помилки відповідають OTP:
{error, enoent},eexist,eisdir(каталог, відкритий чи прочитаний як файл),einval(від'ємна позиція),ebadf(читання файлу лише для запису або запис у файл лише для читання),badargдля неправильних імен і даних. Імена файлів — це рядки, binary або атоми, закодовані як UTF-8. - Модуль, на який посилається програма і який є вбудованою функцією
каталогу (
io:format), більше не тягне в програму однойменний модуль бібліотеки; це робить лише виклик однієї з його функцій на Erlang.
Сокети (57F)
Сокет — це порт, як у бекенді inet_drv в OTP: is_port(Socket) істинне, а
повідомлення активного режиму — це {tcp, Socket, Data},
{tcp_closed, Socket}, {tcp_error, Socket, Reason},
{udp, Socket, Address, Port, Data}. Підмножина: 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; режими {active, true | false | once}, binary/list,
{packet, 0 | 1 | 2 | 4 | raw}, {reuseaddr, Bool}, {backlog, N},
{ip, Address}/{ifaddr, Address}, inet/inet6; адреси IPv4 та IPv6 як
кортежі, імена хостів як рядки чи атоми (включно з loopback). Опції
налаштування nodelay, keepalive, send_timeout, send_timeout_close,
delay_send і exit_on_close приймаються, але не застосовуються; будь-яка
інша опція дає exit(badarg), як для недійсної опції в OTP.
Реалізація (runtime/src/ports/sockets.cpp):
- Потік вводу-виводу runtime (потік вводу-виводу) обслуговує
сокети; стан кожного сокета живе в цьому потоці. Бібліотека відкриває
порт через
{spawn_driver, "tcp_inet" | "udp_inet"}і керує ним черезport_control/3; робочий потік, що викликає, передає операцію потоку вводу-виводу й чекає на її синхронну відповідь (байт стану 0 і результат або 1 і причина POSIX). - Операції, що чекають (connect, accept, recv), відповідають пізніше
повідомленням
{clause_socket, Socket, Reply}процесу, що викликав, тож він блокується у звичайномуreceive, а інші процеси й далі виконуються. Тайм-аут скасовує запит; саме скасування відповідаєcancelledпісля всього, що сокет надіслав раніше, тож відповідь, що виграла перегони, повертається, а не губиться. - Повідомлення сокетів описуються поза купою (
ports/value.hpp) і будуються в купі отримувача під час доставки, за правилами виконавця з розділу потік вводу-виводу. Прийняте з'єднання стає новим портом, що належить процесу, який викликавaccept, і зв'язаний із ним, з режимом слухального сокета. controlling_process/2зупиняє активну доставку, переносить повідомлення сокета, вже надіслані старому власникові, новому, а потім перепід'єднує порт, як це робитьinetв OTP; викликати його може лише власник ({error, not_owner}).- Вивід ставиться в чергу без обмеження й пишеться по порядку в потоці
вводу-виводу. Закриття порту надсилає те, що в черзі, а потім коректно
закриває з'єднання (FIN); закритий порт відповідає кожному процесу, що
чекає,
{error, closed}. Пошук імен виконується на робочому потоці процесу, що викликає, бо він блокувальний. - Порти сокетів плануються як інші порти (завдання портів): повідомлення сокета чекають у його порту, доки завдання порту їх не доставить.
Не надається
Вбудовувані (linked-in) драйвери та NIF, erlang:open_port({spawn_driver, Name})
для імен драйверів OTP, порти розподіленості (distribution), port_call/3
на наданих драйверах, окрім власного протоколу бібліотеки, зайняті порти
сокетів, а також опції overlapped_io, parallelism і busy_limits_msgq.
Clause