🌻個人主頁:路飛雪吖~
? ? ? ?🌠專欄:Linux
目錄
一、掌握Linux信號的基本概念
?🌠前臺進程 VS 后臺進程
🌠 小貼士:
🪄?個系統函數 --- signal()
?🪄查看信號 --- man 7 signal
二、軟硬件上理解 :?OS如何進行信號處理
?硬件上理解
🌠 信號 VS 硬件中斷
?軟件:如何理解信號處理?
三、?信號產生的方式
🚩1. 鍵盤產生
🚩2. 系統指令產生? 發送信號? ---- 底層使用的是系統調用?編輯
🚩3. 系統調用 發送信號
🌠? 發送信號的其他函數:
🪄 raise ---?函數可以給當前進程發送指定的信號(??給??發信號)?
🪄 abort ---?自己給自己發了一個特定的終止自己的信號 --- 6號信號[SIGABRT]
🚩4. 由軟件條件 產生信號
?🌠OS內定時器
四、? alarm() 設置重復鬧鐘 ---?簡單快速理解系統鬧鐘
🚩5. 異常?(野指針、 除 0)
?🪄模擬 野指針
?🪄?模擬 除 0
🌠OS怎么知道我們的進程內部出錯了?為什么會陷入死循環?
???除 0 :
???野指針
🌠core VS Term?
?core 的使用 --- 調試
🌠 如果是子進程異常了呢? core 會不會出現?
🌠小結:
?五、信號處理
1. 默認處理動作
2. 忽略:本身就是一種信號捕捉的方法,動作就是忽略
?3. 自定義 捕捉一個信號
? 信號是內置的,進程認識信號,是程序員內置的特性;
? 信號的處理方法,在信號產生之前,就已經準備好了;
? 何時處理信號?先處理優先級很高的,可能并不是立即處理,在合適的時候處理;
? 怎么處理信號:a. 默認行為,b. 忽略信號,c. 自定義動作
信號產生 ---> 信號保存 ---> 信號處理
一、掌握Linux信號的基本概念
// MakefileBIN=sig
CC=g++
SRC=$(shell ls *.cc)
OBJ=$(SRC:.cc=.o)$(BIN):$(OBJ)$(CC) -o $@ $^ -std=c++11%.o:%.cc$(CC) -c $< -std=c++11.PHONY:clean
clean:rm -f $(BIN)
// Signal.cc#include <iostream>
#include <unistd.h>int main()
{while(true){std::cout << "hello world" << std::endl;sleep(1);}return 0;
}
?🌠前臺進程 VS 后臺進程
??前臺進程【./sig】:在命令行所輸入的所有東西都不能執行。
? > Ctrl + c 終止前臺進程:給的是shell,shell在前臺 --> 信號發給進程。
??后臺進程【./sig &】:bash進程依舊可以進行命令行解釋。
? > kill -9 [pid] 終止后臺進程
? > fg [作業號]?
🌠 小貼士:
<1>
?【1 -- 31】:普通信號;
?【34 -- 64】:實時信號。
<2>?
ctrl + c --> 信號發給進程,被OS接受并解釋成為2號【SIGINT】信號,發送給目標進程 -- 對2號信號的默認處理動作是 終止自己!。
🪄?個系統函數 --- signal()
#include <iostream>
#include <unistd.h>
#include <signal.h>void Handler(int signo)
{// 當對應的信號被觸發,內核會將對應的信號編號,傳遞給自定義方法std::cout << "Get a signal, signal number is : " << signo << std::endl;
}int main()
{signal(SIGINT, Handler);// 默認終止 --改成了--> 執行自定義方法:Handler while(true){std::cout << "hello world" << std::endl;sleep(1);}return 0;
}
#include <iostream>
#include <unistd.h>
#include <signal.h>void Handler(int signo)
{// 當對應的信號被觸發,內核會將對應的信號編號,傳遞給自定義方法std::cout << "Get a signal, signal number is : " << signo << std::endl;
}int main()
{signal(SIGQUIT, Handler);// 默認終止 --改成了--> 執行自定義方法:Handler while(true){std::cout << "hello world" << std::endl;sleep(1);}return 0;
}
ctrl + \ :3號信號,默認也是終止進程的。
?🪄查看信號 --- man 7 signal
#include <iostream>
#include <unistd.h>
#include <signal.h>void Handler(int signo)
{// 當對應的信號被觸發,內核會將對應的信號編號,傳遞給自定義方法std::cout << "Get a signal, signal number is : " << signo << std::endl;
}int main()
{// signal 為什么不放在循環里面? 不需要,只需要設置一次就可以了// signal:如果沒有產生2或者3號信號呢? handler不被調用!// signal(SIGINT, Handler);// 默認終止 --改成了--> 執行自定義方法:Handler// signal(SIGQUIT, Handler);// 默認終止 --改成了--> 執行自定義方法:Handlerfor (int signo = 1; signo < 32; signo++)// 捕捉 1--31 號的信號,使得很多方法殺不掉進程,但是有一些信號捕捉不到{signal(signo, Handler);std::cout << "自定義捕捉信號:" << signo << std::endl;}while (true){std::cout << "hello world" << std::endl;sleep(1);}return 0;
}
二、軟硬件上理解 :?OS如何進行信號處理
?硬件上理解
OS怎么知道鍵盤上面有數據?
當按下鍵盤 首先是被驅動程序識別到 按下的按鍵,OS如何知道鍵盤被按下了?--- 硬件中斷?,鍵盤一旦按下,在硬件上 鍵盤 和 CPU 是連接的,在控制信號,輸入設備是直接和中央處理器連接的,輸入設備首先會給中央處理器發送一個中斷信號【硬件電路】,CPU中央處理器收到這個硬件電路之后,立馬就會告訴OS,當前外設有一個外設準備好了,接下來讓OS主動的去把外設的數據拷貝到內存里,此時OS再也不用主動輪詢檢測任何輸入/輸出設備了,只需要等待發生中斷信號 --- 硬件和OS可以并行執行了!
?
🌠 信號 VS 硬件中斷
? 信號是純軟件,模擬中斷的行為;
? 硬件中斷,純硬件。
?軟件:如何理解信號處理?
? 鍵盤上的組合鍵 是先被OS系統識別到【OS是鍵盤真正的管理者,當系統在運行的時候,OS一直在檢測鍵盤上有沒有信息】再發給進程,當進程不能立即處理這個信號時,進程就會記錄下這個信號【信號是從 1-31 連續的數字,進程是否收到1-31這個數字的信號 --- 在 task_struct?里通過位圖記錄 0000 0000 0000 0000 0000 0000 0000 0000,比特位的位置:信號的編號,比特位的內容:是否為0/1,是否收到對應的信號】,發送信號的本質是什么?【寫入信號】OS修改目標進程的PCB中的信號位圖【0 -> 1】,操作系統有沒有權利修改進程PCB的位圖?有,OS是進程的管理者,所以 無論以什么方式發送信號,最終 都是轉換到OS,讓OS寫入信號,因為OS是進程的唯一管理者。? ?
? 信號產生有很多種,但是信號發送只有OS;
? 判斷進程是否收到信號:進程里面有信號處理的表結構 sighandler_t arr[32] 函數指針數組,根據PCB里面去查位圖,根據位圖就可以檢測出那個比特位為1,拿到比特位為1,這個數字就可以作為該數組的下標【下標-1】,直接去調用函數指針上的方法。
三、?信號產生的方式
🚩1. 鍵盤產生
當在可執行程序在前臺進程產生之后,按住 ctrl+c ,鍵盤被按下,計算機的CPU識別到鍵盤又被按下的動作,喚醒OS,讓OS去讀取鍵盤山的 ctrl+c ,讀到后將 ctrl + c 解釋成 2號 信號【if(案件 == ctrl + c)】,OS會把 ctrl + c 轉化成一段代碼【向目標前臺進程PCB寫入2號信號,即把比特位 0 --> 1,OS信號發送完成】,發送完成之后,該進程在后續合適的時候調度運行,發現自己收到一個信號,當前進程默認就要執行自己對 2號 信號的處理動作。
🚩2. 系統指令產生? 發送信號? ---- 底層使用的是系統調用
🚩3. 系統調用 發送信號
// MakefileBIN=mykill
CC=g++
SRC=$(shell ls *.cc)
OBJ=$(SRC:.cc=.o)$(BIN):$(OBJ)$(CC) -o $@ $^ -std=c++11%.o:%.cc$(CC) -c $< -std=c++11.PHONY:clean
clean:rm -f $(BIN)
// Signal.cc#include <iostream>
#include <unistd.h>
#include <stdlib.h>
#include <string>
#include <signal.h>// 設置自己的killvoid Usage(std::string proc)
{std::cout << "Usage: " << proc << " signumber processid " << std::endl;
}// ./mykill 1信號 12345進程號
int main(int argc, char *argv[])
{if(argc != 3){Usage(argv[0]);exit(1);}int signumber = std::stoi(argv[1]);// 轉換為整數pid_t id = std::stoi(argv[2]);int n = ::kill(id, signumber);if(n < 0){perror("kill");exit(2);}exit(0);
}
信號發送 本質都是操作系統OS發的!!!
🌠? 發送信號的其他函數:
🪄 raise ---?函數可以給當前進程發送指定的信號(??給??發信號)?
#include <iostream> #include <unistd.h> #include <signal.h>int main() {int cnt = 5;while(true){std::cout << "hahaha alive" << std::endl;cnt--;if(cnt <= 0)raise(9);sleep(1);} }
🪄 abort ---?自己給自己發了一個特定的終止自己的信號 --- 6號信號[SIGABRT]
int main() {int cnt = 5;while(true){std::cout << "hahaha alive" << std::endl;cnt--;if(cnt <= 0)// abort();sleep(1);} }
🚩4. 由軟件條件 產生信號
?SIGPIPE 是?種由軟件條件產生的信號。
🪄 alarm 函數 ---?調? alarm 函數可以設定?個鬧鐘,也就是告訴內核在 seconds 秒之后給當前進程發 (14號)SIGALRM 信號,該信號的默認處理動作是終?當前進程。
int main()
{// 統計我的服務器 1s 可以將計數器累加多少!alarm(1);// 我自己,會在 1s 之后收到一個SIGALARM信號int number = 0;while(true){printf("count: %d\n", number);}}
int number = 0;
void die(int signumber)
{printf("get a sig : %d, count: %d\n", signumber, number);exit(0);
}int main()
{// 統計我的服務器 1s 可以將計數器累加多少!alarm(1);// 我自己,會在 1s 之后收到一個SIGALARM信號signal(SIGALRM, die);while(true){number++;}}
這兩個的寫法 計數器累加的次數為什么會差別這么大呢?
【printf("count: %d\n", number);】里面的 IO 影響了計算的速度!,【while(true) number++;】純CPU計算。
?🌠OS內定時器
? 在操作系統內,要對 進程管理、文件管理、多線程、內存管理 進行管理,同時也要做定時管理,OS在開機之后,是會維護時間的,所以設置的鬧鐘,在OS底層就是設置了一個定時器。?
? 許多進程都可以設置鬧鐘,OS里可以同時存在很多被設置的鬧鐘,所以OS就要管理定時器【先描述,再組織】[struct timer{}] ,OS在每次檢測超時的時候,會把定時器所對應的節點,按順序進行升序排序,變成有序的列表,當有超時的時候,只需要從前往后遍歷,遇到第一個沒有超時的,之前的全部是超時的,遍歷的同時,把超時的【struct timer】執行對應的 【func_t f;函數,給目標進程發送SIGALRM信號】在操作系統內給相應的進程發信號。
? 用堆理解定時器的排序:維護成一個最小堆,用超時時間作為鍵值,所以要想知道定時器有沒有超時,只需要查堆頂就可以了,若堆頂沒有超時,則所有的節點都沒有超時;若堆頂超時,就把堆頂pop出來【堆再重新構建】,去執行相應的操作,重復操作,直到不再超時。
? 所以當我們在調用alarm函數時,就相當于在OS內給我們獲取當前時間【currenttime】和 當前的超時時間【seconds】,然后在內核中設置一個節點,放在堆里面,OS就會在超時之后,直接向目標進程發送SIGALRM信號,并且把該節點釋放掉。
? 鬧鐘的返回值 是什么?鬧鐘剩余時間。
int main() {alarm(10);sleep(4);int n = alarm(0);// 0:取消鬧鐘std::cout << "n: " << n << std::endl; }
? 當軟件條件就緒時【超時、鬧鐘、定時器】,OS就可以向目標進程發送信號。
四、? alarm() 設置重復鬧鐘 ---?簡單快速理解系統鬧鐘
#include <iostream>
#include <unistd.h>
#include <stdlib.h>
#include <string>
#include <signal.h>
#include <functional>
#include <vector>// 鬧鐘
using func_t = std::function<void()>;int gcount = 0;
std::vector<func_t> gfuncs;// 把信號 更換 成為 硬件中斷
void handler(int signo)
{for(auto &f : gfuncs){f();}std::cout << "gcount : " << gcount << std::endl;alarm(1);// 上面的鬧鐘響了之后,繼續執行
}int main()
{gfuncs.push_back([](){ std::cout << "我是一個內核刷新操作" << std::endl;});gfuncs.push_back([](){ std::cout << "我是一個檢測進程時間片的操作,如果時間片到了,我會切換進程" << std::endl;});gfuncs.push_back([](){ std::cout << "我是一個內存管理操作,定期清理操作系統內部的內存碎片" << std::endl;});alarm(1);// 一次性的鬧鐘,超時alarm會自動被取消signal(SIGALRM, handler);while(true) // gcount++;{pause();std::cout << "我醒來了..." << std::endl;gcount++;}
}
? 操作系統其實就是一個死循環,當OS啟動之后,會接收外部固定的事件源【時鐘中斷,集成在CPU內部】,每隔很短的時間,向OS觸發硬件中斷,讓OS去執行中斷方法。
? OS的調度、切換、內存管理、系統調用,全都是依靠中斷來完成的。所以OS不應該叫OS,應該叫一個中斷程序。
🚩5. 異常?(野指針、 除 0)
?🪄模擬 野指針
void handler(int signo)
{std::cout << "get a signo: " << signo << std::endl;// 我捕捉了 11 號信號,沒執行默認動作,也沒有退出進程
}int main()
{signal(11, handler);int *p = nullptr;*p = 100;while(true);
}
?🪄?模擬 除 0
void handler(int signo)
{std::cout << "get a signo: " << signo << std::endl;// 我捕捉了 8 號信號,沒執行默認動作,也沒有退出進程
}int main()
{signal(8, handler);int a = 10;a /= 0;while(true);
}
C/C++中,常見的異常,進程崩潰了,原因是 OS給目標進程發送對應錯誤的信號,進而導致該進程退出。
🌠OS怎么知道我們的進程內部出錯了?為什么會陷入死循環?
???除 0 :
狀態寄存器里會有各種描述狀態計算的結果,其中有一個叫做 溢出標記位,正常情況下計算完畢,溢出標記位為0,說明整個計算結果沒有溢出,結果可靠,此時把結果寫回內存里,就完成對應的復制。若對應的溢出標記位為1,就證明CPU內部計算結果出錯了。
當出現溢出,即CPU內部結果出錯了,OS就會知道【CPU[硬件]的中斷有?內部中斷 和 外部中斷,CPU內部一旦出現溢出的錯誤,CPU內部硬件就會觸發內部中斷,就會告訴OS,OS是硬件的管理者,OS在調度、切換,就是在合理的使用CPU資源】,是哪個進程引起的硬件錯誤,OS就會通過信號殺掉進程!即當計算出現硬件錯誤,OS要識別到,就會給目標進程發送對應的信號,殺掉進程!
為什么會陷入死循環【不退出進程】?
這個進程一直沒有退出,還要被調度,進程就會被切換、執行等等,【a /= 0】CPU里面的狀態寄存器的 Eflags的溢出標記位一直都是1,CPU里面的數據一直都是這個進程的上下文內容不會更改,所以就會一直被捕捉,被調度,就會出現死循環,并且一直在觸發8號信號。
???野指針
CR3 寄存器:保存頁表的起始地址,負責虛擬地址和物理地址轉化。
MMU【硬件】被集成在CPU內部:完成虛擬地址和物理地址轉化。
ELP:讀取可執行程序的起始地址。
當傳遞一個 0 地址,在MMU轉化出來 0,我們沒有權利訪問最低的地址0,MMU這個硬件經過 轉化或權限 方面上, 發現根本不能轉化出來這個地址,使得MMU【硬件】報錯,即CPU內部出錯【與上面的類似】,OS就會知道,OS就會向目標進程去識別,就會殺掉這個進程。也要類似 狀態寄存器的東西,轉化錯誤,所以就會死循環。
OS怎么知道我們的進程內部出錯了?程序內部的錯誤,其實都會表現在硬件錯誤上,OS就會知道是哪個地方出錯,進而給對應的目標進程發送信號。
🌠core VS Term?
Term :正常終止進程【不需要進行debug】。
Core:也是終止進程,但會多做一些處理。核心轉儲,在當前目錄下形成文件【pid.core】,OS在進程崩潰的時候,將進程在內存中的部分信息保存起來【保存在磁盤上,持久化起來】,方便后續調試!這個文件【pid.core】一般都會被云服務器關掉。
把core功能關閉的原因:在云服務器上,若野指針、除 0 出現錯誤等等,一般都會在后端立即重啟這個服務。若不關閉,當某個錯誤,一直重復 重啟后掛掉,磁盤上就會出現大量的core文件,甚至磁盤爆滿,就會影響系統 或 某些應用服務直接就掛掉,即 會因為系統問題,導致磁盤爆滿。
?core 的使用 --- 調試
🌠 如果是子進程異常了呢? core 會不會出現?
子進程會不會出現core取決于:退出信號是否終止動作 && 服務器是否開啟 core 功能。
🌠小結:
? 鍵盤、系統指令、系統調用、軟件條件、異常 的信號產生,最終都要有OS來執行,因為OS是進程的管理者,統一由OS來向目標進程發信號;
? 信號的處理方式:不是立即處理,而是在合適的時候處理;
? 信號如果不是被立即處理,信號就會暫時被進程記錄下來,保存在進程對應的PCB中的信號位圖;
? 一個進程在沒有收到信號的時候,知道自己應該對合法信號作何處理:默認、忽略、自定義;
? OS向目標進程發送信號:實則是OS向目標進程寫信號。
?五、信號處理
1. 默認處理動作
void handler(int signo)
{std::cout << "get a signal: " << signo << std::endl;exit(1);
}int main()
{signal(2, SIG_DFL);// default:默認while(true){pause();}
}
2. 忽略:本身就是一種信號捕捉的方法,動作就是忽略
void handler(int signo)
{std::cout << "get a signal: " << signo << std::endl;exit(1);
}int main()
{signal(2, SIG_IGN);// 忽略(不做處理),本身就是一種信號捕捉的方法,動作就是忽略while(true){pause();}
}
?3. 自定義 捕捉一個信號
void handler(int signo)
{std::cout << "get a signal: " << signo << std::endl;exit(1);
}int main()
{// 信號捕捉:// 1. 默認// 2. 忽略// 3. 自定義::signal(2, handler);// 自定義while(true){pause();}
}
如若對你有幫助,記得關注、收藏、點贊哦~ 您的支持是我最大的動力🌹🌹🌹🌹!!!
若有誤,望各位,在評論區留言或者私信我 指點迷津!!!謝謝 ヾ(≧▽≦*)o? \( ?? ω ?? )/