写点什么

程序启停分析与进程常用 API 的使用

作者:EquatorCoco
  • 2024-01-26
    福建
  • 本文字数:7341 字

    阅读完需:约 24 分钟

进程是程序运行的实例,操作系统为进程分配独立的资源,使之拥有独立的空间,互不干扰。


空间布局


拿 c 程序来说,其空间布局包括如下几个部分:


  1. 数据段(初始化的数据段):例如在函数外的声明,int a = 1

  2. block started by symbol(未初始化的数据段):例如在函数外的声明,int b[10]

  3. 栈:保存局部作用域的变量、函数调用需要保存的信息。例如调用一个函数,保存函数的返回地址、调用者的环境信息,给临时变量分配空间

  4. 堆:动态内存分配

  5. 正文段:CPU 执行的指令,通常是只读并共享的,例如同时打开多个文本编辑器进程,只需要读这一份正文段即可

  6. 命令行参数和环境变量



进程启动和停止


进程启动


strace命令来追一个 c 的 hello world:


root@yielde:~/workspace/code-container/cpp# strace ./test1execve("./test1", ["./test1"], 0xfffffedb4960 /* 25 vars */) = 0
复制代码


man 一下 execve,概括来说,execve()初始化栈、堆、bss、初始化数据段、并且将命令行参数、环境变量放到内存中。可以使用https://elixir.bootlin.com/去追一下源码。


SYSCALL_DEFINE3(execve,		const char __user *, filename,		const char __user *const __user *, argv,		const char __user *const __user *, envp){	return do_execve(getname(filename), argv, envp);}
复制代码


execve 通过 do_execve 来执行,do_execve 又通过do_execveat_common()来做具体的事情,


  1. is_rlimit_overlimit()检查资源使用是否超过限制,struct linux_binprm *bprm;是一个结构体,用于记录命令参数、环境变量、要读入 ELF 程序的入口地址、rlimit 等信息。

  2. bprm = alloc_bprm(fd, filename);为该结构分配内存,然后将 bprm 需要的内容 copy 进来。

  3. 构建好 bprm 后执行bprm_execve函数,函数注释sys_execve() executes a new program.该函数会做一些安全性的检查,然后do_open_execat(fd, filename, flags);打开我们的 ELF 程序(编译好的 test1),执行exec_binprm函数来运行新进程

  4. exec_binprm()->search_binary_handler(),看下该函数的关键部分




static int search_binary_handler(struct linux_binprm *bprm){...//cycle the list of binary formats handler, until one recognizes the image list_for_each_entry(fmt, &formats, lh) { if (!try_module_get(fmt->module)) continue; read_unlock(&binfmt_lock);
retval = fmt->load_binary(bprm);
read_lock(&binfmt_lock); put_binfmt(fmt); if (bprm->point_of_no_return || (retval != -ENOEXEC)) { read_unlock(&binfmt_lock); return retval; } }...} // binfmt_elf.c &formats参数static struct linux_binfmt elf_format = { .module = THIS_MODULE, .load_binary = load_elf_binary, // 匹配到的handler .load_shlib = load_elf_library,#ifdef CONFIG_COREDUMP .core_dump = elf_core_dump, .min_coredump = ELF_EXEC_PAGESIZE,#endif};
复制代码


search_binary_handler()会从&formats参数中为识别到的二进制文件匹配一个 handler,即load_elf_binary(),该函数将 ELF 文件(test)的部分内容读入内存,然后为新的进程设置独立的信息


