晚上写了个FastCGI进程管理器

之前一直使用单个进程都php-cgi。存在一个很严重都问题就是,php-cgi不稳定导致程序异常退出时,需要外部程序重新启动php-cgi。而且因为socket还要等待一段时间才能断开,所以要等一段时间(大概30多秒)才能重新启动成功。这样就产生了一段时间无法使用php都网页,很是烦恼。之前就一直想写一个php-cgi都进程管理器。

现在服务器上运行了几个庞大的wordpress,php崩溃都机率又大多了,迫不得已去给homeserver安装一个进程管理器。我测试过使用php-fpm,但是不好使,一是占用内存非常大,二是出现突然间无法响应都现象。后来yousan大牛推荐了用于lighttpd都spawn-fcgi,我在windows测试成功了,终于可以并发处理多个php请求。测试方法是,写一个php文件,里面写,如果刷新两次等待了10秒,那么就说明没有并发处理功能。打开多个页面测试也可以。

我参考了lighttpd都源代码,得知到之前我一直无法手动启用多个php-cgi,是因为没有使用共享socket(或者设置socket的重用标记)。我给Windows和Linux写了不同的实现方式以支持不同平台都使用。不过,因为我对Windows上都权限处理不熟悉,所以没有写用户权限都切换功能。HomeServer是以root形式使用80端口运行的,但php总不能以root运行吧,那样脚本都权限就太大了。应该可以让人指定运行使用都用户和组(例如www-data)。

启动多个php-cgi都方法是,先创建一个socket,设置为可重用(REUSE标记)。然后创建多个线程(用于并发处理php脚本),把这个socket作为输入句柄传给新创建都php-cgi进程,然后线程等待php-cgi进程崩溃或异常退出,如果服务器还在运行,则重启php-cgi。

当HomeServer结束时,杀死所有的php-cgi进程。Linux下,如果父进程被Kill了,子进程都php-cgi也会退出吧,因为都是使用同一个会话,而HomeServer就是这个会话都主进程。对不?

废话少说,下面是对plugin_fcgi.c都扩展代码:

