Порти
Програми отримують порти в тому вигляді, як їх визначає OTP, і зовнішній
ввід-вивід проходить через них. Таблиця портів, правила виходу й завдання
портів містяться в runtime/src/scheduler/ports.cpp, вбудовані функції — в
runtime/src/builtins/erlang/ports.cpp, драйвери й потік вводу-виводу — в
runtime/src/ports/. Еталони OTP: 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
runtime_port_io.
Ідентичність
- Порт — це безпосереднє (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.port_info/1,2:name,links,id,connected,input,output,os_pid,monitors,monitored_by,registered_name,queue_size(зайняті порти).port_close/1,port_connect/2,port_command/2,3; повідомленняPort ! {Pid, {command, Data}},{Pid, close},{Pid, {connect, New}}.link/1,unlink/1,monitor(port, P),exit/2для портів;register/2порту.port_control/3,port_call/3:badargдля драйверів без керування; драйвериfileі сокетів керуються черезport_control/3.open_port({fd, In, Out}, Opts),open_port({spawn, Command}, Opts),open_port({spawn_executable, File}, Opts)і внутрішні драйвери бібліотеки проєкту (file, сокети).
Помилки відповідають 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 (підпроцеси) |
{busy_limits_port, {Low, High} | disabled} | Кількість байтів виводу в черзі, що робить порт зайнятим (зайняті порти) |
Без eof кінець вводу закриває порт із причиною normal, після повідомлення
exit_status, якщо його було запитано. Невідомі опції дають badarg.
Драйвери
Драйвер — це об'єкт C++ за одним портом (runtime/src/ports/): він відкриває
ресурс, приймає вивід (port_command), відповідає на port_control/3, якщо
підтримує керування, повідомляє про ввід і помилки як про події та
закривається. Драйвери:
fd: наявні дескриптори, вивід пишеться синхронно, ввід — через потік вводу-виводу.spawn: дочірня програма, чиї stdin і stdout є каналами (pipes) (підпроцеси).file: відкритий файл модуляfileбібліотеки проєкту; його операції — це синхронні викликиport_control/3, що можуть блокувати робочий потік, на якому виконується процес, що викликає, на час дискового вводу-виводу, як це роблять брудні (dirty) планувальники вводу-виводу OTP (файли).tcp_inet,udp_inet: сокети модулівgen_tcp,gen_udpіinetбібліотеки проєкту на Boost.Asio; з'єднання, прийняття з'єднань і отримання асинхронні (сокети).clause_registry(Windows): реєстр дляwin32reg(реєстр).
Внутрішні драйвери file, сокетів і реєстру відкриваються через
{spawn_driver, Name} під іменами Clause, а їхні операції port_control/3
утворюють протокол Clause: програми використовують модулі бібліотеки, а не
протоколи OTP prim_inet/efile.
Потік вводу-виводу
Один потік вводу-виводу на runtime (detail::Reactor,
runtime/src/ports/reactor.hpp) виконує 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):
його методи лише передають роботу потоку вводу-виводу, де живе весь стан
вводу-виводу. Потік вводу-виводу передає ввід у тому вигляді, як його
прочитано: сирі байти, кінець вводу, помилку читання, код завершення
програми. Події сокетів (повідомлення, прийняті з'єднання) передаються так
само.
Прототип tests/prototypes/poller/ (run.py --wsl) показує пробудження на
Windows (порт завершення) і WSL Linux (poll()): вільний потік планувальника
прокидається через 9–91 мкс після вводу, а завершення роботи зупиняє потік
вводу-виводу без вводу.
Наприкінці програми кожен порт закривається (дочірні програми бачать кінець
вводу; їх не вбивають, як і в OTP), а потік вводу-виводу зупиняється: його
приєднують (join) поза м'ютексом виконавця, бо доставка, що триває, бере цей
м'ютекс; читач fd на Windows, заблокований у читанні, яке не можна
скасувати, від'єднується й більше нічого не доставляє.
Завдання портів
Порт планується як процес: те, що передає потік вводу-виводу, чекає в порту, а порт чекає в черзі портів виконавця, доки робочий потік планувальника не виконає його завдання.
- Робочі потоки по черзі беруть завдання порту й квант часу процесу, поки обидві черги мають роботу, тож порт, що заливає вводом, не може заморити процеси голодом, а процеси не можуть заморити порти; вільний робочий потік бере те, що є в черзі.
- Завдання виконується під м'ютексом виконавця протягом
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пише свій вивід одразу на робочому потоці процесу, що викликає; драйвери каналів і сокетів ставлять вивід у чергу й дають потоку вводу-виводу записати його; зайнятий порт призупиняє відправника (зайняті порти).
Зайняті порти
Вивід, який драйвер ставить у чергу для потоку вводу-виводу (канали запущених
програм), враховується в межах зайнятості порту, 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 на пізніше повідомлення порту не може чекати вічно.
Підпроцеси
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 (натомість нульовий пристрій); з обома опціями чи без жодної порт має обидва канали, як в OTP.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.
Стандартний ввід-вивід і файли
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. Запити кількох процесів обслуговуються в порядку надходження. Ввід декодується з UTF-8 у символи, як OTP читає свій термінал; сервер зберігає опціїstandard_ioOTP у режимі noshell (binary,encoding,echo,log;io:getopts/0,1,io:setopts/1,2): режим binary повертає binary,echoпише спожитий ввід у стандартний вивід,get_passwordбез оболонки повертає{error, enotsup}, як в OTP.- Файлові сервери відповідають на
getoptsіsetoptsяк файловий сервер вводу-виводу OTP (binary/listіencoding;file:open/2приймає{encoding, E}): запити вводу-виводу (io:get_line,get_chars,put_chars,get_until) кодують і декодують дані в кодуванні пристрою, аfile:read/2,read_line/1для файлу з кодуванням відповідають як запитиget_chars/get_lineу latin1 (no_translationдля значень понад 255, що зупиняє сервер). Вивідio:format/3,io:put_chars/2,io:nl/1до будь-якого pid — це запитput_charsпротоколу вводу-виводу. - Модуль
fileбібліотеки керує файловим драйвером runtime,{spawn_driver, "clause_file"}, черезport_control/3. Його операції (ports/file.cpp: open, read, write, position, read_line, sync, close, і операції над шляхами read_file, write_file, delete, rename, list_dir, make_dir, del_dir і read_file_info,ports/file_info.cpp; get_cwd, set_cwd, read_link, read_link_info, make_link і make_symlink,ports/file_paths.cpp) — це синхронні системні виклики на робочому потоці процесу, що викликає, поза м'ютексом виконавця; їхні відповіді починаються з байта стану (0 — ok, 1 — помилка та її причина POSIX, 2 — кінець файлу). Протокол власний для Clause. read_line читає наперед 64 КБ і повертає рядки з цього буфера; будь-яка інша операція над файлом спершу повертає позицію назад на байти, які ще не повернуто, тож позиції лишаються точними. - Операції над шляхами виконують ті самі системні виклики, що й OTP, і жодних
зайвих: стан шляху читається лише після невдалого відкриття чи видалення
(щоб відповісти
eisdirабоeperm), delete від'єднує файл і ніколи не видаляє каталог, а read_file читає файл до розміру, який повідомляє його дескриптор, як OTP, без пробного читання для пошуку кінця. port_control/3передає binary або[]драйверу на місці (інші iodata спершу зводяться до суцільного буфера), а відповідь стає результуючим binary без копіювання: відповіді — цеByteBuffer(runtime/include/byte_buffer.hpp), вектори байтів, що зростають без занулення, тож читання заповнює байти відповіді один раз.file:open/2повертає pid сервера вводу-виводу, якому належить файловий порт і який закривається, коли завершується процес, що відкрив файл, як в OTP (у Windows файл спільно доступний для читання, запису й видалення, як його відкриває OTP, тож відкритий файл можна видалити чи перейменувати) (режимиread,write,append,exclusive,binary,raw; інші режими ігноруються, як OTP ігнорує опції, яких не використовує;writeобрізає файл, якщо не задано такожappend).file:write_file/3відкриває файл зі своїми режимами плюсbinaryіwrite;file:read_file_info/1,2повертають#file_info{}з OTP на основі стану шляху (з переходом за посиланнями), з часом як локальна чи універсальна дата й час або секунди POSIX ({time, local | universal | posix};rawприймається). У Windowsinode,uidіgidдорівнюють 0, аmajor_device— номер диска, як їх повідомляє 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.- Позиційний ввід-вивід і зміни файлів:
pread/2,3іpwrite/2,3встановлюють позицію, а потім читають чи пишуть через файловий сервер (curчитає далі, позиція змінюється), як сервер OTP; файлrawприймає лише цілі зміщення (інакшеbadarg) і зберігає свою позицію.truncate/1обрізає файл на поточній позиції;advise/4приймає атоми рекомендацій OTP (posix_fadviseна Linux, деінде ігнорується, як в OTP);allocate/3резервує місце (fallocate,F_PREALLOCATEна macOS,{error, enotsup}у Windows), спершу перевіряючи аргументи (badargдля нечислових значень,einvalдля від'ємних і чисел із рухомою комою). Як файловий сервер OTP, сервер, у якого запис, позиційний запис, sync, truncate чи advise завершується помилкою, відповідає цією помилкою й завершується, тож подальші запити отримують{error, terminated}; файл raw працює далі.write_file_info/2,3іchange_mode/2,change_owner/2,3,change_group/2,change_time/2,3змінюють власника, права й часові позначки в порядку prim_file (Windows: власники ігноруються, з біта запису власника встановлюється лише атрибут «лише читання», також задається час створення).sendfile/2,5читають файл raw блоками (щонайбільше 1 MiB,chunk_size) і надсилають їх черезgen_tcp:send/2або задану fun — запасний шлях OTP: Clause не має нативного sendfile.format_error/1повертає тексти OTP, для POSIX — зerl_posix_msg:message/1. - Рядки й невдалі читання:
read_line/1іio:get_lineчитають закінчення CRLF як LF (позиція враховує CR), як OTP; файловий сервер, у якого читання завершується помилкою, відповідає помилкою й завершується, а якщо помилкою завершується читання рядка, завершується без відповіді (викликач отримує{error, terminated}), як в OTP. compressedіcompressed_one: запис іде через потік gzip deflate (zlib, біти вікна 31), що завершується при закритті, зокрема коли сервер зупиняється чи завершується процес, що відкрив файл; читання розпаковує файл, який починається з розпізнавальних байтів gzip (compressedчитає з'єднані частини як одну,compressed_oneзупиняється після першої), а будь-який інший файл читає без змін. Позиції рахують нестиснені байти: уперед — читанням чи записом нулів, назад — лише при читанні (знову від початку),eofдаєeinval. Читання файлу для запису чи запис у файл для читання даєebadf;allocate/3і позиційний ввід-вивід для файлів raw даютьenotsup; файловий сервер зупиняється післяtruncate,adviseіsync(enotsup), як в OTP. Одночасні читання й запис або дописування дають{error, einval}.{zstd, Options}: якcompressed, через контекст zstd, створений зOptions({error, badarg}, якщо він їх відхиляє): кожен запис — окремий фрейм, а закриття додає порожній, як пише шар OTP; читання розпаковує фрейми, прочитані блоками по 8 KiB.ram: файл — це його вміст iodata в пам'яті, який обслуговує процес, як інші файли, і який закривається разом із процесом, що його відкрив; лише режимиread,writeіbinary(read, якщо жоден не задано, будь-що інше —badarg,rawразом ізramпороджуєbadarg). Читання, запис, позиції,pread/pwrite(зі збереженням позиції),truncate,sync,adviseіallocateвідповідають як драйвер файлів ram в OTP: запис за кінцем заповнює проміжок нулями, зміщення й розміри поза діапазоном знакового 32-бітного числа даютьeinval, запис у файл лише для читання —ebadf, його обрізання —eacces.- Протокол вводу-виводу OTP: сервер стандартного вводу і сервер кожного
відкритого файлу відповідають на
{io_request, From, ReplyAs, Request}через{io_reply, ReplyAs, Reply}; такі запити надсилаютьio:request/1,2іio:requests/1,2(standard_ioіuserназивають сервер стандартного вводу, інший атом — зареєстрований процес;{error, arguments}, коли його не зареєстровано,{error, calling_self}для самого процесу, що викликає). Запити:get_until(M:F(Cont, Chars | eof, Extra...)починаючи зCont = [], якому подається буферизоване або блоки файлу по 4 KiB, доки не буде{done, Result, Rest};Restнаступний запит читає знову: він лишається в буфері, а для файлу позиція повертається назад на нього),get_line,get_chars,put_chars(символи абоM:F(A)), старі форми без кодування (latin1) і{requests, List}(по порядку, із зупинкою на першому{error, _}). Символи — це байти, як вище; файл у режимі binary повертає списковий результатget_untilяк binary. Інші запити отримують{error, {request, Request}}. Функціяget_untilчиput_chars, що завершується збоєм, змушує сервер файлу відповісти{error, Function}і завершитися з цим збоєм, як в OTP; процес, що відкрив файл, живе далі (файловий сервер OTP не зв'язаний із ним). Запити вводу на стандартному вводі виводять своє запрошення. - Каталоги, посилання й копії:
get_cwd/0записує поточний каталог так, як OTP (прямі скісні риски, мала літера диска),get_cwd/1приймає диск Windows (enotsupдеінде);set_cwd/1змінює його для всієї програми;read_link/1,read_link_all/1(einvalдля шляху, що не є посиланням),read_link_info/1,2(власний стан кінцевого посилання:lstatPOSIX, у Windows поля цілі з типомsymlink),make_link/2,make_symlink/2(у Windows потребує привілею:eperm, як в OTP),list_dir_all/1,del_dir_r/1,copy/2,3(імена,{Name, Modes}або відкриті файли;{ok, BytesCopied}),raw_read_file_info/1,read_file/2,delete/2,native_name_encoding/0(utf8).del_dir/1відповідаєenoentдля відсутнього шляху іenotdirдля файлу. fileз Clause — це файловий шар;filelibіfilenameз Clause працюють поверх нього (filelib:fold_files/5використовуєre);erl_prim_loaderнадається поверхfileз простими відповідямиerrorяк в OTP і шляхом завантажувача["."](set_path/1), а його функції завантаження й архівів відхиляються;prim_fileіprim_bufferлишаються відсутніми, тож викликачіraw_file_io*з OTP ніколи не потрапляють у програму.- Помилки відповідають OTP:
{error, enoent},eexist,eisdir(каталог, відкритий чи прочитаний як файл),einval(від'ємна позиція;badargдля значення, що не є позицією),ebadf(читання файлу лише для запису або запис у файл лише для читання),badargдля неправильних імен і даних. Імена файлів — це рядки, binary або атоми, закодовані як UTF-8.
Сокети
Сокет — це порт, як у бекенді inet_drv в OTP: is_port(Socket) істинне, а
повідомлення активного режиму — це {tcp, Socket, Data},
{tcp_closed, Socket}, {tcp_error, Socket, Reason},
{udp, Socket, Address, Port, Data}. Підмножина:
gen_tcp:connect/2,3,4 (хост і порт або 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 (форми призначення й допоміжні дані 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; режими {active, true | false | once},
binary/list ({mode, M}), {packet, 0 | 1 | 2 | 4 | raw}, {backlog, N},
{ip, Address}/{ifaddr, Address}, inet/inet6; адреси IPv4 та IPv6 як
кортежі, імена хостів як рядки чи атоми (включно з loopback).
Опції, крім режиму даних (ports/socket_options.cpp,
library/kernel/clause_socket_options.erl), — це записи драйвера з кодом і
значенням, які задаються при відкритті сокета (встановлюються до прив'язування
чи з'єднання) або через setopts/2; кожна опція виклику setopts/2
перевіряється до встановлення будь-якої з них. Системні опції (reuseaddr,
keepalive, dontroute, linger, broadcast, sndbuf, recbuf, nodelay,
tos, tclass, ipv6_v6only, multicast_ttl, multicast_loop,
multicast_if, add_membership, drop_membership, exclusiveaddruse у
Windows, priority і reuseport, де система їх має) передаються в
setsockopt і зчитуються через getsockopt; опції, які inet_drv з OTP
зберігає сам (buffer, header, deliver, exit_on_close, send_timeout,
порогові рівні, packet_size, ...), зберігаються з типовими значеннями
inet_drv і не застосовуються (відмінності). Як в inet_drv,
буфер приймання збільшує buffer, доки його не задано, а getopts/2
пропускає опції, яких тип сокета не має (broadcast для сокета TCP,
nodelay для UDP, tclass для IPv4), і відхиляє невідомі через
{error, einval}. Прийняте з'єднання успадковує опції слухального сокета.
send/5 з допоміжними даними задає tos, tclass чи ttl лише для цієї
датаграми; у Windows це {error, einval}, як в OTP.
Кожен сокет рахує свій трафік як inet_drv для getstat/1,2: отримані
пакети — це читання (TCP) чи датаграми, надіслані — виклики send (з їхнім
заголовком пакета) чи датаграми, з поточними середніми inet_drv; send_pend
— це вивід у черзі, який ще не записано. getstatus повертає прапорці стану
inet_drv (open, connected, connecting, listen, accepting), які
info/1 і i/0,1,2 друкують з іменами OTP і стовпцем модуля (inet_tcp,
inet6_udp, ...). Запити до закритого сокета отримують {error, einval},
як в OTP.
Модуль socket (library/kernel/socket.erl) використовує ті самі порти
драйверів у пасивному режимі binary. Потоковий сокет можна прив'язати до
прослуховування чи з'єднання (операція 22 відкриває й прив'язує системний
сокет, який listen перетворює на сокет приймання з'єднань, а connect
з'єднує); сокет датаграм прив'язується через udp_open, неявно при першому
надсиланні. Завершення з'єднання відповідає кожному recv, що чекає
(зокрема тому, що запитує більше байтів, ніж лишилося). socket зберігає
байти, яких recv не забрав, у власному буфері, тож recv/2,3,4 повертає
{error, {timeout, Data}} і {error, {closed, Data}} як в 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}. Пошук імен виконується на робочому потоці процесу, що викликає, бо він блокувальний. - Порти сокетів плануються як інші порти (завдання портів): повідомлення сокета чекають у його порту, доки завдання порту їх не доставить.
- Адреси й хости:
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(послаблені форми OTP: короткі та вісімкові чи шістнадцяткові поля IPv4,::, хвости IPv4,%scopeдляfe80::іff02::),ntoa/1(написання OTP: шістнадцяткові цифри малими літерами, перша найдовша послідовність нулів як::),is_ipv4_address/1,is_ipv6_address/1,is_ip_address/1написані на Erlang;gethostname/0(операція сокета),getaddr/2,getaddrs/2(системний розв'язувач,getaddrinfo) іgethostbyname/1,2(h_name— запитане ім'я, без псевдонімів) відповідають через порт сокета, відкритий для запиту. - Служби, зворотний пошук та інтерфейси (
inet:getservbyname/2,getservbyport/2,gethostbyaddr/1,getifaddrs/0,1) — це вбудовані функціїclause_net, спільні зnet(мережеві імена). gen_tcp,gen_udp,inetіnetз Clause — це мережевий шар; модульsocketз OTP (API NIF сокетів) відсутній, як іprim_socket,prim_net,prim_inetіsocket_registry.
Реєстр
win32reg відкриває {spawn_driver, "clause_registry"} у Windows
(runtime/src/ports/registry_windows.cpp). Драйвер зберігає стан драйвера
реєстру OTP (запитаний доступ, поточний ключ і його корінь, спочатку
HKEY_CLASSES_ROOT) і відповідає на дев'ять команд OTP як на синхронні
операції port_control/3 відповідями, які драйвер OTP надсилає як дані порту:
o, e та ім'я errno (пошкодження реєстру — eio, видалений ключ —
einval, інші помилки Windows — за відображенням C runtime), k та ім'я
ключа, v, тип, ім'я й дані (рядки без кінцевого нуля, DWORD у порядку
big-endian), s і поточний ключ. Переліки повертають кожну відповідь,
перед кожною — її 32-бітний розмір. Імена й рядки використовують кодову
сторінку ANSI, як драйвер OTP.
Не надається
Вбудовувані (linked-in) драйвери та NIF,
erlang:open_port({spawn_driver, Name}) для імен драйверів OTP, порти
розподіленості (distribution), port_call/3 на наданих драйверах, окрім
власного протоколу бібліотеки, зайняті порти сокетів, а також опції
overlapped_io, parallelism і busy_limits_msgq.
Clause