0%

背景

DDL(Data Definition Languages)语句:数据定义语⾔,这些语句定义了不同的数据段、数据库、表、列、索引等数据库对象的定义。常⽤的语句关键字主要包括 create、drop、alter等。
在 mysql 5.5 版本以前,修改表结构如添加索引、修改列,需要锁表,期间不能写⼊,导致库上⼤量线程处于“Waiting for meta data lock”状态。

有⼀系列⼯具从⼏个⻆度解决这个问题。按历史的发展⻆度,可以归纳为

  1. ddl
  2. mysql 原⽣ online DDL
  3. pt-online-schema-change
  4. gh-ost

旧版innodb DDL

5.5版本以前,执⾏ddl主要有两种⽅式copy⽅式和inplace⽅式,inplace⽅式⼜称为(fast indexcreation)。相对于copy⽅式,inplace⽅式不拷⻉数据,因此较快。但是这种⽅式仅⽀持添加、删除⾮主键索引两种⽅式,⽽且与copy⽅式⼀样需要全程锁表,实⽤性不是很强。下⾯以加索引为例,简单介绍这两种⽅式的实现流程。

copy⽅式

  1. 新建带索引的临时表
  2. 锁原表,禁⽌DML,允许查询
  3. 将原表数据拷⻉到临时表(⽆排序,⼀⾏⼀⾏拷⻉)
  4. 进⾏rename,升级字典锁,禁⽌读写
  5. 完成创建索引操作

inplace⽅式

  1. 新建索引的数据字典
  2. 锁表,禁⽌DML,允许查询
  3. 读取聚集索引,构造新的索引项,排序并插⼊新索引
  4. 等待当前表所有已开始的只读事务提交
  5. 创建索引结束

inplace创建⼆级索引时会对原表加上⼀个共享锁,创建过程不需要重建表(no-rebuild);删除InnoDB⼆级索引只需要更新内部视图,并标记这个索引的物理空间可⽤,去掉数据库元数据上该索引的定义即可。

原生online DDL

配置

FIC只对索引的创建删除有效,MySQL 5.6 Online DDL把这种特性扩展到了添加列、删除列、修改列类型、列重命名、设置默认值等等,实际效果要看所使⽤的选项和操作类别来定。MySQL 在线DDL同样分为 INPLACE 和 COPY 两种⽅式。

  • 对于copy⽅式,⽐如修改列类型,删除主键,修改字符集等场景,这些操作都会导致记录格式发⽣变化,⽆法通过简单的全量+增量的⽅式实现online
  • 对于inplace⽅式,mysql内部以“是否修改记录格式”为基准也分为两类,⼀类需要重建表(重新组织记录) ,⽐如optimize table、添加索引、添加/删除列、修改列NULL/NOT NULL属性等;另外
    ⼀类是只需要修改表的元数据,⽐如删除索引、修改列名、修改列默认值、修改列⾃增值等。

Mysql将这两类⽅式分别称为 rebuild ⽅式和 no-rebuild ⽅式。

1
2
3
4
5
6
7
8
? alter table
| ALGORITHM [=] {DEFAULT|INPLACE|COPY}
| ALTER [COLUMN] col_name {SET DEFAULT literal | DROP DEFAULT}
| CHANGE [COLUMN] old_col_name new_col_name column_definition
[FIRST|AFTER col_name]
| LOCK [=] {DEFAULT|NONE|SHARED|EXCLUSIVE}
| MODIFY [COLUMN] col_name column_definition
[FIRST | AFTER col_name]

通过在ALTER语句的ALGORITHM参数指定。

  • ALGORITHM=INPLACE ,可以避免重建表带来的IO和CPU消耗,保证ddl期间依然有良好的性能和并发。
  • ALGORITHM=COPY ,需要拷⻉原始表,所以不允许并发DML写操作,可读。这种copy⽅式的效率还是不如 inplace ,因为前者需要记录undo和redo log,⽽且因为临时占⽤buffer pool引起短时间内性能受影响。

上⾯只是 Online DDL 内部的实现⽅式,此外还有 LOCK 选项控制是否锁表,根据不同的DDL操作类型有不同的表现:默认mysql尽可能不去锁表,但是像修改主键这样的昂贵操作不得不选择锁表。

  • LOCK=NONE ,即DDL期间允许并发读写涉及的表,⽐如为了保证 ALTER TABLE 时不影响⽤⼾注册或⽀付,可以明确指定,好处是如果不幸该 alter语句不⽀持对该表的继续写⼊,则会提⽰失败,⽽不会直接发到库上执⾏。 ALGORITHM=COPY 默认LOCK级别
  • LOCK=SHARED ,即DDL期间表上的写操作会被阻塞,但不影响读取。
  • LOCK=DEFAULT ,让mysql⾃⼰去判断lock的模式,原则是mysql尽可能不去锁表
  • LOCK=EXCLUSIVE ,即DDL期间该表不可⽤,堵塞任何读写请求。如果你想alter操作在最短的时间内完成,或者表短时间内不可⽤能接受,可以⼿动指定。

⽆论任何模式下,online ddl开始之前都需要⼀个短时间排它锁(exclusive)来准备环境,所以alter命令发出后,会⾸先等待该表上的其它操作完成,在alter命令之后的请求会出现等待waiting metadata lock。同样在ddl结束之前,也要等待alter期间所有的事务完成,也会堵塞⼀⼩段时间。所以尽量在ALTER TABLE之前确保没有⼤事务在执⾏,否则⼀样出现连环锁表。

online DDL操作类别

  • In-Place 为Yes是优选项,说明该操作⽀持INPLACE
  • Copies Table 为No是优选项,因为为Yes需要重建表。⼤部分情况与In-Place是相反的
  • Allows Concurrent DML? 为Yes是优选项,说明ddl期间表依然可读写,可以指定 LOCK=NONE(如果操作允许的话mysql⾃动就是NONE)
  • Allows Concurrent Query? 默认所有DDL操作期间都允许查询请求,放在这只是便于参考
  • Notes 会对前⾯⼏列Yes/No带 * 号的限制说明
Operation In-Place? Copies Tables? Allow Concurrent DML? Allows Concurent Query? Notes
添加索引 Yes* No* Yes Yes 对全文索引的一些限制
删除索引 Yes No Yes Yes 仅修改表的元数据
OPTIMIZE TABLE Yes Yes Yes Yes 从5.6.17开始使用ALGORITHM=INPLACE, 当然如果指定了old_alter_table=1或mysqld启动带–skip-new则将还是COPY模式。如果表上有全文索引只支持Cope
对一列设置默认值 Yes No Yes Yes 仅修改表的元数据
对一列修改auto-increment的值 Yes No Yes Yes 仅修改表的元数据
添加foreign key constraint Yes No* Yes Yes 为了避免拷贝表,在约束创建时会禁用foreign_key_checks
删除foreign key constraint Yes No Yes Yes foreign_key_checks不影响
改变列名 Yes* No* Yes* Yes 为了允许DML并发,如果保持相同数据类型,仅改变列名
添加列 Yes* Yes* Yes* Yes 尽管允许ALGORITHM=INPLACE,但数据大幅重组,所以它仍然是一项昂贵的操作。当添加列是auto-increment,不允许DML并发
删除列 Yes Yes* Yes Yes 修改类型或添加长度,都会拷贝表,而且不允许更新操作
修改列数据类型 No Yes* No Yes 尽管允许ALGORITHM=INPLACE,但数据大幅重组,所以它仍然是一项昂贵的操作。
更改列顺序 Yes Yes Yes Yes 尽管允许ALGORITHM=INPLACE,但数据大幅重组,所以它仍然是一项昂贵的操作。
修改ROW-FORMAT和KEY_BLOCK_SIZE Yes Yes Yes Yes 尽管允许ALGORITHM=INPLACE,但数据大幅重组,所以它仍然是一项昂贵的操作。
没置列属性NULL或NOT NULL Yes Yes Yes Yes 尽管允许ALGORITHM=INPLACE,但数据大幅重组,所以它仍然是一项昂贵的操作。如果列定义必须转化NOT NULL,则不允许INPLACE
添加主键 Yes* Yes Yes Yes 在同一个 ALTER TABLE 语句删除就主键、添加新主键时,才允许inplace;数据大幅重组,所以它仍然是一项昂贵的操作。
删除并添加主键 Yes Yes Yes Yes 在同一个ALTER TABLE语句删除旧主键、添加新主键时,才允许INPLACE;数据大幅重组,所以它仍然是一项昂贵的操作。
删除主键 No Yes No Yes 不允许并发DML,要拷贝表,而且如果没有在同一ATLER TABLE 语句里同时添加主键则会收到限制
变更表字符集 No Yes No Yes 如果新的字符集编码不同,重建表

