從應用到內核,分析top命令顯示的進程名包含中括號"[]"的含義

背景

在執行top/ps命令的時候,在COMMAND一列,我們會發現,有些進程名被[]括起來了,例如

  PID  PPID USER     STAT   VSZ %VSZ %CPU COMMAND
 1542   928 root     R     1064   2%   5% top
    1     0 root     S     1348   2%   0% /sbin/procd
  928     1 root     S     1060   2%   0% /bin/ash --login
  115     2 root     SW       0   0%   0% [kworker/u4:2]
    6     2 root     SW       0   0%   0% [kworker/u4:0]
    4     2 root     SW       0   0%   0% [kworker/0:0]
  697     2 root     SW       0   0%   0% [kworker/1:3]
  703     2 root     SW       0   0%   0% [kworker/0:3]
   15     2 root     SW       0   0%   0% [kworker/1:0]
   27     2 root     SW       0   0%   0% [kworker/1:1]

本文除了探索top中[]的含義外,更重要的是,我們如何從僅有的信息定位到問題?

從應用代碼到內核代碼,授人以魚不如授人以漁,你覺得呢?

對分析過程不感興趣的童鞋,可以直接跳轉到結論

應用代碼邏輯分析

關鍵字:COMMAND

獲取busybox的源碼後,試試簡單粗暴的檢索關鍵字

[GMPY@12:22 busybox-1.27.2]$grep "COMMAND" -rnw *

結果發現,太多匹配的數據

applets/usage_pod.c:79: printf("=head1 COMMAND DESCRIPTIONS\n\n");
archival/cpio.c:100:      --rsh-command=COMMAND  Use remote COMMAND instead of rsh
docs/BusyBox.html:1655:<p>which [COMMAND]...</p>
docs/BusyBox.html:1657:<p>Locate a COMMAND</p>
docs/BusyBox.txt:93:COMMAND DESCRIPTIONS
docs/BusyBox.txt:112:        brctl COMMAND [BRIDGE [INTERFACE]]
docs/BusyBox.txt:612:    ip  ip [OPTIONS] address|route|link|neigh|rule [COMMAND]
docs/BusyBox.txt:614:        OPTIONS := -f[amily] inet|inet6|link | -o[neline] COMMAND := ip addr
docs/BusyBox.txt:1354:        which [COMMAND]...
docs/BusyBox.txt:1356:        Locate a COMMAND
......

此時我發現,第一次匹配時因爲存在大量非源碼文件,所以顯得很多,那麼我能不能只檢索C文件呢?

[GMPY@12:25 busybox-1.27.2]$find -name "*.c" -exec grep -Hn --color=auto "COMMAND" {} \;

這次結果只有71行,簡單掃了下匹配的文件,有個有意思的發現

......
./shell/ash.c:9707:         if (cmdentry.u.cmd == COMMANDCMD) {
./editors/vi.c:1109:    // get the COMMAND into cmd[]
./procps/lsof.c:31: * COMMAND    PID USER   FD   TYPE             DEVICE     SIZE       NODE NAME
./procps/top.c:626:     " COMMAND");
./procps/top.c:701:     /* PID PPID USER STAT VSZ %VSZ [%CPU] COMMAND */
./procps/top.c:841: strcpy(line_buf, HDR_STR " COMMAND");
./procps/top.c:854:     /* PID VSZ VSZRW RSS (SHR) DIRTY (SHR) COMMAND */
./procps/ps.c:441:  { 16                 , "comm"  ,"COMMAND",func_comm  ,PSSCAN_COMM    },
......

在busybox中,每一個命令都是單獨一個文件,這代碼邏輯結構好,我們直接進入procps/top.c文件626

函數:display_process_list

procps/top.c626行屬於函數display_process_list,簡單看一下代碼邏輯

