Linux - 进程

目录

进程的概念

进程包括了可执行程序的代码和数据,进程控制块 (PCB)

当一个可执行程序被执行后,内存中就会出现一个进程,如果多个可执行程序被执行,内存中就会出现多个进程,对于这些进程,OS 要进行管理,管理时遵循先描述后组织的方案,先使用结构体task_struct对进程的属性进行描述和保存,再用链表将task_struct组织起来,接下来 OS 对进程的管理就变成了对链表的增删查改了

Linux 中,PCB 实际上就是 task_struct 结构体,它用来描述进程的属性

进程的特性

  • 竞争性:系统中可能存在很多进程,但是 CPU 只有一个,进程会对 CPU 进行争抢,得到在上面运行的资格
  • 独立性:进程之前互相不会干扰
  • 并发性:不同的进程之间按照顺序执行。CPU 在一个进程完成任务时,可以通过进程切换的方式换另一个进程执行
  • 并行性:不同的进程可以同时在 CPU 上执行
  • 进程是动态的,因为它是在运行的,程序是静态的,因为它是没有在运行的
  • 一个被执行的程序可能包含多个进程

子进程,父进程

子进程是由父进程创建出来的进程,父进程如果要创建子进程,就要使用 fork 系统调用

cpp 复制代码
pid_t fork(void);

fork 的功能是给当前的父进程创建一个子进程,如果成功创建,给子进程返回 0,给父进程返回子进程的 id,也就是 pid。如果创建失败,给父进程返回 -1

子进程创建成功后,子进程会和父进程使用内存中的同一份代码和数据,子进程会执行 fork 函数之后的代码。在子进程或父进程要修改数据时,OS 会将内存中的数据拷贝一份,放在新的位置,让子进程或父进程进行修改,也就是写时拷贝

验证

cpp 复制代码
#include <iostream>
#include <cstdio>
#include <unistd.h>
using namespace std;

int main()
{
    int val = 1;
    pid_t pid = fork();

    // 子进程
    if (pid == 0)
    {
        val = 100;
        printf("我是子进程, pid = %d, 父进程的pid = %d, val = %d\n", getpid(), getppid(), val);
        exit(1);
    }
    else if (pid == -1)
    {
        perror("fork");
        exit(2);
    }

    // 父进程
    sleep(10);
    printf("我是父进程, pid = %d, 子进程的pid = %d, val = %d\n", getpid(), pid, val);

    return 0;
}

结果

cpp 复制代码
我是子进程, pid = 3478489, 父进程的pid = 3478488, val = 100
我是父进程, pid = 3478488, 子进程的pid = 3478489, val = 1

父进程在子进程修改完 val 之后,才输出 val,但是 val 仍然是 1,说明了写时拷贝的现象


对于一个父进程而言,它的父进程是 bash,也就是命令行解释器

验证

cpp 复制代码
int main()
{
    int val = 1;
    pid_t pid = fork();

    // 子进程
    if (pid == 0)
    {
        val = 100;
        printf("我是子进程, pid = %d, 父进程的pid = %d\n", getpid(), getppid());
        exit(1);
    }
    else if (pid == -1)
    {
        perror("fork");
        exit(2);
    }

    // 父进程
    sleep(10);
    printf("我是父进程, pid = %d, 我的父进程的pid = %d, 子进程的pid = %d\n", getpid(), getppid(), pid);

    return 0;
}

结果

cpp 复制代码
我是子进程, pid = 3482892, 父进程的pid = 3482891
我是父进程, pid = 3482891, 我的父进程的pid = 3459766, 子进程的pid = 3482892

查看 pid3459766 的进程:

进程状态

状态一览

Linux 中,进程的状态由下方的这个数组来决定:

cpp 复制代码
static const char *const task_state_array[] = {
    "R (running)", 
    "S (sleeping)", 
    "D (disk sleep)", 
    "T (stopped)", 
    "t (tracing stop)", 
    "X (dead)", 
    "Z (zombie)", 
};
  • R:运行状态,进程在 CPU 上运行时所处的状态
  • S:浅睡眠状态,是可中断休眠,进程在等待某种资源时就会处于这个状态
  • D:深度睡眠状态,是不可中断休眠,进程在进行某些 IO 操作,比如向磁盘写数据的时候会处于这个状态
  • T:暂停状态,由用户使用 ctrl + zSIGSTOP 信号强制暂停的进程都会处于这个状态
  • t:调试的时候,进程被断点暂停时会处于这个状态
  • X:结束状态
  • Z:僵尸状态,当子进程运行结束时,子进程不会立马退出,而是会等待父进程获取自己的退出信息,此时子进程就会处于僵尸状态。处于僵尸状态时,进程不会被 kill 命令杀死,只要父进程没有回收退出信息,子进程就会一直待在内存中,耗费内存资源

防止僵尸状态的措施

验证僵尸状态

cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <stdlib.h>
using namespace std;

int main()
{
    int pid = fork();

    // 子进程
    if (pid == 0)
    {
        cout << "我是子进程,pid = " << getpid();
        exit(1);
    }

    // 父进程
    cout << "我是父进程,pid = %d" << getpid();
    while (true)
        ;
}
cpp 复制代码
   PPID     PID    PGID     SID TTY        TPGID STAT   UID   TIME COMMAND
4165291 4165292 4165291 4152775 pts/4    4165291 Z+    1000   0:00 [test] <defunct>

可以看到,子进程的状态是 Z+Z 表示是僵尸状态,+ 表示是前台进程

防止这个现象的出现,可以使用 wait 或者 waitpid 接口

wait

cpp 复制代码
pid_t wait(int *status);

作用

让父进程等待若干个子进程退出。父进程调用 wait 时,如果子进程还没退出,就会阻塞在调用处

参数

status:输出型参数,用来获取进程的退出状态,不需要时可以设置为 NULL

返回值

子进程如果退出了,会返回子进程的 pid,如果没有子进程或者子进程无效会返回 -1

