C++ 巩固基础之一

编译与链接原理

提示

强烈建议在阅读完本节的内容后,多花点时间深入读一遍《深入理解计算机系统 - 第三版》的第 7 章 "链接"。

编译命令

步骤命令
1. 预处理 gcc -E hello.c -o hello.i
2. 编译到汇编代码 gcc -S hello.i -o hello.s
3. 汇编到目标代码(机器语言)gcc -c hello.s -o hello.o
4. 链接,生成可执行文件 gcc hello.o -o hello
以上四个步骤,可以合成一个步骤,直接编译链接成可执行的目标文件 gcc hello.c -o hello

编译参数

参数作用编译示例示例说明
-o 指定输出可执行程序的名称,默认文件名为”a.out”gcc hello.c -o hello 编译单个源文件 hello.c,指定输出可执行程序的名称为 hello,支持同时编译多个源文件
-E 仅作预处理,不进行编译、汇编和链接 gcc -E hello.c -o hello.i 仅预处理源文件,指定生成中间文件 *.i,此阶段主要处理源文件中的 #ifdef、#include、#define 等预处理命令
-S 只编译到汇编语言,不进行汇编和链接,生成汇编代码 gcc -S hello.c -o hello.s 仅编译到汇编语言,指定生成汇编源文件 *.s
-c 只编译、汇编到目标代码,不进行链接,生成目标文件(机器语言)gcc -c hello.s -o hello.o 根据汇编源文件 *.s,指定生成目标文件 *.o,最后根据生成的目标文件,可执行 gcc hello.o -o hello 命令生成可执行程序
-l 指定程序链接哪个静态库或者动态库
-m 表示是数学库,也就是使用 math.h 头文件 gcc hello.c -o hello -lm 编译单个源文件 hello.c,指定输出可执行程序名称为 hello,并指定程序链接到数学库
-I dir 在头文件的搜索路径列表中添加 dir 目录
-L dir 在库文件的搜索路径列表中添加 dir 目录
-O、-O2、-O3 将优化状态打开,该选项不能与”-g” 选项联合使用
-g 在生成的可执行程序中包含标准调试信息
-Wall 在发生警告时取消编译操作,即将警告看作是错误
-pedantic 严格要求代码符合 ANSI/ISO C 标准,若不符合则给出编译警告信息
-w 禁止输出所有警告
-v 打印编译器内部编译各过程的命令行信息和编译器的版本号

编译流程

C++ 的编译和链接过程通常分为四个主要阶段:预处理、编译、汇编和链接。每个阶段都有特定的任务,最终生成可执行文件。这些阶段的流程如下:

  • 预处理:处理 #include#define 等指令,生成纯文本代码。
  • 编译:将代码转换成汇编代码。
  • 汇编:将汇编代码转换成机器代码,生成目标文件。
  • 链接:将目标文件和库文件组合,生成可执行文件。

第一步:预处理

预处理阶段由预处理器处理,主要任务是处理源代码中的以 # 开头的指令,包括 #include#define 等。预处理的主要任务包括:

  • 头文件包含:将 #include 引用的头文件内容插入代码中。
  • 宏替换:将 #define 宏展开成具体内容。
  • 条件编译:根据 #if#ifdef 等条件编译指令有选择地编译代码片段。

经过预处理后的代码形成一个纯文本文件,通常称为 “预处理文件”。

第二步:编译

在编译阶段,编译器将预处理后的代码转换成汇编代码。编译的主要任务包括:

  • 语法分析和语义分析:检查代码的语法和语义是否正确,如变量是否定义、数据类型是否匹配等。
  • 生成中间代码:将源码转换成一种与机器无关的中间表示,以便后续优化和生成目标代码。
  • 优化:编译器可能会优化代码以提高执行效率,比如消除冗余代码、优化循环等。

编译阶段输出的通常是一个 .s 文件,其中包含汇编代码。

第三步:汇编

汇编阶段由汇编器将汇编代码转换为机器代码。机器代码是与目标 CPU 架构相关的低级二进制指令。汇编器会生成一个目标文件,通常带有 .o.obj 后缀。值得一提的是,每个源文件都会经过汇编阶段,生成一个独立的目标文件。

第四步:链接

