详解 Linux 进程组
每个进程都有一个进程组号 (PGID)
- 进程组:一个或多个进程的集合(集合中的进程并不孤立)
- 进程组中的进程通常存在父子关系,兄弟关系,或 功能相近
进程组可方便进程管理(如:同时杀死多个进程,发送一个信号给多个进程)
- 每个进程必定属于一个进程组,也只能属于一个进程组
- 进程除了有 PID 外,还有 PGID (唯一,可变,即某一个进程可以切换进程组)
- 每个进程组有一个进程组长,进程组长的 PID 和 PGID 相同
> ps -o pgid 19843
PGID 977
> kill -- -977
pid_t getpgrp(void);
// 获取当前进程的组标识pid_t getpgid(pid_t pid);
// 获取指定进程的组标识int setpgid(pid_t pid, pid_t pgid);
// 设置进程的组标识pid == pgid
, 将 pid 指定的进程设为组长pid == 0
, 设置当前进程的组标识为 pgidpid == 0
,将 pid 设置为组标识 (即将 pid 所代表的进程设置为进程组长)
进程组示例程序
默认情况下,子进程与父进程属于同一进程组, 是 fork
工作机制的产物(子进程复制当前进程本身)
#include
#include
#include
#include
#include
int main(void)
{
int pid = 0;
int i = 0;
printf("parent = %d, ppid = %d, pgid = %d\n", getpid(), getppid(), getpgrp());
while (i < 5) {
if ((pid = fork()) > 0) {
printf("new : %d\n", pid);
}
else if (pid == 0) {
sleep(1);
printf("child = %d, ppid = %d, pgid = %d\n", getpid(), getppid(), getpgrp());
sleep(60);
printf("last == pgid = %d\n", getpgrp());
break;
}
else {
printf("fork error...\n");
}
++i;
}
if (pid) {
sleep(60);
}
return 0;
}
tiansong@tiansong:~/Desktop/linux$ ./a.out &
[1] 3022
parent = 3022, ppid = 2125, pgid = 3022
new : 3024
new : 3025
new : 3026
new : 3027
new : 3028
tiansong@tiansong:~/Desktop/linux$ child = 3025, ppid = 3022, pgid = 3022
child = 3026, ppid = 3022, pgid = 3022
child = 3027, ppid = 3022, pgid = 3022
child = 3024, ppid = 3022, pgid = 3022
child = 3028, ppid = 3022, pgid = 3022
tiansong@tiansong:~/Desktop/linux$ ps
PID TTY TIME CMD
2125 pts/3 00:00:00 bash
3022 pts/3 00:00:00 a.out
3024 pts/3 00:00:00 a.out
3025 pts/3 00:00:00 a.out
3026 pts/3 00:00:00 a.out
3027 pts/3 00:00:00 a.out
3028 pts/3 00:00:00 a.out
3061 pts/3 00:00:00 ps
tiansong@tiansong:~/Desktop/linux$ kill 3022 // kill 进程组长
[1]+ Terminated ./a.out
tiansong@tiansong:~/Desktop/linux$ ps
PID TTY TIME CMD
2125 pts/3 00:00:00 bash
3024 pts/3 00:00:00 a.out
3025 pts/3 00:00:00 a.out
3026 pts/3 00:00:00 a.out
3027 pts/3 00:00:00 a.out
3028 pts/3 00:00:00 a.out
3149 pts/3 00:00:00 ps
tiansong@tiansong:~/Desktop/linux$ kill -- -3022 // kill 进程组
tiansong@tiansong:~/Desktop/linux$ ps
PID TTY TIME CMD
2125 pts/3 00:00:00 bash
3163 pts/3 00:00:00 ps
进程组深度剖析
深入理解进程组
- 进程组长终止,进程组依然存在(进程组长仅用于创建新进程组)
- 父进程创建子进程后立即通过
setpgid()
改变其组标识(PGID)【当需要将子进程设置到其它进程组时】 - 同时,子进程也需要通过
setpgid()
改变自身组标识(PGID)【当需要将子进程设置到其它进程组时】 当子进程调用
exec()
后- 父进程无法通过
setpgid()
改变子进程组标识(PGID) - 只能自身通过
setpgid()
改变其组标识 (PGID)
- 父进程无法通过
进程组标识设置技巧
实验1:设置子进程为进程组长 为例
#include
#include
#include
#include
#include
int main(void)
{
int pid = 0;
printf("parent = %d, ppid = %d, pgid = %d\n", getpid(), getppid(), getpgrp());
if( (pid = fork()) > 0) {
int r = setpgid(pid, pid); // ① 子进程设置新的进程组
printf("new: %d, r = %d\n", pid, r);
}
else if (pid == 0) {
setpgid(pid, pid); // ② -> setpgid(0,0) -> setpgid(子进程pid, 用子进程id作为进程组id)
sleep(1);
printf("child = %d, ppid = %d, pgid =%d\n", getpid(), getppid(), getpgrp());
}
else {
printf("fork error ...\n");
}
return 0;
}
tiansong@tiansong:~/Desktop/linux$ ./a.out
parent = 3434, ppid = 2125, pgid = 3434
new: 3435, r = 0
child = 3435, ppid = 1, pgid =3435
问:为什么在父子进程都需要调用setpgid
呢?
答:为了双保险。
fork 完成之后,无法确认是父进程先执行还是子进程限制性(现代操作系统一般子进程先执行)。
为了确保不让子进程与父进程在“短暂的时间内”仍出现在相同的进程组中,需要在子进程创建出来之后立即对 “子进程” 进行进程组设置。
实验2:当子进程调用exec()
后,父进程无法通过setpgid()
改变子进程组标识(PGID)
main.c
#include
#include
#include
#include
#include
int main(void)
{
int pid = 0;
printf("parent = %d, ppid = %d, pgid = %d\n", getpid(), getppid(), getpgrp());
if( (pid = fork()) > 0) {
int r = setpgid(pid, pid);
printf("new: %d, r = %d\n", pid, r);
}
else if (pid == 0) {
char *out = "./helloword.out";
char *const ps_argv[] = {out, NULL};
char *const ps_envp[] = {"PATH=/bin:/usr/bin", NULL};
execve(out, ps_argv, ps_envp);
}
else {
printf("fork error ...\n");
}
sleep(60);
return 0;
}
helloword.c
#include
#include
#include
#include
int main(void)
{
printf("child = %d, ppid = %d, pgid = %d\n", getpid(), getppid(), getpgrp());
printf("hello world\n");
sleep(30);
return 0;
}
tiansong@tiansong:~/Desktop/linux$ ./a.out &
[1] 5660
parent = 5660, ppid = 2125, pgid = 5660
new: 5662, r = 0 // r 等于 0, 表示父进程 setpgid 调用成功
child = 5662, ppid = 5660, pgid = 5662
hello world
修改 main.c
#include
#include
#include
#include
#include
int main(void)
{
int pid = 0;
printf("parent = %d, ppid = %d, pgid = %d\n", getpid(), getppid(), getpgrp());
if( (pid = fork()) > 0) {
int r = 0;
sleep(1); // 确保子进程先执行并调用了 execve
r = setpgid(pid, pid);
printf("new: %d, r = %d\n", pid, r);
}
else if (pid == 0) {
char *out = "./helloword.out";
char *const ps_argv[] = {out, NULL};
char *const ps_envp[] = {"PATH=/bin:/usr/bin", NULL};
execve(out, ps_argv, ps_envp);
}
else {
printf("fork error ...\n");
}
sleep(60);
return 0;
}
修改 helloword.c
#include
#include
#include
#include
int main(void)
{
sleep(5); // 等待父进程 setpgid 后再进行后续打印
printf("child = %d, ppid = %d, pgid = %d\n", getpid(), getppid(), getpgrp());
printf("hello world\n");
sleep(30);
return 0;
}
tiansong@tiansong:~/Desktop/linux$ ./a.out &
parent = 5917, ppid = 2125, pgid = 5917
tiansong@tiansong:~/Desktop/linux$ new: 5919, r = -1 // r 等于 -1, 表示 setpgid 执行失败
child = 5919, ppid = 5917, pgid = 5917 // pgid 也未发生变化
hello world
会话与终端的关系
Linux 会话(session)
- 用户通过终端登录系统后产生一个会话
- 会话是一个或多个进程组的集合
每个会话有一个会话标识 (
SID
)- 终端登陆后的第一个进程成为会话首进程,通常是一个
shell/pash
- 对于会话首进程 (
session leader
), 其 PID 与 SID 相等
- 终端登陆后的第一个进程成为会话首进程,通常是一个
通厂情况下,会话与一个终端(控制终端)相关联用于执行输入输出操作
- 会话首进程建立与控制终端的连接(会话首进程又叫做控制进程)
会话中的进程组可分为:
- 前台进程组:可接收控制终端中的输入,也可输出数据到控制终端
- 后台进程组:所有进程后台运行,无法接收终端中的输入,但可输出数据到终端
其中 getty 用于关联终端
会话与前后台进程组
会话中的前台进程组
问题
在终端中输入命令后,发生了什么?
- 当命令行(shell)运行命令后创建一个新的进程组
- 如果运行的命令中有多个子命令则创建多个进程(新创建的进程处于新建的进程组中)
命令不带 &
- shell 将新见的进程组设置为前台进程组,并将自己暂时设置为后台进程组
命令带 shell
- shell 将新见的进程组设置为后台进程组,自己依旧是前台进程组
什么是终端进程组标识(TPGID)
- 标识进程是否处于一个和终端相关的进程组中
- 前台进程:TPGID == PGID (由于前台进程组可能改变,TPGID 用于标识当前的前台进程组)
- 后台进程:TPGID != PGID
- 若进程和任何终端无关:TPGID == -1
通过比较 TPGID 与 PGID 可判断:一个进程属于前台进程组,还是后台进程组
如果进程组和终端相关联,那么当终端关闭断开连接,进程组的进程将全部结束
tiansong@tiansong:~/Desktop/linux$ ps -ajx | grep TPGID
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
2125 6933 6932 2125 pts/3 6932 S+ 1000 0:00 grep --color=auto TPGID
会话编程深度剖析
Linux 会话接口
#include
pid_t getsid(pd_t pid);
// 获取指定进程的 SID, (pid == 0) → 当前进程pid_t setpid(void);
// 用于创建新会话,其中调用进程不能是进程组长,执行了如下动作:- 创建新会话, SID == PID,调用进程会成为会话首进程(在创建的会话中是唯一进程)
- 创建新进程组, PGID == PID, 调用进程成为进程组长 (在创建的进程组中是唯一进程)
- 调用进程没有控制终端,若调用前关联了控制终端,调用后与控制终端断联
#include
#include
#include
#include
#include
int main(void)
{
int pid = 0;
if( (pid = fork()) > 0) {
printf("parent = %d, ppid = %d, pgid = %d, sid = %d\n", getpid(), getppid(), getpgrp(), getsid(getpid()));
printf("new: %d\n", pid);
}
else if (pid == 0) {
setsid(); // 子进程脱离当前会话,创建新的进程会话,新的进程组
sleep(180);
printf("child = %d, ppid = %d, pgid = %d, sid = %d\n", getpid(), getppid(), getpgrp(), getsid(getpid()));
}
else {
printf("fork error ...\n");
}
sleep(240);
return 0;
}
tiansong@tiansong:~/Desktop/linux$ ./a.out // 前台运行
parent = 7291, ppid = 2125, pgid = 7291, sid = 2125
new: 7292
^C // ctrl + c 终止前台进程组中的前台进程
tiansong@tiansong:~/Desktop/linux$ ps // 查看当前终端窗口中运行的基本信息,发现没有 a.out
PID TTY TIME CMD
2125 pts/3 00:00:00 bash
7361 pts/3 00:00:00 ps
tiansong@tiansong:~/Desktop/linux$ ps -ajx | grep a.out // 显示没有控制终端的进程(-x), 搜索 a.out
1 7292 7292 7292 ? -1 Ss 1000 0:00 ./a.out // 7292可知为创建的子进程。? 表明与任何一个终端都不关联,同时 TPGID 为 -1
2125 7383 7382 2125 pts/3 7382 S+ 1000 0:00 grep --color=auto a.out
tiansong@tiansong:~/Desktop/linux$ pstree -p -s -A 7292
systemd(1)---a.out(7292) // 父进程终止运行,被初始化进程接管
#include
#include
#include
#include
#include
int main(void)
{
int pid = 0;
if( (pid = fork()) > 0) {
printf("parent = %d, ppid = %d, pgid = %d, sid = %d\n", getpid(), getppid(), getpgrp(), getsid(getpid()));
printf("new: %d\n", pid);
}
else if (pid == 0) {
setsid();
sleep(3); // 修改此处,方便观察打印
printf("child = %d, ppid = %d, pgid = %d, sid = %d\n", getpid(), getppid(), getpgrp(), getsid(getpid()));
}
else {
printf("fork error ...\n");
}
sleep(240);
return 0;
}
tiansong@tiansong:~/Desktop/linux$ ./a.out
parent = 7947, ppid = 2125, pgid = 7947, sid = 2125
new: 7948
child = 7948, ppid = 7947, pgid = 7948, sid = 7948 // pgid(进程组长), sid(会话首进程)为当前进程,符合预期
^C // ctrl + c 终止前台进程组中的前台进程
tiansong@tiansong:~/Desktop/linux$ ps -ajx | grep 7948
1 7948 7948 7948 ? -1 Ss 1000 0:00 ./a.out // ? 表明与任何一个终端都不关联,同时 TPGID 为 -1
2125 8028 8027 2125 pts/3 8027 S+ 1000 0:00 grep --color=auto 7948
问题:在上述测试中,子进程创建新会话,与当前终端断开,与任何终端都不关联,那么为什么会在当前终端有输出呢?
尽管 setsid 导致子进程再无相关联的终端,但因为 fork 的关系,子进程的 stdout 仍标记的当前所操作的终端上,因此子进程的打印会在当前终端输出
总结:标准输入输出与终端是“无关”的。只不过在默认情况下,标准输入输出和终端挂接到了一起(可以通过重定向使其断连)。(新会话可以没有控制终端,但还是可以有标准输入输出)