Linux系统编程(三):进程

参考引用

  • UNIX 环境高级编程 (第3版)
  • 黑马程序员-Linux 系统编程

1. 进程相关概念

1.1 程序和进程

  • 程序,是指编译好的二进制文件,在磁盘上,不占用系统资源 (CPU、内存、打开的文件、设备、锁…)
    • 程序 → 剧本 (纸)
  • 进程与操作系统原理联系紧密。进程是活跃的程序,占用系统资源,在内存中执行 (程序运行起来,产生一个进程)
    • 进程 → 戏 (舞台、演员、灯光、道具…)

同一个剧本(程序)可以在多个舞台(进程)同时上演。同样,同一个程序也可以加载为不同的进程 (彼此之间互不影响)。如:同时开两个终端,各自都有一个 bash 但彼此 ID 不同

1.2 并发

  • 并发,在操作系统中,一个时间段中有多个进程都处于已启动运行到运行完毕之间的状态。但是任一个时刻点上仍只有一个进程在运行。
    • 例如,当下,我们使用计算机时可以边听音乐边聊天边上网。 若笼统的将他们均看做一个进程的话,为什么可以同时运行呢,因为并发。
  • 分时复用 CPU
    • 1-4 表示不同进程获得 CPU 的顺序,并不是一次只分给单个进程(采用时钟中断来保证),而是每个进程每次分配(缓冲)一点

Linux系统编程(三):进程_第1张图片

1.3 单道、多道程序设计

1.3.1 单道程序设计

  • 所有进程一个一个排对执行。若 A 阻塞,B 只能等待,即使 CPU 处于空闲状态。而在人机交互时阻塞的出现时必然的。所有这种模型在系统资源利用上及其不合理,在计算机发展历史上存在不久,大部分便被淘汰了
    • 例如:微软的 DOS 系统

1.3.2 多道程序设计

  • 在计算机内存中同时存放几道相互独立的程序,它们在管理程序控制之下,相互穿插的运行,多道程序设计必须有硬件基础作为保证
  • 时钟中断:即为多道程序设计模型的理论基础。并发时,任意进程在执行期间都不希望放弃 CPU。因此系统需要一种强制让进程让出 CPU 资源的手段。时钟中断有硬件基础作为保障,对进程而言不可抗拒。操作系统中的中断处理函数,来负责调度程序执行
  • 在多道程序设计模型中,多个进程轮流使用 CPU (分时复用 CPU 资源)。而当下常见 CPU 为纳秒级,1 秒可以执行大约 10 亿条指令。由于人眼的反应速度是毫秒级,所以看似同时在运行

1.4 CPU 和 MMU

  • 存储介质
    • 金字塔越往下存储的量越大,但是存储速度越慢
    • 硬盘读取是物理操作,内存读取是电信号,所以内存读取速度比磁盘快得多
    • cache 缓存是内存和寄存器之间的中间产物
    • 寄存器存储大小为 4 字节(32 位操作系统)

Linux系统编程(三):进程_第2张图片

  • MMU 虚拟内存映射单元
    • 虚拟内存和物理内存映射关系

Linux系统编程(三):进程_第3张图片

Linux系统编程(三):进程_第4张图片

1.5 进程控制块 PCB

  • 每个进程在内核中都有一个进程控制块来维护进程相关的信息,Linux 内核的进程控制块是 task_struct 结构体
  • /usr/src/linux-headers-5.4.0-152-generic/include/linux/sched.h 文件中可以查看 struct task_struct {} 结构体定义,其内部成员有很多,重点掌握以下部分即可
    • 进程 id
      • 系统中每个进程有唯一的 id,在 C 语言中用 pid_t 类型表示,是一个非负整数
      • 查看全部:ps aux 或 查看指定:ps aux | xxx
    • 进程的状态
    • 进程切换时需要保存和恢复的一些 CPU 寄存器
    • 描述虚拟地址空间的信息
    • 描述控制终端的信息
    • 当前工作目录
    • umask 掩码
    • 文件描述符表
      • 包含很多指向 file 结构体的指针
    • 和信号相关的信息
    • 用户 id 和组 id
    • 会话(Session)和进程组
    • 进程可以使用的资源上限

1.6 进程状态

  • 进程基本的状态有 5 种
    • 分别为初始态,就绪态,运行态,挂起态与终止态
    • 其中初始态为进程准备阶段,常与就绪态结合来看

Linux系统编程(三):进程_第5张图片

1.7 环境变量

  • 环境变量,是指在操作系统中用来指定操作系统运行环境的一些参数。通常具备以下特征
    • 字符串 (本质)
    • 有统一的格式:名 = 值[:值]
    • 值用来描述进程环境信息
    • 存储形式:与命令行参数类似。char *[] 数组,数组名 environ,内部存储字符串,NULL 作为哨兵结尾
    • 使用形式:与命令行参数类似
    • 加载位置:位于用户区,高于 stack 的起始位置
    • 引入环境变量表:须声明环境变量。extern char ** environ