static int load_elf_binary(struct linux_binprm *bprm){...    retval = begin_new_exec(bprm); // 清理之前程序的相关信息,设置私有信号表,设置线程组等。。...setup_new_exec(bprm); // 为新程序设置内核相关的状态(例如进程名).../* 我们的test使用的是动态链接的解释器,objdump -s test可以看到.interp /lib/ld-linux-aarch64.so.1,加载解释器,返回值elf_entry为解释器的入口地址,内核准备工作完成后交给用户空间,用户空间的入口即elf_entry*/if (interpreter) {		elf_entry = load_elf_interp(interp_elf_ex,					    interpreter,					    load_bias, interp_elf_phdata,					    &arch_state);...}
// 放入新程序的命令行参数、环境列表等内容到新进程内存中,构建bss和初始化数据段等进程空间的内容...retval = create_elf_tables(bprm, elf_ex, interp_load_addr, e_entry, phdr_addr);
...// 内核控制交给用户空间,进入用户空间后会直接进入解释器的入口elf_entry,由解释器加载动态链接库// 最后开始运行用户程序
START_THREAD(elf_ex, regs, elf_entry, bprm->p);
}
复制代码


  1. 现在我们的程序已经交给动态解释器了,解释器将依赖的二进制库链接给 test,然后进入 test 的 entry。通过objdump -d test看一下是通过_start 函数开始执行 test


Disassembly of section .text:
0000000000000600 <_start>:... 62c: 97ffffe5 bl 5c0 <__libc_start_main@plt> 630: 97fffff0 bl 5f0 <abort@plt>
复制代码


  1. 我们继续寻找用户空间程序的入口点,可以通过 gdb 调试来看 Entry point 为 0xaaaaaaaa0600,在此处打断点


root@yielde:~/workspace/code-container/cpp# gdb test(gdb) i fileSymbols from "/root/workspace/code-container/cpp/test".Native process:	Using the running image of child process 336143.	While running this, GDB does not access memory from...Local exec file:	`/root/workspace/code-container/cpp/test', file type elf64-littleaarch64.	Entry point: 0xaaaaaaaa0600
(gdb) b *0xaaaaaaaa0600(gdb) rThe program being debugged has been started already.Start it from the beginning? (y or n) yStarting program: /root/workspace/code-container/cpp/test [Thread debugging using libthread_db enabled]Using host libthread_db library "/lib/aarch64-linux-gnu/libthread_db.so.1".
Breakpoint 2, 0x0000aaaaaaaa0600 in _start ()
(gdb) bt#0 0x0000aaaaaaaa05c0 in __libc_start_main@plt ()#1 0x0000aaaaaaaa0630 in _start ()
复制代码


不出所料,入口点并不是 main,而是_start()将 main 运行需要的 agc,argv 传递给__libc_start_main()


  1. __libc_start_main()初始化线程子系统,注册rtld_finifini来做程序退出后的清理工作,将。然后运行main(),最后在 main return 后调用exit(return值)来处理退出


进程退出


如果进程正常退出,调用 glibc 的exit(),如果异常崩溃或 kill -9 杀死,那么不经过用户程序,直接由内核的do_group_exit()做处理


// main函数return 5;
// 继续strace部分内容exit_group(5) = ?+++ exited with 5 +++
复制代码


exit()->__run_exit_handlers():会执行我们使用atexit()注册的函数(顺序为先注册的后执行)->_exit(int status) -> INLINE_SYSCALL (exit_group, 1, status);最终就是我们通过 strace 看到的系统调用exit_group(status)


SYSCALL_DEFINE1(exit_group, int, error_code){	do_group_exit((error_code & 0xff) << 8);	/* NOTREACHED */	return 0;}
// do_group_exit做真正的退出工作void __noreturndo_group_exit(int exit_code){ ... do_exit(exit_code);}
// do_exit会释放一系列进程使用的资源https://elixir.bootlin.com/linux/latest/C/ident/switch_countvoid __noreturn do_exit(long code){... exit_mm();
if (group_dead) acct_process(); trace_sched_process_exit(tsk);
exit_sem(tsk); exit_shm(tsk); exit_files(tsk); exit_fs(tsk); if (group_dead) disassociate_ctty(1); exit_task_namespaces(tsk); exit_task_work(tsk); exit_thread(tsk);... cgroup_exit(tsk);... // 给父进程发出SIGCHLD信号 exit_notify(tsk, group_dead);... do_task_dead(); }
复制代码


