Что такое файловый дескриптор в linux
Перейти к содержимому

Что такое файловый дескриптор в linux

  • автор:

File Descriptors, Input and Output redirection and Piping Data in Linux shell

Shirish Pokharel

Streams, redirection, and pipes are some powerful commandline tools in Linux. Linux treats input and output as streams, which is basically a data entity that can be manipulated. Obviously, input comes from keyboard and output is displayed on the screen but you can redirect them from and to files or as input to second command. This makes easier to tie up two commands to complete one task.

Understanding File Descriptors

We’ll first start by understanding file descriptors. Linux treats every object as files including input and output streams. Linux uses file descriptors to identify a particular file object:

Standard Input (STDIN)

It’s file descriptor is 0 (zero). Programs accept input from STDIN. In most cases, it is the input that comes from the keyboard.

Standard Output (STDOUT)

Programs send their output to STDOUT. It is normally displayed on the screen either in a virtual console or terminal emulator. It’s file descriptor is 1 (one).

Standard Error (STDERR)

Programs send high-priority output such as error to STDERR. Normally, STDERR is sent to the same output device as STDOUT but you can redirect it as well. For instance, you could dump all errors into one file to view them later. It’s file descriptor is 2 (two).

Programs treat STDIN, STDOUT, and STDERR as data files like they write to them, they read data from them. This is why file descriptors are necessary and why they can be used in redirection.

Redirecting Input and Output

To redirect input or output, you type the command followed by an operator and possibly with a file descriptor. For instance, to redirect standard output of echo , the command would be like:

A new file ‘path.txt’ is created which contains the output of echo $PATH command. However, to redirect standard output, you can omit the file descriptor and it would work exactly same. For example,

This works same as the previous example however the redirection operator (>) is still needed. You can also omit standard input file descriptor when using appropriate redirection operator.

Below are some redirection operators with their corresponding functions:

If the specified file exists, it is overwritten with standard output otherwise a new file is created containing standard output. No file descriptor necessary.

Appends standard output to the end of the existing file. If no such file exists, a new file is created. No file descriptor necessary.

If the specified file exists, it is overwritten with standard error otherwise new file is created containing standard error. File descriptor necessary.

Appends standard error to the end of the existing file. If no such file exists, a new file is created. File descriptor necessary.

If the specified file exists, it is overwritten with both standard output and standard error otherwise a new file is created containing both STDOUT and STDERR. No file descriptor necessary.

Sends the contents of a specified file as standard input. No file descriptor necessary.

Accepts the contents of following lines as standard input. No file descriptor necessary.

Uses specified file for both standard input and standard output.

A common trick is to redirect standard error or standard output tois a special file in Unix systems known as null device. It is also called bit-bucket or blackhole because it immediately discards anything written to it and only returns EOF(End Of File) when read. So, you can redirect anything you won’t need and that is polluting the output screen to.

The << operator is a bit tricky. It launches the programs specified on the left of the operator, grabs the input from proceeding lines up to and except EOF value. EOF value is specified on the right side of the operator.

In the above example, cat is the program to be executed and END is the EOF value. Some programs take input from command line and expect you to terminate input by pressing Ctrl+D, which corresponds to end-of-file.

Piping data between programs

In Linux commandline, programs frequently operate on another’s program output. For instance, you could redirect output of one command to another text editing program. You can do this using redirection operators: redirect one program’s standard output to one file, and instruct another program to read from that file. This method is tedious, and it creates unnecessary clutter in your system. The solution is to use pipes (aka pipelines), pipes can redirect standard output of one program to standard input of another program without needing to create another file. Pipe is an operator that looks like a vertical bar (|).

Suppose that, the first command generates CPU time, system log, users activity and so on but you’re interested in knowing only a portion of information. You can redirect standard output of first command to be used as standard input of second command which echos only the information you need, one program that does this is grep. Pipes can be of any length, there could be third, fourth, fifth program too.

A common redirection tool often used with pipes is tee command. tee command stores standard input to a file and also displays it in standard output. If you want to view the output of a command and also save it to a text file at the same time you can use the tee command.

By default, tee command overwrites any file. To append to an existing command use tee -a command.

Файл дескриптор в Linux с примерами

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

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

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

Интервьюер прервал меня на последнем слове, дополнив свой вопрос: «Предположим, что данные нам не нужны, это просто дебаг лог, но приложение не работает из-за того, что не может записать дебаг»?

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

