













C语言在Linux中开发没有界面纯后台运行的Demo程序(含日志和Timer)
完全剥离终端界面依赖,仅保留 日志系统 + 精准周期定时器 + 安全启停控制,适合作为底层服务/守护进程的基础骨架。
纯后台运行的C语言开发的程序基础骨架
如有业务逻辑需接入 OnTimer,直接填充即可。
效果图:

工程目录

直接上完整代码
MyCServer.c
#include <stdio.h> //标准输入输出 终端/文件 I/O 基础 #include <stdlib.h> //通用工具与内存管理 进程控制、随机数、字符串转换 #include <string.h> //C 字符串与内存操作 不处理动态分配,仅操作已分配内存 #include <ctype.h> //字符属性判断与转换 安全处理 ASCII/扩展字符 #include <locale.h> //本地化/国际化设置 影响数字格式、字符排序、ctype 行为 #include <time.h> // 必须包含时间头文件 #include <sys/time.h> // 提供 gettimeofday() #include <signal.h> // 后台运行 要在unistd.h上面 #include <unistd.h> // 提供 usleep() /*====================== 显示常量 ======================*/ #define SKIP_LINE 3 // 显示起始行 #define LOOP_TIME 1000 // 定时器周期 (ms) 1000毫秒 /*====================== 全局变量声明 ======================*/ extern int bSetTime; // 定时器标志 extern pthread_t Th; // 定时器线程 /*====================== 函数声明 ======================*/ int InitSys(void); void ExitSys(void); void OnTimer(void); void SetTimer(pthread_t *pTh); void KillTimer(pthread_t Th); void *timer_sr(void *timer_func);
MyCServer.c
#include "MyCServer.h" // 主头文件(包含 TRUNK_CHANNEL 等结构体定义和系统头文件) #include "Logger.h" // 日志模块 /*====================== 全局变量 ======================*/ // 此处为全局变量实际分配内存 int bSetTime = 0; // 定时器标志 pthread_t Th = 0; // 定时器线程(初始化为 0,表示未创建) // 全局安全退出标志 volatile sig_atomic_t g_running = 1; // 信号处理函数 void handle_signal(int sig) { (void)sig; g_running = 0; // 收到信号后修改标志,优雅退出 } int main(void){ printf("Hello World\n"); setvbuf(stdout, NULL, _IONBF, 0); // 关闭标准输出缓冲,日志实时打印 // 初始化日志 Log_Init(); Log_Write("======================================="); Log_Write("======== MyCServer v1.88 启动 =========="); Log_Write("======================================="); if (InitSys() != 0) { Log_Write("[MyCServer][错误] 系统初始化失败"); Log_Close(); return -1; } Log_Write("[MyCServer][信息] 系统初始化成功"); //注册退出信号(Ctrl+C / kill / 终端关闭) signal(SIGINT, handle_signal); // Ctrl+C signal(SIGTERM, handle_signal); // kill 命令默认信号 signal(SIGHUP, handle_signal); // 终端断开 printf("MyCServer 已启动 (PID: %d),将在后台运行...\n", getpid()); //替换原 getch/getchar 循环,改为信号等待 while (g_running) { sleep(1); // 让出 CPU,每秒检查一次退出标志 } Log_Write("[信息] 收到退出信号,正在安全关闭..."); ExitSys(); Log_Close(); printf("程序已安全退出。\n"); return 0; } /*====================== 系统初始化 ======================*/ int InitSys(void) { int nEnableTimer = 1; // Timer 开关:1=启动,其他值=不启动 // 已移除:InitWin(); refresh(); if (nEnableTimer == 1) { SetTimer(&Th); } Log_Write("系统初始化完成==="); return 0; } /*====================== 系统退出 ======================*/ void ExitSys(void) { Log_Write("正在关闭系统..."); // 已移除:clear(); refresh(); endwin(); if (bSetTime == 1) { KillTimer(Th); Log_Write("[MyCServer][信息] Timer定时器已停止"); } } /*====================== 定时器回调 ======================*/ void OnTimer(void) { // 已移除所有 ncurses 刷新与时间显示代码 // 此处可放置纯业务逻辑,例如: // Log_Write("[Timer] 周期触发,执行心跳检查/数据轮询..."); } /*====================== 定时器管理 ======================*/ typedef void (*LP_V_V)(void); void *timer_sr(void *timer_func); // 前向声明 void SetTimer(pthread_t *pTh) { bSetTime = 1; pthread_create(pTh, NULL, timer_sr, (void *)OnTimer); } void KillTimer(pthread_t Th) { if (Th == 0) return; if (bSetTime == 1) { pthread_cancel(Th); bSetTime = 0; pthread_join(Th, NULL); } } /*====================== 定时器工作线程 ======================*/ void *timer_sr(void *timer_func) { struct timeval old, now; long ns, nus; LP_V_V tm_sr = (LP_V_V)timer_func; while (bSetTime) { gettimeofday(&old, NULL); tm_sr(); gettimeofday(&now, NULL); ns = now.tv_sec - old.tv_sec; nus = now.tv_usec - old.tv_usec; if (nus < 0) { ns--; nus += 1000000; } long remaining_us = LOOP_TIME * 1000 - (ns * 1000000 + nus); if (remaining_us > 0) { // 使用 POSIX 标准 nanosleep 替代已废弃的 usleep struct timespec ts; ts.tv_sec = remaining_us / 1000000; ts.tv_nsec = (remaining_us % 1000000) * 1000; nanosleep(&ts, NULL); } } return NULL; }
Logger.h
/* * ============================================================================ * Logger.h - 日志模块头文件 * ============================================================================ */ #ifndef LOGGER_H #define LOGGER_H #include <stdio.h> #include <stdlib.h> #include <string.h> #include <time.h> #include <sys/stat.h> #include <pthread.h> /*====================== 日志配置 ======================*/ #define LOG_BASE_PATH "./logs" // 日志根目录 #define LOG_MAX_SIZE (5 * 1024 * 1024) // 单个日志文件最大大小 (5MB) #define LOG_PATH_MAX 512 // 路径缓冲区大小 #define LOG_BUFFER_MAX 1024 // 日志内容缓冲区大小 #define LOG_TIME_FORMAT_MAX 64 // 时间戳格式缓冲区大小 /*====================== 日志句柄结构 ======================*/ typedef struct { FILE *fp; // 当前日志文件指针 char szFilePath[LOG_PATH_MAX]; // 当前日志文件路径 time_t tLastCheck; // 上次检查时间 long lFileSize; // 当前文件大小 int nLastHour; // 上次小时 int nLastDay; // 上次日期 } LOG_HANDLE; /*====================== 全局变量声明 ======================*/ extern LOG_HANDLE g_Log; // 全局日志句柄 extern pthread_mutex_t g_LogMutex; // 日志互斥锁 /*====================== 函数声明 ======================*/ /** * 初始化日志系统 * 创建日志目录并打开初始日志文件 */ void Log_Init(void); /** * 写入日志 * @param szFormat 格式化字符串 (支持 printf 风格格式) * @param ... 可变参数 */ void Log_Write(const char *szFormat, ...); /** * 检查并切换日志文件 (按小时或文件大小轮转) */ void Log_CheckAndRotate(void); /** * 关闭日志系统 * 关闭日志文件并释放资源 */ void Log_Close(void); /** * 获取当前日志文件路径 * @param szPath 输出路径缓冲区 * @param size 缓冲区大小 */ void Log_GetFilePath(char *szPath, size_t size); /** * 获取文件大小 * @param szPath 文件路径 * @return 文件大小 (字节) */ long Log_GetFileSize(const char *szPath); /** * 创建目录 (递归) * @param szPath 目录路径 */ void Log_CreateDir(const char *szPath); #endif /* SH_LOG_H */
Logger.c
/* * ============================================================================ * Logger.c - 日志模块实现 * ============================================================================ */ #include "Logger.h" #include <stdarg.h> // va_list, va_start, va_end /*====================== 类型定义 ======================*/ #ifndef BOOL_DEFINED #define BOOL_DEFINED typedef int BOOL; #ifndef TRUE #define TRUE 1 #endif #ifndef FALSE #define FALSE 0 #endif #endif /*====================== 全局变量定义 ======================*/ LOG_HANDLE g_Log; // 全局日志句柄 pthread_mutex_t g_LogMutex; // 日志互斥锁 /*====================== 日志函数实现 ======================*/ /** * 创建目录(递归) * @param szPath 目录路径 */ void Log_CreateDir(const char *szPath) { char szDir[LOG_PATH_MAX]; char *p = szDir; strncpy(szDir, szPath, sizeof(szDir) - 1); szDir[sizeof(szDir) - 1] = '\0'; for (p += 1; *p; p++) { if (*p == '/') { *p = '\0'; mkdir(szDir, 0755); *p = '/'; } } mkdir(szDir, 0755); } /** * 获取当前日志文件路径 * 格式:./logs/YYYYMMDD/YYYYMMDD_HH.log * @param szPath 输出路径缓冲区 * @param size 缓冲区大小 */ void Log_GetFilePath(char *szPath, size_t size) { time_t tNow = time(NULL); struct tm *tmNow = localtime(&tNow); snprintf(szPath, size, "%s/%04d%02d%02d/%04d%02d%02d_%02d.log", LOG_BASE_PATH, tmNow->tm_year + 1900, tmNow->tm_mon + 1, tmNow->tm_mday, tmNow->tm_year + 1900, tmNow->tm_mon + 1, tmNow->tm_mday, tmNow->tm_hour); } /** * 获取文件大小 * @param szPath 文件路径 * @return 文件大小 (字节),失败返回 0 */ long Log_GetFileSize(const char *szPath) { struct stat st; if (stat(szPath, &st) == 0) { return st.st_size; } return 0; } /** * 初始化日志系统 * 创建日志目录并打开初始日志文件 */ void Log_Init(void) { memset(&g_Log, 0, sizeof(LOG_HANDLE)); pthread_mutex_init(&g_LogMutex, NULL); // 创建日志根目录 Log_CreateDir(LOG_BASE_PATH); // 打开日志文件 Log_CheckAndRotate(); } /** * 检查并切换日志文件 (按小时或文件大小轮转) * 触发条件: * 1. 小时变化 * 2. 日期变化 * 3. 文件大小超过 5MB */ void Log_CheckAndRotate(void) { time_t tNow = time(NULL); struct tm *tmNow = localtime(&tNow); char szNewPath[LOG_PATH_MAX]; // 检查是否需要切换文件(小时变化或文件过大) BOOL bNeedRotate = FALSE; if (g_Log.fp == NULL) { bNeedRotate = TRUE; } else if (g_Log.nLastHour != tmNow->tm_hour || g_Log.nLastDay != tmNow->tm_mday) { bNeedRotate = TRUE; // 小时或日期变化 } else if (g_Log.lFileSize >= LOG_MAX_SIZE) { bNeedRotate = TRUE; // 文件超过 5MB } if (!bNeedRotate) { return; } // 关闭旧文件 if (g_Log.fp != NULL) { fclose(g_Log.fp); g_Log.fp = NULL; } // 生成新文件路径 Log_GetFilePath(szNewPath, sizeof(szNewPath)); // 创建日期目录 char szDirPath[LOG_PATH_MAX]; time_t tNow2 = time(NULL); struct tm *tmNow2 = localtime(&tNow2); snprintf(szDirPath, sizeof(szDirPath), "%s/%04d%02d%02d", LOG_BASE_PATH, tmNow2->tm_year + 1900, tmNow2->tm_mon + 1, tmNow2->tm_mday); Log_CreateDir(szDirPath); // 打开新文件 g_Log.fp = fopen(szNewPath, "a"); if (g_Log.fp != NULL) { strncpy(g_Log.szFilePath, szNewPath, sizeof(g_Log.szFilePath) - 1); g_Log.lFileSize = Log_GetFileSize(szNewPath); g_Log.nLastHour = tmNow->tm_hour; g_Log.nLastDay = tmNow->tm_mday; g_Log.tLastCheck = tNow; } } /** * 写入日志 * @param szFormat 格式化字符串 (支持 printf 风格格式) * @param ... 可变参数 */ void Log_Write(const char *szFormat, ...) { va_list args; char szBuffer[LOG_BUFFER_MAX]; char szTime[LOG_TIME_FORMAT_MAX]; time_t tNow = time(NULL); struct tm *tmNow = localtime(&tNow); // 生成时间戳 snprintf(szTime, sizeof(szTime), "%04d-%02d-%02d %02d:%02d:%02d", tmNow->tm_year + 1900, tmNow->tm_mon + 1, tmNow->tm_mday, tmNow->tm_hour, tmNow->tm_min, tmNow->tm_sec); // 生成日志内容 va_start(args, szFormat); int nLen = vsnprintf(szBuffer, sizeof(szBuffer), szFormat, args); va_end(args); // 加锁 pthread_mutex_lock(&g_LogMutex); // 检查是否需要切换文件 Log_CheckAndRotate(); // 写入日志 if (g_Log.fp != NULL) { fprintf(g_Log.fp, "[%s] %s\n", szTime, szBuffer); fflush(g_Log.fp); g_Log.lFileSize += nLen + strlen(szTime) + 3; } // 解锁 pthread_mutex_unlock(&g_LogMutex); } /** * 关闭日志系统 * 关闭日志文件并释放资源 */ void Log_Close(void) { pthread_mutex_lock(&g_LogMutex); if (g_Log.fp != NULL) { fclose(g_Log.fp); g_Log.fp = NULL; } pthread_mutex_unlock(&g_LogMutex); pthread_mutex_destroy(&g_LogMutex); }
Makefile
# ========================================== # 编译器与变量定义 # 原生单 *.c 文件的的编译命令 gcc MyCServer.c -o MyCServer # 有Makefile文件的 清理并编译 make clean && make # 运行 ./MyCServer # ========================================== # 定义 C 语言编译器为 gcc CC = gcc # 定义 C++ 编译器为 g++ (虽然本文件主要编译 .c,但预留变量是个好习惯) CXX = g++ # 编译选项 (CFLAGS) # -Wall : 开启几乎所有警告,帮助发现潜在代码问题 # -O2 : 开启二级优化,提高程序运行速度 # -D_XOPEN_SOURCE_EXTENDED : 宏定义,启用 X/Open 标准扩展,通常用于支持宽字符和高级终端功能 # -D_GNU_SOURCE : 宏定义,启用 GNU C 库的扩展功能,确保能使用一些非标准的系统调用 CFLAGS = -Wall -O2 -D_XOPEN_SOURCE_EXTENDED -D_GNU_SOURCE # 链接选项 (LDFLAGS) # -lpthread : 链接 pthread 库,支持多线程编程 # -lshpa3 : 链接 shpa3 库 (可能是你的私有库或特定硬件库) # -lncursesw : 链接 ncursesw 库,'w' 代表 wide-character,用于在终端支持中文和宽字符显示 # -lstdc++ : 链接 C++ 标准库 (因为可能混编了 C++ 代码或依赖库是用 C++ 写的) # -lzmq : 链接 ZeroMQ 库,用于高性能网络消息通信 LDFLAGS = -lpthread -lshpa3 -lncursesw -lstdc++ -lzmq # ========================================== # 2. 目标文件与源文件定义 # ========================================== # 最终生成的可执行文件名 #TARGET = ShPbxServer TARGET = MyCServer # 源文件列表 (所有 .c 文件) #SRCS = ShPbxServer.c Logger.c ZeroMQServer.c ShBoard.c ShMemRecord.c ZeroMQAudioPush.c SRCS = MyCServer.c Logger.c # 目标文件列表 (自动将 SRCS 中的 .c 替换为 .o) # 例如: ShPbxServer.c -> ShPbxServer.o OBJS = $(SRCS:.c=.o) # ========================================== # 3. 伪目标声明 # ========================================== # 声明 .PHONY 告诉 make,all 和 clean 不是文件名,而是动作标签 # 这样可以避免当前目录下有名为 "all" 或 "clean" 的文件时产生冲突 .PHONY: all clean install uninstall # ========================================== # 4. 默认目标 # ========================================== # all 是默认执行的第一个目标,它依赖于 $(TARGET) # 执行 make 时,会先尝试构建 $(TARGET) all: $(TARGET) # ========================================== # 5. 链接规则 (生成可执行文件) # ========================================== # 规则:生成 $(TARGET) 依赖于所有的 $(OBJS) $(TARGET): $(OBJS) # 注意:下面这行开头必须是一个 Tab 键,不能是空格! # 执行链接命令:将所有的 .o 文件链接在一起,生成最终的可执行文件 $(CC) $(CFLAGS) -o $(TARGET) $(OBJS) $(LDFLAGS) # ========================================== # 6. 编译规则 (生成 .o 文件) # ========================================== # 模式规则:告诉 make 如何将任意 .c 文件编译成 .o 文件 # % 是通配符,表示匹配任意文件名 %.o: %.c # 执行编译命令: # -c 表示只编译不链接 # $< 代表第一个依赖文件 (即源文件 .c) # $@ 代表目标文件 (即 .o 文件) $(CC) $(CFLAGS) -c $< -o $@ # ========================================== # 7. 清理规则 # ========================================== # 清理构建产物 clean: # 删除所有的 .o 中间文件和最终的可执行文件 # -f 参数表示强制删除,不提示确认 rm -f $(OBJS) $(TARGET) # ========================================== # 8. 安装与卸载 (可选) # ========================================== # 安装目标:将编译好的程序复制到系统路径,使其可以在任意目录运行 install: $(TARGET) cp $(TARGET) /usr/local/bin/ # 卸载目标:删除系统路径中的程序 uninstall: rm -f /usr/local/bin/$(TARGET) #================几个关键点解释=================== #1、-lncursesw 的作用: #你的程序中使用了 ncursesw 而不是普通的 ncurses。这说明你的程序界面需要显示中文或者特殊符号。如果编译报错找不到库,通常是因为系统没装 libncursesw-dev。 #2、$< 和 $@: #这是 Makefile 的自动变量。 #$<:代表依赖列表中的第一个文件(在这里是 .c 源文件)。 #$@:代表目标文件(在这里是 .o 对象文件)。 #使用它们可以让规则通用,不用为每个文件单独写一行编译命令。 #3、Tab 键缩进: #在 Makefile 中,所有命令(如 $(CC)... 或 rm...)前面必须使用 Tab 键 进行缩进,不能使用空格。这是新手最容易遇到的报错原因(missing separator)。 #4、$(SRCS:.c=.o): #这是一个字符串替换函数。它会自动把 SRCS 变量里所有的 .c 后缀替换成 .o,这样你添加新源文件时,只需要改 SRCS 列表,不用手动去改 OBJS。 #================================================
进入工程目录
执行 清理且编译 命令

