Файловые дескрипторы в дочернем процессе

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

Для контекста этот код представляет собой шаг выполнения базовой оболочки Linux. Объект команды содержит аргументы команд, «имя» ввода-вывода и дескрипторы ввода-вывода (думаю, я мог бы избавиться от дескрипторов файлов в виде полей).

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

1) С моим массивом файловых дескрипторов, используемых для обработки каналов, у родителя есть копия всех этих дескрипторов. Когда дескрипторы, удерживаемые родителем, закрыты? И даже более того, какие дескрипторы? Это все из них? Все из них остались неиспользованными исполняемыми командами?

2) Какие процессы при обработке каналов внутри дочерних процессов остаются открытыми? Скажите, если я выполню команду: ls -l | Grep
«[username]», какие дескрипторы следует оставить открытыми для процесса ls? Просто конец записи трубы? И если да, то когда? Тот же вопрос относится к команде grep.

3) Когда я обрабатываю перенаправление ввода-вывода в файл, новый файл должен быть открыт и дублирован на STDOUT (я не поддерживаю перенаправление ввода). Когда этот дескриптор закрывается? Я видел в примерах, что он закрывается сразу после вызова dup2, но как тогда что-то записывается в файл, если файл был закрыт?

Спасибо заранее Я застрял на этой проблеме в течение нескольких дней, и я действительно хотел бы закончить с этим проектом.

РЕДАКТИРОВАТЬ Я обновил его с помощью измененного кода и примера вывода для всех, кто заинтересован в предложении конкретной помощи по моей проблеме. Сначала у меня есть весь цикл for, который обрабатывает выполнение. Это было обновлено с моими вызовами, чтобы закрыть на различных файловых дескрипторах.

При выполнении этого кода я получаю следующий вывод

Выход из ls -l > output.txt подразумевает, что я закрываю неправильный дескриптор, но закрытие другого связанного дескриптора, не отображая ошибки, не обеспечивает вывод в файл. Как продемонстрировано ls -l , grep "cook" , должен генерировать вывод на консоль.

Решение

С моим массивом файловых дескрипторов, используемых для обработки каналов, родительский
имеет копию всех этих дескрипторов. Когда дескрипторы хранятся
родитель закрыт? И даже более того, какие дескрипторы? Это все из
их? Все из них остались неиспользованными исполняемыми командами?

Дескриптор файла может быть закрыт одним из 3 способов:

  1. Вы явно звоните close() в теме.
  2. Процесс завершается, и операционная система автоматически закрывает каждый дескриптор файла, который все еще был открыт.
  3. Когда процесс вызывает один из семи exec() функции и файловый дескриптор имеет O_CLOEXEC флаг.

Как видите, в большинстве случаев файловые дескрипторы остаются открытыми, пока вы не закроете их вручную. Это то, что происходит в вашем коде тоже — так как вы не указали O_CLOEXEC дескрипторы файлов не закрываются при вызове дочернего процесса execvp() , У ребенка они закрываются после того, как ребенок заканчивается. То же самое касается родителей. Если вы хотите, чтобы это произошло в любое время до завершения, вы должны вручную вызвать close() ,

При обработке каналов внутри дочерних элементов, дескрипторы которых остаются
открыть какими процессами? Скажите, если я выполню команду: ls -l | Grep
«[username]», какие дескрипторы должны быть оставлены открытыми для ls
процесс? Просто конец записи трубы? И если да, то когда? Такой же
вопрос относится к команде grep.

Вот (грубое) представление о том, что делает оболочка при вводе ls -l | grep "username" :

  1. Оболочка вызывает pipe() создать новую трубу. Дескрипторы файла канала наследуются потомками на следующем шаге.
  2. Оболочка дважды разветвляется, давайте назовем эти процессы c1 а также c2 , Давайте предположим c1 побежит ls а также c2 побежит grep ,
  3. В c1 канал чтения канала закрыт close() , а затем он вызывает dup2() с каналом записи трубы и STDOUT_FILENO чтобы написать stdout эквивалентно записи в трубу. Затем один из семи exec() функции вызывается, чтобы начать выполнение ls , ls пишет stdout , но так как мы продублировали stdout на канал записи канала, ls буду писать в трубу.
  4. В c2 , происходит обратное: канал записи канала закрыт, а затем dup2() призван сделать stdin указать канал чтения канала. Затем один из семи exec() функции вызывается, чтобы начать выполнение grep , grep читает из stdin , но так как мы dup2() стандартный ввод в канал чтения канала, grep буду читать из трубы.

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