使用例

cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/wait.h>
using namespace std;

int main()
{
    int pid = fork();

    // 子进程
    if (pid == 0)
    {
        cout << "我是子进程,pid = " << getpid() << endl;
        exit(1);
    }

    // 父进程
    cout << "我是父进程,pid = " << getpid() << endl;

    sleep(10);
    int ret = wait(NULL);
    if (ret < 0)
    {
        perror("wait");
        exit(2);
    }
    cout << "子进程退出成功" << endl;
}

运行结果

cpp 复制代码
我是父进程,pid = 4170564
我是子进程,pid = 4170565
子进程退出成功

子进程退出,父进程还未回收时

cpp 复制代码
   PPID     PID    PGID     SID TTY        TPGID STAT   UID   TIME COMMAND
4152775 4170564 4170564 4152775 pts/4    4170564 S+    1000   0:00 ./test
4170564 4170565 4170564 4152775 pts/4    4170564 Z+    1000   0:00 [test] <defunct>

父进程回收后

cpp 复制代码
   PPID     PID    PGID     SID TTY        TPGID STAT   UID   TIME COMMAND
4152775 4170564 4170564 4152775 pts/4    4170564 S+    1000   0:00 ./test

waitpid

cpp 复制代码
pid_t waitpid(pid_t pid, int *status, int options);

作用

让父进程等待指定的子进程或全部子进程退出

参数

pid :子进程的 pid,取 -1 表示等待所有子进程退出,取大于 0 的值表示等待指定的子进程退出

status:输出型参数,用来获取子进程的退出状态,不需要可以设置为 NULL

options:默认为 0,表示子进程未退出时,父进程会在调用处阻塞并等待。取 WNOHANG 时,不管子进程是否退出,父进程都不会阻塞在调用处

返回值

子进程正常退出,调用会返回 pid。如果 optionsWNOHANG 并且调用时没有已退出的子进程,返回 0。不存在子进程或子进程无效会返回 -1

使用例

cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/wait.h>
using namespace std;

int main()
{
    int pid = fork();

    // 子进程
    if (pid == 0)
    {
        cout << "我是子进程,pid = " << getpid() << endl;
        exit(1);
    }

    // 父进程
    cout << "我是父进程,pid = " << getpid() << endl;

    sleep(10);
    int ret = waitpid(pid, NULL, 0);
    if (ret < 0)
    {
        perror("wait");
        exit(2);
    }
    cout << "子进程退出成功" << endl;
}

运行结果

cpp 复制代码
我是父进程,pid = 1410
我是子进程,pid = 1411
子进程退出成功

子进程运行结束,父进程未回收

cpp 复制代码
   PPID     PID    PGID     SID TTY        TPGID STAT   UID   TIME COMMAND
4152775    1410    1410 4152775 pts/4       1410 S+    1000   0:00 ./test
   1410    1411    1410 4152775 pts/4       1410 Z+    1000   0:00 [test] <defunct>

父进程回收子进程

cpp 复制代码
   PPID     PID    PGID     SID TTY        TPGID STAT   UID   TIME COMMAND
4152775    1410    1410 4152775 pts/4       1410 S+    1000   0:00 ./test

孤儿进程

孤儿进程的概念

当子进程运行未结束,父进程先结束时,子进程就失去了自己的父进程,此时的子进程会被 1 号进程领养,它就变成了孤儿进程,此时的子进程是后台进程,把子进程领养的 1 号进程可以看成 OSOS 的一部分

验证孤儿进程

cpp 复制代码
int main()
{
    int pid = fork();

    // 子进程
    if (pid == 0)
    {
        int cnt = 0;
        while (cnt < 10)
        {
            printf("我是子进程, pid = %d, 我的父进程pid = %d\n", getpid(), getppid());
            sleep(1);
            cnt++;
        }
        exit(1);
    }

    // 父进程
    printf("我是父进程, pid = %d, 我的父进程pid = %d\n", getpid(), getppid());
    printf("父进程退出\n");

    return 0;
}

运行结果

cpp 复制代码
我是子进程, pid = 7167, 我的父进程pid = 7166
我是父进程, pid = 7166, 我的父进程pid = 4152775
父进程退出
我是子进程, pid = 7167, 我的父进程pid = 1
我是子进程, pid = 7167, 我的父进程pid = 1
我是子进程, pid = 7167, 我的父进程pid = 1
我是子进程, pid = 7167, 我的父进程pid = 1
我是子进程, pid = 7167, 我的父进程pid = 1
我是子进程, pid = 7167, 我的父进程pid = 1
我是子进程, pid = 7167, 我的父进程pid = 1
我是子进程, pid = 7167, 我的父进程pid = 1
我是子进程, pid = 7167, 我的父进程pid = 1

1 号进程

cpp 复制代码
   PPID     PID    PGID     SID TTY        TPGID STAT   UID   TIME COMMAND
      0       1       1       1 ?             -1 Ss       0   0:33 /usr/lib/systemd/systemd --system --deserialize=48 showopts

孤儿进程存在的意义

一个子进程在运行结束的时候会进入僵尸状态,如果父进程早就退出了,子进程又不被其他进程领养,那子进程的退出信息永远都不会被回收,它永远都不会结束,造成资源泄露,所以需要 1 号进程进行领养,防止出现这种情况

对于一个父进程,它有自己的父进程 bashbash 一般是不会退出的,所以父进程的退出信息一定会被回收,父进程不会进入僵尸状态或变成孤儿进程

进程优先级

系统的资源是有限的,所以需要让进程按照顺序来获取资源,优先级就是用来决定该顺序

的。在 Linux 中,优先级被保存在 task_struct 中,它的值越大,表明优先级越低,值越小,优先级越高

cpp 复制代码
F S   UID     PID    PPID  C PRI  NI ADDR SZ WCHAN  TTY          TIME CMD
0 S  1000  609610  606473  0  80   0 -  1854 hrtime pts/0    00:00:00 test