static NOINLINE void display_process_list(int lines_rem, int scr_width)
{
    ......
    /* 打印表頭 */
    printf(OPT_BATCH_MODE ? "%.*s" : "\033[7m%.*s\033[0m", scr_width,
        "  PID  PPID USER     STAT   VSZ %VSZ"
        IF_FEATURE_TOP_SMP_PROCESS(" CPU")
        IF_FEATURE_TOP_CPU_USAGE_PERCENTAGE(" %CPU")
        " COMMAND");

    ......
    /* 遍歷每一個進程對應的描述 */
    while (--lines_rem >= 0) {
        if (s->vsz >= 100000)
            sprintf(vsz_str_buf, "%6ldm", s->vsz/1024);
        else
            sprintf(vsz_str_buf, "%7lu", s->vsz);
        /*打印每一行中除了COMMAND之外的信息,例如PID,USER,STAT等 */
        col = snprintf(line_buf, scr_width,
                "\n" "%5u%6u %-8.8s %s%s" FMT
                IF_FEATURE_TOP_SMP_PROCESS(" %3d")
                IF_FEATURE_TOP_CPU_USAGE_PERCENTAGE(FMT)
                " ",
                s->pid, s->ppid, get_cached_username(s->uid),
                s->state, vsz_str_buf,
                SHOW_STAT(pmem)
                IF_FEATURE_TOP_SMP_PROCESS(, s->last_seen_on_cpu)
                IF_FEATURE_TOP_CPU_USAGE_PERCENTAGE(, SHOW_STAT(pcpu))
        );
        /* 關鍵在這,讀取cmdline */
        if ((int)(col + 1) < scr_width)
            read_cmdline(line_buf + col, scr_width - col, s->pid, s->comm);
        ......
    }
}

剔除無關代碼後,函數邏輯就清晰了

  1. 在此函數之前的代碼中已經遍歷了所有進程,並構建了描述結構體
  2. 在display_process_list中遍歷描述結構體,並按規定順序打印信息
  3. 通過read_cmdline,獲取並打印進程名

我們進入到函數read_cmdline

函數:read_cmdline

