Будет ли заблокирован ввод-вывод во время копирования данных из ядра пользователю? - PullRequest
0 голосов
/ 21 марта 2020

Я задаю этот вопрос, потому что я смотрю на мультиплексирование ввода / вывода в Go, который использует epollwait.

Когда сокет будет готов, программа будет активирована и начнет читать сокет в неблокирующем режиме. Если системный вызов read все еще будет заблокирован во время копирования данных из ядра в пользователя, я предполагаю, что поток ядра, к которому присоединен горуин, будет также переведен в спящий режим.

Я не уверен в этом, надеясь кто-то может помочь исправить меня, если я ошибаюсь.

1 Ответ

2 голосов
/ 21 марта 2020

Мне не удается разобрать то, что вы написали.

Я постараюсь высказать предположение и заклинать, что вы, возможно, наблюдаете за тем, что системные вызовы write(2) и read(2) (и те, их родственные значения, такие как send(2) и recv(2)) на сокетах, переведенных в неблокирующий режим , могут свободно потреблять (и возвращать, соответственно) меньше данных, чем запрошено.
Другими словами, write(2) вызов на неблокирующем сокете, который велит записать 1 мегабайт данных, будет потреблять столько же данных, которые в настоящее время помещаются в ассоциированный буфер ядра, и немедленно возвращаться, сигнализируя, что он потребляет только столько данных. Следующий немедленный вызов write(2), скорее всего, вернет EWOULDBLOCK.

То же самое касается вызова read(2): если вы передадите ему буфер, достаточно большой для хранения 1 мегабайта данных, и скажете ему прочитав это количество байтов, вызов будет только истощать содержимое буфера ядра и немедленно возвращать, сигнализируя, сколько данных он фактически скопировал. Следующий немедленный вызов read(2), скорее всего, вернет EWOULDBLOCK.

Таким образом, любая попытка получить или поместить данные в сокет завершается почти сразу: либо после перемещения данных между буфером ядра и пространство пользователя или сразу - с кодом возврата EAGAIN.

Конечно, есть вероятность, что поток ОС может быть приостановлен прямо в середине выполнения такого системного вызова, но это не считается " блокировка в системном вызове. "


Обновление до исходного ответа в ответ на следующий комментарий OP:

<…>
Это то, что я вижу в книге «UNIX Сетевое программирование» (том 1, 3-й), глава 6.2:

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

Он использует «блоки» для описания неблокирующих операций ввода-вывода. Это меня смущает.
Я до сих пор не понимаю, почему книга использует «блокирует процесс», если процесс на самом деле не заблокирован.

Я могу только догадываться, что автор книги намеревался Подчеркните, что процесс действительно заблокирован с момента входа в системный вызов и до его возвращения. Читает и записывает в неблокирующий сокет do block для передачи данных, если таковые имеются, между ядром и пользовательским пространством. Мы в разговорной речи говорим, что это не блокирует, потому что мы имеем в виду "это не блокирует ожидание и ничего не делает в течение неопределенного периода времени".

Автор книги может противопоставить это так называемому асинхронный ввод-вывод (называемый «перекрытием» в Windows ™) - где вы в основном предоставляете ядру буфер с / для данных и просите его полностью избавиться от него параллельно с вашим кодом - в том смысле, что соответствующий системный вызов сразу возвращается и ввод-вывод выполняется в фоновом режиме (с учетом вашего кода пользовательского пространства).
Насколько мне известно, Go не использует функции asyn c ввода-вывода ядра ни на одной из поддерживаемых платформ. Вы можете посмотреть там о событиях, касающихся Linux и ее современной io_uring подсистемы .

О, и еще один момент. Возможно, в книге (по крайней мере на этом этапе повествования) обсуждается упрощенная схема «classi c», в которой нет потоков в процессе, а единственной единицей параллелизма является процесс (с одним потоком выполнения) , В этой схеме любой системный вызов явно блокирует весь процесс. Напротив, Go работает только на ядрах, которые поддерживают потоки, поэтому в программе Go системный вызов никогда не блокирует весь процесс - только поток, к которому он обращен.


Позвольте мне взять еще один попытаться объяснить проблему так, как я ее понимаю, - ОП изложил это.