«ну хорошо», сказал я, «если мы не можем перезапускать приложение и данные нам не важны, то мы можем просто очистить этот открытый файл через файл дескриптор, даже если мы его не видим в команде ls на файловой системе».

Интервьюер остался доволен, а я нет.

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

Тузик

В начале моей карьеры я пытался создать небольшое приложение, в котором нужно было хранить информацию о пользователях. И тогда я думал, а как мне сопоставить пользователя к его данным. Есть, например, у меня Иванов Иван Иваныч, и есть у него какие-то данные, но как их подружить? Я могу указать напрямую, что собака по имени «Тузик» принадлежит этому самому Ивану. Но что, если он сменит имя и вместо Ивана станет, например, Олей? Тогда получится, что наша Оля Ивановна Иванова больше не будет иметь собаки, а наш Тузик все еще будет принадлежать несуществующему Ивану. Решить эту проблему помогла база данных, которая каждому пользователю давала уникальный идентификатор (ID), и мой Тузик привязывался к этому ID, который, по сути, был просто порядковым номером. Таким образом хозяин у тузика был с ID под номером 2, и на какой-то момент времени под этим ID был Иван, а потом под этим же ID стала Оля. Проблема человечества и животноводства была практически решена.

Файл дескриптор

Проблема файла и программы, работающей с этим файлом, примерно такая же как нашей собаки и человека. Предположим я открыл файл под именем ivan.txt и начал в него записывать слово tuzik, но успел записать только первую букву «t» в файл, и этот файл был кем-то переименован, например в olya.txt. Но файл остался тем же самым, и я все еще хочу записать в него своего тузика. Каждый раз при открытии файла системным вызовом open в любом языке программирования я получаю уникальный ID, который указывает мне на файл, этот ID и есть файл дескриптор. И совершенно не важно, что и кто делает с этим файлом дальше, его могут удалить, его могут переименовать, ему могут поменять владельца или забрать права на чтение и запись, я все равно буду иметь к нему доступ, потому что на момент открытия файла у меня были права для его чтения и/или записи и я успел начать с ним работать, а значит должен продолжать это делать.

В Linux библиотека libc открывает для каждого запущенного приложения(процесса) 3 файл дескриптора, с номерами 0,1,2. Больше информации вы можете найти по ссылкам man stdio и man stdout

  • Файл дескриптор 0 называется STDIN и ассоциируется с вводом данных у приложения
  • Файл дескриптор 1 называется STDOUT и используется приложениями для вывода данных, например командами print
  • Файл дескриптор 2 называется STDERR и используется приложениями для вывода данных, сообщающих об ошибке

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

Например, откроем консоль с bash и посмотрим PID нашего процесса

Во второй консоли запустим

Файл дескриптор с номером 255 можете смело игнорировать в рамках данной статьи, он был открыт для своих нужд уже самим bash, а не прилинкованной библиотекой.

Сейчас все 3 файл дескриптора связаны с устройством псевдотерминала /dev/pts, но мы все равно можем ими манипулировать, например запустим во второй консоли

И в первой консоли мы увидим

Redirect и Pipe

Вы можете легко переопределить эти 3 файл дескриптора в любом процессе, в том числе и в bash, например через трубу(pipe), соединяющую два процесса, смотрим

Вы можете сами запустить эту команду с strace -f и увидеть, что происходит внутри, но я вкратце расскажу.

Наш родительский процесс bash с PID 15771 парсит нашу команду и понимает сколько именно команд мы хотим запустить, в нашем случае их две: cat и sleep. Bash знает что ему нужно создать два дочерних процесса, и объединить их одной трубой. Итого bash потребуется 2 дочерних процесса и один pipe.

Перед созданием дочерних процессов bash запускает системный вызов pipe и получает новые файл дескрипторы на временный буфер pipe, но этот буфер никак пока не связывает наши два дочерних процесса.

Для родительского процесса это выглядит так будто pipe уже есть, а дочерних процессов еще нет:

Затем с помощью системного вызова clone bash создает два дочерних процесса, и наши три процесса будут выглядеть так:

Не забываем, что clone клонирует процесс вместе со всеми файл дескрипторами, поэтому в родительском процессе и в дочерних они будут одинаковые. Задача родительского процесса с PID 15771 следить за дочерними процессами, поэтому он просто ждет ответ от дочерних.

Следовательно pipe ему не нужен, и он закрывает файл дескрипторы с номерами 3 и 4.

