Где и почему копирование системных вызовов read (2) и write (2) в пользовательское пространство и из него? - PullRequest
2 голосов
/ 30 апреля 2020

Я недавно читал о sendfile(2), и на странице руководства говорится:

   sendfile() copies data between one file descriptor and another.
   Because this copying is done within the kernel, sendfile() is more
   efficient than the combination of read(2) and write(2), which would
   require transferring data to and from user space.

Это заставило меня задуматься, почему именно комбинация read() / write() помедленнее? Страница man ориентирована на дополнительное копирование, которое должно происходить в пространство пользователя и из него, а не на общее количество требуемых вызовов. Я кратко взглянул на код ядра для чтения и записи, но не увидел копию.

Почему копия существует в первую очередь? Разве ядро ​​не может просто прочитать из переданного буфера на write() без предварительного копирования всего этого в пространство ядра?

А как насчет асинхронных интерфейсов ввода-вывода, таких как AIO и io_uring? Они тоже копируют?

1 Ответ

4 голосов
/ 30 апреля 2020

почему именно комбинация read() / write() медленнее?

На странице руководства об этом совершенно ясно. Выполнение read(), а затем write() требует копирования данных два раза.

Почему копия существует вообще?

Это должно быть совершенно очевидно : поскольку вы вызываете read, вы хотите данные, которые будут скопированы в память вашего процесса в указанном целевом буфере. То же самое касается write: вы хотите данные, которые будут скопированы из памяти вашего процесса. Ядро на самом деле не знает, что вы просто хотите сделать read + write, и этого можно избежать двухкратного копирования туда и обратно.

При выполнении read данные копируются ядром из файлового дескриптора в память процесса. При выполнении write данные копируются ядром из памяти процесса в дескриптор файла.

Не удалось ядру просто прочитать из переданного буфера на write() без предварительного копирования все это в пространство ядра?

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

Содержимое файла уже присутствует в ядре память, отображаемая как страница памяти (или более). В случае read данные должны быть скопированы со страницы памяти ядра этого файла в память процесса, в то время как в случае write данные должны быть скопированы из память процесса до страницы памяти ядра файла. Затем ядро ​​обеспечит правильную запись данных на странице (страницах) памяти ядра, соответствующей файлу, при необходимости (, если вообще необходимо).

Теоретически, этого «промежуточного» сопоставления ядра можно было бы избежать, и файл мог бы быть отображен непосредственно в память пространства пользователя, но тогда приложению пришлось бы управлять им вручную, что сложно и легко испортить. Вот почему для обычных файловых операций файлы отображаются в память ядра. Ядро предоставляет высокоуровневые API-интерфейсы для взаимодействия с ними пользовательских программ, и тяжелая работа остается за самим ядром.

Системный вызов sendfile намного быстрее, поскольку вам не нужно выполнять копирование два раза. , но только один раз. Предполагая, что вы хотите сделать sendfile из файла A в файл B, ядру нужно только скопировать данные из A в B. Однако в случае read + write ядру необходимо сначала скопировать из A в ваш процесс, а затем из вашего процесса в B. Эта двойная копия, конечно, медленнее, и если вам действительно не нужно читать или манипулировать данными, то это пустая трата времени.

Я кратко рассмотрел код ядра для читать и писать, но не видел копию.

С точки зрения кода ядра весь процесс чтения файла очень сложен, но в итоге ядро ​​делает «специальную» версию. из memcpy(), называемый copy_to_user(), который копирует содержимое файла из памяти ядра в память пространства пользователя (делая соответствующие проверки перед выполнением фактической копии). Более конкретно, для файлов используется функция copyout(), но поведение очень схожее, оба в конечном итоге вызывают raw_copy_to_user() (который зависит от архитектуры).

А как насчет асинхронных интерфейсов ввода-вывода, таких как AIO и io_uring? Они также копируют?

Функции aio_{read,write} lib c, определенные POSIX, являются просто асинхронными оболочками вокруг read и write (т.е. они все еще используют read и write под капотом). Они по-прежнему копируют данные в / из пространства пользователя.

io_uring может обеспечить операции нулевого копирования при использовании флага O_DIRECT open (см. Справочную страницу ):

O_DIRECT (since Linux 2.4.10)

    Try to minimize cache effects of the I/O to and from this
    file.  In general this will degrade performance, but it is
    useful in special situations, such as when applications do
    their own caching.  File I/O is done directly to/from user-
    space buffers.  The O_DIRECT flag on its own makes an effort
    to transfer data synchronously, but does not give the
    guarantees of the O_SYNC flag that data and necessary metadata
    are transferred.  To guarantee synchronous I/O, O_SYNC must be
    used in addition to O_DIRECT.  See NOTES below for further
    discussion.

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

См. Также эту связанную подробную ответ об асинхронном вводе-выводе и этой статье LWN на io_uring.

...