经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » 程序设计 » C 语言 » 查看文章
程序启停分析与进程常用API的使用
来源:cnblogs  作者:佟晖  时间:2024/1/26 10:05:30  对本文有异议

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

空间布局

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

  1. 数据段(初始化的数据段):例如在函数外的声明,int a = 1
  2. block started by symbol(未初始化的数据段):例如在函数外的声明,int b[10]
  3. 栈:保存局部作用域的变量、函数调用需要保存的信息。例如调用一个函数,保存函数的返回地址、调用者的环境信息,给临时变量分配空间
  4. 堆:动态内存分配
  5. 正文段:CPU执行的指令,通常是只读并共享的,例如同时打开多个文本编辑器进程,只需要读这一份正文段即可
  6. 命令行参数和环境变量

进程启动和停止

进程启动

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

  1. root@yielde:~/workspace/code-container/cpp# strace ./test1
  2. execve("./test1", ["./test1"], 0xfffffedb4960 /* 25 vars */) = 0

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

  1. SYSCALL_DEFINE3(execve,
  2. const char __user *, filename,
  3. const char __user *const __user *, argv,
  4. const char __user *const __user *, envp)
  5. {
  6. return do_execve(getname(filename), argv, envp);
  7. }

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(),看下该函数的关键部分
  1. static int search_binary_handler(struct linux_binprm *bprm){
  2. ...
  3. //cycle the list of binary formats handler, until one recognizes the image
  4. list_for_each_entry(fmt, &formats, lh) {
  5. if (!try_module_get(fmt->module))
  6. continue;
  7. read_unlock(&binfmt_lock);
  8. retval = fmt->load_binary(bprm);
  9. read_lock(&binfmt_lock);
  10. put_binfmt(fmt);
  11. if (bprm->point_of_no_return || (retval != -ENOEXEC)) {
  12. read_unlock(&binfmt_lock);
  13. return retval;
  14. }
  15. }
  16. ...
  17. }
  18. // binfmt_elf.c &formats参数
  19. static struct linux_binfmt elf_format = {
  20. .module = THIS_MODULE,
  21. .load_binary = load_elf_binary, // 匹配到的handler
  22. .load_shlib = load_elf_library,
  23. #ifdef CONFIG_COREDUMP
  24. .core_dump = elf_core_dump,
  25. .min_coredump = ELF_EXEC_PAGESIZE,
  26. #endif
  27. };

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

  1. static int load_elf_binary(struct linux_binprm *bprm){
  2. ...
  3. retval = begin_new_exec(bprm); // 清理之前程序的相关信息,设置私有信号表,设置线程组等。。
  4. ...
  5. setup_new_exec(bprm); // 为新程序设置内核相关的状态(例如进程名)
  6. ...
  7. /* 我们的test使用的是动态链接的解释器,objdump -s test可以看到
  8. .interp /lib/ld-linux-aarch64.so.1,加载解释器,返回值elf_entry为解释器的入口地址,
  9. 内核准备工作完成后交给用户空间,用户空间的入口即elf_entry
  10. */
  11. if (interpreter) {
  12. elf_entry = load_elf_interp(interp_elf_ex,
  13. interpreter,
  14. load_bias, interp_elf_phdata,
  15. &arch_state);
  16. ...
  17. }
  18. // 放入新程序的命令行参数、环境列表等内容到新进程内存中,构建bss和初始化数据段等进程空间的内容
  19. ...
  20. retval = create_elf_tables(bprm, elf_ex, interp_load_addr,
  21. e_entry, phdr_addr);
  22. ...
  23. // 内核控制交给用户空间,进入用户空间后会直接进入解释器的入口elf_entry,由解释器加载动态链接库
  24. // 最后开始运行用户程序
  25. START_THREAD(elf_ex, regs, elf_entry, bprm->p);
  26. }
  1. 现在我们的程序已经交给动态解释器了,解释器将依赖的二进制库链接给test,然后进入test的entry。通过objdump -d test看一下是通过_start函数开始执行test
  1. Disassembly of section .text:
  2. 0000000000000600 <_start>:
  3. ...
  4. 62c: 97ffffe5 bl 5c0 <__libc_start_main@plt>
  5. 630: 97fffff0 bl 5f0 <abort@plt>
  1. 我们继续寻找用户空间程序的入口点,可以通过gdb调试来看Entry point 为 0xaaaaaaaa0600,在此处打断点
  1. root@yielde:~/workspace/code-container/cpp# gdb test
  2. (gdb) i file
  3. Symbols from "/root/workspace/code-container/cpp/test".
  4. Native process:
  5. Using the running image of child process 336143.
  6. While running this, GDB does not access memory from...
  7. Local exec file:
  8. `/root/workspace/code-container/cpp/test', file type elf64-littleaarch64.
  9. Entry point: 0xaaaaaaaa0600
  10. (gdb) b *0xaaaaaaaa0600
  11. (gdb) r
  12. The program being debugged has been started already.
  13. Start it from the beginning? (y or n) y
  14. Starting program: /root/workspace/code-container/cpp/test
  15. [Thread debugging using libthread_db enabled]
  16. Using host libthread_db library "/lib/aarch64-linux-gnu/libthread_db.so.1".
  17. Breakpoint 2, 0x0000aaaaaaaa0600 in _start ()
  18. (gdb) bt
  19. #0 0x0000aaaaaaaa05c0 in __libc_start_main@plt ()
  20. #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()做处理

  1. // main函数return 5;
  2. // 继续strace部分内容
  3. exit_group(5) = ?
  4. +++ exited with 5 +++

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

  1. SYSCALL_DEFINE1(exit_group, int, error_code)
  2. {
  3. do_group_exit((error_code & 0xff) << 8);
  4. /* NOTREACHED */
  5. return 0;
  6. }
  7. // do_group_exit做真正的退出工作
  8. void __noreturn
  9. do_group_exit(int exit_code){
  10. ...
  11. do_exit(exit_code);
  12. }
  13. // do_exit会释放一系列进程使用的资源https://elixir.bootlin.com/linux/latest/C/ident/switch_count
  14. void __noreturn do_exit(long code)
  15. {
  16. ...
  17. exit_mm();
  18. if (group_dead)
  19. acct_process();
  20. trace_sched_process_exit(tsk);
  21. exit_sem(tsk);
  22. exit_shm(tsk);
  23. exit_files(tsk);
  24. exit_fs(tsk);
  25. if (group_dead)
  26. disassociate_ctty(1);
  27. exit_task_namespaces(tsk);
  28. exit_task_work(tsk);
  29. exit_thread(tsk);
  30. ...
  31. cgroup_exit(tsk);
  32. ...
  33. // 给父进程发出SIGCHLD信号
  34. exit_notify(tsk, group_dead);
  35. ...
  36. do_task_dead();
  37. }

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返回两次,我们会用父子进程执行不同的代码分支。

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

demo

  1. #include <stdio.h>
  2. #include <unistd.h>
  3. int main() {
  4. int ret = fork();
  5. if (ret == 0) {
  6. printf("i'm parent\n");
  7. } else if (ret > 0) {
  8. printf("i'm child\n");
  9. } else {
  10. printf("error handle\n");
  11. }
  12. return 0;
  13. }
  14. // -------输出---------
  15. root@yielde:~/workspace/code-container/cpp# ./test
  16. i'm child
  17. i'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系函数来获取子进程的退出状态,内核也可以释放子进程相关信息,子进程完全消失。

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

wait

demo

  1. #include <errno.h>
  2. #include <stdio.h>
  3. #include <stdlib.h>
  4. #include <unistd.h>
  5. #include <wait.h>
  6. pid_t r_wait(int *stat) {
  7. int ret;
  8. while (((ret = wait(stat)) == -1) && (errno == EINTR))
  9. ;
  10. return ret;
  11. }
  12. int main() {
  13. int stat;
  14. pid_t pid = fork();
  15. if (pid > 0) {
  16. pid_t child_pid;
  17. int ret = r_wait(&stat);
  18. printf("child pid %d exit with code %d\n", ret,
  19. (stat >> 8) & 0xff); // 获取子进程的返回值
  20. } else if (pid == 0) {
  21. pid_t child_pid = getpid();
  22. sleep(3);
  23. printf("i'm child, pid: %d\n", child_pid);
  24. exit(10);
  25. } else {
  26. printf("fork failure\n");
  27. }
  28. return 0;
  29. }
  30. // ------------------------
  31. root@yielde:~/workspace/code-container/cpp# ./test
  32. child: i'm child, pid: 398918
  33. parent: child pid 398918 exit with code 10
  34. parent: no child need to wait

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

  1. 无法wait特定的子进程,只能wait所有子进程,然后通过返回值来判断特定的子进程
  2. 如果没有子进程退出,则wait阻塞
  3. wait函数只能等待终止的子进程,如果子进程是停止状态或者从停止状态恢复运行,wait是无法探知的。

waitpid

  1. pid_t waitpid(pid_t pid, int *wstatus, int options);
  2. // pid可以指定等待哪一个子进程的退出,
  3. // pid=0等待进程组内任意子进程状态改变
  4. // pid=-1与wait()等价
  5. // pid<-1,等待进程组为[pid]的所有子进程
  6. // options是一个位掩码,有如下标志
  7. // 0:等待终止的子进程
  8. // WUNTRACE:可以等待因信号停止的子进程
  9. // WCONTINUED:可以等待收到信号恢复运行的子进程
  10. // WNOHANG:立即返回0,如果没有与pid匹配的进程,则返回-1并设置errno为ECHILD
  1. 直接返回的status值是不可用的(wait也一样),可以通过相关的宏来支持作业控制、子进程正常终止、被信号终止,获取退出状态也是通过宏。man wait查看
  2. waitpid有个问题就是子进程终止和子进程停止无法独立监控,想要只关心停止而忽略终止是不行的。

waittid

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

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

demo

设置WNOWAIT观察子进程的状态

  1. #include <errno.h>
  2. #include <stdio.h>
  3. #include <stdlib.h>
  4. #include <string.h>
  5. #include <unistd.h>
  6. #include <wait.h>
  7. int main() {
  8. int stat;
  9. pid_t pid = fork();
  10. if (pid > 0) {
  11. siginfo_t info;
  12. int ret;
  13. memset(&info, '\0', sizeof(info));
  14. ret = waitid(P_PGID, getpid(), &info, WEXITED | WNOWAIT);
  15. if ((ret == 0) && (info.si_pid == pid)) {
  16. printf("child %d exit, exit event: %d, exit status: %d\n", pid,
  17. info.si_code, info.si_status);
  18. }
  19. } else if (pid == 0) {
  20. sleep(3);
  21. printf("i'm child, pid: %d\n", getpid());
  22. return 10;
  23. } else {
  24. printf("fork failure\n");
  25. }
  26. sleep(15);
  27. return 0;
  28. }
  29. // ---------------
  30. root@yielde:~/workspace/code-container/cpp/blog_demo# ./test
  31. i'm child, pid: 401845
  32. child 401845 exit, exit event: 1, exit status: 10
  33. sleep ....
  34. // 父进程获取到子进程退出信息后,子进程仍然为僵尸状态
  35. root 401844 0.0 0.0 2184 776 pts/3 S+ 23:01 0:00 ./test
  36. root 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

  1. #include <errno.h>
  2. #include <stdio.h>
  3. #include <stdlib.h>
  4. #include <string.h>
  5. #include <unistd.h>
  6. int main() {
  7. // int ret = system("lss -l"); //执行错误的命令
  8. // int ret = system("ls -l"); // 正常执行命令
  9. int ret = system("sleep 50"); // 执行命令进程被信号杀死
  10. if (ret == -1) {
  11. printf("system return -1, errno is: %s", strerror(errno));
  12. } else if (WIFEXITED(ret) && WEXITSTATUS(ret) == 127) {
  13. // WIFEXITED(wstatus) returns true if the child terminated normally(在 man wait中)
  14. // WEXITSTATUS(wstatus) returns the exit status of the child
  15. printf("shell can't exec the command\n");
  16. } else {
  17. if(WIFEXITED(ret)){
  18. printf("normal termination, exit code = %d\n", WEXITSTATUS(ret));
  19. }else if(WIFSIGNALED(ret)){
  20. // WIFSIGNALED(wstatus) returns true if the child process was terminated by a signal.
  21. printf("abnormal termination, signal number = %d\n", WTERMSIG(ret));
  22. }
  23. }
  24. }

分别编译测试三种情况:

  1. 让system执行一个错误的命令,运行如下
  1. root@yielde:~/workspace/code-container/cpp/blog_demo# ./test
  2. sh: 1: lss: not found
  3. shell can't exec the command
  1. 让system正常执行命令
  1. root@yielde:~/workspace/code-container/cpp/blog_demo# ./test
  2. total 40
  3. -rw-r--r-- 1 root root 4107 Jan 19 21:16 epoll_oneshot.cc
  4. -rw-r--r-- 1 root root 2642 Jan 18 19:44 oob_recv_select.cc
  5. -rw-r--r-- 1 root root 1659 Jan 18 22:11 poll.cc
  6. -rw-r--r-- 1 root root 739 Jan 25 23:34 system_test.cc
  7. -rwxr-xr-x 1 root root 9064 Jan 25 23:34 test
  8. -rw-r--r-- 1 root root 795 Jan 25 22:24 wait_test.cc
  9. -rw-r--r-- 1 root root 651 Jan 25 23:01 waittid_test.cc
  10. normal termination, exit code = 0
  1. 给system执行的命令发送kill -9
  1. //kill
  2. root@yielde:~/workspace/code-container/cpp# ps aux|grep sleep
  3. root 403568 0.0 0.0 2304 836 pts/3 S+ 23:42 0:00 sh -c sleep 50
  4. root 403569 0.0 0.0 5180 788 pts/3 S+ 23:42 0:00 sleep 50
  5. root 403613 0.0 0.0 5888 2008 pts/1 S+ 23:42 0:00 grep --color=auto sleep
  6. root@yielde:~/workspace/code-container/cpp#
  7. root@yielde:~/workspace/code-container/cpp# kill -9 403568
  8. // 结果
  9. root@yielde:~/workspace/code-container/cpp/blog_demo# ./test
  10. abnormal termination, signal number = 9

学习自:
《UNIX环境高级编程》
《Linux环境编程从应用到内核》高峰 李彬 著

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

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号