Linux|信號
- 信號的概念
- 信號處理的三種方式
- 捕捉信號的System Call -- signal
- 1.產生信號的5種方式
- 2.信號的保存
- 2.1 core 標志位
- 2.信號的保存
- 2.1 對pending 表 和 block 表操作
- 2.2 阻塞SIGINT信號 并打印pending表例子
- 捕捉信號
- sigaction 函數
- 驗證當前正在處理某信號,則該信號會自動被屏蔽
- 驗證當前信號被處理完之后,會自動解除屏蔽
- 地址空間中操作系統態
- 談談鍵盤輸入的過程
- 兩個深刻的問題
- 如何操作系統是怎么運行的
- 如何理解系統調用
- 可重入函數
- volatile
- sigchild信號
信號的概念
信號:是進程之間異步通知的一種方式,屬于軟中斷。
所謂異步就是 a 和 b 之間沒有聯系,比如同學a 去上廁所了,老師b還是繼續講課,這稱為異步。
信號處理的三種方式
一般情況下是三選一
- 忽略此信號
- 執行該信號的默認處理動作
- 提供一個信號處理函數,要求內核在處理該信號時切換到用戶態執行這個處理函數,這種方式稱為捕捉(Catch)一個信號
捕捉信號的System Call – signal
每個信號都有一個編號和一個宏定義名稱,這些宏定義可以在signal.h中找到,例如其中有定 義 #define SIGINT 2
sighandler_t signal(int signum, sighandler_t handler);
當我們在鍵盤中 按ctrl + c 的時候 就會發送一個SIGINT信號,
我們可以用 signal 這個系統調用驗證
#include <iostream>
#include <unistd.h>
#include <signal.h>
void hander(int sig)
{std::cout<<"catch sig:"<< sig<<std::endl;
}
int main()
{signal(2,hander);while(true)return 0;
}
有同學會想我把所有的信號都捕捉了,那個這個進程是不是就刀槍不入了?不是的 因為9號信號 無法捕捉
1.產生信號的5種方式
1. 通過 kill 命令,向指定的進程發信號
2. 通過鍵盤 ctrl + c
3. 系統調用 kill
raise(sign) 和 kill(getpid(),sign) 是等價的
alrm 也可以產生信號 alrm的返回值是上一個鬧鐘的剩余時間
同一個進程同一個時間只能有一個鬧鐘!
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
void hander(int sig)
{std::cout<<"catch sig:"<< sig<<std::endl;
}
int main()
{signal(2,hander);//kill(getpid(),2);raise(2);sleep(3);return 0;
}
4.軟件條件
比如 管道 我們讀端關閉 , 寫端還在寫,那么就會產生一個SIGPIPE的信號。
5. 異常
a.
void hander(int sg)
{std:: cout<< "捕捉到:"<<sg<<std::endl;
}
int main()
{signal(8,hander);int b = 10 / 0;return 0;
}
可能有同學會問為什么會一直死循環打印捕捉到的8號信號呢?
當處理器檢測到除法錯誤時,它會暫停正常的指令流,保存當前的狀態(包括程序計數器和其他寄存器的內容),然后跳轉到一個預定義的地址來處理這個異常。這個地址指向的是操作系統的異常處理程序,它可以記錄錯誤、終止進程或采取其他恢復措施,由于進程沒有退出,又恢復當前的狀態,到cpu中 ,cpu中的溢出標記位又置為1了。(這也回答cpu是怎么檢測到除以0的)總的來說就是因為進程一直被調度,所以才出現死循環的情況。
終止進程的本質:釋放進程的上下文數據,報告溢出標志數據或其他異常數據
b. 野指針問題:
CR3 + MMU : 將虛擬地址轉換為物理地址
CR2:保存主要用于存儲最近一次發生的頁面錯誤(page fault)時的線性地址。
當異常的時候,操作系統檢測到CR2中的地址,開始發送信號。
2.信號的保存
2.1 core 標志位
當時在進程控制時 waitpid 函數中的 status參數 core dump 標志位 我們現在就馬上知道什么意思了。當程序被信號殺死時,會生成一個core的debug文件。 這個core標記位 ,為0不允許生成,為1運行生成debug文件。
在云服務上 生成這個core文件的功能默認是被關閉的
ulimit - a 查看core file size 的大小
ulimit -c 【size】 設置一下就好了
也有 可能 生成的core 文件不在當前目錄
echo ./core > /proc/sys/kernel/core_pattern 就歐克啦
一重啟就會生成一個core.進程號的文件 如果無限制的重啟 就會生成非常多的core文件 所以云服務器就把這個功能關閉了
調試的時候,我們core-file core文件 把這個debug文件加載進去,調試器就直接顯示出錯的那一行了!
#include <iostream>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>int sum(int star, int end)
{int ret = 0;for (int i = star; i <= end; i++){ret /= 0;ret += i;}return ret;
}
int main()
{pid_t id = fork();if(id == 0){sleep(1);sum(1, 100);exit(0);}int status = 0;pid_t rid = waitpid(id, &status, 0);if(rid > 0){printf("exit code: %d, sig: %d, core dump:%d\n",(status >> 8) &0xff, status &0x7f,(status >> 7) &1);}return 0;
}
當我們把ulimit -c設置為 0時 coredump 標記位就為0了 表示 不生成core dump(核心轉儲)文件
2.信號的保存
信號的保存就保存在這三張表中,block表,peding表,handler表。
每個信號都有兩個標志位分別表示阻塞(block)和未決(pending),還有一個函數指針表示處理動作。信號產生時,內核在進程控制塊中設置該信號的未決標志,直到信號遞達才清除該標志。
執行信號處理的動作稱為信號的遞達。
信號產生到遞達之間稱為未決
如果一個信號被阻塞了,那么它永遠未決。
我們用的signal方法sighandler_t signal(int signum, sighandler_t handler); 其中 我們寫的handler 就是把函數地址寫進對應的handler表下標中 。
兩張位圖+函數指針數組 == 讓進程識別信號
2.1 對pending 表 和 block 表操作
先介紹幾個函數
#include <signal.h>
// 清空位圖
int sigemptyset(sigset_t *set);
// 所有bit位全為1
int sigfillset(sigset_t *set);
// 把某一bit位置為1
int sigaddset (sigset_t *set, int signo);
// 把某一bit位置為0
int sigdelset(sigset_t *set, int signo);
// 判斷某一比特位是不是1
int sigismember(const sigset_t *set, int signo);
signal.h 給我們提供了 用戶級別的位圖,這些函數可以用來操作這個位圖 sigset_t
//調用函數sigprocmask可以讀取或更改進程的信號屏蔽字(阻塞信號集)。
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
// 獲取pending 表
int sigpending (sigset_t * set);
2.2 阻塞SIGINT信號 并打印pending表例子
// 利用上面的函數,我們就是驗證 某一信號被阻塞后,是否一直未決
#include <iostream>
#include <signal.h>
#include <unistd.h>
void PrintPending( sigset_t & pending)
{for(int sig = 31; sig >= 1; sig--){if(sigismember(&pending,sig)){std::cout<<1;}else{std::cout<<0;}}std::cout<<std::endl;
}int main()
{sigset_t block_set , old_set;sigemptyset(&block_set);sigemptyset(&old_set);sigaddset(&block_set , SIGINT);// sigprocmask(SIG_BLOCK,&block_set,&old_set);while(true){sigset_t pending;sigpending(&pending);PrintPending(pending);sleep(1);}return 0;
}
捕捉信號
如果信號的處理動作是用戶自定義函數,在信號遞達時就調用這個函數,這稱為捕捉信號。
信號可能不會立即被處理,而是在合適的時候處理
,這個合適的時候指的是,從用戶態返回內核態的時候進行處理。
用戶態:執行我們自己的數據和代碼的時候
內核態:執行操作系統的代碼和數據的時候
當信號的處理動作是自定義的信號處理函數時才返回時先到用戶態再從內核態到用戶態(因為hander方法 和 main 函數不是調用關系并不能直接返回)。
如果是默認 則直接殺死進程了。 忽略則 除了修改pending 表 由 1 變為 0,其他什么也不干。
舉例:
戶程序注冊了SIGQUIT信號的處理函數sighandler。 當前正在執行main函數,這時發生中斷或異常切換到內核態。 在中斷處理完畢后要返回用戶態的main函數之前檢查到有信號
SIGQUIT遞達。 內核決定返回用戶態后不是恢復main函數的上下文繼續執行,而是執行sighandler函 數,sighandler
和main函數使用不同的堆棧空間,它們之間不存在調用和被調用的關系,是 兩個獨立的控制流程。 sighandler函數返
回后自動執行特殊的系統調用sigreturn再次進入內核態。 如果沒有新的信號要遞達,這次再返回用戶態就是恢復main函數的上下文繼續執行了。
sigaction 函數
**int sigaction(int signum, const struct sigaction act, struct sigaction oldact);
和 signal一樣都是捕捉信號的,它有一個同名的結構體,但這個結構體我們只關心ssiginfo_t 這個函數指針方法字段
void handler(int signal)
{std::cout<<"捕捉到:"<<signal<<std::endl;// while(true)// {// sigset_t pending;// sigpending(&pending);// Print(pending);// sleep(1);// }exit(1);
}int main()
{struct sigaction act, oact;act.sa_flags = 0;//在這種情況下,信號處理將遵循默認的行為,//也就是說,信號處理函數將作為一個普通的函數執行,//而不會觸發任何 sa_flags 標志所定義的特殊行為。act.sa_handler = handler;sigemptyset(&act.sa_mask);//sigaddset(&act.sa_mask,3); // 順帶屏蔽三號信號sigaction(2,&act,&oact);while(true){std::cout<<"pid: "<<getpid()<<std::endl;sleep(1);} return 0;
}
驗證當前正在處理某信號,則該信號會自動被屏蔽
我們在hander方法中一直sleep,不退出hander方法,我們再按ctrl + c信號也不會被處理了。這就驗證了當前信號正在被處理,則該信號會被自動屏蔽。
驗證當前信號被處理完之后,會自動解除屏蔽
我們設置hander方法睡三秒自動退出。 退出之后又可以捕捉到2號信號則證明了該結論
地址空間中操作系統態
內核級頁表所有進程共享一份用戶級頁表每一個進程都有一份。操作系統的代碼數據都通過內核級頁表映射在物理內存中。
談談鍵盤輸入的過程
操作系統怎么知道鍵盤摁下了? 是一直問鍵盤嗎?當然不是,那不然太浪費cpu資源了
每一個硬件都有一個中斷號,硬盤也不例外,當按下一個鍵后,通過8529這個芯片向cpu 發出硬件中斷,某一個寄存器上就有了鍵盤的中斷號,再在中斷向量表中查詢對應的鍵盤讀入方法~這樣就完成了cpu知道鍵盤輸入的一個過程。
我們學習的信號就是模擬硬件中斷實現的!
兩個深刻的問題
如何操作系統是怎么運行的
操作系統調用進程誰由來調度操作系統呢?
硬件上有一個時鐘,時鐘到了就通過中斷提醒操作系統該檢測進程的時間片,時間片到了就切換進程,否則什么也不做
如何理解系統調用
- 有一個函數指針數組,通過下標 可以找到系統調用,這個下標我們稱為系統調用號。
- 我們使用系統調用如fork時,會產生內部中斷(陷阱),執行系統調用的方法,讓cpu找這個函數指針數組。eax 中保存這個函數系統調用號,然后cpu就找到這個系統調用了
可重入函數
像上例這樣,insert函數被不同的控制流程調用,有可能在第一次調用還沒返回時就再次進入該函數,這稱為重入,insert函數訪問一個全局鏈表,有可能因為重入而造成錯亂,像這樣的函數稱為 不可重入函數
volatile
#include <iostream>
#include <signal.h>
int gflag = 0;
void changeData(int signo)
{std::cout<<"gflg:0 -> 1"<<std::endl;gflag = 1;
}
int main()
{signal(2,changeData);while(!gflag);std::cout<<"process quit!"<<std::endl;return 0;
}
當我們用編譯器O1的優化時,main函數 里面又沒有修改 gflag的值,于是編譯器把內存中的值拷貝到寄存器后,就只看寄存器中的值了。
怎么解決這個問題呢?
我們可以在gval前 加一個volatile關鍵字 保證內存的可見性就行了。
sigchild信號
子進程退出的時候會給父進程發送一個sigchild信號
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <stdlib.h>
void notice(int sig)
{std::cout<<"I am fatherprocess,pid: "<<getpid()<<std::endl;std::cout<<"get sig:"<<sig<<std::endl;
}
int main()
{signal(SIGCHLD,notice);pid_t id = fork();if(id == 0){std::cout<<"I am childprocess,pid: "<<getpid()<<std::endl;sleep(3);exit(1);}sleep(100);return 0;
}
如果不關心 子進程的退出信息則可以把SIGCHLD 的捕捉動作改為SIG_IGN
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <stdlib.h>int main()
{signal(SIGCHLD,SIG_IGN);pid_t id = fork();if(id == 0){int cnt = 5;while(cnt--){std::cout<<"child process runing"<<std::endl;std::cout<<"cnt:"<<cnt<<std::endl;sleep(1);}exit(1);}while(true){std::cout<<"father process runing"<<std::endl;sleep(1);}return 0;
}