链接阶段由链接器负责,主要任务是将多个目标文件和库文件组合成一个可执行文件。链接的主要任务包括:

  • 符号解析:将所有函数和变量的引用与其定义关联起来。例如,如果一个文件引用了另一个文件的函数,链接器会将引用解析为实际地址。
  • 地址分配:链接器分配每个符号(函数、变量)在最终可执行文件中的内存地址。
  • 合并代码段和数据段:链接器会将所有目标文件的代码段、数据段等部分合并,形成最终的可执行文件。

链接完成后,生成最终的可执行文件,程序可以在操作系统上直接执行。

编译分析

0

C++ 编译示例代码

  • sum.cpp 的源码
1
2
3
4
5
int gdata = 10;

int sum(int a, int b) {
return a + b;
}
  • main.cpp 的源码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>

using namespace std;

extern int gdata;

int sum(int, int);

int data = 20;

int main() {
int a = gdata;
int b = data;
int result = sum(a, b);
cout << "result = " << result << endl;
return 0;
}
  • 编译代码生成可执行文件(默认是 a.out
1
g++ main.cpp sum.cpp

C++ 编译分析命令

  • 只编译、汇编到目标代码,不进行链接
1
2
g++ -c sum.cpp
g++ -c main.cpp
  • 查看目标文件的符号表,包括函数和全局变量的地址、大小等信息
1
2
objdump -t sum.o
objdump -t main.o
  • 显示目标文件中各个段的内容(即十六进制和 ASCII 表示的原始数据)
1
2
objdump -s sum.o
objdump -s main.o
  • 查看目标文件的文件头信息
1
2
readelf -h sum.o
readelf -h main.o
  • 查看目标文件中各段的详细信息,包括段的名称、大小、地址、类型和属性等
1
2
readelf -S sum.o
readelf -S main.o
  • 将目标文件中的汇编代码(通过反汇编得到)与源代码进行对比显示,以便进行调试和分析
1
2
g++ -c main.cpp -g    // 生成目标文件,-g 参数表示带上调试信息
objdump -S main.o
  • 链接所有目标文件,并查看指定文件的符号表信息
1
2
g++ -e main *.o     // 将所有的目标文件链接成一个完整的程序,默认会生成一个名为 a.out 的可执行文件
objdump -t a.out
  • 查看可执行文件(通常是 ELF 格式)的程序头信息
1
readelf -l a.out

C++ 的内存分配与释放

C++ 的内存分配

new 和 malloc 的概述

  • new 的概述

    • new 是 C++ 的关键字,专门用于动态内存分配。
    • 在分配内存的同时,new 会调用类对象的构造函数(如果是类对象)。
    • 如果分配失败,new 会抛出异常 std::bad_alloc
  • malloc 的概述

    • malloc 是 C 语言中的函数,用于分配一块连续的内存。
    • 它不会初始化分配的内存,也不会调用类对象的构造函数(如果分配的内存是用于类对象)。
    • 如果分配失败,malloc 返回 NULL,需要手动检查。

new 和 malloc 的区别

特性newmalloc
适用语言 C++C 和 C++
是否调用构造函数分配内存,会调用构造函数仅分配内存,不会调用构造函数
初始化自动初始化(如 new int[5]() -> 0)不初始化,分配的内存为未定义的垃圾值
返回值返回具体类型的指针,无需类型转换返回 void *,需要显式类型转换
释放方式使用 deletedelete[] 释放内存使用 free 释放内存
错误处理内存分配失败时抛出 std::bad_alloc 异常内存分配失败时返回 NULL,需手动检查
内存分配用途高级面向对象编程,推荐用于 C++ 动态内存分配兼容 C 语言,适用于简单的内存分配需求

C++ 的内存释放

delete 和 free 的概述

  • delete 的概述

    • delete 关键字用于释放通过 new 分配的内存。
    • 在释放内存之前,delete 会先调用类对象的析构函数,以确保资源(如文件句柄、网络连接等)正确释放。
    • 如果分配的是数组,必须使用 delete[],否则行为未定义。
  • free 的概述

    • free 是 C 标准库函数,用于释放通过 malloc/calloc/realloc 分配的内存。
    • 它只会释放内存,不会执行任何其他操作,例如对象的析构。
  • deletefree 不能混用

    • 通过 new 分配的内存必须用 delete 释放,不能用 free,否则会导致未定义行为。
    • 通过 malloc 分配的内存必须用 free 释放,不能用 delete,否则会导致未定义行为。
  • deletefree 的内存布局和管理差异

    • newdelete 是 C++ 的关键字,它们了解对象的类型,并能为复杂类型的构造和析构做出正确的处理。
    • mallocfree 是 C 的函数,它们只分配和释放内存,不了解对象的类型。

delete 和 free 的区别

特性deletefree
适用语言 C++C 和 C++
适用对象动态分配的对象(通过 new 分配)动态分配的内存块(通过 malloc/calloc 分配)
是否调用析构函数是,会调用类对象的析构函数否,只释放内存,不调用析构函数
分配与释放的匹配要求必须和 new 成对使用必须和 malloc/calloc 成对使用
底层机制 C++ 的运行时库负责,处理更高级的资源管理 C 的运行时库负责,直接释放内存
数组释放使用 delete[] 释放动态数组没有专门的数组释放功能

内存分配与释放的使用

new 和 delete 的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>

using namespace std;

int main() {
int *p = new int(20); // 分配内存,并且会初始化内存为 20
cout << *p << endl;
delete p;

int *arr = new int[5](); // 数组初始化为 0
for (int i = 0; i < 5; ++i) {
cout << arr[i] << " ";
}
delete[] arr; // 释放数组内存
}

程序执行后的输出结果:

1
2
p = 20
0 0 0 0 0

malloc 和 free 的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>

using namespace std;

int main() {
int *p = (int *) malloc(sizeof(int));
if (p == nullptr) {
return -1;
}

*p = 20;
cout << "p = " << *p << endl;

free(p);
return 0;
}

程序执行后的输出结果:

1
p = 20

new 的几种常见用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>

using namespace std;

int main() {
// 第一种用法
int *p1 = new int(20);
delete p1;

// 第二种用法
int *p2 = new (nothrow) int; // 即使内存分配失败,也不抛出异常
delete p2;

// 第三种用法
const int *p3 = new const int(30);
delete p3;

// 第四种用法
int data = 0;
int *p4 = new (&data) int(50);
cout << "data = " << data << endl; // 输出 50
}

程序执行后的输出结果:

1
data = 50

C++ 的 const、指针、引用

指针与引用

  • 引用的本质
    • 引用本质上是一种更安全的指针。
  • 指针与引用的区别
    • 引用必须初始化,指针可以不初始化。
    • 引用只有一级引用,没有多级引用;指针可以有一级指针,也可以有多级指针。
    • 定义一个引用变量和定义一个指针变量,其底层的汇编指令是一模一样的。

C 案例代码一

  • C 语言中常量的使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>

void _main() {
const int a = 20;

// 这行代码 C 编译器编译不通过,因为 a 虽然是常量,但在 C 语言中,它并不是编译期常量(即不是编译器在编译时可以确定的常量),而是运行时的常量
// 标准 C 语言(C89/C90 和 C99)要求数组大小在编译时确定,所以不能使用 const int 变量来定义数组大小
// int array[a] = {};

// C 语言中的 const 变量属于伪常量,有自己的内存空间,可以通过操作指针的方式来修改 const 变量的值
int *p = &a;
*p = 30;
printf("%d %d %d\n", a, *p, *(&a));
}

程序执行后的输出结果:

1
30 30 30

C++ 案例代码一

  • C++ 中常量的使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <iostream>

using namespace std;

void test01() {
// 用字面量常量初始化常量
const int a = 3;

// 在 C++ 中,常量可以作为数组大小,因为 C++ 中 const 常量是在编译时确定的
int array[a] = {};

// 在 C++ 中,不可以通过操作指针的方式来修改 const 变量的值
// 当使用 & 操作符取 const 常量的地址时,编译器会临时开辟一块内存空间
int *p = (int *) &a;
*p = 20;
printf("%d %d %d\n", a, *p, *(&a));
}

void test02() {
int a = 20;
// 用变量来初始化常量(通常叫常变量)
const int b = a;

// 此时不能再用来作为数组大小
// int array[b] = {};

// 此时可以通过操作指针的方式来修改 const 变量的值
int *p = (int *) &a;
*p = 20;
printf("%d %d %d\n", a, *p, *(&a));
}

int main() {
test01();
test02();
return 0;
}

程序执行后的输出结果:

1
2
3 20 3
20 20 20

C++ 案例代码二

  • C++ 中指针与引用的使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#include <iostream>

using namespace std;

struct Person {
int age;
};

// 二级指针作函数参数,参数 p 是一个指向 Person 指针的指针
void allocatMemory1(Person **p) {
// 动态分配内存给 *p,使其指向一个新的 Person 对象
*p = (Person *) malloc(sizeof(Person));
(*p)->age = 18;
}

void test01() {
// 定义一个 Person 类型的指针 p,初始化为 NULL(空指针)
Person *p = NULL;
// 传入 &p(Person* 的地址)
allocatMemory1(&p);
cout << "age = " << p->age << endl;
}

// 指针引用作函数参数,参数 p 是 Person* 类型的引用
void allocatMemory2(Person *&p) {
// 动态分配内存给 p,使其指向一个新的 Person 对象
p = (Person *) malloc(sizeof(Person));
p->age = 20;
}

void test02() {
// 定义一个 Person 类型的指针 p,初始化为 NULL(空指针)
Person *p = NULL;
// 直接传入 p
allocatMemory2(p);
cout << "age = " << p->age << endl;
}

int main() {
// 常量引用
const int &num = 30;

// 在内存的0x0018ff44位置写一个4字节的10
// int *p = (int *) 0x0018ff44;

// 二级指针
int a = 10;
int *p = &a;
int **q = &p;
cout << **q << endl;

// 二级指针
test01();

// 指针引用
test02();

return 0;
}

程序执行后的输出结果:

1
2
3
10
age = 18
age = 20

C++ 案例代码三

  • C++ 中指针与引用、左值引用与右值引用的使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
#include <iostream>

using namespace std;

/**
* 指针与引用
*/
void test01() {
int a = 10;
int *p = &a;
int &b = a; // 引用,其底层是基于指针实现的
// int &c = 20; // 错误写法

*p = 20;
cout << a << " " << *p << " " << b << endl; // 20 20 20

b = 30;
cout << a << " " << *p << " " << b << endl; // 30 30 30
}

void swap1(int *a, int *b) {
int tmp = *a;
*a = *b;
*b = tmp;
}

void swap2(int &a, int &b) {
int tmp = a;
a = b;
b = tmp;
}

/**
* 指针与引用
*/
void test02() {
int a = 10;
int b = 20;
// swap1(&a, &b);
swap2(a, b);
cout << "a = " << a << ", b = " << b << endl; // a = 20, b = 10
}

/**
* 引用数组变量
*/
void test03() {
int array[5] = {};
int *p = array;
cout << sizeof(array) << endl; // 20
cout << sizeof(p) << endl; // 4

// 定义一个引用变量来引用数组变量
int (&q)[5] = array;
cout << sizeof(q) << endl; // 20
}

/**
* 左值引用与右值引用
*/
void test04() {
int a = 10; // 左值,它有内存,有名称,值可以修改
int &b = a; // 左值引用
cout << "b = " << b << endl; // b = 10

// int &c = 20; // 错误写法,左值引用只能引用左值,20 是右值(没有内存和名称),不能引用 20
int &&c = 20; // 正确写法,C++ 提供了右值引用
// int &&c = a; // 错误写法,右值引用只能引用右值,a 是左值,不能引用 a
cout << "c = " << c << endl; // c = 20
}

int main() {
test01();
test02();
test03();
test04();
return 0;
}

程序执行后的输出结果:

1
2
3
4
5
6
7
8
20 20 20
30 30 30
a = 20, b = 10
20
4
20
b = 10
c = 20

C++ 案例代码四

  • const 与一二级指针的使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
#include <iostream>

using namespace std;

/**
* const与一级指针的结合
*/
void test01() {
const int a = 10;
// int *p = &a; // 错误写法,不能将常量的地址泄漏给一个普通的指针或者普通的引用变量
}

/**
* const与一级指针的结合
* <p> C++的语言规范:const 修饰的是离它最近的类型
* <p> const 在 * 的左边则定值,const 在 * 的右边则定向,即左定值右定向
*/
void test02() {
int a = 10;
int b = 20;

// 第一种写法(左定值)
const int *p = &a;
p = &b; // 正确写法,可以任意指向不同的int类型的内存
// *p = 30; // 错误写法,不能通过指针间接修改指向的内存的值
cout << "p = " << *p << endl; // p = 20

// 第一种写法的变体(左定值)
int const *p2 = &a;
p2 = &b; // 正确写法,可以任意指向不同的int类型的内存
// *p2 = 30; // 错误写法,不能通过指针间接修改指向的内存的值
cout << "p2 = " << *p2 << endl; // p2 = 20

// 第二种写法(右定向)
int *const p3 = &a;
// p3 = &b; // 错误写法,不可以任意指向不同的int类型的内存
*p3 = 30; // 正确写法,可以通过指针间接修改指向的内存的值
cout << "p3 = " << *p3 << ", a = " << a << endl; // p3 = 30, a = 30
}

/**
* const与二级指针的结合
*/
void test03() {
int a = 10;
int *p = &a;

// 第一种写法
// const int **q = &p; // 错误写法

// 第二种写法
int *const *q = &p; // 正确写法

// 第三种写法
// int **const q = &p; // 错误写法
}

int main() {
test01();
test02();
test03();
return 0;
}

程序执行后的输出结果:

1
2
3
p = 20
p2 = 20
p3 = 30, a = 30

C++ 形参带默认值的函数

  • 形参设置默认值的时候,必须从右向左设置。
  • 形参是否设置默认值,对函数的调用效率会产生影响(底层生成的汇编指令会有差别)。
  • 函数定义时可以给形参设置默认值,函数声明时也可以给形参设置默认值。
  • 形参设置默认值的时候,不管是在函数定义处,还是在函数声明处,形参的默认值设置只能出现一次。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>

using namespace std;

// 函数的形参带默认值
int sum(int a = 30, int b = 40) {
return a + b;
}

int main() {
int a = 10;
int b = 20;
int result = a + b;
cout << "result = " << result << endl;

int result2 = sum(a);
cout << "result2 = " << result2 << endl;

int result3 = sum();
cout << "result3 = " << result3 << endl;

return 0;
}

程序执行后的输出结果:

1
2
3
result = 30
result2 = 50
result3 = 70

C++ 内联函数的使用

  • 普通函数
    • 普通函数的调用会有开销,比如:参数压栈、函数栈帧的开辟、回退等操作。
  • 内联函数
    • 内联函数在编译过程中,就没有函数的调用开销,而是在函数的调用点直接用函数的代码展开(替换)处理。
    • inline 关键字只是建议编译器将指定的函数处理成内联函数,但并不是所有的 inline 关键字都会被编译器处理成内联函数,比如:递归函数。
    • 在 Debug 版本中,inline 关键字是不起作用的,inline 关键字只有在 Release 版本中才起作用。
    • 当编译器将 inline 关键字修饰的函数处理成内联函数后,就不会再生成该函数对应的符号表内容。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>

using namespace std;

// 定义内联函数
inline int sum(int a, int b) {
return a + b;
}

int main() {
int a = 10;
int b = 20;
int result = sum(a, b);
return 0;
}

C++ 函数重载的使用

一组函数,其中函数名相同,参数列表的个数、类型、顺序不同,那么这一组函数就称为 - 函数重载。

  • (1) 一组函数要称得上是函数重载,那么这组函数必须是处于同一个作用域当中的。
  • (2) 一组函数,函数名称相同,参数列表相同,仅仅返回值不同,这不叫函数重载。
  • (3) C++ 的多态有两种表现形式,包括静态多态(编译时期)和动态多态(运行时期),而函数重载属于静态多态的一种。
  • (4) const 关键字不能作为判断函数是否重载的条件,比如 void sub(int a);void sub(const int a) 之间不是重载关系。

为什么 C++ 支持函数重载,而 C 语言不支持函数重载?

  • C++ 代码产生函数符号的时候,由函数名 + 参数列表决定。
  • C 语言代码产生函数符号的时候,仅由函数名决定。

函数重载案例代码一

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <iostream>
#include <cstring>

using namespace std;

bool compare(int a, int b) {
cout << "compare_int_int" << endl;
return a > b;
}

bool compare(double a, double b) {
cout << "compare_double_double" << endl;
return a > b;
}

bool compare(const char *a, const char *b) {
cout << "compare_char*_char*" << endl;
return strcmp(a, b) > 0;
}

int main() {
compare(10, 20);
compare(3.4, 2.4);
compare("abc", "efd");
return 0;
}

函数代码重载案例二

C++ 调用在 C 语言中定义的函数时,默认会编译失败,这是因为 C++ 与 C 语言生成函数符号的规则是不同的,从而导致 C++ 在编译时找不到在 C 语言中定义的函数。解决方法是,在 C++ 代码中,使用 extern "C" { } 来包裹函数的声明。

  • C 语言的源代码
1
2
3
4
5
#include <stdio.h>

int sum(int a, int b) {
return a + b;
}
  • C++ 的源代码
1
2
3
4
5
6
7
8
9
10
11
// 标记大括号里的函数符号是按照 C 语言的规则来生成
extern "C" {
// 函数声明
int sum(int a, int b);
}

int main() {
int result = sum(1, 2);
cout << "result = " << result << endl;
return 0;
}

提示

如果是 C 语言需要调用在 C++ 中定义的函数,那么在 C++ 的代码中,也需要使用 extern "C" { } 包裹函数的定义。

值得一提的是,在多语言的项目开发中,往往会看到下面这样的 C++ 代码,其中的 __cplusplus 是 C++ 编译器内置的宏名。这样写的好处是,无论是在 C++ 还是 C 语言的代码中调用 sum() 函数,编译器都可以正常编译。

1
2
3
4
5
6
7
8
9
#ifdef __cplusplus
extern "C" {
#endif
int sum(int a, int b) {
return a + b;
}
#ifdef __cplusplus
}
#endif

进程之间的通信方式

在 C++ 中,进程间通信(IPC)有多种方式可供选择。以下是一些常见的 IPC 方法:

  • 管道 (Pipe)

    • 匿名管道:只能在父子进程之间通信。它在 Unix 和 Windows 上都支持。
    • 命名管道:允许不同的进程进行通信。它可以跨不同的无亲缘关系的进程进行通信。
  • 信号 (Signal)

    • 信号是一种异步通知机制,可以用于进程之间的简单通信。通常用于通知进程某个事件发生,例如终止、暂停等。信号适用于 Unix 系统。
  • 消息队列 (Message Queue)

    • 消息队列允许进程之间通过发送和接收消息进行通信。这种方式支持消息的优先级排序,能够在多种操作系统上实现。POSIX 和 System V 都提供了消息队列的实现。
  • 共享内存 (Shared Memory)

    • 共享内存是一种高效的 IPC 方式,允许多个进程共享同一块内存区域。由于数据直接存储在共享的内存空间中,读写速度较快,但需要使用同步机制来避免竞争条件。
  • 信号量 (Semaphore)

    • 信号量是一种用于多进程间同步的机制,通常和共享内存配合使用,以保证进程对共享资源的有序访问。System V 和 POSIX 都提供了信号量的实现。
  • 套接字 (Socket)

    • 套接字是一种强大的通信方式,支持同一台机器上的进程通信(本地套接字)以及不同机器之间的网络通信(网络套接字)。它具有跨平台的特性,广泛用于分布式系统。
  • 内存映射文件 (Memory-Mapped Files)

    • 内存映射文件可以将文件内容映射到进程的地址空间中,多个进程可以通过映射相同的文件来实现数据共享。这种方式在 Unix 和 Windows 系统上都支持。
  • 远程过程调用 (Remote Procedure Call)

    • RPC 允许进程在远程主机上执行函数调用。虽然并非严格意义上的 IPC 机制,但它能够在分布式系统中用于进程通信。gRPC 是一种流行的 RPC 框架。

总结

在选择 IPC 方式时,可以根据应用的需求(如速度、复杂性、跨平台性)来选择合适的方式。例如,管道和消息队列适合简单的通信需求,而共享内存适合大数据量的高效通信。

进程虚拟地址空间的内存布局

在 X86 32 位的 Linux 系统上,Linux 系统会给进程分配 2^32 大小(4GB)的一块空间,其中用户空间(User Space)占 3GB,内核空间(Kernel Space)占 1GB。值得一提的是,每一个进程的用户空间是私有的,但是内核空间是共享的。

  • .text:已编译程序的机器代码。
  • .rodata:只读数据,比如 printf 语句中的格式串和开关语句的跳转表。
  • .data:已初始化的全局和静态 C 变量。局部 C 变量在运行时被保存在栈中,既不出现在 .data 节中,也不出现在 .bss 节中。
  • .bss:未初始化的全局和静态 C 变量,以及所有被初始化为 0 的全局或静态变量。在目标文件中这个节不占据实际的空间,它仅仅是一个占位符。目标文件格式区分已初始化和未初始化变量是为了空间效率:在目标文件中,未初始化变量不需要占据任何实际的磁盘空间。程序运行时,在内存中分配这些变量,初始值为 0