进程地址空间体系

进程地址空间体系图

image-20220507094939651

对于32位X86架构上运行的Linux而言, 其虚拟地址空间的寻址范围从0 ~ 4G,内核将这块空间划分为两个部分,将最高的1G字节0xC0000000 ~ 0xFFFFFFFF称为**“内核空间”, 顾名思义是提供给内核使用;而将较低3G字节0x00000000 ~ 0xBFFFFFFF称为“用户空间”**,即提供给各个运行的进程使用。

理论上,每个进程都是可以访问全部能寻址的4G虚拟内存空间的,但是系统为了防止内核空间被用户进程有意或无意的破坏,所以采用了分级保护措施: 将内核定为0级,将用户进程定为3级, 这样用户进程便无法直接访问内核的虚拟内存空间,仅能通过系统调用来进入内核态,从而来访问被限定的部分内核空间地址。同时,由于访问权限的机制,不同的进程间也都拥有独立的用户空间。这样非对称的访问机制使得Linux系统运行更加的安全稳定。

另外,用户进程是无法访问0x00000000 ~ 0x08048000这一段虚拟内存地址的,在这段地址上有诸多例如C库,动态加载器如ld.so和VDSO等的映射地址。 如果用户进程访问到该区间会返回段错误。

内核空间

每一个进程的用户空间是私有的,但是所有进程共享用一个内核空间。

所以,进程之间的通信方式中的匿名管道通信就应用于此。

ZONE_HIGHMEM

高端内存。在32位Linux系统,内核空间只有1G,在内核中映射高于1G的物理内存时,会使用到高端内存。64位不存在高端内存,因为64位系统的内核空间有512G。

ZONE_NORMAL

内核常用的部分、最重要的部分。

ZONE_DMA

直接内存访问。加快磁盘和内存交换数据。避免了经过CPU寄存器的过程,因此可以避免浪费CPU资源。

命令行参数和环境变量

main函数的原型就是一个例子。

1
int main(int argc, char ** argv, char ** environ)

argc记录了命令行参数的个数。

argv存的是命令行参数。

environ存的是环境变量。

我们简单地引入一个stdio.h头文件,就能使用C语言库函数了,这个头文件并没有在本地,这是因为环境变量指出了库文件存在哪些位置,程序编译链接时加载了环境变量。

这里涉及的命令行参数、环境变量都是存在于栈的高内存地址空间的。

为何要有栈?程序运行时,总要有一个主入口,即

进程中的每一个线程都有属于自己的栈

用户空间中地址最高的段叫做栈,他被用于存放函数参数和动态局部变量。调用一个方法或函数会将一个新的栈帧(stack frame)压入到栈中,这个栈帧会在函数返回时被清理掉。

在程序运行过程中,进程通过函数的调用和返回使得控制权在各个函数间转移,在新函数调用时,原函数的栈帧状态保持不变,并为新的函数开辟其所需的帧空间;当调用函数返回时,该函数的运行空间随着栈帧被弹出而清空,这次进程回到原函数的栈帧环境中继续执行。

通过不断向栈中压入数据,超出其容量就会耗尽栈所对应的内存区域,这将触发一个页故障(page fault),而被Linux的expand_stack()处理,它会调用acct_stack_growth()来检查是否还有合适的地方用于栈的增长。

如果栈的大小低于RLIMIT_STACK(通常为8MB),那么一般情况下栈会被加长,程序继续执行,感觉不到发生了什么事情。这是一种将栈扩展到所需大小的常规机制;

然而,如果达到了最大栈空间的大小,就会栈溢出(stack overflow),程序收到一个段错误(segmentation fault)。

  • 动态栈增长是唯一一种访问未映射内存区域而被允许的情形,其他任何对未映射内存区域的访问都会触发页错误,从而导致段错误。一些被映射的区域是只读的,因此企图写这些区域也会导致段错误。

共享库内存段

在栈和堆之间,有一段是用来存放共享库的。

mmap段

mmap段,即Memory Mapping Segment。

File mappings (including dynamic libraries) and anonymous mappings. Example: /lib/libc.so

在栈段的低一段便是mmap段,mmap是一种高效便捷的文件I/O方式,内核将文件内容映射在此段内存中,常见情形便是加载动态链接库。另外,在Linux中,如果你通过malloc申请一块大于MMAP_THRESHOLD(通常默认为128KB, 可用mallopt()修改)大小的堆空间时, glibc会返回一块匿名的mmap内存块而非一块堆内存。