进程的属性中,PRI 表示进程的优先级,默认是 80,取值范围是 [60, 99]NI 是一个用来对优先级进行修正的属性,取值范围是 [-20, 19],进程的优先级等于 80 + NI80 就是 PRI 的默认值

如果要调整进程的优先级,可以使用 top, nice, renice 指令

这些指令在修改优先级的时候,如果是普通用户,能输入 [0, 19] 之间的数,大于 19 就会直接设置为 19,小于 0 就会拒绝请求。如果是管理员,能输入 [-20, 19] 之间的数,大于 19 就会设置为 19,小于 -20 就会设置为 -20。设置这些限制,是因为如果某一个用户一直调整同一个进程的优先级,将它的优先级调整得非常高,就会导致其它进程一直无法获得资源,进程进入饥饿状态

top

top 指令可以查看当前所有进程的属性,在 top 的界面下,输入 r,再输入进程的 pid,就可以输入一个值来修改指定进程的优先级

cpp 复制代码
#include <unistd.h>
int main()
{
    printf("我的pid = %d\n", getpid());
    sleep(10);
    printf("进程退出\n");
    return 0;
}

运行结果

cpp 复制代码
我的pid = 617582
进程退出

未修改时进程的优先级

cpp 复制代码
F S   UID     PID    PPID  C PRI  NI ADDR SZ WCHAN  TTY          TIME CMD
0 S  1000  618393  606473  0  80   0 -  1854 hrtime pts/0    00:00:00 test

用 top 修改后进程的优先级

cpp 复制代码
F S   UID     PID    PPID  C PRI  NI ADDR SZ WCHAN  TTY          TIME CMD
0 S  1000  618393  606473  0  99  19 -  1854 hrtime pts/0    00:00:00 test

nice

nice 可以修改程序刚启动起来时的优先级,用法为:

cpp 复制代码
nice -n [优先级的值] [执行程序]
cpp 复制代码
#include <unistd.h>
int main()
{
    printf("我的pid = %d\n", getpid());
    sleep(10);
    printf("进程退出\n");
    return 0;
}

运行结果

cpp 复制代码
我的pid = 617582
进程退出

指令及启动时的优先级

cpp 复制代码
nice -n 19 ./test

F S   UID     PID    PPID  C PRI  NI ADDR SZ WCHAN  TTY          TIME CMD
0 S  1000  627122  606473  0  99  19 -  1854 hrtime pts/0    00:00:00 test

renice

renice 可以修改已在运行中的进程的优先级,用法为:

cpp 复制代码
renice [优先级的值] [pid]
cpp 复制代码
#include <unistd.h>
int main()
{
    printf("我的pid = %d\n", getpid());
    sleep(10);
    printf("进程退出\n");
    return 0;
}

运行结果

cpp 复制代码
我的pid = 628918
进程退出

未修改时的优先级

cpp 复制代码
F S   UID     PID    PPID  C PRI  NI ADDR SZ WCHAN  TTY          TIME CMD
0 S  1000  628918  606473  0  80   0 -  1854 hrtime pts/0    00:00:00 test

指令及修改后的优先级

cpp 复制代码
renice 19 628918

F S   UID     PID    PPID  C PRI  NI ADDR SZ WCHAN  TTY          TIME CMD
0 S  1000  628918  606473  0  99  19 -  1854 hrtime pts/0    00:00:00 test

进程切换

进程 A 在运行的时候,会产生上下文数据,这些上下文数据会被保存在 CPU 的寄存器中,当要切换至进程 B 运行时,A 产生的上下文数据会被保存回它的 task_struct 中的 TSS 任务状态段内,保证下次运行可以恢复数据,接下来恢复进程 B 的上下文数据到 CPU 的寄存器内,进程 B 就可以接着运行了

Linux 中,有一个结构体叫做 runqueue,它是一个和进程切换关系非常大的结构体

说明 runqueue 之前,先说明一下 rqueue_elem 结构体,在 rqueue_elem 结构体中:

  • nr_active:表示当前活动进程的数量
  • bitmap :位图,有 5 个 int 型数据,就有了 160 个比特位,可以表示 160 个队列的状态,1 表示 queue 中的某一个队列有进程,0 表示没有
  • queue :数组,保存了 140 个队列,每个队列都是用来存放相同优先级进程的

runqueue 中,有一个可以保存两个 rqueue_elem 类型元素的数组 prio_array,指向 prio_array[0] 的指针 active 和指向 prio_array[1] 的指针 expired,在这其中,prio_array[0] 用来保存就绪进程,prio_array[1] 用来保存时间片用完的进程

创建新进程的时候,如果这个进程是一个实时进程,它的优先级是 [0, 99],会根据优先级被直接插入到 queue[0, 99] 下标所对应的队列中。如果这个进程是普通进程,os 会根据它的优先级来计算要插入的位置,具体方法是 100 + nice + 20nice 的范围是 [-20, 19],因此它会被插入到 [100, 139] 下标对应的队列中

CPU 进行调度的时候,会先根据 active 指针找到 prio_array[0],查看 nr_active 是否大于 0,大于 0 说明有就绪的进程,此时会根据 bitmap 中的二进制位查看 queue 中哪个队列不是空的,找到这个非空队列,拿出队头的第一个进程,将它调度到 CPU 上执行。在进程执行完毕后,根据 expired 指针,将进程放到 prio_array[1]queue 中,然后继续刚才的步骤进行调度,当最后没有就绪进程时,交换 activeexpired,重新进行调度

根据这些,如果一个用户直接去修改进程的优先级 PRI,那么进程就会在队列中重新寻找插入位置,在时间上会有消耗,因此 Linux 设计了 NI 让用户进行修改,当用户修改了 NI 时,进程在队列中的位置不会马上发生改变,而是在进程运行完毕后,再计算新的插入位置进行插入

进程的虚拟地址空间

