Что делает открытие файла на самом деле?

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

Но что делает эта открытая операция?

Страницы руководства для типичных функций фактически не говорят вам ничего, кроме "открывает файл для чтения/записи":

http://www.cplusplus.com/reference/cstdio/fopen/

https://docs.python.org/3/library/functions.html#open

Очевидно, что использование функции, которую вы можете сказать, предполагает создание какого-то объекта, который облегчает доступ к файлу.

Другой способ поставить это, если бы я должен был реализовать open функцию, что ей нужно делать в Linux?

Ответ 1

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

Вот почему аргументы функции библиотеки fopen или Python open очень напоминают аргументы системного вызова open(2).

В дополнение к открытию файла эти функции обычно устанавливают буфер, который, следовательно, будет использоваться с операциями чтения/записи. Назначение этого буфера состоит в том, чтобы гарантировать, что всякий раз, когда вы хотите читать N байтов, соответствующий вызов библиотеки возвращает N байтов, независимо от того, обращаются ли вызовы к базовым системным вызовам меньше.

Я действительно не заинтересован в реализации моей собственной функции; просто понимая, что, черт возьми, происходит... "за пределами языка", если хотите.

В Unix-подобных операционных системах успешный вызов open возвращает "файловый дескриптор", который является просто целым числом в контексте пользовательского процесса. Этот дескриптор, следовательно, передается любому вызову, который взаимодействует с открытым файлом, и после вызова close на нем дескриптор становится недействительным.

Важно отметить, что вызов open действует как точка проверки, при которой выполняются различные проверки. Если не все условия выполнены, вызов завершается неудачей, возвращая -1 вместо дескриптора, а вид ошибки указывается в errno. Существенные проверки:

  • Имеется ли файл,
  • Разрешен ли вызывающему процессу открыть этот файл в указанном режиме. Это определяется путем сопоставления разрешений файлов, идентификатора владельца и идентификатора группы с соответствующим идентификатором вызывающего процесса.

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

Ответ 2

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

0  int sys_open(const char *filename, int flags, int mode) {
1      char *tmp = getname(filename);
2      int fd = get_unused_fd();
3      struct file *f = filp_open(tmp, flags, mode);
4      fd_install(fd, f);
5      putname(tmp);
6      return fd;
7  }

Вкратце, вот что делает этот код, строка за строкой:

  • Выделите блок управляемой ядром памяти и скопируйте в нее имя файла из управляемой пользователем памяти.
  • Выберите неиспользуемый дескриптор файла, который вы можете представить как индекс целого в растущий список открытых файлов. Каждый процесс имеет свой собственный список, хотя он поддерживается ядром; ваш код не может получить к нему доступ напрямую. Запись в списке содержит любую информацию, которую будет использовать базовая файловая система, чтобы вытащить байты с диска, такие как номер inode, разрешения процесса, открытые флаги и т.д.
  • Функция filp_open имеет реализацию

    struct file *filp_open(const char *filename, int flags, int mode) {
            struct nameidata nd;
            open_namei(filename, flags, mode, &nd);
            return dentry_open(nd.dentry, nd.mnt, flags);
    }
    

    который выполняет две функции:

    • Используйте файловую систему для поиска inode (или, в общем, любого типа внутреннего идентификатора, используемого файловой системой), соответствующего имени файла или пути, который был передан.
    • Создайте struct file с необходимой информацией об inode и верните его. Эта структура становится записью в этом списке открытых файлов, о которых я упоминал ранее.
  • Сохранить ( "установить" ) возвращенную структуру в список процессов открытых файлов.

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

Если вы чувствуете амбициозность, вы можете сравнить этот упрощенный пример с реализацией системного вызова open() в ядре Linux, функции do_sys_open(). Вам не должно быть никаких проблем с поиском сходства.


