進程信號

信號量(system v信號量)

  • 作用:實現進程控制,也就是可以實現同步和互斥功能
  • 本質:計數器 + PCB等待隊列。計數器是指對資源的計數,也就是對所佔的資源進行+1,-1操作

實現互斥的方法

  • 前提條件:

信號量當中的資源計數器只有兩個取值,也就是0或者1。其中0表示當前資源不可用,1表示當前資源可用

  • 訪問:

當一個進程需要訪問一個臨界資源的時候,會先獲取信號量,預算信號量當中計數器的值。

  • 預算:對當前信號量中的計數器進行-1操作,然後判斷信號量當中的計數器是否小於0。

如果信號量當中的計數器值小於0,則表示信號量之前的值爲0,表示當前的臨界資源不可以被訪問,然後會將當前獲取信號量的進程放到PCB等待隊列中去,進行阻塞等待。
如果信號量當中的計數器值等於0,則表示信號量之前的值爲1,表示當前的臨界資源可以被訪問,然後對信號量當中的計數器進行-1操作,再訪問臨界資源。

  • 釋放:

如果臨界資源訪問完成,需要結束對臨界資源的訪問,然後對信號量當中的計數器進行+1操作,然後喚醒PCB等待隊列中的進程(先進先出)。

減一操作 --> p操作
加一操作 --> v操作

在這裏插入圖片描述

  • 實現同步的方式

同步即保證進程對臨界資源訪問的合理性,在實現同步的過程中,計數器的取值不再限制爲0或者1,而是任意整數。
初始化信號量:將信號量當中的資源計數器設置爲資源的數量。

  • 訪問臨界資源:

先訪問信號量:對資源計數器進行-1操作,判斷資源計數器的值是否大於0。如果小於0,則將該進程放在PCB等待隊列中;如果大於等於0,則訪問臨界資源。
在這裏插入圖片描述
以停車場爲例,資源計數器就是統計停車場還有多少空位置,PCB等待隊列就是表示車輛在停車場外排隊。
當車位佔滿的時候,表示臨界資源數量爲0,也就是資源計數器的值是0;這時如果還有車子想進入停車場,就會在車庫門前排隊,每排一輛車,就會對信號量中的資源計數器-1操作,如圖有3輛車在等待,資源計數器的值爲-3。

  • 釋放資源,通知PCB等待隊列的做法

當臨界資源被進程所釋放,就會對資源計數器進行+1操作,通知PCB等待隊列當中的的進程訪問臨界資源

  1. 計數器+1操作完畢之後,計數器當中的值小於等於0。這時需要通知PCB等待隊列當中的進程(因爲PCB等待隊列不爲空)。
  2. 計數器+1操作完畢之後,計數器當中的值大於0。就不需要通知PCB等待隊列(因爲PCB等待隊列本身就是空的)。

信號的基本概念

信號是一個軟件中斷,可以打斷當前正在運行的進程,讓該進程去處理信號的事件,當前的進程需要處理信號所帶來的的操作。就像我麼一看到紅燈就會下意識的進行等待,看到綠燈就會想到可以通行。

  • 信號的種類
    在linux操作系統中,總共有62種信號。前31種信號(1 - 31)被稱爲不可靠信號,信號有可能會丟失,非實時信號;後31種信號(34-64)被稱爲可靠信號,信號是不可能被丟失的,實時信號。
    在這裏插入圖片描述

信號的產生

硬件產生

  • ctrl + c:給前臺進程發送一個SIGINT信號(2號信號),中斷當前的進程。
  • ctrl + z:給前臺進程發送一個SIGTSTP信號(20號信號),暫停當前進程。
  • ctrl + |:給前臺進程發送一個SIGQUIT信號(3號信號),使進程崩潰,產生coredump文件

軟件產生

  • kill [pid]:給前臺進程發送一個SIGTERM信號(15號信號)
  • kill -[signalno] [pid]:給指定進程發送指定的信號
    就像kill -9 [pid]表示給進程發送SIGKILL信號(9號信號),強殺信號
