程序启停分析与进程常用API的使用
进程是程序运行的实例,操作系统为进程分配独立的资源,使之拥有独立的空间,互不干扰。
空间布局
拿c程序来说,其空间布局包括如下几个部分:
- 数据段(初始化的数据段):例如在函数外的声明,
int a = 1
- block started by symbol(未初始化的数据段):例如在函数外的声明,
int b[10]
- 栈:保存局部作用域的变量、函数调用需要保存的信息。例如调用一个函数,保存函数的返回地址、调用者的环境信息,给临时变量分配空间
- 堆:动态内存分配
- 正文段:CPU执行的指令,通常是只读并共享的,例如同时打开多个文本编辑器进程,只需要读这一份正文段即可
- 命令行参数和环境变量
进程启动和停止
进程启动
用strace
命令来追一个c的hello world:
root@yielde:~/workspace/code-container/cpp# strace ./test1
execve("./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 file
Symbols 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) r
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting 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 __noreturn
do_group_exit(int exit_code){
...
do_exit(exit_code);
}
// do_exit会释放一系列进程使用的资源https://elixir.bootlin.com/linux/latest/C/ident/switch_count
void __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 child
i'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、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: 398918
parent: child pid 398918 exit with code 10
parent: no child need to wait
使用wait存在以下几个问题:
- 无法wait特定的子进程,只能wait所有子进程,然后通过返回值来判断特定的子进程
- 如果没有子进程退出,则wait阻塞
- 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
- 直接返回的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: 401845
child 401845 exit, exit event: 1, exit status: 10
sleep ....
// 父进程获取到子进程退出信息后,子进程仍然为僵尸状态
root 401844 0.0 0.0 2184 776 pts/3 S+ 23:01 0:00 ./test
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的返回值如下:
- 如果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 found
shell 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.cc
normal termination, exit code = 0
- 给system执行的命令发送kill -9
//kill
root@yielde:~/workspace/code-container/cpp# ps aux|grep sleep
root 403568 0.0 0.0 2304 836 pts/3 S+ 23:42 0:00 sh -c sleep 50
root 403569 0.0 0.0 5180 788 pts/3 S+ 23:42 0:00 sleep 50
root 403613 0.0 0.0 5888 2008 pts/1 S+ 23:42 0:00 grep --color=auto sleep
root@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
学习自:
《UNIX环境高级编程》
《Linux环境编程从应用到内核》高峰 李彬 著
程序启停分析与进程常用API的使用的更多相关文章
- Linux 程序启停脚本
start.sh #!/bin/sh java -jar ./program.jar & echo $! > /var/run/program.pid stop.sh #!/bin/sh ...
- Java 调用 Rest api 设置经典 Linux 虚拟机的实例启停
现象描述 用户可以通过 Rest API 设置经典 Linux 虚拟机实例的启停.在调用该 API 时需要通过 Azure Active Directory(下文简称 AAD) 获取 Token,但是 ...
- TFS2017持续发布中调用PowerShell启停远程应用程序
目前团队项目中有多个Web.服务以及与大数据平台对接接口等应用,每次的发布和部署采用手工的方式进行.停止应用程序,拷贝发布包,启动应用程序,不停的循环着,并且时不时地会出现一些人为错误性问题.这种模式 ...
- Atitit.进程管理常用api
Atitit.进程管理常用api 1 常用api 进程列表 getProcessList 是否存在某个进程判断 isExistProcess 启动进程run Sleep Exit Shutdown 作 ...
- 小程序常用API介绍
小程序常用API接口 wx.request https网络请求 wx.request({ url: 'test.php', //仅为示例,并非真实的接口地址 method:"GET&qu ...
- Oracle常用启停命令
一.监听启停 Oracle监听的启动.停止和状态查看 Oracle监听启动: lsnrctl start Oracle监听停止: lsnrctl stop Oracle监听状态 lsnrctl sta ...
- Delphi常用API,API函数
auxGetDevCaps API 获取附属设备容量 auxGetNumDevs API 返回附属设备数量 auxGetVolume API 获取当前卷设置 auxOutMessage API 向输出 ...
- 人工智能常用 API
人工智能常用 API 转载 2016年07月13日 19:17:27 2047 机器学习与预测 1.AlchemyAPI 在把数据由非结构化向结构化的转化中运用得较多.用于社交媒体监控.商业智能. ...
- Nginx(一)安装及启停
目录 1 nginx安装 2 nginx启停 我发现很多博客排版杂乱,表达不清,读者看了往往云里雾里.我此前的博客也是如此,我自己很不满意.今起,每一篇博客都会用心写,此前的博客我也会尽力修改.至少要 ...
- C++ 中超类化和子类化常用API
在windows平台上,使用C++实现子类化和超类化常用的API并不多,由于这些API函数的详解和使用方法,网上一大把.本文仅作为笔记,简单的记录一下. 子类化:SetWindowLong,GetWi ...
随机推荐
- vue3,对比 vue2 有什么优点?
摘要:Vue3新版本的理念成型于 2018 年末,当时的 Vue 2 已经有两岁半了.比起通用软件的生命周期来这好像也没那么久,Vue3在2020年正式推出,在源码和API都有较大变化,性能得到了显著 ...
- 你知道,什么时候用Vue计算属性吗?
摘要:当我们处理复杂逻辑时,都应该使用计算属性. 本文分享自华为云社区<深入理解计算属性,知道什么时候该用Vue计算属性吗?>,作者: 前端老实人 . 计算属性 有些时候,我们在模板中放入 ...
- Java 全新生态的框架,Solon v1.10.12 发布
一个更现代感的 Java 应用开发框架:更快.更小.更自由.没有 Spring,也没有 Servlet,独立的生态.主框架仅 0.1 MB.Helloworld: @Controller public ...
- JupyterLab 这插件太强了,Excel灵魂附体
终于把 jupyter notebook 玩明白了 JupyterLab 终于出了 Windows 桌面版 今天向大家介绍一款很有意思的 JupyterLab 插件 -- Mito Mito是Jupy ...
- django中间件需要了解的方法 importlib模块 csrf校验策略 csrf相关装饰器
目录 django中间件三个需要了解的方法 process_view process_exception process_template_response 基于django中间件实现功能的插拔式设计 ...
- 离散化/线段树 (POJ - 2528 Mayor's posters)
Mayor's posters https://vjudge.net/problem/POJ-2528#author=szdytom 线段树 + 离散化 讲解:https://blog.csdn.ne ...
- 【营】在开局,提升【豹】发力 - vivo活动插件管理平台
一.背景 随着vivo悟空活动中台活动组件越来越多,活动中台开发的小伙伴们愈发的感知到我们缺少一个可以沉淀通用能力,提升代码复用性的组件库.在这个目标基础之上诞生了acitivity-componen ...
- 活动回顾|阿里云 Serverless 技术实战与创新广州站回放&PPT下载
7月8日"阿里云 Serverless 技术实战与创新"广州站圆满落幕.活动受众以关注Serverless 技术的开发者.企业决策人.云原生领域创业者为主,活动形式为演讲.动手实操 ...
- d3过滤
1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="U ...
- 每天学五分钟 Liunx 000 | 计算机与 Liunx
计算机 计算机是具有数据处理与逻辑运算的机器. 它有输入单元,输出单元,CPU 内部的控制单元,逻辑处理单元以及内存组成. 输入单元如鼠标键盘等输入设备: 输出单元如打印机,显示屏,等输出 ...