1. 进程组
进程组是一个或多个进程的集合,同一进程中的各进程接收来自同一终端的各种信号。
每个进程组都有一个组长进程,组长进程ID就是该进程组ID。
只要进程组中有一个进程存在,该进程组就存在(跟组长进程是否提前终止无关)。
/* API : pid_t getpgrp(void)
* 描述 : 返回调用进程的进程组ID
*/
/* API : pid_t getpgid(pid_t pid)
* 描述 : 返回指定进程的进程组ID
* @pid 为0时表示返回调用进程的进程组ID
*/
/* API : int setpgid(pid_t pid,pid_t pgid)
* 描述 : 设置指定进程的进程组ID
* @pid 为0时表示修改调用进程自身的进程组ID
* @pgid 为0时表示使用指定进程的进程ID作为要设置的进程组ID
*
* 备注 : 根据不同入参值,本API可以产生2类不同语义:
* 将指定进程加入一个现有的进程组;
* 以指定进程为组长进程创建一个新的进程组。
*
* 一个进程只能为自身或其子进程设置进程组ID
*/
保证一个进程不是组长进程的方法:先调用fork,然后使其父进程终止,由于子进程的进程组ID继承自父进程,可以确保子进程不是一个进程组的组长。
2. 会话
会话是一个或多个进程组的集合。
每个会话都有一个会话首进程,会话首进程肯定又是一个进程组的组长进程。
每个会话最多可以有一个控制终端(也可以没有),并且只有会话首进程拥有获取控制终端的资格(这一点很重要)。
每个会话最多可以有一个前台进程组(也可以没有),剩下的都是后台进程组。
SIGINT 和 SIGQUIT 这两个信号都只会专门作用于每个前台进程。
因为跟终端连接断开而产生的 SIGHUP 信号只会发送给控制进程。
/* API : pid_t getsid(pid_t pid)
* 描述 : 返回指定进程的会话首进程的进程组ID(可以简单理解为就是指定进程所在会话的会话ID)
* @pid 为0时表示返回调用进程的会话ID
*/
/* API : pid_t setsid(void)
* 描述 : 创建一个新会话(前提是调用本API的进程不是一个进程组的组长进程,否则创建失败)
*
* 备注 : 本API具体执行了以下这些操作:
* 调用进程将变成新会话的会话首进程;
* 如果调用进程关联了一个控制终端,则将其切断,也就确保新的会话首进程没有控制终端;
* 调用进程将成为一个新进程组的组长进程
*/
后台进程和前台进程的本质区别在于是否关闭了标准输入。
3. 守护进程
守护进程的唯一特性是没有控制终端(需要特别注意的是,没有控制终端不意味着关闭了标准输出),除此之外,以下2点并不对守护进程构成约束:
大部分用户层守护进程都是进程组的组长进程;
大部分用户层守护进程都是会话的首进程.
/* API : int daemon(int nochdir,int noclose)
* 描述 : 创建守护进程
* @nochdir 为0时将进程当前工作目录更改为根目录"/",非0时表示不修改当前工作目录
* @noclose 为0时将进程标准输入、标准输出、标准错误都重定向到/dev/null,非0时表示不修改
*
* 备注 : 创建守护进程的过程主要分为以下几步:
* 调用umask将文件模式创建屏蔽字设置为一个已知值(通常是0表示不作任何屏蔽)
* 调用fork,然后使父进程exit,目的是确保子进程不是一个进程的组长进程
* 调用setsid创建新的会话,使调用进程成为新会话的首进程、成为新进程组的组长进程、没有控制终端
* 将当前工作目录更改为一个已知目录(通常就是根目录"/")
* 关闭不需要的文件描述符
* 将进程标准输入、标准输出、标准错误都重定向到/dev/null(通常的守护进程都会这么做)
*/
4. 例子(验证没有控制终端情况下仍然继承了标准输出)
#include
#include
#include
int main(int argc,char *argv[])
{
if(daemon(0,1) < 0){
perror("daemon");
return -1;
}
printf("current working dir : %s\n",getcwd(NULL,0));
return 0;
}
操作步骤:
[1]. 进入例子test.c所在目录,执行编译
$ gcc -Wall test.c
[2]. 编译成功后,在当前目录下生成对应的可执行文件a.out,执行该程序
$ ./a.out
[3]. 该程序创建一个守护进程(意味着没有控制终端),并输出打印,终端屏幕打印如下
current working dir : /
结论:显然该守护进程从父进程处继承了标准输出