一、任务管理
(一)进程组、作业、会话概念
**(1)进程组概念:**进程组是由一个或多个进程组成的集合,这些进程在某些方面具有关联性。在操作系统中,进程组是用于对进程进行分组管理的一种机制。每个进程组有一个唯一的进程组 ID 来标识它,通常情况下,进程组 ID 是其第一个加入该组的进程的进程 ID。
(2)作业概念: 作业是用户一次提交给计算机系统进行处理的任务集合 。它通常包括一个或多个进程,以及这些进程的相关资源(如输入文件、输出文件、环境变量等)。作业是用户与操作系统交互的基本单位,反映了用户想要完成的特定任务。例如,用户在命令行中输入一个命令(如gcc -o program source.c
),这个命令及其相关的输入文件(source.c
)、输出文件(program
)和编译过程中的临时文件等,共同构成了一个作业。
**(3)会话的概念:**会话是一个或多个进程组的集合。它提供了一种机制,用于将相关进程组织在一起,并管理它们与终端设备的交互。
会话组成部分:
- 控制终端:会话可以有一个控制终端,这是用户与系统交互的设备。控制终端可以是物理终端(如本地登录的终端设备),也可以是伪终端(如通过SSH登录的终端)。控制终端是会话与用户交互的接口,用户通过控制终端输入命令和接收输出。
- 控制进程:建立与控制终端连接的会话首进程被称为控制进程。控制进程是会话的第一个进程,通常是一个登录Shell。控制进程负责初始化会话,并管理会话中的其他进程。
- 前台进程组 :每个会话中有一个前台进程组。前台进程组是当前与控制终端直接交互的进程组。前台进程组可以接收来自控制终端的输入,并将输出发送到控制终端。例如,当用户在终端中按下
Ctrl+C
时,信号会被发送到前台进程组中的所有进程。 - 后台进程组:会话中可以有多个后台进程组。后台进程组不会直接与控制终端交互,它们在后台运行。后台进程组可以通过其他方式(如文件、网络)与用户或其他进程通信,但不会直接接收控制终端的输入。
(二)进程组、作业、会话关系
- 会话:是最高层次的组织单位,包含一个或多个进程组。
- 进程组:是会话中的一个子集,包含一个或多个相关进程。
- 作业:是用户提交的任务集合,通常对应一个进程组。作业可以是前台作业或后台作业。
- 作业 是从用户的角度定义的,它关注的是用户提交的任务。一个作业可以包含一个或多个进程组。例如,一个复杂的作业可能涉及多个步骤,每个步骤由一个进程组完成。
- 当作业中的某个进程创建子进程时,子进程默认会继承父进程的进程组ID。因此,子进程最初属于父进程所在的进程组。然子进程属于父进程所在的进程组,但它并不自动成为原作业的一部分。作业的范围通常由Shell管理,而Shell并不自动将子进程纳入原作业的管理范围。
为什么子进程不属于原作业
- 作业的管理范围:作业是由Shell管理的,Shell通常将作业定义为用户直接提交的任务及其直接相关的进程。如果作业中的某个进程创建了子进程,这些子进程可能超出了Shell对作业的管理范围。
- 资源管理的独立性:子进程可能需要独立于原作业进行资源管理和信号处理。将子进程排除在原作业之外,可以避免作业结束时对子进程产生不必要的影响。
- 作业结束:当作业运行结束时,Shell会将自己提到前台,以便用户可以继续输入新的命令。
- 子进程的处理:如果原前台作业中的某个子进程仍然存在且未终止,它将自动变为后台进程组。这是因为作业的结束并不一定意味着所有相关进程的结束,而Shell需要重新接管前台以便用户可以继续输入新的命令。

这些进程组的控制终端相同,它们同属于一个会话,当用户在控制终端输入特殊的控制键(如Ctrl+C产生SIGINT,Ctrl+\产生SIGQUIT,Ctrl+Z产生SIGTSTP),内核就会发送相应的信号给前台进程组中的所有进程。

[1] 787011
其中[1]是作业的编号,如果同时运行多个作业可以用这个编号进行区分,787011是该作业中某个进程的id(一个作业可以由多个进程组成)。
(三)任务管理相关命令
(1)jobs:查看当前会话有哪些作业

(2)fg + 作业号:可以将某个作业提至前台运行,如果该作业正在后台运行则直接提至前台运行,如果该作业处于停止状态,则给进程组的每个进程发SIGCONT信号使它继续运行并提至前台。

将一个前台进程放到后台运行可以使用Ctrl+Z,但使用Ctrl+Z后该进程就会处于停止状态
(3)bg + 作业号:可以让某个停止的作业在后台继续运行(Running),本质就是给该作业的进程组的每个进程发SIGCONT信号。

