Linux——守护进程
Linux——守护进程
目录
一、守护进程
1.1 概念
1.2 特点
1.3 守护进程的典型应用场景
1.5 编程流程
1. fork() 退出父进程
2. setsid() 创建新会话
3. fork() 退出父进程 / 丢弃组长和会话首进程的身份,防止
4. chdir("/")
5. umask(0)
6. close()
1.6 守护进程实现:定时记录系统时间到日志文件
一、守护进程
1.1 概念
也叫精灵进程。在后台运行不和用户交互,要给信息让用户看只能写到文件中。日志。
会话:会话首进程 eg打开终端,在打开终端时和内核建立了会话。进程组也有id用进程组组长的gid标识,方便控制。每个会话都有sid
进程组:组长进程
1.2 特点
后台运行:脱离终端控制,不会因终端关闭而终止。
生命周期长:通常在系统启动时创建,直到系统关闭才终止。
无控制终端:没有关联的控制终端(TTY),无法直接接收用户输入。
权限特殊:部分守护进程需要 root 权限(如网络服务、日志服务)。
命名规范:通常以 d 结尾(如 sshd, crond, httpd)。
1.3 守护进程的典型应用场景
系统服务:如网络服务(sshd, httpd)、定时任务(crond)、日志服务(syslogd)。
监控任务:监控系统资源(如内存、磁盘)、检测硬件状态。
批处理作业:定期执行备份、数据同步等任务。
1.4 代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>int main() {// 使用printf函数输出进程ID(pid)、会话ID(sid)和进程组ID(pgid)printf("pid=%d,sid=%d,pgid=%d\n", getpid(), getsid(0), getpgrp());return 0;
}
getpid()
获取当前进程ID。getsid(0)
获取当前进程所属会话 ID,参数0
表示获取自身会话 ID 。getpgrp()
获取当前进程所属进程组 ID 。
会话id没有变化,因为没有换终,打开终端运行的第一个进程就是bash,bash的id等于会话id。进程组id等于进程id是因为此时组内只有一个进程
现在增加进程fork增加子进程
第一个进程和组进程id相同,所以说明它是组长进程。子进程组进程也是3755说明它和第一个进程处于一个进程组中
1.5 编程流程
1. fork()
退出父进程
- 代码行为:使用
fork()
系统调用创建一个子进程,父进程执行exit()
退出 。 - 原理:
fork()
会复制当前进程(父进程),产生一个新的子进程,二者几乎拥有相同的资源副本。父进程退出后,子进程就成为了孤儿进程,会被init
进程(在现代 Linux 系统中多为systemd
进程,进程号通常为 1 )收养。这样做的目的是让子进程在后台运行,脱离父进程的控制终端,为后续成为守护进程做准备。如果父进程不退出,子进程和父进程可能会产生一些关联和干扰,比如共享控制终端等,不利于子进程在后台独立运行。
2. setsid()
创建新会话
- 代码行为:子进程调用
setsid()
函数创建一个新的会话,子进程成为这个新会话的首进程。 - 原理:会话是一组进程的集合,会话首进程具有特殊地位。调用
setsid()
后,子进程与原来的控制终端脱离关系,不再受原来终端的控制(比如终端关闭、用户注销),并且获得了一个新的进程组和会话。这是守护进程在后台持续稳定运行的关键一步,使其能够独立于用户的终端操作。只有非进程组组长才能成功调用setsid
创建新会话。
3. fork()
退出父进程 / 丢弃组长和会话首进程的身份,防止
- 代码行为:再次调用
fork()
创建子进程,然后让当前的父进程(即之前setsid
后的会话首进程 )退出。 - 原理:虽然之前调用
setsid
使子进程成为会话首进程,但会话首进程仍有重新获得控制终端的潜在可能(比如某些特殊情况或误操作 )。再次fork()
并让会话首进程退出后,剩下的子进程就不再是会话首进程,进一步确保守护进程无法再关联到控制终端,更加彻底地在后台运行。
4. chdir("/")
- 代码行为:使用
chdir()
函数将当前工作目录更改为根目录/
。 - 原理:守护进程可能会在系统运行期间一直存在,如果其工作目录是某个特定的挂载点,当该挂载点被卸载时,可能会导致守护进程出现异常(比如文件操作失败等 )。将工作目录设置为根目录
/
,可以避免因工作目录相关问题影响守护进程的正常运行,保证其运行环境的稳定性。
5. umask(0)
- 代码行为:调用
umask()
函数,将文件模式创建掩码设置为 0 。 - 原理:文件模式创建掩码(umask)会影响新创建文件和目录的默认权限。默认情况下,系统会有一个初始的 umask 值,它会屏蔽掉一些权限。将 umask 设置为 0 ,意味着守护进程后续创建的文件和目录会尽可能地拥有最大权限,方便进行文件读写等操作,避免因权限问题导致无法创建或访问相关资源。
6. close()
- 代码行为:通常是关闭标准输入(
STDIN_FILENO
)、标准输出(STDOUT_FILENO
)和标准错误(STDERR_FILENO
)对应的文件描述符。 - 原理:守护进程在后台运行,不需要与用户的终端进行交互,也就不需要使用标准输入来接收用户输入,也不需要将输出打印到终端。关闭这些文件描述符可以避免资源浪费,并且防止意外的输入输出操作干扰守护进程的运行,同时也能避免守护进程的输出信息混乱终端显示等情况。
windows上的守护进程,许多正在运行但我们没有看到
1.6 守护进程实现:定时记录系统时间到日志文件
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <time.h>
#include <sys/stat.h>// 获取文件描述符表大小的函数声明(在部分系统中可能需要额外定义或链接相关库)
int getdtablesize(); // 将当前进程转化为守护进程的函数
void set_daemon() {pid_t pid = fork();// 第一次fork,父进程退出if (pid!= 0) {exit(1);}// 创建新会话setsid(); pid = fork();// 第二次fork,父进程退出if (pid!= 0) {exit(1);}// 更改工作目录到根目录chdir("/"); // 设置文件掩码umask(0); int maxfd = getdtablesize();// 关闭所有文件描述符for (int i = 0; i < maxfd; i++) {close(i);}
}int main() {// 将当前进程设置为守护进程set_daemon(); while (1) {time_t tv;// 获取当前时间戳time(&tv); FILE* fp = fopen("/tmp/c2405d.log", "a");if (fp == NULL) {// 打开文件失败则退出循环break; }// 将格式化后的当前时间写入文件fprintf(fp, "Time is %s", asctime(localtime(&tv))); fclose(fp);// 休眠5秒sleep(5); }return 0;
}
第二次调用fork使得剩下的新子进程不再是会话首进程,进一步确保守护进程无法再关联到控制终端,更加彻底地在后台运行。
定义一个
time_t
类型变量tv
用于存储时间戳,并调用time
函数获取当前时间戳,存储到tv
中。
fprintf
函数:
- 作用:将格式化后的当前时间信息写入指定文件
- 参数:
fp
:文件指针(指向已打开的日志文件)。"Time is %s"
:格式化字符串,%s
表示替换为字符串。asctime(...):
实际替换的时间字符串 。- 通过
localtime
函数可以将时间戳 tv =1612137600
(假设对应 2021 年 1 月 31 日 00:00:00 )转换为包含具体年月日时分秒等信息的struct tm
结构体。
asctime
函数 接受一个指向struct tm
结构体的指针timeptr
,将其表示的时间格式化为字符串形式。格式化后的字符串格式固定,例如:"Sun Jun 6 10:23:59 2021\n"
,包含了星期、月份、日期、时间、年份信息,并且字符串末尾自动添加换行符\n
。
fopen
函数:
- 作用:以指定模式打开文件,返回文件指针(
FILE*
)。- 参数:
- 路径:
"/tmp/c2405d.log"
指定日志文件位置。/tmp
是临时目录,程序崩溃或重启不会保留日志,但对短期测试足够。- 模式:
"a"
表示追加模式(Append),特点:
- 文件不存在时自动创建。
- 文件存在时,写入指针定位到文件末尾,不会覆盖原有内容。
- 多进程 / 线程同时写入时,操作系统保证写入操作的原子性(但可能导致日志行交叉,需更高并发控制)。
- 返回值:
- 成功:返回非空文件指针(
FILE*
)。- 失败:返回
NULL
(如权限不足、磁盘已满、路径错误等)