常见环境变量

  • 环境变量字符串都是 name = value 这样的形式,大多数 name 由大写字母加下划线组成,一般把 name 的部分叫做环境变量,value 的部分则是环境变量的值。环境变量定义了进程的运行环境,一些比较重要的环境变量的含义如下

  • PATH

    • 可执行文件的搜索路径。ls 命令也是一个程序,执行它不需要提供完整的路径名 /bin/ls,然而通常执行当前目录下的程序 a.out 却需要提供完整的路径名 ./a.out,这是因为 PATH 环境变量的值里面包含了 ls 命令所在的目录 /bin,却不包含 a.out 所在的目录
    • PATH 环境变量的值可以包含多个目录,用 : 号隔开
    • 在 Shell 中用 echo 命令可以查看这个环境变量的值
    $ echo $PATH
    /opt/ros/melodic/bin:/opt/gcc-arm-none-eabi-9-2020-q2-update/bin:/home/yue/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin
    
  • SHELL

    • 当前 Shell,它的值通常是 /bin/bash
  • TERM

    • 当前终端类型,在图形界面终端下它的值通常是 xterm,终端类型决定了一些程序的输出显示方式,比如图形界面终端可以显示汉字,而字符终端一般不行
  • LANG

    • 语言和 locale,决定了字符编码以及时间、货币等信息的显示格式
  • HOME

    • 当前用户主目录的路径,很多程序需要在主目录下保存配置文件,使得每个用户在运行该程序时都有自己的一套配置

2. 进程环境

2.1 main 函数

  • main 函数的原型
    • argc 是命令行参数的数目,argv 是指向参数的各个指针所构成的数组
    int main(in argc, char* argv[]);
    
  • 当内核执行 C 程序时 (使用一个exec 函数),在调用 main 前先调用一个特殊的启动例程
    • 可执行程序文件将此启动例程指定为程序的起始地址:这由连接编辑器设置,而连接编辑器则由 C 编译器调用
    • 启动例程从内核取得命令行参数和环境变量值,然后为按上述方式调用 main 函数做好安排

2.2 进程终止

  • 有 8 种方式使进程终止 (termination)
    • 其中 5 种为正常终止,它们是
      • (1) 从 main 返回
      • (2) 调用 exit
      • (3) 调用 _exit 或 _Exit
      • (4) 最后一个线程从其启动例程返回
      • (5) 从最后一个线程调用 pthread_exit
    • 异常终止有 3 种方式,它们是
      • (6) 调用 abort
      • (7) 接到一个信号
      • (8) 最后一个线程对取消请求做出响应

2.2.1 退出函数

  • 3 个函数用于正常终止一个程序

    • _exit 和 _Exit 立即进入内核
    • exit 则先执行一些清理处理,然后返回内核
    #include 
    void exit(int status);
    void _Exit(int status);
    
    #include 
    void _exit(int status);
    
  • 3 个退出函数都带一个整型参数,称为终止状态 (或退出状态,exit status)。大多数 UNIX 系统 shell 都提供检查进程终止状态的方法

    • 如果 (a) 调用这些函数时不带终止状态,或 (b) main 执行了一个无返回值的 return 语句,或 © main 没有声明返回类型为整型,则该进程的终止状态是未定义的
    • 但是,若 main 的返回类型是整型,且 main 执行到最后一条语句时返回 (隐式返回),那么该进程终止状态是 0
  • main 函数返回一个整型值与用该值调用exit 是等价的

    // 下两行等价
    exit(0);
    return(0);
    
  • 对一段程序进行编译,然后运行,并打印终止状态

    $ gcc hello.c
    $ ./a.out
    hello world
    $ echo $?    # 打印终止状态
    0
    

2.2.2 函数 atexit

#include 

int atexit(void (*function)(void));
  • 函数返回值

    • 若成功,返回 0
    • 若出错,返回非 0
  • 按照 ISO C 的规定,一个进程可以登记多至 32 个函数,这些函数将由 exit 自动调用。称这些函数为终止处理程序,并调用 atexit 函数来登记这些函数

  • atexit 的参数是一个函数地址,当用此函数时无需向它传递任何参数,也不期望它返回一个值。exit 调用这些函数的顺序与它们登记时候的顺序相反。同一函数如若登记多次,也会被调用多次

  • 一个 C 程序是如何启动和终止的

Linux系统编程(三):进程_第6张图片

注意,内核使程序执行的唯一方法是调用一个 exec 函数。进程自愿终止的唯一方法是显式或隐式地 (通过调用 exit) 调用 _exit 或 _Exit。进程也可非自愿地由一个信号使其终止

2.3 命令行参数

  • 当执行一个程序时,调用 exec 的进程可将命令行参数传递给该新程序
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    
    int main(int argc, char* argv[]) {
        int i;
    
        for (i = 0; argv[i] != NULL; ++i) {
            printf("argv[%d] : %s\n", i, argv[i]);
        }
    
        exit(0);
    }
    
    $ gcc exec.c -o exec
    $ ./exec arg1 TEST foo
    argv[0] : ./exec
    argv[1] : arg1
    argv[2] : TEST
    argv[3] : foo
    

2.4 环境表

  • 每个程序都接收到一张环境表。与参数表一样,环境表也是一个字符指针数组,其中每个指针包含一个以 null 结束的 C 字符的地址。全局变量 environ 则包含了该指针数组的地址:
    extern char **environ;
    
  • 例如,如果该环境包含 5 个字符,那它看起来如下图所示
    • 其中,每个字符的结尾处都显式地有一个 NULL 字节
    • 称 environ 为环境指针,指针数组为环境表,其中各指针指向的字符串为环境字符串

Linux系统编程(三):进程_第7张图片

2.5 C 程序的存储空间布局

  • 历史沿袭至今,C 程序一直由下列几部分组成
    • 正文段
      • 这是由 CPU 执行的机器指令部分
    • 初始化数据段
      • 通常将此段称为数据段,它包含了程序中需明确地赋初值的变量
    • 未初始化数据段
      • 通常将此段称为 bss 段,意思是 “由符号开始的块” (block started by symbol),在程序开始执行之前,内核将此段中的数据初始化为 0 或空指针
      • 自动变量以及每次函数调用时所需保存的信息都存放在此段中
      • 每次函数调用时,其返回地址以及调用者的环境信息 (如某些机器寄存器的值) 都存放在栈中
      • 最近被调用的函数在栈上为其自动和临时变量分配存储空间
      • 通常在堆中进行动态存储分配
      • 堆位于未初始化数据段和栈之间