二、守护进程
(一)守护进程定义
- 守护进程是一种在后台运行的特殊进程,它独立于用户终端,并且通常在系统启动时启动,在系统关闭时才终止。守护进程的主要功能是周期性地执行某些任务,或者等待并处理某些事件,而不需要用户直接干预。
(二)守护进程特点
- 独立于终端:守护进程没有控制终端,它不会与任何用户终端关联,因此不会因用户注销而终止。
- 后台运行:守护进程在后台运行,不会占用前台的终端资源,也不会干扰用户的正常操作。
- 周期性或事件驱动:守护进程通常会周期性地执行某些任务(如定时备份、日志清理等),或者等待某些事件的发生(如网络请求、文件系统变化等)。
- 系统服务:守护进程通常是系统服务的一部分,为系统或其他应用程序提供支持,例如网络服务、定时任务调度等。
(三)守护进程作用
提供系统服务:许多系统服务都是以守护进程的形式运行,例如:
- 网络服务 :如 Web 服务器(
httpd
)、FTP 服务器(ftpd
)、DNS 服务器(named
)等。 - 定时任务调度 :如作业规划进程(
crond
),用于执行定时任务。 - 系统日志服务 :如日志守护进程(
syslogd
),负责收集和记录系统日志。
系统管理:守护进程可以执行一些系统管理任务,例如:
- 磁盘空间监控:定期检查磁盘空间,防止磁盘满导致系统故障。
- 系统备份:定期备份重要数据,确保数据安全。
用户服务:守护进程也可以为用户提供服务,例如:
- 邮件服务 :如邮件传输代理(
sendmail
),负责邮件的发送和接收。 - 打印服务 :如打印守护进程(
lpd
),管理打印任务。
(四)查看守护进程
我们可以用**ps axj
**命令查看系统中的进程:
- 参数a表示不仅列出当前用户的进程,也列出所有其他用户的进程。
- 参数x表示不仅列出有控制终端的进程,也列出所有无控制终端的进程。
- 参数j表示列出与作业控制相关的信息。
凡是TPGID一栏写着-1的都是没有控制终端的进程,也就是守护进程:

(1)内核线程
定义:内核线程是运行在内核空间的线程,完全由内核创建和管理,没有用户空间代码,因此不会出现在用户空间的进程列表中。
特点:
- 名字通常以
k
开头,例如kworker
、kswapd
、ksoftirqd
等。- 在
ps
或top
命令的COMMAND
列中,内核线程的名字会被方括号[]
括起来,以区分普通进程。- 内核线程没有用户空间的上下文,因此没有程序文件名和命令行参数。
(2)常见的守护进程
守护进程是运行在后台的特殊进程,独立于控制终端,通常以
d
结尾的名字来标识。以下是文章中提到的几个常见守护进程及其功能:
udevd
:
- 功能 :负责维护
/dev
目录下的设备文件。- 作用 :当系统检测到硬件设备的插入或移除时,
udevd
会动态创建或删除对应的设备文件,确保用户和程序能够正确访问硬件设备。
acpid
:
- 功能:负责电源管理。
- 作用:监听电源事件(如电池电量低、AC适配器插拔等),并根据配置执行相应的操作(如关机、休眠等)。
syslogd
:
- 功能 :负责维护
/var/log
下的日志文件。- 作用:收集系统日志信息,将其记录到指定的日志文件中,便于系统管理员进行故障排查和系统监控。
(3)守护进程的命名:守护进程通常以
d
结尾,例如httpd
(Web服务器)、sshd
(SSH服务器)、crond
(定时任务守护进程)等。这种命名方式是为了方便区分守护进程和其他普通进程。
(五)守护进程创建
(1)设置文件掩码为0:
umask(0);
目的:将文件掩码设置为0,确保后续守护进程创建文件具有预期的权限
(2)第一次fork后终止父进程,子进程创建新会话
if (fork() > 0) {
exit(0); // 父进程退出
}
setsid(); // 子进程创建新会话,脱离终端
目的:调用setsid()创建新会话;使得进程自成会话,与当前bash脱离关系。要求调用进程不是进程
组组长,因此先fork创建子进程,由子进程调用setsid。
为什么要求调用进程不是进程组组长?
根据 POSIX 标准,
setsid()
的行为有以下限制:
- 如果调用
setsid()
的进程已经是某个进程组的组长,则setsid()
会失败,并返回错误码EPERM
。- 这是因为如果允许进程组组长调用
setsid()
,可能会导致会话和进程组的管理混乱,尤其是在终端管理方面。具体原因:
避免会话管理混乱:
- 如果允许进程组组长调用
setsid()
,可能会导致该进程既属于旧的进程组,又成为新的会话首进程,这会使得会话和进程组的关系变得复杂,难以管理。- 通过要求调用进程不是进程组组长,可以确保会话的创建过程是清晰和一致的。
避免终端管理问题:
- 当一个进程调用
setsid()
时,它会断开与控制终端的关联。如果该进程是进程组组长,那么它可能正在管理其他进程对终端的访问。- 通过要求调用进程不是进程组组长,可以避免因终端管理权限问题导致的混乱。
(3)忽略SIGCHLD信号
signal(SIGCHLD, SIG_IGN);
目的:忽略SIGCHLD信号,防止父进程退出时触发信号,避免僵尸进程。
(4)第二次fork,终止父进程,保持子进程不是会话首进程
if (fork() > 0) {
exit(0); // 父进程退出
}
目的:在 Linux 系统中,只有会话首进程(Session Leader)才有能力打开终端设备。会话首进程是通过 setsid()
创建的,它成为新会话的领导者。如果一个进程不是会话首进程,它将无法打开终端设备。通过第二次 fork()
,孙子进程不再是会话首进程,因此它无法打开终端。这确保了守护进程不会意外地重新连接到某个终端,从而避免了终端资源的占用。
防御性编程:
虽然在大多数情况下,守护进程不会主动尝试打开终端,但进行第二次
fork()
是一种防御性编程的手段。它确保即使守护进程的代码中存在某些意外情况(例如错误地尝试打开终端),守护进程也不会成功打开终端,从而避免潜在的问题。
(5)更改工作目录为根目录:
chdir("/");
守护进程将工作目录设置为根目录后:
- 绝对路径 :仍然从根目录开始解析,与工作目录无关。例如,
/home/user/file.txt
会从根目录开始解析,不受当前工作目录的影响。 - 相对路径 :从根目录开始解析。例如,
./file.txt
实际上指的是/file.txt
,而不是其他目录下的file.txt
。
将守护进程的工作目录设置为根目录主要是为了避免守护进程占用特定的文件系统资源。如果守护进程的工作目录位于某个可卸载的文件系统中(例如,网络挂载目录或可移动存储设备),当该文件系统被卸载时,守护进程可能会受到影响。通过将工作目录设置为根目录,可以避免这种情况。此外,这也减少了对特定目录的依赖,确保守护进程能够独立运行
(6)将标准输入、标准输出、标准错误重定向到/dev/null中
close(0);
int fd = open("/dev/null", O_RDWR);
dup2(fd, 1);
dup2(fd, 2);
目的:将标准输入、输出、错误重定向到/dev/null
,避免守护进程输出信息到终端。
守护进程不能直接和用户交互,也就是说守护进程已经与终端去关联了,因此一般我们会将守护进程的标准输入、标准输出以及标准错误都重定向到/dev/null
,/dev/null
是一个字符文件(设备),通常用于屏蔽/丢弃输入输出信息。(该操作不是必须的)
代码如下:
#include<sys/stat.h>
#include<unistd.h>
#include<stdlib.h>
#include<signal.h>
#include<fcntl.h>
int main()
{
//1.设置权限掩码
umask(0);
//2.创建子进程创建新会话
if(fork()>0) exit(0);
setsid();
//3.忽略SIGCHLD信号
signal(SIGCHLD,SIG_IGN);
//4.防御性编程
if(fork()>0) exit(0);
//5.更改工作目录为根目录
chdir("/");
//6.将标准输入、标准输出、标注你错误重定向到/dev/null中
close(0);
int fd=open("/dev/null",O_RDWR);
dup2(fd,1);
dup2(fd,2);
//代替服务器
while(1);
return 0;
}