do_task_dead()调用set_special_state(TASK_DEAD);将进程标记为 TASK_DEAD 状态,并调用__schedule(SM_NONE);发起调度让出 CPU,进程完全退出。


  • 进程正常退出与异常终止最终都是通过do_group_exit(),但是正常退出会通过__run_exit_handlers()处理exitat()注册的清理工作,异常终止则直接内核接管退出。


常用系统 API


fork


fork 可以创建新的进程,我们追踪 test 启动的时候就是通过 shell fork 出的子进程。fork 返回两次,我们会用父子进程执行不同的代码分支。


pid_t fork(void);
// 成功:向子进程返回0,向父进程返回子进程的pid。// 失败:返回-1,设置errno// errno:// EAGAIN 超出用户或系统进程数上线// ENOMEM 无法为该进程分配足够的内存空间// ENOSYS 不支持fork调用
复制代码


demo


#include <stdio.h>#include <unistd.h>int main() {  int ret = fork();  if (ret == 0) {    printf("i'm parent\n");  } else if (ret > 0) {    printf("i'm child\n");  } else {    printf("error handle\n");  }  return 0;}
// -------输出---------root@yielde:~/workspace/code-container/cpp# ./test i'm childi'm parent
复制代码


fork 之后


内存的拷贝(copy-on-write)


我们追踪 test 时,执行 execve 之后,会释放掉原有的内存结构,并为新进程准备新的内存空间用来映射 ELF 的信息。fork 之后如果拷贝原有进程的堆、栈、数据段,那么紧接着大部分使用场景就是释放这些内容,这使得 fork 性能不佳,linux 使用 copy-on-write 技术解决该问题:


  1. 将子进程的页表项指向与父进程相同的物理内存页,然后复制父进程的页表项,这样父子进程共用一份物理内存,并且将共用的页表标记为只读。

  2. 如果父子进程中任何一方需要修改页表项,会触发缺页异常,内核会为该页分配物理内存,并复制该内存页,此时父子进程各自拥有了独立的物理页,将两个页表设置为可写。


文件描述符


父子进程的文件描述符被子进程复制,并且父子进程共享文件表项,自然会共享文件偏移量,所以父子进程对文件的读写会互相影响。通过 open 调用时设置FD_CLOSEXEC标志,子进程在执行 exec 家族函数的时候会先关闭该文件描述符


其他复制


  • userid,groupid,有效 userid,有效 groupid

  • 进程组 id、会话 id、tty

  • 工作目录、根目录、sig_mask、FD_CLOSEXEC

  • env、共享内存段、rlimit


不复制


  • 未处理的信号集会被清空

  • 父进程设置的文件锁

  • 未处理的 alarm 会被清除


wait、waitpid、waittid


wait 系列函数用于等待子进程的状态改变(包括子进程终止、子进程收到信号停止、已经停止的子进程被信号唤醒)。如果子进程终止,子进程的 pid、内核栈等并不会被释放,但是子进程运行的内存空间已经被释放,此时子进程无法运行,变为僵尸状态,父进程调用 wait 系函数来获取子进程的退出状态,内核也可以释放子进程相关信息,子进程完全消失。


#include <sys/types.h>#include <sys/wait.h>
pid_t wait(int *wstatus);// 成功返回退出子进程的ID// 失败返回-1设置errno:ECHLD表示没有子进程需要等待。EINTR:被信号中断
复制代码


wait

demo