Linux系统编程(三):进程_第8张图片

  • size 命令报告正文段、数据段和 bss 段的长度 (以字节为单位)
    $ size /usr/bin/cc /bin/sh
       text	   data	    bss	    dec	    hex	filename
    1025621	  15120	  10600	1051341	 100acd	/usr/bin/cc
     110609	   4816	  11312	 126737	  1ef11	/bin/sh
    

2.6 共享库

  • 在不同的系统中,程序可能使用不同的方法说明是否要使用共享库
    • 比较典型的有 cc 和 ld 命令的选项
$ gcc -static hello.c    # 阻止 gcc 使用共享库
$ gcc hello.c            # gcc 默认使用共享库

2.7 存储空间分配

  • ISO C 说明了 3 个用于存储空间动态分配的函数

    • (1) malloc,分配指定字节数的存储区。此存储区中的初始值不确定
    • (2) calloc,为指定数量指定长度的对象分配存储空间。该空间中的每一位都初始化为 0
    • (3) realloc,增加或减少以前分配区的长度
      • 当增加长度时,可能需将以前分配区的内容移到另一个足够大的区域,以便在尾端提供增加的存储区,而新增区域内的初始值则不确定
    #include 
    
    void *malloc(size_t size);
    void *calloc(size_t nmemb, size_t size);
    void *realloc(void *ptr, size_t size);
    
    void free(void *ptr);
    
  • 函数返回值

    • 若成功,返回非空指针
    • 若出错,返回 NULL
  • 函数 free 释放 ptr 指向的存储空间。被释放的空间通常被送入可用存储区池,以后,可在调用上述 3 个分配函数时再分配

  • 可能产生的致命性的错误

    • 释放一个已经释放了的块
    • 调用 free 时所用的指针不是 3 个 alloc 函数的返回值

    如若一个进程调用 malloc 函数,但却忘记调用 free 函数,那么该进程占用的存储空间就会连续增加,这被称为泄漏 (leakage)。如果不调用 free 函数释放不再使用的空间,那么进程地址空间长度就会慢慢增加,直至不再有空闲空间。此时,由于过度的换页开销,会造成性能下降

2.8 环境变量

2.8.1 获取环境变量值 getenv

#include 

char* getenv(const char* name);
  • 函数返回值
    • 指向与 name 关联的 value 的指针
    • 若未找到,返回 NULL
  • 注意,此函数返回一个指针,它指向 name = value 字符串中的 value
    • 应当使用 getenv 从环境中取一个指定环境变量的值,而不是直接访问 environ

2.8.2 设置环境变量值 setenv

#include 

int putenv(char *string);
  • 函数返回值
    • 若成功,返回 0
    • 若出错,返回非 0
  • putenv 取形式为 name = value 的字符串,将其放到环境表中。如果 name 已经存在,则先删除其原来的定义
#include 

int setenv(const char *name, const char *value, int overwrite);
int unsetenv(const char *name);
  • 函数返回值

    • 若成功,返回 0
    • 若出错,返回 -1
  • setenv 将 name 设置为 value。如果在环境中 name 已经存在,那么

    • (a) 若 overwrite 非 0,则首先删除其现有的定义
    • (b) overwrite 为 0,则不删除其现有定义 (name 不设置为新的 value,而且也不出错)
  • unsetenv 删除 name 的定义。即使不存在这种定义也不算出错

3. 进程控制

3.1 进程标识

  • 每个进程都有一个非负整型表示的唯一进程 ID

    • 因为进程 ID 标识符总是唯一的,常将其用作其他标识符的一部分以保证其唯一性
  • 虽然进程 ID 是唯一的,但是进程 ID 是可复用的

    • 当一个进程终止后,其进程 ID 就成为复用的候选者。大多数 UNIX 系统实现延迟复用算法,使得赋予新建进程的 ID 不同于最近终止进程所使用的 ID。这防止了将新进程误认为是使用同一 ID 的某个已终止的先前进程
  • 系统中有一些专用进程,但具体细节随实现而不同

    • ID 为 0 的进程通常是调度进程,常常被称为交换进程 (swapper)。该进程是内核的一部分,它并不执行任何磁盘上的程序,因此也被称为系统进程
    • ID 为 1 的进程通常是 init 进程,在自举过程结束时由内核调用
      • init 可成为所有孤儿进程的父进程
    • 每个 UNIX 系统实现都有它自己的一套提供操作系统服务的内核进程,例如,在某些 UNIX 的虚拟存储器实现中,进程 ID 2 是页守护进程 (page daemon),此进程负责支持虚拟存储器系统的分页操作
  • 除了进程 ID,每个进程还有一些其他标识符,下列函数返回这些标识符

    #include 
    
    pid_t getpid(void);     // 返回值:调用进程的进程 ID
    pid_t getppid(void);    // 返回值:调用进程的父进程 ID
    
    uid_t getuid(void);     // 返回值:调用进程的实际用户 ID
    uid_t geteuid(void);    // 返回值:调用进程的有效用户 ID
    
    gid_t getgid(void);     // 返回值:调用进程的实际组 ID
    gid_t getegid(void);    // 返回值:调用进程的有效组 ID
    

3.2 函数 fork

#include 