虚拟地址空间的概念

os 为每个进程都分配了虚拟地址空间,虚拟地址就是其中的一个地址,每个进程都拥有自己的虚拟地址空间。一个地址对应 1B,在 32 位机器上,一个进程的虚拟地址空间一共有 232 个地址,也就是 232B,等于 4GB。在 64 位机器上,一个进程的虚拟地址空间一共有 264 个地址,也就是 264B,等于 234GB

物理地址是内存中的地址

平常在写程序的时候接触到的地址实际上都是虚拟地址,而不是真正的物理地址,一个程序员是看不见物理地址的

虚拟地址空间的样子大致为:

页表

每个进程都会有自己的页表,页表中保存了虚拟地址和物理地址的映射关系,如果某个进程想要访问内存中的代码和数据,就可以用虚拟地址在页表中查找对应的物理地址,转到内存中去访问代码和数据

在一个程序被双击,但是还没有完全启动时(被加载到内存之前),此时 os 会根据程序的代码段,数据段的大小在虚拟地址空间中开辟相同大小的代码段和数据段,再把程序的代码和数据拷贝到物理内存中,最后在页表中建立虚拟地址和物理地址的映射关系

虚拟地址空间的原理

在进程的 PCB,也就是 task_struct 中,有一个 struct mm_struct* 类型的指针 mm,它指向了 mm_struct 类型的结构体,在这个结构体中又有一个变量 mmap,它是 struct vm_area_struct* 类型的指针,指向了一个链表或红黑树,在这个链表或红黑树中,保存了 vm_area_struct 类型的结构体,它的内部有两个长整形变量 vm_startvm_end,这两个变量划分出了虚拟地址空间中的栈,堆,共享区等区域,如果要修改区域的大小,就是修改这两个变量

根据这些,可以说虚拟地址空间本质就是一个结构体 mm_struct,而栈,堆,共享区本质就是 vm_area_struct 结构体中的两个变量

写时拷贝

父进程在未创建出子进程的时候,页表中记录的代码段的权限为只读,数据段的权限为可读可写,在父进程刚创建出子进程的时候,会将页表中数据段的权限修改为只读,然后 os 创建子进程的虚拟地址空间和页表,建立虚拟地址到物理地址的映射关系,此时子进程所用的代码和数据与父进程是一样的

当子进程要修改数据的时候,会通过虚拟地址查找页表,找到页表项后发现数据的权限是只读,于是就会引发错误,os 通过错误知道子进程要修改数据,就会触发写时拷贝机制,把要修改的数据在内存中拷贝一份,再修改页表,让子进程去修改拷贝出来的数据

虚拟地址空间的意义

  • 可执行程序的代码和数据在内存中可能不是连续存放的,但是在引入了虚拟地址空间后,可执行程序的代码和数据在虚拟地址空间中是连续存放的,这样就可以将原本在物理内存中无序的代码和数据转化为有序的,更方便找到代码和数据
  • 由于页表的存在,当进程要做一些非法操作时,就会被 os 拦截,这能够保护物理内存。比如说,动态申请的空间在被释放的时候,会让物理内存中开辟的空间,页表项以及虚拟地址空间内堆的那块空间都被释放,之后进程还想通过虚拟地址访问,就无法在页表中查找到映射关系,会被 os 拦截
  • 可以让进程管理和内存管理进行解耦。比如进程在用虚拟地址查找页表时,如果页表项中没有保存物理地址,说明对应的数据或代码还没有加载到内存中,此时 os 会引发缺页中断,停止进程的执行,将数据或代码加载到内存中,在页表中记录物理地址,然后进程就可以继续访问数据或代码,缺页中断的过程进程不用关心,它只需要拿到最后的物理地址

进程退出

进程退出的概念

进程退出指进程停止运行,一般来说,在三种情况下进程会退出:

  • 进程运行完毕,结果无错误
  • 进程运行完毕,结果有错误
  • 进程遇到异常

进程在退出的时候,会设置自己的退出码,退出码一般和 main 函数的返回值是一致的,如果程序员没有给定,默认返回 0 ,表示运行完毕无错误,返回非 0 表示运行完毕且遇到了错误。使用指令 echo $? 可以查看当前进程的退出码,使用 strerror 函数则可以查看所有的退出码以及它们的含义

验证

cpp 复制代码
#include <stdio.h>
#include <string.h>

int main()
{
    for (int i = 0; i <= 200; ++i)
    {
        printf("%d : %s\n", i, strerror(i));
    }
    return 0;
}

结果

cpp 复制代码
0 : Success
1 : Operation not permitted
2 : No such file or directory
3 : No such process
4 : Interrupted system call
5 : Input/output error
6 : No such device or address
7 : Argument list too long
8 : Exec format error
9 : Bad file descriptor
10 : No child processes
//....

当前进程的退出码

cpp 复制代码
0

如果进程在运行的过程中遇到了异常,比如除 0,野指针问题,那这个进程会被信号杀死,使用 kill -l 可以查看所有的信号

验证

cpp 复制代码
int main()
{
    int x = 1 / 0;
    return 0;
}

结果

cpp 复制代码
Floating point exception (core dumped) //8号信号

所有的信号

cpp 复制代码
 1) SIGHUP       2) SIGINT       3) SIGQUIT      4) SIGILL       5) SIGTRAP
 6) SIGABRT      7) SIGBUS       8) SIGFPE       9) SIGKILL     10) SIGUSR1