static void* spawn_process( void* data )
{
	FILE* fp;
	fcgi_program* fcgi = (fcgi_program*)data;
	int idx = fcgi->spawn_count ++, ret;
	while( !end ){
#ifdef __WIN32__
		STARTUPINFO si={0};
		PROCESS_INFORMATION pi={0};
		ZeroMemory(&si,sizeof(STARTUPINFO));
		si.cb = sizeof(STARTUPINFO);
		si.dwFlags = STARTF_USESTDHANDLES;
		si.hStdOutput = INVALID_HANDLE_VALUE;
		si.hStdInput  = (HANDLE)fcgi->listen_fd;
		si.hStdError  = INVALID_HANDLE_VALUE;
		if( 0 == (ret=CreateProcess(NULL, fcgi->shell,
			NULL,NULL,
			TRUE, CREATE_NO_WINDOW ,
			NULL,NULL,
			&si,&pi)) ){
			printf("[fcgi] failed to create process %s, ret=%d\n", fcgi->shell, ret);
			return NULL;
		}
		fcgi->process_fp[idx] = (int)pi.hProcess;
		printf("[fcgi] starting process %s\n", fcgi->shell );
		WaitForSingleObject(pi.hProcess, INFINITE);
		fcgi->process_fp[idx] = 0;
		CloseHandle(pi.hThread);
#else
		ret = fork();
		switch(ret){
		case 0:{	//child
			/* change uid from root to other user */
			if(getuid()==0){ 
                struct group *grp = NULL;
                struct passwd *pwd = NULL;
				if (*fcgi->username) {
					if (NULL == (pwd = getpwnam(fcgi->username))) {
						printf("[fcgi] %s %s\n", "can't find username", fcgi->username);
						exit(-1);
					}
					if (pwd->pw_uid == 0) {
						printf("[fcgi] %s\n", "what? dest uid == 0?" );
						exit(-1);
					}
				}
				if (*fcgi->groupname) {
					if (NULL == (grp = getgrnam(fcgi->groupname))) {
						printf("[fcgi] %s %s\n", "can't find groupname", fcgi->groupname);
						exit(1);
					}
					if (grp->gr_gid == 0) {
						printf("[fcgi] %s\n", "what? dest gid == 0?" );
						exit(1);
					}
					/* do the change before we do the chroot() */
					setgid(grp->gr_gid);
					setgroups(0, NULL);
					if (fcgi->username) {
						initgroups(fcgi->username, grp->gr_gid);
					}
				}
				if (*fcgi->changeroot) {
					if (-1 == chroot(fcgi->changeroot)) {
						printf("[fcgi] %s %s\n", "can't change root", fcgi->changeroot);
						exit(1);
					}
					if (-1 == chdir("/")) {
						printf("[fcgi] %s %s\n", "can't change dir to", fcgi->changeroot);
						exit(1);
					}
				}
				/* drop root privs */
				if (*fcgi->username) {
					printf("[fcgi] setuid to %s\n", fcgi->username);
					setuid(pwd->pw_uid);
				}
			}
			
			int max_fd = 0, i=0;
			// Set stdin to listen_fd
			close(STDIN_FILENO);
			dup2(fcgi->listen_fd, STDIN_FILENO);
			close(fcgi->listen_fd);
			// Set stdout and stderr to dummy fd
			max_fd = open("/dev/null", O_RDWR);
			close(STDERR_FILENO);
			dup2(max_fd, STDERR_FILENO);
			close(max_fd);
			max_fd = open("/dev/null", O_RDWR);
			close(STDOUT_FILENO);
			dup2(max_fd, STDOUT_FILENO);
			close(max_fd);
			// close other handles
			for(i=3; i<max_fd; i++)
				close(i);
			char *b = malloc(strlen("exec ") + strlen(fcgi->shell) + 1);
			strcpy(b, "exec ");
			strcat(b, fcgi->shell);
			
			/* exec the cgi */
			execl("/bin/sh", "sh", "-c", b, (char *)NULL);
			exit(errno);
			break;
		}
		case -1:
			printf("[fcgi] fork failed\n");
			return NULL;
		default:{
			struct timeval tv = { 0, 100 * 1000 };
			int status;
			select(0, NULL, NULL, NULL, &tv);
			switch(waitpid(ret, &status, WNOHANG)){
			case 0:
				printf("[fcg] spawned process %s: %d\n", fcgi->shell, ret);
				break;
			case -1:
				printf("[fcgi] waitpid failed\n");
				return NULL;
			default:
				if (WIFEXITED(status)) {
						printf("[fcgi] child exited with: %d\n", WEXITSTATUS(status));
				} else if (WIFSIGNALED(status)) {
						printf("[fcgi] child signaled: %d\n", WTERMSIG(status));
				} else {
						printf("[fcgi] child died somehow: %d\n", status);
				}
				return NULL;
			}
			//wait for child process to exit
			fcgi->process_fp[idx] = ret;
			waitpid(ret, &status, 0);
			fcgi->process_fp[idx] = 0;
		}
		}
#endif
	}
	if(!end){
		printf("[fcgi] failed to create process %s\n", fcgi->shell );
	}
	return NULL;
}

static void delete_program( const void* v )
{
	fcgi_program * fp = (fcgi_program*)v;
	int i;
	for( i=0; i<fp->spawn_count; i++ ){
		if( fp->process_fp[i] ){
			int h = fp->process_fp[i];
			printf("[fcgi] terminating %s: %x\n", fp->shell, h );
#ifdef __WIN32__
			TerminateProcess((HANDLE)h, 0);
#else
			kill(fp->process_fp[i], SIGKILL);
#endif
//			pthread_join( fp->threads[i], (void**)0 );
		}
	}
	closesocket( fp->listen_fd );
	free( fp );
}

#ifdef __WIN32__
#define EXPORT __declspec(dllexport) __stdcall 
#else
#define EXPORT
#endif
int EXPORT plugin_entry( webserver* srv )
{
	pthread_attr_t attr;
	pthread_attr_init(&attr);
	pthread_attr_setstacksize(&attr, 64*1024); //64KB
	//load fcgi program information
	loop_create( &loop_program, MAX_FCGI_PROGRAM, delete_program );
	if( xml_redirect( srv->config, "/fastCGI/program", 0 ) ){
		do{
			int spawn_count, i;
			fcgi_program * fcgip;
			fcgip = (fcgi_program*) malloc( sizeof(fcgi_program) );
			memset( fcgip, 0, sizeof(fcgi_program) );
			loop_push_to_head( &loop_program, (void*)fcgip );
			strncpy( fcgip->extension, xml_readstr(srv->config, ":extension"), 63 );
			strncpy( fcgip->username, xml_readstr( srv->config, "spawnProcess:user" ), 31 );
			strncpy( fcgip->groupname, xml_readstr( srv->config, "spawnProcess:group" ), 31 );
			strncpy( fcgip->changeroot, xml_readstr( srv->config, "spawnProcess:root" ), 127 );
			strncpy( fcgip->shell, xml_readstr( srv->config, "spawnProcess:path" ), 127 );
			spawn_count = xml_readnum(srv->config, "spawnProcess:number");
			strncpy( fcgip->server_addr, xml_readstr( srv->config, "serverAddress" ), 63 );
			fcgip->server_port = xml_readnum(srv->config, "serverPort");
			printf("[fcgi] fcgi for %s:%d extension:%s shell:%s\n", fcgip->server_addr, fcgip->server_port, 
				fcgip->extension, fcgip->shell );
			fcgip->dest_addr.sin_family = PF_INET;
			fcgip->dest_addr.sin_addr.s_addr = inet_addr( fcgip->server_addr );
			fcgip->dest_addr.sin_port = htons( fcgip->server_port );
			
			/* Check If we need to start the fastcgi process */
			if( fcgip->shell[0] ){
				fcgip->listen_fd = socket(AF_INET, SOCK_STREAM, 0);
				
				if( connect( fcgip->listen_fd, (struct sockaddr*)&fcgip->dest_addr, sizeof(struct sockaddr_in)) != -1 ){
					printf("[fcgi] %s:%d is already in use. failed to spawn in.\n", fcgip->server_addr, fcgip->server_port );
					closesocket( fcgip->listen_fd );
					return -1;
				}
				closesocket( fcgip->listen_fd );
				//reopen socket
				fcgip->listen_fd = socket(AF_INET, SOCK_STREAM, 0);
				i = 1;
				setsockopt( fcgip->listen_fd, SOL_SOCKET, SO_REUSEADDR, (void*)&i, sizeof(i));
				
				if (-1 == bind( fcgip->listen_fd, (struct sockaddr*)&fcgip->dest_addr, sizeof(struct sockaddr_in)) ) {
					printf("[fcgi] failed to bind %s:%d\n", fcgip->server_addr, fcgip->server_port );
					return -1;
				}
				listen(fcgip->listen_fd, srv->max_threads);
				if( spawn_count < 1 || spawn_count > 64 )
					spawn_count = 5;
				for( i=0; i<spawn_count; i++ )
					if( pthread_create( &fcgip->threads[i], &attr, (void*)spawn_process, (void*)fcgip ) == -1 ){
						printf("[fcgi] failed to create thread: %d\n", errno);
					}
			}
		}while( xml_movenext( srv->config ) );
	}
	srv->vdir_create( srv, "/", (vdir_handler)fcgi_run );
	return 0;
}

int EXPORT plugin_cleanup( webserver* srv )
{
	end = 1;
	loop_cleanup( &loop_program );
	return 0;
}

如有错误或更好都提议,不妨提出来共同讨论。

