行程信號
- 初始信號
- 生活角度中的信號
- 技術應用角度的信號
- 信號概念
- 查看信號串列
- 信號處理常見方式
- 產生信號
- 1.通過終端產生信號
- 2.通過系統呼叫函式產生信號
- 3. 由軟體條件產生信號
- 4.硬體例外產生信號
- 阻塞信號
- 信號其他相關常見概念
- 在內核中表示
- sigset_t
- 信號集操作函式
- sigprocmask函式
- sigpending
- 捕捉信號
- 用戶空間和內核空間
- 內核態和用戶態
- 內核如何捕捉信號
- sigaction
- 可重入函式
- volatile
初始信號
生活角度中的信號
- 當我們在網上買東西,再等待不同商品快遞的到來,但即便快遞沒有到來,我們也知道快遞來臨時,我們該怎么處理快遞,也就是我們能“識別快遞”
- 當快遞到了,但是我們正在忙其他的事情,這段時間內我們沒有取快遞,但是我們是知道有快遞到來了,也就是取快遞的行為并不是一定要立即執行,可以理解成“在合適的時候去取”
- 在收到短信時,再到你拿到快遞期間,是有一個時間視窗的,在這段時間,我們并沒有拿到快遞,但是我們知道有一個快遞已經來了,本質上是我們“記住了有一個快遞要去取”
- 當我們有時間時,順利拿到快遞之后,就要開始處理快遞了,而處理快遞一般方式有三種:1. 執行默認動作(打開快遞,使用商品)2. 執行自定義動作(快遞是吃的,馬上干掉它)3. 忽略快
遞(快遞拿到后放一邊,繼續忙自己的事情)
技術應用角度的信號
我們寫個簡單的死回圈代碼:
1 #include<iostream>
2 #include<unistd.h>
3 using namespace std;
4
5 int main()
6 {
7 while(1)
8 {
9 cout<<"This is signal"<<endl;
10 sleep(1);
11 }
12 return 0;
13 }

為什么我們按下鍵盤的Ctrl+c就終止掉程式了呢?
我們按下Ctrl+c時鍵盤輸入產生一個硬體中斷,被作業系統獲取,然后作業系統解釋成信號(2號信號)發送給行程,行程收到2號信號后退出,我們可以用signal函式來測驗行程是不是收到了2號信號,使用該函式要傳入2個引數,第1個引數是信號編號,第2個引數是你要怎么處理這個信號,

確實是收到了2號信號,但是為什么沒有退出呢?因為我們把它默認退出改成了列印,
Ctrl+c只能發送給前臺行程,
注意:系統當中,打開一個終端一個bash中只允許有一個前臺行程,
把行程放在后臺運行命令:
要運行的程式名+&
前臺行程和后臺行程的區別
前臺行程演示:

前臺行程我們敲命令是沒用的,可以終止行程
后臺行程演示:

后臺行程我們輸入命令還可以執行,但是卻無法用Ctrl+c干掉,
干掉后臺行程可以用fg轉成前臺行程在干掉,或者用kill + pid干掉它,

這樣就干掉了后臺行程,
信號概念
信號是行程之間事件異步通知的一種方式,屬于軟中斷,俗話說就是通知事件發生,
查看信號串列
kill-l //查看信號串列

1-31是普通信號,34-64是實時信號,
信號處理常見方式
信號處理方式有3種:
- 忽略此信號,
- 執行該信號的默認處理動作,
- 提供一個信號處理函式,要求內核在處理該信號時切換到用戶態執行這個處理函式,這種方式稱捕捉一個信號,
產生信號
1.通過終端產生信號
SIGINT的默認處理動作是終止行程,SIGQUIT的默認處理動作是終止行程并且Core Dump,現在我們來驗證一下,
Core Dump
當一個行程要例外終止時,可以選擇把行程的用戶空間記憶體資料全部 保存到磁盤上,檔案名通常是core,這叫做Core Dump,也就是核心轉儲功能,一個行程允許產生多大的core檔案取決于行程的Resource Limit(這個資訊保存 在PCB中),默認是不允許產生core檔案的,因為core檔案中可能包含用戶密碼等敏感資訊,不安全,在開發除錯階段可以用ulimit命令改變這個限制,允許產生core檔案,
用命令查看資源限制:
ulimit -a

默認core檔案是關閉的,為了測驗我們在云服務中開啟coer檔案,用ulimit命令改變Shell行程的Resource Limit,允許core檔案最大為1024K
ulimit -c 1024

10 int main()
11 {
12
13 while(1)
14 {
15 cout<<"This is signal"<<endl;
16 sleep(1);
17 }
18 return 0;
19 }

