Это продолжение серии. Если вы не читали первую часть, рекомендую начать с неё: https://habr.com/ru/articles/1000766/
Что было в прошлый раз
В первой статье я рассказал про базовый shell: как работают fork(), exec(), wait(), и почему cd должна быть встроенной командой. Получился простой shell, который умел запускать программы и перенаправлять вывод.
Что добавилось
За последние недели реализовал четыре крупных фичи:
Pipes — соединять программы (
ls | grep | wc)Background процессы — запуск в фоне (
sleep 100 &)Job Control — управление задачами (
jobs,fg,bg)Сигналы — обработка Ctrl+C и Ctrl+Z
Давайте разберём каждую.
1. Pipes — самая интересная часть!
Что работает:
myshell> ls | grep txt file1.txt file2.txt myshell> cat file.txt | grep "hello" | wc -l 5
Как это работает?
Представьте конвейер на заводе. Первый рабочий делает деталь и передаёт её второму через ленту.
[ls процесс] → [труба в памяти] → [grep процесс] stdout buffer stdin
Код (упрощённо):
int pipefd[2]; pipe(pipefd); // создали трубу // Запускаем ls: if (fork() == 0) { dup2(pipefd[1], STDOUT_FILENO); // stdout → труба close(pipefd[0]); exec("ls"); } // Запускаем grep: if (fork() == 0) { dup2(pipefd[0], STDIN_FILENO); // stdin ← труба close(pipefd[1]); exec("grep"); } // ВАЖНО: родитель закрывает оба конца! close(pipefd[0]); close(pipefd[1]);
Баг, который поймал:
Забыл закрыть pipefd в родительском процессе. Результат: grep зависал, ждал конца данных, но труба оставалась открытой!
Урок: Родитель обязан закрыть оба конца трубы, иначе программы не поймут, что данные закончились.
2. Background процессы — работа в фоне
Что работает:
myshell> sleep 100 & [1] 12345 myshell> # сразу получили приглашение!
Shell не ждёт завершения, можно работать дальше.
Как это работает?
bool background = (last_token == "&"); pid_t pid = fork(); if (pid == 0) { exec(command); } else { if (background) { add_job(pid, command); // добавили в список задач printf("[1] %d\n", pid); // НЕ вызываем wait()! } else { foreground_pid = pid; wait(NULL); } }
Проблема: зомби-процессы!
Когда фоновый процесс завершается, он становится зомби. Решение — обработчик SIGCHLD:
signal(SIGCHLD, sigchld_handler); void sigchld_handler(int sig) { while (waitpid(-1, NULL, WNOHANG) > 0); }
3. Job Control — управление задачами
Это было самое сложное! Нужно отслеживать все фоновые процессы, их состояния, и уметь управлять ими.
Что работает:
myshell> sleep 100 & [1] 12345 myshell> sleep 200 & [2] 12346 myshell> jobs [1] Running sleep 100 [2] Running sleep 200 myshell> fg %1 sleep 100 ^Z [1]+ Stopped sleep 100 myshell> jobs [1] Stopped sleep 100 [2] Running sleep 200 myshell> bg %1 [1]+ sleep 100 & myshell> jobs [1] Running sleep 100 [2] Running sleep 200
Как это работает?
Структура задачи:
struct Job { int job_id; pid_t pid; std::string command; enum { RUNNING, STOPPED, DONE } state; }; std::vector<Job> job_list;
Команда fg (вернуть на передний план):
void cmd_fg(int job_id) { Job* job = find_job(job_id); kill(job->pid, SIGCONT); // продолжить выполнение foreground_pid = job->pid; waitpid(job->pid, nullptr, 0); // ждём завершения job->state = Job::DONE; }
Команда bg (продолжить в фоне):
void cmd_bg(int job_id) { Job* job = find_job(job_id); kill(job->pid, SIGCONT); // продолжить job->state = Job::RUNNING; }
Инсайт:
Процессы могут быть в трёх состояниях:
RUNNING — работает
STOPPED — остановлен (Ctrl+Z)
DONE — завершён
Shell должен отслеживать переходы между этими состояниями через сигналы!
4. Сигналы — Ctrl+C и Ctrl+Z
Задача:
myshell> sleep 100 ^C # Ctrl+C должен убить sleep, но НЕ shell! myshell>
Как это работает?
pid_t foreground_pid = -1; signal(SIGINT, sigint_handler); // Ctrl+C void sigint_handler(int sig) { if (foreground_pid > 0) { kill(foreground_pid, SIGINT); foreground_pid = -1; } printf("\nmyshell> "); }
Ctrl+Z (остановка процесса):
signal(SIGTSTP, sigtstp_handler); void sigtstp_handler(int sig) { if (foreground_pid > 0) { kill(foreground_pid, SIGSTOP); // Добавляем в jobs со статусом STOPPED Job* job = find_job_by_pid(foreground_pid); job->state = Job::STOPPED; foreground_pid = -1; } }
Инсайт: Ctrl+C и Ctrl+Z — это сигналы от ядра, а не команды. Shell их перехватывает и решает, что делать.
Самая сложная часть всего проекта
Координация сигналов и состояний процессов.
Пример проблемы:
Запускаю
sleep 100Жму Ctrl+Z — процесс останавливается
Вызываю
bg %1— процесс продолжается в фонеПроцесс завершается — приходит SIGCHLD
Нужно обновить состояние на DONE
Всё это должно работать согласованно. Одна ошибка — и shell начинает вести себя странно.
Что узнал
Pipes сложнее, чем кажутся
Жонглирование file descriptors. Забыл закрыть один — зависло. Закрыл не тот — broken pipe.
Сигналы — это асинхронность
Обработчик сигнала может сработать в любой момент. Нужно быть осторожным с глобальными переменными.
Job Control — это state machine
Процесс может быть в разных состояниях, переходы между ними управляются сигналами. Это целая машина состояний!
Bash сложнее, чем кажется
Раньше думал "bash — это просто программа". Теперь понимаю сколько там логики: управление задачами, сигналы, состояния, координация процессов.
GitHub
Весь код: GitHub
Буду рад фидбеку и pull request'ам!
P.S. Это учебный проект для изучения OS. Код далёк от production-ready bash, но каждая строка — это понимание системы, которой мы пользуемся каждый день.
Вопросы приветствуются!
