Как работает потоковая передача файлов?

Мне уже давно интересно, как работает потоковая передача файлов? При потоковой передаче файлов я имею в виду доступ к частям файла без загрузки всего файла в память.
Я (верю) знаю, что классы С++ (i|o)fstream делают именно это, но как это реализовано? Возможно ли реализовать потоковое воспроизведение файлов?
Как он работает на низком уровне C/С++ (или на любом языке, поддерживающем потоки файлов)? Выполняют ли функции C fopen, fclose, fread, а указатель FILE* уже заботится о потоковой передаче (т.е. Не загружает весь файл в память)? Если нет, то как вы будете читать непосредственно с жесткого диска и есть ли такой объект alread, реализованный в C/С++?

Любые ссылки, подсказки, указатели в правильном направлении уже будут очень полезны. Я googled, но, похоже, Google не совсем понимает, что мне нужно...


Ninja-Edit. Если кто-нибудь знает что-либо о том, как это работает на уровне сборки/машинного кода, и если это возможно реализовать самостоятельно или если вы должны полагаться на системные вызовы, это будет здорово.:) Не требование для ответа, хотя ссылка в правильном направлении была бы приятной.

Ответы

Ответ 1

На самом низком уровне (по крайней мере, для кода пользователя) вы будете использовать системные вызовы. На UNIX-подобных платформах они включают:

  • open
  • close
  • read
  • write
  • lseek

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

Существуют также вызовы Windows API, похожие на системные вызовы в UNIX:

Windows проходит вокруг HANDLE s, которые похожи на файловые дескрипторы, но, я считаю, немного менее гибкими. (например, в UNIX файловые дескрипторы могут не только представлять файлы, но также сокеты, трубы и т.д.)

Стандартные функции библиотеки C fopen, fclose, fread, fwrite и fseek - это просто обертки вокруг этих системных вызовов.

Когда вы открываете файл, обычно содержимое содержимого не считывается в память. Когда вы используете fread или read, вы указываете операционной системе считывать определенное количество байтов в буфер. Это определенное количество байтов может быть, но не обязательно, длиной файла. Таким образом, при желании вы можете прочитать только часть файла в памяти.

Ответ на ninja-edit:

Вы спросили, как это работает на уровне машинного кода. Я могу только объяснить, как это работает в Linux и 32-битной архитектуре Intel. Когда вы используете системный вызов, некоторые аргументы помещаются в регистры. После того, как аргументы помещаются в регистры, прерывается 0x80. Так, например, чтобы прочитать один килобайт от stdin (дескриптор файла 0) до адреса 0xDEADBEEF, вы можете использовать этот код сборки:

mov eax, 0x03       ; system call number (read = 0x03)
mov ebx, 0          ; file descriptor (stdin = 0)
mov ecx, 0xDEADBEEF ; buffer address
mov edx, 1024       ; number of bytes to read
int 0x80 ; Linux system call interrupt

int 0x80 вызывает прерывание программного обеспечения, которое обычно регистрирует операционная система в таблице векторов прерываний или таблице дескриптора прерывания. Во всяком случае, процессор перейдет в определенное место в памяти. Как только там, как правило, операционная система переходит в режим ядра (если необходимо), а затем выполняет эквивалент C switch на eax. Оттуда он перейдет в реализацию для read. В read он обычно считывает некоторые метаданные об дескрипторе из таблицы дескриптора файла вызывающего процесса. После того, как он имеет все необходимые ему данные, он делает свои вещи, а затем возвращается к пользовательскому коду.

Чтобы "сделать свой материал", допустим, что он читает с диска, а не через трубку или stdin или какое-то другое нефизическое место. Пусть также предполагается, что он читается с основного жесткого диска. Кроме того, предположим, что операционная система все еще может обращаться к прерываниям BIOS.

Чтобы получить доступ к файлу, ему нужно сделать кучу файловой системы. Например, перемещая дерево каталогов, чтобы найти, где находится фактический файл. Я не собираюсь это делать, много, так как я уверен, вы можете догадаться.

Интересной частью является чтение данных с диска, будь то метаданные файловой системы, содержимое файла или что-то еще. Сначала вы получаете логический адрес блока (LBA). LBA - это всего лишь индекс блока данных на диске. Каждый блок обычно составляет 512 байтов (хотя этот показатель может быть датирован). Тем не менее, предполагая, что у нас есть доступ к BIOS, и его использует ОС, он затем преобразует LBA в нотацию CHS. Обозначение CHS (Cylinder-Head-Sector) - это еще один способ ссылки на части жесткого диска. Он привык к физическим концепциям, но в настоящее время он устарел, но почти каждый BIOS поддерживает его. Оттуда ОС будет загружать данные в регистры и прерывать прерывание 0x13, прерывание чтения диска BIOS.

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

Ответ 2

На самом низком уровне на платформах POSIX открытые файлы представлены "дескрипторами" в пользовательском пространстве. Дескриптор файла - это целое число, которое уникально в открытых файлах в любой момент времени. Дескриптор используется для определения того, к какому открытому файлу должна применяться операция, когда вы просите ядро ​​выполнить эту операцию. Таким образом, read(0, charptr, 1024) выполняет чтение из открытого файла, связанного с дескриптором 0 (по соглашению это, вероятно, будет стандартным вводом процесса).

Насколько доступно пользовательскому пространству, единственными частями файла, загружаемого в память, являются те, которые необходимы для выполнения операции типа read. Для чтения байтов из середины файла поддерживается другая операция - "искать". Это указывает ядру изменить положение смещения в определенном файле. Следующая операция read (или write) будет работать с байтами из этого нового смещения. Таким образом, lseek(123, 100, SEEK_SET) устанавливает смещение для файла, связанного с 123 (значение дескриптора, которое я только что составил), в позицию 100-го байта. Следующее прочитанное в 123 будет начинаться с этой позиции, а не с начала файла (или там, где ранее было смещение). И любые неиспользуемые байты не нужно загружать в память.

За кулисами немного сложнее - диск обычно не может читать меньше, чем "блок", который обычно имеет мощность около 4096; ядро, вероятно, делает дополнительное кэширование и что-то называемое "readahead". Но это оптимизация, и основная идея - это то, что я описал выше.