§ 2. Оболочка

01 Одной из программ, которая запускается практически во всех дистрибутивах Linux после загрузки операционной системы, является программа login. Эта программа работает в текстовом режиме и выводит на экран приглашение. Когда вы корректно вводите имя пользователя и пароль, то запускается программа-оболочка.

 Приглашение команды
Приглашение команды login.

02 Эта программа используется в интерактивном режиме для запуска других программ, и с помощью нее можно выполнить вспомогательные действия, такие как передача данных из одной программы в другую, операции с файлами, настройка параметров других программ и ядра операционной системы и многое другое. В неинтерактивном режиме оболочка используется для автоматизации перечисленных вышей действий с помощью скриптов — файлов, которые содержат команды оболочки. Если компьютер загрузился в графическом режиме, то переключиться в текстовый режим и обратно можно с помощью клавиш ctrl-alt-f1, ctrl-alt-f2 и т.д. В отличие от графического интерфейса оболочка является основным интерфейсом взаимодействия между пользователем и системой.

03 У каждого пользователя может быть своя программа-оболочка. На данный момент наиболее популярными являются bash, zsh, fish. В качестве интерактивной оболочки обычно используют любую удобную оболочку, а для написания скриптов — оболочку sh, потому что она является стандартной, а значит, работает одинаково на разных дистрибутивах Linux. В современных дистрибутивах sh является символьной ссылкой на bash или аналогичную оболочку, которая реализует стандарт, когда запускается через эту ссылку.

04 Для запуска любой команды в оболочке необходимо набрать список ее аргументов через пробел. Первый аргумент является названием команды, а полный список является аргументами этой команды. То есть первый аргумент в списке аргументов программы по соглашению является названием команды или полным путем к исполняемому файлу. Команды оболочки деляется на внешние и встроенные. Для выполнения внешних команд оболочка запускает дочерний процесс и загружает в него код из соответствующего исполняемого файла, а встроенная команда выполняется без создания дочерних процессов. Иногда для эффективности оболочки заменяют простые команды на встроенные, чтобы не создавать без необходимости большого количества недолго живущих процессов. Некоторые команды можно реализовать только как встроенные: команда cd изменяет рабочую директорию текущего процесса, и выполнять ее в дочернем процессе не имеет смысла.

КомандаОписание
ls вывести список файлов в текущей директории
ls -l вывести список файлов в текущей директории вместе с правами доступа и другими параметрами
pwd вывести путь к рабочей директории
echo вывести аргументы (эту внешнюю команду часто заменяют на встроенную)
cd /tmp изменить рабочую директорию на /tmp
Некоторые базовые команды, которые есть в каждой оболочке.

05 Внешние команды могут соответствовать любому исполняемому файлу, то есть любая программа может выступать в роли такой команды. Базовые команды предоставляются пакетом GNU coreutils (по умолчанию) или BSD coreutils или BusyBox. Также для некоторых из них есть аналоги с другими именами и с улучшениями производительности. Таким образом в системе можно заменить даже базовые команды на их аналоги, в том числе можно написать свои программы, реализующие эти команды так, как это хочется пользователю системы. Учитывая, что пакеты разрабатываются различными командами и обновляются независимо друг от друга, то единственный способ быть в курсе последних изменений, это следить за их обновлениями в интернете. Одним из популярных ресурсов с описанием большого количества команд является ArchLinux Wiki, а обновления можно найти в популярных пабликах вроде /r/linux.

Процессы с точки зрения ядра

06 Перед тем как обсуждать особенности работы оболочки, стоит на поверхностном уровне разобраться в работе ядра. Основная задача ядра состоит в предоставлении приложениям программного интерфейса для устройств, и самое часто используемое устройство — это процессор. Процессорное время распределяется между приложениями с помощью процессов. Процессом называется единица планирования ресурсов операционной системы, а под ресурсом понимается процессорное время, оперативная память, дисковая память, пропускная способность сети и тому подобное. По умолчанию, процессорное время и пропускная способность сети распределяются равномерно, а память по принципу очередности: кто первым запросил память, тому процессу она и выделяется. Если памяти не хватает, то процесс аварийно завершается.

