信號與進程

信號

一、基本概念

進程間常用的通信手段,如kill掉一個worker進程,master進程就會立即啓動一個新的worker進程,信號用來通知某一個進程發生了某個事情(突發事情),所以進程不知道什麼時候收到信號,也就是說信號是異步發生的,也被稱爲軟件中斷。

【信號如何產生】

  • 某個進程發給另一個進程或發給自己
  • 由內核(操作系統)發送給某個進程(ctrl+c 或 kill 或 內存訪問異常 或 除以0)

信號名字:以SIG開頭,如SIGHUP(終端斷開信號),也就是一些數字(正整數常量,系統頭文件中的宏)。

二、kill

kill其實是給進程發信號,能發多種信號,而不只是殺死進程的意思!

單純的kill,其實是給進程發送了SIGTERM信號(終止信號),也就是kill -15 PID。

【常用數字】(很多信號的缺省動作都是殺掉進程)

1 SIGHUP

2 SIGINT(類似ctrl+c)

9 無視代碼,直接kill

18 SIGCONT 使暫停的進程繼續(運行在後臺)

19 SIGSTOP 停止進程但後臺還在

20 SIGSTP 終端停止但後臺還在(類似ctrl+z)

三、某個信號出現時,3種方式處理:

  • 執行系統默認動作(絕大多數信號是殺進程);
  • 忽略此信號(無法忽略SIGKILL和SIGSTOP,也就是-9和-19);
  • 捕捉該信號(加入處理信號的函數)

四、Unix/Linux操作系統體系結構

類unix操作系統體系結構分爲2個狀態:用戶態和內核態

寫的程序不是運行在用戶態就是內核態,一般在用戶態;當程序要執行特殊代碼時,會自動切換到內核態(無需人爲介入)。

a)os(內核):控制硬件資源,提供應用程序運行環境

b)系統調用:就是一些系統函數

c)shell:bash(borne again shell)是shell的一種,linux上默認採用bash這種shell,bash是一個可執行程序,充當命令解釋器的作用,也就是把用戶輸入的命令翻譯給os。whereis bash,/bin/bash可以在bash裏運行一個bash,exit可以退出當前bash!

d)用戶態和內核態的切換(根據需要自動切換)

用戶態權限小,內核態權限大!

【爲什麼區分?】

  • 一般情況下運行在用戶態,權限小,不至於危害到其他部分;
  • 危害部分操作系統會進行統一管理,系統提供的這些接口就是爲了減少有限資源的訪問和使用上的衝突。

【什麼時候切換到內核態?】

  • 系統調用:如malloc(),printf();
  • 異常事件:比如來了個信號,在內核態中調用信號處理函數;
  • 外圍設備中斷:導致程序處理流程從用戶態跳到內核態。

五、signal函數範例(捕捉信號)

if(signal(SIGUSR1,sig_usr) == SIG_ERR)  
//系統函數,參數1:是個信號,參數2:是個函數指針,代表一個針對該信號的捕捉處理函數
{
    printf("無法捕捉SIGUSR1信號!\n");
}

這樣當kill -USR1 pid時,該進程會調用sig_usr這個信號處理函數:

void sig_usr(int signo)
{     
    if(signo == SIGUSR1)
    {
        printf("收到了SIGUSR1信號!\n");
    }
    else
    {
        printf("收到了未捕捉的信號%d!\n",signo);
    }
}

【問題】如果有一個全局變量,在main中和在信號處理函數中都調用,此時來了信號,先去執行了信號處理函數而改變了此值,就會影響該值在main中的計算結果?

可重入函數:所謂的可重入函數,就是我們在信號處理函數中調用它是安全的。

不可重入函數如:malloc、printf、給全局變量賦值的函數等。

在寫信號處理函數的時候,要注意的事項:

a)在信號處理函數中,儘量使用簡單的語句做簡單的事情,儘量不要調用系統函數以免引起麻煩;

b)如果必須要在信號處理函數中調用一些系統函數,那麼要保證在信號處理函數中調用的系統函數一定要是可重入的(有個表);

c)如果必須要在信號處理函數中調用那些可能修改errno值(出現一些錯誤系統的返回值)的可重入的系統函數,那麼就得事先備份errno值,從信號處理函數返回之前,將errno值恢復。

信號處理函數中一定一定一定要用可重入函數!

signal因爲兼容性和可靠性等一些歷史問題,不建議使用,用sigaction()函數代替!

 

進程

