Как Shell читает ввод от пользователей, ищет команды и выполняет их
Когда вы вводите команды на терминале, как оболочка интерпретирует ваши команды и точно знает, какие команды выполнять? Что происходит за кулисами? Если вы уже задавали себе эти вопросы, эта статья должна вас заинтересовать, поскольку мы анализируем процессы, происходящие за кулисами, когда команда выполняется в оболочке.
Что тебе понадобится
Чтобы понять и следовать этому руководству, вам потребуется следующее:
- Уметь программировать на C
- Вы взаимодействовали с терминалом и имели базовые знания и опыт работы с простыми командами оболочки.
- Работающая система GNU/Linux
- GCC для компиляции вашего кода
- Текстовый редактор для написания кода.
Введение
Что такое оболочка?
Оболочка — это интерфейс между пользователем компьютера и ядром. В частности, оболочка представляет собой интерпретатор командной строки; он читает и интерпретирует вводимые вами команды и организует их выполнение. Когда вы входите в свою систему, оболочка автоматически запускается, и мы взаимодействуем с ней через терминал, где мы вводим наши команды.
Как работает оболочка
Если вы раньше взаимодействовали с оболочкой, вводили основные команды и видели, что они делают, то вы можете начать разбивать внутреннюю работу оболочки на базовую структуру:

Прежде чем мы углубимся в этапы этого процесса, нам нужно понять несколько концепций:
Что такое команда
С точки зрения пользователя, команда — это все, что вы вводите в окно терминала, заканчивающееся новой строкой. С точки зрения системы, команда — это ввод данных пользователем. Команды сами по себе являются программами или исполняемыми файлами, хранящимися где-то в системе; и когда пользователь вводит что-то в командной строке, система ищет соответствующий исполняемый файл для запуска.
Среда и PATH