在mmap段下面便是堆段了,堆段同栈段一样,都是为进程运行提供动态的内存分配,但是其和栈的区别在于堆上内存的生命期和执行分配的函数的生命期不一致,堆上分配的内存只有在对应进程通过系统调用主动释放或进程结束后才会释放。所以,内存泄露这个经典的问题便由此产生。

另外, 由于堆内存的反复申请和释放,也不可避免的会造成堆段碎片化。这种情况可以使用“对象池”的设计手段来避免。

静态内存区域(数据段)–bss段和data段

堆段再往下便是BSS段DATA段这两个静态内存区域,这两段都是用来存储静态局部或静态全局变量,其在编译期间便决定了虚拟内存的消耗。

区别是DATA段存放的是已经初始化的变量,其映射自程序镜像中包含对应静态变量的文件;而BSS段则存放的是未初始化的变量,它不映射自任何一个执行文件

根据C语言标准规定,未初始化的静态成员变量的初始值必须为0,所以内核在加载二进制文件后执行程序前会将BSS段清0。

代码段

BSS和DATA段下是代码段(TEXT),此段存有程序的指令代码。Text段是通过只读的方式加载到内存中的,在多个进程中可以被安全共享。

数据存放于哪个段?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int gdata1 = 10;
int gdata2 = 0;
int gdata3;

static int gdata4 = 11;
static int gdata5 = 0;
static int gdata6;

int main()
{
int a = 12;
int b = 0;
int c;

static int d = 13;
static int e = 0;
static int f;

return 0;
}

全部的指令都存放在代码段,而一个程序除了数据就是指令,因此,函数中的局部变量int a = 12; int b = 0; int c;均为指令。其余的静态变量、全局变量都为数据。

对于全局变量

全局变量都是数据。则gdata1~gdata6都是数据,都会生成对应的符号。

然而,如果未初始化或者初始化为0,均视为未初始化,如gdata2gdata3gdata5gdata6预留在.bss段。

未初始化和初始化为0是不同的概念、动作!即使最终的结果都是把int数据置0且存到bss段。但是显式初始化在C语言中会生成强符号,没有进行初始化的(即没有=0)会生成弱符号!

gdata1gdata4分配到.data段。

对于局部变量

对于非静态量,即abc来说,已经不再属于数据,因为不会生成相应的符号。

而是属于指令的范畴。实际翻译过来的指令含义如下。

1
mov dword ptr[a], 0Ch	# word2字节,dword表示double word,即4字节数据

即把0Ch移到a的内存中。

这些指令产生后,在.data代码段存放。

对于静态量,即def来说,依旧当作数据处理,生成符号,如果未初始化或者初始化为0,均视为未初始化,如ef预留在.bss段。

d分配到.data段。

虚拟地址的实现原理

它存在,你能看得见,这是物理的;
它存在,你看不见,这是透明的;
它不存在,你却看得见,这是虚拟的;
它不存在,你也看不见,这是删除的。


每个进程都运行在一个属于自己的内存沙盒里,这个沙盒即虚拟地址空间,这些虚拟地址再通过页表(page table)来映射到物理内存上,页表由操作系统维护并被CPU所引用。所以用户空间地址的映射是动态变化的;而内核空间则是持续存在的,在每个进程中都映射到相同的物理内存中,这样便于寻址以应对随时出现的中断和系统调用。

MySQL_数据类型

内容

  1. 数值类型
  2. 字符串类型
  3. 日期和时间类型
  4. enum和set

设计数据类型的重要性

业务层是工作在内存上的,速度很快。

而最先达到性能瓶颈的一般都是数据库,因为数据库涉及到磁盘IO。磁盘IO是很慢的。

所以我们定义数据表时,每一个字段的数据类型都要好好去斟酌。

MySQL server虽然工作在内存上,但是它要读取数据、索引时,就要进行磁盘IO。磁盘IO次数越少,则性能越高,所以我们要尽力减少磁盘IO次数。

MySQL数据类型定义了数据的大小范围,因此使用时选择合适的类型,不仅会降低表占用的磁盘空间,还能间接减少磁盘IO的次数,提高了表的访问效率;不仅如此,索引的效率也和数据类型息息相关。

