进程组和会话在 UNIX 系统中是非常重要的概念,特别是在进行作业控制和终端会话管理时。下面是关于进程组和会话的详细解释:
进程组(Process Group)
-
定义与作用:
- 进程组是一个或多个进程的集合,这些进程通常是从同一个作业启动的,共享同一个进程组标识符(PGID)。例如,一个复杂的命令行管道操作(如
cmd1 | cmd2
)中的所有进程可以属于同一个进程组。 - 进程组使得操作系统可以方便地对一组相关进程执行操作,如发送信号。
- 进程组是一个或多个进程的集合,这些进程通常是从同一个作业启动的,共享同一个进程组标识符(PGID)。例如,一个复杂的命令行管道操作(如
-
进程组首进程:
- 每个进程组有一个首进程,这是创建该组的进程,其进程 ID(PID)同时作为该进程组的 ID。
- 进程组首进程在创建时设置组 ID,并可以通过特定的系统调用(如
setpgid()
)修改自己或其他进程的进程组归属。
-
生命周期:
- 进程组的生命周期从首进程创建该组开始,直到最后一个进程退出该组时结束。
- 进程可能会因为终止自身或被其他信号杀死而离开进程组,或者它可以调用系统函数切换到另一个进程组。
会话
会话(session)管理是 UNIX 系统中处理进程组和终端交互的重要机制。这里提供对您提到的各个点的更深入的解释:
会话首进程与会话 ID
- 会话首进程:创建新会话的进程自动成为该会话的首进程。这通常发生在用户登录时或者当一个进程通过调用
setsid()
系统调用来显式创建一个新会话。 - 会话 ID:新会话的创建同时生成一个新的会话标识符(session ID),该标识符等于该会话首进程的进程 ID (PID)。新创建的进程会继承其父进程的会话 ID,除非它们显式地创建或加入一个新的会话。
控制终端
- 建立控制终端:控制终端通常在会话首进程首次打开一个终端设备时被建立。控制终端是会话中进程交互的主要方式,如读取用户的键盘输入和输出信息到屏幕。
- 终端与会话的关系:一个终端设备在任一时间点最多只能是一个会话的控制终端。一旦终端被某个会话首次占用,其他会话将无法将其设置为控制终端。
前台与后台进程组
- 前台进程组:在任何给定时刻,会话中只有一个进程组可以成为前台进程组。这个组的进程可以直接从控制终端接收输入,这对于交互式应用非常关键。
- 后台进程组:不在前台的进程组称为后台进程组。这些进程组不能直接从控制终端读取输入,这样设计可以避免输入冲突和管理复杂度。
信号处理与终端字符
- 终端字符生成信号:用户在控制终端输入特定的键(如 Ctrl+C)会生成信号(如 SIGINT),这些信号会被发送到前台进程组的所有进程。这允许用户可以直接与运行在前台的进程组交互,例如中断正在执行的命令。
控制进程
- 会话首进程的角色:一旦控制终端的连接建立,会话首进程也成为控制进程。这意味着它负责处理来自控制终端的信号和可能的终端断开(如用户注销)。
通过这些机制,UNIX 系统提供了强大的工具来管理和控制多个进程和它们的交互,这对于构建稳定和可管理的多用户环境是必要的。
进程组、会话、控制终端之间的关系
在 UNIX 系统中,进程组、会话和控制终端之间的关系是协调进程之间通信和控制的关键机制。这些概念相互作用,支持复杂的作业控制和会话管理,特别是在多用户环境中。下面是这些概念之间关系的详细解释:
1. 进程组(Process Group)
进程组是一种逻辑分组,用于将一系列相关的进程组织在一起,以便于统一管理。通常情况下,这些进程是从同一个命令行启动的,比如一个 shell 脚本中的多个命令。进程组内的每个进程都共享同一个进程组标识符(PGID),使得操作系统可以方便地对整个组发送信号。
2. 会话(Session)
会话是一或多个进程组的更高级别的集合。每个会话可以包含多个进程组,包括前台和后台进程组。会话通常在用户登录时开始,用户的登录 shell 成为会话首进程,并且会创建一个新的会话。这个会话首进程有权申请或分配一个控制终端。
3. 控制终端(Controlling Terminal)
控制终端是会话首进程与用户交互的界面。它通常是一个物理或虚拟的终端设备(如终端窗口)。会话首进程在首次打开一个终端设备时,该设备成为该会话的控制终端。控制终端可以被用来向会话中的进程发送信号(例如,当用户按下 Ctrl+C 时发送 SIGINT)。
关系概述:
- 会话与进程组:会话是进程组的容器,可以包含多个进程组。在一个会话中,可以指定一个进程组作为前台进程组,其他的则作为后台进程组。前台进程组可以接收来自控制终端的输入,而后台进程组则不能。
- 会话与控制终端:会话首进程可以为会话获取一个控制终端,这个终端成为整个会话及其所有进程组的交互中心。控制终端可以被用来管理会话中的进程,如发送中断信号。
- 进程组与控制终端:在有控制终端的会话中,只有前台进程组可以直接从控制终端接收输入和处理特定的控制信号(如 SIGINT)。后台进程组则被限制,不能直接读取控制终端的输入,以防止输入输出冲突。
这种层次和关系的设置使得 UNIX 系统能够有效地管理多任务操作,特别是在多用户和网络环境中,提供了高度的灵活性和控制能力。
进程组、会话操作函数
在 UNIX 和类 UNIX 系统中,操作进程组和会话的一些核心函数允许程序员在运行时管理进程的组和会话属性。这些函数非常重要,因为它们提供了控制进程如何与操作系统和其他进程交互的能力。
1. 获取和设置进程组
pid_t getpgrp(void);
- 描述:此函数返回当前进程的进程组 ID。
- 返回值:返回调用进程的进程组 ID。
pid_t getpgid(pid_t pid);
- 描述:获取指定进程的进程组 ID。如果
pid
为 0,则返回调用进程的进程组 ID。 - 参数:
pid
:进程标识符。
- 返回值:成功时返回进程组 ID,失败时返回 -1 并设置 errno。
int setpgid(pid_t pid, pid_t pgid);
- 描述:设置指定进程的进程组 ID。这个函数可以用来将进程
pid
加入到进程组pgid
,或者创建一个新的进程组(如果pid
等于pgid
)。 - 参数:
pid
:要更改进程组的进程的 ID,如果pid
是 0,则表示调用进程。pgid
:新的进程组 ID,如果pgid
是 0,则pid
指定的进程将成为新进程组的首进程。
- 返回值:成功时返回 0,失败时返回 -1 并设置 errno。
2. 获取和设置会话
pid_t getsid(pid_t pid);
- 描述:获取指定进程的会话 ID。如果
pid
为 0,则返回调用进程的会话 ID。 - 参数:
pid
:进程标识符。
- 返回值:成功时返回会话 ID,失败时返回 -1 并设置 errno。
pid_t setsid(void);
- 描述:创建一个新的会话,并设置当前进程为该会话的首进程和进程组首进程。当前进程不能是进程组的领头进程。
- 返回值:成功时返回新会话的会话 ID,失败时返回 -1 并设置 errno。
注意事项
- 在使用
setpgid()
或setsid()
时,要注意进程间的权限和状态,因为这些调用可能受到限制,特别是在已经拥有子进程或已属于某个进程组的情况下。 - 调用
setsid()
时,如果调用者已是某个进程组的领头进程,则该调用会失败。这是为了防止一个已有控制终端的进程逃避终端产生的信号。
这些函数提供了在 UNIX 类操作系统中管理和控制进程组和会话的基本机制,对于实现作业控制、守护进程以及终端会话管理等功能至关重要。
守护进程
守护进程(Daemon process)是 Linux 系统中一个非常核心的概念,对于系统的稳定运行和服务的持续提供至关重要。这些进程通常是在系统启动时启动,并在整个系统运行期间持续运行,直到系统关闭。下面详细解释守护进程的特征和其实现方法:
特征
-
长生命周期:
- 守护进程通常在系统启动时创建,例如通过系统的初始化系统(如
systemd
、init
)自动启动。 - 它们不会像交互式用户进程那样在任务完成后退出,而是一直运行,直到系统关闭。
- 守护进程通常在系统启动时创建,例如通过系统的初始化系统(如
-
在后台运行:
- 守护进程通常在后台运行,不与任何用户直接交互。
- 它们通常不拥有控制终端,这防止了终端信号(如 SIGINT 或 SIGQUIT)意外中断它们的运行。
-
服务性质:
- 守护进程通常执行周期性任务或等待特定事件的发生,例如监听网络请求或文件系统变化。
- 例如,HTTP 服务器(如 Apache 的
httpd
)、邮件服务器(如postfix
)和文件服务器(如smbd
)都是典型的守护进程。
实现守护进程
这个过程是在 UNIX 和 Linux 系统中常用的模式,用于创建独立于控制终端的后台服务。以下是如何执行这个转换的详细步骤,包括其中涉及的各个系统调用和目的:
1.创建子进程并结束父进程
这个步骤确保了新的守护进程不是一个会话领导者,从而可以调用 setsid()
创建新会话。
pid_t pid = fork();
if (pid < 0) {// 处理错误exit(EXIT_FAILURE);
}
if (pid > 0) {// 父进程直接退出exit(EXIT_SUCCESS);
}
2. 创建新会话
子进程调用 setsid()
创建一个新的会话,成为会话的领导者,脱离任何控制终端。
if (setsid() < 0) {// 处理错误exit(EXIT_FAILURE);
}
3. 清除文件创建掩码(umask)(非必须)
设置 umask 为 0 确保守护进程创建的任何文件和目录都有适当的访问权限。
umask(0);
4. 更改当前工作目录
将当前工作目录改为根目录 /
,这可以防止守护进程阻止卸载文件系统。
if (chdir("/") < 0) {// 处理错误exit(EXIT_FAILURE);
}
5. 关闭所有继承的文件描述符
这可以防止守护进程无意中保持打开的文件描述符,可能导致资源泄露或不必要的行为。
int x;
for (x = sysconf(_SC_OPEN_MAX); x >= 0; x--) {close(x);
}
6. 重定向标准文件描述符到 /dev/null
关闭了标准输入、输出和错误(文件描述符 0, 1, 2)后,常见做法是打开 /dev/null
并用 dup2()
使得这些描述符指向 /dev/null
。这样,任何尝试读写这些文件描述符的操作都不会有任何效果。
int fd = open("/dev/null", O_RDWR);
dup2(fd, 0); // STDIN
dup2(fd, 1); // STDOUT
dup2(fd, 2); // STDERR
7. 执行核心业务逻辑
在完成所有的设置后,守护进程可以开始执行其核心功能,如周期性检查、处理请求等。
while (1) {// 执行任务sleep(1); // 休眠是为了模拟周期性任务
}
以上步骤创建了一个完全独立的守护进程,它在系统后台安静地运行,几乎不受用户会话影响,适用于需要长时间运行并且不需要用户交互的服务。这种类型的进程是服务器和服务架构的基础。
案例
/*写一个守护进程,每隔2s获取一下系统时间,将这个时间写入到磁盘文件中。
*/#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/time.h>
#include <signal.h>
#include <time.h>
#include <stdlib.h>
#include <string.h>void work(int num) {// 捕捉到信号之后,获取系统时间,写入磁盘文件time_t tm = time(NULL);struct tm * loc = localtime(&tm);// char buf[1024];// sprintf(buf, "%d-%d-%d %d:%d:%d\n",loc->tm_year,loc->tm_mon// ,loc->tm_mday, loc->tm_hour, loc->tm_min, loc->tm_sec);// printf("%s\n", buf);char * str = asctime(loc);int fd = open("time.txt", O_RDWR | O_CREAT | O_APPEND, 0664);write(fd ,str, strlen(str));close(fd);
}int main() {// 1.创建子进程,退出父进程pid_t pid = fork();if(pid > 0) {exit(0);}// 2.将子进程重新创建一个会话,新的会话会脱离原来的控制终端setsid();// 3.设置掩码umask(022);// 4.更改工作目录chdir("/home/nowcoder/");// 5. 关闭、重定向文件描述符int fd = open("/dev/null", O_RDWR);dup2(fd, STDIN_FILENO);dup2(fd, STDOUT_FILENO);dup2(fd, STDERR_FILENO);// 6.业务逻辑// 捕捉定时信号struct sigaction act;act.sa_flags = 0;act.sa_handler = work;sigemptyset(&act.sa_mask);sigaction(SIGALRM, &act, NULL);struct itimerval val;val.it_value.tv_sec = 2;val.it_value.tv_usec = 0;val.it_interval.tv_sec = 2;val.it_interval.tv_usec = 0;// 创建定时器setitimer(ITIMER_REAL, &val, NULL);// 不让进程结束while(1) {sleep(10);}return 0;
}