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


266

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

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

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

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

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

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

Иначе говоря, если бы я реализовал openфункцию, что бы она сделала в Linux?


13
Редактирование этого вопроса, чтобы сосредоточиться на CLinux; поскольку то, что делают Linux и Windows, отличается. В противном случае это слишком широкий. Кроме того, любой язык более высокого уровня в конечном итоге вызовет либо C API для системы, либо компиляцию до C для выполнения, поэтому выход на уровне «C» означает его наименьший общий знаменатель.
Джордж Стокер

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

Ответы:


184

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

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

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

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

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

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

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

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


2
Стоит отметить, что в Unix-подобных ОС дескрипторы файлов структуры ядра отображаются на и называются «описанием открытого файла». Таким образом, процесс FD сопоставляется с ядром OFD. Это важно для понимания документации. Например, посмотрите man dup2и проверьте тонкость между дескриптором открытого файла (то есть FD, который оказывается открытым) и описанием открытого файла (OFD).
Родриго

1
Да, разрешения проверяются во время работы. Вы можете прочитать исходный код "открытой" реализации ядра : lxr.free-electrons.com/source/fs/open.c, хотя он делегирует большую часть работы конкретному драйверу файловой системы.
pjc50

1
(в системах ext2 это будет включать чтение записей каталога, чтобы определить, в каком inode есть метаданные, и затем загрузку этого inode в кэш inode. Обратите внимание, что могут существовать псевдофайловые системы, такие как "/ proc" и "/ sys", которые могут выполнять произвольные действия когда вы открываете файл)
pjc50

1
Обратите внимание, что проверки на открытие файла - что файл существует, что у вас есть разрешение - на практике не достаточно. Файл может исчезнуть, или его разрешения могут измениться, у вас под ногами. Некоторые файловые системы пытаются предотвратить это, но пока ваша ОС поддерживает сетевое хранилище, это невозможно предотвратить (ОС может «паниковать», если локальная файловая система ведет себя плохо, и это разумно: та, которая делает это, когда сетевой ресурс не поддерживает жизнеспособная ОС). Эти проверки также выполняются при открытии файла, но должны (эффективно) выполняться при любом другом доступе к файлу.
Якк - Адам Невраумонт

2
Не забывать оценку и / или создание замков. Они могут быть общими или эксклюзивными и могут влиять на весь файл или только на его часть.
Thinkeye

83

Я бы посоветовал вам взглянуть на это руководство через упрощенную версию 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  }

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

  1. Выделите блок памяти, управляемой ядром, и скопируйте в него имя файла из памяти, контролируемой пользователем.
  2. Выберите неиспользуемый дескриптор файла, который вы можете рассматривать как целочисленный индекс, в растущий список открытых в данный момент файлов. Каждый процесс имеет свой собственный список, хотя он поддерживается ядром; Ваш код не может получить к нему доступ напрямую. Запись в списке содержит любую информацию, которую базовая файловая система будет использовать для извлечения байтов с диска, например номер индекса, разрешения процесса, флаги открытия и т. Д.
  3. 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);
    }

    который делает две вещи:

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

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

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


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

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

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


67

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


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

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

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

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


Небольшая предыстория этого ответа

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

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


10
@BillWoodger: хорошо, да. Но это справедливый вопрос (я имею в виду ваш). Я проголосовал за закрытие как «слишком широкий», и мой ответ призван проиллюстрировать, насколько чрезвычайно широкий вопрос на самом деле.
usr2564301

8
Я думаю, что вы слишком расширили ответ. У ZX Spectrum была команда OPEN, и это полностью отличалось от LOAD. И сложнее понять.
Родриго

3
Я также не согласен с закрытием вопроса, но мне очень нравится ваш ответ.
Энцо Фербер

23
Несмотря на то, что я редактировал свой вопрос, чтобы ограничиться ОС Linux / Windows, пытаясь сохранить его открытым, этот ответ полностью действителен и полезен. Как указано в моем вопросе, я не стремлюсь что-то реализовывать или заставлять других людей выполнять мою работу, я стремлюсь учиться. Чтобы учиться, вы должны задать «большие» вопросы. Если мы постоянно закрываем вопросы о SO как о «слишком широком», это рискует стать местом, где люди просто напишут ваш код для вас, не объясняя, что, где и почему. Я бы предпочел оставить его как место, где я смогу учиться.
Jramm

14
Этот ответ, кажется, доказывает, что ваша интерпретация вопроса слишком широка, а не сам вопрос слишком широк.
JWG

17

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

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


6
Какое это имеет отношение к актуальному вопросу?
Билл Вуджер

1
Он описывает, что происходит на низком уровне, когда вы открываете файл в Linux. Я согласен, что вопрос довольно широкий, так что, возможно, это был не тот ответ, который искал Джрамм.
Алекс

1
Итак, еще раз, нет проверки разрешений?
Билл Вуджер,

11

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

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

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

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

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


7

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

Именно по этим командам будет отправлено реальное чтение.

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

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


5

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

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

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


-7

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

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

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

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

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.