六十一、linux 编程—— 守护进程
61.1 介绍
- 守护进程(daemon)是生存期长的一种进程。它们常常在系统引导装入时启动,在系统关闭时终止
- 守护进程也称为后台进程
- 所有守护进程都以超级用户(用户 ID 为0)的优先权运行。
- 守护进程没有控制终端
- 守护进程的父进程都是 init 进程
62.2 例子
62.2.1 编程步骤
- 使用 umask 将文件模式创建屏蔽字设置为0
- 调用 fork ,然后让父进程退出(exit)
- 调用 setsid 创建一个新会话
- 将当前工作目录更改为根目录
- 关闭不需要的文件描述符
62.2.2 守护进程出错处理
- 由于守护进程完全脱离了控制终端,因此,不能像其他程序一样通过输出错误信息到控制台的方式来通知程序员
- 通常办法是使用 syslog 服务,将出错信息输出到"/var/log/syslog" 系统日志文件中去
- syslog 是 linux 中的系统日志管理服务,通过守护进程 syslog 来维护
- openlog 函数用于打开系统日志服务的一个连接
- syslog 函数用于向日志文件中写入信息,在这里可以规定消息的优先级、消息的输出格式等
- closelog 函数用于关闭系统日志服务的连接
62.2.3 函数原型
61.2.4 例子
echo_tcp_server_daemon.c
1 #include <netdb.h> 2 #include <netinet/in.h> 3 #include <sys/socket.h> 4 #include <sys/wait.h> 5 #include <unistd.h> 6 #include <string.h> 7 #include <stdio.h> 8 #include <stdlib.h> 9 #include <memory.h> 10 #include <signal.h> 11 #include <fcntl.h> 12 #include <time.h> 13 #include <arpa/inet.h> 14 #include <errno.h> 15 #include <pthread.h> 16 #include <syslog.h> 17 #include <sys/types.h> 18 #include <sys/stat.h> 19 #include "vector_fd.h" 20 21 vector_fd *vfd; 22 int sockfd; 23 24 /** 25 * fd 对应于某个连接的客户端,和某一个连接的客户端进行双向通信(非阻塞方式) 26 */ 27 void do_service(int fd) 28 { 29 char buff[512]; 30 memset(buff, 0, sizeof(buff)); 31 32 /** 33 * 因为采用非阻塞方式,若读不到数据直接返回, 34 * 直接服务于下一个客户端, 35 * 因此不需要判断 size < 0 的情况 */ 36 ssize_t size = read(fd, buff, sizeof(buff)); 37 38 if(size == 0){ 39 /** 客户端已经关闭连接 */ 40 syslog(LOG_DEBUG, "client closed"); 41 /** 从动态数组中删除对应的 fd */ 42 remove_fd(vfd, fd); 43 /** 关闭对应客户端的 socket */ 44 close(fd); 45 } 46 else if(size > 0){ 47 syslog(LOG_DEBUG, "%s", buff); 48 if(write(fd, buff, size) < 0){ 49 if(errno == EPIPE){ 50 /** 客户端关闭连接 */ 51 syslog(LOG_DEBUG, "write error:%s\n", strerror(errno)); 52 remove_fd(vfd, fd); 53 close(fd); 54 } 55 } 56 } 57 } 58 59 void out_addr(struct sockaddr_in *clientaddr) 60 { 61 char ip[16]; 62 memset(ip, 0, sizeof(ip)); 63 int port = ntohs(clientaddr->sin_port); 64 inet_ntop(AF_INET, &clientaddr->sin_addr.s_addr, ip, sizeof(ip)); 65 syslog(LOG_DEBUG, "%s(%d) connected!\n", ip, port); 66 } 67 68 /** 遍历出动态数组中所有的描述符并加入到描述符集 set 69 * 中,同时此函数返回动态数组中最大的那个描述符 */ 70 int add_set(fd_set *set) 71 { 72 FD_ZERO(set); ///< 清空描述符集 73 int max_fd = vfd->fd[0]; 74 int i = 0; 75 for(; i < vfd->counter; i++){ 76 int fd = get_fd(vfd, i); 77 if(fd > max_fd) max_fd = fd; 78 FD_SET(fd, set); ///< 将 fd 加入到描述符集中 79 } 80 81 return max_fd; 82 } 83 84 void *th_fn(void *arg) 85 { 86 /** 设置超时时间 2s */ 87 struct timeval t; 88 t.tv_sec = 2; 89 t.tv_usec = 0; 90 91 int n = 0; 92 int maxfd; 93 fd_set set; ///< 描述符集 94 maxfd = add_set(&set); 95 96 /** 97 * 调用 select 函数会阻塞,委托内核去检查传入的描述符是否准备好, 98 * 若有则返回准备好的描述符;超时则返回 0 99 * 第一个参数为描述符集中的描述符的范围(最大描述符 + 1) 100 */ 101 while((n = select(maxfd + 1, &set, NULL, NULL, &t)) >= 0){ 102 /** 检测哪些描述符准备好, 并和这些准备好的描述符对应的客户端进行数据的双向通信 */ 103 if(n > 0){ 104 int i = 0; 105 for(; i < vfd->counter; i++){ 106 int fd = get_fd(vfd, i); 107 if(FD_ISSET(fd, &set)){ 108 do_service(fd); 109 } 110 } 111 } 112 /** 重新设置时间和清空描述符集 */ 113 t.tv_sec = 2; 114 t.tv_usec = 0; 115 /** 重新遍历动态数组中最新的描述符放置到描述符集中 */ 116 maxfd = add_set(&set); 117 } 118 119 return (void *)0; 120 } 121 122 int main(int argc, char *argv[]) 123 { 124 if(argc < 2){ 125 printf("usage: %s #port\n", argv[0]); 126 exit(1); 127 } 128 129 /** 守护进程编程的5个步骤 */ 130 /** 步骤1: 创建屏蔽字为 0 */ 131 umask(0); 132 /** 步骤2: 调用 fork 函数创建子进程,然后父进程退出 */ 133 pid_t pid = fork(); 134 if(pid > 0) exit(0); 135 /** 步骤3: 调用 setsid 函数创建一个新会话 */ 136 setsid(); 137 /** 步骤4: 将当前工作目录更改为更目录 */ 138 chdir("/"); 139 /** 步骤5: 关闭不需要的文件描述符 */ 140 close(STDIN_FILENO); 141 close(STDOUT_FILENO); 142 close(STDERR_FILENO); 143 144 /** 打开系统日志服务的一个连接 */ 145 openlog(argv[0], LOG_PID, LOG_SYSLOG); 146 147 148 /** 步骤1: 创建 socket(套接字) 149 * 注: socket 创建在内核中,是一个结构体. 150 * AF_INET: IPV4 151 * SOCK_STREAM: tcp 协议 152 * AF_INET6: IPV6 153 */ 154 sockfd = socket(AF_INET, SOCK_STREAM, 0); 155 if(sockfd < 0){ 156 /** 将日志信息写入到系统日志文件中(/var/log/syslog) */ 157 syslog(LOG_DEBUG, "socket:%s\n", strerror(errno)); 158 exit(1); 159 } 160 161 /** 162 * 步骤2: 调用 bind 函数将 socket 和地址(包括 ip、port)进行绑定 163 */ 164 struct sockaddr_in serveraddr; 165 memset(&serveraddr, 0, sizeof(struct sockaddr_in)); 166 /** 往地址中填入 ip、port、internet 地址族类型 */ 167 serveraddr.sin_family = AF_INET; ///< IPV4 168 serveraddr.sin_port = htons(atoi(argv[1])); ///< 填入端口 169 serveraddr.sin_addr.s_addr = INADDR_ANY; ///< 填入 IP 地址 170 if(bind(sockfd, (struct sockaddr *)&serveraddr, sizeof(struct sockaddr_in))){ 171 syslog(LOG_DEBUG, "bind:%s\n", strerror(errno)); 172 exit(1); 173 } 174 175 /** 176 * 步骤3: 调用 listen 函数启动监听(指定 port 监听) 177 * 通知系统去接受来自客户端的连接请求 178 * (将接受到的客户端连接请求放置到对应的队列中) 179 * 第二个参数: 指定队列的长度 180 */ 181 if(listen(sockfd, 10) < 0){ 182 syslog(LOG_DEBUG, "listen:%s\n", strerror(errno)); 183 exit(1); 184 } 185 186 /** 创建放置套接字描述符 fd 的动态数组 */ 187 vfd = create_vector_fd(); 188 189 /** 设置线程的分离属性 */ 190 pthread_t th; 191 pthread_attr_t attr; 192 pthread_attr_init(&attr); 193 pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); 194 int err; 195 if((err = pthread_create(&th, &attr, th_fn, (void *)0)) != 0){ 196 syslog(LOG_DEBUG, "pthread create:%s\n", strerror(errno)); 197 exit(1); 198 } 199 pthread_attr_destroy(&attr); 200 201 /** 202 * 1)主控线程获得客户端的链接,将新的 socket 描述符放置到动态数组中 203 * 2) (a)启动的子线程调用 select 函数委托内核去检查传入到 select 204 * 中的描述符是否准备好. 205 * (b)利用 FD_ISSET 来找出准备好的那些描述符, 206 * 并和对应的客户端进行双向通信(非阻塞) 207 */ 208 struct sockaddr_in clientaddr; 209 socklen_t len = sizeof(clientaddr); 210 211 while(1){ 212 /** 213 * 步骤4: 调用 accept 函数从队列中获得一个客户端的请求连接, 并返回新的 214 * socket 描述符 215 * 注意: 若没有客户端连接,调用此函数后会阻塞, 直到获得一个客户端的连接 216 */ 217 /** 主控线程负责调用 accept 去获得客户端的连接 */ 218 int fd = accept(sockfd, (struct sockaddr *)&clientaddr, &len); 219 if(fd < 0){ 220 syslog(LOG_DEBUG, "accept:%s\n", strerror(errno)); 221 continue; 222 } 223 224 out_addr(&clientaddr); 225 226 /** 将返回的新的 socket 描述符加入到动态数组中 */ 227 add_fd(vfd, fd); 228 } 229 closelog(); 230 231 return 0; 232 }
编译运行如下:
查看 /var/log/syslog/