前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >spawn-fcgi原理及源码分析「建议收藏」

spawn-fcgi原理及源码分析「建议收藏」

作者头像
Java架构师必看
发布2022-07-25 14:05:07
2.4K0
发布2022-07-25 14:05:07
举报
文章被收录于专栏:Java架构师必看

大家好,我是架构君,一个会写代码吟诗的架构师。今天说一说spawn-fcgi原理及源码分析「建议收藏」,希望能够帮助大家进步!!!

spawn-fcgi是一个小程序,作用是管理fast-cgi进程,功能和php-fpm类似,简单小巧,原先是属于lighttpd的一部分,后来由于使用比较广泛,所以就迁移出来作为独立项目了,本文介绍的是这个版本“spawn-fcgi-1.6.3”。不过从发布新版本到目前已经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步。

代码语言:javascript
复制
int main(int argc, char **argv)
{
    if (!sockbeforechroot && -1 == (fcgi_fd = bind_socket(addr, port, unixsocket, sockuid, sockgid, sockmode)))
        return -1;
    /* drop root privs */
    if (uid != 0)
    {
        setuid(uid);
    }
    else    //非root用户启动,打开监听端口,进入listen模式。
    {
        if (-1 == (fcgi_fd = bind_socket(addr, port, unixsocket, 0, 0, sockmode)))
            return -1;
    }
    if (fcgi_dir && -1 == chdir(fcgi_dir))
    {
        fprintf(stderr, "spawn-fcgi: chdir('%s') failed: %s\n", fcgi_dir, strerror(errno));
        return -1;
    }
    //fork创建FCGI的进程
    return fcgi_spawn_connection(fcgi_app, fcgi_app_argv, fcgi_fd, fork_count, child_count, pid_fd, nofork);
}

只听到从架构师办公室传来架构君的声音:

平湖油油碧于酒,云锦十里翻风荷。有谁来对上联或下联?

bind_socket函数用来创建套接字,绑定监听端口,进入listen模式。其参数unixsocket表明需要使用unix sock文件,这里不多介绍。函数代码页挺简单,莫过于通用的sock程序步骤:socket()->setsockopt()->bind()->listen();

代码语言:javascript
复制
此代码由Java架构师必看网-架构君整理
static int bind_socket(const char *addr, unsigned short port, const char *unixsocket, uid_t uid, gid_t gid, int mode)
{
    //bind_socket函数用来创建套接字,绑定监听端口,进入listen模式
    if (-1 == (fcgi_fd = socket(socket_type, SOCK_STREAM, 0)))
    {
        fprintf(stderr, "spawn-fcgi: couldn't create socket: %s\n", strerror(errno));
        return -1;
    }
    val = 1;
    if (setsockopt(fcgi_fd, SOL_SOCKET, SO_REUSEADDR, &val, sizeof(val)) < 0)
    {
        fprintf(stderr, "spawn-fcgi: couldn't set SO_REUSEADDR: %s\n", strerror(errno));
        return -1;
    }
    if (-1 == bind(fcgi_fd, fcgi_addr, servlen))
    {
        fprintf(stderr, "spawn-fcgi: bind failed: %s\n", strerror(errno));
        return -1;
    }
    if (unixsocket)
    {
        if (0 != uid || 0 != gid)
        {
            if (0 == uid) uid = -1;
            if (0 == gid) gid = -1;
            if (-1 == chown(unixsocket, uid, gid))
            {
                fprintf(stderr, "spawn-fcgi: couldn't chown socket: %s\n", strerror(errno));
                close(fcgi_fd);
                unlink(unixsocket);
                return -1;
            }
        }
        if (-1 != mode && -1 == chmod(unixsocket, mode))
        {
            fprintf(stderr, "spawn-fcgi: couldn't chmod socket: %s\n", strerror(errno));
            close(fcgi_fd);
            unlink(unixsocket);
            return -1;
        }
    }
    if (-1 == listen(fcgi_fd, 1024))
    {
        fprintf(stderr, "spawn-fcgi: listen failed: %s\n", strerror(errno));
        return -1;
    }
    return fcgi_fd;
}

fcgi_spawn_connection函数的工作是循环一次次创建子进程,然后立即调用execv(appArgv0, appArgv);替换可执行程序,也就试运行demo.fcgi。

代码语言:javascript
复制
static int fcgi_spawn_connection(char *appPath, char **appArgv, int fcgi_fd, int fork_count, int child_count, int pid_fd,
                                 int nofork)
{
    int status, rc = 0;
    struct timeval tv = { 0, 100 * 1000 };
    pid_t child;
    while (fork_count-- > 0)
    {
        if (!nofork)  //正常不会设置nofork的
        {
            child = fork();
        }
        else
        {
            child = 0;
        }
        switch (child)
        {
        case 0:
        {
            //子进程
            char cgi_childs[64];
            int max_fd = 0;
            int i = 0;
            if (child_count >= 0)
            {
                snprintf(cgi_childs, sizeof(cgi_childs), "PHP_FCGI_CHILDREN=%d", child_count);
                putenv(cgi_childs);
            }
            //wuhaiwen:add child id to thread
            char bd_children_id[32];
            snprintf(bd_children_id, sizeof(bd_children_id), "BD_CHILDREN_ID=%d", fork_count);
            putenv(bd_children_id);
            if (fcgi_fd != FCGI_LISTENSOCK_FILENO)
            {
                close(FCGI_LISTENSOCK_FILENO);
                dup2(fcgi_fd, FCGI_LISTENSOCK_FILENO);
                close(fcgi_fd);
            }
            /* loose control terminal */
            if (!nofork)
            {
                setsid();//执行setsid()之后,parent将重新获得一个新的会话session组id,child将仍持有原有的会话session组,
                //这时parent退出之后,将不会影响到child了[luther.gliethttp].
                max_fd = open("/dev/null", O_RDWR);
                if (-1 != max_fd)
                {
                    if (max_fd != STDOUT_FILENO) dup2(max_fd, STDOUT_FILENO);
                    if (max_fd != STDERR_FILENO) dup2(max_fd, STDERR_FILENO);
                    if (max_fd != STDOUT_FILENO && max_fd != STDERR_FILENO) close(max_fd);
                }
                else
                {
                    fprintf(stderr, "spawn-fcgi: couldn't open and redirect stdout/stderr to '/dev/null': %s\n", strerror
                            (errno));
                }
            }

            /* we don't need the client socket */
            for (i = 3; i < max_fd; i++)
            {
                if (i != FCGI_LISTENSOCK_FILENO) close(i);
            }

            /* fork and replace shell */
            if (appArgv)  //如果有外的参数,就用execv执行,否则直接用shell执行
            {
                execv(appArgv[0], appArgv);

            }
            else
            {
                char *b = malloc((sizeof("exec ") - 1) + strlen(appPath) + 1);
                strcpy(b, "exec ");
                strcat(b, appPath);

                /* exec the cgi */
                execl("/bin/sh", "sh", "-c", b, (char *)NULL);
            }

            /* in nofork mode stderr is still open */
            fprintf(stderr, "spawn-fcgi: exec failed: %s\n", strerror(errno));
            exit(errno);

            break;
        }
    }
}

上面是创建子进程的部分代码,基本没啥可说明的。

对于子进程:注意一下dup2函数,由子进程运行,将监听句柄设置为标准输入,输出句柄。比如FCGI_LISTENSOCK_FILENO 0 号在FCGI里面代表标准输入句柄。函数还会关闭其他不必要的socket句柄。

然后调用execv替换可执行程序,运行新的二进制,也就是demo.fcgi的FCGI程序。这样子进程能够继承父进程的所有打开句柄,包括监听socket。这样所有子进程都能够在这个9002端口上进行监听新连接,谁拿到了谁就处理之。

对于父进程: 主要需要用select等待一会,然后调用waitpid用WNOHANG参数获取一下子进程的状态而不等待子进程退出,如果失败就打印消息。否则将其PID写入文件。

代码语言:javascript
复制
此代码由Java架构师必看网-架构君整理
default:
    /* father */

    /* wait */
    select(0, NULL, NULL, NULL, &tv);

    switch (waitpid(child, &status, WNOHANG))
    {
    case 0:
        fprintf(stdout, "spawn-fcgi: child spawned successfully: PID: %d\n", child);
        /* write pid file */
        if (pid_fd != -1)
        {
            /* assume a 32bit pid_t */
            char pidbuf[12];
            snprintf(pidbuf, sizeof(pidbuf) - 1, "%d", child);
            write(pid_fd, pidbuf, strlen(pidbuf));
            /* avoid eol for the last one */
            if (fork_count != 0)
            {
                write(pid_fd, "\n", 1);
            }
        }
        break;

基本就是上面的东西了,代码不多,但该有的都有,命令行解析,socket,fork,dup2等。很久之前看的在这里备忘一下。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2022-07-232,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
云开发 CloudBase
云开发(Tencent CloudBase,TCB)是腾讯云提供的云原生一体化开发环境和工具平台,为200万+企业和开发者提供高可用、自动弹性扩缩的后端云服务,可用于云端一体化开发多种端应用(小程序、公众号、Web 应用等),避免了应用开发过程中繁琐的服务器搭建及运维,开发者可以专注于业务逻辑的实现,开发门槛更低,效率更高。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档