规则总结

  • In-Place为No,DML⼀定是No,说明 ALGORITHM=COPY ⼀定会发⽣拷⻉表,只读。
  • ALGORITHM=INPLACEE 也要可能发⽣拷⻉表,但可以并发DML:
    • 添加、删除列,改变列顺序
    • 添加或删除主键
    • 改变⾏格式ROW_FORMAT和压缩块⼤⼩KEY_BLOCK_SIZE
    • 改变列NULL或NOT NULL
    • 优化表OPTIMIZE TABLE
    • 强制 rebuild 该表
  • 不允许并发DML的情况有
    • 修改列数据类型
    • 删除主键
    • 变更表字符集
  • 更改聚集索引,体现了表数据在物理磁盘上的排列,包含了数据⾏本⾝,需要拷⻉表;⽽普通索引通过包含主键列来定位数据,所以普通索引的创建只需要⼀次扫描主键即可,⽽且是在已有数据的表上建⽴⼆级索引,更紧凑,将来查询效率更⾼。
  • 修改主键也就意味着要重建所有的普通索引。

执⾏流程

  • Prepare阶段 :

    1. 创建新的临时frm⽂件(与InnoDB⽆关)
    2. 持有EXCLUSIVE-MDL锁,禁⽌读写
    3. 根据alter类型,确定执⾏⽅式(copy,online-rebuild,online-norebuild)
    4. 更新数据字典的内存对象
    5. 分配row_log对象记录增量(仅rebuild类型需要)
    6. ⽣成新的临时ibd⽂件(仅rebuild类型需要)
  • ddl执⾏阶段 :

    1. 降级EXCLUSIVE-MDL锁,允许读写
    2. 扫描old_table的聚集索引每⼀条记录rec
    3. 遍历新表的聚集索引和⼆级索引,逐⼀处理
    4. 根据rec构造对应的索引项
    5. 将构造索引项插⼊sort_buffer块排序
    6. 将sort_buffer块更新到新的索引上
    7. 记录ddl执⾏过程中产⽣的增量(仅rebuild类型需要)
    8. 重放row_log中的操作到新索引上(no-rebuild数据是在原表上更新的)
    9. 重放row_log间产⽣dml操作append到row_log最后⼀个Block
  • commit阶段 :

    1. 当前Block为row_log最后⼀个时,禁⽌读写,升级到EXCLUSIVE-MDL锁
    2. 重做row_log中最后⼀部分增量
    3. 更新innodb的数据字典表
    4. 提交事务(刷事务的redo⽇志)
    5. 修改统计信息
    6. rename临时idb⽂件,frm⽂件
    7. 变更完成

⼏个疑问点

  1. 如何实现数据完整性
    row_log记录了ddl变更过程中新产⽣的dml操作,并在ddl执⾏的最后将其应⽤到新的表中,保证数据完整性。
  2. online与数据⼀致性如何兼得
    online ddl在prepare阶段和commit阶段都会持有MDL-Exclusive锁,禁⽌读写。由于prepare和commit阶段相对于ddl执⾏阶段时间特别短,因此基本可以认为是全程online的。Prepare阶段和commit阶段的禁⽌读写,主要是为了保证数据⼀致性。Prepare阶段需要⽣成row_log对象和修改内存的字典;Commit阶段,禁⽌读写后,重做最后⼀部分增量,然后提交,保证数据⼀致。
  3. 如何实现server层和innodb层⼀致性
    在prepare阶段,server层会⽣成⼀个临时的frm⽂件,⾥⾯包含了新表的格式;innodb层⽣成了临时的ibd⽂件(rebuild⽅式);在ddl执⾏阶段,将数据从原表拷⻉到临时ibd⽂件,并且将row_log增量应⽤到临时ibd⽂件;在commit阶段,innodb层修改表的数据字典,然后提交;最后innodb层和mysql层⾯分别重命名frm和idb⽂件。
  4. 主从模式下的延时
    在主从环境下,主库执⾏alter命令在完成之前是不会进⼊binlog记录事件,如果允许dml操作则不影响记录时间,所以期间不会导致延迟。然⽽,由于从库是单个SQL Thread按顺序应⽤relay log,轮到ALTER语句时直到执⾏完才能下⼀条,所以从库会在master ddl完成后开始产⽣延迟。如果主库ddl使⽤了1⼩时,从库将产⽣1⼩时的主从延时。(pt-osc可以控制延迟时间,所以这种场景下它更合适)

pt-online-schema-change

online DDL还是有⼀段锁表时间,这段时间的查询依然会引起meta data lock问题。pt-osc解决了这个问题,有在线修改表不锁表的能⼒。percona公司

pt-osc⼯作过程

  1. 创建⼀个和要执⾏ alter 操作的表⼀样的新的空表结构(是alter之前的结构)
  2. 在新表执⾏alter table 语句(速度应该很快)
  3. 在原表中创建触发器3个触发器分别对应insert,update,delete操作
    1
    2
    3
    6165 Query CREATE TRIGGER `pt_osc_confluence_sbtest3_del` AFTER DELETE ON `confluence`.`sbtest3` FOR EACH ROW DELETE IGNORE FROM `confluence`.`_sbtest3_new` WHERE `confluence`.`_sbtest3_new`.`id` <=> OLD.`id`
    6165 Query CREATE TRIGGER `pt_osc_confluence_sbtest3_upd` AFTER UPDATE ON `confluence`.`sbtest3` FOR EACH ROW REPLACE INTO `confluence`.`_sbtest3_new`(`id`, `k`, `c`, `pad`) VALUES (NEW.`id`, NEW.`k`, NEW.`c`, NEW.`pad`)
    6165 Query CREATE TRIGGER `pt_osc_confluence_sbtest3_ins` AFTER INSERT ON `confluence`.`sbtest3` FOR EACH ROW REPLACE INTO `confluence`.`_sbtest3_new`(`id`, `k`, `c`, `pad`) VALUES (NEW.`id`, NEW.`k`, NEW.`c`, NEW.`pad`)
    replace into :
    a. 如果发现表中已经有此⾏数据(根据主键或者唯⼀索引判断)则先删除此⾏数据,然后插⼊新的数据。
    b. 否则,直接插⼊新数据。
  4. 以⼀定块⼤⼩从原表拷⻉数据到临时表,拷⻉过程对数据⾏持有S锁。拷⻉过程中通过原表上的触发器在原表进⾏的写操作都会更新到新建的临时表。
    1
    2
    3
    6165 Query INSERT LOW_PRIORITY IGNORE INTO `confluence`.`_sbtest3_new`(`id`, `k`, `c`, `pad`) SELECT `id`, `k`, `c`, `pad` FROM `confluence`.`sbtest3` FORCE INDEX(`PRIMARY`) WHERE ((`id` >= '4692805')) AND ((`id` <= '4718680')) LOCK IN SHARE MODE /*pt-online-schema-change 46459 copy nibble*/
    INSERT IGNORE 与INSERT INTO的区别就是INSERT IGNORE会忽略数据库中已经存在 的数据,如果数据库没有数据,就插⼊新的数据,如果有数据的话就跳过这条数据。这样就可以保留数据库中已经存在数据,达到在间隙中插⼊数据的⽬的。
    如果数据修改的时候,还没有拷⻉到新表,修改后再拷⻉,虽然重复覆盖,但是数据也没有出错;如果是数据已经拷⻉,原表发⽣修改,这时触发器同步修改数据,两种情况下都保证了数据的⼀致性;
  5. Rename 原表到old表中,在把临时表Rename为原表
  6. 如果有参考该表的外键,根据alter-foreign-keys-method参数的值,检测外键相关的表,做相应设置的处理
    alter-foreign-keys-method配置:
    • rebuild_constraints ,优先采⽤这种⽅式
      • 它先通过 alter table t2 drop fk1,add _fk1 重建外键参考,指向新表
      • 再 rename t1 t1_old, _t1_new t1 ,交换表名,不影响客⼾端删除旧表 t1_old
      • 但如果字表t2太⼤,以致alter操作可能耗时过⻓,有可能会强制选择 drop_swap。
      • 涉及的主要⽅法在 pt-online-schema-change ⽂件的determine_alter_fk_method, rebuild_constraints, swap_tables三个函数中。
    • drop_swap ,
      • 禁⽤t2表外键约束检查 FOREIGN_KEY_CHECKS=0
      • 然后 drop t1 原表
      • 再 rename _t1_new t1
      • 这种⽅式速度更快,也不会阻塞请求。但有⻛险,第⼀,drop表的瞬间到rename过程,原表t1是不存在的,遇到请求会报错;第⼆,如果因为bug或某种原因,旧表已删,新表rename失败,那就太晚了,但这种情况很少⻅。
      • 我们的开发规范决定,即使表间存在外键参考关系,也不通过表定义强制约束。
  7. 默认最后将旧原表删除
  8. 从库通过binlog执⾏相同操作

与原⽣online ddl⽐较

  • online ddl在必须copy table时成本较⾼,不宜采⽤
  • pt-osc⼯具在表存在触发器时,不适⽤(⼀个表上不能同时有2个相同类型的触发器)
  • 修改索引、外键、列名时,优先采⽤online ddl,并指定 ALGORITHM=INPLACE
  • 其它情况使⽤pt-osc,虽然存在copy data
  • pt-osc⽐online ddl要慢⼀倍左右,因为它是根据负载调整的
  • ⽆论哪种⽅式都选择的业务低峰期执⾏
  • 特殊情况需要利⽤主从特性,先alter从库,主备切换,再改原主库

gh-ost

why gh-ost

