undo表空间

undo tablespace 中包含undo log,是一种独立的表空间。在mysql启动时会默认创建两个undo表空间,且系统要求起码有两个活跃的undo表空间以支持自动truncate功能。(这样可以保证一个离线truncate另一个继续提供服务)。undo表空间文件创建在–innodb_undo_derectory变量指定的位置,默认在data目录下,文件名undo_N格式。每个文件的初始大小基于–innodb_page_size参数(默认16K), 默认大小为10M。

添加undo表空间

8.0.14版本以前,可以在启动时设置–innodb_undo_tablespaces来指定创建的undo表空间,后续版本废弃改参数,创建可以使用

create undo tablespace tablespace_name add datafile "filename";

使用该语法时,指定的filename可以是绝对路径,但是该路径必须是innodb可以识别的,–innodb_directoried变量定义了这些路径,而且不管该变量如何显示定义,都会包含三个默认路径,分别是–innodb_undo_directory, –data_dir,–innodb_data_home_dir,mysql启动时会在这些路径里扫描寻找undo tablespaces。
如果没有指定绝对路径,那么undo tablespace会被创建在innodb_undo_directory下,如果该变量为空,会创建在mysql的data(/var/lib/mysql)目录下.

删除undo表空间

create undo tablespace语法创建的undo表空间可以使用drop undo tablespace语法来删除(默认创建的表空间不可用),在删除前必须保证该空间是empty状态。
在清空一个表空间前必须把它的状态设置为inactive,确保该表空间的rollback segment的undo segment不会分配给事务使用。

alter undo tablespace tablespace_name set inactive;

设置后会等待已经使用该表空间的事务提交,待所有的事务提交后,purge线程开始truncate表空间至初始大小,这时候表空间的状态为empty,即可以被drop。

 drop undo tablespace tablespace_name;

可以在INNODB_TABLESPACES 表里查询到每个表空间的状态,包括undo表空间

 SELECT NAME, STATE FROM INFORMATION_SCHEMA.INNODB_TABLESPACES 
 WHERE NAME LIKE tablepace_name;

默认的undo表空间不可以被drop但是可以设置成inactive状态,但是必须保证起码有两个活跃的表空间以支持表空间的自动truncate,否则会报错。

移动undo表空间

用create undo tablespace语法创建的undo表空间文件可以移动到任何innndb可以识别的路径,mysql在启动时会找到他们,但是默认创建的undo表空间不可移动,如果默认undo表空间被移动了,那么必须在启动时更改–innodb_undo_directory参数为移动后的路径。

清除undo表空间

一个表空间文件默认最大为1G,由–innodb_max_undo_log_size参数控制,innodb提供了自动和手动两种方式清除undo log
自动清除由purge线程(垃圾回收线程,可能有多个,后台独立运行,参数–innodb_purge_threads指定)完成,需要开启–innodb_undo_log_truncate参数,默认是开启的。

由于自动truncate必须保证有两个活动的undo表空间,那么手动就必须有三个获得undo表空间,因为可能有一个正在truncate而处于offline。
两种方式清除过程如上所述,等undo表空间的状态时empty后,状态再设置成active

对于自动清除可以使用–innodb_purge_rseg_truncate_frequency参数来提高清除频率,默认是purge被触发(undo tablespace大小超过–innodb_max_undo_log_size参数)了128次后,执行一次truncate,可以设置更小的值加速truncate。

MVCC多版本并发控制

mvcc可以保证RR隔离级别下一致性读(consistent read)的问题,也解决了读写冲突的问题。

读写冲突的来源

没有mvcc控制的情况下,假如我们的场景是读远大于写的场景,那么事务A对某读频繁的表id为1的行进行更新,根据两阶段锁协议,那么该行是会加一个X锁,且没有释放,这样其他事务在读取的时候会被锁上,导致并发能力降低。

而mvcc允许行被加锁的情况下,读取旧版本数据。

一致性读

innodb的一致性读通过一致性读视图及undo log实现。

Undo Log

数据库中的每一行都保存这不同的版本,旧版本保存在undo log中。

当一行数据被创建时,会有一些额外的数据,如下图(其中rowid只会在没有主键情况下生成,一般某个表没有指定主键innodb会选择第一个唯一索引作为主键,如果在没有会隐式的生成一列rowid):

当改行数据被修改时,

为该行记录生成undo log, 新纪录的db_roll_pt指针指向该条记录。