В первом дочернем процессе bash с PID 9004, системным вызовом dup2, меняет наш STDOUT файл дескриптор с номером 1 на файл дескриптор указывающий на pipe, в нашем случае это номер 3. Таким образом все, что первый дочерний процесс с PID 9004 будет писать в STDOUT, будет автоматически попадать в буфер pipe.

Во втором дочернем процессе с PID 9005 bash меняет с помощью dup2 файл дескриптор STDIN с номером 0. Теперь все, что будет читать наш второй bash с PID 9005, будет читать из pipe.

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

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

Далее в первом дочернем процессе с PID 9004 bash запускает с помощью системного вызова exec исполняемый файл, который мы указали в командной строке, в нашем случае это /usr/bin/cat.

Во втором дочернем процессе с PID 9005 bash запускает второй исполняемый файл, который мы указали, в нашем случае это /usr/bin/sleep.

Системный вызов exec не закрывает файл дескрипторы, если они не были открыты с флагом O_CLOEXEC во время выполнения вызова open. В нашем случае после запуска исполняемых файлов все текущие файл дескрипторы сохранятся.

Проверяем в консоли:

Как видите уникальный номер нашего pipe у нас в обоих процессах совпадает. Таким образом у нас есть связь между двумя разными процессами с одним родителем.

Для тех, кто не знаком с системными вызовами, которые использует bash, крайне рекомендую запустить команды через strace и посмотреть, что происходит внутри, например, так:

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

Запустим программу и посмотрим на файл дескрипторы

Как видим у нас есть наши 3 стандартные файл дескрипторы и еще один, который мы открыли. Проверим размер файла:

данные пишутся, пробуем поменять права на файл:

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

Куда пишутся данные? И пишутся ли вообще? Проверяем:

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

Смотрим на размер файла:

Размер файла 19923457. Пробуем очистить файл:

Как видим размер файла только увеличивается и наш транкейт не сработал. Обратимся к документации по системному вызову open. Если при открытии файла мы используем флаг O_APPEND, то при каждой записи операционная система проверяет размер файла и пишет данные в самый конец файла, причем делает это атомарно. Это позволяет нескольким тредам или процессам писать в один и тот же файл. Но в нашем коде мы не используем этот флаг. Мы можем увидеть другой размер файла в lsof после транкейт только если откроем файл для дозаписи, а значит в нашем коде вместо

мы должны поставить

Проверяем с «w» флагом

Программируем уже запущенный процесс

Часто программисты при создании и тестировании программы используют дебагеры (например GDB) или различные уровни логирования в приложении. Linux предоставляет возможность фактически писать и менять уже запущенную программу, например менять значения переменных, устанавливать breakpoint и тд и тп.

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

Создадим файл для нашего раздела, который мы подмонтируем как отдельный диск:

Создадим файловую систему:

Подмонтируем файловую систему:

Создаем директорию с нашим владельцем:

Откроем файл только на запись в нашей программе:

Ждем несколько секунд

Итак, мы получили проблему, описанную в начале этой статьи. Свободного места 0, занятого 100%.

Мы помним, что по условиям задачи мы пытаемся записать очень важные данные, которые нельзя потерять. И при этом нам нужно починить сервис без перезапуска процесса.

Допустим, у нас все же есть место на диске, но в другом разделе, например в /home.

Попробуем «перепрограммировать на лету» наш код.

Смотрим PID нашего процесса, который съел все место на диске:

Подключаемся к процессу через gdb

Смотрим открытые файл дескрипторы:

Смотрим информацию о файл дескрипторе с номером 3, который нас интересует

Помня о том, какой системный вызов делает Python (смотрите выше, где мы запускали strace и находили вызов open), обрабатывая наш код для открытия файла, мы делаем то же самое самостоятельно от имени нашего процесса, но биты O_WRONLY|O_CREAT|O_TRUNC нам нужно заменить на числовое значение. Для этого открываем исходники ядра, например тут и смотрим какие флаги за что отвечают

#define O_WRONLY 00000001
#define O_CREAT 00000100
#define O_TRUNC 00001000

Объединяем все значения в одно, получаем 00001101

Запускаем наш вызов из gdb

Итак мы получили новый файл дескриптор с номером 4 и новый открытый файл на другом разделе, проверяем:

Мы помним пример с pipe — как bash меняет файл дескрипторы, и уже выучили системный вызов dup2.

Пробуем подменить один файл дескриптор другим

Закрываем файл дескриптор 4, так как нам он не нужен:

И выходим из gdb

Проверяем новый файл:

Как видим, данные пишутся в новый файл, проверяем старый:

