一、进程组
Linux把进程分属一些组,用进程的组标识符赖知道进程所属的组。进程最初是通过fork()
和exec
调用来继承其进程组标识符。
进程组是一个或者多个进程的集合, 一个进程组可以包含多个进程。每一个进程组也有一个唯一的进程组 ID(PGID), 并且这个 PGID 类似于进程ID,同样是一个正整数, 可以存放在 pid_t
数据类型中。PGID:process group id
- 所有
sleep
命令的进程都拥有相同的PGID(110118),表明它们属于同一个进程组。 - 这些进程的SID(会话ID)也是相同的(110087),表明它们属于同一个会话。
PPID
列显示这些进程都是由同一个父进程(PID 110087)创建的,这通常是创建该进程组的bash进程。
在Linux操作系统中,当命令行中使用管道(|
)连接多个命令时,第一个命令(在管道之前的部分)将成为一个进程组的组长,同时也会创建一个新的进程组。这是因为管道操作符创建了一个管道,并且第一个命令作为管道的写入端,而后续的命令作为读取端。
在我的命令中:
zyb@myserver:~$ sleep 10000 | sleep 20000 | sleep 30000
这里发生了两次管道操作,因此会创建三个进程:
- 第一个
sleep 10000
命令会创建一个进程。 - 第二个
sleep 20000
命令会创建一个进程。 - 第三个
sleep 30000
命令会创建一个进程。
每个 sleep
命令都在其自己的进程中运行,并且通过管道连接。第一个 sleep
命令的输出(尽管实际上 sleep
命令不产生输出)会传递给第二个 sleep
命令,第二个 sleep
命令的输出(同样,实际上没有输出)会传递给第三个 sleep
命令。这里的第一个命令 sleep 10000
将成为新创建的进程组的组长。该进程组的进程组ID(PGID)将与第一个 sleep
命令的进程ID(PID)相同。后续的命令 sleep 20000
和 sleep 30000
将是这个进程组的成员,并且它们的PGID将与第一个命令的PID相同。
但是,进程可以使用系统调用setpgrp()
,自己形成一个新的组。
setpgrp()
系统调用用于将一个进程设置为新的进程组的组长,从而创建一个新的进程组。该系统调用的返回值是调用进程的进程组标识符(PGID),该标识符与调用进程的进程标识符(PID)相同。调用进程成为该新进程组的组长进程(process group leader)。通常 setpgrp()
等价于 setpgid(0, 0)
。
一个进程可以用系统调用 getpgrp()
来获得其当前的进程组标识符。函数的返回值就是进程组的标识符。
每一个进程组都有一个组长进程,其 ID(PGID)与其进程 ID(PID)相同。
虽然组长进程有权创建新的进程组或将进程加入现有的组中,但它并不特别管理组内的其他进程。组长进程的主要角色是在系统中标识该进程组的一个唯一标识符。
进程组的生命周期
进程组的生命周期从创建时开始,到组内最后一个进程离开为止。在此期间,即使组长进程终止,只要组内还有其他进程存在,进程组依然继续存在。
-
组长进程终止的影响: 组长进程的终止并不会导致进程组的终止。系统将继续使用原组长进程的 PID 作为进程组 ID,直到组内最后一个进程终止。
-
进程组的终止: 当进程组内所有进程都终止或离开该进程组时,进程组也就自然解散。此时,系统不再保留该进程组的相关信息。
当某个用户退出系统时,则相应的shell进程所启动的全部进程都要被强行终止。系统是根据进程的组标识符来选定应该终止的进程的。
如果一个进程具有与其祖先shell进程相同的组标识符(PGID),那么在用户退出登录shell时,这个进程通常会接收到挂起信号(SIGHUP
),并且默认情况下会被终止。这是因为登录shell在退出时会发送SIGHUP
信号给其所属进程组中的所有进程。因此如果一个进程具有与其祖先shell进程不同的组标识符,那么它的生命期将可超出用户的注册期。这对于需要长时间运行的后台任务是十分有用的。
- 当用户登录系统时,登录shell会成为会话领头进程,并创建一个新的进程组,其PGID与shell的PID相同。
- 默认情况下,shell启动的所有进程都会继承shell的PGID。
- 当用户退出系统时,其登录会话结束。这通常是通过关闭终端会话或执行注销命令来触发的。shell会向其进程组内的所有进程发送
SIGHUP
(挂起信号),导致这些进程默认情况下会被终止。 SIGHUP
信号首先发送给进程组的组长进程,然后组长进程通常会将其传播给进程组内的所有成员进程。- 如果进程的 PGID 被改变,那么它不会因为用户退出 shell而直接收到
SIGHUP
信号。这是因为SIGHUP
信号通常会发送给整个进程组,而该进程已经不在那个进程组中了。 - 如果进程忽略了
SIGHUP
信号,那么即使它仍然在原来的进程组中,它也不会因为SIGHUP
信号而终止。
- 如果进程的 PGID 被改变,那么它不会因为用户退出 shell而直接收到
这就是为什么将后台任务放在一个新的进程组中(或者让它们忽略SIGHUP
信号)对于需要长时间运行的后台任务很有用的原因。这样做可以保证任务在用户注销后不会因为接收到SIGHUP
信号而被终止。
单进程程序是一个进程组吗?
#include <iostream>
#include <string>
#include <unistd.h>
int main()
{
while (true)
{
std::cout << "I am a process :" << getpid() << std::endl;
sleep(1);
}
return 0;
}
多进程程序内的进程属于一个进程组吗?
#include <iostream>
#include <string>
#include <unistd.h>
int main()
{
pid_t id = fork();
if (id == 0)
{
while (true)
{
std::cout << "I am a child process :" << getpid() << std::endl;
sleep(1);
}
}
sleep(3);
std::cout << "I am a parent process :" << getpid() << std::endl;
sleep(100);
return 0;
}
只要某个进程组中有一个进程存在, 则该进程组就存在, 这与其组长进程是否已经终止无关。验证代码:
#include <iostream>
#include <string>
#include <unistd.h>
int main()
{
pid_t id = fork();
if (id == 0)
{
while (true)
{
std::cout << "I am a child process :" << getpid() << std::endl;
sleep(1);
}
}
sleep(3);
std::cout << "I am a parent process :" << getpid() << std::endl;
exit(0);
return 0;
}
进程的标识符
每个进程都有一个实际用户标识符(Real User ID,RUID)和一个实际组标识符(Real Group ID,RGID)。
进程的有效用户标识符(Effective User ID,EUID) 和 有效组标识符(Effective Group ID,EGID) 也许更重要些,它们被用来确定一个用户能否访问某个确定的文件。在通常情况下,它们与实际用户标识符和实际组标识符是一致的。但是,一个进程或其祖先进程可以设置程序文件的置用户标识符权限或置组标识符权限。这样,当通过 exec
调用执行该程序时,其进程的有效用户标识符就取自该文件的文件主的有效用户标识符,而不是启动该进程的用户的有效用户标识符。
在Unix/Linux操作系统中,进程的身份验证和权限检查主要基于以下四个标识符:
- 实际用户标识符(Real User ID, RUID):标识启动进程的实际用户。
- 实际组标识符(Real Group ID, RGID):标识启动进程的实际用户所属的组。
- 有效用户标识符(Effective User ID, EUID):用于权限检查,确定用户对文件和其他资源的访问权限。
- 有效组标识符(Effective Group ID, EGID):与EUID类似,用于权限检查,确定用户所属组对文件和其他资源的访问权限。
以下是关于这些标识符的详细解释:
- 实际用户标识符和实际组标识符:当一个用户启动一个进程时,该进程的RUID和RGID被设置为用户的UID和GID。这些标识符不会在进程执行期间改变,它们标识了启动进程的用户。
- 有效用户标识符和有效组标识符:通常情况下,EUID和EGID与RUID和RGID相同。但是,在某些情况下,进程可能需要以不同的用户身份执行操作。这时,EUID和EGID可以被设置为不同于RUID和RGID的值。
设置用户标识符(Set-User-ID, SUID)和设置组标识符(Set-Group-ID, SGID)位是文件权限位,它们可以改变进程的EUID和EGID:
- 设置用户标识符(SUID):当一个可执行文件设置了SUID位时,任何用户执行这个程序时,进程的EUID会被设置为该文件所有者的UID,而不是执行者的UID。这样,进程就可以访问文件所有者有权限访问的资源。
- 设置组标识符(SGID):类似地,当一个可执行文件设置了SGID位时,执行这个程序时,进程的EGID会被设置为该文件所有组的GID,而不是执行者所属组的GID。
例如,考虑/bin/passwd
命令,它用于更改用户密码。为了允许普通用户更改自己的密码(这通常需要root权限,因为密码文件/etc/shadow
只有root可以写),/bin/passwd
被设置了SUID位。因此,当普通用户执行/bin/passwd
时,尽管进程的实际用户是普通用户,但有效用户是root,这使得进程可以修改/etc/shadow
文件。
总结来说,SUID和SGID位允许进程以不同于启动它的用户的权限运行,这在某些情况下非常有用,尤其是在需要特定权限才能执行操作的程序中。然而,它们也可能引入安全风险,因为如果设置了SUID或SGID位的程序存在漏洞,那么攻击者可能会利用这些权限执行任意代码。
下面几个系统调用可以用来得到进程的用户标识符和组标识符:
另外还有两个系统调用可以用来设置进程的有效用户标识符和有效组标识符:
普通用户进程的标识符设置
普通用户启动的进程只能将有效用户表示符(EUID)和有效组标识符(EGID)重新设置回其实际用户标识符(RUID)和实际组标识符(RGID)。这意味着,如果一个进程是由普通用户启动的,那么它不能通过setuid()
和setgid()
系统调用来提升其权限。以下是对这两个系统调用的简单说明:
setuid(uid_t uid)
:尝试将进程的EUID设置为参数uid
指定的值。如果进程由非root用户启动,则只能将EUID设置回RUID。setgid(uid_t gid)
:尝试将进程的EGID设置为参数gid
指定的值。如果进程由非root用户启动,则只能将EGID设置回RGID。
如果调用成功,这些函数返回0;如果调用失败,它们返回-1。
超级用户进程的标识符设置
root用户启动的进程可以自由地设置其EUID和EGID为任意值。这意味着root进程可以完全控制其权限,包括降低权限。
放弃和恢复root权限:一个由root启动的进程可以通过setuid()
和setgid()
放弃其root权限,但如果它后来需要恢复这些权限,它不能再次使用setuid()
和setgid()
来实现,因为一旦进程的EUID被设置为一个非root值,它就不能再将其EUID设置回root。
为了解决这个问题,Linux提供了以下系统调用:
seteuid(uid_t euid)
:设置进程的EUID。即使进程的EUID已经被设置为非root值,只要进程的RUID是root,它仍然可以使用seteuid()
将EUID重新设置回root。setegid(gid_t egid)
:设置进程的EGID。与seteuid()
类似,它允许进程根据需要改变其EGID。
这些调用允许root进程在必要时安全地降低和恢复权限。
使用
setuid()
和setgid()
时,必须非常小心,因为不正确的权限管理可能导致安全漏洞。通常,一个进程只有在绝对必要时才会放弃root权限,并且在执行不需要特权的操作后立即恢复它们。通过这种方式,可以提高系统的安全性,因为即使进程被攻破,攻击者也无法获得完整的root权限,只能获得进程当时所拥有的权限。
二、会话
在Linux操作系统中,会话(Session)和进程组(Process Group)密切相关。会话可以被看作是一个或多个进程组的集合。每个会话都有一个唯一的会话ID(SID)。会话可以由一个进程(通常是一个终端控制进程)创建,并且可以包含多个进程组。进程组是会话的一部分,每个进程组都有自己的进程组ID(PGID),并且所有属于同一会话的进程共享同一个SID。
会话的主要作用
会话的主要作用是管理和控制与终端相关的进程。例如,一个用户在终端上登录后,登录进程创建一个新的会话,并启动一个shell进程。这个shell进程可以启动其他进程,它们通常共享相同的会话ID。会话还用于处理终端信号,如挂断(SIGHUP)信号,当终端关闭时,会话内的所有进程都会收到这个信号。
因此,会话和进程组之间的关系可以总结如下:
- 会话可以包含一个或多个进程组。
- 每个进程组都有一个唯一的进程组ID(PGID)。
- 每个进程组都属于一个唯一的会话,会话有一个会话ID(SID)。
- 进程组的组长进程通常会创建一个新的进程组,并且成为该进程组的组长进程。
- 进程组的组长进程通常也是会话的组长进程。
会话的组长进程通常会创建一个新的会话。会话的组长进程通常也是会话中第一个进程组的组长进程。
总的来说,会话和进程组是Unix/Linux操作系统中用于管理和控制一组相关进程的概念。它们之间的关系允许系统以有效的方式组织和管理进程。
#include <iostream>
#include <string>
#include <unistd.h>
int main()
{
pid_t id = fork();
if (id == 0)
{
while (true)
{
std::cout << "I am a child process :" << getpid() << std::endl;
sleep(1);
}
}
sleep(3);
std::cout << "I am a parent process :" << getpid() << std::endl;
exit(0);
return 0;
}
当用户登录到Linux系统时,系统会在 /dev/pts/
目录下为该用户创建一个或多个伪终端(pseudo-terminal, pty)设备文件。这些设备文件是用于实现会话的逻辑终端。
伪终端是一种特殊的终端,它允许一个进程与另一个进程之间的通信,就像它们是通过一个真实的物理终端进行通信一样。当用户登录时,系统会为该用户创建一个会话,并在这个会话中创建一个伪终端设备文件。这个伪终端设备文件通常位于 /dev/pts/
目录下,其名称通常与用户的登录名有关。
例如,如果用户名为 user1
,系统可能会在 /dev/pts/
目录下创建一个名为 /dev/pts/1
的伪终端设备文件,用于 user1
的登录会话。这个伪终端设备文件通常与用户的登录终端(例如,终端窗口或终端仿真器)相关联,用于接收用户输入和显示输出。
每个用户登录时,系统都会创建一个伪终端设备文件,用于该用户的会话。这些伪终端设备文件是用于实现会话的逻辑终端,它们允许系统有效地管理多个用户的登录会话。
在一个会话中,可以同时存在多个进程组,但在任何时刻只能有一个前台进程组,而可以有多个后台进程组。
前台进程组: 前台进程组是当前与终端直接交互的进程组。前台进程组中的进程可以从终端获取输入(标准输入)并接收来自终端的信号(如中断信号
SIGINT
和终端挂断信号SIGHUP
)。后台进程组: 后台进程组是在不与终端直接交互的情况下运行的进程组。这些进程组不从终端接收输入,并且在尝试从终端读取时会收到
SIGTTIN
信号。这些信号的默认行为是将进程挂起(阻塞),直到它们被移动到前台。
也就是说:谁是前台进程,谁就可以从标准输入中获取数据。
我们可以通过setsid()
函数创建一个新的会话,并将调用进程设置为该会话的会话组长和进程组组长。调用该函数的进程将是新进程组和新会话中的唯一进程。该函数返回调用进程的新会话ID(也就是其进程ID),如果出现错误则返回-1。该函数常用于创建守护进程。
⚠️调用setsid()
创建新会话时,要求调用进程不能是进程组组长。
由于进程组的组长不能调用setsid()
来创建新会话,所以如果当前进程是进程组的组长(比如,它是通过fork()
创建的,且父进程已经退出),它必须首先创建一个新的子进程,然后退出,这样子进程就不再是进程组的组长,可以调用setsid()
成功。
三、控制终端
控制终端(Controlling Terminal)指的是一个进程(特别是前台进程组中的进程)所关联的终端设备。控制终端提供了用户与进程之间交互的接口,通常是文本界面或命令行界面,用于输入命令和接收输出。
主要功能:
- 用户交互: 控制终端允许用户输入命令和数据,并接收系统或程序的输出。通常情况下,终端程序(如
bash
或sh
)充当中介,接受用户的输入命令,执行相应的程序,并将输出返回给用户。- 当用户通过终端登录系统后,获得一个 shell进程,这个终端设备就成为 shell进程的控制终端。控制终端的信息保存在进程控制块(PCB)中,因此由 shell进程启动的所有子进程都会继承这个控制终端。
- 信号发送: 控制终端可以发送特定的信号给前台进程组中的所有进程。例如,用户可以使用
Ctrl+C
来发送SIGINT
信号以中断前台进程,或使用Ctrl+Z
发送SIGTSTP
信号以挂起前台进程。 - 输入/输出控制: 终端设备通常与标准输入(
stdin
)、标准输出(stdout
)和标准错误输出(stderr
)流相关联。前台进程可以从标准输入读取数据,并将输出写入标准输出和标准错误。
控制终端的工作原理:
-
进程关联: 当用户在终端上登录时,登录进程通常会创建一个新的会话和控制终端。随后启动的shell(如
bash
)会继承这个终端,并成为控制终端的前台进程组的成员。 -
会话和进程组: 控制终端与会话和进程组密切相关。每个会话可以有一个控制终端,而控制终端在任何时刻只能关联一个前台进程组。后台进程组不会与控制终端直接交互,因此无法从中获取输入或接收来自终端的信号。
-
失去控制终端: 进程可以通过调用
setsid()
来脱离当前控制终端并创建新的会话。这样,进程将不再与任何终端关联,从而不会受到终端信号的影响,也不会再从终端读取输入。这对于守护进程(daemon)尤其重要,因为它们需要在后台独立于用户终端运行。
一个典型的例子是用户在终端中运行命令:
- 用户登录到系统,启动一个shell(如
bash
)。 - 用户在shell中输入命令,shell将用户输入传递给相应的程序。
- 程序执行,输出结果通过标准输出返回到终端,用户可以看到结果。
- 用户可以使用终端快捷键发送信号,如
Ctrl+C
中断前台进程。
终端的角色: 终端在会话和进程组之间起着桥梁作用。当用户在终端上运行命令时,终端与前台进程组交互,允许这些进程从标准输入中读取数据和接收信号。在一个会话中,可以有多个进程组,但只有一个前台进程组与控制终端直接交互。前台进程组中的进程可以从控制终端获取输入和接收信号(如SIGINT
),而后台进程组不能。
四、作业
在 shell中,作业控制是通过进程组实现的。用户可以将一组进程作为一个整体进行暂停、恢复或终止操作。
作业(Job)是指一个或多个进程的集合,由用户在交互式shell中启动。作业管理是shell提供的功能,使用户能够控制和监视作业的执行状态。作业管理允许用户在前台和后台之间切换作业、挂起和恢复作业以及终止作业。
zyb@myserver:~$ sleep 10000 | sleep 20000 | sleep 30000
其中 & 下面那行是 [作业号]进程组中最后一个进程的pid,我们可以通过
jobs
命令查看当前会话内的作业使用
jobs
命令,可以查看当前会话当中有哪些作业。使用
fg
命令(foreground),可以将某个作业提至前台运行,如果该作业正在后台运行则直接提至前台运行,如果该作业处于停止状态,则给进程组的每个进程发SIGCONT信号使它继续运行并提至前台。例如,使用
fg 1
命令将1号作业提到前台运行。将一个前台进程放到后台运行可以使用Ctrl+Z,但使用Ctrl+Z后该进程就会处于停止状态(Stopped)。
使用
bg
命令,可以让某个停止的作业在后台继续运行(Running),本质就是给该作业的进程组的每个进程发SIGCONT信号。例如,使用
bg 1
命令让1号作业在后台继续运行。
五、会话 作业 进程组 控制终端的关系
会话和控制终端: 一个会话可以与一个控制终端相关联。控制终端为会话中的前台进程组提供用户交互接口。
会话和进程组: 一个会话可以包含多个进程组。进程组可以是前台进程组或后台进程组。
进程组和作业: 在一个交互式shell中,作业通常对应于一个进程组。前台作业与控制终端交互,后台作业则不直接与控制终端交互。
控制终端和作业: 控制终端与前台作业相关联,允许用户通过终端输入控制前台作业。后台作业不能直接从控制终端读取输入,也不能直接接受终端信号。
Session (SID)
├── Control Terminal
├── Process Group 1 (PGID1)
│ ├── Job 1 (Foreground)
│ └── Job 2 (Background)
├── Process Group 2 (PGID2)
│ └── Job 3 (Background)
└── ...
六、守护进程
守护进程(Daemon) 是指在后台运行并且独立于所有终端控制之外的进程,UNIX/Linux系统通常由有许多的守护进程,它们执行着各种系统服务和管理的任务。
守护进程通常不与任何用户直接交互,也没有控制终端。它们独立运行,不受用户会话的影响。
守护进程也称为精灵进程。也一定是孤儿进程。
为什么需要有独立于终端之外的进程呢?首先,处于安全性的考虑我们不希望这样写进程在执行中的信息在任何一个终端上显示。其次,我们也不希望这些进程被中断所产生的中断信号所打断。最后,虽然我们可以通过&
将程序转为后台执行,我们有时也会需要程序能够自动将其转入后台执行,因此,我们需要守护进程。
- 安全性和隐私:
- 当进程独立于终端运行时,避免了进程输出信息在终端上显示。这对于处理敏感信息的进程尤其重要,因为不希望这些信息在任何终端上可见,以防止潜在的安全漏洞或未经授权的访问。
- 避免中断信号:
- 终端信号,如
SIGINT
(由Ctrl+C
产生)和SIGHUP
(挂断信号),通常会发送给前台进程组。如果一个进程与控制终端断开连接,并以守护进程的形式运行,它就不会收到这些信号,从而避免被用户意外地中断。这对需要长期稳定运行的进程非常重要,比如系统监控工具或服务器程序。- 后台运行的需求:
- 虽然用户可以通过在命令后加上
&
符号来手动将进程转为后台运行,但有时我们需要进程自动进入后台。这种情况下,进程可以在启动时自行转为后台模式,成为守护进程。这种设计使得应用程序的启动和运行更加自动化和可靠,而不需要用户的手动干预。- 资源管理和系统稳定性:
- 守护进程通常在系统启动时自动启动,并在系统运行期间持续存在。它们可以用于管理系统资源,监控系统状态,处理网络请求等。这种设计有助于系统的稳定性和可靠性,因为守护进程能够在后台持续执行任务,而不受用户登录会话的影响。
- 特定任务的持续执行:
- 某些任务需要持续执行,如日志记录、定期备份、网络服务的监听等。守护进程适合执行这些任务,因为它们可以在后台长时间运行而不间断。
- 与控制终端无关的操作:
- 守护进程独立于任何控制终端,可以在系统启动后立即运行,即使没有用户登录也可以执行任务。这在服务器环境中尤为重要,因为许多服务器任务需要在无人值守的情况下自动运行。
也就是说,守护进程为系统提供了一种机制,使得后台任务能够独立于用户会话和终端环境运行,从而确保系统的安全性、稳定性和可靠性。这种设计对于操作系统和应用程序的平稳运行至关重要。
那么创建守护进程需要进行哪些操作呢?
- 终止与父进程的关系:
- 调用
fork()
创建一个子进程。父进程可以立即退出,这样子进程就成为了孤儿进程,使其在后台运行。通常由init
进程(进程号为1)接管。
- 调用
- 在新会话中运行:
- 在子进程中调用
setsid()
来创建一个新的会话,这将使子进程成为新会话的首进程,同时也是新进程组的组长进程,并与控制终端断开连接。
- 在子进程中调用
- 重设文件权限掩码:
- 调用
umask(0)
来重设文件权限掩码,确保守护进程创建文件和目录时不会继承父进程的权限掩码限制。
- 调用
- 更改工作目录:
- 调用
chdir("/")
将当前工作目录更改为根目录,防止守护进程持有任何特定目录的文件系统。
- 调用
- 关闭不需要的文件描述符:
- 关闭所有从父进程继承来的不需要的文件描述符。通常,这涉及到关闭标准输入、标准输出和标准错误(文件描述符0、1和2)。或将标准输入、输出和错误重定向到
/dev/null
,这是一个特殊的设备文件,用于丢弃所有写入它的数据。因为守护进程通常在后台运行,不与用户直接交互,所以不需要这些标准流。
- 关闭所有从父进程继承来的不需要的文件描述符。通常,这涉及到关闭标准输入、标准输出和标准错误(文件描述符0、1和2)。或将标准输入、输出和错误重定向到
- 执行守护进程的任务:
- 执行守护进程的实际任务。这可能包括日志记录、监控或其他后台任务。
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <signal.h>
int main() {
pid_t pid;
// 1. 创建子进程
pid = fork();
if (pid < 0) {
return -1; // 创建失败
} else if (pid > 0) {
exit(0); // 父进程退出
}
// 2. 创建新的会话
setsid();
// 3. 改变工作目录
chdir("/");
// 4. 重设文件权限掩码
umask(0);
// 5. 关闭不需要的文件描述符
close(0);
close(1);
close(2);
// 6. 执行守护进程的主功能
while (1) {
// 守护进程的主要功能代码
}
return 0;
}
daemon()
函数是操作系统提供的用于将程序转变为守护进程(daemon),从而在后台运行而无需控制终端。
- 如果
nochdir
为零,daemon()
将更改进程的当前工作目录到根目录("/"
);否则,当前工作目录保持不变。 - 如果
noclose
为零,daemon()
将重定向标准输入、标准输出和标准错误到/dev/null
;否则,这些文件描述符保持不变。
int main()
{
std::cout << "Pid is: " << getpid() << std::endl;
sleep(1);
daemon(0, 0);
// 0: 要更改工作目录
// 0: 输入输出要进行重定向
// daemon(1, 1); // fork father exit
// 执行下面的代码的不是当前进程,而是当前进程的子进程
while (true)
{
std::cout << "hello world" << std::endl;
sleep(1);
}
return 0;
}
守护进程是一个独立的会话,且pid与之前输出的不同。
任何进程启动时默认打开0 1 2 文件描述符。
守护进程与后台进程的区别
-
启动方式和目的
-
守护进程:
- 通常由系统初始化脚本启动,如
/etc/init.d/
或使用现代的systemd
。 - 设计用于执行系统任务,如网络服务、日志记录、定时任务等,通常在系统启动时开始运行,并在系统关闭时停止。
- 通常是长期运行的。
- 通常由系统初始化脚本启动,如
-
后台进程:
- 可以通过在命令后添加
&
符号来启动,使得进程在后台运行。 - 通常用于执行用户任务,如运行脚本、编译程序等,不需要特别的系统资源管理。
- 可能是临时的,也可能长期运行,但通常不作为系统服务的一部分。
- 可以通过在命令后添加
-
-
会话和进程组
-
守护进程:会创建一个新的会话(调用
setsid()
),从而成为会话的首进程,不再是任何进程组的成员。这样做是为了确保守护进程不受终端控制。 -
后台进程:仍然是当前会话的一部分,并且属于一个进程组。它只是不再与终端关联,但仍然可以通过作业控制命令(如
fg
、bg
)进行管理。
-
-
终端控制
-
守护进程:不再与任何终端关联,无法接收来自终端的输入,也不会将输出打印到终端。通常会将标准输入、标准输出和标准错误重定向到
/dev/null
或日志文件。 -
后台进程:仍然可以与终端交互,只是交互被隐藏了。输出可以通过文件描述符重定向到文件或其他地方,但默认情况下,输出仍然可以出现在终端。
-
-
独立性
-
守护进程:设计上更加独立,不依赖于启动它的终端会话。即使登录用户退出,守护进程仍然可以继续运行。
-
后台进程:如果启动它的终端会话关闭,后台进程可能会被发送 SIGHUP 信号,导致其终止。后台进程通常与特定的用户会话关联。
-
总的来说,守护进程是专门为系统服务设计的,它们在系统启动时启动,在系统关闭时停止,并且通常与系统紧密集成。而后台进程更多是用户层面的操作,用于在不阻塞终端的情况下运行任务。