11) SIGSEGV     12) SIGUSR2     13) SIGPIPE     14) SIGALRM     15) SIGTERM
16) SIGSTKFLT   17) SIGCHLD     18) SIGCONT     19) SIGSTOP     20) SIGTSTP
21) SIGTTIN     22) SIGTTOU     23) SIGURG      24) SIGXCPU     25) SIGXFSZ
26) SIGVTALRM   27) SIGPROF     28) SIGWINCH    29) SIGIO       30) SIGPWR
31) SIGSYS      34) SIGRTMIN    35) SIGRTMIN+1  36) SIGRTMIN+2  37) SIGRTMIN+3
38) SIGRTMIN+4  39) SIGRTMIN+5  40) SIGRTMIN+6  41) SIGRTMIN+7  42) SIGRTMIN+8
43) SIGRTMIN+9  44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9  56) SIGRTMAX-8  57) SIGRTMAX-7
58) SIGRTMAX-6  59) SIGRTMAX-5  60) SIGRTMAX-4  61) SIGRTMAX-3  62) SIGRTMAX-2
63) SIGRTMAX-1  64) SIGRTMAX

设置退出码

设置进程的退出码主要有两种方式,第一种是调用 exit_exit 接口,第二种是在 main 函数中使用 return 语句

exit / _exit

cpp 复制代码
void exit(int status);

作用

终止调用自己的进程,将进程的退出码设置为 status

参数

status:用户给定的退出码

使用例

cpp 复制代码
#include <stdlib.h>
int main()
{
    exit(10);
    return 0;
}

运行并查看退出码

cpp 复制代码
10

cpp 复制代码
void _exit(int status);

作用

终止调用自己的进程,将进程的退出码设置为 status

参数

status:用户给定的退出码

使用例

cpp 复制代码
#include <unistd.h>
int main()
{
    _exit(10);
    return 0;
}

运行并查看退出码

cpp 复制代码
20

exit 和 _exit 的区别

exit 是由 C 语言提供的函数,内部封装了 _exit,在执行后会设置进程的退出码并刷新用户级缓冲区,将用户级缓冲区中的数据进行输出,最后终止进程。_exit 是系统调用,在执行后只会设置进程的退出码,将进程终止,不会刷新用户级缓冲区

验证

cpp 复制代码
int main()
{
    printf("hello world");
    _exit(10);
}

int main()
{
    printf("hello world");
    exit(20);
}

分别将两份代码运行,会发现上面的代码没有输出 hello world,下面的代码输出了 hello world,这是因为 printf 执行时会把 hello world 放到用户级缓冲区中,exit 会刷新缓冲区,_exit 不会刷新

return

main 函数中使用 return n 进行返回的时候,n 会被设置为进程的退出码

验证

cpp 复制代码
int main()
{
    return 10;
}

运行并查看退出码

cpp 复制代码
10

实际上在 return 返回时就是调用了 exit 函数,传入的参数就是 n

进程等待

进程等待的现象

根据前面的信息,子进程在运行完毕后,只会停止运行,不会立即消失,此时子进程在等待父进程回收自己的退出信息,进入了僵尸状态,产生了进程等待,当父进程回收了子进程的退出信息后,子进程就会消失

为了防止子进程进入僵尸状态,父进程就需要用 waitwaitpid 接口来等待子进程,当子进程运行完毕的时候,第一时间将它回收,在父进程调用 waitwaitpid 等待子进程的时候,也属于进程等待

进程等待的接口

进程等待的接口包括 waitwaitpid 两个,在僵尸进程处已经介绍过,在这里只说明一些其他的问题

wait 和 waitpid 中的 status 参数

waitwaitpid 接口都有一个输出型参数 status,它是一个有 16 个比特位的数,前 8 位用作进程的退出码,后 7 位用作信号的编号,从右向左第 8 位作为 core dump 标志,暂时不重要

如果进程正常运行完毕,没有被信号杀死,那么后 8 位就是全 0,前 8 位会被设置成指定的退出码,通过 (status >> 8) & 0xff 的方式可以得到这个退出码,这个计算方式也是宏WEXITSTATUS(status) 的实现方式,给这个宏传入 status 也能获得进程的退出码

如果进程被信号杀死,那么前 8 位就是全 0,后 7 位会被设置为信号的编号,通过 status & 0x7F 的方式可以得到信号的编号,这个计算方式也是宏 WIFEXITED(status) 的实现方式,WIFEXITED 用于查看后 7 位是否为全 0 判断进程是否正常退出

子进程正常退出时

cpp 复制代码
#include <sys/wait.h>
#include <unistd.h>
#include <cstdio>

int main()
{
    pid_t pid = fork();

    // 子进程
    if (pid == 0)
    {
        sleep(2);
        exit(1);
    }

    int status = 0;
    waitpid(pid, &status, 0);
    printf("子进程的退出码:%d, 信号编号:%d\n", (status >> 8) & 0xff, status & 0x7f);
}

运行结果

cpp 复制代码
子进程的退出码:1, 信号编号:0

子进程被信号杀死时

cpp 复制代码
int main()
{
    pid_t pid = fork();

    // 子进程
    if (pid == 0)
    {
        sleep(2);
        int x = 1 / 0;
    }

    int status = 0;
    waitpid(pid, &status, 0);
    printf("子进程的退出码:%d, 信号编号:%d\n", (status >> 8) & 0xff, status & 0x7f);
}

运行结果

cpp 复制代码
子进程的退出码:0, 信号编号:8

阻塞等待和非阻塞等待

waitpid 的第三个参数 option0 时,父进程在调用 waitpid 时,会被阻塞在调用处,直到等待的子进程运行完毕,这叫做阻塞等待

验证

cpp 复制代码
#include <sys/wait.h>
#include <unistd.h>
#include <cstdio>

int main()
{
    pid_t pid = fork();

    // 子进程
    if (pid == 0)
    {
        sleep(2);
        exit(0);
    }

    int status = 0;
    printf("阻塞等待\n");
    waitpid(pid, &status, 0);
    printf("子进程退出\n");
}

结果

cpp 复制代码
阻塞等待
子进程退出

可以看到,只输出了一句子进程退出,说明父进程在子进程退出前,被阻塞在了 waitpid 调用处


waitpid 的第三个参数取 WNOHANG 时,如果子进程未运行完毕,父进程调用 waitpid 时,不会在 waitpid 的调用处被阻塞,而是去继续执行后面的代码,这叫做非阻塞等待