前台运行命令 Ctrl+C 则退出 或 关闭终端

后台运行命令(生产环境)关闭终端 程序仍然运行
nohup ./MyCServer > /dev/null 2>&1 &

其中 [1] 1246333 为进程号 用于关闭程序
关闭程序
先查看进程状态
# 查看进程状态 ps aux | grep MyCServer # 或 pgrep -a MyCServer
优雅关闭进程
# 优雅停止(会触发 handle_signal → 执行 ExitSys() 清理资源) kill <PID> # 或按名称停止 pkill MyCServer

| 场景 | 命令 | 说明 |
|---|---|---|
| 日常调试 | nohup ./MyCServer > run.log 2>&1 & |
忽略终端关闭,控制台日志保存到 run.log |
| 完全静默(生产环境) | nohup ./MyCServer > /dev/null 2>&1 & |
丢弃控制台输出(你的 Logger 已写文件时推荐) |
| 彻底脱离 | setsid ./MyCServer > /dev/null 2>&1 & |
创建独立会话,完全不受父终端控制 |
| 临时测试 | ./MyCServer & |
仅放入后台,关闭终端会退出(不推荐生产) |
# 查看进程状态 ps aux | grep MyCServer # 或 pgrep -a MyCServer # 优雅停止(会触发 handle_signal → 执行 ExitSys() 清理资源) kill <PID> # 或按名称停止 pkill MyCServer # 强制停止(不推荐,可能丢失日志或导致资源未释放) kill -9 <PID> # 查看控制台日志(若重定向到了文件) tail -f run.log
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。