Перейти до вмісту

A1. Власний shell

базовийспирається на модуль 3, модуль 6

Кожна дія оболонки є системним викликом: запуск програми, перенаправлення, конвеєр, фонова задача. Написавши її самостійно, ви побачите ці виклики у власному коді й зрозумієте, як три з них (fork, exec, wait) складаються в усе, що робить bash.

Після цієї роботи ви зможете:

  • запускати програми з C через fork і execvp і коректно чекати на їхнє завершення;
  • пояснити, чому cd не може бути зовнішньою програмою, а > file не потребує від програми жодної підтримки;
  • з’єднувати процеси каналами і знати, чому незакритий дескриптор вішає конвеєр;
  • прибирати завершені фонові процеси, щоб не накопичувати зомбі.

Ті самі механізми стоять за CI-скриптами, systemd, Docker і за кожним | у вашому терміналі. Після цієї роботи вивід strace будь-якої програми читатиметься як знайомий текст.

Написати на C програму myshell, яка читає команди зі стандартного входу рядок за рядком і виконує їх так, як це робить sh, у межах, описаних нижче.

Що має вміти:

  • запускати зовнішні програми з аргументами, шукаючи їх у PATH;
  • вбудовані команди cd, pwd, exit;
  • перенаправлення <, >, >>;
  • конвеєри з довільної кількості команд;
  • фонові задачі через & без накопичення зомбі;
  • Ctrl+C завершує передній процес, а не саму оболонку;
  • ігнорувати порожні рядки; невідома команда не має валити оболонку;
  • завершуватися на Ctrl+D (кінець вводу).

Обмеження. Лише C і системні виклики: без system(), popen() і без виклику готової оболонки. Сенс роботи в тому, щоб зробити ці виклики самому.

Готово, коли:

  • ./check.sh ./myshell проходить усі шістнадцять перевірок;
  • strace -f ./myshell показує clone, execve, wait4, pipe2 і dup2 там, де ви їх очікуєте;
  • у звіті є відповідь на два запитання: чому cd мусить бути вбудованою командою і що станеться, якщо забути закрити кінці каналу в батькові.

Чого робити не треба. Лапки, екранування, підстановка змінних і символи-шаблони до цієї роботи не входять: слова розділяються пробілами, і цього досить. Автоперевірка нічого з переліченого не використовує.

  • Прочитайте в модулі 6 розділи «Створення процесу», «Зомбі та сироти» і «Сигнали», а в модулі 3 розділ «Що відбувається при системному виклику».
  • Розпакуйте архів курсу: перевірка лежить у labs/a1-shell/check.sh.
  • Знадобляться gcc, strace і сторінки man 2 fork, man 2 pipe, man 2 dup2. Підійде будь-який Linux, зокрема контейнер і WSL2.
  1. Запуск однієї команди.

    Прочитати рядок, розбити на слова, fork, у дитині execvp, у батька waitpid. Порожній рядок ігнорувати, Ctrl+D (кінець вводу) завершує оболонку.

    Перевірте strace -f на власній програмі: ви маєте побачити clone, execve і wait4, як описано в модулі 6.

  2. Вбудовані команди: cd, pwd, exit.

    cd мусить виконуватися в самій оболонці, а не в породженому процесі. Спробуйте зробити навпаки й подивіться, що станеться: робочий каталог зміниться в дитині, яка одразу помре.

    Через це cd і немає в /usr/bin.

  3. Перенаправлення >, >>, <.

    Уся робота відбувається між fork і exec: у дитині відкрити файл, підмінити дескриптор 0 або 1 через dup2, закрити зайве, і аж тоді execvp.

    Програма, яку ви запускаєте, про перенаправлення нічого не знає.

  4. Конвеєри a | b | c.

    На кожен стик потрібен pipe(). Дитина зліва пише в pipefd[1], дитина справа читає з pipefd[0]. Батько зобов’язаний закрити обидва кінці в себе, інакше read праворуч ніколи не побачить кінця потоку і конвеєр зависне.

    Кількість команд у конвеєрі — довільна.

  5. Фонові задачі & і прибирання зомбі.

    Команда з & не чекається: оболонка одразу повертає запрошення. Але завершені діти лишаються зомбі, поки хтось не викличе wait, тому потрібен обробник SIGCHLD або waitpid(-1, ..., WNOHANG) у циклі перед кожним запрошенням.

    Перевірка: запустіть sleep 0.2 & десять разів і подивіться ps -eo stat | grep -c Z.

  6. Сигнали.

    Ctrl+C має завершувати передній процес, а не саму оболонку. Оболонка ігнорує SIGINT для себе й повертає типову дію дитині після fork, до exec.

Перевірка лежить в архіві з файлами курсу, і команди нижче виконуються з розпакованого каталогу.

Terminal window
cd labs/a1-shell
./check.sh ./myshell

Скрипт робить шістнадцять перевірок у п’яти групах (запуск команд, вбудовані команди, перенаправлення, конвеєри, фонові задачі) і показує, що не збіглося з очікуваним.

Конвеєр зависає. Батько не закрив свої копії кінців каналу. Правило таке: кожен pipe() дає два дескриптори, після fork вони існують уже в трьох процесах, і закрити треба всі, крім тих двох, що справді потрібні.

cd не працює. Ви виконали його в дитині. Робочий каталог є атрибутом процесу, тож змінювати треба свій власний.

Зомбі накопичуються. Немає прибирання після фонових задач. Один зомбі ніхто й не помітить, а сотня вичерпає таблицю процесів (модуль 6).

Перенаправлення застосувалося до самої оболонки. dup2 виконано до fork замість після. Наслідок помітний одразу: оболонка перестає щось друкувати.

execvp повернувся, і програма продовжила виконуватись. Успішний exec не повертається ніколи, тож його повернення означає помилку. Дитина зобов’язана завершитися через _exit, інакше у вас буде дві оболонки замість однієї.

Керування задачами (jobs, fg, bg) через групи процесів і tcsetpgrp; підстановка змінних; && і ||.