每个进程都有一个进程组号 (PGID)
进程组可方便进程管理 (如:同时杀死多个进程,发送一个信号给多个进程)
pid_t getpgrp(void); // 获取当前进程的组标识
pid_t getpgid(pid_t pid); // 获取指定进程的组标识
int setpgid(pid_t pid, pid_t pgid); // 设置进程的组标识
pgid_j.c
#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;
}
这个程序会创建出5个子进程,打印父进程和5个子进程的 pid 和 pgid
通过打印可以看出父进程的 pid 和 pgid 相同,为 13297,说明父进程是进程组组长,5 个子进程的 pgid 也是 13297,说明父进程和 5 个子进程为同一个进程组
我们使用 kill 命令将 结束信号发送给 13297 这个进程组,这个进程组的所有进程都被干掉了
我们将 13471 这个父进程 kill 掉,发现 5 个子进程还在运行,说明进程组组长运行结束并不会影响其子进程的运行
进程组长终止,进程组依然存在 (进程组长仅用于创建新进程组)
父进程创建子进程后立即通过 setpgid() 改变其组标识 (PGID)
同时,子进程也需要通过 setpgid() 改变自身组标识 (PGID)
子进程调用 exec()
进程组实验
pgid_a.c
#include
#include
#include
#include
#include
int main(void)
{
int pid = 0;
printf("parrent = %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);
sleep(1);
printf("child = %d, ppid = %d, pgid = %d\n", getpid(), getppid(), getpgrp());
}
else
{
printf("fork error...\n");
}
sleep(60);
return 0;
}
这个程序用于改变子进程的子进程的 pgid,将子进程的变为进程组组长
第 16 行和第 21 行,在父进程和子进程中,都通过 setgid(...) 来改变子进程为进程组组长,这是因为在 fork() 之后,我们无法确定是父进程先运行还是子进程先运行,如果只在子进程中设置,而父进程先运行,则在父进程运行的某一小段时间内,子进程的 pgid 并未改变,所以需要在父进程和子进程中都通过 setpgid(...) 来改变子进程的 pgid
程序运行结果如下图所示:
可以看出我们成功通过 setpgid(...) 改变了子进程的 pgid,子进程成为了进程组组长
下面我们验证下子进程调用 exec(...) 后,父进程能够通过 setgpid(...) 来改变子进程的 pgid 吗?
pgid_a.c
#include
#include
#include
#include
#include
int main(void)
{
int pid = 0;
printf("parrent = %d, ppid = %d, pgid = %d\n", getpid(), getppid(), getpgrp());
if( (pid = fork()) > 0 )
{
int r = 0;
sleep(1);
r = setpgid(pid, pid);
printf("new: %d, r = %d\n", pid, r);
}
else if( pid == 0 )
{
char* out = "helloworld.out";
char* const ps_argv[] = {out, NULL};
execve("./helloworld.out", ps_argv, NULL);
}
else
{
printf("fork error...\n");
}
sleep(60);
return 0;
}
helloworld.c
#include
#include
#include
#include
int main(void)
{
sleep(5);
printf("before change pid\n");
printf("child = %d, ppid = %d, pgid = %d\n", getpid(), getppid(), getpgrp());
printf("hello world\n");
printf("after change pid\n");
setpgid(getpid(), getpid());
printf("child = %d, ppid = %d, pgid = %d\n", getpid(), getppid(), getpgrp());
sleep(30);
return 0;
}
我们在子进程调用 execve(...) 后,在父进程中调用 setpgid(...) 改变子进程的进程组
程序运行结果如下图所示:
通过打印可以看出,子进程在调用 execve(...) 后,父进程就无法改变子进程的 pgid 了,不过我们可以在子进程中改变子进程的 pgid,可以成功改变
用户通过终端登录系统后会产生一个会话
会话是一个或多个进程组的集合
每个会话有一个会话标识 (SID)
通常情况下,会话与一个终端 (控制终端) 相关联用于执行输入输出操作
当命令行 (shell) 运行命令后创建一个新的进程组
如果运行的命令中有多个子命令则创建多个进程 (处于新建的进程组中)
命令不带 &
命令中带 &
我们将 pgid_j 程序放到前台运行,这时 shell 会将这个进程所在的进程组设置为前台进程组,然后我们按下 ^C,shell 会发送一个终止信号给前台进程组,前台进程组的所有进程都会终止运行
标识进程是否处于一个和终端相关的进程组中
前台进程组:TPGID == PGID
后台进程组:TPGID != PGID
若进程和任何终端无关:TPGID == -1
第一次前台运行, grep 这个进程的 PGID 和 TPGID 相同,说明这个进程当前处于前台进程组
第二次后台运行, grep 这个进程的 PGID 和 TPGID 不同,并且 TPGID 不等于 -1,说明这个进程当前处于后台进程组
#include
pid_t getsid(pid_t pid); // 获取指定进程的 SID,(pid == 0) => 当前进程
pid_t setsid(void); // 调用进程不能是进程组长
会话实验
session.c
#include
#include
#include
#include
#include
int main(void)
{
int pid = 0;
int i = 0;
if( (pid = fork()) > 0 )
{
printf("parrent = %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(120);
return 0;
}
我们在子进程中调用 setsid(),让子进程成为会话首进程
可以看出子进程的 pid 为 14182,它的 PID == PGID == SID,子进程成功的成为了会话首进程,并且它的 TPGID 为 -1,已经脱离了控制终端,但我们看到在当前的终端中还是可以看到子进程的打印,这是为什么呢?
在子进程 setsid() 后,虽然与控制终端无关,但还是可以有标准输入输出的,这里标准输出 stdout 还是关联到之前的终端,所以在当前的终端中还是会有打印
标准输入输出与控制终端没关系,只不过默认情况下标准输入输出是与终端关联到了一起