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

作者: 妖小灰 | 来源:发表于2020-01-10 18:22 被阅读0次

    Linux/UNIX系统编程手册

    [德] 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

    相关文章

      网友评论

        本文标题:Linux/UNIX系统编程手册-进程

        本文链接:https://www.haomeiwen.com/subject/pkjmactx.html