Linux 进程层次分析

Linux 进程组

每个进程都有一个进程组号 (PGID)

  • 进程组:一个或多个进程的集合 (集合中的进程并不孤立)
  • 进程组中的进程通常存在父子关系,兄弟关系,或功能相近

进程组可方便进程管理 (如:同时杀死多个进程,发送一个信号给多个进程)

  • 每个进程必定属于一个进程组,也只能属于一个进程组
  • 进程除了 PID 外,还有 PGID (唯一,但可变)
  • 每个进程组有一个进程组长,进程组长的 PID 和 PGID 相同

Linux 进程层次分析_第1张图片

pid_t getpgrp(void);  // 获取当前进程的组标识

pid_t getpgid(pid_t pid);  // 获取指定进程的组标识

int setpgid(pid_t pid, pid_t pgid);  // 设置进程的组标识

  • pid == pgid,将 pid 指定的进程设为组长
  • pid == 0,设置当前进程的组标识
  • pgid == 0,则将 pid 作为组标识

进程组示例程序

Linux 进程层次分析_第2张图片

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

Linux 进程层次分析_第3张图片

通过打印可以看出父进程的 pid 和 pgid 相同,为 13297,说明父进程是进程组组长,5 个子进程的 pgid 也是 13297,说明父进程和 5 个子进程为同一个进程组

我们使用 kill 命令将 结束信号发送给 13297 这个进程组,这个进程组的所有进程都被干掉了

Linux 进程层次分析_第4张图片

我们将 13471 这个父进程 kill 掉,发现 5 个子进程还在运行,说明进程组组长运行结束并不会影响其子进程的运行 

深入理解进程组

进程组长终止,进程组依然存在 (进程组长仅用于创建新进程组)

父进程创建子进程后立即通过 setpgid() 改变其组标识 (PGID)

同时,子进程也需要通过 setpgid() 改变自身组标识 (PGID)

子进程调用 exec()

  • 父进程无法通过 setpgid() 改变其组标识 (PGID)
  • 只能自身通过 setpgid() 改变其组标识 (PGID)

进程组标识设置技巧

Linux 进程层次分析_第5张图片

进程组实验

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

程序运行结果如下图所示:

Linux 进程层次分析_第6张图片

可以看出我们成功通过 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(...) 改变子进程的进程组

程序运行结果如下图所示:

Linux 进程层次分析_第7张图片

通过打印可以看出,子进程在调用 execve(...) 后,父进程就无法改变子进程的 pgid 了,不过我们可以在子进程中改变子进程的 pgid,可以成功改变

Linux 会话 (session)

用户通过终端登录系统后会产生一个会话

会话是一个或多个进程组的集合

每个会话有一个会话标识 (SID)

  • 终端登录后的第一个进程成为会话首进程,通常是一个 shell/bash
  • 对于会话首进程 (session leader),其 PID 与 SID 相等

通常情况下,会话与一个终端 (控制终端) 相关联用于执行输入输出操作

  • 会话首进程建立与控制终端的连接 (会话首进程又叫控制进程)
  • 会话中的进程可分为
    • 前台进程组:可接受控制终端中的输入,也可输出数据到控制终端
    • 后台进程组:所有进程后台运行,无法接收终端中的输入,但可以输出数据到终端

Linux 进程层次分析_第8张图片

Linux 进程层次分析_第9张图片

问题:在终端中输入命令后,发生了什么?

当命令行 (shell) 运行命令后创建一个新的进程组

如果运行的命令中有多个子命令则创建多个进程 (处于新建的进程组中)

命令不带 &

  • shell 将新建的进程组设置为前台进程组,并将自己暂时设置为后台进程组

命令中带 &

  • shell 将新建的进程组设置为后台进程组,自己依旧是前台进程组

Linux 进程层次分析_第10张图片

我们将 pgid_j 程序放到前台运行,这时 shell 会将这个进程所在的进程组设置为前台进程组,然后我们按下 ^C,shell 会发送一个终止信号给前台进程组,前台进程组的所有进程都会终止运行

什么是终端进程组标识 (TPGID) ?

标识进程是否处于一个和终端相关的进程组中

前台进程组:TPGID == PGID

后台进程组:TPGID != PGID

若进程和任何终端无关:TPGID == -1

Linux 进程层次分析_第11张图片

Linux 进程层次分析_第12张图片

第一次前台运行, grep 这个进程的 PGID 和 TPGID 相同,说明这个进程当前处于前台进程组

第二次后台运行, grep 这个进程的 PGID 和 TPGID 不同,并且 TPGID 不等于 -1,说明这个进程当前处于后台进程组

Linux 会话接口

#include

pid_t getsid(pid_t pid);  // 获取指定进程的 SID,(pid == 0) => 当前进程

pid_t setsid(void);  // 调用进程不能是进程组长

  • 创建新会话,SID == PID,调用进程成为会话首进程
  • 创建新进程组,PGID == PID,调用进程成为进程组长
  • 调用进程没有控制终端,若调用前关联了控制终端,调用后与控制终端断联

下面的程序输出什么?为什么?

Linux 进程层次分析_第13张图片

会话实验

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(),让子进程成为会话首进程

Linux 进程层次分析_第14张图片

可以看出子进程的 pid 为 14182,它的 PID == PGID == SID,子进程成功的成为了会话首进程,并且它的 TPGID 为 -1,已经脱离了控制终端,但我们看到在当前的终端中还是可以看到子进程的打印,这是为什么呢?

Linux 进程层次分析_第15张图片

在子进程 setsid() 后,虽然与控制终端无关,但还是可以有标准输入输出的,这里标准输出 stdout 还是关联到之前的终端,所以在当前的终端中还是会有打印

标准输入输出与控制终端没关系,只不过默认情况下标准输入输出是与终端关联到了一起

你可能感兴趣的:(Linux系统编程,linux,服务器,运维)