Skip to main content

Command Palette

Search for a command to run...

《Linux是怎样工作的》进程管理

Published
6 min readView as Markdown

不先讲虚拟内存,只是单纯的讲述进程的创建和删除

创建线程

在 Linux 中,创建进程有如下两个目的。

  • 将同一个程序分成多个进程进行处理(例如,使用 Web 服务器接收多个请求)

  • 创建另一个程序(例如,从 bash 启动一个新的程序)

Linux 提供了 fork() 函数与 execve() 函数(其底层分别请求名为 clone() 与 execve() 的系统调用)。

fork() 函数

将一个程序分成多个进程进行处理,使用 fork() 函数即可,在调用 fork() 函数的时候就会基于发起调用的进程,创建一个新的线程。发送请求的进程被称为父线程,新创建的进程子进程

创建新进程的流程如下所示: 1)为子进程申请内存空间,并且复制进程的内存到子进程的内存空间 2)父进程与子进程分裂成两个进程,以执行不同的代码。这个依赖于 fork() 函数的返回不同值给父进程与子进程

image.png

下面的 fork() 函数 1)创建一个新进程 2)父进程输出与子进程的 ID,子进程只输出自身进程的 ID

fork.c

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <err.h>

static void child()
{
    printf("I'm child! my pid is %d.\n", getpid());
    exit(EXIT_SUCCESS);
}

static void parent(pid_t pid_c)
{
    printf("I'm parent! my pid is %d and the pid of my child is %d.\n", getpid(), pid_c);
    exit(EXIT_SUCCESS);
}

int main(void)
{
    pid_t ret;
    ret = fork();
    if(ret == -1)
        err(EXIT_FAILURE, "fork() failed");
    if(ret == 0) {
        //fork() 会返回0给子进程,因此这里调用child()
        child();
    } else {
        //fork() 会返回新创建的子进程的进程ID(大于1)给父进程,因此这里调用 parent()
        parent(ret);
    }
    // 在正常运行时,不可能运行到这里
    err(EXIT_FAILURE, "shouldn't reach here");
}

编译并且运行之后,可以看到下面的结果

# 编译
cc -o fork fork.c

# 运行
./fork

image.png

父进程进程 ID 是 15700 创建了一个子进程 ID 15701 ,在调用 fork() 函数之后,两个进程执行的处理也不同了

execve() 函数

我们打算启动另外一个程序,需要调用 execve() 函数 1)读取可执行文件,并且读取创建进程的内存映像所需的信息 2)用新进程的数据覆盖当前的进程内存 3)从最初的命令考试运行新的进程

注意:它是启动了另外一个程序替换了当前的进程

image.png

下面重点说一下整个流程

读取可执行文件,以及创建进程的内存映像所需的信息。可执行文件中不仅包含进程在运行过程中使用的代码与数据,还包含开始运行程序时所需的数据

包含以下内容

  • 包含代码的代码段在文件中的偏移量、大小、以及内存映像的起始地址

  • 包含代码以外的变量等数据的数据段在文件中的偏移量、大小、以及内存映像的的起始地址

  • 程序执行的第一条指令的内存地址(入口点)

image.png

不同于高级语言的源码,在 CPU 上执行机器语言指令,必须提供操作的内存地址,因此在代码段和数据段中必须包含内存映像的起始地址。

比如用虚拟的高级语言编写了下一段源代码

c = a + b

在机器语言层面,将上面那段代码转化成下面这样的内存地址操作的指令

load m100 r0          ← 将内存地址100(变量a)的值读取到名为r0的寄存器中
load m200 r1          ← 将内存地址200(变量b)的值读取到名为r1的寄存器中
add r0 r1 r2          ← 将r0与r1相加,并将结果储存到名为r2的寄存器中
store r2 m300         ← 将r2的值储存到内存地址300(变量c)

查看 /bin/sleep 的 ELF 信息

readelf -h /bin/sleep
[root@iZbp1di8q20j42i3akz7xiZ book]# readelf -h /bin/sleep
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              EXEC (Executable file)
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x4017b0  // 入口点
  Start of program headers:          64 (bytes into file)
  Start of section headers:          31208 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           56 (bytes)
  Number of program headers:         9
  Size of section headers:           64 (bytes)
  Number of section headers:         30
  Section header string table index: 29

查看具体的 /bin/sleep

readelf -S /bin/sleep

下面这个运行结果

