What are file descriptors, explained in simple terms?
What would be a more simplified description of file descriptors compared to Wikipedia’s? Why are they required? Say, take shell processes as an example and how does it apply for it?
Does a process table contain more than one file descriptor. If yes, why?
13 Answers 13
In simple words, when you open a file, the operating system creates an entry to represent that file and store the information about that opened file. So if there are 100 files opened in your OS then there will be 100 entries in OS (somewhere in kernel). These entries are represented by integers like (. 100, 101, 102. ). This entry number is the file descriptor. So it is just an integer number that uniquely represents an opened file for the process. If your process opens 10 files then your Process table will have 10 entries for file descriptors.
Similarly, when you open a network socket, it is also represented by an integer and it is called Socket Descriptor. I hope you understand.
A file descriptor is an opaque handle that is used in the interface between user and kernel space to identify file/socket resources. Therefore, when you use open() or socket() (system calls to interface to the kernel), you are given a file descriptor, which is an integer (it is actually an index into the processes u structure — but that is not important). Therefore, if you want to interface directly with the kernel, using system calls to read() , write() , close() etc. the handle you use is a file descriptor.
There is a layer of abstraction overlaid on the system calls, which is the stdio interface. This provides more functionality/features than the basic system calls do. For this interface, the opaque handle you get is a FILE* , which is returned by the fopen() call. There are many many functions that use the stdio interface fprintf() , fscanf() , fclose() , which are there to make your life easier. In C, stdin , stdout , and stderr are FILE* , which in UNIX respectively map to file descriptors 0 , 1 and 2 .
Hear it from the Horse’s Mouth : APUE (Richard Stevens).
To the kernel, all open files are referred to by File Descriptors. A file descriptor is a non-negative number.
When we open an existing file or create a new file, the kernel returns a file descriptor to the process. The kernel maintains a table of all open file descriptors, which are in use. The allotment of file descriptors is generally sequential and they are allotted to the file as the next free file descriptor from the pool of free file descriptors. When we closes the file, the file descriptor gets freed and is available for further allotment.
See this image for more details :
When we want to read or write a file, we identify the file with the file descriptor that was returned by open() or create() function call, and use it as an argument to either read() or write().
It is by convention that, UNIX System shells associates the file descriptor 0 with Standard Input of a process, file descriptor 1 with Standard Output, and file descriptor 2 with Standard Error.
File descriptor ranges from 0 to OPEN_MAX. File descriptor max value can be obtained with ulimit -n . For more information, go through 3rd chapter of APUE Book.
File Descriptors In Linux
We have often heard that “Everything In Linux Is A File” and in this context another term which we come across is “File Descriptors”. In this module, we will have a look at what File Descriptors are in Linux and how to work with them.
What are File Descriptors?
File Descriptors are non-negative integers that act as an abstract handle to “Files” or I/O resources (like pipes, sockets, or data streams). These descriptors help us interact with these I/O resources and make working with them very easy.
Every process has it’s own set of file descriptors. Most processes (except for some daemons) have these three File Descriptors :
- stdin: Standard Input denoted by the File Descriptor 0
- stdout: Standard Output denoted by the File Descriptor 1
- stderr: Standard Error denoted by File Descriptor 2
List All File Descriptors Of A Process
Every process has its own set of File Descriptors. To list them all, we need to find its PID. For example, if I want to check all the File Descriptors under the process ‘i3‘
First, we need to find the PID of the process by using the ps command:
Now, to list all the file descriptors under a particular PID the syntax would be:
For our example, this would translate to:
Working with File Descriptors in C
Here, we have written a little C program to describe how we can use File Descriptors .
Here we are reading characters from stdin by using File Descriptor 0 [ read() at line 7 ] and then after concatenating it with a message [ strcat() at line 8 ] and then writes the resultant string to the I/On stream pointed to by File Descriptor 1, i.e, stdout [ write() at line 9 ].
Compiling and running our program :
Conclusion
Hence, we lightly touched upon file descriptors in this module. These make several operations easy as everything can be treated as a file. They are a crucial when it comes to dealing with pipes, sockets and data streams and are an integral part of the OS’s architecture.
Файловые дескрипторы Linux
Основными операциями, предоставляемыми ядром операционной системы программам (а точнее — процессам) для работы с файлами, являются системные вызовы open read, write и close. В соответствии со своими именами, эти системные вызовы предназначены для открытия и закрытия файла, для чтения из файла и записи в файл. Дополнительный системный вызов ioctl (input output control) используется для управления драйверами устройств и, как следствие, применяется в основном для специальных файлов устройств.
При запросе процесса на открытие файла системным вызовом оpen производится его однократный (относительно медленный) поиск имени файла в дереве каталогов и для запросившего процесса создается так называемый файловый дескриптор (описатель, от англ, descriptor).
Файловый дескриптор «содержит» информацию, описывающую файл, например индексный дескриптор inode файла на файловой системе, номера major и minor устройства, на котором располагается файловая система файла, режим открытия файла, и прочую служебную информацию.
При последующих операциях read и write доступ к самим данным файла происходит с использованием файлового дескриптора (что исключает медленный поиск файла в дереве каталогов).
Файловые дескрипторы пронумерованы и содержатся в таблице открытых процессом файлов, которую можно получить при помощи диагностической программы lsof.
В обратную сторону получить список процессов, открывших тот или иной файл, можно при помощи программ lsof и fuser, что бывает полезно для идентификации программ, «занявших» файловую систему, подлежащую отмонтированию.
Таблица файловых дескрипторов
$ lsof -р $$
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
. . . . . . . . .
bash 17975 john 1u CHR 136,2 0t0 5 /dev/pts/2
# lsof /dev/log
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
rsyslogd 543 syslog 0u unix 0xefef5680 0t0 1338 /dev/log
# fuser /dev/log
# ps p 543
PID TTY STAT TIME COMMAND
543 ? Sl 0:43 rsyslogd -c5
# lsof /var/log/syslog
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
rsyslogd 543 syslog lw REG 252,0 29039 26214496 /var/log/syslog
В первом примере из листинга выше показано получение списка файловых дескрипторов (столбец FD) процесса командного интерпретатора bash пользователя john, на котором файловый дескриптор номер 1 описывает открытый на чтение и запись и специальный символьный CHR файл устройства /dev/pts/2.
Во втором примере показано получение информации о процессе, открывшем файловый сокет unix с именем /dev/log (файловый дескриптор номер 0 на чтение и запись u) и обычный файл REG с именем /var/log/sysog (файловый дескриптор номер 1 на запись w).
Пронаблюдать за использованием системных вызовов файлового программного интерфейса в момент выполнения программам позволяет системный трассировщик strace.
Трассировка файлового программного интерфейса
$ date
Вт. окт. 15 18:17:42 MSK % 2018
$ strace -fe open, close, read, write, ioctl date
open(«/etc/localtime», 0_RDONLY|0_CLOEXEC) = 3
$ file /etc/localtime
/etc/localtime: timezone data, version 2, 13 gmt time flags, 13 std time flags, no leap
seconds, 77 transition tines, 13 abbreviation char
[email protected]:
$ ls -la /dev/dvd
lrwxrwxrwx 1 root root 3 окт. 16 18:09 /dev/dvd -> sr0
$ strace -fe open,close, read,write,ioctl eject
$ strace -fe open, read,write,close,ioctl setleds -L +num +scroll
ioctl(0, KDGKBLED, 0xbfe4f4ff) = 0
ioctl(0, KDGETLED, 0xbfe4f4fe) = 0
ioctl(0, KDSETLED, 0x3) = 0
Предположив, что программа date показывает правильное московское время, потому что узнаёт заданную временную зону MSK из некоего конфигурационного файла операционной системы, при трассировке ее «работы можно установить его точное имя — /etc/localtime.
Аналогично предположив, что программа eject открывает лоток привода CD/DVD при помощи специального файла устройства, при трассировке можно узнать имя файла /dev/sr0, номер файлового дескриптора при работе с файлом 3 и команду CDROMEJECT соответствующего устройству
драйвера ioctl_List.
Трассировка команды setleds показывает, что она вообще не открывает никаких файлов, но пользуется файловым дескриптором о так называемого стандартного потока ввода (прикрепленного, к текущему терминалу) и командами kdgetled и kdsetled драйвера консоли console_ioctl.
5.1. Обзор механизмов ввода-вывода в Linux
В языке C для осуществления файлового ввода-вывода используются механизмы стандартной библиотеки языка, объявленные в заголовочном файле stdio.h. Как вы вскоре узнаете консольный ввод-вывод — это не более чем частный случай файлового ввода-вывода.
В C++ для ввода-вывода чаще всего используются потоковые типы данных. Однако все эти механизмы являются всего лишь надстройками над низкоуровневыми механизмами вводавывода ядра операционной системы.
С точки зрения модели КИС (Клиент-Интерфейс-Сервер), сервером стандартных механизмов ввода вывода языка C (printf, scanf, FILE*, fprintf, fputc и т. д.) является библиотека языка. А сервером низкоуровневого ввода-вывода в Linux, которому посвящена эта глава книги, является само ядро операционной системы.
Пользовательские программы взаимодействуют с ядром операционной системы посредством специальных механизмов, называемых системными вызовами (system calls, syscalls). Внешне системные вызовы реализованы в виде обычных функций языка C, однако каждый раз вызывая такую функцию, мы обращаемся непосредственно к ядру операционной системы. Список всех системных вызовов Linux можно найти в файле /usr/include/asm/unistd.h. В этой главе мы рассмотрим основные системные вызовы, осуществляющие ввод-вывод: open(), close(), read(), write(), lseek() и некоторые другие.
5.2. Файловые дескрипторы
В языке C при осуществлении ввода-вывода мы используем указатель FILE*. Даже функция printf() в итоге сводится к вызову vfprintf(stdout. ), разновидности функции fprintf(); константа stdout имеет тип struct _IO_FILE*, синонимом которого является тип FILE*. Это я к тому, что консольный ввод-вывод — это файловый ввод-вывод. Стандартный поток ввода, стандартный поток вывода и поток ошибок (как в C, так и в C++) — это файлы. В Linux все, куда можно что-то записать или откуда можно что-то прочитать представлено (или может быть представлено) в виде файла. Экран, клавиатура, аппаратные и виртуальные устройства, каналы, сокеты — все это файлы. Это очень удобно, поскольку ко всему можно применять одни и те же механизмы ввода-вывода, с которыми мы и познакомимся в этой главе. Владение механизмами низкоуровневого ввода-вывода дает свободу перемещения данных в Linux. Работа с локальными файловыми системами, межсетевое взаимодействие, работа с аппаратными устройствами, — все это осуществляется в Linux посредством низкоуровневого ввода-вывода.
Вы уже знаете из предыдущей главы, что при запуске программы в системе создается новый процесс (здесь есть свои особенности, о которых пока говорить не будем). У каждого процесса (кроме init) есть свой родительский процесс (parent process или просто parent), для которого новоиспеченный процесс является дочерним (child process, child). Каждый процесс получает копию окружения (environment) родительского процесса. Оказывается, кроме окружения дочерний процесс получает в качестве багажа еще и копию таблицы файловых дескрипторов .
Файловый дескриптор (file descriptor) — это целое число (int), соответствующее открытому файлу. Дескриптор, соответствующий реально открытому файлу всегда больше или равен нулю. Копия таблицы дескрипторов (читай: таблицы открытых файлов внутри процесса) скрыта в ядре. Мы не можем получить прямой доступ к этой таблице, как при работе с окружением через environ. Можно, конечно, кое-что «вытянуть» через дерево /proc, но нам
это не надо. Программист должен лишь понимать, что каждый процесс имеет свою копию таблицы дескрипторов. В пределах одного процесса все дескрипторы уникальны (даже если они соответствуют одному и тому же файлу или устройству). В разных процессах дескрипторы могут совпадать или не совпадать — это не имеет никакого значения, поскольку у каждого процесса свой собственный набор открытых файлов.
Возникает вопрос: сколько файлов может открыть процесс? В каждой системе есть свой лимит, зависящий от конфигурации. Если вы используете bash или ksh (Korn Shell), то можете воспользоваться внутренней командой оболочки ulimit, чтобы узнать это значение.
Если вы работаете с оболочкой C-shell (csh, tcsh), то в вашем распоряжении команда limit:
$ limit descriptors descriptors 1024
В командной оболочке, в которой вы работаете (bash, например), открыты три файла: стандартный ввод (дескриптор 0), стандартный вывод (дескриптор 1) и стандартный поток ошибок (дескриптор 2). Когда под оболочкой запускается программа, в системе создается новый процесс, который является для этой оболочки дочерним процессом, следовательно, получает копию таблицы дескрипторов своего родителя (то есть все открытые файлы родительского процесса). Таким образом программа может осуществлять консольный вводвывод через эти дескрипторы. На протяжении всей книги мы будем часто играть с этими дескрипторами.
Таблица дескрипторов, помимо всего прочего, содержит информацию о текущей позиции чтения-записи для каждого дескриптора. При открытии файла, позиция чтения-записи устанавливается в ноль. Каждый прочитанный или записанный байт увеличивает на единицу указатель текущей позиции. Мы вернемся к этой теме в разделе 5.7.
5.3. Открытие файла: системный вызов open()
Чтобы получить возможность прочитать что-то из файла или записать что-то в файл, его нужно открыть. Это делает системный вызов open(). Этот системный вызов не имеет постоянного списка аргументов (за счет использования механизма va_arg); в связи с этим существуют две «разновидности» open(). Не только в С++ есть перегрузка функций 😉 Если интересно, то о механизме va_arg можно прочитать на man-странице stdarg (man 3 stdarg) или в книге Б. Кернигана и Д. Ритчи «Язык программирования Си». Ниже приведены прототипы системного вызова open().
int open (const char * filename, int flags, mode_t mode); int open (const char * filename, int flags);
Системный вызов open() объявлен в заголовочном файле fcntl.h. Ниже приведен прототип open().
int open (const char * filename, int flags, . );
Начнем по порядку. Первый аргумент — имя файла в файловой системе в обычной форме: полный путь к файлу (если файл не находится в текущем каталоге) или сокращенное имя (если файл в текущем каталоге).
Второй аргумент — это режим открытия файла, представляющий собой один или несколько флагов открытия, объединенных оператором побитового ИЛИ. Список доступных флагов приведен в Приложении 2.. Наиболее часто используют только первые семь флагов. Если вы хотите, например, открыть файл в режиме чтения и записи, и при этом автоматически создать файл, если такового не существует, то второй аргумент open() будет выглядеть примерно так: O_RDWR|O_CREAT . Константы-флаги открытия объявлены в заголовочном файле bits/fcntl.h, однако не стоит включать этот файл в свои программы, поскольку он уже включен в файл fcntl.h.
Третий аргумент используется в том случае, если open() создает новый файл. В этом случае файлу нужно задать права доступа (режим), с которыми он появится в файловой системе. Права доступа задаются перечислением флагов, объединенных побитовым ИЛИ. Вместо флагов можно использовать число (как правило восьмиричное), однако первый способ нагляднее и предпочтительнее. Список флагов приведен в Приложении 2. Чтобы, например, созданный файл был доступен в режиме «чтение-запись» пользователем и группой и «только чтение» остальными пользователями, — в третьем аргументе open() надо указать примерно следующее: S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH или 0664 . Флаги режима доступа реально объявлены в заголовочном файле bits/stat.h, но он не предназначен для включения в пользовательские программы, и вместо него мы должны включать файл sys/stat.h. Тип mode_t объявлен в заголовочном файле sys/types.h.
Если файл был успешно открыт, open() возвращает файловый дескриптор, по которому мы будем обращаться к файлу. Если произошла ошибка, то open() возвращает -1.
5.4. Закрытие файла: системный вызов close()
Системный вызов close() закрывает файл. Вообще говоря, по завершении процесса все открытые файлы (кроме файлов с дескрипторами 0, 1 и 2) автоматически закрываются. Тем не менее, это не освобождает нас от самостоятельного вызова close(), когда файл нужно закрыть. К тому же, если файлы не закрывать самостоятельно, то соответствующие дескрипторы не освобождаются, что может привести к превышению лимита открытых файлов. Простой пример: приложение может быть настроено так, чтобы каждую минуту открывать и перечитывать свой файл конфигурации для проверки обновлений. Если каждый раз файл не будет закрываться, то в моей системе, например, приложение может аварийно завершиться примерно через 17 часов. Автоматически! Кроме того, файловая система Linux поддерживает механизм буферизации. Это означает, что данные, которые якобы записываются, реально записываются на носитель (синхронизируются) только через какое-то время, когда система сочтет это правильным и оптимальным. Это повышает производительность системы и даже продлевает ресурс жестких дисков. Системный вызов close() не форсирует запись данных на диск, однако дает больше гарантий того, что данные останутся в целости и сохранности.
Системный вызов close() объявлен в файле unistd.h. Ниже приведен его прототип.
int close (int fd);
Очевидно, что единственный аргумент — это файловый дескриптор. Возвращаемое значение — 0 в случае успеха, и -1 — в случае ошибки. Довольно часто close() вызывают без проверки
возвращаемого значения. Это не очень грубая ошибка, но, тем не менее, иногда закрытие файла бывает неудачным (в случае неправильного дескриптора, в случае прерывания функции по сигналу или в случае ошибки ввода-вывода, например). В любом случае, если программа сообщит пользователю, что файл невозможно закрыть, это хорошо.
Теперь можно написать простенкую программу, использующую системные вызовы open() и close(). Мы еще не умеем читать из файлов и писать в файлы, поэтому напишем программу, которая создает файл с именем, переданным в качестве аргумента (argv[1]) и с правами доступа 0600 (чтение и запись для пользователя). Ниже приведен исходный код программы.