#include <errno.h>#include <stdio.h>#include <stdlib.h>#include <unistd.h>#include <wait.h>pid_t r_wait(int *stat) {  int ret;  while (((ret = wait(stat)) == -1) && (errno == EINTR))    ;  return ret;}int main() {  int stat;  pid_t pid = fork();  if (pid > 0) {    pid_t child_pid;    int ret = r_wait(&stat);    printf("child pid %d exit with code %d\n", ret,           (stat >> 8) & 0xff);  // 获取子进程的返回值  } else if (pid == 0) {    pid_t child_pid = getpid();    sleep(3);    printf("i'm child, pid: %d\n", child_pid);    exit(10);  } else {    printf("fork failure\n");  }  return 0;}
// ------------------------root@yielde:~/workspace/code-container/cpp# ./test child: i'm child, pid: 398918parent: child pid 398918 exit with code 10parent: no child need to wait
复制代码


使用 wait 存在以下几个问题:


  1. 无法 wait 特定的子进程,只能 wait 所有子进程,然后通过返回值来判断特定的子进程

  2. 如果没有子进程退出,则 wait 阻塞

  3. wait 函数只能等待终止的子进程,如果子进程是停止状态或者从停止状态恢复运行,wait 是无法探知的。


waitpid


pid_t waitpid(pid_t pid, int *wstatus, int options);// pid可以指定等待哪一个子进程的退出,// pid=0等待进程组内任意子进程状态改变// pid=-1与wait()等价// pid<-1,等待进程组为[pid]的所有子进程
// options是一个位掩码,有如下标志// 0:等待终止的子进程// WUNTRACE:可以等待因信号停止的子进程// WCONTINUED:可以等待收到信号恢复运行的子进程// WNOHANG:立即返回0,如果没有与pid匹配的进程,则返回-1并设置errno为ECHILD
复制代码


  1. 直接返回的 status 值是不可用的(wait 也一样),可以通过相关的宏来支持作业控制、子进程正常终止、被信号终止,获取退出状态也是通过宏。man wait查看

  2. waitpid 有个问题就是子进程终止和子进程停止无法独立监控,想要只关心停止而忽略终止是不行的。


waittid


解决了上面两种 wait 函数的问题


int waitid(idtype_t idtype, id_t id, siginfo_t *infop, int options);// idtype:P_PID探测id进程,P_PGID探测进程组为id的进程,P_ALL等待任意子进程忽略id
// infop:保存子进程退出的相关信息
// options:WEXITED等待子进程终止// WSTOPPED等待子进程停止// WCONTINUED等待停止的子进程被信号唤醒运行// WNOHANG与waitpid相同// WNOWAIT,wait和waitpid会将子进程的僵尸状态改变为TASK_DEAD,该标志位只获取信息而不改变子进程状态
复制代码


demo


设置 WNOWAIT 观察子进程的状态


#include <errno.h>#include <stdio.h>#include <stdlib.h>#include <string.h>#include <unistd.h>#include <wait.h>int main() {  int stat;  pid_t pid = fork();  if (pid > 0) {    siginfo_t info;    int ret;    memset(&info, '\0', sizeof(info));    ret = waitid(P_PGID, getpid(), &info, WEXITED | WNOWAIT);    if ((ret == 0) && (info.si_pid == pid)) {      printf("child %d exit, exit event: %d, exit status: %d\n", pid,             info.si_code, info.si_status);    }  } else if (pid == 0) {    sleep(3);    printf("i'm child, pid: %d\n", getpid());    return 10;  } else {    printf("fork failure\n");  }  sleep(15);  return 0;}
// ---------------root@yielde:~/workspace/code-container/cpp/blog_demo# ./test i'm child, pid: 401845child 401845 exit, exit event: 1, exit status: 10sleep ....// 父进程获取到子进程退出信息后,子进程仍然为僵尸状态root 401844 0.0 0.0 2184 776 pts/3 S+ 23:01 0:00 ./testroot 401845 0.0 0.0 0 0 pts/3 Z+ 23:01 0:00 [test] <defunct>

复制代码


system


