进程间关系:作业、会话和守护进程

进程并不是完全孤立的个体,或是父子兄弟,或是功能间的相似
通过进程间关系将进程有组织的管理,才让操作系统更加强大、灵活

本文通过介绍进程组、作业、会话讲解进程间关系的基本概念
并延伸介绍一下守护进程

介绍守护进程前,需要先明确作业、会话的概念

进程组

进程在系统中并不是完全独立的个体,每个进程都隶属于一个进程组

使用ps axj可以看到相关信息

1
2
3
4
5
6
# sleep 100 | sleep 200 | sleep 300 & ps axj | head -n1 & ps axj | grep sleep | grep -v grep

PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
3875 4067 4067 2852 pts/0 4070 S 0 0:00 sleep 100
3875 4068 4067 2852 pts/0 4070 S 0 0:00 sleep 200
3875 4069 4067 2852 pts/0 4070 S 0 0:00 sleep 300

其中

  • ‘&’: 表⽰示将进程组放在后台执⾏行
  • ps的参数j表示显示作业相关信息(下文介绍作业)
  • 进程组ID为组长进程ID
  • 组长退出后进程组还在

作业

进程组方便了我们灵活地控制一组进程的状态
但有时我们不仅需要控制状态,还要控制正在进行进程的行为(比如挂起一进程),称为作业控制
在作业控制的角度看进程,进程就是作业

常用的操作比如

1
2
3
4
5
6
7
$ proc1   #表示将proc1作业(进程)放到前台运行
$ proc2 & #表示将proc2作业(进程)放到后台运行

$ fg 进程号 //让指定pid进程进入前台
$ bg 进程号 //让指定pid进程进入后台

$ jobs //查看当前作业

由上例易知,shell创建一个进程即位作业,但作业可以不仅仅包含一个进程

1
$ ps axj | grep root | more &

此时就运行了三个进程

有了作业控制,如果用户在编辑一个文本,可以挂起该作业,运行完其它操作再继续编辑

1
$ vim test.txt #打开文件后按下 ^Z(Ctrl+Z),vim会挂起

此时我们可以在这个shell中做其它操作,当想要回到编辑器时

1
2
3
4
5
$ jobs -l #-l表示显示作业相关进程的pid

[1]+ 5439 Suspended: 18 vim test.txt

$ fg 1 #回到作业1

此外,一个进程fork出的子进程不属于当前作业,很多时候不严格区分进程组和作业

会话

每打开一个终端都会建立一个会话
建立会话的进程叫控制进程,一个会话包含一个前台作业和若干后台作业
Linux下打开一个终端

1
[lxk@localhost ~]$ ls | grep D | more &

再打开一个新的终端

1
$ ls | grep D | more &

$ ps axj | head -1 ; ps axj | grep more | grep -v grep
  PPID   PID   PGID    SID  TTY       TPGID STAT   UID   TIME COMMAND
 11579  11641  11639  11579 pts/1     11758  T    1000   0:00 more
 11692  11749  11747  11692 pts/2     11692  T    1000   0:00 more

可见两个后台的进程属于不同的会话

另外,在mac os下(BSD),不同的终端属于一个会话

守护进程

实际开发我们需要一种进程:
独立于终端之外,不随终端的退出而退出,也不受终端发出的信号的影响,周期地执行某项管理服务

这就是守护进程

我们可以看到很多系统的进程都是守护进程(TPGID = -1)

1
2
3
4
$ ps axj | head -n3
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
0 1 1 1 ? -1 Ss 0 0:16 /usr/lib/systemd/systemd --switched-root --system --deserialize 21
0 2 0 0 ? -1 S 0 0:00 [kthreadd]

预定成俗的,守护进程通常以d结尾,表示daemon(精灵)

打印机例子

打印的任务就用到了守护进程
打印机是一种独占设备,若一个进程打开它又长时间不用,就会导致其他进程都无法使用打印机。
通过创建一个守护进程,当一个进程需要打印文件时,将待打印文件放置到指定目录,由守护进程负责打印工作,即解决了进程空占打印机问题

守护进程的创建

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include<stdio.h>  
#include<unistd.h>
#include<signal.h>
#include<stdlib.h>
int creat_daemon()
{
umask(0);

//调用fork方便随后setsid。
pid_t id = fork();
if(id > 0)
{
exit(1);
}
else if(id == 0)
{
setsid(); //创建新的会话
if(chdir("/")<0) //将新建会话的工作目录改成根目录
{
perror("chdir");
return;
}

//关闭从不需要的父进程继承来的文件
close(0);
close(1);
close(2);

// 注册⼦子进程退出忽略信号,防止僵尸进程
if( sigaction(SIGCHLD, &sa, NULL ) < 0 )
return;
}
}

int main()
{
creat_daemon();
while(1);
return 0;
}

其中

  • 为了使得守护进程权限最大,需要把文件掩码置0
  • 为了使得当前进程脱离当前会话,需要执行setpid
    • pid_t setsid(void);
    • 创建一个新的会话,返回 session id (等于进程id),失败返回-1
    • 若当前进程为进程组组长,则执行失败,所以创建守护进程需要先fork一次
  • 更改工作路径到根目录(或对应盘符无法卸载或目录被强制卸载、守护进程失效)

此外,在更改工作目录前可以fork第二次,
fork的第二次的原因是在执行setsid后进程变成了会话组长,会话组长有权限再次打开终端
通过再次fork,可以保证守护进程无法再次打开终端

但这并不是必须的(很多开源项目都没有使用)

daemon函数

Linux下实际开发中,我们可以使用daemon函数直接创建守护进程

1
2
3
4
5
6
7
#include <stdio.h>
#include <unistd.h>
int main()
{
daemon(0,0);
while(1);
}

其中

  • int daemon(int nochdir, int noclose);
    • nochdir:=0 将当前目录更改至“/”
    • noclose:=0 将标准输入、标准输出、标准错误重定向至“/dev/null”
      • /dev/null被称为黑洞,不管哪个文件描述符对其进行写操作,它都会直接将数据丢弃;
  • 成功返回0,失败放回-1