/***************************************************************************** * @copyright 2024-2024, . POWER SUPPLY CO., LTD. * @file daemon.c * @brief 进程守护 * @author Gary * @date 2024/09/20 * @remark *****************************************************************************/ #ifndef _XOPEN_SOURCE #define _XOPEN_SOURCE 700 #include #include #include #include #include #include #include #include #include #include #include #include #include #include "kernel/kit_log.h" #include "daemon.h" #define STOP_TIMEOUT 10000 #define STOP_TICK 2000 /***************************************************************************** * @brief 给定文件描述符fd,尝试对整个文件加写锁(F_WRLCK),以确保该文件在当前进程中被锁定 * @param[in] fd:文件描述符 * @return 1、小于0:表示锁失败了,(1)当失败的原因是EACCES和EAGAIN,表示已经被锁定了;(2)其他错误,表示系统异常了; * 2、大于等于0时:表示锁成功了 *****************************************************************************/ static int try_lock_file(int fd) { // flock 结构用于描述文件锁的属性 struct flock fl; // 设置 fl 的 l_type 字段为 F_WRLCK,表示我们要申请一个写锁(exclusive lock)。写锁意味着其他进程不能对该文件进行写操作。 fl.l_type = F_WRLCK; // 设置 fl 的 l_type 字段为 F_WRLCK,表示我们要申请一个写锁(exclusive lock)。写锁意味着其他进程不能对该文件进行写操作。 fl.l_start = 0; // 设置 fl 的 l_whence 字段为 SEEK_SET,表示 l_start 是相对于文件开头的偏移量。 fl.l_whence = SEEK_SET; // 设置 fl 的 l_len 字段为 0,表示锁定的长度为0,这意味着锁定整个文件(直到文件结束) fl.l_len = 0; return fcntl(fd, F_SETLK, &fl); } /***************************************************************************** * @brief 等待守护进程退出 * @param[in] ms:表示等待的最大时间(毫秒) * @return 0-成功;1-失败 *****************************************************************************/ static int wait_exit(uint32_t ms) { uint32_t tick = 0; // 记录当前已经等待时间(毫秒) int ret = 1; int fd = -1; // 以只写模式打开文件 fd = open(DAEMON_LOCK_FNAME, O_WRONLY); if (fd < 0) { KITLOG(LOG_DAEMON_EN, ERROR_EN, "无法打开文件%s,错误原因:%s", DAEMON_LOCK_FNAME, strerror(errno)); exit(1); } while (tick < ms) { // 尝试锁PID文件 if (try_lock_file(fd) < 0) { // 锁定失败时,错误是EACCES和EAGAIN时,说明进程正在运行 if (EACCES == errno || EAGAIN == errno) { KITPTF(LOG_DAEMON_EN, INFO_EN, "_EMS_C_V1.0.0 停止中"); } // 锁定失败时,非正常锁定异常 else { close(fd); KITLOG(LOG_DAEMON_EN, ERROR_EN, "锁定'%s'发生错误,错误原因:%s", DAEMON_LOCK_FNAME, strerror(errno)); exit(1); } } else { KITPTF(LOG_DAEMON_EN, INFO_EN, "_EMS_C_V1.0.0 已停止"); ret = 0; break; } // 休眠STOP_TICK毫秒 struct timespec tv = { .tv_sec = STOP_TICK / 1000, .tv_nsec = (STOP_TICK % 1000) * 1000000, }; nanosleep(&tv, NULL); tick += STOP_TICK; } close(fd); return ret; } /***************************************************************************** * @brief 守护进程启动函数 * @return *****************************************************************************/ void daemonize() { struct rlimit rl; // 存储资源限制信息 int descriptors; // 用来记录最大文件描述符 pid_t pid; // 进程 ID,用于创建子进程 struct sigaction sa; // 信号结构体 int ret = 1; // 调用 umask(0) 清除文件创建权限掩码,确保新创建文件拥有预期的权限 umask(0); // 调用 getrlimit() 获取进程可打开的最大文件描述符数。如果获取失败,退出程序 if (getrlimit(RLIMIT_NOFILE, &rl) < 0) { KITPTF(LOG_DAEMON_EN, ERROR_EN, "获取进程可打开的最大文件描述符数失败,失败原因:%s", strerror(errno)); exit(1); } // 将文件描述符限制保存在 nfile 中。如果文件描述符限制为 RLIM_INFINITY,则设置为 1024,否则使用 rl.rlim_max descriptors = (RLIM_INFINITY == rl.rlim_max) ? 1024 : rl.rlim_max; // 调用 fork() 创建子进程。父进程退出,子进程继续执行。 // 调用 setsid() 让子进程成为新会话的领导者,并且失去控制终端。 if ((pid = fork()) < 0) { exit(1); } else if (0 != pid) { exit(0); } setsid(); // 忽略 SIGHUP 信号,防止关闭终端时进程也被终止。 sa.sa_handler = SIG_IGN; sa.sa_flags = 0; sigemptyset(&sa.sa_mask); if (sigaction(SIGHUP, &sa, NULL) < 0) { exit(1); } // 再次调用 fork(),以确保新进程不能再次获得控制终端。父进程退出,子进程继续执行。 if ((pid = fork()) < 0) { exit(1); } else if (0 != pid) { exit(0); } // 关闭当前进程打开的所有文件描述符,以便守护进程完全独立。 for (int i = 0; i < descriptors; ++i) { close(i); } // 将把文件描述符 0(标准输入)、1(标准输出)和 2(标准错误)附加到 /dev/null,确保守护进程不依赖终端 open("/dev/null", O_RDWR); ret = dup(0); assert(ret != -1); ret = dup(0); assert(ret != -1); } /***************************************************************************** * @brief 检查守护进程是否已运行 * @return 0-没有运行,进行上锁;1-正在运行 *****************************************************************************/ int checkDaemonRunning() { int ret = 1; int fd = -1; ssize_t size = -1; char buf[16] = {0}; // 调用 umask(0) 清除文件创建权限掩码,确保新创建文件拥有预期的权限 umask(0); // 以读写模式,并创建模式的打开文件 fd = open(DAEMON_LOCK_FNAME, O_RDWR | O_CREAT, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH); if (fd < 0) { // 再次以读写模式,打开文件 fd = open(DAEMON_LOCK_FNAME, O_RDWR, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH); if (fd < 0) { KITLOG(LOG_DAEMON_EN, ERROR_EN, "无法打开文件%s,错误原因:%s", DAEMON_LOCK_FNAME, strerror(errno)); exit(1); } } // 尝试锁定文件 if (try_lock_file(fd) < 0) { // 锁定失败时,错误是EACCES和EAGAIN时,说明进程正在运行 if (EACCES == errno || EAGAIN == errno) { close(fd); return 1; } KITLOG(LOG_DAEMON_EN, ERROR_EN, "锁定'%s'发生错误,错误原因:%s", DAEMON_LOCK_FNAME, strerror(errno)); exit(1); } // 清空文件的内容,使其变为空文件 ret = ftruncate(fd, 0); if (ret < 0) { KITLOG(LOG_DAEMON_EN, ERROR_EN, "无法清空文件%s,错误原因:%s", DAEMON_LOCK_FNAME, strerror(errno)); exit(1); } // 获取文件锁时,向文件里写入PID进程号 snprintf(buf, sizeof(buf), "%ld", (long)getpid()); size = write(fd, buf, strlen(buf) + 1); if (size < 0) { KITLOG(LOG_DAEMON_EN, ERROR_EN, "无法写入文件%s,错误原因:%s", DAEMON_LOCK_FNAME, strerror(errno)); exit(1); } return 0; } /***************************************************************************** * @brief 停止守护进程 * @return 0-成功;1-失败;2-无法打开文件; 3-无法读取文件; 4-无法根据PID值获取进程的pid_t结构体; *****************************************************************************/ int stop_daemon() { int ret = 1; FILE *fp = fopen(DAEMON_LOCK_FNAME, "r"); if (NULL == fp) { KITLOG(LOG_DAEMON_EN, ERROR_EN, "无法打开文件%s,错误原因:%s", DAEMON_LOCK_FNAME, strerror(errno)); return 2; } long pid = -1; if (1 != fscanf(fp, "%ld", &pid)) { KITLOG(LOG_DAEMON_EN, ERROR_EN, "无法读取文件%s的PID值。", DAEMON_LOCK_FNAME); fclose(fp); return 3; } pid_t gid = getpgid((pid_t)pid); if (-1 == gid) { KITLOG(LOG_DAEMON_EN, ERROR_EN, "无法根据PID值获取进程的pid_t结构体,错误原因:%s。", strerror(errno)); fclose(fp); return 4; } // 向进程组发送 SIGINT 信号,等待进程优雅退出。如果超时,发送 SIGKILL 强制杀死进程。 if (0 == kill((pid_t)(-gid), SIGINT)) { if (wait_exit(STOP_TIMEOUT) == 0) { ret = 0; } else { if (0 == kill((pid_t)(-gid), SIGKILL)) { ret = 0; } } } else { KITLOG(LOG_DAEMON_EN, ERROR_EN, "无法杀死gpid值:%d,错误原因:%s。", (long)gid, strerror(errno)); } return ret; } #endif