验证

cpp 复制代码
int main()
{
    pid_t pid = fork();

    // 子进程
    if (pid == 0)
    {
        sleep(5);
        exit(0);
    }

    int status = 0;
    while (true)
    {
        int ret = waitpid(pid, &status, WNOHANG);
        sleep(1);
        if (ret == 0)
        {
            printf("子进程未退出\n");
        }
        else
        {
            printf("子进程退出\n");
            break;
        }
    }
}

结果

cpp 复制代码
子进程未退出
子进程未退出
子进程未退出
子进程未退出
子进程未退出
子进程退出

进程的程序替换

程序替换的概念

进程的程序替换指将当前进程在内存中的数据段和代码段进行替换,当程序替换成功时,进程就会去执行新的代码,不会执行原先的代码。这个过程中,不会发生进程的切换

父进程创建出子进程,子进程会和父进程共用一样的代码和数据,当子进程发生程序替换的时候,需要修改代码段和数据段,也会发生写时拷贝,将代码段和数据段拷贝到内存新的空间中,再进行替换

程序替换的接口

程序替换的接口一般都是 exec 开头的,后面接的字母代表了它的使用方式:

如果带有 l,说明这个函数有可变参数,需要传入多个参数,这些参数以 NULL 结尾

如果带有 v,说明这个函数接收一个指针数组,指针数组内是多个指定的参数,这些参数也以 NULL 结尾

如果带有 p,说明这个函数传路径时只需要给程序名,它会自动去环境变量 PATH 所知名的目录中查找程序

如果带有 e

这些函数在失败时都返回 -1,在成功时都不会有返回值,因为成功时就发生了程序替换,原来的代码就不再向下执行了,返回值无意义

execl

cpp 复制代码
`int execl(const char* path, const char* arg, ...);`

参数

path:要执行的程序所在的路径

arg:可变参数列表,传入的参数会被作为命令行参数,根据要执行的程序传参,比如要执行命令 ls -l,那么就传三个参数 ls-aNULL,其中 NULL 表示传参结束

使用例

cpp 复制代码
#include <unistd.h>
#include <stdio.h>
#include <sys/wait.h>

int main()
{
    int pid = fork();
    if (pid == 0)
    {
        printf("子进程进行程序替换\n");
        sleep(1);
        execl("/usr/bin/ls", "ls", "-l", NULL);
        printf("测试程序替换\n");
    }

    int ret = waitpid(pid, NULL, 0);
    if (ret < 0)
    {
        printf("waitpid产生错误\n");
    }

    printf("子进程退出成功\n");

    return 0;
}

运行结果

cpp 复制代码
子进程进行程序替换
total 28
-rw-rw-r-- 1 ubuntu ubuntu    91 Mar 20 17:27 makefile
-rwxrwxr-x 1 ubuntu ubuntu 16128 Mar 20 17:34 test
-rw-rw-r-- 1 ubuntu ubuntu   467 Mar 20 17:34 test.cc
-rw-rw-r-- 1 ubuntu ubuntu  2176 Mar 20 17:34 test.o
子进程退出成功

execlp

cpp 复制代码
int execlp(const char* file, const char* arg, ...);

参数

file:要执行的程序的名称,执行时会自动去环境变量 PATH 指定的路径中查找程序

arg:可变参数列表,与 execl 相同的用法

使用例

父进程创建子进程,子进程进行程序替换并执行 ls -a 命令

cpp 复制代码
#include <unistd.h>
#include <stdio.h>
#include <sys/wait.h>

int main()
{
    int pid = fork();
    if (pid == 0)
    {
        printf("子进程进行程序替换\n");
        sleep(1);
        execlp("ls", "ls", "-l", NULL);
        printf("测试程序替换\n");
    }

    int ret = waitpid(pid, NULL, 0);
    if (ret < 0)
    {
        printf("waitpid产生错误\n");
    }

    printf("子进程退出成功\n");

    return 0;
}

运行结果

cpp 复制代码
子进程进行程序替换
total 28
-rw-rw-r-- 1 ubuntu ubuntu    91 Mar 20 17:27 makefile
-rwxrwxr-x 1 ubuntu ubuntu 16128 Mar 20 19:00 test
-rw-rw-r-- 1 ubuntu ubuntu   899 Mar 20 19:00 test.cc
-rw-rw-r-- 1 ubuntu ubuntu  2160 Mar 20 19:00 test.o
子进程退出成功

execv

cpp 复制代码
int execv(const char* path, char* const argv[]);

参数

path:要执行的程序所在的路径

argv:命令行参数表,这是一个指针数组,相当于把命令行参数都放在了一个指针数组内

使用例

父进程创建子进程,子进程进行程序替换并执行 ls -a 命令

cpp 复制代码
#include <unistd.h>
#include <stdio.h>
#include <sys/wait.h>

int main()
{
    int pid = fork();

    char *argv[3] = {(char *)"ls", (char *)"-a", NULL};

    if (pid == 0)
    {
        printf("子进程进行程序替换\n");
        sleep(1);
        execv("/usr/bin/ls", argv);
        printf("测试程序替换\n");
    }

    int ret = waitpid(pid, NULL, 0);
    if (ret < 0)
    {
        printf("waitpid产生错误\n");
    }

    printf("子进程退出成功\n");

    return 0;
}

运行结果

cpp 复制代码
子进程进行程序替换
.  ..  makefile  test  test.cc  test.o
子进程退出成功

execvp

cpp 复制代码
int execvp(const char* file, char* const argv[]);

参数

file:程序的名称,调用时会去环境变量 PATH 给定的路径中查找

argv:命令行参数列表

使用例

父进程创建子进程,子进程进行程序替换并执行 ls -a 命令

cpp 复制代码
#include <unistd.h>
#include <stdio.h>
#include <sys/wait.h>

