初学rust,没有途径修改argv[0]
Tag argv0, rust, 进程标题, on by view 31

我们知道在C语言程序中,可以通过修改argv[0]的值,来实现改变一个进程在ps命令中显示的标题,先给一个C语言的demo如下:

#include <stdio.h>
#include <string.h>
  
extern char **environ;
  
int main(int argc , char *argv[]) {
    int i;
  
    printf("argc:%d\n" , argc);
    for (i = 0; i < argc; i++) {
        printf("argv[%d]:%s\t0x%x\n" , i , argv[i], argv[i]);
    }
  
    for (i = 0; i < argc && environ[i]; i++) {
        printf("evriron[%d]:%s\t0x%x\n" , i , environ[i], environ[i]);
    }

    strcpy(argv[0], "nginx: process is shuting down");

    sleep(1000);
    return 0;
}

进程原本的名称是demo,但是我们通过strcpy修改了argv[0]之后,ps命令显示进程的名称变为我们指定的nginx: process is shuting down,如下

➜  build git:(master) ✗ ps -ef | grep nginx
root     1263942 1252322  0 15:29 pts/2    00:00:00 nginx: process is shuting down
root     1263973 1253542  0 15:29 pts/3    00:00:00 grep --color=auto --exclude-dir=.bzr --exclude-dir=CVS --exclude-dir=.git --exclude-dir=.hg --exclude-dir=.svn --exclude-dir=.idea --exclude-dir=.tox nginx

这也是nginx程序实现在修改进程标题为nginx: master, nginx: worker 以及 nginx: process is shuting down 的原理,从而实现在ps的标题中标识不同类别的进程。

我们在rust语言中如何达到相同的效果呢,查阅资料了解到,通过下面方法可以修改进程名

use nix::libc::{PR_SET_NAME, prctl};
use std::ffi::CString;

fn main() {
    let new_name = CString::new("NewProcessName").expect("CString::new failed");
    unsafe {
        prctl(PR_SET_NAME, new_name.as_ptr() as usize, 0, 0, 0);
    }
}

但是实际使用后,发现,这种方法修改的进程名并不是ps命令显示的进程标题,ps命令显示的进程标题还是不变,而是修改了pstree命令显示的进程树种的进程名称,所以,这种方法并不能达到我们想要的效果。

我们尝试修改argv[0],在rust中是通过env::args()来获取程序的传参,也即argv,追踪到env::args()调用的是env::args_os(),于是我们有这么一段代码尝试修改argv[0]:

use std::env;
use std::ffi::{CStr, CString, OsString};
use std::os::unix::ffi::OsStringExt;

fn set_process_title(title: &str) {
    let args: Vec<OsString> = env::args_os().collect();
    let mut argv: Vec<*mut i8> = args.iter()
        .map(|arg| {
            let arg_cstring = CString::new(arg.as_bytes()).expect("Failed to create CString");
            arg_cstring.into_raw()
        })
        .collect();
    argv.push(std::ptr::null_mut());

    let title_cstring = CString::new(title).expect("Failed to create CString");

    unsafe {
        strcpy(argv[0] as *mut c_char, title_cstring.as_ptr());
    }
}

fn main() {
    set_process_title("MyWorker");

    // 继续执行其他操作...
}

但是很遗憾,并没有修改argv[0]的效果,继续追踪args_os()发现,其实在多个地方存在clone()操作,我们获取到的argv早就不是原始的argvargs_os()的实现如下

#[stable(feature = "env", since = "1.0.0")]
pub fn args_os() -> ArgsOs {
    ArgsOs { inner: sys::args::args() }
}

sys::args::args()的实现如下

/// Returns the command line arguments
pub fn args() -> Args {
    imp::args()
}

imp::args()的实现如下

pub fn args() -> Args {
    Args { iter: clone().into_iter() }
}

这里clone()实现如下

