从应用到内核,分析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.c的626行属于函数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);
......
}
}
剔除无关代码后,函数逻辑就清晰了
- 在此函数之前的代码中已经遍历了所有进程,并构建了描述结构体
- 在display_process_list中遍历描述结构体,并按规定顺序打印信息
- 通过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 : "?");
}
}
剔除无关代码后,我发现
- 通过
/proc/<PID>/cmdline
获取进程名 - 如果
/proc/<PID>/cmdline
为空时,则使用comm
,此时用[]
括起来 - 如果
cmdline
的basename与comm
不一致,则用{}
括起来
为了方便阅读,不再展开分析cmdline
和comm
。
我们把问题聚焦在,什么情况下,/proc/<PID>/cmdline
为空?
内核代码逻辑分析
关键字:cmdline
/proc挂载的是proc,一种特殊的文件系统,cmdline也肯定是其特有的功能,
假设我们是内核小白,此时我们可以做的就是 在内核proc源码中检索关键字cmdline
[GMPY@09:54 proc]$cd fs/proc && grep "cmdline" -rnw *
发现有两个关键的匹配文件 base.c 和 cmdline.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),
经验的直觉告诉我,
- cmdline:是文件名
- S_IRUGO:是文件权限
- 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_struct到mm_struct都是描述进程以及相关资源,那什么时候会修改到arg_start所在的mm_struct呢?进程初始化的时候!
进一步联想到在用户空间创建进程不外乎两个步骤:
- fork
- 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_start和env_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
分析到这,我们确定了赋值逻辑:
- 在
execve
执行新程序时,会初始化mm_struct
- 把
execve
中传递的argv和envp保存到arg_start和env_start指定的地址中 - 在
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);
......
}
}
分析方法跟上文相似,不在累述。总结来说,函数做了两件事
- 唤醒进程
kthread_task
来创建新进程 - 设置进程的属性,其中属性包括comm,但不包括cmdline
回顾用户代码分析,如果/proc/<PID>/cmdline
为空时,则使用comm,此时用[]括起来**
因此,经过kthread_run/ktrhread_create创建的内核进程,/proc/<PID>/cmdline
内容为空
总结
本文以top
、ps
命令中显示的进程名是否含[]
为切入点,从用户程序到内核代码深入分析实现原理。
在本次分析过程中,主要用了以下几种分析方法
- 关键字检索 - 从top程序的COMMAND到内核源码的arg_start、load_binary、exec
- 函数注释 - 函数access_remote_vm的功能说明
- 联想 - 从进程属性联想到用户空间创建进程,进而定位到arg_start关键字的处理函数
- 逆向思维 - 从search_binary_handler向上推导调用关系困难,改为分析execve的系统调用是否可以一步步到search_binary_handler?
根据本次分析,我们得出以下结论
1. 用户空间创建的进程在top/ps显示不需要[]
2. 内核空间创建的进程在top/ps显示会有[]
从实际的ps结果来看,符合上述的分析结果。
由于能力有限,如果上述分析不够严谨的地方,希望一起学习讨论
从应用到内核,分析top命令显示的进程名包含中括号"[]"的含义的更多相关文章
- 转 linux进程内存到底怎么看 剖析top命令显示的VIRT RES SHR值
引 言: top命令作为Linux下最常用的性能分析工具之一,可以监控.收集进程的CPU.IO.内存使用情况.比如我们可以通过top命令获得一个进程使用了多少虚拟内存(VIRT).物理内存(RES). ...
- 剖析top命令显示的VIRT RES SHR值
http://yalung929.blog.163.com/blog/static/203898225201212981731971/ http://www.fuzhijie.me/?p=741 引 ...
- linux进程内存到底怎么看 剖析top命令显示的VIRT RES SHR值
引 言: top命令作为Linux下最常用的性能分析工具之一,可以监控.收集进程的CPU.IO.内存使用情况.比如我们可以通过top命令获得一个进程使用了多少虚拟内存(VIRT).物理内存(RES). ...
- Linux下运行top命令显示的PR\NI\RES\SHR\S\%MEM TIME+都代表什么
PID 进程号 USER 用户名 PR 优先级 NI nice值.负值表示高优先级,正值表示低优先级 RES 进程使用的.未被换出的物理内存大小,单位Kb S 进程状态: D 不可中断的睡眠状态 R ...
- linux分析工具之top命令详解
Linux系统可以通过top命令查看系统的CPU.内存.运行时间.交换分区.执行的线程等信息.通过top命令可以有效的发现系统的缺陷出在哪里.是内存不够.CPU处理能力不够.IO读写过高. 一.top ...
- Top命令 -转
Windows下的任务管理器虽然不好用(个人更喜欢Process Explorer些),但也算方便,可以方便的查看进程,CPU,内存...也可以很容易的结束进程 没有图形化界面下的Linux,也有命令 ...
- Linux中监控命令top命令使用方法详解
收集了两篇关于介绍Linux中监控命令top命令的详细使用方法的文章.总的来说,top命令主要用来查看Linux系统的各个进程和系统资源占用情况,在监控Linux系统性能方面top显得非常有用,下面就 ...
- Top命令内存占用剖析
原文: http://yalung929.blog.163.com/blog/static/203898225201212981731971/ 引 言: top命令作为Linux下最常用的性能分析工具 ...
- top命令总结
top命令主要用来观察和收集运行在系统上的进程的一些有用信息.ps只是一个快照,是ps命令执行的那一瞬间的系统中进程的快照.top则可以用于持续观察. 第一步,在命令行键入top,回车进入top管理界 ...
随机推荐
- 添加Chrome插件时出现“程序包无效”等问题的解决办法
相较之各大浏览器,我最喜欢的便是Chrome了,不只因为Chrome搜索,也因为Google Chrome强大的插件功能. 而这一切的东风,就是"谷歌访问助手". 谷歌访问助手的下 ...
- 牛客网sql刷题解析-完结
查找最晚入职员工的所有信息 解题步骤: 题目:查询最晚入职员工的所有信息 目标:查询员工的所有信息 筛选条件:最晚入职 答案: SELECT *--查询所有信息就用* ...
- 《细说PHP》第四版 样章 第23章 自定义PHP接口规范 6
23.4 API的设计原则和规范 API是服务提供方和使用方之间对接的通道,前面我们设计的一些简单API的例子,基本上比较随意,没有使用任何规范.设想一下,每个平台都可能存在大量的API,如果API ...
- IT兄弟连 Java语法教程 数组 什么是数组
数组是编程语言中最常见的一种数据结构,可用于存储多个数据,每个数组元素存放一个数据,通常可通过数组元素的索引来访问数组元素,包括为数组元素赋值和取出数组元素的值.Java语言的数组则具有其特有的特征, ...
- 使用vue组件需要注意的4个细节
细节1:table(表格)中直接引用自定义组件出现的bug 如上图,tr本应在tbody中面,现在却是同级.造成的原因是h5规定table里必须有tbody,tbody中必须有tr, 当tbody中引 ...
- Oracle基础教程(一)
本文链接:https://blog.csdn.net/GoldenKitten/article/details/84947386 以下内容为转载以上博客,自己做了略微的补充,如需查看原文,请点击上面的 ...
- Solr java.sql.SQLException: null, message from server: "Host 'xxx' is not allowed to connect to this MySQL server
在用solr从mysql导入数据的时候,因为linux和本机的数据库不在同一个ip段上, 又因为本地的mysql没有设置远程其它ip可以访问所以就报了如下错误 解决办法: 在mysql任意可以输入查询 ...
- centos 7 搭建Samba
一.Samba简介 Samba是一个能让Linux系统应用Microsoft网络通讯协议的软件,由客户端和服务端构成. SMB(Server Message Block的缩写,即服务器消息块)主要是作 ...
- 盘点10个CAD难点,看看有没有让你崩溃的,解决方法一并奉上
蜀道难,难于上青天”,对于很多学习CAD的小伙伴来说CAD就跟蜀道一样,太难了,下面小编分享几个在学习CAD过程中会遇到的问题以及解决的方法,一起来看看吧! 1. 如何替换找不到的原文字体? 答:复制 ...
- Shodan搜索引擎在信息搜集中的应用
Shodan搜索引擎在信息搜集中的应用 作者:王宇阳 时间:2019-06-07 soudan(搜蛋),通过互联网后的通道来搜索信息:Google通过网址搜索互联网,shodan搜索互联网的在线.指定 ...