[root@iZbp1di8q20j42i3akz7xiZ book]# readelf -S /bin/sleep
There are 30 section headers, starting at offset 0x79e8:

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .interp           PROGBITS         0000000000400238  00000238
       000000000000001c  0000000000000000   A       0     0     1
  [ 2] .note.ABI-tag     NOTE             0000000000400254  00000254
       0000000000000020  0000000000000000   A       0     0     4
  [ 3] .note.gnu.build-i NOTE             0000000000400274  00000274
       0000000000000024  0000000000000000   A       0     0     4
  [ 4] .gnu.hash         GNU_HASH         0000000000400298  00000298
       000000000000001c  0000000000000000   A       5     0     8
  [ 5] .dynsym           DYNSYM           00000000004002b8  000002b8
       00000000000005d0  0000000000000018   A       6     1     8
  [ 6] .dynstr           STRTAB           0000000000400888  00000888
       0000000000000289  0000000000000000   A       0     0     1
  [ 7] .gnu.version      VERSYM           0000000000400b12  00000b12
       000000000000007c  0000000000000002   A       5     0     2
  [ 8] .gnu.version_r    VERNEED          0000000000400b90  00000b90
       0000000000000060  0000000000000000   A       6     1     8
  [ 9] .rela.dyn         RELA             0000000000400bf0  00000bf0
       00000000000000a8  0000000000000018   A       5     0     8
  [10] .rela.plt         RELA             0000000000400c98  00000c98
       00000000000004f8  0000000000000018  AI       5    24     8
  [11] .init             PROGBITS         0000000000401190  00001190
       000000000000001a  0000000000000000  AX       0     0     4
  [12] .plt              PROGBITS         00000000004011b0  000011b0
       0000000000000360  0000000000000010  AX       0     0     16
  [13] .text             PROGBITS         0000000000401510  00001510
       00000000000030aa  0000000000000000  AX       0     0     16
  [14] .fini             PROGBITS         00000000004045bc  000045bc
       0000000000000009  0000000000000000  AX       0     0     4
  [15] .rodata           PROGBITS         00000000004045e0  000045e0
       0000000000000a4b  0000000000000000   A       0     0     32
  [16] .eh_frame_hdr     PROGBITS         000000000040502c  0000502c
       0000000000000264  0000000000000000   A       0     0     4
  [17] .eh_frame         PROGBITS         0000000000405290  00005290
       0000000000000bf4  0000000000000000   A       0     0     8
  [18] .init_array       INIT_ARRAY       0000000000606d28  00006d28
       0000000000000008  0000000000000008  WA       0     0     8
  [19] .fini_array       FINI_ARRAY       0000000000606d30  00006d30
       0000000000000008  0000000000000008  WA       0     0     8
  [20] .jcr              PROGBITS         0000000000606d38  00006d38
       0000000000000008  0000000000000000  WA       0     0     8
  [21] .data.rel.ro      PROGBITS         0000000000606d40  00006d40
       00000000000000a8  0000000000000000  WA       0     0     32
  [22] .dynamic          DYNAMIC          0000000000606de8  00006de8
       00000000000001d0  0000000000000010  WA       6     0     8
  [23] .got              PROGBITS         0000000000606fb8  00006fb8
       0000000000000038  0000000000000008  WA       0     0     8
  [24] .got.plt          PROGBITS         0000000000607000  00007000
       00000000000001c0  0000000000000008  WA       0     0     8
  [25] .data             PROGBITS         00000000006071c0  000071c0
       0000000000000080  0000000000000000  WA       0     0     32
  [26] .bss              NOBITS           0000000000607240  00007240
       0000000000000180  0000000000000000  WA       0     0     32
  [27] .gnu_debuglink    PROGBITS         0000000000000000  00007240
       0000000000000010  0000000000000000           0     0     4
  [28] .gnu_debugdata    PROGBITS         0000000000000000  00007250
       0000000000000678  0000000000000000           0     0     1
  [29] .shstrtab         STRTAB           0000000000000000  000078c8
       000000000000011a  0000000000000000           0     0     1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
  L (link order), O (extra OS processing required), G (group), T (TLS),
  C (compressed), x (unknown), o (OS specific), E (exclude),
  l (large), p (processor specific)

重点就是 .data、.text

通用格式如下: 第 1 行: [编号] 名称 类型 地址(Address) 偏移量(Offset) 第 2 行: 大小(Size) 表项大小 标志 链接 信息 对齐

[13] .text             PROGBITS         0000000000401510  00001510
       00000000000030aa  0000000000000000  AX       0     0     16

[25] .data             PROGBITS         00000000006071c0  000071c0
       0000000000000080  0000000000000000  WA       0     0     32
名称备注/来源说明
代码段在文件中的偏移量0x1510来自 .text 第1行 第5列 (Offset)
代码段的大小0x30aa来自 .text 第2行 第1列 (Size)
代码段的内存映像的起始地址0x401510来自 .text 第1行 第4列 (Address)
数据段的大小0x80来自 .data 第2行 第1列 (Size)
数据段的内存映像的起始地址0x6071c0来自 .data 第1行 第4列 (Address)
入口点0x4017b0来自 Entry point address

运行程序创建内存映像信息,可以从 /proc/PID/maps 1).text 指的是代码段 2).data 指的是数据段

image.png

可以查看“施工图”


[root@iZbp1di8q20j42i3akz7xiZ ~]# readelf -l /bin/sleep

Elf file type is EXEC (Executable file)
Entry point 0x4017b0
There are 9 program headers, starting at offset 64