pid_t fork(void);
  • 一个现有的进程可以调用 fork 函数创建一个新进程(称为子进程,child process)
    • fork 函数被调用一次,但返回两次
      • 两次返回的区别是:子进程的返回值是 0,而父进程的返回值则是新建子进程的进程 ID
    • 将子进程 ID 返回给父进程的理由
      • 因为一个进程的子进程可以有多个,并且没有一个函数使一个进程可以获得其所有子进程的进程 ID
    • fork 使子进程得到返回值 0 的理由
      • 一个进程只会有一个父进程,所以子进程总是可以调用 getppid 以获得其父进程的进程 ID (进程 ID 0 总是由内核交换进程使用,所以一个子进程的进程 ID 不可能为 0)
    • 子进程和父进程继续执行 fork 调用之后的指令,子进程是父进程的副本
      • 例如,子进程获得父进程数据空间、堆和栈的副本。注意,这是子进程所拥有的副本。父进程和子进程并不共享这些存储空间部分
  • 函数返回值
    • 子进程返回 0,父进程返回子进程 ID
    • 若出错,返回 -1

Linux系统编程(三):进程_第9张图片

  • 在 fork 之后处理文件描述符有以下两种常见的情况

    • (1) 父进程等待子进程完成。在这种情况下,父进程无需对其描述符做任何处理。当子进程终止后,它曾进行过读、写操作的任一共享描述符的文件偏移量已做了相应更新
    • (2) 父进程和子进程各自执行不同的程序段。在这种情况下,在 fork 之后,父进程和子进程各自关闭它们不需使用的文件描述符,这样就不会干扰对方使用的文件描述符。这种方法是网络服务进程经常使用的
  • 父进程和子进程之间的对比

    • 不同点
      • fork 的返回值不同
      • 进程 ID 不同
      • 这两个进程的父进程 ID 不同
        • 子进程的父进程 ID 是创建它的进程的 ID,而父进程的父进程 ID 则不变
      • 子进程的 tms_utime、tms_stime、tms cutime 和 tms_ustime 的值设置为 0
      • 子进程不继承父进程设置的文件锁
      • 子进程的未处理闹钟被清除
      • 子进程的未处理信号集设置为空集
    • 相同点(刚 fork 后)
      • data 段、text 段
      • 堆、栈
      • 环境变量、全局变量
      • 宿主目录位置、进程工作目录位置
      • 信号处理方式
  • 使 fork 失败的两个主要原因

    • 系统中已经有了太多的进程 (通常意味着某个方面出了问题)
    • 该实际用户 ID 的进程总数超过了系统限制
  • fork 有以下两种用法

    • 一个父进程希望复制自己,使父进程和子进程同时执行不同的代码段
      • 这在网络服务进程中是常见的:父进程等待客户端的服务请求。当这种请求到达时,父进程调用 fork,使子进程处理此请求,父进程则继续等待下一个服务请求
    • 一个进程要执行一个不同的程序
      • 这对 shell 是常见的情况。在这种情况下,子进程从 fork 返回后立即调用exec

案例 1

  • 子进程对变量所做的改变并不影响父进程中该变量的值
#include 
#include 
#include 
#include 
#include 
#include 

int globvar = 6;
char buf[] = "a write to stdout\n";

int main(int argc, char* argv[]) {
    int var;
    pid_t pid;

    var = 88;
    if (write(STDOUT_FILENO, buf, sizeof(buf) - 1) != sizeof(buf) - 1) {
        perror("write error");
        exit(1);
    }
    printf("before fork\n");

    if ((pid = fork()) < 0) {
        perror("fork error");
        exit(1);
    } else if (pid == 0) {
        globvar++;
        var++;
    } else {
        sleep(2);  // 父进程使自己休眠 2s,以此使子进程先执行
    }

    printf("pid = %ld, glob = %d, var = %d\n", (long)getpid(), globvar, var);

    return 0;
}
$ gcc fork.c -o fork
$ ./fork
a write to stdout
before fork
pid = 2244, glob = 7, var = 89   # 子进程的变量值改变了
pid = 2243, glob = 6, var = 88   # 父进程的变量值没改变
  • 一般来说,在 fork 之后是父进程先执行还是子进程先执行是不确定的,这取决于内核所使用的调度算法。如果要求父进程和子进程之间相互同步,则要求某种形式的进程间通信

案例 2

#include 
#include 
#include 
#include 
#include 
#include 
#include 

int main(int argc, char* argv[]) {
    printf("before fork-1-\n");
    printf("before fork-2-\n");
    printf("before fork-3-\n");
    printf("before fork-4-\n");

    pid_t pid = fork();
    if (pid == -1) {
        perror("fork error");
        exit(1);
    } else if (pid == 0) {
        printf("---child is created, pid = %d, parent-pid : %d\n", getpid(), getppid());
    } else if (pid > 0) {
        sleep(1);  // 给父进程增加一个等待命令,这样能保证子进程完成时,父进程处于执行状态,子进程就不会成孤儿
        printf("---parent process : my child is %d, my pid : %d, my parent pid : %d\n", pid, getpid(), getppid());
    }

    printf("------end of file\n");

    return 0;
}
$ gcc fork2.c -o fork2
$ ./fork2
before fork-1-
before fork-2-
before fork-3-
before fork-4-
---child is created, pid = 2475, parent-pid : 2474
------end of file
---parent process : my child is 2475, my pid : 2474, my parent pid : 1887
------end of file

# 写的所有进程都是 bash 的子进程
$ ps aux | grep 1887
yue       1887  0.0  0.0  25124  6048 pts/0    Ss   08:41   0:00 bash
yue       2477  0.0  0.0  16180  1088 pts/0    S+   09:39   0:00 grep --color=auto 1887

案例 3

  • 循环创建多个子进程
#include 
#include 
#include 
#include 
#include 
#include 
#include 

