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

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 
extern char ** environ;
int main(int argc, char ** argv){
    for (auto idx = 0; idx < argc; ++idx){
        std::cout << static_cast(argv[idx]) << " = " << argv[idx] << std::endl;
    }
    for(auto idx = 0; nullptr != environ[idx]; ++idx){
        std::cout << static_cast(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(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    //strlen(2) strdup(1)
#include  //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(ppti->origin));
            delete ppti;
            ppti = nullptr;
            return ppti;
        }
        setprogname(name);
    #endif
        if( !set_new_env(environ) ){
            free(const_cast(ppti->origin));
            delete ppti;
            ppti = nullptr;
            return ppti;
        }

        if( !set_new_argv(argc, argv) ){
            free(const_cast(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
    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(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(malloc(sizeof *temp_env));
        if(nullptr == temp_env){
            return false;
        }
        temp_env[0] = nullptr;
        environ = temp_env;
        return true;
    #endif
    }
};
#endif

测试代码:

#include 
#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里的小秘密:设置进程名)