07 Все процессы объединяются в древовидную иерархию, в которой корневой процесс имеет идентификатор равный единице и называется init, а все остальные процессы имеют произвольные идентификаторы. Новый процесс можно создать только путем копирования уже существующего процесса , при этом первый процесс создается ядром. После копирования процесса, у вас появляется новая копия программы, которая продолжит выполняться с того же места, что и породивший ее процесс. Обычно, в этом месте программы происходит динамическая загрузка другой программы из исполняемого файла, после чего процесс сразу начинает выполнять функцию main этой программы.

08 Если родительский процесс завершается раньше дочернего, то иерархия должна нарушиться, и чтобы этого не происходило, ядро переназначает первый процесс в качестве родителя осиротевшего дочернего процесса. Текущее дерево процессов можно вывести с помощью команды ps -o pid,ppid,args -f. Здесь флаг -o указывает список выводимых полей (идентификатор процесса, идентификатор его родителя и его аргументы), а флаг -f включает отображение дерева.

image/svg+xmlpsinitbashnginxpid=1
Дерево процессов.

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

09 У каждого процесса по умолчанию есть три потока ввода/вывода: поток ввода, поток вывода, поток ошибок. Они всегда имеют номера 0, 1, 2. Оболочка может перенаправлять в файлы и объединять потоки ввода/вывода, используя их номера. Для этого надо написать в начале или конце команды номер потока, символ пенераправления (знак больше или меньше) и название файла или номер потока. Номера 0 и 1 можно опускать.

ls 1> ls.log                  # перенаправление вывода в файл ls.log
ls > ls.log                   # перенаправление вывода в файл ls.log
sort < ls.log                 # перенаправление ввода из файла ls.log
sort 0< ls.log                # перенаправление ввода из файла ls.log
ls asdasdasd 2> ls.err        # перенаправление ошибок в файл ls.err
ls asdasdasd 2> &1            # объединение ошибок и вывода
ls asdasdasd 1> ls.all 2 > &1 # объединение ошибок и вывода и их перенаправление в файл ls.all

Многозадачность в оболочке

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

wget https://mirror.corbina.net/ubuntu-cd/20.04.3/ubuntu-20.04.3-desktop-amd64.iso >/tmp/wget.log 2>&1 &

Здесь последний знак (амперсанд) говорит оболочке запустить команду в фоновом режиме. Если вы это делаете на удаленном сервере и потом хотите выйти оттуда, то нужно сделать так, чтобы запущенный в фоне процесс перестал быть дочерним. Для этого надо сразу после запуска набрать команду disown, которая делает последний запущенный дочерний процесс дочерним процессу init. Помимо команд в оболочке существуют различные синтаксические конструкции, которые позавимствованы из обычных языков программирования и которые удобны для написания скриптов.

11 Если вы хотите перевести в фоновый режим уже запущенный обычным образом процесс, то его сначала надо остановить, нажав ctrl-z, а затем перевести в фоновый режим командой bg. Полный список фоновых процессов выводит команда jobs. Она также выводит идентификаторы, которые можно использовать в качестве аргумента для команд bg и fg. Команда fg переводит указанный процесс в обычный режим. В обычном режиме команду можно завершить, нажав ctrl-c. Команды, описанные в этом параграфе, работают не всегда, потому что процесс может перехватить сигналы, посылаемые этими командами, и проигнорировать.

Переменные

12 В оболочке существует два вида переменных: обычные и переменные среды. Обычные переменные объявляются и инициализируются с помощью знака =. Все переменные являются глобальными, а все значения являются строками. Если в строке присутствует пробел, то ее необходимо обрамить кавычками. Пробелы перед и до символом = ставить нельзя, иначе оболочка воспринимает это как команду. Если кавычки двойные, то внутри них работают подстановки других переменных с помощью символа $. Чтобы вывести текущее значение переменной, используют команду echo и подстановку. С помощью подставноки переменную можно использовать, как команду.

