背景

在执行top/ps命令的时候,在COMMAND一列,我们会发现,有些进程名被[]括起来了,例如

  PID  PPID USER     STAT   VSZ %VSZ %CPU COMMAND1542   928 root     R     1064   2%   5% top1     0 root     S     1348   2%   0% /sbin/procd928     1 root     S     1060   2%   0% /bin/ash --login115     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);elsesprintf(vsz_str_buf, "%7lu", s->vsz);/*打印每一行中除了COMMAND之外的信息,例如PID,USER,STAT等 */col = snprintf(line_buf, scr_width,"\n" "%5u%6u %-8.8s %s%s" FMTIF_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结果来看,符合上述的分析结果。

由于能力有限,如果上述分析不够严谨的地方,希望一起学习讨论

转载于:https://www.cnblogs.com/gmpy/p/11267949.html

从应用到内核,分析top命令显示的进程名包含中括号[]的含义相关推荐

  1. 转 linux进程内存到底怎么看 剖析top命令显示的VIRT RES SHR值

    引 言: top命令作为Linux下最常用的性能分析工具之一,可以监控.收集进程的CPU.IO.内存使用情况.比如我们可以通过top命令获得一个进程使用了多少虚拟内存(VIRT).物理内存(RES). ...

  2. 剖析top命令显示的VIRT RES SHR值

    http://yalung929.blog.163.com/blog/static/203898225201212981731971/ http://www.fuzhijie.me/?p=741 引 ...

  3. top 命令显示隐藏参数列

    top 命令显示隐藏参数列 例如 键入top,然后键入f,然后键入s,回车后显示 DATA列 o: VIRT  --  Virtual Image (kb)           The  total  ...

  4. Top 命令显示使用 CPU 核心数

    Top 命令显示使用 CPU 核心数 top ->1 [root@silver ~]# top //按下 1,即可显示使用 CPU 核心数

  5. Linux TOP 命令显示详情

    2019独角兽企业重金招聘Python工程师标准>>> TOP: 当前时间 系统已运行的时间 当前登录用户的数量 相应最近5.10和15分钟内的平均负载 可以使用'l'命令切换upt ...

  6. linux显示mem进行排序,linux下top命令显示详解

    linux下的top命令是系统管理员分析系统运行现状的法宝,但是每当top之后,除了几个用得最多的参数,其他数字对于我来说,只是数字而已,完全不明白其具体含义.由此做一次top专题,对这个命令的参数和 ...

  7. top命令显示参数详解

    Linux中top命令详解 1. 前5行参数解析 2.进程信息列表区 3.特殊操作 top命令主要用来查看系统状况,CPU.内存.进程资源占用情况. 使用格式如下: top -d 10 //表示所打开 ...

  8. 一文辨析,性能分析top命令中进程NI和PR

    ✒️分析 Linux 服务器性能,首先想到的命令肯定是 top, 通过它,我们可以看到当前服务器资源使用情况和进程运行资源占用情况. 在查看进程资源占用情况时,有两列大家是最难区分,PR(priori ...

  9. Linux/Unix下的任务管理器-top命令

    Linux/Unix下的任务管理器-top命令 Posted on 2012-07-11 09:14 fengyv 阅读(15453) 评论(1) 编辑 收藏 Windows下的任务管理器虽然不好用( ...

最新文章

  1. Tensorflow实现神经网络及实现多层神经网络进行时装分类
  2. Linux nethack
  3. c 中ajax不起作用,Jquery AJAX調用:$(this)在成功后不起作用
  4. spark RDD底层原理
  5. Jakarta Commons Logging学习笔记
  6. 装饰器,生成器,迭代器
  7. ExtJs Grid 合计 [Ext | GridPanel | GridSummary]
  8. linux下安装erlang,以及cowboy的初步接触的一些环境安装
  9. Ubuntu 安装hadoop 伪分布式
  10. redis数据类型-set集合
  11. 计算机等级考试 设置表格居中,Word表格水平居中怎么设置
  12. salt 安装kubernetes集群3节点
  13. Odoo CRM获福布斯评为《2022最佳开源CRM》
  14. JSP完成添加商品时的图片上传
  15. iOS开发 宏定义,Pch文件的引入以及Header文件和Pch一起的使用方法(不用一直引入相同的头文件了,让你的开发更加快捷)
  16. TF卡里删掉文件后内存没变大_不用第三方,手机自带软件也能清扫内存!教你4个正确清理技巧...
  17. 杀不死你的,终将使你更强大
  18. xp系统迁移到固态硬盘_通过网络轻松传输,将XP迁移到Windows 7
  19. 必须来GeekPwn的十大理由
  20. Android UI 模板

热门文章

  1. 毕设分享 基于STM32的智能水产养殖系统
  2. Webservice技术详解
  3. py2exe 使用简介
  4. 特征值和特征向量在现代控制理论的简单应用
  5. python使用使用对数坐标系 fig, ax = plt.subplots() ax.set_xscale(“log“) ax.set_yscale(“log“)
  6. 05全局配置文件application.properties详解
  7. FileSaver+html2canvas js页面生成图片并下载保存
  8. opencv判断 线夹角_python opencv实现直线检测并测出倾斜角度(附源码+注释)
  9. 众昂矿业刘金海:萤石是氟化工产品的主要前端原材料
  10. 千锋重庆web前端技术分享之前端VSCode常用插件