Конечно, это только "верхний уровень" того, что происходит, когда вы вызываете open() - или, точнее, это кусок ядра самого высокого уровня, который вызывается при открытии файла. Язык программирования высокого уровня может добавить к нему дополнительные слои. Там много, что происходит на более низких уровнях. (Спасибо Ruslan и pjc50 для объяснения.) Грубо, сверху вниз:

  • open_namei() и dentry_open() вызывать код файловой системы, который также является частью ядра, для доступа к метаданным и контенту для файлов и каталогов. filesystem читает необработанные байты с диска и интерпретирует эти шаблоны байтов как дерево файлов и каталогов.
  • Файловая система использует уровень блочного устройства, снова часть ядра, чтобы получить эти необработанные байты с диска. (Удовлетворительный факт: Linux позволяет вам получать доступ к исходным данным с уровня блочного устройства с помощью /dev/sda и т.п.).
  • Уровень блочного устройства вызывает драйвер устройства хранения данных, который также является кодом ядра, для перевода с инструкций среднего уровня, таких как "чтение сектора X" на отдельные инструкции ввода/вывода в машинный код. Существует несколько типов драйверов устройств хранения, в том числе IDE, (S) ATA, SCSI, Firewire и т.д., что соответствует различным стандартам связи, которые может использовать диск. (Обратите внимание, что именование - беспорядок.)
  • Инструкции ввода/вывода используют встроенные возможности процессорной микросхемы и контроллера материнской платы для отправки и приема электрических сигналов на провод, идущий на физический диск. Это аппаратное обеспечение, а не программное обеспечение.
  • На другом конце провода прошивка диска (встроенный управляющий код) интерпретирует электрические сигналы, чтобы вращать планшеты и перемещать головки (HDD), или читать флэш-память (SSD), или все, что необходимо для доступ к данным на этом типе устройства хранения.

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

Ответ 3

Любая файловая система или операционная система, о которой вы хотите поговорить, в порядке. Ницца!


На ZX-спектре инициализация команды LOAD приведет систему в замкнутый цикл, читая строку Audio In.

