一、應用層
1.1、理解協議
協議是一種 "約定". socket api 的接口, 在讀寫數據時, 都是按 "字符串" 的方式來發送接收的。如果我們要傳輸一些 "結構化的數據" 怎么辦呢?
其實,協議就是雙方約定好的結構化的數據。
1.2、網絡版計算器
例如, 我們需要實現一個服務器版的加法器. 我們需要客戶端把要計算的兩個加數發過去, 然后由服務器進行計算, 最后再把結果返回給客戶端。
約定方案一:
- 客戶端發送一個形如"1+1"的字符串;
- 這個字符串中有兩個操作數, 都是整形;
- 兩個數字之間會有一個字符是運算符, 運算符只能是 + ;
- 數字和運算符之間沒有空格;
- ......
約定方案二:
- 定義結構體來表示我們需要交互的信息;
- 發送數據時將這個結構體按照一個規則轉換成字符串, 接收到數據的時候再按照相同的規則把字符串轉化回結構體;
- 這個過程叫做 "序列化" 和 "反序列化"。
1.3、序列化 和 反序列化
上面計算機例子中,無論我們采用方案一, 還是方案二, 還是其他的方案, 只要保證, 一端發送時構造的數據,在另一端能夠正確的進行解析, 就是 ok 的。這種約定, 就是應用層協議。
二、重新理解 read、write、recv、send 和 tcp 為什么支持全雙工
- 在任何一臺主機上,TCP 連接既有發送緩沖區,又有接受緩沖區,所以,在內核中,可以在發消息的同時,也可以收消息,即全雙工。這就是為什么一個 tcp sockfd 讀寫都是它的原因
- 實際數據什么時候發,發多少,出錯了怎么辦?由 TCP 控制,所以 TCP 叫做傳輸控制協議。
三、自定義實現協議
代碼結構:
Calculate.hpp ????????Makefile ????????Socket.hpp ????????TcpServer.hpp ????????Daemon.hpp?
Protocol.hpp ????????TcpClientMain.cc ????????TcpServerMain.cc
期望的報文格式:
示例代碼鏈接:Linux_blog: Linux博客示例代碼 - Gitee.comhttps://gitee.com/algnmlgb/linux_blog/tree/master/lesson24/NetCal
四、關于流式數據的處理
完整的處理過程應該是:
五、補充
5.1、Jsoncpp
Jsoncpp 是一個用于處理 JSON 數據的 C++ 庫。它提供了將 JSON 數據序列化為字符串以及從字符串反序列化為 C++ 數據結構的功能。Jsoncpp 是開源的,廣泛用于各種需要處理 JSON 數據的 C++ 項目中。
特性:
- 簡單易用:Jsoncpp 提供了直觀的 API,使得處理 JSON 數據變得簡單。
- 高性能:Jsoncpp 的性能經過優化,能夠高效地處理大量 JSON 數據。
- 全面支持:支持 JSON 標準中的所有數據類型,包括對象、數組、字符串、數 字、布爾值和 null。
- 錯誤處理:在解析 JSON 數據時,Jsoncpp 提供了詳細的錯誤信息和位置,方便開發者調試。
當使用 Jsoncpp 庫進行 JSON 的序列化和反序列化時,確實存在不同的做法和工具類可供選擇。以下是對 Jsoncpp 中序列化和反序列化操作的詳細介紹:
安裝:
C++
ubuntu:sudo apt-get install libjsoncpp-dev
Centos: sudo yum install jsoncpp-devel
序列化:
序列化指的是將數據結構或對象轉換為一種格式,以便在網絡上傳輸或存儲到文件中。Jsoncpp 提供了多種方式進行序列化:
1.?使用 Json::Value 的 toStyledString 方法:
- 優點:將 Json::Value 對象直接轉換為格式化的 JSON 字符串。
- 示例代碼:
#include <iostream>
#include <string>
#include <jsoncpp/json/json.h>
int main()
{Json::Value root;root["name"] = "joe";root["sex"] = "男";std::string s = root.toStyledString();std::cout << s << std::endl;
return 0;
}
2.?使用 Json::StreamWriter:
- 優點:提供了更多的定制選項,如縮進、換行符等。
- 示例代碼:
#include <iostream>
#include <string>
#include <sstream>
#include <memory>
#include <jsoncpp/json/json.h>
int main()
{Json::Value root;root["name"] = "joe";root["sex"] = "男";Json::StreamWriterBuilder wbuilder; // StreamWriter 的工廠std::unique_ptr<Json::StreamWriter> writer(wbuilder.newStreamWriter());std::stringstream ss;writer->write(root, &ss);std::cout << ss.str() << std::endl;return 0;
}
3. 使用 Json::FastWriter:
- 優點:比 StyledWriter 更快,因為它不添加額外的空格和換行符。
- 示例代碼:
#include <iostream>
#include <string>
#include <sstream>
#include <memory>
#include <jsoncpp/json/json.h>
int main()
{Json::Value root;root["name"] = "joe";root["sex"] = "男";Json::FastWriter writer;std::string s = writer.write(root);std::cout << s << std::endl;return 0;
}
#include <iostream>
#include <string>
#include <sstream>
#include <memory>
#include <jsoncpp/json/json.h>
int main()
{Json::Value root;root["name"] = "joe";root["sex"] = "男";// Json::FastWriter writer;Json::StyledWriter writer;std::string s = writer.write(root);std::cout << s << std::endl;return 0;
}
反序列化:
反序列化指的是將序列化后的數據重新轉換為原來的數據結構或對象。Jsoncpp 提供了以下方法進行反序列化:
1.?使用 Json::Reader:
- 優點:提供詳細的錯誤信息和位置,方便調試。
- 示例代碼:
#include <iostream>
#include <string>
#include <jsoncpp/json/json.h>
int main()
{// JSON 字符串std::string json_string = "{\"name\":\"張三\",\"age\":30, \"city\":\"北京\"}";// 解析 JSON 字符串Json::Reader reader;Json::Value root;// 從字符串中讀取 JSON 數據bool parsingSuccessful = reader.parse(json_string,root);if (!parsingSuccessful) {// 解析失敗,輸出錯誤信息std::cout << "Failed to parse JSON: " <<reader.getFormattedErrorMessages() <<std::endl;return 1;}// 訪問 JSON 數據std::string name = root["name"].asString();int age = root["age"].asInt();std::string city = root["city"].asString();// 輸出結果std::cout << "Name: " << name << std::endl;std::cout << "Age: " << age << std::endl;std::cout << "City: " << city << std::endl;return 0;
}
2.?使用 Json::CharReader 的派生類:
- 在某些情況下,你可能需要更精細地控制解析過程,可以直接使用Json::CharReader 的派生類。
- 但通常情況下,使用 Json::parseFromStream 或 Json::Reader 的 parse方法就足夠了。
總結:
- toStyledString、StreamWriter 和 FastWriter 提供了不同的序列化選項, 你可以根據具體需求選擇使用。
- Json::Reader 和 parseFromStream 函數是 Jsoncpp 中主要的反序列化工具, 它們提供了強大的錯誤處理機制。
- 在進行序列化和反序列化時,請確保處理所有可能的錯誤情況,并驗證輸入和輸出的有效性。
Json::Value:
Json::Value 是 Jsoncpp 庫中的一個重要類,用于表示和操作 JSON 數據結構。以下是一些常用的 Json::Value 操作列表:
1.?構造函數
- Json::Value():默認構造函數,創建一個空的 Json::Value 對象。
- Json::Value(ValueType type, bool allocated = false):根據給定的ValueType(如 nullValue, intValue, stringValue 等)創建一個 Json::Value 對象。
2. 訪問元素
- Json::Value& operator[](const char* key):通過鍵(字符串)訪問對象 中的元素。如果鍵不存在,則創建一個新的元素。
- Json::Value& operator[](const std::string& key):同上,但使用std::string 類型的鍵。
- Json::Value& operator[](ArrayIndex index):通過索引訪問數組中的元素。如果索引超出范圍,則創建一個新的元素。
- Json::Value& at(const char* key):通過鍵訪問對象中的元素,如果鍵不存在則拋出異常。
-
Json::Value& at(const std::string& key):同上,但使用 std::string類型的鍵。
3. 類型檢查
- bool isNull():檢查值是否為 null。
- bool isBool():檢查值是否為布爾類型。
- bool isInt():檢查值是否為整數類型。
- bool isInt64():檢查值是否為 64 位整數類型。
- bool isUInt():檢查值是否為無符號整數類型。
- bool isUInt64():檢查值是否為 64 位無符號整數類型。
- bool isIntegral():檢查值是否為整數或可轉換為整數的浮點數。
- bool isDouble():檢查值是否為雙精度浮點數。
- bool isNumeric():檢查值是否為數字(整數或浮點數)。
- bool isString():檢查值是否為字符串。
- bool isArray():檢查值是否為數組。
- bool isObject():檢查值是否為對象(即鍵值對的集合)。
4. 賦值與類型轉換
- Json::Value& operator=(bool value):將布爾值賦給 Json::Value 對象。
- Json::Value& operator=(int value):將整數賦給 Json::Value 對象。
- Json::Value& operator=(unsigned int value):將無符號整數賦給Json::Value 對象。
- Json::Value& operator=(Int64 value):將 64 位整數賦給 Json::Value對象。
- Json::Value& operator=(UInt64 value):將 64 位無符號整數賦給Json::Value 對象。
- Json::Value& operator=(double value):將雙精度浮點數賦給Json::Value 對象。
- Json::Value& operator=(const char* value):將 C 字符串賦給Json::Value 對象。
-
Json::Value& operator=(const std::string& value):將 std::string賦給 Json::Value 對象。
- bool asBool():將值轉換為布爾類型(如果可能)。
- int asInt():將值轉換為整數類型(如果可能)。
- Int64 asInt64():將值轉換為 64 位整數類型(如果可能)。
- unsigned int asUInt():將值轉換為無符號整數類型(如果可能)。
- UInt64 asUInt64():將值轉換為 64 位無符號整數類型(如果可能)。
- double asDouble():將值轉換為雙精度浮點數類型(如果可能)。
- std::string asString():將值轉換為字符串類型(如果可能)。
5. 數組和對象操作
- size_t size():返回數組或對象中的元素數量。
- bool empty():檢查數組或對象是否為空。
- void resize(ArrayIndex newSize):調整數組的大小。
- void clear():刪除數組或對象中的所有元素。
- void append(const Json::Value& value):在數組末尾添加一個新元素。
- Json::Value& operator[](const char* key, const Json::Value& defaultValue = Json::nullValue):在對象中插入或訪問一個元素,如果鍵不存 在則使用默認值。
- Json::Value& operator[](const std::string& key, const Json::Value& defaultValue = Json::nullValue):同上,但使用 std::string類型的
六、手寫序列化與反序列化
本質:就是對字符串的處理
示例代碼:
#pragma once#include <iostream>
#include <memory>
#include <jsoncpp/json/json.h>#define SelfDefine 1namespace Protocol
{// 問題// 1. 結構化數據的序列和反序列化// 2. 還要解決用戶區分報文邊界 --- 數據包粘報問題// 總結:// 我們今天定義了幾組協議呢??我們可以同時存在多個協議嗎???可以// "protocol_code\r\nlen\r\nx op y\r\n" : \r\n不屬于報文的一部分,約定const std::string ProtSep = " ";const std::string LineBreakSep = "\r\n";// "len\r\nx op y\r\n" : \r\n不屬于報文的一部分,約定std::string Encode(const std::string &message){std::string len = std::to_string(message.size());std::string package = len + LineBreakSep + message + LineBreakSep;return package;}bool Decode(std::string &package, std::string *message){// 除了解包,我還想判斷報文的完整性, 能否正確處理具有"邊界"的報文auto pos = package.find(LineBreakSep);if (pos == std::string::npos)return false;std::string lens = package.substr(0, pos);int messagelen = std::stoi(lens);int total = lens.size() + messagelen + 2 * LineBreakSep.size();if (package.size() < total)return false;// 至少package內部一定有一個完整的報文了!*message = package.substr(pos + LineBreakSep.size(), messagelen);package.erase(0, total);return true;}class Request{public:Request() : _data_x(0), _data_y(0), _oper(0){}Request(int x, int y, char op) : _data_x(x), _data_y(y), _oper(op){}void Debug(){std::cout << "_data_x: " << _data_x << std::endl;std::cout << "_data_y: " << _data_y << std::endl;std::cout << "_oper: " << _oper << std::endl;}void Inc(){_data_x++;_data_y++;}// 結構化數據->字符串bool Serialize(std::string *out){
#ifdef SelfDefine // 條件編譯*out = std::to_string(_data_x) + ProtSep + _oper + ProtSep + std::to_string(_data_y);return true;
#elseJson::Value root;root["datax"] = _data_x;root["datay"] = _data_y;root["oper"] = _oper;Json::FastWriter writer;*out = writer.write(root);return true;
#endif}bool Deserialize(std::string &in) // "x op y" [){
#ifdef SelfDefineauto left = in.find(ProtSep);if (left == std::string::npos)return false;auto right = in.rfind(ProtSep);if (right == std::string::npos)return false;_data_x = std::stoi(in.substr(0, left));_data_y = std::stoi(in.substr(right + ProtSep.size()));std::string oper = in.substr(left + ProtSep.size(), right - (left + ProtSep.size()));if (oper.size() != 1)return false;_oper = oper[0];return true;
#elseJson::Value root;Json::Reader reader;bool res = reader.parse(in, root);if(res){_data_x = root["datax"].asInt();_data_y = root["datay"].asInt();_oper = root["oper"].asInt();}return res;
#endif}int GetX() { return _data_x; }int GetY() { return _data_y; }char GetOper() { return _oper; }private:// _data_x _oper _data_y// 報文的自描述字段// "len\nx op y\n" : \n不屬于報文的一部分,約定// 很多工作都是在做字符串處理!int _data_x; // 第一個參數int _data_y; // 第二個參數char _oper; // + - * / %};class Response{public:Response() : _result(0), _code(0){}Response(int result, int code) : _result(result), _code(code){}bool Serialize(std::string *out){
#ifdef SelfDefine*out = std::to_string(_result) + ProtSep + std::to_string(_code);return true;
#elseJson::Value root;root["result"] = _result;root["code"] = _code;Json::FastWriter writer;*out = writer.write(root);return true;
#endif}bool Deserialize(std::string &in) // "_result _code" [){
#ifdef SelfDefineauto pos = in.find(ProtSep);if (pos == std::string::npos)return false;_result = std::stoi(in.substr(0, pos));_code = std::stoi(in.substr(pos + ProtSep.size()));return true;
#elseJson::Value root;Json::Reader reader;bool res = reader.parse(in, root);if(res){_result = root["result"].asInt();_code = root["code"].asInt();}return res;
#endif}void SetResult(int res) { _result = res; }void SetCode(int code) { _code = code; }int GetResult() { return _result; }int GetCode() { return _code; }private:// "len\n_result _code\n"int _result; // 運算結果int _code; // 運算狀態};// 簡單的工廠模式,建造類設計模式class Factory{public:std::shared_ptr<Request> BuildRequest(){std::shared_ptr<Request> req = std::make_shared<Request>();return req;}std::shared_ptr<Request> BuildRequest(int x, int y, char op){std::shared_ptr<Request> req = std::make_shared<Request>(x, y, op);return req;}std::shared_ptr<Response> BuildResponse(){std::shared_ptr<Response> resp = std::make_shared<Response>();return resp;}std::shared_ptr<Response> BuildResponse(int result, int code){std::shared_ptr<Response> req = std::make_shared<Response>(result, code);return req;}};
}
七、進程間關系與守護進程
7.1、進程組
7.1.1、什么是進程組
之前我們提到了進程的概念, 其實每一個進程除了有一個進程 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 選項以逗號操作符(,)作為定界符, 可以指定要輸出的列
7.1.2、組長進程
每一個進程組都有一個組長進程。 組長進程的 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 兩個進程。
- 進程組組長的作用: 進程組組長可以創建一個進程組或者創建該組中的進程
- 進程組的生命周期: 從進程組創建開始到其中最后一個進程離開為止。
注意:主要某個進程組中有一個進程存在,則該進程組就存在,這與其組長進程是否已經終止無關。
7.2、會話
7.2.1、什么是會話
會話其實和進程組息息相關,會話可以看成是一個或多個進程組的集合, 一個會話可以包含多個進程組。每一個會話也有一個會話 ID(SID)
通常我們都是使用管道將幾個進程編成一個進程組。 如上圖的進程組 2 和進程組 3 可能是由下列命令形成的:
shell
[node@localhost code]$ proc2 | proc3 &
[node@localhost code]$ proc4 | proc5 | proc6 &
&表示將進程組放在后臺執行
我們舉一個例子觀察一下這個現象:
Shell
# 用管道和 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 字段相關的進程
# 結果如下
PPID? ? PID? ? PGID? ? SID? ? TTY? ????????TPGID? ? STAT? ? UID? ? TIME? ? COMMAND
2806? ? 4223? ?4223? ? 2780? ?pts/2? ? ? ? ?4229? ? ? ?S? ? ? ? 1000? ? 0:00? ? ? ?sleep 100
2806? ? 4224? ?4223? ? 2780? ?pts/2? ? ? ? ?4229? ? ? ?S? ? ? ? 1000? ? 0:00? ? ? ?sleep 200
2806? ? 4225? ?4223? ? 2780? ?pts/2? ? ? ? ?4229? ? ? ?S? ? ? ? 1000? ? 0:00? ? ? ?sleep 300
從上述結果來看 3 個進程對應的 PGID 相同, 即屬于同一個進程組。
7.2.2、如何創建會話
可以調用 setseid 函數來創建一個會話, 前提是調用進程不能是一個進程組的組長。
C
#include<unistd.h>
/*
????????*功能:創建會話
????????*返回值:創建成功返回 SID, 失敗返回-1
*/
pid_t setsid(void);
該接口調用之后會發生:
- 調用進程會變成新會話的會話首進程。 此時, 新會話中只有唯一的一個進程。
- 調用進程會變成進程組組長。 新進程組 ID 就是當前調用進程 ID
- 該進程沒有控制終端。 如果在調用 setsid 之前該進程存在控制終端, 則調用之后會切斷聯系
需要注意的是: 這個接口如果調用進程原來是進程組組長, 則會報錯, 為了避免這種情況, 我們通常的使用方法是先調用 fork 創建子進程, 父進程終止, 子進程繼續執行, 因為子進程會繼承父進程的進程組 ID, 而進程 ID 則是新分配的, 就不會出現錯誤的情況。
7.2.3、會話 ID(SID)
上邊我們提到了會話 ID, 那么會話 ID 是什么呢? 我們可以先說一下會話首進程, 會話首進程是具有唯一進程 ID 的單個進程, 那么我們可以將會話首進程的進程 ID 當做是會話 ID。注意:會話 ID 在有些地方也被稱為 會話首進程的進程組 ID, 因為會話首進程總是一個進程組的組長進程, 所以兩者是等價的。
7.3、控制終端
什么是控制終端?
在 UNIX 系統中,用戶通過終端登錄系統后得到一個 Shell 進程,這個終端成為 Shell 進程的控制終端。控制終端是保存在 PCB 中的信息,我們知道 fork 進程會復制 PCB中的信息,因此由 Shell 進程啟動的其它進程的控制終端也是這個終端。默認情況下 沒有重定向,每個進程的標準輸入、標準輸出和標準錯誤都指向控制終端,進程從標 準輸入讀也就是讀用戶的鍵盤輸入,進程往標準輸出或標準錯誤輸出寫也就是輸出到 顯示器上。另外會話、進程組以及控制終端還有一些其他的關系,我們在下邊詳細介紹一下:
- 一個會話可以有一個控制終端,通常會話首進程打開一個終端(終端設備或 偽終端設備)后,該終端就成為該會話的控制終端。
- 建立與控制終端連接的會話首進程被稱為控制進程。
- 一個會話中的幾個進程組可被分成一個前臺進程組以及一個或者多個后臺進程組。
- 如果一個會話有一個控制終端,則它有一個前臺進程組,會話中的其他進程 組則為后臺進程組。
- 無論何時進入終端的中斷鍵(ctrl+c)或退出鍵(ctrl+\),就會將中斷信號發送給前臺進程組的所有進程。
- 如果終端接口檢測到調制解調器(或網絡)已經斷開,則將掛斷信號發送給控制進程(會話首進程)。
這些特性的關系如下圖所示:
7.4、作業控制
7.4.1、什么是作業(job)和作業控制(Job Control)?
作業是針對用戶來講,用戶完成某項任務而啟動的進程,一個作業既可以只包含一個進程,也可以包含多個進程,進程之間互相協作完成任務, 通常是一個進程管道。
Shell 分前后臺來控制的不是進程而是作業 或者進程組。一個前臺作業可以由多個進程組成,一個后臺作業也可以由多個進程組成,Shell 可以同時運?一個前臺作業和任意多個后臺作業,這稱為作業控制。
例如下列命令就是一個作業,它包括兩個命令,在執?時 Shell 將在前臺啟動由兩個進程組成的作業:
Shell
[node@localhost code]$ cat /etc/filesystems | head -n 5
運?結果如下所示:
xfs
ext4
ext3
ext2
nodev
proc
7.4.2、作業號
放在后臺執?的程序或命令稱為后臺命令,可以在命令的后面加上&符號從而讓 Shell 識別這是一個后臺命令,后臺命令不用等待該命令執?完成,就可立即接收 新的命令,另外后臺進程執行完后會返回一個作業號以及一個進程號(PID)。
例如下面的命令在后臺啟動了一個作業, 該作業由兩個進程組成, 兩個進程都在后臺運?。
Shell
[node@localhost code]$ 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 號分別表示作業號、默認作業、作業狀態以及所執?的命令
關于默認作業:對于一個用戶來說,只能有一個默認作業(+),同時也只能有一 個即將成為默認作業的作業(-),當默認作業退出后,該作業會成為默認作業。
- + : 表示該作業號是默認作業
- -:表示該作業即將成為默認作業
- 無符號: 表示其他作業
7.4.3、作業狀態
常見的作業狀態如下表所示:
7.4.4、作業的掛起與切回
(1) 作業掛起
我們在執?某個作業時,可以通過 Ctrl+Z 鍵將該作業掛起,然后 Shell 會顯示相關的作業號、狀態以及所執?的命令信息。
例如我們運?一個死循環的程序, 通過 Ctrl+Z 將該作業掛起, 觀察一下對應的作業狀態:
#include<stdio.h>?
int main()
{
????????while (1)
????????{
????????????????printf("hello\n");
????????}
????????return 0;
}
下面我運?這個程序, 通過 Ctrl+Z 將該作業掛起:
Shell
# 運行可執行程序
[node@localhost code]$ ./test
#鍵入 Ctrl + Z 觀察現象
運?結果如下:
Shell
# 結果依次對應作業號 默認作業 作業狀態 運行程序信息
[1]+???????? 已停止 ????????./test7
可以發現通過 Ctrl+Z 將作業掛起, 該作業狀態已經變為了停止狀態
(2) 作業切回
如果想將掛起的作業切回,可以通過 fg 命令,fg 后面可以跟作業號或作業的命 令名稱。如果參數缺省則會默認將作業號為 1 的作業切到前臺來執?,若當前系 統只有一個作業在后臺進?,則可以直接使用 fg 命令不帶參數直接切回。 具體的參數參考如下:
例如我們把剛剛掛起來的./test 作業切回到前臺:
Shell
[node@localhost code]$ fg? %%
運?結果為開始無限循環打印 hello, 可以發現該作業已經切換到前臺了。
注意: 當通過 fg 命令切回作業時,若沒有指定作業參數,此時會將默認作業切到前臺執行,即帶有“+”的作業號的作業
7.4.5、查看后臺執行或掛起的作業
我們可以直接通過輸入 jobs 命令查看本用戶當前后臺執?或掛起的作業
- 參數-l 則顯示作業的詳細信息
- 參數-p 則只顯示作業的 PID
例如, 我們先在后臺及前臺運?兩個作業, 并將前臺作業掛起, 來用 jobs 命令 查看作業相關的信息:
Shell
# 在后臺運行一個作業 sleep
[node@localhost code]$ sleep 300 &
# 運行剛才的死循環可執行程序
[node@localhost code]$ ./test
# 鍵入 Ctrl + Z 掛起作業
# 使用 jobs 命令查看后臺及掛起的作業
[node@localhost code]$ jobs -l
運?結果如下所示:
Shell
# 結果依次對應作業號? ?默認作業? ?作業狀態? ? 運行程序信息
[1]-? ? ?2265? ? ?運行中? ? sleep 300 &
[2]+? ? 2267? ? ?停止? ? ? ?./test7
7.4.6、作業控制相關的信號
上面我們提到了鍵入 Ctrl + Z 可以將前臺作業掛起,實際上是將 STGTSTP 信號 發送至前臺進程組作業中的所有進程, 后臺進程組中的作業不受影響。 在 unix系統中, 存在 3 個特殊字符可以使得終端驅動程序產生信號, 并將信號發送至前臺進程組作業, 它們分別是:
- Ctrl + C: 中斷字符, 會產生 SIGINT 信號
- Ctrl + \: 退出字符, 會產生 SIGQUIT 信號
- Ctrl + Z:掛起字符, 會產生 STGTSTP 信號
終端的 I/O(即標準輸入和標準輸出)和終端產生的信號總是從前臺進程組作業連接打破實際終端。我們可以通過下體來看到作業控制的功能:
7.4.7、前后臺作業轉化
- fg + 作業號:將后臺進程放入前臺執行。
- bg?+ 作業號:將前臺進程放入后臺執行。
7.5、守護進程
守護進程(Daemon Process)是操作系統中的一種特殊后臺進程,通常在系統啟動時啟動,持續運行以提供某種服務或執行特定任務,獨立于用戶終端且不受用戶登錄/注銷的影響。以下是其核心特點:
- 后臺運行:無控制終端(TTY),不與用戶直接交互,通常在后臺默默執行任務(如日志管理、網絡服務等)。
- 生命周期長:隨系統啟動而啟動,直到系統關閉才終止,提供持續服務(如 httpd 提供Web服務)。
- 脫離終端與會話:通過fork()創建子進程后,父進程退出,子進程調用setsid()脫離原有會話和終端,避免被信號干擾。父進程需要退出是因為調用 setseid 函數來創建一個會話, 前提是調用進程不能是一個進程組的組長。
7.6、如何將進程守護化
示例代碼:
#pragma once#include <iostream>
#include <cstdlib>
#include <signal.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>#define ROOT "/"
#define devnull "/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(devnull, O_WRONLY);if (fd > 0){// 各種重定向dup2(fd, 0);dup2(fd, 1);dup2(fd, 2);close(fd);}}
}