UNIX环境高级编程-第9章- 进程关系

9.4 进程组 

在UNIX 系统中,每个进程除了自己的进程 ID之外,还属于一个进程组。进程组是一个进程或多个进程的集合,进程组本身也有属于自己的进程组ID,进程组属于一个会话,fork()并不改变进程组ID。

进程组组长:
       
进程ID与进程组ID相等的进程。组长可以改变子进程的进程组ID,使其转移到另一进程组。组长进程可以在所在的进程组创建进程,然后终止,从进程组创建开始到其中最后一个进程离开为止的时间区间称为进程组的生存期,进程组的最后一个进程可以终止或者转移到另一个进程组。

        可以通过以下函数获取进程组信息:

/* 进程组 */  
  
/* 
 * 函数功能:获取进程组ID; 
 * 函数原型: 
 */  
#include <unistd.h>  
  
pid_t getpgrp(void);    /* 返回值:调用进程的进程组ID */  
pid_t getpgid(pid_t pid);/* 返回值:若成功则返回进程组ID,若出错则返回-1 */  
  
/* 
 * 函数功能:加入一个现有进程组或者创建一个新的进程组; 
 * 返回值:若成功则返回0,若出错则返回-1; 
 * 函数原型: 
 */  
int setpgid(pid_t pid, pid_t pgid);/* 将pid进程的进程组ID设置为pgid */  
/* 说明: 
 * 若两参数相等,则由pid指定的进程变成进程组组长; 
 * 如果pid = 0,则使用调用者的进程ID; 
 * 若pgid = 0,则由pid指定的进程ID将用作进程组ID; 
 */  

         一个进程只能为它自己或它的子进程设置进程组ID。在它的子进程调用了exec的函数之一后,它就不再能改变该子进程的进程组ID。

测试程序:

#include "apue.h"  
  
int main(void)  
{  
    pid_t pid,fpid;  
  
    if ((pid=fork())<0)  
        err_sys("fork error");  
    else if (0 == pid)  
    {  
        printf("The child process PID is %d.\n",getpid());  
        printf("The child Group ID is %d.\n",getpgrp());  
        printf("The child Group ID is %d.\n",getpgid(0));  
    }  
    else  
    {  
        fpid = getpid();  
        sleep(3);  
        setpgid(pid,fpid);  
        printf("The parent process PID is %d.\n",fpid);  
        printf("The parent Group ID is %d.\n",getpgrp());  
    }  
    exit(0);  
}  

输出结果:

[root@www chapter_9]# gcc -o 9-1 9-1.c
[root@www chapter_9]# ./9-1
The child process PID is 7467.
The child Group ID is 7466.
The child Group ID is 7466.
The parent process PID is 7466.
The parent Group ID is 7466.
[root@www chapter_9]#

9.5 会话

       会话是一个或多个进程组的集合,通常是由shell的管道线将几个进程编成一组;会话首进程是新建会话时,会话中的唯一进程,其进程ID等于会话ID;

       下面是新建会话和获取会话首进程的进程组ID的函数:

/* 会话 */  
  
/* 
 * 函数功能:建立一个新会话; 
 * 返回值:若成功则返回进程组ID,若出错则返回-1; 
 * 函数原型: 
 */  
#include <unistd.h>  
  
pid_t setsid(void);  
  
/* 
 * 函数功能:获取会话首进程的进程组ID; 
 * 返回值:若成功则返回会话首进程的进程组ID,若出错则返回-1; 
 * 函数原型: 
 */  
pid_t getsid(pid_t pid);  
/* 
 * 若pid = 0,则返回调用进程的会话首进程的进程组ID; 
 */  

       若调用 setsid 函数的进程 不是一个进程组组长,则此函数就会创建一个新会话,结果将发生下面三件事:

(1)该进程变成新会话首进程(session leader)。(会话首进程是创建该会话的进程)此时,该进程是新会话中的唯一进程。

(2)该进程成为一个新进程组的组长进程,新进程组ID是该调用进程的进程ID。

