原文转自:http://chenzhenianqing.cn/articles/936.html
是一个小程序,作用是管理fast-cgi进程,功能和php-fpm类似,简单小巧,原先是属于lighttpd的一部分,后来由于使用比较广泛,所以就了,本文介绍的是这个版本“”。不过从发布新版本到目前已经4年了,代码一直没有变动,需求少,基本满足了。另外php有php-fpm后,码农们再也不担心跑不起FCGI了。
很久之前看的spawn-fcgi的代码,当时因为需要改一下里面的环境变量。今天翻代码看到了就顺手记录一下,就当沉淀.备忘吧。
用spawn启动FCGI程序的方式为:./spawn-fcgi -a 127.0.0.1 -p 9003 -F ${count} -f ${webroot}/bin/demo.fcgi
这样就会启动count个demo.fcgi程序,他们共同监听同一个listen端口9003,从而提供服务。
spawn-fcgi代码不到600行,非常简短精炼,从main看起。其功能主要是打开监听端口,绑定地址,然后fork-exec创建FCGI进程,退出完成工作。
老方法,main函数使用getopt解析命令行参数,从而设置全局变量。如果设置了-P参数,需要保存Pid文件,就用open系统调用打开文件。之后根据是否是root用户启动,如果是root,得做相关的权限设置,比如chroot, chdir, setuid, setgid, setgroups等。
重要的是调用了bind_socket打开绑定本地监听地址,或者sock,再就是调用fcgi_spawn_connection创建FCGI进程,主要就是这2步。
1 | int main( int argc, char **argv) { |
2 | if (!sockbeforechroot && -1 == (fcgi_fd = bind_socket(addr, port, unixsocket, sockuid, sockgid, sockmode))) |
3 | return -1; |
4 |
5 | /* drop root privs */ |
6 | if (uid != 0) { |
7 | setuid(uid); |
8 | } |
9 | } else { //非root用户启动,打开监听端口,进入listen模式。 |
10 | if (-1 == (fcgi_fd = bind_socket(addr, port, unixsocket, 0, 0, sockmode))) |
11 | return -1; |
12 | } |
13 |
14 | if (fcgi_dir && -1 == chdir(fcgi_dir)) { |
15 | fprintf (stderr, "spawn-fcgi: chdir('%s') failed: %s\n" , fcgi_dir, strerror ( errno )); |
16 | return -1; |
17 | } |
18 |
19 | //fork创建FCGI的进程 |
20 | return fcgi_spawn_connection(fcgi_app, fcgi_app_argv, fcgi_fd, fork_count, child_count, pid_fd, nofork); |
21 | } |
bind_socket函数用来创建套接字,绑定监听端口,进入listen模式。其参数unixsocket表明需要使用unix sock文件,这里不多介绍。函数代码页挺简单,莫过于通用的sock程序步骤:socket()->setsockopt()->bind()->listen();
1 | static int bind_socket( const char *addr, unsigned short port, const char *unixsocket, uid_t uid, gid_t gid, int mode) |
2 | { //bind_socket函数用来创建套接字,绑定监听端口,进入listen模式 |
3 | //`````` |
4 | if (-1 == (fcgi_fd = socket(socket_type, SOCK_STREAM, 0))) { |
5 | fprintf (stderr, "spawn-fcgi: couldn't create socket: %s\n" , strerror ( errno )); |
6 | return -1; |
7 | } |
8 |
9 | val = 1; |
10 | if (setsockopt(fcgi_fd, SOL_SOCKET, SO_REUSEADDR, &val, sizeof (val)) < 0) { |
11 | fprintf (stderr, "spawn-fcgi: couldn't set SO_REUSEADDR: %s\n" , strerror ( errno )); |
12 | return -1; |
13 | } |
14 |
15 | if (-1 == bind(fcgi_fd, fcgi_addr, servlen)) { |
16 | fprintf (stderr, "spawn-fcgi: bind failed: %s\n" , strerror ( errno )); |
17 | return -1; |
18 | } |
19 |
20 | if (unixsocket) { |
21 | if (0 != uid || 0 != gid) { |
22 | if (0 == uid) uid = -1; |
23 | if (0 == gid) gid = -1; |
24 | if (-1 == chown(unixsocket, uid, gid)) { |
25 | fprintf (stderr, "spawn-fcgi: couldn't chown socket: %s\n" , strerror ( errno )); |
26 | close(fcgi_fd); |
27 | unlink(unixsocket); |
28 | return -1; |
29 | } |
30 | } |
31 |
32 | if (-1 != mode && -1 == chmod(unixsocket, mode)) { |
33 | fprintf (stderr, "spawn-fcgi: couldn't chmod socket: %s\n" , strerror ( errno )); |
34 | close(fcgi_fd); |
35 | unlink(unixsocket); |
36 | return -1; |
37 | } |
38 | } |
39 | if (-1 == listen(fcgi_fd, 1024)) { |
40 | fprintf (stderr, "spawn-fcgi: listen failed: %s\n" , strerror ( errno )); |
41 | return -1; |
42 | } |
43 |
44 | return fcgi_fd; |
45 | } |
fcgi_spawn_connection函数的工作是循环一次次创建子进程,然后立即调用execv(appArgv[0], appArgv);替换可执行程序,也就试运行demo.fcgi。
1 | static int fcgi_spawn_connection( char *appPath, char **appArgv, int fcgi_fd, int fork_count, int child_count, int pid_fd, |
2 | int nofork) { |
3 | int status, rc = 0; |
4 | struct timeval tv = { 0, 100 * 1000 }; |
5 |
6 | pid_t child; |
7 |
8 | while (fork_count-- > 0) { |
9 |
10 | if (!nofork) { //正常不会设置nofork的 |
11 | child = fork(); |
12 | } else { |
13 | child = 0; |
14 | } |
15 |
16 | switch (child) { |
17 | case 0: { |
18 | //子进程 |
19 | char cgi_childs[64]; |
20 | int max_fd = 0; |
21 |
22 | int i = 0; |
23 | if (child_count >= 0) { |
24 | snprintf(cgi_childs, sizeof (cgi_childs), "PHP_FCGI_CHILDREN=%d" , child_count); |
25 | putenv(cgi_childs); |
26 | } |
27 |
28 | //wuhaiwen:add child id to thread |
29 | char bd_children_id[32]; |
30 | snprintf(bd_children_id, sizeof (bd_children_id), "BD_CHILDREN_ID=%d" , fork_count); |
31 | putenv(bd_children_id); |
32 |
33 | if (fcgi_fd != FCGI_LISTENSOCK_FILENO) { |
34 | close(FCGI_LISTENSOCK_FILENO); |
35 | dup2(fcgi_fd, FCGI_LISTENSOCK_FILENO); |
36 | close(fcgi_fd); |
37 | } |
38 | /* loose control terminal */ |
39 | if (!nofork) { |
40 | setsid(); //执行setsid()之后,parent将重新获得一个新的会话session组id,child将仍持有原有的会话session组, |
41 | //这时parent退出之后,将不会影响到child了[luther.gliethttp]. |
42 | max_fd = open( "/dev/null" , O_RDWR); |
43 | if (-1 != max_fd) { |
44 | if (max_fd != STDOUT_FILENO) dup2(max_fd, STDOUT_FILENO); |
45 | if (max_fd != STDERR_FILENO) dup2(max_fd, STDERR_FILENO); |
46 | if (max_fd != STDOUT_FILENO && max_fd != STDERR_FILENO) close(max_fd); |
47 | } else { |
48 | fprintf (stderr, "spawn-fcgi: couldn't open and redirect stdout/stderr to '/dev/null': %s\n" , strerror |
49 | ( errno )); |
50 | } |
51 | } |
52 |
53 | /* we don't need the client socket */ |
54 | for (i = 3; i < max_fd; i++) { |
55 | if (i != FCGI_LISTENSOCK_FILENO) close(i); |
56 | } |
57 |
58 | /* fork and replace shell */ |
59 | if (appArgv) { //如果有外的参数,就用execv执行,否则直接用shell执行 |
60 | execv(appArgv[0], appArgv); |
61 |
62 | } else { |
63 | char *b = malloc (( sizeof ( "exec " ) - 1) + strlen (appPath) + 1); |
64 | strcpy (b, "exec " ); |
65 | strcat (b, appPath); |
66 |
67 | /* exec the cgi */ |
68 | execl( "/bin/sh" , "sh" , "-c" , b, ( char *)NULL); |
69 | } |
70 |
71 | /* in nofork mode stderr is still open */ |
72 | fprintf (stderr, "spawn-fcgi: exec failed: %s\n" , strerror ( errno )); |
73 | exit ( errno ); |
74 |
75 | break ; |
76 | } |
上面是创建子进程的部分代码,基本没啥可说明的。
对于子进程:注意一下dup2函数,由子进程运行,将监听句柄设置为标准输入,输出句柄。比如FCGI_LISTENSOCK_FILENO 0 号在FCGI里面代表标准输入句柄。函数还会关闭其他不必要的socket句柄。然后调用execv替换可执行程序,运行新的二进制,也就是demo.fcgi的FCGI程序。这样子进程能够继承父进程的所有打开句柄,包括监听socket。这样所有子进程都能够在这个9002端口上进行监听新连接,谁拿到了谁就处理之。对于父进程: 主要需要用select等待一会,然后调用waitpid用WNOHANG参数获取一下子进程的状态而不等待子进程退出,如果失败就打印消息。否则将其PID写入文件。1 | default : |
2 | /* father */ |
3 |
4 | /* wait */ |
5 | select(0, NULL, NULL, NULL, &tv); |
6 |
7 | switch (waitpid(child, &status, WNOHANG)) { |
8 | case 0: |
9 | fprintf (stdout, "spawn-fcgi: child spawned successfully: PID: %d\n" , child); |
10 |
11 | /* write pid file */ |
12 | if (pid_fd != -1) { |
13 | /* assume a 32bit pid_t */ |
14 | char pidbuf[12]; |
15 |
16 | snprintf(pidbuf, sizeof (pidbuf) - 1, "%d" , child); |
17 |
18 | write(pid_fd, pidbuf, strlen (pidbuf)); |
19 | /* avoid eol for the last one */ |
20 | if (fork_count != 0) { |
21 | write(pid_fd, "\n" , 1); |
22 | } |
23 | } |
24 |
25 | break ; |
基本就是上面的东西了,代码不多,但该有的都有,命令行解析,socket,fork,dup2等。很久之前看的在这里备忘一下。