基于触发器的online ddl⽅案的不⾜

  • Triggers, overhead: 触发器是⽤存储过程的实现的,就⽆法避免存储过程本⾝需要的开销。
  • Triggers, locks: 增⼤了同⼀个事务的执⾏步骤,更多的锁争抢。
  • Trigger based migration, no pause: 整个过程⽆法暂停,假如发现影响主库性能,停⽌ OnlineDDL,那么下次就需要从头来过。
  • Triggers, multiple migrations: 多个并⾏的操作是不安全的。
  • Trigger based migration, no reliable production test: ⽆法在⽣产环境做测试。
  • Trigger based migration, bound to server: 触发器和源操作还是在同⼀个事务空间。

gh-ost优点:

  • ⽆触发器
    gh-ost 希望⼆进制⽂件使⽤基于⾏的⽇志格式。也可以使⽤从库,将基于语句的⽇志格式转化成基于⾏的⽇志格式。
  • 轻量级
    因为不需要使⽤触发器,gh-ost 把修改表定义的负载和正常的业务负载解耦开了。它不需要考虑被修改的表上的并发操作和竞争等,这些在⼆进制⽇志中都被序列化了,gh-ost 只操作临时表,完全与原始表不相⼲。事实上,gh-ost 也把⾏拷⻉的写操作与⼆进制⽇志的写操作序列化了,这样,对主库来说只是有⼀条连接在顺序的向临时表中不断写⼊数据,这样的⾏为与常⻅的 ETL 相当不同。
  • 并⾏操作
    对于 gh-ost 来说就是多个对主库的连接来进⾏写操作。
  • 可暂停
    因为所有写操作都是 gh-ost ⽣成的,⽽读取⼆进制⽂件本⾝就是⼀个异步操作,所以在暂停时,gh-ost 是完全可以把所有对主库的写操作全都暂停的。
  • 动态可控
    gh-ost 通过监听 TCP 或者 unix socket ⽂件来获取命令。即使有正在进⾏中的修改⼯作,⽤⼾也可以向 gh-ost 发出命令修改配置
  • 可审计
    和动态可控⼀样,可以通过命令查看任务进度、参数、实例情况等。
  • 可测试
  • 可靠
    综合以上⼏个特性。它是可靠的。

原理

gh-ost 作为⼀个伪装的备库,可以从主库/备库上拉取 binlog,过滤之后重新应⽤到主库上去,相当于主库上的增量操作通过 binlog ⼜应⽤回主库本⾝,不过是应⽤在幽灵表上。
image-004.png

  1. gh-ost ⾸先连接到主库上,根据 alter 语句创建幽灵表
  2. 然后作为⼀个”备库“连接到其中⼀个真正的备库上,⼀边在主库上拷⻉已有的数据到幽灵表,⼀边从备库上拉取增量数据的 binlog,然后不断的把 binlog 应⽤回主库。
  3. cut-over 是最后⼀步,锁住主库的源表,等待 binlog 应⽤完毕,然后替换 gh-ost 表为源表。gh-ost 在执⾏中,会在原本的 binlog event ⾥⾯增加以下 hint 和⼼跳包,⽤来控制整个流程的进度,检测状态等。

⼏种模式

image-005.png
作者并没有对着三种模式有明确的使⽤倾向

模式⼀、连上从库,在主库上修改

这是 gh-ost 默认的⼯作模式,它会查看从库情况,找到集群的主库并且连接上去。修改操作的具体步骤是:

  • 在主库上读写⾏数据;
  • 在从库上读取⼆进制⽇志事件,将变更应⽤到主库上;
  • 在从库上查看表格式、字段、主键、总⾏数等;
  • 在从库上读取 gh-ost 内部事件⽇志(⽐如⼼跳);
  • 在主库上完成表切换;

如果主库的⼆进制⽇志格式是 Statement,就可以使⽤这种模式。但从库就必须配成启⽤⼆进制⽇志(log_bin, log_slave_updates),还要设成 Row 格式(binlog_format=ROW),实际上 gh-ost 会在从库上帮你做这些设置。
事实上,即使把从库改成 Row 格式,这仍然是对主库侵⼊最少的⼯作模式。

模式⼆、直接在主库上修改

如果没有从库,或者不想在从库上操作,那直接⽤主库也是可以的。gh-ost 就会在主库上直接做所有的操作。仍然可以在上⾯查看主从复制延迟。

  • 主库必须产⽣ Row 格式的⼆进制⽇志;
  • 启动 gh-ost 时必须⽤–allow-on-master 选项来开启这种模式;

模式三、在从库上修改和测试

这种模式会在从库上做修改。gh-ost 仍然会连上主库,但所有操作都是在从库上做的,不会对主库产⽣任何影响。在操作过程中,gh-ost 也会不时地暂停,以便从库的数据可以保持最新。

  • –migrate-on-replica 选项让 gh-ost 直接在从库上修改表。最终的切换过程也是在从库正常复制的状态下完成的。
  • –test-on-replica 表明操作只是为了测试⽬的。在进⾏最终的切换操作之前,复制会被停⽌。原始表和临时表会相互切换,再切换回来,最终相当于原始表没被动过。主从复制暂停的状态下,你可以检查和对⽐这两张表中的数据。

时序问题分析

binlog执⾏时间和copy old row时序不会影响最终结果。分析如下:
执⾏器对binlog语句实际执⾏时动作的替换

源类型 目标类型
insert replace
update update
delete delete

对与insert和update是没有问题的,因为⽆论copy old row和apply binlog的先后顺序,如果applybinlog在后,会覆盖掉copy old row,如果apply binlog在前⾯,copy old row因为使⽤insert ignore,因此会被ignore掉;

对与delete数据,abc三个操作,可能存在三种情况(b肯定在a的后⾯):
a.delete old row
b.delete binlog apply
c.copy old row

  1. cab,c会将数据copy到ghost表,最后b会把ghost表中的数据delete掉;
  2. acb,c空操作,b也是空操作;
  3. abc,b空操作,c也是空操作;

参考⽂档

ONLINE DDL VS PT-ONLINE-SCHEMA-CHANGE
gh-ost triggerless design
sbr vs rbr

用户态 内核态

用户态 内核态

Linux探秘之用户态与内核态

6dd7be5598bfd5a51a42ff570813d614.png

内核从本质上看是一种软件——控制计算机的硬件资源,并提供上层应用程序运行的环境。用户态即上层应用程序的活动空间,应用程序的执行必须依托于内核提供的资源,包括CPU资源、存储资源、I/O资源等。为了使上层应用能够访问到这些资源,内核必须为上层应用提供访问的接口:即系统调用。

因为操作系统的资源是有限的,如果访问资源的操作过多,必然会消耗过多的资源,而且如果不对这些操作加以区分,很可能造成资源访问的冲突。所以,为了减少有限资源的访问和使用冲突,Unix/Linux的设计哲学之一就是:对不同的操作赋予不同的执行等级,就是所谓特权的概念。简单说就是有多大能力做多大的事,与系统相关的一些特别关键的操作必须由最高特权的程序来完成。Intel的X86架构的CPU提供了0到3四个特权级,数字越小,特权越高,Linux操作系统中主要采用了0和3两个特权级,分别对应的就是内核态和用户态。运行于用户态的进程可以执行的操作和访问的资源都会受到极大的限制,而运行在内核态的进程则可以执行任何操作并且在资源的使用上没有限制。很多程序开始时运行于用户态,但在执行的过程中,一些操作需要在内核权限下才能执行,这就涉及到一个从用户态切换到内核态的过程。比如C函数库中的内存分配函数malloc(),它具体是使用sbrk()系统调用来分配内存,当malloc调用sbrk()的时候就涉及一次从用户态到内核态的切换,类似的函数还有printf(),调用的是wirte()系统调用来输出字符串,等等。

中断 异常 系统调用

中断 异常 系统调用

  • 系统调用 (system call)(软中断)
    应用程序 主动 向操作系统发出的服务器请求
  • 异常 (exception)
    非法指令或其他原因导致当前 指令执行失败
    如: 内存出错后的处理请求
  • 中断(hardware interrupt)
    来自硬件设备的处理请求

从触发方式上看,可以认为存在前述3种不同的类型,但是从最终实际完成由用户态到内核态的切换操作上来说,涉及的关键步骤是完全一致的,没有任何区别,都相当于执行了一个中断响应的过程,因为系统调用实际上最终是中断机制实现的,而异常和中断的处理机制基本上也是一致的。涉及到由用户态切换到内核态的步骤主要包括:

  1. 从当前进程的描述符中提取其内核栈的ss0及esp0信息。
  2. 使用ss0和esp0指向的内核栈将当前进程的cs,eip,eflags,ss,esp信息保存起来,这个过程也完成了由用户栈到内核栈的切换过程,同时保存了被暂停执行的程序的下一条指令。
  3. 将先前由中断向量检索得到的中断处理程序的cs,eip信息装入相应的寄存器,开始执行中断处理程序,这时就转到了内核态的程序执行了。

异步io

异步IO

线程状态流转

文件类型

普通文件

从Linux的角度来说,类似mp4、pdf、html这样应用层面上的文件类型都属于普通文件Linux用户可以根据访问权限对普通文件进行查看、更改和删除

目录文件(d,directory file)