(3)该进程没有控制终端,如果在调用setsid之前该进程有一个控制终端,那么这种联系也会被中断。

        若该调用进程已经是一个进程组的组长,则此函数返回出错。为了保证不会发生这种情况,通常先调用fork,然后使其父进程终止,则子进程则继续。因为子进程继承了父进程的进程组ID,而其进程ID是新分配的,保证了子进程不会是一个进程组的组长。
测试程序:

#include "apue.h"  
#include <sys/types.h>  
  
int main(void)  
{  
    pid_t pid;  
    if((pid = fork()) < 0)  
        err_sys("fork error");  
    else if(0 == pid)  
    {  
        setsid();  
        printf("child process pid is %d\n",getpid());  
        printf("group id is %d\n",getpgid(0));  
        printf("My father id is %d\n",getppid());  
        printf("My session id is %d\n",getsid(0));  
    }  
    else  
    {  
        printf("parent process pid is %d\n",getpid());  
        printf("group id is %d\n",getpgid(0));  
        printf("My father id is %d\n",getppid());  
        printf("My session id is %d\n",getsid(0));  
    }  
}  

输出结果:

[root@www chapter_9]# gcc -o 9-2 9-2.c
[root@www chapter_9]# ./9-2
child process pid is 7715
group id is 7715
My father id is 7714
My session id is 7715
parent process pid is 7714
group id is 7714
My father id is 4804
My session id is 4804
[root@www chapter_9]#

9.6 控制终端

   首先总结下进程、进程组与会话之间的关系。进程属于一个进程组,进程组属于一个会话,会话可能有或没有控制终端。以下是一些基本概念:

        僵死进程:一个子进程已经终止,但是其父进程没有对其进行善后处理(获取终止子进程有关信息,释放它仍占有的资源),则该子进程就成为僵死进程。消灭僵尸进程的唯一方法是终止其父进程。
        孤儿进程:子进程的父进程已经终止,但是该进程依然存在,则称该子进程为孤儿进程。孤儿进程会被init进程的收养。一个孤儿进程可以组成孤儿进程组。
       会话首进程:新建会话时,会话中的唯一进程,其进程ID等于会话ID。它通常是一个登陆shell,也可以在成为孤儿进程后调用setsid()成为一个新会话。
       会话:一个或多个进程组的集合。一个登陆shell发起的会话,一般由一个会话首进程、一个前台进程组、一个后台进程组组成。
       进程组:一个或多个进程的集合,进程组属于一个会话。fork()并不改变进程组ID。
       进程组组长:进程ID与其所在进程组ID相等的进程。组长可以改变子进程的进程组ID,使其转移到另一进程组。例如一个shell进程(下文均以bash为例),当使用管道线时,如echo "hello" | cat,bash以第一个命令的进程ID为该管道线内所有进程设置进程组ID。此时echo和cat的进程组ID都设置成echo的进程ID。
       前台进程组:该进程组中的进程能够向终端设备进行读、写操作的进程组。登陆shell(例如bash)通过调用tcsetpgrp()函数设置前台进程组,该函数将终端设备的fd(文件描述符)与指定进程组关联。成为前台进程组的进程其控制终端进程组ID等于进程组ID,常常可以通过比较他们来判断前后台进程组。
       后台进程组:一个会话中,除前台进程组、会话首进程以外的所有进程组。该进程组中的进程能够向终端设备写,但是当试图读终端设备时,将会收到SIGTTIN信号,并停止。登录shell可以根据设置在终端上发出一条消息通知用户有进程欲求读终端。

前台进程组ID只能有一个,而后台进程组同时可存在多个。后台进程组的进程组ID不等于控制终端进程组ID。

终端概念

        在UNIX系统中,用户通过终端登录系统后得到一个Shell进程,这个终端成为Shell进程的控制终端(Controlling Terminal),控制终端是保存在PCB中的信息,而我们知道fork会复制PCB中的信息,因此由Shell进程启动的其它进程的控制终端也是这个终端。默认情况下(没有重定向),每个进程的标准输入、标准输出和标准错误输出都指向控制终端,进程从标准输入读也就是读用户的键盘输入,进程往标准输出或标准错误输出写也就是输出到显示器上。在控制终端输入一些特殊的控制键可以给前台进程发信号,例如Ctrl-C表示SIGINT,Ctrl-\表示SIGQUIT。

      会话和进程组有一些其他特性:

一个会话可以有一个控制终端,通常会话的第一个进程打开一个终端(终端设备或伪终端设备)后,该终端就成为该会话的控制终端。

建立与控制终端连接的会话首进程被称为控制进程。(controlling process)

一个会话中的几个进程组可被分成一个前台进程组以及一个或者多个后台进程组。

如果一个会话有一个控制终端,则它有一个前台进程组,会话中的其他进程组则为后台进程组。

无论何时进入终端的中断键(ctrl+c)或退出键(ctrl+\),就会将中断信号发送给前台进程组的所有进程。

如果终端接口检测到调制解调器(或网络)已经断开,则将挂断信号发送给控制进程(会话首进程)。

这些特性的关系如下图所示:


9.7 gcgetpgrp、tcsetpgrp 和 tcgetsid函数

 


9.8 作业控制

        作业控制允许在一个终端上启动多个作业(进程组),它控制哪一个作业可以访问终端,以及哪些作业在后台运行。作业控制要求以下三种形式的支持:

(1)支持作业控制的shell。

(2)内核中的终端驱动程序必须支持作业控制。

(3)内核必须提供对某些作业控制信号的支持。

守护进程

       守护进程是在后台运行不受终端控制的进程,通常情况下守护进程在系统启动时自动运行,用户关闭终端窗口或注销也不会影响守护进程的运行,只能kill掉。守护进程的名称通常以d结尾,比如sshd、xinetd、crond等;我们用ps axj命令查看系统中的进程,凡是TPGID(前台进程组ID)一栏写着-1的都是没有控制终端的进程,或者TTY一栏为?的,也就是守护进程。


守护进程编程步骤
  1. 创建子进程,父进程退出
    •所有工作在子进程中进行
    •形式上脱离了控制终端
  2. 在子进程中创建新会话
    •setsid()函数
    •使子进程完全独立出来,脱离控制
  3. 改变当前目录为根目录
    •chdir()函数
    •防止占用可卸载的文件系统
    •也可以换成其它路径
  4. 重设文件权限掩码
    •umask()函数
    •防止继承的文件创建屏蔽字拒绝某些权限
    •增加守护进程灵活性
  5. 关闭文件描述符
    •继承的打开文件不会用到,浪费系统资源,无法卸载
    •getdtablesize()
    •返回所在进程的文件描述符表的项数,即该进程打开的文件数目
守护进程创建的流程图如下:

 

成功调用setsid函数的结果是:

创建一个新的Session,当前进程成为Session Leader,当前进程的id就是Session的id。

创建一个新的进程组,当前进程成为进程组的Leader,当前进程的id就是进程组的id。

如果当前进程原本有一个控制终端,则它失去这个控制终端,成为一个没有控制终端的进程。

守护进程的测试程序:

#include "apue.h"  
#include <sys/wait.h>  
#include <sys/types.h>  
#include <fcntl.h>  
  
int main() {  
    pid_t pid;  
    int i,fd;  
    char *buf="Daemon program.\n";  
    /* fork 创建子进程 */  
    if ((pid=fork()) < 0)  
    {  
        err_sys("fork error!");  
        exit(1);  
    }  
    /* 退出父进程 */  
    else if (pid > 0)  
        exit(0);  
    /* 在子进程中创建新会话 */  
    setsid();  
    /* 设置根目录为当前工作目录 */  
    chdir("/");  
    /* 设置权限掩码 */  
    umask(0);  
    /* getdtablesize返回子进程文件描述符表的项数 */  
    for(i=0;i<getdtablesize();i++)  
        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(5);  
        printf("Never exit!\n");  
    }    
    exit(0);  
}  

在日志文件输出为:

Daemon program.  

Daemon program.  

Daemon program.  

........

 

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值