void FAST_FUNC read_cmdline(char *buf, int col, unsigned pid, const char *comm)
{
    ......
    sprintf(filename, "/proc/%u/cmdline", pid);
    sz = open_read_close(filename, buf, col - 1);
    if (sz > 0) {
        ......
        while (sz >= 0) {
            if ((unsigned char)(buf[sz]) < ' ')
                buf[sz] = ' ';
            sz--;
        }
        ......
        if (strncmp(base, comm, comm_len) != 0) {
            ......
            snprintf(buf, col, "{%s}", comm);
            ......
    } else {
        snprintf(buf, col, "[%s]", comm ? comm : "?");
    }
}

剔除無關代碼後,我發現

  1. 通過/proc/<PID>/cmdline獲取進程名
  2. 如果/proc/<PID>/cmdline爲空時,則使用comm,此時用[]括起來
  3. 如果cmdline的basename與comm不一致,則用{}括起來

爲了方便閱讀,不再展開分析cmdlinecomm

我們把問題聚焦在,什麼情況下,/proc/<PID>/cmdline爲空?

內核代碼邏輯分析

關鍵字:cmdline

/proc掛載的是proc,一種特殊的文件系統,cmdline也肯定是其特有的功能,

假設我們是內核小白,此時我們可以做的就是 在內核proc源碼中檢索關鍵字cmdline

[GMPY@09:54 proc]$cd fs/proc && grep "cmdline" -rnw *

發現有兩個關鍵的匹配文件 base.ccmdline.c

array.c:11: * Pauline Middelink :  Made cmdline,envline only break at '\0's, to
base.c:224: /* Check if process spawned far enough to have cmdline. */
base.c:708: * May current process learn task's sched/cmdline info (for hide_pid_min=1)
base.c:2902:    REG("cmdline",    S_IRUGO, proc_pid_cmdline_ops),
base.c:3294:    REG("cmdline",   S_IRUGO, proc_pid_cmdline_ops),
cmdline.c:26:   proc_create("cmdline", 0, NULL, &cmdline_proc_fops);
Makefile:16:proc-y  += cmdline.o
vmcore.c:1158:   * If elfcorehdr= has been passed in cmdline or created in 2nd kernel,

cmdline.c的代碼邏輯非常簡單,很容易發現其是/proc/cmdline的實現,並不是我們的需求

讓我們把目光聚焦到base.c,相關代碼

REG("cmdline",   S_IRUGO, proc_pid_cmdline_ops),

經驗的直覺告訴我,

  1. cmdline:是文件名
  2. S_IRUGO:是文件權限
  3. proc_pid_cmdline_ops:是文件對應的操作結構體

果不其然,進入proc_pid_cmdline_ops我們發現其定義爲

static const struct file_operations proc_pid_cmdline_ops = {
    .read   = proc_pid_cmdline_read,
    .llseek = generic_file_llseek,
}

函數:proc_pid_cmdline_read

static ssize_t proc_pid_cmdline_read(struct file *file, char __user *buf,
                size_t _count, loff_t *pos)
{
    ......
    /* 獲取進程對應的虛擬地址空間描述符 */
    mm = get_task_mm(tsk);
    ......
    /* 獲取argv的地址和env的地址 */
    arg_start = mm->arg_start;
    arg_end = mm->arg_end;
    env_start = mm->env_start;
    env_end = mm->env_end;
    ......
    while (count > 0 && len > 0) {
        ......
        /* 計算地址偏移 */
        p = arg_start + *pos;
        while (count > 0 && len > 0) {
            ......
            /* 獲取進程地址空間的數據 */
            nr_read = access_remote_vm(mm, p, page, _count, FOLL_ANON);
            ......
        }
    }
}

小白此時可能就疑惑了,你怎麼知道access_remote_vm是幹嘛的?

很簡單,跳轉到access_remote_vm函數中,可以看到此函數是有註釋的

/**
 * access_remote_vm - access another process' address space
 * @mm:         the mm_struct of the target address space
 * @addr:       start address to access
 * @buf:        source or destination buffer
 * @len:        number of bytes to transfer
 * @gup_flags:  flags modifying lookup behaviour
 *
 * The caller must hold a reference on @mm.
 */
int access_remote_vm(struct mm_struct *mm, unsigned long addr,
        void *buf, int len, unsigned int gup_flags)
{
    return __access_remote_vm(NULL, mm, addr, buf, len, gup_flags);
}

Linux內核源碼中,很多函數都有很規範的功能說明,參數說明,注意事項等等,我們要充分利用這些資源學習代碼。

扯遠了,讓我們回到主題上。

proc_pid_cmdline_read中我們發現,讀/proc/<PID>/cmdline實際上就是讀取arg_start開始的的地址空間數據。所以,當這地址空間數據爲空時,當然就讀不到任何數據了。那麼問題來了,什麼時候arg_start標識的地址空間數據爲空?

關鍵字:arg_start

地址空間相關的,絕對不僅僅是proc的事兒,我們試着在內核源碼全局檢索關鍵字

[GMPY@09:55 proc]$find -name "*.c" -exec grep --color=auto -Hnw "arg_start" {} \;

匹配不少,不想一個一個看,且從檢索出來的代碼找不到方向

./mm/util.c:635:    unsigned long arg_start, arg_end, env_start, env_end;
......
./kernel/sys.c:1747:        offsetof(struct prctl_mm_map, arg_start),
......
./fs/exec.c:709:    mm->arg_start = bprm->p - stack_shift;
./fs/exec.c:722:    mm->arg_start = bprm->p;
......
./fs/binfmt_elf.c:301:  p = current->mm->arg_end = current->mm->arg_start;
./fs/binfmt_elf.c:1495: len = mm->arg_end - mm->arg_start;
./fs/binfmt_elf.c:1499:                (const char __user *)mm->arg_start, len))
......
./fs/proc/base.c:246:   len1 = arg_end - arg_start;
......

但是從匹配的文件名給了我靈感:

/proc/<PID>/cmdline是每個進程的屬性,從task_structmm_struct都是描述進程以及相關資源,那什麼時候會修改到arg_start所在的mm_struct呢?進程初始化的時候!

進一步聯想到在用戶空間創建進程不外乎兩個步驟:

  1. fork
  2. exec

