嵌入式Linux网络编程:守护进程实战开发(自动重启+多客户端支持)
【本文代码已在Linux平台验证通过】
一、守护进程核心需求
1.1 功能特性
1.2 技术指标
指标 | 参数要求 |
---|---|
重启响应时间 | ≤3秒 |
最大并发连接数 | 1024(可配置) |
内存占用 | <10MB(空载) |
CPU占用率 | <1%(空闲状态) |
二、系统架构设计
2.1 守护进程双进程模型
// 主守护进程
pid_t daemon_pid = fork();
if (daemon_pid == 0) {
// 监控进程
while (1) {
pid_t server_pid = fork();
if (server_pid == 0) {
// 业务服务进程
start_server();
exit(EXIT_SUCCESS);
} else {
// 监控服务进程状态
waitpid(server_pid, &status, 0);
sleep(3); // 3秒后重启
}
}
}
2.2 网络服务架构
2.3 关键函数解析
函数 | 用途 |
---|---|
fork() | 创建一个子进程,子进程会复制父进程的地址空间 |
setsid() | 创建一个新的会话,使进程成为无控制终端的会话首进程 |
umask() | 设置文件创建权限掩码,确保守护进程创建的文件权限符合预期 |
dup2() | 将标准输出(stdout)和标准错误(stderr)重定向到日志文件 |
lockf() | 给文件 network_daemon.lock 加锁,防止多个守护进程同时运行 |
syslog() | 向系统日志(/var/log/syslog 或 /var/log/messages )写入日志 |
… | … |
三、完整代码实现
3.1 守护进程核心代码(daemon.c)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/syslog.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <signal.h>
#include <syslog.h>
#include <stdbool.h>
#define LOCK_FILE "/var/run/network_daemon.lock" // 进程锁文件,防止重复运行
#define SERVER_FILE "/home/szsoft02/gooosunwy/TaiShanPai/LinuxSDK/1_code_test/socket/daemon/server" // 服务端执行文件路径
static int create_lock(void);
static void signal_handler(int sig);
static int lockfd = -1; // 锁文件描述符
static pid_t spid ; // 服务进程PID
bool is_shutdown = false; // 是否关闭守护进程
// 守护进程初始化
static void daemon_init(void)
{
pid_t pid = fork(); // 创建子进程
if (pid < 0) exit(EXIT_FAILURE); // fork失败退出
if (pid > 0) exit(EXIT_SUCCESS); // 父进程退出,子进程成为孤儿进程
// 创建新会话,使子进程成为会话首进程,脱离终端
if (setsid() < 0) exit(EXIT_FAILURE);
// 第二次fork(),防止守护进程重新获取终端
if(((pid=fork()) < 0)){
exit(EXIT_FAILURE);
} else if (pid > 0) {
exit(EXIT_SUCCESS);
}
// 设置文件权限掩码,确保守护进程创建的文件有合适的权限
// 这里不屏蔽任何权限,所有用户都可以修改新文件,按需设置
umask(0);
// 修改工作目录为根目录
if(chdir("/") < 0) exit(EXIT_FAILURE);
// 关闭所有打开的文件描述符
for (int x = 0; x <= sysconf(_SC_OPEN_MAX); x++){
close(x);
}
openlog("daemon_server", LOG_PID, LOG_DAEMON);
// 单例检测放在两次fork后才可起作用
if (create_lock() != 0) {
syslog(LOG_ERR, "守护进程已在运行,退出...");
exit(EXIT_FAILURE);
}
// 注册信号处理
signal(SIGTERM, signal_handler); // 终止信号
signal(SIGCHLD, SIG_IGN); // 避免子进程成为僵尸进程
syslog(LOG_INFO, "守护进程启动成功, PID=%d", getpid());
}
// 创建锁文件, 防止多个守护进程实例同时运行
static int create_lock(void)
{
lockfd = open(LOCK_FILE, O_RDWR|O_CREAT, 0640);
if (lockfd < 0) return -1; // 打开失败,可能是无权限
// 尝试锁定文件,如果失败则说明已有实例在运行
if (lockf(lockfd, F_TLOCK, 0) < 0) return -1;
// 写入当前进程PID到锁文件
char str[16];
sprintf(str, "%d\n", getpid());
write(lockfd, str, strlen(str));
syslog(LOG_INFO, "守护进程首次运行");
return 0;
}
// 信号处理
static void signal_handler(int sig)
{
syslog(LOG_INFO, "收到信号: %d", sig);
if (sig == SIGTERM) { // 收到终止信号
syslog(LOG_INFO, "守护进程退出");
// 释放 PID 文件锁
if (lockfd != -1) {
close(lockfd);
unlink(LOCK_FILE);
}
// 终止服务进程
is_shutdown = true;
kill(spid, SIGTERM);
exit(EXIT_SUCCESS);
}
}
// 服务进程监控
void monitor_loop(void)
{
while (1) {
spid = fork();
if (spid == 0) { // 子进程运行实际服务
// 执行服务端程序
syslog(LOG_INFO, "服务端进程启动...");
char *argv[] = {"daemon_server", NULL};
execve(SERVER_FILE, argv, NULL);
syslog(LOG_ERR, "服务端进程启动失败");
exit(EXIT_FAILURE);
} else if (spid > 0) { // 父进程监控
int status;
waitpid(spid, &status, 0); // 等待子进程退出
if (is_shutdown) {
syslog(LOG_NOTICE, "子进程已被回收,即将关闭syslog连接,守护进程退出");
closelog();
exit(EXIT_SUCCESS);
}
syslog(LOG_WARNING, "服务进程退出, 3秒后重启...");
sleep(3);
} else {
syslog(LOG_ERR, "创建子进程失败");
sleep(5); // 创建失败等待5秒
}
}
}
int main(void)
{
// 初始化守护进程
daemon_init();
monitor_loop(); // 进入服务进程监控循环
syslog(LOG_INFO, "守护进程退出");
closelog(); // 关闭日志
return EXIT_SUCCESS;
}
3.2 网络服务代码(server.c)
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <pthread.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <sys/wait.h>
#include <signal.h>
#include <stdbool.h>
#include <syslog.h>
#define SERVER_SOCKPORT 8888 // 设置服务端端口号
#define BACKLOG 5 // 如果连接数超过 BACKLOG 新连接请求将被拒绝(或延迟处理)
#define MAXLENGTH 256 // 最大字节长度
#define handle_error(msg) \
do { perror(msg); exit(EXIT_FAILURE); } while (0)
int sockfd;
static void *read_from_client_handle(void *arg)
{
int client_fd = *(int *)arg;
ssize_t count = 0, send_count = 0;
char *read_buf = NULL;
char *write_buf = NULL;
char log_buf[1024];
memset(log_buf, 0, 1024);
read_buf = malloc(sizeof(char) * 1024);
// 判断内存是否分配成功
if (!read_buf){
sprintf(log_buf, "服务端pid: %d: 读缓存创建异常,断开连接\n", getpid());
syslog(LOG_ERR, "%s", log_buf);
shutdown(client_fd, SHUT_WR);
close(client_fd);
return NULL;
}
// 判断内存是否分配成功
write_buf = malloc(sizeof(char) * 1024);
if (!write_buf){
sprintf(log_buf, "服务端pid: %d: 写缓存创建异常,断开连接\n", getpid());
syslog(LOG_ERR, "%s", log_buf);
free(read_buf);
shutdown(client_fd, SHUT_WR);
close(client_fd);
return NULL;
}
while ((count = recv(client_fd, read_buf, 1024, 0))){
if (count < 0){
syslog(LOG_ERR, "server recv error");
}
sprintf(log_buf, "服务端pid: %d: reveive message from client_fd: %d: %s", getpid(), client_fd, read_buf);
syslog(LOG_INFO, "%s", log_buf);
memset(log_buf, 0, 1024);
sprintf(write_buf, "服务端pid: %d: 已收到数据:%s", getpid(),read_buf);
memset(read_buf, 0, 1024);
send_count = send(client_fd, write_buf, 1024, 0);
}
sprintf(log_buf, "服务端pid: %d: 客户端client_fd: %d请求关闭连接......\n", getpid(), client_fd);
syslog(LOG_NOTICE, "%s", log_buf);
sprintf(write_buf, "服务端pid: %d: receive your shutdown signal\n", getpid());
send_count = send(client_fd, write_buf, 1024, 0);
sprintf(log_buf, "服务端pid: %d: 释放client_fd: %d资源\n", getpid(), client_fd);
syslog(LOG_NOTICE, "%s", log_buf);
shutdown(client_fd, SHUT_WR);
close(client_fd);
free(read_buf);
free(write_buf);
return NULL;
}
/**
* @brief 信号函数,回收所有退出的子进程,避免僵尸进程
*/
static void avoid_zombie(int sig)
{
pid_t pid;
int status;
char buf[1024];
while ((pid = waitpid(-1, &status, WNOHANG)) > 0) {
if(WIFEXITED(status)){
sprintf(buf, "子进程: %d 以 %d 状态正常退出,已被回收\n", pid, WEXITSTATUS(status));
syslog(LOG_INFO, "%s", buf);
}else if(WIFSIGNALED(status)){
sprintf(buf, "子进程: %d 被 %d 信号杀死,已被回收\n", pid, WTERMSIG(status));
syslog(LOG_INFO, "%s", buf);
}else{
sprintf(buf, "子进程: %d 因其它原因退出,已被回收\n", pid);
syslog(LOG_WARNING, "%s", buf);
}
}
}
void sigterm_handler(int sig)
{
syslog(LOG_NOTICE, "服务端接收到守护进程发出的SIGTERM,准备退出...");
syslog(LOG_NOTICE, "释放sockfd");
close(sockfd);
syslog(LOG_NOTICE, "释放syslog连接,服务端进程终止");
closelog();
// 退出
exit(EXIT_SUCCESS);
}
int main(int argc,char *argv[])
{
int res;
struct sockaddr_in server_sockaddr,client_sockaddr;
socklen_t client_sockaddr_len = sizeof(client_sockaddr);
memset(&server_sockaddr, 0, sizeof(server_sockaddr));
memset(&client_sockaddr, 0, sizeof(client_sockaddr));
// 服务端
server_sockaddr.sin_family = AF_INET; // (域名)网络层端点协议:ipv4
server_sockaddr.sin_addr.s_addr = htonl(INADDR_ANY);
server_sockaddr.sin_port = htons(SERVER_SOCKPORT); // 设置服务端端口号
// 客户端网络编程流程
// 1.为通信创建端点
sockfd = socket(AF_INET,SOCK_STREAM,0);
// 2.创建好客户端地址结构,绑定
res = bind(sockfd,(struct sockaddr *)&server_sockaddr,sizeof(server_sockaddr));
// 3.监听
listen(sockfd, BACKLOG);
// 注册信号函数,回收退出的子进程
signal(SIGCLD,avoid_zombie);
// 处理SIGTERM函数
signal(SIGTERM, sigterm_handler);
// 4.多连接服务端
while(1){
int clientfd = accept(sockfd, (struct sockaddr *)&client_sockaddr,&client_sockaddr_len);
pid_t pid = fork(); // 创建一个子进程
if(pid < 0){
perror("fork");
}else if(pid == 0){ // 在子进程内进行收客户端数据
// 子进程不需要处理sockfd,释放文件描述符,使其引用计数减一
close(sockfd);
char buf[1024] = {0};
sprintf(buf, "与客户端 from %s at PORT %d 文件描述符 %d 建立连接\n",
inet_ntoa(client_sockaddr.sin_addr), ntohs(client_sockaddr.sin_port), clientfd);
syslog(LOG_INFO, "%s", buf);
memset(buf, 0, 1024);
sprintf(buf, "新的服务端pid为: %d\n", getpid());
syslog(LOG_INFO, "%s", buf);
read_from_client_handle((void *)&clientfd);
// 释放资源并终止子进程
close(clientfd);
exit(EXIT_SUCCESS);
}else{
// 释放掉父进程不用的资源
close(clientfd);
}
}
printf("释放资源\n");
close(sockfd);
return 0;
}
四、编译与部署
4.1 编译命令
# 工作目录为:/home/szsoft02/gooosunwy/TaiShanPai/LinuxSDK/1_code_test/socket/daemon/
# 这里需要改动为你的工作目录
# 编译守护进程
gcc daemon.c -o daemon
# 编译服务进程 可执行文件名为 server , 若要改动,请把daemon.c代码中的SERVER_FILE修改
gcc server.c -o server
4.2 部署步骤
# 1.建立一个终端用来查看系统日志
sudo tail -F /var/log/syslog
# 2. 启动守护进程-必须使用sudo权限执行,因为创建锁路径为 "/var/run/network_daemon.lock" ,基本都需要sudo权限
# 建立另一个终端
sudo ./daemon
# 3. 查看进程树 或者 普通列举查看进程
pstree -p | grep daemon
或者
ps -ef
# 进程树上能找到 daemon(有个ID)---server(有个ID)
# 普通列举上能找到 daemon 和 daemon_server
# 3. 测试客户端连接
可以自己写一个客户端测试代码,只需要连接本例的端口 SERVER_SOCKPORT (8888) 和本机地址 127.0.0.1
文章最后有完整代码链接
# 4. 测试自动重启
sudo kill <server的ID> # 观察3秒后是否重启
# 5. 彻底停止服务
sudo kill <daemon的ID>
五、关键技术解析
5.1 守护进程核心步骤
5.2 自动重启机制
void monitor_loop() {
while (1) {
pid_t pid = fork();
if (pid == 0) {
// 子进程运行服务
char *argv[] = {"daemon_server", NULL};
execve(SERVER_FILE, argv, NULL);
exit(EXIT_FAILURE);
} else {
// 父进程等待子进程退出
waitpid(pid, &status, 0);
sleep(3); // 等待3秒后重启
}
}
}
5.3 多客户端处理
// 使用fork为每个客户端创建独立进程
while (1) {
client_fd = accept(...);
pid = fork();
if (pid == 0) {
// 子进程处理客户端
handle_client(client_fd);
exit(0);
}
close(client_fd);
}
六、环境增强建议
6.1 安全性增强
# 设置权限(仅允许root操作)
sudo chown root:root daemon
sudo chmod 700 daemon
6.2 资源限制
// 设置最大文件描述符数
struct rlimit lim = {.rlim_cur = 65535, .rlim_max = 65535};
setrlimit(RLIMIT_NOFILE, &lim);
七、常见问题排查
7.1 端口占用错误
# 查看端口占用情况
sudo netstat -tulnp | grep :8888
# 释放端口
sudo fuser -k 8888/tcp
7.2 自动重启失效
# 检查inotify限制
cat /proc/sys/fs/inotify/max_user_instances
# 临时修改限制
echo 65536 | sudo tee /proc/sys/fs/inotify/max_user_instances
7.3 僵尸进程处理
// 添加SIGCHLD处理
signal(SIGCHLD, [](int){
while (waitpid(-1, NULL, WNOHANG) > 0);
});
资源下载:
只取客户端代码连接本地
通过本方案可实现高可靠的网络服务守护进程,满足嵌入式系统对服务稳定性的严苛要求。后续可扩展实现双机热备、配置热加载等高级功能。