Проблема обслуживания нескольких клиентских запросов не нова - одним из наиболее заметных первых ее утверждений является «Проблема C10k» .
Чтобы быстро повторить это, однопоточный сервер с блокировкой Операции над сокетами, которыми он управляет, реально способны обрабатывать только одного клиента за раз.
Для решения этой проблемы существует два простых подхода:

  • Форкировать копию серверного процесса для обработки каждое входящее клиентское соединение.
  • В ОС, которая поддерживает потоки, разветвите новый поток внутри того же процесса для обработки каждого входящего клиента.

У них есть свои плюсы и минусы, но оба они Отстой в отношении использования ресурсов, и, что более важно, они не очень хорошо отражают тот факт, что большинство клиентов имеют относительно низкую скорость и пропускную способность операций ввода-вывода, которые они выполняют в отношении ресурсов обработки, доступных на типичном сервере.
Другими словами, при обслуживании типичного обмена TCP / IP с клиентом обслуживающий поток m в остальное время спит в вызовах write(2) и read(2) на клиентском сокете.
Это - это то, что большинство людей имеют в виду, говоря о «блокирующих операциях» на сокетах : если сокет блокируется, и работа с ним будет блокироваться до тех пор, пока он фактически не будет выполнен, а исходящий поток будет переведен в спящий режим на неопределенное время.

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

Введите "опрос". Он борется с проблемой неэффективного использования ресурсов, замечая, что точки готовности сетевых сокетов относительно редки и находятся далеко между ними, поэтому имеет смысл иметь множество таких сокетов, обслуживаемых одним потоком: он позволяет сохранить поток почти так же, как занят как теоретически возможно, а также позволяет масштабировать при необходимости: если один поток не может справиться с потоком данных, добавить другой поток и т. д.

Этот подход, безусловно, классный, но он имеет Недостаток: код, который читает и записывает данные, должен быть переписан, чтобы использовать стиль обратного вызова вместо оригинального простого последовательного стиля. Писать с обратными вызовами сложно: вам обычно приходится реализовывать сложное управление буфером и конечные автоматы, чтобы справиться с этим.
Среда выполнения Go решает эту проблему, добавляя еще один уровень планирования для своих блоков потока выполнения - goroutines: для goroutines, операции над сокетами всегда блокируются, но когда программа собирается заблокировать сокет, это прозрачно обрабатывается путем приостановки только самой программы - до тех пор, пока запрошенная операция не сможет продолжаться - и с использованием потока, на котором выполнялась программа выполнять другую работу¹.
Это позволяет использовать один из двух подходов: программист может написать классический c последовательный код без проблем с обратным вызовом, но потоки, используемые для обработки сетевых запросов, используются полностью. *

Что касается первоначального вопроса о блокировке, то и процедура, и поток, на котором она выполняется, действительно блокируются, когда происходит передача данных через сокет, но поскольку происходит перенос данных между ядро и буфер пространства пользователя, задержка в большинстве случаев мала и ничем не отличается от случая "опроса" classi c.

Обратите внимание, что выполнение системных вызовов, включая ввод-вывод при незапрашиваемые дескрипторы - в Go (по крайней мере, до, включая Go 1. 14) блокирует блокировку как вызывающей программы, так и потока, в котором она выполняется, но обрабатывается иначе, чем в дескрипторах, подлежащих опросу: когда специальный поток мониторинга замечает, что программа потратила в системном вызове больше определенного времени ( 20 мкс, IIR C), среда выполнения вытягивает так называемый «процессор» (вещь времени выполнения, которая запускает подпрограммы в потоках ОС) из-под gorotuine и пытается заставить его запустить другую программу в другом потоке ОС; если есть программа, желающая запустить, но нет свободного потока ОС, среда выполнения Go создает другую.
Следовательно, «нормальный» блокирующий ввод / вывод все еще блокирует в Go в обоих смыслах: он блокирует обе процедуры и Потоки ОС, но планировщик Go гарантирует, что программа в целом все еще может прогрессировать.

Возможно, это идеальный случай для использования истинного асинхронного ввода-вывода, предоставляемого ядром, но это не так еще нет.


¹ См. этот класс c эссе для получения дополнительной информации.

² Среда выполнения Go, безусловно, не первая, которая стала пионером эта идея. Например, посмотрите на библиотеку потоков состояний (и более позднюю libtask), которые реализуют тот же подход в простом C; библиотека ST имеет превосходные документы, которые объясняют идею.

...