int main(int argc, char* argv[]) {
    int i;
    pid_t pid;

    for (i = 0; i < 5; i++) {
        if (fork() == 0) {
            break;
        }
    }

    if (5 == i) {
        sleep(5);
        printf("I'm parent \n ");
    } else {
        sleep(i);
        printf("I'm %dth child\n", i + 1);
    }

    return 0;
}
$ gcc mulfork.c -o mulfork
$ ./mulfork
I'm 1th child
I'm 2th child
I'm 3th child
I'm 4th child
I'm 5th child
I'm parent

案例 4

  • 父子进程共享:读时共享,写时复制(主要针对全局变量)
    • 共享两个东西:文件描述符和 mmap 映射区
#include 
#include 
#include 

int var = 100;            //.data 

int main(void) {
    pid_t pid;
    pid = fork();
    
    if(pid == -1){	// son
        perror("fork error");
        exit(1);
    } else if (pid > 0) {
        var = 288;
        printf("parent, var = %d\n", var);
        printf("I'm parent pid = %d, getppid = %d\n", getpid(), getppid());
    } else if (pid == 0) {
        var = 200;
        printf("I'm child pid = %d, ppid = %d\n", getpid(), getppid());
        printf("child, var = %d\n", var);
    }
    
    printf("------finish------\n");
    
    return 0;
}
$ gcc shared.c -o shared
$ ./shared
parent, var = 288
I'm parent pid = 2702, getppid = 1887
------finish------
I'm child pid = 2703, ppid = 2702
child, var = 200
------finish------

案例 5

  • 父、子进程 gdb 调试
    • 使用 gdb 调试的时候,gdb 只能跟踪一个进程。可以在 fork 函数调用之前,通过指令设置 gdb 调试工具跟踪父进程或者是跟踪子进程,默认跟踪父进程
    • set follow-fork-mode child 命令设置 gdb 在 fork 之后跟踪子进程
    • set follow-fork-mode parent 设置跟踪父进程

    注意,一定要在 fork 函数调用之前设置才有效