int main()
{
    int pid = fork();

    char *argv[3] = {(char *)"ls", (char *)"-a", NULL};

    if (pid == 0)
    {
        printf("子进程进行程序替换\n");
        sleep(1);
        execvp("ls", argv);
        printf("测试程序替换\n");
    }

    int ret = waitpid(pid, NULL, 0);
    if (ret < 0)
    {
        printf("waitpid产生错误\n");
    }

    printf("子进程退出成功\n");

    return 0;
}

运行结果

cpp 复制代码
子进程进行程序替换
.  ..  makefile  test  test.cc  test.o
子进程退出成功

execvpe

cpp 复制代码
int execvpe(const char* file, char* const argv[], char* const envp[]);

参数

file:要执行的程序的名称

argv:命令行参数列表

envp:环境变量表

使用例

父进程创建子进程,子进程进行程序替换,执行别的程序并输出它的命令行参数和环境变量

cpp 复制代码
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
using namespace std;

int main()
{
    pid_t pid = fork();
    char *argv[3] = {(char *)"-a", (char *)"-l", NULL};
    char *env[3] = {(char *const)"x=1", (char *const)"y=2", NULL};
	
	// 将新的环境变量添加到环境变量表中
    for (int i = 0; env[i]; ++i)
    {
        putenv(env[i]);
    }
    
    // 子进程
    if (pid == 0)
    {
        printf("执行程序替换\n");
        execvpe("./process", argv, environ);
        printf("程序替换失败\n");
        exit(1);
    }

    int ret = waitpid(pid, nullptr, 0);
    if (ret < 0)
    {
        printf("waitpid error");
        exit(2);
    }

    printf("子进程退出成功\n");

    return 0;
}

// process.cc:
#include <stdio.h>

int main(int argc, char *argv[], char *env[])
{
    printf("命令行参数:\n");
    for (int i = 0; i < argc; ++i)
    {
        printf("argv[%d] = %s\n", i, argv[i]);
    }

    printf("环境变量:\n");
    for (int i = 0; env[i]; ++i)
    {
        printf("env[%d] = %s\n", i, env[i]);
    }
    
    return 0;
}

运行结果

cpp 复制代码
执行程序替换
命令行参数:
argv[0] = -a
argv[1] = -l
环境变量:
env[0] = SHELL=/bin/bash
env[1] = COLORTERM=truecolor
env[2] = TERM_PROGRAM_VERSION=1.109.5
env[3] = TST_HACK_BASH_SESSION_ID=2707072416459380
env[4] = PWD=/home/ubuntu/linux-lesson/lesson-review/lesson-5
env[5] = LOGNAME=ubuntu
env[6] = XDG_SESSION_TYPE=tty
env[7] = VSCODE_GIT_ASKPASS_NODE=/home/ubuntu/.vscode-server/cli/servers/Stable-072586267e68ece9a47aa43f8c108e0dcbf44622/server/node
env[8] = HOME=/home/ubuntu
env[9] = LANG=en_US.UTF-8
env[10] = LS_COLORS=rs=0:di=01;34:ln=01;36:mh=00:pi=40;33:so=01;35:do=01;35:bd=40;33;01:cd=40;33;01:or=40;31;01:mi=00:su=37;41:sg=30;43:ca=00:tw=30;42:ow=34;42:st=37;44:ex=01;32:*.tar=01;31:*.tgz=01;31:*.arc=01;31:*.arj=01;31:*.taz=01;31:*.lha=01;31:*.lz4=01;31:*.lzh=01;31:*.lzma=01;31:*.tlz=01;31:*.txz=01;31:*.tzo=01;31:*.t7z=01;31:*.zip=01;31:*.z=01;31:*.dz=01;31:*.gz=01;31:*.lrz=01;31:*.lz=01;31:*.lzo=01;31:*.xz=01;31:*.zst=01;31:*.tzst=01;31:*.bz2=01;31:*.bz=01;31:*.tbz=01;31:*.tbz2=01;31:*.tz=01;31:*.deb=01;31:*.rpm=01;31:*.jar=01;31:*.war=01;31:*.ear=01;31:*.sar=01;31:*.rar=01;31:*.alz=01;31:*.ace=01;31:*.zoo=01;31:*.cpio=01;31:*.7z=01;31:*.rz=01;31:*.cab=01;31:*.wim=01;31:*.swm=01;31:*.dwm=01;31:*.esd=01;31:*.avif=01;35:*.jpg=01;35:*.jpeg=01;35:*.mjpg=01;35:*.mjpeg=01;35:*.gif=01;35:*.bmp=01;35:*.pbm=01;35:*.pgm=01;35:*.ppm=01;35:*.tga=01;35:*.xbm=01;35:*.xpm=01;35:*.tif=01;35:*.tiff=01;35:*.png=01;35:*.svg=01;35:*.svgz=01;35:*.mng=01;35:*.pcx=01;35:*.mov=01;35:*.mpg=01;35:*.mpeg=01;35:*.m2v=01;35:*.mkv=01;35:*.webm=01;35:*.webp=01;35:*.ogm=01;35:*.mp4=01;35:*.m4v=01;35:*.mp4v=01;35:*.vob=01;35:*.qt=01;35:*.nuv=01;35:*.wmv=01;35:*.asf=01;35:*.rm=01;35:*.rmvb=01;35:*.flc=01;35:*.avi=01;35:*.fli=01;35:*.flv=01;35:*.gl=01;35:*.dl=01;35:*.xcf=01;35:*.xwd=01;35:*.yuv=01;35:*.cgm=01;35:*.emf=01;35:*.ogv=01;35:*.ogx=01;35:*.aac=00;36:*.au=00;36:*.flac=00;36:*.m4a=00;36:*.mid=00;36:*.midi=00;36:*.mka=00;36:*.mp3=00;36:*.mpc=00;36:*.ogg=00;36:*.ra=00;36:*.wav=00;36:*.oga=00;36:*.opus=00;36:*.spx=00;36:*.xspf=00;36:*~=00;90:*#=00;90:*.bak=00;90:*.crdownload=00;90:*.dpkg-dist=00;90:*.dpkg-new=00;90:*.dpkg-old=00;90:*.dpkg-tmp=00;90:*.old=00;90:*.orig=00;90:*.part=00;90:*.rej=00;90:*.rpmnew=00;90:*.rpmorig=00;90:*.rpmsave=00;90:*.swp=00;90:*.tmp=00;90:*.ucf-dist=00;90:*.ucf-new=00;90:*.ucf-old=00;90:
env[11] = SSL_CERT_DIR=/usr/lib/ssl/certs
env[12] = GIT_ASKPASS=/home/ubuntu/.vscode-server/cli/servers/Stable-072586267e68ece9a47aa43f8c108e0dcbf44622/server/extensions/git/dist/askpass.sh
env[13] = PROMPT_COMMAND=__vsc_prompt_cmd_original
env[14] = SSH_CONNECTION=113.214.198.203 57365 10.0.4.2 22
env[15] = VSCODE_GIT_ASKPASS_EXTRA_ARGS=
env[16] = VSCODE_PYTHON_AUTOACTIVATE_GUARD=1
env[17] = LESSCLOSE=/usr/bin/lesspipe %s %s
env[18] = XDG_SESSION_CLASS=user
env[19] = TERM=xterm-256color
env[20] = LESSOPEN=| /usr/bin/lesspipe %s
env[21] = USER=ubuntu
env[22] = VSCODE_GIT_IPC_HANDLE=/run/user/1000/vscode-git-477b474b57.sock
env[23] = GOPROXY=https://mirrors.tencent.com/go/
env[24] = SHLVL=1
env[25] = XDG_SESSION_ID=51346
env[26] = XDG_RUNTIME_DIR=/run/user/1000
env[27] = SSL_CERT_FILE=/usr/lib/ssl/cert.pem
env[28] = SSH_CLIENT=113.214.198.203 57365 22
env[29] = DEBUGINFOD_URLS=https://debuginfod.ubuntu.com 
env[30] = VSCODE_GIT_ASKPASS_MAIN=/home/ubuntu/.vscode-server/cli/servers/Stable-072586267e68ece9a47aa43f8c108e0dcbf44622/server/extensions/git/dist/askpass-main.js
env[31] = XDG_DATA_DIRS=/usr/local/share:/usr/share:/var/lib/snapd/desktop
env[32] = BROWSER=/home/ubuntu/.vscode-server/cli/servers/Stable-072586267e68ece9a47aa43f8c108e0dcbf44622/server/bin/helpers/browser.sh
env[33] = PATH=/home/ubuntu/.vscode-server/cli/servers/Stable-072586267e68ece9a47aa43f8c108e0dcbf44622/server/bin/remote-cli:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin
env[34] = DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus
env[35] = TERM_PROGRAM=vscode
env[36] = VSCODE_IPC_HOOK_CLI=/run/user/1000/vscode-ipc-06d342a3-e90b-4b7d-a9e8-51c443b0c470.sock
env[37] = _=./test
env[38] = OLDPWD=/home/ubuntu/linux-lesson/lesson-review
env[39] = x=1
env[40] = y=2
子进程退出成功