x=1         # присвоить значение "1" переменной x
y=hello     # присвоить значение "hello" переменной y
y="hello"   # присвоить значение "hello" переменной y
y='hello'   # присвоить значение "hello" переменной y
y="$x"      # присвоить переменную x переменной y (даже если в значении x есть пробелы)
z="1 2 3"   # присвоить значение "1 2 3" переменной z
y="$z"      # присвоить переменную z переменной y
y=$z        # ошибка!
echo "$y"   # вывести значение переменной y
cmd='ls -l' # присвоить значение "ls -l" переменной cmd
$cmd        # выполнить команду ls -l

Для того чтобы превратить обычную переменную в переменную среды, используется команда export. Альтернативный вариант — присвоить значение переменной на той же строчке, на которой написана команда, или использовать команду env в оболочках, которые не поддерживают такой синтаксис. Переменные среды — это системный объект, который наследуется всеми дочерними процессами автоматически и которые можно считать из вашей программы. Посмотреть все переменные среды конкретного процесса можно с помощью файловой системы /proc.

x=1         # присвоить значение "1" переменной x
export x    # превратить переменную x в переменную среды, которая
            # будет доступны всем создаваемым дочерним процессам
export x=1  # присвоить значение "1" переменной x и превратить ее в переменную среды, которая
            # будет доступны всем создаваемым дочерним процессам
# Присвоить значение "1" переменной y только для дочернего процесса "bash",
# а в дочернем процессе вывести значение этой переменной. Аргумент "-c"
# обозначает команду, которую надо запустить в дочернем процессе.
y=1 bash -c 'echo $y'
# То же самое, но для оболочки, которая не поддерживает синтаксис, использованный выше.
env y=1 bash -c 'echo $y'
# вывести переменные среды процесса с идентификатором 12345
# (команда tr заменяет нулевой символ на символ перевода строки)
tr '\0' '\n' < /proc/12345/environ
env         # вывести все переменные среды текущего процесса

Условные переходы

13 Условные переходы — это специальный синтаксис, который позволяет выполнять команды в зависимости от выполнения каких-либо условий. Условием записывается в виде команды. Если команда возвращает ноль, то условие считается выполненным, любой другое значение означает, что условие не выполнено. Если условие выполнено, то оболочка выполняет команды из первой ветки, иначе из второй. Чаще всего в условии используют команду test, которая умеет сравнивать строки и числа. Возвращаемое значение — это системный объект, который инициализируется числом, которое было возвращено из функции main и доступ к которому может получить родительский процесс.

if test "$x" = 1
then
    echo "x равен 1"
else
    echo "x не равен 1"
fi

14 Помимо классических условных переходов, в оболочке имеются условные переходы, в которых проверяются несколько значений одной переменной.

case "$x" in
    1 echo "x равен 1" ;;
    *.txt) echo "x оканчивается на .txt" ;;
    *) echo "x не равен 1 и не оканчивается на .txt" ;;
esac

Циклы

15 В оболочке есть два типа циклов: for и while. Первый цикл идет по элементам, разделенным пробелами, а второй цикл используют команду в качестве условия продолжения.

# выводит каждое число на отдельной строке
for i in 1 2 3
do
    echo "$i"
done 

# выводит то же самое (однострочный вариант)
for i in 1 2 3; do echo "$i"; done 

# выводит то же самое
for i in $(seq 3)  # $(...) — это подстановка вывода команды путем вызова дочерней оболочки
do
    echo "$i"
done 

# цикл выводит сообщение каждую секунду,
# до тех пор пока файл не будет удален или перемещен
while test -f /tmp/x
do
    echo "Файл /tmp/x существует"
    sleep 1
done

Скрипты

16 Скрипт — это исполняемый файл, в котором содержатся команды оболочки. Все команды, которые мы раньше вводили в интерактивном режиме, можно записать в скрипте. В скрипте также доступны дополнительные команды для работы с аргументами. Все аргументы, которые были переданы в скрипт при запуске, сохраняются в переменных $1, $2. Для того чтобы передать все аргументы другой команде, используют специальные переменные $@ и $*, которые отличаются тем, что первая корректно учитывает пробелы, если ее обрамить двойными кавычками, а вторая нет. Первой строчкой в скрипте как правило идет специальный комментарий (shebang), который указывает путь к интерпретатору, который запустится ядром для выполнения этого скрипта. В скриптах оболочки обычно используют /bin/sh, поскольку он является стандартом POSIX.

