Linux--进程组、会话、守护进程

转:http://www.cnblogs.com/forstudy/archive/2012/04/03/2427683.html

int setpgid(pid_t pid, pid_t pgid);

pid_t getpgid(pid_t pid)

pid_t getpgrp(void)//用来取得目前进程所属的组识别码。此函数相当于调用getpgid(0);返回目前进程所属的组识别码。

pid_t setsid(void);

可以使用ps j这个命令获取进程的PPID (父进程ID), PID (本进程 ID), PGID (进程组 ID) and SID (会话 ID)。

进程组与会话 Linux Process Groups and Sessions

  一个或多个进程的集合

  进程组ID: 正整数

在类Unix系统中,用户通常会跟各种相关的进程打交道。虽然在登录的时候只有一个终端进程(用户对应的登录shell ,通过这个shell启动各种程序和服务),但通常不久以后就会产生许多相关的进程,例如进行如下动作:

  • 在后台运行无交互的程序(例如bash命令中末位的"&")
  • 通过shell的 job control在各种交互进程之间切换
  • 通过管道启动一组程序
  • 在图形环境下(例如X window system)启用多个终端窗口

为了管理这些进程,内核便对这些进程进行了分组,称其为进程组,几个进程组又构成一个会话。例如下图所示,lsless在一个进程组里,而grepwc在一个进程组里。这两个进程组又同属于一个会话

.1 进程组

一个进程组可以包含多个进程

      进程组中的这些进程之间不是孤立的,他们彼此之间或者存在者父子、兄弟关系,或者在功能有相近的联系。

      那linux为什么要有进程组呢?其实提供进程组就是方便管理这些进程。假设要完成一个任务,需要同时并发100个进程,当用户由于

     某种原因要终止这个任务时,要是没有进程组,就需要一个个去杀死这些进程,设置了进程组之后,就可以对进程组中的每个进程进行杀死

每一个进程都属于一个“进程组”,当一个进程被创建的时候,它默认是其父进程所在组的成员。传统上,一个进程的组ID(pgid)等于这个组的第一个成员(也称为进程组领导)。

可以使用ps j这个命令获取进程的PPID (父进程ID), PID (本进程 ID), PGID (进程组 ID) and SID (会话 ID)。

frank@under:~$ ps j
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
24120 24125 24125 24125 pts/5    24125 Ss+   1000   0:00 bash
24120 30633 30633 30633 pts/6    31468 Ss    1000   0:00 bash
30633 31468 31468 30633 pts/6    31468 R+    1000   0:00 ps j

当使用不具有工作管理(job control)shell时(例如ash ),每一个shell创建的子进程都会和shell在同一个进程组和会话里。

当使用具有工作管理shell时(例如bash ),如果使用管道(参考:Pipes: A Brief Introduction), 那么这些管道连接起来的进程单独组成一个进程组,和shell在一个会话中。例如:

% cat paper | ideal | pic | tbl | eqn | ditroff > out

这几个程序运行后的进程都在一个进程组里面。

前台进程组

每一个会话最多有一个进程组是“前台进程组”,控制终端(下面的第二部分会讲)会将输入和信号传给该进程组的成员(例如你在终端按下Ctrl+C就会向前台进程组发送SIGINT信号)。进程可以通过系统调用 tcgetpgrp(fd)获取所在会话的前台进程组ID,其中fd对应会话的控制终端的文件描述符;也可以通过tcsetpgrp(fd,pgrp)设置所在会话的前台进程组,其中fd对应会话的控制终端的文件描述符, pgrp是这个会话中的一个进程组。

那么如何得到fd呢? ctermid() 调用会返回控制终端的名字,在符合 POSIX标准的系统上,它会返回 /dev/tty 这个文件,然后我们就可以用系统调用open()打开这个文件从而得到文件描述符了。

后台进程组

在一个会话中,除前台进程组外的进程组都称为“后台进程组”,这些后台进程组的进程不参与终端的输入输出,如果它们尝试从终端读取数据,会收到SIGTTIN信号(其默认操作是Stop),同时终端会通知用户:

SIGTTIN   21,21,26    Stop    Terminal input for background process

但是如果后台进程忽略或者blockSIGTTIN信号了,或者它所在的进程组是一个“孤儿进程组”(下面有讲),那么读取终端就会得到一个EIO(error in operation)错误而非收到SIGTTIN信号。当后台进程尝试向终端写操作时,它可能会受到SIGTTOU信号:

SIGTTOU   22,22,27    Stop    Terminal output for background process

同样地,如果后台进程忽略或者blockSIGTTOU信号了,或者它所在的进程组是一个“孤儿进程组”,那么读取终端就会得到一个EIO(error in operation)错误而非收到SIGTTOU信号。

为了让后台进程加入前台进程组,可以使用fg命令。

相应操作

可以通过系统调用 setpgid()将进程加入到另一个进程组中:

int setpgid(pid_t pid, pid_t pgid);

其中pid是要操作的进程,0代表本进程;pgid是要加入的进程组,0代表要加入的进程组的pgid是这个进程的pid(也就是说,这个组的组领导就是这个进程)。

使用 setpgid() 要注意以下几点:

  1. 一个进程可能将pgid设置为自己或者它所在组的其他成员,这样的操作可能不会改变其他任意进程的进程组,即使这个进程有root权限。
  2. 会话头进程(第二部分会介绍)不能改变自己所在的进程组。
  3. 一个进程不能被加入到另一个会话中的进程组中,换句话说,setpgid只能在一个会话中使用。由于setpgid()只能将进程在本会话中“移动”,所以两个会话不可能有相同的进程组或者进程。

一个进程可以通过 getpgrp()系统调用获得自己所在组的ID,也可以通过 getpgid(p) 获得pid为p的进程所在组的组ID,当p为0时,获得本进程的组ID:

pid_t getpgid(pid_t pid)
pid_t getpgrp(void)

组长进程

  组长进程标识: 其进程组ID==其进程ID

  组长进程可以创建一个进程组,创建该进程组中的进程,然后终止

  只要进程组中有一个进程存在,进程组就存在,与组长进程是否终止无关

  进程组生存期: 进程组创建到最后一个进程离开(终止或转移到另一个进程组)

一个进程可以为自己或子进程设置进程组ID

  setpgid()加入一个现有的进程组或创建一个新进程组

父进程改变自身和子进程的组id

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main() {
    pid_t pid;

    if ((pid=fork())<0) {
        printf("fork error!");
    }else if (pid==0) {
        printf("The child process PID is %d.\n",getpid());
        printf("The Group ID is %d.\n",getpgrp());
        printf("The Group ID is %d.\n",getpgid(0));
        printf("The Group ID is %d.\n",getpgid(getpid()));
        exit(0);
    }

    sleep(3);
    printf("The parent process PID is %d.\n",getpid());
    printf("The Group ID is %d.\n",getpgrp());

    return 0;
}

孤儿进程组

现在我们来讨论当会话消失的时候进程是如何终止的。

假设有一个在终端下运行的会话,其会话头是一个shell。当这个shell存在时,会话中的进程组处于不同的环境中,可能在运行,也可能被挂起了。当终端关闭时,如果它正在运行,当终端关闭后它就无法读入或者输出了;如果它被挂起了,则它可能永远不会被唤醒(也不会终止)。在这种情况下,原会话的进程组就被称为“孤儿进程组”。POSIX定义为该进程组的父进程也是该进程组的成员或者是别的会话的成员。总之,只要一个进程组的父进程在同一会话的不同组中,它就不是孤儿进程组。

当一个进程组成为孤儿进程组之后,在这个进程组中的每一个进程都会被发送一个SIGHUP信号——通常进程将会被正常关闭。对于收到SIGHUP信号后,选择不终止的程序将会被发送一个SIGCONT,这个信号将会重启任何被挂起的进程。这个信号流程能够关闭大多数的进程并保证剩下的是正在运行的进程(不被挂起)。

当进程被“遗弃”后,它被强制和控制终端分离(其他的用户便可以使用这个终端)。原来的会话ID继续被保留(不作为新生进程的PID)直到每一个会话中的程序退出。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main() {
    pid_t pid;

    if ((pid=fork())<0) {
        printf("fork error!");
        exit(1);
    }else if (pid==0) {
        printf("The child process PID is %d.\n",getpid());
        printf("The Group ID of child is %d.\n",getpgid(0)); // 返回组id
        sleep(5);
        printf("The Group ID of child is changed to %d.\n",getpgid(0));
        exit(0);
    }

    sleep(1);
    setpgid(pid,pid); // 改变子进程的组id为子进程本身
    
    sleep(5);
    printf("The parent process PID is %d.\n",getpid());
    printf("The parent of parent process PID is %d.\n",getppid());
    printf("The Group ID of parent is %d.\n",getpgid(0));
    setpgid(getpid(),getppid()); // 改变父进程的组id为父进程的父进程
    printf("The Group ID of parent is changed to %d.\n",getpgid(0));

    return 0;
}


