守护进程(Daemon Process),也就是通常说的 Daemon 进程(精灵进程),是 Linux 中的后台服务进程。它是一个生存期较长的进程,通常独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。一般采用以
d
结尾的名字。1.进程组
多个进程的集合就是进程组, 这个组中必须有一个组长, 组长就是进程组中的第一个进程,组长以外的都是普通的成员,每个进程组都有一个唯一的组ID,进程组的ID和组长的PID是一样的。
进程组中的成员是可以转移的,如果当前进程组中的成员被转移到了其他的组,或者进制中的所有进程都退出了,那么这个进程组也就不存在了。如果进程组中组长死了, 但是当前进程组中有其他进程,这个进程组还是继续存在的。下面介绍几个常用的进程组函数:
得到当前进程所在的进程组的组ID
pid_t getpgrp(void);
获取指定的进程所在的进程组的组ID,参数 pid 就是指定的进程
pid_t getpgid(pid_t pid);
将某个进程移动到其他进程组中或者创建新的进程组
int setpgid(pid_t pid, pid_t pgid);
- 参数:
pid
: 某个进程的进程IDpgid
: 某个进程组的组ID- 如果pgid对应的进程组存在,pid对应的进程会移动到这个组中, pid != pgid
- 如果pgid对应的进程组不存在,会创建一个新的进程组, 因此要求 pid == pgid, 当前进程就是组长了
- 返回值:函数调用成功返回0,失败返回-1
2.会话
会话(session)是由一个或多个进程组组成的,一个会话可以对应一个控制终端, 也可以没有。一个普通的进程可以调用
setsid()
函数使自己成为新 session 的领头进程(会长),并且这个 session 领头进程还会被放入到一个新的进程组中。先来看一下setsid()
函数的原型:#include <unistd.h> // 获取某个进程所属的会话ID pid_t getsid(pid_t pid); // 将某个进程变成会话 =>> 得到一个守护进程 // 使用哪个进程调用这个函数, 这个进程就会变成一个会话 pid_t setsid(void);
使用这个函数的注意事项:
- 调用这个函数的进程不能是组长进程, 如果是该函数调用失败,如果保证这个函数能调用成功呢?
- 先
fork()
创建子进程, 终止父进程, 让子进程调用这个函数
- 先
- 如果调用这个函数的进程不是进程组长, 会话创建成功
- 这个进程会变成当前会话中的第一个进程,同时也会变成新的进程组的组长
- 该函数调用成功之后, 当前进程就脱离了控制终端,因此不会阻塞终端
3.创建守护进程
如果要创建一个守护进程,标准步骤如下,部分操作可以根据实际需求进行取舍:
- 创建子进程, 让父进程退出
- 因为父进程有可能是组长进程,不符合条件,也没有什么利用价值,退出即可
- 子进程没有任何职务, 目的是让子进程最终变成一个会话, 最终就会得到守护进程
- 通过子进程创建新的会话,调用函数
setsid()
,脱离控制终端, 变成守护进程 - 改变当前进程的工作目录 (可选项, 不是必须要做的)
- 某些文件系统可以被卸载, 比如: U盘, 移动硬盘,进程如果在这些目录中运行,运行期间这些设备被卸载了,运行的进程也就不能正常工作了。
- 修改当前进程的工作目录需要调用函数
chdir()
int chdir(const char *path);
- 重新设置文件的掩码 (可选项, 不是必须要做的)
- 掩码:
umask
, 在创建新文件的时候需要和这个掩码进行运算, 去掉文件的某些权限 - 设置掩码需要使用函数
umask()
mode_t umask(mode_t mask);
- 掩码:
- 关闭/重定向文件描述符 (不做也可以, 但是建议做一下)
- 启动一个进程, 文件描述符表中默认有三个被打开了, 对应的都是当前的终端文件
- 因为进程通过调用
setsid()
已经脱离了当前终端, 因此关联的文件描述符也就没用了, 可以关闭close(STDIN_FILENO); close(STDOUT_FILENO); close(STDERR_FILENO);
- 重定向文件描述符(和关闭二选一): 改变文件描述符关联的默认文件, 让他们指向一个特殊的文件
/dev/null
,只要把数据扔到这个特殊的设备文件中, 数据被被销毁了int fd = open("/dev/null", O_RDWR); dup2(fd, STDIN_FILENO); dup2(fd, STDOUT_FILENO); dup2(fd, STDERR_FILENO);
- 根据实际需求在守护进程中执行某些特定的操作
4.守护进程的应用
写一个守护进程, 每隔2s获取一次系统时间, 并将得到的时间写入到磁盘文件中。
#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; }
5.进程区别
守护进程(Daemon Process),普通进程(Foreground Process)和后台进程(Background Process)是在操作系统中常见的三种进程类型。它们之间的异同点如下:
定义与用途:
- 守护进程:守护进程是在后台运行的进程,独立于终端会话,并且通常在系统启动时启动。它们通常用于执行系统级任务或服务,如网络服务、定时任务等。
- 普通进程:普通进程是由用户启动的进程,在终端会话中运行,并且通常需要用户交互。它们可以是用户应用程序或系统工具,如文本编辑器、浏览器等。
- 后台进程:后台进程是在终端会话中运行的进程,但它们没有占用当前终端的输入和输出。它们可以在后台执行,并且用户可以同时进行其他任务。
终端依赖性:
- 守护进程:守护进程通常与终端无关,不依赖于特定的终端会话。
- 普通进程:普通进程与终端会话相关,需要在终端中运行,并接收用户输入和输出。
- 后台进程:后台进程在终端会话中运行,但不会接收终端的输入,通常将输出重定向到文件或/dev/null。
生命周期:
- 守护进程:守护进程通常在系统启动时启动,并持续运行,直到系统关闭或显式被停止。
- 普通进程:普通进程由用户启动,当用户退出或进程完成任务时,普通进程会终止。
- 后台进程:后台进程在终端会话中运行,但用户可以在后台进程执行期间执行其他任务,而不会阻塞当前终端。
总的来说,守护进程是在后台运行的系统级服务,与终端无关;普通进程是由用户启动的进程,需要在终端中运行并与用户交互;后台进程在终端会话中运行,但不会阻塞当前终端输入,用户可以同时执行其他任务。