fn clone() -> Vec<OsString> {
    unsafe {
        // Load ARGC and ARGV, which hold the unmodified system-provided
        // argc/argv, so we can read the pointed-to memory without atomics
        // or synchronization.
        //
        // If either ARGC or ARGV is still zero or null, then either there
        // really are no arguments, or someone is asking for `args()`
        // before initialization has completed, and we return an empty
        // list.
        let argv = ARGV.load(Ordering::Relaxed);
        let argc = if argv.is_null() { 0 } else { ARGC.load(Ordering::Relaxed) };
        let mut args = Vec::with_capacity(argc as usize);
        for i in 0..argc {
            let ptr = *argv.offset(i) as *const libc::c_char;

            // Some C commandline parsers (e.g. GLib and Qt) are replacing already
            // handled arguments in `argv` with `NULL` and move them to the end. That
            // means that `argc` might be bigger than the actual number of non-`NULL`
            // pointers in `argv` at this point.
            //
            // To handle this we simply stop iterating at the first `NULL` argument.
            //
            // `argv` is also guaranteed to be `NULL`-terminated so any non-`NULL` arguments
            // after the first `NULL` can safely be ignored.
            if ptr.is_null() {
                break;
            }

            let cstr = CStr::from_ptr(ptr);
            args.push(OsStringExt::from_vec(cstr.to_bytes().to_vec()));
        }

        args
    }
}

可以看到argv是从ARGV.load(Ordering::Relaxed);中加载出来的。可以看到argv经历了多次拷贝,最终才到args,然后通过args_os()再呈现在我们面前,实际早就不再是最初的那个argv。


关于kill进程链
Tag 进程, 子进程, kill, on by view 2792

之前在做 Goj 项目的时候执行服务器部分使用 go 语言 kill 进程,发现无法 kill 该进程的子进程,后来发现貌似别的语言也无法直接 kill 掉当前进程的子进程链。kill 子进程链的应用场景是这样的,我在做 judger服务器的时候发现我调用 gcc 编译源码会在某些极端的情况下卡死,然后卡死后我需要 kill 掉 gcc 的进程,我使用 go 语言的相关 api kill 掉 gcc (因为我拿得到 gcc 的进程 id),然而,我发现 gcc 会调用别的命令,记忆中貌似是一个叫做 ar 的命令,实际上是这个子进程卡死了,于是我 kill 掉身为父进程的 gcc 之后,它的子进程依然存在,变成了孤儿进程,继续卡着。但是我没有 api 可以拿到 gcc 卡死的子进程的进程 id,所以我也无法 kill 掉它。我当时的临时解决方案是调用 killall 命令去按照名称杀掉它。

之后一次在某个群里提到这件事情,随口问了一句如何获取某个进程的子进程,linux api 中是没有这种 api 的,一位同学提醒了我,你可以从 /proc 目录中查。今天我用 go 语言实现了它,linux 系统中 /proc 目录记录了所有的进程信息,但是这些信息中依然没有记录某个信息的子进程,不过它记录了某个进程的父进程,于是我遍历了 /proc 目录中的所有进程的进程信息,然后根据进程的父代关系构成了一颗进程树,这颗树的顶端必定是 init 进程,于是,我便可以从这颗进程树上从上往下搜索查找出某个进程的子进程了。这样一来,一个简单的进程树便可以让我轻松的查找出某进程的子进程,同样也可以 kill 掉某颗进程树,将当前进程及其子进程 kill 掉已经不再是一件难事了。

如果你对此感兴趣,欢迎参阅源码 https://github.com/gojudge/proc 

最后,我要说明的一点是:世界上没有什么事情是一棵树解决不了的,如果有,那么两颗树一定能够解决。


Goj开发过程中遇到gcc进程变为孤儿进程的问题
Tag Goj, 孤儿进程, deamon, Online Judge, on by view 3603

尽管现在已经放假,没有太多的心思放在开发上,但是Goj项目开发最近一直在缓慢的保持着。最近主要是处理judger与ojsite通信的问题,其过程中并没有遇到什么特殊的问题,TCP以及HTTP协议什么的都不成问题。但是在今天调试提交编译任务时却发现了一个有趣的事情,那就是gcc在特定情况下变成了deamon进程。

