目录
1.定义
守护进程是在操作系统后台运行的一种特殊类型的进程,它独立于前台用户界面,不与任何终端设备直接关联。这些进程通常在系统启动时启动,并持续运行直到系统关闭,或者它们完成其任务并自行终止。守护进程通常用于服务请求、管理系统或执行周期性任务。
2.控制终端
控制终端是与会话关联的终端设备,它是用户输入和输出的通道。进程通常通过其控制终端与用户交互,接收输入和向用户显示输出。对于守护进程来说,它必须从任何控制终端中脱离,确保其独立于任何用户会话在后台运行,这样才能保证其稳定性和安全性,不受用户直接控制和会话结束等事件的影响。
3.编程规则
步骤如下:
1.创建子进程并结束父进程
在UNIX和类UNIX系统中,进程是通过复制(使用fork())创建的。守护进程需要在后台独立运行。
2.设置会话ID
setsid()创建一个新会话,并使调用它的进程成为新会话的领导者,这样做的主要目的是让守护进程摆脱原来的控制终端。这样,守护进程就不会接收到终端发出的任何信号,例如挂断信号(SIGHUP),从而保证其运行不受前台用户操作的影响。
3.第二次fork()
使得守护进程不是会话领导,没有获取控制终端的能力,避免意外获取控制终端。
4.更改工作目录
将工作目录更改到根目录(/),主要是为了避免守护进程继续占用其启动时的文件系统。这对于可移动的或网络挂载的文件系统尤其重要,确保这些文件系统不需要时可以被卸载。
5.重设文件权限掩码
调用umask(0)确保守护进程创建的文件权限不受继承的umask值的影响,守护进程可以更精确地控制其创建的文件和目录的权限。
6.关闭文件描述符
守护进程通常不需要标准输入、输出和错误文件描述符,因为它们不与终端交互。关闭这些不需要的文件描述符可以避免资源泄露,提高守护进程的安全性和效率。
7.处理信号
SIGHUP 和 SIGTERM 信号。
SIGHUP:虽然守护进程和终端断开,但仍然有可能收到其它进程或内核发来的SIGHUP信号,守护进程不应该因为它而终止。
SIGTERM:SIGTERM信号是终止信号,用于请求守护进程优雅地终止。通过命令行执行kill <pid>命令可以发送SIGTERM信号,接收到这个信号之后,守护进程终止子进程,并清理回收资源,最后退出。
8.执行具体任务
这一步是守护进程的核心,它开始执行为其设计的特定功能,如监听网络请求、定期清理文件系统、执行系统备份等。
4.相关函数
4.1 setsid
#include <sys/types.h>
#include <unistd.h>
/**
* @brief 如果调用进程不是进程组的领导者,则创建一个新的会话。创建者是新会话的领导者
*
* @return pid_t 成功则返回调用进程的新会话ID,失败则返回(pid_t)-1,并设置errno以指明错误原因
*/
pid_t setsid(void);
4.2 umask
#include <sys/types.h>
#include <sys/stat.h>
/**
* @brief 设置调用进程的文件模式创建掩码。
*
* @param mask 掩码。是一个八进制数,它指定哪些权限位在文件或目录创建时应被关闭。我们通过umask(0)确保守护进程创建的文件和目录具有最开放的权限设置。
* @return mode_t 这个系统调用必然成功,返回之前的掩码值
*/
mode_t umask(mode_t mask);
4.3 chdir
#include <unistd.h>
/**
* @brief 更改调用进程的工作目录
*
* @param path 更改后的工作路径
* @return int 成功返回0,失败返回-1,并设置errno
*/
int chdir(const char *path);
4.4 openlog
#include <syslog.h>
/**
* @brief 为程序开启一个面向系统日志的连接
*
* @param ident 每条消息的字符串前缀,按照惯例通常设置为程序名称
* @param option option:指定控制 openlog 和后续 syslog 调用的标志。常见标志包括:
* LOG_PID:在每条日志消息中包含进程ID。
* LOG_CONS:如果无法将消息发送到日志守护进程,则直接将消息写入控制台。
* LOG_NDELAY:立即打开与系统日志守护进程的连接。
* LOG_ODELAY:延迟打开与系统日志守护进程的连接,直到实际写入日志时。
* LOG_PERROR:将日志消息同时输出到标准错误输出。
* @param facility facility:指定日志消息的来源类别,用于区分系统不同部分的日志消息。包括:
* LOG_AUTH:认证系统
* LOG_CRON:cron 和 at 调度守护进程
* LOG_DAEMON:系统守护进程
* LOG_KERN:内核消息
* LOG_LOCAL0 至 LOG_LOCAL7:本地使用
* LOG_MAIL:邮件系统
* LOG_SYSLOG:syslog 自身的消息
* LOG_USER:用户进程
* LOG_UUCP:UUCP 子系统
*/
void openlog(const char *ident, int option, int facility);
4.5 syslog
#include <syslog.h>
/**
* @brief 生成一条日志消息
*
* @param priority 由一个facility和一个level值或操作得到,如果未指定facility,则使用openlog指定的默认值,如果上文没有调用openlog(),则将使用默认值LOG_USER。level取值如下
* LOG_EMERG(系统无法使用)表示系统已经不可用,通常用于严重的紧急情况。例如:系统崩溃或关键硬件故障。
* LOG_ALERT(必须立即采取行动)表示必须立即采取措施解决的问题。例如:磁盘空间用尽或数据库崩溃。
* LOG_CRIT(严重条件)表示严重的错误或问题,但不需要立即采取行动。例如:应用程序的某个重要功能失败。
* LOG_ERR(错误条件)表示一般错误情况,需要注意和修复。例如:无法打开文件或网络连接失败。
* LOG_WARNING(警告条件)表示潜在问题或警告,建议检查,但不会立即影响系统功能。例如:磁盘空间接近用尽或配置文件缺失。
* LOG_NOTICE(正常但重要的情况)表示正常运行过程中需要特别注意的事件。例如:系统启动或关闭成功。
* LOG_INFO(信息性消息)表示一般信息,用于记录正常操作的事件。例如:用户登录或定时任务完成。
* LOG_DEBUG(调试级别消息)表示详细的调试信息,通常用于开发和调试阶段。例如:函数调用跟踪或变量值变化。
* @param format 类似于printf()的格式化字符串
* @param ... 可变参数,可以传递给格式化字符串
*/
void syslog(int priority, const char *format, ...);
4.6 closelog
#include <syslog.h>
/**
* @brief 关闭用于写入系统日志的文件描述符
*
*/
void closelog(void);
4.7 sysconf
#include <unistd.h>
/**
* @brief 获取运行时配置信息
*
* @param name 配置名称,取值太多,可以通过 man 3 sysconf 自行查阅,我们只用到_SC_OPEN_MAX,记录了当前进程可以打开的文件描述符的最大数量
* @return long 配置的值
*/
long sysconf(int name);
5.示例程序
5.1 daemon_test.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <syslog.h>
#include <string.h>
#include <fcntl.h>
#include <signal.h>
#include <string.h>
#include <errno.h>
pid_t pid;
int is_shutdown = 0;
void signal_handler(int sig)
{
switch (sig)
{
case SIGHUP:
syslog(LOG_WARNING, "收到SIGHUP信号...");
break;
case SIGTERM:
syslog(LOG_NOTICE, "接收到终止信号,准备退出守护进程...");
syslog(LOG_NOTICE, "向子进程发送SIGTERM信号...");
is_shutdown = 1;
kill(pid, SIGTERM);
break;
default:
syslog(LOG_INFO, "Received unhandled signal");
}
}
void my_daemonize()
{
pid_t pid;
// Fork off the parent process
pid = fork();
if (pid < 0)
exit(EXIT_FAILURE);
if (pid > 0)
exit(EXIT_SUCCESS);
if (setsid() < 0)
exit(EXIT_FAILURE);
// 处理 SIGHUP、SIGTERM 信号
signal(SIGHUP, signal_handler);
signal(SIGTERM, signal_handler);
pid = fork();
if (pid < 0)
exit(EXIT_FAILURE);
if (pid > 0)
exit(EXIT_SUCCESS);
// 重置umask
umask(0);
// 将工作目录切换为根目录
chdir("/");
// 关闭所有打开的文件描述符
for (int x = 0; x <= sysconf(_SC_OPEN_MAX); x++)
{
close(x);
}
openlog("this is our daemonize process: ", LOG_PID, LOG_DAEMON);
}
int main()
{
my_daemonize();
while (1)
{
pid = fork();
if (pid > 0)
{
syslog(LOG_INFO, "守护进程正在监听服务端进程...");
waitpid(-1, NULL, 0);
if (is_shutdown) {
syslog(LOG_NOTICE, "子进程已被回收,即将关闭syslog连接,守护进程退出");
closelog();
exit(EXIT_SUCCESS);
}
syslog(LOG_ERR, "服务端进程终止,3s后重启...");
sleep(3);
}
else if (pid == 0)
{
syslog(LOG_INFO, "子进程fork成功");
syslog(LOG_INFO, "启动服务端进程");
char *path = "/home/atguigu/daemon_and_multiplex/tcp_server";
char *argv[] = {"my_tcp_server", NULL};
errno = 0;
execve(path, argv, NULL);
char buf[1024];
sprintf(buf, "errno: %d", errno);
syslog(LOG_ERR, "%s", buf);
syslog(LOG_ERR, "服务端进程启动失败");
exit(EXIT_FAILURE);
}
else
{
syslog(LOG_ERR, "子进程fork失败");
}
}
return EXIT_SUCCESS;
}
5.2 tcp_server.c
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#include <pthread.h>
#include <unistd.h>
#include <signal.h>
#include <syslog.h>
int sockfd;
void zombie_dealer(int sig)
{
pid_t pid;
int status;
char buf[1024];
memset(buf, 0, 1024);
// 一个SIGCHLD可能对应多个子进程的退出
// 使用while循环回收所有退出的子进程,避免僵尸进程的出现
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);
}
void read_from_client_then_write(void *argv)
{
int client_fd = *(int *)argv;
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;
}
// 判断内存是否分配成功
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;
}
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: reveived~\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);
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;
}
int main(int argc, char const *argv[])
{
int temp_result;
struct sockaddr_in server_addr, client_addr;
memset(&server_addr, 0, sizeof(server_addr));
memset(&client_addr, 0, sizeof(client_addr));
// 声明IPV4通信协议
server_addr.sin_family = AF_INET;
// 我们需要绑定0.0.0.0地址,转换成网络字节序后完成设置
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
// 端口随便用一个,但是不要用特权端口
server_addr.sin_port = htons(6666);
// 创建server socket
sockfd = socket(AF_INET, SOCK_STREAM, 0);
// 绑定地址
temp_result = bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr));
// 进入监听模式
temp_result = listen(sockfd, 128);
socklen_t cliaddr_len = sizeof(client_addr);
// 注册信号处理函数,处理SIGCHLD信号,避免僵尸进程出现
signal(SIGCHLD, zombie_dealer);
// 处理SIGTERM函数,以优雅退出
signal(SIGTERM, sigterm_handler);
char log_buf[1024];
memset(log_buf, 0, 1024);
// 接受client连接
while (1)
{
int client_fd = accept(sockfd, (struct sockaddr *)&client_addr, &cliaddr_len);
pid_t pid = fork();
if (pid > 0)
{
sprintf(log_buf, "this is father, pid is %d, continue accepting...\n", getpid());
syslog(LOG_INFO, "%s", log_buf);
memset(log_buf, 0, 1024);
// 父进程不需要处理client_fd,释放文件描述符,使其引用计数减一,以便子进程释放client_fd后,其引用计数可以减为0,从而释放资源
close(client_fd);
}
else if (pid == 0)
{
// 子进程不需要处理sockfd,释放文件描述符,使其引用计数减一
close(sockfd);
sprintf(log_buf, "与客户端 from %s at PORT %d 文件描述符 %d 建立连接\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port), client_fd);
syslog(LOG_INFO, "%s", log_buf);
memset(log_buf, 0, 1024);
sprintf(log_buf, "新的服务端pid为: %d\n", getpid());
syslog(LOG_INFO, "%s", log_buf);
memset(log_buf, 0, 1024);
// 读取客户端数据,并打印到 stdout
read_from_client_then_write((void *)&client_fd);
// 释放资源并终止子进程
close(client_fd);
exit(EXIT_SUCCESS);
}
}
return 0;
}
5.3 tcp_client.c
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <pthread.h>
#define handle_error(cmd, result) \
if (result < 0) \
{ \
perror(cmd); \
return -1; \
}
void *read_from_server(void *argv)
{
int sockfd = *(int *)argv;
char *read_buf = NULL;
ssize_t count = 0;
read_buf = malloc(sizeof(char) * 1024);
if (!read_buf)
{
perror("malloc client read_buf");
return NULL;
}
while (count = recv(sockfd, read_buf, 1024, 0))
{
if (count < 0)
{
perror("recv");
}
fputs(read_buf, stdout);
}
printf("收到服务端的终止信号......\n");
free(read_buf);
return NULL;
}
void *write_to_server(void *argv)
{
int sockfd = *(int *)argv;
char *write_buf = NULL;
ssize_t send_count;
write_buf = malloc(sizeof(char) * 1024);
if (!write_buf)
{
printf("写缓存申请异常,断开连接\n");
shutdown(sockfd, SHUT_WR);
perror("malloc client write_buf");
return NULL;
}
while (fgets(write_buf, 1024, stdin) != NULL)
{
send(sockfd, write_buf, 1024, 0);
if (send_count < 0)
{
perror("send");
}
}
printf("接收到命令行的终止信号,不再写入,关闭连接......\n");
shutdown(sockfd, SHUT_WR);
free(write_buf);
return NULL;
}
int main(int argc, char const *argv[])
{
int sockfd, temp_result;
pthread_t pid_read, pid_write;
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
// 连接本机 127.0.0.1
server_addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
// 连接端口 6666
server_addr.sin_port = htons(6666);
// 创建socket
sockfd = socket(AF_INET, SOCK_STREAM, 0);
handle_error("socket", sockfd);
// 连接server
temp_result = connect(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr));
handle_error("connect", temp_result);
// 启动一个子线程,用来读取服务端数据,并打印到 stdout
pthread_create(&pid_read, NULL, read_from_server, (void *)&sockfd);
// 启动一个子线程,用来从命令行读取数据并发送到服务端
pthread_create(&pid_write, NULL, write_to_server, (void *)&sockfd);
// 主线程等待子线程退出
pthread_join(pid_read, NULL);
pthread_join(pid_write, NULL);
printf("关闭资源\n");
close(sockfd);
return 0;
}
5.4 效果
输入 tail -F /var/log/syslog 监控系统日志文件,运行程序可看到日志输出

输入 ps -ef 查看进程,运行服务端后再运行客户端,可同时运行多个客户端发送数据给服务端,
客户端运行 ctrl + d 断开与服务端的连接,使用 kill 杀掉服务端,3s后会重启

kill 杀掉守护进程

1058

被折叠的 条评论
为什么被折叠?



