[德] Michael Kerrisk
第6章 进程
第24章 进程的创建
第25章 进程的终止
第26章 监控子进程
进程
进程和程序(Processes and Programs)
进程是一个可执行程序的实例(A process is an instance of an executing program).
程序(program)是包含了一系列信息的文件,这些信息描述了如何在运行时创建一个进程,所包含的信息有:
- 二进制格式标识(Binary format identification): 每个程序文件都包含用于描述可执行文件格式的元信息(metainformation)。内核利用此信息来解释文件中的其他信息。大多数UNIX(包括Linux)采用Executable
and Linking Format (ELF). - 机器语言指令(Machine-language instructions): 对程序算法进行编码
- 程序入口地址(Program entry-point address):标识程序开始执行时起始指令位置
- 数据(Data): 变量初始值和程序使用的字面常量(literal constant)
- 符号表及重定位表(Symbol and relocation tables): 描述程序中函数和变量的位置及名称。
- 共享库和动态链接信息(Shared-library and dynamic-linking information): 程序运行需要的共享库,以及加载共享库的动态链接器的路径名
- 其他信息: 描述如何创建进程
可以用一个程序创建多个进程。
进程是由内核定义的抽象的实体,并为该实体分配用以执行程序的各项系统资源。
进程号和父进程号
进程号(PID),是用来唯一标识系统中某个进程的一个整数。对系统调用来说,进程号可以作为参数传入,kill()系统调用;也可以作为返回值,比如getpid()系统调用。
Linux内核限制进程号需要<=32767, 可以调整。
$ cat /proc/sys/kernel/pid_max
32768
每个进程都有一个创建自己的父进程,使用系统调用getppid()获取父进程的进程号。
使用pstree
命令可以查看进程树。
进程内存布局(Memory Layout of a Process)
每个进程所分配的内存由很多部分组成,通常称为“段(segments)”,或者“区(section)”:
- 文本段(text segment): 包含进程运行的机器语言指令。文本段具有只读属性,同时可共享,使多个进程使用同一份程序代码拷贝。
- 初始化的数据段(initialized data segment): 包含显示初始化的全局变量和静态变量。
- 未初始化数据段(uninitialized data segment):也被称为BSS(block started by symbol)。 包含未进行显式初始化的全局变量和静态变量。程序启动之前系统将本段内所有内存初始化为0。
- 栈(stack): 动态变化的segment,由栈帧(stack frames)组成。系统会为每个当前调用的函数分配一个栈帧,其中存储函数的局部变量,实参和返回值。
- 堆(heap): 在运行时动态分配内存的区域。堆顶端被称为program break。
size
命令可显示文本段,初始化和未初始化数据段(bss)的大小
> $ man size
NAME
size - list section sizes and total size.
...
> $ size hello
text data bss dec hex filename
1230 548 4 1782 6f6 hello
虚拟内存管理(Virtual Memory Management)
进程的内存布局存在与虚拟内存(Virtual Memory)中。虚拟内存的规划之一就是将每个程序使用的内存切割成小型的、固定大小的“页”(page)单元。相应地,将RAM换成一系列与“页”大小相同的页帧。
为支持这一组织形式,内核为每个进程维护一张页表(page table),用于记录每页在进程虚拟地址空间的位置。
虚拟内存管理使进程的虚拟地址空间与RAM物理地址空间隔离开来。
栈和栈帧(the stack and stack frame)
函数的调用和返回使栈的增长和收缩呈线性。栈驻留在内存的高端比你高向下增长(朝堆的方向)。专用寄存器--栈指针(stack pointer),用于跟踪当前栈顶。每次调用函数和返回函数时,都是在栈上新增和移去栈帧。
栈帧(user stack), 一般指用户栈,区分与内核栈,包含一下信息:
- 函数实参和局部变量
- (函数)调用的链接信息:每个函数都会用到一些CPU寄存器,比如程序计数器。比如调用另一个函数时,保存当前寄存器状态,以便返回时恢复。
命令行参数(command-line argument),argc, argv
- int argc:命令行参数的个数
- char *argv[]: 指向命令行参数的指针数组,每一参数都是以空字符('\0')结尾的字符串.
程序可以通过/proc/PID/cmdline文件访问任一进程的命令行参数,每个参数都以空(NULL)字节终止。
argv和environ(环境变量)数组,以及这些参数最初只想的字符串,都主流在进程栈上的一个单一、连续的内存区域。
进程的创建
创建新进程: fork()系统调用
fork()创建一个新进程(child),几近于对调用进程(parent)的翻版
#include <unistd.h>
pid_t fork(void);
In parent: returns process ID of child on success, or –1 on error;
in successfully created child: always returns 0
完成对其调用后将存在两个进程,每个进程都会从fork()的返回处继续执行,程序代码可通过fork()的返回值来区分父子进程。在父进程中,fork()将返回新创建子进程的进程ID,在子进程中则返回0。
子进程也可调用getpid(), getppid()分别获得自身进程以及父进程的ID。
执行fork()时候,子进程会获得父进程所有文件描述符的副本,也即父子进程共享打开的文件及其属性。
从概念上讲,fork()认作对父进程程序代码段,数据段,堆栈的拷贝,实际上,子进程一般会替换代码段,并重新初始化数据,堆栈,全拷贝就造成了浪费。因此UNIX采用两种技术来避免这种浪费:
- 内核将每一进程的代码段标记为只读,父子进程共享该代码段。
- 对于数据段,堆栈中各页,内核采用写时复制技术(copy-on-write)。即内核会捕捉进程中针对页的修改企图,并为将要修改的页创建拷贝。
fork()之后的竞争条件(race condition)
竞争表现在调用fork()后,无法确定父、子进程谁将率先访问CPU。Linux在版本升级中,多次调整默认优先的进程。
由于会产生所谓“竞争条件”的错误,不应对fork()之后执行父、子进程的特定顺序做任何假设。如若需要保证执行顺序,需要采用同步技术,包括信号量(semaphore)、文件锁(file lock)以及进程间经由管道(pipe)的消息发送。
进程的终止
_exit()和exit()
进程可能通过两种方式终止:
- 异常(abnormal)终止:接受到终止信号(signal),可能产生核心转储(core dump)
- 进程使用系统调用_exit()自主终止
#include <unistd.h>
void _exit(int status);
_exit()的status参数定义了进程的终止状态,父进程可以调用wait()获取该状态。虽然定义为int类型,但仅有低8位可以被父进程使用。调用_exit()的程序总会成功终止,即使从不返回。
一般使用库函数exit()来终止进程,它会在调用_exit()前执行各种动作:
- 调用退出处理程序(通过atexit()和on_exit()注册的函数)
- 刷新stdio流缓冲区
- 使用由status提供的值执行_exit()系统调用
监控子进程
父进程需要了解其某个子进程何时改变了状态,用于监控子进程有两种方式:
- 系统调用wait()
- 信号SIGCHLD
等待子进程
系统调用wait()
wait()等待进程的任一子进程终止,同时在参数status所指向的缓冲区中返回该子进程的终止状态
#include <sys/wait.h>
pid_t wait(int *status);
Returns process ID of terminated child, or –1 on error
wait()执行一下动作:
- 如果调用之前还没有子进程终止,则一直阻塞,如果有,则立即返回
- 如果status非空,那么关于子进程如何终止的信息则会通过status指向的整型变量返回
- 内核将会为父进程下所有子进程的运行总量追加进程CPU时间以及资源使用数据
- 将终止子进程的ID作为wait()的结果返回
系统调用waitpid()
wait()存在诸多限制,而waitpid()则意在突破这些限制
- 无法指定某个特定的子进程,只能按循序等待下一个子进程终止
- 没有进程退出,则wait()总是阻塞
- wait()只能发现终止的子进程,而对终止原因及恢复执行情况无能为力
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *status, int options);
Returns process ID of child, 0 (see text), or –1 on error
等待状态值
wait()和waitpid()返回的status值,可用来区分一下子进程事件:
- 子进程调用_exit()或exit()而终止,并指定一个整型值作为退出状态
- 子进程收到未处理信号终止
- 子进程因为信号而暂停,并以WUNTRACED标志调用waitpid()
- 子进程因收到信号SIGCONT而恢复,并以WCONTINUED标志调用waitpid()
同时还有waitid(), wait3()和wait4()
孤儿进程和僵尸进程(Orphan and ZOmbie)
- 孤儿进程:父进程先于子进程终止,此时init会接管该进程,对getppid()的调用将返回1
- 僵尸进程,父进程在wait()之前,子进程就已终止,内核会将子进程转为僵尸进程,即释放资源,但是保留该进程在进程表里的一条记录,包含进程id,终止状态,资源使用数据等信息。
父进程应执行wait()方法,以确保系统中总是能够清理那些死去的子进程。
SIGCHLD信号
无论一个子进程何时终止,系统都会向其父进程发送SIGCHLD信号。
对该信号的默认处理时将其忽略,可以通过设置信号处理程序signal()或sigaction()来捕获,同时编写信号处理函数使用wait()来处理僵尸进程。
原文链接
https://sun2y.me