Program Headers:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  PHDR           0x0000000000000040 0x0000000000400040 0x0000000000400040
                 0x00000000000001f8 0x00000000000001f8  R E    8
  INTERP         0x0000000000000238 0x0000000000400238 0x0000000000400238
                 0x000000000000001c 0x000000000000001c  R      1
      [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
  LOAD           0x0000000000000000 0x0000000000400000 0x0000000000400000  // 代码段
                 0x0000000000005e84 0x0000000000005e84  R E    200000
  LOAD           0x0000000000006d28 0x0000000000606d28 0x0000000000606d28  // 数据段
                 0x0000000000000518 0x0000000000000698  RW     200000
  DYNAMIC        0x0000000000006de8 0x0000000000606de8 0x0000000000606de8
                 0x00000000000001d0 0x00000000000001d0  RW     8
  NOTE           0x0000000000000254 0x0000000000400254 0x0000000000400254
                 0x0000000000000044 0x0000000000000044  R      4
  GNU_EH_FRAME   0x000000000000502c 0x000000000040502c 0x000000000040502c
                 0x0000000000000264 0x0000000000000264  R      4
  GNU_STACK      0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x0000000000000000 0x0000000000000000  RW     10
  GNU_RELRO      0x0000000000006d28 0x0000000000606d28 0x0000000000606d28
                 0x00000000000002d8 0x00000000000002d8  R      1

 Section to Segment mapping:
  Segment Sections...
   00
   01     .interp
   02     .interp .note.ABI-tag .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt .init .plt .text .fini .rodata .eh_frame_hdr .eh_frame
   03     .init_array .fini_array .jcr .data.rel.ro .dynamic .got .got.plt .data .bss
   04     .dynamic
   05     .note.ABI-tag .note.gnu.build-id
   06     .eh_frame_hdr
   07
   08     .init_array .fini_array .jcr .data.rel.ro .dynamic .got

fork and execve

一般新建一个别的线程时,通常采用 fork and exec 的方法,即由父线程调用 fork 创建子线程,再由子线程调用 exec()

下面的程序的流程是 1)创建一个新的进程 2)在创建 echo hello 程序后,父进程输出自身与子进程的 ID,并且结束运行,子进程输出自身进程 ID 然后结束运行

fork-and-exec.c

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <err.h>
#include <sys/wait.h>  // 必需:为了使用 waitpid

static void child()
{
    char *args[] = {"/bin/echo", "hello", NULL};
    printf("I'm child! my pid is %d.\n", getpid());
    fflush(stdout); // 刷新缓冲区,确保这句话在 execve 之前打印出来
    execve("/bin/echo", args, NULL);

    // 如果 execve 成功,下面的代码永远不会执行
    err(EXIT_FAILURE, "exec() failed");
}

static void parent(pid_t pid_c)
{
    int status;
    printf("I'm parent! my pid is %d and the pid of my child is %d.\n", getpid(), pid_c);

    // 【关键修改】等待子进程 (pid_c) 结束
    // 这样 shell 就不会因为父进程提前退出而过早抢占终端控制权
    if (waitpid(pid_c, &status, 0) == -1) {
        err(EXIT_FAILURE, "waitpid() failed");
    }

    // 等子进程彻底结束后,父进程才退出
    exit(EXIT_SUCCESS);
}

int main(void)
{
    pid_t ret;
    ret = fork();

    if (ret == -1)
        err(EXIT_FAILURE, "fork() failed");

    if (ret == 0) {
        // 子进程逻辑
        child();
    } else {
        // 父进程逻辑
        parent(ret);
    }

    // 正常流程下,parent() 会调用 exit(),所以不会走到这里
    err(EXIT_FAILURE, "shouldn't reach here");
}

编译运行的结果是:

$ cc -o fork-and-execve fork-and-execve.c
$ ./fork-and-execve
I'm parent! my pid is 3077 and the pid of my child is 3078.
I'm child! my pid is 3078.
$ hello

image.png

执行流程是

image.png

结束进程

调用 _exit() 函数(底层发起 exit_group() 系统调用)来结束进程。在进程结束后,所有分配给进程的内存就会被回收

image.png

不过,通常我们很少会直接调用 _exit() 函数,而是通过调用 C 标准库中的 exit() 函数来结束进程的运行。在这种情况下,C 标准库会在调用完自身的终止处理后调用 _exit() 函数。在从 main() 函数中恢复时也是同样的方式。

More from this blog

我用 SubAgent 做了一个 AI 自动修复闭环:流式修代码、自动构建、失败重试

最近用 AI 零代码项目生成前端页面有时候会出现问题,但是直接把原始报错信息返回给前端,再让用户手动点击按钮才能开始修复,用户体验不是很好,那么如何优化呢?我想到了 SubAgent 来进行修复! 如果把这类修复工作一直交给主 Agent 来做,会带来两个明显问题: 修复过程会不断污染主 Agent 的上下文。 错误日志、修复尝试和中间结果会迅速挤占上下文窗口,影响主 Agent 后续继续处理

Mar 30, 20264 min read

leikooo

28 posts