$ gcc mulfork.c -o mulfork -g
$ gdb mulfork
(gdb) list
1	#include 
2	#include 
3	#include 
4	#include 
5	#include 
6	#include 
7	#include 
8	
9	int main(int argc, char* argv[]) {	
10		int i;
(gdb) l
11		pid_t pid;
12	
13		for (i = 0; i < 5; i++) {
14			if (fork() == 0) {
15				break;
16			}
17		}
18	
19		if (5 == i) {
20			sleep(5);
(gdb) b 13
Breakpoint 1 at 0x6e9: file mulfork.c, line 13.
(gdb) r
Starting program: /home/yue/test/mulfork 

Breakpoint 1, main (argc=1, argv=0x7fffffffdbe8) at mulfork.c:13
13		for (i = 0; i < 5; i++) {
(gdb) n
14			if (fork() == 0) {
(gdb) set follow-fork-mode child 
(gdb) n
[New process 2831]
[Switching to process 2831]
main (argc=1, argv=0x7fffffffdbe8) at mulfork.c:15
15				break;
(gdb) I'm 2th child
I'm 3th child
I'm 4th child
I'm 5th child
I'm parent 
 n
19		if (5 == i) {
(gdb) n
23			sleep(i);
(gdb) n
24			printf("I'm %dth child\n", i + 1);
(gdb) n
I'm 1th child
27		return 0;
(gdb) 

3.3 函数 exit

  • 有 8 种方式使进程终止 (termination)

    • 其中 5 种为正常终止,它们是
      • (1) 从 main 返回
      • (2) 调用 exit
      • (3) 调用 _exit 或 _Exit
      • (4) 最后一个线程从其启动例程返回
      • (5) 从最后一个线程调用 pthread_exit
    • 异常终止有 3 种方式,它们是
      • (6) 调用 abort
      • (7) 接到一个信号
      • (8) 最后一个线程对取消请求做出响应
  • 不管进程如何终止,最后都会执行内核中的同一段代码

    • 这段代码为相应进程关闭所有打开描述符,释放它所使用的存储器等
  • 对上述任意一种终止情形,都希望终止进程能够通知其父进程它是如何终止的

    • 对于 3 个终止函数 (exit、_exit 和 _Exit),实现方法是:将其退出状态作为参数传送给函数 (返回给父进程)
    • 在异常终止情况,内核 (不是进程本身) 产生一个指示其异常终止原因的终止状态。在任意一种情况下,该终止进程的父进程都能用 wait 或 waitpid 函数取得其终止状态
  • 孤儿进程

    • 父进程先于子进程结束,则子进程成为孤儿进程,子进程的父进程成为 init 进程,称为 init 进程领养孤儿进程
  • 僵尸进程 (zombie)

    • 在 UNIX 术语中,一个已经终止、但是其父进程尚未对其进行善后处理 (获取终止子进程的有关信息、释放它仍占用的资源) 的进程,子进程残留资源 (PCB) 存放于内核中
    • ps(1) 命令将僵死进程的状态打印为 Z
    • 如果编写一个长期运行的程序,它 fork 了很多子进程,那么除非父进程等待取得子进程的终止状态,不然这些子进程终止后就会变成僵尸进程
  • 一个由 init 程收养的进程终止时会不会变成个僵尸进程?

    • 不会。因为 init 被编写成无论何时只要有一个子进程终止,init 就会调用一个 wait 函数取得其终止状态。这样也就防止了在系统中塞满僵尸进程

案例 1:孤儿进程

#include 
#include 
#include 

int main(int argc, char* argv[]) {
    pid_t pid;
    pid = fork();
    
    if (pid == 0) {
        while (1) {
            printf("I am child, my parent pid = %d\n", getppid());
            sleep(1);
        }
    } else if (pid > 0) {
        printf("I am parent, my pid is = %d\n", getpid());
        sleep(9);
        printf("------parent going to die------\n");
    } else {
        perror("fork");
        return 1;
    }
    
    return 0;
}
$ gcc orphan.c -o orphan
$ ./orphan
I am parent, my pid is = 4464
I am child, my parent pid = 4464
I am child, my parent pid = 4464
I am child, my parent pid = 4464
I am child, my parent pid = 4464
I am child, my parent pid = 4464
I am child, my parent pid = 4464
I am child, my parent pid = 4464
I am child, my parent pid = 4464
I am child, my parent pid = 4464
------parent going to die------
I am child, my parent pid = 1112
I am child, my parent pid = 1112
I am child, my parent pid = 1112
...
# 父进程死亡前
$ ps ajx
4231  4383  4383  4231 pts/0     4383 S+    1000   0:00 ./orphan
4383  4384  4383  4231 pts/0     4383 S+    1000   0:00 ./orphan

# 父进程死亡后
$ ps ajx
1112  4384  4383  4231 pts/0     4231 S     1000   0:00 ./orphan

案例 2:僵尸进程

#include 
#include 
#include 

int main(int argc, char* argv[]) {
    pid_t pid;
    pid = fork();

    if (pid == 0) {
        printf("------child, my parent = %d, going to sleep 10s\n", getppid());
        sleep(10);
        printf("------child die------\n");
    } else if (pid > 0) {
        while (1) {
            printf("I am parent, pid = %d, myson = %d\n", getpid(), pid);
            sleep(1);
        }
    } else {
        perror("fork");
        return 1;
    }

    return 0;
}
$ gcc zoom.c -o zoom
$ ./zoom
I am parent, pid = 4660, myson = 4661
------child, my parent = 4660, going to sleep 10s
I am parent, pid = 4660, myson = 4661
I am parent, pid = 4660, myson = 4661
I am parent, pid = 4660, myson = 4661
I am parent, pid = 4660, myson = 4661
I am parent, pid = 4660, myson = 4661
I am parent, pid = 4660, myson = 4661
I am parent, pid = 4660, myson = 4661
I am parent, pid = 4660, myson = 4661
I am parent, pid = 4660, myson = 4661
------child die------
I am parent, pid = 4660, myson = 4661
I am parent, pid = 4660, myson = 4661
I am parent, pid = 4660, myson = 4661
...
# 子进程死亡前
$ ps ajx
4505  4660  4660  4505 pts/3     4660 S+    1000   0:00 ./zoom
4660  4661  4660  4505 pts/3     4660 S+    1000   0:00 ./zoom

# 子进程死亡后
$ ps ajx
4505  4660  4660  4505 pts/3     4660 S+    1000   0:00 ./zoom
4660  4661  4660  4505 pts/3     4660 Z+    1000   0:00 [zoom]  # defunct 代表死亡

# 每个进程结束后都必然会经历僵尸态,时间长短的差别而已
# 回收僵尸进程,得 kill 它的父进程,让孤儿院去回收它
$ kill -9 4660

3.4 函数 wait 和 waitpid

  • 一个进程正常或异常终止时,内核就向其父进程发送 SIGCHLD 信号。因为子进程终止是个异步事件 (这可以在父进程运行的任何时候发生),所以这种信号也是内核向父进程发的异步通知
    • 父进程可以选择忽略该信号,或者提供一个该信号发生时即被调用执行的函数 (信号处理程序)
  • 调用 wait 或 waitpid 的作用
    • 如果其所有子进程都还在运行,则阻塞(阻塞等待子进程退出
    • 如果一个子进程已终止,正等待父进程获取其终止状态,则取得该子进程的终止状态后立即返回(回收子进程残留资源
    • 如果它没有任何子进程,则立即出错返回(获取子进程结束状态/退出原因

    一次 wait/waitpid 函数调用,只能回收一个进程

#include 

pid_t wait(int* status);
pid_t waitpid(pid_t pid, int* status, int options);
  • 函数返回值

    • 若成功,返回进程 ID
    • 若出错,返回 0 或 -1
  • 这两个函数的区别

    • 在一个子进程终止前,wait 使其调用者阻塞,而 waitpid 有一选项,可使调用者不阻塞
    • waitpid 并不等待在其调用之后的第一个终止子进程,它有若干个选项,可以控制它所等待的进程
    • 如果子进程已经终止,并且是一个僵尸进程,则 wait 立即返回并取得该子进程的状态;否则 wait 使其调用者阻塞,直到一个子进程终止
    • 如果调用者阻塞而且它有多个子进程,则在其某子进程终止时,wait 就立即返回。因为 wait 返回终止子进程的进程 ID,所以它总能了解是哪一个子进程终止了
  • 这两个函数的参数 status 是一个整型指针

    • 如果 status 不是一个空指针,则终止进程的终止状态就存放在它所指向的单元内
    • 如果不关心终止状态,则可将该参数指定为空指针 NULL
  • 检查 wait 和 waitpid 所返回的终止状态的宏

Linux系统编程(三):进程_第10张图片

  • 如果要等待一个指定的进程终止 (假设知道要等待进程的 ID) 那么该如何做呢?

    • 在早期的 UNIX 版本中,必须调用 wait,然后将其返回的进程 ID 和所期的进程 ID 比较
      • 如果终止进程不是所期望的,则将该进程 ID 和终止状态保存起来,然后再次调用 wait。反复这样做,直到所期望的进程终止。下一次又想等待一个特定进程时,先查看已终止的进程列表,若其中已有要等待的进程,则获取相关信息,否则调用 wat
    • 其实,需要的是等待一个特定进程的函数。POSIX 定义了 waitpid 函数以提供这种功能对于 waitpid函数中 pid 参数的作用解释如下
      • pid = -1 回收任一子进程。此种情况下,waitpid 与 wait 等效
      • pid > 0 回收指定 ID 的子进程
      • pid = 0 回收和当前调用 waitpid 一个进程组的所有子进程
      • pid < -1 回收指定进程组内的任一子进程
  • waitpid 函数返回终止子进程的进程 ID,并将该子程的终止状态存放在由 ststus 指向的存储单元中。对于 wait,其唯一的出错是调用进程没有子进程(函数调用被一个信号中断时也可能返回另一种出错),但是对于 waitpid,如果指定的进程或进程组不存在,或者参数 pid 指定的进程不是调用进程的子进程,都可能出错

  • waitpid 的 options 常量

Linux系统编程(三):进程_第11张图片

  • waitpid 函数提供了 wait 函数没有提供的 3 个功能
    • (1) waitpid 可等待一个指定/特定的进程,而 wait 则返回任一终止子进程的状态
    • (2) waitpid 提供了一个 wait 的非阻塞版本。有时希望获取一个子进程的状态,但不想阻塞
    • (3) waitpid 通过 WUNTRACED 和 WCONTINUED 选项支持作业控制

wait 案例

#include 
#include 
#include 
#include 

int main(int argc, char* argv[]) {
    pid_t pid, wpid;
    int status;

    pid = fork();
    // 返回的值为 0,表示当前进程是子进程
    if (pid == 0) {
        printf("---child, my id = %d, going to sleep 5s\n", getpid());
        sleep(10);
        printf("------child die------\n");

        // 子进程执行完毕后,将返回 66,表示子进程正常终止
        return 66;
    } else if (pid > 0) {
     // wpid = wait(NULL);     // 不关心子进程结束原因
        wpid = wait(&status);  // wait() 函数会使当前进程阻塞,直到一个子进程终止
        if (wpid == -1) {
            perror("wait error");
            exit(1);
        }
        if (WIFEXITED(status)) {    // 判断子进程是否正常终止
            printf("child exit with %d\n", WEXITSTATUS(status));
        }
        if (WIFSIGNALED(status)) {  // 判断子进程是否被信号终止
            printf("child kill with signal %d\n", WTERMSIG(status));
        }

        printf("------parent wait finish: %d\n", wpid);
    } else {
        perror("fork");
        return 1;
    }

    return 0;
}
$ gcc zoom_test.c -o zoom_test
$ ./zoom_test
---child, my id = 2774, going to sleep 10s
------child die------
child exit with 66
------parent wait finish: 2774

# 测试子进程被信号终止
$ ./zoom_test
---child, my id = 2864, going to sleep 5s
child kill with signal 9
------parent wait finish: 2864
# 另开一个终端,输入下列指令
$ kill -9 2864

waitpid 案例 1

  • 指定回收一个子进程
#include 
#include 
#include 
#include 
#include 
#include 


int main(int argc, char *argv[]) {
    int i;
    pid_t pid, wpid, tmpid;

    for (i = 0; i < 5; i++) {       
        pid = fork();
        if (pid == 0) {       // 循环期间, 子进程不 fork 
            break;
        }
        if (i == 2) {
            tmpid = pid;
            printf("--------pid = %d\n", tmpid);
        }
    }

    if (5 == i) {       // 父进程, 从表达式 2 跳出
     // sleep(5);

        //wait(NULL);                            // 一次wait/waitpid函数调用,只能回收一个子进程.
        //wpid = waitpid(-1, NULL, WNOHANG);     // 回收任意子进程,没有结束的子进程,父进程直接返回0 
        //wpid = waitpid(tmpid, NULL, 0);        // 指定一个进程回收, 阻塞等待
        printf("i am parent , before waitpid, pid = %d\n", tmpid);

        //wpid = waitpid(tmpid, NULL, WNOHANG);  // 指定一个进程回收, 不阻塞
        wpid = waitpid(tmpid, NULL, 0);          // 指定一个进程回收, 阻塞回收
        if (wpid == -1) {
            perror("waitpid error");
            exit(1);
        }
        printf("I'm parent, wait a child finish : %d \n", wpid);

    } else {            // 子进程, 从 break 跳出
        sleep(i);
        printf("I'm %dth child, pid= %d\n", i+1, getpid());
    }

    return 0;
}
$ gcc waitpid_test.c -o waitpid_test
$ ./waitpid_test
--------pid = 3133
i am parent , before waitpid, pid = 3133
I'm 1th child, pid= 3131
I'm 2th child, pid= 3132
I'm 3th child, pid= 3133
I'm parent, wait a child finish : 3133 
$ I'm 4th child, pid= 3134
I'm 5th child, pid= 3135

waitpid 案例 2

  • 回收多个子进程
#include 
#include 
#include 
#include 
#include 
#include 

int main(int argc, char *argv[]) {
    int i;
    pid_t pid, wpid, tmpid;

    for (i = 0; i < 5; i++) {
        pid = fork();
        if (pid == 0) {
            break;
        }
    }

    if (5 == i) {
        while ((wpid = waitpid(-1, NULL, WNOHANG)) != -1) {
            if (wpid > 0) {
                printf("wait child %d\n", wpid);
            } else if (wpid == 0) {
                sleep(1);
                continue;
            }
        }
    } else {
        sleep(i);
        printf("I'm %dth child, pid = %d\n", i+1, getpid());
    }

    return 0;
}
$ gcc waitpid_while.c -o waitpid_while
$ ./waitpid_while
I'm 1th child, pid = 3360
wait child 3360
I'm 2th child, pid = 3361
wait child 3361
I'm 3th child, pid = 3362
I'm 4th child, pid = 3363
wait child 3362
wait child 3363
I'm 5th child, pid = 3364
wait child 3364

3.5 函数 exec

Linux系统编程(三):进程_第12张图片

  • 用 fork 函数创建新的子进程后,子进程往往要调用一种 exec 函数以执行另一个程序
    • 当进程调用一种 exec 函数时,该进程执行的程序完全替换为新程序,而新程序则从其 main 函数开始执行
    • 因为调用 exec 并不创建新进程,所以前后的进程 ID 并未改变
    • exec 只是用磁盘上的一个新程序替换了当前进程的正文段、数据段、堆段和栈段
    • 将当前进程的 .text、.data 替换为所要加载的程序的 .text、.data,然后让进程从新的 .text 第一条指令开始执行,但进程 ID 不变,换核不换壳
  • 用 fork 可以创建新进程,用 exec 可以初始执行新的程序。exit 函数和 wait 函数处理终止和等待终止
#include 

extern char **environ;

// 字母 p(path) 表示该函数取 filename 作为参数,并且用 PATH 环境变量寻找可执行文件
// 字母 l(list) 表该函数取一个参数表,它与字母 v 互斤
// 字母 v(vector) 表示该函数取一个 argv[] 矢量
// 字母 e(environment) 表示该函数取 envp[] 数组,而不使用当前环境
int execl(const char *path, const char *arg, ... /* (char  *) NULL */);
int execlp(const char *file, const char *arg, ... /* (char  *) NULL */);

int execle(const char *path, const char *arg, ... /*, (char *) NULL, char * const envp[] */);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execve(const char *path, char *const argv[], char *const envp[]);
int fexecve(int fd, char *const argv[], char *const envp[]);
  • 函数返回值

    • 若成功,不返回
    • 若出错,返回 -1
  • 在很多 UNIX 实现中,这 7 个函数中只有 execve 是内核的系统调用。另外 6 个只是库函数,它们最终都要调用该系统调用。这 7 个函数之间的关系如下图

    • 库函数 execlp 和 execvp 使用 PATH 环境变量,查找第一个包含名为 filename 的可执行文件的路径名前缀。fexecve 库函数使用 /proc 把文件描述符参数转换成路径名,execve 用该路径名去执行程序

Linux系统编程(三):进程_第13张图片

案例 1

  • execlp 函数通常用来调用系统程序。如 ls、date、cp、cat 命令
  • execl 函数加载一个进程,通过(路径 + 程序名)来加载
  • execvp 函数加载一个进程,使用自定义环境变量 env
#include 
#include 
#include 
#include 
#include 
#include 
#include 

int main(int argc, char* argv[]) {
    pid_t pid = fork();
    if (pid == -1) {
        perror("fork error");
        exit(1);
    } else if (pid == 0) {
    //  execlp("ls", "-l", "-h", NULL); // 错误,可变参数是从 argv[0] 开始计算
    //  execlp("ls", "ls", "-l", "-h", NULL);
    //  execlp("date", "date", NULL);
    //  execl("/bin/ls", "ls", "-l", "-h", NULL);

        // NULL 为必须提供的,称为 “哨兵”
        char* argv[] = {"ls", "-l", "-h", NULL};
        execvp("ls", argv);

        perror("exec error");
        exit(1);
    } else if (pid > 0) {
        sleep(1);  // 让父进程延时 1 秒,保证终端提示符不和输出干扰
        printf("I'm parent : %d\n", getpid());
    }

    return 0;
}
$ gcc fork_exec.c -o fork_exec
$ ./fork_exec
total 152K
-rwxrwxr-x 1 yue yue 8.6K 9月  16 15:42 a.out
-rwxrwxr-x 1 yue yue 8.2K 9月  16 16:15 exec
-rw-rw-r-- 1 yue yue  282 9月  16 16:15 exec.c
-rwxrwxr-x 1 yue yue 8.2K 9月  15 16:55 fcntl
-rwxrwxr-x 1 yue yue 8.3K 9月  15 18:46 fcntl2
-rwxrwxr-x 1 yue yue 8.5K 9月  17 08:52 fork
-rwxrwxr-x 1 yue yue 8.5K 9月  17 09:38 fork2
-rw-rw-r-- 1 yue yue  672 9月  17 09:37 fork2.c
-rw-rw-r-- 1 yue yue  627 9月  17 08:52 fork.c
-rwxrwxr-x 1 yue yue 8.4K 9月  17 16:33 fork_exec
-rw-rw-r-- 1 yue yue  447 9月  17 16:33 fork_exec.c
-rwxrwxr-x 1 yue yue 8.6K 9月  15 19:33 ls-R
-rw-rw-r-- 1 yue yue  943 9月  15 19:31 ls-R.c
-rwxrwxr-x 1 yue yue  12K 9月  17 14:18 mulfork
-rw-rw-r-- 1 yue yue  398 9月  17 11:30 mulfork.c
-rw-r--r-- 1 yue yue  262 9月  15 18:46 mycat.c
-rwxrwxr-x 1 yue yue 8.4K 9月  17 11:32 shared
-rw-rw-r-- 1 yue yue  572 9月  17 11:32 shared.c
I'm parent : 3964

案例 2

  • 使用 execlp 执行进程查看,并将结果输出到文件里
    • 等价于实现 ps aux 指令
#include 
#include 
#include 
#include 
#include 
#include 
#include 

int main(int argc, char* argv[]) {
    int fd;
    fd = open("ps.out", O_WRONLY | O_CREAT | O_TRUNC, 0644);
    if (fd < 0) {
        perror("open error");
        exit(1);
    }

    dup2(fd, STDOUT_FILENO);
    execlp("ps", "ps", "aux", NULL);

    close(fd);

    return 0;
}
$ gcc exec_ps.c -o exec_ps
$ ./exec_ps
$ cat ps_out

你可能感兴趣的:(Linux系统编程,linux,服务器,学习,笔记,c++,stm32,单片机)