[0x00]redis裏的小祕密:設置進程名
linux macOS下設置進程名
base on redis source code 5.0.3
在redis server啓動過程中, 有一個宏和一個函數顯得很奇特, 他們是server.c
中main()
函數中的第一個宏和第一個函數, 宏INIT_SETPROCTITLE_REPLACEMENT
和函數spt_init(argc, argv);
。他倆組合在一起的主要功能是在macOS和*nix下設置(修改)redis的各個進程名, 例如redis-aof-rewrite
、redis-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
environ
跟int 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
說完argv
、environ
的內存佈局, 我們就可以開始看redis是如何設置進程名的了, 這裏先直接給出答案: argv[0]裏面對應的就是進程名。但是先彆着急, 想要修改它可也沒那麼容易。因爲什麼? 上面我們說了, argv
、environ
可是內存連續的, 如果你設置了一個新的進程名長度比原來的長, 那麼悲劇即將發生。它將覆蓋argv[0]後面的緩衝區, 這將是致命的 。argv
、environ
在進程運行過程中隨時可能會用到,它們很重要。好了, 下面我們看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.c
中line 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.c
的line 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.c
的line 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.c
的line 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.c
的line 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