数据段(初始化的数据段):例如在函数外的声明,int a = 1
block started by symbol(未初始化的数据段):例如在函数外的声明,int b[10]
栈:保存局部浸染域的变量、函数调用须要保存的信息。例如调用一个函数,保存函数的返回地址、调用者的环境信息,给临时变量分配空间
堆:动态内存分配
正文段:CPU实行的指令,常日是只读并共享的,例犹如时打开多个文本编辑器进程,只须要读这一份正文段即可
命令行参数和环境变量
进程启动和停滞进程启动
用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()来做详细的事情,
is_rlimit_overlimit()检讨资源利用是否超过限定,struct linux_binprm bprm;是一个构造体,用于记录命令参数、环境变量、要读入ELF程序的入口地址、rlimit等信息。
bprm = alloc_bprm(fd, filename);为该构造分配内存,然后将bprm须要的内容copy进来。
构建好bprm后实行bprm_execve函数,函数注释sys_execve() executes a new program.该函数会做一些安全性的检讨,然后do_open_execat(fd, filename, flags);打开我们的ELF程序(编译好的test1),实行exec_binprm函数来运行新进程
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); }
现在我们的程序已经交给动态阐明器了,阐明器将依赖的二进制库链接给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>
我们连续探求用户空间程序的入口点,可以通过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()
__libc_start_main()初始化线程子系统,注册rtld_fini和fini来做程序退出后的清理事情,将。然后运行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()注册的清理事情,非常终止则直接内核接管退出。
常用系统APIforkfork可以创建新的进程,我们追踪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技能办理该问题:
将子进程的页表项指向与父进程相同的物理内存页,然后复制父进程的页表项,这样父子进程共用一份物理内存,并且将共用的页表标记为只读。
如果父子进程中任何一方须要修正页表项,会触发缺页非常,内核会为该页分配物理内存,并复制该内存页,此时父子进程各自拥有了独立的物理页,将两个页表设置为可写。
文件描述符父子进程的文件描述符被子进程复制,并且父子进程共享文件表项,自然会共享文件偏移量,以是父子进程对文件的读写会相互影响。通过open调用时设置FD_CLOSEXEC标志,子进程在实行exec家族函数的时候会先关闭该文件描述符
其他复制userid,groupid,有效userid,有效groupid
进程组id、会话id、tty
事情目录、根目录、sig_mask、FD_CLOSEXEC
env、共享内存段、rlimit
不复制未处理的旗子暗记集会被清空
父进程设置的文件锁
未处理的alarm会被打消
wait、waitpid、waittidwait系列函数用于等待子进程的状态改变(包括子进程终止、子进程收到旗子暗记停滞、已经停滞的子进程被旗子暗记唤醒)。如果子进程终止,子进程的pid、内核栈等并不会被开释,但是子进程运行的内存空间已经被开释,此时子进程无法运行,变为僵尸状态,父进程调用wait系函数来获取子进程的退出状态,内核也可以开释子进程干系信息,子进程完备消逝。
#include <sys/types.h>#include <sys/wait.h>pid_t wait(int wstatus);// 成功返回退出子进程的ID// 失落败返回-1设置errno:ECHLD表示没有子进程须要等待。EINTR:被旗子暗记中断
waitdemo#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存在以下几个问题:
无法wait特定的子进程,只能wait所有子进程,然后通过返回值来判断特定的子进程
如果没有子进程退出,则wait壅塞
wait函数只能等待终止的子进程,如果子进程是停滞状态或者从停滞状态规复运行,wait是无法探知的。
waitpidpid_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
直接返回的status值是不可用的(wait也一样),可以通过干系的宏来支持作业掌握、子进程正常终止、被旗子暗记终止,获取退出状态也是通过宏。man wait查看
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>
systemsystem相称于我们fork出子进程->子进程实行exec实行命令->父进程waitpid等待子进程返回,只不过利用system时,system会fork出一个shell,然后shell创建子进程来实行命令,因此调用system的返回值如下:
如果system内部fork失落败或waitpid返回了除EINTR之外的缺点,system返回-1设置errno。如果SIGCHILD被设置为SIG_IGN,那么system返回-1并设置errno为ECHLD,无法判断命令是否实行成功
如果exec失落败,返回127(shell实行失落败的指令,可以在shell写一个不存在的命令,然后echo $?看下)
如果system实行成功,会返回shell的终止状态,即末了一条命令的退出状态
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)); } }}
分别编译测试三种情形:
让system实行一个缺点的命令,运行如下
root@yielde:~/workspace/code-container/cpp/blog_demo# ./test sh: 1: lss: not foundshell can't exec the command
让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
给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
来源:IT净水之家