> 技术文档 > Linux之网络部分-应用层自定义协议与序列化

Linux之网络部分-应用层自定义协议与序列化


一、应用层

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 #include #include 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 #include #include #include #include int main(){ Json::Value root; root[\"name\"] = \"joe\"; root[\"sex\"] = \"男\"; Json::StreamWriterBuilder wbuilder; // StreamWriter 的工厂 std::unique_ptr writer(wbuilder.newStreamWriter()); std::stringstream ss; writer->write(root, &ss); std::cout << ss.str() << std::endl; return 0;}

3. 使用 Json::FastWriter:

  • 优点:比 StyledWriter 更快,因为它不添加额外的空格和换行符。
  • 示例代码:
#include #include #include #include #include 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 #include #include #include #include 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 #include #include 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 #include #include #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 <字符串 bool Serialize(std::string *out) {#ifdef SelfDefine // 条件编译 *out = std::to_string(_data_x) + ProtSep + _oper + ProtSep + std::to_string(_data_y); return true;#else Json::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 SelfDefine auto 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;#else Json::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;#else Json::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 SelfDefine auto 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;#else Json::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 BuildRequest() { std::shared_ptr req = std::make_shared(); return req; } std::shared_ptr BuildRequest(int x, int y, char op) { std::shared_ptr req = std::make_shared(x, y, op); return req; } std::shared_ptr BuildResponse() { std::shared_ptr resp = std::make_shared(); return resp; } std::shared_ptr BuildResponse(int result, int code) { std::shared_ptr req = std::make_shared(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

/*

        *功能:创建会话

        *返回值:创建成功返回 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 

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 #include #include #include #include #include #include #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); } }}