事情经过是这样的,但凡judger都必须经历#include "/dev/random"的考验,至於这是什么、为什么我也懒得解释,下面引用知乎上的一段话,感兴趣的自己看全文

小心编译期间的一些“高级功能”,比如 C 的 include 其实是有很多巧妙的用法,试试看在 Linux 下 #include "/dev/random" 或者 #include "/dev/tty" 之类的(这两个东西会把网络上不少二流 OJ 直接卡死……)。

而我的judger是这么解决这个问题的,创建一个线程监控编译子进程,超过一定时间kill。这样原本是没有太大的问题的,而且我也相信这也是一种正确的解决方案,但问题是我并不是直接用judger去调用gcc的,而是将gcc包装到了shell脚本,然后调用sh去执行脚本,这样做也是初期考虑到灵活性而决定的。于是judger的子进程是bash,而gcc是bash的子进程,编译任务阻塞后kill掉的是sh,于是gcc变为了孤儿进程,被init进程收养。

illeage_include.png

orphan.png

这倒不是什么难题,只要将进程改为能够kill掉gcc及其子进程便可解决,但是若是忽略这个问题将会是一个灾难,每一次#include "/dev/random"都将会让服务器产生deamon化的gcc进程,时间一久再多的内存都将会被gcc吃掉。



fork()函数
Tag fork, 进程, on by view 5573

fork函数可以复制一个当前进程。看代码

#include <stdio.h>
int main(void){
    int pid;
    pid=fork();
    if(pid>0){//在父进程
        printf("Parents Process, The son pid: %d\n", pid);
    }else if(0==pid){//在子进程
        printf("Son Process, The pid return: %d\n", pid);
    }else{
        printf("Error!\n");
    };
    for(;1;){
        sleep(1);
    }
    return 0;
}

为了让程序不会运行结束自动退出我使用for死循环和sleep函数将程序阻塞,fork函数会创建一个进程,至於是否创建了进程我们看任务管理器的截图

t1.png

任务管理器中有两个相同名称的进程了,这说明进程创建成功。 看终端运行结果
 t2.png

结果表明程序成功的创建了一个新进程。 fork()函数可以创建一个新进程并且返回子进程的ID,子进程与父进程完全一样,即执行相同的代码可是当子进程执行到fork()函数处的时候返回的值为0,却没有再创建进程;而父进程执行到fork()处创建了这个子进程并且返回了子进程的ID,至於为什么子进程不继续创建子子进程,这个我暂时就不知道了。 后来我又到fork()函数之前添加了一个printf作为标识,代码如下

#include <stdio.h>
int main(void){
    int pid;
    printf("hello\n";);
    pid=fork();
    if(pid>0){//在父进程
        printf("Parents Process, The son pid: %d\n", pid);
    }else if(0==pid){//在子进程
        printf("Son Process, The pid return: %d\n", pid);
    }else{
        printf("Error!\n");
    };
    for(;1;){
        sleep(1);
    }
    return 0;
}

运行,终端截图

t3.png

图中并没有两行hello,这说明,如果子进程是完全复制父进程从代码的第一行开始执行的话子进程也会打印hello,很明显子进程不是这样干的。 再改代码,在fork函数之后添加printf,为了避开if分支就把printf函数添加到for阻塞代码的前一行[14],代码如下

#include <stdio.h>
int main(void){
    int pid;
    printf("hello\n");
    pid=fork();
    if(pid>0){//在父进程
        printf("Parents Process, The son pid: %d\n", pid);
    }else if(0==pid){//在子进程
        printf("Son Process, The pid return: %d\n", pid);
    }else{
        printf("Error!\n");
    };
    printf("world\n");
    for(;1;){
        sleep(1);
    }
    return 0;
}

运行截图

t4.png

是的,结果如我所料,有两个world,这说明fork后的进程并不是从头开始执行的,而是从fork语句处开始执行的。这不就是在fork函数那个点产生了一个分支吗。对于长期在github里面混的我,这让我想起了github项目管理里面的fork,看样子他们的表达是一样的,github的fork截图

t5.png

你理解了fork吗?反正我是懂了……