Linux--守護進程

守護進程也稱爲精靈進程(Daemon),是運行在後臺的一種特殊進程。它獨立於控制終端並且週期性地執行某種任務或等待處理某些發生的事件。守護進程是一種很有用的進程。Linuxd的大多數服務器就是用守護進程實現的。比如:Internet服務器inetd,Web服務器httpd等。同時,守護進程完成許多系統任務,比如,作業規劃進程crond等。
我們想要了解守護進程,首先要了解一些基礎知識。

進程組,作業,會話 概念

進程組

進程組(Process Group)是一個或多個進程的集合。每個進程除了有一個進程ID之外,還屬於一個進程組。每個進程組有一個唯一的進程ID,每個進程組都可以有一個組長進程。組長進程的標識是,其進程組ID等於其進程ID。
注意:組長進程可以創建一個進程組,創建該組中的進程,然後終止。只要在某個進程組中一個進程存在,則該進程組就存在,與其組長是否終止無關。

作業

Shell分前後臺控制的不是進程而是作業(Job)進程組。一個前臺作業可以由多個進程組成,一個後臺作業也可以由多個進程組成。
作業控制:Shell可以運行一個前臺作業和任意多個後臺作業。
作業與進程組的區別:如果作業中某個進程有創建了子進程,則子進程不屬於作業,它屬於進程組。

會話

會話(Session):是一個或多個進程組的集合。
一個會話可以有一個控制終端。這通常是登陸到其上的終端設備(在終端登陸情況下)或僞終端設備(在網絡登陸情況下)。建立與控制終端連接的會話首進程被稱爲控制進程
⼀個會話中的⼏個進程組可被分爲⼀個前臺進程組以及⼀個或多個後臺進程組。所以⼀個
會話中,應該包括控制進程(會話⾸進程),⼀個前臺進程組和任意後臺進程組。

$proc1 | proc2 &
$proc3 | proc4 |proc5

其中proc1與proc2屬於同一個後臺作業,proc3,proc4和proc5屬於同一個前臺作業,
bash本身屬於一個單獨的作業。這些作業的控制終端相同,它們同屬於一個會話,當用戶在控制終端輸入特殊的控制鍵(如Ctrl+C,產生SIGINT,Ctrk+,產生IGQUIT,Ctrl+Z,產生SIGTSTP),內核發送相應的信號給前臺作業中的所有進程。

終端

在UNIX系統中,用戶通過終端登錄系統後得到一個Shell進程,這個終端成爲Shell進程的控制終端 (Controlling Terminal),控制終端是保存在PCB中的信息,而我們知 道fork會複製PCB中的息,因此由Shell進程啓動的其它進程的控制終端也是這個終端。默認情況 下(沒有重定向),每個進程的標準輸入、標準輸出和標準錯誤輸出都指向控制終端,進程從標準 輸入讀也就是讀用戶的鍵盤輸入,進程往標準輸出或標準錯誤輸出寫也就是輸出到顯示器上。此外 在控制終端輸入一些特殊的控制鍵可以給前臺進程發信號,例如Ctrl-C表 示SIGINT,Ctrl-\表示SIGQUIT。
每個進程都可以通過一個特殊的設備文件/dev/tty訪問它的控制終 端。事實上每個終端設備都對應一個不同的設備文件,/dev/tty提供了一個通用的接口,一個進程 要訪問它的控制終端既可以通過/dev/tty也可以通過該終端設備所對應的設備文件來訪 問。ttyname函數可以由文件描述符查出對應的文件名,該文件描述符必須指向一個終端設備而不 能是任意文件。
下面我們通過實驗看一下各種不同的終端所對應的設備文件名。

#include<stdio.h>
#include<unistd.h>

int main()
{
    printf("fd: %d -> %s\n",0,ttyname(0));
    printf("fd: %d -> %s\n",1,ttyname(1));
    printf("fd: %d -> %s\n",2,ttyname(2));
    return 0;
}

這裏寫圖片描述

守護進程

概念

