美文网首页Redis
[0x00]redis里的小秘密:设置进程名

[0x00]redis里的小秘密:设置进程名

作者: 张夜白哥哥 | 来源:发表于2019-02-18 02:07 被阅读0次

    linux macOS下设置进程名

    base on redis source code 5.0.3

    在redis server启动过程中, 有一个宏和一个函数显得很奇特, 他们是server.cmain()函数中的第一个宏和第一个函数, 宏INIT_SETPROCTITLE_REPLACEMENT和函数spt_init(argc, argv);。他俩组合在一起的主要功能是在macOS和*nix下设置(修改)redis的各个进程名, 例如redis-aof-rewriteredis-rdb-bgsave等。
    那么他们是如何工作的呢?想知道这些, 这得从main函数说起。请看标准的main函数签名:

    int main(int argc, char ** argv);
    


    macOS和*nix系统创建进程后会给进程分配一个全局的environment环境变量char ** environ, 它是一个char*数组, 里面保存的是类似{k=v, k=v, k=v}这样的字符串数组。如果我们想使用它可以像下面这样(实际上redis也是这样做的):

    #include <iostream>
    extern char ** environ;
    int main(int argc, char ** argv){
        for (auto idx = 0; idx < argc; ++idx){
            std::cout << static_cast<void*>(argv[idx]) << " = " << argv[idx] << std::endl;
        }
        for(auto idx = 0; nullptr != environ[idx]; ++idx){
            std::cout << static_cast<void*>(environ[idx]) << " = " << environ[idx] << std::endl;
        }
    }
    
    g++ -Wall -std=c++11 main.cpp -o test
    ./test
    

    在我的mac下运行结果如下(其实就是系统给进程设置的环境变量):

    0x7ffeed1afa80 = ./test     // argv[0]
    0x7ffeed1afa87 = __CF_USER_TEXT_ENCODING=0x1F5:0x0:0x0  //environ[0]
    0x7ffeed1afaad = TMPDIR=/var/folders/5y/c9fbgn3x6p90sl9gbx_y1qnm0000gn/T/
    0x7ffeed1afae6 = HOME=/Users/zhangyebai
    0x7ffeed1afafd = SHELL=/bin/zsh
    0x7ffeed1afb0c = Apple_PubSub_Socket_Render=/private/tmp/com.apple.launchd.Ey7nIaeRPX/Render
    0x7ffeed1afb58 = SSH_AUTH_SOCK=/private/tmp/com.apple.launchd.CT1oMhwGyS/Listeners
    0x7ffeed1afb9a = PATH=/usr/local/opt/icu4c/sbin:/usr/local/opt/icu4c/bin:/usr/local/sbin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/opt/icu4c/sbin:/usr/local/opt/icu4c/bin:/usr/local/sbin:/Users/zhangyebai/scripts:/usr/local/mysql/bin:/Users/zhangyebai/scripts:/usr/local/mysql/bin
    0x7ffeed1afcb0 = LOGNAME=zhangyebai
    0x7ffeed1afcc3 = XPC_SERVICE_NAME=0
    0x7ffeed1afcd6 = COMMAND_MODE=unix2003
    0x7ffeed1afcec = USER=zhangyebai
    0x7fe8f2502450 = XPC_FLAGS=0x0
    0x7ffeed1afd0a = TERM_PROGRAM=vscode
    0x7ffeed1afd1e = TERM=xterm-256color
    0x7ffeed1afd32 = TERM_PROGRAM_VERSION=1.31.1
    0x7ffeed1afd4e = TERM_SESSION_ID=3B84E1B2-C0C2-426C-8AE3-00C01A9F6D5B
    0x7ffeed1afd83 = ZSH=/Users/zhangyebai/.oh-my-zsh
    0x7ffeed1afda4 = PAGER=less
    0x7ffeed1afdaf = LSCOLORS=Gxfxcxdxbxegedabagacad
    0x7ffeed1afdcf = PWD=/Users/zhangyebai/code/cpp/redis
    0x7ffeed1afdf4 = SHLVL=1
    0x7ffeed1afdfc = LESS=-R
    0x7ffeed1afe04 = LC_CTYPE=en_US.UTF-8
    0x7ffeed1afe19 = SECURITYSESSIONID=186a9
    0x7ffeed1afe31 = APPLICATION_INSIGHTS_NO_DIAGNOSTIC_CHANNEL=true
    0x7ffeed1afe61 = OLDPWD=/Users/zhangyebai/code/cpp/redis/testdwnakdihjwaijdiwaljdiowadwadwadwa.dSYM
    0x7ffeed1afeb4 = LANG=en_US.UTF-8
    0x7ffeed1afec5 = _=/Users/zhangyebai/code/cpp/redis/./test
    

    environint main(int argc, char ** argv)有着什么样的关系呢? 如下所示:

    'argv[0][content]\0' 'argv[1][content]\0' ... 'argv[n][content]\0' nullptr 'environ[0][content]\0' 'environ[1][content]\0' ... 'envrion[x][content]\0'
    

    可以看到他们是一段连续的内存, 下面我们举个例子把它实例化看一下:

    argv[0] argv[1]  environ[0] environ[1]
     a \0    b \0       d=2\0       e=3\0
     0  1    2  3       4567        891011
    
    base = 0x7ffeeaf21a80
    offset = 0, 1, 2 ... 11
    

    说完argvenviron的内存布局, 我们就可以开始看redis是如何设置进程名的了, 这里先直接给出答案: argv[0]里面对应的就是进程名。但是先别着急, 想要修改它可也没那么容易。因为什么? 上面我们说了, argvenviron可是内存连续的, 如果你设置了一个新的进程名长度比原来的长, 那么悲剧即将发生。它将覆盖argv[0]后面的缓冲区, 这将是致命的 。argvenviron在进程运行过程中随时可能会用到,它们很重要。好了, 下面我们看redis怎么做的:

    // server.c line 4035 version 5.0.3
    int main(int argc, char ** argv){
        //....more
        /* We need to initialize our libraries, and the server configuration. */
    #ifdef INIT_SETPROCTITLE_REPLACEMENT
        spt_init(argc, argv);
    #endif
        //...more
    }
    

    setproctitle.cline 152展开spt_init(argc, argv);, 我们一行一行来看(这里点名表扬redis的函数尾注释):

    void spt_init(int argc, char *argv[]) {
        char **envp = environ;
        char *base, *end, *nul, *tmp;
        int i, error;
        /*
            注意这里的base, 被赋值argv[0],由于上面说了argv和environ
            是内存连续的, 在经历下面的操作以后argv 和 environ这边连续
            的内存将退化成char*类型的base
        */
        if (!(base = argv[0]))
            return;
        // nul表示argv[0], 也就是base字符串, 
        // 也就是进程名字符串的结束位置(不包括\0)
        nul = &base[strlen(base)];
        end = nul + 1;
        /*
            虽然argv和environ是连续的内存, 但是其中包含\0, 而且不知道
            environ的长度,所以得遍历二者, 将这片内存退化成char*, 
            也就是base
            step 1: 遍历argv
            note: for循环的条件判断很奇怪, 我暂时没遇到满足这样奇怪条件
            的启动参数, 可能redis的开发人员在不同的操作系统上遇到这么怪异
            的问题, 有待探究。
        */
        for (i = 0; i < argc || (i >= argc && argv[i]); i++) {
            if (!argv[i] || argv[i] < end)
                continue;
    
            end = argv[i] + strlen(argv[i]) + 1;
        }
        /*
            step 2: 遍历 environ
            note: for循环中的这个if判断同样很奇怪
        */
        for (i = 0; envp[i]; i++) {
        if (envp[i] < end)
                continue;
    
            end = envp[i] + strlen(envp[i]) + 1;
        }
        /**
            SPT是一个全局变量结构体:
            static struct {
                // original value
                const char *arg0;
    
                // title space available
                char *base, *end;
    
                // pointer to original nul character within base
                char *nul;
    
                _Bool reset;
                int error;
            } SPT;
        */
        // 这一步很关键, 将原进程名备份
        if (!(SPT.arg0 = strdup(argv[0])))
            goto syerr;
    
    #if __GLIBC__
        /**
            释放跟argv有关的内存
        */
        if (!(tmp = strdup(program_invocation_name)))
            goto syerr;
    
        program_invocation_name = tmp;
    
        if (!(tmp = strdup(program_invocation_short_name)))
            goto syerr;
    
        program_invocation_short_name = tmp;
    #elif __APPLE__
        /**
            释放跟argv有关的内存
        */
        if (!(tmp = strdup(getprogname())))
            goto syerr;
    
        setprogname(tmp);
    #endif
    
        /**
            重新设置了env, 具体怎么实现我们等会跳进去看。
            只需要知道, 这个函数执行过以后, 将在新的内存区域
            产生新的environ, 原来的environ内存区域将归base所有
        */
        if ((error = spt_copyenv(envp)))
            goto error;
    
        /**
            重新设置了除argv[0]以外的所有argv, 具体怎么实现我们等会跳进去看。
            只需要知道, 这个函数执行过以后, 将在新的内存区域
            产生新的除argv[0]以外的argv, 原来的argv[1-n]内存区域将归base所有
        */
        if ((error = spt_copyargs(argc, argv)))
            goto error;
    
        /**
            至此, 原来argv environ共有的那片连续内存全部被转换成base, 即
            char*, 也是argv[0], 也就是进程名。argv和environ将不再连续, 新的
            内荣全部由strdup生成
            我们修改进程名, 只需要改SPT->base就可以了。
        */
        SPT.nul  = nul;
        SPT.base = base;
        SPT.end  = end;
    
        return;
    syerr:
        error = errno;
    error:
        SPT.error = error;
    } /* spt_init() */
    

    现在我们在setproctitle.cline 103展开spt_copyenv(envp):

    static int spt_copyenv(char *oldenv[]) {
        extern char **environ;
        char *eq;
        int i, error;
    
        /**
            如果environ != oldenv则说明environ已经被设置过了
            直接返回成功
        */
        if (environ != oldenv)
            return 0;
    
        /**
            让老的environ失效,但是不释放那片内存
            这个函数有一段血泪史, 等下我们展开
        */
        if ((error = spt_clearenv()))
            goto error;
    
        /**
            setenv会生成新的environ, 不必手动设置新environ的内存
            注意setenv中取值的写法, 这是c语言优秀的精华
        */
        for (i = 0; oldenv[i]; i++) {
            if (!(eq = strchr(oldenv[i], '=')))
                continue;
    
            *eq = '\0';
            error = (0 != setenv(oldenv[i], eq + 1, 1))? errno : 0;
            *eq = '=';
    
            if (error)
                goto error;
        }
    
        return 0;
        /**
            如果设置过程中出问题了, 则还原environ的设置
        */
    error:
        environ = oldenv;
    
        return error;
    } /* spt_copyenv() */
    

    现在我们来述说那段血泪史, 我们在setproctitle.cline 83展开spt_clearenv();:
    这里面是redis作者对cleanrenv()最深沉的吐槽

    /*
     * For discussion on the portability of the various methods, see
     * http://lists.freebsd.org/pipermail/freebsd-stable/2008-June/043136.html
     */
    /**
        看上面这段注释中的链接, redis作者写了好几种实现方法来兼容macOS和*nix系统, 至今未果
        现在的这段实现里面macOS是问号,但是我亲测it works
        step 1: 扔给系统一个空environ, 让原来的environ失效
        step 2: 再设置新的environ
    */
    static int spt_clearenv(void) {
    #if __GLIBC__
        clearenv();
    
        return 0;
    #else
        extern char **environ;
        static char **tmp;
        /**
            及其怪异的写法,其实相当于:
            char * arr[1] = {nullptr};
            temp = static_cast<char**>(arr);
            environ = temp;
            它的这个写法, 我思考了好久。这里也不得不吐槽, c语言灵活的没边了...
            void *一时爽, 回看火葬场。
        */
        if (!(tmp = malloc(sizeof *tmp)))
            return errno;
    
        tmp[0]  = NULL;
        environ = tmp;
    
        return 0;
    #endif
    } /* spt_clearenv() */
    

    现在我们在setproctitle.cline 103展开spt_copyargs(argc, argv);:

    static int spt_copyargs(int argc, char *argv[]) {
        char *tmp;
        int i;
    
        /**
            除argv[0]以外的所有argv都由strdup重新生成
            注意由strdup生成的char*是需要free的
            redis里由于就在这里用到, 并且这些参数是给系统使用,
            而且所有函数都只调用一次所以就没有free
        */
        for (i = 1; i < argc || (i >= argc && argv[i]); i++) {
            if (!argv[i])
                continue;
    
            if (!(tmp = strdup(argv[i])))
                return errno;
    
            argv[i] = tmp;
        }
    
        return 0;
    } /* spt_copyargs() */
    

    现在我们在setproctitle.cline 103展开setproctitle(const char fmt, ...):

    //SPT_MAXTITLE = 255;
    void setproctitle(const char *fmt, ...) {
        char buf[SPT_MAXTITLE + 1]; /* use buffer in case argv[0] is passed */
        va_list ap;
        char *nul;
        int len, error;
    
        if (!SPT.base)
            return;
    
        if (fmt) {
            va_start(ap, fmt);
            len = vsnprintf(buf, sizeof buf, fmt, ap);
            va_end(ap);
        } else {
            len = snprintf(buf, sizeof buf, "%s", SPT.arg0);
        }
    
        if (len <= 0)
            { error = errno; goto error; }
    
        if (!SPT.reset) {
            memset(SPT.base, 0, SPT.end - SPT.base);
            SPT.reset = 1;
        } else {
            memset(SPT.base, 0, spt_min(sizeof buf, SPT.end - SPT.base));
        }
        /**
            一顿操作base nul end。终于通过memcpy将我们的新进程名设置进去了
            其实我觉得nul用的没有意义...
            也可能是有序的代码里面会用到?(如果有的话以后更新)
        */
        len = spt_min(len, spt_min(sizeof buf, SPT.end - SPT.base) - 1);
        memcpy(SPT.base, buf, len);
        nul = &SPT.base[len];
    
        if (nul < SPT.nul) {
            *SPT.nul = '.';
        } else if (nul == SPT.nul && &nul[1] < SPT.end) {
            *SPT.nul = ' ';
            *++nul = '\0';
        }
    
        return;
    error:
        SPT.error = error;
    } /* setproctitle() */
    

    总结起来就是:

    1. 重新修改全局environ指针指向
    2. 重新修改除argv[0]以外的argv指针指向
    3. 扩容argv[0]
    4. 通过修改argv[0]的内容设置进程名
    
    不过有一点很费解的是, 理论上来说重新修改argv[0]的指针指向也可以做到修改进程名, 但是
    我测试了一下发现不行, 猜测可能系统kernel在创建进程时缓存了argv的const指针,所以在修改
    argv[0]的指针值时不会生效, 这段验证只是猜测, 未经证实, 有待探究。
    

    最后, 从redis中偷师, 用c++11写了一个header only的设置进程名的小库:
    setproctitle
    同时该工程下还有以下两个小玩意儿:
    c++11线程池 c++11 std::functional lambda
    c++11时间库 跨平台, 几乎能支持所有你的习惯来操作时间~

    部分setproctitle:

    #ifndef _UTIL_H_
    #define _UTIL_H_
    
    #include <string>   //strlen(2) strdup(1)
    #include <math.h> //min(1)
    
    /**
     * origin: 进程原始名
     * base: char数组, 存储修改后的进程名
     * nul:
     * end: 
     * 
    */
    typedef struct _PROC_TITLE_INFO{
        const char * origin;
        char * base, *nul, *end;
    }PTI, *PPTI;
    extern char ** environ;
    
    class Util{
    private:
        Util() = default;
        ~Util() = default;
    
    public:
        /**
         * gloabl init function, keep it be called only once
         * 
        */
        static PPTI init_proc_title_info(int argc, char ** argv){
            auto ** envp = environ;
            auto base = argv[0];
            if(nullptr == base){
                return nullptr;
            }
            char * end = (&base[strlen(base)]) + 1; // +1 for '\0'
            for (auto idx = 0; idx < argc || (idx >= argc && nullptr != argv[idx]); ++idx){
                // 不知道redis为什么这样写, 我猜可能是遇到特殊的argv, 待探索
                if(nullptr == argv[idx] || argv[idx] < end){    //注意这里比较的都是指针,不涉及到数据
                    continue;
                }
                end = argv[idx] + strlen(argv[idx]) + 1; // +1 for '\0'
            }
    
            for (auto idx = 0; nullptr != envp[idx]; ++idx){
                if (envp[idx] < end){   //注意这里比较的都是指针,不涉及到数据
                    continue;
                }
                end = envp[idx] + strlen(envp[idx]) + 1;    // +1 for '\0'
            }
            PPTI ppti = new PTI();
            ppti->origin = strdup(argv[0]);
            if(nullptr == ppti->origin){
                delete ppti;
                ppti = nullptr;
                return ppti;
            }
        #ifdef __GLIBC__
            //TODO
        #elif __APPLE__
            auto name = strdup(getprogname());
            if(nullptr == name){
                free(const_cast<char *>(ppti->origin));
                delete ppti;
                ppti = nullptr;
                return ppti;
            }
            setprogname(name);
        #endif
            if( !set_new_env(environ) ){
                free(const_cast<char *>(ppti->origin));
                delete ppti;
                ppti = nullptr;
                return ppti;
            }
    
            if( !set_new_argv(argc, argv) ){
                free(const_cast<char *>(ppti->origin));
                delete ppti;
                ppti = nullptr;
                return ppti;
            }
            ppti->base = base;
            ppti->end = end;
            return ppti;
        }
    
        static bool set_proc_title(const PPTI ppti, const char * fmt, ...){
            if(nullptr == ppti){
                return false;
            }
            if(nullptr == ppti->base){
                return false;
            }
    
            char buf[256] = {0};
            va_list ap;
            auto len = 0;
            if(nullptr != fmt){
                va_start(ap, fmt);
                len = vsnprintf(buf, sizeof buf, fmt, ap);
                va_end(ap);
            }else{
                len = snprintf(buf, sizeof buf, "%s", ppti->origin);
            }
            if(len <= 0){
                return false;
            }
            memset(ppti->base, 0, len + 1);
            memcpy(ppti->base, buf, len);
            return true;
        } 
        
    
    
    private:
        template<class T>
        static T min(T && l, T && r){
            return l > r ? r : l;
        }
    
        static bool set_new_argv(int argc, char ** argv){
            for(auto idx = 1; idx < argc || (idx >= argc && nullptr != argv[idx]); ++idx){
                if(nullptr == argv[idx]){
                    continue;
                }
                auto arg = strdup(argv[idx]);
                if(nullptr == arg){
                    return false;
                }
                argv[idx] = arg;
            }
            return true;
        }
    
        static bool set_new_env(char ** old_env){
            extern char ** environ;
            if( environ != old_env){
                return true;
            }
            if(! clear_env()){
                environ = old_env;
                return false;
            }
            char * eq = nullptr;
            for (auto idx = 0; nullptr != old_env[idx]; ++idx){
                eq = strchr(old_env[idx], '=');
                if(nullptr == eq){
                    continue;
                }
                *eq = '\0';
                int result = setenv(old_env[idx], eq + 1, true);
                *eq = '=';
                if(0 != result){
                    environ = old_env;
                    return false;
                }
            }
            return true;
        }
    
        static bool clear_env(void){
        #ifdef __GLIBC__
            //TODO
        #else
            extern char ** environ;
    
            /**
             * 相当于 char * arr[1] = {nullptr};
             * static char ** temp_env = statc_cast<char **>(arr);
             * 其主要目的是为了使environ变成空置,从而使environ失效
             * 关于这个函数的实现请点击下面的链接查看redis作者对其的说明和吐槽
             * For discussion on the portability of the various methods, see
             * http://lists.freebsd.org/pipermail/freebsd-stable/2008-June/043136.html
             */
            static char ** temp_env = static_cast<char **>(malloc(sizeof *temp_env));
            if(nullptr == temp_env){
                return false;
            }
            temp_env[0] = nullptr;
            environ = temp_env;
            return true;
        #endif
        }
    };
    #endif
    

    测试代码:

    #include <iostream>
    #include "./common/Util.hpp"
    
    extern char **environ;
    
    //#define UNUSED(x) (void)x;
    
    int main(int argc, char ** argv){
        auto ppti = Util::init_proc_title_info( argc, argv);
        std::cout << ppti->origin << "-" << ppti->base << std::endl;;
        std::cout << Util::set_proc_title(ppti, "%s-%d", "named", 1) << std::endl;
        std::cout << getprogname() << std::endl;
        std::cout << getprogname() << std::endl;
        std::cout << ppti->end - ppti->base << std::endl;
        std::cin.get();
    }
    
    g++ -Wall -std=c++11 -O3 main.cpp -o test
    ./test
    

    结果:

    ps -ef | grep named
    501 99907 50309   0  1:49AM ttys002    0:00.00 named-1
    q
    quit
    

    相关文章

      网友评论

        本文标题:[0x00]redis里的小秘密:设置进程名

        本文链接:https://www.haomeiwen.com/subject/kfdoeqtx.html