Linux 序列化技术、自定义协议实现及守护进程
Linux-序列化与自定义协议
1. 场景
- 进程间通信:
- 管道、消息队列、共享内存、网络套接字等,它们传递的都是原始的字节流。如果你想在两个进程间传递一个
struct student { int id; char name[20]; },你必须先将这个结构体序列化成字节流发送,接收方再反序列化回结构体。
- 管道、消息队列、共享内存、网络套接字等,它们传递的都是原始的字节流。如果你想在两个进程间传递一个
- 数据持久化:
- 将一些数据保存到文件中。关机后内存会丢失,但文件不会。下次启动时,从文件中读取字节流并反序列化,就能恢复到上次的状态。
- 网络通信(最常用):
- 这是最典型的应用。客户端和服务器程序可能运行在不同的机器、不同的操作系统甚至由不同的编程语言编写。它们之间通信的唯一“通用语言”就是字节流。一个定义好的序列化协议确保了双方都能正确理解数据。
2. 序列化与反序列化
2.1 概念
你可以把它们想象成 “打包” 和 “拆包” 的过程。
- 序列化:
- 过程:将一个存在于内存中的、结构复杂的对象(例如,一个 C 语言的结构体
struct,或一个 C++ 的类对象)转换成一个平坦的、连续的字节序列(字节流)。 - 目的:这个字节序列可以被轻松地存储到文件中,或者通过网络发送到另一台机器。
- 过程:将一个存在于内存中的、结构复杂的对象(例如,一个 C 语言的结构体
- 反序列化:
- 过程:将接收到的或从文件中读取的字节序列,重新构造成一个与原始对象一模一样的内存对象。
- 目的:恢复数据的状态,使得程序可以像使用原始对象一样使用它。
2.2 字节流 VS 数据报
2.2.1 什么是字节流?
字节流是一个连续的、有序的字节序列,数据像流水一样,一个字节接一个字节的进行读取或写入。
重要特性:
- 无边界性
- 字节流本身没有消息边界的概念。
- 发送方写入10次的数据,接收方可能1次就全部读完。
- 发送方写入1次的数据,接收方可能分10次读完。
- 有序性
- 先发送的字节一定先到达,顺序不会错乱。
- 面向连接
- 典型的字节流服务(如TCP)需要先建立连接。
2.2.2 什么是数据报?
数据报是一种在网络中传输的独立的数据单元。
重要特性:
- 有边界性
- 数据报保留完整的消息边界。
- 发送方写入N次的数据,接收方一定通过N次读取读完。
- 每个数据报都是独立的,不会与其他数据报混合。
- 无序可能性
- 先发送的数据报可能后到达,顺序无法保证。
- 无连接
- 典型的数据报服务(如UDP)不需要先建立连接。
数据报传输的是一个个独立的包,而字节流传输的是一个连续的、没有固定分界的数据流。
2.3 字节流的发送方式
2.3.1 字符串方式
特点:
- 数据以可读的文本形式组织。
- 通常使用编码格式(如UTF-8、GBK)。
1.纯文本格式
// 发送方
const char* message = "Hello,World,123\n";
send(sockfd, message, strlen(message), 0);// 接收方
char buffer[1024];
recv(sockfd, buffer, sizeof(buffer), 0);
// 接收到的可能是: "Hello,World,123\n"
2.结构化文本格式(JSON、XML等)
// 发送JSON格式
const char* json_str = "{\"name\":\"Alice\",\"age\":25,\"score\":95.5}\n";
send(sockfd, json_str, strlen(json_str), 0);// 接收方需要解析JSON
// {"name":"Alice","age":25,"score":95.5}
2.3.2 二进制方式
特点:
- 数据以原始的二进制格式组织。
- 直接操作内存字节,效率高。
#include <arpa/inet.h>struct Data {uint32_t id;uint16_t age;
};// 发送二进制数据
void send_binary(int sockfd) {struct Data data;data.id = htonl(1001); // 转换字节序data.age = htons(20); // 转换字节序send(sockfd, &data, sizeof(data), 0); // 直接发送结构体
}
2.3.3 总结
字符串方式(文本协议)通常是更推荐的选择。
二进制方式会有如下问题:
- 结构体内存对齐问题。
- 语言不同问题。
2.4 TCP粘包问题
当TCP读取数据的时候,读取到的报文不完整或者多读,导致下一个报文不完整,这个问题被称为“粘包”问题。
TCP是字节流,本身没有“包”的概念,这本质是应用层的表述。
客户端发送了多个结构逻辑数据,在接收时被合并成了一个TCP字节流片段。 本质的原因是因为TCP没有消息边界。因此,我们需要自己在应用层维护。
常见的解决方案:长度前缀法 + 分隔符法。
为序列化后的数据添加报头,报头中存在数据长度字段,报头与数据中可以通过分隔符间隔。
2.5 JSON
在C++中使用JSON,需要采用JsonCpp开源的第三方库,JSON主要被用于序列化和反序列化。
2.5.1 安装(ubuntu)
sudo apt-get install libjsoncpp-dev
注意:
- 编译时需携带 -ljsoncpp
- 头文件引入 #include <jsoncpp/json/json.h>
2.5.1 JSON序列化
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;
}
// 结果:
{"name" : "joe","sex" : "男"
}
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 或 Json::StyledWriter。
#include <iostream>
#include <string>
#include <sstream>
#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;
}
// 结果:
{"name":"joe","sex":"男"}Json::StyledWriter writer;
// 结果:
{"name" : "joe","sex" : "男"
}
2.5.2 JSON反序列化
使用 Json::Reader:
#include <iostream>
#include <string>
#include <jsoncpp/json/json.h>
int main() {// JSON 字符串std::string json_string = "{\"name\":\"张三\",\"age\":30,\"city\":\"北京\"}";Json::Reader reader;Json::Value root;// 从字符串中读取 JSON 数据bool parsingSuccessful = reader.parse(json_string, root);if(!parsingSuccessful) {std::cout << "解析失败" << 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;
}
3. 自定义协议
3.1 概念理解
自定义协议是指在网络通信中,应用程序自己定义的数据格式和通信规则,而不是使用现有的标准协议(如 HTTP、FTP、SMTP 等)。
应用程序为了特定需求而设计的数据格式、消息结构和通信规则的集合。
自定义协议:通信双方约定的完整通信规则和数据格式。
只有序列化(缺少协议):
- 这是什么类型的数据?
- 数据有多长?
- 如果处理这个数据?
序列化实际是自定义协议的一部分。
3.2 基于自定义协议的TCP网络版本计算器实现
传输的数据格式(协议):
[Json字符串的长度]\r\n[Json字符串]\r\n
10\r\n{"x":10, "y":20, "oper":"+"}\r\n
3.2.1 Socket.hpp
Linux中原生Socket的封装。
#pragma once#include "Common.hpp"
#include "Log.hpp"
#include "InetAddr.hpp"namespace SocketMoudle{const static int default_backlog = 16;// 设计为虚基类,TCP和UDP的模板类class Socket{public:virtual ~Socket() {}virtual void socket() = 0;virtual void bind(uint16_t server_port) = 0;virtual void listen(int backlog) = 0;virtual std::shared_ptr<Socket> accept(InetAddr* client_addr) = 0;virtual void close() = 0;virtual int recv(std::string* res) = 0;virtual int send(const std::string& message) = 0;virtual void connect(const InetAddr& server_addr) = 0;public:void buildTcpListenSocket(uint16_t server_port , int backlog = default_backlog) {socket();bind(server_port);listen(default_backlog);}void buildTcpClientListendSocket() {socket();}};const static int default_sockfd = -1;class TcpSocket : public Socket{public: TcpSocket() :_sockfd(default_sockfd) {} // 初始化sockfd为listenfdTcpSocket(int acceptfd) :_sockfd(acceptfd) {} // 初始化sockfd为accpetfdvirtual void socket() override {_sockfd = ::socket(AF_INET , SOCK_STREAM , 0);if(_sockfd < 0) {LogModule::LOG(LogModule::LogLevel::FATAL) << "socket create error";exit(SOCKET_ERROR);}LogModule::LOG(LogModule::LogLevel::INFO) << "socket create success - sockfd: " << _sockfd;}virtual void bind(uint16_t server_port) override {InetAddr server_addr(server_port);int n = ::bind(_sockfd , server_addr.getSockaddr() , server_addr.getSockaddrLen());if(n < 0) {LogModule::LOG(LogModule::LogLevel::FATAL) << "bind error";exit(BIND_ERROR);}LogModule::LOG(LogModule::LogLevel::INFO) << "bind success - sockfd: " << _sockfd;}virtual void listen(int backlog) override {int n = ::listen(_sockfd , backlog);if(n < 0) {LogModule::LOG(LogModule::LogLevel::FATAL) << "listen error";exit(LISTEN_ERROR);}LogModule::LOG(LogModule::LogLevel::INFO) << "listen success - sockfd: " << _sockfd;}virtual std::shared_ptr<Socket> accept(InetAddr* client_addr) override {struct sockaddr_in client;socklen_t addrlen = sizeof(client);int acceptfd = ::accept(_sockfd , (struct sockaddr*)&client , &addrlen);if(acceptfd < 0) {LogModule::LOG(LogModule::LogLevel::WARNING) << "accpet warning";return nullptr;}client_addr->setSockaddrIn(client); // 输出型参数return std::make_shared<TcpSocket>(acceptfd); // 多个服务器可以共享一个客户端的链接}virtual void close() override {if(_sockfd > 0)::close(_sockfd); // close(listenfd) 或 close(accpetfd)}// recv返回值和::recv返回值保持一致virtual int recv(std::string* res) override {char buffer[1024];ssize_t n = ::recv(_sockfd , buffer , sizeof(buffer) , 0);*res += buffer; // 读取可能会不完整return n;}virtual int send(const std::string& message) override {ssize_t n = ::send(_sockfd , message.c_str() , message.size() , 0);return n;}virtual void connect(const InetAddr& server_addr) {int n = ::connect(_sockfd , server_addr.getSockaddr() , server_addr.getSockaddrLen());if(n < 0) {LogModule::LOG(LogModule::LogLevel::FATAL) << "connect server error";exit(CONNECT_ERROR);}}private:int _sockfd; // listenfd 或 accpetfd };
}
3.2.2 Common.hpp
存放公共代码和头文件的头文件。
#pragma once#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <memory>
#include <unistd.h>
#include <functional>enum ExitCode{OK = 0,SOCKET_ERROR,BIND_ERROR,LISTEN_ERROR,USAGE_ERROR,CONNECT_ERROR,FORK_ERRO
};class NoCopy{public:NoCopy() = default;~NoCopy() = default;NoCopy(const NoCopy&) = delete;NoCopy& operator=(const NoCopy&) = delete;
};
3.2.3 InetAddr.hpp
网络序列与主机序列相互转换的头文件。
#pragma once#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string>
#include <cstring>const std::string any_ip = "0.0.0.0";// 解析IP地址和端口号的类
class InetAddr{public:InetAddr() = default;// 这个构造函数用来将 struct sockaddr_in 结构体转换为 // - 1.本地序列的字符串风格的点分十进制的IP // - 2.本地序列的整数端口// 网络转主机InetAddr(const struct sockaddr_in& addr) :_addr(addr){_port = ntohs(addr.sin_port);char ip_buffer[64];inet_ntop(AF_INET , &addr.sin_addr , ip_buffer, sizeof(ip_buffer));_ip = ip_buffer;}void setSockaddrIn(const struct sockaddr_in& addr) {_addr = addr;_port = ntohs(addr.sin_port);char ip_buffer[64];inet_ntop(AF_INET , &addr.sin_addr , ip_buffer, sizeof(ip_buffer));_ip = ip_buffer;}// 主机转网络// #define INADDR_ANY 0InetAddr(const std::string ip , u_int16_t port) :_ip(ip),_port(port){memset(&_addr , 0 , sizeof(_addr));_addr.sin_family = AF_INET;_addr.sin_port = htons(_port);inet_pton(AF_INET , _ip.c_str() , &_addr.sin_addr);}InetAddr(u_int16_t port) :_port(port),_ip(any_ip){memset(&_addr , 0 , sizeof(_addr));_addr.sin_family = AF_INET;_addr.sin_port = htons(_port);_addr.sin_addr.s_addr = INADDR_ANY; // inet_pton(AF_INET , _ip.c_str() , &_addr.sin_addr);}const std::string& getIP() const { return _ip; }u_int16_t getPort() const { return _port; }const struct sockaddr_in& getSockaddrin() const { return _addr; } const struct sockaddr* getSockaddr() const { return (const struct sockaddr*)&_addr; }struct sockaddr* getSockaddr() { return (struct sockaddr*)&_addr; }socklen_t getSockaddrLen() const { return sizeof(_addr); }// 格式化显示IP + Portstd::string showIpPort() const {return "[" + _ip + " : " + std::to_string(_port) + "]";}bool operator==(const InetAddr& addr) const {return _ip == addr.getIP() && _port == addr.getPort(); }private:struct sockaddr_in _addr;std::string _ip;u_int16_t _port;
};
3.2.4 TcpServer.hpp
通过多进程实现的Tcp服务器。
#pragma once#include "Socket.hpp"
#include <sys/types.h>
#include <sys/wait.h>using namespace SocketMoudle;using callback_t = std::function<void(std::shared_ptr<Socket>& , const InetAddr&)>;class TcpServer{public:TcpServer(u_int16_t port , callback_t callback):_tcp_listensocket(std::make_unique<TcpSocket>(port)),_port(port),_is_running(false),_callback(callback){// 多态调用socket/bind/listen_tcp_listensocket->buildTcpListenSocket(_port);}void start() {_is_running = true;while(_is_running) {// 获取客户端的连接InetAddr client_addr;std::shared_ptr<Socket> tcp_acceptsocket = _tcp_listensocket->accept(&client_addr);if(tcp_acceptsocket == nullptr) {continue;}// 多进程/多线程处理客户端连接pid_t pid = fork();if(pid < 0) {LogModule::LOG(LogModule::LogLevel::FATAL) << "server fork error";exit(FORK_ERRO);} else if(pid == 0) {// child process// 子进程关闭不需要的文件描述符_tcp_listensocket->close(); // close(accpetfd), 这里子进程可能会写时拷贝if(fork() > 0)exit(OK); // 子进程正常退出,父进程回收// 孙子进程被系统领养,自动回收资源// 回调函数处理服务器与客户端的连接_callback(tcp_acceptsocket , client_addr); tcp_acceptsocket->close();exit(OK);}// parent process// 父进程关闭不需要的文件描述符tcp_acceptsocket->close(); // close(listenfd)// 回收子进程,循环继续执行获取与客户端的连接waitpid(pid , nullptr , 0);}_is_running = false; }private:std::unique_ptr<Socket> _tcp_listensocket;u_int16_t _port;bool _is_running;callback_t _callback;
};
3.2.5 Protocol.hpp
这是一个协议类,主要功能有提供结构化数据的序列化和反序列化,以及添加报头和分离报头的功能。接收(客户端/服务器)发送的数据,并将处理的结果进行返回。
#pragma once
#include <iostream>
#include <string>
#include "Socket.hpp"
#include <jsoncpp/json/json.h> // 引入jsoncpp第三方库using namespace SocketMoudle;// 基于网络版本的计算器
class Request{public:Request() = default;Request(int x , int y , char op):left(x),right(y),oper(op){}std::string serialization() {Json::Value root;root["x"] = left;root["y"] = right;root["oper"] = oper;Json::StyledWriter writer;return writer.write(root);}bool deserialization(const std::string& data) {Json::Value root;Json::Reader reader;bool ok = reader.parse(data , root);if(!ok) {LogModule::LOG(LogModule::LogLevel::WARNING) << "request deserialization error";return false;}left = root["x"].asInt();right = root["y"].asInt();oper = root["oper"].asInt();return true;}int get_x() const { return left; }int get_y() const { return right; }char get_oper() const { return oper; }private:int left;int right;char oper;
};class Response{public:Response(int res = 0 , bool _valid = false) :result(res),valid(_valid){}std::string serialization() {Json::Value root;root["result"] = result;root["valid"] = valid;Json::StyledWriter writer;return writer.write(root);}bool deserialization(const std::string& data) {Json::Value root;Json::Reader reader;bool ok = reader.parse(data , root);if(!ok) {LogModule::LOG(LogModule::LogLevel::WARNING) << "response deserialization error";return false;}result = root["result"].asInt();valid = root["valid"].asBool();return true;}void showResult() {std::cout << "result: " << result << " [valid:" << valid << "]" << std::endl;}private:int result;bool valid;
};using calculate_t = std::function<Response(const Request&)>;// 协议类,需要解决两个问题
// 1. 需要有序列化和反序列化功能
// 2. 对于Tcp必须保证读到完整的报文
const static std::string sep = "\r\n";
// format: 10\r\n{"x":10, "y":20, "oper":"+"}\r\n
class Protocol{public:Protocol() = default; Protocol(calculate_t cal_handler) :_cal_handler(cal_handler) {}// 添加报头std::string encode(const std::string& jsonstr) {std::string json_len = std::to_string(jsonstr.size());return json_len + sep + jsonstr + sep;}// 分离报头bool decode(std::string& buffer_queue , std::string& res) {size_t pos = buffer_queue.find(sep);if(pos == std::string::npos) {return false;}std::string json_len = buffer_queue.substr(0 , pos); // 有效载荷总长度int packet_len = json_len.size() + std::stoi(json_len) + 2 * sep.size();if(packet_len > buffer_queue.size()) {return false; //说明当前读取的数据不足一个完整的报文,读取失败,应该继续读取}// 来到这里,当前已经有一个完整的报文或者多个完整的报文,或者一个半报文res = buffer_queue.substr(json_len.size() + sep.size() , std::stoi(json_len)); //将有效载荷带回上层// 将整个报文从buffer_queue分离buffer_queue.erase(0 , packet_len);return true;}void getClientAccept(std::shared_ptr<Socket>& client_accpet , const InetAddr& client_addr) {LogModule::LOG(LogModule::LogLevel::INFO) << "accept success client: " << client_addr.showIpPort();std::string buffer;while(true) {int n = client_accpet->recv(&buffer);if(n > 0) {// 1.可能读到不是一个完整的报文 decode为false内层循环不进去,执行外层循环继续读取// 2.也可能读取到多个完整的报文 decode为true内层循环持续处理多个完整报文std::string jsonstr;while(decode(buffer , jsonstr)) {// 3.服务端接收客户端的计算任务,需要反序列化Request req;req.deserialization(jsonstr);// 4.将反序列化的结果交给上层计算模块处理Response res = _cal_handler(req);// 5.将计算后的结果序列化std::string resp_json = res.serialization();// 6.将序列化后的json字符串添加报头std::string packet = encode(resp_json);// 7.发送给客户端client_accpet->send(packet);}} else if(n == 0) {// client quitLogModule::LOG(LogModule::LogLevel::INFO) << "client: " << client_addr.showIpPort() << " quit";break;} else {LogModule::LOG(LogModule::LogLevel::ERROR) << "server recv error";break;}}}std::string buildClientRequest(int x, int y , char oper) {// 1.构建客户端请求Request req(x , y , oper);// 2.序列化std::string json_req = req.serialization();// 3.添加报头 return encode(json_req);}bool getServerResponse(std::shared_ptr<Socket>& client ,std::string& buffer_queue, Response* resq) {while(true) {// 可能读取到不是一个完整的报文 int n = client->recv(&buffer_queue);if(n > 0) {std::string json_response;bool ok = decode(buffer_queue , json_response);if(!ok)continue; //不是一个完整的报文,继续读取while(ok) {// 保证了肯定有一个完整的报文,但是可能会有多个,所以需要连续处理// 4.反序列化resq->deserialization(json_response);// 5.显示结果resq->showResult();// sleep(100); debugok = decode(buffer_queue , json_response);}return true;} else if(n == 0) {// server quitLogModule::LOG(LogModule::LogLevel::INFO) << "server quit";return false;} else {LogModule::LOG(LogModule::LogLevel::INFO) << "recv error";return false;}}}private:calculate_t _cal_handler;
};
3.2.6 Calculate.hpp
主要负责处理数据的类。
#pragma once#include "Protocol.hpp"class Calculate{public:Response execute(const Request& req) {switch(req.get_oper()) {case '+':return Response(req.get_x() + req.get_y() , true);case '-':return Response(req.get_x() - req.get_y() , true);case '*':return Response(req.get_x() * req.get_y() , true);case '/':{if(req.get_y() == 0)return Response(0, false);else return Response(req.get_x() / req.get_y() , true);}default:LogModule::LOG(LogModule::LogLevel::WARNING) << "未知的操作符";}return Response(0 , false);}
};
3.2.7 TcpServer.cc
#include "TcpServer.hpp"
#include "Protocol.hpp"
#include "Calculate.hpp"// tcpserver port
int main(int argc , char* argv[]) {if(argc != 2) {LogModule::LOG(LogModule::LogLevel::FATAL) << argv[0] << " port";exit(USAGE_ERROR);}uint16_t server_port = std::stoi(argv[1]);// 1.应用层处理上层业务Calculate cal;// 2.表示层负责收发数据并进行序列化和反序列化Protocol protocol([&cal](const Request& req)->Response{return cal.execute(req);});// 3.会话层负责建立与客户端的链接std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(server_port , [&protocol](std::shared_ptr<Socket>& client_accpet , const InetAddr& client_addr){protocol.getClientAccept(client_accpet,client_addr);});tsvr->start(); // 启动服务器return 0;
}
3.2.8 TcpClient.cc
#include "Socket.hpp"
#include "Protocol.hpp"int main(int argc , char* argv[]) {if(argc != 3) {std::cout << argv[0] << " server_ip server port" << std::endl;exit(USAGE_ERROR);}std::string server_ip = argv[1];u_int16_t server_port = std::stoi(argv[2]);InetAddr server_addr(server_ip , server_port);std::shared_ptr<SocketMoudle::Socket> sptr = std::make_shared<SocketMoudle::TcpSocket>();sptr->buildTcpClientListendSocket();sptr->connect(server_addr); // 与服务器建立连接Protocol protocol;std::string buffer_queue;while(true) {int x , y;char oper;std::cout << "Please input x: ";std::cin >> x;std::cout << "Please input y: ";std::cin >> y;std::cout << "Please input operator: ";std::cin >> oper;// debug// std::cout << "x: " << x << " y: " << y << " oper: " << oper << std::endl;// 1.构建客户端请求std::string packet = protocol.buildClientRequest(x , y , oper);// 2.向服务器发送 sptr->send(packet);// 3.接收服务器返回结果Response resq;bool ok = protocol.getServerResponse(sptr ,buffer_queue, &resq);if(!ok) {break;}}sptr->close();return 0;
}
应用层、表示层、会话层一般来说都是用户自己需要实现的部分,这三层经常经常被合并为一层就是应用层。代码中的体现:
- Calculate.hpp 代表的是应用层处理业务的逻辑。
- Protocol.hpp 代表表示层负责收发数据并进行序列化和反序列化。
- TcpServer.hpp 代表会话层负责建立与客户端的连接。
4. 进程间关系与守护进程
4.1 进程组
每一个进程除了有一个进程唯一ID之外,还属于一个进程组。进程组是一个或者多个进程的集合,一个进程组可以包含多个进程。 每一个进程组也有一个唯一的ID,被称为PGID。
每一个进程组都有一个组长进程。组长进程的ID等于其进程组ID。
- 进程组组长的作用:进程组组长可以创建一个进程组或者创建该组中的进程。
- 进程组的生命周期:,从进程组创建开始到其中最后一个进程离开为止。注意: 主要某个进程组中,有一个进程存在,则该进程组就存在,这与其组长进程是否已经终止无关。
- 通常通过管道将几个进程编成一个进程组。
- 向一个前台进程组发送信号,该信号会被传递给组内的所有进程。
4.2 会话
4.2.1 什么是会话?
会话可以看成是一个或多个进程组的集合,一个会话可以包含多个进程组。每一个会话也会有一个会话ID(SID)。会话ID是会话中首进程的进程ID。
4.2.2 如何创建会话?
通过 setsid 函数来创建一个会话,前提是调用进程不能是一个进程组的组长。
#include <unistd.h>
/*功能:创建会话返回值:创建成功返回SID, 失败返回-1
*/
pid_t setsid(void);
该接口调用之后会发生:
- 调用进程会变成新会话的会话首进程。 此时,新会话中只有唯一的一个进程。
- 调用进程会变成进程组组长。 新进程组ID就是当前调用进程ID
- 该进程没有控制终端。 如果在调用setsid之前该进程存在控制终端,则调用之后会切断联系。
需要注意的是: 这个接口如果调用进程是进程组组长,则会报错,,为了避免这种情况,通常使用方法是先调用fork创建子进程,父进程终止,子进程继续执行,因为子进程会继承父进程的进程组ID,而进程ID则是新分配的,就不会出现错误的情况。
- 一个会话通常对应一个终端(如
tty1,pts/0)。 - 一个会话中,有且只有一个前台进程组,可以有多个后台进程组。
- 只有前台进程组中的进程才能从终端读取输入并向终端输出。
当你通过SSH登陆Linux时,系统会为你创建一个新的会话,并启动bash命令行解释器(其实就是首进程),让bash进程成为前台任务并关联终端
4.3 守护进程
守护进程是Linux中一种特殊的后台进程,它独立于控制终端并且长期运行,通常在操作系统启动时开启,操作系统关闭时终止。
Daemon.hpp
#pragma once#include <iostream>
#include <string>
#include <cstdio>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
#include <sys/stat.h>
#include <fcntl.h>const std::string dev = "/dev/null";// 将服务进行守护进程化的服务
void Daemon(int nochdir, int noclose)
{// 1. 忽略IO,子进程退出等相关的信号signal(SIGPIPE, SIG_IGN);signal(SIGCHLD, SIG_IGN);// 2. 父进程直接结束if (fork() > 0)exit(0);// 3. 只能是子进程,孤儿了,父进程就是1setsid(); // 成为一个独立的会话if(nochdir == 0)chdir("/");// 守护进程,不从键盘输入,也不需要向显示器打印// 打开/dev/null, 重定向标准输入,标准输出,标准错误到/dev/nullif (noclose == 0){int fd = ::open(dev.c_str(), O_RDWR);if (fd < 0){exit(OPEN_ERR);}else{dup2(fd, 0);dup2(fd, 1);dup2(fd, 2);close(fd);}}
}
- 为什么忽略
SIGPIPE?- 当一个进程向一个已经关闭的管道、socket等写入会触发终止进程的行为,守护进程通常作为网络服务运行,可能会遇到客户端意外断开连接的情况,如果不忽略,可能会网络写入失败导致守护进程被意外终止。
- 为什么忽略
SIGCHLD?- 守护进程通常不会创建子进程,或者创建了也不关系子进程的退出状态。设置为
SIG_IGN可以让内核自动回收子进程,避免僵尸进程。
- 守护进程通常不会创建子进程,或者创建了也不关系子进程的退出状态。设置为
- 为什么要将工作目录改为
/?- 根目录总是存在的,确保守护进程中使用到的路径都是以根目录为相对路径开始的。
daemon函数(现成将当前进程变成守护进程的函数)
#include <unistd.h>int daemon(int nochdir, int noclose);
函数:
nochdir:是否改变工作目录到根目录。- 0:改变工作目录到 /
- 非0:保持当前工作目录
noclose:是否重定向标准输入、输出、错误到 /dev/null。- 0:重定向到 /dev/null
- 非0:保持原来的文件描述符
返回值:
- 成功:返回0。
- 失败:返回-1。