在fork時只是創建新的task_struct,父子進程共用一份mm_struct,只有在exec的時候,纔會獨立出mm_struct,所以arg_start一定是在exec時被修改!而匹配arg_start的文件中,剛好有exec.c

查看了fs/exec.c中關鍵字所在函數setup_arg_pages後,並沒找到關鍵代碼,於是繼續查看匹配的文件名,產生了進一步聯想:

exec執行一個新的程序,實際是加載新程序的bin文件,關鍵字匹配的文件中剛好也有binfmt_elf.c

定位問題不僅僅要看得懂代碼,聯想有時候也是非常有效的

函數:create_elf_tables

binfmt_elf.c中匹配關鍵字arg_start的是函數create_elf_tables,函數挺長,我們精簡一下

static int
create_elf_tables(struct linux_binprm *bprm, struct elfhdr *exec,
        unsigned long load_addr, unsigned long interp_load_addr)
{
    ......
    /* Populate argv and envp */
    p = current->mm->arg_end = current->mm->arg_start;
    while (argc-- > 0) {
        ......
        if (__put_user((elf_addr_t)p, argv++))
            return -EFAULT;
        ......
    }
    ......
    current->mm->arg_end = current->mm->env_start = p;
    while (envc-- > 0) {
        ......
        if (__put_user((elf_addr_t)p, envp++))
            return -EFAULT;
        ......
    }
    ......
}

在此函數中,實現了把argv和envp方別存入arg_startenv_start的地址空間。

接下來,我們試試溯本逐源,一起追溯函數create_elf_tables的調用

首先,create_elf_tables聲明爲static,表示其有效範圍不可能超過所在文件。在文件中檢索,發現上級函數爲

static int load_elf_binary(struct linux_binprm *bprm)

竟然還是static,進而繼續在本文件中檢索load_elf_binary,找到了以下代碼:

static struct linux_binfmt elf_format = {
    .module         = THIS_MODULE,
    .load_binary    = load_elf_binary,
    .load_shlib     = load_elf_library
    .core_dump      = elf_core_dump,
    .min_coredump   = ELF_EXEC_PAGESIZE,
};

static int __init init_elf_binfmt(void)
{
    register_binfmt(&elf_format);
    return 0;
}

core_initcall(init_elf_binfmt);