守護進程也稱爲精靈進程(Daemon),是運行在後臺的一種特殊進程。它獨立於控制終端並且週期性地執行某種任務或等待處理某些發生的事件。守護進程是一種很有用的進程。Linuxd的大多數服務器就是用守護進程實現的。比如:Internet服務器inetd,Web服務器httpd等。同時,守護進程完成許多系統任務,比如,作業規劃進程crond等。
linux系統啓動時會啓動很多系統服務進程,這些系統服務進程沒有控制終端,不能直接和用戶交互。其它進程都是在用戶登錄或運行程序時創建,在運行結束或用戶註銷時終止,但系統服務進程不受用戶登錄註銷的影響,它們一直在運行着。這種進程有一個名稱叫守護進程(Daemon)。

守護進程的特點

  1. 所有的守護進程都沒有控制終端,其終端名(TTY)設置爲問號(?)。
  2. 自成會話,自成進程組。不與其他會話或進程組相互關聯,干擾。所以一般一個守護進程的進程ID,組ID,會話ID都相同。(自成進程組這點說的也不太嚴謹,若父進程是守護進程,父進程fork的子進程也是守護進程。這時父子進程屬於同一進程組)
  3. 命令以‘d’結尾。
  4. 守護進程不受用戶登錄註銷的影響,當你註銷或者重登後,守護進程一直在運行。
  5. 生存期長,在系統引導裝入時啓動,僅在系統關閉時終止。
  6. 在後臺運行(原因可歸結於沒有控制終端)。
  7. 大多數的守護進程都以root特權運行。

系統中的進程

下面呢我們通過ps axj這個命令來查看一下系統中的進程:參數a表示不僅列當前用戶的進程,也列出所有其他用戶的進程,參數x表示不僅列有控制終端的進程,也列出所有無控制終端的進程,參數j表示列出與作業控制相關的信息。
這裏寫圖片描述
我們可以看到,凡是TGPID一欄寫着-1的都是沒有控制終端的進程,也就是守護進程。在COMMAND一列用[]括起來的名字表示內核線程,這些線程在內核裏創建,沒有用戶空間代碼,因此沒有程序文件名和命令行, 通常採用以k開頭的名字,表示Kernel。 init進程我們已經很熟悉了,udevd負責維護/dev目錄下的設備文件,acpid負責電源管理,syslogd負責維護/var/log下的日誌文件,可以看出,守護進程通常採用以d結尾的名字,表示Daemon。

創建守護進程

函數setsid,創建守護進程很關鍵的一步就是調用setsid函數創建一個新的會話(Session),並讓當前的進程稱爲這個會話的Leader,即會話首進程。

#include<stdio.h>
pid_t setsid(void)

返回值:成功返回進程組ID,失敗返回-1。
函數的結果爲:

  1. 創建一個新會話,使該進程變成新會話的會話首進程(會話首進程也可理解爲創建會話的進程),此時,該進程是當前會話的唯一進程。
  2. 使該進程稱爲一個新進程組的組長進程,進程組ID就是該進程的進程ID。
  3. 使該進程沒有控制終端,如果在調用setsid之前有,那麼就切斷控制終端與當前進程的聯繫。
  4. 當前進程的進程ID,進程組ID,會話ID都相等。

    注意:
    setsid函數調用之前有一個特殊要求:該進程不能是一個進程組的組長進程。如果是,函數調用將返回出錯。所以我們通常爲了防止出現這種情況會用以下做法:讓一個進程fork出一個子進程,然後立即將父進程終止,而子進程繼續。子進程的進程ID是新分配的,子進程的PCB集成了父進程的進程組ID,所以兩者不可能相等。這樣就保證了子進程不是一個進程組的組長。