#include <stdio.h>
#include <signal.h>
#include <unistd.h>

int main()
{
   printf("i am finfshed\n");
   kill(getpid(),2);//給當前進程發送一箇中斷信號
   while(1)
  {
     printf("如果打印到這裏,就說明有問題!\n");
     sleep(1);                                                                            
  }
  return 0;
}

調用函數產生

#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);

#include <stdlib.h>
void abort(void);//封裝kill函數,誰調用就給誰發送SIGABRY信號(6號信號)

#include <unistd.h>
unsigned int alarm(unsigned int seconds);
//設定一個鬧鐘,也就是告訴內核在seconds秒之後給當前進程發SIGALRM信號, 
//該信號的默認處理動作是終止當前進程。

在這裏插入圖片描述
還有當我們出現訪問空指針或者內存越界訪問的時候,就會出現11號信號,SIGSEGV段錯誤信號
在這裏插入圖片描述

man 7 signal //查看信號的作用

在這裏插入圖片描述

信號的註冊

  • 查看信號的源碼
    因爲在linux中默認是不安裝kernel的內核源碼,所以說我們在查看的時候,需要先將內核源碼安裝好。
sudo yum install kernel -devel -y

然後在root用戶下進行查找sched.h文件

 find /usr/src/ -name sched.h

在這裏插入圖片描述
因爲信號的不同頭文件下存在着一些引用的關係,所以說一些結構體的定義不在sched.h頭文件下,我們可以在查找之前,在/usr/src/kernels/3.10.0-1062.18.1.el7.x86_64/include/目錄下建立tags索引文件,然後可以使用tags的查找方式進行跨文件查找。
在這裏插入圖片描述

ctags -R //建立tags索引文件
在文件中可以使用ctrl + ] 獲取當前光標下的單詞作爲tag名字進行跳轉
ctrl + T //跳轉到前一次的tag處

然後用vim 打開這個目錄下的sched.h文件,使用/[關鍵字],然後就可以看到task_struct結構體了。下面是註冊碼的一些與信號有關的成員變量之間的關係。
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述

兩種註冊信號的情況

sigqueue隊列是內核當中維護的一個隊列,其隊列當中的每一個元素對應信號的一個處理節點。

  • 非可靠信號:(1 - 31)

會更改sig位圖中對應的字節爲1,在sigqueue隊列中增加對應信號所對應的節點
對於非可靠信號,當sigqueue隊列多次收到同樣一個信號的時候,只添加一個節點,也就是相當於第二次收到的同樣的信號被丟棄掉。

  • 可靠信號:(34 - 64)

當sigqueue隊列第一次收到一個可靠信號的時候,會更改sig位圖中對應的字節爲1,然後在隊列中增加對應信號所對應的節點。
而當第二次收到同樣一個信號的時候,先判斷sig位圖中的對應字節位置是否爲1,並且在sigqueue隊列中增加信號所對應的節點

兩種信號註銷的情況

  • 非可靠信號:(1 - 31)

會將sig位圖中對應的字節位置置0,並將在sigqueue隊列中的節點去除掉(操作系統拿着節點去行使該信號的功能)

  • 可靠信號:(34 - 64)

將待處理的信號在sigqueue隊列中的對應節點進行盤算,判斷當前處理的對應信號的節點是否在sigqueue隊列中還有相同類型的節點,
有:則不改變sig位圖的對應字節上的1,即不把1置爲0
沒有:則直接將sig位圖對應位置的1置爲0

信號捕捉

  • 信號的處理方式

默認處理方式:SIG_DEL (執行一個動作或者執行一個函數)
忽略時的處理:SIG_IGN (不會做任何事)
自定義處理方式:我們自定義的處理方式

問題1:爲什麼會產生殭屍進程?爲什麼父進程來不及回收子進程的退出信息,導致子進程爲殭屍進程?

