diff --git a/.github/workflows/vtsh.yml b/.github/workflows/vt25hell.yml similarity index 88% rename from .github/workflows/vtsh.yml rename to .github/workflows/vt25hell.yml index 2fbfefe..5cd04e0 100644 --- a/.github/workflows/vtsh.yml +++ b/.github/workflows/vt25hell.yml @@ -1,4 +1,4 @@ -name: Test VT Shell +name: Test VT25hell on: push: @@ -6,7 +6,7 @@ on: - main pull_request: paths: - - 'lab/vtsh/**' + - 'lab/vt25hell/**' jobs: build: @@ -20,7 +20,7 @@ jobs: image: silkeh/clang:latest defaults: run: - working-directory: ./lab/vtsh + working-directory: ./lab/vt25hell steps: - name: Checkout uses: actions/checkout@v4 @@ -45,5 +45,5 @@ jobs: clang-tidy -p ./build {} \; - name: Test - working-directory: ./lab/vtsh/test + working-directory: ./lab/vt25hell/test run: python3 -m unittest discover diff --git a/lab/vt25hell/.clang-format b/lab/vt25hell/.clang-format new file mode 100644 index 0000000..dd99110 --- /dev/null +++ b/lab/vt25hell/.clang-format @@ -0,0 +1,16 @@ +BasedOnStyle: Google + +AccessModifierOffset: -2 + +AllowShortFunctionsOnASingleLine: None +AllowShortIfStatementsOnASingleLine: false +AllowShortLoopsOnASingleLine: false + +DerivePointerAlignment: false +BreakConstructorInitializers: BeforeComma +AlignAfterOpenBracket: BlockIndent + +BinPackArguments: false +BinPackParameters: false + +AlignArrayOfStructures: Right diff --git a/lab/vt25hell/.clang-tidy b/lab/vt25hell/.clang-tidy new file mode 100644 index 0000000..388d247 --- /dev/null +++ b/lab/vt25hell/.clang-tidy @@ -0,0 +1,17 @@ +Checks: | + -*, + bugprone-*, + cert-*, + clang-analyzer-*, + concurrency-*, + cppcoreguidelines-*, + google-*, + hicpp-*, + misc-*, + modernize-*, + performance-*, + portability-*, + -portability-avoid-pragma-once, + readability-* +WarningsAsErrors: '*' +FormatStyle: file diff --git a/lab/vt25hell/.gitignore b/lab/vt25hell/.gitignore new file mode 100644 index 0000000..fb90d94 --- /dev/null +++ b/lab/vt25hell/.gitignore @@ -0,0 +1,8 @@ +# CMake +build/ + +# Clangd +.cache/ + +# Python +__pycache__/ diff --git a/lab/vt25hell/.vscode/extensions.json b/lab/vt25hell/.vscode/extensions.json new file mode 100644 index 0000000..d6f2099 --- /dev/null +++ b/lab/vt25hell/.vscode/extensions.json @@ -0,0 +1,25 @@ + { + "recommendations": [ + "ms-vscode.cmake-tools", // CMake support (docs, runners) + + /** + * C/C++ extensions group alternative 1: + */ + "llvm-vs-code-extensions.vscode-clangd", // LSP + "vadimcn.vscode-lldb", // debugger extension for clang + /* or official lldb extension */ + // "llvm-vs-code-extensions.lldb-dap", + + /** + * C/C++ extensions group alternative 2: + */ + // "ms-vscode.cpptools", // C/C++ extension pack with debugger & LSP + + /** + * Optional extensions: + */ + // "usernamehw.errorlens", // For inlineerror displaying + // "ms-vscode-remote.remote-wsl", // For WSL support + // "ms-vscode-remote.remote-containers", // For devcontainer support + ] + } \ No newline at end of file diff --git a/lab/vt25hell/.vscode/settings.json b/lab/vt25hell/.vscode/settings.json new file mode 100644 index 0000000..db30e90 --- /dev/null +++ b/lab/vt25hell/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "clangd.arguments": [ + "-compile-commands-dir=./build", + "-header-insertion=never", + ], + "files.eol": "\n", +} diff --git a/lab/vt25hell/CMakeLists.txt b/lab/vt25hell/CMakeLists.txt new file mode 100644 index 0000000..dc4ebca --- /dev/null +++ b/lab/vt25hell/CMakeLists.txt @@ -0,0 +1,13 @@ +cmake_minimum_required(VERSION 3.12) + +project( + vtsh + VERSION 0.1 + DESCRIPTION "Good Old VT Shell" + LANGUAGES C +) + +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +add_subdirectory(bin) +add_subdirectory(lib) diff --git a/lab/vt25hell/README.md b/lab/vt25hell/README.md new file mode 100644 index 0000000..35bf051 --- /dev/null +++ b/lab/vt25hell/README.md @@ -0,0 +1,82 @@ +# Shell + +## Введение + +Базовый пользовательский интерфейс операционной системы - это терминал, с его +помощью можно взаимодействовать с ОС, давать ей команды и запускать программы и +наблюдать результат их выполнения. А имея возможность запускать программы можно +расширить возможности данной ОС. В данной лабораторной работе вам предлагается +реализовать простую командную оболочку наподобие Bash. + +## Часть 1. Запуск программ + +Необходимо реализовать собственную оболочку командной строки - shell. Shell +должен предоставлять пользователю возможность запускать программы на компьютере +с переданными аргументами командной строки и отображать время их выполнения +(расчитать как разность времени завершения и времени запуска). + +## Часть 2. Логические операторы + +В разработанное приложение командной оболочки необходимо добавить поддержку +логических операторов (аналогичных операторам из Bash) согласно выданному варианту: + +1. `&&` - логическое И (AND), +2. `||` - логическое ИЛИ (OR), +3. `;` - последовательное выполнение, +4. `&` - выполнение в фоне. + +## Требования к реализации + +1. Программа (комплекс программ) должна быть реализован на языке C. + +2. Дочерние процессы должны быть созданы через указанные в варианте системные + вызовы операционной системы, с обеспечением корректного запуска и завершения + процессов. Запрещено использовать высокоуровневые абстракции над системными + вызовами. Необходимо использовать, процедуры libc. + +## Требования к отчету и защите + +Отчет должен содержать: + +1. Титульный лист с указанием номера и названия ЛР, вашего ФИО, ФИО + преподавателя практики, номера вашей группы, варианта ЛР. + +2. Текст задания в соответствии с вариантом. + +3. Листинг исходного кода всех программ, написанных в рамках данной ЛР. + +7. Заключение. + +Студент должен быть готов: + +1. Продемонстрировать работоспособность Shell и предоставить исходный код. + +2. Ответить на вопросы по реализации программ и контрольные вопросы. + +## Описание вариантов + +Запуск подпрограмм (группа **proc**): + +- `proc-fork`: Запуск подпроцесса с помощью системного вызова fork + +- `proc-vfork`: Запуск подпроцесса с помощью системного вызова vfork + +- `proc-clone`: Запуск подпроцесса с помощью системного вызова clone + +- `proc-clone3`: Запуск подпроцесса с помощью системного вызова clone3 + +Дополнительный оператор потока управления Shell (группа **shell**): + +- `shell-and`: Выполнение команд с логическим оператором AND (`&&` в bash). + +- `shell-or`: Выполнение команд с логическим оператором OR (`||` в bash). + +- `shell-seq`: Выполнение команд последовательно (`;` в bash). + +- `shell-bg`: Выполнение команды в фоновом процессе (`&` в bash). + +## Источники + +- Тесты взяты из репозитория [sergey-v-galtsev/shad-os][shad-os-tests] + +[shad-os-tests]: https://gitlab.com/sergey-v-galtsev/shad-os/-/blob/master/bsh/test.py diff --git a/lab/vtsh/bin/CMakeLists.txt b/lab/vt25hell/bin/CMakeLists.txt similarity index 100% rename from lab/vtsh/bin/CMakeLists.txt rename to lab/vt25hell/bin/CMakeLists.txt diff --git a/lab/vtsh/bin/main.c b/lab/vt25hell/bin/main.c similarity index 100% rename from lab/vtsh/bin/main.c rename to lab/vt25hell/bin/main.c diff --git a/lab/vtsh/lib/CMakeLists.txt b/lab/vt25hell/lib/CMakeLists.txt similarity index 100% rename from lab/vtsh/lib/CMakeLists.txt rename to lab/vt25hell/lib/CMakeLists.txt diff --git a/lab/vtsh/lib/vtsh.c b/lab/vt25hell/lib/vtsh.c similarity index 100% rename from lab/vtsh/lib/vtsh.c rename to lab/vt25hell/lib/vtsh.c diff --git a/lab/vtsh/lib/vtsh.h b/lab/vt25hell/lib/vtsh.h similarity index 100% rename from lab/vtsh/lib/vtsh.h rename to lab/vt25hell/lib/vtsh.h diff --git a/lab/vt25hell/test/base_test.py b/lab/vt25hell/test/base_test.py new file mode 100644 index 0000000..343c879 --- /dev/null +++ b/lab/vt25hell/test/base_test.py @@ -0,0 +1,26 @@ +import os +from typing import Optional +from unittest import TestCase + +from shell import Shell + + +class BaseShellTest(TestCase): + def setUp(self): + self.shell = Shell("../build/bin/vtsh") + self.test_files = set() + + def tearDown(self): + for file in self.test_files: + if os.path.exists(file): + os.remove(file) + + def add_test_file(self, filename: str): + self.test_files.add(filename) + + def execute(self, cmd: str, expected: Optional[str] = None): + status, stdout = self.shell.execute(cmd) + + self.assertEqual(status, 0) + if expected is not None: + self.assertEqual(stdout, expected) diff --git a/lab/vt25hell/test/shell.py b/lab/vt25hell/test/shell.py new file mode 100644 index 0000000..d5b40e5 --- /dev/null +++ b/lab/vt25hell/test/shell.py @@ -0,0 +1,18 @@ +import subprocess + + +class Shell: + def __init__(self, path: str): + self._path = path + + def execute(self, cmd: str): + shell = subprocess.Popen( + self._path, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + encoding="utf8", + ) + + stdout, _ = shell.communicate(cmd + "\n", timeout=2) + return shell.returncode, stdout.replace("vtsh> ", "").strip() diff --git a/lab/vt25hell/test/test_basics.py b/lab/vt25hell/test/test_basics.py new file mode 100644 index 0000000..06d79aa --- /dev/null +++ b/lab/vt25hell/test/test_basics.py @@ -0,0 +1,40 @@ +import os + +from base_test import BaseShellTest + + +class TestShellBasics(BaseShellTest): + def test_empty_command(self): + self.execute("", "") + + def test_simple_echo(self): + self.execute("echo hello", "hello") + self.execute(" echo hello world", "hello world") + + def test_multiple_commands(self): + self.execute("echo hello\necho world", "hello\nworld") + + def test_file_operations(self): + self.add_test_file("./foo") + + self.execute("touch ./foo", "") + self.assertTrue(os.path.exists("./foo")) + self.execute("rm ./foo", "") + self.assertFalse(os.path.exists("./foo")) + + def test_multiple_file_operations(self): + self.add_test_file("./bar") + + self.execute("touch ./bar\ncat ./bar\nwc -c bar\nrm ./bar", "0 bar") + self.assertFalse(os.path.exists("./bar")) + + def test_stdin_interaction(self): + self.execute("cat\nhello\nworld", "hello\nworld") + + def test_nested_shells(self): + self.execute("./shell\necho hi\n./shell\necho hello", "hi\nhello") + + def test_invalid_paths(self): + self.execute("cat /sys/proc/foo/bar", "") + self.execute("foobar", "Command not found") + diff --git a/lab/vt25hell/test/test_redirection.py b/lab/vt25hell/test/test_redirection.py new file mode 100644 index 0000000..cb14495 --- /dev/null +++ b/lab/vt25hell/test/test_redirection.py @@ -0,0 +1,65 @@ +import unittest + +from base_test import BaseShellTest + +REQUIRED_REDIRECTION_FUNCTIONALITY = False + +@unittest.skipIf(not REQUIRED_REDIRECTION_FUNCTIONALITY, + ("Redirection functionality is not required in the task. " + "This functionality is for an additional task.")) +class TestShellRedirection(BaseShellTest): + def test_simple_redirection(self): + self.add_test_file("aaa") + + self.execute("echo vt flood > aaa", "") + self.execute("cat /dev/null < aaa", "") + + def test_redirection_overwrite(self): + self.add_test_file("aaa") + + self.execute("echo c forever >aaa", "") + self.execute("cat < aaa", "c forever") + + def test_redirection_with_spaces(self): + self.add_test_file("aaa") + + self.execute("echo >aaa andrew tanenbaum", "") + self.execute(" aaa cat\nhello\nworld", "") + self.execute("< aaa cat", "hello\nworld") + + def test_complex_redirection(self): + self.add_test_file("aaa") + self.add_test_file("bbb") + + self.execute(">aaa head -c 250 < /dev/zero", "") + self.execute("wc > bbb -c aaa", "") + self.execute("< bbb cat", "250 aaa") + + def test_invalid_redirection_syntax(self): + self.execute("echo test foo bar>bbb", "test foo bar>bbb") + self.execute("echo testbbb", "testbbb") + + def test_combined_redirection(self): + self.add_test_file("lol") + self.add_test_file("wut") + + self.execute("echo >lolfoo > bar", "Syntax error") + self.execute("cat two hello", "Syntax error") + self.execute("cat >>hello", "Syntax error") + + def test_io_errors(self): + self.execute(" /sys/proc/foo/bar", "I/O error") + self.execute("cat < /foo/bar/baz", "I/O error") + self.execute("cat < baz > foo", "I/O error") diff --git a/lab/vtsh/CMakeLists.txt b/lab/vtsh/CMakeLists.txt index 1553d59..522d358 100644 --- a/lab/vtsh/CMakeLists.txt +++ b/lab/vtsh/CMakeLists.txt @@ -9,5 +9,4 @@ project( set(CMAKE_EXPORT_COMPILE_COMMANDS ON) -add_subdirectory(bin) -add_subdirectory(lib) +add_subdirectory(src) diff --git a/lab/vtsh/README.md b/lab/vtsh/README.md index 35bf051..e4f2880 100644 --- a/lab/vtsh/README.md +++ b/lab/vtsh/README.md @@ -1,82 +1,437 @@ -# Shell +# VTSH: простой Shell на C -## Введение +| Вариант | Описание | +| ------- | -------- | +| fork/clone | Способ создания подпроцесса | +| pipe | * Расширение для перенаправления ввода-вывода | +| bg | * Расширение для запуска фоновых процессов | +| `&&` / `\|\|` / `;` | * Расширение с логическими операторами для цепочки вызовов | +| NPROC / NOFILE / FSIZE / STACK / AS | * Расширение с возможностью установки ограничений на ресурсы в подпроцессе | -Базовый пользовательский интерфейс операционной системы - это терминал, с его -помощью можно взаимодействовать с ОС, давать ей команды и запускать программы и -наблюдать результат их выполнения. А имея возможность запускать программы можно -расширить возможности данной ОС. В данной лабораторной работе вам предлагается -реализовать простую командную оболочку наподобие Bash. +\* — Расширения опциональные, задаются преподавателем в составе полного +варианта. -## Часть 1. Запуск программ +Для каждого расширения в разделе 6 приведено описание. -Необходимо реализовать собственную оболочку командной строки - shell. Shell -должен предоставлять пользователю возможность запускать программы на компьютере -с переданными аргументами командной строки и отображать время их выполнения -(расчитать как разность времени завершения и времени запуска). +## Сборка и запуск -## Часть 2. Логические операторы +Из каталога `lab/vtsh`: -В разработанное приложение командной оболочки необходимо добавить поддержку -логических операторов (аналогичных операторам из Bash) согласно выданному варианту: +```sh +cmake -S . -B build +cmake --build build +./build/bin/vtsh +``` -1. `&&` - логическое И (AND), -2. `||` - логическое ИЛИ (OR), -3. `;` - последовательное выполнение, -4. `&` - выполнение в фоне. +Собственный набор Python-тестов использует этот же путь к исполняемому файлу: -## Требования к реализации +```sh +python3 -m unittest discover -s test +``` -1. Программа (комплекс программ) должна быть реализован на языке C. +## 1. Цель работы -2. Дочерние процессы должны быть созданы через указанные в варианте системные - вызовы операционной системы, с обеспечением корректного запуска и завершения - процессов. Запрещено использовать высокоуровневые абстракции над системными - вызовами. Необходимо использовать, процедуры libc. +В этой лабораторной работе необходимо разработать **простую командную оболочку**. -## Требования к отчету и защите +Оболочка должна показать, как пользовательская программа запускает другие +программы с помощью системных вызовов ОС, передаёт им аргументы и окружение, +ожидает их завершения и управляет фоновыми процессами. -Отчет должен содержать: +Задача не состоит в повторении Bash. Не требуется поддерживать весь синтаксис +POSIX Shell. Главное — понять: -1. Титульный лист с указанием номера и названия ЛР, вашего ФИО, ФИО - преподавателя практики, номера вашей группы, варианта ЛР. +- зачем операционной системе нужна командная оболочка; +- как создаются и завершаются процессы; +- чем отличаются родительский и дочерний процессы; +- как передаются аргументы и переменные окружения; +- как оболочка получает код завершения программы; +- как реализуются последовательное, условное и фоновое выполнение; +- как устроены сигналы, группы процессов и каналы (`pipe`). -2. Текст задания в соответствии с вариантом. +В руководстве намеренно нет готовой реализации. Названия системных вызовов, +этапы алгоритмов, примеры ввода и вопросы для проверки должны помочь спроектировать +решение самостоятельно. -3. Листинг исходного кода всех программ, написанных в рамках данной ЛР. +## 2. Состав задания -7. Заключение. +### 2. Функциональность VTSH -Студент должен быть готов: +Оболочка должна: -1. Продемонстрировать работоспособность Shell и предоставить исходный код. +1. быть написана на C (или другой низкоуровневый язык с возможностью выполнения + системных вызовов напрямую через C-API); +2. показывать приглашение к вводу; +3. запускать внешнюю программу по абсолютному пути; +4. поддерживать команды `cd` и `exit`; +5. передавать новой программе аргументы и текущее окружение оболочки; +6. корректно ожидать завершения запущенных подпроцессов и получать их статус; -2. Ответить на вопросы по реализации программ и контрольные вопросы. +Дополнительные функции (по варианту): -## Описание вариантов +1. поддерживать конвейеры с помощью `|`; +2. запускать простую команду в фоновом режиме с помощью `&`; +3. поддерживать логические операторы в соответствии с вариантом; +4. ограничивать ресурсы запускаемых программ в соответствии с вариантом. -Запуск подпрограмм (группа **proc**): +## 3. Минимальный синтаксис -- `proc-fork`: Запуск подпроцесса с помощью системного вызова fork +Для базовой версии достаточно следующей модели: -- `proc-vfork`: Запуск подпроцесса с помощью системного вызова vfork +- команда — имя программы и ноль или больше аргументов; +- слова разделяются одним или несколькими пробельными символами; +- операторы `;`, `&&`, `||`, `&`, `|` отделяются пробелами; +- `&` применяется только к одинарной простой команде (без операторов); +- некорректная или неполная конструкция должна выдавать понятную ошибку, а не + приводить к аварийному завершению оболочки. -- `proc-clone`: Запуск подпроцесса с помощью системного вызова clone +Примеры допустимого ввода: -- `proc-clone3`: Запуск подпроцесса с помощью системного вызова clone3 +```text +/bin/echo hello world +printf one ; printf two +false && echo not-run +false || echo fallback +sleep 2 & +``` -Дополнительный оператор потока управления Shell (группа **shell**): +Можно дополнительно расширить возможности синтаксиса. Ограничения синтаксиса +должны быть описаны в отчёте и проверены тестами. -- `shell-and`: Выполнение команд с логическим оператором AND (`&&` в bash). -- `shell-or`: Выполнение команд с логическим оператором OR (`||` в bash). +## 4. Рекомендуемая архитектура -- `shell-seq`: Выполнение команд последовательно (`;` в bash). +Программу можно разделить на четыре логические части: -- `shell-bg`: Выполнение команды в фоновом процессе (`&` в bash). +1. **REPL** — приглашение, чтение строк и завершение оболочки; +2. **parser** — преобразование строки в команды, аргументы и операторы; +3. **executor** — запуск команд и применение правил логических операторов; +4. **process manager** — ожидание, хранение фоновых задач и сбор завершившихся + процессов. -## Источники +Необязательно помещать каждую часть в отдельный файл, но их обязанности не +следует смешивать. Например, parser не должен создавать процессы, а обработчик +сигнала не должен разбирать новую командную строку. -- Тесты взяты из репозитория [sergey-v-galtsev/shad-os][shad-os-tests] +Полезно заранее определить две внутренние сущности: -[shad-os-tests]: https://gitlab.com/sergey-v-galtsev/shad-os/-/blob/master/bsh/test.py +- **простая команда**: массив аргументов, где первый элемент — имя программы; +- **план выполнения**: список команд и операторов между ними. + +Для фоновых задач дополнительно понадобятся идентификатор задачи, PID или PGID, +исходный текст команды и атрибут текущего состояния. + +## 5. Пошаговая инструкция к разработке + +### Шаг 1. Реализуйте цикл оболочки + +На каждой итерации: + +1. при интерактивном запуске покажите приглашение; +2. прочитайте одну строку целиком; +3. удалите завершающий перевод строки; +4. пропустите пустую строку; +5. передайте строку parser; +6. выполните полученный план; +7. перед следующим приглашением проверьте завершившиеся фоновые процессы. + +Приглашение лучше выводить только тогда, когда стандартный ввод связан с +терминалом. Это упрощает автоматическое тестирование через перенаправление +ввода. + +Сочетание клавиш `Ctrl-D` не посылает сигнал. Терминал сообщает программе конец +файла (EOF), если во входном буфере нет данных. В этом случае оболочка должна +аккуратно завершить цикл, освободить память и решить, что делать с ещё +работающими фоновыми задачами. Выбранная политика должна быть описана в отчёте. + +### Шаг 2. Разберите простую команду + +Сначала поддержите только имя программы и аргументы. Проверьте случаи: + +- пустая строка; +- пробелы в начале и конце; +- несколько пробелов между словами; +- команда без аргументов; +- большое число аргументов; +- слишком длинная строка. + +Массив аргументов для запускаемой программы должен завершаться специальным +нулевым элементом. Решите, кто владеет выделенной памятью и в какой момент она +освобождается. + +Не начинайте с кавычек и подстановок. Сначала добейтесь надёжного запуска простой +команды. + +### Шаг 3. Запустите одну программу + +Общий сценарий запуска выглядит так: + +1. родитель создаёт дочерний процесс способом, соответствующим варианту (`fork`/...); +2. дочерний процесс подготавливает своё состояние и заменяет свой образ внешней + программой через вызов системной функции из семейства `exec`; +3. при ошибке `exec` дочерний процесс печатает диагностическое сообщение и + немедленно завершается; +4. родитель сохраняет PID и ожидает завершения дочернего процесса с помощью + функции `wait`, если команда не выполняется в фоновом режиме; +5. родитель преобразует результат функции ожидания в код выполнения команды. + +Запрещено вызывать внешнюю программу через `system`, `popen` или запуск `/bin/sh -c` +— задача состоит в самостоятельной разработке shell, а не в использовании готового. + +Рекомендуемые соглашения об ошибках: + +- программа не найдена — статус `127`; +- программа найдена, но не может быть запущена — статус `126`; +- ошибка создания процесса остаётся ошибкой текущей команды, но не превращается + в ошибку shell как приложения, то есть его работа не завершается и shell + продолжает принимать новые команды. + +Перед реализацией изучите соответствующие страницы `man`: + +- [`fork(2)`](https://man7.org/linux/man-pages/man2/fork.2.html), + [`vfork(2)`](https://man7.org/linux/man-pages/man2/vfork.2.html), + [`clone(2)`](https://man7.org/linux/man-pages/man2/clone.2.html); +- [`exec(3)`](https://man7.org/linux/man-pages/man3/exec.3.html) и + [`execve(2)`](https://man7.org/linux/man-pages/man2/execve.2.html); +- [`wait(2)` / `waitpid(2)`](https://man7.org/linux/man-pages/man2/wait.2.html); +- [`_exit(2)`](https://man7.org/linux/man-pages/man2/_exit.2.html). + +### Шаг 4. Пробросьте окружение + +Новая программа должна получить переменные окружения shell. Проверьте это с +помощью утилит `env` или `printenv`. + +### Шаг 5. Корректно обработайте завершение процесса + +Ожидание подпроцесса может завершиться по разным причинам. Обработайте статус с +помощью предназначенных для этого макросов и различайте: + +- обычное завершение; +- завершение по сигналу; +- остановку или продолжение процесса, если эти состояния отслеживаются. + +Ожидание может быть прервано сигналом, например `SIGINT`. В этом случае +системный вызов может вернуть ошибку `EINTR`; если сигнал не должен завершать +операцию ожидания, его нужно обработать и повторить ожидание. + +Для процесса, завершённого сигналом, удобно сформировать внутренний ненулевой +статус по правилу `128 + номер сигнала`. Главное — использовать одно соглашение +во всём executor. + +## 6. Расширения + +### 6.1. Конвейеры + +Конвейер связывает стандартный вывод одной программы со стандартным вводом +следующей: + +```text +printf hello | wc -c +``` + +Для каждого соседнего звена нужен канал, созданный через `pipe`. После создания +ребёнка нужные концы подключаются к `STDIN_FILENO` и `STDOUT_FILENO` через +`dup2`. Все неиспользуемые файловые дескрипторы закрываются и в детях, и в +родителе. + +Типичная причина зависания — открытый в родителе дескриптор записи: последний +читатель не получает EOF. Сначала реализуйте конвейер из двух программ, затем +обобщите его на произвольное число звеньев. Родитель должен дождаться всех +процессов foreground-конвейера; статусом конвейера обычно считают статус его +последнего звена. + +Подумайте, как с помощью конвейеров теперь можно сделать перенаправление потока +в файл (`cat kek.txt lol.txt > idk.txt`). Проверьте своё решение. + +### 6.2. Фоновое выполнение + +Для команды с `&` родитель не должен выполнять блокирующее ожидание. Он +сохраняет информацию о задаче и сразу показывает следующее приглашение к вводу. + +Однако завершившийся в фоне подпроцесс всё равно должен быть обработан. +Периодически вызывайте **неблокирующее** ожидание: + +- перед показом приглашения к вводу; +- после обработки строки; +- при получении сигнала `SIGCHLD`, если реализовано расширенное управление + сигналами. + +> [!NOTE] +> Обработчик `SIGCHLD` должен делать минимум работы. Сложное обновление структур +> и печать лучше выполнять в основном цикле. +> +> Помните, что один сигнал может соответствовать завершению нескольких +> процессов, поэтому проверка должна продолжаться, пока доступен хотя бы один +> изменивший состояние дочерний процесс. + +### 6.3. Логические операторы + +Сначала parser должен отделить команды от операторов и обнаружить синтаксические +ошибки. + +Executor хранит статус последней действительно выполненной команды и решает, +нужно ли запускать следующую: + +- после `;` — всегда; +- после `&&` — только после успеха; +- после `||` — только после ошибки. + +Порядок выполнения команд ленивый: если первый операнд даёт понять, какой будет +итоговый результат, то второй операнд вычислять не требуется, то есть вторая команда +не выполняется и не создаёт процесс, не меняет статус последней выполненной +команды. + +| Оператор | Поведение | +|---|---| +| `cmd1 ; cmd2` | выполнить `cmd1`, затем выполнить `cmd2` независимо от статуса `cmd1` | +| `cmd1 && cmd2` | выполнить `cmd2`, только если `cmd1` завершилась успешно | +| `cmd1 \|\| cmd2` | выполнить `cmd2`, только если `cmd1` завершилась с ошибкой | + +Успехом считается код завершения `0`. Любой ненулевой код или завершение по сигналу считается ошибкой. + +Проверьте не только пары, но и цепочки, например: + +```text +true && echo recovered && false +false ; echo no ; echo yes +false || false || echo recovered +``` + +### 6.4. Вспомогательные ограничения ресурсов + +Реализуйте **ограничение ресурсов запускаемых программ** по варианту расширения, +назначенному преподавателем: + +| Вариант | Ограничение | Идея проверки | +|---|---|---| +| 1 | `RLIMIT_NPROC` — число процессов и потоков пользователя | ограниченное число попыток создания потомков, проверка отказа | +| 2 | `RLIMIT_NOFILE` — файловые дескрипторы | открытие файлов/сокетов до отказа с удержанием дескрипторов | +| 3 | `RLIMIT_FSIZE` — размер отдельного файла | запись сверх лимита, проверка размера файла и статуса | +| 4 | `RLIMIT_STACK` — размер стека | контролируемая глубокая рекурсия с фактическим расходом стека | +| 5 | `RLIMIT_AS` — виртуальное адресное пространство | последовательное выделение и удержание памяти до отказа | + +Используйте `getrlimit()` / `setrlimit()`; значение лимита должно быть +настраиваемым. Ограничение должно действовать на запускаемую программу и +наследоваться её потомками, не меняя без необходимости лимиты самого shell. +Документация: `man 2 getrlimit` / `man 2 setrlimit`. + +Для тестирования запускайте небольшую вспомогательную программу сначала с +достаточным, затем с уменьшенным лимитом. Проверяйте реальный отказ операции +или завершение по сигналу, а не только успех `setrlimit()`, и убедитесь, что +shell продолжает работу. Настоящую fork bomb запускать не стоит. + +Особенности Linux, безопасная установка лимитов и подробности проверок приведены +в [приложении A](#приложение-a-ограничения-ресурсов-в-linux). + + +## 7. Собственные тесты на Python + +Тесты являются частью работы, а не только способом выставления оценки. Создайте +отдельный набор тестов, который запускает собранный shell как дочерний процесс, +передаёт команды через `stdin` и читает `stdout` и `stderr`. + +Рекомендуется использовать стандартные модули `subprocess`, `unittest` или +`pytest`. Не запускайте проверяемые команды через `shell=True`, т.к. вам нужно +протестировать свой vtsh, а не системную оболочку. + +Минимальный набор сценариев (с учетом вариантов): + +1. пустая строка и EOF; +2. вызов программы по абсолютному пути; +3. программа без аргументов и с несколькими аргументами; +4. лишние пробелы; +5. передача специально заданной переменной окружения; +6. несуществующая и неисполняемая команда; +7. ошибочный синтаксис не завершает shell; +8. коды завершения `0` и не `0`; +9. каждая ветвь логических операторов (`;`, `&&`, `||`); +10. цепочка из трёх и более команд; +11. фоновая команда не блокирует следующую; +12. после фоновой команды не остаётся zombie; +13. конвейер передаёт данные и получает EOF; +14. после выполнения `cd` новые команды выполняются в контексте указанной + директории; +15. ограничения на ресурсы работают. + +## 8. Рекомендуемый порядок работы + +Не пытайтесь реализовать всё сразу. После каждого этапа сохраните работающую +версию и добавьте тесты: + +1. REPL и EOF; +2. parser одной команды; +3. запуск по абсолютному пути; +4. ожидание и статусы; +5. `;` / `&&` / `||`; +6. конвейер передачи данных `|`; +7. `&` и сбор фоновых процессов; +8. ограничение ресурсов; +9. дополнительные тесты; +10. отчёт с выводами по работе. + +Для диагностики полезны: + +- `strace`; +- просмотр дерева процессов и `atop` в режиме записи; +- отладчик GDB; +- инструменты поиска ошибок памяти («санитайзеры»). + +Будьте готовы объяснить различия между `fork`, `vfork`, `clone`, `exec`, +`wait`, `pipe` и `rlimit`, даже если реализовывали только свой вариант. + +> [!TIP] +> Подумайте, почему shell устроен именно так и как его можно было бы устроить иначе. + + +## 10. Полезная документация + +Перед реализацией прочитайте соответствующие страницы руководства `man`: + +- `fork(2)`, `vfork(2)` или `clone(2)` — по варианту; +- `execve(2)`, `exec(3)`; +- `wait(2)`, `waitpid(2)`; +- `environ(7)`; +- `signal(7)`, `sigaction(2)`; +- `setpgid(2)`, `tcsetpgrp(3)`; +- `pipe(2)`, `dup2(2)` — для конвейеров; +- `getrlimit(2)`, `setrlimit(2)` — для ограничения ресурсов; +- `prlimit(1)`, `proc_pid_limits(5)` — для проверки лимитов в Linux. + +Суффикс `(1)` обозначает пользовательские команды, `(2)` — системные вызовы, +`(3)` — функции библиотечного API, `(5)` — форматы файлов, `(7)` — обзорные +разделы документации. Это различие также следует уметь объяснить на защите. + +## Приложение A. Ограничения ресурсов в Linux + +### Установка ограничений и их наследование + +Ограничение (лимит) обычно устанавливают после `fork` в дочернем процессе до +`exec`. Лимиты наследуются при создании потомков и сохраняются после `exec`, но +обычно учитывают ресурсы каждого процесса отдельно, а не сумму по дереву +потомков. У `RLIMIT_NPROC` особая область учёта — реальный пользователь. + +Есть два параметра ограничений: **soft** и **hard limit** — уточните, в чем +разница и в каком случае следует какие устанавливать. При установке нового +значения учитывайте исходные значения, полученные с помощью `getrlimit()`. + +Проверяйте ошибки установки лимита. Shell должен продолжить принимать +команды и не запускать программу без заданного ограничения. + +> [!WARNING] +> После `vfork()` нельзя произвольно вызывать функции, в том числе `setrlimit()`, +> до `exec()` или `_exit()`. Для такого варианта потребуется отдельная программа- +> обёртка, запускаемая через `exec`: она установит лимит и выполнит целевую +> программу. Для `clone()` учитывайте выбранные флаги совместного использования +> памяти и ресурсов; не переносите схему для `fork()` без проверки документации. + +Текущие лимиты можно проверить через `/proc//limits` (`man 5 proc_pid_limits`), +а для экспериментов вне shell использовать `prlimit` (`man 1 +prlimit`). + +Дополнительные материалы: + +- [`getrlimit(2)` / `setrlimit(2)`](https://man7.org/linux/man-pages/man2/getrlimit.2.html) + — семантика лимитов, наследование и ошибки; +- Michael Kerrisk, *The Linux Programming Interface*, глава **Process Resources**; +- [cgroup v2 PIDs controller](https://docs.kernel.org/admin-guide/cgroup-v2.html#pid) + — `pids.max` ограничивает число задач в cgroup и её дочерних cgroup, в отличие + от учёта по real UID в `RLIMIT_NPROC`. diff --git a/lab/vtsh/src/CMakeLists.txt b/lab/vtsh/src/CMakeLists.txt new file mode 100644 index 0000000..7b4c71e --- /dev/null +++ b/lab/vtsh/src/CMakeLists.txt @@ -0,0 +1,27 @@ +add_executable( + vtsh + main.c + vtsh.c +) + +set_target_properties(vtsh PROPERTIES + C_STANDARD 11 + C_STANDARD_REQUIRED YES + C_EXTENSIONS NO + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin" +) + +target_include_directories( + vtsh + PRIVATE + "${CMAKE_CURRENT_SOURCE_DIR}" +) + +if(CMAKE_C_COMPILER_ID MATCHES "GNU|Clang") + target_compile_options(vtsh PRIVATE + -Wall + -Wextra + -Wpedantic + -Wstrict-prototypes + ) +endif() diff --git a/lab/vtsh/src/main.c b/lab/vtsh/src/main.c new file mode 100644 index 0000000..a33633c --- /dev/null +++ b/lab/vtsh/src/main.c @@ -0,0 +1,8 @@ +#include + +#include "vtsh.h" + +int main(void) { + printf("%s", vtsh_prompt()); + printf("Hello, world!\n"); +} diff --git a/lab/vtsh/src/vtsh.c b/lab/vtsh/src/vtsh.c new file mode 100644 index 0000000..1d75ad9 --- /dev/null +++ b/lab/vtsh/src/vtsh.c @@ -0,0 +1,5 @@ +#include "vtsh.h" + +const char* vtsh_prompt(void) { + return "vtsh> "; +} diff --git a/lab/vtsh/src/vtsh.h b/lab/vtsh/src/vtsh.h new file mode 100644 index 0000000..cf6592b --- /dev/null +++ b/lab/vtsh/src/vtsh.h @@ -0,0 +1,3 @@ +#pragma once + +const char* vtsh_prompt(void); diff --git a/lab/vtsh/test/base_test.py b/lab/vtsh/test/base_test.py index 343c879..6f63b8f 100644 --- a/lab/vtsh/test/base_test.py +++ b/lab/vtsh/test/base_test.py @@ -1,13 +1,17 @@ import os +from pathlib import Path from typing import Optional from unittest import TestCase from shell import Shell +PROJECT_ROOT = Path(__file__).resolve().parent.parent +VTSH_EXECUTABLE = PROJECT_ROOT / "build" / "bin" / "vtsh" + class BaseShellTest(TestCase): def setUp(self): - self.shell = Shell("../build/bin/vtsh") + self.shell = Shell(str(VTSH_EXECUTABLE)) self.test_files = set() def tearDown(self):