Как процессы с изолированной памятью передают друг другу данные
ReadFile и WriteFile с учётом частичного обменаПроцессы изолированы: один не может прочитать память другого (раздел 14, пункт 05). Когда им нужно обменяться данными, они просят систему. Способы:
| Способ | Как | Раздел |
|---|---|---|
| файл | один пишет, другой потом читает; медленно, нужна договорённость «когда готово» | 11 |
| канал (pipe) | труба в памяти системы: один процесс пишет в один конец, другой читает из другого — в том же порядке | 17 |
| сокет | то же, но между компьютерами по сети | 19 |
| общая память | одна область памяти видна обоим процессам; быстро, но нужна синхронизация | 18 (упоминание) |
Канал — объект ядра с буфером. WriteFile кладёт байты в буфер, ReadFile забирает их в том же порядке. Если буфер пуст, чтение ждёт данных; если полон — ждёт запись. Когда все пишущие концы закрыты и буфер пуст, чтение возвращает «конец данных». Так работает символ | в консоли: в команде dir | sort вывод dir идёт в канал, а sort читает его как свой ввод.
| Анонимный канал | Именованный канал | |
|---|---|---|
| Создание | CreatePipe — сразу два дескриптора: чтения и записи | CreateNamedPipeA("\\\\.\\pipe\\имя", …) — сервер |
| Как второй процесс получает канал | по наследству от родителя | открывает по имени: CreateFileA |
| Направление | одно | одно или оба |
| Кто с кем | родитель и потомок | любые процессы, в том числе на разных компьютерах сети |
Дескриптор имеет смысл только в своём процессе (раздел 14, пункт 03). Чтобы потомок получил конец канала, дескриптор должен быть наследуемым, а CreateProcessA — вызвана с разрешением наследовать. Тогда система копирует наследуемые дескрипторы родителя в таблицу потомка — с теми же номерами.
SECURITY_ATTRIBUTES inherit = {sizeof(inherit), nullptr, TRUE}; // третье поле: наследуется
HANDLE readEnd;
HANDLE writeEnd;
if (!CreatePipe(&readEnd, &writeEnd, &inherit, 0)) { // 0 — размер буфера по умолчанию
printError("CreatePipe");
return 2;
}
SetHandleInformation(readEnd, HANDLE_FLAG_INHERIT, 0); // читающий конец потомку не нужен
SECURITY_ATTRIBUTES делает наследуемыми оба конца. Тот, что остаётся у родителя, наследование лучше снять SetHandleInformation: иначе у потомка окажется лишний дескриптор, и канал не закроется, пока потомок жив.std::cin или std::cout и даже не знает о канале.В STARTUPINFOA есть поля для стандартных потоков потомка. Флаг STARTF_USESTDHANDLES говорит системе взять их оттуда:
// Фрагмент capture.cpp: вывод потомка — в канал, родитель читает порциями. Полностью — в примерах раздела
STARTUPINFOA si = {};
si.cb = sizeof(si);
si.dwFlags = STARTF_USESTDHANDLES; // стандартные потоки — из полей ниже
si.hStdInput = GetStdHandle(STD_INPUT_HANDLE);
si.hStdOutput = writeEnd;
si.hStdError = writeEnd;
PROCESS_INFORMATION pi = {};
if (!CreateProcessA(nullptr, cmd, nullptr, nullptr, TRUE, 0, nullptr, nullptr, &si, &pi)) {
printError("CreateProcess");
return 2;
}
CloseHandle(writeEnd); // свой пишущий конец закрыть — иначе ReadFile не узнает о конце данных
CloseHandle(pi.hThread);
char buffer[CHUNK];
DWORD read = 0;
long long total = 0;
int reads = 0;
int lines = 0;
while (ReadFile(readEnd, buffer, CHUNK, &read, nullptr) && read > 0) {
++reads;
total += read;
for (DWORD i = 0; i < read; ++i) {
if (buffer[i] == '\n') {
++lines;
}
}
std::cout.write(buffer, read); // порция — не строка: '\0' в конце нет
}
// ReadFile вернула FALSE с ошибкой 109 (ERROR_BROKEN_PIPE): все пишущие концы закрыты — данных больше не будет
CloseHandle(readEnd);
> capture "child 3 0"
child 6152: sleep 0 ms, exit 3
---
32 bytes in 1 reads, 1 lines, exit code 3
bInheritHandles = TRUE — пятый параметр CreateProcessA: без него флаг наследования у дескрипторов ничего не даёт.ReadFile будет ждать данных вечно — программа «зависнет» после вывода потомка.ReadFile читает столько, сколько есть в буфере, но не больше CHUNK: порция может оборваться посреди строки. Прочитанное — байты, а не строка: нулевого символа в конце нет, поэтому вывод — std::cout.write(buffer, read), а не std::cout << buffer.std::cout потомка в текстовом режиме записал перевод строки как \r\n (раздел 11, пункт 04). Вывод русских команд Windows (например, cmd /c dir) приходит в кодировке 866 — при странице 1251 он будет нечитаем; для опытов берите программы с латинским выводом.Анонимный канал односторонний. Чтобы и писать потомку, и читать его ответ, нужны два канала: один — к его stdin, другой — от его stdout. Пример tosort.cpp отправляет строки программе Windows sort и читает их упорядоченными:
// Фрагмент tosort.cpp: после CreateProcessA. Полностью — в примерах раздела
CloseHandle(toChildRead); // концы потомка родителю больше не нужны
CloseHandle(fromChildWrite);
CloseHandle(pi.hThread);
const char lines[] = "banana\r\napple\r\ncherry\r\n";
if (!writeAll(toChildWrite, lines, sizeof(lines) - 1)) { // без завершающего '\0'
printError("WriteFile");
}
CloseHandle(toChildWrite); // конец ввода: sort начинает сортировать только теперь
char buffer[CHUNK];
DWORD read = 0;
while (ReadFile(fromChildRead, buffer, CHUNK, &read, nullptr) && read > 0) {
std::cout.write(buffer, read);
}
apple
banana
cherry
PeekNamedPipe, пункт 08).ReadFile и WriteFileКаналы, файлы, консоль, устройства — в Windows всё читается и пишется одними функциями: ReadFile(h, буфер, сколькоНадо, &сколькоВышло, nullptr) и WriteFile с такими же параметрами. Последний параметр — для асинхронного обмена, в курсе — nullptr.
| Ситуация | ReadFile | WriteFile |
|---|---|---|
| всё в порядке | TRUE, в read — от 1 до «сколько надо» байт: сколько было в канале | TRUE, записано обычно всё, но может быть меньше |
| данных нет, пишущие концы открыты | ждёт | — |
| буфер канала полон | — | ждёт, пока читатель заберёт данные |
| другой конец закрыт | FALSE, ошибка 109 ERROR_BROKEN_PIPE — нормальный конец данных | FALSE, ошибка 232 ERROR_NO_DATA или 109 |
| сообщение длиннее буфера (режим сообщений) | FALSE, ошибка 234 ERROR_MORE_DATA, прочитана часть | — |
Поэтому запись «всего» — цикл, который дописывает остаток:
// Пишет size байт целиком: WriteFile может записать меньше, чем просили, — тогда дописывает остаток
bool writeAll(HANDLE h, const char data[], DWORD size)
{
DWORD done = 0;
while (done < size) {
DWORD written = 0;
if (!WriteFile(h, data + done, size - done, &written, nullptr)) {
return false;
}
done += written;
}
return true;
}
Чтение «всего» — цикл до конца данных, как в пункте 03. Нельзя рассчитывать, что одна запись на одном конце даст одно чтение на другом: в байтовом канале 100 байт могут прийти за один ReadFile или за три. Если читателю нужны границы («где кончается сообщение»), их задаёт протокол: перевод строки, длина перед данными — или режим сообщений (пункт 08).
Именованный канал создаёт сервер и ждёт клиентов. Имя — \\.\pipe\любое-имя (точка — «этот компьютер»; с именем компьютера вместо точки к каналу обращаются по сети). В строке C++ каждая обратная черта удваивается: "\\\\.\\pipe\\sysprog-echo". Сервер echoserver отвечает на каждое сообщение им же заглавными буквами:
// Фрагмент echoserver.cpp. Полностью — в примерах раздела
constexpr char PIPE_NAME[] = "\\\\.\\pipe\\sysprog-echo";
constexpr DWORD BUFFER_SIZE = 4096;
int main()
{
bool running = true;
while (running) {
HANDLE pipe = CreateNamedPipeA(PIPE_NAME, PIPE_ACCESS_DUPLEX,
PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT,
1, BUFFER_SIZE, BUFFER_SIZE, 0, nullptr);
if (pipe == INVALID_HANDLE_VALUE) {
printError("CreateNamedPipe");
return 2;
}
std::cout << "waiting for a client on " << PIPE_NAME << '\n';
// ERROR_PIPE_CONNECTED: клиент подключился между CreateNamedPipe и ConnectNamedPipe — тоже успех
if (ConnectNamedPipe(pipe, nullptr) || GetLastError() == ERROR_PIPE_CONNECTED) {
std::cout << "client connected\n";
running = serveClient(pipe);
std::cout << "client disconnected\n";
} else {
printError("ConnectNamedPipe");
}
FlushFileBuffers(pipe); // дождаться, пока клиент прочитает последний ответ
DisconnectNamedPipe(pipe);
CloseHandle(pipe);
}
std::cout << "server stopped\n";
return 0;
}
// Сообщение — ответ, пока клиент не отключится; false — клиент попросил остановить сервер
bool serveClient(HANDLE pipe)
{
char request[BUFFER_SIZE];
char reply[BUFFER_SIZE];
DWORD read = 0;
while (ReadFile(pipe, request, BUFFER_SIZE - 1, &read, nullptr)) { // одно сообщение целиком
request[read] = '\0';
std::cout << "request: " << request << '\n';
const bool keepRunning = handleRequest(request, reply, BUFFER_SIZE);
DWORD written = 0;
if (!WriteFile(pipe, reply, static_cast<DWORD>(length(reply)), &written, nullptr)) {
printError("WriteFile");
return true;
}
if (!keepRunning) {
return false;
}
}
// ReadFile = FALSE: 109 — клиент закрыл канал, 234 (ERROR_MORE_DATA) — сообщение длиннее буфера
return true;
}
Параметр CreateNamedPipeA | Здесь | Смысл |
|---|---|---|
| направление | PIPE_ACCESS_DUPLEX | читать и писать; …_INBOUND — только от клиента, …_OUTBOUND — только к клиенту |
| режим | PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT | сообщения (пункт 08), операции ждут |
| экземпляров | 1 | сколько клиентов одновременно; PIPE_UNLIMITED_INSTANCES — сколько угодно (задание 3 практической работы) |
| буферы | 4096, 4096 | размеры буферов вывода и ввода — подсказка системе |
| тайм-аут по умолчанию | 0 | для WaitNamedPipe клиента с NMPWAIT_USE_DEFAULT_WAIT; 0 — 50 мс |
ConnectNamedPipe ждёт, пока клиент откроет канал. DisconnectNamedPipe разрывает соединение с текущим клиентом — после этого экземпляр можно снова ждать или закрыть. Непрочитанные клиентом данные при этом пропадают: если сервер отключит клиента сразу после ответа на shutdown, клиент получит не bye, а ошибку 233 «С обоих концов канала отсутствуют процессы». Поэтому перед DisconnectNamedPipe стоит FlushFileBuffers — он ждёт, пока клиент прочитает всё, что записано в канал. Этот сервер обслуживает клиентов по очереди: пока подключён один, второй ждёт свободного экземпляра.
// Фрагмент pipeclient.cpp: подключение. Полностью — в примерах раздела
const std::string name = std::string("\\\\.\\pipe\\") + argv[1];
if (!WaitNamedPipeA(name.c_str(), CONNECT_TIMEOUT_MS)) { // ждать свободного экземпляра канала
printError("WaitNamedPipe"); // 2 — сервера нет, 121 — не дождались
return 2;
}
HANDLE pipe = CreateFileA(name.c_str(), GENERIC_READ | GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, 0, nullptr);
if (pipe == INVALID_HANDLE_VALUE) {
printError("CreateFile");
return 2;
}
DWORD mode = PIPE_READMODE_MESSAGE; // клиент по умолчанию читает байтами — переключить на сообщения
if (!SetNamedPipeHandleState(pipe, &mode, nullptr, nullptr)) {
printError("SetNamedPipeHandleState");
CloseHandle(pipe);
return 2;
}
Дальше клиент в цикле читает строку с клавиатуры, отправляет её WriteFile и ждёт ответ ReadFile. Строка ввода здесь — std::string, а в WriteFile уходит line.c_str() (раздел 10, пункт 08). Два окна консоли:
> echoserver > pipeclient sysprog-echo
waiting for a client on \\.\pipe\... connected to \\.\pipe\sysprog-echo
client connected hello, pipe
request: hello, pipe HELLO, PIPE
request: shutdown shutdown
client disconnected bye
server stopped quit
CreateFileA — та же функция, что открывает файлы (раздел 14, пункт 04): для системы канал — ещё одно «устройство». Если сервер не запущен, WaitNamedPipeA сразу вернёт ошибку 2; если все экземпляры заняты — будет ждать до тайм-аута (ошибка 121). Клиент pipeclient универсальный: с ним работают все серверы практической работы — достаточно указать имя канала.
Байтовый режим PIPE_TYPE_BYTE | Режим сообщений PIPE_TYPE_MESSAGE | |
|---|---|---|
| Что хранит канал | поток байтов без границ | сообщения: каждая запись — отдельное сообщение |
Что читает ReadFile | сколько есть, до размера буфера; границы записей теряются | ровно одно сообщение; не помещается — часть и ошибка 234, остаток — следующим чтением |
| Когда удобен | поток данных: вывод программы, копия файла | запрос — ответ: команды, как в практической работе |
Тип канала (PIPE_TYPE_…) задаёт сервер при создании. Режим чтения (PIPE_READMODE_…) — у каждого конца свой: сервер задаёт свой в CreateNamedPipeA, клиент открывает канал в байтовом режиме и переключает SetNamedPipeHandleState. Анонимные каналы — всегда байтовые.
PeekNamedPipe позволяет заглянуть в канал, ничего не забирая и не ожидая: сколько байт уже пришло. Так читают без риска «зависнуть» — только когда есть что читать:
// Сколько байт можно прочитать из канала прямо сейчас; -1 — канал закрыт или ошибка. Не ждёт
long long available(HANDLE pipe)
{
DWORD bytes = 0;
if (!PeekNamedPipe(pipe, nullptr, 0, nullptr, &bytes, nullptr)) {
return -1;
}
return bytes;
}
Работает и для анонимных каналов. Цикл «проверить — прочитать, если есть — заняться другим — Sleep» похож на pollKey из раздела 15.
| Кто ждёт | Чего | Как не ждать вечно |
|---|---|---|
| клиент | свободного экземпляра канала | WaitNamedPipeA(имя, мс) — ошибка 121 по истечении |
| сервер | клиента в ConnectNamedPipe | ожидание в отдельном потоке (раздел 16) или асинхронный режим (вне курса) |
| читатель | данных в ReadFile | PeekNamedPipe перед чтением; чтение в отдельном потоке |
| писатель | места в полном буфере | читатель должен читать; не писать много, не читая (пункт 04) |
| родитель | завершения потомка | WaitForSingleObject(hProcess, мс) (раздел 14) |
Надёжный сервер не ждёт клиента бесконечно и не зависает из-за одного медленного клиента. Обычная схема — задание 3 практической работы: главный поток создаёт экземпляр канала и ждёт подключения, а каждого подключившегося клиента обслуживает отдельный поток. В разделе 19 сервер на сокетах устроен так же.
| Ошибка | Признак | Сообщение g++ / ошибка Windows | Как избежать |
|---|---|---|---|
DisconnectNamedPipe сразу после последнего ответа | клиент иногда не получает ответ, ошибка 233 | нет | FlushFileBuffers(pipe) перед DisconnectNamedPipe |
одинарные \ в имени канала | канал с неверным именем | unknown escape sequence: '\.' | "\\\\.\\pipe\\имя" |
| родитель не закрыл свой пишущий конец | программа «висит» после вывода потомка | — | CloseHandle(writeEnd) сразу после CreateProcessA |
дескриптор не наследуемый или bInheritHandles = FALSE | потомок пишет в никуда, родитель сразу получает конец данных | — | SECURITY_ATTRIBUTES с TRUE и TRUE в CreateProcessA |
нет STARTF_USESTDHANDLES | потомок пишет в консоль, а не в канал | — | флаг в si.dwFlags |
| прочитанное выводится как строка | мусор после данных | — | std::cout.write(buffer, read) или buffer[read] = '\0' с запасом в буфере |
| ждут, что одна запись = одно чтение (байтовый канал) | сообщения «склеиваются» или рвутся | — | режим сообщений или разделители в протоколе |
| клиент не переключил режим чтения | сообщения читаются как байты | — | SetNamedPipeHandleState(… PIPE_READMODE_MESSAGE …) |
| сообщение длиннее буфера | ReadFile = FALSE | 234 ERROR_MORE_DATA | буфер не меньше самого длинного сообщения или дочитывание |
| сервер не запущен | клиент не подключается | 2 у WaitNamedPipe | сначала сервер; понятное сообщение клиента |
| пишет много, не читая ответ | зависание обоих процессов | — | чтение в отдельном потоке или порциями |
bInheritHandles = TRUE, ненужные концы закрытьSTARTF_USESTDHANDLES и концы канала в hStdInput/hStdOutput/hStdErrorReadFile возвращает FALSE с ошибкой 109ReadFile/WriteFile обмениваются порциями: читать до конца, писать остаток; данные — байты без '\0'CreateNamedPipeA, ConnectNamedPipe, FlushFileBuffers, DisconnectNamedPipe; клиент — WaitNamedPipeA, CreateFileASetNamedPipeHandleStatechar, std::string и c_str(); раздел 11 — текстовый и двоичный режим; раздел 14 — CreateProcessA, дескрипторы, CreateFileA, printError; раздел 16 — потоки, взаимная блокировка. Нужен для: раздела 19 — клиент и сервер на сокетах, протокол «запрос — ответ»; итогового проекта «Конвейер обработки»: родитель передаёт данные потомку по каналу.