ctrl+c和ctrl+\都可以終止掉行程,但我們打開core,ctrl+\產生了core dumped檔案

ulimit命令改變了Shell行程的Resource Limit,test行程的PCB由Shell行程復制而來,所以也具 有和Shell行程相同的Resource Limit值,這樣就可以產生Core Dump了,
10 int main()
11 {
12 // signal(2,handler);
13 int a = 10/0;
14 return 0;
15 }
我們來個整數除0,使用一下core檔案
core-file core.+行程pid

使用除錯時要加上-g選項,Liunx用的是release版本
核心轉儲功能:我們可以通過除錯找到代碼的問題所在,這種除錯這叫做事后除錯,
2.通過系統呼叫函式產生信號
通過kill命令向行程發送信號
kill -l +信號 +行程pid
寫一個死回圈試驗:

kill -信號編號 +行程pid

kill命令是通過呼叫kill函式實作的
函式原型:
int kill(pid_t pid, int signo);
成功回傳0,失敗回傳-1.
6 void handler(int sig)
7 {
8 printf("catch a sig:%d\n",sig);
9 }
10
11 int main(int argc,char* argv[])
12 {
13 if(argc == 3)
14 {
15 kill(atoi(argv[1]),atoi(argv[2]));
16 }
17 return 0;
18 }

我們把它放在后臺睡眠,通過給它發送9號信號就干掉它了,
rasie函式
raise函式可以給當前行程發送指定的信號(自己給自己發信號),
成功回傳0,錯誤回傳-1.
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<signal.h>
4 #include<sys/types.h>
5 void handler(int sig)
6 {
7 printf("catch a sig:%d\n",sig);
8 }
9
10
11 int main()
12 {
13
14 signal(3,handler);
15 while(1)
16 {
17 raise(3);
18 sleep(1);
19 }
20 return 0;
21 }

給當前行程發送3號信號,
abort函式使當前行程接收到信號而例外終止,
函式原型:
#include <stdlib.h>
void abort(void);
//就像exit函式一樣,abort函式總是會成功的,所以沒有回傳值,
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<signal.h>
4 #include<sys/types.h>
5 #include<stdlib.h>
6 void handler(int sig)
7 {
8 printf("catch a sig:%d\n",sig);
9 }
10
11
12 int main()
13 {
14
15 signal(6,handler);
16 while(1)
17 {
18 abort();
19 sleep(1);
20 }
21 return 0;
22 }

就終止掉了行程,
3. 由軟體條件產生信號
SIGPIPE是一種由軟體條件產生的信號,在“管道”中遇見過了,
alarm函式
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
呼叫alarm函式可以設定一個鬧鐘,也就是告訴內核在seconds秒之后給當前行程發SIGALRM信號, 該信號的默認處理動作是終止當前行程,
11 int main()
12 {
13 int count = 0;
14 alarm(1);
15 while(1)
16 {
17 count++;
18 printf("count:%d\n",count);
19 }
20
21 return 0;
22 }

到4萬多就退了,而我們的cpu運算是很快的,我們的代碼是++一次列印到螢屏一次涉及到IO,效率就會很低,我們可以等+的時間到了在列印資料,定義一個全域的count++,
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<signal.h>
4 #include<sys/types.h>
5 #include<stdlib.h>
6 int count = 0;
7 void handler(int sig)
8 {
9 printf("catch a sig:%d\n",sig);
10 printf("count=%d\n",count);
11 exit(0);
12 }
13
14 int main()
15 {
16 signal(SIGALRM,handler);
17 alarm(1);
18 while(1)
19 {
20 count++;
21 }
22
23 return 0;
24 }

這次的count就非常大了,達到了4億多,足以看出IO的效率很低,
4.硬體例外產生信號
硬體例外被硬體以某種方式被硬體檢測到并通知內核,然后內核向當前行程發送適當的信號,

14 int main()
15 {
16 int *p;
17 *p = 10;
18 return 0;
19 }

就是11號信號,
阻塞信號
信號其他相關常見概念
- 實際執行信號的處理動作稱為信號遞達(Delivery)
- 信號從產生到遞達之間的狀態,稱為信號未決(Pending),
- 行程可以選擇阻塞 (Block )某個信號,
- 被阻塞的信號產生時將保持在未決狀態,直到行程解除對此信號的阻塞,才執行遞達的動作.
- 注意,阻塞和忽略是不同的,只要信號被阻塞就不會遞達,而忽略是在遞達之后可選的一種處理動作,
在內核中表示