Начало данных указывается постоянным тоном, после чего последовательность длинных/коротких импульсов сопровождается коротким импульсом для двоичного 0 и более длинного для двоичного 1 (https://en.wikipedia.org/wiki/ZX_Spectrum_software). Контейнер с плотной нагрузкой собирает биты до тех пор, пока он не заполнит байт (8 бит), сохранит его в памяти, увеличит указатель на память, а затем снова вернется к сканированию большего количества бит.

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

Состояние конечного файла может быть распознано путем приема как можно большего количества байтов (либо фиксированное количество байтов, либо аппаратное обеспечение в программном обеспечении, либо число переменных, указанное в заголовке). Произошла ошибка, если цикл загрузки не получил импульс в ожидаемом диапазоне частот в течение определенного промежутка времени.


Немного истории об этом ответе

Описанная процедура загружает данные с обычной аудиокассеты - следовательно, необходимо сканировать Audio In (она связана со стандартным подключением к магнитофонам). Команда LOAD технически совпадает с open файлом, но физически связана с фактической загрузкой файла. Это связано с тем, что магнитофон не управляется компьютером, и вы не можете (успешно) открыть файл, но не загружать его.

"Плотная петля" упоминается, потому что (1) CPU, Z80-A (если память используется), была очень медленной: 3,5 МГц и (2) у Спектрума не было внутренних часов! Это означает, что он должен был точно подсчитывать количество T-состояний (время обучения) для каждого. Один. инструкция. внутри этого цикла, просто для поддержания точного времени сигнала.
К счастью, эта низкая скорость ЦП имела явное преимущество в том, что вы могли бы рассчитать количество циклов на листе бумаги и, следовательно, в реальном мире, которое они будут делать.

Ответ 4

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

В Linux каждый файл распознается структурой inode. Каждая структура имеет уникальный номер, и каждый файл получает только один номер inode. Эта структура хранит метаданные для файла, например размер файла, разрешения файлов, метки времени и указатель на блоки диска, однако это не само имя самого файла. Каждый файл (и каталог) содержит запись имени файла и номер inode для поиска. Когда вы открываете файл, если у вас есть соответствующие разрешения, создается дескриптор файла с использованием уникального номера inode, связанного с именем файла. Поскольку многие процессы/приложения могут указывать на один и тот же файл, inode имеет поле ссылки, которое поддерживает общее количество ссылок на файл. Если файл присутствует в каталоге, его количество ссылок равно единице, если у него есть жесткая ссылка, счетчик ссылок будет равен двум, и если файл будет открыт процессом, количество ссылок будет увеличено на 1.

Ответ 5

Бухгалтерия, в основном. Это включает в себя различные проверки типа "существует ли файл?". и "У меня есть разрешения на открытие этого файла для записи?".

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

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

  • Система может отслеживать все файлы, которые в настоящее время открыты, и предотвращать их удаление (например).
  • Современные ОС построены вокруг ручек - там много полезных вещей, которые вы можете делать с ручками, и все разные ручки ведут себя почти одинаково. Например, когда операция асинхронного ввода-вывода завершается в дескрипторе файла Windows, ручка сигнализируется - это позволяет блокировать дескриптор до тех пор, пока он не будет сигнализирован, или полностью выполнить операцию асинхронно. Ожидание дескриптора файла точно такое же, как ожидание на ручке потока (сигнализируется, например, когда заканчивается поток), дескриптор процесса (опять же, сигнализируется при завершении процесса) или сокет (когда завершается какая-либо асинхронная операция). Также важно, чтобы ручки принадлежали их соответствующим процессам, поэтому, когда процесс неожиданно прекращается (или приложение плохо написано), ОС знает, какие дескрипторы он может освободить.
  • Большинство операций являются позиционными - вы read от последней позиции в вашем файле. Используя дескриптор для определения определенного "открытия" файла, вы можете иметь несколько параллельных ручек в один и тот же файл, каждый из которых считывается из своих мест. В некотором смысле дескриптор выступает в качестве перемещаемого окна в файл (и способ выдавать асинхронные запросы ввода-вывода, которые очень удобны).
  • Ручки намного меньше, чем имена файлов. Ручкой обычно является размер указателя, обычно 4 или 8 байтов. С другой стороны, имена файлов могут иметь сотни байтов.
  • Ручки позволяют ОС перемещать файл, даже если приложения его открывают - дескриптор по-прежнему действителен и по-прежнему указывает на тот же файл, даже если имя файла изменилось.

Также есть и другие трюки, которые вы можете сделать (например, обмениваться дескрипторами между процессами с каналом связи без использования физического файла, в системах Unix файлы также используются для устройств и различных других виртуальных каналов, так что это не " t строго необходимо), но они действительно не привязаны к самой операции open, поэтому я не буду вникать в это.

Ответ 6

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

Это на тех командах, которые будет отправлено фактическое чтение.

OS часто получает начало чтения при запуске операции чтения, чтобы заполнить буфер, связанный с дескриптором. Затем, когда вы на самом деле читаете, он может сразу вернуть содержимое буфера, а затем ждать на диске IO.

Для открытия нового файла для записи ОС необходимо будет добавить запись в каталог для нового (в настоящее время пустого) файла. И снова создается дескриптор, на котором вы можете выдавать команды записи.

Ответ 7

В принципе, вызов open должен найти файл, а затем записать все, что ему нужно, чтобы последующие операции ввода-вывода могли найти его снова. Это довольно расплывчато, но это будет верно для всех операционных систем, о которых я могу сразу подумать. Специфика варьируется от платформы к платформе. Многие ответы уже здесь говорят о современных настольных операционных системах. Я немного программировал на CP/M, поэтому я буду предлагать свои знания о том, как это работает на CP/M (MS-DOS, вероятно, работает одинаково, но по соображениям безопасности обычно это делается не так, как сегодня).

В CP/M у вас есть вещь, называемая FCB (как вы упомянули C, вы можете назвать ее структурой, это действительно 35-байтная непрерывная область в ОЗУ, содержащая различные поля). FCB имеет поля для записи имени файла и целочисленного (4-разрядного) целого числа, определяющего диск. Затем, когда вы вызываете ядро ​​Open File, вы передаете указатель на эту структуру, помещая его в один из регистров процессора. Спустя некоторое время операционная система возвращается со структурой, слегка изменившейся. Независимо от того, что вы делаете с этим файлом, вы передаете указатель на эту структуру системному вызову.

Что делает CP/M с этим FCB? Он резервирует определенные поля для собственного использования и использует их для отслеживания файла, поэтому вам лучше не трогать их изнутри вашей программы. Операция "Открыть файл" выполняет поиск по таблице в начале диска для файла с тем же именем, что и в FCB (символ подстановки "?" Соответствует любому символу). Если он находит файл, он копирует некоторую информацию в FCB, включая физическое местоположение (файлы) файла на диске, так что последующие вызовы ввода-вывода в конечном итоге вызывают BIOS, который может передать эти места в драйвер диска. На этом уровне специфика различна.

Ответ 8

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

Команда open генерирует системный вызов, который, в свою очередь, копирует содержимое файла из вторичного хранилища (жесткий диск) в основное хранилище (Ram).

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

Надеюсь, что это поможет.