檢索到這裏,代碼結構非常清晰了,load_elf_binary函數賦值於struct linux_binfmt,通過`register_binfmt向上層註冊,提供上層回調。

關鍵字:load_binary

爲什麼要鎖定關鍵字load_binary呢?既然.load_binary = load_elf_binary,,表示上層的調用應該是XXX->load_binary(...),因此鎖定關鍵字load_binary即可定位,哪裏調用了此回調。

[GMPY@09:55 proc]$ grep "\->load_binary" -rn *

非常幸運,此回調只有fs/exec.c調用

fs/exec.c:78:   if (WARN_ON(!fmt->load_binary))
fs/exec.c:1621:     retval = fmt->load_binary(bprm);

進入fs/exex.c的1621行,歸屬於函數search_binary_handler,而不幸的是EXPORT_SYMBOL(search_binary_handler);的存在,表示很可能此函數會有多處被調用,此時繼續正向分析顯然非常困難,爲什麼不試試逆向分析呢?

道路走不通的時候,換個角度看問題,答案就在眼前

既然從search_binary_handler繼續分析不容易,我們不妨看看execve的系統調用是否可以一步步到search_binary_handler?

關鍵字:exec

在Linux-4.9上,系統調用的定義一般是SYSCALL_DEFILNE<參數數量>(<函數名>...,因此我們全局檢索關鍵字,先確定系統調用定義在哪裏?

[GMPY@09:55 proc]$ grep "SYSCALL_DEFINE.*exec" -rn *

定位到文件fs/exec.c

fs/exec.c:1905:SYSCALL_DEFINE3(execve,
fs/exec.c:1913:SYSCALL_DEFINE5(execveat,
fs/exec.c:1927:COMPAT_SYSCALL_DEFINE3(execve, const char __user *, filename,
fs/exec.c:1934:COMPAT_SYSCALL_DEFINE5(execveat, int, fd,
kernel/kexec.c:187:SYSCALL_DEFINE4(kexec_load, unsigned long, entry, unsigned long, nr_segments,
kernel/kexec.c:233:COMPAT_SYSCALL_DEFINE4(kexec_load, compat_ulong_t, entry,
kernel/kexec_file.c:256:SYSCALL_DEFINE5(kexec_file_load, int, kernel_fd, int, initrd_fd,

後面跟進函數的調用不再累贅,總結其調用關係爲

execve -> do_execveat -> do_execveat_common -> exec_binprm -> search_binary_handler

終究是迴歸到了search_binary_handler

分析到這,我們確定了賦值邏輯:

  1. execve執行新程序時,會初始化mm_struct
  2. execve中傳遞的argvenvp保存到arg_startenv_start指定的地址中
  3. cat /proc/<PID>/cmdline時則從arg_start的虛擬地址獲取數據

因此,只要是用戶空間創建的進程經過execve的系統調用,都會有/proc/<PID>/cmdline,但依然沒澄清,什麼時候會cmdline會爲空?

我們知道,在Linux中,進程可分爲用戶空間進程和內核空間進程,既然用戶空間進程cmdline非空,我們再看看內核進程。

函數:kthread_run

內核驅動中,經常通過kthread_run創建內核進程,我們以此函數爲切入口,分析創建內核進程時,是否會賦值cmdline?

直接從kthread_run開始,跟蹤調用關係,發現真正幹活的是函數__kthread_create_on_node

kthread_run -> kthread_create -> kthread_create_on_node -> __kthread_create_on_node

去掉冗餘代碼,專注於函數做了什麼

static struct task_struct *__kthread_create_on_node(int (*threadfn)(void *data),
                void *data, int node, const char namefmt[], va_list args)
{
    /* 把新進程相關的屬性存於 kthread_create_info 的結構體中 */
    struct kthread_create_info *create = kmalloc(sizeof(*create), GFP_KERNEL);
    create->threadfn = threadfn;
    create->data = data;
    create->node = node;
    create->done = &done;
    
    /* 把初始化後的create加入到鏈表,並喚醒kthreadd_task進程來完成創建工作 */
    list_add_tail(&create->list, &kthread_create_list);
    wake_up_process(kthreadd_task);
    /* 等待創建完成 */
    wait_for_completion_killable(&done)
    
    ......

    task = create->result;
    if (!IS_ERR(task)) {
        ......
        /* 創建後,設置進程名,此處的進程名屬性爲comm,不同於cmdline */
        vsnprintf(name, sizeof(name), namefmt, args);
        set_task_comm(task, name);
        ......
    }
}

分析方法跟上文相似,不在累述。總結來說,函數做了兩件事

  1. 喚醒進程kthread_task來創建新進程
  2. 設置進程的屬性,其中屬性包括comm,但不包括cmdline

回顧用戶代碼分析,如果/proc/<PID>/cmdline爲空時,則使用comm,此時用[]括起來**

因此,經過kthread_run/ktrhread_create創建的內核進程,/proc/<PID>/cmdline內容爲空

總結

本文以topps命令中顯示的進程名是否含[]爲切入點,從用戶程序到內核代碼深入分析實現原理。

在本次分析過程中,主要用了以下幾種分析方法

  1. 關鍵字檢索 - 從top程序的COMMAND到內核源碼的arg_start、load_binary、exec
  2. 函數註釋 - 函數access_remote_vm的功能說明
  3. 聯想 - 從進程屬性聯想到用戶空間創建進程,進而定位到arg_start關鍵字的處理函數
  4. 逆向思維 - 從search_binary_handler向上推導調用關係困難,改爲分析execve的系統調用是否可以一步步到search_binary_handler?

根據本次分析,我們得出以下結論

1. 用戶空間創建的進程在top/ps顯示不需要[]
2. 內核空間創建的進程在top/ps顯示會有[]

從實際的ps結果來看,符合上述的分析結果。

由於能力有限,如果上述分析不夠嚴謹的地方,希望一起學習討論

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