Данные не потеряны, приложение работает, логи пишутся в новое место.

Немного усложним задачу

Представим, что данные нам важны, но места на диске у нас нет ни в одном из разделов и подключить диск мы не можем.

Что мы можем сделать, так это перенаправить куда-то наши данные, например в pipe, а данные из pipe в свою очередь перенаправить в сеть через какую-либо программу, например netcat.
Мы можем создать именованный pipe командой mkfifo. Она создаст псевдофайл на файловой системе, даже если на ней нет свободного места.

Перезапускаем приложение, и проверяем:

Места на диске нет, но мы успешно создаем там именованный pipe:

Теперь нам надо как-то завернуть все данные, что попадают в этот pipe на другой сервер через сеть, для этого подойдет все тот же netcat.

На сервере remote-server.example.com запускаем

На нашем проблемном сервере запускаем в отдельном терминале

Теперь все данные, которые попадут в pipe автоматически попадут на stdin в netcat, который их отправит в сеть на порт 7777.

Все что нам осталось сделать это начать писать наши данные в этот именованный pipe.

У нас уже есть запущенное приложение:

Из всех флагов нам нужен только O_WRONLY так как файл уже существует и очищать нам его не нужно

Проверяем удаленный сервер remote-server.example.com

Данные идут, проверяем проблемный сервер

Данные сохранились, проблема решена.

Пользуясь случаем, передаю привет коллегам из компании Degiro.
Слушайте подкасты Радио-Т.

В качестве домашнего задания предлагаю подумать, что будет в файл дескрипторах процесса cat и sleep если запустить такую команду:

What are File Descriptors in Linux

In this article, you will learn everything about file descriptors, like their uses in Linux, what a file descriptor table is, how to view the file descriptors under a specific process, and how to change the limit of a file descriptor in Linux.

Table of Contents

What are File Descriptors in Linux?

A file descriptor is a positive integer that acts as a unique identifier (or handle) for “files” and other I/O resources, such as pipes, sockets, blocks, devices, or terminal I/O.

All the file descriptor records are kept in a file descriptor table in the kernel. When a file is opened, a new file descriptor (or integer value) is given to that file in the file descriptor table.

For example, if you open a “example_file1.txt” file (which is nothing but a process), it will be allocated with the available file descriptor (for example, 101), and a new entry will be created in the file descriptor table.

And when you open another file like “example_file2.txt“, it will be allocated to another available file descriptor like 102, and another entry will be created in the file descriptor table.

File Descriptor Process
101 example_file1.txt
102 example_file2.txt

The file descriptor for the referenced file will be available for use by another process once you close the file.

Short Recap : A file descriptor is a unique, non-negative number that is given to each process or other I/O resource (when they make a successful request) in the kernel’s file descriptor table. Once the file is closed, the file descriptor can be given to another process.

So, when you open hundreds of files or other I/O resources in your Linux system, there will be 100 entries in the file descriptor table, and each entry will reference a unique file descriptor (or integer value like 100, 102, 103…) to identify the file.

What is the File Descriptor Table in Linux?

When a process or I/O device makes a successful request, the kernel returns a file descriptor to that process and keeps the list of current and all running process file descriptors in the file descriptor table, which is somewhere in the kernel.

Now, your process might depend on other system resources like input and output; as this event is also a process, it also has a file descriptor, which will be attached to your process in the file descriptor table.

Each file descriptor in the file descriptor table points to an entry in the kernel’s global file table. The file table entry maintains the record of file (or other I/O resource) modes like (r)ead, (w)rite, and (e)xecute.

Also, the file table entry points to a third table known as the inode table that points to actual file information like size, modification date, pointer, etc.

Kernel table

Kernel table

Predefined File Descriptors

By default, three types of standard POSIX file descriptors exist in the file descriptor table, and you might already be familiar with them as data streams in Linux:

File Descriptor Name Abbreviation
0 Standard Input stdin
1 Standard Output stdout
2 Standard Error stderr

Apart from them, every other process has its own set of file descriptors, but few of them (except for some daemons) also utilize the above mentioned file descriptors to handle input, output, and errors for the process.

To make sure that the process is using the above file descriptor, just look for the above file descriptor (in integer format) under “/proc/PID/fd/“, where PID stands for “process identifier.”

For example, I’ve started the GEDIT editor on my system, which uses all of the file descriptors mentioned above, as shown.

Checking the predefined file descriptor for a process

Checking the predefined file descriptor for a process

List all of a Running Process’s File Descriptors

