redis裏的小祕密:設置進程名

原文鏈接:https://www.jianshu.com/p/36c301ac87df

[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 <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
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章