信號的使用及原理
信號實(shí)質(zhì)上是一種軟中斷既们,既然是一種中斷居扒,就說明信號是異步的占调,信號接收函數(shù)不需要一直阻塞等待信號的到達(dá)暂题。
當(dāng)信號發(fā)出后,如果有地方注冊了這個(gè)信號究珊,就會執(zhí)行響應(yīng)函數(shù)薪者,如果沒有地方注冊這個(gè)信號,該信號就會被忽略剿涮。
#include <signal.h>
sighandler_t signal(int signum, sighandler_t handler); //信號注冊函數(shù)
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact); //信號注冊函數(shù)
struct sigaction {
void (*sa_handler)(int); //信號處理程序言津,不接受額外數(shù)據(jù),SIG_IGN 為忽略取试,SIG_DFL 為默認(rèn)動作
void (*sa_sigaction)(int, siginfo_t *, void *); //信號處理程序悬槽,能夠接受額外數(shù)據(jù)和sigqueue配合使用
sigset_t sa_mask;//阻塞關(guān)鍵字的信號集,可以再調(diào)用捕捉函數(shù)之前瞬浓,把信號添加到信號阻塞字初婆,信號捕捉函數(shù)返回之前恢復(fù)為原先的值。
int sa_flags;//影響信號的行為SA_SIGINFO表示能夠接受數(shù)據(jù)
};
?
int kill(pid_t pid, int sig); //信號發(fā)送函數(shù)
int sigqueue(pid_t pid, int sig, const union sigval value); //信號發(fā)送函數(shù)
//……
注冊信號有兩個(gè)方法
signal()函數(shù):signal不支持傳遞信息猿棉,signum入?yún)樾盘柫堪跖眩琱andler入?yún)樾盘柼幚砗瘮?shù)
sigaction()函數(shù):sigaction支持傳遞信息,信息放在sigaction數(shù)據(jù)結(jié)構(gòu)中
信號發(fā)送
信號發(fā)送函數(shù)比較多萨赁,這里我列舉一下弊琴。
- kill():用于向進(jìn)程或進(jìn)程組發(fā)送信號;
- sigqueue():只能向一個(gè)進(jìn)程發(fā)送信號杖爽,不能向進(jìn)程組發(fā)送信號敲董;
- alarm():用于調(diào)用進(jìn)程指定時(shí)間后發(fā)出SIGALARM信號紫皇;
- setitimer():設(shè)置定時(shí)器,計(jì)時(shí)達(dá)到后給進(jìn)程發(fā)送SIGALRM信號臣缀,功能比alarm更強(qiáng)大坝橡;
- abort():向進(jìn)程發(fā)送SIGABORT信號,默認(rèn)進(jìn)程會異常退出精置。
- raise():用于向進(jìn)程自身發(fā)送信號计寇;
通過kill -l指令可以查看Android手機(jī)支持的信號,從下圖可以看到脂倦,總共有64個(gè)番宁,前31個(gè)信號是普通信號,后33個(gè)信號是實(shí)時(shí)信號赖阻,實(shí)時(shí)信號支持隊(duì)列蝶押,可以保證信號不會丟失。
前幾個(gè)信號的作用
1 | SIGHUP | 掛起 |
---|---|---|
2 | SIGINT | 中斷 |
3 | SIGQUIT | 中斷 |
3 | SIGQUIT | 退出 |
4 | SIGILL | 非法指令 |
5 | SIGTRAP 斷點(diǎn)或陷阱指令 | |
6 | SIGABRT | abort發(fā)出的信號 |
7 | SIGBUS | 非法內(nèi)存訪問 |
8 | SIGFPE | 浮點(diǎn)異常 |
9 | SIGKILL | 殺進(jìn)程信息 |
當(dāng)我們調(diào)用信號發(fā)送函數(shù)后火欧,信號是怎么傳到注冊的方法調(diào)用中去的呢棋电?這里以kill()這個(gè)信號發(fā)送函數(shù)講解一下這個(gè)流程。
kill()函數(shù)會經(jīng)過系統(tǒng)調(diào)用方法sys_tkill()進(jìn)入內(nèi)核苇侵,sys_tkill是SYSCALL_DEFINE2這個(gè)方法來實(shí)現(xiàn)赶盔,這個(gè)方法的實(shí)現(xiàn)是一個(gè)宏定義。
我會從這個(gè)方法一路往底追蹤榆浓,這里我會忽略細(xì)節(jié)實(shí)現(xiàn)于未,只看關(guān)鍵部分的代碼。
//文件-->syscalls.h
asmlinkage long sys_kill(int pid, int sig);
?
//文件-->kernel/signal.c
SYSCALL_DEFINE2(kill, pid_t, pid, int, sig)
{
struct kernel_siginfo info;
?
clear_siginfo(&info);
info.si_signo = sig;
info.si_errno = 0;
info.si_code = SI_USER;
info.si_pid = task_tgid_vnr(current);
info.si_uid = from_kuid_munged(current_user_ns(), current_uid());
?
return kill_something_info(sig, &info, pid);
}
?
static int kill_something_info(int sig, struct kernel_siginfo *info, pid_t pid)
{
int ret;
?
if (pid > 0) {
ret = kill_pid_info(sig, info, find_vpid(pid));
return ret;
}
……
}
?
int kill_pid_info(int sig, struct kernel_siginfo *info, struct pid *pid)
{
……
for (;;) {
error = group_send_sig_info(sig, info, p, PIDTYPE_TGID);
……
}
}
?
int group_send_sig_info(int sig, struct kernel_siginfo *info,
struct task_struct *p, enum pid_type type)
{
……
ret = do_send_sig_info(sig, info, p, type);
return ret;
}
?
int do_send_sig_info(int sig, struct kernel_siginfo *info, struct task_struct *p,
enum pid_type type)
{
……
ret = send_signal(sig, info, p, type);
return ret;
}
?
static int send_signal(int sig, struct kernel_siginfo *info, struct task_struct *t,
enum pid_type type)
{
return __send_signal(sig, info, t, type, from_ancestor_ns);
}
我們從sys_kill函數(shù)一路追蹤陡鹃,最終調(diào)用了__send_signal函數(shù),我們接著看這個(gè)函數(shù)的實(shí)現(xiàn)烘浦。
//文件-->kernel/signal.c
static int __send_signal(int sig, struct kernel_siginfo *info, struct task_struct *t,
enum pid_type type, int from_ancestor_ns)
{
……
out_set:
signalfd_notify(t, sig); //將信號發(fā)送給監(jiān)聽的fd
sigaddset(&pending->signal, sig);
complete_signal(sig, t, type); //完成信號發(fā)送
ret:
trace_signal_generate(sig, info, t, type != PIDTYPE_PID, result);
return ret;
}
?
static void complete_signal(int sig, struct task_struct *p, enum pid_type type)
{
struct signal_struct *signal = p->signal;
struct task_struct *t;
?
//尋找處理信號的線程
if (wants_signal(sig, p))
t = p;
else if ((type == PIDTYPE_PID) || thread_group_empty(p))
return;
else {
t = signal->curr_target;
while (!wants_signal(sig, t)) {
t = next_thread(t);
if (t == signal->curr_target)
return;
}
signal->curr_target = t;
}
?
//如果是SIGKILL信號,則殺掉線程組
if (sig_fatal(p, sig) &&
!(signal->flags & SIGNAL_GROUP_EXIT) &&
!sigismember(&t->real_blocked, sig) &&
(sig == SIGKILL || !p->ptrace)) {
/*
* This signal will be fatal to the whole group.
*/
if (!sig_kernel_coredump(sig)) {
/*
* Start a group exit and wake everybody up.
* This way we don't have other threads
* running and doing things after a slower
* thread has the fatal signal pending.
*/
signal->flags = SIGNAL_GROUP_EXIT;
signal->group_exit_code = sig;
signal->group_stop_count = 0;
t = p;
do {
task_clear_jobctl_pending(t, JOBCTL_PENDING_MASK);
sigaddset(&t->pending.signal, SIGKILL);
signal_wake_up(t, 1);
} while_each_thread(p, t);
return;
}
}
/*
* The signal is already in the shared-pending queue.
* Tell the chosen thread to wake up and dequeue it.
*/
signal_wake_up(t, sig == SIGKILL);
return;
}
可以看到萍鲸,信號最終被分發(fā)到了監(jiān)聽的fd中闷叉,交給了我們注冊的函數(shù)處理,從最后部分也可以看到脊阴,如果是SIGKILL信號片习,內(nèi)核會專門處理去殺進(jìn)程。
信號在Android中的使用場景
那么我們在來看一個(gè)Android系統(tǒng)中使用信號的場景:殺進(jìn)程蹬叭。從上面部分可以看到藕咏,SIGKILL信號是由內(nèi)核捕獲并處理的,我們看一下Android是怎么調(diào)用殺進(jìn)程的信號的吧秽五。
//文件-->Process.java
public static final void killProcess(int pid) {
sendSignal(pid, SIGNAL_KILL);
}
?
//文件-->android_util_Process.cpp
void android_os_Process_sendSignal(JNIEnv* env, jobject clazz, jint pid, jint sig) {
if (pid > 0) {
//打印Signal信息
ALOGI("Sending signal. PID: %" PRId32 " SIG: %" PRId32, pid, sig);
kill(pid, sig);
}
}
可以看到孽查,當(dāng)我們調(diào)用Process的killProcess函數(shù)殺掉某個(gè)進(jìn)程時(shí),最終會調(diào)用到native方法kill()坦喘,入?yún)ig信號量就是SIGKILL盲再,這個(gè)kill()方法西设,就是我在上面講的信號量發(fā)送函數(shù),最終內(nèi)核會響應(yīng)我們的SIGKILL答朋,殺掉進(jìn)程贷揽。
4 信號(Signal)
4.1 什么是信號?
- 信號是比較復(fù)雜的通信方式梦碗,用于通知接受進(jìn)程有某種事件發(fā)生禽绪,除了用于進(jìn)程間通信外,進(jìn)程還可以發(fā)送信號給進(jìn)程本身洪规;
- Linux除了支持Unix早期信號語義函數(shù)sigal外印屁,還支持語義服務(wù)Posix.1標(biāo)準(zhǔn)的信號函數(shù)sigaction(實(shí)際上,該函數(shù)是基于BSD的斩例,BSD為了實(shí)現(xiàn)可靠信號機(jī)制雄人,有能夠統(tǒng)一對外接口,用sigaction函數(shù)重新實(shí)現(xiàn)了signal函數(shù))
4.2 信號的種類
- 每種信號類型都有對應(yīng)的信號處理程序(也叫信號的操作)念赶,就好像每個(gè)中斷都有一個(gè)中斷服務(wù)例程一樣础钠。
- 大多數(shù)信號的默認(rèn)操作是結(jié)束接受信號的進(jìn)程;然而一個(gè)進(jìn)程通巢婷眨可以請求系統(tǒng)采取某些代替的操作
- 各種代替操作是
- 忽略信號珍坊。隨著這一選項(xiàng)的設(shè)置,進(jìn)程將忽略信號的出現(xiàn)正罢。有兩個(gè)信號不可以被忽略:SIGKILL,它將結(jié)束進(jìn)程:SIGSTOP驻民,它是作業(yè)控制機(jī)制的一部分翻具,將掛起作業(yè)的執(zhí)行。
- 恢復(fù)信號的默認(rèn)操作
- 執(zhí)行一個(gè)預(yù)先安排的信號處理函數(shù)回还。進(jìn)程可以登記特殊的信號處理函數(shù)裆泳。當(dāng)進(jìn)程收到信號時(shí),信號處理函數(shù)將像中斷服務(wù)例程一樣被調(diào)用柠硕,當(dāng)從信號處理函數(shù)返回時(shí)工禾,控制被返回給主程序,并且繼續(xù)正常執(zhí)行蝗柔。
- 但是闻葵,信號和中斷有所不同。中斷的響應(yīng)和處理都發(fā)生在內(nèi)核空間癣丧,而信號的響應(yīng)發(fā)生在內(nèi)核空間槽畔,信號處理程序的執(zhí)行卻發(fā)生在用戶空間。
那么什么時(shí)候檢測和響應(yīng)信號胁编?通常發(fā)生在兩種情況下:
- 當(dāng)前進(jìn)程由于系統(tǒng)調(diào)用厢钧、中斷或異常而進(jìn)入內(nèi)核空間以后鳞尔,從內(nèi)核空間返回到用戶空間前戲
- 當(dāng)前進(jìn)程在內(nèi)核進(jìn)入睡眠以后剛被喚醒的時(shí)候,由于檢測到信號的存在而提前返回到用戶空間
4.3 信號的本質(zhì)
- 信號是在軟件層次上對中斷機(jī)制的一種模擬早直,在原理上寥假,一個(gè)進(jìn)程收到一個(gè)信號與處理器收到一個(gè)中斷請求可以說是一樣的。
- 信號是異步的霞扬,一個(gè)進(jìn)程不必通過任何操作來等待信號的到達(dá)糕韧,事實(shí)上,進(jìn)程也不知道信號到底什么時(shí)候到達(dá)祥得。
- 信號是進(jìn)程間通信機(jī)制中唯一的異步通信機(jī)制兔沃,可以看作是異步通知,通知接收信號的進(jìn)程有哪些事情發(fā)生了级及。
- 信號機(jī)制經(jīng)過POSIX實(shí)時(shí)擴(kuò)展后乒疏,功能更加強(qiáng)大,除了基本通知功能外饮焦,還可以傳遞附加信息
4.4 信號來源
- 信號事件的發(fā)生有兩個(gè)來源:硬件來源(比如我們按下鍵盤或者其他硬件故障)怕吴;潤健來源,最常用發(fā)送信號的系統(tǒng)函數(shù)是kill县踢,raise,alarm和setitimer以及sigqueue函數(shù)转绷,軟件來源還包括一些非法運(yùn)算等操作。
4.5 關(guān)于信號處理機(jī)制的原理(內(nèi)核角度)
- 內(nèi)核給一個(gè)進(jìn)程發(fā)送中斷信號硼啤,是在進(jìn)程所在的進(jìn)程表項(xiàng)的信號域設(shè)置對應(yīng)于該信號的位议经。
- 如果信號發(fā)送給一個(gè)正在睡眠的進(jìn)程,那么要看該進(jìn)程進(jìn)入睡眠的優(yōu)先級谴返,如果進(jìn)程睡眠在可被中斷的優(yōu)先級上煞肾,則喚醒正在睡眠的進(jìn)程;否則僅設(shè)置進(jìn)程表中信號域相應(yīng)的位嗓袱,而不是喚醒進(jìn)程籍救。
- 進(jìn)程檢查是否收到信號的時(shí)機(jī)是:一個(gè)進(jìn)程在即將從內(nèi)核態(tài)返回到用戶態(tài)時(shí);或者渠抹,在一個(gè)進(jìn)程進(jìn)入或離開一個(gè)適當(dāng)?shù)牡驼{(diào)度優(yōu)先級睡眠狀態(tài)時(shí)蝙昙。
- 內(nèi)核處理一個(gè)進(jìn)程吸收的信號的時(shí)機(jī)是在一個(gè)進(jìn)程從內(nèi)核態(tài)返回用戶態(tài)時(shí)。所以梧却,當(dāng)一個(gè)進(jìn)程在內(nèi)核態(tài)下運(yùn)行時(shí)奇颠,軟中斷信號并不立即起作用,要等到將返回用戶態(tài)時(shí)才處理放航。進(jìn)程只有處理完信號才會返回用戶態(tài)大刊,進(jìn)程在用戶態(tài)下不會有未處理完的信號。
- 內(nèi)核處理一個(gè)進(jìn)程收到的軟中斷信號是在該進(jìn)程的上下文中。因此缺菌,進(jìn)程必須處于運(yùn)行狀態(tài)葫辐。如果進(jìn)程收到一個(gè)要捕捉的信號,那么進(jìn)程從內(nèi)核態(tài)返回用戶態(tài)時(shí)執(zhí)行用戶定義的函數(shù)伴郁。
- 而且執(zhí)行用戶定義的函數(shù)的方法很巧妙耿战,內(nèi)核是在用戶棧上創(chuàng)建一個(gè)新的層,該層中將返回地址的值設(shè)置成用戶定義的處理函數(shù)的地址焊傅,這樣進(jìn)程從內(nèi)核返回彈出棧頂時(shí)就返回到用戶定義的函數(shù)出剂陡,從函數(shù)返回再彈出棧頂時(shí),才返回原先進(jìn)入內(nèi)核的地方狐胎,接著原來的地方繼續(xù)運(yùn)行鸭栖。這樣做的原因是用戶定義的處理函數(shù)不能且不允許在內(nèi)核態(tài)下執(zhí)行。