Windows - безопасно ли вызывать CloseHandle (), когда выполняется перекрывающийся ввод-вывод? - PullRequest
0 голосов
/ 27 ноября 2018

У меня есть программа, которая имеет несколько потоков, которые должны использовать один и тот же дескриптор (он открывается с помощью FILE_SHARE_READ) для записи.Что произойдет, если один поток закроет дескриптор, в то время как другой в данный момент выполняет ввод-вывод (WriteFile, в моем случае)?Блокирует ли CloseHandle до тех пор, пока запись не завершится?

Документация MSDN по этой теме выглядит очень разреженной.

Ответы [ 2 ]

0 голосов
/ 07 декабря 2018

с системного вида - это правильно, безопасно, хорошо, звоните CloseHandle в любое время.это зависит от того, существует ли какой-либо запрос ввода-вывода в файле, на который указывает этот дескриптор.еще больше вызов CloseHandle лучший способ ( начинать с перспективы, на xp - это зависит от драйвера, обычный ввод-вывод отменяется и в этом случае ) отменить все ожидающие запросы ввода-вывода

Что произойдет, если один поток закроет дескриптор, в то время как другой в данный момент выполняет ввод-вывод

, если это не последний дескриптор файла - ничего не произойдет.в противном случае (это обычный случай, и вам кажется, что вы имеете в виду именно этот случай - у вас открыт один дескриптор файла), в драйвере будет вызван обработчик IRP_MJ_CLEANUP (который создает объект устройства, на котором создан ваш файл).тогда уже нет общего и общего ответа - драйвер специфичен.однако большинство встроенных драйверов Windows на этом этапе просто завершается с некоторой ошибкой (обычно с STATUS_CANCELLED, но не всегда, скажем, npfs.sys (драйвер канала) использует STATUS_PIPE_BROKEN ошибка здесь) все ожидающие запросы ивернуть контроль.на xp / 2003 - это все.так что это обязанность водителя завершить все ожидающие запросы на данный момент.однако некоторые сторонние драйверы могут и не делать этого - поэтому некоторые операции ввода-вывода могут все еще оставаться в ожидании (неопределенное время) после закрытия дескриптора.но начать с перспективы здесь было серьезное изменение.FILE_OBJECT был расширен (в xp / 2003 поле CompletionContext было последним) - добавлено IrpList.теперь внутри FILE_OBJECT системный список некоторых активных запросов ввода-вывода в файле.когда мы отправляем асинхронный запрос IRP, он ставится в очередь в Thread (всегда перед vista) или в FILE_OBJECT - если порт IOCP связан с этим файловым объектом, ApcContext не равен 0 (из представления win32 - указатель на OVERLAPPED не равен 0) и пользовательское событие отсутствуетв IRP (из win32 просмотра OVERLAPPED.hEvent == 0).это влияет, когда система отменяет IRP - когда завершается поток или когда закрывается последний дескриптор файла.в перспективе после IRP_MJ_CLEANUP возвращение обработчика - проверка системы IrpList и, если оно не пустое, - IoCancelIrp для каждого Irp из этого списка.Конечно, драйвер снова зависел, но все драйверы правильного проектирования (включая все драйверы Microsoft) при обязательном возврате в ожидании запроса (Irp) должны вызываться обязательным регистром CancelRoutine в случае отмены указанного IRP.эта процедура вызывается, когда системный вызов IoCancelIrp (если не был удален ранее) и внутри этого драйвера программы завершает запрос с STATUS_CANCELLED.это означает, что если драйвер не завершил Irp внутри IRP_MJ_CLEANUP - система самостоятельно отменяет этот irp сразу после возврата IRP_MJ_CLEANUP, и это приводит к тому, что драйвер в любом случае завершает этот Irp внутри * CancelRoutine

, поэтому вывод - закройте последнийдескриптор файла (последний вызов CloseHandle) - эффективный способ отменить все запросы ввода-вывода для этого файла (с некоторыми исключениями для xp и только для драйверов сторонних производителей). Это законно, безопасно и эффективно (если мы хотимcancell al io и закрыть файл)

Блокирует ли CloseHandle до завершения записи?

, потому что внутри этого вызова вызывается драйвер, поставляемый обработчиком IRP_MJ_CLEANUP - может быть все.четный блок.но если говорить о встроенных драйверах Windows - этого никогда не произойдет.и даже для сторонних водителей - я никогда не рассматриваю это поведение.так на практике - CloseHandle не блокировать и не ждать, когда все запросы ввода / вывода для файла завершены