execle

cpp 复制代码
int execle(const char* path, const char* arg, ..., char* const envp[]);

参数

path:要执行的程序的路径

arg:可变参数列表,传入的参数会被作为命令行参数

envp:环境变量表

使用例

父进程创建子进程,子进程执行新的程序,输出它的命令行参数和环境变量

cpp 复制代码
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
using namespace std;

int main()
{
    pid_t pid = fork();
    char *env[3] = {(char *)"x=1", (char *)"y=2", NULL};

    if (pid == 0)
    {
        printf("进行程序替换\n");
        execle("./process", "-a", "-l", NULL, env);
        printf("程序替换失败\n");
        exit(1);
    }

    int ret = waitpid(pid, nullptr, 0);
    if (ret < 0)
    {
        printf("waitpid error\n");
        exit(2);
    }

    printf("子进程退出\n");
    return 0;
}

// process.cc:
#include <stdio.h>

int main(int argc, char *argv[], char *env[])
{
    printf("命令行参数:\n");
    for (int i = 0; i < argc; ++i)
    {
        printf("argv[%d] = %s\n", i, argv[i]);
    }

    printf("环境变量:\n");
    for (int i = 0; env[i]; ++i)
    {
        printf("env[%d] = %s\n", i, env[i]);
    }
    
    return 0;
}

运行结果

cpp 复制代码
进行程序替换
命令行参数:
argv[0] = -a
argv[1] = -l
环境变量:
env[0] = x=1
env[1] = y=2
子进程退出
相关推荐
Yupureki2 小时前
《Linux系统编程》16.进程间通信-共享内存
linux·运维·服务器·c语言·数据结构·c++
小小工匠2 小时前
Linux - ARP Cache:从 `ip neigh` 到交换机转发,一次讲透主机路由表、ARP 缓存与 MAC 表
linux·tcp/ip·缓存
ayaya_mana2 小时前
NPS 内网穿透,二次开源版新增多种连接协议(含 P2P 配置)
linux·运维·服务器·网络协议·内网穿透·p2p·nps
枫桥骤雨2 小时前
Ubuntu配置XRDP远程桌面
linux·运维·ubuntu·xrdp
2401_877274242 小时前
System V 共享内存:Linux 最高性能 IPC 的设计与实现
linux·服务器·c语言
mifengxing2 小时前
操作系统(三)
操作系统·多线程·os·进程信息传递
hljqfl2 小时前
银河麒麟桌面操作系统更改ROOT密码
linux·运维·服务器
哈__2 小时前
VERT:本地文件转换自由,随时随地轻松实现
linux
今儿敲了吗2 小时前
Linux学习笔记第二章——虚拟机基础操作
linux·笔记·学习