pending和block都是位圖,有32個位元位,
pending位圖保存信號,有2個意思,是誰?是否?是誰表示是哪個信號,是否收到,block位圖記錄信號被屏蔽的資訊,它們2個位元位的位置是一樣的,但是位元位的內容是不一樣的,handler是陣列,陣列內容是函式指標,自己的函式地址填入叫做自定義捕捉信號,
- 上面的圖中,1號信號沒有被屏蔽,沒有收到1號信號,當它遞達是執行默認處理動作,
- 2號信號產生過,但正在被阻塞,所以暫時不能遞達,雖然它的處理動作是忽略,但在沒有解除阻塞之前不能忽略這個信號,因為行程仍有機會改變處理動作之后再解除阻塞,
- 3號信號未產生過,一旦產生SIGQUIT信號將被阻塞,它的處理動作是用戶自定義函式handler
sigset_t
從上圖來看,每個信號只有一個bit的未決標志,非0即1,不記錄該信號產生了多少次,阻塞標志也是這樣表示的,因此,未決和阻塞標志可以用相同的資料型別sigset_t來存盤,sigset_t稱為信號集,這個型別可以表示每個信號的“有效”或“無效”狀態,在阻塞信號集中“有效”和“無效”的含義是該信號是否被阻塞,而在未決信號集中“有 效”和“無效”的含義是該信號是否處于未決狀態,
也就是說我們可以用sigset來設定信號操作,但是不建議使用,建議使用下面的函式進行操作,
信號集操作函式
下面的一系列函式供我們來操作,
#include <signal.h>
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset (sigset_t *set, int signo);
int sigdelset(sigset_t *set, int signo);
int sigismember(const sigset_t *set, int signo);
- 函式sigemptyset初始化set所指向的信號集,使其中所有信號的對應bit清零,表示該信號集不包含 任何有效信號,
- 函式sigfillset初始化set所指向的信號集,使其中所有信號的對應bit置位,表示 該信號集的有效信號包括系統支持的所有信號
- 注意,在使用sigset_ t型別的變數之前,一定要調 用sigemptyset或sigfillset做初始化,使信號集處于確定的狀態,初始化sigset_t變數之后就可以在呼叫sigaddset和sigdelset在該信號集中添加或洗掉某種有效信號,
- 這四個函式都是成功回傳0,出錯回傳-1,sigismember是一個布爾函式,用于判斷一個信號集的有效信號中是否包含某種 信號,若包含則回傳1,不包含則回傳0,出錯回傳-1,
sigprocmask函式
呼叫函式sigprocmask可以讀取或更改行程的信號屏蔽字(阻塞信號集)
函式原型:
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
成功回傳0,出錯回傳-1,
引數說明:
如果oset是非空指標,則讀取行程的當前信號屏蔽字通過oset引數傳出,如果set是非空指標,則 更改行程的信號屏蔽字,引數how指示如何更改,如果oset和set都是非空指標,則先將原來的信號 屏蔽字備份到oset里,然后根據set和how引數更改信號屏蔽字,

注意:如果呼叫sigprocmask解除了對當前若干個未決信號的阻塞,則在sigprocmask回傳前,至少將其中一個信號遞達,
sigpending
讀取當前行程的未決信號集,通過set引數傳出,呼叫成功則回傳0,出錯則回傳-1,
int sigpending(sigset_t *set);
做一個測驗:
1.利用上述的函式將2號信號屏蔽
2.向行程發送2號信號
3.此時2號信號被屏蔽,處于pending狀態
4.通過sigpending函式來讀取pending信號集來查看驗證,
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<signal.h>
4 #include<sys/types.h>
5 #include<stdlib.h>
6
7 void printsigset(sigset_t *set)
8 {
9 int i = 1;
10 for(;i<32;++i)
11 {
12 if(sigismember(set,i))
13 {
14 printf("1");
15 }
16 else
17 {
18 printf("0");
19 }
20 }
21 printf("\n");
22 }
23
24 int main()
25 {
26 sigset_t set,oset;
27 sigemptyset(&set);//初始化信號集物件
28 sigemptyset(&oset);
29 sigaddset(&set,SIGINT);//發送2號信號
30
31 sigprocmask(SIG_BLOCK,&set,NULL);//阻塞2號信號
32 sigset_t pending;
33 sigemptyset(&pending);//pending位圖置空
34
35 while(1)
36 {
37 sigpending(&pending);//獲取未決信號集
38 printsigset(&pending);
39 sleep(1);
40 }
41 return 0;
42
43 }
效果如下:

一開始是沒有收到任何信號的,當給它發送2號信號,第2個位元位由0變成了1,為了看到2號信號遞達后pending的變化,我們可以設定一段時間后解除對2號信號的屏蔽,并且我們對2號信號進行捕捉自定義執行我們自己的動作,
效果如下:

當解除2號信號時,它執行我們自定義的動作,第2個位元位也從1變為了0.
捕捉信號
用戶空間和內核空間

(在32位下)程式地址空間中有1-3GB是用戶區,3-4GB是內核區,我們的行程映射的物理空間用的用戶級頁表,每個行程都會有自己的用戶級頁表,內核區用的是內核級頁表映射達到物理記憶體,所有的行程用的是同樣一張的內核頁表,用戶是沒有權限隨意的訪問系統的代碼和資料的,
內核態和用戶態
- 用戶態:用來執行系統的代碼時的狀態有很大的權限
- 內核態:執行普通用戶的代碼的狀態,權限小
我們的代碼是在用戶態和內核態進行切換的,如下圖所示:

內核如何捕捉信號
一個信號被遞達:是在內核態切換到用戶態是進行相關檢測的,
那內核是怎么捕捉信號的呢?

為了方便好記可以畫個簡化的圖:

就像數學中的正無窮的符號差不多,但是相交的點是在信號檢測,是在內核區的,和橫線的4個交點就說明進行了4的狀態切換,
信號到用戶自定義的函式,為什么切換到用戶在執行呢?內核是由權限執行用戶的代碼
因為如果是非法的代碼由內核來執行就會容易中病毒,因為內核具有高的權限的,所以系統進行切換到用戶區執行,用戶態的權限是微小的,
sigaction
還可以用sigaction函式進行信號捕捉,
函式原型:
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);
引數說明:
- signo:指定信號的編號
- 若act指標非空,則根據act修改該信號的處理動作
- 若oact指標非 空,則通過oact傳出該信號原來的處理動作,
- act和oact指向sigaction結構體
sigaction結構體如下:
struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
};
第2個和第5個成員是關于實時信號我們不用管,
sa_handler:收到信號,做什么動作
sa_mask:要屏蔽的信號,默認為0
sa_flags:默認設為0
來個例子測驗一手:
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<signal.h>
4 #include<sys/types.h>
5 #include<stdlib.h>
6
7 void handler(int sig)
8 {
9 printf("get a sig:%d\n",sig);
10 }
11
12
13 int main()
14 {
15 struct sigaction act,oact;
16
17 act.sa_handler = handler;
18 act.sa_flags = 0;
19 sigemptyset(&act.sa_mask);
20
21 sigaction(2,&act,&oact);
22 while(1)
23 {
24 printf("i am pid\n");
25 sleep(1);
26 }
27 return 0;
28 }

我們按下Ctrl+c,行程收到了2號信號,由于是我們自定義處理所以它沒有退出,
可重入函式

當我們插入鏈表時,先插入node1,剛讓node1->next指向新節點時候來了一個信號,這個信號也是讓我們進行插入操作,此時從用戶態到內核態中處理,插入完畢后回到用戶態回到main函式里繼續執行插入操作,此時head從指向node2變成了了指向node1,但是node2卻被丟了造成了記憶體泄漏問題,
像上例這樣,insert函式被不同的控制流程呼叫,有可能在第一次呼叫還沒回傳時就再次進入該函式,這稱為重入,insert函式訪問一個全域鏈表,有可能因為重入而造成錯亂,像這樣的函式稱為 不可重入函式,反之,如果一個函式只訪問自己的區域變數或引數,則稱為可重入函式,
當一個函式符合以下條件之一則是不可重入的:
- 呼叫了malloc或free,因為malloc也是用全域鏈表來管理堆的
- 呼叫了標準I/O庫函式,標準I/O庫的很多實作都以不可重入的方式使用全域資料結構,
volatile
先來看一段代碼:
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<signal.h>
4 #include<sys/types.h>
5
6 int flag= 0;
7
8 void handler(int sig)
9 {
10 printf("flag is to 1\n");
11 flag = 1;
12 }
13 int main()
14 {
15
16 signal(2,handler);
17 while(!flag);
18 printf("i am quit!\n");
19
20 return 0;
21 }
定義1個全域flag變數,不發送2號信號會在死回圈,當我們按下Ctrl+c是行程退出,

我們在編譯是加上-O2選項


我們按Ctrl+c但是行程卻沒有退出,我們加了選項把flag的值優化到了CPU的暫存器中,while回圈檢查的flag不是記憶體中最新的flag,就會出現二義性的問題,此時就要用volatile,

即使有優化,當我們按Ctrl+c時行程退出了,
volatile 作用:保持記憶體的可見性,告知編譯器,被該關鍵字修飾的變數,不允許被優化,對該變數的任何操作,都必須在真實的記憶體中進行操作,
本篇文章到這就已結束了,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/403965.html
標籤:其他
