В этой работе тебе предстоит сделать интерпретатор командной строки (оболочку) на подобие bash
— интерпретатора, который используется в созданной тобой ВМ. Назначение оболочки в том, чтобы дать пользователям возможность запускать программы и управлять ими. Ядро операционной системы предоставляет хорошо документированные интерфейсы для построения оболочек. При создании собственного интерпретатора командной строки ты как раз и разберёшься с этими интерфейсами, и вообще узнаешь новое.
Как и в прошлый раз тебе будет предоставлена ссылка, ведущая на GitHub Classroom. Перейди по ссылке, нажми «Accept this assignment». Для этого задания будет создан репозиторий с адресом https://github.com/uniyar-os/hw_02-твой_github_юзернейм
.
Сначала сделай клон репозитория в ВМ и войди в директорий с репозиторием.
$ git clone [email protected]:uniyar-os/hw-02-твой_github_юзернейм.git $ cd hw-02-твой_github_юзернейм.git
Внутри ты обнаружишь заготовку для выполнения этого задания, включающую shell.c
(собственно оболочка), tokenizer.c
(токенайзер для разбора строк на слова) и Makefile
. Попробуй скомпилировать и запустить shell
.
$ make $ ./shell
Для выхода из shell
можно набрать exit
или нажать Ctrl-D
.
Представленная заготовка shell
содержит диспетчер «встроенных» команд. Любая оболочка должна поддерживать некоторое количество встроенных команд, которые по факту являются функциями shell
, а не внешними программами. Например, команда exit
должна быть встроенной, поскольку позволяет выйти из shell
, а не запустить внешнюю программу. Сейчас в твоём коде поддерживаются только такие встроенные команды:
-
?
— выводит справочную информацию. -
exit
— завершает работуshell
.
Добавь новую встроенную команду pwd
, которая выводит текущий рабочий директорий в стандартный вывод stdout
. Затем добавь встроенную команду cd
, принимающую один аргумент (путь к директорию) и меняющую текущий рабочий директорий на значение этого аргумента.
Как закончишь, не забудь коммитнуть изменения и протолкнуть их на GitHub.
$ git add shell.c $ git commit -m "Закончено добавление базовой функциональности в shell." $ git push personal master
ℹ️
|
Делай коммиты часто, чтоб всегда можно было вернуться в произвольную версию твоего кода. Старайся, чтобы коммиты были логически завершёнными. Зафиксированное состояние с неработающим или некомпилирующимся кодом не очень полезно. В этом пункте логичными были бы два коммита — один после реализации и проверки работы pwd , второй после реализации и проверки cd . Но сейчас сойдёт и один единый коммит.
|
Попробуй ввести что-то в shell
, что не является встроенной командой — в ответ shell
сообщит, что не умеет запускать внешние команды. Доработай исходный код, чтобы это было возможно. Первое слово во введённой строке — это название программы (имя исполнимого файла, в котором она хранится). Остальная часть строки может содержать аргументы командной строки, необходимые этой программе.
На этом этапе считай, что первое слово в команде будет содержать не только название программы, но и полный путь к ней. Так, вместо wc
, пользователь shell
будет вводить /usr/bin/wc
. В следующем пункте ты это изменишь, на более привычный способ запуска с помощью только имён.
Тебе пригодится код находящийся в tokenizer.c
, позволяющий разделять строки на слова. Не нужно реализовывать какие-то дополнительные способы разбора помимо тех, что уже представлены в tokenizer.c
. Как только это задание будет выполнено, ты сможешь запускать программы из shell
.
$ ./shell 0: /usr/bin/wc shell.c 77 262 1843 shell.c 1: exit
Для запуска внешней программы shell
должен породить (fork
) дочерний процесс, который сразу вызовет exec
, для запуска желаемой программы. Родительский процесс должен ждать завершение дочернего процесса, и после этого продолжить принимать команды.
Коммит!
Наверняка сейчас ты чувствуешь боль… Боль от того, что в предыдущем пункте тебе приходилось набивать руками все эти пути к программам полностью. К счастью, любая программа (включая shell
) может доступиться к набору так называемых «переменных окружения» (фактически это хэш-таблица строк-ключей на строки-значения). Одна из таких переменных — переменная PATH
. Её значение можно вывести на экран (используй для этого bash
, а не свой shell
).
$ echo $PATH /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:...
Когда пользователь bash
пытается запустить, например, wc
, интерпретатор ищет такой исполнимый файл сначала в текущем рабочем директории, а потом анализирует все пути, указанные в PATH
. Как только будет найдено первое совпадение, оболочка запустит найденный wc
. В переменной PATH
директории разделяются двоеточиями.
Допиши shell
так, чтобы при запуске команд типа wc
, твоя программа искала исполнимый файл wc
по всем путям указанным в PATH
и запускала первое найденное совпадение. Однако, возможность ввода полного пути к исполнимому файлу не должна поломаться. Не используй execvp
. Это не будет засчитано. Вместо этого используй execv
и реализуй собственный поиск в PATH
.
Сохрани всё сделанное в git.
При запуске программ иногда удобно забирать данные для stdin
из файла или перенаправлять вывод stdout
в файл. Например синтаксис [команда] > [файл]
мог бы сообщать твоей оболочке, что следует весь вывод записать в файл. С другой стороны, синтаксис [команда] < [файл]
пригодился бы при указании в качестве источника данных использовать файл, а не stdin
.
Настало время реализовать в коде поддержку этих перенаправлений. Тебе не нужно поддерживать перенаправление stdin
/stdout
для встроенных команд. Также тебе не нужно поддерживать перенаправление stderr
и дописывание в файл (то есть поддерживать синтаксис [команда] >> [файл]
). Считай, что символы <
и >
всегда окружены пробелами. Части вводимой строки > [файл]
и < [файл]
не должны передаваться аргументами в программу.
Как закончишь, сделай коммит.
Большинство оболочек позволяют прерывать или приостанавливать запущенный процесс с помощью особых сочетаний клавиш. Сочетания клавиш, вроде Ctrl-C
или Ctrl-Z
, взаимодействуют с дочерним процессом, отправляя сигналы. Например, нажатие Ctrl-C
отправляет сигнал SIGINT
, обычно останавливающий запущенную программу. Сигнал Ctrl-Z
отправляет сигнал SIGTSTP
, который приостанавливает выполнение программы и передает управление в оболочку (продолжить выполнение программы можно набрав команду fg
). Если ты попробуешь сделать такие нажатия в своём shell
, то они повлияют на работу самого процесса shell
. Но это не то, что нужно, нажатие Ctrl-Z
должно подействовать на дочерний процесс (субпроцесс).
Перед объяснением того, как этого достигнуть придётся бегло рассмотреть некоторые возможности операционных систем.
Ты уже знаешь, что каждому процессу ОС присваивает уникальный идентификатор pid
. Наряду с ним, процесс может иметь дополнительный (неуникальный) идентификатор группы процессов pgid
, который, по-умолчанию, копируется из pgid
родительского процесса. Процессы могут управлять своей принадлежностью к той или иной группе процессов с помощью функций getpgid()
, setpgid()
, getpgrp()
и setpgrp()
.
Помни, что когда оболочка запускает программу, она возможно породит еще несколько процессов. Все эти процессы будут наследовать идентификатор группы процессов изначального процесса. Хорошей идеей было бы выполнять каждую команду оболочки в отдельной группе процессов. Это позволило бы легко идентифицировать все субсубсубпроцессы. Создание группы процессов и перенесение туда текущего процесса заключается в присваивании значению pgid
значения pid
. И всего-то.
Каждый терминал выделяет специальную группу процессов — процессы переднего плана (фореграунд, активные процессы). При нажатии Ctrl-C
сигнал посылается всем процессам этой группы. Ты можешь управлять тем, какая группа процессов считается «активной» с помощью tcsetpgrp(int fd, pid_t pgrp)
. Значение fd
должно быть равно 0
для stdin
.
Сигналы — это асинхронные сообщения, которые предназначаются процессам. У каждого вида сигналов есть свой уникальный код (число). Для указания кодов сигналов применяют человекочитаемые названия, начинающиеся с SIG
. Наиболее часто встречаются такие сигналы:
-
SIGINT
— доставляется при нажатииCtrl-D
. Обычно останавливает программу. -
SIGQUIT
— доставляется при нажатииCtrl-\
. Этот сигнал нужен тоже для останова программы, но он более «сильный» и программа должна прислушиваться к нему более серьёзно, чем кSIGINT
. Также выполняется попытка создать дамп ядра перед выходом программы. -
SIGKILL
— комбинация клавиш отсутствует. Этот сигнал принудительно прерывает программу, реакция на него не может быть переопределена программой. (Большинство других сигналов программа может игнорировать.) -
SIGTERM
— тоже не вызвать комбинацией клавиш. Работает так же какSIGQUIT
. -
SIGTSTP
— клавишиCtrl-Z
. По-умолчанию, приостанавливает программу. В оболочкеbash
при нажатииCtrl-Z
текущая активная группа процессов «ставится на паузу» иbash
начинает опять принимать команды. -
SIGCONT
— доставляется в процесс при выполнении вbash
командfg
илиfg <номер>
. Этот сигнал снимает паузу и программа продолжает выполняться. -
SIGTTIN
— доставляется теневому (бэкграунд) процессу, когда последний пытается получить ввод от пользователя. Такой сигнал приводит к приостановке программы, поскольку теневой процесс не может считать набираемое пользователем. При возобновлении работы бэкграунд-процесса (SIGCONT
) и превращении его в фореграунд-процесс, процесс может попытаться принять пользовательский ввод с клавиатуры. -
SIGTTOU
— доставляется бэкграунд-процессу, который пытается что-то вывести в консоль терминала, но терминал занят другим фореграунд-процессом.
Для генерации сигналов, в том числе и в твоей оболочке shell
, можно использовать kill -XXX PID
, где XXX
окончание названия сигнала, а PID
номер процесса которому будет отправлен этот сигнал. Например, kill -TERM PID
направит сигнал SIGTERM
процессу с PID
.
Для изменения стандартной обработки сигналов программой в C используется функция signal
. Оболочка должна игнорировать большинство сигналов, тогда как запущенные оболочкой процессы должны реагировать обычным образом. К примеру оболочка должна игнорировать SIGTTOU
, а дочерние процессы не должны.
|
Дочерний процесс наследует обработчики сигналов оригинального процесса. Прочитай man 2 signal и man 7 signal для получения большей информации. Также уточни роль констант SIG_DFL и SIG_IGN . За дополнительной информацией обратись к вот этому туториалу (на английском языке).
|
Тебе надо удостовериться, что каждая каждая запускаемая из shell
программа относится к собственной группе процессов и эта группа становится активной (фореграунд). Останавливающие сигналы должны действовать только на процессы этой группы.
Коммит!!!
Итак твоя оболочка ждёт завершения запущенной программы прежде чем дать возможность запустить следующую. Многие оболочки поддерживают возможность запускать команду в бэкграунде (теневое исполнение) и не дожидаться её завершения. Это достигается с помощью символа &
поставленного в конце команды.
Добавь эту возможность в shell
для запуска программ (для встроенных команд этого делать не нужно). Как только это задание будет выполнено ты сможешь запускать программы в бэкграунде так /bin/ls &
.
Также необходимо добавить встроенную команду wait
, которая будет приостанавливать работу оболочки до тех пор, пока не завершатся все бэкграунд-процессы.
Считай, что перед символом &
всегда есть пробел. Также можно считать, что если в команде присутствует символ &
, то он является последним токеном команды.
Не забудь добавить результат в репозиторий.
Большинство оболочек позволяют запускать переключать процессы между выполнением в бэкграунде и фореграунде. Добавь две встроенные команды для обеспечения этой возможности.
-
fg [pid]
— переключить процесс сpid
в фореграунд. Процесс должен продолжить выполнение, если был до этого приостановлен. Еслиpid
не указан, то следует выбрать последний запущенный. -
bg [pid]
— продолжить выполнение приостановленного бэкграунд-процесса. Еслиpid
не указан, то следует выбрать последний запущенный.
Тебе придётся хранить список всех запущенных программ, помнить работают они в бэкграунде или фореграунде. Кроме того для каждого элемента списка нужно будет хранить struct termios
. Прогугли что это такое.
Проверь, что на GitHub есть все твои коммиты!