目录文件对于用惯Windows的用户来说不太容易理解,目录也是文件的一种目录文件包含了各自目录下的文件名和指向这些文件的指针,打开目录事实上就是打开目录文件,只要有访问权限,你就可以随意访问这些目录下的文件(普通文件的执行权限就是目录文件的访问权限),但是只有内核的进程能够修改它们虽然不能修改,但是我们能够通过vim去查看目录文件的内容

符号链接(l,symbolic link)

这种类型的文件类似Windows中的快捷方式,是指向另一个文件的间接指针,也就是我们常说的软链接

块设备文件(b,block)和字符设备文件(c,char)

这些文件一般隐藏在/dev目录下,在进行设备读取和外设交互时会被使用到比如磁盘光驱就是块设备文件,串口设备则属于字符设备文件系统中的所有设备要么是块设备文件,要么是字符设备文件,无一例外

FIFO(p,pipe)

管道文件主要用于进程间通讯。比如使用mkfifo命令可以创建一个FIFO文件,启用一个进程A从FIFO文件里读数据,启动进程B往FIFO里写数据,先进先出,随写随读。

套接字(s,socket)

用于进程间的网络通信,也可以用于本机之间的非网络通信这些文件一般隐藏在/var/run目录下,证明着相关进程的存在

Linux 的文件是没有所谓的扩展名的,一个 Linux文件能不能被执行与它是否可执行的属性有关,只要你的权限中有 x ,比如[ -rwx-r-xr-x ] 就代表这个文件可以被执行,与文件名没有关系。跟在 Windows下能被执行的文件扩展名通常是 .com .exe .bat 等不同。不过,可以被执行跟可以执行成功不一样。比如在 root 主目彔下的 install.log 是一个文本文件,修改权限成为 -rwxrwxrwx 后这个文件能够真的执行成功吗? 当然不行,因为它的内容根本就没有可以执行的数据。所以说,这个 x 代表这个文件具有可执行的能力, 但是能不能执行成功,当然就得要看该文件的内容了。虽然如此,不过我们仍然希望能从扩展名来了解该文件是什么东西,所以一般我们还是会以适当的扩展名来表示该文件是什么种类的。所以Linux 系统上的文件名真的只是让你了解该文件可能的用途而已, 真正的执行与否仍然需要权限的规范才行。比如常见的/bin/ls 这个显示文件属性的指令要是权限被修改为无法执行,那么ls 就变成不能执行了。这种问题最常发生在文件传送的过程中。例如你在网络上下载一个可执行文件,但是偏偏在你的 Linux 系统中就是无法执行,那就可能是档案的属性被改变了。而且从网络上传送到你 的 Linux 系统中,文件的属性权限确实是会被改变的

文件描述符

文件描述符、文件、进程间的关系

  • 每个文件描述符会与一个打开的文件相对应
  • 不同的文件描述符也可能指向同一个文件
  • 相同的文件可以被不同的进程打开,也可以在同一个进程被多次打开

系统为维护文件描述符,建立了三个表

  • 进程级的文件描述符表
  • 系统级的文件描述符表
  • 文件系统的i-node表

82928b8141ddfaf30600569da698c4dc.png

示例:

%!(EXTRA markdown.ResourceType=, string=, string=)

  • 在进程A中,文件描述符1和30都指向了同一个打开的文件句柄(#23),这可能是该进程多次对执行打开操作
  • 进程A中的文件描述符2和进程B的文件描述符2都指向了同一个打开的文件句柄(#73),这种情况有几种可能,
    1. 进程A和进程B可能是父子进程关系;
    2. 进程A和进程B打开了同一个文件,且文件描述符相同(低概率事件);
    3. A、B中某个进程通过UNIX域套接字将一个打开的文件描述符传递给另一个进程。
  • 进程A的描述符0和进程B的描述符3分别指向不同的打开文件句柄,但这些句柄均指向i-node表的相同条目(#1936),换言之,指向同一个文件。发生这种情况是因为每个进程各自对同一个文件发起了打开请求。同一个进程两次打开同一个文件,也会发生类似情况。

文件描述符限制

格式

Linux操作系统支持很多不同的文件系统,比如ext2、ext3、XFS、FAT等等,而Linux把对不同文件系统的访问交给了VFS(虚拟文件系统),VFS能访问和管理各种不同的文件系统。所以有了区之后就需要把它格式化成具体的文件系统以便VFS访问。

标准的Linux文件系统Ext2是使用「基于inode的文件系统」

一般操作系统的文件数据除了文件实际内容外, 还带有很多属性,例如 Linux 操作系统的文件权限(rwx)与文件属性(拥有者、群组、 时间参数等),文件系统通常会将属性和实际内容这两部分数据分别存放在不同的区块在基于inode的文件系统中,权限与属性放置到 inode 中,实际数据放到 data block 区块中,而且inode和data block都有编号Ext2 文件系统在此基础上

文件系统最前面有一个启动扇区(boot sector)这个启动扇区可以安装开机管理程序, 这个设计让我们能将不同的引导装载程序安装到个别的文件系统前端,而不用覆盖整个硬盘唯一的MBR, 也就是这样才能实现多重引导的功能把每个区进一步分为多个块组 (block group),每个块组有独立的inode/block体系如果文件系统高达数百 GB 时,把所有的 inode 和block 通通放在一起会因为 inode 和 block的数量太庞大,不容易管理这其实很好理解,因为分区是用户的分区,实际计算机管理时还有个最适合的大小,于是计算机会进一步的在分区中分块(但这样岂不是可能出现大文件放不了的问题?有什么机制善后吗?)每个块组实际还会分为分为6个部分,除了inode table 和 data block外还有4个附属模块,起到优化和完善系统性能的作用所以整个分区大概会这样划分:

  1. inode table
    主要记录文件的属性以及该文件实际数据是放置在哪些block中,它记录的信息至少有这些:大小、真正内容的block号码(一个或多个)访问模式(read/write/excute)拥有者与群组(owner/group)各种时间:建立或状态改变的时间、最近一次的读取时间、最近修改的时间没有文件名!文件名在目录的block中!一个文件占用一个 inode,每个inode有编号Linux 系统存在 inode 号被用完但磁盘空间还有剩余的情况注意,这里的文件不单单是普通文件,目录文件也就是文件夹其实也是一个文件,还有其他的也是inode 的数量与大小在格式化时就已经固定了,每个inode 大小均固定为128 bytes (新的ext4 与xfs 可设定到256 bytes)文件系统能够建立的文件数量与inode 的数量有关,存在空间还够但inode不够的情况系统读取文件时需要先找到inode,并分析inode 所记录的权限与使用者是否符合,若符合才能够开始实际读取 block 的内容inode 要记录的资料非常多,但偏偏又只有128bytes , 而inode 记录一个block 号码要花掉4byte ,假设我一个文件有400MB 且每个block 为4K 时, 那么至少也要十万条block 号码的记录!inode 哪有这么多空间来存储?为此我们的系统很聪明的将inode 记录block 号码的区域定义为12个直接,一个间接, 一个双间接与一个三间接记录区(详细见附录)
  2. data block
    放置文件内容数据的地方在格式化时block的大小就固定了,且每个block都有编号,以方便inode的记录原则上,block 的大小与数量在格式化完就不能够再改变了(除非重新格式化)在Ext2文件系统中所支持的block大小有1K, 2K及4K三种,由于block大小的区别,会导致该文件系统能够支持的最大磁盘容量与最大单一文件容量各不相同:Block 大小 1KB 2KB 4KB最大单一档案限制 16GB 256GB 2TB最大档案系统总容量 2TB 8TB 16TB每个block 内最多只能够放置一个文件的资料,但一个文件可以放在多个block中(大的话)若文件小于block ,则该block 的剩余容量就不能够再被使用了(磁盘空间会浪费)所以如果你的档案都非常小,但是你的block 在格式化时却选用最大的4K 时,可能会产生容量的浪费既然大的block 可能会产生较严重的磁碟容量浪费,那么我们是否就将block 大小定为1K ?这也不妥,因为如果block 较小的话,那么大型档案将会占用数量更多的block ,而inode 也要记录更多的block 号码,此时将可能导致档案系统不良的读写效能事实上现在的磁盘容量都太大了,所以一般都会选择4K 的block 大小
  3. superblock
    记录整个文件系统相关信息的地方,一般大小为1024bytes,记录的信息主要有:block 与inode 的总量未使用与已使用的inode / block 数量一个valid bit 数值,若此文件系统已被挂载,则valid bit 为0 ,若未被挂载,则valid bit 为1block 与inode 的大小 (block 为1, 2, 4K,inode 为128bytes 或256bytes);其他各种文件系统相关信息:filesystem 的挂载时间、最近一次写入资料的时间、最近一次检验磁碟(fsck) 的时间Superblock是非常重要的, 没有Superblock ,就没有这个文件系统了,因此如果superblock死掉了,你的文件系统可能就需要花费很多时间去挽救每个块都可能含有superblock,但是我们也说一个文件系统应该仅有一个superblock 而已,那是怎么回事?事实上除了第一个块内会含有superblock 之外,后续的块不一定含有superblock,而若含有superblock则该superblock主要是做为第一个块内superblock的备份,这样可以进行superblock的救援
  4. Filesystem Description
    文件系统描述这个区段可以描述每个block group的开始与结束的block号码,以及说明每个区段(superblock, bitmap, inodemap, data block)分别介于哪一个block号码之间
  5. block bitmap
    块对照表如果你想要新增文件时要使用哪个block 来记录呢?当然是选择「空的block」来记录。那你怎么知道哪个block 是空的?这就得要通过block bitmap了,它会记录哪些block是空的,因此我们的系统就能够很快速的找到可使用的空间来记录同样在你删除某些文件时,那些文件原本占用的block号码就得要释放出来, 此时在block bitmap 中对应该block号码的标志位就得要修改成为「未使用中」
  6. inode bitmap
    与block bitmap 是类似的功能,只是block bitmap 记录的是使用与未使用的block 号码, 至于inode bitmap 则是记录使用与未使用的inode 号码

流程

创建文件过程

当在ext2下建立一个一般文件时, ext2 会分配一个inode 与相对于该文件大小的block 数量给该文件

  1. 例如:假设我的一个block 为4 Kbytes ,而我要建立一个100 KBytes 的文件,那么linux 将分配一个inode 与25 个block 来储存该文件
  2. 但同时请注意,由于inode 仅有12 个直接指向,因此还要多一个block 来作为区块号码的记录

创建目录过程

当在ext2文件系统建立一个目录时(就是新建了一个目录文件),文件系统会分配一个inode与至少一块block给该目录

  1. inode记录该目录的相关权限与属性,并记录分配到的那块block号码
  2. 而block则是记录在这个目录下的文件名与该文件对应的inode号
  3. block中还会自动生成两条记录,一条是.文件夹记录,inode指向自身,另一条是..文件夹记录,inode指向父文件夹

从目录树中读取某个文件过程

因为文件名是记录在目录的block当中,因此当我们要读取某个文件时,就一定会经过目录的inode与block ,然后才能够找到那个待读取文件的inode号码,最终才会读到正确的文件的block内的资料。

由于目录树是由根目录开始,因此操作系统先通过挂载信息找到挂载点的inode号,由此得到根目录的inode内容,并依据该inode读取根目录的block信息,再一层一层的往下读到正确的文件。举例来说,如果我想要读取/etc/passwd 这个文件时,系统是如何读取的呢?先看一下这个文件以及有关路径文件夹的信息:

1$ ll -di / /etc /etc/passwd2     128 dr-xr-x r-x . 17 root root 4096 May 4 17:56 /333595521 drwxr-x r-x . 131 root root 8192 Jun 17 00:20 /etc436628004 -rw-r– r– . 1 root root 2092 Jun 17 00:20 /etc/passwd复制代码

于是该文件的读取流程为:

  1. /的inode:通过挂载点的信息找到inode号码为128的根目录inode,且inode规定的权限让我们可以读取该block的内容(有r与x)
  2. /的block:经过上个步骤取得block的号码,并找到该内容有etc/目录的inode号码(33595521)
  3. etc/的inode:读取33595521号inode得知具有r与x的权限,因此可以读取etc/的block内容
  4. etc/的block:经过上个步骤取得block号码,并找到该内容有passwd文件的inode号码(36628004)
  5. passwd的inode:读取36628004号inode得知具有r的权限,因此可以读取passwd的block内容
  6. passwd的block:最后将该block内容的资料读出来

进程

进程控制块

进程控制块(Processing Control Block),是操作系统核心中一种数据结构,主要表示进程状态。其作用是使一个在多道程序环境下不能独立运行的程序(含数据),成为一个能独立运行的基本单位或与其它进程并发执行的进程。或者说,OS是根据PCB来对并发执行的进程进行控制和管理的。 PCB通常是系统内存占用区中的一个连续存区,它存放着操作系统用于描述进程情况及控制进程运行所需的全部信息,它使一个在多道程序环境下不能独立运行的程序成为一个能独立运行的基本单位或一个能与其他进程并发执行的进程。

进程控制块(PCB)是系统为了管理进程设置的一个专门的数据结构。系统用它来记录进程的外部特征,描述进程的运动变化过程。同时,系统可以利用PCB来控制和管理进程,所以说,PCB(进程控制块)是系统感知进程存在的唯一标志。

PCB内容

PCB通常记载进程之相关信息,包括

  • 程序计数器:接着要运行的指令地址。
  • 进程状态:可以是new、ready、running、waiting或 blocked等。
  • CPU暂存器:如累加器、索引暂存器(Index register)、堆栈指针以及一般用途暂存器、状况代码等,主要用途在于中断时暂时存储数据,以便稍后继续利用;其数量及类因电脑架构有所差异。
  • CPU排班法:优先级、排班队列等指针以及其他参数。
  • 存储器管理:如标签页表等。
  • 会计信息:如CPU与实际时间之使用数量、时限、账号、工作或进程号码。
  • 输入输出状态:配置进程使用I/O设备,如磁带机。

或按照信息类型划分:

  1. 进程标识信息
    进程标识信息用于唯一地标识一个进程,一个进程通常有两种标识符:内部标志符&外部标识符。
    为了描述进程间的家族关系,通常还设有父进程标识和子进程标识,以表示进程间的家族关系。
    此外,还设有用户名或用户标识号表示该进程属于哪个用户。
    • 内部标志符: 由操作系统赋予每个进程的一个唯一的数字标识符,它通常为一个进程的序号,方便了系统使用。
    • 外部标识符: 由创建者产生,是由字母和数字组成的字符串,为用户进程访问该进程提供方便。
  2. 处理机状态
    处理机状态信息主要由处理机的各个寄存器内的信息组成。 进程运行时的许多信息均存放在处理机的各种寄存器中。其中程序状态字(PSW)是相当重要的,处理机根据程序状态寄存器中的PSW来控制程序的运行。
  3. 进程调度信息
    PCB中还存放着一些与进程调度有关的信息。
    • 进程状态。 标识进程的当前状态(就绪、运行、阻塞),作为进程调度的依据。
    • 进程优先级。 表示进程获得处理机的优先程度。
    • 为进程调度算法提供依据的其他信息。例如,进程等待时间、进程已经获得处理器的总时间和进程占用内存的时间等。
    • 事件。 是指进程由某一状态转变为另一状态所等待发生的事件。(比如等待I/O释放)
  4. 进程控制信息
    • 程序和数据地址。 是指组成进程的程序和数据所在内存或外存中的首地址,以便在调度该进程时能从其PCB中找到相应的程序和数据。
    • 进程同步和通信机制。 指实现进程同步和通信时所采取的机制,如消息队列指针和信号量等,他们可以全部或部分存在PCB中。
    • 资源清单。 列出了进程所需的全部资源 及 已经分配给该进程的资源,但不包括CPU.
    • 链接指针。它给出了处于同一队列中的下一个PCB的首地址。

进程控制块PCB的组织方式

  1. 线性表方式:不论进程的状态如何,将所有的PCB连续地存放在内存的系统区。这种方式适用于系统中进程数目不多的情况。
  2. 索引表方式:该方式是线性表方式的改进,系统按照进程的状态分别建立就绪索引表、阻塞索引表等。
  3. 链接表方式:系统按照进程的状态将进程的PCB组成队列,从而形成就绪队列、阻塞队列、运行队列等。

进程上下文

进程是在操作系统支持下执行的,进程执行时需要操作系统为其设置相应的执行环境,如系统堆栈、地址映像寄存器、程序计数器、程序状态字、打开文件表以及相关通用寄存器等。 所以,把进程的物理实体与支持进程执行的物理环境合称为进程上下文。

1f6e169cea44343037de0dbebb6c0557.jpg

  • 上文: 把已执行的进程指令和数据在相关寄存器与堆栈中的内容称为上文。
  • 正文: 把正在执行的进程指令和数据在相关寄存器与堆栈中的内容称为正文。
  • 下文: 把待执行的进程指令和数据在相关寄存器与堆栈中的内容称为下文。

Unix System Ⅴ 的进程上下文组成:由用户级上下文、寄存器上下文、系统级上下文组成。 

0e68beb3d25e9fbc2b9c723c9acb676f.jpg

  • 用户级上下文:由进程的用户程序段部分编译而成的用户正文段、用户数据和用户栈等组成。
  • 寄存器上下文:由程序计数器(PC)、处理机状态字(PS)、栈指针和通用寄存器组成。 PC给出CPU将要执行的下一条指令的虚地址;PS给出机器与该进程相关联时的硬件状态;栈指针指向下一项的当前地址;通用寄存器则用于不同执行模式之间的参数传递。
  • 系统级上下文又分为静态部分和动态部分。 这里的动态部分是指进入和退出不同的上下文层次时,系统为各层上下文中相关联的寄存器值所保存和恢复的记录。 系统级上下文静态部分包括PCB结构、将进程虚地址空间映射到物理空间的有关表格、核心栈等。 这里,核心栈主要用来装载进程中所使用的系统调用的调用序列。

系统级上下文的动态部分是与寄存器上下文相关联的。

进程上下文的层次概念主要体现在动态部分中,即系统级上下文的动态部分可看成是由一些数量变化的层次组成,其变化规则符合许先进后出的堆栈方式。

进程状态

三状态进程模型:

4f950c55190031ed24b2ba24eabcc6db.png

挂起:处在挂起状态的进程映像在磁盘上,目的是减少内存占用

具有挂起状态的进程模型:

02b9703e5015f12db2a62786226553d7.png

进程切换

进程上下文切换发生在不同的进程之间而不是同一个进程内。

进城上下文切换分成三个步骤:

  1. 把被切换进程的相关信息保存到有关存储区,例如该进程的PCB中。
  2. 操作系统中的调度和资源分配程序执行,选取新的进程。
  3. 将被选中进程的原来保存的正文部分从有关存储区中取出,并送至寄存器与堆栈中,激活被选中进程执行。

总结: 进程上下文切换的切换过程涉及由谁来保护和获取进程的正文的问题,也就是如何使寄存器和堆栈等中的数据流入流出 PCB 的存储区。 另外,进程上下文切换还涉及系统调度和分配程序,这些都比较耗费CPU时间。

fork

由fork创建的新进程被称为子进程(child process)。该函数被调用一次,但返回两次。两次返回的区别是子进程的返回值是0,而父进程的返回值则是新进程(子进程)的进程 id。将子进程id返回给父进程的理由是:因为一个进程的子进程可以多于一个,没有一个函数使一个进程可以获得其所有子进程的进程id。对子进程来说,之所以fork返回0给它,是因为它随时可以调用getpid()来获取自己的pid;也可以调用getppid()来获取父进程的id。(进程id 0总是由交换进程使用,所以一个子进程的进程id不可能为0 )。

fork之后,操作系统会复制一个与父进程完全相同的子进程,虽说是父子关系,但是在操作系统看来,他们更像兄弟关系,这2个进程共享代码空间,但是数据空间是互相独立的,子进程数据空间中的内容是父进程的完整拷贝,指令指针也完全相同,子进程拥有父进程当前运行到的位置(两进程的程序计数器pc值相同,也就是说,子进程是从fork返回处开始执行的),但有一点不同,如果fork成功,子进程中fork的返回值是0,父进程中fork的返回值是子进程的进程号,如果fork不成功,父进程会返回错误。

可以这样想象,2个进程一直同时运行,而且步调一致,在fork之后,他们分别作不同的工作,也就是分岔了。这也是fork为什么叫fork的原因

至于那一个最先运行,可能与操作系统(调度算法)有关,而且这个问题在实际应用中并不重要,如果需要父子进程协同,可以通过原语的办法解决。

进程通讯方式

管道/匿名管道(pipe)

  • 管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道。
  • 只能用于父子进程或者兄弟进程之间(具有亲缘关系的进程);
  • 单独构成一种独立的文件系统:管道对于管道两端的进程而言,就是一个文件,但它不是普通的文件,它不属于某种文件系统,而是自立门户,单独构成一种文件系统,并且只存在与内存中。
  • 数据的读出和写入:一个进程向管道中写的内容被管道另一端的进程读出。写入的内容每次都添加在管道缓冲区的末尾,并且每次都是从缓冲区的头部读出数据。

672e7e41631b3f4aafbd98a709861169.png

管道的实质:

管道的实质是一个内核缓冲区,进程以先进先出的方式从缓冲区存取数据,管道一端的进程顺序的将数据写入缓冲区,另一端的进程则顺序的读出数据。

该缓冲区可以看做是一个循环队列,读和写的位置都是自动增长的,不能随意改变,一个数据只能被读一次,读出来以后在缓冲区就不复存在了。

当缓冲区读空或者写满时,有一定的规则控制相应的读进程或者写进程进入等待队列,当空的缓冲区有新数据写入或者满的缓冲区有数据读出来时,就唤醒等待队列中的进程继续读写。

管道的局限:

管道的主要局限性正体现在它的特点上:

  • 只支持单向数据流;
  • 只能用于具有亲缘关系的进程之间;
  • 没有名字;
  • 管道的缓冲区是有限的(管道制存在于内存中,在管道创建时,为缓冲区分配一个页面大小);
  • 管道所传送的是无格式字节流,这就要求管道的读出方和写入方必须事先约定好数据的格式,比如多少字节算作一个消息(或命令、或记录)等等;

有名管道(FIFO)

匿名管道,由于没有名字,只能用于亲缘关系的进程间通信。为了克服这个缺点,提出了有名管道(FIFO)。

有名管道不同于匿名管道之处在于它提供了一个路径名与之关联,以有名管道的文件形式存在于文件系统中,这样,即使与有名管道的创建进程不存在亲缘关系的进程,只要可以访问该路径,就能够彼此通过有名管道相互通信,因此,通过有名管道不相关的进程也能交换数据。值的注意的是,有名管道严格遵循先进先出(first in first out),对匿名管道及有名管道的读总是从开始处返回数据,对它们的写则把数据添加到末尾。它们不支持诸如lseek()等文件定位操作。有名管道的名字存在于文件系统中,内容存放在内存中。

匿名管道和有名管道总结:

  1. 管道是特殊类型的文件,在满足先入先出的原则条件下可以进行读写,但不能进行定位读写。
  2. 匿名管道是单向的,只能在有亲缘关系的进程间通信;有名管道以磁盘文件的方式存在,可以实现本机任意两个进程通信。
  3. 无名管道阻塞问题:无名管道无需显示打开,创建时直接返回文件描述符,在读写时需要确定对方的存在,否则将退出。如果当前进程向无名管道的一端写数据,必须确定另一端有某一进程。如果写入无名管道的数据超过其最大值,写操作将阻塞,如果管道中没有数据,读操作将阻塞,如果管道发现另一端断开,将自动退出。
  4. 有名管道阻塞问题:有名管道在打开时需要确实对方的存在,否则将阻塞。即以读方式打开某管道,在此之前必须一个进程以写方式打开管道,否则阻塞。此外,可以以读写(O_RDWR)模式打开有名管道,即当前进程读,当前进程写,不会阻塞。

信号(Signal)

  • 信号是Linux系统中用于进程间互相通信或者操作的一种机制,信号可以在任何时候发给某一进程,而无需知道该进程的状态。
  • 如果该进程当前并未处于执行状态,则该信号就有内核保存起来,知道该进程回复执行并传递给它为止。
  • 如果一个信号被进程设置为阻塞,则该信号的传递被延迟,直到其阻塞被取消是才被传递给进程。

Linux系统中常用信号:

  • SIGHUP:用户从终端注销,所有已启动进程都将收到该进程。系统缺省状态下对该信号的处理是终止进程。
  • SIGINT:程序终止信号。程序运行过程中,按Ctrl+C键将产生该信号。
  • SIGQUIT:程序退出信号。程序运行过程中,按Ctrl+\键将产生该信号。
  • SIGBUS和SIGSEGV:进程访问非法地址。
  • SIGFPE:运算中出现致命错误,如除零操作、数据溢出等。
  • SIGKILL:用户终止进程执行信号。shell下执行kill -9发送该信号。
  • SIGTERM:结束进程信号。shell下执行kill 进程pid发送该信号。
  • SIGALRM:定时器信号。
  • SIGCLD:子进程退出信号。如果其父进程没有忽略该信号也没有处理该信号,则子进程退出后将形成僵尸进程。

信号来源

信号是软件层次上对中断机制的一种模拟,是一种异步通信方式,,信号可以在用户空间进程和内核之间直接交互,内核可以利用信号来通知用户空间的进程发生了哪些系统事件,信号事件主要有两个来源:

  • 硬件来源:用户按键输入Ctrl+C退出、硬件异常如无效的存储访问等。
  • 软件终止:终止进程信号、其他进程调用kill函数、软件异常产生信号。

信号生命周期和处理流程

  1. 信号被某个进程产生,并设置此信号传递的对象(一般为对应进程的pid),然后传递给操作系统;
  2. 操作系统根据接收进程的设置(是否阻塞)而选择性的发送给接收者,如果接收者阻塞该信号(且该信号是可以阻塞的),操作系统将暂时保留该信号,而不传递,直到该进程解除了对此信号的阻塞(如果对应进程已经退出,则丢弃此信号),如果对应进程没有阻塞,操作系统将传递此信号。
  3. 目的进程接收到此信号后,将根据当前进程对此信号设置的预处理方式,暂时终止当前代码的执行,保护上下文(主要包括临时寄存器数据,当前程序位置以及当前CPU的状态)、转而执行中断服务程序,执行完成后在回复到中断的位置。当然,对于抢占式内核,在中断返回时还将引发新的调度。
    be72227e3dbe8f5eeebe997c2775e2ba.png

消息(Message)队列

  • 消息队列是存放在内核中的消息链表,每个消息队列由消息队列标识符表示。
  • 与管道(无名管道:只存在于内存中的文件;命名管道:存在于实际的磁盘介质或者文件系统)不同的是消息队列存放在内核中,只有在内核重启(即,操作系统重启)或者显示地删除一个消息队列时,该消息队列才会被真正的删除。
  • 另外与管道不同的是,消息队列在某个进程往一个队列写入消息之前,并不需要另外某个进程在该队列上等待消息的到达。

消息队列特点总结:

  1. 消息队列是消息的链表,具有特定的格式,存放在内存中并由消息队列标识符标识.
  2. 消息队列允许一个或多个进程向它写入与读取消息.
  3. 管道和消息队列的通信数据都是先进先出的原则。
  4. 消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取.比FIFO更有优势。
  5. 消息队列克服了信号承载信息量少,管道只能承载无格式字 节流以及缓冲区大小受限等缺。
  6. 目前主要有两种类型的消息队列:POSIX消息队列以及System V消息队列,系统V消息队列目前被大量使用。系统V消息队列是随内核持续的,只有在内核重起或者人工删除时,该消息队列才会被删除。

共享内存(share memory)

  • 使得多个进程可以可以直接读写同一块内存空间,是最快的可用IPC形式。是针对其他通信机制运行效率较低而设计的。
  • 为了在多个进程间交换信息,内核专门留出了一块内存区,可以由需要访问的进程将其映射到自己的私有地址空间。进程就可以直接读写这一块内存而不需要进行数据的拷贝,从而大大提高效率。
  • 由于多个进程共享一段内存,因此需要依靠某种同步机制(如信号量)来达到进程间的同步及互斥。
    %!(EXTRA markdown.ResourceType=, string=, string=)

信号量(semaphore)

信号量是一个计数器,用于多进程对共享数据的访问,信号量的意图在于进程间同步。

为了获得共享资源,进程需要执行下列操作:

  1. 创建一个信号量:这要求调用者指定初始值,对于二值信号量来说,它通常是1,也可是0。
  2. 等待一个信号量:该操作会测试这个信号量的值,如果小于0,就阻塞。也称为P操作。
  3. 挂出一个信号量:该操作将信号量的值加1,也称为V操作。

为了正确地实现信号量,信号量值的测试及减1操作应当是原子操作。为此,信号量通常是在内核中实现的。Linux环境中,有三种类型:Posix(可移植性操作系统接口)有名信号量(使用Posix IPC名字标识)、Posix基于内存的信号量(存放在共享内存区中)、System V信号量(在内核中维护)。这三种信号量都可用于进程间或线程间的同步。

fb6aeefa2e3196f534e55dcf5f170bfe.png

信号量与普通整型变量的区别:

  1. 信号量是非负整型变量,除了初始化之外,它只能通过两个标准原子操作:wait(semap) , signal(semap) ; 来进行访问;
  2. 操作也被成为PV原语(P来源于荷兰语proberen”测试”,V来源于荷兰语verhogen”增加”,P表示通过的意思,V表示释放的意思),而普通整型变量则可以在任何语句块中被访问;

信号量与互斥量之间的区别:

  1. 互斥量用于线程的互斥,信号量用于线程的同步。这是互斥量和信号量的根本区别,也就是互斥和同步之间的区别。
    互斥:是指某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。但互斥无法限制访问者对资源的访问顺序,即访问是无序的。
    同步:是指在互斥的基础上(大多数情况),通过其它机制实现访问者对资源的有序访问。
    在大多数情况下,同步已经实现了互斥,特别是所有写入资源的情况必定是互斥的。少数情况是指可以允许多个访问者同时访问资源
  2. 互斥量值只能为0/1,信号量值可以为非负整数。
    也就是说,一个互斥量只能用于一个资源的互斥访问,它不能实现多个资源的多线程互斥问题。信号量可以实现多个同类资源的多线程互斥和同步。当信号量为单值信号量是,也可以完成一个资源的互斥访问。
  3. 互斥量的加锁和解锁必须由同一线程分别对应使用,信号量可以由一个线程释放,另一个线程得到。

套接字(socket)

套接字是一种通信机制,凭借这种机制,客户/服务器(即要进行通信的进程)系统的开发工作既可以在本地单机上进行,也可以跨网络进行。也就是说它可以让不在同一台计算机但通过网络连接计算机上的进程进行通信。

%!(EXTRA markdown.ResourceType=, string=, string=)

线程

用户线程

用户线程是完全建立在用户空间的线程库,用户线程的创建、调度、同步和销毁全又库函数在用户空间完成,不需要内核的帮助。因此这种线程是极其低消耗和高效的。

  • 处理器竞争:单纯的用户线程是建立在用户空间,其对内核是透明的,因此其所属进程单独参与处理器的竞争,而进程的所有线程参与竞争该进程的资源。
  • 使用资源:与所属进程共享进程地址空间和系统资源。
  • 调度:由在用户空间实现的线程库,在所属进程内进行调度

LWP虽然本质上属于用户线程,但LWP线程库是建立在内核之上的,LWP的许多操作都要进行系统调用,因此效率不高。而这里的用户线程指的是完全建立在用户空间的线程库,用户线程的建立,同步,销毁,调度完全在用户空间完成,不需要内核的帮助。因此这种线程的操作是极其快速的且低消耗的。

72b5300edbacc1be5f18b5248a12651a.png

上图是最初的一个用户线程模型,从中可以看出,进程中包含线程,用户线程在用户空间中实现,内核并没有直接对用户线程进程调度,内核的调度对象和传统进程一样,还是进程本身,内核并不知道用户线程的存在。

用户线程之间的调度由在用户空间实现的线程库实现。

这种模型对应着多对一线程模型,其缺点是一个用户线程如果阻塞在系统调用中,则整个进程都将会阻塞。

内核线程

内核线程就是内核的分身,一个分身可以处理一件特定事情。这在处理异步事件如异步IO时特别有用。内核线程的使用是廉价的,唯一使用的资源就是内核栈和上下文切换时保存寄存器的空间。支持多线程的内核叫做多线程内核(Multi-Threads kernel )。

内核线程只运行在内核态,不受用户态上下文的拖累。

  • 处理器竞争:可以在全系统范围内竞争处理器资源;
  • 使用资源:唯一使用的资源是内核栈和上下文切换时保持寄存器的空间
  • 调度:调度的开销可能和进程自身差不多昂贵
  • 同步效率:资源的同步和数据共享比整个进程的数据同步和共享要低一些。

轻量级进程LWP

轻量级进程(LWP)是建立在内核之上并由内核支持的用户线程,它是内核线程的高度抽象,每一个轻量级进程都与一个特定的内核线程关联。内核线程只能由内核管理并像普通进程一样被调度。

轻量级进程由clone()系统调用创建,参数是CLONE_VM,即与父进程是共享进程地址空间和系统资源。

与普通进程区别:LWP只有一个最小的执行上下文和调度程序所需的统计信息。

  • 处理器竞争:因与特定内核线程关联,因此可以在全系统范围内竞争处理器资源
  • 使用资源:与父进程共享进程地址空间
  • 调度:像普通进程一样调度

轻量级线程(LWP)是一种由内核支持的用户线程。它是基于内核线程的高级抽象,因此只有先支持内核线程,才能有LWP。每一个进程有一个或多个LWPs,每个LWP由一个内核线程支持。这种模型实际上就是一对一线程模型。在这种实现的操作系统中,LWP就是用户线程。

由于每个LWP都与一个特定的内核线程关联,因此每个LWP都是一个独立的线程调度单元。即使有一个LWP在系统调用中阻塞,也不会影响整个进程的执行。

轻量级进程具有局限性。

  • 首先,大多数LWP的操作,如建立、析构以及同步,都需要进行系统调用。系统调用的代价相对较高:需要在user mode和kernel mode中切换。
  • 其次,每个LWP都需要有一个内核线程支持,因此LWP要消耗内核资源(内核线程的栈空间)。因此一个系统不能支持大量的LWP。

4bd64f1b4c5cc9bc5fe4fbb8d931e8ad.png

线程和进程区别

我的理解是进程是指在系统中正在运行的一个应用程序;程序一旦运行就是进程,或者更专业化来说:进程是指程序执行时的一个实例。

线程是进程的一个实体。

进程——资源分配的最小单位,线程——程序执行的最小单位。

  1. 因为进程拥有独立的堆栈空间和数据段,所以每当启动一个新的进程必须分配给它独立的地址空间,建立众多的数据表来维护它的代码段、堆栈段和数据段,这对于多进程来说十分“奢侈”,系统开销比较大,而线程不一样,线程拥有独立的堆栈空间,但是共享数据段,它们彼此之间使用相同的地址空间,共享大部分数据,比进程更节俭,开销比较小,切换速度也比进程快,效率高,但是正由于进程之间独立的特点,使得进程安全性比较高,也因为进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。一个线程死掉就等于整个进程死掉。
  2. 体现在通信机制上面,正因为进程之间互不干扰,相互独立,进程的通信机制相对很复杂,譬如管道,信号,消息队列,共享内存,套接字等通信机制,而线程由于共享数据段所以通信机制很方便。
  3. 属于同一个进程的所有线程共享该进程的所有资源,包括文件描述符。而不同过的进程相互独立。
  4. 线程又称为轻量级进程,进程有进程控制块,线程有线程控制块;
  5. 线程必定也只能属于一个进程,而进程可以拥有多个线程而且至少拥有一个线程;
  6. 体现在程序结构上,举一个简明易懂的列子:当我们使用进程的时候,我们不自主的使用if else嵌套来判断pid,使得程序结构繁琐,但是当我们使用线程的时候,基本上可以甩掉它,当然程序内部执行功能单元需要使用的时候还是要使用,所以线程对程序结构的改善有很大帮助。

Hadoop

架构

HDFS

69cef71cd3a6500cce831d9103cea037.jpg

Blocks

物理磁盘中有块的概念,磁盘的物理Block是磁盘操作最小的单元,读写操作均以Block为最小单元,一般为512 Byte。文件系统在物理Block之上抽象了另一层概念,文件系统Block物理磁盘Block的整数倍。通常为几KB。Hadoop提供的df、fsck这类运维工具都是在文件系统的Block级别上进行操作。

HDFS的Block块比一般单机文件系统大得多,默认为128M。HDFS的文件被拆分成block-sized的chunk,chunk作为独立单元存储。比Block小的文件不会占用整个Block,只会占据实际大小。例如, 如果一个文件大小为1M,则在HDFS中只会占用1M的空间,而不是128M。

HDFS的Block为什么这么大?

是为了最小化查找(seek)时间,控制定位文件与传输文件所用的时间比例。假设定位到Block所需的时间为10ms,磁盘传输速度为100M/s。如果要将定位到Block所用时间占传输时间的比例控制1%,则Block大小需要约100M。

但是如果Block设置过大,在MapReduce任务中,Map或者Reduce任务的个数 如果小于集群机器数量,会使得作业运行效率很低。

Block抽象的好处

block的拆分使得单个文件大小可以大于整个磁盘的容量,构成文件的Block可以分布在整个集群, 理论上,单个文件可以占据集群中所有机器的磁盘。

Block的抽象也简化了存储系统,对于Block,无需关注其权限,所有者等内容(这些内容都在文件级别上进行控制)。

Block作为容错和高可用机制中的副本单元,即以Block为单位进行复制。

NameNode

NameNode也叫名称节点,它管理文件系统的命名空间,维护着文件系统树及整棵树内所有的文件和目录。这些信息以两个文件形式永久保存在本地磁盘上:

  • 命名空间镜像文件(namespcae image)
  • 编辑日志文件(edit log)

对整个分布式文件系统进行总控制,会纪录所有的元数据分布存储的状态信息。比如文件是如何分割成数据块的,以及这些数据块被存储到哪些节点上,还有对内存和I/O进行集中管理。但是持久化数据中不包括Block所在的节点列表,及文件的Block分布在集群中的哪些节点上,这些信息是在系统重启的时候重新构建(通过Datanode汇报的Block信息)。用户首先会访问Namenode,获取文件分布的状态信息,然后访问相应的节点,把文件拿到。

整个HDFS可存储的文件数受限于NameNode的内存大小。

不过这是个单点,如果NameNode宕机,那么整个集群就瘫痪了。

Secondary Namenode

Secondary NameNode,该部分主要是定时对NameNode数据进行备份,这样尽量降低NameNode崩溃之后,导致数据的丢失,其实所作的工作就是从NameNode获得fsimage(命名空间镜像文件)和edits(编辑日志文件)把二者重新合并然后发给NameNode,这样,既能减轻NameNode的负担又能保险地备份。

DataNode

数据节点,提供真实文件数据的存储服务。每台从服务器节点都运行一个,负责把HDFS数据块读、写到本地文件系统。

文件写入

67c3fe04d3b08c86544a99de14ed1c2e.png

  1. Client向NameNode发起文件写入的请求
  2. NameNode根据文件大小和文件块配置情况,返回给Client它所管理部分DataNode的信息。
  3. Client将文件划分为多个文件块,根据DataNode的地址信息,按顺序写入到每一个DataNode块中。

文件读取

deadf8e74fffebfdb743bfff497b6d2b.png

  1. Client向NameNode发起文件读取的请求
  2. NameNode返回文件存储的DataNode的信息。
  3. Client读取文件信息。

MapReduce

MapReduce是一种编程模型,用于大规模数据集的并行运算。Map(映射)和Reduce(化简),采用分而治之思想,先把任务分发到集群多个节点上,并行计算,然后再把计算结果合并,从而得到最终计算结果。多节点计算,所涉及的任务调度、负载均衡、容错处理等,都由MapReduce框架完成,不需要编程人员关心这些内容。

1.0

MapReduce采用Master/Slave结构。

  • Master:是整个集群的唯一的全局管理者,功能包括:作业管理、状态监控和任务调度等,即MapReduce中的JobTracker。
  • Slave:负责任务的执行和任务状态的回报,即MapReduce中的TaskTracker。

JobTracker

作业跟踪器,运行在主节点(Namenode)上的一个很重要的进程,是MapReduce体系的调度器。用于处理作业(用户提交的代码)的后台程序,决定有哪些文件参与作业的处理,然后把作业切割成为一个个的小task,并把它们分配到所需要的数据所在的子节点。(程序跟着数据跑,这样子就可以避免对数据进行重新分配)

TaskTracker

任务跟踪器,运行在每个slave节点上,与datanode结合(代码与数据一起的原则),管理各自节点上的task(由jobtracker分配),每个节点只有一个tasktracker,但一个tasktracker可以启动多个JVM,用于并行执行map或reduce任务,它与jobtracker交互通信,可以告知jobtracker子任务完成情况。

两者联系:JobTracker会给TaskTracker下达各种命令,主要包括:启动任务(LaunchTaskAction)、提交任务(CommitTaskAction)、杀死任务(KillTaskAction)、杀死作业(KillJobAction)和重新初始化(TaskTrackerReinitAction)。

2.0

Yarn

HA

MapReduce流程

bfa448bc34abbb54be7e829a46c8147d.png

Map阶段

  1. 读取输入文件的内容,并解析成键值对(<key, value>)的形式,输入文件中的每一行被解析成一个<key, value>对,每个<key, value>对调用一次map()函数。
  2. 用户写map()函数,对输入的<key,value>对进行处理,并输出新的<key,value>对。
    3。 对Step 2中得到的<key,value>进行分区操作。
  3. 不同分区的数据,按照key值进行排序和分组,具有相同key值的value则放到同一个集合中。
  4. 分组后的数据进行规约。

Shuffle

39fbc5dfa0947b5ef45d89e99a90e2ec.png

3b3ea4985b0374411973e1861f8c3f7d.jpg

f535cdbe61002ca7249595176d10342f.png

Map Shuffle

  1. 环形内存缓存区:每个split数据交由一个map任务处理,map的处理结果不会直接写到硬盘上,会先输送到环形内存缓存区中,默认的大小是100M(可通过配置修改),当缓冲区的内容达到80%后会开始溢出,此时缓存区的溢出内容会被写到磁盘上,形成一个个spill file,注意这个文件没有固定大小。
  2. 在内存中经过分区、排序后溢出到磁盘:分区主要功能是用来指定 map 的输出结果交给哪个 reduce 任务处理,默认是通过 map 输出结果的 key 值取hashcode 对代码中配置的 redue task数量取模运算,值一样的分到一个区,也就是一个 reduce 任务对应一个分区的数据。这样做的好处就是可以避免有的 reduce 任务分配到大量的数据,而有的 reduce 任务只分配到少量甚至没有数据,平均 reduce 的处理能力。并且在每一个分区(partition)中,都会有一个 sort by key 排序,如果此时设置了 Combiner,将排序后的结果进行 Combine 操作,相当于 map 阶段的本地 reduce,这样做的目的是让尽可能少的数据写入到磁盘。
  3. 合并溢出文件:随着 map 任务的执行,不断溢出文件,直到输出最后一个记录,可能会产生大量的溢出文件,这时需要对这些大量的溢出文件进行合并,在合并文件的过程中会不断的进行排序跟 Combine 操作,这样做有两个好处:减少每次写入磁盘的数据量&减少下一步 reduce 阶段网络传输的数据量。最后合并成了一个分区且排序的大文件,此时可以再进行配置压缩处理,可以减少不同节点间的网络传输量。合并完成后着手将数据拷贝给相对应的reduce 处理,那么要怎么找到分区数据对应的那个 reduce 任务呢?简单来说就是 JobTracker 中保存了整个集群中的宏观信息,只要 reduce 任务向 JobTracker 获取对应的 map 输出位置就可以了。具体请参考上方的MapReduce工作原理。

Reduce Shuffle

reduce 会接收到不同 map 任务传来的有序数据,如果 reduce 接收到的数据较小,则会存在内存缓冲区中,直到数据量达到该缓存区的一定比例时对数据进行合并后溢写到磁盘上。随着溢写的文件越来越多,后台的线程会将他们合并成一个更大的有序的文件,可以为后面合并节省时间。这其实跟 map端的操作一样,都是反复的进行排序、合并,这也是 Hadoop 的灵魂所在,但是如果在 map 已经压缩过,在合并排序之前要先进行解压缩。合并的过程会产生很多中间文件,但是最后一个合并的结果是不需要写到磁盘上,而是可以直接输入到 reduce 函数中计算,每个 reduce 对应一个输出结果文件。

Reduce阶段

  1. 对于多个map任务的输出,按照不同的分区,通过网络传输到不同的Reduce节点。
  2. 对多个map任务的输出结果进行合并、排序,用户书写reduce函数,对输入的key、value进行处理,得到新的key、value输出结果。
  3. 将reduce的输出结果保存在文件中。

例子

22c99cc806e317ad2fd48ea3effdf8a6.png