так что звонить CloseHandle безопасно в любое время, но здесь существует другая проблема, не связанная с дизайном системы, но связанная с дизайном вашего кода.в вашем коде (и обычно) дескриптор является общим ресурсом - его будут использовать несколько разных потоков.и это обычная проблема, когда объект (дескриптор в вашем случае) используется несколькими потоками - кто и когда должен закрыть дескриптор.что произойдет, если один ваш поток закроет дескриптор, когда другой поток начнет запрос ввода-вывода с этим дескриптором, после того как он будет закрыт?Вы или получили STATUS_INVALID_HANDLE от вызова API, если дескриптор не действителен в настоящее время.но возможен и более плохой случай.после закрытия дескриптора - какой-то другой код может создать другой объект.и для нового созданного объекта будет назначен последний закрытый дескриптор (система использует модель стека для свободных дескрипторов).дескриптор результата может быть действительным, когда вы начинаете операцию ввода-вывода (после закрытия дескриптора из другого потока), но .. этот дескриптор уже будет указывать на другой объект.если это не дескриптор файлового объекта (скажем, дескриптор события или потока) - вы получили STATUS_OBJECT_TYPE_MISMATCH из запроса ввода-вывода.в худшем случае - это будет дескриптор файлового объекта, но для другого файла.так что вы можете начать операцию ввода-вывода с другим файлом и непредсказуемым результатом (скажем, записать данные в другой, произвольный файл).

, поэтому вам нужно как-то защитить или синхронизировать использование и закрытие общего дескриптора между потоками.сначала подумайте - используйте подсчет ссылок для дескриптора и звоните CloseHandle, когда ссылки больше нет.но это не применимо на практике - если, скажем, какой-то постоянный поток выполняет ввод / вывод для файла (канала) (скажем, чтение из него, когда чтение завершено - просто вызовите другое чтение и т. д.) - у нас нет шансов вызвать CloseHandle издругая нить.

для конкретного примера мы вызываем ReadDirectoryChangesW для файла асинхронного.когда этот вызов завершится - просто снова позвоните ReadDirectoryChangesW и так далее.как разорвать эту петлю?установить какой-нибудь флаг остановки и позвонить CancelIoEx?но возможно, что CancelIoEx будет вызываться между 2 вызовами ReadDirectoryChangesW, когда на самом деле нет ввода-вывода в файле.так что это ничего не отменяет .. эффективный способ остановить это - вызвать CloseHandle, но если мы сделаем это без защиты - существует риск, что при следующем вызове ReadDirectoryChangesW будет использован произвольный объект (значение дескриптора будетто же самое, но оно будет недействительным или будет указывать на другой объект).используйте подсчет ссылок - не так, как здесь - кому нужен постоянный вызов ReadDirectoryChangesW, у которого должна быть собственная ссылка и ссылка никогда не будет обогащена 0. Поэтому мы никогда не называем CloseHandle и никогда не прерываем цикл.

эффективное и приятное решение здесь - используйте Защита от спада для ручки.к сожалению, не API в пользовательском режиме для поддержки этого, но не трудно самостоятельно реализовать это

возможное использование (если реализовать именно так, как это реализовано в ядре)

потоков перед использованием m_hFile, защищено m_RunRef, выполните следующий код

ULONG dwError = ERROR_INVALID_HANDLE;

if (AcquireRundownProtection(&m_RunRef))
{
  // use m_hFile in some I/O request
  // for example
  // dwError = ReadDirectoryChangesW(m_hFile, ..);
  ReleaseRundownProtection(&m_RunRef);
}

поток, который хочет закрыть дескриптор, сделать следующий

WaitForRundownProtectionRelease(&m_RunRef);
CloseHandle(m_hFile);

обратите внимание, что WaitForRundownProtectionRelease не ждет, когда завершится весь ввод-выводно только когда все другие потоки выходят из блока AcquireRundownProtection / ReleaseRundownProtection.также после вызова WaitForRundownProtectionRelease AcquireRundownProtection всегда возвращать false - так что предотвращать новое получение и что для выпуска существует.

однако я предпочитаю другую реализацию для начала отсрочки:

асинхронный вызов BeginRundown(&m_RunRef)(после чего все новые AcquireRundownProtection возвращают false) - но не ждите в этом месте.конец, когда завершение работы - обратный вызов будет вызван.и внутри этого обратного вызова мы безопасно вызываем CloseHandle - в этом месте больше нет использования дескриптора, и все старое использование дескриптора (в вызове io) уже завершено.примечание - использование дескриптора завершено - не означает, что ввод / вывод завершен - возможно, это выполняется.но обрабатывать нужно только для запуска ввода / вывода

0 голосов
/ 27 ноября 2018

В зависимости от того, насколько близок ввод / вывод к завершению, он может либо завершиться нормально, либо быть отменен.Или, если в объекте файла ядра используется ненулевое количество использования (например, DuplicateHandle()), эти операции могут продолжаться в обычном режиме, пока другой дескриптор также не будет закрыт.

CloseHandle() может блокироваться, но есливы действительно хотите дождаться завершения (успеха или отмены) ожидающего ввода-вывода, дождитесь события HANDLE в его структуре OVERLAPPED после вызова CloseHandle().

Хорошая дополнительная информация: https://community.osr.com/discussion/213975

...