FastCGI(FCGI)是一种让交互程序与Web服务器通信的协议,是CGI(Common Gateway Interface)的增强版本。FCGI进程可以常驻内存,处理多个请求,避免了CGI每次请求都需要创建新进程的开销。本文将详细介绍一个FCGI常驻服务程序的设计与实现,包括FCGI初始化、守护进程模式、服务启动和停止等关键环节。
项目源码:https://gitcode.com/embeddedPrj/webserver/tree/main/src/fcgiServer
FCGI服务器的初始化主要包括以下步骤:
FCGX_Init();
这个函数初始化FCGI库,为后续操作做准备。
// 移除可能存在的旧套接字文件
unlink(FCGI_SOCKET_PATH);
// 创建新的套接字
cgi_sock = FCGX_OpenSocket(FCGI_SOCKET_PATH, 512);
if(cgi_sock < 0) {
log_info("open FCGX socket failed\n");
return -1;
}
// 设置套接字文件权限,确保Web服务器(如Nginx)可以访问
if (chmod(FCGI_SOCKET_PATH, 0666) < 0) {
log_info("chmod socket file failed\n");
close(cgi_sock);
unlink(FCGI_SOCKET_PATH);
return -1;
}
这段代码创建了一个FCGI套接字,用于与Web服务器通信。FCGI_SOCKET_PATH
定义了套接字文件的路径,512
是连接队列的大小。创建套接字后,通过chmod
设置适当的权限,确保Web服务器可以访问该套接字。
for(i = 1; i < CGI_THREAD_NUM; i++) {
pthread_create(&id[i], NULL, thread_cgi, (void*)&i);
}
为了处理并发请求,程序创建了多个工作线程。每个线程执行thread_cgi
函数,等待并处理FCGI请求。
void *thread_cgi(void *param)
{
int ret;
FCGX_Request cgi_request;
while(1){
memset(&cgi_request, 0, sizeof(cgi_request));
FCGX_InitRequest(&cgi_request, cgi_sock, 0);
ret = FCGX_Accept_r(&cgi_request);
if(ret == 0){
cgi_service(&cgi_request); //处理cgi request的请求
FCGX_Finish_r(&cgi_request);
} else {
printf("CGI accept fail\n");
}
}
}
工作线程在一个无限循环中执行以下操作:
守护进程(Daemon)是在后台运行的服务进程,不受终端控制。实现守护进程的关键步骤如下:
void daemonize() {
pid_t pid;
// 创建子进程
pid = fork();
// 创建子进程失败
if (pid < 0) {
log_error("Failed to fork daemon process");
exit(1);
}
// 父进程退出
if (pid > 0) {
exit(0);
}
// 子进程继续
// 创建新会话,使子进程成为会话首进程
if (setsid() < 0) {
log_error("Failed to create new session");
exit(1);
}
// 忽略SIGHUP信号
signal(SIGHUP, SIG_IGN);
// 再次fork,确保进程不是会话首进程,防止获取控制终端
pid = fork();
if (pid < 0) {
log_error("Failed to fork daemon process (second fork)");
exit(1);
}
if (pid > 0) {
exit(0);
}
// 更改工作目录到根目录
if (chdir("/") < 0) {
log_error("Failed to change working directory");
exit(1);
}
// 重设文件创建掩码
umask(0);
// 关闭所有打开的文件描述符
for (int i = 0; i < 1024; i++) {
close(i);
}
// 重定向标准输入、输出和错误到/dev/null
int fd = open("/dev/null", O_RDWR);
if (fd < 0) {
log_error("Failed to open /dev/null");
exit(1);
}
dup2(fd, STDIN_FILENO);
dup2(fd, STDOUT_FILENO);
dup2(fd, STDERR_FILENO);
if (fd > 2) {
close(fd);
}
log_info("Process daemonized successfully with PID: %d", getpid());
}
守护进程的创建过程包括以下关键步骤:
setsid()
创建新的会话,使进程脱离原来的控制终端。/dev/null
。// 如果指定了守护进程模式,则将进程转换为守护进程
if (daemon_mode) {
log_info("Running in daemon mode");
daemonize();
// 在守护进程模式下,如果PID文件创建失败,则退出程序
if (write_pid_file() != 0) {
log_error("Failed to create PID file in daemon mode, exiting");
exit(EXIT_FAILURE);
}
log_info("PID file created successfully with PID: %d", getpid());
} else {
// 非守护进程模式下,创建PID文件但失败不退出
if (write_pid_file() != 0) {
log_error("Failed to create PID file, continuing anyway");
} else {
log_info("PID file created successfully with PID: %d", getpid());
}
}
程序通过命令行参数-d
或--daemon
来决定是否以守护进程模式运行。在守护进程模式下,如果PID文件创建失败,程序会直接退出;而在非守护进程模式下,即使PID文件创建失败,程序也会继续运行。
// 解析命令行参数
static struct option long_options[] = {
{"daemon", no_argument, 0, 'd'},
{"stop", no_argument, 0, 's'},
{0, 0, 0, 0}
};
while ((opt = getopt_long(argc, argv, "ds", long_options, NULL)) != -1) {
switch (opt) {
case 'd':
daemon_mode = 1;
break;
case 's':
stop_mode = 1;
break;
default:
fprintf(stderr, "Usage: %s [-d|--daemon] [-s|--stop]\n", argv[0]);
fprintf(stderr, " -d, --daemon Run as daemon\n");
fprintf(stderr, " -s, --stop Stop running server\n");
exit(EXIT_FAILURE);
}
}
程序支持两个主要的命令行选项:
-d
或--daemon
:以守护进程模式运行-s
或--stop
:停止正在运行的服务PID文件是管理守护进程的重要工具,它存储了进程的PID,便于后续操作(如停止服务)。
int write_pid_file() {
FILE *fp;
pid_t pid = getpid();
// 打开PID文件
fp = fopen(PID_FILE, "w");
if (fp == NULL) {
log_error("Failed to open PID file: %s", strerror(errno));
return -1;
}
// 写入PID
fprintf(fp, "%d\n", pid);
fflush(fp); // 确保数据被写入磁盘
fclose(fp);
// 设置PID文件权限
if (chmod(PID_FILE, 0644) < 0) {
log_error("Failed to set PID file permissions: %s", strerror(errno));
return -1;
}
log_info("PID file created: %s (PID: %d)", PID_FILE, pid);
return 0;
}
写入PID文件的过程包括:
int stop_running_server() {
FILE *fp;
pid_t pid;
int ret = -1;
// 打开PID文件
fp = fopen(PID_FILE, "r");
if (fp == NULL) {
log_error("Failed to open PID file: %s. Is the server running?", strerror(errno));
return -1;
}
// 读取PID
char pid_str[32] = {0};
if (fgets(pid_str, sizeof(pid_str), fp) == NULL) {
log_error("Failed to read PID from file: %s", strerror(errno));
fclose(fp);
return -1;
}
// 转换PID字符串为整数
pid = atoi(pid_str);
if (pid <= 0) {
log_error("Invalid PID read from file: %s", pid_str);
fclose(fp);
return -1;
}
fclose(fp);
log_info("Stopping fcgiServer with PID: %d", pid);
// 发送SIGTERM信号
if (kill(pid, SIGTERM) == 0) {
// 等待进程退出
int max_wait = 10; // 最多等待10秒
while (max_wait-- > 0) {
if (kill(pid, 0) < 0) {
if (errno == ESRCH) {
// 进程已经退出
ret = 0;
break;
}
}
sleep(1);
}
if (ret != 0) {
log_error("Process did not terminate within timeout, sending SIGKILL");
kill(pid, SIGKILL);
ret = 0;
}
} else {
if (errno == ESRCH) {
log_error("No process with PID %d is running", pid);
// 进程不存在,可能已经退出,删除PID文件
unlink(PID_FILE);
ret = 0;
} else {
log_error("Failed to send signal to process: %s", strerror(errno));
}
}
// 删除PID文件
if (ret == 0) {
unlink(PID_FILE);
log_info("fcgiServer stopped successfully");
}
return ret;
}
停止服务的过程包括:
// 如果是停止模式,则停止运行中的服务并退出
if (stop_mode) {
log_init(FCGI_SERVER_LOG_FILE_NAME);
log_info("Stopping fcgiServer...");
int ret = stop_running_server();
return (ret == 0) ? EXIT_SUCCESS : EXIT_FAILURE;
}
当用户使用-s
或--stop
选项启动程序时,程序会尝试停止正在运行的服务,然后退出。
// 信号处理函数,用于清理资源并退出
void cleanup_handler(int sig) {
log_info("接收到信号 %d,清理资源并退出...", sig);
// 关闭FCGI套接字
if (cgi_sock >= 0) {
close(cgi_sock);
cgi_sock = -1;
}
// 删除套接字文件
unlink(FCGI_SOCKET_PATH);
// 删除PID文件
unlink(PID_FILE);
log_info("清理完成,退出程序");
exit(0);
}
信号处理函数cleanup_handler
负责在程序接收到终止信号时进行资源清理:
// 注册信号处理函数
signal(SIGTERM, cleanup_handler);
signal(SIGINT, cleanup_handler);
程序注册了两个信号的处理函数:
SIGTERM
:终止信号,通常由kill
命令发送SIGINT
:中断信号,通常由Ctrl+C产生这确保了无论程序是正常终止还是被强制终止,都能进行适当的资源清理。
FCGI服务器的主循环是整个程序的核心,它负责接受和处理请求。在多线程模式下,主线程也参与请求处理:
// 主线程也处理请求
thread_cgi(NULL);
主线程和工作线程都执行相同的thread_cgi
函数,形成一个请求处理池。这种设计充分利用了系统资源,提高了并发处理能力。
FCGI常驻服务程序的设计涉及多个关键方面:
这种设计模式适用于需要长时间运行的服务程序,特别是Web后端服务。通过FCGI协议,服务可以高效地处理来自Web服务器的请求,而守护进程模式则确保服务能够在后台稳定运行。
为了方便服务的日常管理,我们通常会编写启动脚本来封装服务的启动、停止和重启操作。下面是一个典型的FCGI服务启动脚本示例(scripts/fcgiServer.sh
):
#!/bin/bash
set -e
CURRENT_DIR=$(pwd)
FCGISERVER_ROOT_DIR=$CURRENT_DIR/../bin
NAME=fcgiServer
start_fcgiServer()
{
cd $FCGISERVER_ROOT_DIR
sudo ./fcgiServer -d
cd -
}
shut_fcgiServer()
{
cd $FCGISERVER_ROOT_DIR
sudo ./fcgiServer -s
cd -
}
case "$1" in
start)
start_fcgiServer
;;
stop)
shut_fcgiServer
;;
restart)
shut_fcgiServer
start_fcgiServer
;;
*)
echo "Usage: $NAME {start|stop|restart}" >&2
exit 2
;;
esac
这个启动脚本的设计遵循了常见的Linux服务管理脚本模式,主要包括以下几个部分:
环境设置:
set -e
CURRENT_DIR=$(pwd)
FCGISERVER_ROOT_DIR=$CURRENT_DIR/../bin
NAME=fcgiServer
set -e
:当脚本中的任何命令返回非零状态时立即退出,提高脚本的健壮性启动函数:
start_fcgiServer()
{
cd $FCGISERVER_ROOT_DIR
sudo ./fcgiServer -d
cd -
}
sudo
以管理员权限启动服务,并使用-d
参数以守护进程模式运行停止函数:
shut_fcgiServer()
{
cd $FCGISERVER_ROOT_DIR
sudo ./fcgiServer -s
cd -
}
sudo
以管理员权限停止服务,通过-s
参数发送停止信号命令行参数处理:
case "$1" in
start)
start_fcgiServer
;;
stop)
shut_fcgiServer
;;
restart)
shut_fcgiServer
start_fcgiServer
;;
*)
echo "Usage: $NAME {start|stop|restart}" >&2
exit 2
;;
esac
脚本的使用方法非常简单:
启动服务:
./scripts/fcgiServer.sh start
停止服务:
./scripts/fcgiServer.sh stop
重启服务:
./scripts/fcgiServer.sh restart
在实现FCGI常驻服务程序时,可以考虑以下最佳实践:
setrlimit
设置资源限制,防止程序占用过多系统资源。通过遵循这些最佳实践,可以构建更加健壮、安全和高效的FCGI常驻服务程序。