Linux之守护进程
在Linux系统中,进程一般分为前台进程、后台进程和守护进程3类。
一 守护进程
定义:
1.守护进程是在操作系统后台运行的一种特殊类型的进程,它独立于前台用户界面,不与任何终端设备直接关联。这些进程通常在系统启动时启动,并持续运行直到系统关闭,或者它们完成其任务并自行终止。守护进程通常用于服务请求、管理系统或执行周期性任务。
2.守护进程是后台运行的一种特殊进程,它通常不与用户直接交互,也不受用户登录或注销的影响。它是为了完成某种特定的任务而运行的,例如提供服务或者监控系统状态。
3.Linux系统的大多数服务器就是通过守护进程实现的。常见的守护进程包括系统日志进程syslogd、 web服务器httpd、邮件服务器sendmail和数据库服务器mysqld等。
4.守护进程通常在系统启动时开始运行,并以超级用户权限运行。它们经常需要访问特殊的资源或使用特殊的端口(1-1024)。守护进程会一直运行直到系统关机,除非被强制终止。它们的父进程是init进程,因为它们的真正父进程在fork出子进程后就先于子进程exit退出了。因此,它们是由init继承的孤儿进程。由于守护进程是非交互式程序且没有控制终端,任何输出都需要特殊处理。通常,守护进程的名称以d结尾,例如sshd、xinetd和crond。
二控制终端
控制终端是与会话关联的终端设备,它是用户输入和输出的通道。进程通常通过其控制终端与用户交互,接收输入和向用户显示输出。对于守护进程来说,它必须从任何控制终端中脱离,确保其独立于任何用户会话在后台运行,这样才能保证其稳定性和安全性,不受用户直接控制和会话结束等事件的影响。
三 编程规则
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.执行具体任务
这一步是守护进程的核心,它开始执行为其设计的特定功能,如监听网络请求、定期清理文件系统、执行系统备份等
四 相关系统调用和库函数
1.setsid
#include <sys/types.h>
#include <unistd.h>
/**
* @brief 如果调用进程不是进程组的领导者,则创建一个新的会话。创建者是新会话的领导者
*
* @return pid_t 成功则返回调用进程的新会话ID,失败则返回(pid_t)-1,并设置errno以指明错误原因
*/
pid_t setsid(void);
2.umask
#include <sys/types.h>
#include <sys/stat.h>
/**
* @brief 设置调用进程的文件模式创建掩码。
*
* @param mask 掩码。是一个八进制数,它指定哪些权限位在文件或目录创建时应被关闭。我们通过umask(0)确保守护进程创建的文件和目录具有最开放的权限设置。
* @return mode_t 这个系统调用必然成功,返回之前的掩码值
*/
mode_t umask(mode_t mask);
3.chdir
#include <unistd.h>
/**
* @brief 更改调用进程的工作目录
*
* @param path 更改后的工作路径
* @return int 成功返回0,失败返回-1,并设置errno
*/
int chdir(const char *path);
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);
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, ...);
6.closelog
#include <syslog.h>
/**
* @brief 关闭用于写入系统日志的文件描述符
*
*/
void closelog(void);
7.sysconf
#include <unistd.h>
/**
* @brief 获取运行时配置信息
*
* @param name 配置名称,取值太多,可以通过 man 3 sysconf 自行查阅,我们只用到_SC_OPEN_MAX,记录了当前进程可以打开的文件描述符的最大数量
* @return long 配置的值
*/
long sysconf(int name);
五.举例
1.创建一个守护进程
//创建一个守护进程#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/stat.h>int main(void)
{pid_t pid;// 步骤一:创建一个新的进程pid = fork();//父进程直接退出if (pid > 0){exit(0);}if (pid == 0){// 步骤二:调用setsid函数摆脱控制终端setsid();// 步骤三:更改工作目录chdir("/");// 步骤四:重新设置umask文件源码umask(0);// 步骤五:0 1 2 三个文件描述符for (int i = 1; i < 4; i++){close(i);}while (1){}}return 0;
}
2 创建daemon_test.c
//创建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;/*** @brief 信号处理函数* * 该函数根据接收到的信号类型执行不同的操作。它主要用于处理SIGHUP和SIGTERM信号,* 并在接收到未处理的信号时记录信息。当接收到SIGTERM信号时,它会设置一个全局标志以启动退出流程,* 并向某个特定的子进程发送SIGTERM信号要求其终止。* * @param sig 接收到的信号编号,用于决定执行哪种处理逻辑*/
void signal_handler(int sig)
{switch (sig){case SIGHUP:// 当接收到SIGHUP信号时,记录警告信息syslog(LOG_WARNING, "收到SIGHUP信号...");break;case SIGTERM:// 当接收到SIGTERM信号时,记录通知信息并启动退出流程syslog(LOG_NOTICE, "接收到终止信号,准备退出守护进程...");// 记录发送终止信号给子进程的操作syslog(LOG_NOTICE, "向子进程发送SIGTERM信号...");// 设置全局变量以启动退出流程is_shutdown = 1;// 向指定的子进程发送SIGTERM信号kill(pid, SIGTERM);break;default:// 当接收到未处理的信号时,记录信息syslog(LOG_INFO, "Received unhandled signal");}
}/*** 将当前进程转化为守护进程的函数*/
void my_daemonize()
{pid_t pid;// fork() 调用会在父进程中返回子进程的 PID,在子进程中返回 0。如果发生错误,则返回 -1。//1.守护进程第一步创建子进程,并结束父进程pid = fork();if (pid < 0)exit(EXIT_FAILURE);if (pid > 0)exit(EXIT_SUCCESS);//2设置会话IDif (setsid() < 0)exit(EXIT_FAILURE);// 处理 SIGHUP、SIGTERM 信号signal(SIGHUP, signal_handler);signal(SIGTERM, signal_handler);//3 第二次fork()pid = fork();if (pid < 0)exit(EXIT_FAILURE);if (pid > 0)exit(EXIT_SUCCESS);// 4.重置umask,调用umask(0)确保守护进程创建的文件权限不受继承的umask值的影响,守护进程可以更精确地控制其创建的文件和目录的权限umask(0);// 5.将工作目录切换为根目录,chdir("/");// 6.关闭所有打开的文件描述符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函数来替换当前进程的执行程序* * @param path 指向新程序的路径字符串* @param argv 指向参数的指针数组,这些参数将传递给新程序* @param NULL 表示环境变量的数组为空* * 注意:这个函数不会返回错误码,因为如果新程序成功启动,当前进程的执行流将被新程序取代*/execve(path, argv, NULL);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;
}
3. 创建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>// 声明服务器的socket文件描述符,用于监听客户端连接
int sockfd;/*** 处理僵尸进程的信号处理函数* 当接收到SIGCHLD信号时,调用本函数回收退出的子进程*/
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);}}
}/*** 处理SIGTERM信号的函数* 当服务端接收到SIGTERM信号时,调用本函数进行清理工作并退出*/
void sigterm_handler(int sig) {syslog(LOG_NOTICE, "服务端接收到守护进程发出的SIGTERM,准备退出...");syslog(LOG_NOTICE, "释放sockfd");close(sockfd);syslog(LOG_NOTICE, "释放syslog连接,服务端进程终止");closelog();// 退出exit(EXIT_SUCCESS);
}/*** 从客户端读取数据然后写回的函数* @param argv 客户端的文件描述符*/
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;
}/*** 主函数* 负责初始化服务器socket,绑定端口,监听客户端连接* 并为每个客户端连接创建一个子进程进行处理*/
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 socketsockfd = 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);// 读取客户端数据,并打印到 stdoutread_from_client_then_write((void *)&client_fd);// 释放资源并终止子进程close(client_fd);exit(EXIT_SUCCESS);}}return 0;
}
4.创建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; \}/*** 从服务器读取数据的线程函数* @param argv 线程参数,这里是一个指向socket文件描述符的指针* @return NULL*/
/*
/*
该函数是用于多线程编程中,作为线程的入口函数。函数名为 read_from_server,是一个返回值为 void* 类型的函数,接受一个 void* 类型参数(通常用于传递线程所需数据)。
常用于创建线程时指定的执行函数,实现从服务器读取数据的功能。
*/
void *read_from_server(void *argv)
{/*这段代码将 `argv` 指针指向的内容解释为一个整型指针,并将其解引用后赋值给 `sockfd`。 通俗来说,就是从 `argv` 中取出一个整数作为套接字文件描述符。 简要说明:
- `argv` 是一个 `void*` 类型的指针(通常用于传递参数)
- `(int *)argv` 将其转换为整型指针
- `*(int *)argv` 取出该指针指向位置的整数值
- `sockfd` 保存了这个值,表示一个 socket 文件描述符*/int sockfd = *(int *)argv;/*这段代码定义了一个名为 `read_buf` 的字符指针变量,并将其初始化为 `NULL`。表示该指针当前不指向任何有效的内存空间,常用于后续动态分配内存或作为缓冲区读取字符串数据前的准备工作。*/char *read_buf = NULL;ssize_t count = 0;/*这段代码动态分配了一个大小为 1024 字节的内存空间,用于存储字符数据。- `malloc(sizeof(char) * 1024)`:申请 1024 个 `char` 类型大小的连续内存空间;
- `read_buf =`:将分配到的内存首地址赋值给指针 `read_buf`。*/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;
}/*** 向服务器写入数据的线程函数* @param argv 线程参数,这里是一个指向socket文件描述符的指针* @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");/*```c
shutdown(sockfd, SHUT_WR);
```**功能解释:**该函数调用关闭套接字 `sockfd` 的写入端,表示本端不再发送数据,但仍可接收数据。 - `SHUT_WR`:表示关闭写操作
- `sockfd`:是待操作的套接字描述符*/shutdown(sockfd, SHUT_WR);perror("malloc client write_buf");return NULL;}/*这段代码的功能是:
从标准输入(如键盘)读取一行文本,最多读取 `1023` 个字符(加上自动添加的 `\0`),并存储到 `write_buf` 指向的缓冲区中。 简要说明如下:
- `fgets` 是用于读取字符串的标准库函数
- `write_buf` 是目标缓冲区
- `1024` 表示最大读取长度(包括结束符 `\0`)
- `stdin` 表示输入来源为标准输入流*/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;
}/*** 主函数* @param argc 命令行参数个数* @param argv 命令行参数列表* @return 程序退出状态*/
/*
这段代码是C语言程序的主函数定义,表示程序的入口点。 - `int main(int argc, char const *argv[])` 表示主函数带有两个参数: - `argc` 是命令行参数的数量(argument count) - `argv` 是命令行参数的数组(argument vector),每个元素是一个字符串
- 函数返回一个整型值,通常用于表示程序退出状态(0 表示成功,非0表示错误)
*/
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.1server_addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);// 连接端口 6666server_addr.sin_port = htons(6666);// 创建socketsockfd = socket(AF_INET, SOCK_STREAM, 0);handle_error("socket", sockfd);// 连接servertemp_result = connect(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr));handle_error("connect", temp_result);// 启动一个子线程,用来读取服务端数据,并打印到 stdoutpthread_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;
}