还记得我们之前学的前台和后台任务嘛,如下:
我们一般把不需要交互的任务放在 后台,我们之前也说了,如下:
我们可以发现在前台任务执行时,输入其他指令也不会产生别的影响,而在后台任务中,我们输入的每个指令都会有相对应的输出,因此我们可以知道:
🧀 作业是针对用户来讲,用户完成某项任务而启动的进程
Shell 分前后台来控制的不是进程,而是 作业 或者 进程组
🔥 放在后台执行的程序或命令称为后台命令,可以在命令的后面加上&符号从而让 Shell 识别这是一个后台命令,后台命令不用等待该命令执行完成,就可立即接收新的命令
第一行表示作业号和进程 ID, 可以看到作业号是 1, 进程 ID 是 3251046
作业状态 | 含义 |
---|---|
正在运行【Running】 | 后台作业(&),表示正在执行 |
完成【Done】 | 作业已完成,返回的状态码为 0 |
完成并退出【Done(code)】 | 作业已完成并退出,返回的状态码为 0 |
已停止【Stopped】 | 前台作业,当前被 Ctrl + Z 挂起 |
已终止【Terminated】 | 作业被终止 |
🐇 如果想将挂起的作业切回,可以通过 fg 命令,fg 后面可以跟作业号或作业的命令名称。如果参数缺省则会默认将作业号为 1 的作业切到前台来执行,若当前系统只有一个作业在后台进行,则可以直接使用 fg 命令不带参数直接切回。
具体的参数参考如下:
如下操作:
注意: 当通过 fg 命令切回作业时,若没有指定作业参数,此时会将默认作业切到前台执行,即带有 “+” 的作业号的作业
🐇 我们在执行某个作业时,可以通过 Ctrl+Z 键将该作业挂起,然后 Shell 会显示相关的作业号、状态以及所执行的命令信息。
例如我们把刚刚切回起来的作业挂起到后台:
我们可以直接通过输入 jobs 命令,查看本用户当前后台执行或挂起的作业
关于默认作业:对于一个用户来说,只能有一个默认作业(+),同时也只能有一个即将成为默认作业的作业(-),当默认作业退出后,该作业会成为默认作业。
上面提到了键入 Ctrl + Z 可以将前台作业挂起,实际上是将 STGTSTP 信号发送至前台进程组作业中的所有进程, 后台进程组中的作业不受影响。 在 unix 系统中, 存在 3 个特殊字符可以使得终端驱动程序产生信号, 并将信号发送至前台进程组作业, 它们分别是:
🔥 结论:
💢 对于上面学的,我们基于管道(|)把所有的任务级联起来
补充:通常我们都是使用管道将几个进程编成一个进程组,如上面
下面我们来做个详细了解
💦 上面我们说到其实每一个进程除了有一个进程 ID(PID)之外 还属于一个进程组。
$ ps -eo pid,pgid,ppid,comm | grep test
#结果如下
PID PGID PPID COMMAND
2830 2830 2259 test
# -e 选项表示 every 的意思, 表示输出每一个进程信息
# -o 选项以逗号操作符(,)作为定界符, 可以指定要输出的列
每一个进程组都有一个组长进程。 组长进程的 ID 等于其进程 ID。我们可以通过 ps 命令看到组长进程的现象:
$ ps ajx | head -1 && ps ajx | grep sleep
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
3185939 3288744 3185939 3185939 ? -1 S 1001 0:00 sleep 180
3099626 3289414 3289414 3099626 pts/0 3289414 S+ 1001 0:00 sleep 10000
3099626 3289415 3289414 3099626 pts/0 3289414 S+ 1001 0:00 sleep 20000
3099626 3289416 3289414 3099626 pts/0 3289414 S+ 1001 0:00 sleep 30000
3203921 3289620 3289619 3203921 pts/1 3289619 S+ 1001 0:00 grep --color=auto sleep
从结果上看 sleep 10000 进程的 PID 和 PGID 相同, 那也就是说明 sleep 10000 进程是该进程组的组长进程, 该进程组包括 sleep 10000 、sleep 20000、sleep 30000 三个进程
注意:主要某个进程组中有一个进程存在, 则该进程组就存在, 这与其组长进程是否已经终止无关。
补充一句,在 Linux 中我们一般使用 ps
命令查看进程的,如下:
$ ps -o pid,ppid,pgid,sid,comm
# 输出
PID PPID PGID SID COMMAND
3203921 3203920 3203921 3203921 bash
3292594 3203921 3292594 3203921 ps
刚刚我们谈到了进程组的概念, 那么会话又是什么呢?
会话其实和进程组息息相关,会话可以看成是一个或多个进程组的集合, 一个会话可以包含多个进程组。每一个会话也有一个会话 ID(SID)
上边我们提到了会话 ID, 那么会话 ID 是什么呢? 我们可以先说一下会话首进程, 会话首进程是具有唯一进程 ID 的单个进程, 那么我们可以将会话首进程的进程 ID 当做是会话 ID。
可以调用 setsid 函数来创建一个会话, 前提是调用进程不能是一个进程组的组长。
#include <unistd.h>
/*
*功能:创建会话
*返回值:创建成功返回 SID, 失败返回-1
*/
pid_t setsid(void);
该接口调用之后会发生:
需要注意的是: 这个接口如果调用进程原来是进程组组长, 则会报错, 为了避免这种情况, 我们通常的使用方法是:先调用 fork 创建子进程, 父进程终止, 子进程继续执行, 因为子进程会继承父进程的进程组 ID, 而进程 ID 则是新分配的, 就不会出现错误的情况
先说下什么是控制终端?
🔥 在 UNIX 系统中,用户通过终端登录系统后得到一个 Shell 进程,这个终端成为 Shell 进程的控制终端。
另外会话、进程组以及控制终端还有一些其他的关系,我们在下边详细介绍一下:
这些特性的关系如下图所示:
如果我们想要一个不受会话影响的进程,该怎么做呢?
守护进程也属于后台进程的一种,但是两者有个本质区别
守护进程(Daemon Process) 是一种在后台运行的特殊进程,通常用于执行系统任务或服务,而不依赖于用户交互。理解守护进程的关键在于它的两个核心特性:
这里我们以我们之前写的 网络版计算器 为例
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>
#define ROOT "/"
#define devnull "/dev/null"
void Daemon(bool ischdir, bool isclose)
{
// 1. 守护进程一般要屏蔽到特定的异常信号
signal(SIGCHLD, SIG_IGN);
signal(SIGPIPE, SIG_IGN);
// 2. 成为非组长, fork之后父进程直接退出, 子进程代替父进程向后运行
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);
}
}
}
TcpServer.cc 修改如下:
结果如下:
现在我们就可以把终端关掉了,然后该进程就在我们的云服务器上 24 小时运行了,此时我们也可以把它上传到网上进行使用的,来随时访问了
注意一下:这里当我们直接发送动态链接的可执行文件会出现阶层库的问题
所以如果有问题的话,我们最好还是发送其静态链接最好,加个 -static,编译的时候
补充 -- 系统提供的 daemon