在前面的文章中,我們的服務器跑起來之後,實際上並不是會一直在運行,為什麼這麼説呢?因為受我們XShell的限制,只要我們將XShell關閉,我們運行的服務器也就中斷了。現實中我們使用的軟件肯定不是這樣的,這些軟件的服務器正常情況下會在主機上一直運行。所以我們這篇文章就來介紹一下如何做到
文章目錄
- 1. 進程組
- 1.1 什麼是進程組
- 1.2 組長進程
- 2. 會話
- 2.1 什麼是會話
- 2.2 如何創建會話
- 2.3 會話ID(SID)
- 3. 控制終端
- 4. 作業控制
- 4.1 什麼是作業(job)和作業控制(Job Control)?
- 4.2 作業號
- 在這裏插入圖片描述
- 4.3 作業的掛起與切回
- 4.4 查看後台執行或掛起的作業
- 4.5 作業控制相關的信號
- 在這裏插入圖片描述
- 5. 守護進程
- 6. 守護進程 vs 後台進程
1. 進程組
1.1 什麼是進程組
之前我們提到了進程的概念, 其實每一個進程除了有一個進程ID(PID)之外,還屬於一個進程組。進程組是一個或者多個進程的集合,一個進程組可以包含多個進程。 每一個進程組也有一個唯一的進程組ID(PGID), 並且這個PGID類似於進程ID,同樣是一個正整數,可以存放在pid_t數據類型中。
如下代碼:
#include <iostream>
#include <unistd.h>
int main()
{
while(true)
{
std::cout << "hello server" <<std::endl;
sleep(1);
}
return 0;
}
編譯運行後通過ps指令查看
注意:
# -e 選項表⽰every的意思, 表示輸出每一個進程信息
# -o 選項以逗號操作符(,)作為定界符, 可以指定要輸出的列
可以看到進程組pgid和進程pid相等,那是因為單個進程自己獨立成為進程組
1.2 組長進程
每一個進程組都有一個組長進程。 組長進程的ID等於其進程ID。我們可以通過ps命令看到組長進程的現象:
ltx@My-Xshell-8-Pro-Max-Ultra:~$ ps -o pid,pgid,ppid,comm | cat
PID PGID PPID COMMAND
634309 634309 634308 bash
634381 634381 634309 ps
634382 634381 634309 cat
從結果上看ps進程的PID和PGID相同, 那也就是説明ps進程是該進程組的組長進程, 該進程組包括ps和cat兩個進程。
- 進程組組長的作用: 進程組組長可以創建一個進程組或者創建該組中的進程
- 進程組的生命週期: 從進程組創建開始到其中最後一個進程離開為止。注意: 只要某個進程組中有一個進程存在, 則該進程組就存在, 這與其組長進程是否已經終止無關。此時的進程組被稱為 ”孤兒進程組“
2. 會話
2.1 什麼是會話
剛剛我們談到了進程組的概念, 那麼會話又是什麼呢? 會話其實和進程組息息相關, 會話可以看成是一個或多個進程組的集合, 一個會話可以包含多個進程組。每一個會話也有一個會話ID(SID)
通常我們都是使用管道將幾個進程編成一個進程組。 如上圖的進程組2和進程組3可能是由下列命令形成的:
ltx@My-Xshell-8-Pro-Max-Ultra:~$ sleep 1000 | sleep 2000 | sleep 3000 &
[1] 638455
# &表⽰將進程組放在後台執⾏
我們通過ps指令查看
# 過濾sleep相關的進程信息
ltx@My-Xshell-8-Pro-Max-Ultra:~$ ps axj | head -n1 && ps axj | grep sleep | grep -v grep
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
634309 638453 638453 634309 pts/0 638653 S 1004 0:00 sleep 1000
634309 638454 638453 634309 pts/0 638653 S 1004 0:00 sleep 2000
634309 638455 638453 634309 pts/0 638653 S 1004 0:00 sleep 3000
# a選項表⽰不僅列當前用户的進程,也列出所有其他用户的進程
# x選項表⽰不僅列有控制終端的進程,也列出所有⽆控制終端的進程
# j選項表⽰列出與作業控制相關的信息, 作業控制後續會講
# grep的-v選項表⽰反向過濾, 即不過濾帶有grep字段相關的進程
從上述結果來看3個進程對應的PGID相同, 即屬於同一個進程組。同樣3個進程的SID也相同,因為屬於同一個進程組肯定也屬於同一個會話
2.2 如何創建會話
可以調用setseid函數來創建一個會話, 前提是調用進程不能是一個進程組的組長。
#include <unistd.h>
/*
*功能:創建會話
*返回值:創建成功返回SID, 失敗返回-1
*/
pid_t setsid(void);
該接口調用之後會發生:
- 調用進程會變成新會話的會話首進程。此時,新會話中只有唯一的一個進程
- 調用進程會變成進程組組長。新進程組ID就是當前調用進程ID
- 該進程沒有控制終端。如果在調用setsid之前該進程存在控制終端, 則調用之後會切斷聯繫
需要注意的是: 這個接口如果調用進程原來是進程組組長,則會報錯,為了避免這種情況, 我們通常的使用方法是先調用fork創建子進程,父進程終止,子進程繼續執行,因為子進程會繼承父進程的進程組ID,而進程ID則是新分配的,也就意味此時子進程不可能是進程組長,就不會出現錯誤的情況。
2.3 會話ID(SID)
上邊我們提到了會話ID, 那麼會話ID是什麼呢? 我們可以先説一下會話首進程, 會話首進程是具有唯一進程ID的單個進程,那麼我們可以將會話首進程的進程ID當做是會話ID。注意:會話ID在有些地方也被稱為會話首進程的進程組ID, 因為會話首進程總是一個進程組的組長進程, 所以兩者是等價的。
下面我們來詳細介紹一下會話首進程
會話首進程 (session leader) 是創建該會話的進程,或者更準確地説,它是會話中第一個進程,並且該進程的進程ID就是會話ID(SID)。在Unix/Linux系統中,一個會話是一個或多個進程組的集合,通常與一個終端(控制終端)相關聯。
會話首進程的關鍵特性:
- 創建會話:通過調用setsid(),一個進程會創建一個新會話,併成為該會話的首進程。調用setsid()的進程不能是進程組組長,因此通常先fork一個子進程,然後讓子進程調用setsid()。
- 會話ID(SID):會話ID就是會話首進程的進程ID(PID)。因此,會話首進程的PID和SID是相同的。
- 進程組組長:會話首進程同時也是一個進程組的組長,其進程組ID(PGID)等於它的PID(也就是SID)。
- 控制終端:一個會話可以有一個控制終端,通常是用户登錄時使用的終端設備。會話首進程通常是打開控制終端的進程,但並不是必須的。不過,如果會話有控制終端,那麼會話首進程通常是與控制終端建立連接的進程。
- 終端斷開信號:當控制終端斷開連接(例如,終端窗口關閉)時,會話首進程會收到SIGHUP信號。通常,會話首進程會處理這個信號,比如終止會話中的所有進程。
會話首進程的作用:
- 管理會話:會話首進程通常用於管理整個會話的生命週期。例如,當用户註銷時,系統會向會話首進程發送SIGHUP信號,會話首進程通常會將這個信號轉發給會話中的所有進程組,然後終止會話。
- 作業控制:在shell中,每個作業(一個命令或管道序列)通常是一個進程組,而shell本身是會話首進程。shell使用會話和進程組來管理作業控制,比如前後台作業的切換。
示例説明:
在一個典型的登錄環境中:
- 用户登錄時,系統啓動一個登錄shell,該shell成為新會話的首進程。它的PID就是會話ID。
- 當在這個shell中運行命令時,每個命令(可能是多個進程通過管道連接)會被放入一個新的進程組。但所有這些進程組都屬於同一個會話。
- 如果shell退出,會話首進程終止,那麼系統會向該會話中的所有進程發送SIGHUP信號,導致它們終止(除非它們已經忽略了SIGHUP信號或者已經變成了守護進程)。
3. 控制終端
先説一下什麼是控制終端?
在UNIX系統中,用户通過終端登錄系統後得到一個Shell進程,這個終端成為Shell進程的控制終端。控制終端是保存在PCB中的信息,我們知道fork進程會複製PCB中的信息,因此由Shell進程啓動的其它進程的控制終端也是這個終端。默認情況下沒有重定向,每個進程的標準輸入、標準輸出和標準錯誤都指向控制終端,進程從標準輸入讀,也就是讀用户的鍵盤輸入,進程往標準輸出或標準錯誤輸出寫,也就是輸出到顯示器上。另外會話、進程組以及控制終端還有一些其他的關係,我們在下邊詳細介紹
一下:
- 一個會話可以有一個控制終端,通常會話首進程打開一個終端(終端設備或偽終端設備)後,該終端就成為該會話的控制終端。
- 建立與控制終端連接的會話首進程被稱為控制進程。
- 一個會話中的幾個進程組可被分成一個前台進程組以及一個或者多個後台進程組。
- 如果一個會話有一個控制終端,則它有一個前台進程組,會話中的其他進程組則為後台進程組。
- 無論何時進入終端的中斷鍵(ctrl+c)或退出鍵(ctrl+\),就會將中斷信號發送給前台進程組的所有進程。
- 如果終端接口檢測到調制解調器(或網絡)已經斷開,則將掛斷信號發送給控制進程(會話首進程)。
這些特性的關係如下圖所示:
- 控制終端與整個會話關聯,而不是僅與前台進程組關聯。但是,只有前台進程組中的進程可以從控制終端讀取輸入並向其輸出,同時接收終端產生的信號(如SIGINT、SIGQUIT、SIGTSTP等)。
- 會話首進程是創建該會話的進程,它通常是一個shell進程。會話首進程不一定在前台進程組中。實際上,當我們在shell中啓動一個前台作業時,shell(會話首進程)會將自己設置為後台進程組,而將新啓動的進程組設置為前台進程組。
所以,當有一個前台進程時,控制終端與前台進程組關聯,會話首進程(通常是shell)則在後台進程組中。但是,會話首進程仍然與控制終端關聯(因為它是會話的一部分),只是它不接收終端輸入和終端產生的信號(因為只有前台進程組接收這些)。
舉個例子:
假設我們有一個shell(比如bash),它是會話首進程。我們在shell中運行一個前台命令,比如 vim。
- 此時,shell會創建一個新的進程組,將vim放入該進程組,並將該進程組設置為前台進程組。
- 然後,shell自身所在的進程組就成為後台進程組。
- 控制終端仍然與整個會話關聯,但只有vim(前台進程組)可以接收終端輸入和信號。如果按下Ctrl+C,SIGINT會發送給vim,而不會發送給shell(因為shell在後台進程組)。
但是,如果控制終端斷開(比如網絡斷開),則掛斷信號(SIGHUP)會發送給會話首進程(即shell),然後shell通常會將SIGHUP發送給所有子進程(包括vim),然後自己退出。
4. 作業控制
4.1 什麼是作業(job)和作業控制(Job Control)?
作業是針對用户來講,用户完成某項任務而啓動的進程,一個作業既可以只包含一個進程,也可以包含多個進程,進程之間互相協作完成任務, 通常是一個進程管道。
Shell分前後台來控制的不是進程而是作業或者進程組。一個前台作業可以由多個進程組成,一個後台作業也可以由多個進程組成,Shell可以同時運行一個前台作業和任意多個後台作業,這稱為作業控制。
例如下列命令就是一個作業,它包括兩個命令,在執行時Shell將在前台啓動由兩個進程組成的作業:
ltx@My-Xshell-8-Pro-Max-Ultra:~$ cat /etc/filesystems | head -n 5
xfs
ext4
ext3
ext2
nodev proc
4.2 作業號
放在後台執行的程序或命令稱為後台命令,可以在命令的後面加上 & 符號從而讓Shell識別這是一個後台命令,後台命令不用等待該命令執行完成,就可立即接收新的命令,另外後台進程執行完後會返回一個作業號以及一個進程號(PID)。
例如下面的命令在後台啓動了一個作業,該作業由兩個進程組成,兩個進程都在後台運行:
ltx@My-Xshell-8-Pro-Max-Ultra:~$ cat /etc/filesystems | grep ext &
[1] 2202
ext4
ext3
ext2
# 按下回⻋
[1]+ 完成 cat /etc/filesystems | grep --color=auto ext
- 第一行表示作業號和進程ID,可以看到作業號是1,進程ID是2202
- 第3-4行表示該程序運行的結果,過濾 /etc/filesystems 有關 ext 的內容
- 第6行分別表示作業號、默認作業、作業狀態以及所執行的命令
關於默認作業:對於一個用户來説,只能有一個默認作業 (+) ,同時也只能有一個即將成為默認作業的作業 (-) ,當默認作業退出後,該作業會成為默認作業。
- + : 表示該作業號是默認作業
- - :表示該作業即將成為默認作業
- 無符號: 表示其他作業
在Shell中,當我們啓動多個後台作業時,Shell會為每個作業分配一個作業號,並標記其中一個為默認作業(通常是最近啓動的作業或最近被操作的作業)。
常見的作業狀態如下表所示:
4.3 作業的掛起與切回
作業掛起
我們在執行某個作業時,可以通過 Ctrl+Z 鍵將該作業掛起,然後Shell會顯示相關的作業號、狀態以及所執行的命令信息。
例如我們運行一個死循環的程序, 通過 Ctrl+Z 將該作業掛起, 觀察一下對應的作業狀態:
#include <iostream>
#include <unistd.h>
int main()
{
while(true)
{
std::cout << "hello server" <<std::endl;
sleep(1);
}
return 0;
}
下面我運行這個程序, 通過 Ctrl+Z 將該作業掛起:
ltx@My-Xshell-8-Pro-Max-Ultra:~/gitLinux/Linux_network/Daemon/test$ ./proc
hello server
hello server
^Z #鍵⼊Ctrl + Z觀察現象
# 結果依次對應作業號 默認作業 作業狀態 運⾏程序信息
[1]+ Stopped ./proc
可以發現通過 Ctrl+Z 將作業掛起, 該作業狀態已經變為了停止狀態
作業切回
如果想將掛起的作業切回,可以通過 fg 命令, fg 後面可以跟 作業號 或 作業的命令名稱 。如果參數缺省則會默認將作業號為1的作業切到前台來執行,若當前系統只有一個作業在後台進行,則可以直接使用fg命令不帶參數直接切回。 具體的參數參考如下:
例如我們把剛剛掛起來的 ./test 作業切回到前台:
ltx@My-Xshell-8-Pro-Max-Ultra:~/gitLinux/Linux_network/Daemon/test$ fg %%
./proc
hello server
hello server
hello server
hello server
hello server
^C
運行結果為開始無限循環打印 hello server,可以發現該作業已經切換到前台了。
注意: 當通過 fg 命令切回作業時,若沒有指定作業參數,此時會將默認作業切到前台執行,即帶有“+”的作業號的作業
4.4 查看後台執行或掛起的作業
我們可以直接通過輸入 jobs 命令查看用户當前後台執行或掛起的作業
- 參數 -l 則顯示作業的詳細信息
- 參數 -p 則只顯示作業的PID
- 參數 -r 只顯示運行中的作業
- 參數 -s 只顯示停止的作業
- 參數 -n 顯示狀態變化的作業 (只顯示自上次通知後狀態發生變化的作業)
例如, 我們先在後台及前台運行兩個作業,並將前台作業掛起, 來用 jobs 命令查看作業相關的信息:
ltx@My-Xshell-8-Pro-Max-Ultra:~/gitLinux/Linux_network/Daemon/test$ sleep 300 &
[1] 800147
ltx@My-Xshell-8-Pro-Max-Ultra:~/gitLinux/Linux_network/Daemon/test$ ./proc
hello server
hello server
^Z
[2]+ Stopped ./proc
ltx@My-Xshell-8-Pro-Max-Ultra:~/gitLinux/Linux_network/Daemon/test$ jobs -l
[1]- 800147 Running sleep 300 &
[2]+ 800188 Stopped ./proc
注意: jobs 命令是Shell內置的作業控制功能,作用範圍僅限於當前Shell會話。
4.5 作業控制相關的信號
上面我們提到了鍵入 Ctrl + Z 可以將前台作業掛起,實際上是將 STGTSTP 信號發送至前台進程組作業中的所有進程, 後台進程組中的作業不受影響。 在unix系統中, 存在3個特殊字符可以使得終端驅動程序產生信號, 並將信號發送至前台進程組作業, 它們分別是:
- Ctrl + C : 中斷字符, 會產生 SIGINT 信號
- Ctrl + \ : 退出字符, 會產生 SIGQUIT 信號
- Ctrl + Z :掛起字符, 會產生 STGTSTP 信號
終端的I/O(即標準輸入和標準輸出)和終端產生的信號總是從前台進程組作業連接打破實際終端。我們可以通過下體來看到作業控制的功能:
5. 守護進程
守護進程(Daemon Process)是計算機操作系統中一種在後台運行的特殊進程,它獨立於控制終端,通常用於執行週期性任務或長期運行的服務。這類進程在系統啓動時自動運行,直到系統關閉才終止。
主要特點:
- 脱離終端控制:守護進程在後台運行,不與任何用户終端直接交互,避免被終端信號中斷。
- 以 root 權限運行:通常需要特殊權限(如使用特定端口或資源),因此多以 root 用户身份啓動。
- 孤兒進程特性:其父進程在 fork() 創建子進程後立即退出,使守護進程被 init 進程(PID=1)接管,成為孤兒進程。
- 常駐內存:生命週期長,從系統啓動持續運行到關機,提供持續服務(如日誌記錄、網絡服務等)。
傳統的守護進程創建
#pragma once
#include <iostream>
#include <cstdlib>
#include <signal.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
const char *root = "/";
const char *dev_null = "/dev/null";
void Daemon(bool ischdir, bool isclose)
{
// 1. 忽略可能引起程序異常退出的信號
signal(SIGCHLD, SIG_IGN);
signal(SIGPIPE, SIG_IGN);
// 2. 讓自己不要成為組長
if (fork() > 0)
exit(0);
// 3. 設置讓自己成為一個新的會話, 後面的代碼其實是子進程在走
setsid();
// 4. 每一個進程都有自己的CWD,是否將當前進程的CWD更改成為 / 根目錄
if (ischdir)
chdir(root);
// 5. 已經變成守護進程啦,不需要和用户的輸入輸出,錯誤進行關聯了
if (isclose)
{
close(0);
close(1);
close(2);
}
else
{
// 這裏一般建議就用這種
int fd = open(dev_null, O_RDWR);
if (fd > 0)
{
dup2(fd, 0);
dup2(fd, 1);
dup2(fd, 2);
close(fd);
}
}
}
常見的系統守護進程
|
守護進程
|
功能
|
管理命令
|
|
sshd
|
SSH服務器
|
systemctl status sshd
|
|
crond
|
定時任務
|
systemctl status crond
|
|
rsyslogd
|
系統日誌
|
systemctl status rsyslog
|
|
httpd/nginx
|
Web服務器
|
systemctl status nginx
|
|
mysqld
|
數據庫
|
systemctl status mysql
|
6. 守護進程 vs 後台進程
守護進程(Daemon Process)和後台進程(Background Process)都是在後台運行的進程,但它們在設計目標、運行方式和生命週期管理上存在本質區別。
守護進程(Daemon Process):
- 守護進程是運行在後台的一種特殊進程,它獨立於控制終端並且週期性地執行某種任務或等待處理某些發生的事件。
- 守護進程通常隨着系統的啓動而啓動,並一直運行直到系統關閉。它們不受用户登錄和註銷的影響。
- 守護進程的父進程是init進程(PID=1),因為它是由init進程收養的。
- 守護進程沒有控制終端(它的TTY是?),所以它不會與用户交互。
- 常見的守護進程有:sshd、httpd、mysqld等。
後台進程(Background Process):
- 後台進程也是運行在後台的進程,但是它通常是由用户通過Shell(如bash)在終端中啓動的,並在後台運行。
- 後台進程與終端有關聯(雖然它在後台運行,但它仍然屬於當前終端會話)。如果終端關閉,那麼後台進程會收到SIGHUP信號(默認會終止)。
- 後台進程的父進程是啓動它的Shell進程。
- 用户可以通過fg命令將後台進程切換到前台,也可以通過bg命令將暫停的進程放到後台運行。
- 後台進程通常用於讓用户能夠繼續在同一個終端中工作,而不必等待進程結束。
關鍵差異詳解
- 與會話和終端的關係
- 後台進程:通過 & 或 Ctrl+Z + bg 命令創建,它仍然是當前登錄會話(Session) 和進程組(Process Group) 的一部分。這意味着當用户註銷或啓動它的終端關閉時,該進程會收到 SIGHUP 信號並被終止。
- 守護進程:通過調用 setsid() 等系統調用創建了一個新的獨立會話,並擺脱了與任何控制終端的關聯。因此,終端關閉或用户註銷不會影響其運行,它由 init 進程(PID 1)直接接管。
- 文件描述符與輸出
- 後台進程:默認繼承父進程(通常是Shell)的文件描述符。它仍然可以向終端輸出信息,也可能意外地讀取終端輸入。
- 守護進程:會主動關閉所有從父進程繼承的文件描述符(包括標準輸入、輸出、錯誤),並將它們重定向到 /dev/null 或特定的日誌文件,確保其運行不受終端干擾。
- 運行環境
- 後台進程:其工作目錄通常是啓動它時所在的目錄。如果該目錄是一個可卸載的文件系統(如U盤),可能會導致問題。
- 守護進程:通常會通過 chdir(“/”) 將其工作目錄更改為根目錄,以避免此類問題,並調用 umask(0) 以確保創建文件時具有預期的權限。
總結
簡單來説,後台進程是“依附”於終端會話的臨時後台任務,而守護進程是“脱離”一切終端獨立存在的系統服務。