首先总结下进程、进程组与会话之间的关系。进程属于一个进程组,进程组属于一个会话,会话可能有或没有控制终端。以下是一些基本概念:
僵死进程:一个子进程已经终止,但是其父进程没有对其进行善后处理(获取终止子进程有关信息,释放它仍占有的资源),则该子进程就成为僵死进程。消灭僵尸进程的唯一方法是终止其父进程。孤儿进程:子进程的父进程已经终止,但是该进程依然存在,则称该子进程为孤儿进程。孤儿进程会被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+\),就会将中断信号发送给前台进程组的所有进程。
- 如果终端接口检测到调制解调器(或网络)已经断开,则将挂断信号发送给控制进程(会话首进程)。
这些特性的关系如下图所示:
作业控制
作业控制允许在一个终端上启动多个作业(进程组),它控制哪一个作业可以访问终端,以及哪些作业在后台运行。作业控制要求以下三种形式的支持:
- 支持作业控制的shell。
- 内核中的终端驱动程序必须支持作业控制。
- 内核必须提供对某些作业控制信号的支持。
守护进程
守护进程是在后台运行不受终端控制的进程,通常情况下守护进程在系统启动时自动运行,用户关闭终端窗口或注销也不会影响守护进程的运行,只能kill掉。守护进程的名称通常以d结尾,比如sshd、xinetd、crond等;我们用ps axj命令查看系统中的进程,凡是TPGID(前台进程组ID)一栏写着-1的都是没有控制终端的进程,或者TTY一栏为?的,也就是守护进程。
守护进程编程步骤
1. 创建子进程,父进程退出
•所有工作在子进程中进行
•形式上脱离了控制终端
2. 在子进程中创建新会话
•setsid()函数
•使子进程完全独立出来,脱离控制
3. 改变当前目录为根目录
•chdir()函数
•防止占用可卸载的文件系统
•也可以换成其它路径
4. 重设文件权限掩码
•umask()函数
•防止继承的文件创建屏蔽字拒绝某些权限
•增加守护进程灵活性
5. 关闭文件描述符
•继承的打开文件不会用到,浪费系统资源,无法卸载
•getdtablesize()
•返回所在进程的文件描述符表的项数,即该进程打开的文件数目
守护进程创建的流程图如下:
- 创建一个新的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.
参考资料:
http://www.cnblogs.com/forstudy/archive/2012/04/03/2427683.html
https://lesca.me/archives/process-relationship.html
http://blog.csdn.net/jnu_simba/article/details/8932176
《UNIX高级环境编程》