Итак, когда вы звоните dup2(a, b) Либо одно из них верно:

  • a == b , В этом случае ничего не происходит и dup2() возвращается преждевременно. Нет файловых дескрипторов закрыты.
  • a != b , В этом случае, b закрывается при необходимости, а затем b сделано для ссылки на ту же запись таблицы файлов, что и a , Запись таблицы файлов представляет собой структуру, которая содержит текущее смещение файла и флаги состояния файла; несколько файловых дескрипторов могут указывать на одну и ту же запись таблицы файлов, и это именно то, что происходит, когда вы дублируете файловый дескриптор. Так, dup2(a, b) имеет эффект создания a а также b поделиться одной и той же записи таблицы файлов. Как следствие, писать a или же b в конечном итоге запись в тот же файл. Таким образом, файл, который закрыт b не a , если ты dup2(a, STDOUT_FILENO) ты закрываешь stdout и вы делаете stdout дескриптор файла указывает на ту же запись таблицы файлов, что и a , Любая программа, которая пишет в stdout будет вместо этого писать в файл, так как stdout Описатель файла указывает на файл, который вы скопировали.
Читайте также:  Работа с фрагментами изображения

ОБНОВИТЬ:

Итак, для вашей конкретной проблемы, вот что я должен сказать после краткого просмотра кода:

Вы не должны звонить close(STDOUT_FILENO) здесь:

Если вы закроете stdout , вы получите ошибку в будущем, когда вы попытаетесь написать stdout , Вот почему вы получаете ls: write error: Bad file descriptor , В конце концов, ls пишет stdout , но вы закрыли это. К сожалению!

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

Итак, продолжайте и сделайте это:

Обратите внимание на финал if необходимо: если outfd случайно оказывается равным STDOUT_FILENO Вы не хотите закрывать его по причинам, которые я только что упомянул.

То же самое относится и к коду внутри else if (command->getOutputFD() == REDIRECTAPPEND) : хочешь закрыть outfd скорее, чем STDOUT_FILENO :

Это должно, по крайней мере, заставить вас ls -l работать как положено.

Что касается проблемы с трубами: ваше управление трубами на самом деле не правильно. Из кода, который вы показали, где и как не понятно pipefd и сколько каналов вы создаете, но обратите внимание, что:

  1. Процесс никогда не сможет читать из канала и записывать в другой канал. Например, если outfile не является STDOUT а также infile не является STDIN в итоге вы закрываете каналы чтения и записи (и, что еще хуже, после закрытия канала чтения вы пытаетесь продублировать его). Это никак не сработает.
  2. Родительский процесс закрывает каждый канал перед ожиданием завершения дочерних процессов. Это провоцирует состояние гонки.
Читайте также:  Весы кухонные hotter отзывы

fork() — системный вызов в Unix-подобных операционных системах, создающий новый процесс (потомок), который является практически полной копией процесса-родителя, выполняющего этот вызов.

Концепция ветвления процессов впервые описана в 1962 году Мелвином Конвеем [en] и в 1964 году реализована в форме системного вызова в Project Genie [en] , откуда заимствована Томпсоном при реализации Unix; позднее вызов включён в стандарт POSIX.

Между процессом-потомком, порождаемым вызовом fork() , и процессом-родителем существуют различия:

  • PID процесса-потомка отличен от PID процесса-родителя;
  • значению PPID процесса-потомка присваивается значение PID процесса-родителя;
  • процесс-потомок получает собственную таблицу файловых дескрипторов, являющуюся копией таблицы процесса-родителя на момент вызова fork() ; это означает, что открытые файлы наследуются, но если процесс-потомок, например, закроет какой-либо файл, то это не повлияет на таблицу дескрипторов процесса-родителя;
  • для процесса-потомка очищаются все ожидающие доставки сигналы;
  • временная статистика выполнения процесса-потомка в таблицах ОС обнуляется;
  • блокировки памяти и записи, установленные в процессе-родителе, не наследуются.

После вызова fork() алгоритм обычно разветвляется (в случае успешного выполнения функции fork() , она возвращает PID процесса-потомка родительскому процессу и нуль — процессу-потомку. Если порождение процесса-потомка закончилось неудачей, функция fork() возвращает значение −1).

