Понимание типов оболочек: interactive, non-interactive, login, non-login
Да-да, с оболочками есть еще и такой прикол. Многих людей сбивает с толку процесс настройки оболочек именно по тому, что они не в курсе о типовых различиях. Сегодня я постараюсь дать вам базовое понимание каждого из типов, чтобы потом легче было разобраться с конфигами.
Оболочки "interactive"
Максимально упрощая, интерактивная оболочка - та, с которой мы работаем через ручной ввод - мы печатаем команду, передаем ее оболочке на вход и видим результат по завершению. Когда вы запускаете терминал, оболочка стартует в интерактивном режиме.
Оболочки "non-interactive"
Это тот режим запуска оболочки, когда она не взаимодействуют напрямую с пользователем. Обычно они запускаются для выполнения команд или скриптов и завершаются по мере окончания задачи. Все же помнят про тот самый shebang:
Так вот, для запуска скрипта мы изначально открываем bash (интерактивная оболочка) и указываем путь:
Далее bash запустит интерпретатор sh (неинтерактивная оболочка), после чего тот "втихую" выполнит скрипт.
Это ВАЖНО помнить! Скрипты отрабатывают в неинтерактивных оболочках и ваши настройки в файле "~/.bashrc" окажутся бесполезными. Почему такие оболочки не подгружают конфиги? Хороший вопрос. Есть несколько причин:
1) скрипту не следует полагаться на пользовательские настройки - быть "пользователезависимым". Если юзер "настругал" скрипт, опираясь, допустим, на свои алиасы, будет нарушена портируемость и у другого человека он, вероятно, не стартанет.
2) чтение и отработка конфигов может занимать время. Без дополнительных подготовок скрипт банально быстрее отработает.
Оболочки "login"
Такая оболочка создается первым пользовательским процессом, когда тот успешно логинится в системе и открывает сессию через tty или ssh. Если вы авторизовались через GUI - дисплей менеджер, "login shell" обычно заменяется оконным менеджером либо менеджером сессии.
Данный тип оболочки не создается все время, как мы открываем терминал и отличается тем, что читает дополнительные конфиги. Явно его можно идентифицировать по префиксу '-' для исполняемого файла. Давайте зайдем пользователем через tty и проверим:
А теперь сами запустим еще одну оболочку внутри родительской и убедимся, что статус "login shell" выдается невсегда:
Также узнать тип оболочки можно через переменную '0':
Оболочки "non-login"
Создаются при обычном старте - без авторизации пользователя. Все, что нужно для инициализации такой оболочки - просто открыть терминал либо самому запустить исполняемый файл без дополнительных флагов, которые переводят оболочку в режим "login": "-l" и "--login".
Linux++ | IT-Образование
Да-да, с оболочками есть еще и такой прикол. Многих людей сбивает с толку процесс настройки оболочек именно по тому, что они не в курсе о типовых различиях. Сегодня я постараюсь дать вам базовое понимание каждого из типов, чтобы потом легче было разобраться с конфигами.
Оболочки "interactive"
Максимально упрощая, интерактивная оболочка - та, с которой мы работаем через ручной ввод - мы печатаем команду, передаем ее оболочке на вход и видим результат по завершению. Когда вы запускаете терминал, оболочка стартует в интерактивном режиме.
Оболочки "non-interactive"
Это тот режим запуска оболочки, когда она не взаимодействуют напрямую с пользователем. Обычно они запускаются для выполнения команд или скриптов и завершаются по мере окончания задачи. Все же помнят про тот самый shebang:
#!/bin/sh
Так вот, для запуска скрипта мы изначально открываем bash (интерактивная оболочка) и указываем путь:
$ ./noscript.sh
Далее bash запустит интерпретатор sh (неинтерактивная оболочка), после чего тот "втихую" выполнит скрипт.
Это ВАЖНО помнить! Скрипты отрабатывают в неинтерактивных оболочках и ваши настройки в файле "~/.bashrc" окажутся бесполезными. Почему такие оболочки не подгружают конфиги? Хороший вопрос. Есть несколько причин:
1) скрипту не следует полагаться на пользовательские настройки - быть "пользователезависимым". Если юзер "настругал" скрипт, опираясь, допустим, на свои алиасы, будет нарушена портируемость и у другого человека он, вероятно, не стартанет.
2) чтение и отработка конфигов может занимать время. Без дополнительных подготовок скрипт банально быстрее отработает.
Оболочки "login"
Такая оболочка создается первым пользовательским процессом, когда тот успешно логинится в системе и открывает сессию через tty или ssh. Если вы авторизовались через GUI - дисплей менеджер, "login shell" обычно заменяется оконным менеджером либо менеджером сессии.
Данный тип оболочки не создается все время, как мы открываем терминал и отличается тем, что читает дополнительные конфиги. Явно его можно идентифицировать по префиксу '-' для исполняемого файла. Давайте зайдем пользователем через tty и проверим:
$ ps -auxf
(root) /bin/login -p --
(user) \_ -bash
\_ ps -auxf
А теперь сами запустим еще одну оболочку внутри родительской и убедимся, что статус "login shell" выдается невсегда:
$ bash
$ ps -auxf
/bin/login -p --
\_ -bash
\_ bash
\_ ps -auxf
Также узнать тип оболочки можно через переменную '0':
$ echo $0
-bash
Оболочки "non-login"
Создаются при обычном старте - без авторизации пользователя. Все, что нужно для инициализации такой оболочки - просто открыть терминал либо самому запустить исполняемый файл без дополнительных флагов, которые переводят оболочку в режим "login": "-l" и "--login".
Linux++ | IT-Образование
👍19❤🔥16🔥10
Конфигурация оболочки
И так, продолжаем топать дальше и вникать в устройство оболочек. Сегодня рассмотрим то, какие файлы конфигурации и в какой последовательности оболочка исполняет для того, чтобы настроить свое окружение.
Мы с вами будем говорить о конфигах в контексте оболочки bash, но между их вариациями различий не так много.
Этапы конфигурации "login shell"
И так, мы с вами успешно авторизовались и открыли сессию через tty либо ssh... Какие файлы были использованы нашей "прародительской ☝️" оболочкой "-bash" и в какой последовательности?
1) оболочка выполняет системный скрипт "/etc/profile"
2) скрипт "/etc/profile" выполняет каждый файл "*.sh" (доступный для чтения), что лежит в каталоге "/etc/profile.d/":
3) далее выполняется ТОЛЬКО один из следующих пользовательских файлов (прям в такой последовательности): ~/.bash_profile, ~/.bash_login, ~/.profile. Как правило, каждый из этих скриптов в результате должен позвать ~/.bashrc:
4) когда все, оболочка завершается, по возможности отрабатывает скрипт "~/.bash_logout".
Этапы конфигурации "non-login shell"
Тут обычно все проще: когда интерактивная оболочка запущена в обычном режиме (non-login), отрабатывает только пользовательский скрипт ~/.bashrc. Выполняется он каждый раз, как стартует оболочка, что подразумевает наличие в нем "многоразовых" команд.
Также, в некоторых дистрах, бинарник оболочки bash может быть собран с флагом "-DSYS_BASHRC", что приводит к исполнению системного скрипта "/etc/bash.bashrc" перед пользовательским "~/.bashrc".
Так, стоп, а "/etc/profile" не вызывается? Неа, задача файлов "profile" - выполнять команды только для оболочек формата "login". Их выполнение, как правило, требуется один раз за всю сессию.
Linux++ | IT-Образование
И так, продолжаем топать дальше и вникать в устройство оболочек. Сегодня рассмотрим то, какие файлы конфигурации и в какой последовательности оболочка исполняет для того, чтобы настроить свое окружение.
Мы с вами будем говорить о конфигах в контексте оболочки bash, но между их вариациями различий не так много.
Этапы конфигурации "login shell"
И так, мы с вами успешно авторизовались и открыли сессию через tty либо ssh... Какие файлы были использованы нашей "прародительской ☝️" оболочкой "-bash" и в какой последовательности?
1) оболочка выполняет системный скрипт "/etc/profile"
2) скрипт "/etc/profile" выполняет каждый файл "*.sh" (доступный для чтения), что лежит в каталоге "/etc/profile.d/":
if [ -d /etc/profile.d ]; then
for i in /etc/profile.d/*.sh; do
if [ -r $i ]; then
. $i
fi
done
unset i
fi
3) далее выполняется ТОЛЬКО один из следующих пользовательских файлов (прям в такой последовательности): ~/.bash_profile, ~/.bash_login, ~/.profile. Как правило, каждый из этих скриптов в результате должен позвать ~/.bashrc:
# include .bashrc if it exists
if [ -f "$HOME/.bashrc" ]; then
. "$HOME/.bashrc"
fi
4) когда все, оболочка завершается, по возможности отрабатывает скрипт "~/.bash_logout".
Этапы конфигурации "non-login shell"
Тут обычно все проще: когда интерактивная оболочка запущена в обычном режиме (non-login), отрабатывает только пользовательский скрипт ~/.bashrc. Выполняется он каждый раз, как стартует оболочка, что подразумевает наличие в нем "многоразовых" команд.
Также, в некоторых дистрах, бинарник оболочки bash может быть собран с флагом "-DSYS_BASHRC", что приводит к исполнению системного скрипта "/etc/bash.bashrc" перед пользовательским "~/.bashrc".
Так, стоп, а "/etc/profile" не вызывается? Неа, задача файлов "profile" - выполнять команды только для оболочек формата "login". Их выполнение, как правило, требуется один раз за всю сессию.
Linux++ | IT-Образование
👍21❤🔥9🔥7✍2❤1
Сигналы в Linux: что это и зачем нужно?
Предположим, что есть две программы. Одна программа должна передать другой большое количество данных - через файл.
Работает эта история таким образом, что одна программа открывает файл (на запись) и передает данные, другая открывает его же (на чтение) и получает инфу.
Проблема заключается в том, что вторая программа должна как-то узнать о том, что пришло время считывать данные.
Как реализовать процесс уведомления? Ну, можно через сигналы)
Сигнал — это механизм оповещения процесса о том, что произошло некое событие.
Иногда они описываются, как программные прерывания, т.к. приостанавливают нормальное выполнение программы.
Один процесс может отправить сигнал другому с помощью системного вызова kill(), который является аналогом команды оболочки kill:
Важно понимать, что сигналы доставляются процессам через ядро операционки:
Каждому сигналу присваивается уникальный идентификатор — целое число, начиная с 1. Эти числа определены в файле <signal.h>. Каждому номеру соответствует символьное обозначение.
В удобном формате посмотреть список сигналов можно с помощью команды "kill -l":
Сигналы можно разделить на две большие категории:
1) стандартные - используются ядром для оповещения процессов о свершении событий.
В Linux стандартные сигналы пронумерованы от 1 до 31;
2) сигналы реального времени - обычно используются для коммуникации между процессами или потоками.
В отличие от стандартных, у сигналов реального времени нет заранее определённых имён.
Они идентифицируются выражением вида (SIGRTMIN + n), где n — это целое число от 0 до (SIGRTMAX – SIGRTMIN):
Сигналы реального времени пронумерованы от 34 до 64: SIGRTMIN -> SIGRTMAX:
В ответ на сигнал, процесс либо выполняет заранее определенное действие (приостановка, завершение, возобновление работы), либо происходит перехват и отработка кастомного вызова, либо 0 реакции - игнор.
Игнорировать процесс может и сигналы, к которым подвязано какое-то действие. Реализуется это через определение сигнальной маски.
Таким образом мы гарантируем то, что выполнение фрагмента кода не будет прервано доставкой сигнала.
Linux++ | IT-Образование
Предположим, что есть две программы. Одна программа должна передать другой большое количество данных - через файл.
Работает эта история таким образом, что одна программа открывает файл (на запись) и передает данные, другая открывает его же (на чтение) и получает инфу.
Проблема заключается в том, что вторая программа должна как-то узнать о том, что пришло время считывать данные.
Как реализовать процесс уведомления? Ну, можно через сигналы)
Сигнал — это механизм оповещения процесса о том, что произошло некое событие.
Иногда они описываются, как программные прерывания, т.к. приостанавливают нормальное выполнение программы.
Один процесс может отправить сигнал другому с помощью системного вызова kill(), который является аналогом команды оболочки kill:
#include <signal.h>
int kill(pid_t pid, int sig);
Важно понимать, что сигналы доставляются процессам через ядро операционки:
процесс_1 -> ядро -> процесс_2
Каждому сигналу присваивается уникальный идентификатор — целое число, начиная с 1. Эти числа определены в файле <signal.h>. Каждому номеру соответствует символьное обозначение.
В удобном формате посмотреть список сигналов можно с помощью команды "kill -l":
$ kill -l
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
Сигналы можно разделить на две большие категории:
1) стандартные - используются ядром для оповещения процессов о свершении событий.
В Linux стандартные сигналы пронумерованы от 1 до 31;
2) сигналы реального времени - обычно используются для коммуникации между процессами или потоками.
В отличие от стандартных, у сигналов реального времени нет заранее определённых имён.
Они идентифицируются выражением вида (SIGRTMIN + n), где n — это целое число от 0 до (SIGRTMAX – SIGRTMIN):
$ kill -l
34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2
...
Сигналы реального времени пронумерованы от 34 до 64: SIGRTMIN -> SIGRTMAX:
#include <stdio.h>
#include <signal.h>
int main() {
printf("SIGRTMIN: %d\n", SIGRTMIN);
printf("SIGRTMAX: %d\n", SIGRTMAX);
return 0;
}
SIGRTMIN: 34
SIGRTMAX: 64
В ответ на сигнал, процесс либо выполняет заранее определенное действие (приостановка, завершение, возобновление работы), либо происходит перехват и отработка кастомного вызова, либо 0 реакции - игнор.
Игнорировать процесс может и сигналы, к которым подвязано какое-то действие. Реализуется это через определение сигнальной маски.
Таким образом мы гарантируем то, что выполнение фрагмента кода не будет прервано доставкой сигнала.
Linux++ | IT-Образование
👍48🔥18❤🔥6❤1🍌1
Перехват и обработка сигналов
Обработчик сигнала — функция, написанная программистом и выполняющая нужные действия при получении сигнала.
Например, для оболочки может быть определен обработчик сигнала SIGINT, который генерируется комбинацией Ctrl+C. Он заставляет оболочку прекратить выполнение текущей задачи и вернуть управление в основной цикл так, что пользователь опять видит приглашение на ввод команды:
И так, существует два способа перехвата сигнала: signal() и sigaction(). Функция signal() реализована в glibc как библиотечная, поверх системного вызова sigaction().
Давайте напишем мини-программу, которая никогда не завершается и все время что-то там выполняет:
Прям никогда не завершается?)) Еще как, нам всего-то стоит нажать Ctrl+C:
Что произошло? Оболочка и ее дочерний процесс (675989) получили сигнал SIGINT. Bash, т.к. отработал перехват, не завершил выполнение, а вот наша программа потухла, т.к. ничего в ней не переопределяет дефолтного поведения. Давайте внесем некоторые правки и кое-что добавим:
Если какой-то хитрый разраб такое провернет, знайте, что есть еще сигналы, на которые прога завершается. Отправим SIGTERM и, если на него не стоит обработчиков, продолжим спокойно работать:
Есть, кста, 1 сигнал, на который невозможно повесить обработчик - SIGKILL. Так устроено, что этот сигнал является для процесса ПРИКАЗОМ, нежели просьбой:
Обратите внимание, что невозможно установить перехват сигнала таким образом, чтобы, без явного указания, он завершал процесс. Максимум, как мы можем приблизиться к этому, — прописать в обработчике одну из двух функций: exit() или abort().
Функция abort() генерирует для процесса сигнал SIGABRT, который приводит к сбросу дампа и завершению. Даже если на SIGABRT повесить обработчик, программа все равно завершится - особенность реализации.
Linux++ | IT-Образование
Обработчик сигнала — функция, написанная программистом и выполняющая нужные действия при получении сигнала.
Например, для оболочки может быть определен обработчик сигнала SIGINT, который генерируется комбинацией Ctrl+C. Он заставляет оболочку прекратить выполнение текущей задачи и вернуть управление в основной цикл так, что пользователь опять видит приглашение на ввод команды:
parallels@ubuntu-linux-22-04-02:~$ ./app
App executes...
parallels@ubuntu-linux-22-04-02:~$
И так, существует два способа перехвата сигнала: signal() и sigaction(). Функция signal() реализована в glibc как библиотечная, поверх системного вызова sigaction().
Давайте напишем мини-программу, которая никогда не завершается и все время что-то там выполняет:
#include "stdio.h"
int main()
{
while (1) {
printf("Do some work, pid = %d\n", getpid());
sleep(1);
}
return 0;
}
Прям никогда не завершается?)) Еще как, нам всего-то стоит нажать Ctrl+C:
$ ./prog
Do some work, pid = 675989
Do some work, pid = 675989
^C
Что произошло? Оболочка и ее дочерний процесс (675989) получили сигнал SIGINT. Bash, т.к. отработал перехват, не завершил выполнение, а вот наша программа потухла, т.к. ничего в ней не переопределяет дефолтного поведения. Давайте внесем некоторые правки и кое-что добавим:
#include "stdio.h"
#include "unistd.h"
#include "signal.h"
void handler (int num) {
write(STDOUT_FILENO, "I won't die!\n", 13);
}
int main()
{
signal(SIGINT, handler);
while (1) {
printf("Do some work, pid = %d\n", getpid());
sleep(1);
}
return 0;
}
$ ./prog
Do some work, pid = 681049
Do some work, pid = 681049
Do some work, pid = 681049
^CI won't die!
Do some work, pid = 681049
^CI won't die!
Если какой-то хитрый разраб такое провернет, знайте, что есть еще сигналы, на которые прога завершается. Отправим SIGTERM и, если на него не стоит обработчиков, продолжим спокойно работать:
$ kill -TERM 681049
Do some work, pid = 681049
Terminated
Есть, кста, 1 сигнал, на который невозможно повесить обработчик - SIGKILL. Так устроено, что этот сигнал является для процесса ПРИКАЗОМ, нежели просьбой:
$ kill -KILL 683152
Обратите внимание, что невозможно установить перехват сигнала таким образом, чтобы, без явного указания, он завершал процесс. Максимум, как мы можем приблизиться к этому, — прописать в обработчике одну из двух функций: exit() или abort().
Функция abort() генерирует для процесса сигнал SIGABRT, который приводит к сбросу дампа и завершению. Даже если на SIGABRT повесить обработчик, программа все равно завершится - особенность реализации.
Linux++ | IT-Образование
👍34🔥10❤2❤🔥2
Рад всех видеть на моем канале! Го знакомиться, меня зовут Кирилл. Я - действующий программист, занимаюсь прикладной разработкой системного софта под Линукс.
Моя задача - в увлекательном формате делиться опытом и погружать вас в мир системной разработки и администрирования!
Разнообразие материала позволит найти тут свое место как абсолютным новичкам, так и до дыр обученным профессионалам. Будет интересно, полезно и, главное, по делу. Добро пожаловать в наше сообщество!
Навигация по контенту
План по разделяемым библиотекам
Итоги квартала (август 2024)
Итоги квартала (ноябрь 2024)
Итоги квартала (февраль 2025)
Итоги квартала (август 2025)
LinuxCamp | #info
Please open Telegram to view this post
VIEW IN TELEGRAM
👍51❤42🔥14❤🔥4😍1
LinuxCamp | DevOps pinned «👋 Приветственный пост Рад всех видеть на моем канале! Го знакомиться, меня зовут Кирилл. Я - действующий программист, занимаюсь прикладной разработкой системного софта под Линукс. Моя задача - в увлекательном формате делиться опытом и погружать вас в мир…»
Итоги квартала
Делаю сводку ключевых постов (сентябрь - ноябрь 2024).
Общая разработка:
1. Говорим про либы: GTK и QT [1]
Процессы и программы
1. Ресурсы процессов
2. Демоны в Linux
3. Идентификаторы процессов: пользователи и группы [1]
4. Идентификаторы процессов: пользователи и группы [2]
5. Процессы и программы: переменные окружения
6. Как демоны выполняют логгирование?
7. Базовые принципы коммуникации "пользователь -> приложение"
8. Топ команд по управлению процессами
Пользователи и группы
1. Файл групп: /etc/group
2. Файлы паролей: passwd и shadow
Разбор команд
1. stat
2. chmod и chown
3. visudo
4. chroot
5. history
6. alias
Командные оболочки
1. Что такое командная оболочка?
2. Особенности оболочек: bash, sh, zsh, fish
3. Понимание типов оболочек: interactive, non-interactive, login, non-login
4. Конфигурация оболочки
Модули ядра
1. Что такое модуль ядра?
2. Администрирование модулей ядра
Сигналы
1. Концепция сигналов в Linux
2. Перехват и обработка сигналов
LinuxCamp
Делаю сводку ключевых постов (сентябрь - ноябрь 2024).
Общая разработка:
1. Говорим про либы: GTK и QT [1]
Процессы и программы
1. Ресурсы процессов
2. Демоны в Linux
3. Идентификаторы процессов: пользователи и группы [1]
4. Идентификаторы процессов: пользователи и группы [2]
5. Процессы и программы: переменные окружения
6. Как демоны выполняют логгирование?
7. Базовые принципы коммуникации "пользователь -> приложение"
8. Топ команд по управлению процессами
Пользователи и группы
1. Файл групп: /etc/group
2. Файлы паролей: passwd и shadow
Разбор команд
1. stat
2. chmod и chown
3. visudo
4. chroot
5. history
6. alias
Командные оболочки
1. Что такое командная оболочка?
2. Особенности оболочек: bash, sh, zsh, fish
3. Понимание типов оболочек: interactive, non-interactive, login, non-login
4. Конфигурация оболочки
Модули ядра
1. Что такое модуль ядра?
2. Администрирование модулей ядра
Сигналы
1. Концепция сигналов в Linux
2. Перехват и обработка сигналов
LinuxCamp
👍50🔥16❤13❤🔥6
Отправка сигналов: kill()
Один процесс может отправить сигнал другому с помощью системного вызова kill(), который является базой для команды оболочки kill:
Термин kill был выбран потому, что в "древности" (в ранних версиях UNIX) для большинства сигналов действием по умолчанию было завершение процесса.
Для вызова kill, аргумент pid идентифицирует один или несколько процессов, в которые будет направлен сигнал, заданный параметром sig. Четыре различных случая определяют, каким образом интерпретируется значение аргумента pid:
1) pid > 0, сигнал отправляется в процесс, идентификатор которого указан в аргументе pid;
2) pid == 0, сигнал отправляется во все процессы той же группы, что и вызывающий процесс, в том числе и в сам вызывающий процесс;
3) pid < –1, сигнал отправляется во все процессы группы, идентификатор которой == абсолютному значению аргумента pid;
4) pid == –1, сигнал отправляется во все процессы, для которых у отправителя есть разрешение, кроме init (PID == 1) и самого себя;
Если сигнал отправляется привилегированным процессом, он будет доставлен во все процессы в системе, кроме двух выше обозначенных.
Если ни один из процессов не подходит под pid, функция kill() завершается с ошибкой и устанавливает для переменной errno значение ESRCH («Нет такого процесса»).
Важно помнить, что процессу для отправки сигнала другому требуются соответствующие разрешения:
1) процесс с привилегией CAP_KILL может игнорировать ограничения и посылать сигналы куда угодно. Выдать исполняемому файлу права можно через команду setcap:
2) процесс init, запущенный пользователем и группой root, — особый случай. В него можно посылать только те сигналы, для которых у него установлены обработчики.
Это предотвращает возникновение ситуаций, когда системный администратор случайно аварийно завершает процесс init, фундаментальный для работы системы;
3) непривилегированный процесс может отправлять сигнал другому, если его реальный (RUID) или эффективный (EUID), совпадает с теми же параметрами получателя. Про различные типы ID процессов мы ранее говорили вот тут;
4) сигнал SIGCONT обрабатывается по особым правилам. Непривилегированный процесс может послать этот сигнал в любой процесс, запущенный в той же сессии, минуя проверку ID пользователей;
Если у отправителя нет разрешения на отправку сигнала в процесс pid, вызов kill() завершится неудачно с установкой значения EPERM в errno.
Пример программы, отправляющей сигнал:
LinuxCamp
Один процесс может отправить сигнал другому с помощью системного вызова kill(), который является базой для команды оболочки kill:
#include <signal.h>
int kill(pid_t pid, int sig);
Термин kill был выбран потому, что в "древности" (в ранних версиях UNIX) для большинства сигналов действием по умолчанию было завершение процесса.
Для вызова kill, аргумент pid идентифицирует один или несколько процессов, в которые будет направлен сигнал, заданный параметром sig. Четыре различных случая определяют, каким образом интерпретируется значение аргумента pid:
1) pid > 0, сигнал отправляется в процесс, идентификатор которого указан в аргументе pid;
2) pid == 0, сигнал отправляется во все процессы той же группы, что и вызывающий процесс, в том числе и в сам вызывающий процесс;
3) pid < –1, сигнал отправляется во все процессы группы, идентификатор которой == абсолютному значению аргумента pid;
4) pid == –1, сигнал отправляется во все процессы, для которых у отправителя есть разрешение, кроме init (PID == 1) и самого себя;
Если сигнал отправляется привилегированным процессом, он будет доставлен во все процессы в системе, кроме двух выше обозначенных.
Если ни один из процессов не подходит под pid, функция kill() завершается с ошибкой и устанавливает для переменной errno значение ESRCH («Нет такого процесса»).
Важно помнить, что процессу для отправки сигнала другому требуются соответствующие разрешения:
1) процесс с привилегией CAP_KILL может игнорировать ограничения и посылать сигналы куда угодно. Выдать исполняемому файлу права можно через команду setcap:
setcap cap_kill+ep /path/to/bin
2) процесс init, запущенный пользователем и группой root, — особый случай. В него можно посылать только те сигналы, для которых у него установлены обработчики.
Это предотвращает возникновение ситуаций, когда системный администратор случайно аварийно завершает процесс init, фундаментальный для работы системы;
3) непривилегированный процесс может отправлять сигнал другому, если его реальный (RUID) или эффективный (EUID), совпадает с теми же параметрами получателя. Про различные типы ID процессов мы ранее говорили вот тут;
4) сигнал SIGCONT обрабатывается по особым правилам. Непривилегированный процесс может послать этот сигнал в любой процесс, запущенный в той же сессии, минуя проверку ID пользователей;
Если у отправителя нет разрешения на отправку сигнала в процесс pid, вызов kill() завершится неудачно с установкой значения EPERM в errno.
Пример программы, отправляющей сигнал:
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
int main(int argc, char *argv[])
{
long pid = strtol(argv[1], NULL, 10);
int signal = (int) strtol(argv[2], NULL, 10);
int rcode = kill(pid, signal);
printf("PID = %ld\n", pid);
printf("SIGID = %d\n", signal);
if (rcode == 0) {
printf("Process exists, the signal is sent\n");
}
else if (errno == EPERM) {
printf("We don't have rights\n");
}
else if (errno == ESRCH) {
printf("Process does not exist\n");
}
else {
exit(EXIT_FAILURE);
}
}
$ ./prog 20597 9
PID = 20597
SIGID = 9
Process exists, the signal is sent
LinuxCamp
🔥19👍11❤🔥4❤3
Отправка сигнала самому себе: raise()
Иногда полезной практикой является отправка процессом сигнала самому себе. Эту задачу выполняет функция стандартной библиотеки (libc) raise():
В программе с одним потоком, вызов raise() аналогичен:
В приложениях с несколькими потоками, вызов raise() аналогичен:
Такая реализация означает, что сигнал будет доставлен ТОЛЬКО в тот поток, из которого был выполнен raise(). Вызов kill(getpid(), sig) посылает сигнал в вызывающий процесс => сигнал может быть доставлен в любой его поток.
Приведу небольшой пример для демонстрации работы функции (вызов signal() и механизм перехвата объясняются вот тут):
Выполним программу и получим:
Возвращаемое значение
Обратите внимание, что функция raise() возвращает ненулевой результат (не обязательно –1) при возникновении ошибки. Единственная проблема, которая может тут возникнуть — EINVAL (неверное значение sig).
LinuxCamp
Иногда полезной практикой является отправка процессом сигнала самому себе. Эту задачу выполняет функция стандартной библиотеки (libc) raise():
#include <signal.h>
int raise(int sig);
В программе с одним потоком, вызов raise() аналогичен:
kill(getpid(), sig);
В приложениях с несколькими потоками, вызов raise() аналогичен:
pthread_kill(pthread_self(), sig);
Такая реализация означает, что сигнал будет доставлен ТОЛЬКО в тот поток, из которого был выполнен raise(). Вызов kill(getpid(), sig) посылает сигнал в вызывающий процесс => сигнал может быть доставлен в любой его поток.
Приведу небольшой пример для демонстрации работы функции (вызов signal() и механизм перехвата объясняются вот тут):
#include <signal.h>
#include <stdio.h>
void handler(int sig) {
printf("Signal received : %d\n", sig);
}
int main() {
signal(SIGILL, handler);
printf("Signal sent : %d\n", SIGILL);
raise(SIGILL);
return 0;
}
Выполним программу и получим:
$ ./prog
Signal sent : 4
Signal received : 4
Возвращаемое значение
Обратите внимание, что функция raise() возвращает ненулевой результат (не обязательно –1) при возникновении ошибки. Единственная проблема, которая может тут возникнуть — EINVAL (неверное значение sig).
LinuxCamp
👍22❤🔥6⚡4🔥2🤣2❤1
Защита от незваных гостей: сигнальная маска
Для каждого процесса ядро хранит специальную маску, которая определяет те сигналы, которые будут временно заблокированы и необработаны.
Если в процесс отправляется заблокированный сигнал, то доставка этого сигнала откладывается до тех пор, пока он не будет разблокирован путем удаления из сигнальной маски.
Как же нам определить эту чудо-маску и замутить пару сигналов?) Использовать системный вызов sigprocmask():
Аргумент how определяет то, каким образом набор сигналов, передаваемый в set, изменит текущую маску процесса:
1. SIG_BLOCK — сигналы, включенные set, добавляются в сигнальную маску. Другими словами, блокируемые сигналы остаются таковыми, а новые добавляются к ним;
2. SIG_UNBLOCK — сигналы, включенные set, исключаются из сигнальной маски;
3. SIG_SETMASK — сигналы, включенные set, переопределяют текущую маску. Сигналы, не включённые в новое множество, больше не блокируются;
Если аргумент oldset не равен NULL, то он указывает на переменную типа sigset_t, в которую записывается текущее значение сигнальной маски (до изменения).
Если мы хотим через oldset получить сигнальную маску без каких-то изменений, можно установить значение NULL для аргумента set, в этом случае аргумент how будет проигнорирован.
Ну все, давайте, ближе к делу, ближе к коду. Сейчас используем вообще все наши знания по сигналам: напишем перехватчик, отправим сигнал самому себе, определим маску и посмотрим на результат:
Теперь скомпилим и запустим прогу:
И что... Бам, никакого вывода мы не увидим - маска успешно отработала.
А теперь го еще прикол. Помните, ранее речь заходила о том, что отправленные сигналы не отменяются, а откладываются - как только мы уберем его из маски, процесс его примет и обработает.
Проверить мы это сможем, если добавим следующий код после вызова "raise(SIGINT);":
Ждем 5 сек и наблюдаем лог:
Теперь пару слов про сигналы: SIGKILL и SIGSTOP. Их мы игнорировать не можем, УВЫ и АХ. Если мы попытаемся заблокировать эти сигналы, функция sigprocmask() ошибку не выдаст, но и желаемого результата мы также не получим.
Для того чтобы добавить все сигналы в маску, кроме SIGKILL и SIGSTOP, мы можем использовать следующий вызов:
LinuxCamp
Для каждого процесса ядро хранит специальную маску, которая определяет те сигналы, которые будут временно заблокированы и необработаны.
Если в процесс отправляется заблокированный сигнал, то доставка этого сигнала откладывается до тех пор, пока он не будет разблокирован путем удаления из сигнальной маски.
Как же нам определить эту чудо-маску и замутить пару сигналов?) Использовать системный вызов sigprocmask():
#include <signal.h>
int sigprogmask(int how, const sigset_t *set, sigset_t *oldset);
Аргумент how определяет то, каким образом набор сигналов, передаваемый в set, изменит текущую маску процесса:
1. SIG_BLOCK — сигналы, включенные set, добавляются в сигнальную маску. Другими словами, блокируемые сигналы остаются таковыми, а новые добавляются к ним;
2. SIG_UNBLOCK — сигналы, включенные set, исключаются из сигнальной маски;
3. SIG_SETMASK — сигналы, включенные set, переопределяют текущую маску. Сигналы, не включённые в новое множество, больше не блокируются;
Если аргумент oldset не равен NULL, то он указывает на переменную типа sigset_t, в которую записывается текущее значение сигнальной маски (до изменения).
Если мы хотим через oldset получить сигнальную маску без каких-то изменений, можно установить значение NULL для аргумента set, в этом случае аргумент how будет проигнорирован.
Ну все, давайте, ближе к делу, ближе к коду. Сейчас используем вообще все наши знания по сигналам: напишем перехватчик, отправим сигнал самому себе, определим маску и посмотрим на результат:
#include <signal.h>
#include <unistd.h>
void handler (int num) {
write(STDOUT_FILENO, "I won't die!\n", 13);
}
int main() {
signal(SIGINT, handler);
sigset_t blockSet, prevMask;
sigemptyset(&blockSet);
sigaddset(&blockSet, SIGINT);
sigprocmask(SIG_BLOCK, &blockSet, &prevMask);
raise(SIGINT);
return 0;
}
Теперь скомпилим и запустим прогу:
$ gcc -o prog main.c
$ ./prog
И что... Бам, никакого вывода мы не увидим - маска успешно отработала.
А теперь го еще прикол. Помните, ранее речь заходила о том, что отправленные сигналы не отменяются, а откладываются - как только мы уберем его из маски, процесс его примет и обработает.
Проверить мы это сможем, если добавим следующий код после вызова "raise(SIGINT);":
sleep(5);
sigprocmask(SIG_SETMASK, &prevMask, NULL);
Ждем 5 сек и наблюдаем лог:
$ ./prog
I won't die!
Теперь пару слов про сигналы: SIGKILL и SIGSTOP. Их мы игнорировать не можем, УВЫ и АХ. Если мы попытаемся заблокировать эти сигналы, функция sigprocmask() ошибку не выдаст, но и желаемого результата мы также не получим.
Для того чтобы добавить все сигналы в маску, кроме SIGKILL и SIGSTOP, мы можем использовать следующий вызов:
sigfillset(&blockSet);
LinuxCamp
👍25🔥12❤🔥5✍2❤1
Работа с ожидающими сигналами
Друзья, еще недолго осталось ворочать сигналы, но нужно дожать) Сегодня обсудим то, как можно проработать сигналы, которые заблокированы маской, но направлены процессу.
Если процесс получает сигнал, который в данный момент подлежит блокированию - входит в сигнальную маску, то он добавляется в набор ожидающих сигналов. Когда сигнал будет разблокирован, он 100% будет доставлен в процесс.
Для определения того, какие сигналы процесса находятся в режиме ожидания и пытаются пробиться, мы можем использовать sigpending():
Функция возвращает в set набор сигналов, которые находятся в режиме ожидания процесса.
После этого мы сможем проверить содержимое set с помощью функции sigismember(), которая даст нам понимание того, входит ли указанный сигнал в структурку:
Функция возвращает 1, если sig является членом set, 0, если не является, и –1 при ошибке (например, sig не является допустимым номером сигнала):
Сигналы не ставятся в очередь
Набор ожидающих сигналов — это лишь маска, она показывает факт возникновения того или иного сигнала, но не их количество.
Иначе говоря, если один и тот же сигнал был сгенерирован несколько раз (будучи заблокированным), то он записывается в набор, а затем доставляется, но лишь ОДНАЖДЫ.
LinuxCamp
Друзья, еще недолго осталось ворочать сигналы, но нужно дожать) Сегодня обсудим то, как можно проработать сигналы, которые заблокированы маской, но направлены процессу.
Если процесс получает сигнал, который в данный момент подлежит блокированию - входит в сигнальную маску, то он добавляется в набор ожидающих сигналов. Когда сигнал будет разблокирован, он 100% будет доставлен в процесс.
Для определения того, какие сигналы процесса находятся в режиме ожидания и пытаются пробиться, мы можем использовать sigpending():
#include <signal.h>
int sigpending(sigset_t *set);
Функция возвращает в set набор сигналов, которые находятся в режиме ожидания процесса.
После этого мы сможем проверить содержимое set с помощью функции sigismember(), которая даст нам понимание того, входит ли указанный сигнал в структурку:
#include <signal.h>
int sigismember(sigset_t *set, int sig);
Функция возвращает 1, если sig является членом set, 0, если не является, и –1 при ошибке (например, sig не является допустимым номером сигнала):
sigset_t pending;
sigpending(&pending);
if (sigismember(&pending, SIGUSR1)) {
printf("SIGUSR1 заблокирован\n");
}
Сигналы не ставятся в очередь
Набор ожидающих сигналов — это лишь маска, она показывает факт возникновения того или иного сигнала, но не их количество.
Иначе говоря, если один и тот же сигнал был сгенерирован несколько раз (будучи заблокированным), то он записывается в набор, а затем доставляется, но лишь ОДНАЖДЫ.
LinuxCamp
👍23🔥8❤🔥4
Перехватывай сигналы, как профи: sigaction()
Сегодня поговорим про вызов, который активно рекомендуется к использованию в качестве альтернативы примитивному signal(). Несмотря на то, что sigaction() сложнее в использовании, взамен он предоставляет большую гибкость:
Аргумент sig означает сигнал, который мы хотим перехватить. Значением может быть любой экземпляр, кроме SIGKILL или SIGSTOP.
Аргумент act — указатель на структуру, определяющую условия перехвата. Если нам необходимо получить актуальное значения, без изменений, можно указать NULL.
Аргумент oldact — это указатель на структуру такого же типа. Используется для возврата информации о предыдущих правилах перехвата. Если нам это неинтересно, можно также задать значение NULL.
Структура, на которую указывают аргументы act и oldact, выглядит следующим образом:
Поле sa_handler соотносится с аргументом handler, передаваемым функции signal(). В данном поле указывается либо адрес обработчика либо одна из констант — SIG_IGN (сигнал будет проигнорирован) или SIG_DFL (процесс ответит дефолтным поведением на сигнал).
Поле sa_mask определяет список сигналов, блокируемых во время активации обработчика. После старта его выполнения, все сигналы из набора автоматически добавляются в сигнальную маску. После того, как произойдет возврат из обработчика, они автоматически удалятся.
Сделано это для того, чтобы никакой из сигналов не прервал выполнение обработчика.
Поле sa_flags — битовая маска, устанавливающая разные параметры, контролирующие обработку сигнала. Биты могут быть объединены в этом поле битовой операцией ИЛИ (|). Более детально ознакомиться с флагами можете в мануале.
Простой пример использования вызова sigaction():
И да, как видите, я проинициализировал почти все поля структуры. Если вы работаете на C, не забывайте, что это не C++ и тут нет конструкторов, деструкторов, дефолтной инициализации. Если явно не указать значения переменным, они могут спокойно заполниться мусором, что приведет к UB (undefined behavior).
Где sigaction() прям 100% полезен?
Мы с вами еще не говорили подробно о сигналах реального времени, но при работе с ними вам прям станет ясна необходимость sigaction(). Дело в том, что через настройку перехватчика мы cможем принимать дополнительные аргументы - пользовательские данные. Использовать для этого нам нужно флаг SA_SIGINFO:
Если флаг указан, обработчик должен присваиваться полю sa_sigaction и принимать расширенный интерфейс для пользовательских данных:
LinuxCamp
Сегодня поговорим про вызов, который активно рекомендуется к использованию в качестве альтернативы примитивному signal(). Несмотря на то, что sigaction() сложнее в использовании, взамен он предоставляет большую гибкость:
#include <signal.h>
// Возвращает 0 при успешном завершении, –1 при ошибке
int sigaction(int sig, const struct sigaction *act, struct sigaction *oldact);
Аргумент sig означает сигнал, который мы хотим перехватить. Значением может быть любой экземпляр, кроме SIGKILL или SIGSTOP.
Аргумент act — указатель на структуру, определяющую условия перехвата. Если нам необходимо получить актуальное значения, без изменений, можно указать NULL.
Аргумент oldact — это указатель на структуру такого же типа. Используется для возврата информации о предыдущих правилах перехвата. Если нам это неинтересно, можно также задать значение NULL.
Структура, на которую указывают аргументы act и oldact, выглядит следующим образом:
struct sigaction
{
/* Адрес обработчика */
void (*sa_handler)(int);
/* Адрес расширенного обработчика */
void (*sa_sigaction)(int, siginfo_t *, void *);
/* Сигналы, блокируемые во время вызова обработчика */
sigset_t sa_mask;
/* Флаги, контролирующие активацию обработчика */
int sa_flags;
/* Устарел и не должен использоваться */
void (*sa_restorer)(void);
};
Поле sa_handler соотносится с аргументом handler, передаваемым функции signal(). В данном поле указывается либо адрес обработчика либо одна из констант — SIG_IGN (сигнал будет проигнорирован) или SIG_DFL (процесс ответит дефолтным поведением на сигнал).
Поле sa_mask определяет список сигналов, блокируемых во время активации обработчика. После старта его выполнения, все сигналы из набора автоматически добавляются в сигнальную маску. После того, как произойдет возврат из обработчика, они автоматически удалятся.
Сделано это для того, чтобы никакой из сигналов не прервал выполнение обработчика.
Поле sa_flags — битовая маска, устанавливающая разные параметры, контролирующие обработку сигнала. Биты могут быть объединены в этом поле битовой операцией ИЛИ (|). Более детально ознакомиться с флагами можете в мануале.
Простой пример использования вызова sigaction():
#include <signal.h>
#include <stdio.h>
void handle_signal(int sig) {
printf("Caught signal %d\n", sig);
}
int main() {
struct sigaction sa;
sa.sa_handler = handle_signal;
sa.sa_flags = 0;
sigemptyset(&sa.sa_mask);
if (sigaction(SIGINT, &sa, NULL) == -1) {
perror("sigaction");
return 1;
}
while (1);
}
И да, как видите, я проинициализировал почти все поля структуры. Если вы работаете на C, не забывайте, что это не C++ и тут нет конструкторов, деструкторов, дефолтной инициализации. Если явно не указать значения переменным, они могут спокойно заполниться мусором, что приведет к UB (undefined behavior).
Где sigaction() прям 100% полезен?
Мы с вами еще не говорили подробно о сигналах реального времени, но при работе с ними вам прям станет ясна необходимость sigaction(). Дело в том, что через настройку перехватчика мы cможем принимать дополнительные аргументы - пользовательские данные. Использовать для этого нам нужно флаг SA_SIGINFO:
sa.sa_flags = SA_SIGINFO;
Если флаг указан, обработчик должен присваиваться полю sa_sigaction и принимать расширенный интерфейс для пользовательских данных:
void handler(int sig, siginfo_t *info, void *context);
sa.sa_sigaction = handler;
LinuxCamp
🔥19👍12❤🔥6
Функции для работы с набором сигналов
Многие системные вызовы, связанные с сигналами, работают с типом данных sigset_t, который представляет их набор. Например, sigaction() и sigprocmask() позволяют программе указать группу сигналов, которые должны быть заблокированы процессом, sigpending() возвращает группу, находящуюся в режиме ожидания.
В общем-то, работать с сетом напрямую - дикость, поэтому сейчас рассмотрим несколько фунций, которые для этого используются.
Функция sigemptyset() инициализирует пустой набор сигналов, sigfillset(), напротив, записывает в него все сигналы:
Для инициализации набора сигналов должна быть использована одна из функций выше. Это необходимо потому, что C за вас ничего не определит. Если проигнорировать данный шаг, можно спокойно аварийно положить приложение, т.к. сет заполнится мусором.
После инициализации отдельные сигналы могут быть добавлены в набор с помощью функции sigaddset() и удалены — с помощью sigdelset():
Функция sigismember() проверяет, является ли сигнал членом набора:
В GNU-библиотеке C реализованы три нестандартные функции, которые дополняют те, что были описаны выше:
1) sigandset() — выполняет логическое И (пересечение) над двумя наборами left и right, помещает результат в dest;
2) sigorset() — выполняет логическое ИЛИ (объединение) над двумя наборами left и right, помещает результат в dest;
3) sigisemptyset() — возвращает 1, если в set нет ни одного сигнала, 0 в противном случае;
LinuxCamp
Многие системные вызовы, связанные с сигналами, работают с типом данных sigset_t, который представляет их набор. Например, sigaction() и sigprocmask() позволяют программе указать группу сигналов, которые должны быть заблокированы процессом, sigpending() возвращает группу, находящуюся в режиме ожидания.
В общем-то, работать с сетом напрямую - дикость, поэтому сейчас рассмотрим несколько фунций, которые для этого используются.
Функция sigemptyset() инициализирует пустой набор сигналов, sigfillset(), напротив, записывает в него все сигналы:
#include <signal.h>
// Возвращают 0 при успешном завершении, –1 при ошибке
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t, *set);
Для инициализации набора сигналов должна быть использована одна из функций выше. Это необходимо потому, что C за вас ничего не определит. Если проигнорировать данный шаг, можно спокойно аварийно положить приложение, т.к. сет заполнится мусором.
После инициализации отдельные сигналы могут быть добавлены в набор с помощью функции sigaddset() и удалены — с помощью sigdelset():
#include <signal.h>
int sigaddset(sigset_t *set, int sig);
int sigdelset(sigset_t *set, int sig);
Функция sigismember() проверяет, является ли сигнал членом набора:
#include <signal.h>
int sigismember(sigset_t *set, int sig);
В GNU-библиотеке C реализованы три нестандартные функции, которые дополняют те, что были описаны выше:
#define _GNU_SOURCE
#include <signal.h>
int sigandset(sigset_t *set, sigset_t *left, sigset_t *right);
int sigorset(sigset_t *set, sigset_t *left, sigset_t *right);
int sigisemptyset(const sigset_t *set);
1) sigandset() — выполняет логическое И (пересечение) над двумя наборами left и right, помещает результат в dest;
2) sigorset() — выполняет логическое ИЛИ (объединение) над двумя наборами left и right, помещает результат в dest;
3) sigisemptyset() — возвращает 1, если в set нет ни одного сигнала, 0 в противном случае;
LinuxCamp
👍18🔥7❤🔥4
Зачем процессы туда данные отправляют?
Этот пост является неким анонсом публикации про перенаправление ввода-вывода: >, >>, |. Чтобы понять, как оно работает, следует сначала разобрать потоки данных, в которые перенаправление и происходит.
И так, у каждой программы существует 3 системных потока: stdout, stderr, stdin. Потоки представляют собой сущности для транспорта информации - для простоты пока можно считать их файлами под дескрипторами 0 (in), 1 (out), 2 (err):
В примере выше мы зашли в виртуальную директорию процесса 551981 (PID), в которой хранятся дескрипторы открытых файлов. По ним, как раз, происходит чтение и запись информации.
STDIN
Данный поток используется процессом для получения информации извне. Когда мы запрашиваем какой-то ввод от пользователя, данные попадают в stdin, после чего считываются из него программой.
Давайте запустим команду cat:
В примере выше cat ожидает от пользователя ввод, который впоследствии считывается с потока stdin (по 0 дескриптору) и направляется в поток stdout для вывода в терминал. Как результат, мы видим дубликат нашего текста.
Никто нам также не запрещает закрыть дескриптор, отвечающий за stdin - в таком случае программа просто не сможет получить данные.
STDOUT
Поток stdout (дескриптор 1) отвечает за вывод информации программой. По умолчанию все, что в него попадает, выводится в терминал:
STDERR
Еще один поток вывода информации (дескриптор 2), который отвечает за отображение ошибок. Если программа не смогла сделать все как надо — она пишет именно в него. Например, когда rm пытается удалить несуществующий файл:
Если вы вдруг не различили stdout от stderr либо хотите убедиться в том, что программа завершилась с ошибкой, можете воспользоваться следующей командой для получения кода возврата:
Значение 1 говорит о том, что в программе были ошибки, 0 - все хорошо. Такой прием бывает полезен для написания скриптов автоматизации - нам иногда следует понимать, нормально ли прога отработала.
LinuxCamp
Этот пост является неким анонсом публикации про перенаправление ввода-вывода: >, >>, |. Чтобы понять, как оно работает, следует сначала разобрать потоки данных, в которые перенаправление и происходит.
И так, у каждой программы существует 3 системных потока: stdout, stderr, stdin. Потоки представляют собой сущности для транспорта информации - для простоты пока можно считать их файлами под дескрипторами 0 (in), 1 (out), 2 (err):
$ cd /proc/551981/fd
$ ls
0 1 103 2 43
В примере выше мы зашли в виртуальную директорию процесса 551981 (PID), в которой хранятся дескрипторы открытых файлов. По ним, как раз, происходит чтение и запись информации.
STDIN
Данный поток используется процессом для получения информации извне. Когда мы запрашиваем какой-то ввод от пользователя, данные попадают в stdin, после чего считываются из него программой.
Давайте запустим команду cat:
$ cat
Hello bro
Hello bro
В примере выше cat ожидает от пользователя ввод, который впоследствии считывается с потока stdin (по 0 дескриптору) и направляется в поток stdout для вывода в терминал. Как результат, мы видим дубликат нашего текста.
Никто нам также не запрещает закрыть дескриптор, отвечающий за stdin - в таком случае программа просто не сможет получить данные.
STDOUT
Поток stdout (дескриптор 1) отвечает за вывод информации программой. По умолчанию все, что в него попадает, выводится в терминал:
$ echo $USER
parallels
STDERR
Еще один поток вывода информации (дескриптор 2), который отвечает за отображение ошибок. Если программа не смогла сделать все как надо — она пишет именно в него. Например, когда rm пытается удалить несуществующий файл:
$ rm example.txt
rm: example.txt: No such file or directory
Если вы вдруг не различили stdout от stderr либо хотите убедиться в том, что программа завершилась с ошибкой, можете воспользоваться следующей командой для получения кода возврата:
$ rm example.txt
rm: example.txt: No such file or directory
$ echo $?
1
Значение 1 говорит о том, что в программе были ошибки, 0 - все хорошо. Такой прием бывает полезен для написания скриптов автоматизации - нам иногда следует понимать, нормально ли прога отработала.
LinuxCamp
👍31🔥9❤🔥3❤2🍌1
Для чего учишь Linux?
Пост нацелен на то, чтобы понять мотивацию аудитории и проработать контент.
Пост нацелен на то, чтобы понять мотивацию аудитории и проработать контент.
Anonymous Poll
13%
Я - системный разработчик, мне нужно знать Linux API и хорошо понимать принцип устройства ОС.
23%
Я - разработчик, мне нужно знать принципы администрирования и уметь деплоить приложение под Linux.
20%
Я - не разработчик, пользуюсь Linux по причине удобства и хочу чувствовать себя в нем уверенно.
28%
Я - не программист, но имею отношение к IT (тестировщик, сисадмин) и хочу освоить Linux для работы.
15%
Я - еще не в IT, но хочу залететь и поэтому изучаю Linux.
🔥38👍8🤝4❤🔥1
Операторы перенаправления ввода-вывода [1]
Недавно мы вот узнали, через какие потоки процесс пишет и читает данные. Теперь пора глянуть на операторы оболочки, которые позволяют перенаправлять потоки на разные источники данных. Например, вывод одной команды подать на вход другой либо записать stdout, stderr в файл.
Сделаем серию из 3 постов. Сегодня рассмотрим операторы для перенаправления вывода STDOUT и STDERR: >, >>. В следующие разы поговорим про STDIN и завершим тему оператором '|'.
Перенаправление потоков вывода:
Предположим, вы хотите создать файл, в который будет записан вывод STDOUT какой-то программы (разберем на примере date).
Для того, чтобы данные оказались в файле, нужно добавить символ '>' после команды и перед именем целевого файла:
Перенаправление вывода с использованием '>' создаст новый файл или полностью перезапишет содержимое существующего.
Использование '>>' позволяет также создать новый файл при отсутствии и добавить данные в его конец - без полной перезаписи:
Подчеркну, что перенаправляется только STDOUT. Если программа писала ошибки в поток STDERR, они так и выведутся в терминал:
Давайте дополнительно обработаем и STDERR - для этого нам явно нужно указать номер целевого дескриптора "2>":
Также бывает полезно подчистить вывод какой-нибудь команды от всех ошибок. Для этого нам нужно перенаправить STDERR в "/dev/null":
Все, что попадает в "/dev/null" исчезает на веки вечные. Такой себе "black hole".
И, наконец, если нужно, чтобы всё попало в один файл, можно перенаправить оба потока в одно и то же место:
Выражение "2>&1" означает «отправлять stderr туда, куда направляется stdout».
LinuxCamp
Недавно мы вот узнали, через какие потоки процесс пишет и читает данные. Теперь пора глянуть на операторы оболочки, которые позволяют перенаправлять потоки на разные источники данных. Например, вывод одной команды подать на вход другой либо записать stdout, stderr в файл.
Сделаем серию из 3 постов. Сегодня рассмотрим операторы для перенаправления вывода STDOUT и STDERR: >, >>. В следующие разы поговорим про STDIN и завершим тему оператором '|'.
Перенаправление потоков вывода:
Предположим, вы хотите создать файл, в который будет записан вывод STDOUT какой-то программы (разберем на примере date).
Для того, чтобы данные оказались в файле, нужно добавить символ '>' после команды и перед именем целевого файла:
$ date > date.txt
$ cat date.txt
Sat Dec 14 20:50:46 MSK 2024
Перенаправление вывода с использованием '>' создаст новый файл или полностью перезапишет содержимое существующего.
Использование '>>' позволяет также создать новый файл при отсутствии и добавить данные в его конец - без полной перезаписи:
$ date >> date.txt
$ cat date.txt
Sat Dec 14 20:50:46 MSK 2024
Sat Dec 14 20:51:40 MSK 2024
Подчеркну, что перенаправляется только STDOUT. Если программа писала ошибки в поток STDERR, они так и выведутся в терминал:
$ find /etc -type f > ~/results.txt
find: ‘/etc/ssl/private’: Permission denied
Давайте дополнительно обработаем и STDERR - для этого нам явно нужно указать номер целевого дескриптора "2>":
$ find /etc -type f > ~/results.txt 2> ~/errors.txt
$ cat ~/errors.txt
find: ‘/etc/ssl/private’: Permission denied
Также бывает полезно подчистить вывод какой-нибудь команды от всех ошибок. Для этого нам нужно перенаправить STDERR в "/dev/null":
$ find /etc -type f 2> /dev/null
Все, что попадает в "/dev/null" исчезает на веки вечные. Такой себе "black hole".
И, наконец, если нужно, чтобы всё попало в один файл, можно перенаправить оба потока в одно и то же место:
$ ls > /tmp/lsdata 2>&1
$ ls &> /tmp/lsdata
Выражение "2>&1" означает «отправлять stderr туда, куда направляется stdout».
LinuxCamp
👍40🔥8❤🔥5🤝2🌭1
Операторы перенаправления ввода-вывода [2]
В некоторых случаях полезно перенаправить стандартный ввод STDIN, чтобы он поступал из файла, а не с клавиатуры. Выполнить данную задачу мы можем, используя оператор '<'.
Например, команда tr принимает данные от пользователя (через клавиатуру), чтобы выполнить преобразование всех символов нижнего регистра в верхний:
Команда не получает файл в качестве аргумента. Чтобы все-таки скормить ей статический поток данных, можно использовать то самое перенаправление STDIN:
Оператор '<<' heredoc
Оператор '<<' дает возможность перенаправлять многострочный поток данных (массив строк) на вход программе:
Может быть удобно, если нужно передать текстовые данные прямо в команду без использования файлов - допустим, в скриптах оболочки для задач автоматизации.
Как работает "heredoc"
После оператора указывается метка (произвольное слово), которая обозначает начало и конец текста.
Все строки между метками считаются входными данными для команды. Когда оболочка повторно встречает слово, которое шло сразу за '<<', ввод завершается:
В примере выше мы отправили команде cat на вход (STDIN) массив строк, вывод (STDOUT) которых перенаправили в конфиг.
Еще одним полезным примером может быть передача SQL-запросов в БД через ручной ввод:
LinuxCamp
В некоторых случаях полезно перенаправить стандартный ввод STDIN, чтобы он поступал из файла, а не с клавиатуры. Выполнить данную задачу мы можем, используя оператор '<'.
Например, команда tr принимает данные от пользователя (через клавиатуру), чтобы выполнить преобразование всех символов нижнего регистра в верхний:
$ tr 'a-z' 'A-Z'
hello
HELLO
Команда не получает файл в качестве аргумента. Чтобы все-таки скормить ей статический поток данных, можно использовать то самое перенаправление STDIN:
$ cat animals.txt
1 retriever
2 badger
$ tr 'a-z' 'A-Z' < animals.txt
1 RETRIEVER
2 BADGER
Оператор '<<' heredoc
Оператор '<<' дает возможность перенаправлять многострочный поток данных (массив строк) на вход программе:
$ cat << EOF
> Сurrent user: $(whoami)
> Status: sysadmin
> EOF
Сurrent user: parallels
Status: sysadmin
Может быть удобно, если нужно передать текстовые данные прямо в команду без использования файлов - допустим, в скриптах оболочки для задач автоматизации.
Как работает "heredoc"
После оператора указывается метка (произвольное слово), которая обозначает начало и конец текста.
Все строки между метками считаются входными данными для команды. Когда оболочка повторно встречает слово, которое шло сразу за '<<', ввод завершается:
$ cat << EOF > config.conf
[settings]
host=localhost
port=8080
debug=true
EOF
В примере выше мы отправили команде cat на вход (STDIN) массив строк, вывод (STDOUT) которых перенаправили в конфиг.
Еще одним полезным примером может быть передача SQL-запросов в БД через ручной ввод:
$ mysql -u root -p << EOF
CREATE DATABASE my_database;
USE my_database;
...
EOF
LinuxCamp
🔥37👍17❤🔥3❤1
Операторы перенаправления ввода-вывода [3]
Я же уверен, что 80% из вас когда-то использовали оператор "|". Он невероятно полезен - позволяет подключить стандартный вывод (STDOUT) одной команды к стандартному вводу (STDIN) другой, чтобы первая смогла передать свои выходные данные во вторую.
Вертикальная черта "|" между командами — это символ канала (pipe), который реализовывает механизм конвейера (pipeline).
Посмотрим на самый узнаваемый сценарий работы конвейера - отфильтровать вывод по шаблону:
В результате мы видим не все содержимое директории, а только то, с чем grep нашел пересечение по шаблону.
Команды обычно и не знают, что являются частью конвейера: ls считает, что выводит данные на дисплей, хотя на самом деле ее вывод был перенаправлен на grep, который верит, что читает данные с клавиатуры, когда на самом деле получает вывод ls:
Тут уже оболочка, на своем уровне, выполняет всю магию и использует системный вызов "pipe()" для перенаправления:
Важно понимать, что любая утилита, которая читает стандартный ввод или записывает вывод, может участвовать в создании конвейера и быть полезна для решения комплексной задачи.
Допустим, мы хотим узнать, сколько подкаталогов находится в "/usr/lib". Нет простой команды для получения ответа, поэтому создадим конвейер.
Начнем с простого вывода содержимого директории. Обратите внимание, что команда "ls –l" помечает каталоги буквой "d" в начале строки:
Используем cut, чтобы вывести первый столбец:
Затем используем grep, чтобы оставить только строки, содержащие букву "d":
Наконец, подсчитаем строки с помощью команды wc и получим ответ, созданный конвейером из четырех команд:
Результат: директория "/usr/lib" содержит 145 подкаталогов.
Что мы сделали? Превратили небольшую горстку команд в набор комбинируемых инструментов. Как говорится, целое всегда есть нечто большее, чем сумма его частей.
LinuxCamp
Я же уверен, что 80% из вас когда-то использовали оператор "|". Он невероятно полезен - позволяет подключить стандартный вывод (STDOUT) одной команды к стандартному вводу (STDIN) другой, чтобы первая смогла передать свои выходные данные во вторую.
Вертикальная черта "|" между командами — это символ канала (pipe), который реализовывает механизм конвейера (pipeline).
Посмотрим на самый узнаваемый сценарий работы конвейера - отфильтровать вывод по шаблону:
$ ls /usr/lib | grep nma
libnma.so.0
libnma.so.0.0.0
В результате мы видим не все содержимое директории, а только то, с чем grep нашел пересечение по шаблону.
Команды обычно и не знают, что являются частью конвейера: ls считает, что выводит данные на дисплей, хотя на самом деле ее вывод был перенаправлен на grep, который верит, что читает данные с клавиатуры, когда на самом деле получает вывод ls:
ls (STDOUT) -> grep (STDIN) -> grep (STDOUT)
Тут уже оболочка, на своем уровне, выполняет всю магию и использует системный вызов "pipe()" для перенаправления:
#include <unistd.h>
int pipe(int pipefd[2]);
Важно понимать, что любая утилита, которая читает стандартный ввод или записывает вывод, может участвовать в создании конвейера и быть полезна для решения комплексной задачи.
Допустим, мы хотим узнать, сколько подкаталогов находится в "/usr/lib". Нет простой команды для получения ответа, поэтому создадим конвейер.
Начнем с простого вывода содержимого директории. Обратите внимание, что команда "ls –l" помечает каталоги буквой "d" в начале строки:
$ ls -l /usr/lib
drwxrwxr-x ... 4kstogram
drwxr-xr-x ... NetworkManager
Используем cut, чтобы вывести первый столбец:
$ ls -l /usr/lib | cut -c1
d
d
d
-
-
-
Затем используем grep, чтобы оставить только строки, содержащие букву "d":
$ ls -l /usr/lib | cut -c1 | grep d
d
d
d
Наконец, подсчитаем строки с помощью команды wc и получим ответ, созданный конвейером из четырех команд:
$ ls -l /usr/lib | cut -c1 | grep d | wc -l
145
Результат: директория "/usr/lib" содержит 145 подкаталогов.
Что мы сделали? Превратили небольшую горстку команд в набор комбинируемых инструментов. Как говорится, целое всегда есть нечто большее, чем сумма его частей.
LinuxCamp
👍54🔥12❤🔥4
Набираем обороты с pattern matching
Чтобы повысить продуктивность, оболочка предусматривает ряд спецсимволов для оптимизации работы и выполнения ряда задач.
Концепция работы с символами: *, ?, [], называется сопоставлением с шаблоном (pattern matching).
И так, команда может включать в себя символ групповых операций * для одновременной ссылки на несколько файлов:
Этот символ обрабатывается командной оболочкой, а не программой ls. Оболочка заменяет выражение "*.py" списком имен подходящих файлов еще до запуска программы ls.
Иными словами, ls не увидит символ групповой операции. С точки зрения утилиты, вы ввели команду следующего вида:
Символ * соответствует любой последовательности из символов в путях к файлам или каталогам.
Например, гораздо проще и быстрее, вместо прямого перечисления:
Использовать базовый шаблон:
Остается за кадром, как командная оболочка (не программа grep) преобразует шаблон "chapter*" в список подходящих имен файлов. И только после этого оболочка запускает grep.
Если шаблон не соответствует ни одному файлу, оболочка передает его в качестве аргумента команды:
Еще бывает полезно использовать знак ?, который соответствует любому единичному символу. Например, вы можете выполнить поиск слова Linux в главах с 1 по 9, используя ? для поиска совпадений:
Для поиска в главах с 10 по 99 придется использовать "??":
Менее известно использование скобок [] для запроса у оболочки соответствия одному из символов набора. Например, вы можете искать только в первых 3 главах:
Любые символы, не только цифры, могут быть помещены в квадратные скобки для сопоставления.
Например, следующая команда заставит оболочку искать имена файлов, начинающиеся с заглавной буквы, содержащие символ _ и заканчивающиеся символом @:
LinuxCamp
Чтобы повысить продуктивность, оболочка предусматривает ряд спецсимволов для оптимизации работы и выполнения ряда задач.
Концепция работы с символами: *, ?, [], называется сопоставлением с шаблоном (pattern matching).
Если вы все еще путаетесь в том, что такое оболочка, предлагаю обратиться к моей последней сводке постов.
И так, команда может включать в себя символ групповых операций * для одновременной ссылки на несколько файлов:
$ ls *.py
data.py main.py user_interface.py
Этот символ обрабатывается командной оболочкой, а не программой ls. Оболочка заменяет выражение "*.py" списком имен подходящих файлов еще до запуска программы ls.
Иными словами, ls не увидит символ групповой операции. С точки зрения утилиты, вы ввели команду следующего вида:
$ ls data.py main.py user_interface.py
Символ * соответствует любой последовательности из символов в путях к файлам или каталогам.
Например, гораздо проще и быстрее, вместо прямого перечисления:
$ grep Linux chapter1 chapter2 chapter3 chapter4 chapter5...
Использовать базовый шаблон:
$ grep Linux chapter*
Остается за кадром, как командная оболочка (не программа grep) преобразует шаблон "chapter*" в список подходящих имен файлов. И только после этого оболочка запускает grep.
Если шаблон не соответствует ни одному файлу, оболочка передает его в качестве аргумента команды:
$ grep Linux chapter*
grep: chapter*: No such file or directory
Еще бывает полезно использовать знак ?, который соответствует любому единичному символу. Например, вы можете выполнить поиск слова Linux в главах с 1 по 9, используя ? для поиска совпадений:
$ grep Linux chapter?
Для поиска в главах с 10 по 99 придется использовать "??":
$ grep Linux chapter??
Менее известно использование скобок [] для запроса у оболочки соответствия одному из символов набора. Например, вы можете искать только в первых 3 главах:
$ grep Linux chapter[123]
$ grep Linux chapter[1-3]
Любые символы, не только цифры, могут быть помещены в квадратные скобки для сопоставления.
Например, следующая команда заставит оболочку искать имена файлов, начинающиеся с заглавной буквы, содержащие символ _ и заканчивающиеся символом @:
$ ls [A-Z]*_*@
LinuxCamp
👍38🔥15❤🔥3
Вычисление переменных и развеивание заблуждений
Мы с вами уже как-то говорили про суть переменных оболочки и окружения. Сегодня рассмотрим оператор $, который позволяет определить значение переменной.
Когда командная оболочка вычисляет переменную, определяет ее значение и подставляет его вместо имени. Чтобы выполнить задачу, нужно просто поставить знак $ перед именем.
Самый простой способ увидеть, как оболочка обрабатывает символ — запустить команду echo, которая выводит свои аргументы (после того, как оболочка завершит их вычисление):
Когда вы выводите на экран значение переменной с помощью команды echo, вы можете подумать, что сама команда проверяет переменную и выводит ее значение.
На самом деле это не так, команда ничего не знает о переменных. Она просто выводит на экран любые аргументы, которые вы ей передаете. Значения для HOME и USER вычисляет оболочка перед запуском команды.
Вы можете определить или изменить переменную в любое время, используя следующий синтаксис:
Например, если вы часто работаете с каталогом "Projects" внутри домашней директории, вы можете присвоить его имя переменной:
И использовать его как удобное сокращение при работе с cd:
Вы можете передавать $work любой команде, ожидающей имя каталога, в качестве аргумента:
Вообще, этот принцип важно понять: оболочка вычисляет переменные, шаблоны и другие конструкции перед выполнением команды.
Шаблоны vs переменные
Предположим, вы хотите перенести ряд файлов с расширением ".txt" из одного каталога в другой.
Вот два способа сделать это, но один работает, а другой нет:
Метод 1 работает, потому что шаблон соответствует всему пути к файлу — имя каталога dir1 является частью совпадений:
Таким образом, метод 1 работает так, как если бы вы набрали следующую команду:
В методе 2 используются переменные, имеющие только свои буквальные значения, и нет специального инструмента для вычисления путей к файлам:
Следовательно, метод 2 работает так, как если бы вы набрали следующую команду:
LinuxCamp
Мы с вами уже как-то говорили про суть переменных оболочки и окружения. Сегодня рассмотрим оператор $, который позволяет определить значение переменной.
Когда командная оболочка вычисляет переменную, определяет ее значение и подставляет его вместо имени. Чтобы выполнить задачу, нужно просто поставить знак $ перед именем.
Самый простой способ увидеть, как оболочка обрабатывает символ — запустить команду echo, которая выводит свои аргументы (после того, как оболочка завершит их вычисление):
$ echo My name is $USER and my files are in $HOME
My name is xoadmin and my files are in /home/xoadmin
$ echo ch*ter9
chapter9
Когда вы выводите на экран значение переменной с помощью команды echo, вы можете подумать, что сама команда проверяет переменную и выводит ее значение.
На самом деле это не так, команда ничего не знает о переменных. Она просто выводит на экран любые аргументы, которые вы ей передаете. Значения для HOME и USER вычисляет оболочка перед запуском команды.
Вы можете определить или изменить переменную в любое время, используя следующий синтаксис:
name=value
Например, если вы часто работаете с каталогом "Projects" внутри домашней директории, вы можете присвоить его имя переменной:
$ work=$HOME/Projects
И использовать его как удобное сокращение при работе с cd:
$ cd $work
$ pwd
/home/xoadmin/Projects
Вы можете передавать $work любой команде, ожидающей имя каталога, в качестве аргумента:
$ cp myfile $work
$ ls $work
Myfile
Вообще, этот принцип важно понять: оболочка вычисляет переменные, шаблоны и другие конструкции перед выполнением команды.
Шаблоны vs переменные
Предположим, вы хотите перенести ряд файлов с расширением ".txt" из одного каталога в другой.
Вот два способа сделать это, но один работает, а другой нет:
# Метод 1
$ mv dir1/*.txt dir2
# Метод 2
$ FILES="file1.txt file1.txt"
$ mv dir1/$FILES dir2
Метод 1 работает, потому что шаблон соответствует всему пути к файлу — имя каталога dir1 является частью совпадений:
$ echo dir1/*.txt
dir1/file1.txt dir1/file2.txt
Таким образом, метод 1 работает так, как если бы вы набрали следующую команду:
$ mv dir1/file1.txt dir1/file2.txt dir2
В методе 2 используются переменные, имеющие только свои буквальные значения, и нет специального инструмента для вычисления путей к файлам:
$ echo dir1/$FILES
dir1/file1.txt file2.txt
Следовательно, метод 2 работает так, как если бы вы набрали следующую команду:
$ mv dir1/file1.txt file2.txt dir2
/bin/mv: cannot stat 'file2.txt': No such file or directory
LinuxCamp
👍36🔥23❤🔥4
Выполняй только то, что нужно! Условные списки Bash
Скорее всего, вы каждый день запускаете команды, которые зависят от выполнения предыдущих.
Механизм списков позволяет не только разом выполнить группу команд, но и выстроить условную последовательность через операторы "&& (И)" и "|| (ИЛИ)".
Условные списки
Предположим, вы хотите создать файл new.txt в каталоге dir. Типичная последовательность может быть следующей:
Запуск второй команды зависит от успешного выполнения первой. Если каталог не существует, то нет смысла и команду запускать.
Оболочка позволяет сделать эту зависимость явной, объединив команды оператором && (И):
Если вы используете систему контроля версий Git, то, вероятно, знакомы со следующей последовательностью команд фиксации и внесения изменений:
Если какая-либо из команд завершится ошибкой, остальные вы не запустите. Поэтому эта цепочка хорошо работает в формате условного списка.
Так же, как оператор && запускает вторую команду только в случае успеха первой, оператор || (ИЛИ) запускает вторую команду только в случае сбоя первой.
Например, следующая команда пытается войти в каталог и, если это не удается, создает его:
Вы часто будете видеть этот оператор в сценариях оболочки. В частности, для выхода в случае возникновения ошибки:
Операторы && и || можно использовать вместе, чтобы настраивать более сложные действия:
Последовательность предполагает попытку войти в каталог dir, если это не удается, создать каталог и войти в него, а если и это не удается, вывести ошибку.
Безусловные списки
Команды в списке не обязательно должны зависеть друг от друга. Если вы разделяете команды точкой с запятой, они просто выполняются по порядку.
Вот пример команды, которая спит течение двух часов, а затем создает резервную копию важных файлов:
Безусловные списки дают те же результаты, что и ввод команд по отдельности.
Единственное существенное различие - коды возврата. В таком списке коды отдельных команд отбрасываются, кроме последней.
Только код возврата последней команды в списке присваивается переменной оболочки "?":
LinuxCamp
Скорее всего, вы каждый день запускаете команды, которые зависят от выполнения предыдущих.
Механизм списков позволяет не только разом выполнить группу команд, но и выстроить условную последовательность через операторы "&& (И)" и "|| (ИЛИ)".
Условные списки
Предположим, вы хотите создать файл new.txt в каталоге dir. Типичная последовательность может быть следующей:
$ cd dir
$ touch new.txt
Запуск второй команды зависит от успешного выполнения первой. Если каталог не существует, то нет смысла и команду запускать.
Оболочка позволяет сделать эту зависимость явной, объединив команды оператором && (И):
$ cd dir && touch new.txt
Если вы используете систему контроля версий Git, то, вероятно, знакомы со следующей последовательностью команд фиксации и внесения изменений:
$ git add . && git commit -m"fixed a bug" && git push
Если какая-либо из команд завершится ошибкой, остальные вы не запустите. Поэтому эта цепочка хорошо работает в формате условного списка.
Так же, как оператор && запускает вторую команду только в случае успеха первой, оператор || (ИЛИ) запускает вторую команду только в случае сбоя первой.
Например, следующая команда пытается войти в каталог и, если это не удается, создает его:
$ cd dir || mkdir dir
Вы часто будете видеть этот оператор в сценариях оболочки. В частности, для выхода в случае возникновения ошибки:
cd dir || exit 1
Операторы && и || можно использовать вместе, чтобы настраивать более сложные действия:
$ cd dir || mkdir dir && cd dir || echo "I failed"
Последовательность предполагает попытку войти в каталог dir, если это не удается, создать каталог и войти в него, а если и это не удается, вывести ошибку.
Безусловные списки
Команды в списке не обязательно должны зависеть друг от друга. Если вы разделяете команды точкой с запятой, они просто выполняются по порядку.
Вот пример команды, которая спит течение двух часов, а затем создает резервную копию важных файлов:
$ sleep 7200; cp -a ~/files /mnt/backup_drive
Безусловные списки дают те же результаты, что и ввод команд по отдельности.
Единственное существенное различие - коды возврата. В таком списке коды отдельных команд отбрасываются, кроме последней.
Только код возврата последней команды в списке присваивается переменной оболочки "?":
$ mv file1 file2; mv file2 file3; mv file3 file4
# Код возврата команды «mv file3 file4»
$ echo $?
0
LinuxCamp
👍40🔥10❤🔥7❤1