創建守護進程的具體步驟

  1. 用umask將文件屏蔽字設置爲0
    文件權限掩碼是屏蔽掉文件權限中的對應位。由於使用fork()函數新創建的子進程繼承了父進程的文件權限掩碼,這就給該子進程使用文件帶了很多的麻煩(比如父進程中的文件沒有執行文件的權限,然而在子進程中希望執行相應的文件這個時候就會出問題)。因此在子進程中要把文件的權限掩碼設置成爲0,即在此時有最大的權限,這樣可以大大增強該守護進程的靈活性。

  2. 調用fork,父進程進行退出
    原因:
    (1)如果該守護進程是作爲一條簡單的shell命令啓動的,那麼⽗父進程終止使得shell認爲該命令已經執行完畢。
    (2)並且該操作保證子進程不是一個進程組的組長進程。

  3. 用setsid創建一個新的會話session
  4. 將當前工作目錄更改爲根目錄
    防止當前目錄有一個目錄被刪除,導致守護進程無效。 這是因爲使用fork()創建的子進程是繼承了父進程的當前工作目錄。然而在進程運行中,當前目錄所在的文件系統是不能卸載的,這對以後使用會造成很多的麻煩。因此通常的做法是讓“/”作爲守護進程的目錄,當然也可以指定其他的別的目錄來作爲守護進程的工作目錄。
  5. 關閉不需要的文件描述符
    同文件權限碼一樣,用fork()函數新建的子進程會從父進程那裏繼承一些已經打開了的文件。這些文件被打開的文件可能永遠不會被守護進程讀寫,如果不進行關閉的話將會浪費系統的資源,造成進程所在的文件系統無法卸下以及引起預料的錯誤。
  6. 忽略SIG_CHLD信號

代碼實現

#include<stdio.h>
#include<unistd.h>
#include<signal.h>
#include<stdlib.h>
#include<fcntl.h>
#include<sys/stat.h>

void create_daemon()
{
    umask(0);   //1.設置umask爲0
    pid_t pid = fork();  //2.fork出子進程,終止父進程後,子進程調用setsid創建新的會話
    if(pid < 0)
    {
        perror("pid");
        return ;
    }
    else if(pid > 0)
    {
        exit(0);//終止父進程
    }
    setsid();

    //3.設置當前的工作目錄爲根目錄
    if(chdir("/") < 0)
    {
        perror("chdir error");
        return;
    }
    //4.關閉文件描述表
    close(0);
    close(1);
    close(2);
    //5.忽略SIG_CHLD信號
    signal(SIGCHLD,SIG_IGN);
}

int main()
{
    create_daemon();
    while(1)
        sleep(1);
    return 0;
}

運行結果:
這裏寫圖片描述

daemon()函數創建守護進程

上面爲我們創建守護進程的一種方式,其實Linux爲我們提供了專門的函數接口來創建守護進程。

#include <unistd.h>
int daemon(int nochdir,int noclose);

參數:
第一個參數nochdir如果設置爲0的話表示將工作目錄改爲根目錄。
二個參數noclose如果設置爲0的話就將文件描述符重定向到/dev/null文件。
代碼實現:

#include <stdio.h>
#include <unistd.h>

int main()
{
    daemon(0,0);
    while(1);
    return 0;
}

創建守護進程fork兩次??

(1)調用一次fork的作用:
第一次fork的作用是讓shell認爲這條命令已經終止,不用掛在終端輸入上,還有就是爲了後面的setsid服務,因爲調用setsid函數的進程不能是進程組組長,如果不fork出子進程,則此時的父進程是進程組組長,就無法調用setsid。當子進程調用完setsid函數之後,子進程是會話組長也是進程組組長,並且脫離了控制終端,此時,不管控制終端如何操作,新的進程都不會收到一些信號使得進程退出。
(2)第二次fork的作用:
雖然當前關閉了和終端的聯繫,但是後期可能會誤操作打開了終端。
只有會話首進程能打開終端設備,也就是再fork一次,再把父進程退出,再次fork的子進程作爲守護進程繼續運行,保證了該精靈進程不是對話期的首進程,
第二次不是必須的,是可選的,市面上有些開源項目也是fork一次

發佈了103 篇原創文章 · 獲贊 53 · 訪問量 15萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章