一、終端關閉時如何讓進程不退出

方法一:nginx進程攔截SIGHUP信號,告訴OS不要動

(1)用nohup啓動nginx,忽略SIGHUP信號,而且屏幕輸出重定位到當前目錄的nohup.out中;

nohup ./nginx 

(2)代碼中加入如下內容以忽略SIGHUP信號。

signal(SIGHUP,SIG_IGN);

此時關掉終端,父死子活,nginx的PPID爲1,TT爲?,變爲孤兒進程!

方法二:nginx進程和bash進程不在同一個session(其實就是創建了一個守護進程!)

(1)用setsid啓動nginx;

setsid ./nginx 

(2)代碼中如下:

#include <stdio.h>
#include <unistd.h>
 
int main(int argc, char *const *argv)
{       
    pid_t pid;    
    pid = fork(); 
    if(pid < 0)
    {
        printf("fork()進程出錯!\n");
    }
    else if(pid == 0)
    {
        printf("子進程開始執行!\n");
        setsid(); //新建立一個不同的session,但是進程組組長調用setsid()是無效的
        for(;;)
        {
            sleep(1); //休息1秒
            printf("子進程休息1秒\n");
        }
        return 0;
    }
    else
    {
        //父進程會走到這裏
        for(;;)
        {
            sleep(1); //休息1秒
            printf("父進程休息1秒\n");
        }
        return 0;
    }
    return 0;
}

二、後臺運行:在後面加 &

./nginx &

後臺運行能正常操作,如ls,cd等都可以顯示信息,所以ctrl+c也是不能終止進程的,只能fg切換到前臺在ctrl+c,當然前臺ls,cd等命令是沒有效果的。

關閉終端,進程停止,這不取決於進程在前臺還是後臺運行。

三、fork

1、fork概念

進程:一個可執行程序,執行起來就是一個進程,再執行起來一次又是一個進程(多個進程可以共享同一個可執行文件)。

其他解釋:程序執行的一個實例,用fork創建一個子進程,相當於創建含有相同一段的兩條執行通路。

fork()之後,是父進程fork()之後的代碼先執行還是子進程fork()之後的代碼先執行是不一定的,這個跟內核調度算法有關!

【問題】kill子進程,觀察父進程收到什麼信號?

【回答】用strace,父進程收到來自子進程的SIGCHLD信號,子進程隨後變爲殭屍進程,STAT爲Z+。殭屍進程佔用資源的,至少佔用pid號,系統中是有限制的,所以開發者要杜絕殭屍進程的存在!

2、殭屍進程的產生和解決

1)產生

在Unix系統中,子死(可能是被kill也可能只是結束了)父活,但父沒有調用(wait/waitpid)函數來進行額外的處置,子進程就會變成一個殭屍進程;

※ 殭屍進程已經被終止,不幹活了,但還沒有被內核丟棄,因爲內核認爲父進程可能還用子進程的一些信息。

2)解決

  • 重啓電腦;
  • 手工地把殭屍進程的父進程kill掉,殭屍進程就會自動消失;
  • SIGCHLD信號:一個進程被終止或者停止時,這個信號會被子進程發送給父進程;所以,對於源碼中有fork()行爲的進程,我們應該攔截並處理SIGCHLD信號。
//寫在父進程的一個死循環中!
pid_t pid = waitpid(-1,&status,WNOHANG);
//第一個參數爲-1,表示等待任何子進程
//第二個參數:保存子進程的狀態信息
//第三個參數:提供額外選項,WNOHANG表示不要阻塞,讓這個waitpid()立即返回
if(pid == 0)
//子進程沒結束,可能休息一秒再進入下次循環                       
    continue;
if(pid == -1)
//這表示這個waitpid調用有錯誤,立即返回
    return;
//走到這裏,表示子進程結束了,直接return
return;   

