【Linux网络编程】之守护进程
- 进程组
- 进程组的概念
- 组长进程
- 会话
- 会话的概念
- 会话ID
- 控制终端
- 控制终端的概念
- 控制终端的作用
- 会话、终端、bash三者的关系
- 前台进程与后台进程
- 概念
- 特点
- 查看当前终端的后台进程
- 前台进程与后台进程的切换
- 作业控制
- 相关概念
- 作业状态(一般指后台作业)
- 守护进程
- 概念
- 将进程守护化
- 进程守护化的步骤
- 代码实现
进程组
进程组的概念
当我们使用以下命令查与进程相关的属性时,会看到一个叫PGID
的属性:
ps ajx
它标识某一个进程属于哪个进程组。
进程组是一个进程或者多个进程的集合。每一个进程组有唯一的PGID
,它是一个正整数,和PPID
和PID
一样,可以在C语言中用pid_t
类型表示。
例如,在终端中执行如下命令:
sleep 1000 | sleep 2000 | sleep 3000
使用ps
查看:
ps ajx | head -1 && ps ajx | grep -v 'grep' | grep sleep
这三个进程的PPID
也就是父进程都是一样的,也就是-bash
进程,所以这三个sleep
进程是兄弟进程,它们同属于一个进程组。
就算只有一个进程,它也会自成一个进程组:
组长进程
组长进程就是进程组中的第一个创建的进程(按照时间),如果这个进程组中就只有一个进程,那么它就是进程组中的组长。
当进程组中的组长终止后,这个进程组并不会终止,要等到这个进程组中的最后一个进程终止,它才会终止。
代码验证1:使用下面代码先验证,C语言fork
子进程,父进程和子进程属于一个进程组,且父进程是组长进程。
#include<stdio.h>
#include<unistd.h>
int main()
{
pid_t id = fork();
if(id == 0)//子进程执行
{
while(1)
{
printf("i am child,my pid is %d\n",getpid());
sleep(1);
}
}
while(1);
return 0;
}
运行结果:
代码验证2:当组长进程结束,进程组并不会立即终止,而是等这个组中所有的进程终止后再终止:
#include<stdio.h>
#include<unistd.h>
int main()
{
pid_t id = fork();
if(id == 0)//子进程执行
{
while(1)
{
printf("i am child,my pid is %d\n",getpid());
sleep(1);
}
}
return 0;
}
运行结果:
会话
会话的概念
会话可以看作是多个进程组的集合,一个会话会有多个进程组,会话也会有唯一的会话ID。
一般来说,会话中的第一个进程组是-bash
对应的进程组,会话ID等于该-bash
进程组的组ID。
通常来说,进程组可以由以下方式创建:
- 通过管道创建兄弟进程,这些兄弟进程是一个进程组
- 父进程
fork
子进程,父子进程是一个进程组。
会话ID
上面我们提到了会话ID,会话ID就是该会话中首进程的进程ID或者说是首进程的组ID。(就是-bash
进程的进程ID,-bash
进程总是会首进程,也是会首进程的组长进程)。
控制终端
控制终端的概念
在Linux/类Uinx系统中,控制终端通常与会话关联。即一个会话对应一个控制终端。
当用户通过一个终端登录系统,会得到一个首进程shell
进程,这个终端成为shell
进程的控制终端,由于有关控制终端的信息存储在进程PCB
中,后续的其它进程都是通过shell
进程fork
的,所以其它进程的控制终端也都是这个控制终端。
实际上控制终端是一个逻辑概念,每个控制终端都对应一个终端文件。这些文件被称为终端文件或tty设备文件。
在Linux
中,终端文件在/dev/pts
路径下:
ls /dev/pts
实验验证:实验步骤如下。
-
终端1,循环执行以下脚本指令:
while :;do ls -l /dev/pts;sleep 1;echo "~~~~~~~~~~~~~~~~~";done
-
不断创建新的终端,观察打印的终端文件是否增多。
-
关闭打开的终端,观察打印的终端文件是否减少。
实验现象如下:
控制终端的作用
-
信号发送:可以通过特定的组合键向前台进程组发送信号,比如
Ctrl+C
发送SIGINT
信号来中断当前操作,或者Ctrl+Z
发送SIGTSTP
暂停一个进程。 -
输入输出:控制终端为进程提供标准输入、输出和错误流。大多数情况下,这些流直接对应于用户的键盘输入和屏幕显示。
-
什么意思呢,就是我们向键盘输入一个内容都会显示在控制终端上,
printf
等往显示器打印的函数,打印的内容也会显示到终端上。因为**当进程启动时,如果没有特别指定其他的输入输出目的地(重定向),其标准输入、输出和错误流默认就会关联到控制终端对应的设备文件上。**云服务器上文件描述符0
、1
、2
指向的文件: -
虚拟机中(也类似):
-
-
作业控制:允许用户管理属于当前shell会话的不同任务(作业)。这包括将作业放到后台执行或从前台恢复执行。作业和前台后台进程的概念我们稍后会谈。
会话、终端、bash三者的关系
Linux中的shell
进程叫做bash,当用户通过xshell
等ssh远程登录客户端中的终端登录后,这个终端成为bash进程的控制终端,而这个终端中的所有进程组(前台和后台)构成一个会话(当你登录系统并启动bash时,实际上就开启了一个新的会话。),画图来表示就是下面这样:
前台进程与后台进程
概念
前台进程和后台进程都是进程,唯一区别就是前台进程可以通过终端接收用户的输入,同时也可以接收来自用户的命令(ctrl C
、ctrl Z
),前台进程还可以将输出输入到显示器(也就是终端上)。但是后台进程则不同,它不直接与用户交互,即它们不接受键盘输入,也不将输出直接显示给用户(除非特别配置)。
特点
- 前台进程:
- 独占性:同一终端,同一时刻,只能有一个进程或进程组。这意味着前台进程对输入输出有独占性。
- 用户交互:前台进程可以直接通过终端与用户进行交互。这意味着它可以接受用户的输入,并将其显示在屏幕上。通常用户输入的命令,就是在前台运行。
- 信号响应:前台进程组可以接收到某些类型的信号,比如通过按下Ctrl+C发送的SIGINT中断信号来终止当前操作。这是前台进程的一个重要特性,允许用户直接控制正在运行的程序。
- 后台进程:
- 并发执行:多个后台进程可以同时执行(并发是看似同时执行,实则轮询执行),一个终端可以有多个后台作业同时执行。
- 无需与用户交互:不接收用户的输入,也不会将输出直接显示给用户。
- 信号限制:
ctrl C
等命令无法作用于后台进程,要使用kill
、killall
等命令。
查看当前终端的后台进程
命令jobs
可以查看当前终端的后台进程,它会显示每个后台作业的作业号和状态。
jobs
选项-l
:添加 -l
选项后,jobs
命令不仅显示基本的作业信息,还会额外列出每个作业的进程ID(PID)。
前台进程与后台进程的切换
-
前台进程切换为后台进程:
-
方法1
:在执行一个程序时,在后面加上&
。 -
方法2
:ctrl Z
暂停某个正在执行的前台进程,它将被切为后台进程: -
方法3
:当某个子进程的父进程结束,它还在运行时它就会变成孤儿进程。无法通过ctrl C
命令终止它的运行,因为孤儿进程被init
(SID
、PID
为1)进程收养,但可以通过kill
、killall
命令终止它。可以认为这是一种特殊情况。
-
-
后台进程切换为前台进程:
-
使用命令
fg
(foreground的缩写)将后台进程切换为前台进程:fg 作业号
-
扩展命令
bg
,这个命令可以将暂停的命令继续在后台运行。bg
`
-
作业控制
相关概念
作业:
作业是指Linux系统中正在运行的一个进程或者进程组(多个进程)。进程之间一般通过管道来互相协作。
作业控制:
shell
可以同时运行一个前台作业和多个后台作业,这叫做作业控制。前台作业和后台作业都可以由多个进程组成。
作业号:
作业号是后台作业专有的,它们在开始执行时或者执行完后会返回一个作业号。
示例1:
echo "1111" &
示例2:
sleep 1000 | sleep 2000 | sleep 3000 &
[1]
就是该作业的作业号。
作业状态(一般指后台作业)
常见的作业有以下状态:
作业状态 | 含义 |
---|---|
正在运行(Running) | 后台作业(&),正在运行 |
暂停运行(Stopped) | 前台作业被ctrl z (或者是后台作业被相应的信号所暂停)暂停了 |
完成(Done) | 后台作业已完成执行(返回的状态码为0) |
完成(Done(code)) | 后台作业完成执行(返回的状态码非0) |
终止(Terminated) | 后台作业被终止 |
-
Running
状态: -
Stopped
状态: -
Done
状态: -
Terminated
状态: -
Done(code)
状态。后台执行下面的脚本文件:exit 42 # 返回退出状态码42
守护进程
概念
守护进程不同于普通的后台进程(作业),普通的后台作业它有关联的终端,即使不与用户交互或者不输出内容到用户的显示器上,但是守护进程有自己独立的会话,并且它无终端关联,脱离终端控制,除非系统关闭,否则守护进程不会轻易的关闭。
常见的守护进程:比如httpd
(Apache HTTP服务器)、sshd
(SSH服务器)、crond
(定时任务调度器)等都是典型的守护进程,它们为系统提供核心服务。
普通后台程序:例如,你在终端中运行的一个长时间的数据处理脚本,并在其后加上&
让它在后台运行。这个脚本就是一个普通的后台程序,而不是守护进程。
将进程守护化
进程守护化的步骤
- 父进程
fork
子进程:让父进程退出,后续都是子进程来执行相关程序,并成为守护进程,因为创建一个新的会话需要该进程不是进程组的组长。这是系统设计所决定的,如果不这样做,会有循环依赖问题。 - setsid() 创建新会话:使子进程成为新会话的领导者,并脱离所有终端的控制,确保它没有控制终端。
- 改变当前工作目录(如果需要):通常是切换到根目录(
/
),以避免占用挂载点。 - 重定向标准输入、输出和错误流(如果后面不使用也可以关闭):通常指向
/dev/null
或其他适当的日志文件,以防止无意中使用这些默认流。/dev/null
文件读取都会读到null
,写入都会被自动丢弃。 - 关闭不需要的文件描述符:确保除了必要的资源外,其他文件描述符都被关闭。
代码实现
#include<stdio.h>
#include<unistd.h>
#include<stdbool.h>
#include <fcntl.h>
#include <stdlib.h>
#include <signal.h>
const char* newdir = "/";
const char* newpath = "/dev/null";
void Demon(bool IsChangeWorkDir,bool IsRedir)
{
//0.忽略可能引起程序异常的信号
// 忽略SIGCHLD信号,防止产生僵尸进程
signal(SIGCHLD, SIG_IGN);
// 忽略SIGPIPE信号,避免程序因写入已关闭的pipe/socket而终止
signal(SIGPIPE, SIG_IGN);
//1.创建子进程,并让父进程退出
if(fork() > 0)
{
printf("pid is %d\n",getpid());
exit(0);
}
//2.让子进程新建一个会话,成为守护进程,子进程成为新会话的leader进程,会话ID和这个子进程的进程ID相同
setsid();
//3. 已经成为守护进程,查看是否需要更改工作目录
if(IsChangeWorkDir)
{
chdir(newdir);
}
//4.查看是否需要重定向0、1、2标准输入、输入、错误流
if(IsRedir)
{
int fd = open(newpath,O_RDWR);//以读写模式打开
dup2(fd,0);
dup2(fd,1);
dup2(fd,2);
close(fd);//不需要fd了
}
else
{
close(0);
close(1);
close(2);
}
}
int main()
{
Demon(true,true);
while(true);//变成守护进程了
return 0;
}
运行结果: