守护进程(Daemon Process),也就是通常说的 Daemon 进程(精灵进程),是 Linux 中的后台服务进程。它是一个生存期较长的进程,通常独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。一般采用以d结尾的名字。

1. 进程组

多个进程的集合就是进程组, 这个组中必须有一个组长, 组长就是进程组中的第一个进程,组长以外的都是普通的成员,每个进程组都有一个唯一的组ID,进程组的ID和组长的PID是一样的。

进程组中的成员是可以转移的,如果当前进程组中的成员被转移到了其他的组,或者进制中的所有进程都退出了,那么这个进程组也就不存在了。如果进程组中组长死了, 但是当前进程组中有其他进程,这个进程组还是继续存在的。下面介绍几个常用的进程组函数:

得到当前进程所在的进程组的组ID

1
pid_t getpgrp(void);

获取指定的进程所在的进程组的组ID,参数 pid 就是指定的进程

1
pid_t getpgid(pid_t pid);

将某个进程移动到其他进程组中或者创建新的进程组

1
int setpgid(pid_t pid, pid_t pgid);
  • 参数:

    • pid: 某个进程的进程ID
    • pgid: 某个进程组的组ID
      • 如果pgid对应的进程组存在,pid对应的进程会移动到这个组中, pid != pgid
      • 如果pgid对应的进程组不存在,会创建一个新的进程组, 因此要求 pid == pgid, 当前进程就是组长了
  • 返回值:函数调用成功返回0,失败返回-1

2. 会话

会话(session)是由一个或多个进程组组成的,一个会话可以对应一个控制终端, 也可以没有。一个普通的进程可以调用 setsid() 函数使自己成为新 session 的领头进程(会长),并且这个 session 领头进程还会被放入到一个新的进程组中。先来看一下setsid()函数的原型:

1
2
3
4
5
6
7
8
9
#include <unistd.h>

// 获取某个进程所属的会话ID
pid_t getsid(pid_t pid);

// 将某个进程变成会话 =>> 得到一个守护进程
// 使用哪个进程调用这个函数, 这个进程就会变成一个会话
pid_t setsid(void);

使用这个函数的注意事项:

  1. 调用这个函数的进程不能是组长进程, 如果是该函数调用失败,如果保证这个函数能调用成功呢?
    • 先fork()创建子进程, 终止父进程, 让子进程调用这个函数
  2. 如果调用这个函数的进程不是进程组长, 会话创建成功
    • 这个进程会变成当前会话中的第一个进程,同时也会变成新的进程组的组长
    • 该函数调用成功之后, 当前进程就脱离了控制终端,因此不会阻塞终端

3. 创建守护进程

如果要创建一个守护进程,标准步骤如下,部分操作可以根据实际需求进行取舍:

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

    • 因为父进程有可能是组长进程,不符合条件,也没有什么利用价值,退出即可
    • 子进程没有任何职务, 目的是让子进程最终变成一个会话, 最终就会得到守护进程
  2. 通过子进程创建新的会话,调用函数 setsid(),脱离控制终端, 变成守护进程

  3. 改变当前进程的工作目录 (可选项, 不是必须要做的)

    • 某些文件系统可以被卸载, 比如: U盘, 移动硬盘,进程如果在这些目录中运行,运行期间这些设备被卸载了,运行的进程也就不能正常工作了。

    • 修改当前进程的工作目录需要调用函数 chdir()

      1
      int chdir(const char *path);
  4. 重新设置文件的掩码 (可选项, 不是必须要做的)

    • 掩码: umask, 在创建新文件的时候需要和这个掩码进行运算, 去掉文件的某些权限

    • 设置掩码需要使用函数 umask()

      1
      mode_t umask(mode_t mask);
  5. 关闭/重定向文件描述符 (不做也可以, 但是建议做一下)

    • 启动一个进程, 文件描述符表中默认有三个被打开了, 对应的都是当前的终端文件

    • 因为进程通过调用 setsid() 已经脱离了当前终端, 因此关联的文件描述符也就没用了, 可以关闭

      1
      2
      3
      close(STDIN_FILENO);
      close(STDOUT_FILENO);
      close(STDERR_FILENO);
    • 重定向文件描述符(和关闭二选一): 改变文件描述符关联的默认文件, 让他们指向一个特殊的文件/dev/null,只要把数据扔到这个特殊的设备文件中, 数据被被销毁了

      1
      2
      3
      4
      5
      int fd = open("/dev/null", O_RDWR);
      // 重定向之后, 这三个文件描述符就和当前终端没有任何关系了
      dup2(fd, STDIN_FILENO);
      dup2(fd, STDOUT_FILENO);
      dup2(fd, STDERR_FILENO);
  6. 根据实际需求在守护进程中执行某些特定的操作

4. 守护进程的应用

写一个守护进程, 每隔2s获取一次系统时间, 并将得到的时间写入到磁盘文件中。

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <signal.h>
#include <sys/time.h>
#include <time.h>

// 信号的处理动作
void writeFile(int num)
{
// 得到系统时间
time_t seconds = time(NULL);
// 时间转换, 总秒数 -> 可以识别的时间字符串
struct tm* loc = localtime(&seconds);
// sprintf();
char* curtime = asctime(loc); // 自带换行
// 打开一个文件, 如果文件不存在, 就创建, 文件需要有追加属性
// ./对应的是哪个目录? /home/robin
// 0664 & ~022
int fd = open("./time+++++++.log", O_WRONLY|O_CREAT|O_APPEND, 0664);
write(fd, curtime, strlen(curtime));
close(fd);
}

int main()
{
// 1. 创建子进程, 杀死父进程
pid_t pid = fork();
if(pid > 0)
{
// 父进程
exit(0); // kill(getpid(), 9); raise(9); abort();
}

// 2. 子进程, 将其变成会话, 脱离当前终端
setsid();

// 3. 修改进程的工作目录, 修改到一个不能被修改和删除的目录中 /home/robin
chdir("/home/robin");

// 4. 设置掩码, 在进程中创建文件的时候这个掩码就起作用了
umask(022);

// 5. 重定向和终端关联的文件描述符 -> /dev/null
int fd = open("/dev/null", O_RDWR);
dup2(fd, STDIN_FILENO);
dup2(fd, STDOUT_FILENO);
dup2(fd, STDERR_FILENO);

// 5. 委托内核捕捉并处理将来发生的信号-SIGALRM(14)
struct sigaction act;
act.sa_flags = 0;
act.sa_handler = writeFile;
sigemptyset(&act.sa_mask);
sigaction(SIGALRM, &act, NULL);

// 6. 设置定时器
struct itimerval val;
val.it_value.tv_sec = 2;
val.it_value.tv_usec = 0;
val.it_interval.tv_sec = 2;
val.it_interval.tv_usec = 0;
setitimer(ITIMER_REAL, &val, NULL);

while(1)
{
sleep(100);
}

return 0;
}