晚上写了个FastCGI进程管理器》有37个想法

  1. 狙击手

    小虾的coding能力真是强啊。之前给过你一个fcgi.c的文件,我也是参考lighttpd的spawn-fcgi写的。以为你用上了,原来这么久都没有用到……另外,你应该充分发挥linux的多用户的特长,不应该用root去执行homeserver,最好的是新建一个用户,以那个用户的权限去执行homeserver和php。

    回复
    1. Xiaoxia 文章作者

      嗯,之前是因为侦听80端口需要用到root权限,所以就一直以root方式启动HomeServer来运行。也考虑过通过端口转发让HomeServer以普通用户执行。不过我觉得现在它也只是一个http服务器,除非它存在溢出等漏洞,否则也没有什么必要限制它的权限了。而像php这些脚本引擎使用普通用户权限应该是必须的。

      回复
      1. 黑孩儿

        是有点慢,我不想我的博客数据在进入国内时被什么东西提前浏览了一下。主要原因是我现在用的联通网络所有外出HTTP请求全部会经过一个透明代理服务器,这个代理服务器会修改HTTP请求头的UA,弄的我登录不了博客还有很多一些论坛的验证码都会莫名其妙的不对,只有用代理。

        回复
    1. Xiaoxia 文章作者

      PHP_FCGI_MAX_REQUESTS=1 是不是php-cgi运行一次只处理一个请求,然后结束? 还是什么意思?

      回复
      1. 黑孩儿

        现在 php-cgi 本身就有一个进程管理器,PHP_FCGI_MAX_REQUESTS=1 意思是 php-cgi 的子工作进程只处理一次请求就结束,只要处理过一次请求后,整个子进程至少占用了大概10几M的内存不释放。主进程不退出且会再创建一个子工作进程,不过这个新的子工作进程使用内存特别的少。这样512M的VPS在很多的用户情况下不会感到内存不足。

        回复
        1. Xiaoxia 文章作者

          我的vps才128MB内存,现在ps一下,占用内存最多的就是3个php-cgi进程了,每个基本上占用将近20MB物理内存。web server程序才占了2MB。我还打算只用2个就够了。。。

          回复
          1. 黑孩儿

            那不错,我有4个虚拟主机,每个虚拟主机有3个php-cgi进程和1个cgi进程加上数据库和一些用户代理的sshd进程,用了220M内存,剩余的资源还是比较多的,有点浪费了。

            回复
            1. Xiaoxia 文章作者

              哇。。。你怎么有钱用4个虚拟主机。。。都用来放网站?商业的吗?
              其实我的vps上128M的物理内存,通常还有30多MB是空闲的。大部分程序的内存都被交换出去了,例如mysql占用130多MB的虚拟内存,在物理内存里只保留着7MB。

              回复
              1. 黑孩儿

                虚拟主机越多越省钱啊。我是和朋友合租了一个512M内存的VPS,然后在上面做了多个虚拟主机,这样很划算。我用的是 BurstNET 的VPS,不支持 swap 。

                回复
                1. Xiaoxia 文章作者

                  呵呵,原来如此。我的vps上也有10几个站点。你的BurstNET的VPS是从哪个代理商里买的?

                  回复
                    1. Xiaoxia 文章作者

                      权限分离的。每个不同的站点都是用一个不同的用户。这样每个站点都可以有独立的ftp、web、mysql。只不过现在有点问题是php-cgi是以www-data用户运行的,如果A账户目录里要设置一个能让php写入的目录,则B账号也能够写入A账号的那个目录了,当然,前提是B知道A的确切目录路径。
                      现在都是自己或者朋友的网站,也没那么高要求做到严格的脚本权限分离。只不过担心一个账户被盗了,会影响到其他的账户。呵呵~

  2. 黑孩儿

    哦,不能回复了。。。
    我那个方案是以 www-data 执行 nginx 的,nginx 可以访问每个虚拟主机的文件,不过我给 nginx 做了一个模板,限制只有虚拟主机配置文件中允许的文件隶属才能被访问。不同虚拟主机的 php-cgi 是以不同用户身份执行的,数据库也是不同用户的。我没有使用 ftp 而是用了 sftp 。虽然也都是很熟悉的朋友,但是弄VPS的乐趣就是搞好这么一些方案。我从 LAMP 的 itk 方案再到 suexec 一直到 LNMP 的这个方案都尝试过了,呵呵。

    回复
      1. Xiaoxia 文章作者

        嗯。现在HomeServer占据了80端口,除了几个网站服务器,还有代理服务(普通代理和fox代理)。

        回复
    1. Xiaoxia 文章作者

      Sorry!我居然把mod_fcgi看成mod_php了。。。
      我现在的HomeServer是多线程的,但占用资源比基于事件的nginx少很多,我想主要是nginx本身编译的时候集成的功能或者模块太多。以后有空我也打算做一个基于事件响应的服务器程序来玩玩。

      回复
  3. 一些FastCGI方面的问题

    您好!我在linux底下写了一个服务器,现在想支持PHP动态网页功能,虽然现在有一个思路,但是由于对其中的机制不太了解,就想着先写一个c/c++的FastCGI程序,然后和服务器进行交互,虽然知道是使用FastCGI协议,我还是对这个不太了解,你那儿有什么资料吗? 或者如果您能给我一些建议,先谢谢了!

    回复
  4. 一些FastCGI方面的问题

    你这个网站怎么在Ubuntu上部署? 我现在安装好了PHP5.3.2 打了php-cgi包 最后怎么访问phpinfo.xiaoxia 访问不到?

    回复
      1. 一些FastCGI方面的问题

        我输了php-cgi -b 127.0.0.1:8080 啊
        还有通过加入调试信息 访问已经定向到了phpinfo.xiaoxia
        但是 执行后 没有返回信息, 应该是 没有执行 网页程序, 是不是我 php-cgi 程序没有执行起来? 还是 别的什么问题?

        回复
        1. Xiaoxia 文章作者

          你使用什么服务器程序?返回空白页面?可能是通信过程中遇到问题,没有连接成功。
          另外,如果出现Access错误则要检查一下php文件的权限是否可读。

          回复
      2. 一些FastCGI方面的问题

        我这两天想赶紧把这个问题搞定,现在还有几个问题,如果你有时间的话,能不能把你的联系方式告诉我?邮箱 或者 qq都可以,先谢谢了

        回复
  5. Forer

    php-fpm占用内存很少的吧,默认安装完后确实使用一些时间内存就会用很大,但里面的配置文件改下就行了,让它3分释放一下即可。

    回复
  6. Pingback引用通告: [转]FastCGI管理器 | 喵了个咪

回复 Xiaofei 取消回复

您的电子邮箱地址不会被公开。 必填项已用*标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据