As you just learned, each running process in Linux has its own set of file descriptors, but it also uses others to identify the specific file when communicating with kernel space via system calls or library calls.

Find the Process ID (or PID)

First, find out your process identifier (or PID) using the ps command before viewing the file descriptors under it.

Replace “gedit” with your running process name, or you can place “$$” to pass the current bash session.

Finding the PID for the referenced process

Finding the PID for the referenced process

Now, you have two ways to list the file descriptors under a specific process, followed by:

Using the ls command

List all of the file descriptors and the files they refer to under a certain PID by listing the content of the “/proc/PID/fd/” path, where PID is the process ID using the ls command.

Listing the process file descriptors using the ls command

Listing the process file descriptors using the ls command

Using the lsof command

The lsof command is used to list the information of running processes in the system and can also be used to list the file descriptor under a specific PID.

For that, use the “ -d ” flag to specify a range of file descriptors, with the “ -p ” option specifying the PID. To combine this selection, use the “ -a ” flag.

Listing the process file descriptors using the lsof command

Listing the process file descriptors using the lsof command

What is the Purpose of File Descriptors in the First Place?

The file descriptor, along with the file table, keep track of each running process’s permissions in your system and maintain data integrity.

A running process can inherit the functionality of another process by inheriting its file descriptor, as you just learned in this article.

What Happens If You Run Out of File Descriptors?

This is crucial because a file descriptor is an integer value that the kernel returns to the process (or other I/O resource) after a successful attempt to open a file.

There is a limit to the number of file descriptors (or integer values) that can be given to a process. When that limit is reached, data can be lost.

In Linux, generally, there are two types of file descriptors: process-level file descriptors and system-level file descriptors.

Process-Level File Descriptor Limits

Check the current process-level file descriptor limit using the ulimit command.

Checking the process-level file descriptor limits

Checking the process-level file descriptor limits

Reset the limit by adding a custom positive number after the command.

Note that non-root users are also able to use the above command to change the process-level limits (<Kernel 2.4.x), but you need to add the following lines in “/etc/security/limits.conf” to assign the user modification permission:

System-Level File Descriptor Limits

Check the limit of the system-level descriptor using the cat command.

Checking the system-level file descriptor limits

Checking the system-level file descriptor limits

Modify the file with the new value by using the “>” redirection symbol.

After modifying the above file, modify the value in the “nr_open” file.

Final Tips!

I hope this article makes it clear for you to understand the workings of file descriptors in Linux computing.

If you have any questions or queries related to this topic, then feel free to ask them in the comment section.

Что такое файловый дескриптор в Linux

Файловый дескриптор в Linux характеризует процесс в системе, то как процесс использует при своей работе диск.

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

Файловый дескриптор в Linux

Файловый дескриптор может существовать на двух уровнях: общесистемном и для каждого запущенного процесса.

Файловые дескрипторы для каждого процесса (fdtable) не уникальны, но привязываются к v-node таблице, которая содержит значения для всей системы.

Системные вызовы fopen() и fileno() возвращают номера дескрипторов для процесса, то есть fdtable. Далее будем рассматривать только эту таблицу.

Дескрипторы привязываются к процессу.

Каждый процесс имеет ввод, вывод и вывод ошибок, значения соответствуют определенному коду.

  • 0 — STDIN
  • 1 — STDOUT
  • 2 — STDERROR

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

Для примера запустим процесс в фоне, за счёт амперсанда открепляем вывод (stdout) от консоли и можем работать в консоли при запущенном процессе.

[1] 9590

В консоль будет выведен идентификатор процесса, это в примере 9590. По нему можно смотреть информацию в частности при помощи утилиты lsof (list open files).

файловый дескриптор linux

Здесь интерес представляют четвертая и пятая колонки: FD и TYPE (файловый дескриптор и тип файлового дескриптора).

FD и TYPE для дескриптора

Для FD могут быть такие значения:

  • cwd – Current Working Directory
  • txt – Text file
  • mem – Memory mapped file
  • mmap – Memory mapped device

Номер дескриптора можно увидеть в последних строках вывода lsof в колонке FD: 0u, 1u и так далее.

Цифра это номер дескриптора. Буква показывает в каком режиме открыт файл.

r — чтение

w — запись

u — чтение и запись

Значения TYPE могут быть такими:

  • REG – Regular File
  • DIR – Directory
  • FIFO – First In First Out

Еще можно увидеть дескрипторы в каталоге процесса (9590 — номер процесса)

0 1 2 3 4

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

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *