信號
信號,什么是信號?
在現實生活中,鬧鐘,紅綠燈,電話鈴聲等等;這些都是現實生活中的信號,當鬧鐘想起時,我就要起床;當電話鈴聲想起時,我就知道有人給我打電話,就要接聽電話;
現實生活中的這些信號,我們接收到之后就要停止當前正在做的事;所以可以說:**號是發送給進程的,而信號是一種事件通知的異步通知機制
在計算機操作系統中,信號是發送給進程的,而信號是一種事件通知的異步通知機制
簡單來說就是,進程在沒有收到信號時,在執行自己的代碼;信號的產生和進程的運行,是異步的
同步異步
這里簡單了解以下同步異步:
同步: 任務按照順序執行,前面任務沒有完成,后面任務就要阻塞等待;
異步: 多個任務可以同時執行,也就是說事件可以同時發生。
相關概念
在深入探究信號之前,先來了解信號相關的概念:
- 在沒有產生信號時,進程就已經知道如何處理信號了
就像在現實生活中一樣,在鬧鐘沒有響之前,我們就知道鬧鐘響了就要起床了。
- 信號處理,可以立即處理,也可以過一段時間再處理(在合適的時候處理)
- 進程當中早已內置了對于信號的識別和處理
我們知道操作系統也是程序員寫的,在設計寫操作系統時,進程當中已內置了如何接受信號和處理信號。
- 信號源非常多
信號是發送給進程的,那信號是誰產生發送給進程的呢?
信號的產生源非常多,就比如Ctrl + C
,Ctrl + \
,kill
指令都是給進程發送信號。
信號分類
簡單了解了信號是什么,那在Linux
系統中都存在哪些信號呢?如何查看這些信號呢?
kill -l
命令用來查看所有的信號:
可以看到一共有62
個信號,對于這62
個信號可以粗略的分為兩部分:
1 - 31
號信號:這部分信號可以不被立即處理(非實時信號)34 - 64
號信號:這部分信號必須被立即處理(實時信號)
信號處理
信號從產生到處理,可以分為信號產生、信號保存、信號處理三個階段;
進程對于信號的處理方式有三種:
- 默認處理:
SIG_DFL
,進程處理信號的默認處理方式就是終止進程。- 自定義處理:我們可以修改進程對于信號的處理方式。
- 忽略處理:
SIG_IGN
信號產生
了解了信號是發送給進程的,那信號是如何產生的呢?
1. 通過終端按鍵(鍵盤)產生信號
在之前,我們通過Ctrl + C
可以終止進程,為什么呢?
這就是因為Ctrl + C
本質上就是向目標進程發送信號,而進程對于相當一部分信號的處理方式都是終止進程。
Ctrl + C
是向進程發送幾號信號呢?
這里Ctrl + C
是向進程發送2
號信號。
系統調用signal
signal
用來替換進程某種信號的默認處理方式;
存在兩個參數:signum
表示要替換信號的數字標號handler
是函數指針類型,表示要替換的函數
#include <iostream>
#include <unistd.h>
#include <signal.h>
void handler(int sig)
{std::cout << "收到信號 " << sig << std::endl;
}int main()
{signal(2, handler);int cnt = 0;while (true){printf("cnt : %d, pid : %d\n", cnt++, getpid());sleep(1);}return 0;
}
可以看到,進程在收到2
號信號之后,沒有執行默認處理方式,而是執行handler
函數。
按Ctrl + C
就是給進程發送2
號信號。
這里按Ctrl + C
是給進程發送2
號信號,除此之外Ctrl + \
是發送3號信號、Ctrl + Z
是發送20號信號。
這里就將進程對于1 - 31
號信號的處理方式都替換成自定義處理:
#include <iostream>
#include <unistd.h>
#include <signal.h>
void handler(int sig)
{std::cout << "receive signal : " << sig << std::endl;
}
int main()
{for (int i = 1; i < 32; i++)signal(i, handler);int cnt = 0;while (true){printf("cnt : %d, pid : %d\n", cnt++, getpid());sleep(1);}return 0;
}
可以看到Ctrl + c
、Ctrl + \
能夠讓進程退出就是給目標進程發送對應的信號,而進程對于信號的處理發送就是終止進程。
這里就存在一些疑問:
- 進程將對于所有的信號的處理方式都替換成自定義處理,那信號就不能殺死進程了?
Ctrl + c
、Ctrl + \
是給目標進程發送信號,那目標進程是什么呢?
首先,在信號在存在一些信號,是不能替換進程對于該信號的處理方式的,例如9
號信號(上面的進程我們依然可以發送9
號信號殺掉該進程)。
目標進程
Ctrl + c
這種通過鍵盤來給目標進程發送信號,那什么是目標進程呢?簡單來說就是前臺進程。
前臺/后臺進程
如上圖所示,直接啟動程序,默認是在前臺運行的,這時我們輸入指令,沒有任何反應;
在程序退出后,命令才得以執行。
而在啟動程序時,讓程序在后臺進程;也就是進程在后臺運行,此時輸入命令行指令,指令可以被執行。
前臺進程只有一個,后臺進程可以存在多個
這是因為,鍵盤輸入只有一個,也就是同時只能存在一個進程讀取鍵盤輸入的數據;也就是前臺進程。
而多個進程能夠同時向一個顯示器文件中寫入,也就是輸出到屏幕中。
相關操作
關于前臺進程和后臺進程,我們可以進程查看后臺進程、將后臺進程變成前臺進程、暫停前提正在運行的進程(讓它變成后臺)、以及讓后臺進程運行起來等一系列操作。
jobs
查看后臺進程
使用jobs
命令可以查看當前所有的后臺進程,可以查看到所有后臺進程的任務號、狀態等等信息。
fg
將后臺進程變成前臺進程
我們可以讓進程在后臺運行,也可以查看后臺進程;當然也可以將一個后臺進程變成前臺進程。
Ctrl + Z
暫停前臺進程
我們知道,Ctrl + Z
可以暫停目標進程,而Ctrl + Z
也是給目標進程發送信號;本質上來說Ctrl + Z
就是給前臺進程發送20
號信號
前臺進程被暫停之后,就會變成后臺進程
簡單來說就是,前臺進程要獲取我們用戶的輸入信息,前臺進程無法被暫停。
我們通過
Ctrl + Z
暫停一個前臺進程之后,該進程就會變成后臺進程了。
這里就不修改程序對于信號的處理方式了。
#include <iostream>
#include <unistd.h>
int main()
{while (true){std::cout << "pid : " << getpid() << std::endl;sleep(2);}return 0;
}
bg
讓后臺進程運行起來
前臺進程被暫停就會變成后臺進程,那處于暫停狀態的后臺進程呢?
我們可以通過bg
命令來讓一個暫停狀態的后臺進程運行起來。
OS如何管理硬件資源
先來看一下代碼:
int main()
{int x = 0;std::cout << "in begin" << std::endl;std::cin >> x;std::cout << "in sucess" << std::endl;return 0;
}
我們知道,在輸入cin/printf
時,程序就會等待我們輸入數據之后,才會接著運行;也就是說進程會等待鍵盤輸入數據,進程就從運行態到阻塞態(內核數據結構從CPU
運行隊列到鍵盤等待隊列)。
等待我們輸入數據時,進程才會繼續運行;
那進程是如何知道鍵盤上輸入數據了呢?
我們知道OS
管理軟硬件資源,所以操作系統肯定是知道鍵盤上是否存在數據的,那問題是:OS
是如何知道鍵盤存儲數據了呢?
這里并不是OS
定期排查,來看鍵盤是否有數據的;
簡單來說,就是當鍵盤當中存在數據時,鍵盤就會向
CPU
發送硬件中斷;在CPU
當中存在對應的針腳,CPU
通過識別高低鍛電壓來區別是否存在硬件中斷;當存在硬件中斷時,就CPU
就會執行操作系統處理數據的代碼;而OS
就會停止當前工作,將數據讀入內存。
2. 通過系統調用發送信號
信號可以由終端按鍵,例如Ctrl + C
目標進程發送信號;當然我們也可以通過系統調用來發送信號。
常用的系統調用有kill
、raise
、abort
等
kill
kill
系統調用可以給任意進程發送信號;
參數
pid
:指要發送信號給進程,進程的pid
sig
:指要發送幾號信號,信號的標號。
了解了kill
系統調用可以給任意進程發送信號,那就可以使用kill
來實現一個自己的kill
命令:mykill
//mykill.cc
#include <iostream>
#include <string.h>
#include <signal.h>
int main(int argc, char* argv[])
{if(argc !=3){return -1;}int id = std::stoi(argv[2]);char* str = argv[1]+1;int sig = std::stoi(str);kill(id,sig);return 0;
}
//test.cc
#include <iostream>
#include <unistd.h>
#include <signal.h>
void handler(int sig)
{std::cout << "receive signal : " << sig << std::endl;
}
int main()
{for (int i = 1; i < 32; i++)signal(i, handler);std::cout << "pid : " << getpid() << std::endl;while (true){sleep(1);}return 0;
}
raise
kill
系統調用可以給任意進程發送任意信號而
raise
是庫函數,它可以給進程自己發送任意信號。
簡單來說就是進程調用raise
,可以給自己發送任意信號。
#include <iostream>
#include <unistd.h>
#include <signal.h>void handler(int sig)
{std::cout << "receive signal : " << sig << std::endl;
}int main()
{for (int i = 1; i < 32; i++)signal(i, handler);for (int i = 1; i < 32; i++){if (i == 9 || i == 19)continue;std::cout << "send signal " << i << std::endl;raise(i);}return 0;
}
這里9
號信號和19
號信號無法進程自定義捕捉,就不發送9和19 號信號。
abort
kill
可以給任意進程發送任意信號、raise
可以給進程自己發送任意信號;而
abort
用來給進程自己發送特定的信號(6
號信號),來終止進程。
abort
的作用就是終止進程。
#include <iostream>
#include <unistd.h>
#include <signal.h>
void handler(int sig)
{std::cout << "receive signal : " << sig << std::endl;
}
int main()
{for(int i = 1; i<32;i++)signal(i,handler);std::cout << "pid : " << getpid() << std::endl;abort();while(1)sleep(1);return 0;
}
可以看到,abort
是給進程自己發送6
號信號;6
號信號是SIGABRT
。
但是這里是修改了進程對于1-32
的處理方式的(9
和19
無法修改),并且進程在收到abort
發送的6
號信號之后,是執行了自定義處理發送handler
的,那為什么進程還是退出了?
abort
函數的作用就是終止進程,這里就是修改了進程對于6
號進程的處理發送,但是abort
還是會終止進程。
3. 硬件異常
我們知道,當程序中存在/0
、野指針(越界訪問)時,進程就會直接退出;那進程是如何退出的呢?
答案就是信號,當程序出現錯誤時,OS
統就會給當前進程發送信號從而殺掉進程。
操作系統是如何知道程序出錯了呢?
當程序出錯時,操作系統會通過信號殺掉進程,那操作系統是如何知道程序出錯了呢?
例如
/0
,CPU
在執行/0
操作,寄存器就會發生浮點數溢出,就會觸發硬件中斷,從而執行OS
相關的方法。野指針同理,當進行野指針訪問時,
CPU
在執行時發出錯就會觸發硬件中斷,然后執行OS
相關方法。
除0
void handler(int sig)
{std::cout<<"recive signal : "<< sig << std::endl;exit(1);
}
int main()
{for(int i = 1; i<32;i++)signal(i, handler);//除0int x = 3;x/=0;while(true){}return 0;
}
野指針
void handler(int sig)
{std::cout<<"recive signal : "<< sig << std::endl;exit(1);
}
int main()
{for(int i = 1; i<32;i++)signal(i, handler);//野指針int* p = nullptr;*p = 1;//訪問nullptrwhile(true){}return 0;
}
子進程退出core dump
還記得當子進程退出時,存在一個退出碼;退出碼的低7
位指子進程被哪個信號殺死,而第8
位在標識進程是否被信號殺死。
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
void handler(int sig)
{std::cout << "recive signal : " << sig << std::endl;
}
int main()
{int id = fork();if (id < 0)exit(1);else if (id == 0){sleep(1);int x = 10;x /= 0;}for (int i = 1; i < 32; i++)signal(i, handler);int status = 0;waitpid(-1, &status, 0);printf("status : %d, exit signal: %d, core dump: %d\n", status, status & 0x7F, (status >> 7) & 1);return 0;
}
在上述代碼中,父進程創建子進程,子進程進行/0
操作,子進程就會被信號殺掉;
父進程修改對8(SIGFPE)
信號的處理方式,然后獲取子進程的退出信息。
在子進程的退出信息中,低7位存儲子進程被幾號信號殺掉,第8
位標識子進程是否被信號殺掉。
4. 軟件條件
軟件異常產生中斷,顧名思義進程軟件條件不滿足從而產生信號;
例如:進程間通過管道文件進行通信,讀端退出,OS
系統就會殺掉寫端;(通過發送信號讓寫端退出)。
這里簡單測試一下:
//process1.cc
#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#define PATHNAME "./fifo"
int main()
{// 創建管道文件mkfifo(PATHNAME, 0666);// 打開int rfd = open(PATHNAME, O_RDONLY);// 讀取char buff[1024];int cnt = 3;while (cnt--){int x = read(rfd, buff, sizeof(buff));buff[x] = 0;std::cout << "read : " << buff << std::endl;}// 關閉close(rfd);return 0;
}
//process2.cc
#include <iostream>
#include <fcntl.h>
#include <unistd.h>
#include <cstring>
#include <signal.h>
#define PATHNAME "./fifo"
void handler(int sig)
{std::cout << "receive signal : " << sig << std::endl;exit(1);
}
int main()
{for(int i =1 ;i<32;i++)signal(i,handler);// 打開管道文件int wfd = open(PATHNAME, O_WRONLY);// 寫入const char *msg = "abcd";while (true){write(wfd, msg, strlen(msg));sleep(1);std::cout << "write : " << msg << std::endl;}return 0;
}
alarm
先來看一下alarm
函數
alarm
只有一個參數seconds
,alarm
的作用就是給當前進程設置鬧鐘;
簡單來說就是,在seconds
秒后給進程發送信號。
對于alarm
的返回值,可能為0
,也可能是上次設置鬧鐘的剩余時間。
#include <iostream>
#include <unistd.h>
#include <signal.h>
void handler(int sig)
{std::cout << "recive signal : " << sig << std::endl;
}
int main()
{for(int i = 1;i<32;i++)signal(i,handler);alarm(3);sleep(5);return 0;
}
可以看到alarm
設置鬧鐘,就是在seconds
秒過后給進程發送14
號信號。
所以,我們就可以通過給進程設定鬧鐘,讓進程周期性的完成一些任務;
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <functional>
#include <vector>
void sch()
{std::cout << "process scheduling" << std::endl;
}
void check()
{std::cout << "memory check" << std::endl;
}
std::vector<std::function<void()>> task_list;
void handler(int sig)
{for (auto &e : task_list){e();}alarm(1);
}
int main()
{task_list.push_back(sch);task_list.push_back(check);signal(14, handler);alarm(1);while (true){pause();}return 0;
}
理解系統鬧鐘
系統鬧鐘,本質上就是操作系統給對應進程發送信號,所以操作系統本身就要具有定時的功能;(例如:時間戳)
而在OS
中,定時器也可能存在很多,如此多的定時器也要被管理起來;Linux
內核數據結構如下:
struct timer_list {struct list_head entry;unsigned long expires;void (*function)(unsigned long);unsigned long data;struct tvec_t_base_s *base;
};
可以看到timer_list
也是被鏈表鏈接起來的;其中還包括exipries
定時器超時時間和function
處理方式。
管理定時器,采用的是時間輪的方法,可以簡單理解成堆結構。
總結
簡單總結上述內容:
信號是事件的一種異步通知機制
信號產生的方式
終端按鍵
系統調用:
kill
、raise
、abort
硬件異常:/0
、野指針、子進程退出軟件條件:
alarm
、軟件條件不滿足