一、进程组
每个进程除了有进程 ID(PID)之外,还属于一个进程组。进程组是一个或多个进程的集合,一个进程组可以包含多个进程,每个进程组也有一个唯一的进程组 ID(PGID)
,类似于进程 ID
每个进程组都有一个组长进程。组长进程的 ID 等于其进程 ID,进程组组长可以创建一个进程组或者创建该组中的进程
进程组的生命周期:从进程组创建开始到其中最后一个进程离开为止,只要进程组中有一个进程存在,则该进程组就存在,这与组长进程是否终止无关
二、会话
什么是会话
会话可以看作是一个或多个进程组的集合,一个会话可以包含多个进程组,每个会话也有一个会话 ID(SID)
创建一个会话,一般会形成一个终端文件,然后关联一个 bash 进程,bash 进程单独一个进程组,会话 ID 一般是会话中第一个进程 的 ID,一般是 bash 进程 ID
同一个会话中,可以同时存在多个进程组,但是,任何时刻只允许一个前台进程(组)运行,可以允许多个进程(组)后台运行
后台运行进程
如何创建会话
可以调用 setsid 函数,前提是调用进程不能是一个进程组的组长
#include <unistd.h>
pid_t setsid(void);
- 返回值:创建成功返回SID, 失败返回-1
该接⼝调⽤之后会发⽣:
- 调⽤进程会变成新会话的会话⾸进程。此时,新会话中只有唯⼀的⼀个进程
- 调⽤进程会变成进程组组⻓。新进程组ID就是当前调⽤进程ID
- 该进程没有控制终端。如果在调⽤setsid之前该进程存在控制终端,则调⽤之后会切断联系
需要注意的是:这个接⼝如果调⽤进程原来是进程组组⻓,则会报错,为了避免这种情况,我们通
常的使⽤⽅法是先调⽤fork创建⼦进程,⽗进程终⽌,⼦进程继续执⾏,因为⼦进程会继承⽗进程的
进程组ID,⽽进程ID则是新分配的,就不会出现错误的情况。
会话 ID
会话首进程的进程 ID 就是会话 ID
,会话首进程一般是 bash 进程,bash 进程组中就一个 bash 进程
三、控制终端
在UNIX 操作系统中,用户通过终端登录系统得到一个 shell 进程。这个终端成为 shell 进程的控制终端。控制终端是保存在 PCB 中的信息,我们知道 fork 进程会复制 PCB 中的信息,因此 shell 进程启动的其他进程的控制终端也是这个终端
-
一个会话可以有一个控制终端,通常会话首进程打开一个终端后,该终端就会成为该会话的控制终端
-
建立与控制终端连接的会话首进程被称为控制进程
-
一个会话中的进程组可被分为一个前台进程组和一个或多个后端进程组
-
进入终端的中断键(ctrl+c)或退出键(ctrl+),就会将 信号发送给前台进程组的所有进程
四、作业控制
作业和作业控制
作业是针对用户来讲,用户完成某项任务而启动的进程,一个作业既可以只包含一个进程,也可以包含多个进程,进程之间相互协作完成任务
shell 分前后台来控制的不是进程而是作业或进程组。一个前台作业可以有多个进程组成,shell 可以同时运行一个前台作业和任意多个后台作业,这被称为作业控制
作业号
放在后台执行的程序或命令称为后台命令,可以在命令的后面加上&符号
从而让 shell 识别到这是一个后台命令
,后台命令不用等待该命令执行完成,就可立即接受新的命令,另外后台进程执行完后会返回一个作业号以及一个进程号
可以看到作业号是 1,进程 ID 是 2600582
五、守护进程
守护进程(Daemon)是一类在后台运行的特殊进程,它们通常在系统启动时启动,并在系统运行期间提供各种服务,或者等待执行特定任务
守护进程的特点包括:
- 后台运行:守护进程在后台运行,不与任何控制终端直接关联,它们独立于用户直接交互。
- 长时间运行:守护进程通常在整个系统运行期间持续运行,或者至少在需要时保持活动状态。
- 服务提供者:守护进程提供系统或网络服务,例如Web服务器、数据库服务器、邮件服务器等。
- 监听请求:守护进程通常监听特定的端口或系统事件,等待客户端的请求或系统的通知。
- 无用户交互:守护进程不提供直接的用户交互界面,它们通过程序接口与系统或其他进程通信。
- 自动重启:在某些系统中,如果守护进程崩溃或被终止,系统可能会自动重启它们,以确保服务的连续性。
- 系统管理:守护进程可以用于执行系统管理任务,如日志记录、资源监控、定时任务等。
- 权限:守护进程通常以特定的用户身份运行(如root或其他系统用户),以执行需要特定权限的操作。
- 进程特性:在Unix-like系统中,守护进程可以通过修改进程的umask值、关闭文件描述符、创建新的会话和进程组等操作来设置自己的运行环境。
- 日志记录:守护进程通常会将运行状态和错误信息记录到日志文件中,以便系统管理员监控和调试。
守护进程的代码
六、如何将服务进程变成守护进程
守护进程的代码
#pragma once
#include <iostream>
#include <cstdlib>
#include <signal.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
const char *root = "/";
const char *dev_null = "/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(dev_null, O_RDWR);
if (fd > 0)
{
dup2(fd, 0);
dup2(fd, 1);
dup2(fd, 2);
close(fd);
}
}
}
使用自定义封装的函数
int main(int argc, char *argv[])
{
if (argc != 2)
{
std::cout << "Usage : " << argv[0] << " port" << std::endl;
return 0;
}
uint16_t localport = std::stoi(argv[1]);
Daemon(false, false);
std::unique_ptr<TcpServer> svr(new TcpServer(localport, HandlerRequest));
svr->Loop();
return 0;
}
使用库函数
daemon
#include <unistd.h>
int daemon(int nochdir, int noclose);
-
nochdir
如果 nochdir 为 0,daemon() 会将进程的工作目录更改为根目录 (/),以避免占用挂载的 文件系统。
如果 nochdir 为 非0,进程的当前工作目录保持不变。
-
noclose
如果 noclose 为 0,daemon() 会将标准输入 (stdin)、标准输出 (stdout) 和标准错误 (stderr) 重定向到 /dev/null,防止它们在终端输出或读取数据。
如果 noclose 为 非0,这些文件描述符保持不变。
-
返回值:
成功时返回 0。
失败时返回 -1,并设置 errno。