【守护进程 】【序列化与反序列化】
目录
- 1. 前后台任务
- 2. 守护进程
- 3. TCP的其它概念
- 4. 序列化与反序列化
1. 前后台任务
我们之前在 信号(一)【概念篇】 介绍过 Crtl + c 的本质就是给前台进程发送一个信号,进程执行该信号的默认处理动作,进而终止进程,随后我们还证明后台进程是无法接收到 Crtl + c 之类的信号的,即只有前台进程才能获得标准输入。
并且,我们曾经是这样说的:“ 一次登录就启动一个终端,一个终端一般配一个bash,每一次登陆,只允许一个进程是前台进程 ”,其中每次登录,linux 系统就会形成一个会话,每一个会话都会创建一个 bash 进程,一个session只能有一个前台进程在运行,而键盘信号只能发送给前台进程。而因为键盘文件只有一个,因此前台进程只能有一个。
[outlier@aliyun process]$ ./test >> log.txt &
[1] 9381
[outlier@aliyun process]$ ./test >> log2.txt &
[2] 9383
[outlier@aliyun process]$ ./test >> log3.txt &
[3] 9384
# 其中的[1] [2] [3] 称为后台任务号
[outlier@aliyun process]$ jobs # 查看后台任务
[1] Running ./test >> log.txt &
[2]- Running ./test >> log2.txt &
[3]+ Running ./test >> log3.txt &
[outlier@aliyun process]$ fg 2 # 将后台进程提升为前台进程
./test >> log2.txt
[outlier@aliyun process]$ fg 3
./test >> log3.txt
^Z # 通过暂停进程将前台变为后台
[3]+ Stopped ./test >> log3.txt
[outlier@aliyun process]$ jobs
[1]- Running ./test >> log.txt &
[3]+ Stopped ./test >> log3.txt
[outlier@aliyun process]$ bg 3 # 将暂停的后台任务重新运行
[3]+ ./test >> log3.txt &
[outlier@aliyun process]$ jobs
[1]- Running ./test >> log.txt &
[3]+ Running ./test >> log3.txt &
[outlier@aliyun process]$ ./test >> log.txt &
[1] 9490
[outlier@aliyun process]$ sleep 1000 | sleep 2000 | sleep 300 &
[2] 9493
[outlier@aliyun process]$ ps ajx | head -1 ; ps ajx | grep -Ei 'sleep|test'
进程组ID
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
8465 9490 9490 8465 pts/0 9507 S 1001 0:00 ./test
8465 9491 9491 8465 pts/0 9507 S 1001 0:00 sleep 1000
8465 9492 9491 8465 pts/0 9507 S 1001 0:00 sleep 2000
8465 9493 9491 8465 pts/0 9507 S 1001 0:00 sleep 300
其中的 session id = bash,多个任务(进程组),只要是在同一个会话中启动的,其 session id 是一样的。
任务与进程组的关系:任务的派发是以进程组为单位进行的。每个任务由指定的一个进程组完成。
因此所谓的前、后台进程,我们称为前台任务、后台任务。
当整个会话退出后,理论上,会话内的后台进程也要退出(比如 windows 的用户注销功能),而在连接远端 linux 时,会话关闭了,后台进程是保留下来的(只要不涉及到标准输出)。
现象:即便在会话关闭后,后台进程保留下来了,但依旧受到了用户登录和退出的影响:进程转而被OS领养,没有所谓的终端启动信息。
如果我们想要启动一种不受用户登录和退出影响的进程,即进程守护化。
2. 守护进程
进程是在会话中启动的,因此要使得某个进程不受用户登录和退出影响,那么只需要让指定进程自成一个会话,脱离原本的会话,将来会话退出了,该进程就也不受任何影响(因为它已经不属于原本的会话)。
守护进程:自成进程组 && 自成会话
NAME
setsid - create session and set process group ID
SYNOPSIS
#include <unistd.h>
pid_t setsid(void); // 使调用进程成为新会话的唯一进程(调用进程不能是一个进程组的组长)
RETURN VALUE
Upon successful completion, setsid() shall return the value of the new process group ID of the calling process.
Otherwise, it shall return (pid_t)-1 and set errno to indicate the error.
// Daemon.hpp
#pragma once
#include <iostream>
#include <string>
#include <cstdlib>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
const std::string nullfile = "/dev/null";
void Daemon(const std::string& cwd = "")
{
// 1. 忽略其它异常信号
signal(SIGCHLD, SIG_IGN);
signal(SIGSTOP, SIG_IGN);
signal(SIGPIPE, SIG_IGN);
// 2. 使进程独立会话
if(fork() > 0) exit(0); /* 因为守护进程不能是进程组的leader,所以fork一次,然后父进程直接退出
setsid(); 因此守护进程的本质也是孤儿进程,只不过孤儿时设置了新的session id */
// 3. 更改调用进程的工作目录
if(!cwd.empty()) chdir(cwd.c_str());
// 4. 标准输入、输出、错误重定向到 /dev/null
// /dev/null 相当于一个垃圾桶,凡是写到这个字符文件的数据都会被丢弃
// 之所以不直接关闭,原本向标准输入、输出、错误打印的数据就会出错。
int fd = open(nullfile.c_str(), O_WRONLY);
if(fd > 0)
{
dup2(fd, 0);
dup2(fd, 1);
dup2(fd, 2);
close(fd);
}
}
// TcpServer.hpp
#include "Daemon.hpp"
class TcpServer
{
void Start()
{
log.Enable(Classfile); // 文件日志
Daemon(); // 进程守护化
...
}
}
具体的TCP通信编码请跳转 tcp_scoket 观阅。
关于守护进程,Linux 也提供了可直接调用的系统接口
NAME
daemon - run in the background
SYNOPSIS
#include <unistd.h>
int daemon(int nochdir, int noclose)
参数:
nochdir:是否改变当前工作目录,0 表更改为根目录 /, 非零则保留当前工作目录。
noclose:是否关闭标准输入、标准输出和标准错误,0表关闭,非0表保持。
3. TCP的其它概念
TCP 通信是全双工的,全双工指的是:允许读和写操作同时进行,服务端在往客户端写入数据的同时也能够接收来自客户端的信息。
TCP 之所以能做到全双工,就是因为底层的发送和接收的缓冲区是独立的,因此即便读和写同时进行,数据也不会互相影响。
4. 序列化与反序列化
-
在TCP、UDP通信中,我们通常使用 write 发送数据、read 接收数据。但是,当我们从网络中读取数据时,接收到的内容实际上是一个字符串,这就引发了几个关键问题,比如:对方发送数据的速度非常快,可能会连续发送多个报文,但是另一方 read 一次性就读取了多个报文,那么我们该如何确定读取到的数据是一个完整的报文,或者我怎么知道从哪里到哪里是一个报文,之后是一个报文?即如何保证能够正常的协议解析?
因此,面对这些问题,我们需要在网络通信中引入一些机制,以确保报文的完整性和可解析性,即协议定制。
-
TCP 通信是全双工的,可以同时进行数据发送与接收。但是当用户把数据通过系统调用拷贝到 TCP 的发送缓冲区,接着由TCP 负责由数据发送到对方的缓冲区,而 TCP 是保证可靠性的,可能在网络传输中会有数据丢包等问题,TCP 就需要做数据重传等动作。换言之,TCP是传输控制协议,即,数据从用户层拷贝到了TCP 的缓冲区后,TCP 对数据的一切操作由 TCP 自己来控制(什么时候发数据,发多少,出错了如何处理),上层调用的 write、read等接口,只是将数据从用户拷贝到内核的某个缓冲区而已(相当于用户把数据交给OS处理),用户调用了 write,即便调用完成返回了,但这只是把数据拷贝到 TCP 的发送缓冲区,不一定就已经发送给对方了(这一点就如同我们在介绍用户缓冲区是一致的,用户调用 fprintf 之类的接口将数据写入外设,但函数调用完成后,不代表数据就已经写入到外设中了,数据只是到了内核的缓冲区而已,接着由内核全权负责接下来的工作)。
数据发送了,总得有人接收吧?但是接收方可并不知道发送方一次发送多少数据啊,用户自己都不知道(因为用户把数据交给 TCP 了,可能用户一次性 write 5个报文,但 TCP 只发送了一个),那接收方就无法得知,自己 read 读取到的数据是完整的一个报文,还是半个,还是多个,每个报文之间的界限在哪里,这些对接收方都是完全不确定的事情。
-
因此,基于上述问题,我们就需要定制协议:
比如我们要实现一个网络版计算器,实现 + - × ÷ %,客户端把要计算的两个加数发过去,然后由服务器进行计算,最后再把结果返回给客户端。
客户端以
struct Request { int a; int b; };
将数据发送给服务端,服务端以同样的 struct 结构体对数据进行接收,对于struct Response { int sum; };
计算结果的返回也是如此,双方都约定好相同的数据类型。但是,在应用层一般还要再加上一种机制 ---- 序列化与反序列化,因为结构体有内存对齐的概念,在不同平台的不同编译器下,同样的结构体编译出来的结构体大小可能是不一样的(内存对齐的标准不一样),所以如果直接以结构体类型发送数据,那么客户端和服务端可能发送与接收到的数据大小就不一致,那这还玩个毛。
下一篇文章 网络版计算器,我们会自定义协议 + 序列化与反序列的具体实现,结合 TCP 网络通信实现一个网络版计算器。
如果感觉该篇文章给你带来了收获,可以 点赞👍 + 收藏⭐️ + 关注➕ 支持一下!
感谢各位观看!