#!/bin/sh
ls "$@" # вызвать команду ls и передать в нее все аргументы скрипта с учетом пробелов
echo "$1" # вывести первый аргумент скрипта или пустую строчку, если такого нет

17 С помощью циклов и условных переходом реализуется обработка аргументов скрипта.

#!/bin/sh
set -e # завершить скрипт, как только команда вернет ненулевое значение
while test -n "$1" # пока первый аргумент не является пустой строкой
do
    case "$1" in
        # вывести мини-документацию при указании флага "-h" или "--help"
        -h|--help echo "usage: ./my-script [-h] [--help] [--version] ... " ;;
        # вывести версию скрипта при указании флага "--version"
        --version) echo "1.1.1" ;;
        # вывести сообщение об ошибке в поток ошибок и вернуть ненулевое значение
        *) echo "Неизвестный аргумент: $1" >&2; exit 1 ;;
    esac
    shift # сдвинуть все аргументы на один влево:
          # первый аргумент теряется, второй становится первым и т.д.
done

18 Для того чтобы запускать скрипт, как обычную команду, его нужно сделать исполняемем с помощью команды chmod +x my-script. После этого его можно будет запустить, набрав путь к скрипту вместо команды: ./my-script 1 2 3. Чтобы запускать скрипт, как и все остальные команды, директорию с ним нужно добавить в список директорий, в которых оболочка ищет команды без указания путей. Это делается так: export PATH=$PATH:$HOME/scripts. Здесь мы присвоили переменной PATH новое значение, которое такое же, как и старое, но в конце через двоеточие добавлена директория с нашим скриптом. Переменная HOME содержит путь к домашней директории текущего пользователя. Текущие значения всех переменных среды можно узнать с помощью команды env.

Конвейер

19 Часто возникает необходимость передать данные из одной команды в другую. Для этого, в основном, используют файлы, но есть и более эффективный способ, который называется конвейер (pipeline). Чтобы создать конвейер, необходимо разделить команды вертикальной чертой. Эта черта означает, что поток вывода команды слева будет использован как поток ввода команды справа. При этом обе команды будут запущены параллельно, и вторая команда будет читать вывод по мере его поступления от первой команды, то есть в режиме конвейера.

Задания

Конвертация изображений 1 балл

20 Напишите скрипт, который преобразует все переданные в него в качестве аргументов файлы в формат PNG. Для этого скачайте файлы JPG или аналогичные из интернета (команда wget здесь может помочь) и в цикле вызовите команду convert для преобразования формата файлов. Это команла из пакета ImageMagick, документацию можно найти в интернете или man convert.

Мониторинг 1 балл

21 Напишите скрипт, который выводит показания всех датчиков температуры, которые поддерживаются ядром операционной системы. Для этого воспользуйтесь командой find, чтобы найти файлы в директории /sys, которые начинаются на temp и заканчиваются на input. Затем в цикле пройдитесь по найденным файлам и выведите их содержимое командой cat.

Параллельная обработка 1 балл

22 Перепишите скрипт из первого задания так, чтобы конвертировались несколько изображений параллельно. Для этого воспользуйтесь запуском в фоновом режиме и командой wait, которая ждет завершения всех фоновых процессов. Проверить, что команды работают параллельно, можно с помощью команды top, которая показывает загрузку ядер процессора в реальном времени. Эту команду надо запустить в отдельном терминале.

Конвейер 2 балла

23 Напишите скрипт, который ищет файлы с изображениями формата JPG, конвертирует их в формат PNG. Поиск файлов и конвертация должны быть звеньями конвейера. Как вы думаете, для большого количества файлов (например, тысячи) этот скрипт будет работать быстрее скрипта из третьего задания или нет?