Linux/UNIX 系统编程手册 - 笔记

Linux/UNIX系统编程手册-进程

2020-01-10  本文已影响0人  妖小灰

Linux/UNIX系统编程手册

[德] Michael Kerrisk

第6章 进程
第24章 进程的创建
第25章 进程的终止
第26章 监控子进程

进程

进程和程序(Processes and Programs)

进程是一个可执行程序的实例(A process is an instance of an executing program).

程序(program)是包含了一系列信息的文件,这些信息描述了如何在运行时创建一个进程,所包含的信息有:

可以用一个程序创建多个进程。

进程是由内核定义的抽象的实体,并为该实体分配用以执行程序的各项系统资源。

进程号和父进程号

进程号(PID),是用来唯一标识系统中某个进程的一个整数。对系统调用来说,进程号可以作为参数传入,kill()系统调用;也可以作为返回值,比如getpid()系统调用。

Linux内核限制进程号需要<=32767, 可以调整。

$ cat /proc/sys/kernel/pid_max
32768

每个进程都有一个创建自己的父进程,使用系统调用getppid()获取父进程的进程号。

使用pstree命令可以查看进程树。

进程内存布局(Memory Layout of a Process)

每个进程所分配的内存由很多部分组成,通常称为“段(segments)”,或者“区(section)”:

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), 一般指用户栈,区分与内核栈,包含一下信息:

命令行参数(command-line argument),argc, argv

程序可以通过/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采用两种技术来避免这种浪费:

fork()之后的竞争条件(race condition)

竞争表现在调用fork()后,无法确定父、子进程谁将率先访问CPU。Linux在版本升级中,多次调整默认优先的进程。
由于会产生所谓“竞争条件”的错误,不应对fork()之后执行父、子进程的特定顺序做任何假设。如若需要保证执行顺序,需要采用同步技术,包括信号量(semaphore)、文件锁(file lock)以及进程间经由管道(pipe)的消息发送。

进程的终止

_exit()和exit()

进程可能通过两种方式终止:

#include <unistd.h>
void _exit(int status);

_exit()的status参数定义了进程的终止状态,父进程可以调用wait()获取该状态。虽然定义为int类型,但仅有低8位可以被父进程使用。调用_exit()的程序总会成功终止,即使从不返回。

一般使用库函数exit()来终止进程,它会在调用_exit()前执行各种动作:

监控子进程

父进程需要了解其某个子进程何时改变了状态,用于监控子进程有两种方式:

等待子进程

系统调用wait()

wait()等待进程的任一子进程终止,同时在参数status所指向的缓冲区中返回该子进程的终止状态

#include <sys/wait.h>
pid_t wait(int *status);
    Returns process ID of terminated child, or –1 on error

wait()执行一下动作:

系统调用waitpid()

wait()存在诸多限制,而waitpid()则意在突破这些限制

#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值,可用来区分一下子进程事件:

同时还有waitid(), wait3()和wait4()

孤儿进程和僵尸进程(Orphan and ZOmbie)

父进程应执行wait()方法,以确保系统中总是能够清理那些死去的子进程。

SIGCHLD信号

无论一个子进程何时终止,系统都会向其父进程发送SIGCHLD信号。
对该信号的默认处理时将其忽略,可以通过设置信号处理程序signal()或sigaction()来捕获,同时编写信号处理函数使用wait()来处理僵尸进程。

原文链接
https://sun2y.me

上一篇 下一篇

猜你喜欢

热点阅读