会话

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

  开始于用户登录

  终止与用户退出

  此期间所有进程都属于这个会话期

当一个用户注销的时候,内核会终止用户之前启动的所有进程(不然这些进程会在那一直运行并等待输入的到来)。为了简化这个任务,内核将几个进程组并为一个“会话”。会话的ID就是通过setsid()启动这个会话的进程的PID (也就是这个会话的第一个进程,通常是用户的shell),这个进程也称为“会话头”,它随后产生的所有子孙进程都默认在这个会话里。

进程也可以使用setsid使自己离开自己的会话,其参数为空,返回新的会话的ID:

#include <unistd.h>

pid_t setsid(void);

建立新会话:setsid()函数

  该调用进程是组长进程,则出错返回

    先调用fork, 父进程终止,子进程调用

  该调用进程不是组长进程,则创建一个新会话

    •该进程变成新会话首进程(session header)

    •该进程成为一个新进程组的组长进程。

    •该进程没有控制终端,如果之前有,则会被中断

组长进程不能成为新会话首进程,新会话首进程必定会成为组长进程

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main() {
    pid_t pid;

    if ((pid=fork())<0) {
        printf("fork error!");
        exit(1);
    }else if (pid==0) {
        printf("The child process PID is %d.\n",getpid());
        printf("The Group ID of child is %d.\n",getpgid(0));
        printf("The Session ID of child is %d.\n",getsid(0));
        sleep(10);
        setsid(); // 子进程非组长进程,故其成为新会话首进程,且成为组长进程。该进程组id即为会话进程
        printf("Changed:\n");
        printf("The child process PID is %d.\n",getpid());
        printf("The Group ID of child is %d.\n",getpgid(0));
        printf("The Session ID of child is %d.\n",getsid(0));
        sleep(20);
        exit(0);
    }

    return 0;
}

在子进程中调用setsid()后,子进程成为新会话首进程,且成为一个组长进程,其进程组id等于会话id

守护进程

  Linux大多数服务都是通过守护进程实现的,完成许多系统任务

  0: 调度进程,称为交换进程(swapper),内核一部分,系统进程

  1: init进程, 内核调用,负责内核启动后启动Linux系统

  没有终端限制

  让某个进程不因为用户、终端或者其他的变化而受到影响,那么就必须把这个进程变成一个守护进程

守护进程编程步骤

  1. 创建子进程,父进程退出

    •所有工作在子进程中进行

    •形式上脱离了控制终端

  2. 在子进程中创建新会话

    •setsid()函数

    •使子进程完全独立出来,脱离控制

  3. 改变当前目录为根目录

    •chdir()函数

    •防止占用可卸载的文件系统

    •也可以换成其它路径

  4. 重设文件权限掩码

    •umask()函数

    •防止继承的文件创建屏蔽字拒绝某些权限

    •增加守护进程灵活性

  5. 关闭文件描述符

    •继承的打开文件不会用到,浪费系统资源,无法卸载

    •getdtablesize()

    •返回所在进程的文件描述符表的项数,即该进程打开的文件数目

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <fcntl.h>

int main() {
    pid_t pid;
    int i,fd;
    char *buf="This is a daemon program.\n";

    if ((pid=fork())<0) {
        printf("fork error!");
        exit(1);
    }else if (pid>0)  // fork且退出父进程
        exit(0);
    
    setsid();    // 在子进程中创建新会话。
    chdir("/");  // 设置工作目录为根
    umask(0);    // 设置权限掩码
    for(i=0;i<getdtablesize();i++)  //getdtablesize返回子进程文件描述符表的项数
        close(i);                // 关闭这些不将用到的文件描述符

    while(1) {// 死循环表征它将一直运行
// 以读写方式打开"/tmp/daemon.log",返回的文件描述符赋给fd
        if ((fd=open("/tmp/daemon.log",O_CREAT|O_WRONLY|O_APPEND,0600))<0) {
            printf("Open file error!\n");
            exit(1);
        }
        // 将buf写到fd中
        write(fd,buf,strlen(buf)+1);
        close(fd);
        sleep(10);
        printf("Never output!\n");
    }

    return 0;
}

猜你喜欢

转载自blog.csdn.net/h490516509/article/details/85096031