3、fork函數的進一步認識(寫時複製機制

fork()產生新進程的速度非常快,fork()產生的新進程並不複製原進程的內存空間,而是和原進程(父進程)一起共享一個內存空間,但這個內存空間的特性是“寫時複製”,也就是說:原來的進程和fork()出來的子進程可以同時、自由的讀取內存,但如果子進程(父進程)對內存進行修改的話,那麼這個內存就會複製一份給該其他進程單獨使用,以免影響到共享這個內存空間的其他進程使用。

如果有一個全局變量g_mygbltest,而且某個進程中對該值有改變動作,會導致父子進程內存分開(寫時複製機制),所以即使是全局變量,兩個g_mygbltest的值也是不同的(因爲是在不同的進程中)。

4、fork()回返回兩次

父進程中返回一次,子進程中返回一次。而且,fork()在父進程中返回的值和在子進程中返回的值是不同的,子進程的fork()返回值是0,父進程的fork()返回值是新建立的子進程的ID(所以返回pid是大於0的)。

5、fork失敗的可能性

也就是超過這些量fork進程會失敗!

  • 系統中進程太多:缺省情況最大的pid爲32767;
  • 每個用戶有個允許開啓的進程總數:sysconf(_SC_CHILD_MAX)查看,大約7788。

四、守護進程

1、回顧

  • 進程有對應的終端,如果終端退出,那麼對應的進程也就消失了,它的父進程是一個bash;
  • 終端被佔住了,輸入各種命令這個終端都沒有反應。

2、基本概念

一種長期運行的進程,這種進程在後臺運行,並且不跟任何的控制終端關聯。

【基本特點】

a)生存期長(不是必須,但一般這樣做),一般是操作系統啓動的時候他就啓動,操作系統關閉的時候它才關閉;

b)守護進程跟終端無關聯,也就是說他們沒有控制終端,所以你控制終端退出,也不會導致守護進程退出;

c)守護進程是在後臺運行,不會佔着終端,終端可以執行其他命令。

linux操作系統本身是有很多的守護進程在默默的運行,維持着系統的日常活動。大概30-50個。

a)ppid = 0:內核進程,跟隨系統啓動而啓動,聲明週期貫穿整個系統;

b)CMD列名字帶[ ]這種,叫內核守護進程;

c)老祖init:也是系統守護進程,它負責啓動各運行層次特定的系統服務;所以很多進程的PPID是init,而且這個init也負責收養孤兒進程;

d)CMD列中名字不帶[ ]的普通守護進程(用戶級守護進程)

【共同點總結】

  • 大多數守護進程都是以超級用戶特權運行的
  • 守護進程沒有控制終端,TT這列顯示?
  • 內核守護進程以無控制終端方式啓動
  • 普通守護進程可能是守護進程調用了setsid的結果(無控制端)

3、守護進程編寫規則

a)調用umask(0):umask是個函數,用來限制(屏蔽)一些文件權限的。

b)fork()一個子進程(脫離終端)出來,然後父進程退出(把終端空出來,不讓終端卡住),固定套路。fork()的目的是想成功調用setsid()來建立新會話,目的是子進程有單獨的sid(因爲進程組組長沒法setsid),這樣,子進程成爲了一個新進程組的組長進程,子進程也不關聯任何終端了。

c)守護進程雖然可以通過終端啓動,但是和終端不掛鉤。守護進程是在後臺運行,它不應該從鍵盤上接收任何東西,也不應該把輸出結果打印到屏幕或者終端上來。一般按照江湖規矩,我們要把守護進程的標準輸入和標準輸出重定向到空設備(黑洞,/dev/null),從而確保守護進程不從鍵盤接收任何東西,也不把輸出結果打印到屏幕。

d)守護進程可以用命令啓動,如果想開機啓動,則需要藉助系統初始化腳本來啓動。

4、守護進程不會受到的信號

1)SIGHUP信號:守護進程不會收到來自內核的 SIGHUP 信號,潛臺詞就是如果守護進程收到了 SIGHUP 信號,那麼肯定是另外的進程發給來的(SIGHUP是Session Leader發給其他進程的,守護進程不關聯終端,所以不會受到)。

很多守護進程把這個信號作爲通知信號,表示配置文件已經發生改動,守護進程應該重新讀入其配置文件。

如在nginx中,就是用SIGHUP信號來通知會話首進程(master)配置文件有變動,需要重啓4個worker進程。

sudo ./nginx -s reload //執行這行後,重啓4個worker進程
等價於
sudo kill -1 master進程號

2)SIGINT、SIGWINCH信號

守護進程不會收到來自內核的 SIGINT(ctrl+c),SIGWINCH(終端窗口大小改變)信號,所以可以拿來自己用。

5、守護進程和後臺進程的區別

  • 守護進程和終端不掛鉤,後臺進程能往終端上輸出東西(如 printf 照樣打印,是和終端掛鉤的);
  • 守護進程關閉終端時不受影響,後臺進程會隨着終端的退出而退出。

參考網易雲課堂@KuangXiang,自學用,侵刪!

 

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