Загалом, щоб убити процеси ми генеруємо сигнали , такі як SIGKILL
, і SIGTSTP
т.д.
Але як відомо, хто замовив той чи інший сигнал, хто надіслав його до певного процесу, і взагалі як сигнали виконують свої операції? Як сигнали працюють внутрішньо?
Загалом, щоб убити процеси ми генеруємо сигнали , такі як SIGKILL
, і SIGTSTP
т.д.
Але як відомо, хто замовив той чи інший сигнал, хто надіслав його до певного процесу, і взагалі як сигнали виконують свої операції? Як сигнали працюють внутрішньо?
Відповіді:
Погляд на 50 000 футів:
Сигнал генерується або ядром внутрішньо (наприклад, SIGSEGV
коли доступ до недійсної адреси, або SIGQUIT
при натисканні Ctrl+ \), або програмою, що використовує kill
syscall (або декілька пов'язаних з ними).
Якщо це за допомогою одного з системних викликів, то ядро підтверджує, що процес виклику має достатньо привілеїв для передачі сигналу. Якщо ні, помилка повертається (і сигнал не відбувається).
Якщо це один із двох спеціальних сигналів, ядро беззастережно діє на нього, без будь-якого введення цільового процесу. Два спеціальних сигналу - SIGKILL і SIGSTOP. Усі наведені нижче матеріали про дії за замовчуванням, блокування сигналів тощо не мають значення для цих двох.
Далі ядро з'ясовує, що робити із сигналом:
Для кожного процесу існує дія, пов'язана з кожним сигналом. Існує купа за замовчуванням, і програми можуть встановлювати різні, використовуючи sigaction
, signal
і т. Д. До них відносяться такі речі, як "ігнорувати його повністю", "вбивати процес", "вбивати процес за допомогою дамп-ядра", "зупиняти процес", тощо.
Програми також можуть відключити доставку сигналів ("заблокованих") на основі сигналу за сигналом. Потім сигнал залишається в очікуванні до розблокування.
Програми можуть вимагати, щоб замість того, щоб ядро здійснювало певні дії, воно надсилало сигнал процесу або синхронно (з sigwait
, і т. Ін., Або signalfd
), або асинхронно (перериваючи все, що робить процес, і викликаючи вказану функцію).
Існує другий набір сигналів під назвою "сигнали в режимі реального часу", які не мають конкретного значення, а також дозволяють встановити в чергу кілька сигналів (звичайні сигнали в черзі лише один з кожного, коли сигнал заблокований). Вони використовуються в багатопотокових програмах для передачі потоків між собою. Наприклад, декілька використовуються в реалізації потоків POSIX потоків glibc. Вони також можуть використовуватися для зв'язку між різними процесами (наприклад, ви можете використовувати декілька сигналів у реальному часі, щоб програма fooctl надсилала повідомлення демону foo).
Щоб переглянути ногу не на 50 000, спробуйте man 7 signal
також документацію (або джерело) для внутрішнього ядра.
Реалізація сигналу дуже складна і специфічна для ядра. Іншими словами, різні ядра будуть по-різному реалізовувати сигнали. Спрощене пояснення полягає в наступному:
ЦП, заснований на спеціальному значенні реєстру, має адресу в пам'яті, де він очікує знайти "таблицю дескрипторів переривання", яка є фактично векторною таблицею. Існує один вектор для кожного можливого винятку, як поділ на нуль або пастка, як INT 3 (налагодження). Коли CPU стикається з винятком, він зберігає прапорці та поточний вказівник інструкції на стеку, а потім переходить на адресу, вказану відповідним вектором. У Linux цей вектор завжди вказує на ядро, де є обробник винятків. Зараз процесор готовий, а ядро Linux бере на себе.
Зауважте, що ви також можете викликати виняток із програмного забезпечення. Наприклад, користувач натискає CTRL- C, тоді цей виклик переходить до ядра, яке викликає власний обробник винятків. Як правило, існують різні способи дістатися до обробника, але незалежно від того самого основного, що відбувається: контекст зберігається в стеці, і до нього обробляється обробник виключення ядра.
Потім обробник винятків вирішує, який потік повинен приймати сигнал. Якщо трапилось щось на зразок поділу на нуль, то це легко: потік, який спричинив виняток, отримує сигнал, але для інших типів сигналів рішення може бути дуже складним і в деяких незвичайних випадках може мати більш-менш випадковий потік отримати сигнал.
Для передачі сигналу, що робить ядро, спочатку встановлюється значення, що вказує тип сигналу, SIGHUP
або будь-який інший. Це просто ціле число. Кожен процес має область пам'яті "очікуючий сигнал", де зберігається це значення. Потім ядро створює структуру даних з інформацією про сигнал. Ця структура включає сигнал "диспозиція", який може бути за замовчуванням, ігнорувати або обробляти. Потім ядро викликає власну функцію do_signal()
. Починається наступна фаза.
do_signal()
спочатку вирішує, чи буде він обробляти сигнал. Наприклад, якщо це вбивство , то do_signal()
просто вбиває процес, кінець історії. Інакше це дивиться на диспозицію. Якщо диспозиція за замовчуванням, то do_signal()
обробляє сигнал відповідно до політики за замовчуванням, яка залежить від сигналу. Якщо диспозиція є ручкою, то це означає, що в програмі користувача є функція, призначена для обробки відповідного сигналу, і покажчик на цю функцію буде знаходитись у вищезгаданій структурі даних. У цьому випадку do_signal () викликає іншу функцію ядра,handle_signal()
, який потім проходить процес переходу в режим користувача та виклику цієї функції. Деталі цієї передачі надзвичайно складні. Цей код у вашій програмі зазвичай автоматично підключається до вашої програми під час використання функцій в signal.h
.
Вивчивши відповідне значення сигналу, що очікує, ядро може визначити, чи обробляє процес всі сигнали, і вживе відповідних дій, якщо його немає, що може перевести процес сну або вбити його чи іншу дію, залежно від сигналу.
Хоча на це питання відповіли, дозвольте мені опублікувати детальний потік подій у ядрі Linux.
Це повністю скопійовано з постів Linux: Сигнали Linux - Внутрішні
записи в блозі "Пост Linux" на сайті sklinuxblog.blogspot.in.
Почнемо з написання простої програми програмного забезпечення для простору користувача C:
#include<signal.h>
#include<stdio.h>
/* Handler function */
void handler(int sig) {
printf("Receive signal: %u\n", sig);
};
int main(void) {
struct sigaction sig_a;
/* Initialize the signal handler structure */
sig_a.sa_handler = handler;
sigemptyset(&sig_a.sa_mask);
sig_a.sa_flags = 0;
/* Assign a new handler function to the SIGINT signal */
sigaction(SIGINT, &sig_a, NULL);
/* Block and wait until a signal arrives */
while (1) {
sigsuspend(&sig_a.sa_mask);
printf("loop\n");
}
return 0;
};
Цей код призначає новий обробник сигналу SIGINT. SIGINT може бути відправлений до запущеного процесу, використовуючи комбінацію клавіш Ctrl+ C. Коли Ctrl+ Cнатискається, асинхронний сигнал SIGINT надсилається завдання. Це також еквівалентно відправленню kill -INT <pid>
команди в інший термінал.
Якщо ви робите kill -l
(це малий регістр L
, який означає "список"), ви дізнаєтесь про різні сигнали, які можна надіслати до запущеного процесу.
[root@linux ~]# kill -l
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR
31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3
38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7
58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
63) SIGRTMAX-1 64) SIGRTMAX
Також для передачі певних сигналів може використовуватися наступна комбінація клавіш:
Якщо ви компілюєте та запускаєте вищевказану програму C, ви отримаєте такий вихід:
[root@linux signal]# ./a.out
Receive signal: 2
loop
Receive signal: 2
loop
^CReceive signal: 2
loop
Навіть з Ctrl+ Cабо kill -2 <pid>
процес не припиниться. Замість цього він виконає обробник сигналу та повернеться.
Якщо ми побачимо внутрішню передачу сигналу в процес і поставимо Jprobe з dump_stack у __send_signal
функцію, ми побачимо наступний слід виклику:
May 5 16:18:37 linux kernel: dump_stack+0x19/0x1b
May 5 16:18:37 linux kernel: my_handler+0x29/0x30 (probe)
May 5 16:18:37 linux kernel: complete_signal+0x205/0x250
May 5 16:18:37 linux kernel: __send_signal+0x194/0x4b0
May 5 16:18:37 linux kernel: send_signal+0x3e/0x80
May 5 16:18:37 linux kernel: do_send_sig_info+0x52/0xa0
May 5 16:18:37 linux kernel: group_send_sig_info+0x46/0x50
May 5 16:18:37 linux kernel: __kill_pgrp_info+0x4d/0x80
May 5 16:18:37 linux kernel: kill_pgrp+0x35/0x50
May 5 16:18:37 linux kernel: n_tty_receive_char+0x42b/0xe30
May 5 16:18:37 linux kernel: ? ftrace_ops_list_func+0x106/0x120
May 5 16:18:37 linux kernel: n_tty_receive_buf+0x1ac/0x470
May 5 16:18:37 linux kernel: flush_to_ldisc+0x109/0x160
May 5 16:18:37 linux kernel: process_one_work+0x17b/0x460
May 5 16:18:37 linux kernel: worker_thread+0x11b/0x400
May 5 16:18:37 linux kernel: rescuer_thread+0x400/0x400
May 5 16:18:37 linux kernel: kthread+0xcf/0xe0
May 5 16:18:37 linux kernel: kthread_create_on_node+0x140/0x140
May 5 16:18:37 linux kernel: ret_from_fork+0x7c/0xb0
May 5 16:18:37 linux kernel: ? kthread_create_on_node+0x140/0x140
Отже, основні виклики функції передачі сигналу виглядають так:
First shell send the Ctrl+C signal using n_tty_receive_char
n_tty_receive_char()
isig()
kill_pgrp()
__kill_pgrp_info()
group_send_sig_info() -- for each PID in group call this function
do_send_sig_info()
send_signal()
__send_signal() -- allocates a signal structure and add to task pending signals
complete_signal()
signal_wake_up()
signal_wake_up_state() -- sets TIF_SIGPENDING in the task_struct flags. Then it wake up the thread to which signal was delivered.
Тепер все налаштовано і вноситься необхідні зміни до task_struct
процесу.
Сигнал перевіряється / обробляється процесом, коли він повертається з системного виклику або якщо відбувається повернення з переривання. Повернення з системного виклику присутнє у файлі entry_64.S
.
Функція int_signal викликається функцією, з entry_64.S
якої викликає функцію do_notify_resume()
.
Перевіримо функцію do_notify_resume()
. Ця функція перевіряє, чи TIF_SIGPENDING
встановлений у нас прапор у task_struct
:
/* deal with pending signal delivery */
if (thread_info_flags & _TIF_SIGPENDING)
do_signal(regs);
do_signal calls handle_signal to call the signal specific handler
Signals are actually run in user mode in function:
__setup_rt_frame -- this sets up the instruction pointer to handler: regs->ip = (unsigned long) ksig->ka.sa.sa_handler;
"Повільні" системні виклики, наприклад, блокування читання / запису, переведення процесів у стан очікування:
TASK_INTERRUPTIBLE
або TASK_UNINTERRUPTIBLE
.
Завдання в стані TASK_INTERRUPTIBLE
буде змінено на TASK_RUNNING
стан сигналом. TASK_RUNNING
означає, що процес може бути запланований.
Якщо він виконаний, його обробник сигналу буде запущений до завершення "повільного" системного виклику. Значення syscall
за замовчуванням не завершується.
Якщо SA_RESTART
прапор встановлено, syscall
після закінчення обробника сигналу перезапускається.
kill
команду, яка є вбудованою оболонкою). (2c) Хоча крапки з комою після закриття }
функції не є, суворо кажучи, помилками, вони є непотрібними і вкрай неортодоксальними. (3) Навіть якби все було правильно, це було б не дуже вдалою відповіддю на питання. (3a) Це питання, хоча дещо незрозуміле, фокусується на тому, як суб'єкти (користувачі та процес) ініціюють (тобто надсилають ) сигнали. … (Продовжував)