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

空间布局

拿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 ./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()来做具体的事情,

  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 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()

  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 __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技术解决该问题:

  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: 398918
parent: child pid 398918 exit with code 10
parent: 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: 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的返回值如下:

  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 found
shell 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.cc
normal termination, exit code = 0
  1. 给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的使用的更多相关文章

  1. Linux 程序启停脚本

    start.sh #!/bin/sh java -jar ./program.jar & echo $! > /var/run/program.pid stop.sh #!/bin/sh ...

  2. Java 调用 Rest api 设置经典 Linux 虚拟机的实例启停

    现象描述 用户可以通过 Rest API 设置经典 Linux 虚拟机实例的启停.在调用该 API 时需要通过 Azure Active Directory(下文简称 AAD) 获取 Token,但是 ...

  3. TFS2017持续发布中调用PowerShell启停远程应用程序

    目前团队项目中有多个Web.服务以及与大数据平台对接接口等应用,每次的发布和部署采用手工的方式进行.停止应用程序,拷贝发布包,启动应用程序,不停的循环着,并且时不时地会出现一些人为错误性问题.这种模式 ...

  4. Atitit.进程管理常用api

    Atitit.进程管理常用api 1 常用api 进程列表 getProcessList 是否存在某个进程判断 isExistProcess 启动进程run Sleep Exit Shutdown 作 ...

  5. 小程序常用API介绍

    小程序常用API接口  wx.request https网络请求 wx.request({ url: 'test.php', //仅为示例,并非真实的接口地址 method:"GET&qu ...

  6. Oracle常用启停命令

    一.监听启停 Oracle监听的启动.停止和状态查看 Oracle监听启动: lsnrctl start Oracle监听停止: lsnrctl stop Oracle监听状态 lsnrctl sta ...

  7. Delphi常用API,API函数

    auxGetDevCaps API 获取附属设备容量 auxGetNumDevs API 返回附属设备数量 auxGetVolume API 获取当前卷设置 auxOutMessage API 向输出 ...

  8. 人工智能常用 API

    人工智能常用 API 转载  2016年07月13日 19:17:27 2047 机器学习与预测 1.AlchemyAPI  在把数据由非结构化向结构化的转化中运用得较多.用于社交媒体监控.商业智能. ...

  9. Nginx(一)安装及启停

    目录 1 nginx安装 2 nginx启停 我发现很多博客排版杂乱,表达不清,读者看了往往云里雾里.我此前的博客也是如此,我自己很不满意.今起,每一篇博客都会用心写,此前的博客我也会尽力修改.至少要 ...

  10. C++ 中超类化和子类化常用API

    在windows平台上,使用C++实现子类化和超类化常用的API并不多,由于这些API函数的详解和使用方法,网上一大把.本文仅作为笔记,简单的记录一下. 子类化:SetWindowLong,GetWi ...

随机推荐

  1. appuploader 入门使用

    回想一下我们发布 iOS 应用,不仅步骤繁琐,非常耗时.一旦其中一步失误了,又得重新来.作为一名优秀的工程师不应该让这些重复的工作在浪费我们的人生.在软件工程里面,我们一直都推崇把重复.流程化的工作交 ...

  2. Solon 路由的 Url 大小写匹配与事项注意

    Solon 路由器对 url 的匹配默认是 "忽略大小写" 的.如果有需要,可以强制开启:v2.2.14 后支持 @SolonMain public class App{ publ ...

  3. Netty 框架学习 —— 初识 Netty

    Netty 是一款异步的事件驱动的网络应用程序框架,支持快速地开发可维护的高性能的面向协议的服务器和客户端 Java 网络编程 早期的 Java API 只支持由本地系统套接字库提供的所谓的阻塞函数, ...

  4. 【django drf】 阶段练习

    目录 需求 settings.py views.py urls.py serializers.py permissions.py page.py authenticate.py model.py 权限 ...

  5. Visual Studio 2022 激活,安装教程,内附Visual Studio激活码、密钥

    visual studio 2022(vs 2022)是由微软官方出品的最新版本的开发工具包系列产品.它是一个完整的开发工具集,囊括了整 visual studio 2022是一款由微软全新研发推出的 ...

  6. Go--时间日期相关

    1 获取当天零点的时间戳 //当天0点的时间戳 //获取当前时间 t := time.Now() nowTime := time.Date(t.Year(), t.Month(), t.Day(), ...

  7. JSP | 常见 JSP 简答题

    一.简述 JSP 的工作原理 当我们访问一个JSP页面的时候,这个文件首先会被JSP引擎翻译为一个Java源文件,其实就是一个Servlet,并进行编译,然后像其他Servlet一样,由Servlet ...

  8. C. Given Length and Sum of Digits... (贪心)

    https://codeforces.com/problemset/problem/489/C C. Given Length and Sum of Digits... You have a posi ...

  9. U64949 棋盘覆盖(二分图)| 二分图匹配总结

    https://ac.nowcoder.com/acm/contest/1062/B [题目] 给出一张n×n(n≤100)的国际象棋棋盘,其中被删除了一些点,问可以使用多少1*2的多米诺骨牌进行掩盖 ...

  10. poi4版本处理word里表格中的文字换行问题和设置字体样式

    开发中遇到生成word文档的需求,其中里面存在表格,发现表格中一旦存在换行生成的模板就出现各种问题,反正就是出不来想要的结果.网上找了一些方法基本都不好用,最后找到一个靠谱点的方法 XWPFParag ...