数值类型

整数类型 字节 最小值 最大值
TINYINT 1 有符号-128;无符号0 有符号127;无符号255
SMALLINT 2 有符号-32768;无符号0 有符号32767;无符号65535
MEDIUMINT 3 有符号-8388608;无符号0 有符号8388607;无符号1677215
INTINTEGER 4 有符号-2147483648;无符号0 有符号2147483647;无符号4294967295
BIGINT 8 有符号-9223372036854775808;无符号0 有符号9223372036854775807;无符号18446744073709551615
浮点数类型 字节 最小值 最大值
FLOAT 4 ±1.175494351E38\pm 1.175494351E-38 ±3.402823466E+38\pm 3.402823466E+38
DOUBLE 8 ±2.2250738585072014E308\pm 2.2250738585072014E-308 ±1.7976931348623517E+308\pm 1.7976931348623517E+308

其中浮点类型推荐使用decimal类型(保存为字符串格式)。数据精度范围很大,约28位,而且计算时,越界、溢出会报错,而普通整数、浮点只是截断。

  • 注意
    • age INT(9)这不是意味着定义了一个长度为9字节的整型。数值类型占用内存的大小是固定的,和具体的类型是强相关的,括号内的数目只是代表它显示的宽度。

示例

1
create table user(age TINYINT unsigned NOT NULL default 0)

字符串类型

字符串类型 字节 描述及存储需求
CHAR(M) M M为0~255之间的整数。占用空间M字节。
VARCHAR(M) M为0~65535之间的整数,值的长度+1个字节
TINYBLOB 允许长度0~255字节,值的长度+1字节
BLOB 允许长度0~65535字节,值的长度+2字节
MEDIUMBLOB 允许长度0~167772150字节,值的长度+3字节
LONGBLOB 允许长度0~4294967295字节,值的长度+4字节
TINYTEXT 允许长度0~255字节,值的长度+2字节
TEXT 允许长度0~65535字节,值的长度+2字节
MEDIUMTEXT 允许长度0~167772150字节,值的长度+3字节
LONGTEXT 允许长度0~4294967295字节,值的长度+4字节
VARBINARY(M) 允许长度0~M个字节的变长字节字符串,值的长度+1字节
BINARY(M) M 允许长度0~M个字节的定长字节字符串
  • CHAR(12)VARCHAR(12)的区别在于,如果实际的字符串长度不够12,VARCHAR大小则为实际的长度,而CHAR的大小则依旧是12字节;共同点:如果数据超过12则都会产生截断。
  • BLOB一般用于存储二进制格式的图片、音频。
  • TEXT的应用:留言板,商品的备注说明,聊天信息的存储。一些大文本。

初期设计时,可能低估了将来的数据量,可能会超过设计的范围。

数据库中字符串以单引号包裹。

  • SQL注入式攻击——字符串拼接导致的问题。

日期和时间类型

日期和时间类型 字节 最小值 最大值
DATE 4 1000-01-01 9999-12-31
DATETIME 8 1000-01-01 00:00:00 9999-12-31 23:59:59
TIMESTAMP 4 19700101080001 2038年的某个时刻
TIME 3 -838:59:59 838:59:59
YEAR 1 1901 2155
  • 日期类型是做项目过程中经常使用的类型信息,尤其是TIMESTAMPDATETIME两个类型;
  • 注意TIMESTAMP会自动更新时间,非常适合需要记录最新更新时间的场景,对应的函数是unix_timestamp(now());而DATETIME需要手动更新。
  • 这里虽然介绍了MySQL的时间类型,但是现代的后端开发,MySQL基本已经很少使用自身的存储过程、存储函数、触发器、外键设置了,尽量把复杂的业务在业务层处理。涉及的时间操作也是同理。应该让MySQL聚焦在核心的增删改查操作上。

时间戳示例

1
2
select now();					# 2022-04-28 19:30:11
select unix_timestamp(now()); # 1617683855

unix_timestamp(now());用于生成UNIX的时间戳,数值含义是自1970年起的秒数。用整型存储。

枚举和集合(enum和set)

  • 这两种类型都是限制该字段只能取预定义的固定的某些值。比如性别——只能男或女。
  • 枚举字段只能取一个唯一的值。
  • 集合字段可以取任意个值。

示例

1
sex enum('M','W') default 'M'