什么是守护进程
守护进程(Daemon Process),也就是通常说的 Daemon 进程(精灵进程),是 Linux 中的后台服务进程。它是一个生存期较长的进程,通常独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。
在了解守护进程之前我们先了解我们平时进程的一些状态
可以看到除了我们平时常知的PPID和PID外,还有许多我们未曾了解过的信息,在这些里面有我们晦涩难懂的名词:进程组,会话
进程组
进程组是一个或多个进程的集合,这些进程共享同一个进程组ID(PGID),并且它们可以通过进程组ID来进行控制。
一般来说当一个进程被创建时,它会被分配到与其父进程相同的进程组中。这个组中必须有一个组长, 组长就是进程组中的第一个进程,组长以外的都是普通的成员,每个进程组都有一个唯一的组ID,进程组的ID和组长的PID是一样的。
进程组中的成员是可以转移的,如果当前进程组中的成员被转移到了其他的组,或者进制中的所有进程都退出了,那么这个进程组也就不存在了。如果进程组中组长死了, 但是当前进程组中有其他进程,这个进程组还是继续存在的。
进程租的主要特征就是信号可以发给进程组中的所有进程:这个信号可以使同一个进程组中的所有进程终止,停止或者继续运行。
下面是一些有关进程组的函数:
//得到当前进程所在的进程组的组ID
pid_t getpgrp(void);
//获取指定的进程所在的进程组的组ID,参数 pid 就是指定的进程
pid_t getpgid(pid_t pid);
//将某个进程移动到其他进程组中或者创建新的进程组
int setpgid(pid_t pid, pid_t pgid);
//如果pgid对应的进程组存在,pid对应的进程会移动到这个组中, pid != pgid
//如果pgid对应的进程组不存在,会创建一个新的进程组, 因此要求 pid == pgid, 当前进程就是组长了
会话
会话是一个或多个进程的集合,这些进程通常是由同一个终端启动的,并且它们共享同一个控制终端。
当用户在终端上启动一个进程时,会话就会被创建。会话有一个会话组长进程,它是会话中第一个创建的进程,通常是shell进程。
下面是一些有关会话的函数:
// 获取某个进程所属的会话ID
pid_t getsid(pid_t pid);
// 使用哪个进程调用这个函数, 这个进程就会变成一个会话
pid_t setsid(void);
在使用第二个函数接口时我们要注意一些事项:
- 调用这个函数的进程不能是组长进程
- 如果调用成功这个进程会变成当前会话中的第一个进程,同时也会变成新的进程组的组长
- 该函数调用成功之后, 当前进程就脱离了控制终端,因此不会阻塞终端
守护进程
我们知道Linux系统启动是会启动很多服务的,这些系统服务进程没有控制终端,不能直接和用户交互,其他进程都是在用户登录或运行程序时创建,在运行结束或者用户注销时终止,但系统服务进程不受用户登录注销的影响,它一直存在,这就是一种守护进程
我们自己如何创建一个守护进程
如果要创建一个守护进程,标准步骤如下,部分操作可以根据实际需求进行取舍:
1.创建子进程, 让父进程退出
因为父进程有可能是组长进程,不符合条件,也没有什么利用价值,退出即可
子进程没有任何职务, 目的是让子进程最终变成一个会话, 最终就会得到守护进程
2.通过子进程创建新的会话,调用函数 setsid(),脱离控制终端, 变成守护进程
3.改变当前进程的工作目录 (可选项, 不是必须要做的)
某些文件系统可以被卸载, 比如: U盘,移动硬盘,进程如果在这些目录中运行,运行期间这些设备被卸载了,运行的进程也就不能正常工作了。
修改当前进程的工作目录需要调用函数 chdir()
int chdir(const char *path);
4.重新设置文件的掩码 (可选项, 不是必须要做的)
掩码
: umask,,在创建新文件的时候需要和这个掩码进行运算,,去掉文件的某些权限
设置掩码需要使用函数 umask()
mode_t umask(mode_t mask);
5.关闭/重定向文件描述符 (不做也可以, 但是建议做一下)
启动一个进程,文件描述符表中默认有三个被打开了(标准输入,标准输出,标准错误),对应的都是当前的终端文件
因为进程通过调用 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);
/dev/null 又叫信息黑洞,是一个特殊的设备文件,用于接收和丢弃所有写入它的数据。当你将数据重定向到 /dev/null 时,所有的输出都会被丢弃,不会在终端上显示,也不会写入到任何文件。
6.根据实际需求在守护进程中执行某些特定的操作
话不多说,我们直接开始动手实践
写一个守护进程, 每隔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;
}
其他的创建守护进程的方式
系统调用daemon()函数
Daemon ()函数用于希望将自己从控制终端分离出来并作为系统守护进程在后台运行的程序。
如果 nochdir 为零,daemon ()将调用进程的当前工作目录更改为 root 目录(“/”) ,
否则当前工作目录保持不变。
如果 noclose 为零,则 daemon ()将标准输入、标准输出和标准错误重定向到/dev/null;
否则不对这些文件描述符进行任何更改。
命令行指令让进程守护化
nohup [进程名] &
补充知识:前端进程和后端进程
在Linux中,前端进程和后端进程是指与终端(或控制终端)的关联方式不同的两种进程。
- 前端进程(Foreground Process)是指与终端直接关联的进程。它会接收终端的输入,并将输出发送到终端。前端进程通常是由用户在终端上启动的,用户可以与其进行交互。例如,当你在终端上执行一个命令时,这个命令就是一个前端进程。前端进程通常会占用终端,直到其执行完毕或被中断。
- 后端进程(Background Process)是指与终端没有直接关联的进程。它不会接收终端的输入,也不会将输出发送到终端。后端进程通常是通过在命令行中在命令的末尾添加"&"符号启动的。例如,当你在终端上执行一个命令并在末尾添加"&"时,这个命令就是一个后端进程。后端进程在后台运行,不会占用终端,可以让用户继续在终端上执行其他操作。
前端进程和后端进程的区别主要体现在与终端的关联方式和对终端的占用情况上。前端进程与终端直接关联,会占用终端并接收终端的输入;后端进程与终端没有直接关联,不会占用终端,也不会接收终端的输入。
总结
守护进程的主要目的是提供某种服务或功能,例如网络服务、系统监控等。它们通常以root用户身份运行,并且具有较高的权限来执行特定的任务。守护进程通常不与用户进行交互,它们在后台默默地运行,并在需要时响应特定的事件或请求。
以下是守护进程的一些常见特征和要点:
- 后台运行:守护进程在后台运行,不与任何终端关联。它们通常通过将标准输入、输出和错误输出重定向到文件来实现。
- 无终端关联:守护进程不会受到终端关闭或用户注销的影响。它们独立于用户登录会话,并且在系统启动时启动。
- 高权限:守护进程通常以root用户身份运行,以便执行特定的任务,例如访问系统资源、配置文件等。
- 事件驱动:守护进程通常通过监听特定的事件或请求来执行任务。它们可以通过套接字、信号、文件系统监控等方式来接收和处理事件。
- 日志记录:守护进程通常会将运行日志记录到文件中,以便后续的故障排查和性能分析。
- 进程控制:守护进程可以通过信号来控制其行为,例如启动、停止、重启等。
守护进程的示例:常见的守护进程包括网络服务(如Apache、Nginx)、数据库服务(如MySQL、PostgreSQL)、系统监控工具(如syslogd、crond)等。
创建一个守护进程通常需要以下步骤:
- 创建子进程:使用fork()系统调用创建一个子进程。
- 脱离终端:使用setsid()系统调用将子进程从终端中脱离。 关闭终端文件描述符:关闭子进程继承的终端文件描述符。
- 重定向标准输入、输出和错误输出:将标准输入、输出和错误输出重定向到文件。
- 设置工作目录:将工作目录切换到一个适当的位置。
- 设置文件权限掩码:使用umask()系统调用设置文件权限掩码。
- 执行守护进程的主要任务:执行守护进程的主要任务或服务。
- 记录日志:将运行日志记录到文件中。
- 处理信号:通过信号处理函数来处理守护进程接收到的信号。
守护进程的设计和实现需要考虑到很多因素,例如进程间通信、并发性、资源管理等。正确地设计和实现守护进程可以提高系统的可靠性和性能。