進程組
什么是進程組
之前我們提到了進程的概念, 其實每一個進程除了有一個進程 ID(PID)之外 還屬于一個進程組。進程組是一個或者多個進程的集合, 一個進程組可以包含多個進程。 每一個進程組也有一個唯一的進程組 ID(PGID), 并且這個 PGID 類似于進程 ID, 同樣是一個正整數, 可以存放在 pid_t 數據類型中。
C++
$ ps -eo pid,pgid,ppid,comm | grep test
#結果如下
PID PGID PPID COMMAND
2830 2830 2259 test
# -e 選項表示 every 的意思, 表示輸出每一個進程信息
# -o 選項以逗號操作符(,)作為定界符, 可以指定要輸出的列
組長進程
每一個進程組都有一個組長進程。 組長進程的 ID 等于其進程 ID。我們可以通過 ps 命
令看到組長進程的現象:
Shell
[node@localhost code]$ ps -o pid,pgid,ppid,comm | cat
# 輸出結果
PID PGID PPID COMMAND
2806 2806 2805 bash
2880 2880 2806 ps
2881 2880 2806 cat
從結果上看 ps 進程的 PID 和 PGID 相同, 那也就是說明 ps 進程是該進程組的組長進程, 該進程組包括 ps 和 cat 兩個進程。進程組組長的作用: 進程組組長可以創建一個進程組或者創建該組中的進程進程組的生命周期: 從進程組創建開始到其中最后一個進程離開為止。注意:主要某個進程組中有一個進程存在, 則該進程組就存在, 這與其組長進程是否已經終止無關
會話
什么是會話
剛剛我們談到了進程組的概念, 那么會話又是什么呢? 會話其實和進程組息息相關,
會話可以看成是一個或多個進程組的集合, 一個會話可以包含多個進程組。每一個會
話也有一個會話 ID(SID)
通常我們都是使用管道將幾個進程編成一個進程組。 如上圖的進程組 2 和進程組 3 可
能是由下列命令形成的:
Shell
[node@localhost code]$ proc2 | proc3 &
[node@localhost code]$ proc4 | proc5 | proc6 &
# &表示將進程組放在后臺執行
我們舉一個例子觀察一下這個現象:
# 用管道和 sleep 組成一個進程組放在后臺運行
[node@localhost code]$ sleep 100 | sleep 200 | sleep 300 &
# 查看 ps 命令打出來的列描述信息
[node@localhost code]$ ps axj | head -n1
?
# 過濾 sleep 相關的進程信息
[node@localhost code]$ ps axj | grep sleep | grep -v grep
# a 選項表示不僅列當前?戶的進程,也列出所有其他?戶的進程
# x 選項表示不僅列有控制終端的進程,也列出所有?控制終端的進程
# j 選項表示列出與作業控制相關的信息, 作業控制后續會講
# grep 的-v 選項表示反向過濾, 即不過濾帶有 grep 字段相關的進程
從結果來看 3 個進程對應的 PGID 相同, 即屬于同一個進程組。
如何創建會話
可以調用 setseid 函數來創建一個會話, 前提是調用進程不能是一個進程組的組長。
#include <unistd.h>
/*
*功能:創建會話
*返回值:創建成功返回 SID, 失敗返回-1
*/
pid_t setsid(void);
該接口調用之后會發生:調用進程會變成新會話的會話首進程。此時, 新會話中只有唯一的一個進程,調用進程會變成進程組組長。 新進程組 ID 就是當前調用進程 ID,該進程沒有控制終端。 如果在調用 setsid 之前該進程存在控制終端, 則調用之后會切斷聯系
需要注意的是: 這個接口如果調用進程原來是進程組組長, 則會報錯, 為了避免這種情況, 我們通常的使用方法是先調用 fork 創建子進程, 父進程終止, 子進程繼續執行, 因為子進程會繼承父進程的進程組 ID, 而進程 ID 則是新分配的, 就不會出現錯誤的情況。
會話 ID(SID)
上邊我們提到了會話 ID, 那么會話 ID 是什么呢? 我們可以先說一下會話首進程, 會話首進程是具有唯一進程 ID 的單個進程, 那么我們可以將會話首進程的進程 ID 當做是會話 ID。注意:會話 ID 在有些地方也被稱為 會話首進程的進程組 ID, 因為會話首進程總是一個進程組的組長進程, 所以兩者是等價的。
控制終端
先說一下什么是控制終端?
在 UNIX 系統中,用戶通過終端登錄系統后得到一個 Shell 進程,這個終端成為 Shell進程的控制終端。控制終端是保存在 PCB 中的信息,我們知道 fork 進程會復制 PCB中的信息,因此由 Shell 進程啟動的其它進程的控制終端也是這個終端。默認情況下沒有重定向,每個進程的標準輸入、標準輸出和標準錯誤都指向控制終端,進程從標準輸入讀也就是讀用戶的鍵盤輸入,進程往標準輸出或標準錯誤輸出寫也就是輸出到顯示器上。另外會話、進程組以及控制終端還有一些其他的關系,我們在下邊詳細介紹一下:
一個會話可以有一個控制終端,通常會話首進程打開一個終端(終端設備或偽終端設備)后,該終端就成為該會話的控制終端。建立與控制終端連接的會話首進程被稱為控制進程。一個會話中的幾個進程組可被分成一個前臺進程組以及一個或者多個后臺進程組。如果一個會話有一個控制終端,則它有一個前臺進程組,會話中的其他進程組則為后臺進程組。無論何時進入終端的中斷鍵(ctrl+c)或退出鍵(ctrl+\),就會將中斷信號發送給前臺進程組的所有進程。如果終端接口檢測到調制解調器(或網絡)已經斷開,則將掛斷信號發送給控制進程(會話首進程)。
這些特性的關系如下圖所示:
守護進程
上篇博客寫了TCP,將服務器守護進程化
其余內容不變,只修改main.cc和引入封裝Daemon.hpp
Daemon.hpp
#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);}}
}
main.cc
#include"TcpServer.hpp"
#include <memory>
#include "Protocol.hpp"
#include"NetCal.hpp"
#include "Daemon.hpp"
void Usage(std::string proc)
{std::cerr<<"Usage: "<<proc<<"prot"<<std::endl;
}// ./tcpserver
int main(int argc,char* argv[])
{if(argc !=2){Usage(argv[0]);exit(USAGE_ERR);}Daemon(false, false);//1.頂層std::unique_ptr<Cal> cal = std::make_unique<Cal>();//2.協議層std::unique_ptr<Protocol> protocol = std::make_unique<Protocol>([&cal](Request &req)->Response{return cal->Execute(req);});//3.服務器層std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(std::stoi(argv[1]),[&protocol](std::shared_ptr<Socket>&sock,InetAddr &client){protocol->GetReqquest(sock,client);});tsvr->Start();return 0;
}
此時將服務器放在后臺進程里鍵盤輸入的任何消息只會發送給前臺進程接收
?也可以調用系統調用
deamon
?
#include"TcpServer.hpp"
#include <memory>
#include "Protocol.hpp"
#include"NetCal.hpp"
#include "Daemon.hpp"#include <unistd.h>
void Usage(std::string proc)
{std::cerr<<"Usage: "<<proc<<"prot"<<std::endl;
}// ./tcpserver
int main(int argc,char* argv[])
{if(argc !=2){Usage(argv[0]);exit(USAGE_ERR);}daemon(0, 0);//1.頂層std::unique_ptr<Cal> cal = std::make_unique<Cal>();//2.協議層std::unique_ptr<Protocol> protocol = std::make_unique<Protocol>([&cal](Request &req)->Response{return cal->Execute(req);});//3.服務器層std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(std::stoi(argv[1]),[&protocol](std::shared_ptr<Socket>&sock,InetAddr &client){protocol->GetReqquest(sock,client);});tsvr->Start();return 0;
}
效果也是一樣的