Хотите построить простую оболочку? Вот как это сделать.

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

Предпосылки для чтения:

  • Базовое понимание функциональности языка программирования C, включая указатели, двойные указатели, mallocing и функциональное программирование. "Ресурс".
  • Базовое понимание Linux. "Ресурс".
  • Понимание того, как файл проходит процесс компиляции с gcc. "Ресурс".
  • Готовность изучать новые концепции.

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

Начнем с простого

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

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

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

Открытие оболочки

Прежде чем создавать простую оболочку, мы должны понять, как она работает. Прежде чем мы введем какие-либо команды в нашу оболочку для выполнения, когда вы открываете терминал и открывается программа оболочки, она сначала проверяет вашу переменную PS1 в переменных среды и использует значение из PS1 для форматирования приглашения оболочки. После форматирования приглашения оболочки вы можете ввести свою команду: $ ls -l.

Псевдонимы и специальные символы

После того, как вы наберете команду и нажмете Enter, произойдет несколько вещей. Сначала ваша программа оболочки проверяет любые псевдонимы, связанные с введенными вами командами. Если псевдоним найден, он расширяет этот псевдоним перед выполнением. После проверки всех псевдонимов ваша программа оболочки ищет специальные символы, такие как: ", ', \, *, &, #, и выполняет логику, связанную с каждым специальным символом.

ДОРОЖКА

После того, как все псевдонимы и специальные символы раскрыты, оболочка просматривает первое слово команды, чтобы проверить, является ли это встроенной функцией, прежде чем проверять программу в PATH. После проверки наличия встроенных функций оболочка проверяет вашу переменную PATH и использует каждый каталог в переменной PATH, чтобы проверить, существует ли команда, в данном случае: isls находится в каждом каталоге ?. (К вашему сведению, ls является встроенным, но мы продолжим в этом случае, как если бы это было не так). Проверьте все переменные среды, введя $ env в программу оболочки и найдите переменную PATH.

Пример вывода:

PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin/:bin

Ваша программа оболочки просматривает каждый каталог, разделенный знаком :, чтобы увидеть, существует ли команда ls в каждом из этих каталогов. При воссоздании оболочки мы использовали функцию stat, чтобы проверить, существует ли команда ls, добавив /ls к каждому каталогу PATH и введя новый каталог PATH в функцию stat, чтобы увидеть, существует ли исполняемый файл в каждом каталоге. Если его не было, мы воссоздали сообщение об ошибке, подобное обычному выводу оболочки.

Создание токенов

Если первая встроенная команда или команда существует, мы выполняем другую логику. Сначала мы захватим всю команду и создадим массив с двойным указателем, каждая команда будет сохранена как указатель char, за которым следует NULL-терминатор в конце нашего массива с двойным указателем. Мы делаем это, сначала находя количество команд в приглашении и добавляя одну для нулевого терминатора. Затем мы используем функцию strtok, man strtok, если вам нужна дополнительная информация, и используем " " в качестве разделителя. Мы выполняем итерацию в цикле до тех пор, пока каждый токен, присвоенный функцией strtok, не станет NULL. На каждой итерации нам необходимо выделить достаточно места для каждой команды, а это означает, что мы должны знать точную длину каждой команды перед ее выполнением. Затем мы используем функцию strncpy, чтобы скопировать каждый токен во вновь созданное пространство, или, в нашем случае, нашу собственную функцию _strncpy, которую мы создали. Ниже приведен пример команды, ее структуры данных и исходного кода.

char **token_holder = [*token, *token, *token, NULL];

char **token_holder = ["ls", "-l", "/tmp", NULL];

Затем мы возвращаем массив с двойным указателем, который мы будем использовать позже, чтобы каждый раз проверять, находится ли первый * ptr или, в нашем примере выше, команда ls в каком-либо из каталогов PATH. В приведенном выше примере мы жестко запрограммировали каталог PATH, в котором находится команда ls. Мы используем функцию execve для выполнения программы. man execve для получения более подробной информации.

Нахождение ПУТИ

Если мы передадим только ls без PATH, в котором находится команда, наша простая оболочка не будет работать. Вот почему у нас должно быть другое условие, которое находит все каталоги в переменной PATH, добавляет /ls к каждой переменной PATH, а затем использует stat, чтобы проверить, существует ли файл. Если он существует, используйте execve для выполнения программы.

Например, возьмем используемую выше переменную PATH:

PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

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

Пример структуры:

char **all_directories = [*directory, *directory, *directory, *directory, *directory, *directory, NULL];

char *all_directories = ["/usr/local/sbin/ls", "/usr/local/bin/ls", "/usr/sbin/ls", "/usr/sbin/ls", "/usr/bin/ls", "/sbin/ls", "/bin/ls", NULL];

Исходный код можно найти здесь.

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

Создание новых процессов

Прежде чем читать дальше, man fork и прочтите это. Чтобы получить дополнительный кредит, введите: pstree -pn в свой терминал. Это показывает все процессы, запущенные на вашем компьютере, с помощью красивой иллюстрации в вашем терминале. Чтобы ваша программа продолжала работать, вам понадобятся две вещи. Вам понадобится цикл while, который будет выполняться вечно или до тех пор, пока не будет выполнено определенное условие, например, достижение конца файла, и вам понадобится возможность создать новый дочерний процесс из родительского процесса, связанного с вашим исполняемым файлом (./hsh в нашем example) во время каждой итерации цикла while.

Чтобы получить интерактивный пример, клонируйте это репо, перейдите в test_folder и скомпилируйте программу следующим образом:

gcc -Wall -Werror -Wextra -pedantic environment.c error_message.c free_it_all.c helper_functions.c strtok_example.c prompt.c

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

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

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

Https://www.youtube.com/watch?v=qtsAFMF0wRc

Исходный код Simple Shell можно найти здесь.

Если у вас есть какие-либо вопросы или комментарии, не стесняйтесь добавлять их ниже. Вы можете подписаться на меня в твиттере @ NTTL_LTTN. Спасибо за ваше время.