Skip to content

uniyar-os/hw_02

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Домашняя работа № 2: Оболочка

В этой работе тебе предстоит сделать интерпретатор командной строки (оболочку) на подобие bash — интерпретатора, который используется в созданной тобой ВМ. Назначение оболочки в том, чтобы дать пользователям возможность запускать программы и управлять ими. Ядро операционной системы предоставляет хорошо документированные интерфейсы для построения оболочек. При создании собственного интерпретатора командной строки ты как раз и разберёшься с этими интерфейсами, и вообще узнаешь новое.

1. Для начала

Как и в прошлый раз тебе будет предоставлена ссылка, ведущая на 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.

2. Поддержка cd и pwd

Представленная заготовка shell содержит диспетчер «встроенных» команд. Любая оболочка должна поддерживать некоторое количество встроенных команд, которые по факту являются функциями shell, а не внешними программами. Например, команда exit должна быть встроенной, поскольку позволяет выйти из shell, а не запустить внешнюю программу. Сейчас в твоём коде поддерживаются только такие встроенные команды:

  • ? — выводит справочную информацию.

  • exit — завершает работу shell.

Добавь новую встроенную команду pwd, которая выводит текущий рабочий директорий в стандартный вывод stdout. Затем добавь встроенную команду cd, принимающую один аргумент (путь к директорию) и меняющую текущий рабочий директорий на значение этого аргумента.

Как закончишь, не забудь коммитнуть изменения и протолкнуть их на GitHub.

$ git add shell.c
$ git commit -m "Закончено добавление базовой функциональности в shell."
$ git push personal master
ℹ️
Делай коммиты часто, чтоб всегда можно было вернуться в произвольную версию твоего кода. Старайся, чтобы коммиты были логически завершёнными. Зафиксированное состояние с неработающим или некомпилирующимся кодом не очень полезно. В этом пункте логичными были бы два коммита — один после реализации и проверки работы pwd, второй после реализации и проверки cd. Но сейчас сойдёт и один единый коммит.

3. Запуск программы

Попробуй ввести что-то в 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, для запуска желаемой программы. Родительский процесс должен ждать завершение дочернего процесса, и после этого продолжить принимать команды.

Коммит!

4. Работа с путями

Наверняка сейчас ты чувствуешь боль…​ Боль от того, что в предыдущем пункте тебе приходилось набивать руками все эти пути к программам полностью. К счастью, любая программа (включая 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.

5. Перенаправление ввода/вывода

При запуске программ иногда удобно забирать данные для stdin из файла или перенаправлять вывод stdout в файл. Например синтаксис [команда] > [файл] мог бы сообщать твоей оболочке, что следует весь вывод записать в файл. С другой стороны, синтаксис [команда] < [файл] пригодился бы при указании в качестве источника данных использовать файл, а не stdin.

Настало время реализовать в коде поддержку этих перенаправлений. Тебе не нужно поддерживать перенаправление stdin/stdout для встроенных команд. Также тебе не нужно поддерживать перенаправление stderr и дописывание в файл (то есть поддерживать синтаксис [команда] >> [файл]). Считай, что символы < и > всегда окружены пробелами. Части вводимой строки > [файл] и < [файл] не должны передаваться аргументами в программу.

Как закончишь, сделай коммит.

6. Обработка сигналов и управление терминалом

Большинство оболочек позволяют прерывать или приостанавливать запущенный процесс с помощью особых сочетаний клавиш. Сочетания клавиш, вроде Ctrl-C или Ctrl-Z, взаимодействуют с дочерним процессом, отправляя сигналы. Например, нажатие Ctrl-C отправляет сигнал SIGINT, обычно останавливающий запущенную программу. Сигнал Ctrl-Z отправляет сигнал SIGTSTP, который приостанавливает выполнение программы и передает управление в оболочку (продолжить выполнение программы можно набрав команду fg). Если ты попробуешь сделать такие нажатия в своём shell, то они повлияют на работу самого процесса shell. Но это не то, что нужно, нажатие Ctrl-Z должно подействовать на дочерний процесс (субпроцесс).

Перед объяснением того, как этого достигнуть придётся бегло рассмотреть некоторые возможности операционных систем.

6.1. Группы процессов

Ты уже знаешь, что каждому процессу ОС присваивает уникальный идентификатор pid. Наряду с ним, процесс может иметь дополнительный (неуникальный) идентификатор группы процессов pgid, который, по-умолчанию, копируется из pgid родительского процесса. Процессы могут управлять своей принадлежностью к той или иной группе процессов с помощью функций getpgid(), setpgid(), getpgrp() и setpgrp().

Помни, что когда оболочка запускает программу, она возможно породит еще несколько процессов. Все эти процессы будут наследовать идентификатор группы процессов изначального процесса. Хорошей идеей было бы выполнять каждую команду оболочки в отдельной группе процессов. Это позволило бы легко идентифицировать все субсубсубпроцессы. Создание группы процессов и перенесение туда текущего процесса заключается в присваивании значению pgid значения pid. И всего-то.

6.2. Активные процессы терминала

Каждый терминал выделяет специальную группу процессов — процессы переднего плана (фореграунд, активные процессы). При нажатии Ctrl-C сигнал посылается всем процессам этой группы. Ты можешь управлять тем, какая группа процессов считается «активной» с помощью tcsetpgrp(int fd, pid_t pgrp). Значение fd должно быть равно 0 для stdin.

6.3. Обзор сигналов

Сигналы — это асинхронные сообщения, которые предназначаются процессам. У каждого вида сигналов есть свой уникальный код (число). Для указания кодов сигналов применяют человекочитаемые названия, начинающиеся с 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 программа относится к собственной группе процессов и эта группа становится активной (фореграунд). Останавливающие сигналы должны действовать только на процессы этой группы.

Коммит!!!

7. Теневое исполнение

Итак твоя оболочка ждёт завершения запущенной программы прежде чем дать возможность запустить следующую. Многие оболочки поддерживают возможность запускать команду в бэкграунде (теневое исполнение) и не дожидаться её завершения. Это достигается с помощью символа & поставленного в конце команды.

Добавь эту возможность в shell для запуска программ (для встроенных команд этого делать не нужно). Как только это задание будет выполнено ты сможешь запускать программы в бэкграунде так /bin/ls &.

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

Считай, что перед символом & всегда есть пробел. Также можно считать, что если в команде присутствует символ &, то он является последним токеном команды.

Не забудь добавить результат в репозиторий.

8. Опционально: Переключение между фореграундом/бэкграундом

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

  • fg [pid] — переключить процесс с pid в фореграунд. Процесс должен продолжить выполнение, если был до этого приостановлен. Если pid не указан, то следует выбрать последний запущенный.

  • bg [pid] — продолжить выполнение приостановленного бэкграунд-процесса. Если pid не указан, то следует выбрать последний запущенный.

Тебе придётся хранить список всех запущенных программ, помнить работают они в бэкграунде или фореграунде. Кроме того для каждого элемента списка нужно будет хранить struct termios. Прогугли что это такое.

Проверь, что на GitHub есть все твои коммиты!

About

Домашняя работа №2

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published