Linux進程信號的用法及說明
信號產生
生活中的信號類比(交通信號燈、警報),當產生這些信號時,我們會立馬想到對應的動作。
在Linux中,信號是事件發(fā)生對進程的通知機制亦稱軟件中斷,由操作系統(tǒng)內核、進程本身或者其他進程向目標進程異步事件發(fā)送機制(即收到某種信號,并不會立馬去執(zhí)行)。通知進程發(fā)生某種預定義的時間,要求進程作出相應響應。
信號與硬件中斷相似之處在于會打斷程序執(zhí)行流程。
常見信號
硬件異常:
- 如內存越界(
SIGSEGV
) - 除零錯誤(
SIGFPE
) - 由內核自動生成
系統(tǒng)調用:
kill()
:向指定進程發(fā)送信號raise()
:進程向自身發(fā)送信號alarm()
:設置定時器,超時后發(fā)送SIGALRM
終端輸入:
Ctrl+C
:SIGINT
(終止進程)Ctrl+Z
:SIGTSTP
(暫停進程)Ctrl+\
:
軟件條件:
- 子進程退出時,父進程收到
SIGCHLD
- 定時器到期(如
alarm()
)觸發(fā)信號
核心轉儲 :
信號的分類
信號分為兩大類。
(編號1-31)為傳統(tǒng)信號信號,內核向進程通知且遞送一次,(編號34-64)為實時信號使信號按序遞送。
信號處理方式
- 默認動作: 部分是終止自己,暫停等
- 忽略動作: 是一種信號處理的方式,只不過動作就是什么都不干
- 自定義動作: 使用signal方法修改信號的處理動作。(即用戶程序員編寫的函數:將默認動作轉化為自定義動作)
改變信號處理方式
signal,sigaction
信號阻塞
概念悉知
- 實際執(zhí)行信號的處理動作稱為信號遞達(Delivery)
- 信號從產生到遞達之間的狀態(tài),稱為信號未決(Pending)。
- 進程可以選擇阻塞 (Block )某個信號。
- 被阻塞的信號產生時將保持在未決狀態(tài),直到進程解除對此信號的阻塞,才執(zhí)行遞達的動作.
- 阻塞和忽略是不同的,只要信號被阻塞就不會遞達,而忽略是在遞達之后可選的一種處理動作。
信號產生后會稍后遞達給某進程,信號在產生和到達期間會一直處于pending等待狀態(tài)。
信號在內核中的表示
每個進程都有一個信號屏蔽字亦稱阻塞信號集,一個 sigset_t
類型的位圖,每一位對應一個信號(如 SIGINT
、SIGQUIT
等)。用來標記哪些信號當前被阻塞。被阻塞的信號若已產生,會進入未決狀態(tài),直到阻塞被解除才會觸發(fā)處理。位為1生效,0不生效。
block與pending之間聯(lián)系
pending集是信號產生后的暫存區(qū),block是控制信號是否能從pending暫存區(qū)遞交給進程的開關
- block:決定哪些信號會被阻塞,近而將其放入未決信號集
- pending:記錄已產生但未被處理的信號(因被阻塞或正在處理其他信號)
- 通過
sigpending()
査詢掛起的信號集,通過sigprocmask()
控制阻塞狀態(tài)。
其中block
位圖用于表示進程是否阻塞。
pending
位圖用于是否有信號寫入(信號是否產生,信號產生時,內核在進程控制塊中設置該信號的pending
位圖,直到信號抵達才消失)。handler
函數指針數組用于進程執(zhí)行何種動作(其中存放的是動作函數指針,每個信號的編號就是其數組下標)。
關鍵系統(tǒng)調用
信號集操作函數
sigset_t 本質上是一個位圖,每一位代表一個信號。當某一位被設置為 1 時,表示對應的信號被包含在信號集中;為 0 則表示不包含。使用者只能調用以下函數來操作sigset_t
變量,不用關注內部數據。
在使用sigset_t
類型的變量之前,一定要調用sigemptyset()
函數初始化一個未包含任何成員的信號集或者sigfillset()
函數則初始化一個信號集,使其包含所有信號(包括所有實時信號)。
#include <signal.h> // 清空信號集,置0 int sigemptyset(sigset_t *set); // 填充所有信號, 置有效狀態(tài)1? int sigfillset(sigset_t *set);
信號集初始化后,可以分別使用 sigaddset()
和sigdelset()
函數向一個集合中添加或者移除單個信號。使用sigismember()
測試信號是否是信號集set
的成員。
#include <signal.h> // 添加單個信號 int sigaddset(sigset_t *set, int signo); // 刪除單個信號 int sigdelset(sigset_t *set, int signo); // 判斷信號是否存在 int sigismember(const sigset_t *set, int signo);
信號掩碼(阻塞信號傳遞)
內核會為每個進程維護一個信號掩碼(一組信號),阻塞其針對該進程的傳遞。如果將被阻塞的信號發(fā)送給某進程,那么對該信號的傳遞將延后,直至從進程信號掩碼中移除該信號,從而解除阻塞為止。阻塞信號集內0
變1
只能由其觸發(fā)。
#include <signal.h> int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
參數 :
how
:指定了sigprocmask()函數想給信號掩碼帶來的變化。
SIG_BLOCK
:set包含了我們希望添加到當前信號屏蔽字的信號(邏輯或)SIG_UNBLOCK
:set包含了我們希望從當前信號屏蔽字中解除阻塞的信號(與非)SIG_SETMASK
:設置當前信號屏蔽字為set所指向的值(直接賦值)
set
- 要操作的信號集,(為NULL則忽略)
oldset
- 保存舊的信號掩碼(可為NULL)
返回值:成功返回 0,失敗返回 -1(設置 errno
)
- 阻塞:信號暫存到未決信號集直到解除阻塞
- 忽略:直接丟棄
操作示例:
void handler(int no){ cout << "execcute user-defined actions" << " "; } int main(){ //更改2號信號執(zhí)行動作 signal(2, handler); //創(chuàng)建兩個信號集 sigset_t set, old_set; //清空信號集 sigemptyset(&set); sigaddset(&set, SIGINT);//將2號信號添加進set集01000...000 //將set信號集中指定信號添加到當前阻塞信號集中,同時將原來的信號集保存到old_set中 sigprocmask(SIG_BLOCK, &set, &old_set);//阻塞set中包含的信號集 cout << "SIG_INT is blocked, ctrl+c has failed" << endl; sleep(5);//5秒內ctrl+c不會有反應 //使用old_set替換當前所有阻塞信號集 sigprocmask(SIG_SETMASK, &old_set, NULL);//恢復之前阻塞信號集 cout << "SIG_INT has been resloved" << endl; sleep(5); return 0; }
阻塞期間發(fā)送2號信號?
信號不會立馬處理,而是將該信號加入pending信號集
(即pending位圖
的SIG_INT
位從0-->1
)
信號阻塞接觸后?
pending信號集
內該信號立即處理(處理方式鑒于默認動作,忽略(丟棄) 或 自定義動作),該位由1-->0
。
sigpending獲取未決信號集
如果某進程接受了一個該進程正在阻塞的信號,那么會將該信號填加到進程的等待信號集中。當之后解除了對該信號的鎖定時,會隨之將信號傳遞給此進程。
#include <signal.h> int sigpending(sigset_t *set);
系統(tǒng)調用返回后,用戶空間的set變量
包含了當前的未決信號集,可以使用sigismember()
檢查特定信號是否在未決信號集中。
作用: 返回處于等待狀態(tài)的信號集,并將其置于 set
指向的sigset_t
結構中。
操作 | 是否進入未決? | 后續(xù)動作 |
---|---|---|
信號被阻塞,且期間被發(fā)送 | 是 | 該信號記錄在未決信號集中,直到阻塞解除后被處理 |
信號被阻塞,期間未被發(fā)送 | 否 | 不記錄該信號,解除阻塞后若收到信號則直接處理 |
信號未被阻塞,但被發(fā)送 | 否 | 信號直接調用對應函數即可 |
signal() - 設置信號處理
#include <signal.h> void (*signal(int sig, void (*func)(int)))(int);
返回函數指針的函數:
代碼示例:
void handler(int signo){ cout << "execcute user-defined actions" << " "; } ... //更改2號信號執(zhí)行動作 signal(2, handler);
聲明該函數類型是一個函數指針類型,signal
是函數名(如:int (*p)(int,int)
,p
是函數名,有兩個int
類型參數,返回值是一個int
類型)。signal
返回的是一個函數指針,該函數指向的函數參數是一個int類型,返回值是void
。所以外層是指明這是一個函數指針類型。里面才是使用的函數本體。
signal
是一個函數- 它接受一個整數和一個函數指針作為參數
- 它返回一個與第二個參數類型相同的函數指針
返回函數指針的數組: 對?
sigaction() - 設置信號處理
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
signum
: 目標信號
act
新的信號處理方式
sa_handler
:信號處理函數指針(或SIG_IGN
、SIG_DFL
)。sa_mask
:處理該信號時額外阻塞的信號集。sa_flags
:控制選項(如SA_RESTART
、SA_NODEFER
)NULL
oldact
- 保存舊的處理配置(可為
NULL
)
返回值:成功返回 0,失敗返回 -1(設置 errno
)
struct sigaction { void (*sa_handler)(int); // 簡單處理函數 void (*sa_sigaction)(int, siginfo_t *, void *); // 高級處理函數 sigset_t sa_mask; // 處理期間屏蔽的信號集 int sa_flags; // 控制標志 void (*sa_restorer)(void); // 內部使用(已廢棄) };
現(xiàn)在我們使用sigprocmask
函數和sigpending
函數完成一個函數。思路是
- 對2號信號寫入信號集
- 將該信號集使用
sigprocmask
函數將2號
信號block住 - 我們鍵盤輸入
ctrl+c
,產生2號
信號,發(fā)送至該進程。 - 使用
sigpending
函數輸出pending位圖,因為我們將2號
進程block
住了,該信號并不能抵達。我們再向其發(fā)送2號
信號,信號被block住了并不會影響信號的寫入。因此我們再發(fā)送2號
進程之前pending
位圖應該全為0
,發(fā)送2號
進程之后會看見pending
位圖發(fā)送由0
至1
的變化。
執(zhí)行流程:
- 前 5 秒內,
SIGINT
(Ctrl+C)被阻塞,信號進入未決狀態(tài)但不觸發(fā)處理。 - 5 秒后解除阻塞,若之前有未決的
SIGINT
,會立即觸發(fā)handle
函數。
#include <iostream> #include <cassert> #include <unistd.h> #include <signal.h> using namespace std; static void handler(int signo){ cout << signo << " 號信號確實遞達了" << endl; //最終不退出進程 } void DisplayPending(const sigset_t pending){ // 打印 pending 表 int i = 1; while (i < 32) { if (sigismember(&pending, i)) cout << "1"; else cout << "0"; i++; } cout << endl; } int main(){ // 更改 2 號信號的執(zhí)行動作 signal(2, handler); // 創(chuàng)建信號集 sigset_t set, oset; // 信號集清空 0 sigemptyset(&set); sigemptyset(&oset); sigaddset(&set, 2); //將2號信號寫入set // 設置當前進程的屏蔽信號集 sigprocmask(SIG_BLOCK, &set, &oset);//0給進程 // 死循環(huán) int n = 0; while (true){ if (n == 5){ // 采用 SIG_SETMASK 的方式,覆蓋進程的 block 表 sigprocmask(SIG_SETMASK, &oset, NULL); // 不接收進程的 block 表 } // 獲取進程的 未決信號集 sigset_t pending; sigemptyset(&pending); int ret = sigpending(&pending); assert(ret == 0); (void)ret; // 避免 release 模式中出錯 DisplayPending(pending); n++; sleep(1); } return 0; }
信號處理
進程地址空間
為了進程可以通過訪問虛擬地址來間接訪問物理資源,我們需要建立虛擬地址空間與物理內存的映射關系。用戶區(qū)和內核區(qū)都需要通過內核級頁表或者用戶級頁表進行映射使用。
系統(tǒng)調用:
系統(tǒng)調用是受控的內核入口,借助于這一機制,進程可以請求內核以自己的名義去執(zhí)行某些動作。以應用程序編程接口(API
)的形式,內核提供有一系列服務供程序訪問。
信號處理過程
用戶態(tài)的時候執(zhí)行用戶代碼,在中斷/異常/系統(tǒng)調用的時候切換到內核態(tài),査看進程的三張表。(信號捕捉過程)當需要執(zhí)行自定義動作的時候,切回用戶態(tài)執(zhí)行自定義動作,自定動作完成之后,再切到內核態(tài)執(zhí)行sys_sigretur()
函數,最后再切回用戶態(tài)中用戶代碼的下一行繼續(xù)執(zhí)行。
當執(zhí)行默認動作或者忽略動作的時候,直接可以在內核中完成,完成之后切回用戶態(tài)中的用戶代碼的下一行,繼續(xù)執(zhí)行。
- sigaction()
- volatile關鍵字
- SIGCHLD
總結
以上為個人經驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關文章
CentOS 6.5 環(huán)境實現(xiàn)本地局域網搭建YUM的方法【基于FTP】
這篇文章主要介紹了CentOS 6.5 環(huán)境實現(xiàn)本地局域網搭建YUM的方法,結合實例形式分析了CentOS基于FTP本地局域網搭建YUM的具體步驟、相關命令與操作技巧,需要的朋友可以參考下2018-04-04詳解CentOS7下PostgreSQL 11的安裝和配置教程
這篇文章主要介紹了CentOS7下PostgreSQL 11的安裝和配置教程,本文給大家介紹的非常詳細,具有一定的參考借鑒價值,需要的朋友可以參考下2019-10-10