聊一道口头面试题

我是老李,大家好!一来是最近比较忙,二来是预祝大家春节愉快!

鉴于今天文章内容可能会比较正规一些,所以封面图就也跟着一起正规起来一下。封面人物: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中启动一个程序都是会默认带入命令行参数的,做个实验你们复制走试一下:

#include <sys/prctl.h>
#include <unistd.h>
// 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~千万不要错过代码中的注释!

#include <stdio.h>
#include <unistd.h>
#include <string.h>
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:

#include <stdio.h>
#include <unistd.h>
#include <string.h>
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下面代码:

#include <stdio.h>
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,不过用于演示俨然是没有问题的,即便如此,对于一些新手来说可能还是会非常绕弯儿

#include <stdio.h>
#include <sys/prctl.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#define BUF_SIZE 1024
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第七章节部分内容

本文分享自微信公众号 - 高性能API社区(high-performance-api)

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-10-12

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • RESTful 架构基础

    REST(Representational State Transfer)架构风格是一种世界观,把信息提升为架构中的一等公民。通过 REST 可以实现系统的高性...

    zhisheng
  • PHPstorm配置PHP环境

    缺MSVCR110.dll下载这个:Visual C++ Redistributable for Visual Studio 2012 Update 4 点我下...

    cherishspring
  • PHP-fpm 远程代码执行漏洞(CVE-2019-11043)分析

    国外安全研究员 Andrew Danau在解决一道 CTF 题目时发现,向目标服务器 URL 发送 %0a 符号时,服务返回异常,疑似存在漏洞。

    知道创宇云安全
  • 2016年系统架构师软考案例分析考点

    cwl_java
  • 腾讯云服务器上部署LNMP环境

    最近在学Laravel,同参考文章,本来只是在虚拟机上运行,但现在正好因为手上有腾讯云服务器,所以就直接拿来部署Laravel。

    用户6468650
  • 300万知乎用户数据如何大规模爬取?如何做数据分析?

    很早就有采集知乎用户数据的想法,要实现这个想法,需要写一个网络爬虫(Web Spider)。因为在学习 python,正好 python 写爬虫也是极好的选择,...

    机器学习AI算法工程
  • 令人惊叹的前端路由原理解析和实现方式

    ? 在单页应用如此流行的今天,曾经令人惊叹的前端路由已经成为各大框架的基础标配,每个框架都提供了强大的路由功能,导致路由实现变的复杂。想要搞懂路由内部实现还是...

    腾讯技术工程官方号
  • 相对路径和绝对路径

    根目录下有demo1和images/1.jpg,demo1下有index1.html文件和demo1.1文件夹。demo1.1下有index2.html和2.j...

    于小勇
  • 腾讯云服务器搭建 WordPress站点『图文教程』

    WordPress 是一款常用的搭建个人博客网站软件,该软件使用 PHP 语言开发。您可通过在腾讯云服务器的简单操作部署 WordPress,发布个人博客。

    用户6559734
  • C# 如何获取Url的host以及是否是http

    参考资料:https://sites.google.com/site/netcorenote/asp-net-core/get-scheme-url-host

    跟着阿笨一起玩NET

扫码关注云+社区

领取腾讯云代金券