system 相当于我们 fork 出子进程->子进程执行 exec 执行命令->父进程 waitpid 等待子进程返回,只不过使用 system 时,system 会 fork 出一个 shell,然后 shell 创建子进程来执行命令,因此调用 system 的返回值如下:


  1. 如果 system 内部 fork 失败或 waitpid 返回了除 EINTR 之外的错误,system 返回-1 设置 errno。如果 SIGCHILD 被设置为 SIG_IGN,那么 system 返回-1 并设置 errno 为 ECHLD,无法判断命令是否执行成功

  2. 如果 exec 失败,返回 127(shell 执行失败的指令,可以在 shell 写一个不存在的命令,然后 echo $?看下)

  3. 如果 system 执行成功,会返回 shell 的终止状态,即最后一条命令的退出状态

  4. system(NULL)探测 shell 是否可用,如果返回 0 表示 shell 不可用,返回 1 表示 shell 可用


demo


#include <errno.h>#include <stdio.h>#include <stdlib.h>#include <string.h>#include <unistd.h>
int main() { // int ret = system("lss -l"); //执行错误的命令 // int ret = system("ls -l"); // 正常执行命令 int ret = system("sleep 50"); // 执行命令进程被信号杀死 if (ret == -1) { printf("system return -1, errno is: %s", strerror(errno)); } else if (WIFEXITED(ret) && WEXITSTATUS(ret) == 127) { // WIFEXITED(wstatus) returns true if the child terminated normally(在 man wait中) // WEXITSTATUS(wstatus) returns the exit status of the child printf("shell can't exec the command\n"); } else { if(WIFEXITED(ret)){ printf("normal termination, exit code = %d\n", WEXITSTATUS(ret)); }else if(WIFSIGNALED(ret)){ // WIFSIGNALED(wstatus) returns true if the child process was terminated by a signal. printf("abnormal termination, signal number = %d\n", WTERMSIG(ret)); } }}
复制代码


分别编译测试三种情况:


  1. 让 system 执行一个错误的命令,运行如下


root@yielde:~/workspace/code-container/cpp/blog_demo# ./test sh: 1: lss: not foundshell can't exec the command
复制代码


  1. 让 system 正常执行命令


root@yielde:~/workspace/code-container/cpp/blog_demo# ./test total 40-rw-r--r-- 1 root root 4107 Jan 19 21:16 epoll_oneshot.cc-rw-r--r-- 1 root root 2642 Jan 18 19:44 oob_recv_select.cc-rw-r--r-- 1 root root 1659 Jan 18 22:11 poll.cc-rw-r--r-- 1 root root  739 Jan 25 23:34 system_test.cc-rwxr-xr-x 1 root root 9064 Jan 25 23:34 test-rw-r--r-- 1 root root  795 Jan 25 22:24 wait_test.cc-rw-r--r-- 1 root root  651 Jan 25 23:01 waittid_test.ccnormal termination, exit code = 0
复制代码


  1. 给 system 执行的命令发送 kill -9


//killroot@yielde:~/workspace/code-container/cpp# ps aux|grep sleeproot      403568  0.0  0.0   2304   836 pts/3    S+   23:42   0:00 sh -c sleep 50root      403569  0.0  0.0   5180   788 pts/3    S+   23:42   0:00 sleep 50root      403613  0.0  0.0   5888  2008 pts/1    S+   23:42   0:00 grep --color=auto sleeproot@yielde:~/workspace/code-container/cpp# root@yielde:~/workspace/code-container/cpp# kill -9 403568// 结果root@yielde:~/workspace/code-container/cpp/blog_demo# ./test abnormal termination, signal number = 9
复制代码


文章转载自:佟晖

原文链接:https://www.cnblogs.com/tongh/p/17988449

体验地址:http://www.jnpfsoft.com/?from=001

用户头像

EquatorCoco

关注

还未添加个人签名 2023-06-19 加入

还未添加个人简介

评论

发布
暂无评论
程序启停分析与进程常用API的使用_redis_EquatorCoco_InfoQ写作社区