目录
常见进程类别
守护进程&精灵进程
任务管理
进程组
作业
作业 | 进程组
会话
w命令
守护进程
守护进程的创建
setsid()函数
daemon()函数
模拟实现daemon函数
前台进程 | 后台进程
僵尸进程 | 孤儿进程
僵尸进程的一些细节
守护进程 | 后台进程
守护进程 | 僵尸进程 | 孤儿进程
#:前台进程和后台进程在Linux系统中有着不同的意义和作用。
- ctr+z 将当前运行的程序放入后台挂起。
- jobs 命令,显示后台被挂起的所有进程。
- bg N 使第N个序号的任务在后台(background)运行。
- fg N 使第N个序号的任务在前台(foreground)运行。
精灵进程&守护进程是一样的,不同的翻译叫法而已。
在Linux系统中,每个进程都属于一个进程组。进程组是一组相关联的进程的集合,这些进程通常是由同一个父进程创建的,并且它们之间可以相互协作。每个进程组都有一个唯一的进程组ID(PGID),而每个进程也有一个唯一的进程ID(PID)。在同一个进程组中,其中一个进程会被指定为该组的领头进程(也称为“组长”),其PID等于PGID。只要在某个进程组中有一个进程存在,则该进程组就存在,这与其组长进程是否终止无关。
#:进程必定属于一个进程组,也只能属于一个进程组。
在Unix/Linux系统中,可以使用setpgid()系统调用将一个子进程添加到不同的进程组中。使用getpgrp()系统调用来获取当前进程所在进程组号。
#include
int setpgid(pid_t pid, pid_t pgid);
#include
#include
#include
int main()
{
pid_t id = fork();
if (id == 0) {
// 子进程
printf("Child process: PID=%d, PGID=%d\n", getpid(), getpgrp());
// 将子进程添加到新的进程组中
setpgid(0, 0);
printf("Child process after setpgid(): PID=%d, PGID=%d\n", getpid(), getpgrp());
// 子进程执行一些任务
sleep(10);
exit(EXIT_SUCCESS);
}
else if(id > 0)
{
// 父进程
printf("Parent process: PID=%d, PGID=%d\n", getpid(), getpgrp());
// 父进程执行一些任务
sleep(5);
exit(EXIT_SUCCESS);
}
else
{
perror("fork");
exit(EXIT_FAILURE);
}
}
子进程首先输出自己的PID和PGID,然后使用setpgid()函数将自己添加到一个新的进程组中,并再次输出自己的PID和PGID。父进程也输出自己的PID和PGID,并在子进程执行任务期间休眠5秒钟。运行该程序后,就可以看到子进程的PGID与父进程不同,即子进程已经成为一个新的进程组的组长。
[qcr@VM-16-6-centos test_2023_9_9]$ ./test
Parent process: PID=10606, PGID=10606
Child process: PID=10607, PGID=10606
Child process after setpgid(): PID=10607, PGID=10607
作业指的是在终端或者控制台上运行的进程。当你在命令行输入一个命令并按下回车键后,这个命令就会以进程的方式在后台运行,并且会被分配一个唯一的作业号(job ID)。你可以使用job ID来管理和控制这个进程。在Linux中,有两种类型的作业:前台作业和后台作业。前台作业是指当前正在终端或者控制台上运行的进程,而后台作业则是指在后台运行的进程。
作业控制控制前后台进程的步骤:
1、启动一个进程并将其放到后台运行
在命令行终端上启动一个命令时,在该命令末尾加上 '&' 符号即可将其放到后台运行。例如:$ long_running_command &
2、查看当前正在运行的作业
可以使用 'jobs' 命令查看当前正在运行的作业及其状态。例如:$ jobs [1]+ Running long_running_command &
3、将后台进程调回前台
可以使用 'fg' 命令将一个后台进程调回前台。例如:$ fg n
4、将前台进程放到后台
可以使用 'Ctrl+Z' 键将当前前台进程暂停,并将其放到后台运行,但使用Ctrl+Z后该进程就会处于停止状态(Stopped)。例如:^Z [1]+ Stopped long_running_command
5、恢复被暂停的后台进程
可以使用 'bg' 命令将被暂停的后台进程恢复运行。例如:$ bg n [1]+ long_running_command &
6、终止进程
可以使用 'Ctrl+C' 键向前台进程发送 'SIGINT' 信号,终止该进程的运行。如果想要强制终止一个进程,可以使用 'kill' 命令。例如:$ kill -9
#:融会贯通的理解
假设要完成一个任务,需要同时并发100个进程。当用户处于某种原因要终止这个任务时,要是没有进程组,就需要手动的一个个去杀死这100个进程,并且必须要严格按照进程间父子兄弟关系顺序,否则会扰乱进程树。有了进程组,就可以将这100个进程设置为一个进程组,它们共有1个组号(pgrp),并且有选取一个进程作为组长(通常是“辈分”最高的那个,通常该进程的ID也就作为进程组的ID)。现在就可以通过杀死整个进程组,来关闭这100个进程,并且是严格有序的。组长进程可以创建一个进程组,创建该组中的进程,然后终止。(来源:Linux-进程、进程组、作业、会话、控制终端详解 - John_ABC)
#:融会贯通的理解
在Linux中,一个进程可以通过管道(pipe)、作业控制(job control)、信号(signal)等方式与其他进程进行直接关联。
因此,尽管作业和进程组都是由多个程序或任务组成的集合,但它们之间有所不同。作业更加高级,包含更多元素(如文件、输入输出等),而进程组则更注重管理和控制多个相关联的进程。
#:都是操作系统中的概念,用于方便对一组相关联的进程进行管理和控制。
作业与进程组的区别:如果作业中的某个进程又创建了子进程,则子进程不属于作业。一旦作业运行结束,Shell就把自己提到前台,如果原来的前台进程还存在(如果这个子进程还没终止),它自动变为后台进程组。父进程创建的子进程默认情况下属于同一进程组。在Unix/Linux系统中,每个进程都有一个唯一的进程ID(PID)和一个进程组ID(PGID)。当父进程创建子进程时,子进程会继承父进程的PGID,因此它们属于同一进程组。
由于Linux是多用户多任务的分时系统,所以必须要支持多个用户同时使用一个操作系统。当一个用户登录一次系统就形成一次会话 。一个会话可包含多个进程组,但只能有一个前台进程组。每个会话都有一个会话首领(leader),即创建会话的进程。
一个用户可以在同一时间拥有多个会话。在Linux系统中,每个用户都可以通过终端、SSH等方式登录到系统中,并启动一个新的会话。这些会话可以同时运行不同的进程和任务,且互相独立,互不干扰。每个会话都有一个唯一的ID号,称为Session ID(SID),用于区分不同的会话。而每个进程也有一个唯一的Process ID(PID),用于区分不同的进程。通过命令 "who" 或者 "w" 可以查看当前登录系统的用户和他们所拥有的会话。
w命令实际上用于显示当前终端的前台进程,而一个会话只能有一个前台进程。因此,通过观察w命令的输出,所以可以间接推断出当前系统中有多少个会话。
(此处有一个会话执行w命令,所以w对应的进程变为了前台进程,bash变为了后台进程)
w命令的属性字段:
USER : 登录用户的用户名。
TTY : 登录用户所使用的终端设备。
FROM : 登录用户的IP地址或远程主机名。
LOGIN@ : 登录时间,格式为月日时分。
IDLE : 用户空闲时间,即从上次输入开始到现在的时间。
JCPU : 所有进程占用CPU时间的总和,单位为分钟。
PCPU : 当前进程占用CPU时间百分比。
WHAT : 当前登录用户所执行的命令或进程。
参数说明:
-f 开启或关闭显示用户从何处登入系统。
-h 不显示各栏位的标题信息列。
-l 使用详细格式列表,此为预设值。
-s 使用简洁格式列表,不显示用户登入时间,终端机阶段作业和程序所耗费的CPU时间。
-u 忽略执行程序的名称,以及该程序耗费CPU时间的信息。
-V 显示版本信息。
#:融会贯通的理解
一个会话可以有一个控制终端。这通常是登陆到其上的终端设备(在终端登陆情况下)或伪终端设备(在网络登陆情况下)。建立与控制终端连接的会话首进程被称为控制进程。一个会话中的几个进程组可被分为一个前台进程组以及一个或多个后台进程组。所以一个会话中,应该包括控制进程(会话首进程),一个前台进程组和任意后台进程组。
在Linux/Unix系统中,当我们启动一个新的shell时,通常情况下会先由init进程启动一个getty或login进程,当我们在终端输入用户名和密码后,getty或login进程会验证用户身份,并为该用户创建一个新的会话(session)和控制终端(controlling terminal)。然后,该进程会使用exec函数族调用启动一个新的shell程序(如bash),并将其加入到该会话的前台进程组中。此时,init进程成为了该会话的控制进程,而bash进程则成为了该会话的前台进程。#:前台进程只能有一个,当一个进程变成前台进程后,bash会自动变为后台进程,此时bash就无法进行命令行解释了。
setsid()调用能创建一个会话。必须注意的是,只有当前进程不是进程组的组长时,才能创建一个新的会话。调用setsid 之后,该进程成为新会话的leader。
#include
pid_t setsid(void);
使用 'setsid' 函数可以将一个进程从其父进程所在的终端分离出来,使其成为一个独立的后台进程,并且不再受到终端关闭等事件的影响。
#include
#include
#include
#include
#include
#include
int main()
{
pid_t pid;
// 创建子进程
pid = fork();
if(pid == -1)
{
perror("fork");
exit(EXIT_FAILURE);
}
else if(pid > 0)
{
// 父进程退出
exit(EXIT_SUCCESS);
}
// 在子进程中创建新会话
if(setsid() == -1)
{
perror("setsid");
exit(EXIT_FAILURE);
}
// 关闭不需要的文件描述符
close(STDIN_FILENO);
close(STDOUT_FILENO);
close(STDERR_FILENO);
// 更改当前工作目录
chdir("/");
// 重定向标准输入、输出、错误输出到/dev/null
int fd = open("/dev/null", O_RDWR, 0);
dup2(fd, STDIN_FILENO);
dup2(fd, STDOUT_FILENO);
dup2(fd, STDERR_FILENO);
while(1)
{
// 后台进程逻辑代码
sleep(1);
}
return 0;
}
该程序在执行时,会创建一个后台进程,并将其从终端分离出来。后台进程的逻辑代码可以在while循环中实现,此处只是简单地使用sleep函数模拟了一下。
#:守护进程也称精灵进程(Daemon),是运行在后台的一种特殊进程(一种长期运行的后台进程),它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件,如监控硬件设备、网络服务等。
特点:
凡是TPGID一栏写着-1的都是没有控制终端的进程,也就是守护进程。
在COMMAND一列用 [ ] 括起来的名字表示内核线程,这些线程在内核里创建,没有用户空间代码,因此没有程序文件名和命令行,通常采用以k开头的名字,表示Kernel
setsid()调用能创建一个会话。必须注意的是,只有当前进程不是进程组的组长时,才能创建一个新的会话。调用setsid 之后,该进程成为新会话的leader。
#include
pid_t setsid(void);
- 返回值
- 如果调用成功,返回新的会话ID。
- 如果调用失败,返回 -1 并设置errno变量。
/dev/null
,/dev/null
是一个字符文件(设备),通常用于屏蔽/丢弃输入输出信息。#include
#include
#include
#include
#include
#include
int main()
{
// 创建子进程
pid_t pid = fork();
// 如果创建子进程失败,则直接退出
if(pid == -1)
{
perror("fork");
exit(1);
}
// 如果是父进程,则直接退出
if(pid > 0)
{
exit(0);
}
// 在子进程中创建新会话,并成为领头进程
if(setsid() == -1)
{
perror("setsid");
exit(1);
}
// 第二次fork,创建孙子进程
pid = fork();
// 如果创建孙子进程失败,则直接退出
if(pid == -1)
{
perror("fork");
exit(1);
}
// 如果是父进程,则直接退出
if(pid > 0)
{
exit(0);
}
// 将标准输入、标准输出、标准错误重定向到/dev/null
close(0);
int fd = open("/dev/null", O_RDWR);
dup2(fd, 1);
dup2(fd, 2);
// 修改工作目录和文件屏蔽字
chdir("/");
umask(0);
// 正式执行守护进程任务代码
while(1)
{
// do something...
sleep(10);
}
return 0;
}
第二次fork()主要是为了避免守护进程在未来可能会因为某些原因重新获得控制终端而导致进程退出的问题。
#:融汇贯通的理解
具体来说,会有个SIGHUP信号,其是一种由操作系统发送给进程的信号,通常表示 “终端挂起” 。在Linux操作系统中,当一个进程打开了一个终端设备(例如控制台窗口),并且该终端设备被关闭或者与该进程失去联系时,操作系统会向该进程发送SIGHUP信号。
在守护进程中,如果只进行一次fork()操作,则该子进程仍然会与原来的控制终端相关联。如果此时用户关闭了控制终端,则操作系统会向该子进程发送SIGHUP信号。如果程序没有处理这个信号,则可能会导致程序异常退出。
因此,需要进行第二次fork()操作。这样,在第二次fork()之后,会产生一个孙子进程,并使孙子进程成为真正的守护进程。而孙子进程与其父进程和原来的控制终端、信号等都已经完全脱离关系。
[qcr@VM-16-6-centos test_2023_9_10]$ ps axj | head -1 && ps axj | grep test
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
1 4382 4381 4381 ? -1 S 1001 0:00 ./test
9217 4483 4482 9160 pts/0 4482 S+ 1001 0:00 grep --color=auto test
运行代码,用ps命令查看该进程,会发现该进程的TPGID为-1,TTY显示的是 ?,也就意味着该进程已经与终端去关联了。
进程的SID与bash进程的SID是不同的,即它们不属于同一个会话。
ls /proc/进程id -al
命令,可以看到该进程的工作目录已经成功改为了根目录。ls /proc/进程id/fd -al
命令,可以看到该进程的标准输入、标准输出以及标准错误也成功重定向到了/dev/null
。
/proc
在Linux系统中,/proc是一个虚拟文件系统,它提供了对系统内核和进程的信息的访问。/proc目录下包含了很多文件和子目录,每个文件或子目录都代表着一个进程或系统内核的一些信息。
/proc/cpuinfo : 包含了CPU的信息。 /proc/meminfo : 包含了内存的信息。 /proc/net : 包含了网络相关的信息。 /proc/sys : 包含了一些内核参数和系统配置信息。 /proc/
: 代表着一个进程,其中pid是进程的ID号。该目录下包含了该进程的很多信息,如进程状态、打开的文件、内存映射、线程等。
在Unix/Linux系统中,我们可以使用daemon()函数来创建Daemon进程。
#include
int daemon(int nochdir, int noclose);
- 参数
- nochdir:如果该参数为0,则在调用daemon()函数后,进程的当前工作目录会被设置为根目录 "/" ;如果该参数不为0,则进程的当前工作目录不会改变。
- noclose:如果该参数为0,则在调用daemon()函数后,进标准输入、标准输出以及标准错误重定向到
/dev/null
;如果该参数不为0,则进程不会关闭任何文件描述符。- 返回值
- 0表示成功。
- 返回-1表示失败,并设置errno变量。
#include
int main()
{
daemon(0, 0);
while (1);
return 0;
}
调用daemon函数创建的守护进程与setsit函数创建的守护进程差距不大,唯一区别就是daemon函数创建出来的守护进程,既是组长进程也是会话首进程。也就是说系统实现的daemon函数没有防止守护进程打开终端,因此我们实现的反而比系统更加完善。
#include
#include
#include
#include
#include
int my_daemon(int nochdir, int noclose)
{
pid_t pid;
// 创建子进程
pid = fork();
if (pid == -1) {
perror("fork error");
exit(EXIT_FAILURE);
} else if (pid > 0) {
// 父进程退出
exit(EXIT_SUCCESS);
}
// 子进程调用setsid()函数创建新会话,并成为会话组长和进程组长
if (setsid() == -1) {
perror("setsid error");
exit(EXIT_FAILURE);
}
// 忽略SIGHUP信号
signal(SIGHUP, SIG_IGN);
// 再次创建子进程,退出父进程,保证守护进程不是会话组长和进程组长
pid = fork();
if (pid == -1) {
perror("fork error");
exit(EXIT_FAILURE);
} else if (pid > 0) {
// 父进程退出
exit(EXIT_SUCCESS);
}
// 如果nochdir参数为0,则将当前工作目录更改为根目录
if (nochdir == 0) {
chdir("/");
}
// 将标准输入、标准输出、标准错误重定向到/dev/null
if (noclose == 0){
close(0);
int fd = open("/dev/null", O_RDWR);
dup2(fd, 1);
dup2(fd, 2);
}
// 设置umask为0,以便守护进程可以创建任意权限的文件
umask(0);
return 0;
}
my_daemon()函数首先通过调用 'fork()' 函数创建了一个子进程。然后,在父进程中直接退出,而在子进程中调用 'setsid()' 函数创建新会话,并成为会话组长和进程组长。接着,忽略SIGHUP信号,并再次创建子进程,退出父进程,保证守护进程不是会话组长和进程组长。最后,根据传入的参数设置工作目录、关闭标准输入输出和标准错误输出、以及设置umask。
#:前台进程和后台进程是两个不同的概念!
#:孤儿进程和僵尸进程是两个不同的概念!
僵尸进程已经死亡,因此无法使用kill命令杀死它们。实际上,僵尸进程的退出状态信息已经被内核保存了下来,但是其父进程没有及时处理这些信息,导致僵尸进程一直处于“僵死”状态。要彻底清除僵尸进程,需要通过其父进程将其退出状态信息处理完毕,并调用wait或waitpid函数来回收其资源。
#:僵尸进程父进程又没有回收?
父进程正执行:
如果僵尸进程的父进程一直没有回收它,那么这个僵尸进程所占用的资源(如内存、文件描述符等)将一直被占用,直到系统重启或者其他操作将其释放。长时间存在大量僵尸进程会浪费系统资源,降低系统性能。
解决方案:
可以通过手动杀死父进程来强制回收所有僵尸进程的资源(正是下一种方法)。但是这种操作需要谨慎处理,因为杀死父进程可能会影响其他正在运行的进程。更好的方法是通过修改程序代码,在父进程中添加对子进程退出状态信息的处理代码,及时回收子进程的资源。
父进程已死亡:
#:并不是网上有些说的会变为孤儿进程。
此时,子进程的父进程ID会被设置为1,也就是init进程的ID。然后init进程会定期轮询检查是否有僵尸进程需要处理,并回收它们的资源。
因此,终止父进程后僵尸进程并不会转变为孤儿进程交给init处理,而是由init负责回收其资源。
#:后台进程和守护进程是两个不同的概念!可以看作守护进程是特殊的后台进程。
后台进程是指运行在后台的进程,它们不会占用当前终端窗口并且不需要用户输入,而是在后台默默地执行。用户可以使用特定的命令将一个前台进程转化为后台进程,或者直接启动一个后台进程。而守护进程则是一种特殊的后台进程,它通常是由系统启动时自动启动的,并且会一直运行,直到系统关闭。它们通常不受用户交互影响,并且会在后台执行某些系统任务或服务。例如,网络服务(如Web服务器、FTP服务器等)就是通过守护进程来实现的。
后台进程相比守护进程特点:
总之,后台进程和守护进程都是Linux系统中常见的进程类型,但守护进程通常会比普通的后台进程更加复杂和功能强大。
精灵进程&守护进程是一样的,不同的翻译叫法而已,它的父进程是1号进程,退出后不会成为僵尸进程、孤儿进程。
#:init进程
init进程是Linux系统中的第一个进程,其进程号始终为1。在Linux系统启动时,内核会首先启动init进程,并由它来启动其他所有进程。init进程主要负责初始化系统环境、启动各种服务和守护进程,并监控这些进程的运行状态。如果某个进程异常退出或崩溃,init进程会尝试重新启动该进程,以确保系统稳定运行。