《Linux是怎样工作的》进程管理
不先讲虚拟内存,只是单纯的讲述进程的创建和删除
创建线程
在 Linux 中,创建进程有如下两个目的。
将同一个程序分成多个进程进行处理(例如,使用 Web 服务器接收多个请求)
创建另一个程序(例如,从 bash 启动一个新的程序)
Linux 提供了 fork() 函数与 execve() 函数(其底层分别请求名为 clone() 与 execve() 的系统调用)。
fork() 函数
将一个程序分成多个进程进行处理,使用 fork() 函数即可,在调用 fork() 函数的时候就会基于发起调用的进程,创建一个新的线程。发送请求的进程被称为父线程,新创建的进程子进程
创建新进程的流程如下所示: 1)为子进程申请内存空间,并且复制进程的内存到子进程的内存空间 2)父进程与子进程分裂成两个进程,以执行不同的代码。这个依赖于 fork() 函数的返回不同值给父进程与子进程

下面的 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

父进程进程 ID 是 15700 创建了一个子进程 ID 15701 ,在调用 fork() 函数之后,两个进程执行的处理也不同了
execve() 函数
我们打算启动另外一个程序,需要调用 execve() 函数 1)读取可执行文件,并且读取创建进程的内存映像所需的信息 2)用新进程的数据覆盖当前的进程内存 3)从最初的命令考试运行新的进程
注意:它是启动了另外一个程序替换了当前的进程

下面重点说一下整个流程
读取可执行文件,以及创建进程的内存映像所需的信息。可执行文件中不仅包含进程在运行过程中使用的代码与数据,还包含开始运行程序时所需的数据
包含以下内容
包含代码的代码段在文件中的偏移量、大小、以及内存映像的起始地址
包含代码以外的变量等数据的数据段在文件中的偏移量、大小、以及内存映像的的起始地址
程序执行的第一条指令的内存地址(入口点)

不同于高级语言的源码,在 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 指的是数据段

可以查看“施工图”
[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

执行流程是

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

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