我是老李,大家好!一来是最近比较忙,二来是预祝大家春节愉快!
鉴于今天文章内容可能会比较正规一些,所以封面图就也跟着一起正规起来一下。封面人物:Dennis MacAlistair Ritchie,即丹尼斯里奇,或称D.M.R,Ken Tom的好盆友,C语言与UNIX发明人之一,大爷肉身已不在人世,精神依然长流!
众所周知,很久很久很久很久之前,我曾经去某东讨BD西伐TX南征AL北战快手的宇宙级公司挑战(折磨)过,让我暗爽的是历经摧残苦尽甘来后我依旧主动抛弃了他们,代价据说是(据说是)会被锁定半年,看意思大概就是该简历进入了一个半年的生理不应期。
因为要时时刻刻崇尚工作业余时间自由充足可掌控
昨晚我在写山寨Redis的时候就联想到了当时的一道面试题,仔细琢磨了一下当时虽然回答出来了,但其实并不全面和深入。题面是这样的,你们感受一下(并不是用笔和纸回答的面试题,就是和面试官沟通交流中他随意且随机地问):
Linux下如何为程序设定新的进程名称
那个,这问题听起来是不是很沙雕?但实际上这个问题背后内涵相当丰厚,在我看来他至少涉及到了如下两个面:
*NIX环境变量
exec运行时程序内存分配图
命令行参数
我先说下当时我的回答,脱口而出的那种,一共用了不到3秒钟:
直接修改argv[0]参数
我当时就是这么回答的,这么回答没有错,只是不全面。记得当时我俩就大眼瞪小眼,他问我:完了?我怂怂地点了点头... ...面试官既然问你这个问题,想必是想了解更多【关于你对这些东西的掌握广度面以及深度】,这么聊聊一句现在想来也显得颇为尴尬。
主要是我确实只能想到这个啊
现如今,我打算着手解决一下这个看起来【平淡无奇】的问题,而且根据以往的经验看,我必须也要说下PHP...
一般说来我们,我们输入个ps -ef,就会看到下面这种东西,我截几个图你们感受一下:
比如nginx的进程名
比如Postgres的进程名
比如Swoole服务(可以配置自定义)
比如Workerman的进程名
这么做好处很多,一是识别度很高,二是在grep的时候会很方便,三可能会看起来比较正规(我感觉正规这个词快被我用坏了)... ...
那,我们到直接CVS(Ctrl+C、Ctrl+V、Ctrl+S)阶段?
可能是世界上最好的语言
PHP里非常粗暴地提供了一个叫做cli_set_process_title的函数,不过文档上也明确指出了:此函数用于处理top或ps命令后查看到的进程名,而且此函数只能在PHP cli模式下使用。鉴于我等众雕都是大量使用php-fpm而不是php-cli的人,所以没准真的有很多PHPer压根都没听过cli_set_process_title这个函数。我码个demo吧,你们感受下:
<?php
cli_set_process_title( '某 server master process' );
// 阻塞住昂,退出了进程就没了...
sleep( 1000000 );
然后启动后grep一下【某】字,好使,完美:
不过值得注意的是,这个函数在Mac下被直接干挺了(至于用Windows的佬们,自己试哈)。这个函数在Mac下无法工作也不是一天两天的事儿了,反正你们注意下就行:
在swoole里,官方则是提供了一个swoole_set_process_name的函数来搞定的,这个具体实现我没看源码嫌太麻烦,有兴趣同学可以去看下~
在Workerman里,李亮是这么实现的,我就直接复制过来了哈:
protected static function setProcessTitle($title) {
\set_error_handler(function(){});
// >=php 5.5
if (\function_exists('cli_set_process_title')) {
\cli_set_process_title($title);
} // Need proctitle when php<=5.5 .
elseif (\extension_loaded('proctitle') && \function_exists('setproctitle')) {
\setproctitle($title);
}
\restore_error_handler();
}
嗯,长见识了,合着PHP还有一个叫做proctitle的扩展?
可能是圈里较为古老的语言
这个就比较恶心麻烦了,但实际上也【可能是较为标准】的答案。好了,你们准备一下,我要开始表演了。首先我可以尝试随便瞎写一个C语言程序,比如helloworld:
#include <stdio.h>
#include <unistd.h>
int main() {
printf( "sleep me...\n" );
sleep( 1000 );
return 0;
}
编译一下一跑,大概就是下图这么个结果,我们的任务就是要改动这个玩意:
在Linux中有一个叫做prctl的标准函数,据man页说明这个函数可以调整【调用进程或线程的名称】,可以先尝试一下:
#include <sys/prctl.h>
#include <unistd.h>
int main( int argc, char * argv[] ) {
// 试图把当前进程名调整为 tidis-server
char * process_name = "tidis-server";
prctl( PR_SET_NAME, process_name, NULL, NULL, NULL );
// 保证进程不会退出...不然ps -ef看不到
sleep( 100000 );
return 0;
}
这个编译搞定后(gcc默认的文件名a.out)我们用ps -ef | grep tidis,然而实际上结果为空,去掉grep才注意到,此时进程的名字依然为./a.out;我们用top命令查看,发现进程名也依然是./a.out~~~改名失败了?
后来看手册才知晓,这个函数修改的是*NIX的/procs目录下的一些内容(/proc目录的作用自己手动查下哈),怎么查看下呢?
首先看下./a.out的进程pid是什么,然后直接cat查看/proc/[pid]/stat和/proc/[pid]/status两个文件即可,注意其中的Name应该已经是tidis-server了。
这个函数并不能调整进程在ps和top命令中的进程名,应该是只能调整在/proc/[pid]下的一些数据信息。而且man页上也额外指出如下内容:“ If the length of the string, including the terminating null byte,exceeds 16 bytes, the string is silently truncated. ”,就是说这个函数接受的进程名参数长度最长应该是16字节而且包括末尾的null byte(C语言中字符串最后一个元素是null byte)。但不能说这个函数没用,因为一些工具命令获取信息就是从/proc目录中获取的,比如vmstat等,如果一个查看进程的工具获取数据就是从/proc目录中获取数据的,那么我们就会达到我们想要的结果。
那么,ps和top这两个常用命令中显示的进程名如何修改?此时不得不引入一下命令行参数的概念,实际上cli中启动一个程序都是会默认带入命令行参数的,做个实验你们复制走试一下:
// argc就表示命令参数的个数
// argv是一个指针数组,就是一个数组,里面全是一坨指针,而且是字符串指针
int main( int argc, char * argv[] ) {
printf( "一共收到%d个命令行参数:\n", argc );
for( int i = 0; i < argc ; i++ ) {
printf( "%s\n", argv[ i ] );
}
return 0;
}
运行命令我们用这个尝试下./a.out -h 127.0.0.1 -p 3306,运行结果入下图:
这会儿你在结合我当初那个虎批的回答:直接修改argv[0]参数~上图中的./a.out就是argv[0],同时也是显示在ps、top中的进程名,所以理论上我们修改argv[0]指针指向的字符串内容就应该可以修改进程名,let's rock~千万不要错过代码中的注释!
int main( int argc, char * argv[] ) {
printf( "argv[0]的内存地址:%p\n", argv[ 0 ] );
// 修改argv[0]指针指向的内存中的字符串的内容
// 我知道,一定有,很多人想用 argv[0] = "tidis";但是,这么写,
// 连编译都过不了的,因为argv[0]实际上是一个“指针常量”,
// 你修改不了的~实在get不到这个点,就多想想多看《C与指针》多写!
strcpy( argv[ 0 ], "tidis-server-master-process" );
// 保证进程不会退出
sleep( 100000 );
return 0;
}
编译后run一下,然后结合ps -ef | grep tidis查看一下:
好像成功了???当然没有!不然这文章还怎么编下去... ...我总不能搁一句【老铁们,我实在编不下去了】就全剧终吧?然而现在就是到了尴尬的境地,就是看起来成功了实际上并没有成功,我还得装作什么都不知道继续编下去,你们说难受不难受?好了,改代码!按照下面的COPY:
int main( int argc, char * argv[] ) {
printf( "argv[0]的内存地址:%p\n", argv[ 0 ] );
strcpy( argv[ 0 ], "tidis-server-master-process" );
for ( int i = 0; i < argc; i++ ) {
printf( "argv[%d] : %s\n", i, argv[ i ] );
}
sleep( 100000 );
return 0;
}
编译成功后我们使用./a.out -h 127.0.0.1 -p 3306执行,感受下?
怎么会是这样?!!!
而且你们仔细观察下,好像还有规律。我再改下文件名,估计你们能慢慢回过味儿来,我们把a.out文件名直接修改为tidis-server-master-process,然后再用./tidis-server-master-process -h 127.0.0.1 -p 3306跑一下,怎么样?没问题了吧?图我就不贴了。事情到这里我们得出一个结论:
直接修改argv[0]可以实现目标,但是有个缺陷就是新的进程名长度不可以超过程序的文件名
我们当然可以通过在代码里做条件检测来约束这个问题,但终究不是彻底解决方案。为了能搞明白这个问题,是时候引入程序运行时地址分配图和环境变量的概念了。这里的环境变量就是说你们平时经常从网上复制粘贴的那些linux环境变量,其实就是字符串,配置什么Java环境Golang环境时候你们一定都搞过这个,就是key=value。再一次翻阅APUE,里面倒是给出了命令行参数和环境变量的数据结构,其实就是一大坨char *然后形成了一个char **:
无论是argv还是environ,他们的指针数组最后一个元素一定是NULL;然后是指针指向的内存空间是连续紧挨着的,具体说就是argv在前面,environ环境紧跟在后。看到这里,你们知道为啥前面长长的进程名会出现那个【有规律】的现象了吧?因为argv内存空间是连续,太长会直接覆盖后面单元中数据,如果你愿意动手试下,可以用如下代码读取出一下你的argv和environ,各位看官,请copy下面代码:
extern char **environ;
int main( int argc, char *argv[] ) {
for ( int i = 0; i < argc; i++ ) {
printf( "%s\n", argv[ i ] );
}
printf( "=============================\n" );
int i = 0;
// 注意此处用 ++i 而不是 i++,不然你换下,有惊喜~
while ( environ[ ++i ] ) {
printf( "%s\n", environ[ i ] );
}
return 0;
}
那程序运行时地址分配图是什么玩意?当一个程序在命令行跑起来的时候,实际上相当于exec族系统调用执行了一个程序,就是TA把命令行参数和环境变量透传给main函数的,一个程序的运行时地址分配是这样shai儿的:
注意是【命令行参数与环境变量】
注意,这个里的堆和数据结构中那个堆不是一回事,C中malloc获取内存空间就是从堆内存中获取的。我们的argv们就和环境变量们在一起,一起拥挤在最最最最上面,位于堆内存和栈内存的顶上。现如今我们要在程序运行后调整argv[0]的值,如果新的进程名长度超过了原文件名长度,我们就只能申请新的空间,但是如果想让程序在运行后整体向下移动堆栈、正文段几乎是不可能的,所以我们就可以像Nginx那样这样来实现一下:
申请一块儿新的内存
修改argv[0]内容
把argv[1]一直到最后一个argv[x]以及环境变量放到申请的新内存中
NOTICE:下面这段可供copy的代码,虽然大概率可能存在bug,不过用于演示俨然是没有问题的,即便如此,对于一些新手来说可能还是会非常绕弯儿
extern char **environ;
int main( int argc, char * argv[] ) {
char ** origin_argv;
char ** origin_environ;
char * last_argv;
char * process_name = "tidis-master-process";
int index;
char new_argv[ BUF_SIZE ];
size_t buf_len;
origin_environ = environ;
origin_argv = argv;
// 将argv中除了argv[ 0 ]之外的全部保存到new_argv中
// 以字符串的形式将argv[ 1 ]-argv[ x ]的参数保存到new_argv中
memset( new_argv, '\0', BUF_SIZE );
for ( index = 1; index < argc; index++ ) {
strcat( new_argv, argv[ index ] );
strcat( new_argv, " " );
}
// 将envrion中环境变量保存到new_environ中
int environ_count = 0;
for ( environ_count = 0; environ[ environ_count ] != NULL; environ_count++ )
continue;
// 这句可能需要好好理解一下...
environ = ( char ** )malloc( sizeof( char * ) * ( environ_count + 1 ) );
// 将老的environ逐一copy到新的environ中
for ( index = 0; index < environ_count; index++ ) {
// 这两句的意思就是:先分配好内存空间,然后复制过去
environ[ index ] = ( char * )malloc( sizeof( char ) * strlen( origin_environ[ index ] ) );
strcpy( environ[ index ], origin_environ[ index ] );
}
// 确保environ环境变量最后一个指针为空.
environ[ index ] = NULL;
// 这个逻辑也比较绕,目的是为了获取最后一个argv参数.
// index一般说来,肯定都大于0,因为一定会有环境变量的...
last_argv = index > 0 ? origin_environ[ index - 1 ] + strlen( origin_environ[ index - 1 ] ) :
origin_argv[ argc - 1 ] + strlen( argv[ argc - 1 ] );
// 同时设定argv[ 0 ]和prctl,双重保险.
// 这里意味着,只要命令行参数不超2048即可,如果更长,该这个数值即可...
char argv_buffer[ 2048 ];
size_t argv_buffer_length;
strcpy( argv_buffer, process_name );
strcat( argv_buffer, " " );
strcat( argv_buffer, new_argv );
argv_buffer_length = strlen( argv_buffer );
strcpy( origin_argv[0], argv_buffer );
char * pt_last = &origin_argv[0][ argv_buffer_length ];
while ( pt_last < last_argv )
// 那个..看到这句有崩溃的么...
// 说下哈,++优先级比*高,所以这句就是先产生pt_last的一个拷贝然后执行++,然后*
// 作为左值的含义就是给某一个内存位置存储数值,也就是'\0'
*pt_last++ = '\0';
origin_argv[ 1 ] = NULL;
prctl( PR_SET_NAME, process_name, NULL, NULL, NULL );
printf( "\n\ntidis-server start!\n\n" );
sleep( 100000 );
return 0;
}
好了,这坨代码也TM折腾的我筋疲力尽,不过好在能用,你们感受下:
完美
如果要搞明白涉及上面的内容,三本书离不开:APUE、C与指针、c primer plus。不过话说回来,搞不搞明白这些问题实际上也没有太大意义,不影响砌砖头赚钱 ~ 至于这几本书,他们不属于那种快速阅读快速理解的那种,对付这几本书籍,你需要参考下毛泽东同志的《论持久战》,如果想快速21天精通的,好像不大行... ...
我知道今天内容有点儿枯燥恶心,以后不写这个了...
参考链接与资料:
1. http://lxr.nginx.org/source/src/os/unix/ngx_setproctitle.c
2. https://blog.csdn.net/duyiwuer2009/article/details/8447802
3. APUE第七章节部分内容