В Unix и Unix-подобных системах среда определяется переменными среды. Переменные среды представляют собой набор динамических именованных значений в формате ИМЯ=Значение, как показано выше, которые хранятся в системе и используются приложениями, запускаемыми в оболочках или подоболочках. Некоторые переменные среды устанавливаются системой, другие — вами, а некоторые — оболочкой или любой программой, которая загружает другую программу.
Переменная окружения, которая нас больше всего интересует в этой статье, — это PATH. Эта переменная содержит список каталогов, как показано ниже. Путь указывает список каталогов, в которых нужно искать команду.
Что происходит, когда вы вводите ls -l *.c в оболочке?
Чтобы продемонстрировать внутреннюю работу оболочки, мы рассмотрим эту простую команду ls -l *.c и посмотрим, как выполняется команда, следуя шагам, показанным на диаграмме, которую мы рассмотрели ранее. .
Шаг 1. Отображение подсказки
Когда вы входите в терминал, первое, что вы видите, — это приглашение, и это, по сути, поле ввода в терминале, которое позволяет вам вводить команды.
~/simpleshell$
Шаг 2. Получение и анализ пользовательского ввода
Загрузка
После того, как вы введете свои команды после приглашения и нажмете Enter, оболочка прочитает то, что вы ввели, и сохранит эту информацию. Для этого мы рассмотрим функцию под названием getline().
ssize_t getline(char **restrict lineptr, size_t *restrict n,
FILE *restrict stream);
lineptr: это буфер, в котором хранится ваш ввод.
n: размер буфера
stream: указывает, откуда считывать ввод, и в нашем случае это стандартный ввод.
В нашем случае вся строка ls -l *.c считывается и сохраняется в lineptr. На практике вся функция может выглядеть так:
Анализ
В нашей команде: ls -l *.c, первая часть, ls — это фактическая команда, -l — это параметры, с которыми мы не можем запускать нашу команду, и *. c — аргумент команды out. Чтобы идентифицировать эти части команды, оболочка разбивает наш ввод на отдельные слова или токены, разделенные пробелами. Для этого рассмотрим функцию strtok().
char *strtok(char *restrict str, const char *restrict delim);
Эта функция разбивает строку, указанную в str, на последовательность из нуля или более непустых токенов. Аргумент delim указывает, какой символ будет использоваться для разделения строки str, в нашем случае это пробел, который отделяет одно слово от следующего в нашем вводе. Простая реализация этого будет выглядеть так:
В нашей первой функции word_count() мы начинаем с подсчета количества слов во входной строке. Во второй функции split_string() мы выделяем память для массива указателей для хранения каждой лексемы, отделенной от входной строки, в зависимости от количества полученных слов. Затем мы разделяем строку по первому токену, который в нашем случае будет ls, сохраняем эту строку в первом указателе в нашем массиве, а затем инициализируем цикл для повторения этого процесса, сохраняя последующие токены в последующих указателях. в нашем массиве, пока не останется токенов, т.е. token == NULL.
Шаг 3. Поиск команды
Как мы уже говорили ранее, переменные PATH хранят список каталогов, которые просматриваются при поиске команды, введенной пользователем. Мы бы использовали системный вызов stat(), чтобы проверить, существует ли команда.
int stat(const char *restrict pathname,
struct stat *restrict statbuf);
Что нас больше всего интересует в этой функции, так это возвращаемое значение, если файл не существует, возвращается -1. И поэтому мы можем использовать эту информацию, чтобы проверить, выходит ли команда, введенная пользователем, в каталогах PATH или нет, как показано ниже:
В первой функции getPATH() мы получаем доступ к переменным среды нашей системы, используя внешнюю переменную environ. Мы обращаемся к переменной PATH специально и сохраняем каждый каталог, указанный в PATH, в массиве.
Во второй функции get_abs_pathname() мы проверяем существующие команды с помощью stat(). Мы начинаем с доступа к каждому каталогу, который мы получили из функции getPATH, один за другим, и каждый раз мы добавляем команду, введенную пользователем, к этому каталогу и используем системный вызов stat(), чтобы проверить, существует ли команда в этом каталоге. каталог. Если это не так, мы переходим к следующему каталогу и делаем то же самое, пока не исчерпаем наш список каталогов.
Шаг 4. Выполнение команды
После того, как исполняемый файл, соответствующий ключевому слову, введенному пользователем, был найден, последний шаг — запустить этот файл и, следовательно, выполнить команду. Для этого нам пришлось бы вызывать эти системные вызовы: fork(), execve(), wait().
pid_t fork(void);
int execve(const char *pathname, char *const argv[],
char *const envp[]);
pid_t wait(int *wstatus);
Чтобы продемонстрировать это на практике:
Начнем с создания нового процесса с помощью fork(). В этом новом дочернем процессе мы вызываем системный вызов execve() и передаем путь, полученный из get_abs_pathname(). функция в качестве первого аргумента для execve, список слов, введенных пользователем во втором аргументе этой функции, и NULL в качестве третьего. По сути, используя execve, мы сообщаем системе выполнить команду, указанную в ее первом аргументе, со списком опций и аргументов, указанных во втором аргументе, и без переменных среды (NULL).
Наконец, в родительском процессе мы используем системный вызов wait(), чтобы указать системе дождаться завершения любых запущенных дочерних процессов, прежде чем продолжить выполнение остальной части программы в родительском процессе.
Вывод команды: ls -l *.c

Оболочка работает в цикле, то есть после выполнения команды приглашение отображается снова, пока пользователь не введет выход как команду или ^C для выхода из оболочки.
На этом мы завершаем наш простой обзор того, как оболочка интерпретирует и выполняет команды, введенные пользователем.
Для практики я предлагаю вам попробовать создать собственную простую версию оболочки и запустить в ней простые команды. Удачного кодирования!
Чтобы просмотреть полный список функций и реализаций, выделенных в этой статье, перейдите в этот репозиторий GitHub.