LinuxCamp | DevOps – Telegram
LinuxCamp | DevOps
14.2K subscribers
194 photos
7 videos
299 links
Обо мне: C/C++/Linux эксперт. Говорим про разработку, Linux, DevOps, сети и администрирование.

Админ (реклама): @XoDefender
Чат: @linuxcamp_chat

Менеджер: @Spiral_Yuri
Биржа: https://telega.in/c/linuxcamp_tg

№ 6327102672
Download Telegram
Понимание типов оболочек: interactive, non-interactive, login, non-login

Да-да, с оболочками есть еще и такой прикол. Многих людей сбивает с толку процесс настройки оболочек именно по тому, что они не в курсе о типовых различиях. Сегодня я постараюсь дать вам базовое понимание каждого из типов, чтобы потом легче было разобраться с конфигами.

Оболочки "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/":


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🔥721
Сигналы в Linux: что это и зачем нужно?

Предположим, что есть две программы. Одна программа должна передать другой большое количество данных - через файл.

Работает эта история таким образом, что одна программа открывает файл (на запись) и передает данные, другая открывает его же (на чтение) и получает инфу.

Проблема заключается в том, что вторая программа должна как-то узнать о том, что пришло время считывать данные.

Как реализовать процесс уведомления? Ну, можно через сигналы)

Сигнал — это механизм оповещения процесса о том, что произошло некое событие.

Иногда они описываются, как программные прерывания, т.к. приостанавливают нормальное выполнение программы.

Один процесс может отправить сигнал другому с помощью системного вызова 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❤‍🔥61🍌1
Перехват и обработка сигналов

Обработчик сигнала — функция, написанная программистом и выполняющая нужные действия при получении сигнала.

Например, для оболочки может быть определен обработчик сигнала 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🔥102❤‍🔥2
👋 Приветственный пост

Рад всех видеть на моем канале! Го знакомиться, меня зовут Кирилл. Я - действующий программист, занимаюсь прикладной разработкой системного софта под Линукс.

Моя задача - в увлекательном формате делиться опытом и погружать вас в мир системной разработки и администрирования!

Разнообразие материала позволит найти тут свое место как абсолютным новичкам, так и до дыр обученным профессионалам. Будет интересно, полезно и, главное, по делу. Добро пожаловать в наше сообщество!

📱 ITCamp
🔈 LinuxCamp | Chat

Навигация по контенту

План по разделяемым библиотекам
Итоги квартала (август 2024)
Итоги квартала (ноябрь 2024)
Итоги квартала (февраль 2025)
Итоги квартала (август 2025)

LinuxCamp | #info
Please open Telegram to view this post
VIEW IN TELEGRAM
👍5142🔥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
👍50🔥1613❤‍🔥6
Отправка сигналов: kill()

Один процесс может отправить сигнал другому с помощью системного вызова 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❤‍🔥43
Отправка сигнала самому себе: raise()

Иногда полезной практикой является отправка процессом сигнала самому себе. Эту задачу выполняет функция стандартной библиотеки (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❤‍🔥64🔥2🤣21
Защита от незваных гостей: сигнальная маска

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

Если в процесс отправляется заблокированный сигнал, то доставка этого сигнала откладывается до тех пор, пока он не будет разблокирован путем удаления из сигнальной маски.

Как же нам определить эту чудо-маску и замутить пару сигналов?) Использовать системный вызов 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❤‍🔥521
Работа с ожидающими сигналами

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

Если процесс получает сигнал, который в данный момент подлежит блокированию - входит в сигнальную маску, то он добавляется в набор ожидающих сигналов. Когда сигнал будет разблокирован, он 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() сложнее в использовании, взамен он предоставляет большую гибкость:


#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(), напротив, записывает в него все сигналы:


#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):


$ 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❤‍🔥32🍌1
Операторы перенаправления ввода-вывода [1]

Недавно мы вот узнали, через какие потоки процесс пишет и читает данные. Теперь пора глянуть на операторы оболочки, которые позволяют перенаправлять потоки на разные источники данных. Например, вывод одной команды подать на вход другой либо записать 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 принимает данные от пользователя (через клавиатуру), чтобы выполнить преобразование всех символов нижнего регистра в верхний:


$ 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❤‍🔥31
Операторы перенаправления ввода-вывода [3]

Я же уверен, что 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
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 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. Типичная последовательность может быть следующей:


$ 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❤‍🔥71