运行代码,用ps命令查看该进程,发现该进程TPGID为-1,TTY显示为?,说明该进程已经与终端去关联了。
其次就是我们还可以看见该进程的PID与其PGID和SID不同,也就是说该进程既不是组长进程也不是会话首进程。

此外,我们还可以看到该进程的SID与bash进程的SID是不同的,即它们不属于同一个会话。

通过ls /proc/进程pid -al命令,可以看到该进程的工作目录设置为根目录

通过ls /proc/进程pid/fd -al命令,可以看见该进程的标准输入、标准输出以及标准错误成功重定向到/dev/null上。
(六)调用daemon函数创建守护进程
|---------------------------------------|------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---|
| 函数 | 返回值 | 参数 | |
| int daemon(int nochdir, int noclose); | 如果成功,返回 0
。 如果失败,返回 -1
,并设置 errno
以指示错误原因。 | nochdir
: * 如果设置为 0
,守护进程的工作目录会被更改为根目录(/
)。 * 如果设置为非 0
,守护进程的工作目录不会更改。 noclose
: * 如果设置为 0
,守护进程的标准输入、标准输出和标准错误会被重定向到 /dev/null
。 * 如果设置为非 0
,守护进程的标准输入、标准输出和标准错误不会被重定向。 | |

调用daemon函数创建的守护进程与我们原生创建的守护进程差距不大,唯一区别就是daemon函数创建出来的守护进程,既是组长进程也是会话首进程。也就是说系统实现的daemon并没有防止守护进程打开终端,系统并没有进行防御性编程
(七)模拟实现daemon函数
#include <sys/stat.h>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>
#include <fcntl.h>
void my_daemon(int nochdir, int noclose)
{
// 1.设置权限掩码
umask(0);
// 2.创建子进程创建新会话
if (fork() > 0)
exit(0);
setsid();
// 3.忽略SIGCHLD信号
signal(SIGCHLD, SIG_IGN);
// 4.防御性编程
if (fork() > 0)
exit(0);
// 5.更改工作目录为根目录
if (nochdir == 0)
chdir("/");
// 6.将标准输入、标准输出、标注你错误重定向到/dev/null中
if (noclose == 0)
{
close(0);
int fd = open("/dev/null", O_RDWR);
dup2(fd, 1);
dup2(fd, 2);
}
}