殭屍進程的產生是因爲當子進程退出時,父進程沒有回收子進程的退出狀態,這個時候子進程退出的消息父進程沒有接收,子進程就成爲一個殭屍進程。至於爲什麼父進程來不及處理,是因爲子進程在退出的時候,會給父進程發鬆一個SIGCHID信號,而操作系統對SIGCHID信號的處理方式恰好爲忽略狀態(SIG_IGN),這時父進程就是不會做任何事的狀態。

問題2:從信號的角度來看,應該如何解決殭屍進程的問題?

  1. 使用signal(SIGCHLD,SIG_IGN),在這種方式下,子進程狀態信息會被丟棄,也就是自動回收了,所以不會產生殭屍進程。(爸爸不管兒子了,兒子自己釋放掉自己吧)
  2. 從其他角度看,我們可以fork兩次,讓第一次fork的子進程在fork完成後使用(exit)直接退出,這樣第二次fork得到的子進程就沒有父進程了,就變成了孤兒進程,它會被自動過繼給老祖宗init進程,init會負責釋放它的資源,這樣就不會由"殭屍"產生了(把原本的父子關係變成爺孫關係,然後直接殺掉中間的爸爸,爺爺也不管孫子了,孫子就成了孤兒,自己釋放自己)
  3. 因爲父子進程之間的搶佔式執行,所以說父子進程之間無法確定哪一個進程先執行,爲了防止子進程在退出的時候,父進程陷入循環無法得到子進程的退出狀態,可以在子進程進行的時候,讓父進程進行進程等待, 以便子進程可以正常退出。(這種方法,爸爸會照顧兒子一生,直至兒子死亡,並妥善處理完兒子的後事)
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
  • signum:需要我們自己定義的一個信號值
  • handler:一個函數指針類型的參數 --> typedef void (*sighandler_t)(int);
    • void:是返回值的類型
    • int:參數類型,即是哪一個信號觸發操作系統調用該函數

在這裏插入圖片描述
上面的代碼,我們在運行的時候,通過pstack [pid]查看的時候顯示是程序是在sleep()函數中
在這裏插入圖片描述
但是當我們採取硬件中斷ctrl+c 和 ctrl+\ 時,卻發現程序調用了signal函數。signal函數的這種使用方式是向系統註冊了一個函數,當發生某種特定的事件的時候,會回調之前註冊的函數,即回調函數。這裏的程序在運行的過程中其實可以說是並行運行的。
在這裏插入圖片描述

自定義信號的處理流程

前提

  1. 在task_struct結構體中,有一個指向sighand_struct的結構體指針,在該結構體當中有一個action結構體數組,這個數組每一個元素的類型都是struct k_sigation結構體類型,數組中的每一個元素都對應一個信號的處理邏輯。
  2. 在struct_sigaction結構體中有一個元素是struct sigaction sa,在struct sigaction結構體中有一個sighandler_t類型的元素,這個sighander_t是一個函數指針類型,typedef void(*sighandler)(int),保存信號默認執行的函數。

操作系統對信號的處理

  • 操作系統對默認信號的處理

當sig位圖中收到一個信號的時候,意味着sig位圖當中的某一個bite位置會被置爲1,操作系統處理該信號的時候,就會從PCB中找sighang_struct這個結構體的指針,從而找到sa_handler,進而操作系統內核回去調用sa_handler保存的函數地址,然後完成該信號的功能。

  • 自定義信號處理函數
    signal:相當於改掉了sa_handler保存的函數地址,也就是當收到自定義信號的時候,操作系統內核會去調用sa_handler保存的新的函數地址,而這個函數是我們自己定義的,調用之後就可以達到我們所期望的執行效果。
#include <signal.h>
int sigaction(int signum, const struct sigaction *act,
                struct sigaction *oldact);
//相當於更改掉了action數組當中的元素,相當於直接改掉了結構體,
//從而達到修改信號處理函數地址的目的
 struct sigaction {
       void     (*sa_handler)(int);
       void     (*sa_sigaction)(int, siginfo_t *, void *);
       sigset_t   sa_mask;
       int        sa_flags;
       void     (*sa_restorer)(void);
};
  • void (*sa_handler)(int); : 操作系統爲每一個信號自定義的默認調用函數

  • void (*sa_sigaction)(int, siginfo_t *, void *); : 一個函數指針,但是這個函數指針是預留的,需要配合sa_flags使用,當sa_flags爲SA_SIGINFO的時候,操作系統就會調用該函數指針當中保存的函數地址

  • sigset_t sa_mask; : 一般當前進程在處理信號的時候,可能還會收到新的信號,爲了有效地保存新的信號,就把新的信號放在sa_mask中。

  • int sa_flags; : 配合sa_sigaction使用

  • void (*sa_restorer)(void); : 預留信息

  • act:表示把signum信號修改爲act處理方式

  • oldact:表示是操作系統之前被signum所定義的處理方式

  • struct sigaction 相當於action[]數組中的元素類型(k_sigaction),signal函數就是調用sigaction函數實現的

signal(2,sigcallback);
while(1)
{
    sleep(1);
}

對於上面的代碼,程序正常情況下會一直執行sleep的循環

  1. 當程序收到ctrl + c,也就是2號信號的時候
  2. 當程序執行sleep函數時,從用戶態切換到內核態,然後執行內核代碼
  3. 執行完sleep的邏輯之後,需要調用do_signal函數去處理接受到的信號信息。此時如果程序沒有接收到2號信號,會直接調用sysreturn函數從內核態切換到用戶態;要是接收到2號信號,則會切換到用戶態去執行自定義的sigcallback函數。
  4. 執行完畢之後,調用sigreturn 函數切換回內核態,然後再次調用do_signal 函數,重複第3步的邏輯,直到當前程序接受到的信號被處理完畢。
  5. 確保當前程序沒有信號的中斷的時候,會調用sysreturn 函數返回用戶態繼續執行代碼。

在這裏插入圖片描述
程序進入內核態的情況

  1. 調用系統功能調用函數
  2. 調用庫函數的時候,庫函數底層要是調用了系統功能調用函數,就會進入內核態
  3. 程序訪問異常,就像空指針的訪問(11號信號),內存訪問越界(11號信號),double free(6號信號)。

是否可以使用free函數去釋放NULL指針呢?
free(NULL)並不會產生程序崩潰,也不會收到信號

信號阻塞

  • 前提

信號想要發生一個阻塞,得要在task_struct結構體當中保存了一個blocked位圖
在這裏插入圖片描述
信號的阻塞並不是說信號不可以被註冊,而是當收到一個阻塞的信號的時候,如果通過blocked位圖發現該信號被阻塞,就不會處理這個信號,但是他的阻塞不會影響信號更改pending位圖和增加sigqueue節點

  • 操作系統處理信號的邏輯

當程序從用戶態切換到內核態之後,處理do_signal函數的時候,發現收到了某個信號,想要處理這個信號之前,得先判讀block位圖當中對應信號的bite位是否爲1
blocked當中對應的bite位置爲1,則不處理該信號,sigqueue當中對應的信號的節點還是存在
blocked當中對應的bite位置爲0,則處理該信號
更改sigset_t位圖中某個bite位的值

#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
  • how:

SIG_BLOCK --> 設置某個信號爲阻塞狀態,用修改位圖來達到目的
SIG_UNBLOCK --> 設置某個信號爲非阻塞狀態
SIG_SETMASK --> 設置新的阻塞的sigset_t位圖

  • set:要設置的新的阻塞位圖
  • oldset:之前程序當中阻塞的位圖,出參

在這裏插入圖片描述
可以驗證一下我們之前關於可靠信號與非可靠信號在sigqueue隊列中的結論,非可靠信號收到多個相同的非可靠信號的時候,只會添加一次sigqueue節點,也就是隻處理一次;而可靠信號收到多個相同的信號的時候,會添加多個sigqueue節點,也就是每一個可靠信號都會被處理。
在這裏插入圖片描述
9,19號信號不可以被阻塞
源碼地址:
https://github.com/duchenlong/linux-text/blob/master/sigblock.c

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章