undo logs会有一个单独的purge线程负责清除,保证undo logs不会无限增长。

undo logs会有一些参数控制:

innodb_undo_directory:将undo log存储位置独立出来,默认位置为安装目录。

inndb_undo_tablespace:mysql初始化时分配的表空间个数,默认为128,之后会在data目录下创建制定个数的undo文件,文件名以undo_000,undo_001递增。(8.0.14版本后不再支持,具体undo tablespace的介绍,看详情)

(之所以mysql可以把undo log存储独立出来,原因是可以把日志部署到ssd上,提高事务的并发读。)

undo log都是保存在undo log segment中,undo log segment 保存在rollback segment中。

每个回滚段包含1024个undo log segment, 而mysql最大支持128个回滚段(innodb_rollback_segments参数可以显示控制)。因为每个事务在读写undo log时都要独占一块undo log segment,所以理论上最大支持1024 * 128个并发事务操作。

 

一致性读的实现

RR级别下,数据可见性的规则就是,事务启动前已经提交的事务的更改可见,事务启动后的事务提交不可见。(begin/start transaction并不是一个事务的起点,在该指令后的第一个操作标的语句才标识着事务启动。)

每个事务在启动时会获得一个递增的事务id,记trx_id,并且会创建一个read-view的struct,该结构体包含一个当前所有活动事务的id集合(包括当前事务),一个最小活动事务id和一个最大活动事务id,之前提到过,每一行数据都有一个额外的db_trx_id, 读取到该行时,如果该db_trx_id与当前事务id不一致,就开始读取undo log,然后利用undo log和活动事务集合判断出可以读取的值,判断逻辑图如下:

以上概念称为一致性读,与之对应的有一个”当前读”, 即:更新数据都是先读后写的,而这个读,只能读当前的值,称为“当前读”(current read)

举个例子:

k初始为1。

那么B再去查询时结果为3,这就是当前读。

缓冲区溢出攻击

输入的大小超出了分配的缓冲区内存大小,导致溢出。

char string[5];
gets(string);  // 输入helloworld

程序相关的数据一般保存在内存的以下区域:

  1.  数据段:静态存储区(比如字符串字面量)和全局变量
  2. 代码段:程序机器码
  3. 栈:函数栈帧,一个函数栈帧包括参数,返回地址(函数返回后cpu执行的下一条指令的地址),局部变量等
  4. 堆:程序运行时动态申请的内存数据

当调用函数时,逻辑堆栈帧被压入栈中,堆栈帧包括函数的参数、返回地址、EBP(EBP是当前函数的存取指针,即存储或者读取数时的指针基地址,可以看成一个标准的函数起始代码)和局部变量(如果函数有局部变量)。程序执行结束后,局部变量的内容将会消失,但是不会被清除。
当函数返回时,逻辑堆栈帧从栈中被弹出,然后弹出EBP,恢复堆栈到调用函数时的地址,最后弹出返回地址到EIP(寄存器存放下一个CPU指令存放的内存地址,当CPU执行完当前的指令后,从EIP寄存器中读取下一条指令的内存地址,然后继续执行。),从而继续运行程序。所以假如有下图的情况:

由于栈是低地址方向增长的,因此局部数组buffer的指针在缓冲区的下方。当把data的数据拷贝到buffer内时,超过缓冲区区域的高地址部分数据会“淹没”原本的其他栈帧数据,根据淹没数据的内容不同,可能会有产生以下情况:

  1. 淹没了其他的局部变量。如果被淹没的局部变量是条件变量,那么可能会改变函数原本的执行流程。这种方式可以用于破解简单的软件验证。

       2. 淹没了ebp的值。修改了函数执行结束后要恢复的栈指针,将会导致栈帧失去平衡。

       3. 淹没了返回地址。这是栈溢出原理的核心所在,通过淹没的方式修改函数的返回地址,使程序代码执行“意外”的流程!

       4. 淹没参数变量。修改函数的参数变量也可能改变当前函数的执行结果和流程。

       5. 淹没上级函数的栈帧,情况与上述4点类似,只不过影响的是上级函数的执行。当然这里的前提是保证函数能正常返回,即函数地址不能被随意修改(这可能很麻烦!)。

而缓冲区溢出攻击就是利用了第三点,改变了cpu要执行的下一条指令的地址。

但是从实现上来说很难,因为没法预估返回地址在栈区的什么位置,也就无法确定溢出多少字节才能覆盖到返回地址。