После fork() процесс-потомок чаще всего выполняет системный вызов exec() , загружающий в пространство процесса новую программу (именно так, и только так, в Unix-системе выполняется запуск программы в отдельном процессе). Так, первый (нулевой) процесс Unix (ядро системы) создаёт свою копию, чтобы запустить init (процесс с P >

Некоторые программы создают дочерние процессы не для запуска другой программы, а для выполнения параллельной задачи. Так, например, поступают простые сетевые серверы — при подсоединении клиента, сервер создаёт свою копию (дочерний процесс), которая обслуживает клиентское соединение и завершается по его закрытии. Родительский же процесс продолжает ожидать новых соединений.

Вызов fork() выполняется довольно долго, так как требует копирования большого количества данных. Для того чтобы это обойти, некоторые сетевые серверы (например, веб-серверы Apache и Lighttpd), создают дочерние процессы заранее, чтобы уменьшить время отклика сервера. Также существуют «облегчённые» реализации fork() (например, в ядре Linux [1] ), отображающие в новый процесс страницы памяти родительского, вместо того чтобы их копировать (новая страница создаётся только при изменении её содержимого одним из процессов), что существенно снижает время создания нового процесса (техника copy-on-write).

Персональные инструменты

Дескриптор файла – это целое число без знака, с помощью которого процесс обращается к открытому файлу. Количество дескрипторов файлов, доступных процессу, ограничено параметром /OPEN_MAX, заданным в файле sys/limits.h. Кроме того, количество дескрипторов файлов можно задать с помощью флага -n команды ulimit . Дескрипторы файлов создаются при выполнении функций open , pipe , creat и fcntl . Обычно каждый процесс работает с уникальным набором дескрипторов. Однако эти же дескрипторы могут применяться и дочерними процессами, созданными с помощью функции fork. Кроме того, дескрипторы можно скопировать с помощью функций fontal , dup и dup2 .

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

Содержание

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

Стандарт POSIX.1 заменил «магические числа» 0,1,2 символическими константами STDIN_FILENO, STDOUT_FILENO и STDERR_FILENO соответственно.

Файловые дескрипторы могут принимать значения от 0 до OPEN_MAX. Старые версии UNIX имели верхний предел до 19, позволяя одному процессу открывать до 20 файлов. Сейчас это значение увеличено до нескольких тысяч.

Дескрипторы файлов выполняют роль индексов таблицы дескрипторов, которая расположена в области u_block и создается ядром для каждого процесса. Чаще всего процесс получает дескрипторы с помощью операций open и creat , а также путем наследования от родительского процесса. При выполнении операции fork таблица дескрипторов копируется для дочернего процесса. В результате дочерний процесс получает право обращаться к файлам родительского процесса.

Читайте также:  Что такое данные кэша в памяти телефона

Таблицы дескрипторов файлов и системные таблицы открытых файлов

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

Таблица дескрипторов файлов

Преобразует индексы таблицы (дескрипторы файлов) в указатели на открытые файлы. Для каждого процесса в области u_block создается своя собственная таблица дескрипторов. Каждая запись такой таблицы содержит следующие поля: поле флагов и указатель на файл. Допустимо не более OPEN_MAX дескрипторов файлов. Таблица дескрипторов файлов имеет следующую структуру:

Таблица открытых файлов

Содержит записи с информацией обо всех открытых файлах. В записи этой таблицы хранится текущее смещение указателя в файле, которое используется во всех операциях чтения и записи в файл, а также режим открытия файла (O_RDONLY, O_WRONLY или O_RDWR). В структуре таблицы открытых файлов хранится смещение указателя в файле. При выполнении операции чтения-записи система выполняет неявный сдвиг указателя. Например, при чтении или записи x байт указатель также будет перемещен на x байт. Для изменения положения указателя в файлах с прямым доступом применяется функция seek . Для потоковых файлов (например, каналов и сокетов) понятие смещения не поддерживается, так как произвольный доступ к этим файлам невозможен.

Управление дескрипторами файлов

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

Несколько обращений к файлу может потребоваться в следующих случаях:

  • Файл открыт еще одним процессом
  • Дочерний процесс унаследовал дескрипторы файлов, открытых родительским процессом
  • Дескриптор файла скопирован с помощью функции fcntl или dup

Совместная работа с открытыми файлами

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

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

Копирование дескрипторов файлов

Существуют следующие способы копирования дескрипторов файлов: функция dup или dup2 , функция fork и функция fcntl .

Функции dup и dup2

Функция fork

Функция fcntl

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

При запуске программы в оболочке открывается три дескриптора 0, 1 и 2. По умолчанию с ними связаны следующие файлы:

Стандартный ввод.
1 Стандартный вывод.
2 Стандартный вывод сообщений об ошибках.

Перечисленные дескрипторы файлов связаны с терминалом. Это означает, что при чтении данных из файла с дескриптором 0 программа получает ввод с терминала, а при записи данных в файлы с дескрипторами 1 и 2 они выводятся на терминал. При открытии других файлов дескрипторы присваиваются в порядке возрастания.

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

В данном примере дескриптор 0 будет связан с файлом FileX, а дескриптор 1 – с файлом FileY. Дескриптор 2 не будет изменен. Программе достаточно знать, что дескриптор 0 представляет файл ввода, а дескрипторы 1 и 2 – файлы вывода. Информация о том, с какими конкретно файлами связаны эти дескрипторы, ей не нужна.

В следующем примере программы продемонстрировано перенаправление стандартного вывода:

При получении запроса на дескриптор выделяется первый свободный дескриптор из таблицы дескрипторов (дескриптор с наименьшим номером). Однако с помощью функции dup файлу можно присвоить любой дескриптор.

Ограничение на число дескрипторов файлов

Максимальное число дескрипторов, которое может использоваться в одном процессе, ограничено. Значение по умолчанию указывается в файле /etc/security/limits и обычно равно 2000. Для изменения ограничения можно воспользоваться командой ulimit или функцией setrlimit . Максимальное число определяется константой OPEN_MAX.